mixdog 1.1.0 → 1.1.1
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/README.md +13 -6
- package/package.json +1 -1
- package/scripts/latest-guard.mjs +63 -0
- package/scripts/lib/run-node-tests.mjs +103 -27
- package/scripts/lib/test-failure-summary.mjs +7 -2
- package/scripts/merge-mac-update-feed.mjs +107 -0
- package/scripts/mobile-release.mjs +210 -0
- package/scripts/post-release-check.mjs +353 -0
- package/scripts/publish-asset-release.mjs +43 -0
- package/scripts/release-guard.mjs +63 -0
- package/scripts/test.mjs +29 -7
- package/src/defaults/shipped-history.json +2 -0
- package/src/defaults/skills/computer-use/SKILL.md +1 -1
- package/src/defaults/skills/setup/references/actions.md +1 -0
- package/src/lib/keychain-cjs.cjs +582 -17
- package/src/runtime/agent/orchestrator/config-storage.mjs +32 -4
- package/src/runtime/agent/orchestrator/config.mjs +18 -0
- package/src/runtime/agent/orchestrator/context/instruction-files.mjs +130 -0
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/lib/open-login-url.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +254 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +218 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +239 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +233 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth-login.mjs +247 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +225 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +542 -0
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +14 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +99 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +4 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +2 -7
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +4 -0
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +15 -1
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +11 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-call-ids.mjs +90 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +19 -11
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/turn-effort.mjs +18 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +11 -8
- package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +9 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +245 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/command-preflight.mjs +13 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +49 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -7
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +61 -8
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +28 -3
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +73 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +17 -6
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +23 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +93 -43
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +32 -6
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +12 -15
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -81
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +19 -4
- package/src/runtime/computer-bridge/client.mjs +25 -1
- package/src/runtime/computer-bridge/linux-action-capabilities.mjs +84 -0
- package/src/runtime/computer-bridge/linux-readiness.d.mts +52 -0
- package/src/runtime/computer-bridge/linux-readiness.mjs +411 -0
- package/src/runtime/computer-bridge/test-linux-display.mjs +5 -0
- package/src/runtime/effort-judge/compact-tokenizer.mjs +13 -8
- package/src/runtime/effort-judge/judge-input.mjs +5 -2
- package/src/runtime/effort-judge/model-install.mjs +9 -3
- package/src/runtime/effort-judge/model-manifest.json +7 -7
- package/src/runtime/memory/lib/embedding-worker.mjs +9 -3
- package/src/runtime/memory/lib/pg/supervisor.mjs +3 -0
- package/src/runtime/shared/config.mjs +50 -2
- package/src/runtime/shared/developer-options.mjs +7 -0
- package/src/runtime/shared/instruction-files-mode.mjs +9 -0
- package/src/runtime/shared/llm/model-catalog.mjs +2 -1
- package/src/runtime/shared/llm/openai-daybreak.mjs +79 -0
- package/src/runtime/shared/pristine-execution-contract.json +9 -0
- package/src/runtime/shared/pristine-execution.mjs +1 -1
- package/src/runtime/shared/provider-accounts.mjs +2 -0
- package/src/runtime/shared/resource-admission.mjs +10 -3
- package/src/runtime/shared/schedule-model-ref.mjs +11 -0
- package/src/runtime/shared/session-approval-wait.mjs +35 -0
- package/src/runtime/shared/shell-display.mjs +4 -2
- package/src/runtime/shared/turn-worktree-snapshot.mjs +11 -10
- package/src/session-runtime/boot/apis.mjs +5 -0
- package/src/session-runtime/model-recency.mjs +3 -0
- package/src/session-runtime/provider-auth/login.mjs +10 -10
- package/src/session-runtime/provider-auth/setup.mjs +22 -2
- package/src/session-runtime/provider-models/row-hydration.mjs +12 -4
- package/src/session-runtime/provider-readiness.mjs +4 -0
- package/src/session-runtime/runtime-facade.mjs +5 -0
- package/src/session-runtime/schedule-session-run.mjs +3 -1
- package/src/session-runtime/services/agent-tool/index-write-queue.mjs +55 -14
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +15 -3
- package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +2 -0
- package/src/session-runtime/services/agent-tool/worker-index/approval-wait.mjs +97 -0
- package/src/session-runtime/services/agent-tool/worker-index/mutation-batch.mjs +4 -2
- package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -2
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +3 -0
- package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +58 -2
- package/src/session-runtime/services/agent-tool/worker-index.mjs +69 -15
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +2 -0
- package/src/session-runtime/services/channel-admin.mjs +4 -2
- package/src/session-runtime/services/provider-admin.mjs +80 -17
- package/src/session-runtime/services/seeds.mjs +7 -10
- package/src/session-runtime/services/usage-dashboard-model.mjs +6 -0
- package/src/session-runtime/settings-builtin-tools-api.mjs +15 -0
- package/src/session-runtime/setup-tool/executor.mjs +5 -0
- package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
- package/src/session-runtime/tool-policy-refresh.mjs +3 -0
- package/src/session-runtime/webhook-session-run.mjs +3 -1
- package/src/standalone/session-protocol.mjs +1 -0
- package/src/tui/app/model-options.mjs +13 -1
- package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +10 -4
- package/src/tui/app/provider-setup/oauth-actions.mjs +5 -0
- package/src/tui/dist/index.mjs +84 -34
- package/src/tui/session/context-state.mjs +2 -0
- package/src/tui/session/live-share/owner.mjs +17 -0
- package/src/tui/session/oauth-flows.mjs +2 -0
- package/src/tui/session/session-api/integrations.mjs +8 -8
- package/src/tui/session/session-api/routes.mjs +2 -2
- package/src/tui/session/session-api/settings.mjs +1 -0
- package/src/tui/session/tool-card-results/grouped-result.mjs +11 -2
- package/src/tui/session/transcript-spill/spill-dir.mjs +22 -6
- package/src/tui/spinner-meta.mjs +26 -10
package/src/lib/keychain-cjs.cjs
CHANGED
|
@@ -60,6 +60,12 @@ function _cacheInvalidate(account) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
function invalidateSecretCache(account) {
|
|
63
|
+
// Linux reads are cache-only after one snapshot: an external invalidation
|
|
64
|
+
// means the snapshot may be stale, so the next read reloads it.
|
|
65
|
+
if (process.platform === 'linux') {
|
|
66
|
+
_linuxLoadedUntil = 0;
|
|
67
|
+
_prewarmPromise = null;
|
|
68
|
+
}
|
|
63
69
|
if (account == null) {
|
|
64
70
|
_secretCache.clear();
|
|
65
71
|
_cacheGenerations.clear();
|
|
@@ -300,7 +306,7 @@ function secretToolHint() {
|
|
|
300
306
|
// environment, which would otherwise be visible via /proc/<pid>/environ for the
|
|
301
307
|
// lifetime of this child. Only PATH resolution, locale, and the libsecret
|
|
302
308
|
// D-Bus session bridge are passed through.
|
|
303
|
-
function
|
|
309
|
+
function secretToolEnv() {
|
|
304
310
|
const passthroughKeys = [
|
|
305
311
|
'PATH',
|
|
306
312
|
'HOME',
|
|
@@ -321,37 +327,457 @@ function secretTool(args, input) {
|
|
|
321
327
|
for (const key of passthroughKeys) {
|
|
322
328
|
if (process.env[key] !== undefined) env[key] = process.env[key];
|
|
323
329
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
});
|
|
332
|
-
|
|
330
|
+
return env;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Raw tool output (stderr) may carry paths or service internals; keep it out of
|
|
334
|
+
// user-facing errors and print it only when explicitly debugging. Secrets are
|
|
335
|
+
// never part of it: they travel on stdin only.
|
|
336
|
+
function keychainDebug(what, detail) {
|
|
337
|
+
if (process.env.MIXDOG_KEYCHAIN_DEBUG) console.error(`[keychain:debug] ${what}: ${String(detail).slice(0, 500)}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function secretToolCheck(r) {
|
|
341
|
+
if (r.error?.code === 'ENOENT') {
|
|
342
|
+
throw new Error(
|
|
343
|
+
'[keychain] secret-tool is not installed — install libsecret-tools (sudo apt install libsecret-tools), ' +
|
|
344
|
+
'or set the relevant MIXDOG_*/PROVIDER_API_KEY environment variable instead'
|
|
345
|
+
);
|
|
346
|
+
}
|
|
333
347
|
if (r.error?.code === 'ETIMEDOUT') {
|
|
334
|
-
throw new Error(
|
|
348
|
+
throw new Error(
|
|
349
|
+
`[keychain] secret-tool timed out after ${r.error.timeoutMs || KEYCHAIN_TIMEOUT_MS}ms — the keyring is locked or its unlock prompt ` +
|
|
350
|
+
'was not answered. Unlock your keyring (e.g. log in again or open Passwords and Keys) and retry.'
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
if (r.error) {
|
|
354
|
+
keychainDebug('secret-tool failed to start', r.error.message);
|
|
355
|
+
throw new Error('[keychain] secret-tool could not be started — check that libsecret-tools is installed and a Secret Service is running');
|
|
335
356
|
}
|
|
336
|
-
if (r.error) throw new Error(`[keychain] secret-tool failed to start: ${r.error.message}`);
|
|
337
357
|
return r;
|
|
338
358
|
}
|
|
339
359
|
|
|
360
|
+
function secretTool(args, input) {
|
|
361
|
+
return secretToolCheck(
|
|
362
|
+
spawnSync('secret-tool', args, {
|
|
363
|
+
env: secretToolEnv(),
|
|
364
|
+
input: input ?? '',
|
|
365
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
366
|
+
timeout: KEYCHAIN_TIMEOUT_MS,
|
|
367
|
+
encoding: 'utf8',
|
|
368
|
+
windowsHide: true,
|
|
369
|
+
})
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Non-blocking variant: the daemon event loop keeps running while a locked
|
|
374
|
+
// keyring waits on its unlock prompt; the child is killed at the same bound.
|
|
375
|
+
function secretToolAsync(args, input, timeoutMs = KEYCHAIN_TIMEOUT_MS) {
|
|
376
|
+
return new Promise((resolve, reject) => {
|
|
377
|
+
let child;
|
|
378
|
+
try {
|
|
379
|
+
child = spawn('secret-tool', args, {
|
|
380
|
+
env: secretToolEnv(),
|
|
381
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
382
|
+
shell: false,
|
|
383
|
+
windowsHide: true,
|
|
384
|
+
});
|
|
385
|
+
} catch (error) {
|
|
386
|
+
reject(error);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
let stdout = '';
|
|
390
|
+
let stderr = '';
|
|
391
|
+
let settled = false;
|
|
392
|
+
const done = (r) => {
|
|
393
|
+
if (settled) return;
|
|
394
|
+
settled = true;
|
|
395
|
+
clearTimeout(timer);
|
|
396
|
+
try {
|
|
397
|
+
resolve(secretToolCheck(r));
|
|
398
|
+
} catch (error) {
|
|
399
|
+
reject(error);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
const timer = setTimeout(() => {
|
|
403
|
+
done({
|
|
404
|
+
status: null,
|
|
405
|
+
stdout,
|
|
406
|
+
stderr,
|
|
407
|
+
error: Object.assign(new Error('timeout'), { code: 'ETIMEDOUT', timeoutMs }),
|
|
408
|
+
});
|
|
409
|
+
try {
|
|
410
|
+
child.kill('SIGKILL');
|
|
411
|
+
} catch {}
|
|
412
|
+
}, timeoutMs);
|
|
413
|
+
child.stdout.setEncoding('utf8');
|
|
414
|
+
child.stderr.setEncoding('utf8');
|
|
415
|
+
child.stdout.on('data', (chunk) => {
|
|
416
|
+
stdout += chunk;
|
|
417
|
+
});
|
|
418
|
+
child.stderr.on('data', (chunk) => {
|
|
419
|
+
stderr += chunk;
|
|
420
|
+
});
|
|
421
|
+
child.once('error', (error) => done({ status: null, stdout, stderr, error }));
|
|
422
|
+
child.once('close', (status) => done({ status, stdout, stderr, error: null }));
|
|
423
|
+
child.stdin.on('error', () => {
|
|
424
|
+
/* child exited early — close reports it */
|
|
425
|
+
});
|
|
426
|
+
child.stdin.end(input ?? '');
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
340
430
|
// A missing item exits non-zero with nothing on stderr; anything on stderr is
|
|
341
431
|
// the Secret Service itself failing (no D-Bus session, locked collection).
|
|
342
432
|
function secretToolFailure(action, r) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
);
|
|
433
|
+
const detail = (r.stderr || '').trim() || `exit ${r.status}`;
|
|
434
|
+
// User-facing text is fixed; the raw tool output only goes to the debug log.
|
|
435
|
+
keychainDebug(`secret-tool ${action} failed`, detail);
|
|
436
|
+
if (/locked|denied|dismissed|cancel|not authori[sz]ed|IsLocked/i.test(detail)) {
|
|
437
|
+
return new Error(
|
|
438
|
+
`[keychain] secret-tool ${action} failed: the keyring is locked or the unlock prompt was declined. ` +
|
|
439
|
+
'Unlock your keyring and retry.'
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
return new Error(`[keychain] secret-tool ${action} failed — ${secretToolHint()}`);
|
|
346
443
|
}
|
|
347
444
|
|
|
348
|
-
function
|
|
445
|
+
function linuxGetBlocking(account) {
|
|
349
446
|
const r = secretTool(['lookup', 'service', SERVICE, 'account', account]);
|
|
350
447
|
if (r.status === 0) return r.stdout;
|
|
351
448
|
if ((r.stderr || '').trim()) throw secretToolFailure('lookup', r);
|
|
352
449
|
return null;
|
|
353
450
|
}
|
|
354
451
|
|
|
452
|
+
// Daemon-safe state. The synchronous read path NEVER spawns secret-tool on
|
|
453
|
+
// Linux: a locked keyring would freeze the event loop for the whole timeout.
|
|
454
|
+
// One bounded async `secret-tool search --all` snapshot (prewarmSecrets) fills
|
|
455
|
+
// the read cache; afterwards an uncached account is simply absent. Until that
|
|
456
|
+
// snapshot succeeds the sync read throws the actionable keyring error (callers
|
|
457
|
+
// already treat a throw as "no stored key") and a retry is scheduled lazily.
|
|
458
|
+
const LINUX_PREWARM_TIMEOUT_MS = Number(process.env.MIXDOG_KEYCHAIN_PREWARM_TIMEOUT_MS || 4000);
|
|
459
|
+
const LINUX_PREWARM_RETRY_MS = Number(process.env.MIXDOG_KEYCHAIN_RETRY_MS || 5000);
|
|
460
|
+
// An explicit "Unlock keyring" waits on the user typing a password.
|
|
461
|
+
const LINUX_INTERACTIVE_TIMEOUT_MS = Number(process.env.MIXDOG_KEYCHAIN_INTERACTIVE_TIMEOUT_MS || 120000);
|
|
462
|
+
// A window-focus re-check skips the TTL but not this floor (focus can flap).
|
|
463
|
+
const LINUX_RECHECK_MIN_MS = Number(process.env.MIXDOG_KEYCHAIN_RECHECK_MIN_MS || 2000);
|
|
464
|
+
const LINUX_LOCKED_MESSAGE =
|
|
465
|
+
'[keychain] the keyring is locked — unlock it (log in again or open Passwords and Keys) and retry';
|
|
466
|
+
let _linuxLoadedUntil = 0;
|
|
467
|
+
let _linuxLastFailureAt = 0;
|
|
468
|
+
let _linuxPrewarmInFlight = false;
|
|
469
|
+
let _linuxSnapshotAt = 0;
|
|
470
|
+
let _linuxSnapshotChanged = false;
|
|
471
|
+
const LINUX_SNAPSHOT_TTL_MS = Number(process.env.MIXDOG_KEYCHAIN_SNAPSHOT_TTL_MS || 45000);
|
|
472
|
+
let _linuxBgTimer = null;
|
|
473
|
+
let _linuxBgAttempt = 0;
|
|
474
|
+
let _interactiveUnlock = false;
|
|
475
|
+
let _linuxPrewarmInteractive = false;
|
|
476
|
+
const _recoveryListeners = new Set();
|
|
477
|
+
let _keychainStatus = { state: 'unknown', message: '' };
|
|
478
|
+
|
|
479
|
+
function _noteKeychainOk() {
|
|
480
|
+
_keychainStatus = { state: 'ok', message: '' };
|
|
481
|
+
_linuxBgAttempt = 0;
|
|
482
|
+
clearTimeout(_linuxBgTimer);
|
|
483
|
+
_linuxBgTimer = null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Subscribe to a failed Linux keyring becoming readable (e.g. user unlocked it). */
|
|
487
|
+
function onKeychainRecovered(listener) {
|
|
488
|
+
_recoveryListeners.add(listener);
|
|
489
|
+
return () => _recoveryListeners.delete(listener);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// While the keyring is locked/failing nothing may depend on a read to retry, so
|
|
493
|
+
// retry in the background with a bounded backoff (5s → 10s). A missing
|
|
494
|
+
// secret-tool cannot fix itself and is not retried.
|
|
495
|
+
function _scheduleLinuxBackgroundRetry() {
|
|
496
|
+
if (_linuxBgTimer || _keychainStatus.state === 'missing' || _gdbusMissing) return;
|
|
497
|
+
const delay = Math.min(LINUX_PREWARM_RETRY_MS * 2, LINUX_PREWARM_RETRY_MS + _linuxBgAttempt * (LINUX_PREWARM_RETRY_MS / 2));
|
|
498
|
+
_linuxBgAttempt++;
|
|
499
|
+
_linuxBgTimer = setTimeout(async () => {
|
|
500
|
+
_linuxBgTimer = null;
|
|
501
|
+
await retryKeychain();
|
|
502
|
+
}, delay);
|
|
503
|
+
_linuxBgTimer.unref?.();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Passive access to the Secret Service over D-Bus. Unlike secret-tool (whose
|
|
507
|
+
// lookup/search raises GNOME's "Authentication required" prompt as soon as a
|
|
508
|
+
// MATCHING item sits in a locked collection) SearchItems never prompts: it
|
|
509
|
+
// answers with the matching item paths split into unlocked and locked ones.
|
|
510
|
+
// Only Mixdog's own items matter — a locked collection that holds none of them
|
|
511
|
+
// (e.g. the login keyring next to an unlocked default one) is irrelevant.
|
|
512
|
+
// 'unknown' (null) means no gdbus / service unreachable / unparsable; passive
|
|
513
|
+
// callers must treat it as "do not run secret-tool".
|
|
514
|
+
let _gdbusMissing = false;
|
|
515
|
+
const SECRETS_DEST = 'org.freedesktop.secrets';
|
|
516
|
+
const SECRETS_ROOT = '/org/freedesktop/secrets';
|
|
517
|
+
const SECRET_SERVICE_IFACE = 'org.freedesktop.Secret.Service';
|
|
518
|
+
const SECRET_ITEM_IFACE = 'org.freedesktop.Secret.Item';
|
|
519
|
+
|
|
520
|
+
/** One bounded gdbus method call; resolves { ok, out, err } (never rejects). */
|
|
521
|
+
function gdbusCall(objectPath, method, args = []) {
|
|
522
|
+
return new Promise((resolve) => {
|
|
523
|
+
let child;
|
|
524
|
+
try {
|
|
525
|
+
child = spawn(
|
|
526
|
+
'gdbus',
|
|
527
|
+
['call', '--session', '--dest', SECRETS_DEST, '--object-path', objectPath, '--method', method, ...args],
|
|
528
|
+
{ env: secretToolEnv(), stdio: ['ignore', 'pipe', 'pipe'], shell: false, windowsHide: true }
|
|
529
|
+
);
|
|
530
|
+
} catch {
|
|
531
|
+
resolve({ ok: false, out: '', err: '' });
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
let stdout = '';
|
|
535
|
+
let stderr = '';
|
|
536
|
+
let settled = false;
|
|
537
|
+
const done = (ok) => {
|
|
538
|
+
if (settled) return;
|
|
539
|
+
settled = true;
|
|
540
|
+
clearTimeout(timer);
|
|
541
|
+
resolve({ ok, out: stdout, err: stderr });
|
|
542
|
+
};
|
|
543
|
+
const timer = setTimeout(() => {
|
|
544
|
+
try {
|
|
545
|
+
child.kill('SIGKILL');
|
|
546
|
+
} catch {}
|
|
547
|
+
done(false);
|
|
548
|
+
}, 2000);
|
|
549
|
+
child.stdout.setEncoding('utf8');
|
|
550
|
+
child.stdout.on('data', (chunk) => {
|
|
551
|
+
stdout += chunk;
|
|
552
|
+
});
|
|
553
|
+
child.stderr.setEncoding('utf8');
|
|
554
|
+
child.stderr.on('data', (chunk) => {
|
|
555
|
+
stderr += chunk;
|
|
556
|
+
});
|
|
557
|
+
child.once('error', (error) => {
|
|
558
|
+
if (error?.code === 'ENOENT') _gdbusMissing = true;
|
|
559
|
+
done(false);
|
|
560
|
+
});
|
|
561
|
+
child.once('close', (status) => done(status === 0));
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// gdbus prints the (ao, ao) reply as `([objectpath '/…/7'], @ao [])`.
|
|
566
|
+
function parseSearchItems(out) {
|
|
567
|
+
const array = '(@ao \\[\\s*\\]|\\[[^\\]]*\\])';
|
|
568
|
+
const m = new RegExp(`^\\(\\s*${array}\\s*,\\s*${array}\\s*\\)$`).exec(String(out).trim());
|
|
569
|
+
if (!m) return null;
|
|
570
|
+
const paths = (text) => [...text.matchAll(/'(\/org\/freedesktop\/secrets\/collection\/[^']+)'/g)].map((x) => x[1]);
|
|
571
|
+
return { unlocked: paths(m[1]), locked: paths(m[2]) };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** Mixdog's items by lock state, or null when the service cannot be asked. */
|
|
575
|
+
async function searchMixdogItems() {
|
|
576
|
+
const r = await gdbusCall(SECRETS_ROOT, `${SECRET_SERVICE_IFACE}.SearchItems`, [`{'service': '${SERVICE}'}`]);
|
|
577
|
+
return r.ok ? parseSearchItems(r.out) : null;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// An item's attributes are readable without unlocking its collection.
|
|
581
|
+
// Resolves the account name, '' for an item that vanished since the search
|
|
582
|
+
// (a keyring can list a just-deleted item), null when the call itself failed.
|
|
583
|
+
async function itemAccount(objectPath) {
|
|
584
|
+
const r = await gdbusCall(objectPath, 'org.freedesktop.DBus.Properties.Get', [SECRET_ITEM_IFACE, 'Attributes']);
|
|
585
|
+
if (!r.ok) return /does not exist|UnknownObject|No such (object|item)/i.test(r.err) ? '' : null;
|
|
586
|
+
const m = /'account':\s*'((?:[^'\\]|\\.)*)'/.exec(r.out);
|
|
587
|
+
return m ? m[1].replace(/\\(.)/g, '$1') : '';
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const LINUX_UNREACHABLE_MESSAGE =
|
|
591
|
+
'[keychain] the keyring is not reachable — make sure it is running, then use Unlock keyring / Retry';
|
|
592
|
+
function _noteKeychainFailure(error) {
|
|
593
|
+
const message = String(error?.message || error);
|
|
594
|
+
const state =
|
|
595
|
+
error?.code === 'KEYRING_UNREACHABLE'
|
|
596
|
+
? 'unreachable'
|
|
597
|
+
: /not installed/.test(message)
|
|
598
|
+
? 'missing'
|
|
599
|
+
: /locked|timed out/.test(message)
|
|
600
|
+
? 'locked'
|
|
601
|
+
: 'error';
|
|
602
|
+
_keychainStatus = { state, message };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** Linux keyring health for the UI: ok | locked | missing | error | unknown. */
|
|
606
|
+
function keychainStatus() {
|
|
607
|
+
return { ..._keychainStatus };
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function _linuxLoaded() {
|
|
611
|
+
return _linuxLoadedUntil > Date.now();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function linuxGet(account) {
|
|
615
|
+
if (KEYCHAIN_CACHE_TTL_MS === 0) return linuxGetBlocking(account);
|
|
616
|
+
if (_linuxLoaded()) return null;
|
|
617
|
+
if (Date.now() - _linuxLastFailureAt > LINUX_PREWARM_RETRY_MS) void prewarmSecrets();
|
|
618
|
+
throw new Error(
|
|
619
|
+
_keychainStatus.message ||
|
|
620
|
+
'[keychain] the Linux keyring has not been read yet — unlock your keyring (log in again or open Passwords and Keys)'
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// `secret-tool search --all` splits its report across streams: header, label,
|
|
625
|
+
// secret, created/modified and schema go to stdout while the attribute.* lines
|
|
626
|
+
// go to stderr (observed on GNOME). Pairing items across the split is
|
|
627
|
+
// unreliable, so the search is only used to DISCOVER account names (the
|
|
628
|
+
// attribute.account lines of either stream); values come from per-account
|
|
629
|
+
// `lookup`. The discovered count must match the item count, otherwise the
|
|
630
|
+
// snapshot could be silently incomplete and is rejected.
|
|
631
|
+
function parseSecretToolSearchAccounts(stdout, stderr) {
|
|
632
|
+
const text = `${String(stdout || '')}\n${String(stderr || '')}`;
|
|
633
|
+
const accounts = new Set();
|
|
634
|
+
let attributeLines = 0;
|
|
635
|
+
let items = 0;
|
|
636
|
+
for (const line of text.split(/\r?\n/)) {
|
|
637
|
+
if (/^\[\/.*\]$/.test(line)) items++;
|
|
638
|
+
const m = line.match(/^attribute\.account = (.+)$/);
|
|
639
|
+
if (m) {
|
|
640
|
+
attributeLines++;
|
|
641
|
+
accounts.add(m[1]);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (attributeLines !== items) {
|
|
645
|
+
// A locked keyring answers the search with items but no attributes. Keep
|
|
646
|
+
// the parse detail out of the UI-facing message.
|
|
647
|
+
const error = new Error(LINUX_LOCKED_MESSAGE);
|
|
648
|
+
error.detail = `secret-tool search returned ${items} item(s) but ${attributeLines} account attribute(s)`;
|
|
649
|
+
throw error;
|
|
650
|
+
}
|
|
651
|
+
return [...accounts];
|
|
652
|
+
}
|
|
653
|
+
// Which accounts to read, and whether Mixdog items are stuck in a locked
|
|
654
|
+
// collection. Passive reads never prompt: SearchItems says which items are
|
|
655
|
+
// unlocked, and only those are read. secret-tool is used only where that cannot
|
|
656
|
+
// prompt without the user asking: to list accounts when no Mixdog item exists
|
|
657
|
+
// at all (this also proves the tool is installed), and — for an explicit
|
|
658
|
+
// `interactive` unlock — `search --unlock`, which is what raises the prompt
|
|
659
|
+
// (a plain search lists locked items without attributes and never asks).
|
|
660
|
+
// Its listing is not used: attributes and secrets arrive split across stdout
|
|
661
|
+
// and stderr and one stale item makes the whole command fail, so the unlocked
|
|
662
|
+
// items are listed through D-Bus again afterwards.
|
|
663
|
+
async function _linuxAccountsToRead(interactive, remaining) {
|
|
664
|
+
let found = await searchMixdogItems();
|
|
665
|
+
if (found === null && !interactive) {
|
|
666
|
+
const error = new Error(LINUX_UNREACHABLE_MESSAGE);
|
|
667
|
+
error.code = 'KEYRING_UNREACHABLE';
|
|
668
|
+
throw error;
|
|
669
|
+
}
|
|
670
|
+
const listAccounts = async (args) => {
|
|
671
|
+
const r = await secretToolAsync(args, undefined, remaining());
|
|
672
|
+
// Exit 1 with an empty stderr just means "no items".
|
|
673
|
+
if (r.status !== 0 && (r.stderr || '').trim()) throw secretToolFailure('search', r);
|
|
674
|
+
return { accounts: parseSecretToolSearchAccounts(r.stdout, r.stderr), lockedLeft: false };
|
|
675
|
+
};
|
|
676
|
+
if (found !== null && found.unlocked.length === 0 && found.locked.length === 0) {
|
|
677
|
+
return listAccounts(['search', '--all', 'service', SERVICE]);
|
|
678
|
+
}
|
|
679
|
+
if (interactive && (found === null || found.locked.length > 0)) {
|
|
680
|
+
const unlock = await secretToolAsync(['search', '--all', '--unlock', 'service', SERVICE], undefined, remaining());
|
|
681
|
+
found = await searchMixdogItems();
|
|
682
|
+
if (found === null) {
|
|
683
|
+
if (unlock.status !== 0 && (unlock.stderr || '').trim()) throw secretToolFailure('search', unlock);
|
|
684
|
+
return { accounts: parseSecretToolSearchAccounts(unlock.stdout, unlock.stderr), lockedLeft: false };
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const names = await Promise.all(found.unlocked.map((objectPath) => itemAccount(objectPath)));
|
|
688
|
+
if (names.includes(null)) {
|
|
689
|
+
const error = new Error(LINUX_UNREACHABLE_MESSAGE);
|
|
690
|
+
error.code = 'KEYRING_UNREACHABLE';
|
|
691
|
+
throw error;
|
|
692
|
+
}
|
|
693
|
+
return { accounts: [...new Set(names.filter(Boolean))], lockedLeft: found.locked.length > 0 };
|
|
694
|
+
}
|
|
695
|
+
async function _prewarmLinuxOnce() {
|
|
696
|
+
_linuxPrewarmInFlight = true;
|
|
697
|
+
const interactive = _interactiveUnlock;
|
|
698
|
+
_linuxPrewarmInteractive = interactive;
|
|
699
|
+
try {
|
|
700
|
+
if (KEYCHAIN_CACHE_TTL_MS === 0) return;
|
|
701
|
+
// One deadline covers the search AND every per-account lookup.
|
|
702
|
+
const deadline = Date.now() + (interactive ? LINUX_INTERACTIVE_TIMEOUT_MS : LINUX_PREWARM_TIMEOUT_MS);
|
|
703
|
+
const remaining = () => {
|
|
704
|
+
const ms = deadline - Date.now();
|
|
705
|
+
if (ms <= 0) {
|
|
706
|
+
const error = new Error(LINUX_LOCKED_MESSAGE);
|
|
707
|
+
error.detail = 'prewarm deadline exceeded';
|
|
708
|
+
throw error;
|
|
709
|
+
}
|
|
710
|
+
return ms;
|
|
711
|
+
};
|
|
712
|
+
const epoch = _cacheEpoch;
|
|
713
|
+
const generations = new Map(_cacheGenerations);
|
|
714
|
+
const { accounts, lockedLeft } = await _linuxAccountsToRead(interactive, remaining);
|
|
715
|
+
if (_cacheEpoch !== epoch) return;
|
|
716
|
+
const values = [];
|
|
717
|
+
for (const account of accounts) {
|
|
718
|
+
const value = await linuxGetAsync(account, remaining());
|
|
719
|
+
if (value !== null) values.push([account, value]);
|
|
720
|
+
} if (_cacheEpoch !== epoch) return;
|
|
721
|
+
// Replace, not merge: values the keyring now holds overwrite the cache and
|
|
722
|
+
// cached accounts the keyring no longer has are dropped, so external CLI /
|
|
723
|
+
// other-process add, replace and delete are picked up. A same-process
|
|
724
|
+
// set/delete that ran meanwhile bumped the account's generation and wins.
|
|
725
|
+
const fresh = new Map(values);
|
|
726
|
+
const prefix = `${SERVICE}\0`;
|
|
727
|
+
let changed = false;
|
|
728
|
+
// Mixdog items in a locked collection cannot be listed: a cached account the
|
|
729
|
+
// readable items no longer show may live there, so nothing is dropped.
|
|
730
|
+
for (const key of lockedLeft ? [] : [..._secretCache.keys()]) {
|
|
731
|
+
if (!key.startsWith(prefix)) continue;
|
|
732
|
+
const account = key.slice(prefix.length);
|
|
733
|
+
if ((_cacheGenerations.get(account) || 0) !== (generations.get(account) || 0)) continue;
|
|
734
|
+
if (!fresh.has(account)) {
|
|
735
|
+
_secretCache.delete(key);
|
|
736
|
+
changed = true;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
for (const [account, value] of fresh) {
|
|
740
|
+
if ((_cacheGenerations.get(account) || 0) !== (generations.get(account) || 0)) continue;
|
|
741
|
+
if (_cacheGet(account) !== value) changed = true;
|
|
742
|
+
_cacheSet(account, value);
|
|
743
|
+
}
|
|
744
|
+
_linuxSnapshotChanged = changed;
|
|
745
|
+
if (lockedLeft) {
|
|
746
|
+
// The readable items are cached; the rest of the keyring still needs an
|
|
747
|
+
// unlock, so the status stays locked and uncached accounts keep throwing
|
|
748
|
+
// that error instead of reading as absent.
|
|
749
|
+
const error = new Error(LINUX_LOCKED_MESSAGE);
|
|
750
|
+
error.detail = 'Mixdog items sit in a locked collection (passive search)';
|
|
751
|
+
throw error;
|
|
752
|
+
}
|
|
753
|
+
_linuxSnapshotAt = Date.now();
|
|
754
|
+
_linuxLoadedUntil =
|
|
755
|
+
KEYCHAIN_CACHE_TTL_MS === Number.POSITIVE_INFINITY
|
|
756
|
+
? Number.POSITIVE_INFINITY
|
|
757
|
+
: Date.now() + KEYCHAIN_CACHE_TTL_MS;
|
|
758
|
+
_linuxLastFailureAt = 0;
|
|
759
|
+
const recovered = _keychainStatus.state !== 'ok' && _keychainStatus.state !== 'unknown';
|
|
760
|
+
_noteKeychainOk();
|
|
761
|
+
if (recovered) {
|
|
762
|
+
for (const listener of [..._recoveryListeners]) {
|
|
763
|
+
try {
|
|
764
|
+
listener();
|
|
765
|
+
} catch {
|
|
766
|
+
/* listener failures must not break keyring recovery */
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
} catch (error) {
|
|
771
|
+
_linuxLastFailureAt = Date.now();
|
|
772
|
+
_noteKeychainFailure(error);
|
|
773
|
+
// Allow a later lazy retry once the cool-down passes.
|
|
774
|
+
_prewarmPromise = null;
|
|
775
|
+
_scheduleLinuxBackgroundRetry();
|
|
776
|
+
} finally {
|
|
777
|
+
_linuxPrewarmInFlight = false;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
355
781
|
function linuxSet(account, value) {
|
|
356
782
|
const r = secretTool(['store', `--label=${SERVICE} ${account}`, 'service', SERVICE, 'account', account], value);
|
|
357
783
|
if (r.status !== 0) throw secretToolFailure('store', r);
|
|
@@ -362,6 +788,26 @@ function linuxDelete(account) {
|
|
|
362
788
|
if (r.status !== 0 && (r.stderr || '').trim()) throw secretToolFailure('clear', r);
|
|
363
789
|
}
|
|
364
790
|
|
|
791
|
+
async function linuxGetAsync(account, timeoutMs) {
|
|
792
|
+
const r = await secretToolAsync(['lookup', 'service', SERVICE, 'account', account], undefined, timeoutMs);
|
|
793
|
+
if (r.status === 0) return r.stdout;
|
|
794
|
+
if ((r.stderr || '').trim()) throw secretToolFailure('lookup', r);
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
async function linuxSetAsync(account, value) {
|
|
799
|
+
const r = await secretToolAsync(
|
|
800
|
+
['store', `--label=${SERVICE} ${account}`, 'service', SERVICE, 'account', account],
|
|
801
|
+
value
|
|
802
|
+
);
|
|
803
|
+
if (r.status !== 0) throw secretToolFailure('store', r);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
async function linuxDeleteAsync(account) {
|
|
807
|
+
const r = await secretToolAsync(['clear', 'service', SERVICE, 'account', account]);
|
|
808
|
+
if (r.status !== 0 && (r.stderr || '').trim()) throw secretToolFailure('clear', r);
|
|
809
|
+
}
|
|
810
|
+
|
|
365
811
|
// ---------------------------------------------------------------------------
|
|
366
812
|
// win32 — DPAPI via PowerShell
|
|
367
813
|
// ---------------------------------------------------------------------------
|
|
@@ -426,6 +872,7 @@ const PS_UNPROTECT_BATCH = [
|
|
|
426
872
|
|
|
427
873
|
async function _prewarmSecretsOnce() {
|
|
428
874
|
try {
|
|
875
|
+
if (platform() === 'linux') return await _prewarmLinuxOnce();
|
|
429
876
|
if (platform() !== 'win32' || KEYCHAIN_CACHE_TTL_MS === 0) return;
|
|
430
877
|
const dir = secretsDir();
|
|
431
878
|
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
@@ -475,6 +922,54 @@ function prewarmSecrets() {
|
|
|
475
922
|
return _prewarmPromise;
|
|
476
923
|
}
|
|
477
924
|
|
|
925
|
+
/** Passive refresh, called on provider-setup reads. While readable it detects a
|
|
926
|
+
* keyring that locked meanwhile (no prompt) and, once the snapshot is older
|
|
927
|
+
* than the TTL (or, for a window-focus re-check, `force`, at most every
|
|
928
|
+
* LINUX_RECHECK_MIN_MS), re-snapshots it so external changes are picked up.
|
|
929
|
+
* Resolves true when cached values changed. Never prompts. */
|
|
930
|
+
async function refreshKeychainStatus({ force = false } = {}) {
|
|
931
|
+
if (platform() !== 'linux' || _keychainStatus.state !== 'ok' || KEYCHAIN_CACHE_TTL_MS === 0) return false;
|
|
932
|
+
const age = Date.now() - _linuxSnapshotAt;
|
|
933
|
+
if (age >= (force ? LINUX_RECHECK_MIN_MS : LINUX_SNAPSHOT_TTL_MS)) {
|
|
934
|
+
if (_linuxPrewarmInFlight && _prewarmPromise) await _prewarmPromise;
|
|
935
|
+
else {
|
|
936
|
+
_prewarmPromise = null;
|
|
937
|
+
_linuxSnapshotChanged = false;
|
|
938
|
+
await prewarmSecrets();
|
|
939
|
+
}
|
|
940
|
+
return _linuxSnapshotChanged;
|
|
941
|
+
}
|
|
942
|
+
const found = await searchMixdogItems();
|
|
943
|
+
if (found && found.locked.length > 0) {
|
|
944
|
+
_keychainStatus = { state: 'locked', message: LINUX_LOCKED_MESSAGE };
|
|
945
|
+
_scheduleLinuxBackgroundRetry();
|
|
946
|
+
}
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
/** Re-read a Linux keyring that previously failed (locked, tool missing, bad
|
|
950
|
+
* snapshot) — e.g. after the user unlocked it. Resolves true when this call
|
|
951
|
+
* recovered it, so callers can drop caches built while it was unavailable.
|
|
952
|
+
* `interactive` (an explicit user action) may raise the unlock prompt: a
|
|
953
|
+
* passive attempt already in flight cannot, so it is awaited and then
|
|
954
|
+
* followed by one interactive attempt — unless that attempt was interactive
|
|
955
|
+
* itself, which already showed the (possibly declined) prompt. */
|
|
956
|
+
async function retryKeychain({ interactive = false } = {}) {
|
|
957
|
+
if (platform() !== 'linux' || _keychainStatus.state === 'ok' || _keychainStatus.state === 'unknown') return false;
|
|
958
|
+
if (_linuxPrewarmInFlight && _prewarmPromise) {
|
|
959
|
+
await _prewarmPromise;
|
|
960
|
+
if (_keychainStatus.state === 'ok' || interactive !== true || _linuxPrewarmInteractive) {
|
|
961
|
+
return _keychainStatus.state === 'ok';
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
_prewarmPromise = null;
|
|
965
|
+
_interactiveUnlock = interactive === true;
|
|
966
|
+
try {
|
|
967
|
+
await prewarmSecrets();
|
|
968
|
+
} finally {
|
|
969
|
+
_interactiveUnlock = false;
|
|
970
|
+
}
|
|
971
|
+
return _keychainStatus.state === 'ok';
|
|
972
|
+
}
|
|
478
973
|
function win32Set(account, value) {
|
|
479
974
|
const dir = secretsDir();
|
|
480
975
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
@@ -499,7 +994,14 @@ function win32Delete(account) {
|
|
|
499
994
|
|
|
500
995
|
const BACKENDS = {
|
|
501
996
|
darwin: { get: darwinGet, set: darwinSet, delete: darwinDelete },
|
|
502
|
-
linux: {
|
|
997
|
+
linux: {
|
|
998
|
+
get: linuxGet,
|
|
999
|
+
set: linuxSet,
|
|
1000
|
+
delete: linuxDelete,
|
|
1001
|
+
getAsync: linuxGetAsync,
|
|
1002
|
+
setAsync: linuxSetAsync,
|
|
1003
|
+
deleteAsync: linuxDeleteAsync,
|
|
1004
|
+
},
|
|
503
1005
|
win32: { get: win32Get, set: win32Set, delete: win32Delete },
|
|
504
1006
|
};
|
|
505
1007
|
|
|
@@ -529,10 +1031,14 @@ function hasSecret(account) {
|
|
|
529
1031
|
}
|
|
530
1032
|
|
|
531
1033
|
function setSecret(account, value) {
|
|
1034
|
+
let stored = false;
|
|
532
1035
|
try {
|
|
533
1036
|
backend().set(account, value);
|
|
1037
|
+
stored = true;
|
|
534
1038
|
} finally {
|
|
535
1039
|
_cacheInvalidate(account);
|
|
1040
|
+
// Linux reads are cache-only, so a fresh write must stay readable.
|
|
1041
|
+
if (stored && platform() === 'linux') _cacheSet(account, value);
|
|
536
1042
|
}
|
|
537
1043
|
}
|
|
538
1044
|
|
|
@@ -544,12 +1050,71 @@ function deleteSecret(account) {
|
|
|
544
1050
|
}
|
|
545
1051
|
}
|
|
546
1052
|
|
|
1053
|
+
// Async entry points: on Linux the secret-tool child runs without blocking the
|
|
1054
|
+
// event loop; other platforms keep their existing (sync) providers.
|
|
1055
|
+
async function getSecretAsync(account) {
|
|
1056
|
+
const cached = _cacheGet(account);
|
|
1057
|
+
if (cached !== undefined) return cached;
|
|
1058
|
+
const b = backend();
|
|
1059
|
+
if (!b.getAsync) return getSecret(account);
|
|
1060
|
+
if (_linuxLoaded()) return null;
|
|
1061
|
+
const generation = _cacheGenerations.get(account) || 0;
|
|
1062
|
+
let value;
|
|
1063
|
+
try {
|
|
1064
|
+
value = await b.getAsync(account);
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
_noteKeychainFailure(error);
|
|
1067
|
+
throw error;
|
|
1068
|
+
}
|
|
1069
|
+
_noteKeychainOk();
|
|
1070
|
+
if ((_cacheGenerations.get(account) || 0) === generation) _cacheSet(account, value);
|
|
1071
|
+
return value;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
async function setSecretAsync(account, value) {
|
|
1075
|
+
const b = backend();
|
|
1076
|
+
if (!b.setAsync) return setSecret(account, value);
|
|
1077
|
+
let stored = false;
|
|
1078
|
+
try {
|
|
1079
|
+
await b.setAsync(account, value);
|
|
1080
|
+
stored = true;
|
|
1081
|
+
_noteKeychainOk();
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
_noteKeychainFailure(error);
|
|
1084
|
+
throw error;
|
|
1085
|
+
} finally {
|
|
1086
|
+
_cacheInvalidate(account);
|
|
1087
|
+
if (stored) _cacheSet(account, value);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
async function deleteSecretAsync(account) {
|
|
1092
|
+
const b = backend();
|
|
1093
|
+
if (!b.deleteAsync) return deleteSecret(account);
|
|
1094
|
+
try {
|
|
1095
|
+
await b.deleteAsync(account);
|
|
1096
|
+
_noteKeychainOk();
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
_noteKeychainFailure(error);
|
|
1099
|
+
throw error;
|
|
1100
|
+
} finally {
|
|
1101
|
+
_cacheInvalidate(account);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
547
1105
|
module.exports = {
|
|
548
1106
|
getSecret,
|
|
549
1107
|
setSecret,
|
|
550
1108
|
deleteSecret,
|
|
1109
|
+
getSecretAsync,
|
|
1110
|
+
setSecretAsync,
|
|
1111
|
+
deleteSecretAsync,
|
|
551
1112
|
hasSecret,
|
|
1113
|
+
keychainStatus,
|
|
552
1114
|
invalidateSecretCache,
|
|
553
1115
|
prewarmSecrets,
|
|
1116
|
+
retryKeychain,
|
|
1117
|
+
refreshKeychainStatus,
|
|
1118
|
+
onKeychainRecovered,
|
|
554
1119
|
SERVICE,
|
|
555
1120
|
};
|