Repository navigation
Commit b358e79
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D6hhjvTokP3gaKQjZx1GtR5)_
domain:server#3 · os-elon-musk · session_016WV4cxRP1AqTeP2PWAhwGK
Refs #26710
Clause-②: yes (widening: one member added to the instance-auth union;
optional members added to the provider context and the materialization;
an optional third parameter on `connectorFetchOptions`; two exports
added to `@objectstack/spec/integration`. Nothing accepted before is
refused)
Metadata-protocol: added
## 维护者速读
- 改了什么:连接器实例 `auth` 新增 `signed`(请求签名)类型,第一个签名方案是阿里云 OpenAPI V3
签名(`aliyun-acs3-hmac-sha256`)。元数据只写方案名、公开的 key id 和密钥引用
`credentialRef`;`rest` 和 `openapi` 两个连接器对每一次请求(包括重试)重新签名。
- 为什么改:阿里云、腾讯云、支付宝这类上游不收令牌,而是要求每个请求带上对请求本身的签名,以前在开源版里无法声明(ADR-0143
D2.3)。这是第一片(S1),另外两个方案在后续切片里随各自的签名器和向量加入。
- 拒绝什么:未知或缺失的 scheme、内联密钥(`secret` / `privateKey` / `secretKey` /
`accessKeySecret`)、手写 `signature`,以及 `provider: 'mcp'` 上的
`signed`,发布时按名拒绝。数据源借用 `signed` 实例返回 422。不确认会签名的
provider,服务端拒绝启动该实例,绝不发出未签名请求。
- 没变的:`bearer` / `api-key` / `basic` 的请求逐字节不变;入站认证不动;OAuth2
换取令牌不在本片(#26702)。
-
先看:`packages/spec/src/shared/connector-auth.zod.ts`(新形状和拒绝)、`packages/spec/src/integration/connector-fetch-policy.ts`(每次尝试重新签名)、`packages/services/service-automation/src/plugin.ts`(密钥绑定到签名器、确认缺失即拒绝)。
- 两分钟验证:`pnpm --filter @objectstack/service-automation exec vitest run
src/request-signing`,看到阿里云官方示例的规范请求、待签字符串和签名逐字节复现,错误密钥得到不同签名。
- 需要你做的:无。契约复审(CONTRACT_REVIEW_TIER)待做;Tier S,席位验收后落地。
## What changes
**`@objectstack/spec`**
- `ConnectorInstanceAuthSchema` gains `signed` (ADR-0143 D2.3). It is a
union on `scheme` with one `strictObject` per scheme. This slice ships
one scheme: `{ type: 'signed', scheme: 'aliyun-acs3-hmac-sha256', keyId,
credentialRef }`. `keyId` is the public key identifier. The signing key
is only ever a `credentialRef`. `ConnectorSigningSchemeSchema` is the
closed scheme list, and a pin keeps it equal to the union's members.
- What the publish door (`DeclarativeConnectorEntrySchema`, both
authoring doors) refuses:
- An unknown or missing scheme, at `auth.scheme`. The message names the
value it got and the schemes that ship.
- An inline `secret`, `privateKey`, `secretKey` or `accessKeySecret`, by
name, with the `credentialRef` prescription.
- An authored `signature`.
- `accessKeyId` is answered with `keyId`.
- `signed` on `provider: 'mcp'`, at `auth.type`.
- The request-signer seam in `connector-provider.ts`:
- `ConnectorRequestSigner` has `sign(request)`, which returns the
headers to set and optionally a canonical URL, plus an optional
`verifyResponse(response)` for schemes that sign their responses
(ADR-0143 Q4 condition 2: the seam carries it from the first slice).
- `ConnectorProviderContext.requestSigner` is set only for a `signed`
instance, and `auth` is then `undefined`.
- `ConnectorMaterialization.requestSignerApplied` is the provider's
acknowledgement.
- `connectorFetchOptions(policy, base, signer?)`: with a signer, it
wraps `fetchImpl`, so every attempt `resilientFetch` makes is signed
fresh. A retry gets a new nonce and date. `resilient-fetch.ts` is not
touched.
- `readConnectorResponse(response, signer?)` verifies the raw body
before parsing when the scheme verifies responses. With no signer, both
functions behave exactly as the code they replace. Neither uses
`node:crypto`: signers live on the server.
**`@objectstack/service-automation`**
- A `signed` instance's `credentialRef` resolves through the same chain
as the static kinds (#27237). An unresolved reference degrades the
instance with the reference named, as before.
- The key is bound to its scheme's signer in `src/request-signing/` (a
registry with an exhaustive switch over the scheme enum, so a scheme
without a signer does not compile). The provider receives only the
signer.
- The service refuses to serve a `signed` instance on a provider whose
materialization does not acknowledge the signer. That is a configuration
fault naming the instance, the scheme and the provider. Its connection
is closed, and it never dispatches unsigned (ADR-0049).
- A key that cannot sign (an empty one) is a configuration fault, and
its message never contains the key.
- `aliyun-acs3-hmac-sha256` builds the canonical request:
- method
- path, RFC 3986-encoded per segment
- sorted query
- `host`, `content-type` and `x-acs-*` headers
- SHA-256 of the body
It signs it with HMAC-SHA256 and sets `x-acs-date`,
`x-acs-signature-nonce`, `x-acs-content-sha256` and `Authorization`.
- A datasource that borrows a `signed` instance through
`external.connector` is refused with `422 VALIDATION_ERROR`, because a
driver cannot sign per request.
**`@objectstack/connector-rest`, `@objectstack/connector-openapi`**
- The providers pass `ctx.requestSigner` to the transport and
acknowledge it.
- A static credential together with a signer is refused at construction.
- An `openapi` instance's remote spec URL is fetched unsigned, as
before. Only API calls are signed.
**Docs:** `content/docs/automation/connectors.mdx` gains a "Request
signing: `signed`" section and a failure-table row.
`packages/spec/docs/SYNC_ARCHITECTURE.md` names the kind. The
variant-docs entry and the ADR anchor for the signer registry are
registered. The generated references are regenerated with
`check:generated --fix`.
## Why
ADR-0143 D2.3 opens per-request signing on declarative instances, with a
closed scheme list that grows only with implemented signers and vectors.
Q4 ruled A, so the first members are `alipay-v3`,
`aliyun-acs3-hmac-sha256` and `tencentcloud-tc3-hmac-sha256`. This slice
is Aliyun because Alibaba Cloud publishes a complete worked example, so
the seam is proven end to end against a byte-exact vector.
`tencentcloud-tc3-hmac-sha256` (S2) and `alipay-v3` with response
verification (S3, `Fixes #26710`) each join the enum together with their
signer.
## Verification
- **Vectors:**
`packages/services/service-automation/src/request-signing/aliyun-acs3-hmac-sha256.test.ts`
reproduces Alibaba Cloud's published V3 signature example byte for byte:
canonical request, string to sign, signature and `Authorization`. The
source is Alibaba Cloud's "V3 request structure and signature" document
(help.aliyun.com 2593177), with its own placeholder key id and secret. A
wrong key gives a different signature, and the canonical encoding of the
query and path is pinned (11 tests).
- **Publish door:**
`packages/spec/src/integration/connector-auth-signed.test.ts`. Each
refusal is pinned by issue `code` and `path`, and there is a CONTROL
accept on `rest` and `openapi`. `connector-auth-grant-kinds.test.ts`
still refuses unknown kinds. `bearer`, `api-key` and `basic` parse as
before.
- **Per-attempt signing:** `connector-fetch-policy.test.ts`:
- A 503 then 200 is signed twice with different nonces.
- A signer header replaces a same-named header, and the signer's URL is
the one sent.
- Non-text bodies are refused.
- `readConnectorResponse` verifies the raw body, and a verification
failure throws.
- CONTROL: with no signer, the fetch is untouched.
- **Host:** `connector-materialization.test.ts`:
- The signer is bound to the resolved key, `auth` is undefined, and the
key is absent from the context.
- A provider that does not acknowledge the signer fails boot and is
closed.
- An unresolved reference degrades, naming it.
- CONTROL: `bearer` gets no signer.
- **Borrow:** `connector-credential-source.test.ts` covers the 422.
- **Providers:** `rest-provider.test.ts` and `openapi-provider.test.ts`
cover every attempt signed (a retry included), the acknowledgement, a
verification failure rejecting, the static CONTROL without
acknowledgement, and static plus signer refused.
- **End to end:** `connector-rest-plugin.test.ts` runs a real host with
the rest provider. Two calls carry ACS3 `Authorization` headers with
different nonces and signatures, and the key appears in no call and no
descriptor.
- **Suites:**
- spec integration plus pins: 13 files, 211 passed.
- service-automation signing, materialization and credential source: 93
passed.
- connector-rest: 32 passed.
- connector-openapi: 75 passed.
- `pnpm --filter <pkg> typecheck` is green for spec, service-automation,
connector-rest and connector-openapi.
- Ablations: the host's acknowledgement check removed ⇒ the two
boot-refusal pins red; the signature cached across attempts ⇒ the
per-attempt pin red; the rest provider not passing the signer ⇒ 4 pins
red, the end-to-end one among them. With the code as pushed, all green.
- Every family `dispatch-gates --commands` derives (115) ran and exited
0, reconciled with `--ran`: 115 run, 0 NOT-MEASURED, after a
dependency-closure build and the pinned teardown fixture fetch. `pnpm
lint` exits 0.
Skills-docs: `grep -rlnE "credentialRef|app-credential|api-key" skills`
hits
`skills/objectstack-automation/{SKILL.md,references/integration-recipes.md,evals/integrations-declared.json}`,
which list the instance auth kinds. The `signed` text rides #28226 (the
skills follow-up for the ADR-0143 kinds), because a `skills/**` edit
would make this slice Tier H. `content/docs/automation/connectors.mdx`
carries it here.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(The signing wrap extends the existing `connectorFetchOptions` transport
helper, which already lives in spec beside `resilientFetch`. It holds no
crypto; signers live in `service-automation`.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request (slice 1 of 3 of #26710).
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the 维护者速读 block is the
lane's maintainer summary).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016WV4cxRP1AqTeP2PWAhwGK
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 361ea24 commit b358e79
36 files changed
Lines changed: 1611 additions & 49 deletions
File tree
- .changeset
- content/docs
- automation
- references
- integration
- docs/audits/2026-07-unknown-key-strictness-ledger.counts
- packages
- connectors
- connector-openapi/src
- connector-rest/src
- services/service-automation/src
- request-signing
- spec
- api-surface
- authorable-surface
- declaration-map
- docs
- export-origins
- json-schema.manifest
- src
- integration
- scripts/adr-anchors
| 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 | |
|---|---|---|---|
| |||
467 | 467 | | |
468 | 468 | | |
469 | 469 | | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
470 | 524 | | |
471 | 525 | | |
472 | 526 | | |
| |||
517 | 571 | | |
518 | 572 | | |
519 | 573 | | |
| 574 | + | |
520 | 575 | | |
521 | 576 | | |
522 | 577 | | |
| |||
| 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 | | |
| |||
191 | 191 | | |
192 | 192 | | |
193 | 193 | | |
194 | | - | |
| 194 | + | |
195 | 195 | | |
196 | 196 | | |
197 | 197 | | |
198 | 198 | | |
199 | 199 | | |
200 | | - | |
| 200 | + | |
201 | 201 | | |
202 | 202 | | |
203 | 203 | | |
| |||
0 commit comments