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/README.md CHANGED
@@ -32,7 +32,7 @@ dsh plugin --profile web add dsh-opendesign-corouter
32
32
  也可打开 [Actions → CI](https://github.com/KouzakiUmi/dsh-opendesign-corouter/actions/workflows/ci.yml),从成功构建下载 **`dsh-opendesign-corouter-build`** artifact。登录 GitHub 后解压 ZIP,取得 `.tgz` 与 `.sha256`,再安装本地包:
33
33
 
34
34
  ```console
35
- dsh plugin --profile web add /absolute/path/to/dsh-opendesign-corouter-0.1.2.tgz
35
+ dsh plugin --profile web add /absolute/path/to/dsh-opendesign-corouter-0.1.5.tgz
36
36
  ```
37
37
 
38
38
  `web` 只是示例,请替换为目标 Profile。Desktop 使用桌面安装器提供的 `dsh` 入口;独立公共 CLI 不能代管保留的 `desktop` Profile。安装后的启用与重载按目标 DSH 的管理流程进行。
@@ -40,11 +40,11 @@ dsh plugin --profile web add /absolute/path/to/dsh-opendesign-corouter-0.1.2.tgz
40
40
  ### 2. 配置连接
41
41
 
42
42
  1. 打开 **设置 → OpenDesign CoRouter → 连接与登录**。
43
- 2. 为推理配置 CoRouter 工作空间 **API Key**,或沿用启动环境中的 `COROUTER_API_KEY`。
44
- 3. 额度默认读取桌面登录态;没有桌面端时,点击 **登录并保存会话**,在官方页面自行完成登录。
43
+ 2. 点击 **开始设备授权**,再用 **在新窗口打开授权页** 显示官方页面,核对授权码并确认;也可复制链接到你自己的常用浏览器,或单独配置工作空间 API Key / 沿用 `COROUTER_API_KEY`。
44
+ 3. 返回设置页等待保存成功。额度默认优先读取桌面登录态;要明确使用设备授权的账号,可关闭桌面优先。
45
45
  4. 回到模型选择器,选择 **OpenDesign CoRouter · 套餐赠送** 或 **OpenDesign CoRouter · 付费模型**。
46
46
 
47
- **API Key 用于推理,桌面 / 控制台会话用于查额度。两者不能互相代替。** [完整连接说明](docs/usage.md)
47
+ **设备授权同时取得查额度和官方网关推理的两类令牌;显式配置的 API Key 仍优先。** [完整连接说明](docs/usage.md)
48
48
 
49
49
  ## 功能预览
50
50
 
@@ -72,7 +72,7 @@ dsh plugin --profile web add /absolute/path/to/dsh-opendesign-corouter-0.1.2.tgz
72
72
 
73
73
  ### 桌面优先,也能独立登录
74
74
 
75
- 不复制或修改桌面配置。独立登录使用临时 Chrome 窗口,用户亲自完成官方登录与人机验证;验证通过才保存到 DSH 凭据服务。支持取消、超时和卸载清理,不触碰日常浏览器。
75
+ 独立登录采用设备授权,官方授权页可交给插件自己启动的临时 Chrome 窗口显示(不读取或改动你已有的浏览器配置),也可复制链接到你自己的浏览器沿用已有登录态、密码管理器和自动填充。确认后设备令牌保存到 DSH 凭据服务;取消、超时和卸载停止轮询并关闭临时窗口,不关闭你自己的浏览器,也不修改桌面配置。
76
76
 
77
77
  <picture>
78
78
  <source media="(prefers-color-scheme: dark)" srcset="docs/previews/theme-dark.png">
@@ -98,7 +98,7 @@ dsh plugin --profile web add /absolute/path/to/dsh-opendesign-corouter-0.1.2.tgz
98
98
  | 目标 DSH Core | `0.2.1-rc.3`,以官方同版本发布包验证 |
99
99
  | Core 声明范围 | `>=0.2.1-rc.3 <0.3.0-0`,范围内其他版本不代表已实测 |
100
100
  | Node.js | `>=24.9.0` |
101
- | 独立登录 | 本机 Google Chrome;插件不会自动下载 |
101
+ | 独立登录 | 能访问官方授权页的常用浏览器;无需浏览器自动化或扩展 |
102
102
  | 默认推理接口 | `https://amr-link.open-design.ai/v1/chat/completions` |
103
103
 
104
104
  CI 在 Windows 与 Ubuntu 执行构建、类型检查及隔离测试;Ubuntu 另执行四页签浏览器和亮暗主题检查。通过 CI 不等于已安装或已通过真实账号验收。[开发与验证范围](docs/development.md)
package/SECURITY.md CHANGED
@@ -1,11 +1,12 @@
1
1
  # 安全边界
2
2
 
3
3
  - 推理、桌面控制面和控制台 Cookie 分属独立通道,不互相替代鉴权材料。
4
- - API Key 与独立登录 Cookie 仅经 DSH 凭据服务写入插件专属记录;设置页只读取存在性和状态,不回填秘密。
4
+ - API Key 与设备授权的 controlKey / runtimeKey 仅经 DSH 凭据服务写入插件专属记录;设置页只读取存在性和状态,不回填秘密。
5
5
  - 桌面配置只读。仅支持已核实的生产环境,控制令牌只能发送到 `https://amr-api.open-design.ai`。控制台和推理请求禁止自动跟随重定向。
6
+ - 设备授权双令牌原子保存;runtimeKey 只自动提供给请求快照中的官方推理 origin,controlKey 只发往官方控制面。旧 Cookie grant 保留只读兼容。
6
7
  - 推理自定义地址由用户显式配置;除本机回环外要求 HTTPS,拒绝 URL 内嵌凭据、查询串与片段。
7
8
  - 额度分池不跨身份混合。桌面认证失效时,可回退到单独保存的控制台登录;权限不足和未知额度不伪装成 0。
8
- - 独立登录使用临时浏览器上下文。用户自行完成登录和验证;取消、超时和卸载关闭插件拥有的窗口。持久化成功是不可撤回的提交边界,之后取消不会假称已撤回。
9
+ - 独立登录使用设备授权。Host 可用 `playwright-core` 启动一个短暂的 Chrome 窗口显示官方激活页;这是插件自己创建的临时浏览器上下文,不创建、不驱动、不读取用户已有的浏览器配置或 Cookie,也不在该页面输入、点击或读取内容。由系统外壳拦截 `target="_blank"` 的宿主里,官方链接仍作为手动路径保留。deviceSecret 只在 Host 内存用于轮询;授权码、激活链接和到期时间仅在等待期提供给 UI,打开结果只返回固定代码,不返回 URL、浏览器句柄或错误正文。取消、超时和卸载中止请求,并有界请求关闭该临时窗口(每阶段最多约 5 秒,超时为尽力清理);不关闭用户自己的浏览器。持久化成功是不可撤回的提交边界,之后取消不会假称已撤回。
9
10
  - `OPENDESIGN_VELA_PATH` 是用户信任的本地程序路径。插件仅以固定 argv 执行只读查询,不通过 shell;不校验该程序的签名或下载它。不要将此变量指向来源不明的程序。
10
11
  - RPC 使用严格白名单解析;原始错误正文、Cookie、请求头、Key 和桌面本地路径不进入状态返回。并发保存有版本冲突检测。
11
12
  - 请求、流空闲、额度接口、CLI、登录和 RPC 均有有界超时。停用插件会撤销 Provider / RPC 并中止自身请求。
@@ -1,11 +1,32 @@
1
1
  # 第三方代码与资料
2
2
 
3
- 本项目的消息/图片兼容处理、套餐模型元数据参考 [dsh-opendesign-go 0.1.9](https://github.com/EliteOtaku/dsh-opendesign-go)。额度归一化和临时浏览器登录流程改编自 [dsh-subusage 0.16.2](https://github.com/KouzakiUmi/dsh-subusage)。空轮判定参考 [dsh-empty-turn-guard 0.4.0](https://github.com/FYTXY/dsh-empty-turn-guard)。设置布局参考 [dsh-code-pill](https://github.com/KouzakiUmi/dsh-code-pill)。
3
+ 本项目的消息/图片兼容处理、套餐模型元数据参考 [dsh-opendesign-go 0.1.9](https://github.com/EliteOtaku/dsh-opendesign-go)。额度归一化及登录生命周期的基础处理改编自 [dsh-subusage 0.16.2](https://github.com/KouzakiUmi/dsh-subusage);临时授权窗口的 Chrome 启动策略改编自 [dsh-subusage 0.16.3](https://github.com/KouzakiUmi/dsh-subusage) 的 `lib/cookie-login.js`。空轮判定参考 [dsh-empty-turn-guard 0.4.0](https://github.com/FYTXY/dsh-empty-turn-guard)。设置布局参考 [dsh-code-pill](https://github.com/KouzakiUmi/dsh-code-pill)。
4
4
 
5
5
  离线主题夹具使用 [DeepSeek Harness 官方主题令牌](https://github.com/deepseek-ai/deepseek-harness/blob/22ddee3792402c3e70ec5f86a63c3e3e895a2f60/packages/client/ui-theme/src/styles/design-platform.css),仅供测试,不随插件安装包分发。
6
6
 
7
7
  Client bundle 内含 [Zod 4.6.6](https://github.com/colinhacks/zod) 与 DSH 协议辅助代码,分别保留 Colin McDonnell 与 DeepSeek 的 MIT 声明。
8
8
 
9
+ Host 侧运行时依赖 [playwright-core 1.63.0](https://github.com/microsoft/playwright)(Apache-2.0),只用于启动显示官方授权页的临时 Chrome 窗口:不下载浏览器,使用本机已安装的 Chrome。它按 npm 依赖安装,不并入本项目源码。
10
+
11
+ ```text
12
+ Apache License
13
+ Version 2.0, January 2004
14
+ http://www.apache.org/licenses/
15
+
16
+ Copyright (c) Microsoft Corporation.
17
+ Licensed under the Apache License, Version 2.0 (the "License");
18
+ you may not use this file except in compliance with the License.
19
+ You may obtain a copy of the License at
20
+
21
+ https://www.apache.org/licenses/LICENSE-2.0
22
+
23
+ Unless required by applicable law or agreed to in writing, software
24
+ distributed under the License is distributed on an "AS IS" BASIS,
25
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
26
+ See the License for the specific language governing permissions and
27
+ limitations under the License.
28
+ ```
29
+
9
30
  以上项目为 MIT 许可。保留如下版权和许可声明,适用于相应移植与改编部分:
10
31
 
11
32
  ```text
package/lib/adapter.d.ts CHANGED
@@ -5,7 +5,7 @@ import { type RuntimeConfig } from './config.js';
5
5
  export interface AdapterOptions {
6
6
  config(): RuntimeConfig;
7
7
  models?(): readonly CatalogModel[];
8
- apiKey(ref: string): Promise<string | undefined>;
8
+ apiKey(ref: string, baseURL?: string): Promise<string | undefined>;
9
9
  imageDataUrl?(image: ImageAttachmentRef, signal: AbortSignal): Promise<string | undefined>;
10
10
  onEmptyTurn?(): void;
11
11
  fetch?: typeof fetch;
package/lib/auth.d.ts CHANGED
@@ -77,11 +77,22 @@ export interface DesktopDeps {
77
77
  */
78
78
  export declare function readDesktopCredential(deps?: DesktopDeps): Promise<DesktopRead>;
79
79
  /** Cookie payload persisted through the credential seam as a `grant` record. */
80
- export interface LoginGrant {
80
+ export interface CookieLoginGrant {
81
81
  cookie: string;
82
- /** Epoch ms; `0`/absent means the console declared no expiry. */
82
+ /** Legacy cookie expiry only; never applied to device tokens. */
83
83
  expiresAt?: number;
84
84
  }
85
+ export interface DeviceLoginGrant {
86
+ kind: 'device';
87
+ controlKey: string;
88
+ runtimeKey: string;
89
+ savedAt?: string;
90
+ }
91
+ export type LoginGrant = CookieLoginGrant | DeviceLoginGrant;
92
+ /** Validate both keys as one snapshot; never combine an incomplete grant with another source. */
93
+ export declare function deviceGrant(value: unknown): DeviceLoginGrant | undefined;
94
+ /** Device runtime credentials may only reach this exact HTTPS origin, never a custom gateway. */
95
+ export declare function isOfficialRuntimeUrl(raw: string): boolean;
85
96
  export type AuthSource = 'desktop' | 'login' | 'environment';
86
97
  export interface AuthSession extends Session {
87
98
  source: SessionSource;
@@ -112,11 +123,15 @@ export interface AuthDeps extends DesktopDeps {
112
123
  }
113
124
  /** Desktop `controlKey` as a Bearer header (daemon evidence: `/api/v1/wallet/balance`). */
114
125
  export declare function desktopSession(credential: DesktopCredential, apiUrl?: string): AuthSession;
126
+ /** Device control token: Bearer on the official control plane only. No inferred token expiry. */
127
+ export declare function deviceSession(tokens: {
128
+ controlKey: string;
129
+ } | string): AuthSession;
115
130
  /** A persisted or environment-supplied console cookie as a `cookie` header. */
116
131
  export declare function cookieSession(cookie: string, source: SessionSource, apiUrl?: string): AuthSession | undefined;
117
132
  /**
118
133
  * Resolve the session in the order the user asked for: **desktop first**, then the
119
- * cookie saved by this plugin's own login, then environment references.
134
+ * device grant saved by this plugin, then legacy cookies and environment references.
120
135
  *
121
136
  * A desktop config whose `apiUrl` is not an official origin is *skipped*, not used: the
122
137
  * token must not travel to a host the plugin cannot vouch for. The skip is reported as a
@@ -0,0 +1,104 @@
1
+ /** Bounds on one navigation; the window stays open for the user afterwards. */
2
+ export declare const ACTIVATION_NAVIGATE_TIMEOUT_MS = 15000;
3
+ /**
4
+ * End-to-end budget for one open request: launch + newPage + navigate. Kept below the
5
+ * client's 30 s RPC wait so a successful open is reported to the user, not a timeout.
6
+ */
7
+ export declare const ACTIVATION_OPEN_BUDGET_MS = 20000;
8
+ /** Bound on closing a window; a wedged browser must not pin disposal. */
9
+ export declare const ACTIVATION_CLOSE_TIMEOUT_MS = 5000;
10
+ /** Why the window could not be offered. Codes are public; no URL or error text is echoed. */
11
+ export type ActivationOpenCode = 'opened' | 'invalid-url' | 'unavailable' | 'failed';
12
+ export interface ActivationOpenResult {
13
+ code: ActivationOpenCode;
14
+ /** True only when a Chrome window is showing the activation page. */
15
+ opened: boolean;
16
+ }
17
+ /** The subset of playwright-core this module uses, so tests can inject a stand-in. */
18
+ export interface ActivationPage {
19
+ goto(url: string, options: {
20
+ waitUntil: 'domcontentloaded';
21
+ timeout: number;
22
+ }): Promise<unknown>;
23
+ /** playwright's Page.close(); default does not run unload handlers. */
24
+ close?(): Promise<void>;
25
+ }
26
+ export interface ActivationBrowser {
27
+ close(): Promise<unknown>;
28
+ isConnected(): boolean;
29
+ newPage(): Promise<ActivationPage>;
30
+ on?(event: 'disconnected', listener: () => void): unknown;
31
+ }
32
+ export interface ActivationLauncherOptions {
33
+ /** Injected for offline tests; production resolves playwright-core lazily. */
34
+ launch?: () => Promise<ActivationBrowser>;
35
+ timeoutMs?: number;
36
+ /** Reports whether a launch is even possible, e.g. a missing Chrome install. */
37
+ available?: () => boolean;
38
+ }
39
+ /**
40
+ * Chrome without the automation flag subusage found counterproductive on guarded pages.
41
+ * `chromiumSandbox: true` is explicit on purpose: playwright otherwise appends
42
+ * `--no-sandbox` (verified in playwright-core 1.63.0), and this window renders a page
43
+ * this plugin does not control.
44
+ */
45
+ export declare function chromeLaunchOptions(): {
46
+ channel: "chrome";
47
+ headless: boolean;
48
+ chromiumSandbox: boolean;
49
+ ignoreDefaultArgs: string[];
50
+ args: string[];
51
+ };
52
+ export declare class ActivationLauncher {
53
+ private browser;
54
+ private page;
55
+ private pending;
56
+ private disposal;
57
+ /** In-flight window closure, shared so disposal can join it. */
58
+ private closing;
59
+ /** Closures of handles that were never adopted; disposal joins them too. */
60
+ private tracked;
61
+ /** Chrome starts in flight, so disposal waits for the handle they will produce. */
62
+ private starting;
63
+ private disposed;
64
+ /** Bumped by `close()`: an attempt from an older session must not resurrect a window. */
65
+ private session;
66
+ private readonly options;
67
+ constructor(options?: ActivationLauncherOptions);
68
+ /**
69
+ * Show `url` in the temporary window, reusing one already open. Concurrent calls share
70
+ * a single attempt: the caller's button stays honest about what actually happened.
71
+ */
72
+ open(url: string): Promise<ActivationOpenResult>;
73
+ /**
74
+ * Close the temporary window and stay usable: a cancelled or expired job takes its
75
+ * window away, while the next login may open a new one.
76
+ */
77
+ close(): Promise<void>;
78
+ /**
79
+ * Retire the launcher for good. Cleanup is best-effort and bounded: it joins in-flight
80
+ * starts and closures, but a browser that ignores `close()` may outlive the 5 s wait.
81
+ */
82
+ dispose(): Promise<void>;
83
+ /**
84
+ * Close the window this launcher owns right now. Ownership is taken before the async
85
+ * close, so a job that adopts a new window afterwards is not affected by this closure.
86
+ * The closure promise is shared so a later `dispose()` joins it.
87
+ */
88
+ private closeCurrent;
89
+ private attempt;
90
+ /** The job that started this attempt ended, or the launcher retired. */
91
+ private stale;
92
+ private ensureBrowser;
93
+ private ensurePage;
94
+ /** A handle that arrived too late is closed, and still counts as work disposal joins. */
95
+ private trackClose;
96
+ private trackPageClose;
97
+ private adopt;
98
+ /**
99
+ * Bound one launch step by the attempt's shared deadline. A step that settles only
100
+ * after that deadline still hands its handle back: dropping it would leak a Chrome
101
+ * process nobody can close.
102
+ */
103
+ private bounded;
104
+ }
package/lib/client.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import openDesignCoRouterRemote, { type CoRouterRemote } from './generated/typert.remote-client.js';
3
3
  import type { TypertClientRemote } from '@deepseek-ai/dsh-typert-protocol';
4
- import { type Status, type StatusModel } from './remote.js';
4
+ import { type LoginState, type Status, type StatusModel } from './remote.js';
5
5
  import type { QuotaSnapshot, QuotaWindow } from './quota.js';
6
6
  import { type ApiProtocol } from './protocol-options.js';
7
7
  import type { ModelSettings, ModelSettingsMap } from './model-settings.js';
@@ -183,6 +183,19 @@ export interface AdvancedProps {
183
183
  export declare function Advanced({ status, formSnapshot, setFormField, pendingField }: AdvancedProps): React.FunctionComponentElement<{
184
184
  children?: React.ReactNode | undefined;
185
185
  }>;
186
+ /** Approval is an ordinary official link; the Host may also show it in a temporary window. */
187
+ export declare function LoginApproval({ login, onOpen, opening, notice }: {
188
+ login: LoginState | undefined;
189
+ onOpen?: () => void;
190
+ opening?: boolean;
191
+ notice?: {
192
+ kind: 'status' | 'error';
193
+ text: string;
194
+ } | null;
195
+ }): React.DetailedReactHTMLElement<{
196
+ className: string;
197
+ 'aria-label': string;
198
+ }, HTMLElement> | null;
186
199
  export declare function tabAfter(current: TabId, key: string): TabId | undefined;
187
200
  export declare function SettingsPage({ caller, form }: SettingsProps): React.DetailedReactHTMLElement<{
188
201
  className: string;
package/lib/client.js CHANGED
@@ -32,6 +32,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
32
32
  var client_exports = {};
33
33
  __export(client_exports, {
34
34
  Advanced: () => Advanced,
35
+ LoginApproval: () => LoginApproval,
35
36
  ModelRow: () => ModelRow,
36
37
  Models: () => Models,
37
38
  Overview: () => Overview,
@@ -19925,6 +19926,27 @@ function record2(value) {
19925
19926
  return value !== null && typeof value === "object" && !Array.isArray(value);
19926
19927
  }
19927
19928
 
19929
+ // src/device-auth.ts
19930
+ function validateActivationUrl(value, userCode) {
19931
+ if (typeof value !== "string" || value.length > 512 || !/^https:\/\/open-design\.ai\/cloud\/cli\/activate(?:\?[^#]*)?$/u.test(value) || /[\s\\]/u.test(value)) return null;
19932
+ try {
19933
+ const url2 = new URL(value);
19934
+ if (url2.origin !== "https://open-design.ai" || url2.pathname !== "/cloud/cli/activate" || url2.username || url2.password || url2.hash) return null;
19935
+ const entries = [...url2.searchParams];
19936
+ if (entries.length > 1) return null;
19937
+ if (entries.length) {
19938
+ const entry = entries[0];
19939
+ if (!["code", "user_code"].includes(entry[0]) || !validUserCode(entry[1]) || userCode !== void 0 && entry[1] !== userCode) return null;
19940
+ }
19941
+ return url2.href;
19942
+ } catch {
19943
+ return null;
19944
+ }
19945
+ }
19946
+ function validUserCode(value) {
19947
+ return typeof value === "string" && /^[a-z0-9][a-z0-9-]{3,31}$/iu.test(value);
19948
+ }
19949
+
19928
19950
  // src/protocol-options.ts
19929
19951
  var API_PROTOCOL_IDS = ["chat-completions", "responses", "messages"];
19930
19952
  function openAiBaseURL(baseURL) {
@@ -19999,6 +20021,7 @@ var MAX_API_KEY = 16384;
19999
20021
  var HTTP_ASCII = /^[\x21-\x7e]+$/u;
20000
20022
  var ENV_REF = /^[A-Za-z_][A-Za-z0-9_]*$/u;
20001
20023
  var MODEL_ID = /^[A-Za-z0-9._:/-]{1,128}$/u;
20024
+ var ACTIVATION_OPEN_CODES = ["opened", "no-link", "invalid-url", "unavailable", "failed"];
20002
20025
  var LOGIN_PHASES = ["idle", "launching", "waiting", "verifying", "success", "cancelled", "error"];
20003
20026
  var QUOTA_STATES = ["ok", "partial", "need-login", "error"];
20004
20027
  var SESSION_SOURCES = ["desktop", "login", "environment"];
@@ -20086,9 +20109,19 @@ function parseLoginState(value) {
20086
20109
  if (!record2(value)) throw new Error("\u767B\u5F55\u72B6\u6001\u5FC5\u987B\u662F\u5BF9\u8C61");
20087
20110
  const jobId = value.jobId;
20088
20111
  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");
20112
+ const state = member(value.state, LOGIN_PHASES, "\u767B\u5F55\u9636\u6BB5");
20113
+ let approval = {};
20114
+ if (state === "waiting" && (value.activationUrl !== void 0 || value.userCode !== void 0 || value.expiresAt !== void 0)) {
20115
+ const userCode = text(value.userCode, "\u8BBE\u5907\u6388\u6743\u7801", 64);
20116
+ if (!/^[A-Za-z0-9-]+$/u.test(userCode)) throw new Error("\u8BBE\u5907\u6388\u6743\u7801\u683C\u5F0F\u65E0\u6548");
20117
+ const activationUrl = validateActivationUrl(value.activationUrl, userCode);
20118
+ if (activationUrl === null) throw new Error("\u8BBE\u5907\u6388\u6743\u94FE\u63A5\u65E0\u6548");
20119
+ approval = { activationUrl, userCode, expiresAt: isoTime(value.expiresAt, "\u8BBE\u5907\u6388\u6743\u5230\u671F\u65F6\u95F4") };
20120
+ }
20089
20121
  return {
20090
20122
  jobId: typeof jobId === "string" && jobId.length > 0 ? text(jobId, "\u767B\u5F55\u4EFB\u52A1 id", MAX_JOB_ID) : null,
20091
- state: member(value.state, LOGIN_PHASES, "\u767B\u5F55\u9636\u6BB5"),
20123
+ state,
20124
+ ...approval,
20092
20125
  ...value.message === void 0 ? {} : { message: text(value.message, "\u767B\u5F55\u8BF4\u660E", MAX_MESSAGE) },
20093
20126
  ...value.startedAt === void 0 ? {} : { startedAt: isoTime(value.startedAt, "\u767B\u5F55\u5F00\u59CB\u65F6\u95F4") },
20094
20127
  ...value.finishedAt === void 0 ? {} : { finishedAt: isoTime(value.finishedAt, "\u767B\u5F55\u7ED3\u675F\u65F6\u95F4") }
@@ -20164,6 +20197,10 @@ function parseSaveApiKeyRequest(value) {
20164
20197
  if (key.length > MAX_API_KEY) throw new Error(`API Key \u4E0D\u80FD\u8D85\u8FC7 ${MAX_API_KEY} \u5B57\u7B26`);
20165
20198
  return { value: key, expectedRevision: text(value.expectedRevision, "\u914D\u7F6E\u7248\u672C", MAX_REVISION) };
20166
20199
  }
20200
+ function parseActivationOpenState(value) {
20201
+ if (!record2(value)) throw new Error("\u6388\u6743\u9875\u6253\u5F00\u7ED3\u679C\u5FC5\u987B\u662F\u5BF9\u8C61");
20202
+ 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") };
20203
+ }
20167
20204
 
20168
20205
  // src/generated/typert.remote-client.ts
20169
20206
  var StatusCodec = { mode: "strict", typeSymbol: "dsh-opendesign-corouter#Status", create: () => external_exports.unknown().transform((value) => parseStatus(value)) };
@@ -20171,6 +20208,7 @@ var QuotaSnapshotCodec = { mode: "strict", typeSymbol: "dsh-opendesign-corouter#
20171
20208
  var LoginStateCodec = { mode: "strict", typeSymbol: "dsh-opendesign-corouter#LoginState", create: () => external_exports.unknown().transform((value) => parseLoginState(value)) };
20172
20209
  var StartLoginRequestCodec = { mode: "strict", typeSymbol: "dsh-opendesign-corouter#StartLoginRequest", create: () => external_exports.unknown().transform((value) => parseStartLoginRequest(value)) };
20173
20210
  var CancelLoginRequestCodec = { mode: "strict", typeSymbol: "dsh-opendesign-corouter#CancelLoginRequest", create: () => external_exports.unknown().transform((value) => parseCancelLoginRequest(value)) };
20211
+ var ActivationOpenStateCodec = { mode: "strict", typeSymbol: "dsh-opendesign-corouter#ActivationOpenState", create: () => external_exports.unknown().transform((value) => parseActivationOpenState(value)) };
20174
20212
  var SaveApiKeyRequestCodec = { mode: "strict", typeSymbol: "dsh-opendesign-corouter#SaveApiKeyRequest", create: () => external_exports.unknown().transform((value) => parseSaveApiKeyRequest(value)) };
20175
20213
  var TYPERT_REMOTE = createTypertRemoteInfo({ package: "dsh-opendesign-corouter", methods: {
20176
20214
  "openDesignCoRouter/status": { service: "openDesignCoRouter", invocation: { kind: "direct" }, parameters: [], result: StatusCodec },
@@ -20180,6 +20218,7 @@ var TYPERT_REMOTE = createTypertRemoteInfo({ package: "dsh-opendesign-corouter",
20180
20218
  "openDesignCoRouter/startLogin": { service: "openDesignCoRouter", invocation: { kind: "direct" }, parameters: [{ name: "input", source: "json", codec: StartLoginRequestCodec, strictCodec: StartLoginRequestCodec }], result: LoginStateCodec },
20181
20219
  "openDesignCoRouter/loginStatus": { service: "openDesignCoRouter", invocation: { kind: "direct" }, parameters: [], result: LoginStateCodec },
20182
20220
  "openDesignCoRouter/cancelLogin": { service: "openDesignCoRouter", invocation: { kind: "direct" }, parameters: [{ name: "input", source: "json", codec: CancelLoginRequestCodec, strictCodec: CancelLoginRequestCodec }], result: LoginStateCodec },
20221
+ "openDesignCoRouter/openActivationPage": { service: "openDesignCoRouter", invocation: { kind: "direct" }, parameters: [], result: ActivationOpenStateCodec },
20183
20222
  "openDesignCoRouter/saveApiKey": { service: "openDesignCoRouter", invocation: { kind: "direct" }, parameters: [{ name: "input", source: "json", codec: SaveApiKeyRequestCodec, strictCodec: SaveApiKeyRequestCodec }], result: StatusCodec }
20184
20223
  } }, external_exports);
20185
20224
  var typert_remote_client_default = TYPERT_REMOTE;
@@ -20203,15 +20242,16 @@ var MAX_REQUEST_TIMEOUT_MS = 36e5;
20203
20242
  var DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
20204
20243
  var MIN_STREAM_IDLE_TIMEOUT_MS = 1e3;
20205
20244
  var MAX_STREAM_IDLE_TIMEOUT_MS = 18e5;
20245
+ var RPC_TIMEOUT_MS = 3e4;
20206
20246
  var MAX_OUTPUT_TOKENS = 1e7;
20207
20247
  var MAX_CONTEXT_WINDOW = 1e8;
20208
20248
  var GENERIC_ERROR = "\u64CD\u4F5C\u672A\u5B8C\u6210\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
20209
20249
  var LOGIN_LABELS = {
20210
20250
  idle: "\u5C1A\u672A\u5F00\u59CB\u72EC\u7ACB\u767B\u5F55",
20211
- launching: "\u6B63\u5728\u6253\u5F00\u5B98\u65B9\u767B\u5F55\u9875\u9762\u2026",
20212
- waiting: "\u7B49\u5F85\u4F60\u5728\u5B98\u65B9\u9875\u9762\u5B8C\u6210\u767B\u5F55\u2026",
20213
- verifying: "\u6B63\u5728\u9A8C\u8BC1\u5E76\u4FDD\u5B58\u4F1A\u8BDD\u2026",
20214
- success: "\u767B\u5F55\u6210\u529F\uFF0C\u4F1A\u8BDD\u5DF2\u4FDD\u5B58\u3002",
20251
+ launching: "\u6B63\u5728\u7533\u8BF7\u8BBE\u5907\u6388\u6743\u7801\u2026",
20252
+ waiting: "\u8BF7\u5728\u6388\u6743\u7A97\u53E3\u6216\u5E38\u7528\u6D4F\u89C8\u5668\u4E2D\u786E\u8BA4\u672C\u6B21\u767B\u5F55\u3002",
20253
+ verifying: "\u6B63\u5728\u4FDD\u5B58\u8BBE\u5907\u6388\u6743\u2026",
20254
+ success: "\u767B\u5F55\u6210\u529F\uFF0C\u8BBE\u5907\u6388\u6743\u5DF2\u4FDD\u5B58\u3002",
20215
20255
  cancelled: "\u5DF2\u53D6\u6D88\u672C\u6B21\u767B\u5F55\u3002",
20216
20256
  error: "\u767B\u5F55\u672A\u5B8C\u6210\uFF0C\u8BF7\u91CD\u8BD5\u3002"
20217
20257
  };
@@ -20300,7 +20340,7 @@ function createCaller(ctx) {
20300
20340
  if (!operation) throw new Error("Unknown generated Remote method");
20301
20341
  const parameter = operation.parameters[0];
20302
20342
  const input2 = operation.input({ strict: true }).create().parse(parameter ? { [parameter.wire]: request } : {});
20303
- const response = await withTimeout(Promise.resolve(operation.invoke(remote, input2)), 25e3, controller.signal);
20343
+ const response = await withTimeout(Promise.resolve(operation.invoke(remote, input2)), RPC_TIMEOUT_MS, controller.signal);
20304
20344
  if (!("ok" in response) || !response.ok) throw new Error("RPC failed");
20305
20345
  return response.value;
20306
20346
  };
@@ -20531,7 +20571,7 @@ function Overview({ status, busy, onRefresh, nextAutoRefreshAt, refreshIntervalS
20531
20571
  "div",
20532
20572
  null,
20533
20573
  h("h3", null, "\u5148\u5B8C\u6210\u8FDE\u63A5"),
20534
- h("p", { className: "odc-hint" }, status.apiKeyConfigured === false ? "\u63A8\u7406\u5C1A\u7F3A API Key\uFF1B\u989D\u5EA6\u767B\u5F55\u4E0E\u63A8\u7406\u51ED\u636E\u5206\u522B\u914D\u7F6E\u3002" : "API Key \u5DF2\u914D\u7F6E\uFF1B\u767B\u5F55\u540E\u5373\u53EF\u67E5\u770B\u8D26\u53F7\u989D\u5EA6\u3002")
20574
+ h("p", { className: "odc-hint" }, status.apiKeyConfigured === false ? "\u5C1A\u65E0\u5F53\u524D\u7F51\u5173\u7684\u63A8\u7406\u51ED\u636E\uFF1B\u53EF\u5B8C\u6210\u8BBE\u5907\u6388\u6743\u6216\u5355\u72EC\u914D\u7F6E API Key\u3002" : "API Key \u5DF2\u914D\u7F6E\uFF1B\u767B\u5F55\u540E\u5373\u53EF\u67E5\u770B\u8D26\u53F7\u989D\u5EA6\u3002")
20535
20575
  ),
20536
20576
  onConnection === void 0 ? null : h("button", { type: "button", className: "odc-button odc-button-primary", onClick: onConnection }, "\u524D\u5F80\u8FDE\u63A5\u4E0E\u767B\u5F55")
20537
20577
  ) : null,
@@ -21069,6 +21109,40 @@ function Switch({ checked, disabled, label, hint, onChange }) {
21069
21109
  function loginInFlight(state) {
21070
21110
  return state === "launching" || state === "waiting" || state === "verifying";
21071
21111
  }
21112
+ var ACTIVATION_OPEN_LABELS = {
21113
+ opened: "\u5DF2\u5728\u65B0\u7A97\u53E3\u6253\u5F00\u5B98\u65B9\u6388\u6743\u9875\uFF1B\u8BF7\u5728\u8BE5\u7A97\u53E3\u786E\u8BA4\u8BBE\u5907\u7801\u3002",
21114
+ "no-link": "\u6388\u6743\u7801\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u7533\u8BF7\u3002",
21115
+ "invalid-url": "\u6388\u6743\u94FE\u63A5\u672A\u901A\u8FC7\u6821\u9A8C\uFF0C\u8BF7\u590D\u5236\u94FE\u63A5\u624B\u52A8\u6253\u5F00\u3002",
21116
+ unavailable: "\u65E0\u6CD5\u542F\u52A8\u4E34\u65F6\u6D4F\u89C8\u5668\uFF0C\u8BF7\u786E\u8BA4\u5DF2\u5B89\u88C5 Chrome\uFF1B\u4E5F\u53EF\u590D\u5236\u94FE\u63A5\u624B\u52A8\u6253\u5F00\u3002",
21117
+ failed: "\u4E34\u65F6\u6D4F\u89C8\u5668\u7A97\u53E3\u672A\u80FD\u6253\u5F00\uFF0C\u8BF7\u590D\u5236\u94FE\u63A5\u624B\u52A8\u6253\u5F00\u3002"
21118
+ };
21119
+ function LoginApproval({ login, onOpen, opening, notice }) {
21120
+ if (login?.state !== "waiting" || login.userCode === void 0 || login.expiresAt === void 0) return null;
21121
+ const url2 = validateActivationUrl(login.activationUrl, login.userCode);
21122
+ if (url2 === null) return null;
21123
+ return h(
21124
+ "div",
21125
+ { className: "odc-readonly", "aria-label": "\u8BBE\u5907\u6388\u6743" },
21126
+ h("span", { className: "odc-label" }, "\u6838\u5BF9\u6388\u6743\u7801"),
21127
+ h("code", { className: "odc-metric-value", "aria-label": "\u8BBE\u5907\u6388\u6743\u7801" }, login.userCode),
21128
+ h(
21129
+ "div",
21130
+ { className: "odc-actions" },
21131
+ h("button", {
21132
+ className: "odc-button odc-button-primary",
21133
+ type: "button",
21134
+ disabled: opening === true || onOpen === void 0,
21135
+ onClick: () => {
21136
+ onOpen?.();
21137
+ }
21138
+ }, opening === true ? "\u6B63\u5728\u6253\u5F00\u2026" : "\u5728\u65B0\u7A97\u53E3\u6253\u5F00\u6388\u6743\u9875"),
21139
+ h("a", { className: "odc-button", href: url2, target: "_blank", rel: "noreferrer noopener" }, "\u590D\u5236\u94FE\u63A5\u624B\u52A8\u6253\u5F00")
21140
+ ),
21141
+ notice ? h(NoticeView, { notice }) : null,
21142
+ h("p", { className: "odc-hint" }, `\u6388\u6743\u7801\u6709\u6548\u81F3 ${formatTime(login.expiresAt)}\u3002\u5728\u6388\u6743\u7A97\u53E3\u786E\u8BA4\u540E\uFF0C\u8FD4\u56DE\u6B64\u9875\u67E5\u770B\u7ED3\u679C\u3002`),
21143
+ h("p", { className: "odc-hint" }, "\u65B0\u7A97\u53E3\u662F\u72EC\u7ACB\u7684\u4E34\u65F6 Chrome\uFF0C\u4E0D\u4F1A\u8BFB\u53D6\u6216\u6539\u52A8\u4F60\u6B63\u5728\u4F7F\u7528\u7684\u6D4F\u89C8\u5668\u914D\u7F6E\uFF1B\u9700\u8981\u6CBF\u7528\u5DF2\u6709\u767B\u5F55\u6001\u65F6\uFF0C\u53F3\u952E\u590D\u5236\u94FE\u63A5\u5230\u5E38\u7528\u6D4F\u89C8\u5668\u3002\u4EC5\u786E\u8BA4\u4F60\u672C\u4EBA\u53D1\u8D77\u7684\u6388\u6743\u3002")
21144
+ );
21145
+ }
21072
21146
  function Connection({ status, formSnapshot, setFormField, pendingField, caller, loadStatus, refreshAfterLogin }) {
21073
21147
  const mounted = useMounted();
21074
21148
  const actionVersion = React.useRef(0);
@@ -21076,21 +21150,34 @@ function Connection({ status, formSnapshot, setFormField, pendingField, caller,
21076
21150
  const [notice, setNotice] = React.useState(null);
21077
21151
  const [apiKey, setApiKey] = React.useState("");
21078
21152
  const [keyBusy, setKeyBusy] = React.useState(false);
21153
+ const [opening, setOpening] = React.useState(false);
21154
+ const [activationNotice, setActivationNotice] = React.useState(null);
21155
+ const activationJob = React.useRef(null);
21156
+ const activationToken = React.useRef(0);
21079
21157
  React.useEffect(() => () => {
21080
21158
  actionVersion.current += 1;
21081
21159
  }, []);
21082
21160
  const login = status?.login;
21161
+ React.useEffect(() => {
21162
+ const job = login?.jobId ?? null;
21163
+ if (activationJob.current === job) return;
21164
+ activationJob.current = job;
21165
+ activationToken.current += 1;
21166
+ setActivationNotice(null);
21167
+ setOpening(false);
21168
+ }, [login?.jobId]);
21083
21169
  const state = login?.state ?? "idle";
21084
21170
  const inFlight = loginInFlight(state);
21085
21171
  const startLogin = async () => {
21086
21172
  if (status === void 0 || busy || keyBusy) return;
21087
21173
  const version2 = ++actionVersion.current;
21088
21174
  setBusy(true);
21089
- setNotice({ kind: "status", text: "\u6B63\u5728\u542F\u52A8\u5B98\u65B9\u767B\u5F55\u2026" });
21175
+ setNotice({ kind: "status", text: "\u6B63\u5728\u7533\u8BF7\u8BBE\u5907\u6388\u6743\u2026" });
21090
21176
  try {
21091
21177
  const next = await caller("startLogin", { expectedRevision: status.revision });
21092
21178
  if (!mounted.current || version2 !== actionVersion.current) return;
21093
- setNotice({ kind: "status", text: "\u8BF7\u5728\u6253\u5F00\u7684\u5B98\u65B9\u9875\u9762\u4E2D\u4EB2\u81EA\u5B8C\u6210\u767B\u5F55\u3002" });
21179
+ setNotice(null);
21180
+ setActivationNotice(null);
21094
21181
  await (next.state === "success" ? refreshAfterLogin() : loadStatus());
21095
21182
  } catch {
21096
21183
  if (mounted.current && version2 === actionVersion.current) setNotice({ kind: "error", text: "\u65E0\u6CD5\u542F\u52A8\u767B\u5F55\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002" });
@@ -21136,6 +21223,23 @@ function Connection({ status, formSnapshot, setFormField, pendingField, caller,
21136
21223
  if (mounted.current && version2 === actionVersion.current) setKeyBusy(false);
21137
21224
  }
21138
21225
  };
21226
+ const openActivation = async () => {
21227
+ if (status === void 0 || busy || keyBusy || opening || login?.state !== "waiting") return;
21228
+ const job = login.jobId;
21229
+ const token = ++activationToken.current;
21230
+ activationJob.current = job;
21231
+ setOpening(true);
21232
+ setActivationNotice(null);
21233
+ const current = () => mounted.current && activationToken.current === token && activationJob.current === job;
21234
+ try {
21235
+ const result = await caller("openActivationPage");
21236
+ if (current()) setActivationNotice({ kind: result.opened ? "status" : "error", text: ACTIVATION_OPEN_LABELS[result.code] });
21237
+ } catch {
21238
+ if (current()) setActivationNotice({ kind: "error", text: ACTIVATION_OPEN_LABELS.failed });
21239
+ } finally {
21240
+ if (mounted.current && activationToken.current === token) setOpening(false);
21241
+ }
21242
+ };
21139
21243
  const desktopChecked = formSnapshot.value?.useDesktopCredential ?? status?.config.useDesktopCredential ?? true;
21140
21244
  return h(
21141
21245
  React.Fragment,
@@ -21163,27 +21267,30 @@ function Connection({ status, formSnapshot, setFormField, pendingField, caller,
21163
21267
  h(
21164
21268
  "section",
21165
21269
  { className: "odc-card" },
21166
- h("h3", null, "\u72EC\u7ACB\u767B\u5F55"),
21167
- h("p", { className: "odc-hint" }, "\u767B\u5F55\u9700\u7531\u4F60\u5728\u5B98\u65B9\u9875\u9762\u4EB2\u81EA\u5B8C\u6210\uFF1B\u63D2\u4EF6\u4E0D\u4F1A\u4EE3\u586B\u8D26\u53F7\u6216\u5BC6\u7801\u3002\u9A8C\u8BC1\u901A\u8FC7\u540E\u624D\u4FDD\u5B58\u4F1A\u8BDD\u3002"),
21270
+ h("h3", null, "\u8BBE\u5907\u6388\u6743\u767B\u5F55"),
21271
+ h("p", { className: "odc-hint" }, "\u7533\u8BF7\u6388\u6743\u7801\u540E\uFF0C\u7531\u63D2\u4EF6\u542F\u52A8\u4E34\u65F6\u7A97\u53E3\u663E\u793A\u5B98\u65B9\u6388\u6743\u9875\uFF0C\u4E5F\u53EF\u590D\u5236\u94FE\u63A5\u5230\u4F60\u81EA\u5DF1\u7684\u5E38\u7528\u6D4F\u89C8\u5668\u3002\u786E\u8BA4\u540E\u81EA\u52A8\u4FDD\u5B58\u8BBE\u5907\u4EE4\u724C\uFF0C\u7528\u4E8E\u67E5\u8BE2\u989D\u5EA6\u53CA\u5B98\u65B9\u7F51\u5173\u63A8\u7406\u3002"),
21168
21272
  h(
21169
21273
  "div",
21170
21274
  { className: "odc-actions", style: { marginTop: 14 } },
21171
21275
  h("button", { className: "odc-button odc-button-primary", type: "button", disabled: status === void 0 || busy || keyBusy || inFlight, onClick: () => {
21172
21276
  void startLogin();
21173
- } }, "\u767B\u5F55\u5E76\u4FDD\u5B58\u4F1A\u8BDD"),
21277
+ } }, "\u5F00\u59CB\u8BBE\u5907\u6388\u6743"),
21174
21278
  inFlight ? h("button", { className: "odc-button", type: "button", disabled: busy || keyBusy || login?.jobId == null, onClick: () => {
21175
21279
  void cancelLogin();
21176
21280
  } }, "\u53D6\u6D88\u767B\u5F55") : null,
21177
21281
  h("a", { className: "odc-link", href: OFFICIAL_SITE, target: "_blank", rel: "noreferrer noopener" }, "\u6253\u5F00 CoRouter \u5B98\u7F51")
21178
21282
  ),
21179
- h("p", { className: "odc-meta", role: state === "error" ? "alert" : "status" }, LOGIN_LABELS[state]),
21283
+ h(LoginApproval, { login, onOpen: () => {
21284
+ void openActivation();
21285
+ }, opening, notice: activationNotice }),
21286
+ h("p", { className: "odc-meta", role: state === "error" ? "alert" : "status" }, login?.message ?? LOGIN_LABELS[state]),
21180
21287
  h(NoticeView, { notice })
21181
21288
  ),
21182
21289
  h(
21183
21290
  "section",
21184
21291
  { className: "odc-card" },
21185
- h("h3", null, "API Key"),
21186
- h("p", { className: "odc-hint" }, status?.apiKeyConfigured === true ? "\u5DF2\u914D\u7F6E API Key\u3002\u4FDD\u5B58\u4EC5\u66FF\u6362\u672C\u63D2\u4EF6\u4FDD\u5B58\u7684 Key\uFF1B\u73AF\u5883\u53D8\u91CF\u6216\u5DF2\u6709\u51ED\u636E\u5F15\u7528\u4ECD\u4F18\u5148\u3002" : "\u5C1A\u672A\u914D\u7F6E API Key\uFF1B\u63A8\u7406\u9700\u8981 CoRouter Key\uFF0C\u989D\u5EA6\u767B\u5F55\u4E0D\u80FD\u4EE3\u66FF\u3002"),
21292
+ h("h3", null, "API Key\uFF08\u53EF\u9009\u8986\u76D6\uFF09"),
21293
+ h("p", { className: "odc-hint" }, status?.apiKeyConfigured === true ? "\u5F53\u524D\u7F51\u5173\u5DF2\u6709\u53EF\u7528\u63A8\u7406\u51ED\u636E\u3002\u4F18\u5148\u7EA7\uFF1A\u73AF\u5883\u6216\u51ED\u636E\u5F15\u7528 \u2192 \u624B\u52A8\u4FDD\u5B58\u7684 API Key \u2192 \u8BBE\u5907\u6388\u6743\u4EE4\u724C\u3002" : "\u5F53\u524D\u7F51\u5173\u5C1A\u65E0\u63A8\u7406\u51ED\u636E\u3002\u53EF\u5B8C\u6210\u8BBE\u5907\u6388\u6743\u4EE5\u4F7F\u7528\u5B98\u65B9\u7F51\u5173\uFF0C\u6216\u4E3A\u5F53\u524D\u7F51\u5173\u5355\u72EC\u4FDD\u5B58 API Key\u3002"),
21187
21294
  h(
21188
21295
  "form",
21189
21296
  { onSubmit: (event) => {
@@ -0,0 +1,36 @@
1
+ export interface DeviceTokens {
2
+ controlKey: string;
3
+ runtimeKey: string;
4
+ }
5
+ export declare const DEVICE_AUTH_ENDPOINT = "https://amr-api.open-design.ai/api/v1/cli/device-authorizations";
6
+ export declare const DEVICE_AUTH_PLUGIN_VERSION = "0.1.5";
7
+ export declare const DEVICE_REQUEST_TIMEOUT_MS = 20000;
8
+ /** One bounded launch step of the temporary activation window. */
9
+ export declare const DEVICE_OPEN_TIMEOUT_MS = 30000;
10
+ export declare const DEVICE_RESPONSE_MAX_BYTES = 65536;
11
+ export declare const DEVICE_POLL_MIN_MS = 1000;
12
+ export declare const DEVICE_POLL_MAX_MS = 300000;
13
+ export declare class DeviceAuthError extends Error {
14
+ readonly code: string;
15
+ constructor(code: string);
16
+ }
17
+ /** Strict public-link allowlist: only an optional, single, non-secret code is allowed. */
18
+ export declare function validateActivationUrl(value: unknown, userCode?: string): string | null;
19
+ export interface DeviceAuthorization {
20
+ deviceId: string;
21
+ deviceSecret: string;
22
+ userCode: string;
23
+ activationUrl: string;
24
+ pollMs: number;
25
+ expiresAt: number;
26
+ }
27
+ export declare function parseDeviceAuthorization(value: unknown): DeviceAuthorization;
28
+ export type DevicePollResult = {
29
+ state: 'pending';
30
+ } | {
31
+ state: 'success';
32
+ tokens: DeviceTokens;
33
+ };
34
+ export declare function parseDevicePoll(value: unknown): DevicePollResult;
35
+ /** Bound fetch *and body consumption*, even if an injected transport ignores abort. */
36
+ export declare function postDeviceJson(fetcher: typeof fetch, url: string, body: unknown, signal: AbortSignal, timeoutMs?: number): Promise<unknown>;