@vellumai/assistant 0.4.53 → 0.4.55
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/bun.lock +62 -349
- package/docs/architecture/integrations.md +1 -1
- package/docs/architecture/keychain-broker.md +94 -29
- package/docs/architecture/security.md +2 -2
- package/knip.json +7 -29
- package/package.json +2 -9
- package/src/__tests__/agent-loop.test.ts +1 -1
- package/src/__tests__/app-git-history.test.ts +0 -2
- package/src/__tests__/app-git-service.test.ts +1 -6
- package/src/__tests__/approval-cascade.test.ts +0 -1
- package/src/__tests__/avatar-e2e.test.ts +0 -1
- package/src/__tests__/browser-fill-credential.test.ts +1 -6
- package/src/__tests__/call-domain.test.ts +0 -1
- package/src/__tests__/call-routes-http.test.ts +0 -1
- package/src/__tests__/channel-guardian.test.ts +4 -4
- package/src/__tests__/channel-readiness-routes.test.ts +0 -1
- package/src/__tests__/channel-readiness-service.test.ts +0 -1
- package/src/__tests__/checker.test.ts +13 -11
- package/src/__tests__/claude-code-skill-regression.test.ts +0 -1
- package/src/__tests__/claude-code-tool-profiles.test.ts +1 -2
- package/src/__tests__/config-loader-backfill.test.ts +0 -3
- package/src/__tests__/config-schema.test.ts +3 -9
- package/src/__tests__/config-watcher.test.ts +11 -3
- package/src/__tests__/credential-broker-browser-fill.test.ts +27 -24
- package/src/__tests__/credential-broker-server-use.test.ts +60 -24
- package/src/__tests__/credential-security-e2e.test.ts +1 -6
- package/src/__tests__/credential-security-invariants.test.ts +13 -8
- package/src/__tests__/credential-vault-unit.test.ts +28 -12
- package/src/__tests__/credential-vault.test.ts +40 -28
- package/src/__tests__/credentials-cli.test.ts +1 -21
- package/src/__tests__/email-invite-adapter.test.ts +0 -1
- package/src/__tests__/fixtures/credential-security-fixtures.ts +3 -3
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -79
- package/src/__tests__/gateway-only-enforcement.test.ts +1 -21
- package/src/__tests__/guardian-action-conversation-turn.test.ts +8 -8
- package/src/__tests__/guardian-action-late-reply.test.ts +13 -14
- package/src/__tests__/guardian-action-store.test.ts +0 -57
- package/src/__tests__/guardian-outbound-http.test.ts +1 -1
- package/src/__tests__/guardian-verification-voice-binding.test.ts +1 -3
- package/src/__tests__/hooks-blocking.test.ts +1 -1
- package/src/__tests__/hooks-config.test.ts +5 -29
- package/src/__tests__/hooks-discovery.test.ts +1 -1
- package/src/__tests__/hooks-integration.test.ts +1 -1
- package/src/__tests__/hooks-manager.test.ts +1 -1
- package/src/__tests__/hooks-runner.test.ts +1 -23
- package/src/__tests__/hooks-settings.test.ts +1 -1
- package/src/__tests__/hooks-templates.test.ts +1 -1
- package/src/__tests__/integration-status.test.ts +0 -1
- package/src/__tests__/invite-routes-http.test.ts +0 -3
- package/src/__tests__/list-messages-attachments.test.ts +4 -4
- package/src/__tests__/llm-usage-store.test.ts +50 -0
- package/src/__tests__/managed-proxy-context.test.ts +41 -41
- package/src/__tests__/media-generate-image.test.ts +2 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +1 -6
- package/src/__tests__/memory-regressions.experimental.test.ts +4 -4
- package/src/__tests__/memory-regressions.test.ts +27 -27
- package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
- package/src/__tests__/memory-upsert-concurrency.test.ts +4 -4
- package/src/__tests__/notification-decision-fallback.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +1 -4
- package/src/__tests__/oauth-store.test.ts +1 -3
- package/src/__tests__/openai-provider.test.ts +7 -7
- package/src/__tests__/platform.test.ts +14 -4
- package/src/__tests__/pricing.test.ts +0 -223
- package/src/__tests__/provider-commit-message-generator.test.ts +1 -4
- package/src/__tests__/provider-fail-open-selection.test.ts +58 -54
- package/src/__tests__/provider-managed-proxy-integration.test.ts +63 -63
- package/src/__tests__/provider-registry-ollama.test.ts +3 -3
- package/src/__tests__/public-ingress-urls.test.ts +1 -1
- package/src/__tests__/registry.test.ts +3 -103
- package/src/__tests__/script-proxy-injection-runtime.test.ts +2 -7
- package/src/__tests__/secret-onetime-send.test.ts +1 -6
- package/src/__tests__/secret-routes-managed-proxy.test.ts +6 -13
- package/src/__tests__/secure-keys.test.ts +241 -229
- package/src/__tests__/session-abort-tool-results.test.ts +0 -1
- package/src/__tests__/session-confirmation-signals.test.ts +0 -1
- package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -7
- package/src/__tests__/session-pre-run-repair.test.ts +0 -1
- package/src/__tests__/session-provider-retry-repair.test.ts +0 -1
- package/src/__tests__/session-queue.test.ts +2 -4
- package/src/__tests__/session-slash-known.test.ts +0 -1
- package/src/__tests__/session-slash-queue.test.ts +0 -1
- package/src/__tests__/session-slash-unknown.test.ts +0 -1
- package/src/__tests__/session-workspace-injection.test.ts +0 -1
- package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
- package/src/__tests__/slack-channel-config.test.ts +1 -7
- package/src/__tests__/swarm-recursion.test.ts +0 -1
- package/src/__tests__/swarm-session-integration.test.ts +0 -1
- package/src/__tests__/swarm-tool.test.ts +0 -1
- package/src/__tests__/task-compiler.test.ts +1 -1
- package/src/__tests__/test-support/browser-skill-harness.ts +0 -18
- package/src/__tests__/test-support/computer-use-skill-harness.ts +0 -23
- package/src/__tests__/tool-executor.test.ts +1 -1
- package/src/__tests__/trust-store.test.ts +3 -82
- package/src/__tests__/twilio-config.test.ts +0 -1
- package/src/__tests__/twilio-provider.test.ts +0 -5
- package/src/__tests__/twilio-routes.test.ts +0 -1
- package/src/__tests__/usage-cache-backfill-migration.test.ts +10 -10
- package/src/calls/guardian-question-copy.ts +1 -1
- package/src/cli/commands/bash.ts +3 -0
- package/src/cli/commands/doctor.ts +10 -34
- package/src/cli/commands/memory.ts +3 -5
- package/src/cli/commands/sessions.ts +1 -1
- package/src/cli/commands/usage.ts +359 -0
- package/src/cli/http-client.ts +22 -12
- package/src/cli/program.ts +2 -0
- package/src/cli/reference.ts +1 -0
- package/src/cli.ts +251 -181
- package/src/config/assistant-feature-flags.ts +0 -7
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
- package/src/config/bundled-skills/claude-code/SKILL.md +1 -1
- package/src/config/bundled-skills/claude-code/TOOLS.json +1 -1
- package/src/config/bundled-skills/gmail/SKILL.md +0 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -2
- package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +0 -1
- package/src/config/bundled-skills/sequences/SKILL.md +0 -1
- package/src/config/env.ts +13 -0
- package/src/config/feature-flag-registry.json +9 -41
- package/src/config/schemas/security.ts +1 -2
- package/src/config/skills.ts +1 -1
- package/src/contacts/contact-store.ts +0 -50
- package/src/daemon/approved-devices-store.ts +0 -44
- package/src/daemon/classifier.ts +1 -1
- package/src/daemon/config-watcher.ts +14 -8
- package/src/daemon/handlers/config-model.ts +1 -1
- package/src/daemon/handlers/sessions.ts +4 -116
- package/src/daemon/handlers/skills.ts +1 -1
- package/src/daemon/lifecycle.ts +13 -15
- package/src/daemon/providers-setup.ts +1 -1
- package/src/daemon/server.ts +20 -3
- package/src/daemon/session-slash.ts +2 -2
- package/src/daemon/shutdown-handlers.ts +15 -0
- package/src/daemon/watch-handler.ts +2 -2
- package/src/email/guardrails.ts +1 -1
- package/src/email/service.ts +0 -5
- package/src/hooks/templates.ts +1 -1
- package/src/media/app-icon-generator.ts +2 -2
- package/src/media/avatar-router.ts +2 -2
- package/src/media/gemini-image-service.ts +5 -5
- package/src/memory/admin.ts +2 -2
- package/src/memory/app-git-service.ts +0 -7
- package/src/memory/conversation-crud.ts +1 -1
- package/src/memory/conversation-title-service.ts +2 -2
- package/src/memory/embedding-backend.ts +30 -26
- package/src/memory/external-conversation-store.ts +0 -30
- package/src/memory/guardian-action-store.ts +0 -31
- package/src/memory/guardian-approvals.ts +1 -56
- package/src/memory/indexer.ts +4 -3
- package/src/memory/items-extractor.ts +1 -1
- package/src/memory/job-handlers/backfill.ts +5 -2
- package/src/memory/job-handlers/index-maintenance.ts +2 -2
- package/src/memory/job-handlers/media-processing.ts +2 -2
- package/src/memory/job-handlers/summarization.ts +1 -1
- package/src/memory/job-utils.ts +1 -2
- package/src/memory/jobs-worker.ts +2 -2
- package/src/memory/llm-usage-store.ts +57 -11
- package/src/memory/media-store.ts +4 -535
- package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +2 -2
- package/src/memory/migrations/110-channel-guardian.ts +0 -1
- package/src/memory/published-pages-store.ts +0 -83
- package/src/memory/qdrant-circuit-breaker.ts +0 -8
- package/src/memory/retriever.ts +1 -1
- package/src/memory/schema/calls.ts +0 -67
- package/src/memory/search/semantic.ts +1 -8
- package/src/memory/shared-app-links-store.ts +0 -15
- package/src/messaging/registry.ts +0 -5
- package/src/messaging/style-analyzer.ts +1 -1
- package/src/notifications/copy-composer.ts +5 -13
- package/src/notifications/decision-engine.ts +2 -2
- package/src/notifications/deliveries-store.ts +0 -39
- package/src/notifications/guardian-question-mode.ts +6 -10
- package/src/notifications/preference-extractor.ts +1 -1
- package/src/oauth/byo-connection.test.ts +29 -20
- package/src/oauth/provider-behaviors.ts +1 -1
- package/src/permissions/checker.ts +1 -1
- package/src/permissions/shell-identity.ts +0 -5
- package/src/permissions/trust-store.ts +0 -37
- package/src/prompts/system-prompt.ts +4 -4
- package/src/prompts/templates/SOUL.md +1 -1
- package/src/providers/managed-proxy/constants.ts +8 -10
- package/src/providers/managed-proxy/context.ts +14 -9
- package/src/providers/provider-send-message.ts +4 -52
- package/src/providers/registry.ts +16 -50
- package/src/runtime/actor-token-store.ts +0 -23
- package/src/runtime/auth/__tests__/guard-tests.test.ts +64 -0
- package/src/runtime/http-router.ts +5 -1
- package/src/runtime/http-server.ts +101 -4
- package/src/runtime/invite-instruction-generator.ts +25 -51
- package/src/runtime/invite-service.ts +0 -20
- package/src/runtime/routes/attachment-routes.ts +1 -1
- package/src/runtime/routes/brain-graph-routes.ts +1 -1
- package/src/runtime/routes/call-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +32 -11
- package/src/runtime/routes/debug-routes.ts +1 -1
- package/src/runtime/routes/diagnostics-routes.ts +2 -2
- package/src/runtime/routes/documents-routes.ts +3 -3
- package/src/runtime/routes/global-search-routes.ts +1 -1
- package/src/runtime/routes/guardian-bootstrap-routes.ts +0 -20
- package/src/runtime/routes/guardian-refresh-routes.ts +0 -20
- package/src/runtime/routes/secret-routes.ts +4 -4
- package/src/runtime/routes/session-management-routes.ts +27 -0
- package/src/runtime/routes/trust-rules-routes.ts +1 -1
- package/src/security/credential-backend.ts +148 -0
- package/src/security/oauth2.ts +1 -1
- package/src/security/secret-allowlist.ts +1 -1
- package/src/security/secure-keys.ts +98 -160
- package/src/security/token-manager.ts +0 -7
- package/src/sequence/guardrails.ts +0 -4
- package/src/sequence/store.ts +1 -20
- package/src/sequence/types.ts +1 -36
- package/src/signals/bash.ts +33 -0
- package/src/signals/cancel.ts +69 -0
- package/src/signals/conversation-undo.ts +127 -0
- package/src/signals/trust-rule.ts +174 -0
- package/src/skills/clawhub.ts +5 -5
- package/src/skills/managed-store.ts +4 -4
- package/src/subagent/manager.ts +8 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +366 -0
- package/src/telemetry/usage-telemetry-reporter.ts +181 -0
- package/src/tools/claude-code/claude-code.ts +2 -2
- package/src/tools/credentials/vault.ts +8 -4
- package/src/tools/memory/handlers.test.ts +24 -26
- package/src/tools/memory/handlers.ts +1 -13
- package/src/tools/registry.ts +5 -100
- package/src/tools/terminal/parser.ts +34 -4
- package/src/tools/tool-manifest.ts +0 -10
- package/src/usage/actors.ts +0 -12
- package/src/util/canonicalize-identity.ts +0 -9
- package/src/util/errors.ts +0 -3
- package/src/util/platform.ts +24 -7
- package/src/util/pricing.ts +0 -38
- package/src/watcher/constants.ts +0 -7
- package/src/watcher/providers/linear.ts +1 -1
- package/src/work-items/work-item-store.ts +4 -4
- package/src/workspace/commit-message-provider.ts +1 -1
- package/src/workspace/git-service.ts +44 -1
- package/src/workspace/provider-commit-message-generator.ts +1 -1
- package/src/__tests__/fixtures/proxy-fixtures.ts +0 -147
- package/src/browser-extension-relay/client.ts +0 -155
- package/src/contacts/index.ts +0 -18
- package/src/daemon/tls-certs.ts +0 -270
- package/src/errors.ts +0 -41
- package/src/events/index.ts +0 -18
- package/src/followups/index.ts +0 -10
- package/src/playbooks/index.ts +0 -10
- package/src/runtime/auth/index.ts +0 -44
- package/src/tasks/candidate-store.ts +0 -95
- package/src/tools/browser/api-map.ts +0 -313
- package/src/tools/browser/auto-navigate.ts +0 -469
- package/src/tools/browser/headless-browser.ts +0 -590
- package/src/tools/browser/recording-store.ts +0 -75
- package/src/tools/computer-use/registry.ts +0 -21
- package/src/tools/tasks/index.ts +0 -27
|
@@ -34,6 +34,7 @@ let mockBrokerGetError = false;
|
|
|
34
34
|
let mockBrokerSetError = false;
|
|
35
35
|
let mockBrokerDelError = false;
|
|
36
36
|
let mockBrokerGetCalled = false;
|
|
37
|
+
let mockBrokerSetCalled = false;
|
|
37
38
|
|
|
38
39
|
mock.module("../security/keychain-broker-client.js", () => ({
|
|
39
40
|
createBrokerClient: () => ({
|
|
@@ -48,6 +49,7 @@ mock.module("../security/keychain-broker-client.js", () => ({
|
|
|
48
49
|
return { found: false };
|
|
49
50
|
},
|
|
50
51
|
set: async (account: string, value: string) => {
|
|
52
|
+
mockBrokerSetCalled = true;
|
|
51
53
|
if (mockBrokerSetError)
|
|
52
54
|
return {
|
|
53
55
|
status: "rejected" as const,
|
|
@@ -67,18 +69,13 @@ mock.module("../security/keychain-broker-client.js", () => ({
|
|
|
67
69
|
}),
|
|
68
70
|
}));
|
|
69
71
|
|
|
72
|
+
import * as encryptedStore from "../security/encrypted-store.js";
|
|
70
73
|
import { _setStorePath } from "../security/encrypted-store.js";
|
|
71
74
|
import {
|
|
72
75
|
_resetBackend,
|
|
73
|
-
_setBackend,
|
|
74
|
-
deleteSecureKey,
|
|
75
76
|
deleteSecureKeyAsync,
|
|
76
|
-
getBackendType,
|
|
77
|
-
getSecureKey,
|
|
78
77
|
getSecureKeyAsync,
|
|
79
|
-
|
|
80
|
-
listSecureKeys,
|
|
81
|
-
setSecureKey,
|
|
78
|
+
listSecureKeysAsync,
|
|
82
79
|
setSecureKeyAsync,
|
|
83
80
|
} from "../security/secure-keys.js";
|
|
84
81
|
|
|
@@ -103,6 +100,10 @@ describe("secure-keys", () => {
|
|
|
103
100
|
mockBrokerSetError = false;
|
|
104
101
|
mockBrokerDelError = false;
|
|
105
102
|
mockBrokerGetCalled = false;
|
|
103
|
+
mockBrokerSetCalled = false;
|
|
104
|
+
|
|
105
|
+
// Ensure VELLUM_DEV is NOT set so broker tests work by default
|
|
106
|
+
delete process.env.VELLUM_DEV;
|
|
106
107
|
|
|
107
108
|
if (existsSync(TEST_DIR)) {
|
|
108
109
|
rmSync(TEST_DIR, { recursive: true });
|
|
@@ -114,6 +115,7 @@ describe("secure-keys", () => {
|
|
|
114
115
|
afterEach(() => {
|
|
115
116
|
_setStorePath(null);
|
|
116
117
|
_resetBackend();
|
|
118
|
+
delete process.env.VELLUM_DEV;
|
|
117
119
|
});
|
|
118
120
|
|
|
119
121
|
afterAll(() => {
|
|
@@ -123,337 +125,347 @@ describe("secure-keys", () => {
|
|
|
123
125
|
});
|
|
124
126
|
|
|
125
127
|
// -----------------------------------------------------------------------
|
|
126
|
-
//
|
|
128
|
+
// CRUD operations (via encrypted store backend — broker unavailable)
|
|
127
129
|
// -----------------------------------------------------------------------
|
|
128
|
-
describe("backend
|
|
129
|
-
test("
|
|
130
|
-
|
|
130
|
+
describe("CRUD with encrypted backend (broker unavailable)", () => {
|
|
131
|
+
test("set and get a key", async () => {
|
|
132
|
+
await setSecureKeyAsync("openai", "sk-openai-789");
|
|
133
|
+
expect(await getSecureKeyAsync("openai")).toBe("sk-openai-789");
|
|
131
134
|
});
|
|
132
135
|
|
|
133
|
-
test("returns
|
|
134
|
-
|
|
135
|
-
expect(getBackendType()).toBe("broker");
|
|
136
|
+
test("get returns undefined for nonexistent key", async () => {
|
|
137
|
+
expect(await getSecureKeyAsync("nonexistent")).toBeUndefined();
|
|
136
138
|
});
|
|
137
139
|
|
|
138
|
-
test("
|
|
139
|
-
|
|
140
|
+
test("delete removes a key", async () => {
|
|
141
|
+
await setSecureKeyAsync("gemini", "gem-key");
|
|
142
|
+
expect(await deleteSecureKeyAsync("gemini")).toBe("deleted");
|
|
143
|
+
expect(await getSecureKeyAsync("gemini")).toBeUndefined();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("delete returns not-found for nonexistent key", async () => {
|
|
147
|
+
expect(await deleteSecureKeyAsync("missing")).toBe("not-found");
|
|
140
148
|
});
|
|
141
149
|
});
|
|
142
150
|
|
|
143
151
|
// -----------------------------------------------------------------------
|
|
144
|
-
//
|
|
152
|
+
// Single-writer: writes go to keychain only when broker available
|
|
145
153
|
// -----------------------------------------------------------------------
|
|
146
|
-
describe("
|
|
147
|
-
test("
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("get returns undefined for nonexistent key", () => {
|
|
153
|
-
expect(getSecureKey("nonexistent")).toBeUndefined();
|
|
154
|
-
});
|
|
154
|
+
describe("single-writer with broker available", () => {
|
|
155
|
+
test("setSecureKeyAsync writes to broker only (not encrypted store)", async () => {
|
|
156
|
+
mockBrokerAvailable = true;
|
|
157
|
+
_resetBackend();
|
|
155
158
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
expect(
|
|
159
|
+
const result = await setSecureKeyAsync("api-key", "new-value");
|
|
160
|
+
expect(result).toBe(true);
|
|
161
|
+
// Value is in the broker store
|
|
162
|
+
expect(mockBrokerStore.get("api-key")).toBe("new-value");
|
|
163
|
+
// Value should NOT be in the encrypted store (single-writer)
|
|
164
|
+
expect(encryptedStore.getKey("api-key")).toBeUndefined();
|
|
160
165
|
});
|
|
161
166
|
|
|
162
|
-
test("
|
|
163
|
-
|
|
164
|
-
|
|
167
|
+
test("setSecureKeyAsync returns false on broker set error", async () => {
|
|
168
|
+
mockBrokerAvailable = true;
|
|
169
|
+
mockBrokerSetError = true;
|
|
170
|
+
_resetBackend();
|
|
165
171
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const keys = listSecureKeys();
|
|
170
|
-
expect(keys).toContain("anthropic");
|
|
171
|
-
expect(keys).toContain("openai");
|
|
172
|
-
expect(keys.length).toBe(2);
|
|
172
|
+
const result = await setSecureKeyAsync("api-key", "new-value");
|
|
173
|
+
expect(result).toBe(false);
|
|
174
|
+
expect(mockBrokerStore.has("api-key")).toBe(false);
|
|
173
175
|
});
|
|
174
176
|
});
|
|
175
177
|
|
|
176
178
|
// -----------------------------------------------------------------------
|
|
177
|
-
//
|
|
179
|
+
// Reads: primary backend first, legacy fallback to encrypted store
|
|
178
180
|
// -----------------------------------------------------------------------
|
|
179
|
-
describe("
|
|
180
|
-
test("
|
|
181
|
+
describe("reads with broker available", () => {
|
|
182
|
+
test("getSecureKeyAsync reads from broker (primary backend)", async () => {
|
|
181
183
|
mockBrokerAvailable = true;
|
|
184
|
+
_resetBackend();
|
|
185
|
+
|
|
182
186
|
mockBrokerStore.set("api-key", "broker-value");
|
|
183
|
-
|
|
184
|
-
expect(
|
|
185
|
-
|
|
186
|
-
setSecureKey("api-key", "encrypted-value");
|
|
187
|
-
expect(getSecureKey("api-key")).toBe("encrypted-value");
|
|
187
|
+
const result = await getSecureKeyAsync("api-key");
|
|
188
|
+
expect(result).toBe("broker-value");
|
|
189
|
+
expect(mockBrokerGetCalled).toBe(true);
|
|
188
190
|
});
|
|
189
191
|
|
|
190
|
-
test("
|
|
192
|
+
test("getSecureKeyAsync falls back to encrypted store for legacy keys", async () => {
|
|
191
193
|
mockBrokerAvailable = true;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
//
|
|
195
|
-
|
|
194
|
+
_resetBackend();
|
|
195
|
+
|
|
196
|
+
// Pre-populate encrypted store directly (legacy key not in broker)
|
|
197
|
+
encryptedStore.setKey("legacy-key", "legacy-value");
|
|
198
|
+
|
|
199
|
+
const result = await getSecureKeyAsync("legacy-key");
|
|
200
|
+
expect(result).toBe("legacy-value");
|
|
201
|
+
// Broker was checked first (returned nothing), then encrypted store
|
|
202
|
+
expect(mockBrokerGetCalled).toBe(true);
|
|
196
203
|
});
|
|
197
204
|
|
|
198
|
-
test("
|
|
205
|
+
test("getSecureKeyAsync returns undefined when neither store has the key", async () => {
|
|
199
206
|
mockBrokerAvailable = true;
|
|
207
|
+
_resetBackend();
|
|
208
|
+
|
|
209
|
+
expect(await getSecureKeyAsync("missing-key")).toBeUndefined();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("getSecureKeyAsync returns broker value even when encrypted store also has a value", async () => {
|
|
213
|
+
mockBrokerAvailable = true;
|
|
214
|
+
_resetBackend();
|
|
215
|
+
|
|
216
|
+
// Both stores have a value — broker (primary) should win
|
|
200
217
|
mockBrokerStore.set("api-key", "broker-value");
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
expect(mockBrokerStore.has("api-key")).toBe(true);
|
|
218
|
+
encryptedStore.setKey("api-key", "encrypted-value");
|
|
219
|
+
|
|
220
|
+
const result = await getSecureKeyAsync("api-key");
|
|
221
|
+
expect(result).toBe("broker-value");
|
|
206
222
|
});
|
|
207
223
|
});
|
|
208
224
|
|
|
209
225
|
// -----------------------------------------------------------------------
|
|
210
|
-
//
|
|
226
|
+
// Dev mode bypass — VELLUM_DEV=1 uses encrypted store only
|
|
211
227
|
// -----------------------------------------------------------------------
|
|
212
|
-
describe("
|
|
213
|
-
test("
|
|
228
|
+
describe("dev mode bypass (VELLUM_DEV=1)", () => {
|
|
229
|
+
test("setSecureKeyAsync writes to encrypted store only, ignoring broker", async () => {
|
|
230
|
+
process.env.VELLUM_DEV = "1";
|
|
214
231
|
mockBrokerAvailable = true;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
expect(
|
|
219
|
-
|
|
232
|
+
_resetBackend();
|
|
233
|
+
|
|
234
|
+
const result = await setSecureKeyAsync("api-key", "dev-value");
|
|
235
|
+
expect(result).toBe(true);
|
|
236
|
+
// Written to encrypted store
|
|
237
|
+
expect(encryptedStore.getKey("api-key")).toBe("dev-value");
|
|
238
|
+
// NOT written to broker
|
|
239
|
+
expect(mockBrokerStore.has("api-key")).toBe(false);
|
|
240
|
+
expect(mockBrokerSetCalled).toBe(false);
|
|
220
241
|
});
|
|
221
242
|
|
|
222
|
-
test("getSecureKeyAsync
|
|
243
|
+
test("getSecureKeyAsync reads from encrypted store only, ignoring broker", async () => {
|
|
244
|
+
process.env.VELLUM_DEV = "1";
|
|
223
245
|
mockBrokerAvailable = true;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
246
|
+
_resetBackend();
|
|
247
|
+
|
|
248
|
+
mockBrokerStore.set("api-key", "broker-value");
|
|
249
|
+
encryptedStore.setKey("api-key", "encrypted-value");
|
|
250
|
+
|
|
251
|
+
const result = await getSecureKeyAsync("api-key");
|
|
252
|
+
expect(result).toBe("encrypted-value");
|
|
253
|
+
// Broker should not have been contacted
|
|
228
254
|
expect(mockBrokerGetCalled).toBe(false);
|
|
229
255
|
});
|
|
230
256
|
|
|
231
|
-
test("getSecureKeyAsync returns undefined when
|
|
257
|
+
test("getSecureKeyAsync returns undefined when encrypted store is empty (does not check broker)", async () => {
|
|
258
|
+
process.env.VELLUM_DEV = "1";
|
|
232
259
|
mockBrokerAvailable = true;
|
|
233
|
-
|
|
234
|
-
expect(await getSecureKeyAsync("missing-key")).toBeUndefined();
|
|
235
|
-
});
|
|
260
|
+
_resetBackend();
|
|
236
261
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
// the broker error flag is irrelevant.
|
|
242
|
-
setSecureKey("api-key", "encrypted-value");
|
|
243
|
-
expect(await getSecureKeyAsync("api-key")).toBe("encrypted-value");
|
|
262
|
+
mockBrokerStore.set("api-key", "broker-value");
|
|
263
|
+
|
|
264
|
+
const result = await getSecureKeyAsync("api-key");
|
|
265
|
+
expect(result).toBeUndefined();
|
|
244
266
|
expect(mockBrokerGetCalled).toBe(false);
|
|
245
267
|
});
|
|
268
|
+
});
|
|
246
269
|
|
|
247
|
-
|
|
270
|
+
// -----------------------------------------------------------------------
|
|
271
|
+
// Delete always attempts both stores
|
|
272
|
+
// -----------------------------------------------------------------------
|
|
273
|
+
describe("delete attempts both stores", () => {
|
|
274
|
+
test("deleteSecureKeyAsync removes from both stores when broker available", async () => {
|
|
248
275
|
mockBrokerAvailable = true;
|
|
249
|
-
|
|
250
|
-
expect(result).toBe(true);
|
|
251
|
-
expect(mockBrokerStore.get("api-key")).toBe("new-value");
|
|
252
|
-
// Also persisted to encrypted store for sync callers
|
|
253
|
-
expect(getSecureKey("api-key")).toBe("new-value");
|
|
254
|
-
});
|
|
276
|
+
_resetBackend();
|
|
255
277
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const result = await
|
|
260
|
-
|
|
261
|
-
// leave the broker with stale data that async readers still see.
|
|
262
|
-
expect(result).toBe(false);
|
|
278
|
+
mockBrokerStore.set("api-key", "broker-value");
|
|
279
|
+
encryptedStore.setKey("api-key", "encrypted-value");
|
|
280
|
+
|
|
281
|
+
const result = await deleteSecureKeyAsync("api-key");
|
|
282
|
+
expect(result).toBe("deleted");
|
|
263
283
|
expect(mockBrokerStore.has("api-key")).toBe(false);
|
|
264
|
-
|
|
265
|
-
expect(getSecureKey("api-key")).toBeUndefined();
|
|
284
|
+
expect(encryptedStore.getKey("api-key")).toBeUndefined();
|
|
266
285
|
});
|
|
267
286
|
|
|
268
|
-
test("deleteSecureKeyAsync
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
287
|
+
test("deleteSecureKeyAsync returns deleted when only encrypted store has key", async () => {
|
|
288
|
+
// Broker unavailable — only encrypted store
|
|
289
|
+
encryptedStore.setKey("api-key", "encrypted-value");
|
|
290
|
+
|
|
272
291
|
const result = await deleteSecureKeyAsync("api-key");
|
|
273
292
|
expect(result).toBe("deleted");
|
|
274
|
-
expect(
|
|
275
|
-
expect(getSecureKey("api-key")).toBeUndefined();
|
|
293
|
+
expect(encryptedStore.getKey("api-key")).toBeUndefined();
|
|
276
294
|
});
|
|
277
295
|
|
|
278
|
-
test("deleteSecureKeyAsync returns error
|
|
296
|
+
test("deleteSecureKeyAsync returns error when broker delete fails", async () => {
|
|
279
297
|
mockBrokerAvailable = true;
|
|
280
298
|
mockBrokerDelError = true;
|
|
281
|
-
|
|
299
|
+
_resetBackend();
|
|
300
|
+
|
|
301
|
+
mockBrokerStore.set("api-key", "broker-value");
|
|
302
|
+
encryptedStore.setKey("api-key", "encrypted-value");
|
|
303
|
+
|
|
282
304
|
const result = await deleteSecureKeyAsync("api-key");
|
|
283
|
-
// Must return "error" — falling through to encrypted-only delete would
|
|
284
|
-
// leave the broker with the key, and async readers would still see it.
|
|
285
305
|
expect(result).toBe("error");
|
|
286
|
-
// Encrypted store should NOT have been modified either.
|
|
287
|
-
expect(getSecureKey("api-key")).toBe("encrypted-value");
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
// -----------------------------------------------------------------------
|
|
292
|
-
// Async variants — broker unavailable path
|
|
293
|
-
// -----------------------------------------------------------------------
|
|
294
|
-
describe("async variants with broker unavailable", () => {
|
|
295
|
-
test("getSecureKeyAsync uses encrypted store", async () => {
|
|
296
|
-
setSecureKey("api-key", "encrypted-value");
|
|
297
|
-
expect(await getSecureKeyAsync("api-key")).toBe("encrypted-value");
|
|
298
306
|
});
|
|
299
307
|
|
|
300
|
-
test("
|
|
301
|
-
|
|
302
|
-
|
|
308
|
+
test("deleteSecureKeyAsync in dev mode still attempts both stores", async () => {
|
|
309
|
+
process.env.VELLUM_DEV = "1";
|
|
310
|
+
mockBrokerAvailable = true;
|
|
311
|
+
_resetBackend();
|
|
303
312
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
expect(result).toBe(true);
|
|
307
|
-
expect(getSecureKey("api-key")).toBe("new-value");
|
|
308
|
-
});
|
|
313
|
+
mockBrokerStore.set("api-key", "broker-value");
|
|
314
|
+
encryptedStore.setKey("api-key", "encrypted-value");
|
|
309
315
|
|
|
310
|
-
test("deleteSecureKeyAsync uses encrypted store", async () => {
|
|
311
|
-
setSecureKey("api-key", "value");
|
|
312
316
|
const result = await deleteSecureKeyAsync("api-key");
|
|
313
317
|
expect(result).toBe("deleted");
|
|
314
|
-
expect(
|
|
318
|
+
expect(mockBrokerStore.has("api-key")).toBe(false);
|
|
319
|
+
expect(encryptedStore.getKey("api-key")).toBeUndefined();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("deleteSecureKeyAsync returns not-found when key missing from both stores", async () => {
|
|
323
|
+
// Broker unavailable, encrypted store empty
|
|
324
|
+
const result = await deleteSecureKeyAsync("missing-key");
|
|
325
|
+
expect(result).toBe("not-found");
|
|
315
326
|
});
|
|
316
327
|
});
|
|
317
328
|
|
|
318
329
|
// -----------------------------------------------------------------------
|
|
319
|
-
//
|
|
330
|
+
// Legacy read fallback
|
|
320
331
|
// -----------------------------------------------------------------------
|
|
321
|
-
describe("
|
|
322
|
-
test("returns value
|
|
332
|
+
describe("legacy read fallback", () => {
|
|
333
|
+
test("returns encrypted store value when broker has no key (legacy migration)", async () => {
|
|
323
334
|
mockBrokerAvailable = true;
|
|
324
|
-
|
|
325
|
-
mockBrokerStore.set("test-account", "broker-secret");
|
|
326
|
-
|
|
327
|
-
const result = await getSecureKeyAsync("test-account");
|
|
328
|
-
expect(result).toBe("test-secret");
|
|
329
|
-
// Broker should never have been called — encrypted store hit
|
|
330
|
-
// short-circuits the lookup.
|
|
331
|
-
expect(mockBrokerGetCalled).toBe(false);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
test("falls back to broker when encrypted store returns undefined", async () => {
|
|
335
|
-
mockBrokerAvailable = true;
|
|
336
|
-
// Encrypted store has nothing for this key
|
|
337
|
-
mockBrokerStore.set("test-account", "broker-secret");
|
|
338
|
-
|
|
339
|
-
const result = await getSecureKeyAsync("test-account");
|
|
340
|
-
expect(result).toBe("broker-secret");
|
|
341
|
-
// Broker should have been called as fallback
|
|
342
|
-
expect(mockBrokerGetCalled).toBe(true);
|
|
343
|
-
});
|
|
335
|
+
_resetBackend();
|
|
344
336
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
337
|
+
// Simulate a legacy key that was written to encrypted store before
|
|
338
|
+
// the single-writer migration — broker doesn't have it.
|
|
339
|
+
encryptedStore.setKey("legacy-account", "legacy-secret");
|
|
348
340
|
|
|
349
|
-
const result = await getSecureKeyAsync("
|
|
350
|
-
expect(result).
|
|
341
|
+
const result = await getSecureKeyAsync("legacy-account");
|
|
342
|
+
expect(result).toBe("legacy-secret");
|
|
351
343
|
});
|
|
352
344
|
|
|
353
|
-
test("
|
|
354
|
-
// Broker
|
|
355
|
-
|
|
345
|
+
test("does not fall back to encrypted store when already using encrypted store backend", async () => {
|
|
346
|
+
// Broker unavailable — primary backend IS the encrypted store.
|
|
347
|
+
// No fallback needed.
|
|
348
|
+
encryptedStore.setKey("account", "value");
|
|
349
|
+
encryptedStore.setKey("other-account", "other-value");
|
|
356
350
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
351
|
+
// Should read directly from encrypted store (primary)
|
|
352
|
+
const result = await getSecureKeyAsync("account");
|
|
353
|
+
expect(result).toBe("value");
|
|
354
|
+
// Broker should not have been contacted
|
|
360
355
|
expect(mockBrokerGetCalled).toBe(false);
|
|
361
356
|
});
|
|
362
357
|
});
|
|
363
358
|
|
|
364
359
|
// -----------------------------------------------------------------------
|
|
365
|
-
// Stale-value prevention
|
|
360
|
+
// Stale-value prevention
|
|
366
361
|
// -----------------------------------------------------------------------
|
|
367
362
|
describe("stale-value prevention", () => {
|
|
368
|
-
test("setSecureKeyAsync
|
|
363
|
+
test("setSecureKeyAsync failure does not corrupt broker store", async () => {
|
|
369
364
|
mockBrokerAvailable = true;
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
365
|
+
_resetBackend();
|
|
366
|
+
|
|
367
|
+
// Pre-seed broker with original value
|
|
368
|
+
mockBrokerStore.set("api-key", "original-value");
|
|
373
369
|
|
|
374
|
-
//
|
|
370
|
+
// Now fail the next set
|
|
371
|
+
mockBrokerSetError = true;
|
|
375
372
|
const ok = await setSecureKeyAsync("api-key", "new-value");
|
|
376
|
-
expect(ok).toBe(
|
|
373
|
+
expect(ok).toBe(false);
|
|
377
374
|
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
expect(value).toBe("new-value");
|
|
381
|
-
// Both stores should agree
|
|
382
|
-
expect(mockBrokerStore.get("api-key")).toBe("new-value");
|
|
383
|
-
expect(getSecureKey("api-key")).toBe("new-value");
|
|
375
|
+
// Broker should still have original value
|
|
376
|
+
expect(mockBrokerStore.get("api-key")).toBe("original-value");
|
|
384
377
|
});
|
|
378
|
+
});
|
|
385
379
|
|
|
386
|
-
|
|
380
|
+
// -----------------------------------------------------------------------
|
|
381
|
+
// listSecureKeysAsync — merged/deduplicated key listing
|
|
382
|
+
// -----------------------------------------------------------------------
|
|
383
|
+
describe("listSecureKeysAsync", () => {
|
|
384
|
+
test("returns merged, deduplicated keys when broker is primary and encrypted store has legacy keys", async () => {
|
|
387
385
|
mockBrokerAvailable = true;
|
|
388
|
-
|
|
389
|
-
setSecureKey("api-key", "old-encrypted-value");
|
|
386
|
+
_resetBackend();
|
|
390
387
|
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
|
|
388
|
+
// Broker has some keys
|
|
389
|
+
mockBrokerStore.set("broker-key-1", "val1");
|
|
390
|
+
mockBrokerStore.set("shared-key", "broker-val");
|
|
394
391
|
|
|
395
|
-
//
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
});
|
|
392
|
+
// Encrypted store has legacy keys (some overlapping)
|
|
393
|
+
encryptedStore.setKey("legacy-key-1", "val2");
|
|
394
|
+
encryptedStore.setKey("shared-key", "enc-val");
|
|
399
395
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
396
|
+
const keys = await listSecureKeysAsync();
|
|
397
|
+
expect(keys).toContain("broker-key-1");
|
|
398
|
+
expect(keys).toContain("legacy-key-1");
|
|
399
|
+
expect(keys).toContain("shared-key");
|
|
400
|
+
// Should be exactly 3 unique keys (no duplicates)
|
|
401
|
+
expect(keys.length).toBe(3);
|
|
402
|
+
});
|
|
403
403
|
|
|
404
|
-
|
|
405
|
-
|
|
404
|
+
test("returns only encrypted store keys when broker is unavailable", async () => {
|
|
405
|
+
// Broker unavailable (default state) — primary backend is encrypted store
|
|
406
|
+
encryptedStore.setKey("enc-key-1", "val1");
|
|
407
|
+
encryptedStore.setKey("enc-key-2", "val2");
|
|
406
408
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
expect(
|
|
411
|
-
expect(mockBrokerGetCalled).toBe(false);
|
|
409
|
+
const keys = await listSecureKeysAsync();
|
|
410
|
+
expect(keys).toContain("enc-key-1");
|
|
411
|
+
expect(keys).toContain("enc-key-2");
|
|
412
|
+
expect(keys.length).toBe(2);
|
|
412
413
|
});
|
|
413
414
|
|
|
414
|
-
test("
|
|
415
|
+
test("returns only encrypted store keys when VELLUM_DEV=1 (even if broker available)", async () => {
|
|
416
|
+
process.env.VELLUM_DEV = "1";
|
|
415
417
|
mockBrokerAvailable = true;
|
|
416
|
-
|
|
417
|
-
mockBrokerStore.set("api-key", "original-value");
|
|
418
|
-
setSecureKey("api-key", "original-value");
|
|
418
|
+
_resetBackend();
|
|
419
419
|
|
|
420
|
-
|
|
421
|
-
|
|
420
|
+
// Broker has keys that should be ignored
|
|
421
|
+
mockBrokerStore.set("broker-only", "val1");
|
|
422
422
|
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
|
|
423
|
+
// Encrypted store has keys
|
|
424
|
+
encryptedStore.setKey("dev-key-1", "val2");
|
|
425
|
+
encryptedStore.setKey("dev-key-2", "val3");
|
|
426
|
+
|
|
427
|
+
const keys = await listSecureKeysAsync();
|
|
428
|
+
expect(keys).toContain("dev-key-1");
|
|
429
|
+
expect(keys).toContain("dev-key-2");
|
|
430
|
+
// broker-only key should NOT appear since primary backend is encrypted store
|
|
431
|
+
expect(keys).not.toContain("broker-only");
|
|
432
|
+
expect(keys.length).toBe(2);
|
|
426
433
|
});
|
|
427
434
|
|
|
428
|
-
test("
|
|
435
|
+
test("returns broker-only keys when encrypted store is empty", async () => {
|
|
429
436
|
mockBrokerAvailable = true;
|
|
430
|
-
|
|
431
|
-
mockBrokerStore.set("api-key", "value");
|
|
432
|
-
setSecureKey("api-key", "value");
|
|
437
|
+
_resetBackend();
|
|
433
438
|
|
|
434
|
-
|
|
435
|
-
|
|
439
|
+
mockBrokerStore.set("broker-key-1", "val1");
|
|
440
|
+
mockBrokerStore.set("broker-key-2", "val2");
|
|
436
441
|
|
|
437
|
-
|
|
438
|
-
expect(
|
|
439
|
-
expect(
|
|
442
|
+
const keys = await listSecureKeysAsync();
|
|
443
|
+
expect(keys).toContain("broker-key-1");
|
|
444
|
+
expect(keys).toContain("broker-key-2");
|
|
445
|
+
expect(keys.length).toBe(2);
|
|
440
446
|
});
|
|
441
|
-
});
|
|
442
447
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
448
|
+
test("deduplicates keys that exist in both stores", async () => {
|
|
449
|
+
mockBrokerAvailable = true;
|
|
450
|
+
_resetBackend();
|
|
451
|
+
|
|
452
|
+
// Same key in both stores
|
|
453
|
+
mockBrokerStore.set("api-key", "broker-val");
|
|
454
|
+
encryptedStore.setKey("api-key", "enc-val");
|
|
455
|
+
|
|
456
|
+
const keys = await listSecureKeysAsync();
|
|
457
|
+
expect(keys).toContain("api-key");
|
|
458
|
+
// Only one copy, not two
|
|
459
|
+
expect(keys.length).toBe(1);
|
|
460
|
+
expect(keys.filter((k) => k === "api-key").length).toBe(1);
|
|
451
461
|
});
|
|
452
462
|
|
|
453
|
-
test("
|
|
463
|
+
test("returns empty array when both stores are empty", async () => {
|
|
464
|
+
mockBrokerAvailable = true;
|
|
454
465
|
_resetBackend();
|
|
455
|
-
|
|
456
|
-
|
|
466
|
+
|
|
467
|
+
const keys = await listSecureKeysAsync();
|
|
468
|
+
expect(keys).toEqual([]);
|
|
457
469
|
});
|
|
458
470
|
});
|
|
459
471
|
});
|