@humain/terminal 0.1.1 → 0.2.2

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 (197) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/bundle/chunks/{anthropic-messages-XULXUBIG.js → anthropic-messages-QFCEKGI3.js} +1 -1
  3. package/dist/bundle/chunks/{azure-openai-responses-JEG2XOYC.js → azure-openai-responses-G4ZMHV37.js} +1 -1
  4. package/dist/bundle/chunks/chunk-7T6XCLJF.js +22 -0
  5. package/dist/bundle/chunks/{chunk-LYAMSPR2.js → chunk-ADQYVZ6A.js} +483 -506
  6. package/dist/bundle/chunks/chunk-D6HHRSB5.js +103 -0
  7. package/dist/bundle/chunks/{chunk-JUPSJTFQ.js → chunk-K4JAZYQH.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-HPPQHK3J.js → chunk-KKIB2TVR.js} +1 -1
  9. package/dist/bundle/chunks/chunk-LI2YVPWP.js +2 -0
  10. package/dist/bundle/chunks/chunk-NMPIM7MN.js +2 -0
  11. package/dist/bundle/chunks/{chunk-BZQ53VGE.js → chunk-XSR434CO.js} +1 -1
  12. package/dist/bundle/chunks/compat-CN7WN6DA.js +2 -0
  13. package/dist/bundle/chunks/dist-PRPIUY3U.js +2 -0
  14. package/dist/bundle/chunks/esm-E7UVEYTO.js +4 -0
  15. package/dist/bundle/chunks/{extract-TRW5FBY6.js → extract-CTIFQZDJ.js} +15 -14
  16. package/dist/bundle/chunks/{google-generative-ai-OX6VSCA6.js → google-generative-ai-RSUU4MWO.js} +1 -1
  17. package/dist/bundle/chunks/{google-vertex-GW76GSS2.js → google-vertex-CKQPOITO.js} +1 -1
  18. package/dist/bundle/chunks/host-WP3MVKS2.js +2 -0
  19. package/dist/bundle/chunks/{node-KOIP55YZ.js → node-NF43OPII.js} +1 -1
  20. package/dist/bundle/chunks/{node-XY3CBY3C.js → node-UMFM7CDZ.js} +1 -1
  21. package/dist/bundle/chunks/{openai-codex-responses-NPLWP6ZC.js → openai-codex-responses-S34V5ZSD.js} +1 -1
  22. package/dist/bundle/chunks/{openai-completions-OT2GTJKC.js → openai-completions-T2IGVUEI.js} +1 -1
  23. package/dist/bundle/chunks/{openai-responses-5A6CR7GI.js → openai-responses-GYZODCT3.js} +1 -1
  24. package/dist/bundle/chunks/readability-LAGF34IB.js +3 -0
  25. package/dist/bundle/chunks/turndown.cjs-HRMZVRUL.js +2 -0
  26. package/dist/bundle/cli.js +1 -1
  27. package/dist/bundle/index.js +1 -1
  28. package/dist/bundle/rpc-entry.js +1 -1
  29. package/dist/core/model-runtime.d.ts +21 -0
  30. package/dist/core/model-runtime.d.ts.map +1 -1
  31. package/dist/core/model-runtime.js +34 -4
  32. package/dist/core/model-runtime.js.map +1 -1
  33. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
  34. package/dist/core/tools/file-mutation-queue.js +21 -8
  35. package/dist/core/tools/file-mutation-queue.js.map +1 -1
  36. package/dist/core/tools/subagent-env.d.ts +20 -0
  37. package/dist/core/tools/subagent-env.d.ts.map +1 -0
  38. package/dist/core/tools/subagent-env.js +47 -0
  39. package/dist/core/tools/subagent-env.js.map +1 -0
  40. package/dist/core/tools/subagent-scheduler.d.ts +36 -0
  41. package/dist/core/tools/subagent-scheduler.d.ts.map +1 -0
  42. package/dist/core/tools/subagent-scheduler.js +213 -0
  43. package/dist/core/tools/subagent-scheduler.js.map +1 -0
  44. package/dist/core/tools/subagent.d.ts +83 -3
  45. package/dist/core/tools/subagent.d.ts.map +1 -1
  46. package/dist/core/tools/subagent.js +215 -76
  47. package/dist/core/tools/subagent.js.map +1 -1
  48. package/dist/humain/mcp-adapter/vendor/CHANGELOG.md +78 -0
  49. package/dist/humain/mcp-adapter/vendor/README.md +125 -19
  50. package/dist/humain/mcp-adapter/vendor/agent-plugin-loader.ts +69 -30
  51. package/dist/humain/mcp-adapter/vendor/agent-plugin-provenance.ts +36 -0
  52. package/dist/humain/mcp-adapter/vendor/claude-plugin-loader.ts +365 -0
  53. package/dist/humain/mcp-adapter/vendor/commands.ts +13 -15
  54. package/dist/humain/mcp-adapter/vendor/config.ts +198 -30
  55. package/dist/humain/mcp-adapter/vendor/consent-manager.ts +40 -9
  56. package/dist/humain/mcp-adapter/vendor/direct-tool-surface.ts +250 -0
  57. package/dist/humain/mcp-adapter/vendor/direct-tools.ts +9 -228
  58. package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-loader.js +74 -32
  59. package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-loader.js.map +1 -1
  60. package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.d.ts +1 -0
  61. package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.js +31 -0
  62. package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.js.map +1 -0
  63. package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.d.ts +9 -0
  64. package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.js +362 -0
  65. package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.js.map +1 -0
  66. package/dist/humain/mcp-adapter/vendor/dist/config.d.ts +3 -1
  67. package/dist/humain/mcp-adapter/vendor/dist/config.js +189 -27
  68. package/dist/humain/mcp-adapter/vendor/dist/config.js.map +1 -1
  69. package/dist/humain/mcp-adapter/vendor/dist/metadata-cache.js +7 -16
  70. package/dist/humain/mcp-adapter/vendor/dist/metadata-cache.js.map +1 -1
  71. package/dist/humain/mcp-adapter/vendor/dist/package-mcp-loader.js +5 -17
  72. package/dist/humain/mcp-adapter/vendor/dist/package-mcp-loader.js.map +1 -1
  73. package/dist/humain/mcp-adapter/vendor/dist/types.d.ts +30 -4
  74. package/dist/humain/mcp-adapter/vendor/dist/types.js +26 -0
  75. package/dist/humain/mcp-adapter/vendor/dist/types.js.map +1 -1
  76. package/dist/humain/mcp-adapter/vendor/dist/utils.d.ts +6 -0
  77. package/dist/humain/mcp-adapter/vendor/dist/utils.js +65 -6
  78. package/dist/humain/mcp-adapter/vendor/dist/utils.js.map +1 -1
  79. package/dist/humain/mcp-adapter/vendor/host-html-template.ts +20 -19
  80. package/dist/humain/mcp-adapter/vendor/http-ca.ts +84 -0
  81. package/dist/humain/mcp-adapter/vendor/index.bundle.mjs +37102 -15103
  82. package/dist/humain/mcp-adapter/vendor/index.ts +828 -164
  83. package/dist/humain/mcp-adapter/vendor/init.ts +48 -10
  84. package/dist/humain/mcp-adapter/vendor/lazy-loader.ts +7 -0
  85. package/dist/humain/mcp-adapter/vendor/mcp-auth-fetch.ts +113 -0
  86. package/dist/humain/mcp-adapter/vendor/mcp-auth-flow.ts +256 -77
  87. package/dist/humain/mcp-adapter/vendor/mcp-auth.ts +316 -54
  88. package/dist/humain/mcp-adapter/vendor/mcp-code.ts +45 -18
  89. package/dist/humain/mcp-adapter/vendor/mcp-install.ts +60 -0
  90. package/dist/humain/mcp-adapter/vendor/mcp-oauth-provider.ts +70 -6
  91. package/dist/humain/mcp-adapter/vendor/mcp-output-guard.ts +53 -42
  92. package/dist/humain/mcp-adapter/vendor/mcp-panel-theme.ts +105 -0
  93. package/dist/humain/mcp-adapter/vendor/mcp-panel.ts +366 -331
  94. package/dist/humain/mcp-adapter/vendor/mcp-references.ts +9 -14
  95. package/dist/humain/mcp-adapter/vendor/mcp-script-worker.mjs +3 -1
  96. package/dist/humain/mcp-adapter/vendor/mcp-setup-panel.ts +383 -341
  97. package/dist/humain/mcp-adapter/vendor/mcp-status.ts +5 -0
  98. package/dist/humain/mcp-adapter/vendor/metadata-cache.ts +7 -16
  99. package/dist/humain/mcp-adapter/vendor/namespace-tools.ts +61 -3
  100. package/dist/humain/mcp-adapter/vendor/oauth.ts +6 -2
  101. package/dist/humain/mcp-adapter/vendor/package-mcp-loader.ts +5 -17
  102. package/dist/humain/mcp-adapter/vendor/package.json +13 -3
  103. package/dist/humain/mcp-adapter/vendor/prompts.ts +35 -3
  104. package/dist/humain/mcp-adapter/vendor/proxy-modes.ts +108 -7
  105. package/dist/humain/mcp-adapter/vendor/runtime-owner.ts +16 -1
  106. package/dist/humain/mcp-adapter/vendor/sampling-handler.ts +9 -32
  107. package/dist/humain/mcp-adapter/vendor/sandbox-proxy-template.ts +20 -93
  108. package/dist/humain/mcp-adapter/vendor/server-manager.ts +227 -40
  109. package/dist/humain/mcp-adapter/vendor/session-approvals.ts +187 -0
  110. package/dist/humain/mcp-adapter/vendor/skills/mcp-scripting/SKILL.md +9 -1
  111. package/dist/humain/mcp-adapter/vendor/state.ts +10 -1
  112. package/dist/humain/mcp-adapter/vendor/tool-approval.ts +10 -22
  113. package/dist/humain/mcp-adapter/vendor/tool-metadata.ts +14 -0
  114. package/dist/humain/mcp-adapter/vendor/types.ts +57 -4
  115. package/dist/humain/mcp-adapter/vendor/ui-server.ts +36 -28
  116. package/dist/humain/mcp-adapter/vendor/utils.ts +64 -6
  117. package/dist/humain/web-access/vendor/abortable.d.ts +3 -0
  118. package/dist/humain/web-access/vendor/abortable.d.ts.map +1 -0
  119. package/dist/humain/web-access/vendor/abortable.js +22 -0
  120. package/dist/humain/web-access/vendor/abortable.js.map +1 -0
  121. package/dist/humain/web-access/vendor/crawl4ai.d.ts +9 -0
  122. package/dist/humain/web-access/vendor/crawl4ai.d.ts.map +1 -0
  123. package/dist/humain/web-access/vendor/crawl4ai.js +187 -0
  124. package/dist/humain/web-access/vendor/crawl4ai.js.map +1 -0
  125. package/dist/humain/web-access/vendor/credential-source.d.ts.map +1 -1
  126. package/dist/humain/web-access/vendor/credential-source.js +1 -0
  127. package/dist/humain/web-access/vendor/credential-source.js.map +1 -1
  128. package/dist/humain/web-access/vendor/curator-page.d.ts.map +1 -1
  129. package/dist/humain/web-access/vendor/curator-page.js +20 -2
  130. package/dist/humain/web-access/vendor/curator-page.js.map +1 -1
  131. package/dist/humain/web-access/vendor/curator-run.d.ts +11 -0
  132. package/dist/humain/web-access/vendor/curator-run.d.ts.map +1 -0
  133. package/dist/humain/web-access/vendor/curator-run.js +38 -0
  134. package/dist/humain/web-access/vendor/curator-run.js.map +1 -0
  135. package/dist/humain/web-access/vendor/curator-server.d.ts +1 -0
  136. package/dist/humain/web-access/vendor/curator-server.d.ts.map +1 -1
  137. package/dist/humain/web-access/vendor/curator-server.js +2 -0
  138. package/dist/humain/web-access/vendor/curator-server.js.map +1 -1
  139. package/dist/humain/web-access/vendor/duckduckgo.d.ts.map +1 -1
  140. package/dist/humain/web-access/vendor/duckduckgo.js +1 -1
  141. package/dist/humain/web-access/vendor/duckduckgo.js.map +1 -1
  142. package/dist/humain/web-access/vendor/extract.d.ts +1 -1
  143. package/dist/humain/web-access/vendor/extract.d.ts.map +1 -1
  144. package/dist/humain/web-access/vendor/extract.js +58 -11
  145. package/dist/humain/web-access/vendor/extract.js.map +1 -1
  146. package/dist/humain/web-access/vendor/fetch-params.js +1 -1
  147. package/dist/humain/web-access/vendor/fetch-params.js.map +1 -1
  148. package/dist/humain/web-access/vendor/gemini-search.d.ts +2 -2
  149. package/dist/humain/web-access/vendor/gemini-search.d.ts.map +1 -1
  150. package/dist/humain/web-access/vendor/gemini-search.js +12 -5
  151. package/dist/humain/web-access/vendor/gemini-search.js.map +1 -1
  152. package/dist/humain/web-access/vendor/index.d.ts.map +1 -1
  153. package/dist/humain/web-access/vendor/index.js +23 -19
  154. package/dist/humain/web-access/vendor/index.js.map +1 -1
  155. package/dist/humain/web-access/vendor/openai-search.d.ts.map +1 -1
  156. package/dist/humain/web-access/vendor/openai-search.js +40 -14
  157. package/dist/humain/web-access/vendor/openai-search.js.map +1 -1
  158. package/dist/humain/web-access/vendor/page-query.d.ts.map +1 -1
  159. package/dist/humain/web-access/vendor/page-query.js +6 -2
  160. package/dist/humain/web-access/vendor/page-query.js.map +1 -1
  161. package/dist/humain/web-access/vendor/query-rewrite.d.ts.map +1 -1
  162. package/dist/humain/web-access/vendor/query-rewrite.js +6 -2
  163. package/dist/humain/web-access/vendor/query-rewrite.js.map +1 -1
  164. package/dist/humain/web-access/vendor/serpapi.d.ts +4 -0
  165. package/dist/humain/web-access/vendor/serpapi.d.ts.map +1 -0
  166. package/dist/humain/web-access/vendor/serpapi.js +215 -0
  167. package/dist/humain/web-access/vendor/serpapi.js.map +1 -0
  168. package/dist/humain/web-access/vendor/ssrf-protection.d.ts.map +1 -1
  169. package/dist/humain/web-access/vendor/ssrf-protection.js +5 -1
  170. package/dist/humain/web-access/vendor/ssrf-protection.js.map +1 -1
  171. package/dist/humain/web-access/vendor/storage.d.ts.map +1 -1
  172. package/dist/humain/web-access/vendor/storage.js +9 -0
  173. package/dist/humain/web-access/vendor/storage.js.map +1 -1
  174. package/dist/humain/web-access/vendor/summary-review.d.ts +1 -1
  175. package/dist/humain/web-access/vendor/summary-review.d.ts.map +1 -1
  176. package/dist/humain/web-access/vendor/summary-review.js +35 -24
  177. package/dist/humain/web-access/vendor/summary-review.js.map +1 -1
  178. package/dist/humain/web-access/vendor/utils.d.ts.map +1 -1
  179. package/dist/humain/web-access/vendor/utils.js +5 -3
  180. package/dist/humain/web-access/vendor/utils.js.map +1 -1
  181. package/dist/modes/json-event.d.ts +8 -0
  182. package/dist/modes/json-event.d.ts.map +1 -1
  183. package/dist/modes/json-event.js +17 -0
  184. package/dist/modes/json-event.js.map +1 -1
  185. package/dist/modes/print-mode.d.ts.map +1 -1
  186. package/dist/modes/print-mode.js +3 -2
  187. package/dist/modes/print-mode.js.map +1 -1
  188. package/docs/environment-variables.md +9 -0
  189. package/docs/json.md +16 -0
  190. package/npm-shrinkwrap.json +6 -6
  191. package/package.json +2 -2
  192. package/src/humain/mcp-adapter/UPSTREAM.md +56 -7
  193. package/src/humain/mcp-adapter/upstream.json +6 -4
  194. package/src/humain/mcp-adapter/vendor/CHANGELOG.md +78 -0
  195. package/src/humain/web-access/UPSTREAM.md +18 -8
  196. package/src/humain/web-access/upstream.json +6 -5
  197. package/dist/bundle/chunks/chunk-2LC3S3VV.js +0 -105
