@gajae-code/ai 0.1.1
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/CHANGELOG.md +2644 -0
- package/README.md +1181 -0
- package/dist/types/api-registry.d.ts +30 -0
- package/dist/types/auth-broker/client.d.ts +66 -0
- package/dist/types/auth-broker/index.d.ts +5 -0
- package/dist/types/auth-broker/refresher.d.ts +25 -0
- package/dist/types/auth-broker/remote-store.d.ts +96 -0
- package/dist/types/auth-broker/server.d.ts +32 -0
- package/dist/types/auth-broker/types.d.ts +105 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +412 -0
- package/dist/types/auth-gateway/http.d.ts +39 -0
- package/dist/types/auth-gateway/index.d.ts +3 -0
- package/dist/types/auth-gateway/server.d.ts +17 -0
- package/dist/types/auth-gateway/types.d.ts +115 -0
- package/dist/types/auth-storage.d.ts +641 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/index.d.ts +49 -0
- package/dist/types/model-cache.d.ts +17 -0
- package/dist/types/model-manager.d.ts +62 -0
- package/dist/types/model-thinking.d.ts +71 -0
- package/dist/types/models.d.ts +12 -0
- package/dist/types/provider-details.d.ts +24 -0
- package/dist/types/provider-models/bundled-references.d.ts +4 -0
- package/dist/types/provider-models/descriptors.d.ts +48 -0
- package/dist/types/provider-models/google.d.ts +20 -0
- package/dist/types/provider-models/index.d.ts +5 -0
- package/dist/types/provider-models/ollama.d.ts +7 -0
- package/dist/types/provider-models/openai-compat.d.ts +237 -0
- package/dist/types/provider-models/special.d.ts +16 -0
- package/dist/types/providers/amazon-bedrock.d.ts +36 -0
- package/dist/types/providers/anthropic-messages-server-schema.d.ts +450 -0
- package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
- package/dist/types/providers/anthropic.d.ts +188 -0
- package/dist/types/providers/aws-credentials.d.ts +43 -0
- package/dist/types/providers/aws-eventstream.d.ts +38 -0
- package/dist/types/providers/aws-sigv4.d.ts +55 -0
- package/dist/types/providers/azure-openai-responses.d.ts +15 -0
- package/dist/types/providers/cursor/gen/agent_pb.d.ts +13022 -0
- package/dist/types/providers/cursor.d.ts +42 -0
- package/dist/types/providers/error-message.d.ts +27 -0
- package/dist/types/providers/github-copilot-headers.d.ts +40 -0
- package/dist/types/providers/gitlab-duo.d.ts +27 -0
- package/dist/types/providers/google-auth.d.ts +24 -0
- package/dist/types/providers/google-gemini-cli.d.ts +72 -0
- package/dist/types/providers/google-gemini-headers.d.ts +18 -0
- package/dist/types/providers/google-shared.d.ts +163 -0
- package/dist/types/providers/google-types.d.ts +138 -0
- package/dist/types/providers/google-vertex.d.ts +7 -0
- package/dist/types/providers/google.d.ts +4 -0
- package/dist/types/providers/grammar.d.ts +1 -0
- package/dist/types/providers/kimi.d.ts +27 -0
- package/dist/types/providers/mock.d.ts +175 -0
- package/dist/types/providers/ollama.d.ts +6 -0
- package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +814 -0
- package/dist/types/providers/openai-chat-server.d.ts +16 -0
- package/dist/types/providers/openai-codex/constants.d.ts +26 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +49 -0
- package/dist/types/providers/openai-codex/response-handler.d.ts +17 -0
- package/dist/types/providers/openai-codex-responses.d.ts +67 -0
- package/dist/types/providers/openai-completions-compat.d.ts +25 -0
- package/dist/types/providers/openai-completions.d.ts +33 -0
- package/dist/types/providers/openai-responses-server-schema.d.ts +392 -0
- package/dist/types/providers/openai-responses-server.d.ts +17 -0
- package/dist/types/providers/openai-responses-shared.d.ts +89 -0
- package/dist/types/providers/openai-responses.d.ts +32 -0
- package/dist/types/providers/pi-native-client.d.ts +13 -0
- package/dist/types/providers/pi-native-server.d.ts +68 -0
- package/dist/types/providers/register-builtins.d.ts +31 -0
- package/dist/types/providers/synthetic.d.ts +26 -0
- package/dist/types/providers/transform-messages.d.ts +12 -0
- package/dist/types/providers/vision-guard.d.ts +8 -0
- package/dist/types/rate-limit-utils.d.ts +19 -0
- package/dist/types/stream.d.ts +24 -0
- package/dist/types/types.d.ts +746 -0
- package/dist/types/usage/claude.d.ts +3 -0
- package/dist/types/usage/gemini.d.ts +2 -0
- package/dist/types/usage/github-copilot.d.ts +7 -0
- package/dist/types/usage/google-antigravity.d.ts +2 -0
- package/dist/types/usage/kimi.d.ts +2 -0
- package/dist/types/usage/minimax-code.d.ts +2 -0
- package/dist/types/usage/openai-codex.d.ts +3 -0
- package/dist/types/usage/shared.d.ts +1 -0
- package/dist/types/usage/zai.d.ts +2 -0
- package/dist/types/usage.d.ts +258 -0
- package/dist/types/utils/abort.d.ts +19 -0
- package/dist/types/utils/anthropic-auth.d.ts +31 -0
- package/dist/types/utils/discovery/antigravity.d.ts +61 -0
- package/dist/types/utils/discovery/codex.d.ts +38 -0
- package/dist/types/utils/discovery/cursor.d.ts +23 -0
- package/dist/types/utils/discovery/gemini.d.ts +25 -0
- package/dist/types/utils/discovery/index.d.ts +4 -0
- package/dist/types/utils/discovery/openai-compatible.d.ts +72 -0
- package/dist/types/utils/event-stream.d.ts +28 -0
- package/dist/types/utils/fireworks-model-id.d.ts +10 -0
- package/dist/types/utils/foundry.d.ts +1 -0
- package/dist/types/utils/h2-fetch.d.ts +22 -0
- package/dist/types/utils/http-inspector.d.ts +31 -0
- package/dist/types/utils/idle-iterator.d.ts +67 -0
- package/dist/types/utils/json-parse.d.ts +10 -0
- package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +18 -0
- package/dist/types/utils/oauth/anthropic.d.ts +22 -0
- package/dist/types/utils/oauth/api-key-login.d.ts +35 -0
- package/dist/types/utils/oauth/api-key-validation.d.ts +27 -0
- package/dist/types/utils/oauth/callback-server.d.ts +57 -0
- package/dist/types/utils/oauth/cerebras.d.ts +1 -0
- package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +18 -0
- package/dist/types/utils/oauth/cursor.d.ts +15 -0
- package/dist/types/utils/oauth/deepseek.d.ts +10 -0
- package/dist/types/utils/oauth/firepass.d.ts +1 -0
- package/dist/types/utils/oauth/fireworks.d.ts +1 -0
- package/dist/types/utils/oauth/github-copilot.d.ts +38 -0
- package/dist/types/utils/oauth/gitlab-duo.d.ts +3 -0
- package/dist/types/utils/oauth/google-antigravity.d.ts +11 -0
- package/dist/types/utils/oauth/google-gemini-cli.d.ts +10 -0
- package/dist/types/utils/oauth/google-oauth-shared.d.ts +28 -0
- package/dist/types/utils/oauth/huggingface.d.ts +19 -0
- package/dist/types/utils/oauth/index.d.ts +38 -0
- package/dist/types/utils/oauth/kagi.d.ts +17 -0
- package/dist/types/utils/oauth/kilo.d.ts +5 -0
- package/dist/types/utils/oauth/kimi.d.ts +21 -0
- package/dist/types/utils/oauth/litellm.d.ts +18 -0
- package/dist/types/utils/oauth/lm-studio.d.ts +17 -0
- package/dist/types/utils/oauth/minimax-code.d.ts +28 -0
- package/dist/types/utils/oauth/moonshot.d.ts +1 -0
- package/dist/types/utils/oauth/nanogpt.d.ts +1 -0
- package/dist/types/utils/oauth/nvidia.d.ts +18 -0
- package/dist/types/utils/oauth/ollama-cloud.d.ts +2 -0
- package/dist/types/utils/oauth/ollama.d.ts +18 -0
- package/dist/types/utils/oauth/openai-codex.d.ts +21 -0
- package/dist/types/utils/oauth/opencode.d.ts +18 -0
- package/dist/types/utils/oauth/parallel.d.ts +17 -0
- package/dist/types/utils/oauth/perplexity.d.ts +9 -0
- package/dist/types/utils/oauth/pkce.d.ts +8 -0
- package/dist/types/utils/oauth/qianfan.d.ts +17 -0
- package/dist/types/utils/oauth/qwen-portal.d.ts +19 -0
- package/dist/types/utils/oauth/synthetic.d.ts +1 -0
- package/dist/types/utils/oauth/tavily.d.ts +17 -0
- package/dist/types/utils/oauth/together.d.ts +1 -0
- package/dist/types/utils/oauth/types.d.ts +44 -0
- package/dist/types/utils/oauth/venice.d.ts +18 -0
- package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +18 -0
- package/dist/types/utils/oauth/vllm.d.ts +16 -0
- package/dist/types/utils/oauth/xiaomi.d.ts +19 -0
- package/dist/types/utils/oauth/zai.d.ts +18 -0
- package/dist/types/utils/oauth/zenmux.d.ts +1 -0
- package/dist/types/utils/overflow.d.ts +54 -0
- package/dist/types/utils/parse-bind.d.ts +23 -0
- package/dist/types/utils/provider-response.d.ts +3 -0
- package/dist/types/utils/retry-after.d.ts +3 -0
- package/dist/types/utils/retry.d.ts +26 -0
- package/dist/types/utils/schema/adapt.d.ts +24 -0
- package/dist/types/utils/schema/compatibility.d.ts +30 -0
- package/dist/types/utils/schema/dereference.d.ts +11 -0
- package/dist/types/utils/schema/draft.d.ts +10 -0
- package/dist/types/utils/schema/equality.d.ts +4 -0
- package/dist/types/utils/schema/fields.d.ts +49 -0
- package/dist/types/utils/schema/index.d.ts +13 -0
- package/dist/types/utils/schema/json-schema-validator.d.ts +12 -0
- package/dist/types/utils/schema/meta-validator.d.ts +2 -0
- package/dist/types/utils/schema/normalize.d.ts +93 -0
- package/dist/types/utils/schema/spill.d.ts +8 -0
- package/dist/types/utils/schema/stamps.d.ts +25 -0
- package/dist/types/utils/schema/types.d.ts +4 -0
- package/dist/types/utils/schema/wire.d.ts +54 -0
- package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
- package/dist/types/utils/sse-debug.d.ts +10 -0
- package/dist/types/utils/tool-call-healing.d.ts +71 -0
- package/dist/types/utils/tool-choice.d.ts +50 -0
- package/dist/types/utils/validation.d.ts +17 -0
- package/dist/types/utils.d.ts +28 -0
- package/package.json +146 -0
- package/src/api-registry.ts +96 -0
- package/src/auth-broker/client.ts +358 -0
- package/src/auth-broker/index.ts +5 -0
- package/src/auth-broker/refresher.ts +127 -0
- package/src/auth-broker/remote-store.ts +623 -0
- package/src/auth-broker/server.ts +644 -0
- package/src/auth-broker/types.ts +127 -0
- package/src/auth-broker/wire-schemas.ts +200 -0
- package/src/auth-gateway/http.ts +194 -0
- package/src/auth-gateway/index.ts +3 -0
- package/src/auth-gateway/server.ts +717 -0
- package/src/auth-gateway/types.ts +134 -0
- package/src/auth-storage.ts +4104 -0
- package/src/cli.ts +262 -0
- package/src/index.ts +54 -0
- package/src/model-cache.ts +129 -0
- package/src/model-manager.ts +450 -0
- package/src/model-thinking.ts +691 -0
- package/src/models.json +73853 -0
- package/src/models.json.d.ts +9 -0
- package/src/models.ts +56 -0
- package/src/prompts/turn-aborted-guidance.md +4 -0
- package/src/provider-details.ts +90 -0
- package/src/provider-models/bundled-references.ts +38 -0
- package/src/provider-models/descriptors.ts +308 -0
- package/src/provider-models/google.ts +91 -0
- package/src/provider-models/index.ts +5 -0
- package/src/provider-models/ollama.ts +153 -0
- package/src/provider-models/openai-compat.ts +2275 -0
- package/src/provider-models/special.ts +67 -0
- package/src/providers/amazon-bedrock.ts +849 -0
- package/src/providers/anthropic-messages-server-schema.ts +229 -0
- package/src/providers/anthropic-messages-server.ts +677 -0
- package/src/providers/anthropic.ts +2696 -0
- package/src/providers/aws-credentials.ts +501 -0
- package/src/providers/aws-eventstream.ts +185 -0
- package/src/providers/aws-sigv4.ts +218 -0
- package/src/providers/azure-openai-responses.ts +337 -0
- package/src/providers/cursor/gen/agent_pb.ts +15274 -0
- package/src/providers/cursor/proto/agent.proto +3526 -0
- package/src/providers/cursor/proto/buf.gen.yaml +6 -0
- package/src/providers/cursor/proto/buf.yaml +17 -0
- package/src/providers/cursor.ts +2561 -0
- package/src/providers/error-message.ts +21 -0
- package/src/providers/github-copilot-headers.ts +140 -0
- package/src/providers/gitlab-duo.ts +372 -0
- package/src/providers/google-auth.ts +252 -0
- package/src/providers/google-gemini-cli.ts +795 -0
- package/src/providers/google-gemini-headers.ts +41 -0
- package/src/providers/google-shared.ts +902 -0
- package/src/providers/google-types.ts +167 -0
- package/src/providers/google-vertex.ts +88 -0
- package/src/providers/google.ts +41 -0
- package/src/providers/grammar.ts +70 -0
- package/src/providers/kimi.ts +52 -0
- package/src/providers/mock.ts +500 -0
- package/src/providers/ollama.ts +544 -0
- package/src/providers/openai-anthropic-shim.ts +138 -0
- package/src/providers/openai-chat-server-schema.ts +243 -0
- package/src/providers/openai-chat-server.ts +628 -0
- package/src/providers/openai-codex/constants.ts +43 -0
- package/src/providers/openai-codex/request-transformer.ts +161 -0
- package/src/providers/openai-codex/response-handler.ts +81 -0
- package/src/providers/openai-codex-responses.ts +2598 -0
- package/src/providers/openai-completions-compat.ts +279 -0
- package/src/providers/openai-completions.ts +1853 -0
- package/src/providers/openai-responses-server-schema.ts +290 -0
- package/src/providers/openai-responses-server.ts +1183 -0
- package/src/providers/openai-responses-shared.ts +800 -0
- package/src/providers/openai-responses.ts +621 -0
- package/src/providers/pi-native-client.ts +228 -0
- package/src/providers/pi-native-server.ts +210 -0
- package/src/providers/register-builtins.ts +412 -0
- package/src/providers/synthetic.ts +50 -0
- package/src/providers/transform-messages.ts +309 -0
- package/src/providers/vision-guard.ts +31 -0
- package/src/rate-limit-utils.ts +84 -0
- package/src/stream.ts +895 -0
- package/src/types.ts +884 -0
- package/src/usage/claude.ts +431 -0
- package/src/usage/gemini.ts +250 -0
- package/src/usage/github-copilot.ts +421 -0
- package/src/usage/google-antigravity.ts +201 -0
- package/src/usage/kimi.ts +271 -0
- package/src/usage/minimax-code.ts +31 -0
- package/src/usage/openai-codex.ts +503 -0
- package/src/usage/shared.ts +10 -0
- package/src/usage/zai.ts +247 -0
- package/src/usage.ts +183 -0
- package/src/utils/abort.ts +51 -0
- package/src/utils/anthropic-auth.ts +87 -0
- package/src/utils/discovery/antigravity.ts +261 -0
- package/src/utils/discovery/codex.ts +371 -0
- package/src/utils/discovery/cursor.ts +306 -0
- package/src/utils/discovery/gemini.ts +248 -0
- package/src/utils/discovery/index.ts +4 -0
- package/src/utils/discovery/openai-compatible.ts +224 -0
- package/src/utils/event-stream.ts +142 -0
- package/src/utils/fireworks-model-id.ts +30 -0
- package/src/utils/foundry.ts +8 -0
- package/src/utils/h2-fetch.ts +60 -0
- package/src/utils/http-inspector.ts +176 -0
- package/src/utils/idle-iterator.ts +250 -0
- package/src/utils/json-parse.ts +148 -0
- package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
- package/src/utils/oauth/anthropic.ts +200 -0
- package/src/utils/oauth/api-key-login.ts +87 -0
- package/src/utils/oauth/api-key-validation.ts +92 -0
- package/src/utils/oauth/callback-server.ts +276 -0
- package/src/utils/oauth/cerebras.ts +16 -0
- package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
- package/src/utils/oauth/cursor.ts +157 -0
- package/src/utils/oauth/deepseek.ts +53 -0
- package/src/utils/oauth/firepass.ts +24 -0
- package/src/utils/oauth/fireworks.ts +15 -0
- package/src/utils/oauth/github-copilot.ts +362 -0
- package/src/utils/oauth/gitlab-duo.ts +123 -0
- package/src/utils/oauth/google-antigravity.ts +200 -0
- package/src/utils/oauth/google-gemini-cli.ts +256 -0
- package/src/utils/oauth/google-oauth-shared.ts +110 -0
- package/src/utils/oauth/huggingface.ts +62 -0
- package/src/utils/oauth/index.ts +444 -0
- package/src/utils/oauth/kagi.ts +47 -0
- package/src/utils/oauth/kilo.ts +87 -0
- package/src/utils/oauth/kimi.ts +254 -0
- package/src/utils/oauth/litellm.ts +47 -0
- package/src/utils/oauth/lm-studio.ts +38 -0
- package/src/utils/oauth/minimax-code.ts +78 -0
- package/src/utils/oauth/moonshot.ts +16 -0
- package/src/utils/oauth/nanogpt.ts +15 -0
- package/src/utils/oauth/nvidia.ts +70 -0
- package/src/utils/oauth/oauth.html +199 -0
- package/src/utils/oauth/ollama-cloud.ts +28 -0
- package/src/utils/oauth/ollama.ts +47 -0
- package/src/utils/oauth/openai-codex.ts +299 -0
- package/src/utils/oauth/opencode.ts +49 -0
- package/src/utils/oauth/parallel.ts +46 -0
- package/src/utils/oauth/perplexity.ts +206 -0
- package/src/utils/oauth/pkce.ts +18 -0
- package/src/utils/oauth/qianfan.ts +58 -0
- package/src/utils/oauth/qwen-portal.ts +60 -0
- package/src/utils/oauth/synthetic.ts +16 -0
- package/src/utils/oauth/tavily.ts +46 -0
- package/src/utils/oauth/together.ts +16 -0
- package/src/utils/oauth/types.ts +94 -0
- package/src/utils/oauth/venice.ts +59 -0
- package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
- package/src/utils/oauth/vllm.ts +40 -0
- package/src/utils/oauth/xiaomi.ts +137 -0
- package/src/utils/oauth/zai.ts +60 -0
- package/src/utils/oauth/zenmux.ts +15 -0
- package/src/utils/overflow.ts +137 -0
- package/src/utils/parse-bind.ts +54 -0
- package/src/utils/provider-response.ts +30 -0
- package/src/utils/retry-after.ts +110 -0
- package/src/utils/retry.ts +54 -0
- package/src/utils/schema/CONSTRAINTS.md +164 -0
- package/src/utils/schema/adapt.ts +36 -0
- package/src/utils/schema/compatibility.ts +435 -0
- package/src/utils/schema/dereference.ts +98 -0
- package/src/utils/schema/draft.ts +341 -0
- package/src/utils/schema/equality.ts +97 -0
- package/src/utils/schema/fields.ts +190 -0
- package/src/utils/schema/index.ts +13 -0
- package/src/utils/schema/json-schema-validator.ts +577 -0
- package/src/utils/schema/meta-validator.ts +167 -0
- package/src/utils/schema/normalize.ts +1588 -0
- package/src/utils/schema/spill.ts +43 -0
- package/src/utils/schema/stamps.ts +97 -0
- package/src/utils/schema/types.ts +11 -0
- package/src/utils/schema/wire.ts +213 -0
- package/src/utils/schema/zod-decontaminate.ts +331 -0
- package/src/utils/sse-debug.ts +289 -0
- package/src/utils/tool-call-healing.ts +271 -0
- package/src/utils/tool-choice.ts +99 -0
- package/src/utils/validation.ts +1019 -0
- package/src/utils.ts +166 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared factory for API-key-paste "login" flows.
|
|
3
|
+
*
|
|
4
|
+
* Several providers (Cerebras, Synthetic, Moonshot, Together, NanoGPT, ZenMux)
|
|
5
|
+
* don't actually implement OAuth — they just ask the user to paste an API key,
|
|
6
|
+
* optionally validate it, and return the trimmed key.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { validateApiKeyAgainstModelsEndpoint, validateOpenAICompatibleApiKey } from "./api-key-validation";
|
|
10
|
+
import type { OAuthController } from "./types";
|
|
11
|
+
|
|
12
|
+
type ChatCompletionsValidation = {
|
|
13
|
+
kind: "chat-completions";
|
|
14
|
+
provider: string;
|
|
15
|
+
baseUrl: string;
|
|
16
|
+
model: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type ModelsEndpointValidation = {
|
|
20
|
+
kind: "models-endpoint";
|
|
21
|
+
provider: string;
|
|
22
|
+
modelsUrl: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ApiKeyLoginConfig = {
|
|
26
|
+
/** Display name used in error messages, e.g. "Cerebras", "NanoGPT". */
|
|
27
|
+
providerLabel: string;
|
|
28
|
+
/** URL opened in browser for the user to grab their key. */
|
|
29
|
+
authUrl: string;
|
|
30
|
+
/** Instructions shown with the onAuth callback. */
|
|
31
|
+
instructions: string;
|
|
32
|
+
/** Prompt message shown when asking for the key paste. */
|
|
33
|
+
promptMessage: string;
|
|
34
|
+
/** Placeholder string for the prompt (e.g. "sk-...", "csk-..."). */
|
|
35
|
+
placeholder: string;
|
|
36
|
+
/** Validation strategy, or `null` to skip validation. */
|
|
37
|
+
validation: ChatCompletionsValidation | ModelsEndpointValidation | null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function createApiKeyLogin(config: ApiKeyLoginConfig): (options: OAuthController) => Promise<string> {
|
|
41
|
+
return async function login(options: OAuthController): Promise<string> {
|
|
42
|
+
if (!options.onPrompt) {
|
|
43
|
+
throw new Error(`${config.providerLabel} login requires onPrompt callback`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
options.onAuth?.({
|
|
47
|
+
url: config.authUrl,
|
|
48
|
+
instructions: config.instructions,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const apiKey = await options.onPrompt({
|
|
52
|
+
message: config.promptMessage,
|
|
53
|
+
placeholder: config.placeholder,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (options.signal?.aborted) {
|
|
57
|
+
throw new Error("Login cancelled");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const trimmed = apiKey.trim();
|
|
61
|
+
if (!trimmed) {
|
|
62
|
+
throw new Error("API key is required");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (config.validation) {
|
|
66
|
+
options.onProgress?.("Validating API key...");
|
|
67
|
+
if (config.validation.kind === "chat-completions") {
|
|
68
|
+
await validateOpenAICompatibleApiKey({
|
|
69
|
+
provider: config.validation.provider,
|
|
70
|
+
apiKey: trimmed,
|
|
71
|
+
baseUrl: config.validation.baseUrl,
|
|
72
|
+
model: config.validation.model,
|
|
73
|
+
signal: options.signal,
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
await validateApiKeyAgainstModelsEndpoint({
|
|
77
|
+
provider: config.validation.provider,
|
|
78
|
+
apiKey: trimmed,
|
|
79
|
+
modelsUrl: config.validation.modelsUrl,
|
|
80
|
+
signal: options.signal,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return trimmed;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
type OpenAICompatibleValidationOptions = {
|
|
2
|
+
provider: string;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
model: string;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type ModelListValidationOptions = {
|
|
10
|
+
provider: string;
|
|
11
|
+
apiKey: string;
|
|
12
|
+
modelsUrl: string;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const VALIDATION_TIMEOUT_MS = 15_000;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validate an API key against an OpenAI-compatible chat completions endpoint.
|
|
20
|
+
*
|
|
21
|
+
* Performs a minimal request to verify credentials and endpoint access.
|
|
22
|
+
*/
|
|
23
|
+
export async function validateOpenAICompatibleApiKey(options: OpenAICompatibleValidationOptions): Promise<void> {
|
|
24
|
+
const timeoutSignal = AbortSignal.timeout(VALIDATION_TIMEOUT_MS);
|
|
25
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal;
|
|
26
|
+
|
|
27
|
+
const response = await fetch(`${options.baseUrl}/chat/completions`, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({
|
|
34
|
+
model: options.model,
|
|
35
|
+
messages: [{ role: "user", content: "ping" }],
|
|
36
|
+
max_tokens: 1,
|
|
37
|
+
temperature: 0,
|
|
38
|
+
}),
|
|
39
|
+
signal,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (response.ok) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let details = "";
|
|
47
|
+
try {
|
|
48
|
+
details = (await response.text()).trim();
|
|
49
|
+
} catch {
|
|
50
|
+
// ignore body parse errors, status is enough
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const message = details
|
|
54
|
+
? `${options.provider} API key validation failed (${response.status}): ${details}`
|
|
55
|
+
: `${options.provider} API key validation failed (${response.status})`;
|
|
56
|
+
throw new Error(message);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Validate an API key against a provider models endpoint.
|
|
61
|
+
*
|
|
62
|
+
* Useful for providers where access to specific models may vary by plan and
|
|
63
|
+
* should not block key validation.
|
|
64
|
+
*/
|
|
65
|
+
export async function validateApiKeyAgainstModelsEndpoint(options: ModelListValidationOptions): Promise<void> {
|
|
66
|
+
const timeoutSignal = AbortSignal.timeout(VALIDATION_TIMEOUT_MS);
|
|
67
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal;
|
|
68
|
+
|
|
69
|
+
const response = await fetch(options.modelsUrl, {
|
|
70
|
+
method: "GET",
|
|
71
|
+
headers: {
|
|
72
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
73
|
+
},
|
|
74
|
+
signal,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (response.ok) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let details = "";
|
|
82
|
+
try {
|
|
83
|
+
details = (await response.text()).trim();
|
|
84
|
+
} catch {
|
|
85
|
+
// ignore body parse errors, status is enough
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const message = details
|
|
89
|
+
? `${options.provider} API key validation failed (${response.status}): ${details}`
|
|
90
|
+
: `${options.provider} API key validation failed (${response.status})`;
|
|
91
|
+
throw new Error(message);
|
|
92
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract base class for OAuth flows with local callback servers.
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Port allocation (tries expected port, falls back to random)
|
|
6
|
+
* - Callback server setup and request handling
|
|
7
|
+
* - Common OAuth flow logic
|
|
8
|
+
*
|
|
9
|
+
* Providers extend this and implement:
|
|
10
|
+
* - generateAuthUrl(): Build provider-specific authorization URL
|
|
11
|
+
* - exchangeToken(): Exchange authorization code for tokens
|
|
12
|
+
*/
|
|
13
|
+
import templateHtml from "./oauth.html" with { type: "text" };
|
|
14
|
+
import type { OAuthController, OAuthCredentials } from "./types";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_TIMEOUT = 300_000;
|
|
17
|
+
const DEFAULT_HOSTNAME = "localhost";
|
|
18
|
+
const CALLBACK_PATH = "/callback";
|
|
19
|
+
|
|
20
|
+
export type CallbackResult = { code: string; state: string };
|
|
21
|
+
|
|
22
|
+
export interface OAuthCallbackFlowOptions {
|
|
23
|
+
preferredPort: number;
|
|
24
|
+
callbackPath?: string;
|
|
25
|
+
callbackHostname?: string;
|
|
26
|
+
/** Exact redirect URI advertised to the provider; disables port fallback. */
|
|
27
|
+
redirectUri?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Abstract base class for OAuth flows with local callback servers.
|
|
32
|
+
*/
|
|
33
|
+
export abstract class OAuthCallbackFlow {
|
|
34
|
+
ctrl: OAuthController;
|
|
35
|
+
preferredPort: number;
|
|
36
|
+
callbackPath: string;
|
|
37
|
+
callbackHostname: string;
|
|
38
|
+
redirectUri?: string;
|
|
39
|
+
#callbackResolve?: (result: CallbackResult) => void;
|
|
40
|
+
#callbackReject?: (error: string) => void;
|
|
41
|
+
|
|
42
|
+
constructor(
|
|
43
|
+
ctrl: OAuthController,
|
|
44
|
+
preferredPortOrOptions: number | OAuthCallbackFlowOptions,
|
|
45
|
+
callbackPath: string = CALLBACK_PATH,
|
|
46
|
+
) {
|
|
47
|
+
this.ctrl = ctrl;
|
|
48
|
+
if (typeof preferredPortOrOptions === "number") {
|
|
49
|
+
this.preferredPort = preferredPortOrOptions;
|
|
50
|
+
this.callbackPath = callbackPath;
|
|
51
|
+
this.callbackHostname = DEFAULT_HOSTNAME;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.preferredPort = preferredPortOrOptions.preferredPort;
|
|
56
|
+
this.callbackPath = preferredPortOrOptions.callbackPath ?? CALLBACK_PATH;
|
|
57
|
+
this.callbackHostname = preferredPortOrOptions.callbackHostname ?? DEFAULT_HOSTNAME;
|
|
58
|
+
this.redirectUri = preferredPortOrOptions.redirectUri;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Generate provider-specific authorization URL.
|
|
63
|
+
* @param state - CSRF state token
|
|
64
|
+
* @param redirectUri - The actual redirect URI to use (may differ from expected if port fallback occurred)
|
|
65
|
+
* @returns Authorization URL and optional instructions
|
|
66
|
+
*/
|
|
67
|
+
abstract generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Exchange authorization code for OAuth tokens.
|
|
71
|
+
* @param code - Authorization code from callback
|
|
72
|
+
* @param state - CSRF state token
|
|
73
|
+
* @param redirectUri - The actual redirect URI used (must match authorization request)
|
|
74
|
+
* @returns OAuth credentials
|
|
75
|
+
*/
|
|
76
|
+
abstract exchangeToken(code: string, state: string, redirectUri: string): Promise<OAuthCredentials>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Generate CSRF state token. Override if provider needs custom state generation.
|
|
80
|
+
*/
|
|
81
|
+
generateState(): string {
|
|
82
|
+
const bytes = new Uint8Array(16);
|
|
83
|
+
crypto.getRandomValues(bytes);
|
|
84
|
+
return Array.from(bytes)
|
|
85
|
+
.map(value => value.toString(16).padStart(2, "0"))
|
|
86
|
+
.join("");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Execute the OAuth login flow.
|
|
91
|
+
*/
|
|
92
|
+
async login(): Promise<OAuthCredentials> {
|
|
93
|
+
const state = this.generateState();
|
|
94
|
+
|
|
95
|
+
// Start callback server first to get actual redirect URI
|
|
96
|
+
const { server, redirectUri } = await this.#startCallbackServer(state);
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
// Generate auth URL with the ACTUAL redirect URI (may differ from expected if port was busy)
|
|
100
|
+
const { url: authUrl, instructions } = await this.generateAuthUrl(state, redirectUri);
|
|
101
|
+
|
|
102
|
+
// Notify controller that auth is ready
|
|
103
|
+
this.ctrl.onAuth?.({ url: authUrl, instructions });
|
|
104
|
+
this.ctrl.onProgress?.("Waiting for browser authentication...");
|
|
105
|
+
|
|
106
|
+
// Wait for callback or manual input
|
|
107
|
+
const { code } = await this.#waitForCallback(state);
|
|
108
|
+
|
|
109
|
+
this.ctrl.onProgress?.("Exchanging authorization code for tokens...");
|
|
110
|
+
|
|
111
|
+
return await this.exchangeToken(code, state, redirectUri);
|
|
112
|
+
} finally {
|
|
113
|
+
server.stop();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Start callback server, trying preferred port first, falling back to random.
|
|
119
|
+
*/
|
|
120
|
+
async #startCallbackServer(expectedState: string): Promise<{ server: Bun.Server<unknown>; redirectUri: string }> {
|
|
121
|
+
try {
|
|
122
|
+
const server = this.#createServer(this.preferredPort, expectedState);
|
|
123
|
+
if (this.redirectUri) {
|
|
124
|
+
return { server, redirectUri: this.redirectUri };
|
|
125
|
+
}
|
|
126
|
+
const redirectUri = `http://${this.callbackHostname}:${this.preferredPort}${this.callbackPath}`;
|
|
127
|
+
return { server, redirectUri };
|
|
128
|
+
} catch {
|
|
129
|
+
if (this.redirectUri) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`OAuth callback port ${this.preferredPort} unavailable; cannot fall back to a random port when oauth.redirectUri is set`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const server = this.#createServer(0, expectedState);
|
|
135
|
+
const actualPort = server.port;
|
|
136
|
+
const redirectUri = `http://${this.callbackHostname}:${actualPort}${this.callbackPath}`;
|
|
137
|
+
this.ctrl.onProgress?.(`Preferred port ${this.preferredPort} unavailable, using port ${actualPort}`);
|
|
138
|
+
return { server, redirectUri };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create HTTP server for OAuth callback.
|
|
144
|
+
*/
|
|
145
|
+
#createServer(port: number, expectedState: string): Bun.Server<unknown> {
|
|
146
|
+
return Bun.serve({
|
|
147
|
+
hostname: this.callbackHostname,
|
|
148
|
+
port,
|
|
149
|
+
reusePort: false,
|
|
150
|
+
fetch: req => this.#handleCallback(req, expectedState),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Handle OAuth callback HTTP request.
|
|
156
|
+
*/
|
|
157
|
+
#handleCallback(req: Request, expectedState: string): Response {
|
|
158
|
+
const url = new URL(req.url);
|
|
159
|
+
|
|
160
|
+
if (url.pathname !== this.callbackPath) {
|
|
161
|
+
return new Response("Not Found", { status: 404 });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const code = url.searchParams.get("code");
|
|
165
|
+
const state = url.searchParams.get("state") || "";
|
|
166
|
+
const error = url.searchParams.get("error") || "";
|
|
167
|
+
const errorDescription = url.searchParams.get("error_description") || error;
|
|
168
|
+
|
|
169
|
+
type OkState = { ok: true; code: string; state: string };
|
|
170
|
+
type ErrorState = { ok?: false; error?: string };
|
|
171
|
+
let resultState: OkState | ErrorState;
|
|
172
|
+
|
|
173
|
+
if (error) {
|
|
174
|
+
resultState = { ok: false, error: `Authorization failed: ${errorDescription}` };
|
|
175
|
+
} else if (!code) {
|
|
176
|
+
resultState = { ok: false, error: "Missing authorization code" };
|
|
177
|
+
} else if (expectedState && state !== expectedState) {
|
|
178
|
+
resultState = { ok: false, error: "State mismatch - possible CSRF attack" };
|
|
179
|
+
} else {
|
|
180
|
+
resultState = { ok: true, code, state };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Signal to waitForCallback - capture refs before they could be cleared
|
|
184
|
+
const resolve = this.#callbackResolve;
|
|
185
|
+
const reject = this.#callbackReject;
|
|
186
|
+
queueMicrotask(() => {
|
|
187
|
+
if (resultState.ok) {
|
|
188
|
+
resolve?.({ code: resultState.code, state: resultState.state });
|
|
189
|
+
} else {
|
|
190
|
+
reject?.(resultState.error ?? "Unknown error");
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return new Response(
|
|
195
|
+
(templateHtml as unknown as string).replaceAll("__OAUTH_STATE__", JSON.stringify(resultState)),
|
|
196
|
+
{
|
|
197
|
+
status: resultState.ok ? 200 : 500,
|
|
198
|
+
headers: { "Content-Type": "text/html" },
|
|
199
|
+
},
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Wait for OAuth callback or manual input (whichever comes first).
|
|
205
|
+
*/
|
|
206
|
+
#waitForCallback(expectedState: string): Promise<CallbackResult> {
|
|
207
|
+
const timeoutSignal = AbortSignal.timeout(DEFAULT_TIMEOUT);
|
|
208
|
+
const signal = this.ctrl.signal ? AbortSignal.any([this.ctrl.signal, timeoutSignal]) : timeoutSignal;
|
|
209
|
+
|
|
210
|
+
const callbackPromise = new Promise<CallbackResult>((resolve, reject) => {
|
|
211
|
+
this.#callbackResolve = resolve;
|
|
212
|
+
this.#callbackReject = reject;
|
|
213
|
+
|
|
214
|
+
signal.addEventListener("abort", () => {
|
|
215
|
+
this.#callbackResolve = undefined;
|
|
216
|
+
this.#callbackReject = undefined;
|
|
217
|
+
reject(new Error(`OAuth callback cancelled: ${signal.reason}`));
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Manual input race (if supported)
|
|
222
|
+
if (this.ctrl.onManualCodeInput) {
|
|
223
|
+
const requestManualInput = this.ctrl.onManualCodeInput;
|
|
224
|
+
const manualPromise = (async (): Promise<CallbackResult> => {
|
|
225
|
+
while (true) {
|
|
226
|
+
const result = await Promise.race([
|
|
227
|
+
callbackPromise,
|
|
228
|
+
requestManualInput()
|
|
229
|
+
.then((input): CallbackResult | null => {
|
|
230
|
+
const parsed = parseCallbackInput(input);
|
|
231
|
+
if (!parsed.code) return null;
|
|
232
|
+
if (expectedState && parsed.state && parsed.state !== expectedState) return null;
|
|
233
|
+
return { code: parsed.code, state: parsed.state ?? "" };
|
|
234
|
+
})
|
|
235
|
+
.catch((): CallbackResult | null => null),
|
|
236
|
+
]);
|
|
237
|
+
if (result) return result;
|
|
238
|
+
}
|
|
239
|
+
})();
|
|
240
|
+
|
|
241
|
+
return Promise.race([callbackPromise, manualPromise]);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return callbackPromise;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Parse a redirect URL or code string to extract code and state.
|
|
250
|
+
*/
|
|
251
|
+
export function parseCallbackInput(input: string): { code?: string; state?: string } {
|
|
252
|
+
const value = input.trim();
|
|
253
|
+
if (!value) return {};
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const url = new URL(value);
|
|
257
|
+
return {
|
|
258
|
+
code: url.searchParams.get("code") ?? undefined,
|
|
259
|
+
state: url.searchParams.get("state") ?? undefined,
|
|
260
|
+
};
|
|
261
|
+
} catch {
|
|
262
|
+
// Not a URL - check for query string format
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (value.includes("code=")) {
|
|
266
|
+
const params = new URLSearchParams(value.replace(/^[?#]/, ""));
|
|
267
|
+
return {
|
|
268
|
+
code: params.get("code") ?? undefined,
|
|
269
|
+
state: params.get("state") ?? undefined,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Assume raw code, possibly with state after #
|
|
274
|
+
const [code, state] = value.split("#", 2);
|
|
275
|
+
return { code, state };
|
|
276
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Cerebras login flow (API key paste against https://api.cerebras.ai/v1). */
|
|
2
|
+
import { createApiKeyLogin } from "./api-key-login";
|
|
3
|
+
|
|
4
|
+
export const loginCerebras = createApiKeyLogin({
|
|
5
|
+
providerLabel: "Cerebras",
|
|
6
|
+
authUrl: "https://cloud.cerebras.ai/platform/",
|
|
7
|
+
instructions: "Copy your API key from the Cerebras dashboard",
|
|
8
|
+
promptMessage: "Paste your Cerebras API key",
|
|
9
|
+
placeholder: "csk-...",
|
|
10
|
+
validation: {
|
|
11
|
+
kind: "chat-completions",
|
|
12
|
+
provider: "Cerebras",
|
|
13
|
+
baseUrl: "https://api.cerebras.ai/v1",
|
|
14
|
+
model: "gpt-oss-120b",
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare AI Gateway login flow.
|
|
3
|
+
*
|
|
4
|
+
* Cloudflare AI Gateway proxies upstream model providers.
|
|
5
|
+
*
|
|
6
|
+
* This is not OAuth - it's a simple API key flow:
|
|
7
|
+
* 1. Open Cloudflare AI Gateway docs/dashboard
|
|
8
|
+
* 2. User copies their Cloudflare AI Gateway token/API key
|
|
9
|
+
* 3. User pastes the API key into the CLI
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { OAuthController } from "./types";
|
|
13
|
+
|
|
14
|
+
const AUTH_URL = "https://developers.cloudflare.com/ai-gateway/configuration/authentication/";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Login to Cloudflare AI Gateway.
|
|
18
|
+
*
|
|
19
|
+
* Opens browser to Cloudflare AI Gateway authentication docs and prompts for a gateway token/API key.
|
|
20
|
+
* Returns the API key directly (not OAuthCredentials - this isn't OAuth).
|
|
21
|
+
*/
|
|
22
|
+
export async function loginCloudflareAiGateway(options: OAuthController): Promise<string> {
|
|
23
|
+
if (!options.onPrompt) {
|
|
24
|
+
throw new Error("Cloudflare AI Gateway login requires onPrompt callback");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
options.onAuth?.({
|
|
28
|
+
url: AUTH_URL,
|
|
29
|
+
instructions:
|
|
30
|
+
"Copy your Cloudflare AI Gateway token/API key. Configure account/gateway base URL in models config.",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const apiKey = await options.onPrompt({
|
|
34
|
+
message: "Paste your Cloudflare AI Gateway token/API key",
|
|
35
|
+
placeholder: "cf-aig-...",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (options.signal?.aborted) {
|
|
39
|
+
throw new Error("Login cancelled");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const trimmed = apiKey.trim();
|
|
43
|
+
if (!trimmed) {
|
|
44
|
+
throw new Error("API key is required");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return trimmed;
|
|
48
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { generatePKCE } from "./pkce";
|
|
2
|
+
import type { OAuthCredentials } from "./types";
|
|
3
|
+
|
|
4
|
+
const CURSOR_LOGIN_URL = "https://cursor.com/loginDeepControl";
|
|
5
|
+
const CURSOR_POLL_URL = "https://api2.cursor.sh/auth/poll";
|
|
6
|
+
const CURSOR_REFRESH_URL = "https://api2.cursor.sh/auth/exchange_user_api_key";
|
|
7
|
+
|
|
8
|
+
const POLL_MAX_ATTEMPTS = 150;
|
|
9
|
+
const POLL_BASE_DELAY = 1000;
|
|
10
|
+
const POLL_MAX_DELAY = 10000;
|
|
11
|
+
const POLL_BACKOFF_MULTIPLIER = 1.2;
|
|
12
|
+
|
|
13
|
+
export interface CursorAuthParams {
|
|
14
|
+
verifier: string;
|
|
15
|
+
challenge: string;
|
|
16
|
+
uuid: string;
|
|
17
|
+
loginUrl: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function generateCursorAuthParams(): Promise<CursorAuthParams> {
|
|
21
|
+
const { verifier, challenge } = await generatePKCE();
|
|
22
|
+
const uuid = crypto.randomUUID();
|
|
23
|
+
|
|
24
|
+
const params = new URLSearchParams({
|
|
25
|
+
challenge,
|
|
26
|
+
uuid,
|
|
27
|
+
mode: "login",
|
|
28
|
+
redirectTarget: "cli",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const loginUrl = `${CURSOR_LOGIN_URL}?${params.toString()}`;
|
|
32
|
+
|
|
33
|
+
return { verifier, challenge, uuid, loginUrl };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function pollCursorAuth(
|
|
37
|
+
uuid: string,
|
|
38
|
+
verifier: string,
|
|
39
|
+
): Promise<{ accessToken: string; refreshToken: string }> {
|
|
40
|
+
let delay = POLL_BASE_DELAY;
|
|
41
|
+
let consecutiveErrors = 0;
|
|
42
|
+
|
|
43
|
+
for (let attempt = 0; attempt < POLL_MAX_ATTEMPTS; attempt++) {
|
|
44
|
+
await Bun.sleep(delay);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch(`${CURSOR_POLL_URL}?uuid=${uuid}&verifier=${verifier}`);
|
|
48
|
+
|
|
49
|
+
if (response.status === 404) {
|
|
50
|
+
consecutiveErrors = 0;
|
|
51
|
+
delay = Math.min(delay * POLL_BACKOFF_MULTIPLIER, POLL_MAX_DELAY);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (response.ok) {
|
|
56
|
+
const data = (await response.json()) as {
|
|
57
|
+
accessToken: string;
|
|
58
|
+
refreshToken: string;
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
accessToken: data.accessToken,
|
|
62
|
+
refreshToken: data.refreshToken,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
throw new Error(`Poll failed: ${response.status}`);
|
|
67
|
+
} catch {
|
|
68
|
+
consecutiveErrors++;
|
|
69
|
+
if (consecutiveErrors >= 3) {
|
|
70
|
+
throw new Error("Too many consecutive errors during Cursor auth polling");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw new Error("Cursor authentication polling timeout");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function loginCursor(
|
|
79
|
+
onAuthUrl: (url: string) => void,
|
|
80
|
+
onPollStart?: () => void,
|
|
81
|
+
): Promise<OAuthCredentials> {
|
|
82
|
+
const { verifier, uuid, loginUrl } = await generateCursorAuthParams();
|
|
83
|
+
|
|
84
|
+
onAuthUrl(loginUrl);
|
|
85
|
+
onPollStart?.();
|
|
86
|
+
|
|
87
|
+
const { accessToken, refreshToken } = await pollCursorAuth(uuid, verifier);
|
|
88
|
+
|
|
89
|
+
const expiresAt = getTokenExpiry(accessToken);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
access: accessToken,
|
|
93
|
+
refresh: refreshToken,
|
|
94
|
+
expires: expiresAt,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function refreshCursorToken(apiKeyOrRefreshToken: string): Promise<OAuthCredentials> {
|
|
99
|
+
const response = await fetch(CURSOR_REFRESH_URL, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: {
|
|
102
|
+
Authorization: `Bearer ${apiKeyOrRefreshToken}`,
|
|
103
|
+
"Content-Type": "application/json",
|
|
104
|
+
},
|
|
105
|
+
body: "{}",
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
const error = await response.text();
|
|
110
|
+
throw new Error(`Cursor token refresh failed: ${error}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const data = (await response.json()) as {
|
|
114
|
+
accessToken: string;
|
|
115
|
+
refreshToken: string;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const expiresAt = getTokenExpiry(data.accessToken);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
access: data.accessToken,
|
|
122
|
+
refresh: data.refreshToken || apiKeyOrRefreshToken,
|
|
123
|
+
expires: expiresAt,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getTokenExpiry(token: string): number {
|
|
128
|
+
try {
|
|
129
|
+
const parts = token.split(".");
|
|
130
|
+
if (parts.length !== 3) {
|
|
131
|
+
return Date.now() + 3600 * 1000;
|
|
132
|
+
}
|
|
133
|
+
const payload = parts[1];
|
|
134
|
+
if (!payload) {
|
|
135
|
+
return Date.now() + 3600 * 1000;
|
|
136
|
+
}
|
|
137
|
+
const decoded = JSON.parse(atob(payload.replace(/-/g, "+").replace(/_/g, "/")));
|
|
138
|
+
if (decoded && typeof decoded === "object" && typeof decoded.exp === "number") {
|
|
139
|
+
return decoded.exp * 1000 - 5 * 60 * 1000;
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// Ignore parsing errors
|
|
143
|
+
}
|
|
144
|
+
return Date.now() + 3600 * 1000;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function isCursorTokenExpiringSoon(token: string, thresholdSeconds = 300): boolean {
|
|
148
|
+
try {
|
|
149
|
+
const [, payload] = token.split(".");
|
|
150
|
+
if (!payload) return true;
|
|
151
|
+
const decoded = JSON.parse(atob(payload.replace(/-/g, "+").replace(/_/g, "/")));
|
|
152
|
+
const currentTime = Math.floor(Date.now() / 1000);
|
|
153
|
+
return decoded.exp - currentTime < thresholdSeconds;
|
|
154
|
+
} catch {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
}
|