Skip to content

Commit b358e79

Browse files
feat(spec): signed connector instance auth, with aliyun-acs3-hmac-sha256 signing every request (ADR-0143 D2.3, slice 1 of #26710) (#28273)
<!-- 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
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-automation': minor
4+
'@objectstack/connector-rest': minor
5+
'@objectstack/connector-openapi': minor
6+
---
7+
8+
feat(spec): `signed` joins declarative connector-instance auth, with `aliyun-acs3-hmac-sha256` as its first scheme (ADR-0143 D2.3, #26710)
9+
10+
Clause-②: yes (widening)
11+
Metadata-protocol: added
12+
13+
<!-- adr-0087: not-required (no-migration-prescription) One member is added to the instance-auth union and optional members to the provider context and materialization. Nothing an author or a provider wrote before is refused or re-shaped, so there is nothing for a migration to rewrite. -->
14+
15+
An upstream that authenticates every request by a signature over the request itself, rather than by a token it is sent, had no way to be declared on a connector instance. ADR-0143 D2.3 adds that kind, `signed`, with a closed list of schemes. A scheme joins the list only together with its signer and its vendor test vectors. This release ships one scheme: Alibaba Cloud's OpenAPI V3 signature.
16+
17+
- **`@objectstack/spec`**
18+
- `ConnectorInstanceAuthSchema` gains `{ type: 'signed', scheme, keyId, credentialRef }`, a closed shape for each scheme. `keyId` is the public key identifier. The signing key is only ever a `credentialRef`.
19+
- The publish door refuses an unknown or missing `scheme` at `auth.scheme`, naming the value it got and the schemes that ship. It refuses an inline `secret`, `privateKey`, `secretKey` or `accessKeySecret` by name, with the `credentialRef` prescription, and an authored `signature`. It refuses `signed` on `provider: 'mcp'` at `auth.type`.
20+
- `@objectstack/spec/integration` gains the request-signer seam: `ConnectorRequestSigner` (`sign`, plus an optional `verifyResponse` for schemes that sign their responses), `ConnectorProviderContext.requestSigner`, and `ConnectorMaterialization.requestSignerApplied`.
21+
- `connectorFetchOptions` takes an optional signer and signs every attempt `resilientFetch` makes, retries included. `readConnectorResponse` runs the scheme's response verification before parsing.
22+
- With no signer, both behave exactly as before.
23+
- **`@objectstack/service-automation`**
24+
- A `signed` instance's key resolves through the same credential chain as the static kinds, with the same degrade on an unresolved reference.
25+
- The key is bound to the scheme's signer at materialization, and the provider receives only the signer.
26+
- The service refuses to serve a `signed` instance on a provider that does not acknowledge the signer, so such an instance never calls its upstream unsigned.
27+
- A datasource that borrows a `signed` instance through `external.connector` is refused with `422`.
28+
- **`@objectstack/connector-rest`, `@objectstack/connector-openapi`**
29+
- Both apply a handed request signer to every request and acknowledge it.
30+
- A static credential together with a signer is refused.
31+
- `bearer`, `api-key` and `basic` requests are unchanged.

‎content/docs/automation/connectors.mdx‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,60 @@ them, declare a static kind (`bearer`, `api-key` or `basic`) to dispatch now;
467467
don't emulate a grant by putting a short-lived access token behind a `bearer`
468468
ref.
469469

470+
### Request signing: `signed`
471+
472+
Some upstreams take no token at all: every request carries a signature over its
473+
own method, path, query, headers and body, computed with a key the caller never
474+
sends. The `signed` kind declares that (ADR-0143 D2.3). The metadata names the
475+
**scheme** and the public **key id**; the signing key itself is a
476+
`credentialRef`, resolved through the same chain as every other kind.
477+
478+
```ts
479+
// Alibaba Cloud OpenAPI, V3 signature (ACS3-HMAC-SHA256):
480+
auth: {
481+
type: 'signed',
482+
scheme: 'aliyun-acs3-hmac-sha256',
483+
keyId: 'YourAccessKeyId', // the AccessKey ID: public, stays in metadata
484+
credentialRef: 'ALIYUN_ACCESS_KEY_SECRET',
485+
}
486+
```
487+
488+
What the platform does with it:
489+
490+
- **Every request is signed, each attempt fresh.** A retry is signed again with
491+
a new nonce and date, so a scheme that refuses a replayed nonce accepts it.
492+
- **The key never reaches the provider.** The automation service binds the
493+
resolved key to the scheme's signer at materialization and hands the
494+
provider the signer only. A datasource cannot borrow a `signed` instance
495+
(`external.connector` is refused with `422`): a driver cannot sign per
496+
request.
497+
- **Schemes that sign their responses are verified** before a result is
498+
returned; a response that does not verify fails the dispatch.
499+
- **`aliyun-acs3-hmac-sha256`** signs `host`, `content-type` and every
500+
`x-acs-*` header, and sets `x-acs-date`, `x-acs-signature-nonce`,
501+
`x-acs-content-sha256` and `Authorization`. The API's own action and version
502+
are yours to send, as `x-acs-action` / `x-acs-version` in `defaultHeaders`
503+
(rest) or in each call's headers.
504+
505+
What the publish door refuses:
506+
507+
- **An unknown `scheme`**, at `auth.scheme`, naming the scheme it got and the
508+
ones this platform ships. A scheme joins the list only together with its
509+
signer and its test vectors, so a scheme the server cannot run is never
510+
accepted.
511+
- **An inline key**: `secret`, `privateKey`, `secretKey` or `accessKeySecret`
512+
is refused by name, pointing at `credentialRef`. An authored `signature` is
513+
refused too: it is computed for every request.
514+
- **`provider: 'mcp'`**, at `auth.type`: an MCP server is not signed per
515+
request. Declare `provider: 'rest'` or `provider: 'openapi'`.
516+
517+
`signed` and a static credential do not combine: a request is authenticated
518+
one way. An `openapi` instance's remote `spec` URL is fetched unsigned, as
519+
before; only the API calls are signed. A third-party provider factory serves a
520+
`signed` instance only when it says it applies the signer
521+
(`requestSignerApplied` on its materialization); one that does not is refused at
522+
boot rather than allowed to call the upstream unsigned.
523+
470524
### The runtime shape: `authentication`
471525

472526
The `authentication` field is the **runtime** auth config, with four static
@@ -517,6 +571,7 @@ retry):
517571
| `provider` names no installed factory | **Hard boot error** naming the entry, the provider, and the plugin that supplies it — add the executor to `plugins:` |
518572
| Invalid `providerConfig` (missing `baseUrl` / `spec` / `transport`, wrong shape) | **Hard boot error** from the provider factory |
519573
| `auth` is a grant-bearing kind (`oauth2`, `app-credential`) on a server that does not yet run its per-dispatch resolution | **Not fatal**: the instance registers **degraded**, naming the kind, and the upstream is never called without its credential; a datasource borrowing it is refused with `422` |
574+
| `auth` is `signed` on a provider that does not say it applies the request signer | **Hard boot error** naming the instance, the scheme and the provider: its calls would go out unsigned — declare it through `rest` or `openapi` |
520575
| `credentialRef` resolves to nothing | **Not fatal**: the instance registers **degraded**, naming the reference — set it (`PUT …/credential-refs/:ref`), and it materializes without a restart |
521576
| The upstream refuses the credential (`401` / `403`) | **Not fatal**: the instance registers **degraded**. A stored credential is marked `rejected` and waits until it is set again (or a restart); an environment or vault credential is retried on the backoff |
522577
| Instance `name` collides with a plugin-registered connector | **Hard boot error** — there is no silent precedence between the two worlds |

‎content/docs/references/index.mdx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Protocol reference — every schema by module
33
navTitle: Protocol Reference
4-
description: Every schema published by @objectstack/spec — 1566 schemas across 14 protocol modules
4+
description: Every schema published by @objectstack/spec — 1569 schemas across 14 protocol modules
55
---
66

77
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -25,7 +25,7 @@ counts are sums of the rows they head. Regenerate with
2525
| [Automation Protocol](/docs/references/automation) | 15 | 77 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. |
2626
| [Data Protocol](/docs/references/data) | 31 | 174 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. |
2727
| [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. |
28-
| [Integration Protocol](/docs/references/integration) | 1 | 18 | The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. |
28+
| [Integration Protocol](/docs/references/integration) | 1 | 21 | The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. |
2929
| [Kernel Protocol](/docs/references/kernel) | 29 | 151 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. |
3030
| [Marketplace Protocol](/docs/references/marketplace) | 4 | 30 | The package & marketplace format — package identity and versions, listing, publish, review, search, install, template manifests. |
3131
| [QA Protocol](/docs/references/qa) | 1 | 8 | Declarative test suites — scenarios, steps, actions and assertions. |
@@ -34,7 +34,7 @@ counts are sums of the rows they head. Regenerate with
3434
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
3535
| [System Protocol](/docs/references/system) | 37 | 287 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
3636
| [UI Protocol](/docs/references/ui) | 16 | 172 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
37-
| **Total** | **203** | **1566** | 14 protocol modules |
37+
| **Total** | **203** | **1569** | 14 protocol modules |
3838

3939
---
4040

@@ -191,13 +191,13 @@ Users and accounts, organizations, positions, SCIM provisioning.
191191

192192
## Integration Protocol
193193

194-
**Source:** `packages/spec/src/integration/` · **Import:** `@objectstack/spec/integration` · **1 page, 18 schemas**
194+
**Source:** `packages/spec/src/integration/` · **Import:** `@objectstack/spec/integration` · **1 page, 21 schemas**
195195

196196
The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances.
197197

198198
| File | Schemas |
199199
| :--- | :--- |
200-
| [`connector.zod.ts`](/docs/references/integration/connector) | `Connector`, `ConnectorAction`, `ConnectorActionEffect`, `ConnectorAppCredentialVendor`, `ConnectorInstanceAPIKeyAuth`, `ConnectorInstanceAppCredentialAuth`, `ConnectorInstanceAuth`, `ConnectorInstanceBasicAuth`, `ConnectorInstanceBearerAuth`, `ConnectorInstanceNoAuth`, `ConnectorInstanceOAuth2Auth`, `ConnectorInstanceOAuth2AuthorizationCodeAuth`, `ConnectorInstanceOAuth2ClientCredentialsAuth`, `ConnectorOAuth2Grant`, `ConnectorRetryStrategy`, `ConnectorType`, `DeclarativeConnectorEntry`, `RetryConfig` |
200+
| [`connector.zod.ts`](/docs/references/integration/connector) | `Connector`, `ConnectorAction`, `ConnectorActionEffect`, `ConnectorAppCredentialVendor`, `ConnectorInstanceAPIKeyAuth`, `ConnectorInstanceAppCredentialAuth`, `ConnectorInstanceAuth`, `ConnectorInstanceBasicAuth`, `ConnectorInstanceBearerAuth`, `ConnectorInstanceNoAuth`, `ConnectorInstanceOAuth2Auth`, `ConnectorInstanceOAuth2AuthorizationCodeAuth`, `ConnectorInstanceOAuth2ClientCredentialsAuth`, `ConnectorInstanceSignedAliyunAcs3Auth`, `ConnectorInstanceSignedAuth`, `ConnectorOAuth2Grant`, `ConnectorRetryStrategy`, `ConnectorSigningScheme`, `ConnectorType`, `DeclarativeConnectorEntry`, `RetryConfig` |
201201

202202
---
203203

0 commit comments

Comments
 (0)