@@ -4,16 +4,16 @@
4
4
  * Handles secure storage of OAuth credentials, tokens, client information,
5
5
  * and legacy PKCE state for MCP servers.
6
6
  *
7
- * Persistent OAuth entries are stored in the operating system credential store.
8
- * Legacy plaintext entries are imported from $MCP_OAUTH_DIR/sha256-<server-hash>/tokens.json
9
- * when set, otherwise <Pi agent dir>/mcp-oauth/sha256-<server-hash>/tokens.json,
10
- * then the plaintext file is removed.
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
11
  */
12
12
 
13
13
  import { spawnSync } from 'child_process';
14
- import { createHash } from 'crypto';
14
+ import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto';
15
15
  import { createRequire } from 'module';
16
- import { readFileSync, existsSync, rmSync } from 'fs';
16
+ import { chmodSync, closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from 'fs';
17
17
  import { dirname, join } from 'path';
18
18
  import { fileURLToPath } from 'url';
19
19
  import { getAgentPath } from './agent-dir.ts';
@@ -40,6 +40,53 @@ const TEST_LINUX_KEYRING_RECOVERY_ENV = 'PI_MCP_ADAPTER_TEST_LINUX_KEYRING_RECOV
40
40
  const AUTH_CACHE_DISABLED_ENV = 'PI_MCP_ADAPTER_DISABLE_AUTH_CACHE';
41
41
  const KEYRING_RECOVERY_TIMEOUT_MS = 10_000;
42
42
  const AUTH_CHUNK_MANIFEST_KEY = '__piMcpAdapterOAuthChunked';
43
+ const OAUTH_FILE_KEY_ENV = 'PI_MCP_ADAPTER_OAUTH_FILE_KEY';
44
+ const ENCRYPTED_FILE_AAD_CONTEXT = 'pi-mcp-adapter.oauth.encrypted-file.v1';
45
+
46
+ export type OAuthAuthority = () => void;
47
+
48
+ type OAuthLifecycleRecord = {
49
+ generation: object;
50
+ revocations: number;
51
+ };
52
+
53
+ const oauthLifecycleRecords = new Map<string, OAuthLifecycleRecord>();
54
+
55
+ function getOAuthLifecycleRecord(serverName: string): OAuthLifecycleRecord {
56
+ let record = oauthLifecycleRecords.get(serverName);
57
+ if (!record) {
58
+ record = { generation: {}, revocations: 0 };
59
+ oauthLifecycleRecords.set(serverName, record);
60
+ }
61
+ return record;
62
+ }
63
+
64
+ /** Capture immutable process-local authority for one server's OAuth lifecycle. */
65
+ export function captureOAuthAuthority(serverName: string, assertNow = true): OAuthAuthority {
66
+ const record = getOAuthLifecycleRecord(serverName);
67
+ const generation = record.generation;
68
+ const capturedDuringRevocation = record.revocations > 0;
69
+ const assertAuthority = (): void => {
70
+ if (capturedDuringRevocation || record.generation !== generation || record.revocations > 0) {
71
+ throw new Error('OAuth flow is no longer active');
72
+ }
73
+ };
74
+ if (assertNow) assertAuthority();
75
+ return assertAuthority;
76
+ }
77
+
78
+ /** Begin an overlap-safe process-local logout interval. */
79
+ export function beginOAuthRevocation(serverName: string): () => void {
80
+ const record = getOAuthLifecycleRecord(serverName);
81
+ record.generation = {};
82
+ record.revocations += 1;
83
+ let released = false;
84
+ return () => {
85
+ if (released) return;
86
+ released = true;
87
+ record.revocations -= 1;
88
+ };
89
+ }
43
90
 
44
91
  /** OAuth token storage format */
45
92
  export interface StoredTokens {
@@ -82,6 +129,7 @@ export interface AuthEntry {
82
129
  export interface AuthStorageOptions {
83
130
  /** Legacy plaintext import directory. Persistent secrets no longer use this as their store. */
84
131
  baseDir?: string;
132
+ credentialStore?: 'encrypted-file';
85
133
  }
86
134
 
87
135
  export class OAuthCredentialStoreError extends Error {
@@ -91,6 +139,7 @@ export class OAuthCredentialStoreError extends Error {
91
139
  message: string,
92
140
  readonly operation: 'read' | 'write' | 'remove',
93
141
  cause: unknown,
142
+ readonly backend?: 'encrypted-file',
94
143
  ) {
95
144
  super(message, { cause });
96
145
  this.name = 'OAuthCredentialStoreError';
@@ -118,9 +167,18 @@ function causeChainContains(error: unknown, pattern: RegExp): boolean {
118
167
  }
119
168
 
120
169
  export function formatOAuthCredentialStoreUnavailable(error: OAuthCredentialStoreError): string {
170
+ if (error.backend === 'encrypted-file') {
171
+ if (causeChainContains(error, /PI_MCP_ADAPTER_OAUTH_FILE_KEY/)) {
172
+ return 'Encrypted OAuth credential file store unavailable. Set PI_MCP_ADAPTER_OAUTH_FILE_KEY to canonical base64 for exactly 32 random bytes and retry.';
173
+ }
174
+ return 'Encrypted OAuth credential file store unavailable. Check the encrypted credential file and key, then reauthenticate.';
175
+ }
121
176
  if (process.platform === 'linux' && causeChainContains(error, /key\s*(?:has been\s*)?revoked|keyrevoked/i)) {
122
177
  return 'OAuth credential store unavailable: the Linux session keyring may be revoked. Start Pi from a fresh login/keyring session and retry.';
123
178
  }
179
+ if (causeChainContains(error, /ERROR_NO_SUCH_LOGON_SESSION|\b1312\b/i)) {
180
+ 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.';
181
+ }
124
182
  return 'OAuth credential store unavailable. Configure or unlock the OS credential store and retry.';
125
183
  }
126
184
 
@@ -135,11 +193,16 @@ type KeyringModule = { Entry: KeyringEntryConstructor };
135
193
  type KeyringRequire = ((id: string) => unknown) & { resolve(id: string): string };
136
194
 
137
195
  interface AuthSecretStore {
196
+ readonly kind?: 'encrypted-file';
138
197
  read(account: string): string | undefined;
139
198
  write(account: string, payload: string): void;
140
199
  remove(account: string): void;
141
200
  }
142
201
 
202
+ function authSecretStoreLabel(store: AuthSecretStore): string {
203
+ return store.kind === 'encrypted-file' ? 'encrypted OAuth credential file store' : 'OS secure credential store';
204
+ }
205
+
143
206
  interface AuthEntryChunkManifest {
144
207
  [AUTH_CHUNK_MANIFEST_KEY]: 1;
145
208
  chunkCount: number;
@@ -202,6 +265,13 @@ const sizeLimitedAuthSecretStore: AuthSecretStore = {
202
265
  },
203
266
  };
204
267
 
268
+ const writeFailingAuthSecretStore: AuthSecretStore = {
269
+ ...memoryAuthSecretStore,
270
+ write() {
271
+ throw new Error('simulated secure credential store write failure');
272
+ },
273
+ };
274
+
205
275
  const unavailableAuthSecretStore: AuthSecretStore = {
206
276
  read() {
207
277
  testAuthSecretStoreReadCount++;
@@ -254,9 +324,127 @@ export function removeTestAuthSecretStoreEntry(account: string): void {
254
324
  memoryAuthEntries.delete(account);
255
325
  }
256
326
 
257
- function getAuthSecretStore(): AuthSecretStore {
327
+ export function setTestAuthSecretStoreEntry(account: string, payload: string): void {
328
+ memoryAuthEntries.set(account, payload);
329
+ }
330
+
331
+ function decodeCanonicalBase64(value: unknown, expectedBytes?: number): Buffer {
332
+ if (typeof value !== 'string' || value.length === 0 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
333
+ throw new Error('value is not canonical base64');
334
+ }
335
+ const decoded = Buffer.from(value, 'base64');
336
+ if (decoded.toString('base64') !== value || (expectedBytes !== undefined && decoded.length !== expectedBytes)) {
337
+ throw new Error(expectedBytes === undefined ? 'value is not canonical base64' : `value must decode to exactly ${expectedBytes} bytes`);
338
+ }
339
+ return decoded;
340
+ }
341
+
342
+ function getEncryptedFileKey(): Buffer {
343
+ const encoded = process.env[OAUTH_FILE_KEY_ENV];
344
+ if (!encoded) throw new Error(`${OAUTH_FILE_KEY_ENV} is required for the encrypted OAuth credential file store`);
345
+ try {
346
+ return decodeCanonicalBase64(encoded, 32);
347
+ } catch (error) {
348
+ throw new Error(`${OAUTH_FILE_KEY_ENV} must be canonical base64 for exactly 32 random bytes`, { cause: error });
349
+ }
350
+ }
351
+
352
+ function encryptedEntryPath(root: string, account: string): string {
353
+ return join(root, account, 'credentials.json');
354
+ }
355
+
356
+ function validatePrivateRegularFile(path: string): boolean {
357
+ let stat;
358
+ try {
359
+ stat = lstatSync(path);
360
+ } catch (error) {
361
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;
362
+ throw error;
363
+ }
364
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Refusing non-regular OAuth credential file at ${path}`);
365
+ if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0) {
366
+ throw new Error(`OAuth credential file has group or other permissions at ${path}`);
367
+ }
368
+ return true;
369
+ }
370
+
371
+ function ensurePrivateDirectory(path: string): void {
372
+ mkdirSync(path, { recursive: true, mode: 0o700 });
373
+ const stat = lstatSync(path);
374
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`Refusing non-directory OAuth credential path at ${path}`);
375
+ if (process.platform !== 'win32') chmodSync(path, 0o700);
376
+ }
377
+
378
+ function createEncryptedFileAuthSecretStore(): AuthSecretStore {
379
+ const root = getAgentPath('mcp-oauth-encrypted');
380
+ return {
381
+ kind: 'encrypted-file',
382
+ read(account) {
383
+ const key = getEncryptedFileKey();
384
+ const path = encryptedEntryPath(root, account);
385
+ if (!validatePrivateRegularFile(path)) return undefined;
386
+ const envelope = parseJsonPayload(account, readFileSync(path, 'utf8'), path) as Record<string, unknown>;
387
+ if (!envelope || envelope.version !== 1 || envelope.algorithm !== 'aes-256-gcm') {
388
+ throw new Error(`Unsupported encrypted OAuth credential envelope at ${path}`);
389
+ }
390
+ const iv = decodeCanonicalBase64(envelope.iv, 12);
391
+ const ciphertext = decodeCanonicalBase64(envelope.ciphertext);
392
+ const tag = decodeCanonicalBase64(envelope.tag, 16);
393
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
394
+ decipher.setAAD(Buffer.from(`${ENCRYPTED_FILE_AAD_CONTEXT}\0${account}`, 'utf8'));
395
+ decipher.setAuthTag(tag);
396
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
397
+ },
398
+ write(account, payload) {
399
+ const key = getEncryptedFileKey();
400
+ const iv = randomBytes(12);
401
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
402
+ cipher.setAAD(Buffer.from(`${ENCRYPTED_FILE_AAD_CONTEXT}\0${account}`, 'utf8'));
403
+ const ciphertext = Buffer.concat([cipher.update(payload, 'utf8'), cipher.final()]);
404
+ const envelope = JSON.stringify({
405
+ version: 1,
406
+ algorithm: 'aes-256-gcm',
407
+ iv: iv.toString('base64'),
408
+ ciphertext: ciphertext.toString('base64'),
409
+ tag: cipher.getAuthTag().toString('base64'),
410
+ });
411
+ const dir = join(root, account);
412
+ ensurePrivateDirectory(root);
413
+ ensurePrivateDirectory(dir);
414
+ const destination = encryptedEntryPath(root, account);
415
+ validatePrivateRegularFile(destination);
416
+ const temporary = join(dir, `.credentials-${randomBytes(12).toString('hex')}.tmp`);
417
+ let fd: number | undefined;
418
+ try {
419
+ fd = openSync(temporary, 'wx', 0o600);
420
+ writeFileSync(fd, envelope, 'utf8');
421
+ fsyncSync(fd);
422
+ closeSync(fd);
423
+ fd = undefined;
424
+ renameSync(temporary, destination);
425
+ } catch (error) {
426
+ if (fd !== undefined) {
427
+ try { closeSync(fd); } catch {}
428
+ }
429
+ try { rmSync(temporary, { force: true }); } catch {}
430
+ throw error;
431
+ }
432
+ },
433
+ remove(account) {
434
+ getEncryptedFileKey();
435
+ const path = encryptedEntryPath(root, account);
436
+ if (!validatePrivateRegularFile(path)) return;
437
+ rmSync(path);
438
+ try { rmSync(dirname(path)); } catch {}
439
+ },
440
+ };
441
+ }
442
+
443
+ function getAuthSecretStore(options: AuthStorageOptions = {}): AuthSecretStore {
444
+ if (options.credentialStore === 'encrypted-file') return createEncryptedFileAuthSecretStore();
258
445
  if (process.env[TEST_AUTH_STORE_ENV] === 'memory') return memoryAuthSecretStore;
259
446
  if (process.env[TEST_AUTH_STORE_ENV] === 'sizelimited') return sizeLimitedAuthSecretStore;
447
+ if (process.env[TEST_AUTH_STORE_ENV] === 'writefailing') return writeFailingAuthSecretStore;
260
448
  if (process.env[TEST_AUTH_STORE_ENV] === 'unavailable') return unavailableAuthSecretStore;
261
449
  if (process.env[TEST_AUTH_STORE_ENV] === 'keyrevoked') return keyRevokedAuthSecretStore;
262
450
  return keyringAuthSecretStore;
@@ -408,7 +596,11 @@ export function loadTestKeyringEntryClass(keyringRequire: KeyringRequire, platfo
408
596
  return loadKeyringEntryClass(keyringRequire, platform, arch);
409
597
  }
410
598
 
411
- export function getAuthStorageOptions(oauthDir: unknown, cwd = process.cwd()): AuthStorageOptions {
599
+ export function getAuthStorageOptions(oauthDir: unknown, cwd = process.cwd(), oauthCredentialStore?: unknown): AuthStorageOptions {
600
+ if (oauthCredentialStore !== undefined && oauthCredentialStore !== 'encrypted-file') {
601
+ throw new Error('settings.oauthCredentialStore must be "encrypted-file" when set');
602
+ }
603
+ if (oauthCredentialStore === 'encrypted-file') return { credentialStore: 'encrypted-file' };
412
604
  const baseDir = resolveConfiguredOAuthDir(oauthDir, cwd);
413
605
  return baseDir ? { baseDir } : {};
414
606
  }
@@ -575,7 +767,7 @@ function readChunkManifestFromPayload(serverName: string, payload: string, sourc
575
767
  function readExistingChunkManifest(store: AuthSecretStore, serverName: string, account: string): AuthEntryChunkManifest | undefined {
576
768
  try {
577
769
  const payload = store.read(account);
578
- return payload === undefined ? undefined : readChunkManifestFromPayload(serverName, payload, 'OS secure credential store');
770
+ return payload === undefined ? undefined : readChunkManifestFromPayload(serverName, payload, authSecretStoreLabel(store));
579
771
  } catch {
580
772
  return undefined;
581
773
  }
@@ -596,31 +788,56 @@ function tryRemoveChunkPayloads(store: AuthSecretStore, account: string, manifes
596
788
  }
597
789
  }
598
790
 
599
- function createChunkManifest(payload: string): AuthEntryChunkManifest {
791
+ function shouldChunkAuthPayload(store: AuthSecretStore, payload: string): boolean {
792
+ return store.kind !== 'encrypted-file' && payload.length > AUTH_SECRET_CHUNK_SIZE
793
+ && (process.platform === 'win32' || process.env[TEST_AUTH_STORE_ENV] === 'sizelimited');
794
+ }
795
+
796
+ function getAuthEntryChunkDigest(payload: string): string {
797
+ return createHash('sha256').update(payload, 'utf8').digest('hex').slice(0, 16);
798
+ }
799
+
800
+ function splitAuthPayload(payload: string): string[] {
801
+ const chunks: string[] = [];
802
+ for (let start = 0; start < payload.length;) {
803
+ let end = Math.min(start + AUTH_SECRET_CHUNK_SIZE, payload.length);
804
+ const lastCodeUnit = payload.charCodeAt(end - 1);
805
+ if (end < payload.length && lastCodeUnit >= 0xd800 && lastCodeUnit <= 0xdbff) end--;
806
+ chunks.push(payload.slice(start, end));
807
+ start = end;
808
+ }
809
+ return chunks;
810
+ }
811
+
812
+ function createChunkManifest(payload: string, chunkCount: number): AuthEntryChunkManifest {
600
813
  return {
601
814
  [AUTH_CHUNK_MANIFEST_KEY]: 1,
602
- chunkCount: Math.ceil(payload.length / AUTH_SECRET_CHUNK_SIZE),
603
- chunkDigest: createHash('sha256').update(payload, 'utf8').digest('hex').slice(0, 16),
815
+ chunkCount,
816
+ chunkDigest: getAuthEntryChunkDigest(payload),
604
817
  };
605
818
  }
606
819
 
607
820
  function readChunkedAuthEntry(store: AuthSecretStore, serverName: string, account: string, manifest: AuthEntryChunkManifest): AuthEntry {
608
- const chunks = getAuthEntryChunkAccounts(account, manifest).map((chunkAccount) => {
609
- try {
821
+ let payload: string;
822
+ try {
823
+ payload = getAuthEntryChunkAccounts(account, manifest).map((chunkAccount) => {
610
824
  const chunk = store.read(chunkAccount);
611
825
  if (chunk === undefined) {
612
826
  throw new Error(`Missing OAuth credential chunk ${chunkAccount} for ${serverName}`);
613
827
  }
614
828
  return chunk;
615
- } catch (error) {
616
- throw new OAuthCredentialStoreError(
617
- `Failed to read OAuth credentials for ${serverName} from the OS secure credential store`,
618
- 'read',
619
- error,
620
- );
829
+ }).join('');
830
+ if (getAuthEntryChunkDigest(payload) !== manifest.chunkDigest) {
831
+ throw new Error('OAuth credential chunk integrity check failed');
621
832
  }
622
- });
623
- return parseAuthEntryPayload(serverName, chunks.join(''), 'OS secure credential store chunks');
833
+ } catch (error) {
834
+ throw new OAuthCredentialStoreError(
835
+ `Failed to read OAuth credentials for ${serverName} from the ${authSecretStoreLabel(store)}`,
836
+ 'read',
837
+ error,
838
+ );
839
+ }
840
+ return parseAuthEntryPayload(serverName, payload, `${authSecretStoreLabel(store)} chunks`);
624
841
  }
625
842
 
626
843
  function readLegacyAuthEntry(serverName: string, options?: AuthStorageOptions): AuthEntry | undefined {
@@ -651,12 +868,12 @@ function writeSecureAuthEntryToStore(store: AuthSecretStore, serverName: string,
651
868
  const account = getAuthEntryAccount(serverName);
652
869
  const payload = JSON.stringify(entry);
653
870
  const previousManifest = readExistingChunkManifest(store, serverName, account);
654
- const manifest = payload.length > AUTH_SECRET_CHUNK_SIZE ? createChunkManifest(payload) : undefined;
871
+ const chunks = shouldChunkAuthPayload(store, payload) ? splitAuthPayload(payload) : undefined;
872
+ const manifest = chunks ? createChunkManifest(payload, chunks.length) : undefined;
655
873
 
656
874
  try {
657
- if (manifest) {
658
- for (let index = 0; index < manifest.chunkCount; index++) {
659
- const chunk = payload.slice(index * AUTH_SECRET_CHUNK_SIZE, (index + 1) * AUTH_SECRET_CHUNK_SIZE);
875
+ if (manifest && chunks) {
876
+ for (const [index, chunk] of chunks.entries()) {
660
877
  store.write(getAuthEntryChunkAccount(account, manifest, index), chunk);
661
878
  }
662
879
  store.write(account, JSON.stringify(manifest));
@@ -670,38 +887,58 @@ function writeSecureAuthEntryToStore(store: AuthSecretStore, serverName: string,
670
887
  } catch (error) {
671
888
  tryRemoveChunkPayloads(store, account, manifest);
672
889
  throw new OAuthCredentialStoreError(
673
- `Failed to write OAuth credentials for ${serverName} to the OS secure credential store`,
890
+ `Failed to write OAuth credentials for ${serverName} to the ${authSecretStoreLabel(store)}`,
674
891
  'write',
675
892
  error,
893
+ store.kind,
676
894
  );
677
895
  }
896
+ }
678
897
 
679
- publishAuthEntryToCache(serverName, payload);
898
+ function authEntryCacheKey(
899
+ serverName: string,
900
+ options: AuthStorageOptions = {},
901
+ operation: 'read' | 'write' | 'remove' = 'read',
902
+ ): string {
903
+ if (options.credentialStore !== 'encrypted-file') return `os\0${serverName}`;
904
+ try {
905
+ const generation = createHash('sha256').update(getEncryptedFileKey()).digest('hex');
906
+ return `encrypted-file:${generation}\0${serverName}`;
907
+ } catch (error) {
908
+ throw new OAuthCredentialStoreError(
909
+ `Failed to ${operation} OAuth credentials for ${serverName} with the encrypted OAuth credential file store`,
910
+ operation,
911
+ error,
912
+ 'encrypted-file',
913
+ );
914
+ }
680
915
  }
681
916
 
682
- function publishAuthEntryToCache(serverName: string, payload: string): void {
917
+ function publishAuthEntryToCache(serverName: string, payload: string, options?: AuthStorageOptions): void {
683
918
  if (!isAuthEntryCacheEnabled()) return;
919
+ const cacheKey = authEntryCacheKey(serverName, options, 'write');
684
920
  // Cache the same normalized shape a fresh persistent-store read returns.
685
921
  const normalized = toAuthEntry(JSON.parse(payload) as unknown);
686
922
  if (!normalized) {
687
- authEntryCache.delete(serverName);
923
+ authEntryCache.delete(cacheKey);
688
924
  return;
689
925
  }
690
- authEntryCache.set(serverName, cloneAuthEntry(normalized));
926
+ authEntryCache.set(cacheKey, cloneAuthEntry(normalized));
691
927
  }
692
928
 
693
- function writeSecureAuthEntry(serverName: string, entry: AuthEntry): void {
929
+ function writeSecureAuthEntry(serverName: string, entry: AuthEntry, options?: AuthStorageOptions): void {
694
930
  try {
695
- writeSecureAuthEntryToStore(getAuthSecretStore(), serverName, entry);
931
+ writeSecureAuthEntryToStore(getAuthSecretStore(options), serverName, entry);
696
932
  } catch (error) {
697
- if (!shouldAttemptLinuxKeyringRecovery(error)) throw error;
933
+ if (options?.credentialStore === 'encrypted-file' || !shouldAttemptLinuxKeyringRecovery(error)) throw error;
698
934
  writeSecureAuthEntryToStore(linuxKeyringRecoveryAuthSecretStore, serverName, entry);
699
935
  }
936
+ publishAuthEntryToCache(serverName, JSON.stringify(entry), options);
700
937
  }
701
938
 
702
939
  /**
703
- * Read the auth entry for a server from the OS secure store, importing and
704
- * deleting a legacy plaintext entry when present.
940
+ * Read from the selected store. The OS backend imports and deletes a legacy
941
+ * plaintext entry when present.
705
942
  */
706
943
  function readAuthEntryFromStore(
707
944
  store: AuthSecretStore,
@@ -715,21 +952,28 @@ function readAuthEntryFromStore(
715
952
  payload = store.read(account);
716
953
  } catch (error) {
717
954
  throw new OAuthCredentialStoreError(
718
- `Failed to read OAuth credentials for ${serverName} from the OS secure credential store`,
955
+ `Failed to read OAuth credentials for ${serverName} from the ${authSecretStoreLabel(store)}`,
719
956
  'read',
720
957
  error,
958
+ store.kind,
721
959
  );
722
960
  }
723
961
 
724
962
  if (payload !== undefined) {
725
- const manifest = readChunkManifestFromPayload(serverName, payload, 'OS secure credential store');
963
+ const manifest = store.kind === 'encrypted-file'
964
+ ? undefined
965
+ : readChunkManifestFromPayload(serverName, payload, authSecretStoreLabel(store));
726
966
  const entry = manifest
727
967
  ? readChunkedAuthEntry(store, serverName, account, manifest)
728
- : parseAuthEntryPayload(serverName, payload, 'OS secure credential store');
729
- removeLegacyAuthEntry(serverName, options);
968
+ : parseAuthEntryPayload(serverName, payload, authSecretStoreLabel(store));
969
+ if (store.kind !== 'encrypted-file') removeLegacyAuthEntry(serverName, options);
970
+ if (manifest && behavior.migrateLegacy !== false && !shouldChunkAuthPayload(store, JSON.stringify(entry))) {
971
+ writeSecureAuthEntryToStore(store, serverName, entry);
972
+ }
730
973
  return entry;
731
974
  }
732
975
 
976
+ if (store.kind === 'encrypted-file') return undefined;
733
977
  const legacyEntry = readLegacyAuthEntry(serverName, options);
734
978
  if (!legacyEntry) return undefined;
735
979
  if (behavior.migrateLegacy === false) return legacyEntry;
@@ -746,19 +990,20 @@ function readAuthEntry(
746
990
  // Status-only reads deliberately bypass the cache because they do not
747
991
  // migrate legacy entries.
748
992
  const cacheable = behavior.migrateLegacy !== false && isAuthEntryCacheEnabled();
749
- if (cacheable && authEntryCache.has(serverName)) {
750
- return cloneAuthEntry(authEntryCache.get(serverName));
993
+ const cacheKey = authEntryCacheKey(serverName, options);
994
+ if (cacheable && authEntryCache.has(cacheKey)) {
995
+ return cloneAuthEntry(authEntryCache.get(cacheKey));
751
996
  }
752
997
 
753
998
  let entry: AuthEntry | undefined;
754
999
  try {
755
- entry = readAuthEntryFromStore(getAuthSecretStore(), serverName, options, behavior);
1000
+ entry = readAuthEntryFromStore(getAuthSecretStore(options), serverName, options, behavior);
756
1001
  } catch (error) {
757
- if (!shouldAttemptLinuxKeyringRecovery(error)) throw error;
1002
+ if (options?.credentialStore === 'encrypted-file' || !shouldAttemptLinuxKeyringRecovery(error)) throw error;
758
1003
  entry = readAuthEntryFromStore(linuxKeyringRecoveryAuthSecretStore, serverName, options, behavior);
759
1004
  }
760
1005
 
761
- if (cacheable) authEntryCache.set(serverName, cloneAuthEntry(entry));
1006
+ if (cacheable) authEntryCache.set(cacheKey, cloneAuthEntry(entry));
762
1007
  return entry;
763
1008
  }
764
1009
 
@@ -814,8 +1059,8 @@ export function saveAuthEntry(serverName: string, entry: AuthEntry, serverUrl?:
814
1059
  if (serverUrl) {
815
1060
  entry.serverUrl = serverUrl;
816
1061
  }
817
- writeSecureAuthEntry(serverName, entry);
818
- removeLegacyAuthEntry(serverName, options);
1062
+ writeSecureAuthEntry(serverName, entry, options);
1063
+ if (options?.credentialStore !== 'encrypted-file') removeLegacyAuthEntry(serverName, options);
819
1064
  }
820
1065
 
821
1066
  /**
@@ -824,35 +1069,52 @@ export function saveAuthEntry(serverName: string, entry: AuthEntry, serverUrl?:
824
1069
  function removeAuthEntryFromStore(store: AuthSecretStore, serverName: string): void {
825
1070
  const account = getAuthEntryAccount(serverName);
826
1071
  try {
1072
+ if (store.kind === 'encrypted-file') {
1073
+ store.remove(account);
1074
+ return;
1075
+ }
827
1076
  const payload = store.read(account);
828
- const manifest = payload === undefined ? undefined : readChunkManifestFromPayload(serverName, payload, 'OS secure credential store');
1077
+ const manifest = payload === undefined ? undefined : readChunkManifestFromPayload(serverName, payload, authSecretStoreLabel(store));
829
1078
  if (manifest) removeChunkPayloads(store, account, manifest);
830
1079
  store.remove(account);
831
1080
  } catch (error) {
832
1081
  throw new OAuthCredentialStoreError(
833
- `Failed to remove OAuth credentials for ${serverName} from the OS secure credential store`,
1082
+ `Failed to remove OAuth credentials for ${serverName} from the ${authSecretStoreLabel(store)}`,
834
1083
  'remove',
835
1084
  error,
1085
+ store.kind,
836
1086
  );
837
1087
  }
838
1088
  }
839
1089
 
840
1090
  export function removeAuthEntry(serverName: string, options?: AuthStorageOptions): void {
841
1091
  try {
842
- removeAuthEntryFromStore(getAuthSecretStore(), serverName);
1092
+ removeAuthEntryFromStore(getAuthSecretStore(options), serverName);
843
1093
  } catch (error) {
844
- if (!shouldAttemptLinuxKeyringRecovery(error)) throw error;
1094
+ if (options?.credentialStore === 'encrypted-file' || !shouldAttemptLinuxKeyringRecovery(error)) throw error;
845
1095
  removeAuthEntryFromStore(linuxKeyringRecoveryAuthSecretStore, serverName);
846
1096
  }
847
- authEntryCache.delete(serverName);
848
- removeLegacyAuthEntry(serverName, options);
1097
+ evictAuthEntryCache(serverName, options);
1098
+ if (options?.credentialStore !== 'encrypted-file') removeLegacyAuthEntry(serverName, options);
1099
+ }
1100
+
1101
+ function evictAuthEntryCache(serverName: string, options: AuthStorageOptions = {}): void {
1102
+ if (options.credentialStore !== 'encrypted-file') {
1103
+ authEntryCache.delete(authEntryCacheKey(serverName, options, 'remove'));
1104
+ return;
1105
+ }
1106
+ for (const key of authEntryCache.keys()) {
1107
+ const separator = key.indexOf('\0');
1108
+ if (key.startsWith('encrypted-file:') && separator !== -1 && key.slice(separator + 1) === serverName) authEntryCache.delete(key);
1109
+ }
849
1110
  }
850
1111
 
851
1112
  /**
852
1113
  * Forget a cached entry so the next ordinary read reloads secure storage.
853
1114
  */
854
1115
  export function invalidateAuthEntryCache(serverName: string): void {
855
- authEntryCache.delete(serverName);
1116
+ evictAuthEntryCache(serverName);
1117
+ evictAuthEntryCache(serverName, { credentialStore: 'encrypted-file' });
856
1118
  }
857
1119
 
858
1120
  /**