@vellumai/assistant 0.4.35 → 0.4.37
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/AGENTS.md +1 -1
- package/ARCHITECTURE.md +44 -49
- package/README.md +32 -20
- package/docs/architecture/keychain-broker.md +186 -0
- package/docs/architecture/security.md +110 -116
- package/docs/runbook-trusted-contacts.md +2 -2
- package/docs/skills.md +25 -25
- package/package.json +5 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +11 -2
- package/src/__tests__/actor-token-service.test.ts +1 -0
- package/src/__tests__/amazon-cdp-integration.test.ts +74 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +38 -9
- package/src/__tests__/assistant-id-boundary-guard.test.ts +29 -0
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/bundle-scanner.test.ts +1 -1
- package/src/__tests__/channel-guardian.test.ts +102 -102
- package/src/__tests__/channel-invite-transport.test.ts +155 -256
- package/src/__tests__/channel-readiness-routes.test.ts +336 -0
- package/src/__tests__/checker.test.ts +6 -6
- package/src/__tests__/chrome-cdp.test.ts +350 -0
- package/src/__tests__/computer-use-session-lifecycle.test.ts +3 -3
- package/src/__tests__/computer-use-session-working-dir.test.ts +86 -52
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +1 -1
- package/src/__tests__/config-loader-migration.test.ts +85 -0
- package/src/__tests__/conversation-pairing.test.ts +370 -5
- package/src/__tests__/credential-broker-browser-fill.test.ts +1 -10
- package/src/__tests__/credential-broker-server-use.test.ts +1 -10
- package/src/__tests__/credential-security-e2e.test.ts +7 -1
- package/src/__tests__/credential-security-invariants.test.ts +14 -20
- package/src/__tests__/credential-vault-unit.test.ts +1 -11
- package/src/__tests__/credential-vault.test.ts +5 -19
- package/src/__tests__/credentials-cli.test.ts +814 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +23 -4
- package/src/__tests__/email-invite-adapter.test.ts +78 -0
- package/src/__tests__/email-service-config-fallback.test.ts +102 -0
- package/src/__tests__/encrypted-store.test.ts +6 -6
- package/src/__tests__/ephemeral-permissions.test.ts +3 -3
- package/src/__tests__/gateway-only-enforcement.test.ts +5 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +70 -12
- package/src/__tests__/guardian-outbound-http.test.ts +53 -47
- package/src/__tests__/handle-user-message-secret-resume.test.ts +23 -0
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +32 -23
- package/src/__tests__/handlers-telegram-config.test.ts +8 -2
- package/src/__tests__/handlers-twitter-config.test.ts +2 -2
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +108 -7
- package/src/__tests__/ingress-reconcile.test.ts +6 -0
- package/src/__tests__/intent-routing.test.ts +23 -4
- package/src/__tests__/invite-routes-http.test.ts +12 -0
- package/src/__tests__/ipc-snapshot.test.ts +8 -2
- package/src/__tests__/keychain-broker-client.test.ts +543 -0
- package/src/__tests__/llm-usage-store.test.ts +344 -0
- package/src/__tests__/mcp-client-auth.test.ts +2 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
- package/src/__tests__/migration-transport.test.ts +49 -0
- package/src/__tests__/notification-broadcaster.test.ts +205 -5
- package/src/__tests__/notification-deep-link.test.ts +365 -1
- package/src/__tests__/oauth-connect-handler.test.ts +2 -2
- package/src/__tests__/onboarding-starter-tasks.test.ts +17 -4
- package/src/__tests__/proxy-approval-callback.test.ts +1 -1
- package/src/__tests__/recording-handler.test.ts +1 -1
- package/src/__tests__/recording-intent-handler.test.ts +6 -1
- package/src/__tests__/recording-state-machine.test.ts +1 -1
- package/src/__tests__/relay-server.test.ts +9 -1
- package/src/__tests__/ride-shotgun-handler.test.ts +499 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +160 -1
- package/src/__tests__/script-proxy-injection-runtime.test.ts +299 -2
- package/src/__tests__/script-proxy-profile-template-fallback.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +8 -2
- package/src/__tests__/secure-keys.test.ts +175 -216
- package/src/__tests__/session-confirmation-signals.test.ts +1 -1
- package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/session-queue.test.ts +2 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +2 -2
- package/src/__tests__/skill-feature-flags-integration.test.ts +29 -4
- package/src/__tests__/skill-feature-flags.test.ts +12 -9
- package/src/__tests__/skill-load-feature-flag.test.ts +26 -5
- package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
- package/src/__tests__/skills.test.ts +34 -4
- package/src/__tests__/slack-channel-config.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +26 -4
- package/src/__tests__/telegram-bot-username-resolution.test.ts +212 -0
- package/src/__tests__/telegram-invite-adapter.test.ts +164 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-permission-simulate-handler.test.ts +8 -2
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +9 -1
- package/src/__tests__/twitter-auth-handler.test.ts +2 -2
- package/src/__tests__/twitter-oauth-client.test.ts +1 -1
- package/src/__tests__/usage-routes.test.ts +339 -0
- package/src/__tests__/whatsapp-invite-adapter.test.ts +94 -0
- package/src/agent/loop.ts +3 -0
- package/src/amazon/checkout.ts +0 -1
- package/src/approvals/guardian-request-resolvers.ts +9 -1
- package/src/bundler/app-bundler.ts +28 -12
- package/src/bundler/bundle-scanner.ts +1 -1
- package/src/bundler/bundle-signer.ts +3 -3
- package/src/bundler/manifest.ts +1 -1
- package/src/bundler/signature-verifier.ts +3 -3
- package/src/channels/config.ts +1 -1
- package/src/cli/AGENTS.md +63 -0
- package/src/cli/__tests__/notifications.test.ts +470 -0
- package/src/cli/amazon.ts +344 -167
- package/src/cli/audit.ts +85 -0
- package/src/cli/autonomy.ts +369 -0
- package/src/cli/channels.ts +51 -0
- package/src/cli/completions.ts +208 -0
- package/src/cli/config.ts +220 -0
- package/src/cli/contacts.ts +471 -0
- package/src/cli/credentials.ts +564 -0
- package/src/cli/default-action.ts +14 -0
- package/src/cli/dev.ts +131 -0
- package/src/cli/doctor.ts +398 -0
- package/src/cli/email.ts +494 -0
- package/src/cli/influencer.ts +72 -0
- package/src/cli/integrations.ts +248 -57
- package/src/cli/keys.ts +114 -0
- package/src/cli/map.ts +46 -54
- package/src/cli/mcp.ts +111 -3
- package/src/cli/{config-commands.ts → memory.ts} +134 -245
- package/src/cli/notifications.ts +407 -0
- package/src/cli/program.ts +65 -0
- package/src/cli/reference.ts +48 -0
- package/src/cli/sequence.ts +154 -0
- package/src/cli/sessions.ts +262 -0
- package/src/cli/trust.ts +175 -0
- package/src/cli/twitter.ts +323 -106
- package/src/config/__tests__/build-cli-reference-section.test.ts +49 -0
- package/src/config/bundled-skills/amazon/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/TOOLS.json +26 -0
- package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +13 -0
- package/src/config/bundled-skills/contacts/SKILL.md +178 -10
- package/src/config/bundled-skills/doordash/doordash-cli.ts +23 -168
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +135 -34
- package/src/config/bundled-skills/messaging/tools/shared.ts +4 -1
- package/src/config/bundled-skills/twilio-setup/SKILL.md +70 -17
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/core-schema.ts +7 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/loader.ts +26 -0
- package/src/config/schema.ts +4 -0
- package/src/config/skill-state.ts +0 -13
- package/src/config/system-prompt.ts +27 -0
- package/src/contacts/contact-store.ts +25 -0
- package/src/daemon/computer-use-session.ts +1 -1
- package/src/daemon/handlers/apps.ts +1 -0
- package/src/daemon/handlers/config-channels.ts +3 -3
- package/src/daemon/handlers/config-dispatch.ts +29 -0
- package/src/daemon/handlers/config-inbox.ts +4 -3
- package/src/daemon/handlers/config.ts +3 -43
- package/src/daemon/handlers/contacts.ts +34 -0
- package/src/daemon/handlers/index.ts +17 -3
- package/src/daemon/handlers/session-user-message.ts +7 -0
- package/src/daemon/handlers/sessions.ts +21 -2
- package/src/daemon/handlers/shared.ts +17 -0
- package/src/daemon/ipc-contract/apps.ts +2 -0
- package/src/daemon/ipc-contract/computer-use.ts +9 -0
- package/src/daemon/ipc-contract/contacts.ts +3 -3
- package/src/daemon/ipc-contract/inbox.ts +2 -0
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +0 -5
- package/src/daemon/ride-shotgun-handler.ts +139 -25
- package/src/daemon/session-agent-loop-handlers.ts +100 -0
- package/src/daemon/session-agent-loop.ts +72 -0
- package/src/daemon/session-tool-setup.ts +7 -0
- package/src/daemon/session.ts +23 -1
- package/src/daemon/tool-side-effects.ts +39 -1
- package/src/email/service.ts +59 -2
- package/src/index.ts +2 -60
- package/src/mcp/mcp-oauth-provider.ts +90 -8
- package/src/media/app-icon-generator.ts +86 -0
- package/src/memory/db-init.ts +11 -0
- package/src/memory/llm-usage-store.ts +186 -0
- package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
- package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/shared-app-links-store.ts +1 -1
- package/src/messaging/registry.ts +27 -0
- package/src/notifications/README.md +79 -70
- package/src/notifications/broadcaster.ts +2 -1
- package/src/notifications/conversation-pairing.ts +147 -13
- package/src/notifications/copy-composer.ts +7 -3
- package/src/notifications/destination-resolver.ts +14 -1
- package/src/notifications/emit-signal.ts +3 -2
- package/src/notifications/signal.ts +105 -1
- package/src/notifications/types.ts +16 -0
- package/src/permissions/checker.ts +29 -3
- package/src/permissions/prompter.ts +11 -3
- package/src/runtime/access-request-helper.ts +2 -1
- package/src/runtime/auth/route-policy.ts +7 -1
- package/src/runtime/channel-invite-transport.ts +40 -63
- package/src/runtime/channel-invite-transports/email.ts +13 -39
- package/src/runtime/channel-invite-transports/slack.ts +5 -34
- package/src/runtime/channel-invite-transports/sms.ts +8 -29
- package/src/runtime/channel-invite-transports/telegram.ts +69 -28
- package/src/runtime/channel-invite-transports/voice.ts +0 -7
- package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
- package/src/runtime/channel-readiness-service.ts +202 -45
- package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
- package/src/runtime/guardian-outbound-actions.ts +8 -5
- package/src/runtime/http-server.ts +2 -0
- package/src/runtime/invite-instruction-generator.ts +178 -0
- package/src/runtime/invite-service.ts +22 -25
- package/src/runtime/migrations/migration-transport.ts +13 -0
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
- package/src/runtime/routes/channel-readiness-routes.ts +30 -11
- package/src/runtime/routes/contact-routes.ts +54 -26
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
- package/src/runtime/routes/integration-routes.ts +1 -1
- package/src/runtime/routes/invite-routes.ts +1 -1
- package/src/runtime/routes/secret-routes.ts +31 -7
- package/src/runtime/routes/twilio-routes.ts +32 -1
- package/src/runtime/routes/usage-routes.ts +114 -0
- package/src/runtime/tool-grant-request-helper.ts +2 -1
- package/src/security/encrypted-store.ts +9 -5
- package/src/security/keychain-broker-client.ts +393 -0
- package/src/security/secure-keys.ts +106 -321
- package/src/tools/apps/executors.ts +73 -0
- package/src/tools/browser/auto-navigate.ts +15 -6
- package/src/tools/browser/chrome-cdp.ts +211 -0
- package/src/tools/browser/network-recorder.test.ts +83 -0
- package/src/tools/browser/network-recorder.ts +8 -7
- package/src/tools/browser/x-auto-navigate.ts +12 -6
- package/src/tools/credentials/policy-types.ts +24 -0
- package/src/tools/credentials/vault.ts +22 -27
- package/src/tools/network/script-proxy/session-manager.ts +47 -3
- package/src/tools/permission-checker.ts +1 -0
- package/src/tools/types.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +1 -2
- package/src/tools/watch/watch-state.ts +2 -0
- package/src/__tests__/key-migration.test.ts +0 -240
- package/src/__tests__/keychain.test.ts +0 -286
- package/src/cli/core-commands.ts +0 -899
- package/src/security/keychain-to-encrypted-migration.ts +0 -66
- package/src/security/keychain.ts +0 -490
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* One-time migration: copies existing macOS keychain items into the
|
|
3
|
-
* encrypted file store so the daemon can stop using the keychain CLI.
|
|
4
|
-
* Runs once on first startup after the change, then skips via a marker key.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { API_KEY_PROVIDERS } from "../config/loader.js";
|
|
8
|
-
import { getLogger } from "../util/logger.js";
|
|
9
|
-
import { isMacOS } from "../util/platform.js";
|
|
10
|
-
import * as encryptedStore from "./encrypted-store.js";
|
|
11
|
-
import * as keychain from "./keychain.js";
|
|
12
|
-
|
|
13
|
-
const log = getLogger("keychain-migration");
|
|
14
|
-
const MIGRATION_MARKER = "keychain-to-encrypted-migrated";
|
|
15
|
-
|
|
16
|
-
/** Known credential keys that the daemon may have stored in the keychain. */
|
|
17
|
-
const CREDENTIAL_KEYS = [
|
|
18
|
-
"credential:twilio:account_sid",
|
|
19
|
-
"credential:twilio:auth_token",
|
|
20
|
-
"credential:twilio:phone_number",
|
|
21
|
-
"credential:twilio:user_phone_number",
|
|
22
|
-
"credential:telegram:bot_token",
|
|
23
|
-
"credential:telegram:webhook_secret",
|
|
24
|
-
"credential:elevenlabs:api_key",
|
|
25
|
-
"credential:integration:gmail:access_token",
|
|
26
|
-
"credential:integration:gmail:refresh_token",
|
|
27
|
-
"credential:integration:twitter:access_token",
|
|
28
|
-
"credential:integration:twitter:refresh_token",
|
|
29
|
-
"credential:integration:slack:access_token",
|
|
30
|
-
"credential:integration:slack:refresh_token",
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
export function migrateKeychainToEncrypted(): void {
|
|
34
|
-
if (!isMacOS()) return;
|
|
35
|
-
if (encryptedStore.getKey(MIGRATION_MARKER) === "true") return;
|
|
36
|
-
|
|
37
|
-
let migrated = 0;
|
|
38
|
-
let hadErrors = false;
|
|
39
|
-
const allKeys = [...API_KEY_PROVIDERS, ...CREDENTIAL_KEYS];
|
|
40
|
-
|
|
41
|
-
for (const account of allKeys) {
|
|
42
|
-
try {
|
|
43
|
-
const value = keychain.getKey(account);
|
|
44
|
-
if (value != null && !encryptedStore.getKey(account)) {
|
|
45
|
-
encryptedStore.setKey(account, value);
|
|
46
|
-
migrated++;
|
|
47
|
-
}
|
|
48
|
-
} catch {
|
|
49
|
-
hadErrors = true;
|
|
50
|
-
log.warn({ account }, "Keychain read failed during migration");
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (hadErrors) {
|
|
55
|
-
log.warn("Skipping migration marker — will retry on next startup");
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
encryptedStore.setKey(MIGRATION_MARKER, "true");
|
|
60
|
-
if (migrated > 0) {
|
|
61
|
-
log.info(
|
|
62
|
-
{ count: migrated },
|
|
63
|
-
"Migrated keys from keychain to encrypted store",
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
}
|
package/src/security/keychain.ts
DELETED
|
@@ -1,490 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OS keychain abstraction — platform-agnostic secure credential storage.
|
|
3
|
-
*
|
|
4
|
-
* - macOS: uses the `security` CLI to interact with Keychain
|
|
5
|
-
* - Linux: uses `secret-tool` (libsecret) for GNOME/KDE keyrings
|
|
6
|
-
*
|
|
7
|
-
* Sync variants (getKey, setKey, deleteKey) match the config loader's sync API.
|
|
8
|
-
* Async variants (getKeyAsync, setKeyAsync, deleteKeyAsync) avoid blocking
|
|
9
|
-
* the event loop and should be preferred for non-startup code paths.
|
|
10
|
-
*
|
|
11
|
-
* Callers should check `isKeychainAvailable()` before use and fall back
|
|
12
|
-
* to encrypted-at-rest storage when the keychain is not accessible.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { execFile, execFileSync } from "node:child_process";
|
|
16
|
-
import { promisify } from "node:util";
|
|
17
|
-
|
|
18
|
-
import { getLogger } from "../util/logger.js";
|
|
19
|
-
import { isLinux, isMacOS } from "../util/platform.js";
|
|
20
|
-
|
|
21
|
-
const log = getLogger("keychain");
|
|
22
|
-
const execFileAsync = promisify(execFile);
|
|
23
|
-
|
|
24
|
-
const SERVICE_NAME = "vellum-assistant";
|
|
25
|
-
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
// Injectable deps — avoids process-global mock.module for testing
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
|
|
30
|
-
const deps = {
|
|
31
|
-
execFileSync: execFileSync as typeof execFileSync,
|
|
32
|
-
isMacOS,
|
|
33
|
-
isLinux,
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/** @internal test-only — override deps to avoid mock.module conflicts */
|
|
37
|
-
export function _overrideDeps(overrides: Partial<typeof deps>): void {
|
|
38
|
-
Object.assign(deps, overrides);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** @internal test-only — restore original deps */
|
|
42
|
-
export function _resetDeps(): void {
|
|
43
|
-
deps.execFileSync = execFileSync;
|
|
44
|
-
deps.isMacOS = isMacOS;
|
|
45
|
-
deps.isLinux = isLinux;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Check if the OS keychain is available on this system. */
|
|
49
|
-
export function isKeychainAvailable(): boolean {
|
|
50
|
-
try {
|
|
51
|
-
if (deps.isMacOS()) {
|
|
52
|
-
// Verify `security` CLI exists and can list keychains
|
|
53
|
-
deps.execFileSync("security", ["list-keychains"], {
|
|
54
|
-
stdio: ["ignore", "ignore", "ignore"],
|
|
55
|
-
timeout: 5000,
|
|
56
|
-
});
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (deps.isLinux()) {
|
|
61
|
-
// Verify `secret-tool` exists
|
|
62
|
-
deps.execFileSync("which", ["secret-tool"], {
|
|
63
|
-
stdio: ["ignore", "ignore", "ignore"],
|
|
64
|
-
timeout: 5000,
|
|
65
|
-
});
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return false;
|
|
70
|
-
} catch {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Async version of `isKeychainAvailable` — probes without blocking the event loop. */
|
|
76
|
-
export async function isKeychainAvailableAsync(): Promise<boolean> {
|
|
77
|
-
try {
|
|
78
|
-
if (deps.isMacOS()) {
|
|
79
|
-
await execFileAsync("security", ["list-keychains"], {
|
|
80
|
-
timeout: 5000,
|
|
81
|
-
});
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (deps.isLinux()) {
|
|
86
|
-
await execFileAsync("which", ["secret-tool"], {
|
|
87
|
-
timeout: 5000,
|
|
88
|
-
});
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return false;
|
|
93
|
-
} catch {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Retrieve a secret from the OS keychain.
|
|
100
|
-
* Returns `null` if the key doesn't exist.
|
|
101
|
-
* Throws on runtime errors (keychain unavailable, locked, etc.).
|
|
102
|
-
*/
|
|
103
|
-
export function getKey(account: string): string | null {
|
|
104
|
-
if (deps.isMacOS()) {
|
|
105
|
-
return macosGetKey(account);
|
|
106
|
-
}
|
|
107
|
-
if (deps.isLinux()) {
|
|
108
|
-
return linuxGetKey(account);
|
|
109
|
-
}
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Store a secret in the OS keychain.
|
|
115
|
-
* Returns true on success, false on failure.
|
|
116
|
-
*/
|
|
117
|
-
export function setKey(account: string, value: string): boolean {
|
|
118
|
-
try {
|
|
119
|
-
if (deps.isMacOS()) {
|
|
120
|
-
return macosSetKey(account, value);
|
|
121
|
-
}
|
|
122
|
-
if (deps.isLinux()) {
|
|
123
|
-
return linuxSetKey(account, value);
|
|
124
|
-
}
|
|
125
|
-
return false;
|
|
126
|
-
} catch (err) {
|
|
127
|
-
log.warn({ err, account }, "Failed to write to keychain");
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Delete a secret from the OS keychain.
|
|
134
|
-
* Returns true on success, false if not found or on failure.
|
|
135
|
-
*/
|
|
136
|
-
export function deleteKey(account: string): boolean {
|
|
137
|
-
try {
|
|
138
|
-
if (deps.isMacOS()) {
|
|
139
|
-
return macosDeleteKey(account);
|
|
140
|
-
}
|
|
141
|
-
if (deps.isLinux()) {
|
|
142
|
-
return linuxDeleteKey(account);
|
|
143
|
-
}
|
|
144
|
-
return false;
|
|
145
|
-
} catch (err) {
|
|
146
|
-
log.debug({ err, account }, "Failed to delete from keychain");
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ---------------------------------------------------------------------------
|
|
152
|
-
// macOS Keychain via `security` CLI
|
|
153
|
-
// ---------------------------------------------------------------------------
|
|
154
|
-
|
|
155
|
-
function macosGetKey(account: string): string | null {
|
|
156
|
-
try {
|
|
157
|
-
const result = deps.execFileSync(
|
|
158
|
-
"security",
|
|
159
|
-
[
|
|
160
|
-
"find-generic-password",
|
|
161
|
-
"-s",
|
|
162
|
-
SERVICE_NAME,
|
|
163
|
-
"-a",
|
|
164
|
-
account,
|
|
165
|
-
"-w", // output password only
|
|
166
|
-
],
|
|
167
|
-
{
|
|
168
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
169
|
-
timeout: 5000,
|
|
170
|
-
encoding: "utf-8",
|
|
171
|
-
},
|
|
172
|
-
);
|
|
173
|
-
// Strip only the trailing newline added by the security CLI
|
|
174
|
-
return result.replace(/\n$/, "") || null;
|
|
175
|
-
} catch (err: unknown) {
|
|
176
|
-
// Exit code 44 = item not found — return null.
|
|
177
|
-
// All other errors are runtime failures — re-throw.
|
|
178
|
-
if (
|
|
179
|
-
err &&
|
|
180
|
-
typeof err === "object" &&
|
|
181
|
-
"status" in err &&
|
|
182
|
-
(err as { status: number }).status === 44
|
|
183
|
-
) {
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
throw err;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function macosSetKey(account: string, value: string): boolean {
|
|
191
|
-
// -U flag handles update-if-exists, no need to delete first.
|
|
192
|
-
// macOS `security` requires the password as the argument to -w;
|
|
193
|
-
// it does NOT read from stdin. Using `-w` without a value causes
|
|
194
|
-
// the next flag to be consumed as the password.
|
|
195
|
-
try {
|
|
196
|
-
deps.execFileSync(
|
|
197
|
-
"security",
|
|
198
|
-
[
|
|
199
|
-
"add-generic-password",
|
|
200
|
-
"-s",
|
|
201
|
-
SERVICE_NAME,
|
|
202
|
-
"-a",
|
|
203
|
-
account,
|
|
204
|
-
"-w",
|
|
205
|
-
value,
|
|
206
|
-
"-U", // update if exists
|
|
207
|
-
],
|
|
208
|
-
{
|
|
209
|
-
stdio: ["ignore", "ignore", "ignore"],
|
|
210
|
-
timeout: 5000,
|
|
211
|
-
},
|
|
212
|
-
);
|
|
213
|
-
return true;
|
|
214
|
-
} catch {
|
|
215
|
-
return false;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function macosDeleteKey(account: string): boolean {
|
|
220
|
-
try {
|
|
221
|
-
deps.execFileSync(
|
|
222
|
-
"security",
|
|
223
|
-
["delete-generic-password", "-s", SERVICE_NAME, "-a", account],
|
|
224
|
-
{
|
|
225
|
-
stdio: ["ignore", "ignore", "ignore"],
|
|
226
|
-
timeout: 5000,
|
|
227
|
-
},
|
|
228
|
-
);
|
|
229
|
-
return true;
|
|
230
|
-
} catch {
|
|
231
|
-
return false;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// ---------------------------------------------------------------------------
|
|
236
|
-
// Linux via `secret-tool` (libsecret)
|
|
237
|
-
// ---------------------------------------------------------------------------
|
|
238
|
-
|
|
239
|
-
function linuxGetKey(account: string): string | null {
|
|
240
|
-
try {
|
|
241
|
-
const result = deps.execFileSync(
|
|
242
|
-
"secret-tool",
|
|
243
|
-
["lookup", "service", SERVICE_NAME, "account", account],
|
|
244
|
-
{
|
|
245
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
246
|
-
timeout: 5000,
|
|
247
|
-
encoding: "utf-8",
|
|
248
|
-
},
|
|
249
|
-
);
|
|
250
|
-
// Strip only the trailing newline added by secret-tool
|
|
251
|
-
return result.replace(/\n$/, "") || null;
|
|
252
|
-
} catch (err: unknown) {
|
|
253
|
-
// secret-tool exits with code 1 for BOTH "not found" and runtime errors
|
|
254
|
-
// (D-Bus failures, keyring locked, etc.). Distinguish by checking stderr:
|
|
255
|
-
// empty stderr → key not found; non-empty stderr → runtime error.
|
|
256
|
-
if (
|
|
257
|
-
err &&
|
|
258
|
-
typeof err === "object" &&
|
|
259
|
-
"status" in err &&
|
|
260
|
-
(err as { status: number }).status === 1
|
|
261
|
-
) {
|
|
262
|
-
const stderr = String((err as { stderr?: unknown }).stderr ?? "").trim();
|
|
263
|
-
if (stderr.length > 0) {
|
|
264
|
-
throw err;
|
|
265
|
-
}
|
|
266
|
-
return null;
|
|
267
|
-
}
|
|
268
|
-
throw err;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function linuxSetKey(account: string, value: string): boolean {
|
|
273
|
-
try {
|
|
274
|
-
deps.execFileSync(
|
|
275
|
-
"secret-tool",
|
|
276
|
-
[
|
|
277
|
-
"store",
|
|
278
|
-
"--label",
|
|
279
|
-
`${SERVICE_NAME}: ${account}`,
|
|
280
|
-
"service",
|
|
281
|
-
SERVICE_NAME,
|
|
282
|
-
"account",
|
|
283
|
-
account,
|
|
284
|
-
],
|
|
285
|
-
{
|
|
286
|
-
input: value,
|
|
287
|
-
stdio: ["pipe", "ignore", "ignore"],
|
|
288
|
-
timeout: 5000,
|
|
289
|
-
},
|
|
290
|
-
);
|
|
291
|
-
return true;
|
|
292
|
-
} catch {
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function linuxDeleteKey(account: string): boolean {
|
|
298
|
-
try {
|
|
299
|
-
deps.execFileSync(
|
|
300
|
-
"secret-tool",
|
|
301
|
-
["clear", "service", SERVICE_NAME, "account", account],
|
|
302
|
-
{
|
|
303
|
-
stdio: ["ignore", "ignore", "ignore"],
|
|
304
|
-
timeout: 5000,
|
|
305
|
-
},
|
|
306
|
-
);
|
|
307
|
-
return true;
|
|
308
|
-
} catch {
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// ---------------------------------------------------------------------------
|
|
314
|
-
// Async variants — non-blocking alternatives to the sync functions above.
|
|
315
|
-
// Preferred for non-startup code paths to avoid blocking the event loop.
|
|
316
|
-
// ---------------------------------------------------------------------------
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Async version of `getKey` — retrieve a secret without blocking the event loop.
|
|
320
|
-
* Returns `null` if the key doesn't exist.
|
|
321
|
-
* Throws on runtime errors (keychain unavailable, locked, etc.).
|
|
322
|
-
*/
|
|
323
|
-
export async function getKeyAsync(account: string): Promise<string | null> {
|
|
324
|
-
if (deps.isMacOS()) return macosGetKeyAsync(account);
|
|
325
|
-
if (deps.isLinux()) return linuxGetKeyAsync(account);
|
|
326
|
-
return null;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Async version of `setKey` — store a secret without blocking the event loop.
|
|
331
|
-
* Returns true on success, false on failure.
|
|
332
|
-
*/
|
|
333
|
-
export async function setKeyAsync(
|
|
334
|
-
account: string,
|
|
335
|
-
value: string,
|
|
336
|
-
): Promise<boolean> {
|
|
337
|
-
try {
|
|
338
|
-
if (deps.isMacOS()) return await macosSetKeyAsync(account, value);
|
|
339
|
-
if (deps.isLinux()) return await linuxSetKeyAsync(account, value);
|
|
340
|
-
return false;
|
|
341
|
-
} catch (err) {
|
|
342
|
-
log.warn({ err, account }, "Failed to write to keychain");
|
|
343
|
-
return false;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Async version of `deleteKey` — delete a secret without blocking the event loop.
|
|
349
|
-
* Returns true on success, false if not found or on failure.
|
|
350
|
-
*/
|
|
351
|
-
export async function deleteKeyAsync(account: string): Promise<boolean> {
|
|
352
|
-
try {
|
|
353
|
-
if (deps.isMacOS()) return await macosDeleteKeyAsync(account);
|
|
354
|
-
if (deps.isLinux()) return await linuxDeleteKeyAsync(account);
|
|
355
|
-
return false;
|
|
356
|
-
} catch (err) {
|
|
357
|
-
log.debug({ err, account }, "Failed to delete from keychain");
|
|
358
|
-
return false;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// ---------------------------------------------------------------------------
|
|
363
|
-
// Async macOS Keychain
|
|
364
|
-
// ---------------------------------------------------------------------------
|
|
365
|
-
|
|
366
|
-
async function macosGetKeyAsync(account: string): Promise<string | null> {
|
|
367
|
-
try {
|
|
368
|
-
const { stdout } = await execFileAsync(
|
|
369
|
-
"security",
|
|
370
|
-
["find-generic-password", "-s", SERVICE_NAME, "-a", account, "-w"],
|
|
371
|
-
{ timeout: 5000 },
|
|
372
|
-
);
|
|
373
|
-
return stdout.replace(/\n$/, "") || null;
|
|
374
|
-
} catch (err: unknown) {
|
|
375
|
-
// Exit code 44 = item not found — return null.
|
|
376
|
-
if (
|
|
377
|
-
err &&
|
|
378
|
-
typeof err === "object" &&
|
|
379
|
-
"code" in err &&
|
|
380
|
-
(err as { code: number }).code === 44
|
|
381
|
-
) {
|
|
382
|
-
return null;
|
|
383
|
-
}
|
|
384
|
-
throw err;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
async function macosSetKeyAsync(
|
|
389
|
-
account: string,
|
|
390
|
-
value: string,
|
|
391
|
-
): Promise<boolean> {
|
|
392
|
-
try {
|
|
393
|
-
await execFileAsync(
|
|
394
|
-
"security",
|
|
395
|
-
[
|
|
396
|
-
"add-generic-password",
|
|
397
|
-
"-s",
|
|
398
|
-
SERVICE_NAME,
|
|
399
|
-
"-a",
|
|
400
|
-
account,
|
|
401
|
-
"-w",
|
|
402
|
-
value,
|
|
403
|
-
"-U",
|
|
404
|
-
],
|
|
405
|
-
{ timeout: 5000 },
|
|
406
|
-
);
|
|
407
|
-
return true;
|
|
408
|
-
} catch {
|
|
409
|
-
return false;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
async function macosDeleteKeyAsync(account: string): Promise<boolean> {
|
|
414
|
-
try {
|
|
415
|
-
await execFileAsync(
|
|
416
|
-
"security",
|
|
417
|
-
["delete-generic-password", "-s", SERVICE_NAME, "-a", account],
|
|
418
|
-
{ timeout: 5000 },
|
|
419
|
-
);
|
|
420
|
-
return true;
|
|
421
|
-
} catch {
|
|
422
|
-
return false;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// ---------------------------------------------------------------------------
|
|
427
|
-
// Async Linux via `secret-tool`
|
|
428
|
-
// ---------------------------------------------------------------------------
|
|
429
|
-
|
|
430
|
-
async function linuxGetKeyAsync(account: string): Promise<string | null> {
|
|
431
|
-
try {
|
|
432
|
-
const { stdout } = await execFileAsync(
|
|
433
|
-
"secret-tool",
|
|
434
|
-
["lookup", "service", SERVICE_NAME, "account", account],
|
|
435
|
-
{ timeout: 5000 },
|
|
436
|
-
);
|
|
437
|
-
return stdout.replace(/\n$/, "") || null;
|
|
438
|
-
} catch (err: unknown) {
|
|
439
|
-
if (
|
|
440
|
-
err &&
|
|
441
|
-
typeof err === "object" &&
|
|
442
|
-
"code" in err &&
|
|
443
|
-
(err as { code: number }).code === 1
|
|
444
|
-
) {
|
|
445
|
-
const stderr = String((err as { stderr?: unknown }).stderr ?? "").trim();
|
|
446
|
-
if (stderr.length > 0) throw err;
|
|
447
|
-
return null;
|
|
448
|
-
}
|
|
449
|
-
throw err;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
async function linuxSetKeyAsync(
|
|
454
|
-
account: string,
|
|
455
|
-
value: string,
|
|
456
|
-
): Promise<boolean> {
|
|
457
|
-
// secret-tool reads the secret from stdin
|
|
458
|
-
return new Promise<boolean>((resolve) => {
|
|
459
|
-
const child = execFile(
|
|
460
|
-
"secret-tool",
|
|
461
|
-
[
|
|
462
|
-
"store",
|
|
463
|
-
"--label",
|
|
464
|
-
`${SERVICE_NAME}: ${account}`,
|
|
465
|
-
"service",
|
|
466
|
-
SERVICE_NAME,
|
|
467
|
-
"account",
|
|
468
|
-
account,
|
|
469
|
-
],
|
|
470
|
-
{ timeout: 5000 },
|
|
471
|
-
(err) => {
|
|
472
|
-
resolve(!err);
|
|
473
|
-
},
|
|
474
|
-
);
|
|
475
|
-
child.stdin?.end(value);
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
async function linuxDeleteKeyAsync(account: string): Promise<boolean> {
|
|
480
|
-
try {
|
|
481
|
-
await execFileAsync(
|
|
482
|
-
"secret-tool",
|
|
483
|
-
["clear", "service", SERVICE_NAME, "account", account],
|
|
484
|
-
{ timeout: 5000 },
|
|
485
|
-
);
|
|
486
|
-
return true;
|
|
487
|
-
} catch {
|
|
488
|
-
return false;
|
|
489
|
-
}
|
|
490
|
-
}
|