u1s1-cli 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.d.ts CHANGED
@@ -1,4 +1,10 @@
1
1
  import { type ApiThinkingCapabilities, type CliConfig } from "./config.js";
2
+ /** Preserve caller cancellation while always enforcing the operation deadline. */
3
+ export declare function signalWithTimeout(signal: AbortSignal | undefined, timeoutMs: number): AbortSignal;
4
+ /** Read a complete response without allowing a custom or faulty server to exhaust CLI memory. */
5
+ export declare function readResponseTextCapped(resp: Response, maxBytes?: number): Promise<string>;
6
+ /** Parse JSON only after the complete response has passed the shared byte cap. */
7
+ export declare function readJsonResponseCapped<T = unknown>(resp: Response, maxBytes?: number): Promise<T>;
2
8
  export interface MeResponse {
3
9
  email: string | null;
4
10
  signup_credit_usd?: number;
@@ -66,6 +72,8 @@ export interface ModelsResponse {
66
72
  announcement?: ApiAnnouncement | null;
67
73
  /** Opaque, device-bound canary echoed only by the official signing proxy. */
68
74
  clientAttestation?: string;
75
+ /** Remaining lifetime of the canary, so the proxy can refresh it before it expires. */
76
+ clientAttestationExpiresInSeconds?: number;
69
77
  }
70
78
  /** 401:凭证失效(设备被移除/换过钥匙),调用方应引导重新登录而不是继续硬跑。 */
71
79
  export declare class AuthError extends Error {
package/dist/api.js CHANGED
@@ -1,12 +1,67 @@
1
1
  import { apiEndpointToCustom, loadEndpointsCache, saveEndpointsCache, setCustomEndpoints, VERSION, } from "./config.js";
2
2
  import { authorizedFetch } from "./device-auth.js";
3
+ const MAX_API_RESPONSE_BYTES = 8 * 1024 * 1024;
4
+ const MAX_API_ERROR_BYTES = 64 * 1024;
5
+ const MAX_IMAGE_RESULT_BYTES = 64 * 1024;
6
+ function jsonRecord(value) {
7
+ return value !== null && typeof value === "object" && !Array.isArray(value)
8
+ ? value
9
+ : null;
10
+ }
11
+ /** Preserve caller cancellation while always enforcing the operation deadline. */
12
+ export function signalWithTimeout(signal, timeoutMs) {
13
+ const timeout = AbortSignal.timeout(timeoutMs);
14
+ return signal ? AbortSignal.any([signal, timeout]) : timeout;
15
+ }
16
+ /** Read a complete response without allowing a custom or faulty server to exhaust CLI memory. */
17
+ export async function readResponseTextCapped(resp, maxBytes = MAX_API_RESPONSE_BYTES) {
18
+ const limit = Math.max(1, Math.floor(maxBytes));
19
+ const declaredLength = Number(resp.headers.get("content-length"));
20
+ if (Number.isFinite(declaredLength) && declaredLength > limit) {
21
+ void resp.body?.cancel("API response body limit reached").catch(() => { });
22
+ throw new Error(`API response body exceeds ${limit} bytes`);
23
+ }
24
+ if (!resp.body)
25
+ throw new Error("API response body is missing");
26
+ const reader = resp.body.getReader();
27
+ const decoder = new TextDecoder();
28
+ let bytes = 0;
29
+ let text = "";
30
+ try {
31
+ for (;;) {
32
+ const { done, value } = await reader.read();
33
+ if (done) {
34
+ text += decoder.decode();
35
+ break;
36
+ }
37
+ if (bytes + value.byteLength > limit) {
38
+ void reader.cancel("API response body limit reached").catch(() => { });
39
+ throw new Error(`API response body exceeds ${limit} bytes`);
40
+ }
41
+ bytes += value.byteLength;
42
+ text += decoder.decode(value, { stream: true });
43
+ }
44
+ }
45
+ finally {
46
+ try {
47
+ reader.releaseLock();
48
+ }
49
+ catch { }
50
+ }
51
+ return text;
52
+ }
53
+ /** Parse JSON only after the complete response has passed the shared byte cap. */
54
+ export async function readJsonResponseCapped(resp, maxBytes = MAX_API_RESPONSE_BYTES) {
55
+ return JSON.parse(await readResponseTextCapped(resp, maxBytes));
56
+ }
3
57
  /** 401:凭证失效(设备被移除/换过钥匙),调用方应引导重新登录而不是继续硬跑。 */
4
58
  export class AuthError extends Error {
5
59
  }
6
60
  /** 网关错误壳统一是 { error: { message } };解析不出来时回退到状态码提示。 */
7
61
  async function errorMessageFromResponse(resp, fallback) {
8
- const body = (await resp.json().catch(() => null));
9
- return body?.error?.message ?? fallback;
62
+ const body = jsonRecord(await readJsonResponseCapped(resp, MAX_API_ERROR_BYTES).catch(() => null));
63
+ const error = jsonRecord(body?.error);
64
+ return typeof error?.message === "string" ? error.message.slice(0, 2_000) : fallback;
10
65
  }
11
66
  export async function fetchModels(cfg) {
12
67
  let resp;
@@ -23,13 +78,17 @@ export async function fetchModels(cfg) {
23
78
  throw new AuthError("登录已失效,请重新运行 u1s1 login");
24
79
  if (!resp.ok)
25
80
  throw new Error(await errorMessageFromResponse(resp, `服务端返回 ${resp.status},稍后再试`));
26
- const body = (await resp.json());
81
+ const body = await readJsonResponseCapped(resp);
82
+ if (!body || !Array.isArray(body.data))
83
+ throw new Error("服务端模型列表格式不正确");
27
84
  const token = body.client_attestation?.token;
85
+ const expiresIn = body.client_attestation?.expires_in;
28
86
  return {
29
87
  models: body.data,
30
88
  features: body.features ?? {},
31
89
  announcement: body.announcement,
32
90
  clientAttestation: typeof token === "string" && token.length <= 1024 ? token : undefined,
91
+ clientAttestationExpiresInSeconds: typeof expiresIn === "number" && Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : undefined,
33
92
  };
34
93
  }
35
94
  /**
@@ -49,8 +108,8 @@ export async function fetchUserEndpoints(cfg) {
49
108
  }
50
109
  if (!resp.ok)
51
110
  throw new Error(await errorMessageFromResponse(resp, `服务端返回 ${resp.status}`));
52
- const body = (await resp.json());
53
- return Array.isArray(body.endpoints) ? body.endpoints : [];
111
+ const body = jsonRecord(await readJsonResponseCapped(resp));
112
+ return Array.isArray(body?.endpoints) ? body.endpoints : [];
54
113
  }
55
114
  /**
56
115
  * 拉取云端配置的自定义端点并装载进 CUSTOM_ENDPOINTS;失败(离线/老网关)回退
@@ -79,17 +138,37 @@ export async function searchWeb(cfg, input) {
79
138
  "content-type": "application/json",
80
139
  },
81
140
  body: JSON.stringify({ query: input.query, max_results: input.maxResults }),
82
- signal: input.signal ?? AbortSignal.timeout(60_000),
141
+ signal: signalWithTimeout(input.signal, 60_000),
83
142
  });
84
143
  }
85
- catch {
86
- throw new Error(`连不上 ${cfg.baseUrl},检查一下网络?`);
144
+ catch (error) {
145
+ if (input.signal?.aborted)
146
+ throw error;
147
+ throw new Error(`连不上 ${cfg.baseUrl},检查一下网络?`, { cause: error });
87
148
  }
88
149
  if (resp.status === 401)
89
150
  throw new Error("登录已失效,请重新运行 u1s1 login");
90
151
  if (!resp.ok)
91
152
  throw new Error(await errorMessageFromResponse(resp, `搜索服务返回 ${resp.status},稍后再试`));
92
- return (await resp.json());
153
+ const body = jsonRecord(await readJsonResponseCapped(resp));
154
+ if (!body || !Array.isArray(body.results))
155
+ throw new Error("搜索服务返回格式不正确");
156
+ const results = [];
157
+ for (const value of body.results.slice(0, 10)) {
158
+ const result = jsonRecord(value);
159
+ if (!result
160
+ || typeof result.title !== "string"
161
+ || typeof result.url !== "string"
162
+ || typeof result.snippet !== "string") {
163
+ continue;
164
+ }
165
+ results.push({ title: result.title, url: result.url, snippet: result.snippet });
166
+ }
167
+ return {
168
+ query: typeof body.query === "string" ? body.query : input.query,
169
+ answer: typeof body.answer === "string" ? body.answer : null,
170
+ results,
171
+ };
93
172
  }
94
173
  /** web_fetch 直连失败时的回退:网关用 Cloudflare Browser Rendering 渲染后转 markdown。 */
95
174
  export async function renderPage(cfg, url, signal) {
@@ -104,31 +183,40 @@ export async function renderPage(cfg, url, signal) {
104
183
  "content-type": "application/json",
105
184
  },
106
185
  body: JSON.stringify({ url }),
107
- signal: signal ?? AbortSignal.timeout(90_000),
186
+ signal: signalWithTimeout(signal, 90_000),
108
187
  });
109
188
  }
110
- catch {
111
- throw new Error(`连不上 ${cfg.baseUrl}`);
189
+ catch (error) {
190
+ if (signal?.aborted)
191
+ throw error;
192
+ throw new Error(`连不上 ${cfg.baseUrl}`, { cause: error });
112
193
  }
113
194
  if (resp.status === 401)
114
195
  throw new Error("登录已失效,请重新运行 u1s1 login");
115
196
  if (!resp.ok)
116
197
  throw new Error(await errorMessageFromResponse(resp, `渲染服务返回 ${resp.status}`));
117
- return (await resp.json());
198
+ const body = jsonRecord(await readJsonResponseCapped(resp));
199
+ if (!body || typeof body.url !== "string" || typeof body.markdown !== "string") {
200
+ throw new Error("渲染服务返回格式不正确");
201
+ }
202
+ return { url: body.url, markdown: body.markdown };
118
203
  }
119
204
  const IMAGE_URL_HEADER = "x-u1s1-image-url";
120
205
  const IMAGE_SIZE_HEADER = "x-u1s1-image-size";
121
206
  function imageResultFromHeaders(resp) {
122
207
  const encodedUrl = resp.headers.get(IMAGE_URL_HEADER);
123
- if (!encodedUrl)
208
+ if (!encodedUrl || encodedUrl.length > 8_000)
124
209
  return null;
125
210
  try {
126
211
  const url = decodeURIComponent(encodedUrl);
212
+ if (/[\u0000-\u001f\u007f]/.test(url))
213
+ return null;
127
214
  const parsed = new URL(url);
128
215
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
129
216
  return null;
130
217
  const encodedSize = resp.headers.get(IMAGE_SIZE_HEADER);
131
- return { url, size: encodedSize ? decodeURIComponent(encodedSize) : null };
218
+ const size = encodedSize ? decodeURIComponent(encodedSize) : null;
219
+ return { url, size: size && size.length <= 100 ? size : null };
132
220
  }
133
221
  catch {
134
222
  return null;
@@ -147,15 +235,28 @@ function responseErrorDetail(error) {
147
235
  export async function readGeneratedImageResponse(resp) {
148
236
  const headerResult = imageResultFromHeaders(resp);
149
237
  if (headerResult) {
150
- await resp.body?.cancel().catch(() => undefined);
238
+ void resp.body?.cancel("image result recovered from response headers").catch(() => undefined);
151
239
  return headerResult;
152
240
  }
153
241
  try {
154
- const body = (await resp.json());
155
- if (typeof body.url !== "string" || !/^https?:\/\//.test(body.url)) {
242
+ const body = jsonRecord(await readJsonResponseCapped(resp, MAX_IMAGE_RESULT_BYTES));
243
+ if (typeof body?.url !== "string") {
244
+ throw new Error("服务端没有返回图片下载地址");
245
+ }
246
+ let parsed;
247
+ try {
248
+ parsed = new URL(body.url);
249
+ }
250
+ catch {
251
+ throw new Error("服务端没有返回图片下载地址");
252
+ }
253
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
156
254
  throw new Error("服务端没有返回图片下载地址");
157
255
  }
158
- return { url: body.url, size: typeof body.size === "string" ? body.size : null };
256
+ return {
257
+ url: body.url,
258
+ size: typeof body.size === "string" && body.size.length <= 100 ? body.size : null,
259
+ };
159
260
  }
160
261
  catch (error) {
161
262
  throw new Error(`图片可能已经生成,但客户端下载结果响应失败: ${responseErrorDetail(error)}。` +
@@ -175,7 +276,7 @@ export async function generateImage(cfg, req, signal) {
175
276
  "content-type": "application/json",
176
277
  },
177
278
  body: JSON.stringify(req),
178
- signal: signal ?? AbortSignal.timeout(180_000),
279
+ signal: signalWithTimeout(signal, 180_000),
179
280
  });
180
281
  }
181
282
  catch (error) {
@@ -206,5 +307,8 @@ export async function fetchMe(cfg) {
206
307
  throw new Error("登录已失效,请重新运行 u1s1 login");
207
308
  if (!resp.ok)
208
309
  throw new Error(await errorMessageFromResponse(resp, `服务端返回 ${resp.status},稍后再试`));
209
- return (await resp.json());
310
+ const body = await readJsonResponseCapped(resp);
311
+ if (!body || typeof body !== "object")
312
+ throw new Error("服务端账户响应格式不正确");
313
+ return body;
210
314
  }
package/dist/bench.js CHANGED
@@ -3,7 +3,7 @@ import { join, dirname } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { homedir } from "node:os";
5
5
  import { CUSTOM_ENDPOINTS, loadConfig, MODELS, PROVIDER_ID, } from "./config.js";
6
- import { loadCustomEndpoints } from "./api.js";
6
+ import { loadCustomEndpoints, readJsonResponseCapped, readResponseTextCapped } from "./api.js";
7
7
  import { authorizedFetch, hasDeviceCredential } from "./device-auth.js";
8
8
  import { costStr, errorRate, formatDuration, generateReport, renderTable, scoreBar, scoreRate, sortByScore, summarizeByModel, tokPerSec, } from "./bench-report.js";
9
9
  import { scoreResponse } from "./bench-scoring.js";
@@ -108,15 +108,32 @@ async function callModel(input) {
108
108
  : await fetch(`${baseUrl}/chat/completions`, request);
109
109
  const latencyMs = Math.round(performance.now() - start);
110
110
  if (!res.ok) {
111
- const errBody = await res.text().catch(() => "未知错误");
111
+ const errBody = await readResponseTextCapped(res, 64 * 1024).catch(() => "未知错误");
112
112
  return { response: "", latencyMs, tokensIn: 0, tokensOut: 0, error: `HTTP ${res.status}: ${errBody.slice(0, 200)}` };
113
113
  }
114
- const data = (await res.json());
114
+ const value = await readJsonResponseCapped(res);
115
+ const data = value && typeof value === "object" && !Array.isArray(value)
116
+ ? value
117
+ : null;
118
+ const choices = Array.isArray(data?.choices) ? data.choices : [];
119
+ const firstChoice = choices[0] && typeof choices[0] === "object" && !Array.isArray(choices[0])
120
+ ? choices[0]
121
+ : null;
122
+ const message = firstChoice?.message && typeof firstChoice.message === "object" && !Array.isArray(firstChoice.message)
123
+ ? firstChoice.message
124
+ : null;
125
+ const usage = data?.usage && typeof data.usage === "object" && !Array.isArray(data.usage)
126
+ ? data.usage
127
+ : null;
115
128
  return {
116
- response: data.choices?.[0]?.message?.content ?? "",
129
+ response: typeof message?.content === "string" ? message.content : "",
117
130
  latencyMs,
118
- tokensIn: data.usage?.prompt_tokens ?? 0,
119
- tokensOut: data.usage?.completion_tokens ?? 0,
131
+ tokensIn: typeof usage?.prompt_tokens === "number" && Number.isFinite(usage.prompt_tokens)
132
+ ? Math.max(0, usage.prompt_tokens)
133
+ : 0,
134
+ tokensOut: typeof usage?.completion_tokens === "number" && Number.isFinite(usage.completion_tokens)
135
+ ? Math.max(0, usage.completion_tokens)
136
+ : 0,
120
137
  };
121
138
  }
122
139
  catch (e) {
package/dist/deploy.d.ts CHANGED
@@ -1,4 +1,10 @@
1
1
  import { type CliConfig } from "./config.js";
2
+ interface SiteFile {
3
+ path: string;
4
+ abs: string;
5
+ bytes: number;
6
+ }
7
+ export declare function collectFiles(root: string): SiteFile[];
2
8
  type SiteVisibility = "public" | "private";
3
9
  interface DeployArgs {
4
10
  name?: string;
package/dist/deploy.js CHANGED
@@ -1,8 +1,9 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
1
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
2
  import { basename, join, relative, resolve, sep } from "node:path";
3
3
  import { createInterface } from "node:readline/promises";
4
4
  import { VERSION, u1s1Dir } from "./config.js";
5
5
  import { authorizedFetch } from "./device-auth.js";
6
+ import { readJsonResponseCapped } from "./api.js";
6
7
  /**
7
8
  * u1s1 deploy publishes a static site to <project>.<account-code>.u1s1.app.
8
9
  * Detect the site root, ask for a project slug, and remember it in ~/.u1s1/deploys.json.
@@ -12,6 +13,8 @@ const deploysFile = join(u1s1Dir, "deploys.json");
12
13
  /** 构建产物目录优先:Vite/Next 等项目根的 index.html 是源码,不是能直接上线的产物。 */
13
14
  const BUILD_DIRS = ["dist", "build", "out", "_site", "public"];
14
15
  const SKIP_DIRS = new Set(["node_modules", "__pycache__"]);
16
+ const MAX_DEPLOY_API_RESPONSE_BYTES = 256 * 1024;
17
+ const MAX_DEPLOY_ERROR_RESPONSE_BYTES = 64 * 1024;
15
18
  function readDeploys() {
16
19
  try {
17
20
  return JSON.parse(readFileSync(deploysFile, "utf8"));
@@ -56,14 +59,18 @@ function resolveSiteDir(explicit) {
56
59
  " 在网站目录里运行 u1s1 deploy,或指定目录:u1s1 deploy <目录>\n" +
57
60
  " 如果项目需要构建(如 Vite/Next),先跑构建再部署 dist/ 等产物目录");
58
61
  }
59
- function collectFiles(root) {
62
+ export function collectFiles(root) {
60
63
  const files = [];
61
64
  const walk = (dir) => {
62
65
  for (const name of readdirSync(dir)) {
63
66
  if (name.startsWith(".") || SKIP_DIRS.has(name))
64
67
  continue;
65
68
  const abs = join(dir, name);
66
- const st = statSync(abs);
69
+ const st = lstatSync(abs);
70
+ // Never upload or recurse through links: a project-controlled link could
71
+ // escape the selected root, expose credentials, or create a directory cycle.
72
+ if (st.isSymbolicLink())
73
+ continue;
67
74
  if (st.isDirectory())
68
75
  walk(abs);
69
76
  else if (st.isFile()) {
@@ -136,12 +143,25 @@ async function api(cfg, request) {
136
143
  catch {
137
144
  throw new Error(`连不上 ${cfg.baseUrl},检查一下网络?`);
138
145
  }
139
- const data = (await resp.json().catch(() => null));
146
+ const value = await readJsonResponseCapped(resp, MAX_DEPLOY_API_RESPONSE_BYTES).catch(() => null);
147
+ const data = value && typeof value === "object" && !Array.isArray(value)
148
+ ? value
149
+ : null;
140
150
  if (!resp.ok) {
141
- const e = new Error(data?.error?.message ?? `服务端返回 ${resp.status},稍后再试`);
142
- e.code = data?.error?.code;
151
+ const detail = data?.error && typeof data.error === "object"
152
+ ? data.error
153
+ : undefined;
154
+ const message = typeof detail?.message === "string"
155
+ ? detail.message.slice(0, 2_000)
156
+ : `服务端返回 ${resp.status},稍后再试`;
157
+ const e = new Error(message);
158
+ if (typeof detail?.code === "string" && detail.code.length <= 100) {
159
+ e.code = detail.code;
160
+ }
143
161
  throw e;
144
162
  }
163
+ if (!data)
164
+ throw new Error("服务端部署响应格式不正确");
145
165
  return data;
146
166
  }
147
167
  /** 逐个上传,失败重试一次;并发数保守取 6。 */
@@ -157,12 +177,24 @@ async function uploadAll(cfg, start, files) {
157
177
  signal: AbortSignal.timeout(120_000),
158
178
  });
159
179
  let resp = await put().catch(() => null);
160
- if (!resp?.ok)
180
+ if (resp && !resp.ok) {
181
+ void resp.body?.cancel("retrying rejected deploy upload").catch(() => { });
161
182
  resp = await put().catch(() => null);
183
+ }
184
+ else if (!resp) {
185
+ resp = await put().catch(() => null);
186
+ }
162
187
  if (!resp?.ok) {
163
- const body = resp ? (await resp.json().catch(() => null)) : null;
164
- throw new Error(`上传 ${f.path} 失败:${body?.error?.message ?? "网络错误"}`);
188
+ const value = resp
189
+ ? await readJsonResponseCapped(resp, MAX_DEPLOY_ERROR_RESPONSE_BYTES).catch(() => null)
190
+ : null;
191
+ const body = value && typeof value === "object" && !Array.isArray(value) ? value : null;
192
+ const message = body?.error && typeof body.error === "object" && typeof body.error.message === "string"
193
+ ? body.error.message.slice(0, 2_000)
194
+ : "网络错误";
195
+ throw new Error(`上传 ${f.path} 失败:${message}`);
165
196
  }
197
+ void resp.body?.cancel("deploy upload completed").catch(() => { });
166
198
  done++;
167
199
  process.stdout.write(`\r 上传中 ${done}/${files.length} ${f.path.slice(0, 48).padEnd(48)}`);
168
200
  };
@@ -1,4 +1,5 @@
1
1
  import { webcrypto } from "node:crypto";
2
+ import { type IncomingMessage } from "node:http";
2
3
  import { type CliConfig } from "./config.js";
3
4
  export declare function hasDeviceCredential(cfg: CliConfig): boolean;
4
5
  export declare function generateDeviceKeyPair(): Promise<{
@@ -8,17 +9,46 @@ export declare function generateDeviceKeyPair(): Promise<{
8
9
  export declare function dpopHeaders(cfg: CliConfig, method: string, url: string): Promise<Record<string, string>>;
9
10
  /** Fetch a gateway route with a fresh proof; generic keys remain a read-only compatibility fallback. */
10
11
  export declare function authorizedFetch(cfg: CliConfig, input: string | URL, init?: RequestInit): Promise<Response>;
12
+ /** Buffer a loopback request only up to the same JSON limit enforced by Gateway. */
13
+ export declare function readSigningProxyRequestBody(request: IncomingMessage, maxBytes?: number): Promise<Buffer | undefined>;
14
+ /**
15
+ * How the proxy obtains and refreshes the device attestation canary. The token
16
+ * is device-bound and expires (7 days server-side); the proxy keeps it fresh so
17
+ * a healthy Gateway always sees a valid attestation instead of falling back to
18
+ * the weaker "legacy official" verdict when a transient /v1/models fetch missed.
19
+ */
20
+ export interface ClientAttestationSource {
21
+ token?: string;
22
+ expiresInSeconds?: number;
23
+ /** Fetch a fresh token (e.g. via GET /v1/models). Single-flighted by the proxy. */
24
+ refresh?: () => Promise<{
25
+ token?: string;
26
+ expiresInSeconds?: number;
27
+ }>;
28
+ }
29
+ interface AttestationHolder {
30
+ token?: string;
31
+ expiresAtMs?: number;
32
+ refresh?: () => Promise<{
33
+ token?: string;
34
+ expiresInSeconds?: number;
35
+ }>;
36
+ refreshing?: Promise<void>;
37
+ lastFailureMs?: number;
38
+ }
11
39
  interface SigningProxy {
12
40
  baseUrl: string;
13
41
  localKey: string;
14
42
  token: string;
15
43
  client: ClientSurface;
16
- clientAttestation?: string;
44
+ attestation: AttestationHolder;
17
45
  }
46
+ /** Attach a fresh (self-healing) attestation header to an outbound proxy request. */
47
+ export declare function attachAttestationHeader(holder: AttestationHolder, headers: Headers): Promise<void>;
18
48
  export type ClientSurface = "terminal" | "web" | "desktop" | "cloud";
19
49
  /**
20
50
  * pi accepts static provider headers only. Keep it behind a loopback proxy that
21
51
  * replaces the local bearer credential with a fresh DPoP proof per request.
22
52
  */
23
- export declare function ensureSigningProxy(cfg: CliConfig, fallbackClient?: ClientSurface, clientAttestation?: string): Promise<SigningProxy>;
53
+ export declare function ensureSigningProxy(cfg: CliConfig, fallbackClient?: ClientSurface, attestationSource?: ClientAttestationSource): Promise<SigningProxy>;
24
54
  export {};