@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.
Files changed (278) hide show
  1. package/AGENTS.md +9 -1
  2. package/ARCHITECTURE.md +48 -48
  3. package/Dockerfile +2 -0
  4. package/README.md +1 -1
  5. package/docs/architecture/integrations.md +6 -13
  6. package/docs/architecture/memory.md +7 -12
  7. package/docs/architecture/security.md +5 -5
  8. package/docs/credential-execution-service.md +9 -9
  9. package/docs/skills.md +1 -1
  10. package/node_modules/@vellumai/credential-storage/src/index.ts +2 -2
  11. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +1 -1
  12. package/openapi.yaml +7130 -0
  13. package/package.json +2 -1
  14. package/scripts/generate-openapi.ts +562 -0
  15. package/src/__tests__/acp-session.test.ts +239 -44
  16. package/src/__tests__/assistant-feature-flag-guard.test.ts +8 -8
  17. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +5 -86
  18. package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -14
  19. package/src/__tests__/browser-skill-endstate.test.ts +1 -1
  20. package/src/__tests__/btw-routes.test.ts +8 -0
  21. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +10 -10
  22. package/src/__tests__/channel-approvals.test.ts +7 -7
  23. package/src/__tests__/channel-readiness-service.test.ts +41 -0
  24. package/src/__tests__/config-schema.test.ts +10 -2
  25. package/src/__tests__/context-memory-e2e.test.ts +2 -6
  26. package/src/__tests__/conversation-skill-tools.test.ts +1 -3
  27. package/src/__tests__/conversation-title-service.test.ts +2 -15
  28. package/src/__tests__/credential-execution-feature-gates.test.ts +4 -8
  29. package/src/__tests__/credential-execution-managed-contract.test.ts +8 -8
  30. package/src/__tests__/credential-security-e2e.test.ts +4 -4
  31. package/src/__tests__/credential-security-invariants.test.ts +3 -3
  32. package/src/__tests__/credentials-cli.test.ts +3 -3
  33. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -1
  34. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  35. package/src/__tests__/heartbeat-service.test.ts +35 -0
  36. package/src/__tests__/host-shell-tool.test.ts +1 -1
  37. package/src/__tests__/inline-skill-load-permissions.test.ts +3 -3
  38. package/src/__tests__/llm-request-log-turn-query.test.ts +64 -0
  39. package/src/__tests__/log-export-workspace.test.ts +1 -1
  40. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  41. package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
  42. package/src/__tests__/memory-recall-log-store.test.ts +182 -0
  43. package/src/__tests__/memory-recall-quality.test.ts +6 -8
  44. package/src/__tests__/memory-regressions.test.ts +53 -42
  45. package/src/__tests__/memory-retrieval.benchmark.test.ts +5 -9
  46. package/src/__tests__/messaging-skill-split.test.ts +2 -17
  47. package/src/__tests__/oauth-cli.test.ts +98 -551
  48. package/src/__tests__/platform-callback-registration.test.ts +119 -0
  49. package/src/__tests__/secret-ingress-channel.test.ts +261 -0
  50. package/src/__tests__/secret-ingress-cli.test.ts +201 -0
  51. package/src/__tests__/secret-ingress-http.test.ts +312 -0
  52. package/src/__tests__/secret-ingress.test.ts +283 -0
  53. package/src/__tests__/secret-onetime-send.test.ts +4 -4
  54. package/src/__tests__/skill-feature-flags-integration.test.ts +4 -4
  55. package/src/__tests__/skill-feature-flags.test.ts +11 -19
  56. package/src/__tests__/skill-load-feature-flag.test.ts +1 -1
  57. package/src/__tests__/skill-load-inline-command.test.ts +3 -3
  58. package/src/__tests__/skill-load-inline-includes.test.ts +2 -2
  59. package/src/__tests__/skill-memory.test.ts +2 -4
  60. package/src/__tests__/skill-projection-feature-flag.test.ts +2 -4
  61. package/src/__tests__/skill-projection.benchmark.test.ts +1 -3
  62. package/src/__tests__/skills.test.ts +16 -2
  63. package/src/__tests__/slack-channel-config.test.ts +1 -1
  64. package/src/__tests__/slack-skill.test.ts +5 -69
  65. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -1
  66. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +5 -238
  67. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -206
  68. package/src/__tests__/workspace-migration-018-rekey-compound-credential-keys.test.ts +181 -0
  69. package/src/__tests__/workspace-migrations-runner.test.ts +15 -7
  70. package/src/acp/client-handler.ts +113 -31
  71. package/src/acp/session-manager.ts +29 -27
  72. package/src/approvals/guardian-request-resolvers.ts +1 -1
  73. package/src/cli/AGENTS.md +73 -0
  74. package/src/cli/commands/autonomy.ts +3 -5
  75. package/src/cli/commands/credential-execution.ts +1 -2
  76. package/src/cli/commands/credentials.ts +4 -4
  77. package/src/cli/commands/memory.ts +2 -3
  78. package/src/cli/commands/oauth/__tests__/connect.test.ts +785 -0
  79. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +760 -0
  80. package/src/cli/commands/oauth/__tests__/mode.test.ts +672 -0
  81. package/src/cli/commands/oauth/__tests__/ping.test.ts +690 -0
  82. package/src/cli/commands/oauth/__tests__/status.test.ts +579 -0
  83. package/src/cli/commands/oauth/__tests__/token.test.ts +467 -0
  84. package/src/cli/commands/oauth/apps.ts +29 -11
  85. package/src/cli/commands/oauth/connect.ts +373 -0
  86. package/src/cli/commands/oauth/connections.ts +14 -493
  87. package/src/cli/commands/oauth/disconnect.ts +333 -0
  88. package/src/cli/commands/oauth/index.ts +62 -10
  89. package/src/cli/commands/oauth/mode.ts +263 -0
  90. package/src/cli/commands/oauth/ping.ts +222 -0
  91. package/src/cli/commands/oauth/providers.ts +30 -3
  92. package/src/cli/commands/oauth/request.ts +576 -0
  93. package/src/cli/commands/oauth/shared.ts +132 -0
  94. package/src/cli/commands/oauth/status.ts +202 -0
  95. package/src/cli/commands/oauth/token.ts +159 -0
  96. package/src/cli/commands/platform.ts +20 -14
  97. package/src/cli.ts +82 -17
  98. package/src/config/assistant-feature-flags.ts +74 -11
  99. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  100. package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -1
  101. package/src/config/bundled-skills/messaging/SKILL.md +13 -36
  102. package/src/config/bundled-skills/messaging/TOOLS.json +9 -9
  103. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +1 -1
  104. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  105. package/src/config/bundled-skills/schedule/SKILL.md +2 -2
  106. package/src/config/bundled-skills/settings/SKILL.md +5 -3
  107. package/src/config/bundled-skills/settings/TOOLS.json +17 -0
  108. package/src/config/bundled-skills/settings/tools/avatar-get.ts +50 -0
  109. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +7 -0
  110. package/src/config/bundled-skills/settings/tools/avatar-update.ts +6 -1
  111. package/src/config/bundled-skills/settings/tools/identity-avatar.ts +55 -0
  112. package/src/config/bundled-skills/skills-catalog/SKILL.md +3 -3
  113. package/src/config/bundled-skills/slack/SKILL.md +58 -44
  114. package/src/config/bundled-tool-registry.ts +2 -19
  115. package/src/config/env.ts +5 -1
  116. package/src/config/feature-flag-registry.json +57 -41
  117. package/src/config/loader.ts +4 -0
  118. package/src/config/schemas/platform.ts +0 -8
  119. package/src/config/schemas/security.ts +9 -1
  120. package/src/config/schemas/services.ts +1 -1
  121. package/src/config/skill-state.ts +1 -3
  122. package/src/config/skills.ts +2 -4
  123. package/src/credential-execution/feature-gates.ts +9 -16
  124. package/src/credential-execution/process-manager.ts +12 -0
  125. package/src/daemon/config-watcher.ts +4 -0
  126. package/src/daemon/conversation-agent-loop-handlers.ts +10 -0
  127. package/src/daemon/conversation-agent-loop.ts +49 -2
  128. package/src/daemon/conversation-memory.ts +0 -1
  129. package/src/daemon/handlers/config-slack-channel.ts +43 -1
  130. package/src/daemon/handlers/conversations.ts +41 -33
  131. package/src/daemon/lifecycle.ts +28 -5
  132. package/src/daemon/message-types/acp.ts +0 -15
  133. package/src/daemon/message-types/memory.ts +0 -1
  134. package/src/daemon/message-types/messages.ts +9 -1
  135. package/src/daemon/message-types/schedules.ts +9 -0
  136. package/src/daemon/server.ts +19 -7
  137. package/src/email/feature-gate.ts +3 -3
  138. package/src/heartbeat/heartbeat-service.ts +48 -0
  139. package/src/inbound/platform-callback-registration.ts +61 -7
  140. package/src/mcp/mcp-oauth-provider.ts +3 -3
  141. package/src/memory/app-store.ts +3 -3
  142. package/src/memory/conversation-crud.ts +124 -0
  143. package/src/memory/conversation-title-service.ts +7 -17
  144. package/src/memory/db-init.ts +8 -0
  145. package/src/memory/embedding-local.ts +47 -2
  146. package/src/memory/indexer.ts +13 -10
  147. package/src/memory/items-extractor.ts +12 -4
  148. package/src/memory/job-utils.ts +5 -0
  149. package/src/memory/jobs-store.ts +10 -2
  150. package/src/memory/journal-memory.ts +6 -2
  151. package/src/memory/llm-request-log-store.ts +88 -21
  152. package/src/memory/memory-recall-log-store.ts +128 -0
  153. package/src/memory/migrations/194-memory-recall-logs.ts +50 -0
  154. package/src/memory/migrations/195-oauth-providers-ping-config.ts +23 -0
  155. package/src/memory/migrations/index.ts +2 -0
  156. package/src/memory/migrations/validate-migration-state.ts +14 -1
  157. package/src/memory/retriever.test.ts +4 -5
  158. package/src/memory/schema/infrastructure.ts +31 -0
  159. package/src/memory/schema/oauth.ts +3 -0
  160. package/src/messaging/providers/telegram-bot/adapter.ts +1 -1
  161. package/src/oauth/connect-orchestrator.ts +54 -0
  162. package/src/oauth/manual-token-connection.ts +5 -5
  163. package/src/oauth/oauth-store.ts +26 -5
  164. package/src/oauth/seed-providers.ts +10 -1
  165. package/src/permissions/checker.ts +2 -2
  166. package/src/permissions/trust-client.ts +2 -2
  167. package/src/platform/client.ts +2 -2
  168. package/src/prompts/journal-context.ts +6 -1
  169. package/src/providers/anthropic/client.ts +143 -1
  170. package/src/runtime/auth/__tests__/middleware.test.ts +19 -0
  171. package/src/runtime/auth/route-policy.ts +0 -1
  172. package/src/runtime/btw-sidechain.ts +7 -1
  173. package/src/runtime/channel-approvals.ts +2 -2
  174. package/src/runtime/channel-readiness-service.ts +30 -7
  175. package/src/runtime/http-router.ts +31 -0
  176. package/src/runtime/http-server.ts +21 -4
  177. package/src/runtime/http-types.ts +2 -0
  178. package/src/runtime/pending-interactions.ts +21 -3
  179. package/src/runtime/routes/acp-routes.ts +46 -28
  180. package/src/runtime/routes/app-management-routes.ts +123 -0
  181. package/src/runtime/routes/app-routes.ts +31 -0
  182. package/src/runtime/routes/approval-routes.ts +108 -3
  183. package/src/runtime/routes/attachment-routes.ts +45 -0
  184. package/src/runtime/routes/avatar-routes.ts +16 -0
  185. package/src/runtime/routes/brain-graph-routes.ts +18 -0
  186. package/src/runtime/routes/btw-routes.ts +20 -0
  187. package/src/runtime/routes/call-routes.ts +81 -0
  188. package/src/runtime/routes/channel-readiness-routes.ts +48 -7
  189. package/src/runtime/routes/channel-routes.ts +18 -0
  190. package/src/runtime/routes/channel-verification-routes.ts +49 -1
  191. package/src/runtime/routes/contact-routes.ts +77 -0
  192. package/src/runtime/routes/conversation-attention-routes.ts +37 -0
  193. package/src/runtime/routes/conversation-management-routes.ts +94 -0
  194. package/src/runtime/routes/conversation-query-routes.ts +78 -0
  195. package/src/runtime/routes/conversation-routes.ts +115 -38
  196. package/src/runtime/routes/conversation-starter-routes.ts +29 -0
  197. package/src/runtime/routes/debug-routes.ts +23 -0
  198. package/src/runtime/routes/diagnostics-routes.ts +30 -0
  199. package/src/runtime/routes/documents-routes.ts +42 -0
  200. package/src/runtime/routes/events-routes.ts +10 -0
  201. package/src/runtime/routes/global-search-routes.ts +35 -0
  202. package/src/runtime/routes/guardian-action-routes.ts +47 -2
  203. package/src/runtime/routes/guardian-approval-prompt.ts +77 -2
  204. package/src/runtime/routes/heartbeat-routes.ts +278 -0
  205. package/src/runtime/routes/host-bash-routes.ts +16 -1
  206. package/src/runtime/routes/host-cu-routes.ts +23 -1
  207. package/src/runtime/routes/host-file-routes.ts +18 -1
  208. package/src/runtime/routes/identity-routes.ts +35 -0
  209. package/src/runtime/routes/inbound-message-handler.ts +46 -25
  210. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +30 -2
  211. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -2
  212. package/src/runtime/routes/integrations/twilio.ts +32 -22
  213. package/src/runtime/routes/invite-routes.ts +83 -0
  214. package/src/runtime/routes/log-export-routes.ts +14 -0
  215. package/src/runtime/routes/memory-item-routes.ts +99 -1
  216. package/src/runtime/routes/migration-rollback-routes.ts +25 -0
  217. package/src/runtime/routes/migration-routes.ts +40 -0
  218. package/src/runtime/routes/notification-routes.ts +20 -0
  219. package/src/runtime/routes/oauth-apps.ts +11 -3
  220. package/src/runtime/routes/pairing-routes.ts +15 -0
  221. package/src/runtime/routes/recording-routes.ts +72 -0
  222. package/src/runtime/routes/schedule-routes.ts +77 -5
  223. package/src/runtime/routes/secret-routes.ts +63 -1
  224. package/src/runtime/routes/settings-routes.ts +91 -1
  225. package/src/runtime/routes/skills-routes.ts +98 -16
  226. package/src/runtime/routes/subagents-routes.ts +38 -3
  227. package/src/runtime/routes/surface-action-routes.ts +66 -24
  228. package/src/runtime/routes/surface-content-routes.ts +20 -0
  229. package/src/runtime/routes/telemetry-routes.ts +12 -0
  230. package/src/runtime/routes/trace-event-routes.ts +25 -0
  231. package/src/runtime/routes/trust-rules-routes.ts +46 -0
  232. package/src/runtime/routes/tts-routes.ts +15 -4
  233. package/src/runtime/routes/upgrade-broadcast-routes.ts +38 -0
  234. package/src/runtime/routes/usage-routes.ts +59 -0
  235. package/src/runtime/routes/watch-routes.ts +28 -0
  236. package/src/runtime/routes/work-items-routes.ts +59 -0
  237. package/src/runtime/routes/workspace-commit-routes.ts +12 -0
  238. package/src/runtime/routes/workspace-routes.ts +102 -0
  239. package/src/schedule/scheduler.ts +7 -1
  240. package/src/security/AGENTS.md +7 -0
  241. package/src/security/credential-backend.ts +1 -1
  242. package/src/security/encrypted-store.ts +3 -3
  243. package/src/security/oauth2.ts +55 -0
  244. package/src/security/secret-ingress.ts +174 -0
  245. package/src/security/secret-patterns.ts +133 -0
  246. package/src/security/secret-scanner.ts +28 -117
  247. package/src/signals/confirm.ts +12 -8
  248. package/src/signals/user-message.ts +18 -3
  249. package/src/skills/skill-memory.ts +1 -2
  250. package/src/tasks/task-runner.ts +7 -1
  251. package/src/tools/credentials/broker.ts +1 -1
  252. package/src/tools/credentials/metadata-store.ts +1 -1
  253. package/src/tools/credentials/vault.ts +2 -3
  254. package/src/tools/memory/definitions.ts +1 -1
  255. package/src/tools/memory/handlers.test.ts +2 -4
  256. package/src/tools/skills/load.ts +1 -1
  257. package/src/tools/terminal/safe-env.ts +7 -0
  258. package/src/tools/tool-manifest.ts +1 -1
  259. package/src/util/log-redact.ts +9 -34
  260. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +13 -148
  261. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +7 -145
  262. package/src/workspace/migrations/AGENTS.md +11 -0
  263. package/src/workspace/migrations/runner.ts +16 -6
  264. package/src/workspace/migrations/types.ts +7 -0
  265. package/docs/architecture/keychain-broker.md +0 -69
  266. package/src/__tests__/keychain-broker-client.test.ts +0 -800
  267. package/src/cli/commands/oauth/platform.ts +0 -525
  268. package/src/config/bundled-skills/slack/TOOLS.json +0 -272
  269. package/src/config/bundled-skills/slack/tools/shared.ts +0 -34
  270. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +0 -27
  271. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +0 -38
  272. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +0 -146
  273. package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +0 -105
  274. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +0 -26
  275. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +0 -27
  276. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +0 -25
  277. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +0 -372
  278. 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
- }