@vellumai/assistant 0.4.46 → 0.4.48
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/ARCHITECTURE.md +5 -5
- package/docs/architecture/security.md +5 -5
- package/package.json +1 -1
- package/src/__tests__/browser-fill-credential.test.ts +5 -2
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
- package/src/__tests__/channel-readiness-routes.test.ts +20 -19
- package/src/__tests__/cli.test.ts +23 -0
- package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
- package/src/__tests__/credential-broker-server-use.test.ts +22 -21
- package/src/__tests__/credential-broker.test.ts +2 -1
- package/src/__tests__/credential-metadata-store.test.ts +240 -18
- package/src/__tests__/credential-resolve.test.ts +5 -4
- package/src/__tests__/credential-security-e2e.test.ts +8 -8
- package/src/__tests__/credential-security-invariants.test.ts +104 -6
- package/src/__tests__/credential-vault-unit.test.ts +22 -20
- package/src/__tests__/credential-vault.test.ts +284 -12
- package/src/__tests__/credentials-cli.test.ts +11 -6
- package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
- package/src/__tests__/gemini-image-service.test.ts +75 -45
- package/src/__tests__/gemini-provider.test.ts +9 -6
- package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
- package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
- package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
- package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
- package/src/__tests__/guardian-grant-minting.test.ts +35 -0
- package/src/__tests__/integration-status.test.ts +53 -21
- package/src/__tests__/managed-proxy-context.test.ts +5 -3
- package/src/__tests__/media-generate-image.test.ts +63 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
- package/src/__tests__/messaging-send-tool.test.ts +4 -6
- package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
- package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
- package/src/__tests__/schema-transforms.test.ts +226 -0
- package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
- package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +5 -3
- package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
- package/src/__tests__/skills-uninstall.test.ts +2 -2
- package/src/__tests__/skills.test.ts +0 -9
- package/src/__tests__/slack-channel-config.test.ts +9 -8
- package/src/__tests__/slack-share-routes.test.ts +11 -6
- package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
- package/src/__tests__/twilio-config.test.ts +2 -1
- package/src/__tests__/twilio-provider.test.ts +4 -2
- package/src/__tests__/twilio-routes.test.ts +5 -4
- package/src/calls/call-domain.ts +7 -4
- package/src/calls/twilio-config.ts +2 -1
- package/src/calls/twilio-provider.ts +2 -1
- package/src/calls/twilio-rest.ts +2 -1
- package/src/cli/commands/browser-relay.ts +40 -15
- package/src/cli/commands/credentials.ts +9 -8
- package/src/cli/commands/oauth.ts +1 -1
- package/src/cli.ts +3 -2
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
- package/src/config/bundled-skills/gmail/SKILL.md +4 -4
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
- package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +70 -29
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
- package/src/config/bundled-skills/messaging/SKILL.md +6 -6
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
- package/src/config/bundled-skills/messaging/tools/shared.ts +11 -11
- package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
- package/src/config/loader.ts +6 -0
- package/src/daemon/computer-use-session.ts +7 -1
- package/src/daemon/guardian-action-generators.ts +4 -5
- package/src/daemon/handlers/config-slack-channel.ts +37 -20
- package/src/daemon/handlers/config-telegram.ts +33 -20
- package/src/daemon/lifecycle.ts +9 -1
- package/src/daemon/message-types/integrations.ts +1 -0
- package/src/daemon/ride-shotgun-handler.ts +3 -1
- package/src/daemon/session-messaging.ts +3 -1
- package/src/daemon/session-tool-setup.ts +18 -2
- package/src/daemon/session.ts +1 -1
- package/src/email/providers/index.ts +2 -1
- package/src/instrument.ts +15 -1
- package/src/media/app-icon-generator.ts +30 -4
- package/src/media/avatar-router.ts +26 -3
- package/src/media/gemini-image-service.ts +28 -2
- package/src/memory/guardian-action-store.ts +1 -1
- package/src/memory/schema/guardian.ts +1 -1
- package/src/messaging/provider.ts +16 -10
- package/src/messaging/providers/gmail/adapter.ts +40 -23
- package/src/messaging/providers/gmail/client.ts +203 -122
- package/src/messaging/providers/gmail/people-client.ts +26 -18
- package/src/messaging/providers/slack/adapter.ts +29 -19
- package/src/messaging/providers/slack/client.ts +265 -78
- package/src/messaging/providers/telegram-bot/adapter.ts +5 -4
- package/src/messaging/providers/whatsapp/adapter.ts +6 -3
- package/src/messaging/registry.ts +2 -1
- package/src/oauth/byo-connection.test.ts +436 -0
- package/src/oauth/byo-connection.ts +112 -0
- package/src/oauth/connect-orchestrator.ts +27 -0
- package/src/oauth/connection-resolver.ts +34 -0
- package/src/oauth/connection.ts +38 -0
- package/src/oauth/platform-connection.test.ts +163 -0
- package/src/oauth/platform-connection.ts +110 -0
- package/src/oauth/provider-base-urls.ts +21 -0
- package/src/oauth/provider-profiles.ts +1 -1
- package/src/oauth/token-persistence.ts +20 -20
- package/src/permissions/checker.ts +5 -1
- package/src/prompts/system-prompt.ts +49 -12
- package/src/providers/gemini/client.ts +15 -6
- package/src/providers/managed-proxy/constants.ts +2 -2
- package/src/providers/managed-proxy/context.ts +5 -1
- package/src/providers/ratelimit.ts +17 -0
- package/src/providers/registry.ts +2 -2
- package/src/runtime/AGENTS.md +17 -0
- package/src/runtime/channel-invite-transports/telegram.ts +2 -1
- package/src/runtime/channel-readiness-service.ts +168 -195
- package/src/runtime/channel-readiness-types.ts +4 -0
- package/src/runtime/guardian-action-conversation-turn.ts +1 -3
- package/src/runtime/guardian-action-followup-executor.ts +1 -1
- package/src/runtime/guardian-action-message-composer.ts +3 -23
- package/src/runtime/http-server.ts +9 -4
- package/src/runtime/http-types.ts +0 -1
- package/src/runtime/middleware/rate-limiter.ts +74 -20
- package/src/runtime/routes/channel-readiness-routes.ts +2 -0
- package/src/runtime/routes/diagnostics-routes.ts +11 -9
- package/src/runtime/routes/guardian-approval-interception.ts +20 -5
- package/src/runtime/routes/integrations/slack/share.ts +3 -2
- package/src/runtime/routes/integrations/twilio.ts +6 -5
- package/src/runtime/routes/secret-routes.ts +3 -2
- package/src/runtime/routes/settings-routes.ts +75 -17
- package/src/runtime/telegram-streaming-delivery.test.ts +132 -0
- package/src/runtime/telegram-streaming-delivery.ts +11 -1
- package/src/schedule/integration-status.ts +5 -4
- package/src/security/credential-key.ts +170 -0
- package/src/security/token-manager.ts +36 -7
- package/src/tools/apps/definitions.ts +0 -5
- package/src/tools/assets/materialize.ts +0 -5
- package/src/tools/assets/search.ts +0 -5
- package/src/tools/browser/headless-browser.ts +1 -67
- package/src/tools/claude-code/claude-code.ts +0 -5
- package/src/tools/computer-use/request-computer-control.ts +0 -5
- package/src/tools/credentials/broker.ts +6 -4
- package/src/tools/credentials/metadata-store.ts +72 -20
- package/src/tools/credentials/resolve.ts +2 -1
- package/src/tools/credentials/vault.ts +77 -16
- package/src/tools/filesystem/edit.ts +1 -6
- package/src/tools/filesystem/read.ts +0 -5
- package/src/tools/filesystem/write.ts +1 -6
- package/src/tools/host-filesystem/edit.ts +1 -6
- package/src/tools/host-filesystem/read.ts +1 -6
- package/src/tools/host-filesystem/write.ts +1 -6
- package/src/tools/mcp/mcp-tool-factory.ts +18 -1
- package/src/tools/memory/definitions.ts +0 -5
- package/src/tools/network/web-fetch.ts +0 -5
- package/src/tools/network/web-search.ts +0 -5
- package/src/tools/schema-transforms.ts +99 -0
- package/src/tools/skills/load.ts +0 -5
- package/src/tools/swarm/delegate.ts +0 -5
- package/src/tools/system/avatar-generator.ts +0 -5
- package/src/tools/ui-surface/definitions.ts +0 -15
- package/src/tools/watch/screen-watch.ts +0 -5
- package/src/version.ts +10 -0
- package/src/watcher/providers/github.ts +51 -52
- package/src/watcher/providers/gmail.ts +88 -80
- package/src/watcher/providers/google-calendar.ts +93 -86
- package/src/watcher/providers/linear.ts +87 -93
|
@@ -294,6 +294,52 @@ describe("credential metadata store", () => {
|
|
|
294
294
|
});
|
|
295
295
|
});
|
|
296
296
|
|
|
297
|
+
// ── v4 Schema: hasRefreshToken ──────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
describe("v4 schema — hasRefreshToken", () => {
|
|
300
|
+
test("creates record with hasRefreshToken", () => {
|
|
301
|
+
const record = upsertCredentialMetadata("github", "access_token", {
|
|
302
|
+
hasRefreshToken: true,
|
|
303
|
+
});
|
|
304
|
+
expect(record.hasRefreshToken).toBe(true);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("creates record with hasRefreshToken false", () => {
|
|
308
|
+
const record = upsertCredentialMetadata("github", "access_token", {
|
|
309
|
+
hasRefreshToken: false,
|
|
310
|
+
});
|
|
311
|
+
expect(record.hasRefreshToken).toBe(false);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("defaults hasRefreshToken to undefined when not provided", () => {
|
|
315
|
+
const record = upsertCredentialMetadata("github", "access_token");
|
|
316
|
+
expect(record.hasRefreshToken).toBeUndefined();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("updates hasRefreshToken on existing record", () => {
|
|
320
|
+
upsertCredentialMetadata("github", "access_token", {
|
|
321
|
+
hasRefreshToken: false,
|
|
322
|
+
});
|
|
323
|
+
const updated = upsertCredentialMetadata("github", "access_token", {
|
|
324
|
+
hasRefreshToken: true,
|
|
325
|
+
});
|
|
326
|
+
expect(updated.hasRefreshToken).toBe(true);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("round-trip: hasRefreshToken survives serialization", () => {
|
|
330
|
+
upsertCredentialMetadata("github", "access_token", {
|
|
331
|
+
hasRefreshToken: true,
|
|
332
|
+
allowedTools: ["api_request"],
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Re-read from disk
|
|
336
|
+
const loaded = getCredentialMetadata("github", "access_token");
|
|
337
|
+
expect(loaded).toBeDefined();
|
|
338
|
+
expect(loaded!.hasRefreshToken).toBe(true);
|
|
339
|
+
expect(loaded!.allowedTools).toEqual(["api_request"]);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
297
343
|
// ── Version migration ──────────────────────────────────────────────
|
|
298
344
|
|
|
299
345
|
describe("version migration", () => {
|
|
@@ -371,7 +417,7 @@ describe("credential metadata store", () => {
|
|
|
371
417
|
expect(record!.credentialId).toBe("cred-stable-id");
|
|
372
418
|
});
|
|
373
419
|
|
|
374
|
-
test("v2 file is
|
|
420
|
+
test("v2 file is migrated to v4 (strips oauth2ClientSecret)", () => {
|
|
375
421
|
const v2Data = {
|
|
376
422
|
version: 2,
|
|
377
423
|
credentials: [
|
|
@@ -382,6 +428,7 @@ describe("credential metadata store", () => {
|
|
|
382
428
|
allowedTools: ["api_request"],
|
|
383
429
|
allowedDomains: ["fal.ai"],
|
|
384
430
|
alias: "fal-primary",
|
|
431
|
+
oauth2ClientSecret: "should-be-stripped",
|
|
385
432
|
injectionTemplates: [
|
|
386
433
|
{
|
|
387
434
|
hostPattern: "*.fal.ai",
|
|
@@ -402,9 +449,184 @@ describe("credential metadata store", () => {
|
|
|
402
449
|
expect(record!.alias).toBe("fal-primary");
|
|
403
450
|
expect(record!.injectionTemplates).toHaveLength(1);
|
|
404
451
|
expect(record!.injectionTemplates![0].hostPattern).toBe("*.fal.ai");
|
|
452
|
+
// oauth2ClientSecret must be stripped by v2→v3 migration
|
|
453
|
+
expect("oauth2ClientSecret" in record!).toBe(false);
|
|
454
|
+
|
|
455
|
+
// On-disk file should be upgraded to v4
|
|
456
|
+
const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
|
|
457
|
+
expect(raw.version).toBe(4);
|
|
458
|
+
expect(raw.credentials[0]).not.toHaveProperty("oauth2ClientSecret");
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("v3 file is migrated to v4 (removes ghost refresh_token records)", () => {
|
|
462
|
+
const v3Data = {
|
|
463
|
+
version: 3,
|
|
464
|
+
credentials: [
|
|
465
|
+
{
|
|
466
|
+
credentialId: "cred-v3-at",
|
|
467
|
+
service: "github",
|
|
468
|
+
field: "access_token",
|
|
469
|
+
allowedTools: ["api_request"],
|
|
470
|
+
allowedDomains: ["github.com"],
|
|
471
|
+
createdAt: 1700000000000,
|
|
472
|
+
updatedAt: 1700000000000,
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
credentialId: "cred-v3-rt",
|
|
476
|
+
service: "github",
|
|
477
|
+
field: "refresh_token",
|
|
478
|
+
allowedTools: [],
|
|
479
|
+
allowedDomains: [],
|
|
480
|
+
createdAt: 1700000000000,
|
|
481
|
+
updatedAt: 1700000000000,
|
|
482
|
+
},
|
|
483
|
+
],
|
|
484
|
+
};
|
|
485
|
+
writeFileSync(META_PATH, JSON.stringify(v3Data, null, 2), "utf-8");
|
|
486
|
+
|
|
487
|
+
const records = listCredentialMetadata();
|
|
488
|
+
// Ghost refresh_token record removed
|
|
489
|
+
expect(records).toHaveLength(1);
|
|
490
|
+
expect(records[0].field).toBe("access_token");
|
|
491
|
+
expect(records[0].hasRefreshToken).toBe(true);
|
|
492
|
+
|
|
493
|
+
// On-disk file should be upgraded to v4
|
|
494
|
+
const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
|
|
495
|
+
expect(raw.version).toBe(4);
|
|
496
|
+
expect(raw.credentials).toHaveLength(1);
|
|
497
|
+
expect(raw.credentials[0].field).toBe("access_token");
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test("v3 file without refresh_token records migrates cleanly", () => {
|
|
501
|
+
const v3Data = {
|
|
502
|
+
version: 3,
|
|
503
|
+
credentials: [
|
|
504
|
+
{
|
|
505
|
+
credentialId: "cred-v3-001",
|
|
506
|
+
service: "fal-ai",
|
|
507
|
+
field: "api_key",
|
|
508
|
+
allowedTools: ["api_request"],
|
|
509
|
+
allowedDomains: ["fal.ai"],
|
|
510
|
+
alias: "fal-primary",
|
|
511
|
+
injectionTemplates: [
|
|
512
|
+
{
|
|
513
|
+
hostPattern: "*.fal.ai",
|
|
514
|
+
injectionType: "header",
|
|
515
|
+
headerName: "Authorization",
|
|
516
|
+
valuePrefix: "Key ",
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
createdAt: 1700000000000,
|
|
520
|
+
updatedAt: 1700000000000,
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
};
|
|
524
|
+
writeFileSync(META_PATH, JSON.stringify(v3Data, null, 2), "utf-8");
|
|
525
|
+
|
|
526
|
+
const record = getCredentialMetadata("fal-ai", "api_key");
|
|
527
|
+
expect(record).toBeDefined();
|
|
528
|
+
expect(record!.alias).toBe("fal-primary");
|
|
529
|
+
expect(record!.injectionTemplates).toHaveLength(1);
|
|
530
|
+
expect(record!.injectionTemplates![0].hostPattern).toBe("*.fal.ai");
|
|
531
|
+
expect(record!.hasRefreshToken).toBeUndefined();
|
|
532
|
+
|
|
533
|
+
// On-disk file should be upgraded to v4
|
|
534
|
+
const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
|
|
535
|
+
expect(raw.version).toBe(4);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test("v3 migration handles multiple services with ghost records", () => {
|
|
539
|
+
const v3Data = {
|
|
540
|
+
version: 3,
|
|
541
|
+
credentials: [
|
|
542
|
+
{
|
|
543
|
+
credentialId: "at-github",
|
|
544
|
+
service: "github",
|
|
545
|
+
field: "access_token",
|
|
546
|
+
allowedTools: [],
|
|
547
|
+
allowedDomains: [],
|
|
548
|
+
createdAt: 1700000000000,
|
|
549
|
+
updatedAt: 1700000000000,
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
credentialId: "rt-github",
|
|
553
|
+
service: "github",
|
|
554
|
+
field: "refresh_token",
|
|
555
|
+
allowedTools: [],
|
|
556
|
+
allowedDomains: [],
|
|
557
|
+
createdAt: 1700000000000,
|
|
558
|
+
updatedAt: 1700000000000,
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
credentialId: "at-slack",
|
|
562
|
+
service: "slack",
|
|
563
|
+
field: "access_token",
|
|
564
|
+
allowedTools: [],
|
|
565
|
+
allowedDomains: [],
|
|
566
|
+
createdAt: 1700000000000,
|
|
567
|
+
updatedAt: 1700000000000,
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
credentialId: "at-stripe",
|
|
571
|
+
service: "stripe",
|
|
572
|
+
field: "access_token",
|
|
573
|
+
allowedTools: [],
|
|
574
|
+
allowedDomains: [],
|
|
575
|
+
createdAt: 1700000000000,
|
|
576
|
+
updatedAt: 1700000000000,
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
credentialId: "rt-stripe",
|
|
580
|
+
service: "stripe",
|
|
581
|
+
field: "refresh_token",
|
|
582
|
+
allowedTools: [],
|
|
583
|
+
allowedDomains: [],
|
|
584
|
+
createdAt: 1700000000000,
|
|
585
|
+
updatedAt: 1700000000000,
|
|
586
|
+
},
|
|
587
|
+
],
|
|
588
|
+
};
|
|
589
|
+
writeFileSync(META_PATH, JSON.stringify(v3Data, null, 2), "utf-8");
|
|
590
|
+
|
|
591
|
+
const records = listCredentialMetadata();
|
|
592
|
+
// refresh_token records removed, only access_token records remain
|
|
593
|
+
expect(records).toHaveLength(3);
|
|
594
|
+
expect(records.every((r) => r.field !== "refresh_token")).toBe(true);
|
|
595
|
+
// github and stripe had refresh tokens
|
|
596
|
+
const github = records.find((r) => r.service === "github");
|
|
597
|
+
const slack = records.find((r) => r.service === "slack");
|
|
598
|
+
const stripe = records.find((r) => r.service === "stripe");
|
|
599
|
+
expect(github!.hasRefreshToken).toBe(true);
|
|
600
|
+
expect(slack!.hasRefreshToken).toBeUndefined();
|
|
601
|
+
expect(stripe!.hasRefreshToken).toBe(true);
|
|
405
602
|
});
|
|
406
603
|
|
|
407
|
-
test("
|
|
604
|
+
test("v4 file is loaded without migration", () => {
|
|
605
|
+
const v4Data = {
|
|
606
|
+
version: 4,
|
|
607
|
+
credentials: [
|
|
608
|
+
{
|
|
609
|
+
credentialId: "cred-v4-001",
|
|
610
|
+
service: "fal-ai",
|
|
611
|
+
field: "api_key",
|
|
612
|
+
allowedTools: ["api_request"],
|
|
613
|
+
allowedDomains: ["fal.ai"],
|
|
614
|
+
alias: "fal-primary",
|
|
615
|
+
hasRefreshToken: true,
|
|
616
|
+
createdAt: 1700000000000,
|
|
617
|
+
updatedAt: 1700000000000,
|
|
618
|
+
},
|
|
619
|
+
],
|
|
620
|
+
};
|
|
621
|
+
writeFileSync(META_PATH, JSON.stringify(v4Data, null, 2), "utf-8");
|
|
622
|
+
|
|
623
|
+
const record = getCredentialMetadata("fal-ai", "api_key");
|
|
624
|
+
expect(record).toBeDefined();
|
|
625
|
+
expect(record!.alias).toBe("fal-primary");
|
|
626
|
+
expect(record!.hasRefreshToken).toBe(true);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
test("future version (v5+) returns unknown version and refuses writes", () => {
|
|
408
630
|
const futureData = {
|
|
409
631
|
version: 99,
|
|
410
632
|
credentials: [],
|
|
@@ -439,7 +661,7 @@ describe("credential metadata store", () => {
|
|
|
439
661
|
},
|
|
440
662
|
);
|
|
441
663
|
|
|
442
|
-
test("upsert on migrated v1 file saves as
|
|
664
|
+
test("upsert on migrated v1 file saves as v4", () => {
|
|
443
665
|
const v1Data = {
|
|
444
666
|
version: 1,
|
|
445
667
|
credentials: [
|
|
@@ -459,13 +681,13 @@ describe("credential metadata store", () => {
|
|
|
459
681
|
// Upsert triggers load (migration) + save (at current version)
|
|
460
682
|
upsertCredentialMetadata("github", "token", { alias: "gh-main" });
|
|
461
683
|
|
|
462
|
-
// Verify on-disk file is now
|
|
684
|
+
// Verify on-disk file is now v4
|
|
463
685
|
const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
|
|
464
|
-
expect(raw.version).toBe(
|
|
686
|
+
expect(raw.version).toBe(4);
|
|
465
687
|
expect(raw.credentials[0].alias).toBe("gh-main");
|
|
466
688
|
});
|
|
467
689
|
|
|
468
|
-
test("v1 load auto-persists as
|
|
690
|
+
test("v1 load auto-persists as v4 on disk without requiring a write", () => {
|
|
469
691
|
const v1Data = {
|
|
470
692
|
version: 1,
|
|
471
693
|
credentials: [
|
|
@@ -482,11 +704,11 @@ describe("credential metadata store", () => {
|
|
|
482
704
|
};
|
|
483
705
|
writeFileSync(META_PATH, JSON.stringify(v1Data, null, 2), "utf-8");
|
|
484
706
|
|
|
485
|
-
// A read-only operation should still persist the
|
|
707
|
+
// A read-only operation should still persist the v4 upgrade
|
|
486
708
|
listCredentialMetadata();
|
|
487
709
|
|
|
488
710
|
const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
|
|
489
|
-
expect(raw.version).toBe(
|
|
711
|
+
expect(raw.version).toBe(4);
|
|
490
712
|
expect(raw.credentials[0].credentialId).toBe("cred-autopersist");
|
|
491
713
|
});
|
|
492
714
|
|
|
@@ -586,20 +808,20 @@ describe("credential metadata store", () => {
|
|
|
586
808
|
test("file with non-array credentials field is treated as empty list", () => {
|
|
587
809
|
writeFileSync(
|
|
588
810
|
META_PATH,
|
|
589
|
-
JSON.stringify({ version:
|
|
811
|
+
JSON.stringify({ version: 4, credentials: "not-an-array" }),
|
|
590
812
|
"utf-8",
|
|
591
813
|
);
|
|
592
814
|
expect(listCredentialMetadata()).toEqual([]);
|
|
593
815
|
});
|
|
594
816
|
|
|
595
817
|
test("file with missing credentials field is treated as empty list", () => {
|
|
596
|
-
writeFileSync(META_PATH, JSON.stringify({ version:
|
|
818
|
+
writeFileSync(META_PATH, JSON.stringify({ version: 4 }), "utf-8");
|
|
597
819
|
expect(listCredentialMetadata()).toEqual([]);
|
|
598
820
|
});
|
|
599
821
|
|
|
600
822
|
test("malformed records within credentials array are filtered out", () => {
|
|
601
823
|
const data = {
|
|
602
|
-
version:
|
|
824
|
+
version: 4,
|
|
603
825
|
credentials: [
|
|
604
826
|
// Valid record
|
|
605
827
|
{
|
|
@@ -709,7 +931,7 @@ describe("credential metadata store", () => {
|
|
|
709
931
|
|
|
710
932
|
const raw = readFileSync(META_PATH, "utf-8");
|
|
711
933
|
const parsed = JSON.parse(raw);
|
|
712
|
-
expect(parsed.version).toBe(
|
|
934
|
+
expect(parsed.version).toBe(4);
|
|
713
935
|
expect(parsed.credentials).toHaveLength(1);
|
|
714
936
|
expect(parsed.credentials[0].service).toBe("slack");
|
|
715
937
|
});
|
|
@@ -717,23 +939,23 @@ describe("credential metadata store", () => {
|
|
|
717
939
|
test("file written by saveFile has version field", () => {
|
|
718
940
|
upsertCredentialMetadata("test", "key");
|
|
719
941
|
const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
|
|
720
|
-
expect(raw.version).toBe(
|
|
942
|
+
expect(raw.version).toBe(4);
|
|
721
943
|
});
|
|
722
944
|
});
|
|
723
945
|
|
|
724
946
|
// ── Empty credential lists ────────────────────────────────────────
|
|
725
947
|
|
|
726
948
|
describe("empty credential lists", () => {
|
|
727
|
-
test("empty
|
|
949
|
+
test("empty v4 file returns empty array", () => {
|
|
728
950
|
writeFileSync(
|
|
729
951
|
META_PATH,
|
|
730
|
-
JSON.stringify({ version:
|
|
952
|
+
JSON.stringify({ version: 4, credentials: [] }, null, 2),
|
|
731
953
|
"utf-8",
|
|
732
954
|
);
|
|
733
955
|
expect(listCredentialMetadata()).toEqual([]);
|
|
734
956
|
});
|
|
735
957
|
|
|
736
|
-
test("empty v1 file is migrated to
|
|
958
|
+
test("empty v1 file is migrated to v4 with empty credentials", () => {
|
|
737
959
|
writeFileSync(
|
|
738
960
|
META_PATH,
|
|
739
961
|
JSON.stringify({ version: 1, credentials: [] }, null, 2),
|
|
@@ -741,9 +963,9 @@ describe("credential metadata store", () => {
|
|
|
741
963
|
);
|
|
742
964
|
expect(listCredentialMetadata()).toEqual([]);
|
|
743
965
|
|
|
744
|
-
// Should be persisted as
|
|
966
|
+
// Should be persisted as v4
|
|
745
967
|
const raw = JSON.parse(readFileSync(META_PATH, "utf-8"));
|
|
746
|
-
expect(raw.version).toBe(
|
|
968
|
+
expect(raw.version).toBe(4);
|
|
747
969
|
expect(raw.credentials).toEqual([]);
|
|
748
970
|
});
|
|
749
971
|
|
|
@@ -12,6 +12,7 @@ mock.module("../util/logger.js", () => ({
|
|
|
12
12
|
}),
|
|
13
13
|
}));
|
|
14
14
|
|
|
15
|
+
import { credentialKey } from "../security/credential-key.js";
|
|
15
16
|
import {
|
|
16
17
|
_setMetadataPath,
|
|
17
18
|
upsertCredentialMetadata,
|
|
@@ -56,7 +57,7 @@ describe("credential resolver", () => {
|
|
|
56
57
|
expect(result!.credentialId).toBe(created.credentialId);
|
|
57
58
|
expect(result!.service).toBe("github");
|
|
58
59
|
expect(result!.field).toBe("token");
|
|
59
|
-
expect(result!.storageKey).toBe("
|
|
60
|
+
expect(result!.storageKey).toBe(credentialKey("github", "token"));
|
|
60
61
|
expect(result!.metadata.allowedTools).toEqual([
|
|
61
62
|
"browser_fill_credential",
|
|
62
63
|
]);
|
|
@@ -111,7 +112,7 @@ describe("credential resolver", () => {
|
|
|
111
112
|
expect(result!.credentialId).toBe(created.credentialId);
|
|
112
113
|
expect(result!.service).toBe("gmail");
|
|
113
114
|
expect(result!.field).toBe("password");
|
|
114
|
-
expect(result!.storageKey).toBe("
|
|
115
|
+
expect(result!.storageKey).toBe(credentialKey("gmail", "password"));
|
|
115
116
|
});
|
|
116
117
|
|
|
117
118
|
test("returns undefined for non-existent ID", () => {
|
|
@@ -190,10 +191,10 @@ describe("credential resolver", () => {
|
|
|
190
191
|
});
|
|
191
192
|
|
|
192
193
|
describe("storage key format", () => {
|
|
193
|
-
test("storage key follows credential
|
|
194
|
+
test("storage key follows credential/{service}/{field} format", () => {
|
|
194
195
|
upsertCredentialMetadata("my-service", "api-key");
|
|
195
196
|
const result = resolveByServiceField("my-service", "api-key");
|
|
196
|
-
expect(result!.storageKey).toBe("
|
|
197
|
+
expect(result!.storageKey).toBe(credentialKey("my-service", "api-key"));
|
|
197
198
|
});
|
|
198
199
|
});
|
|
199
200
|
|
|
@@ -147,12 +147,12 @@ describe("E2E: secure store and list lifecycle", () => {
|
|
|
147
147
|
// Value must NOT appear in tool output (invariant 1)
|
|
148
148
|
expect(result.content).not.toContain("ghp_abc123");
|
|
149
149
|
// Value must be in keychain
|
|
150
|
-
expect(storedKeys.get("credential
|
|
150
|
+
expect(storedKeys.get("credential/github/token")).toBe("ghp_abc123");
|
|
151
151
|
});
|
|
152
152
|
|
|
153
153
|
test("list returns service/field pairs without secret values", async () => {
|
|
154
|
-
storedKeys.set("credential
|
|
155
|
-
storedKeys.set("credential
|
|
154
|
+
storedKeys.set("credential/github/token", "secret1");
|
|
155
|
+
storedKeys.set("credential/aws/access_key", "secret2");
|
|
156
156
|
metadataStore.set("github:token", {
|
|
157
157
|
credentialId: "cred-github-token",
|
|
158
158
|
service: "github",
|
|
@@ -177,14 +177,14 @@ describe("E2E: secure store and list lifecycle", () => {
|
|
|
177
177
|
});
|
|
178
178
|
|
|
179
179
|
test("delete removes credential from keychain", async () => {
|
|
180
|
-
storedKeys.set("credential
|
|
180
|
+
storedKeys.set("credential/github/token", "secret1");
|
|
181
181
|
|
|
182
182
|
const result = await credentialStoreTool.execute(
|
|
183
183
|
{ action: "delete", service: "github", field: "token" },
|
|
184
184
|
makeContext(),
|
|
185
185
|
);
|
|
186
186
|
expect(result.isError).toBe(false);
|
|
187
|
-
expect(storedKeys.has("credential
|
|
187
|
+
expect(storedKeys.has("credential/github/token")).toBe(false);
|
|
188
188
|
});
|
|
189
189
|
});
|
|
190
190
|
|
|
@@ -267,7 +267,7 @@ describe("E2E: one-time send override", () => {
|
|
|
267
267
|
);
|
|
268
268
|
expect(result.isError).toBe(true);
|
|
269
269
|
expect(result.content).toContain("not enabled");
|
|
270
|
-
expect(storedKeys.has("credential
|
|
270
|
+
expect(storedKeys.has("credential/svc/key")).toBe(false);
|
|
271
271
|
});
|
|
272
272
|
|
|
273
273
|
test("accepts transient_send when config gate is on", async () => {
|
|
@@ -285,7 +285,7 @@ describe("E2E: one-time send override", () => {
|
|
|
285
285
|
expect(result.isError).toBe(false);
|
|
286
286
|
expect(result.content).toContain("NOT saved");
|
|
287
287
|
// Value must NOT be in keychain
|
|
288
|
-
expect(storedKeys.has("credential
|
|
288
|
+
expect(storedKeys.has("credential/svc/key")).toBe(false);
|
|
289
289
|
// Value must NOT appear in output
|
|
290
290
|
expect(result.content).not.toContain("tmp1");
|
|
291
291
|
});
|
|
@@ -303,7 +303,7 @@ describe("E2E: one-time send override", () => {
|
|
|
303
303
|
ctx,
|
|
304
304
|
);
|
|
305
305
|
expect(result.isError).toBe(false);
|
|
306
|
-
expect(storedKeys.has("credential
|
|
306
|
+
expect(storedKeys.has("credential/svc/key")).toBe(true);
|
|
307
307
|
});
|
|
308
308
|
});
|
|
309
309
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { mkdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
@@ -63,11 +63,13 @@ mock.module("../tools/registry.js", () => ({
|
|
|
63
63
|
// ---------------------------------------------------------------------------
|
|
64
64
|
|
|
65
65
|
import { DEFAULT_CONFIG } from "../config/defaults.js";
|
|
66
|
+
import { credentialKey } from "../security/credential-key.js";
|
|
66
67
|
import { redactSensitiveFields } from "../security/redaction.js";
|
|
67
|
-
import { setSecureKey } from "../security/secure-keys.js";
|
|
68
|
+
import { getSecureKey, setSecureKey } from "../security/secure-keys.js";
|
|
68
69
|
import { CredentialBroker } from "../tools/credentials/broker.js";
|
|
69
70
|
import {
|
|
70
71
|
_setMetadataPath,
|
|
72
|
+
getCredentialMetadata,
|
|
71
73
|
upsertCredentialMetadata,
|
|
72
74
|
} from "../tools/credentials/metadata-store.js";
|
|
73
75
|
|
|
@@ -199,6 +201,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
|
|
|
199
201
|
// Hard boundary: only these production files may import from secure-keys.
|
|
200
202
|
// Any new import must be reviewed for secret-leak risk and added here.
|
|
201
203
|
const ALLOWED_IMPORTERS = new Set([
|
|
204
|
+
"security/credential-key.ts", // credential key builder + migration
|
|
202
205
|
"config/loader.ts", // config management (API keys)
|
|
203
206
|
"tools/credentials/vault.ts", // credential store tool
|
|
204
207
|
"tools/credentials/broker.ts", // brokered credential access
|
|
@@ -229,7 +232,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
|
|
|
229
232
|
"runtime/routes/secret-routes.ts", // HTTP secret management routes (set/delete secrets)
|
|
230
233
|
"daemon/ride-shotgun-handler.ts", // learn session cookie persistence
|
|
231
234
|
"daemon/session-messaging.ts", // credential storage during session messaging
|
|
232
|
-
"runtime/routes/settings-routes.ts", // settings routes OAuth credential lookup (
|
|
235
|
+
"runtime/routes/settings-routes.ts", // settings routes OAuth credential lookup (client_secret)
|
|
233
236
|
]);
|
|
234
237
|
|
|
235
238
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -245,7 +248,11 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
|
|
|
245
248
|
const s = statSync(full);
|
|
246
249
|
if (s.isDirectory()) {
|
|
247
250
|
collectTsFiles(full, files);
|
|
248
|
-
} else if (
|
|
251
|
+
} else if (
|
|
252
|
+
entry.endsWith(".ts") &&
|
|
253
|
+
!entry.endsWith(".d.ts") &&
|
|
254
|
+
!entry.endsWith(".test.ts")
|
|
255
|
+
) {
|
|
249
256
|
files.push(full);
|
|
250
257
|
}
|
|
251
258
|
}
|
|
@@ -413,7 +420,10 @@ describe("Invariant 4: credentials only used for allowed purpose", () => {
|
|
|
413
420
|
allowedTools: tc.allowedTools,
|
|
414
421
|
allowedDomains: tc.allowedDomains,
|
|
415
422
|
});
|
|
416
|
-
setSecureKey(
|
|
423
|
+
setSecureKey(
|
|
424
|
+
credentialKey(tc.credentialId, "token"),
|
|
425
|
+
"test-secret-value",
|
|
426
|
+
);
|
|
417
427
|
|
|
418
428
|
const result = await broker.browserFill({
|
|
419
429
|
service: tc.credentialId,
|
|
@@ -438,7 +448,7 @@ describe("Invariant 4: credentials only used for allowed purpose", () => {
|
|
|
438
448
|
allowedTools: ["browser_fill_credential"],
|
|
439
449
|
allowedDomains: ["github.com"],
|
|
440
450
|
});
|
|
441
|
-
setSecureKey("
|
|
451
|
+
setSecureKey(credentialKey("github", "token"), "ghp_secret123");
|
|
442
452
|
|
|
443
453
|
const result = await broker.browserFill({
|
|
444
454
|
service: "github",
|
|
@@ -470,6 +480,94 @@ describe("Invariant 4: credentials only used for allowed purpose", () => {
|
|
|
470
480
|
});
|
|
471
481
|
});
|
|
472
482
|
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
// Invariant 6 — oauth2ClientSecret never in plaintext metadata
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
|
|
487
|
+
describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store", () => {
|
|
488
|
+
beforeEach(() => {
|
|
489
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
490
|
+
_setStorePath(STORE_PATH);
|
|
491
|
+
_resetBackend();
|
|
492
|
+
_setMetadataPath(join(TEST_DIR, "metadata.json"));
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
afterEach(() => {
|
|
496
|
+
_setMetadataPath(null);
|
|
497
|
+
_setStorePath(null);
|
|
498
|
+
_resetBackend();
|
|
499
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("upsertCredentialMetadata does not accept oauth2ClientSecret", () => {
|
|
503
|
+
const record = upsertCredentialMetadata(
|
|
504
|
+
"integration:gmail",
|
|
505
|
+
"access_token",
|
|
506
|
+
{
|
|
507
|
+
oauth2TokenUrl: "https://oauth2.googleapis.com/token",
|
|
508
|
+
oauth2ClientId: "test-client-id",
|
|
509
|
+
},
|
|
510
|
+
);
|
|
511
|
+
expect("oauth2ClientSecret" in record).toBe(false);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test("client secret is read from secure store, not metadata", () => {
|
|
515
|
+
setSecureKey(
|
|
516
|
+
credentialKey("integration:gmail", "client_secret"),
|
|
517
|
+
"my-secret",
|
|
518
|
+
);
|
|
519
|
+
upsertCredentialMetadata("integration:gmail", "access_token", {
|
|
520
|
+
oauth2TokenUrl: "https://oauth2.googleapis.com/token",
|
|
521
|
+
oauth2ClientId: "test-client-id",
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
const meta = getCredentialMetadata("integration:gmail", "access_token");
|
|
525
|
+
expect(meta).toBeDefined();
|
|
526
|
+
expect("oauth2ClientSecret" in meta!).toBe(false);
|
|
527
|
+
|
|
528
|
+
// Secret is in secure store
|
|
529
|
+
expect(
|
|
530
|
+
getSecureKey(credentialKey("integration:gmail", "client_secret")),
|
|
531
|
+
).toBe("my-secret");
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test("v2 metadata with oauth2ClientSecret is stripped on migration", () => {
|
|
535
|
+
const v2Data = {
|
|
536
|
+
version: 2,
|
|
537
|
+
credentials: [
|
|
538
|
+
{
|
|
539
|
+
credentialId: "cred-v2-secret",
|
|
540
|
+
service: "integration:gmail",
|
|
541
|
+
field: "access_token",
|
|
542
|
+
allowedTools: [],
|
|
543
|
+
allowedDomains: [],
|
|
544
|
+
oauth2TokenUrl: "https://oauth2.googleapis.com/token",
|
|
545
|
+
oauth2ClientId: "test-client-id",
|
|
546
|
+
oauth2ClientSecret: "plaintext-secret-should-be-stripped",
|
|
547
|
+
createdAt: 1700000000000,
|
|
548
|
+
updatedAt: 1700000000000,
|
|
549
|
+
},
|
|
550
|
+
],
|
|
551
|
+
};
|
|
552
|
+
writeFileSync(
|
|
553
|
+
join(TEST_DIR, "metadata.json"),
|
|
554
|
+
JSON.stringify(v2Data, null, 2),
|
|
555
|
+
"utf-8",
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
const meta = getCredentialMetadata("integration:gmail", "access_token");
|
|
559
|
+
expect(meta).toBeDefined();
|
|
560
|
+
expect("oauth2ClientSecret" in meta!).toBe(false);
|
|
561
|
+
|
|
562
|
+
// Verify on-disk file no longer contains the secret
|
|
563
|
+
const raw = JSON.parse(
|
|
564
|
+
readFileSync(join(TEST_DIR, "metadata.json"), "utf-8"),
|
|
565
|
+
);
|
|
566
|
+
expect(raw.credentials[0]).not.toHaveProperty("oauth2ClientSecret");
|
|
567
|
+
expect(raw.version).toBe(4);
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
|
|
473
571
|
// ---------------------------------------------------------------------------
|
|
474
572
|
// Cross-Cutting — One-Time Send Override
|
|
475
573
|
// ---------------------------------------------------------------------------
|