Repository navigation
Commit 3238750
feat(spec)!: oauth2 and app-credential connector instance auth; the runtime oauth2 variant retired (ADR-0143 S0) (#28213)
domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6Jao4xopuwRpgz7BBJKw6iZpKi)_
Fixes #28137
Clause-②: yes (narrowing)
Metadata-protocol: narrowed
Skills-docs: follow-up #28226
## 维护者速读
- 改了什么:连接器实例的 `auth` 新增两种需要换取令牌的类型,`oauth2`(授权码 / 客户端凭证)和
`app-credential`(飞书、钉钉、企业微信),密钥只能写引用;旧的运行时 `oauth2` 写法被移除并给出改写提示。
- 为什么改:以前对接要 OAuth 的系统在开源版里无处声明,旧写法把密钥和刷新令牌明文写进配置,也从来没人执行;ADR-0143
把这类认证纳入开源层,这是第一步(只定形状)。
- 风险与回滚:写了 `authentication: { type: 'oauth2' }`
的连接器会被拒绝(有迁移提示);新类型的实例在服务端真正支持前一律标为"降级、不调用上游",数据源借用返回 422,不会出现未认证调用。回滚即
revert 本 PR。
- 先看:`packages/spec/src/shared/connector-auth.zod.ts`(新形状与拒绝提示)和
`packages/services/service-automation/src/plugin.ts` 里新加的降级分支。
- 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run
src/integration/connector-auth-grant-kinds.test.ts
src/shared/connector-auth.test.ts`,看到每个拒绝都有错误码和位置、每种类型都有正例通过。
- 需要你做的:无。不属于受管面,契约复审 PASS(评论 6529303069),由席位落地;技能文档与 lint 提示的更新已立卡
#28226,随 S1 落地。
## What changes
**`@objectstack/spec`**
- `ConnectorInstanceAuthSchema` gains two members (ADR-0143 D2.1, D2.2).
Both are closed (`strictObject`), and both carry their secret only as
`credentialRef`.
- `oauth2` is a union on `grant`, one strict shape per grant.
- `authorization_code` takes `authorizationUrl`, `tokenUrl`, `scopes`,
`resource`, `clientId` and an optional `credentialRef` (a public client
has no secret).
- `client_credentials` has no `authorizationUrl` key, and its
`credentialRef` is required.
- Both use the projectable `dependentRequired` refinement, so the JSON
Schema carries the rule and no dropped-refinements entry is owed.
- `app-credential` takes `vendor` (`feishu` | `dingtalk` | `wecom`),
`appId` and a required `credentialRef`.
- An inline secret or token key is refused by name with the
`credentialRef` prescription: `clientSecret`, `secret`, `accessToken`,
`refreshToken`, `token` and `pkce` on `oauth2`; `appSecret`, `secret`,
`corpSecret`, `accessToken` and `token` on `app-credential`. The aliases
`audience` → `resource` and `appKey` / `corpId` / `clientId` → `appId`
answer with the right key.
- ⛔ There is no `signed` member (#26710, D2.3).
- On `DeclarativeConnectorEntrySchema` (both authoring doors), an
`oauth2` instance must name `tokenUrl` and `clientId`, plus
`authorizationUrl` on `authorization_code`. Each missing key is refused
at `auth.<key>`. The one exception is `provider: 'mcp'` over an `http`
transport, where the MCP authorization flow discovers the endpoints and
can register the client. An mcp instance over stdio gets the same
refusals as any other instance.
- **D7.** `ConnectorOAuth2Schema` leaves `ConnectorAuthConfigSchema`.
- `authentication: { type: 'oauth2' }` is refused at `type` with a
prescription that names ADR-0143 D7 and the instance `auth` that
replaces it. The union's error map fires only for that literal, the
`ViewDataSchema` `provider: 'api'` precedent.
- `ResolvedConnectorAuth` stays the four static kinds.
- The schema was module-level only and never on a published entry point.
The type it narrows, `integration/Connector:authentication`, is
authorable, hence the narrowing line.
- The ADR-0087 D3 entry `connector-auth-config-oauth2-retired` records
the change, with its `STEP18_RATIONALE` fragment (order 115).
- **D4.** The per-dispatch handle types move into
`packages/spec/src/integration/connector-provider.ts`:
`ConnectorCredentialHandle`, `BorrowedConnectorCredential`,
`BorrowedConnectorAuth` and `ConnectorCredentialRequest`, structurally
identical. `ConnectorProviderContext` gains `readonly credentialHandle?:
ConnectorCredentialHandle`.
- **Token-endpoint client authentication** (card item 4) has no key. The
ADR leaves the method (`client_secret_basic` vs `client_secret_post`) to
the runtime that calls the token endpoint, so S1 owns it. The seat
accepted this as an S1 choice.
- The header text "OAuth2 is intentionally absent" is rewritten to the
open-tier reading. The six new isomorphic aliases are pinned in
`type-alias-convention.pin.test.ts` (count 795 → 801, with its receipt).
**`@objectstack/types`**: `connector-credential-source.ts` re-exports
the four handle types from `@objectstack/spec/integration` under the
same names. The service-datasource typecheck and its 1069 tests are
unchanged and green.
**`@objectstack/service-automation`** (seat ruling A). Widening the
union made two switches non-exhaustive, so the minimal exhaustive
handling is:
- `plugin.ts`: an `oauth2` or `app-credential` instance is held
**degraded** through the existing `failDegradedConnectorInstance` path,
with a reason that names its kind. Its factory is never called, so it
never reaches the upstream unauthenticated.
- `connector-credential-source.ts`: a datasource borrow of such an
instance is refused with `422 VALIDATION_ERROR`, naming the kind and the
instance.
**Docs and fixtures**
- `content/docs/automation/connectors.mdx`: "Where is `oauth2`?" is
replaced by "Grant-bearing kinds: `oauth2` and `app-credential`", plus a
note on the retired runtime variant and a failure-modes row.
- `packages/spec/docs/SYNC_ARCHITECTURE.md`: the L3 example moves off
the retired variant. The auth table drops `jwt` and `saml`, which were
never variants.
- `variant-docs.json`: re-keyed for the three unions.
- The frozen fixture `packages/qa/downstream-contract` `DcConnector`
changes per its README, because the break is intended and documented
here: `authentication` is now `bearer`, with a comment citing the D7
entry.
## S1 replaces these lines
These are the stand-in lines S1 (#26702) replaces when per-dispatch
resolution lands. Every one of them is in
`packages/services/service-automation/src/`:
- `plugin.ts`, the `GrantBearingInstanceAuth` type and
`grantBearingNotServedReason()` (lines 402–417 at head).
- `plugin.ts`, the narrowed `resolveInstanceAuth(auth:
Exclude<ConnectorInstanceAuth, GrantBearingInstanceAuth> | undefined,
…)` signature and its doc sentence "A grant-bearing kind never reaches
here either…" (lines 424–431).
- `plugin.ts`, the reconcile branch `if (entry.auth?.type === 'oauth2'
|| entry.auth?.type === 'app-credential') {
this.failDegradedConnectorInstance(…); continue; }` with its three-line
comment (lines 2269–2283).
- `connector-credential-source.ts`, `Lendable.auth:
Exclude<ConnectorInstanceAuth, { type: 'none' | 'oauth2' |
'app-credential' }>` (line 84).
- `connector-credential-source.ts`, the `lendRefusal` branch `if
(auth.type === 'oauth2' || auth.type === 'app-credential') { return … }`
with its comment (lines 118–126).
- The tests that pin the stand-in: `connector-materialization.test.ts`
describe "ADR-0143 D4 — a grant-bearing instance is held degraded…", and
`connector-credential-source.test.ts` describe "[ADR-0143 D4]
createConnectorCredentialSource — a grant-bearing kind is refused, not
lent". Their CONTROL cases for the four static kinds stay.
- `content/docs/automation/connectors.mdx`: the "A server that does not
yet run the per-dispatch token resolution…" paragraph and the
failure-modes row for a grant-bearing kind.
What S1 needs that this slice leaves out:
- A provider that reads `ConnectorProviderContext.credentialHandle`. The
member is typed here and nothing sets it yet.
- The token-endpoint client-auth method choice (basic vs post).
- The connection record that holds an `authorization_code` grant.
- The mcp `client_credentials` path through dynamic client registration,
when `clientId` is omitted.
- The skill bullet named under `Skills-docs:` above.
## Deviations from the dispatch order
- The order said "Do not touch server packages". Widening
`ConnectorInstanceAuth` turned the service-automation typecheck red (6
errors), so I reported it to the seat. The seat ruled **A**: touch only
`connector-credential-source.ts` and `plugin.ts`, minimal and
exhaustive, as above.
- The order's lines were `Clause-②: yes (widening)` and
`Metadata-protocol: added`. D7 removes an authorable value
(`integration/Connector:authentication` `type: 'oauth2'`), so the seat
ruled `yes (narrowing)` / `narrowed`, with a BREAKING changeset and the
registered ADR-0087 entry. The lines are corrected here and in the
changeset.
- Beyond the expected landing, these files changed on the producer side:
`integration/connector.zod.ts` (the entry rule and re-exports),
`shared/connector-auth.test.ts` and `integration/connector.test.ts`
(retired-variant tests), `type-alias-convention.pin.test.ts` (ADR-0122
gate), `variant-docs.json`, `content/docs/automation/connectors.mdx`,
`packages/spec/docs/SYNC_ARCHITECTURE.md` (its L3 example is compiled by
`connector-author-shape.test.ts`) and the qa fixture.
## Verification
Head `4d12a2786bc03dfb6d046d7a31f9761cec235616` (includes a merge of
`origin/main`).
**Tests**
| Command | Result |
|:---|:---|
| `vitest run src/integration src/shared src/migrations
src/type-alias-convention.pin.test.ts` (spec) | 54 files, 1335 tests,
green |
| `pnpm --filter @objectstack/spec typecheck` (incl. test layer) | exit
0 |
| `pnpm --filter @objectstack/types typecheck && test` | exit 0 ·
825/825 |
| `pnpm --filter @objectstack/service-automation typecheck && test` |
exit 0 · 2926/2926 (merged head) |
| `pnpm --filter @objectstack/service-datasource typecheck && test` |
exit 0 · 1069/1069 |
| `pnpm --filter @objectstack/downstream-contract typecheck` +
`test/contract.test.ts` | exit 0 · 13/13 |
`consumer-specifier-ledger.test.ts` refuses locally because
`@objectstack/cli` has no dist in this box. That is a prerequisite
refusal, not a finding, and CI builds it.
**Gates.** `dispatch-gates.mjs --commands` derived 109 commands. I ran
each one with `</dev/null` and recorded `<command> :: exit <code>` on
disk. All 109 exit 0.
`--ran` reconciliation: "109 derived, 109 run, 0 NOT-MEASURED, 0 UNRUN".
Four commands first refused for local reasons and were re-run green:
- `check-plugin-teardown-shape --self-test`, on a shallow clone, after
fetching its pinned commit.
- The `.d.ts`-reading spec gates, against a stale dist after the rebase,
after a rebuild.
- `check:skill-examples`, with no client-react dist, after building it.
- `check:dual-build-cjs-loads`, with no package dists, after a packages
build.
One real finding, `check:spec-parsed-alias` (ADR-0122, 6 new aliases),
is fixed by the pins. `check-platform-checklist --changed-from
origin/main`: OK.
**Ablations.** I used `scripts/ablation-replace.mjs`. Each mutation
landed (anchor 1 → 0) and was restored (blob == HEAD):
- A1, the mcp-http exception removed: 3 red in
`connector-auth-grant-kinds.test.ts`, the two mcp CONTROLs and the
secret-without-client-id case.
- A2, the D7 prescription removed: 2 red in `connector-auth.test.ts`.
- A3, `client_credentials` no longer requiring `credentialRef`: 1 red.
- A4, the `plugin.ts` degrade branch disabled: 2 red in
`connector-materialization.test.ts`, one per kind.
- A5, the lend refusal disabled: 2 red in
`connector-credential-source.test.ts`, one per kind.
**Dogfood battery.** Full `@objectstack/dogfood` on the final head
`4d12a2786`, run under `os-verify-lock` as 6 vitest shards, each
`VERDICT command-exit 0`. Result: Test Files 359 passed, 3 skipped
(362). Tests 2891 passed, 20 skipped (2911). No failures. It was also
green on the pre-merge head `4609d0cb6`.
## Acceptance notes
- **Located errors.** Every refusal is pinned by `code @ path`. Each of
the two members has a CONTROL accept, and so does `provider: 'mcp'` on
http with the endpoints and `clientId` omitted
(`connector-auth-grant-kinds.test.ts`):
- a missing endpoint: `custom @ auth.tokenUrl` / `auth.clientId` /
`auth.authorizationUrl`
- an unknown grant: `invalid_union @ auth.grant`
- an inline secret, token or `pkce`: `unrecognized_keys @ auth` with the
key named
- `credentialRef` without `clientId`, or `client_credentials` without a
secret: `custom @ auth.credentialRef`
- an unknown vendor: `invalid_value @ auth.vendor`
- a missing app secret reference: `invalid_type @ auth.credentialRef`
- `signed` / `oauth1`: `invalid_union @ type`
- An inline secret is refused on both new members (ADR Acceptance).
- `ResolvedConnectorAuth` is unchanged (four static kinds).
`@objectstack/types` keeps re-exporting the moved handle types, and the
datasource borrow path's importers typecheck and test green.
- The changeset is `.changeset/28137-connector-grant-bearing-auth.md`:
BREAKING, with `<!-- adr-0087: registered
connector-auth-config-oauth2-retired -->`. `check-adr-0087-registration`
and `check-changeset-no-major` are green.
- A contract review at `CONTRACT_REVIEW_TIER` is owed before the queue.
The triage seat runs it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01L9qibvmENgz1tZkajp5SuJ
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L9qibvmENgz1tZkajp5SuJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 7c3cc88 commit 3238750
27 files changed
Lines changed: 1301 additions & 306 deletions
File tree
- .changeset
- content/docs
- automation
- references
- integration
- docs/audits/2026-07-unknown-key-strictness-ledger.counts
- packages
- qa/downstream-contract/src
- services/service-automation/src
- spec
- api-surface
- authorable-surface
- declaration-map
- docs
- export-origins
- json-schema.manifest
- src
- integration
- migrations
- entries/semantic
- types/src
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
394 | 394 | | |
395 | 395 | | |
396 | 396 | | |
397 | | - | |
398 | | - | |
399 | | - | |
400 | | - | |
401 | | - | |
402 | | - | |
403 | | - | |
404 | | - | |
405 | | - | |
406 | | - | |
407 | | - | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
408 | 452 | | |
409 | 453 | | |
410 | 454 | | |
411 | | - | |
412 | | - | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
413 | 458 | | |
414 | 459 | | |
415 | 460 | | |
| |||
418 | 463 | | |
419 | 464 | | |
420 | 465 | | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
421 | 471 | | |
422 | 472 | | |
423 | 473 | | |
| |||
449 | 499 | | |
450 | 500 | | |
451 | 501 | | |
| 502 | + | |
452 | 503 | | |
453 | 504 | | |
454 | 505 | | |
| |||
| 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 | | |
| |||
190 | 190 | | |
191 | 191 | | |
192 | 192 | | |
193 | | - | |
| 193 | + | |
194 | 194 | | |
195 | 195 | | |
196 | 196 | | |
197 | 197 | | |
198 | 198 | | |
199 | | - | |
| 199 | + | |
200 | 200 | | |
201 | 201 | | |
202 | 202 | | |
| |||
0 commit comments