@code-yeongyu/senpi 2026.11.10-1 → 2026.11.10-3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/bundle/chunks/{args-77GYOPEO.js → args-HFYB6URH.js} +1 -1
  3. package/dist/bundle/chunks/{azure-openai-responses-YFZ734T3.js → azure-openai-responses-MNN2TINI.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-ZUQCYIRY.js → chunk-2ODGHBUD.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-YPAQXRP3.js → chunk-47T6YMI2.js} +2 -2
  6. package/dist/bundle/chunks/{chunk-I5ASHSLZ.js → chunk-4JMGWVRH.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-MSDBBOB2.js → chunk-5H43O4MA.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-3IYWH6RO.js → chunk-6Q7Y2G5A.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-X4XWAB3J.js → chunk-AC6MDRAE.js} +81 -81
  10. package/dist/bundle/chunks/{chunk-33MZJTQ3.js → chunk-B4WD5XHP.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-V5FSENW6.js → chunk-COKX65TE.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-LHEP3ACI.js → chunk-DEQLHN2J.js} +2 -2
  13. package/dist/bundle/chunks/{chunk-EJB5E2JK.js → chunk-DHAAWL2H.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-2EJ4BGBM.js → chunk-DHORIIVQ.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-GPXZTSEX.js → chunk-DIHIEJFV.js} +3 -3
  16. package/dist/bundle/chunks/{chunk-NH7SYIGN.js → chunk-DLPV4B3R.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-RCHIMH7N.js → chunk-DQY26AX3.js} +1 -1
  18. package/dist/bundle/chunks/{chunk-Y7Z7XPUF.js → chunk-DZQA5LVN.js} +1 -1
  19. package/dist/bundle/chunks/{chunk-2CNSCMYC.js → chunk-E5O4SP5I.js} +1 -1
  20. package/dist/bundle/chunks/{chunk-3Q5ZENSD.js → chunk-E6ZBTSC6.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-HI5JSLG5.js → chunk-H3VJVKUK.js} +4 -4
  22. package/dist/bundle/chunks/{chunk-L6NRDUWJ.js → chunk-HZDKQOWB.js} +1 -1
  23. package/dist/bundle/chunks/chunk-IREUIMFE.js +11 -0
  24. package/dist/bundle/chunks/{chunk-FAFIGBUD.js → chunk-JSD6O3RJ.js} +2 -2
  25. package/dist/bundle/chunks/{chunk-BELNOPAI.js → chunk-L3KVP7VJ.js} +3 -3
  26. package/dist/bundle/chunks/{chunk-7MN7YNO5.js → chunk-L7DGTA3L.js} +1 -1
  27. package/dist/bundle/chunks/{chunk-J2NGJKXL.js → chunk-LXINMDMX.js} +1 -1
  28. package/dist/bundle/chunks/{chunk-H33A33M4.js → chunk-M2VJVJMK.js} +1 -1
  29. package/dist/bundle/chunks/{chunk-ARUV3US4.js → chunk-NN7NESW2.js} +1 -1
  30. package/dist/bundle/chunks/{chunk-2U6HXWXS.js → chunk-NNQ34ZDW.js} +1 -1
  31. package/dist/bundle/chunks/chunk-NQKCJQHB.js +2 -0
  32. package/dist/bundle/chunks/{chunk-TGW4R7SI.js → chunk-O5WJVIEU.js} +1 -1
  33. package/dist/bundle/chunks/{chunk-O3FG6HML.js → chunk-PJI5GT7U.js} +1 -1
  34. package/dist/bundle/chunks/{chunk-BPNSOZ2C.js → chunk-QUVSIW2R.js} +1 -1
  35. package/dist/bundle/chunks/{chunk-WXYMAUFX.js → chunk-QX7WD3IM.js} +1 -1
  36. package/dist/bundle/chunks/{chunk-Y4ONXQMR.js → chunk-RGHVPUUX.js} +1 -1
  37. package/dist/bundle/chunks/{chunk-RYPM5BZQ.js → chunk-S7NAJQB3.js} +1 -1
  38. package/dist/bundle/chunks/{chunk-LT353NSF.js → chunk-T4CUD7RF.js} +1 -1
  39. package/dist/bundle/chunks/{chunk-J7FW7MED.js → chunk-TCZORL3V.js} +1 -1
  40. package/dist/bundle/chunks/{chunk-KU72IKV5.js → chunk-VGSWOLIY.js} +1 -1
  41. package/dist/bundle/chunks/{chunk-DBQBJTMF.js → chunk-VUY4BZDI.js} +1 -1
  42. package/dist/bundle/chunks/chunk-XI6THO4F.js +2 -0
  43. package/dist/bundle/chunks/{chunk-THYEZBUG.js → chunk-Z2C5N2LM.js} +2 -2
  44. package/dist/bundle/chunks/{cli-main-VG3C2EXI.js → cli-main-ZTT3N4DO.js} +1 -1
  45. package/dist/bundle/chunks/{credential-accounts-LVVPKFEY.js → credential-accounts-5KHVGSCY.js} +1 -1
  46. package/dist/bundle/chunks/{dispatch-EP35KKJ3.js → dispatch-YUYZQXRP.js} +1 -1
  47. package/dist/bundle/chunks/{hand-entry-3LQKUTYS.js → hand-entry-UV5XPMBF.js} +3 -3
  48. package/dist/bundle/chunks/{headless-host-HKGNJGYT.js → headless-host-53UMJMM6.js} +1 -1
  49. package/dist/bundle/chunks/{health-write-FBF74QJ7.js → health-write-JLSF5EVV.js} +1 -1
  50. package/dist/bundle/chunks/{help-fast-path-5SCPY3OH.js → help-fast-path-552QLMWT.js} +1 -1
  51. package/dist/bundle/chunks/{host-command-CCASS7W4.js → host-command-BZN7WYYV.js} +1 -1
  52. package/dist/bundle/chunks/{host-lifecycle-WWC7HXQ6.js → host-lifecycle-TNHASSFJ.js} +1 -1
  53. package/dist/bundle/chunks/host-lifecycle.js +1 -1
  54. package/dist/bundle/chunks/{host-session-command-YJB2FLKU.js → host-session-command-O5WYHZXH.js} +1 -1
  55. package/dist/bundle/chunks/{host-socket-request-DMYXC56U.js → host-socket-request-QN4Q2P5E.js} +1 -1
  56. package/dist/bundle/chunks/{interactive-mode-CUN3WMUF.js → interactive-mode-OCU4NDPC.js} +1 -1
  57. package/dist/bundle/chunks/{login-health-EVXVLHB2.js → login-health-TOSQT5RG.js} +1 -1
  58. package/dist/bundle/chunks/{main-P7XRC2OI.js → main-CNDP6HCF.js} +1 -1
  59. package/dist/bundle/chunks/{multi-session-host-244X3P6L.js → multi-session-host-ODGJAGJP.js} +11 -8
  60. package/dist/bundle/chunks/openai-codex-responses-VRGOAFEG.js +10 -0
  61. package/dist/bundle/chunks/{openai-responses-NIVBWE2H.js → openai-responses-I7PXRWCM.js} +2 -2
  62. package/dist/bundle/chunks/{package-manager-cli-UVWJOQA3.js → package-manager-cli-HSPUG4RJ.js} +1 -1
  63. package/dist/bundle/chunks/{role-HQUN4XXK.js → role-AEOLJRTP.js} +1 -1
  64. package/dist/bundle/chunks/{rotation-stream-F3BEMXBE.js → rotation-stream-E3FJFHXJ.js} +1 -1
  65. package/dist/bundle/chunks/rpc-mode-KOZPXU3U.js +2 -0
  66. package/dist/bundle/chunks/{rpc-monitor-source-C6VQ3BC4.js → rpc-monitor-source-PGTDSGWP.js} +1 -1
  67. package/dist/bundle/chunks/{run-attached-tui-R3RB5P7G.js → run-attached-tui-4AENRP7P.js} +1 -1
  68. package/dist/bundle/chunks/{schedule-command-5FG2OKSR.js → schedule-command-SG44FE23.js} +1 -1
  69. package/dist/bundle/chunks/session-control-endpoint-5A24JYPE.js +3 -0
  70. package/dist/bundle/chunks/{session-picker-2FZSNFS7.js → session-picker-73YRS3GE.js} +1 -1
  71. package/dist/bundle/chunks/{session-slot-CZGQQCOC.js → session-slot-RH7JLO3F.js} +1 -1
  72. package/dist/bundle/chunks/session-worker.js +199 -196
  73. package/dist/bundle/chunks/{state-store-S7QAESIF.js → state-store-IECVTYXC.js} +1 -1
  74. package/dist/bundle/cli.js +2 -2
  75. package/dist/bundle/host-supervisor.js +1 -1
  76. package/dist/bundle/index.js +1 -1
  77. package/dist/bundle/rpc-entry.js +1 -1
  78. package/dist/bundle/runtime-manifest.json +1 -1
  79. package/dist/core/agent-session-runtime.d.ts +2 -0
  80. package/dist/core/agent-session-runtime.js +5 -0
  81. package/dist/core/agent-session.d.ts +13 -3
  82. package/dist/core/agent-session.js +89 -18
  83. package/dist/core/assistant-error-redaction.d.ts +9 -0
  84. package/dist/core/assistant-error-redaction.js +27 -0
  85. package/dist/core/config-reference-redaction.js +90 -10
  86. package/dist/core/context-files.d.ts +10 -0
  87. package/dist/core/context-files.js +58 -12
  88. package/dist/core/egress/wire.d.ts +13 -0
  89. package/dist/core/extensions/builtin/config-reload/change-groups.js +6 -2
  90. package/dist/core/extensions/builtin/mcp/auth/catalog-identity.js +32 -8
  91. package/dist/core/extensions/builtin/mcp/auth/oauth-grant.d.ts +43 -0
  92. package/dist/core/extensions/builtin/mcp/auth/oauth-grant.js +196 -0
  93. package/dist/core/extensions/builtin/mcp/auth/oauth-provider.d.ts +4 -17
  94. package/dist/core/extensions/builtin/mcp/auth/oauth-provider.js +64 -75
  95. package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.d.ts +1 -1
  96. package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.js +25 -4
  97. package/dist/core/extensions/builtin/mcp/auth/oauth.js +10 -7
  98. package/dist/core/extensions/builtin/mcp/auth/token-store.d.ts +18 -0
  99. package/dist/core/extensions/builtin/mcp/auth/token-store.js +3 -0
  100. package/dist/core/extensions/builtin/mcp/catalog-cache.js +7 -6
  101. package/dist/core/extensions/builtin/mcp/catalog.js +3 -2
  102. package/dist/core/extensions/builtin/mcp/invocation.js +6 -0
  103. package/dist/core/extensions/builtin/mcp/service-register.js +16 -2
  104. package/dist/core/extensions/builtin/mcp/service-tools-changed.js +61 -54
  105. package/dist/core/extensions/builtin/mcp/startup-race.d.ts +2 -0
  106. package/dist/core/extensions/builtin/mcp/startup-race.js +25 -7
  107. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-2.js +6 -6
  108. package/dist/core/extensions/loader.js +24 -4
  109. package/dist/core/extensions/runner.d.ts +9 -1
  110. package/dist/core/extensions/runner.js +9 -4
  111. package/dist/core/extensions/types.d.ts +17 -3
  112. package/dist/core/hand/hook-advertise.d.ts +41 -0
  113. package/dist/core/hand/hook-advertise.js +98 -0
  114. package/dist/core/hand/hook-contract.d.ts +66 -0
  115. package/dist/core/hand/hook-contract.js +138 -0
  116. package/dist/core/open-timing.d.ts +2 -0
  117. package/dist/core/open-timing.js +7 -1
  118. package/dist/core/resource-loader.js +2 -0
  119. package/dist/core/retry-fallback/log.d.ts +7 -1
  120. package/dist/core/retry-fallback/log.js +82 -5
  121. package/dist/core/retry-fallback/model-config-error.js +2 -1
  122. package/dist/core/session-manager.d.ts +2 -0
  123. package/dist/core/session-manager.js +15 -1
  124. package/dist/core/settings-manager.d.ts +6 -0
  125. package/dist/core/settings-manager.js +8 -0
  126. package/dist/core/turn-origin.d.ts +19 -13
  127. package/dist/core/turn-origin.js +34 -30
  128. package/dist/core/ultrafast-lanes.d.ts +1 -0
  129. package/dist/core/ultrafast-lanes.js +4 -2
  130. package/dist/modes/interactive/components/footer.d.ts +23 -5
  131. package/dist/modes/interactive/components/footer.js +45 -16
  132. package/dist/modes/interactive/interactive-mode.js +9 -1
  133. package/dist/modes/interactive/session-control-rpc-commands.d.ts +1 -1
  134. package/dist/modes/interactive/session-control-rpc-commands.js +5 -0
  135. package/dist/modes/rpc/admit-external-message-command.d.ts +2 -2
  136. package/dist/modes/rpc/admit-external-message-command.js +2 -10
  137. package/dist/modes/rpc/compact-admission-validation.js +4 -1
  138. package/dist/modes/rpc/connection-handler.d.ts +2 -0
  139. package/dist/modes/rpc/connection-handler.js +20 -8
  140. package/dist/modes/rpc/connection-roles.d.ts +2 -0
  141. package/dist/modes/rpc/connection-roles.js +4 -0
  142. package/dist/modes/rpc/custom-capability.d.ts +4 -0
  143. package/dist/modes/rpc/custom-capability.js +4 -0
  144. package/dist/modes/rpc/delegated-drive-origin.d.ts +6 -6
  145. package/dist/modes/rpc/delegated-drive-origin.js +7 -8
  146. package/dist/modes/rpc/delegation-declaration.d.ts +2 -0
  147. package/dist/modes/rpc/delegation-declaration.js +5 -1
  148. package/dist/modes/rpc/effect-commands.d.ts +21 -0
  149. package/dist/modes/rpc/host-capabilities.d.ts +1 -0
  150. package/dist/modes/rpc/host-capabilities.js +5 -1
  151. package/dist/modes/rpc/host-observe-request.d.ts +2 -0
  152. package/dist/modes/rpc/host-observe-request.js +6 -1
  153. package/dist/modes/rpc/host-park-ledger.d.ts +19 -0
  154. package/dist/modes/rpc/host-park-ledger.js +110 -0
  155. package/dist/modes/rpc/lifecycle-delivery.js +4 -1
  156. package/dist/modes/rpc/loopless-tool-handlers.d.ts +6 -0
  157. package/dist/modes/rpc/loopless-tool-handlers.js +14 -1
  158. package/dist/modes/rpc/media-placeholders.js +1 -0
  159. package/dist/modes/rpc/method-classes-schema-host.d.ts +42 -1
  160. package/dist/modes/rpc/method-classes-schema-host.js +24 -21
  161. package/dist/modes/rpc/method-classes-schema.d.ts +42 -1
  162. package/dist/modes/rpc/method-classes-schema.js +5 -2
  163. package/dist/modes/rpc/method-classes.d.ts +2 -0
  164. package/dist/modes/rpc/method-classes.js +3 -0
  165. package/dist/modes/rpc/multi-session-host.js +1 -1
  166. package/dist/modes/rpc/rpc-host-command-types.d.ts +14 -1
  167. package/dist/modes/rpc/rpc-host-response-types.d.ts +21 -0
  168. package/dist/modes/rpc/session-attachment-gate.js +2 -0
  169. package/dist/modes/rpc/session-binding.d.ts +1 -0
  170. package/dist/modes/rpc/session-binding.js +6 -1
  171. package/dist/modes/rpc/session-connection-context.d.ts +2 -0
  172. package/dist/modes/rpc/session-connection-context.js +4 -0
  173. package/dist/modes/rpc/session-registry-attach.d.ts +2 -0
  174. package/dist/modes/rpc/session-registry-attach.js +15 -11
  175. package/dist/modes/rpc/session-registry-claims.js +2 -0
  176. package/dist/modes/rpc/session-registry-open.js +13 -7
  177. package/dist/modes/rpc/session-registry-opening.js +2 -1
  178. package/dist/modes/rpc/session-registry-types.d.ts +4 -0
  179. package/dist/modes/rpc/session-registry.d.ts +3 -0
  180. package/dist/modes/rpc/session-registry.js +27 -3
  181. package/dist/modes/rpc/session-router-close.js +1 -0
  182. package/dist/modes/rpc/session-router-context.d.ts +4 -2
  183. package/dist/modes/rpc/session-router-context.js +3 -0
  184. package/dist/modes/rpc/session-router-host-reads.js +1 -0
  185. package/dist/modes/rpc/session-router-occupancy.js +10 -1
  186. package/dist/modes/rpc/session-router-open-result.d.ts +12 -0
  187. package/dist/modes/rpc/session-router-open-result.js +63 -0
  188. package/dist/modes/rpc/session-router-open.d.ts +3 -0
  189. package/dist/modes/rpc/session-router-open.js +15 -63
  190. package/dist/modes/rpc/session-router-park.d.ts +15 -0
  191. package/dist/modes/rpc/session-router-park.js +176 -0
  192. package/dist/modes/rpc/session-router-routing.d.ts +1 -1
  193. package/dist/modes/rpc/session-router-routing.js +7 -0
  194. package/dist/modes/rpc/session-status-source.js +2 -2
  195. package/docs/engine-socket/fixtures/admission-routes/h-legacy.json +1 -1
  196. package/docs/engine-socket/fixtures/rpc-inbound-samples.json +22 -2
  197. package/docs/extensions.md +10 -0
  198. package/docs/rpc.md +50 -0
  199. package/docs/settings.md +1 -0
  200. package/docs/usage.md +1 -1
  201. package/native/socket-secret/prebuilds/win32-arm64/senpi_socket_secret.win32-arm64.node +0 -0
  202. package/native/socket-secret/prebuilds/win32-x64/senpi_socket_secret.win32-x64.node +0 -0
  203. package/package.json +6 -6
  204. package/dist/bundle/chunks/chunk-436DNN4Z.js +0 -2
  205. package/dist/bundle/chunks/chunk-DLAY7CIU.js +0 -2
  206. package/dist/bundle/chunks/chunk-XH2MWTXA.js +0 -11
  207. package/dist/bundle/chunks/openai-codex-responses-ABB2J43U.js +0 -10
  208. package/dist/bundle/chunks/rpc-mode-G4I3HHPB.js +0 -2
  209. package/dist/bundle/chunks/session-control-endpoint-XWCFLJYC.js +0 -3
