Skip to content

fix(auth): treat null optional fields in token responses as absent - #2462

Open
claude[bot] wants to merge 14 commits into
mainfrom
claude/oauth-tokens-null-fields-v2
Open

fix(auth): treat null optional fields in token responses as absent#2462
claude[bot] wants to merge 14 commits into
mainfrom
claude/oauth-tokens-null-fields-v2

Conversation

@claude

@claude claude Bot commented Jul 7, 2026

Copy link
Copy Markdown

Requested by Den Delimarsky · Slack thread

Before: connecting to an MCP server whose authorization server returns "refresh_token": null (or "scope": null / "id_token": null) fails with a Zod validation error ("expected string, received null") inside exchangeAuthorization/refreshAuthorization, before any tokens are saved. Worse, "expires_in": null passed validation but silently coerced to 0 (Number(null) === 0), producing a token the client treats as already expired.

After: those responses are accepted, and the null-valued fields are treated exactly as if they had been omitted. expires_in: null parses as absent, not 0.

How: the exported OAuthTokensSchema is unchanged from main — still a plain object schema that rejects nulls, with its .shape/.extend and z.input behavior intact (so specTypeSchemas/isSpecType input types are untouched). A new OAuthTokenResponseSchema wraps it in an object-level z.preprocess that removes null-valued optional members before validation — the member list is derived from the schema's shape, not hardcoded, mirroring the ElicitResult content null-leniency idiom — and the SDK's own token-response parse sites now use it: executeTokenRequest (client token exchange + refresh), the JWT-grant cross-app exchange (crossAppAccess.ts), and server-legacy's proxyProvider. Following the #2477 schema-source move, the new schema is defined in packages/core/src/auth.ts (next to OAuthTokensSchema) and forwarded through core-internal's shared/auth.ts re-export shim, so every existing import path keeps working. Removing the key (rather than mapping it to undefined) means null members are strictly absent from the parsed output, so refreshAuthorization's merge with previously-stored tokens keeps the prior refresh token; that merge is additionally hardened to { ...tokens, refresh_token: tokens.refresh_token ?? refreshToken }. access_token: null and a missing token_type are still rejected.

Revision after review: an adversarial review of the first version of this PR found that its per-field z.preprocess(v => v ?? undefined, ...) mechanic left null-valued keys present with an undefined value rather than absent, so refreshAuthorization's spread of the previous refresh token was clobbered by the explicit refresh_token: undefined — for exactly the null-emitting servers this PR targets, every refresh would have silently destroyed the stored refresh token (it also degraded z.input of the exported schema on zod <4.4). This revision fixes that by normalizing at the SDK's parse sites via the new response schema and hardening the merge.

Tests (packages/core-internal/test/shared/auth.test.ts, packages/client/test/client/auth.test.ts): strict key-absence assertions (toStrictEqual / 'field' in parsed checks, which distinguish absent keys from present-but-undefined ones), each null optional field, all-optionals-null, expires_in: null !== 0, string expires_in coercion, access_token: null and missing token_type rejection, regression pins that the exported OAuthTokensSchema still rejects nulls and keeps its ZodObject shape/extend/input behavior, a shape-driven drift guard covering every optional member, an end-to-end exchangeAuthorization with an all-null-optional response, and an end-to-end refreshAuthorization where the server returns refresh_token: null asserting the original refresh token is preserved (verified to fail against the previous mechanic). The changeset (patch for core, core-internal, client, server-legacy) is updated to describe the final mechanic; core is bumped because, after #2477, the new schema ships in core's /internal entry (core's public barrel and the authSchemas registry are deliberately unchanged).

RFC 6749 doesn't sanction null for absent members, but real-world servers emit it anyway — see #754 for the same null-emitting-server pattern (Ory Hydra) hitting the client registration response schema (that schema is intentionally left out of scope here).

