@herbertgao/pi-extensions 2026.9.11 → 2026.9.13

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 (192) hide show
  1. package/README.md +4 -4
  2. package/THIRD_PARTY_NOTICES.md +1 -1
  3. package/node_modules/@herbertgao/pi-cc-extensions/package.json +1 -1
  4. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  5. package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
  6. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +5 -4
  7. package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +39 -16
  8. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  9. package/node_modules/@narumitw/pi-btw/README.md +21 -4
  10. package/node_modules/@narumitw/pi-btw/dist/index.ts +1668 -958
  11. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
  12. package/node_modules/@narumitw/pi-btw/docs/workflows.md +10 -2
  13. package/node_modules/@narumitw/pi-btw/package.json +1 -1
  14. package/node_modules/@narumitw/pi-btw/src/btw.ts +17 -79
  15. package/node_modules/@narumitw/pi-btw/src/conversation-context.ts +74 -0
  16. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +196 -7
  17. package/node_modules/@narumitw/pi-btw/src/main-thread-updates.ts +40 -0
  18. package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
  19. package/node_modules/@narumitw/pi-btw/src/settings.ts +49 -0
  20. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +9 -1
  21. package/node_modules/@narumitw/pi-btw/src/workspace-layout.ts +559 -0
  22. package/node_modules/pi-mcp-adapter/CHANGELOG.md +62 -0
  23. package/node_modules/pi-mcp-adapter/README.md +75 -7
  24. package/node_modules/pi-mcp-adapter/bearer-command-resolver.ts +193 -0
  25. package/node_modules/pi-mcp-adapter/cli.js +56 -5
  26. package/node_modules/pi-mcp-adapter/commands.ts +96 -5
  27. package/node_modules/pi-mcp-adapter/config.ts +73 -6
  28. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +22 -28
  29. package/node_modules/pi-mcp-adapter/direct-tools.ts +13 -5
  30. package/node_modules/pi-mcp-adapter/dist/abort.d.ts +2 -0
  31. package/node_modules/pi-mcp-adapter/dist/abort.js +36 -0
  32. package/node_modules/pi-mcp-adapter/dist/abort.js.map +1 -0
  33. package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.d.ts +6 -0
  34. package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js +194 -0
  35. package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js.map +1 -0
  36. package/node_modules/pi-mcp-adapter/dist/config.d.ts +5 -0
  37. package/node_modules/pi-mcp-adapter/dist/config.js +75 -6
  38. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  39. package/node_modules/pi-mcp-adapter/dist/consent-manager.d.ts +18 -0
  40. package/node_modules/pi-mcp-adapter/dist/consent-manager.js +88 -0
  41. package/node_modules/pi-mcp-adapter/dist/consent-manager.js.map +1 -0
  42. package/node_modules/pi-mcp-adapter/dist/elicitation-handler.d.ts +17 -0
  43. package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js +316 -0
  44. package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js.map +1 -0
  45. package/node_modules/pi-mcp-adapter/dist/errors.d.ts +131 -0
  46. package/node_modules/pi-mcp-adapter/dist/errors.js +278 -0
  47. package/node_modules/pi-mcp-adapter/dist/errors.js.map +1 -0
  48. package/node_modules/pi-mcp-adapter/dist/http-ca.d.ts +8 -0
  49. package/node_modules/pi-mcp-adapter/dist/http-ca.js +88 -0
  50. package/node_modules/pi-mcp-adapter/dist/http-ca.js.map +1 -0
  51. package/node_modules/pi-mcp-adapter/dist/jev-client.d.ts +15 -0
  52. package/node_modules/pi-mcp-adapter/dist/jev-client.js +340 -0
  53. package/node_modules/pi-mcp-adapter/dist/jev-client.js.map +1 -0
  54. package/node_modules/pi-mcp-adapter/dist/jev-contracts.d.ts +77 -0
  55. package/node_modules/pi-mcp-adapter/dist/jev-contracts.js +2 -0
  56. package/node_modules/pi-mcp-adapter/dist/jev-contracts.js.map +1 -0
  57. package/node_modules/pi-mcp-adapter/dist/jev-key-store.d.ts +22 -0
  58. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js +85 -0
  59. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js.map +1 -0
  60. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.d.ts +2 -0
  61. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +55 -0
  62. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -0
  63. package/node_modules/pi-mcp-adapter/dist/lifecycle.d.ts +56 -0
  64. package/node_modules/pi-mcp-adapter/dist/lifecycle.js +410 -0
  65. package/node_modules/pi-mcp-adapter/dist/lifecycle.js.map +1 -0
  66. package/node_modules/pi-mcp-adapter/dist/logger.d.ts +51 -0
  67. package/node_modules/pi-mcp-adapter/dist/logger.js +131 -0
  68. package/node_modules/pi-mcp-adapter/dist/logger.js.map +1 -0
  69. package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.d.ts +26 -0
  70. package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js +103 -0
  71. package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js.map +1 -0
  72. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.d.ts +118 -0
  73. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js +1098 -0
  74. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js.map +1 -0
  75. package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +168 -0
  76. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +1117 -0
  77. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -0
  78. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +7 -140
  79. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +1 -1
  80. package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.d.ts +53 -0
  81. package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js +441 -0
  82. package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js.map +1 -0
  83. package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.d.ts +148 -0
  84. package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js +689 -0
  85. package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js.map +1 -0
  86. package/node_modules/pi-mcp-adapter/dist/mcp-probe.d.ts +6 -0
  87. package/node_modules/pi-mcp-adapter/dist/mcp-probe.js +166 -0
  88. package/node_modules/pi-mcp-adapter/dist/mcp-probe.js.map +1 -0
  89. package/node_modules/pi-mcp-adapter/dist/mcp-tasks.d.ts +102 -0
  90. package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js +369 -0
  91. package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js.map +1 -0
  92. package/node_modules/pi-mcp-adapter/dist/mcp-trace.d.ts +95 -0
  93. package/node_modules/pi-mcp-adapter/dist/mcp-trace.js +242 -0
  94. package/node_modules/pi-mcp-adapter/dist/mcp-trace.js.map +1 -0
  95. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +2 -11
  96. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
  97. package/node_modules/pi-mcp-adapter/dist/npx-resolver.d.ts +6 -0
  98. package/node_modules/pi-mcp-adapter/dist/npx-resolver.js +505 -0
  99. package/node_modules/pi-mcp-adapter/dist/npx-resolver.js.map +1 -0
  100. package/node_modules/pi-mcp-adapter/dist/request-headers-command.d.ts +10 -0
  101. package/node_modules/pi-mcp-adapter/dist/request-headers-command.js +312 -0
  102. package/node_modules/pi-mcp-adapter/dist/request-headers-command.js.map +1 -0
  103. package/node_modules/pi-mcp-adapter/dist/runtime-owner.d.ts +13 -0
  104. package/node_modules/pi-mcp-adapter/dist/runtime-owner.js +97 -0
  105. package/node_modules/pi-mcp-adapter/dist/runtime-owner.js.map +1 -0
  106. package/node_modules/pi-mcp-adapter/dist/sampling-handler.d.ts +17 -0
  107. package/node_modules/pi-mcp-adapter/dist/sampling-handler.js +203 -0
  108. package/node_modules/pi-mcp-adapter/dist/sampling-handler.js.map +1 -0
  109. package/node_modules/pi-mcp-adapter/dist/secure-keyring.d.ts +11 -0
  110. package/node_modules/pi-mcp-adapter/dist/secure-keyring.js +172 -0
  111. package/node_modules/pi-mcp-adapter/dist/secure-keyring.js.map +1 -0
  112. package/node_modules/pi-mcp-adapter/dist/server-manager.d.ts +157 -0
  113. package/node_modules/pi-mcp-adapter/dist/server-manager.js +1743 -0
  114. package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -0
  115. package/node_modules/pi-mcp-adapter/dist/session-approvals.d.ts +34 -0
  116. package/node_modules/pi-mcp-adapter/dist/session-approvals.js +140 -0
  117. package/node_modules/pi-mcp-adapter/dist/session-approvals.js.map +1 -0
  118. package/node_modules/pi-mcp-adapter/dist/session-recovery.d.ts +26 -0
  119. package/node_modules/pi-mcp-adapter/dist/session-recovery.js +142 -0
  120. package/node_modules/pi-mcp-adapter/dist/session-recovery.js.map +1 -0
  121. package/node_modules/pi-mcp-adapter/dist/state.d.ts +73 -0
  122. package/node_modules/pi-mcp-adapter/dist/state.js +2 -0
  123. package/node_modules/pi-mcp-adapter/dist/state.js.map +1 -0
  124. package/node_modules/pi-mcp-adapter/dist/types.d.ts +36 -0
  125. package/node_modules/pi-mcp-adapter/dist/types.js +18 -0
  126. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
  127. package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.d.ts +17 -0
  128. package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js +219 -0
  129. package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js.map +1 -0
  130. package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.d.ts +15 -0
  131. package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js +85 -0
  132. package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js.map +1 -0
  133. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +1 -0
  134. package/node_modules/pi-mcp-adapter/dist/utils.js +13 -0
  135. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
  136. package/node_modules/pi-mcp-adapter/elicitation-handler.ts +29 -19
  137. package/node_modules/pi-mcp-adapter/examples/jev-accessibility-loop.mjs +85 -0
  138. package/node_modules/pi-mcp-adapter/examples/jev-semantic-filter.mjs +34 -0
  139. package/node_modules/pi-mcp-adapter/index.ts +129 -6
  140. package/node_modules/pi-mcp-adapter/init.ts +6 -19
  141. package/node_modules/pi-mcp-adapter/jev-client.ts +273 -0
  142. package/node_modules/pi-mcp-adapter/jev-contracts.ts +53 -0
  143. package/node_modules/pi-mcp-adapter/jev-key-store.ts +79 -0
  144. package/node_modules/pi-mcp-adapter/lifecycle.ts +14 -3
  145. package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +7 -142
  146. package/node_modules/pi-mcp-adapter/mcp-code.ts +119 -11
  147. package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +4 -0
  148. package/node_modules/pi-mcp-adapter/mcp-references.ts +5 -3
  149. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +5 -0
  150. package/node_modules/pi-mcp-adapter/mcp-tasks.ts +467 -0
  151. package/node_modules/pi-mcp-adapter/metadata-cache.ts +2 -13
  152. package/node_modules/pi-mcp-adapter/namespace-tools.ts +1 -1
  153. package/node_modules/pi-mcp-adapter/package.json +17 -9
  154. package/node_modules/pi-mcp-adapter/proxy-modes.ts +319 -166
  155. package/node_modules/pi-mcp-adapter/request-headers-command.ts +6 -3
  156. package/node_modules/pi-mcp-adapter/search-ranking.ts +38 -7
  157. package/node_modules/pi-mcp-adapter/secure-keyring.ts +170 -0
  158. package/node_modules/pi-mcp-adapter/semantic-search.ts +186 -0
  159. package/node_modules/pi-mcp-adapter/server-manager.ts +293 -50
  160. package/node_modules/pi-mcp-adapter/session-approvals.ts +14 -0
  161. package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +2 -0
  162. package/node_modules/pi-mcp-adapter/state.ts +3 -1
  163. package/node_modules/pi-mcp-adapter/tool-approval.ts +26 -4
  164. package/node_modules/pi-mcp-adapter/tool-metadata.ts +6 -14
  165. package/node_modules/pi-mcp-adapter/tool-registrar.ts +8 -6
  166. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +4 -1
  167. package/node_modules/pi-mcp-adapter/types.ts +53 -0
  168. package/node_modules/pi-mcp-adapter/ui-server.ts +17 -9
  169. package/node_modules/pi-mcp-adapter/utils.ts +16 -0
  170. package/node_modules/pi-multi-account/CHANGELOG.md +23 -0
  171. package/node_modules/pi-multi-account/README.md +38 -11
  172. package/node_modules/pi-multi-account/index.ts +371 -96
  173. package/node_modules/pi-multi-account/package.json +5 -4
  174. package/node_modules/pi-multi-account/provider-payload-stream.ts +36 -28
  175. package/node_modules/pi-multi-account/usage.ts +31 -2
  176. package/node_modules/pi-typesafe/README.md +3 -1
  177. package/node_modules/pi-typesafe/dist/auth.d.ts +10 -3
  178. package/node_modules/pi-typesafe/dist/auth.js +13 -7
  179. package/node_modules/pi-typesafe/dist/backends.d.ts +31 -0
  180. package/node_modules/pi-typesafe/dist/backends.js +33 -0
  181. package/node_modules/pi-typesafe/dist/client.d.ts +3 -9
  182. package/node_modules/pi-typesafe/dist/client.js +63 -39
  183. package/node_modules/pi-typesafe/dist/credentials.d.ts +11 -5
  184. package/node_modules/pi-typesafe/dist/credentials.js +15 -8
  185. package/node_modules/pi-typesafe/dist/extension.js +4 -1
  186. package/node_modules/pi-typesafe/dist/index.d.ts +1 -1
  187. package/node_modules/pi-typesafe/dist/index.js +1 -1
  188. package/node_modules/pi-typesafe/dist/login.d.ts +13 -7
  189. package/node_modules/pi-typesafe/dist/login.js +17 -8
  190. package/node_modules/pi-typesafe/dist/schema.js +19 -13
  191. package/node_modules/pi-typesafe/package.json +1 -1
  192. package/package.json +11 -10
