@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
|
@@ -17,7 +17,10 @@ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
|
17
17
|
const testDir = mkdtempSync(join(tmpdir(), "tc-approval-notifier-test-"));
|
|
18
18
|
|
|
19
19
|
// ── Platform mock ──
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
21
|
+
const realPlatform = require("../util/platform.js");
|
|
20
22
|
mock.module("../util/platform.js", () => ({
|
|
23
|
+
...realPlatform,
|
|
21
24
|
getDataDir: () => testDir,
|
|
22
25
|
isMacOS: () => process.platform === "darwin",
|
|
23
26
|
isLinux: () => process.platform === "linux",
|
|
@@ -34,7 +37,10 @@ mock.module("../util/platform.js", () => ({
|
|
|
34
37
|
}));
|
|
35
38
|
|
|
36
39
|
// ── Logger mock ──
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
41
|
+
const realLogger = require("../util/logger.js");
|
|
37
42
|
mock.module("../util/logger.js", () => ({
|
|
43
|
+
...realLogger,
|
|
38
44
|
getLogger: () =>
|
|
39
45
|
new Proxy({} as Record<string, unknown>, {
|
|
40
46
|
get: () => () => {},
|
|
@@ -130,9 +136,11 @@ mock.module("../config/env.js", () => ({
|
|
|
130
136
|
}));
|
|
131
137
|
|
|
132
138
|
// ── User reference mock ──
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
140
|
+
const realUserReference = require("../config/user-reference.js");
|
|
133
141
|
mock.module("../config/user-reference.js", () => ({
|
|
142
|
+
...realUserReference,
|
|
134
143
|
resolveUserReference: () => "my human",
|
|
135
|
-
DEFAULT_USER_REFERENCE: "my human",
|
|
136
144
|
resolveGuardianName: (guardianDisplayName?: string | null): string => {
|
|
137
145
|
// Mirror the real implementation: USER.md name > guardianDisplayName > default
|
|
138
146
|
const userRef = "my human"; // In tests, resolveUserReference() returns this
|
|
@@ -79,9 +79,9 @@ mock.module("../security/secure-keys.js", () => ({
|
|
|
79
79
|
deleteSecureKey: (account: string) => {
|
|
80
80
|
if (account in secureKeyStore) {
|
|
81
81
|
delete secureKeyStore[account];
|
|
82
|
-
return
|
|
82
|
+
return "deleted";
|
|
83
83
|
}
|
|
84
|
-
return
|
|
84
|
+
return "not-found";
|
|
85
85
|
},
|
|
86
86
|
listSecureKeys: () => Object.keys(secureKeyStore),
|
|
87
87
|
getBackendType: () => "encrypted",
|
|
@@ -10,7 +10,7 @@ mock.module("../security/secure-keys.js", () => ({
|
|
|
10
10
|
secureKeyStore[account] = value;
|
|
11
11
|
return true;
|
|
12
12
|
},
|
|
13
|
-
deleteSecureKey: () =>
|
|
13
|
+
deleteSecureKey: () => "deleted",
|
|
14
14
|
listSecureKeys: () => Object.keys(secureKeyStore),
|
|
15
15
|
getBackendType: () => "encrypted",
|
|
16
16
|
isDowngradedFromKeychain: () => false,
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
const testDir = mkdtempSync(join(tmpdir(), "usage-routes-test-"));
|
|
7
|
+
|
|
8
|
+
mock.module("../util/platform.js", () => ({
|
|
9
|
+
getDataDir: () => testDir,
|
|
10
|
+
isMacOS: () => process.platform === "darwin",
|
|
11
|
+
isLinux: () => process.platform === "linux",
|
|
12
|
+
isWindows: () => process.platform === "win32",
|
|
13
|
+
getSocketPath: () => join(testDir, "test.sock"),
|
|
14
|
+
getPidPath: () => join(testDir, "test.pid"),
|
|
15
|
+
getDbPath: () => join(testDir, "test.db"),
|
|
16
|
+
getLogPath: () => join(testDir, "test.log"),
|
|
17
|
+
ensureDataDir: () => {},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
mock.module("../util/logger.js", () => ({
|
|
21
|
+
getLogger: () =>
|
|
22
|
+
new Proxy({} as Record<string, unknown>, {
|
|
23
|
+
get: () => () => {},
|
|
24
|
+
}),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
import { getSqlite, initializeDb, resetDb } from "../memory/db.js";
|
|
28
|
+
import { recordUsageEvent } from "../memory/llm-usage-store.js";
|
|
29
|
+
import { usageRouteDefinitions } from "../runtime/routes/usage-routes.js";
|
|
30
|
+
|
|
31
|
+
initializeDb();
|
|
32
|
+
|
|
33
|
+
afterAll(() => {
|
|
34
|
+
resetDb();
|
|
35
|
+
try {
|
|
36
|
+
rmSync(testDir, { recursive: true });
|
|
37
|
+
} catch {
|
|
38
|
+
/* best effort */
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
function clearUsageEvents() {
|
|
43
|
+
getSqlite().run("DELETE FROM llm_usage_events");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Build a simple dispatch helper from route definitions
|
|
47
|
+
const routes = usageRouteDefinitions();
|
|
48
|
+
|
|
49
|
+
function dispatch(method: string, path: string): Promise<Response> | Response {
|
|
50
|
+
const url = new URL(`http://localhost/v1/${path}`);
|
|
51
|
+
const req = new Request(url.toString(), { method });
|
|
52
|
+
const route = routes.find(
|
|
53
|
+
(r) =>
|
|
54
|
+
r.method === method &&
|
|
55
|
+
`usage/${url.pathname.split("/v1/usage/")[1]?.split("?")[0]}` ===
|
|
56
|
+
r.endpoint,
|
|
57
|
+
);
|
|
58
|
+
if (!route) throw new Error(`No route for ${method} /v1/${path}`);
|
|
59
|
+
return route.handler({
|
|
60
|
+
req,
|
|
61
|
+
url,
|
|
62
|
+
server: null as never,
|
|
63
|
+
authContext: {} as never,
|
|
64
|
+
params: {},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Seed data helper
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
function seedEvents() {
|
|
73
|
+
const day1 = new Date("2025-01-15T10:00:00Z").getTime();
|
|
74
|
+
const day2 = new Date("2025-01-16T14:00:00Z").getTime();
|
|
75
|
+
|
|
76
|
+
// Two events on day 1, one on day 2
|
|
77
|
+
recordUsageEvent(
|
|
78
|
+
{
|
|
79
|
+
conversationId: "conv-1",
|
|
80
|
+
runId: "run-1",
|
|
81
|
+
requestId: "req-1",
|
|
82
|
+
actor: "main_agent",
|
|
83
|
+
provider: "anthropic",
|
|
84
|
+
model: "claude-sonnet-4-20250514",
|
|
85
|
+
inputTokens: 1000,
|
|
86
|
+
outputTokens: 200,
|
|
87
|
+
cacheCreationInputTokens: 50,
|
|
88
|
+
cacheReadInputTokens: 100,
|
|
89
|
+
},
|
|
90
|
+
{ estimatedCostUsd: 0.005, pricingStatus: "priced" },
|
|
91
|
+
);
|
|
92
|
+
// Backdate the first event
|
|
93
|
+
getSqlite().run(
|
|
94
|
+
"UPDATE llm_usage_events SET created_at = ? WHERE request_id = 'req-1'",
|
|
95
|
+
[day1],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
recordUsageEvent(
|
|
99
|
+
{
|
|
100
|
+
conversationId: "conv-1",
|
|
101
|
+
runId: "run-1",
|
|
102
|
+
requestId: "req-2",
|
|
103
|
+
actor: "context_compactor",
|
|
104
|
+
provider: "anthropic",
|
|
105
|
+
model: "claude-haiku-3",
|
|
106
|
+
inputTokens: 500,
|
|
107
|
+
outputTokens: 100,
|
|
108
|
+
cacheCreationInputTokens: 0,
|
|
109
|
+
cacheReadInputTokens: 0,
|
|
110
|
+
},
|
|
111
|
+
{ estimatedCostUsd: 0.001, pricingStatus: "priced" },
|
|
112
|
+
);
|
|
113
|
+
getSqlite().run(
|
|
114
|
+
"UPDATE llm_usage_events SET created_at = ? WHERE request_id = 'req-2'",
|
|
115
|
+
[day1 + 3600_000],
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
recordUsageEvent(
|
|
119
|
+
{
|
|
120
|
+
conversationId: "conv-2",
|
|
121
|
+
runId: "run-2",
|
|
122
|
+
requestId: "req-3",
|
|
123
|
+
actor: "main_agent",
|
|
124
|
+
provider: "openai",
|
|
125
|
+
model: "gpt-4o",
|
|
126
|
+
inputTokens: 2000,
|
|
127
|
+
outputTokens: 400,
|
|
128
|
+
cacheCreationInputTokens: 0,
|
|
129
|
+
cacheReadInputTokens: 0,
|
|
130
|
+
},
|
|
131
|
+
{ estimatedCostUsd: 0, pricingStatus: "unpriced" },
|
|
132
|
+
);
|
|
133
|
+
getSqlite().run(
|
|
134
|
+
"UPDATE llm_usage_events SET created_at = ? WHERE request_id = 'req-3'",
|
|
135
|
+
[day2],
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
return { day1, day2 };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Tests
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
describe("usage routes", () => {
|
|
146
|
+
beforeEach(clearUsageEvents);
|
|
147
|
+
|
|
148
|
+
// -- query parsing / validation --
|
|
149
|
+
|
|
150
|
+
describe("query parameter validation", () => {
|
|
151
|
+
test("returns 400 when from/to are missing", async () => {
|
|
152
|
+
const res = await dispatch("GET", "usage/totals");
|
|
153
|
+
expect(res.status).toBe(400);
|
|
154
|
+
const body = (await res.json()) as { error: { message: string } };
|
|
155
|
+
expect(body.error.message).toContain("from");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("returns 400 when from is missing", async () => {
|
|
159
|
+
const res = await dispatch("GET", "usage/totals?to=1000");
|
|
160
|
+
expect(res.status).toBe(400);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("returns 400 when to is missing", async () => {
|
|
164
|
+
const res = await dispatch("GET", "usage/totals?from=1000");
|
|
165
|
+
expect(res.status).toBe(400);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("returns 400 when from/to are not numbers", async () => {
|
|
169
|
+
const res = await dispatch("GET", "usage/totals?from=abc&to=def");
|
|
170
|
+
expect(res.status).toBe(400);
|
|
171
|
+
const body = (await res.json()) as { error: { message: string } };
|
|
172
|
+
expect(body.error.message).toContain("valid numbers");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("returns 400 when from > to", async () => {
|
|
176
|
+
const res = await dispatch("GET", "usage/totals?from=2000&to=1000");
|
|
177
|
+
expect(res.status).toBe(400);
|
|
178
|
+
const body = (await res.json()) as { error: { message: string } };
|
|
179
|
+
expect(body.error.message).toContain("less than or equal");
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// -- totals --
|
|
184
|
+
|
|
185
|
+
describe("GET /v1/usage/totals", () => {
|
|
186
|
+
test("returns zeros for empty range", async () => {
|
|
187
|
+
const res = await dispatch("GET", "usage/totals?from=0&to=999999999999");
|
|
188
|
+
expect(res.status).toBe(200);
|
|
189
|
+
const body = (await res.json()) as Record<string, number>;
|
|
190
|
+
expect(body.totalInputTokens).toBe(0);
|
|
191
|
+
expect(body.totalOutputTokens).toBe(0);
|
|
192
|
+
expect(body.totalEstimatedCostUsd).toBe(0);
|
|
193
|
+
expect(body.eventCount).toBe(0);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("returns correct totals for seeded data", async () => {
|
|
197
|
+
const { day1, day2 } = seedEvents();
|
|
198
|
+
const from = day1 - 1000;
|
|
199
|
+
const to = day2 + 1000;
|
|
200
|
+
|
|
201
|
+
const res = await dispatch("GET", `usage/totals?from=${from}&to=${to}`);
|
|
202
|
+
expect(res.status).toBe(200);
|
|
203
|
+
const body = (await res.json()) as Record<string, number>;
|
|
204
|
+
expect(body.totalInputTokens).toBe(3500);
|
|
205
|
+
expect(body.totalOutputTokens).toBe(700);
|
|
206
|
+
expect(body.totalCacheCreationTokens).toBe(50);
|
|
207
|
+
expect(body.totalCacheReadTokens).toBe(100);
|
|
208
|
+
expect(body.eventCount).toBe(3);
|
|
209
|
+
expect(body.pricedEventCount).toBe(2);
|
|
210
|
+
expect(body.unpricedEventCount).toBe(1);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("filters by time range", async () => {
|
|
214
|
+
const { day1 } = seedEvents();
|
|
215
|
+
// Only day 1 events
|
|
216
|
+
const from = day1 - 1000;
|
|
217
|
+
const to = day1 + 86400_000 - 1;
|
|
218
|
+
|
|
219
|
+
const res = await dispatch("GET", `usage/totals?from=${from}&to=${to}`);
|
|
220
|
+
const body = (await res.json()) as Record<string, number>;
|
|
221
|
+
expect(body.eventCount).toBe(2);
|
|
222
|
+
expect(body.totalInputTokens).toBe(1500);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// -- daily buckets --
|
|
227
|
+
|
|
228
|
+
describe("GET /v1/usage/daily", () => {
|
|
229
|
+
test("returns empty buckets array for empty range", async () => {
|
|
230
|
+
const res = await dispatch("GET", "usage/daily?from=0&to=999999999999");
|
|
231
|
+
expect(res.status).toBe(200);
|
|
232
|
+
const body = (await res.json()) as { buckets: unknown[] };
|
|
233
|
+
expect(body.buckets).toEqual([]);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("returns daily buckets for seeded data", async () => {
|
|
237
|
+
const { day1, day2 } = seedEvents();
|
|
238
|
+
const from = day1 - 1000;
|
|
239
|
+
const to = day2 + 1000;
|
|
240
|
+
|
|
241
|
+
const res = await dispatch("GET", `usage/daily?from=${from}&to=${to}`);
|
|
242
|
+
expect(res.status).toBe(200);
|
|
243
|
+
const body = (await res.json()) as {
|
|
244
|
+
buckets: Array<{
|
|
245
|
+
date: string;
|
|
246
|
+
totalInputTokens: number;
|
|
247
|
+
eventCount: number;
|
|
248
|
+
}>;
|
|
249
|
+
};
|
|
250
|
+
expect(body.buckets).toHaveLength(2);
|
|
251
|
+
expect(body.buckets[0].date).toBe("2025-01-15");
|
|
252
|
+
expect(body.buckets[0].eventCount).toBe(2);
|
|
253
|
+
expect(body.buckets[1].date).toBe("2025-01-16");
|
|
254
|
+
expect(body.buckets[1].eventCount).toBe(1);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// -- breakdown --
|
|
259
|
+
|
|
260
|
+
describe("GET /v1/usage/breakdown", () => {
|
|
261
|
+
test("returns 400 when groupBy is missing", async () => {
|
|
262
|
+
const res = await dispatch(
|
|
263
|
+
"GET",
|
|
264
|
+
"usage/breakdown?from=0&to=999999999999",
|
|
265
|
+
);
|
|
266
|
+
expect(res.status).toBe(400);
|
|
267
|
+
const body = (await res.json()) as { error: { message: string } };
|
|
268
|
+
expect(body.error.message).toContain("groupBy");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("returns 400 for invalid groupBy value", async () => {
|
|
272
|
+
const res = await dispatch(
|
|
273
|
+
"GET",
|
|
274
|
+
"usage/breakdown?from=0&to=999999999999&groupBy=invalid",
|
|
275
|
+
);
|
|
276
|
+
expect(res.status).toBe(400);
|
|
277
|
+
const body = (await res.json()) as { error: { message: string } };
|
|
278
|
+
expect(body.error.message).toContain("invalid");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("groups by provider", async () => {
|
|
282
|
+
const { day1, day2 } = seedEvents();
|
|
283
|
+
const from = day1 - 1000;
|
|
284
|
+
const to = day2 + 1000;
|
|
285
|
+
|
|
286
|
+
const res = await dispatch(
|
|
287
|
+
"GET",
|
|
288
|
+
`usage/breakdown?from=${from}&to=${to}&groupBy=provider`,
|
|
289
|
+
);
|
|
290
|
+
expect(res.status).toBe(200);
|
|
291
|
+
const body = (await res.json()) as {
|
|
292
|
+
breakdown: Array<{
|
|
293
|
+
group: string;
|
|
294
|
+
totalInputTokens: number;
|
|
295
|
+
eventCount: number;
|
|
296
|
+
}>;
|
|
297
|
+
};
|
|
298
|
+
expect(body.breakdown).toHaveLength(2);
|
|
299
|
+
const groups = body.breakdown.map((b) => b.group).sort();
|
|
300
|
+
expect(groups).toEqual(["anthropic", "openai"]);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("groups by actor", async () => {
|
|
304
|
+
const { day1, day2 } = seedEvents();
|
|
305
|
+
const from = day1 - 1000;
|
|
306
|
+
const to = day2 + 1000;
|
|
307
|
+
|
|
308
|
+
const res = await dispatch(
|
|
309
|
+
"GET",
|
|
310
|
+
`usage/breakdown?from=${from}&to=${to}&groupBy=actor`,
|
|
311
|
+
);
|
|
312
|
+
expect(res.status).toBe(200);
|
|
313
|
+
const body = (await res.json()) as {
|
|
314
|
+
breakdown: Array<{ group: string; eventCount: number }>;
|
|
315
|
+
};
|
|
316
|
+
expect(body.breakdown).toHaveLength(2);
|
|
317
|
+
const assistantGroup = body.breakdown.find(
|
|
318
|
+
(b) => b.group === "main_agent",
|
|
319
|
+
);
|
|
320
|
+
expect(assistantGroup?.eventCount).toBe(2);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("groups by model", async () => {
|
|
324
|
+
const { day1, day2 } = seedEvents();
|
|
325
|
+
const from = day1 - 1000;
|
|
326
|
+
const to = day2 + 1000;
|
|
327
|
+
|
|
328
|
+
const res = await dispatch(
|
|
329
|
+
"GET",
|
|
330
|
+
`usage/breakdown?from=${from}&to=${to}&groupBy=model`,
|
|
331
|
+
);
|
|
332
|
+
expect(res.status).toBe(200);
|
|
333
|
+
const body = (await res.json()) as {
|
|
334
|
+
breakdown: Array<{ group: string; eventCount: number }>;
|
|
335
|
+
};
|
|
336
|
+
expect(body.breakdown).toHaveLength(3);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the WhatsApp channel invite adapter.
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp uses Meta WhatsApp Business API, not Twilio. The display phone
|
|
5
|
+
* number is resolved from workspace config (`whatsapp.phoneNumber`), falling
|
|
6
|
+
* back to undefined (triggering generic instructions) when not configured.
|
|
7
|
+
*/
|
|
8
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Mocks — must be set up before importing the adapter
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
let mockWhatsAppPhoneNumber: string | undefined;
|
|
15
|
+
let mockGetConfigThrows = false;
|
|
16
|
+
|
|
17
|
+
mock.module("../config/loader.js", () => ({
|
|
18
|
+
loadRawConfig: () => ({}),
|
|
19
|
+
getConfig: () => {
|
|
20
|
+
if (mockGetConfigThrows) throw new Error("config not found");
|
|
21
|
+
return { whatsapp: { phoneNumber: mockWhatsAppPhoneNumber ?? "" } };
|
|
22
|
+
},
|
|
23
|
+
invalidateConfigCache: () => {},
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Import under test
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
import { whatsappInviteAdapter } from "../runtime/channel-invite-transports/whatsapp.js";
|
|
31
|
+
import { resolveWhatsAppDisplayNumber } from "../runtime/channel-invite-transports/whatsapp.js";
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Tests
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
describe("whatsapp invite adapter", () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
mockWhatsAppPhoneNumber = undefined;
|
|
40
|
+
mockGetConfigThrows = false;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("adapter is registered for the whatsapp channel", () => {
|
|
44
|
+
expect(whatsappInviteAdapter.channel).toBe("whatsapp");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// -------------------------------------------------------------------------
|
|
48
|
+
// Handle resolution — configured path
|
|
49
|
+
// -------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
test("returns configured phone number from workspace config", () => {
|
|
52
|
+
mockWhatsAppPhoneNumber = "+15551234567";
|
|
53
|
+
const handle = whatsappInviteAdapter.resolveChannelHandle!();
|
|
54
|
+
expect(handle).toBe("+15551234567");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("resolveWhatsAppDisplayNumber returns configured number", () => {
|
|
58
|
+
mockWhatsAppPhoneNumber = "+15559876543";
|
|
59
|
+
expect(resolveWhatsAppDisplayNumber()).toBe("+15559876543");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// -------------------------------------------------------------------------
|
|
63
|
+
// Handle resolution — unconfigured fallback
|
|
64
|
+
// -------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
test("returns undefined when whatsapp config is missing", () => {
|
|
67
|
+
const handle = whatsappInviteAdapter.resolveChannelHandle!();
|
|
68
|
+
expect(handle).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("returns undefined when phoneNumber is empty string", () => {
|
|
72
|
+
mockWhatsAppPhoneNumber = "";
|
|
73
|
+
const handle = whatsappInviteAdapter.resolveChannelHandle!();
|
|
74
|
+
expect(handle).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("returns undefined when config loading throws", () => {
|
|
78
|
+
mockGetConfigThrows = true;
|
|
79
|
+
const handle = whatsappInviteAdapter.resolveChannelHandle!();
|
|
80
|
+
expect(handle).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// -------------------------------------------------------------------------
|
|
84
|
+
// Adapter shape
|
|
85
|
+
// -------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
test("does not implement buildShareLink", () => {
|
|
88
|
+
expect(whatsappInviteAdapter.buildShareLink).toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("does not implement extractInboundToken", () => {
|
|
92
|
+
expect(whatsappInviteAdapter.extractInboundToken).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
});
|
package/src/agent/loop.ts
CHANGED
|
@@ -115,6 +115,7 @@ export class AgentLoop {
|
|
|
115
115
|
name: string,
|
|
116
116
|
input: Record<string, unknown>,
|
|
117
117
|
onOutput?: (chunk: string) => void,
|
|
118
|
+
toolUseId?: string,
|
|
118
119
|
) => Promise<{
|
|
119
120
|
content: string;
|
|
120
121
|
isError: boolean;
|
|
@@ -140,6 +141,7 @@ export class AgentLoop {
|
|
|
140
141
|
name: string,
|
|
141
142
|
input: Record<string, unknown>,
|
|
142
143
|
onOutput?: (chunk: string) => void,
|
|
144
|
+
toolUseId?: string,
|
|
143
145
|
) => Promise<{
|
|
144
146
|
content: string;
|
|
145
147
|
isError: boolean;
|
|
@@ -507,6 +509,7 @@ export class AgentLoop {
|
|
|
507
509
|
chunk,
|
|
508
510
|
});
|
|
509
511
|
},
|
|
512
|
+
toolUse.id,
|
|
510
513
|
);
|
|
511
514
|
|
|
512
515
|
const toolDurationMs = Date.now() - toolStart;
|
package/src/amazon/checkout.ts
CHANGED
|
@@ -291,7 +291,6 @@ export async function getCheckoutSummary(): Promise<CheckoutSummary> {
|
|
|
291
291
|
export async function placeOrder(
|
|
292
292
|
opts: {
|
|
293
293
|
paymentMethodId?: string;
|
|
294
|
-
deliverySlotId?: string;
|
|
295
294
|
} = {},
|
|
296
295
|
): Promise<PlaceOrderResult> {
|
|
297
296
|
const { tabId } = await prepareRequest();
|
|
@@ -19,6 +19,10 @@ import {
|
|
|
19
19
|
getCanonicalGuardianRequest,
|
|
20
20
|
} from "../memory/canonical-guardian-store.js";
|
|
21
21
|
import { emitNotificationSignal } from "../notifications/emit-signal.js";
|
|
22
|
+
import {
|
|
23
|
+
isNotificationSourceChannel,
|
|
24
|
+
type NotificationSourceChannel,
|
|
25
|
+
} from "../notifications/signal.js";
|
|
22
26
|
import { addRule } from "../permissions/trust-store.js";
|
|
23
27
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
|
|
24
28
|
import { mintDaemonDeliveryToken } from "../runtime/auth/token-service.js";
|
|
@@ -345,7 +349,11 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
345
349
|
|
|
346
350
|
async resolve(ctx: ResolverContext): Promise<ResolverResult> {
|
|
347
351
|
const { request, decision, channelDeliveryContext } = ctx;
|
|
348
|
-
const channel =
|
|
352
|
+
const channel: NotificationSourceChannel = isNotificationSourceChannel(
|
|
353
|
+
request.sourceChannel,
|
|
354
|
+
)
|
|
355
|
+
? request.sourceChannel
|
|
356
|
+
: "vellum";
|
|
349
357
|
const requesterExternalUserId = request.requesterExternalUserId ?? "";
|
|
350
358
|
const requesterChatId =
|
|
351
359
|
request.requesterChatId ?? request.requesterExternalUserId ?? "";
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Core packaging logic for .
|
|
2
|
+
* Core packaging logic for .vellum zip archives.
|
|
3
3
|
*
|
|
4
4
|
* Reads an app from the app-store, generates a manifest, and produces a
|
|
5
5
|
* zip archive written to a temp file.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { createHash
|
|
9
|
-
import { createWriteStream } from "node:fs";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { createWriteStream, existsSync, readFileSync } from "node:fs";
|
|
10
10
|
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
11
11
|
import { tmpdir } from "node:os";
|
|
12
12
|
import { extname, join } from "node:path";
|
|
@@ -14,7 +14,7 @@ import { extname, join } from "node:path";
|
|
|
14
14
|
import archiver from "archiver";
|
|
15
15
|
import JSZip from "jszip";
|
|
16
16
|
|
|
17
|
-
import { getApp } from "../memory/app-store.js";
|
|
17
|
+
import { getApp, getAppsDir } from "../memory/app-store.js";
|
|
18
18
|
import { computeContentId } from "../util/content-id.js";
|
|
19
19
|
import { getLogger } from "../util/logger.js";
|
|
20
20
|
import type { SigningCallback } from "./bundle-signer.js";
|
|
@@ -27,7 +27,6 @@ const bundlerLog = getLogger("app-bundler");
|
|
|
27
27
|
import { APP_VERSION } from "../version.js";
|
|
28
28
|
const PACKAGE_VERSION = APP_VERSION;
|
|
29
29
|
|
|
30
|
-
const SHORT_HASH_LENGTH = 8;
|
|
31
30
|
const HASH_DISPLAY_LENGTH = 12;
|
|
32
31
|
const MAX_BUNDLE_SIZE_BYTES = 25 * 1024 * 1024; // 25 MB
|
|
33
32
|
const ASSET_FETCH_TIMEOUT_MS = 10_000;
|
|
@@ -173,15 +172,17 @@ export async function materializeAssets(
|
|
|
173
172
|
export interface BundleResult {
|
|
174
173
|
bundlePath: string;
|
|
175
174
|
manifest: AppManifest;
|
|
175
|
+
/** Base64-encoded PNG of the app icon, if one was generated. */
|
|
176
|
+
iconImageBase64?: string;
|
|
176
177
|
}
|
|
177
178
|
|
|
178
179
|
/**
|
|
179
|
-
* Package an app into a .
|
|
180
|
+
* Package an app into a .vellum zip archive.
|
|
180
181
|
*
|
|
181
182
|
* @param appId - The ID of the app to package (from the app-store).
|
|
182
183
|
* @param requestSignature - Optional callback to request an Ed25519 signature from the Swift client.
|
|
183
184
|
* If provided, the bundle will be signed and include a signature.json.
|
|
184
|
-
* @returns The path to the created .
|
|
185
|
+
* @returns The path to the created .vellum file and the manifest.
|
|
185
186
|
* @throws If the app is not found, or the bundle exceeds the size limit.
|
|
186
187
|
*/
|
|
187
188
|
export async function packageApp(
|
|
@@ -236,10 +237,12 @@ export async function packageApp(
|
|
|
236
237
|
const allAssets = [...allAssetsMap.values()];
|
|
237
238
|
|
|
238
239
|
// Create the zip archive
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
240
|
+
const safeName = app.name.replace(/[/\\:*?"<>|]/g, "_").trim() || "App";
|
|
241
|
+
const uniqueSuffix = createHash("sha256")
|
|
242
|
+
.update(`${appId}-${Date.now()}`)
|
|
243
|
+
.digest("hex")
|
|
244
|
+
.slice(0, 8);
|
|
245
|
+
const bundleFilename = `${safeName}-${uniqueSuffix}.vellum`;
|
|
243
246
|
const bundlePath = join(tmpdir(), bundleFilename);
|
|
244
247
|
|
|
245
248
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -277,6 +280,12 @@ export async function packageApp(
|
|
|
277
280
|
archive.append(asset.data, { name: asset.archivePath });
|
|
278
281
|
}
|
|
279
282
|
|
|
283
|
+
// Include app icon if one was generated
|
|
284
|
+
const iconPath = join(getAppsDir(), appId, "icon.png");
|
|
285
|
+
if (existsSync(iconPath)) {
|
|
286
|
+
archive.append(readFileSync(iconPath), { name: "icon.png" });
|
|
287
|
+
}
|
|
288
|
+
|
|
280
289
|
archive.finalize();
|
|
281
290
|
});
|
|
282
291
|
|
|
@@ -318,5 +327,12 @@ export async function packageApp(
|
|
|
318
327
|
);
|
|
319
328
|
}
|
|
320
329
|
|
|
321
|
-
|
|
330
|
+
// Read icon for inclusion in the response
|
|
331
|
+
let iconImageBase64: string | undefined;
|
|
332
|
+
const iconFilePath = join(getAppsDir(), appId, "icon.png");
|
|
333
|
+
if (existsSync(iconFilePath)) {
|
|
334
|
+
iconImageBase64 = readFileSync(iconFilePath).toString("base64");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return { bundlePath, manifest, iconImageBase64 };
|
|
322
338
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* BundleScanner — Security validation and static analysis for .
|
|
2
|
+
* BundleScanner — Security validation and static analysis for .vellum bundles.
|
|
3
3
|
*
|
|
4
4
|
* Validates zip bundles before they are opened, returning structured results
|
|
5
5
|
* with block-level (reject) and warn-level (flag) findings.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bundle signing for .
|
|
2
|
+
* Bundle signing for .vellum archives.
|
|
3
3
|
*
|
|
4
4
|
* Computes content hashes, constructs a canonical signing payload,
|
|
5
5
|
* and requests an Ed25519 signature from the Swift client via IPC.
|
|
@@ -78,9 +78,9 @@ async function computeContentHashes(
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
|
-
* Sign a .
|
|
81
|
+
* Sign a .vellum bundle.
|
|
82
82
|
*
|
|
83
|
-
* @param bundlePath - Path to the .
|
|
83
|
+
* @param bundlePath - Path to the .vellum zip archive.
|
|
84
84
|
* @param requestSignature - Callback to request a signature from the Swift client.
|
|
85
85
|
* @returns The SignatureJson to embed in the archive.
|
|
86
86
|
*/
|
package/src/bundler/manifest.ts
CHANGED