@@ -0,0 +1,43 @@
1
+ import type { OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
2
+ import type { McpAuthorizationProof, McpClientRegistration, McpStoredAuth } from "./token-store.ts";
3
+ export type McpGrantRecord = Pick<McpStoredAuth, "accessToken" | "refreshToken" | "issuer" | "resource" | "scope" | "refreshScope"> & {
4
+ readonly authorization?: unknown;
5
+ readonly clientInfo?: Pick<McpClientRegistration, "client_id" | "client_secret" | "token_endpoint_auth_method" | "issuer">;
6
+ readonly discoveryState?: {
7
+ readonly authorizationServerUrl?: string | URL;
8
+ };
9
+ };
10
+ export interface McpGrantContext {
11
+ readonly serverName: string;
12
+ readonly serverUrl: string;
13
+ readonly agentDir: string;
14
+ readonly clientId?: string;
15
+ readonly clientMetadataUrl?: string;
16
+ }
17
+ export declare function storedGrantIssuer(record: McpGrantRecord | undefined): string | undefined;
18
+ export declare function freezeGrantAttribution(record: McpStoredAuth | undefined): McpStoredAuth;
19
+ export declare function storedAuthToTokens(record: McpStoredAuth | undefined, now?: number): OAuthTokens | undefined;
20
+ /** Maps one response; the caller decides which preceding fields it is entitled to inherit. */
21
+ export declare function mergeTokensIntoStoredAuth(current: McpStoredAuth, tokens: OAuthTokens): McpStoredAuth;
22
+ export interface McpTokenResponse {
23
+ readonly tokens: OAuthTokens;
24
+ readonly resource: string;
25
+ readonly fresh: boolean;
26
+ readonly presented?: McpStoredAuth;
27
+ readonly clientInfo?: McpClientRegistration;
28
+ }
29
+ /** An SDK response has no locked predecessor; only explicit fresh acquisition can establish its initial proof. */
30
+ export declare function mergeOAuthTokenResponse(current: McpStoredAuth, context: McpGrantContext, response: McpTokenResponse): McpStoredAuth;
31
+ /** Active binding excludes pending registrations and display/redirect metadata. */
32
+ export declare function grantBinding(record: McpGrantRecord, context: McpGrantContext): {
33
+ issuer: string | undefined;
34
+ resource: string | undefined;
35
+ clientIdentity: string | undefined;
36
+ scope: string | undefined;
37
+ refreshScope: string | undefined;
38
+ };
39
+ export declare function validGrantProof(record: McpGrantRecord, context: McpGrantContext): McpAuthorizationProof | undefined;
40
+ /** Only the refresh transaction passes a predecessor, read while holding its existing token-store lock. */
41
+ export declare function withGrantAuthorization(record: McpStoredAuth, context: McpGrantContext, previous?: McpStoredAuth): McpStoredAuth;
42
+ export declare function oauthGrantIdentity(record: McpGrantRecord, context: McpGrantContext): string;
43
+ //# sourceMappingURL=oauth-grant.d.ts.map
@@ -0,0 +1,196 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Type } from "typebox";
3
+ import { Check } from "typebox/value";
4
+ import { isEphemeralIdentity, keyedIdentity } from "../config-identity-key.js";
5
+ import { canonicalAuthorizationServerIssuer } from "./oauth-issuer.js";
6
+ const authorizationSchema = Type.Object({
7
+ version: Type.Literal(1),
8
+ id: Type.String({ pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" }),
9
+ proof: Type.Optional(Type.Object({
10
+ credentialIdentity: Type.String({ pattern: "^[0-9a-f]{64}$" }),
11
+ previousCredentialIdentity: Type.Optional(Type.String({ pattern: "^[0-9a-f]{64}$" })),
12
+ issuer: Type.String({ minLength: 1 }),
13
+ resource: Type.String({ minLength: 1 }),
14
+ clientIdentity: Type.String({ pattern: "^[0-9a-f]{64}$" }),
15
+ scope: Type.String({ minLength: 1 }),
16
+ refreshScope: Type.Optional(Type.String({ minLength: 1 })),
17
+ })),
18
+ });
19
+ const epochSchema = Type.Object({
20
+ version: Type.Literal(1),
21
+ id: authorizationSchema.properties.id,
22
+ });
23
+ function authorizationEpoch(record) {
24
+ const authorization = record.authorization;
25
+ return Check(epochSchema, authorization) ? authorization.id : undefined;
26
+ }
27
+ function normalizeGrantedScope(scope) {
28
+ if (scope === undefined || !/^[\x21\x23-\x5b\x5d-\x7e]+(?: +[\x21\x23-\x5b\x5d-\x7e]+)*$/.test(scope))
29
+ return undefined;
30
+ return [...new Set(scope.split(" "))].filter(Boolean).sort().join(" ");
31
+ }
32
+ export function storedGrantIssuer(record) {
33
+ return record?.issuer ?? record?.discoveryState?.authorizationServerUrl?.toString();
34
+ }
35
+ export function freezeGrantAttribution(record) {
36
+ const next = { ...(record ?? {}) };
37
+ if (next.issuer !== undefined || (next.accessToken === undefined && next.refreshToken === undefined))
38
+ return next;
39
+ const issuer = storedGrantIssuer(next);
40
+ if (issuer !== undefined)
41
+ next.issuer = issuer;
42
+ else
43
+ delete next.refreshToken;
44
+ return next;
45
+ }
46
+ export function storedAuthToTokens(record, now = Date.now()) {
47
+ if (record?.accessToken === undefined || record.accessToken.length === 0)
48
+ return undefined;
49
+ const expiresIn = record.expiresAt === undefined ? undefined : Math.max(0, Math.ceil((record.expiresAt - now) / 1000));
50
+ const issuer = storedGrantIssuer(record);
51
+ return {
52
+ access_token: record.accessToken,
53
+ refresh_token: issuer === undefined ? undefined : record.refreshToken,
54
+ token_type: "Bearer",
55
+ scope: normalizeGrantedScope(record.scope),
56
+ ...(expiresIn === undefined ? {} : { expires_in: expiresIn }),
57
+ ...(issuer === undefined ? {} : { issuer }),
58
+ };
59
+ }
60
+ /** Maps one response; the caller decides which preceding fields it is entitled to inherit. */
61
+ export function mergeTokensIntoStoredAuth(current, tokens) {
62
+ return {
63
+ ...current,
64
+ accessToken: tokens.access_token,
65
+ refreshToken: tokens.refresh_token ?? current.refreshToken,
66
+ scope: normalizeGrantedScope(tokens.scope),
67
+ expiresAt: tokens.expires_in === undefined ? undefined : Date.now() + tokens.expires_in * 1000,
68
+ issuer: tokens.issuer ?? current.issuer,
69
+ };
70
+ }
71
+ /** An SDK response has no locked predecessor; only explicit fresh acquisition can establish its initial proof. */
72
+ export function mergeOAuthTokenResponse(current, context, response) {
73
+ const { tokens, resource, fresh, presented, clientInfo } = response;
74
+ // Attribute a response to the client selected before the request, never to a later store writer.
75
+ const active = {
76
+ ...current,
77
+ clientInfo,
78
+ pendingClientInfo: current.pendingClientInfo?.client_id === clientInfo?.client_id ? undefined : current.pendingClientInfo,
79
+ };
80
+ const binding = grantBinding({ ...active, issuer: tokens.issuer, resource }, context);
81
+ const canInherit = !fresh &&
82
+ presented !== undefined &&
83
+ JSON.stringify(grantBinding(presented, context)) === JSON.stringify(grantBinding(current, context)) &&
84
+ JSON.stringify(grantBinding(presented, context)) === JSON.stringify(binding) &&
85
+ presented.accessToken === current.accessToken &&
86
+ presented.refreshToken === current.refreshToken &&
87
+ presented.authorization?.id === current.authorization?.id;
88
+ const base = {
89
+ ...active,
90
+ issuer: tokens.issuer,
91
+ resource,
92
+ refreshToken: canInherit ? presented.refreshToken : undefined,
93
+ scope: undefined,
94
+ refreshScope: undefined,
95
+ authorization: undefined,
96
+ };
97
+ const next = mergeTokensIntoStoredAuth(base, tokens);
98
+ return fresh
99
+ ? withGrantAuthorization({ ...next, refreshScope: next.scope }, context)
100
+ : { ...next, authorization: { version: 1, id: randomUUID() } };
101
+ }
102
+ /** Active binding excludes pending registrations and display/redirect metadata. */
103
+ export function grantBinding(record, context) {
104
+ const issuer = storedGrantIssuer(record);
105
+ const registration = record.clientInfo;
106
+ const clientId = context.clientId ?? context.clientMetadataUrl ?? registration?.client_id;
107
+ const configured = context.clientId !== undefined || context.clientMetadataUrl !== undefined;
108
+ const clientIssuer = configured ? issuer : (registration?.issuer ?? issuer);
109
+ const clientIdentity = clientId === undefined
110
+ ? undefined
111
+ : keyedIdentity(context.agentDir, JSON.stringify([
112
+ "mcp-oauth-client-v1",
113
+ clientId,
114
+ configured ? null : (registration?.client_secret ?? null),
115
+ configured ? null : (registration?.token_endpoint_auth_method ?? null),
116
+ clientIssuer === undefined ? null : canonicalAuthorizationServerIssuer(clientIssuer),
117
+ ]));
118
+ return {
119
+ issuer: issuer === undefined ? undefined : canonicalAuthorizationServerIssuer(issuer),
120
+ resource: record.resource,
121
+ clientIdentity,
122
+ scope: normalizeGrantedScope(record.scope),
123
+ refreshScope: normalizeGrantedScope(record.refreshScope),
124
+ };
125
+ }
126
+ function credentialCommitment(record, context, id) {
127
+ const binding = grantBinding(record, context);
128
+ return keyedIdentity(context.agentDir, JSON.stringify([
129
+ "mcp-oauth-credential-v1",
130
+ context.serverName,
131
+ context.serverUrl,
132
+ id ?? null,
133
+ record.accessToken ?? null,
134
+ record.refreshToken ?? null,
135
+ binding.issuer ?? null,
136
+ binding.resource ?? null,
137
+ binding.clientIdentity ?? null,
138
+ binding.scope ?? null,
139
+ binding.refreshScope ?? null,
140
+ ]));
141
+ }
142
+ export function validGrantProof(record, context) {
143
+ const authorization = record.authorization;
144
+ if (!Check(authorizationSchema, authorization) || authorization.proof === undefined)
145
+ return undefined;
146
+ const proof = authorization.proof;
147
+ const binding = grantBinding(record, context);
148
+ if (Object.entries(binding).some(([key, value]) => Reflect.get(proof, key) !== value))
149
+ return undefined;
150
+ return proof.credentialIdentity === credentialCommitment(record, context, authorization.id) ? proof : undefined;
151
+ }
152
+ /** Only the refresh transaction passes a predecessor, read while holding its existing token-store lock. */
153
+ export function withGrantAuthorization(record, context, previous) {
154
+ const predecessor = previous === undefined ? undefined : validGrantProof(previous, context);
155
+ const binding = grantBinding(record, context);
156
+ const sameBinding = predecessor !== undefined &&
157
+ Object.entries(binding).every(([key, value]) => Reflect.get(predecessor, key) === value);
158
+ const id = sameBinding && previous?.authorization !== undefined ? previous.authorization.id : randomUUID();
159
+ const credentialIdentity = credentialCommitment(record, context, id);
160
+ const proof = binding.issuer !== undefined &&
161
+ binding.resource !== undefined &&
162
+ binding.clientIdentity !== undefined &&
163
+ binding.scope !== undefined &&
164
+ !isEphemeralIdentity(credentialIdentity)
165
+ ? {
166
+ ...binding,
167
+ issuer: binding.issuer,
168
+ resource: binding.resource,
169
+ clientIdentity: binding.clientIdentity,
170
+ scope: binding.scope,
171
+ credentialIdentity,
172
+ previousCredentialIdentity: previous === undefined
173
+ ? undefined
174
+ : credentialCommitment(previous, context, authorizationEpoch(previous)),
175
+ }
176
+ : undefined;
177
+ return { ...record, authorization: { version: 1, id, proof } };
178
+ }
179
+ export function oauthGrantIdentity(record, context) {
180
+ const epoch = authorizationEpoch(record);
181
+ const proof = validGrantProof(record, context);
182
+ return proof === undefined
183
+ ? keyedIdentity(context.agentDir, JSON.stringify(["mcp-oauth-opaque-v1", credentialCommitment(record, context, epoch)]))
184
+ : keyedIdentity(context.agentDir, JSON.stringify([
185
+ "mcp-oauth-authorization-v1",
186
+ context.serverName,
187
+ context.serverUrl,
188
+ epoch,
189
+ proof.issuer,
190
+ proof.resource,
191
+ proof.clientIdentity,
192
+ proof.scope,
193
+ proof.refreshScope ?? null,
194
+ ]));
195
+ }
196
+ //# sourceMappingURL=oauth-grant.js.map
@@ -1,8 +1,8 @@
1
1
  import type { OAuthClientProvider, OAuthDiscoveryState } from "@modelcontextprotocol/sdk/client/auth.js";
2
2
  import type { OAuthClientInformationFull, OAuthClientInformationMixed, OAuthClientMetadata, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
3
3
  import type { McpLogger } from "../log.ts";
4
- import type { McpStoredAuth, McpTokenStore } from "./token-store.ts";
5
- export declare const REFRESH_LEEWAY_MS: number;
4
+ import { type McpGrantContext } from "./oauth-grant.ts";
5
+ import type { McpTokenStore } from "./token-store.ts";
6
6
  export interface McpOAuthProviderOptions {
7
7
  serverName: string;
8
8
  serverUrl: string;
@@ -16,21 +16,6 @@ export interface McpOAuthProviderOptions {
16
16
  logger?: McpLogger;
17
17
  onRedirect?: (url: URL) => void | Promise<void>;
18
18
  }
19
- export declare function tokenExpiresAt(tokens: OAuthTokens, now?: number): number | undefined;
20
- /**
21
- * The authorization server a stored grant belongs to (senpi#2940, GHSA-6qxp-vccf-f47h): its `issuer` stamp, or
22
- * for a record saved before the stamp existed, the authorization server its sign-in discovered. A record with
23
- * neither cannot be attributed, so its refresh token is never presented anywhere.
24
- */
25
- export declare function storedGrantIssuer(record: McpStoredAuth | undefined): string | undefined;
26
- /**
27
- * Fixes who an unstamped grant belongs to before the discovery record it falls back to is replaced (senpi#2940).
28
- * A later discovery reflects what the MCP server advertises now, not where the grant was issued, so it must never
29
- * attribute an existing grant: the grant keeps the server it was attributed to, or loses its refresh token.
30
- */
31
- export declare function freezeGrantAttribution(record: McpStoredAuth | undefined): McpStoredAuth;
32
- export declare function storedAuthToTokens(record: McpStoredAuth | undefined, now?: number): OAuthTokens | undefined;
33
- export declare function mergeTokensIntoStoredAuth(current: McpStoredAuth | undefined, tokens: OAuthTokens, serverUrl: string, issuer?: string): McpStoredAuth;
34
19
  export declare class McpOAuthProvider implements OAuthClientProvider {
35
20
  #private;
36
21
  constructor(options: McpOAuthProviderOptions);
@@ -47,6 +32,7 @@ export declare class McpOAuthProvider implements OAuthClientProvider {
47
32
  saveDiscoveryState(state: OAuthDiscoveryState): Promise<void>;
48
33
  discoveryState(): OAuthDiscoveryState | undefined;
49
34
  tokens(): OAuthTokens | undefined;
35
+ withFreshAuthorization<T>(exchange: () => Promise<T>): Promise<T>;
50
36
  saveTokens(tokens: OAuthTokens): Promise<void>;
51
37
  saveCodeVerifier(codeVerifier: string): Promise<void>;
52
38
  codeVerifier(): string;
@@ -57,5 +43,6 @@ export declare class McpOAuthProvider implements OAuthClientProvider {
57
43
  get serverUrl(): string;
58
44
  get store(): McpTokenStore;
59
45
  get scopes(): string[] | undefined;
46
+ get grantContext(): McpGrantContext;
60
47
  }
61
48
  //# sourceMappingURL=oauth-provider.d.ts.map
@@ -2,72 +2,9 @@ import { randomBytes } from "node:crypto";
2
2
  import { oauthBrandName } from "@earendil-works/pi-ai/utils/oauth-page";
3
3
  import { fingerprintSecret } from "../log.js";
4
4
  import { OAuthFlowError } from "./oauth-errors.js";
5
+ import { freezeGrantAttribution, grantBinding, mergeOAuthTokenResponse, storedAuthToTokens, storedGrantIssuer, } from "./oauth-grant.js";
5
6
  import { canonicalAuthorizationServerIssuer, credentialsMatchIssuer, discoveryNamesItsServer, validatedAuthorizationServerIssuer, } from "./oauth-issuer.js";
6
7
  import { grantForFlow, registrationForFlow, withPromotedRegistration, withRegistration, } from "./registered-redirect.js";
7
- // Buffer before absolute token expiry at which a refresh is considered due.
8
- export const REFRESH_LEEWAY_MS = 5 * 60 * 1000;
9
- export function tokenExpiresAt(tokens, now = Date.now()) {
10
- if (tokens.expires_in === undefined)
11
- return undefined;
12
- return now + tokens.expires_in * 1000;
13
- }
14
- /**
15
- * The authorization server a stored grant belongs to (senpi#2940, GHSA-6qxp-vccf-f47h): its `issuer` stamp, or
16
- * for a record saved before the stamp existed, the authorization server its sign-in discovered. A record with
17
- * neither cannot be attributed, so its refresh token is never presented anywhere.
18
- */
19
- export function storedGrantIssuer(record) {
20
- return record?.issuer ?? record?.discoveryState?.authorizationServerUrl?.toString();
21
- }
22
- /**
23
- * Fixes who an unstamped grant belongs to before the discovery record it falls back to is replaced (senpi#2940).
24
- * A later discovery reflects what the MCP server advertises now, not where the grant was issued, so it must never
25
- * attribute an existing grant: the grant keeps the server it was attributed to, or loses its refresh token.
26
- */
27
- export function freezeGrantAttribution(record) {
28
- const next = { ...(record ?? {}) };
29
- if (next.issuer !== undefined || (next.accessToken === undefined && next.refreshToken === undefined))
30
- return next;
31
- const issuer = storedGrantIssuer(next);
32
- if (issuer !== undefined)
33
- next.issuer = issuer;
34
- else
35
- delete next.refreshToken;
36
- return next;
37
- }
38
- export function storedAuthToTokens(record, now = Date.now()) {
39
- if (record?.accessToken === undefined || record.accessToken.length === 0)
40
- return undefined;
41
- const expiresIn = record.expiresAt === undefined ? undefined : Math.max(0, Math.ceil((record.expiresAt - now) / 1000));
42
- const issuer = storedGrantIssuer(record);
43
- return {
44
- access_token: record.accessToken,
45
- // An unattributed refresh token is withheld: the SDK would post it to whichever server it discovers.
46
- refresh_token: issuer === undefined ? undefined : record.refreshToken,
47
- token_type: "Bearer",
48
- ...(expiresIn === undefined ? {} : { expires_in: expiresIn }),
49
- ...(issuer === undefined ? {} : { issuer }),
50
- };
51
- }
52
- export function mergeTokensIntoStoredAuth(current, tokens, serverUrl, issuer) {
53
- const next = {
54
- ...(current ?? {}),
55
- accessToken: tokens.access_token,
56
- resource: current?.resource ?? serverUrl,
57
- };
58
- const refreshToken = tokens.refresh_token ?? current?.refreshToken;
59
- if (refreshToken !== undefined)
60
- next.refreshToken = refreshToken;
61
- const expiresAt = tokenExpiresAt(tokens);
62
- if (expiresAt !== undefined)
63
- next.expiresAt = expiresAt;
64
- else
65
- delete next.expiresAt;
66
- const stamp = tokens.issuer ?? issuer;
67
- if (stamp !== undefined)
68
- next.issuer = stamp;
69
- return next;
70
- }
71
8
  // Implements the SDK OAuthClientProvider backed by the URL-bound token store.
72
9
  // All persistence goes through the store so tokens survive process restarts and
73
10
  // stay lockable across processes.
@@ -75,6 +12,10 @@ export class McpOAuthProvider {
75
12
  #options;
76
13
  #expectedState;
77
14
  #lastAuthorizationUrl;
15
+ #freshAuthorization = false;
16
+ #resource;
17
+ #presentedGrant;
18
+ #selectedClient;
78
19
  constructor(options) {
79
20
  this.#options = options;
80
21
  }
@@ -121,14 +62,23 @@ export class McpOAuthProvider {
121
62
  return { client_id: this.#options.clientId };
122
63
  if (this.#options.clientMetadataUrl !== undefined)
123
64
  return { client_id: this.#options.clientMetadataUrl };
124
- return registrationForFlow(this.#store.read(), this.#options);
65
+ this.#selectedClient = registrationForFlow(this.#store.read(), this.#options);
66
+ return this.#selectedClient;
125
67
  }
126
68
  async saveClientInformation(info) {
127
69
  const discovery = this.discoveryState();
128
70
  const issuer = discovery === undefined
129
71
  ? info.issuer && canonicalAuthorizationServerIssuer(info.issuer)
130
72
  : validatedAuthorizationServerIssuer(discovery, this.serverName);
131
- await this.#store.update((current) => withRegistration(current, { ...info, issuer }, this.#options));
73
+ const saved = await this.#store.update((current) => {
74
+ const next = withRegistration(current, { ...info, issuer }, this.#options);
75
+ return current !== undefined &&
76
+ JSON.stringify(grantBinding(current, this.grantContext)) !==
77
+ JSON.stringify(grantBinding(next, this.grantContext))
78
+ ? { ...next, scope: undefined, refreshScope: undefined, authorization: undefined }
79
+ : next;
80
+ });
81
+ this.#selectedClient = registrationForFlow(saved, this.#options);
132
82
  }
133
83
  async saveDiscoveryState(state) {
134
84
  const issuer = validatedAuthorizationServerIssuer(state, this.serverName);
@@ -171,16 +121,37 @@ export class McpOAuthProvider {
171
121
  return state;
172
122
  }
173
123
  tokens() {
174
- return storedAuthToTokens(grantForFlow(this.#store.read(), this.#options));
124
+ const grant = grantForFlow(this.#store.read(), this.#options);
125
+ this.#presentedGrant = withPromotedRegistration(grant, this.#options);
126
+ return storedAuthToTokens(grant);
127
+ }
128
+ async withFreshAuthorization(exchange) {
129
+ const previous = this.#freshAuthorization;
130
+ this.#freshAuthorization = true;
131
+ try {
132
+ return await exchange();
133
+ }
134
+ finally {
135
+ this.#freshAuthorization = previous;
136
+ }
175
137
  }
176
138
  async saveTokens(tokens) {
177
139
  const discovery = this.discoveryState();
178
- const issuer = discovery === undefined
179
- ? tokens.issuer && canonicalAuthorizationServerIssuer(tokens.issuer)
180
- : validatedAuthorizationServerIssuer(discovery, this.serverName);
140
+ // SDK results carry the exchange's issuer. Never restamp a late response from a newer file snapshot.
141
+ const issuer = tokens.issuer !== undefined
142
+ ? canonicalAuthorizationServerIssuer(tokens.issuer)
143
+ : this.#freshAuthorization && discovery !== undefined
144
+ ? validatedAuthorizationServerIssuer(discovery, this.serverName)
145
+ : storedGrantIssuer(this.#presentedGrant);
181
146
  // A flow's staged registration replaces the signed-in client in the same write as its tokens (senpi#6327).
182
- const stamped = { ...tokens, issuer };
183
- await this.#store.update((current) => mergeTokensIntoStoredAuth(withPromotedRegistration(current, this.#options), stamped, this.serverUrl));
147
+ const response = {
148
+ tokens: { ...tokens, issuer },
149
+ resource: this.#resource ?? new URL(this.serverUrl).href,
150
+ fresh: this.#freshAuthorization,
151
+ presented: this.#presentedGrant,
152
+ clientInfo: this.#selectedClient ?? this.#presentedGrant?.clientInfo,
153
+ };
154
+ this.#presentedGrant = await this.#store.update((current) => mergeOAuthTokenResponse(current ?? {}, this.grantContext, response));
184
155
  this.#logFingerprint("saveTokens", tokens.access_token);
185
156
  }
186
157
  async saveCodeVerifier(codeVerifier) {
@@ -201,7 +172,9 @@ export class McpOAuthProvider {
201
172
  }
202
173
  // RFC 8707: bind the token to this MCP resource.
203
174
  validateResourceURL(serverUrl, resource) {
204
- return Promise.resolve(new URL(resource ?? String(serverUrl)));
175
+ const selected = new URL(resource ?? String(serverUrl));
176
+ this.#resource = selected.href;
177
+ return Promise.resolve(selected);
205
178
  }
206
179
  async invalidateCredentials(scope) {
207
180
  if (scope === "all") {
@@ -217,10 +190,17 @@ export class McpOAuthProvider {
217
190
  delete next.refreshToken;
218
191
  delete next.expiresAt;
219
192
  }
193
+ if (scope === "tokens" || scope === "client") {
194
+ return {
195
+ ...next,
196
+ scope: undefined,
197
+ refreshScope: undefined,
198
+ authorization: undefined,
199
+ ...(scope === "client" ? { clientInfo: undefined } : {}),
200
+ };
201
+ }
220
202
  if (scope === "verifier")
221
203
  next.codeVerifier = undefined;
222
- if (scope === "client")
223
- next.clientInfo = undefined;
224
204
  if (scope === "discovery")
225
205
  next.discoveryState = undefined;
226
206
  return next;
@@ -238,6 +218,15 @@ export class McpOAuthProvider {
238
218
  get scopes() {
239
219
  return this.#options.scopes;
240
220
  }
221
+ get grantContext() {
222
+ return {
223
+ serverName: this.serverName,
224
+ serverUrl: this.serverUrl,
225
+ agentDir: this.store.agentDir,
226
+ clientId: this.#options.clientId,
227
+ clientMetadataUrl: this.#options.clientMetadataUrl,
228
+ };
229
+ }
241
230
  get #store() {
242
231
  return this.#options.store;
243
232
  }
@@ -1,7 +1,7 @@
1
1
  import type { OAuthServerInfo } from "@modelcontextprotocol/sdk/client/auth.js";
2
2
  import type { AuthorizationServerMetadata, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
3
3
  import type { FetchLike } from "@modelcontextprotocol/sdk/shared/transport.js";
4
- import { type McpOAuthProvider } from "./oauth-provider.ts";
4
+ import type { McpOAuthProvider } from "./oauth-provider.ts";
5
5
  import type { McpStoredAuth } from "./token-store.ts";
6
6
  export interface RefreshManagerOptions {
7
7
  fetchFn?: FetchLike;
@@ -2,8 +2,9 @@ import { loadMcpSdkAuth } from "../sdk.lazy.js";
2
2
  import { safeDelay } from "../wrap.js";
3
3
  import { isInvalidGrant, isTransientTokenError, OAuthFlowError } from "./oauth-errors.js";
4
4
  import { oauthFetch } from "./oauth-fetch.js";
5
- import { credentialsMatchIssuer, validatedAuthorizationServerIssuer } from "./oauth-issuer.js";
6
- import { mergeTokensIntoStoredAuth, REFRESH_LEEWAY_MS, storedAuthToTokens, storedGrantIssuer, } from "./oauth-provider.js";
5
+ import { canonicalAuthorizationServerIssuer, credentialsMatchIssuer, validatedAuthorizationServerIssuer, } from "./oauth-issuer.js";
6
+ import { mergeTokensIntoStoredAuth, storedAuthToTokens, storedGrantIssuer, validGrantProof, withGrantAuthorization, } from "./oauth-grant.js";
7
+ const REFRESH_LEEWAY_MS = 5 * 60 * 1000;
7
8
  export function isTokenStale(record, now) {
8
9
  const token = record?.accessToken;
9
10
  if (token === undefined || token.length === 0)
@@ -82,6 +83,7 @@ export class McpRefreshManager {
82
83
  });
83
84
  }
84
85
  const resource = new URL(current.resource ?? this.#provider.serverUrl);
86
+ const proof = validGrantProof(current, this.#provider.grantContext);
85
87
  const { refreshAuthorization } = await loadMcpSdkAuth();
86
88
  const maxRetries = this.#options.maxRetries ?? 2;
87
89
  for (let attempt = 0;; attempt++) {
@@ -93,10 +95,29 @@ export class McpRefreshManager {
93
95
  resource,
94
96
  fetchFn: oauthFetch(this.#options.fetchFn),
95
97
  });
96
- this.#provider.store.writeUnlocked(mergeTokensIntoStoredAuth(current, tokens, this.#provider.serverUrl, authorizationServer));
97
- return { ...tokens, issuer: authorizationServer };
98
+ if (tokens.issuer !== undefined &&
99
+ canonicalAuthorizationServerIssuer(tokens.issuer) !==
100
+ canonicalAuthorizationServerIssuer(authorizationServer)) {
101
+ this.#provider.store.writeUnlocked(undefined);
102
+ throw new OAuthFlowError("needs_auth", "MCP refresh response names a different issuer.", {
103
+ serverName: this.#provider.serverName,
104
+ });
105
+ }
106
+ const next = mergeTokensIntoStoredAuth({ ...current, issuer: authorizationServer, resource: resource.href, refreshScope: proof?.refreshScope }, {
107
+ ...tokens,
108
+ issuer: authorizationServer,
109
+ scope: tokens.scope === undefined ? proof?.refreshScope : tokens.scope,
110
+ });
111
+ const committed = withGrantAuthorization(next, this.#provider.grantContext, current);
112
+ this.#provider.store.writeUnlocked(committed);
113
+ const result = storedAuthToTokens(committed);
114
+ if (result === undefined)
115
+ throw new OAuthFlowError("needs_auth", "MCP refresh returned no access token.");
116
+ return result;
98
117
  }
99
118
  catch (error) {
119
+ if (error instanceof OAuthFlowError)
120
+ throw error;
100
121
  if (isInvalidGrant(error)) {
101
122
  // Terminal: drop credentials so the next use forces a clean re-auth.
102
123
  this.#provider.store.writeUnlocked(undefined);
@@ -34,11 +34,11 @@ export async function finishAuthorization(provider, code, options = {}, iss) {
34
34
  let result;
35
35
  const { auth } = await loadMcpSdkAuth();
36
36
  try {
37
- result = await auth(provider, {
37
+ result = await provider.withFreshAuthorization(() => auth(provider, {
38
38
  serverUrl: provider.serverUrl,
39
39
  authorizationCode: code,
40
40
  fetchFn: oauthFetch(options.fetchFn),
41
- });
41
+ }));
42
42
  }
43
43
  catch (error) {
44
44
  if (isInvalidGrant(error) || isRejectedAuthorizationCode(error)) {
@@ -111,12 +111,15 @@ export async function clientCredentialsGrant(provider, options = {}) {
111
111
  },
112
112
  };
113
113
  const { fetchToken } = await loadMcpSdkAuth();
114
- const tokens = await fetchToken(credentialsProvider, info.authorizationServerUrl, {
115
- metadata: info.authorizationServerMetadata,
116
- resource: new URL(provider.serverUrl),
117
- fetchFn: oauthFetch(options.fetchFn),
114
+ await provider.validateResourceURL(provider.serverUrl, provider.serverUrl);
115
+ await provider.withFreshAuthorization(async () => {
116
+ const tokens = await fetchToken(credentialsProvider, info.authorizationServerUrl, {
117
+ metadata: info.authorizationServerMetadata,
118
+ resource: new URL(provider.serverUrl),
119
+ fetchFn: oauthFetch(options.fetchFn),
120
+ });
121
+ await provider.saveTokens({ ...tokens, issuer: validatedAuthorizationServerIssuer(info, provider.serverName) });
118
122
  });
119
- await provider.saveTokens({ ...tokens, issuer: validatedAuthorizationServerIssuer(info, provider.serverName) });
120
123
  }
121
124
  export async function logout(provider) {
122
125
  await provider.store.clear();
@@ -3,7 +3,24 @@ import type { OAuthClientInformationFull } from "@modelcontextprotocol/sdk/share
3
3
  export type McpClientRegistration = OAuthClientInformationFull & {
4
4
  redirectUrisSent?: string[];
5
5
  };
6
+ export interface McpAuthorizationProof {
7
+ readonly credentialIdentity: string;
8
+ readonly previousCredentialIdentity?: string;
9
+ readonly issuer: string;
10
+ readonly resource: string;
11
+ readonly clientIdentity: string;
12
+ readonly scope: string;
13
+ readonly refreshScope?: string;
14
+ }
15
+ export interface McpAuthorization {
16
+ readonly version: 1;
17
+ readonly id: string;
18
+ readonly proof?: McpAuthorizationProof;
19
+ }
6
20
  export interface McpStoredAuth {
21
+ readonly scope?: string;
22
+ readonly refreshScope?: string;
23
+ readonly authorization?: McpAuthorization;
7
24
  accessToken?: string;
8
25
  refreshToken?: string;
9
26
  clientInfo?: McpClientRegistration;
@@ -37,6 +54,7 @@ export declare class McpTokenStore<TRecord extends McpStoredAuth = McpStoredAuth
37
54
  readonly serverUrl: string;
38
55
  constructor(options: TokenStoreOptions);
39
56
  get rootDir(): string;
57
+ get agentDir(): string;
40
58
  get dir(): string;
41
59
  get tokensPath(): string;
42
60
  get lockPath(): string;
@@ -40,6 +40,9 @@ export class McpTokenStore {
40
40
  get rootDir() {
41
41
  return join(this.#agentDir, AUTH_ROOT);
42
42
  }
43
+ get agentDir() {
44
+ return this.#agentDir;
45
+ }
43
46
  get dir() {
44
47
  return join(this.rootDir, this.#hash);
45
48
  }
@@ -39,24 +39,25 @@ export function cachedCatalogNeedsRefresh(cached, now = Date.now()) {
39
39
  return now - cached.fetchedAt > CACHE_TTL_MS;
40
40
  }
41
41
  export async function collectServerCatalogForCache(connection, config, configHash, credentialIdentity) {
42
- const tools = await collectAllPages((cursor) => listMcpTools(connection.client, cursor === undefined ? {} : { cursor }, {
42
+ const client = connection.client;
43
+ const tools = await collectAllPages((cursor) => listMcpTools(client, cursor === undefined ? {} : { cursor }, {
43
44
  timeout: config.requestTimeoutMs,
44
45
  }));
45
- const resources = await collectOptionalPages((cursor) => connection.client.listResources(cursor === undefined ? {} : { cursor }, {
46
+ const resources = await collectOptionalPages((cursor) => client.listResources(cursor === undefined ? {} : { cursor }, {
46
47
  timeout: config.requestTimeoutMs,
47
48
  }));
48
- const prompts = await collectOptionalPages((cursor) => connection.client.listPrompts(cursor === undefined ? {} : { cursor }, {
49
+ const prompts = await collectOptionalPages((cursor) => client.listPrompts(cursor === undefined ? {} : { cursor }, {
49
50
  timeout: config.requestTimeoutMs,
50
51
  }));
51
- const resourceTemplates = await collectOptionalPages((cursor) => connection.client.listResourceTemplates(cursor === undefined ? {} : { cursor }, {
52
+ const resourceTemplates = await collectOptionalPages((cursor) => client.listResourceTemplates(cursor === undefined ? {} : { cursor }, {
52
53
  timeout: config.requestTimeoutMs,
53
54
  }));
54
55
  return {
55
56
  configHash,
56
- ...(isModernMcpClient(connection.client) ? { modern: true } : {}),
57
+ ...(isModernMcpClient(client) ? { modern: true } : {}),
57
58
  credentialIdentity,
58
59
  fetchedAt: Date.now(),
59
- instructions: connection.client.getInstructions(),
60
+ instructions: client.getInstructions(),
60
61
  prompts,
61
62
  resources,
62
63
  resourceTemplates,