@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.
- package/CHANGELOG.md +22 -0
- package/dist/bundle/chunks/{anthropic-messages-XULXUBIG.js → anthropic-messages-QFCEKGI3.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-JEG2XOYC.js → azure-openai-responses-G4ZMHV37.js} +1 -1
- package/dist/bundle/chunks/chunk-7T6XCLJF.js +22 -0
- package/dist/bundle/chunks/{chunk-LYAMSPR2.js → chunk-ADQYVZ6A.js} +483 -506
- package/dist/bundle/chunks/chunk-D6HHRSB5.js +103 -0
- package/dist/bundle/chunks/{chunk-JUPSJTFQ.js → chunk-K4JAZYQH.js} +1 -1
- package/dist/bundle/chunks/{chunk-HPPQHK3J.js → chunk-KKIB2TVR.js} +1 -1
- package/dist/bundle/chunks/chunk-LI2YVPWP.js +2 -0
- package/dist/bundle/chunks/chunk-NMPIM7MN.js +2 -0
- package/dist/bundle/chunks/{chunk-BZQ53VGE.js → chunk-XSR434CO.js} +1 -1
- package/dist/bundle/chunks/compat-CN7WN6DA.js +2 -0
- package/dist/bundle/chunks/dist-PRPIUY3U.js +2 -0
- package/dist/bundle/chunks/esm-E7UVEYTO.js +4 -0
- package/dist/bundle/chunks/{extract-TRW5FBY6.js → extract-CTIFQZDJ.js} +15 -14
- package/dist/bundle/chunks/{google-generative-ai-OX6VSCA6.js → google-generative-ai-RSUU4MWO.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-GW76GSS2.js → google-vertex-CKQPOITO.js} +1 -1
- package/dist/bundle/chunks/host-WP3MVKS2.js +2 -0
- package/dist/bundle/chunks/{node-KOIP55YZ.js → node-NF43OPII.js} +1 -1
- package/dist/bundle/chunks/{node-XY3CBY3C.js → node-UMFM7CDZ.js} +1 -1
- package/dist/bundle/chunks/{openai-codex-responses-NPLWP6ZC.js → openai-codex-responses-S34V5ZSD.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-OT2GTJKC.js → openai-completions-T2IGVUEI.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-5A6CR7GI.js → openai-responses-GYZODCT3.js} +1 -1
- package/dist/bundle/chunks/readability-LAGF34IB.js +3 -0
- package/dist/bundle/chunks/turndown.cjs-HRMZVRUL.js +2 -0
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/core/model-runtime.d.ts +21 -0
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js +34 -4
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
- package/dist/core/tools/file-mutation-queue.js +21 -8
- package/dist/core/tools/file-mutation-queue.js.map +1 -1
- package/dist/core/tools/subagent-env.d.ts +20 -0
- package/dist/core/tools/subagent-env.d.ts.map +1 -0
- package/dist/core/tools/subagent-env.js +47 -0
- package/dist/core/tools/subagent-env.js.map +1 -0
- package/dist/core/tools/subagent-scheduler.d.ts +36 -0
- package/dist/core/tools/subagent-scheduler.d.ts.map +1 -0
- package/dist/core/tools/subagent-scheduler.js +213 -0
- package/dist/core/tools/subagent-scheduler.js.map +1 -0
- package/dist/core/tools/subagent.d.ts +83 -3
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +215 -76
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/CHANGELOG.md +78 -0
- package/dist/humain/mcp-adapter/vendor/README.md +125 -19
- package/dist/humain/mcp-adapter/vendor/agent-plugin-loader.ts +69 -30
- package/dist/humain/mcp-adapter/vendor/agent-plugin-provenance.ts +36 -0
- package/dist/humain/mcp-adapter/vendor/claude-plugin-loader.ts +365 -0
- package/dist/humain/mcp-adapter/vendor/commands.ts +13 -15
- package/dist/humain/mcp-adapter/vendor/config.ts +198 -30
- package/dist/humain/mcp-adapter/vendor/consent-manager.ts +40 -9
- package/dist/humain/mcp-adapter/vendor/direct-tool-surface.ts +250 -0
- package/dist/humain/mcp-adapter/vendor/direct-tools.ts +9 -228
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-loader.js +74 -32
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-loader.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.d.ts +1 -0
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.js +31 -0
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.js.map +1 -0
- package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.d.ts +9 -0
- package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.js +362 -0
- package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.js.map +1 -0
- package/dist/humain/mcp-adapter/vendor/dist/config.d.ts +3 -1
- package/dist/humain/mcp-adapter/vendor/dist/config.js +189 -27
- package/dist/humain/mcp-adapter/vendor/dist/config.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/metadata-cache.js +7 -16
- package/dist/humain/mcp-adapter/vendor/dist/metadata-cache.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/package-mcp-loader.js +5 -17
- package/dist/humain/mcp-adapter/vendor/dist/package-mcp-loader.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/types.d.ts +30 -4
- package/dist/humain/mcp-adapter/vendor/dist/types.js +26 -0
- package/dist/humain/mcp-adapter/vendor/dist/types.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/utils.d.ts +6 -0
- package/dist/humain/mcp-adapter/vendor/dist/utils.js +65 -6
- package/dist/humain/mcp-adapter/vendor/dist/utils.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/host-html-template.ts +20 -19
- package/dist/humain/mcp-adapter/vendor/http-ca.ts +84 -0
- package/dist/humain/mcp-adapter/vendor/index.bundle.mjs +37102 -15103
- package/dist/humain/mcp-adapter/vendor/index.ts +828 -164
- package/dist/humain/mcp-adapter/vendor/init.ts +48 -10
- package/dist/humain/mcp-adapter/vendor/lazy-loader.ts +7 -0
- package/dist/humain/mcp-adapter/vendor/mcp-auth-fetch.ts +113 -0
- package/dist/humain/mcp-adapter/vendor/mcp-auth-flow.ts +256 -77
- package/dist/humain/mcp-adapter/vendor/mcp-auth.ts +316 -54
- package/dist/humain/mcp-adapter/vendor/mcp-code.ts +45 -18
- package/dist/humain/mcp-adapter/vendor/mcp-install.ts +60 -0
- package/dist/humain/mcp-adapter/vendor/mcp-oauth-provider.ts +70 -6
- package/dist/humain/mcp-adapter/vendor/mcp-output-guard.ts +53 -42
- package/dist/humain/mcp-adapter/vendor/mcp-panel-theme.ts +105 -0
- package/dist/humain/mcp-adapter/vendor/mcp-panel.ts +366 -331
- package/dist/humain/mcp-adapter/vendor/mcp-references.ts +9 -14
- package/dist/humain/mcp-adapter/vendor/mcp-script-worker.mjs +3 -1
- package/dist/humain/mcp-adapter/vendor/mcp-setup-panel.ts +383 -341
- package/dist/humain/mcp-adapter/vendor/mcp-status.ts +5 -0
- package/dist/humain/mcp-adapter/vendor/metadata-cache.ts +7 -16
- package/dist/humain/mcp-adapter/vendor/namespace-tools.ts +61 -3
- package/dist/humain/mcp-adapter/vendor/oauth.ts +6 -2
- package/dist/humain/mcp-adapter/vendor/package-mcp-loader.ts +5 -17
- package/dist/humain/mcp-adapter/vendor/package.json +13 -3
- package/dist/humain/mcp-adapter/vendor/prompts.ts +35 -3
- package/dist/humain/mcp-adapter/vendor/proxy-modes.ts +108 -7
- package/dist/humain/mcp-adapter/vendor/runtime-owner.ts +16 -1
- package/dist/humain/mcp-adapter/vendor/sampling-handler.ts +9 -32
- package/dist/humain/mcp-adapter/vendor/sandbox-proxy-template.ts +20 -93
- package/dist/humain/mcp-adapter/vendor/server-manager.ts +227 -40
- package/dist/humain/mcp-adapter/vendor/session-approvals.ts +187 -0
- package/dist/humain/mcp-adapter/vendor/skills/mcp-scripting/SKILL.md +9 -1
- package/dist/humain/mcp-adapter/vendor/state.ts +10 -1
- package/dist/humain/mcp-adapter/vendor/tool-approval.ts +10 -22
- package/dist/humain/mcp-adapter/vendor/tool-metadata.ts +14 -0
- package/dist/humain/mcp-adapter/vendor/types.ts +57 -4
- package/dist/humain/mcp-adapter/vendor/ui-server.ts +36 -28
- package/dist/humain/mcp-adapter/vendor/utils.ts +64 -6
- package/dist/humain/web-access/vendor/abortable.d.ts +3 -0
- package/dist/humain/web-access/vendor/abortable.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/abortable.js +22 -0
- package/dist/humain/web-access/vendor/abortable.js.map +1 -0
- package/dist/humain/web-access/vendor/crawl4ai.d.ts +9 -0
- package/dist/humain/web-access/vendor/crawl4ai.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/crawl4ai.js +187 -0
- package/dist/humain/web-access/vendor/crawl4ai.js.map +1 -0
- package/dist/humain/web-access/vendor/credential-source.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/credential-source.js +1 -0
- package/dist/humain/web-access/vendor/credential-source.js.map +1 -1
- package/dist/humain/web-access/vendor/curator-page.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/curator-page.js +20 -2
- package/dist/humain/web-access/vendor/curator-page.js.map +1 -1
- package/dist/humain/web-access/vendor/curator-run.d.ts +11 -0
- package/dist/humain/web-access/vendor/curator-run.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/curator-run.js +38 -0
- package/dist/humain/web-access/vendor/curator-run.js.map +1 -0
- package/dist/humain/web-access/vendor/curator-server.d.ts +1 -0
- package/dist/humain/web-access/vendor/curator-server.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/curator-server.js +2 -0
- package/dist/humain/web-access/vendor/curator-server.js.map +1 -1
- package/dist/humain/web-access/vendor/duckduckgo.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/duckduckgo.js +1 -1
- package/dist/humain/web-access/vendor/duckduckgo.js.map +1 -1
- package/dist/humain/web-access/vendor/extract.d.ts +1 -1
- package/dist/humain/web-access/vendor/extract.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/extract.js +58 -11
- package/dist/humain/web-access/vendor/extract.js.map +1 -1
- package/dist/humain/web-access/vendor/fetch-params.js +1 -1
- package/dist/humain/web-access/vendor/fetch-params.js.map +1 -1
- package/dist/humain/web-access/vendor/gemini-search.d.ts +2 -2
- package/dist/humain/web-access/vendor/gemini-search.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/gemini-search.js +12 -5
- package/dist/humain/web-access/vendor/gemini-search.js.map +1 -1
- package/dist/humain/web-access/vendor/index.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/index.js +23 -19
- package/dist/humain/web-access/vendor/index.js.map +1 -1
- package/dist/humain/web-access/vendor/openai-search.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/openai-search.js +40 -14
- package/dist/humain/web-access/vendor/openai-search.js.map +1 -1
- package/dist/humain/web-access/vendor/page-query.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/page-query.js +6 -2
- package/dist/humain/web-access/vendor/page-query.js.map +1 -1
- package/dist/humain/web-access/vendor/query-rewrite.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/query-rewrite.js +6 -2
- package/dist/humain/web-access/vendor/query-rewrite.js.map +1 -1
- package/dist/humain/web-access/vendor/serpapi.d.ts +4 -0
- package/dist/humain/web-access/vendor/serpapi.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/serpapi.js +215 -0
- package/dist/humain/web-access/vendor/serpapi.js.map +1 -0
- package/dist/humain/web-access/vendor/ssrf-protection.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/ssrf-protection.js +5 -1
- package/dist/humain/web-access/vendor/ssrf-protection.js.map +1 -1
- package/dist/humain/web-access/vendor/storage.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/storage.js +9 -0
- package/dist/humain/web-access/vendor/storage.js.map +1 -1
- package/dist/humain/web-access/vendor/summary-review.d.ts +1 -1
- package/dist/humain/web-access/vendor/summary-review.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/summary-review.js +35 -24
- package/dist/humain/web-access/vendor/summary-review.js.map +1 -1
- package/dist/humain/web-access/vendor/utils.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/utils.js +5 -3
- package/dist/humain/web-access/vendor/utils.js.map +1 -1
- package/dist/modes/json-event.d.ts +8 -0
- package/dist/modes/json-event.d.ts.map +1 -1
- package/dist/modes/json-event.js +17 -0
- package/dist/modes/json-event.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +3 -2
- package/dist/modes/print-mode.js.map +1 -1
- package/docs/environment-variables.md +9 -0
- package/docs/json.md +16 -0
- package/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
- package/src/humain/mcp-adapter/UPSTREAM.md +56 -7
- package/src/humain/mcp-adapter/upstream.json +6 -4
- package/src/humain/mcp-adapter/vendor/CHANGELOG.md +78 -0
- package/src/humain/web-access/UPSTREAM.md +18 -8
- package/src/humain/web-access/upstream.json +6 -5
- 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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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 {
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
603
|
-
chunkDigest:
|
|
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
|
-
|
|
609
|
-
|
|
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
|
-
}
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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(
|
|
923
|
+
authEntryCache.delete(cacheKey);
|
|
688
924
|
return;
|
|
689
925
|
}
|
|
690
|
-
authEntryCache.set(
|
|
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
|
|
704
|
-
*
|
|
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
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
750
|
-
|
|
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(
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1116
|
+
evictAuthEntryCache(serverName);
|
|
1117
|
+
evictAuthEntryCache(serverName, { credentialStore: 'encrypted-file' });
|
|
856
1118
|
}
|
|
857
1119
|
|
|
858
1120
|
/**
|