@vellumai/assistant 0.6.3 → 0.6.4
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 +273 -10
- package/Dockerfile +2 -3
- package/bun.lock +5 -13
- package/docs/backup-troubleshooting.md +52 -0
- package/docs/browser-use-architecture-phase2.md +174 -0
- package/docs/stt-provider-onboarding.md +120 -0
- package/knip.json +12 -2
- package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
- package/node_modules/@vellumai/ces-contracts/package.json +3 -3
- package/openapi.yaml +982 -72
- package/package.json +4 -6
- package/scripts/generate-openapi.ts +0 -1
- package/scripts/test.sh +73 -18
- package/src/__tests__/agent-image-optimize.test.ts +28 -0
- package/src/__tests__/agent-loop.test.ts +123 -0
- package/src/__tests__/anthropic-provider.test.ts +263 -10
- package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
- package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
- package/src/__tests__/browser-fill-credential.test.ts +11 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
- package/src/__tests__/browser-skill-endstate.test.ts +31 -7
- package/src/__tests__/btw-routes.test.ts +7 -0
- package/src/__tests__/call-controller.test.ts +581 -20
- package/src/__tests__/catalog-files.test.ts +138 -0
- package/src/__tests__/channel-invite-transport.test.ts +2 -2
- package/src/__tests__/channel-readiness-routes.test.ts +16 -20
- package/src/__tests__/channel-readiness-service.test.ts +12 -7
- package/src/__tests__/checker.test.ts +157 -10
- package/src/__tests__/clawhub-files.test.ts +347 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
- package/src/__tests__/config-analysis.test.ts +100 -0
- package/src/__tests__/config-schema.test.ts +1013 -66
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
- package/src/__tests__/config-watcher.test.ts +43 -8
- package/src/__tests__/contact-store-user-file.test.ts +512 -0
- package/src/__tests__/contacts-write.test.ts +197 -0
- package/src/__tests__/context-window-manager.test.ts +88 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +98 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +135 -0
- package/src/__tests__/conversation-error.test.ts +70 -0
- package/src/__tests__/conversation-history-web-search.test.ts +11 -4
- package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
- package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
- package/src/__tests__/conversation-list-source.test.ts +145 -0
- package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
- package/src/__tests__/conversation-queue.test.ts +901 -60
- package/src/__tests__/conversation-routes-disk-view.test.ts +270 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +55 -0
- package/src/__tests__/conversation-skill-tools.test.ts +7 -4
- package/src/__tests__/conversation-slash-commands.test.ts +33 -0
- package/src/__tests__/conversation-slash-queue.test.ts +89 -18
- package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
- package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
- package/src/__tests__/credential-health-service.test.ts +352 -0
- package/src/__tests__/credential-security-invariants.test.ts +5 -3
- package/src/__tests__/credential-vault-unit.test.ts +379 -3
- package/src/__tests__/credentials-cli.test.ts +40 -16
- package/src/__tests__/cross-provider-web-search.test.ts +146 -35
- package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
- package/src/__tests__/device-id.test.ts +112 -0
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
- package/src/__tests__/email-html-renderer.test.ts +71 -0
- package/src/__tests__/email-invite-adapter.test.ts +36 -32
- package/src/__tests__/emit-event-signal.test.ts +71 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +75 -8
- package/src/__tests__/fixtures/mock-chrome-extension.ts +11 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
- package/src/__tests__/gateway-only-guard.test.ts +0 -1
- package/src/__tests__/gemini-provider.test.ts +64 -0
- package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
- package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
- package/src/__tests__/gmail-archive-gate.test.ts +246 -0
- package/src/__tests__/gmail-preferences.test.ts +117 -0
- package/src/__tests__/headless-browser-interactions.test.ts +43 -0
- package/src/__tests__/headless-browser-mode.test.ts +614 -0
- package/src/__tests__/headless-browser-navigate.test.ts +142 -5
- package/src/__tests__/headless-browser-read-tools.test.ts +11 -0
- package/src/__tests__/headless-browser-snapshot.test.ts +10 -0
- package/src/__tests__/heartbeat-service.test.ts +70 -17
- package/src/__tests__/home-state-routes.test.ts +162 -0
- package/src/__tests__/host-bash-proxy.test.ts +0 -5
- package/src/__tests__/host-browser-e2e-cloud.test.ts +138 -4
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +4 -4
- package/src/__tests__/host-browser-ws-events-e2e.test.ts +103 -0
- package/src/__tests__/host-cu-proxy.test.ts +0 -5
- package/src/__tests__/identity-intro-cache.test.ts +40 -10
- package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
- package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
- package/src/__tests__/llm-context-normalization.test.ts +488 -0
- package/src/__tests__/llm-context-route-provider.test.ts +86 -5
- package/src/__tests__/llm-usage-store.test.ts +363 -0
- package/src/__tests__/media-stream-output.test.ts +555 -0
- package/src/__tests__/media-stream-parser.test.ts +374 -0
- package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
- package/src/__tests__/media-stream-stt-session.test.ts +588 -0
- package/src/__tests__/media-turn-detector.test.ts +440 -0
- package/src/__tests__/message-queue.test.ts +125 -0
- package/src/__tests__/migration-export-http.test.ts +6 -6
- package/src/__tests__/migration-import-commit-http.test.ts +8 -6
- package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
- package/src/__tests__/migration-validate-http.test.ts +3 -3
- package/src/__tests__/mock-gateway-ipc.ts +151 -0
- package/src/__tests__/model-intents.test.ts +2 -2
- package/src/__tests__/oauth-apps-routes.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +2 -0
- package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
- package/src/__tests__/oauth-providers-routes.test.ts +2 -0
- package/src/__tests__/oauth-store.test.ts +85 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +249 -6
- package/src/__tests__/onboarding-template-contract.test.ts +6 -13
- package/src/__tests__/openai-provider.test.ts +176 -0
- package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
- package/src/__tests__/openai-responses-provider.test.ts +1105 -0
- package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
- package/src/__tests__/outlook-unsubscribe.test.ts +31 -2
- package/src/__tests__/persona-resolver.test.ts +251 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
- package/src/__tests__/platform.test.ts +92 -1
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
- package/src/__tests__/pricing.test.ts +174 -0
- package/src/__tests__/qdrant-manager.test.ts +29 -8
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
- package/src/__tests__/relationship-state-contract.test.ts +175 -0
- package/src/__tests__/relay-server.test.ts +423 -5
- package/src/__tests__/search-skills-unified.test.ts +118 -0
- package/src/__tests__/secret-scanner-executor.test.ts +4 -0
- package/src/__tests__/secure-keys.test.ts +107 -0
- package/src/__tests__/send-endpoint-busy.test.ts +5 -1
- package/src/__tests__/sequence-store.test.ts +1 -1
- package/src/__tests__/server-history-render.test.ts +49 -0
- package/src/__tests__/settings-routes.test.ts +201 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
- package/src/__tests__/skills-file-content-endpoint.test.ts +276 -145
- package/src/__tests__/skills-files-catalog-fallback.test.ts +381 -93
- package/src/__tests__/skills.test.ts +5 -2
- package/src/__tests__/skillssh-files.test.ts +446 -0
- package/src/__tests__/slack-block-formatting.test.ts +110 -0
- package/src/__tests__/slack-channel-config.test.ts +564 -1
- package/src/__tests__/stt-catalog-parity.test.ts +282 -0
- package/src/__tests__/stt-stream-session.test.ts +535 -0
- package/src/__tests__/system-prompt.test.ts +112 -26
- package/src/__tests__/telephony-stt-routing.test.ts +329 -0
- package/src/__tests__/terminal-tools.test.ts +18 -7
- package/src/__tests__/test-preload.ts +18 -0
- package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
- package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +33 -24
- package/src/__tests__/tool-result-truncation.test.ts +36 -0
- package/src/__tests__/trust-store.test.ts +7 -1
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
- package/src/__tests__/tts-catalog-parity.test.ts +345 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
- package/src/__tests__/twilio-routes.test.ts +376 -0
- package/src/__tests__/unicode.test.ts +293 -0
- package/src/__tests__/update-bulletin-format.test.ts +59 -0
- package/src/__tests__/update-bulletin.test.ts +206 -5
- package/src/__tests__/usage-routes.test.ts +25 -4
- package/src/__tests__/user-reference.test.ts +46 -61
- package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
- package/src/__tests__/voice-config-update.test.ts +403 -0
- package/src/__tests__/voice-quality.test.ts +434 -19
- package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
- package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
- package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
- package/src/__tests__/workspace-migration-meets.test.ts +244 -0
- package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
- package/src/__tests__/workspace-policy.test.ts +2 -0
- package/src/agent/image-optimize.ts +24 -12
- package/src/agent/loop.ts +43 -3
- package/src/backup/__tests__/backup-key.test.ts +152 -0
- package/src/backup/__tests__/backup-worker.test.ts +767 -0
- package/src/backup/__tests__/list-snapshots.test.ts +87 -0
- package/src/backup/__tests__/local-writer.test.ts +218 -0
- package/src/backup/__tests__/offsite-writer.test.ts +641 -0
- package/src/backup/__tests__/paths.test.ts +300 -0
- package/src/backup/__tests__/restore.test.ts +498 -0
- package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
- package/src/backup/__tests__/stream-crypt.test.ts +228 -0
- package/src/backup/backup-key.ts +137 -0
- package/src/backup/backup-worker.ts +459 -0
- package/src/backup/list-snapshots.ts +147 -0
- package/src/backup/local-writer.ts +133 -0
- package/src/backup/offsite-writer.ts +222 -0
- package/src/backup/paths.ts +226 -0
- package/src/backup/restore.ts +322 -0
- package/src/backup/snapshot-lock.ts +431 -0
- package/src/backup/stream-crypt.ts +263 -0
- package/src/bundler/package-resolver.ts +4 -0
- package/src/calls/audio-store.ts +11 -5
- package/src/calls/call-controller.ts +226 -71
- package/src/calls/call-domain.ts +9 -0
- package/src/calls/call-speech-output.ts +190 -0
- package/src/calls/call-transport.ts +77 -0
- package/src/calls/media-stream-audio-transcode.ts +173 -0
- package/src/calls/media-stream-output.ts +660 -0
- package/src/calls/media-stream-parser.ts +300 -0
- package/src/calls/media-stream-protocol.ts +166 -0
- package/src/calls/media-stream-server.ts +592 -0
- package/src/calls/media-stream-stt-session.ts +460 -0
- package/src/calls/media-turn-detector.ts +230 -0
- package/src/calls/relay-server.ts +90 -75
- package/src/calls/resolve-call-tts-provider.ts +136 -0
- package/src/calls/telephony-stt-routing.ts +145 -0
- package/src/calls/tts-call-strategy.ts +161 -0
- package/src/calls/tts-text-sanitizer.ts +32 -16
- package/src/calls/twilio-routes.ts +281 -17
- package/src/calls/voice-quality.ts +78 -35
- package/src/calls/voice-session-bridge.ts +8 -1
- package/src/channels/types.ts +16 -0
- package/src/cli/__tests__/run-assistant-command.ts +11 -1
- package/src/cli/commands/__tests__/backup.test.ts +1165 -0
- package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
- package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
- package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
- package/src/cli/commands/__tests__/email-download.test.ts +16 -1
- package/src/cli/commands/__tests__/email-list.test.ts +22 -4
- package/src/cli/commands/__tests__/email-register.test.ts +4 -4
- package/src/cli/commands/__tests__/email-send.test.ts +37 -4
- package/src/cli/commands/__tests__/email-status.test.ts +5 -1
- package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
- package/src/cli/commands/backup.ts +993 -0
- package/src/cli/commands/conversations.ts +77 -0
- package/src/cli/commands/credentials.ts +0 -1
- package/src/cli/commands/domain.ts +210 -0
- package/src/cli/commands/email.ts +255 -3
- package/src/cli/commands/oauth/__tests__/connect.test.ts +12 -0
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +1 -0
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -0
- package/src/cli/commands/oauth/mode.ts +12 -3
- package/src/cli/commands/oauth/providers.ts +15 -0
- package/src/cli/commands/oauth/shared.ts +2 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +4 -9
- package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
- package/src/cli/program.ts +30 -4
- package/src/config/__tests__/backup-schema.test.ts +134 -0
- package/src/config/assistant-feature-flags.ts +61 -62
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +37 -1
- package/src/config/bundled-skills/browser/SKILL.md +30 -5
- package/src/config/bundled-skills/browser/TOOLS.json +123 -0
- package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
- package/src/config/bundled-skills/contacts/SKILL.md +2 -2
- package/src/config/bundled-skills/gmail/SKILL.md +53 -7
- package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
- package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
- package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
- package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
- package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
- package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
- package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
- package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
- package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
- package/src/config/bundled-skills/messaging/SKILL.md +3 -3
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
- package/src/config/bundled-skills/outlook/SKILL.md +2 -2
- package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
- package/src/config/bundled-skills/slack/SKILL.md +1 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
- package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
- package/src/config/bundled-tool-registry.ts +8 -0
- package/src/config/env-registry.ts +24 -0
- package/src/config/env.ts +34 -10
- package/src/config/feature-flag-registry.json +46 -14
- package/src/config/loader.ts +26 -12
- package/src/config/schema.ts +35 -10
- package/src/config/schemas/__tests__/stt.test.ts +43 -0
- package/src/config/schemas/analysis.ts +51 -0
- package/src/config/schemas/backup.ts +72 -0
- package/src/config/schemas/calls.ts +1 -26
- package/src/config/schemas/elevenlabs.ts +0 -59
- package/src/config/schemas/filing.ts +47 -7
- package/src/config/schemas/heartbeat.ts +27 -5
- package/src/config/schemas/host-browser.ts +47 -1
- package/src/config/schemas/inference.ts +1 -1
- package/src/config/schemas/memory-lifecycle.ts +14 -2
- package/src/config/schemas/services.ts +44 -0
- package/src/config/schemas/stt.ts +59 -0
- package/src/config/schemas/tts.ts +230 -0
- package/src/config/schemas/updates.ts +14 -0
- package/src/config/skills.ts +4 -0
- package/src/config/types.ts +4 -0
- package/src/contacts/contact-store.ts +56 -11
- package/src/contacts/contacts-write.ts +38 -1
- package/src/context/post-turn-tool-result-truncation.ts +3 -2
- package/src/context/tool-result-truncation.ts +2 -1
- package/src/context/window-manager.ts +45 -12
- package/src/credential-execution/executable-discovery.ts +12 -2
- package/src/credential-execution/process-manager.ts +33 -2
- package/src/credential-health/credential-health-service.ts +366 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +17 -8
- package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
- package/src/daemon/config-watcher.ts +99 -5
- package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
- package/src/daemon/conversation-agent-loop.ts +101 -24
- package/src/daemon/conversation-error.ts +11 -0
- package/src/daemon/conversation-history.ts +40 -6
- package/src/daemon/conversation-launch.ts +220 -0
- package/src/daemon/conversation-lifecycle.ts +59 -9
- package/src/daemon/conversation-messaging.ts +37 -3
- package/src/daemon/conversation-notifiers.ts +5 -0
- package/src/daemon/conversation-process.ts +581 -19
- package/src/daemon/conversation-queue-manager.ts +24 -0
- package/src/daemon/conversation-runtime-assembly.ts +11 -1
- package/src/daemon/conversation-slash.ts +36 -0
- package/src/daemon/conversation-surfaces.ts +94 -4
- package/src/daemon/conversation-tool-setup.ts +25 -0
- package/src/daemon/conversation-usage.ts +7 -4
- package/src/daemon/conversation.ts +86 -28
- package/src/daemon/handlers/config-slack-channel.ts +269 -94
- package/src/daemon/handlers/conversations.ts +4 -1
- package/src/daemon/handlers/shared.ts +22 -0
- package/src/daemon/handlers/skills.ts +321 -77
- package/src/daemon/host-browser-proxy.ts +2 -1
- package/src/daemon/lifecycle.ts +122 -25
- package/src/daemon/message-protocol.ts +6 -0
- package/src/daemon/message-types/conversations.ts +34 -1
- package/src/daemon/message-types/home.ts +40 -0
- package/src/daemon/message-types/meet.ts +143 -0
- package/src/daemon/message-types/messages.ts +14 -0
- package/src/daemon/message-types/schedules.ts +34 -2
- package/src/daemon/message-types/skills.ts +16 -0
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/server.ts +347 -2
- package/src/daemon/shutdown-handlers.ts +32 -4
- package/src/daemon/shutdown-registry.ts +40 -0
- package/src/daemon/tool-side-effects.ts +9 -0
- package/src/email/html-renderer.ts +76 -0
- package/src/heartbeat/heartbeat-service.ts +93 -7
- package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
- package/src/home/__tests__/emit-feed-event.test.ts +169 -0
- package/src/home/__tests__/feed-scheduler.test.ts +194 -0
- package/src/home/__tests__/feed-types.test.ts +275 -0
- package/src/home/__tests__/feed-writer.test.ts +688 -0
- package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
- package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
- package/src/home/__tests__/progress-formula.test.ts +213 -0
- package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
- package/src/home/__tests__/rollup-producer.test.ts +398 -0
- package/src/home/assistant-feed-authoring.ts +124 -0
- package/src/home/emit-feed-event.ts +158 -0
- package/src/home/feed-scheduler.ts +247 -0
- package/src/home/feed-types.ts +181 -0
- package/src/home/feed-writer.ts +469 -0
- package/src/home/platform-gmail-digest.ts +163 -0
- package/src/home/progress-formula.ts +86 -0
- package/src/home/relationship-state-writer.ts +824 -0
- package/src/home/relationship-state.ts +143 -0
- package/src/home/rollup-producer.ts +384 -0
- package/src/hooks/runner.ts +7 -0
- package/src/inbound/platform-callback-registration.ts +12 -3
- package/src/inbound/public-ingress-urls.ts +12 -0
- package/src/instrument.ts +1 -1
- package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
- package/src/ipc/cli-client.ts +151 -0
- package/src/ipc/cli-server.ts +234 -0
- package/src/ipc/gateway-client.ts +180 -0
- package/src/ipc/routes/index.ts +5 -0
- package/src/ipc/routes/wake-conversation.ts +19 -0
- package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
- package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
- package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
- package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
- package/src/memory/app-store.ts +1 -1
- package/src/memory/attachments-store.ts +70 -0
- package/src/memory/auto-analysis-enqueue.ts +127 -0
- package/src/memory/auto-analysis-guard.ts +27 -0
- package/src/memory/cleanup-schedule-state.ts +37 -0
- package/src/memory/conversation-analyze-job.ts +73 -0
- package/src/memory/conversation-crud.ts +99 -0
- package/src/memory/conversation-disk-view.ts +7 -0
- package/src/memory/conversation-group-migration.ts +34 -2
- package/src/memory/conversation-queries.ts +6 -5
- package/src/memory/db-init.ts +6 -0
- package/src/memory/db-maintenance.ts +108 -0
- package/src/memory/db.ts +1 -0
- package/src/memory/graph/conversation-graph-memory.ts +15 -0
- package/src/memory/graph/extraction.test.ts +23 -0
- package/src/memory/graph/extraction.ts +8 -0
- package/src/memory/graph/retriever.ts +27 -18
- package/src/memory/graph/scoring.test.ts +186 -0
- package/src/memory/graph/scoring.ts +31 -1
- package/src/memory/graph/tools.ts +1 -1
- package/src/memory/group-crud.ts +6 -1
- package/src/memory/indexer.ts +95 -16
- package/src/memory/job-handlers/cleanup.ts +11 -8
- package/src/memory/job-handlers/conversation-starters.ts +16 -10
- package/src/memory/jobs-store.ts +64 -4
- package/src/memory/jobs-worker.ts +22 -9
- package/src/memory/llm-usage-store.ts +92 -56
- package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
- package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
- package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/qdrant-manager.ts +43 -16
- package/src/memory/schema/conversations.ts +2 -0
- package/src/memory/schema/oauth.ts +3 -0
- package/src/memory/usage-buckets.ts +396 -0
- package/src/messaging/providers/gmail/client.ts +57 -6
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
- package/src/messaging/providers/slack/adapter.ts +143 -38
- package/src/messaging/providers/slack/client.ts +16 -0
- package/src/messaging/providers/slack/types.ts +4 -0
- package/src/notifications/decision-engine.ts +3 -3
- package/src/notifications/signal.ts +5 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +1 -0
- package/src/oauth/byo-connection.test.ts +18 -1
- package/src/oauth/byo-connection.ts +3 -1
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.ts +6 -2
- package/src/oauth/connection.ts +2 -0
- package/src/oauth/oauth-store.ts +9 -0
- package/src/oauth/platform-connection.test.ts +98 -0
- package/src/oauth/platform-connection.ts +52 -31
- package/src/oauth/seed-providers.ts +7 -0
- package/src/permissions/checker.ts +16 -6
- package/src/permissions/defaults.ts +49 -1
- package/src/permissions/trust-store.ts +3 -3
- package/src/permissions/workspace-policy.ts +3 -0
- package/src/platform/client.test.ts +10 -0
- package/src/platform/sync-identity.ts +129 -0
- package/src/prompts/persona-resolver.ts +126 -2
- package/src/prompts/system-prompt.ts +59 -18
- package/src/prompts/templates/BOOTSTRAP.md +5 -5
- package/src/prompts/templates/SOUL.md +3 -1
- package/src/prompts/templates/UPDATES.md +12 -0
- package/src/prompts/templates/channels/slack.md +20 -0
- package/src/prompts/update-bulletin-format.ts +26 -9
- package/src/prompts/update-bulletin.ts +34 -23
- package/src/prompts/user-reference.ts +20 -17
- package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
- package/src/providers/anthropic/client.ts +157 -61
- package/src/providers/fireworks/client.ts +2 -2
- package/src/providers/gemini/client.ts +9 -1
- package/src/providers/model-catalog.ts +6 -0
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/ollama/client.ts +2 -2
- package/src/providers/openai/chat-completions-provider.ts +474 -0
- package/src/providers/openai/client.ts +25 -440
- package/src/providers/openai/responses-provider.ts +502 -0
- package/src/providers/openrouter/client.ts +101 -4
- package/src/providers/provider-secret-catalog.ts +139 -0
- package/src/providers/registry.ts +2 -2
- package/src/providers/retry.ts +14 -3
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
- package/src/providers/speech-to-text/deepgram.test.ts +332 -0
- package/src/providers/speech-to-text/deepgram.ts +115 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
- package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
- package/src/providers/speech-to-text/google-gemini.ts +101 -0
- package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
- package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
- package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
- package/src/providers/speech-to-text/openai-whisper.ts +63 -33
- package/src/providers/speech-to-text/provider-catalog.ts +306 -0
- package/src/providers/speech-to-text/resolve.ts +386 -6
- package/src/providers/types.ts +9 -0
- package/src/runtime/AGENTS.md +43 -1
- package/src/runtime/__tests__/agent-wake.test.ts +831 -0
- package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
- package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
- package/src/runtime/agent-wake.ts +512 -0
- package/src/runtime/auth/__tests__/route-policy.test.ts +40 -0
- package/src/runtime/auth/route-policy.ts +30 -5
- package/src/runtime/auth/token-service.ts +56 -1
- package/src/runtime/btw-sidechain.ts +2 -0
- package/src/runtime/capability-tokens.ts +10 -10
- package/src/runtime/channel-invite-transport.ts +1 -1
- package/src/runtime/channel-invite-transports/email.ts +14 -6
- package/src/runtime/channel-readiness-service.ts +12 -22
- package/src/runtime/chrome-extension-registry.ts +38 -2
- package/src/runtime/http-server.ts +395 -10
- package/src/runtime/http-types.ts +6 -2
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +36 -0
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
- package/src/runtime/migrations/migration-transport.ts +1 -0
- package/src/runtime/migrations/migration-wizard.ts +1 -0
- package/src/runtime/migrations/vbundle-import-analyzer.ts +77 -1
- package/src/runtime/migrations/vbundle-importer.ts +34 -0
- package/src/runtime/pending-interactions.ts +0 -11
- package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
- package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
- package/src/runtime/routes/app-management-routes.ts +12 -18
- package/src/runtime/routes/attachment-routes.test.ts +9 -3
- package/src/runtime/routes/attachment-routes.ts +216 -17
- package/src/runtime/routes/backup-routes.ts +519 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +82 -23
- package/src/runtime/routes/btw-routes.ts +8 -6
- package/src/runtime/routes/contact-routes.test.ts +298 -0
- package/src/runtime/routes/contact-routes.ts +132 -5
- package/src/runtime/routes/conversation-analysis-routes.ts +22 -142
- package/src/runtime/routes/conversation-management-routes.ts +115 -0
- package/src/runtime/routes/conversation-routes.ts +367 -146
- package/src/runtime/routes/filing-routes.ts +93 -0
- package/src/runtime/routes/home-feed-routes.ts +334 -0
- package/src/runtime/routes/home-state-routes.ts +138 -0
- package/src/runtime/routes/host-browser-routes.ts +3 -14
- package/src/runtime/routes/identity-intro-cache.ts +7 -3
- package/src/runtime/routes/identity-routes.ts +3 -17
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
- package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
- package/src/runtime/routes/integrations/slack/channel.ts +11 -3
- package/src/runtime/routes/integrations/slack/share.ts +45 -7
- package/src/runtime/routes/llm-context-normalization.ts +303 -0
- package/src/runtime/routes/memory-item-routes.test.ts +3 -2
- package/src/runtime/routes/migration-routes.ts +40 -5
- package/src/runtime/routes/settings-routes.ts +22 -5
- package/src/runtime/routes/skills-routes.ts +76 -7
- package/src/runtime/routes/stt-routes.ts +233 -0
- package/src/runtime/routes/surface-action-routes.ts +41 -2
- package/src/runtime/routes/tts-routes.ts +108 -24
- package/src/runtime/routes/usage-routes.ts +30 -2
- package/src/runtime/routes/user-route-dispatcher.ts +50 -5
- package/src/runtime/routes/user-routes.ts +13 -1
- package/src/runtime/routes/work-items-routes.ts +8 -1
- package/src/runtime/runtime-mode.ts +33 -0
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
- package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
- package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
- package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
- package/src/runtime/services/analyze-conversation.ts +344 -0
- package/src/runtime/services/analyze-deps-singleton.ts +32 -0
- package/src/runtime/services/auto-analysis-prompt.ts +55 -0
- package/src/runtime/skill-route-registry.ts +49 -0
- package/src/runtime/slack-block-formatting.ts +437 -10
- package/src/schedule/scheduler.ts +50 -0
- package/src/security/oauth2.ts +26 -4
- package/src/security/secure-keys.ts +25 -2
- package/src/security/token-manager.ts +8 -0
- package/src/sequence/engine.ts +23 -0
- package/src/sequence/types.ts +1 -1
- package/src/skills/catalog-files.ts +64 -2
- package/src/skills/category-inference.ts +122 -0
- package/src/skills/clawhub-files.ts +213 -0
- package/src/skills/clawhub.ts +84 -23
- package/src/skills/skill-file-provider.ts +40 -0
- package/src/skills/skillssh-files.ts +395 -0
- package/src/skills/skillssh-registry.ts +4 -4
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
- package/src/stt/__tests__/types.test.ts +89 -0
- package/src/stt/daemon-batch-transcriber.ts +195 -0
- package/src/stt/stt-stream-session.ts +499 -0
- package/src/stt/types.ts +330 -0
- package/src/stt/wav-encoder.test.ts +373 -0
- package/src/stt/wav-encoder.ts +175 -0
- package/src/subagent/manager.ts +38 -14
- package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
- package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
- package/src/tools/browser/browser-execution.ts +1163 -23
- package/src/tools/browser/browser-manager.ts +45 -0
- package/src/tools/browser/browser-mode-constants.ts +12 -0
- package/src/tools/browser/browser-mode.ts +92 -0
- package/src/tools/browser/browser-status-constants.ts +33 -0
- package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +393 -0
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +29 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1648 -32
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +264 -0
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +183 -17
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +254 -21
- package/src/tools/browser/cdp-client/errors.ts +15 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +39 -16
- package/src/tools/browser/cdp-client/factory.ts +797 -87
- package/src/tools/browser/cdp-client/index.ts +16 -2
- package/src/tools/browser/cdp-client/types.ts +68 -0
- package/src/tools/credentials/vault.ts +35 -6
- package/src/tools/network/web-fetch.ts +5 -2
- package/src/tools/network/web-search.ts +5 -2
- package/src/tools/shared/shell-output.ts +3 -1
- package/src/tools/side-effects.ts +2 -0
- package/src/tools/skills/sandbox-runner.ts +3 -2
- package/src/tools/terminal/safe-env.ts +10 -2
- package/src/tools/terminal/shell.ts +15 -4
- package/src/tools/tool-manifest.ts +21 -0
- package/src/tools/types.ts +17 -0
- package/src/tools/ui-surface/definitions.ts +6 -1
- package/src/tts/__tests__/provider-adapters.test.ts +834 -0
- package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
- package/src/tts/__tests__/provider-catalog.test.ts +183 -0
- package/src/tts/__tests__/provider-registry.test.ts +90 -0
- package/src/tts/provider-catalog.ts +201 -0
- package/src/tts/provider-registry.ts +73 -0
- package/src/tts/providers/deepgram-provider.ts +219 -0
- package/src/tts/providers/elevenlabs-provider.ts +211 -0
- package/src/tts/providers/fish-audio-provider.ts +183 -0
- package/src/tts/providers/index.ts +42 -0
- package/src/tts/providers/register-builtins.ts +130 -0
- package/src/tts/synthesize-text.ts +110 -0
- package/src/tts/tts-config-resolver.ts +78 -0
- package/src/tts/types.ts +153 -0
- package/src/types/onboarding-context.ts +7 -0
- package/src/util/abort-reasons.ts +58 -0
- package/src/util/device-id.ts +32 -16
- package/src/util/errors.ts +9 -1
- package/src/util/platform.ts +54 -10
- package/src/util/pricing.ts +66 -3
- package/src/util/spawn.ts +1 -1
- package/src/util/truncate.ts +4 -2
- package/src/util/unicode.ts +201 -0
- package/src/version.ts +19 -24
- package/src/watcher/engine.ts +23 -0
- package/src/watcher/watcher-store.ts +31 -0
- package/src/workspace/migrations/003-seed-device-id.ts +9 -3
- package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
- package/src/workspace/migrations/029-seed-pkb.ts +1 -1
- package/src/workspace/migrations/031-drop-user-md.ts +317 -0
- package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
- package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
- package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
- package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
- package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
- package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
- package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
- package/src/workspace/migrations/registry.ts +16 -0
- package/src/workspace/top-level-renderer.ts +13 -1
- package/src/workspace/turn-commit.ts +31 -0
- package/src/__tests__/email-cli.test.ts +0 -297
- package/src/__tests__/email-service-config-fallback.test.ts +0 -102
- package/src/cli/commands/browser-relay.ts +0 -466
- package/src/email/guardrails.ts +0 -221
- package/src/email/provider.ts +0 -117
- package/src/email/providers/agentmail.ts +0 -361
- package/src/email/providers/index.ts +0 -65
- package/src/email/service.ts +0 -384
- package/src/email/types.ts +0 -126
- package/src/prompts/templates/USER.md +0 -13
- package/src/providers/speech-to-text/types.ts +0 -17
- package/src/runtime/routes/browser-cdp-routes.ts +0 -229
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { BatchTranscriber } from "../../../stt/types.js";
|
|
4
|
+
import { SttError } from "../../../stt/types.js";
|
|
4
5
|
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// Mocks — must be set up before importing the module under test
|
|
@@ -17,7 +18,7 @@ let mockAttachments: Array<{
|
|
|
17
18
|
thumbnailBase64: string | null;
|
|
18
19
|
createdAt: number;
|
|
19
20
|
}> = [];
|
|
20
|
-
let
|
|
21
|
+
let mockTranscriber: BatchTranscriber | null = null;
|
|
21
22
|
|
|
22
23
|
mock.module("../../../config/assistant-feature-flags.js", () => ({
|
|
23
24
|
isAssistantFeatureFlagEnabled: () => mockFeatureFlagEnabled,
|
|
@@ -35,7 +36,18 @@ mock.module("../../../memory/attachments-store.js", () => ({
|
|
|
35
36
|
}));
|
|
36
37
|
|
|
37
38
|
mock.module("../../../providers/speech-to-text/resolve.js", () => ({
|
|
38
|
-
|
|
39
|
+
resolveBatchTranscriber: async () => mockTranscriber,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
mock.module("../../../stt/daemon-batch-transcriber.js", () => ({
|
|
43
|
+
normalizeSttError: (err: unknown): SttError => {
|
|
44
|
+
if (err instanceof SttError) return err;
|
|
45
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
46
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
47
|
+
return new SttError("timeout", message);
|
|
48
|
+
}
|
|
49
|
+
return new SttError("provider-error", message);
|
|
50
|
+
},
|
|
39
51
|
}));
|
|
40
52
|
|
|
41
53
|
mock.module("../../../util/logger.js", () => ({
|
|
@@ -105,7 +117,7 @@ describe("tryTranscribeAudioAttachments", () => {
|
|
|
105
117
|
beforeEach(() => {
|
|
106
118
|
mockFeatureFlagEnabled = true;
|
|
107
119
|
mockAttachments = [];
|
|
108
|
-
|
|
120
|
+
mockTranscriber = null;
|
|
109
121
|
});
|
|
110
122
|
|
|
111
123
|
afterEach(() => {
|
|
@@ -115,7 +127,9 @@ describe("tryTranscribeAudioAttachments", () => {
|
|
|
115
127
|
test("audio attachment is transcribed and returns transcribed result", async () => {
|
|
116
128
|
const audio = makeAudioAttachment("a1");
|
|
117
129
|
mockAttachments = [audio];
|
|
118
|
-
|
|
130
|
+
mockTranscriber = {
|
|
131
|
+
providerId: "openai-whisper",
|
|
132
|
+
boundaryId: "daemon-batch",
|
|
119
133
|
transcribe: async () => ({ text: "Hello, how are you?" }),
|
|
120
134
|
};
|
|
121
135
|
|
|
@@ -131,7 +145,9 @@ describe("tryTranscribeAudioAttachments", () => {
|
|
|
131
145
|
const doc = makeDocumentAttachment("d1");
|
|
132
146
|
const img = makeImageAttachment("i1");
|
|
133
147
|
mockAttachments = [doc, img];
|
|
134
|
-
|
|
148
|
+
mockTranscriber = {
|
|
149
|
+
providerId: "openai-whisper",
|
|
150
|
+
boundaryId: "daemon-batch",
|
|
135
151
|
transcribe: async () => ({ text: "should not be called" }),
|
|
136
152
|
};
|
|
137
153
|
|
|
@@ -140,23 +156,26 @@ describe("tryTranscribeAudioAttachments", () => {
|
|
|
140
156
|
expect(result).toEqual({ status: "no_audio" });
|
|
141
157
|
});
|
|
142
158
|
|
|
143
|
-
test("no
|
|
159
|
+
test("no provider returns no_provider with service-agnostic reason", async () => {
|
|
144
160
|
const audio = makeAudioAttachment("a1");
|
|
145
161
|
mockAttachments = [audio];
|
|
146
|
-
|
|
162
|
+
mockTranscriber = null; // No transcriber resolved
|
|
147
163
|
|
|
148
164
|
const result = await tryTranscribeAudioAttachments(["a1"]);
|
|
149
165
|
|
|
150
166
|
expect(result.status).toBe("no_provider");
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
);
|
|
167
|
+
const reason = (result as { reason: string }).reason;
|
|
168
|
+
expect(reason).toContain("speech-to-text");
|
|
169
|
+
expect(reason).toContain("voice message transcription");
|
|
170
|
+
expect(reason).not.toContain("OpenAI");
|
|
154
171
|
});
|
|
155
172
|
|
|
156
173
|
test("API failure returns error with reason", async () => {
|
|
157
174
|
const audio = makeAudioAttachment("a1");
|
|
158
175
|
mockAttachments = [audio];
|
|
159
|
-
|
|
176
|
+
mockTranscriber = {
|
|
177
|
+
providerId: "openai-whisper",
|
|
178
|
+
boundaryId: "daemon-batch",
|
|
160
179
|
transcribe: async () => {
|
|
161
180
|
throw new Error("API rate limit exceeded");
|
|
162
181
|
},
|
|
@@ -183,29 +202,9 @@ describe("tryTranscribeAudioAttachments", () => {
|
|
|
183
202
|
test("30-second timeout fires and returns error without blocking", async () => {
|
|
184
203
|
const audio = makeAudioAttachment("a1");
|
|
185
204
|
mockAttachments = [audio];
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return new Promise((_resolve, reject) => {
|
|
190
|
-
if (signal?.aborted) {
|
|
191
|
-
reject(new DOMException("The operation was aborted", "AbortError"));
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
const onAbort = () => {
|
|
195
|
-
reject(new DOMException("The operation was aborted", "AbortError"));
|
|
196
|
-
};
|
|
197
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
198
|
-
});
|
|
199
|
-
},
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
// The timeout is 30s in the real code, but the test's mock provider
|
|
203
|
-
// aborts immediately when signaled. We verify the error path works
|
|
204
|
-
// by checking the result type. For a true timeout test we'd need
|
|
205
|
-
// to override the timeout constant, but this confirms the abort
|
|
206
|
-
// path produces the correct result.
|
|
207
|
-
// Instead, let's test with a provider that checks signal state:
|
|
208
|
-
mockProvider = {
|
|
205
|
+
mockTranscriber = {
|
|
206
|
+
providerId: "openai-whisper",
|
|
207
|
+
boundaryId: "daemon-batch",
|
|
209
208
|
transcribe: async () => {
|
|
210
209
|
throw new DOMException("The operation was aborted", "AbortError");
|
|
211
210
|
},
|
|
@@ -225,7 +224,9 @@ describe("tryTranscribeAudioAttachments", () => {
|
|
|
225
224
|
mockAttachments = [a1, a2];
|
|
226
225
|
|
|
227
226
|
let callCount = 0;
|
|
228
|
-
|
|
227
|
+
mockTranscriber = {
|
|
228
|
+
providerId: "openai-whisper",
|
|
229
|
+
boundaryId: "daemon-batch",
|
|
229
230
|
transcribe: async () => {
|
|
230
231
|
callCount++;
|
|
231
232
|
return { text: callCount === 1 ? "First message" : "Second message" };
|
|
@@ -247,7 +248,9 @@ describe("tryTranscribeAudioAttachments", () => {
|
|
|
247
248
|
mockAttachments = [audio, doc];
|
|
248
249
|
|
|
249
250
|
let transcribeCallCount = 0;
|
|
250
|
-
|
|
251
|
+
mockTranscriber = {
|
|
252
|
+
providerId: "openai-whisper",
|
|
253
|
+
boundaryId: "daemon-batch",
|
|
251
254
|
transcribe: async () => {
|
|
252
255
|
transcribeCallCount++;
|
|
253
256
|
return { text: "Voice transcription" };
|
|
@@ -264,7 +267,9 @@ describe("tryTranscribeAudioAttachments", () => {
|
|
|
264
267
|
});
|
|
265
268
|
|
|
266
269
|
test("empty attachment IDs returns no_audio", async () => {
|
|
267
|
-
|
|
270
|
+
mockTranscriber = {
|
|
271
|
+
providerId: "openai-whisper",
|
|
272
|
+
boundaryId: "daemon-batch",
|
|
268
273
|
transcribe: async () => ({ text: "should not be called" }),
|
|
269
274
|
};
|
|
270
275
|
|
|
@@ -276,7 +281,9 @@ describe("tryTranscribeAudioAttachments", () => {
|
|
|
276
281
|
test("attachment with empty transcription returns no_audio", async () => {
|
|
277
282
|
const audio = makeAudioAttachment("a1");
|
|
278
283
|
mockAttachments = [audio];
|
|
279
|
-
|
|
284
|
+
mockTranscriber = {
|
|
285
|
+
providerId: "openai-whisper",
|
|
286
|
+
boundaryId: "daemon-batch",
|
|
280
287
|
transcribe: async () => ({ text: " " }), // whitespace-only
|
|
281
288
|
};
|
|
282
289
|
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
import { isAssistantFeatureFlagEnabled } from "../../../config/assistant-feature-flags.js";
|
|
11
11
|
import { getConfig } from "../../../config/loader.js";
|
|
12
12
|
import * as attachmentsStore from "../../../memory/attachments-store.js";
|
|
13
|
-
import {
|
|
13
|
+
import { resolveBatchTranscriber } from "../../../providers/speech-to-text/resolve.js";
|
|
14
|
+
import { normalizeSttError } from "../../../stt/daemon-batch-transcriber.js";
|
|
14
15
|
import { getLogger } from "../../../util/logger.js";
|
|
15
16
|
|
|
16
17
|
const log = getLogger("transcribe-audio");
|
|
@@ -55,13 +56,13 @@ export async function tryTranscribeAudioAttachments(
|
|
|
55
56
|
return { status: "no_audio" };
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
// Resolve STT provider
|
|
59
|
-
const
|
|
60
|
-
if (!
|
|
59
|
+
// Resolve STT provider via daemon batch transcriber facade
|
|
60
|
+
const transcriber = await resolveBatchTranscriber();
|
|
61
|
+
if (!transcriber) {
|
|
61
62
|
return {
|
|
62
63
|
status: "no_provider",
|
|
63
64
|
reason:
|
|
64
|
-
"No
|
|
65
|
+
"No speech-to-text provider configured. Add an API key for your STT service to enable voice message transcription.",
|
|
65
66
|
};
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -89,11 +90,11 @@ export async function tryTranscribeAudioAttachments(
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
const buffer = Buffer.from(hydrated.dataBase64, "base64");
|
|
92
|
-
const result = await
|
|
93
|
-
buffer,
|
|
94
|
-
attachment.mimeType,
|
|
95
|
-
abortController.signal,
|
|
96
|
-
);
|
|
93
|
+
const result = await transcriber.transcribe({
|
|
94
|
+
audio: buffer,
|
|
95
|
+
mimeType: attachment.mimeType,
|
|
96
|
+
signal: abortController.signal,
|
|
97
|
+
});
|
|
97
98
|
|
|
98
99
|
if (result.text.trim()) {
|
|
99
100
|
transcriptions.push(result.text.trim());
|
|
@@ -109,12 +110,11 @@ export async function tryTranscribeAudioAttachments(
|
|
|
109
110
|
clearTimeout(timeoutId);
|
|
110
111
|
}
|
|
111
112
|
} catch (err: unknown) {
|
|
113
|
+
const sttErr = normalizeSttError(err);
|
|
112
114
|
const reason =
|
|
113
|
-
|
|
114
|
-
?
|
|
115
|
-
|
|
116
|
-
: err.message
|
|
117
|
-
: String(err);
|
|
115
|
+
sttErr.category === "timeout"
|
|
116
|
+
? "Transcription timed out"
|
|
117
|
+
: sttErr.message;
|
|
118
118
|
log.warn({ err }, "Audio transcription failed");
|
|
119
119
|
return { status: "error", reason };
|
|
120
120
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the POST /v1/integrations/slack/channel/config HTTP route.
|
|
3
|
+
*
|
|
4
|
+
* Mocks `setSlackChannelConfig` in the config-slack-channel handler module so
|
|
5
|
+
* the test can observe which arguments the HTTP handler forwards from the
|
|
6
|
+
* request body — particularly the optional `userToken` field.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
10
|
+
|
|
11
|
+
import type { SlackChannelConfigResult } from "../../../../../daemon/handlers/config-slack-channel.js";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Module mock — must appear before importing the module under test
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
interface SetConfigCall {
|
|
18
|
+
botToken?: string;
|
|
19
|
+
appToken?: string;
|
|
20
|
+
userToken?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let lastSetConfigCall: SetConfigCall | null = null;
|
|
24
|
+
let mockSetConfigResult: SlackChannelConfigResult = {
|
|
25
|
+
success: true,
|
|
26
|
+
hasBotToken: false,
|
|
27
|
+
hasAppToken: false,
|
|
28
|
+
hasUserToken: false,
|
|
29
|
+
connected: false,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
mock.module("../../../../../daemon/handlers/config-slack-channel.js", () => ({
|
|
33
|
+
setSlackChannelConfig: async (
|
|
34
|
+
botToken?: string,
|
|
35
|
+
appToken?: string,
|
|
36
|
+
userToken?: string,
|
|
37
|
+
): Promise<SlackChannelConfigResult> => {
|
|
38
|
+
lastSetConfigCall = { botToken, appToken, userToken };
|
|
39
|
+
return mockSetConfigResult;
|
|
40
|
+
},
|
|
41
|
+
getSlackChannelConfig: async (): Promise<SlackChannelConfigResult> => ({
|
|
42
|
+
success: true,
|
|
43
|
+
hasBotToken: false,
|
|
44
|
+
hasAppToken: false,
|
|
45
|
+
hasUserToken: false,
|
|
46
|
+
connected: false,
|
|
47
|
+
}),
|
|
48
|
+
clearSlackChannelConfig: async (): Promise<SlackChannelConfigResult> => ({
|
|
49
|
+
success: true,
|
|
50
|
+
hasBotToken: false,
|
|
51
|
+
hasAppToken: false,
|
|
52
|
+
hasUserToken: false,
|
|
53
|
+
connected: false,
|
|
54
|
+
}),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
const { handleSetSlackChannelConfig } = await import("../channel.js");
|
|
58
|
+
|
|
59
|
+
describe("POST /v1/integrations/slack/channel/config", () => {
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
lastSetConfigCall = null;
|
|
62
|
+
mockSetConfigResult = {
|
|
63
|
+
success: true,
|
|
64
|
+
hasBotToken: false,
|
|
65
|
+
hasAppToken: false,
|
|
66
|
+
hasUserToken: false,
|
|
67
|
+
connected: false,
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("forwards userToken from request body as the third argument", async () => {
|
|
72
|
+
const req = new Request("http://localhost/v1/integrations/slack/channel/config", {
|
|
73
|
+
method: "POST",
|
|
74
|
+
body: JSON.stringify({ userToken: "xoxp-test-user-token" }),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const res = await handleSetSlackChannelConfig(req);
|
|
78
|
+
expect(res.status).toBe(200);
|
|
79
|
+
|
|
80
|
+
expect(lastSetConfigCall).not.toBeNull();
|
|
81
|
+
expect(lastSetConfigCall?.botToken).toBeUndefined();
|
|
82
|
+
expect(lastSetConfigCall?.appToken).toBeUndefined();
|
|
83
|
+
expect(lastSetConfigCall?.userToken).toBe("xoxp-test-user-token");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("forwards all three tokens when present in body", async () => {
|
|
87
|
+
const req = new Request("http://localhost/v1/integrations/slack/channel/config", {
|
|
88
|
+
method: "POST",
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
botToken: "xoxb-bot",
|
|
91
|
+
appToken: "xapp-app",
|
|
92
|
+
userToken: "xoxp-user",
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const res = await handleSetSlackChannelConfig(req);
|
|
97
|
+
expect(res.status).toBe(200);
|
|
98
|
+
|
|
99
|
+
expect(lastSetConfigCall?.botToken).toBe("xoxb-bot");
|
|
100
|
+
expect(lastSetConfigCall?.appToken).toBe("xapp-app");
|
|
101
|
+
expect(lastSetConfigCall?.userToken).toBe("xoxp-user");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("leaves userToken undefined when absent from body", async () => {
|
|
105
|
+
const req = new Request("http://localhost/v1/integrations/slack/channel/config", {
|
|
106
|
+
method: "POST",
|
|
107
|
+
body: JSON.stringify({ botToken: "xoxb-bot", appToken: "xapp-app" }),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const res = await handleSetSlackChannelConfig(req);
|
|
111
|
+
expect(res.status).toBe(200);
|
|
112
|
+
|
|
113
|
+
expect(lastSetConfigCall?.botToken).toBe("xoxb-bot");
|
|
114
|
+
expect(lastSetConfigCall?.appToken).toBe("xapp-app");
|
|
115
|
+
expect(lastSetConfigCall?.userToken).toBeUndefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("returns 400 when handler reports success: false", async () => {
|
|
119
|
+
mockSetConfigResult = {
|
|
120
|
+
success: false,
|
|
121
|
+
hasBotToken: false,
|
|
122
|
+
hasAppToken: false,
|
|
123
|
+
hasUserToken: false,
|
|
124
|
+
connected: false,
|
|
125
|
+
error: "Invalid user token: must start with \"xoxp-\"",
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const req = new Request("http://localhost/v1/integrations/slack/channel/config", {
|
|
129
|
+
method: "POST",
|
|
130
|
+
body: JSON.stringify({ userToken: "abc-123" }),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const res = await handleSetSlackChannelConfig(req);
|
|
134
|
+
expect(res.status).toBe(400);
|
|
135
|
+
expect(lastSetConfigCall?.userToken).toBe("abc-123");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the Share UI Slack route handlers.
|
|
3
|
+
*
|
|
4
|
+
* Verifies the read/write auth split mirrors `messaging/providers/slack/adapter.ts`:
|
|
5
|
+
* - Channel enumeration (GET /v1/slack/channels) is a read path and must
|
|
6
|
+
* prefer the user_token when present so the picker surfaces channels the
|
|
7
|
+
* user is in but the bot isn't.
|
|
8
|
+
* - Channel sharing (POST /v1/slack/share) is a write path and must always
|
|
9
|
+
* use the bot_token so posts come from the bot identity.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
13
|
+
|
|
14
|
+
import { credentialKey } from "../../../../../security/credential-key.js";
|
|
15
|
+
|
|
16
|
+
// ── Module mocks ────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const getSecureKeyAsyncMock = mock(
|
|
19
|
+
async (_key: string): Promise<string | null> => null,
|
|
20
|
+
);
|
|
21
|
+
mock.module("../../../../../security/secure-keys.js", () => ({
|
|
22
|
+
getSecureKeyAsync: getSecureKeyAsyncMock,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
// share.ts imports getConnectionByProvider from oauth-store at module load.
|
|
26
|
+
// Stub it to return undefined so Socket Mode tokens are the only source.
|
|
27
|
+
mock.module("../../../../../oauth/oauth-store.js", () => ({
|
|
28
|
+
getConnectionByProvider: () => undefined,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
// Stub the app store so handleShareToSlackChannel finds the app.
|
|
32
|
+
const FAKE_APP = { id: "app-1", name: "Test App", description: "desc" };
|
|
33
|
+
mock.module("../../../../../memory/app-store.js", () => ({
|
|
34
|
+
getApp: (id: string) => (id === FAKE_APP.id ? FAKE_APP : undefined),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
const { handleListSlackChannels, handleShareToSlackChannel } = await import(
|
|
38
|
+
"../share.js"
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// ── fetch capture ───────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
type CapturedRequest = {
|
|
44
|
+
url: string;
|
|
45
|
+
method: string;
|
|
46
|
+
authorization: string | null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const captured: CapturedRequest[] = [];
|
|
50
|
+
const originalFetch = globalThis.fetch;
|
|
51
|
+
|
|
52
|
+
function installFetchStub() {
|
|
53
|
+
globalThis.fetch = (async (
|
|
54
|
+
input: RequestInfo | URL,
|
|
55
|
+
init?: RequestInit,
|
|
56
|
+
): Promise<Response> => {
|
|
57
|
+
const url =
|
|
58
|
+
typeof input === "string"
|
|
59
|
+
? input
|
|
60
|
+
: input instanceof URL
|
|
61
|
+
? input.toString()
|
|
62
|
+
: input.url;
|
|
63
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
64
|
+
const headers = new Headers(init?.headers ?? {});
|
|
65
|
+
captured.push({
|
|
66
|
+
url,
|
|
67
|
+
method,
|
|
68
|
+
authorization: headers.get("authorization"),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Minimal OK Slack envelopes so handlers don't throw on shape mismatches.
|
|
72
|
+
const body = fakeSlackResponse(url);
|
|
73
|
+
return new Response(JSON.stringify(body), {
|
|
74
|
+
status: 200,
|
|
75
|
+
headers: { "Content-Type": "application/json" },
|
|
76
|
+
});
|
|
77
|
+
}) as typeof fetch;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function fakeSlackResponse(url: string): Record<string, unknown> {
|
|
81
|
+
if (url.includes("/conversations.list")) {
|
|
82
|
+
return { ok: true, channels: [], response_metadata: { next_cursor: "" } };
|
|
83
|
+
}
|
|
84
|
+
if (url.includes("/chat.postMessage")) {
|
|
85
|
+
return { ok: true, ts: "1700000000.000100", channel: "C123" };
|
|
86
|
+
}
|
|
87
|
+
return { ok: true };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Test fixtures ───────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
const BOT_TOKEN = "xoxb-BOT";
|
|
93
|
+
const USER_TOKEN = "xoxp-USER";
|
|
94
|
+
|
|
95
|
+
describe("Slack share route token routing", () => {
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
captured.length = 0;
|
|
98
|
+
getSecureKeyAsyncMock.mockReset();
|
|
99
|
+
installFetchStub();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
afterEach(() => {
|
|
103
|
+
globalThis.fetch = originalFetch;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("GET /v1/slack/channels: bot-only install reads with bot token", async () => {
|
|
107
|
+
getSecureKeyAsyncMock.mockImplementation(async (key: string) => {
|
|
108
|
+
if (key === credentialKey("slack_channel", "bot_token")) return BOT_TOKEN;
|
|
109
|
+
return null;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const res = await handleListSlackChannels();
|
|
113
|
+
expect(res.status).toBe(200);
|
|
114
|
+
|
|
115
|
+
const listCall = captured.find((c) =>
|
|
116
|
+
c.url.includes("/conversations.list"),
|
|
117
|
+
);
|
|
118
|
+
expect(listCall).toBeDefined();
|
|
119
|
+
expect(listCall!.authorization).toBe(`Bearer ${BOT_TOKEN}`);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("GET /v1/slack/channels: bot + user tokens prefer user_token for reads", async () => {
|
|
123
|
+
// Core fix: with both tokens stored, the Share UI picker must see every
|
|
124
|
+
// channel the USER can see — not just ones the bot is a member of.
|
|
125
|
+
getSecureKeyAsyncMock.mockImplementation(async (key: string) => {
|
|
126
|
+
if (key === credentialKey("slack_channel", "bot_token")) return BOT_TOKEN;
|
|
127
|
+
if (key === credentialKey("slack_channel", "user_token"))
|
|
128
|
+
return USER_TOKEN;
|
|
129
|
+
return null;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const res = await handleListSlackChannels();
|
|
133
|
+
expect(res.status).toBe(200);
|
|
134
|
+
|
|
135
|
+
const listCall = captured.find((c) =>
|
|
136
|
+
c.url.includes("/conversations.list"),
|
|
137
|
+
);
|
|
138
|
+
expect(listCall).toBeDefined();
|
|
139
|
+
expect(listCall!.authorization).toBe(`Bearer ${USER_TOKEN}`);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("POST /v1/slack/share: bot + user tokens still write with bot token", async () => {
|
|
143
|
+
// SAFETY invariant: posts MUST come from the bot identity. If the handler
|
|
144
|
+
// ever routed the write through user_token, the posted message would
|
|
145
|
+
// appear as the user — unambiguously wrong.
|
|
146
|
+
getSecureKeyAsyncMock.mockImplementation(async (key: string) => {
|
|
147
|
+
if (key === credentialKey("slack_channel", "bot_token")) return BOT_TOKEN;
|
|
148
|
+
if (key === credentialKey("slack_channel", "user_token"))
|
|
149
|
+
return USER_TOKEN;
|
|
150
|
+
return null;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const req = new Request("http://localhost/v1/slack/share", {
|
|
154
|
+
method: "POST",
|
|
155
|
+
body: JSON.stringify({ appId: FAKE_APP.id, channelId: "C123" }),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const res = await handleShareToSlackChannel(req);
|
|
159
|
+
expect(res.status).toBe(200);
|
|
160
|
+
|
|
161
|
+
const postCall = captured.find((c) => c.url.includes("/chat.postMessage"));
|
|
162
|
+
expect(postCall).toBeDefined();
|
|
163
|
+
expect(postCall!.authorization).toBe(`Bearer ${BOT_TOKEN}`);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("no tokens configured: both handlers return 503", async () => {
|
|
167
|
+
getSecureKeyAsyncMock.mockImplementation(async () => null);
|
|
168
|
+
|
|
169
|
+
const listRes = await handleListSlackChannels();
|
|
170
|
+
expect(listRes.status).toBe(503);
|
|
171
|
+
|
|
172
|
+
const shareReq = new Request("http://localhost/v1/slack/share", {
|
|
173
|
+
method: "POST",
|
|
174
|
+
body: JSON.stringify({ appId: FAKE_APP.id, channelId: "C123" }),
|
|
175
|
+
});
|
|
176
|
+
const shareRes = await handleShareToSlackChannel(shareReq);
|
|
177
|
+
expect(shareRes.status).toBe(503);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -28,13 +28,21 @@ export async function handleGetSlackChannelConfig(): Promise<Response> {
|
|
|
28
28
|
/**
|
|
29
29
|
* POST /v1/integrations/slack/channel/config
|
|
30
30
|
*
|
|
31
|
-
* Body: { botToken?: string, appToken?: string }
|
|
31
|
+
* Body: { botToken?: string, appToken?: string, userToken?: string }
|
|
32
32
|
*/
|
|
33
33
|
export async function handleSetSlackChannelConfig(
|
|
34
34
|
req: Request,
|
|
35
35
|
): Promise<Response> {
|
|
36
|
-
const body = (await req.json()) as {
|
|
37
|
-
|
|
36
|
+
const body = (await req.json()) as {
|
|
37
|
+
botToken?: string;
|
|
38
|
+
appToken?: string;
|
|
39
|
+
userToken?: string;
|
|
40
|
+
};
|
|
41
|
+
const result = await setSlackChannelConfig(
|
|
42
|
+
body.botToken,
|
|
43
|
+
body.appToken,
|
|
44
|
+
body.userToken,
|
|
45
|
+
);
|
|
38
46
|
const status = result.success ? 200 : 400;
|
|
39
47
|
return Response.json(result, { status });
|
|
40
48
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "../../../../messaging/providers/slack/client.js";
|
|
14
14
|
import type { SlackConversation } from "../../../../messaging/providers/slack/types.js";
|
|
15
15
|
import { getConnectionByProvider } from "../../../../oauth/oauth-store.js";
|
|
16
|
+
import { credentialKey } from "../../../../security/credential-key.js";
|
|
16
17
|
import { getSecureKeyAsync } from "../../../../security/secure-keys.js";
|
|
17
18
|
import { getLogger } from "../../../../util/logger.js";
|
|
18
19
|
import { httpError } from "../../../http-errors.js";
|
|
@@ -25,13 +26,46 @@ const log = getLogger("slack-share");
|
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
|
-
* Resolve
|
|
29
|
+
* Resolve a Slack token for the Share UI, mirroring the read/write auth split
|
|
30
|
+
* in `messaging/providers/slack/adapter.ts`.
|
|
31
|
+
*
|
|
32
|
+
* For Socket Mode installs (tokens stored under `credential/slack_channel/*`),
|
|
33
|
+
* prefer the user OAuth token (xoxp-) for reads when present — this lets the
|
|
34
|
+
* channel picker surface channels the user belongs to but the bot doesn't.
|
|
35
|
+
* Fall back to the bot token (xoxb-) otherwise.
|
|
36
|
+
*
|
|
37
|
+
* Writes MUST always use the bot token so posted messages come from the bot
|
|
38
|
+
* identity, never the user. Passing `user_token` to chat.postMessage would
|
|
39
|
+
* post as the user — unambiguously wrong for Share UI behavior.
|
|
40
|
+
*
|
|
41
|
+
* For legacy OAuth installs (no Socket Mode tokens), fall back to the OAuth
|
|
42
|
+
* connection's access_token, which is the bot token in Slack's OAuth v2 flow.
|
|
29
43
|
*/
|
|
30
|
-
async function resolveSlackToken(
|
|
44
|
+
async function resolveSlackToken(
|
|
45
|
+
mode: "read" | "write",
|
|
46
|
+
): Promise<string | undefined> {
|
|
47
|
+
// Socket Mode path — tokens stored directly in the credential vault.
|
|
48
|
+
const botToken = await getSecureKeyAsync(
|
|
49
|
+
credentialKey("slack_channel", "bot_token"),
|
|
50
|
+
);
|
|
51
|
+
if (botToken) {
|
|
52
|
+
if (mode === "read") {
|
|
53
|
+
const userToken = await getSecureKeyAsync(
|
|
54
|
+
credentialKey("slack_channel", "user_token"),
|
|
55
|
+
);
|
|
56
|
+
return userToken ?? botToken;
|
|
57
|
+
}
|
|
58
|
+
// SAFETY: writes must use the bot token. Using the user token here would
|
|
59
|
+
// post as the user rather than the bot.
|
|
60
|
+
return botToken;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Legacy OAuth path. Slack's OAuth v2 access_token is the bot token; there
|
|
64
|
+
// is no separate user token stored for this install, so reads and writes
|
|
65
|
+
// both use access_token.
|
|
31
66
|
const conn = getConnectionByProvider("slack");
|
|
32
|
-
return
|
|
33
|
-
|
|
34
|
-
: undefined;
|
|
67
|
+
if (!conn) return undefined;
|
|
68
|
+
return await getSecureKeyAsync(`oauth_connection/${conn.id}/access_token`);
|
|
35
69
|
}
|
|
36
70
|
|
|
37
71
|
// ---------------------------------------------------------------------------
|
|
@@ -61,7 +95,9 @@ const TYPE_SORT_ORDER: Record<string, number> = {
|
|
|
61
95
|
};
|
|
62
96
|
|
|
63
97
|
export async function handleListSlackChannels(): Promise<Response> {
|
|
64
|
-
|
|
98
|
+
// Channel enumeration is a read path — prefer user_token when present so
|
|
99
|
+
// the picker surfaces channels the user is in but the bot isn't.
|
|
100
|
+
const token = await resolveSlackToken("read");
|
|
65
101
|
if (!token) {
|
|
66
102
|
return httpError("SERVICE_UNAVAILABLE", "No Slack token configured", 503);
|
|
67
103
|
}
|
|
@@ -137,7 +173,9 @@ export async function handleListSlackChannels(): Promise<Response> {
|
|
|
137
173
|
export async function handleShareToSlackChannel(
|
|
138
174
|
req: Request,
|
|
139
175
|
): Promise<Response> {
|
|
140
|
-
|
|
176
|
+
// Posting a message is a write path — must use the bot token so the message
|
|
177
|
+
// comes from the bot identity, never the user.
|
|
178
|
+
const token = await resolveSlackToken("write");
|
|
141
179
|
if (!token) {
|
|
142
180
|
return httpError("SERVICE_UNAVAILABLE", "No Slack token configured", 503);
|
|
143
181
|
}
|