@vellumai/assistant 0.6.2 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +273 -10
- package/Dockerfile +2 -3
- package/bun.lock +41 -49
- package/bunfig.toml +3 -0
- package/docs/architecture/memory.md +1 -1
- package/docs/backup-troubleshooting.md +52 -0
- package/docs/browser-use-architecture-phase2.md +174 -0
- package/docs/stt-provider-onboarding.md +120 -0
- package/knip.json +12 -2
- package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
- package/node_modules/@vellumai/ces-contracts/package.json +3 -3
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
- package/openapi.yaml +1111 -86
- package/package.json +40 -42
- package/scripts/generate-openapi.ts +0 -2
- package/scripts/test.sh +73 -18
- package/src/__tests__/acp-session.test.ts +43 -0
- package/src/__tests__/agent-image-optimize.test.ts +28 -0
- package/src/__tests__/agent-loop.test.ts +123 -0
- package/src/__tests__/anthropic-provider.test.ts +263 -10
- package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +1 -0
- package/src/__tests__/app-source-watcher.test.ts +37 -11
- package/src/__tests__/approval-routes-http.test.ts +178 -1
- package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
- package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
- package/src/__tests__/browser-fill-credential.test.ts +240 -94
- package/src/__tests__/browser-manager.test.ts +40 -27
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
- package/src/__tests__/browser-skill-endstate.test.ts +31 -7
- package/src/__tests__/btw-routes.test.ts +7 -0
- package/src/__tests__/call-controller.test.ts +581 -20
- package/src/__tests__/catalog-files.test.ts +1000 -0
- package/src/__tests__/channel-approvals.test.ts +53 -0
- package/src/__tests__/channel-invite-transport.test.ts +2 -2
- package/src/__tests__/channel-readiness-routes.test.ts +16 -20
- package/src/__tests__/channel-readiness-service.test.ts +12 -7
- package/src/__tests__/checker.test.ts +157 -10
- package/src/__tests__/clawhub-files.test.ts +347 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
- package/src/__tests__/config-analysis.test.ts +100 -0
- package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
- package/src/__tests__/config-schema-cmd.test.ts +2 -2
- package/src/__tests__/config-schema.test.ts +1248 -224
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
- package/src/__tests__/config-watcher.test.ts +43 -8
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
- package/src/__tests__/contact-store-user-file.test.ts +512 -0
- package/src/__tests__/contacts-write.test.ts +197 -0
- package/src/__tests__/context-overflow-approval.test.ts +16 -1
- package/src/__tests__/context-window-manager.test.ts +88 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -1
- package/src/__tests__/conversation-agent-loop.test.ts +99 -3
- package/src/__tests__/conversation-analysis-routes.test.ts +2 -2
- package/src/__tests__/conversation-attachments.test.ts +80 -4
- package/src/__tests__/conversation-confirmation-signals.test.ts +290 -0
- package/src/__tests__/conversation-error.test.ts +70 -0
- package/src/__tests__/conversation-fork-crud.test.ts +17 -0
- package/src/__tests__/conversation-history-web-search.test.ts +12 -4
- package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
- package/src/__tests__/conversation-inject-context.test.ts +103 -0
- package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
- package/src/__tests__/conversation-list-source.test.ts +145 -0
- package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
- package/src/__tests__/conversation-queue.test.ts +946 -62
- package/src/__tests__/conversation-routes-disk-view.test.ts +275 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +324 -46
- package/src/__tests__/conversation-skill-tools.test.ts +7 -4
- package/src/__tests__/conversation-slash-commands.test.ts +33 -0
- package/src/__tests__/conversation-slash-queue.test.ts +89 -18
- package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
- package/src/__tests__/conversation-starter-routes.test.ts +126 -0
- package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
- package/src/__tests__/conversation-store.test.ts +195 -0
- package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
- package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -1
- package/src/__tests__/credential-health-service.test.ts +352 -0
- package/src/__tests__/credential-security-invariants.test.ts +6 -3
- package/src/__tests__/credential-vault-unit.test.ts +383 -7
- package/src/__tests__/credential-vault.test.ts +152 -13
- package/src/__tests__/credentials-cli.test.ts +42 -18
- package/src/__tests__/cross-provider-web-search.test.ts +146 -35
- package/src/__tests__/date-context.test.ts +4 -4
- package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
- package/src/__tests__/device-id.test.ts +112 -0
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
- package/src/__tests__/email-html-renderer.test.ts +71 -0
- package/src/__tests__/email-invite-adapter.test.ts +36 -32
- package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
- package/src/__tests__/emit-event-signal.test.ts +71 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +222 -0
- package/src/__tests__/fixtures/mock-chrome-extension.ts +386 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
- package/src/__tests__/gateway-only-guard.test.ts +2 -0
- package/src/__tests__/gemini-provider.test.ts +66 -2
- package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
- package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
- package/src/__tests__/gmail-archive-gate.test.ts +246 -0
- package/src/__tests__/gmail-preferences.test.ts +117 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
- package/src/__tests__/headless-browser-interactions.test.ts +738 -359
- package/src/__tests__/headless-browser-mode.test.ts +614 -0
- package/src/__tests__/headless-browser-navigate.test.ts +528 -49
- package/src/__tests__/headless-browser-read-tools.test.ts +274 -100
- package/src/__tests__/headless-browser-snapshot.test.ts +250 -77
- package/src/__tests__/heartbeat-service.test.ts +70 -17
- package/src/__tests__/home-state-routes.test.ts +162 -0
- package/src/__tests__/host-bash-proxy.test.ts +145 -1
- package/src/__tests__/host-browser-e2e-cloud.test.ts +596 -0
- package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
- package/src/__tests__/host-browser-event-routes.test.ts +350 -0
- package/src/__tests__/host-browser-proxy.test.ts +444 -0
- package/src/__tests__/host-browser-routes.test.ts +198 -0
- package/src/__tests__/host-browser-ws-events-e2e.test.ts +423 -0
- package/src/__tests__/host-cu-proxy.test.ts +166 -1
- package/src/__tests__/host-file-proxy.test.ts +185 -1
- package/src/__tests__/host-file-read-tool.test.ts +52 -0
- package/src/__tests__/host-proxy-interface.test.ts +165 -0
- package/src/__tests__/host-shell-tool.test.ts +1 -11
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/identity-intro-cache.test.ts +40 -10
- package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
- package/src/__tests__/integration-status.test.ts +6 -7
- package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
- package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
- package/src/__tests__/llm-context-normalization.test.ts +488 -0
- package/src/__tests__/llm-context-route-provider.test.ts +86 -5
- package/src/__tests__/llm-usage-store.test.ts +363 -0
- package/src/__tests__/mcp-client-auth.test.ts +40 -4
- package/src/__tests__/mcp-health-check.test.ts +10 -3
- package/src/__tests__/media-stream-output.test.ts +555 -0
- package/src/__tests__/media-stream-parser.test.ts +374 -0
- package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
- package/src/__tests__/media-stream-stt-session.test.ts +588 -0
- package/src/__tests__/media-turn-detector.test.ts +440 -0
- package/src/__tests__/message-queue.test.ts +125 -0
- package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
- package/src/__tests__/migration-export-http.test.ts +67 -8
- package/src/__tests__/migration-export-streaming.test.ts +66 -0
- package/src/__tests__/migration-import-commit-http.test.ts +109 -7
- package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
- package/src/__tests__/migration-validate-http.test.ts +3 -3
- package/src/__tests__/mock-gateway-ipc.ts +151 -0
- package/src/__tests__/model-intents.test.ts +2 -2
- package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
- package/src/__tests__/oauth-apps-routes.test.ts +18 -12
- package/src/__tests__/oauth-cli.test.ts +709 -60
- package/src/__tests__/oauth-connect-orchestrator.test.ts +118 -24
- package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +147 -10
- package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
- package/src/__tests__/oauth-providers-routes.test.ts +52 -14
- package/src/__tests__/oauth-store.test.ts +1465 -176
- package/src/__tests__/oauth2-gateway-transport.test.ts +460 -26
- package/src/__tests__/onboarding-template-contract.test.ts +81 -70
- package/src/__tests__/openai-provider.test.ts +178 -2
- package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
- package/src/__tests__/openai-responses-provider.test.ts +1105 -0
- package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
- package/src/__tests__/outlook-categories.test.ts +1 -1
- package/src/__tests__/outlook-client-automation.test.ts +1 -1
- package/src/__tests__/outlook-compose-tools.test.ts +1 -1
- package/src/__tests__/outlook-email-watcher.test.ts +1 -1
- package/src/__tests__/outlook-follow-up.test.ts +1 -1
- package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
- package/src/__tests__/outlook-trash.test.ts +1 -1
- package/src/__tests__/outlook-unsubscribe.test.ts +32 -3
- package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
- package/src/__tests__/permission-mode.test.ts +28 -56
- package/src/__tests__/persona-resolver.test.ts +251 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
- package/src/__tests__/platform-callback-registration.test.ts +19 -0
- package/src/__tests__/platform.test.ts +92 -1
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +343 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
- package/src/__tests__/pricing.test.ts +174 -0
- package/src/__tests__/proxy-approval-callback.test.ts +18 -0
- package/src/__tests__/qdrant-manager.test.ts +29 -8
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
- package/src/__tests__/relationship-state-contract.test.ts +175 -0
- package/src/__tests__/relay-server.test.ts +423 -5
- package/src/__tests__/require-fresh-approval.test.ts +40 -1
- package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
- package/src/__tests__/schedule-routes.test.ts +162 -0
- package/src/__tests__/search-skills-unified.test.ts +118 -0
- package/src/__tests__/secret-detection-handler.test.ts +84 -0
- package/src/__tests__/secret-ingress-http.test.ts +1 -0
- package/src/__tests__/secret-scanner-executor.test.ts +4 -0
- package/src/__tests__/secure-keys.test.ts +107 -0
- package/src/__tests__/send-endpoint-busy.test.ts +8 -1
- package/src/__tests__/sequence-store.test.ts +1 -1
- package/src/__tests__/server-history-render.test.ts +49 -0
- package/src/__tests__/set-permission-mode.test.ts +13 -250
- package/src/__tests__/settings-routes.test.ts +201 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
- package/src/__tests__/skills-file-content-endpoint.test.ts +801 -0
- package/src/__tests__/skills-files-catalog-fallback.test.ts +738 -0
- package/src/__tests__/skills.test.ts +5 -2
- package/src/__tests__/skillssh-files.test.ts +446 -0
- package/src/__tests__/slack-block-formatting.test.ts +110 -0
- package/src/__tests__/slack-channel-config.test.ts +576 -16
- package/src/__tests__/stt-catalog-parity.test.ts +282 -0
- package/src/__tests__/stt-stream-session.test.ts +535 -0
- package/src/__tests__/subagent-detail.test.ts +44 -2
- package/src/__tests__/subagent-disposal.test.ts +1 -0
- package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
- package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
- package/src/__tests__/subagent-manager-notify.test.ts +1 -0
- package/src/__tests__/subagent-notify-parent.test.ts +1 -0
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
- package/src/__tests__/subagent-tools.test.ts +1 -0
- package/src/__tests__/subagent-types.test.ts +1 -0
- package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
- package/src/__tests__/system-prompt.test.ts +184 -27
- package/src/__tests__/task-scheduler.test.ts +32 -6
- package/src/__tests__/telegram-config.test.ts +10 -13
- package/src/__tests__/telephony-stt-routing.test.ts +329 -0
- package/src/__tests__/terminal-tools.test.ts +25 -5
- package/src/__tests__/test-preload.ts +18 -0
- package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
- package/src/__tests__/tool-approval-handler.test.ts +73 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
- package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +33 -24
- package/src/__tests__/tool-result-truncation.test.ts +36 -0
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
- package/src/__tests__/top-level-renderer.test.ts +73 -1
- package/src/__tests__/transport-hints-queue.test.ts +14 -29
- package/src/__tests__/trust-store.test.ts +7 -1
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
- package/src/__tests__/tts-catalog-parity.test.ts +345 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
- package/src/__tests__/twilio-routes.test.ts +376 -0
- package/src/__tests__/unicode.test.ts +293 -0
- package/src/__tests__/update-bulletin-format.test.ts +59 -0
- package/src/__tests__/update-bulletin.test.ts +206 -5
- package/src/__tests__/usage-routes.test.ts +25 -4
- package/src/__tests__/user-reference.test.ts +46 -61
- package/src/__tests__/v2-consent-policy.test.ts +103 -0
- package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
- package/src/__tests__/voice-config-update.test.ts +403 -0
- package/src/__tests__/voice-quality.test.ts +434 -19
- package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
- package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
- package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
- package/src/__tests__/workspace-migration-meets.test.ts +244 -0
- package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
- package/src/__tests__/workspace-policy.test.ts +2 -0
- package/src/acp/client-handler.ts +30 -4
- package/src/agent/image-optimize.ts +24 -12
- package/src/agent/loop.ts +55 -9
- package/src/approvals/guardian-request-resolvers.ts +21 -15
- package/src/backup/__tests__/backup-key.test.ts +152 -0
- package/src/backup/__tests__/backup-worker.test.ts +767 -0
- package/src/backup/__tests__/list-snapshots.test.ts +87 -0
- package/src/backup/__tests__/local-writer.test.ts +218 -0
- package/src/backup/__tests__/offsite-writer.test.ts +641 -0
- package/src/backup/__tests__/paths.test.ts +300 -0
- package/src/backup/__tests__/restore.test.ts +498 -0
- package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
- package/src/backup/__tests__/stream-crypt.test.ts +228 -0
- package/src/backup/backup-key.ts +137 -0
- package/src/backup/backup-worker.ts +459 -0
- package/src/backup/list-snapshots.ts +147 -0
- package/src/backup/local-writer.ts +133 -0
- package/src/backup/offsite-writer.ts +222 -0
- package/src/backup/paths.ts +226 -0
- package/src/backup/restore.ts +322 -0
- package/src/backup/snapshot-lock.ts +431 -0
- package/src/backup/stream-crypt.ts +263 -0
- package/src/browser-session/__tests__/manager.test.ts +297 -0
- package/src/browser-session/backends/cdp-inspect.ts +30 -0
- package/src/browser-session/backends/extension.ts +26 -0
- package/src/browser-session/backends/local.ts +24 -0
- package/src/browser-session/events.ts +164 -0
- package/src/browser-session/index.ts +27 -0
- package/src/browser-session/manager.ts +159 -0
- package/src/browser-session/types.ts +28 -0
- package/src/bundler/package-resolver.ts +4 -0
- package/src/calls/audio-store.ts +11 -5
- package/src/calls/call-controller.ts +226 -71
- package/src/calls/call-domain.ts +9 -0
- package/src/calls/call-speech-output.ts +190 -0
- package/src/calls/call-transport.ts +77 -0
- package/src/calls/media-stream-audio-transcode.ts +173 -0
- package/src/calls/media-stream-output.ts +660 -0
- package/src/calls/media-stream-parser.ts +300 -0
- package/src/calls/media-stream-protocol.ts +166 -0
- package/src/calls/media-stream-server.ts +592 -0
- package/src/calls/media-stream-stt-session.ts +460 -0
- package/src/calls/media-turn-detector.ts +230 -0
- package/src/calls/relay-server.ts +90 -75
- package/src/calls/resolve-call-tts-provider.ts +136 -0
- package/src/calls/telephony-stt-routing.ts +145 -0
- package/src/calls/tts-call-strategy.ts +161 -0
- package/src/calls/tts-text-sanitizer.ts +32 -16
- package/src/calls/twilio-routes.ts +281 -17
- package/src/calls/voice-quality.ts +78 -35
- package/src/calls/voice-session-bridge.ts +8 -1
- package/src/channels/__tests__/types.test.ts +134 -0
- package/src/channels/types.ts +69 -3
- package/src/cli/__tests__/run-assistant-command.ts +11 -1
- package/src/cli/commands/__tests__/backup.test.ts +1165 -0
- package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
- package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
- package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
- package/src/cli/commands/__tests__/email-download.test.ts +16 -1
- package/src/cli/commands/__tests__/email-list.test.ts +22 -4
- package/src/cli/commands/__tests__/email-register.test.ts +4 -4
- package/src/cli/commands/__tests__/email-send.test.ts +37 -4
- package/src/cli/commands/__tests__/email-status.test.ts +5 -1
- package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
- package/src/cli/commands/backup.ts +993 -0
- package/src/cli/commands/conversations.ts +77 -0
- package/src/cli/commands/credentials.ts +3 -4
- package/src/cli/commands/domain.ts +210 -0
- package/src/cli/commands/email.ts +273 -16
- package/src/cli/commands/mcp.ts +16 -4
- package/src/cli/commands/oauth/__tests__/connect.test.ts +56 -44
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
- package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
- package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +32 -33
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +330 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +117 -12
- package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
- package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
- package/src/cli/commands/oauth/apps.ts +7 -4
- package/src/cli/commands/oauth/connect.ts +6 -3
- package/src/cli/commands/oauth/disconnect.ts +1 -1
- package/src/cli/commands/oauth/mode.ts +12 -3
- package/src/cli/commands/oauth/providers.ts +215 -36
- package/src/cli/commands/oauth/shared.ts +7 -6
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +254 -0
- package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
- package/src/cli/commands/platform/index.ts +107 -10
- package/src/cli/commands/usage.ts +10 -9
- package/src/cli/lib/daemon-credential-client.ts +4 -0
- package/src/cli/program.ts +30 -4
- package/src/config/__tests__/backup-schema.test.ts +134 -0
- package/src/config/assistant-feature-flags.ts +61 -62
- package/src/config/bundled-skills/app-builder/SKILL.md +26 -249
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +141 -0
- package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
- package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
- package/src/config/bundled-skills/browser/SKILL.md +30 -5
- package/src/config/bundled-skills/browser/TOOLS.json +123 -0
- package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
- package/src/config/bundled-skills/contacts/SKILL.md +5 -2
- package/src/config/bundled-skills/document/SKILL.md +4 -0
- package/src/config/bundled-skills/gmail/SKILL.md +54 -8
- package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
- package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
- package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
- package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
- package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
- package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
- package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
- package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
- package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
- package/src/config/bundled-skills/messaging/SKILL.md +3 -3
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
- package/src/config/bundled-skills/outlook/SKILL.md +9 -2
- package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
- package/src/config/bundled-skills/slack/SKILL.md +1 -0
- package/src/config/bundled-skills/subagent/SKILL.md +21 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
- package/src/config/bundled-skills/tasks/SKILL.md +5 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
- package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
- package/src/config/bundled-tool-registry.ts +8 -0
- package/src/config/env-registry.ts +38 -0
- package/src/config/env.ts +49 -4
- package/src/config/feature-flag-registry.json +85 -14
- package/src/config/loader.ts +82 -13
- package/src/config/sanitize-for-transfer.ts +47 -0
- package/src/config/schema.ts +81 -15
- package/src/config/schemas/__tests__/stt.test.ts +43 -0
- package/src/config/schemas/analysis.ts +51 -0
- package/src/config/schemas/backup.ts +72 -0
- package/src/config/schemas/calls.ts +1 -26
- package/src/config/schemas/elevenlabs.ts +0 -59
- package/src/config/schemas/filing.ts +47 -7
- package/src/config/schemas/heartbeat.ts +27 -5
- package/src/config/schemas/host-browser.ts +112 -0
- package/src/config/schemas/inference.ts +1 -1
- package/src/config/schemas/memory-lifecycle.ts +14 -2
- package/src/config/schemas/memory-retrieval.ts +103 -0
- package/src/config/schemas/security.ts +0 -6
- package/src/config/schemas/services.ts +52 -0
- package/src/config/schemas/stt.ts +59 -0
- package/src/config/schemas/tts.ts +230 -0
- package/src/config/schemas/updates.ts +14 -0
- package/src/config/skills.ts +4 -0
- package/src/config/types.ts +4 -1
- package/src/contacts/contact-store.ts +56 -11
- package/src/contacts/contacts-write.ts +38 -1
- package/src/context/post-turn-tool-result-truncation.ts +177 -0
- package/src/context/tool-result-truncation.ts +2 -1
- package/src/context/window-manager.ts +61 -10
- package/src/credential-execution/approval-bridge.ts +49 -15
- package/src/credential-execution/executable-discovery.ts +12 -2
- package/src/credential-execution/process-manager.ts +33 -2
- package/src/credential-health/credential-health-service.ts +366 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +195 -0
- package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
- package/src/daemon/app-source-watcher.ts +35 -0
- package/src/daemon/config-watcher.ts +99 -5
- package/src/daemon/context-overflow-approval.ts +5 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +23 -2
- package/src/daemon/conversation-agent-loop.ts +153 -42
- package/src/daemon/conversation-attachments.ts +40 -0
- package/src/daemon/conversation-error.ts +11 -0
- package/src/daemon/conversation-history.ts +40 -6
- package/src/daemon/conversation-launch.ts +220 -0
- package/src/daemon/conversation-lifecycle.ts +59 -9
- package/src/daemon/conversation-messaging.ts +37 -3
- package/src/daemon/conversation-notifiers.ts +5 -0
- package/src/daemon/conversation-process.ts +622 -13
- package/src/daemon/conversation-queue-manager.ts +24 -0
- package/src/daemon/conversation-runtime-assembly.ts +128 -36
- package/src/daemon/conversation-slash.ts +36 -0
- package/src/daemon/conversation-surfaces.ts +131 -40
- package/src/daemon/conversation-tool-setup.ts +99 -8
- package/src/daemon/conversation-usage.ts +7 -4
- package/src/daemon/conversation-workspace.ts +12 -0
- package/src/daemon/conversation.ts +292 -16
- package/src/daemon/date-context.ts +10 -10
- package/src/daemon/first-greeting.ts +3 -2
- package/src/daemon/handlers/config-slack-channel.ts +269 -94
- package/src/daemon/handlers/conversations.ts +13 -141
- package/src/daemon/handlers/shared.ts +80 -0
- package/src/daemon/handlers/skills.ts +483 -44
- package/src/daemon/host-bash-proxy.ts +48 -13
- package/src/daemon/host-browser-proxy.ts +192 -0
- package/src/daemon/host-cu-proxy.ts +36 -11
- package/src/daemon/host-file-proxy.ts +57 -9
- package/src/daemon/lifecycle.ts +179 -28
- package/src/daemon/message-protocol.ts +13 -0
- package/src/daemon/message-types/conversations.ts +89 -14
- package/src/daemon/message-types/home.ts +40 -0
- package/src/daemon/message-types/host-browser.ts +100 -0
- package/src/daemon/message-types/meet.ts +143 -0
- package/src/daemon/message-types/messages.ts +19 -5
- package/src/daemon/message-types/schedules.ts +34 -2
- package/src/daemon/message-types/skills.ts +26 -0
- package/src/daemon/message-types/subagents.ts +2 -0
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/server.ts +439 -14
- package/src/daemon/shutdown-handlers.ts +32 -4
- package/src/daemon/shutdown-registry.ts +40 -0
- package/src/daemon/tool-side-effects.ts +15 -0
- package/src/daemon/transport-hints.ts +5 -24
- package/src/email/html-renderer.ts +76 -0
- package/src/heartbeat/heartbeat-service.ts +93 -7
- package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
- package/src/home/__tests__/emit-feed-event.test.ts +169 -0
- package/src/home/__tests__/feed-scheduler.test.ts +194 -0
- package/src/home/__tests__/feed-types.test.ts +275 -0
- package/src/home/__tests__/feed-writer.test.ts +688 -0
- package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
- package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
- package/src/home/__tests__/progress-formula.test.ts +213 -0
- package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
- package/src/home/__tests__/rollup-producer.test.ts +398 -0
- package/src/home/assistant-feed-authoring.ts +124 -0
- package/src/home/emit-feed-event.ts +158 -0
- package/src/home/feed-scheduler.ts +247 -0
- package/src/home/feed-types.ts +181 -0
- package/src/home/feed-writer.ts +469 -0
- package/src/home/platform-gmail-digest.ts +163 -0
- package/src/home/progress-formula.ts +86 -0
- package/src/home/relationship-state-writer.ts +824 -0
- package/src/home/relationship-state.ts +143 -0
- package/src/home/rollup-producer.ts +384 -0
- package/src/hooks/runner.ts +7 -0
- package/src/inbound/platform-callback-registration.ts +30 -20
- package/src/inbound/public-ingress-urls.ts +12 -0
- package/src/instrument.ts +1 -1
- package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
- package/src/ipc/cli-client.ts +151 -0
- package/src/ipc/cli-server.ts +234 -0
- package/src/ipc/gateway-client.ts +180 -0
- package/src/ipc/routes/index.ts +5 -0
- package/src/ipc/routes/wake-conversation.ts +19 -0
- package/src/mcp/client.ts +59 -24
- package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
- package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
- package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
- package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
- package/src/memory/app-store.ts +31 -1
- package/src/memory/attachments-store.ts +70 -0
- package/src/memory/auto-analysis-enqueue.ts +127 -0
- package/src/memory/auto-analysis-guard.ts +27 -0
- package/src/memory/cleanup-schedule-state.ts +37 -0
- package/src/memory/conversation-analyze-job.ts +73 -0
- package/src/memory/conversation-crud.ts +122 -0
- package/src/memory/conversation-disk-view.ts +7 -0
- package/src/memory/conversation-group-migration.ts +34 -2
- package/src/memory/conversation-queries.ts +6 -5
- package/src/memory/conversation-starters-cadence.ts +76 -0
- package/src/memory/conversation-title-service.ts +5 -2
- package/src/memory/db-init.ts +18 -0
- package/src/memory/db-maintenance.ts +108 -0
- package/src/memory/db.ts +1 -0
- package/src/memory/embedding-backend.test.ts +75 -0
- package/src/memory/embedding-backend.ts +131 -5
- package/src/memory/embedding-gemini.test.ts +54 -0
- package/src/memory/embedding-gemini.ts +20 -9
- package/src/memory/embedding-local.ts +176 -17
- package/src/memory/graph/consolidation.ts +10 -23
- package/src/memory/graph/conversation-graph-memory.ts +15 -0
- package/src/memory/graph/extraction-job.ts +15 -0
- package/src/memory/graph/extraction.test.ts +23 -0
- package/src/memory/graph/extraction.ts +8 -0
- package/src/memory/graph/retriever.ts +67 -40
- package/src/memory/graph/scoring.test.ts +186 -0
- package/src/memory/graph/scoring.ts +31 -1
- package/src/memory/graph/store.test.ts +7 -3
- package/src/memory/graph/store.ts +47 -12
- package/src/memory/graph/tools.ts +1 -1
- package/src/memory/group-crud.ts +6 -1
- package/src/memory/indexer.ts +95 -16
- package/src/memory/job-handlers/cleanup.ts +11 -8
- package/src/memory/job-handlers/conversation-starters.ts +16 -10
- package/src/memory/jobs-store.ts +64 -4
- package/src/memory/jobs-worker.ts +22 -9
- package/src/memory/llm-usage-store.ts +137 -60
- package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
- package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
- package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
- package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
- package/src/memory/migrations/217-conversation-host-access.ts +40 -0
- package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
- package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
- package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
- package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
- package/src/memory/migrations/index.ts +12 -0
- package/src/memory/migrations/registry.ts +16 -0
- package/src/memory/qdrant-manager.ts +43 -16
- package/src/memory/schema/conversations.ts +3 -0
- package/src/memory/schema/oauth.ts +21 -13
- package/src/memory/usage-buckets.ts +396 -0
- package/src/messaging/providers/gmail/client.ts +57 -6
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
- package/src/messaging/providers/slack/adapter.ts +143 -38
- package/src/messaging/providers/slack/client.ts +16 -0
- package/src/messaging/providers/slack/types.ts +4 -0
- package/src/notifications/decision-engine.ts +3 -3
- package/src/notifications/signal.ts +5 -0
- package/src/oauth/AGENTS.md +76 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +25 -19
- package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
- package/src/oauth/byo-connection.test.ts +26 -9
- package/src/oauth/byo-connection.ts +10 -8
- package/src/oauth/connect-orchestrator.ts +25 -21
- package/src/oauth/connect-types.ts +3 -3
- package/src/oauth/connection-resolver.test.ts +17 -4
- package/src/oauth/connection-resolver.ts +22 -18
- package/src/oauth/connection.ts +3 -1
- package/src/oauth/manual-token-connection.ts +13 -13
- package/src/oauth/oauth-store.ts +223 -100
- package/src/oauth/platform-connection.test.ts +101 -3
- package/src/oauth/platform-connection.ts +56 -35
- package/src/oauth/provider-serializer.ts +31 -5
- package/src/oauth/revoke.ts +76 -0
- package/src/oauth/seed-providers.ts +133 -87
- package/src/oauth/token-persistence.ts +1 -1
- package/src/permissions/checker.ts +16 -6
- package/src/permissions/defaults.ts +49 -1
- package/src/permissions/permission-mode.ts +4 -11
- package/src/permissions/prompter.ts +13 -1
- package/src/permissions/trust-store.ts +3 -3
- package/src/permissions/v2-consent-policy.ts +87 -0
- package/src/permissions/workspace-policy.ts +3 -0
- package/src/platform/client.test.ts +10 -0
- package/src/platform/sync-identity.ts +129 -0
- package/src/prompts/persona-resolver.ts +126 -2
- package/src/prompts/system-prompt.ts +76 -38
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
- package/src/prompts/templates/BOOTSTRAP.md +59 -105
- package/src/prompts/templates/SOUL.md +3 -1
- package/src/prompts/templates/UPDATES.md +12 -0
- package/src/prompts/templates/channels/slack.md +20 -0
- package/src/prompts/update-bulletin-format.ts +26 -9
- package/src/prompts/update-bulletin.ts +34 -23
- package/src/prompts/user-reference.ts +20 -17
- package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
- package/src/providers/anthropic/client.ts +157 -60
- package/src/providers/fireworks/client.ts +2 -2
- package/src/providers/gemini/client.ts +9 -1
- package/src/providers/model-catalog.ts +6 -0
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/ollama/client.ts +2 -2
- package/src/providers/openai/chat-completions-provider.ts +474 -0
- package/src/providers/openai/client.ts +25 -440
- package/src/providers/openai/responses-provider.ts +502 -0
- package/src/providers/openrouter/client.ts +101 -4
- package/src/providers/provider-secret-catalog.ts +139 -0
- package/src/providers/registry.ts +2 -2
- package/src/providers/retry.ts +14 -3
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
- package/src/providers/speech-to-text/deepgram.test.ts +332 -0
- package/src/providers/speech-to-text/deepgram.ts +115 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
- package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
- package/src/providers/speech-to-text/google-gemini.ts +101 -0
- package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
- package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
- package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
- package/src/providers/speech-to-text/openai-whisper.ts +63 -33
- package/src/providers/speech-to-text/provider-catalog.ts +306 -0
- package/src/providers/speech-to-text/resolve.ts +386 -6
- package/src/providers/types.ts +10 -1
- package/src/runtime/AGENTS.md +65 -0
- package/src/runtime/__tests__/agent-wake.test.ts +831 -0
- package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
- package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
- package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
- package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
- package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
- package/src/runtime/agent-wake.ts +512 -0
- package/src/runtime/assistant-event-hub.ts +2 -2
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
- package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +48 -0
- package/src/runtime/auth/middleware.ts +98 -0
- package/src/runtime/auth/route-policy.ts +33 -9
- package/src/runtime/auth/token-service.ts +56 -1
- package/src/runtime/btw-sidechain.ts +2 -0
- package/src/runtime/capability-tokens.ts +414 -0
- package/src/runtime/channel-approvals.ts +18 -5
- package/src/runtime/channel-invite-transport.ts +1 -1
- package/src/runtime/channel-invite-transports/email.ts +14 -6
- package/src/runtime/channel-readiness-service.ts +12 -22
- package/src/runtime/chrome-extension-registry.ts +368 -0
- package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
- package/src/runtime/guardian-decision-types.ts +7 -0
- package/src/runtime/http-server.ts +815 -75
- package/src/runtime/http-types.ts +6 -2
- package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
- package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +198 -0
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
- package/src/runtime/migrations/migration-transport.ts +7 -0
- package/src/runtime/migrations/migration-wizard.ts +23 -2
- package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
- package/src/runtime/migrations/vbundle-builder.ts +145 -38
- package/src/runtime/migrations/vbundle-import-analyzer.ts +96 -1
- package/src/runtime/migrations/vbundle-importer.ts +89 -5
- package/src/runtime/pending-interactions.ts +18 -13
- package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
- package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
- package/src/runtime/routes/app-management-routes.ts +12 -18
- package/src/runtime/routes/approval-routes.ts +90 -16
- package/src/runtime/routes/attachment-routes.test.ts +9 -3
- package/src/runtime/routes/attachment-routes.ts +216 -17
- package/src/runtime/routes/backup-routes.ts +519 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +556 -0
- package/src/runtime/routes/btw-routes.ts +8 -6
- package/src/runtime/routes/contact-routes.test.ts +298 -0
- package/src/runtime/routes/contact-routes.ts +132 -5
- package/src/runtime/routes/conversation-analysis-routes.ts +22 -141
- package/src/runtime/routes/conversation-management-routes.ts +223 -0
- package/src/runtime/routes/conversation-routes.ts +598 -103
- package/src/runtime/routes/conversation-starter-routes.ts +78 -16
- package/src/runtime/routes/filing-routes.ts +93 -0
- package/src/runtime/routes/guardian-action-routes.ts +24 -13
- package/src/runtime/routes/home-feed-routes.ts +334 -0
- package/src/runtime/routes/home-state-routes.ts +138 -0
- package/src/runtime/routes/host-browser-routes.ts +268 -0
- package/src/runtime/routes/host-file-routes.ts +9 -1
- package/src/runtime/routes/identity-intro-cache.ts +7 -3
- package/src/runtime/routes/identity-routes.ts +262 -33
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
- package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
- package/src/runtime/routes/integrations/slack/channel.ts +11 -3
- package/src/runtime/routes/integrations/slack/share.ts +45 -7
- package/src/runtime/routes/llm-context-normalization.ts +303 -0
- package/src/runtime/routes/log-export-routes.ts +42 -22
- package/src/runtime/routes/memory-item-routes.test.ts +3 -2
- package/src/runtime/routes/memory-item-routes.ts +1 -7
- package/src/runtime/routes/migration-routes.ts +122 -2
- package/src/runtime/routes/oauth-apps.ts +15 -17
- package/src/runtime/routes/oauth-providers.ts +4 -0
- package/src/runtime/routes/schedule-routes.ts +24 -11
- package/src/runtime/routes/settings-routes.ts +31 -102
- package/src/runtime/routes/skills-routes.ts +128 -9
- package/src/runtime/routes/stt-routes.ts +233 -0
- package/src/runtime/routes/subagents-routes.ts +14 -10
- package/src/runtime/routes/surface-action-routes.ts +41 -2
- package/src/runtime/routes/tts-routes.ts +108 -24
- package/src/runtime/routes/usage-routes.ts +38 -9
- package/src/runtime/routes/user-route-dispatcher.ts +50 -5
- package/src/runtime/routes/user-routes.ts +13 -1
- package/src/runtime/routes/work-items-routes.ts +8 -1
- package/src/runtime/routes/workspace-routes.test.ts +22 -0
- package/src/runtime/routes/workspace-routes.ts +8 -1
- package/src/runtime/routes/workspace-utils.ts +2 -0
- package/src/runtime/runtime-mode.ts +33 -0
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
- package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
- package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
- package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
- package/src/runtime/services/analyze-conversation.ts +344 -0
- package/src/runtime/services/analyze-deps-singleton.ts +32 -0
- package/src/runtime/services/auto-analysis-prompt.ts +55 -0
- package/src/runtime/skill-route-registry.ts +49 -0
- package/src/runtime/slack-block-formatting.ts +437 -10
- package/src/schedule/scheduler.ts +57 -5
- package/src/security/ces-credential-client.ts +20 -0
- package/src/security/ces-rpc-credential-backend.ts +17 -0
- package/src/security/credential-backend.ts +5 -0
- package/src/security/oauth2.ts +68 -29
- package/src/security/secure-keys.ts +143 -27
- package/src/security/token-manager.ts +31 -10
- package/src/sequence/engine.ts +23 -0
- package/src/sequence/types.ts +1 -1
- package/src/skills/catalog-files.ts +554 -0
- package/src/skills/category-inference.ts +122 -0
- package/src/skills/clawhub-files.ts +213 -0
- package/src/skills/clawhub.ts +84 -23
- package/src/skills/skill-file-provider.ts +40 -0
- package/src/skills/skillssh-files.ts +395 -0
- package/src/skills/skillssh-registry.ts +4 -4
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
- package/src/stt/__tests__/types.test.ts +89 -0
- package/src/stt/daemon-batch-transcriber.ts +195 -0
- package/src/stt/stt-stream-session.ts +499 -0
- package/src/stt/types.ts +330 -0
- package/src/stt/wav-encoder.test.ts +373 -0
- package/src/stt/wav-encoder.ts +175 -0
- package/src/subagent/manager.ts +169 -40
- package/src/subagent/types.ts +19 -0
- package/src/tools/apps/executors.ts +11 -2
- package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
- package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
- package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
- package/src/tools/browser/auth-detector.ts +43 -12
- package/src/tools/browser/browser-execution.ts +1787 -342
- package/src/tools/browser/browser-manager.ts +81 -12
- package/src/tools/browser/browser-mode-constants.ts +12 -0
- package/src/tools/browser/browser-mode.ts +92 -0
- package/src/tools/browser/browser-status-constants.ts +33 -0
- package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
- package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
- package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +1263 -0
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +359 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1993 -0
- package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
- package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
- package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
- package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
- package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +1007 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +744 -0
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +868 -0
- package/src/tools/browser/cdp-client/errors.ts +49 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +148 -0
- package/src/tools/browser/cdp-client/factory.ts +914 -0
- package/src/tools/browser/cdp-client/index.ts +28 -0
- package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
- package/src/tools/browser/cdp-client/types.ts +120 -0
- package/src/tools/credentials/vault.ts +35 -6
- package/src/tools/filesystem/edit.ts +1 -1
- package/src/tools/filesystem/list.ts +1 -1
- package/src/tools/filesystem/read.ts +1 -1
- package/src/tools/filesystem/write.ts +2 -1
- package/src/tools/host-filesystem/edit.ts +1 -1
- package/src/tools/host-filesystem/read.ts +12 -15
- package/src/tools/host-filesystem/write.ts +1 -1
- package/src/tools/host-terminal/host-shell.ts +21 -16
- package/src/tools/network/web-fetch.ts +5 -2
- package/src/tools/network/web-search.ts +5 -2
- package/src/tools/permission-checker.ts +77 -82
- package/src/tools/registry.ts +0 -2
- package/src/tools/secret-detection-handler.ts +34 -0
- package/src/tools/shared/filesystem/image-read.ts +61 -40
- package/src/tools/shared/shell-output.ts +3 -1
- package/src/tools/side-effects.ts +2 -0
- package/src/tools/skills/sandbox-runner.ts +3 -2
- package/src/tools/subagent/spawn.ts +47 -3
- package/src/tools/subagent/status.ts +2 -0
- package/src/tools/system/register.ts +2 -16
- package/src/tools/terminal/safe-env.ts +15 -0
- package/src/tools/terminal/shell.ts +36 -20
- package/src/tools/tool-approval-handler.ts +48 -2
- package/src/tools/tool-manifest.ts +21 -0
- package/src/tools/types.ts +19 -0
- package/src/tools/ui-surface/definitions.ts +6 -1
- package/src/tts/__tests__/provider-adapters.test.ts +834 -0
- package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
- package/src/tts/__tests__/provider-catalog.test.ts +183 -0
- package/src/tts/__tests__/provider-registry.test.ts +90 -0
- package/src/tts/provider-catalog.ts +201 -0
- package/src/tts/provider-registry.ts +73 -0
- package/src/tts/providers/deepgram-provider.ts +219 -0
- package/src/tts/providers/elevenlabs-provider.ts +211 -0
- package/src/tts/providers/fish-audio-provider.ts +183 -0
- package/src/tts/providers/index.ts +42 -0
- package/src/tts/providers/register-builtins.ts +130 -0
- package/src/tts/synthesize-text.ts +110 -0
- package/src/tts/tts-config-resolver.ts +78 -0
- package/src/tts/types.ts +153 -0
- package/src/types/onboarding-context.ts +7 -0
- package/src/util/abort-reasons.ts +58 -0
- package/src/util/device-id.ts +32 -16
- package/src/util/errors.ts +9 -1
- package/src/util/platform.ts +63 -24
- package/src/util/pricing.ts +66 -3
- package/src/util/spawn.ts +1 -1
- package/src/util/truncate.ts +4 -2
- package/src/util/unicode.ts +201 -0
- package/src/version.ts +19 -24
- package/src/watcher/engine.ts +23 -0
- package/src/watcher/watcher-store.ts +31 -0
- package/src/workspace/migrations/003-seed-device-id.ts +9 -3
- package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
- package/src/workspace/migrations/029-seed-pkb.ts +1 -1
- package/src/workspace/migrations/031-drop-user-md.ts +317 -0
- package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
- package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
- package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
- package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
- package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
- package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
- package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
- package/src/workspace/migrations/registry.ts +16 -0
- package/src/workspace/top-level-renderer.ts +31 -1
- package/src/workspace/turn-commit.ts +31 -0
- package/src/__tests__/chrome-cdp.test.ts +0 -419
- package/src/__tests__/email-cli.test.ts +0 -297
- package/src/__tests__/email-service-config-fallback.test.ts +0 -102
- package/src/__tests__/permission-mode-sse.test.ts +0 -418
- package/src/__tests__/permission-mode-store.test.ts +0 -277
- package/src/browser-extension-relay/protocol.ts +0 -63
- package/src/browser-extension-relay/server.ts +0 -203
- package/src/cli/commands/browser-relay.ts +0 -536
- package/src/config/schemas/sandbox.ts +0 -14
- package/src/email/guardrails.ts +0 -221
- package/src/email/provider.ts +0 -117
- package/src/email/providers/agentmail.ts +0 -361
- package/src/email/providers/index.ts +0 -65
- package/src/email/service.ts +0 -384
- package/src/email/types.ts +0 -126
- package/src/permissions/permission-mode-store.ts +0 -180
- package/src/prompts/templates/USER.md +0 -13
- package/src/providers/speech-to-text/types.ts +0 -17
- package/src/tools/browser/chrome-cdp.ts +0 -239
- package/src/tools/system/set-permission-mode.ts +0 -103
|
@@ -24,6 +24,8 @@ mock.module("../security/secure-keys.js", () => ({
|
|
|
24
24
|
import { eq } from "drizzle-orm";
|
|
25
25
|
|
|
26
26
|
import { getDb, initializeDb, resetDb, resetTestTables } from "../memory/db.js";
|
|
27
|
+
import { getSqliteFrom } from "../memory/db-connection.js";
|
|
28
|
+
import { migrateOAuthProvidersTokenAuthMethodDefault } from "../memory/migrations/216-oauth-providers-token-auth-method.js";
|
|
27
29
|
import { oauthProviders } from "../memory/schema/oauth.js";
|
|
28
30
|
import {
|
|
29
31
|
createConnection,
|
|
@@ -43,18 +45,21 @@ import {
|
|
|
43
45
|
registerProvider,
|
|
44
46
|
seedProviders,
|
|
45
47
|
updateConnection,
|
|
48
|
+
updateProvider,
|
|
46
49
|
upsertApp,
|
|
47
50
|
} from "../oauth/oauth-store.js";
|
|
51
|
+
import { seedOAuthProviders } from "../oauth/seed-providers.js";
|
|
52
|
+
import { getMockFetchCalls, mockFetch, resetMockFetch } from "./mock-fetch.js";
|
|
48
53
|
|
|
49
54
|
initializeDb();
|
|
50
55
|
|
|
51
56
|
/** Seed a minimal provider row for FK satisfaction. */
|
|
52
|
-
function seedTestProvider(
|
|
57
|
+
function seedTestProvider(provider = "github"): void {
|
|
53
58
|
seedProviders([
|
|
54
59
|
{
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
provider,
|
|
61
|
+
authorizeUrl: `https://${provider}.example.com/authorize`,
|
|
62
|
+
tokenExchangeUrl: `https://${provider}.example.com/token`,
|
|
58
63
|
defaultScopes: ["read"],
|
|
59
64
|
scopePolicy: {},
|
|
60
65
|
},
|
|
@@ -62,9 +67,9 @@ function seedTestProvider(providerKey = "github"): void {
|
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
/** Create an app linked to the given provider. Returns the app row. */
|
|
65
|
-
async function createTestApp(
|
|
66
|
-
seedTestProvider(
|
|
67
|
-
return await upsertApp(
|
|
70
|
+
async function createTestApp(provider = "github", clientId = "client-1") {
|
|
71
|
+
seedTestProvider(provider);
|
|
72
|
+
return await upsertApp(provider, clientId);
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
beforeEach(() => {
|
|
@@ -74,6 +79,7 @@ beforeEach(() => {
|
|
|
74
79
|
mockDeleteSecureKeyAsync.mockClear();
|
|
75
80
|
mockSetSecureKeyAsync.mockClear();
|
|
76
81
|
secureKeyValues.clear();
|
|
82
|
+
resetMockFetch();
|
|
77
83
|
});
|
|
78
84
|
|
|
79
85
|
afterAll(() => {
|
|
@@ -89,34 +95,36 @@ describe("provider operations", () => {
|
|
|
89
95
|
test("creates rows for new providers", () => {
|
|
90
96
|
seedProviders([
|
|
91
97
|
{
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
provider: "github",
|
|
99
|
+
authorizeUrl: "https://github.com/login/oauth/authorize",
|
|
100
|
+
tokenExchangeUrl: "https://github.com/login/oauth/access_token",
|
|
95
101
|
defaultScopes: ["repo", "user"],
|
|
96
102
|
scopePolicy: { required: ["repo"] },
|
|
97
103
|
},
|
|
98
104
|
{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
provider: "google",
|
|
106
|
+
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
107
|
+
tokenExchangeUrl: "https://oauth2.googleapis.com/token",
|
|
102
108
|
defaultScopes: ["openid", "email"],
|
|
103
109
|
scopePolicy: {},
|
|
104
|
-
|
|
110
|
+
authorizeParams: { access_type: "offline" },
|
|
105
111
|
},
|
|
106
112
|
]);
|
|
107
113
|
|
|
108
114
|
const gh = getProvider("github");
|
|
109
115
|
expect(gh).toBeDefined();
|
|
110
|
-
expect(gh!.
|
|
111
|
-
expect(gh!.
|
|
112
|
-
expect(gh!.
|
|
116
|
+
expect(gh!.provider).toBe("github");
|
|
117
|
+
expect(gh!.authorizeUrl).toBe("https://github.com/login/oauth/authorize");
|
|
118
|
+
expect(gh!.tokenExchangeUrl).toBe(
|
|
119
|
+
"https://github.com/login/oauth/access_token",
|
|
120
|
+
);
|
|
113
121
|
expect(JSON.parse(gh!.defaultScopes)).toEqual(["repo", "user"]);
|
|
114
122
|
expect(JSON.parse(gh!.scopePolicy)).toEqual({ required: ["repo"] });
|
|
115
123
|
|
|
116
124
|
const goog = getProvider("google");
|
|
117
125
|
expect(goog).toBeDefined();
|
|
118
|
-
expect(goog!.
|
|
119
|
-
expect(JSON.parse(goog!.
|
|
126
|
+
expect(goog!.provider).toBe("google");
|
|
127
|
+
expect(JSON.parse(goog!.authorizeParams!)).toEqual({
|
|
120
128
|
access_type: "offline",
|
|
121
129
|
});
|
|
122
130
|
});
|
|
@@ -124,9 +132,9 @@ describe("provider operations", () => {
|
|
|
124
132
|
test("updates implementation fields while preserving user-customizable fields on re-seed", () => {
|
|
125
133
|
seedProviders([
|
|
126
134
|
{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
135
|
+
provider: "github",
|
|
136
|
+
authorizeUrl: "https://github.com/login/oauth/authorize",
|
|
137
|
+
tokenExchangeUrl: "https://github.com/login/oauth/access_token",
|
|
130
138
|
defaultScopes: ["repo"],
|
|
131
139
|
scopePolicy: {},
|
|
132
140
|
baseUrl: "https://api.github.com",
|
|
@@ -141,9 +149,9 @@ describe("provider operations", () => {
|
|
|
141
149
|
// Re-seed with corrected values (simulates a code fix deployed on upgrade)
|
|
142
150
|
seedProviders([
|
|
143
151
|
{
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
152
|
+
provider: "github",
|
|
153
|
+
authorizeUrl: "https://github.com/login/oauth/authorize-v2",
|
|
154
|
+
tokenExchangeUrl: "https://github.com/login/oauth/access_token-v2",
|
|
147
155
|
defaultScopes: ["repo", "user"],
|
|
148
156
|
scopePolicy: { required: ["repo"] },
|
|
149
157
|
baseUrl: "https://api.github.com/v2",
|
|
@@ -153,8 +161,10 @@ describe("provider operations", () => {
|
|
|
153
161
|
const row = getProvider("github");
|
|
154
162
|
expect(row).toBeDefined();
|
|
155
163
|
// Implementation fields should be overwritten by the re-seed
|
|
156
|
-
expect(row!.
|
|
157
|
-
|
|
164
|
+
expect(row!.authorizeUrl).toBe(
|
|
165
|
+
"https://github.com/login/oauth/authorize-v2",
|
|
166
|
+
);
|
|
167
|
+
expect(row!.tokenExchangeUrl).toBe(
|
|
158
168
|
"https://github.com/login/oauth/access_token-v2",
|
|
159
169
|
);
|
|
160
170
|
// User-customizable fields (baseUrl, defaultScopes, scopePolicy) are
|
|
@@ -166,172 +176,1069 @@ describe("provider operations", () => {
|
|
|
166
176
|
expect(row!.createdAt).toBe(originalCreatedAt);
|
|
167
177
|
});
|
|
168
178
|
|
|
169
|
-
test("persists pingUrl when provided", () => {
|
|
179
|
+
test("persists pingUrl when provided", () => {
|
|
180
|
+
seedProviders([
|
|
181
|
+
{
|
|
182
|
+
provider: "github",
|
|
183
|
+
authorizeUrl: "https://github.com/authorize",
|
|
184
|
+
tokenExchangeUrl: "https://github.com/token",
|
|
185
|
+
defaultScopes: ["repo"],
|
|
186
|
+
scopePolicy: {},
|
|
187
|
+
pingUrl: "https://api.github.com/user",
|
|
188
|
+
},
|
|
189
|
+
]);
|
|
190
|
+
const row = getProvider("github");
|
|
191
|
+
expect(row!.pingUrl).toBe("https://api.github.com/user");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("pingUrl defaults to null when omitted", () => {
|
|
195
|
+
seedProviders([
|
|
196
|
+
{
|
|
197
|
+
provider: "github",
|
|
198
|
+
authorizeUrl: "https://github.com/authorize",
|
|
199
|
+
tokenExchangeUrl: "https://github.com/token",
|
|
200
|
+
defaultScopes: ["repo"],
|
|
201
|
+
scopePolicy: {},
|
|
202
|
+
},
|
|
203
|
+
]);
|
|
204
|
+
const row = getProvider("github");
|
|
205
|
+
expect(row!.pingUrl).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("preserves user-customizable fields while overwriting implementation fields on re-seed", () => {
|
|
209
|
+
// Initial seed with all fields
|
|
210
|
+
seedProviders([
|
|
211
|
+
{
|
|
212
|
+
provider: "github",
|
|
213
|
+
authorizeUrl: "https://github.com/authorize",
|
|
214
|
+
tokenExchangeUrl: "https://github.com/token",
|
|
215
|
+
tokenEndpointAuthMethod: "client_secret_post",
|
|
216
|
+
defaultScopes: ["repo"],
|
|
217
|
+
scopePolicy: { required: ["repo"] },
|
|
218
|
+
userinfoUrl: "https://api.github.com/user",
|
|
219
|
+
baseUrl: "https://api.github.com",
|
|
220
|
+
authorizeParams: { prompt: "consent" },
|
|
221
|
+
|
|
222
|
+
pingUrl: "https://api.github.com/user",
|
|
223
|
+
},
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
// Manually update user-customizable fields to simulate user edits
|
|
227
|
+
const db = getDb();
|
|
228
|
+
db.update(oauthProviders)
|
|
229
|
+
.set({
|
|
230
|
+
defaultScopes: JSON.stringify(["repo", "user", "gist"]),
|
|
231
|
+
scopePolicy: JSON.stringify({
|
|
232
|
+
required: ["repo"],
|
|
233
|
+
allowAdditionalScopes: true,
|
|
234
|
+
}),
|
|
235
|
+
baseUrl: "https://custom.github.com/api",
|
|
236
|
+
})
|
|
237
|
+
.where(eq(oauthProviders.provider, "github"))
|
|
238
|
+
.run();
|
|
239
|
+
|
|
240
|
+
// Verify the manual updates took effect
|
|
241
|
+
const beforeReseed = getProvider("github");
|
|
242
|
+
expect(JSON.parse(beforeReseed!.defaultScopes)).toEqual([
|
|
243
|
+
"repo",
|
|
244
|
+
"user",
|
|
245
|
+
"gist",
|
|
246
|
+
]);
|
|
247
|
+
expect(beforeReseed!.baseUrl).toBe("https://custom.github.com/api");
|
|
248
|
+
|
|
249
|
+
// Re-seed with updated implementation fields
|
|
250
|
+
seedProviders([
|
|
251
|
+
{
|
|
252
|
+
provider: "github",
|
|
253
|
+
authorizeUrl: "https://github.com/authorize-v2",
|
|
254
|
+
tokenExchangeUrl: "https://github.com/token-v2",
|
|
255
|
+
tokenEndpointAuthMethod: "client_secret_basic",
|
|
256
|
+
defaultScopes: ["repo-only"],
|
|
257
|
+
scopePolicy: {},
|
|
258
|
+
userinfoUrl: "https://api.github.com/user-v2",
|
|
259
|
+
baseUrl: "https://api.github.com/v2",
|
|
260
|
+
authorizeParams: { prompt: "login" },
|
|
261
|
+
|
|
262
|
+
pingUrl: "https://api.github.com/user-v2",
|
|
263
|
+
},
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
const row = getProvider("github");
|
|
267
|
+
expect(row).toBeDefined();
|
|
268
|
+
|
|
269
|
+
// User-customizable fields should retain their manual values
|
|
270
|
+
expect(JSON.parse(row!.defaultScopes)).toEqual(["repo", "user", "gist"]);
|
|
271
|
+
expect(JSON.parse(row!.scopePolicy)).toEqual({
|
|
272
|
+
required: ["repo"],
|
|
273
|
+
allowAdditionalScopes: true,
|
|
274
|
+
});
|
|
275
|
+
expect(row!.baseUrl).toBe("https://custom.github.com/api");
|
|
276
|
+
|
|
277
|
+
// Implementation fields should be overwritten from the seed data
|
|
278
|
+
expect(row!.authorizeUrl).toBe("https://github.com/authorize-v2");
|
|
279
|
+
expect(row!.tokenExchangeUrl).toBe("https://github.com/token-v2");
|
|
280
|
+
expect(row!.tokenEndpointAuthMethod).toBe("client_secret_basic");
|
|
281
|
+
expect(row!.userinfoUrl).toBe("https://api.github.com/user-v2");
|
|
282
|
+
expect(JSON.parse(row!.authorizeParams!)).toEqual({ prompt: "login" });
|
|
283
|
+
expect(row!.pingUrl).toBe("https://api.github.com/user-v2");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("persists custom scopeSeparator when provided", () => {
|
|
287
|
+
seedProviders([
|
|
288
|
+
{
|
|
289
|
+
provider: "test-provider",
|
|
290
|
+
authorizeUrl: "https://example.com/authorize",
|
|
291
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
292
|
+
defaultScopes: ["read", "write"],
|
|
293
|
+
scopePolicy: {},
|
|
294
|
+
scopeSeparator: ",",
|
|
295
|
+
},
|
|
296
|
+
]);
|
|
297
|
+
|
|
298
|
+
const row = getProvider("test-provider");
|
|
299
|
+
expect(row).toBeDefined();
|
|
300
|
+
expect(row!.scopeSeparator).toBe(",");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("scopeSeparator defaults to ' ' when omitted", () => {
|
|
304
|
+
seedProviders([
|
|
305
|
+
{
|
|
306
|
+
provider: "github",
|
|
307
|
+
authorizeUrl: "https://github.com/authorize",
|
|
308
|
+
tokenExchangeUrl: "https://github.com/token",
|
|
309
|
+
defaultScopes: ["repo"],
|
|
310
|
+
scopePolicy: {},
|
|
311
|
+
},
|
|
312
|
+
]);
|
|
313
|
+
|
|
314
|
+
const row = getProvider("github");
|
|
315
|
+
expect(row).toBeDefined();
|
|
316
|
+
expect(row!.scopeSeparator).toBe(" ");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("re-seeding with a changed scopeSeparator overwrites the stored value", () => {
|
|
320
|
+
seedProviders([
|
|
321
|
+
{
|
|
322
|
+
provider: "linear",
|
|
323
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
324
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
325
|
+
defaultScopes: ["read"],
|
|
326
|
+
scopePolicy: {},
|
|
327
|
+
scopeSeparator: " ",
|
|
328
|
+
},
|
|
329
|
+
]);
|
|
330
|
+
|
|
331
|
+
const first = getProvider("linear");
|
|
332
|
+
expect(first!.scopeSeparator).toBe(" ");
|
|
333
|
+
|
|
334
|
+
// Re-seed with a different separator — it should be overwritten,
|
|
335
|
+
// proving scopeSeparator is in the onConflictDoUpdate set clause.
|
|
336
|
+
seedProviders([
|
|
337
|
+
{
|
|
338
|
+
provider: "linear",
|
|
339
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
340
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
341
|
+
defaultScopes: ["read"],
|
|
342
|
+
scopePolicy: {},
|
|
343
|
+
scopeSeparator: ",",
|
|
344
|
+
},
|
|
345
|
+
]);
|
|
346
|
+
|
|
347
|
+
const row = getProvider("linear");
|
|
348
|
+
expect(row!.scopeSeparator).toBe(",");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("persists refreshUrl when provided", () => {
|
|
352
|
+
seedProviders([
|
|
353
|
+
{
|
|
354
|
+
provider: "test-provider",
|
|
355
|
+
authorizeUrl: "https://example.com/authorize",
|
|
356
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
357
|
+
refreshUrl: "https://refresh.example.com/token",
|
|
358
|
+
defaultScopes: ["read"],
|
|
359
|
+
scopePolicy: {},
|
|
360
|
+
},
|
|
361
|
+
]);
|
|
362
|
+
|
|
363
|
+
const row = getProvider("test-provider");
|
|
364
|
+
expect(row).toBeDefined();
|
|
365
|
+
expect(row!.refreshUrl).toBe("https://refresh.example.com/token");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("refreshUrl defaults to null when omitted", () => {
|
|
369
|
+
seedProviders([
|
|
370
|
+
{
|
|
371
|
+
provider: "test-provider",
|
|
372
|
+
authorizeUrl: "https://example.com/authorize",
|
|
373
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
374
|
+
defaultScopes: ["read"],
|
|
375
|
+
scopePolicy: {},
|
|
376
|
+
},
|
|
377
|
+
]);
|
|
378
|
+
|
|
379
|
+
const row = getProvider("test-provider");
|
|
380
|
+
expect(row).toBeDefined();
|
|
381
|
+
expect(row!.refreshUrl).toBeNull();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("re-seeding with a changed refreshUrl overwrites the stored value", () => {
|
|
385
|
+
seedProviders([
|
|
386
|
+
{
|
|
387
|
+
provider: "test-provider",
|
|
388
|
+
authorizeUrl: "https://example.com/authorize",
|
|
389
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
390
|
+
refreshUrl: "https://refresh.example.com/token",
|
|
391
|
+
defaultScopes: ["read"],
|
|
392
|
+
scopePolicy: {},
|
|
393
|
+
},
|
|
394
|
+
]);
|
|
395
|
+
|
|
396
|
+
const first = getProvider("test-provider");
|
|
397
|
+
expect(first!.refreshUrl).toBe("https://refresh.example.com/token");
|
|
398
|
+
|
|
399
|
+
// Re-seed with a different refreshUrl — it should be overwritten,
|
|
400
|
+
// proving refreshUrl is in the onConflictDoUpdate set clause (not
|
|
401
|
+
// preserved like defaultScopes).
|
|
402
|
+
seedProviders([
|
|
403
|
+
{
|
|
404
|
+
provider: "test-provider",
|
|
405
|
+
authorizeUrl: "https://example.com/authorize",
|
|
406
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
407
|
+
refreshUrl: "https://refresh-v2.example.com/token",
|
|
408
|
+
defaultScopes: ["read"],
|
|
409
|
+
scopePolicy: {},
|
|
410
|
+
},
|
|
411
|
+
]);
|
|
412
|
+
|
|
413
|
+
const row = getProvider("test-provider");
|
|
414
|
+
expect(row!.refreshUrl).toBe("https://refresh-v2.example.com/token");
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test("persists revokeUrl and revokeBodyTemplate when provided", () => {
|
|
418
|
+
seedProviders([
|
|
419
|
+
{
|
|
420
|
+
provider: "test-provider",
|
|
421
|
+
authorizeUrl: "https://example.com/authorize",
|
|
422
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
423
|
+
revokeUrl: "https://revoke.example.com",
|
|
424
|
+
revokeBodyTemplate: { token: "{access_token}" },
|
|
425
|
+
defaultScopes: ["read"],
|
|
426
|
+
scopePolicy: {},
|
|
427
|
+
},
|
|
428
|
+
]);
|
|
429
|
+
|
|
430
|
+
const row = getProvider("test-provider");
|
|
431
|
+
expect(row).toBeDefined();
|
|
432
|
+
expect(row!.revokeUrl).toBe("https://revoke.example.com");
|
|
433
|
+
expect(JSON.parse(row!.revokeBodyTemplate!)).toEqual({
|
|
434
|
+
token: "{access_token}",
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test("revokeUrl and revokeBodyTemplate default to null when omitted", () => {
|
|
439
|
+
seedProviders([
|
|
440
|
+
{
|
|
441
|
+
provider: "test-provider",
|
|
442
|
+
authorizeUrl: "https://example.com/authorize",
|
|
443
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
444
|
+
defaultScopes: ["read"],
|
|
445
|
+
scopePolicy: {},
|
|
446
|
+
},
|
|
447
|
+
]);
|
|
448
|
+
|
|
449
|
+
const row = getProvider("test-provider");
|
|
450
|
+
expect(row).toBeDefined();
|
|
451
|
+
expect(row!.revokeUrl).toBeNull();
|
|
452
|
+
expect(row!.revokeBodyTemplate).toBeNull();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test("writes logoUrl on insert", () => {
|
|
456
|
+
seedProviders([
|
|
457
|
+
{
|
|
458
|
+
provider: "google",
|
|
459
|
+
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
460
|
+
tokenExchangeUrl: "https://oauth2.googleapis.com/token",
|
|
461
|
+
defaultScopes: ["openid"],
|
|
462
|
+
scopePolicy: {},
|
|
463
|
+
logoUrl: "https://cdn.simpleicons.org/google",
|
|
464
|
+
},
|
|
465
|
+
]);
|
|
466
|
+
|
|
467
|
+
const row = getProvider("google");
|
|
468
|
+
expect(row).toBeDefined();
|
|
469
|
+
expect(row!.logoUrl).toBe("https://cdn.simpleicons.org/google");
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("overwrites logoUrl on conflict", () => {
|
|
473
|
+
seedProviders([
|
|
474
|
+
{
|
|
475
|
+
provider: "google",
|
|
476
|
+
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
477
|
+
tokenExchangeUrl: "https://oauth2.googleapis.com/token",
|
|
478
|
+
defaultScopes: ["openid"],
|
|
479
|
+
scopePolicy: {},
|
|
480
|
+
logoUrl: "https://cdn.simpleicons.org/google",
|
|
481
|
+
},
|
|
482
|
+
]);
|
|
483
|
+
|
|
484
|
+
expect(getProvider("google")!.logoUrl).toBe(
|
|
485
|
+
"https://cdn.simpleicons.org/google",
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
// Re-seed with a different logoUrl — it should be overwritten,
|
|
489
|
+
// proving logoUrl is in the onConflictDoUpdate set clause alongside
|
|
490
|
+
// the other display-metadata fields.
|
|
491
|
+
seedProviders([
|
|
492
|
+
{
|
|
493
|
+
provider: "google",
|
|
494
|
+
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
495
|
+
tokenExchangeUrl: "https://oauth2.googleapis.com/token",
|
|
496
|
+
defaultScopes: ["openid"],
|
|
497
|
+
scopePolicy: {},
|
|
498
|
+
logoUrl: "https://cdn.simpleicons.org/google-v2",
|
|
499
|
+
},
|
|
500
|
+
]);
|
|
501
|
+
|
|
502
|
+
const row = getProvider("google");
|
|
503
|
+
expect(row!.logoUrl).toBe("https://cdn.simpleicons.org/google-v2");
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test("re-seeding with a changed revokeUrl overwrites the stored value", () => {
|
|
507
|
+
seedProviders([
|
|
508
|
+
{
|
|
509
|
+
provider: "test-provider",
|
|
510
|
+
authorizeUrl: "https://example.com/authorize",
|
|
511
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
512
|
+
revokeUrl: "https://revoke.example.com",
|
|
513
|
+
defaultScopes: ["read"],
|
|
514
|
+
scopePolicy: {},
|
|
515
|
+
},
|
|
516
|
+
]);
|
|
517
|
+
|
|
518
|
+
const first = getProvider("test-provider");
|
|
519
|
+
expect(first!.revokeUrl).toBe("https://revoke.example.com");
|
|
520
|
+
|
|
521
|
+
// Re-seed with a different revokeUrl — it should be overwritten,
|
|
522
|
+
// proving revokeUrl is in the onConflictDoUpdate set clause (not
|
|
523
|
+
// preserved like defaultScopes).
|
|
524
|
+
seedProviders([
|
|
525
|
+
{
|
|
526
|
+
provider: "test-provider",
|
|
527
|
+
authorizeUrl: "https://example.com/authorize",
|
|
528
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
529
|
+
revokeUrl: "https://revoke-v2.example.com",
|
|
530
|
+
defaultScopes: ["read"],
|
|
531
|
+
scopePolicy: {},
|
|
532
|
+
},
|
|
533
|
+
]);
|
|
534
|
+
|
|
535
|
+
const row = getProvider("test-provider");
|
|
536
|
+
expect(row!.revokeUrl).toBe("https://revoke-v2.example.com");
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test("seedOAuthProviders seeds Google, Twitter, and Linear with revoke config and leaves other providers null", () => {
|
|
540
|
+
seedOAuthProviders();
|
|
541
|
+
|
|
542
|
+
const google = getProvider("google");
|
|
543
|
+
expect(google).toBeDefined();
|
|
544
|
+
expect(google!.revokeUrl).toBe("https://oauth2.googleapis.com/revoke");
|
|
545
|
+
expect(JSON.parse(google!.revokeBodyTemplate!)).toEqual({
|
|
546
|
+
token: "{access_token}",
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
const twitter = getProvider("twitter");
|
|
550
|
+
expect(twitter).toBeDefined();
|
|
551
|
+
expect(twitter!.revokeUrl).toBe("https://api.x.com/2/oauth2/revoke");
|
|
552
|
+
expect(JSON.parse(twitter!.revokeBodyTemplate!)).toEqual({
|
|
553
|
+
token: "{access_token}",
|
|
554
|
+
token_type_hint: "access_token",
|
|
555
|
+
client_id: "{client_id}",
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
const linear = getProvider("linear");
|
|
559
|
+
expect(linear).toBeDefined();
|
|
560
|
+
expect(linear!.revokeUrl).toBe("https://api.linear.app/oauth/revoke");
|
|
561
|
+
expect(JSON.parse(linear!.revokeBodyTemplate!)).toEqual({
|
|
562
|
+
token: "{access_token}",
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
const slack = getProvider("slack");
|
|
566
|
+
expect(slack).toBeDefined();
|
|
567
|
+
expect(slack!.revokeUrl).toBeNull();
|
|
568
|
+
expect(slack!.revokeBodyTemplate).toBeNull();
|
|
569
|
+
|
|
570
|
+
const github = getProvider("github");
|
|
571
|
+
expect(github).toBeDefined();
|
|
572
|
+
expect(github!.revokeUrl).toBeNull();
|
|
573
|
+
|
|
574
|
+
const outlook = getProvider("outlook");
|
|
575
|
+
expect(outlook).toBeDefined();
|
|
576
|
+
expect(outlook!.revokeUrl).toBeNull();
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
test("applies client_secret_post default when tokenEndpointAuthMethod is omitted from seed", () => {
|
|
580
|
+
seedProviders([
|
|
581
|
+
{
|
|
582
|
+
provider: "no-auth-method-provider",
|
|
583
|
+
authorizeUrl: "https://example.com/authorize",
|
|
584
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
585
|
+
defaultScopes: [],
|
|
586
|
+
scopePolicy: {},
|
|
587
|
+
// Note: tokenEndpointAuthMethod intentionally omitted
|
|
588
|
+
},
|
|
589
|
+
]);
|
|
590
|
+
const row = getProvider("no-auth-method-provider");
|
|
591
|
+
expect(row).toBeDefined();
|
|
592
|
+
expect(row!.tokenEndpointAuthMethod).toBe("client_secret_post");
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test("defaults tokenExchangeBodyFormat to 'form' when omitted from seed", () => {
|
|
596
|
+
seedProviders([
|
|
597
|
+
{
|
|
598
|
+
provider: "no-body-format-provider",
|
|
599
|
+
authorizeUrl: "https://example.com/authorize",
|
|
600
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
601
|
+
defaultScopes: [],
|
|
602
|
+
scopePolicy: {},
|
|
603
|
+
// Note: tokenExchangeBodyFormat intentionally omitted
|
|
604
|
+
},
|
|
605
|
+
]);
|
|
606
|
+
const row = getProvider("no-body-format-provider");
|
|
607
|
+
expect(row).toBeDefined();
|
|
608
|
+
expect(row!.tokenExchangeBodyFormat).toBe("form");
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
test("persists explicit tokenExchangeBodyFormat value on seed", () => {
|
|
612
|
+
seedProviders([
|
|
613
|
+
{
|
|
614
|
+
provider: "json-body-format-provider",
|
|
615
|
+
authorizeUrl: "https://example.com/authorize",
|
|
616
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
617
|
+
defaultScopes: [],
|
|
618
|
+
scopePolicy: {},
|
|
619
|
+
tokenExchangeBodyFormat: "json",
|
|
620
|
+
},
|
|
621
|
+
]);
|
|
622
|
+
const row = getProvider("json-body-format-provider");
|
|
623
|
+
expect(row).toBeDefined();
|
|
624
|
+
expect(row!.tokenExchangeBodyFormat).toBe("json");
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test("migration 216 backfills NULL token_endpoint_auth_method to client_secret_post", () => {
|
|
628
|
+
// Use raw SQLite to bypass Drizzle's NOT NULL enforcement and insert
|
|
629
|
+
// a legacy-shaped row with NULL token_endpoint_auth_method.
|
|
630
|
+
const db = getDb();
|
|
631
|
+
const raw = getSqliteFrom(db);
|
|
632
|
+
raw.exec(`
|
|
633
|
+
INSERT INTO oauth_providers (
|
|
634
|
+
provider_key, auth_url, token_url, token_endpoint_auth_method,
|
|
635
|
+
default_scopes, scope_policy, scope_separator, requires_client_secret,
|
|
636
|
+
created_at, updated_at
|
|
637
|
+
) VALUES (
|
|
638
|
+
'legacy-null-provider',
|
|
639
|
+
'https://example.com/authorize',
|
|
640
|
+
'https://example.com/token',
|
|
641
|
+
NULL,
|
|
642
|
+
'[]',
|
|
643
|
+
'{}',
|
|
644
|
+
' ',
|
|
645
|
+
1,
|
|
646
|
+
${Date.now()},
|
|
647
|
+
${Date.now()}
|
|
648
|
+
)
|
|
649
|
+
`);
|
|
650
|
+
|
|
651
|
+
// Run the migration directly
|
|
652
|
+
migrateOAuthProvidersTokenAuthMethodDefault(db);
|
|
653
|
+
|
|
654
|
+
// Verify the row was backfilled
|
|
655
|
+
const row = raw
|
|
656
|
+
.prepare(
|
|
657
|
+
`SELECT token_endpoint_auth_method FROM oauth_providers WHERE provider_key = 'legacy-null-provider'`,
|
|
658
|
+
)
|
|
659
|
+
.get() as { token_endpoint_auth_method: string };
|
|
660
|
+
expect(row.token_endpoint_auth_method).toBe("client_secret_post");
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
test("migration 216 is idempotent — running twice on backfilled rows is a no-op", () => {
|
|
664
|
+
seedProviders([
|
|
665
|
+
{
|
|
666
|
+
provider: "already-set-provider",
|
|
667
|
+
authorizeUrl: "https://example.com/authorize",
|
|
668
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
669
|
+
tokenEndpointAuthMethod: "client_secret_basic",
|
|
670
|
+
defaultScopes: [],
|
|
671
|
+
scopePolicy: {},
|
|
672
|
+
},
|
|
673
|
+
]);
|
|
674
|
+
|
|
675
|
+
const db = getDb();
|
|
676
|
+
migrateOAuthProvidersTokenAuthMethodDefault(db);
|
|
677
|
+
migrateOAuthProvidersTokenAuthMethodDefault(db);
|
|
678
|
+
|
|
679
|
+
const row = getProvider("already-set-provider");
|
|
680
|
+
expect(row!.tokenEndpointAuthMethod).toBe("client_secret_basic");
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
describe("getProvider", () => {
|
|
685
|
+
test("returns the correct row", () => {
|
|
686
|
+
seedProviders([
|
|
687
|
+
{
|
|
688
|
+
provider: "github",
|
|
689
|
+
authorizeUrl: "https://github.com/authorize",
|
|
690
|
+
tokenExchangeUrl: "https://github.com/token",
|
|
691
|
+
defaultScopes: ["repo"],
|
|
692
|
+
scopePolicy: {},
|
|
693
|
+
},
|
|
694
|
+
]);
|
|
695
|
+
|
|
696
|
+
const row = getProvider("github");
|
|
697
|
+
expect(row).toBeDefined();
|
|
698
|
+
expect(row!.provider).toBe("github");
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
test("returns undefined for unknown keys", () => {
|
|
702
|
+
expect(getProvider("nonexistent")).toBeUndefined();
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
describe("registerProvider", () => {
|
|
707
|
+
test("creates a new row", () => {
|
|
708
|
+
const row = registerProvider({
|
|
709
|
+
provider: "linear",
|
|
710
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
711
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
712
|
+
defaultScopes: ["read"],
|
|
713
|
+
scopePolicy: {},
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
expect(row.provider).toBe("linear");
|
|
717
|
+
expect(row.authorizeUrl).toBe("https://linear.app/oauth/authorize");
|
|
718
|
+
|
|
719
|
+
const fetched = getProvider("linear");
|
|
720
|
+
expect(fetched).toBeDefined();
|
|
721
|
+
expect(fetched!.provider).toBe("linear");
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
test("throws for duplicate provider_key", () => {
|
|
725
|
+
registerProvider({
|
|
726
|
+
provider: "linear",
|
|
727
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
728
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
729
|
+
defaultScopes: ["read"],
|
|
730
|
+
scopePolicy: {},
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
expect(() =>
|
|
734
|
+
registerProvider({
|
|
735
|
+
provider: "linear",
|
|
736
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
737
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
738
|
+
defaultScopes: ["read"],
|
|
739
|
+
scopePolicy: {},
|
|
740
|
+
}),
|
|
741
|
+
).toThrow(/already exists.*linear/);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
test("persists scopeSeparator and round-trips via getProvider", () => {
|
|
745
|
+
registerProvider({
|
|
746
|
+
provider: "linear",
|
|
747
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
748
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
749
|
+
defaultScopes: ["read"],
|
|
750
|
+
scopePolicy: {},
|
|
751
|
+
scopeSeparator: ";",
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
const fetched = getProvider("linear");
|
|
755
|
+
expect(fetched).toBeDefined();
|
|
756
|
+
expect(fetched!.scopeSeparator).toBe(";");
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
test("scopeSeparator defaults to ' ' when omitted", () => {
|
|
760
|
+
registerProvider({
|
|
761
|
+
provider: "linear",
|
|
762
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
763
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
764
|
+
defaultScopes: ["read"],
|
|
765
|
+
scopePolicy: {},
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
const fetched = getProvider("linear");
|
|
769
|
+
expect(fetched).toBeDefined();
|
|
770
|
+
expect(fetched!.scopeSeparator).toBe(" ");
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
test("persists refreshUrl and round-trips via getProvider", () => {
|
|
774
|
+
registerProvider({
|
|
775
|
+
provider: "linear",
|
|
776
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
777
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
778
|
+
refreshUrl: "https://api.linear.app/oauth/refresh",
|
|
779
|
+
defaultScopes: ["read"],
|
|
780
|
+
scopePolicy: {},
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
const fetched = getProvider("linear");
|
|
784
|
+
expect(fetched).toBeDefined();
|
|
785
|
+
expect(fetched!.refreshUrl).toBe("https://api.linear.app/oauth/refresh");
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
test("refreshUrl defaults to null when omitted", () => {
|
|
789
|
+
registerProvider({
|
|
790
|
+
provider: "linear",
|
|
791
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
792
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
793
|
+
defaultScopes: ["read"],
|
|
794
|
+
scopePolicy: {},
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
const fetched = getProvider("linear");
|
|
798
|
+
expect(fetched).toBeDefined();
|
|
799
|
+
expect(fetched!.refreshUrl).toBeNull();
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
test("persists revokeUrl and revokeBodyTemplate and round-trips via getProvider", () => {
|
|
803
|
+
registerProvider({
|
|
804
|
+
provider: "linear",
|
|
805
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
806
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
807
|
+
revokeUrl: "https://api.linear.app/oauth/revoke",
|
|
808
|
+
revokeBodyTemplate: { token: "{access_token}" },
|
|
809
|
+
defaultScopes: ["read"],
|
|
810
|
+
scopePolicy: {},
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
const fetched = getProvider("linear");
|
|
814
|
+
expect(fetched).toBeDefined();
|
|
815
|
+
expect(fetched!.revokeUrl).toBe("https://api.linear.app/oauth/revoke");
|
|
816
|
+
expect(JSON.parse(fetched!.revokeBodyTemplate!)).toEqual({
|
|
817
|
+
token: "{access_token}",
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
test("applies client_secret_post default when tokenEndpointAuthMethod is omitted", () => {
|
|
822
|
+
const row = registerProvider({
|
|
823
|
+
provider: "custom-default-test",
|
|
824
|
+
authorizeUrl: "https://example.com/authorize",
|
|
825
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
826
|
+
defaultScopes: [],
|
|
827
|
+
scopePolicy: {},
|
|
828
|
+
// Note: tokenEndpointAuthMethod intentionally omitted
|
|
829
|
+
});
|
|
830
|
+
expect(row.tokenEndpointAuthMethod).toBe("client_secret_post");
|
|
831
|
+
|
|
832
|
+
const fetched = getProvider("custom-default-test");
|
|
833
|
+
expect(fetched!.tokenEndpointAuthMethod).toBe("client_secret_post");
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
test("preserves explicit client_secret_basic when registering a provider", () => {
|
|
837
|
+
const row = registerProvider({
|
|
838
|
+
provider: "custom-basic-test",
|
|
839
|
+
authorizeUrl: "https://example.com/authorize",
|
|
840
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
841
|
+
defaultScopes: [],
|
|
842
|
+
scopePolicy: {},
|
|
843
|
+
tokenEndpointAuthMethod: "client_secret_basic",
|
|
844
|
+
});
|
|
845
|
+
expect(row.tokenEndpointAuthMethod).toBe("client_secret_basic");
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
test("defaults tokenExchangeBodyFormat to 'form' when omitted", () => {
|
|
849
|
+
const row = registerProvider({
|
|
850
|
+
provider: "no-body-format-test",
|
|
851
|
+
authorizeUrl: "https://example.com/authorize",
|
|
852
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
853
|
+
defaultScopes: [],
|
|
854
|
+
scopePolicy: {},
|
|
855
|
+
// Note: tokenExchangeBodyFormat intentionally omitted
|
|
856
|
+
});
|
|
857
|
+
expect(row.tokenExchangeBodyFormat).toBe("form");
|
|
858
|
+
|
|
859
|
+
const fetched = getProvider("no-body-format-test");
|
|
860
|
+
expect(fetched!.tokenExchangeBodyFormat).toBe("form");
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
test("persists explicit tokenExchangeBodyFormat 'json' when registering a provider", () => {
|
|
864
|
+
const row = registerProvider({
|
|
865
|
+
provider: "json-body-format-test",
|
|
866
|
+
authorizeUrl: "https://example.com/authorize",
|
|
867
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
868
|
+
defaultScopes: [],
|
|
869
|
+
scopePolicy: {},
|
|
870
|
+
tokenExchangeBodyFormat: "json",
|
|
871
|
+
});
|
|
872
|
+
expect(row.tokenExchangeBodyFormat).toBe("json");
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
test("stores logoUrl when provided", () => {
|
|
876
|
+
registerProvider({
|
|
877
|
+
provider: "notion",
|
|
878
|
+
authorizeUrl: "https://api.notion.com/v1/oauth/authorize",
|
|
879
|
+
tokenExchangeUrl: "https://api.notion.com/v1/oauth/token",
|
|
880
|
+
defaultScopes: ["read"],
|
|
881
|
+
scopePolicy: {},
|
|
882
|
+
logoUrl: "https://cdn.simpleicons.org/notion",
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
const fetched = getProvider("notion");
|
|
886
|
+
expect(fetched).toBeDefined();
|
|
887
|
+
expect(fetched!.logoUrl).toBe("https://cdn.simpleicons.org/notion");
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
test("defaults logoUrl to null when omitted", () => {
|
|
891
|
+
registerProvider({
|
|
892
|
+
provider: "linear",
|
|
893
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
894
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
895
|
+
defaultScopes: ["read"],
|
|
896
|
+
scopePolicy: {},
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
const fetched = getProvider("linear");
|
|
900
|
+
expect(fetched).toBeDefined();
|
|
901
|
+
expect(fetched!.logoUrl).toBeNull();
|
|
902
|
+
});
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
describe("updateProvider", () => {
|
|
906
|
+
test("updates scopeSeparator on an existing row", () => {
|
|
907
|
+
seedProviders([
|
|
908
|
+
{
|
|
909
|
+
provider: "github",
|
|
910
|
+
authorizeUrl: "https://github.com/authorize",
|
|
911
|
+
tokenExchangeUrl: "https://github.com/token",
|
|
912
|
+
defaultScopes: ["repo"],
|
|
913
|
+
scopePolicy: {},
|
|
914
|
+
},
|
|
915
|
+
]);
|
|
916
|
+
|
|
917
|
+
const before = getProvider("github");
|
|
918
|
+
expect(before!.scopeSeparator).toBe(" ");
|
|
919
|
+
|
|
920
|
+
const updated = updateProvider("github", { scopeSeparator: "," });
|
|
921
|
+
expect(updated).toBeDefined();
|
|
922
|
+
expect(updated!.scopeSeparator).toBe(",");
|
|
923
|
+
|
|
924
|
+
const fetched = getProvider("github");
|
|
925
|
+
expect(fetched!.scopeSeparator).toBe(",");
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
test("coerces empty-string scopeSeparator to default ' '", () => {
|
|
929
|
+
// An empty separator would join scopes into a single concatenated token
|
|
930
|
+
// (e.g. ["read","write"].join("") === "readwrite") which is never a
|
|
931
|
+
// valid OAuth authorize URL value. Coerce to the default.
|
|
932
|
+
seedProviders([
|
|
933
|
+
{
|
|
934
|
+
provider: "github",
|
|
935
|
+
authorizeUrl: "https://github.com/authorize",
|
|
936
|
+
tokenExchangeUrl: "https://github.com/token",
|
|
937
|
+
defaultScopes: ["repo"],
|
|
938
|
+
scopePolicy: {},
|
|
939
|
+
scopeSeparator: ",",
|
|
940
|
+
},
|
|
941
|
+
]);
|
|
942
|
+
|
|
943
|
+
expect(getProvider("github")!.scopeSeparator).toBe(",");
|
|
944
|
+
|
|
945
|
+
const updated = updateProvider("github", { scopeSeparator: "" });
|
|
946
|
+
expect(updated).toBeDefined();
|
|
947
|
+
expect(updated!.scopeSeparator).toBe(" ");
|
|
948
|
+
expect(getProvider("github")!.scopeSeparator).toBe(" ");
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
test("sets refreshUrl on an existing row where it was previously null", () => {
|
|
952
|
+
seedProviders([
|
|
953
|
+
{
|
|
954
|
+
provider: "github",
|
|
955
|
+
authorizeUrl: "https://github.com/authorize",
|
|
956
|
+
tokenExchangeUrl: "https://github.com/token",
|
|
957
|
+
defaultScopes: ["repo"],
|
|
958
|
+
scopePolicy: {},
|
|
959
|
+
},
|
|
960
|
+
]);
|
|
961
|
+
|
|
962
|
+
const before = getProvider("github");
|
|
963
|
+
expect(before!.refreshUrl).toBeNull();
|
|
964
|
+
|
|
965
|
+
const updated = updateProvider("github", {
|
|
966
|
+
refreshUrl: "https://github.com/login/oauth/refresh",
|
|
967
|
+
});
|
|
968
|
+
expect(updated).toBeDefined();
|
|
969
|
+
expect(updated!.refreshUrl).toBe(
|
|
970
|
+
"https://github.com/login/oauth/refresh",
|
|
971
|
+
);
|
|
972
|
+
|
|
973
|
+
const fetched = getProvider("github");
|
|
974
|
+
expect(fetched!.refreshUrl).toBe(
|
|
975
|
+
"https://github.com/login/oauth/refresh",
|
|
976
|
+
);
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
test("leaves refreshUrl unchanged when not passed to updateProvider", () => {
|
|
980
|
+
seedProviders([
|
|
981
|
+
{
|
|
982
|
+
provider: "github",
|
|
983
|
+
authorizeUrl: "https://github.com/authorize",
|
|
984
|
+
tokenExchangeUrl: "https://github.com/token",
|
|
985
|
+
refreshUrl: "https://github.com/login/oauth/refresh",
|
|
986
|
+
defaultScopes: ["repo"],
|
|
987
|
+
scopePolicy: {},
|
|
988
|
+
},
|
|
989
|
+
]);
|
|
990
|
+
|
|
991
|
+
expect(getProvider("github")!.refreshUrl).toBe(
|
|
992
|
+
"https://github.com/login/oauth/refresh",
|
|
993
|
+
);
|
|
994
|
+
|
|
995
|
+
// Update a different field — refreshUrl should be left alone.
|
|
996
|
+
const updated = updateProvider("github", {
|
|
997
|
+
displayLabel: "GitHub (updated)",
|
|
998
|
+
});
|
|
999
|
+
expect(updated).toBeDefined();
|
|
1000
|
+
expect(updated!.refreshUrl).toBe(
|
|
1001
|
+
"https://github.com/login/oauth/refresh",
|
|
1002
|
+
);
|
|
1003
|
+
expect(updated!.displayLabel).toBe("GitHub (updated)");
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
test("sets revokeUrl on an existing row where it was previously null", () => {
|
|
170
1007
|
seedProviders([
|
|
171
1008
|
{
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
1009
|
+
provider: "github",
|
|
1010
|
+
authorizeUrl: "https://github.com/authorize",
|
|
1011
|
+
tokenExchangeUrl: "https://github.com/token",
|
|
175
1012
|
defaultScopes: ["repo"],
|
|
176
1013
|
scopePolicy: {},
|
|
177
|
-
pingUrl: "https://api.github.com/user",
|
|
178
1014
|
},
|
|
179
1015
|
]);
|
|
180
|
-
|
|
181
|
-
|
|
1016
|
+
|
|
1017
|
+
const before = getProvider("github");
|
|
1018
|
+
expect(before!.revokeUrl).toBeNull();
|
|
1019
|
+
|
|
1020
|
+
const updated = updateProvider("github", {
|
|
1021
|
+
revokeUrl: "https://github.com/login/oauth/revoke",
|
|
1022
|
+
});
|
|
1023
|
+
expect(updated).toBeDefined();
|
|
1024
|
+
expect(updated!.revokeUrl).toBe("https://github.com/login/oauth/revoke");
|
|
1025
|
+
|
|
1026
|
+
const fetched = getProvider("github");
|
|
1027
|
+
expect(fetched!.revokeUrl).toBe("https://github.com/login/oauth/revoke");
|
|
182
1028
|
});
|
|
183
1029
|
|
|
184
|
-
test("
|
|
1030
|
+
test("sets revokeBodyTemplate on an existing row and JSON round-trips", () => {
|
|
185
1031
|
seedProviders([
|
|
186
1032
|
{
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
1033
|
+
provider: "github",
|
|
1034
|
+
authorizeUrl: "https://github.com/authorize",
|
|
1035
|
+
tokenExchangeUrl: "https://github.com/token",
|
|
190
1036
|
defaultScopes: ["repo"],
|
|
191
1037
|
scopePolicy: {},
|
|
192
1038
|
},
|
|
193
1039
|
]);
|
|
194
|
-
|
|
195
|
-
|
|
1040
|
+
|
|
1041
|
+
const before = getProvider("github");
|
|
1042
|
+
expect(before!.revokeBodyTemplate).toBeNull();
|
|
1043
|
+
|
|
1044
|
+
const updated = updateProvider("github", {
|
|
1045
|
+
revokeBodyTemplate: {
|
|
1046
|
+
token: "{access_token}",
|
|
1047
|
+
client_id: "{client_id}",
|
|
1048
|
+
},
|
|
1049
|
+
});
|
|
1050
|
+
expect(updated).toBeDefined();
|
|
1051
|
+
expect(JSON.parse(updated!.revokeBodyTemplate!)).toEqual({
|
|
1052
|
+
token: "{access_token}",
|
|
1053
|
+
client_id: "{client_id}",
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
const fetched = getProvider("github");
|
|
1057
|
+
expect(JSON.parse(fetched!.revokeBodyTemplate!)).toEqual({
|
|
1058
|
+
token: "{access_token}",
|
|
1059
|
+
client_id: "{client_id}",
|
|
1060
|
+
});
|
|
196
1061
|
});
|
|
197
1062
|
|
|
198
|
-
test("
|
|
199
|
-
// Initial seed with all fields
|
|
1063
|
+
test("leaves revokeUrl and revokeBodyTemplate unchanged when not passed to updateProvider", () => {
|
|
200
1064
|
seedProviders([
|
|
201
1065
|
{
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
1066
|
+
provider: "github",
|
|
1067
|
+
authorizeUrl: "https://github.com/authorize",
|
|
1068
|
+
tokenExchangeUrl: "https://github.com/token",
|
|
1069
|
+
revokeUrl: "https://github.com/login/oauth/revoke",
|
|
1070
|
+
revokeBodyTemplate: { token: "{access_token}" },
|
|
206
1071
|
defaultScopes: ["repo"],
|
|
207
|
-
scopePolicy: {
|
|
208
|
-
userinfoUrl: "https://api.github.com/user",
|
|
209
|
-
baseUrl: "https://api.github.com",
|
|
210
|
-
extraParams: { prompt: "consent" },
|
|
211
|
-
|
|
212
|
-
pingUrl: "https://api.github.com/user",
|
|
1072
|
+
scopePolicy: {},
|
|
213
1073
|
},
|
|
214
1074
|
]);
|
|
215
1075
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
required: ["repo"],
|
|
223
|
-
allowAdditionalScopes: true,
|
|
224
|
-
}),
|
|
225
|
-
baseUrl: "https://custom.github.com/api",
|
|
226
|
-
})
|
|
227
|
-
.where(eq(oauthProviders.providerKey, "github"))
|
|
228
|
-
.run();
|
|
1076
|
+
expect(getProvider("github")!.revokeUrl).toBe(
|
|
1077
|
+
"https://github.com/login/oauth/revoke",
|
|
1078
|
+
);
|
|
1079
|
+
expect(JSON.parse(getProvider("github")!.revokeBodyTemplate!)).toEqual({
|
|
1080
|
+
token: "{access_token}",
|
|
1081
|
+
});
|
|
229
1082
|
|
|
230
|
-
//
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
1083
|
+
// Update a different field — revoke fields should be left alone.
|
|
1084
|
+
const updated = updateProvider("github", {
|
|
1085
|
+
displayLabel: "GitHub (updated)",
|
|
1086
|
+
});
|
|
1087
|
+
expect(updated).toBeDefined();
|
|
1088
|
+
expect(updated!.revokeUrl).toBe("https://github.com/login/oauth/revoke");
|
|
1089
|
+
expect(JSON.parse(updated!.revokeBodyTemplate!)).toEqual({
|
|
1090
|
+
token: "{access_token}",
|
|
1091
|
+
});
|
|
1092
|
+
expect(updated!.displayLabel).toBe("GitHub (updated)");
|
|
1093
|
+
});
|
|
238
1094
|
|
|
239
|
-
|
|
1095
|
+
test("coerces empty string tokenEndpointAuthMethod to client_secret_post", () => {
|
|
240
1096
|
seedProviders([
|
|
241
1097
|
{
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
1098
|
+
provider: "update-empty-test",
|
|
1099
|
+
authorizeUrl: "https://example.com/authorize",
|
|
1100
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
245
1101
|
tokenEndpointAuthMethod: "client_secret_basic",
|
|
246
|
-
defaultScopes: [
|
|
1102
|
+
defaultScopes: [],
|
|
247
1103
|
scopePolicy: {},
|
|
248
|
-
userinfoUrl: "https://api.github.com/user-v2",
|
|
249
|
-
baseUrl: "https://api.github.com/v2",
|
|
250
|
-
extraParams: { prompt: "login" },
|
|
251
|
-
|
|
252
|
-
pingUrl: "https://api.github.com/user-v2",
|
|
253
1104
|
},
|
|
254
1105
|
]);
|
|
255
1106
|
|
|
256
|
-
|
|
257
|
-
|
|
1107
|
+
expect(getProvider("update-empty-test")!.tokenEndpointAuthMethod).toBe(
|
|
1108
|
+
"client_secret_basic",
|
|
1109
|
+
);
|
|
258
1110
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
expect(JSON.parse(row!.scopePolicy)).toEqual({
|
|
262
|
-
required: ["repo"],
|
|
263
|
-
allowAdditionalScopes: true,
|
|
1111
|
+
const updated = updateProvider("update-empty-test", {
|
|
1112
|
+
tokenEndpointAuthMethod: "",
|
|
264
1113
|
});
|
|
265
|
-
expect(
|
|
1114
|
+
expect(updated).toBeDefined();
|
|
1115
|
+
expect(updated!.tokenEndpointAuthMethod).toBe("client_secret_post");
|
|
266
1116
|
|
|
267
|
-
|
|
268
|
-
expect(row!.
|
|
269
|
-
expect(row!.tokenUrl).toBe("https://github.com/token-v2");
|
|
270
|
-
expect(row!.tokenEndpointAuthMethod).toBe("client_secret_basic");
|
|
271
|
-
expect(row!.userinfoUrl).toBe("https://api.github.com/user-v2");
|
|
272
|
-
expect(JSON.parse(row!.extraParams!)).toEqual({ prompt: "login" });
|
|
273
|
-
expect(row!.pingUrl).toBe("https://api.github.com/user-v2");
|
|
1117
|
+
const row = getProvider("update-empty-test");
|
|
1118
|
+
expect(row!.tokenEndpointAuthMethod).toBe("client_secret_post");
|
|
274
1119
|
});
|
|
275
|
-
});
|
|
276
1120
|
|
|
277
|
-
|
|
278
|
-
test("returns the correct row", () => {
|
|
1121
|
+
test("coerces empty string tokenExchangeBodyFormat to 'form'", () => {
|
|
279
1122
|
seedProviders([
|
|
280
1123
|
{
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
1124
|
+
provider: "update-empty-body-format-test",
|
|
1125
|
+
authorizeUrl: "https://example.com/authorize",
|
|
1126
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
1127
|
+
tokenExchangeBodyFormat: "json",
|
|
1128
|
+
defaultScopes: [],
|
|
285
1129
|
scopePolicy: {},
|
|
286
1130
|
},
|
|
287
1131
|
]);
|
|
288
1132
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
});
|
|
1133
|
+
expect(
|
|
1134
|
+
getProvider("update-empty-body-format-test")!.tokenExchangeBodyFormat,
|
|
1135
|
+
).toBe("json");
|
|
293
1136
|
|
|
294
|
-
|
|
295
|
-
|
|
1137
|
+
const updated = updateProvider("update-empty-body-format-test", {
|
|
1138
|
+
tokenExchangeBodyFormat: "",
|
|
1139
|
+
});
|
|
1140
|
+
expect(updated).toBeDefined();
|
|
1141
|
+
expect(updated!.tokenExchangeBodyFormat).toBe("form");
|
|
1142
|
+
|
|
1143
|
+
const row = getProvider("update-empty-body-format-test");
|
|
1144
|
+
expect(row!.tokenExchangeBodyFormat).toBe("form");
|
|
296
1145
|
});
|
|
297
|
-
});
|
|
298
1146
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
tokenUrl: "https://api.linear.app/oauth/token",
|
|
1147
|
+
test("sets logoUrl on an existing row where it was previously null", () => {
|
|
1148
|
+
registerProvider({
|
|
1149
|
+
provider: "linear",
|
|
1150
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
1151
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
305
1152
|
defaultScopes: ["read"],
|
|
306
1153
|
scopePolicy: {},
|
|
307
1154
|
});
|
|
308
1155
|
|
|
309
|
-
expect(
|
|
310
|
-
|
|
1156
|
+
expect(getProvider("linear")!.logoUrl).toBeNull();
|
|
1157
|
+
|
|
1158
|
+
const updated = updateProvider("linear", {
|
|
1159
|
+
logoUrl: "https://cdn.simpleicons.org/linear",
|
|
1160
|
+
});
|
|
1161
|
+
expect(updated).toBeDefined();
|
|
1162
|
+
expect(updated!.logoUrl).toBe("https://cdn.simpleicons.org/linear");
|
|
311
1163
|
|
|
312
1164
|
const fetched = getProvider("linear");
|
|
313
|
-
expect(fetched).
|
|
314
|
-
expect(fetched!.providerKey).toBe("linear");
|
|
1165
|
+
expect(fetched!.logoUrl).toBe("https://cdn.simpleicons.org/linear");
|
|
315
1166
|
});
|
|
316
1167
|
|
|
317
|
-
test("
|
|
1168
|
+
test("clears logoUrl when passed null", () => {
|
|
318
1169
|
registerProvider({
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
1170
|
+
provider: "notion",
|
|
1171
|
+
authorizeUrl: "https://api.notion.com/v1/oauth/authorize",
|
|
1172
|
+
tokenExchangeUrl: "https://api.notion.com/v1/oauth/token",
|
|
322
1173
|
defaultScopes: ["read"],
|
|
323
1174
|
scopePolicy: {},
|
|
1175
|
+
logoUrl: "https://cdn.simpleicons.org/notion",
|
|
324
1176
|
});
|
|
325
1177
|
|
|
326
|
-
expect(()
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
1178
|
+
expect(getProvider("notion")!.logoUrl).toBe(
|
|
1179
|
+
"https://cdn.simpleicons.org/notion",
|
|
1180
|
+
);
|
|
1181
|
+
|
|
1182
|
+
const updated = updateProvider("notion", { logoUrl: null });
|
|
1183
|
+
expect(updated).toBeDefined();
|
|
1184
|
+
expect(updated!.logoUrl).toBeNull();
|
|
1185
|
+
|
|
1186
|
+
expect(getProvider("notion")!.logoUrl).toBeNull();
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
test("leaves logoUrl unchanged when not passed to updateProvider", () => {
|
|
1190
|
+
registerProvider({
|
|
1191
|
+
provider: "notion",
|
|
1192
|
+
authorizeUrl: "https://api.notion.com/v1/oauth/authorize",
|
|
1193
|
+
tokenExchangeUrl: "https://api.notion.com/v1/oauth/token",
|
|
1194
|
+
defaultScopes: ["read"],
|
|
1195
|
+
scopePolicy: {},
|
|
1196
|
+
logoUrl: "https://cdn.simpleicons.org/notion",
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
expect(getProvider("notion")!.logoUrl).toBe(
|
|
1200
|
+
"https://cdn.simpleicons.org/notion",
|
|
1201
|
+
);
|
|
1202
|
+
|
|
1203
|
+
// Update a different field — logoUrl should be left alone.
|
|
1204
|
+
const updated = updateProvider("notion", {
|
|
1205
|
+
displayLabel: "Notion (updated)",
|
|
1206
|
+
});
|
|
1207
|
+
expect(updated).toBeDefined();
|
|
1208
|
+
expect(updated!.logoUrl).toBe("https://cdn.simpleicons.org/notion");
|
|
1209
|
+
expect(updated!.displayLabel).toBe("Notion (updated)");
|
|
1210
|
+
});
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
describe("scopeSeparator empty-string coercion", () => {
|
|
1214
|
+
test("seedProviders coerces empty-string scopeSeparator to ' '", () => {
|
|
1215
|
+
seedProviders([
|
|
1216
|
+
{
|
|
1217
|
+
provider: "linear",
|
|
1218
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
1219
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
331
1220
|
defaultScopes: ["read"],
|
|
332
1221
|
scopePolicy: {},
|
|
333
|
-
|
|
334
|
-
|
|
1222
|
+
scopeSeparator: "",
|
|
1223
|
+
},
|
|
1224
|
+
]);
|
|
1225
|
+
|
|
1226
|
+
const row = getProvider("linear");
|
|
1227
|
+
expect(row!.scopeSeparator).toBe(" ");
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
test("registerProvider coerces empty-string scopeSeparator to ' '", () => {
|
|
1231
|
+
registerProvider({
|
|
1232
|
+
provider: "linear",
|
|
1233
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
1234
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
1235
|
+
defaultScopes: ["read"],
|
|
1236
|
+
scopePolicy: {},
|
|
1237
|
+
scopeSeparator: "",
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
const row = getProvider("linear");
|
|
1241
|
+
expect(row!.scopeSeparator).toBe(" ");
|
|
335
1242
|
});
|
|
336
1243
|
});
|
|
337
1244
|
});
|
|
@@ -351,13 +1258,13 @@ describe("app operations", () => {
|
|
|
351
1258
|
expect(app.id).toMatch(
|
|
352
1259
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
|
|
353
1260
|
);
|
|
354
|
-
expect(app.
|
|
1261
|
+
expect(app.provider).toBe("github");
|
|
355
1262
|
expect(app.clientId).toBe("client-abc");
|
|
356
1263
|
expect(app.createdAt).toBeGreaterThan(0);
|
|
357
1264
|
expect(app.updatedAt).toBeGreaterThan(0);
|
|
358
1265
|
});
|
|
359
1266
|
|
|
360
|
-
test("returns the existing app when called again with same (
|
|
1267
|
+
test("returns the existing app when called again with same (provider, clientId)", async () => {
|
|
361
1268
|
seedTestProvider("github");
|
|
362
1269
|
const first = await upsertApp("github", "client-abc");
|
|
363
1270
|
const second = await upsertApp("github", "client-abc");
|
|
@@ -476,7 +1383,7 @@ describe("app operations", () => {
|
|
|
476
1383
|
|
|
477
1384
|
expect(fetched).toBeDefined();
|
|
478
1385
|
expect(fetched!.id).toBe(app.id);
|
|
479
|
-
expect(fetched!.
|
|
1386
|
+
expect(fetched!.provider).toBe("github");
|
|
480
1387
|
expect(fetched!.clientId).toBe("client-1");
|
|
481
1388
|
});
|
|
482
1389
|
|
|
@@ -564,7 +1471,7 @@ describe("connection operations", () => {
|
|
|
564
1471
|
const app = await createTestApp("github", "client-1");
|
|
565
1472
|
const conn = createConnection({
|
|
566
1473
|
oauthAppId: app.id,
|
|
567
|
-
|
|
1474
|
+
provider: "github",
|
|
568
1475
|
grantedScopes: ["repo", "user"],
|
|
569
1476
|
hasRefreshToken: true,
|
|
570
1477
|
accountInfo: "user@example.com",
|
|
@@ -574,7 +1481,7 @@ describe("connection operations", () => {
|
|
|
574
1481
|
|
|
575
1482
|
expect(conn.id).toBeTruthy();
|
|
576
1483
|
expect(conn.oauthAppId).toBe(app.id);
|
|
577
|
-
expect(conn.
|
|
1484
|
+
expect(conn.provider).toBe("github");
|
|
578
1485
|
expect(conn.status).toBe("active");
|
|
579
1486
|
expect(JSON.parse(conn.grantedScopes)).toEqual(["repo", "user"]);
|
|
580
1487
|
expect(conn.hasRefreshToken).toBe(1);
|
|
@@ -590,7 +1497,7 @@ describe("connection operations", () => {
|
|
|
590
1497
|
const app = await createTestApp("github", "client-1");
|
|
591
1498
|
const conn = createConnection({
|
|
592
1499
|
oauthAppId: app.id,
|
|
593
|
-
|
|
1500
|
+
provider: "github",
|
|
594
1501
|
grantedScopes: ["repo"],
|
|
595
1502
|
hasRefreshToken: false,
|
|
596
1503
|
});
|
|
@@ -598,7 +1505,7 @@ describe("connection operations", () => {
|
|
|
598
1505
|
const fetched = getConnection(conn.id);
|
|
599
1506
|
expect(fetched).toBeDefined();
|
|
600
1507
|
expect(fetched!.id).toBe(conn.id);
|
|
601
|
-
expect(fetched!.
|
|
1508
|
+
expect(fetched!.provider).toBe("github");
|
|
602
1509
|
});
|
|
603
1510
|
|
|
604
1511
|
test("returns undefined for unknown id", () => {
|
|
@@ -612,7 +1519,7 @@ describe("connection operations", () => {
|
|
|
612
1519
|
|
|
613
1520
|
createConnection({
|
|
614
1521
|
oauthAppId: app.id,
|
|
615
|
-
|
|
1522
|
+
provider: "github",
|
|
616
1523
|
grantedScopes: ["repo"],
|
|
617
1524
|
hasRefreshToken: false,
|
|
618
1525
|
createdAt: 1000,
|
|
@@ -620,7 +1527,7 @@ describe("connection operations", () => {
|
|
|
620
1527
|
|
|
621
1528
|
const conn2 = createConnection({
|
|
622
1529
|
oauthAppId: app.id,
|
|
623
|
-
|
|
1530
|
+
provider: "github",
|
|
624
1531
|
grantedScopes: ["repo", "user"],
|
|
625
1532
|
hasRefreshToken: true,
|
|
626
1533
|
createdAt: 2000,
|
|
@@ -636,7 +1543,7 @@ describe("connection operations", () => {
|
|
|
636
1543
|
|
|
637
1544
|
const conn1 = createConnection({
|
|
638
1545
|
oauthAppId: app.id,
|
|
639
|
-
|
|
1546
|
+
provider: "github",
|
|
640
1547
|
accountInfo: "user1@example.com",
|
|
641
1548
|
grantedScopes: ["repo"],
|
|
642
1549
|
hasRefreshToken: false,
|
|
@@ -645,7 +1552,7 @@ describe("connection operations", () => {
|
|
|
645
1552
|
|
|
646
1553
|
createConnection({
|
|
647
1554
|
oauthAppId: app.id,
|
|
648
|
-
|
|
1555
|
+
provider: "github",
|
|
649
1556
|
accountInfo: "user2@example.com",
|
|
650
1557
|
grantedScopes: ["repo"],
|
|
651
1558
|
hasRefreshToken: false,
|
|
@@ -665,7 +1572,7 @@ describe("connection operations", () => {
|
|
|
665
1572
|
|
|
666
1573
|
const conn1 = createConnection({
|
|
667
1574
|
oauthAppId: app1.id,
|
|
668
|
-
|
|
1575
|
+
provider: "github",
|
|
669
1576
|
grantedScopes: ["repo"],
|
|
670
1577
|
hasRefreshToken: false,
|
|
671
1578
|
createdAt: 1000,
|
|
@@ -673,7 +1580,7 @@ describe("connection operations", () => {
|
|
|
673
1580
|
|
|
674
1581
|
createConnection({
|
|
675
1582
|
oauthAppId: app2.id,
|
|
676
|
-
|
|
1583
|
+
provider: "github",
|
|
677
1584
|
grantedScopes: ["repo"],
|
|
678
1585
|
hasRefreshToken: false,
|
|
679
1586
|
createdAt: 2000,
|
|
@@ -689,7 +1596,7 @@ describe("connection operations", () => {
|
|
|
689
1596
|
|
|
690
1597
|
createConnection({
|
|
691
1598
|
oauthAppId: app.id,
|
|
692
|
-
|
|
1599
|
+
provider: "github",
|
|
693
1600
|
grantedScopes: ["repo"],
|
|
694
1601
|
hasRefreshToken: false,
|
|
695
1602
|
});
|
|
@@ -705,7 +1612,7 @@ describe("connection operations", () => {
|
|
|
705
1612
|
|
|
706
1613
|
const conn = createConnection({
|
|
707
1614
|
oauthAppId: app.id,
|
|
708
|
-
|
|
1615
|
+
provider: "github",
|
|
709
1616
|
grantedScopes: ["repo"],
|
|
710
1617
|
hasRefreshToken: false,
|
|
711
1618
|
});
|
|
@@ -727,7 +1634,7 @@ describe("connection operations", () => {
|
|
|
727
1634
|
// Create two connections with explicit timestamps so ordering is deterministic
|
|
728
1635
|
createConnection({
|
|
729
1636
|
oauthAppId: app.id,
|
|
730
|
-
|
|
1637
|
+
provider: "github",
|
|
731
1638
|
grantedScopes: ["repo"],
|
|
732
1639
|
hasRefreshToken: false,
|
|
733
1640
|
createdAt: 1000,
|
|
@@ -735,7 +1642,7 @@ describe("connection operations", () => {
|
|
|
735
1642
|
|
|
736
1643
|
const conn2 = createConnection({
|
|
737
1644
|
oauthAppId: app.id,
|
|
738
|
-
|
|
1645
|
+
provider: "github",
|
|
739
1646
|
grantedScopes: ["repo", "user"],
|
|
740
1647
|
hasRefreshToken: true,
|
|
741
1648
|
createdAt: 2000,
|
|
@@ -751,14 +1658,14 @@ describe("connection operations", () => {
|
|
|
751
1658
|
|
|
752
1659
|
const conn1 = createConnection({
|
|
753
1660
|
oauthAppId: app.id,
|
|
754
|
-
|
|
1661
|
+
provider: "github",
|
|
755
1662
|
grantedScopes: ["repo"],
|
|
756
1663
|
hasRefreshToken: false,
|
|
757
1664
|
});
|
|
758
1665
|
|
|
759
1666
|
const conn2 = createConnection({
|
|
760
1667
|
oauthAppId: app.id,
|
|
761
|
-
|
|
1668
|
+
provider: "github",
|
|
762
1669
|
grantedScopes: ["repo", "user"],
|
|
763
1670
|
hasRefreshToken: true,
|
|
764
1671
|
});
|
|
@@ -776,7 +1683,7 @@ describe("connection operations", () => {
|
|
|
776
1683
|
|
|
777
1684
|
const conn = createConnection({
|
|
778
1685
|
oauthAppId: app.id,
|
|
779
|
-
|
|
1686
|
+
provider: "github",
|
|
780
1687
|
grantedScopes: ["repo"],
|
|
781
1688
|
hasRefreshToken: false,
|
|
782
1689
|
});
|
|
@@ -798,7 +1705,7 @@ describe("connection operations", () => {
|
|
|
798
1705
|
|
|
799
1706
|
const conn1 = createConnection({
|
|
800
1707
|
oauthAppId: app.id,
|
|
801
|
-
|
|
1708
|
+
provider: "github",
|
|
802
1709
|
accountInfo: "user1@example.com",
|
|
803
1710
|
grantedScopes: ["repo"],
|
|
804
1711
|
hasRefreshToken: false,
|
|
@@ -807,7 +1714,7 @@ describe("connection operations", () => {
|
|
|
807
1714
|
|
|
808
1715
|
createConnection({
|
|
809
1716
|
oauthAppId: app.id,
|
|
810
|
-
|
|
1717
|
+
provider: "github",
|
|
811
1718
|
accountInfo: "user2@example.com",
|
|
812
1719
|
grantedScopes: ["repo"],
|
|
813
1720
|
hasRefreshToken: false,
|
|
@@ -827,7 +1734,7 @@ describe("connection operations", () => {
|
|
|
827
1734
|
|
|
828
1735
|
const conn = createConnection({
|
|
829
1736
|
oauthAppId: app.id,
|
|
830
|
-
|
|
1737
|
+
provider: "github",
|
|
831
1738
|
accountInfo: "user@example.com",
|
|
832
1739
|
grantedScopes: ["repo"],
|
|
833
1740
|
hasRefreshToken: false,
|
|
@@ -843,7 +1750,7 @@ describe("connection operations", () => {
|
|
|
843
1750
|
|
|
844
1751
|
createConnection({
|
|
845
1752
|
oauthAppId: app.id,
|
|
846
|
-
|
|
1753
|
+
provider: "github",
|
|
847
1754
|
accountInfo: "user@example.com",
|
|
848
1755
|
grantedScopes: ["repo"],
|
|
849
1756
|
hasRefreshToken: false,
|
|
@@ -861,7 +1768,7 @@ describe("connection operations", () => {
|
|
|
861
1768
|
|
|
862
1769
|
const conn = createConnection({
|
|
863
1770
|
oauthAppId: app.id,
|
|
864
|
-
|
|
1771
|
+
provider: "github",
|
|
865
1772
|
accountInfo: "user@example.com",
|
|
866
1773
|
grantedScopes: ["repo"],
|
|
867
1774
|
hasRefreshToken: false,
|
|
@@ -882,7 +1789,7 @@ describe("connection operations", () => {
|
|
|
882
1789
|
|
|
883
1790
|
createConnection({
|
|
884
1791
|
oauthAppId: app.id,
|
|
885
|
-
|
|
1792
|
+
provider: "github",
|
|
886
1793
|
accountInfo: "user1@example.com",
|
|
887
1794
|
grantedScopes: ["repo"],
|
|
888
1795
|
hasRefreshToken: false,
|
|
@@ -890,7 +1797,7 @@ describe("connection operations", () => {
|
|
|
890
1797
|
|
|
891
1798
|
createConnection({
|
|
892
1799
|
oauthAppId: app.id,
|
|
893
|
-
|
|
1800
|
+
provider: "github",
|
|
894
1801
|
accountInfo: "user2@example.com",
|
|
895
1802
|
grantedScopes: ["repo"],
|
|
896
1803
|
hasRefreshToken: false,
|
|
@@ -905,7 +1812,7 @@ describe("connection operations", () => {
|
|
|
905
1812
|
|
|
906
1813
|
createConnection({
|
|
907
1814
|
oauthAppId: app.id,
|
|
908
|
-
|
|
1815
|
+
provider: "github",
|
|
909
1816
|
accountInfo: "user1@example.com",
|
|
910
1817
|
grantedScopes: ["repo"],
|
|
911
1818
|
hasRefreshToken: false,
|
|
@@ -913,7 +1820,7 @@ describe("connection operations", () => {
|
|
|
913
1820
|
|
|
914
1821
|
const conn2 = createConnection({
|
|
915
1822
|
oauthAppId: app.id,
|
|
916
|
-
|
|
1823
|
+
provider: "github",
|
|
917
1824
|
accountInfo: "user2@example.com",
|
|
918
1825
|
grantedScopes: ["repo"],
|
|
919
1826
|
hasRefreshToken: false,
|
|
@@ -936,7 +1843,7 @@ describe("connection operations", () => {
|
|
|
936
1843
|
const app = await createTestApp("github", "client-1");
|
|
937
1844
|
const conn = createConnection({
|
|
938
1845
|
oauthAppId: app.id,
|
|
939
|
-
|
|
1846
|
+
provider: "github",
|
|
940
1847
|
grantedScopes: ["repo"],
|
|
941
1848
|
hasRefreshToken: false,
|
|
942
1849
|
});
|
|
@@ -950,7 +1857,7 @@ describe("connection operations", () => {
|
|
|
950
1857
|
const app = await createTestApp("github", "client-1");
|
|
951
1858
|
createConnection({
|
|
952
1859
|
oauthAppId: app.id,
|
|
953
|
-
|
|
1860
|
+
provider: "github",
|
|
954
1861
|
grantedScopes: ["repo"],
|
|
955
1862
|
hasRefreshToken: false,
|
|
956
1863
|
});
|
|
@@ -967,7 +1874,7 @@ describe("connection operations", () => {
|
|
|
967
1874
|
const app = await createTestApp("github", "client-1");
|
|
968
1875
|
const conn = createConnection({
|
|
969
1876
|
oauthAppId: app.id,
|
|
970
|
-
|
|
1877
|
+
provider: "github",
|
|
971
1878
|
grantedScopes: ["repo"],
|
|
972
1879
|
hasRefreshToken: false,
|
|
973
1880
|
});
|
|
@@ -984,7 +1891,7 @@ describe("connection operations", () => {
|
|
|
984
1891
|
const app = await createTestApp("github", "client-1");
|
|
985
1892
|
const conn = createConnection({
|
|
986
1893
|
oauthAppId: app.id,
|
|
987
|
-
|
|
1894
|
+
provider: "github",
|
|
988
1895
|
grantedScopes: ["repo"],
|
|
989
1896
|
hasRefreshToken: false,
|
|
990
1897
|
});
|
|
@@ -1021,7 +1928,7 @@ describe("connection operations", () => {
|
|
|
1021
1928
|
|
|
1022
1929
|
const conn = createConnection({
|
|
1023
1930
|
oauthAppId: app1.id,
|
|
1024
|
-
|
|
1931
|
+
provider: "github",
|
|
1025
1932
|
grantedScopes: ["repo"],
|
|
1026
1933
|
hasRefreshToken: false,
|
|
1027
1934
|
});
|
|
@@ -1051,13 +1958,13 @@ describe("connection operations", () => {
|
|
|
1051
1958
|
|
|
1052
1959
|
createConnection({
|
|
1053
1960
|
oauthAppId: ghApp.id,
|
|
1054
|
-
|
|
1961
|
+
provider: "github",
|
|
1055
1962
|
grantedScopes: ["repo"],
|
|
1056
1963
|
hasRefreshToken: false,
|
|
1057
1964
|
});
|
|
1058
1965
|
createConnection({
|
|
1059
1966
|
oauthAppId: googApp.id,
|
|
1060
|
-
|
|
1967
|
+
provider: "google",
|
|
1061
1968
|
grantedScopes: ["email"],
|
|
1062
1969
|
hasRefreshToken: true,
|
|
1063
1970
|
});
|
|
@@ -1073,24 +1980,24 @@ describe("connection operations", () => {
|
|
|
1073
1980
|
|
|
1074
1981
|
createConnection({
|
|
1075
1982
|
oauthAppId: ghApp.id,
|
|
1076
|
-
|
|
1983
|
+
provider: "github",
|
|
1077
1984
|
grantedScopes: ["repo"],
|
|
1078
1985
|
hasRefreshToken: false,
|
|
1079
1986
|
});
|
|
1080
1987
|
createConnection({
|
|
1081
1988
|
oauthAppId: googApp.id,
|
|
1082
|
-
|
|
1989
|
+
provider: "google",
|
|
1083
1990
|
grantedScopes: ["email"],
|
|
1084
1991
|
hasRefreshToken: true,
|
|
1085
1992
|
});
|
|
1086
1993
|
|
|
1087
1994
|
const ghConns = listConnections("github");
|
|
1088
1995
|
expect(ghConns).toHaveLength(1);
|
|
1089
|
-
expect(ghConns[0].
|
|
1996
|
+
expect(ghConns[0].provider).toBe("github");
|
|
1090
1997
|
|
|
1091
1998
|
const googConns = listConnections("google");
|
|
1092
1999
|
expect(googConns).toHaveLength(1);
|
|
1093
|
-
expect(googConns[0].
|
|
2000
|
+
expect(googConns[0].provider).toBe("google");
|
|
1094
2001
|
});
|
|
1095
2002
|
|
|
1096
2003
|
test("returns empty array when no connections exist", () => {
|
|
@@ -1103,7 +2010,7 @@ describe("connection operations", () => {
|
|
|
1103
2010
|
const app = await createTestApp("github", "client-1");
|
|
1104
2011
|
const conn = createConnection({
|
|
1105
2012
|
oauthAppId: app.id,
|
|
1106
|
-
|
|
2013
|
+
provider: "github",
|
|
1107
2014
|
grantedScopes: ["repo"],
|
|
1108
2015
|
hasRefreshToken: false,
|
|
1109
2016
|
});
|
|
@@ -1124,17 +2031,40 @@ describe("connection operations", () => {
|
|
|
1124
2031
|
// ---------------------------------------------------------------------------
|
|
1125
2032
|
|
|
1126
2033
|
describe("disconnectOAuthProvider", () => {
|
|
2034
|
+
/**
|
|
2035
|
+
* Seed a provider with revokeUrl and (optionally) a revokeBodyTemplate.
|
|
2036
|
+
*/
|
|
2037
|
+
function seedProviderWithRevoke(
|
|
2038
|
+
provider: string,
|
|
2039
|
+
revokeUrl: string | null,
|
|
2040
|
+
revokeBodyTemplate?: Record<string, string>,
|
|
2041
|
+
): void {
|
|
2042
|
+
seedProviders([
|
|
2043
|
+
{
|
|
2044
|
+
provider,
|
|
2045
|
+
authorizeUrl: `https://${provider}.example.com/authorize`,
|
|
2046
|
+
tokenExchangeUrl: `https://${provider}.example.com/token`,
|
|
2047
|
+
defaultScopes: ["read"],
|
|
2048
|
+
scopePolicy: {},
|
|
2049
|
+
...(revokeUrl ? { revokeUrl } : {}),
|
|
2050
|
+
...(revokeBodyTemplate ? { revokeBodyTemplate } : {}),
|
|
2051
|
+
},
|
|
2052
|
+
]);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
1127
2055
|
test("returns 'not-found' when no connection exists for the provider", async () => {
|
|
1128
2056
|
const result = await disconnectOAuthProvider("github");
|
|
1129
2057
|
expect(result).toBe("not-found");
|
|
1130
2058
|
expect(mockDeleteSecureKeyAsync).not.toHaveBeenCalled();
|
|
2059
|
+
// No upstream call should be made when there is no connection at all.
|
|
2060
|
+
expect(getMockFetchCalls().length).toBe(0);
|
|
1131
2061
|
});
|
|
1132
2062
|
|
|
1133
2063
|
test("returns 'disconnected' and deletes connection row and secure keys when connection exists", async () => {
|
|
1134
2064
|
const app = await createTestApp("github", "client-1");
|
|
1135
2065
|
const conn = createConnection({
|
|
1136
2066
|
oauthAppId: app.id,
|
|
1137
|
-
|
|
2067
|
+
provider: "github",
|
|
1138
2068
|
grantedScopes: ["repo"],
|
|
1139
2069
|
hasRefreshToken: true,
|
|
1140
2070
|
});
|
|
@@ -1154,6 +2084,365 @@ describe("disconnectOAuthProvider", () => {
|
|
|
1154
2084
|
// Verify connection row was deleted
|
|
1155
2085
|
expect(getConnection(conn.id)).toBeUndefined();
|
|
1156
2086
|
});
|
|
2087
|
+
|
|
2088
|
+
test("calls upstream revoke when provider has revokeUrl and access token exists", async () => {
|
|
2089
|
+
seedProviderWithRevoke("google", "https://oauth2.googleapis.com/revoke", {
|
|
2090
|
+
token: "{access_token}",
|
|
2091
|
+
client_id: "{client_id}",
|
|
2092
|
+
});
|
|
2093
|
+
const app = await upsertApp("google", "client-1");
|
|
2094
|
+
const conn = createConnection({
|
|
2095
|
+
oauthAppId: app.id,
|
|
2096
|
+
provider: "google",
|
|
2097
|
+
grantedScopes: ["email"],
|
|
2098
|
+
hasRefreshToken: true,
|
|
2099
|
+
});
|
|
2100
|
+
secureKeyValues.set(
|
|
2101
|
+
`oauth_connection/${conn.id}/access_token`,
|
|
2102
|
+
"fake-token-xyz",
|
|
2103
|
+
);
|
|
2104
|
+
|
|
2105
|
+
mockFetch(
|
|
2106
|
+
"https://oauth2.googleapis.com/revoke",
|
|
2107
|
+
{ method: "POST" },
|
|
2108
|
+
{ status: 200, body: {} },
|
|
2109
|
+
);
|
|
2110
|
+
|
|
2111
|
+
const result = await disconnectOAuthProvider("google");
|
|
2112
|
+
expect(result).toBe("disconnected");
|
|
2113
|
+
|
|
2114
|
+
const calls = getMockFetchCalls();
|
|
2115
|
+
expect(calls.length).toBe(1);
|
|
2116
|
+
expect(calls[0]!.path).toContain("https://oauth2.googleapis.com/revoke");
|
|
2117
|
+
|
|
2118
|
+
const body = String(calls[0]!.init.body ?? "");
|
|
2119
|
+
const params = new URLSearchParams(body);
|
|
2120
|
+
expect(params.get("token")).toBe("fake-token-xyz");
|
|
2121
|
+
expect(params.get("client_id")).toBe("client-1");
|
|
2122
|
+
});
|
|
2123
|
+
|
|
2124
|
+
test("skips upstream revoke when provider has no revokeUrl", async () => {
|
|
2125
|
+
// GitHub seeded by createTestApp via seedTestProvider — no revokeUrl.
|
|
2126
|
+
const app = await createTestApp("github", "client-1");
|
|
2127
|
+
const conn = createConnection({
|
|
2128
|
+
oauthAppId: app.id,
|
|
2129
|
+
provider: "github",
|
|
2130
|
+
grantedScopes: ["repo"],
|
|
2131
|
+
hasRefreshToken: false,
|
|
2132
|
+
});
|
|
2133
|
+
secureKeyValues.set(
|
|
2134
|
+
`oauth_connection/${conn.id}/access_token`,
|
|
2135
|
+
"github-token",
|
|
2136
|
+
);
|
|
2137
|
+
|
|
2138
|
+
const result = await disconnectOAuthProvider("github");
|
|
2139
|
+
expect(result).toBe("disconnected");
|
|
2140
|
+
expect(getMockFetchCalls().length).toBe(0);
|
|
2141
|
+
});
|
|
2142
|
+
|
|
2143
|
+
test("skips upstream revoke when no access token exists in secure storage", async () => {
|
|
2144
|
+
seedProviderWithRevoke("google", "https://oauth2.googleapis.com/revoke", {
|
|
2145
|
+
token: "{access_token}",
|
|
2146
|
+
});
|
|
2147
|
+
const app = await upsertApp("google", "client-1");
|
|
2148
|
+
createConnection({
|
|
2149
|
+
oauthAppId: app.id,
|
|
2150
|
+
provider: "google",
|
|
2151
|
+
grantedScopes: ["email"],
|
|
2152
|
+
hasRefreshToken: false,
|
|
2153
|
+
});
|
|
2154
|
+
// No access token seeded into secureKeyValues.
|
|
2155
|
+
|
|
2156
|
+
const result = await disconnectOAuthProvider("google");
|
|
2157
|
+
expect(result).toBe("disconnected");
|
|
2158
|
+
expect(getMockFetchCalls().length).toBe(0);
|
|
2159
|
+
});
|
|
2160
|
+
|
|
2161
|
+
test("continues local cleanup when upstream revoke returns non-2xx", async () => {
|
|
2162
|
+
seedProviderWithRevoke("google", "https://oauth2.googleapis.com/revoke", {
|
|
2163
|
+
token: "{access_token}",
|
|
2164
|
+
});
|
|
2165
|
+
const app = await upsertApp("google", "client-1");
|
|
2166
|
+
const conn = createConnection({
|
|
2167
|
+
oauthAppId: app.id,
|
|
2168
|
+
provider: "google",
|
|
2169
|
+
grantedScopes: ["email"],
|
|
2170
|
+
hasRefreshToken: true,
|
|
2171
|
+
});
|
|
2172
|
+
secureKeyValues.set(
|
|
2173
|
+
`oauth_connection/${conn.id}/access_token`,
|
|
2174
|
+
"fake-token-xyz",
|
|
2175
|
+
);
|
|
2176
|
+
|
|
2177
|
+
mockFetch(
|
|
2178
|
+
"https://oauth2.googleapis.com/revoke",
|
|
2179
|
+
{ method: "POST" },
|
|
2180
|
+
{ status: 400, body: { error: "invalid_token" } },
|
|
2181
|
+
);
|
|
2182
|
+
|
|
2183
|
+
const result = await disconnectOAuthProvider("google");
|
|
2184
|
+
expect(result).toBe("disconnected");
|
|
2185
|
+
expect(getMockFetchCalls().length).toBe(1);
|
|
2186
|
+
// Local cleanup still happened
|
|
2187
|
+
expect(mockDeleteSecureKeyAsync).toHaveBeenCalledWith(
|
|
2188
|
+
`oauth_connection/${conn.id}/access_token`,
|
|
2189
|
+
);
|
|
2190
|
+
expect(getConnection(conn.id)).toBeUndefined();
|
|
2191
|
+
});
|
|
2192
|
+
|
|
2193
|
+
test("continues local cleanup when upstream revoke throws (network error)", async () => {
|
|
2194
|
+
seedProviderWithRevoke("google", "https://oauth2.googleapis.com/revoke", {
|
|
2195
|
+
token: "{access_token}",
|
|
2196
|
+
});
|
|
2197
|
+
const app = await upsertApp("google", "client-1");
|
|
2198
|
+
const conn = createConnection({
|
|
2199
|
+
oauthAppId: app.id,
|
|
2200
|
+
provider: "google",
|
|
2201
|
+
grantedScopes: ["email"],
|
|
2202
|
+
hasRefreshToken: true,
|
|
2203
|
+
});
|
|
2204
|
+
secureKeyValues.set(
|
|
2205
|
+
`oauth_connection/${conn.id}/access_token`,
|
|
2206
|
+
"fake-token-xyz",
|
|
2207
|
+
);
|
|
2208
|
+
|
|
2209
|
+
// 500 exercises the same swallow path as a network error.
|
|
2210
|
+
mockFetch(
|
|
2211
|
+
"https://oauth2.googleapis.com/revoke",
|
|
2212
|
+
{ method: "POST" },
|
|
2213
|
+
{ status: 500, body: { error: "server_error" } },
|
|
2214
|
+
);
|
|
2215
|
+
|
|
2216
|
+
const result = await disconnectOAuthProvider("google");
|
|
2217
|
+
expect(result).toBe("disconnected");
|
|
2218
|
+
expect(getConnection(conn.id)).toBeUndefined();
|
|
2219
|
+
});
|
|
2220
|
+
|
|
2221
|
+
test("substitutes {access_token} and {client_id} in body template values", async () => {
|
|
2222
|
+
seedProviderWithRevoke("google", "https://oauth2.googleapis.com/revoke", {
|
|
2223
|
+
token: "{access_token}",
|
|
2224
|
+
client_id: "{client_id}",
|
|
2225
|
+
token_type_hint: "access_token",
|
|
2226
|
+
});
|
|
2227
|
+
const app = await upsertApp("google", "client-substitution");
|
|
2228
|
+
const conn = createConnection({
|
|
2229
|
+
oauthAppId: app.id,
|
|
2230
|
+
provider: "google",
|
|
2231
|
+
grantedScopes: ["email"],
|
|
2232
|
+
hasRefreshToken: false,
|
|
2233
|
+
});
|
|
2234
|
+
secureKeyValues.set(
|
|
2235
|
+
`oauth_connection/${conn.id}/access_token`,
|
|
2236
|
+
"tok-substitution",
|
|
2237
|
+
);
|
|
2238
|
+
|
|
2239
|
+
mockFetch(
|
|
2240
|
+
"https://oauth2.googleapis.com/revoke",
|
|
2241
|
+
{ method: "POST" },
|
|
2242
|
+
{ status: 200, body: {} },
|
|
2243
|
+
);
|
|
2244
|
+
|
|
2245
|
+
await disconnectOAuthProvider("google");
|
|
2246
|
+
|
|
2247
|
+
const calls = getMockFetchCalls();
|
|
2248
|
+
expect(calls.length).toBe(1);
|
|
2249
|
+
const body = String(calls[0]!.init.body ?? "");
|
|
2250
|
+
const params = new URLSearchParams(body);
|
|
2251
|
+
expect(params.get("token")).toBe("tok-substitution");
|
|
2252
|
+
expect(params.get("client_id")).toBe("client-substitution");
|
|
2253
|
+
expect(params.get("token_type_hint")).toBe("access_token");
|
|
2254
|
+
});
|
|
2255
|
+
|
|
2256
|
+
test("treats $-prefixed patterns in access token as literal text (String.replace gotcha)", async () => {
|
|
2257
|
+
// String.prototype.replace interprets $-prefixed patterns in the
|
|
2258
|
+
// replacement string as special sequences ($& = matched substring,
|
|
2259
|
+
// $' = after match, $` = before match, $$ = literal $). If the access
|
|
2260
|
+
// token contains "$&", a naive `.replace("{access_token}", accessToken)`
|
|
2261
|
+
// would expand it to "{access_token}" (the matched string) instead of
|
|
2262
|
+
// substituting literally. This test guards against that by asserting
|
|
2263
|
+
// the captured body contains the literal "tok$&abc" — which only holds
|
|
2264
|
+
// when we use a function-replacement callback that preserves literal
|
|
2265
|
+
// semantics and mirrors Python's str.replace() behavior.
|
|
2266
|
+
seedProviderWithRevoke("google", "https://revoke.example.com/r", {
|
|
2267
|
+
token: "{access_token}",
|
|
2268
|
+
});
|
|
2269
|
+
const app = await upsertApp("google", "client-1");
|
|
2270
|
+
const conn = createConnection({
|
|
2271
|
+
oauthAppId: app.id,
|
|
2272
|
+
provider: "google",
|
|
2273
|
+
grantedScopes: ["email"],
|
|
2274
|
+
hasRefreshToken: false,
|
|
2275
|
+
});
|
|
2276
|
+
secureKeyValues.set(`oauth_connection/${conn.id}/access_token`, "tok$&abc");
|
|
2277
|
+
|
|
2278
|
+
mockFetch(
|
|
2279
|
+
"https://revoke.example.com/r",
|
|
2280
|
+
{ method: "POST" },
|
|
2281
|
+
{ status: 200, body: {} },
|
|
2282
|
+
);
|
|
2283
|
+
|
|
2284
|
+
await disconnectOAuthProvider("google");
|
|
2285
|
+
|
|
2286
|
+
const calls = getMockFetchCalls();
|
|
2287
|
+
expect(calls.length).toBe(1);
|
|
2288
|
+
const body = String(calls[0]!.init.body ?? "");
|
|
2289
|
+
const params = new URLSearchParams(body);
|
|
2290
|
+
// The literal access token — not the $&-expanded version, which would
|
|
2291
|
+
// be "tok{access_token}abc" (where $& matched "{access_token}").
|
|
2292
|
+
expect(params.get("token")).toBe("tok$&abc");
|
|
2293
|
+
});
|
|
2294
|
+
|
|
2295
|
+
test("replaces all occurrences of {access_token} in body template values (matching Python str.replace)", async () => {
|
|
2296
|
+
// Python's str.replace(old, new) replaces ALL occurrences by default,
|
|
2297
|
+
// whereas JavaScript's String.prototype.replace with a string pattern
|
|
2298
|
+
// only replaces the FIRST occurrence. The platform's try_revoke_token
|
|
2299
|
+
// is implemented in Python, so any template value containing repeated
|
|
2300
|
+
// placeholders must have ALL of them substituted to preserve parity.
|
|
2301
|
+
// This test guards against a regression to .replace(), which would
|
|
2302
|
+
// leave the second {access_token} as a literal placeholder.
|
|
2303
|
+
seedProviderWithRevoke("google", "https://revoke.example.com/r", {
|
|
2304
|
+
token: "token={access_token}&also={access_token}",
|
|
2305
|
+
});
|
|
2306
|
+
const app = await upsertApp("google", "client-1");
|
|
2307
|
+
const conn = createConnection({
|
|
2308
|
+
oauthAppId: app.id,
|
|
2309
|
+
provider: "google",
|
|
2310
|
+
grantedScopes: ["email"],
|
|
2311
|
+
hasRefreshToken: false,
|
|
2312
|
+
});
|
|
2313
|
+
secureKeyValues.set(`oauth_connection/${conn.id}/access_token`, "fake-abc");
|
|
2314
|
+
|
|
2315
|
+
mockFetch(
|
|
2316
|
+
"https://revoke.example.com/r",
|
|
2317
|
+
{ method: "POST" },
|
|
2318
|
+
{ status: 200, body: {} },
|
|
2319
|
+
);
|
|
2320
|
+
|
|
2321
|
+
await disconnectOAuthProvider("google");
|
|
2322
|
+
|
|
2323
|
+
const calls = getMockFetchCalls();
|
|
2324
|
+
expect(calls.length).toBe(1);
|
|
2325
|
+
const body = String(calls[0]!.init.body ?? "");
|
|
2326
|
+
const params = new URLSearchParams(body);
|
|
2327
|
+
// Both {access_token} placeholders must be substituted. With the buggy
|
|
2328
|
+
// .replace() (single-occurrence), this would be
|
|
2329
|
+
// "token=fake-abc&also={access_token}" instead.
|
|
2330
|
+
expect(params.get("token")).toBe("token=fake-abc&also=fake-abc");
|
|
2331
|
+
});
|
|
2332
|
+
|
|
2333
|
+
test("coerces non-string body template values to strings", async () => {
|
|
2334
|
+
// seedProviderWithRevoke restricts to Record<string, string>; bypass it
|
|
2335
|
+
// here by inserting a template with a numeric value via a direct seed.
|
|
2336
|
+
seedProviders([
|
|
2337
|
+
{
|
|
2338
|
+
provider: "google",
|
|
2339
|
+
authorizeUrl: "https://google.example.com/authorize",
|
|
2340
|
+
tokenExchangeUrl: "https://google.example.com/token",
|
|
2341
|
+
defaultScopes: ["email"],
|
|
2342
|
+
scopePolicy: {},
|
|
2343
|
+
revokeUrl: "https://oauth2.googleapis.com/revoke",
|
|
2344
|
+
revokeBodyTemplate: {
|
|
2345
|
+
token: "{access_token}",
|
|
2346
|
+
// expires_in is a number — must be coerced via String(value).
|
|
2347
|
+
expires_in: 3600,
|
|
2348
|
+
} as unknown as Record<string, string>,
|
|
2349
|
+
},
|
|
2350
|
+
]);
|
|
2351
|
+
const app = await upsertApp("google", "client-1");
|
|
2352
|
+
const conn = createConnection({
|
|
2353
|
+
oauthAppId: app.id,
|
|
2354
|
+
provider: "google",
|
|
2355
|
+
grantedScopes: ["email"],
|
|
2356
|
+
hasRefreshToken: false,
|
|
2357
|
+
});
|
|
2358
|
+
secureKeyValues.set(
|
|
2359
|
+
`oauth_connection/${conn.id}/access_token`,
|
|
2360
|
+
"fake-token-xyz",
|
|
2361
|
+
);
|
|
2362
|
+
|
|
2363
|
+
mockFetch(
|
|
2364
|
+
"https://oauth2.googleapis.com/revoke",
|
|
2365
|
+
{ method: "POST" },
|
|
2366
|
+
{ status: 200, body: {} },
|
|
2367
|
+
);
|
|
2368
|
+
|
|
2369
|
+
await disconnectOAuthProvider("google");
|
|
2370
|
+
|
|
2371
|
+
const calls = getMockFetchCalls();
|
|
2372
|
+
expect(calls.length).toBe(1);
|
|
2373
|
+
const body = String(calls[0]!.init.body ?? "");
|
|
2374
|
+
const params = new URLSearchParams(body);
|
|
2375
|
+
expect(params.get("token")).toBe("fake-token-xyz");
|
|
2376
|
+
expect(params.get("expires_in")).toBe("3600");
|
|
2377
|
+
});
|
|
2378
|
+
|
|
2379
|
+
test("revokes BEFORE deleting tokens from secure storage", async () => {
|
|
2380
|
+
seedProviderWithRevoke("google", "https://oauth2.googleapis.com/revoke", {
|
|
2381
|
+
token: "{access_token}",
|
|
2382
|
+
});
|
|
2383
|
+
const app = await upsertApp("google", "client-1");
|
|
2384
|
+
const conn = createConnection({
|
|
2385
|
+
oauthAppId: app.id,
|
|
2386
|
+
provider: "google",
|
|
2387
|
+
grantedScopes: ["email"],
|
|
2388
|
+
hasRefreshToken: true,
|
|
2389
|
+
});
|
|
2390
|
+
secureKeyValues.set(
|
|
2391
|
+
`oauth_connection/${conn.id}/access_token`,
|
|
2392
|
+
"fake-token-xyz",
|
|
2393
|
+
);
|
|
2394
|
+
|
|
2395
|
+
const order: string[] = [];
|
|
2396
|
+
|
|
2397
|
+
// Wrap the mockFetch entry's response handler by registering a fetch
|
|
2398
|
+
// mock that records the call order via the existing mockFetch helper.
|
|
2399
|
+
// The mock fetch records to getMockFetchCalls; we tag ordering by
|
|
2400
|
+
// pushing a marker as soon as the response is constructed below.
|
|
2401
|
+
mockFetch(
|
|
2402
|
+
"https://oauth2.googleapis.com/revoke",
|
|
2403
|
+
{ method: "POST" },
|
|
2404
|
+
new Response(JSON.stringify({}), {
|
|
2405
|
+
status: 200,
|
|
2406
|
+
headers: { "Content-Type": "application/json" },
|
|
2407
|
+
}),
|
|
2408
|
+
);
|
|
2409
|
+
|
|
2410
|
+
// Wrap delete to record its order. We replace the mock implementation
|
|
2411
|
+
// for the duration of this test only.
|
|
2412
|
+
mockDeleteSecureKeyAsync.mockImplementation(() => {
|
|
2413
|
+
order.push("delete");
|
|
2414
|
+
return Promise.resolve("deleted" as const);
|
|
2415
|
+
});
|
|
2416
|
+
|
|
2417
|
+
// Wrap fetch one more layer: tap into the actual fetch call to record
|
|
2418
|
+
// ordering. We do this by overriding globalThis.fetch with a wrapper
|
|
2419
|
+
// that calls through to the existing mock and records ordering first.
|
|
2420
|
+
const wrappedFetch = globalThis.fetch;
|
|
2421
|
+
globalThis.fetch = (async (
|
|
2422
|
+
input: RequestInfo | URL,
|
|
2423
|
+
init?: RequestInit,
|
|
2424
|
+
) => {
|
|
2425
|
+
order.push("fetch");
|
|
2426
|
+
return wrappedFetch(input, init);
|
|
2427
|
+
}) as typeof globalThis.fetch;
|
|
2428
|
+
|
|
2429
|
+
try {
|
|
2430
|
+
const result = await disconnectOAuthProvider("google");
|
|
2431
|
+
expect(result).toBe("disconnected");
|
|
2432
|
+
} finally {
|
|
2433
|
+
// Restore the wrapper layer; resetMockFetch in beforeEach will reset
|
|
2434
|
+
// the underlying mock for the next test.
|
|
2435
|
+
globalThis.fetch = wrappedFetch;
|
|
2436
|
+
mockDeleteSecureKeyAsync.mockImplementation(
|
|
2437
|
+
(): Promise<"deleted" | "not-found" | "error"> =>
|
|
2438
|
+
Promise.resolve("deleted" as const),
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
expect(order[0]).toBe("fetch");
|
|
2443
|
+
expect(order).toContain("delete");
|
|
2444
|
+
expect(order.indexOf("fetch")).toBeLessThan(order.indexOf("delete"));
|
|
2445
|
+
});
|
|
1157
2446
|
});
|
|
1158
2447
|
|
|
1159
2448
|
// ---------------------------------------------------------------------------
|
|
@@ -1172,7 +2461,7 @@ describe("FK constraints", () => {
|
|
|
1172
2461
|
expect(() =>
|
|
1173
2462
|
createConnection({
|
|
1174
2463
|
oauthAppId: "nonexistent-app-id",
|
|
1175
|
-
|
|
2464
|
+
provider: "github",
|
|
1176
2465
|
grantedScopes: ["repo"],
|
|
1177
2466
|
hasRefreshToken: false,
|
|
1178
2467
|
}),
|