@vellumai/assistant 0.8.3 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docker-entrypoint.sh +0 -1
- package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
- package/openapi.yaml +610 -16
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
- package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
- package/src/__tests__/agent-loop.test.ts +88 -3
- package/src/__tests__/anthropic-provider.test.ts +272 -0
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
- package/src/__tests__/channel-delivery-store.test.ts +193 -0
- package/src/__tests__/channel-reply-delivery.test.ts +284 -5
- package/src/__tests__/channel-retry-sweep.test.ts +274 -1
- package/src/__tests__/compaction-events.test.ts +1 -1
- package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
- package/src/__tests__/config-watcher.test.ts +1 -1
- package/src/__tests__/context-token-estimator.test.ts +91 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
- package/src/__tests__/conversation-agent-loop.test.ts +25 -7
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
- package/src/__tests__/conversation-clean-command.test.ts +137 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-fork-crud.test.ts +161 -0
- package/src/__tests__/conversation-lifecycle.test.ts +1 -1
- package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
- package/src/__tests__/conversation-pairing.test.ts +2 -2
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +264 -81
- package/src/__tests__/conversation-seed-composer.test.ts +66 -4
- package/src/__tests__/conversation-slash-commands.test.ts +36 -8
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
- package/src/__tests__/credential-security-invariants.test.ts +6 -0
- package/src/__tests__/cu-unified-flow.test.ts +10 -1
- package/src/__tests__/dm-backfill.test.ts +64 -0
- package/src/__tests__/dm-persistence.test.ts +33 -0
- package/src/__tests__/document-find-replace.test.ts +501 -0
- package/src/__tests__/first-greeting.test.ts +23 -2
- package/src/__tests__/headless-browser-navigate.test.ts +172 -0
- package/src/__tests__/host-bash-proxy.test.ts +6 -0
- package/src/__tests__/host-browser-proxy.test.ts +10 -0
- package/src/__tests__/host-cu-proxy.test.ts +8 -1
- package/src/__tests__/host-file-proxy.test.ts +8 -1
- package/src/__tests__/host-transfer-proxy.test.ts +8 -1
- package/src/__tests__/identity-routes.test.ts +57 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
- package/src/__tests__/injector-chain.test.ts +2 -0
- package/src/__tests__/injector-document-comments.test.ts +378 -0
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
- package/src/__tests__/list-messages-attachments.test.ts +21 -17
- package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
- package/src/__tests__/list-messages-page-latest.test.ts +130 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
- package/src/__tests__/llm-context-normalization.test.ts +0 -2
- package/src/__tests__/llm-resolver.test.ts +85 -1
- package/src/__tests__/log-export-routes.test.ts +99 -2
- package/src/__tests__/message-queue-steer.test.ts +114 -0
- package/src/__tests__/openai-provider.test.ts +105 -0
- package/src/__tests__/openai-responses-provider.test.ts +4 -4
- package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
- package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
- package/src/__tests__/platform.test.ts +0 -3
- package/src/__tests__/plugin-source-watcher.test.ts +302 -0
- package/src/__tests__/process-message-background-slack.test.ts +1 -51
- package/src/__tests__/process-message-display-content.test.ts +21 -16
- package/src/__tests__/server-history-render.test.ts +83 -4
- package/src/__tests__/steer-tool-repair.test.ts +249 -0
- package/src/__tests__/system-prompt.test.ts +51 -28
- package/src/__tests__/terminal-tools.test.ts +11 -1
- package/src/__tests__/thinking-block-replay.test.ts +113 -0
- package/src/__tests__/thread-backfill.test.ts +370 -22
- package/src/__tests__/tool-executor.test.ts +90 -1
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
- package/src/__tests__/twilio-routes.test.ts +1 -1
- package/src/__tests__/web-fetch.test.ts +2 -2
- package/src/__tests__/workspace-git-service.test.ts +88 -5
- package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
- package/src/agent/attachments.ts +1 -0
- package/src/agent/loop.ts +57 -20
- package/src/background-wake/next-wake.test.ts +289 -0
- package/src/background-wake/next-wake.ts +172 -0
- package/src/browser/operations.ts +15 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
- package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
- package/src/cli/commands/conversations.ts +128 -1
- package/src/cli/commands/inference-providers.ts +147 -1
- package/src/cli/commands/memory-v2.ts +308 -0
- package/src/cli/commands/notifications.ts +24 -2
- package/src/cli/utils/conversation-id.ts +17 -5
- package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
- package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
- package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
- package/src/config/bundled-skills/schedule/SKILL.md +8 -0
- package/src/config/bundled-tool-registry.ts +22 -12
- package/src/config/call-site-defaults.ts +19 -0
- package/src/config/feature-flag-registry.json +99 -3
- package/src/config/llm-resolver.ts +16 -2
- package/src/config/schemas/__tests__/memory-v2.test.ts +4 -0
- package/src/config/schemas/call-site-catalog.ts +21 -0
- package/src/config/schemas/llm.ts +3 -0
- package/src/config/schemas/memory-v2.ts +48 -1
- package/src/context/compactor.ts +8 -1
- package/src/context/token-estimator.ts +47 -4
- package/src/context/window-manager.ts +25 -0
- package/src/credential-health/credential-health-service.ts +34 -19
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
- package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
- package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +153 -23
- package/src/daemon/conversation-agent-loop.ts +223 -54
- package/src/daemon/conversation-lifecycle.ts +142 -116
- package/src/daemon/conversation-messaging.ts +3 -0
- package/src/daemon/conversation-process.ts +273 -0
- package/src/daemon/conversation-queue-manager.ts +14 -0
- package/src/daemon/conversation-runtime-assembly.ts +135 -75
- package/src/daemon/conversation-slash.ts +37 -5
- package/src/daemon/conversation-surfaces.ts +45 -2
- package/src/daemon/conversation-tool-setup.ts +7 -0
- package/src/daemon/conversation.ts +42 -5
- package/src/daemon/first-greeting.ts +10 -0
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
- package/src/daemon/handlers/config-a2a.ts +160 -0
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/conversations.ts +79 -0
- package/src/daemon/handlers/shared.ts +92 -29
- package/src/daemon/host-bash-proxy.ts +1 -1
- package/src/daemon/host-cu-proxy.ts +1 -1
- package/src/daemon/host-file-proxy.ts +1 -1
- package/src/daemon/host-transfer-proxy.ts +1 -1
- package/src/daemon/lifecycle.ts +18 -4
- package/src/daemon/message-protocol.ts +4 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/document-comments.ts +50 -0
- package/src/daemon/message-types/messages.ts +68 -1
- package/src/daemon/message-types/surfaces.ts +3 -1
- package/src/daemon/message-types/web-activity.ts +57 -0
- package/src/daemon/plugin-source-watcher.ts +135 -3
- package/src/daemon/process-message.ts +69 -12
- package/src/daemon/query-complexity-router.ts +75 -0
- package/src/daemon/trust-context.ts +6 -0
- package/src/documents/document-comments-store.test.ts +338 -0
- package/src/documents/document-comments-store.ts +237 -0
- package/src/documents/document-store.ts +202 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
- package/src/heartbeat/heartbeat-service.ts +1 -0
- package/src/home/__tests__/suggested-prompts.test.ts +33 -2
- package/src/home/feed-types.ts +6 -1
- package/src/home/home-content-refresh.ts +52 -0
- package/src/home/home-greeting-cache.ts +69 -0
- package/src/home/home-greeting.ts +94 -0
- package/src/home/suggested-prompts.ts +177 -9
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
- package/src/memory/__tests__/memory-retrospective-job.test.ts +320 -6
- package/src/memory/conversation-crud.ts +133 -43
- package/src/memory/db-init.ts +16 -0
- package/src/memory/delivery-crud.ts +41 -0
- package/src/memory/delivery-status.ts +141 -15
- package/src/memory/external-conversation-store.ts +32 -1
- package/src/memory/jobs-worker.ts +21 -1
- package/src/memory/memory-retrospective-constants.ts +28 -0
- package/src/memory/memory-retrospective-enqueue.ts +3 -2
- package/src/memory/memory-retrospective-job.ts +408 -18
- package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
- package/src/memory/memory-v2-activation-log-store.ts +26 -8
- package/src/memory/migrations/100-core-tables.ts +1 -0
- package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
- package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
- package/src/memory/migrations/253-document-comments.ts +47 -0
- package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
- package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
- package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
- package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
- package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
- package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
- package/src/memory/migrations/index.ts +17 -0
- package/src/memory/migrations/registry.ts +25 -0
- package/src/memory/onboarding-events-store.ts +7 -0
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/conversations.ts +3 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
- package/src/memory/v2/__tests__/injection.test.ts +31 -14
- package/src/memory/v2/__tests__/page-index.test.ts +365 -1
- package/src/memory/v2/__tests__/router.test.ts +489 -1
- package/src/memory/v2/consolidation-job.ts +14 -0
- package/src/memory/v2/injection-events.ts +101 -0
- package/src/memory/v2/injection.ts +21 -10
- package/src/memory/v2/page-index.ts +209 -7
- package/src/memory/v2/page-store.ts +18 -0
- package/src/memory/v2/router.ts +209 -55
- package/src/messaging/providers/index.ts +7 -1
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
- package/src/messaging/providers/slack/adapter.ts +178 -25
- package/src/messaging/providers/slack/api.test.ts +54 -0
- package/src/messaging/providers/slack/api.ts +119 -3
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/deep-link.ts +20 -1
- package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
- package/src/messaging/providers/slack/message-metadata.ts +156 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
- package/src/messaging/providers/slack/render-transcript.ts +176 -49
- package/src/messaging/providers/slack/send.test.ts +77 -0
- package/src/messaging/providers/slack/send.ts +8 -2
- package/src/messaging/providers/slack/types.ts +14 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
- package/src/notifications/conversation-seed-composer.ts +14 -2
- package/src/notifications/deferred-emit.ts +135 -0
- package/src/notifications/emit-signal.ts +9 -1
- package/src/notifications/home-feed-side-effect.ts +60 -30
- package/src/oauth/connect-orchestrator.ts +3 -0
- package/src/oauth/credential-token-resolver.ts +2 -0
- package/src/oauth/manual-token-connection.ts +19 -0
- package/src/oauth/oauth-store.ts +12 -0
- package/src/oauth/seed-providers.ts +22 -0
- package/src/permissions/prompter.ts +5 -2
- package/src/permissions/secret-prompter.ts +4 -1
- package/src/plugins/defaults/injectors.ts +82 -9
- package/src/prompts/__tests__/system-prompt.test.ts +46 -2
- package/src/prompts/normalize-onboarding.ts +40 -0
- package/src/prompts/sections.ts +32 -14
- package/src/prompts/system-prompt.ts +105 -68
- package/src/prompts/template-detection.ts +37 -0
- package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
- package/src/prompts/templates/BOOTSTRAP.md +8 -0
- package/src/prompts/templates/VOICE.md +3 -0
- package/src/prompts/templates/system-sections.ts +53 -3
- package/src/providers/anthropic/client.ts +132 -5
- package/src/providers/fireworks/client.ts +20 -2
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
- package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
- package/src/providers/inference/adapter-factory.ts +15 -1
- package/src/providers/inference/auth.ts +3 -3
- package/src/providers/inference/codex-token-refresh.ts +128 -0
- package/src/providers/inference/resolve-auth.ts +49 -6
- package/src/providers/model-catalog.ts +48 -1
- package/src/providers/openai/chat-completions-provider.ts +57 -20
- package/src/providers/openai/responses-provider.ts +9 -3
- package/src/providers/openrouter/client.ts +5 -1
- package/src/providers/types.ts +25 -0
- package/src/runtime/__tests__/agent-wake.test.ts +214 -0
- package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
- package/src/runtime/agent-wake.ts +151 -56
- package/src/runtime/auth/route-policy.ts +7 -3
- package/src/runtime/background-job-runner.ts +26 -0
- package/src/runtime/channel-reply-delivery.ts +182 -47
- package/src/runtime/channel-retry-sweep.ts +141 -16
- package/src/runtime/http-types.ts +7 -4
- package/src/runtime/pending-interactions.ts +51 -8
- package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +55 -1
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
- package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
- package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
- package/src/runtime/routes/approval-routes.ts +4 -1
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
- package/src/runtime/routes/content-source-routes.ts +78 -0
- package/src/runtime/routes/conversation-cli-routes.ts +146 -1
- package/src/runtime/routes/conversation-query-routes.ts +60 -1
- package/src/runtime/routes/conversation-routes.ts +281 -76
- package/src/runtime/routes/document-comments-routes.ts +287 -0
- package/src/runtime/routes/documents-routes.ts +33 -0
- package/src/runtime/routes/home-feed-routes.ts +6 -3
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-browser-routes.ts +8 -1
- package/src/runtime/routes/identity-routes.ts +21 -0
- package/src/runtime/routes/inbound-message-handler.ts +288 -58
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
- package/src/runtime/routes/index.ts +12 -4
- package/src/runtime/routes/inference-provider-connection-routes.ts +63 -7
- package/src/runtime/routes/integrations/a2a.ts +60 -1
- package/src/runtime/routes/log-export-routes.ts +39 -0
- package/src/runtime/routes/memory-v2-routes.ts +217 -0
- package/src/runtime/routes/notification-routes.ts +19 -2
- package/src/runtime/routes/question-routes.ts +4 -1
- package/src/runtime/routes/sanity-routes.ts +159 -0
- package/src/runtime/routes/slack-channel-routes.ts +187 -0
- package/src/runtime/services/conversation-serializer.ts +30 -4
- package/src/schedule/integration-status.ts +3 -1
- package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
- package/src/security/oauth2-device-code.ts +307 -0
- package/src/security/oauth2.ts +26 -9
- package/src/security/secure-keys.ts +5 -0
- package/src/skills/catalog-install.ts +6 -2
- package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
- package/src/tools/browser/browser-execution.ts +93 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
- package/src/tools/browser/cdp-client/factory.ts +87 -3
- package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
- package/src/tools/browser/cdp-client/types.ts +36 -0
- package/src/tools/browser/pinned-tabs.ts +90 -0
- package/src/tools/document/document-comment-tool.test.ts +379 -0
- package/src/tools/document/document-comment-tool.ts +156 -0
- package/src/tools/document/document-tool.ts +128 -2
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
- package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
- package/src/tools/network/domain-normalize.ts +17 -0
- package/src/tools/network/web-fetch.ts +213 -64
- package/src/tools/network/web-search.ts +191 -66
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/tools/tool-approval-handler.ts +19 -12
- package/src/tools/types.ts +4 -0
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/types/onboarding-context.ts +4 -0
- package/src/util/__tests__/favicon.test.ts +84 -0
- package/src/util/favicon.ts +40 -0
- package/src/util/platform.ts +0 -5
- package/src/workspace/git-service.ts +75 -4
- package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/config/bundled-skills/document/SKILL.md +0 -54
- package/src/config/bundled-skills/document/TOOLS.json +0 -106
- package/src/daemon/seed-files.ts +0 -18
- package/src/runtime/routes/interface-routes.ts +0 -43
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
3
|
+
|
|
4
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
5
|
+
|
|
6
|
+
import { migrateCreateProviderConnections } from "../../../memory/migrations/243-provider-connections.js";
|
|
7
|
+
import { migrateProviderConnectionStatusLabel } from "../../../memory/migrations/244-provider-connection-status-label.js";
|
|
8
|
+
import { migrateProviderConnectionBaseUrlAndModels } from "../../../memory/migrations/250-provider-connection-base-url-and-models.js";
|
|
9
|
+
import { migrateStripBaseUrlNonOpenaiCompatible } from "../../../memory/migrations/257-strip-base-url-non-openai-compatible.js";
|
|
10
|
+
import * as schema from "../../../memory/schema.js";
|
|
11
|
+
import { providerConnections } from "../../../memory/schema/inference.js";
|
|
12
|
+
import { getConnection } from "../connections.js";
|
|
13
|
+
import { resolveAuth } from "../resolve-auth.js";
|
|
14
|
+
|
|
15
|
+
function createTestDb() {
|
|
16
|
+
const sqlite = new Database(":memory:");
|
|
17
|
+
sqlite.exec("PRAGMA journal_mode=WAL");
|
|
18
|
+
return drizzle(sqlite, { schema });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function bootDb() {
|
|
22
|
+
const db = createTestDb();
|
|
23
|
+
migrateCreateProviderConnections(db);
|
|
24
|
+
migrateProviderConnectionStatusLabel(db);
|
|
25
|
+
migrateProviderConnectionBaseUrlAndModels(db);
|
|
26
|
+
return db;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Migration: strip base_url from non-openai-compatible connections
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
describe("migration 257: strip base_url from non-openai-compatible connections", () => {
|
|
34
|
+
test("clears base_url on anthropic connection", () => {
|
|
35
|
+
const db = bootDb();
|
|
36
|
+
|
|
37
|
+
// Manually insert a row with a base_url on an anthropic provider (simulating
|
|
38
|
+
// a row created before the validation was added).
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
db.insert(providerConnections)
|
|
41
|
+
.values({
|
|
42
|
+
name: "bad-anthropic",
|
|
43
|
+
provider: "anthropic",
|
|
44
|
+
auth: JSON.stringify({ type: "api_key", credential: "cred-abc" }),
|
|
45
|
+
status: "active",
|
|
46
|
+
baseUrl: "https://evil.example.com/v1",
|
|
47
|
+
createdAt: now,
|
|
48
|
+
updatedAt: now,
|
|
49
|
+
})
|
|
50
|
+
.run();
|
|
51
|
+
|
|
52
|
+
migrateStripBaseUrlNonOpenaiCompatible(db);
|
|
53
|
+
|
|
54
|
+
const conn = getConnection(db, "bad-anthropic");
|
|
55
|
+
expect(conn).not.toBeNull();
|
|
56
|
+
expect(conn!.baseUrl).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("preserves base_url on openai-compatible connection", () => {
|
|
60
|
+
const db = bootDb();
|
|
61
|
+
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
db.insert(providerConnections)
|
|
64
|
+
.values({
|
|
65
|
+
name: "good-vllm",
|
|
66
|
+
provider: "openai-compatible",
|
|
67
|
+
auth: JSON.stringify({ type: "api_key", credential: "cred-vllm" }),
|
|
68
|
+
status: "active",
|
|
69
|
+
baseUrl: "https://my-vllm.example.com/v1",
|
|
70
|
+
models: JSON.stringify([{ id: "my-model" }]),
|
|
71
|
+
createdAt: now,
|
|
72
|
+
updatedAt: now,
|
|
73
|
+
})
|
|
74
|
+
.run();
|
|
75
|
+
|
|
76
|
+
migrateStripBaseUrlNonOpenaiCompatible(db);
|
|
77
|
+
|
|
78
|
+
const conn = getConnection(db, "good-vllm");
|
|
79
|
+
expect(conn).not.toBeNull();
|
|
80
|
+
expect(conn!.baseUrl).toBe("https://my-vllm.example.com/v1");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("is idempotent", () => {
|
|
84
|
+
const db = bootDb();
|
|
85
|
+
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
db.insert(providerConnections)
|
|
88
|
+
.values({
|
|
89
|
+
name: "bad-openai",
|
|
90
|
+
provider: "openai",
|
|
91
|
+
auth: JSON.stringify({ type: "api_key", credential: "cred-abc" }),
|
|
92
|
+
status: "active",
|
|
93
|
+
baseUrl: "https://evil.example.com/v1",
|
|
94
|
+
createdAt: now,
|
|
95
|
+
updatedAt: now,
|
|
96
|
+
})
|
|
97
|
+
.run();
|
|
98
|
+
|
|
99
|
+
migrateStripBaseUrlNonOpenaiCompatible(db);
|
|
100
|
+
migrateStripBaseUrlNonOpenaiCompatible(db);
|
|
101
|
+
|
|
102
|
+
const conn = getConnection(db, "bad-openai");
|
|
103
|
+
expect(conn).not.toBeNull();
|
|
104
|
+
expect(conn!.baseUrl).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// resolveAuth defense-in-depth: strip baseUrl for non-openai-compatible
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
describe("resolveAuth defense-in-depth", () => {
|
|
113
|
+
// Mock the secure key store to return a predictable value.
|
|
114
|
+
mock.module("../../../security/secure-keys.js", () => ({
|
|
115
|
+
getSecureKeyAsync: async (credential: string) =>
|
|
116
|
+
credential === "cred-test" ? "sk-test-key" : null,
|
|
117
|
+
}));
|
|
118
|
+
|
|
119
|
+
// Mock the platform proxy context to avoid real platform calls.
|
|
120
|
+
mock.module("../../../providers/platform-proxy/context.js", () => ({
|
|
121
|
+
buildManagedBaseUrl: async () => null,
|
|
122
|
+
resolveManagedProxyContext: async () => ({
|
|
123
|
+
assistantApiKey: "platform-key",
|
|
124
|
+
}),
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
test("strips baseUrl for anthropic provider", async () => {
|
|
128
|
+
const result = await resolveAuth(
|
|
129
|
+
{ type: "api_key", credential: "cred-test" },
|
|
130
|
+
"anthropic",
|
|
131
|
+
{ baseUrl: "https://evil.example.com/v1" },
|
|
132
|
+
);
|
|
133
|
+
expect(result.ok).toBe(true);
|
|
134
|
+
if (result.ok) {
|
|
135
|
+
expect(result.resolved.kind).toBe("header");
|
|
136
|
+
if (result.resolved.kind === "header") {
|
|
137
|
+
expect(result.resolved.baseUrl).toBeUndefined();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("strips baseUrl for openai provider", async () => {
|
|
143
|
+
const result = await resolveAuth(
|
|
144
|
+
{ type: "api_key", credential: "cred-test" },
|
|
145
|
+
"openai",
|
|
146
|
+
{ baseUrl: "https://evil.example.com/v1" },
|
|
147
|
+
);
|
|
148
|
+
expect(result.ok).toBe(true);
|
|
149
|
+
if (result.ok && result.resolved.kind === "header") {
|
|
150
|
+
expect(result.resolved.baseUrl).toBeUndefined();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("strips baseUrl for gemini provider", async () => {
|
|
155
|
+
const result = await resolveAuth(
|
|
156
|
+
{ type: "api_key", credential: "cred-test" },
|
|
157
|
+
"gemini",
|
|
158
|
+
{ baseUrl: "https://evil.example.com/v1" },
|
|
159
|
+
);
|
|
160
|
+
expect(result.ok).toBe(true);
|
|
161
|
+
if (result.ok && result.resolved.kind === "header") {
|
|
162
|
+
expect(result.resolved.baseUrl).toBeUndefined();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("preserves baseUrl for openai-compatible provider", async () => {
|
|
167
|
+
const result = await resolveAuth(
|
|
168
|
+
{ type: "api_key", credential: "cred-test" },
|
|
169
|
+
"openai-compatible",
|
|
170
|
+
{ baseUrl: "https://my-vllm.example.com/v1" },
|
|
171
|
+
);
|
|
172
|
+
expect(result.ok).toBe(true);
|
|
173
|
+
if (result.ok && result.resolved.kind === "header") {
|
|
174
|
+
expect(result.resolved.baseUrl).toBe("https://my-vllm.example.com/v1");
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("handles null baseUrl gracefully for any provider", async () => {
|
|
179
|
+
const result = await resolveAuth(
|
|
180
|
+
{ type: "api_key", credential: "cred-test" },
|
|
181
|
+
"anthropic",
|
|
182
|
+
{ baseUrl: null },
|
|
183
|
+
);
|
|
184
|
+
expect(result.ok).toBe(true);
|
|
185
|
+
if (result.ok && result.resolved.kind === "header") {
|
|
186
|
+
expect(result.resolved.baseUrl).toBeUndefined();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterEach,
|
|
3
|
+
beforeEach,
|
|
4
|
+
describe,
|
|
5
|
+
expect,
|
|
6
|
+
mock,
|
|
7
|
+
test,
|
|
8
|
+
} from "bun:test";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Mocks
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
mock.module("../../../util/logger.js", () => ({
|
|
15
|
+
getLogger: () =>
|
|
16
|
+
new Proxy({} as Record<string, unknown>, {
|
|
17
|
+
get: () => () => {},
|
|
18
|
+
}),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const mockGetSecureKey = mock<(key: string) => Promise<string | undefined>>(
|
|
22
|
+
async () => undefined,
|
|
23
|
+
);
|
|
24
|
+
const mockSetSecureKey = mock<(key: string, value: string) => Promise<boolean>>(
|
|
25
|
+
async () => true,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
mock.module("../../../security/secure-keys.js", () => ({
|
|
29
|
+
getSecureKeyAsync: mockGetSecureKey,
|
|
30
|
+
setSecureKeyAsync: mockSetSecureKey,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
const mockRefreshOAuth2Token = mock<
|
|
34
|
+
(...args: unknown[]) => Promise<{
|
|
35
|
+
accessToken: string;
|
|
36
|
+
refreshToken?: string;
|
|
37
|
+
expiresIn?: number;
|
|
38
|
+
}>
|
|
39
|
+
>(async () => ({
|
|
40
|
+
accessToken: "new-access-token",
|
|
41
|
+
refreshToken: "new-refresh-token",
|
|
42
|
+
expiresIn: 3600,
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
mock.module("../../../security/oauth2.js", () => ({
|
|
46
|
+
refreshOAuth2Token: mockRefreshOAuth2Token,
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Import under test (after mocks)
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
import {
|
|
54
|
+
_resetRefreshMutex,
|
|
55
|
+
getValidCodexAccessToken,
|
|
56
|
+
} from "../codex-token-refresh.js";
|
|
57
|
+
|
|
58
|
+
const PREFIX = "credential/openai-codex";
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Helpers
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
function setSecureKeyMap(map: Record<string, string>): void {
|
|
65
|
+
mockGetSecureKey.mockImplementation(async (key: string) => map[key]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function futureTimestamp(secondsFromNow: number): string {
|
|
69
|
+
return String(Math.floor(Date.now() / 1000) + secondsFromNow);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function pastTimestamp(secondsAgo: number): string {
|
|
73
|
+
return String(Math.floor(Date.now() / 1000) - secondsAgo);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Tests
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
describe("getValidCodexAccessToken", () => {
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
mockGetSecureKey.mockReset();
|
|
83
|
+
mockSetSecureKey.mockReset();
|
|
84
|
+
mockRefreshOAuth2Token.mockReset();
|
|
85
|
+
_resetRefreshMutex();
|
|
86
|
+
|
|
87
|
+
mockSetSecureKey.mockImplementation(async () => true);
|
|
88
|
+
mockRefreshOAuth2Token.mockImplementation(async () => ({
|
|
89
|
+
accessToken: "new-access-token",
|
|
90
|
+
refreshToken: "new-refresh-token",
|
|
91
|
+
expiresIn: 3600,
|
|
92
|
+
}));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
_resetRefreshMutex();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("returns null when no access token is stored", async () => {
|
|
100
|
+
setSecureKeyMap({});
|
|
101
|
+
const result = await getValidCodexAccessToken(PREFIX);
|
|
102
|
+
expect(result).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("returns access token when no expires_at is stored (graceful degradation)", async () => {
|
|
106
|
+
setSecureKeyMap({
|
|
107
|
+
[`${PREFIX}/access_token`]: "my-token",
|
|
108
|
+
});
|
|
109
|
+
const result = await getValidCodexAccessToken(PREFIX);
|
|
110
|
+
expect(result).toBe("my-token");
|
|
111
|
+
expect(mockRefreshOAuth2Token).not.toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("returns access token when not expired", async () => {
|
|
115
|
+
setSecureKeyMap({
|
|
116
|
+
[`${PREFIX}/access_token`]: "my-token",
|
|
117
|
+
[`${PREFIX}/expires_at`]: futureTimestamp(600), // 10 minutes from now
|
|
118
|
+
});
|
|
119
|
+
const result = await getValidCodexAccessToken(PREFIX);
|
|
120
|
+
expect(result).toBe("my-token");
|
|
121
|
+
expect(mockRefreshOAuth2Token).not.toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("refreshes token when expired", async () => {
|
|
125
|
+
const keys: Record<string, string> = {
|
|
126
|
+
[`${PREFIX}/access_token`]: "old-token",
|
|
127
|
+
[`${PREFIX}/refresh_token`]: "old-refresh",
|
|
128
|
+
[`${PREFIX}/expires_at`]: pastTimestamp(60), // expired 1 minute ago
|
|
129
|
+
};
|
|
130
|
+
setSecureKeyMap(keys);
|
|
131
|
+
|
|
132
|
+
const result = await getValidCodexAccessToken(PREFIX);
|
|
133
|
+
|
|
134
|
+
expect(result).toBe("new-access-token");
|
|
135
|
+
expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
|
|
136
|
+
expect(mockRefreshOAuth2Token).toHaveBeenCalledWith(
|
|
137
|
+
"https://auth.openai.com/oauth/token",
|
|
138
|
+
"app_EMoamEEZ73f0CkXaXp7hrann",
|
|
139
|
+
"old-refresh",
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Verify new tokens are stored
|
|
143
|
+
expect(mockSetSecureKey).toHaveBeenCalledWith(
|
|
144
|
+
`${PREFIX}/access_token`,
|
|
145
|
+
"new-access-token",
|
|
146
|
+
);
|
|
147
|
+
expect(mockSetSecureKey).toHaveBeenCalledWith(
|
|
148
|
+
`${PREFIX}/refresh_token`,
|
|
149
|
+
"new-refresh-token",
|
|
150
|
+
);
|
|
151
|
+
// expires_at should be stored as well
|
|
152
|
+
const expiresAtCall = mockSetSecureKey.mock.calls.find(
|
|
153
|
+
(c) => c[0] === `${PREFIX}/expires_at`,
|
|
154
|
+
);
|
|
155
|
+
expect(expiresAtCall).toBeDefined();
|
|
156
|
+
const storedExpiresAt = Number(expiresAtCall![1]);
|
|
157
|
+
const now = Math.floor(Date.now() / 1000);
|
|
158
|
+
// Should be approximately now + 3600 (within 5 seconds tolerance)
|
|
159
|
+
expect(storedExpiresAt).toBeGreaterThan(now + 3590);
|
|
160
|
+
expect(storedExpiresAt).toBeLessThanOrEqual(now + 3610);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("refreshes token when about to expire (within 5-minute margin)", async () => {
|
|
164
|
+
setSecureKeyMap({
|
|
165
|
+
[`${PREFIX}/access_token`]: "old-token",
|
|
166
|
+
[`${PREFIX}/refresh_token`]: "old-refresh",
|
|
167
|
+
[`${PREFIX}/expires_at`]: futureTimestamp(60), // only 1 minute left
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const result = await getValidCodexAccessToken(PREFIX);
|
|
171
|
+
|
|
172
|
+
expect(result).toBe("new-access-token");
|
|
173
|
+
expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("concurrent refresh calls are deduplicated (mutex)", async () => {
|
|
177
|
+
setSecureKeyMap({
|
|
178
|
+
[`${PREFIX}/access_token`]: "old-token",
|
|
179
|
+
[`${PREFIX}/refresh_token`]: "old-refresh",
|
|
180
|
+
[`${PREFIX}/expires_at`]: pastTimestamp(60),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Use a deferred promise to control when the refresh completes.
|
|
184
|
+
// We create it upfront so the mock captures it synchronously.
|
|
185
|
+
let resolveRefresh!: (v: {
|
|
186
|
+
accessToken: string;
|
|
187
|
+
refreshToken: string;
|
|
188
|
+
expiresIn: number;
|
|
189
|
+
}) => void;
|
|
190
|
+
const refreshPromise = new Promise<{
|
|
191
|
+
accessToken: string;
|
|
192
|
+
refreshToken: string;
|
|
193
|
+
expiresIn: number;
|
|
194
|
+
}>((resolve) => {
|
|
195
|
+
resolveRefresh = resolve;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
mockRefreshOAuth2Token.mockImplementation(() => refreshPromise);
|
|
199
|
+
|
|
200
|
+
// Fire two concurrent refreshes
|
|
201
|
+
const p1 = getValidCodexAccessToken(PREFIX);
|
|
202
|
+
const p2 = getValidCodexAccessToken(PREFIX);
|
|
203
|
+
|
|
204
|
+
// Allow the async get-key calls to settle before resolving refresh
|
|
205
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
206
|
+
|
|
207
|
+
// Resolve the single in-flight refresh
|
|
208
|
+
resolveRefresh({
|
|
209
|
+
accessToken: "shared-new-token",
|
|
210
|
+
refreshToken: "shared-new-refresh",
|
|
211
|
+
expiresIn: 3600,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const [r1, r2] = await Promise.all([p1, p2]);
|
|
215
|
+
|
|
216
|
+
expect(r1).toBe("shared-new-token");
|
|
217
|
+
expect(r2).toBe("shared-new-token");
|
|
218
|
+
// Only one refresh call should have been made
|
|
219
|
+
expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("falls back to existing token when refresh fails", async () => {
|
|
223
|
+
setSecureKeyMap({
|
|
224
|
+
[`${PREFIX}/access_token`]: "old-token",
|
|
225
|
+
[`${PREFIX}/refresh_token`]: "old-refresh",
|
|
226
|
+
[`${PREFIX}/expires_at`]: pastTimestamp(60),
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
mockRefreshOAuth2Token.mockImplementation(async () => {
|
|
230
|
+
throw new Error("OAuth2 token refresh failed (HTTP 400: invalid_grant)");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const result = await getValidCodexAccessToken(PREFIX);
|
|
234
|
+
|
|
235
|
+
// Should fall back to the existing access token
|
|
236
|
+
expect(result).toBe("old-token");
|
|
237
|
+
expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("falls back to existing token when no refresh token available", async () => {
|
|
241
|
+
setSecureKeyMap({
|
|
242
|
+
[`${PREFIX}/access_token`]: "old-token",
|
|
243
|
+
// no refresh_token
|
|
244
|
+
[`${PREFIX}/expires_at`]: pastTimestamp(60),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const result = await getValidCodexAccessToken(PREFIX);
|
|
248
|
+
|
|
249
|
+
// Should return the existing access token
|
|
250
|
+
expect(result).toBe("old-token");
|
|
251
|
+
// Should not attempt a refresh
|
|
252
|
+
expect(mockRefreshOAuth2Token).not.toHaveBeenCalled();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
@@ -41,6 +41,8 @@ export interface AdapterCreateOpts {
|
|
|
41
41
|
baseURL?: string;
|
|
42
42
|
/** Forwarded to providers that wire native provider-side web search. */
|
|
43
43
|
useNativeWebSearch: boolean;
|
|
44
|
+
/** When true, the OpenAI adapter targets the Codex subscription endpoint. */
|
|
45
|
+
codexSubscription?: boolean;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
type AdapterFactory = (opts: AdapterCreateOpts) => Provider;
|
|
@@ -65,10 +67,18 @@ const ADAPTER_FACTORIES: Record<string, AdapterFactory> = {
|
|
|
65
67
|
streamTimeoutMs,
|
|
66
68
|
...(baseURL ? { baseURL } : {}),
|
|
67
69
|
}),
|
|
68
|
-
openai: ({
|
|
70
|
+
openai: ({
|
|
71
|
+
apiKey,
|
|
72
|
+
model,
|
|
73
|
+
streamTimeoutMs,
|
|
74
|
+
baseURL,
|
|
75
|
+
useNativeWebSearch,
|
|
76
|
+
codexSubscription,
|
|
77
|
+
}) =>
|
|
69
78
|
new OpenAIResponsesProvider(apiKey, model, {
|
|
70
79
|
useNativeWebSearch,
|
|
71
80
|
streamTimeoutMs,
|
|
81
|
+
codexSubscription,
|
|
72
82
|
...(baseURL ? { baseURL } : {}),
|
|
73
83
|
}),
|
|
74
84
|
gemini: ({ apiKey, model, streamTimeoutMs, baseURL }) =>
|
|
@@ -176,12 +186,16 @@ export function createAdapterFromConnection(
|
|
|
176
186
|
const baseURL =
|
|
177
187
|
resolvedAuth.kind === "header" ? resolvedAuth.baseUrl : undefined;
|
|
178
188
|
|
|
189
|
+
const codexSubscription =
|
|
190
|
+
connection.auth.type === "oauth_subscription" && provider === "openai";
|
|
191
|
+
|
|
179
192
|
const adapter = buildProviderAdapter(provider, {
|
|
180
193
|
apiKey,
|
|
181
194
|
model: opts.model,
|
|
182
195
|
streamTimeoutMs: opts.streamTimeoutMs ?? 1_800_000,
|
|
183
196
|
baseURL,
|
|
184
197
|
useNativeWebSearch: opts.useNativeWebSearch ?? false,
|
|
198
|
+
codexSubscription,
|
|
185
199
|
});
|
|
186
200
|
if (!adapter) return null;
|
|
187
201
|
|
|
@@ -9,13 +9,13 @@ import { PROVIDER_CATALOG } from "../model-catalog.js";
|
|
|
9
9
|
/**
|
|
10
10
|
* Auth configuration stored in the `provider_connections` table.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* Runtime-supported variants:
|
|
13
13
|
* - api_key: look up `credential` in vault, inject as bearer/provider header.
|
|
14
14
|
* - platform: route via Vellum managed proxy; no client-side credential.
|
|
15
15
|
* - none: no auth (e.g. Ollama running locally).
|
|
16
|
+
* - oauth_subscription: OAuth-based subscription auth (e.g. ChatGPT Codex).
|
|
16
17
|
*
|
|
17
|
-
*
|
|
18
|
-
* - oauth_subscription: OAuth-based subscription auth.
|
|
18
|
+
* Schema-accepted variants (runtime rejects with a clear "not yet shipped" error):
|
|
19
19
|
* - service_account: service-account credentials (Vertex AI, Bedrock).
|
|
20
20
|
*/
|
|
21
21
|
export const AuthSchema = z.discriminatedUnion("type", [
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic token refresh for ChatGPT Codex OAuth (subscription auth).
|
|
3
|
+
*
|
|
4
|
+
* OpenAI rotates refresh tokens on every use — concurrent refreshes will
|
|
5
|
+
* invalidate one token. A module-level mutex prevents this race.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { refreshOAuth2Token } from "../../security/oauth2.js";
|
|
9
|
+
import {
|
|
10
|
+
getSecureKeyAsync,
|
|
11
|
+
setSecureKeyAsync,
|
|
12
|
+
} from "../../security/secure-keys.js";
|
|
13
|
+
import { getLogger } from "../../util/logger.js";
|
|
14
|
+
|
|
15
|
+
const log = getLogger("codex-token-refresh");
|
|
16
|
+
|
|
17
|
+
const CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
18
|
+
const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
19
|
+
|
|
20
|
+
/** Refresh 5 minutes before expiry to avoid using a nearly-expired token. */
|
|
21
|
+
const REFRESH_MARGIN_SECONDS = 300;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Module-level mutex to prevent concurrent refresh races.
|
|
25
|
+
* OpenAI rotates refresh tokens on every use — two concurrent refreshes
|
|
26
|
+
* will invalidate one token.
|
|
27
|
+
*/
|
|
28
|
+
let refreshInFlight: Promise<string | null> | null = null;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Return a valid Codex access token, refreshing transparently if expired.
|
|
32
|
+
*
|
|
33
|
+
* @param credentialPrefix - Credential key prefix, e.g. `"credential/chatgpt"`.
|
|
34
|
+
* The function reads `<prefix>/access_token`, `<prefix>/refresh_token`,
|
|
35
|
+
* and `<prefix>/expires_at` from the credential store.
|
|
36
|
+
* @returns The access token string, or `null` if no token is stored.
|
|
37
|
+
*/
|
|
38
|
+
export async function getValidCodexAccessToken(
|
|
39
|
+
credentialPrefix: string,
|
|
40
|
+
): Promise<string | null> {
|
|
41
|
+
const accessToken = await getSecureKeyAsync(
|
|
42
|
+
`${credentialPrefix}/access_token`,
|
|
43
|
+
);
|
|
44
|
+
if (!accessToken) return null;
|
|
45
|
+
|
|
46
|
+
const expiresAtStr = await getSecureKeyAsync(
|
|
47
|
+
`${credentialPrefix}/expires_at`,
|
|
48
|
+
);
|
|
49
|
+
if (!expiresAtStr) return accessToken; // no expiry info — use token as-is
|
|
50
|
+
|
|
51
|
+
const expiresAt = Number(expiresAtStr);
|
|
52
|
+
const now = Date.now() / 1000;
|
|
53
|
+
|
|
54
|
+
if (now < expiresAt - REFRESH_MARGIN_SECONDS) {
|
|
55
|
+
return accessToken; // token is still fresh
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Token is expired or about to expire — refresh it.
|
|
59
|
+
// Use mutex to prevent concurrent refresh races.
|
|
60
|
+
if (refreshInFlight) {
|
|
61
|
+
return await refreshInFlight;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
refreshInFlight = doRefresh(credentialPrefix);
|
|
65
|
+
try {
|
|
66
|
+
return await refreshInFlight;
|
|
67
|
+
} finally {
|
|
68
|
+
refreshInFlight = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function doRefresh(credentialPrefix: string): Promise<string | null> {
|
|
73
|
+
const refreshToken = await getSecureKeyAsync(
|
|
74
|
+
`${credentialPrefix}/refresh_token`,
|
|
75
|
+
);
|
|
76
|
+
if (!refreshToken) {
|
|
77
|
+
log.warn("No refresh token available for Codex OAuth");
|
|
78
|
+
// Return the existing access token — it might still work even if expired
|
|
79
|
+
return (
|
|
80
|
+
(await getSecureKeyAsync(`${credentialPrefix}/access_token`)) ?? null
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const result = await refreshOAuth2Token(
|
|
86
|
+
CODEX_TOKEN_URL,
|
|
87
|
+
CODEX_CLIENT_ID,
|
|
88
|
+
refreshToken,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Store the new tokens
|
|
92
|
+
await setSecureKeyAsync(
|
|
93
|
+
`${credentialPrefix}/access_token`,
|
|
94
|
+
result.accessToken,
|
|
95
|
+
);
|
|
96
|
+
if (result.refreshToken) {
|
|
97
|
+
await setSecureKeyAsync(
|
|
98
|
+
`${credentialPrefix}/refresh_token`,
|
|
99
|
+
result.refreshToken,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (result.expiresIn) {
|
|
103
|
+
const newExpiresAt = Math.floor(Date.now() / 1000 + result.expiresIn);
|
|
104
|
+
await setSecureKeyAsync(
|
|
105
|
+
`${credentialPrefix}/expires_at`,
|
|
106
|
+
String(newExpiresAt),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
log.info("Codex OAuth token refreshed successfully");
|
|
111
|
+
return result.accessToken;
|
|
112
|
+
} catch (err) {
|
|
113
|
+
log.error({ err }, "Codex OAuth token refresh failed");
|
|
114
|
+
// Return the existing access token as fallback
|
|
115
|
+
return (
|
|
116
|
+
(await getSecureKeyAsync(`${credentialPrefix}/access_token`)) ?? null
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Test helpers
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
/** @internal Test-only: reset the in-flight refresh mutex. */
|
|
126
|
+
export function _resetRefreshMutex(): void {
|
|
127
|
+
refreshInFlight = null;
|
|
128
|
+
}
|