Repository navigation
Commit f79fa74
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
Fixes #26710
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: updated here
(`skills/objectstack-automation/references/integration-recipes.md`;
`grep -rln "tencentcloud-tc3\|aliyun-acs3\|alipay" skills` hits that one
file)
## 维护者速读
- **改了什么**:连接器的 `signed` 签名认证新增第三个方案 `alipay-v3`。市场里对接支付宝开放平台 v3
的连接器,现在每个请求都会用应用私钥签名,每个响应都会用支付宝公钥验签;验签不过的响应直接让调用失败,内容不会进入流程。
- **为什么改**:#26710 的最后一片(S1 阿里云、S2 腾讯云已合并)。支付宝 v3 是目前找到的、国内平台里唯一官方发布
OpenAPI 文档的,有了它,中国区市场的支付宝连接器才能以纯元数据声明出来。
- **风险与回滚**:只新增一个方案,已有的 `bearer` / `api-key` / `basic` / `oauth2` / 阿里云
/ 腾讯云实例一行不变;回滚即整 PR 回退。本 PR 含 ADR-0143 D2.3 的一条日期注记(向量规则第三分支、验签范围),所以是
Tier H,需要人工批准。
- **先看**:①
`packages/services/service-automation/src/request-signing/alipay-v3.ts`(签名与验签,`alipayV3ResponseMustVerify`
一行定验签范围);② `docs/adr/0143-outbound-connector-auth-is-open-tier.md` 的两条
2026-10-31 注记;③
`packages/connectors/connector-openapi/src/alipay-v3-acceptance.test.ts`(验收)。
- **两分钟验证**:`pnpm --filter @objectstack/connector-openapi exec vitest
run src/alipay-v3-acceptance.test.ts`,预期 3
个用例全绿:签名请求被本地支付宝网关接受、篡改或未签名的响应让调用失败、私钥不出现在任何请求与描述里。
- **需要你做的**:批准 ADR-0143 的两条 2026-10-31 注记(Tier H 的人工批准),契约复审与落地由席位完成。
## What changes
`alipay-v3` joins the `signed` scheme enum (ADR-0143 Q4, condition 1),
with its signer, its response verifier and vectors two official SDKs
agree on. This is slice 3 of 3; S1 (#28273, `aliyun-acs3-hmac-sha256`)
and S2 (#28279, `tencentcloud-tc3-hmac-sha256`) are merged.
- **`@objectstack/spec`**: `ConnectorSigningSchemeSchema` gains
`alipay-v3`; the `signed` union gains
`ConnectorInstanceSignedAlipayV3AuthSchema`, `{ type: 'signed', scheme:
'alipay-v3', keyId, credentialRef, vendorPublicKeyRef }`. `keyId` is the
app id, `credentialRef` the application private key,
`vendorPublicKeyRef` (required) the Alipay public key. The S1 publish
refusals apply (inline key spellings by name, an authored `signature`,
`signed` on `provider: 'mcp'`), plus `appPrivateKey` by name and
`alipayPublicKey` pointed at `vendorPublicKeyRef`; `appId` is answered
with `keyId`. The unknown-scheme refusal lists all three schemes.
- **`@objectstack/service-automation`**: `request-signing/alipay-v3.ts`
signs every attempt `ALIPAY-SHA256withRSA` with a fresh nonce and
millisecond timestamp, and verifies responses with the Alipay public key
before any body is parsed. The signer registry takes `{ key,
vendorPublicKey? }` instead of the bare key; its switch stays
exhaustive, so a scheme without a signer does not compile. The host
resolves `vendorPublicKeyRef` through the same chain as `credentialRef`:
an unresolved public key degrades the instance naming that reference, a
key that is not RSA fails boot naming which reference (never the value),
and the credential door lists both references against the instance, so
setting or replacing either one re-materializes it.
- **Docs**: `content/docs/automation/connectors.mdx` documents the
scheme, its two references and the boot table rows. It also fixes one S2
sentence: Tencent's default `Content-Type` applies to a request "without
a content type", not "without a body" (the signer keys on the header, as
its own docblock says).
- **ADR-0143** (Tier H): one dated note under D2.3 (the third branch of
the vector rule, the response-verification rule) and one under
Acceptance (the local end-to-end test counts, per the ruling quoted
below).
### The response-verification rule: a seat call, named
The two official SDKs disagree on which responses carry a signature that
must verify. Java v3 3.1.89 verifies a 2xx unless its body is empty and
verifies an error response only when it is signed; Node `alipay-sdk`
4.14.0 verifies every response below 400, an empty body included, and
never verifies an error response. The seat chose the stricter rule in
each case:
- every response **below 400** must carry a valid signature, **an empty
body included**;
- a **signed** error response (400 and above) is verified;
- an **unsigned** error response passes through as the error it already
is.
The rule is one function, `alipayV3ResponseMustVerify`, so a different
ruling changes one line. The ADR note records it.
### Rulings this rests on (binding, quoted)
- Vectors, option B: the maintainer, 2026-10-31, 「其他同意」. The vectors
come from `com.alipay.sdk:alipay-sdk-java-v3:3.1.89.ALL` and
`alipay-sdk` 4.14.0 (Node) over one placeholder key that belongs to no
app; the two outputs are byte-identical; the generator is kept in
`packages/services/service-automation/test-vectors/alipay-v3/`.
- Acceptance: the maintainer, 2026-10-31, 「算」: the in-repo end-to-end
test against a local vendor-shaped server counts, and no live sandbox
call is owed. That is why this PR carries the closing line above.
### Flags for the contract review
- **PD#2 (no business logic in `packages/spec`).** Response verification
runs through `readConnectorResponse` and the per-attempt signing wrap in
`packages/spec/src/integration/connector-fetch-policy.ts`, which landed
with S1. This slice adds no logic there, but it is the first scheme
whose `verifyResponse` is non-empty, so it is the first that relies on
that runtime code living in the spec package.
- **ADR-0143 D4 (grant-bearing kinds resolve per dispatch).** D4 names
"a signer bound to its key for `signed`" as the per-dispatch answer. As
in S1 and S2, the signer is built at materialization, and the key's
fingerprint joins the instance signature, so a rotated key
re-materializes instead of being re-read per dispatch. This slice
extends the same binding to `vendorPublicKeyRef` (the credential-door
test pins the re-materialization on replace).
- **Tier H.** The ADR-0143 notes make this PR Tier H.
**Independent review: none run (quota); the PR waits for the seat's
contract-review hand-off.**
## Tests
- **Vectors** (`request-signing/alipay-v3.test.ts`): the POST (JSON
body) and GET (query, no body) `Authorization` headers and one response
signature, as both SDKs produced them, reproduced byte for byte; a wrong
key differs; tampered body, tampered nonce and missing signature on a
2xx refused; unsigned empty 2xx refused; signed error verified, unsigned
error passed; refusals never carry the body, the signature or a key;
fresh nonce and instant per attempt; bare-base64 and PEM keys read;
non-RSA keys refused without echo.
- **Host** (`connector-materialization.test.ts`,
`connector-credential-door.test.ts`): signer bound to both keys and
neither key in the provider context; public key unresolved degrades
naming it; a non-key public key fails boot without echo; the door lists
both references, and setting or replacing the public key
re-materializes.
- **Spec** (`connector-auth-signed.test.ts`,
`type-alias-convention.pin.test.ts`): the shape on `rest` and `openapi`,
the refusals above, `vendorPublicKeyRef` required on `alipay-v3` and
refused on the other schemes.
- **Acceptance** (`connector-openapi/src/alipay-v3-acceptance.test.ts`):
a real boot (LiteKernel, AutomationServicePlugin,
ConnectorOpenApiPlugin) with an `openapi` instance over the vendor's
`alipay.trade.query` operation, dispatched through a flow
`connector_action` to a local gateway stand-in that checks the request
signature and signs its answer; a tampered or unsigned answer fails the
dispatch and its body never reaches the flow; the private key appears in
no request and no connector descriptor.
## Acceptance notes
- Public-key mode only; Alipay's certificate mode (`app_cert_sn`) is not
implemented, and the docs say so.
- The placeholder RSA pair under `test-vectors/alipay-v3/` is named
`PLACEHOLDER-test-only-…` and belongs to no Alipay app.
- `skills/objectstack-automation/references/integration-recipes.md` is
corrected here (commits `10c6ddf7b3`, `1083e85431`; the seat amended the
claim surface on #26710): the `signed` scheme list now names
`alipay-v3`, and the "RSA-signing vendor … blocked on the
request-signing card" line now says only WeChat Pay has no scheme yet.
The earlier plan to carry this on #28226 did not hold: #28226 is closed
and never added the Alipay text. `skills/**` is a second Tier H surface
on a PR that is already Tier H.
## Verification
All readings below are on head `7497850ebb`, except the skills fix on
`1083e85431`: its six derived skills gates (`check-skills-token-ratchet`
and its self-test, `check:agent-test-spelling`,
`check:pm-skill-id-lint`, `check:skill-compatibility`,
`check:skill-frame-sync`) exit 0, `--ran` 116 of 116; the file stays
under its token ceiling (2235 of 2236). The rest of this section reads
(the branch after merging `origin/main` at `aaa5c1df8d`; the merge
regenerated `content/docs/references/index.mdx` and `check:generated` is
green on all 14 artifacts).
<details><summary>Gates: 110 derived, 110 run, all exit 0; `--ran` 0
NOT-MEASURED; `pnpm lint` exit 0</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 110 commands; each was run with its
exit code captured before any pipe. 109 exited 0 on the first run. `pnpm
check:type-check-debt` first exited 3 (PREREQUISITE NOT MET: its closure
build never got a turn on the shared verify lock), and exited 0 when
re-run inside the lock (0 ledger entries above their recorded number).
- `dispatch-gates --ran` over the `command :: exit code` record: 110
derived, 110 run, 0 NOT-MEASURED (derived from the recorded codes).
- `pnpm lint`: exit 0.
- `pnpm --filter @objectstack/spec check:generated`: all 14 generated
artifacts up to date.
</details>
<details><summary>Tests and typecheck</summary>
- `pnpm typecheck` for `@objectstack/spec`,
`@objectstack/service-automation`, `@objectstack/connector-openapi`,
`@objectstack/connector-rest`: exit 0.
- `@objectstack/service-automation`: 245 files, 3126 tests passed.
- `@objectstack/connector-openapi`: 8 files, 92 tests passed (includes
the acceptance test).
- `@objectstack/connector-rest`: 5 files, 43 tests passed.
- `@objectstack/spec` (`src/integration`, `src/shared`,
`src/type-alias-convention.pin.test.ts`): 50 files, 1170 tests passed.
The full spec suite is left to CI.
</details>
<details><summary>Ablations: each mutation landed on disk, went red, and
was restored to the HEAD blob</summary>
Run with `scripts/ablation-replace.mjs` (anchor hit 1 to 0, blob
changed, restore proven against the HEAD blob; `git status --porcelain`
empty afterwards). The subjects are reached through source (a relative
import, or the `@objectstack/service-automation` source alias in
`connector-openapi`'s vitest config), so no build is involved.
1. **Drop the response-signature check** (`verifyResponse` in
`alipay-v3.ts` made a no-op): `alipay-v3.test.ts` 4 red (tampered body /
nonce / missing signature on a 2xx; unsigned empty 2xx; signed error;
refusal text), and the acceptance test's case 2 (tampered or unsigned
answer fails the dispatch) red.
2. **Swap one byte of the POST vector** (`sign=F…` to `sign=G…` in the
pinned `Authorization`): 5 red, led by "reproduces the POST (JSON body)
request signature".
3. **Drop `alipay-v3` from the signer switch**
(`request-signing/index.ts`): `service-automation` typecheck exit 2,
`TS2322` naming the `alipay-v3` shape as not assignable to `never`.
</details>
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 119931d commit f79fa74
32 files changed
Lines changed: 1329 additions & 61 deletions
File tree
- .changeset
- content/docs
- automation
- references
- integration
- docs
- adr
- audits/2026-07-unknown-key-strictness-ledger.counts
- packages
- connectors/connector-openapi/src
- services/service-automation
- src
- request-signing
- test-vectors/alipay-v3
- spec
- api-surface
- authorable-surface
- declaration-map
- export-origins
- json-schema.manifest
- src
- integration
- skills/objectstack-automation/references
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
617 | 617 | | |
618 | 618 | | |
619 | 619 | | |
620 | | - | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
621 | 623 | | |
622 | 624 | | |
623 | 625 | | |
| |||
635 | 637 | | |
636 | 638 | | |
637 | 639 | | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
638 | 649 | | |
639 | 650 | | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
640 | 656 | | |
641 | 657 | | |
642 | 658 | | |
| |||
648 | 664 | | |
649 | 665 | | |
650 | 666 | | |
651 | | - | |
| 667 | + | |
| 668 | + | |
652 | 669 | | |
653 | 670 | | |
654 | 671 | | |
| |||
659 | 676 | | |
660 | 677 | | |
661 | 678 | | |
662 | | - | |
663 | | - | |
664 | | - | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
665 | 689 | | |
666 | 690 | | |
667 | 691 | | |
| |||
670 | 694 | | |
671 | 695 | | |
672 | 696 | | |
673 | | - | |
674 | | - | |
675 | | - | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
676 | 702 | | |
677 | 703 | | |
678 | 704 | | |
| |||
736 | 762 | | |
737 | 763 | | |
738 | 764 | | |
739 | | - | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
740 | 768 | | |
741 | 769 | | |
742 | 770 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | | - | |
| 28 | + | |
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| |||
192 | 192 | | |
193 | 193 | | |
194 | 194 | | |
195 | | - | |
| 195 | + | |
196 | 196 | | |
197 | 197 | | |
198 | 198 | | |
199 | 199 | | |
200 | 200 | | |
201 | | - | |
| 201 | + | |
202 | 202 | | |
203 | 203 | | |
204 | 204 | | |
| |||
0 commit comments