@vellumai/assistant 0.5.9 → 0.5.11
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 +9 -1
- package/ARCHITECTURE.md +48 -48
- package/Dockerfile +2 -0
- package/README.md +1 -1
- package/docs/architecture/integrations.md +6 -13
- package/docs/architecture/memory.md +7 -12
- package/docs/architecture/security.md +5 -5
- package/docs/credential-execution-service.md +9 -9
- package/docs/skills.md +1 -1
- package/node_modules/@vellumai/credential-storage/src/index.ts +2 -2
- package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +1 -1
- package/openapi.yaml +7130 -0
- package/package.json +2 -1
- package/scripts/generate-openapi.ts +562 -0
- package/src/__tests__/acp-session.test.ts +239 -44
- package/src/__tests__/assistant-feature-flag-guard.test.ts +8 -8
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +5 -86
- package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -14
- package/src/__tests__/browser-skill-endstate.test.ts +1 -1
- package/src/__tests__/btw-routes.test.ts +8 -0
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +10 -10
- package/src/__tests__/channel-approvals.test.ts +7 -7
- package/src/__tests__/channel-readiness-service.test.ts +41 -0
- package/src/__tests__/config-schema.test.ts +10 -2
- package/src/__tests__/context-memory-e2e.test.ts +2 -6
- package/src/__tests__/conversation-skill-tools.test.ts +1 -3
- package/src/__tests__/conversation-title-service.test.ts +2 -15
- package/src/__tests__/credential-execution-feature-gates.test.ts +4 -8
- package/src/__tests__/credential-execution-managed-contract.test.ts +8 -8
- package/src/__tests__/credential-security-e2e.test.ts +4 -4
- package/src/__tests__/credential-security-invariants.test.ts +3 -3
- package/src/__tests__/credentials-cli.test.ts +3 -3
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -1
- package/src/__tests__/gateway-only-guard.test.ts +3 -0
- package/src/__tests__/heartbeat-service.test.ts +35 -0
- package/src/__tests__/host-shell-tool.test.ts +1 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +3 -3
- package/src/__tests__/llm-request-log-turn-query.test.ts +64 -0
- package/src/__tests__/log-export-workspace.test.ts +1 -1
- package/src/__tests__/mcp-client-auth.test.ts +1 -1
- package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
- package/src/__tests__/memory-recall-log-store.test.ts +182 -0
- package/src/__tests__/memory-recall-quality.test.ts +6 -8
- package/src/__tests__/memory-regressions.test.ts +53 -42
- package/src/__tests__/memory-retrieval.benchmark.test.ts +5 -9
- package/src/__tests__/messaging-skill-split.test.ts +2 -17
- package/src/__tests__/oauth-cli.test.ts +98 -551
- package/src/__tests__/platform-callback-registration.test.ts +119 -0
- package/src/__tests__/secret-ingress-channel.test.ts +261 -0
- package/src/__tests__/secret-ingress-cli.test.ts +201 -0
- package/src/__tests__/secret-ingress-http.test.ts +312 -0
- package/src/__tests__/secret-ingress.test.ts +283 -0
- package/src/__tests__/secret-onetime-send.test.ts +4 -4
- package/src/__tests__/skill-feature-flags-integration.test.ts +4 -4
- package/src/__tests__/skill-feature-flags.test.ts +11 -19
- package/src/__tests__/skill-load-feature-flag.test.ts +1 -1
- package/src/__tests__/skill-load-inline-command.test.ts +3 -3
- package/src/__tests__/skill-load-inline-includes.test.ts +2 -2
- package/src/__tests__/skill-memory.test.ts +2 -4
- package/src/__tests__/skill-projection-feature-flag.test.ts +2 -4
- package/src/__tests__/skill-projection.benchmark.test.ts +1 -3
- package/src/__tests__/skills.test.ts +16 -2
- package/src/__tests__/slack-channel-config.test.ts +1 -1
- package/src/__tests__/slack-skill.test.ts +5 -69
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -1
- package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +5 -238
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -206
- package/src/__tests__/workspace-migration-018-rekey-compound-credential-keys.test.ts +181 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +15 -7
- package/src/acp/client-handler.ts +113 -31
- package/src/acp/session-manager.ts +29 -27
- package/src/approvals/guardian-request-resolvers.ts +1 -1
- package/src/cli/AGENTS.md +73 -0
- package/src/cli/commands/autonomy.ts +3 -5
- package/src/cli/commands/credential-execution.ts +1 -2
- package/src/cli/commands/credentials.ts +4 -4
- package/src/cli/commands/memory.ts +2 -3
- package/src/cli/commands/oauth/__tests__/connect.test.ts +785 -0
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +760 -0
- package/src/cli/commands/oauth/__tests__/mode.test.ts +672 -0
- package/src/cli/commands/oauth/__tests__/ping.test.ts +690 -0
- package/src/cli/commands/oauth/__tests__/status.test.ts +579 -0
- package/src/cli/commands/oauth/__tests__/token.test.ts +467 -0
- package/src/cli/commands/oauth/apps.ts +29 -11
- package/src/cli/commands/oauth/connect.ts +373 -0
- package/src/cli/commands/oauth/connections.ts +14 -493
- package/src/cli/commands/oauth/disconnect.ts +333 -0
- package/src/cli/commands/oauth/index.ts +62 -10
- package/src/cli/commands/oauth/mode.ts +263 -0
- package/src/cli/commands/oauth/ping.ts +222 -0
- package/src/cli/commands/oauth/providers.ts +30 -3
- package/src/cli/commands/oauth/request.ts +576 -0
- package/src/cli/commands/oauth/shared.ts +132 -0
- package/src/cli/commands/oauth/status.ts +202 -0
- package/src/cli/commands/oauth/token.ts +159 -0
- package/src/cli/commands/platform.ts +20 -14
- package/src/cli.ts +82 -17
- package/src/config/assistant-feature-flags.ts +74 -11
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +13 -36
- package/src/config/bundled-skills/messaging/TOOLS.json +9 -9
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +1 -1
- package/src/config/bundled-skills/notifications/SKILL.md +1 -1
- package/src/config/bundled-skills/schedule/SKILL.md +2 -2
- package/src/config/bundled-skills/settings/SKILL.md +5 -3
- package/src/config/bundled-skills/settings/TOOLS.json +17 -0
- package/src/config/bundled-skills/settings/tools/avatar-get.ts +50 -0
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +7 -0
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +6 -1
- package/src/config/bundled-skills/settings/tools/identity-avatar.ts +55 -0
- package/src/config/bundled-skills/skills-catalog/SKILL.md +3 -3
- package/src/config/bundled-skills/slack/SKILL.md +58 -44
- package/src/config/bundled-tool-registry.ts +2 -19
- package/src/config/env.ts +5 -1
- package/src/config/feature-flag-registry.json +57 -41
- package/src/config/loader.ts +4 -0
- package/src/config/schemas/platform.ts +0 -8
- package/src/config/schemas/security.ts +9 -1
- package/src/config/schemas/services.ts +1 -1
- package/src/config/skill-state.ts +1 -3
- package/src/config/skills.ts +2 -4
- package/src/credential-execution/feature-gates.ts +9 -16
- package/src/credential-execution/process-manager.ts +12 -0
- package/src/daemon/config-watcher.ts +4 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +10 -0
- package/src/daemon/conversation-agent-loop.ts +49 -2
- package/src/daemon/conversation-memory.ts +0 -1
- package/src/daemon/handlers/config-slack-channel.ts +43 -1
- package/src/daemon/handlers/conversations.ts +41 -33
- package/src/daemon/lifecycle.ts +28 -5
- package/src/daemon/message-types/acp.ts +0 -15
- package/src/daemon/message-types/memory.ts +0 -1
- package/src/daemon/message-types/messages.ts +9 -1
- package/src/daemon/message-types/schedules.ts +9 -0
- package/src/daemon/server.ts +19 -7
- package/src/email/feature-gate.ts +3 -3
- package/src/heartbeat/heartbeat-service.ts +48 -0
- package/src/inbound/platform-callback-registration.ts +61 -7
- package/src/mcp/mcp-oauth-provider.ts +3 -3
- package/src/memory/app-store.ts +3 -3
- package/src/memory/conversation-crud.ts +124 -0
- package/src/memory/conversation-title-service.ts +7 -17
- package/src/memory/db-init.ts +8 -0
- package/src/memory/embedding-local.ts +47 -2
- package/src/memory/indexer.ts +13 -10
- package/src/memory/items-extractor.ts +12 -4
- package/src/memory/job-utils.ts +5 -0
- package/src/memory/jobs-store.ts +10 -2
- package/src/memory/journal-memory.ts +6 -2
- package/src/memory/llm-request-log-store.ts +88 -21
- package/src/memory/memory-recall-log-store.ts +128 -0
- package/src/memory/migrations/194-memory-recall-logs.ts +50 -0
- package/src/memory/migrations/195-oauth-providers-ping-config.ts +23 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/migrations/validate-migration-state.ts +14 -1
- package/src/memory/retriever.test.ts +4 -5
- package/src/memory/schema/infrastructure.ts +31 -0
- package/src/memory/schema/oauth.ts +3 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +1 -1
- package/src/oauth/connect-orchestrator.ts +54 -0
- package/src/oauth/manual-token-connection.ts +5 -5
- package/src/oauth/oauth-store.ts +26 -5
- package/src/oauth/seed-providers.ts +10 -1
- package/src/permissions/checker.ts +2 -2
- package/src/permissions/trust-client.ts +2 -2
- package/src/platform/client.ts +2 -2
- package/src/prompts/journal-context.ts +6 -1
- package/src/providers/anthropic/client.ts +143 -1
- package/src/runtime/auth/__tests__/middleware.test.ts +19 -0
- package/src/runtime/auth/route-policy.ts +0 -1
- package/src/runtime/btw-sidechain.ts +7 -1
- package/src/runtime/channel-approvals.ts +2 -2
- package/src/runtime/channel-readiness-service.ts +30 -7
- package/src/runtime/http-router.ts +31 -0
- package/src/runtime/http-server.ts +21 -4
- package/src/runtime/http-types.ts +2 -0
- package/src/runtime/pending-interactions.ts +21 -3
- package/src/runtime/routes/acp-routes.ts +46 -28
- package/src/runtime/routes/app-management-routes.ts +123 -0
- package/src/runtime/routes/app-routes.ts +31 -0
- package/src/runtime/routes/approval-routes.ts +108 -3
- package/src/runtime/routes/attachment-routes.ts +45 -0
- package/src/runtime/routes/avatar-routes.ts +16 -0
- package/src/runtime/routes/brain-graph-routes.ts +18 -0
- package/src/runtime/routes/btw-routes.ts +20 -0
- package/src/runtime/routes/call-routes.ts +81 -0
- package/src/runtime/routes/channel-readiness-routes.ts +48 -7
- package/src/runtime/routes/channel-routes.ts +18 -0
- package/src/runtime/routes/channel-verification-routes.ts +49 -1
- package/src/runtime/routes/contact-routes.ts +77 -0
- package/src/runtime/routes/conversation-attention-routes.ts +37 -0
- package/src/runtime/routes/conversation-management-routes.ts +94 -0
- package/src/runtime/routes/conversation-query-routes.ts +78 -0
- package/src/runtime/routes/conversation-routes.ts +115 -38
- package/src/runtime/routes/conversation-starter-routes.ts +29 -0
- package/src/runtime/routes/debug-routes.ts +23 -0
- package/src/runtime/routes/diagnostics-routes.ts +30 -0
- package/src/runtime/routes/documents-routes.ts +42 -0
- package/src/runtime/routes/events-routes.ts +10 -0
- package/src/runtime/routes/global-search-routes.ts +35 -0
- package/src/runtime/routes/guardian-action-routes.ts +47 -2
- package/src/runtime/routes/guardian-approval-prompt.ts +77 -2
- package/src/runtime/routes/heartbeat-routes.ts +278 -0
- package/src/runtime/routes/host-bash-routes.ts +16 -1
- package/src/runtime/routes/host-cu-routes.ts +23 -1
- package/src/runtime/routes/host-file-routes.ts +18 -1
- package/src/runtime/routes/identity-routes.ts +35 -0
- package/src/runtime/routes/inbound-message-handler.ts +46 -25
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +30 -2
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -2
- package/src/runtime/routes/integrations/twilio.ts +32 -22
- package/src/runtime/routes/invite-routes.ts +83 -0
- package/src/runtime/routes/log-export-routes.ts +14 -0
- package/src/runtime/routes/memory-item-routes.ts +99 -1
- package/src/runtime/routes/migration-rollback-routes.ts +25 -0
- package/src/runtime/routes/migration-routes.ts +40 -0
- package/src/runtime/routes/notification-routes.ts +20 -0
- package/src/runtime/routes/oauth-apps.ts +11 -3
- package/src/runtime/routes/pairing-routes.ts +15 -0
- package/src/runtime/routes/recording-routes.ts +72 -0
- package/src/runtime/routes/schedule-routes.ts +77 -5
- package/src/runtime/routes/secret-routes.ts +63 -1
- package/src/runtime/routes/settings-routes.ts +91 -1
- package/src/runtime/routes/skills-routes.ts +98 -16
- package/src/runtime/routes/subagents-routes.ts +38 -3
- package/src/runtime/routes/surface-action-routes.ts +66 -24
- package/src/runtime/routes/surface-content-routes.ts +20 -0
- package/src/runtime/routes/telemetry-routes.ts +12 -0
- package/src/runtime/routes/trace-event-routes.ts +25 -0
- package/src/runtime/routes/trust-rules-routes.ts +46 -0
- package/src/runtime/routes/tts-routes.ts +15 -4
- package/src/runtime/routes/upgrade-broadcast-routes.ts +38 -0
- package/src/runtime/routes/usage-routes.ts +59 -0
- package/src/runtime/routes/watch-routes.ts +28 -0
- package/src/runtime/routes/work-items-routes.ts +59 -0
- package/src/runtime/routes/workspace-commit-routes.ts +12 -0
- package/src/runtime/routes/workspace-routes.ts +102 -0
- package/src/schedule/scheduler.ts +7 -1
- package/src/security/AGENTS.md +7 -0
- package/src/security/credential-backend.ts +1 -1
- package/src/security/encrypted-store.ts +3 -3
- package/src/security/oauth2.ts +55 -0
- package/src/security/secret-ingress.ts +174 -0
- package/src/security/secret-patterns.ts +133 -0
- package/src/security/secret-scanner.ts +28 -117
- package/src/signals/confirm.ts +12 -8
- package/src/signals/user-message.ts +18 -3
- package/src/skills/skill-memory.ts +1 -2
- package/src/tasks/task-runner.ts +7 -1
- package/src/tools/credentials/broker.ts +1 -1
- package/src/tools/credentials/metadata-store.ts +1 -1
- package/src/tools/credentials/vault.ts +2 -3
- package/src/tools/memory/definitions.ts +1 -1
- package/src/tools/memory/handlers.test.ts +2 -4
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/terminal/safe-env.ts +7 -0
- package/src/tools/tool-manifest.ts +1 -1
- package/src/util/log-redact.ts +9 -34
- package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +13 -148
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +7 -145
- package/src/workspace/migrations/AGENTS.md +11 -0
- package/src/workspace/migrations/runner.ts +16 -6
- package/src/workspace/migrations/types.ts +7 -0
- package/docs/architecture/keychain-broker.md +0 -69
- package/src/__tests__/keychain-broker-client.test.ts +0 -800
- package/src/cli/commands/oauth/platform.ts +0 -525
- package/src/config/bundled-skills/slack/TOOLS.json +0 -272
- package/src/config/bundled-skills/slack/tools/shared.ts +0 -34
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +0 -27
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +0 -38
- package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +0 -146
- package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +0 -105
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +0 -26
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +0 -27
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +0 -25
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +0 -372
- package/src/security/keychain-broker-client.ts +0 -446
|
@@ -1,446 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TypeScript client for the keychain broker Unix domain socket protocol.
|
|
3
|
-
*
|
|
4
|
-
* The keychain broker runs inside the macOS app and exposes SecItem*
|
|
5
|
-
* operations over a newline-delimited JSON protocol on a UDS. This client
|
|
6
|
-
* provides a graceful-fallback interface: every public method returns a
|
|
7
|
-
* safe default on failure and never throws.
|
|
8
|
-
*
|
|
9
|
-
* Socket path: derived from getRootDir() as `~/.vellum/keychain-broker.sock`.
|
|
10
|
-
* Auth token: read from ~/.vellum/protected/keychain-broker.token on first
|
|
11
|
-
* connection, cached for process lifetime.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { randomUUID } from "node:crypto";
|
|
15
|
-
import { readFileSync } from "node:fs";
|
|
16
|
-
import type { Socket } from "node:net";
|
|
17
|
-
import { createConnection } from "node:net";
|
|
18
|
-
import { join } from "node:path";
|
|
19
|
-
|
|
20
|
-
import { pathExists } from "../util/fs.js";
|
|
21
|
-
import { getLogger } from "../util/logger.js";
|
|
22
|
-
import { getRootDir } from "../util/platform.js";
|
|
23
|
-
|
|
24
|
-
const log = getLogger("keychain-broker-client");
|
|
25
|
-
|
|
26
|
-
const REQUEST_TIMEOUT_MS = 5_000;
|
|
27
|
-
const CONNECT_TIMEOUT_MS = 3_000;
|
|
28
|
-
|
|
29
|
-
/** Cooldown periods (ms) after consecutive connection failures: 5s, 15s, 30s, 60s, 5min, then cap at 5min. */
|
|
30
|
-
const RECONNECT_COOLDOWN_MS = [5_000, 15_000, 30_000, 60_000, 300_000];
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// Types
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
/** Result of a `get()` call. `null` means broker error (caller should fall
|
|
37
|
-
* back); `{ found: false }` means the key doesn't exist in the keychain. */
|
|
38
|
-
export type BrokerGetResult = { found: boolean; value?: string } | null;
|
|
39
|
-
|
|
40
|
-
/** Result of a `set()` call — distinguishes broker-unreachable from an active
|
|
41
|
-
* rejection so callers can log meaningful diagnostics. */
|
|
42
|
-
export type BrokerSetResult =
|
|
43
|
-
| { status: "ok" }
|
|
44
|
-
| { status: "unreachable" }
|
|
45
|
-
| { status: "rejected"; code: string; message: string };
|
|
46
|
-
|
|
47
|
-
export interface KeychainBrokerClient {
|
|
48
|
-
isAvailable(): boolean;
|
|
49
|
-
ping(): Promise<{ pong: boolean } | null>;
|
|
50
|
-
get(account: string): Promise<BrokerGetResult>;
|
|
51
|
-
set(account: string, value: string): Promise<BrokerSetResult>;
|
|
52
|
-
del(account: string): Promise<boolean>;
|
|
53
|
-
list(): Promise<string[]>;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
interface BrokerRequest {
|
|
57
|
-
v: number;
|
|
58
|
-
id: string;
|
|
59
|
-
method: string;
|
|
60
|
-
token: string;
|
|
61
|
-
params?: Record<string, unknown>;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
interface BrokerResponse {
|
|
65
|
-
id: string;
|
|
66
|
-
ok: boolean;
|
|
67
|
-
result?: Record<string, unknown>;
|
|
68
|
-
error?: { code: string; message: string };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
interface PendingRequest {
|
|
72
|
-
resolve: (response: BrokerResponse) => void;
|
|
73
|
-
timer: ReturnType<typeof setTimeout>;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
// Internal state
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
|
|
80
|
-
function getTokenPath(): string {
|
|
81
|
-
return join(getRootDir(), "protected", "keychain-broker.token");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function getSocketPath(): string {
|
|
85
|
-
return join(getRootDir(), "keychain-broker.sock");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ---------------------------------------------------------------------------
|
|
89
|
-
// Client implementation
|
|
90
|
-
// ---------------------------------------------------------------------------
|
|
91
|
-
|
|
92
|
-
export function createBrokerClient(): KeychainBrokerClient {
|
|
93
|
-
let socket: Socket | null = null;
|
|
94
|
-
/** Promise that resolves when the in-flight connect() completes. */
|
|
95
|
-
let connectPromise: Promise<Socket> | null = null;
|
|
96
|
-
/** Timestamp when the broker became unavailable, or null if available. */
|
|
97
|
-
let unavailableSince: number | null = null;
|
|
98
|
-
/** Number of consecutive connection failures (drives cooldown escalation). */
|
|
99
|
-
let consecutiveFailures = 0;
|
|
100
|
-
/** Cached token string, or undefined if not yet successfully read. */
|
|
101
|
-
let cachedToken: string | undefined;
|
|
102
|
-
|
|
103
|
-
/** Buffer for incoming data — responses are newline-delimited JSON. */
|
|
104
|
-
let inBuffer = "";
|
|
105
|
-
|
|
106
|
-
const pending = new Map<string, PendingRequest>();
|
|
107
|
-
|
|
108
|
-
// -------------------------------------------------------------------------
|
|
109
|
-
// Token management
|
|
110
|
-
// -------------------------------------------------------------------------
|
|
111
|
-
|
|
112
|
-
function readToken(): string | null {
|
|
113
|
-
try {
|
|
114
|
-
const tokenPath = getTokenPath();
|
|
115
|
-
if (!pathExists(tokenPath)) return null;
|
|
116
|
-
return readFileSync(tokenPath, "utf-8").trim();
|
|
117
|
-
} catch {
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function getToken(): string | null {
|
|
123
|
-
if (cachedToken !== undefined) return cachedToken;
|
|
124
|
-
const token = readToken();
|
|
125
|
-
// Only cache non-null results so we re-attempt on next call if the
|
|
126
|
-
// token file hasn't appeared yet (startup race).
|
|
127
|
-
if (token) cachedToken = token;
|
|
128
|
-
return token;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Re-read the token from disk (handles app restart with new token). */
|
|
132
|
-
function refreshToken(): string | null {
|
|
133
|
-
const token = readToken();
|
|
134
|
-
// Update the cache: set to the new value if found, clear if not so
|
|
135
|
-
// subsequent getToken() calls will re-read from disk.
|
|
136
|
-
cachedToken = token ?? undefined;
|
|
137
|
-
return token;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// -------------------------------------------------------------------------
|
|
141
|
-
// Socket lifecycle
|
|
142
|
-
// -------------------------------------------------------------------------
|
|
143
|
-
|
|
144
|
-
function handleData(chunk: Buffer | string): void {
|
|
145
|
-
inBuffer += chunk.toString();
|
|
146
|
-
let newlineIdx: number;
|
|
147
|
-
while ((newlineIdx = inBuffer.indexOf("\n")) !== -1) {
|
|
148
|
-
const line = inBuffer.slice(0, newlineIdx).trim();
|
|
149
|
-
inBuffer = inBuffer.slice(newlineIdx + 1);
|
|
150
|
-
if (!line) continue;
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
const response = JSON.parse(line) as BrokerResponse;
|
|
154
|
-
const entry = pending.get(response.id);
|
|
155
|
-
if (entry) {
|
|
156
|
-
clearTimeout(entry.timer);
|
|
157
|
-
pending.delete(response.id);
|
|
158
|
-
entry.resolve(response);
|
|
159
|
-
}
|
|
160
|
-
} catch {
|
|
161
|
-
log.warn("Received malformed JSON from keychain broker");
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function cleanupSocket(): void {
|
|
167
|
-
if (socket) {
|
|
168
|
-
socket.removeAllListeners();
|
|
169
|
-
socket.destroy();
|
|
170
|
-
socket = null;
|
|
171
|
-
}
|
|
172
|
-
inBuffer = "";
|
|
173
|
-
// Reject all pending requests
|
|
174
|
-
for (const [id, entry] of pending) {
|
|
175
|
-
clearTimeout(entry.timer);
|
|
176
|
-
entry.resolve({
|
|
177
|
-
id,
|
|
178
|
-
ok: false,
|
|
179
|
-
error: { code: "DISCONNECTED", message: "disconnected" },
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
pending.clear();
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function connect(): Promise<Socket> {
|
|
186
|
-
return new Promise((resolve, reject) => {
|
|
187
|
-
const socketPath = getSocketPath();
|
|
188
|
-
if (!pathExists(socketPath)) {
|
|
189
|
-
reject(new Error("No socket path"));
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const sock = createConnection({ path: socketPath });
|
|
194
|
-
|
|
195
|
-
const connectTimer = setTimeout(() => {
|
|
196
|
-
sock.destroy();
|
|
197
|
-
reject(new Error("Connect timeout"));
|
|
198
|
-
}, CONNECT_TIMEOUT_MS);
|
|
199
|
-
|
|
200
|
-
sock.on("connect", () => {
|
|
201
|
-
clearTimeout(connectTimer);
|
|
202
|
-
socket = sock;
|
|
203
|
-
consecutiveFailures = 0;
|
|
204
|
-
unavailableSince = null;
|
|
205
|
-
resolve(sock);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
sock.on("error", (err) => {
|
|
209
|
-
clearTimeout(connectTimer);
|
|
210
|
-
log.warn({ err }, "Keychain broker socket error");
|
|
211
|
-
cleanupSocket();
|
|
212
|
-
reject(err);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
sock.on("close", () => {
|
|
216
|
-
cleanupSocket();
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
sock.on("data", handleData);
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** Compute the cooldown duration for the current failure count. The first
|
|
224
|
-
* two failures (initial + immediate retry) map to index 0 (30s). */
|
|
225
|
-
function getCooldownMs(): number {
|
|
226
|
-
const idx = Math.min(
|
|
227
|
-
Math.max(consecutiveFailures - 2, 0),
|
|
228
|
-
RECONNECT_COOLDOWN_MS.length - 1,
|
|
229
|
-
);
|
|
230
|
-
return RECONNECT_COOLDOWN_MS[idx];
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
async function ensureConnected(): Promise<Socket | null> {
|
|
234
|
-
// If in cooldown, check whether the cooldown period has elapsed.
|
|
235
|
-
if (unavailableSince != null) {
|
|
236
|
-
if (Date.now() - unavailableSince < getCooldownMs()) {
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
// Cooldown elapsed — clear and attempt reconnection below.
|
|
240
|
-
unavailableSince = null;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (socket && !socket.destroyed) return socket;
|
|
244
|
-
|
|
245
|
-
// If a connect() is already in flight, wait for it instead of returning
|
|
246
|
-
// null — this prevents concurrent callers from silently failing.
|
|
247
|
-
if (connectPromise) {
|
|
248
|
-
try {
|
|
249
|
-
return await connectPromise;
|
|
250
|
-
} catch {
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
connectPromise = connect();
|
|
256
|
-
try {
|
|
257
|
-
const sock = await connectPromise;
|
|
258
|
-
return sock;
|
|
259
|
-
} catch {
|
|
260
|
-
consecutiveFailures++;
|
|
261
|
-
|
|
262
|
-
// First failure triggers one immediate retry (preserves the original
|
|
263
|
-
// "try once more" behavior).
|
|
264
|
-
if (consecutiveFailures === 1) {
|
|
265
|
-
connectPromise = connect();
|
|
266
|
-
try {
|
|
267
|
-
return await connectPromise;
|
|
268
|
-
} catch {
|
|
269
|
-
consecutiveFailures++;
|
|
270
|
-
unavailableSince = Date.now();
|
|
271
|
-
log.warn(
|
|
272
|
-
`Keychain broker reconnect failed, will retry in ${getCooldownMs() / 1000}s`,
|
|
273
|
-
);
|
|
274
|
-
return null;
|
|
275
|
-
} finally {
|
|
276
|
-
connectPromise = null;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Subsequent failures — enter cooldown.
|
|
281
|
-
unavailableSince = Date.now();
|
|
282
|
-
log.warn(
|
|
283
|
-
`Keychain broker connection failed (attempt ${consecutiveFailures}), will retry in ${getCooldownMs() / 1000}s`,
|
|
284
|
-
);
|
|
285
|
-
return null;
|
|
286
|
-
} finally {
|
|
287
|
-
connectPromise = null;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// -------------------------------------------------------------------------
|
|
292
|
-
// Request / response
|
|
293
|
-
// -------------------------------------------------------------------------
|
|
294
|
-
|
|
295
|
-
function sendRequest(request: BrokerRequest): Promise<BrokerResponse> {
|
|
296
|
-
return new Promise((resolve) => {
|
|
297
|
-
if (!socket || socket.destroyed) {
|
|
298
|
-
resolve({
|
|
299
|
-
id: request.id,
|
|
300
|
-
ok: false,
|
|
301
|
-
error: { code: "NOT_CONNECTED", message: "not connected" },
|
|
302
|
-
});
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const timer = setTimeout(() => {
|
|
307
|
-
pending.delete(request.id);
|
|
308
|
-
resolve({
|
|
309
|
-
id: request.id,
|
|
310
|
-
ok: false,
|
|
311
|
-
error: { code: "TIMEOUT", message: "timeout" },
|
|
312
|
-
});
|
|
313
|
-
}, REQUEST_TIMEOUT_MS);
|
|
314
|
-
|
|
315
|
-
pending.set(request.id, { resolve, timer });
|
|
316
|
-
|
|
317
|
-
const data = JSON.stringify(request) + "\n";
|
|
318
|
-
socket.write(data, (err) => {
|
|
319
|
-
if (err) {
|
|
320
|
-
clearTimeout(timer);
|
|
321
|
-
pending.delete(request.id);
|
|
322
|
-
resolve({
|
|
323
|
-
id: request.id,
|
|
324
|
-
ok: false,
|
|
325
|
-
error: { code: "WRITE_ERROR", message: "write error" },
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
async function doRequest(
|
|
333
|
-
method: string,
|
|
334
|
-
params: Record<string, unknown> = {},
|
|
335
|
-
): Promise<BrokerResponse | null> {
|
|
336
|
-
const sock = await ensureConnected();
|
|
337
|
-
if (!sock) return null;
|
|
338
|
-
|
|
339
|
-
const token = getToken();
|
|
340
|
-
if (!token) return null;
|
|
341
|
-
|
|
342
|
-
const id = randomUUID();
|
|
343
|
-
const request: BrokerRequest = {
|
|
344
|
-
v: 1,
|
|
345
|
-
id,
|
|
346
|
-
method,
|
|
347
|
-
token,
|
|
348
|
-
...(Object.keys(params).length > 0 ? { params } : {}),
|
|
349
|
-
};
|
|
350
|
-
const response = await sendRequest(request);
|
|
351
|
-
|
|
352
|
-
// On UNAUTHORIZED, re-read the token once and retry. This handles
|
|
353
|
-
// the case where the app restarted with a new token while the daemon
|
|
354
|
-
// is still running with the old cached one.
|
|
355
|
-
if (response.error?.code === "UNAUTHORIZED") {
|
|
356
|
-
const newToken = refreshToken();
|
|
357
|
-
if (!newToken || newToken === request.token) return response;
|
|
358
|
-
|
|
359
|
-
const retryRequest: BrokerRequest = {
|
|
360
|
-
...request,
|
|
361
|
-
id: randomUUID(),
|
|
362
|
-
token: newToken,
|
|
363
|
-
};
|
|
364
|
-
return await sendRequest(retryRequest);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return response;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// -------------------------------------------------------------------------
|
|
371
|
-
// Public API
|
|
372
|
-
// -------------------------------------------------------------------------
|
|
373
|
-
|
|
374
|
-
return {
|
|
375
|
-
isAvailable(): boolean {
|
|
376
|
-
if (unavailableSince != null) {
|
|
377
|
-
if (Date.now() - unavailableSince < getCooldownMs()) return false;
|
|
378
|
-
}
|
|
379
|
-
if (!pathExists(getSocketPath())) return false;
|
|
380
|
-
return pathExists(getTokenPath());
|
|
381
|
-
},
|
|
382
|
-
|
|
383
|
-
async ping(): Promise<{ pong: boolean } | null> {
|
|
384
|
-
try {
|
|
385
|
-
const response = await doRequest("broker.ping");
|
|
386
|
-
if (!response || !response.ok) return null;
|
|
387
|
-
return {
|
|
388
|
-
pong: !!(response.result as Record<string, unknown> | undefined)
|
|
389
|
-
?.pong,
|
|
390
|
-
};
|
|
391
|
-
} catch {
|
|
392
|
-
return null;
|
|
393
|
-
}
|
|
394
|
-
},
|
|
395
|
-
|
|
396
|
-
async get(account: string): Promise<BrokerGetResult> {
|
|
397
|
-
try {
|
|
398
|
-
const response = await doRequest("key.get", { account });
|
|
399
|
-
if (!response) return null;
|
|
400
|
-
if (!response.ok) return null;
|
|
401
|
-
const result = response.result as
|
|
402
|
-
| { found?: boolean; value?: string }
|
|
403
|
-
| undefined;
|
|
404
|
-
if (!result) return null;
|
|
405
|
-
return { found: !!result.found, value: result.value };
|
|
406
|
-
} catch {
|
|
407
|
-
return null;
|
|
408
|
-
}
|
|
409
|
-
},
|
|
410
|
-
|
|
411
|
-
async set(account: string, value: string): Promise<BrokerSetResult> {
|
|
412
|
-
try {
|
|
413
|
-
const response = await doRequest("key.set", { account, value });
|
|
414
|
-
if (!response) return { status: "unreachable" };
|
|
415
|
-
if (response.ok) return { status: "ok" };
|
|
416
|
-
return {
|
|
417
|
-
status: "rejected",
|
|
418
|
-
code: response.error?.code ?? "UNKNOWN",
|
|
419
|
-
message: response.error?.message ?? "unknown error",
|
|
420
|
-
};
|
|
421
|
-
} catch {
|
|
422
|
-
return { status: "unreachable" };
|
|
423
|
-
}
|
|
424
|
-
},
|
|
425
|
-
|
|
426
|
-
async del(account: string): Promise<boolean> {
|
|
427
|
-
try {
|
|
428
|
-
const response = await doRequest("key.delete", { account });
|
|
429
|
-
return response?.ok === true;
|
|
430
|
-
} catch {
|
|
431
|
-
return false;
|
|
432
|
-
}
|
|
433
|
-
},
|
|
434
|
-
|
|
435
|
-
async list(): Promise<string[]> {
|
|
436
|
-
try {
|
|
437
|
-
const response = await doRequest("key.list");
|
|
438
|
-
if (!response || !response.ok) return [];
|
|
439
|
-
const result = response.result as { accounts?: string[] } | undefined;
|
|
440
|
-
return result?.accounts ?? [];
|
|
441
|
-
} catch {
|
|
442
|
-
return [];
|
|
443
|
-
}
|
|
444
|
-
},
|
|
445
|
-
};
|
|
446
|
-
}
|