dsh-opendesign-corouter 0.1.3 → 0.1.5

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.5";
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,241 +2399,450 @@ 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
+ /** Host-only: the live approval link, so the Host can open it in a bounded window. */
2410
+ activationLink() {
2411
+ const status = this.status();
2412
+ return status.state === "waiting" ? status.activationUrl : void 0;
2413
+ }
2414
+ /** Synchronous/non-blocking and idempotent while capture/network/commit is pending. */
2237
2415
  start(request) {
2238
2416
  if (this.disposed) throw new LoginError("opendesign/login-disposed");
2417
+ this.status();
2239
2418
  if (this.job && ACTIVE.has(this.job.status.state)) return this.status();
2419
+ const started = this.now();
2420
+ const requested = this.options.timeoutMs;
2421
+ const timeoutMs = typeof requested === "number" && Number.isFinite(requested) && requested > 0 ? Math.min(MAX_TIMEOUT_MS, requested) : MAX_TIMEOUT_MS;
2240
2422
  const job = {
2241
2423
  id: randomUUID(),
2242
- startedAt: new Date(this.now()).toISOString(),
2243
2424
  controller: new AbortController(),
2244
- expectedRevision: request.expectedRevision,
2245
- status: { jobId: "", state: "launching", message: "\u6B63\u5728\u6253\u5F00\u4E34\u65F6 Chrome \u767B\u5F55\u7A97\u53E3" },
2425
+ deadline: started + timeoutMs,
2426
+ status: { jobId: null, state: "launching", startedAt: new Date(started).toISOString(), message: "\u6B63\u5728\u7533\u8BF7\u5B98\u65B9\u8BBE\u5907\u6388\u6743\u7801" },
2246
2427
  captured: void 0,
2247
- capturedAt: void 0,
2248
- verifying: false,
2249
2428
  committing: false,
2250
- cancelRequested: false
2429
+ cancelRequested: false,
2430
+ settled: false
2251
2431
  };
2252
2432
  job.status.jobId = job.id;
2253
- job.status.startedAt = job.startedAt;
2254
2433
  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");
2434
+ this.armExpiry(job);
2435
+ void Promise.resolve().then(async () => {
2436
+ if (!this.alive(job)) return;
2437
+ try {
2438
+ job.captured = await this.callbacks.capture(request.expectedRevision);
2439
+ } catch (error) {
2440
+ 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");
2441
+ return;
2442
+ }
2443
+ if (this.alive(job)) await this.run(job);
2444
+ }).catch((error) => {
2445
+ if (this.alive(job)) this.finish(job, "error", failureMessage(error), error instanceof DeviceAuthError ? error.code : "opendesign/login-network");
2263
2446
  });
2264
2447
  return this.status();
2265
2448
  }
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
2449
  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");
2450
+ if (this.job?.id === request.jobId) this.requestCancellation(this.job, "\u767B\u5F55\u5DF2\u53D6\u6D88\uFF0C\u672A\u4FDD\u5B58\u6388\u6743");
2276
2451
  return this.status();
2277
2452
  }
2278
- /** Plugin teardown: abort the attempt and close the browser. */
2279
2453
  dispose() {
2280
- const job = this.job;
2281
- if (job) this.requestCancellation(job, "\u670D\u52A1\u5DF2\u5378\u8F7D\uFF0C\u767B\u5F55\u5DF2\u53D6\u6D88");
2282
2454
  this.disposed = true;
2455
+ const job = this.job;
2456
+ if (job) {
2457
+ this.requestCancellation(job, "\u670D\u52A1\u5DF2\u5378\u8F7D\uFF0C\u767B\u5F55\u5DF2\u53D6\u6D88");
2458
+ this.settle(job);
2459
+ }
2460
+ }
2461
+ alive(job) {
2462
+ return this.job === job && !this.disposed && !job.controller.signal.aborted && ACTIVE.has(job.status.state);
2463
+ }
2464
+ stopTimer(job) {
2465
+ if (job.timer !== void 0) {
2466
+ clearTimeout(job.timer);
2467
+ job.timer = void 0;
2468
+ }
2469
+ }
2470
+ armExpiry(job) {
2471
+ this.stopTimer(job);
2472
+ job.timer = setTimeout(() => this.expire(job), Math.max(1, job.deadline - this.now()));
2473
+ job.timer.unref?.();
2474
+ }
2475
+ clearCode(job) {
2476
+ delete job.status.activationUrl;
2477
+ delete job.status.userCode;
2478
+ delete job.status.expiresAt;
2479
+ }
2480
+ expire(job) {
2481
+ if (job.committing) this.requestCancellation(job, "\u6388\u6743\u5DF2\u8FC7\u671F\uFF0C\u672A\u4FDD\u5B58\u6388\u6743");
2482
+ else this.finish(job, "error", "\u8BBE\u5907\u6388\u6743\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB", "opendesign/login-expired");
2283
2483
  }
2284
- /** Every cancellation source must let an in-flight atomic store decide the final outcome. */
2285
2484
  requestCancellation(job, message) {
2286
2485
  if (this.job !== job || !ACTIVE.has(job.status.state)) return;
2486
+ this.clearCode(job);
2287
2487
  if (!job.committing) {
2288
2488
  this.finish(job, "cancelled", message);
2289
2489
  return;
2290
2490
  }
2291
2491
  if (!job.cancelRequested) {
2292
2492
  job.cancellationMessage = message;
2293
- job.status.message = "\u6B63\u5728\u786E\u8BA4\u4F1A\u8BDD\u4FDD\u5B58\u7ED3\u679C";
2493
+ job.status.message = "\u6B63\u5728\u786E\u8BA4\u6388\u6743\u4FDD\u5B58\u7ED3\u679C";
2294
2494
  }
2295
2495
  job.cancelRequested = true;
2296
2496
  this.stopTimer(job);
2297
2497
  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
- }
2305
- stopTimer(job) {
2306
- if (job.timer) {
2307
- clearTimeout(job.timer);
2308
- job.timer = void 0;
2309
- }
2310
2498
  }
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;
2499
+ finish(job, state, message, errorCode) {
2500
+ if (this.job !== job || !ACTIVE.has(job.status.state)) return;
2322
2501
  job.status.state = state;
2323
2502
  job.status.finishedAt = new Date(this.now()).toISOString();
2324
2503
  job.status.message = message;
2325
- if (errorCode2) job.status.errorCode = errorCode2;
2326
- job.secret = void 0;
2504
+ if (errorCode) job.status.errorCode = errorCode;
2505
+ this.clearCode(job);
2327
2506
  this.stopTimer(job);
2328
2507
  job.controller.abort();
2329
- void this.close(job);
2508
+ this.settle(job);
2330
2509
  }
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
- }
2340
- 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);
2357
- return;
2358
- }
2510
+ /** Report one terminal transition per job, exactly once. */
2511
+ settle(job) {
2512
+ if (job.settled) return;
2513
+ job.settled = true;
2359
2514
  try {
2360
- await page.goto(site.loginUrl, { waitUntil: "domcontentloaded", timeout: 3e4 });
2515
+ this.options.onSettled?.();
2361
2516
  } 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
2517
  }
