@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
|
@@ -315,7 +315,7 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
|
|
|
315
315
|
},
|
|
316
316
|
open_bundle: {
|
|
317
317
|
type: "open_bundle",
|
|
318
|
-
filePath: "/tmp/My_App.
|
|
318
|
+
filePath: "/tmp/My_App.vellum",
|
|
319
319
|
},
|
|
320
320
|
sign_bundle_payload_response: {
|
|
321
321
|
type: "sign_bundle_payload_response",
|
|
@@ -1041,6 +1041,12 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
|
|
|
1041
1041
|
sessionId: "sess-routed-001",
|
|
1042
1042
|
interactionType: "computer_use",
|
|
1043
1043
|
},
|
|
1044
|
+
ride_shotgun_error: {
|
|
1045
|
+
type: "ride_shotgun_error",
|
|
1046
|
+
watchId: "watch-shotgun-001",
|
|
1047
|
+
sessionId: "sess-shotgun-001",
|
|
1048
|
+
message: "Failed to start browser — Chrome CDP could not be launched.",
|
|
1049
|
+
},
|
|
1044
1050
|
ride_shotgun_progress: {
|
|
1045
1051
|
type: "ride_shotgun_progress",
|
|
1046
1052
|
watchId: "watch-shotgun-001",
|
|
@@ -1266,7 +1272,7 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
|
|
|
1266
1272
|
},
|
|
1267
1273
|
bundle_app_response: {
|
|
1268
1274
|
type: "bundle_app_response",
|
|
1269
|
-
bundlePath: "/tmp/My_App-abc12345.
|
|
1275
|
+
bundlePath: "/tmp/My_App-abc12345.vellum",
|
|
1270
1276
|
manifest: {
|
|
1271
1277
|
format_version: 1,
|
|
1272
1278
|
name: "My App",
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import type { Server } from "node:net";
|
|
4
|
+
import { createServer } from "node:net";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import {
|
|
8
|
+
afterAll,
|
|
9
|
+
afterEach,
|
|
10
|
+
beforeAll,
|
|
11
|
+
beforeEach,
|
|
12
|
+
describe,
|
|
13
|
+
expect,
|
|
14
|
+
mock,
|
|
15
|
+
test,
|
|
16
|
+
} from "bun:test";
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Mock logger to silence output
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
mock.module("../util/logger.js", () => ({
|
|
23
|
+
getLogger: () =>
|
|
24
|
+
new Proxy({} as Record<string, unknown>, {
|
|
25
|
+
get: () => () => {},
|
|
26
|
+
}),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Test fixtures
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
const TEST_DIR = join(
|
|
34
|
+
tmpdir(),
|
|
35
|
+
`vellum-broker-test-${randomBytes(4).toString("hex")}`,
|
|
36
|
+
);
|
|
37
|
+
const TOKEN_DIR = join(TEST_DIR, ".vellum", "protected");
|
|
38
|
+
const TOKEN_PATH = join(TOKEN_DIR, "keychain-broker.token");
|
|
39
|
+
const SOCKET_PATH = join(TEST_DIR, "broker.sock");
|
|
40
|
+
const TEST_TOKEN = "test-auth-token-abc123";
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Helpers
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create a mock UDS server that speaks the broker protocol.
|
|
48
|
+
* Returns the server and a handler setter for customizing responses.
|
|
49
|
+
*/
|
|
50
|
+
function createMockBroker(): {
|
|
51
|
+
server: Server;
|
|
52
|
+
setHandler: (
|
|
53
|
+
fn: (request: Record<string, unknown>) => Record<string, unknown>,
|
|
54
|
+
) => void;
|
|
55
|
+
start: () => Promise<void>;
|
|
56
|
+
stop: () => Promise<void>;
|
|
57
|
+
} {
|
|
58
|
+
let handler: (
|
|
59
|
+
request: Record<string, unknown>,
|
|
60
|
+
) => Record<string, unknown> = () => ({ ok: true });
|
|
61
|
+
|
|
62
|
+
const connections = new Set<import("node:net").Socket>();
|
|
63
|
+
|
|
64
|
+
const server = createServer((conn) => {
|
|
65
|
+
connections.add(conn);
|
|
66
|
+
conn.on("close", () => connections.delete(conn));
|
|
67
|
+
let buffer = "";
|
|
68
|
+
conn.on("data", (chunk) => {
|
|
69
|
+
buffer += chunk.toString();
|
|
70
|
+
let idx: number;
|
|
71
|
+
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
72
|
+
const line = buffer.slice(0, idx).trim();
|
|
73
|
+
buffer = buffer.slice(idx + 1);
|
|
74
|
+
if (!line) continue;
|
|
75
|
+
try {
|
|
76
|
+
const request = JSON.parse(line);
|
|
77
|
+
const response = handler(request);
|
|
78
|
+
conn.write(JSON.stringify({ id: request.id, ...response }) + "\n");
|
|
79
|
+
} catch {
|
|
80
|
+
// Malformed request — ignore
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
server,
|
|
88
|
+
setHandler: (fn) => {
|
|
89
|
+
handler = fn;
|
|
90
|
+
},
|
|
91
|
+
start: () =>
|
|
92
|
+
new Promise<void>((resolve) => {
|
|
93
|
+
server.listen(SOCKET_PATH, () => resolve());
|
|
94
|
+
}),
|
|
95
|
+
stop: () =>
|
|
96
|
+
new Promise<void>((resolve) => {
|
|
97
|
+
// Destroy active connections so server.close() can complete
|
|
98
|
+
for (const conn of connections) conn.destroy();
|
|
99
|
+
connections.clear();
|
|
100
|
+
server.close(() => resolve());
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Setup / teardown
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
let originalEnv: string | undefined;
|
|
110
|
+
|
|
111
|
+
beforeAll(() => {
|
|
112
|
+
mkdirSync(TOKEN_DIR, { recursive: true });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
beforeEach(() => {
|
|
116
|
+
originalEnv = process.env.VELLUM_KEYCHAIN_BROKER_SOCKET;
|
|
117
|
+
// Clean up socket file from prior test
|
|
118
|
+
try {
|
|
119
|
+
rmSync(SOCKET_PATH, { force: true });
|
|
120
|
+
} catch {
|
|
121
|
+
/* ignore */
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
afterEach(() => {
|
|
126
|
+
if (originalEnv === undefined) {
|
|
127
|
+
delete process.env.VELLUM_KEYCHAIN_BROKER_SOCKET;
|
|
128
|
+
} else {
|
|
129
|
+
process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = originalEnv;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
afterAll(() => {
|
|
134
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Mock platform to point getRootDir at our test directory
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
mock.module("../util/platform.js", () => ({
|
|
142
|
+
getRootDir: () => join(TEST_DIR, ".vellum"),
|
|
143
|
+
isMacOS: () => true,
|
|
144
|
+
getPlatformName: () => "darwin",
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
// Import after mocks are set up
|
|
148
|
+
const { createBrokerClient } =
|
|
149
|
+
await import("../security/keychain-broker-client.js");
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Tests
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
describe("keychain-broker-client", () => {
|
|
156
|
+
// -----------------------------------------------------------------------
|
|
157
|
+
// isAvailable()
|
|
158
|
+
// -----------------------------------------------------------------------
|
|
159
|
+
describe("isAvailable", () => {
|
|
160
|
+
test("returns false when env var is unset", () => {
|
|
161
|
+
delete process.env.VELLUM_KEYCHAIN_BROKER_SOCKET;
|
|
162
|
+
writeFileSync(TOKEN_PATH, TEST_TOKEN);
|
|
163
|
+
const client = createBrokerClient();
|
|
164
|
+
expect(client.isAvailable()).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("returns false when token file does not exist", () => {
|
|
168
|
+
process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
|
|
169
|
+
try {
|
|
170
|
+
rmSync(TOKEN_PATH, { force: true });
|
|
171
|
+
} catch {
|
|
172
|
+
/* ignore */
|
|
173
|
+
}
|
|
174
|
+
const client = createBrokerClient();
|
|
175
|
+
expect(client.isAvailable()).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("returns true when both env var and token file exist", () => {
|
|
179
|
+
process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
|
|
180
|
+
writeFileSync(TOKEN_PATH, TEST_TOKEN);
|
|
181
|
+
const client = createBrokerClient();
|
|
182
|
+
expect(client.isAvailable()).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// -----------------------------------------------------------------------
|
|
187
|
+
// Request/response serialization
|
|
188
|
+
// -----------------------------------------------------------------------
|
|
189
|
+
describe("request/response", () => {
|
|
190
|
+
let broker: ReturnType<typeof createMockBroker>;
|
|
191
|
+
|
|
192
|
+
beforeEach(async () => {
|
|
193
|
+
process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
|
|
194
|
+
writeFileSync(TOKEN_PATH, TEST_TOKEN);
|
|
195
|
+
broker = createMockBroker();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
afterEach(async () => {
|
|
199
|
+
await broker.stop();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("ping returns pong from broker", async () => {
|
|
203
|
+
broker.setHandler((req) => {
|
|
204
|
+
expect(req.v).toBe(1);
|
|
205
|
+
if (req.method === "broker.ping") {
|
|
206
|
+
return { ok: true, result: { pong: true } };
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
error: { code: "INVALID_REQUEST", message: "unknown method" },
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
await broker.start();
|
|
214
|
+
|
|
215
|
+
const client = createBrokerClient();
|
|
216
|
+
const result = await client.ping();
|
|
217
|
+
expect(result).toEqual({ pong: true });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("get returns found result from broker", async () => {
|
|
221
|
+
broker.setHandler((req) => {
|
|
222
|
+
expect(req.v).toBe(1);
|
|
223
|
+
const params = req.params as { account?: string } | undefined;
|
|
224
|
+
if (req.method === "key.get" && params?.account === "my-key") {
|
|
225
|
+
return { ok: true, result: { found: true, value: "secret-value" } };
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
error: { code: "INVALID_REQUEST", message: "not found" },
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
await broker.start();
|
|
233
|
+
|
|
234
|
+
const client = createBrokerClient();
|
|
235
|
+
const result = await client.get("my-key");
|
|
236
|
+
expect(result).toEqual({ found: true, value: "secret-value" });
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("get returns not-found result from broker", async () => {
|
|
240
|
+
broker.setHandler((req) => {
|
|
241
|
+
expect(req.v).toBe(1);
|
|
242
|
+
if (req.method === "key.get") {
|
|
243
|
+
return { ok: true, result: { found: false } };
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
ok: false,
|
|
247
|
+
error: { code: "INVALID_REQUEST", message: "bad" },
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
await broker.start();
|
|
251
|
+
|
|
252
|
+
const client = createBrokerClient();
|
|
253
|
+
const result = await client.get("missing-key");
|
|
254
|
+
expect(result).toEqual({ found: false, value: undefined });
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("set returns true on success", async () => {
|
|
258
|
+
broker.setHandler((req) => {
|
|
259
|
+
expect(req.v).toBe(1);
|
|
260
|
+
const params = req.params as
|
|
261
|
+
| { account?: string; value?: string }
|
|
262
|
+
| undefined;
|
|
263
|
+
if (
|
|
264
|
+
req.method === "key.set" &&
|
|
265
|
+
params?.account === "my-key" &&
|
|
266
|
+
params?.value === "new-value"
|
|
267
|
+
) {
|
|
268
|
+
return { ok: true, result: { stored: true } };
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
ok: false,
|
|
272
|
+
error: { code: "INVALID_REQUEST", message: "failed" },
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
await broker.start();
|
|
276
|
+
|
|
277
|
+
const client = createBrokerClient();
|
|
278
|
+
const result = await client.set("my-key", "new-value");
|
|
279
|
+
expect(result).toBe(true);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("del returns true on success", async () => {
|
|
283
|
+
broker.setHandler((req) => {
|
|
284
|
+
expect(req.v).toBe(1);
|
|
285
|
+
const params = req.params as { account?: string } | undefined;
|
|
286
|
+
if (req.method === "key.delete" && params?.account === "my-key") {
|
|
287
|
+
return { ok: true, result: { deleted: true } };
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
ok: false,
|
|
291
|
+
error: { code: "INVALID_REQUEST", message: "not found" },
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
await broker.start();
|
|
295
|
+
|
|
296
|
+
const client = createBrokerClient();
|
|
297
|
+
const result = await client.del("my-key");
|
|
298
|
+
expect(result).toBe(true);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("list returns account names", async () => {
|
|
302
|
+
broker.setHandler((req) => {
|
|
303
|
+
expect(req.v).toBe(1);
|
|
304
|
+
if (req.method === "key.list") {
|
|
305
|
+
return {
|
|
306
|
+
ok: true,
|
|
307
|
+
result: { accounts: ["key-a", "key-b", "key-c"] },
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
ok: false,
|
|
312
|
+
error: { code: "INVALID_REQUEST", message: "failed" },
|
|
313
|
+
};
|
|
314
|
+
});
|
|
315
|
+
await broker.start();
|
|
316
|
+
|
|
317
|
+
const client = createBrokerClient();
|
|
318
|
+
const result = await client.list();
|
|
319
|
+
expect(result).toEqual(["key-a", "key-b", "key-c"]);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("sends auth token and v:1 with every request", async () => {
|
|
323
|
+
let receivedToken: unknown;
|
|
324
|
+
let receivedVersion: unknown;
|
|
325
|
+
broker.setHandler((req) => {
|
|
326
|
+
receivedToken = req.token;
|
|
327
|
+
receivedVersion = req.v;
|
|
328
|
+
return { ok: true, result: { pong: true } };
|
|
329
|
+
});
|
|
330
|
+
await broker.start();
|
|
331
|
+
|
|
332
|
+
const client = createBrokerClient();
|
|
333
|
+
await client.ping();
|
|
334
|
+
expect(receivedToken).toBe(TEST_TOKEN);
|
|
335
|
+
expect(receivedVersion).toBe(1);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// -----------------------------------------------------------------------
|
|
340
|
+
// Timeout handling
|
|
341
|
+
// -----------------------------------------------------------------------
|
|
342
|
+
describe("timeout", () => {
|
|
343
|
+
let broker: ReturnType<typeof createMockBroker>;
|
|
344
|
+
|
|
345
|
+
beforeEach(async () => {
|
|
346
|
+
process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
|
|
347
|
+
writeFileSync(TOKEN_PATH, TEST_TOKEN);
|
|
348
|
+
broker = createMockBroker();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
afterEach(async () => {
|
|
352
|
+
await broker.stop();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("returns graceful fallback when broker does not respond within timeout", async () => {
|
|
356
|
+
// Handler that never responds
|
|
357
|
+
broker.setHandler(() => {
|
|
358
|
+
// Intentionally do not return a response — the broker mock won't send anything
|
|
359
|
+
return undefined as unknown as Record<string, unknown>;
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Override handler at the server level to swallow requests
|
|
363
|
+
broker.server.removeAllListeners("connection");
|
|
364
|
+
broker.server.on("connection", (_conn) => {
|
|
365
|
+
// Accept connection but never respond
|
|
366
|
+
});
|
|
367
|
+
await broker.start();
|
|
368
|
+
|
|
369
|
+
const client = createBrokerClient();
|
|
370
|
+
|
|
371
|
+
// get should return null on timeout (broker error)
|
|
372
|
+
const result = await client.get("test-key");
|
|
373
|
+
expect(result).toBeNull();
|
|
374
|
+
}, 10_000);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// -----------------------------------------------------------------------
|
|
378
|
+
// UNAUTHORIZED -> token re-read -> retry
|
|
379
|
+
// -----------------------------------------------------------------------
|
|
380
|
+
describe("UNAUTHORIZED retry", () => {
|
|
381
|
+
let broker: ReturnType<typeof createMockBroker>;
|
|
382
|
+
|
|
383
|
+
beforeEach(async () => {
|
|
384
|
+
process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
|
|
385
|
+
writeFileSync(TOKEN_PATH, "old-token");
|
|
386
|
+
broker = createMockBroker();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
afterEach(async () => {
|
|
390
|
+
await broker.stop();
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("re-reads token and retries on UNAUTHORIZED", async () => {
|
|
394
|
+
let callCount = 0;
|
|
395
|
+
broker.setHandler((req) => {
|
|
396
|
+
callCount++;
|
|
397
|
+
if (req.token === "new-token") {
|
|
398
|
+
return { ok: true, result: { pong: true } };
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
ok: false,
|
|
402
|
+
error: { code: "UNAUTHORIZED", message: "Invalid auth token" },
|
|
403
|
+
};
|
|
404
|
+
});
|
|
405
|
+
await broker.start();
|
|
406
|
+
|
|
407
|
+
const client = createBrokerClient();
|
|
408
|
+
|
|
409
|
+
// First call will use "old-token" and get UNAUTHORIZED.
|
|
410
|
+
// Simulate the token file being updated before the retry.
|
|
411
|
+
// We need to update it after the first request but before the retry.
|
|
412
|
+
// Since the handler runs synchronously, update the file in the handler.
|
|
413
|
+
broker.setHandler((req) => {
|
|
414
|
+
callCount++;
|
|
415
|
+
if (callCount === 1) {
|
|
416
|
+
// First request with old token — write new token file and return UNAUTHORIZED
|
|
417
|
+
writeFileSync(TOKEN_PATH, "new-token");
|
|
418
|
+
return {
|
|
419
|
+
ok: false,
|
|
420
|
+
error: { code: "UNAUTHORIZED", message: "Invalid auth token" },
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
// Retry with re-read token
|
|
424
|
+
if (req.token === "new-token") {
|
|
425
|
+
return { ok: true, result: { pong: true } };
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
ok: false,
|
|
429
|
+
error: { code: "UNAUTHORIZED", message: "Invalid auth token" },
|
|
430
|
+
};
|
|
431
|
+
});
|
|
432
|
+
callCount = 0;
|
|
433
|
+
|
|
434
|
+
const result = await client.ping();
|
|
435
|
+
expect(result).toEqual({ pong: true });
|
|
436
|
+
expect(callCount).toBe(2);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// -----------------------------------------------------------------------
|
|
441
|
+
// Graceful degradation
|
|
442
|
+
// -----------------------------------------------------------------------
|
|
443
|
+
describe("graceful degradation", () => {
|
|
444
|
+
test("get returns null when broker is not running", async () => {
|
|
445
|
+
process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
|
|
446
|
+
writeFileSync(TOKEN_PATH, TEST_TOKEN);
|
|
447
|
+
const client = createBrokerClient();
|
|
448
|
+
const result = await client.get("test-key");
|
|
449
|
+
expect(result).toBeNull();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("set returns false when broker is not running", async () => {
|
|
453
|
+
process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
|
|
454
|
+
writeFileSync(TOKEN_PATH, TEST_TOKEN);
|
|
455
|
+
const client = createBrokerClient();
|
|
456
|
+
const result = await client.set("test-key", "value");
|
|
457
|
+
expect(result).toBe(false);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("del returns false when broker is not running", async () => {
|
|
461
|
+
process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
|
|
462
|
+
writeFileSync(TOKEN_PATH, TEST_TOKEN);
|
|
463
|
+
const client = createBrokerClient();
|
|
464
|
+
const result = await client.del("test-key");
|
|
465
|
+
expect(result).toBe(false);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("list returns empty array when broker is not running", async () => {
|
|
469
|
+
process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
|
|
470
|
+
writeFileSync(TOKEN_PATH, TEST_TOKEN);
|
|
471
|
+
const client = createBrokerClient();
|
|
472
|
+
const result = await client.list();
|
|
473
|
+
expect(result).toEqual([]);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test("ping returns null when broker is not running", async () => {
|
|
477
|
+
process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
|
|
478
|
+
writeFileSync(TOKEN_PATH, TEST_TOKEN);
|
|
479
|
+
const client = createBrokerClient();
|
|
480
|
+
const result = await client.ping();
|
|
481
|
+
expect(result).toBeNull();
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("returns fallbacks when socket path env var is unset", async () => {
|
|
485
|
+
delete process.env.VELLUM_KEYCHAIN_BROKER_SOCKET;
|
|
486
|
+
writeFileSync(TOKEN_PATH, TEST_TOKEN);
|
|
487
|
+
const client = createBrokerClient();
|
|
488
|
+
expect(await client.get("key")).toBeNull();
|
|
489
|
+
expect(await client.set("key", "val")).toBe(false);
|
|
490
|
+
expect(await client.del("key")).toBe(false);
|
|
491
|
+
expect(await client.list()).toEqual([]);
|
|
492
|
+
expect(await client.ping()).toBeNull();
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("returns fallbacks when token file is missing", async () => {
|
|
496
|
+
process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
|
|
497
|
+
try {
|
|
498
|
+
rmSync(TOKEN_PATH, { force: true });
|
|
499
|
+
} catch {
|
|
500
|
+
/* ignore */
|
|
501
|
+
}
|
|
502
|
+
const client = createBrokerClient();
|
|
503
|
+
expect(await client.get("key")).toBeNull();
|
|
504
|
+
expect(await client.set("key", "val")).toBe(false);
|
|
505
|
+
expect(await client.del("key")).toBe(false);
|
|
506
|
+
expect(await client.list()).toEqual([]);
|
|
507
|
+
expect(await client.ping()).toBeNull();
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// -----------------------------------------------------------------------
|
|
512
|
+
// Connection persistence
|
|
513
|
+
// -----------------------------------------------------------------------
|
|
514
|
+
describe("connection persistence", () => {
|
|
515
|
+
let broker: ReturnType<typeof createMockBroker>;
|
|
516
|
+
|
|
517
|
+
beforeEach(async () => {
|
|
518
|
+
process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
|
|
519
|
+
writeFileSync(TOKEN_PATH, TEST_TOKEN);
|
|
520
|
+
broker = createMockBroker();
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
afterEach(async () => {
|
|
524
|
+
await broker.stop();
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test("reuses the same connection across multiple requests", async () => {
|
|
528
|
+
let connectionCount = 0;
|
|
529
|
+
broker.server.on("connection", () => {
|
|
530
|
+
connectionCount++;
|
|
531
|
+
});
|
|
532
|
+
broker.setHandler(() => ({ ok: true, result: { pong: true } }));
|
|
533
|
+
await broker.start();
|
|
534
|
+
|
|
535
|
+
const client = createBrokerClient();
|
|
536
|
+
await client.ping();
|
|
537
|
+
await client.ping();
|
|
538
|
+
await client.ping();
|
|
539
|
+
|
|
540
|
+
expect(connectionCount).toBe(1);
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
});
|