Scope of the normalization: this change deliberately covers only RFC 6749 §5.1 token responses at the SDK's token parse sites. Two sibling parse sites intentionally remain strict about null members: the OAuth error-response schema (OAuthErrorResponseSchema, used by parseErrorResponse) and the RFC 8693 §2.2.1 ID-JAG token-exchange schema (IdJagTokenExchangeResponseSchema in crossAppAccess.ts). That is consistent with the #754 registration-schema carve-out above; either can be revisited if field evidence of null-emitting servers turns up for those responses.

Sibling PR with the same fix against v1.x: #2461.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KXnNnp3fxQYR5HF9BUVhYP


Generated by Claude Code

Update (2026-08-05)

OAuthTokenResponseSchema is now also exported from @modelcontextprotocol/core's public root, since the sibling v1.x PR #2461 makes it public v1 API and migrating code needs a v2 home for it. The v1-to-v2 codemod's AUTH_SCHEMA_NAMES allowlist now includes the name, so imports of it from @modelcontextprotocol/sdk/shared/auth.js rewrite to @modelcontextprotocol/core instead of landing on a package that does not export it. Drift-guard tests in core and the codemod pin the new membership, and the changeset now covers the codemod package.

Versioning note for maintainers: the new public core export arguably warrants a minor per this repo's precedent for export-surface additions (#2354, #2513). However, flipping the changeset's core entry to minor currently major-bumps @modelcontextprotocol/express/fastify/hono/node to 3.0.0: they peer-depend on @modelcontextprotocol/server via workspace:^, server rides the fixed group to 2.1.0, and .changeset/config.json does not set onlyUpdatePeerDependentsWhenOutOfRange: true, so changesets' default major-bumps peer-dependents on any non-patch peer bump. The changeset is left at patch pending a maintainer decision: either add that config flag and flip core to minor, or accept the additive export shipping in a patch.

claude added 2 commits July 7, 2026 18:58
Some authorization servers serialize absent optional members as JSON null,
which RFC 6749 does not sanction but is common in the wild. Previously,
OAuthTokensSchema rejected refresh_token/scope/id_token when null with a
Zod validation error, and expires_in: null silently coerced to 0
(Number(null) === 0), producing a token the client treated as already
expired. This broke token exchange and refresh against such servers.

Normalize null optional members to absent (undefined) before validation.
Inferred output types are unchanged (string | undefined, number |
undefined), so OAuthTokens consumers are unaffected.

Related: #754 (same null-serialization pattern hitting the client
registration schema).
@changeset-bot

changeset-bot Bot commented Jul 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0e4a6f1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/core Patch
@modelcontextprotocol/core-internal Patch
@modelcontextprotocol/client Patch
@modelcontextprotocol/server-legacy Patch
@modelcontextprotocol/codemod Patch
@modelcontextprotocol/server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jul 7, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@2462

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/@modelcontextprotocol/codemod@2462

@modelcontextprotocol/core

npm i https://pkg.pr.new/@modelcontextprotocol/core@2462

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@2462

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/@modelcontextprotocol/server-legacy@2462

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@2462

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2462

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@2462

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@2462

commit: 0e4a6f1

claude added 3 commits July 7, 2026 20:50
…in OAuthTokensSchema

Rework after review: the per-field z.preprocess mechanic left null-valued
keys present-as-undefined in the parsed output rather than absent, so
refreshAuthorization's spread over the previous refresh token was clobbered
by the explicit refresh_token: undefined — for exactly the null-emitting
servers this fix targets, every refresh silently destroyed the stored
refresh token. It also degraded z.input of the exported schema on zod <4.4.

- Revert OAuthTokensSchema to its original plain object definition,
  restoring .shape/.extend/z.input for consumers (and the derived
  specTypeSchemas/isSpecType input types).
- Add OAuthTokenResponseSchema, which removes null-valued optional members
  (derived from the schema shape, not a hardcoded field list) before
  validation, mirroring ElicitResult's null-leniency idiom, and use it at
  the SDK's own token-response parse sites: executeTokenRequest, the
  JWT-grant cross-app exchange, and server-legacy's proxyProvider.