2518
+ }
2519
+ async run(job) {
2520
+ const signal = job.controller.signal;
2521
+ const raw = await postDeviceJson(this.fetcher, DEVICE_AUTH_ENDPOINT, {
2522
+ // The third-party protocol's official-CLI version is NOT our version. Compatibility
2523
+ // of this actual plugin version requires a real-account check outside offline tests.
2524
+ cliVersion: DEVICE_AUTH_PLUGIN_VERSION,
2525
+ host: "DSH CoRouter",
2526
+ profile: "prod"
2527
+ }, signal, job.deadline - this.now());
2369
2528
  if (!this.alive(job)) return;
2529
+ const device = parseDeviceAuthorization(raw);
2530
+ job.deadline = Math.min(job.deadline, device.expiresAt);
2531
+ if (this.now() >= job.deadline) {
2532
+ this.expire(job);
2533
+ return;
2534
+ }
2535
+ this.armExpiry(job);
2370
2536
  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;
2537
+ job.status.message = "\u8BF7\u5728\u6388\u6743\u7A97\u53E3\u6216\u5E38\u7528\u6D4F\u89C8\u5668\u6253\u5F00\u5B98\u65B9\u6388\u6743\u9875\uFF0C\u767B\u5F55\u5E76\u786E\u8BA4\u8BBE\u5907\u7801";
2538
+ job.status.activationUrl = device.activationUrl;
2539
+ job.status.userCode = device.userCode;
2540
+ job.status.expiresAt = new Date(job.deadline).toISOString();
2375
2541
  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
- }
2542
+ await this.delay(Math.min(device.pollMs, Math.max(1, job.deadline - this.now())), signal);
2543
+ if (!this.alive(job)) return;
2544
+ if (this.now() >= job.deadline) {
2545
+ this.expire(job);
2546
+ return;
2547
+ }
2548
+ const result = parseDevicePoll(await postDeviceJson(
2549
+ this.fetcher,
2550
+ `${DEVICE_AUTH_ENDPOINT}/${device.deviceId}/token`,
2551
+ { deviceSecret: device.deviceSecret },
2552
+ signal,
2553
+ job.deadline - this.now()
2554
+ ));
2555
+ if (!this.alive(job)) return;
2556
+ if (this.now() >= job.deadline) {
2557
+ this.expire(job);
2558
+ return;
2559
+ }
2560
+ if (result.state === "success") {
2561
+ await this.commit(job, result.tokens);
2562
+ return;
2406
2563
  }
2407
- await this.delay(pollMs, job.controller.signal);
2408
2564
  }
2409
2565
  }
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";
2566
+ async commit(job, tokens) {
2567
+ if (!this.alive(job) || job.committing) return;
2417
2568
  job.committing = true;
2569
+ this.clearCode(job);
2418
2570
  job.status.state = "verifying";
2419
- job.status.message = "\u767B\u5F55\u5DF2\u9A8C\u8BC1\uFF0C\u6B63\u5728\u4FDD\u5B58\u4F1A\u8BDD";
2571
+ job.status.message = "\u5B98\u65B9\u6388\u6743\u5DF2\u5B8C\u6210\uFF0C\u6B63\u5728\u4FDD\u5B58\u6388\u6743";
2420
2572
  try {
2421
- await this.callbacks.commit(session.cookie, job.captured, job.controller.signal, session.expiresAt);
2573
+ await this.callbacks.commit(tokens, job.captured, job.controller.signal);
2422
2574
  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";
2575
+ this.finish(job, "success", "\u8BBE\u5907\u6388\u6743\u5DF2\u5B8C\u6210\u5E76\u4FDD\u5B58\uFF1B\u989D\u5EA6\u72B6\u6001\u53E6\u884C\u663E\u793A");
2425
2576
  } catch (error) {
2426
2577
  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";
2578
+ if (job.cancelRequested || job.controller.signal.aborted) this.finish(job, "cancelled", job.cancellationMessage ?? "\u767B\u5F55\u5DF2\u53D6\u6D88\uFF0C\u672A\u4FDD\u5B58\u6388\u6743");
2579
+ 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
2580
  }
2439
2581
  }
2440
2582
  };
