Skip to content

feat(spec,service-automation): alipay-v3 joins the signed connector auth schemes, with response verification (ADR-0143 D2.3, slice 3 of #26710) - #28446

Merged
objectstack-fleet[bot] merged 13 commits into
mainfrom
claude/issue-26710-alipay-v3-signing
Oct 31, 2026
Merged

objectstack-fleet[bot] merged 13 commits into
mainfrom
claude/issue-26710-alipay-v3-signing

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

🤖 Generated with 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 的连接器,现在每个请求都会用应用私钥签名,每个响应都会用支付宝公钥验签;验签不过的响应直接让调用失败,内容不会进入流程。
  • 为什么改:connectors: request-signing auth kind for the openapi and rest providers (RSA2 / HMAC canonical-request signers: Alipay, WeChat Pay, Aliyun, Tencent Cloud, e签宝) — the vendor-auth half that #26702 does not cover #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

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).

Gates: 110 derived, 110 run, all exit 0; `--ran` 0 NOT-MEASURED; `pnpm lint` exit 0
  • 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.
Tests and typecheck
  • 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.
Ablations: each mutation landed on disk, went red, and was restored to the HEAD blob

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.

…schemes; only WeChat Pay stays unsigned

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 31, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

维护者速读(席位终稿 · head 2686771a)

  • 改了什么: 连接器的“请求签名”认证新增 alipay-v3 方案。接支付宝开放平台 v3 的连接器,每个请求都用应用私钥签名;支付宝的每个回包也会用支付宝公钥验签,验不过的回包直接判失败,内容不会进流程。私钥和支付宝公钥都只放在密钥层,配置里只写引用。
  • 为什么改: 应用市场的中国区要接支付、云和电子签平台,这些平台的接口不用 OAuth,而是逐个请求签名。阿里云、腾讯云两种方案已经上线(feat(spec): signed connector instance auth, with aliyun-acs3-hmac-sha256 signing every request (ADR-0143 D2.3, slice 1 of #26710) #28273、feat(spec): tencentcloud-tc3-hmac-sha256 joins the signed connector auth schemes (ADR-0143 D2.3, slice 2 of #26710) #28279),这是第三种,也是第一种带回包验签的。
  • 风险与回滚: 只新增一个方案,已有的连接器配置一行不变。回包验签的规则是席位定的:状态码低于 400 的回包必须带有效签名,空回包也一样;出错的回包带了签名就验,没带就照常按错误处理(两个官方 SDK 在这点上做法不同,取了更严的一边)。回退本 PR 即可。
  • 先看: packages/services/service-automation/src/request-signing/alipay-v3.ts(签名和验签);docs/adr/0143-outbound-connector-auth-is-open-tier.md 新增的两条 2026-10-31 注记。
  • 两分钟验证: pnpm --filter @objectstack/connector-openapi exec vitest run src/alipay-v3-acceptance.test.ts,应全部通过(本地模拟网关,验请求签名、签回包、拒篡改)。
  • 需要你做的: 本 PR 改了 ADR-0143 和一份技能文档,属于 Tier H,需要 os-zhuang 或 hotlong 批准。契约复审 PASS 后,席位会请求批准。

席位意见: ACCEPT(车道部分)。向量走裁定的 B 方案(Java v3 和 Node 两个官方 SDK 输出逐字节一致,生成脚本在仓库里);验收按「算」的裁定,用仓库内的端到端测试。回包验签规则是一个函数(alipayV3ResponseMustVerify),以后改裁定只动一行。

席位核对 · ACCEPT (lane part) · card #26710 slice 3 · 2026-10-31T17:04Z

  1. 形态: 32 files, +1329 / −61 at head 2686771a (resume head e8422d9e0f, two clean merges of main by the dev, the skills fix 10c6ddf7b3 + 1083e85431, and this seat's merge of main f91ab5a08a with the references index regenerated), draft, Fixes #26710 once, Clause-②: yes (widening) / Metadata-protocol: added / Skills-docs: updated here, changeset .changeset/26710-signed-connector-auth-alipay.md (@objectstack/spec + @objectstack/service-automation minor). check-governed-merges.mjs --pr 28446: GOVERNED (docs/adr/** ×1, skills/** ×1), Tier H.
  2. 前提实测: holds. On main, ConnectorSigningSchemeSchema was aliyun-acs3-hmac-sha256 | tencentcloud-tc3-hmac-sha256 (S1 feat(spec): signed connector instance auth, with aliyun-acs3-hmac-sha256 signing every request (ADR-0143 D2.3, slice 1 of #26710) #28273, S2 feat(spec): tencentcloud-tc3-hmac-sha256 joins the signed connector auth schemes (ADR-0143 D2.3, slice 2 of #26710) #28279). The head adds alipay-v3 to the enum and one strict shape to the signed union (keyId = app id, credentialRef = application private key, vendorPublicKeyRef = Alipay public key; appPrivateKey and alipayPublicKey are refused with guidance toward the secrets layer). The seat read the spec and ADR hunks in full.
  3. 门禁 check-runs: on 2686771a CI has just started; the seat re-reads every check before any landing stroke. Dev: 116 gate commands, all exit 0 (check:type-check-debt first exited 3 on the shared build lock, then 0); --ran 116 of 116; pnpm lint 0. Ablations: response-signature check off → 4 unit reds + the acceptance test's tampered-response case red; one vector byte swapped → 5 reds; alipay-v3 dropped from the signer switch → TS2322 "not assignable to never". service-automation 3126, connector-openapi 92, connector-rest 43, spec src/integration + src/shared + the type-alias pin 1170. os-dev-report 6546335469. After this seat's merge: check:generated --fix, check:liveness, check:authorable-surface exit 0.
  4. 接受的偏差:
    • The response-verification rule is a seat call, named in the PR body and in the ADR note: below 400 must verify (empty body included); a signed error is verified; an unsigned error passes through as the error.
    • PD#2. Verification runs through readConnectorResponse and the per-attempt signing wrap in packages/spec/src/integration/connector-fetch-policy.ts, landed with S1. This slice adds no logic there but is the first scheme that relies on it. For the contract reviewer to confirm this stays S1's accepted placement.
    • ADR-0143 D4. D4 names "a signer bound to its key" as the per-dispatch answer; as in S1 and S2, the signer is built at materialization, the key fingerprint joins the instance signature, and a rotated key re-materializes. This slice extends the same binding to vendorPublicKeyRef.
    • Describe wording. The ConnectorInstanceSignedAlipayV3AuthSchema TSDoc says "every response is checked … before its body is read"; the exact rule (below 400, or a signed error) is in the ADR note and the signer. For the contract reviewer to judge whether that TSDoc must be narrowed now.
    • Surface amended by this seat (6546348598): skills/objectstack-automation/references/integration-recipes.md, the two lines this PR made false. It adds a second Tier H surface on a PR already Tier H.
  5. 范围外去向: WeChat Pay has no signed scheme yet (wechatpay-rsa2048 is still refused as unknown, pinned by a spec test); not filed here, since the card's scope is the three ADR-0143 Q4 members. Alipay certificate mode (app_cert_sn) is not implemented, and the docs say so. Skills-docs: updated here.
  6. 落地条款: Tier H. Owed before pr_ready: green CI on the landing head; a same-shape CONTRACT_REVIEW_TIER PASS (packages/spec/src/** non-test, the ADR, the skills hunk); then an APPROVED review from os-zhuang or hotlong (GOVERNED_APPROVERS), requested by this seat after the PASS. needs-user-decision stays on until that approval. This seat cannot serve the review tier now and hands it over below.

Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (HTTP 429 "You've reached your Fable limit", 2026-10-30T04:26Z) · head 2686771

@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-automation, @objectstack/spec, touching 33 documentable anchor(s). ⚠️ 12 changed file(s) yielded no anchor (packages/services/service-automation/test-vectors/alipay-v3/GenerateAlipayV3Vectors.java, packages/services/service-automation/test-vectors/alipay-v3/PLACEHOLDER-test-only-rsa2048-private-key.pkcs8.b64, packages/services/service-automation/test-vectors/alipay-v3/PLACEHOLDER-test-only-rsa2048-public-key.spki.b64, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 9b1dc992c7cdfffd2d4f20581fc817f18981601c.

⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 12 changed file(s) yielded no anchor (packages/services/service-automation/test-vectors/alipay-v3/GenerateAlipayV3Vectors.java, packages/services/service-automation/test-vectors/alipay-v3/PLACEHOLDER-test-only-rsa2048-private-key.pkcs8.b64, packages/services/service-automation/test-vectors/alipay-v3/PLACEHOLDER-test-only-rsa2048-public-key.spki.b64, …) — pages documenting those are invisible to this run
  • 12 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 212 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 168 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 9b1dc992c7cdfffd2d4f20581fc817f18981601c → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 612b9d383c37b1bd72e6b8d4f758a37e84f092ec — the merge of head 7420bc20c9de006608ac33ecb12a149b4704ad5c into base 9b1dc992c7cdfffd2d4f20581fc817f18981601c, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 612b9d383c37b1bd72e6b8d4f758a37e84f092ec && git checkout 612b9d383c37b1bd72e6b8d4f758a37e84f092ec
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9b1dc992c7cdfffd2d4f20581fc817f18981601c 7420bc20c9de006608ac33ecb12a149b4704ad5c && git checkout -B drift-repro 9b1dc992c7cdfffd2d4f20581fc817f18981601c && git merge --no-ff 7420bc20c9de006608ac33ecb12a149b4704ad5c

node scripts/docs-audit/affected-docs.mjs --json 9b1dc992c7cdfffd2d4f20581fc817f18981601c

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 9b1dc992c7cdfffd2d4f20581fc817f18981601c → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 2686771adc8afea173a408db70aad51304f29595 · 2026-10-31T17:15Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2686771adc8afea173a408db70aad51304f29595
Local-runs: none
Scope: packages/spec/src/shared/connector-auth.zod.ts (ConnectorSigningSchemeSchema + alipay-v3, ConnectorInstanceSignedAlipayV3AuthSchema with guidance/aliases, type), packages/spec/src/integration/connector.zod.ts re-exports, the regenerated integration surfaces + variant-docs + strictness ledger, packages/services/service-automation request-signing/alipay-v3.ts (new) + index.ts (ConnectorSigningKeys, exhaustive switch), plugin.ts (vendorPublicKeyRefOf, credentialRefsOf, resolution/degrade/signature/door listing), the test vectors folder (generator + placeholder keys), docs/adr/0143 (two dated notes), content/docs/automation/connectors.mdx + references, skills/objectstack-automation/references/integration-recipes.md (two lines), the changeset (@objectstack/spec + @objectstack/service-automation minor), 7 test files. 32 files, +1329/−61. Fixes #26710 slice 3 of 3 (S1 #28273, S2 #28279 merged); seat hand-off 6546016773 → 6546497764 with ACCEPT (lane part) and the triage CONTRACT_REVIEW_TIER hand-off; surface amendment 6546348598 (skills lines). Tier H: docs/adr/** ×1, skills/** ×1.
Inputs: the card's rulings as quoted in the ADR note and PR body (vectors option B 「其他同意」, acceptance 「算」, both 2026-10-31 per the seat), the seat's two hand-offs (six checks, three questions for the reviewer), the PR body, the PR diff at this head (spec, signer, registry, plugin, ADR, skills and changeset hunks read in full; tests by name and the ablation list), git grep/git show on origin/main and the fetched head (readConnectorResponse/verifyResponse placement in connector-fetch-policy.ts L88-207, the skills token ratchet row 8942 bytes → ceiling 2236 vs head 8938 bytes = 2235 tokens), check-runs (read 2026-10-31T17:42Z: all green — 39 success, 11 skipped, 0 failed, 0 in progress; latest run per check name) — 50 checks. Nothing built or run here; the seat's numbers (service-automation 3126, connector-openapi 92, spec 1170, 116 gates, three ablations) are the dev report's.

① Derived judgments

  • Surface widening, declared right. ConnectorSigningSchemeSchema gains the third literal; the signed union gains one strict shape { type: 'signed', scheme: 'alipay-v3', keyId, credentialRef, vendorPublicKeyRef } — all three strings min(1), vendorPublicKeyRef required here and refused on the other two schemes (spec test). Guidance refuses appPrivateKey (inline secret by name) and alipayPublicKey (points at vendorPublicKeyRef); appId aliases to keyId. Clause-②: yes (widening), Metadata-protocol: added, minor on both packages — the same declaration pair S1/S2 carried. ADR-0087 not-required: no existing key moves; the signer registry's third parameter changes from a string to { key, vendorPublicKey? } — a service-automation internal, not an authored surface.
  • Signing and verification are as the ADR note says. computeAlipayV3Signature builds app_id=…,nonce=…,timestamp=… + method + path?query + body, signs RSA-SHA256, emits authorization: ALIPAY-SHA256withRSA …,sign=…; fresh nonce and ms timestamp per attempt through the injectable clock. verifyAlipayV3Response applies alipayV3ResponseMustVerify(status, signed) = status < 400 || signed — every sub-400 response must verify (empty body included), a signed error is verified, an unsigned error passes through as the error. It runs in readConnectorResponse over the raw text before any parse (S1's placement in packages/spec/src/integration/connector-fetch-policy.ts, main L192-207), so a failed check throws and the body never reaches the flow. Refusal texts carry the connector name and HTTP status only — no body, signature or key.
  • PD#2 (hand-off question): placement stays S1's accepted one. This slice adds no logic to the spec package; it is the first scheme whose verifyResponse is non-empty, exercising the hook S1 landed and the lane reviewed then. No new runtime code enters packages/spec.
  • Keys never leave the host. Both keys are parsed into KeyObjects at materialization (readAlipayPrivateKey/readAlipayPublicKey: PEM or base64 PKCS#8/PKCS#1/SPKI, must be rsa, else "cannot sign/verify: it is not an RSA … key" naming the reference, never the value); the provider context carries the signer, not the keys (materialization test). The vendor public key resolves through resolveConnectorCredential on the same chain as credentialRef; unresolved → degradeConnectorForCredential naming vendorPublicKeyRef; both references join the instance signature (credentialRefsOf), so replacing either re-materializes (credential-door test), and the door lists the instance under both references. D4's "signer bound to its key at materialization" is S1/S2's accepted reading, extended to the public key.
  • Vectors (ruling B) and acceptance (「算」). Vectors from two official SDKs over a placeholder key that belongs to no app, byte-identical, generator and PLACEHOLDER-test-only-* keys in the repo; the acceptance test boots a real stack against a local gateway stand-in that checks request signatures and signs answers, with tampered/unsigned answers failing the dispatch. The ADR note records the third branch of the vector rule and the verification rule in one place.
  • Describe wording (hand-off question): acceptable as is. The TSDoc "every response is checked … before its body is read" states the invariant for the responses the rule covers; the exact status rule lives in the signer and the ADR note. Not a contract defect; a one-line tightening is the seat's option, not owed.
  • Skills and docs. integration-recipes.md: the scheme list names alipay-v3; the "RSA-signing vendor … blocked" line now names WeChat Pay only (wechatpay-rsa2048 still refused as unknown, pinned). Token ratchet: 8938 bytes = 2235 tokens under the row's 2236 ceiling. connectors.mdx documents the scheme and corrects S2's Content-Type sentence to match its signer's docblock.

② Rulings cited

③ Narrowing / widening

  • Widening only: one enum member, one union shape, one optional registry parameter member, new exports. Existing bearer/api-key/basic/oauth2/app-credential/aliyun/tencent instances parse and run unchanged. The one stricter behaviour (responses must verify) applies only to the new scheme.

④ Behaviour / flags

  • Certificate mode (app_cert_sn) not implemented; public-key mode only — docs say so. Not measured against Alipay's live sandbox by ruling.
  • The verification rule is a seat call recorded in the ADR; a different ruling changes alipayV3ResponseMustVerify only.
  • Alipay's alipay-signature header family is read case-insensitively through Headers; a response signed with a rotated vendor key fails until vendorPublicKeyRef is replaced (re-materializes) — the intended fail-closed shape.

Implemented-by: claude/issue-26710-alipay-v3-signing
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz
VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

Governed approval requested · 2026-10-31T17:43Z · head 2686771adc8afea173a408db70aad51304f29595

  • Tier H: docs/adr/0143-outbound-connector-auth-is-open-tier.md (two dated 2026-10-31 notes: the third branch of the vector rule and the response-verification rule; the 「算」 acceptance) and skills/objectstack-automation/references/integration-recipes.md (two lines).
  • Contract review PASS 6546917838 on this head; seat ACCEPT (lane part) 6546497764; CI green (one superseded Check Changeset run cancelled, its rerun green).
  • Asked of os-zhuang or hotlong: an APPROVED review on this head. After it, the seat lands (pr_ready, then auto-merge).

@os-zhuang
os-zhuang marked this pull request as ready for review October 31, 2026 20:12
Resolve the type-alias pin count: #28406 took 805 -> 806 on main, so the
alipay-v3 signed shape is now 806 -> 807. Regenerate the reference index.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

Head moved by a merge of main only · 2026-10-31T20:16Z · 2686771adc8afea173a408db70aad51304f29595 → 1730f73f0c1194a279ea442ca4b2bee1831da992

The PR went dirty after #28447 (#28406) landed. The merge resolves one conflict and touches no reviewed file of this PR:

The contract-review PASS (6546917838) was on 2686771a; a delta review of the merge commit is asked below. The governed approval request (17:43Z) stands for the new head: the Tier H files are byte-identical to the reviewed head.

Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (HTTP 429 "You've reached your Fable limit", 2026-10-30T04:26Z) · head 1730f73 · delta from 2686771 (merge of main only)
Thread-read: none

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 1730f73f0c1194a279ea442ca4b2bee1831da992 · 2026-10-31T20:44Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review (delta)

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1730f73f0c1194a279ea442ca4b2bee1831da992
Local-runs: none
Scope: the delta from the reviewed head 2686771a (PASS 6546917838) to 1730f73f, one merge commit of origin/main (seat note 6548619936, hand-off line there). Tier H files (docs/adr/0143-outbound-connector-auth-is-open-tier.md, skills/objectstack-automation/references/integration-recipes.md) byte-identical to the reviewed head.
Inputs: git show --cc 1730f73f (conflict-resolution hunks: packages/spec/src/type-alias-convention.pin.test.ts 806 → 807 in the count, the prose and the toHaveLength, and content/docs/references/index.mdx 1575 → 1576), the PR-only interdiff (this PR's diff against its own merge-base, before and after, hunk headers ignored: exactly those two count moves, plus one materializingHandle context line in plugin.ts that main added and the PR does not touch; no other byte of the PR's own change moved), check-runs (read 2026-10-31T20:45Z: all 50 latest-per-name check-runs completed, 39 success + 11 skipped, none failed, none running) — 50 checks.

Delta judgment

Implemented-by: claude/issue-26710-alipay-v3-signing
Reviewed-by: triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

VERDICT: PASS (delta)

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

Landing stroke · 2026-10-31T21:08Z · head 1730f73f0c1194a279ea442ca4b2bee1831da992.

  • Contract review: PASS 6546917838 on 2686771a, plus delta PASS 6548912880 on this head (a merge of main only; the Tier H files are byte-identical to the reviewed head). No must-fix.
  • Governed approval: os-zhuang APPROVED (review 5724748027, 2026-10-31 20:13Z). check-governed-merges.mjs --pr 28446: Tier H (docs/adr/0143-…, skills/objectstack-automation/references/integration-recipes.md), so an authorized APPROVED review is needed, and then the owning seat lands it. Per GOVERNED_APPROVERS the approval counts regardless of commit_id.
  • Rulings this rests on: vectors option B (「其他同意」), local end-to-end acceptance (「算」), both quoted in the PR body. needs-user-decision and needs:contract-review come off.
  • Checks on this head: 58 completed, 43 success, 15 skipped, 0 failed; check-expected-skips.mjs --pr 28446 OK (15 skips, every one in main's roster). mergeable_state clean.
  • The seat lands it: auto-merge.

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 31, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 31, 2026
Resolve the type-alias pin count again: #28005 took 806 -> 807 on main,
so the alipay-v3 signed shape is now 807 -> 808. Regenerate the reference
index.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

Head moved by a second merge of main only · 2026-10-31T21:32Z · 1730f73f0c1194a279ea442ca4b2bee1831da992 → 7420bc20c9de006608ac33ecb12a149b4704ad5c

The merge queue dequeued the PR at 21:27Z (MERGE_CONFLICT): #28440 (#28005) landed and took 806 -> 807 in the same pin.

The os-zhuang approval (5724748027) stands: per GOVERNED_APPROVERS it counts regardless of commit_id. A delta review of this merge commit is asked below. After it passes and CI is green, the seat re-arms auto-merge.

Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (HTTP 429 "You've reached your Fable limit", 2026-10-30T04:26Z) · head 7420bc2 · delta from 1730f73 (merge of main only; delta PASS 6548912880 was on 1730f73)
Thread-read: none

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 7420bc20c9de006608ac33ecb12a149b4704ad5c · 2026-10-31T21:44Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review (delta)

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7420bc20c9de006608ac33ecb12a149b4704ad5c
Local-runs: none
Scope: the delta from the reviewed head 1730f73f (PASS (delta) 6548912880) to 7420bc20, one merge commit of origin/main after #28440 dequeued the queue (seat note 6549414654, hand-off line there; claim 6549529022). Tier H files (docs/adr/0143-outbound-connector-auth-is-open-tier.md, skills/objectstack-automation/references/integration-recipes.md) carry no PR-side change: ADR-0143 took main's #28456 D5 hunk by auto-merge, the PR's own ADR and skills lines are byte-identical to the reviewed head; os-zhuang's approval 5724748027 stands.
Inputs: git show --cc 7420bc20 (conflict-resolution hunks: packages/spec/src/type-alias-convention.pin.test.ts 807 → 808 with main's #28005 line kept beside the alipay-v3 line, and content/docs/references/index.mdx 1576 → 1577), the PR-only interdiff (this PR's diff against its own merge-base before and after, hunk headers ignored: exactly those two count moves, nothing else of the PR's own change moved), merge-tree against origin/main clean, check-runs (read 2026-10-31T22:04Z: all 50 latest-per-name check-runs completed, 39 success + 11 skipped, none failed, none running) — 50 checks.

Delta judgment

Implemented-by: claude/issue-26710-alipay-v3-signing
Reviewed-by: triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

VERDICT: PASS (delta)

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

Landing stroke (re-arm after the 21:27Z MERGE_CONFLICT dequeue) · 2026-10-31T22:06Z · head 7420bc20c9de006608ac33ecb12a149b4704ad5c.

  • Contract review: PASS 6546917838 (2686771a), delta PASS 6548912880 (1730f73f) and delta PASS 6549742522 on this head. Each delta was a merge of main only, and no must-fix was raised.
  • Governed approval: os-zhuang APPROVED 5724748027. The PR's Tier H lines are byte-identical to the approved head.
  • Checks on this head: 55 completed, 42 success, 13 skipped, 0 failed. check-expected-skips.mjs --pr 28446 is OK. mergeable_state is clean. needs:contract-review comes off.
  • The seat re-arms auto-merge.

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 31, 2026
Merged via the queue into main with commit f79fa74 Oct 31, 2026
58 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-26710-alipay-v3-signing branch October 31, 2026 22:28
marchtian pushed a commit that referenced this pull request Oct 31, 2026
Regenerate the references index from the merged tree after #28446.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants