Repository navigation
Commit b1845ed
feat(spec/contracts): verifyMcpAccessToken's verified result carries organizationId, the organization an OAuth consent bound (#24189)
Fixes #24165
Clause-②: yes
The `packages/spec` contract slice of ruling `6232069920` (letter A, the
maintainer's 「同意」), recorded on #24137. #24137 remains open, and #24166
remains open: this PR declares the member only.
## What changed
- `packages/spec/src/contracts/auth-service.ts`:
`IAuthService.verifyMcpAccessToken`'s verified result, `{ userId:
string; scopes: string[]; clientId?: string }`, gains `organizationId?:
string`. Its TSDoc says what the member is:
- the organization the user's OAuth consent bound (the consenting user's
current organization at the moment of consent), read from the access
token's `organization_id` claim;
- absent for a token minted before consents bound an organization;
absent means no organization, so only global grants apply;
- a claim, not an authorization: the caller vets it on every request by
the rule that vets a session's active organization
(`vetOrganizationClaim`, `@objectstack/core`); once no current
membership backs it, the principal resolves with no organization (global
grants only: tenant reads come back empty, tenant writes are refused),
never in another one; never the union of the user's organizations, never
the browser session's currently selected organization; under ADR-0090
D10 it is the organization the delegator's grants are resolved in.
- No tracker number in the new prose.
- `packages/spec/src/contracts/auth-service.test.ts`: one case in the
interface's existing test home. Two typed reads (`string | undefined`)
of `organizationId`, off a bound result and an unbound one, are the pin;
`check:test-typecheck` compiles the file (zero ledger debt for it).
- `.changeset/24165-mcp-token-organization-id.md`: `@objectstack/spec`
`minor`, `Clause-②: yes (widening)`.
- `content/docs/kernel/contracts/auth-service.mdx` (`:41`-`:42`), patch
round 1 at `890c170d5`: the interface block's `verifyMcpAccessToken`
signature carries `organizationId?: string` as the contract spells the
result. Its one-line comment says the optional `organizationId` is the
organization the user's OAuth consent bound, absent for a token minted
before consents bound an organization. The docs-drift check named five
other pages (`kernel/contracts/index.mdx`, `kernel/index.mdx`,
`kernel/services-checklist.mdx`, `kernel/services.mdx`,
`permissions/authentication.mdx`). Each names `IAuthService` once and
none restates the result shape, so they are unchanged. (Added by the
dispatching seat from the dev's patch-round report `6236545086`.)
- Patch round 2 at `292bf5f32`, after contract review `6236564856`
FAILed on one point: the `client_credentials` / machine-to-machine
absence clause is removed from all four carriers (the TSDoc, the pin's
comment, the changeset's **Absent** bullet and the docs page's comment).
The one implementer refuses such a token before any result exists
(`auth-manager.ts`, `sub` equal to `client_id` or `azp` answers `null`),
so the clause described a result the surface never returns. The clause
came from the card body's gloss, through the dispatch. Contract review
`6237222227` PASSes on this head. (Added by the dispatching seat.)
Ruling items this slice carries, verbatim from `6232069920`: "The access
token carries it as the claim **`organization_id`**." · "Every request
re-checks membership through the existing session path. ⛔ Never the
union of the user's organizations, never a widened global grant, never
the browser session's current organization (B), never a D9-style pick
(C2)." · "The token falls back to no organization, so only global grants
apply: reads come back empty and tenant writes are refused loudly." ·
"The `packages/spec` contract type for the verified-token result. Spec
seat, `Clause-②: yes (widening)`."
## Premise, measured before editing
- On `origin/main` `cb8756b00`, `auth-service.ts:297` read
`verifyMcpAccessToken?(token: string)` with the three-member result and
no `organizationId` (`grep -n verifyMcpAccessToken packages/spec/src`
gave that one line; the positive control `clientId?: string` hits the
same line).
- The one implementer,
`packages/plugins/plugin-auth/src/auth-manager.ts`
`verifyMcpAccessToken` (`:6786`), is not typed against the interface,
and the runtime reader
(`packages/runtime/src/security/resolve-execution-context.ts:159`)
reaches it through `any`. The member is optional, so neither changes.
## Verification (all at `06210ccf8`)
- `pnpm --filter @objectstack/spec build`: exit 0; the tree stayed clean
(`git status --porcelain` empty), so no generated artifact moved. The
rebuilt `dist/contracts/index.d.ts` and `index.d.mts` carry the member
(1 hit each); `index.js` and `index.mjs` carry 0, because an interface
is erased, so no emitted JavaScript byte changed.
- `pnpm --filter @objectstack/spec check:generated`: "All 14 generated
artifacts are up to date". The `api-surface` shard does not move: it
records export names, and no export was added.
- `pnpm --filter @objectstack/spec exec vitest run --project local
--maxWorkers=2`: 692 files passed, 20975 tests passed, 1 todo.
- `pnpm --filter @objectstack/spec typecheck`: exit 0 (build program,
scripts program, test layer).
- Downstream consumers of `@objectstack/spec` that hold `IAuthService`
(dependency closures built with `turbo run build --filter='PKG^...'
--concurrency=2`), each `typecheck` exit 0: `@objectstack/mcp`,
`@objectstack/plugin-auth`, `@objectstack/runtime`,
`@objectstack/cloud-connection`.
- Reverse verification (one-off, not committed): a temporary
`packages/mcp/src` file read `organizationId` as `string | undefined`
and assigned `organizationId: 123` on an annotated result. `tsc
--noEmit` gave exactly one error, `TS2322: Type 'number' is not
assignable to type 'string'` on the second line. Against the old
declaration the read would be TS2339 and the literal TS2353, so
consumers read the rebuilt `.d.ts`. File deleted; tree clean.
- Ablation (one-off, through `scripts/ablation-replace.mjs`, with an
EXIT/INT/TERM restore): deleting the `organizationId?: string;` line
made `check:test-typecheck` red with
"src/contracts/auth-service.test.ts: 2 type error(s)". Restore proven:
blob equals HEAD (`e2d1d8138861`) and `git diff HEAD` is empty.
- Derived gate families: `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` gave 84 commands at `06210ccf8`. All
84 ran with exit codes recorded. Four first exited 3 (prerequisites: a
pinned fixture commit outside the shallow clone, and unbuilt dists).
After fetching that commit and building, all four re-ran exit 0. `--ran`
reconciliation: "84 derived famil(ies) accounted for — 84 run, 0
NOT-MEASURED".
## Acceptance notes
- `packages/runtime/src/security/resolve-execution-context.ts:154` says
`verifyMcpAccessToken` is "implemented (auth-manager.ts) and
undeclared". That was already false on `origin/main` (the contract
declares the method) and stays false. It is runtime's file and the
runtime slice's lane, so it is not edited here. Carrier: #24137's
runtime slice, which edits that block.
- The implementer answers `null` where the contract's type says
`undefined`. The contract's prose ("a falsy value") covers both, and
nothing types the implementer against the interface, so it is dormant.
Noted, not filed.
- `main` moved three commits after the base (`727091c82`). None touches
this diff's files or any generated artifact this diff could move. CI
runs on the merge ref.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01TyRhWbVbbQpQJTDbW8VahJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 8a88901 commit b1845ed
4 files changed
Lines changed: 61 additions & 3 deletions
File tree
- .changeset
- content/docs/kernel/contracts
- packages/spec/src/contracts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
41 | | - | |
42 | | - | |
| 41 | + | |
| 42 | + | |
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
115 | 115 | | |
116 | 116 | | |
117 | 117 | | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
118 | 139 | | |
119 | 140 | | |
120 | 141 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
294 | 294 | | |
295 | 295 | | |
296 | 296 | | |
297 | | - | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
298 | 323 | | |
299 | 324 | | |
300 | 325 | | |
| |||
0 commit comments