2441
- function errorCode(error) {
2442
- if (error && typeof error === "object" && "code" in error && typeof error.code === "string") return error.code;
2583
+ function hostCode(error) {
2584
+ if (error !== null && typeof error === "object" && "code" in error && typeof error.code === "string" && SAFE_HOST_CODES.has(error.code)) return error.code;
2443
2585
  return void 0;
2444
2586
  }
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]
2587
+ function failureMessage(error) {
2588
+ const code = error instanceof DeviceAuthError ? error.code : "";
2589
+ if (code === "opendesign/login-expired") return "\u8BBE\u5907\u6388\u6743\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB";
2590
+ if (code === "opendesign/login-denied") return "\u8BBE\u5907\u6388\u6743\u88AB\u62D2\u7EDD\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB";
2591
+ if (code === "opendesign/login-request-timeout") return "\u8BBE\u5907\u6388\u6743\u8BF7\u6C42\u8D85\u65F6\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB";
2592
+ return "\u8BBE\u5907\u6388\u6743\u8BF7\u6C42\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB";
2593
+ }
2594
+
2595
+ // src/browser-open.ts
2596
+ var ACTIVATION_NAVIGATE_TIMEOUT_MS = 15e3;
2597
+ var ACTIVATION_OPEN_BUDGET_MS = 2e4;
2598
+ var ACTIVATION_CLOSE_TIMEOUT_MS = 5e3;
2599
+ function chromeLaunchOptions() {
2600
+ return {
2601
+ channel: "chrome",
2602
+ headless: false,
2603
+ chromiumSandbox: true,
2604
+ ignoreDefaultArgs: ["--enable-automation"],
2605
+ args: ["--disable-blink-features=AutomationControlled"]
2606
+ };
2607
+ }
2608
+ var ActivationLauncher = class {
2609
+ browser;
2610
+ page;
2611
+ pending;
2612
+ disposal;
2613
+ /** In-flight window closure, shared so disposal can join it. */
2614
+ closing;
2615
+ /** Closures of handles that were never adopted; disposal joins them too. */
2616
+ tracked = [];
2617
+ /** Chrome starts in flight, so disposal waits for the handle they will produce. */
2618
+ starting = [];
2619
+ disposed = false;
2620
+ /** Bumped by `close()`: an attempt from an older session must not resurrect a window. */
2621
+ session = 0;
2622
+ options;
2623
+ constructor(options = {}) {
2624
+ this.options = options;
2625
+ }
2626
+ /**
2627
+ * Show `url` in the temporary window, reusing one already open. Concurrent calls share
2628
+ * a single attempt: the caller's button stays honest about what actually happened.
2629
+ */
2630
+ async open(url) {
2631
+ if (validateActivationUrl(url) === null) return { code: "invalid-url", opened: false };
2632
+ if (this.disposed) return { code: "failed", opened: false };
2633
+ if (this.pending) return this.pending;
2634
+ const attempt = this.attempt(url);
2635
+ this.pending = attempt;
2636
+ try {
2637
+ return await attempt;
2638
+ } finally {
2639
+ if (this.pending === attempt) this.pending = void 0;
2640
+ }
2641
+ }
2642
+ /**
2643
+ * Close the temporary window and stay usable: a cancelled or expired job takes its
2644
+ * window away, while the next login may open a new one.
2645
+ */
2646
+ async close() {
2647
+ this.session++;
2648
+ await this.closeCurrent();
2649
+ }
2650
+ /**
2651
+ * Retire the launcher for good. Cleanup is best-effort and bounded: it joins in-flight
2652
+ * starts and closures, but a browser that ignores `close()` may outlive the 5 s wait.
2653
+ */
2654
+ async dispose() {
2655
+ this.disposed = true;
2656
+ this.session++;
2657
+ if (this.disposal) return this.disposal;
2658
+ this.disposal = (async () => {
2659
+ await this.bounded(Date.now() + ACTIVATION_CLOSE_TIMEOUT_MS, async () => {
2660
+ await Promise.allSettled(this.starting);
2661
+ });
2662
+ await this.closing;
2663
+ await this.closeCurrent();
2664
+ await this.bounded(Date.now() + ACTIVATION_CLOSE_TIMEOUT_MS, async () => {
2665
+ await Promise.all(this.tracked);
2666
+ });
2667
+ })();
2668
+ return this.disposal;
2669
+ }
2670
+ /**
2671
+ * Close the window this launcher owns right now. Ownership is taken before the async
2672
+ * close, so a job that adopts a new window afterwards is not affected by this closure.
2673
+ * The closure promise is shared so a later `dispose()` joins it.
2674
+ */
2675
+ async closeCurrent() {
2676
+ if (this.closing === void 0) {
2677
+ const browser = this.browser;
2678
+ this.browser = void 0;
2679
+ this.page = void 0;
2680
+ if (browser === void 0) return;
2681
+ this.closing = closeBounded(browser);
2682
+ }
2683
+ const closing = this.closing;
2684
+ try {
2685
+ await closing;
2686
+ } finally {
2687
+ if (this.closing === closing) this.closing = void 0;
2688
+ }
2689
+ }
2690
+ async attempt(url) {
2691
+ const session = this.session;
2692
+ const budget = this.options.timeoutMs ?? ACTIVATION_OPEN_BUDGET_MS;
2693
+ const deadline = Date.now() + budget;
2694
+ try {
2695
+ if (this.options.available?.() === false) return { code: "unavailable", opened: false };
2696
+ const browser = await this.ensureBrowser(session, deadline);
2697
+ if (browser === void 0) return { code: this.stale(session) ? "failed" : "unavailable", opened: false };
2698
+ if (this.stale(session)) {
2699
+ await this.bounded(deadline, () => this.closeCurrent());
2700
+ return { code: "failed", opened: false };
2701
+ }
2702
+ const page = await this.ensurePage(browser, session, deadline);
2703
+ if (page === void 0) {
2704
+ await this.bounded(deadline, () => this.closeCurrent());
2705
+ return { code: "failed", opened: false };
2706
+ }
2707
+ const remaining = Math.max(1, Math.min(ACTIVATION_NAVIGATE_TIMEOUT_MS, deadline - Date.now()));
2708
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: remaining });
2709
+ if (this.stale(session)) {
2710
+ await this.bounded(deadline, () => this.closeCurrent());
2711
+ return { code: "failed", opened: false };
2712
+ }
2713
+ return { code: "opened", opened: true };
2714
+ } catch {
2715
+ await this.bounded(deadline, () => this.closeCurrent());
2716
+ return { code: "failed", opened: false };
2717
+ }
2718
+ }
2719
+ /** The job that started this attempt ended, or the launcher retired. */
2720
+ stale(session) {
2721
+ return this.disposed || session !== this.session;
2722
+ }
2723
+ async ensureBrowser(session, deadline) {
2724
+ const open = this.browser;
2725
+ if (open !== void 0) {
2726
+ try {
2727
+ if (open.isConnected()) return open;
2728
+ } catch {
2729
+ }
2730
+ await this.bounded(deadline, () => this.closeCurrent());
2731
+ }
2732
+ for (let round = 0; round < 2; round++) {
2733
+ await this.bounded(deadline, async () => {
2734
+ await Promise.allSettled([this.closing, ...this.tracked, ...this.starting]);
2735
+ });
2736
+ if (this.stale(session) || Date.now() >= deadline) return void 0;
2737
+ if (this.closing === void 0 && this.tracked.length === 0 && this.starting.length === 0) break;
2738
+ }
2739
+ if (this.stale(session) || Date.now() >= deadline) return void 0;
2740
+ const launch = this.options.launch ?? defaultLaunch;
2741
+ const starting = launch();
2742
+ this.starting.push(starting);
2743
+ const browser = await this.bounded(deadline, () => starting);
2744
+ void starting.then((handle) => {
2745
+ if (handle !== browser) void this.trackClose(handle);
2746
+ }).catch(() => void 0).finally(() => {
2747
+ this.starting = this.starting.filter((item) => item !== starting);
2748
+ });
2749
+ if (browser === void 0) return void 0;
2750
+ if (this.stale(session)) {
2751
+ void this.trackClose(browser);
2752
+ return void 0;
2753
+ }
2754
+ this.adopt(browser);
2755
+ return browser;
2756
+ }
2757
+ async ensurePage(browser, session, deadline) {
2758
+ if (this.page !== void 0) return this.page;
2759
+ const page = await this.bounded(deadline, () => browser.newPage(), (late) => {
2760
+ void this.trackPageClose(late);
2761
+ });
2762
+ if (page === void 0) return void 0;
2763
+ if (this.stale(session) || this.browser !== browser) {
2764
+ await this.bounded(deadline, () => this.closeCurrent());
2765
+ return void 0;
2766
+ }
2767
+ this.page = page;
2768
+ return page;
2769
+ }
2770
+ /** A handle that arrived too late is closed, and still counts as work disposal joins. */
2771
+ trackClose(browser) {
2772
+ const closing = closeBounded(browser);
2773
+ this.tracked.push(closing);
2774
+ void closing.finally(() => {
2775
+ this.tracked = this.tracked.filter((item) => item !== closing);
2776
+ });
2777
+ return closing;
2778
+ }
2779
+ trackPageClose(page) {
2780
+ try {
2781
+ return Promise.resolve(page.close?.()).catch(() => void 0);
2782
+ } catch {
2783
+ return Promise.resolve();
2784
+ }
2785
+ }
2786
+ adopt(browser) {
2787
+ this.browser = browser;
2788
+ browser.on?.("disconnected", () => {
2789
+ if (this.browser === browser) {
2790
+ this.browser = void 0;
2791
+ this.page = void 0;
2792
+ }
2793
+ });
2794
+ }
2795
+ /**
2796
+ * Bound one launch step by the attempt's shared deadline. A step that settles only
2797
+ * after that deadline still hands its handle back: dropping it would leak a Chrome
2798
+ * process nobody can close.
2799
+ */
2800
+ async bounded(deadline, start, discardOnLate) {
2801
+ let timer;
2802
+ let expired = false;
2803
+ const work = (async () => {
2804
+ try {
2805
+ return await start();
2806
+ } catch {
2807
+ return void 0;
2808
+ }
2809
+ })().then((value) => {
2810
+ if (expired && value !== void 0) discardOnLate?.(value);
2811
+ return value;
2812
+ });
2813
+ const timeout = new Promise((resolve) => {
2814
+ timer = setTimeout(() => {
2815
+ expired = true;
2816
+ resolve(void 0);
2817
+ }, Math.max(1, deadline - Date.now()));
2818
+ });
2819
+ try {
2820
+ return await Promise.race([work, timeout]);
2821
+ } finally {
2822
+ if (timer !== void 0) clearTimeout(timer);
2823
+ }
2824
+ }
2825
+ };
2826
+ function closeBounded(browser) {
2827
+ let timer;
2828
+ return Promise.race([
2829
+ (async () => {
2830
+ try {
2831
+ await browser.close();
2832
+ } catch {
2833
+ }
2834
+ })(),
2835
+ new Promise((resolve) => {
2836
+ timer = setTimeout(resolve, ACTIVATION_CLOSE_TIMEOUT_MS);
2837
+ })
2838
+ ]).finally(() => {
2839
+ if (timer !== void 0) clearTimeout(timer);
2462
2840
  });
2463
2841
  }
2842
+ async function defaultLaunch() {
2843
+ const playwright = await import("playwright-core");
2844
+ return playwright.chromium.launch(chromeLaunchOptions());
2845
+ }
2464
2846
 
2465
2847
  // src/quota.ts
2466
2848
  import { attributionHeaders as attributionHeaders3 } from "@deepseek-ai/dsh-llm";
@@ -2468,6 +2850,7 @@ var API_BASE = "https://amr-api.open-design.ai/api/v1";
2468
2850
  var CURRENT_URL = `${API_BASE}/workspaces/current`;
2469
2851
  var SUMMARY_URL = `${API_BASE}/billing/summary`;
2470
2852
  var usageUrl = (workspace) => `${API_BASE}/workspaces/${encodeURIComponent(workspace)}/billing/coding-plan-usage`;
2853
+ var preflightUrl = (workspace) => `${API_BASE}/workspaces/${encodeURIComponent(workspace)}/billing/preflight`;
2471
2854
  var ConsoleError = class extends Error {
2472
2855
  constructor(kind, status) {
2473
2856
  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 +2861,90 @@ var ConsoleError = class extends Error {
2478
2861
  status;
2479
2862
  };
2480
2863
  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 });
2864
+ signal?.throwIfAborted();
2865
+ if (session.credentialType === "device") {
2866
+ let target;
2867
+ try {
2868
+ target = new URL(url);
2869
+ } catch {
2870
+ throw new ConsoleError("response");
2506
2871
  }
2507
- raw += decoder.decode();
2508
- } finally {
2509
- await reader.cancel().catch(() => void 0);
2510
- reader.releaseLock();
2872
+ if (target.origin !== "https://amr-api.open-design.ai" || target.username || target.password) throw new ConsoleError("response");
2511
2873
  }
2512
- let body;
2874
+ const requestSignal = combinedSignal(signal, 2e4);
2875
+ let rejectAbort = () => void 0;
2876
+ const aborted = new Promise((_resolve, reject) => {
2877
+ rejectAbort = reject;
2878
+ });
2879
+ const onAbort = () => rejectAbort(new ConsoleError("network"));
2880
+ requestSignal.addEventListener("abort", onAbort, { once: true });
2881
+ const read = async () => {
2882
+ const response = await fetcher(url, { headers: { ...attributionHeaders3(), accept: "application/json", ...session.headers }, signal: requestSignal, redirect: "error", credentials: "omit" });
2883
+ const cancelBody = () => {
2884
+ void response.body?.cancel().catch(() => void 0);
2885
+ };
2886
+ if (requestSignal.aborted) {
2887
+ cancelBody();
2888
+ throw new ConsoleError("network");
2889
+ }
2890
+ if (response.redirected || response.url && response.url !== url) {
2891
+ cancelBody();
2892
+ throw new ConsoleError("response");
2893
+ }
2894
+ if (response.status === 401 || response.status === 403) {
2895
+ cancelBody();
2896
+ throw new ConsoleError(response.status === 401 ? "auth" : "permission", response.status);
2897
+ }
2898
+ if (!response.ok) {
2899
+ cancelBody();
2900
+ throw new ConsoleError(response.status === 429 ? "rate-limit" : "network", response.status);
2901
+ }
2902
+ const declared = response.headers.get("content-length");
2903
+ if (declared !== null && (!/^\d+$/u.test(declared) || Number(declared) > 2 * 1024 * 1024)) {
2904
+ cancelBody();
2905
+ throw new ConsoleError("response");
2906
+ }
2907
+ const reader = response.body?.getReader();
2908
+ if (!reader) throw new ConsoleError("response");
2909
+ const cancelReader = () => {
2910
+ void reader.cancel().catch(() => void 0);
2911
+ };
2912
+ requestSignal.addEventListener("abort", cancelReader, { once: true });
2913
+ let size = 0, raw = "";
2914
+ const decoder = new TextDecoder("utf-8", { fatal: true });
2915
+ try {
2916
+ while (true) {
2917
+ const chunk = await reader.read();
2918
+ if (requestSignal.aborted) throw new ConsoleError("network");
2919
+ if (chunk.done) break;
2920
+ size += chunk.value.byteLength;
2921
+ if (size > 2 * 1024 * 1024) throw new ConsoleError("response");
2922
+ raw += decoder.decode(chunk.value, { stream: true });
2923
+ }
2924
+ raw += decoder.decode();
2925
+ } finally {
2926
+ requestSignal.removeEventListener("abort", cancelReader);
2927
+ cancelReader();
2928
+ reader.releaseLock();
2929
+ }
2930
+ let body;
2931
+ try {
2932
+ body = JSON.parse(raw);
2933
+ } catch {
2934
+ throw new ConsoleError("response");
2935
+ }
2936
+ if (record(body) && (body.code === 401 || body.code === "401")) throw new ConsoleError("auth", 401);
2937
+ if (record(body) && (body.code === 403 || body.code === "403")) throw new ConsoleError("permission", 403);
2938
+ return body;
2939
+ };
2513
2940
  try {
2514
- body = JSON.parse(raw);
2515
- } catch {
2516
- throw new ConsoleError("response");
2941
+ return await Promise.race([read(), aborted]);
2942
+ } catch (error) {
2943
+ signal?.throwIfAborted();
2944
+ throw error instanceof ConsoleError ? error : new ConsoleError("network");
2945
+ } finally {
2946
+ requestSignal.removeEventListener("abort", onAbort);
2517
2947
  }
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
2948
  }
