@vellumai/assistant 0.3.5 → 0.3.7
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/README.md +51 -0
- package/eslint.config.mjs +31 -0
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +4 -1
- package/scripts/ipc/generate-swift.ts +18 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +338 -1
- package/src/__tests__/approval-conversation-turn.test.ts +214 -0
- package/src/__tests__/browser-manager.test.ts +1 -0
- package/src/__tests__/call-conversation-messages.test.ts +130 -0
- package/src/__tests__/call-orchestrator.test.ts +752 -271
- package/src/__tests__/call-pointer-messages.test.ts +148 -0
- package/src/__tests__/call-recovery.test.ts +3 -0
- package/src/__tests__/call-routes-http.test.ts +5 -0
- package/src/__tests__/call-store.test.ts +3 -0
- package/src/__tests__/channel-approval-routes.test.ts +1260 -85
- package/src/__tests__/channel-approval.test.ts +37 -0
- package/src/__tests__/channel-approvals.test.ts +4 -65
- package/src/__tests__/channel-guardian.test.ts +556 -0
- package/src/__tests__/channel-readiness-service.test.ts +74 -7
- package/src/__tests__/checker.test.ts +14 -7
- package/src/__tests__/clarification-resolver.test.ts +44 -24
- package/src/__tests__/commit-message-enrichment-service.test.ts +9 -4
- package/src/__tests__/computer-use-session-working-dir.test.ts +8 -0
- package/src/__tests__/config-schema.test.ts +12 -7
- package/src/__tests__/context-window-manager.test.ts +30 -2
- package/src/__tests__/contradiction-checker.test.ts +20 -5
- package/src/__tests__/credential-security-invariants.test.ts +6 -2
- package/src/__tests__/db-migration-rollback.test.ts +752 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -0
- package/src/__tests__/fuzzy-match-property.test.ts +5 -5
- package/src/__tests__/guardian-action-store.test.ts +123 -0
- package/src/__tests__/guardian-action-sweep.test.ts +277 -0
- package/src/__tests__/guardian-dispatch.test.ts +389 -0
- package/src/__tests__/guardian-question-copy.test.ts +47 -0
- package/src/__tests__/handlers-telegram-config.test.ts +4 -2
- package/src/__tests__/handlers-twilio-config.test.ts +126 -0
- package/src/__tests__/intent-routing.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +228 -1
- package/src/__tests__/memory-upsert-concurrency.test.ts +828 -0
- package/src/__tests__/model-intents.test.ts +96 -0
- package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +42 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +130 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +2 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +89 -13
- package/src/__tests__/provider-error-scenarios.test.ts +621 -0
- package/src/__tests__/provider-fail-open-selection.test.ts +119 -0
- package/src/__tests__/qdrant-manager.test.ts +27 -20
- package/src/__tests__/relay-server.test.ts +779 -40
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +2 -0
- package/src/__tests__/run-orchestrator.test.ts +20 -4
- package/src/__tests__/runtime-runs-http.test.ts +17 -1
- package/src/__tests__/runtime-runs.test.ts +16 -0
- package/src/__tests__/schedule-store.test.ts +18 -4
- package/src/__tests__/scheduler-recurrence.test.ts +13 -4
- package/src/__tests__/session-abort-tool-results.test.ts +6 -0
- package/src/__tests__/session-agent-loop.test.ts +857 -0
- package/src/__tests__/session-conflict-gate.test.ts +6 -0
- package/src/__tests__/session-pre-run-repair.test.ts +6 -0
- package/src/__tests__/session-profile-injection.test.ts +6 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +6 -0
- package/src/__tests__/session-queue.test.ts +6 -0
- package/src/__tests__/session-runtime-assembly.test.ts +237 -13
- package/src/__tests__/session-slash-known.test.ts +6 -0
- package/src/__tests__/session-slash-queue.test.ts +6 -0
- package/src/__tests__/session-slash-unknown.test.ts +6 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +2 -0
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/session-workspace-injection.test.ts +6 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +6 -0
- package/src/__tests__/skills.test.ts +2 -0
- package/src/__tests__/sms-messaging-provider.test.ts +2 -1
- package/src/__tests__/starter-task-flow.test.ts +2 -0
- package/src/__tests__/swarm-dag-pathological.test.ts +535 -0
- package/src/__tests__/system-prompt.test.ts +2 -0
- package/src/__tests__/task-management-tools.test.ts +2 -2
- package/src/__tests__/task-runner.test.ts +14 -4
- package/src/__tests__/terminal-tools.test.ts +25 -19
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +545 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +11 -11
- package/src/__tests__/tool-executor.test.ts +23 -24
- package/src/__tests__/trust-store.test.ts +3 -3
- package/src/__tests__/twilio-rest.test.ts +29 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +3 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +11 -0
- package/src/__tests__/twilio-routes.test.ts +141 -21
- package/src/__tests__/user-reference.test.ts +2 -0
- package/src/__tests__/voice-quality.test.ts +222 -0
- package/src/__tests__/web-search.test.ts +45 -29
- package/src/agent/loop.ts +1 -1
- package/src/agent-heartbeat/agent-heartbeat-service.ts +2 -10
- package/src/amazon/client.ts +1418 -0
- package/src/amazon/request-extractor.ts +135 -0
- package/src/amazon/session.ts +109 -0
- package/src/autonomy/autonomy-store.ts +5 -5
- package/src/browser-extension-relay/client.ts +124 -0
- package/src/browser-extension-relay/protocol.ts +63 -0
- package/src/browser-extension-relay/server.ts +177 -0
- package/src/bundler/app-bundler.ts +3 -3
- package/src/bundler/bundle-signer.ts +1 -1
- package/src/bundler/signature-verifier.ts +1 -1
- package/src/calls/call-conversation-messages.ts +33 -0
- package/src/calls/call-domain.ts +106 -5
- package/src/calls/call-orchestrator.ts +252 -54
- package/src/calls/call-pointer-messages.ts +53 -0
- package/src/calls/call-recovery.ts +3 -8
- package/src/calls/call-store.ts +69 -87
- package/src/calls/elevenlabs-config.ts +3 -2
- package/src/calls/guardian-action-sweep.ts +105 -0
- package/src/calls/guardian-dispatch.ts +203 -0
- package/src/calls/guardian-question-copy.ts +133 -0
- package/src/calls/relay-server.ts +466 -8
- package/src/calls/speaker-identification.ts +1 -1
- package/src/calls/twilio-config.ts +7 -5
- package/src/calls/twilio-provider.ts +6 -4
- package/src/calls/twilio-rest.ts +40 -15
- package/src/calls/twilio-routes.ts +60 -45
- package/src/calls/types.ts +3 -1
- package/src/channels/types.ts +25 -0
- package/src/cli/amazon.ts +815 -0
- package/src/cli/config-commands.ts +2 -2
- package/src/cli/core-commands.ts +4 -3
- package/src/cli/influencer.ts +244 -0
- package/src/cli/map.ts +89 -6
- package/src/cli.ts +1 -1
- package/src/config/agent-schema.ts +171 -0
- package/src/config/bundled-skills/amazon/SKILL.md +127 -0
- package/src/config/bundled-skills/amazon/icon.svg +13 -0
- package/src/config/bundled-skills/api-mapping/SKILL.md +78 -0
- package/src/config/bundled-skills/browser/SKILL.md +1 -0
- package/src/config/bundled-skills/browser/TOOLS.json +17 -0
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +25 -0
- package/src/config/bundled-skills/doordash/SKILL.md +51 -51
- package/src/config/bundled-skills/email-setup/SKILL.md +14 -5
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +183 -0
- package/src/config/bundled-skills/influencer/SKILL.md +144 -0
- package/src/config/bundled-skills/macos-automation/icon.svg +12 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +72 -95
- package/src/config/bundled-skills/media-processing/TOOLS.json +57 -147
- package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +77 -0
- package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +69 -0
- package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +303 -0
- package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +55 -0
- package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +86 -0
- package/src/config/bundled-skills/media-processing/services/gemini-map.ts +339 -0
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +551 -0
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +7 -9
- package/src/config/bundled-skills/media-processing/services/reduce.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +88 -253
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +22 -153
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -2
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +28 -51
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +35 -270
- package/src/config/bundled-skills/messaging/SKILL.md +12 -2
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -7
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +2 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +86 -21
- package/src/config/bundled-skills/twitter/icon.svg +14 -0
- package/src/config/bundled-tool-registry.ts +310 -0
- package/src/config/calls-schema.ts +181 -0
- package/src/config/core-schema.ts +309 -0
- package/src/config/defaults.ts +27 -3
- package/src/config/env-registry.ts +169 -0
- package/src/config/env.ts +175 -0
- package/src/config/loader.ts +6 -6
- package/src/config/memory-schema.ts +528 -0
- package/src/config/sandbox-schema.ts +55 -0
- package/src/config/schema.ts +157 -1138
- package/src/config/skill-state.ts +1 -1
- package/src/config/skills-schema.ts +32 -0
- package/src/config/skills.ts +35 -24
- package/src/config/system-prompt.ts +107 -56
- package/src/config/templates/SOUL.md +1 -1
- package/src/config/types.ts +1 -0
- package/src/config/user-reference.ts +4 -9
- package/src/config/vellum-skills/catalog.json +0 -7
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +5 -1
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +1 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +112 -14
- package/src/context/window-manager.ts +27 -7
- package/src/daemon/approval-generators.ts +186 -0
- package/src/daemon/approved-devices-store.ts +140 -0
- package/src/daemon/assistant-attachments.ts +1 -1
- package/src/daemon/classifier.ts +35 -32
- package/src/daemon/config-watcher.ts +1 -1
- package/src/daemon/daemon-control.ts +254 -0
- package/src/daemon/handlers/apps.ts +2 -3
- package/src/daemon/handlers/config-channels.ts +158 -0
- package/src/daemon/handlers/config-inbox.ts +540 -0
- package/src/daemon/handlers/config-ingress.ts +231 -0
- package/src/daemon/handlers/config-integrations.ts +258 -0
- package/src/daemon/handlers/config-model.ts +143 -0
- package/src/daemon/handlers/config-parental.ts +163 -0
- package/src/daemon/handlers/config-scheduling.ts +172 -0
- package/src/daemon/handlers/config-slack.ts +92 -0
- package/src/daemon/handlers/config-telegram.ts +301 -0
- package/src/daemon/handlers/config-tools.ts +177 -0
- package/src/daemon/handlers/config-trust.ts +104 -0
- package/src/daemon/handlers/config-twilio.ts +1080 -0
- package/src/daemon/handlers/config.ts +53 -2463
- package/src/daemon/handlers/diagnostics.ts +1 -1
- package/src/daemon/handlers/dictation.ts +4 -6
- package/src/daemon/handlers/documents.ts +18 -32
- package/src/daemon/handlers/index.ts +9 -0
- package/src/daemon/handlers/misc.ts +3 -5
- package/src/daemon/handlers/pairing.ts +98 -0
- package/src/daemon/handlers/sessions.ts +74 -5
- package/src/daemon/handlers/shared.ts +3 -1
- package/src/daemon/handlers/skills.ts +1 -1
- package/src/daemon/handlers/twitter-auth.ts +2 -0
- package/src/daemon/handlers/work-items.ts +2 -2
- package/src/daemon/handlers/workspace-files.ts +4 -3
- package/src/daemon/install-cli-launchers.ts +113 -0
- package/src/daemon/ipc-contract/apps.ts +356 -0
- package/src/daemon/ipc-contract/browser.ts +74 -0
- package/src/daemon/ipc-contract/computer-use.ts +151 -0
- package/src/daemon/ipc-contract/diagnostics.ts +56 -0
- package/src/daemon/ipc-contract/documents.ts +74 -0
- package/src/daemon/ipc-contract/inbox.ts +209 -0
- package/src/daemon/ipc-contract/integrations.ts +284 -0
- package/src/daemon/ipc-contract/memory.ts +48 -0
- package/src/daemon/ipc-contract/messages.ts +211 -0
- package/src/daemon/ipc-contract/pairing.ts +45 -0
- package/src/daemon/ipc-contract/parental-control.ts +95 -0
- package/src/daemon/ipc-contract/schedules.ts +97 -0
- package/src/daemon/ipc-contract/sessions.ts +321 -0
- package/src/daemon/ipc-contract/shared.ts +42 -0
- package/src/daemon/ipc-contract/skills.ts +120 -0
- package/src/daemon/ipc-contract/subagents.ts +58 -0
- package/src/daemon/ipc-contract/surfaces.ts +250 -0
- package/src/daemon/ipc-contract/trust.ts +60 -0
- package/src/daemon/ipc-contract/work-items.ts +225 -0
- package/src/daemon/ipc-contract/workspace.ts +113 -0
- package/src/daemon/ipc-contract-inventory.json +62 -0
- package/src/daemon/ipc-contract-inventory.ts +55 -29
- package/src/daemon/ipc-contract.ts +227 -2527
- package/src/daemon/ipc-protocol.ts +1 -1
- package/src/daemon/ipc-validate.ts +7 -0
- package/src/daemon/lifecycle.ts +97 -379
- package/src/daemon/pairing-store.ts +177 -0
- package/src/daemon/providers-setup.ts +43 -0
- package/src/daemon/ride-shotgun-handler.ts +67 -2
- package/src/daemon/server.ts +60 -44
- package/src/daemon/session-agent-loop-handlers.ts +421 -0
- package/src/daemon/session-agent-loop.ts +113 -275
- package/src/daemon/session-dynamic-profile.ts +1 -1
- package/src/daemon/session-history.ts +1 -1
- package/src/daemon/session-media-retry.ts +1 -1
- package/src/daemon/session-messaging.ts +37 -2
- package/src/daemon/session-notifiers.ts +5 -25
- package/src/daemon/session-process.ts +99 -59
- package/src/daemon/session-queue-manager.ts +98 -4
- package/src/daemon/session-runtime-assembly.ts +149 -15
- package/src/daemon/session-surfaces.ts +26 -4
- package/src/daemon/session-tool-setup.ts +28 -30
- package/src/daemon/session-workspace.ts +1 -1
- package/src/daemon/session.ts +24 -1
- package/src/daemon/shutdown-handlers.ts +122 -0
- package/src/daemon/trace-emitter.ts +1 -1
- package/src/daemon/watch-handler.ts +36 -33
- package/src/doordash/cart-queries.ts +787 -0
- package/src/doordash/client.ts +144 -127
- package/src/doordash/order-queries.ts +85 -0
- package/src/doordash/queries.ts +10 -1308
- package/src/doordash/search-queries.ts +203 -0
- package/src/doordash/session.ts +3 -2
- package/src/doordash/store-queries.ts +246 -0
- package/src/doordash/types.ts +367 -0
- package/src/email/providers/agentmail.ts +2 -1
- package/src/email/providers/index.ts +3 -2
- package/src/email/service.ts +3 -2
- package/src/errors.ts +43 -0
- package/src/home-base/prebuilt/seed.ts +1 -1
- package/src/hooks/cli.ts +6 -5
- package/src/hooks/config.ts +6 -8
- package/src/hooks/discovery.ts +6 -5
- package/src/hooks/manager.ts +4 -3
- package/src/hooks/runner.ts +2 -2
- package/src/hooks/templates.ts +5 -5
- package/src/inbound/public-ingress-urls.ts +3 -1
- package/src/index.ts +4 -2
- package/src/influencer/client.ts +1104 -0
- package/src/instrument.ts +4 -3
- package/src/logfire.ts +4 -3
- package/src/memory/admin.ts +25 -35
- package/src/memory/attachments-store.ts +4 -7
- package/src/memory/channel-delivery-store.ts +30 -1
- package/src/memory/channel-guardian-store.ts +200 -1
- package/src/memory/clarification-resolver.ts +37 -33
- package/src/memory/conflict-store.ts +67 -61
- package/src/memory/contradiction-checker.ts +141 -117
- package/src/memory/conversation-store.ts +335 -51
- package/src/memory/db-connection.ts +27 -4
- package/src/memory/db-init.ts +121 -4
- package/src/memory/db.ts +14 -1
- package/src/memory/embedding-backend.ts +27 -5
- package/src/memory/embedding-ollama.ts +2 -1
- package/src/memory/entity-extractor.ts +38 -35
- package/src/memory/guardian-action-store.ts +430 -0
- package/src/memory/inbox-escalation-projection.ts +59 -0
- package/src/memory/inbox-thread-store.ts +218 -0
- package/src/memory/ingress-invite-store.ts +338 -0
- package/src/memory/ingress-member-store.ts +350 -0
- package/src/memory/items-extractor.ts +91 -97
- package/src/memory/job-handlers/index-maintenance.ts +3 -3
- package/src/memory/job-handlers/media-processing.ts +11 -42
- package/src/memory/job-handlers/summarization.ts +32 -26
- package/src/memory/job-utils.ts +3 -10
- package/src/memory/jobs-store.ts +6 -9
- package/src/memory/jobs-worker.ts +51 -36
- package/src/memory/migrations/001-job-deferrals.ts +45 -0
- package/src/memory/migrations/002-tool-invocations-fk.ts +43 -0
- package/src/memory/migrations/003-memory-fts-backfill.ts +24 -0
- package/src/memory/migrations/004-entity-relation-dedup.ts +87 -0
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +80 -0
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +62 -0
- package/src/memory/migrations/007-assistant-id-to-self.ts +254 -0
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +208 -0
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +83 -0
- package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +56 -0
- package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +63 -0
- package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +19 -0
- package/src/memory/migrations/013-guardian-action-tables.ts +68 -0
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +76 -0
- package/src/memory/migrations/015-drop-active-search-index.ts +27 -0
- package/src/memory/migrations/016-memory-segments-indexes.ts +11 -0
- package/src/memory/migrations/017-memory-items-indexes.ts +12 -0
- package/src/memory/migrations/018-remaining-table-indexes.ts +13 -0
- package/src/memory/migrations/index.ts +24 -0
- package/src/memory/migrations/registry.ts +79 -0
- package/src/memory/migrations/validate-migration-state.ts +69 -0
- package/src/memory/qdrant-manager.ts +49 -8
- package/src/memory/query-builder.ts +1 -1
- package/src/memory/raw-query.ts +119 -0
- package/src/memory/recall-cache.ts +4 -1
- package/src/memory/retriever.ts +163 -47
- package/src/memory/schema-migration.ts +25 -984
- package/src/memory/schema.ts +130 -7
- package/src/memory/search/entity.ts +10 -19
- package/src/memory/search/lexical.ts +81 -52
- package/src/memory/search/ranking.ts +21 -22
- package/src/memory/search/semantic.ts +157 -19
- package/src/memory/shared-app-links-store.ts +4 -5
- package/src/memory/validation.ts +19 -0
- package/src/messaging/draft-store.ts +5 -6
- package/src/messaging/providers/sms/adapter.ts +3 -6
- package/src/messaging/providers/telegram-bot/adapter.ts +2 -5
- package/src/messaging/providers/whatsapp/adapter.ts +136 -0
- package/src/messaging/providers/whatsapp/client.ts +67 -0
- package/src/messaging/style-analyzer.ts +5 -4
- package/src/messaging/thread-summarizer.ts +61 -69
- package/src/messaging/triage-engine.ts +62 -71
- package/src/migrations/config-merge.ts +53 -0
- package/src/migrations/data-layout.ts +68 -0
- package/src/migrations/data-merge.ts +33 -0
- package/src/migrations/hooks-merge.ts +90 -0
- package/src/migrations/index.ts +6 -0
- package/src/migrations/log.ts +23 -0
- package/src/migrations/skills-merge.ts +33 -0
- package/src/migrations/workspace-layout.ts +79 -0
- package/src/permissions/checker.ts +126 -11
- package/src/permissions/prompter.ts +14 -0
- package/src/permissions/shell-identity.ts +31 -1
- package/src/permissions/trust-store.ts +21 -1
- package/src/providers/anthropic/client.ts +4 -4
- package/src/providers/failover.ts +2 -2
- package/src/providers/model-intents.ts +70 -0
- package/src/providers/ollama/client.ts +2 -1
- package/src/providers/provider-send-message.ts +176 -0
- package/src/providers/registry.ts +71 -30
- package/src/providers/retry.ts +35 -1
- package/src/providers/types.ts +12 -1
- package/src/runtime/approval-conversation-turn.ts +97 -0
- package/src/runtime/approval-message-composer.ts +115 -5
- package/src/runtime/assistant-event-hub.ts +3 -1
- package/src/runtime/channel-approval-parser.ts +36 -2
- package/src/runtime/channel-approvals.ts +0 -21
- package/src/runtime/channel-guardian-service.ts +48 -7
- package/src/runtime/channel-readiness-service.ts +160 -34
- package/src/runtime/channel-readiness-types.ts +10 -4
- package/src/runtime/channel-retry-sweep.ts +184 -0
- package/src/runtime/guardian-context-resolver.ts +108 -0
- package/src/runtime/http-server.ts +289 -745
- package/src/runtime/http-types.ts +56 -3
- package/src/runtime/middleware/auth.ts +116 -0
- package/src/runtime/middleware/error-handler.ts +33 -0
- package/src/runtime/middleware/twilio-validation.ts +127 -0
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/call-routes.ts +49 -6
- package/src/runtime/routes/channel-delivery-routes.ts +170 -0
- package/src/runtime/routes/channel-guardian-routes.ts +1191 -0
- package/src/runtime/routes/channel-inbound-routes.ts +1152 -0
- package/src/runtime/routes/channel-route-shared.ts +144 -0
- package/src/runtime/routes/channel-routes.ts +32 -1634
- package/src/runtime/routes/conversation-routes.ts +50 -7
- package/src/runtime/routes/events-routes.ts +2 -2
- package/src/runtime/routes/identity-routes.ts +126 -0
- package/src/runtime/routes/pairing-routes.ts +144 -0
- package/src/runtime/routes/run-routes.ts +15 -1
- package/src/runtime/run-orchestrator.ts +52 -34
- package/src/schedule/schedule-store.ts +36 -32
- package/src/schedule/scheduler.ts +3 -3
- package/src/security/encrypted-store.ts +5 -7
- package/src/security/oauth2.ts +45 -15
- package/src/security/parental-control-store.ts +183 -0
- package/src/security/secret-allowlist.ts +4 -3
- package/src/security/secret-scanner.ts +5 -5
- package/src/security/secure-keys.ts +1 -1
- package/src/security/token-manager.ts +3 -2
- package/src/services/vercel-deploy.ts +6 -2
- package/src/skills/tool-manifest.ts +3 -3
- package/src/skills/vellum-catalog-remote.ts +75 -16
- package/src/slack/slack-webhook.ts +2 -1
- package/src/swarm/orchestrator.ts +92 -1
- package/src/swarm/router-planner.ts +6 -9
- package/src/swarm/worker-prompts.ts +9 -12
- package/src/tasks/task-compiler.ts +19 -28
- package/src/tasks/task-runner.ts +1 -1
- package/src/tools/assets/search.ts +15 -14
- package/src/tools/browser/__tests__/auth-detector.test.ts +1 -0
- package/src/tools/browser/auto-navigate.ts +1 -0
- package/src/tools/browser/browser-execution.ts +13 -1
- package/src/tools/browser/browser-manager.ts +119 -4
- package/src/tools/browser/network-recorder.ts +5 -0
- package/src/tools/credentials/broker.ts +11 -2
- package/src/tools/credentials/metadata-store.ts +18 -14
- package/src/tools/credentials/post-connect-hooks.ts +61 -0
- package/src/tools/credentials/vault.ts +49 -23
- package/src/tools/executor.ts +80 -18
- package/src/tools/host-terminal/cli-discover.ts +1 -1
- package/src/tools/network/script-proxy/http-forwarder.ts +1 -1
- package/src/tools/network/script-proxy/mitm-handler.ts +1 -1
- package/src/tools/network/script-proxy/server.ts +1 -1
- package/src/tools/network/script-proxy/session-manager.ts +6 -5
- package/src/tools/network/web-fetch.ts +18 -2
- package/src/tools/network/web-search.ts +7 -3
- package/src/tools/reminder/reminder-store.ts +14 -15
- package/src/tools/schedule/create.ts +1 -0
- package/src/tools/schedule/list.ts +2 -1
- package/src/tools/shared/filesystem/file-ops-service.ts +5 -7
- package/src/tools/skills/skill-script-runner.ts +24 -9
- package/src/tools/skills/skill-tool-factory.ts +1 -0
- package/src/tools/tasks/work-item-enqueue.ts +2 -2
- package/src/tools/terminal/evaluate-typescript.ts +21 -12
- package/src/tools/terminal/parser.ts +50 -0
- package/src/tools/watcher/delete.ts +6 -0
- package/src/tools/weather/service.ts +1 -1
- package/src/twitter/client.ts +190 -24
- package/src/twitter/session.ts +4 -3
- package/src/util/clipboard.ts +1 -1
- package/src/util/errors.ts +65 -8
- package/src/util/fs.ts +40 -0
- package/src/util/json.ts +10 -0
- package/src/util/log-redact.ts +189 -0
- package/src/util/logger.ts +25 -18
- package/src/util/object.ts +3 -0
- package/src/util/platform.ts +72 -365
- package/src/util/pricing.ts +1 -1
- package/src/util/promise-guard.ts +1 -1
- package/src/util/retry.ts +19 -0
- package/src/util/row-mapper.ts +79 -0
- package/src/util/silently.ts +21 -0
- package/src/watcher/engine.ts +5 -1
- package/src/watcher/provider-types.ts +20 -0
- package/src/watcher/providers/github.ts +156 -0
- package/src/watcher/providers/gmail.ts +1 -0
- package/src/watcher/providers/google-calendar.ts +1 -0
- package/src/watcher/providers/linear.ts +460 -0
- package/src/watcher/providers/slack.ts +1 -0
- package/src/work-items/work-item-runner.ts +1 -1
- package/src/workspace/git-service.ts +1 -1
- package/src/workspace/provider-commit-message-generator.ts +51 -22
- package/src/__tests__/call-bridge.test.ts +0 -517
- package/src/__tests__/session-process-bridge.test.ts +0 -244
- package/src/calls/call-bridge.ts +0 -168
- package/src/config/bundled-skills/media-processing/services/capability-registry.ts +0 -137
- package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +0 -280
- package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +0 -144
- package/src/config/bundled-skills/media-processing/services/feedback-store.ts +0 -136
- package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +0 -95
- package/src/config/bundled-skills/media-processing/services/timeline-service.ts +0 -267
- package/src/config/bundled-skills/media-processing/tools/detect-events.ts +0 -110
- package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +0 -235
- package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +0 -142
- package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +0 -150
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +0 -199
|
@@ -1,2467 +1,57 @@
|
|
|
1
|
-
import * as net from 'node:net';
|
|
2
|
-
import { getConfig, loadRawConfig, saveRawConfig } from '../../config/loader.js';
|
|
3
|
-
import { initializeProviders } from '../../providers/registry.js';
|
|
4
|
-
import { addRule, removeRule, updateRule, getAllRules, acceptStarterBundle } from '../../permissions/trust-store.js';
|
|
5
|
-
import { classifyRisk, check, generateAllowlistOptions, generateScopeOptions } from '../../permissions/checker.js';
|
|
6
|
-
import { isSideEffectTool } from '../../tools/executor.js';
|
|
7
|
-
import { resolveExecutionTarget, type ManifestOverride } from '../../tools/execution-target.js';
|
|
8
|
-
import { getAllTools, getTool } from '../../tools/registry.js';
|
|
9
|
-
import { loadSkillCatalog } from '../../config/skills.js';
|
|
10
|
-
import { parseToolManifestFile } from '../../skills/tool-manifest.js';
|
|
11
|
-
import { join } from 'node:path';
|
|
12
|
-
import { listSchedules, updateSchedule, deleteSchedule, describeCronExpression } from '../../schedule/schedule-store.js';
|
|
13
|
-
import { listReminders, cancelReminder } from '../../tools/reminder/reminder-store.js';
|
|
14
|
-
import { getSecureKey, setSecureKey, deleteSecureKey } from '../../security/secure-keys.js';
|
|
15
|
-
import { upsertCredentialMetadata, deleteCredentialMetadata, getCredentialMetadata } from '../../tools/credentials/metadata-store.js';
|
|
16
|
-
import { postToSlackWebhook } from '../../slack/slack-webhook.js';
|
|
17
|
-
import { getApp } from '../../memory/app-store.js';
|
|
18
|
-
import * as externalConversationStore from '../../memory/external-conversation-store.js';
|
|
19
|
-
import { readHttpToken } from '../../util/platform.js';
|
|
20
|
-
import type {
|
|
21
|
-
ModelSetRequest,
|
|
22
|
-
ImageGenModelSetRequest,
|
|
23
|
-
AddTrustRule,
|
|
24
|
-
RemoveTrustRule,
|
|
25
|
-
UpdateTrustRule,
|
|
26
|
-
ScheduleToggle,
|
|
27
|
-
ScheduleRemove,
|
|
28
|
-
ReminderCancel,
|
|
29
|
-
ShareToSlackRequest,
|
|
30
|
-
SlackWebhookConfigRequest,
|
|
31
|
-
IngressConfigRequest,
|
|
32
|
-
VercelApiConfigRequest,
|
|
33
|
-
TwitterIntegrationConfigRequest,
|
|
34
|
-
TelegramConfigRequest,
|
|
35
|
-
TwilioConfigRequest,
|
|
36
|
-
ChannelReadinessRequest,
|
|
37
|
-
GuardianVerificationRequest,
|
|
38
|
-
ToolPermissionSimulateRequest,
|
|
39
|
-
} from '../ipc-protocol.js';
|
|
40
|
-
import {
|
|
41
|
-
hasTwilioCredentials,
|
|
42
|
-
listIncomingPhoneNumbers,
|
|
43
|
-
searchAvailableNumbers,
|
|
44
|
-
provisionPhoneNumber,
|
|
45
|
-
updatePhoneNumberWebhooks,
|
|
46
|
-
getTollFreeVerificationStatus,
|
|
47
|
-
submitTollFreeVerification,
|
|
48
|
-
updateTollFreeVerification,
|
|
49
|
-
deleteTollFreeVerification,
|
|
50
|
-
getPhoneNumberSid,
|
|
51
|
-
releasePhoneNumber,
|
|
52
|
-
fetchMessageStatus,
|
|
53
|
-
type TollFreeVerificationSubmitParams,
|
|
54
|
-
} from '../../calls/twilio-rest.js';
|
|
55
|
-
import {
|
|
56
|
-
getTwilioVoiceWebhookUrl,
|
|
57
|
-
getTwilioStatusCallbackUrl,
|
|
58
|
-
getTwilioSmsWebhookUrl,
|
|
59
|
-
type IngressConfig,
|
|
60
|
-
} from '../../inbound/public-ingress-urls.js';
|
|
61
|
-
import { createVerificationChallenge, getGuardianBinding, revokeBinding as revokeGuardianBinding } from '../../runtime/channel-guardian-service.js';
|
|
62
|
-
import { createReadinessService, type ChannelReadinessService } from '../../runtime/channel-readiness-service.js';
|
|
63
|
-
import { log, CONFIG_RELOAD_DEBOUNCE_MS, defineHandlers, type HandlerContext } from './shared.js';
|
|
64
|
-
import { MODEL_TO_PROVIDER } from '../session-slash.js';
|
|
65
|
-
|
|
66
|
-
// Lazily capture the env-provided INGRESS_PUBLIC_BASE_URL on first access
|
|
67
|
-
// rather than at module load time. The daemon loads ~/.vellum/.env inside
|
|
68
|
-
// runDaemon() (see lifecycle.ts), which runs AFTER static ES module imports
|
|
69
|
-
// resolve. A module-level snapshot would miss dotenv-provided values.
|
|
70
|
-
let _originalIngressEnvCaptured = false;
|
|
71
|
-
let _originalIngressEnv: string | undefined;
|
|
72
|
-
function getOriginalIngressEnv(): string | undefined {
|
|
73
|
-
if (!_originalIngressEnvCaptured) {
|
|
74
|
-
_originalIngressEnv = process.env.INGRESS_PUBLIC_BASE_URL;
|
|
75
|
-
_originalIngressEnvCaptured = true;
|
|
76
|
-
}
|
|
77
|
-
return _originalIngressEnv;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const TELEGRAM_BOT_TOKEN_IN_URL_PATTERN = /\/bot\d{8,10}:[A-Za-z0-9_-]{30,120}\//g;
|
|
81
|
-
const TELEGRAM_BOT_TOKEN_PATTERN = /(?<![A-Za-z0-9_])\d{8,10}:[A-Za-z0-9_-]{30,120}(?![A-Za-z0-9_])/g;
|
|
82
|
-
|
|
83
|
-
function redactTelegramBotTokens(value: string): string {
|
|
84
|
-
return value
|
|
85
|
-
.replace(TELEGRAM_BOT_TOKEN_IN_URL_PATTERN, '/bot[REDACTED]/')
|
|
86
|
-
.replace(TELEGRAM_BOT_TOKEN_PATTERN, '[REDACTED]');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function summarizeTelegramError(err: unknown): string {
|
|
90
|
-
const parts: string[] = [];
|
|
91
|
-
if (err instanceof Error) {
|
|
92
|
-
parts.push(err.message);
|
|
93
|
-
} else {
|
|
94
|
-
parts.push(String(err));
|
|
95
|
-
}
|
|
96
|
-
const path = (err as { path?: unknown })?.path;
|
|
97
|
-
if (typeof path === 'string' && path.length > 0) {
|
|
98
|
-
parts.push(`path=${path}`);
|
|
99
|
-
}
|
|
100
|
-
const code = (err as { code?: unknown })?.code;
|
|
101
|
-
if (typeof code === 'string' && code.length > 0) {
|
|
102
|
-
parts.push(`code=${code}`);
|
|
103
|
-
}
|
|
104
|
-
return redactTelegramBotTokens(parts.join(' '));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function handleModelGet(socket: net.Socket, ctx: HandlerContext): void {
|
|
108
|
-
const config = getConfig();
|
|
109
|
-
const configured = Object.keys(config.apiKeys).filter((k) => !!config.apiKeys[k]);
|
|
110
|
-
if (!configured.includes('ollama')) configured.push('ollama');
|
|
111
|
-
ctx.send(socket, {
|
|
112
|
-
type: 'model_info',
|
|
113
|
-
model: config.model,
|
|
114
|
-
provider: config.provider,
|
|
115
|
-
configuredProviders: configured,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function handleModelSet(
|
|
120
|
-
msg: ModelSetRequest,
|
|
121
|
-
socket: net.Socket,
|
|
122
|
-
ctx: HandlerContext,
|
|
123
|
-
): void {
|
|
124
|
-
try {
|
|
125
|
-
// If the requested model is already the current model AND the provider
|
|
126
|
-
// is already aligned with what MODEL_TO_PROVIDER expects, skip expensive
|
|
127
|
-
// reinitialization but still send model_info so the client confirms.
|
|
128
|
-
// If the provider has drifted (e.g. manual config edit), fall through
|
|
129
|
-
// so the full reinit path can repair it.
|
|
130
|
-
{
|
|
131
|
-
const current = getConfig();
|
|
132
|
-
const expectedProvider = MODEL_TO_PROVIDER[msg.model];
|
|
133
|
-
const providerAligned = !expectedProvider || current.provider === expectedProvider;
|
|
134
|
-
if (msg.model === current.model && providerAligned) {
|
|
135
|
-
const configured = Object.keys(current.apiKeys).filter((k) => !!current.apiKeys[k]);
|
|
136
|
-
if (!configured.includes('ollama')) configured.push('ollama');
|
|
137
|
-
ctx.send(socket, {
|
|
138
|
-
type: 'model_info',
|
|
139
|
-
model: current.model,
|
|
140
|
-
provider: current.provider,
|
|
141
|
-
configuredProviders: configured,
|
|
142
|
-
});
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Validate API key before switching
|
|
148
|
-
const provider = MODEL_TO_PROVIDER[msg.model];
|
|
149
|
-
if (provider && provider !== 'ollama') {
|
|
150
|
-
const currentConfig = getConfig();
|
|
151
|
-
if (!currentConfig.apiKeys[provider]) {
|
|
152
|
-
// Send current model_info so the client resyncs its optimistic state
|
|
153
|
-
// (don't use generic 'error' type — it would interrupt in-flight chat)
|
|
154
|
-
const configured = Object.keys(currentConfig.apiKeys).filter((k) => !!currentConfig.apiKeys[k]);
|
|
155
|
-
if (!configured.includes('ollama')) configured.push('ollama');
|
|
156
|
-
ctx.send(socket, { type: 'model_info', model: currentConfig.model, provider: currentConfig.provider, configuredProviders: configured });
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Use raw config to avoid persisting env-var API keys to disk
|
|
162
|
-
const raw = loadRawConfig();
|
|
163
|
-
raw.model = msg.model;
|
|
164
|
-
// Infer provider from model ID to keep provider and model in sync
|
|
165
|
-
raw.provider = provider ?? raw.provider;
|
|
166
|
-
|
|
167
|
-
// Suppress the file watcher callback — handleModelSet already does
|
|
168
|
-
// the full reload sequence; a redundant watcher-triggered reload
|
|
169
|
-
// would incorrectly evict sessions created after this method returns.
|
|
170
|
-
const wasSuppressed = ctx.suppressConfigReload;
|
|
171
|
-
ctx.setSuppressConfigReload(true);
|
|
172
|
-
try {
|
|
173
|
-
saveRawConfig(raw);
|
|
174
|
-
} catch (err) {
|
|
175
|
-
ctx.setSuppressConfigReload(wasSuppressed);
|
|
176
|
-
throw err;
|
|
177
|
-
}
|
|
178
|
-
ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
179
|
-
|
|
180
|
-
// Re-initialize provider with the new model so LLM calls use it
|
|
181
|
-
const config = getConfig();
|
|
182
|
-
initializeProviders(config);
|
|
183
|
-
|
|
184
|
-
// Evict idle sessions immediately; mark busy ones as stale so they
|
|
185
|
-
// get recreated with the new provider once they finish processing.
|
|
186
|
-
for (const [id, session] of ctx.sessions) {
|
|
187
|
-
if (!session.isProcessing()) {
|
|
188
|
-
session.dispose();
|
|
189
|
-
ctx.sessions.delete(id);
|
|
190
|
-
} else {
|
|
191
|
-
session.markStale();
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
ctx.updateConfigFingerprint();
|
|
196
|
-
|
|
197
|
-
ctx.send(socket, {
|
|
198
|
-
type: 'model_info',
|
|
199
|
-
model: config.model,
|
|
200
|
-
provider: config.provider,
|
|
201
|
-
});
|
|
202
|
-
} catch (err) {
|
|
203
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
204
|
-
ctx.send(socket, { type: 'error', message: `Failed to set model: ${message}` });
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export function handleImageGenModelSet(
|
|
209
|
-
msg: ImageGenModelSetRequest,
|
|
210
|
-
_socket: net.Socket,
|
|
211
|
-
ctx: HandlerContext,
|
|
212
|
-
): void {
|
|
213
|
-
try {
|
|
214
|
-
const raw = loadRawConfig();
|
|
215
|
-
raw.imageGenModel = msg.model;
|
|
216
|
-
|
|
217
|
-
const wasSuppressed = ctx.suppressConfigReload;
|
|
218
|
-
ctx.setSuppressConfigReload(true);
|
|
219
|
-
try {
|
|
220
|
-
saveRawConfig(raw);
|
|
221
|
-
} catch (err) {
|
|
222
|
-
ctx.setSuppressConfigReload(wasSuppressed);
|
|
223
|
-
throw err;
|
|
224
|
-
}
|
|
225
|
-
ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
226
|
-
|
|
227
|
-
ctx.updateConfigFingerprint();
|
|
228
|
-
log.info({ model: msg.model }, 'Image generation model updated');
|
|
229
|
-
} catch (err) {
|
|
230
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
231
|
-
log.error({ err }, `Failed to set image gen model: ${message}`);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export function handleAddTrustRule(
|
|
236
|
-
msg: AddTrustRule,
|
|
237
|
-
_socket: net.Socket,
|
|
238
|
-
_ctx: HandlerContext,
|
|
239
|
-
): void {
|
|
240
|
-
try {
|
|
241
|
-
const hasMetadata = msg.allowHighRisk != null
|
|
242
|
-
|| msg.executionTarget != null;
|
|
243
|
-
|
|
244
|
-
addRule(
|
|
245
|
-
msg.toolName,
|
|
246
|
-
msg.pattern,
|
|
247
|
-
msg.scope,
|
|
248
|
-
msg.decision,
|
|
249
|
-
undefined, // priority — use default
|
|
250
|
-
hasMetadata
|
|
251
|
-
? {
|
|
252
|
-
allowHighRisk: msg.allowHighRisk,
|
|
253
|
-
executionTarget: msg.executionTarget,
|
|
254
|
-
}
|
|
255
|
-
: undefined,
|
|
256
|
-
);
|
|
257
|
-
log.info({ toolName: msg.toolName, pattern: msg.pattern, scope: msg.scope, decision: msg.decision }, 'Trust rule added via client');
|
|
258
|
-
} catch (err) {
|
|
259
|
-
log.error({ err, toolName: msg.toolName, pattern: msg.pattern, scope: msg.scope }, 'Failed to add trust rule via client');
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
export function handleTrustRulesList(socket: net.Socket, ctx: HandlerContext): void {
|
|
264
|
-
const rules = getAllRules();
|
|
265
|
-
ctx.send(socket, { type: 'trust_rules_list_response', rules });
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export function handleRemoveTrustRule(
|
|
269
|
-
msg: RemoveTrustRule,
|
|
270
|
-
_socket: net.Socket,
|
|
271
|
-
_ctx: HandlerContext,
|
|
272
|
-
): void {
|
|
273
|
-
try {
|
|
274
|
-
const removed = removeRule(msg.id);
|
|
275
|
-
if (!removed) {
|
|
276
|
-
log.warn({ id: msg.id }, 'Trust rule not found for removal');
|
|
277
|
-
} else {
|
|
278
|
-
log.info({ id: msg.id }, 'Trust rule removed via client');
|
|
279
|
-
}
|
|
280
|
-
} catch (err) {
|
|
281
|
-
log.error({ err }, 'Failed to remove trust rule');
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
export function handleUpdateTrustRule(
|
|
286
|
-
msg: UpdateTrustRule,
|
|
287
|
-
_socket: net.Socket,
|
|
288
|
-
_ctx: HandlerContext,
|
|
289
|
-
): void {
|
|
290
|
-
try {
|
|
291
|
-
updateRule(msg.id, {
|
|
292
|
-
tool: msg.tool,
|
|
293
|
-
pattern: msg.pattern,
|
|
294
|
-
scope: msg.scope,
|
|
295
|
-
decision: msg.decision,
|
|
296
|
-
priority: msg.priority,
|
|
297
|
-
});
|
|
298
|
-
log.info({ id: msg.id }, 'Trust rule updated via client');
|
|
299
|
-
} catch (err) {
|
|
300
|
-
log.error({ err }, 'Failed to update trust rule');
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
export function handleAcceptStarterBundle(
|
|
305
|
-
socket: net.Socket,
|
|
306
|
-
ctx: HandlerContext,
|
|
307
|
-
): void {
|
|
308
|
-
try {
|
|
309
|
-
const result = acceptStarterBundle();
|
|
310
|
-
ctx.send(socket, {
|
|
311
|
-
type: 'accept_starter_bundle_response',
|
|
312
|
-
accepted: result.accepted,
|
|
313
|
-
rulesAdded: result.rulesAdded,
|
|
314
|
-
alreadyAccepted: result.alreadyAccepted,
|
|
315
|
-
});
|
|
316
|
-
log.info({ rulesAdded: result.rulesAdded, alreadyAccepted: result.alreadyAccepted }, 'Starter bundle accepted via client');
|
|
317
|
-
} catch (err) {
|
|
318
|
-
log.error({ err }, 'Failed to accept starter bundle');
|
|
319
|
-
ctx.send(socket, { type: 'error', message: 'Failed to accept starter bundle' });
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
export function handleSchedulesList(socket: net.Socket, ctx: HandlerContext): void {
|
|
324
|
-
const jobs = listSchedules();
|
|
325
|
-
ctx.send(socket, {
|
|
326
|
-
type: 'schedules_list_response',
|
|
327
|
-
schedules: jobs.map((j) => ({
|
|
328
|
-
id: j.id,
|
|
329
|
-
name: j.name,
|
|
330
|
-
enabled: j.enabled,
|
|
331
|
-
syntax: j.syntax,
|
|
332
|
-
expression: j.expression,
|
|
333
|
-
cronExpression: j.cronExpression,
|
|
334
|
-
timezone: j.timezone,
|
|
335
|
-
message: j.message,
|
|
336
|
-
nextRunAt: j.nextRunAt,
|
|
337
|
-
lastRunAt: j.lastRunAt,
|
|
338
|
-
lastStatus: j.lastStatus,
|
|
339
|
-
description: j.syntax === 'cron' ? describeCronExpression(j.cronExpression) : j.expression,
|
|
340
|
-
})),
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
export function handleScheduleToggle(
|
|
345
|
-
msg: ScheduleToggle,
|
|
346
|
-
socket: net.Socket,
|
|
347
|
-
ctx: HandlerContext,
|
|
348
|
-
): void {
|
|
349
|
-
try {
|
|
350
|
-
updateSchedule(msg.id, { enabled: msg.enabled });
|
|
351
|
-
log.info({ id: msg.id, enabled: msg.enabled }, 'Schedule toggled via client');
|
|
352
|
-
} catch (err) {
|
|
353
|
-
log.error({ err }, 'Failed to toggle schedule');
|
|
354
|
-
}
|
|
355
|
-
handleSchedulesList(socket, ctx);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
export function handleScheduleRemove(
|
|
359
|
-
msg: ScheduleRemove,
|
|
360
|
-
socket: net.Socket,
|
|
361
|
-
ctx: HandlerContext,
|
|
362
|
-
): void {
|
|
363
|
-
try {
|
|
364
|
-
const removed = deleteSchedule(msg.id);
|
|
365
|
-
if (!removed) {
|
|
366
|
-
log.warn({ id: msg.id }, 'Schedule not found for removal');
|
|
367
|
-
} else {
|
|
368
|
-
log.info({ id: msg.id }, 'Schedule removed via client');
|
|
369
|
-
}
|
|
370
|
-
} catch (err) {
|
|
371
|
-
log.error({ err }, 'Failed to remove schedule');
|
|
372
|
-
}
|
|
373
|
-
handleSchedulesList(socket, ctx);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
export function handleRemindersList(socket: net.Socket, ctx: HandlerContext): void {
|
|
377
|
-
const items = listReminders();
|
|
378
|
-
ctx.send(socket, {
|
|
379
|
-
type: 'reminders_list_response',
|
|
380
|
-
reminders: items.map((r) => ({
|
|
381
|
-
id: r.id,
|
|
382
|
-
label: r.label,
|
|
383
|
-
message: r.message,
|
|
384
|
-
fireAt: r.fireAt,
|
|
385
|
-
mode: r.mode,
|
|
386
|
-
status: r.status,
|
|
387
|
-
firedAt: r.firedAt,
|
|
388
|
-
createdAt: r.createdAt,
|
|
389
|
-
})),
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
export function handleReminderCancel(
|
|
394
|
-
msg: ReminderCancel,
|
|
395
|
-
socket: net.Socket,
|
|
396
|
-
ctx: HandlerContext,
|
|
397
|
-
): void {
|
|
398
|
-
try {
|
|
399
|
-
const cancelled = cancelReminder(msg.id);
|
|
400
|
-
if (!cancelled) {
|
|
401
|
-
log.warn({ id: msg.id }, 'Reminder not found or already fired/cancelled');
|
|
402
|
-
} else {
|
|
403
|
-
log.info({ id: msg.id }, 'Reminder cancelled via client');
|
|
404
|
-
}
|
|
405
|
-
} catch (err) {
|
|
406
|
-
log.error({ err }, 'Failed to cancel reminder');
|
|
407
|
-
}
|
|
408
|
-
handleRemindersList(socket, ctx);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
export async function handleShareToSlack(
|
|
412
|
-
msg: ShareToSlackRequest,
|
|
413
|
-
socket: net.Socket,
|
|
414
|
-
ctx: HandlerContext,
|
|
415
|
-
): Promise<void> {
|
|
416
|
-
try {
|
|
417
|
-
const config = loadRawConfig();
|
|
418
|
-
const webhookUrl = config.slackWebhookUrl as string | undefined;
|
|
419
|
-
if (!webhookUrl) {
|
|
420
|
-
ctx.send(socket, {
|
|
421
|
-
type: 'share_to_slack_response',
|
|
422
|
-
success: false,
|
|
423
|
-
error: 'No Slack webhook URL configured. Provide one here in the chat, or set it from the Settings page.',
|
|
424
|
-
});
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const app = getApp(msg.appId);
|
|
429
|
-
if (!app) {
|
|
430
|
-
ctx.send(socket, {
|
|
431
|
-
type: 'share_to_slack_response',
|
|
432
|
-
success: false,
|
|
433
|
-
error: `App not found: ${msg.appId}`,
|
|
434
|
-
});
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
await postToSlackWebhook(
|
|
439
|
-
webhookUrl,
|
|
440
|
-
app.name,
|
|
441
|
-
app.description ?? '',
|
|
442
|
-
'\u{1F4F1}',
|
|
443
|
-
);
|
|
444
|
-
|
|
445
|
-
ctx.send(socket, { type: 'share_to_slack_response', success: true });
|
|
446
|
-
} catch (err) {
|
|
447
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
448
|
-
log.error({ err, appId: msg.appId }, 'Failed to share app to Slack');
|
|
449
|
-
ctx.send(socket, {
|
|
450
|
-
type: 'share_to_slack_response',
|
|
451
|
-
success: false,
|
|
452
|
-
error: message,
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
export function handleSlackWebhookConfig(
|
|
458
|
-
msg: SlackWebhookConfigRequest,
|
|
459
|
-
socket: net.Socket,
|
|
460
|
-
ctx: HandlerContext,
|
|
461
|
-
): void {
|
|
462
|
-
try {
|
|
463
|
-
const config = loadRawConfig();
|
|
464
|
-
if (msg.action === 'get') {
|
|
465
|
-
ctx.send(socket, {
|
|
466
|
-
type: 'slack_webhook_config_response',
|
|
467
|
-
webhookUrl: (config.slackWebhookUrl as string) ?? undefined,
|
|
468
|
-
success: true,
|
|
469
|
-
});
|
|
470
|
-
} else {
|
|
471
|
-
config.slackWebhookUrl = msg.webhookUrl ?? '';
|
|
472
|
-
saveRawConfig(config);
|
|
473
|
-
ctx.send(socket, {
|
|
474
|
-
type: 'slack_webhook_config_response',
|
|
475
|
-
success: true,
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
|
-
} catch (err) {
|
|
479
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
480
|
-
log.error({ err }, 'Failed to handle Slack webhook config');
|
|
481
|
-
ctx.send(socket, {
|
|
482
|
-
type: 'slack_webhook_config_response',
|
|
483
|
-
success: false,
|
|
484
|
-
error: message,
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function computeGatewayTarget(): string {
|
|
490
|
-
if (process.env.GATEWAY_INTERNAL_BASE_URL) {
|
|
491
|
-
return process.env.GATEWAY_INTERNAL_BASE_URL.replace(/\/+$/, '');
|
|
492
|
-
}
|
|
493
|
-
const portRaw = process.env.GATEWAY_PORT || '7830';
|
|
494
|
-
const port = Number(portRaw) || 7830;
|
|
495
|
-
return `http://127.0.0.1:${port}`;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* Best-effort call to the gateway's internal reconcile endpoint so that
|
|
500
|
-
* Telegram webhook registration is updated immediately when the ingress
|
|
501
|
-
* URL changes, without requiring a gateway restart.
|
|
502
|
-
*/
|
|
503
|
-
function triggerGatewayReconcile(ingressPublicBaseUrl: string | undefined): void {
|
|
504
|
-
const gatewayBase = computeGatewayTarget();
|
|
505
|
-
const token = readHttpToken();
|
|
506
|
-
if (!token) {
|
|
507
|
-
log.debug('Skipping gateway reconcile trigger: no HTTP bearer token available');
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
const url = `${gatewayBase}/internal/telegram/reconcile`;
|
|
512
|
-
const body = JSON.stringify({ ingressPublicBaseUrl: ingressPublicBaseUrl ?? '' });
|
|
513
|
-
|
|
514
|
-
fetch(url, {
|
|
515
|
-
method: 'POST',
|
|
516
|
-
headers: {
|
|
517
|
-
'Content-Type': 'application/json',
|
|
518
|
-
'Authorization': `Bearer ${token}`,
|
|
519
|
-
},
|
|
520
|
-
body,
|
|
521
|
-
signal: AbortSignal.timeout(5_000),
|
|
522
|
-
}).then((res) => {
|
|
523
|
-
if (res.ok) {
|
|
524
|
-
log.info('Gateway Telegram webhook reconcile triggered successfully');
|
|
525
|
-
} else {
|
|
526
|
-
log.warn({ status: res.status }, 'Gateway Telegram webhook reconcile returned non-OK status');
|
|
527
|
-
}
|
|
528
|
-
}).catch((err) => {
|
|
529
|
-
log.debug({ err }, 'Gateway Telegram webhook reconcile failed (gateway may not be running)');
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
|
|
533
1
|
/**
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
* Computes the voice, status-callback, and SMS webhook URLs from the current
|
|
537
|
-
* ingress config and pushes them to the Twilio IncomingPhoneNumber API.
|
|
2
|
+
* Config handler barrel — re-exports all config domain handlers and assembles
|
|
3
|
+
* the combined `configHandlers` dispatch map.
|
|
538
4
|
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const voiceUrl = getTwilioVoiceWebhookUrl(ingressConfig);
|
|
552
|
-
const statusCallbackUrl = getTwilioStatusCallbackUrl(ingressConfig);
|
|
553
|
-
const smsUrl = getTwilioSmsWebhookUrl(ingressConfig);
|
|
554
|
-
await updatePhoneNumberWebhooks(accountSid, authToken, phoneNumber, {
|
|
555
|
-
voiceUrl,
|
|
556
|
-
statusCallbackUrl,
|
|
557
|
-
smsUrl,
|
|
558
|
-
});
|
|
559
|
-
log.info({ phoneNumber }, 'Twilio webhooks configured successfully');
|
|
560
|
-
return { success: true };
|
|
561
|
-
} catch (err) {
|
|
562
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
563
|
-
log.warn({ err, phoneNumber }, `Webhook configuration skipped: ${message}`);
|
|
564
|
-
return { success: false, warning: `Webhook configuration skipped: ${message}` };
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
export async function handleIngressConfig(
|
|
569
|
-
msg: IngressConfigRequest,
|
|
570
|
-
socket: net.Socket,
|
|
571
|
-
ctx: HandlerContext,
|
|
572
|
-
): Promise<void> {
|
|
573
|
-
const localGatewayTarget = computeGatewayTarget();
|
|
574
|
-
try {
|
|
575
|
-
if (msg.action === 'get') {
|
|
576
|
-
const raw = loadRawConfig();
|
|
577
|
-
const ingress = (raw?.ingress ?? {}) as Record<string, unknown>;
|
|
578
|
-
const publicBaseUrl = (ingress.publicBaseUrl as string) ?? '';
|
|
579
|
-
// Backward compatibility: if `enabled` was never explicitly set,
|
|
580
|
-
// infer from whether a publicBaseUrl is configured so existing users
|
|
581
|
-
// who predate the toggle aren't silently disabled.
|
|
582
|
-
const enabled = (ingress.enabled as boolean | undefined) ?? (publicBaseUrl ? true : false);
|
|
583
|
-
ctx.send(socket, { type: 'ingress_config_response', enabled, publicBaseUrl, localGatewayTarget, success: true });
|
|
584
|
-
} else if (msg.action === 'set') {
|
|
585
|
-
const value = (msg.publicBaseUrl ?? '').trim().replace(/\/+$/, '');
|
|
586
|
-
// Ensure we capture the original env value before any mutation below
|
|
587
|
-
getOriginalIngressEnv();
|
|
588
|
-
const raw = loadRawConfig();
|
|
589
|
-
|
|
590
|
-
// Update ingress.publicBaseUrl — this is the single source of truth for
|
|
591
|
-
// the canonical public ingress URL. The gateway receives this value via
|
|
592
|
-
// the INGRESS_PUBLIC_BASE_URL env var at spawn time (see hatch.ts).
|
|
593
|
-
// The gateway also validates Twilio signatures against forwarded public
|
|
594
|
-
// URL headers, so local tunnel updates generally apply without restarts.
|
|
595
|
-
const ingress = (raw?.ingress ?? {}) as Record<string, unknown>;
|
|
596
|
-
ingress.publicBaseUrl = value || undefined;
|
|
597
|
-
if (msg.enabled !== undefined) {
|
|
598
|
-
ingress.enabled = msg.enabled;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
const wasSuppressed = ctx.suppressConfigReload;
|
|
602
|
-
ctx.setSuppressConfigReload(true);
|
|
603
|
-
try {
|
|
604
|
-
saveRawConfig({ ...raw, ingress });
|
|
605
|
-
} catch (err) {
|
|
606
|
-
ctx.setSuppressConfigReload(wasSuppressed);
|
|
607
|
-
throw err;
|
|
608
|
-
}
|
|
609
|
-
ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
610
|
-
|
|
611
|
-
// Propagate to the gateway's process environment so it picks up the
|
|
612
|
-
// new URL when it is restarted. For the local-deployment path the
|
|
613
|
-
// gateway runs as a child process that inherited the assistant's env,
|
|
614
|
-
// so updating process.env here ensures the value is visible when the
|
|
615
|
-
// gateway is restarted (e.g. by the self-upgrade skill or a manual
|
|
616
|
-
// `pkill -f gateway`).
|
|
617
|
-
// Only export the URL when ingress is enabled; clearing it when
|
|
618
|
-
// disabled ensures the gateway stops accepting inbound webhooks.
|
|
619
|
-
const isEnabled = (ingress.enabled as boolean | undefined) ?? (value ? true : false);
|
|
620
|
-
if (value && isEnabled) {
|
|
621
|
-
process.env.INGRESS_PUBLIC_BASE_URL = value;
|
|
622
|
-
} else if (isEnabled && getOriginalIngressEnv() !== undefined) {
|
|
623
|
-
// Ingress is enabled but the user cleared the URL — fall back to the
|
|
624
|
-
// env var that was present when the process started.
|
|
625
|
-
process.env.INGRESS_PUBLIC_BASE_URL = getOriginalIngressEnv()!;
|
|
626
|
-
} else {
|
|
627
|
-
// Ingress is disabled or no URL is configured and no startup env var
|
|
628
|
-
// exists — remove the env var so the gateway stops accepting webhooks.
|
|
629
|
-
delete process.env.INGRESS_PUBLIC_BASE_URL;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
ctx.send(socket, { type: 'ingress_config_response', enabled: isEnabled, publicBaseUrl: value, localGatewayTarget, success: true });
|
|
633
|
-
|
|
634
|
-
// Trigger immediate Telegram webhook reconcile on the gateway so
|
|
635
|
-
// that changing the ingress URL takes effect without a restart.
|
|
636
|
-
// Called unconditionally so the gateway clears its in-memory URL
|
|
637
|
-
// when ingress is disabled, preventing stale re-registration on
|
|
638
|
-
// credential rotation.
|
|
639
|
-
// Use the effective URL from process.env (which accounts for the
|
|
640
|
-
// fallback branch above) rather than the raw `value` from the UI.
|
|
641
|
-
const effectiveUrl = isEnabled ? process.env.INGRESS_PUBLIC_BASE_URL : undefined;
|
|
642
|
-
triggerGatewayReconcile(effectiveUrl);
|
|
643
|
-
|
|
644
|
-
// Best-effort Twilio webhook reconciliation: when ingress is being
|
|
645
|
-
// enabled/updated and Twilio numbers are assigned with valid credentials,
|
|
646
|
-
// push the new webhook URLs to Twilio so calls and SMS route correctly.
|
|
647
|
-
if (isEnabled && hasTwilioCredentials()) {
|
|
648
|
-
const currentConfig = loadRawConfig();
|
|
649
|
-
const smsConfig = (currentConfig?.sms ?? {}) as Record<string, unknown>;
|
|
650
|
-
const assignedNumbers = new Set<string>();
|
|
651
|
-
const legacyNumber = (smsConfig.phoneNumber as string) ?? '';
|
|
652
|
-
if (legacyNumber) assignedNumbers.add(legacyNumber);
|
|
653
|
-
|
|
654
|
-
const assistantPhoneNumbers = smsConfig.assistantPhoneNumbers;
|
|
655
|
-
if (assistantPhoneNumbers && typeof assistantPhoneNumbers === 'object' && !Array.isArray(assistantPhoneNumbers)) {
|
|
656
|
-
for (const number of Object.values(assistantPhoneNumbers as Record<string, unknown>)) {
|
|
657
|
-
if (typeof number === 'string' && number) {
|
|
658
|
-
assignedNumbers.add(number);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
if (assignedNumbers.size > 0) {
|
|
664
|
-
const acctSid = getSecureKey('credential:twilio:account_sid')!;
|
|
665
|
-
const acctToken = getSecureKey('credential:twilio:auth_token')!;
|
|
666
|
-
// Fire-and-forget: webhook sync failure must not block the ingress save.
|
|
667
|
-
// Reconcile every assigned number so assistant-scoped mappings do not
|
|
668
|
-
// retain stale Twilio webhook URLs after ingress URL changes.
|
|
669
|
-
for (const assignedNumber of assignedNumbers) {
|
|
670
|
-
syncTwilioWebhooks(assignedNumber, acctSid, acctToken, currentConfig as IngressConfig)
|
|
671
|
-
.catch(() => {
|
|
672
|
-
// Already logged inside syncTwilioWebhooks
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
} else {
|
|
678
|
-
ctx.send(socket, { type: 'ingress_config_response', enabled: false, publicBaseUrl: '', localGatewayTarget, success: false, error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}` });
|
|
679
|
-
}
|
|
680
|
-
} catch (err) {
|
|
681
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
682
|
-
ctx.send(socket, { type: 'ingress_config_response', enabled: false, publicBaseUrl: '', localGatewayTarget, success: false, error: message });
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
export function handleVercelApiConfig(
|
|
687
|
-
msg: VercelApiConfigRequest,
|
|
688
|
-
socket: net.Socket,
|
|
689
|
-
ctx: HandlerContext,
|
|
690
|
-
): void {
|
|
691
|
-
try {
|
|
692
|
-
if (msg.action === 'get') {
|
|
693
|
-
const existing = getSecureKey('credential:vercel:api_token');
|
|
694
|
-
ctx.send(socket, {
|
|
695
|
-
type: 'vercel_api_config_response',
|
|
696
|
-
hasToken: !!existing,
|
|
697
|
-
success: true,
|
|
698
|
-
});
|
|
699
|
-
} else if (msg.action === 'set') {
|
|
700
|
-
if (!msg.apiToken) {
|
|
701
|
-
ctx.send(socket, {
|
|
702
|
-
type: 'vercel_api_config_response',
|
|
703
|
-
hasToken: false,
|
|
704
|
-
success: false,
|
|
705
|
-
error: 'apiToken is required for set action',
|
|
706
|
-
});
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
const stored = setSecureKey('credential:vercel:api_token', msg.apiToken);
|
|
710
|
-
if (!stored) {
|
|
711
|
-
ctx.send(socket, {
|
|
712
|
-
type: 'vercel_api_config_response',
|
|
713
|
-
hasToken: false,
|
|
714
|
-
success: false,
|
|
715
|
-
error: 'Failed to store API token in secure storage',
|
|
716
|
-
});
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
|
-
upsertCredentialMetadata('vercel', 'api_token', {
|
|
720
|
-
allowedTools: ['publish_page', 'unpublish_page'],
|
|
721
|
-
});
|
|
722
|
-
ctx.send(socket, {
|
|
723
|
-
type: 'vercel_api_config_response',
|
|
724
|
-
hasToken: true,
|
|
725
|
-
success: true,
|
|
726
|
-
});
|
|
727
|
-
} else {
|
|
728
|
-
deleteSecureKey('credential:vercel:api_token');
|
|
729
|
-
deleteCredentialMetadata('vercel', 'api_token');
|
|
730
|
-
ctx.send(socket, {
|
|
731
|
-
type: 'vercel_api_config_response',
|
|
732
|
-
hasToken: false,
|
|
733
|
-
success: true,
|
|
734
|
-
});
|
|
735
|
-
}
|
|
736
|
-
} catch (err) {
|
|
737
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
738
|
-
log.error({ err }, 'Failed to handle Vercel API config');
|
|
739
|
-
ctx.send(socket, {
|
|
740
|
-
type: 'vercel_api_config_response',
|
|
741
|
-
hasToken: false,
|
|
742
|
-
success: false,
|
|
743
|
-
error: message,
|
|
744
|
-
});
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
export function handleTwitterIntegrationConfig(
|
|
749
|
-
msg: TwitterIntegrationConfigRequest,
|
|
750
|
-
socket: net.Socket,
|
|
751
|
-
ctx: HandlerContext,
|
|
752
|
-
): void {
|
|
753
|
-
try {
|
|
754
|
-
if (msg.action === 'get') {
|
|
755
|
-
const raw = loadRawConfig();
|
|
756
|
-
const mode = (raw.twitterIntegrationMode as 'local_byo' | 'managed' | undefined) ?? 'local_byo';
|
|
757
|
-
const strategy = (raw.twitterOperationStrategy as 'oauth' | 'browser' | 'auto' | undefined) ?? 'auto';
|
|
758
|
-
const strategyConfigured = Object.prototype.hasOwnProperty.call(raw, 'twitterOperationStrategy');
|
|
759
|
-
const localClientConfigured = !!getSecureKey('credential:integration:twitter:oauth_client_id');
|
|
760
|
-
const connected = !!getSecureKey('credential:integration:twitter:access_token');
|
|
761
|
-
const meta = getCredentialMetadata('integration:twitter', 'access_token');
|
|
762
|
-
ctx.send(socket, {
|
|
763
|
-
type: 'twitter_integration_config_response',
|
|
764
|
-
success: true,
|
|
765
|
-
mode,
|
|
766
|
-
managedAvailable: false,
|
|
767
|
-
localClientConfigured,
|
|
768
|
-
connected,
|
|
769
|
-
accountInfo: meta?.accountInfo ?? undefined,
|
|
770
|
-
strategy,
|
|
771
|
-
strategyConfigured,
|
|
772
|
-
});
|
|
773
|
-
} else if (msg.action === 'get_strategy') {
|
|
774
|
-
const raw = loadRawConfig();
|
|
775
|
-
const strategy = (raw.twitterOperationStrategy as 'oauth' | 'browser' | 'auto' | undefined) ?? 'auto';
|
|
776
|
-
const strategyConfigured = Object.prototype.hasOwnProperty.call(raw, 'twitterOperationStrategy');
|
|
777
|
-
ctx.send(socket, {
|
|
778
|
-
type: 'twitter_integration_config_response',
|
|
779
|
-
success: true,
|
|
780
|
-
managedAvailable: false,
|
|
781
|
-
localClientConfigured: !!getSecureKey('credential:integration:twitter:oauth_client_id'),
|
|
782
|
-
connected: !!getSecureKey('credential:integration:twitter:access_token'),
|
|
783
|
-
strategy,
|
|
784
|
-
strategyConfigured,
|
|
785
|
-
});
|
|
786
|
-
} else if (msg.action === 'set_strategy') {
|
|
787
|
-
const valid = ['oauth', 'browser', 'auto'];
|
|
788
|
-
const value = msg.strategy;
|
|
789
|
-
if (!value || !valid.includes(value)) {
|
|
790
|
-
ctx.send(socket, {
|
|
791
|
-
type: 'twitter_integration_config_response',
|
|
792
|
-
success: false,
|
|
793
|
-
managedAvailable: false,
|
|
794
|
-
localClientConfigured: false,
|
|
795
|
-
connected: false,
|
|
796
|
-
error: `Invalid strategy value: ${String(value)}. Must be one of: ${valid.join(', ')}`,
|
|
797
|
-
});
|
|
798
|
-
return;
|
|
799
|
-
}
|
|
800
|
-
const raw = loadRawConfig();
|
|
801
|
-
raw.twitterOperationStrategy = value;
|
|
802
|
-
saveRawConfig(raw);
|
|
803
|
-
ctx.send(socket, {
|
|
804
|
-
type: 'twitter_integration_config_response',
|
|
805
|
-
success: true,
|
|
806
|
-
managedAvailable: false,
|
|
807
|
-
localClientConfigured: !!getSecureKey('credential:integration:twitter:oauth_client_id'),
|
|
808
|
-
connected: !!getSecureKey('credential:integration:twitter:access_token'),
|
|
809
|
-
strategy: value as 'oauth' | 'browser' | 'auto',
|
|
810
|
-
strategyConfigured: true,
|
|
811
|
-
});
|
|
812
|
-
} else if (msg.action === 'set_mode') {
|
|
813
|
-
const raw = loadRawConfig();
|
|
814
|
-
raw.twitterIntegrationMode = msg.mode ?? 'local_byo';
|
|
815
|
-
saveRawConfig(raw);
|
|
816
|
-
ctx.send(socket, {
|
|
817
|
-
type: 'twitter_integration_config_response',
|
|
818
|
-
success: true,
|
|
819
|
-
mode: msg.mode ?? 'local_byo',
|
|
820
|
-
managedAvailable: false,
|
|
821
|
-
localClientConfigured: !!getSecureKey('credential:integration:twitter:oauth_client_id'),
|
|
822
|
-
connected: !!getSecureKey('credential:integration:twitter:access_token'),
|
|
823
|
-
});
|
|
824
|
-
} else if (msg.action === 'set_local_client') {
|
|
825
|
-
if (!msg.clientId) {
|
|
826
|
-
ctx.send(socket, {
|
|
827
|
-
type: 'twitter_integration_config_response',
|
|
828
|
-
success: false,
|
|
829
|
-
managedAvailable: false,
|
|
830
|
-
localClientConfigured: false,
|
|
831
|
-
connected: false,
|
|
832
|
-
error: 'clientId is required for set_local_client action',
|
|
833
|
-
});
|
|
834
|
-
return;
|
|
835
|
-
}
|
|
836
|
-
const previousClientId = getSecureKey('credential:integration:twitter:oauth_client_id');
|
|
837
|
-
const storedId = setSecureKey('credential:integration:twitter:oauth_client_id', msg.clientId);
|
|
838
|
-
if (!storedId) {
|
|
839
|
-
ctx.send(socket, {
|
|
840
|
-
type: 'twitter_integration_config_response',
|
|
841
|
-
success: false,
|
|
842
|
-
managedAvailable: false,
|
|
843
|
-
localClientConfigured: false,
|
|
844
|
-
connected: false,
|
|
845
|
-
error: 'Failed to store client ID in secure storage',
|
|
846
|
-
});
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
if (msg.clientSecret) {
|
|
850
|
-
const storedSecret = setSecureKey('credential:integration:twitter:oauth_client_secret', msg.clientSecret);
|
|
851
|
-
if (!storedSecret) {
|
|
852
|
-
// Roll back the client ID to its previous value to avoid inconsistent OAuth state
|
|
853
|
-
if (previousClientId) {
|
|
854
|
-
setSecureKey('credential:integration:twitter:oauth_client_id', previousClientId);
|
|
855
|
-
} else {
|
|
856
|
-
deleteSecureKey('credential:integration:twitter:oauth_client_id');
|
|
857
|
-
}
|
|
858
|
-
ctx.send(socket, {
|
|
859
|
-
type: 'twitter_integration_config_response',
|
|
860
|
-
success: false,
|
|
861
|
-
managedAvailable: false,
|
|
862
|
-
localClientConfigured: !!previousClientId,
|
|
863
|
-
connected: false,
|
|
864
|
-
error: 'Failed to store client secret in secure storage',
|
|
865
|
-
});
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
} else {
|
|
869
|
-
// Clear any stale secret when updating client without a secret (e.g. switching to PKCE)
|
|
870
|
-
deleteSecureKey('credential:integration:twitter:oauth_client_secret');
|
|
871
|
-
}
|
|
872
|
-
ctx.send(socket, {
|
|
873
|
-
type: 'twitter_integration_config_response',
|
|
874
|
-
success: true,
|
|
875
|
-
managedAvailable: false,
|
|
876
|
-
localClientConfigured: true,
|
|
877
|
-
connected: !!getSecureKey('credential:integration:twitter:access_token'),
|
|
878
|
-
});
|
|
879
|
-
} else if (msg.action === 'clear_local_client') {
|
|
880
|
-
// If connected, disconnect first
|
|
881
|
-
if (getSecureKey('credential:integration:twitter:access_token')) {
|
|
882
|
-
deleteSecureKey('credential:integration:twitter:access_token');
|
|
883
|
-
deleteSecureKey('credential:integration:twitter:refresh_token');
|
|
884
|
-
deleteCredentialMetadata('integration:twitter', 'access_token');
|
|
885
|
-
}
|
|
886
|
-
deleteSecureKey('credential:integration:twitter:oauth_client_id');
|
|
887
|
-
deleteSecureKey('credential:integration:twitter:oauth_client_secret');
|
|
888
|
-
ctx.send(socket, {
|
|
889
|
-
type: 'twitter_integration_config_response',
|
|
890
|
-
success: true,
|
|
891
|
-
managedAvailable: false,
|
|
892
|
-
localClientConfigured: false,
|
|
893
|
-
connected: false,
|
|
894
|
-
});
|
|
895
|
-
} else if (msg.action === 'disconnect') {
|
|
896
|
-
deleteSecureKey('credential:integration:twitter:access_token');
|
|
897
|
-
deleteSecureKey('credential:integration:twitter:refresh_token');
|
|
898
|
-
deleteCredentialMetadata('integration:twitter', 'access_token');
|
|
899
|
-
ctx.send(socket, {
|
|
900
|
-
type: 'twitter_integration_config_response',
|
|
901
|
-
success: true,
|
|
902
|
-
managedAvailable: false,
|
|
903
|
-
localClientConfigured: !!getSecureKey('credential:integration:twitter:oauth_client_id'),
|
|
904
|
-
connected: false,
|
|
905
|
-
});
|
|
906
|
-
} else {
|
|
907
|
-
ctx.send(socket, {
|
|
908
|
-
type: 'twitter_integration_config_response',
|
|
909
|
-
success: false,
|
|
910
|
-
managedAvailable: false,
|
|
911
|
-
localClientConfigured: false,
|
|
912
|
-
connected: false,
|
|
913
|
-
error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}`,
|
|
914
|
-
});
|
|
915
|
-
}
|
|
916
|
-
} catch (err) {
|
|
917
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
918
|
-
log.error({ err }, 'Failed to handle Twitter integration config');
|
|
919
|
-
ctx.send(socket, {
|
|
920
|
-
type: 'twitter_integration_config_response',
|
|
921
|
-
success: false,
|
|
922
|
-
managedAvailable: false,
|
|
923
|
-
localClientConfigured: false,
|
|
924
|
-
connected: false,
|
|
925
|
-
error: message,
|
|
926
|
-
});
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
export async function handleTelegramConfig(
|
|
931
|
-
msg: TelegramConfigRequest,
|
|
932
|
-
socket: net.Socket,
|
|
933
|
-
ctx: HandlerContext,
|
|
934
|
-
): Promise<void> {
|
|
935
|
-
try {
|
|
936
|
-
if (msg.action === 'get') {
|
|
937
|
-
const hasBotToken = !!getSecureKey('credential:telegram:bot_token');
|
|
938
|
-
const hasWebhookSecret = !!getSecureKey('credential:telegram:webhook_secret');
|
|
939
|
-
const meta = getCredentialMetadata('telegram', 'bot_token');
|
|
940
|
-
const botUsername = meta?.accountInfo ?? undefined;
|
|
941
|
-
ctx.send(socket, {
|
|
942
|
-
type: 'telegram_config_response',
|
|
943
|
-
success: true,
|
|
944
|
-
hasBotToken,
|
|
945
|
-
botUsername,
|
|
946
|
-
connected: hasBotToken && hasWebhookSecret,
|
|
947
|
-
hasWebhookSecret,
|
|
948
|
-
});
|
|
949
|
-
} else if (msg.action === 'set') {
|
|
950
|
-
// Resolve token: prefer explicit msg.botToken, fall back to secure storage.
|
|
951
|
-
// Track provenance so we only rollback tokens that were freshly provided.
|
|
952
|
-
const isNewToken = !!msg.botToken;
|
|
953
|
-
const botToken = msg.botToken || getSecureKey('credential:telegram:bot_token');
|
|
954
|
-
if (!botToken) {
|
|
955
|
-
ctx.send(socket, {
|
|
956
|
-
type: 'telegram_config_response',
|
|
957
|
-
success: false,
|
|
958
|
-
hasBotToken: false,
|
|
959
|
-
connected: false,
|
|
960
|
-
hasWebhookSecret: false,
|
|
961
|
-
error: 'botToken is required for set action',
|
|
962
|
-
});
|
|
963
|
-
return;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
// Validate token via Telegram getMe API
|
|
967
|
-
let botUsername: string;
|
|
968
|
-
try {
|
|
969
|
-
const res = await fetch(`https://api.telegram.org/bot${botToken}/getMe`);
|
|
970
|
-
if (!res.ok) {
|
|
971
|
-
const body = await res.text();
|
|
972
|
-
ctx.send(socket, {
|
|
973
|
-
type: 'telegram_config_response',
|
|
974
|
-
success: false,
|
|
975
|
-
hasBotToken: false,
|
|
976
|
-
connected: false,
|
|
977
|
-
hasWebhookSecret: false,
|
|
978
|
-
error: `Telegram API validation failed: ${body}`,
|
|
979
|
-
});
|
|
980
|
-
return;
|
|
981
|
-
}
|
|
982
|
-
const data = await res.json() as { ok: boolean; result?: { username?: string } };
|
|
983
|
-
if (!data.ok || !data.result?.username) {
|
|
984
|
-
ctx.send(socket, {
|
|
985
|
-
type: 'telegram_config_response',
|
|
986
|
-
success: false,
|
|
987
|
-
hasBotToken: false,
|
|
988
|
-
connected: false,
|
|
989
|
-
hasWebhookSecret: false,
|
|
990
|
-
error: 'Telegram API returned unexpected response',
|
|
991
|
-
});
|
|
992
|
-
return;
|
|
993
|
-
}
|
|
994
|
-
botUsername = data.result.username;
|
|
995
|
-
} catch (err) {
|
|
996
|
-
const message = summarizeTelegramError(err);
|
|
997
|
-
ctx.send(socket, {
|
|
998
|
-
type: 'telegram_config_response',
|
|
999
|
-
success: false,
|
|
1000
|
-
hasBotToken: false,
|
|
1001
|
-
connected: false,
|
|
1002
|
-
hasWebhookSecret: false,
|
|
1003
|
-
error: `Failed to validate bot token: ${message}`,
|
|
1004
|
-
});
|
|
1005
|
-
return;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
// Store bot token securely
|
|
1009
|
-
const stored = setSecureKey('credential:telegram:bot_token', botToken);
|
|
1010
|
-
if (!stored) {
|
|
1011
|
-
ctx.send(socket, {
|
|
1012
|
-
type: 'telegram_config_response',
|
|
1013
|
-
success: false,
|
|
1014
|
-
hasBotToken: false,
|
|
1015
|
-
connected: false,
|
|
1016
|
-
hasWebhookSecret: false,
|
|
1017
|
-
error: 'Failed to store bot token in secure storage',
|
|
1018
|
-
});
|
|
1019
|
-
return;
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
// Store metadata with bot username
|
|
1023
|
-
upsertCredentialMetadata('telegram', 'bot_token', {
|
|
1024
|
-
accountInfo: botUsername,
|
|
1025
|
-
});
|
|
1026
|
-
|
|
1027
|
-
// Ensure webhook secret exists (generate if missing)
|
|
1028
|
-
let hasWebhookSecret = !!getSecureKey('credential:telegram:webhook_secret');
|
|
1029
|
-
if (!hasWebhookSecret) {
|
|
1030
|
-
const { randomUUID } = await import('node:crypto');
|
|
1031
|
-
const webhookSecret = randomUUID();
|
|
1032
|
-
const secretStored = setSecureKey('credential:telegram:webhook_secret', webhookSecret);
|
|
1033
|
-
if (secretStored) {
|
|
1034
|
-
upsertCredentialMetadata('telegram', 'webhook_secret', {});
|
|
1035
|
-
hasWebhookSecret = true;
|
|
1036
|
-
} else {
|
|
1037
|
-
// Only roll back the bot token if it was freshly provided.
|
|
1038
|
-
// When the token came from secure storage it was already valid
|
|
1039
|
-
// configuration; deleting it would destroy working state.
|
|
1040
|
-
if (isNewToken) {
|
|
1041
|
-
deleteSecureKey('credential:telegram:bot_token');
|
|
1042
|
-
deleteCredentialMetadata('telegram', 'bot_token');
|
|
1043
|
-
}
|
|
1044
|
-
ctx.send(socket, {
|
|
1045
|
-
type: 'telegram_config_response',
|
|
1046
|
-
success: false,
|
|
1047
|
-
hasBotToken: !isNewToken,
|
|
1048
|
-
connected: false,
|
|
1049
|
-
hasWebhookSecret: false,
|
|
1050
|
-
error: 'Failed to store webhook secret',
|
|
1051
|
-
});
|
|
1052
|
-
return;
|
|
1053
|
-
}
|
|
1054
|
-
} else {
|
|
1055
|
-
// Self-heal: ensure metadata exists even when the secret was
|
|
1056
|
-
// already present (covers previously lost/corrupted metadata).
|
|
1057
|
-
upsertCredentialMetadata('telegram', 'webhook_secret', {});
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
ctx.send(socket, {
|
|
1061
|
-
type: 'telegram_config_response',
|
|
1062
|
-
success: true,
|
|
1063
|
-
hasBotToken: true,
|
|
1064
|
-
botUsername,
|
|
1065
|
-
connected: true,
|
|
1066
|
-
hasWebhookSecret,
|
|
1067
|
-
});
|
|
1068
|
-
|
|
1069
|
-
// Trigger gateway reconcile so the webhook registration updates immediately
|
|
1070
|
-
const effectiveUrl = process.env.INGRESS_PUBLIC_BASE_URL;
|
|
1071
|
-
if (effectiveUrl) {
|
|
1072
|
-
triggerGatewayReconcile(effectiveUrl);
|
|
1073
|
-
}
|
|
1074
|
-
} else if (msg.action === 'clear') {
|
|
1075
|
-
// Deregister the Telegram webhook before deleting credentials.
|
|
1076
|
-
// The gateway reconcile short-circuits when credentials are absent,
|
|
1077
|
-
// so we must call the Telegram API directly while the token is still
|
|
1078
|
-
// available.
|
|
1079
|
-
const botToken = getSecureKey('credential:telegram:bot_token');
|
|
1080
|
-
if (botToken) {
|
|
1081
|
-
try {
|
|
1082
|
-
await fetch(`https://api.telegram.org/bot${botToken}/deleteWebhook`);
|
|
1083
|
-
} catch (err) {
|
|
1084
|
-
log.warn(
|
|
1085
|
-
{ error: summarizeTelegramError(err) },
|
|
1086
|
-
'Failed to deregister Telegram webhook (proceeding with credential cleanup)',
|
|
1087
|
-
);
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
deleteSecureKey('credential:telegram:bot_token');
|
|
1092
|
-
deleteCredentialMetadata('telegram', 'bot_token');
|
|
1093
|
-
deleteSecureKey('credential:telegram:webhook_secret');
|
|
1094
|
-
deleteCredentialMetadata('telegram', 'webhook_secret');
|
|
1095
|
-
|
|
1096
|
-
ctx.send(socket, {
|
|
1097
|
-
type: 'telegram_config_response',
|
|
1098
|
-
success: true,
|
|
1099
|
-
hasBotToken: false,
|
|
1100
|
-
connected: false,
|
|
1101
|
-
hasWebhookSecret: false,
|
|
1102
|
-
});
|
|
1103
|
-
|
|
1104
|
-
// Trigger reconcile to deregister webhook
|
|
1105
|
-
const effectiveUrl = process.env.INGRESS_PUBLIC_BASE_URL;
|
|
1106
|
-
if (effectiveUrl) {
|
|
1107
|
-
triggerGatewayReconcile(effectiveUrl);
|
|
1108
|
-
}
|
|
1109
|
-
} else if (msg.action === 'set_commands') {
|
|
1110
|
-
const storedToken = getSecureKey('credential:telegram:bot_token');
|
|
1111
|
-
if (!storedToken) {
|
|
1112
|
-
ctx.send(socket, {
|
|
1113
|
-
type: 'telegram_config_response',
|
|
1114
|
-
success: false,
|
|
1115
|
-
hasBotToken: false,
|
|
1116
|
-
connected: false,
|
|
1117
|
-
hasWebhookSecret: false,
|
|
1118
|
-
error: 'Bot token not configured. Run set action first.',
|
|
1119
|
-
});
|
|
1120
|
-
return;
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
const commands = msg.commands ?? [
|
|
1124
|
-
{ command: 'new', description: 'Start a new conversation' },
|
|
1125
|
-
{ command: 'guardian_verify', description: 'Verify your guardian identity' },
|
|
1126
|
-
];
|
|
1127
|
-
|
|
1128
|
-
try {
|
|
1129
|
-
const res = await fetch(`https://api.telegram.org/bot${storedToken}/setMyCommands`, {
|
|
1130
|
-
method: 'POST',
|
|
1131
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1132
|
-
body: JSON.stringify({ commands }),
|
|
1133
|
-
});
|
|
1134
|
-
if (!res.ok) {
|
|
1135
|
-
const body = await res.text();
|
|
1136
|
-
ctx.send(socket, {
|
|
1137
|
-
type: 'telegram_config_response',
|
|
1138
|
-
success: false,
|
|
1139
|
-
hasBotToken: true,
|
|
1140
|
-
connected: !!getSecureKey('credential:telegram:webhook_secret'),
|
|
1141
|
-
hasWebhookSecret: !!getSecureKey('credential:telegram:webhook_secret'),
|
|
1142
|
-
error: `Failed to set bot commands: ${body}`,
|
|
1143
|
-
});
|
|
1144
|
-
return;
|
|
1145
|
-
}
|
|
1146
|
-
} catch (err) {
|
|
1147
|
-
const message = summarizeTelegramError(err);
|
|
1148
|
-
ctx.send(socket, {
|
|
1149
|
-
type: 'telegram_config_response',
|
|
1150
|
-
success: false,
|
|
1151
|
-
hasBotToken: true,
|
|
1152
|
-
connected: !!getSecureKey('credential:telegram:webhook_secret'),
|
|
1153
|
-
hasWebhookSecret: !!getSecureKey('credential:telegram:webhook_secret'),
|
|
1154
|
-
error: `Failed to set bot commands: ${message}`,
|
|
1155
|
-
});
|
|
1156
|
-
return;
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
const hasBotToken = !!getSecureKey('credential:telegram:bot_token');
|
|
1160
|
-
const hasWebhookSecret = !!getSecureKey('credential:telegram:webhook_secret');
|
|
1161
|
-
ctx.send(socket, {
|
|
1162
|
-
type: 'telegram_config_response',
|
|
1163
|
-
success: true,
|
|
1164
|
-
hasBotToken,
|
|
1165
|
-
connected: hasBotToken && hasWebhookSecret,
|
|
1166
|
-
hasWebhookSecret,
|
|
1167
|
-
});
|
|
1168
|
-
} else {
|
|
1169
|
-
ctx.send(socket, {
|
|
1170
|
-
type: 'telegram_config_response',
|
|
1171
|
-
success: false,
|
|
1172
|
-
hasBotToken: false,
|
|
1173
|
-
connected: false,
|
|
1174
|
-
hasWebhookSecret: false,
|
|
1175
|
-
error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}`,
|
|
1176
|
-
});
|
|
1177
|
-
}
|
|
1178
|
-
} catch (err) {
|
|
1179
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1180
|
-
log.error({ err }, 'Failed to handle Telegram config');
|
|
1181
|
-
ctx.send(socket, {
|
|
1182
|
-
type: 'telegram_config_response',
|
|
1183
|
-
success: false,
|
|
1184
|
-
hasBotToken: false,
|
|
1185
|
-
connected: false,
|
|
1186
|
-
hasWebhookSecret: false,
|
|
1187
|
-
error: message,
|
|
1188
|
-
});
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
/** In-memory store for the last SMS send test result. Shared between sms_send_test and sms_doctor. */
|
|
1193
|
-
let _lastTestResult: {
|
|
1194
|
-
messageSid: string;
|
|
1195
|
-
to: string;
|
|
1196
|
-
initialStatus: string;
|
|
1197
|
-
finalStatus: string;
|
|
1198
|
-
errorCode?: string;
|
|
1199
|
-
errorMessage?: string;
|
|
1200
|
-
timestamp: number;
|
|
1201
|
-
} | undefined;
|
|
1202
|
-
|
|
1203
|
-
/** Map a Twilio error code to a human-readable remediation suggestion. */
|
|
1204
|
-
function mapTwilioErrorRemediation(errorCode: string | undefined): string | undefined {
|
|
1205
|
-
if (!errorCode) return undefined;
|
|
1206
|
-
const map: Record<string, string> = {
|
|
1207
|
-
'30003': 'Unreachable destination. The handset may be off or out of service.',
|
|
1208
|
-
'30004': 'Message blocked by carrier or recipient.',
|
|
1209
|
-
'30005': 'Unknown destination phone number. Verify the number is valid.',
|
|
1210
|
-
'30006': 'Landline or unreachable carrier. SMS cannot be delivered to this number.',
|
|
1211
|
-
'30007': 'Message flagged as spam by carrier. Adjust content or register for A2P.',
|
|
1212
|
-
'30008': 'Unknown error from the carrier network.',
|
|
1213
|
-
'21610': 'Recipient has opted out (STOP). Cannot send until they opt back in.',
|
|
1214
|
-
};
|
|
1215
|
-
return map[errorCode];
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
export async function handleTwilioConfig(
|
|
1219
|
-
msg: TwilioConfigRequest,
|
|
1220
|
-
socket: net.Socket,
|
|
1221
|
-
ctx: HandlerContext,
|
|
1222
|
-
): Promise<void> {
|
|
1223
|
-
try {
|
|
1224
|
-
if (msg.action === 'get') {
|
|
1225
|
-
const hasCredentials = hasTwilioCredentials();
|
|
1226
|
-
const raw = loadRawConfig();
|
|
1227
|
-
const sms = (raw?.sms ?? {}) as Record<string, unknown>;
|
|
1228
|
-
// When assistantId is provided, look up in assistantPhoneNumbers first,
|
|
1229
|
-
// fall back to the legacy phoneNumber field
|
|
1230
|
-
let phoneNumber: string;
|
|
1231
|
-
if (msg.assistantId) {
|
|
1232
|
-
const mapping = (sms.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
|
|
1233
|
-
phoneNumber = mapping[msg.assistantId] ?? (sms.phoneNumber as string) ?? '';
|
|
1234
|
-
} else {
|
|
1235
|
-
phoneNumber = (sms.phoneNumber as string) ?? '';
|
|
1236
|
-
}
|
|
1237
|
-
ctx.send(socket, {
|
|
1238
|
-
type: 'twilio_config_response',
|
|
1239
|
-
success: true,
|
|
1240
|
-
hasCredentials,
|
|
1241
|
-
phoneNumber: phoneNumber || undefined,
|
|
1242
|
-
});
|
|
1243
|
-
} else if (msg.action === 'set_credentials') {
|
|
1244
|
-
if (!msg.accountSid || !msg.authToken) {
|
|
1245
|
-
ctx.send(socket, {
|
|
1246
|
-
type: 'twilio_config_response',
|
|
1247
|
-
success: false,
|
|
1248
|
-
hasCredentials: hasTwilioCredentials(),
|
|
1249
|
-
error: 'accountSid and authToken are required for set_credentials action',
|
|
1250
|
-
});
|
|
1251
|
-
return;
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
// Validate credentials by calling the Twilio API
|
|
1255
|
-
const authHeader = 'Basic ' + Buffer.from(`${msg.accountSid}:${msg.authToken}`).toString('base64');
|
|
1256
|
-
try {
|
|
1257
|
-
const res = await fetch(
|
|
1258
|
-
`https://api.twilio.com/2010-04-01/Accounts/${msg.accountSid}.json`,
|
|
1259
|
-
{
|
|
1260
|
-
method: 'GET',
|
|
1261
|
-
headers: { Authorization: authHeader },
|
|
1262
|
-
},
|
|
1263
|
-
);
|
|
1264
|
-
if (!res.ok) {
|
|
1265
|
-
const body = await res.text();
|
|
1266
|
-
ctx.send(socket, {
|
|
1267
|
-
type: 'twilio_config_response',
|
|
1268
|
-
success: false,
|
|
1269
|
-
hasCredentials: hasTwilioCredentials(),
|
|
1270
|
-
error: `Twilio API validation failed (${res.status}): ${body}`,
|
|
1271
|
-
});
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1274
|
-
} catch (err) {
|
|
1275
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1276
|
-
ctx.send(socket, {
|
|
1277
|
-
type: 'twilio_config_response',
|
|
1278
|
-
success: false,
|
|
1279
|
-
hasCredentials: hasTwilioCredentials(),
|
|
1280
|
-
error: `Failed to validate Twilio credentials: ${message}`,
|
|
1281
|
-
});
|
|
1282
|
-
return;
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
// Store credentials securely
|
|
1286
|
-
const sidStored = setSecureKey('credential:twilio:account_sid', msg.accountSid);
|
|
1287
|
-
if (!sidStored) {
|
|
1288
|
-
ctx.send(socket, {
|
|
1289
|
-
type: 'twilio_config_response',
|
|
1290
|
-
success: false,
|
|
1291
|
-
hasCredentials: false,
|
|
1292
|
-
error: 'Failed to store Account SID in secure storage',
|
|
1293
|
-
});
|
|
1294
|
-
return;
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
const tokenStored = setSecureKey('credential:twilio:auth_token', msg.authToken);
|
|
1298
|
-
if (!tokenStored) {
|
|
1299
|
-
// Roll back the Account SID
|
|
1300
|
-
deleteSecureKey('credential:twilio:account_sid');
|
|
1301
|
-
ctx.send(socket, {
|
|
1302
|
-
type: 'twilio_config_response',
|
|
1303
|
-
success: false,
|
|
1304
|
-
hasCredentials: false,
|
|
1305
|
-
error: 'Failed to store Auth Token in secure storage',
|
|
1306
|
-
});
|
|
1307
|
-
return;
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
upsertCredentialMetadata('twilio', 'account_sid', {});
|
|
1311
|
-
upsertCredentialMetadata('twilio', 'auth_token', {});
|
|
1312
|
-
|
|
1313
|
-
ctx.send(socket, {
|
|
1314
|
-
type: 'twilio_config_response',
|
|
1315
|
-
success: true,
|
|
1316
|
-
hasCredentials: true,
|
|
1317
|
-
});
|
|
1318
|
-
} else if (msg.action === 'clear_credentials') {
|
|
1319
|
-
// Only clear authentication credentials (Account SID and Auth Token).
|
|
1320
|
-
// Preserve the phone number in both config (sms.phoneNumber) and secure
|
|
1321
|
-
// key (credential:twilio:phone_number) so that re-entering credentials
|
|
1322
|
-
// resumes working without needing to reassign the number.
|
|
1323
|
-
deleteSecureKey('credential:twilio:account_sid');
|
|
1324
|
-
deleteSecureKey('credential:twilio:auth_token');
|
|
1325
|
-
deleteCredentialMetadata('twilio', 'account_sid');
|
|
1326
|
-
deleteCredentialMetadata('twilio', 'auth_token');
|
|
1327
|
-
|
|
1328
|
-
ctx.send(socket, {
|
|
1329
|
-
type: 'twilio_config_response',
|
|
1330
|
-
success: true,
|
|
1331
|
-
hasCredentials: false,
|
|
1332
|
-
});
|
|
1333
|
-
} else if (msg.action === 'provision_number') {
|
|
1334
|
-
if (!hasTwilioCredentials()) {
|
|
1335
|
-
ctx.send(socket, {
|
|
1336
|
-
type: 'twilio_config_response',
|
|
1337
|
-
success: false,
|
|
1338
|
-
hasCredentials: false,
|
|
1339
|
-
error: 'Twilio credentials not configured. Set credentials first.',
|
|
1340
|
-
});
|
|
1341
|
-
return;
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
const accountSid = getSecureKey('credential:twilio:account_sid')!;
|
|
1345
|
-
const authToken = getSecureKey('credential:twilio:auth_token')!;
|
|
1346
|
-
const country = msg.country ?? 'US';
|
|
1347
|
-
|
|
1348
|
-
// Search for an available number
|
|
1349
|
-
const available = await searchAvailableNumbers(accountSid, authToken, country, msg.areaCode);
|
|
1350
|
-
if (available.length === 0) {
|
|
1351
|
-
ctx.send(socket, {
|
|
1352
|
-
type: 'twilio_config_response',
|
|
1353
|
-
success: false,
|
|
1354
|
-
hasCredentials: true,
|
|
1355
|
-
error: `No available phone numbers found for country=${country}${msg.areaCode ? ` areaCode=${msg.areaCode}` : ''}`,
|
|
1356
|
-
});
|
|
1357
|
-
return;
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
// Purchase the first available number
|
|
1361
|
-
const purchased = await provisionPhoneNumber(accountSid, authToken, available[0].phoneNumber);
|
|
1362
|
-
|
|
1363
|
-
// Auto-assign: persist the purchased number in secure storage and config
|
|
1364
|
-
// (same persistence as assign_number for consistency)
|
|
1365
|
-
const phoneStored = setSecureKey('credential:twilio:phone_number', purchased.phoneNumber);
|
|
1366
|
-
if (!phoneStored) {
|
|
1367
|
-
ctx.send(socket, {
|
|
1368
|
-
type: 'twilio_config_response',
|
|
1369
|
-
success: false,
|
|
1370
|
-
hasCredentials: hasTwilioCredentials(),
|
|
1371
|
-
phoneNumber: purchased.phoneNumber,
|
|
1372
|
-
error: `Phone number ${purchased.phoneNumber} was purchased but could not be saved. Use assign_number to assign it manually.`,
|
|
1373
|
-
});
|
|
1374
|
-
return;
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
const raw = loadRawConfig();
|
|
1378
|
-
const sms = (raw?.sms ?? {}) as Record<string, unknown>;
|
|
1379
|
-
// When assistantId is provided, only set the legacy global phoneNumber
|
|
1380
|
-
// if it's not already set — this prevents multi-assistant assignments
|
|
1381
|
-
// from clobbering each other's outbound SMS number.
|
|
1382
|
-
if (msg.assistantId) {
|
|
1383
|
-
if (!sms.phoneNumber) {
|
|
1384
|
-
sms.phoneNumber = purchased.phoneNumber;
|
|
1385
|
-
}
|
|
1386
|
-
} else {
|
|
1387
|
-
sms.phoneNumber = purchased.phoneNumber;
|
|
1388
|
-
}
|
|
1389
|
-
// When assistantId is provided, also persist into the per-assistant mapping
|
|
1390
|
-
if (msg.assistantId) {
|
|
1391
|
-
const mapping = (sms.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
|
|
1392
|
-
mapping[msg.assistantId] = purchased.phoneNumber;
|
|
1393
|
-
sms.assistantPhoneNumbers = mapping;
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
const wasSuppressed = ctx.suppressConfigReload;
|
|
1397
|
-
ctx.setSuppressConfigReload(true);
|
|
1398
|
-
try {
|
|
1399
|
-
saveRawConfig({ ...raw, sms });
|
|
1400
|
-
} catch (err) {
|
|
1401
|
-
ctx.setSuppressConfigReload(wasSuppressed);
|
|
1402
|
-
throw err;
|
|
1403
|
-
}
|
|
1404
|
-
ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
1405
|
-
|
|
1406
|
-
// Best-effort webhook configuration — non-fatal so the number is
|
|
1407
|
-
// still usable even if ingress isn't configured yet.
|
|
1408
|
-
const webhookResult = await syncTwilioWebhooks(
|
|
1409
|
-
purchased.phoneNumber,
|
|
1410
|
-
accountSid,
|
|
1411
|
-
authToken,
|
|
1412
|
-
loadRawConfig() as IngressConfig,
|
|
1413
|
-
);
|
|
1414
|
-
|
|
1415
|
-
ctx.send(socket, {
|
|
1416
|
-
type: 'twilio_config_response',
|
|
1417
|
-
success: true,
|
|
1418
|
-
hasCredentials: true,
|
|
1419
|
-
phoneNumber: purchased.phoneNumber,
|
|
1420
|
-
warning: webhookResult.warning,
|
|
1421
|
-
});
|
|
1422
|
-
} else if (msg.action === 'assign_number') {
|
|
1423
|
-
if (!msg.phoneNumber) {
|
|
1424
|
-
ctx.send(socket, {
|
|
1425
|
-
type: 'twilio_config_response',
|
|
1426
|
-
success: false,
|
|
1427
|
-
hasCredentials: hasTwilioCredentials(),
|
|
1428
|
-
error: 'phoneNumber is required for assign_number action',
|
|
1429
|
-
});
|
|
1430
|
-
return;
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
// Persist the phone number in the secure credential store so the
|
|
1434
|
-
// active Twilio runtime can read it via credential:twilio:phone_number
|
|
1435
|
-
const phoneStored = setSecureKey('credential:twilio:phone_number', msg.phoneNumber);
|
|
1436
|
-
if (!phoneStored) {
|
|
1437
|
-
ctx.send(socket, {
|
|
1438
|
-
type: 'twilio_config_response',
|
|
1439
|
-
success: false,
|
|
1440
|
-
hasCredentials: hasTwilioCredentials(),
|
|
1441
|
-
error: 'Failed to store phone number in secure storage',
|
|
1442
|
-
});
|
|
1443
|
-
return;
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
// Also persist in assistant config (non-secret) for the UI
|
|
1447
|
-
const raw = loadRawConfig();
|
|
1448
|
-
const sms = (raw?.sms ?? {}) as Record<string, unknown>;
|
|
1449
|
-
// When assistantId is provided, only set the legacy global phoneNumber
|
|
1450
|
-
// if it's not already set — this prevents multi-assistant assignments
|
|
1451
|
-
// from clobbering each other's outbound SMS number.
|
|
1452
|
-
if (msg.assistantId) {
|
|
1453
|
-
if (!sms.phoneNumber) {
|
|
1454
|
-
sms.phoneNumber = msg.phoneNumber;
|
|
1455
|
-
}
|
|
1456
|
-
} else {
|
|
1457
|
-
sms.phoneNumber = msg.phoneNumber;
|
|
1458
|
-
}
|
|
1459
|
-
// When assistantId is provided, also persist into the per-assistant mapping
|
|
1460
|
-
if (msg.assistantId) {
|
|
1461
|
-
const mapping = (sms.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
|
|
1462
|
-
mapping[msg.assistantId] = msg.phoneNumber;
|
|
1463
|
-
sms.assistantPhoneNumbers = mapping;
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
const wasSuppressed = ctx.suppressConfigReload;
|
|
1467
|
-
ctx.setSuppressConfigReload(true);
|
|
1468
|
-
try {
|
|
1469
|
-
saveRawConfig({ ...raw, sms });
|
|
1470
|
-
} catch (err) {
|
|
1471
|
-
ctx.setSuppressConfigReload(wasSuppressed);
|
|
1472
|
-
throw err;
|
|
1473
|
-
}
|
|
1474
|
-
ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
1475
|
-
|
|
1476
|
-
// Best-effort webhook configuration when credentials are available
|
|
1477
|
-
let webhookWarning: string | undefined;
|
|
1478
|
-
if (hasTwilioCredentials()) {
|
|
1479
|
-
const acctSid = getSecureKey('credential:twilio:account_sid')!;
|
|
1480
|
-
const acctToken = getSecureKey('credential:twilio:auth_token')!;
|
|
1481
|
-
const webhookResult = await syncTwilioWebhooks(
|
|
1482
|
-
msg.phoneNumber,
|
|
1483
|
-
acctSid,
|
|
1484
|
-
acctToken,
|
|
1485
|
-
loadRawConfig() as IngressConfig,
|
|
1486
|
-
);
|
|
1487
|
-
webhookWarning = webhookResult.warning;
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
ctx.send(socket, {
|
|
1491
|
-
type: 'twilio_config_response',
|
|
1492
|
-
success: true,
|
|
1493
|
-
hasCredentials: hasTwilioCredentials(),
|
|
1494
|
-
phoneNumber: msg.phoneNumber,
|
|
1495
|
-
warning: webhookWarning,
|
|
1496
|
-
});
|
|
1497
|
-
} else if (msg.action === 'list_numbers') {
|
|
1498
|
-
if (!hasTwilioCredentials()) {
|
|
1499
|
-
ctx.send(socket, {
|
|
1500
|
-
type: 'twilio_config_response',
|
|
1501
|
-
success: false,
|
|
1502
|
-
hasCredentials: false,
|
|
1503
|
-
error: 'Twilio credentials not configured. Set credentials first.',
|
|
1504
|
-
});
|
|
1505
|
-
return;
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
const accountSid = getSecureKey('credential:twilio:account_sid')!;
|
|
1509
|
-
const authToken = getSecureKey('credential:twilio:auth_token')!;
|
|
1510
|
-
const numbers = await listIncomingPhoneNumbers(accountSid, authToken);
|
|
1511
|
-
|
|
1512
|
-
ctx.send(socket, {
|
|
1513
|
-
type: 'twilio_config_response',
|
|
1514
|
-
success: true,
|
|
1515
|
-
hasCredentials: true,
|
|
1516
|
-
numbers,
|
|
1517
|
-
});
|
|
1518
|
-
} else if (msg.action === 'sms_compliance_status') {
|
|
1519
|
-
if (!hasTwilioCredentials()) {
|
|
1520
|
-
ctx.send(socket, {
|
|
1521
|
-
type: 'twilio_config_response',
|
|
1522
|
-
success: false,
|
|
1523
|
-
hasCredentials: false,
|
|
1524
|
-
error: 'Twilio credentials not configured. Set credentials first.',
|
|
1525
|
-
});
|
|
1526
|
-
return;
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
const raw = loadRawConfig();
|
|
1530
|
-
const sms = (raw?.sms ?? {}) as Record<string, unknown>;
|
|
1531
|
-
let phoneNumber: string;
|
|
1532
|
-
if (msg.assistantId) {
|
|
1533
|
-
const mapping = (sms.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
|
|
1534
|
-
phoneNumber = mapping[msg.assistantId] ?? (sms.phoneNumber as string) ?? '';
|
|
1535
|
-
} else {
|
|
1536
|
-
phoneNumber = (sms.phoneNumber as string) ?? '';
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
if (!phoneNumber) {
|
|
1540
|
-
ctx.send(socket, {
|
|
1541
|
-
type: 'twilio_config_response',
|
|
1542
|
-
success: false,
|
|
1543
|
-
hasCredentials: true,
|
|
1544
|
-
error: 'No phone number assigned. Assign a number first.',
|
|
1545
|
-
});
|
|
1546
|
-
return;
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
const accountSid = getSecureKey('credential:twilio:account_sid')!;
|
|
1550
|
-
const authToken = getSecureKey('credential:twilio:auth_token')!;
|
|
1551
|
-
|
|
1552
|
-
// Determine number type from prefix
|
|
1553
|
-
const tollFreePrefixes = ['+1800', '+1833', '+1844', '+1855', '+1866', '+1877', '+1888'];
|
|
1554
|
-
const isTollFree = tollFreePrefixes.some((prefix) => phoneNumber.startsWith(prefix));
|
|
1555
|
-
const numberType = isTollFree ? 'toll_free' : 'local_10dlc';
|
|
1556
|
-
|
|
1557
|
-
if (!isTollFree) {
|
|
1558
|
-
// Non-toll-free numbers don't need toll-free verification
|
|
1559
|
-
ctx.send(socket, {
|
|
1560
|
-
type: 'twilio_config_response',
|
|
1561
|
-
success: true,
|
|
1562
|
-
hasCredentials: true,
|
|
1563
|
-
phoneNumber,
|
|
1564
|
-
compliance: { numberType },
|
|
1565
|
-
});
|
|
1566
|
-
return;
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
// Look up the phone number SID and check verification status
|
|
1570
|
-
const phoneSid = await getPhoneNumberSid(accountSid, authToken, phoneNumber);
|
|
1571
|
-
if (!phoneSid) {
|
|
1572
|
-
ctx.send(socket, {
|
|
1573
|
-
type: 'twilio_config_response',
|
|
1574
|
-
success: false,
|
|
1575
|
-
hasCredentials: true,
|
|
1576
|
-
phoneNumber,
|
|
1577
|
-
error: `Phone number ${phoneNumber} not found on Twilio account`,
|
|
1578
|
-
});
|
|
1579
|
-
return;
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
const verification = await getTollFreeVerificationStatus(accountSid, authToken, phoneSid);
|
|
1583
|
-
|
|
1584
|
-
ctx.send(socket, {
|
|
1585
|
-
type: 'twilio_config_response',
|
|
1586
|
-
success: true,
|
|
1587
|
-
hasCredentials: true,
|
|
1588
|
-
phoneNumber,
|
|
1589
|
-
compliance: {
|
|
1590
|
-
numberType,
|
|
1591
|
-
verificationSid: verification?.sid,
|
|
1592
|
-
verificationStatus: verification?.status,
|
|
1593
|
-
rejectionReason: verification?.rejectionReason,
|
|
1594
|
-
rejectionReasons: verification?.rejectionReasons,
|
|
1595
|
-
errorCode: verification?.errorCode,
|
|
1596
|
-
editAllowed: verification?.editAllowed,
|
|
1597
|
-
editExpiration: verification?.editExpiration,
|
|
1598
|
-
},
|
|
1599
|
-
});
|
|
1600
|
-
} else if (msg.action === 'sms_submit_tollfree_verification') {
|
|
1601
|
-
if (!hasTwilioCredentials()) {
|
|
1602
|
-
ctx.send(socket, {
|
|
1603
|
-
type: 'twilio_config_response',
|
|
1604
|
-
success: false,
|
|
1605
|
-
hasCredentials: false,
|
|
1606
|
-
error: 'Twilio credentials not configured. Set credentials first.',
|
|
1607
|
-
});
|
|
1608
|
-
return;
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
const vp = msg.verificationParams;
|
|
1612
|
-
if (!vp) {
|
|
1613
|
-
ctx.send(socket, {
|
|
1614
|
-
type: 'twilio_config_response',
|
|
1615
|
-
success: false,
|
|
1616
|
-
hasCredentials: true,
|
|
1617
|
-
error: 'verificationParams is required for sms_submit_tollfree_verification action',
|
|
1618
|
-
});
|
|
1619
|
-
return;
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
// Validate required fields
|
|
1623
|
-
const requiredFields: Array<[string, unknown]> = [
|
|
1624
|
-
['tollfreePhoneNumberSid', vp.tollfreePhoneNumberSid],
|
|
1625
|
-
['businessName', vp.businessName],
|
|
1626
|
-
['businessWebsite', vp.businessWebsite],
|
|
1627
|
-
['notificationEmail', vp.notificationEmail],
|
|
1628
|
-
['useCaseCategories', vp.useCaseCategories],
|
|
1629
|
-
['useCaseSummary', vp.useCaseSummary],
|
|
1630
|
-
['productionMessageSample', vp.productionMessageSample],
|
|
1631
|
-
['optInImageUrls', vp.optInImageUrls],
|
|
1632
|
-
['optInType', vp.optInType],
|
|
1633
|
-
['messageVolume', vp.messageVolume],
|
|
1634
|
-
];
|
|
1635
|
-
|
|
1636
|
-
const missing = requiredFields
|
|
1637
|
-
.filter(([, v]) => v === undefined || v === null || v === '' || (Array.isArray(v) && v.length === 0))
|
|
1638
|
-
.map(([name]) => name);
|
|
1639
|
-
|
|
1640
|
-
if (missing.length > 0) {
|
|
1641
|
-
ctx.send(socket, {
|
|
1642
|
-
type: 'twilio_config_response',
|
|
1643
|
-
success: false,
|
|
1644
|
-
hasCredentials: true,
|
|
1645
|
-
error: `Missing required verification fields: ${missing.join(', ')}`,
|
|
1646
|
-
});
|
|
1647
|
-
return;
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
// Validate enum values
|
|
1651
|
-
const validUseCaseCategories = [
|
|
1652
|
-
'TWO_FACTOR_AUTHENTICATION', 'ACCOUNT_NOTIFICATION', 'CUSTOMER_CARE',
|
|
1653
|
-
'DELIVERY_NOTIFICATION', 'FRAUD_ALERT', 'HIGHER_EDUCATION', 'MARKETING',
|
|
1654
|
-
'POLLING_AND_VOTING', 'PUBLIC_SERVICE_ANNOUNCEMENT', 'SECURITY_ALERT',
|
|
1655
|
-
];
|
|
1656
|
-
const invalidCategories = (vp.useCaseCategories ?? []).filter((c) => !validUseCaseCategories.includes(c));
|
|
1657
|
-
if (invalidCategories.length > 0) {
|
|
1658
|
-
ctx.send(socket, {
|
|
1659
|
-
type: 'twilio_config_response',
|
|
1660
|
-
success: false,
|
|
1661
|
-
hasCredentials: true,
|
|
1662
|
-
error: `Invalid useCaseCategories: ${invalidCategories.join(', ')}. Valid values: ${validUseCaseCategories.join(', ')}`,
|
|
1663
|
-
});
|
|
1664
|
-
return;
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
const validOptInTypes = ['VERBAL', 'WEB_FORM', 'PAPER_FORM', 'VIA_TEXT', 'MOBILE_QR_CODE'];
|
|
1668
|
-
if (!validOptInTypes.includes(vp.optInType!)) {
|
|
1669
|
-
ctx.send(socket, {
|
|
1670
|
-
type: 'twilio_config_response',
|
|
1671
|
-
success: false,
|
|
1672
|
-
hasCredentials: true,
|
|
1673
|
-
error: `Invalid optInType: ${vp.optInType}. Valid values: ${validOptInTypes.join(', ')}`,
|
|
1674
|
-
});
|
|
1675
|
-
return;
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
const validMessageVolumes = [
|
|
1679
|
-
'10', '100', '1,000', '10,000', '100,000', '250,000',
|
|
1680
|
-
'500,000', '750,000', '1,000,000', '5,000,000', '10,000,000+',
|
|
1681
|
-
];
|
|
1682
|
-
if (!validMessageVolumes.includes(vp.messageVolume!)) {
|
|
1683
|
-
ctx.send(socket, {
|
|
1684
|
-
type: 'twilio_config_response',
|
|
1685
|
-
success: false,
|
|
1686
|
-
hasCredentials: true,
|
|
1687
|
-
error: `Invalid messageVolume: ${vp.messageVolume}. Valid values: ${validMessageVolumes.join(', ')}`,
|
|
1688
|
-
});
|
|
1689
|
-
return;
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
const accountSid = getSecureKey('credential:twilio:account_sid')!;
|
|
1693
|
-
const authToken = getSecureKey('credential:twilio:auth_token')!;
|
|
1694
|
-
|
|
1695
|
-
const submitParams: TollFreeVerificationSubmitParams = {
|
|
1696
|
-
tollfreePhoneNumberSid: vp.tollfreePhoneNumberSid!,
|
|
1697
|
-
businessName: vp.businessName!,
|
|
1698
|
-
businessWebsite: vp.businessWebsite!,
|
|
1699
|
-
notificationEmail: vp.notificationEmail!,
|
|
1700
|
-
useCaseCategories: vp.useCaseCategories!,
|
|
1701
|
-
useCaseSummary: vp.useCaseSummary!,
|
|
1702
|
-
productionMessageSample: vp.productionMessageSample!,
|
|
1703
|
-
optInImageUrls: vp.optInImageUrls!,
|
|
1704
|
-
optInType: vp.optInType!,
|
|
1705
|
-
messageVolume: vp.messageVolume!,
|
|
1706
|
-
businessType: vp.businessType ?? 'SOLE_PROPRIETOR',
|
|
1707
|
-
customerProfileSid: vp.customerProfileSid,
|
|
1708
|
-
};
|
|
1709
|
-
|
|
1710
|
-
const verification = await submitTollFreeVerification(accountSid, authToken, submitParams);
|
|
1711
|
-
|
|
1712
|
-
ctx.send(socket, {
|
|
1713
|
-
type: 'twilio_config_response',
|
|
1714
|
-
success: true,
|
|
1715
|
-
hasCredentials: true,
|
|
1716
|
-
compliance: {
|
|
1717
|
-
numberType: 'toll_free',
|
|
1718
|
-
verificationSid: verification.sid,
|
|
1719
|
-
verificationStatus: verification.status,
|
|
1720
|
-
},
|
|
1721
|
-
});
|
|
1722
|
-
} else if (msg.action === 'sms_update_tollfree_verification') {
|
|
1723
|
-
if (!hasTwilioCredentials()) {
|
|
1724
|
-
ctx.send(socket, {
|
|
1725
|
-
type: 'twilio_config_response',
|
|
1726
|
-
success: false,
|
|
1727
|
-
hasCredentials: false,
|
|
1728
|
-
error: 'Twilio credentials not configured. Set credentials first.',
|
|
1729
|
-
});
|
|
1730
|
-
return;
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
if (!msg.verificationSid) {
|
|
1734
|
-
ctx.send(socket, {
|
|
1735
|
-
type: 'twilio_config_response',
|
|
1736
|
-
success: false,
|
|
1737
|
-
hasCredentials: true,
|
|
1738
|
-
error: 'verificationSid is required for sms_update_tollfree_verification action',
|
|
1739
|
-
});
|
|
1740
|
-
return;
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
const accountSid = getSecureKey('credential:twilio:account_sid')!;
|
|
1744
|
-
const authToken = getSecureKey('credential:twilio:auth_token')!;
|
|
1745
|
-
|
|
1746
|
-
const verification = await updateTollFreeVerification(
|
|
1747
|
-
accountSid,
|
|
1748
|
-
authToken,
|
|
1749
|
-
msg.verificationSid,
|
|
1750
|
-
msg.verificationParams ?? {},
|
|
1751
|
-
);
|
|
1752
|
-
|
|
1753
|
-
ctx.send(socket, {
|
|
1754
|
-
type: 'twilio_config_response',
|
|
1755
|
-
success: true,
|
|
1756
|
-
hasCredentials: true,
|
|
1757
|
-
compliance: {
|
|
1758
|
-
numberType: 'toll_free',
|
|
1759
|
-
verificationSid: verification.sid,
|
|
1760
|
-
verificationStatus: verification.status,
|
|
1761
|
-
editAllowed: verification.editAllowed,
|
|
1762
|
-
editExpiration: verification.editExpiration,
|
|
1763
|
-
},
|
|
1764
|
-
});
|
|
1765
|
-
} else if (msg.action === 'sms_delete_tollfree_verification') {
|
|
1766
|
-
if (!hasTwilioCredentials()) {
|
|
1767
|
-
ctx.send(socket, {
|
|
1768
|
-
type: 'twilio_config_response',
|
|
1769
|
-
success: false,
|
|
1770
|
-
hasCredentials: false,
|
|
1771
|
-
error: 'Twilio credentials not configured. Set credentials first.',
|
|
1772
|
-
});
|
|
1773
|
-
return;
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
if (!msg.verificationSid) {
|
|
1777
|
-
ctx.send(socket, {
|
|
1778
|
-
type: 'twilio_config_response',
|
|
1779
|
-
success: false,
|
|
1780
|
-
hasCredentials: true,
|
|
1781
|
-
error: 'verificationSid is required for sms_delete_tollfree_verification action',
|
|
1782
|
-
});
|
|
1783
|
-
return;
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
const accountSid = getSecureKey('credential:twilio:account_sid')!;
|
|
1787
|
-
const authToken = getSecureKey('credential:twilio:auth_token')!;
|
|
1788
|
-
|
|
1789
|
-
await deleteTollFreeVerification(accountSid, authToken, msg.verificationSid);
|
|
1790
|
-
|
|
1791
|
-
ctx.send(socket, {
|
|
1792
|
-
type: 'twilio_config_response',
|
|
1793
|
-
success: true,
|
|
1794
|
-
hasCredentials: true,
|
|
1795
|
-
warning: 'Toll-free verification deleted. Re-submitting may reset your position in the review queue.',
|
|
1796
|
-
});
|
|
1797
|
-
} else if (msg.action === 'release_number') {
|
|
1798
|
-
if (!hasTwilioCredentials()) {
|
|
1799
|
-
ctx.send(socket, {
|
|
1800
|
-
type: 'twilio_config_response',
|
|
1801
|
-
success: false,
|
|
1802
|
-
hasCredentials: false,
|
|
1803
|
-
error: 'Twilio credentials not configured. Set credentials first.',
|
|
1804
|
-
});
|
|
1805
|
-
return;
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
const raw = loadRawConfig();
|
|
1809
|
-
const sms = (raw?.sms ?? {}) as Record<string, unknown>;
|
|
1810
|
-
let phoneNumber: string;
|
|
1811
|
-
if (msg.phoneNumber) {
|
|
1812
|
-
phoneNumber = msg.phoneNumber;
|
|
1813
|
-
} else if (msg.assistantId) {
|
|
1814
|
-
const mapping = (sms.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
|
|
1815
|
-
phoneNumber = mapping[msg.assistantId] ?? (sms.phoneNumber as string) ?? '';
|
|
1816
|
-
} else {
|
|
1817
|
-
phoneNumber = (sms.phoneNumber as string) ?? '';
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
if (!phoneNumber) {
|
|
1821
|
-
ctx.send(socket, {
|
|
1822
|
-
type: 'twilio_config_response',
|
|
1823
|
-
success: false,
|
|
1824
|
-
hasCredentials: true,
|
|
1825
|
-
error: 'No phone number to release. Specify phoneNumber or ensure one is assigned.',
|
|
1826
|
-
});
|
|
1827
|
-
return;
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
|
-
const accountSid = getSecureKey('credential:twilio:account_sid')!;
|
|
1831
|
-
const authToken = getSecureKey('credential:twilio:auth_token')!;
|
|
1832
|
-
|
|
1833
|
-
await releasePhoneNumber(accountSid, authToken, phoneNumber);
|
|
1834
|
-
|
|
1835
|
-
// Clear the number from config and secure key store
|
|
1836
|
-
if (sms.phoneNumber === phoneNumber) {
|
|
1837
|
-
delete sms.phoneNumber;
|
|
1838
|
-
}
|
|
1839
|
-
const assistantPhoneNumbers = sms.assistantPhoneNumbers as Record<string, string> | undefined;
|
|
1840
|
-
if (assistantPhoneNumbers) {
|
|
1841
|
-
for (const [id, num] of Object.entries(assistantPhoneNumbers)) {
|
|
1842
|
-
if (num === phoneNumber) {
|
|
1843
|
-
delete assistantPhoneNumbers[id];
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
if (Object.keys(assistantPhoneNumbers).length === 0) {
|
|
1847
|
-
delete sms.assistantPhoneNumbers;
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
const wasSuppressed = ctx.suppressConfigReload;
|
|
1852
|
-
ctx.setSuppressConfigReload(true);
|
|
1853
|
-
try {
|
|
1854
|
-
saveRawConfig({ ...raw, sms });
|
|
1855
|
-
} catch (err) {
|
|
1856
|
-
ctx.setSuppressConfigReload(wasSuppressed);
|
|
1857
|
-
throw err;
|
|
1858
|
-
}
|
|
1859
|
-
ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
1860
|
-
|
|
1861
|
-
// Clear the phone number from secure key store if it matches
|
|
1862
|
-
const storedPhone = getSecureKey('credential:twilio:phone_number');
|
|
1863
|
-
if (storedPhone === phoneNumber) {
|
|
1864
|
-
deleteSecureKey('credential:twilio:phone_number');
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
ctx.send(socket, {
|
|
1868
|
-
type: 'twilio_config_response',
|
|
1869
|
-
success: true,
|
|
1870
|
-
hasCredentials: true,
|
|
1871
|
-
warning: 'Phone number released from Twilio. Any associated toll-free verification context is lost.',
|
|
1872
|
-
});
|
|
1873
|
-
} else if (msg.action === 'sms_send_test') {
|
|
1874
|
-
// ── SMS send test ────────────────────────────────────────────────
|
|
1875
|
-
if (!hasTwilioCredentials()) {
|
|
1876
|
-
ctx.send(socket, {
|
|
1877
|
-
type: 'twilio_config_response',
|
|
1878
|
-
success: false,
|
|
1879
|
-
hasCredentials: false,
|
|
1880
|
-
error: 'Twilio credentials not configured. Set credentials first.',
|
|
1881
|
-
});
|
|
1882
|
-
return;
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
const to = msg.phoneNumber;
|
|
1886
|
-
if (!to) {
|
|
1887
|
-
ctx.send(socket, {
|
|
1888
|
-
type: 'twilio_config_response',
|
|
1889
|
-
success: false,
|
|
1890
|
-
hasCredentials: true,
|
|
1891
|
-
error: 'phoneNumber is required for sms_send_test action.',
|
|
1892
|
-
});
|
|
1893
|
-
return;
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
const raw = loadRawConfig();
|
|
1897
|
-
const smsSection = (raw?.sms ?? {}) as Record<string, unknown>;
|
|
1898
|
-
const from = (smsSection.phoneNumber as string | undefined)
|
|
1899
|
-
|| getSecureKey('credential:twilio:phone_number')
|
|
1900
|
-
|| '';
|
|
1901
|
-
if (!from) {
|
|
1902
|
-
ctx.send(socket, {
|
|
1903
|
-
type: 'twilio_config_response',
|
|
1904
|
-
success: false,
|
|
1905
|
-
hasCredentials: true,
|
|
1906
|
-
error: 'No phone number assigned. Run the twilio-setup skill to assign a number.',
|
|
1907
|
-
});
|
|
1908
|
-
return;
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
const accountSid = getSecureKey('credential:twilio:account_sid')!;
|
|
1912
|
-
const authToken = getSecureKey('credential:twilio:auth_token')!;
|
|
1913
|
-
const text = msg.text || 'Test SMS from your Vellum assistant';
|
|
1914
|
-
|
|
1915
|
-
// Send via gateway's /deliver/sms endpoint
|
|
1916
|
-
const bearerToken = readHttpToken();
|
|
1917
|
-
const gatewayPort = Number(process.env.GATEWAY_PORT) || 7830;
|
|
1918
|
-
const gatewayUrl = process.env.GATEWAY_INTERNAL_BASE_URL?.replace(/\/+$/, '') || `http://127.0.0.1:${gatewayPort}`;
|
|
1919
|
-
|
|
1920
|
-
const sendResp = await fetch(`${gatewayUrl}/deliver/sms`, {
|
|
1921
|
-
method: 'POST',
|
|
1922
|
-
headers: {
|
|
1923
|
-
'Content-Type': 'application/json',
|
|
1924
|
-
...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
|
|
1925
|
-
},
|
|
1926
|
-
body: JSON.stringify({ to, text }),
|
|
1927
|
-
signal: AbortSignal.timeout(30_000),
|
|
1928
|
-
});
|
|
1929
|
-
|
|
1930
|
-
if (!sendResp.ok) {
|
|
1931
|
-
const errBody = await sendResp.text().catch(() => '<unreadable>');
|
|
1932
|
-
ctx.send(socket, {
|
|
1933
|
-
type: 'twilio_config_response',
|
|
1934
|
-
success: false,
|
|
1935
|
-
hasCredentials: true,
|
|
1936
|
-
error: `SMS send failed (${sendResp.status}): ${errBody}`,
|
|
1937
|
-
});
|
|
1938
|
-
return;
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
const sendData = await sendResp.json().catch(() => ({})) as {
|
|
1942
|
-
messageSid?: string;
|
|
1943
|
-
status?: string;
|
|
1944
|
-
};
|
|
1945
|
-
const messageSid = sendData.messageSid || '';
|
|
1946
|
-
const initialStatus = sendData.status || 'unknown';
|
|
1947
|
-
|
|
1948
|
-
// Poll Twilio for final status (up to 3 times, 2s apart)
|
|
1949
|
-
let finalStatus = initialStatus;
|
|
1950
|
-
let errorCode: string | undefined;
|
|
1951
|
-
let errorMessage: string | undefined;
|
|
1952
|
-
|
|
1953
|
-
if (messageSid) {
|
|
1954
|
-
for (let i = 0; i < 3; i++) {
|
|
1955
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
1956
|
-
try {
|
|
1957
|
-
const pollResult = await fetchMessageStatus(accountSid, authToken, messageSid);
|
|
1958
|
-
finalStatus = pollResult.status;
|
|
1959
|
-
errorCode = pollResult.errorCode;
|
|
1960
|
-
errorMessage = pollResult.errorMessage;
|
|
1961
|
-
// Stop polling if we've reached a terminal status
|
|
1962
|
-
if (['delivered', 'undelivered', 'failed'].includes(finalStatus)) break;
|
|
1963
|
-
} catch {
|
|
1964
|
-
// Polling failure is non-fatal; we'll use the last known status
|
|
1965
|
-
break;
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
|
-
const testResult = {
|
|
1971
|
-
messageSid,
|
|
1972
|
-
to,
|
|
1973
|
-
initialStatus,
|
|
1974
|
-
finalStatus,
|
|
1975
|
-
...(errorCode ? { errorCode } : {}),
|
|
1976
|
-
...(errorMessage ? { errorMessage } : {}),
|
|
1977
|
-
};
|
|
1978
|
-
|
|
1979
|
-
// Store for sms_doctor
|
|
1980
|
-
_lastTestResult = { ...testResult, timestamp: Date.now() };
|
|
1981
|
-
|
|
1982
|
-
ctx.send(socket, {
|
|
1983
|
-
type: 'twilio_config_response',
|
|
1984
|
-
success: true,
|
|
1985
|
-
hasCredentials: true,
|
|
1986
|
-
testResult,
|
|
1987
|
-
});
|
|
1988
|
-
|
|
1989
|
-
} else if (msg.action === 'sms_doctor') {
|
|
1990
|
-
// ── SMS doctor diagnostic ────────────────────────────────────────
|
|
1991
|
-
const hasCredentials = hasTwilioCredentials();
|
|
1992
|
-
|
|
1993
|
-
// 1. Channel readiness check
|
|
1994
|
-
let readinessReady = false;
|
|
1995
|
-
const readinessIssues: string[] = [];
|
|
1996
|
-
try {
|
|
1997
|
-
const readinessService = getReadinessService();
|
|
1998
|
-
const snapshot = await readinessService.getReadiness('sms', { includeRemote: false });
|
|
1999
|
-
readinessReady = snapshot.ready;
|
|
2000
|
-
for (const r of snapshot.reasons) {
|
|
2001
|
-
readinessIssues.push(r.text);
|
|
2002
|
-
}
|
|
2003
|
-
} catch (err) {
|
|
2004
|
-
readinessIssues.push(`Readiness check failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2005
|
-
}
|
|
2006
|
-
|
|
2007
|
-
// 2. Compliance status
|
|
2008
|
-
let complianceStatus = 'unknown';
|
|
2009
|
-
let complianceDetail: string | undefined;
|
|
2010
|
-
let complianceRemediation: string | undefined;
|
|
2011
|
-
if (hasCredentials) {
|
|
2012
|
-
try {
|
|
2013
|
-
const raw = loadRawConfig();
|
|
2014
|
-
const smsSection = (raw?.sms ?? {}) as Record<string, unknown>;
|
|
2015
|
-
const phoneNumber = (smsSection.phoneNumber as string | undefined) || getSecureKey('credential:twilio:phone_number') || '';
|
|
2016
|
-
if (phoneNumber) {
|
|
2017
|
-
const accountSid = getSecureKey('credential:twilio:account_sid')!;
|
|
2018
|
-
const authToken = getSecureKey('credential:twilio:auth_token')!;
|
|
2019
|
-
// Determine number type and verification status
|
|
2020
|
-
const isTollFree = phoneNumber.startsWith('+1') && ['800','888','877','866','855','844','833'].some(
|
|
2021
|
-
(p) => phoneNumber.startsWith(`+1${p}`),
|
|
2022
|
-
);
|
|
2023
|
-
if (isTollFree) {
|
|
2024
|
-
try {
|
|
2025
|
-
const verification = await getTollFreeVerificationStatus(accountSid, authToken, phoneNumber);
|
|
2026
|
-
if (verification) {
|
|
2027
|
-
const status = verification.status;
|
|
2028
|
-
complianceStatus = status;
|
|
2029
|
-
complianceDetail = `Toll-free verification: ${status}`;
|
|
2030
|
-
if (status === 'TWILIO_APPROVED') {
|
|
2031
|
-
complianceRemediation = undefined;
|
|
2032
|
-
} else if (status === 'PENDING_REVIEW' || status === 'IN_REVIEW') {
|
|
2033
|
-
complianceRemediation = 'Toll-free verification is pending. Messaging may have limited throughput until approved.';
|
|
2034
|
-
} else if (status === 'TWILIO_REJECTED') {
|
|
2035
|
-
complianceRemediation = 'Toll-free verification was rejected. Check rejection reasons and resubmit.';
|
|
2036
|
-
} else {
|
|
2037
|
-
complianceRemediation = 'Submit a toll-free verification to enable full messaging throughput.';
|
|
2038
|
-
}
|
|
2039
|
-
} else {
|
|
2040
|
-
complianceStatus = 'unverified';
|
|
2041
|
-
complianceDetail = 'Toll-free number without verification';
|
|
2042
|
-
complianceRemediation = 'Submit a toll-free verification request to avoid filtering.';
|
|
2043
|
-
}
|
|
2044
|
-
} catch {
|
|
2045
|
-
complianceStatus = 'check_failed';
|
|
2046
|
-
complianceDetail = 'Could not retrieve toll-free verification status';
|
|
2047
|
-
}
|
|
2048
|
-
} else {
|
|
2049
|
-
complianceStatus = 'local_10dlc';
|
|
2050
|
-
complianceDetail = 'Local/10DLC number — carrier registration handled externally';
|
|
2051
|
-
}
|
|
2052
|
-
} else {
|
|
2053
|
-
complianceStatus = 'no_number';
|
|
2054
|
-
complianceDetail = 'No phone number assigned';
|
|
2055
|
-
complianceRemediation = 'Assign a phone number via the twilio-setup skill.';
|
|
2056
|
-
}
|
|
2057
|
-
} catch {
|
|
2058
|
-
complianceStatus = 'check_failed';
|
|
2059
|
-
complianceDetail = 'Could not determine compliance status';
|
|
2060
|
-
}
|
|
2061
|
-
} else {
|
|
2062
|
-
complianceStatus = 'no_credentials';
|
|
2063
|
-
complianceDetail = 'Twilio credentials are not configured';
|
|
2064
|
-
complianceRemediation = 'Set Twilio credentials via the twilio-setup skill.';
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
// 3. Last send test result
|
|
2068
|
-
let lastSend: { status: string; errorCode?: string; remediation?: string } | undefined;
|
|
2069
|
-
if (_lastTestResult) {
|
|
2070
|
-
lastSend = {
|
|
2071
|
-
status: _lastTestResult.finalStatus,
|
|
2072
|
-
...((_lastTestResult.errorCode) ? { errorCode: _lastTestResult.errorCode } : {}),
|
|
2073
|
-
...((_lastTestResult.errorCode) ? { remediation: mapTwilioErrorRemediation(_lastTestResult.errorCode) } : {}),
|
|
2074
|
-
};
|
|
2075
|
-
}
|
|
2076
|
-
|
|
2077
|
-
// 4. Determine overall status
|
|
2078
|
-
const actionItems: string[] = [];
|
|
2079
|
-
let overallStatus: 'healthy' | 'degraded' | 'broken' = 'healthy';
|
|
2080
|
-
|
|
2081
|
-
if (!hasCredentials) {
|
|
2082
|
-
overallStatus = 'broken';
|
|
2083
|
-
actionItems.push('Configure Twilio credentials.');
|
|
2084
|
-
}
|
|
2085
|
-
if (!readinessReady) {
|
|
2086
|
-
overallStatus = 'broken';
|
|
2087
|
-
for (const issue of readinessIssues) actionItems.push(issue);
|
|
2088
|
-
}
|
|
2089
|
-
if (complianceStatus === 'unverified' || complianceStatus === 'PENDING_REVIEW' || complianceStatus === 'IN_REVIEW') {
|
|
2090
|
-
if (overallStatus === 'healthy') overallStatus = 'degraded';
|
|
2091
|
-
if (complianceRemediation) actionItems.push(complianceRemediation);
|
|
2092
|
-
}
|
|
2093
|
-
if (complianceStatus === 'TWILIO_REJECTED' || complianceStatus === 'no_number') {
|
|
2094
|
-
overallStatus = 'broken';
|
|
2095
|
-
if (complianceRemediation) actionItems.push(complianceRemediation);
|
|
2096
|
-
}
|
|
2097
|
-
if (_lastTestResult && ['failed', 'undelivered'].includes(_lastTestResult.finalStatus)) {
|
|
2098
|
-
if (overallStatus === 'healthy') overallStatus = 'degraded';
|
|
2099
|
-
const remediation = mapTwilioErrorRemediation(_lastTestResult.errorCode);
|
|
2100
|
-
actionItems.push(remediation || `Last test SMS ${_lastTestResult.finalStatus}. Check Twilio logs for details.`);
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
ctx.send(socket, {
|
|
2104
|
-
type: 'twilio_config_response',
|
|
2105
|
-
success: true,
|
|
2106
|
-
hasCredentials,
|
|
2107
|
-
diagnostics: {
|
|
2108
|
-
readiness: { ready: readinessReady, issues: readinessIssues },
|
|
2109
|
-
compliance: {
|
|
2110
|
-
status: complianceStatus,
|
|
2111
|
-
...(complianceDetail ? { detail: complianceDetail } : {}),
|
|
2112
|
-
...(complianceRemediation ? { remediation: complianceRemediation } : {}),
|
|
2113
|
-
},
|
|
2114
|
-
...(lastSend ? { lastSend } : {}),
|
|
2115
|
-
overallStatus,
|
|
2116
|
-
actionItems,
|
|
2117
|
-
},
|
|
2118
|
-
});
|
|
2119
|
-
|
|
2120
|
-
} else {
|
|
2121
|
-
ctx.send(socket, {
|
|
2122
|
-
type: 'twilio_config_response',
|
|
2123
|
-
success: false,
|
|
2124
|
-
hasCredentials: hasTwilioCredentials(),
|
|
2125
|
-
error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}`,
|
|
2126
|
-
});
|
|
2127
|
-
}
|
|
2128
|
-
} catch (err) {
|
|
2129
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2130
|
-
log.error({ err }, 'Failed to handle Twilio config');
|
|
2131
|
-
ctx.send(socket, {
|
|
2132
|
-
type: 'twilio_config_response',
|
|
2133
|
-
success: false,
|
|
2134
|
-
hasCredentials: hasTwilioCredentials(),
|
|
2135
|
-
error: message,
|
|
2136
|
-
});
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
|
|
2140
|
-
export function handleGuardianVerification(
|
|
2141
|
-
msg: GuardianVerificationRequest,
|
|
2142
|
-
socket: net.Socket,
|
|
2143
|
-
ctx: HandlerContext,
|
|
2144
|
-
): void {
|
|
2145
|
-
// Use the assistant ID from the request when available; fall back to
|
|
2146
|
-
// 'self' for backward compatibility with single-assistant mode.
|
|
2147
|
-
const assistantId = msg.assistantId ?? 'self';
|
|
2148
|
-
const channel = msg.channel ?? 'telegram';
|
|
2149
|
-
|
|
2150
|
-
try {
|
|
2151
|
-
if (msg.action === 'create_challenge') {
|
|
2152
|
-
const result = createVerificationChallenge(assistantId, channel, msg.sessionId);
|
|
2153
|
-
|
|
2154
|
-
ctx.send(socket, {
|
|
2155
|
-
type: 'guardian_verification_response',
|
|
2156
|
-
success: true,
|
|
2157
|
-
secret: result.secret,
|
|
2158
|
-
instruction: result.instruction,
|
|
2159
|
-
channel,
|
|
2160
|
-
});
|
|
2161
|
-
} else if (msg.action === 'status') {
|
|
2162
|
-
const binding = getGuardianBinding(assistantId, channel);
|
|
2163
|
-
let guardianUsername: string | undefined;
|
|
2164
|
-
let guardianDisplayName: string | undefined;
|
|
2165
|
-
if (binding?.metadataJson) {
|
|
2166
|
-
try {
|
|
2167
|
-
const parsed = JSON.parse(binding.metadataJson) as Record<string, unknown>;
|
|
2168
|
-
if (typeof parsed.username === 'string' && parsed.username.trim().length > 0) {
|
|
2169
|
-
guardianUsername = parsed.username.trim();
|
|
2170
|
-
}
|
|
2171
|
-
if (typeof parsed.displayName === 'string' && parsed.displayName.trim().length > 0) {
|
|
2172
|
-
guardianDisplayName = parsed.displayName.trim();
|
|
2173
|
-
}
|
|
2174
|
-
} catch {
|
|
2175
|
-
// ignore malformed metadata
|
|
2176
|
-
}
|
|
2177
|
-
}
|
|
2178
|
-
if (binding?.guardianDeliveryChatId && (!guardianUsername || !guardianDisplayName)) {
|
|
2179
|
-
const ext = externalConversationStore.getBindingByChannelChat(
|
|
2180
|
-
channel,
|
|
2181
|
-
binding.guardianDeliveryChatId,
|
|
2182
|
-
);
|
|
2183
|
-
if (!guardianUsername && ext?.username) {
|
|
2184
|
-
guardianUsername = ext.username;
|
|
2185
|
-
}
|
|
2186
|
-
if (!guardianDisplayName && ext?.displayName) {
|
|
2187
|
-
guardianDisplayName = ext.displayName;
|
|
2188
|
-
}
|
|
2189
|
-
}
|
|
2190
|
-
ctx.send(socket, {
|
|
2191
|
-
type: 'guardian_verification_response',
|
|
2192
|
-
success: true,
|
|
2193
|
-
bound: binding !== null,
|
|
2194
|
-
guardianExternalUserId: binding?.guardianExternalUserId,
|
|
2195
|
-
guardianUsername,
|
|
2196
|
-
guardianDisplayName,
|
|
2197
|
-
channel,
|
|
2198
|
-
assistantId,
|
|
2199
|
-
guardianDeliveryChatId: binding?.guardianDeliveryChatId,
|
|
2200
|
-
});
|
|
2201
|
-
} else if (msg.action === 'revoke') {
|
|
2202
|
-
revokeGuardianBinding(assistantId, channel);
|
|
2203
|
-
ctx.send(socket, {
|
|
2204
|
-
type: 'guardian_verification_response',
|
|
2205
|
-
success: true,
|
|
2206
|
-
bound: false,
|
|
2207
|
-
channel,
|
|
2208
|
-
});
|
|
2209
|
-
} else {
|
|
2210
|
-
ctx.send(socket, {
|
|
2211
|
-
type: 'guardian_verification_response',
|
|
2212
|
-
success: false,
|
|
2213
|
-
error: `Unknown action: ${String(msg.action)}`,
|
|
2214
|
-
channel,
|
|
2215
|
-
});
|
|
2216
|
-
}
|
|
2217
|
-
} catch (err) {
|
|
2218
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2219
|
-
log.error({ err }, 'Failed to handle guardian verification');
|
|
2220
|
-
ctx.send(socket, {
|
|
2221
|
-
type: 'guardian_verification_response',
|
|
2222
|
-
success: false,
|
|
2223
|
-
error: message,
|
|
2224
|
-
channel,
|
|
2225
|
-
});
|
|
2226
|
-
}
|
|
2227
|
-
}
|
|
2228
|
-
|
|
2229
|
-
// Lazy singleton — created on first use so module-load stays lightweight.
|
|
2230
|
-
let _readinessService: ChannelReadinessService | undefined;
|
|
2231
|
-
function getReadinessService(): ChannelReadinessService {
|
|
2232
|
-
if (!_readinessService) {
|
|
2233
|
-
_readinessService = createReadinessService();
|
|
2234
|
-
}
|
|
2235
|
-
return _readinessService;
|
|
2236
|
-
}
|
|
2237
|
-
|
|
2238
|
-
export async function handleChannelReadiness(
|
|
2239
|
-
msg: ChannelReadinessRequest,
|
|
2240
|
-
socket: net.Socket,
|
|
2241
|
-
ctx: HandlerContext,
|
|
2242
|
-
): Promise<void> {
|
|
2243
|
-
try {
|
|
2244
|
-
const service = getReadinessService();
|
|
2245
|
-
|
|
2246
|
-
if (msg.action === 'refresh') {
|
|
2247
|
-
if (msg.channel) {
|
|
2248
|
-
service.invalidateChannel(msg.channel);
|
|
2249
|
-
} else {
|
|
2250
|
-
service.invalidateAll();
|
|
2251
|
-
}
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
|
-
const snapshots = await service.getReadiness(msg.channel, msg.includeRemote);
|
|
2255
|
-
|
|
2256
|
-
ctx.send(socket, {
|
|
2257
|
-
type: 'channel_readiness_response',
|
|
2258
|
-
success: true,
|
|
2259
|
-
snapshots: snapshots.map((s) => ({
|
|
2260
|
-
channel: s.channel,
|
|
2261
|
-
ready: s.ready,
|
|
2262
|
-
checkedAt: s.checkedAt,
|
|
2263
|
-
stale: s.stale,
|
|
2264
|
-
reasons: s.reasons,
|
|
2265
|
-
localChecks: s.localChecks,
|
|
2266
|
-
remoteChecks: s.remoteChecks,
|
|
2267
|
-
})),
|
|
2268
|
-
});
|
|
2269
|
-
} catch (err) {
|
|
2270
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2271
|
-
log.error({ err }, 'Failed to handle channel readiness');
|
|
2272
|
-
ctx.send(socket, {
|
|
2273
|
-
type: 'channel_readiness_response',
|
|
2274
|
-
success: false,
|
|
2275
|
-
error: message,
|
|
2276
|
-
});
|
|
2277
|
-
}
|
|
2278
|
-
}
|
|
2279
|
-
|
|
2280
|
-
export function handleEnvVarsRequest(socket: net.Socket, ctx: HandlerContext): void {
|
|
2281
|
-
const vars: Record<string, string> = {};
|
|
2282
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
2283
|
-
if (value !== undefined) vars[key] = value;
|
|
2284
|
-
}
|
|
2285
|
-
ctx.send(socket, { type: 'env_vars_response', vars });
|
|
2286
|
-
}
|
|
2287
|
-
|
|
2288
|
-
/**
|
|
2289
|
-
* Look up manifest metadata for a tool that isn't in the live registry.
|
|
2290
|
-
* Searches all installed skills' TOOLS.json manifests for a matching tool name.
|
|
5
|
+
* Individual handlers live in domain-specific files:
|
|
6
|
+
* config-model.ts — Model selection (LLM + image gen)
|
|
7
|
+
* config-trust.ts — Trust rules (permissions allowlist)
|
|
8
|
+
* config-scheduling.ts — Schedules & reminders
|
|
9
|
+
* config-slack.ts — Slack webhook sharing
|
|
10
|
+
* config-ingress.ts — Public ingress URL & gateway reconciliation
|
|
11
|
+
* config-integrations.ts — Vercel API & Twitter integration
|
|
12
|
+
* config-telegram.ts — Telegram bot configuration
|
|
13
|
+
* config-twilio.ts — Twilio SMS/voice configuration
|
|
14
|
+
* config-channels.ts — Channel guardian & readiness
|
|
15
|
+
* config-tools.ts — Env vars, tool permission simulation, tool names
|
|
16
|
+
* config-parental.ts — Parental control PIN + content/tool restrictions
|
|
2291
17
|
*/
|
|
2292
|
-
function resolveManifestOverride(toolName: string): ManifestOverride | undefined {
|
|
2293
|
-
if (getTool(toolName)) return undefined;
|
|
2294
|
-
try {
|
|
2295
|
-
const catalog = loadSkillCatalog();
|
|
2296
|
-
for (const skill of catalog) {
|
|
2297
|
-
if (!skill.toolManifest?.present || !skill.toolManifest.valid) continue;
|
|
2298
|
-
try {
|
|
2299
|
-
const manifest = parseToolManifestFile(join(skill.directoryPath, 'TOOLS.json'));
|
|
2300
|
-
const entry = manifest.tools.find((t) => t.name === toolName);
|
|
2301
|
-
if (entry) {
|
|
2302
|
-
return { risk: entry.risk, execution_target: entry.execution_target };
|
|
2303
|
-
}
|
|
2304
|
-
} catch {
|
|
2305
|
-
// Skip unparseable manifests
|
|
2306
|
-
}
|
|
2307
|
-
}
|
|
2308
|
-
} catch {
|
|
2309
|
-
// Non-fatal
|
|
2310
|
-
}
|
|
2311
|
-
return undefined;
|
|
2312
|
-
}
|
|
2313
|
-
|
|
2314
|
-
export async function handleToolPermissionSimulate(
|
|
2315
|
-
msg: ToolPermissionSimulateRequest,
|
|
2316
|
-
socket: net.Socket,
|
|
2317
|
-
ctx: HandlerContext,
|
|
2318
|
-
): Promise<void> {
|
|
2319
|
-
try {
|
|
2320
|
-
if (!msg.toolName || typeof msg.toolName !== 'string') {
|
|
2321
|
-
ctx.send(socket, {
|
|
2322
|
-
type: 'tool_permission_simulate_response',
|
|
2323
|
-
success: false,
|
|
2324
|
-
error: 'toolName is required',
|
|
2325
|
-
});
|
|
2326
|
-
return;
|
|
2327
|
-
}
|
|
2328
|
-
if (!msg.input || typeof msg.input !== 'object') {
|
|
2329
|
-
ctx.send(socket, {
|
|
2330
|
-
type: 'tool_permission_simulate_response',
|
|
2331
|
-
success: false,
|
|
2332
|
-
error: 'input is required and must be an object',
|
|
2333
|
-
});
|
|
2334
|
-
return;
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
const workingDir = msg.workingDir ?? process.cwd();
|
|
2338
|
-
|
|
2339
|
-
// For unregistered skill tools, resolve manifest metadata so the simulation
|
|
2340
|
-
// uses accurate risk/execution_target values instead of falling back to defaults.
|
|
2341
|
-
const manifestOverride = resolveManifestOverride(msg.toolName);
|
|
2342
|
-
|
|
2343
|
-
const executionTarget = resolveExecutionTarget(msg.toolName, manifestOverride);
|
|
2344
|
-
const policyContext = { executionTarget };
|
|
2345
|
-
|
|
2346
|
-
const riskLevel = await classifyRisk(msg.toolName, msg.input, workingDir, undefined, manifestOverride);
|
|
2347
|
-
const result = await check(msg.toolName, msg.input, workingDir, policyContext, manifestOverride);
|
|
2348
|
-
|
|
2349
|
-
// Private-thread override: promote allow → prompt for side-effect tools
|
|
2350
|
-
if (
|
|
2351
|
-
msg.forcePromptSideEffects
|
|
2352
|
-
&& result.decision === 'allow'
|
|
2353
|
-
&& isSideEffectTool(msg.toolName, msg.input)
|
|
2354
|
-
) {
|
|
2355
|
-
result.decision = 'prompt';
|
|
2356
|
-
result.reason = 'Private thread: side-effect tools require explicit approval';
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
// Non-interactive override: convert prompt → deny
|
|
2360
|
-
if (msg.isInteractive === false && result.decision === 'prompt') {
|
|
2361
|
-
result.decision = 'deny';
|
|
2362
|
-
result.reason = 'Non-interactive session: no client to approve prompt';
|
|
2363
|
-
}
|
|
2364
|
-
|
|
2365
|
-
// When decision is prompt, generate the full payload the UI needs
|
|
2366
|
-
let promptPayload: {
|
|
2367
|
-
allowlistOptions: Array<{ label: string; description: string; pattern: string }>;
|
|
2368
|
-
scopeOptions: Array<{ label: string; scope: string }>;
|
|
2369
|
-
persistentDecisionsAllowed: boolean;
|
|
2370
|
-
} | undefined;
|
|
2371
|
-
|
|
2372
|
-
if (result.decision === 'prompt') {
|
|
2373
|
-
const allowlistOptions = await generateAllowlistOptions(msg.toolName, msg.input);
|
|
2374
|
-
const scopeOptions = generateScopeOptions(workingDir, msg.toolName);
|
|
2375
|
-
const persistentDecisionsAllowed = !(
|
|
2376
|
-
msg.toolName === 'bash'
|
|
2377
|
-
&& msg.input.network_mode === 'proxied'
|
|
2378
|
-
);
|
|
2379
|
-
promptPayload = { allowlistOptions, scopeOptions, persistentDecisionsAllowed };
|
|
2380
|
-
}
|
|
2381
|
-
|
|
2382
|
-
ctx.send(socket, {
|
|
2383
|
-
type: 'tool_permission_simulate_response',
|
|
2384
|
-
success: true,
|
|
2385
|
-
decision: result.decision,
|
|
2386
|
-
riskLevel,
|
|
2387
|
-
reason: result.reason,
|
|
2388
|
-
executionTarget,
|
|
2389
|
-
matchedRuleId: result.matchedRule?.id,
|
|
2390
|
-
promptPayload,
|
|
2391
|
-
});
|
|
2392
|
-
} catch (err) {
|
|
2393
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2394
|
-
log.error({ err }, 'Failed to simulate tool permission');
|
|
2395
|
-
ctx.send(socket, {
|
|
2396
|
-
type: 'tool_permission_simulate_response',
|
|
2397
|
-
success: false,
|
|
2398
|
-
error: message,
|
|
2399
|
-
});
|
|
2400
|
-
}
|
|
2401
|
-
}
|
|
2402
|
-
|
|
2403
|
-
export function handleToolNamesList(socket: net.Socket, ctx: HandlerContext): void {
|
|
2404
|
-
const tools = getAllTools();
|
|
2405
|
-
const nameSet = new Set(tools.map((t) => t.name));
|
|
2406
|
-
const schemas: Record<string, import('../ipc-contract.js').ToolInputSchema> = {};
|
|
2407
|
-
for (const tool of tools) {
|
|
2408
|
-
try {
|
|
2409
|
-
const def = tool.getDefinition();
|
|
2410
|
-
schemas[tool.name] = def.input_schema as import('../ipc-contract.js').ToolInputSchema;
|
|
2411
|
-
} catch {
|
|
2412
|
-
// Skip tools whose definitions can't be resolved
|
|
2413
|
-
}
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
|
-
// Include tools from all installed skills, even those not currently
|
|
2417
|
-
// activated in any session.
|
|
2418
|
-
try {
|
|
2419
|
-
const catalog = loadSkillCatalog();
|
|
2420
|
-
for (const skill of catalog) {
|
|
2421
|
-
if (!skill.toolManifest?.present || !skill.toolManifest.valid) continue;
|
|
2422
|
-
try {
|
|
2423
|
-
const manifest = parseToolManifestFile(join(skill.directoryPath, 'TOOLS.json'));
|
|
2424
|
-
for (const entry of manifest.tools) {
|
|
2425
|
-
if (nameSet.has(entry.name)) continue;
|
|
2426
|
-
nameSet.add(entry.name);
|
|
2427
|
-
schemas[entry.name] = entry.input_schema as unknown as import('../ipc-contract.js').ToolInputSchema;
|
|
2428
|
-
}
|
|
2429
|
-
} catch {
|
|
2430
|
-
// Skip skills whose manifests can't be parsed
|
|
2431
|
-
}
|
|
2432
|
-
}
|
|
2433
|
-
} catch {
|
|
2434
|
-
// Non-fatal — fall back to registered tools only
|
|
2435
|
-
}
|
|
2436
|
-
|
|
2437
|
-
const names = Array.from(nameSet).sort((a, b) => a.localeCompare(b));
|
|
2438
|
-
ctx.send(socket, { type: 'tool_names_list_response', names, schemas });
|
|
2439
|
-
}
|
|
2440
18
|
|
|
2441
|
-
export
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
19
|
+
// Re-export individual handlers for direct import by tests and other modules
|
|
20
|
+
export { handleModelGet, handleModelSet, handleImageGenModelSet } from './config-model.js';
|
|
21
|
+
export { handleAddTrustRule, handleTrustRulesList, handleRemoveTrustRule, handleUpdateTrustRule, handleAcceptStarterBundle } from './config-trust.js';
|
|
22
|
+
export { handleSchedulesList, handleScheduleToggle, handleScheduleRemove, handleScheduleRunNow, handleRemindersList, handleReminderCancel } from './config-scheduling.js';
|
|
23
|
+
export { handleShareToSlack, handleSlackWebhookConfig } from './config-slack.js';
|
|
24
|
+
export { handleIngressConfig, computeGatewayTarget, triggerGatewayReconcile, syncTwilioWebhooks } from './config-ingress.js';
|
|
25
|
+
export { handleVercelApiConfig, handleTwitterIntegrationConfig } from './config-integrations.js';
|
|
26
|
+
export { handleTelegramConfig, summarizeTelegramError } from './config-telegram.js';
|
|
27
|
+
export { handleTwilioConfig } from './config-twilio.js';
|
|
28
|
+
export { handleGuardianVerification, handleChannelReadiness, getReadinessService } from './config-channels.js';
|
|
29
|
+
export { handleEnvVarsRequest, handleToolPermissionSimulate, handleToolNamesList } from './config-tools.js';
|
|
30
|
+
export { handleParentalControlGet, handleParentalControlVerifyPin, handleParentalControlSetPin, handleParentalControlUpdate } from './config-parental.js';
|
|
31
|
+
|
|
32
|
+
// Assemble the combined dispatch map from domain-specific handler groups
|
|
33
|
+
import { modelHandlers } from './config-model.js';
|
|
34
|
+
import { trustHandlers } from './config-trust.js';
|
|
35
|
+
import { schedulingHandlers } from './config-scheduling.js';
|
|
36
|
+
import { slackHandlers } from './config-slack.js';
|
|
37
|
+
import { ingressHandlers } from './config-ingress.js';
|
|
38
|
+
import { integrationHandlers } from './config-integrations.js';
|
|
39
|
+
import { telegramHandlers } from './config-telegram.js';
|
|
40
|
+
import { twilioHandlers } from './config-twilio.js';
|
|
41
|
+
import { channelHandlers } from './config-channels.js';
|
|
42
|
+
import { toolHandlers } from './config-tools.js';
|
|
43
|
+
import { parentalControlHandlers } from './config-parental.js';
|
|
44
|
+
|
|
45
|
+
export const configHandlers = {
|
|
46
|
+
...modelHandlers,
|
|
47
|
+
...trustHandlers,
|
|
48
|
+
...schedulingHandlers,
|
|
49
|
+
...slackHandlers,
|
|
50
|
+
...ingressHandlers,
|
|
51
|
+
...integrationHandlers,
|
|
52
|
+
...telegramHandlers,
|
|
53
|
+
...twilioHandlers,
|
|
54
|
+
...channelHandlers,
|
|
55
|
+
...toolHandlers,
|
|
56
|
+
...parentalControlHandlers,
|
|
57
|
+
};
|