@vellumai/assistant 0.4.49 → 0.4.51
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 +24 -33
- package/README.md +3 -3
- package/docs/architecture/integrations.md +2 -2
- package/docs/architecture/keychain-broker.md +6 -6
- package/docs/architecture/memory.md +180 -119
- package/knip.json +32 -0
- package/package.json +3 -2
- package/src/__tests__/agent-loop.test.ts +3 -1
- package/src/__tests__/anthropic-provider.test.ts +114 -23
- package/src/__tests__/approval-cascade.test.ts +1 -15
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
- package/src/__tests__/btw-routes.test.ts +61 -5
- package/src/__tests__/canonical-guardian-store.test.ts +95 -0
- package/src/__tests__/checker.test.ts +13 -0
- package/src/__tests__/config-schema.test.ts +1 -68
- package/src/__tests__/config-watcher.test.ts +8 -0
- package/src/__tests__/context-memory-e2e.test.ts +11 -100
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-security-e2e.test.ts +1 -0
- package/src/__tests__/credential-security-invariants.test.ts +8 -7
- package/src/__tests__/credential-vault-unit.test.ts +23 -18
- package/src/__tests__/credential-vault.test.ts +30 -18
- package/src/__tests__/credentials-cli.test.ts +257 -82
- package/src/__tests__/cu-unified-flow.test.ts +532 -0
- package/src/__tests__/date-context.test.ts +93 -77
- package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
- package/src/__tests__/history-repair.test.ts +245 -0
- package/src/__tests__/host-cu-proxy.test.ts +165 -3
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +36 -7
- package/src/__tests__/integration-status.test.ts +31 -30
- package/src/__tests__/invite-redemption-service.test.ts +166 -13
- package/src/__tests__/invite-routes-http.test.ts +166 -5
- package/src/__tests__/keychain-broker-client.test.ts +4 -4
- package/src/__tests__/list-messages-attachments.test.ts +193 -0
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
- package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
- package/src/__tests__/memory-recall-quality.test.ts +244 -407
- package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
- package/src/__tests__/memory-regressions.test.ts +477 -2841
- package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
- package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
- package/src/__tests__/mime-builder.test.ts +28 -0
- package/src/__tests__/native-web-search.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +824 -31
- package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
- package/src/__tests__/oauth-store.test.ts +363 -17
- package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
- package/src/__tests__/registry.test.ts +0 -1
- package/src/__tests__/relay-server.test.ts +55 -1
- package/src/__tests__/schedule-tools.test.ts +32 -0
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -0
- package/src/__tests__/secret-routes-managed-proxy.test.ts +183 -0
- package/src/__tests__/secure-keys.test.ts +78 -18
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/server-history-render.test.ts +2 -2
- package/src/__tests__/session-abort-tool-results.test.ts +1 -14
- package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
- package/src/__tests__/session-agent-loop.test.ts +19 -15
- package/src/__tests__/session-confirmation-signals.test.ts +1 -15
- package/src/__tests__/session-error.test.ts +124 -2
- package/src/__tests__/session-history-web-search.test.ts +918 -0
- package/src/__tests__/session-pre-run-repair.test.ts +1 -14
- package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
- package/src/__tests__/session-queue.test.ts +37 -27
- package/src/__tests__/session-runtime-assembly.test.ts +54 -0
- package/src/__tests__/session-slash-known.test.ts +1 -15
- package/src/__tests__/session-slash-queue.test.ts +1 -15
- package/src/__tests__/session-slash-unknown.test.ts +1 -15
- package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
- package/src/__tests__/session-workspace-injection.test.ts +3 -37
- package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
- package/src/__tests__/skills-install-extract.test.ts +93 -0
- package/src/__tests__/skills.test.ts +2 -2
- package/src/__tests__/skillssh-registry.test.ts +451 -0
- package/src/__tests__/slack-channel-config.test.ts +10 -8
- package/src/__tests__/trust-store.test.ts +15 -0
- package/src/__tests__/twilio-config.test.ts +11 -10
- package/src/__tests__/twilio-provider.test.ts +9 -4
- package/src/__tests__/voice-invite-redemption.test.ts +85 -5
- package/src/agent/ax-tree-compaction.test.ts +51 -0
- package/src/agent/loop.ts +39 -12
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-request-resolvers.ts +14 -2
- package/src/bundler/compiler-tools.ts +66 -2
- package/src/calls/call-domain.ts +134 -3
- package/src/calls/call-store.ts +6 -0
- package/src/calls/relay-server.ts +44 -6
- package/src/calls/relay-setup-router.ts +17 -1
- package/src/calls/twilio-config.ts +5 -4
- package/src/calls/twilio-provider.ts +14 -9
- package/src/calls/twilio-rest.ts +10 -7
- package/src/calls/types.ts +3 -1
- package/src/cli/commands/config.ts +14 -9
- package/src/cli/commands/contacts.ts +3 -0
- package/src/cli/commands/credentials.ts +170 -174
- package/src/cli/commands/doctor.ts +11 -8
- package/src/cli/commands/keys.ts +9 -9
- package/src/cli/commands/mcp.ts +46 -59
- package/src/cli/commands/memory.ts +16 -165
- package/src/cli/commands/oauth/apps.ts +68 -10
- package/src/cli/commands/oauth/connections.ts +475 -105
- package/src/cli/commands/oauth/index.ts +3 -3
- package/src/cli/commands/oauth/providers.ts +18 -4
- package/src/cli/commands/sessions.ts +5 -2
- package/src/cli/commands/skills.ts +173 -1
- package/src/cli/http-client.ts +0 -20
- package/src/cli/main-screen.tsx +2 -2
- package/src/cli/program.ts +5 -6
- package/src/cli.ts +20 -22
- package/src/config/__tests__/feature-flag-registry-bundled.test.ts +39 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
- package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
- package/src/config/bundled-skills/contacts/SKILL.md +35 -11
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
- package/src/config/bundled-skills/gmail/SKILL.md +1 -1
- package/src/config/bundled-skills/gmail/TOOLS.json +52 -0
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +13 -3
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +9 -2
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +9 -2
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +5 -1
- package/src/config/bundled-skills/google-calendar/TOOLS.json +20 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +8 -2
- package/src/config/bundled-skills/messaging/SKILL.md +1 -1
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +7 -5
- package/src/config/bundled-skills/slack/tools/shared.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +1 -1
- package/src/config/bundled-tool-registry.ts +2 -5
- package/src/config/loader.ts +6 -42
- package/src/config/schema.ts +1 -12
- package/src/config/schemas/memory-lifecycle.ts +0 -9
- package/src/config/schemas/memory-processing.ts +0 -180
- package/src/config/schemas/memory-retrieval.ts +32 -104
- package/src/config/schemas/memory.ts +0 -10
- package/src/config/types.ts +0 -4
- package/src/contacts/contact-store.ts +39 -2
- package/src/contacts/contacts-write.ts +9 -0
- package/src/context/window-manager.ts +4 -1
- package/src/daemon/config-watcher.ts +55 -2
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/date-context.ts +114 -31
- package/src/daemon/handlers/config-ingress.ts +2 -2
- package/src/daemon/handlers/config-slack-channel.ts +59 -39
- package/src/daemon/handlers/config-telegram.ts +23 -14
- package/src/daemon/handlers/session-history.ts +1 -358
- package/src/daemon/handlers/sessions.ts +18 -13
- package/src/daemon/handlers/shared.ts +3 -17
- package/src/daemon/handlers/skills.ts +20 -1
- package/src/daemon/history-repair.ts +72 -8
- package/src/daemon/host-cu-proxy.ts +55 -26
- package/src/daemon/lifecycle.ts +39 -4
- package/src/daemon/mcp-reload-service.ts +2 -2
- package/src/daemon/message-types/computer-use.ts +1 -12
- package/src/daemon/message-types/memory.ts +4 -16
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/sessions.ts +4 -42
- package/src/daemon/server.ts +6 -1
- package/src/daemon/session-agent-loop-handlers.ts +38 -0
- package/src/daemon/session-agent-loop.ts +334 -48
- package/src/daemon/session-error.ts +89 -6
- package/src/daemon/session-history.ts +17 -7
- package/src/daemon/session-media-retry.ts +6 -2
- package/src/daemon/session-memory.ts +69 -149
- package/src/daemon/session-process.ts +10 -1
- package/src/daemon/session-runtime-assembly.ts +49 -19
- package/src/daemon/session-slash.ts +3 -5
- package/src/daemon/session-surfaces.ts +4 -1
- package/src/daemon/session-tool-setup.ts +7 -1
- package/src/daemon/session.ts +12 -2
- package/src/email/providers/index.ts +2 -2
- package/src/instrument.ts +61 -1
- package/src/media/avatar-router.ts +1 -1
- package/src/memory/admin.ts +2 -191
- package/src/memory/canonical-guardian-store.ts +38 -2
- package/src/memory/conversation-crud.ts +0 -33
- package/src/memory/conversation-queries.ts +25 -83
- package/src/memory/db-init.ts +32 -0
- package/src/memory/embedding-backend.ts +84 -8
- package/src/memory/embedding-types.ts +9 -1
- package/src/memory/indexer.ts +7 -46
- package/src/memory/invite-store.ts +19 -0
- package/src/memory/items-extractor.ts +274 -76
- package/src/memory/job-handlers/backfill.ts +2 -127
- package/src/memory/job-handlers/cleanup.ts +2 -16
- package/src/memory/job-handlers/extraction.ts +2 -138
- package/src/memory/job-handlers/index-maintenance.ts +1 -6
- package/src/memory/job-handlers/summarization.ts +3 -148
- package/src/memory/job-utils.ts +21 -59
- package/src/memory/jobs-store.ts +1 -159
- package/src/memory/jobs-worker.ts +9 -52
- package/src/memory/migrations/104-core-indexes.ts +3 -3
- package/src/memory/migrations/149-oauth-tables.ts +2 -0
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
- package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
- package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
- package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
- package/src/memory/migrations/154-drop-fts.ts +20 -0
- package/src/memory/migrations/155-drop-conflicts.ts +7 -0
- package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
- package/src/memory/migrations/157-invite-contact-id.ts +104 -0
- package/src/memory/migrations/index.ts +8 -0
- package/src/memory/migrations/registry.ts +6 -0
- package/src/memory/qdrant-client.ts +148 -51
- package/src/memory/raw-query.ts +1 -1
- package/src/memory/retriever.test.ts +294 -273
- package/src/memory/retriever.ts +421 -645
- package/src/memory/schema/calls.ts +2 -0
- package/src/memory/schema/contacts.ts +1 -0
- package/src/memory/schema/memory-core.ts +3 -48
- package/src/memory/schema/oauth.ts +2 -0
- package/src/memory/search/formatting.ts +263 -176
- package/src/memory/search/lexical.ts +1 -254
- package/src/memory/search/ranking.ts +0 -455
- package/src/memory/search/semantic.ts +100 -14
- package/src/memory/search/staleness.ts +47 -0
- package/src/memory/search/tier-classifier.ts +21 -0
- package/src/memory/search/types.ts +15 -77
- package/src/memory/task-memory-cleanup.ts +4 -6
- package/src/messaging/provider.ts +1 -1
- package/src/messaging/providers/gmail/adapter.ts +1 -1
- package/src/messaging/providers/gmail/mime-builder.ts +17 -7
- package/src/messaging/providers/telegram-bot/adapter.ts +17 -8
- package/src/messaging/providers/whatsapp/adapter.ts +13 -9
- package/src/messaging/registry.ts +9 -5
- package/src/oauth/byo-connection.test.ts +40 -25
- package/src/oauth/connect-orchestrator.ts +4 -10
- package/src/oauth/connection-resolver.ts +20 -6
- package/src/oauth/manual-token-connection.ts +5 -5
- package/src/oauth/oauth-store.ts +183 -31
- package/src/oauth/platform-connection.test.ts +1 -1
- package/src/oauth/provider-behaviors.ts +503 -4
- package/src/oauth/seed-providers.ts +214 -8
- package/src/oauth/token-persistence.ts +31 -16
- package/src/permissions/defaults.ts +1 -0
- package/src/permissions/trust-store.ts +23 -1
- package/src/playbooks/playbook-compiler.ts +1 -1
- package/src/prompts/system-prompt.ts +18 -2
- package/src/providers/anthropic/client.ts +56 -126
- package/src/providers/types.ts +7 -1
- package/src/runtime/AGENTS.md +9 -0
- package/src/runtime/auth/route-policy.ts +6 -3
- package/src/runtime/channel-readiness-service.ts +48 -40
- package/src/runtime/guardian-reply-router.ts +24 -22
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/http-types.ts +2 -0
- package/src/runtime/invite-redemption-service.ts +72 -12
- package/src/runtime/invite-service.ts +43 -0
- package/src/runtime/middleware/twilio-validation.ts +1 -1
- package/src/runtime/pending-interactions.ts +2 -2
- package/src/runtime/routes/brain-graph-routes.ts +10 -90
- package/src/runtime/routes/btw-routes.ts +10 -5
- package/src/runtime/routes/conversation-routes.ts +56 -11
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
- package/src/runtime/routes/integrations/slack/channel.ts +2 -2
- package/src/runtime/routes/integrations/telegram.ts +2 -2
- package/src/runtime/routes/integrations/twilio.ts +17 -17
- package/src/runtime/routes/invite-routes.ts +29 -4
- package/src/runtime/routes/memory-item-routes.test.ts +754 -0
- package/src/runtime/routes/memory-item-routes.ts +503 -0
- package/src/runtime/routes/secret-routes.ts +17 -0
- package/src/runtime/routes/session-management-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +3 -3
- package/src/runtime/routes/trust-rules-routes.ts +14 -0
- package/src/runtime/routes/workspace-routes.ts +9 -4
- package/src/runtime/routes/workspace-utils.ts +8 -2
- package/src/schedule/integration-status.ts +26 -19
- package/src/security/keychain-broker-client.ts +17 -4
- package/src/security/oauth2.ts +6 -7
- package/src/security/secure-keys.ts +44 -19
- package/src/security/token-manager.ts +46 -39
- package/src/services/vercel-deploy.ts +0 -24
- package/src/signals/confirm.ts +78 -0
- package/src/signals/mcp-reload.ts +18 -0
- package/src/skills/catalog-install.ts +74 -18
- package/src/skills/skillssh-registry.ts +503 -0
- package/src/tools/assets/search.ts +5 -1
- package/src/tools/computer-use/definitions.ts +0 -10
- package/src/tools/computer-use/registry.ts +1 -1
- package/src/tools/credentials/vault.ts +22 -7
- package/src/tools/memory/definitions.ts +4 -13
- package/src/tools/memory/handlers.test.ts +83 -103
- package/src/tools/memory/handlers.ts +50 -85
- package/src/tools/network/script-proxy/session-manager.ts +8 -8
- package/src/tools/schedule/create.ts +10 -3
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/skills/load.ts +25 -2
- package/src/watcher/provider-types.ts +1 -1
- package/src/watcher/providers/github.ts +1 -1
- package/src/watcher/providers/gmail.ts +3 -3
- package/src/watcher/providers/google-calendar.ts +3 -3
- package/src/watcher/providers/linear.ts +1 -1
- package/src/__tests__/clarification-resolver.test.ts +0 -193
- package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
- package/src/__tests__/conflict-policy.test.ts +0 -269
- package/src/__tests__/conflict-store.test.ts +0 -372
- package/src/__tests__/contradiction-checker.test.ts +0 -361
- package/src/__tests__/entity-extractor.test.ts +0 -211
- package/src/__tests__/entity-search.test.ts +0 -1117
- package/src/__tests__/profile-compiler.test.ts +0 -392
- package/src/__tests__/session-conflict-gate.test.ts +0 -1228
- package/src/__tests__/session-profile-injection.test.ts +0 -557
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
- package/src/daemon/session-conflict-gate.ts +0 -167
- package/src/daemon/session-dynamic-profile.ts +0 -77
- package/src/memory/clarification-resolver.ts +0 -417
- package/src/memory/conflict-intent.ts +0 -205
- package/src/memory/conflict-policy.ts +0 -127
- package/src/memory/conflict-store.ts +0 -410
- package/src/memory/contradiction-checker.ts +0 -508
- package/src/memory/entity-extractor.ts +0 -535
- package/src/memory/format-recall.ts +0 -47
- package/src/memory/fts-reconciler.ts +0 -165
- package/src/memory/job-handlers/conflict.ts +0 -200
- package/src/memory/profile-compiler.ts +0 -195
- package/src/memory/recall-cache.ts +0 -117
- package/src/memory/search/entity.ts +0 -535
- package/src/memory/search/query-expansion.test.ts +0 -70
- package/src/memory/search/query-expansion.ts +0 -118
- package/src/runtime/routes/mcp-routes.ts +0 -20
|
@@ -14,6 +14,7 @@ mock.module("../util/logger.js", () => ({
|
|
|
14
14
|
|
|
15
15
|
mock.module("../security/secure-keys.js", () => ({
|
|
16
16
|
getSecureKey: (key: string) => mockSecureKeys[key] ?? null,
|
|
17
|
+
getSecureKeyAsync: async (key: string) => mockSecureKeys[key] ?? null,
|
|
17
18
|
}));
|
|
18
19
|
|
|
19
20
|
mock.module("../config/loader.js", () => ({
|
|
@@ -41,8 +42,8 @@ describe("twilio-config", () => {
|
|
|
41
42
|
};
|
|
42
43
|
});
|
|
43
44
|
|
|
44
|
-
test("returns config when credentials and phone number are set", () => {
|
|
45
|
-
const config = getTwilioConfig();
|
|
45
|
+
test("returns config when credentials and phone number are set", async () => {
|
|
46
|
+
const config = await getTwilioConfig();
|
|
46
47
|
expect(config.accountSid).toBe("AC_test_sid");
|
|
47
48
|
expect(config.authToken).toBe("test_auth_token");
|
|
48
49
|
expect(config.phoneNumber).toBe("+15551234567");
|
|
@@ -50,16 +51,16 @@ describe("twilio-config", () => {
|
|
|
50
51
|
expect(config.wssBaseUrl).toBe("wss://test.example.com/twilio/relay");
|
|
51
52
|
});
|
|
52
53
|
|
|
53
|
-
test("throws ConfigError when account SID is missing", () => {
|
|
54
|
+
test("throws ConfigError when account SID is missing", async () => {
|
|
54
55
|
mockLoadConfigResult = {
|
|
55
56
|
twilio: { accountSid: "", phoneNumber: "+15551234567" },
|
|
56
57
|
};
|
|
57
|
-
expect(
|
|
58
|
+
expect(getTwilioConfig()).rejects.toThrow(
|
|
58
59
|
/Twilio credentials not configured/,
|
|
59
60
|
);
|
|
60
61
|
});
|
|
61
62
|
|
|
62
|
-
test("throws ConfigError when auth token is missing", () => {
|
|
63
|
+
test("throws ConfigError when auth token is missing", async () => {
|
|
63
64
|
mockSecureKeys = {};
|
|
64
65
|
mockLoadConfigResult = {
|
|
65
66
|
twilio: {
|
|
@@ -67,26 +68,26 @@ describe("twilio-config", () => {
|
|
|
67
68
|
phoneNumber: "+15551234567",
|
|
68
69
|
},
|
|
69
70
|
};
|
|
70
|
-
expect(
|
|
71
|
+
expect(getTwilioConfig()).rejects.toThrow(
|
|
71
72
|
/Twilio credentials not configured/,
|
|
72
73
|
);
|
|
73
74
|
});
|
|
74
75
|
|
|
75
|
-
test("throws ConfigError when phone number is missing", () => {
|
|
76
|
+
test("throws ConfigError when phone number is missing", async () => {
|
|
76
77
|
mockLoadConfigResult = {
|
|
77
78
|
twilio: {
|
|
78
79
|
accountSid: "AC_test_sid",
|
|
79
80
|
phoneNumber: "",
|
|
80
81
|
},
|
|
81
82
|
};
|
|
82
|
-
expect(
|
|
83
|
+
expect(getTwilioConfig()).rejects.toThrow(
|
|
83
84
|
/Twilio phone number not configured/,
|
|
84
85
|
);
|
|
85
86
|
});
|
|
86
87
|
|
|
87
|
-
test("throws ConfigError when twilio config section is absent", () => {
|
|
88
|
+
test("throws ConfigError when twilio config section is absent", async () => {
|
|
88
89
|
mockLoadConfigResult = {};
|
|
89
|
-
expect(
|
|
90
|
+
expect(getTwilioConfig()).rejects.toThrow(
|
|
90
91
|
/Twilio credentials not configured/,
|
|
91
92
|
);
|
|
92
93
|
});
|
|
@@ -46,6 +46,11 @@ mock.module("../security/secure-keys.js", () => ({
|
|
|
46
46
|
if (key === credentialKey("twilio", "account_sid")) return mockAccountSid;
|
|
47
47
|
return undefined;
|
|
48
48
|
},
|
|
49
|
+
getSecureKeyAsync: async (key: string) => {
|
|
50
|
+
if (key === credentialKey("twilio", "auth_token")) return mockAuthToken;
|
|
51
|
+
if (key === credentialKey("twilio", "account_sid")) return mockAccountSid;
|
|
52
|
+
return undefined;
|
|
53
|
+
},
|
|
49
54
|
}));
|
|
50
55
|
|
|
51
56
|
import { TwilioConversationRelayProvider } from "../calls/twilio-provider.js";
|
|
@@ -143,15 +148,15 @@ describe("TwilioConversationRelayProvider", () => {
|
|
|
143
148
|
});
|
|
144
149
|
|
|
145
150
|
describe("getAuthToken", () => {
|
|
146
|
-
test("returns the auth token when configured", () => {
|
|
151
|
+
test("returns the auth token when configured", async () => {
|
|
147
152
|
mockAuthToken = "my-secret-token";
|
|
148
|
-
const token = TwilioConversationRelayProvider.getAuthToken();
|
|
153
|
+
const token = await TwilioConversationRelayProvider.getAuthToken();
|
|
149
154
|
expect(token).toBe("my-secret-token");
|
|
150
155
|
});
|
|
151
156
|
|
|
152
|
-
test("returns null when auth token is not configured", () => {
|
|
157
|
+
test("returns null when auth token is not configured", async () => {
|
|
153
158
|
mockAuthToken = undefined;
|
|
154
|
-
const token = TwilioConversationRelayProvider.getAuthToken();
|
|
159
|
+
const token = await TwilioConversationRelayProvider.getAuthToken();
|
|
155
160
|
expect(token).toBeNull();
|
|
156
161
|
});
|
|
157
162
|
});
|
|
@@ -23,7 +23,11 @@ mock.module("../util/logger.js", () => ({
|
|
|
23
23
|
}),
|
|
24
24
|
}));
|
|
25
25
|
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
findContactChannel,
|
|
28
|
+
getContact,
|
|
29
|
+
upsertContact,
|
|
30
|
+
} from "../contacts/contact-store.js";
|
|
27
31
|
import { upsertContactChannel } from "../contacts/contacts-write.js";
|
|
28
32
|
import { getSqlite, initializeDb, resetDb } from "../memory/db.js";
|
|
29
33
|
import { createInvite, revokeInvite } from "../memory/invite-store.js";
|
|
@@ -47,6 +51,11 @@ function resetTables() {
|
|
|
47
51
|
getSqlite().run("DELETE FROM contacts");
|
|
48
52
|
}
|
|
49
53
|
|
|
54
|
+
/** Create a throwaway contact and return its ID, for use as the invite's contactId. */
|
|
55
|
+
function createTargetContact(displayName = "Target Contact"): string {
|
|
56
|
+
return upsertContact({ displayName, role: "contact" }).id;
|
|
57
|
+
}
|
|
58
|
+
|
|
50
59
|
// ---------------------------------------------------------------------------
|
|
51
60
|
// generateVoiceCode
|
|
52
61
|
// ---------------------------------------------------------------------------
|
|
@@ -140,14 +149,18 @@ describe("redeemVoiceInviteCode", () => {
|
|
|
140
149
|
expiresInMs?: number;
|
|
141
150
|
voiceCodeDigits?: number;
|
|
142
151
|
assistantId?: string;
|
|
152
|
+
contactId?: string;
|
|
143
153
|
} = {},
|
|
144
154
|
) {
|
|
145
155
|
const digits = opts.voiceCodeDigits ?? 6;
|
|
146
156
|
const code = generateVoiceCode(digits);
|
|
147
157
|
const codeHash = hashVoiceCode(code);
|
|
148
158
|
|
|
159
|
+
const contactId = opts.contactId ?? createTargetContact();
|
|
160
|
+
|
|
149
161
|
const { invite } = createInvite({
|
|
150
162
|
sourceChannel: "phone",
|
|
163
|
+
contactId,
|
|
151
164
|
maxUses: opts.maxUses ?? 1,
|
|
152
165
|
expiresInMs: opts.expiresInMs,
|
|
153
166
|
expectedExternalUserId: opts.callerPhone ?? "+15551234567",
|
|
@@ -277,11 +290,13 @@ describe("redeemVoiceInviteCode", () => {
|
|
|
277
290
|
// Create a non-voice invite with voice code metadata to simulate a
|
|
278
291
|
// hypothetical misconfiguration. The redemption service filters by
|
|
279
292
|
// sourceChannel='phone', so non-phone invites are invisible.
|
|
293
|
+
const targetContactId = createTargetContact();
|
|
280
294
|
const code = generateVoiceCode(6);
|
|
281
295
|
const codeHash = hashVoiceCode(code);
|
|
282
296
|
|
|
283
297
|
createInvite({
|
|
284
298
|
sourceChannel: "telegram",
|
|
299
|
+
contactId: targetContactId,
|
|
285
300
|
maxUses: 1,
|
|
286
301
|
expectedExternalUserId: "+15551234567",
|
|
287
302
|
voiceCodeHash: codeHash,
|
|
@@ -301,16 +316,21 @@ describe("redeemVoiceInviteCode", () => {
|
|
|
301
316
|
|
|
302
317
|
test("already-member caller gets already_member outcome", () => {
|
|
303
318
|
const phone = "+15551234567";
|
|
304
|
-
const { code } = createVoiceInvite({ callerPhone: phone });
|
|
305
319
|
|
|
306
320
|
// Pre-create an active member for this phone on voice channel
|
|
307
|
-
upsertContactChannel({
|
|
321
|
+
const member = upsertContactChannel({
|
|
308
322
|
sourceChannel: "phone",
|
|
309
323
|
externalUserId: phone,
|
|
310
324
|
status: "active",
|
|
311
325
|
policy: "allow",
|
|
312
326
|
});
|
|
313
327
|
|
|
328
|
+
// Create a voice invite targeting the same contact that owns the channel
|
|
329
|
+
const { code } = createVoiceInvite({
|
|
330
|
+
callerPhone: phone,
|
|
331
|
+
contactId: member!.contact.id,
|
|
332
|
+
});
|
|
333
|
+
|
|
314
334
|
const result = redeemVoiceInviteCode({
|
|
315
335
|
callerExternalUserId: phone,
|
|
316
336
|
sourceChannel: "phone",
|
|
@@ -327,15 +347,21 @@ describe("redeemVoiceInviteCode", () => {
|
|
|
327
347
|
|
|
328
348
|
test("blocked member gets generic failure to avoid leaking membership status", () => {
|
|
329
349
|
const phone = "+15551234567";
|
|
330
|
-
const { code } = createVoiceInvite({ callerPhone: phone });
|
|
331
350
|
|
|
332
|
-
|
|
351
|
+
// Pre-create a blocked member and find their contact
|
|
352
|
+
const member = upsertContactChannel({
|
|
333
353
|
sourceChannel: "phone",
|
|
334
354
|
externalUserId: phone,
|
|
335
355
|
status: "blocked",
|
|
336
356
|
policy: "deny",
|
|
337
357
|
});
|
|
338
358
|
|
|
359
|
+
// Create a voice invite targeting the same contact that owns the channel
|
|
360
|
+
const { code } = createVoiceInvite({
|
|
361
|
+
callerPhone: phone,
|
|
362
|
+
contactId: member!.contact.id,
|
|
363
|
+
});
|
|
364
|
+
|
|
339
365
|
const result = redeemVoiceInviteCode({
|
|
340
366
|
callerExternalUserId: phone,
|
|
341
367
|
sourceChannel: "phone",
|
|
@@ -354,4 +380,58 @@ describe("redeemVoiceInviteCode", () => {
|
|
|
354
380
|
|
|
355
381
|
expect(result).toEqual({ ok: false, reason: "invalid_or_expired" });
|
|
356
382
|
});
|
|
383
|
+
|
|
384
|
+
test("binds redeemer to the invite's target contact, not the guardian, on voice redemption", () => {
|
|
385
|
+
const phone = "+15559998888";
|
|
386
|
+
|
|
387
|
+
// Pre-create a guardian contact with a revoked phone channel
|
|
388
|
+
const guardianContact = upsertContact({
|
|
389
|
+
displayName: "Guardian",
|
|
390
|
+
role: "guardian",
|
|
391
|
+
channels: [
|
|
392
|
+
{
|
|
393
|
+
type: "phone",
|
|
394
|
+
address: phone,
|
|
395
|
+
externalUserId: phone,
|
|
396
|
+
status: "revoked",
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Create a separate target contact "Mom"
|
|
402
|
+
const momContact = upsertContact({
|
|
403
|
+
displayName: "Mom",
|
|
404
|
+
role: "contact",
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Create a voice invite targeting Mom's contact
|
|
408
|
+
const { code } = createVoiceInvite({
|
|
409
|
+
callerPhone: phone,
|
|
410
|
+
contactId: momContact.id,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const result = redeemVoiceInviteCode({
|
|
414
|
+
callerExternalUserId: phone,
|
|
415
|
+
sourceChannel: "phone",
|
|
416
|
+
code,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Should succeed — redeemer's channel is bound to Mom
|
|
420
|
+
expect(result.ok).toBe(true);
|
|
421
|
+
expect((result as { type: string }).type).toBe("redeemed");
|
|
422
|
+
|
|
423
|
+
// Verify the redeemer's phone is now bound to Mom's contact
|
|
424
|
+
const contactResult = findContactChannel({
|
|
425
|
+
channelType: "phone",
|
|
426
|
+
externalUserId: phone,
|
|
427
|
+
});
|
|
428
|
+
expect(contactResult).not.toBeNull();
|
|
429
|
+
expect(contactResult!.contact.id).toBe(momContact.id);
|
|
430
|
+
expect(contactResult!.channel.status).toBe("active");
|
|
431
|
+
|
|
432
|
+
// Verify the original guardian contact was NOT modified
|
|
433
|
+
const guardian = getContact(guardianContact.id);
|
|
434
|
+
expect(guardian).not.toBeNull();
|
|
435
|
+
expect(guardian!.role).toBe("guardian");
|
|
436
|
+
});
|
|
357
437
|
});
|
|
@@ -182,6 +182,57 @@ describe("compactAxTreeHistory", () => {
|
|
|
182
182
|
expect(result).toEqual([]);
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
+
test("counts AX trees per block, not per message", () => {
|
|
186
|
+
// One message has two AX tree blocks — they should count as 2 trees
|
|
187
|
+
const messages: Message[] = [
|
|
188
|
+
{
|
|
189
|
+
role: "user",
|
|
190
|
+
content: [
|
|
191
|
+
{
|
|
192
|
+
type: "tool_result",
|
|
193
|
+
tool_use_id: "t1a",
|
|
194
|
+
content: "<ax-tree>\ntree-1a\n</ax-tree>",
|
|
195
|
+
is_error: false,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
type: "tool_result",
|
|
199
|
+
tool_use_id: "t1b",
|
|
200
|
+
content: "<ax-tree>\ntree-1b\n</ax-tree>",
|
|
201
|
+
is_error: false,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
assistantText("ok"),
|
|
206
|
+
axTreeToolResult("t2", "tree-2"),
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
const result = compactAxTreeHistory(messages);
|
|
210
|
+
|
|
211
|
+
// 3 total AX tree blocks, keep last 2 → strip only first block (t1a)
|
|
212
|
+
const msg0 = result[0];
|
|
213
|
+
const block0 = msg0.content[0];
|
|
214
|
+
expect(block0.type).toBe("tool_result");
|
|
215
|
+
if (block0.type === "tool_result") {
|
|
216
|
+
expect(block0.content).toContain("<ax_tree_omitted />");
|
|
217
|
+
expect(block0.content).not.toContain("<ax-tree>");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Second block in same message (t1b) should be kept
|
|
221
|
+
const block1 = msg0.content[1];
|
|
222
|
+
expect(block1.type).toBe("tool_result");
|
|
223
|
+
if (block1.type === "tool_result") {
|
|
224
|
+
expect(block1.content).toContain("<ax-tree>");
|
|
225
|
+
expect(block1.content).not.toContain("<ax_tree_omitted />");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Last message (t2) should also be kept
|
|
229
|
+
const lastBlock = result[2].content[0];
|
|
230
|
+
expect(lastBlock.type).toBe("tool_result");
|
|
231
|
+
if (lastBlock.type === "tool_result") {
|
|
232
|
+
expect(lastBlock.content).toContain("<ax-tree>");
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
185
236
|
test("is pure — does not mutate input messages", () => {
|
|
186
237
|
const messages: Message[] = [
|
|
187
238
|
axTreeToolResult("t1", "tree-1"),
|
package/src/agent/loop.ts
CHANGED
|
@@ -35,6 +35,7 @@ export interface CheckpointInfo {
|
|
|
35
35
|
turnIndex: number;
|
|
36
36
|
toolCount: number;
|
|
37
37
|
hasToolUse: boolean;
|
|
38
|
+
history: Message[]; // current history snapshot for token estimation
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export type CheckpointDecision = "continue" | "yield";
|
|
@@ -71,7 +72,13 @@ export type AgentEvent =
|
|
|
71
72
|
toolUseId: string;
|
|
72
73
|
accumulatedJson: string;
|
|
73
74
|
}
|
|
74
|
-
| {
|
|
75
|
+
| {
|
|
76
|
+
type: "server_tool_start";
|
|
77
|
+
name: string;
|
|
78
|
+
toolUseId: string;
|
|
79
|
+
input: Record<string, unknown>;
|
|
80
|
+
}
|
|
81
|
+
| { type: "server_tool_complete"; toolUseId: string }
|
|
75
82
|
| { type: "error"; error: Error }
|
|
76
83
|
| {
|
|
77
84
|
type: "usage";
|
|
@@ -305,6 +312,12 @@ export class AgentLoop {
|
|
|
305
312
|
type: "server_tool_start",
|
|
306
313
|
name: event.name,
|
|
307
314
|
toolUseId: event.toolUseId,
|
|
315
|
+
input: event.input,
|
|
316
|
+
});
|
|
317
|
+
} else if (event.type === "server_tool_complete") {
|
|
318
|
+
onEvent({
|
|
319
|
+
type: "server_tool_complete",
|
|
320
|
+
toolUseId: event.toolUseId,
|
|
308
321
|
});
|
|
309
322
|
}
|
|
310
323
|
},
|
|
@@ -561,6 +574,7 @@ export class AgentLoop {
|
|
|
561
574
|
turnIndex: toolUseTurns - 1, // 0-based (toolUseTurns was already incremented)
|
|
562
575
|
toolCount: toolUseBlocks.length,
|
|
563
576
|
hasToolUse: true,
|
|
577
|
+
history,
|
|
564
578
|
});
|
|
565
579
|
if (decision === "yield") {
|
|
566
580
|
break;
|
|
@@ -622,40 +636,53 @@ export function escapeAxTreeContent(content: string): string {
|
|
|
622
636
|
* `MAX_AX_TREES_IN_HISTORY` `<ax-tree>` blocks have been replaced with a
|
|
623
637
|
* short placeholder. This keeps the conversation context small so that
|
|
624
638
|
* TTFT does not grow linearly with step count in computer-use sessions.
|
|
639
|
+
*
|
|
640
|
+
* Counting is per-block, not per-message — a single user message can
|
|
641
|
+
* contain multiple tool_result blocks each with their own AX tree snapshot.
|
|
625
642
|
*/
|
|
626
643
|
export function compactAxTreeHistory(messages: Message[]): Message[] {
|
|
627
|
-
// Collect
|
|
628
|
-
const
|
|
644
|
+
// Collect (messageIndex, blockIndex) for every tool_result block with <ax-tree>
|
|
645
|
+
const axBlocks: Array<{ msgIdx: number; blockIdx: number }> = [];
|
|
629
646
|
for (let i = 0; i < messages.length; i++) {
|
|
630
647
|
const msg = messages[i];
|
|
631
648
|
if (msg.role !== "user") continue;
|
|
632
|
-
for (
|
|
649
|
+
for (let j = 0; j < msg.content.length; j++) {
|
|
650
|
+
const block = msg.content[j];
|
|
633
651
|
if (
|
|
634
652
|
block.type === "tool_result" &&
|
|
635
653
|
typeof block.content === "string" &&
|
|
636
654
|
block.content.includes("<ax-tree>")
|
|
637
655
|
) {
|
|
638
|
-
|
|
639
|
-
break;
|
|
656
|
+
axBlocks.push({ msgIdx: i, blockIdx: j });
|
|
640
657
|
}
|
|
641
658
|
}
|
|
642
659
|
}
|
|
643
660
|
|
|
644
|
-
if (
|
|
661
|
+
if (axBlocks.length <= MAX_AX_TREES_IN_HISTORY) {
|
|
645
662
|
return messages;
|
|
646
663
|
}
|
|
647
664
|
|
|
648
|
-
|
|
665
|
+
// Build a set of "msgIdx:blockIdx" keys for blocks that should be stripped
|
|
666
|
+
const toStrip = new Set(
|
|
667
|
+
axBlocks
|
|
668
|
+
.slice(0, -MAX_AX_TREES_IN_HISTORY)
|
|
669
|
+
.map((b) => `${b.msgIdx}:${b.blockIdx}`),
|
|
670
|
+
);
|
|
649
671
|
|
|
650
672
|
return messages.map((msg, idx) => {
|
|
651
|
-
|
|
673
|
+
// Quick check: does this message have any blocks to strip?
|
|
674
|
+
const hasStripTarget = msg.content.some((_, j) =>
|
|
675
|
+
toStrip.has(`${idx}:${j}`),
|
|
676
|
+
);
|
|
677
|
+
if (!hasStripTarget) return msg;
|
|
678
|
+
|
|
652
679
|
return {
|
|
653
680
|
...msg,
|
|
654
|
-
content: msg.content.map((block) => {
|
|
681
|
+
content: msg.content.map((block, j) => {
|
|
655
682
|
if (
|
|
683
|
+
toStrip.has(`${idx}:${j}`) &&
|
|
656
684
|
block.type === "tool_result" &&
|
|
657
|
-
typeof block.content === "string"
|
|
658
|
-
block.content.includes("<ax-tree>")
|
|
685
|
+
typeof block.content === "string"
|
|
659
686
|
) {
|
|
660
687
|
return {
|
|
661
688
|
...block,
|
package/src/approvals/AGENTS.md
CHANGED
|
@@ -16,7 +16,7 @@ Conversational guardian verification control-plane invocation is guardian-only.
|
|
|
16
16
|
|
|
17
17
|
## Memory Provenance Invariant
|
|
18
18
|
|
|
19
|
-
All memory
|
|
19
|
+
All memory retrieval decisions must consider actor-role provenance. Untrusted actors (non-guardian, unverified_channel) must not receive memory recall results. This invariant is enforced in `indexer.ts` (write gate) and `session-memory.ts` (read gate).
|
|
20
20
|
|
|
21
21
|
## Guardian Privilege Isolation Invariant
|
|
22
22
|
|
|
@@ -424,11 +424,17 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
424
424
|
dedupeKey: `trusted-contact:denied:${request.id}`,
|
|
425
425
|
});
|
|
426
426
|
} else if (desktopDeliverUrl && requesterChatId) {
|
|
427
|
+
// For Slack, route to DM via requesterExternalUserId (user ID) instead
|
|
428
|
+
// of requesterChatId (channel ID) to avoid posting in public channels.
|
|
429
|
+
const targetChatId =
|
|
430
|
+
channel === "slack" && requesterExternalUserId
|
|
431
|
+
? requesterExternalUserId
|
|
432
|
+
: requesterChatId;
|
|
427
433
|
try {
|
|
428
434
|
await deliverChannelReply(
|
|
429
435
|
desktopDeliverUrl,
|
|
430
436
|
{
|
|
431
|
-
chatId:
|
|
437
|
+
chatId: targetChatId,
|
|
432
438
|
text: "Your access request has been denied by the guardian.",
|
|
433
439
|
assistantId,
|
|
434
440
|
},
|
|
@@ -601,11 +607,17 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
601
607
|
});
|
|
602
608
|
}
|
|
603
609
|
} else if (desktopDeliverUrl && requesterChatId) {
|
|
610
|
+
// For Slack, route to DM via requesterExternalUserId (user ID) instead
|
|
611
|
+
// of requesterChatId (channel ID) to avoid posting in public channels.
|
|
612
|
+
const targetChatId =
|
|
613
|
+
channel === "slack" && requesterExternalUserId
|
|
614
|
+
? requesterExternalUserId
|
|
615
|
+
: requesterChatId;
|
|
604
616
|
try {
|
|
605
617
|
await deliverChannelReply(
|
|
606
618
|
desktopDeliverUrl,
|
|
607
619
|
{
|
|
608
|
-
chatId:
|
|
620
|
+
chatId: targetChatId,
|
|
609
621
|
text:
|
|
610
622
|
"Your access request has been approved! " +
|
|
611
623
|
"Please enter the 6-digit verification code you receive from the guardian.",
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* same pattern as EmbeddingRuntimeManager.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
9
10
|
import {
|
|
10
11
|
chmodSync,
|
|
11
12
|
existsSync,
|
|
@@ -54,11 +55,69 @@ function npmTarballUrl(pkg: string, version: string): string {
|
|
|
54
55
|
return `https://registry.npmjs.org/${encoded}/-/${basename}-${version}.tgz`;
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
async function fetchNpmIntegrity(
|
|
59
|
+
pkg: string,
|
|
60
|
+
version: string,
|
|
61
|
+
): Promise<string> {
|
|
62
|
+
const encoded = pkg.replace("/", "%2f");
|
|
63
|
+
const metadataUrl = `https://registry.npmjs.org/${encoded}/${version}`;
|
|
64
|
+
const response = await fetch(metadataUrl);
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Failed to fetch npm metadata for ${pkg}@${version}: ${response.status} ${response.statusText}`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const data = (await response.json()) as {
|
|
72
|
+
dist?: { integrity?: string; shasum?: string };
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (
|
|
76
|
+
typeof data.dist?.integrity === "string" &&
|
|
77
|
+
data.dist.integrity.length > 0
|
|
78
|
+
) {
|
|
79
|
+
return data.dist.integrity;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (typeof data.dist?.shasum === "string" && data.dist.shasum.length > 0) {
|
|
83
|
+
return `sha1-${Buffer.from(data.dist.shasum, "hex").toString("base64")}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw new Error(`Missing npm integrity metadata for ${pkg}@${version}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function verifyIntegrity(
|
|
90
|
+
tarball: Uint8Array,
|
|
91
|
+
integrity: string,
|
|
92
|
+
pkg: string,
|
|
93
|
+
version: string,
|
|
94
|
+
): void {
|
|
95
|
+
const [algorithm, expectedDigest] = integrity.split("-", 2);
|
|
96
|
+
if (!algorithm || !expectedDigest) {
|
|
97
|
+
throw new Error(`Invalid integrity metadata for ${pkg}@${version}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (algorithm !== "sha512" && algorithm !== "sha1") {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Unsupported integrity algorithm ${algorithm} for ${pkg}@${version}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const actualDigest = createHash(algorithm).update(tarball).digest("base64");
|
|
107
|
+
if (actualDigest !== expectedDigest) {
|
|
108
|
+
throw new Error(`Integrity verification failed for ${pkg}@${version}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
57
112
|
async function downloadAndExtract(
|
|
113
|
+
pkg: string,
|
|
114
|
+
version: string,
|
|
58
115
|
url: string,
|
|
59
116
|
targetDir: string,
|
|
60
117
|
): Promise<void> {
|
|
61
|
-
log.info({ url, targetDir }, "Downloading npm package");
|
|
118
|
+
log.info({ pkg, version, url, targetDir }, "Downloading npm package");
|
|
119
|
+
|
|
120
|
+
const integrity = await fetchNpmIntegrity(pkg, version);
|
|
62
121
|
|
|
63
122
|
const response = await fetch(url);
|
|
64
123
|
if (!response.ok) {
|
|
@@ -67,7 +126,8 @@ async function downloadAndExtract(
|
|
|
67
126
|
);
|
|
68
127
|
}
|
|
69
128
|
|
|
70
|
-
const tarball = await response.arrayBuffer();
|
|
129
|
+
const tarball = new Uint8Array(await response.arrayBuffer());
|
|
130
|
+
verifyIntegrity(tarball, integrity, pkg, version);
|
|
71
131
|
mkdirSync(targetDir, { recursive: true });
|
|
72
132
|
|
|
73
133
|
const tmpTar = join(targetDir, `download-${Date.now()}.tgz`);
|
|
@@ -191,10 +251,14 @@ async function install(baseDir: string): Promise<void> {
|
|
|
191
251
|
// Download esbuild binary + preact in parallel
|
|
192
252
|
await Promise.all([
|
|
193
253
|
downloadAndExtract(
|
|
254
|
+
`@esbuild/${esbuildPlatform}`,
|
|
255
|
+
ESBUILD_VERSION,
|
|
194
256
|
npmTarballUrl(`@esbuild/${esbuildPlatform}`, ESBUILD_VERSION),
|
|
195
257
|
join(tmpDir, "esbuild-pkg"),
|
|
196
258
|
),
|
|
197
259
|
downloadAndExtract(
|
|
260
|
+
"preact",
|
|
261
|
+
PREACT_VERSION,
|
|
198
262
|
npmTarballUrl("preact", PREACT_VERSION),
|
|
199
263
|
join(tmpDir, "node_modules", "preact"),
|
|
200
264
|
),
|