- Harden refreshAuthorization's merge to
  { ...tokens, refresh_token: tokens.refresh_token ?? refreshToken } so a
  present-but-undefined key can never clobber the preserved token.
- Tests now pin strict key absence (toStrictEqual / 'in' checks), null
  access_token and missing token_type rejection, the exported schema's
  unchanged shape/extend/input behavior, a shape-driven drift guard for
  future optional members, and a refreshAuthorization e2e with
  refresh_token: null that fails against the previous mechanic.

Related: #754 (same null-serialization pattern hitting the client
registration schema).
Stripping a null scope from a token response makes it indistinguishable
from an omitted scope, which RFC 6749 §5.1 defines as an assertion that
the granted scope is identical to the requested scope. Document that
consumers must not infer the granted scope from its absence and should
use token introspection for the authoritative grant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXnNnp3fxQYR5HF9BUVhYP
@localden
localden marked this pull request as ready for review August 5, 2026 05:09
@localden
localden requested a review from a team as a code owner August 5, 2026 05:09
Comment thread packages/core-internal/src/shared/auth.ts Outdated
Comment thread packages/client/src/client/auth.ts
Comment thread .changeset/oauth-tokens-null-optional-fields.md
The changeset no longer bumps @modelcontextprotocol/core: its exported
OAuthTokensSchema is deliberately unchanged and OAuthTokenResponseSchema
is not part of core's shipped surface (not exported from core's index,
not in core-internal's authSchemas registry that core's export group is
test-pinned to), matching the sibling changeset convention of patching
core-internal without core.

The OAuthTokenResponseSchema JSDoc no longer claims the SDK never uses
tokens.scope for authorization decisions: the 403 insufficient_scope
step-up path feeds tokens.scope into isStrictScopeSuperset, where an
absent scope is treated as the empty set and forces a fresh
authorization request rather than a refresh.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXnNnp3fxQYR5HF9BUVhYP

