@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
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
|
|
5
|
+
import type { CredentialMetadata } from "../tools/credentials/metadata-store.js";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// In-memory mock state
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
let secureKeyStore = new Map<string, string>();
|
|
12
|
+
let metadataStore: CredentialMetadata[] = [];
|
|
13
|
+
let idCounter = 0;
|
|
14
|
+
|
|
15
|
+
function nextUUID(): string {
|
|
16
|
+
idCounter += 1;
|
|
17
|
+
return `00000000-0000-0000-0000-${String(idCounter).padStart(12, "0")}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Track mock call counts
|
|
21
|
+
let _getSecureKeyCalls = 0;
|
|
22
|
+
let _setSecureKeyCalls = 0;
|
|
23
|
+
let _deleteSecureKeyCalls = 0;
|
|
24
|
+
let _listMetadataCalls = 0;
|
|
25
|
+
let _getMetadataCalls = 0;
|
|
26
|
+
let _getMetadataByIdCalls = 0;
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Mock secure-keys
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
mock.module("../security/secure-keys.js", () => ({
|
|
33
|
+
getSecureKey: (account: string): string | undefined => {
|
|
34
|
+
_getSecureKeyCalls += 1;
|
|
35
|
+
return secureKeyStore.get(account);
|
|
36
|
+
},
|
|
37
|
+
setSecureKey: (account: string, value: string): boolean => {
|
|
38
|
+
_setSecureKeyCalls += 1;
|
|
39
|
+
secureKeyStore.set(account, value);
|
|
40
|
+
return true;
|
|
41
|
+
},
|
|
42
|
+
deleteSecureKey: (account: string) => {
|
|
43
|
+
_deleteSecureKeyCalls += 1;
|
|
44
|
+
if (secureKeyStore.has(account)) {
|
|
45
|
+
secureKeyStore.delete(account);
|
|
46
|
+
return "deleted" as const;
|
|
47
|
+
}
|
|
48
|
+
return "not-found" as const;
|
|
49
|
+
},
|
|
50
|
+
listSecureKeys: (): string[] => {
|
|
51
|
+
return [...secureKeyStore.keys()];
|
|
52
|
+
},
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Mock metadata-store
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
mock.module("../tools/credentials/metadata-store.js", () => ({
|
|
60
|
+
assertMetadataWritable: (): void => {},
|
|
61
|
+
upsertCredentialMetadata: (
|
|
62
|
+
service: string,
|
|
63
|
+
field: string,
|
|
64
|
+
policy?: {
|
|
65
|
+
allowedTools?: string[];
|
|
66
|
+
allowedDomains?: string[];
|
|
67
|
+
usageDescription?: string;
|
|
68
|
+
expiresAt?: number | null;
|
|
69
|
+
grantedScopes?: string[];
|
|
70
|
+
accountInfo?: string | null;
|
|
71
|
+
alias?: string | null;
|
|
72
|
+
injectionTemplates?: unknown[] | null;
|
|
73
|
+
},
|
|
74
|
+
): CredentialMetadata => {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const existing = metadataStore.find(
|
|
77
|
+
(c) => c.service === service && c.field === field,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
if (existing) {
|
|
81
|
+
if (policy?.allowedTools !== undefined)
|
|
82
|
+
existing.allowedTools = policy.allowedTools;
|
|
83
|
+
if (policy?.allowedDomains !== undefined)
|
|
84
|
+
existing.allowedDomains = policy.allowedDomains;
|
|
85
|
+
if (policy?.usageDescription !== undefined)
|
|
86
|
+
existing.usageDescription = policy.usageDescription;
|
|
87
|
+
if (policy?.alias !== undefined) {
|
|
88
|
+
if (policy.alias == null) {
|
|
89
|
+
delete existing.alias;
|
|
90
|
+
} else {
|
|
91
|
+
existing.alias = policy.alias;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
existing.updatedAt = now;
|
|
95
|
+
return existing;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const record: CredentialMetadata = {
|
|
99
|
+
credentialId: nextUUID(),
|
|
100
|
+
service,
|
|
101
|
+
field,
|
|
102
|
+
allowedTools: policy?.allowedTools ?? [],
|
|
103
|
+
allowedDomains: policy?.allowedDomains ?? [],
|
|
104
|
+
usageDescription: policy?.usageDescription,
|
|
105
|
+
alias: policy?.alias ?? undefined,
|
|
106
|
+
createdAt: now,
|
|
107
|
+
updatedAt: now,
|
|
108
|
+
};
|
|
109
|
+
metadataStore.push(record);
|
|
110
|
+
return record;
|
|
111
|
+
},
|
|
112
|
+
getCredentialMetadata: (
|
|
113
|
+
service: string,
|
|
114
|
+
field: string,
|
|
115
|
+
): CredentialMetadata | undefined => {
|
|
116
|
+
_getMetadataCalls += 1;
|
|
117
|
+
return metadataStore.find(
|
|
118
|
+
(c) => c.service === service && c.field === field,
|
|
119
|
+
);
|
|
120
|
+
},
|
|
121
|
+
getCredentialMetadataById: (
|
|
122
|
+
credentialId: string,
|
|
123
|
+
): CredentialMetadata | undefined => {
|
|
124
|
+
_getMetadataByIdCalls += 1;
|
|
125
|
+
return metadataStore.find((c) => c.credentialId === credentialId);
|
|
126
|
+
},
|
|
127
|
+
deleteCredentialMetadata: (service: string, field: string): boolean => {
|
|
128
|
+
const idx = metadataStore.findIndex(
|
|
129
|
+
(c) => c.service === service && c.field === field,
|
|
130
|
+
);
|
|
131
|
+
if (idx === -1) return false;
|
|
132
|
+
metadataStore.splice(idx, 1);
|
|
133
|
+
return true;
|
|
134
|
+
},
|
|
135
|
+
listCredentialMetadata: (): CredentialMetadata[] => {
|
|
136
|
+
_listMetadataCalls += 1;
|
|
137
|
+
return [...metadataStore];
|
|
138
|
+
},
|
|
139
|
+
}));
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Import the module under test (after mocks are registered)
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
const { registerCredentialsCommand } = await import("../cli/credentials.js");
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Test helper
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
async function runCli(
|
|
152
|
+
args: string[],
|
|
153
|
+
): Promise<{ exitCode: number; stdout: string }> {
|
|
154
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
155
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
156
|
+
const stdoutChunks: string[] = [];
|
|
157
|
+
|
|
158
|
+
process.stdout.write = ((chunk: unknown) => {
|
|
159
|
+
stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
|
|
160
|
+
return true;
|
|
161
|
+
}) as typeof process.stdout.write;
|
|
162
|
+
|
|
163
|
+
// Suppress stderr so Commander error messages don't leak to test runner
|
|
164
|
+
process.stderr.write = (() => true) as typeof process.stderr.write;
|
|
165
|
+
|
|
166
|
+
process.exitCode = 0;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const program = new Command();
|
|
170
|
+
program.exitOverride();
|
|
171
|
+
program.configureOutput({
|
|
172
|
+
writeErr: () => {},
|
|
173
|
+
writeOut: (str: string) => stdoutChunks.push(str),
|
|
174
|
+
});
|
|
175
|
+
registerCredentialsCommand(program);
|
|
176
|
+
await program.parseAsync(["node", "vellum", "credentials", ...args]);
|
|
177
|
+
} catch {
|
|
178
|
+
// Commander throws on --help and on missing required args; treat as non-zero exit
|
|
179
|
+
if (process.exitCode === 0) process.exitCode = 1;
|
|
180
|
+
} finally {
|
|
181
|
+
process.stdout.write = originalStdoutWrite;
|
|
182
|
+
process.stderr.write = originalStderrWrite;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const exitCode = process.exitCode ?? 0;
|
|
186
|
+
process.exitCode = 0;
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
exitCode,
|
|
190
|
+
stdout: stdoutChunks.join(""),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Pre-populate mock stores with a credential.
|
|
196
|
+
*/
|
|
197
|
+
function seedCredential(
|
|
198
|
+
service: string,
|
|
199
|
+
field: string,
|
|
200
|
+
secret: string,
|
|
201
|
+
extra?: Partial<CredentialMetadata>,
|
|
202
|
+
): CredentialMetadata {
|
|
203
|
+
const now = Date.now();
|
|
204
|
+
const record: CredentialMetadata = {
|
|
205
|
+
credentialId: nextUUID(),
|
|
206
|
+
service,
|
|
207
|
+
field,
|
|
208
|
+
allowedTools: [],
|
|
209
|
+
allowedDomains: [],
|
|
210
|
+
createdAt: now,
|
|
211
|
+
updatedAt: now,
|
|
212
|
+
...extra,
|
|
213
|
+
};
|
|
214
|
+
metadataStore.push(record);
|
|
215
|
+
secureKeyStore.set(`credential:${service}:${field}`, secret);
|
|
216
|
+
return record;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Pre-populate mock stores with metadata only (no secret).
|
|
221
|
+
*/
|
|
222
|
+
function seedMetadataOnly(
|
|
223
|
+
service: string,
|
|
224
|
+
field: string,
|
|
225
|
+
extra?: Partial<CredentialMetadata>,
|
|
226
|
+
): CredentialMetadata {
|
|
227
|
+
const now = Date.now();
|
|
228
|
+
const record: CredentialMetadata = {
|
|
229
|
+
credentialId: nextUUID(),
|
|
230
|
+
service,
|
|
231
|
+
field,
|
|
232
|
+
allowedTools: [],
|
|
233
|
+
allowedDomains: [],
|
|
234
|
+
createdAt: now,
|
|
235
|
+
updatedAt: now,
|
|
236
|
+
...extra,
|
|
237
|
+
};
|
|
238
|
+
metadataStore.push(record);
|
|
239
|
+
return record;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Tests
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
describe("vellum credentials CLI", () => {
|
|
247
|
+
beforeEach(() => {
|
|
248
|
+
secureKeyStore = new Map();
|
|
249
|
+
metadataStore = [];
|
|
250
|
+
idCounter = 0;
|
|
251
|
+
_getSecureKeyCalls = 0;
|
|
252
|
+
_setSecureKeyCalls = 0;
|
|
253
|
+
_deleteSecureKeyCalls = 0;
|
|
254
|
+
_listMetadataCalls = 0;
|
|
255
|
+
_getMetadataCalls = 0;
|
|
256
|
+
_getMetadataByIdCalls = 0;
|
|
257
|
+
process.exitCode = 0;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// =========================================================================
|
|
261
|
+
// list
|
|
262
|
+
// =========================================================================
|
|
263
|
+
|
|
264
|
+
describe("list", () => {
|
|
265
|
+
test("returns empty array when no credentials exist", async () => {
|
|
266
|
+
const result = await runCli(["list", "--json"]);
|
|
267
|
+
expect(result.exitCode).toBe(0);
|
|
268
|
+
const parsed = JSON.parse(result.stdout);
|
|
269
|
+
expect(parsed).toEqual({ ok: true, credentials: [] });
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("returns all credentials with correct shapes", async () => {
|
|
273
|
+
seedCredential("twilio", "account_sid", "AC12345678abcdefgh");
|
|
274
|
+
seedCredential("twilio", "auth_token", "auth_secret_val");
|
|
275
|
+
seedCredential("github", "token", "ghp_abcdefghij1234");
|
|
276
|
+
|
|
277
|
+
const result = await runCli(["list", "--json"]);
|
|
278
|
+
expect(result.exitCode).toBe(0);
|
|
279
|
+
const parsed = JSON.parse(result.stdout);
|
|
280
|
+
expect(parsed.ok).toBe(true);
|
|
281
|
+
expect(parsed.credentials).toHaveLength(3);
|
|
282
|
+
|
|
283
|
+
for (const cred of parsed.credentials) {
|
|
284
|
+
expect(cred).toHaveProperty("ok", true);
|
|
285
|
+
expect(cred).toHaveProperty("service");
|
|
286
|
+
expect(cred).toHaveProperty("field");
|
|
287
|
+
expect(cred).toHaveProperty("credentialId");
|
|
288
|
+
expect(cred).toHaveProperty("scrubbedValue");
|
|
289
|
+
expect(cred).toHaveProperty("hasSecret");
|
|
290
|
+
expect(cred).toHaveProperty("alias");
|
|
291
|
+
expect(cred).toHaveProperty("usageDescription");
|
|
292
|
+
expect(cred).toHaveProperty("allowedTools");
|
|
293
|
+
expect(cred).toHaveProperty("allowedDomains");
|
|
294
|
+
expect(cred).toHaveProperty("accountInfo");
|
|
295
|
+
expect(cred).toHaveProperty("grantedScopes");
|
|
296
|
+
expect(cred).toHaveProperty("expiresAt");
|
|
297
|
+
expect(cred).toHaveProperty("createdAt");
|
|
298
|
+
expect(cred).toHaveProperty("updatedAt");
|
|
299
|
+
expect(cred).toHaveProperty("injectionTemplateCount");
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("filters by --search matching service name", async () => {
|
|
304
|
+
seedCredential("twilio", "account_sid", "AC123456789012");
|
|
305
|
+
seedCredential("twilio", "auth_token", "auth_secret_1234");
|
|
306
|
+
seedCredential("github", "token", "ghp_abcdefghij");
|
|
307
|
+
|
|
308
|
+
const result = await runCli(["list", "--search", "twilio", "--json"]);
|
|
309
|
+
expect(result.exitCode).toBe(0);
|
|
310
|
+
const parsed = JSON.parse(result.stdout);
|
|
311
|
+
expect(parsed.ok).toBe(true);
|
|
312
|
+
expect(parsed.credentials).toHaveLength(2);
|
|
313
|
+
expect(parsed.credentials[0].service).toBe("twilio");
|
|
314
|
+
expect(parsed.credentials[1].service).toBe("twilio");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("filters by --search matching alias/label", async () => {
|
|
318
|
+
seedCredential("twilio", "account_sid", "AC123456789012", {
|
|
319
|
+
alias: "prod",
|
|
320
|
+
});
|
|
321
|
+
seedCredential("github", "token", "ghp_abcdefghij");
|
|
322
|
+
|
|
323
|
+
const result = await runCli(["list", "--search", "prod", "--json"]);
|
|
324
|
+
expect(result.exitCode).toBe(0);
|
|
325
|
+
const parsed = JSON.parse(result.stdout);
|
|
326
|
+
expect(parsed.ok).toBe(true);
|
|
327
|
+
expect(parsed.credentials).toHaveLength(1);
|
|
328
|
+
expect(parsed.credentials[0].service).toBe("twilio");
|
|
329
|
+
expect(parsed.credentials[0].alias).toBe("prod");
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("filters by --search matching field name", async () => {
|
|
333
|
+
seedCredential("twilio", "account_sid", "AC123456789012");
|
|
334
|
+
seedCredential("slack", "bot_token", "xoxb-1234567890");
|
|
335
|
+
seedCredential("github", "token", "ghp_abcdefghij");
|
|
336
|
+
|
|
337
|
+
const result = await runCli(["list", "--search", "bot_token", "--json"]);
|
|
338
|
+
expect(result.exitCode).toBe(0);
|
|
339
|
+
const parsed = JSON.parse(result.stdout);
|
|
340
|
+
expect(parsed.ok).toBe(true);
|
|
341
|
+
expect(parsed.credentials).toHaveLength(1);
|
|
342
|
+
expect(parsed.credentials[0].field).toBe("bot_token");
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("filters by --search matching description", async () => {
|
|
346
|
+
seedCredential("fal", "api_key", "key_live_abc123456", {
|
|
347
|
+
usageDescription: "Image generation",
|
|
348
|
+
});
|
|
349
|
+
seedCredential("github", "token", "ghp_abcdefghij");
|
|
350
|
+
|
|
351
|
+
const result = await runCli(["list", "--search", "image", "--json"]);
|
|
352
|
+
expect(result.exitCode).toBe(0);
|
|
353
|
+
const parsed = JSON.parse(result.stdout);
|
|
354
|
+
expect(parsed.ok).toBe(true);
|
|
355
|
+
expect(parsed.credentials).toHaveLength(1);
|
|
356
|
+
expect(parsed.credentials[0].service).toBe("fal");
|
|
357
|
+
expect(parsed.credentials[0].usageDescription).toBe("Image generation");
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("returns empty array when --search has no matches", async () => {
|
|
361
|
+
seedCredential("twilio", "account_sid", "AC123456789012");
|
|
362
|
+
seedCredential("github", "token", "ghp_abcdefghij");
|
|
363
|
+
|
|
364
|
+
const result = await runCli([
|
|
365
|
+
"list",
|
|
366
|
+
"--search",
|
|
367
|
+
"nonexistent",
|
|
368
|
+
"--json",
|
|
369
|
+
]);
|
|
370
|
+
expect(result.exitCode).toBe(0);
|
|
371
|
+
const parsed = JSON.parse(result.stdout);
|
|
372
|
+
expect(parsed).toEqual({ ok: true, credentials: [] });
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("list items have the same shape as inspect output", async () => {
|
|
376
|
+
seedCredential("twilio", "account_sid", "AC123456789012");
|
|
377
|
+
|
|
378
|
+
const listResult = await runCli(["list", "--json"]);
|
|
379
|
+
const listParsed = JSON.parse(listResult.stdout);
|
|
380
|
+
const listItem = listParsed.credentials[0];
|
|
381
|
+
|
|
382
|
+
const inspectResult = await runCli([
|
|
383
|
+
"inspect",
|
|
384
|
+
"twilio:account_sid",
|
|
385
|
+
"--json",
|
|
386
|
+
]);
|
|
387
|
+
const inspectParsed = JSON.parse(inspectResult.stdout);
|
|
388
|
+
|
|
389
|
+
const listKeys = Object.keys(listItem).sort();
|
|
390
|
+
const inspectKeys = Object.keys(inspectParsed).sort();
|
|
391
|
+
expect(listKeys).toEqual(inspectKeys);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// =========================================================================
|
|
396
|
+
// set
|
|
397
|
+
// =========================================================================
|
|
398
|
+
|
|
399
|
+
describe("set", () => {
|
|
400
|
+
test("stores secret and creates metadata", async () => {
|
|
401
|
+
const result = await runCli([
|
|
402
|
+
"set",
|
|
403
|
+
"twilio:account_sid",
|
|
404
|
+
"AC1234567890",
|
|
405
|
+
"--json",
|
|
406
|
+
]);
|
|
407
|
+
expect(result.exitCode).toBe(0);
|
|
408
|
+
const parsed = JSON.parse(result.stdout);
|
|
409
|
+
expect(parsed.ok).toBe(true);
|
|
410
|
+
expect(parsed.service).toBe("twilio");
|
|
411
|
+
expect(parsed.field).toBe("account_sid");
|
|
412
|
+
expect(parsed.credentialId).toBeTruthy();
|
|
413
|
+
|
|
414
|
+
// Verify secret stored in mock map
|
|
415
|
+
expect(secureKeyStore.get("credential:twilio:account_sid")).toBe(
|
|
416
|
+
"AC1234567890",
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
// Verify metadata created
|
|
420
|
+
const meta = metadataStore.find(
|
|
421
|
+
(m) => m.service === "twilio" && m.field === "account_sid",
|
|
422
|
+
);
|
|
423
|
+
expect(meta).toBeTruthy();
|
|
424
|
+
expect(meta!.service).toBe("twilio");
|
|
425
|
+
expect(meta!.field).toBe("account_sid");
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("stores metadata with --label and --description", async () => {
|
|
429
|
+
const result = await runCli([
|
|
430
|
+
"set",
|
|
431
|
+
"fal:api_key",
|
|
432
|
+
"key_live_abc",
|
|
433
|
+
"--label",
|
|
434
|
+
"fal-prod",
|
|
435
|
+
"--description",
|
|
436
|
+
"Image generation",
|
|
437
|
+
"--json",
|
|
438
|
+
]);
|
|
439
|
+
expect(result.exitCode).toBe(0);
|
|
440
|
+
const parsed = JSON.parse(result.stdout);
|
|
441
|
+
expect(parsed.ok).toBe(true);
|
|
442
|
+
|
|
443
|
+
const meta = metadataStore.find(
|
|
444
|
+
(m) => m.service === "fal" && m.field === "api_key",
|
|
445
|
+
);
|
|
446
|
+
expect(meta).toBeTruthy();
|
|
447
|
+
expect(meta!.alias).toBe("fal-prod");
|
|
448
|
+
expect(meta!.usageDescription).toBe("Image generation");
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("rejects invalid name without colon", async () => {
|
|
452
|
+
const result = await runCli([
|
|
453
|
+
"set",
|
|
454
|
+
"invalid_name",
|
|
455
|
+
"some_value",
|
|
456
|
+
"--json",
|
|
457
|
+
]);
|
|
458
|
+
expect(result.exitCode).toBe(1);
|
|
459
|
+
const parsed = JSON.parse(result.stdout);
|
|
460
|
+
expect(parsed.ok).toBe(false);
|
|
461
|
+
expect(parsed.error).toContain("Invalid credential name");
|
|
462
|
+
expect(parsed.error).toContain("service:field");
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("rejects name with leading colon", async () => {
|
|
466
|
+
const result = await runCli([
|
|
467
|
+
"set",
|
|
468
|
+
":field_only",
|
|
469
|
+
"some_value",
|
|
470
|
+
"--json",
|
|
471
|
+
]);
|
|
472
|
+
expect(result.exitCode).toBe(1);
|
|
473
|
+
const parsed = JSON.parse(result.stdout);
|
|
474
|
+
expect(parsed.ok).toBe(false);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test("errors when value argument is missing", async () => {
|
|
478
|
+
const result = await runCli(["set", "twilio:account_sid", "--json"]);
|
|
479
|
+
// Commander should error on missing required arg
|
|
480
|
+
expect(result.exitCode).not.toBe(0);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("stores metadata with --allowed-tools", async () => {
|
|
484
|
+
const result = await runCli([
|
|
485
|
+
"set",
|
|
486
|
+
"twilio:auth_token",
|
|
487
|
+
"sometoken",
|
|
488
|
+
"--allowed-tools",
|
|
489
|
+
"bash,host_bash",
|
|
490
|
+
"--json",
|
|
491
|
+
]);
|
|
492
|
+
expect(result.exitCode).toBe(0);
|
|
493
|
+
const parsed = JSON.parse(result.stdout);
|
|
494
|
+
expect(parsed.ok).toBe(true);
|
|
495
|
+
|
|
496
|
+
const meta = metadataStore.find(
|
|
497
|
+
(m) => m.service === "twilio" && m.field === "auth_token",
|
|
498
|
+
);
|
|
499
|
+
expect(meta).toBeTruthy();
|
|
500
|
+
expect(meta!.allowedTools).toEqual(["bash", "host_bash"]);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("updates existing credential on second set", async () => {
|
|
504
|
+
// First set
|
|
505
|
+
await runCli(["set", "twilio:account_sid", "original_value", "--json"]);
|
|
506
|
+
const meta1 = metadataStore.find(
|
|
507
|
+
(m) => m.service === "twilio" && m.field === "account_sid",
|
|
508
|
+
);
|
|
509
|
+
expect(meta1).toBeTruthy();
|
|
510
|
+
const firstUpdatedAt = meta1!.updatedAt;
|
|
511
|
+
|
|
512
|
+
// Small delay to ensure timestamp differs
|
|
513
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
514
|
+
|
|
515
|
+
// Second set
|
|
516
|
+
await runCli(["set", "twilio:account_sid", "new_value", "--json"]);
|
|
517
|
+
const meta2 = metadataStore.find(
|
|
518
|
+
(m) => m.service === "twilio" && m.field === "account_sid",
|
|
519
|
+
);
|
|
520
|
+
expect(meta2).toBeTruthy();
|
|
521
|
+
expect(meta2!.updatedAt).toBeGreaterThan(firstUpdatedAt);
|
|
522
|
+
|
|
523
|
+
// Verify secret is overwritten
|
|
524
|
+
expect(secureKeyStore.get("credential:twilio:account_sid")).toBe(
|
|
525
|
+
"new_value",
|
|
526
|
+
);
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// =========================================================================
|
|
531
|
+
// delete
|
|
532
|
+
// =========================================================================
|
|
533
|
+
|
|
534
|
+
describe("delete", () => {
|
|
535
|
+
test("removes both secret and metadata", async () => {
|
|
536
|
+
seedCredential("twilio", "auth_token", "secret_value_here");
|
|
537
|
+
|
|
538
|
+
const result = await runCli(["delete", "twilio:auth_token", "--json"]);
|
|
539
|
+
expect(result.exitCode).toBe(0);
|
|
540
|
+
const parsed = JSON.parse(result.stdout);
|
|
541
|
+
expect(parsed.ok).toBe(true);
|
|
542
|
+
expect(parsed.service).toBe("twilio");
|
|
543
|
+
expect(parsed.field).toBe("auth_token");
|
|
544
|
+
|
|
545
|
+
// Verify both removed
|
|
546
|
+
expect(secureKeyStore.has("credential:twilio:auth_token")).toBe(false);
|
|
547
|
+
expect(
|
|
548
|
+
metadataStore.find(
|
|
549
|
+
(m) => m.service === "twilio" && m.field === "auth_token",
|
|
550
|
+
),
|
|
551
|
+
).toBeUndefined();
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test("errors on nonexistent credential", async () => {
|
|
555
|
+
const result = await runCli(["delete", "twilio:nonexistent", "--json"]);
|
|
556
|
+
expect(result.exitCode).toBe(1);
|
|
557
|
+
const parsed = JSON.parse(result.stdout);
|
|
558
|
+
expect(parsed.ok).toBe(false);
|
|
559
|
+
expect(parsed.error).toContain("not found");
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test("rejects invalid name without colon", async () => {
|
|
563
|
+
const result = await runCli(["delete", "badname", "--json"]);
|
|
564
|
+
expect(result.exitCode).toBe(1);
|
|
565
|
+
const parsed = JSON.parse(result.stdout);
|
|
566
|
+
expect(parsed.ok).toBe(false);
|
|
567
|
+
expect(parsed.error).toContain("Invalid credential name");
|
|
568
|
+
expect(parsed.error).toContain("service:field");
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
test("succeeds when only metadata exists (no secret)", async () => {
|
|
572
|
+
seedMetadataOnly("twilio", "auth_token");
|
|
573
|
+
|
|
574
|
+
const result = await runCli(["delete", "twilio:auth_token", "--json"]);
|
|
575
|
+
expect(result.exitCode).toBe(0);
|
|
576
|
+
const parsed = JSON.parse(result.stdout);
|
|
577
|
+
expect(parsed.ok).toBe(true);
|
|
578
|
+
|
|
579
|
+
// Verify metadata removed
|
|
580
|
+
expect(
|
|
581
|
+
metadataStore.find(
|
|
582
|
+
(m) => m.service === "twilio" && m.field === "auth_token",
|
|
583
|
+
),
|
|
584
|
+
).toBeUndefined();
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// =========================================================================
|
|
589
|
+
// inspect
|
|
590
|
+
// =========================================================================
|
|
591
|
+
|
|
592
|
+
describe("inspect", () => {
|
|
593
|
+
test("shows metadata and scrubbed value by service:field", async () => {
|
|
594
|
+
const meta = seedCredential("twilio", "account_sid", "AC123456789012");
|
|
595
|
+
|
|
596
|
+
const result = await runCli(["inspect", "twilio:account_sid", "--json"]);
|
|
597
|
+
expect(result.exitCode).toBe(0);
|
|
598
|
+
const parsed = JSON.parse(result.stdout);
|
|
599
|
+
expect(parsed.ok).toBe(true);
|
|
600
|
+
expect(parsed.service).toBe("twilio");
|
|
601
|
+
expect(parsed.field).toBe("account_sid");
|
|
602
|
+
expect(parsed.credentialId).toBe(meta.credentialId);
|
|
603
|
+
expect(parsed.scrubbedValue).toBe("****9012");
|
|
604
|
+
expect(parsed.hasSecret).toBe(true);
|
|
605
|
+
expect(parsed.createdAt).toBe(new Date(meta.createdAt).toISOString());
|
|
606
|
+
expect(parsed.updatedAt).toBe(new Date(meta.updatedAt).toISOString());
|
|
607
|
+
expect(parsed).toHaveProperty("alias");
|
|
608
|
+
expect(parsed).toHaveProperty("usageDescription");
|
|
609
|
+
expect(parsed).toHaveProperty("allowedTools");
|
|
610
|
+
expect(parsed).toHaveProperty("allowedDomains");
|
|
611
|
+
expect(parsed).toHaveProperty("accountInfo");
|
|
612
|
+
expect(parsed).toHaveProperty("grantedScopes");
|
|
613
|
+
expect(parsed).toHaveProperty("expiresAt");
|
|
614
|
+
expect(parsed).toHaveProperty("injectionTemplateCount");
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test("looks up credential by UUID", async () => {
|
|
618
|
+
const meta = seedCredential("github", "token", "ghp_abcdefghij1234");
|
|
619
|
+
|
|
620
|
+
const result = await runCli(["inspect", meta.credentialId, "--json"]);
|
|
621
|
+
expect(result.exitCode).toBe(0);
|
|
622
|
+
const parsed = JSON.parse(result.stdout);
|
|
623
|
+
expect(parsed.ok).toBe(true);
|
|
624
|
+
expect(parsed.service).toBe("github");
|
|
625
|
+
expect(parsed.field).toBe("token");
|
|
626
|
+
expect(parsed.credentialId).toBe(meta.credentialId);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
test("scrubs normal-length secret (>4 chars): shows last 4", async () => {
|
|
630
|
+
seedCredential("test", "normal", "abcdefgh");
|
|
631
|
+
|
|
632
|
+
const result = await runCli(["inspect", "test:normal", "--json"]);
|
|
633
|
+
const parsed = JSON.parse(result.stdout);
|
|
634
|
+
expect(parsed.scrubbedValue).toBe("****efgh");
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
test("scrubs short secret (<=4 chars): shows only ****", async () => {
|
|
638
|
+
seedCredential("test", "short", "ab");
|
|
639
|
+
|
|
640
|
+
const result = await runCli(["inspect", "test:short", "--json"]);
|
|
641
|
+
const parsed = JSON.parse(result.stdout);
|
|
642
|
+
expect(parsed.scrubbedValue).toBe("****");
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
test("shows (not set) when no secret exists", async () => {
|
|
646
|
+
seedMetadataOnly("test", "nosecret");
|
|
647
|
+
|
|
648
|
+
const result = await runCli(["inspect", "test:nosecret", "--json"]);
|
|
649
|
+
const parsed = JSON.parse(result.stdout);
|
|
650
|
+
expect(parsed.ok).toBe(true);
|
|
651
|
+
expect(parsed.scrubbedValue).toBe("(not set)");
|
|
652
|
+
expect(parsed.hasSecret).toBe(false);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
test("rejects invalid name without colon", async () => {
|
|
656
|
+
const result = await runCli(["inspect", "badname", "--json"]);
|
|
657
|
+
expect(result.exitCode).toBe(1);
|
|
658
|
+
const parsed = JSON.parse(result.stdout);
|
|
659
|
+
expect(parsed.ok).toBe(false);
|
|
660
|
+
expect(parsed.error).toContain("not found");
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
test("rejects name with leading colon", async () => {
|
|
664
|
+
const result = await runCli(["inspect", ":field_only", "--json"]);
|
|
665
|
+
expect(result.exitCode).toBe(1);
|
|
666
|
+
const parsed = JSON.parse(result.stdout);
|
|
667
|
+
expect(parsed.ok).toBe(false);
|
|
668
|
+
expect(parsed.error).toContain("Invalid credential name");
|
|
669
|
+
expect(parsed.error).toContain("service:field");
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test("errors on nonexistent credential", async () => {
|
|
673
|
+
const result = await runCli(["inspect", "nonexistent:field", "--json"]);
|
|
674
|
+
expect(result.exitCode).toBe(1);
|
|
675
|
+
const parsed = JSON.parse(result.stdout);
|
|
676
|
+
expect(parsed.ok).toBe(false);
|
|
677
|
+
expect(parsed.error).toContain("not found");
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
test("--json flag produces compact JSON (single line)", async () => {
|
|
681
|
+
seedCredential("twilio", "account_sid", "AC123456789012");
|
|
682
|
+
|
|
683
|
+
const result = await runCli(["inspect", "twilio:account_sid", "--json"]);
|
|
684
|
+
const lines = result.stdout.trim().split("\n");
|
|
685
|
+
expect(lines).toHaveLength(1);
|
|
686
|
+
// Verify it parses as valid JSON
|
|
687
|
+
expect(() => JSON.parse(lines[0])).not.toThrow();
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
test("shows hasSecret: false when metadata exists but no secret", async () => {
|
|
691
|
+
seedMetadataOnly("test", "metaonly");
|
|
692
|
+
|
|
693
|
+
const result = await runCli(["inspect", "test:metaonly", "--json"]);
|
|
694
|
+
const parsed = JSON.parse(result.stdout);
|
|
695
|
+
expect(parsed.ok).toBe(true);
|
|
696
|
+
expect(parsed.hasSecret).toBe(false);
|
|
697
|
+
expect(parsed.scrubbedValue).toBe("(not set)");
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// =========================================================================
|
|
702
|
+
// reveal
|
|
703
|
+
// =========================================================================
|
|
704
|
+
|
|
705
|
+
describe("reveal", () => {
|
|
706
|
+
test("returns plaintext value by service:field", async () => {
|
|
707
|
+
seedCredential("twilio", "account_sid", "AC123456789012");
|
|
708
|
+
|
|
709
|
+
const result = await runCli(["reveal", "twilio:account_sid", "--json"]);
|
|
710
|
+
expect(result.exitCode).toBe(0);
|
|
711
|
+
const parsed = JSON.parse(result.stdout);
|
|
712
|
+
expect(parsed.ok).toBe(true);
|
|
713
|
+
expect(parsed.value).toBe("AC123456789012");
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
test("returns plaintext value by UUID", async () => {
|
|
717
|
+
const meta = seedCredential("github", "token", "ghp_abcdefghij1234");
|
|
718
|
+
|
|
719
|
+
const result = await runCli(["reveal", meta.credentialId, "--json"]);
|
|
720
|
+
expect(result.exitCode).toBe(0);
|
|
721
|
+
const parsed = JSON.parse(result.stdout);
|
|
722
|
+
expect(parsed.ok).toBe(true);
|
|
723
|
+
expect(parsed.value).toBe("ghp_abcdefghij1234");
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
test("errors on nonexistent credential", async () => {
|
|
727
|
+
const result = await runCli(["reveal", "nonexistent:field", "--json"]);
|
|
728
|
+
expect(result.exitCode).toBe(1);
|
|
729
|
+
const parsed = JSON.parse(result.stdout);
|
|
730
|
+
expect(parsed.ok).toBe(false);
|
|
731
|
+
expect(parsed.error).toContain("not found");
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
test("errors on nonexistent UUID", async () => {
|
|
735
|
+
const result = await runCli([
|
|
736
|
+
"reveal",
|
|
737
|
+
"00000000-0000-0000-0000-000000000099",
|
|
738
|
+
"--json",
|
|
739
|
+
]);
|
|
740
|
+
expect(result.exitCode).toBe(1);
|
|
741
|
+
const parsed = JSON.parse(result.stdout);
|
|
742
|
+
expect(parsed.ok).toBe(false);
|
|
743
|
+
expect(parsed.error).toContain("not found");
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
test("rejects invalid name without colon", async () => {
|
|
747
|
+
const result = await runCli(["reveal", ":field_only", "--json"]);
|
|
748
|
+
expect(result.exitCode).toBe(1);
|
|
749
|
+
const parsed = JSON.parse(result.stdout);
|
|
750
|
+
expect(parsed.ok).toBe(false);
|
|
751
|
+
expect(parsed.error).toContain("Invalid credential name");
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
test("reveal in human mode emits bare secret with trailing newline", async () => {
|
|
755
|
+
seedCredential("twilio", "auth_token", "secret_xyz_789");
|
|
756
|
+
|
|
757
|
+
const result = await runCli(["reveal", "twilio:auth_token"]);
|
|
758
|
+
expect(result.exitCode).toBe(0);
|
|
759
|
+
expect(result.stdout).toBe("secret_xyz_789\n");
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test("errors when metadata exists but no secret stored", async () => {
|
|
763
|
+
seedMetadataOnly("test", "nosecret");
|
|
764
|
+
|
|
765
|
+
const result = await runCli(["reveal", "test:nosecret", "--json"]);
|
|
766
|
+
expect(result.exitCode).toBe(1);
|
|
767
|
+
const parsed = JSON.parse(result.stdout);
|
|
768
|
+
expect(parsed.ok).toBe(false);
|
|
769
|
+
expect(parsed.error).toContain("not found");
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
// =========================================================================
|
|
774
|
+
// help text quality
|
|
775
|
+
// =========================================================================
|
|
776
|
+
|
|
777
|
+
describe("help text", () => {
|
|
778
|
+
test("credentials --help contains naming convention table and storage description", async () => {
|
|
779
|
+
const result = await runCli(["--help"]);
|
|
780
|
+
const out = result.stdout;
|
|
781
|
+
expect(out).toContain("twilio:account_sid");
|
|
782
|
+
expect(out).toContain("AES-256-GCM");
|
|
783
|
+
expect(out).toContain("Examples:");
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
test("credentials list --help contains --search description and examples", async () => {
|
|
787
|
+
const result = await runCli(["list", "--help"]);
|
|
788
|
+
const out = result.stdout;
|
|
789
|
+
expect(out).toContain("--search");
|
|
790
|
+
expect(out).toContain("Examples:");
|
|
791
|
+
expect(out).toContain("credentials list --search twilio");
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
test("credentials set --help contains Arguments: and Examples: sections", async () => {
|
|
795
|
+
const result = await runCli(["set", "--help"]);
|
|
796
|
+
const out = result.stdout;
|
|
797
|
+
expect(out).toContain("Arguments:");
|
|
798
|
+
expect(out).toContain("Examples:");
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
test("credentials inspect --help mentions UUID support", async () => {
|
|
802
|
+
const result = await runCli(["inspect", "--help"]);
|
|
803
|
+
const out = result.stdout;
|
|
804
|
+
expect(out).toContain("UUID");
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
test("credentials reveal --help mentions piping and examples", async () => {
|
|
808
|
+
const result = await runCli(["reveal", "--help"]);
|
|
809
|
+
const out = result.stdout;
|
|
810
|
+
expect(out).toContain("stdout");
|
|
811
|
+
expect(out).toContain("Examples:");
|
|
812
|
+
});
|
|
813
|
+
});
|
|
814
|
+
});
|