dsh-opendesign-corouter 0.1.4 → 0.1.6

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.
@@ -8,22 +8,29 @@ function record(value) {
8
8
  }
9
9
 
10
10
  // src/device-auth.ts
11
- function validateActivationUrl(value, userCode) {
11
+ function validateActivationUrl(value, userCode, deviceId) {
12
12
  if (typeof value !== "string" || value.length > 512 || !/^https:\/\/open-design\.ai\/cloud\/cli\/activate(?:\?[^#]*)?$/u.test(value) || /[\s\\]/u.test(value)) return null;
13
13
  try {
14
14
  const url = new URL(value);
15
15
  if (url.origin !== "https://open-design.ai" || url.pathname !== "/cloud/cli/activate" || url.username || url.password || url.hash) return null;
16
16
  const entries = [...url.searchParams];
17
- if (entries.length > 1) return null;
18
- if (entries.length) {
17
+ if (entries.length === 2) {
18
+ if (entries.some(([key]) => key !== "deviceId" && key !== "userCode")) return null;
19
+ const id = url.searchParams.get("deviceId");
20
+ const code = url.searchParams.get("userCode");
21
+ if (!validDeviceId(id) || !validUserCode(code) || deviceId !== void 0 && id !== deviceId || userCode !== void 0 && code !== userCode) return null;
22
+ } else if (entries.length === 1) {
19
23
  const entry = entries[0];
20
24
  if (!["code", "user_code"].includes(entry[0]) || !validUserCode(entry[1]) || userCode !== void 0 && entry[1] !== userCode) return null;
21
- }
25
+ } else if (entries.length > 2) return null;
22
26
  return url.href;
23
27
  } catch {
24
28
  return null;
25
29
  }
26
30
  }
31
+ function validDeviceId(value) {
32
+ return typeof value === "string" && /^[a-z0-9_-]{1,200}$/iu.test(value);
33
+ }
27
34
  function validUserCode(value) {
28
35
  return typeof value === "string" && /^[a-z0-9][a-z0-9-]{3,31}$/iu.test(value);
29
36
  }
@@ -89,6 +96,7 @@ var MAX_API_KEY = 16384;
89
96
  var HTTP_ASCII = /^[\x21-\x7e]+$/u;
90
97
  var ENV_REF = /^[A-Za-z_][A-Za-z0-9_]*$/u;
91
98
  var MODEL_ID = /^[A-Za-z0-9._:/-]{1,128}$/u;
99
+ var ACTIVATION_OPEN_CODES = ["opened", "no-link", "invalid-url", "unavailable", "failed"];
92
100
  var LOGIN_PHASES = ["idle", "launching", "waiting", "verifying", "success", "cancelled", "error"];
93
101
  var QUOTA_STATES = ["ok", "partial", "need-login", "error"];
94
102
  var SESSION_SOURCES = ["desktop", "login", "environment"];
@@ -264,6 +272,10 @@ function parseSaveApiKeyRequest(value) {
264
272
  if (key.length > MAX_API_KEY) throw new Error(`API Key \u4E0D\u80FD\u8D85\u8FC7 ${MAX_API_KEY} \u5B57\u7B26`);
265
273
  return { value: key, expectedRevision: text(value.expectedRevision, "\u914D\u7F6E\u7248\u672C", MAX_REVISION) };
266
274
  }
275
+ function parseActivationOpenState(value) {
276
+ if (!record(value)) throw new Error("\u6388\u6743\u9875\u6253\u5F00\u7ED3\u679C\u5FC5\u987B\u662F\u5BF9\u8C61");
277
+ 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") };
278
+ }
267
279
 
268
280
  // src/generated/typert.remote-client.ts
269
281
  var StatusCodec = { mode: "strict", typeSymbol: "dsh-opendesign-corouter#Status", create: () => z.unknown().transform((value) => parseStatus(value)) };
@@ -271,6 +283,7 @@ var QuotaSnapshotCodec = { mode: "strict", typeSymbol: "dsh-opendesign-corouter#
271
283
  var LoginStateCodec = { mode: "strict", typeSymbol: "dsh-opendesign-corouter#LoginState", create: () => z.unknown().transform((value) => parseLoginState(value)) };
272
284
  var StartLoginRequestCodec = { mode: "strict", typeSymbol: "dsh-opendesign-corouter#StartLoginRequest", create: () => z.unknown().transform((value) => parseStartLoginRequest(value)) };
273
285
  var CancelLoginRequestCodec = { mode: "strict", typeSymbol: "dsh-opendesign-corouter#CancelLoginRequest", create: () => z.unknown().transform((value) => parseCancelLoginRequest(value)) };
286
+ var ActivationOpenStateCodec = { mode: "strict", typeSymbol: "dsh-opendesign-corouter#ActivationOpenState", create: () => z.unknown().transform((value) => parseActivationOpenState(value)) };
274
287
  var SaveApiKeyRequestCodec = { mode: "strict", typeSymbol: "dsh-opendesign-corouter#SaveApiKeyRequest", create: () => z.unknown().transform((value) => parseSaveApiKeyRequest(value)) };
275
288
  var TYPERT_REMOTE = createTypertRemoteInfo({ package: "dsh-opendesign-corouter", methods: {
276
289
  "openDesignCoRouter/status": { service: "openDesignCoRouter", invocation: { kind: "direct" }, parameters: [], result: StatusCodec },
@@ -280,6 +293,7 @@ var TYPERT_REMOTE = createTypertRemoteInfo({ package: "dsh-opendesign-corouter",
280
293
  "openDesignCoRouter/startLogin": { service: "openDesignCoRouter", invocation: { kind: "direct" }, parameters: [{ name: "input", source: "json", codec: StartLoginRequestCodec, strictCodec: StartLoginRequestCodec }], result: LoginStateCodec },
281
294
  "openDesignCoRouter/loginStatus": { service: "openDesignCoRouter", invocation: { kind: "direct" }, parameters: [], result: LoginStateCodec },
282
295
  "openDesignCoRouter/cancelLogin": { service: "openDesignCoRouter", invocation: { kind: "direct" }, parameters: [{ name: "input", source: "json", codec: CancelLoginRequestCodec, strictCodec: CancelLoginRequestCodec }], result: LoginStateCodec },
296
+ "openDesignCoRouter/openActivationPage": { service: "openDesignCoRouter", invocation: { kind: "direct" }, parameters: [], result: ActivationOpenStateCodec },
283
297
  "openDesignCoRouter/saveApiKey": { service: "openDesignCoRouter", invocation: { kind: "direct" }, parameters: [{ name: "input", source: "json", codec: SaveApiKeyRequestCodec, strictCodec: SaveApiKeyRequestCodec }], result: StatusCodec }
284
298
  } }, z);
285
299
  var typert_remote_client_default = TYPERT_REMOTE;
package/lib/index.d.ts CHANGED
@@ -5,9 +5,10 @@ import type { CatalogModel } from './catalog.js';
5
5
  import { readConfig, type LiveConfig } from './config.js';
6
6
  import { resolveSession } from './auth.js';
7
7
  import { type LoginOptions } from './login.js';
8
+ import { type ActivationLauncherOptions } from './browser-open.js';
8
9
  import { readQuota, type QuotaSnapshot } from './quota.js';
9
10
  import { readDesktopQuota } from './desktop-quota.js';
10
- import { type Status, type LoginState, type StartLoginRequest, type CancelLoginRequest, type SaveApiKeyRequest } from './remote.js';
11
+ import { type Status, type LoginState, type StartLoginRequest, type CancelLoginRequest, type SaveApiKeyRequest, type ActivationOpenState } from './remote.js';
11
12
  export { Config } from './config.js';
12
13
  export { CoRouterAdapter } from './adapter.js';
13
14
  export { MODELS } from './catalog.js';
@@ -20,11 +21,13 @@ export interface HostDependencies {
20
21
  fetchModels?: typeof fetchModelCatalog;
21
22
  modelsChanged?: () => void;
22
23
  loginOptions?: LoginOptions;
24
+ activationOptions?: ActivationLauncherOptions;
23
25
  }
24
26
  export declare class OpenDesignCoRouterService extends TypertRemoteService {
25
27
  readonly dependencies: HostDependencies;
26
28
  private readonly _config;
27
29
  private readonly _login;
30
+ private readonly _activation;
28
31
  private readonly _lifetime;
29
32
  private _revision;
30
33
  private _fingerprint;
@@ -62,6 +65,7 @@ export declare class OpenDesignCoRouterService extends TypertRemoteService {
62
65
  startLogin(input: StartLoginRequest): Promise<LoginState>;
63
66
  loginStatus(): LoginState;
64
67
  cancelLogin(input: CancelLoginRequest): LoginState;
68
+ openActivationPage(): Promise<ActivationOpenState>;
65
69
  saveApiKey(input: SaveApiKeyRequest): Promise<Status>;
66
70
  }
67
71
  declare module '@deepseek-ai/cordis' {
package/lib/index.js CHANGED
@@ -159,7 +159,7 @@ var MODELS = [
159
159
  reasoningEfforts: EFFORTS_LOW_HIGH
160
160
  },
161
161
  { id: "glm-5.2", name: "GLM-5.2", group: "paid" },
162
- // ── Moonshot(2) ───────────────────────────────────────────
162
+ // ── Moonshot(3) ───────────────────────────────────────────
163
163
  {
164
164
  id: "kimi-k2.7-code",
165
165
  name: "Kimi K2.7 Code",
@@ -170,7 +170,9 @@ var MODELS = [
170
170
  reasoningEfforts: EFFORTS_STD
171
171
  },
172
172
  { id: "kimi-k3", name: "Kimi K3", group: "paid" },
173
- // ── Google(2) ─────────────────────────────────────────────
173
+ { id: "kimi-k3.1-pro", name: "Kimi K3.1 Pro", group: "paid" },
174
+ // ── Google(3) ─────────────────────────────────────────────
175
+ { id: "gemini-4-argon", name: "Gemini 4 Argon", group: "paid" },
174
176
  { id: "gemini-3.8-flash", name: "Gemini 3.8 Flash", group: "paid" },
175
177
  { id: "gemini-3.7-flash", name: "Gemini 3.7 Flash", group: "paid" },
176
178
  // ── xAI(3) ────────────────────────────────────────────────
@@ -207,7 +209,11 @@ var MODELS = [
207
209
  reasoningEfforts: EFFORTS_STD
208
210
  },
209
211
  // ── Alibaba(1) ────────────────────────────────────────────
210
- { id: "qwen3.7-max", name: "Qwen3.7 Max", group: "paid" }
212
+ { id: "qwen3.7-max", name: "Qwen3.7 Max", group: "paid" },
213
+ // Official API additions; display labels for undocumented entries are local formatting.
214
+ { id: "minimax-m3.1-flash", name: "MiniMax M3.1 Flash", group: "plan" },
215
+ { id: "muse-spark-1.3", name: "Muse Spark 1.3", group: "paid" },
216
+ { id: "muse-wonder", name: "Muse Wonder", group: "paid" }
211
217
  ];
212
218
  var BY_ID = new Map(MODELS.map((model) => [model.id, model]));
213
219
  function findModel(id) {
@@ -2194,7 +2200,7 @@ import { randomUUID } from "node:crypto";
2194
2200
 
2195
2201
  // src/device-auth.ts
2196
2202
  var DEVICE_AUTH_ENDPOINT = "https://amr-api.open-design.ai/api/v1/cli/device-authorizations";
2197
- var DEVICE_AUTH_PLUGIN_VERSION = "0.1.4";
2203
+ var DEVICE_AUTH_PLUGIN_VERSION = "0.1.6";
2198
2204
  var DEVICE_REQUEST_TIMEOUT_MS = 2e4;
2199
2205
  var DEVICE_RESPONSE_MAX_BYTES = 65536;
2200
2206
  var DEVICE_POLL_MIN_MS = 1e3;
@@ -2206,17 +2212,21 @@ var DeviceAuthError = class extends Error {
2206
2212
  }
2207
2213
  code;
2208
2214
  };
2209
- function validateActivationUrl(value, userCode) {
2215
+ function validateActivationUrl(value, userCode, deviceId) {
2210
2216
  if (typeof value !== "string" || value.length > 512 || !/^https:\/\/open-design\.ai\/cloud\/cli\/activate(?:\?[^#]*)?$/u.test(value) || /[\s\\]/u.test(value)) return null;
2211
2217
  try {
2212
2218
  const url = new URL(value);
2213
2219
  if (url.origin !== "https://open-design.ai" || url.pathname !== "/cloud/cli/activate" || url.username || url.password || url.hash) return null;
2214
2220
  const entries = [...url.searchParams];
2215
- if (entries.length > 1) return null;
2216
- if (entries.length) {
2221
+ if (entries.length === 2) {
2222
+ if (entries.some(([key]) => key !== "deviceId" && key !== "userCode")) return null;
2223
+ const id = url.searchParams.get("deviceId");
2224
+ const code = url.searchParams.get("userCode");
2225
+ if (!validDeviceId(id) || !validUserCode(code) || deviceId !== void 0 && id !== deviceId || userCode !== void 0 && code !== userCode) return null;
2226
+ } else if (entries.length === 1) {
2217
2227
  const entry = entries[0];
2218
2228
  if (!["code", "user_code"].includes(entry[0]) || !validUserCode(entry[1]) || userCode !== void 0 && entry[1] !== userCode) return null;
2219
- }
2229
+ } else if (entries.length > 2) return null;
2220
2230
  return url.href;
2221
2231
  } catch {
2222
2232
  return null;
@@ -2225,6 +2235,9 @@ function validateActivationUrl(value, userCode) {
2225
2235
  function object(value) {
2226
2236
  return value !== null && typeof value === "object" && !Array.isArray(value);
2227
2237
  }
2238
+ function validDeviceId(value) {
2239
+ return typeof value === "string" && /^[a-z0-9_-]{1,200}$/iu.test(value);
2240
+ }
2228
2241
  function validUserCode(value) {
2229
2242
  return typeof value === "string" && /^[a-z0-9][a-z0-9-]{3,31}$/iu.test(value);
2230
2243
  }
@@ -2232,8 +2245,8 @@ function secret(value) {
2232
2245
  return typeof value === "string" && value.length > 0 && value.length <= 8192 && /^[\x21-\x7e]+$/u.test(value);
2233
2246
  }
2234
2247
  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);
2248
+ if (!object(value) || !validDeviceId(value.deviceId) || !secret(value.deviceSecret) || !validUserCode(value.userCode)) throw new DeviceAuthError("opendesign/login-response");
2249
+ const activationUrl = validateActivationUrl(value.activationUrl, value.userCode, value.deviceId);
2237
2250
  if (!activationUrl || value.userCode.toLowerCase().includes(value.deviceSecret.toLowerCase()) || decodeURIComponent(activationUrl).toLowerCase().includes(value.deviceSecret.toLowerCase())) throw new DeviceAuthError("opendesign/login-response");
2238
2251
  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
2252
  if (!Number.isFinite(expiresAt) || typeof value.pollIntervalSeconds !== "number" || !Number.isFinite(value.pollIntervalSeconds) || value.pollIntervalSeconds <= 0) throw new DeviceAuthError("opendesign/login-response");
@@ -2252,6 +2265,7 @@ function parseDevicePoll(value) {
2252
2265
  if (["expired", "cancelled", "canceled", "denied", "rejected", "failed", "error"].includes(state)) throw new DeviceAuthError(state === "expired" ? "opendesign/login-expired" : "opendesign/login-denied");
2253
2266
  if (value.error !== void 0 && value.error !== null && value.error !== "") throw new DeviceAuthError("opendesign/login-response");
2254
2267
  const user = object(value.user) ? value.user : {};
2268
+ if ((state === "pending" || state === "authorization_pending") && [value.controlKey, value.runtimeKey, user.controlKey, user.runtimeKey].every((key) => key === void 0 || key === null)) return { state: "pending" };
2255
2269
  const topPresent = value.controlKey !== void 0 || value.runtimeKey !== void 0;
2256
2270
  const source = topPresent ? value : user;
2257
2271
  if (source.controlKey !== void 0 || source.runtimeKey !== void 0) {
@@ -2406,6 +2420,11 @@ var ConsoleLogin = class {
2406
2420
  } : {}
2407
2421
  };
2408
2422
  }
2423
+ /** Host-only: the live approval link, so the Host can open it in a bounded window. */
2424
+ activationLink() {
2425
+ const status = this.status();
2426
+ return status.state === "waiting" ? status.activationUrl : void 0;
2427
+ }
2409
2428
  /** Synchronous/non-blocking and idempotent while capture/network/commit is pending. */
2410
2429
  start(request) {
2411
2430
  if (this.disposed) throw new LoginError("opendesign/login-disposed");
@@ -2421,7 +2440,8 @@ var ConsoleLogin = class {
2421
2440
  status: { jobId: null, state: "launching", startedAt: new Date(started).toISOString(), message: "\u6B63\u5728\u7533\u8BF7\u5B98\u65B9\u8BBE\u5907\u6388\u6743\u7801" },
2422
2441
  captured: void 0,
2423
2442
  committing: false,
2424
- cancelRequested: false
2443
+ cancelRequested: false,
2444
+ settled: false
2425
2445
  };
2426
2446
  job.status.jobId = job.id;
2427
2447
  this.job = job;
@@ -2446,7 +2466,11 @@ var ConsoleLogin = class {
2446
2466
  }
2447
2467
  dispose() {
2448
2468
  this.disposed = true;
2449
- if (this.job) this.requestCancellation(this.job, "\u670D\u52A1\u5DF2\u5378\u8F7D\uFF0C\u767B\u5F55\u5DF2\u53D6\u6D88");
2469
+ const job = this.job;
2470
+ if (job) {
2471
+ this.requestCancellation(job, "\u670D\u52A1\u5DF2\u5378\u8F7D\uFF0C\u767B\u5F55\u5DF2\u53D6\u6D88");
2472
+ this.settle(job);
2473
+ }
2450
2474
  }
2451
2475
  alive(job) {
2452
2476
  return this.job === job && !this.disposed && !job.controller.signal.aborted && ACTIVE.has(job.status.state);
@@ -2495,6 +2519,16 @@ var ConsoleLogin = class {
2495
2519
  this.clearCode(job);
2496
2520
  this.stopTimer(job);
2497
2521
  job.controller.abort();
2522
+ this.settle(job);
2523
+ }
2524
+ /** Report one terminal transition per job, exactly once. */
2525
+ settle(job) {
2526
+ if (job.settled) return;
2527
+ job.settled = true;
2528
+ try {
2529
+ this.options.onSettled?.();
2530
+ } catch {
2531
+ }
2498
2532
  }
2499
2533
  async run(job) {
2500
2534
  const signal = job.controller.signal;
@@ -2514,7 +2548,7 @@ var ConsoleLogin = class {
2514
2548
  }
2515
2549
  this.armExpiry(job);
2516
2550
  job.status.state = "waiting";
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";
2551
+ 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";
2518
2552
  job.status.activationUrl = device.activationUrl;
2519
2553
  job.status.userCode = device.userCode;
2520
2554
  job.status.expiresAt = new Date(job.deadline).toISOString();
@@ -2572,6 +2606,258 @@ function failureMessage(error) {
2572
2606
  return "\u8BBE\u5907\u6388\u6743\u8BF7\u6C42\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB";
2573
2607
  }
2574
2608
 
2609
+ // src/browser-open.ts
2610
+ var ACTIVATION_NAVIGATE_TIMEOUT_MS = 15e3;
2611
+ var ACTIVATION_OPEN_BUDGET_MS = 2e4;
2612
+ var ACTIVATION_CLOSE_TIMEOUT_MS = 5e3;
2613
+ function chromeLaunchOptions() {
2614
+ return {
2615
+ channel: "chrome",
2616
+ headless: false,
2617
+ chromiumSandbox: true,
2618
+ ignoreDefaultArgs: ["--enable-automation"],
2619
+ args: ["--disable-blink-features=AutomationControlled"]
2620
+ };
2621
+ }
2622
+ var ActivationLauncher = class {
2623
+ browser;
2624
+ page;
2625
+ pending;
2626
+ disposal;
2627
+ /** In-flight window closure, shared so disposal can join it. */
2628
+ closing;
2629
+ /** Closures of handles that were never adopted; disposal joins them too. */
2630
+ tracked = [];
2631
+ /** Chrome starts in flight, so disposal waits for the handle they will produce. */
2632
+ starting = [];
2633
+ disposed = false;
2634
+ /** Bumped by `close()`: an attempt from an older session must not resurrect a window. */
2635
+ session = 0;
2636
+ options;
2637
+ constructor(options = {}) {
2638
+ this.options = options;
2639
+ }
2640
+ /**
2641
+ * Show `url` in the temporary window, reusing one already open. Concurrent calls share
2642
+ * a single attempt: the caller's button stays honest about what actually happened.
2643
+ */
2644
+ async open(url) {
2645
+ if (validateActivationUrl(url) === null) return { code: "invalid-url", opened: false };
2646
+ if (this.disposed) return { code: "failed", opened: false };
2647
+ if (this.pending) return this.pending;
2648
+ const attempt = this.attempt(url);
2649
+ this.pending = attempt;
2650
+ try {
2651
+ return await attempt;
2652
+ } finally {
2653
+ if (this.pending === attempt) this.pending = void 0;
2654
+ }
2655
+ }
2656
+ /**
2657
+ * Close the temporary window and stay usable: a cancelled or expired job takes its
2658
+ * window away, while the next login may open a new one.
2659
+ */
2660
+ async close() {
2661
+ this.session++;
2662
+ await this.closeCurrent();
2663
+ }
2664
+ /**
2665
+ * Retire the launcher for good. Cleanup is best-effort and bounded: it joins in-flight
2666
+ * starts and closures, but a browser that ignores `close()` may outlive the 5 s wait.
2667
+ */
2668
+ async dispose() {
2669
+ this.disposed = true;
2670
+ this.session++;
2671
+ if (this.disposal) return this.disposal;
2672
+ this.disposal = (async () => {
2673
+ await this.bounded(Date.now() + ACTIVATION_CLOSE_TIMEOUT_MS, async () => {
2674
+ await Promise.allSettled(this.starting);
2675
+ });
2676
+ await this.closing;
2677
+ await this.closeCurrent();
2678
+ await this.bounded(Date.now() + ACTIVATION_CLOSE_TIMEOUT_MS, async () => {
2679
+ await Promise.all(this.tracked);
2680
+ });
2681
+ })();
2682
+ return this.disposal;
2683
+ }
2684
+ /**
2685
+ * Close the window this launcher owns right now. Ownership is taken before the async
2686
+ * close, so a job that adopts a new window afterwards is not affected by this closure.
2687
+ * The closure promise is shared so a later `dispose()` joins it.
2688
+ */
2689
+ async closeCurrent() {
2690
+ if (this.closing === void 0) {
2691
+ const browser = this.browser;
2692
+ this.browser = void 0;
2693
+ this.page = void 0;
2694
+ if (browser === void 0) return;
2695
+ this.closing = closeBounded(browser);
2696
+ }
2697
+ const closing = this.closing;
2698
+ try {
2699
+ await closing;
2700
+ } finally {
2701
+ if (this.closing === closing) this.closing = void 0;
2702
+ }
2703
+ }
2704
+ async attempt(url) {
2705
+ const session = this.session;
2706
+ const budget = this.options.timeoutMs ?? ACTIVATION_OPEN_BUDGET_MS;
2707
+ const deadline = Date.now() + budget;
2708
+ try {
2709
+ if (this.options.available?.() === false) return { code: "unavailable", opened: false };
2710
+ const browser = await this.ensureBrowser(session, deadline);
2711
+ if (browser === void 0) return { code: this.stale(session) ? "failed" : "unavailable", opened: false };
2712
+ if (this.stale(session)) {
2713
+ await this.bounded(deadline, () => this.closeCurrent());
2714
+ return { code: "failed", opened: false };
2715
+ }
2716
+ const page = await this.ensurePage(browser, session, deadline);
2717
+ if (page === void 0) {
2718
+ await this.bounded(deadline, () => this.closeCurrent());
2719
+ return { code: "failed", opened: false };
2720
+ }
2721
+ const remaining = Math.max(1, Math.min(ACTIVATION_NAVIGATE_TIMEOUT_MS, deadline - Date.now()));
2722
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: remaining });
2723
+ if (this.stale(session)) {
2724
+ await this.bounded(deadline, () => this.closeCurrent());
2725
+ return { code: "failed", opened: false };
2726
+ }
2727
+ return { code: "opened", opened: true };
2728
+ } catch {
2729
+ await this.bounded(deadline, () => this.closeCurrent());
2730
+ return { code: "failed", opened: false };
2731
+ }
2732
+ }
2733
+ /** The job that started this attempt ended, or the launcher retired. */
2734
+ stale(session) {
2735
+ return this.disposed || session !== this.session;
2736
+ }
2737
+ async ensureBrowser(session, deadline) {
2738
+ const open = this.browser;
2739
+ if (open !== void 0) {
2740
+ try {
2741
+ if (open.isConnected()) return open;
2742
+ } catch {
2743
+ }
2744
+ await this.bounded(deadline, () => this.closeCurrent());
2745
+ }
2746
+ for (let round = 0; round < 2; round++) {
2747
+ await this.bounded(deadline, async () => {
2748
+ await Promise.allSettled([this.closing, ...this.tracked, ...this.starting]);
2749
+ });
2750
+ if (this.stale(session) || Date.now() >= deadline) return void 0;
2751
+ if (this.closing === void 0 && this.tracked.length === 0 && this.starting.length === 0) break;
2752
+ }
2753
+ if (this.stale(session) || Date.now() >= deadline) return void 0;
2754
+ const launch = this.options.launch ?? defaultLaunch;
2755
+ const starting = launch();
2756
+ this.starting.push(starting);
2757
+ const browser = await this.bounded(deadline, () => starting);
2758
+ void starting.then((handle) => {
2759
+ if (handle !== browser) void this.trackClose(handle);
2760
+ }).catch(() => void 0).finally(() => {
2761
+ this.starting = this.starting.filter((item) => item !== starting);
2762
+ });
2763
+ if (browser === void 0) return void 0;
2764
+ if (this.stale(session)) {
2765
+ void this.trackClose(browser);
2766
+ return void 0;
2767
+ }
2768
+ this.adopt(browser);
2769
+ return browser;
2770
+ }
2771
+ async ensurePage(browser, session, deadline) {
2772
+ if (this.page !== void 0) return this.page;
2773
+ const page = await this.bounded(deadline, () => browser.newPage(), (late) => {
2774
+ void this.trackPageClose(late);
2775
+ });
2776
+ if (page === void 0) return void 0;
2777
+ if (this.stale(session) || this.browser !== browser) {
2778
+ await this.bounded(deadline, () => this.closeCurrent());
2779
+ return void 0;
2780
+ }
2781
+ this.page = page;
2782
+ return page;
2783
+ }
2784
+ /** A handle that arrived too late is closed, and still counts as work disposal joins. */
2785
+ trackClose(browser) {
2786
+ const closing = closeBounded(browser);
2787
+ this.tracked.push(closing);
2788
+ void closing.finally(() => {
2789
+ this.tracked = this.tracked.filter((item) => item !== closing);
2790
+ });
2791
+ return closing;
2792
+ }
2793
+ trackPageClose(page) {
2794
+ try {
2795
+ return Promise.resolve(page.close?.()).catch(() => void 0);
2796
+ } catch {
2797
+ return Promise.resolve();
2798
+ }
2799
+ }
2800
+ adopt(browser) {
2801
+ this.browser = browser;
2802
+ browser.on?.("disconnected", () => {
2803
+ if (this.browser === browser) {
2804
+ this.browser = void 0;
2805
+ this.page = void 0;
2806
+ }
2807
+ });
2808
+ }
2809
+ /**
2810
+ * Bound one launch step by the attempt's shared deadline. A step that settles only
2811
+ * after that deadline still hands its handle back: dropping it would leak a Chrome
2812
+ * process nobody can close.
2813
+ */
2814
+ async bounded(deadline, start, discardOnLate) {
2815
+ let timer;
2816
+ let expired = false;
2817
+ const work = (async () => {
2818
+ try {
2819
+ return await start();
2820
+ } catch {
2821
+ return void 0;
2822
+ }
2823
+ })().then((value) => {
2824
+ if (expired && value !== void 0) discardOnLate?.(value);
2825
+ return value;
2826
+ });
2827
+ const timeout = new Promise((resolve) => {
2828
+ timer = setTimeout(() => {
2829
+ expired = true;
2830
+ resolve(void 0);
2831
+ }, Math.max(1, deadline - Date.now()));
2832
+ });
2833
+ try {
2834
+ return await Promise.race([work, timeout]);
2835
+ } finally {
2836
+ if (timer !== void 0) clearTimeout(timer);
2837
+ }
2838
+ }
2839
+ };
2840
+ function closeBounded(browser) {
2841
+ let timer;
2842
+ return Promise.race([
2843
+ (async () => {
2844
+ try {
2845
+ await browser.close();
2846
+ } catch {
2847
+ }
2848
+ })(),
2849
+ new Promise((resolve) => {
2850
+ timer = setTimeout(resolve, ACTIVATION_CLOSE_TIMEOUT_MS);
2851
+ })
2852
+ ]).finally(() => {
2853
+ if (timer !== void 0) clearTimeout(timer);
2854
+ });
2855
+ }
2856
+ async function defaultLaunch() {
2857
+ const playwright = await import("playwright-core");
2858
+ return playwright.chromium.launch(chromeLaunchOptions());
2859
+ }
2860
+
2575
2861
  // src/quota.ts
2576
2862
  import { attributionHeaders as attributionHeaders3 } from "@deepseek-ai/dsh-llm";
2577
2863
  var API_BASE = "https://amr-api.open-design.ai/api/v1";
@@ -2859,6 +3145,7 @@ var MAX_API_KEY = 16384;
2859
3145
  var HTTP_ASCII = /^[\x21-\x7e]+$/u;
2860
3146
  var ENV_REF = /^[A-Za-z_][A-Za-z0-9_]*$/u;
2861
3147
  var MODEL_ID2 = /^[A-Za-z0-9._:/-]{1,128}$/u;
3148
+ var ACTIVATION_OPEN_CODES = ["opened", "no-link", "invalid-url", "unavailable", "failed"];
2862
3149
  var LOGIN_PHASES = ["idle", "launching", "waiting", "verifying", "success", "cancelled", "error"];
2863
3150
  var QUOTA_STATES = ["ok", "partial", "need-login", "error"];
2864
3151
  var SESSION_SOURCES = ["desktop", "login", "environment"];
@@ -3034,6 +3321,10 @@ function parseSaveApiKeyRequest(value) {
3034
3321
  if (key.length > MAX_API_KEY) throw new Error(`API Key \u4E0D\u80FD\u8D85\u8FC7 ${MAX_API_KEY} \u5B57\u7B26`);
3035
3322
  return { value: key, expectedRevision: text(value.expectedRevision, "\u914D\u7F6E\u7248\u672C", MAX_REVISION) };
3036
3323
  }
3324
+ function parseActivationOpenState(value) {
3325
+ if (!record(value)) throw new Error("\u6388\u6743\u9875\u6253\u5F00\u7ED3\u679C\u5FC5\u987B\u662F\u5BF9\u8C61");
3326
+ 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") };
3327
+ }
3037
3328
 
3038
3329
  // src/index.ts
3039
3330
  var name = "llm-opendesign-corouter";
@@ -3048,14 +3339,15 @@ function grantOf(value) {
3048
3339
  if (typeof value.payload.cookie !== "string") return;
3049
3340
  return { cookie: value.payload.cookie, ...typeof value.payload.expiresAt === "number" ? { expiresAt: value.payload.expiresAt } : {} };
3050
3341
  }
3051
- var _saveApiKey_dec, _cancelLogin_dec, _loginStatus_dec, _startLogin_dec, _refreshQuotaNow_dec, _refreshQuota_dec, _refreshModels_dec, _status_dec, _a, _init;
3052
- 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) {
3342
+ var _saveApiKey_dec, _openActivationPage_dec, _cancelLogin_dec, _loginStatus_dec, _startLogin_dec, _refreshQuotaNow_dec, _refreshQuota_dec, _refreshModels_dec, _status_dec, _a, _init;
3343
+ 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) {
3053
3344
  constructor(ctx, config, dependencies = { resolveSession, readQuota, readDesktopQuota }) {
3054
3345
  super(ctx, "openDesignCoRouter");
3055
3346
  this.dependencies = dependencies;
3056
3347
  __runInitializers(_init, 5, this);
3057
3348
  __publicField(this, "_config");
3058
3349
  __publicField(this, "_login");
3350
+ __publicField(this, "_activation");
3059
3351
  __publicField(this, "_lifetime", new AbortController());
3060
3352
  __publicField(this, "_revision", randomUUID2());
3061
3353
  __publicField(this, "_fingerprint", "");
@@ -3098,10 +3390,19 @@ var OpenDesignCoRouterService = class extends (_a = TypertRemoteService, _status
3098
3390
  this._modelsFingerprint = "";
3099
3391
  this._invalidateModels();
3100
3392
  }
3101
- }, dependencies.loginOptions);
3393
+ }, {
3394
+ ...dependencies.loginOptions,
3395
+ // The Host owns the temporary window, so it closes it on every terminal transition —
3396
+ // including expiry, which happens on a timer even when no UI is polling.
3397
+ onSettled: () => {
3398
+ void this._activation.close();
3399
+ }
3400
+ });
3401
+ this._activation = new ActivationLauncher(dependencies.activationOptions);
3102
3402
  ctx.effect(() => () => {
3103
3403
  this._lifetime.abort();
3104
3404
  this._login.dispose();
3405
+ return this._activation.dispose();
3105
3406
  });
3106
3407
  }
3107
3408
  dependencies;
@@ -3353,6 +3654,12 @@ var OpenDesignCoRouterService = class extends (_a = TypertRemoteService, _status
3353
3654
  cancelLogin(input) {
3354
3655
  return parseLoginState(this._login.cancel(parseCancelLoginRequest(input)));
3355
3656
  }
3657
+ async openActivationPage() {
3658
+ const url = this._login.activationLink();
3659
+ if (url === void 0) return { code: "no-link", opened: false };
3660
+ const result = await this._activation.open(url);
3661
+ return parseActivationOpenState({ code: result.code, opened: result.opened });
3662
+ }
3356
3663
  async saveApiKey(input) {
3357
3664
  const request = parseSaveApiKeyRequest(input), state = await this._state();
3358
3665
  this._expect(request.expectedRevision);
@@ -3372,6 +3679,7 @@ __decorateElement(_init, 1, "refreshQuotaNow", _refreshQuotaNow_dec, OpenDesignC
3372
3679
  __decorateElement(_init, 1, "startLogin", _startLogin_dec, OpenDesignCoRouterService);
3373
3680
  __decorateElement(_init, 1, "loginStatus", _loginStatus_dec, OpenDesignCoRouterService);
3374
3681
  __decorateElement(_init, 1, "cancelLogin", _cancelLogin_dec, OpenDesignCoRouterService);
3682
+ __decorateElement(_init, 1, "openActivationPage", _openActivationPage_dec, OpenDesignCoRouterService);
3375
3683
  __decorateElement(_init, 1, "saveApiKey", _saveApiKey_dec, OpenDesignCoRouterService);
3376
3684
  __decoratorMetadata(_init, OpenDesignCoRouterService);
3377
3685
  function apply(ctx, config) {