@@ -0,0 +1,1117 @@
1
+ /**
2
+ * MCP Auth Storage Module
3
+ *
4
+ * Handles secure storage of OAuth credentials, tokens, client information,
5
+ * and legacy PKCE state for MCP servers.
6
+ *
7
+ * Persistent OAuth entries are stored in the operating system credential store
8
+ * unless the externally keyed encrypted-file backend is explicitly selected.
9
+ * The default backend imports and removes legacy plaintext entries from
10
+ * $MCP_OAUTH_DIR or <Pi agent dir>/mcp-oauth.
11
+ */
12
+ import { spawnSync } from 'child_process';
13
+ import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto';
14
+ import { createRequire } from 'module';
15
+ import { chmodSync, closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from 'fs';
16
+ import { dirname, join } from 'path';
17
+ import { fileURLToPath } from 'url';
18
+ import { getAgentPath } from "./agent-dir.js";
19
+ import { resolveConfiguredOAuthDir } from "./config.js";
20
+ const require = createRequire(import.meta.url);
21
+ const AUTH_SECRET_SERVICE = 'pi-mcp-adapter.oauth';
22
+ const TEST_AUTH_STORE_ENV = 'PI_MCP_ADAPTER_TEST_AUTH_STORE';
23
+ /**
24
+ * Windows Credential Manager caps one value at CRED_MAX_CREDENTIAL_BLOB_SIZE
25
+ * (2560 bytes) and stores it as UTF-16, so the real ceiling is
26
+ * AUTH_SECRET_VALUE_LIMIT characters. Chunks must stay below that, and so must
27
+ * the threshold that decides whether to chunk at all, or oversized records still
28
+ * fail to persist on Windows.
29
+ */
30
+ const AUTH_SECRET_CHUNK_SIZE = 1000;
31
+ /** Largest single value the strictest supported credential store accepts. */
32
+ const AUTH_SECRET_VALUE_LIMIT = 1280;
33
+ const KEYRING_RECOVERY_DISABLED_ENV = 'PI_MCP_ADAPTER_DISABLE_KEYRING_RECOVERY';
34
+ const KEYRING_RECOVERY_KEYCTL_ENV = 'PI_MCP_ADAPTER_KEYRING_RECOVERY_KEYCTL';
35
+ const KEYRING_RECOVERY_NODE_ENV = 'PI_MCP_ADAPTER_KEYRING_RECOVERY_NODE';
36
+ const KEYRING_RECOVERY_HELPER_ENV = 'PI_MCP_ADAPTER_KEYRING_RECOVERY_HELPER';
37
+ const TEST_LINUX_KEYRING_RECOVERY_ENV = 'PI_MCP_ADAPTER_TEST_LINUX_KEYRING_RECOVERY';
38
+ const AUTH_CACHE_DISABLED_ENV = 'PI_MCP_ADAPTER_DISABLE_AUTH_CACHE';
39
+ const KEYRING_RECOVERY_TIMEOUT_MS = 10_000;
40
+ const AUTH_CHUNK_MANIFEST_KEY = '__piMcpAdapterOAuthChunked';
41
+ const OAUTH_FILE_KEY_ENV = 'PI_MCP_ADAPTER_OAUTH_FILE_KEY';
42
+ const ENCRYPTED_FILE_AAD_CONTEXT = 'pi-mcp-adapter.oauth.encrypted-file.v1';
43
+ const oauthLifecycleRecords = new Map();
44
+ function getOAuthLifecycleRecord(serverName) {
45
+ let record = oauthLifecycleRecords.get(serverName);
46
+ if (!record) {
47
+ record = { generation: {}, revocations: 0 };
48
+ oauthLifecycleRecords.set(serverName, record);
49
+ }
50
+ return record;
51
+ }
52
+ /** Capture immutable process-local authority for one server's OAuth lifecycle. */
53
+ export function captureOAuthAuthority(serverName, assertNow = true) {
54
+ const record = getOAuthLifecycleRecord(serverName);
55
+ const generation = record.generation;
56
+ const capturedDuringRevocation = record.revocations > 0;
57
+ const assertAuthority = () => {
58
+ if (capturedDuringRevocation || record.generation !== generation || record.revocations > 0) {
59
+ throw new Error('OAuth flow is no longer active');
60
+ }
61
+ };
62
+ if (assertNow)
63
+ assertAuthority();
64
+ return assertAuthority;
65
+ }
66
+ /** Begin an overlap-safe process-local logout interval. */
67
+ export function beginOAuthRevocation(serverName) {
68
+ const record = getOAuthLifecycleRecord(serverName);
69
+ record.generation = {};
70
+ record.revocations += 1;
71
+ let released = false;
72
+ return () => {
73
+ if (released)
74
+ return;
75
+ released = true;
76
+ record.revocations -= 1;
77
+ };
78
+ }
79
+ export class OAuthCredentialStoreError extends Error {
80
+ operation;
81
+ backend;
82
+ code = 'OAUTH_CREDENTIAL_STORE_UNAVAILABLE';
83
+ constructor(message, operation, cause, backend) {
84
+ super(message, { cause });
85
+ this.operation = operation;
86
+ this.backend = backend;
87
+ this.name = 'OAuthCredentialStoreError';
88
+ }
89
+ }
90
+ function causeChainContains(error, pattern) {
91
+ const seen = new Set();
92
+ let current = error;
93
+ while ((typeof current === 'object' && current !== null) || typeof current === 'function') {
94
+ if (seen.has(current))
95
+ break;
96
+ seen.add(current);
97
+ const candidate = current;
98
+ if ([candidate.name, candidate.message, candidate.code].some(value => typeof value === 'string' && pattern.test(value))) {
99
+ return true;
100
+ }
101
+ current = candidate.cause;
102
+ }
103
+ return false;
104
+ }
105
+ export function formatOAuthCredentialStoreUnavailable(error) {
106
+ if (error.backend === 'encrypted-file') {
107
+ if (causeChainContains(error, /PI_MCP_ADAPTER_OAUTH_FILE_KEY/)) {
108
+ return 'Encrypted OAuth credential file store unavailable. Set PI_MCP_ADAPTER_OAUTH_FILE_KEY to canonical base64 for exactly 32 random bytes and retry.';
109
+ }
110
+ return 'Encrypted OAuth credential file store unavailable. Check the encrypted credential file and key, then reauthenticate.';
111
+ }
112
+ if (process.platform === 'linux' && causeChainContains(error, /key\s*(?:has been\s*)?revoked|keyrevoked/i)) {
113
+ return 'OAuth credential store unavailable: the Linux session keyring may be revoked. Start Pi from a fresh login/keyring session and retry.';
114
+ }
115
+ if (causeChainContains(error, /ERROR_NO_SUCH_LOGON_SESSION|\b1312\b/i)) {
116
+ return 'OAuth credential store unavailable: Windows Credential Manager is unavailable from this network logon. To opt in to encrypted file storage for OpenSSH/headless use, set settings.oauthCredentialStore to "encrypted-file" and provide PI_MCP_ADAPTER_OAUTH_FILE_KEY.';
117
+ }
118
+ return 'OAuth credential store unavailable. Configure or unlock the OS credential store and retry.';
119
+ }
120
+ function authSecretStoreLabel(store) {
121
+ return store.kind === 'encrypted-file' ? 'encrypted OAuth credential file store' : 'OS secure credential store';
122
+ }
123
+ let KeyringEntryClass;
124
+ const memoryAuthEntries = new Map();
125
+ let testAuthSecretStoreReadCount = 0;
126
+ const authEntryCache = new Map();
127
+ function isAuthEntryCacheEnabled() {
128
+ return process.env[AUTH_CACHE_DISABLED_ENV] !== '1';
129
+ }
130
+ function cloneAuthEntry(entry) {
131
+ return entry === undefined ? undefined : structuredClone(entry);
132
+ }
133
+ const memoryAuthSecretStore = {
134
+ read(account) {
135
+ testAuthSecretStoreReadCount++;
136
+ return memoryAuthEntries.get(account);
137
+ },
138
+ write(account, payload) {
139
+ memoryAuthEntries.set(account, payload);
140
+ },
141
+ remove(account) {
142
+ memoryAuthEntries.delete(account);
143
+ },
144
+ };
145
+ const keyringAuthSecretStore = {
146
+ read(account) {
147
+ return getKeyringEntry(account).getPassword() ?? undefined;
148
+ },
149
+ write(account, payload) {
150
+ getKeyringEntry(account).setPassword(payload);
151
+ },
152
+ remove(account) {
153
+ getKeyringEntry(account).deleteCredential();
154
+ },
155
+ };
156
+ /** Mimics the Windows Credential Manager per-value ceiling for tests. */
157
+ const sizeLimitedAuthSecretStore = {
158
+ read(account) {
159
+ testAuthSecretStoreReadCount++;
160
+ return memoryAuthEntries.get(account);
161
+ },
162
+ write(account, payload) {
163
+ if (payload.length > AUTH_SECRET_VALUE_LIMIT) {
164
+ throw new Error(`Value of 'password encoded as UTF-16' is longer than the platform limit of ${AUTH_SECRET_VALUE_LIMIT * 2} chars`);
165
+ }
166
+ memoryAuthEntries.set(account, payload);
167
+ },
168
+ remove(account) {
169
+ memoryAuthEntries.delete(account);
170
+ },
171
+ };
172
+ const writeFailingAuthSecretStore = {
173
+ ...memoryAuthSecretStore,
174
+ write() {
175
+ throw new Error('simulated secure credential store write failure');
176
+ },
177
+ };
178
+ const unavailableAuthSecretStore = {
179
+ read() {
180
+ testAuthSecretStoreReadCount++;
181
+ throw new Error('simulated secure credential store unavailable');
182
+ },
183
+ write() {
184
+ throw new Error('simulated secure credential store unavailable');
185
+ },
186
+ remove() {
187
+ throw new Error('simulated secure credential store unavailable');
188
+ },
189
+ };
190
+ function createKeyRevokedTestError() {
191
+ return new Error("Couldn't access platform storage: KeyRevoked", { cause: new Error('KeyRevoked') });
192
+ }
193
+ const keyRevokedAuthSecretStore = {
194
+ read() {
195
+ testAuthSecretStoreReadCount++;
196
+ throw createKeyRevokedTestError();
197
+ },
198
+ write() {
199
+ throw createKeyRevokedTestError();
200
+ },
201
+ remove() {
202
+ throw createKeyRevokedTestError();
203
+ },
204
+ };
205
+ export function resetTestAuthSecretStore() {
206
+ memoryAuthEntries.clear();
207
+ authEntryCache.clear();
208
+ testAuthSecretStoreReadCount = 0;
209
+ }
210
+ export function resetAuthEntryCache() {
211
+ authEntryCache.clear();
212
+ }
213
+ export function getTestAuthSecretStoreReadCount() {
214
+ return testAuthSecretStoreReadCount;
215
+ }
216
+ export function getTestAuthSecretStoreEntries() {
217
+ return [...memoryAuthEntries.entries()];
218
+ }
219
+ export function removeTestAuthSecretStoreEntry(account) {
220
+ memoryAuthEntries.delete(account);
221
+ }
222
+ export function setTestAuthSecretStoreEntry(account, payload) {
223
+ memoryAuthEntries.set(account, payload);
224
+ }
225
+ function decodeCanonicalBase64(value, expectedBytes) {
226
+ if (typeof value !== 'string' || value.length === 0 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
227
+ throw new Error('value is not canonical base64');
228
+ }
229
+ const decoded = Buffer.from(value, 'base64');
230
+ if (decoded.toString('base64') !== value || (expectedBytes !== undefined && decoded.length !== expectedBytes)) {
231
+ throw new Error(expectedBytes === undefined ? 'value is not canonical base64' : `value must decode to exactly ${expectedBytes} bytes`);
232
+ }
233
+ return decoded;
234
+ }
235
+ function getEncryptedFileKey() {
236
+ const encoded = process.env[OAUTH_FILE_KEY_ENV];
237
+ if (!encoded)
238
+ throw new Error(`${OAUTH_FILE_KEY_ENV} is required for the encrypted OAuth credential file store`);
239
+ try {
240
+ return decodeCanonicalBase64(encoded, 32);
241
+ }
242
+ catch (error) {
243
+ throw new Error(`${OAUTH_FILE_KEY_ENV} must be canonical base64 for exactly 32 random bytes`, { cause: error });
244
+ }
245
+ }
246
+ function encryptedEntryPath(root, account) {
247
+ return join(root, account, 'credentials.json');
248
+ }
249
+ function validatePrivateRegularFile(path) {
250
+ let stat;
251
+ try {
252
+ stat = lstatSync(path);
253
+ }
254
+ catch (error) {
255
+ if (error.code === 'ENOENT')
256
+ return false;
257
+ throw error;
258
+ }
259
+ if (!stat.isFile() || stat.isSymbolicLink())
260
+ throw new Error(`Refusing non-regular OAuth credential file at ${path}`);
261
+ if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0) {
262
+ throw new Error(`OAuth credential file has group or other permissions at ${path}`);
263
+ }
264
+ return true;
265
+ }
266
+ function ensurePrivateDirectory(path) {
267
+ mkdirSync(path, { recursive: true, mode: 0o700 });
268
+ const stat = lstatSync(path);
269
+ if (!stat.isDirectory() || stat.isSymbolicLink())
270
+ throw new Error(`Refusing non-directory OAuth credential path at ${path}`);
271
+ if (process.platform !== 'win32')
272
+ chmodSync(path, 0o700);
273
+ }
274
+ function createEncryptedFileAuthSecretStore() {
275
+ const root = getAgentPath('mcp-oauth-encrypted');
276
+ return {
277
+ kind: 'encrypted-file',
278
+ read(account) {
279
+ const key = getEncryptedFileKey();
280
+ const path = encryptedEntryPath(root, account);
281
+ if (!validatePrivateRegularFile(path))
282
+ return undefined;
283
+ const envelope = parseJsonPayload(account, readFileSync(path, 'utf8'), path);
284
+ if (!envelope || envelope.version !== 1 || envelope.algorithm !== 'aes-256-gcm') {
285
+ throw new Error(`Unsupported encrypted OAuth credential envelope at ${path}`);
286
+ }
287
+ const iv = decodeCanonicalBase64(envelope.iv, 12);
288
+ const ciphertext = decodeCanonicalBase64(envelope.ciphertext);
289
+ const tag = decodeCanonicalBase64(envelope.tag, 16);
290
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
291
+ decipher.setAAD(Buffer.from(`${ENCRYPTED_FILE_AAD_CONTEXT}\0${account}`, 'utf8'));
292
+ decipher.setAuthTag(tag);
293
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
294
+ },
295
+ write(account, payload) {
296
+ const key = getEncryptedFileKey();
297
+ const iv = randomBytes(12);
298
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
299
+ cipher.setAAD(Buffer.from(`${ENCRYPTED_FILE_AAD_CONTEXT}\0${account}`, 'utf8'));
300
+ const ciphertext = Buffer.concat([cipher.update(payload, 'utf8'), cipher.final()]);
301
+ const envelope = JSON.stringify({
302
+ version: 1,
303
+ algorithm: 'aes-256-gcm',
304
+ iv: iv.toString('base64'),
305
+ ciphertext: ciphertext.toString('base64'),
306
+ tag: cipher.getAuthTag().toString('base64'),
307
+ });
308
+ const dir = join(root, account);
309
+ ensurePrivateDirectory(root);
310
+ ensurePrivateDirectory(dir);
311
+ const destination = encryptedEntryPath(root, account);
312
+ validatePrivateRegularFile(destination);
313
+ const temporary = join(dir, `.credentials-${randomBytes(12).toString('hex')}.tmp`);
314
+ let fd;
315
+ try {
316
+ fd = openSync(temporary, 'wx', 0o600);
317
+ writeFileSync(fd, envelope, 'utf8');
318
+ fsyncSync(fd);
319
+ closeSync(fd);
320
+ fd = undefined;
321
+ renameSync(temporary, destination);
322
+ }
323
+ catch (error) {
324
+ if (fd !== undefined) {
325
+ try {
326
+ closeSync(fd);
327
+ }
328
+ catch { }
329
+ }
330
+ try {
331
+ rmSync(temporary, { force: true });
332
+ }
333
+ catch { }
334
+ throw error;
335
+ }
336
+ },
337
+ remove(account) {
338
+ getEncryptedFileKey();
339
+ const path = encryptedEntryPath(root, account);
340
+ if (!validatePrivateRegularFile(path))
341
+ return;
342
+ rmSync(path);
343
+ try {
344
+ rmSync(dirname(path));
345
+ }
346
+ catch { }
347
+ },
348
+ };
349
+ }
350
+ function getAuthSecretStore(options = {}) {
351
+ if (options.credentialStore === 'encrypted-file')
352
+ return createEncryptedFileAuthSecretStore();
353
+ if (process.env[TEST_AUTH_STORE_ENV] === 'memory')
354
+ return memoryAuthSecretStore;
355
+ if (process.env[TEST_AUTH_STORE_ENV] === 'sizelimited')
356
+ return sizeLimitedAuthSecretStore;
357
+ if (process.env[TEST_AUTH_STORE_ENV] === 'writefailing')
358
+ return writeFailingAuthSecretStore;
359
+ if (process.env[TEST_AUTH_STORE_ENV] === 'unavailable')
360
+ return unavailableAuthSecretStore;
361
+ if (process.env[TEST_AUTH_STORE_ENV] === 'keyrevoked')
362
+ return keyRevokedAuthSecretStore;
363
+ return keyringAuthSecretStore;
364
+ }
365
+ function getKeyringEntry(account) {
366
+ try {
367
+ KeyringEntryClass ??= loadKeyringEntryClass();
368
+ return new KeyringEntryClass(AUTH_SECRET_SERVICE, account);
369
+ }
370
+ catch (error) {
371
+ throw new Error('OAuth secure credential storage is unavailable. Configure the OS credential store and retry authentication.', { cause: error });
372
+ }
373
+ }
374
+ function loadKeyringEntryClass(keyringRequire = require, platform = process.platform, arch = process.arch) {
375
+ try {
376
+ return keyringRequire('@napi-rs/keyring').Entry;
377
+ }
378
+ catch (loaderError) {
379
+ try {
380
+ return loadKeyringNativeBindingFallback(keyringRequire, platform, arch).Entry;
381
+ }
382
+ catch (fallbackError) {
383
+ throw new Error(`Failed to load @napi-rs/keyring; absolute-path native binding fallback also failed: ${formatErrorMessage(fallbackError)}`, {
384
+ cause: loaderError,
385
+ });
386
+ }
387
+ }
388
+ }
389
+ function loadKeyringNativeBindingFallback(keyringRequire, platform, arch) {
390
+ const targets = getKeyringNativeBindingTargets(platform, arch);
391
+ if (targets.length === 0) {
392
+ throw new Error(`Unsupported @napi-rs/keyring native binding target: ${platform}-${arch}`);
393
+ }
394
+ let lastError;
395
+ for (const target of targets) {
396
+ try {
397
+ const packageJsonPath = keyringRequire.resolve(`${target.packageName}/package.json`);
398
+ return keyringRequire(join(dirname(packageJsonPath), target.bindingFile));
399
+ }
400
+ catch (error) {
401
+ lastError = error;
402
+ }
403
+ }
404
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
405
+ }
406
+ function getKeyringNativeBindingTargets(platform, arch) {
407
+ return getKeyringNativeBindingSuffixes(platform, arch).map(suffix => ({
408
+ packageName: `@napi-rs/keyring-${suffix}`,
409
+ bindingFile: `keyring.${suffix}.node`,
410
+ }));
411
+ }
412
+ function getKeyringNativeBindingSuffixes(platform, arch) {
413
+ if (platform === 'darwin') {
414
+ if (arch === 'arm64')
415
+ return ['darwin-arm64'];
416
+ if (arch === 'x64')
417
+ return ['darwin-x64'];
418
+ }
419
+ if (platform === 'win32') {
420
+ if (arch === 'arm64')
421
+ return ['win32-arm64-msvc'];
422
+ if (arch === 'x64')
423
+ return ['win32-x64-msvc'];
424
+ if (arch === 'ia32')
425
+ return ['win32-ia32-msvc'];
426
+ }
427
+ if (platform === 'linux') {
428
+ if (arch === 'arm64')
429
+ return ['linux-arm64-gnu', 'linux-arm64-musl'];
430
+ if (arch === 'arm')
431
+ return ['linux-arm-gnueabihf'];
432
+ if (arch === 'riscv64')
433
+ return ['linux-riscv64-gnu'];
434
+ if (arch === 'x64')
435
+ return ['linux-x64-gnu', 'linux-x64-musl'];
436
+ }
437
+ if (platform === 'freebsd' && arch === 'x64')
438
+ return ['freebsd-x64'];
439
+ return [];
440
+ }
441
+ function formatErrorMessage(error) {
442
+ return error instanceof Error ? error.message : String(error);
443
+ }
444
+ function isLinuxKeyringRecoveryEnabled() {
445
+ if (process.env[KEYRING_RECOVERY_DISABLED_ENV] === '1')
446
+ return false;
447
+ return process.platform === 'linux' || process.env[TEST_LINUX_KEYRING_RECOVERY_ENV] === '1';
448
+ }
449
+ function shouldAttemptLinuxKeyringRecovery(error) {
450
+ return isLinuxKeyringRecoveryEnabled()
451
+ && causeChainContains(error, /key\s*(?:has been\s*)?revoked|keyrevoked/i);
452
+ }
453
+ function runLinuxKeyringRecoveryOperation(operation, account, payload) {
454
+ const keyctl = process.env[KEYRING_RECOVERY_KEYCTL_ENV]?.trim() || 'keyctl';
455
+ const node = process.env[KEYRING_RECOVERY_NODE_ENV]?.trim() || 'node';
456
+ const helper = process.env[KEYRING_RECOVERY_HELPER_ENV]?.trim()
457
+ || fileURLToPath(new URL('./mcp-keyring-helper.cjs', import.meta.url));
458
+ const request = JSON.stringify({ operation, service: AUTH_SECRET_SERVICE, account, payload });
459
+ const result = spawnSync(keyctl, ['session', '-', node, helper], {
460
+ input: `${request}\n`,
461
+ encoding: 'utf8',
462
+ maxBuffer: 1024 * 1024,
463
+ timeout: KEYRING_RECOVERY_TIMEOUT_MS,
464
+ windowsHide: true,
465
+ });
466
+ if (result.error) {
467
+ throw new Error(`Linux keyring recovery helper could not start: ${result.error.message}`, { cause: result.error });
468
+ }
469
+ if (result.status !== 0) {
470
+ throw new Error(`Linux keyring recovery helper failed with exit code ${result.status ?? 'unknown'}`);
471
+ }
472
+ let response;
473
+ try {
474
+ response = JSON.parse(result.stdout.trim());
475
+ }
476
+ catch (error) {
477
+ throw new Error('Linux keyring recovery helper returned invalid JSON', { cause: error });
478
+ }
479
+ if (typeof response !== 'object' || response === null || typeof response.ok !== 'boolean') {
480
+ throw new Error('Linux keyring recovery helper returned an invalid response');
481
+ }
482
+ const typedResponse = response;
483
+ if (typedResponse.ok === false) {
484
+ throw new Error(typedResponse.error || 'Linux keyring recovery helper failed');
485
+ }
486
+ if (operation === 'read' && typedResponse.found === true && typeof typedResponse.value !== 'string') {
487
+ throw new Error('Linux keyring recovery helper returned an invalid read response');
488
+ }
489
+ return typedResponse;
490
+ }
491
+ const linuxKeyringRecoveryAuthSecretStore = {
492
+ read(account) {
493
+ const response = runLinuxKeyringRecoveryOperation('read', account);
494
+ return response.ok && response.found === true ? response.value : undefined;
495
+ },
496
+ write(account, payload) {
497
+ runLinuxKeyringRecoveryOperation('write', account, payload);
498
+ },
499
+ remove(account) {
500
+ runLinuxKeyringRecoveryOperation('remove', account);
501
+ },
502
+ };
503
+ export function loadTestKeyringEntryClass(keyringRequire, platform, arch) {
504
+ return loadKeyringEntryClass(keyringRequire, platform, arch);
505
+ }
506
+ export function getAuthStorageOptions(oauthDir, cwd = process.cwd(), oauthCredentialStore) {
507
+ if (oauthCredentialStore !== undefined && oauthCredentialStore !== 'encrypted-file') {
508
+ throw new Error('settings.oauthCredentialStore must be "encrypted-file" when set');
509
+ }
510
+ if (oauthCredentialStore === 'encrypted-file')
511
+ return { credentialStore: 'encrypted-file' };
512
+ const baseDir = resolveConfiguredOAuthDir(oauthDir, cwd);
513
+ return baseDir ? { baseDir } : {};
514
+ }
515
+ export function getAuthBaseDir(options = {}) {
516
+ const override = process.env.MCP_OAUTH_DIR?.trim();
517
+ if (override)
518
+ return override;
519
+ return options.baseDir ?? getAgentPath('mcp-oauth');
520
+ }
521
+ /**
522
+ * Get the legacy server-specific directory path.
523
+ */
524
+ function getServerDir(serverName, options) {
525
+ if (typeof serverName !== 'string') {
526
+ throw new Error(`Invalid MCP server name: ${JSON.stringify(serverName)}`);
527
+ }
528
+ const storageKey = getAuthEntryAccount(serverName);
529
+ return join(getAuthBaseDir(options), storageKey);
530
+ }
531
+ function getAuthEntryAccount(serverName) {
532
+ if (typeof serverName !== 'string') {
533
+ throw new Error(`Invalid MCP server name: ${JSON.stringify(serverName)}`);
534
+ }
535
+ return `sha256-${createHash('sha256').update(serverName, 'utf8').digest('hex')}`;
536
+ }
537
+ /**
538
+ * Get the legacy plaintext tokens file path for a server.
539
+ */
540
+ export function getAuthEntryFilePath(serverName, options) {
541
+ return join(getServerDir(serverName, options), 'tokens.json');
542
+ }
543
+ function parseJsonPayload(serverName, payload, source) {
544
+ try {
545
+ return JSON.parse(payload);
546
+ }
547
+ catch (error) {
548
+ throw new Error(`Failed to parse OAuth credentials for ${serverName} from ${source}`, { cause: error });
549
+ }
550
+ }
551
+ function parseAuthEntryPayload(serverName, payload, source) {
552
+ const parsed = parseJsonPayload(serverName, payload, source);
553
+ const entry = toAuthEntry(parsed);
554
+ if (!entry) {
555
+ throw new Error(`Failed to parse OAuth credentials for ${serverName} from ${source}: invalid credential shape`);
556
+ }
557
+ return entry;
558
+ }
559
+ function toAuthEntry(value) {
560
+ const entry = toRecord(value);
561
+ if (!entry)
562
+ return undefined;
563
+ const codeVerifier = optionalString(entry.codeVerifier);
564
+ const oauthState = optionalString(entry.oauthState);
565
+ const serverUrl = optionalString(entry.serverUrl);
566
+ if (codeVerifier === null || oauthState === null || serverUrl === null)
567
+ return undefined;
568
+ const tokens = entry.tokens === undefined ? undefined : toStoredTokens(entry.tokens);
569
+ const clientInfo = entry.clientInfo === undefined ? undefined : toStoredClientInfo(entry.clientInfo);
570
+ if ((entry.tokens !== undefined && !tokens) || (entry.clientInfo !== undefined && !clientInfo))
571
+ return undefined;
572
+ const authEntry = {};
573
+ if (tokens)
574
+ authEntry.tokens = tokens;
575
+ if (clientInfo)
576
+ authEntry.clientInfo = clientInfo;
577
+ if (codeVerifier !== undefined)
578
+ authEntry.codeVerifier = codeVerifier;
579
+ if (oauthState !== undefined)
580
+ authEntry.oauthState = oauthState;
581
+ if (serverUrl !== undefined)
582
+ authEntry.serverUrl = serverUrl;
583
+ return authEntry;
584
+ }
585
+ function toStoredTokens(value) {
586
+ const tokens = toRecord(value);
587
+ if (!tokens || typeof tokens.accessToken !== 'string')
588
+ return undefined;
589
+ const refreshToken = optionalString(tokens.refreshToken);
590
+ const scope = optionalString(tokens.scope);
591
+ const issuer = optionalString(tokens.issuer);
592
+ const expiresAt = optionalNumber(tokens.expiresAt);
593
+ if (refreshToken === null || scope === null || issuer === null || expiresAt === null)
594
+ return undefined;
595
+ const storedTokens = { accessToken: tokens.accessToken };
596
+ if (refreshToken !== undefined)
597
+ storedTokens.refreshToken = refreshToken;
598
+ if (expiresAt !== undefined)
599
+ storedTokens.expiresAt = expiresAt;
600
+ if (scope !== undefined)
601
+ storedTokens.scope = scope;
602
+ if (issuer !== undefined)
603
+ storedTokens.issuer = issuer;
604
+ return storedTokens;
605
+ }
606
+ function toStoredClientInfo(value) {
607
+ const clientInfo = toRecord(value);
608
+ if (!clientInfo || typeof clientInfo.clientId !== 'string')
609
+ return undefined;
610
+ const clientSecret = optionalString(clientInfo.clientSecret);
611
+ const issuer = optionalString(clientInfo.issuer);
612
+ const clientIdIssuedAt = optionalNumber(clientInfo.clientIdIssuedAt);
613
+ const clientSecretExpiresAt = optionalNumber(clientInfo.clientSecretExpiresAt);
614
+ const configPreRegistered = optionalBoolean(clientInfo.configPreRegistered);
615
+ if (clientSecret === null || issuer === null || clientIdIssuedAt === null || clientSecretExpiresAt === null || configPreRegistered === null)
616
+ return undefined;
617
+ const storedClient = { clientId: clientInfo.clientId };
618
+ const redirectUris = stringArray(clientInfo.redirectUris);
619
+ if (clientSecret !== undefined)
620
+ storedClient.clientSecret = clientSecret;
621
+ if (clientIdIssuedAt !== undefined)
622
+ storedClient.clientIdIssuedAt = clientIdIssuedAt;
623
+ if (clientSecretExpiresAt !== undefined)
624
+ storedClient.clientSecretExpiresAt = clientSecretExpiresAt;
625
+ if (redirectUris !== undefined)
626
+ storedClient.redirectUris = redirectUris;
627
+ if (issuer !== undefined)
628
+ storedClient.issuer = issuer;
629
+ if (configPreRegistered !== undefined)
630
+ storedClient.configPreRegistered = configPreRegistered;
631
+ return storedClient;
632
+ }
633
+ function toRecord(value) {
634
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
635
+ ? value
636
+ : undefined;
637
+ }
638
+ function optionalString(value) {
639
+ if (value === undefined)
640
+ return undefined;
641
+ return typeof value === 'string' ? value : null;
642
+ }
643
+ function optionalNumber(value) {
644
+ if (value === undefined)
645
+ return undefined;
646
+ return typeof value === 'number' ? value : null;
647
+ }
648
+ function optionalBoolean(value) {
649
+ if (value === undefined)
650
+ return undefined;
651
+ return typeof value === 'boolean' ? value : null;
652
+ }
653
+ function stringArray(value) {
654
+ return Array.isArray(value) && value.every(uri => typeof uri === 'string') ? value : undefined;
655
+ }
656
+ function isAuthEntryChunkManifest(value) {
657
+ if (typeof value !== 'object' || value === null)
658
+ return false;
659
+ const manifest = value;
660
+ return manifest[AUTH_CHUNK_MANIFEST_KEY] === 1
661
+ && typeof manifest.chunkCount === 'number'
662
+ && Number.isInteger(manifest.chunkCount)
663
+ && manifest.chunkCount > 0
664
+ && typeof manifest.chunkDigest === 'string'
665
+ && /^[a-f0-9]{16}$/.test(manifest.chunkDigest);
666
+ }
667
+ function getAuthEntryChunkAccount(account, manifest, index) {
668
+ return `${account}.chunk.${manifest.chunkDigest}.${index}`;
669
+ }
670
+ function getAuthEntryChunkAccounts(account, manifest) {
671
+ return Array.from({ length: manifest.chunkCount }, (_, index) => getAuthEntryChunkAccount(account, manifest, index));
672
+ }
673
+ function readChunkManifestFromPayload(serverName, payload, source) {
674
+ const parsed = parseJsonPayload(serverName, payload, source);
675
+ return isAuthEntryChunkManifest(parsed) ? parsed : undefined;
676
+ }
677
+ function readExistingChunkManifest(store, serverName, account) {
678
+ try {
679
+ const payload = store.read(account);
680
+ return payload === undefined ? undefined : readChunkManifestFromPayload(serverName, payload, authSecretStoreLabel(store));
681
+ }
682
+ catch {
683
+ return undefined;
684
+ }
685
+ }
686
+ function removeChunkPayloads(store, account, manifest) {
687
+ for (const chunkAccount of getAuthEntryChunkAccounts(account, manifest)) {
688
+ store.remove(chunkAccount);
689
+ }
690
+ }
691
+ function tryRemoveChunkPayloads(store, account, manifest) {
692
+ if (!manifest)
693
+ return;
694
+ try {
695
+ removeChunkPayloads(store, account, manifest);
696
+ }
697
+ catch {
698
+ // Stale chunk cleanup must not hide a successful credential write.
699
+ }
700
+ }
701
+ function shouldChunkAuthPayload(store, payload) {
702
+ return store.kind !== 'encrypted-file' && payload.length > AUTH_SECRET_CHUNK_SIZE
703
+ && (process.platform === 'win32' || process.env[TEST_AUTH_STORE_ENV] === 'sizelimited');
704
+ }
705
+ function getAuthEntryChunkDigest(payload) {
706
+ return createHash('sha256').update(payload, 'utf8').digest('hex').slice(0, 16);
707
+ }
708
+ function splitAuthPayload(payload) {
709
+ const chunks = [];
710
+ for (let start = 0; start < payload.length;) {
711
+ let end = Math.min(start + AUTH_SECRET_CHUNK_SIZE, payload.length);
712
+ const lastCodeUnit = payload.charCodeAt(end - 1);
713
+ if (end < payload.length && lastCodeUnit >= 0xd800 && lastCodeUnit <= 0xdbff)
714
+ end--;
715
+ chunks.push(payload.slice(start, end));
716
+ start = end;
717
+ }
718
+ return chunks;
719
+ }
720
+ function createChunkManifest(payload, chunkCount) {
721
+ return {
722
+ [AUTH_CHUNK_MANIFEST_KEY]: 1,
723
+ chunkCount,
724
+ chunkDigest: getAuthEntryChunkDigest(payload),
725
+ };
726
+ }
727
+ function readChunkedAuthEntry(store, serverName, account, manifest) {
728
+ let payload;
729
+ try {
730
+ payload = getAuthEntryChunkAccounts(account, manifest).map((chunkAccount) => {
731
+ const chunk = store.read(chunkAccount);
732
+ if (chunk === undefined) {
733
+ throw new Error(`Missing OAuth credential chunk ${chunkAccount} for ${serverName}`);
734
+ }
735
+ return chunk;
736
+ }).join('');
737
+ if (getAuthEntryChunkDigest(payload) !== manifest.chunkDigest) {
738
+ throw new Error('OAuth credential chunk integrity check failed');
739
+ }
740
+ }
741
+ catch (error) {
742
+ throw new OAuthCredentialStoreError(`Failed to read OAuth credentials for ${serverName} from the ${authSecretStoreLabel(store)}`, 'read', error);
743
+ }
744
+ return parseAuthEntryPayload(serverName, payload, `${authSecretStoreLabel(store)} chunks`);
745
+ }
746
+ function readLegacyAuthEntry(serverName, options) {
747
+ const filePath = getAuthEntryFilePath(serverName, options);
748
+ if (!existsSync(filePath))
749
+ return undefined;
750
+ const data = readFileSync(filePath, 'utf-8');
751
+ return parseAuthEntryPayload(serverName, data, filePath);
752
+ }
753
+ function removeLegacyAuthEntry(serverName, options) {
754
+ const filePath = getAuthEntryFilePath(serverName, options);
755
+ if (!existsSync(filePath))
756
+ return;
757
+ try {
758
+ rmSync(filePath, { force: true });
759
+ }
760
+ catch (error) {
761
+ throw new Error(`Failed to remove legacy plaintext OAuth credentials for ${serverName} at ${filePath}`, { cause: error });
762
+ }
763
+ const dir = getServerDir(serverName, options);
764
+ try {
765
+ rmSync(dir, { recursive: true });
766
+ }
767
+ catch {
768
+ // Directory may contain future non-secret metadata; the plaintext file was already removed.
769
+ }
770
+ }
771
+ function writeSecureAuthEntryToStore(store, serverName, entry) {
772
+ const account = getAuthEntryAccount(serverName);
773
+ const payload = JSON.stringify(entry);
774
+ const previousManifest = readExistingChunkManifest(store, serverName, account);
775
+ const chunks = shouldChunkAuthPayload(store, payload) ? splitAuthPayload(payload) : undefined;
776
+ const manifest = chunks ? createChunkManifest(payload, chunks.length) : undefined;
777
+ try {
778
+ if (manifest && chunks) {
779
+ for (const [index, chunk] of chunks.entries()) {
780
+ store.write(getAuthEntryChunkAccount(account, manifest, index), chunk);
781
+ }
782
+ store.write(account, JSON.stringify(manifest));
783
+ }
784
+ else {
785
+ // Compact: multiline secrets corrupt gnome-keyring plaintext (GKeyFile) collections.
786
+ store.write(account, payload);
787
+ }
788
+ if (previousManifest?.chunkDigest !== manifest?.chunkDigest) {
789
+ tryRemoveChunkPayloads(store, account, previousManifest);
790
+ }
791
+ }
792
+ catch (error) {
793
+ tryRemoveChunkPayloads(store, account, manifest);
794
+ throw new OAuthCredentialStoreError(`Failed to write OAuth credentials for ${serverName} to the ${authSecretStoreLabel(store)}`, 'write', error, store.kind);
795
+ }
796
+ }
797
+ function authEntryCacheKey(serverName, options = {}, operation = 'read') {
798
+ if (options.credentialStore !== 'encrypted-file')
799
+ return `os\0${serverName}`;
800
+ try {
801
+ const generation = createHash('sha256').update(getEncryptedFileKey()).digest('hex');
802
+ return `encrypted-file:${generation}\0${serverName}`;
803
+ }
804
+ catch (error) {
805
+ throw new OAuthCredentialStoreError(`Failed to ${operation} OAuth credentials for ${serverName} with the encrypted OAuth credential file store`, operation, error, 'encrypted-file');
806
+ }
807
+ }
808
+ function publishAuthEntryToCache(serverName, payload, options) {
809
+ if (!isAuthEntryCacheEnabled())
810
+ return;
811
+ const cacheKey = authEntryCacheKey(serverName, options, 'write');
812
+ // Cache the same normalized shape a fresh persistent-store read returns.
813
+ const normalized = toAuthEntry(JSON.parse(payload));
814
+ if (!normalized) {
815
+ authEntryCache.delete(cacheKey);
816
+ return;
817
+ }
818
+ authEntryCache.set(cacheKey, cloneAuthEntry(normalized));
819
+ }
820
+ function writeSecureAuthEntry(serverName, entry, options) {
821
+ try {
822
+ writeSecureAuthEntryToStore(getAuthSecretStore(options), serverName, entry);
823
+ }
824
+ catch (error) {
825
+ if (options?.credentialStore === 'encrypted-file' || !shouldAttemptLinuxKeyringRecovery(error))
826
+ throw error;
827
+ writeSecureAuthEntryToStore(linuxKeyringRecoveryAuthSecretStore, serverName, entry);
828
+ }
829
+ publishAuthEntryToCache(serverName, JSON.stringify(entry), options);
830
+ }
831
+ /**
832
+ * Read from the selected store. The OS backend imports and deletes a legacy
833
+ * plaintext entry when present.
834
+ */
835
+ function readAuthEntryFromStore(store, serverName, options, behavior = {}) {
836
+ const account = getAuthEntryAccount(serverName);
837
+ let payload;
838
+ try {
839
+ payload = store.read(account);
840
+ }
841
+ catch (error) {
842
+ throw new OAuthCredentialStoreError(`Failed to read OAuth credentials for ${serverName} from the ${authSecretStoreLabel(store)}`, 'read', error, store.kind);
843
+ }
844
+ if (payload !== undefined) {
845
+ const manifest = store.kind === 'encrypted-file'
846
+ ? undefined
847
+ : readChunkManifestFromPayload(serverName, payload, authSecretStoreLabel(store));
848
+ const entry = manifest
849
+ ? readChunkedAuthEntry(store, serverName, account, manifest)
850
+ : parseAuthEntryPayload(serverName, payload, authSecretStoreLabel(store));
851
+ if (store.kind !== 'encrypted-file')
852
+ removeLegacyAuthEntry(serverName, options);
853
+ if (manifest && behavior.migrateLegacy !== false && !shouldChunkAuthPayload(store, JSON.stringify(entry))) {
854
+ writeSecureAuthEntryToStore(store, serverName, entry);
855
+ }
856
+ return entry;
857
+ }
858
+ if (store.kind === 'encrypted-file')
859
+ return undefined;
860
+ const legacyEntry = readLegacyAuthEntry(serverName, options);
861
+ if (!legacyEntry)
862
+ return undefined;
863
+ if (behavior.migrateLegacy === false)
864
+ return legacyEntry;
865
+ writeSecureAuthEntryToStore(store, serverName, legacyEntry);
866
+ removeLegacyAuthEntry(serverName, options);
867
+ return legacyEntry;
868
+ }
869
+ function readAuthEntry(serverName, options, behavior = {}) {
870
+ // Status-only reads deliberately bypass the cache because they do not
871
+ // migrate legacy entries.
872
+ const cacheable = behavior.migrateLegacy !== false && isAuthEntryCacheEnabled();
873
+ const cacheKey = authEntryCacheKey(serverName, options);
874
+ if (cacheable && authEntryCache.has(cacheKey)) {
875
+ return cloneAuthEntry(authEntryCache.get(cacheKey));
876
+ }
877
+ let entry;
878
+ try {
879
+ entry = readAuthEntryFromStore(getAuthSecretStore(options), serverName, options, behavior);
880
+ }
881
+ catch (error) {
882
+ if (options?.credentialStore === 'encrypted-file' || !shouldAttemptLinuxKeyringRecovery(error))
883
+ throw error;
884
+ entry = readAuthEntryFromStore(linuxKeyringRecoveryAuthSecretStore, serverName, options, behavior);
885
+ }
886
+ if (cacheable)
887
+ authEntryCache.set(cacheKey, cloneAuthEntry(entry));
888
+ return entry;
889
+ }
890
+ /**
891
+ * Get auth entry for a server.
892
+ */
893
+ export function getAuthEntry(serverName, options) {
894
+ return readAuthEntry(serverName, options);
895
+ }
896
+ /**
897
+ * Get auth entry and validate it's for the correct URL.
898
+ * Returns undefined if URL has changed (credentials are invalid).
899
+ */
900
+ export function getAuthForUrl(serverName, serverUrl, options) {
901
+ const entry = getAuthEntry(serverName, options);
902
+ if (!entry)
903
+ return undefined;
904
+ // If no serverUrl is stored, this is from an old version - consider it invalid
905
+ if (!entry.serverUrl)
906
+ return undefined;
907
+ // If URL has changed, credentials are invalid
908
+ if (entry.serverUrl !== serverUrl)
909
+ return undefined;
910
+ return entry;
911
+ }
912
+ /**
913
+ * Inspect credentials for status-only UI paths without treating an unavailable
914
+ * secure store as missing credentials. Authentication operations continue to
915
+ * use getAuthForUrl() directly and therefore remain fail-closed.
916
+ */
917
+ export function inspectAuthForUrl(serverName, serverUrl, options) {
918
+ try {
919
+ const entry = readAuthEntry(serverName, options, { migrateLegacy: false });
920
+ if (!entry?.serverUrl || entry.serverUrl !== serverUrl)
921
+ return { status: 'absent' };
922
+ return { status: 'present', entry };
923
+ }
924
+ catch (error) {
925
+ if (!(error instanceof OAuthCredentialStoreError))
926
+ throw error;
927
+ return { status: 'unavailable', message: formatOAuthCredentialStoreUnavailable(error) };
928
+ }
929
+ }
930
+ /**
931
+ * Save auth entry for a server.
932
+ */
933
+ export function saveAuthEntry(serverName, entry, serverUrl, options) {
934
+ // Always update serverUrl if provided
935
+ if (serverUrl) {
936
+ entry.serverUrl = serverUrl;
937
+ }
938
+ writeSecureAuthEntry(serverName, entry, options);
939
+ if (options?.credentialStore !== 'encrypted-file')
940
+ removeLegacyAuthEntry(serverName, options);
941
+ }
942
+ /**
943
+ * Remove auth entry for a server.
944
+ */
945
+ function removeAuthEntryFromStore(store, serverName) {
946
+ const account = getAuthEntryAccount(serverName);
947
+ try {
948
+ if (store.kind === 'encrypted-file') {
949
+ store.remove(account);
950
+ return;
951
+ }
952
+ const payload = store.read(account);
953
+ const manifest = payload === undefined ? undefined : readChunkManifestFromPayload(serverName, payload, authSecretStoreLabel(store));
954
+ if (manifest)
955
+ removeChunkPayloads(store, account, manifest);
956
+ store.remove(account);
957
+ }
958
+ catch (error) {
959
+ throw new OAuthCredentialStoreError(`Failed to remove OAuth credentials for ${serverName} from the ${authSecretStoreLabel(store)}`, 'remove', error, store.kind);
960
+ }
961
+ }
962
+ export function removeAuthEntry(serverName, options) {
963
+ try {
964
+ removeAuthEntryFromStore(getAuthSecretStore(options), serverName);
965
+ }
966
+ catch (error) {
967
+ if (options?.credentialStore === 'encrypted-file' || !shouldAttemptLinuxKeyringRecovery(error))
968
+ throw error;
969
+ removeAuthEntryFromStore(linuxKeyringRecoveryAuthSecretStore, serverName);
970
+ }
971
+ evictAuthEntryCache(serverName, options);
972
+ if (options?.credentialStore !== 'encrypted-file')
973
+ removeLegacyAuthEntry(serverName, options);
974
+ }
975
+ function evictAuthEntryCache(serverName, options = {}) {
976
+ if (options.credentialStore !== 'encrypted-file') {
977
+ authEntryCache.delete(authEntryCacheKey(serverName, options, 'remove'));
978
+ return;
979
+ }
980
+ for (const key of authEntryCache.keys()) {
981
+ const separator = key.indexOf('\0');
982
+ if (key.startsWith('encrypted-file:') && separator !== -1 && key.slice(separator + 1) === serverName)
983
+ authEntryCache.delete(key);
984
+ }
985
+ }
986
+ /**
987
+ * Forget a cached entry so the next ordinary read reloads secure storage.
988
+ */
989
+ export function invalidateAuthEntryCache(serverName) {
990
+ evictAuthEntryCache(serverName);
991
+ evictAuthEntryCache(serverName, { credentialStore: 'encrypted-file' });
992
+ }
993
+ /**
994
+ * Update tokens for a server.
995
+ */
996
+ export function updateTokens(serverName, tokens, serverUrl, options) {
997
+ const entry = getAuthEntry(serverName, options) ?? {};
998
+ if (serverUrl && entry.serverUrl !== serverUrl) {
999
+ delete entry.clientInfo;
1000
+ delete entry.codeVerifier;
1001
+ delete entry.oauthState;
1002
+ }
1003
+ entry.tokens = tokens;
1004
+ saveAuthEntry(serverName, entry, serverUrl, options);
1005
+ }
1006
+ /**
1007
+ * Update client info for a server.
1008
+ */
1009
+ export function updateClientInfo(serverName, clientInfo, serverUrl, options) {
1010
+ const entry = getAuthEntry(serverName, options) ?? {};
1011
+ if (serverUrl && entry.serverUrl !== serverUrl) {
1012
+ delete entry.tokens;
1013
+ delete entry.codeVerifier;
1014
+ delete entry.oauthState;
1015
+ }
1016
+ entry.clientInfo = clientInfo;
1017
+ saveAuthEntry(serverName, entry, serverUrl, options);
1018
+ }
1019
+ /**
1020
+ * Update code verifier for a server.
1021
+ */
1022
+ export function updateCodeVerifier(serverName, codeVerifier, serverUrl, options) {
1023
+ const entry = getAuthEntry(serverName, options) ?? {};
1024
+ if (serverUrl && entry.serverUrl !== serverUrl) {
1025
+ delete entry.tokens;
1026
+ delete entry.clientInfo;
1027
+ delete entry.oauthState;
1028
+ }
1029
+ entry.codeVerifier = codeVerifier;
1030
+ saveAuthEntry(serverName, entry, serverUrl, options);
1031
+ }
1032
+ /**
1033
+ * Clear code verifier for a server.
1034
+ */
1035
+ export function clearCodeVerifier(serverName, options) {
1036
+ const entry = getAuthEntry(serverName, options);
1037
+ if (entry) {
1038
+ delete entry.codeVerifier;
1039
+ saveAuthEntry(serverName, entry, undefined, options);
1040
+ }
1041
+ }
1042
+ /**
1043
+ * Update OAuth state for a server.
1044
+ */
1045
+ export function updateOAuthState(serverName, state, serverUrl, options) {
1046
+ const entry = getAuthEntry(serverName, options) ?? {};
1047
+ if (serverUrl && entry.serverUrl !== serverUrl) {
1048
+ delete entry.tokens;
1049
+ delete entry.clientInfo;
1050
+ delete entry.codeVerifier;
1051
+ }
1052
+ entry.oauthState = state;
1053
+ saveAuthEntry(serverName, entry, serverUrl, options);
1054
+ }
1055
+ /**
1056
+ * Get OAuth state for a server.
1057
+ */
1058
+ export function getOAuthState(serverName, options) {
1059
+ const entry = getAuthEntry(serverName, options);
1060
+ return entry?.oauthState;
1061
+ }
1062
+ /**
1063
+ * Clear OAuth state for a server.
1064
+ */
1065
+ export function clearOAuthState(serverName, options) {
1066
+ const entry = getAuthEntry(serverName, options);
1067
+ if (entry) {
1068
+ delete entry.oauthState;
1069
+ saveAuthEntry(serverName, entry, undefined, options);
1070
+ }
1071
+ }
1072
+ /**
1073
+ * Check if stored tokens are expired.
1074
+ * Returns null if no tokens exist, false if no expiry or not expired, true if expired.
1075
+ */
1076
+ export function isTokenExpired(serverName, options) {
1077
+ const entry = getAuthEntry(serverName, options);
1078
+ if (!entry?.tokens)
1079
+ return null;
1080
+ if (!entry.tokens.expiresAt)
1081
+ return false;
1082
+ return entry.tokens.expiresAt < Date.now() / 1000;
1083
+ }
1084
+ /**
1085
+ * Check if a server has stored tokens.
1086
+ */
1087
+ export function hasStoredTokens(serverName, options) {
1088
+ const entry = getAuthEntry(serverName, options);
1089
+ return !!entry?.tokens;
1090
+ }
1091
+ /**
1092
+ * Clear all credentials for a server.
1093
+ */
1094
+ export function clearAllCredentials(serverName, options) {
1095
+ removeAuthEntry(serverName, options);
1096
+ }
1097
+ /**
1098
+ * Clear only client info for a server.
1099
+ */
1100
+ export function clearClientInfo(serverName, options) {
1101
+ const entry = getAuthEntry(serverName, options);
1102
+ if (entry) {
1103
+ delete entry.clientInfo;
1104
+ saveAuthEntry(serverName, entry, undefined, options);
1105
+ }
1106
+ }
1107
+ /**
1108
+ * Clear only tokens for a server.
1109
+ */
1110
+ export function clearTokens(serverName, options) {
1111
+ const entry = getAuthEntry(serverName, options);
1112
+ if (entry) {
1113
+ delete entry.tokens;
1114
+ saveAuthEntry(serverName, entry, undefined, options);
1115
+ }
1116
+ }
1117
+ //# sourceMappingURL=mcp-auth.js.map