@vellumai/assistant 0.6.1 → 0.6.3
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/bun.lock +40 -40
- package/bunfig.toml +3 -0
- package/docker-entrypoint.sh +12 -2
- package/docs/architecture/memory.md +1 -1
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
- package/openapi.yaml +184 -69
- package/package.json +41 -41
- package/scripts/generate-openapi.ts +1 -2
- package/src/__tests__/acp-session.test.ts +43 -0
- 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__/assistant-event-hub.test.ts +30 -0
- package/src/__tests__/browser-fill-credential.test.ts +229 -94
- package/src/__tests__/browser-manager.test.ts +40 -27
- package/src/__tests__/catalog-files.test.ts +862 -0
- package/src/__tests__/channel-approvals.test.ts +53 -0
- package/src/__tests__/checker.test.ts +104 -170
- package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
- 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 +125 -48
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
- package/src/__tests__/context-overflow-approval.test.ts +21 -6
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop.test.ts +1 -1
- package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
- package/src/__tests__/conversation-attachments.test.ts +80 -4
- package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
- package/src/__tests__/conversation-directories-parse.test.ts +105 -0
- package/src/__tests__/conversation-fork-crud.test.ts +17 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -0
- package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
- package/src/__tests__/conversation-inject-context.test.ts +103 -0
- package/src/__tests__/conversation-queue.test.ts +45 -2
- package/src/__tests__/conversation-routes-disk-view.test.ts +5 -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 +269 -46
- 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-workspace-cache-state.test.ts +193 -0
- package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -3
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/credential-vault-unit.test.ts +4 -4
- package/src/__tests__/credential-vault.test.ts +152 -13
- package/src/__tests__/credentials-cli.test.ts +2 -2
- package/src/__tests__/date-context.test.ts +4 -4
- package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +155 -0
- package/src/__tests__/fixtures/mock-chrome-extension.ts +375 -0
- package/src/__tests__/gateway-only-guard.test.ts +3 -0
- package/src/__tests__/gemini-provider.test.ts +2 -2
- package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
- package/src/__tests__/headless-browser-interactions.test.ts +707 -371
- package/src/__tests__/headless-browser-navigate.test.ts +389 -47
- package/src/__tests__/headless-browser-read-tools.test.ts +266 -103
- package/src/__tests__/headless-browser-snapshot.test.ts +240 -77
- package/src/__tests__/host-bash-proxy.test.ts +150 -1
- package/src/__tests__/host-browser-e2e-cloud.test.ts +462 -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 +320 -0
- package/src/__tests__/host-cu-proxy.test.ts +171 -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__/init-feature-flag-overrides.test.ts +167 -0
- package/src/__tests__/inline-command-runner.test.ts +7 -5
- package/src/__tests__/integration-status.test.ts +6 -7
- package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
- package/src/__tests__/log-export-workspace.test.ts +190 -0
- package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
- package/src/__tests__/mcp-client-auth.test.ts +40 -4
- package/src/__tests__/mcp-health-check.test.ts +10 -3
- package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
- package/src/__tests__/migration-export-http.test.ts +61 -2
- package/src/__tests__/migration-export-streaming.test.ts +66 -0
- package/src/__tests__/migration-import-commit-http.test.ts +101 -1
- package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
- package/src/__tests__/navigate-settings-tab.test.ts +14 -1
- package/src/__tests__/notification-broadcaster.test.ts +65 -0
- package/src/__tests__/oauth-apps-routes.test.ts +17 -12
- package/src/__tests__/oauth-cli.test.ts +707 -60
- package/src/__tests__/oauth-connect-orchestrator.test.ts +116 -24
- package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +146 -10
- package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
- package/src/__tests__/oauth-providers-routes.test.ts +50 -14
- package/src/__tests__/oauth-store.test.ts +1386 -182
- package/src/__tests__/oauth2-gateway-transport.test.ts +211 -20
- package/src/__tests__/onboarding-template-contract.test.ts +74 -55
- package/src/__tests__/openai-provider.test.ts +2 -2
- 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 +1 -1
- package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
- package/src/__tests__/permission-mode.test.ts +28 -56
- package/src/__tests__/pkb-autoinject.test.ts +96 -0
- package/src/__tests__/platform-callback-registration.test.ts +19 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +296 -0
- package/src/__tests__/proxy-approval-callback.test.ts +18 -0
- package/src/__tests__/require-fresh-approval.test.ts +40 -3
- package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
- package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
- package/src/__tests__/schedule-routes.test.ts +162 -0
- package/src/__tests__/secret-detection-handler.test.ts +84 -0
- package/src/__tests__/secret-ingress-http.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/set-permission-mode.test.ts +13 -250
- package/src/__tests__/skills-file-content-endpoint.test.ts +670 -0
- package/src/__tests__/skills-files-catalog-fallback.test.ts +450 -0
- package/src/__tests__/slack-channel-config.test.ts +12 -15
- 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 +72 -1
- package/src/__tests__/task-scheduler.test.ts +32 -6
- package/src/__tests__/telegram-config.test.ts +10 -13
- package/src/__tests__/terminal-sandbox.test.ts +1 -1
- package/src/__tests__/terminal-tools.test.ts +11 -5
- package/src/__tests__/test-preload.ts +14 -0
- package/src/__tests__/tool-approval-handler.test.ts +73 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
- package/src/__tests__/tool-executor.test.ts +0 -1
- 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 +62 -0
- package/src/__tests__/trust-store.test.ts +4 -4
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
- package/src/__tests__/v2-consent-policy.test.ts +103 -0
- package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
- package/src/__tests__/workspace-policy.test.ts +2 -7
- package/src/acp/client-handler.ts +30 -4
- package/src/agent/loop.ts +12 -35
- package/src/approvals/guardian-request-resolvers.ts +21 -15
- 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/channels/__tests__/types.test.ts +134 -0
- package/src/channels/types.ts +55 -0
- package/src/cli/__tests__/run-assistant-command.ts +34 -7
- package/src/cli/__tests__/unknown-command.test.ts +33 -0
- package/src/cli/commands/browser-relay.ts +339 -409
- package/src/cli/commands/credentials.ts +3 -3
- package/src/cli/commands/default-action.ts +68 -1
- package/src/cli/commands/email.ts +18 -13
- package/src/cli/commands/mcp.ts +16 -4
- package/src/cli/commands/oauth/__tests__/connect.test.ts +68 -41
- 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 +31 -33
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +329 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +116 -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 +16 -2
- package/src/cli/commands/oauth/disconnect.ts +1 -1
- package/src/cli/commands/oauth/providers.ts +200 -36
- package/src/cli/commands/oauth/shared.ts +5 -5
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +259 -0
- package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
- 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 +10 -3
- package/src/config/assistant-feature-flags.ts +59 -55
- package/src/config/bundled-skills/app-builder/SKILL.md +33 -173
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +105 -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/contacts/SKILL.md +3 -0
- package/src/config/bundled-skills/document/SKILL.md +4 -0
- package/src/config/bundled-skills/gmail/SKILL.md +12 -7
- package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
- package/src/config/bundled-skills/outlook/SKILL.md +7 -0
- package/src/config/bundled-skills/settings/TOOLS.json +1 -1
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
- 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/env-registry.ts +14 -0
- package/src/config/env.ts +21 -0
- package/src/config/feature-flag-registry.json +46 -7
- package/src/config/loader.ts +56 -1
- package/src/config/sanitize-for-transfer.ts +47 -0
- package/src/config/schema.ts +46 -5
- package/src/config/schemas/host-browser.ts +66 -0
- package/src/config/schemas/memory-lifecycle.ts +1 -1
- package/src/config/schemas/memory-retrieval.ts +103 -0
- package/src/config/schemas/security.ts +0 -6
- package/src/config/schemas/services.ts +16 -0
- package/src/config/types.ts +0 -1
- package/src/context/post-turn-tool-result-truncation.ts +176 -0
- package/src/context/window-manager.ts +19 -1
- package/src/credential-execution/approval-bridge.ts +49 -16
- package/src/credential-execution/managed-catalog.ts +3 -7
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
- package/src/daemon/app-source-watcher.ts +35 -0
- package/src/daemon/config-watcher.ts +6 -2
- package/src/daemon/context-overflow-approval.ts +5 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
- package/src/daemon/conversation-agent-loop.ts +74 -19
- package/src/daemon/conversation-attachments.ts +40 -1
- package/src/daemon/conversation-messaging.ts +3 -0
- package/src/daemon/conversation-process.ts +66 -3
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +159 -20
- package/src/daemon/conversation-surfaces.ts +78 -12
- package/src/daemon/conversation-tool-setup.ts +74 -11
- package/src/daemon/conversation-workspace.ts +12 -0
- package/src/daemon/conversation.ts +227 -11
- package/src/daemon/date-context.ts +10 -10
- package/src/daemon/first-greeting.ts +3 -2
- package/src/daemon/handlers/conversations.ts +9 -139
- package/src/daemon/handlers/shared.ts +65 -0
- package/src/daemon/handlers/skills.ts +232 -37
- package/src/daemon/host-bash-proxy.ts +48 -13
- package/src/daemon/host-browser-proxy.ts +191 -0
- package/src/daemon/host-cu-proxy.ts +36 -11
- package/src/daemon/host-file-proxy.ts +57 -9
- package/src/daemon/lifecycle.ts +86 -12
- package/src/daemon/message-protocol.ts +7 -0
- package/src/daemon/message-types/conversations.ts +59 -13
- package/src/daemon/message-types/host-browser.ts +100 -0
- package/src/daemon/message-types/messages.ts +5 -6
- package/src/daemon/message-types/notifications.ts +12 -0
- package/src/daemon/message-types/settings.ts +12 -0
- package/src/daemon/message-types/skills.ts +10 -0
- package/src/daemon/message-types/subagents.ts +2 -0
- package/src/daemon/server.ts +112 -35
- package/src/daemon/tool-side-effects.ts +6 -0
- package/src/daemon/transport-hints.ts +14 -0
- package/src/inbound/platform-callback-registration.ts +18 -17
- package/src/index.ts +1 -1
- package/src/mcp/client.ts +59 -24
- package/src/memory/app-store.ts +31 -1
- package/src/memory/conversation-crud.ts +38 -10
- package/src/memory/conversation-directories.ts +39 -0
- package/src/memory/conversation-group-migration.ts +65 -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 +12 -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 +177 -18
- package/src/memory/graph/capability-seed.ts +3 -5
- package/src/memory/graph/consolidation.ts +10 -23
- package/src/memory/graph/extraction-job.ts +15 -0
- package/src/memory/graph/retriever.ts +40 -22
- package/src/memory/graph/store.test.ts +7 -3
- package/src/memory/graph/store.ts +47 -12
- package/src/memory/group-crud.ts +25 -9
- package/src/memory/llm-usage-store.ts +45 -4
- 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/index.ts +6 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +1 -0
- package/src/memory/schema/oauth.ts +18 -13
- package/src/messaging/provider.ts +1 -1
- package/src/notifications/broadcaster.ts +6 -0
- package/src/notifications/conversation-pairing.ts +12 -4
- package/src/notifications/emit-signal.ts +14 -0
- package/src/notifications/signal.ts +11 -0
- package/src/oauth/AGENTS.md +76 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +24 -19
- package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
- package/src/oauth/byo-connection.test.ts +8 -8
- package/src/oauth/byo-connection.ts +7 -7
- package/src/oauth/connect-orchestrator.ts +23 -21
- package/src/oauth/connect-types.ts +3 -3
- package/src/oauth/connection-resolver.test.ts +17 -4
- package/src/oauth/connection-resolver.ts +16 -16
- package/src/oauth/connection.ts +1 -1
- package/src/oauth/manual-token-connection.ts +13 -13
- package/src/oauth/oauth-store.ts +214 -100
- package/src/oauth/platform-connection.test.ts +5 -5
- package/src/oauth/platform-connection.ts +4 -4
- package/src/oauth/provider-serializer.ts +31 -5
- package/src/oauth/revoke.ts +76 -0
- package/src/oauth/seed-providers.ts +127 -87
- package/src/oauth/token-persistence.ts +1 -1
- package/src/permissions/checker.ts +3 -3
- package/src/permissions/defaults.ts +7 -8
- package/src/permissions/permission-mode.ts +4 -11
- package/src/permissions/prompter.ts +13 -3
- package/src/permissions/v2-consent-policy.ts +87 -0
- package/src/platform/client.ts +1 -1
- package/src/prompts/system-prompt.ts +18 -21
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
- package/src/prompts/templates/BOOTSTRAP.md +59 -96
- package/src/prompts/templates/SOUL.md +11 -11
- package/src/providers/anthropic/client.ts +1 -0
- package/src/providers/types.ts +1 -1
- package/src/runtime/AGENTS.md +23 -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/assistant-event-hub.ts +24 -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 +8 -0
- package/src/runtime/auth/middleware.ts +98 -0
- package/src/runtime/auth/route-policy.ts +6 -7
- package/src/runtime/auth/token-service.ts +8 -0
- package/src/runtime/capability-tokens.ts +414 -0
- package/src/runtime/channel-approvals.ts +18 -5
- package/src/runtime/chrome-extension-registry.ts +332 -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 +425 -70
- 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 +162 -0
- package/src/runtime/migrations/migration-transport.ts +6 -0
- package/src/runtime/migrations/migration-wizard.ts +22 -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 +19 -0
- package/src/runtime/migrations/vbundle-importer.ts +55 -5
- package/src/runtime/pending-interactions.ts +29 -13
- package/src/runtime/routes/approval-routes.ts +90 -16
- package/src/runtime/routes/browser-cdp-routes.ts +229 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +497 -0
- package/src/runtime/routes/conversation-analysis-routes.ts +18 -5
- package/src/runtime/routes/conversation-management-routes.ts +108 -0
- package/src/runtime/routes/conversation-routes.ts +308 -28
- package/src/runtime/routes/conversation-starter-routes.ts +78 -16
- package/src/runtime/routes/group-routes.ts +22 -8
- package/src/runtime/routes/guardian-action-routes.ts +24 -13
- package/src/runtime/routes/host-browser-routes.ts +279 -0
- package/src/runtime/routes/host-file-routes.ts +9 -1
- package/src/runtime/routes/identity-routes.ts +259 -16
- package/src/runtime/routes/log-export/AGENTS.md +104 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
- package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
- package/src/runtime/routes/log-export-routes.ts +60 -25
- package/src/runtime/routes/memory-item-routes.ts +1 -7
- package/src/runtime/routes/migration-routes.ts +87 -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 +9 -97
- package/src/runtime/routes/skills-routes.ts +52 -2
- package/src/runtime/routes/subagents-routes.ts +14 -10
- package/src/runtime/routes/usage-routes.ts +8 -7
- 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/schedule/scheduler.ts +7 -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 +42 -25
- package/src/security/secure-keys.ts +118 -25
- package/src/security/token-manager.ts +23 -10
- package/src/skills/catalog-files.ts +492 -0
- package/src/skills/inline-command-runner.ts +12 -14
- package/src/subagent/manager.ts +131 -26
- 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/auth-detector.ts +43 -12
- package/src/tools/browser/browser-execution.ts +645 -340
- package/src/tools/browser/browser-manager.ts +36 -12
- 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 +870 -0
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +330 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +377 -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 +743 -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 +578 -0
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +635 -0
- package/src/tools/browser/cdp-client/errors.ts +34 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +125 -0
- package/src/tools/browser/cdp-client/factory.ts +204 -0
- package/src/tools/browser/cdp-client/index.ts +14 -0
- package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
- package/src/tools/browser/cdp-client/types.ts +52 -0
- 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/permission-checker.ts +77 -100
- package/src/tools/registry.ts +0 -2
- package/src/tools/secret-detection-handler.ts +34 -1
- package/src/tools/shared/filesystem/image-read.ts +61 -40
- package/src/tools/skills/sandbox-runner.ts +3 -6
- 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 +7 -0
- package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
- package/src/tools/terminal/sandbox.ts +4 -1
- package/src/tools/terminal/shell.ts +24 -21
- package/src/tools/tool-approval-handler.ts +48 -2
- package/src/tools/types.ts +2 -3
- package/src/util/platform.ts +14 -19
- package/src/watcher/provider-types.ts +1 -1
- package/src/workspace/migrations/029-seed-pkb.ts +1 -0
- package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/workspace/top-level-renderer.ts +19 -1
- package/src/__tests__/chrome-cdp.test.ts +0 -419
- 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/config/schemas/sandbox.ts +0 -14
- package/src/permissions/permission-mode-store.ts +0 -180
- 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,984 @@ 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("migration 216 backfills NULL token_endpoint_auth_method to client_secret_post", () => {
|
|
596
|
+
// Use raw SQLite to bypass Drizzle's NOT NULL enforcement and insert
|
|
597
|
+
// a legacy-shaped row with NULL token_endpoint_auth_method.
|
|
598
|
+
const db = getDb();
|
|
599
|
+
const raw = getSqliteFrom(db);
|
|
600
|
+
raw.exec(`
|
|
601
|
+
INSERT INTO oauth_providers (
|
|
602
|
+
provider_key, auth_url, token_url, token_endpoint_auth_method,
|
|
603
|
+
default_scopes, scope_policy, scope_separator, requires_client_secret,
|
|
604
|
+
created_at, updated_at
|
|
605
|
+
) VALUES (
|
|
606
|
+
'legacy-null-provider',
|
|
607
|
+
'https://example.com/authorize',
|
|
608
|
+
'https://example.com/token',
|
|
609
|
+
NULL,
|
|
610
|
+
'[]',
|
|
611
|
+
'{}',
|
|
612
|
+
' ',
|
|
613
|
+
1,
|
|
614
|
+
${Date.now()},
|
|
615
|
+
${Date.now()}
|
|
616
|
+
)
|
|
617
|
+
`);
|
|
618
|
+
|
|
619
|
+
// Run the migration directly
|
|
620
|
+
migrateOAuthProvidersTokenAuthMethodDefault(db);
|
|
621
|
+
|
|
622
|
+
// Verify the row was backfilled
|
|
623
|
+
const row = raw
|
|
624
|
+
.prepare(
|
|
625
|
+
`SELECT token_endpoint_auth_method FROM oauth_providers WHERE provider_key = 'legacy-null-provider'`,
|
|
626
|
+
)
|
|
627
|
+
.get() as { token_endpoint_auth_method: string };
|
|
628
|
+
expect(row.token_endpoint_auth_method).toBe("client_secret_post");
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
test("migration 216 is idempotent — running twice on backfilled rows is a no-op", () => {
|
|
632
|
+
seedProviders([
|
|
633
|
+
{
|
|
634
|
+
provider: "already-set-provider",
|
|
635
|
+
authorizeUrl: "https://example.com/authorize",
|
|
636
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
637
|
+
tokenEndpointAuthMethod: "client_secret_basic",
|
|
638
|
+
defaultScopes: [],
|
|
639
|
+
scopePolicy: {},
|
|
640
|
+
},
|
|
641
|
+
]);
|
|
642
|
+
|
|
643
|
+
const db = getDb();
|
|
644
|
+
migrateOAuthProvidersTokenAuthMethodDefault(db);
|
|
645
|
+
migrateOAuthProvidersTokenAuthMethodDefault(db);
|
|
646
|
+
|
|
647
|
+
const row = getProvider("already-set-provider");
|
|
648
|
+
expect(row!.tokenEndpointAuthMethod).toBe("client_secret_basic");
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
describe("getProvider", () => {
|
|
653
|
+
test("returns the correct row", () => {
|
|
654
|
+
seedProviders([
|
|
655
|
+
{
|
|
656
|
+
provider: "github",
|
|
657
|
+
authorizeUrl: "https://github.com/authorize",
|
|
658
|
+
tokenExchangeUrl: "https://github.com/token",
|
|
659
|
+
defaultScopes: ["repo"],
|
|
660
|
+
scopePolicy: {},
|
|
661
|
+
},
|
|
662
|
+
]);
|
|
663
|
+
|
|
664
|
+
const row = getProvider("github");
|
|
665
|
+
expect(row).toBeDefined();
|
|
666
|
+
expect(row!.provider).toBe("github");
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
test("returns undefined for unknown keys", () => {
|
|
670
|
+
expect(getProvider("nonexistent")).toBeUndefined();
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
describe("registerProvider", () => {
|
|
675
|
+
test("creates a new row", () => {
|
|
676
|
+
const row = registerProvider({
|
|
677
|
+
provider: "linear",
|
|
678
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
679
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
680
|
+
defaultScopes: ["read"],
|
|
681
|
+
scopePolicy: {},
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
expect(row.provider).toBe("linear");
|
|
685
|
+
expect(row.authorizeUrl).toBe("https://linear.app/oauth/authorize");
|
|
686
|
+
|
|
687
|
+
const fetched = getProvider("linear");
|
|
688
|
+
expect(fetched).toBeDefined();
|
|
689
|
+
expect(fetched!.provider).toBe("linear");
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
test("throws for duplicate provider_key", () => {
|
|
693
|
+
registerProvider({
|
|
694
|
+
provider: "linear",
|
|
695
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
696
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
697
|
+
defaultScopes: ["read"],
|
|
698
|
+
scopePolicy: {},
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
expect(() =>
|
|
702
|
+
registerProvider({
|
|
703
|
+
provider: "linear",
|
|
704
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
705
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
706
|
+
defaultScopes: ["read"],
|
|
707
|
+
scopePolicy: {},
|
|
708
|
+
}),
|
|
709
|
+
).toThrow(/already exists.*linear/);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
test("persists scopeSeparator and round-trips via getProvider", () => {
|
|
713
|
+
registerProvider({
|
|
714
|
+
provider: "linear",
|
|
715
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
716
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
717
|
+
defaultScopes: ["read"],
|
|
718
|
+
scopePolicy: {},
|
|
719
|
+
scopeSeparator: ";",
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
const fetched = getProvider("linear");
|
|
723
|
+
expect(fetched).toBeDefined();
|
|
724
|
+
expect(fetched!.scopeSeparator).toBe(";");
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
test("scopeSeparator defaults to ' ' when omitted", () => {
|
|
728
|
+
registerProvider({
|
|
729
|
+
provider: "linear",
|
|
730
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
731
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
732
|
+
defaultScopes: ["read"],
|
|
733
|
+
scopePolicy: {},
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
const fetched = getProvider("linear");
|
|
737
|
+
expect(fetched).toBeDefined();
|
|
738
|
+
expect(fetched!.scopeSeparator).toBe(" ");
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
test("persists refreshUrl and round-trips via getProvider", () => {
|
|
742
|
+
registerProvider({
|
|
743
|
+
provider: "linear",
|
|
744
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
745
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
746
|
+
refreshUrl: "https://api.linear.app/oauth/refresh",
|
|
747
|
+
defaultScopes: ["read"],
|
|
748
|
+
scopePolicy: {},
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
const fetched = getProvider("linear");
|
|
752
|
+
expect(fetched).toBeDefined();
|
|
753
|
+
expect(fetched!.refreshUrl).toBe("https://api.linear.app/oauth/refresh");
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
test("refreshUrl defaults to null when omitted", () => {
|
|
757
|
+
registerProvider({
|
|
758
|
+
provider: "linear",
|
|
759
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
760
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
761
|
+
defaultScopes: ["read"],
|
|
762
|
+
scopePolicy: {},
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
const fetched = getProvider("linear");
|
|
766
|
+
expect(fetched).toBeDefined();
|
|
767
|
+
expect(fetched!.refreshUrl).toBeNull();
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
test("persists revokeUrl and revokeBodyTemplate and round-trips via getProvider", () => {
|
|
771
|
+
registerProvider({
|
|
772
|
+
provider: "linear",
|
|
773
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
774
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
775
|
+
revokeUrl: "https://api.linear.app/oauth/revoke",
|
|
776
|
+
revokeBodyTemplate: { token: "{access_token}" },
|
|
777
|
+
defaultScopes: ["read"],
|
|
778
|
+
scopePolicy: {},
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
const fetched = getProvider("linear");
|
|
782
|
+
expect(fetched).toBeDefined();
|
|
783
|
+
expect(fetched!.revokeUrl).toBe("https://api.linear.app/oauth/revoke");
|
|
784
|
+
expect(JSON.parse(fetched!.revokeBodyTemplate!)).toEqual({
|
|
785
|
+
token: "{access_token}",
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
test("applies client_secret_post default when tokenEndpointAuthMethod is omitted", () => {
|
|
790
|
+
const row = registerProvider({
|
|
791
|
+
provider: "custom-default-test",
|
|
792
|
+
authorizeUrl: "https://example.com/authorize",
|
|
793
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
794
|
+
defaultScopes: [],
|
|
795
|
+
scopePolicy: {},
|
|
796
|
+
// Note: tokenEndpointAuthMethod intentionally omitted
|
|
797
|
+
});
|
|
798
|
+
expect(row.tokenEndpointAuthMethod).toBe("client_secret_post");
|
|
799
|
+
|
|
800
|
+
const fetched = getProvider("custom-default-test");
|
|
801
|
+
expect(fetched!.tokenEndpointAuthMethod).toBe("client_secret_post");
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
test("preserves explicit client_secret_basic when registering a provider", () => {
|
|
805
|
+
const row = registerProvider({
|
|
806
|
+
provider: "custom-basic-test",
|
|
807
|
+
authorizeUrl: "https://example.com/authorize",
|
|
808
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
809
|
+
defaultScopes: [],
|
|
810
|
+
scopePolicy: {},
|
|
811
|
+
tokenEndpointAuthMethod: "client_secret_basic",
|
|
812
|
+
});
|
|
813
|
+
expect(row.tokenEndpointAuthMethod).toBe("client_secret_basic");
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
test("stores logoUrl when provided", () => {
|
|
817
|
+
registerProvider({
|
|
818
|
+
provider: "notion",
|
|
819
|
+
authorizeUrl: "https://api.notion.com/v1/oauth/authorize",
|
|
820
|
+
tokenExchangeUrl: "https://api.notion.com/v1/oauth/token",
|
|
821
|
+
defaultScopes: ["read"],
|
|
822
|
+
scopePolicy: {},
|
|
823
|
+
logoUrl: "https://cdn.simpleicons.org/notion",
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
const fetched = getProvider("notion");
|
|
827
|
+
expect(fetched).toBeDefined();
|
|
828
|
+
expect(fetched!.logoUrl).toBe("https://cdn.simpleicons.org/notion");
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
test("defaults logoUrl to null when omitted", () => {
|
|
832
|
+
registerProvider({
|
|
833
|
+
provider: "linear",
|
|
834
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
835
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
836
|
+
defaultScopes: ["read"],
|
|
837
|
+
scopePolicy: {},
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
const fetched = getProvider("linear");
|
|
841
|
+
expect(fetched).toBeDefined();
|
|
842
|
+
expect(fetched!.logoUrl).toBeNull();
|
|
843
|
+
});
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
describe("updateProvider", () => {
|
|
847
|
+
test("updates scopeSeparator on an existing row", () => {
|
|
848
|
+
seedProviders([
|
|
849
|
+
{
|
|
850
|
+
provider: "github",
|
|
851
|
+
authorizeUrl: "https://github.com/authorize",
|
|
852
|
+
tokenExchangeUrl: "https://github.com/token",
|
|
853
|
+
defaultScopes: ["repo"],
|
|
854
|
+
scopePolicy: {},
|
|
855
|
+
},
|
|
856
|
+
]);
|
|
857
|
+
|
|
858
|
+
const before = getProvider("github");
|
|
859
|
+
expect(before!.scopeSeparator).toBe(" ");
|
|
860
|
+
|
|
861
|
+
const updated = updateProvider("github", { scopeSeparator: "," });
|
|
862
|
+
expect(updated).toBeDefined();
|
|
863
|
+
expect(updated!.scopeSeparator).toBe(",");
|
|
864
|
+
|
|
865
|
+
const fetched = getProvider("github");
|
|
866
|
+
expect(fetched!.scopeSeparator).toBe(",");
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
test("coerces empty-string scopeSeparator to default ' '", () => {
|
|
870
|
+
// An empty separator would join scopes into a single concatenated token
|
|
871
|
+
// (e.g. ["read","write"].join("") === "readwrite") which is never a
|
|
872
|
+
// valid OAuth authorize URL value. Coerce to the default.
|
|
873
|
+
seedProviders([
|
|
874
|
+
{
|
|
875
|
+
provider: "github",
|
|
876
|
+
authorizeUrl: "https://github.com/authorize",
|
|
877
|
+
tokenExchangeUrl: "https://github.com/token",
|
|
878
|
+
defaultScopes: ["repo"],
|
|
879
|
+
scopePolicy: {},
|
|
880
|
+
scopeSeparator: ",",
|
|
881
|
+
},
|
|
882
|
+
]);
|
|
883
|
+
|
|
884
|
+
expect(getProvider("github")!.scopeSeparator).toBe(",");
|
|
885
|
+
|
|
886
|
+
const updated = updateProvider("github", { scopeSeparator: "" });
|
|
887
|
+
expect(updated).toBeDefined();
|
|
888
|
+
expect(updated!.scopeSeparator).toBe(" ");
|
|
889
|
+
expect(getProvider("github")!.scopeSeparator).toBe(" ");
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
test("sets refreshUrl on an existing row where it was previously null", () => {
|
|
893
|
+
seedProviders([
|
|
894
|
+
{
|
|
895
|
+
provider: "github",
|
|
896
|
+
authorizeUrl: "https://github.com/authorize",
|
|
897
|
+
tokenExchangeUrl: "https://github.com/token",
|
|
898
|
+
defaultScopes: ["repo"],
|
|
899
|
+
scopePolicy: {},
|
|
900
|
+
},
|
|
901
|
+
]);
|
|
902
|
+
|
|
903
|
+
const before = getProvider("github");
|
|
904
|
+
expect(before!.refreshUrl).toBeNull();
|
|
905
|
+
|
|
906
|
+
const updated = updateProvider("github", {
|
|
907
|
+
refreshUrl: "https://github.com/login/oauth/refresh",
|
|
908
|
+
});
|
|
909
|
+
expect(updated).toBeDefined();
|
|
910
|
+
expect(updated!.refreshUrl).toBe(
|
|
911
|
+
"https://github.com/login/oauth/refresh",
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
const fetched = getProvider("github");
|
|
915
|
+
expect(fetched!.refreshUrl).toBe(
|
|
916
|
+
"https://github.com/login/oauth/refresh",
|
|
917
|
+
);
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
test("leaves refreshUrl unchanged when not passed to updateProvider", () => {
|
|
921
|
+
seedProviders([
|
|
922
|
+
{
|
|
923
|
+
provider: "github",
|
|
924
|
+
authorizeUrl: "https://github.com/authorize",
|
|
925
|
+
tokenExchangeUrl: "https://github.com/token",
|
|
926
|
+
refreshUrl: "https://github.com/login/oauth/refresh",
|
|
927
|
+
defaultScopes: ["repo"],
|
|
928
|
+
scopePolicy: {},
|
|
929
|
+
},
|
|
930
|
+
]);
|
|
931
|
+
|
|
932
|
+
expect(getProvider("github")!.refreshUrl).toBe(
|
|
933
|
+
"https://github.com/login/oauth/refresh",
|
|
934
|
+
);
|
|
935
|
+
|
|
936
|
+
// Update a different field — refreshUrl should be left alone.
|
|
937
|
+
const updated = updateProvider("github", {
|
|
938
|
+
displayLabel: "GitHub (updated)",
|
|
939
|
+
});
|
|
940
|
+
expect(updated).toBeDefined();
|
|
941
|
+
expect(updated!.refreshUrl).toBe(
|
|
942
|
+
"https://github.com/login/oauth/refresh",
|
|
943
|
+
);
|
|
944
|
+
expect(updated!.displayLabel).toBe("GitHub (updated)");
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
test("sets revokeUrl on an existing row where it was previously null", () => {
|
|
170
948
|
seedProviders([
|
|
171
949
|
{
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
950
|
+
provider: "github",
|
|
951
|
+
authorizeUrl: "https://github.com/authorize",
|
|
952
|
+
tokenExchangeUrl: "https://github.com/token",
|
|
175
953
|
defaultScopes: ["repo"],
|
|
176
954
|
scopePolicy: {},
|
|
177
|
-
pingUrl: "https://api.github.com/user",
|
|
178
955
|
},
|
|
179
956
|
]);
|
|
180
|
-
|
|
181
|
-
|
|
957
|
+
|
|
958
|
+
const before = getProvider("github");
|
|
959
|
+
expect(before!.revokeUrl).toBeNull();
|
|
960
|
+
|
|
961
|
+
const updated = updateProvider("github", {
|
|
962
|
+
revokeUrl: "https://github.com/login/oauth/revoke",
|
|
963
|
+
});
|
|
964
|
+
expect(updated).toBeDefined();
|
|
965
|
+
expect(updated!.revokeUrl).toBe("https://github.com/login/oauth/revoke");
|
|
966
|
+
|
|
967
|
+
const fetched = getProvider("github");
|
|
968
|
+
expect(fetched!.revokeUrl).toBe("https://github.com/login/oauth/revoke");
|
|
182
969
|
});
|
|
183
970
|
|
|
184
|
-
test("
|
|
971
|
+
test("sets revokeBodyTemplate on an existing row and JSON round-trips", () => {
|
|
185
972
|
seedProviders([
|
|
186
973
|
{
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
974
|
+
provider: "github",
|
|
975
|
+
authorizeUrl: "https://github.com/authorize",
|
|
976
|
+
tokenExchangeUrl: "https://github.com/token",
|
|
190
977
|
defaultScopes: ["repo"],
|
|
191
978
|
scopePolicy: {},
|
|
192
979
|
},
|
|
193
980
|
]);
|
|
194
|
-
|
|
195
|
-
|
|
981
|
+
|
|
982
|
+
const before = getProvider("github");
|
|
983
|
+
expect(before!.revokeBodyTemplate).toBeNull();
|
|
984
|
+
|
|
985
|
+
const updated = updateProvider("github", {
|
|
986
|
+
revokeBodyTemplate: {
|
|
987
|
+
token: "{access_token}",
|
|
988
|
+
client_id: "{client_id}",
|
|
989
|
+
},
|
|
990
|
+
});
|
|
991
|
+
expect(updated).toBeDefined();
|
|
992
|
+
expect(JSON.parse(updated!.revokeBodyTemplate!)).toEqual({
|
|
993
|
+
token: "{access_token}",
|
|
994
|
+
client_id: "{client_id}",
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
const fetched = getProvider("github");
|
|
998
|
+
expect(JSON.parse(fetched!.revokeBodyTemplate!)).toEqual({
|
|
999
|
+
token: "{access_token}",
|
|
1000
|
+
client_id: "{client_id}",
|
|
1001
|
+
});
|
|
196
1002
|
});
|
|
197
1003
|
|
|
198
|
-
test("
|
|
199
|
-
// Initial seed with all fields
|
|
1004
|
+
test("leaves revokeUrl and revokeBodyTemplate unchanged when not passed to updateProvider", () => {
|
|
200
1005
|
seedProviders([
|
|
201
1006
|
{
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
1007
|
+
provider: "github",
|
|
1008
|
+
authorizeUrl: "https://github.com/authorize",
|
|
1009
|
+
tokenExchangeUrl: "https://github.com/token",
|
|
1010
|
+
revokeUrl: "https://github.com/login/oauth/revoke",
|
|
1011
|
+
revokeBodyTemplate: { token: "{access_token}" },
|
|
206
1012
|
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",
|
|
1013
|
+
scopePolicy: {},
|
|
213
1014
|
},
|
|
214
1015
|
]);
|
|
215
1016
|
|
|
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();
|
|
1017
|
+
expect(getProvider("github")!.revokeUrl).toBe(
|
|
1018
|
+
"https://github.com/login/oauth/revoke",
|
|
1019
|
+
);
|
|
1020
|
+
expect(JSON.parse(getProvider("github")!.revokeBodyTemplate!)).toEqual({
|
|
1021
|
+
token: "{access_token}",
|
|
1022
|
+
});
|
|
229
1023
|
|
|
230
|
-
//
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
1024
|
+
// Update a different field — revoke fields should be left alone.
|
|
1025
|
+
const updated = updateProvider("github", {
|
|
1026
|
+
displayLabel: "GitHub (updated)",
|
|
1027
|
+
});
|
|
1028
|
+
expect(updated).toBeDefined();
|
|
1029
|
+
expect(updated!.revokeUrl).toBe("https://github.com/login/oauth/revoke");
|
|
1030
|
+
expect(JSON.parse(updated!.revokeBodyTemplate!)).toEqual({
|
|
1031
|
+
token: "{access_token}",
|
|
1032
|
+
});
|
|
1033
|
+
expect(updated!.displayLabel).toBe("GitHub (updated)");
|
|
1034
|
+
});
|
|
238
1035
|
|
|
239
|
-
|
|
1036
|
+
test("coerces empty string tokenEndpointAuthMethod to client_secret_post", () => {
|
|
240
1037
|
seedProviders([
|
|
241
1038
|
{
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
1039
|
+
provider: "update-empty-test",
|
|
1040
|
+
authorizeUrl: "https://example.com/authorize",
|
|
1041
|
+
tokenExchangeUrl: "https://example.com/token",
|
|
245
1042
|
tokenEndpointAuthMethod: "client_secret_basic",
|
|
246
|
-
defaultScopes: [
|
|
1043
|
+
defaultScopes: [],
|
|
247
1044
|
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
1045
|
},
|
|
254
1046
|
]);
|
|
255
1047
|
|
|
256
|
-
|
|
257
|
-
|
|
1048
|
+
expect(getProvider("update-empty-test")!.tokenEndpointAuthMethod).toBe(
|
|
1049
|
+
"client_secret_basic",
|
|
1050
|
+
);
|
|
258
1051
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
expect(JSON.parse(row!.scopePolicy)).toEqual({
|
|
262
|
-
required: ["repo"],
|
|
263
|
-
allowAdditionalScopes: true,
|
|
1052
|
+
const updated = updateProvider("update-empty-test", {
|
|
1053
|
+
tokenEndpointAuthMethod: "",
|
|
264
1054
|
});
|
|
265
|
-
expect(
|
|
1055
|
+
expect(updated).toBeDefined();
|
|
1056
|
+
expect(updated!.tokenEndpointAuthMethod).toBe("client_secret_post");
|
|
266
1057
|
|
|
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");
|
|
1058
|
+
const row = getProvider("update-empty-test");
|
|
1059
|
+
expect(row!.tokenEndpointAuthMethod).toBe("client_secret_post");
|
|
274
1060
|
});
|
|
275
|
-
});
|
|
276
1061
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
scopePolicy: {},
|
|
286
|
-
},
|
|
287
|
-
]);
|
|
1062
|
+
test("sets logoUrl on an existing row where it was previously null", () => {
|
|
1063
|
+
registerProvider({
|
|
1064
|
+
provider: "linear",
|
|
1065
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
1066
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
1067
|
+
defaultScopes: ["read"],
|
|
1068
|
+
scopePolicy: {},
|
|
1069
|
+
});
|
|
288
1070
|
|
|
289
|
-
|
|
290
|
-
expect(row).toBeDefined();
|
|
291
|
-
expect(row!.providerKey).toBe("github");
|
|
292
|
-
});
|
|
1071
|
+
expect(getProvider("linear")!.logoUrl).toBeNull();
|
|
293
1072
|
|
|
294
|
-
|
|
295
|
-
|
|
1073
|
+
const updated = updateProvider("linear", {
|
|
1074
|
+
logoUrl: "https://cdn.simpleicons.org/linear",
|
|
1075
|
+
});
|
|
1076
|
+
expect(updated).toBeDefined();
|
|
1077
|
+
expect(updated!.logoUrl).toBe("https://cdn.simpleicons.org/linear");
|
|
1078
|
+
|
|
1079
|
+
const fetched = getProvider("linear");
|
|
1080
|
+
expect(fetched!.logoUrl).toBe("https://cdn.simpleicons.org/linear");
|
|
296
1081
|
});
|
|
297
|
-
});
|
|
298
1082
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
tokenUrl: "https://api.linear.app/oauth/token",
|
|
1083
|
+
test("clears logoUrl when passed null", () => {
|
|
1084
|
+
registerProvider({
|
|
1085
|
+
provider: "notion",
|
|
1086
|
+
authorizeUrl: "https://api.notion.com/v1/oauth/authorize",
|
|
1087
|
+
tokenExchangeUrl: "https://api.notion.com/v1/oauth/token",
|
|
305
1088
|
defaultScopes: ["read"],
|
|
306
1089
|
scopePolicy: {},
|
|
1090
|
+
logoUrl: "https://cdn.simpleicons.org/notion",
|
|
307
1091
|
});
|
|
308
1092
|
|
|
309
|
-
expect(
|
|
310
|
-
|
|
1093
|
+
expect(getProvider("notion")!.logoUrl).toBe(
|
|
1094
|
+
"https://cdn.simpleicons.org/notion",
|
|
1095
|
+
);
|
|
311
1096
|
|
|
312
|
-
const
|
|
313
|
-
expect(
|
|
314
|
-
expect(
|
|
1097
|
+
const updated = updateProvider("notion", { logoUrl: null });
|
|
1098
|
+
expect(updated).toBeDefined();
|
|
1099
|
+
expect(updated!.logoUrl).toBeNull();
|
|
1100
|
+
|
|
1101
|
+
expect(getProvider("notion")!.logoUrl).toBeNull();
|
|
315
1102
|
});
|
|
316
1103
|
|
|
317
|
-
test("
|
|
1104
|
+
test("leaves logoUrl unchanged when not passed to updateProvider", () => {
|
|
318
1105
|
registerProvider({
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
1106
|
+
provider: "notion",
|
|
1107
|
+
authorizeUrl: "https://api.notion.com/v1/oauth/authorize",
|
|
1108
|
+
tokenExchangeUrl: "https://api.notion.com/v1/oauth/token",
|
|
322
1109
|
defaultScopes: ["read"],
|
|
323
1110
|
scopePolicy: {},
|
|
1111
|
+
logoUrl: "https://cdn.simpleicons.org/notion",
|
|
324
1112
|
});
|
|
325
1113
|
|
|
326
|
-
expect(()
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
1114
|
+
expect(getProvider("notion")!.logoUrl).toBe(
|
|
1115
|
+
"https://cdn.simpleicons.org/notion",
|
|
1116
|
+
);
|
|
1117
|
+
|
|
1118
|
+
// Update a different field — logoUrl should be left alone.
|
|
1119
|
+
const updated = updateProvider("notion", {
|
|
1120
|
+
displayLabel: "Notion (updated)",
|
|
1121
|
+
});
|
|
1122
|
+
expect(updated).toBeDefined();
|
|
1123
|
+
expect(updated!.logoUrl).toBe("https://cdn.simpleicons.org/notion");
|
|
1124
|
+
expect(updated!.displayLabel).toBe("Notion (updated)");
|
|
1125
|
+
});
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
describe("scopeSeparator empty-string coercion", () => {
|
|
1129
|
+
test("seedProviders coerces empty-string scopeSeparator to ' '", () => {
|
|
1130
|
+
seedProviders([
|
|
1131
|
+
{
|
|
1132
|
+
provider: "linear",
|
|
1133
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
1134
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
331
1135
|
defaultScopes: ["read"],
|
|
332
1136
|
scopePolicy: {},
|
|
333
|
-
|
|
334
|
-
|
|
1137
|
+
scopeSeparator: "",
|
|
1138
|
+
},
|
|
1139
|
+
]);
|
|
1140
|
+
|
|
1141
|
+
const row = getProvider("linear");
|
|
1142
|
+
expect(row!.scopeSeparator).toBe(" ");
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
test("registerProvider coerces empty-string scopeSeparator to ' '", () => {
|
|
1146
|
+
registerProvider({
|
|
1147
|
+
provider: "linear",
|
|
1148
|
+
authorizeUrl: "https://linear.app/oauth/authorize",
|
|
1149
|
+
tokenExchangeUrl: "https://api.linear.app/oauth/token",
|
|
1150
|
+
defaultScopes: ["read"],
|
|
1151
|
+
scopePolicy: {},
|
|
1152
|
+
scopeSeparator: "",
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
const row = getProvider("linear");
|
|
1156
|
+
expect(row!.scopeSeparator).toBe(" ");
|
|
335
1157
|
});
|
|
336
1158
|
});
|
|
337
1159
|
});
|
|
@@ -351,13 +1173,13 @@ describe("app operations", () => {
|
|
|
351
1173
|
expect(app.id).toMatch(
|
|
352
1174
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
|
|
353
1175
|
);
|
|
354
|
-
expect(app.
|
|
1176
|
+
expect(app.provider).toBe("github");
|
|
355
1177
|
expect(app.clientId).toBe("client-abc");
|
|
356
1178
|
expect(app.createdAt).toBeGreaterThan(0);
|
|
357
1179
|
expect(app.updatedAt).toBeGreaterThan(0);
|
|
358
1180
|
});
|
|
359
1181
|
|
|
360
|
-
test("returns the existing app when called again with same (
|
|
1182
|
+
test("returns the existing app when called again with same (provider, clientId)", async () => {
|
|
361
1183
|
seedTestProvider("github");
|
|
362
1184
|
const first = await upsertApp("github", "client-abc");
|
|
363
1185
|
const second = await upsertApp("github", "client-abc");
|
|
@@ -476,7 +1298,7 @@ describe("app operations", () => {
|
|
|
476
1298
|
|
|
477
1299
|
expect(fetched).toBeDefined();
|
|
478
1300
|
expect(fetched!.id).toBe(app.id);
|
|
479
|
-
expect(fetched!.
|
|
1301
|
+
expect(fetched!.provider).toBe("github");
|
|
480
1302
|
expect(fetched!.clientId).toBe("client-1");
|
|
481
1303
|
});
|
|
482
1304
|
|
|
@@ -564,7 +1386,7 @@ describe("connection operations", () => {
|
|
|
564
1386
|
const app = await createTestApp("github", "client-1");
|
|
565
1387
|
const conn = createConnection({
|
|
566
1388
|
oauthAppId: app.id,
|
|
567
|
-
|
|
1389
|
+
provider: "github",
|
|
568
1390
|
grantedScopes: ["repo", "user"],
|
|
569
1391
|
hasRefreshToken: true,
|
|
570
1392
|
accountInfo: "user@example.com",
|
|
@@ -574,7 +1396,7 @@ describe("connection operations", () => {
|
|
|
574
1396
|
|
|
575
1397
|
expect(conn.id).toBeTruthy();
|
|
576
1398
|
expect(conn.oauthAppId).toBe(app.id);
|
|
577
|
-
expect(conn.
|
|
1399
|
+
expect(conn.provider).toBe("github");
|
|
578
1400
|
expect(conn.status).toBe("active");
|
|
579
1401
|
expect(JSON.parse(conn.grantedScopes)).toEqual(["repo", "user"]);
|
|
580
1402
|
expect(conn.hasRefreshToken).toBe(1);
|
|
@@ -590,7 +1412,7 @@ describe("connection operations", () => {
|
|
|
590
1412
|
const app = await createTestApp("github", "client-1");
|
|
591
1413
|
const conn = createConnection({
|
|
592
1414
|
oauthAppId: app.id,
|
|
593
|
-
|
|
1415
|
+
provider: "github",
|
|
594
1416
|
grantedScopes: ["repo"],
|
|
595
1417
|
hasRefreshToken: false,
|
|
596
1418
|
});
|
|
@@ -598,7 +1420,7 @@ describe("connection operations", () => {
|
|
|
598
1420
|
const fetched = getConnection(conn.id);
|
|
599
1421
|
expect(fetched).toBeDefined();
|
|
600
1422
|
expect(fetched!.id).toBe(conn.id);
|
|
601
|
-
expect(fetched!.
|
|
1423
|
+
expect(fetched!.provider).toBe("github");
|
|
602
1424
|
});
|
|
603
1425
|
|
|
604
1426
|
test("returns undefined for unknown id", () => {
|
|
@@ -612,7 +1434,7 @@ describe("connection operations", () => {
|
|
|
612
1434
|
|
|
613
1435
|
createConnection({
|
|
614
1436
|
oauthAppId: app.id,
|
|
615
|
-
|
|
1437
|
+
provider: "github",
|
|
616
1438
|
grantedScopes: ["repo"],
|
|
617
1439
|
hasRefreshToken: false,
|
|
618
1440
|
createdAt: 1000,
|
|
@@ -620,7 +1442,7 @@ describe("connection operations", () => {
|
|
|
620
1442
|
|
|
621
1443
|
const conn2 = createConnection({
|
|
622
1444
|
oauthAppId: app.id,
|
|
623
|
-
|
|
1445
|
+
provider: "github",
|
|
624
1446
|
grantedScopes: ["repo", "user"],
|
|
625
1447
|
hasRefreshToken: true,
|
|
626
1448
|
createdAt: 2000,
|
|
@@ -636,7 +1458,7 @@ describe("connection operations", () => {
|
|
|
636
1458
|
|
|
637
1459
|
const conn1 = createConnection({
|
|
638
1460
|
oauthAppId: app.id,
|
|
639
|
-
|
|
1461
|
+
provider: "github",
|
|
640
1462
|
accountInfo: "user1@example.com",
|
|
641
1463
|
grantedScopes: ["repo"],
|
|
642
1464
|
hasRefreshToken: false,
|
|
@@ -645,7 +1467,7 @@ describe("connection operations", () => {
|
|
|
645
1467
|
|
|
646
1468
|
createConnection({
|
|
647
1469
|
oauthAppId: app.id,
|
|
648
|
-
|
|
1470
|
+
provider: "github",
|
|
649
1471
|
accountInfo: "user2@example.com",
|
|
650
1472
|
grantedScopes: ["repo"],
|
|
651
1473
|
hasRefreshToken: false,
|
|
@@ -665,7 +1487,7 @@ describe("connection operations", () => {
|
|
|
665
1487
|
|
|
666
1488
|
const conn1 = createConnection({
|
|
667
1489
|
oauthAppId: app1.id,
|
|
668
|
-
|
|
1490
|
+
provider: "github",
|
|
669
1491
|
grantedScopes: ["repo"],
|
|
670
1492
|
hasRefreshToken: false,
|
|
671
1493
|
createdAt: 1000,
|
|
@@ -673,7 +1495,7 @@ describe("connection operations", () => {
|
|
|
673
1495
|
|
|
674
1496
|
createConnection({
|
|
675
1497
|
oauthAppId: app2.id,
|
|
676
|
-
|
|
1498
|
+
provider: "github",
|
|
677
1499
|
grantedScopes: ["repo"],
|
|
678
1500
|
hasRefreshToken: false,
|
|
679
1501
|
createdAt: 2000,
|
|
@@ -689,7 +1511,7 @@ describe("connection operations", () => {
|
|
|
689
1511
|
|
|
690
1512
|
createConnection({
|
|
691
1513
|
oauthAppId: app.id,
|
|
692
|
-
|
|
1514
|
+
provider: "github",
|
|
693
1515
|
grantedScopes: ["repo"],
|
|
694
1516
|
hasRefreshToken: false,
|
|
695
1517
|
});
|
|
@@ -705,7 +1527,7 @@ describe("connection operations", () => {
|
|
|
705
1527
|
|
|
706
1528
|
const conn = createConnection({
|
|
707
1529
|
oauthAppId: app.id,
|
|
708
|
-
|
|
1530
|
+
provider: "github",
|
|
709
1531
|
grantedScopes: ["repo"],
|
|
710
1532
|
hasRefreshToken: false,
|
|
711
1533
|
});
|
|
@@ -727,7 +1549,7 @@ describe("connection operations", () => {
|
|
|
727
1549
|
// Create two connections with explicit timestamps so ordering is deterministic
|
|
728
1550
|
createConnection({
|
|
729
1551
|
oauthAppId: app.id,
|
|
730
|
-
|
|
1552
|
+
provider: "github",
|
|
731
1553
|
grantedScopes: ["repo"],
|
|
732
1554
|
hasRefreshToken: false,
|
|
733
1555
|
createdAt: 1000,
|
|
@@ -735,7 +1557,7 @@ describe("connection operations", () => {
|
|
|
735
1557
|
|
|
736
1558
|
const conn2 = createConnection({
|
|
737
1559
|
oauthAppId: app.id,
|
|
738
|
-
|
|
1560
|
+
provider: "github",
|
|
739
1561
|
grantedScopes: ["repo", "user"],
|
|
740
1562
|
hasRefreshToken: true,
|
|
741
1563
|
createdAt: 2000,
|
|
@@ -751,14 +1573,14 @@ describe("connection operations", () => {
|
|
|
751
1573
|
|
|
752
1574
|
const conn1 = createConnection({
|
|
753
1575
|
oauthAppId: app.id,
|
|
754
|
-
|
|
1576
|
+
provider: "github",
|
|
755
1577
|
grantedScopes: ["repo"],
|
|
756
1578
|
hasRefreshToken: false,
|
|
757
1579
|
});
|
|
758
1580
|
|
|
759
1581
|
const conn2 = createConnection({
|
|
760
1582
|
oauthAppId: app.id,
|
|
761
|
-
|
|
1583
|
+
provider: "github",
|
|
762
1584
|
grantedScopes: ["repo", "user"],
|
|
763
1585
|
hasRefreshToken: true,
|
|
764
1586
|
});
|
|
@@ -776,7 +1598,7 @@ describe("connection operations", () => {
|
|
|
776
1598
|
|
|
777
1599
|
const conn = createConnection({
|
|
778
1600
|
oauthAppId: app.id,
|
|
779
|
-
|
|
1601
|
+
provider: "github",
|
|
780
1602
|
grantedScopes: ["repo"],
|
|
781
1603
|
hasRefreshToken: false,
|
|
782
1604
|
});
|
|
@@ -798,7 +1620,7 @@ describe("connection operations", () => {
|
|
|
798
1620
|
|
|
799
1621
|
const conn1 = createConnection({
|
|
800
1622
|
oauthAppId: app.id,
|
|
801
|
-
|
|
1623
|
+
provider: "github",
|
|
802
1624
|
accountInfo: "user1@example.com",
|
|
803
1625
|
grantedScopes: ["repo"],
|
|
804
1626
|
hasRefreshToken: false,
|
|
@@ -807,7 +1629,7 @@ describe("connection operations", () => {
|
|
|
807
1629
|
|
|
808
1630
|
createConnection({
|
|
809
1631
|
oauthAppId: app.id,
|
|
810
|
-
|
|
1632
|
+
provider: "github",
|
|
811
1633
|
accountInfo: "user2@example.com",
|
|
812
1634
|
grantedScopes: ["repo"],
|
|
813
1635
|
hasRefreshToken: false,
|
|
@@ -827,7 +1649,7 @@ describe("connection operations", () => {
|
|
|
827
1649
|
|
|
828
1650
|
const conn = createConnection({
|
|
829
1651
|
oauthAppId: app.id,
|
|
830
|
-
|
|
1652
|
+
provider: "github",
|
|
831
1653
|
accountInfo: "user@example.com",
|
|
832
1654
|
grantedScopes: ["repo"],
|
|
833
1655
|
hasRefreshToken: false,
|
|
@@ -843,7 +1665,7 @@ describe("connection operations", () => {
|
|
|
843
1665
|
|
|
844
1666
|
createConnection({
|
|
845
1667
|
oauthAppId: app.id,
|
|
846
|
-
|
|
1668
|
+
provider: "github",
|
|
847
1669
|
accountInfo: "user@example.com",
|
|
848
1670
|
grantedScopes: ["repo"],
|
|
849
1671
|
hasRefreshToken: false,
|
|
@@ -861,7 +1683,7 @@ describe("connection operations", () => {
|
|
|
861
1683
|
|
|
862
1684
|
const conn = createConnection({
|
|
863
1685
|
oauthAppId: app.id,
|
|
864
|
-
|
|
1686
|
+
provider: "github",
|
|
865
1687
|
accountInfo: "user@example.com",
|
|
866
1688
|
grantedScopes: ["repo"],
|
|
867
1689
|
hasRefreshToken: false,
|
|
@@ -882,7 +1704,7 @@ describe("connection operations", () => {
|
|
|
882
1704
|
|
|
883
1705
|
createConnection({
|
|
884
1706
|
oauthAppId: app.id,
|
|
885
|
-
|
|
1707
|
+
provider: "github",
|
|
886
1708
|
accountInfo: "user1@example.com",
|
|
887
1709
|
grantedScopes: ["repo"],
|
|
888
1710
|
hasRefreshToken: false,
|
|
@@ -890,7 +1712,7 @@ describe("connection operations", () => {
|
|
|
890
1712
|
|
|
891
1713
|
createConnection({
|
|
892
1714
|
oauthAppId: app.id,
|
|
893
|
-
|
|
1715
|
+
provider: "github",
|
|
894
1716
|
accountInfo: "user2@example.com",
|
|
895
1717
|
grantedScopes: ["repo"],
|
|
896
1718
|
hasRefreshToken: false,
|
|
@@ -905,7 +1727,7 @@ describe("connection operations", () => {
|
|
|
905
1727
|
|
|
906
1728
|
createConnection({
|
|
907
1729
|
oauthAppId: app.id,
|
|
908
|
-
|
|
1730
|
+
provider: "github",
|
|
909
1731
|
accountInfo: "user1@example.com",
|
|
910
1732
|
grantedScopes: ["repo"],
|
|
911
1733
|
hasRefreshToken: false,
|
|
@@ -913,7 +1735,7 @@ describe("connection operations", () => {
|
|
|
913
1735
|
|
|
914
1736
|
const conn2 = createConnection({
|
|
915
1737
|
oauthAppId: app.id,
|
|
916
|
-
|
|
1738
|
+
provider: "github",
|
|
917
1739
|
accountInfo: "user2@example.com",
|
|
918
1740
|
grantedScopes: ["repo"],
|
|
919
1741
|
hasRefreshToken: false,
|
|
@@ -936,7 +1758,7 @@ describe("connection operations", () => {
|
|
|
936
1758
|
const app = await createTestApp("github", "client-1");
|
|
937
1759
|
const conn = createConnection({
|
|
938
1760
|
oauthAppId: app.id,
|
|
939
|
-
|
|
1761
|
+
provider: "github",
|
|
940
1762
|
grantedScopes: ["repo"],
|
|
941
1763
|
hasRefreshToken: false,
|
|
942
1764
|
});
|
|
@@ -950,7 +1772,7 @@ describe("connection operations", () => {
|
|
|
950
1772
|
const app = await createTestApp("github", "client-1");
|
|
951
1773
|
createConnection({
|
|
952
1774
|
oauthAppId: app.id,
|
|
953
|
-
|
|
1775
|
+
provider: "github",
|
|
954
1776
|
grantedScopes: ["repo"],
|
|
955
1777
|
hasRefreshToken: false,
|
|
956
1778
|
});
|
|
@@ -967,7 +1789,7 @@ describe("connection operations", () => {
|
|
|
967
1789
|
const app = await createTestApp("github", "client-1");
|
|
968
1790
|
const conn = createConnection({
|
|
969
1791
|
oauthAppId: app.id,
|
|
970
|
-
|
|
1792
|
+
provider: "github",
|
|
971
1793
|
grantedScopes: ["repo"],
|
|
972
1794
|
hasRefreshToken: false,
|
|
973
1795
|
});
|
|
@@ -984,7 +1806,7 @@ describe("connection operations", () => {
|
|
|
984
1806
|
const app = await createTestApp("github", "client-1");
|
|
985
1807
|
const conn = createConnection({
|
|
986
1808
|
oauthAppId: app.id,
|
|
987
|
-
|
|
1809
|
+
provider: "github",
|
|
988
1810
|
grantedScopes: ["repo"],
|
|
989
1811
|
hasRefreshToken: false,
|
|
990
1812
|
});
|
|
@@ -1021,7 +1843,7 @@ describe("connection operations", () => {
|
|
|
1021
1843
|
|
|
1022
1844
|
const conn = createConnection({
|
|
1023
1845
|
oauthAppId: app1.id,
|
|
1024
|
-
|
|
1846
|
+
provider: "github",
|
|
1025
1847
|
grantedScopes: ["repo"],
|
|
1026
1848
|
hasRefreshToken: false,
|
|
1027
1849
|
});
|
|
@@ -1051,13 +1873,13 @@ describe("connection operations", () => {
|
|
|
1051
1873
|
|
|
1052
1874
|
createConnection({
|
|
1053
1875
|
oauthAppId: ghApp.id,
|
|
1054
|
-
|
|
1876
|
+
provider: "github",
|
|
1055
1877
|
grantedScopes: ["repo"],
|
|
1056
1878
|
hasRefreshToken: false,
|
|
1057
1879
|
});
|
|
1058
1880
|
createConnection({
|
|
1059
1881
|
oauthAppId: googApp.id,
|
|
1060
|
-
|
|
1882
|
+
provider: "google",
|
|
1061
1883
|
grantedScopes: ["email"],
|
|
1062
1884
|
hasRefreshToken: true,
|
|
1063
1885
|
});
|
|
@@ -1073,24 +1895,24 @@ describe("connection operations", () => {
|
|
|
1073
1895
|
|
|
1074
1896
|
createConnection({
|
|
1075
1897
|
oauthAppId: ghApp.id,
|
|
1076
|
-
|
|
1898
|
+
provider: "github",
|
|
1077
1899
|
grantedScopes: ["repo"],
|
|
1078
1900
|
hasRefreshToken: false,
|
|
1079
1901
|
});
|
|
1080
1902
|
createConnection({
|
|
1081
1903
|
oauthAppId: googApp.id,
|
|
1082
|
-
|
|
1904
|
+
provider: "google",
|
|
1083
1905
|
grantedScopes: ["email"],
|
|
1084
1906
|
hasRefreshToken: true,
|
|
1085
1907
|
});
|
|
1086
1908
|
|
|
1087
1909
|
const ghConns = listConnections("github");
|
|
1088
1910
|
expect(ghConns).toHaveLength(1);
|
|
1089
|
-
expect(ghConns[0].
|
|
1911
|
+
expect(ghConns[0].provider).toBe("github");
|
|
1090
1912
|
|
|
1091
1913
|
const googConns = listConnections("google");
|
|
1092
1914
|
expect(googConns).toHaveLength(1);
|
|
1093
|
-
expect(googConns[0].
|
|
1915
|
+
expect(googConns[0].provider).toBe("google");
|
|
1094
1916
|
});
|
|
1095
1917
|
|
|
1096
1918
|
test("returns empty array when no connections exist", () => {
|
|
@@ -1103,7 +1925,7 @@ describe("connection operations", () => {
|
|
|
1103
1925
|
const app = await createTestApp("github", "client-1");
|
|
1104
1926
|
const conn = createConnection({
|
|
1105
1927
|
oauthAppId: app.id,
|
|
1106
|
-
|
|
1928
|
+
provider: "github",
|
|
1107
1929
|
grantedScopes: ["repo"],
|
|
1108
1930
|
hasRefreshToken: false,
|
|
1109
1931
|
});
|
|
@@ -1124,17 +1946,40 @@ describe("connection operations", () => {
|
|
|
1124
1946
|
// ---------------------------------------------------------------------------
|
|
1125
1947
|
|
|
1126
1948
|
describe("disconnectOAuthProvider", () => {
|
|
1949
|
+
/**
|
|
1950
|
+
* Seed a provider with revokeUrl and (optionally) a revokeBodyTemplate.
|
|
1951
|
+
*/
|
|
1952
|
+
function seedProviderWithRevoke(
|
|
1953
|
+
provider: string,
|
|
1954
|
+
revokeUrl: string | null,
|
|
1955
|
+
revokeBodyTemplate?: Record<string, string>,
|
|
1956
|
+
): void {
|
|
1957
|
+
seedProviders([
|
|
1958
|
+
{
|
|
1959
|
+
provider,
|
|
1960
|
+
authorizeUrl: `https://${provider}.example.com/authorize`,
|
|
1961
|
+
tokenExchangeUrl: `https://${provider}.example.com/token`,
|
|
1962
|
+
defaultScopes: ["read"],
|
|
1963
|
+
scopePolicy: {},
|
|
1964
|
+
...(revokeUrl ? { revokeUrl } : {}),
|
|
1965
|
+
...(revokeBodyTemplate ? { revokeBodyTemplate } : {}),
|
|
1966
|
+
},
|
|
1967
|
+
]);
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1127
1970
|
test("returns 'not-found' when no connection exists for the provider", async () => {
|
|
1128
1971
|
const result = await disconnectOAuthProvider("github");
|
|
1129
1972
|
expect(result).toBe("not-found");
|
|
1130
1973
|
expect(mockDeleteSecureKeyAsync).not.toHaveBeenCalled();
|
|
1974
|
+
// No upstream call should be made when there is no connection at all.
|
|
1975
|
+
expect(getMockFetchCalls().length).toBe(0);
|
|
1131
1976
|
});
|
|
1132
1977
|
|
|
1133
1978
|
test("returns 'disconnected' and deletes connection row and secure keys when connection exists", async () => {
|
|
1134
1979
|
const app = await createTestApp("github", "client-1");
|
|
1135
1980
|
const conn = createConnection({
|
|
1136
1981
|
oauthAppId: app.id,
|
|
1137
|
-
|
|
1982
|
+
provider: "github",
|
|
1138
1983
|
grantedScopes: ["repo"],
|
|
1139
1984
|
hasRefreshToken: true,
|
|
1140
1985
|
});
|
|
@@ -1154,6 +1999,365 @@ describe("disconnectOAuthProvider", () => {
|
|
|
1154
1999
|
// Verify connection row was deleted
|
|
1155
2000
|
expect(getConnection(conn.id)).toBeUndefined();
|
|
1156
2001
|
});
|
|
2002
|
+
|
|
2003
|
+
test("calls upstream revoke when provider has revokeUrl and access token exists", async () => {
|
|
2004
|
+
seedProviderWithRevoke("google", "https://oauth2.googleapis.com/revoke", {
|
|
2005
|
+
token: "{access_token}",
|
|
2006
|
+
client_id: "{client_id}",
|
|
2007
|
+
});
|
|
2008
|
+
const app = await upsertApp("google", "client-1");
|
|
2009
|
+
const conn = createConnection({
|
|
2010
|
+
oauthAppId: app.id,
|
|
2011
|
+
provider: "google",
|
|
2012
|
+
grantedScopes: ["email"],
|
|
2013
|
+
hasRefreshToken: true,
|
|
2014
|
+
});
|
|
2015
|
+
secureKeyValues.set(
|
|
2016
|
+
`oauth_connection/${conn.id}/access_token`,
|
|
2017
|
+
"fake-token-xyz",
|
|
2018
|
+
);
|
|
2019
|
+
|
|
2020
|
+
mockFetch(
|
|
2021
|
+
"https://oauth2.googleapis.com/revoke",
|
|
2022
|
+
{ method: "POST" },
|
|
2023
|
+
{ status: 200, body: {} },
|
|
2024
|
+
);
|
|
2025
|
+
|
|
2026
|
+
const result = await disconnectOAuthProvider("google");
|
|
2027
|
+
expect(result).toBe("disconnected");
|
|
2028
|
+
|
|
2029
|
+
const calls = getMockFetchCalls();
|
|
2030
|
+
expect(calls.length).toBe(1);
|
|
2031
|
+
expect(calls[0]!.path).toContain("https://oauth2.googleapis.com/revoke");
|
|
2032
|
+
|
|
2033
|
+
const body = String(calls[0]!.init.body ?? "");
|
|
2034
|
+
const params = new URLSearchParams(body);
|
|
2035
|
+
expect(params.get("token")).toBe("fake-token-xyz");
|
|
2036
|
+
expect(params.get("client_id")).toBe("client-1");
|
|
2037
|
+
});
|
|
2038
|
+
|
|
2039
|
+
test("skips upstream revoke when provider has no revokeUrl", async () => {
|
|
2040
|
+
// GitHub seeded by createTestApp via seedTestProvider — no revokeUrl.
|
|
2041
|
+
const app = await createTestApp("github", "client-1");
|
|
2042
|
+
const conn = createConnection({
|
|
2043
|
+
oauthAppId: app.id,
|
|
2044
|
+
provider: "github",
|
|
2045
|
+
grantedScopes: ["repo"],
|
|
2046
|
+
hasRefreshToken: false,
|
|
2047
|
+
});
|
|
2048
|
+
secureKeyValues.set(
|
|
2049
|
+
`oauth_connection/${conn.id}/access_token`,
|
|
2050
|
+
"github-token",
|
|
2051
|
+
);
|
|
2052
|
+
|
|
2053
|
+
const result = await disconnectOAuthProvider("github");
|
|
2054
|
+
expect(result).toBe("disconnected");
|
|
2055
|
+
expect(getMockFetchCalls().length).toBe(0);
|
|
2056
|
+
});
|
|
2057
|
+
|
|
2058
|
+
test("skips upstream revoke when no access token exists in secure storage", async () => {
|
|
2059
|
+
seedProviderWithRevoke("google", "https://oauth2.googleapis.com/revoke", {
|
|
2060
|
+
token: "{access_token}",
|
|
2061
|
+
});
|
|
2062
|
+
const app = await upsertApp("google", "client-1");
|
|
2063
|
+
createConnection({
|
|
2064
|
+
oauthAppId: app.id,
|
|
2065
|
+
provider: "google",
|
|
2066
|
+
grantedScopes: ["email"],
|
|
2067
|
+
hasRefreshToken: false,
|
|
2068
|
+
});
|
|
2069
|
+
// No access token seeded into secureKeyValues.
|
|
2070
|
+
|
|
2071
|
+
const result = await disconnectOAuthProvider("google");
|
|
2072
|
+
expect(result).toBe("disconnected");
|
|
2073
|
+
expect(getMockFetchCalls().length).toBe(0);
|
|
2074
|
+
});
|
|
2075
|
+
|
|
2076
|
+
test("continues local cleanup when upstream revoke returns non-2xx", async () => {
|
|
2077
|
+
seedProviderWithRevoke("google", "https://oauth2.googleapis.com/revoke", {
|
|
2078
|
+
token: "{access_token}",
|
|
2079
|
+
});
|
|
2080
|
+
const app = await upsertApp("google", "client-1");
|
|
2081
|
+
const conn = createConnection({
|
|
2082
|
+
oauthAppId: app.id,
|
|
2083
|
+
provider: "google",
|
|
2084
|
+
grantedScopes: ["email"],
|
|
2085
|
+
hasRefreshToken: true,
|
|
2086
|
+
});
|
|
2087
|
+
secureKeyValues.set(
|
|
2088
|
+
`oauth_connection/${conn.id}/access_token`,
|
|
2089
|
+
"fake-token-xyz",
|
|
2090
|
+
);
|
|
2091
|
+
|
|
2092
|
+
mockFetch(
|
|
2093
|
+
"https://oauth2.googleapis.com/revoke",
|
|
2094
|
+
{ method: "POST" },
|
|
2095
|
+
{ status: 400, body: { error: "invalid_token" } },
|
|
2096
|
+
);
|
|
2097
|
+
|
|
2098
|
+
const result = await disconnectOAuthProvider("google");
|
|
2099
|
+
expect(result).toBe("disconnected");
|
|
2100
|
+
expect(getMockFetchCalls().length).toBe(1);
|
|
2101
|
+
// Local cleanup still happened
|
|
2102
|
+
expect(mockDeleteSecureKeyAsync).toHaveBeenCalledWith(
|
|
2103
|
+
`oauth_connection/${conn.id}/access_token`,
|
|
2104
|
+
);
|
|
2105
|
+
expect(getConnection(conn.id)).toBeUndefined();
|
|
2106
|
+
});
|
|
2107
|
+
|
|
2108
|
+
test("continues local cleanup when upstream revoke throws (network error)", async () => {
|
|
2109
|
+
seedProviderWithRevoke("google", "https://oauth2.googleapis.com/revoke", {
|
|
2110
|
+
token: "{access_token}",
|
|
2111
|
+
});
|
|
2112
|
+
const app = await upsertApp("google", "client-1");
|
|
2113
|
+
const conn = createConnection({
|
|
2114
|
+
oauthAppId: app.id,
|
|
2115
|
+
provider: "google",
|
|
2116
|
+
grantedScopes: ["email"],
|
|
2117
|
+
hasRefreshToken: true,
|
|
2118
|
+
});
|
|
2119
|
+
secureKeyValues.set(
|
|
2120
|
+
`oauth_connection/${conn.id}/access_token`,
|
|
2121
|
+
"fake-token-xyz",
|
|
2122
|
+
);
|
|
2123
|
+
|
|
2124
|
+
// 500 exercises the same swallow path as a network error.
|
|
2125
|
+
mockFetch(
|
|
2126
|
+
"https://oauth2.googleapis.com/revoke",
|
|
2127
|
+
{ method: "POST" },
|
|
2128
|
+
{ status: 500, body: { error: "server_error" } },
|
|
2129
|
+
);
|
|
2130
|
+
|
|
2131
|
+
const result = await disconnectOAuthProvider("google");
|
|
2132
|
+
expect(result).toBe("disconnected");
|
|
2133
|
+
expect(getConnection(conn.id)).toBeUndefined();
|
|
2134
|
+
});
|
|
2135
|
+
|
|
2136
|
+
test("substitutes {access_token} and {client_id} in body template values", async () => {
|
|
2137
|
+
seedProviderWithRevoke("google", "https://oauth2.googleapis.com/revoke", {
|
|
2138
|
+
token: "{access_token}",
|
|
2139
|
+
client_id: "{client_id}",
|
|
2140
|
+
token_type_hint: "access_token",
|
|
2141
|
+
});
|
|
2142
|
+
const app = await upsertApp("google", "client-substitution");
|
|
2143
|
+
const conn = createConnection({
|
|
2144
|
+
oauthAppId: app.id,
|
|
2145
|
+
provider: "google",
|
|
2146
|
+
grantedScopes: ["email"],
|
|
2147
|
+
hasRefreshToken: false,
|
|
2148
|
+
});
|
|
2149
|
+
secureKeyValues.set(
|
|
2150
|
+
`oauth_connection/${conn.id}/access_token`,
|
|
2151
|
+
"tok-substitution",
|
|
2152
|
+
);
|
|
2153
|
+
|
|
2154
|
+
mockFetch(
|
|
2155
|
+
"https://oauth2.googleapis.com/revoke",
|
|
2156
|
+
{ method: "POST" },
|
|
2157
|
+
{ status: 200, body: {} },
|
|
2158
|
+
);
|
|
2159
|
+
|
|
2160
|
+
await disconnectOAuthProvider("google");
|
|
2161
|
+
|
|
2162
|
+
const calls = getMockFetchCalls();
|
|
2163
|
+
expect(calls.length).toBe(1);
|
|
2164
|
+
const body = String(calls[0]!.init.body ?? "");
|
|
2165
|
+
const params = new URLSearchParams(body);
|
|
2166
|
+
expect(params.get("token")).toBe("tok-substitution");
|
|
2167
|
+
expect(params.get("client_id")).toBe("client-substitution");
|
|
2168
|
+
expect(params.get("token_type_hint")).toBe("access_token");
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
test("treats $-prefixed patterns in access token as literal text (String.replace gotcha)", async () => {
|
|
2172
|
+
// String.prototype.replace interprets $-prefixed patterns in the
|
|
2173
|
+
// replacement string as special sequences ($& = matched substring,
|
|
2174
|
+
// $' = after match, $` = before match, $$ = literal $). If the access
|
|
2175
|
+
// token contains "$&", a naive `.replace("{access_token}", accessToken)`
|
|
2176
|
+
// would expand it to "{access_token}" (the matched string) instead of
|
|
2177
|
+
// substituting literally. This test guards against that by asserting
|
|
2178
|
+
// the captured body contains the literal "tok$&abc" — which only holds
|
|
2179
|
+
// when we use a function-replacement callback that preserves literal
|
|
2180
|
+
// semantics and mirrors Python's str.replace() behavior.
|
|
2181
|
+
seedProviderWithRevoke("google", "https://revoke.example.com/r", {
|
|
2182
|
+
token: "{access_token}",
|
|
2183
|
+
});
|
|
2184
|
+
const app = await upsertApp("google", "client-1");
|
|
2185
|
+
const conn = createConnection({
|
|
2186
|
+
oauthAppId: app.id,
|
|
2187
|
+
provider: "google",
|
|
2188
|
+
grantedScopes: ["email"],
|
|
2189
|
+
hasRefreshToken: false,
|
|
2190
|
+
});
|
|
2191
|
+
secureKeyValues.set(`oauth_connection/${conn.id}/access_token`, "tok$&abc");
|
|
2192
|
+
|
|
2193
|
+
mockFetch(
|
|
2194
|
+
"https://revoke.example.com/r",
|
|
2195
|
+
{ method: "POST" },
|
|
2196
|
+
{ status: 200, body: {} },
|
|
2197
|
+
);
|
|
2198
|
+
|
|
2199
|
+
await disconnectOAuthProvider("google");
|
|
2200
|
+
|
|
2201
|
+
const calls = getMockFetchCalls();
|
|
2202
|
+
expect(calls.length).toBe(1);
|
|
2203
|
+
const body = String(calls[0]!.init.body ?? "");
|
|
2204
|
+
const params = new URLSearchParams(body);
|
|
2205
|
+
// The literal access token — not the $&-expanded version, which would
|
|
2206
|
+
// be "tok{access_token}abc" (where $& matched "{access_token}").
|
|
2207
|
+
expect(params.get("token")).toBe("tok$&abc");
|
|
2208
|
+
});
|
|
2209
|
+
|
|
2210
|
+
test("replaces all occurrences of {access_token} in body template values (matching Python str.replace)", async () => {
|
|
2211
|
+
// Python's str.replace(old, new) replaces ALL occurrences by default,
|
|
2212
|
+
// whereas JavaScript's String.prototype.replace with a string pattern
|
|
2213
|
+
// only replaces the FIRST occurrence. The platform's try_revoke_token
|
|
2214
|
+
// is implemented in Python, so any template value containing repeated
|
|
2215
|
+
// placeholders must have ALL of them substituted to preserve parity.
|
|
2216
|
+
// This test guards against a regression to .replace(), which would
|
|
2217
|
+
// leave the second {access_token} as a literal placeholder.
|
|
2218
|
+
seedProviderWithRevoke("google", "https://revoke.example.com/r", {
|
|
2219
|
+
token: "token={access_token}&also={access_token}",
|
|
2220
|
+
});
|
|
2221
|
+
const app = await upsertApp("google", "client-1");
|
|
2222
|
+
const conn = createConnection({
|
|
2223
|
+
oauthAppId: app.id,
|
|
2224
|
+
provider: "google",
|
|
2225
|
+
grantedScopes: ["email"],
|
|
2226
|
+
hasRefreshToken: false,
|
|
2227
|
+
});
|
|
2228
|
+
secureKeyValues.set(`oauth_connection/${conn.id}/access_token`, "fake-abc");
|
|
2229
|
+
|
|
2230
|
+
mockFetch(
|
|
2231
|
+
"https://revoke.example.com/r",
|
|
2232
|
+
{ method: "POST" },
|
|
2233
|
+
{ status: 200, body: {} },
|
|
2234
|
+
);
|
|
2235
|
+
|
|
2236
|
+
await disconnectOAuthProvider("google");
|
|
2237
|
+
|
|
2238
|
+
const calls = getMockFetchCalls();
|
|
2239
|
+
expect(calls.length).toBe(1);
|
|
2240
|
+
const body = String(calls[0]!.init.body ?? "");
|
|
2241
|
+
const params = new URLSearchParams(body);
|
|
2242
|
+
// Both {access_token} placeholders must be substituted. With the buggy
|
|
2243
|
+
// .replace() (single-occurrence), this would be
|
|
2244
|
+
// "token=fake-abc&also={access_token}" instead.
|
|
2245
|
+
expect(params.get("token")).toBe("token=fake-abc&also=fake-abc");
|
|
2246
|
+
});
|
|
2247
|
+
|
|
2248
|
+
test("coerces non-string body template values to strings", async () => {
|
|
2249
|
+
// seedProviderWithRevoke restricts to Record<string, string>; bypass it
|
|
2250
|
+
// here by inserting a template with a numeric value via a direct seed.
|
|
2251
|
+
seedProviders([
|
|
2252
|
+
{
|
|
2253
|
+
provider: "google",
|
|
2254
|
+
authorizeUrl: "https://google.example.com/authorize",
|
|
2255
|
+
tokenExchangeUrl: "https://google.example.com/token",
|
|
2256
|
+
defaultScopes: ["email"],
|
|
2257
|
+
scopePolicy: {},
|
|
2258
|
+
revokeUrl: "https://oauth2.googleapis.com/revoke",
|
|
2259
|
+
revokeBodyTemplate: {
|
|
2260
|
+
token: "{access_token}",
|
|
2261
|
+
// expires_in is a number — must be coerced via String(value).
|
|
2262
|
+
expires_in: 3600,
|
|
2263
|
+
} as unknown as Record<string, string>,
|
|
2264
|
+
},
|
|
2265
|
+
]);
|
|
2266
|
+
const app = await upsertApp("google", "client-1");
|
|
2267
|
+
const conn = createConnection({
|
|
2268
|
+
oauthAppId: app.id,
|
|
2269
|
+
provider: "google",
|
|
2270
|
+
grantedScopes: ["email"],
|
|
2271
|
+
hasRefreshToken: false,
|
|
2272
|
+
});
|
|
2273
|
+
secureKeyValues.set(
|
|
2274
|
+
`oauth_connection/${conn.id}/access_token`,
|
|
2275
|
+
"fake-token-xyz",
|
|
2276
|
+
);
|
|
2277
|
+
|
|
2278
|
+
mockFetch(
|
|
2279
|
+
"https://oauth2.googleapis.com/revoke",
|
|
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
|
+
expect(params.get("token")).toBe("fake-token-xyz");
|
|
2291
|
+
expect(params.get("expires_in")).toBe("3600");
|
|
2292
|
+
});
|
|
2293
|
+
|
|
2294
|
+
test("revokes BEFORE deleting tokens from secure storage", async () => {
|
|
2295
|
+
seedProviderWithRevoke("google", "https://oauth2.googleapis.com/revoke", {
|
|
2296
|
+
token: "{access_token}",
|
|
2297
|
+
});
|
|
2298
|
+
const app = await upsertApp("google", "client-1");
|
|
2299
|
+
const conn = createConnection({
|
|
2300
|
+
oauthAppId: app.id,
|
|
2301
|
+
provider: "google",
|
|
2302
|
+
grantedScopes: ["email"],
|
|
2303
|
+
hasRefreshToken: true,
|
|
2304
|
+
});
|
|
2305
|
+
secureKeyValues.set(
|
|
2306
|
+
`oauth_connection/${conn.id}/access_token`,
|
|
2307
|
+
"fake-token-xyz",
|
|
2308
|
+
);
|
|
2309
|
+
|
|
2310
|
+
const order: string[] = [];
|
|
2311
|
+
|
|
2312
|
+
// Wrap the mockFetch entry's response handler by registering a fetch
|
|
2313
|
+
// mock that records the call order via the existing mockFetch helper.
|
|
2314
|
+
// The mock fetch records to getMockFetchCalls; we tag ordering by
|
|
2315
|
+
// pushing a marker as soon as the response is constructed below.
|
|
2316
|
+
mockFetch(
|
|
2317
|
+
"https://oauth2.googleapis.com/revoke",
|
|
2318
|
+
{ method: "POST" },
|
|
2319
|
+
new Response(JSON.stringify({}), {
|
|
2320
|
+
status: 200,
|
|
2321
|
+
headers: { "Content-Type": "application/json" },
|
|
2322
|
+
}),
|
|
2323
|
+
);
|
|
2324
|
+
|
|
2325
|
+
// Wrap delete to record its order. We replace the mock implementation
|
|
2326
|
+
// for the duration of this test only.
|
|
2327
|
+
mockDeleteSecureKeyAsync.mockImplementation(() => {
|
|
2328
|
+
order.push("delete");
|
|
2329
|
+
return Promise.resolve("deleted" as const);
|
|
2330
|
+
});
|
|
2331
|
+
|
|
2332
|
+
// Wrap fetch one more layer: tap into the actual fetch call to record
|
|
2333
|
+
// ordering. We do this by overriding globalThis.fetch with a wrapper
|
|
2334
|
+
// that calls through to the existing mock and records ordering first.
|
|
2335
|
+
const wrappedFetch = globalThis.fetch;
|
|
2336
|
+
globalThis.fetch = (async (
|
|
2337
|
+
input: RequestInfo | URL,
|
|
2338
|
+
init?: RequestInit,
|
|
2339
|
+
) => {
|
|
2340
|
+
order.push("fetch");
|
|
2341
|
+
return wrappedFetch(input, init);
|
|
2342
|
+
}) as typeof globalThis.fetch;
|
|
2343
|
+
|
|
2344
|
+
try {
|
|
2345
|
+
const result = await disconnectOAuthProvider("google");
|
|
2346
|
+
expect(result).toBe("disconnected");
|
|
2347
|
+
} finally {
|
|
2348
|
+
// Restore the wrapper layer; resetMockFetch in beforeEach will reset
|
|
2349
|
+
// the underlying mock for the next test.
|
|
2350
|
+
globalThis.fetch = wrappedFetch;
|
|
2351
|
+
mockDeleteSecureKeyAsync.mockImplementation(
|
|
2352
|
+
(): Promise<"deleted" | "not-found" | "error"> =>
|
|
2353
|
+
Promise.resolve("deleted" as const),
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
expect(order[0]).toBe("fetch");
|
|
2358
|
+
expect(order).toContain("delete");
|
|
2359
|
+
expect(order.indexOf("fetch")).toBeLessThan(order.indexOf("delete"));
|
|
2360
|
+
});
|
|
1157
2361
|
});
|
|
1158
2362
|
|
|
1159
2363
|
// ---------------------------------------------------------------------------
|
|
@@ -1172,7 +2376,7 @@ describe("FK constraints", () => {
|
|
|
1172
2376
|
expect(() =>
|
|
1173
2377
|
createConnection({
|
|
1174
2378
|
oauthAppId: "nonexistent-app-id",
|
|
1175
|
-
|
|
2379
|
+
provider: "github",
|
|
1176
2380
|
grantedScopes: ["repo"],
|
|
1177
2381
|
hasRefreshToken: false,
|
|
1178
2382
|
}),
|