@acosmi/sdk-ts 2.19.0 → 2.19.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.
@@ -134,12 +134,13 @@ function parseSettlement(ev) {
134
134
  callRemaining: s.callRemaining ?? -1
135
135
  };
136
136
  }
137
- exports.BucketClassCommercial = void 0; exports.BucketClassGeneric = void 0; exports.ThinkingOff = void 0; exports.ThinkingHigh = void 0; exports.ThinkingMax = void 0; exports.ThinkingHighMinMaxTokens = void 0; exports.ThinkingMaxFallbackMaxTokens = void 0; exports.ServerToolTypeWebSearch = void 0;
137
+ exports.BucketClassCommercial = void 0; exports.BucketClassGeneric = void 0; exports.ThinkingOff = void 0; exports.ThinkingLow = void 0; exports.ThinkingHigh = void 0; exports.ThinkingMax = void 0; exports.ThinkingHighMinMaxTokens = void 0; exports.ThinkingMaxFallbackMaxTokens = void 0; exports.ServerToolTypeWebSearch = void 0;
138
138
  var init_types = __esm({
139
139
  "src/models/types.ts"() {
140
140
  exports.BucketClassCommercial = "COMMERCIAL";
141
141
  exports.BucketClassGeneric = "GENERIC";
142
142
  exports.ThinkingOff = "off";
143
+ exports.ThinkingLow = "low";
143
144
  exports.ThinkingHigh = "high";
144
145
  exports.ThinkingMax = "max";
145
146
  exports.ThinkingHighMinMaxTokens = 32e3;
@@ -149,6 +150,55 @@ var init_types = __esm({
149
150
  });
150
151
 
151
152
  // src/shared/errors.ts
153
+ function nullableID(value) {
154
+ return value === null || typeof value === "string";
155
+ }
156
+ function decodeGatewayErrorContract(value) {
157
+ if (value == null || typeof value !== "object") return null;
158
+ const v = value;
159
+ const version = v.errorContractVersion;
160
+ const domain = v.faultDomain;
161
+ const disposition = v.requestDisposition;
162
+ const errorCode = v.errorCode;
163
+ if (version !== 1 || typeof domain !== "string" || !gatewayFaultDomains.includes(domain)) return null;
164
+ if (typeof errorCode !== "string" || errorCode.length === 0) return null;
165
+ if (disposition !== "not_accepted" && disposition !== "accepted" && disposition !== "unknown") return null;
166
+ if (!nullableID(v.transportRequestId) || !nullableID(v.consumeRequestId) || !nullableID(v.providerRequestId)) return null;
167
+ if (typeof v.retryable !== "boolean") return null;
168
+ return {
169
+ errorContractVersion: 1,
170
+ faultDomain: domain,
171
+ errorCode,
172
+ transportRequestId: v.transportRequestId,
173
+ consumeRequestId: v.consumeRequestId,
174
+ providerRequestId: v.providerRequestId,
175
+ requestDisposition: disposition,
176
+ retryable: v.retryable
177
+ };
178
+ }
179
+ function readGatewayErrorContract(error) {
180
+ const direct = decodeGatewayErrorContract(error);
181
+ if (direct) return direct;
182
+ if (error == null || typeof error !== "object") return null;
183
+ const e = error;
184
+ const axios = decodeGatewayErrorContract(e.response?.data);
185
+ if (axios) return axios;
186
+ if (typeof e.body === "string") {
187
+ try {
188
+ return decodeGatewayErrorContract(JSON.parse(e.body));
189
+ } catch {
190
+ return null;
191
+ }
192
+ }
193
+ return null;
194
+ }
195
+ function isUserAccessTokenRejected(error) {
196
+ if (error == null || typeof error !== "object") return false;
197
+ const e = error;
198
+ const status = e.statusCode ?? e.response?.status;
199
+ const contract = readGatewayErrorContract(error);
200
+ return status === 401 && contract?.faultDomain === "user_auth" && contract.errorCode === "USER_ACCESS_TOKEN_INVALID" && contract.requestDisposition === "not_accepted";
201
+ }
152
202
  function isWindowLimitError(err) {
153
203
  if (!(err instanceof exports.HTTPError)) return false;
154
204
  if (err.errorCode === windowLimitErrorCode) return true;
@@ -176,7 +226,7 @@ function getWindowLimitStreamDetails(err) {
176
226
  windowOverridable: typeof e.windowOverridable === "boolean" ? e.windowOverridable : void 0
177
227
  };
178
228
  }
179
- exports.RateLimitError = void 0; exports.BusinessError = void 0; exports.OrderTerminalError = void 0; exports.ModelNotFoundError = void 0; exports.HTTPError = void 0; exports.NetworkError = void 0; exports.StreamError = void 0; var windowLimitErrorCode, windowLimitStreamCode;
229
+ exports.RateLimitError = void 0; exports.BusinessError = void 0; exports.OrderTerminalError = void 0; exports.ModelNotFoundError = void 0; exports.HTTPError = void 0; exports.NetworkError = void 0; exports.StreamError = void 0; var gatewayFaultDomains, windowLimitErrorCode, windowLimitStreamCode;
180
230
  var init_errors = __esm({
181
231
  "src/shared/errors.ts"() {
182
232
  exports.RateLimitError = class extends Error {
@@ -235,6 +285,13 @@ var init_errors = __esm({
235
285
  * false/缺失 = 周窗/显式禁止/灰度关 (硬等待, 无豁免路径, 老网关不返回时为 undefined)。
236
286
  */
237
287
  windowOverridable;
288
+ errorContractVersion;
289
+ faultDomain;
290
+ requestDisposition;
291
+ transportRequestId;
292
+ consumeRequestId;
293
+ providerRequestId;
294
+ retryable;
238
295
  constructor(statusCode, opts = {}) {
239
296
  let msg;
240
297
  if (opts.type) {
@@ -256,6 +313,13 @@ var init_errors = __esm({
256
313
  this.windowKind = opts.windowKind;
257
314
  this.windowResetAt = opts.windowResetAt;
258
315
  this.windowOverridable = opts.windowOverridable;
316
+ this.errorContractVersion = opts.errorContractVersion;
317
+ this.faultDomain = opts.faultDomain;
318
+ this.requestDisposition = opts.requestDisposition;
319
+ this.transportRequestId = opts.transportRequestId;
320
+ this.consumeRequestId = opts.consumeRequestId;
321
+ this.providerRequestId = opts.providerRequestId;
322
+ this.retryable = opts.retryable === true && opts.requestDisposition === "not_accepted";
259
323
  }
260
324
  };
261
325
  exports.NetworkError = class extends Error {
@@ -266,6 +330,7 @@ var init_errors = __esm({
266
330
  cause;
267
331
  timeout;
268
332
  eof;
333
+ requestDisposition = "unknown";
269
334
  constructor(op, url, cause, opts = {}) {
270
335
  const causeMsg = cause instanceof Error ? cause.message : cause != null ? String(cause) : "network error";
271
336
  super(`${op} ${url}: ${causeMsg}`);
@@ -286,6 +351,8 @@ var init_errors = __esm({
286
351
  exports.StreamError = class extends Error {
287
352
  /** 例: "empty_response" / "rate_limit" / "overloaded" / "" */
288
353
  code;
354
+ /** D23 gateway machine code; mirrors the legacy `code` field. */
355
+ errorCode;
289
356
  /** 例: "provider" / "settlement" */
290
357
  stage;
291
358
  /** 用户友好提示 (中文); 历史字段, 与 rawError 区分 */
@@ -294,23 +361,47 @@ var init_errors = __esm({
294
361
  rawError;
295
362
  /** 客户端是否值得重试 */
296
363
  retryable;
364
+ errorContractVersion;
365
+ faultDomain;
366
+ requestDisposition;
367
+ transportRequestId;
368
+ consumeRequestId;
369
+ providerRequestId;
297
370
  constructor(opts = {}) {
298
371
  const code = opts.code ?? "";
299
372
  const stage = opts.stage ?? "";
300
373
  const userMessage = opts.message ?? "";
301
374
  const rawError = opts.rawError ?? "";
302
- const retryable = opts.retryable ?? false;
375
+ const retryable = opts.retryable === true && opts.requestDisposition === "not_accepted";
303
376
  const body = rawError !== "" ? rawError : userMessage;
304
377
  const msg = stage !== "" ? `stream failed: ${stage}: ${body}` : `stream failed: ${body}`;
305
378
  super(msg);
306
379
  this.name = "StreamError";
307
380
  this.code = code;
381
+ this.errorCode = code;
308
382
  this.stage = stage;
309
383
  this.userMessage = userMessage;
310
384
  this.rawError = rawError;
311
385
  this.retryable = retryable;
386
+ this.errorContractVersion = opts.errorContractVersion;
387
+ this.faultDomain = opts.faultDomain;
388
+ this.requestDisposition = opts.requestDisposition;
389
+ this.transportRequestId = opts.transportRequestId;
390
+ this.consumeRequestId = opts.consumeRequestId;
391
+ this.providerRequestId = opts.providerRequestId;
312
392
  }
313
393
  };
394
+ gatewayFaultDomains = [
395
+ "user_auth",
396
+ "caller_credentials",
397
+ "account_quota",
398
+ "account_permission",
399
+ "provider",
400
+ "gateway",
401
+ "transport",
402
+ "stream_ticket",
403
+ "protocol"
404
+ ];
314
405
  windowLimitErrorCode = "WINDOW_LIMIT_EXCEEDED";
315
406
  windowLimitStreamCode = "window_limit_exceeded";
316
407
  }
@@ -416,6 +507,8 @@ function resolveThinkingLevel(body, req, caps) {
416
507
  }
417
508
  if (caps.supports_effort) {
418
509
  let effortLevel = "high";
510
+ if (level === "low") effortLevel = "low";
511
+ if (level === "medium" || level === "xhigh") effortLevel = level;
419
512
  if (level === exports.ThinkingMax && caps.supports_max_effort) {
420
513
  effortLevel = "max";
421
514
  }
@@ -593,23 +686,28 @@ __export(openai_exports, {
593
686
  resolveOpenAIReasoningEffort: () => resolveOpenAIReasoningEffort,
594
687
  resolveOpenAIResponseFormat: () => resolveOpenAIResponseFormat
595
688
  });
596
- function resolveOpenAIReasoningEffort(req) {
689
+ function resolveOpenAIReasoningEffort(req, supportsMax = false) {
597
690
  if (req.effort && req.effort.level !== "") {
598
691
  switch (req.effort.level) {
599
692
  case "low":
600
693
  case "medium":
601
694
  case "high":
695
+ case "xhigh":
602
696
  return req.effort.level;
603
697
  case "max":
604
- return "high";
698
+ return supportsMax ? "max" : "high";
605
699
  }
606
700
  }
607
701
  if (req.thinking) {
608
702
  switch (req.thinking.level) {
703
+ case "low":
704
+ case "medium":
705
+ case "xhigh":
706
+ return req.thinking.level;
609
707
  case exports.ThinkingHigh:
610
708
  return "high";
611
709
  case exports.ThinkingMax:
612
- return "high";
710
+ return supportsMax ? "max" : "high";
613
711
  case exports.ThinkingOff:
614
712
  return "";
615
713
  }
@@ -813,10 +911,13 @@ var init_openai = __esm({
813
911
  if (req.tools != null) {
814
912
  body["tools"] = req.tools;
815
913
  }
816
- const eff = resolveOpenAIReasoningEffort(req);
914
+ const eff = resolveOpenAIReasoningEffort(req, _caps.supports_max_effort);
817
915
  if (eff !== "") {
818
916
  body["reasoning_effort"] = eff;
819
917
  }
918
+ if (_caps.supports_thinking && req.thinking?.level === exports.ThinkingOff) {
919
+ body["thinking"] = { type: "disabled" };
920
+ }
820
921
  if (req.speed && req.speed !== "") {
821
922
  body["speed"] = req.speed;
822
923
  }
@@ -893,16 +994,90 @@ var init_openai = __esm({
893
994
  * 可能已被 text/tool 推进的 this.blockIndex (否则 content_block_stop 索引错配)。 */
894
995
  thinkingBlockIndex = 0;
895
996
  textStarted = false;
896
- /** OpenAI tool_call index → Anthropic block index */
997
+ /** OpenAI tool_call → Anthropic block index。键正常是 `tc.index`;上游省略
998
+ * index 时退化为 `id:<tool_call_id>`,两者都没有时沿用上一个键(见
999
+ * {@link resolveToolKey})。 */
897
1000
  toolBlockIndex = /* @__PURE__ */ new Map();
898
1001
  blockIndex = 0;
1002
+ /** 每个 tool block 已发出的 `partial_json` 累积,用于识别「每片重发全量参数」
1003
+ * 的上游(见 tool_calls 分支的累计判别)。 */
1004
+ toolArgsAccum = /* @__PURE__ */ new Map();
1005
+ /** 上一次解析出的 tool 键,供缺 index 且缺 id 的后续增量沿用。 */
1006
+ lastToolKey = null;
1007
+ /** 已发出 message_delta/message_stop,避免 finish_reason 与 `[DONE]` 各收一次。 */
1008
+ messageClosed = false;
1009
+ /**
1010
+ * 解析一条 tool_call delta 归属的块键。
1011
+ *
1012
+ * OpenAI 流式规范里 `index` 是必填,但兼容实现常有省略。此前这里直接用
1013
+ * `tc.index` 做 Map 键:两个都省略 index 的 tool_call 会共用键 `undefined`,
1014
+ * 于是只开一个块、两段参数拼进同一条 `partial_json` 流,产出 `{…}{…}` 这种
1015
+ * 必然非法的 JSON。这里按「index → id → 沿用上一个」三级降级,让至少一种
1016
+ * 稳定标识生效。
1017
+ */
1018
+ resolveToolKey(tc) {
1019
+ if (typeof tc.index === "number" && Number.isFinite(tc.index)) {
1020
+ this.lastToolKey = tc.index;
1021
+ return tc.index;
1022
+ }
1023
+ if (typeof tc.id === "string" && tc.id !== "") {
1024
+ const key = `id:${tc.id}`;
1025
+ this.lastToolKey = key;
1026
+ return key;
1027
+ }
1028
+ if (this.lastToolKey !== null) return this.lastToolKey;
1029
+ this.lastToolKey = 0;
1030
+ return 0;
1031
+ }
1032
+ /**
1033
+ * 关闭仍打开的 text / thinking / tool 块,并收口 message。
1034
+ *
1035
+ * 由 `finish_reason` 分支与 `[DONE]` 分支共用:上游断流或只发 `[DONE]` 而不发
1036
+ * `finish_reason` 时,此前一个 `content_block_stop` 都不会发,下游拿到的是一个
1037
+ * 永不闭合的 tool_use 块。
1038
+ */
1039
+ closeOpenBlocks(events, stopReason) {
1040
+ if (this.messageClosed) return;
1041
+ this.messageClosed = true;
1042
+ if (this.textStarted) {
1043
+ events.push({
1044
+ event: "content_block_stop",
1045
+ data: JSON.stringify({ type: "content_block_stop", index: this.blockIndex })
1046
+ });
1047
+ this.textStarted = false;
1048
+ } else if (this.thinkingStarted && !this.thinkingStopped) {
1049
+ this.thinkingStopped = true;
1050
+ events.push({
1051
+ event: "content_block_stop",
1052
+ data: JSON.stringify({ type: "content_block_stop", index: this.thinkingBlockIndex })
1053
+ });
1054
+ }
1055
+ for (const idx of this.toolBlockIndex.values()) {
1056
+ events.push({
1057
+ event: "content_block_stop",
1058
+ data: JSON.stringify({ type: "content_block_stop", index: idx })
1059
+ });
1060
+ }
1061
+ events.push({
1062
+ event: "message_delta",
1063
+ data: JSON.stringify({ type: "message_delta", delta: { stop_reason: stopReason } })
1064
+ });
1065
+ events.push({
1066
+ event: "message_stop",
1067
+ data: JSON.stringify({ type: "message_stop" })
1068
+ });
1069
+ }
899
1070
  /**
900
1071
  * 将一行 OpenAI SSE data 转换为零或多个 Anthropic 格式 StreamEvent
901
1072
  * 返回 { events, done }
902
1073
  */
903
1074
  convert(data) {
904
1075
  if (data === "[DONE]") {
905
- return { events: [], done: true };
1076
+ const events2 = [];
1077
+ if (this.messageStarted) {
1078
+ this.closeOpenBlocks(events2, "end_turn");
1079
+ }
1080
+ return { events: events2, done: true };
906
1081
  }
907
1082
  let chunk;
908
1083
  try {
@@ -974,7 +1149,9 @@ var init_openai = __esm({
974
1149
  events.push({ event: "content_block_delta", data: deltaJSON });
975
1150
  }
976
1151
  for (const tc of choice.delta.tool_calls ?? []) {
977
- if (!this.toolBlockIndex.has(tc.index)) {
1152
+ const fn = tc.function;
1153
+ const toolKey = this.resolveToolKey(tc);
1154
+ if (!this.toolBlockIndex.has(toolKey)) {
978
1155
  if (this.thinkingStarted && !this.thinkingStopped) {
979
1156
  this.thinkingStopped = true;
980
1157
  const stopJSON = JSON.stringify({
@@ -993,56 +1170,46 @@ var init_openai = __esm({
993
1170
  this.blockIndex++;
994
1171
  this.textStarted = false;
995
1172
  }
996
- this.toolBlockIndex.set(tc.index, this.blockIndex);
1173
+ this.toolBlockIndex.set(toolKey, this.blockIndex);
997
1174
  const blockJSON = JSON.stringify({
998
1175
  type: "content_block_start",
999
1176
  index: this.blockIndex,
1000
1177
  content_block: {
1001
1178
  type: "tool_use",
1002
1179
  id: tc.id,
1003
- name: tc.function.name,
1180
+ name: fn?.name,
1004
1181
  input: {}
1005
1182
  }
1006
1183
  });
1007
1184
  events.push({ event: "content_block_start", data: blockJSON });
1008
1185
  this.blockIndex++;
1009
1186
  }
1010
- if (tc.function.arguments && tc.function.arguments !== "") {
1011
- const idx = this.toolBlockIndex.get(tc.index);
1012
- const deltaJSON = JSON.stringify({
1013
- type: "content_block_delta",
1014
- index: idx,
1015
- delta: {
1016
- type: "input_json_delta",
1017
- partial_json: tc.function.arguments
1018
- }
1019
- });
1020
- events.push({ event: "content_block_delta", data: deltaJSON });
1187
+ if (fn?.arguments !== void 0 && fn.arguments !== null && fn.arguments !== "") {
1188
+ const rawArgs = typeof fn.arguments === "string" ? fn.arguments : JSON.stringify(fn.arguments);
1189
+ const accum = this.toolArgsAccum.get(toolKey) ?? "";
1190
+ let emit = rawArgs;
1191
+ if (accum !== "" && rawArgs.length > accum.length && rawArgs.startsWith(accum)) {
1192
+ emit = rawArgs.slice(accum.length);
1193
+ this.toolArgsAccum.set(toolKey, rawArgs);
1194
+ } else {
1195
+ this.toolArgsAccum.set(toolKey, accum + rawArgs);
1196
+ }
1197
+ if (emit !== "") {
1198
+ const idx = this.toolBlockIndex.get(toolKey);
1199
+ const deltaJSON = JSON.stringify({
1200
+ type: "content_block_delta",
1201
+ index: idx,
1202
+ delta: {
1203
+ type: "input_json_delta",
1204
+ partial_json: emit
1205
+ }
1206
+ });
1207
+ events.push({ event: "content_block_delta", data: deltaJSON });
1208
+ }
1021
1209
  }
1022
1210
  }
1023
1211
  if (choice.finish_reason != null && choice.finish_reason !== "") {
1024
- if (this.textStarted) {
1025
- const stopJSON2 = JSON.stringify({
1026
- type: "content_block_stop",
1027
- index: this.blockIndex
1028
- });
1029
- events.push({ event: "content_block_stop", data: stopJSON2 });
1030
- } else if (this.thinkingStarted && !this.thinkingStopped) {
1031
- this.thinkingStopped = true;
1032
- const stopJSON2 = JSON.stringify({
1033
- type: "content_block_stop",
1034
- index: this.thinkingBlockIndex
1035
- });
1036
- events.push({ event: "content_block_stop", data: stopJSON2 });
1037
- }
1038
- for (const idx of this.toolBlockIndex.values()) {
1039
- const stopJSON2 = JSON.stringify({
1040
- type: "content_block_stop",
1041
- index: idx
1042
- });
1043
- events.push({ event: "content_block_stop", data: stopJSON2 });
1044
- }
1045
- let stopReason = "end_turn";
1212
+ let stopReason;
1046
1213
  switch (choice.finish_reason) {
1047
1214
  case "tool_calls":
1048
1215
  stopReason = "tool_use";
@@ -1050,14 +1217,13 @@ var init_openai = __esm({
1050
1217
  case "length":
1051
1218
  stopReason = "max_tokens";
1052
1219
  break;
1220
+ case "stop":
1221
+ stopReason = "end_turn";
1222
+ break;
1223
+ default:
1224
+ stopReason = choice.finish_reason;
1053
1225
  }
1054
- const deltaJSON = JSON.stringify({
1055
- type: "message_delta",
1056
- delta: { stop_reason: stopReason }
1057
- });
1058
- events.push({ event: "message_delta", data: deltaJSON });
1059
- const stopJSON = JSON.stringify({ type: "message_stop" });
1060
- events.push({ event: "message_stop", data: stopJSON });
1226
+ this.closeOpenBlocks(events, stopReason);
1061
1227
  }
1062
1228
  return { events, done: false };
1063
1229
  }
@@ -1118,6 +1284,560 @@ var init_adapters = __esm({
1118
1284
  }
1119
1285
  });
1120
1286
 
1287
+ // src/core/credentials.ts
1288
+ var uuid = () => globalThis.crypto.randomUUID();
1289
+ var pause = () => new Promise((resolve) => setTimeout(resolve, 25));
1290
+ var abort = (signal) => {
1291
+ if (signal?.aborted) throw new Error("aborted");
1292
+ };
1293
+ var sameRequestOwner = (snapshot, expected) => snapshot.storeInstanceId === expected.storeInstanceId && snapshot.authSessionId === expected.authSessionId && snapshot.principal?.issuer === expected.principal?.issuer && snapshot.principal?.subject === expected.principal?.subject && snapshot.principal?.organizationId === expected.principal?.organizationId;
1294
+ var CredentialLifecycle = class {
1295
+ constructor(store, protocol) {
1296
+ this.store = store;
1297
+ this.protocol = protocol;
1298
+ }
1299
+ store;
1300
+ protocol;
1301
+ listeners = /* @__PURE__ */ new Set();
1302
+ notificationKey = "";
1303
+ observations = /* @__PURE__ */ new Map();
1304
+ read(signal) {
1305
+ return this.store.readSnapshot(signal);
1306
+ }
1307
+ subscribe(listener) {
1308
+ this.listeners.add(listener);
1309
+ return () => {
1310
+ this.listeners.delete(listener);
1311
+ };
1312
+ }
1313
+ async notify(snapshot) {
1314
+ let current;
1315
+ try {
1316
+ current = await this.read();
1317
+ } catch {
1318
+ return;
1319
+ }
1320
+ if (current.storeInstanceId !== snapshot.storeInstanceId || current.revision !== snapshot.revision)
1321
+ return;
1322
+ const projection = {
1323
+ storeInstanceId: snapshot.storeInstanceId,
1324
+ authorityConfig: snapshot.authorityConfig && {
1325
+ serverURL: snapshot.authorityConfig.serverURL,
1326
+ issuer: snapshot.authorityConfig.issuer,
1327
+ oauthProfile: "desktop",
1328
+ authContractVersion: 2,
1329
+ errorContractVersion: 1
1330
+ },
1331
+ revision: snapshot.revision,
1332
+ authSessionId: snapshot.authSessionId,
1333
+ credentialState: snapshot.credentialState,
1334
+ reason: snapshot.reason,
1335
+ principal: snapshot.principal && {
1336
+ issuer: snapshot.principal.issuer,
1337
+ subject: snapshot.principal.subject,
1338
+ organizationId: snapshot.principal.organizationId
1339
+ },
1340
+ refreshOperation: snapshot.refreshOperation && {
1341
+ operationId: snapshot.refreshOperation.operationId,
1342
+ sessionId: snapshot.refreshOperation.sessionId,
1343
+ baseRevision: snapshot.refreshOperation.baseRevision,
1344
+ phase: snapshot.refreshOperation.phase,
1345
+ returnState: snapshot.refreshOperation.returnState,
1346
+ startedAt: snapshot.refreshOperation.startedAt,
1347
+ dispatchedAt: snapshot.refreshOperation.dispatchedAt,
1348
+ deadlineAt: snapshot.refreshOperation.deadlineAt
1349
+ },
1350
+ loginAttempt: snapshot.loginAttempt && {
1351
+ attemptId: snapshot.loginAttempt.attemptId,
1352
+ baseSessionId: snapshot.loginAttempt.baseSessionId,
1353
+ startedAt: snapshot.loginAttempt.startedAt
1354
+ },
1355
+ lastMutation: snapshot.lastMutation && {
1356
+ mutationId: snapshot.lastMutation.mutationId,
1357
+ operationId: snapshot.lastMutation.operationId,
1358
+ resultRevision: snapshot.lastMutation.resultRevision
1359
+ },
1360
+ lastLoginAttemptId: snapshot.lastLoginAttemptId,
1361
+ verifiedIdentity: snapshot.verifiedIdentity && {
1362
+ authSessionId: snapshot.verifiedIdentity.authSessionId,
1363
+ principal: {
1364
+ issuer: snapshot.verifiedIdentity.principal.issuer,
1365
+ subject: snapshot.verifiedIdentity.principal.subject,
1366
+ organizationId: snapshot.verifiedIdentity.principal.organizationId
1367
+ },
1368
+ displayName: snapshot.verifiedIdentity.displayName,
1369
+ avatarUrl: snapshot.verifiedIdentity.avatarUrl,
1370
+ email: snapshot.verifiedIdentity.email,
1371
+ imageUrl: snapshot.verifiedIdentity.imageUrl,
1372
+ accountCreatedAt: snapshot.verifiedIdentity.accountCreatedAt,
1373
+ requiresPhoneBinding: snapshot.verifiedIdentity.requiresPhoneBinding,
1374
+ hasExtraUsageEnabled: snapshot.verifiedIdentity.hasExtraUsageEnabled,
1375
+ billingType: snapshot.verifiedIdentity.billingType,
1376
+ subscriptionCreatedAt: snapshot.verifiedIdentity.subscriptionCreatedAt,
1377
+ rateLimitTier: snapshot.verifiedIdentity.rateLimitTier,
1378
+ organizationName: snapshot.verifiedIdentity.organizationName,
1379
+ verifiedAt: snapshot.verifiedIdentity.verifiedAt
1380
+ }
1381
+ };
1382
+ const key = JSON.stringify({ ...projection, revision: void 0, lastMutation: void 0 });
1383
+ if (key === this.notificationKey) return;
1384
+ this.notificationKey = key;
1385
+ for (const listener of this.listeners) {
1386
+ try {
1387
+ listener(structuredClone(projection));
1388
+ } catch {
1389
+ }
1390
+ }
1391
+ }
1392
+ async reconcile(signal) {
1393
+ const s = await this.read(signal);
1394
+ await this.notify(s);
1395
+ return s;
1396
+ }
1397
+ cas(s, changes, mutationId = uuid(), signal) {
1398
+ return this.store.compareAndSwap(
1399
+ {
1400
+ storeInstanceId: s.storeInstanceId,
1401
+ revision: s.revision,
1402
+ authSessionId: s.authSessionId,
1403
+ state: s.credentialState,
1404
+ operationId: s.refreshOperation?.operationId ?? s.loginAttempt?.attemptId ?? null
1405
+ },
1406
+ { ...s, ...changes },
1407
+ mutationId,
1408
+ signal
1409
+ );
1410
+ }
1411
+ async commit(s, changes, signal) {
1412
+ const r = await this.cas(s, changes, uuid(), signal);
1413
+ if (r.status === "storage_error") throw new Error("storage_unavailable");
1414
+ if (r.status === "committed") await this.notify(r.snapshot);
1415
+ return r;
1416
+ }
1417
+ async metadata(s, signal) {
1418
+ const m = await this.protocol.metadata(signal);
1419
+ if (m.crabcode_auth_contract_version !== 2 || m.gateway_error_contract_version !== 1 || !m.issuer)
1420
+ throw new Error("auth_contract_unsupported");
1421
+ if (s.authorityConfig && (s.authorityConfig.serverURL !== this.protocol.serverURL || s.authorityConfig.issuer !== m.issuer || s.authorityConfig.oauthProfile !== "desktop" || s.authorityConfig.authContractVersion !== 2 || s.authorityConfig.errorContractVersion !== 1))
1422
+ throw new Error("auth_contract_unsupported");
1423
+ return m;
1424
+ }
1425
+ async reserveLogin(signal) {
1426
+ const attemptId = uuid();
1427
+ let reserved;
1428
+ for (; ; ) {
1429
+ abort(signal);
1430
+ const s = await this.read(signal);
1431
+ if (s.credentialState !== "signed_out") throw new Error("local_logout_required");
1432
+ const r = await this.commit(s, { loginAttempt: { attemptId, baseSessionId: s.authSessionId, startedAt: (/* @__PURE__ */ new Date()).toISOString() } }, signal);
1433
+ if (r.status === "committed") {
1434
+ reserved = r.snapshot;
1435
+ break;
1436
+ }
1437
+ }
1438
+ try {
1439
+ const m = await this.metadata(reserved, signal);
1440
+ for (; ; ) {
1441
+ abort(signal);
1442
+ const current = await this.read(signal);
1443
+ if (current.storeInstanceId !== reserved.storeInstanceId || current.loginAttempt?.attemptId !== attemptId) throw new Error("superseded");
1444
+ if (current.authorityConfig) return { attemptId, metadata: m };
1445
+ const r = await this.commit(current, { authorityConfig: { serverURL: this.protocol.serverURL, issuer: m.issuer, oauthProfile: "desktop", authContractVersion: 2, errorContractVersion: 1 } }, signal);
1446
+ if (r.status === "committed") return { attemptId, metadata: m };
1447
+ }
1448
+ } catch (error) {
1449
+ await this.cancelLogin(attemptId);
1450
+ throw error;
1451
+ }
1452
+ }
1453
+ async installLogin(attemptId, tokens, signal) {
1454
+ this.validateTokens(tokens);
1455
+ for (; ; ) {
1456
+ abort(signal);
1457
+ const s = await this.read(signal);
1458
+ if (s.loginAttempt?.attemptId !== attemptId || s.loginAttempt.baseSessionId !== s.authSessionId)
1459
+ throw new Error("superseded");
1460
+ const r = await this.commit(
1461
+ s,
1462
+ {
1463
+ authSessionId: uuid(),
1464
+ tokenSet: tokens,
1465
+ credentialState: "pending_identity",
1466
+ principal: null,
1467
+ verifiedIdentity: null,
1468
+ loginAttempt: null,
1469
+ lastLoginAttemptId: attemptId,
1470
+ reason: "identity_unavailable"
1471
+ },
1472
+ signal
1473
+ );
1474
+ if (r.status === "committed") return this.bindIdentity(r.snapshot.authSessionId, signal);
1475
+ }
1476
+ }
1477
+ async assertLogin(attemptId, signal) {
1478
+ abort(signal);
1479
+ const s = await this.read(signal);
1480
+ if (s.loginAttempt?.attemptId !== attemptId) throw new Error("superseded");
1481
+ }
1482
+ async cancelLogin(attemptId) {
1483
+ const s = await this.read();
1484
+ if (s.loginAttempt?.attemptId === attemptId) await this.commit(s, { loginAttempt: null });
1485
+ }
1486
+ validateTokens(t) {
1487
+ if (!t.access_token?.trim() || !t.refresh_token?.trim() || !t.client_id?.trim() || t.server_url !== this.protocol.serverURL || !Number.isFinite(Date.parse(t.expires_at)) || Date.parse(t.expires_at) <= Date.now())
1488
+ throw new Error("invalid_response");
1489
+ }
1490
+ usable(s) {
1491
+ return !!s.tokenSet && Date.parse(s.tokenSet.expires_at) > Date.now() + 3e5;
1492
+ }
1493
+ async bindIdentity(sessionId, signal) {
1494
+ const s = await this.ensureSnapshot(signal, void 0, true);
1495
+ if (s.authSessionId !== sessionId) throw new Error("superseded");
1496
+ if (s.credentialState === "ready") return s;
1497
+ await this.metadata(s, signal);
1498
+ const beforeProfile = await this.read(signal);
1499
+ if (beforeProfile.storeInstanceId !== s.storeInstanceId || beforeProfile.authSessionId !== sessionId || beforeProfile.revision !== s.revision || beforeProfile.credentialState !== "pending_identity")
1500
+ throw new Error("superseded");
1501
+ let identity;
1502
+ try {
1503
+ identity = await this.protocol.profile(s.tokenSet, signal);
1504
+ } catch (error) {
1505
+ const message = error instanceof Error ? error.message : "";
1506
+ if (message === "invalid_scope" || message === "auth_contract_unsupported")
1507
+ await this.commit(s, { credentialState: "configuration_error", reason: message });
1508
+ throw error;
1509
+ }
1510
+ abort(signal);
1511
+ const current = await this.read(signal);
1512
+ if (current.storeInstanceId !== s.storeInstanceId || current.authSessionId !== sessionId || current.revision !== s.revision)
1513
+ throw new Error("superseded");
1514
+ if (!identity.subject) throw new Error("invalid_response");
1515
+ const principal = {
1516
+ issuer: s.authorityConfig.issuer,
1517
+ subject: identity.subject,
1518
+ organizationId: identity.organizationId
1519
+ };
1520
+ const r = await this.commit(
1521
+ s,
1522
+ {
1523
+ principal,
1524
+ verifiedIdentity: {
1525
+ authSessionId: sessionId,
1526
+ principal,
1527
+ displayName: identity.displayName,
1528
+ avatarUrl: identity.avatarUrl,
1529
+ email: identity.email,
1530
+ imageUrl: identity.imageUrl,
1531
+ accountCreatedAt: identity.accountCreatedAt,
1532
+ requiresPhoneBinding: identity.requiresPhoneBinding,
1533
+ hasExtraUsageEnabled: identity.hasExtraUsageEnabled,
1534
+ billingType: identity.billingType,
1535
+ subscriptionCreatedAt: identity.subscriptionCreatedAt,
1536
+ rateLimitTier: identity.rateLimitTier,
1537
+ organizationName: identity.organizationName,
1538
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
1539
+ },
1540
+ credentialState: "ready",
1541
+ reason: null
1542
+ },
1543
+ signal
1544
+ );
1545
+ if (r.status !== "committed") throw new Error("superseded");
1546
+ return r.snapshot;
1547
+ }
1548
+ async ensure(signal, rejected, expected) {
1549
+ const snapshot = await this.ensureSnapshot(signal, rejected, false, expected);
1550
+ this.observations.set(snapshot.tokenSet.access_token, snapshot);
1551
+ if (this.observations.size > 128)
1552
+ this.observations.delete(this.observations.keys().next().value);
1553
+ return snapshot.tokenSet.access_token;
1554
+ }
1555
+ async forceRefresh(signal, rejectedToken, expected) {
1556
+ const rejected = rejectedToken ? this.observations.get(rejectedToken) : await this.read(signal);
1557
+ if (rejectedToken && !rejected) throw new Error("credential_observation_unavailable");
1558
+ return this.ensure(signal, rejected, expected);
1559
+ }
1560
+ async ensureSnapshot(signal, rejected, pending = false, expected) {
1561
+ let session;
1562
+ let instance;
1563
+ let initialRevision;
1564
+ let refreshed = false;
1565
+ let initialAccess;
1566
+ for (; ; ) {
1567
+ abort(signal);
1568
+ const s = await this.read(signal);
1569
+ if (expected && !sameRequestOwner(s, expected)) throw new Error("superseded");
1570
+ if (s.authorityConfig && s.authorityConfig.serverURL !== this.protocol.serverURL)
1571
+ throw new Error("auth_contract_unsupported");
1572
+ if (session === void 0) {
1573
+ session = s.authSessionId;
1574
+ instance = s.storeInstanceId;
1575
+ initialRevision = s.revision;
1576
+ initialAccess = s.tokenSet?.access_token;
1577
+ }
1578
+ if (s.storeInstanceId !== instance || s.authSessionId !== session || rejected && (s.authSessionId !== rejected.authSessionId || s.storeInstanceId !== rejected.storeInstanceId))
1579
+ throw new Error("superseded");
1580
+ if (s.credentialState === "refresh_dispatched") {
1581
+ if (Date.now() >= Date.parse(s.refreshOperation.deadlineAt))
1582
+ await this.commit(s, {
1583
+ credentialState: "reauth_required",
1584
+ tokenSet: null,
1585
+ refreshOperation: null,
1586
+ reason: "refresh_outcome_unknown"
1587
+ });
1588
+ else await pause();
1589
+ continue;
1590
+ }
1591
+ if (s.credentialState === "refresh_reserved") {
1592
+ if (Date.now() >= Date.parse(s.refreshOperation.startedAt) + 3e4)
1593
+ await this.commit(s, {
1594
+ credentialState: s.refreshOperation.returnState,
1595
+ refreshOperation: null
1596
+ });
1597
+ else await pause();
1598
+ continue;
1599
+ }
1600
+ if (s.credentialState !== "ready" && !(pending && s.credentialState === "pending_identity"))
1601
+ throw new Error(`credential_unavailable:${s.credentialState}:${s.reason ?? ""}`);
1602
+ const live = !!s.tokenSet && Date.parse(s.tokenSet.expires_at) > Date.now();
1603
+ if (live && (refreshed || s.revision !== (rejected?.revision ?? initialRevision) && (rejected !== void 0 || s.tokenSet.access_token !== initialAccess)))
1604
+ return s;
1605
+ if (this.usable(s) && !rejected) return s;
1606
+ const metadata = await this.metadata(s, signal);
1607
+ abort(signal);
1608
+ const operationId = uuid();
1609
+ const r = await this.commit(
1610
+ s,
1611
+ {
1612
+ credentialState: "refresh_reserved",
1613
+ refreshOperation: {
1614
+ operationId,
1615
+ sessionId: s.authSessionId,
1616
+ baseRevision: s.revision,
1617
+ phase: "reserved",
1618
+ returnState: s.credentialState,
1619
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1620
+ dispatchedAt: null,
1621
+ deadlineAt: null
1622
+ }
1623
+ },
1624
+ signal
1625
+ );
1626
+ if (r.status !== "committed") continue;
1627
+ const owner = this.refresh(r.snapshot, metadata);
1628
+ await this.wait(owner, signal);
1629
+ refreshed = true;
1630
+ rejected = void 0;
1631
+ }
1632
+ }
1633
+ wait(promise, signal) {
1634
+ if (!signal) return promise;
1635
+ return new Promise((resolve, reject) => {
1636
+ const onAbort = () => {
1637
+ signal.removeEventListener("abort", onAbort);
1638
+ reject(new Error("aborted"));
1639
+ };
1640
+ signal.addEventListener("abort", onAbort, { once: true });
1641
+ promise.then(
1642
+ (value) => {
1643
+ signal.removeEventListener("abort", onAbort);
1644
+ resolve(value);
1645
+ },
1646
+ (error) => {
1647
+ signal.removeEventListener("abort", onAbort);
1648
+ reject(error);
1649
+ }
1650
+ );
1651
+ if (signal.aborted) onAbort();
1652
+ });
1653
+ }
1654
+ async refresh(reserved, metadata) {
1655
+ const dispatchedAt = Date.now();
1656
+ const r = await this.commit(reserved, {
1657
+ credentialState: "refresh_dispatched",
1658
+ refreshOperation: {
1659
+ ...reserved.refreshOperation,
1660
+ phase: "dispatched",
1661
+ dispatchedAt: new Date(dispatchedAt).toISOString(),
1662
+ deadlineAt: new Date(dispatchedAt + 3e4).toISOString()
1663
+ }
1664
+ });
1665
+ if (r.status !== "committed") return;
1666
+ const s = r.snapshot;
1667
+ const ctl = new AbortController();
1668
+ const timeout = setTimeout(() => ctl.abort(), 3e4);
1669
+ let tokens;
1670
+ try {
1671
+ const result = await this.wait(
1672
+ (async () => {
1673
+ const response = await this.protocol.fetch(metadata.token_endpoint, {
1674
+ method: "POST",
1675
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1676
+ body: new URLSearchParams({
1677
+ grant_type: "refresh_token",
1678
+ client_id: s.tokenSet.client_id,
1679
+ refresh_token: s.tokenSet.refresh_token
1680
+ }),
1681
+ signal: ctl.signal
1682
+ });
1683
+ const body2 = await response.json();
1684
+ return { response, body: body2 };
1685
+ })(),
1686
+ ctl.signal
1687
+ );
1688
+ if (!result.response.ok) {
1689
+ const code = result.body.error;
1690
+ let reason = "refresh_outcome_unknown";
1691
+ let state = "reauth_required";
1692
+ if (code === "invalid_grant") reason = code;
1693
+ else if (["invalid_client", "invalid_scope", "unsupported_grant_type"].includes(code)) {
1694
+ reason = code;
1695
+ state = "configuration_error";
1696
+ } else if (result.body.rotationOutcome === "not_committed" && ["temporarily_unavailable", "server_error"].includes(code)) {
1697
+ await this.commit(s, {
1698
+ credentialState: s.refreshOperation.returnState,
1699
+ refreshOperation: null
1700
+ });
1701
+ throw new Error("refresh_temporarily_unavailable");
1702
+ }
1703
+ await this.commit(s, {
1704
+ credentialState: state,
1705
+ tokenSet: null,
1706
+ refreshOperation: null,
1707
+ reason
1708
+ });
1709
+ throw new Error(reason);
1710
+ }
1711
+ const body = result.body;
1712
+ if (!Number.isFinite(body.expires_in) || body.expires_in <= 0)
1713
+ throw new Error("invalid_response");
1714
+ tokens = {
1715
+ ...s.tokenSet,
1716
+ access_token: body.access_token,
1717
+ refresh_token: body.refresh_token,
1718
+ expires_at: new Date(Date.now() + body.expires_in * 1e3).toISOString(),
1719
+ scope: body.scope ?? s.tokenSet.scope
1720
+ };
1721
+ this.validateTokens(tokens);
1722
+ } catch (error) {
1723
+ await this.commit(s, {
1724
+ credentialState: "reauth_required",
1725
+ tokenSet: null,
1726
+ refreshOperation: null,
1727
+ reason: "refresh_outcome_unknown"
1728
+ });
1729
+ throw error;
1730
+ } finally {
1731
+ clearTimeout(timeout);
1732
+ }
1733
+ const started = performance.now(), mutationId = uuid();
1734
+ for (; ; ) {
1735
+ if (Date.now() >= Date.parse(s.refreshOperation.deadlineAt)) {
1736
+ await this.commit(s, {
1737
+ credentialState: "reauth_required",
1738
+ tokenSet: null,
1739
+ refreshOperation: null,
1740
+ reason: "refresh_outcome_unknown"
1741
+ });
1742
+ void this.revoke(tokens, metadata);
1743
+ throw new Error("refresh_outcome_unknown");
1744
+ }
1745
+ const result = await this.cas(
1746
+ s,
1747
+ {
1748
+ tokenSet: tokens,
1749
+ credentialState: s.refreshOperation.returnState,
1750
+ refreshOperation: null
1751
+ },
1752
+ mutationId
1753
+ );
1754
+ if (result.status === "committed") {
1755
+ await this.notify(result.snapshot);
1756
+ return;
1757
+ }
1758
+ if (result.status === "superseded") {
1759
+ void this.revoke(tokens, metadata);
1760
+ return;
1761
+ }
1762
+ if (performance.now() - started >= 2e3) throw new Error("credential_persist_failed");
1763
+ await pause();
1764
+ }
1765
+ }
1766
+ async revoke(tokens, metadata, authority) {
1767
+ const ctl = new AbortController();
1768
+ const timer = setTimeout(() => ctl.abort(), 3e4);
1769
+ try {
1770
+ const m = metadata ?? await this.wait(authority ? this.metadata(authority, ctl.signal) : this.protocol.metadata(ctl.signal), ctl.signal);
1771
+ if (!m.revocation_endpoint) return "unsupported";
1772
+ const response = await this.wait(this.protocol.fetch(m.revocation_endpoint, {
1773
+ method: "POST",
1774
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1775
+ body: new URLSearchParams({ token: tokens.refresh_token, token_type_hint: "refresh_token" }),
1776
+ signal: ctl.signal
1777
+ }), ctl.signal);
1778
+ return response.ok ? "confirmed" : "failed";
1779
+ } catch {
1780
+ return "failed";
1781
+ } finally {
1782
+ clearTimeout(timer);
1783
+ }
1784
+ }
1785
+ async logout(signal, expected) {
1786
+ let s = await this.read(signal);
1787
+ if (expected && (s.storeInstanceId !== expected.storeInstanceId || s.authSessionId !== expected.authSessionId)) {
1788
+ return {
1789
+ logoutOperationId: uuid(),
1790
+ expectedAuthSessionId: expected.authSessionId,
1791
+ storeInstanceId: expected.storeInstanceId,
1792
+ revision: s.revision,
1793
+ status: "superseded"
1794
+ };
1795
+ }
1796
+ const session = s.authSessionId, instance = s.storeInstanceId, started = performance.now(), logoutOperationId = uuid();
1797
+ const receipt = () => ({
1798
+ logoutOperationId,
1799
+ expectedAuthSessionId: session,
1800
+ storeInstanceId: instance,
1801
+ revision: s.revision
1802
+ });
1803
+ for (; ; ) {
1804
+ abort(signal);
1805
+ if (s.storeInstanceId !== instance || s.authSessionId !== session)
1806
+ return { ...receipt(), status: "superseded" };
1807
+ if (s.credentialState === "signed_out" && !s.loginAttempt)
1808
+ return { ...receipt(), status: "already_signed_out" };
1809
+ const r = await this.cas(
1810
+ s,
1811
+ {
1812
+ credentialState: "signed_out",
1813
+ authSessionId: null,
1814
+ principal: null,
1815
+ tokenSet: null,
1816
+ refreshOperation: null,
1817
+ loginAttempt: null,
1818
+ lastLoginAttemptId: null,
1819
+ verifiedIdentity: null,
1820
+ reason: null
1821
+ },
1822
+ logoutOperationId,
1823
+ signal
1824
+ );
1825
+ if (r.status === "committed") {
1826
+ await this.notify(r.snapshot);
1827
+ return {
1828
+ ...receipt(),
1829
+ revision: r.snapshot.revision,
1830
+ status: "committed",
1831
+ revocation: s.tokenSet ? this.revoke(s.tokenSet, void 0, s) : Promise.resolve("unsupported")
1832
+ };
1833
+ }
1834
+ if (performance.now() - started >= 2e3) throw new Error("storage_busy");
1835
+ await pause();
1836
+ s = await this.read(signal);
1837
+ }
1838
+ }
1839
+ };
1840
+
1121
1841
  // src/core/client.ts
1122
1842
  init_types();
1123
1843
 
@@ -1557,33 +2277,35 @@ async function revokeToken(meta, token, signal, fetchImpl = globalThis.fetch) {
1557
2277
  }
1558
2278
  async function postToken(endpoint, data, signal, fetchImpl = globalThis.fetch) {
1559
2279
  const ctl = withTimeout(authTimeoutMs, signal);
1560
- let resp;
1561
2280
  try {
1562
- resp = await fetchImpl(endpoint, {
1563
- method: "POST",
1564
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
1565
- body: data,
1566
- signal: ctl.signal
1567
- });
1568
- } catch (e) {
1569
- throw new Error(`token request: ${e instanceof Error ? e.message : String(e)}`);
1570
- } finally {
1571
- ctl.dispose();
1572
- }
1573
- if (!resp.ok) {
1574
- let errBody = {};
2281
+ let resp;
1575
2282
  try {
1576
- errBody = await resp.json();
1577
- } catch {
2283
+ resp = await fetchImpl(endpoint, {
2284
+ method: "POST",
2285
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
2286
+ body: data,
2287
+ signal: ctl.signal
2288
+ });
2289
+ } catch (e) {
2290
+ throw new Error(`token request: ${e instanceof Error ? e.message : String(e)}`);
1578
2291
  }
1579
- const oauthError = typeof errBody.error === "string" ? errBody.error : "";
1580
- const errorDescription = typeof errBody.error_description === "string" ? errBody.error_description : "";
1581
- throw new OAuthTokenEndpointError(resp.status, oauthError, errorDescription);
1582
- }
1583
- try {
1584
- return await resp.json();
1585
- } catch (e) {
1586
- throw new Error(`token: decode: ${e instanceof Error ? e.message : String(e)}`);
2292
+ if (!resp.ok) {
2293
+ let errBody = {};
2294
+ try {
2295
+ errBody = await resp.json();
2296
+ } catch {
2297
+ }
2298
+ const oauthError = typeof errBody.error === "string" ? errBody.error : "";
2299
+ const errorDescription = typeof errBody.error_description === "string" ? errBody.error_description : "";
2300
+ throw new OAuthTokenEndpointError(resp.status, oauthError, errorDescription);
2301
+ }
2302
+ try {
2303
+ return await resp.json();
2304
+ } catch (e) {
2305
+ throw new Error(`token: decode: ${e instanceof Error ? e.message : String(e)}`);
2306
+ }
2307
+ } finally {
2308
+ ctl.dispose();
1587
2309
  }
1588
2310
  }
1589
2311
  function newTokenSet(resp, clientID, serverURL) {
@@ -2066,10 +2788,31 @@ function parseHTTPErrorWithHeader(statusCode, body, header) {
2066
2788
  } else if (typeof top.message === "string") {
2067
2789
  message = top.message;
2068
2790
  }
2791
+ const contractSource = errObj && typeof errObj === "object" ? errObj : top;
2069
2792
  if (typeof top.errorCode === "string") errorCode = top.errorCode;
2793
+ else if (typeof contractSource.errorCode === "string") errorCode = contractSource.errorCode;
2070
2794
  if (top.windowKind === "FIVE_HOUR" || top.windowKind === "WEEKLY") windowKind = top.windowKind;
2071
2795
  if (typeof top.windowResetAt === "string") windowResetAt = top.windowResetAt;
2072
2796
  if (typeof top.windowOverridable === "boolean") windowOverridable = top.windowOverridable;
2797
+ const disposition = contractSource.requestDisposition === "not_accepted" || contractSource.requestDisposition === "accepted" || contractSource.requestDisposition === "unknown" ? contractSource.requestDisposition : void 0;
2798
+ const domains = ["user_auth", "caller_credentials", "account_quota", "account_permission", "provider", "gateway", "transport", "stream_ticket", "protocol"];
2799
+ return new exports.HTTPError(statusCode, {
2800
+ type,
2801
+ message,
2802
+ retryAfter,
2803
+ body: bodyStr,
2804
+ errorCode,
2805
+ windowKind,
2806
+ windowResetAt,
2807
+ windowOverridable,
2808
+ errorContractVersion: contractSource.errorContractVersion === 1 ? 1 : void 0,
2809
+ faultDomain: typeof contractSource.faultDomain === "string" && domains.includes(contractSource.faultDomain) ? contractSource.faultDomain : void 0,
2810
+ requestDisposition: disposition,
2811
+ transportRequestId: typeof contractSource.transportRequestId === "string" ? contractSource.transportRequestId : null,
2812
+ consumeRequestId: typeof contractSource.consumeRequestId === "string" ? contractSource.consumeRequestId : null,
2813
+ providerRequestId: typeof contractSource.providerRequestId === "string" ? contractSource.providerRequestId : null,
2814
+ retryable: contractSource.retryable === true
2815
+ });
2073
2816
  }
2074
2817
  } catch {
2075
2818
  }
@@ -2131,7 +2874,19 @@ function parseStreamError(data) {
2131
2874
  if (code === "" && errObj.type) code = errObj.type;
2132
2875
  }
2133
2876
  }
2134
- return new exports.StreamError({ code, stage, message, rawError, retryable });
2877
+ return new exports.StreamError({
2878
+ code,
2879
+ stage,
2880
+ message,
2881
+ rawError,
2882
+ retryable,
2883
+ errorContractVersion: payload.errorContractVersion,
2884
+ faultDomain: payload.faultDomain,
2885
+ requestDisposition: payload.requestDisposition,
2886
+ transportRequestId: payload.transportRequestId,
2887
+ consumeRequestId: payload.consumeRequestId,
2888
+ providerRequestId: payload.providerRequestId
2889
+ });
2135
2890
  }
2136
2891
  function isOrderSuccess(status) {
2137
2892
  switch (status) {
@@ -2365,6 +3120,13 @@ var Client = class _Client {
2365
3120
  tokens = null;
2366
3121
  /** token 持久化 */
2367
3122
  store;
3123
+ credentialMode;
3124
+ versionedCredentialStore;
3125
+ accessTokenProvider;
3126
+ beforeCredentialInstall;
3127
+ credentialAuthority;
3128
+ credentialRequestOwner;
3129
+ lifecycle = null;
2368
3130
  /** fetch 实现 (默认 globalThis.fetch) */
2369
3131
  fetchImpl;
2370
3132
  /** 互斥锁 (TS 用 Promise chain 替代 sync.Mutex) */
@@ -2395,6 +3157,7 @@ var Client = class _Client {
2395
3157
  /** V29 系数缓存 (TTL 8s, listCoefficients 内部用) */
2396
3158
  coefCacheData = null;
2397
3159
  coefCacheTimeMs = 0;
3160
+ credentialOwnerKey = null;
2398
3161
  /** 串行化锁 (替代 Go sync.Mutex) */
2399
3162
  coefMu = Promise.resolve();
2400
3163
  constructor(cfg = {}) {
@@ -2405,8 +3168,92 @@ var Client = class _Client {
2405
3168
  this.oauthMetadataProfile = cfg.oauthMetadataProfile ?? "desktop";
2406
3169
  this.browserRefreshMode = cfg.browserRefreshMode ?? "direct";
2407
3170
  this.refreshProxyURL = cfg.refreshProxyURL ?? null;
2408
- this.store = cfg.store ?? defaultTokenStore();
3171
+ this.credentialMode = cfg.credentialMode ?? "legacy";
3172
+ this.credentialAuthority = new URL(this.serverURL).origin;
3173
+ if (this.credentialMode === "versioned" && !cfg.versionedCredentialStore) {
3174
+ throw new Error("versionedCredentialStore is required for credentialMode=versioned");
3175
+ }
3176
+ if (cfg.credentialRequestOwner && this.credentialMode !== "versioned") {
3177
+ throw new Error("credentialRequestOwner requires credentialMode=versioned");
3178
+ }
3179
+ if (this.credentialMode === "legacy" && cfg.versionedCredentialStore) {
3180
+ throw new Error("credentialMode=versioned is required when versionedCredentialStore is provided");
3181
+ }
3182
+ if (this.credentialMode === "external" && cfg.versionedCredentialStore) {
3183
+ throw new Error("versionedCredentialStore is incompatible with credentialMode=external");
3184
+ }
3185
+ if (this.credentialMode !== "external" && cfg.accessTokenProvider) {
3186
+ throw new Error("accessTokenProvider requires credentialMode=external");
3187
+ }
3188
+ if (this.credentialMode === "external" && !cfg.accessTokenProvider) {
3189
+ throw new Error("accessTokenProvider is required for credentialMode=external");
3190
+ }
3191
+ if (this.credentialMode !== "versioned" && cfg.beforeCredentialInstall) {
3192
+ throw new Error("beforeCredentialInstall requires credentialMode=versioned");
3193
+ }
3194
+ if (this.credentialMode !== "legacy") {
3195
+ const authority = new URL(this.serverURL).origin;
3196
+ for (const [name, override] of [["apiBaseURL", this.apiBaseURL], ["complianceBaseURL", this.complianceBaseURL]]) {
3197
+ if (override && new URL(override).origin !== authority) {
3198
+ throw new Error(`${name} must use the credential authority ${authority}`);
3199
+ }
3200
+ }
3201
+ }
3202
+ this.accessTokenProvider = cfg.accessTokenProvider;
3203
+ this.beforeCredentialInstall = cfg.beforeCredentialInstall;
3204
+ this.credentialRequestOwner = cfg.credentialRequestOwner ? {
3205
+ storeInstanceId: cfg.credentialRequestOwner.storeInstanceId,
3206
+ authSessionId: cfg.credentialRequestOwner.authSessionId,
3207
+ principal: cfg.credentialRequestOwner.principal ? {
3208
+ issuer: cfg.credentialRequestOwner.principal.issuer,
3209
+ subject: cfg.credentialRequestOwner.principal.subject,
3210
+ organizationId: cfg.credentialRequestOwner.principal.organizationId
3211
+ } : null
3212
+ } : null;
2409
3213
  this.fetchImpl = cfg.fetchImpl ?? globalThis.fetch.bind(globalThis);
3214
+ this.versionedCredentialStore = cfg.versionedCredentialStore ?? null;
3215
+ if (this.versionedCredentialStore) this.lifecycle = new CredentialLifecycle(this.versionedCredentialStore, {
3216
+ serverURL: this.serverURL,
3217
+ fetch: this.fetchImpl,
3218
+ metadata: (signal) => discoverWithProfile(this.serverURL, "desktop", signal, this.fetchImpl),
3219
+ profile: async (tokens, signal) => {
3220
+ const gatewayRoot = this.serverURL.replace(/\/api\/v4$/, "");
3221
+ const profileURL = `${gatewayRoot}/api/oauth/profile`;
3222
+ this.assertCredentialURL(profileURL);
3223
+ const response = await this.fetchImpl(profileURL, { headers: { Authorization: `Bearer ${tokens.access_token}` }, signal });
3224
+ if (!response.ok) {
3225
+ const bodyBytes = response.body ? await readLimited(response.body, maxErrorBodySize) : new Uint8Array();
3226
+ const failure = parseHTTPErrorWithHeader(response.status, bodyBytes, response.headers);
3227
+ const contract = readGatewayErrorContract(failure);
3228
+ const code = contract?.errorCode ?? null;
3229
+ if (code === "INVALID_SCOPE") throw new Error("invalid_scope");
3230
+ if (code === "AUTH_CONTRACT_UNSUPPORTED") throw new Error("auth_contract_unsupported");
3231
+ if (code === "ACCOUNT_NOT_FOUND") throw new Error("account_permission");
3232
+ throw new Error("identity_unavailable");
3233
+ }
3234
+ const body = await response.json();
3235
+ const subject = body.account?.uuid;
3236
+ if (typeof subject !== "string" || !subject) throw new Error("invalid_response");
3237
+ const account = body.account;
3238
+ const organization = body.organization;
3239
+ return {
3240
+ subject,
3241
+ organizationId: organization?.uuid || null,
3242
+ ...typeof account.display_name === "string" ? { displayName: account.display_name } : {},
3243
+ ...[account.avatar_url, account.picture, account.image, body.avatar_url, body.picture, body.image].find((value) => typeof value === "string") !== void 0 ? { avatarUrl: [account.avatar_url, account.picture, account.image, body.avatar_url, body.picture, body.image].find((value) => typeof value === "string") } : {},
3244
+ ...typeof account.email === "string" ? { email: account.email } : {},
3245
+ ...typeof account.image_url === "string" ? { imageUrl: account.image_url } : {},
3246
+ ...typeof account.created_at === "string" ? { accountCreatedAt: account.created_at } : {},
3247
+ ...typeof account.requires_phone_binding === "boolean" ? { requiresPhoneBinding: account.requires_phone_binding } : {},
3248
+ ...typeof organization?.has_extra_usage_enabled === "boolean" ? { hasExtraUsageEnabled: organization.has_extra_usage_enabled } : {},
3249
+ ...typeof organization?.billing_type === "string" ? { billingType: organization.billing_type } : {},
3250
+ ...typeof organization?.subscription_created_at === "string" ? { subscriptionCreatedAt: organization.subscription_created_at } : {},
3251
+ ...typeof organization?.rate_limit_tier === "string" ? { rateLimitTier: organization.rate_limit_tier } : {},
3252
+ ...typeof organization?.name === "string" ? { organizationName: organization.name } : {}
3253
+ };
3254
+ }
3255
+ });
3256
+ this.store = cfg.store ?? defaultTokenStore();
2410
3257
  this.retryPolicy = effectivePolicy(cfg.retryPolicy ?? null);
2411
3258
  }
2412
3259
  /**
@@ -2415,6 +3262,11 @@ var Client = class _Client {
2415
3262
  */
2416
3263
  static async create(cfg = {}) {
2417
3264
  const c = new _Client(cfg);
3265
+ if (c.credentialMode === "external") return c;
3266
+ if (c.credentialMode === "versioned") {
3267
+ await c.reconcileCredentials();
3268
+ return c;
3269
+ }
2418
3270
  try {
2419
3271
  const tokens = await c.store.load();
2420
3272
  if (tokens) {
@@ -2428,6 +3280,63 @@ var Client = class _Client {
2428
3280
  }
2429
3281
  return c;
2430
3282
  }
3283
+ /** Read the durable authority. Available only in explicit versioned mode. */
3284
+ async getCredentialSnapshot(signal) {
3285
+ if (!this.versionedCredentialStore) {
3286
+ throw new Error("getCredentialSnapshot requires credentialMode=versioned");
3287
+ }
3288
+ return this.versionedCredentialStore.readSnapshot(signal);
3289
+ }
3290
+ /** Reconcile memory from durable state; storage errors never clear confirmed memory. */
3291
+ async reconcileCredentials(signal) {
3292
+ const snapshot = await this.lifecycle.reconcile(signal);
3293
+ this.adoptCredentialOwner(snapshot);
3294
+ this.tokens = snapshot.credentialState === "ready" ? snapshot.tokenSet : null;
3295
+ return snapshot;
3296
+ }
3297
+ ownerKey(snapshot) {
3298
+ return `${snapshot.storeInstanceId}\0${snapshot.authSessionId ?? ""}\0${snapshot.principal?.issuer ?? ""}\0${snapshot.principal?.subject ?? ""}\0${snapshot.principal?.organizationId ?? ""}`;
3299
+ }
3300
+ adoptCredentialOwner(snapshot) {
3301
+ const next = this.ownerKey(snapshot);
3302
+ if (this.credentialOwnerKey !== null && this.credentialOwnerKey !== next) {
3303
+ this.modelCache = [];
3304
+ this.modelCacheTimeMs = 0;
3305
+ this.coefCacheData = null;
3306
+ this.coefCacheTimeMs = 0;
3307
+ }
3308
+ this.credentialOwnerKey = next;
3309
+ }
3310
+ async ensureCredential(signal) {
3311
+ if (!this.lifecycle) return this.ensureToken(signal);
3312
+ const token = await this.lifecycle.ensure(signal, void 0, this.credentialRequestOwner ?? void 0);
3313
+ const snapshot = await this.lifecycle.read(signal);
3314
+ this.adoptCredentialOwner(snapshot);
3315
+ this.tokens = snapshot.credentialState === "ready" ? snapshot.tokenSet : null;
3316
+ return token;
3317
+ }
3318
+ subscribeCredentialState(listener) {
3319
+ if (!this.lifecycle) throw new Error("subscribeCredentialState requires credentialMode=versioned");
3320
+ return this.lifecycle.subscribe(listener);
3321
+ }
3322
+ async retryCredentialIdentity(signal) {
3323
+ const snapshot = await this.getCredentialSnapshot(signal);
3324
+ if (!snapshot.authSessionId) throw new Error("not authorized");
3325
+ const ready = await this.lifecycle.bindIdentity(snapshot.authSessionId, signal);
3326
+ this.adoptCredentialOwner(ready);
3327
+ this.tokens = ready.credentialState === "ready" ? ready.tokenSet : null;
3328
+ return ready;
3329
+ }
3330
+ async logoutCredential(signal, expected) {
3331
+ if (!this.lifecycle) throw new Error("logoutCredential requires credentialMode=versioned");
3332
+ const result = await this.lifecycle.logout(signal, expected);
3333
+ if (result.status === "committed" || result.status === "already_signed_out") {
3334
+ const current = await this.lifecycle.read(signal);
3335
+ this.adoptCredentialOwner(current);
3336
+ this.tokens = null;
3337
+ }
3338
+ return result;
3339
+ }
2431
3340
  // ===========================================================================
2432
3341
  // 授权生命周期
2433
3342
  // ===========================================================================
@@ -2478,6 +3387,44 @@ var Client = class _Client {
2478
3387
  return this.loginInternal(appName, scopes, { handler, ...opts }, signal);
2479
3388
  }
2480
3389
  async loginInternal(appName, scopes, opts, signal) {
3390
+ if (this.lifecycle) {
3391
+ const attempt = await this.lifecycle.reserveLogin(signal);
3392
+ let installHookRejected = false;
3393
+ try {
3394
+ const registration = await register(attempt.metadata, appName, signal, this.fetchImpl);
3395
+ await this.lifecycle.assertLogin(attempt.attemptId, signal);
3396
+ const authorization = await authorize(attempt.metadata, registration.client_id, scopes, { ...opts, handler: opts?.handler ?? void 0, signal });
3397
+ await this.lifecycle.assertLogin(attempt.attemptId, signal);
3398
+ const response = await exchangeCode(attempt.metadata, registration.client_id, authorization.result.code, authorization.result.redirectURI, authorization.verifier, signal, this.fetchImpl);
3399
+ if (!Number.isFinite(response.expires_in) || response.expires_in <= 0) throw new Error("invalid_response");
3400
+ await this.lifecycle.assertLogin(attempt.attemptId, signal);
3401
+ try {
3402
+ await this.beforeCredentialInstall?.({
3403
+ accessToken: response.access_token,
3404
+ attemptId: attempt.attemptId,
3405
+ serverURL: this.serverURL,
3406
+ clientId: registration.client_id
3407
+ }, signal);
3408
+ } catch (error) {
3409
+ installHookRejected = true;
3410
+ throw error;
3411
+ }
3412
+ await this.lifecycle.assertLogin(attempt.attemptId, signal);
3413
+ const ready = await this.lifecycle.installLogin(attempt.attemptId, newTokenSet(response, registration.client_id, this.serverURL), signal);
3414
+ const current = await this.getCredentialSnapshot(signal);
3415
+ if (current.revision !== ready.revision || current.authSessionId !== ready.authSessionId) throw new Error("superseded");
3416
+ this.tokens = ready.tokenSet;
3417
+ opts?.handler?.({ type: EventComplete, attemptId: attempt.attemptId });
3418
+ } catch (error) {
3419
+ const current = await this.getCredentialSnapshot();
3420
+ if (current.loginAttempt?.attemptId === attempt.attemptId || current.lastLoginAttemptId === attempt.attemptId) {
3421
+ opts?.handler?.(installHookRejected ? { type: EventError, attemptId: attempt.attemptId, err_code: "credential_install_rejected", error: "credential_install_rejected" } : { type: EventError, attemptId: attempt.attemptId, error: "credential_login_failed" });
3422
+ }
3423
+ await this.lifecycle.cancelLogin(attempt.attemptId);
3424
+ throw error;
3425
+ }
3426
+ return;
3427
+ }
2481
3428
  const handler = opts?.handler ?? void 0;
2482
3429
  const emit = (e) => {
2483
3430
  if (handler) handler(e);
@@ -2586,6 +3533,11 @@ var Client = class _Client {
2586
3533
  }
2587
3534
  /** 吊销 token 并清除本地存储 */
2588
3535
  async logout(signal) {
3536
+ if (this.lifecycle) {
3537
+ await this.logoutCredential(signal);
3538
+ await this.reconcileCredentials(signal);
3539
+ return;
3540
+ }
2589
3541
  const tokens = this.tokens;
2590
3542
  let meta = this.meta;
2591
3543
  this.tokens = null;
@@ -2623,6 +3575,13 @@ var Client = class _Client {
2623
3575
  * 避免应用启动期 "login + 多个 API 调用" 并发场景下 4+ 条 "not authorized" 误报.
2624
3576
  */
2625
3577
  async ensureToken(signal) {
3578
+ if (this.credentialMode === "external") {
3579
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
3580
+ const token = await this.accessTokenProvider(signal);
3581
+ if (typeof token !== "string" || token.length === 0) throw new Error("external access token unavailable");
3582
+ return token;
3583
+ }
3584
+ if (this.lifecycle) return this.lifecycle.ensure(signal, void 0, this.credentialRequestOwner ?? void 0);
2626
3585
  let tokens = this.tokens;
2627
3586
  const ready = this.tokenReady.promise;
2628
3587
  const inFlight = this.loginInFlight;
@@ -2670,7 +3629,14 @@ var Client = class _Client {
2670
3629
  );
2671
3630
  }
2672
3631
  /** 强制刷新 token (用于 401 重试) */
2673
- async forceRefresh(signal) {
3632
+ async forceRefresh(signal, rejectedToken) {
3633
+ if (this.credentialMode === "external") throw new Error("external credentials cannot be refreshed by the SDK");
3634
+ if (this.lifecycle) {
3635
+ await this.lifecycle.forceRefresh(signal, rejectedToken, this.credentialRequestOwner ?? void 0);
3636
+ const snapshot = await this.lifecycle.read(signal);
3637
+ this.tokens = snapshot.credentialState === "ready" ? snapshot.tokenSet : null;
3638
+ return;
3639
+ }
2674
3640
  return this.withMu(
2675
3641
  () => this.storeWithLock(async () => {
2676
3642
  await this.syncFromDisk();
@@ -2865,6 +3831,7 @@ var Client = class _Client {
2865
3831
  */
2866
3832
  async listModelsWithStatus(signal, opts) {
2867
3833
  const includeLocked = opts?.includeLocked === true;
3834
+ const requestOwner = this.lifecycle ? this.ownerKey(await this.getCredentialSnapshot(signal)) : null;
2868
3835
  const path = includeLocked ? "/managed-models?picker=1" : "/managed-models";
2869
3836
  const { result, headers } = await this.doJSONFull(
2870
3837
  "GET",
@@ -2873,6 +3840,11 @@ var Client = class _Client {
2873
3840
  signal
2874
3841
  );
2875
3842
  const normalized = normalizeInputModalities(result.data);
3843
+ if (requestOwner !== null) {
3844
+ const current = await this.getCredentialSnapshot(signal);
3845
+ if (this.ownerKey(current) !== requestOwner) throw new Error("superseded");
3846
+ this.adoptCredentialOwner(current);
3847
+ }
2876
3848
  if (!includeLocked) {
2877
3849
  this.modelCache = normalized;
2878
3850
  this.modelCacheTimeMs = Date.now();
@@ -3292,12 +4264,11 @@ var Client = class _Client {
3292
4264
  throw classifyTransport("POST " + endpoint, url, e);
3293
4265
  }
3294
4266
  if (resp.status === 401 && !retried) {
4267
+ const bodyBytes = resp.body ? await readLimited(resp.body, maxErrorBodySize) : new Uint8Array();
4268
+ const authError = parseHTTPErrorWithHeader(resp.status, bodyBytes, resp.headers);
4269
+ if (!isUserAccessTokenRejected(authError)) throw authError;
3295
4270
  try {
3296
- await resp.body?.cancel();
3297
- } catch {
3298
- }
3299
- try {
3300
- await this.forceRefresh(signal);
4271
+ await this.forceRefresh(signal, token);
3301
4272
  } catch (refreshErr) {
3302
4273
  throw new Error(
3303
4274
  `stream: unauthorized and refresh failed: ${refreshErr instanceof Error ? refreshErr.message : String(refreshErr)}`
@@ -3363,12 +4334,11 @@ var Client = class _Client {
3363
4334
  throw classifyTransport("POST " + endpoint, url, e);
3364
4335
  }
3365
4336
  if (resp.status === 401 && !retried) {
4337
+ const bodyBytes = resp.body ? await readLimited(resp.body, maxErrorBodySize) : new Uint8Array();
4338
+ const authError = parseHTTPErrorWithHeader(resp.status, bodyBytes, resp.headers);
4339
+ if (!isUserAccessTokenRejected(authError)) throw authError;
3366
4340
  try {
3367
- await resp.body?.cancel();
3368
- } catch {
3369
- }
3370
- try {
3371
- await this.forceRefresh(signal);
4341
+ await this.forceRefresh(signal, token);
3372
4342
  } catch (refreshErr) {
3373
4343
  throw new Error(
3374
4344
  `messages stream: unauthorized and refresh failed: ${refreshErr instanceof Error ? refreshErr.message : String(refreshErr)}`
@@ -3458,7 +4428,9 @@ var Client = class _Client {
3458
4428
  if (!base.endsWith("/api/v4")) {
3459
4429
  base += "/api/v4";
3460
4430
  }
3461
- return base + path;
4431
+ const url = base + path;
4432
+ this.assertCredentialURL(url);
4433
+ return url;
3462
4434
  }
3463
4435
  /**
3464
4436
  * Compliance API URL 拼接。
@@ -3471,7 +4443,14 @@ var Client = class _Client {
3471
4443
  */
3472
4444
  complianceURL(path) {
3473
4445
  const base = this.complianceBaseURL ?? this.serverURL + "/admin-api";
3474
- return base + path;
4446
+ const url = base + path;
4447
+ this.assertCredentialURL(url);
4448
+ return url;
4449
+ }
4450
+ assertCredentialURL(url) {
4451
+ if (this.credentialMode !== "legacy" && new URL(url).origin !== this.credentialAuthority) {
4452
+ throw new Error("credential request authority changed");
4453
+ }
3475
4454
  }
3476
4455
  /** GET/POST/... 通用 JSON 调用 (返回 result 已 typed) */
3477
4456
  async doJSON(method, path, body, signal) {
@@ -3500,12 +4479,11 @@ var Client = class _Client {
3500
4479
  ctl.signal
3501
4480
  );
3502
4481
  if (resp.status === 401 && !retried) {
4482
+ const bodyBytes = resp.body ? await readLimited(resp.body, maxErrorBodySize) : new Uint8Array();
4483
+ const authError = parseHTTPErrorWithHeader(resp.status, bodyBytes, resp.headers);
4484
+ if (!isUserAccessTokenRejected(authError)) throw authError;
3503
4485
  try {
3504
- await resp.body?.cancel();
3505
- } catch {
3506
- }
3507
- try {
3508
- await this.forceRefresh(ctl.signal);
4486
+ await this.forceRefresh(ctl.signal, token);
3509
4487
  } catch (refreshErr) {
3510
4488
  throw new Error(
3511
4489
  `unauthorized and refresh failed: ${refreshErr instanceof Error ? refreshErr.message : String(refreshErr)}`
@@ -3570,12 +4548,11 @@ var Client = class _Client {
3570
4548
  ctl.signal
3571
4549
  );
3572
4550
  if (resp.status === 401 && !retried) {
4551
+ const bodyBytes = resp.body ? await readLimited(resp.body, maxErrorBodySize) : new Uint8Array();
4552
+ const authError = parseHTTPErrorWithHeader(resp.status, bodyBytes, resp.headers);
4553
+ if (!isUserAccessTokenRejected(authError)) throw authError;
3573
4554
  try {
3574
- await resp.body?.cancel();
3575
- } catch {
3576
- }
3577
- try {
3578
- await this.forceRefresh(ctl.signal);
4555
+ await this.forceRefresh(ctl.signal, token);
3579
4556
  } catch (refreshErr) {
3580
4557
  throw new Error(
3581
4558
  `unauthorized and refresh failed: ${refreshErr instanceof Error ? refreshErr.message : String(refreshErr)}`
@@ -3650,6 +4627,7 @@ var Client = class _Client {
3650
4627
  * 6 处原始 fetch() 全部走此 helper
3651
4628
  */
3652
4629
  async doRequest(req, signal) {
4630
+ if (new Headers(req.headers).has("Authorization")) this.assertCredentialURL(req.url);
3653
4631
  try {
3654
4632
  return await this.fetchImpl(req.url, {
3655
4633
  method: req.method,
@@ -4647,12 +5625,11 @@ async function uploadSkillInternal(c, zipData, scope, intent, retried, signal) {
4647
5625
  throw classifyTransport("POST /skill-store/upload", url, e);
4648
5626
  }
4649
5627
  if (resp.status === 401 && !retried) {
5628
+ const bodyBytes = resp.body ? await readLimited(resp.body, maxErrorBodySize) : new Uint8Array();
5629
+ const authError = parseHTTPErrorWithHeader(resp.status, bodyBytes, resp.headers);
5630
+ if (!isUserAccessTokenRejected(authError)) throw authError;
4650
5631
  try {
4651
- await resp.body?.cancel();
4652
- } catch {
4653
- }
4654
- try {
4655
- await c.forceRefresh(ctl.signal);
5632
+ await c.forceRefresh(ctl.signal, token);
4656
5633
  } catch (refreshErr) {
4657
5634
  throw new Error(
4658
5635
  `upload: unauthorized and refresh failed: ${refreshErr instanceof Error ? refreshErr.message : String(refreshErr)}`
@@ -4823,9 +5800,7 @@ Client.prototype.updateNotificationPreference = async function(typeCode, pref, s
4823
5800
 
4824
5801
  // src/notifications/ws.ts
4825
5802
  Client.prototype.connect = async function(cfg, signal) {
4826
- if (this.ws) {
4827
- await this.disconnect();
4828
- }
5803
+ const oldDisconnect = this.ws ? this.disconnect() : Promise.resolve();
4829
5804
  const noop = () => {
4830
5805
  };
4831
5806
  const filledCfg = {
@@ -4841,21 +5816,31 @@ Client.prototype.connect = async function(cfg, signal) {
4841
5816
  const done = new Promise((r) => {
4842
5817
  resolveDone = r;
4843
5818
  });
4844
- const abort = new AbortController();
5819
+ const abort2 = new AbortController();
4845
5820
  if (signal) {
4846
- if (signal.aborted) abort.abort();
4847
- else signal.addEventListener("abort", () => abort.abort());
5821
+ if (signal.aborted) abort2.abort();
5822
+ else signal.addEventListener("abort", () => abort2.abort());
4848
5823
  }
4849
5824
  const ws = {
4850
5825
  conn: null,
4851
5826
  cfg: filledCfg,
4852
- abort,
5827
+ abort: abort2,
4853
5828
  done,
4854
5829
  doneResolve: resolveDone,
4855
- connected: false
5830
+ connected: false,
5831
+ owner: null
4856
5832
  };
4857
- await wsConnectOnce(this, ws);
4858
5833
  this.ws = ws;
5834
+ try {
5835
+ await oldDisconnect;
5836
+ await assertCurrent(this, ws);
5837
+ await wsConnectOnce(this, ws);
5838
+ } catch (error) {
5839
+ ws.abort.abort();
5840
+ if (this.ws === ws) this.ws = null;
5841
+ ws.doneResolve();
5842
+ throw error;
5843
+ }
4859
5844
  void wsLoop(this, ws);
4860
5845
  };
4861
5846
  Client.prototype.disconnect = async function() {
@@ -4893,15 +5878,47 @@ function getWebSocketCtor() {
4893
5878
  }
4894
5879
  return WSCtor;
4895
5880
  }
5881
+ function sameOwner(a, b) {
5882
+ return a?.storeInstanceId === b?.storeInstanceId && a?.authSessionId === b?.authSessionId && a?.principal?.issuer === b?.principal?.issuer && a?.principal?.subject === b?.principal?.subject && a?.principal?.organizationId === b?.principal?.organizationId;
5883
+ }
5884
+ async function readOwner(c, signal) {
5885
+ if (c.credentialMode !== "versioned") return null;
5886
+ const snapshot = await c.getCredentialSnapshot(signal);
5887
+ return {
5888
+ storeInstanceId: snapshot.storeInstanceId,
5889
+ authSessionId: snapshot.authSessionId,
5890
+ principal: snapshot.principal
5891
+ };
5892
+ }
5893
+ async function assertCurrent(c, ws) {
5894
+ if (ws.abort.signal.aborted || c.ws !== ws) throw new Error("websocket connection superseded");
5895
+ if (ws.owner !== null && !sameOwner(ws.owner, await readOwner(c, ws.abort.signal))) {
5896
+ ws.abort.abort();
5897
+ throw new Error("websocket credential owner changed");
5898
+ }
5899
+ }
4896
5900
  async function wsConnectOnce(c, ws) {
5901
+ const observedOwner = await readOwner(c, ws.abort.signal);
5902
+ if (ws.owner === null) ws.owner = observedOwner;
5903
+ else if (!sameOwner(ws.owner, observedOwner)) throw new Error("websocket credential owner changed");
5904
+ await assertCurrent(c, ws);
4897
5905
  const url = wsURL(c);
4898
5906
  const WSCtor = getWebSocketCtor();
4899
- const ticketResp = await c.doJSON(
4900
- "POST",
4901
- "/ws/stream-ticket",
4902
- null,
4903
- ws.abort.signal
4904
- );
5907
+ const token = await c.ensureToken(ws.abort.signal);
5908
+ await assertCurrent(c, ws);
5909
+ const ticketURL = c.apiURL("/ws/stream-ticket");
5910
+ await assertCurrent(c, ws);
5911
+ const ticketHTTP = await c.doRequest({
5912
+ method: "POST",
5913
+ url: ticketURL,
5914
+ headers: { Authorization: `Bearer ${token}` }
5915
+ }, ws.abort.signal);
5916
+ if (!ticketHTTP.ok) {
5917
+ const body = ticketHTTP.body ? await readLimited(ticketHTTP.body, maxErrorBodySize) : new Uint8Array();
5918
+ throw parseHTTPErrorWithHeader(ticketHTTP.status, body, ticketHTTP.headers);
5919
+ }
5920
+ const ticketResp = await ticketHTTP.json();
5921
+ await assertCurrent(c, ws);
4905
5922
  const ticket = ticketResp.data.ticket;
4906
5923
  const u = new URL(url);
4907
5924
  u.searchParams.set("ticket", ticket);
@@ -4922,11 +5939,28 @@ async function wsConnectOnce(c, ws) {
4922
5939
  reject(new Error("dial: handshake timeout"));
4923
5940
  }
4924
5941
  }, 3e4);
5942
+ const abortHandshake = () => {
5943
+ clearTimeout(handshakeTimer);
5944
+ try {
5945
+ conn.close();
5946
+ } catch {
5947
+ }
5948
+ reject(new Error("websocket connection aborted"));
5949
+ };
5950
+ ws.abort.signal.addEventListener("abort", abortHandshake, { once: true });
4925
5951
  conn.addEventListener("open", () => {
5952
+ if (ws.abort.signal.aborted || c.ws !== ws) {
5953
+ try {
5954
+ conn.close();
5955
+ } catch {
5956
+ }
5957
+ return;
5958
+ }
4926
5959
  opened = true;
4927
5960
  });
4928
5961
  conn.addEventListener("error", (e) => {
4929
5962
  clearTimeout(handshakeTimer);
5963
+ ws.abort.signal.removeEventListener("abort", abortHandshake);
4930
5964
  reject(new Error(`dial: ${e.message ?? "connection error"}`));
4931
5965
  });
4932
5966
  conn.addEventListener("message", (e) => {
@@ -4935,6 +5969,7 @@ async function wsConnectOnce(c, ws) {
4935
5969
  const welcome = JSON.parse(msg);
4936
5970
  if (welcome.type !== "welcome") {
4937
5971
  clearTimeout(handshakeTimer);
5972
+ ws.abort.signal.removeEventListener("abort", abortHandshake);
4938
5973
  try {
4939
5974
  conn.close();
4940
5975
  } catch {
@@ -4942,31 +5977,34 @@ async function wsConnectOnce(c, ws) {
4942
5977
  reject(new Error(`unexpected first message: ${welcome.type}`));
4943
5978
  return;
4944
5979
  }
4945
- clearTimeout(handshakeTimer);
4946
- ws.conn = conn;
4947
- ws.connected = true;
4948
- if (ws.cfg.topics.length > 0) {
4949
- try {
4950
- conn.send(
4951
- JSON.stringify({
4952
- type: "subscribe",
4953
- topics: ws.cfg.topics
4954
- })
4955
- );
4956
- } catch (sendErr) {
4957
- ws.conn = null;
4958
- ws.connected = false;
5980
+ void assertCurrent(c, ws).then(() => {
5981
+ clearTimeout(handshakeTimer);
5982
+ ws.abort.signal.removeEventListener("abort", abortHandshake);
5983
+ ws.conn = conn;
5984
+ ws.connected = true;
5985
+ if (ws.cfg.topics.length > 0) {
4959
5986
  try {
4960
- conn.close();
4961
- } catch {
5987
+ conn.send(
5988
+ JSON.stringify({
5989
+ type: "subscribe",
5990
+ topics: ws.cfg.topics
5991
+ })
5992
+ );
5993
+ } catch (sendErr) {
5994
+ ws.conn = null;
5995
+ ws.connected = false;
5996
+ try {
5997
+ conn.close();
5998
+ } catch {
5999
+ }
6000
+ reject(new Error(`send subscribe: ${sendErr instanceof Error ? sendErr.message : String(sendErr)}`));
6001
+ return;
4962
6002
  }
4963
- reject(new Error(`send subscribe: ${sendErr instanceof Error ? sendErr.message : String(sendErr)}`));
4964
- return;
4965
6003
  }
4966
- }
4967
- ws.cfg.onConnect();
4968
- console.log(`[acosmi-sdk] websocket connected, connId=${welcome.connId ?? ""}`);
4969
- resolve();
6004
+ ws.cfg.onConnect();
6005
+ console.log(`[acosmi-sdk] websocket connected, connId=${welcome.connId ?? ""}`);
6006
+ resolve();
6007
+ }).catch(reject);
4970
6008
  } catch (parseErr) {
4971
6009
  clearTimeout(handshakeTimer);
4972
6010
  try {
@@ -4981,7 +6019,7 @@ async function wsConnectOnce(c, ws) {
4981
6019
  async function wsLoop(c, ws) {
4982
6020
  try {
4983
6021
  while (true) {
4984
- await wsReadLoop(ws);
6022
+ await wsReadLoop(c, ws);
4985
6023
  if (ws.abort.signal.aborted) return;
4986
6024
  if (ws.conn) {
4987
6025
  try {
@@ -5012,7 +6050,7 @@ async function wsLoop(c, ws) {
5012
6050
  ws.doneResolve();
5013
6051
  }
5014
6052
  }
5015
- async function wsReadLoop(ws) {
6053
+ async function wsReadLoop(c, ws) {
5016
6054
  const conn = ws.conn;
5017
6055
  if (!conn) return;
5018
6056
  return new Promise((resolve) => {
@@ -5020,10 +6058,17 @@ async function wsReadLoop(ws) {
5020
6058
  try {
5021
6059
  const data = e.data;
5022
6060
  const event = JSON.parse(data);
5023
- try {
5024
- ws.cfg.onEvent(event);
5025
- } catch {
5026
- }
6061
+ void assertCurrent(c, ws).then(() => {
6062
+ try {
6063
+ ws.cfg.onEvent(event);
6064
+ } catch {
6065
+ }
6066
+ }).catch(() => {
6067
+ try {
6068
+ conn.close();
6069
+ } catch {
6070
+ }
6071
+ });
5027
6072
  } catch {
5028
6073
  }
5029
6074
  };
@@ -5236,6 +6281,7 @@ function isTerminalRemoteEvent(ev) {
5236
6281
  }
5237
6282
 
5238
6283
  // src/agent-runs/client.ts
6284
+ init_errors();
5239
6285
  var agentRunsByClient = /* @__PURE__ */ new WeakMap();
5240
6286
  Object.defineProperty(Client.prototype, "agentRuns", {
5241
6287
  configurable: true,
@@ -5615,11 +6661,10 @@ var AgentRunsClient = class {
5615
6661
  }
5616
6662
  const resp = await this.client.doRequest({ method, url, headers, body: bodyStr }, signal);
5617
6663
  if (resp.status === 401 && opts.retryOn401 && !retried) {
5618
- try {
5619
- await resp.body?.cancel();
5620
- } catch {
5621
- }
5622
- await this.client.forceRefresh(signal);
6664
+ const bodyBytes = resp.body ? await readLimited(resp.body, maxErrorBodySize) : new Uint8Array();
6665
+ const authError = parseHTTPErrorWithHeader(resp.status, bodyBytes, resp.headers);
6666
+ if (!isUserAccessTokenRejected(authError)) throw authError;
6667
+ await this.client.forceRefresh(signal, token);
5623
6668
  return this.requestRawInner(method, path, body, signal, opts, true);
5624
6669
  }
5625
6670
  if (resp.status < 200 || resp.status >= 300) {
@@ -6246,6 +7291,7 @@ function isComplianceBusinessError(err) {
6246
7291
  }
6247
7292
 
6248
7293
  // src/compliance/client.ts
7294
+ init_errors();
6249
7295
  var cache = /* @__PURE__ */ new WeakMap();
6250
7296
  Object.defineProperty(Client.prototype, "compliance", {
6251
7297
  configurable: true,
@@ -7062,11 +8108,10 @@ var ComplianceClient = class {
7062
8108
  }
7063
8109
  const resp = await this.client.doRequest({ method, url, headers, body: bodyStr }, signal);
7064
8110
  if (resp.status === 401 && opts.retryOn401 && !retried) {
7065
- try {
7066
- await resp.body?.cancel();
7067
- } catch {
7068
- }
7069
- await this.client.forceRefresh(signal);
8111
+ const bodyBytes = resp.body ? await readLimited(resp.body, maxErrorBodySize) : new Uint8Array();
8112
+ const authError = parseHTTPErrorWithHeader(resp.status, bodyBytes, resp.headers);
8113
+ if (!isUserAccessTokenRejected(authError)) throw authError;
8114
+ await this.client.forceRefresh(signal, token);
7070
8115
  return this.executeJsonInner(method, path, body, signal, opts, true);
7071
8116
  }
7072
8117
  if (resp.status < 200 || resp.status >= 300) {
@@ -7987,6 +9032,7 @@ exports.isRegion = isRegion;
7987
9032
  exports.isSSECommentLine = isSSECommentLine;
7988
9033
  exports.isSSLError = isSSLError;
7989
9034
  exports.isTerminalRemoteEvent = isTerminalRemoteEvent;
9035
+ exports.isUserAccessTokenRejected = isUserAccessTokenRejected;
7990
9036
  exports.isValidTokenSet = isValidTokenSet;
7991
9037
  exports.isWindowLimitError = isWindowLimitError;
7992
9038
  exports.isWindowLimitStreamError = isWindowLimitStreamError;
@@ -8004,6 +9050,7 @@ exports.parseNotificationEvent = parseNotificationEvent;
8004
9050
  exports.parseRemoteControlEvent = parseRemoteControlEvent;
8005
9051
  exports.parseSettlement = parseSettlement;
8006
9052
  exports.parseSourcesEvent = parseSourcesEvent;
9053
+ exports.readGatewayErrorContract = readGatewayErrorContract;
8007
9054
  exports.refreshToken = refreshToken;
8008
9055
  exports.register = register;
8009
9056
  exports.registerWebOAuthClient = registerWebOAuthClient;