@claude claude Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new issues found on this revision — the latest commit resolves two of the three items from my earlier review (the changeset no longer bumps @modelcontextprotocol/core, and the OAuthTokenResponseSchema JSDoc now correctly describes the step-up path's conservative use of tokens.scope) — but since this touches OAuth token handling across client, cross-app, and proxy-provider flows, it should get a human maintainer's sign-off.

What was reviewed on this pass:

  • Confirmed the null-stripping preprocess only removes keys whose member schema accepts undefined, so access_token: null / missing token_type still reject, and .strip() semantics are preserved.
  • Confirmed the refreshAuthorization merge hardening plus the strict key-absence tests pin the clobbering regression from the first revision.
  • The one item still open from my earlier review is the sibling-parse-site scoping question (OAuthErrorResponseSchema, IdJagTokenExchangeResponseSchema) — deliberate scoping there seems defensible; that's a maintainer call, not a blocker.
Extended reasoning...

Overview

This PR makes the SDK's own OAuth token-response parse sites tolerant of authorization servers that serialize absent optional members as JSON null (nonconformant with RFC 6749 §5.1 but common in the wild, e.g. Ory Hydra per #754). It introduces OAuthTokenResponseSchema in packages/core-internal/src/shared/auth.ts — an object-level z.preprocess that deletes null-valued keys whose member schema accepts undefined, then delegates to the unchanged OAuthTokensSchema — and switches four wire-parse sites to it (executeTokenRequest, exchangeJwtAuthGrant, and both proxyProvider exchanges). It also hardens refreshAuthorization's merge to { ...tokens, refresh_token: tokens.refresh_token ?? refreshToken } so a present-but-undefined key can never clobber the stored refresh token.

Security risks

This is auth code, and the change is a leniency change in token-response validation. The risk surface is narrow: only null-valued optional members are stripped (the drift-guard test derives the member list from the schema shape and the tests pin that access_token: null and a missing token_type still reject), the exported OAuthTokensSchema is unchanged, and expires_in: null now parses as absent rather than silently coercing to 0. The semantic subtlety — a stripped scope: null becoming indistinguishable from RFC 6749 §5.1's "granted = requested" assertion — is documented in both the JSDoc and the changeset, and the step-up path treats absence conservatively (forces re-authorization). I did not find a way for the normalization to widen what a malicious AS can assert.

Level of scrutiny

High — this is production OAuth flow code shipped in three packages, and it deliberately relaxes validation against RFC-nonconformant servers. That trade-off (interop vs. strictness), plus the introduction of a new internal schema alongside the exported one, is exactly the kind of judgment a human maintainer should ratify, per the repo's minimalism/burden-of-proof conventions. The mechanics themselves are sound and unusually well-tested (strict key-absence assertions, drift guard over future optional members, end-to-end refresh-token-preservation regression test).

Other factors

This revision responds to prior review: commit e3d7aab drops @modelcontextprotocol/core from the changeset frontmatter (verified — the frontmatter now lists only core-internal/client/server-legacy, matching changeset-bot's 3-package report) and corrects the JSDoc's overbroad "never uses tokens.scope" claim to accurately describe the 403 insufficient_scope step-up behavior. The remaining open item from my earlier inline comments — sibling parse sites (OAuthErrorResponseSchema at both executeTokenRequest error branches, IdJagTokenExchangeResponseSchema in requestJwtAuthorizationGrant) still rejecting null-valued optionals — is unchanged; the PR description explicitly scopes out the registration schema, and whether to extend the normalization to error/exchange responses is a scoping decision for the maintainer rather than a defect in what this PR ships. No bugs were found by this run's bug-hunting pass.

Resolves the conflict with #2477 (schema sources moved into
@modelcontextprotocol/core): OAuthTokenResponseSchema now lives in
packages/core/src/auth.ts next to OAuthTokensSchema, and core-internal's
shared/auth.ts re-export shim forwards it, per the schemaShims purity
pins. The normalization design is unchanged; all parse sites keep
importing from @modelcontextprotocol/core-internal, which now forwards
from core.

Because the schema is now part of core's shipped /internal entry, the
changeset re-adds the @modelcontextprotocol/core patch bump that was
dropped earlier today (that removal was correct against the pre-#2477
base, where core's shipped surface was untouched).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXnNnp3fxQYR5HF9BUVhYP
Comment thread packages/core/src/auth.ts
…uide

The guide claimed core's exports are the exact constants the SDK
validates OAuth payloads against, and pointed gateway authors at
OAuthTokensSchema for token responses. Since the SDK's own token
parse sites now validate with OAuthTokenResponseSchema (an internal
null-tolerant wrapper around OAuthTokensSchema), qualify the intro
and note the wrapper next to the OAuth naming-convention list.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXnNnp3fxQYR5HF9BUVhYP

@claude claude Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found no bugs on the current revision, and the earlier review findings (JSDoc scope-usage claim, the core changeset bump, and the wire-schemas doc drift) are all addressed by e3d7aab, 0c105c1, and 791c379 — but this changes OAuth token-exchange/refresh behavior across client, server-legacy, and core's shipped /internal surface, so a maintainer should make the final call.

What was reviewed this run:

  • The null-stripping preprocess in OAuthTokenResponseSchema — only optional members with a null value are removed; access_token/token_type stay strict, and expires_in: null no longer coerces to 0.
  • Migration completeness — grepped packages/*/src for surviving OAuthTokensSchema wire-parse sites: none remain (error-response and ID-JAG schemas stay strict by documented design).
  • The refreshAuthorization merge reorder — tokens.refresh_token ?? refreshToken preserves the stored refresh token, pinned by an end-to-end test.
Extended reasoning...

Overview

This PR makes the SDK tolerate authorization servers that serialize absent optional token-response members as JSON null (Ory-Hydra-style, cf. #754). It adds OAuthTokenResponseSchema — an object-level z.preprocess wrapper around the unchanged OAuthTokensSchema that deletes null-valued optional keys before validation — defined in packages/core/src/auth.ts and forwarded through core-internal's re-export shim. All four SDK token parse sites migrate to it (executeTokenRequest, exchangeJwtAuthGrant, and both proxyProvider exchanges), and refreshAuthorization's merge is hardened to { ...tokens, refresh_token: tokens.refresh_token ?? refreshToken }. Test coverage is unusually thorough: strict key-absence assertions, per-field and all-null cases, expires_in null-vs-0, required-field rejection pins, a shape-driven drift guard, and end-to-end exchange/refresh tests.

Security risks

Low. The change loosens parsing only for optional members whose value is null; required members (access_token, token_type) still reject null/absence, so no token can be minted from a degenerate response. The refresh-merge reorder is the one behavioral edge — verified that the parsed response's keys are strictly absent (not present-but-undefined) so the stored refresh token can't be clobbered, and the test suite pins exactly that failure mode from the PR's first revision. Still, this is the OAuth client token path, which is squarely auth-sensitive code.

Level of scrutiny

High — token exchange, refresh, and the cross-app JWT-grant flow are production auth paths spanning three published packages, and the PR adds a symbol to core's shipped /internal surface (with a deliberate decision, recorded in 0c105c1, to re-add the core patch bump after #2477 moved the schema source into core). Those are exactly the API-surface and release-hygiene calls a maintainer should ratify, per this repo's minimalism/burden-of-proof conventions.

Other factors

All three earlier inline findings from prior review runs are resolved in the current head: the JSDoc no longer over-claims about tokens.scope (e3d7aab), the core changeset bump is now justified by the schema shipping in core's /internal entry post-#2477 (0c105c1), and docs/advanced/wire-schemas.md now documents the null-tolerant wrapper (791c379). The remaining strict sibling schemas (OAuthErrorResponseSchema, IdJagTokenExchangeResponseSchema) are an explicitly documented scope carve-out in the PR description rather than an oversight. Nothing here blocks the PR; deferring only because auth-path behavior changes and a new shipped-surface symbol warrant human sign-off.

…to-v2 codemod

The sibling v1.x PR makes OAuthTokenResponseSchema public v1 API, so the
codemod must route its imports somewhere that exports it. Core's root barrel
now exports the schema, AUTH_SCHEMA_NAMES includes it, and the drift-guard
tests pin the new membership.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q2uDjqaRGTv8vFth6iiU17
Comment thread docs/advanced/wire-schemas.md Outdated
…ctness

OAuthTokenResponseSchema is a public core export as of the codemod
mapping commit, so the wire-schemas guide no longer calls it internal
and now points raw-wire consumers at it directly. Both the guide and
the changeset also stop claiming the plain OAuthTokensSchema rejects
nulls outright: it rejects null for its string-typed members, but
expires_in: null coerces to 0 there (verified empirically against the
branch schemas).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXnNnp3fxQYR5HF9BUVhYP
Comment thread packages/client/src/client/auth.ts
Comment thread packages/core/src/auth.ts Outdated
…nalogy

RFC 6749 sends no scope parameter on refresh, so a refresh response
without a scope member (including one whose scope: null was stripped
by OAuthTokenResponseSchema) asserts the grant is unchanged. The
refresh branch previously saved the response as-is, erasing the
stored scope; after a restart the insufficient_scope step-up would
compute its union without the original grant and force interactive
re-authorization. Mirror the refresh_token hardening: preserve the
stored scope when the response has none, while a scope the server
does return stays authoritative. Tests pin null, omitted, and
server-narrowed scope on the auth() refresh path.

Also reword the OAuthTokenResponseSchema JSDoc: it claimed the key
removal mirrors ElicitResult's content null normalization, but that
mechanic maps null to a present-but-undefined member — exactly what
this schema rejects. The analogy now states the shared goal and the
stronger mechanic.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXnNnp3fxQYR5HF9BUVhYP
Comment thread packages/client/src/client/auth.ts Outdated
The scope-preservation merge wrote scope: undefined into the
saveTokens payload when both the refresh response and the stored
tokens lacked a scope — the present-but-undefined key shape this PR's
normalization exists to prevent. A conditional spread now includes
the scope member only when a preserved value exists. New test pins
the no-scope-anywhere refresh payload with toStrictEqual (asserting
on the refresh save, not the SEP-2352 issuer back-stamp call).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXnNnp3fxQYR5HF9BUVhYP
Comment thread .changeset/oauth-tokens-null-optional-fields.md
claude added 2 commits August 5, 2026 07:55
… export

Additive public API on core's root barrel is a minor under semver, and
core's changelog ships export-surface additions as minor (#2354, #2513).
The changeset fixed group lifts core, client, server, server-legacy, and
codemod together to 2.1.0.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXnNnp3fxQYR5HF9BUVhYP
Flipping core to minor major-bumps express, fastify, hono, and node to
3.0.0: they peer-depend on server via workspace:^, server rides the
fixed group to minor, and .changeset/config.json does not set
onlyUpdatePeerDependentsWhenOutOfRange, so changesets' default
major-bumps peer-dependents on any non-patch peer bump. The additive
public export arguably warrants a minor per repo precedent, but that
blast radius is a maintainer decision; the PR body records the options.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXnNnp3fxQYR5HF9BUVhYP

@claude claude Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new issues found this run — the follow-up commits resolve all of my earlier inline findings, but this is OAuth token-handling code with an explicitly open maintainer decision (changeset bump level), so it needs a human sign-off rather than a shadow approval.

What was reviewed this run:

  • Confirmed the refresh save site now preserves the stored scope via a conditional spread, keeping scope strictly absent when neither side has one (auth.ts:1323-1324), with tests covering all four scope cases.
  • Confirmed the OAuthTokenResponseSchema JSDoc now correctly contrasts its key-removal mechanic with ElicitResult's null→undefined mapping, and that wire-schemas.md and the changeset now accurately describe the expires_in: null coercion caveat in the strict schema.
  • Re-checked the preprocess: only optional members are stripped (required access_token/token_type still reject null), non-object inputs pass through, and the codemod allowlist/drift guards pin the new public export.
Extended reasoning...

Overview

This PR makes the SDK tolerate OAuth token responses whose optional members are serialized as JSON null (nonconformant but common, e.g. Ory-Hydra-style servers). It adds OAuthTokenResponseSchema — an object-level z.preprocess wrapper that removes null-valued optional keys before OAuthTokensSchema validates — and switches all four SDK token parse sites to it (client exchange/refresh, JWT-grant cross-app exchange, server-legacy proxy provider). It also hardens the refresh merge (refresh_token and scope preservation), exports the new schema publicly from core, updates the codemod allowlist, and documents everything in wire-schemas.md and a changeset.

Security risks

This is auth-flow code, so the risk surface is real: token persistence, refresh-token preservation, and scope handling. The changes are conservative — required members (access_token, token_type) still reject null, only optional members are normalized, and the merge hardening only fills absent values from previously-stored tokens (a server-returned scope remains authoritative, so no scope-widening path is introduced). Prior review rounds on this PR specifically adversarially examined refresh-token clobbering and scope erasure; the current revision fixes both with strict key-absence tests (toStrictEqual / 'key' in assertions).

Level of scrutiny

High. OAuth client/server token handling is security-sensitive per the approval guidelines, which alone rules out shadow approval regardless of correctness. Additionally, the PR body explicitly leaves a versioning decision to maintainers: the new public core export arguably warrants a minor bump, but flipping it currently major-bumps the middleware packages via changesets' peer-dependent default — a repo-policy call no bot should make.

Other factors

Test coverage is thorough: unit tests for the new schema (per-field null stripping, key-absence pins, expires_in coercion behavior, a shape-driven drift guard for future members), end-to-end exchangeAuthorization/refreshAuthorization/auth() tests including all four scope-preservation cases, and drift guards in core and the codemod pinning the export surface. All previous inline findings from earlier review runs (doc drift, JSDoc analogy inversion, scope erasure, present-but-undefined scope key, bump level) were verified as addressed in the follow-up commits. The remaining open item is purely the bump-level maintainer call documented in the PR body.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant