pi-mcp-adapter 2.16.0 → 2.18.0
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 +29 -0
- package/README.md +58 -8
- package/commands.ts +8 -0
- package/config.ts +40 -0
- package/direct-tools.ts +25 -0
- package/host-html-template.ts +54 -16
- package/index.ts +64 -2
- package/init.ts +27 -6
- package/mcp-auth-flow.ts +18 -2
- package/mcp-auth.ts +205 -12
- package/mcp-code.ts +295 -0
- package/mcp-keyring-helper.cjs +89 -0
- package/mcp-oauth-provider.ts +25 -1
- package/mcp-probe.ts +90 -0
- package/mcp-script-worker.mjs +115 -0
- package/mcp-setup-panel.ts +104 -34
- package/metadata-cache.ts +6 -0
- package/package.json +13 -1
- package/proxy-modes.ts +104 -62
- package/search-ranking.ts +126 -0
- package/server-manager.ts +15 -1
- package/skills/mcp-scripting/SKILL.md +36 -0
- package/state.ts +2 -0
- package/tool-approval.ts +78 -0
- package/tool-metadata.ts +7 -0
- package/tool-result-renderer.ts +62 -10
- package/ts-shape.ts +142 -0
- package/types.ts +47 -4
- package/ui-server.ts +178 -19
- package/ui-session.ts +101 -10
- package/ui-tool-visibility.ts +25 -0
- package/utils.ts +2 -1
package/init.ts
CHANGED
|
@@ -147,6 +147,7 @@ export async function initializeMcp(
|
|
|
147
147
|
const serverInstructions = new Map<string, string>();
|
|
148
148
|
const failureTracker = new Map<string, number>();
|
|
149
149
|
const failureMessages = new Map<string, string>();
|
|
150
|
+
const approvedToolCalls = new Map<string, true>();
|
|
150
151
|
const uiResourceHandler = new UiResourceHandler(manager, config);
|
|
151
152
|
const consentManager = new ConsentManager("once-per-server");
|
|
152
153
|
const state: McpExtensionState = {
|
|
@@ -164,6 +165,7 @@ export async function initializeMcp(
|
|
|
164
165
|
authStorageOptions,
|
|
165
166
|
failureTracker,
|
|
166
167
|
failureMessages,
|
|
168
|
+
approvedToolCalls,
|
|
167
169
|
uiResourceHandler,
|
|
168
170
|
consentManager,
|
|
169
171
|
uiServer: null,
|
|
@@ -261,7 +263,8 @@ export async function initializeMcp(
|
|
|
261
263
|
});
|
|
262
264
|
|
|
263
265
|
if (ui && startupServers.length > 0) {
|
|
264
|
-
|
|
266
|
+
const status = formatMcpStatus(state.config, `connecting to ${startupServers.length} servers...`);
|
|
267
|
+
ui.setStatus("mcp", status);
|
|
265
268
|
}
|
|
266
269
|
|
|
267
270
|
const results = await parallelLimit(startupServers, 10, async ([name, definition]) => {
|
|
@@ -399,6 +402,9 @@ export async function initializeMcp(
|
|
|
399
402
|
|
|
400
403
|
owner.throwIfInactive();
|
|
401
404
|
lifecycle.startHealthChecks(runtimeSignal);
|
|
405
|
+
if (config.settings?.mcpFooterStatus === "off") {
|
|
406
|
+
ui?.setStatus("mcp", undefined);
|
|
407
|
+
}
|
|
402
408
|
publishMcpStatusSnapshot(state);
|
|
403
409
|
|
|
404
410
|
return state;
|
|
@@ -524,10 +530,24 @@ export function updateStatusBar(state: McpExtensionState): void {
|
|
|
524
530
|
const definition = state.config.mcpServers[name];
|
|
525
531
|
return connection.status === "connected" && definition !== undefined && !isServerDisabled(definition);
|
|
526
532
|
}).length;
|
|
527
|
-
|
|
528
|
-
if (
|
|
529
|
-
|
|
530
|
-
|
|
533
|
+
const footerStatus = state.config.settings?.mcpFooterStatus ?? "full";
|
|
534
|
+
if (footerStatus === "off") {
|
|
535
|
+
ui.setStatus("mcp", undefined);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
let status = footerStatus === "compact"
|
|
540
|
+
? `MCP ${connectedCount}/${enabledCount}`
|
|
541
|
+
: `${enabledCount} ${enabledCount === 1 ? "server" : "servers"} enabled`;
|
|
542
|
+
if (footerStatus === "full") {
|
|
543
|
+
if (connectedCount > 0) status += ` (${connectedCount} connected)`;
|
|
544
|
+
if (disabledCount > 0) status += ` (${disabledCount} disabled)`;
|
|
545
|
+
}
|
|
546
|
+
const formattedStatus = footerStatus === "compact" ? status : formatMcpStatus(state.config, status);
|
|
547
|
+
if (formattedStatus === undefined) {
|
|
548
|
+
ui.setStatus("mcp", undefined);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
531
551
|
ui.setStatus("mcp", ui.theme ? ui.theme.fg("accent", formattedStatus) : formattedStatus);
|
|
532
552
|
}
|
|
533
553
|
|
|
@@ -565,7 +585,8 @@ export async function lazyConnect(state: McpExtensionState, serverName: string,
|
|
|
565
585
|
|
|
566
586
|
try {
|
|
567
587
|
if (state.ui) {
|
|
568
|
-
|
|
588
|
+
const status = formatMcpStatus(state.config, `connecting to ${serverName}...`);
|
|
589
|
+
state.ui.setStatus("mcp", status);
|
|
569
590
|
}
|
|
570
591
|
const newConnection = await state.manager.connect(serverName, definition, ownedSignal);
|
|
571
592
|
if (newConnection.status === "needs-auth") {
|
package/mcp-auth-flow.ts
CHANGED
|
@@ -57,6 +57,10 @@ type AuthDiscovery = {
|
|
|
57
57
|
scope?: string
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function applyConfiguredScope(discovery: AuthDiscovery, config: McpOAuthConfig): AuthDiscovery {
|
|
61
|
+
return config.scope !== undefined ? { ...discovery, scope: config.scope } : discovery
|
|
62
|
+
}
|
|
63
|
+
|
|
60
64
|
type PendingAuth = {
|
|
61
65
|
serverName: string
|
|
62
66
|
authProvider: McpOAuthProvider
|
|
@@ -162,6 +166,18 @@ export function extractOAuthConfig(definition: ServerEntry): McpOAuthConfig {
|
|
|
162
166
|
if (typeof definition.oauth.scope !== "string") throw new Error("OAuth scope must be a string")
|
|
163
167
|
config.scope = interpolateEnvVars(definition.oauth.scope)
|
|
164
168
|
}
|
|
169
|
+
if (definition.oauth?.authorizationParams !== undefined) {
|
|
170
|
+
const params = definition.oauth.authorizationParams
|
|
171
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) {
|
|
172
|
+
throw new Error("OAuth authorizationParams must be an object")
|
|
173
|
+
}
|
|
174
|
+
config.authorizationParams = {}
|
|
175
|
+
for (const [key, value] of Object.entries(params)) {
|
|
176
|
+
if (!key) throw new Error("OAuth authorizationParams keys must not be empty")
|
|
177
|
+
if (typeof value !== "string") throw new Error(`OAuth authorizationParams.${key} must be a string`)
|
|
178
|
+
config.authorizationParams[key] = interpolateEnvVars(value)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
165
181
|
if (definition.oauth?.redirectUri !== undefined) {
|
|
166
182
|
if (typeof definition.oauth.redirectUri !== "string") {
|
|
167
183
|
throw new Error("OAuth redirectUri must be a string")
|
|
@@ -304,7 +320,7 @@ export async function startAuth(
|
|
|
304
320
|
},
|
|
305
321
|
}, authStorageOptions, runtime.signal)
|
|
306
322
|
try {
|
|
307
|
-
const discovery = await probeAuthDiscovery(serverUrl, definition, signal)
|
|
323
|
+
const discovery = applyConfiguredScope(await probeAuthDiscovery(serverUrl, definition, signal), config)
|
|
308
324
|
throwIfAborted(signal)
|
|
309
325
|
const result = await abortable(runSdkAuth(authProvider, { serverUrl, ...discovery }), signal)
|
|
310
326
|
throwIfAborted(signal)
|
|
@@ -370,7 +386,7 @@ export async function startAuth(
|
|
|
370
386
|
|
|
371
387
|
throwIfAborted(signal)
|
|
372
388
|
|
|
373
|
-
const discovery = await probeAuthDiscovery(serverUrl, definition, signal)
|
|
389
|
+
const discovery = applyConfiguredScope(await probeAuthDiscovery(serverUrl, definition, signal), config)
|
|
374
390
|
throwIfAborted(signal)
|
|
375
391
|
const result = await abortable(runSdkAuth(authProvider, { serverUrl, ...discovery }), signal)
|
|
376
392
|
throwIfAborted(signal)
|
package/mcp-auth.ts
CHANGED
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
* then the plaintext file is removed.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { spawnSync } from 'child_process';
|
|
13
14
|
import { createHash } from 'crypto';
|
|
14
15
|
import { createRequire } from 'module';
|
|
15
16
|
import { readFileSync, existsSync, rmSync } from 'fs';
|
|
16
|
-
import { join } from 'path';
|
|
17
|
+
import { dirname, join } from 'path';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
17
19
|
import { getAgentPath } from './agent-dir.ts';
|
|
18
20
|
import { resolveConfiguredOAuthDir } from './config.ts';
|
|
19
21
|
|
|
@@ -21,6 +23,12 @@ const require = createRequire(import.meta.url);
|
|
|
21
23
|
const AUTH_SECRET_SERVICE = 'pi-mcp-adapter.oauth';
|
|
22
24
|
const TEST_AUTH_STORE_ENV = 'PI_MCP_ADAPTER_TEST_AUTH_STORE';
|
|
23
25
|
const AUTH_SECRET_CHUNK_SIZE = 1800;
|
|
26
|
+
const KEYRING_RECOVERY_DISABLED_ENV = 'PI_MCP_ADAPTER_DISABLE_KEYRING_RECOVERY';
|
|
27
|
+
const KEYRING_RECOVERY_KEYCTL_ENV = 'PI_MCP_ADAPTER_KEYRING_RECOVERY_KEYCTL';
|
|
28
|
+
const KEYRING_RECOVERY_NODE_ENV = 'PI_MCP_ADAPTER_KEYRING_RECOVERY_NODE';
|
|
29
|
+
const KEYRING_RECOVERY_HELPER_ENV = 'PI_MCP_ADAPTER_KEYRING_RECOVERY_HELPER';
|
|
30
|
+
const TEST_LINUX_KEYRING_RECOVERY_ENV = 'PI_MCP_ADAPTER_TEST_LINUX_KEYRING_RECOVERY';
|
|
31
|
+
const KEYRING_RECOVERY_TIMEOUT_MS = 10_000;
|
|
24
32
|
const AUTH_CHUNK_MANIFEST_KEY = '__piMcpAdapterOAuthChunked';
|
|
25
33
|
|
|
26
34
|
/** OAuth token storage format */
|
|
@@ -113,6 +121,8 @@ interface KeyringEntry {
|
|
|
113
121
|
}
|
|
114
122
|
|
|
115
123
|
type KeyringEntryConstructor = new (service: string, account: string) => KeyringEntry;
|
|
124
|
+
type KeyringModule = { Entry: KeyringEntryConstructor };
|
|
125
|
+
type KeyringRequire = ((id: string) => unknown) & { resolve(id: string): string };
|
|
116
126
|
|
|
117
127
|
interface AuthSecretStore {
|
|
118
128
|
read(account: string): string | undefined;
|
|
@@ -165,6 +175,22 @@ const unavailableAuthSecretStore: AuthSecretStore = {
|
|
|
165
175
|
},
|
|
166
176
|
};
|
|
167
177
|
|
|
178
|
+
function createKeyRevokedTestError(): Error {
|
|
179
|
+
return new Error("Couldn't access platform storage: KeyRevoked", { cause: new Error('KeyRevoked') });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const keyRevokedAuthSecretStore: AuthSecretStore = {
|
|
183
|
+
read() {
|
|
184
|
+
throw createKeyRevokedTestError();
|
|
185
|
+
},
|
|
186
|
+
write() {
|
|
187
|
+
throw createKeyRevokedTestError();
|
|
188
|
+
},
|
|
189
|
+
remove() {
|
|
190
|
+
throw createKeyRevokedTestError();
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
168
194
|
export function resetTestAuthSecretStore(): void {
|
|
169
195
|
memoryAuthEntries.clear();
|
|
170
196
|
}
|
|
@@ -180,18 +206,156 @@ export function removeTestAuthSecretStoreEntry(account: string): void {
|
|
|
180
206
|
function getAuthSecretStore(): AuthSecretStore {
|
|
181
207
|
if (process.env[TEST_AUTH_STORE_ENV] === 'memory') return memoryAuthSecretStore;
|
|
182
208
|
if (process.env[TEST_AUTH_STORE_ENV] === 'unavailable') return unavailableAuthSecretStore;
|
|
209
|
+
if (process.env[TEST_AUTH_STORE_ENV] === 'keyrevoked') return keyRevokedAuthSecretStore;
|
|
183
210
|
return keyringAuthSecretStore;
|
|
184
211
|
}
|
|
185
212
|
|
|
186
213
|
function getKeyringEntry(account: string): KeyringEntry {
|
|
187
214
|
try {
|
|
188
|
-
KeyringEntryClass ??= (
|
|
215
|
+
KeyringEntryClass ??= loadKeyringEntryClass();
|
|
189
216
|
return new KeyringEntryClass(AUTH_SECRET_SERVICE, account);
|
|
190
217
|
} catch (error) {
|
|
191
218
|
throw new Error('OAuth secure credential storage is unavailable. Configure the OS credential store and retry authentication.', { cause: error });
|
|
192
219
|
}
|
|
193
220
|
}
|
|
194
221
|
|
|
222
|
+
function loadKeyringEntryClass(keyringRequire: KeyringRequire = require, platform: NodeJS.Platform = process.platform, arch: NodeJS.Architecture = process.arch): KeyringEntryConstructor {
|
|
223
|
+
try {
|
|
224
|
+
return (keyringRequire('@napi-rs/keyring') as KeyringModule).Entry;
|
|
225
|
+
} catch (loaderError) {
|
|
226
|
+
try {
|
|
227
|
+
return loadKeyringNativeBindingFallback(keyringRequire, platform, arch).Entry;
|
|
228
|
+
} catch (fallbackError) {
|
|
229
|
+
throw new Error(`Failed to load @napi-rs/keyring; absolute-path native binding fallback also failed: ${formatErrorMessage(fallbackError)}`, {
|
|
230
|
+
cause: loaderError,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function loadKeyringNativeBindingFallback(keyringRequire: KeyringRequire, platform: NodeJS.Platform, arch: NodeJS.Architecture): KeyringModule {
|
|
237
|
+
const targets = getKeyringNativeBindingTargets(platform, arch);
|
|
238
|
+
if (targets.length === 0) {
|
|
239
|
+
throw new Error(`Unsupported @napi-rs/keyring native binding target: ${platform}-${arch}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let lastError: unknown;
|
|
243
|
+
for (const target of targets) {
|
|
244
|
+
try {
|
|
245
|
+
const packageJsonPath = keyringRequire.resolve(`${target.packageName}/package.json`);
|
|
246
|
+
return keyringRequire(join(dirname(packageJsonPath), target.bindingFile)) as KeyringModule;
|
|
247
|
+
} catch (error) {
|
|
248
|
+
lastError = error;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function getKeyringNativeBindingTargets(platform: NodeJS.Platform, arch: NodeJS.Architecture): { packageName: string; bindingFile: string }[] {
|
|
256
|
+
return getKeyringNativeBindingSuffixes(platform, arch).map(suffix => ({
|
|
257
|
+
packageName: `@napi-rs/keyring-${suffix}`,
|
|
258
|
+
bindingFile: `keyring.${suffix}.node`,
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function getKeyringNativeBindingSuffixes(platform: NodeJS.Platform, arch: NodeJS.Architecture): string[] {
|
|
263
|
+
if (platform === 'darwin') {
|
|
264
|
+
if (arch === 'arm64') return ['darwin-arm64'];
|
|
265
|
+
if (arch === 'x64') return ['darwin-x64'];
|
|
266
|
+
}
|
|
267
|
+
if (platform === 'win32') {
|
|
268
|
+
if (arch === 'arm64') return ['win32-arm64-msvc'];
|
|
269
|
+
if (arch === 'x64') return ['win32-x64-msvc'];
|
|
270
|
+
if (arch === 'ia32') return ['win32-ia32-msvc'];
|
|
271
|
+
}
|
|
272
|
+
if (platform === 'linux') {
|
|
273
|
+
if (arch === 'arm64') return ['linux-arm64-gnu', 'linux-arm64-musl'];
|
|
274
|
+
if (arch === 'arm') return ['linux-arm-gnueabihf'];
|
|
275
|
+
if (arch === 'riscv64') return ['linux-riscv64-gnu'];
|
|
276
|
+
if (arch === 'x64') return ['linux-x64-gnu', 'linux-x64-musl'];
|
|
277
|
+
}
|
|
278
|
+
if (platform === 'freebsd' && arch === 'x64') return ['freebsd-x64'];
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function formatErrorMessage(error: unknown): string {
|
|
283
|
+
return error instanceof Error ? error.message : String(error);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
type KeyringRecoveryOperation = 'read' | 'write' | 'remove';
|
|
287
|
+
|
|
288
|
+
type KeyringRecoveryResponse =
|
|
289
|
+
| { ok: true; found?: boolean; value?: string }
|
|
290
|
+
| { ok: false; error?: string };
|
|
291
|
+
|
|
292
|
+
function isLinuxKeyringRecoveryEnabled(): boolean {
|
|
293
|
+
if (process.env[KEYRING_RECOVERY_DISABLED_ENV] === '1') return false;
|
|
294
|
+
return process.platform === 'linux' || process.env[TEST_LINUX_KEYRING_RECOVERY_ENV] === '1';
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function shouldAttemptLinuxKeyringRecovery(error: unknown): boolean {
|
|
298
|
+
return isLinuxKeyringRecoveryEnabled()
|
|
299
|
+
&& causeChainContains(error, /key\s*(?:has been\s*)?revoked|keyrevoked/i);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function runLinuxKeyringRecoveryOperation(operation: KeyringRecoveryOperation, account: string, payload?: string): KeyringRecoveryResponse {
|
|
303
|
+
const keyctl = process.env[KEYRING_RECOVERY_KEYCTL_ENV]?.trim() || 'keyctl';
|
|
304
|
+
const node = process.env[KEYRING_RECOVERY_NODE_ENV]?.trim() || 'node';
|
|
305
|
+
const helper = process.env[KEYRING_RECOVERY_HELPER_ENV]?.trim()
|
|
306
|
+
|| fileURLToPath(new URL('./mcp-keyring-helper.cjs', import.meta.url));
|
|
307
|
+
const request = JSON.stringify({ operation, service: AUTH_SECRET_SERVICE, account, payload });
|
|
308
|
+
const result = spawnSync(keyctl, ['session', '-', node, helper], {
|
|
309
|
+
input: `${request}\n`,
|
|
310
|
+
encoding: 'utf8',
|
|
311
|
+
maxBuffer: 1024 * 1024,
|
|
312
|
+
timeout: KEYRING_RECOVERY_TIMEOUT_MS,
|
|
313
|
+
windowsHide: true,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
if (result.error) {
|
|
317
|
+
throw new Error(`Linux keyring recovery helper could not start: ${result.error.message}`, { cause: result.error });
|
|
318
|
+
}
|
|
319
|
+
if (result.status !== 0) {
|
|
320
|
+
throw new Error(`Linux keyring recovery helper failed with exit code ${result.status ?? 'unknown'}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let response: unknown;
|
|
324
|
+
try {
|
|
325
|
+
response = JSON.parse(result.stdout.trim()) as unknown;
|
|
326
|
+
} catch (error) {
|
|
327
|
+
throw new Error('Linux keyring recovery helper returned invalid JSON', { cause: error });
|
|
328
|
+
}
|
|
329
|
+
if (typeof response !== 'object' || response === null || typeof (response as { ok?: unknown }).ok !== 'boolean') {
|
|
330
|
+
throw new Error('Linux keyring recovery helper returned an invalid response');
|
|
331
|
+
}
|
|
332
|
+
const typedResponse = response as KeyringRecoveryResponse;
|
|
333
|
+
if (typedResponse.ok === false) {
|
|
334
|
+
throw new Error(typedResponse.error || 'Linux keyring recovery helper failed');
|
|
335
|
+
}
|
|
336
|
+
if (operation === 'read' && typedResponse.found === true && typeof typedResponse.value !== 'string') {
|
|
337
|
+
throw new Error('Linux keyring recovery helper returned an invalid read response');
|
|
338
|
+
}
|
|
339
|
+
return typedResponse;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const linuxKeyringRecoveryAuthSecretStore: AuthSecretStore = {
|
|
343
|
+
read(account) {
|
|
344
|
+
const response = runLinuxKeyringRecoveryOperation('read', account);
|
|
345
|
+
return response.ok && response.found === true ? response.value : undefined;
|
|
346
|
+
},
|
|
347
|
+
write(account, payload) {
|
|
348
|
+
runLinuxKeyringRecoveryOperation('write', account, payload);
|
|
349
|
+
},
|
|
350
|
+
remove(account) {
|
|
351
|
+
runLinuxKeyringRecoveryOperation('remove', account);
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
export function loadTestKeyringEntryClass(keyringRequire: KeyringRequire, platform: NodeJS.Platform, arch: NodeJS.Architecture): KeyringEntryConstructor {
|
|
356
|
+
return loadKeyringEntryClass(keyringRequire, platform, arch);
|
|
357
|
+
}
|
|
358
|
+
|
|
195
359
|
export function getAuthStorageOptions(oauthDir: unknown, cwd = process.cwd()): AuthStorageOptions {
|
|
196
360
|
const baseDir = resolveConfiguredOAuthDir(oauthDir, cwd);
|
|
197
361
|
return baseDir ? { baseDir } : {};
|
|
@@ -296,8 +460,7 @@ function createChunkManifest(payload: string): AuthEntryChunkManifest {
|
|
|
296
460
|
};
|
|
297
461
|
}
|
|
298
462
|
|
|
299
|
-
function readChunkedAuthEntry(serverName: string, account: string, manifest: AuthEntryChunkManifest): AuthEntry {
|
|
300
|
-
const store = getAuthSecretStore();
|
|
463
|
+
function readChunkedAuthEntry(store: AuthSecretStore, serverName: string, account: string, manifest: AuthEntryChunkManifest): AuthEntry {
|
|
301
464
|
const chunks = getAuthEntryChunkAccounts(account, manifest).map((chunkAccount) => {
|
|
302
465
|
try {
|
|
303
466
|
const chunk = store.read(chunkAccount);
|
|
@@ -340,9 +503,8 @@ function removeLegacyAuthEntry(serverName: string, options?: AuthStorageOptions)
|
|
|
340
503
|
}
|
|
341
504
|
}
|
|
342
505
|
|
|
343
|
-
function
|
|
506
|
+
function writeSecureAuthEntryToStore(store: AuthSecretStore, serverName: string, entry: AuthEntry): void {
|
|
344
507
|
const account = getAuthEntryAccount(serverName);
|
|
345
|
-
const store = getAuthSecretStore();
|
|
346
508
|
const payload = JSON.stringify(entry);
|
|
347
509
|
const previousManifest = readExistingChunkManifest(store, serverName, account);
|
|
348
510
|
const manifest = payload.length > AUTH_SECRET_CHUNK_SIZE ? createChunkManifest(payload) : undefined;
|
|
@@ -371,11 +533,21 @@ function writeSecureAuthEntry(serverName: string, entry: AuthEntry): void {
|
|
|
371
533
|
}
|
|
372
534
|
}
|
|
373
535
|
|
|
536
|
+
function writeSecureAuthEntry(serverName: string, entry: AuthEntry): void {
|
|
537
|
+
try {
|
|
538
|
+
writeSecureAuthEntryToStore(getAuthSecretStore(), serverName, entry);
|
|
539
|
+
} catch (error) {
|
|
540
|
+
if (!shouldAttemptLinuxKeyringRecovery(error)) throw error;
|
|
541
|
+
writeSecureAuthEntryToStore(linuxKeyringRecoveryAuthSecretStore, serverName, entry);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
374
545
|
/**
|
|
375
546
|
* Read the auth entry for a server from the OS secure store, importing and
|
|
376
547
|
* deleting a legacy plaintext entry when present.
|
|
377
548
|
*/
|
|
378
|
-
function
|
|
549
|
+
function readAuthEntryFromStore(
|
|
550
|
+
store: AuthSecretStore,
|
|
379
551
|
serverName: string,
|
|
380
552
|
options?: AuthStorageOptions,
|
|
381
553
|
behavior: { migrateLegacy?: boolean } = {},
|
|
@@ -383,7 +555,7 @@ function readAuthEntry(
|
|
|
383
555
|
const account = getAuthEntryAccount(serverName);
|
|
384
556
|
let payload: string | undefined;
|
|
385
557
|
try {
|
|
386
|
-
payload =
|
|
558
|
+
payload = store.read(account);
|
|
387
559
|
} catch (error) {
|
|
388
560
|
throw new OAuthCredentialStoreError(
|
|
389
561
|
`Failed to read OAuth credentials for ${serverName} from the OS secure credential store`,
|
|
@@ -395,7 +567,7 @@ function readAuthEntry(
|
|
|
395
567
|
if (payload !== undefined) {
|
|
396
568
|
const manifest = readChunkManifestFromPayload(serverName, payload, 'OS secure credential store');
|
|
397
569
|
const entry = manifest
|
|
398
|
-
? readChunkedAuthEntry(serverName, account, manifest)
|
|
570
|
+
? readChunkedAuthEntry(store, serverName, account, manifest)
|
|
399
571
|
: parseAuthEntryPayload(serverName, payload, 'OS secure credential store');
|
|
400
572
|
removeLegacyAuthEntry(serverName, options);
|
|
401
573
|
return entry;
|
|
@@ -404,11 +576,24 @@ function readAuthEntry(
|
|
|
404
576
|
const legacyEntry = readLegacyAuthEntry(serverName, options);
|
|
405
577
|
if (!legacyEntry) return undefined;
|
|
406
578
|
if (behavior.migrateLegacy === false) return legacyEntry;
|
|
407
|
-
|
|
579
|
+
writeSecureAuthEntryToStore(store, serverName, legacyEntry);
|
|
408
580
|
removeLegacyAuthEntry(serverName, options);
|
|
409
581
|
return legacyEntry;
|
|
410
582
|
}
|
|
411
583
|
|
|
584
|
+
function readAuthEntry(
|
|
585
|
+
serverName: string,
|
|
586
|
+
options?: AuthStorageOptions,
|
|
587
|
+
behavior: { migrateLegacy?: boolean } = {},
|
|
588
|
+
): AuthEntry | undefined {
|
|
589
|
+
try {
|
|
590
|
+
return readAuthEntryFromStore(getAuthSecretStore(), serverName, options, behavior);
|
|
591
|
+
} catch (error) {
|
|
592
|
+
if (!shouldAttemptLinuxKeyringRecovery(error)) throw error;
|
|
593
|
+
return readAuthEntryFromStore(linuxKeyringRecoveryAuthSecretStore, serverName, options, behavior);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
412
597
|
/**
|
|
413
598
|
* Get auth entry for a server.
|
|
414
599
|
*/
|
|
@@ -468,9 +653,8 @@ export function saveAuthEntry(serverName: string, entry: AuthEntry, serverUrl?:
|
|
|
468
653
|
/**
|
|
469
654
|
* Remove auth entry for a server.
|
|
470
655
|
*/
|
|
471
|
-
|
|
656
|
+
function removeAuthEntryFromStore(store: AuthSecretStore, serverName: string): void {
|
|
472
657
|
const account = getAuthEntryAccount(serverName);
|
|
473
|
-
const store = getAuthSecretStore();
|
|
474
658
|
try {
|
|
475
659
|
const payload = store.read(account);
|
|
476
660
|
const manifest = payload === undefined ? undefined : readChunkManifestFromPayload(serverName, payload, 'OS secure credential store');
|
|
@@ -483,6 +667,15 @@ export function removeAuthEntry(serverName: string, options?: AuthStorageOptions
|
|
|
483
667
|
error,
|
|
484
668
|
);
|
|
485
669
|
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export function removeAuthEntry(serverName: string, options?: AuthStorageOptions): void {
|
|
673
|
+
try {
|
|
674
|
+
removeAuthEntryFromStore(getAuthSecretStore(), serverName);
|
|
675
|
+
} catch (error) {
|
|
676
|
+
if (!shouldAttemptLinuxKeyringRecovery(error)) throw error;
|
|
677
|
+
removeAuthEntryFromStore(linuxKeyringRecoveryAuthSecretStore, serverName);
|
|
678
|
+
}
|
|
486
679
|
removeLegacyAuthEntry(serverName, options);
|
|
487
680
|
}
|
|
488
681
|
|