@vellumai/assistant 0.4.48 → 0.4.49
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 +2 -2
- package/README.md +2 -23
- package/docs/architecture/integrations.md +45 -41
- package/docs/architecture/keychain-broker.md +3 -3
- package/docs/runbook-trusted-contacts.md +3 -8
- package/hook-templates/debug-prompt-logger/hook.json +1 -1
- package/hook-templates/debug-prompt-logger/run.sh +1 -3
- package/package.json +1 -1
- package/src/__tests__/actor-token-service.test.ts +0 -1
- package/src/__tests__/anthropic-provider.test.ts +156 -0
- package/src/__tests__/approval-cascade.test.ts +810 -0
- package/src/__tests__/approval-primitive.test.ts +0 -1
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-attachments.test.ts +12 -34
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
- package/src/__tests__/channel-guardian.test.ts +0 -2
- package/src/__tests__/channel-readiness-routes.test.ts +15 -6
- package/src/__tests__/channel-readiness-service.test.ts +10 -9
- package/src/__tests__/checker.test.ts +9 -29
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
- package/src/__tests__/computer-use-tools.test.ts +2 -19
- package/src/__tests__/config-watcher.test.ts +0 -1
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
- package/src/__tests__/context-image-dimensions.test.ts +332 -0
- package/src/__tests__/context-token-estimator.test.ts +196 -13
- package/src/__tests__/conversation-attention-store.test.ts +0 -1
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +144 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-metadata-store.test.ts +64 -73
- package/src/__tests__/credential-security-invariants.test.ts +13 -7
- package/src/__tests__/credential-vault-unit.test.ts +280 -49
- package/src/__tests__/credential-vault.test.ts +138 -16
- package/src/__tests__/credentials-cli.test.ts +71 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
- package/src/__tests__/ephemeral-permissions.test.ts +3 -3
- package/src/__tests__/gateway-only-guard.test.ts +0 -1
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +0 -1
- package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
- package/src/__tests__/heartbeat-service.test.ts +0 -1
- package/src/__tests__/host-cu-proxy.test.ts +629 -0
- package/src/__tests__/host-shell-tool.test.ts +27 -15
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/ingress-url-consistency.test.ts +14 -21
- package/src/__tests__/integration-status.test.ts +32 -51
- package/src/__tests__/intent-routing.test.ts +0 -1
- package/src/__tests__/invite-routes-http.test.ts +10 -9
- package/src/__tests__/keychain-broker-client.test.ts +11 -43
- package/src/__tests__/notification-routing-intent.test.ts +0 -1
- package/src/__tests__/oauth-cli.test.ts +373 -14
- package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
- package/src/__tests__/oauth-scope-policy.test.ts +4 -6
- package/src/__tests__/oauth-store.test.ts +756 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
- package/src/__tests__/provider-error-scenarios.test.ts +0 -1
- package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
- package/src/__tests__/public-ingress-urls.test.ts +15 -21
- package/src/__tests__/recording-handler.test.ts +3 -4
- package/src/__tests__/registry.test.ts +2 -2
- package/src/__tests__/runtime-events-sse.test.ts +55 -7
- package/src/__tests__/schedule-store.test.ts +0 -1
- package/src/__tests__/scheduler-recurrence.test.ts +0 -1
- package/src/__tests__/scoped-approval-grants.test.ts +0 -1
- package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
- package/src/__tests__/secret-ingress-handler.test.ts +0 -1
- package/src/__tests__/send-endpoint-busy.test.ts +21 -6
- package/src/__tests__/sequence-store.test.ts +0 -1
- package/src/__tests__/session-init.benchmark.test.ts +4 -5
- package/src/__tests__/skill-include-graph.test.ts +66 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
- package/src/__tests__/skill-load-tool.test.ts +149 -1
- package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
- package/src/__tests__/skills-uninstall.test.ts +1 -1
- package/src/__tests__/skills.test.ts +3 -3
- package/src/__tests__/slack-channel-config.test.ts +67 -3
- package/src/__tests__/slack-share-routes.test.ts +17 -19
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
- package/src/__tests__/terminal-tools.test.ts +4 -3
- package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
- package/src/__tests__/tool-approval-handler.test.ts +0 -1
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
- package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
- package/src/__tests__/tool-executor.test.ts +0 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
- package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
- package/src/__tests__/trust-store.test.ts +1 -22
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
- package/src/__tests__/twilio-routes.test.ts +0 -16
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/agent/ax-tree-compaction.test.ts +235 -0
- package/src/agent/loop.ts +76 -130
- package/src/calls/call-domain.ts +1 -6
- package/src/calls/relay-server.ts +9 -13
- package/src/calls/twilio-config.ts +2 -7
- package/src/calls/twilio-routes.ts +1 -2
- package/src/calls/voice-ingress-preflight.ts +1 -1
- package/src/cli/commands/browser-relay.ts +18 -12
- package/src/cli/commands/completions.ts +0 -3
- package/src/cli/commands/credentials.ts +101 -15
- package/src/cli/commands/oauth/apps.ts +255 -0
- package/src/cli/commands/oauth/connections.ts +299 -0
- package/src/cli/commands/oauth/index.ts +52 -0
- package/src/cli/commands/oauth/providers.ts +242 -0
- package/src/cli/commands/skills.ts +4 -338
- package/src/cli/program.ts +1 -5
- package/src/cli/reference.ts +1 -3
- package/src/config/assistant-feature-flags.ts +0 -3
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
- package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
- package/src/config/bundled-skills/computer-use/TOOLS.json +22 -4
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +21 -16
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -4
- package/src/config/bundled-skills/settings/SKILL.md +1 -1
- package/src/config/bundled-skills/settings/TOOLS.json +2 -8
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
- package/src/config/env-registry.ts +14 -83
- package/src/config/env.ts +11 -50
- package/src/config/feature-flag-registry.json +16 -16
- package/src/config/loader.ts +0 -6
- package/src/config/schema.ts +3 -1
- package/src/config/skills.ts +21 -2
- package/src/context/image-dimensions.ts +229 -0
- package/src/context/token-estimator.ts +75 -12
- package/src/context/window-manager.ts +49 -10
- package/src/daemon/assistant-attachments.ts +1 -13
- package/src/daemon/handlers/config-ingress.ts +8 -33
- package/src/daemon/handlers/config-slack-channel.ts +49 -46
- package/src/daemon/handlers/config-telegram.ts +32 -16
- package/src/daemon/handlers/sessions.ts +10 -24
- package/src/daemon/handlers/shared.ts +0 -130
- package/src/daemon/host-cu-proxy.ts +401 -0
- package/src/daemon/lifecycle.ts +36 -68
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/computer-use.ts +2 -119
- package/src/daemon/message-types/host-cu.ts +19 -0
- package/src/daemon/message-types/messages.ts +3 -0
- package/src/daemon/server.ts +14 -21
- package/src/daemon/session-agent-loop-handlers.ts +2 -0
- package/src/daemon/session-attachments.ts +1 -2
- package/src/daemon/session-slash.ts +1 -1
- package/src/daemon/session-surfaces.ts +40 -28
- package/src/daemon/session-tool-setup.ts +2 -9
- package/src/daemon/session.ts +138 -15
- package/src/daemon/tool-side-effects.ts +2 -8
- package/src/daemon/watch-handler.ts +2 -2
- package/src/events/tool-metrics-listener.ts +2 -2
- package/src/hooks/manager.ts +1 -4
- package/src/inbound/public-ingress-urls.ts +7 -7
- package/src/logfire.ts +16 -5
- package/src/memory/conversation-key-store.ts +21 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/149-oauth-tables.ts +60 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/oauth.ts +65 -0
- package/src/messaging/provider.ts +4 -4
- package/src/messaging/providers/gmail/client.ts +82 -2
- package/src/messaging/providers/gmail/people-client.ts +10 -10
- package/src/messaging/providers/telegram-bot/adapter.ts +17 -17
- package/src/messaging/providers/whatsapp/adapter.ts +11 -8
- package/src/messaging/registry.ts +2 -32
- package/src/notifications/copy-composer.ts +0 -5
- package/src/notifications/signal.ts +4 -5
- package/src/oauth/byo-connection.test.ts +126 -25
- package/src/oauth/byo-connection.ts +22 -6
- package/src/oauth/connect-orchestrator.ts +113 -57
- package/src/oauth/connect-types.ts +17 -23
- package/src/oauth/connection-resolver.ts +35 -11
- package/src/oauth/connection.ts +1 -1
- package/src/oauth/manual-token-connection.ts +104 -0
- package/src/oauth/oauth-store.ts +496 -0
- package/src/oauth/platform-connection.test.ts +29 -0
- package/src/oauth/platform-connection.ts +6 -5
- package/src/oauth/provider-behaviors.ts +124 -0
- package/src/oauth/scope-policy.ts +9 -2
- package/src/oauth/seed-providers.ts +161 -0
- package/src/oauth/token-persistence.ts +74 -78
- package/src/permissions/checker.ts +3 -3
- package/src/permissions/defaults.ts +0 -1
- package/src/permissions/prompter.ts +10 -1
- package/src/permissions/trust-store.ts +13 -0
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
- package/src/prompts/system-prompt.ts +28 -40
- package/src/providers/anthropic/client.ts +133 -24
- package/src/providers/retry.ts +1 -27
- package/src/runtime/auth/route-policy.ts +0 -3
- package/src/runtime/channel-reply-delivery.ts +0 -40
- package/src/runtime/gateway-client.ts +0 -7
- package/src/runtime/http-server.ts +8 -6
- package/src/runtime/http-types.ts +2 -2
- package/src/runtime/middleware/twilio-validation.ts +1 -11
- package/src/runtime/pending-interactions.ts +14 -12
- package/src/runtime/routes/channel-delivery-routes.ts +0 -1
- package/src/runtime/routes/conversation-routes.ts +73 -19
- package/src/runtime/routes/events-routes.ts +21 -11
- package/src/runtime/routes/host-cu-routes.ts +97 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
- package/src/runtime/routes/integrations/slack/share.ts +6 -7
- package/src/runtime/routes/log-export-routes.ts +126 -8
- package/src/runtime/routes/settings-routes.ts +55 -48
- package/src/runtime/routes/surface-action-routes.ts +1 -1
- package/src/runtime/routes/watch-routes.ts +128 -0
- package/src/schedule/integration-status.ts +10 -9
- package/src/security/credential-key.ts +0 -156
- package/src/security/keychain-broker-client.ts +5 -6
- package/src/security/oauth2.ts +1 -1
- package/src/security/token-manager.ts +119 -46
- package/src/skills/catalog-install.ts +358 -0
- package/src/skills/include-graph.ts +32 -0
- package/src/telegram/bot-username.ts +2 -3
- package/src/tools/browser/network-recorder.ts +1 -1
- package/src/tools/browser/network-recording-types.ts +1 -1
- package/src/tools/computer-use/definitions.ts +46 -11
- package/src/tools/computer-use/registry.ts +4 -5
- package/src/tools/credentials/broker.ts +1 -2
- package/src/tools/credentials/metadata-store.ts +17 -121
- package/src/tools/credentials/vault.ts +94 -167
- package/src/tools/registry.ts +2 -7
- package/src/tools/skills/load.ts +62 -3
- package/src/tools/watch/watch-state.ts +0 -12
- package/src/util/logger.ts +7 -41
- package/src/util/platform.ts +9 -28
- package/src/watcher/providers/google-calendar.ts +2 -1
- package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
- package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
- package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
- package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
- package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
- package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
- package/src/cli/commands/dev.ts +0 -129
- package/src/cli/commands/map.ts +0 -391
- package/src/cli/commands/oauth.ts +0 -77
- package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +0 -16
- package/src/daemon/computer-use-session.ts +0 -1026
- package/src/daemon/ride-shotgun-handler.ts +0 -569
- package/src/oauth/provider-base-urls.ts +0 -21
- package/src/oauth/provider-profiles.ts +0 -192
- package/src/prompts/computer-use-prompt.ts +0 -98
- package/src/runtime/routes/computer-use-routes.ts +0 -641
- package/src/runtime/telegram-streaming-delivery.test.ts +0 -729
- package/src/runtime/telegram-streaming-delivery.ts +0 -393
- package/src/tools/computer-use/request-computer-control.ts +0 -56
|
@@ -18,16 +18,8 @@ const VOICE_SETTINGS = {
|
|
|
18
18
|
userDefaultsKey: "pttActivationKey",
|
|
19
19
|
type: "string" as const,
|
|
20
20
|
},
|
|
21
|
-
|
|
22
|
-
userDefaultsKey: "
|
|
23
|
-
type: "boolean" as const,
|
|
24
|
-
},
|
|
25
|
-
wake_word_keyword: {
|
|
26
|
-
userDefaultsKey: "wakeWordKeyword",
|
|
27
|
-
type: "string" as const,
|
|
28
|
-
},
|
|
29
|
-
wake_word_timeout: {
|
|
30
|
-
userDefaultsKey: "wakeWordTimeoutSeconds",
|
|
21
|
+
conversation_timeout: {
|
|
22
|
+
userDefaultsKey: "voiceConversationTimeoutSeconds",
|
|
31
23
|
type: "number" as const,
|
|
32
24
|
},
|
|
33
25
|
tts_voice_id: { userDefaultsKey: "ttsVoiceId", type: "string" as const },
|
|
@@ -41,9 +33,7 @@ const VALID_TIMEOUTS = [5, 10, 15, 30, 60];
|
|
|
41
33
|
|
|
42
34
|
const FRIENDLY_NAMES: Record<VoiceSettingName, string> = {
|
|
43
35
|
activation_key: "PTT activation key",
|
|
44
|
-
|
|
45
|
-
wake_word_keyword: "Wake word keyword",
|
|
46
|
-
wake_word_timeout: "Wake word timeout",
|
|
36
|
+
conversation_timeout: "Conversation timeout",
|
|
47
37
|
tts_voice_id: "ElevenLabs voice",
|
|
48
38
|
};
|
|
49
39
|
|
|
@@ -76,30 +66,12 @@ function validateSetting(
|
|
|
76
66
|
}
|
|
77
67
|
return { ok: true, coerced: result.value };
|
|
78
68
|
}
|
|
79
|
-
case "
|
|
80
|
-
if (typeof value === "boolean") return { ok: true, coerced: value };
|
|
81
|
-
if (value === "true") return { ok: true, coerced: true };
|
|
82
|
-
if (value === "false") return { ok: true, coerced: false };
|
|
83
|
-
return {
|
|
84
|
-
ok: false,
|
|
85
|
-
error: 'wake_word_enabled must be a boolean (or "true"/"false" string)',
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
case "wake_word_keyword": {
|
|
89
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
90
|
-
return {
|
|
91
|
-
ok: false,
|
|
92
|
-
error: "wake_word_keyword must be a non-empty string",
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
return { ok: true, coerced: value.trim() };
|
|
96
|
-
}
|
|
97
|
-
case "wake_word_timeout": {
|
|
69
|
+
case "conversation_timeout": {
|
|
98
70
|
const num = typeof value === "number" ? value : Number(value);
|
|
99
71
|
if (Number.isNaN(num) || !VALID_TIMEOUTS.includes(num)) {
|
|
100
72
|
return {
|
|
101
73
|
ok: false,
|
|
102
|
-
error: `
|
|
74
|
+
error: `conversation_timeout must be one of: ${VALID_TIMEOUTS.join(
|
|
103
75
|
", ",
|
|
104
76
|
)}`,
|
|
105
77
|
};
|
|
@@ -24,13 +24,6 @@ function flag(name: string): boolean {
|
|
|
24
24
|
return raw === "true" || raw === "1";
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function flagTriState(name: string): boolean | undefined {
|
|
28
|
-
const raw = str(name);
|
|
29
|
-
if (raw === "true" || raw === "1") return true;
|
|
30
|
-
if (raw === "false" || raw === "0") return false;
|
|
31
|
-
return undefined;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
27
|
// ── Registry ─────────────────────────────────────────────────────────────────
|
|
35
28
|
// Each entry documents the env var name, type, default, and purpose.
|
|
36
29
|
|
|
@@ -43,62 +36,6 @@ export function getBaseDataDir(): string | undefined {
|
|
|
43
36
|
return str("BASE_DATA_DIR");
|
|
44
37
|
}
|
|
45
38
|
|
|
46
|
-
/**
|
|
47
|
-
* VELLUM_DAEMON_TCP_PORT — number, default: 8765
|
|
48
|
-
* TCP port for the daemon's TCP listener (used by iOS clients).
|
|
49
|
-
*/
|
|
50
|
-
export function getDaemonTcpPort(): number {
|
|
51
|
-
const raw = str("VELLUM_DAEMON_TCP_PORT");
|
|
52
|
-
if (raw) {
|
|
53
|
-
const port = parseInt(raw, 10);
|
|
54
|
-
if (!isNaN(port) && port > 0 && port <= 65535) return port;
|
|
55
|
-
}
|
|
56
|
-
return 8765;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* VELLUM_DAEMON_TCP_ENABLED — boolean tri-state, default: undefined (falls back to flag file)
|
|
61
|
-
* Whether the daemon TCP listener should be active.
|
|
62
|
-
* 'true'/'1' → on, 'false'/'0' → off, unset → check flag file.
|
|
63
|
-
*/
|
|
64
|
-
export function getDaemonTcpEnabled(): boolean | undefined {
|
|
65
|
-
return flagTriState("VELLUM_DAEMON_TCP_ENABLED");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* VELLUM_DAEMON_TCP_HOST — string, default: context-dependent (127.0.0.1 or 0.0.0.0)
|
|
70
|
-
* Hostname/address for the TCP listener. When unset, platform.ts resolves
|
|
71
|
-
* based on whether iOS pairing is enabled.
|
|
72
|
-
*/
|
|
73
|
-
export function getDaemonTcpHost(): string | undefined {
|
|
74
|
-
return str("VELLUM_DAEMON_TCP_HOST");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* VELLUM_DAEMON_IOS_PAIRING — boolean tri-state, default: undefined (falls back to flag file)
|
|
79
|
-
* Whether iOS pairing mode is enabled. When on, TCP binds to 0.0.0.0.
|
|
80
|
-
* 'true'/'1' → on, 'false'/'0' → off, unset → check flag file.
|
|
81
|
-
*/
|
|
82
|
-
export function getDaemonIosPairing(): boolean | undefined {
|
|
83
|
-
return flagTriState("VELLUM_DAEMON_IOS_PAIRING");
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* VELLUM_DEBUG — boolean, default: false
|
|
88
|
-
* Enables debug-level logging and verbose output.
|
|
89
|
-
*/
|
|
90
|
-
export function getDebugMode(): boolean {
|
|
91
|
-
return flag("VELLUM_DEBUG");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* VELLUM_LOG_STDERR — boolean, default: false
|
|
96
|
-
* Forces logger output to stderr instead of log files.
|
|
97
|
-
*/
|
|
98
|
-
export function getLogStderr(): boolean {
|
|
99
|
-
return flag("VELLUM_LOG_STDERR");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
39
|
/**
|
|
103
40
|
* DEBUG_STDOUT_LOGS — boolean, default: false
|
|
104
41
|
* Enables additional log output to stdout (alongside file logging).
|
|
@@ -107,14 +44,6 @@ export function getDebugStdoutLogs(): boolean {
|
|
|
107
44
|
return flag("DEBUG_STDOUT_LOGS");
|
|
108
45
|
}
|
|
109
46
|
|
|
110
|
-
/**
|
|
111
|
-
* VELLUM_ENABLE_MONITORING — boolean, default: false
|
|
112
|
-
* Enables monitoring/telemetry (Logfire, etc.).
|
|
113
|
-
*/
|
|
114
|
-
export function getEnableMonitoring(): boolean {
|
|
115
|
-
return flag("VELLUM_ENABLE_MONITORING");
|
|
116
|
-
}
|
|
117
|
-
|
|
118
47
|
/**
|
|
119
48
|
* IS_CONTAINERIZED — boolean, default: false
|
|
120
49
|
* When true, indicates the assistant is running inside a container (e.g. Docker).
|
|
@@ -132,24 +61,26 @@ export function getIsContainerized(): boolean {
|
|
|
132
61
|
* to warn about typos or unrecognized variables.
|
|
133
62
|
*/
|
|
134
63
|
const KNOWN_VELLUM_VARS = new Set([
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
-
"VELLUM_DAEMON_NOAUTH",
|
|
64
|
+
"VELLUM_ASSISTANT_NAME",
|
|
65
|
+
"VELLUM_AWS_ROLE_ARN",
|
|
66
|
+
"VELLUM_CLAUDE_CODE_DEPTH",
|
|
67
|
+
"VELLUM_CUSTOM_QR_CODE_PATH",
|
|
140
68
|
"VELLUM_DAEMON_AUTOSTART",
|
|
141
|
-
"
|
|
142
|
-
"
|
|
143
|
-
"
|
|
69
|
+
"VELLUM_DAEMON_NOAUTH",
|
|
70
|
+
"VELLUM_DATA_DIR",
|
|
71
|
+
"VELLUM_DESKTOP_APP",
|
|
72
|
+
"VELLUM_DEV",
|
|
73
|
+
"VELLUM_ENABLE_INSECURE_LAN_PAIRING",
|
|
74
|
+
"VELLUM_HATCHED_BY",
|
|
144
75
|
"VELLUM_HOOK_EVENT",
|
|
145
76
|
"VELLUM_HOOK_NAME",
|
|
146
77
|
"VELLUM_HOOK_SETTINGS",
|
|
78
|
+
"VELLUM_LOCKFILE_DIR",
|
|
79
|
+
"VELLUM_PLATFORM_URL",
|
|
147
80
|
"VELLUM_ROOT_DIR",
|
|
148
|
-
"
|
|
149
|
-
"VELLUM_CLAUDE_CODE_DEPTH",
|
|
150
|
-
"VELLUM_ASSISTANT_PLATFORM_URL",
|
|
81
|
+
"VELLUM_SSH_USER",
|
|
151
82
|
"VELLUM_UNSAFE_AUTH_BYPASS",
|
|
152
|
-
"
|
|
83
|
+
"VELLUM_WORKSPACE_DIR",
|
|
153
84
|
]);
|
|
154
85
|
|
|
155
86
|
/**
|
package/src/config/env.ts
CHANGED
|
@@ -8,17 +8,13 @@
|
|
|
8
8
|
* - Fail-fast validation via validateEnv() at startup
|
|
9
9
|
* - Shared derived values (e.g. gateway base URL) instead of duplicated logic
|
|
10
10
|
*
|
|
11
|
-
* Bootstrap-level env vars (BASE_DATA_DIR,
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* without circular imports.
|
|
11
|
+
* Bootstrap-level env vars (BASE_DATA_DIR, DEBUG_STDOUT_LOGS) are defined
|
|
12
|
+
* in config/env-registry.ts which has no internal dependencies and can be
|
|
13
|
+
* imported from platform/logger without circular imports.
|
|
15
14
|
*/
|
|
16
15
|
|
|
17
16
|
import { getLogger } from "../util/logger.js";
|
|
18
|
-
import {
|
|
19
|
-
checkUnrecognizedEnvVars,
|
|
20
|
-
getEnableMonitoring,
|
|
21
|
-
} from "./env-registry.js";
|
|
17
|
+
import { checkUnrecognizedEnvVars } from "./env-registry.js";
|
|
22
18
|
|
|
23
19
|
const log = getLogger("env");
|
|
24
20
|
|
|
@@ -55,33 +51,23 @@ export function getGatewayPort(): number {
|
|
|
55
51
|
return int("GATEWAY_PORT", DEFAULT_GATEWAY_PORT);
|
|
56
52
|
}
|
|
57
53
|
|
|
58
|
-
/**
|
|
59
|
-
* Resolve the gateway base URL for internal service-to-service calls.
|
|
60
|
-
* Prefers GATEWAY_INTERNAL_BASE_URL if set, then INTERNAL_GATEWAY_BASE_URL
|
|
61
|
-
* (used by skill subprocesses), otherwise derives from port.
|
|
62
|
-
*/
|
|
54
|
+
/** Resolve the gateway base URL for internal service-to-service calls. */
|
|
63
55
|
export function getGatewayInternalBaseUrl(): string {
|
|
64
|
-
const explicit = str("GATEWAY_INTERNAL_BASE_URL");
|
|
65
|
-
if (explicit) return explicit.replace(/\/+$/, "");
|
|
66
|
-
const skillInjected = str("INTERNAL_GATEWAY_BASE_URL");
|
|
67
|
-
if (skillInjected) return skillInjected.replace(/\/+$/, "");
|
|
68
56
|
return `http://127.0.0.1:${getGatewayPort()}`;
|
|
69
57
|
}
|
|
70
58
|
|
|
71
59
|
// ── Ingress ──────────────────────────────────────────────────────────────────
|
|
72
60
|
|
|
73
|
-
|
|
61
|
+
let _ingressPublicBaseUrl: string | undefined;
|
|
62
|
+
|
|
63
|
+
/** Read the ingress public base URL (module-level state, mutated at runtime by config handlers). */
|
|
74
64
|
export function getIngressPublicBaseUrl(): string | undefined {
|
|
75
|
-
return
|
|
65
|
+
return _ingressPublicBaseUrl;
|
|
76
66
|
}
|
|
77
67
|
|
|
78
|
-
/** Set or clear the
|
|
68
|
+
/** Set or clear the ingress public base URL (used by config handlers). */
|
|
79
69
|
export function setIngressPublicBaseUrl(value: string | undefined): void {
|
|
80
|
-
|
|
81
|
-
process.env.INGRESS_PUBLIC_BASE_URL = value;
|
|
82
|
-
} else {
|
|
83
|
-
delete process.env.INGRESS_PUBLIC_BASE_URL;
|
|
84
|
-
}
|
|
70
|
+
_ingressPublicBaseUrl = value;
|
|
85
71
|
}
|
|
86
72
|
|
|
87
73
|
// ── Runtime HTTP ─────────────────────────────────────────────────────────────
|
|
@@ -117,37 +103,12 @@ export function hasUngatedHttpAuthDisabled(): boolean {
|
|
|
117
103
|
return str("VELLUM_UNSAFE_AUTH_BYPASS")?.trim() !== "1";
|
|
118
104
|
}
|
|
119
105
|
|
|
120
|
-
// ── Twilio ───────────────────────────────────────────────────────────────────
|
|
121
|
-
|
|
122
|
-
export function getTwilioPhoneNumberEnv(): string | undefined {
|
|
123
|
-
return str("TWILIO_PHONE_NUMBER");
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function getTwilioUserPhoneNumber(): string | undefined {
|
|
127
|
-
return str("TWILIO_USER_PHONE_NUMBER");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function isTwilioWebhookValidationDisabled(): boolean {
|
|
131
|
-
// Intentionally strict: only exact "true" disables validation (not "1").
|
|
132
|
-
// This is a security-sensitive bypass — we don't want environments that
|
|
133
|
-
// template booleans as "1" to silently skip webhook signature checks.
|
|
134
|
-
return process.env.TWILIO_WEBHOOK_VALIDATION_DISABLED === "true";
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function getCallWelcomeGreeting(): string | undefined {
|
|
138
|
-
return str("CALL_WELCOME_GREETING");
|
|
139
|
-
}
|
|
140
|
-
|
|
141
106
|
// ── Monitoring ───────────────────────────────────────────────────────────────
|
|
142
107
|
|
|
143
108
|
export function getLogfireToken(): string | undefined {
|
|
144
109
|
return str("LOGFIRE_TOKEN");
|
|
145
110
|
}
|
|
146
111
|
|
|
147
|
-
export function isMonitoringEnabled(): boolean {
|
|
148
|
-
return getEnableMonitoring();
|
|
149
|
-
}
|
|
150
|
-
|
|
151
112
|
const DEFAULT_SENTRY_DSN =
|
|
152
113
|
"https://db2d38a082e4ee35eeaea08c44b376ec@o4504590528675840.ingest.us.sentry.io/4510874712276992";
|
|
153
114
|
|
|
@@ -33,22 +33,6 @@
|
|
|
33
33
|
"description": "Enable messaging skill section in the system prompt",
|
|
34
34
|
"defaultEnabled": true
|
|
35
35
|
},
|
|
36
|
-
{
|
|
37
|
-
"id": "messaging-gmail",
|
|
38
|
-
"scope": "assistant",
|
|
39
|
-
"key": "feature_flags.messaging.gmail.enabled",
|
|
40
|
-
"label": "Messaging: Gmail",
|
|
41
|
-
"description": "Allow messaging tools to operate on the Gmail platform",
|
|
42
|
-
"defaultEnabled": true
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"id": "messaging-telegram",
|
|
46
|
-
"scope": "assistant",
|
|
47
|
-
"key": "feature_flags.messaging.telegram.enabled",
|
|
48
|
-
"label": "Messaging: Telegram",
|
|
49
|
-
"description": "Allow messaging tools to operate on the Telegram platform",
|
|
50
|
-
"defaultEnabled": false
|
|
51
|
-
},
|
|
52
36
|
{
|
|
53
37
|
"id": "collect-usage-data",
|
|
54
38
|
"scope": "assistant",
|
|
@@ -81,6 +65,14 @@
|
|
|
81
65
|
"description": "Show the Contacts tab in Settings for viewing and managing contacts",
|
|
82
66
|
"defaultEnabled": false
|
|
83
67
|
},
|
|
68
|
+
{
|
|
69
|
+
"id": "email-channel",
|
|
70
|
+
"scope": "assistant",
|
|
71
|
+
"key": "feature_flags.email-channel.enabled",
|
|
72
|
+
"label": "Email Channel",
|
|
73
|
+
"description": "Show the Email channel card on the Contacts page and enable the email-setup skill",
|
|
74
|
+
"defaultEnabled": false
|
|
75
|
+
},
|
|
84
76
|
{
|
|
85
77
|
"id": "outbound-proxy-sidecar",
|
|
86
78
|
"scope": "assistant",
|
|
@@ -128,6 +120,14 @@
|
|
|
128
120
|
"label": "Settings Developer Nav",
|
|
129
121
|
"description": "Control Developer nav visibility in macOS settings",
|
|
130
122
|
"defaultEnabled": true
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"id": "logfire",
|
|
126
|
+
"scope": "assistant",
|
|
127
|
+
"key": "feature_flags.logfire.enabled",
|
|
128
|
+
"label": "Logfire LLM Observability",
|
|
129
|
+
"description": "Enable Logfire tracing for LLM request/response telemetry when LOGFIRE_TOKEN is set",
|
|
130
|
+
"defaultEnabled": false
|
|
131
131
|
}
|
|
132
132
|
]
|
|
133
133
|
}
|
package/src/config/loader.ts
CHANGED
|
@@ -333,12 +333,6 @@ export function loadConfig(): AssistantConfig {
|
|
|
333
333
|
}
|
|
334
334
|
|
|
335
335
|
// Environment variables override everything
|
|
336
|
-
if (process.env.VELLUM_CONFIG_SANDBOX_ENABLED === "false") {
|
|
337
|
-
config.sandbox.enabled = false;
|
|
338
|
-
} else if (process.env.VELLUM_CONFIG_SANDBOX_ENABLED === "true") {
|
|
339
|
-
config.sandbox.enabled = true;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
336
|
if (process.env.ANTHROPIC_API_KEY) {
|
|
343
337
|
config.apiKeys.anthropic = process.env.ANTHROPIC_API_KEY;
|
|
344
338
|
}
|
package/src/config/schema.ts
CHANGED
|
@@ -289,7 +289,9 @@ export const AssistantConfigSchema = z
|
|
|
289
289
|
PermissionsConfigSchema.parse({}),
|
|
290
290
|
),
|
|
291
291
|
auditLog: AuditLogConfigSchema.default(AuditLogConfigSchema.parse({})),
|
|
292
|
-
logFile: LogFileConfigSchema.default(
|
|
292
|
+
logFile: LogFileConfigSchema.default(
|
|
293
|
+
LogFileConfigSchema.parse({ dir: getDataDir() + "/logs" }),
|
|
294
|
+
),
|
|
293
295
|
pricingOverrides: z.array(ModelPricingOverrideSchema).default([]),
|
|
294
296
|
heartbeat: HeartbeatConfigSchema.default(HeartbeatConfigSchema.parse({})),
|
|
295
297
|
swarm: SwarmConfigSchema.default(SwarmConfigSchema.parse({})),
|
package/src/config/skills.ts
CHANGED
|
@@ -146,14 +146,23 @@ export interface SkillDefinition extends SkillSummary {
|
|
|
146
146
|
body: string;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
export type SkillLookupErrorCode =
|
|
150
|
+
| "not_found"
|
|
151
|
+
| "ambiguous"
|
|
152
|
+
| "empty_catalog"
|
|
153
|
+
| "invalid_selector"
|
|
154
|
+
| "load_failed";
|
|
155
|
+
|
|
149
156
|
export interface SkillLookupResult {
|
|
150
157
|
skill?: SkillDefinition;
|
|
151
158
|
error?: string;
|
|
159
|
+
errorCode?: SkillLookupErrorCode;
|
|
152
160
|
}
|
|
153
161
|
|
|
154
162
|
export interface SkillSelectorResult {
|
|
155
163
|
skill?: SkillSummary;
|
|
156
164
|
error?: string;
|
|
165
|
+
errorCode?: SkillLookupErrorCode;
|
|
157
166
|
}
|
|
158
167
|
|
|
159
168
|
// ─── Skill Tool Manifest Types ────────────────────────────────────────────────
|
|
@@ -1210,6 +1219,7 @@ export function resolveSkillSelector(
|
|
|
1210
1219
|
if (!needle) {
|
|
1211
1220
|
return {
|
|
1212
1221
|
error: "Skill selector is required and must be a non-empty string.",
|
|
1222
|
+
errorCode: "invalid_selector",
|
|
1213
1223
|
};
|
|
1214
1224
|
}
|
|
1215
1225
|
|
|
@@ -1218,6 +1228,7 @@ export function resolveSkillSelector(
|
|
|
1218
1228
|
return {
|
|
1219
1229
|
error:
|
|
1220
1230
|
"No skills are available. Configure ~/.vellum/workspace/skills/SKILLS.md or add skill directories.",
|
|
1231
|
+
errorCode: "empty_catalog",
|
|
1221
1232
|
};
|
|
1222
1233
|
}
|
|
1223
1234
|
|
|
@@ -1236,7 +1247,10 @@ export function resolveSkillSelector(
|
|
|
1236
1247
|
}
|
|
1237
1248
|
if (exactNameMatches.length > 1) {
|
|
1238
1249
|
const ids = exactNameMatches.map((skill) => skill.id).join(", ");
|
|
1239
|
-
return {
|
|
1250
|
+
return {
|
|
1251
|
+
error: `Ambiguous skill name "${needle}". Matching IDs: ${ids}`,
|
|
1252
|
+
errorCode: "ambiguous",
|
|
1253
|
+
};
|
|
1240
1254
|
}
|
|
1241
1255
|
|
|
1242
1256
|
const idPrefixMatches = catalog.filter((skill) =>
|
|
@@ -1249,12 +1263,14 @@ export function resolveSkillSelector(
|
|
|
1249
1263
|
const ids = idPrefixMatches.map((skill) => skill.id).join(", ");
|
|
1250
1264
|
return {
|
|
1251
1265
|
error: `Ambiguous skill id prefix "${needle}". Matching IDs: ${ids}`,
|
|
1266
|
+
errorCode: "ambiguous",
|
|
1252
1267
|
};
|
|
1253
1268
|
}
|
|
1254
1269
|
|
|
1255
1270
|
const knownSkills = catalog.map((skill) => skill.id).join(", ");
|
|
1256
1271
|
return {
|
|
1257
1272
|
error: `No skill matched "${needle}". Available skills: ${knownSkills}`,
|
|
1273
|
+
errorCode: "not_found",
|
|
1258
1274
|
};
|
|
1259
1275
|
}
|
|
1260
1276
|
|
|
@@ -1264,7 +1280,10 @@ export function loadSkillBySelector(
|
|
|
1264
1280
|
): SkillLookupResult {
|
|
1265
1281
|
const resolved = resolveSkillSelector(selector, workspaceSkillsDir);
|
|
1266
1282
|
if (!resolved.skill) {
|
|
1267
|
-
return {
|
|
1283
|
+
return {
|
|
1284
|
+
error: resolved.error ?? "Failed to resolve skill selector.",
|
|
1285
|
+
errorCode: resolved.errorCode ?? "load_failed",
|
|
1286
|
+
};
|
|
1268
1287
|
}
|
|
1269
1288
|
return loadSkillDefinition(resolved.skill);
|
|
1270
1289
|
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses image dimensions from base64-encoded image data by reading binary headers.
|
|
3
|
+
* Supports PNG, JPEG, GIF, and WebP formats.
|
|
4
|
+
* Returns null if parsing fails for any reason (corrupt, truncated, unrecognized).
|
|
5
|
+
*/
|
|
6
|
+
export function parseImageDimensions(
|
|
7
|
+
base64Data: string,
|
|
8
|
+
mediaType: string,
|
|
9
|
+
): { width: number; height: number } | null {
|
|
10
|
+
try {
|
|
11
|
+
switch (mediaType) {
|
|
12
|
+
case "image/png":
|
|
13
|
+
return parsePng(base64Data);
|
|
14
|
+
case "image/jpeg":
|
|
15
|
+
return parseJpeg(base64Data);
|
|
16
|
+
case "image/gif":
|
|
17
|
+
return parseGif(base64Data);
|
|
18
|
+
case "image/webp":
|
|
19
|
+
return parseWebp(base64Data);
|
|
20
|
+
default:
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function decodeBase64Bytes(
|
|
29
|
+
base64Data: string,
|
|
30
|
+
maxBytes: number,
|
|
31
|
+
): Buffer | null {
|
|
32
|
+
// Estimate how much base64 we need: every 4 base64 chars = 3 bytes
|
|
33
|
+
const charsNeeded = Math.ceil((maxBytes * 4) / 3);
|
|
34
|
+
const slice = base64Data.slice(0, charsNeeded + 4); // a little extra for padding
|
|
35
|
+
try {
|
|
36
|
+
const buf = Buffer.from(slice, "base64");
|
|
37
|
+
return buf.length > 0 ? buf : null;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readUint32BE(buf: Buffer, offset: number): number {
|
|
44
|
+
if (offset + 4 > buf.length) return -1;
|
|
45
|
+
return buf.readUInt32BE(offset);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readUint16BE(buf: Buffer, offset: number): number {
|
|
49
|
+
if (offset + 2 > buf.length) return -1;
|
|
50
|
+
return buf.readUInt16BE(offset);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readUint16LE(buf: Buffer, offset: number): number {
|
|
54
|
+
if (offset + 2 > buf.length) return -1;
|
|
55
|
+
return buf.readUInt16LE(offset);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readUint32LE(buf: Buffer, offset: number): number {
|
|
59
|
+
if (offset + 4 > buf.length) return -1;
|
|
60
|
+
return buf.readUInt32LE(offset);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readUint24LE(buf: Buffer, offset: number): number {
|
|
64
|
+
if (offset + 3 > buf.length) return -1;
|
|
65
|
+
return buf[offset]! | (buf[offset + 1]! << 8) | (buf[offset + 2]! << 16);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parsePng(
|
|
69
|
+
base64Data: string,
|
|
70
|
+
): { width: number; height: number } | null {
|
|
71
|
+
const buf = decodeBase64Bytes(base64Data, 32);
|
|
72
|
+
if (!buf || buf.length < 24) return null;
|
|
73
|
+
|
|
74
|
+
// Validate PNG signature: 89 50 4E 47
|
|
75
|
+
if (
|
|
76
|
+
buf[0] !== 0x89 ||
|
|
77
|
+
buf[1] !== 0x50 ||
|
|
78
|
+
buf[2] !== 0x4e ||
|
|
79
|
+
buf[3] !== 0x47
|
|
80
|
+
) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const width = readUint32BE(buf, 16);
|
|
85
|
+
const height = readUint32BE(buf, 20);
|
|
86
|
+
if (width <= 0 || height <= 0) return null;
|
|
87
|
+
|
|
88
|
+
return { width, height };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseJpeg(
|
|
92
|
+
base64Data: string,
|
|
93
|
+
): { width: number; height: number } | null {
|
|
94
|
+
// Scan up to 1 MiB to handle JPEGs with large EXIF/ICC metadata before the SOF marker
|
|
95
|
+
const buf = decodeBase64Bytes(base64Data, 1_048_576);
|
|
96
|
+
if (!buf || buf.length < 2) return null;
|
|
97
|
+
|
|
98
|
+
// Validate JPEG SOI marker
|
|
99
|
+
if (buf[0] !== 0xff || buf[1] !== 0xd8) return null;
|
|
100
|
+
|
|
101
|
+
let offset = 2;
|
|
102
|
+
while (offset < buf.length - 1) {
|
|
103
|
+
// Find next marker
|
|
104
|
+
if (buf[offset] !== 0xff) {
|
|
105
|
+
offset++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Skip padding 0xFF bytes
|
|
110
|
+
while (offset < buf.length && buf[offset] === 0xff) {
|
|
111
|
+
offset++;
|
|
112
|
+
}
|
|
113
|
+
if (offset >= buf.length) return null;
|
|
114
|
+
|
|
115
|
+
const marker = buf[offset]!;
|
|
116
|
+
offset++;
|
|
117
|
+
|
|
118
|
+
// Check for SOF markers: C0-CF excluding C4 (DHT) and CC (DAC)
|
|
119
|
+
if (
|
|
120
|
+
marker >= 0xc0 &&
|
|
121
|
+
marker <= 0xcf &&
|
|
122
|
+
marker !== 0xc4 &&
|
|
123
|
+
marker !== 0xcc
|
|
124
|
+
) {
|
|
125
|
+
// SOF marker found: skip 2-byte length + 1-byte precision
|
|
126
|
+
if (offset + 7 > buf.length) return null;
|
|
127
|
+
const height = readUint16BE(buf, offset + 3);
|
|
128
|
+
const width = readUint16BE(buf, offset + 5);
|
|
129
|
+
if (width <= 0 || height <= 0) return null;
|
|
130
|
+
return { width, height };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Skip this marker's payload
|
|
134
|
+
if (offset + 1 >= buf.length) return null;
|
|
135
|
+
const segmentLength = readUint16BE(buf, offset);
|
|
136
|
+
if (segmentLength < 2) return null;
|
|
137
|
+
offset += segmentLength;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parseGif(
|
|
144
|
+
base64Data: string,
|
|
145
|
+
): { width: number; height: number } | null {
|
|
146
|
+
const buf = decodeBase64Bytes(base64Data, 12);
|
|
147
|
+
if (!buf || buf.length < 10) return null;
|
|
148
|
+
|
|
149
|
+
// Validate GIF signature: 47 49 46 38 (GIF8)
|
|
150
|
+
if (
|
|
151
|
+
buf[0] !== 0x47 ||
|
|
152
|
+
buf[1] !== 0x49 ||
|
|
153
|
+
buf[2] !== 0x46 ||
|
|
154
|
+
buf[3] !== 0x38
|
|
155
|
+
) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const width = readUint16LE(buf, 6);
|
|
160
|
+
const height = readUint16LE(buf, 8);
|
|
161
|
+
if (width <= 0 || height <= 0) return null;
|
|
162
|
+
|
|
163
|
+
return { width, height };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseWebp(
|
|
167
|
+
base64Data: string,
|
|
168
|
+
): { width: number; height: number } | null {
|
|
169
|
+
const buf = decodeBase64Bytes(base64Data, 32);
|
|
170
|
+
if (!buf || buf.length < 16) return null;
|
|
171
|
+
|
|
172
|
+
// Validate RIFF signature
|
|
173
|
+
if (
|
|
174
|
+
buf[0] !== 0x52 ||
|
|
175
|
+
buf[1] !== 0x49 ||
|
|
176
|
+
buf[2] !== 0x46 ||
|
|
177
|
+
buf[3] !== 0x46
|
|
178
|
+
) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
// Validate WEBP signature at offset 8
|
|
182
|
+
if (
|
|
183
|
+
buf[8] !== 0x57 ||
|
|
184
|
+
buf[9] !== 0x45 ||
|
|
185
|
+
buf[10] !== 0x42 ||
|
|
186
|
+
buf[11] !== 0x50
|
|
187
|
+
) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Identify sub-format at offset 12
|
|
192
|
+
const subFormat =
|
|
193
|
+
String.fromCharCode(buf[12]!) +
|
|
194
|
+
String.fromCharCode(buf[13]!) +
|
|
195
|
+
String.fromCharCode(buf[14]!) +
|
|
196
|
+
String.fromCharCode(buf[15]!);
|
|
197
|
+
|
|
198
|
+
if (subFormat === "VP8 ") {
|
|
199
|
+
// VP8 lossy
|
|
200
|
+
if (buf.length < 30) return null;
|
|
201
|
+
const width = readUint16LE(buf, 26) & 0x3fff;
|
|
202
|
+
const height = readUint16LE(buf, 28) & 0x3fff;
|
|
203
|
+
if (width <= 0 || height <= 0) return null;
|
|
204
|
+
return { width, height };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (subFormat === "VP8L") {
|
|
208
|
+
// VP8L lossless — validate signature byte 0x2f at offset 20
|
|
209
|
+
if (buf.length < 25) return null;
|
|
210
|
+
if (buf[20] !== 0x2f) return null;
|
|
211
|
+
const bits = readUint32LE(buf, 21);
|
|
212
|
+
if (bits < 0) return null;
|
|
213
|
+
const width = (bits & 0x3fff) + 1;
|
|
214
|
+
const height = ((bits >> 14) & 0x3fff) + 1;
|
|
215
|
+
if (width <= 0 || height <= 0) return null;
|
|
216
|
+
return { width, height };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (subFormat === "VP8X") {
|
|
220
|
+
// VP8X extended
|
|
221
|
+
if (buf.length < 30) return null;
|
|
222
|
+
const width = readUint24LE(buf, 24) + 1;
|
|
223
|
+
const height = readUint24LE(buf, 27) + 1;
|
|
224
|
+
if (width <= 0 || height <= 0) return null;
|
|
225
|
+
return { width, height };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return null;
|
|
229
|
+
}
|