Repository navigation
feat(spec,service-automation): alipay-v3 joins the signed connector auth schemes, with response verification (ADR-0143 D2.3, slice 3 of #26710) - #28446
Conversation
… the Alipay public key as vendorPublicKeyRef Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxRP1AqTeP2PWAhwGK
… with vectors two official SDKs agree on Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxRP1AqTeP2PWAhwGK
… a local gateway stand-in Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxRP1AqTeP2PWAhwGK
…amendments; the changeset Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxRP1AqTeP2PWAhwGK
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…schemes; only WeChat Pay stays unsigned Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…y line names Alipay's scheme in fewer words Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
|
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V 维护者速读(席位终稿 · head
|
📓 Docs Drift CheckThis PR changes 2 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 168 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: ① Derived judgments
② Rulings cited
③ Narrowing / widening
④ Behaviour / flags
Implemented-by: |
|
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V Governed approval requested · 2026-10-31T17:43Z · head
|
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
|
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V Head moved by a merge of The PR went
The contract-review PASS (6546917838) was on 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) |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract review (delta)Served-tier: Delta judgment
Implemented-by: VERDICT: PASS (delta) |
|
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V Landing stroke · 2026-10-31T21:08Z · head
|
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
|
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V Head moved by a second merge of The merge queue dequeued the PR at 21:27Z (
The os-zhuang approval (5724748027) stands: per 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) |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract review (delta)Served-tier: Delta judgment
Implemented-by: VERDICT: PASS (delta) |
|
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V Landing stroke (re-arm after the 21:27Z
|
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
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" skillshits that one file)维护者速读
signed签名认证新增第三个方案alipay-v3。市场里对接支付宝开放平台 v3 的连接器,现在每个请求都会用应用私钥签名,每个响应都会用支付宝公钥验签;验签不过的响应直接让调用失败,内容不会进入流程。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 个用例全绿:签名请求被本地支付宝网关接受、篡改或未签名的响应让调用失败、私钥不出现在任何请求与描述里。What changes
alipay-v3joins thesignedscheme 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:ConnectorSigningSchemeSchemagainsalipay-v3; thesignedunion gainsConnectorInstanceSignedAlipayV3AuthSchema,{ type: 'signed', scheme: 'alipay-v3', keyId, credentialRef, vendorPublicKeyRef }.keyIdis the app id,credentialRefthe application private key,vendorPublicKeyRef(required) the Alipay public key. The S1 publish refusals apply (inline key spellings by name, an authoredsignature,signedonprovider: 'mcp'), plusappPrivateKeyby name andalipayPublicKeypointed atvendorPublicKeyRef;appIdis answered withkeyId. The unknown-scheme refusal lists all three schemes.@objectstack/service-automation:request-signing/alipay-v3.tssigns every attemptALIPAY-SHA256withRSAwith 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 resolvesvendorPublicKeyRefthrough the same chain ascredentialRef: 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.content/docs/automation/connectors.mdxdocuments the scheme, its two references and the boot table rows. It also fixes one S2 sentence: Tencent's defaultContent-Typeapplies to a request "without a content type", not "without a body" (the signer keys on the header, as its own docblock says).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-sdk4.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:The rule is one function,
alipayV3ResponseMustVerify, so a different ruling changes one line. The ADR note records it.Rulings this rests on (binding, quoted)
com.alipay.sdk:alipay-sdk-java-v3:3.1.89.ALLandalipay-sdk4.14.0 (Node) over one placeholder key that belongs to no app; the two outputs are byte-identical; the generator is kept inpackages/services/service-automation/test-vectors/alipay-v3/.Flags for the contract review
packages/spec). Response verification runs throughreadConnectorResponseand the per-attempt signing wrap inpackages/spec/src/integration/connector-fetch-policy.ts, which landed with S1. This slice adds no logic there, but it is the first scheme whoseverifyResponseis non-empty, so it is the first that relies on that runtime code living in the spec package.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 tovendorPublicKeyRef(the credential-door test pins the re-materialization on replace).Independent review: none run (quota); the PR waits for the seat's contract-review hand-off.
Tests
request-signing/alipay-v3.test.ts): the POST (JSON body) and GET (query, no body)Authorizationheaders 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.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.connector-auth-signed.test.ts,type-alias-convention.pin.test.ts): the shape onrestandopenapi, the refusals above,vendorPublicKeyRefrequired onalipay-v3and refused on the other schemes.connector-openapi/src/alipay-v3-acceptance.test.ts): a real boot (LiteKernel, AutomationServicePlugin, ConnectorOpenApiPlugin) with anopenapiinstance over the vendor'salipay.trade.queryoperation, dispatched through a flowconnector_actionto 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
app_cert_sn) is not implemented, and the docs say so.test-vectors/alipay-v3/is namedPLACEHOLDER-test-only-…and belongs to no Alipay app.skills/objectstack-automation/references/integration-recipes.mdis corrected here (commits10c6ddf7b3,1083e85431; the seat amended the claim surface on 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): thesignedscheme list now namesalipay-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 Teach oauth2 and app-credential connector auth once S1 serves them: skills recipe bullet, its eval, and lint steer strings still say the union has no oauth2 #28226 did not hold: Teach oauth2 and app-credential connector auth once S1 serves them: skills recipe bullet, its eval, and lint steer strings still say the union has no oauth2 #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 on1083e85431: its six derived skills gates (check-skills-token-ratchetand its self-test,check:agent-test-spelling,check:pm-skill-id-lint,check:skill-compatibility,check:skill-frame-sync) exit 0,--ran116 of 116; the file stays under its token ceiling (2235 of 2236). The rest of this section reads (the branch after mergingorigin/mainataaa5c1df8d; the merge regeneratedcontent/docs/references/index.mdxandcheck:generatedis 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/objectstackderived 110 commands; each was run with its exit code captured before any pipe. 109 exited 0 on the first run.pnpm check:type-check-debtfirst 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 --ranover thecommand :: exit coderecord: 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 typecheckfor@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 --porcelainempty afterwards). The subjects are reached through source (a relative import, or the@objectstack/service-automationsource alias inconnector-openapi's vitest config), so no build is involved.verifyResponseinalipay-v3.tsmade a no-op):alipay-v3.test.ts4 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.sign=F…tosign=G…in the pinnedAuthorization): 5 red, led by "reproduces the POST (JSON body) request signature".alipay-v3from the signer switch (request-signing/index.ts):service-automationtypecheck exit 2,TS2322naming thealipay-v3shape as not assignable tonever.