omk-ai 0.96.2 → 0.97.0

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/dist/models.js CHANGED
@@ -5,7 +5,10 @@ const modelRegistry = new Map();
5
5
  for (const [provider, models] of Object.entries(MODELS)) {
6
6
  const providerModels = new Map();
7
7
  for (const [id, model] of Object.entries(models)) {
8
- const registered = model;
8
+ const generated = model;
9
+ const registered = generated.id.toLowerCase().includes("gpt-5.6")
10
+ ? { ...generated, contextWindow: 1_000_000 }
11
+ : generated;
9
12
  providerModels.set(id, provider === "xai" ? applyGrokThinking(registered) : registered);
10
13
  }
11
14
  modelRegistry.set(provider, providerModels);
@@ -1 +1 @@
1
- {"version":3,"file":"models.js","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAGjE,MAAM,aAAa,GAAyC,IAAI,GAAG,EAAE,CAAC;AAEtE,iDAAiD;AACjD,KAAK,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;IACzD,MAAM,cAAc,GAAG,IAAI,GAAG,EAAsB,CAAC;IACrD,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,MAAM,UAAU,GAAG,KAAmB,CAAC;QACvC,cAAc,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,KAAK,KAAK,CAAC,CAAC,CAAC,iBAAiB,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;IACzF,CAAC;IACD,aAAa,CAAC,GAAG,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;AAC7C,CAAC;AAOD,MAAM,UAAU,QAAQ,CACvB,QAAmB,EACnB,OAAiB,EACsB;IACvC,MAAM,cAAc,GAAG,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACnD,OAAO,cAAc,EAAE,GAAG,CAAC,OAAiB,CAAyC,CAAC;AAAA,CACtF;AAED,MAAM,UAAU,YAAY,GAAoB;IAC/C,OAAO,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,CAAoB,CAAC;AAAA,CAC3D;AAED,MAAM,UAAU,SAAS,CACxB,QAAmB,EAC8C;IACjE,MAAM,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC3C,OAAO,MAAM,CAAC,CAAC,CAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAoE,CAAC,CAAC,CAAC,EAAE,CAAC;AAAA,CACrH;AAED,MAAM,UAAU,aAAa,CAAmB,KAAkB,EAAE,KAAY,EAAiB;IAChG,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC;IAC9D,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC;IACjE,KAAK,CAAC,IAAI,CAAC,SAAS,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,GAAG,KAAK,CAAC,SAAS,CAAC;IAC1E,KAAK,CAAC,IAAI,CAAC,UAAU,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,GAAG,KAAK,CAAC,UAAU,CAAC;IAC7E,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC;IACvG,OAAO,KAAK,CAAC,IAAI,CAAC;AAAA,CAClB;AAED,MAAM,wBAAwB,GAAyB;IACtD,KAAK;IACL,SAAS;IACT,KAAK;IACL,QAAQ;IACR,MAAM;IACN,OAAO;IACP,KAAK;IACL,OAAO;CACP,CAAC;AAEF,MAAM,UAAU,0BAA0B,CAAmB,KAAkB,EAAwB;IACtG,IAAI,CAAC,KAAK,CAAC,SAAS;QAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAErC,OAAO,wBAAwB,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;QACjD,MAAM,MAAM,GAAG,KAAK,CAAC,gBAAgB,EAAE,CAAC,KAAK,CAAC,CAAC;QAC/C,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAClC,4FAA4F;QAC5F,IAAI,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,OAAO;YAAE,OAAO,MAAM,KAAK,SAAS,CAAC;QAC3F,OAAO,IAAI,CAAC;IAAA,CACZ,CAAC,CAAC;AAAA,CACH;AAED,MAAM,UAAU,kBAAkB,CACjC,KAAkB,EAClB,KAAyB,EACJ;IACrB,MAAM,eAAe,GAAG,0BAA0B,CAAC,KAAK,CAAC,CAAC;IAC1D,IAAI,eAAe,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAElD,MAAM,cAAc,GAAG,wBAAwB,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC/D,IAAI,cAAc,KAAK,CAAC,CAAC;QAAE,OAAO,eAAe,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC;IAE9D,KAAK,IAAI,CAAC,GAAG,cAAc,EAAE,CAAC,GAAG,wBAAwB,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACvE,MAAM,SAAS,GAAG,wBAAwB,CAAC,CAAC,CAAC,CAAC;QAC9C,IAAI,eAAe,CAAC,QAAQ,CAAC,SAAS,CAAC;YAAE,OAAO,SAAS,CAAC;IAC3D,CAAC;IACD,KAAK,IAAI,CAAC,GAAG,cAAc,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC9C,MAAM,SAAS,GAAG,wBAAwB,CAAC,CAAC,CAAC,CAAC;QAC9C,IAAI,eAAe,CAAC,QAAQ,CAAC,SAAS,CAAC;YAAE,OAAO,SAAS,CAAC;IAC3D,CAAC;IACD,OAAO,eAAe,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC;AAAA,CACnC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC7B,CAAiC,EACjC,CAAiC,EACvB;IACV,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3B,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ,CAAC;AAAA,CAClD","sourcesContent":["import { MODELS } from \"./models.generated.ts\";\nimport { applyGrokThinking } from \"./providers/grok-thinking.ts\";\nimport type { Api, KnownProvider, Model, ModelThinkingLevel, Usage } from \"./types.ts\";\n\nconst modelRegistry: Map<string, Map<string, Model<Api>>> = new Map();\n\n// Initialize registry from MODELS on module load\nfor (const [provider, models] of Object.entries(MODELS)) {\n\tconst providerModels = new Map<string, Model<Api>>();\n\tfor (const [id, model] of Object.entries(models)) {\n\t\tconst registered = model as Model<Api>;\n\t\tproviderModels.set(id, provider === \"xai\" ? applyGrokThinking(registered) : registered);\n\t}\n\tmodelRegistry.set(provider, providerModels);\n}\n\ntype ModelApi<\n\tTProvider extends KnownProvider,\n\tTModelId extends keyof (typeof MODELS)[TProvider],\n> = (typeof MODELS)[TProvider][TModelId] extends { api: infer TApi } ? (TApi extends Api ? TApi : never) : never;\n\nexport function getModel<TProvider extends KnownProvider, TModelId extends keyof (typeof MODELS)[TProvider]>(\n\tprovider: TProvider,\n\tmodelId: TModelId,\n): Model<ModelApi<TProvider, TModelId>> {\n\tconst providerModels = modelRegistry.get(provider);\n\treturn providerModels?.get(modelId as string) as Model<ModelApi<TProvider, TModelId>>;\n}\n\nexport function getProviders(): KnownProvider[] {\n\treturn Array.from(modelRegistry.keys()) as KnownProvider[];\n}\n\nexport function getModels<TProvider extends KnownProvider>(\n\tprovider: TProvider,\n): Model<ModelApi<TProvider, keyof (typeof MODELS)[TProvider]>>[] {\n\tconst models = modelRegistry.get(provider);\n\treturn models ? (Array.from(models.values()) as Model<ModelApi<TProvider, keyof (typeof MODELS)[TProvider]>>[]) : [];\n}\n\nexport function calculateCost<TApi extends Api>(model: Model<TApi>, usage: Usage): Usage[\"cost\"] {\n\tusage.cost.input = (model.cost.input / 1000000) * usage.input;\n\tusage.cost.output = (model.cost.output / 1000000) * usage.output;\n\tusage.cost.cacheRead = (model.cost.cacheRead / 1000000) * usage.cacheRead;\n\tusage.cost.cacheWrite = (model.cost.cacheWrite / 1000000) * usage.cacheWrite;\n\tusage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;\n\treturn usage.cost;\n}\n\nconst EXTENDED_THINKING_LEVELS: ModelThinkingLevel[] = [\n\t\"off\",\n\t\"minimal\",\n\t\"low\",\n\t\"medium\",\n\t\"high\",\n\t\"xhigh\",\n\t\"max\",\n\t\"ultra\",\n];\n\nexport function getSupportedThinkingLevels<TApi extends Api>(model: Model<TApi>): ModelThinkingLevel[] {\n\tif (!model.reasoning) return [\"off\"];\n\n\treturn EXTENDED_THINKING_LEVELS.filter((level) => {\n\t\tconst mapped = model.thinkingLevelMap?.[level];\n\t\tif (mapped === null) return false;\n\t\t// xhigh, max, and ultra are top-tier levels only exposed when a model explicitly maps them.\n\t\tif (level === \"xhigh\" || level === \"max\" || level === \"ultra\") return mapped !== undefined;\n\t\treturn true;\n\t});\n}\n\nexport function clampThinkingLevel<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tlevel: ModelThinkingLevel,\n): ModelThinkingLevel {\n\tconst availableLevels = getSupportedThinkingLevels(model);\n\tif (availableLevels.includes(level)) return level;\n\n\tconst requestedIndex = EXTENDED_THINKING_LEVELS.indexOf(level);\n\tif (requestedIndex === -1) return availableLevels[0] ?? \"off\";\n\n\tfor (let i = requestedIndex; i < EXTENDED_THINKING_LEVELS.length; i++) {\n\t\tconst candidate = EXTENDED_THINKING_LEVELS[i];\n\t\tif (availableLevels.includes(candidate)) return candidate;\n\t}\n\tfor (let i = requestedIndex - 1; i >= 0; i--) {\n\t\tconst candidate = EXTENDED_THINKING_LEVELS[i];\n\t\tif (availableLevels.includes(candidate)) return candidate;\n\t}\n\treturn availableLevels[0] ?? \"off\";\n}\n\n/**\n * Check if two models are equal by comparing both their id and provider.\n * Returns false if either model is null or undefined.\n */\nexport function modelsAreEqual<TApi extends Api>(\n\ta: Model<TApi> | null | undefined,\n\tb: Model<TApi> | null | undefined,\n): boolean {\n\tif (!a || !b) return false;\n\treturn a.id === b.id && a.provider === b.provider;\n}\n"]}
1
+ {"version":3,"file":"models.js","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"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","sourcesContent":["import { MODELS } from \"./models.generated.ts\";\nimport { applyGrokThinking } from \"./providers/grok-thinking.ts\";\nimport type { Api, KnownProvider, Model, ModelThinkingLevel, Usage } from \"./types.ts\";\n\nconst modelRegistry: Map<string, Map<string, Model<Api>>> = new Map();\n\n// Initialize registry from MODELS on module load\nfor (const [provider, models] of Object.entries(MODELS)) {\n\tconst providerModels = new Map<string, Model<Api>>();\n\tfor (const [id, model] of Object.entries(models)) {\n\t\tconst generated = model as Model<Api>;\n\t\tconst registered = generated.id.toLowerCase().includes(\"gpt-5.6\")\n\t\t\t? { ...generated, contextWindow: 1_000_000 }\n\t\t\t: generated;\n\t\tproviderModels.set(id, provider === \"xai\" ? applyGrokThinking(registered) : registered);\n\t}\n\tmodelRegistry.set(provider, providerModels);\n}\n\ntype ModelApi<\n\tTProvider extends KnownProvider,\n\tTModelId extends keyof (typeof MODELS)[TProvider],\n> = (typeof MODELS)[TProvider][TModelId] extends { api: infer TApi } ? (TApi extends Api ? TApi : never) : never;\n\nexport function getModel<TProvider extends KnownProvider, TModelId extends keyof (typeof MODELS)[TProvider]>(\n\tprovider: TProvider,\n\tmodelId: TModelId,\n): Model<ModelApi<TProvider, TModelId>> {\n\tconst providerModels = modelRegistry.get(provider);\n\treturn providerModels?.get(modelId as string) as Model<ModelApi<TProvider, TModelId>>;\n}\n\nexport function getProviders(): KnownProvider[] {\n\treturn Array.from(modelRegistry.keys()) as KnownProvider[];\n}\n\nexport function getModels<TProvider extends KnownProvider>(\n\tprovider: TProvider,\n): Model<ModelApi<TProvider, keyof (typeof MODELS)[TProvider]>>[] {\n\tconst models = modelRegistry.get(provider);\n\treturn models ? (Array.from(models.values()) as Model<ModelApi<TProvider, keyof (typeof MODELS)[TProvider]>>[]) : [];\n}\n\nexport function calculateCost<TApi extends Api>(model: Model<TApi>, usage: Usage): Usage[\"cost\"] {\n\tusage.cost.input = (model.cost.input / 1000000) * usage.input;\n\tusage.cost.output = (model.cost.output / 1000000) * usage.output;\n\tusage.cost.cacheRead = (model.cost.cacheRead / 1000000) * usage.cacheRead;\n\tusage.cost.cacheWrite = (model.cost.cacheWrite / 1000000) * usage.cacheWrite;\n\tusage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;\n\treturn usage.cost;\n}\n\nconst EXTENDED_THINKING_LEVELS: ModelThinkingLevel[] = [\n\t\"off\",\n\t\"minimal\",\n\t\"low\",\n\t\"medium\",\n\t\"high\",\n\t\"xhigh\",\n\t\"max\",\n\t\"ultra\",\n];\n\nexport function getSupportedThinkingLevels<TApi extends Api>(model: Model<TApi>): ModelThinkingLevel[] {\n\tif (!model.reasoning) return [\"off\"];\n\n\treturn EXTENDED_THINKING_LEVELS.filter((level) => {\n\t\tconst mapped = model.thinkingLevelMap?.[level];\n\t\tif (mapped === null) return false;\n\t\t// xhigh, max, and ultra are top-tier levels only exposed when a model explicitly maps them.\n\t\tif (level === \"xhigh\" || level === \"max\" || level === \"ultra\") return mapped !== undefined;\n\t\treturn true;\n\t});\n}\n\nexport function clampThinkingLevel<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tlevel: ModelThinkingLevel,\n): ModelThinkingLevel {\n\tconst availableLevels = getSupportedThinkingLevels(model);\n\tif (availableLevels.includes(level)) return level;\n\n\tconst requestedIndex = EXTENDED_THINKING_LEVELS.indexOf(level);\n\tif (requestedIndex === -1) return availableLevels[0] ?? \"off\";\n\n\tfor (let i = requestedIndex; i < EXTENDED_THINKING_LEVELS.length; i++) {\n\t\tconst candidate = EXTENDED_THINKING_LEVELS[i];\n\t\tif (availableLevels.includes(candidate)) return candidate;\n\t}\n\tfor (let i = requestedIndex - 1; i >= 0; i--) {\n\t\tconst candidate = EXTENDED_THINKING_LEVELS[i];\n\t\tif (availableLevels.includes(candidate)) return candidate;\n\t}\n\treturn availableLevels[0] ?? \"off\";\n}\n\n/**\n * Check if two models are equal by comparing both their id and provider.\n * Returns false if either model is null or undefined.\n */\nexport function modelsAreEqual<TApi extends Api>(\n\ta: Model<TApi> | null | undefined,\n\tb: Model<TApi> | null | undefined,\n): boolean {\n\tif (!a || !b) return false;\n\treturn a.id === b.id && a.provider === b.provider;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"google-oauth-shared.d.ts","sourceRoot":"","sources":["../../../src/utils/oauth/google-oauth-shared.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAExE,MAAM,WAAW,qBAAqB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAClG;AAgHD,4CAA4C;AAC5C,wBAAsB,mBAAmB,CACxC,SAAS,EAAE,mBAAmB,EAC9B,MAAM,EAAE,qBAAqB,GAC3B,OAAO,CAAC,gBAAgB,CAAC,CAyE3B;AAED,oCAAoC;AACpC,wBAAsB,kBAAkB,CACvC,MAAM,EAAE,IAAI,CAAC,qBAAqB,EAAE,UAAU,GAAG,cAAc,GAAG,UAAU,CAAC,EAC7E,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,GACf,OAAO,CAAC,gBAAgB,CAAC,CA6B3B","sourcesContent":["/**\n * Shared Google OAuth flow for Gemini CLI and Antigravity.\n *\n * Both use the same authorization-code flow with a local callback server;\n * only client credentials, scopes, and project-discovery logic differ.\n * Adapted from vendor/oh-my-pi for Node.js (no Bun dependencies).\n */\nimport type { Server } from \"node:http\";\nimport { oauthErrorHtml, oauthSuccessHtml } from \"./oauth-page.ts\";\nimport type { OAuthCredentials, OAuthLoginCallbacks } from \"./types.ts\";\n\nexport interface GoogleOAuthFlowConfig {\n\tclientId: string;\n\tclientSecret: string;\n\tauthUrl: string;\n\ttokenUrl: string;\n\tscopes: string[];\n\tcallbackPort: number;\n\tcallbackPath: string;\n\tdiscoverProject: (accessToken: string, onProgress?: (message: string) => void) => Promise<string>;\n}\n\ntype NodeApis = {\n\tcreateServer: typeof import(\"node:http\").createServer;\n};\n\nlet nodeApis: NodeApis | null = null;\nlet nodeApisPromise: Promise<NodeApis> | null = null;\n\nasync function getNodeApis(): Promise<NodeApis> {\n\tif (nodeApis) return nodeApis;\n\tif (!nodeApisPromise) {\n\t\tif (typeof process === \"undefined\" || (!process.versions?.node && !process.versions?.bun)) {\n\t\t\tthrow new Error(\"Google OAuth is only available in Node.js environments\");\n\t\t}\n\t\tnodeApisPromise = import(\"node:http\").then((httpModule) => ({\n\t\t\tcreateServer: httpModule.createServer,\n\t\t}));\n\t}\n\tnodeApis = await nodeApisPromise;\n\treturn nodeApis;\n}\n\nasync function getUserEmail(accessToken: string): Promise<string | undefined> {\n\ttry {\n\t\tconst response = await fetch(\"https://www.googleapis.com/oauth2/v1/userinfo?alt=json\", {\n\t\t\theaders: { Authorization: `Bearer ${accessToken}` },\n\t\t});\n\t\tif (response.ok) {\n\t\t\tconst data = (await response.json()) as { email?: string };\n\t\t\treturn data.email;\n\t\t}\n\t} catch {\n\t\t// Ignore errors, email is optional\n\t}\n\treturn undefined;\n}\n\nfunction startCallbackServer(\n\tport: number,\n\tcallbackPath: string,\n): Promise<{\n\tserver: Server;\n\tredirectUri: string;\n\twaitForCode: () => Promise<{ code: string; state: string } | null>;\n\tcancelWait: () => void;\n}> {\n\tconst hostname = \"127.0.0.1\";\n\tconst redirectUri = `http://${hostname}:${port}${callbackPath}`;\n\n\treturn getNodeApis().then(({ createServer }) => {\n\t\treturn new Promise((resolveServer) => {\n\t\t\tlet settled = false;\n\n\t\t\tconst server = createServer((req, res) => {\n\t\t\t\tif (!req.url?.startsWith(callbackPath)) {\n\t\t\t\t\tres.writeHead(404).end();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst url = new URL(req.url, `http://${req.headers.host || \"localhost\"}`);\n\t\t\t\tconst code = url.searchParams.get(\"code\");\n\t\t\t\tconst state = url.searchParams.get(\"state\");\n\t\t\t\tconst error = url.searchParams.get(\"error\");\n\n\t\t\t\tif (error || !code) {\n\t\t\t\t\tres.writeHead(400, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\t\tres.end(oauthErrorHtml);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tres.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\tres.end(oauthSuccessHtml);\n\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\tsetImmediate(() => {\n\t\t\t\t\t\tresolveServer({\n\t\t\t\t\t\t\tserver,\n\t\t\t\t\t\t\tredirectUri,\n\t\t\t\t\t\t\twaitForCode: () => Promise.resolve({ code, state: state || \"\" }),\n\t\t\t\t\t\t\tcancelWait: () => {},\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tserver.listen(port, hostname, () => {\n\t\t\t\t// Server ready\n\t\t\t});\n\n\t\t\tserver.on(\"error\", () => {\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\t// Try random port fallback\n\t\t\t\t\tconst fallback = createServer(server.listeners(\"request\")[0] as any);\n\t\t\t\t\tfallback.listen(0, hostname, () => {\n\t\t\t\t\t\tconst addr = fallback.address();\n\t\t\t\t\t\tconst actualPort = typeof addr === \"object\" && addr ? addr.port : port;\n\t\t\t\t\t\tconst fallbackUri = `http://${hostname}:${actualPort}${callbackPath}`;\n\t\t\t\t\t\tresolveServer({\n\t\t\t\t\t\t\tserver: fallback,\n\t\t\t\t\t\t\tredirectUri: fallbackUri,\n\t\t\t\t\t\t\twaitForCode: () => Promise.resolve(null),\n\t\t\t\t\t\t\tcancelWait: () => {},\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t});\n}\n\n/** Run the full Google OAuth login flow. */\nexport async function runGoogleOAuthLogin(\n\tcallbacks: OAuthLoginCallbacks,\n\tconfig: GoogleOAuthFlowConfig,\n): Promise<OAuthCredentials> {\n\tconst state = crypto.randomUUID();\n\tconst server = await startCallbackServer(config.callbackPort, config.callbackPath);\n\n\tconst authParams = new URLSearchParams({\n\t\tclient_id: config.clientId,\n\t\tresponse_type: \"code\",\n\t\tredirect_uri: server.redirectUri,\n\t\tscope: config.scopes.join(\" \"),\n\t\tstate,\n\t\taccess_type: \"offline\",\n\t\tprompt: \"consent\",\n\t});\n\n\tconst authUrl = `${config.authUrl}?${authParams.toString()}`;\n\n\tcallbacks.onAuth({ url: authUrl, instructions: \"Complete the sign-in in your browser.\" });\n\tcallbacks.onProgress?.(\"Waiting for browser authentication...\");\n\n\tlet result: { code: string; state: string } | null;\n\ttry {\n\t\tresult = await server.waitForCode();\n\t} finally {\n\t\tserver.cancelWait();\n\t\tserver.server.close();\n\t}\n\n\tif (!result) {\n\t\tthrow new Error(\"Login cancelled or timed out\");\n\t}\n\n\tcallbacks.onProgress?.(\"Exchanging authorization code for tokens...\");\n\n\tconst tokenResponse = await fetch(config.tokenUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n\t\tbody: new URLSearchParams({\n\t\t\tclient_id: config.clientId,\n\t\t\tclient_secret: config.clientSecret,\n\t\t\tcode: result.code,\n\t\t\tgrant_type: \"authorization_code\",\n\t\t\tredirect_uri: server.redirectUri,\n\t\t}),\n\t});\n\n\tif (!tokenResponse.ok) {\n\t\tconst error = await tokenResponse.text();\n\t\tthrow new Error(`Token exchange failed: ${error}`);\n\t}\n\n\tconst tokenData = (await tokenResponse.json()) as {\n\t\taccess_token: string;\n\t\trefresh_token: string;\n\t\texpires_in: number;\n\t};\n\n\tif (!tokenData.refresh_token) {\n\t\tthrow new Error(\"No refresh token received. Please try again.\");\n\t}\n\n\tcallbacks.onProgress?.(\"Getting user info...\");\n\tconst email = await getUserEmail(tokenData.access_token);\n\n\tcallbacks.onProgress?.(\"Discovering project...\");\n\tconst projectId = await config.discoverProject(tokenData.access_token, callbacks.onProgress);\n\n\treturn {\n\t\trefresh: tokenData.refresh_token,\n\t\taccess: tokenData.access_token,\n\t\texpires: Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000,\n\t\tprojectId,\n\t\temail,\n\t};\n}\n\n/** Refresh a Google OAuth token. */\nexport async function refreshGoogleToken(\n\tconfig: Pick<GoogleOAuthFlowConfig, \"clientId\" | \"clientSecret\" | \"tokenUrl\">,\n\trefreshToken: string,\n\tprojectId: string,\n): Promise<OAuthCredentials> {\n\tconst response = await fetch(config.tokenUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n\t\tbody: new URLSearchParams({\n\t\t\tclient_id: config.clientId,\n\t\t\tclient_secret: config.clientSecret,\n\t\t\trefresh_token: refreshToken,\n\t\t\tgrant_type: \"refresh_token\",\n\t\t}),\n\t});\n\n\tif (!response.ok) {\n\t\tconst error = await response.text();\n\t\tthrow new Error(`Google token refresh failed: ${error}`);\n\t}\n\n\tconst data = (await response.json()) as {\n\t\taccess_token: string;\n\t\texpires_in: number;\n\t\trefresh_token?: string;\n\t};\n\n\treturn {\n\t\trefresh: data.refresh_token || refreshToken,\n\t\taccess: data.access_token,\n\t\texpires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,\n\t\tprojectId,\n\t};\n}\n"]}
1
+ {"version":3,"file":"google-oauth-shared.d.ts","sourceRoot":"","sources":["../../../src/utils/oauth/google-oauth-shared.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAExE,MAAM,WAAW,qBAAqB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAClG;AAgHD,4CAA4C;AAC5C,wBAAsB,mBAAmB,CACxC,SAAS,EAAE,mBAAmB,EAC9B,MAAM,EAAE,qBAAqB,GAC3B,OAAO,CAAC,gBAAgB,CAAC,CAyE3B;AAED,oCAAoC;AACpC,wBAAsB,kBAAkB,CACvC,MAAM,EAAE,IAAI,CAAC,qBAAqB,EAAE,UAAU,GAAG,cAAc,GAAG,UAAU,CAAC,EAC7E,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,GACf,OAAO,CAAC,gBAAgB,CAAC,CA6B3B","sourcesContent":["/**\n * Shared Google OAuth flow for Gemini CLI and Antigravity.\n *\n * Both use the same authorization-code flow with a local callback server;\n * only client credentials, scopes, and project-discovery logic differ.\n * Originally adapted from the oh-my-pi project for Node.js (no Bun dependencies);\n * that tree is no longer vendored here (ADR-OMP-010).\n */\nimport type { Server } from \"node:http\";\nimport { oauthErrorHtml, oauthSuccessHtml } from \"./oauth-page.ts\";\nimport type { OAuthCredentials, OAuthLoginCallbacks } from \"./types.ts\";\n\nexport interface GoogleOAuthFlowConfig {\n\tclientId: string;\n\tclientSecret: string;\n\tauthUrl: string;\n\ttokenUrl: string;\n\tscopes: string[];\n\tcallbackPort: number;\n\tcallbackPath: string;\n\tdiscoverProject: (accessToken: string, onProgress?: (message: string) => void) => Promise<string>;\n}\n\ntype NodeApis = {\n\tcreateServer: typeof import(\"node:http\").createServer;\n};\n\nlet nodeApis: NodeApis | null = null;\nlet nodeApisPromise: Promise<NodeApis> | null = null;\n\nasync function getNodeApis(): Promise<NodeApis> {\n\tif (nodeApis) return nodeApis;\n\tif (!nodeApisPromise) {\n\t\tif (typeof process === \"undefined\" || (!process.versions?.node && !process.versions?.bun)) {\n\t\t\tthrow new Error(\"Google OAuth is only available in Node.js environments\");\n\t\t}\n\t\tnodeApisPromise = import(\"node:http\").then((httpModule) => ({\n\t\t\tcreateServer: httpModule.createServer,\n\t\t}));\n\t}\n\tnodeApis = await nodeApisPromise;\n\treturn nodeApis;\n}\n\nasync function getUserEmail(accessToken: string): Promise<string | undefined> {\n\ttry {\n\t\tconst response = await fetch(\"https://www.googleapis.com/oauth2/v1/userinfo?alt=json\", {\n\t\t\theaders: { Authorization: `Bearer ${accessToken}` },\n\t\t});\n\t\tif (response.ok) {\n\t\t\tconst data = (await response.json()) as { email?: string };\n\t\t\treturn data.email;\n\t\t}\n\t} catch {\n\t\t// Ignore errors, email is optional\n\t}\n\treturn undefined;\n}\n\nfunction startCallbackServer(\n\tport: number,\n\tcallbackPath: string,\n): Promise<{\n\tserver: Server;\n\tredirectUri: string;\n\twaitForCode: () => Promise<{ code: string; state: string } | null>;\n\tcancelWait: () => void;\n}> {\n\tconst hostname = \"127.0.0.1\";\n\tconst redirectUri = `http://${hostname}:${port}${callbackPath}`;\n\n\treturn getNodeApis().then(({ createServer }) => {\n\t\treturn new Promise((resolveServer) => {\n\t\t\tlet settled = false;\n\n\t\t\tconst server = createServer((req, res) => {\n\t\t\t\tif (!req.url?.startsWith(callbackPath)) {\n\t\t\t\t\tres.writeHead(404).end();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst url = new URL(req.url, `http://${req.headers.host || \"localhost\"}`);\n\t\t\t\tconst code = url.searchParams.get(\"code\");\n\t\t\t\tconst state = url.searchParams.get(\"state\");\n\t\t\t\tconst error = url.searchParams.get(\"error\");\n\n\t\t\t\tif (error || !code) {\n\t\t\t\t\tres.writeHead(400, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\t\tres.end(oauthErrorHtml);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tres.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\tres.end(oauthSuccessHtml);\n\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\tsetImmediate(() => {\n\t\t\t\t\t\tresolveServer({\n\t\t\t\t\t\t\tserver,\n\t\t\t\t\t\t\tredirectUri,\n\t\t\t\t\t\t\twaitForCode: () => Promise.resolve({ code, state: state || \"\" }),\n\t\t\t\t\t\t\tcancelWait: () => {},\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tserver.listen(port, hostname, () => {\n\t\t\t\t// Server ready\n\t\t\t});\n\n\t\t\tserver.on(\"error\", () => {\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\t// Try random port fallback\n\t\t\t\t\tconst fallback = createServer(server.listeners(\"request\")[0] as any);\n\t\t\t\t\tfallback.listen(0, hostname, () => {\n\t\t\t\t\t\tconst addr = fallback.address();\n\t\t\t\t\t\tconst actualPort = typeof addr === \"object\" && addr ? addr.port : port;\n\t\t\t\t\t\tconst fallbackUri = `http://${hostname}:${actualPort}${callbackPath}`;\n\t\t\t\t\t\tresolveServer({\n\t\t\t\t\t\t\tserver: fallback,\n\t\t\t\t\t\t\tredirectUri: fallbackUri,\n\t\t\t\t\t\t\twaitForCode: () => Promise.resolve(null),\n\t\t\t\t\t\t\tcancelWait: () => {},\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t});\n}\n\n/** Run the full Google OAuth login flow. */\nexport async function runGoogleOAuthLogin(\n\tcallbacks: OAuthLoginCallbacks,\n\tconfig: GoogleOAuthFlowConfig,\n): Promise<OAuthCredentials> {\n\tconst state = crypto.randomUUID();\n\tconst server = await startCallbackServer(config.callbackPort, config.callbackPath);\n\n\tconst authParams = new URLSearchParams({\n\t\tclient_id: config.clientId,\n\t\tresponse_type: \"code\",\n\t\tredirect_uri: server.redirectUri,\n\t\tscope: config.scopes.join(\" \"),\n\t\tstate,\n\t\taccess_type: \"offline\",\n\t\tprompt: \"consent\",\n\t});\n\n\tconst authUrl = `${config.authUrl}?${authParams.toString()}`;\n\n\tcallbacks.onAuth({ url: authUrl, instructions: \"Complete the sign-in in your browser.\" });\n\tcallbacks.onProgress?.(\"Waiting for browser authentication...\");\n\n\tlet result: { code: string; state: string } | null;\n\ttry {\n\t\tresult = await server.waitForCode();\n\t} finally {\n\t\tserver.cancelWait();\n\t\tserver.server.close();\n\t}\n\n\tif (!result) {\n\t\tthrow new Error(\"Login cancelled or timed out\");\n\t}\n\n\tcallbacks.onProgress?.(\"Exchanging authorization code for tokens...\");\n\n\tconst tokenResponse = await fetch(config.tokenUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n\t\tbody: new URLSearchParams({\n\t\t\tclient_id: config.clientId,\n\t\t\tclient_secret: config.clientSecret,\n\t\t\tcode: result.code,\n\t\t\tgrant_type: \"authorization_code\",\n\t\t\tredirect_uri: server.redirectUri,\n\t\t}),\n\t});\n\n\tif (!tokenResponse.ok) {\n\t\tconst error = await tokenResponse.text();\n\t\tthrow new Error(`Token exchange failed: ${error}`);\n\t}\n\n\tconst tokenData = (await tokenResponse.json()) as {\n\t\taccess_token: string;\n\t\trefresh_token: string;\n\t\texpires_in: number;\n\t};\n\n\tif (!tokenData.refresh_token) {\n\t\tthrow new Error(\"No refresh token received. Please try again.\");\n\t}\n\n\tcallbacks.onProgress?.(\"Getting user info...\");\n\tconst email = await getUserEmail(tokenData.access_token);\n\n\tcallbacks.onProgress?.(\"Discovering project...\");\n\tconst projectId = await config.discoverProject(tokenData.access_token, callbacks.onProgress);\n\n\treturn {\n\t\trefresh: tokenData.refresh_token,\n\t\taccess: tokenData.access_token,\n\t\texpires: Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000,\n\t\tprojectId,\n\t\temail,\n\t};\n}\n\n/** Refresh a Google OAuth token. */\nexport async function refreshGoogleToken(\n\tconfig: Pick<GoogleOAuthFlowConfig, \"clientId\" | \"clientSecret\" | \"tokenUrl\">,\n\trefreshToken: string,\n\tprojectId: string,\n): Promise<OAuthCredentials> {\n\tconst response = await fetch(config.tokenUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n\t\tbody: new URLSearchParams({\n\t\t\tclient_id: config.clientId,\n\t\t\tclient_secret: config.clientSecret,\n\t\t\trefresh_token: refreshToken,\n\t\t\tgrant_type: \"refresh_token\",\n\t\t}),\n\t});\n\n\tif (!response.ok) {\n\t\tconst error = await response.text();\n\t\tthrow new Error(`Google token refresh failed: ${error}`);\n\t}\n\n\tconst data = (await response.json()) as {\n\t\taccess_token: string;\n\t\texpires_in: number;\n\t\trefresh_token?: string;\n\t};\n\n\treturn {\n\t\trefresh: data.refresh_token || refreshToken,\n\t\taccess: data.access_token,\n\t\texpires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,\n\t\tprojectId,\n\t};\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"google-oauth-shared.js","sourceRoot":"","sources":["../../../src/utils/oauth/google-oauth-shared.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared Google OAuth flow for Gemini CLI and Antigravity.\n *\n * Both use the same authorization-code flow with a local callback server;\n * only client credentials, scopes, and project-discovery logic differ.\n * Adapted from vendor/oh-my-pi for Node.js (no Bun dependencies).\n */\nimport type { Server } from \"node:http\";\nimport { oauthErrorHtml, oauthSuccessHtml } from \"./oauth-page.ts\";\nimport type { OAuthCredentials, OAuthLoginCallbacks } from \"./types.ts\";\n\nexport interface GoogleOAuthFlowConfig {\n\tclientId: string;\n\tclientSecret: string;\n\tauthUrl: string;\n\ttokenUrl: string;\n\tscopes: string[];\n\tcallbackPort: number;\n\tcallbackPath: string;\n\tdiscoverProject: (accessToken: string, onProgress?: (message: string) => void) => Promise<string>;\n}\n\ntype NodeApis = {\n\tcreateServer: typeof import(\"node:http\").createServer;\n};\n\nlet nodeApis: NodeApis | null = null;\nlet nodeApisPromise: Promise<NodeApis> | null = null;\n\nasync function getNodeApis(): Promise<NodeApis> {\n\tif (nodeApis) return nodeApis;\n\tif (!nodeApisPromise) {\n\t\tif (typeof process === \"undefined\" || (!process.versions?.node && !process.versions?.bun)) {\n\t\t\tthrow new Error(\"Google OAuth is only available in Node.js environments\");\n\t\t}\n\t\tnodeApisPromise = import(\"node:http\").then((httpModule) => ({\n\t\t\tcreateServer: httpModule.createServer,\n\t\t}));\n\t}\n\tnodeApis = await nodeApisPromise;\n\treturn nodeApis;\n}\n\nasync function getUserEmail(accessToken: string): Promise<string | undefined> {\n\ttry {\n\t\tconst response = await fetch(\"https://www.googleapis.com/oauth2/v1/userinfo?alt=json\", {\n\t\t\theaders: { Authorization: `Bearer ${accessToken}` },\n\t\t});\n\t\tif (response.ok) {\n\t\t\tconst data = (await response.json()) as { email?: string };\n\t\t\treturn data.email;\n\t\t}\n\t} catch {\n\t\t// Ignore errors, email is optional\n\t}\n\treturn undefined;\n}\n\nfunction startCallbackServer(\n\tport: number,\n\tcallbackPath: string,\n): Promise<{\n\tserver: Server;\n\tredirectUri: string;\n\twaitForCode: () => Promise<{ code: string; state: string } | null>;\n\tcancelWait: () => void;\n}> {\n\tconst hostname = \"127.0.0.1\";\n\tconst redirectUri = `http://${hostname}:${port}${callbackPath}`;\n\n\treturn getNodeApis().then(({ createServer }) => {\n\t\treturn new Promise((resolveServer) => {\n\t\t\tlet settled = false;\n\n\t\t\tconst server = createServer((req, res) => {\n\t\t\t\tif (!req.url?.startsWith(callbackPath)) {\n\t\t\t\t\tres.writeHead(404).end();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst url = new URL(req.url, `http://${req.headers.host || \"localhost\"}`);\n\t\t\t\tconst code = url.searchParams.get(\"code\");\n\t\t\t\tconst state = url.searchParams.get(\"state\");\n\t\t\t\tconst error = url.searchParams.get(\"error\");\n\n\t\t\t\tif (error || !code) {\n\t\t\t\t\tres.writeHead(400, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\t\tres.end(oauthErrorHtml);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tres.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\tres.end(oauthSuccessHtml);\n\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\tsetImmediate(() => {\n\t\t\t\t\t\tresolveServer({\n\t\t\t\t\t\t\tserver,\n\t\t\t\t\t\t\tredirectUri,\n\t\t\t\t\t\t\twaitForCode: () => Promise.resolve({ code, state: state || \"\" }),\n\t\t\t\t\t\t\tcancelWait: () => {},\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tserver.listen(port, hostname, () => {\n\t\t\t\t// Server ready\n\t\t\t});\n\n\t\t\tserver.on(\"error\", () => {\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\t// Try random port fallback\n\t\t\t\t\tconst fallback = createServer(server.listeners(\"request\")[0] as any);\n\t\t\t\t\tfallback.listen(0, hostname, () => {\n\t\t\t\t\t\tconst addr = fallback.address();\n\t\t\t\t\t\tconst actualPort = typeof addr === \"object\" && addr ? addr.port : port;\n\t\t\t\t\t\tconst fallbackUri = `http://${hostname}:${actualPort}${callbackPath}`;\n\t\t\t\t\t\tresolveServer({\n\t\t\t\t\t\t\tserver: fallback,\n\t\t\t\t\t\t\tredirectUri: fallbackUri,\n\t\t\t\t\t\t\twaitForCode: () => Promise.resolve(null),\n\t\t\t\t\t\t\tcancelWait: () => {},\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t});\n}\n\n/** Run the full Google OAuth login flow. */\nexport async function runGoogleOAuthLogin(\n\tcallbacks: OAuthLoginCallbacks,\n\tconfig: GoogleOAuthFlowConfig,\n): Promise<OAuthCredentials> {\n\tconst state = crypto.randomUUID();\n\tconst server = await startCallbackServer(config.callbackPort, config.callbackPath);\n\n\tconst authParams = new URLSearchParams({\n\t\tclient_id: config.clientId,\n\t\tresponse_type: \"code\",\n\t\tredirect_uri: server.redirectUri,\n\t\tscope: config.scopes.join(\" \"),\n\t\tstate,\n\t\taccess_type: \"offline\",\n\t\tprompt: \"consent\",\n\t});\n\n\tconst authUrl = `${config.authUrl}?${authParams.toString()}`;\n\n\tcallbacks.onAuth({ url: authUrl, instructions: \"Complete the sign-in in your browser.\" });\n\tcallbacks.onProgress?.(\"Waiting for browser authentication...\");\n\n\tlet result: { code: string; state: string } | null;\n\ttry {\n\t\tresult = await server.waitForCode();\n\t} finally {\n\t\tserver.cancelWait();\n\t\tserver.server.close();\n\t}\n\n\tif (!result) {\n\t\tthrow new Error(\"Login cancelled or timed out\");\n\t}\n\n\tcallbacks.onProgress?.(\"Exchanging authorization code for tokens...\");\n\n\tconst tokenResponse = await fetch(config.tokenUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n\t\tbody: new URLSearchParams({\n\t\t\tclient_id: config.clientId,\n\t\t\tclient_secret: config.clientSecret,\n\t\t\tcode: result.code,\n\t\t\tgrant_type: \"authorization_code\",\n\t\t\tredirect_uri: server.redirectUri,\n\t\t}),\n\t});\n\n\tif (!tokenResponse.ok) {\n\t\tconst error = await tokenResponse.text();\n\t\tthrow new Error(`Token exchange failed: ${error}`);\n\t}\n\n\tconst tokenData = (await tokenResponse.json()) as {\n\t\taccess_token: string;\n\t\trefresh_token: string;\n\t\texpires_in: number;\n\t};\n\n\tif (!tokenData.refresh_token) {\n\t\tthrow new Error(\"No refresh token received. Please try again.\");\n\t}\n\n\tcallbacks.onProgress?.(\"Getting user info...\");\n\tconst email = await getUserEmail(tokenData.access_token);\n\n\tcallbacks.onProgress?.(\"Discovering project...\");\n\tconst projectId = await config.discoverProject(tokenData.access_token, callbacks.onProgress);\n\n\treturn {\n\t\trefresh: tokenData.refresh_token,\n\t\taccess: tokenData.access_token,\n\t\texpires: Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000,\n\t\tprojectId,\n\t\temail,\n\t};\n}\n\n/** Refresh a Google OAuth token. */\nexport async function refreshGoogleToken(\n\tconfig: Pick<GoogleOAuthFlowConfig, \"clientId\" | \"clientSecret\" | \"tokenUrl\">,\n\trefreshToken: string,\n\tprojectId: string,\n): Promise<OAuthCredentials> {\n\tconst response = await fetch(config.tokenUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n\t\tbody: new URLSearchParams({\n\t\t\tclient_id: config.clientId,\n\t\t\tclient_secret: config.clientSecret,\n\t\t\trefresh_token: refreshToken,\n\t\t\tgrant_type: \"refresh_token\",\n\t\t}),\n\t});\n\n\tif (!response.ok) {\n\t\tconst error = await response.text();\n\t\tthrow new Error(`Google token refresh failed: ${error}`);\n\t}\n\n\tconst data = (await response.json()) as {\n\t\taccess_token: string;\n\t\texpires_in: number;\n\t\trefresh_token?: string;\n\t};\n\n\treturn {\n\t\trefresh: data.refresh_token || refreshToken,\n\t\taccess: data.access_token,\n\t\texpires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,\n\t\tprojectId,\n\t};\n}\n"]}
1
+ {"version":3,"file":"google-oauth-shared.js","sourceRoot":"","sources":["../../../src/utils/oauth/google-oauth-shared.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared Google OAuth flow for Gemini CLI and Antigravity.\n *\n * Both use the same authorization-code flow with a local callback server;\n * only client credentials, scopes, and project-discovery logic differ.\n * Originally adapted from the oh-my-pi project for Node.js (no Bun dependencies);\n * that tree is no longer vendored here (ADR-OMP-010).\n */\nimport type { Server } from \"node:http\";\nimport { oauthErrorHtml, oauthSuccessHtml } from \"./oauth-page.ts\";\nimport type { OAuthCredentials, OAuthLoginCallbacks } from \"./types.ts\";\n\nexport interface GoogleOAuthFlowConfig {\n\tclientId: string;\n\tclientSecret: string;\n\tauthUrl: string;\n\ttokenUrl: string;\n\tscopes: string[];\n\tcallbackPort: number;\n\tcallbackPath: string;\n\tdiscoverProject: (accessToken: string, onProgress?: (message: string) => void) => Promise<string>;\n}\n\ntype NodeApis = {\n\tcreateServer: typeof import(\"node:http\").createServer;\n};\n\nlet nodeApis: NodeApis | null = null;\nlet nodeApisPromise: Promise<NodeApis> | null = null;\n\nasync function getNodeApis(): Promise<NodeApis> {\n\tif (nodeApis) return nodeApis;\n\tif (!nodeApisPromise) {\n\t\tif (typeof process === \"undefined\" || (!process.versions?.node && !process.versions?.bun)) {\n\t\t\tthrow new Error(\"Google OAuth is only available in Node.js environments\");\n\t\t}\n\t\tnodeApisPromise = import(\"node:http\").then((httpModule) => ({\n\t\t\tcreateServer: httpModule.createServer,\n\t\t}));\n\t}\n\tnodeApis = await nodeApisPromise;\n\treturn nodeApis;\n}\n\nasync function getUserEmail(accessToken: string): Promise<string | undefined> {\n\ttry {\n\t\tconst response = await fetch(\"https://www.googleapis.com/oauth2/v1/userinfo?alt=json\", {\n\t\t\theaders: { Authorization: `Bearer ${accessToken}` },\n\t\t});\n\t\tif (response.ok) {\n\t\t\tconst data = (await response.json()) as { email?: string };\n\t\t\treturn data.email;\n\t\t}\n\t} catch {\n\t\t// Ignore errors, email is optional\n\t}\n\treturn undefined;\n}\n\nfunction startCallbackServer(\n\tport: number,\n\tcallbackPath: string,\n): Promise<{\n\tserver: Server;\n\tredirectUri: string;\n\twaitForCode: () => Promise<{ code: string; state: string } | null>;\n\tcancelWait: () => void;\n}> {\n\tconst hostname = \"127.0.0.1\";\n\tconst redirectUri = `http://${hostname}:${port}${callbackPath}`;\n\n\treturn getNodeApis().then(({ createServer }) => {\n\t\treturn new Promise((resolveServer) => {\n\t\t\tlet settled = false;\n\n\t\t\tconst server = createServer((req, res) => {\n\t\t\t\tif (!req.url?.startsWith(callbackPath)) {\n\t\t\t\t\tres.writeHead(404).end();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst url = new URL(req.url, `http://${req.headers.host || \"localhost\"}`);\n\t\t\t\tconst code = url.searchParams.get(\"code\");\n\t\t\t\tconst state = url.searchParams.get(\"state\");\n\t\t\t\tconst error = url.searchParams.get(\"error\");\n\n\t\t\t\tif (error || !code) {\n\t\t\t\t\tres.writeHead(400, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\t\tres.end(oauthErrorHtml);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tres.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\tres.end(oauthSuccessHtml);\n\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\tsetImmediate(() => {\n\t\t\t\t\t\tresolveServer({\n\t\t\t\t\t\t\tserver,\n\t\t\t\t\t\t\tredirectUri,\n\t\t\t\t\t\t\twaitForCode: () => Promise.resolve({ code, state: state || \"\" }),\n\t\t\t\t\t\t\tcancelWait: () => {},\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tserver.listen(port, hostname, () => {\n\t\t\t\t// Server ready\n\t\t\t});\n\n\t\t\tserver.on(\"error\", () => {\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\t// Try random port fallback\n\t\t\t\t\tconst fallback = createServer(server.listeners(\"request\")[0] as any);\n\t\t\t\t\tfallback.listen(0, hostname, () => {\n\t\t\t\t\t\tconst addr = fallback.address();\n\t\t\t\t\t\tconst actualPort = typeof addr === \"object\" && addr ? addr.port : port;\n\t\t\t\t\t\tconst fallbackUri = `http://${hostname}:${actualPort}${callbackPath}`;\n\t\t\t\t\t\tresolveServer({\n\t\t\t\t\t\t\tserver: fallback,\n\t\t\t\t\t\t\tredirectUri: fallbackUri,\n\t\t\t\t\t\t\twaitForCode: () => Promise.resolve(null),\n\t\t\t\t\t\t\tcancelWait: () => {},\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t});\n}\n\n/** Run the full Google OAuth login flow. */\nexport async function runGoogleOAuthLogin(\n\tcallbacks: OAuthLoginCallbacks,\n\tconfig: GoogleOAuthFlowConfig,\n): Promise<OAuthCredentials> {\n\tconst state = crypto.randomUUID();\n\tconst server = await startCallbackServer(config.callbackPort, config.callbackPath);\n\n\tconst authParams = new URLSearchParams({\n\t\tclient_id: config.clientId,\n\t\tresponse_type: \"code\",\n\t\tredirect_uri: server.redirectUri,\n\t\tscope: config.scopes.join(\" \"),\n\t\tstate,\n\t\taccess_type: \"offline\",\n\t\tprompt: \"consent\",\n\t});\n\n\tconst authUrl = `${config.authUrl}?${authParams.toString()}`;\n\n\tcallbacks.onAuth({ url: authUrl, instructions: \"Complete the sign-in in your browser.\" });\n\tcallbacks.onProgress?.(\"Waiting for browser authentication...\");\n\n\tlet result: { code: string; state: string } | null;\n\ttry {\n\t\tresult = await server.waitForCode();\n\t} finally {\n\t\tserver.cancelWait();\n\t\tserver.server.close();\n\t}\n\n\tif (!result) {\n\t\tthrow new Error(\"Login cancelled or timed out\");\n\t}\n\n\tcallbacks.onProgress?.(\"Exchanging authorization code for tokens...\");\n\n\tconst tokenResponse = await fetch(config.tokenUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n\t\tbody: new URLSearchParams({\n\t\t\tclient_id: config.clientId,\n\t\t\tclient_secret: config.clientSecret,\n\t\t\tcode: result.code,\n\t\t\tgrant_type: \"authorization_code\",\n\t\t\tredirect_uri: server.redirectUri,\n\t\t}),\n\t});\n\n\tif (!tokenResponse.ok) {\n\t\tconst error = await tokenResponse.text();\n\t\tthrow new Error(`Token exchange failed: ${error}`);\n\t}\n\n\tconst tokenData = (await tokenResponse.json()) as {\n\t\taccess_token: string;\n\t\trefresh_token: string;\n\t\texpires_in: number;\n\t};\n\n\tif (!tokenData.refresh_token) {\n\t\tthrow new Error(\"No refresh token received. Please try again.\");\n\t}\n\n\tcallbacks.onProgress?.(\"Getting user info...\");\n\tconst email = await getUserEmail(tokenData.access_token);\n\n\tcallbacks.onProgress?.(\"Discovering project...\");\n\tconst projectId = await config.discoverProject(tokenData.access_token, callbacks.onProgress);\n\n\treturn {\n\t\trefresh: tokenData.refresh_token,\n\t\taccess: tokenData.access_token,\n\t\texpires: Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000,\n\t\tprojectId,\n\t\temail,\n\t};\n}\n\n/** Refresh a Google OAuth token. */\nexport async function refreshGoogleToken(\n\tconfig: Pick<GoogleOAuthFlowConfig, \"clientId\" | \"clientSecret\" | \"tokenUrl\">,\n\trefreshToken: string,\n\tprojectId: string,\n): Promise<OAuthCredentials> {\n\tconst response = await fetch(config.tokenUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n\t\tbody: new URLSearchParams({\n\t\t\tclient_id: config.clientId,\n\t\t\tclient_secret: config.clientSecret,\n\t\t\trefresh_token: refreshToken,\n\t\t\tgrant_type: \"refresh_token\",\n\t\t}),\n\t});\n\n\tif (!response.ok) {\n\t\tconst error = await response.text();\n\t\tthrow new Error(`Google token refresh failed: ${error}`);\n\t}\n\n\tconst data = (await response.json()) as {\n\t\taccess_token: string;\n\t\texpires_in: number;\n\t\trefresh_token?: string;\n\t};\n\n\treturn {\n\t\trefresh: data.refresh_token || refreshToken,\n\t\taccess: data.access_token,\n\t\texpires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,\n\t\tprojectId,\n\t};\n}\n"]}
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from "./types.ts";
10
10
  export declare const QWEN_OAUTH_PROVIDER_ID = "qwen-oauth";
11
- /** Normalize `resource_url` into an absolute base URL ending in `/v1`. */
11
+ /** Normalize an OAuth `resource_url`, rejecting origins that could receive the bearer token. */
12
12
  export declare function normalizeQwenBaseUrl(resourceUrl: string | undefined): string;
13
13
  /** Run the Qwen device-authorization login flow. */
14
14
  export declare function loginQwen(options: {
@@ -1 +1 @@
1
- {"version":3,"file":"qwen.d.ts","sourceRoot":"","sources":["../../../src/utils/oauth/qwen.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEhG,eAAO,MAAM,sBAAsB,eAAe,CAAC;AAiCnD,0EAA0E;AAC1E,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAO5E;AAoGD,oDAAoD;AACpD,wBAAsB,SAAS,CAAC,OAAO,EAAE;IACxC,YAAY,EAAE,mBAAmB,CAAC,cAAc,CAAC,CAAC;IAClD,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAU5B;AAED,kCAAkC;AAClC,wBAAsB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAYtF;AAmBD,eAAO,MAAM,iBAAiB,EAAE,sBAgC/B,CAAC","sourcesContent":["/**\n * Qwen OAuth (Qwen Code subscription).\n *\n * RFC 8628 device-authorization flow with PKCE. The client id, endpoints and\n * scopes mirror the open-source qwen-code CLI. After login the token response\n * carries a `resource_url` pointing at an OpenAI-compatible chat endpoint, so\n * Qwen models are exposed through the standard `openai-completions` API.\n */\n\nimport type { Api, Model } from \"../../types.ts\";\nimport { pollOAuthDeviceCodeFlow } from \"./device-code.ts\";\nimport { generatePKCE } from \"./pkce.ts\";\nimport type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from \"./types.ts\";\n\nexport const QWEN_OAUTH_PROVIDER_ID = \"qwen-oauth\";\n\nconst DEVICE_CODE_URL = \"https://chat.qwen.ai/api/v1/oauth2/device/code\";\nconst TOKEN_URL = \"https://chat.qwen.ai/api/v1/oauth2/token\";\nconst CLIENT_ID = \"f0304373b74a44d2b584a3fb70ca9e56\";\nconst SCOPE = \"openid profile email model.completion\";\nconst DEVICE_GRANT_TYPE = \"urn:ietf:params:oauth:grant-type:device_code\";\nconst DEFAULT_BASE_URL = \"https://dashscope.aliyuncs.com/compatible-mode/v1\";\nconst DEFAULT_VERIFICATION_URI = \"https://chat.qwen.ai\";\nconst DEVICE_CODE_TIMEOUT_SECONDS = 15 * 60;\n\ntype QwenDeviceAuthResponse = {\n\tdevice_code?: string;\n\tuser_code?: string;\n\tverification_uri?: string;\n\tverification_uri_complete?: string;\n\texpires_in?: number;\n\tinterval?: number;\n};\n\ntype QwenTokenResponse = {\n\taccess_token?: string;\n\trefresh_token?: string;\n\texpires_in?: number;\n\tresource_url?: string;\n\terror?: string;\n\terror_description?: string;\n};\n\nfunction formEncode(data: Record<string, string>): string {\n\treturn new URLSearchParams(data).toString();\n}\n\n/** Normalize `resource_url` into an absolute base URL ending in `/v1`. */\nexport function normalizeQwenBaseUrl(resourceUrl: string | undefined): string {\n\tif (!resourceUrl || resourceUrl.trim().length === 0) {\n\t\treturn DEFAULT_BASE_URL;\n\t}\n\tconst withProtocol = /^https?:\\/\\//i.test(resourceUrl) ? resourceUrl : `https://${resourceUrl}`;\n\tconst trimmed = withProtocol.replace(/\\/+$/, \"\");\n\treturn trimmed.endsWith(\"/v1\") ? trimmed : `${trimmed}/v1`;\n}\n\nfunction credentialsFromToken(json: QwenTokenResponse, fallbackRefresh?: string): OAuthCredentials {\n\tif (!json.access_token) {\n\t\tthrow new Error(`Qwen token response missing access_token: ${JSON.stringify(json)}`);\n\t}\n\tconst refresh = json.refresh_token ?? fallbackRefresh;\n\tif (!refresh) {\n\t\tthrow new Error(\"Qwen token response missing refresh_token\");\n\t}\n\tconst expiresInSeconds = typeof json.expires_in === \"number\" ? json.expires_in : 3600;\n\tconst credentials: OAuthCredentials = {\n\t\taccess: json.access_token,\n\t\trefresh,\n\t\texpires: Date.now() + expiresInSeconds * 1000 - 30_000,\n\t};\n\tif (typeof json.resource_url === \"string\" && json.resource_url.length > 0) {\n\t\tcredentials.resource_url = json.resource_url;\n\t}\n\treturn credentials;\n}\n\nasync function requestDeviceCode(challenge: string, signal?: AbortSignal): Promise<QwenDeviceAuthResponse> {\n\tconst response = await fetch(DEVICE_CODE_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\tbody: formEncode({\n\t\t\tclient_id: CLIENT_ID,\n\t\t\tscope: SCOPE,\n\t\t\tcode_challenge: challenge,\n\t\t\tcode_challenge_method: \"S256\",\n\t\t}),\n\t\tsignal,\n\t});\n\tif (!response.ok) {\n\t\tconst text = await response.text().catch(() => \"\");\n\t\tthrow new Error(`Qwen device code request failed (${response.status}): ${text || response.statusText}`);\n\t}\n\tconst json = (await response.json()) as QwenDeviceAuthResponse;\n\tif (!json?.device_code || !json.user_code) {\n\t\tthrow new Error(`Invalid Qwen device code response: ${JSON.stringify(json)}`);\n\t}\n\treturn json;\n}\n\nasync function pollForToken(\n\tdeviceCode: string,\n\tverifier: string,\n\tintervalSeconds: number | undefined,\n\tsignal: AbortSignal | undefined,\n): Promise<OAuthCredentials> {\n\treturn pollOAuthDeviceCodeFlow<OAuthCredentials>({\n\t\tintervalSeconds,\n\t\texpiresInSeconds: DEVICE_CODE_TIMEOUT_SECONDS,\n\t\tsignal,\n\t\tpoll: async () => {\n\t\t\tconst response = await fetch(TOKEN_URL, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\t\t\tbody: formEncode({\n\t\t\t\t\tgrant_type: DEVICE_GRANT_TYPE,\n\t\t\t\t\tclient_id: CLIENT_ID,\n\t\t\t\t\tdevice_code: deviceCode,\n\t\t\t\t\tcode_verifier: verifier,\n\t\t\t\t}),\n\t\t\t\tsignal,\n\t\t\t});\n\n\t\t\tif (response.ok) {\n\t\t\t\tconst json = (await response.json()) as QwenTokenResponse;\n\t\t\t\tif (!json.access_token) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t\t\tmessage: `Qwen token response missing access_token: ${JSON.stringify(json)}`,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\treturn { status: \"complete\", value: credentialsFromToken(json) };\n\t\t\t}\n\n\t\t\tconst text = await response.text().catch(() => \"\");\n\t\t\tlet errorCode: string | undefined;\n\t\t\ttry {\n\t\t\t\terrorCode = (JSON.parse(text) as QwenTokenResponse).error;\n\t\t\t} catch {\n\t\t\t\t// non-JSON error body\n\t\t\t}\n\t\t\tif (errorCode === \"authorization_pending\") {\n\t\t\t\treturn { status: \"pending\" };\n\t\t\t}\n\t\t\tif (errorCode === \"slow_down\") {\n\t\t\t\treturn { status: \"slow_down\" };\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tstatus: \"failed\",\n\t\t\t\tmessage: `Qwen token poll failed (${response.status}): ${text || response.statusText}`,\n\t\t\t};\n\t\t},\n\t});\n}\n\n/** Run the Qwen device-authorization login flow. */\nexport async function loginQwen(options: {\n\tonDeviceCode: OAuthLoginCallbacks[\"onDeviceCode\"];\n\tsignal?: AbortSignal;\n}): Promise<OAuthCredentials> {\n\tconst { verifier, challenge } = await generatePKCE();\n\tconst device = await requestDeviceCode(challenge, options.signal);\n\toptions.onDeviceCode({\n\t\tuserCode: device.user_code ?? \"\",\n\t\tverificationUri: device.verification_uri_complete || device.verification_uri || DEFAULT_VERIFICATION_URI,\n\t\tintervalSeconds: device.interval,\n\t\texpiresInSeconds: device.expires_in ?? DEVICE_CODE_TIMEOUT_SECONDS,\n\t});\n\treturn pollForToken(device.device_code ?? \"\", verifier, device.interval, options.signal);\n}\n\n/** Refresh a Qwen OAuth token. */\nexport async function refreshQwenToken(refreshToken: string): Promise<OAuthCredentials> {\n\tconst response = await fetch(TOKEN_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\tbody: formEncode({ grant_type: \"refresh_token\", client_id: CLIENT_ID, refresh_token: refreshToken }),\n\t});\n\tif (!response.ok) {\n\t\tconst text = await response.text().catch(() => \"\");\n\t\tthrow new Error(`Qwen token refresh failed (${response.status}): ${text || response.statusText}`);\n\t}\n\tconst json = (await response.json()) as QwenTokenResponse;\n\treturn credentialsFromToken(json, refreshToken);\n}\n\nfunction qwenModels(baseUrl: string): Model<\"openai-completions\">[] {\n\tconst shared = {\n\t\tapi: \"openai-completions\" as const,\n\t\tprovider: QWEN_OAUTH_PROVIDER_ID,\n\t\tbaseUrl,\n\t\treasoning: false,\n\t\tinput: [\"text\"] as (\"text\" | \"image\")[],\n\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tcontextWindow: 1_048_576,\n\t\tmaxTokens: 65_536,\n\t};\n\treturn [\n\t\t{ ...shared, id: \"qwen3-coder-plus\", name: \"Qwen3 Coder Plus\" },\n\t\t{ ...shared, id: \"qwen3-coder-flash\", name: \"Qwen3 Coder Flash\" },\n\t];\n}\n\nexport const qwenOAuthProvider: OAuthProviderInterface = {\n\tid: QWEN_OAUTH_PROVIDER_ID,\n\tname: \"Qwen (Qwen Code Subscription)\",\n\tusesCallbackServer: false,\n\n\tasync login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n\t\treturn loginQwen({ onDeviceCode: callbacks.onDeviceCode, signal: callbacks.signal });\n\t},\n\n\tasync refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n\t\tconst refreshed = await refreshQwenToken(credentials.refresh);\n\t\t// The refresh response may omit resource_url; keep the previously resolved endpoint.\n\t\tif (refreshed.resource_url === undefined && typeof credentials.resource_url === \"string\") {\n\t\t\trefreshed.resource_url = credentials.resource_url;\n\t\t}\n\t\treturn refreshed;\n\t},\n\n\tgetApiKey(credentials: OAuthCredentials): string {\n\t\treturn credentials.access;\n\t},\n\n\tmodifyModels(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[] {\n\t\tconst resourceUrl = typeof credentials.resource_url === \"string\" ? credentials.resource_url : undefined;\n\t\tconst baseUrl = normalizeQwenBaseUrl(resourceUrl);\n\t\t// Non-destructive: keep any user-configured qwen-oauth models, only add missing defaults.\n\t\tconst existingIds = new Set(\n\t\t\tmodels.filter((model) => model.provider === QWEN_OAUTH_PROVIDER_ID).map((model) => model.id),\n\t\t);\n\t\tconst additions = qwenModels(baseUrl).filter((model) => !existingIds.has(model.id));\n\t\treturn [...models, ...additions];\n\t},\n};\n"]}
1
+ {"version":3,"file":"qwen.d.ts","sourceRoot":"","sources":["../../../src/utils/oauth/qwen.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEhG,eAAO,MAAM,sBAAsB,eAAe,CAAC;AAkCnD,gGAAgG;AAChG,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CA4B5E;AAkHD,oDAAoD;AACpD,wBAAsB,SAAS,CAAC,OAAO,EAAE;IACxC,YAAY,EAAE,mBAAmB,CAAC,cAAc,CAAC,CAAC;IAClD,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAU5B;AAED,kCAAkC;AAClC,wBAAsB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAWtF;AAmBD,eAAO,MAAM,iBAAiB,EAAE,sBAgC/B,CAAC","sourcesContent":["/**\n * Qwen OAuth (Qwen Code subscription).\n *\n * RFC 8628 device-authorization flow with PKCE. The client id, endpoints and\n * scopes mirror the open-source qwen-code CLI. After login the token response\n * carries a `resource_url` pointing at an OpenAI-compatible chat endpoint, so\n * Qwen models are exposed through the standard `openai-completions` API.\n */\n\nimport type { Api, Model } from \"../../types.ts\";\nimport { pollOAuthDeviceCodeFlow } from \"./device-code.ts\";\nimport { generatePKCE } from \"./pkce.ts\";\nimport type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from \"./types.ts\";\n\nexport const QWEN_OAUTH_PROVIDER_ID = \"qwen-oauth\";\n\nconst DEVICE_CODE_URL = \"https://chat.qwen.ai/api/v1/oauth2/device/code\";\nconst TOKEN_URL = \"https://chat.qwen.ai/api/v1/oauth2/token\";\nconst CLIENT_ID = \"f0304373b74a44d2b584a3fb70ca9e56\";\nconst SCOPE = \"openid profile email model.completion\";\nconst DEVICE_GRANT_TYPE = \"urn:ietf:params:oauth:grant-type:device_code\";\nconst DEFAULT_BASE_URL = \"https://dashscope.aliyuncs.com/compatible-mode/v1\";\nconst DEFAULT_VERIFICATION_URI = \"https://chat.qwen.ai\";\nconst DEVICE_CODE_TIMEOUT_SECONDS = 15 * 60;\nconst QWEN_RESOURCE_HOST_SUFFIXES = [\"qwen.ai\", \"aliyuncs.com\"] as const;\n\ntype QwenDeviceAuthResponse = {\n\tdevice_code?: string;\n\tuser_code?: string;\n\tverification_uri?: string;\n\tverification_uri_complete?: string;\n\texpires_in?: number;\n\tinterval?: number;\n};\n\ntype QwenTokenResponse = {\n\taccess_token?: string;\n\trefresh_token?: string;\n\texpires_in?: number;\n\tresource_url?: string;\n\terror?: string;\n\terror_description?: string;\n};\n\nfunction formEncode(data: Record<string, string>): string {\n\treturn new URLSearchParams(data).toString();\n}\n\n/** Normalize an OAuth `resource_url`, rejecting origins that could receive the bearer token. */\nexport function normalizeQwenBaseUrl(resourceUrl: string | undefined): string {\n\tif (!resourceUrl || resourceUrl.trim().length === 0) {\n\t\treturn DEFAULT_BASE_URL;\n\t}\n\ttry {\n\t\tconst raw = resourceUrl.trim();\n\t\tconst url = new URL(raw.includes(\"://\") ? raw : `https://${raw}`);\n\t\tconst hostname = url.hostname.toLowerCase();\n\t\tconst approvedHost = QWEN_RESOURCE_HOST_SUFFIXES.some(\n\t\t\t(suffix) => hostname === suffix || hostname.endsWith(`.${suffix}`),\n\t\t);\n\t\tif (\n\t\t\turl.protocol !== \"https:\" ||\n\t\t\t!approvedHost ||\n\t\t\turl.username !== \"\" ||\n\t\t\turl.password !== \"\" ||\n\t\t\t(url.port !== \"\" && url.port !== \"443\") ||\n\t\t\turl.search !== \"\" ||\n\t\t\turl.hash !== \"\"\n\t\t) {\n\t\t\tthrow new Error(\"unapproved origin\");\n\t\t}\n\t\tconst path = url.pathname.replace(/\\/+$/, \"\");\n\t\turl.pathname = path.endsWith(\"/v1\") ? path : `${path}/v1`;\n\t\treturn url.toString();\n\t} catch {\n\t\tthrow new Error(\"Qwen resource_url must use an approved HTTPS origin\");\n\t}\n}\n\nfunction normalizeVerificationUri(...candidates: Array<string | undefined>): string {\n\tfor (const candidate of candidates) {\n\t\tif (!candidate) continue;\n\t\ttry {\n\t\t\tconst url = new URL(candidate);\n\t\t\tif (\n\t\t\t\turl.protocol === \"https:\" &&\n\t\t\t\turl.hostname.toLowerCase() === \"chat.qwen.ai\" &&\n\t\t\t\turl.username === \"\" &&\n\t\t\t\turl.password === \"\"\n\t\t\t) {\n\t\t\t\treturn url.toString();\n\t\t\t}\n\t\t} catch {\n\t\t\t// Try the next fixed-origin candidate.\n\t\t}\n\t}\n\treturn DEFAULT_VERIFICATION_URI;\n}\n\nfunction credentialsFromToken(json: QwenTokenResponse, fallbackRefresh?: string): OAuthCredentials {\n\tif (!json.access_token) {\n\t\tthrow new Error(\"Qwen token response missing access_token\");\n\t}\n\tconst refresh = json.refresh_token ?? fallbackRefresh;\n\tif (!refresh) {\n\t\tthrow new Error(\"Qwen token response missing refresh_token\");\n\t}\n\tconst expiresInSeconds = typeof json.expires_in === \"number\" ? json.expires_in : 3600;\n\tconst credentials: OAuthCredentials = {\n\t\taccess: json.access_token,\n\t\trefresh,\n\t\texpires: Date.now() + expiresInSeconds * 1000 - 30_000,\n\t};\n\tif (typeof json.resource_url === \"string\" && json.resource_url.length > 0) {\n\t\tnormalizeQwenBaseUrl(json.resource_url);\n\t\tcredentials.resource_url = json.resource_url;\n\t}\n\treturn credentials;\n}\n\nasync function requestDeviceCode(challenge: string, signal?: AbortSignal): Promise<QwenDeviceAuthResponse> {\n\tconst response = await fetch(DEVICE_CODE_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\tbody: formEncode({\n\t\t\tclient_id: CLIENT_ID,\n\t\t\tscope: SCOPE,\n\t\t\tcode_challenge: challenge,\n\t\t\tcode_challenge_method: \"S256\",\n\t\t}),\n\t\tsignal,\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`Qwen device code request failed (${response.status})`);\n\t}\n\tconst json = (await response.json()) as QwenDeviceAuthResponse;\n\tif (!json?.device_code || !json.user_code) {\n\t\tthrow new Error(\"Invalid Qwen device code response\");\n\t}\n\treturn json;\n}\n\nasync function pollForToken(\n\tdeviceCode: string,\n\tverifier: string,\n\tintervalSeconds: number | undefined,\n\tsignal: AbortSignal | undefined,\n): Promise<OAuthCredentials> {\n\treturn pollOAuthDeviceCodeFlow<OAuthCredentials>({\n\t\tintervalSeconds,\n\t\texpiresInSeconds: DEVICE_CODE_TIMEOUT_SECONDS,\n\t\tsignal,\n\t\tpoll: async () => {\n\t\t\tconst response = await fetch(TOKEN_URL, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\t\t\tbody: formEncode({\n\t\t\t\t\tgrant_type: DEVICE_GRANT_TYPE,\n\t\t\t\t\tclient_id: CLIENT_ID,\n\t\t\t\t\tdevice_code: deviceCode,\n\t\t\t\t\tcode_verifier: verifier,\n\t\t\t\t}),\n\t\t\t\tsignal,\n\t\t\t});\n\n\t\t\tif (response.ok) {\n\t\t\t\tconst json = (await response.json()) as QwenTokenResponse;\n\t\t\t\tif (!json.access_token) {\n\t\t\t\t\treturn { status: \"failed\", message: \"Qwen token response missing access_token\" };\n\t\t\t\t}\n\t\t\t\treturn { status: \"complete\", value: credentialsFromToken(json) };\n\t\t\t}\n\n\t\t\tconst text = await response.text().catch(() => \"\");\n\t\t\tlet errorCode: string | undefined;\n\t\t\ttry {\n\t\t\t\terrorCode = (JSON.parse(text) as QwenTokenResponse).error;\n\t\t\t} catch {\n\t\t\t\t// non-JSON error body\n\t\t\t}\n\t\t\tif (errorCode === \"authorization_pending\") {\n\t\t\t\treturn { status: \"pending\" };\n\t\t\t}\n\t\t\tif (errorCode === \"slow_down\") {\n\t\t\t\treturn { status: \"slow_down\" };\n\t\t\t}\n\t\t\treturn { status: \"failed\", message: `Qwen token poll failed (${response.status})` };\n\t\t},\n\t});\n}\n\n/** Run the Qwen device-authorization login flow. */\nexport async function loginQwen(options: {\n\tonDeviceCode: OAuthLoginCallbacks[\"onDeviceCode\"];\n\tsignal?: AbortSignal;\n}): Promise<OAuthCredentials> {\n\tconst { verifier, challenge } = await generatePKCE();\n\tconst device = await requestDeviceCode(challenge, options.signal);\n\toptions.onDeviceCode({\n\t\tuserCode: device.user_code ?? \"\",\n\t\tverificationUri: normalizeVerificationUri(device.verification_uri_complete, device.verification_uri),\n\t\tintervalSeconds: device.interval,\n\t\texpiresInSeconds: device.expires_in ?? DEVICE_CODE_TIMEOUT_SECONDS,\n\t});\n\treturn pollForToken(device.device_code ?? \"\", verifier, device.interval, options.signal);\n}\n\n/** Refresh a Qwen OAuth token. */\nexport async function refreshQwenToken(refreshToken: string): Promise<OAuthCredentials> {\n\tconst response = await fetch(TOKEN_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\tbody: formEncode({ grant_type: \"refresh_token\", client_id: CLIENT_ID, refresh_token: refreshToken }),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`Qwen token refresh failed (${response.status})`);\n\t}\n\tconst json = (await response.json()) as QwenTokenResponse;\n\treturn credentialsFromToken(json, refreshToken);\n}\n\nfunction qwenModels(baseUrl: string): Model<\"openai-completions\">[] {\n\tconst shared = {\n\t\tapi: \"openai-completions\" as const,\n\t\tprovider: QWEN_OAUTH_PROVIDER_ID,\n\t\tbaseUrl,\n\t\treasoning: false,\n\t\tinput: [\"text\"] as (\"text\" | \"image\")[],\n\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tcontextWindow: 1_048_576,\n\t\tmaxTokens: 65_536,\n\t};\n\treturn [\n\t\t{ ...shared, id: \"qwen3-coder-plus\", name: \"Qwen3 Coder Plus\" },\n\t\t{ ...shared, id: \"qwen3-coder-flash\", name: \"Qwen3 Coder Flash\" },\n\t];\n}\n\nexport const qwenOAuthProvider: OAuthProviderInterface = {\n\tid: QWEN_OAUTH_PROVIDER_ID,\n\tname: \"Qwen (Qwen Code Subscription)\",\n\tusesCallbackServer: false,\n\n\tasync login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n\t\treturn loginQwen({ onDeviceCode: callbacks.onDeviceCode, signal: callbacks.signal });\n\t},\n\n\tasync refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n\t\tconst refreshed = await refreshQwenToken(credentials.refresh);\n\t\t// The refresh response may omit resource_url; keep the previously resolved endpoint.\n\t\tif (refreshed.resource_url === undefined && typeof credentials.resource_url === \"string\") {\n\t\t\trefreshed.resource_url = credentials.resource_url;\n\t\t}\n\t\treturn refreshed;\n\t},\n\n\tgetApiKey(credentials: OAuthCredentials): string {\n\t\treturn credentials.access;\n\t},\n\n\tmodifyModels(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[] {\n\t\tconst resourceUrl = typeof credentials.resource_url === \"string\" ? credentials.resource_url : undefined;\n\t\tconst baseUrl = normalizeQwenBaseUrl(resourceUrl);\n\t\t// Non-destructive: keep any user-configured qwen-oauth models, only add missing defaults.\n\t\tconst existingIds = new Set(\n\t\t\tmodels.filter((model) => model.provider === QWEN_OAUTH_PROVIDER_ID).map((model) => model.id),\n\t\t);\n\t\tconst additions = qwenModels(baseUrl).filter((model) => !existingIds.has(model.id));\n\t\treturn [...models, ...additions];\n\t},\n};\n"]}
@@ -17,21 +17,59 @@ const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
17
17
  const DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1";
18
18
  const DEFAULT_VERIFICATION_URI = "https://chat.qwen.ai";
19
19
  const DEVICE_CODE_TIMEOUT_SECONDS = 15 * 60;
20
+ const QWEN_RESOURCE_HOST_SUFFIXES = ["qwen.ai", "aliyuncs.com"];
20
21
  function formEncode(data) {
21
22
  return new URLSearchParams(data).toString();
22
23
  }
23
- /** Normalize `resource_url` into an absolute base URL ending in `/v1`. */
24
+ /** Normalize an OAuth `resource_url`, rejecting origins that could receive the bearer token. */
24
25
  export function normalizeQwenBaseUrl(resourceUrl) {
25
26
  if (!resourceUrl || resourceUrl.trim().length === 0) {
26
27
  return DEFAULT_BASE_URL;
27
28
  }
28
- const withProtocol = /^https?:\/\//i.test(resourceUrl) ? resourceUrl : `https://${resourceUrl}`;
29
- const trimmed = withProtocol.replace(/\/+$/, "");
30
- return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
29
+ try {
30
+ const raw = resourceUrl.trim();
31
+ const url = new URL(raw.includes("://") ? raw : `https://${raw}`);
32
+ const hostname = url.hostname.toLowerCase();
33
+ const approvedHost = QWEN_RESOURCE_HOST_SUFFIXES.some((suffix) => hostname === suffix || hostname.endsWith(`.${suffix}`));
34
+ if (url.protocol !== "https:" ||
35
+ !approvedHost ||
36
+ url.username !== "" ||
37
+ url.password !== "" ||
38
+ (url.port !== "" && url.port !== "443") ||
39
+ url.search !== "" ||
40
+ url.hash !== "") {
41
+ throw new Error("unapproved origin");
42
+ }
43
+ const path = url.pathname.replace(/\/+$/, "");
44
+ url.pathname = path.endsWith("/v1") ? path : `${path}/v1`;
45
+ return url.toString();
46
+ }
47
+ catch {
48
+ throw new Error("Qwen resource_url must use an approved HTTPS origin");
49
+ }
50
+ }
51
+ function normalizeVerificationUri(...candidates) {
52
+ for (const candidate of candidates) {
53
+ if (!candidate)
54
+ continue;
55
+ try {
56
+ const url = new URL(candidate);
57
+ if (url.protocol === "https:" &&
58
+ url.hostname.toLowerCase() === "chat.qwen.ai" &&
59
+ url.username === "" &&
60
+ url.password === "") {
61
+ return url.toString();
62
+ }
63
+ }
64
+ catch {
65
+ // Try the next fixed-origin candidate.
66
+ }
67
+ }
68
+ return DEFAULT_VERIFICATION_URI;
31
69
  }
32
70
  function credentialsFromToken(json, fallbackRefresh) {
33
71
  if (!json.access_token) {
34
- throw new Error(`Qwen token response missing access_token: ${JSON.stringify(json)}`);
72
+ throw new Error("Qwen token response missing access_token");
35
73
  }
36
74
  const refresh = json.refresh_token ?? fallbackRefresh;
37
75
  if (!refresh) {
@@ -44,6 +82,7 @@ function credentialsFromToken(json, fallbackRefresh) {
44
82
  expires: Date.now() + expiresInSeconds * 1000 - 30_000,
45
83
  };
46
84
  if (typeof json.resource_url === "string" && json.resource_url.length > 0) {
85
+ normalizeQwenBaseUrl(json.resource_url);
47
86
  credentials.resource_url = json.resource_url;
48
87
  }
49
88
  return credentials;
@@ -61,12 +100,11 @@ async function requestDeviceCode(challenge, signal) {
61
100
  signal,
62
101
  });
63
102
  if (!response.ok) {
64
- const text = await response.text().catch(() => "");
65
- throw new Error(`Qwen device code request failed (${response.status}): ${text || response.statusText}`);
103
+ throw new Error(`Qwen device code request failed (${response.status})`);
66
104
  }
67
105
  const json = (await response.json());
68
106
  if (!json?.device_code || !json.user_code) {
69
- throw new Error(`Invalid Qwen device code response: ${JSON.stringify(json)}`);
107
+ throw new Error("Invalid Qwen device code response");
70
108
  }
71
109
  return json;
72
110
  }
@@ -90,10 +128,7 @@ async function pollForToken(deviceCode, verifier, intervalSeconds, signal) {
90
128
  if (response.ok) {
91
129
  const json = (await response.json());
92
130
  if (!json.access_token) {
93
- return {
94
- status: "failed",
95
- message: `Qwen token response missing access_token: ${JSON.stringify(json)}`,
96
- };
131
+ return { status: "failed", message: "Qwen token response missing access_token" };
97
132
  }
98
133
  return { status: "complete", value: credentialsFromToken(json) };
99
134
  }
@@ -111,10 +146,7 @@ async function pollForToken(deviceCode, verifier, intervalSeconds, signal) {
111
146
  if (errorCode === "slow_down") {
112
147
  return { status: "slow_down" };
113
148
  }
114
- return {
115
- status: "failed",
116
- message: `Qwen token poll failed (${response.status}): ${text || response.statusText}`,
117
- };
149
+ return { status: "failed", message: `Qwen token poll failed (${response.status})` };
118
150
  },
119
151
  });
120
152
  }
@@ -124,7 +156,7 @@ export async function loginQwen(options) {
124
156
  const device = await requestDeviceCode(challenge, options.signal);
125
157
  options.onDeviceCode({
126
158
  userCode: device.user_code ?? "",
127
- verificationUri: device.verification_uri_complete || device.verification_uri || DEFAULT_VERIFICATION_URI,
159
+ verificationUri: normalizeVerificationUri(device.verification_uri_complete, device.verification_uri),
128
160
  intervalSeconds: device.interval,
129
161
  expiresInSeconds: device.expires_in ?? DEVICE_CODE_TIMEOUT_SECONDS,
130
162
  });
@@ -138,8 +170,7 @@ export async function refreshQwenToken(refreshToken) {
138
170
  body: formEncode({ grant_type: "refresh_token", client_id: CLIENT_ID, refresh_token: refreshToken }),
139
171
  });
140
172
  if (!response.ok) {
141
- const text = await response.text().catch(() => "");
142
- throw new Error(`Qwen token refresh failed (${response.status}): ${text || response.statusText}`);
173
+ throw new Error(`Qwen token refresh failed (${response.status})`);
143
174
  }
144
175
  const json = (await response.json());
145
176
  return credentialsFromToken(json, refreshToken);
@@ -1 +1 @@
1
- {"version":3,"file":"qwen.js","sourceRoot":"","sources":["../../../src/utils/oauth/qwen.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAGzC,MAAM,CAAC,MAAM,sBAAsB,GAAG,YAAY,CAAC;AAEnD,MAAM,eAAe,GAAG,gDAAgD,CAAC;AACzE,MAAM,SAAS,GAAG,0CAA0C,CAAC;AAC7D,MAAM,SAAS,GAAG,kCAAkC,CAAC;AACrD,MAAM,KAAK,GAAG,uCAAuC,CAAC;AACtD,MAAM,iBAAiB,GAAG,8CAA8C,CAAC;AACzE,MAAM,gBAAgB,GAAG,mDAAmD,CAAC;AAC7E,MAAM,wBAAwB,GAAG,sBAAsB,CAAC;AACxD,MAAM,2BAA2B,GAAG,EAAE,GAAG,EAAE,CAAC;AAoB5C,SAAS,UAAU,CAAC,IAA4B,EAAU;IACzD,OAAO,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;AAAA,CAC5C;AAED,0EAA0E;AAC1E,MAAM,UAAU,oBAAoB,CAAC,WAA+B,EAAU;IAC7E,IAAI,CAAC,WAAW,IAAI,WAAW,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrD,OAAO,gBAAgB,CAAC;IACzB,CAAC;IACD,MAAM,YAAY,GAAG,eAAe,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,WAAW,EAAE,CAAC;IAChG,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACjD,OAAO,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,KAAK,CAAC;AAAA,CAC3D;AAED,SAAS,oBAAoB,CAAC,IAAuB,EAAE,eAAwB,EAAoB;IAClG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,6CAA6C,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACtF,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,IAAI,eAAe,CAAC;IACtD,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAC9D,CAAC;IACD,MAAM,gBAAgB,GAAG,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;IACtF,MAAM,WAAW,GAAqB;QACrC,MAAM,EAAE,IAAI,CAAC,YAAY;QACzB,OAAO;QACP,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,gBAAgB,GAAG,IAAI,GAAG,MAAM;KACtD,CAAC;IACF,IAAI,OAAO,IAAI,CAAC,YAAY,KAAK,QAAQ,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3E,WAAW,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;IAC9C,CAAC;IACD,OAAO,WAAW,CAAC;AAAA,CACnB;AAED,KAAK,UAAU,iBAAiB,CAAC,SAAiB,EAAE,MAAoB,EAAmC;IAC1G,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,eAAe,EAAE;QAC7C,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE,MAAM,EAAE,kBAAkB,EAAE;QAC5F,IAAI,EAAE,UAAU,CAAC;YAChB,SAAS,EAAE,SAAS;YACpB,KAAK,EAAE,KAAK;YACZ,cAAc,EAAE,SAAS;YACzB,qBAAqB,EAAE,MAAM;SAC7B,CAAC;QACF,MAAM;KACN,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QAClB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACnD,MAAM,IAAI,KAAK,CAAC,oCAAoC,QAAQ,CAAC,MAAM,MAAM,IAAI,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;IACzG,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA2B,CAAC;IAC/D,IAAI,CAAC,IAAI,EAAE,WAAW,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CAAC,sCAAsC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,KAAK,UAAU,YAAY,CAC1B,UAAkB,EAClB,QAAgB,EAChB,eAAmC,EACnC,MAA+B,EACH;IAC5B,OAAO,uBAAuB,CAAmB;QAChD,eAAe;QACf,gBAAgB,EAAE,2BAA2B;QAC7C,MAAM;QACN,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YACjB,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE;gBACvC,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE,MAAM,EAAE,kBAAkB,EAAE;gBAC5F,IAAI,EAAE,UAAU,CAAC;oBAChB,UAAU,EAAE,iBAAiB;oBAC7B,SAAS,EAAE,SAAS;oBACpB,WAAW,EAAE,UAAU;oBACvB,aAAa,EAAE,QAAQ;iBACvB,CAAC;gBACF,MAAM;aACN,CAAC,CAAC;YAEH,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAsB,CAAC;gBAC1D,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;oBACxB,OAAO;wBACN,MAAM,EAAE,QAAQ;wBAChB,OAAO,EAAE,6CAA6C,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE;qBAC5E,CAAC;gBACH,CAAC;gBACD,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC;YAClE,CAAC;YAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YACnD,IAAI,SAA6B,CAAC;YAClC,IAAI,CAAC;gBACJ,SAAS,GAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAuB,CAAC,KAAK,CAAC;YAC3D,CAAC;YAAC,MAAM,CAAC;gBACR,sBAAsB;YACvB,CAAC;YACD,IAAI,SAAS,KAAK,uBAAuB,EAAE,CAAC;gBAC3C,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;YAC9B,CAAC;YACD,IAAI,SAAS,KAAK,WAAW,EAAE,CAAC;gBAC/B,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;YAChC,CAAC;YACD,OAAO;gBACN,MAAM,EAAE,QAAQ;gBAChB,OAAO,EAAE,2BAA2B,QAAQ,CAAC,MAAM,MAAM,IAAI,IAAI,QAAQ,CAAC,UAAU,EAAE;aACtF,CAAC;QAAA,CACF;KACD,CAAC,CAAC;AAAA,CACH;AAED,oDAAoD;AACpD,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAG/B,EAA6B;IAC7B,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM,YAAY,EAAE,CAAC;IACrD,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAClE,OAAO,CAAC,YAAY,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC,SAAS,IAAI,EAAE;QAChC,eAAe,EAAE,MAAM,CAAC,yBAAyB,IAAI,MAAM,CAAC,gBAAgB,IAAI,wBAAwB;QACxG,eAAe,EAAE,MAAM,CAAC,QAAQ;QAChC,gBAAgB,EAAE,MAAM,CAAC,UAAU,IAAI,2BAA2B;KAClE,CAAC,CAAC;IACH,OAAO,YAAY,CAAC,MAAM,CAAC,WAAW,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;AAAA,CACzF;AAED,kCAAkC;AAClC,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,YAAoB,EAA6B;IACvF,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE;QACvC,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE,MAAM,EAAE,kBAAkB,EAAE;QAC5F,IAAI,EAAE,UAAU,CAAC,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,YAAY,EAAE,CAAC;KACpG,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QAClB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACnD,MAAM,IAAI,KAAK,CAAC,8BAA8B,QAAQ,CAAC,MAAM,MAAM,IAAI,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;IACnG,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAsB,CAAC;IAC1D,OAAO,oBAAoB,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;AAAA,CAChD;AAED,SAAS,UAAU,CAAC,OAAe,EAAiC;IACnE,MAAM,MAAM,GAAG;QACd,GAAG,EAAE,oBAA6B;QAClC,QAAQ,EAAE,sBAAsB;QAChC,OAAO;QACP,SAAS,EAAE,KAAK;QAChB,KAAK,EAAE,CAAC,MAAM,CAAyB;QACvC,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE;QAC1D,aAAa,EAAE,SAAS;QACxB,SAAS,EAAE,MAAM;KACjB,CAAC;IACF,OAAO;QACN,EAAE,GAAG,MAAM,EAAE,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,kBAAkB,EAAE;QAC/D,EAAE,GAAG,MAAM,EAAE,EAAE,EAAE,mBAAmB,EAAE,IAAI,EAAE,mBAAmB,EAAE;KACjE,CAAC;AAAA,CACF;AAED,MAAM,CAAC,MAAM,iBAAiB,GAA2B;IACxD,EAAE,EAAE,sBAAsB;IAC1B,IAAI,EAAE,+BAA+B;IACrC,kBAAkB,EAAE,KAAK;IAEzB,KAAK,CAAC,KAAK,CAAC,SAA8B,EAA6B;QACtE,OAAO,SAAS,CAAC,EAAE,YAAY,EAAE,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC;IAAA,CACrF;IAED,KAAK,CAAC,YAAY,CAAC,WAA6B,EAA6B;QAC5E,MAAM,SAAS,GAAG,MAAM,gBAAgB,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAC9D,qFAAqF;QACrF,IAAI,SAAS,CAAC,YAAY,KAAK,SAAS,IAAI,OAAO,WAAW,CAAC,YAAY,KAAK,QAAQ,EAAE,CAAC;YAC1F,SAAS,CAAC,YAAY,GAAG,WAAW,CAAC,YAAY,CAAC;QACnD,CAAC;QACD,OAAO,SAAS,CAAC;IAAA,CACjB;IAED,SAAS,CAAC,WAA6B,EAAU;QAChD,OAAO,WAAW,CAAC,MAAM,CAAC;IAAA,CAC1B;IAED,YAAY,CAAC,MAAoB,EAAE,WAA6B,EAAgB;QAC/E,MAAM,WAAW,GAAG,OAAO,WAAW,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC;QACxG,MAAM,OAAO,GAAG,oBAAoB,CAAC,WAAW,CAAC,CAAC;QAClD,0FAA0F;QAC1F,MAAM,WAAW,GAAG,IAAI,GAAG,CAC1B,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,sBAAsB,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAC5F,CAAC;QACF,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;QACpF,OAAO,CAAC,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IAAA,CACjC;CACD,CAAC","sourcesContent":["/**\n * Qwen OAuth (Qwen Code subscription).\n *\n * RFC 8628 device-authorization flow with PKCE. The client id, endpoints and\n * scopes mirror the open-source qwen-code CLI. After login the token response\n * carries a `resource_url` pointing at an OpenAI-compatible chat endpoint, so\n * Qwen models are exposed through the standard `openai-completions` API.\n */\n\nimport type { Api, Model } from \"../../types.ts\";\nimport { pollOAuthDeviceCodeFlow } from \"./device-code.ts\";\nimport { generatePKCE } from \"./pkce.ts\";\nimport type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from \"./types.ts\";\n\nexport const QWEN_OAUTH_PROVIDER_ID = \"qwen-oauth\";\n\nconst DEVICE_CODE_URL = \"https://chat.qwen.ai/api/v1/oauth2/device/code\";\nconst TOKEN_URL = \"https://chat.qwen.ai/api/v1/oauth2/token\";\nconst CLIENT_ID = \"f0304373b74a44d2b584a3fb70ca9e56\";\nconst SCOPE = \"openid profile email model.completion\";\nconst DEVICE_GRANT_TYPE = \"urn:ietf:params:oauth:grant-type:device_code\";\nconst DEFAULT_BASE_URL = \"https://dashscope.aliyuncs.com/compatible-mode/v1\";\nconst DEFAULT_VERIFICATION_URI = \"https://chat.qwen.ai\";\nconst DEVICE_CODE_TIMEOUT_SECONDS = 15 * 60;\n\ntype QwenDeviceAuthResponse = {\n\tdevice_code?: string;\n\tuser_code?: string;\n\tverification_uri?: string;\n\tverification_uri_complete?: string;\n\texpires_in?: number;\n\tinterval?: number;\n};\n\ntype QwenTokenResponse = {\n\taccess_token?: string;\n\trefresh_token?: string;\n\texpires_in?: number;\n\tresource_url?: string;\n\terror?: string;\n\terror_description?: string;\n};\n\nfunction formEncode(data: Record<string, string>): string {\n\treturn new URLSearchParams(data).toString();\n}\n\n/** Normalize `resource_url` into an absolute base URL ending in `/v1`. */\nexport function normalizeQwenBaseUrl(resourceUrl: string | undefined): string {\n\tif (!resourceUrl || resourceUrl.trim().length === 0) {\n\t\treturn DEFAULT_BASE_URL;\n\t}\n\tconst withProtocol = /^https?:\\/\\//i.test(resourceUrl) ? resourceUrl : `https://${resourceUrl}`;\n\tconst trimmed = withProtocol.replace(/\\/+$/, \"\");\n\treturn trimmed.endsWith(\"/v1\") ? trimmed : `${trimmed}/v1`;\n}\n\nfunction credentialsFromToken(json: QwenTokenResponse, fallbackRefresh?: string): OAuthCredentials {\n\tif (!json.access_token) {\n\t\tthrow new Error(`Qwen token response missing access_token: ${JSON.stringify(json)}`);\n\t}\n\tconst refresh = json.refresh_token ?? fallbackRefresh;\n\tif (!refresh) {\n\t\tthrow new Error(\"Qwen token response missing refresh_token\");\n\t}\n\tconst expiresInSeconds = typeof json.expires_in === \"number\" ? json.expires_in : 3600;\n\tconst credentials: OAuthCredentials = {\n\t\taccess: json.access_token,\n\t\trefresh,\n\t\texpires: Date.now() + expiresInSeconds * 1000 - 30_000,\n\t};\n\tif (typeof json.resource_url === \"string\" && json.resource_url.length > 0) {\n\t\tcredentials.resource_url = json.resource_url;\n\t}\n\treturn credentials;\n}\n\nasync function requestDeviceCode(challenge: string, signal?: AbortSignal): Promise<QwenDeviceAuthResponse> {\n\tconst response = await fetch(DEVICE_CODE_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\tbody: formEncode({\n\t\t\tclient_id: CLIENT_ID,\n\t\t\tscope: SCOPE,\n\t\t\tcode_challenge: challenge,\n\t\t\tcode_challenge_method: \"S256\",\n\t\t}),\n\t\tsignal,\n\t});\n\tif (!response.ok) {\n\t\tconst text = await response.text().catch(() => \"\");\n\t\tthrow new Error(`Qwen device code request failed (${response.status}): ${text || response.statusText}`);\n\t}\n\tconst json = (await response.json()) as QwenDeviceAuthResponse;\n\tif (!json?.device_code || !json.user_code) {\n\t\tthrow new Error(`Invalid Qwen device code response: ${JSON.stringify(json)}`);\n\t}\n\treturn json;\n}\n\nasync function pollForToken(\n\tdeviceCode: string,\n\tverifier: string,\n\tintervalSeconds: number | undefined,\n\tsignal: AbortSignal | undefined,\n): Promise<OAuthCredentials> {\n\treturn pollOAuthDeviceCodeFlow<OAuthCredentials>({\n\t\tintervalSeconds,\n\t\texpiresInSeconds: DEVICE_CODE_TIMEOUT_SECONDS,\n\t\tsignal,\n\t\tpoll: async () => {\n\t\t\tconst response = await fetch(TOKEN_URL, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\t\t\tbody: formEncode({\n\t\t\t\t\tgrant_type: DEVICE_GRANT_TYPE,\n\t\t\t\t\tclient_id: CLIENT_ID,\n\t\t\t\t\tdevice_code: deviceCode,\n\t\t\t\t\tcode_verifier: verifier,\n\t\t\t\t}),\n\t\t\t\tsignal,\n\t\t\t});\n\n\t\t\tif (response.ok) {\n\t\t\t\tconst json = (await response.json()) as QwenTokenResponse;\n\t\t\t\tif (!json.access_token) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t\t\tmessage: `Qwen token response missing access_token: ${JSON.stringify(json)}`,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\treturn { status: \"complete\", value: credentialsFromToken(json) };\n\t\t\t}\n\n\t\t\tconst text = await response.text().catch(() => \"\");\n\t\t\tlet errorCode: string | undefined;\n\t\t\ttry {\n\t\t\t\terrorCode = (JSON.parse(text) as QwenTokenResponse).error;\n\t\t\t} catch {\n\t\t\t\t// non-JSON error body\n\t\t\t}\n\t\t\tif (errorCode === \"authorization_pending\") {\n\t\t\t\treturn { status: \"pending\" };\n\t\t\t}\n\t\t\tif (errorCode === \"slow_down\") {\n\t\t\t\treturn { status: \"slow_down\" };\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tstatus: \"failed\",\n\t\t\t\tmessage: `Qwen token poll failed (${response.status}): ${text || response.statusText}`,\n\t\t\t};\n\t\t},\n\t});\n}\n\n/** Run the Qwen device-authorization login flow. */\nexport async function loginQwen(options: {\n\tonDeviceCode: OAuthLoginCallbacks[\"onDeviceCode\"];\n\tsignal?: AbortSignal;\n}): Promise<OAuthCredentials> {\n\tconst { verifier, challenge } = await generatePKCE();\n\tconst device = await requestDeviceCode(challenge, options.signal);\n\toptions.onDeviceCode({\n\t\tuserCode: device.user_code ?? \"\",\n\t\tverificationUri: device.verification_uri_complete || device.verification_uri || DEFAULT_VERIFICATION_URI,\n\t\tintervalSeconds: device.interval,\n\t\texpiresInSeconds: device.expires_in ?? DEVICE_CODE_TIMEOUT_SECONDS,\n\t});\n\treturn pollForToken(device.device_code ?? \"\", verifier, device.interval, options.signal);\n}\n\n/** Refresh a Qwen OAuth token. */\nexport async function refreshQwenToken(refreshToken: string): Promise<OAuthCredentials> {\n\tconst response = await fetch(TOKEN_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\tbody: formEncode({ grant_type: \"refresh_token\", client_id: CLIENT_ID, refresh_token: refreshToken }),\n\t});\n\tif (!response.ok) {\n\t\tconst text = await response.text().catch(() => \"\");\n\t\tthrow new Error(`Qwen token refresh failed (${response.status}): ${text || response.statusText}`);\n\t}\n\tconst json = (await response.json()) as QwenTokenResponse;\n\treturn credentialsFromToken(json, refreshToken);\n}\n\nfunction qwenModels(baseUrl: string): Model<\"openai-completions\">[] {\n\tconst shared = {\n\t\tapi: \"openai-completions\" as const,\n\t\tprovider: QWEN_OAUTH_PROVIDER_ID,\n\t\tbaseUrl,\n\t\treasoning: false,\n\t\tinput: [\"text\"] as (\"text\" | \"image\")[],\n\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tcontextWindow: 1_048_576,\n\t\tmaxTokens: 65_536,\n\t};\n\treturn [\n\t\t{ ...shared, id: \"qwen3-coder-plus\", name: \"Qwen3 Coder Plus\" },\n\t\t{ ...shared, id: \"qwen3-coder-flash\", name: \"Qwen3 Coder Flash\" },\n\t];\n}\n\nexport const qwenOAuthProvider: OAuthProviderInterface = {\n\tid: QWEN_OAUTH_PROVIDER_ID,\n\tname: \"Qwen (Qwen Code Subscription)\",\n\tusesCallbackServer: false,\n\n\tasync login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n\t\treturn loginQwen({ onDeviceCode: callbacks.onDeviceCode, signal: callbacks.signal });\n\t},\n\n\tasync refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n\t\tconst refreshed = await refreshQwenToken(credentials.refresh);\n\t\t// The refresh response may omit resource_url; keep the previously resolved endpoint.\n\t\tif (refreshed.resource_url === undefined && typeof credentials.resource_url === \"string\") {\n\t\t\trefreshed.resource_url = credentials.resource_url;\n\t\t}\n\t\treturn refreshed;\n\t},\n\n\tgetApiKey(credentials: OAuthCredentials): string {\n\t\treturn credentials.access;\n\t},\n\n\tmodifyModels(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[] {\n\t\tconst resourceUrl = typeof credentials.resource_url === \"string\" ? credentials.resource_url : undefined;\n\t\tconst baseUrl = normalizeQwenBaseUrl(resourceUrl);\n\t\t// Non-destructive: keep any user-configured qwen-oauth models, only add missing defaults.\n\t\tconst existingIds = new Set(\n\t\t\tmodels.filter((model) => model.provider === QWEN_OAUTH_PROVIDER_ID).map((model) => model.id),\n\t\t);\n\t\tconst additions = qwenModels(baseUrl).filter((model) => !existingIds.has(model.id));\n\t\treturn [...models, ...additions];\n\t},\n};\n"]}
1
+ {"version":3,"file":"qwen.js","sourceRoot":"","sources":["../../../src/utils/oauth/qwen.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAGzC,MAAM,CAAC,MAAM,sBAAsB,GAAG,YAAY,CAAC;AAEnD,MAAM,eAAe,GAAG,gDAAgD,CAAC;AACzE,MAAM,SAAS,GAAG,0CAA0C,CAAC;AAC7D,MAAM,SAAS,GAAG,kCAAkC,CAAC;AACrD,MAAM,KAAK,GAAG,uCAAuC,CAAC;AACtD,MAAM,iBAAiB,GAAG,8CAA8C,CAAC;AACzE,MAAM,gBAAgB,GAAG,mDAAmD,CAAC;AAC7E,MAAM,wBAAwB,GAAG,sBAAsB,CAAC;AACxD,MAAM,2BAA2B,GAAG,EAAE,GAAG,EAAE,CAAC;AAC5C,MAAM,2BAA2B,GAAG,CAAC,SAAS,EAAE,cAAc,CAAU,CAAC;AAoBzE,SAAS,UAAU,CAAC,IAA4B,EAAU;IACzD,OAAO,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;AAAA,CAC5C;AAED,gGAAgG;AAChG,MAAM,UAAU,oBAAoB,CAAC,WAA+B,EAAU;IAC7E,IAAI,CAAC,WAAW,IAAI,WAAW,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrD,OAAO,gBAAgB,CAAC;IACzB,CAAC;IACD,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,EAAE,CAAC;QAC/B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,GAAG,EAAE,CAAC,CAAC;QAClE,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;QAC5C,MAAM,YAAY,GAAG,2BAA2B,CAAC,IAAI,CACpD,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,KAAK,MAAM,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,MAAM,EAAE,CAAC,CAClE,CAAC;QACF,IACC,GAAG,CAAC,QAAQ,KAAK,QAAQ;YACzB,CAAC,YAAY;YACb,GAAG,CAAC,QAAQ,KAAK,EAAE;YACnB,GAAG,CAAC,QAAQ,KAAK,EAAE;YACnB,CAAC,GAAG,CAAC,IAAI,KAAK,EAAE,IAAI,GAAG,CAAC,IAAI,KAAK,KAAK,CAAC;YACvC,GAAG,CAAC,MAAM,KAAK,EAAE;YACjB,GAAG,CAAC,IAAI,KAAK,EAAE,EACd,CAAC;YACF,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;QACtC,CAAC;QACD,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC9C,GAAG,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,CAAC;QAC1D,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;IACvB,CAAC;IAAC,MAAM,CAAC;QACR,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;IACxE,CAAC;AAAA,CACD;AAED,SAAS,wBAAwB,CAAC,GAAG,UAAqC,EAAU;IACnF,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACpC,IAAI,CAAC,SAAS;YAAE,SAAS;QACzB,IAAI,CAAC;YACJ,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;YAC/B,IACC,GAAG,CAAC,QAAQ,KAAK,QAAQ;gBACzB,GAAG,CAAC,QAAQ,CAAC,WAAW,EAAE,KAAK,cAAc;gBAC7C,GAAG,CAAC,QAAQ,KAAK,EAAE;gBACnB,GAAG,CAAC,QAAQ,KAAK,EAAE,EAClB,CAAC;gBACF,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;YACvB,CAAC;QACF,CAAC;QAAC,MAAM,CAAC;YACR,uCAAuC;QACxC,CAAC;IACF,CAAC;IACD,OAAO,wBAAwB,CAAC;AAAA,CAChC;AAED,SAAS,oBAAoB,CAAC,IAAuB,EAAE,eAAwB,EAAoB;IAClG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;IAC7D,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,IAAI,eAAe,CAAC;IACtD,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAC9D,CAAC;IACD,MAAM,gBAAgB,GAAG,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;IACtF,MAAM,WAAW,GAAqB;QACrC,MAAM,EAAE,IAAI,CAAC,YAAY;QACzB,OAAO;QACP,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,gBAAgB,GAAG,IAAI,GAAG,MAAM;KACtD,CAAC;IACF,IAAI,OAAO,IAAI,CAAC,YAAY,KAAK,QAAQ,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3E,oBAAoB,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACxC,WAAW,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;IAC9C,CAAC;IACD,OAAO,WAAW,CAAC;AAAA,CACnB;AAED,KAAK,UAAU,iBAAiB,CAAC,SAAiB,EAAE,MAAoB,EAAmC;IAC1G,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,eAAe,EAAE;QAC7C,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE,MAAM,EAAE,kBAAkB,EAAE;QAC5F,IAAI,EAAE,UAAU,CAAC;YAChB,SAAS,EAAE,SAAS;YACpB,KAAK,EAAE,KAAK;YACZ,cAAc,EAAE,SAAS;YACzB,qBAAqB,EAAE,MAAM;SAC7B,CAAC;QACF,MAAM;KACN,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CAAC,oCAAoC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;IACzE,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA2B,CAAC;IAC/D,IAAI,CAAC,IAAI,EAAE,WAAW,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,KAAK,UAAU,YAAY,CAC1B,UAAkB,EAClB,QAAgB,EAChB,eAAmC,EACnC,MAA+B,EACH;IAC5B,OAAO,uBAAuB,CAAmB;QAChD,eAAe;QACf,gBAAgB,EAAE,2BAA2B;QAC7C,MAAM;QACN,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YACjB,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE;gBACvC,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE,MAAM,EAAE,kBAAkB,EAAE;gBAC5F,IAAI,EAAE,UAAU,CAAC;oBAChB,UAAU,EAAE,iBAAiB;oBAC7B,SAAS,EAAE,SAAS;oBACpB,WAAW,EAAE,UAAU;oBACvB,aAAa,EAAE,QAAQ;iBACvB,CAAC;gBACF,MAAM;aACN,CAAC,CAAC;YAEH,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAsB,CAAC;gBAC1D,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;oBACxB,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,0CAA0C,EAAE,CAAC;gBAClF,CAAC;gBACD,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC;YAClE,CAAC;YAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YACnD,IAAI,SAA6B,CAAC;YAClC,IAAI,CAAC;gBACJ,SAAS,GAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAuB,CAAC,KAAK,CAAC;YAC3D,CAAC;YAAC,MAAM,CAAC;gBACR,sBAAsB;YACvB,CAAC;YACD,IAAI,SAAS,KAAK,uBAAuB,EAAE,CAAC;gBAC3C,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;YAC9B,CAAC;YACD,IAAI,SAAS,KAAK,WAAW,EAAE,CAAC;gBAC/B,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;YAChC,CAAC;YACD,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,2BAA2B,QAAQ,CAAC,MAAM,GAAG,EAAE,CAAC;QAAA,CACpF;KACD,CAAC,CAAC;AAAA,CACH;AAED,oDAAoD;AACpD,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAG/B,EAA6B;IAC7B,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM,YAAY,EAAE,CAAC;IACrD,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAClE,OAAO,CAAC,YAAY,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC,SAAS,IAAI,EAAE;QAChC,eAAe,EAAE,wBAAwB,CAAC,MAAM,CAAC,yBAAyB,EAAE,MAAM,CAAC,gBAAgB,CAAC;QACpG,eAAe,EAAE,MAAM,CAAC,QAAQ;QAChC,gBAAgB,EAAE,MAAM,CAAC,UAAU,IAAI,2BAA2B;KAClE,CAAC,CAAC;IACH,OAAO,YAAY,CAAC,MAAM,CAAC,WAAW,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;AAAA,CACzF;AAED,kCAAkC;AAClC,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,YAAoB,EAA6B;IACvF,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE;QACvC,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE,MAAM,EAAE,kBAAkB,EAAE;QAC5F,IAAI,EAAE,UAAU,CAAC,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,YAAY,EAAE,CAAC;KACpG,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CAAC,8BAA8B,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;IACnE,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAsB,CAAC;IAC1D,OAAO,oBAAoB,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;AAAA,CAChD;AAED,SAAS,UAAU,CAAC,OAAe,EAAiC;IACnE,MAAM,MAAM,GAAG;QACd,GAAG,EAAE,oBAA6B;QAClC,QAAQ,EAAE,sBAAsB;QAChC,OAAO;QACP,SAAS,EAAE,KAAK;QAChB,KAAK,EAAE,CAAC,MAAM,CAAyB;QACvC,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE;QAC1D,aAAa,EAAE,SAAS;QACxB,SAAS,EAAE,MAAM;KACjB,CAAC;IACF,OAAO;QACN,EAAE,GAAG,MAAM,EAAE,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,kBAAkB,EAAE;QAC/D,EAAE,GAAG,MAAM,EAAE,EAAE,EAAE,mBAAmB,EAAE,IAAI,EAAE,mBAAmB,EAAE;KACjE,CAAC;AAAA,CACF;AAED,MAAM,CAAC,MAAM,iBAAiB,GAA2B;IACxD,EAAE,EAAE,sBAAsB;IAC1B,IAAI,EAAE,+BAA+B;IACrC,kBAAkB,EAAE,KAAK;IAEzB,KAAK,CAAC,KAAK,CAAC,SAA8B,EAA6B;QACtE,OAAO,SAAS,CAAC,EAAE,YAAY,EAAE,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC;IAAA,CACrF;IAED,KAAK,CAAC,YAAY,CAAC,WAA6B,EAA6B;QAC5E,MAAM,SAAS,GAAG,MAAM,gBAAgB,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAC9D,qFAAqF;QACrF,IAAI,SAAS,CAAC,YAAY,KAAK,SAAS,IAAI,OAAO,WAAW,CAAC,YAAY,KAAK,QAAQ,EAAE,CAAC;YAC1F,SAAS,CAAC,YAAY,GAAG,WAAW,CAAC,YAAY,CAAC;QACnD,CAAC;QACD,OAAO,SAAS,CAAC;IAAA,CACjB;IAED,SAAS,CAAC,WAA6B,EAAU;QAChD,OAAO,WAAW,CAAC,MAAM,CAAC;IAAA,CAC1B;IAED,YAAY,CAAC,MAAoB,EAAE,WAA6B,EAAgB;QAC/E,MAAM,WAAW,GAAG,OAAO,WAAW,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC;QACxG,MAAM,OAAO,GAAG,oBAAoB,CAAC,WAAW,CAAC,CAAC;QAClD,0FAA0F;QAC1F,MAAM,WAAW,GAAG,IAAI,GAAG,CAC1B,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,sBAAsB,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAC5F,CAAC;QACF,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;QACpF,OAAO,CAAC,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IAAA,CACjC;CACD,CAAC","sourcesContent":["/**\n * Qwen OAuth (Qwen Code subscription).\n *\n * RFC 8628 device-authorization flow with PKCE. The client id, endpoints and\n * scopes mirror the open-source qwen-code CLI. After login the token response\n * carries a `resource_url` pointing at an OpenAI-compatible chat endpoint, so\n * Qwen models are exposed through the standard `openai-completions` API.\n */\n\nimport type { Api, Model } from \"../../types.ts\";\nimport { pollOAuthDeviceCodeFlow } from \"./device-code.ts\";\nimport { generatePKCE } from \"./pkce.ts\";\nimport type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from \"./types.ts\";\n\nexport const QWEN_OAUTH_PROVIDER_ID = \"qwen-oauth\";\n\nconst DEVICE_CODE_URL = \"https://chat.qwen.ai/api/v1/oauth2/device/code\";\nconst TOKEN_URL = \"https://chat.qwen.ai/api/v1/oauth2/token\";\nconst CLIENT_ID = \"f0304373b74a44d2b584a3fb70ca9e56\";\nconst SCOPE = \"openid profile email model.completion\";\nconst DEVICE_GRANT_TYPE = \"urn:ietf:params:oauth:grant-type:device_code\";\nconst DEFAULT_BASE_URL = \"https://dashscope.aliyuncs.com/compatible-mode/v1\";\nconst DEFAULT_VERIFICATION_URI = \"https://chat.qwen.ai\";\nconst DEVICE_CODE_TIMEOUT_SECONDS = 15 * 60;\nconst QWEN_RESOURCE_HOST_SUFFIXES = [\"qwen.ai\", \"aliyuncs.com\"] as const;\n\ntype QwenDeviceAuthResponse = {\n\tdevice_code?: string;\n\tuser_code?: string;\n\tverification_uri?: string;\n\tverification_uri_complete?: string;\n\texpires_in?: number;\n\tinterval?: number;\n};\n\ntype QwenTokenResponse = {\n\taccess_token?: string;\n\trefresh_token?: string;\n\texpires_in?: number;\n\tresource_url?: string;\n\terror?: string;\n\terror_description?: string;\n};\n\nfunction formEncode(data: Record<string, string>): string {\n\treturn new URLSearchParams(data).toString();\n}\n\n/** Normalize an OAuth `resource_url`, rejecting origins that could receive the bearer token. */\nexport function normalizeQwenBaseUrl(resourceUrl: string | undefined): string {\n\tif (!resourceUrl || resourceUrl.trim().length === 0) {\n\t\treturn DEFAULT_BASE_URL;\n\t}\n\ttry {\n\t\tconst raw = resourceUrl.trim();\n\t\tconst url = new URL(raw.includes(\"://\") ? raw : `https://${raw}`);\n\t\tconst hostname = url.hostname.toLowerCase();\n\t\tconst approvedHost = QWEN_RESOURCE_HOST_SUFFIXES.some(\n\t\t\t(suffix) => hostname === suffix || hostname.endsWith(`.${suffix}`),\n\t\t);\n\t\tif (\n\t\t\turl.protocol !== \"https:\" ||\n\t\t\t!approvedHost ||\n\t\t\turl.username !== \"\" ||\n\t\t\turl.password !== \"\" ||\n\t\t\t(url.port !== \"\" && url.port !== \"443\") ||\n\t\t\turl.search !== \"\" ||\n\t\t\turl.hash !== \"\"\n\t\t) {\n\t\t\tthrow new Error(\"unapproved origin\");\n\t\t}\n\t\tconst path = url.pathname.replace(/\\/+$/, \"\");\n\t\turl.pathname = path.endsWith(\"/v1\") ? path : `${path}/v1`;\n\t\treturn url.toString();\n\t} catch {\n\t\tthrow new Error(\"Qwen resource_url must use an approved HTTPS origin\");\n\t}\n}\n\nfunction normalizeVerificationUri(...candidates: Array<string | undefined>): string {\n\tfor (const candidate of candidates) {\n\t\tif (!candidate) continue;\n\t\ttry {\n\t\t\tconst url = new URL(candidate);\n\t\t\tif (\n\t\t\t\turl.protocol === \"https:\" &&\n\t\t\t\turl.hostname.toLowerCase() === \"chat.qwen.ai\" &&\n\t\t\t\turl.username === \"\" &&\n\t\t\t\turl.password === \"\"\n\t\t\t) {\n\t\t\t\treturn url.toString();\n\t\t\t}\n\t\t} catch {\n\t\t\t// Try the next fixed-origin candidate.\n\t\t}\n\t}\n\treturn DEFAULT_VERIFICATION_URI;\n}\n\nfunction credentialsFromToken(json: QwenTokenResponse, fallbackRefresh?: string): OAuthCredentials {\n\tif (!json.access_token) {\n\t\tthrow new Error(\"Qwen token response missing access_token\");\n\t}\n\tconst refresh = json.refresh_token ?? fallbackRefresh;\n\tif (!refresh) {\n\t\tthrow new Error(\"Qwen token response missing refresh_token\");\n\t}\n\tconst expiresInSeconds = typeof json.expires_in === \"number\" ? json.expires_in : 3600;\n\tconst credentials: OAuthCredentials = {\n\t\taccess: json.access_token,\n\t\trefresh,\n\t\texpires: Date.now() + expiresInSeconds * 1000 - 30_000,\n\t};\n\tif (typeof json.resource_url === \"string\" && json.resource_url.length > 0) {\n\t\tnormalizeQwenBaseUrl(json.resource_url);\n\t\tcredentials.resource_url = json.resource_url;\n\t}\n\treturn credentials;\n}\n\nasync function requestDeviceCode(challenge: string, signal?: AbortSignal): Promise<QwenDeviceAuthResponse> {\n\tconst response = await fetch(DEVICE_CODE_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\tbody: formEncode({\n\t\t\tclient_id: CLIENT_ID,\n\t\t\tscope: SCOPE,\n\t\t\tcode_challenge: challenge,\n\t\t\tcode_challenge_method: \"S256\",\n\t\t}),\n\t\tsignal,\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`Qwen device code request failed (${response.status})`);\n\t}\n\tconst json = (await response.json()) as QwenDeviceAuthResponse;\n\tif (!json?.device_code || !json.user_code) {\n\t\tthrow new Error(\"Invalid Qwen device code response\");\n\t}\n\treturn json;\n}\n\nasync function pollForToken(\n\tdeviceCode: string,\n\tverifier: string,\n\tintervalSeconds: number | undefined,\n\tsignal: AbortSignal | undefined,\n): Promise<OAuthCredentials> {\n\treturn pollOAuthDeviceCodeFlow<OAuthCredentials>({\n\t\tintervalSeconds,\n\t\texpiresInSeconds: DEVICE_CODE_TIMEOUT_SECONDS,\n\t\tsignal,\n\t\tpoll: async () => {\n\t\t\tconst response = await fetch(TOKEN_URL, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\t\t\tbody: formEncode({\n\t\t\t\t\tgrant_type: DEVICE_GRANT_TYPE,\n\t\t\t\t\tclient_id: CLIENT_ID,\n\t\t\t\t\tdevice_code: deviceCode,\n\t\t\t\t\tcode_verifier: verifier,\n\t\t\t\t}),\n\t\t\t\tsignal,\n\t\t\t});\n\n\t\t\tif (response.ok) {\n\t\t\t\tconst json = (await response.json()) as QwenTokenResponse;\n\t\t\t\tif (!json.access_token) {\n\t\t\t\t\treturn { status: \"failed\", message: \"Qwen token response missing access_token\" };\n\t\t\t\t}\n\t\t\t\treturn { status: \"complete\", value: credentialsFromToken(json) };\n\t\t\t}\n\n\t\t\tconst text = await response.text().catch(() => \"\");\n\t\t\tlet errorCode: string | undefined;\n\t\t\ttry {\n\t\t\t\terrorCode = (JSON.parse(text) as QwenTokenResponse).error;\n\t\t\t} catch {\n\t\t\t\t// non-JSON error body\n\t\t\t}\n\t\t\tif (errorCode === \"authorization_pending\") {\n\t\t\t\treturn { status: \"pending\" };\n\t\t\t}\n\t\t\tif (errorCode === \"slow_down\") {\n\t\t\t\treturn { status: \"slow_down\" };\n\t\t\t}\n\t\t\treturn { status: \"failed\", message: `Qwen token poll failed (${response.status})` };\n\t\t},\n\t});\n}\n\n/** Run the Qwen device-authorization login flow. */\nexport async function loginQwen(options: {\n\tonDeviceCode: OAuthLoginCallbacks[\"onDeviceCode\"];\n\tsignal?: AbortSignal;\n}): Promise<OAuthCredentials> {\n\tconst { verifier, challenge } = await generatePKCE();\n\tconst device = await requestDeviceCode(challenge, options.signal);\n\toptions.onDeviceCode({\n\t\tuserCode: device.user_code ?? \"\",\n\t\tverificationUri: normalizeVerificationUri(device.verification_uri_complete, device.verification_uri),\n\t\tintervalSeconds: device.interval,\n\t\texpiresInSeconds: device.expires_in ?? DEVICE_CODE_TIMEOUT_SECONDS,\n\t});\n\treturn pollForToken(device.device_code ?? \"\", verifier, device.interval, options.signal);\n}\n\n/** Refresh a Qwen OAuth token. */\nexport async function refreshQwenToken(refreshToken: string): Promise<OAuthCredentials> {\n\tconst response = await fetch(TOKEN_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\tbody: formEncode({ grant_type: \"refresh_token\", client_id: CLIENT_ID, refresh_token: refreshToken }),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`Qwen token refresh failed (${response.status})`);\n\t}\n\tconst json = (await response.json()) as QwenTokenResponse;\n\treturn credentialsFromToken(json, refreshToken);\n}\n\nfunction qwenModels(baseUrl: string): Model<\"openai-completions\">[] {\n\tconst shared = {\n\t\tapi: \"openai-completions\" as const,\n\t\tprovider: QWEN_OAUTH_PROVIDER_ID,\n\t\tbaseUrl,\n\t\treasoning: false,\n\t\tinput: [\"text\"] as (\"text\" | \"image\")[],\n\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tcontextWindow: 1_048_576,\n\t\tmaxTokens: 65_536,\n\t};\n\treturn [\n\t\t{ ...shared, id: \"qwen3-coder-plus\", name: \"Qwen3 Coder Plus\" },\n\t\t{ ...shared, id: \"qwen3-coder-flash\", name: \"Qwen3 Coder Flash\" },\n\t];\n}\n\nexport const qwenOAuthProvider: OAuthProviderInterface = {\n\tid: QWEN_OAUTH_PROVIDER_ID,\n\tname: \"Qwen (Qwen Code Subscription)\",\n\tusesCallbackServer: false,\n\n\tasync login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n\t\treturn loginQwen({ onDeviceCode: callbacks.onDeviceCode, signal: callbacks.signal });\n\t},\n\n\tasync refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n\t\tconst refreshed = await refreshQwenToken(credentials.refresh);\n\t\t// The refresh response may omit resource_url; keep the previously resolved endpoint.\n\t\tif (refreshed.resource_url === undefined && typeof credentials.resource_url === \"string\") {\n\t\t\trefreshed.resource_url = credentials.resource_url;\n\t\t}\n\t\treturn refreshed;\n\t},\n\n\tgetApiKey(credentials: OAuthCredentials): string {\n\t\treturn credentials.access;\n\t},\n\n\tmodifyModels(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[] {\n\t\tconst resourceUrl = typeof credentials.resource_url === \"string\" ? credentials.resource_url : undefined;\n\t\tconst baseUrl = normalizeQwenBaseUrl(resourceUrl);\n\t\t// Non-destructive: keep any user-configured qwen-oauth models, only add missing defaults.\n\t\tconst existingIds = new Set(\n\t\t\tmodels.filter((model) => model.provider === QWEN_OAUTH_PROVIDER_ID).map((model) => model.id),\n\t\t);\n\t\tconst additions = qwenModels(baseUrl).filter((model) => !existingIds.has(model.id));\n\t\treturn [...models, ...additions];\n\t},\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omk-ai",
3
- "version": "0.96.2",
3
+ "version": "0.97.0",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",