2522
2949
  function number(value, negative = false) {
2523
2950
  const n = typeof value === "number" ? value : typeof value === "string" && /^-?\d+(\.\d+)?$/u.test(value.trim()) ? Number(value) : NaN;
@@ -2555,7 +2982,51 @@ function normalizeBalances(summary) {
2555
2982
  const totalUsd = number(summary.balanceUsd, true) ?? number(summary.totalAvailableCreditsUsd, true) ?? (divisor !== void 0 && total !== void 0 ? total / divisor : void 0);
2556
2983
  return { ...divisor !== void 0 && subscription !== void 0 ? { subscriptionUsd: subscription / divisor } : {}, ...divisor !== void 0 && recharge !== void 0 ? { rechargeUsd: recharge / divisor } : {}, ...totalUsd !== void 0 ? { totalUsd } : {} };
2557
2984
  }
2985
+ async function readDeviceQuota(session, signal, fetcher = fetch) {
2986
+ const common = { source: session.source, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
2987
+ let current;
2988
+ try {
2989
+ current = await consoleJson(CURRENT_URL, session, signal, fetcher);
2990
+ } catch (error) {
2991
+ signal?.throwIfAborted();
2992
+ 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" };
2993
+ throw error;
2994
+ }
2995
+ if (!record(current)) throw new ConsoleError("response");
2996
+ const workspaceId = typeof current.activeWorkspaceId === "string" && current.activeWorkspaceId.trim() ? current.activeWorkspaceId : current.workspaceId;
2997
+ if (typeof workspaceId !== "string" || !/^[A-Za-z0-9_-]{1,64}$/u.test(workspaceId)) throw new ConsoleError("response");
2998
+ const personal = current.workspaceType === "personal";
2999
+ const [preflight, billing] = await Promise.allSettled([
3000
+ consoleJson(preflightUrl(workspaceId), session, signal, fetcher),
3001
+ personal ? consoleJson(SUMMARY_URL, session, signal, fetcher) : Promise.resolve(void 0)
3002
+ ]);
3003
+ signal?.throwIfAborted();
3004
+ for (const item of [preflight, billing]) if (item.status === "rejected" && item.reason instanceof ConsoleError && item.reason.kind === "auth") throw item.reason;
3005
+ const summary = billing.status === "fulfilled" ? billing.value : void 0;
3006
+ const flight = preflight.status === "fulfilled" && record(preflight.value) ? preflight.value : void 0;
3007
+ let normalized;
3008
+ try {
3009
+ normalized = normalizeQuota(flight?.codingPlan, personal ? summary : void 0);
3010
+ } catch (error) {
3011
+ if (!(error instanceof ConsoleError)) throw error;
3012
+ }
3013
+ const balances = personal ? normalizeBalances(summary) : {};
3014
+ const total = personal ? number(flight?.balanceUsd, true) : void 0;
3015
+ if (total !== void 0) balances.totalUsd = total;
3016
+ const permission = [preflight, billing].some((item) => item.status === "rejected" && item.reason instanceof ConsoleError && item.reason.kind === "permission");
3017
+ if (!normalized && !Object.keys(balances).length && !permission) {
3018
+ if (preflight.status === "rejected") throw preflight.reason;
3019
+ throw new ConsoleError("response");
3020
+ }
3021
+ const windows = normalized?.windows ?? [];
3022
+ const missingWindows = normalized?.eligible !== false && !windows.length;
3023
+ const missingBalances = personal && Object.keys(balances).length < 3;
3024
+ const partial = missingWindows || missingBalances || !personal || permission;
3025
+ 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;
3026
+ return { ...common, ...normalized, ...normalizeSubscription(personal ? summary : void 0), windows, balances, state: partial ? "partial" : "ok", ...note ? { note } : {} };
3027
+ }
2558
3028
  async function readQuota(session, signal, fetcher = fetch) {
3029
+ if (session.credentialType === "device") return readDeviceQuota(session, signal, fetcher);
2559
3030
  const current = await consoleJson(CURRENT_URL, session, signal, fetcher);
2560
3031
  if (!record(current) || typeof current.workspaceId !== "string" || !/^[A-Za-z0-9_-]{1,64}$/u.test(current.workspaceId)) throw new ConsoleError("response");
2561
3032
  const personal = current.workspaceType === "personal";
@@ -2660,6 +3131,7 @@ var MAX_API_KEY = 16384;
2660
3131
  var HTTP_ASCII = /^[\x21-\x7e]+$/u;
2661
3132
  var ENV_REF = /^[A-Za-z_][A-Za-z0-9_]*$/u;
2662
3133
  var MODEL_ID2 = /^[A-Za-z0-9._:/-]{1,128}$/u;
3134
+ var ACTIVATION_OPEN_CODES = ["opened", "no-link", "invalid-url", "unavailable", "failed"];
2663
3135
  var LOGIN_PHASES = ["idle", "launching", "waiting", "verifying", "success", "cancelled", "error"];
2664
3136
  var QUOTA_STATES = ["ok", "partial", "need-login", "error"];
2665
3137
  var SESSION_SOURCES = ["desktop", "login", "environment"];
@@ -2747,9 +3219,19 @@ function parseLoginState(value) {
2747
3219
  if (!record(value)) throw new Error("\u767B\u5F55\u72B6\u6001\u5FC5\u987B\u662F\u5BF9\u8C61");
2748
3220
  const jobId = value.jobId;
2749
3221
  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");
3222
+ const state = member(value.state, LOGIN_PHASES, "\u767B\u5F55\u9636\u6BB5");
3223
+ let approval = {};
3224
+ if (state === "waiting" && (value.activationUrl !== void 0 || value.userCode !== void 0 || value.expiresAt !== void 0)) {
3225
+ const userCode = text(value.userCode, "\u8BBE\u5907\u6388\u6743\u7801", 64);
3226
+ if (!/^[A-Za-z0-9-]+$/u.test(userCode)) throw new Error("\u8BBE\u5907\u6388\u6743\u7801\u683C\u5F0F\u65E0\u6548");
3227
+ const activationUrl = validateActivationUrl(value.activationUrl, userCode);
3228
+ if (activationUrl === null) throw new Error("\u8BBE\u5907\u6388\u6743\u94FE\u63A5\u65E0\u6548");
3229
+ approval = { activationUrl, userCode, expiresAt: isoTime(value.expiresAt, "\u8BBE\u5907\u6388\u6743\u5230\u671F\u65F6\u95F4") };
3230
+ }
2750
3231
  return {
2751
3232
  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"),
3233
+ state,
3234
+ ...approval,
2753
3235
  ...value.message === void 0 ? {} : { message: text(value.message, "\u767B\u5F55\u8BF4\u660E", MAX_MESSAGE) },
2754
3236
  ...value.startedAt === void 0 ? {} : { startedAt: isoTime(value.startedAt, "\u767B\u5F55\u5F00\u59CB\u65F6\u95F4") },
2755
3237
  ...value.finishedAt === void 0 ? {} : { finishedAt: isoTime(value.finishedAt, "\u767B\u5F55\u7ED3\u675F\u65F6\u95F4") }
@@ -2825,6 +3307,10 @@ function parseSaveApiKeyRequest(value) {
2825
3307
  if (key.length > MAX_API_KEY) throw new Error(`API Key \u4E0D\u80FD\u8D85\u8FC7 ${MAX_API_KEY} \u5B57\u7B26`);
2826
3308
  return { value: key, expectedRevision: text(value.expectedRevision, "\u914D\u7F6E\u7248\u672C", MAX_REVISION) };
2827
3309
  }
3310
+ function parseActivationOpenState(value) {
3311
+ if (!record(value)) throw new Error("\u6388\u6743\u9875\u6253\u5F00\u7ED3\u679C\u5FC5\u987B\u662F\u5BF9\u8C61");
3312
+ return { code: member(value.code, ACTIVATION_OPEN_CODES, "\u6388\u6743\u9875\u6253\u5F00\u7ED3\u679C"), opened: flag(value.opened, "\u6388\u6743\u9875\u662F\u5426\u5DF2\u6253\u5F00") };
3313
+ }
2828
3314
 
2829
3315
  // src/index.ts
2830
3316
  var name = "llm-opendesign-corouter";
@@ -2834,17 +3320,20 @@ var SESSION = credentialKey(name, "console");
2834
3320
  var hash = (value) => createHash("sha256").update(JSON.stringify(value) ?? "").digest("hex");
2835
3321
  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
3322
  function grantOf(value) {
2837
- if (value?.kind !== "grant" || !record(value.payload) || typeof value.payload.cookie !== "string") return;
3323
+ if (value?.kind !== "grant" || !record(value.payload)) return;
3324
+ if (value.payload.kind === "device") return deviceGrant(value.payload);
3325
+ if (typeof value.payload.cookie !== "string") return;
2838
3326
  return { cookie: value.payload.cookie, ...typeof value.payload.expiresAt === "number" ? { expiresAt: value.payload.expiresAt } : {} };
2839
3327
  }
2840
- var _saveApiKey_dec, _cancelLogin_dec, _loginStatus_dec, _startLogin_dec, _refreshQuotaNow_dec, _refreshQuota_dec, _refreshModels_dec, _status_dec, _a, _init;
2841
- var OpenDesignCoRouterService = class extends (_a = TypertRemoteService, _status_dec = [Remote("status")], _refreshModels_dec = [Remote("refreshModels")], _refreshQuota_dec = [Remote("refreshQuota")], _refreshQuotaNow_dec = [Remote("refreshQuotaNow")], _startLogin_dec = [Remote("startLogin")], _loginStatus_dec = [Remote("loginStatus")], _cancelLogin_dec = [Remote("cancelLogin")], _saveApiKey_dec = [Remote("saveApiKey")], _a) {
3328
+ var _saveApiKey_dec, _openActivationPage_dec, _cancelLogin_dec, _loginStatus_dec, _startLogin_dec, _refreshQuotaNow_dec, _refreshQuota_dec, _refreshModels_dec, _status_dec, _a, _init;
3329
+ var OpenDesignCoRouterService = class extends (_a = TypertRemoteService, _status_dec = [Remote("status")], _refreshModels_dec = [Remote("refreshModels")], _refreshQuota_dec = [Remote("refreshQuota")], _refreshQuotaNow_dec = [Remote("refreshQuotaNow")], _startLogin_dec = [Remote("startLogin")], _loginStatus_dec = [Remote("loginStatus")], _cancelLogin_dec = [Remote("cancelLogin")], _openActivationPage_dec = [Remote("openActivationPage")], _saveApiKey_dec = [Remote("saveApiKey")], _a) {
2842
3330
  constructor(ctx, config, dependencies = { resolveSession, readQuota, readDesktopQuota }) {
2843
3331
  super(ctx, "openDesignCoRouter");
2844
3332
  this.dependencies = dependencies;
2845
3333
  __runInitializers(_init, 5, this);
2846
3334
  __publicField(this, "_config");
2847
3335
  __publicField(this, "_login");
3336
+ __publicField(this, "_activation");
2848
3337
  __publicField(this, "_lifetime", new AbortController());
2849
3338
  __publicField(this, "_revision", randomUUID2());
2850
3339
  __publicField(this, "_fingerprint", "");
@@ -2871,13 +3360,7 @@ var OpenDesignCoRouterService = class extends (_a = TypertRemoteService, _status
2871
3360
  this._credentials();
2872
3361
  return { revision: expectedRevision, sessionHash: hash(state.sessionRecord), configHash: hash(state.config) };
2873
3362
  },
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) => {
3363
+ commit: async (tokens, captured, signal) => {
2881
3364
  if (!record(captured) || typeof captured.revision !== "string" || typeof captured.sessionHash !== "string" || typeof captured.configHash !== "string") throw this._conflict();
2882
3365
  await this._state();
2883
3366
  this._expect(captured.revision);
@@ -2885,14 +3368,27 @@ var OpenDesignCoRouterService = class extends (_a = TypertRemoteService, _status
2885
3368
  await this._credentials().modifyRecord(SESSION, async (current) => {
2886
3369
  signal.throwIfAborted();
2887
3370
  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() } };
3371
+ const grant = deviceGrant({ kind: "device", ...tokens });
3372
+ if (!grant) throw new ConsoleError("auth");
3373
+ return { kind: "grant", payload: { ...grant, savedAt: (/* @__PURE__ */ new Date()).toISOString() } };
2889
3374
  });
2890
3375
  this._invalidateQuota();
3376
+ this._modelsFingerprint = "";
3377
+ this._invalidateModels();
3378
+ }
3379
+ }, {
3380
+ ...dependencies.loginOptions,
3381
+ // The Host owns the temporary window, so it closes it on every terminal transition —
3382
+ // including expiry, which happens on a timer even when no UI is polling.
3383
+ onSettled: () => {
3384
+ void this._activation.close();
2891
3385
  }
2892
3386
  });
3387
+ this._activation = new ActivationLauncher(dependencies.activationOptions);
2893
3388
  ctx.effect(() => () => {
2894
3389
  this._lifetime.abort();
2895
3390
  this._login.dispose();
3391
+ return this._activation.dispose();
2896
3392
  });
2897
3393
  }
2898
3394
  dependencies;
@@ -2904,28 +3400,43 @@ var OpenDesignCoRouterService = class extends (_a = TypertRemoteService, _status
2904
3400
  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
3401
  return service;
2906
3402
  }
2907
- async resolveApiKey(ref) {
3403
+ async _key(ref, baseURL, apiRecord, sessionRecord) {
2908
3404
  const credentials = this.ctx.get("credentials");
2909
3405
  const ambient = credentials ? (await credentials.resolve(credentialRef(ref)))?.value : launchEnvironmentOf(this.ctx).get(ref)?.value;
2910
3406
  if (ambient?.trim()) return ambient;
2911
- const saved = await credentials?.readRecord(API_KEY);
2912
- return saved?.kind === "api-key" ? saved.key : void 0;
3407
+ if (apiRecord?.kind === "api-key" && apiRecord.key?.trim()) return apiRecord.key;
3408
+ const grant = grantOf(sessionRecord);
3409
+ return isOfficialRuntimeUrl(baseURL) && grant && "kind" in grant && grant.kind === "device" ? grant.runtimeKey : void 0;
3410
+ }
3411
+ async resolveApiKey(ref, baseURL = this._config().baseURL) {
3412
+ const credentials = this.ctx.get("credentials");
3413
+ const [apiRecord, sessionRecord] = await Promise.all([credentials?.readRecord(API_KEY), credentials?.readRecord(SESSION)]);
3414
+ return this._key(ref, baseURL, apiRecord, sessionRecord);
3415
+ }
3416
+ _invalidateModels() {
3417
+ const changed = this._modelsUpdatedAt !== void 0;
3418
+ this._modelsEpoch++;
3419
+ this._modelsController?.abort();
3420
+ this._modelsPending = void 0;
3421
+ this._models = MODELS;
3422
+ this._modelsUpdatedAt = void 0;
3423
+ this._modelsError = void 0;
3424
+ if (changed) {
3425
+ try {
3426
+ this.dependencies.modelsChanged?.();
3427
+ } catch {
3428
+ }
3429
+ }
2913
3430
  }
2914
3431
  async _state() {
2915
3432
  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)]);
3433
+ const [apiRecord, sessionRecord] = await Promise.all([credentials?.readRecord(API_KEY), credentials?.readRecord(SESSION)]);
3434
+ const key = await this._key(config.apiKeyEnv, config.baseURL, apiRecord, sessionRecord);
2917
3435
  const state = { apiRecord, sessionRecord, key, config };
2918
3436
  const modelsFingerprint = hash({ key, baseURL: config.baseURL });
2919
3437
  if (modelsFingerprint !== this._modelsFingerprint) {
2920
- const changed = this._modelsUpdatedAt !== void 0;
2921
3438
  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?.();
3439
+ this._invalidateModels();
2929
3440
  }
2930
3441
  const fingerprint = hash(state);
2931
3442
  if (fingerprint !== this._fingerprint) {
@@ -3048,9 +3559,10 @@ var OpenDesignCoRouterService = class extends (_a = TypertRemoteService, _status
3048
3559
  let identity;
3049
3560
  let refreshFailed = false;
3050
3561
  try {
3562
+ const grant = grantOf(await this.ctx.get("credentials")?.readRecord(SESSION));
3051
3563
  const authDeps = {
3052
3564
  useDesktop: config.useDesktopCredential,
3053
- readGrant: async () => grantOf(await this.ctx.get("credentials")?.readRecord(SESSION)),
3565
+ readGrant: async () => grant,
3054
3566
  resolveRef: async (ref) => {
3055
3567
  const credentials = this.ctx.get("credentials");
3056
3568
  return credentials ? (await credentials.resolve(credentialRef(ref)))?.value : launchEnvironmentOf(this.ctx).get(ref)?.value;
@@ -3068,7 +3580,10 @@ var OpenDesignCoRouterService = class extends (_a = TypertRemoteService, _status
3068
3580
  if (fallback.state === "ready") {
3069
3581
  try {
3070
3582
  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}` : ""}` };
3583
+ if (complete.state === "ok" && (complete.windows.length > 0 || complete.eligible === false)) {
3584
+ 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}` : ""}` };
3585
+ identity = hash(fallback.session);
3586
+ }
3072
3587
  } catch (error) {
3073
3588
  if (signal.aborted) throw error;
3074
3589
  }
@@ -3078,6 +3593,7 @@ var OpenDesignCoRouterService = class extends (_a = TypertRemoteService, _status
3078
3593
  if (!(error instanceof ConsoleError) || !["auth", "permission"].includes(error.kind)) throw error;
3079
3594
  const fallback = await this.dependencies.resolveSession({ ...authDeps, useDesktop: false });
3080
3595
  if (fallback.state !== "ready") throw new ConsoleError("auth");
3596
+ identity = hash(fallback.session);
3081
3597
  result = await this.dependencies.readQuota(fallback.session, signal);
3082
3598
  }
3083
3599
  }
@@ -3124,6 +3640,12 @@ var OpenDesignCoRouterService = class extends (_a = TypertRemoteService, _status
3124
3640
  cancelLogin(input) {
3125
3641
  return parseLoginState(this._login.cancel(parseCancelLoginRequest(input)));
3126
3642
  }
3643
+ async openActivationPage() {
3644
+ const url = this._login.activationLink();
3645
+ if (url === void 0) return { code: "no-link", opened: false };
3646
+ const result = await this._activation.open(url);
3647
+ return parseActivationOpenState({ code: result.code, opened: result.opened });
3648
+ }
3127
3649
  async saveApiKey(input) {
3128
3650
  const request = parseSaveApiKeyRequest(input), state = await this._state();
3129
3651
  this._expect(request.expectedRevision);
@@ -3143,6 +3665,7 @@ __decorateElement(_init, 1, "refreshQuotaNow", _refreshQuotaNow_dec, OpenDesignC
3143
3665
  __decorateElement(_init, 1, "startLogin", _startLogin_dec, OpenDesignCoRouterService);
3144
3666
  __decorateElement(_init, 1, "loginStatus", _loginStatus_dec, OpenDesignCoRouterService);
3145
3667
  __decorateElement(_init, 1, "cancelLogin", _cancelLogin_dec, OpenDesignCoRouterService);
3668
+ __decorateElement(_init, 1, "openActivationPage", _openActivationPage_dec, OpenDesignCoRouterService);
3146
3669
  __decorateElement(_init, 1, "saveApiKey", _saveApiKey_dec, OpenDesignCoRouterService);
3147
3670
  __decoratorMetadata(_init, OpenDesignCoRouterService);
3148
3671
  function apply(ctx, config) {
@@ -3155,7 +3678,7 @@ function apply(ctx, config) {
3155
3678
  const adapter = new CoRouterAdapter({
3156
3679
  config: current,
3157
3680
  models: () => service.modelCatalog(),
3158
- apiKey: (ref) => service.resolveApiKey(ref),
3681
+ apiKey: (ref, baseURL) => service.resolveApiKey(ref, baseURL),
3159
3682
  onEmptyTurn: () => {
3160
3683
  service.guardedTurns++;
3161
3684
  },
@@ -3178,7 +3701,7 @@ function apply(ctx, config) {
3178
3701
  ctx.llm.registerModelDiscovery(SETTINGS_NS, async (request, signal) => {
3179
3702
  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
3703
  const config2 = current();
3181
- const key = request.apiKey ?? await service.resolveApiKey(config2.apiKeyEnv);
3704
+ const key = request.apiKey ?? await service.resolveApiKey(config2.apiKeyEnv, config2.baseURL);
3182
3705
  if (!key) throw new LlmError11("\u8BF7\u5148\u914D\u7F6E CoRouter API Key", "MISSING_CREDENTIAL");
3183
3706
  const models = await fetchModelCatalog(config2.baseURL, key, signal);
3184
3707
  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 } : {} }));