dsh-opendesign-corouter 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1752,8 +1752,8 @@ var CoRouterAdapter = class extends LlmAdapter {
1752
1752
  async *#stream(options, config, row) {
1753
1753
  const signal = AbortSignal.any([combinedSignal(options.signal, config.requestTimeoutMs), this.#lifetime.signal]);
1754
1754
  signal.throwIfAborted();
1755
- const raw = await this.options.apiKey(config.apiKeyEnv);
1756
- if (!raw) throw new LlmError10("\u672A\u914D\u7F6E CoRouter API Key\uFF1B\u8BF7\u5728\u63D2\u4EF6\u8BBE\u7F6E\u4E2D\u4FDD\u5B58\u6216\u8BBE\u7F6E\u5BF9\u5E94\u73AF\u5883\u53D8\u91CF", "MISSING_CREDENTIAL");
1755
+ const raw = await this.options.apiKey(config.apiKeyEnv, config.baseURL);
1756
+ if (!raw) throw new LlmError10("\u672A\u914D\u7F6E\u5F53\u524D\u7F51\u5173\u7684\u63A8\u7406\u51ED\u636E\uFF1B\u8BF7\u5B8C\u6210\u8BBE\u5907\u6388\u6743\u6216\u914D\u7F6E\u5BF9\u5E94 API Key", "MISSING_CREDENTIAL");
1757
1757
  const key = assertUsableApiKey(raw, "dsh-opendesign-corouter", config.apiKeyEnv);
1758
1758
  const setting = config.modelSettings?.[row.id] ?? {};
1759
1759
  const protocolId = config.apiProtocol ?? "chat-completions";
@@ -2005,7 +2005,7 @@ function isOfficialApiUrl(raw) {
2005
2005
  if (url.username || url.password) return false;
2006
2006
  if (url.search || url.hash) return false;
2007
2007
  if (url.pathname !== "/" && url.pathname !== "") return false;
2008
- return url.hostname.toLowerCase() === API_HOST;
2008
+ return url.hostname.toLowerCase() === API_HOST && !url.port;
2009
2009
  }
2010
2010
  function normalizeApiUrl(raw) {
2011
2011
  const value = typeof raw === "string" ? raw.trim() : "";
@@ -2080,6 +2080,24 @@ async function readDesktopCredential(deps = {}) {
2080
2080
  }
2081
2081
  return parseDesktopConfigText(text2, profile);
2082
2082
  }
2083
+ function deviceGrant(value) {
2084
+ if (!record(value) || value.kind !== "device" || typeof value.controlKey !== "string" || typeof value.runtimeKey !== "string") return;
2085
+ try {
2086
+ const controlKey = safeSecret(value.controlKey), runtimeKey = safeSecret(value.runtimeKey);
2087
+ if ([controlKey, runtimeKey].some((key) => key.length > 8192 || /\s/u.test(key))) return;
2088
+ return { kind: "device", controlKey, runtimeKey, ...typeof value.savedAt === "string" ? { savedAt: value.savedAt } : {} };
2089
+ } catch {
2090
+ return void 0;
2091
+ }
2092
+ }
2093
+ function isOfficialRuntimeUrl(raw) {
2094
+ try {
2095
+ const url = new URL(raw);
2096
+ return url.origin === "https://amr-link.open-design.ai" && !url.username && !url.password && !url.search && !url.hash;
2097
+ } catch {
2098
+ return false;
2099
+ }
2100
+ }
2083
2101
  function cookieHeader(raw) {
2084
2102
  const value = raw.trim();
2085
2103
  if (!value || !value.includes("=")) return void 0;
@@ -2100,6 +2118,10 @@ function desktopSession(credential, apiUrl = credential.apiUrl ?? DEFAULT_API_UR
2100
2118
  ...credential.plan ? { plan: credential.plan } : {}
2101
2119
  };
2102
2120
  }
2121
+ function deviceSession(tokens) {
2122
+ const controlKey = typeof tokens === "string" ? tokens : tokens.controlKey;
2123
+ return { headers: { authorization: `Bearer ${safeSecret(controlKey)}` }, source: "login", via: "login", apiUrl: DEFAULT_API_URL, credentialType: "device" };
2124
+ }
2103
2125
  function cookieSession(cookie, source, apiUrl = DEFAULT_API_URL) {
2104
2126
  const header = cookieHeader(cookie);
2105
2127
  if (!header) return void 0;
@@ -2140,10 +2162,15 @@ async function resolveSession(deps = {}) {
2140
2162
  } catch {
2141
2163
  grant = void 0;
2142
2164
  }
2143
- const expiry = typeof grant?.expiresAt === "number" && Number.isFinite(grant.expiresAt) ? grant.expiresAt : 0;
2144
- if (grant && (expiry === 0 || expiry > now())) {
2145
- const session = cookieSession(grant.cookie, "login");
2146
- if (session) return { state: "ready", session, warnings };
2165
+ if (grant && "kind" in grant && grant.kind === "device") {
2166
+ const device = deviceGrant(grant);
2167
+ if (device) return { state: "ready", session: deviceSession(device), warnings };
2168
+ } else if (grant && "cookie" in grant) {
2169
+ const expiry = typeof grant.expiresAt === "number" && Number.isFinite(grant.expiresAt) ? grant.expiresAt : 0;
2170
+ if (expiry === 0 || expiry > now()) {
2171
+ const session = cookieSession(grant.cookie, "login");
2172
+ if (session) return { state: "ready", session, warnings };
2173
+ }
2147
2174
  }
2148
2175
  }
2149
2176
  if (deps.resolveRef) {
@@ -2164,61 +2191,207 @@ async function resolveSession(deps = {}) {
2164
2191
 
2165
2192
  // src/login.ts
2166
2193
  import { randomUUID } from "node:crypto";
2167
- var CONSOLE_ORIGIN = "https://open-design.ai";
2168
- var CONSOLE_DASHBOARD_URL = `${CONSOLE_ORIGIN}/cloud/dashboard`;
2169
- var API_COOKIE_URL = "https://amr-api.open-design.ai/api/v1/workspaces/current";
2170
- function extractConsoleSession(cookies, now = Date.now()) {
2171
- const usable = cookies.filter((c) => typeof c.name === "string" && c.name.length > 0 && typeof c.value === "string" && c.value.length > 0 && (c.expires === void 0 || c.expires === -1 || c.expires * 1e3 > now));
2172
- if (!usable.length) return null;
2173
- const declared = usable.map((c) => c.expires).filter((expires) => typeof expires === "number" && Number.isFinite(expires) && expires > 0);
2194
+
2195
+ // src/device-auth.ts
2196
+ var DEVICE_AUTH_ENDPOINT = "https://amr-api.open-design.ai/api/v1/cli/device-authorizations";
2197
+ var DEVICE_AUTH_PLUGIN_VERSION = "0.1.4";
2198
+ var DEVICE_REQUEST_TIMEOUT_MS = 2e4;
2199
+ var DEVICE_RESPONSE_MAX_BYTES = 65536;
2200
+ var DEVICE_POLL_MIN_MS = 1e3;
2201
+ var DEVICE_POLL_MAX_MS = 3e5;
2202
+ var DeviceAuthError = class extends Error {
2203
+ constructor(code) {
2204
+ super("\u8BBE\u5907\u6388\u6743\u672A\u5B8C\u6210\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB");
2205
+ this.code = code;
2206
+ }
2207
+ code;
2208
+ };
2209
+ function validateActivationUrl(value, userCode) {
2210
+ if (typeof value !== "string" || value.length > 512 || !/^https:\/\/open-design\.ai\/cloud\/cli\/activate(?:\?[^#]*)?$/u.test(value) || /[\s\\]/u.test(value)) return null;
2211
+ try {
2212
+ const url = new URL(value);
2213
+ if (url.origin !== "https://open-design.ai" || url.pathname !== "/cloud/cli/activate" || url.username || url.password || url.hash) return null;
2214
+ const entries = [...url.searchParams];
2215
+ if (entries.length > 1) return null;
2216
+ if (entries.length) {
2217
+ const entry = entries[0];
2218
+ if (!["code", "user_code"].includes(entry[0]) || !validUserCode(entry[1]) || userCode !== void 0 && entry[1] !== userCode) return null;
2219
+ }
2220
+ return url.href;
2221
+ } catch {
2222
+ return null;
2223
+ }
2224
+ }
2225
+ function object(value) {
2226
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2227
+ }
2228
+ function validUserCode(value) {
2229
+ return typeof value === "string" && /^[a-z0-9][a-z0-9-]{3,31}$/iu.test(value);
2230
+ }
2231
+ function secret(value) {
2232
+ return typeof value === "string" && value.length > 0 && value.length <= 8192 && /^[\x21-\x7e]+$/u.test(value);
2233
+ }
2234
+ function parseDeviceAuthorization(value) {
2235
+ if (!object(value) || typeof value.deviceId !== "string" || !/^[a-z0-9_-]{1,200}$/iu.test(value.deviceId) || !secret(value.deviceSecret) || !validUserCode(value.userCode)) throw new DeviceAuthError("opendesign/login-response");
2236
+ const activationUrl = validateActivationUrl(value.activationUrl, value.userCode);
2237
+ if (!activationUrl || value.userCode.toLowerCase().includes(value.deviceSecret.toLowerCase()) || decodeURIComponent(activationUrl).toLowerCase().includes(value.deviceSecret.toLowerCase())) throw new DeviceAuthError("opendesign/login-response");
2238
+ const expiresAt = typeof value.expiresAt === "string" && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,3})?(?:Z|[+-]\d\d:\d\d)$/u.test(value.expiresAt) ? Date.parse(value.expiresAt) : NaN;
2239
+ if (!Number.isFinite(expiresAt) || typeof value.pollIntervalSeconds !== "number" || !Number.isFinite(value.pollIntervalSeconds) || value.pollIntervalSeconds <= 0) throw new DeviceAuthError("opendesign/login-response");
2174
2240
  return {
2175
- cookie: usable.map((c) => `${c.name}=${c.value}`).join("; "),
2176
- expiresAt: declared.length ? Math.min(...declared) * 1e3 : 0
2241
+ deviceId: value.deviceId,
2242
+ deviceSecret: value.deviceSecret,
2243
+ userCode: value.userCode,
2244
+ activationUrl,
2245
+ expiresAt,
2246
+ pollMs: Math.max(DEVICE_POLL_MIN_MS, Math.min(DEVICE_POLL_MAX_MS, value.pollIntervalSeconds * 1e3))
2177
2247
  };
2178
2248
  }
2179
- function consoleSite(overrides = {}) {
2180
- const origin = overrides.origin ?? CONSOLE_ORIGIN;
2181
- return {
2182
- origin,
2183
- loginUrl: overrides.loginUrl ?? (origin === CONSOLE_ORIGIN ? CONSOLE_DASHBOARD_URL : `${origin}/cloud/dashboard`),
2184
- cookieUrls: overrides.cookieUrls ?? [API_COOKIE_URL],
2185
- extract: extractConsoleSession
2249
+ function parseDevicePoll(value) {
2250
+ if (!object(value)) throw new DeviceAuthError("opendesign/login-response");
2251
+ const state = typeof value.status === "string" ? value.status.toLowerCase() : "";
2252
+ if (["expired", "cancelled", "canceled", "denied", "rejected", "failed", "error"].includes(state)) throw new DeviceAuthError(state === "expired" ? "opendesign/login-expired" : "opendesign/login-denied");
2253
+ if (value.error !== void 0 && value.error !== null && value.error !== "") throw new DeviceAuthError("opendesign/login-response");
2254
+ const user = object(value.user) ? value.user : {};
2255
+ const topPresent = value.controlKey !== void 0 || value.runtimeKey !== void 0;
2256
+ const source = topPresent ? value : user;
2257
+ if (source.controlKey !== void 0 || source.runtimeKey !== void 0) {
2258
+ if (!secret(source.controlKey) || !secret(source.runtimeKey)) throw new DeviceAuthError("opendesign/login-incomplete");
2259
+ if (topPresent && (user.controlKey !== void 0 && user.controlKey !== source.controlKey || user.runtimeKey !== void 0 && user.runtimeKey !== source.runtimeKey)) throw new DeviceAuthError("opendesign/login-response");
2260
+ return { state: "success", tokens: { controlKey: source.controlKey, runtimeKey: source.runtimeKey } };
2261
+ }
2262
+ if (state === "pending" || state === "authorization_pending") return { state: "pending" };
2263
+ throw new DeviceAuthError("opendesign/login-response");
2264
+ }
2265
+ function validRequestUrl(value) {
2266
+ return value === DEVICE_AUTH_ENDPOINT || /^https:\/\/amr-api\.open-design\.ai\/api\/v1\/cli\/device-authorizations\/[a-zA-Z0-9_-]{1,200}\/token$/u.test(value);
2267
+ }
2268
+ async function postDeviceJson(fetcher, url, body, signal, timeoutMs = DEVICE_REQUEST_TIMEOUT_MS) {
2269
+ if (!validRequestUrl(url)) throw new DeviceAuthError("opendesign/login-url");
2270
+ signal.throwIfAborted();
2271
+ const controller = new AbortController();
2272
+ let timedOut = false;
2273
+ let timer;
2274
+ let rejectAbort = () => void 0;
2275
+ const aborted = new Promise((_resolve, reject) => {
2276
+ rejectAbort = reject;
2277
+ });
2278
+ const cancel = () => {
2279
+ controller.abort();
2280
+ rejectAbort(new DeviceAuthError(timedOut ? "opendesign/login-request-timeout" : "opendesign/login-cancelled"));
2186
2281
  };
2282
+ signal.addEventListener("abort", cancel, { once: true });
2283
+ timer = setTimeout(() => {
2284
+ timedOut = true;
2285
+ cancel();
2286
+ }, Math.max(1, Math.min(DEVICE_REQUEST_TIMEOUT_MS, timeoutMs)));
2287
+ const read = async () => {
2288
+ let response;
2289
+ try {
2290
+ response = await fetcher(url, {
2291
+ method: "POST",
2292
+ redirect: "error",
2293
+ credentials: "omit",
2294
+ referrerPolicy: "no-referrer",
2295
+ headers: { accept: "application/json", "content-type": "application/json" },
2296
+ body: JSON.stringify(body),
2297
+ signal: controller.signal
2298
+ });
2299
+ } catch {
2300
+ throw new DeviceAuthError("opendesign/login-network");
2301
+ }
2302
+ if (controller.signal.aborted) {
2303
+ void response.body?.cancel().catch(() => void 0);
2304
+ throw new DeviceAuthError("opendesign/login-cancelled");
2305
+ }
2306
+ const rejectResponse = (code) => {
2307
+ void response.body?.cancel().catch(() => void 0);
2308
+ throw new DeviceAuthError(code);
2309
+ };
2310
+ if (response.redirected || response.url && response.url !== url || response.status >= 300 && response.status < 400) return rejectResponse("opendesign/login-redirect");
2311
+ if (response.status === 401 || response.status === 403) return rejectResponse("opendesign/login-denied");
2312
+ if (response.status === 404 || response.status === 410) return rejectResponse("opendesign/login-expired");
2313
+ if (!response.ok) return rejectResponse("opendesign/login-network");
2314
+ const declared = response.headers.get("content-length");
2315
+ if (declared !== null && (!/^\d+$/u.test(declared) || Number(declared) > DEVICE_RESPONSE_MAX_BYTES)) return rejectResponse("opendesign/login-response-size");
2316
+ if (!response.body) throw new DeviceAuthError("opendesign/login-response");
2317
+ const reader = response.body.getReader();
2318
+ const stopReading = () => {
2319
+ void reader.cancel().catch(() => void 0);
2320
+ };
2321
+ controller.signal.addEventListener("abort", stopReading, { once: true });
2322
+ const chunks = [];
2323
+ let length = 0;
2324
+ try {
2325
+ for (; ; ) {
2326
+ const part = await reader.read();
2327
+ if (controller.signal.aborted) throw new DeviceAuthError("opendesign/login-cancelled");
2328
+ if (part.done) break;
2329
+ length += part.value.byteLength;
2330
+ if (length > DEVICE_RESPONSE_MAX_BYTES) {
2331
+ stopReading();
2332
+ throw new DeviceAuthError("opendesign/login-response-size");
2333
+ }
2334
+ chunks.push(part.value);
2335
+ }
2336
+ const bytes = new Uint8Array(length);
2337
+ let offset = 0;
2338
+ for (const chunk of chunks) {
2339
+ bytes.set(chunk, offset);
2340
+ offset += chunk.length;
2341
+ }
2342
+ try {
2343
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
2344
+ } catch {
2345
+ throw new DeviceAuthError("opendesign/login-response");
2346
+ }
2347
+ } finally {
2348
+ controller.signal.removeEventListener("abort", stopReading);
2349
+ reader.releaseLock();
2350
+ }
2351
+ };
2352
+ try {
2353
+ return await Promise.race([read(), aborted]);
2354
+ } catch (error) {
2355
+ if (error instanceof DeviceAuthError) throw error;
2356
+ throw new DeviceAuthError("opendesign/login-network");
2357
+ } finally {
2358
+ if (timer !== void 0) clearTimeout(timer);
2359
+ signal.removeEventListener("abort", cancel);
2360
+ }
2187
2361
  }
2362
+
2363
+ // src/login.ts
2188
2364
  var LoginError = class extends Error {
2189
2365
  constructor(code) {
2190
- super(code === "opendesign/login-state" ? "\u767B\u5F55\u4EFB\u52A1\u4E0D\u5B58\u5728\u6216\u72B6\u6001\u5DF2\u6539\u53D8" : code === "opendesign/login-disposed" ? "\u767B\u5F55\u670D\u52A1\u5DF2\u5378\u8F7D" : "\u65E0\u6CD5\u6355\u83B7\u5F53\u524D\u8BBE\u7F6E\u72B6\u6001\uFF0C\u767B\u5F55\u5DF2\u53D6\u6D88");
2366
+ super(code === "opendesign/login-disposed" ? "\u767B\u5F55\u670D\u52A1\u5DF2\u5378\u8F7D" : "\u767B\u5F55\u4EFB\u52A1\u4E0D\u5B58\u5728\u6216\u72B6\u6001\u5DF2\u6539\u53D8");
2191
2367
  this.code = code;
2192
2368
  }
2193
2369
  code;
2194
2370
  };
2195
2371
  var ACTIVE = /* @__PURE__ */ new Set(["launching", "waiting", "verifying"]);
2196
- var DEFAULT_TIMEOUT_MS = 3e5;
2197
- var DEFAULT_POLL_MS = 1e3;
2198
- var FATAL_COMMIT_CODES = /* @__PURE__ */ new Set(["opendesign/revision-conflict", "opendesign/invalid-request", "opendesign/config", "opendesign/login-disposed"]);
2199
- var CHROME_LAUNCH_OPTIONS = {
2200
- channel: "chrome",
2201
- headless: false,
2202
- ignoreDefaultArgs: ["--enable-automation"],
2203
- args: ["--disable-blink-features=AutomationControlled"]
2204
- };
2372
+ var MAX_TIMEOUT_MS = 3e5;
2373
+ var SAFE_HOST_CODES = /* @__PURE__ */ new Set(["opendesign/revision-conflict", "opendesign/invalid-request", "opendesign/config", "opendesign/login-disposed"]);
2205
2374
  var ConsoleLogin = class {
2206
- job = null;
2207
- disposed = false;
2208
- callbacks;
2209
- options;
2210
- now;
2211
- delay;
2212
2375
  constructor(callbacks, options = {}) {
2213
2376
  this.callbacks = callbacks;
2214
2377
  this.options = options;
2215
2378
  this.now = options.now ?? Date.now;
2216
2379
  this.delay = options.delay ?? sleep;
2380
+ this.fetcher = options.fetcher ?? fetch;
2217
2381
  }
2218
- /** Idempotent, credential-free view of the current attempt. */
2382
+ callbacks;
2383
+ options;
2384
+ job = null;
2385
+ disposed = false;
2386
+ now;
2387
+ delay;
2388
+ fetcher;
2389
+ /** Explicit public allowlist. Codes are never available after waiting or expiry. */
2219
2390
  status() {
2220
2391
  if (!this.job) return { jobId: null, state: "idle" };
2221
- const s = this.job.status;
2392
+ const job = this.job;
2393
+ if (job.status.state === "waiting" && this.now() >= job.deadline) this.expire(job);
2394
+ const s = job.status;
2222
2395
  return {
2223
2396
  jobId: s.jobId,
2224
2397
  state: s.state,
@@ -2226,240 +2399,177 @@ var ConsoleLogin = class {
2226
2399
  ...s.finishedAt ? { finishedAt: s.finishedAt } : {},
2227
2400
  ...s.message ? { message: s.message } : {},
2228
2401
  ...s.errorCode ? { errorCode: s.errorCode } : {},
2229
- ...s.browserOpenFailed ? { browserOpenFailed: true } : {}
2402
+ ...s.state === "waiting" && this.now() < job.deadline ? {
2403
+ ...s.activationUrl ? { activationUrl: s.activationUrl } : {},
2404
+ ...s.userCode ? { userCode: s.userCode } : {},
2405
+ ...s.expiresAt ? { expiresAt: s.expiresAt } : {}
2406
+ } : {}
2230
2407
  };
2231
2408
  }
2232
- /**
2233
- * Queue one attempt and return immediately: the RPC that starts a login must not wait on a
2234
- * browser window or on a human. Re-entrant calls while an attempt is live return that
2235
- * attempt's status instead of opening a second browser.
2236
- */
2409
+ /** Synchronous/non-blocking and idempotent while capture/network/commit is pending. */
2237
2410
  start(request) {
2238
2411
  if (this.disposed) throw new LoginError("opendesign/login-disposed");
2412
+ this.status();
2239
2413
  if (this.job && ACTIVE.has(this.job.status.state)) return this.status();
2414
+ const started = this.now();
2415
+ const requested = this.options.timeoutMs;
2416
+ const timeoutMs = typeof requested === "number" && Number.isFinite(requested) && requested > 0 ? Math.min(MAX_TIMEOUT_MS, requested) : MAX_TIMEOUT_MS;
2240
2417
  const job = {
2241
2418
  id: randomUUID(),
2242
- startedAt: new Date(this.now()).toISOString(),
2243
2419
  controller: new AbortController(),
2244
- expectedRevision: request.expectedRevision,
2245
- status: { jobId: "", state: "launching", message: "\u6B63\u5728\u6253\u5F00\u4E34\u65F6 Chrome \u767B\u5F55\u7A97\u53E3" },
2420
+ deadline: started + timeoutMs,
2421
+ status: { jobId: null, state: "launching", startedAt: new Date(started).toISOString(), message: "\u6B63\u5728\u7533\u8BF7\u5B98\u65B9\u8BBE\u5907\u6388\u6743\u7801" },
2246
2422
  captured: void 0,
2247
- capturedAt: void 0,
2248
- verifying: false,
2249
2423
  committing: false,
2250
2424
  cancelRequested: false
2251
2425
  };
2252
2426
  job.status.jobId = job.id;
2253
- job.status.startedAt = job.startedAt;
2254
2427
  this.job = job;
2255
- job.timer = setTimeout(() => this.requestCancellation(job, "\u767B\u5F55\u5DF2\u8D85\u8FC7\u4E94\u5206\u949F\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB"), this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
2256
- job.timer.unref?.();
2257
- job.capturedAt = Promise.resolve().then(() => this.callbacks.capture(job.expectedRevision)).then((captured) => {
2258
- job.captured = captured;
2259
- return captured;
2260
- });
2261
- void job.capturedAt.then(() => this.run(job)).catch((error) => {
2262
- if (this.alive(job)) this.finish(job, "error", "\u767B\u5F55\u65E0\u6CD5\u5F00\u59CB\uFF0C\u8BF7\u91CD\u8BD5", errorCode(error) ?? "opendesign/login-capture");
2428
+ this.armExpiry(job);
2429
+ void Promise.resolve().then(async () => {
2430
+ if (!this.alive(job)) return;
2431
+ try {
2432
+ job.captured = await this.callbacks.capture(request.expectedRevision);
2433
+ } catch (error) {
2434
+ if (this.alive(job)) this.finish(job, "error", "\u65E0\u6CD5\u6355\u83B7\u5F53\u524D\u8BBE\u7F6E\u72B6\u6001\uFF0C\u8BF7\u91CD\u65B0\u8BFB\u53D6\u540E\u91CD\u8BD5", hostCode(error) ?? "opendesign/login-capture");
2435
+ return;
2436
+ }
2437
+ if (this.alive(job)) await this.run(job);
2438
+ }).catch((error) => {
2439
+ if (this.alive(job)) this.finish(job, "error", failureMessage(error), error instanceof DeviceAuthError ? error.code : "opendesign/login-network");
2263
2440
  });
2264
2441
  return this.status();
2265
2442
  }
2266
- /**
2267
- * Abandon the attempt. While `commit` is in flight this only aborts the signal and records
2268
- * the request: the Host's atomic store decides, because the commit is the linearization
2269
- * point. A cancel that wins that race reports `cancelled`; one that loses reports `success`
2270
- * and the cookie really is stored.
2271
- */
2272
2443
  cancel(request) {
2273
- const job = this.job;
2274
- if (!job || job.status.jobId !== request.jobId) return this.status();
2275
- this.requestCancellation(job, "\u767B\u5F55\u5DF2\u53D6\u6D88\uFF0C\u672A\u4FDD\u5B58 Cookie");
2444
+ if (this.job?.id === request.jobId) this.requestCancellation(this.job, "\u767B\u5F55\u5DF2\u53D6\u6D88\uFF0C\u672A\u4FDD\u5B58\u6388\u6743");
2276
2445
  return this.status();
2277
2446
  }
2278
- /** Plugin teardown: abort the attempt and close the browser. */
2279
2447
  dispose() {
2280
- const job = this.job;
2281
- if (job) this.requestCancellation(job, "\u670D\u52A1\u5DF2\u5378\u8F7D\uFF0C\u767B\u5F55\u5DF2\u53D6\u6D88");
2282
2448
  this.disposed = true;
2449
+ if (this.job) this.requestCancellation(this.job, "\u670D\u52A1\u5DF2\u5378\u8F7D\uFF0C\u767B\u5F55\u5DF2\u53D6\u6D88");
2450
+ }
2451
+ alive(job) {
2452
+ return this.job === job && !this.disposed && !job.controller.signal.aborted && ACTIVE.has(job.status.state);
2453
+ }
2454
+ stopTimer(job) {
2455
+ if (job.timer !== void 0) {
2456
+ clearTimeout(job.timer);
2457
+ job.timer = void 0;
2458
+ }
2459
+ }
2460
+ armExpiry(job) {
2461
+ this.stopTimer(job);
2462
+ job.timer = setTimeout(() => this.expire(job), Math.max(1, job.deadline - this.now()));
2463
+ job.timer.unref?.();
2464
+ }
2465
+ clearCode(job) {
2466
+ delete job.status.activationUrl;
2467
+ delete job.status.userCode;
2468
+ delete job.status.expiresAt;
2469
+ }
2470
+ expire(job) {
2471
+ if (job.committing) this.requestCancellation(job, "\u6388\u6743\u5DF2\u8FC7\u671F\uFF0C\u672A\u4FDD\u5B58\u6388\u6743");
2472
+ else this.finish(job, "error", "\u8BBE\u5907\u6388\u6743\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB", "opendesign/login-expired");
2283
2473
  }
2284
- /** Every cancellation source must let an in-flight atomic store decide the final outcome. */
2285
2474
  requestCancellation(job, message) {
2286
2475
  if (this.job !== job || !ACTIVE.has(job.status.state)) return;
2476
+ this.clearCode(job);
2287
2477
  if (!job.committing) {
2288
2478
  this.finish(job, "cancelled", message);
2289
2479
  return;
2290
2480
  }
2291
2481
  if (!job.cancelRequested) {
2292
2482
  job.cancellationMessage = message;
2293
- job.status.message = "\u6B63\u5728\u786E\u8BA4\u4F1A\u8BDD\u4FDD\u5B58\u7ED3\u679C";
2483
+ job.status.message = "\u6B63\u5728\u786E\u8BA4\u6388\u6743\u4FDD\u5B58\u7ED3\u679C";
2294
2484
  }
2295
2485
  job.cancelRequested = true;
2296
2486
  this.stopTimer(job);
2297
2487
  job.controller.abort();
2298
- void this.close(job);
2299
- }
2300
- alive(job) {
2301
- if (this.disposed || this.job !== job) return false;
2302
- if (job.controller.signal.aborted && !job.committing) return false;
2303
- return job.status.state !== "success" && job.status.state !== "cancelled" && job.status.state !== "error";
2304
2488
  }
2305
- stopTimer(job) {
2306
- if (job.timer) {
2307
- clearTimeout(job.timer);
2308
- job.timer = void 0;
2309
- }
2310
- }
2311
- async close(job) {
2312
- const browser = job.browser;
2313
- job.browser = void 0;
2314
- try {
2315
- await browser?.close();
2316
- } catch {
2317
- }
2318
- }
2319
- finish(job, state, message, errorCode2) {
2320
- if (this.job !== job) return;
2321
- if (job.status.state === "success" || job.status.state === "cancelled" || job.status.state === "error") return;
2489
+ finish(job, state, message, errorCode) {
2490
+ if (this.job !== job || !ACTIVE.has(job.status.state)) return;
2322
2491
  job.status.state = state;
2323
2492
  job.status.finishedAt = new Date(this.now()).toISOString();
2324
2493
  job.status.message = message;
2325
- if (errorCode2) job.status.errorCode = errorCode2;
2326
- job.secret = void 0;
2494
+ if (errorCode) job.status.errorCode = errorCode;
2495
+ this.clearCode(job);
2327
2496
  this.stopTimer(job);
2328
2497
  job.controller.abort();
2329
- void this.close(job);
2330
- }
2331
- async resolveBrowser(job) {
2332
- if (!this.alive(job)) return null;
2333
- const launch = this.options.launch ?? defaultLaunch;
2334
- const browser = await launch(job.controller.signal);
2335
- if (!browser) return null;
2336
- job.browser = browser;
2337
- browser.on?.("disconnected", () => this.requestCancellation(job, "\u767B\u5F55\u7A97\u53E3\u5DF2\u5173\u95ED\uFF0C\u672A\u4FDD\u5B58 Cookie"));
2338
- return browser;
2339
2498
  }
2340
2499
  async run(job) {
2341
- const site = this.options.site ?? consoleSite();
2342
- const browser = await this.resolveBrowser(job);
2343
- if (!browser) return;
2344
- if (!this.alive(job)) {
2345
- await this.close(job);
2346
- return;
2347
- }
2348
- const context = await browser.newContext();
2349
- if (!this.alive(job)) {
2350
- await this.close(job);
2351
- return;
2352
- }
2353
- const page = await context.newPage();
2354
- page.on?.("close", () => this.requestCancellation(job, "\u767B\u5F55\u7A97\u53E3\u5DF2\u5173\u95ED\uFF0C\u672A\u4FDD\u5B58 Cookie"));
2355
- if (!this.alive(job)) {
2356
- await this.close(job);
2500
+ const signal = job.controller.signal;
2501
+ const raw = await postDeviceJson(this.fetcher, DEVICE_AUTH_ENDPOINT, {
2502
+ // The third-party protocol's official-CLI version is NOT our version. Compatibility
2503
+ // of this actual plugin version requires a real-account check outside offline tests.
2504
+ cliVersion: DEVICE_AUTH_PLUGIN_VERSION,
2505
+ host: "DSH CoRouter",
2506
+ profile: "prod"
2507
+ }, signal, job.deadline - this.now());
2508
+ if (!this.alive(job)) return;
2509
+ const device = parseDeviceAuthorization(raw);
2510
+ job.deadline = Math.min(job.deadline, device.expiresAt);
2511
+ if (this.now() >= job.deadline) {
2512
+ this.expire(job);
2357
2513
  return;
2358
2514
  }
2359
- try {
2360
- await page.goto(site.loginUrl, { waitUntil: "domcontentloaded", timeout: 3e4 });
2361
- } catch {
2362
- if (!this.alive(job)) return;
2363
- try {
2364
- await page.goto(site.origin, { waitUntil: "domcontentloaded", timeout: 3e4 });
2365
- } catch {
2366
- job.status.browserOpenFailed = true;
2367
- }
2368
- }
2369
- if (!this.alive(job)) return;
2515
+ this.armExpiry(job);
2370
2516
  job.status.state = "waiting";
2371
- job.status.message = "\u8BF7\u5728\u5B98\u65B9\u7A97\u53E3\u81EA\u884C\u5B8C\u6210\u767B\u5F55\uFF1B\u8D26\u6237\u9A8C\u8BC1\u901A\u8FC7\u540E\u4FDD\u5B58\uFF0C\u989D\u5EA6\u72B6\u6001\u53E6\u884C\u663E\u793A";
2372
- let nextAttempt = 0;
2373
- let failures = 0;
2374
- const pollMs = this.options.pollMs ?? DEFAULT_POLL_MS;
2517
+ job.status.message = "\u8BF7\u5728\u5E38\u7528\u6D4F\u89C8\u5668\u6253\u5F00\u5B98\u65B9\u6388\u6743\u9875\uFF0C\u4F7F\u7528\u5DF2\u4FDD\u5B58\u7684\u5BC6\u7801\u767B\u5F55\u5E76\u786E\u8BA4\u8BBE\u5907\u7801";
2518
+ job.status.activationUrl = device.activationUrl;
2519
+ job.status.userCode = device.userCode;
2520
+ job.status.expiresAt = new Date(job.deadline).toISOString();
2375
2521
  while (this.alive(job)) {
2376
- if (!job.verifying) {
2377
- const session = site.extract(await context.cookies(site.cookieUrls), this.now());
2378
- if (!this.alive(job)) return;
2379
- if (session && this.now() >= nextAttempt) {
2380
- job.verifying = true;
2381
- job.status.state = "verifying";
2382
- job.status.message = "\u6B63\u5728\u9A8C\u8BC1\u767B\u5F55\u548C\u8BA2\u9605\u989D\u5EA6\uFF08\u5C1A\u672A\u4FDD\u5B58\uFF09";
2383
- try {
2384
- const verified = await this.callbacks.verify(session.cookie, job.controller.signal);
2385
- if (!this.alive(job)) return;
2386
- if (verified.usageError || !verified.balanceOk) {
2387
- job.status.state = "waiting";
2388
- job.status.message = "\u5DF2\u8BFB\u53D6\u8D26\u6237\uFF0C\u4F46\u8BA2\u9605\u989D\u5EA6\u5C1A\u672A\u5B8C\u6574\u9A8C\u8BC1\uFF1B\u5C1A\u672A\u4FDD\u5B58\uFF0C\u8BF7\u786E\u8BA4\u5957\u9910\u6216\u7EE7\u7EED\u767B\u5F55";
2389
- nextAttempt = this.now() + 15e3;
2390
- } else {
2391
- const outcome = await this.commit(job, session);
2392
- if (outcome !== "retry") return;
2393
- failures += 1;
2394
- nextAttempt = this.now() + Math.max(5e3, Math.min(6e4, 2e3 * 2 ** Math.min(failures, 5)));
2395
- }
2396
- } catch (error) {
2397
- if (!this.alive(job)) return;
2398
- failures += 1;
2399
- nextAttempt = this.now() + Math.max(5e3, Math.min(6e4, 2e3 * 2 ** Math.min(failures, 5)), retryAfterMs(error));
2400
- job.status.state = "waiting";
2401
- job.status.message = errorCode(error) === "opendesign/auth" ? "\u767B\u5F55\u5C1A\u672A\u751F\u6548\uFF0C\u8BF7\u7EE7\u7EED\u5728\u5B98\u65B9\u7A97\u53E3\u5B8C\u6210\u767B\u5F55\uFF1B\u672A\u4FDD\u5B58" : "\u989D\u5EA6\u9A8C\u8BC1\u6682\u672A\u6210\u529F\uFF0C\u5C06\u9650\u9891\u91CD\u8BD5\uFF1B\u672A\u4FDD\u5B58";
2402
- } finally {
2403
- job.verifying = false;
2404
- }
2405
- }
2522
+ await this.delay(Math.min(device.pollMs, Math.max(1, job.deadline - this.now())), signal);
2523
+ if (!this.alive(job)) return;
2524
+ if (this.now() >= job.deadline) {
2525
+ this.expire(job);
2526
+ return;
2527
+ }
2528
+ const result = parseDevicePoll(await postDeviceJson(
2529
+ this.fetcher,
2530
+ `${DEVICE_AUTH_ENDPOINT}/${device.deviceId}/token`,
2531
+ { deviceSecret: device.deviceSecret },
2532
+ signal,
2533
+ job.deadline - this.now()
2534
+ ));
2535
+ if (!this.alive(job)) return;
2536
+ if (this.now() >= job.deadline) {
2537
+ this.expire(job);
2538
+ return;
2539
+ }
2540
+ if (result.state === "success") {
2541
+ await this.commit(job, result.tokens);
2542
+ return;
2406
2543
  }
2407
- await this.delay(pollMs, job.controller.signal);
2408
2544
  }
2409
2545
  }
2410
- /**
2411
- * Persist the verified session; `commit` resolving is the linearization point. Returns
2412
- * `retry` when the store rejected the write transiently, so the caller decides when to try
2413
- * again instead of claiming a success that did not happen.
2414
- */
2415
- async commit(job, session) {
2416
- if (job.committing || !this.alive(job)) return "cancelled";
2546
+ async commit(job, tokens) {
2547
+ if (!this.alive(job) || job.committing) return;
2417
2548
  job.committing = true;
2549
+ this.clearCode(job);
2418
2550
  job.status.state = "verifying";
2419
- job.status.message = "\u767B\u5F55\u5DF2\u9A8C\u8BC1\uFF0C\u6B63\u5728\u4FDD\u5B58\u4F1A\u8BDD";
2551
+ job.status.message = "\u5B98\u65B9\u6388\u6743\u5DF2\u5B8C\u6210\uFF0C\u6B63\u5728\u4FDD\u5B58\u6388\u6743";
2420
2552
  try {
2421
- await this.callbacks.commit(session.cookie, job.captured, job.controller.signal, session.expiresAt);
2553
+ await this.callbacks.commit(tokens, job.captured, job.controller.signal);
2422
2554
  job.committing = false;
2423
- this.finish(job, "success", "\u767B\u5F55\u53CA\u8BA2\u9605\u989D\u5EA6\u5DF2\u9A8C\u8BC1\uFF0CCookie \u5DF2\u4FDD\u5B58");
2424
- return "success";
2555
+ this.finish(job, "success", "\u8BBE\u5907\u6388\u6743\u5DF2\u5B8C\u6210\u5E76\u4FDD\u5B58\uFF1B\u989D\u5EA6\u72B6\u6001\u53E6\u884C\u663E\u793A");
2425
2556
  } catch (error) {
2426
2557
  job.committing = false;
2427
- if (job.cancelRequested || job.controller.signal.aborted) {
2428
- this.finish(job, "cancelled", job.cancellationMessage ?? "\u767B\u5F55\u5DF2\u53D6\u6D88\uFF0C\u672A\u4FDD\u5B58 Cookie");
2429
- return "cancelled";
2430
- }
2431
- if (FATAL_COMMIT_CODES.has(errorCode(error) ?? "")) {
2432
- this.finish(job, "error", "\u8BBE\u7F6E\u5DF2\u6539\u53D8\u6216\u65E0\u6CD5\u4FDD\u5B58\uFF0C\u8BF7\u91CD\u65B0\u8BFB\u53D6\u540E\u91CD\u8BD5", errorCode(error));
2433
- return "error";
2434
- }
2435
- job.status.state = "waiting";
2436
- job.status.message = "\u767B\u5F55\u5DF2\u9A8C\u8BC1\uFF0C\u4F46\u4FDD\u5B58\u6682\u672A\u6210\u529F\uFF0C\u5C06\u9650\u9891\u91CD\u8BD5\uFF1B\u5C1A\u672A\u4FDD\u5B58";
2437
- return "retry";
2558
+ if (job.cancelRequested || job.controller.signal.aborted) this.finish(job, "cancelled", job.cancellationMessage ?? "\u767B\u5F55\u5DF2\u53D6\u6D88\uFF0C\u672A\u4FDD\u5B58\u6388\u6743");
2559
+ else this.finish(job, "error", hostCode(error) ? "\u8BBE\u7F6E\u5DF2\u6539\u53D8\u6216\u65E0\u6CD5\u4FDD\u5B58\uFF0C\u8BF7\u91CD\u65B0\u8BFB\u53D6\u540E\u91CD\u8BD5" : "\u6388\u6743\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB", hostCode(error) ?? "opendesign/login-commit");
2438
2560
  }
2439
2561
  }
2440
2562
  };
2441
- function errorCode(error) {
2442
- if (error && typeof error === "object" && "code" in error && typeof error.code === "string") return error.code;
2563
+ function hostCode(error) {
2564
+ if (error !== null && typeof error === "object" && "code" in error && typeof error.code === "string" && SAFE_HOST_CODES.has(error.code)) return error.code;
2443
2565
  return void 0;
2444
2566
  }
2445
- function retryAfterMs(error) {
2446
- if (!error || typeof error !== "object") return 0;
2447
- const details = error.details;
2448
- if (!details || typeof details !== "object") return 0;
2449
- const value = details.retryAfterMs;
2450
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.min(3e5, value) : 0;
2451
- }
2452
- async function defaultLaunch(signal) {
2453
- if (signal.aborted) return null;
2454
- const { chromium } = await import("playwright-core");
2455
- if (signal.aborted) return null;
2456
- return await chromium.launch({
2457
- channel: CHROME_LAUNCH_OPTIONS.channel,
2458
- headless: CHROME_LAUNCH_OPTIONS.headless,
2459
- // Copied so Playwright, which wants mutable arrays, cannot freeze on the readonly literals.
2460
- ignoreDefaultArgs: [...CHROME_LAUNCH_OPTIONS.ignoreDefaultArgs],
2461
- args: [...CHROME_LAUNCH_OPTIONS.args]
2462
- });
2567
+ function failureMessage(error) {
2568
+ const code = error instanceof DeviceAuthError ? error.code : "";
2569
+ if (code === "opendesign/login-expired") return "\u8BBE\u5907\u6388\u6743\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB";
2570
+ if (code === "opendesign/login-denied") return "\u8BBE\u5907\u6388\u6743\u88AB\u62D2\u7EDD\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB";
2571
+ if (code === "opendesign/login-request-timeout") return "\u8BBE\u5907\u6388\u6743\u8BF7\u6C42\u8D85\u65F6\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB";
2572
+ return "\u8BBE\u5907\u6388\u6743\u8BF7\u6C42\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB";
2463
2573
  }
2464
2574
 
2465
2575
  // src/quota.ts
@@ -2468,6 +2578,7 @@ var API_BASE = "https://amr-api.open-design.ai/api/v1";
2468
2578
  var CURRENT_URL = `${API_BASE}/workspaces/current`;
2469
2579
  var SUMMARY_URL = `${API_BASE}/billing/summary`;
2470
2580
  var usageUrl = (workspace) => `${API_BASE}/workspaces/${encodeURIComponent(workspace)}/billing/coding-plan-usage`;
2581
+ var preflightUrl = (workspace) => `${API_BASE}/workspaces/${encodeURIComponent(workspace)}/billing/preflight`;
2471
2582
  var ConsoleError = class extends Error {
2472
2583
  constructor(kind, status) {
2473
2584
  super(kind === "auth" ? "OpenDesign \u767B\u5F55\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" : kind === "permission" ? "OpenDesign \u5F53\u524D\u767B\u5F55\u6001\u65E0\u6743\u8BFB\u53D6\u8BE5\u989D\u5EA6\u5C42" : kind === "rate-limit" ? "OpenDesign \u989D\u5EA6\u63A5\u53E3\u9650\u6D41\uFF0C\u8BF7\u7A0D\u540E\u5237\u65B0" : kind === "response" ? "OpenDesign \u989D\u5EA6\u63A5\u53E3\u8FD4\u56DE\u683C\u5F0F\u53D1\u751F\u53D8\u5316" : "OpenDesign \u989D\u5EA6\u6682\u65F6\u65E0\u6CD5\u8BFB\u53D6");
@@ -2478,46 +2589,90 @@ var ConsoleError = class extends Error {
2478
2589
  status;
2479
2590
  };
2480
2591
  async function consoleJson(url, session, signal, fetcher = fetch) {
2481
- let response;
2482
- try {
2483
- response = await fetcher(url, { headers: { ...attributionHeaders3(), accept: "application/json", ...session.headers }, signal: combinedSignal(signal, 2e4), redirect: "error" });
2484
- } catch {
2485
- throw new ConsoleError("network");
2486
- }
2487
- if (response.status === 401 || response.status === 403) {
2488
- await response.body?.cancel();
2489
- throw new ConsoleError(response.status === 401 ? "auth" : "permission", response.status);
2490
- }
2491
- if (!response.ok) {
2492
- await response.body?.cancel();
2493
- throw new ConsoleError(response.status === 429 ? "rate-limit" : "network", response.status);
2494
- }
2495
- const reader = response.body?.getReader();
2496
- if (!reader) throw new ConsoleError("response");
2497
- let size = 0, raw = "";
2498
- const decoder = new TextDecoder();
2499
- try {
2500
- while (true) {
2501
- const chunk = await reader.read();
2502
- if (chunk.done) break;
2503
- size += chunk.value.byteLength;
2504
- if (size > 2 * 1024 * 1024) throw new ConsoleError("response");
2505
- raw += decoder.decode(chunk.value, { stream: true });
2592
+ signal?.throwIfAborted();
2593
+ if (session.credentialType === "device") {
2594
+ let target;
2595
+ try {
2596
+ target = new URL(url);
2597
+ } catch {
2598
+ throw new ConsoleError("response");
2506
2599
  }
2507
- raw += decoder.decode();
2508
- } finally {
2509
- await reader.cancel().catch(() => void 0);
2510
- reader.releaseLock();
2600
+ if (target.origin !== "https://amr-api.open-design.ai" || target.username || target.password) throw new ConsoleError("response");
2511
2601
  }
2512
- let body;
2602
+ const requestSignal = combinedSignal(signal, 2e4);
2603
+ let rejectAbort = () => void 0;
2604
+ const aborted = new Promise((_resolve, reject) => {
2605
+ rejectAbort = reject;
2606
+ });
2607
+ const onAbort = () => rejectAbort(new ConsoleError("network"));
2608
+ requestSignal.addEventListener("abort", onAbort, { once: true });
2609
+ const read = async () => {
2610
+ const response = await fetcher(url, { headers: { ...attributionHeaders3(), accept: "application/json", ...session.headers }, signal: requestSignal, redirect: "error", credentials: "omit" });
2611
+ const cancelBody = () => {
2612
+ void response.body?.cancel().catch(() => void 0);
2613
+ };
2614
+ if (requestSignal.aborted) {
2615
+ cancelBody();
2616
+ throw new ConsoleError("network");
2617
+ }
2618
+ if (response.redirected || response.url && response.url !== url) {
2619
+ cancelBody();
2620
+ throw new ConsoleError("response");
2621
+ }
2622
+ if (response.status === 401 || response.status === 403) {
2623
+ cancelBody();
2624
+ throw new ConsoleError(response.status === 401 ? "auth" : "permission", response.status);
2625
+ }
2626
+ if (!response.ok) {
2627
+ cancelBody();
2628
+ throw new ConsoleError(response.status === 429 ? "rate-limit" : "network", response.status);
2629
+ }
2630
+ const declared = response.headers.get("content-length");
2631
+ if (declared !== null && (!/^\d+$/u.test(declared) || Number(declared) > 2 * 1024 * 1024)) {
2632
+ cancelBody();
2633
+ throw new ConsoleError("response");
2634
+ }
2635
+ const reader = response.body?.getReader();
2636
+ if (!reader) throw new ConsoleError("response");
2637
+ const cancelReader = () => {
2638
+ void reader.cancel().catch(() => void 0);
2639
+ };
2640
+ requestSignal.addEventListener("abort", cancelReader, { once: true });
2641
+ let size = 0, raw = "";
2642
+ const decoder = new TextDecoder("utf-8", { fatal: true });
2643
+ try {
2644
+ while (true) {
2645
+ const chunk = await reader.read();
2646
+ if (requestSignal.aborted) throw new ConsoleError("network");
2647
+ if (chunk.done) break;
2648
+ size += chunk.value.byteLength;
2649
+ if (size > 2 * 1024 * 1024) throw new ConsoleError("response");
2650
+ raw += decoder.decode(chunk.value, { stream: true });
2651
+ }
2652
+ raw += decoder.decode();
2653
+ } finally {
2654
+ requestSignal.removeEventListener("abort", cancelReader);
2655
+ cancelReader();
2656
+ reader.releaseLock();
2657
+ }
2658
+ let body;
2659
+ try {
2660
+ body = JSON.parse(raw);
2661
+ } catch {
2662
+ throw new ConsoleError("response");
2663
+ }
2664
+ if (record(body) && (body.code === 401 || body.code === "401")) throw new ConsoleError("auth", 401);
2665
+ if (record(body) && (body.code === 403 || body.code === "403")) throw new ConsoleError("permission", 403);
2666
+ return body;
2667
+ };
2513
2668
  try {
2514
- body = JSON.parse(raw);
2515
- } catch {
2516
- throw new ConsoleError("response");
2669
+ return await Promise.race([read(), aborted]);
2670
+ } catch (error) {
2671
+ signal?.throwIfAborted();
2672
+ throw error instanceof ConsoleError ? error : new ConsoleError("network");
2673
+ } finally {
2674
+ requestSignal.removeEventListener("abort", onAbort);
2517
2675
  }
2518
- if (record(body) && (body.code === 401 || body.code === "401")) throw new ConsoleError("auth", 401);
2519
- if (record(body) && (body.code === 403 || body.code === "403")) throw new ConsoleError("permission", 403);
2520
- return body;
2521
2676
  }
2522
2677
  function number(value, negative = false) {
2523
2678
  const n = typeof value === "number" ? value : typeof value === "string" && /^-?\d+(\.\d+)?$/u.test(value.trim()) ? Number(value) : NaN;
@@ -2555,7 +2710,51 @@ function normalizeBalances(summary) {
2555
2710
  const totalUsd = number(summary.balanceUsd, true) ?? number(summary.totalAvailableCreditsUsd, true) ?? (divisor !== void 0 && total !== void 0 ? total / divisor : void 0);
2556
2711
  return { ...divisor !== void 0 && subscription !== void 0 ? { subscriptionUsd: subscription / divisor } : {}, ...divisor !== void 0 && recharge !== void 0 ? { rechargeUsd: recharge / divisor } : {}, ...totalUsd !== void 0 ? { totalUsd } : {} };
2557
2712
  }
2713
+ async function readDeviceQuota(session, signal, fetcher = fetch) {
2714
+ const common = { source: session.source, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
2715
+ let current;
2716
+ try {
2717
+ current = await consoleJson(CURRENT_URL, session, signal, fetcher);
2718
+ } catch (error) {
2719
+ signal?.throwIfAborted();
2720
+ if (error instanceof ConsoleError && error.kind === "permission") return { ...common, state: "partial", windows: [], balances: {}, note: "\u5F53\u524D\u8BBE\u5907\u6388\u6743\u65E0\u6743\u8BFB\u53D6\u5DE5\u4F5C\u533A\uFF1B\u989D\u5EA6\u6682\u4E0D\u53EF\u786E\u8BA4" };
2721
+ throw error;
2722
+ }
2723
+ if (!record(current)) throw new ConsoleError("response");
2724
+ const workspaceId = typeof current.activeWorkspaceId === "string" && current.activeWorkspaceId.trim() ? current.activeWorkspaceId : current.workspaceId;
2725
+ if (typeof workspaceId !== "string" || !/^[A-Za-z0-9_-]{1,64}$/u.test(workspaceId)) throw new ConsoleError("response");
2726
+ const personal = current.workspaceType === "personal";
2727
+ const [preflight, billing] = await Promise.allSettled([
2728
+ consoleJson(preflightUrl(workspaceId), session, signal, fetcher),
2729
+ personal ? consoleJson(SUMMARY_URL, session, signal, fetcher) : Promise.resolve(void 0)
2730
+ ]);
2731
+ signal?.throwIfAborted();
2732
+ for (const item of [preflight, billing]) if (item.status === "rejected" && item.reason instanceof ConsoleError && item.reason.kind === "auth") throw item.reason;
2733
+ const summary = billing.status === "fulfilled" ? billing.value : void 0;
2734
+ const flight = preflight.status === "fulfilled" && record(preflight.value) ? preflight.value : void 0;
2735
+ let normalized;
2736
+ try {
2737
+ normalized = normalizeQuota(flight?.codingPlan, personal ? summary : void 0);
2738
+ } catch (error) {
2739
+ if (!(error instanceof ConsoleError)) throw error;
2740
+ }
2741
+ const balances = personal ? normalizeBalances(summary) : {};
2742
+ const total = personal ? number(flight?.balanceUsd, true) : void 0;
2743
+ if (total !== void 0) balances.totalUsd = total;
2744
+ const permission = [preflight, billing].some((item) => item.status === "rejected" && item.reason instanceof ConsoleError && item.reason.kind === "permission");
2745
+ if (!normalized && !Object.keys(balances).length && !permission) {
2746
+ if (preflight.status === "rejected") throw preflight.reason;
2747
+ throw new ConsoleError("response");
2748
+ }
2749
+ const windows = normalized?.windows ?? [];
2750
+ const missingWindows = normalized?.eligible !== false && !windows.length;
2751
+ const missingBalances = personal && Object.keys(balances).length < 3;
2752
+ const partial = missingWindows || missingBalances || !personal || permission;
2753
+ const note = !personal ? "\u56E2\u961F\u6216\u672A\u77E5\u5DE5\u4F5C\u533A\uFF1A\u8D26\u53F7\u94B1\u5305\u53E3\u5F84\u672A\u6838\u5B9E\uFF0C\u6682\u4E0D\u663E\u793A" : missingWindows ? "\u5957\u9910\u989D\u5EA6\u7A97\u53E3\u6682\u4E0D\u53EF\u786E\u8BA4" : missingBalances ? "\u90E8\u5206\u94B1\u5305\u4F59\u989D\u6682\u4E0D\u53EF\u786E\u8BA4" : permission ? "\u90E8\u5206\u989D\u5EA6\u5C42\u65E0\u6743\u8BFB\u53D6" : normalized?.eligible === false ? "\u8BE5\u8D26\u53F7\u6CA1\u6709 Design Plan \u8BA2\u9605\uFF1B\u94B1\u5305\u4F59\u989D\u7167\u5E38\u663E\u793A" : void 0;
2754
+ return { ...common, ...normalized, ...normalizeSubscription(personal ? summary : void 0), windows, balances, state: partial ? "partial" : "ok", ...note ? { note } : {} };
2755
+ }
2558
2756
  async function readQuota(session, signal, fetcher = fetch) {
2757
+ if (session.credentialType === "device") return readDeviceQuota(session, signal, fetcher);
2559
2758
  const current = await consoleJson(CURRENT_URL, session, signal, fetcher);
2560
2759
  if (!record(current) || typeof current.workspaceId !== "string" || !/^[A-Za-z0-9_-]{1,64}$/u.test(current.workspaceId)) throw new ConsoleError("response");
2561
2760
  const personal = current.workspaceType === "personal";
@@ -2747,9 +2946,19 @@ function parseLoginState(value) {
2747
2946
  if (!record(value)) throw new Error("\u767B\u5F55\u72B6\u6001\u5FC5\u987B\u662F\u5BF9\u8C61");
2748
2947
  const jobId = value.jobId;
2749
2948
  if (jobId !== null && jobId !== void 0 && typeof jobId !== "string") throw new Error("\u767B\u5F55\u4EFB\u52A1 id \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6216 null");
2949
+ const state = member(value.state, LOGIN_PHASES, "\u767B\u5F55\u9636\u6BB5");
2950
+ let approval = {};
2951
+ if (state === "waiting" && (value.activationUrl !== void 0 || value.userCode !== void 0 || value.expiresAt !== void 0)) {
2952
+ const userCode = text(value.userCode, "\u8BBE\u5907\u6388\u6743\u7801", 64);
2953
+ if (!/^[A-Za-z0-9-]+$/u.test(userCode)) throw new Error("\u8BBE\u5907\u6388\u6743\u7801\u683C\u5F0F\u65E0\u6548");
2954
+ const activationUrl = validateActivationUrl(value.activationUrl, userCode);
2955
+ if (activationUrl === null) throw new Error("\u8BBE\u5907\u6388\u6743\u94FE\u63A5\u65E0\u6548");
2956
+ approval = { activationUrl, userCode, expiresAt: isoTime(value.expiresAt, "\u8BBE\u5907\u6388\u6743\u5230\u671F\u65F6\u95F4") };
2957
+ }
2750
2958
  return {
2751
2959
  jobId: typeof jobId === "string" && jobId.length > 0 ? text(jobId, "\u767B\u5F55\u4EFB\u52A1 id", MAX_JOB_ID) : null,
2752
- state: member(value.state, LOGIN_PHASES, "\u767B\u5F55\u9636\u6BB5"),
2960
+ state,
2961
+ ...approval,
2753
2962
  ...value.message === void 0 ? {} : { message: text(value.message, "\u767B\u5F55\u8BF4\u660E", MAX_MESSAGE) },
2754
2963
  ...value.startedAt === void 0 ? {} : { startedAt: isoTime(value.startedAt, "\u767B\u5F55\u5F00\u59CB\u65F6\u95F4") },
2755
2964
  ...value.finishedAt === void 0 ? {} : { finishedAt: isoTime(value.finishedAt, "\u767B\u5F55\u7ED3\u675F\u65F6\u95F4") }
@@ -2834,7 +3043,9 @@ var SESSION = credentialKey(name, "console");
2834
3043
  var hash = (value) => createHash("sha256").update(JSON.stringify(value) ?? "").digest("hex");
2835
3044
  var needLogin = () => ({ state: "need-login", updatedAt: (/* @__PURE__ */ new Date()).toISOString(), windows: [], balances: {}, note: "\u5C1A\u672A\u8BFB\u53D6\u989D\u5EA6\uFF1B\u53EF\u4F7F\u7528\u684C\u9762\u767B\u5F55\u6001\u6216\u5728\u8FDE\u63A5\u9875\u767B\u5F55" });
2836
3045
  function grantOf(value) {
2837
- if (value?.kind !== "grant" || !record(value.payload) || typeof value.payload.cookie !== "string") return;
3046
+ if (value?.kind !== "grant" || !record(value.payload)) return;
3047
+ if (value.payload.kind === "device") return deviceGrant(value.payload);
3048
+ if (typeof value.payload.cookie !== "string") return;
2838
3049
  return { cookie: value.payload.cookie, ...typeof value.payload.expiresAt === "number" ? { expiresAt: value.payload.expiresAt } : {} };
2839
3050
  }
2840
3051
  var _saveApiKey_dec, _cancelLogin_dec, _loginStatus_dec, _startLogin_dec, _refreshQuotaNow_dec, _refreshQuota_dec, _refreshModels_dec, _status_dec, _a, _init;
@@ -2871,13 +3082,7 @@ var OpenDesignCoRouterService = class extends (_a = TypertRemoteService, _status
2871
3082
  this._credentials();
2872
3083
  return { revision: expectedRevision, sessionHash: hash(state.sessionRecord), configHash: hash(state.config) };
2873
3084
  },
2874
- verify: async (cookie, signal) => {
2875
- const session = cookieSession(cookie, "login");
2876
- if (!session) throw new ConsoleError("auth");
2877
- const quota = await this.dependencies.readQuota(session, signal);
2878
- return { balanceOk: true, coverage: quota.state === "ok" ? "complete" : "partial", windows: quota.windows };
2879
- },
2880
- commit: async (cookie, captured, signal, expiresAt) => {
3085
+ commit: async (tokens, captured, signal) => {
2881
3086
  if (!record(captured) || typeof captured.revision !== "string" || typeof captured.sessionHash !== "string" || typeof captured.configHash !== "string") throw this._conflict();
2882
3087
  await this._state();
2883
3088
  this._expect(captured.revision);
@@ -2885,11 +3090,15 @@ var OpenDesignCoRouterService = class extends (_a = TypertRemoteService, _status
2885
3090
  await this._credentials().modifyRecord(SESSION, async (current) => {
2886
3091
  signal.throwIfAborted();
2887
3092
  if (hash(current) !== captured.sessionHash || hash(this._config()) !== captured.configHash) throw this._conflict();
2888
- return { kind: "grant", payload: { cookie, expiresAt, savedAt: (/* @__PURE__ */ new Date()).toISOString() } };
3093
+ const grant = deviceGrant({ kind: "device", ...tokens });
3094
+ if (!grant) throw new ConsoleError("auth");
3095
+ return { kind: "grant", payload: { ...grant, savedAt: (/* @__PURE__ */ new Date()).toISOString() } };
2889
3096
  });
2890
3097
  this._invalidateQuota();
3098
+ this._modelsFingerprint = "";
3099
+ this._invalidateModels();
2891
3100
  }
2892
- });
3101
+ }, dependencies.loginOptions);
2893
3102
  ctx.effect(() => () => {
2894
3103
  this._lifetime.abort();
2895
3104
  this._login.dispose();
@@ -2904,28 +3113,43 @@ var OpenDesignCoRouterService = class extends (_a = TypertRemoteService, _status
2904
3113
  if (!service) throw new RemoteError("opendesign/credentials-unavailable", "DSH \u51ED\u636E\u670D\u52A1\u4E0D\u53EF\u7528\uFF1B\u53EF\u5148\u4F7F\u7528\u542F\u52A8\u73AF\u5883\u53D8\u91CF", void 0);
2905
3114
  return service;
2906
3115
  }
2907
- async resolveApiKey(ref) {
3116
+ async _key(ref, baseURL, apiRecord, sessionRecord) {
2908
3117
  const credentials = this.ctx.get("credentials");
2909
3118
  const ambient = credentials ? (await credentials.resolve(credentialRef(ref)))?.value : launchEnvironmentOf(this.ctx).get(ref)?.value;
2910
3119
  if (ambient?.trim()) return ambient;
2911
- const saved = await credentials?.readRecord(API_KEY);
2912
- return saved?.kind === "api-key" ? saved.key : void 0;
3120
+ if (apiRecord?.kind === "api-key" && apiRecord.key?.trim()) return apiRecord.key;
3121
+ const grant = grantOf(sessionRecord);
3122
+ return isOfficialRuntimeUrl(baseURL) && grant && "kind" in grant && grant.kind === "device" ? grant.runtimeKey : void 0;
3123
+ }
3124
+ async resolveApiKey(ref, baseURL = this._config().baseURL) {
3125
+ const credentials = this.ctx.get("credentials");
3126
+ const [apiRecord, sessionRecord] = await Promise.all([credentials?.readRecord(API_KEY), credentials?.readRecord(SESSION)]);
3127
+ return this._key(ref, baseURL, apiRecord, sessionRecord);
3128
+ }
3129
+ _invalidateModels() {
3130
+ const changed = this._modelsUpdatedAt !== void 0;
3131
+ this._modelsEpoch++;
3132
+ this._modelsController?.abort();
3133
+ this._modelsPending = void 0;
3134
+ this._models = MODELS;
3135
+ this._modelsUpdatedAt = void 0;
3136
+ this._modelsError = void 0;
3137
+ if (changed) {
3138
+ try {
3139
+ this.dependencies.modelsChanged?.();
3140
+ } catch {
3141
+ }
3142
+ }
2913
3143
  }
2914
3144
  async _state() {
2915
3145
  const credentials = this.ctx.get("credentials"), config = this._config();
2916
- const [apiRecord, sessionRecord, key] = await Promise.all([credentials?.readRecord(API_KEY), credentials?.readRecord(SESSION), this.resolveApiKey(config.apiKeyEnv)]);
3146
+ const [apiRecord, sessionRecord] = await Promise.all([credentials?.readRecord(API_KEY), credentials?.readRecord(SESSION)]);
3147
+ const key = await this._key(config.apiKeyEnv, config.baseURL, apiRecord, sessionRecord);
2917
3148
  const state = { apiRecord, sessionRecord, key, config };
2918
3149
  const modelsFingerprint = hash({ key, baseURL: config.baseURL });
2919
3150
  if (modelsFingerprint !== this._modelsFingerprint) {
2920
- const changed = this._modelsUpdatedAt !== void 0;
2921
3151
  this._modelsFingerprint = modelsFingerprint;
2922
- this._modelsEpoch++;
2923
- this._modelsController?.abort();
2924
- this._modelsPending = void 0;
2925
- this._models = MODELS;
2926
- this._modelsUpdatedAt = void 0;
2927
- this._modelsError = void 0;
2928
- if (changed) this.dependencies.modelsChanged?.();
3152
+ this._invalidateModels();
2929
3153
  }
2930
3154
  const fingerprint = hash(state);
2931
3155
  if (fingerprint !== this._fingerprint) {
@@ -3048,9 +3272,10 @@ var OpenDesignCoRouterService = class extends (_a = TypertRemoteService, _status
3048
3272
  let identity;
3049
3273
  let refreshFailed = false;
3050
3274
  try {
3275
+ const grant = grantOf(await this.ctx.get("credentials")?.readRecord(SESSION));
3051
3276
  const authDeps = {
3052
3277
  useDesktop: config.useDesktopCredential,
3053
- readGrant: async () => grantOf(await this.ctx.get("credentials")?.readRecord(SESSION)),
3278
+ readGrant: async () => grant,
3054
3279
  resolveRef: async (ref) => {
3055
3280
  const credentials = this.ctx.get("credentials");
3056
3281
  return credentials ? (await credentials.resolve(credentialRef(ref)))?.value : launchEnvironmentOf(this.ctx).get(ref)?.value;
@@ -3068,7 +3293,10 @@ var OpenDesignCoRouterService = class extends (_a = TypertRemoteService, _status
3068
3293
  if (fallback.state === "ready") {
3069
3294
  try {
3070
3295
  const complete = await this.dependencies.readQuota(fallback.session, signal);
3071
- if (complete.state === "ok" && (complete.windows.length > 0 || complete.eligible === false)) result = { ...complete, note: `\u684C\u9762\u767B\u5F55\u6001\u4EC5\u63D0\u4F9B\u90E8\u5206\u989D\u5EA6\uFF1B\u5F53\u524D\u663E\u793A\u72EC\u7ACB\u767B\u5F55\u8D26\u53F7\u7684\u5B8C\u6574\u989D\u5EA6\u3002${complete.note ? ` ${complete.note}` : ""}` };
3296
+ if (complete.state === "ok" && (complete.windows.length > 0 || complete.eligible === false)) {
3297
+ result = { ...complete, note: `\u684C\u9762\u767B\u5F55\u6001\u4EC5\u63D0\u4F9B\u90E8\u5206\u989D\u5EA6\uFF1B\u5F53\u524D\u663E\u793A\u72EC\u7ACB\u767B\u5F55\u8D26\u53F7\u7684\u5B8C\u6574\u989D\u5EA6\u3002${complete.note ? ` ${complete.note}` : ""}` };
3298
+ identity = hash(fallback.session);
3299
+ }
3072
3300
  } catch (error) {
3073
3301
  if (signal.aborted) throw error;
3074
3302
  }
@@ -3078,6 +3306,7 @@ var OpenDesignCoRouterService = class extends (_a = TypertRemoteService, _status
3078
3306
  if (!(error instanceof ConsoleError) || !["auth", "permission"].includes(error.kind)) throw error;
3079
3307
  const fallback = await this.dependencies.resolveSession({ ...authDeps, useDesktop: false });
3080
3308
  if (fallback.state !== "ready") throw new ConsoleError("auth");
3309
+ identity = hash(fallback.session);
3081
3310
  result = await this.dependencies.readQuota(fallback.session, signal);
3082
3311
  }
3083
3312
  }
@@ -3155,7 +3384,7 @@ function apply(ctx, config) {
3155
3384
  const adapter = new CoRouterAdapter({
3156
3385
  config: current,
3157
3386
  models: () => service.modelCatalog(),
3158
- apiKey: (ref) => service.resolveApiKey(ref),
3387
+ apiKey: (ref, baseURL) => service.resolveApiKey(ref, baseURL),
3159
3388
  onEmptyTurn: () => {
3160
3389
  service.guardedTurns++;
3161
3390
  },
@@ -3178,7 +3407,7 @@ function apply(ctx, config) {
3178
3407
  ctx.llm.registerModelDiscovery(SETTINGS_NS, async (request, signal) => {
3179
3408
  if (request.provider !== void 0 && ![PLAN_PROVIDER, PAID_PROVIDER].includes(request.provider)) throw new LlmError11("\u4E0D\u662F\u672C\u63D2\u4EF6\u7684\u6A21\u578B\u901A\u9053", "INVALID_REQUEST");
3180
3409
  const config2 = current();
3181
- const key = request.apiKey ?? await service.resolveApiKey(config2.apiKeyEnv);
3410
+ const key = request.apiKey ?? await service.resolveApiKey(config2.apiKeyEnv, config2.baseURL);
3182
3411
  if (!key) throw new LlmError11("\u8BF7\u5148\u914D\u7F6E CoRouter API Key", "MISSING_CREDENTIAL");
3183
3412
  const models = await fetchModelCatalog(config2.baseURL, key, signal);
3184
3413
  return models.map((row) => ({ id: row.id, name: row.name, ...row.contextWindow === void 0 ? {} : { contextWindow: row.contextWindow }, ...row.maxOutputTokens === void 0 ? {} : { maxTokens: row.maxOutputTokens }, ...row.inputModalities ? { inputModalities: row.inputModalities } : {} }));