@acosmi/sdk-ts 2.19.0 → 2.19.1
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/CHANGELOG.md +10 -0
- package/README.md +2 -1
- package/dist/browser/index.mjs +1189 -155
- package/dist/browser/index.mjs.map +1 -1
- package/dist/index.mjs +1189 -155
- package/dist/index.mjs.map +1 -1
- package/dist/node/adapters/anthropic.cjs.map +1 -1
- package/dist/node/adapters/anthropic.mjs.map +1 -1
- package/dist/node/adapters/openai.cjs +110 -45
- package/dist/node/adapters/openai.cjs.map +1 -1
- package/dist/node/adapters/openai.d.cts +1 -1
- package/dist/node/adapters/openai.d.ts +1 -1
- package/dist/node/adapters/openai.mjs +110 -45
- package/dist/node/adapters/openai.mjs.map +1 -1
- package/dist/node/index.cjs +1190 -154
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.d.cts +344 -149
- package/dist/node/index.d.ts +344 -149
- package/dist/node/index.mjs +1189 -155
- package/dist/node/index.mjs.map +1 -1
- package/dist/node/{openai-Cyvi8g6B.d.ts → openai-BbCQOMNY.d.ts} +28 -1
- package/dist/node/{openai-DR_KhEdM.d.cts → openai-CMrvATF_.d.cts} +28 -1
- package/package.json +1 -1
package/dist/node/index.cjs
CHANGED
|
@@ -149,6 +149,55 @@ var init_types = __esm({
|
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
// src/shared/errors.ts
|
|
152
|
+
function nullableID(value) {
|
|
153
|
+
return value === null || typeof value === "string";
|
|
154
|
+
}
|
|
155
|
+
function decodeGatewayErrorContract(value) {
|
|
156
|
+
if (value == null || typeof value !== "object") return null;
|
|
157
|
+
const v = value;
|
|
158
|
+
const version = v.errorContractVersion;
|
|
159
|
+
const domain = v.faultDomain;
|
|
160
|
+
const disposition = v.requestDisposition;
|
|
161
|
+
const errorCode = v.errorCode;
|
|
162
|
+
if (version !== 1 || typeof domain !== "string" || !gatewayFaultDomains.includes(domain)) return null;
|
|
163
|
+
if (typeof errorCode !== "string" || errorCode.length === 0) return null;
|
|
164
|
+
if (disposition !== "not_accepted" && disposition !== "accepted" && disposition !== "unknown") return null;
|
|
165
|
+
if (!nullableID(v.transportRequestId) || !nullableID(v.consumeRequestId) || !nullableID(v.providerRequestId)) return null;
|
|
166
|
+
if (typeof v.retryable !== "boolean") return null;
|
|
167
|
+
return {
|
|
168
|
+
errorContractVersion: 1,
|
|
169
|
+
faultDomain: domain,
|
|
170
|
+
errorCode,
|
|
171
|
+
transportRequestId: v.transportRequestId,
|
|
172
|
+
consumeRequestId: v.consumeRequestId,
|
|
173
|
+
providerRequestId: v.providerRequestId,
|
|
174
|
+
requestDisposition: disposition,
|
|
175
|
+
retryable: v.retryable
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function readGatewayErrorContract(error) {
|
|
179
|
+
const direct = decodeGatewayErrorContract(error);
|
|
180
|
+
if (direct) return direct;
|
|
181
|
+
if (error == null || typeof error !== "object") return null;
|
|
182
|
+
const e = error;
|
|
183
|
+
const axios = decodeGatewayErrorContract(e.response?.data);
|
|
184
|
+
if (axios) return axios;
|
|
185
|
+
if (typeof e.body === "string") {
|
|
186
|
+
try {
|
|
187
|
+
return decodeGatewayErrorContract(JSON.parse(e.body));
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
function isUserAccessTokenRejected(error) {
|
|
195
|
+
if (error == null || typeof error !== "object") return false;
|
|
196
|
+
const e = error;
|
|
197
|
+
const status = e.statusCode ?? e.response?.status;
|
|
198
|
+
const contract = readGatewayErrorContract(error);
|
|
199
|
+
return status === 401 && contract?.faultDomain === "user_auth" && contract.errorCode === "USER_ACCESS_TOKEN_INVALID" && contract.requestDisposition === "not_accepted";
|
|
200
|
+
}
|
|
152
201
|
function isWindowLimitError(err) {
|
|
153
202
|
if (!(err instanceof exports.HTTPError)) return false;
|
|
154
203
|
if (err.errorCode === windowLimitErrorCode) return true;
|
|
@@ -176,7 +225,7 @@ function getWindowLimitStreamDetails(err) {
|
|
|
176
225
|
windowOverridable: typeof e.windowOverridable === "boolean" ? e.windowOverridable : void 0
|
|
177
226
|
};
|
|
178
227
|
}
|
|
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;
|
|
228
|
+
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
229
|
var init_errors = __esm({
|
|
181
230
|
"src/shared/errors.ts"() {
|
|
182
231
|
exports.RateLimitError = class extends Error {
|
|
@@ -235,6 +284,13 @@ var init_errors = __esm({
|
|
|
235
284
|
* false/缺失 = 周窗/显式禁止/灰度关 (硬等待, 无豁免路径, 老网关不返回时为 undefined)。
|
|
236
285
|
*/
|
|
237
286
|
windowOverridable;
|
|
287
|
+
errorContractVersion;
|
|
288
|
+
faultDomain;
|
|
289
|
+
requestDisposition;
|
|
290
|
+
transportRequestId;
|
|
291
|
+
consumeRequestId;
|
|
292
|
+
providerRequestId;
|
|
293
|
+
retryable;
|
|
238
294
|
constructor(statusCode, opts = {}) {
|
|
239
295
|
let msg;
|
|
240
296
|
if (opts.type) {
|
|
@@ -256,6 +312,13 @@ var init_errors = __esm({
|
|
|
256
312
|
this.windowKind = opts.windowKind;
|
|
257
313
|
this.windowResetAt = opts.windowResetAt;
|
|
258
314
|
this.windowOverridable = opts.windowOverridable;
|
|
315
|
+
this.errorContractVersion = opts.errorContractVersion;
|
|
316
|
+
this.faultDomain = opts.faultDomain;
|
|
317
|
+
this.requestDisposition = opts.requestDisposition;
|
|
318
|
+
this.transportRequestId = opts.transportRequestId;
|
|
319
|
+
this.consumeRequestId = opts.consumeRequestId;
|
|
320
|
+
this.providerRequestId = opts.providerRequestId;
|
|
321
|
+
this.retryable = opts.retryable === true && opts.requestDisposition === "not_accepted";
|
|
259
322
|
}
|
|
260
323
|
};
|
|
261
324
|
exports.NetworkError = class extends Error {
|
|
@@ -266,6 +329,7 @@ var init_errors = __esm({
|
|
|
266
329
|
cause;
|
|
267
330
|
timeout;
|
|
268
331
|
eof;
|
|
332
|
+
requestDisposition = "unknown";
|
|
269
333
|
constructor(op, url, cause, opts = {}) {
|
|
270
334
|
const causeMsg = cause instanceof Error ? cause.message : cause != null ? String(cause) : "network error";
|
|
271
335
|
super(`${op} ${url}: ${causeMsg}`);
|
|
@@ -286,6 +350,8 @@ var init_errors = __esm({
|
|
|
286
350
|
exports.StreamError = class extends Error {
|
|
287
351
|
/** 例: "empty_response" / "rate_limit" / "overloaded" / "" */
|
|
288
352
|
code;
|
|
353
|
+
/** D23 gateway machine code; mirrors the legacy `code` field. */
|
|
354
|
+
errorCode;
|
|
289
355
|
/** 例: "provider" / "settlement" */
|
|
290
356
|
stage;
|
|
291
357
|
/** 用户友好提示 (中文); 历史字段, 与 rawError 区分 */
|
|
@@ -294,23 +360,47 @@ var init_errors = __esm({
|
|
|
294
360
|
rawError;
|
|
295
361
|
/** 客户端是否值得重试 */
|
|
296
362
|
retryable;
|
|
363
|
+
errorContractVersion;
|
|
364
|
+
faultDomain;
|
|
365
|
+
requestDisposition;
|
|
366
|
+
transportRequestId;
|
|
367
|
+
consumeRequestId;
|
|
368
|
+
providerRequestId;
|
|
297
369
|
constructor(opts = {}) {
|
|
298
370
|
const code = opts.code ?? "";
|
|
299
371
|
const stage = opts.stage ?? "";
|
|
300
372
|
const userMessage = opts.message ?? "";
|
|
301
373
|
const rawError = opts.rawError ?? "";
|
|
302
|
-
const retryable = opts.retryable
|
|
374
|
+
const retryable = opts.retryable === true && opts.requestDisposition === "not_accepted";
|
|
303
375
|
const body = rawError !== "" ? rawError : userMessage;
|
|
304
376
|
const msg = stage !== "" ? `stream failed: ${stage}: ${body}` : `stream failed: ${body}`;
|
|
305
377
|
super(msg);
|
|
306
378
|
this.name = "StreamError";
|
|
307
379
|
this.code = code;
|
|
380
|
+
this.errorCode = code;
|
|
308
381
|
this.stage = stage;
|
|
309
382
|
this.userMessage = userMessage;
|
|
310
383
|
this.rawError = rawError;
|
|
311
384
|
this.retryable = retryable;
|
|
385
|
+
this.errorContractVersion = opts.errorContractVersion;
|
|
386
|
+
this.faultDomain = opts.faultDomain;
|
|
387
|
+
this.requestDisposition = opts.requestDisposition;
|
|
388
|
+
this.transportRequestId = opts.transportRequestId;
|
|
389
|
+
this.consumeRequestId = opts.consumeRequestId;
|
|
390
|
+
this.providerRequestId = opts.providerRequestId;
|
|
312
391
|
}
|
|
313
392
|
};
|
|
393
|
+
gatewayFaultDomains = [
|
|
394
|
+
"user_auth",
|
|
395
|
+
"caller_credentials",
|
|
396
|
+
"account_quota",
|
|
397
|
+
"account_permission",
|
|
398
|
+
"provider",
|
|
399
|
+
"gateway",
|
|
400
|
+
"transport",
|
|
401
|
+
"stream_ticket",
|
|
402
|
+
"protocol"
|
|
403
|
+
];
|
|
314
404
|
windowLimitErrorCode = "WINDOW_LIMIT_EXCEEDED";
|
|
315
405
|
windowLimitStreamCode = "window_limit_exceeded";
|
|
316
406
|
}
|
|
@@ -893,16 +983,90 @@ var init_openai = __esm({
|
|
|
893
983
|
* 可能已被 text/tool 推进的 this.blockIndex (否则 content_block_stop 索引错配)。 */
|
|
894
984
|
thinkingBlockIndex = 0;
|
|
895
985
|
textStarted = false;
|
|
896
|
-
/** OpenAI tool_call
|
|
986
|
+
/** OpenAI tool_call 键 → Anthropic block index。键正常是 `tc.index`;上游省略
|
|
987
|
+
* index 时退化为 `id:<tool_call_id>`,两者都没有时沿用上一个键(见
|
|
988
|
+
* {@link resolveToolKey})。 */
|
|
897
989
|
toolBlockIndex = /* @__PURE__ */ new Map();
|
|
898
990
|
blockIndex = 0;
|
|
991
|
+
/** 每个 tool block 已发出的 `partial_json` 累积,用于识别「每片重发全量参数」
|
|
992
|
+
* 的上游(见 tool_calls 分支的累计判别)。 */
|
|
993
|
+
toolArgsAccum = /* @__PURE__ */ new Map();
|
|
994
|
+
/** 上一次解析出的 tool 键,供缺 index 且缺 id 的后续增量沿用。 */
|
|
995
|
+
lastToolKey = null;
|
|
996
|
+
/** 已发出 message_delta/message_stop,避免 finish_reason 与 `[DONE]` 各收一次。 */
|
|
997
|
+
messageClosed = false;
|
|
998
|
+
/**
|
|
999
|
+
* 解析一条 tool_call delta 归属的块键。
|
|
1000
|
+
*
|
|
1001
|
+
* OpenAI 流式规范里 `index` 是必填,但兼容实现常有省略。此前这里直接用
|
|
1002
|
+
* `tc.index` 做 Map 键:两个都省略 index 的 tool_call 会共用键 `undefined`,
|
|
1003
|
+
* 于是只开一个块、两段参数拼进同一条 `partial_json` 流,产出 `{…}{…}` 这种
|
|
1004
|
+
* 必然非法的 JSON。这里按「index → id → 沿用上一个」三级降级,让至少一种
|
|
1005
|
+
* 稳定标识生效。
|
|
1006
|
+
*/
|
|
1007
|
+
resolveToolKey(tc) {
|
|
1008
|
+
if (typeof tc.index === "number" && Number.isFinite(tc.index)) {
|
|
1009
|
+
this.lastToolKey = tc.index;
|
|
1010
|
+
return tc.index;
|
|
1011
|
+
}
|
|
1012
|
+
if (typeof tc.id === "string" && tc.id !== "") {
|
|
1013
|
+
const key = `id:${tc.id}`;
|
|
1014
|
+
this.lastToolKey = key;
|
|
1015
|
+
return key;
|
|
1016
|
+
}
|
|
1017
|
+
if (this.lastToolKey !== null) return this.lastToolKey;
|
|
1018
|
+
this.lastToolKey = 0;
|
|
1019
|
+
return 0;
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* 关闭仍打开的 text / thinking / tool 块,并收口 message。
|
|
1023
|
+
*
|
|
1024
|
+
* 由 `finish_reason` 分支与 `[DONE]` 分支共用:上游断流或只发 `[DONE]` 而不发
|
|
1025
|
+
* `finish_reason` 时,此前一个 `content_block_stop` 都不会发,下游拿到的是一个
|
|
1026
|
+
* 永不闭合的 tool_use 块。
|
|
1027
|
+
*/
|
|
1028
|
+
closeOpenBlocks(events, stopReason) {
|
|
1029
|
+
if (this.messageClosed) return;
|
|
1030
|
+
this.messageClosed = true;
|
|
1031
|
+
if (this.textStarted) {
|
|
1032
|
+
events.push({
|
|
1033
|
+
event: "content_block_stop",
|
|
1034
|
+
data: JSON.stringify({ type: "content_block_stop", index: this.blockIndex })
|
|
1035
|
+
});
|
|
1036
|
+
this.textStarted = false;
|
|
1037
|
+
} else if (this.thinkingStarted && !this.thinkingStopped) {
|
|
1038
|
+
this.thinkingStopped = true;
|
|
1039
|
+
events.push({
|
|
1040
|
+
event: "content_block_stop",
|
|
1041
|
+
data: JSON.stringify({ type: "content_block_stop", index: this.thinkingBlockIndex })
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
for (const idx of this.toolBlockIndex.values()) {
|
|
1045
|
+
events.push({
|
|
1046
|
+
event: "content_block_stop",
|
|
1047
|
+
data: JSON.stringify({ type: "content_block_stop", index: idx })
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
events.push({
|
|
1051
|
+
event: "message_delta",
|
|
1052
|
+
data: JSON.stringify({ type: "message_delta", delta: { stop_reason: stopReason } })
|
|
1053
|
+
});
|
|
1054
|
+
events.push({
|
|
1055
|
+
event: "message_stop",
|
|
1056
|
+
data: JSON.stringify({ type: "message_stop" })
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
899
1059
|
/**
|
|
900
1060
|
* 将一行 OpenAI SSE data 转换为零或多个 Anthropic 格式 StreamEvent
|
|
901
1061
|
* 返回 { events, done }
|
|
902
1062
|
*/
|
|
903
1063
|
convert(data) {
|
|
904
1064
|
if (data === "[DONE]") {
|
|
905
|
-
|
|
1065
|
+
const events2 = [];
|
|
1066
|
+
if (this.messageStarted) {
|
|
1067
|
+
this.closeOpenBlocks(events2, "end_turn");
|
|
1068
|
+
}
|
|
1069
|
+
return { events: events2, done: true };
|
|
906
1070
|
}
|
|
907
1071
|
let chunk;
|
|
908
1072
|
try {
|
|
@@ -974,7 +1138,9 @@ var init_openai = __esm({
|
|
|
974
1138
|
events.push({ event: "content_block_delta", data: deltaJSON });
|
|
975
1139
|
}
|
|
976
1140
|
for (const tc of choice.delta.tool_calls ?? []) {
|
|
977
|
-
|
|
1141
|
+
const fn = tc.function;
|
|
1142
|
+
const toolKey = this.resolveToolKey(tc);
|
|
1143
|
+
if (!this.toolBlockIndex.has(toolKey)) {
|
|
978
1144
|
if (this.thinkingStarted && !this.thinkingStopped) {
|
|
979
1145
|
this.thinkingStopped = true;
|
|
980
1146
|
const stopJSON = JSON.stringify({
|
|
@@ -993,56 +1159,46 @@ var init_openai = __esm({
|
|
|
993
1159
|
this.blockIndex++;
|
|
994
1160
|
this.textStarted = false;
|
|
995
1161
|
}
|
|
996
|
-
this.toolBlockIndex.set(
|
|
1162
|
+
this.toolBlockIndex.set(toolKey, this.blockIndex);
|
|
997
1163
|
const blockJSON = JSON.stringify({
|
|
998
1164
|
type: "content_block_start",
|
|
999
1165
|
index: this.blockIndex,
|
|
1000
1166
|
content_block: {
|
|
1001
1167
|
type: "tool_use",
|
|
1002
1168
|
id: tc.id,
|
|
1003
|
-
name:
|
|
1169
|
+
name: fn?.name,
|
|
1004
1170
|
input: {}
|
|
1005
1171
|
}
|
|
1006
1172
|
});
|
|
1007
1173
|
events.push({ event: "content_block_start", data: blockJSON });
|
|
1008
1174
|
this.blockIndex++;
|
|
1009
1175
|
}
|
|
1010
|
-
if (
|
|
1011
|
-
const
|
|
1012
|
-
const
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1176
|
+
if (fn?.arguments !== void 0 && fn.arguments !== null && fn.arguments !== "") {
|
|
1177
|
+
const rawArgs = typeof fn.arguments === "string" ? fn.arguments : JSON.stringify(fn.arguments);
|
|
1178
|
+
const accum = this.toolArgsAccum.get(toolKey) ?? "";
|
|
1179
|
+
let emit = rawArgs;
|
|
1180
|
+
if (accum !== "" && rawArgs.length > accum.length && rawArgs.startsWith(accum)) {
|
|
1181
|
+
emit = rawArgs.slice(accum.length);
|
|
1182
|
+
this.toolArgsAccum.set(toolKey, rawArgs);
|
|
1183
|
+
} else {
|
|
1184
|
+
this.toolArgsAccum.set(toolKey, accum + rawArgs);
|
|
1185
|
+
}
|
|
1186
|
+
if (emit !== "") {
|
|
1187
|
+
const idx = this.toolBlockIndex.get(toolKey);
|
|
1188
|
+
const deltaJSON = JSON.stringify({
|
|
1189
|
+
type: "content_block_delta",
|
|
1190
|
+
index: idx,
|
|
1191
|
+
delta: {
|
|
1192
|
+
type: "input_json_delta",
|
|
1193
|
+
partial_json: emit
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
events.push({ event: "content_block_delta", data: deltaJSON });
|
|
1197
|
+
}
|
|
1021
1198
|
}
|
|
1022
1199
|
}
|
|
1023
1200
|
if (choice.finish_reason != null && choice.finish_reason !== "") {
|
|
1024
|
-
|
|
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";
|
|
1201
|
+
let stopReason;
|
|
1046
1202
|
switch (choice.finish_reason) {
|
|
1047
1203
|
case "tool_calls":
|
|
1048
1204
|
stopReason = "tool_use";
|
|
@@ -1050,14 +1206,13 @@ var init_openai = __esm({
|
|
|
1050
1206
|
case "length":
|
|
1051
1207
|
stopReason = "max_tokens";
|
|
1052
1208
|
break;
|
|
1209
|
+
case "stop":
|
|
1210
|
+
stopReason = "end_turn";
|
|
1211
|
+
break;
|
|
1212
|
+
default:
|
|
1213
|
+
stopReason = choice.finish_reason;
|
|
1053
1214
|
}
|
|
1054
|
-
|
|
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 });
|
|
1215
|
+
this.closeOpenBlocks(events, stopReason);
|
|
1061
1216
|
}
|
|
1062
1217
|
return { events, done: false };
|
|
1063
1218
|
}
|
|
@@ -1118,6 +1273,560 @@ var init_adapters = __esm({
|
|
|
1118
1273
|
}
|
|
1119
1274
|
});
|
|
1120
1275
|
|
|
1276
|
+
// src/core/credentials.ts
|
|
1277
|
+
var uuid = () => globalThis.crypto.randomUUID();
|
|
1278
|
+
var pause = () => new Promise((resolve) => setTimeout(resolve, 25));
|
|
1279
|
+
var abort = (signal) => {
|
|
1280
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
1281
|
+
};
|
|
1282
|
+
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;
|
|
1283
|
+
var CredentialLifecycle = class {
|
|
1284
|
+
constructor(store, protocol) {
|
|
1285
|
+
this.store = store;
|
|
1286
|
+
this.protocol = protocol;
|
|
1287
|
+
}
|
|
1288
|
+
store;
|
|
1289
|
+
protocol;
|
|
1290
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1291
|
+
notificationKey = "";
|
|
1292
|
+
observations = /* @__PURE__ */ new Map();
|
|
1293
|
+
read(signal) {
|
|
1294
|
+
return this.store.readSnapshot(signal);
|
|
1295
|
+
}
|
|
1296
|
+
subscribe(listener) {
|
|
1297
|
+
this.listeners.add(listener);
|
|
1298
|
+
return () => {
|
|
1299
|
+
this.listeners.delete(listener);
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
async notify(snapshot) {
|
|
1303
|
+
let current;
|
|
1304
|
+
try {
|
|
1305
|
+
current = await this.read();
|
|
1306
|
+
} catch {
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
if (current.storeInstanceId !== snapshot.storeInstanceId || current.revision !== snapshot.revision)
|
|
1310
|
+
return;
|
|
1311
|
+
const projection = {
|
|
1312
|
+
storeInstanceId: snapshot.storeInstanceId,
|
|
1313
|
+
authorityConfig: snapshot.authorityConfig && {
|
|
1314
|
+
serverURL: snapshot.authorityConfig.serverURL,
|
|
1315
|
+
issuer: snapshot.authorityConfig.issuer,
|
|
1316
|
+
oauthProfile: "desktop",
|
|
1317
|
+
authContractVersion: 2,
|
|
1318
|
+
errorContractVersion: 1
|
|
1319
|
+
},
|
|
1320
|
+
revision: snapshot.revision,
|
|
1321
|
+
authSessionId: snapshot.authSessionId,
|
|
1322
|
+
credentialState: snapshot.credentialState,
|
|
1323
|
+
reason: snapshot.reason,
|
|
1324
|
+
principal: snapshot.principal && {
|
|
1325
|
+
issuer: snapshot.principal.issuer,
|
|
1326
|
+
subject: snapshot.principal.subject,
|
|
1327
|
+
organizationId: snapshot.principal.organizationId
|
|
1328
|
+
},
|
|
1329
|
+
refreshOperation: snapshot.refreshOperation && {
|
|
1330
|
+
operationId: snapshot.refreshOperation.operationId,
|
|
1331
|
+
sessionId: snapshot.refreshOperation.sessionId,
|
|
1332
|
+
baseRevision: snapshot.refreshOperation.baseRevision,
|
|
1333
|
+
phase: snapshot.refreshOperation.phase,
|
|
1334
|
+
returnState: snapshot.refreshOperation.returnState,
|
|
1335
|
+
startedAt: snapshot.refreshOperation.startedAt,
|
|
1336
|
+
dispatchedAt: snapshot.refreshOperation.dispatchedAt,
|
|
1337
|
+
deadlineAt: snapshot.refreshOperation.deadlineAt
|
|
1338
|
+
},
|
|
1339
|
+
loginAttempt: snapshot.loginAttempt && {
|
|
1340
|
+
attemptId: snapshot.loginAttempt.attemptId,
|
|
1341
|
+
baseSessionId: snapshot.loginAttempt.baseSessionId,
|
|
1342
|
+
startedAt: snapshot.loginAttempt.startedAt
|
|
1343
|
+
},
|
|
1344
|
+
lastMutation: snapshot.lastMutation && {
|
|
1345
|
+
mutationId: snapshot.lastMutation.mutationId,
|
|
1346
|
+
operationId: snapshot.lastMutation.operationId,
|
|
1347
|
+
resultRevision: snapshot.lastMutation.resultRevision
|
|
1348
|
+
},
|
|
1349
|
+
lastLoginAttemptId: snapshot.lastLoginAttemptId,
|
|
1350
|
+
verifiedIdentity: snapshot.verifiedIdentity && {
|
|
1351
|
+
authSessionId: snapshot.verifiedIdentity.authSessionId,
|
|
1352
|
+
principal: {
|
|
1353
|
+
issuer: snapshot.verifiedIdentity.principal.issuer,
|
|
1354
|
+
subject: snapshot.verifiedIdentity.principal.subject,
|
|
1355
|
+
organizationId: snapshot.verifiedIdentity.principal.organizationId
|
|
1356
|
+
},
|
|
1357
|
+
displayName: snapshot.verifiedIdentity.displayName,
|
|
1358
|
+
avatarUrl: snapshot.verifiedIdentity.avatarUrl,
|
|
1359
|
+
email: snapshot.verifiedIdentity.email,
|
|
1360
|
+
imageUrl: snapshot.verifiedIdentity.imageUrl,
|
|
1361
|
+
accountCreatedAt: snapshot.verifiedIdentity.accountCreatedAt,
|
|
1362
|
+
requiresPhoneBinding: snapshot.verifiedIdentity.requiresPhoneBinding,
|
|
1363
|
+
hasExtraUsageEnabled: snapshot.verifiedIdentity.hasExtraUsageEnabled,
|
|
1364
|
+
billingType: snapshot.verifiedIdentity.billingType,
|
|
1365
|
+
subscriptionCreatedAt: snapshot.verifiedIdentity.subscriptionCreatedAt,
|
|
1366
|
+
rateLimitTier: snapshot.verifiedIdentity.rateLimitTier,
|
|
1367
|
+
organizationName: snapshot.verifiedIdentity.organizationName,
|
|
1368
|
+
verifiedAt: snapshot.verifiedIdentity.verifiedAt
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
const key = JSON.stringify({ ...projection, revision: void 0, lastMutation: void 0 });
|
|
1372
|
+
if (key === this.notificationKey) return;
|
|
1373
|
+
this.notificationKey = key;
|
|
1374
|
+
for (const listener of this.listeners) {
|
|
1375
|
+
try {
|
|
1376
|
+
listener(structuredClone(projection));
|
|
1377
|
+
} catch {
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
async reconcile(signal) {
|
|
1382
|
+
const s = await this.read(signal);
|
|
1383
|
+
await this.notify(s);
|
|
1384
|
+
return s;
|
|
1385
|
+
}
|
|
1386
|
+
cas(s, changes, mutationId = uuid(), signal) {
|
|
1387
|
+
return this.store.compareAndSwap(
|
|
1388
|
+
{
|
|
1389
|
+
storeInstanceId: s.storeInstanceId,
|
|
1390
|
+
revision: s.revision,
|
|
1391
|
+
authSessionId: s.authSessionId,
|
|
1392
|
+
state: s.credentialState,
|
|
1393
|
+
operationId: s.refreshOperation?.operationId ?? s.loginAttempt?.attemptId ?? null
|
|
1394
|
+
},
|
|
1395
|
+
{ ...s, ...changes },
|
|
1396
|
+
mutationId,
|
|
1397
|
+
signal
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
async commit(s, changes, signal) {
|
|
1401
|
+
const r = await this.cas(s, changes, uuid(), signal);
|
|
1402
|
+
if (r.status === "storage_error") throw new Error("storage_unavailable");
|
|
1403
|
+
if (r.status === "committed") await this.notify(r.snapshot);
|
|
1404
|
+
return r;
|
|
1405
|
+
}
|
|
1406
|
+
async metadata(s, signal) {
|
|
1407
|
+
const m = await this.protocol.metadata(signal);
|
|
1408
|
+
if (m.crabcode_auth_contract_version !== 2 || m.gateway_error_contract_version !== 1 || !m.issuer)
|
|
1409
|
+
throw new Error("auth_contract_unsupported");
|
|
1410
|
+
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))
|
|
1411
|
+
throw new Error("auth_contract_unsupported");
|
|
1412
|
+
return m;
|
|
1413
|
+
}
|
|
1414
|
+
async reserveLogin(signal) {
|
|
1415
|
+
const attemptId = uuid();
|
|
1416
|
+
let reserved;
|
|
1417
|
+
for (; ; ) {
|
|
1418
|
+
abort(signal);
|
|
1419
|
+
const s = await this.read(signal);
|
|
1420
|
+
if (s.credentialState !== "signed_out") throw new Error("local_logout_required");
|
|
1421
|
+
const r = await this.commit(s, { loginAttempt: { attemptId, baseSessionId: s.authSessionId, startedAt: (/* @__PURE__ */ new Date()).toISOString() } }, signal);
|
|
1422
|
+
if (r.status === "committed") {
|
|
1423
|
+
reserved = r.snapshot;
|
|
1424
|
+
break;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
try {
|
|
1428
|
+
const m = await this.metadata(reserved, signal);
|
|
1429
|
+
for (; ; ) {
|
|
1430
|
+
abort(signal);
|
|
1431
|
+
const current = await this.read(signal);
|
|
1432
|
+
if (current.storeInstanceId !== reserved.storeInstanceId || current.loginAttempt?.attemptId !== attemptId) throw new Error("superseded");
|
|
1433
|
+
if (current.authorityConfig) return { attemptId, metadata: m };
|
|
1434
|
+
const r = await this.commit(current, { authorityConfig: { serverURL: this.protocol.serverURL, issuer: m.issuer, oauthProfile: "desktop", authContractVersion: 2, errorContractVersion: 1 } }, signal);
|
|
1435
|
+
if (r.status === "committed") return { attemptId, metadata: m };
|
|
1436
|
+
}
|
|
1437
|
+
} catch (error) {
|
|
1438
|
+
await this.cancelLogin(attemptId);
|
|
1439
|
+
throw error;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
async installLogin(attemptId, tokens, signal) {
|
|
1443
|
+
this.validateTokens(tokens);
|
|
1444
|
+
for (; ; ) {
|
|
1445
|
+
abort(signal);
|
|
1446
|
+
const s = await this.read(signal);
|
|
1447
|
+
if (s.loginAttempt?.attemptId !== attemptId || s.loginAttempt.baseSessionId !== s.authSessionId)
|
|
1448
|
+
throw new Error("superseded");
|
|
1449
|
+
const r = await this.commit(
|
|
1450
|
+
s,
|
|
1451
|
+
{
|
|
1452
|
+
authSessionId: uuid(),
|
|
1453
|
+
tokenSet: tokens,
|
|
1454
|
+
credentialState: "pending_identity",
|
|
1455
|
+
principal: null,
|
|
1456
|
+
verifiedIdentity: null,
|
|
1457
|
+
loginAttempt: null,
|
|
1458
|
+
lastLoginAttemptId: attemptId,
|
|
1459
|
+
reason: "identity_unavailable"
|
|
1460
|
+
},
|
|
1461
|
+
signal
|
|
1462
|
+
);
|
|
1463
|
+
if (r.status === "committed") return this.bindIdentity(r.snapshot.authSessionId, signal);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
async assertLogin(attemptId, signal) {
|
|
1467
|
+
abort(signal);
|
|
1468
|
+
const s = await this.read(signal);
|
|
1469
|
+
if (s.loginAttempt?.attemptId !== attemptId) throw new Error("superseded");
|
|
1470
|
+
}
|
|
1471
|
+
async cancelLogin(attemptId) {
|
|
1472
|
+
const s = await this.read();
|
|
1473
|
+
if (s.loginAttempt?.attemptId === attemptId) await this.commit(s, { loginAttempt: null });
|
|
1474
|
+
}
|
|
1475
|
+
validateTokens(t) {
|
|
1476
|
+
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())
|
|
1477
|
+
throw new Error("invalid_response");
|
|
1478
|
+
}
|
|
1479
|
+
usable(s) {
|
|
1480
|
+
return !!s.tokenSet && Date.parse(s.tokenSet.expires_at) > Date.now() + 3e5;
|
|
1481
|
+
}
|
|
1482
|
+
async bindIdentity(sessionId, signal) {
|
|
1483
|
+
const s = await this.ensureSnapshot(signal, void 0, true);
|
|
1484
|
+
if (s.authSessionId !== sessionId) throw new Error("superseded");
|
|
1485
|
+
if (s.credentialState === "ready") return s;
|
|
1486
|
+
await this.metadata(s, signal);
|
|
1487
|
+
const beforeProfile = await this.read(signal);
|
|
1488
|
+
if (beforeProfile.storeInstanceId !== s.storeInstanceId || beforeProfile.authSessionId !== sessionId || beforeProfile.revision !== s.revision || beforeProfile.credentialState !== "pending_identity")
|
|
1489
|
+
throw new Error("superseded");
|
|
1490
|
+
let identity;
|
|
1491
|
+
try {
|
|
1492
|
+
identity = await this.protocol.profile(s.tokenSet, signal);
|
|
1493
|
+
} catch (error) {
|
|
1494
|
+
const message = error instanceof Error ? error.message : "";
|
|
1495
|
+
if (message === "invalid_scope" || message === "auth_contract_unsupported")
|
|
1496
|
+
await this.commit(s, { credentialState: "configuration_error", reason: message });
|
|
1497
|
+
throw error;
|
|
1498
|
+
}
|
|
1499
|
+
abort(signal);
|
|
1500
|
+
const current = await this.read(signal);
|
|
1501
|
+
if (current.storeInstanceId !== s.storeInstanceId || current.authSessionId !== sessionId || current.revision !== s.revision)
|
|
1502
|
+
throw new Error("superseded");
|
|
1503
|
+
if (!identity.subject) throw new Error("invalid_response");
|
|
1504
|
+
const principal = {
|
|
1505
|
+
issuer: s.authorityConfig.issuer,
|
|
1506
|
+
subject: identity.subject,
|
|
1507
|
+
organizationId: identity.organizationId
|
|
1508
|
+
};
|
|
1509
|
+
const r = await this.commit(
|
|
1510
|
+
s,
|
|
1511
|
+
{
|
|
1512
|
+
principal,
|
|
1513
|
+
verifiedIdentity: {
|
|
1514
|
+
authSessionId: sessionId,
|
|
1515
|
+
principal,
|
|
1516
|
+
displayName: identity.displayName,
|
|
1517
|
+
avatarUrl: identity.avatarUrl,
|
|
1518
|
+
email: identity.email,
|
|
1519
|
+
imageUrl: identity.imageUrl,
|
|
1520
|
+
accountCreatedAt: identity.accountCreatedAt,
|
|
1521
|
+
requiresPhoneBinding: identity.requiresPhoneBinding,
|
|
1522
|
+
hasExtraUsageEnabled: identity.hasExtraUsageEnabled,
|
|
1523
|
+
billingType: identity.billingType,
|
|
1524
|
+
subscriptionCreatedAt: identity.subscriptionCreatedAt,
|
|
1525
|
+
rateLimitTier: identity.rateLimitTier,
|
|
1526
|
+
organizationName: identity.organizationName,
|
|
1527
|
+
verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1528
|
+
},
|
|
1529
|
+
credentialState: "ready",
|
|
1530
|
+
reason: null
|
|
1531
|
+
},
|
|
1532
|
+
signal
|
|
1533
|
+
);
|
|
1534
|
+
if (r.status !== "committed") throw new Error("superseded");
|
|
1535
|
+
return r.snapshot;
|
|
1536
|
+
}
|
|
1537
|
+
async ensure(signal, rejected, expected) {
|
|
1538
|
+
const snapshot = await this.ensureSnapshot(signal, rejected, false, expected);
|
|
1539
|
+
this.observations.set(snapshot.tokenSet.access_token, snapshot);
|
|
1540
|
+
if (this.observations.size > 128)
|
|
1541
|
+
this.observations.delete(this.observations.keys().next().value);
|
|
1542
|
+
return snapshot.tokenSet.access_token;
|
|
1543
|
+
}
|
|
1544
|
+
async forceRefresh(signal, rejectedToken, expected) {
|
|
1545
|
+
const rejected = rejectedToken ? this.observations.get(rejectedToken) : await this.read(signal);
|
|
1546
|
+
if (rejectedToken && !rejected) throw new Error("credential_observation_unavailable");
|
|
1547
|
+
return this.ensure(signal, rejected, expected);
|
|
1548
|
+
}
|
|
1549
|
+
async ensureSnapshot(signal, rejected, pending = false, expected) {
|
|
1550
|
+
let session;
|
|
1551
|
+
let instance;
|
|
1552
|
+
let initialRevision;
|
|
1553
|
+
let refreshed = false;
|
|
1554
|
+
let initialAccess;
|
|
1555
|
+
for (; ; ) {
|
|
1556
|
+
abort(signal);
|
|
1557
|
+
const s = await this.read(signal);
|
|
1558
|
+
if (expected && !sameRequestOwner(s, expected)) throw new Error("superseded");
|
|
1559
|
+
if (s.authorityConfig && s.authorityConfig.serverURL !== this.protocol.serverURL)
|
|
1560
|
+
throw new Error("auth_contract_unsupported");
|
|
1561
|
+
if (session === void 0) {
|
|
1562
|
+
session = s.authSessionId;
|
|
1563
|
+
instance = s.storeInstanceId;
|
|
1564
|
+
initialRevision = s.revision;
|
|
1565
|
+
initialAccess = s.tokenSet?.access_token;
|
|
1566
|
+
}
|
|
1567
|
+
if (s.storeInstanceId !== instance || s.authSessionId !== session || rejected && (s.authSessionId !== rejected.authSessionId || s.storeInstanceId !== rejected.storeInstanceId))
|
|
1568
|
+
throw new Error("superseded");
|
|
1569
|
+
if (s.credentialState === "refresh_dispatched") {
|
|
1570
|
+
if (Date.now() >= Date.parse(s.refreshOperation.deadlineAt))
|
|
1571
|
+
await this.commit(s, {
|
|
1572
|
+
credentialState: "reauth_required",
|
|
1573
|
+
tokenSet: null,
|
|
1574
|
+
refreshOperation: null,
|
|
1575
|
+
reason: "refresh_outcome_unknown"
|
|
1576
|
+
});
|
|
1577
|
+
else await pause();
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
if (s.credentialState === "refresh_reserved") {
|
|
1581
|
+
if (Date.now() >= Date.parse(s.refreshOperation.startedAt) + 3e4)
|
|
1582
|
+
await this.commit(s, {
|
|
1583
|
+
credentialState: s.refreshOperation.returnState,
|
|
1584
|
+
refreshOperation: null
|
|
1585
|
+
});
|
|
1586
|
+
else await pause();
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
if (s.credentialState !== "ready" && !(pending && s.credentialState === "pending_identity"))
|
|
1590
|
+
throw new Error(`credential_unavailable:${s.credentialState}:${s.reason ?? ""}`);
|
|
1591
|
+
const live = !!s.tokenSet && Date.parse(s.tokenSet.expires_at) > Date.now();
|
|
1592
|
+
if (live && (refreshed || s.revision !== (rejected?.revision ?? initialRevision) && (rejected !== void 0 || s.tokenSet.access_token !== initialAccess)))
|
|
1593
|
+
return s;
|
|
1594
|
+
if (this.usable(s) && !rejected) return s;
|
|
1595
|
+
const metadata = await this.metadata(s, signal);
|
|
1596
|
+
abort(signal);
|
|
1597
|
+
const operationId = uuid();
|
|
1598
|
+
const r = await this.commit(
|
|
1599
|
+
s,
|
|
1600
|
+
{
|
|
1601
|
+
credentialState: "refresh_reserved",
|
|
1602
|
+
refreshOperation: {
|
|
1603
|
+
operationId,
|
|
1604
|
+
sessionId: s.authSessionId,
|
|
1605
|
+
baseRevision: s.revision,
|
|
1606
|
+
phase: "reserved",
|
|
1607
|
+
returnState: s.credentialState,
|
|
1608
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1609
|
+
dispatchedAt: null,
|
|
1610
|
+
deadlineAt: null
|
|
1611
|
+
}
|
|
1612
|
+
},
|
|
1613
|
+
signal
|
|
1614
|
+
);
|
|
1615
|
+
if (r.status !== "committed") continue;
|
|
1616
|
+
const owner = this.refresh(r.snapshot, metadata);
|
|
1617
|
+
await this.wait(owner, signal);
|
|
1618
|
+
refreshed = true;
|
|
1619
|
+
rejected = void 0;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
wait(promise, signal) {
|
|
1623
|
+
if (!signal) return promise;
|
|
1624
|
+
return new Promise((resolve, reject) => {
|
|
1625
|
+
const onAbort = () => {
|
|
1626
|
+
signal.removeEventListener("abort", onAbort);
|
|
1627
|
+
reject(new Error("aborted"));
|
|
1628
|
+
};
|
|
1629
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1630
|
+
promise.then(
|
|
1631
|
+
(value) => {
|
|
1632
|
+
signal.removeEventListener("abort", onAbort);
|
|
1633
|
+
resolve(value);
|
|
1634
|
+
},
|
|
1635
|
+
(error) => {
|
|
1636
|
+
signal.removeEventListener("abort", onAbort);
|
|
1637
|
+
reject(error);
|
|
1638
|
+
}
|
|
1639
|
+
);
|
|
1640
|
+
if (signal.aborted) onAbort();
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
async refresh(reserved, metadata) {
|
|
1644
|
+
const dispatchedAt = Date.now();
|
|
1645
|
+
const r = await this.commit(reserved, {
|
|
1646
|
+
credentialState: "refresh_dispatched",
|
|
1647
|
+
refreshOperation: {
|
|
1648
|
+
...reserved.refreshOperation,
|
|
1649
|
+
phase: "dispatched",
|
|
1650
|
+
dispatchedAt: new Date(dispatchedAt).toISOString(),
|
|
1651
|
+
deadlineAt: new Date(dispatchedAt + 3e4).toISOString()
|
|
1652
|
+
}
|
|
1653
|
+
});
|
|
1654
|
+
if (r.status !== "committed") return;
|
|
1655
|
+
const s = r.snapshot;
|
|
1656
|
+
const ctl = new AbortController();
|
|
1657
|
+
const timeout = setTimeout(() => ctl.abort(), 3e4);
|
|
1658
|
+
let tokens;
|
|
1659
|
+
try {
|
|
1660
|
+
const result = await this.wait(
|
|
1661
|
+
(async () => {
|
|
1662
|
+
const response = await this.protocol.fetch(metadata.token_endpoint, {
|
|
1663
|
+
method: "POST",
|
|
1664
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1665
|
+
body: new URLSearchParams({
|
|
1666
|
+
grant_type: "refresh_token",
|
|
1667
|
+
client_id: s.tokenSet.client_id,
|
|
1668
|
+
refresh_token: s.tokenSet.refresh_token
|
|
1669
|
+
}),
|
|
1670
|
+
signal: ctl.signal
|
|
1671
|
+
});
|
|
1672
|
+
const body2 = await response.json();
|
|
1673
|
+
return { response, body: body2 };
|
|
1674
|
+
})(),
|
|
1675
|
+
ctl.signal
|
|
1676
|
+
);
|
|
1677
|
+
if (!result.response.ok) {
|
|
1678
|
+
const code = result.body.error;
|
|
1679
|
+
let reason = "refresh_outcome_unknown";
|
|
1680
|
+
let state = "reauth_required";
|
|
1681
|
+
if (code === "invalid_grant") reason = code;
|
|
1682
|
+
else if (["invalid_client", "invalid_scope", "unsupported_grant_type"].includes(code)) {
|
|
1683
|
+
reason = code;
|
|
1684
|
+
state = "configuration_error";
|
|
1685
|
+
} else if (result.body.rotationOutcome === "not_committed" && ["temporarily_unavailable", "server_error"].includes(code)) {
|
|
1686
|
+
await this.commit(s, {
|
|
1687
|
+
credentialState: s.refreshOperation.returnState,
|
|
1688
|
+
refreshOperation: null
|
|
1689
|
+
});
|
|
1690
|
+
throw new Error("refresh_temporarily_unavailable");
|
|
1691
|
+
}
|
|
1692
|
+
await this.commit(s, {
|
|
1693
|
+
credentialState: state,
|
|
1694
|
+
tokenSet: null,
|
|
1695
|
+
refreshOperation: null,
|
|
1696
|
+
reason
|
|
1697
|
+
});
|
|
1698
|
+
throw new Error(reason);
|
|
1699
|
+
}
|
|
1700
|
+
const body = result.body;
|
|
1701
|
+
if (!Number.isFinite(body.expires_in) || body.expires_in <= 0)
|
|
1702
|
+
throw new Error("invalid_response");
|
|
1703
|
+
tokens = {
|
|
1704
|
+
...s.tokenSet,
|
|
1705
|
+
access_token: body.access_token,
|
|
1706
|
+
refresh_token: body.refresh_token,
|
|
1707
|
+
expires_at: new Date(Date.now() + body.expires_in * 1e3).toISOString(),
|
|
1708
|
+
scope: body.scope ?? s.tokenSet.scope
|
|
1709
|
+
};
|
|
1710
|
+
this.validateTokens(tokens);
|
|
1711
|
+
} catch (error) {
|
|
1712
|
+
await this.commit(s, {
|
|
1713
|
+
credentialState: "reauth_required",
|
|
1714
|
+
tokenSet: null,
|
|
1715
|
+
refreshOperation: null,
|
|
1716
|
+
reason: "refresh_outcome_unknown"
|
|
1717
|
+
});
|
|
1718
|
+
throw error;
|
|
1719
|
+
} finally {
|
|
1720
|
+
clearTimeout(timeout);
|
|
1721
|
+
}
|
|
1722
|
+
const started = performance.now(), mutationId = uuid();
|
|
1723
|
+
for (; ; ) {
|
|
1724
|
+
if (Date.now() >= Date.parse(s.refreshOperation.deadlineAt)) {
|
|
1725
|
+
await this.commit(s, {
|
|
1726
|
+
credentialState: "reauth_required",
|
|
1727
|
+
tokenSet: null,
|
|
1728
|
+
refreshOperation: null,
|
|
1729
|
+
reason: "refresh_outcome_unknown"
|
|
1730
|
+
});
|
|
1731
|
+
void this.revoke(tokens, metadata);
|
|
1732
|
+
throw new Error("refresh_outcome_unknown");
|
|
1733
|
+
}
|
|
1734
|
+
const result = await this.cas(
|
|
1735
|
+
s,
|
|
1736
|
+
{
|
|
1737
|
+
tokenSet: tokens,
|
|
1738
|
+
credentialState: s.refreshOperation.returnState,
|
|
1739
|
+
refreshOperation: null
|
|
1740
|
+
},
|
|
1741
|
+
mutationId
|
|
1742
|
+
);
|
|
1743
|
+
if (result.status === "committed") {
|
|
1744
|
+
await this.notify(result.snapshot);
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
if (result.status === "superseded") {
|
|
1748
|
+
void this.revoke(tokens, metadata);
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
if (performance.now() - started >= 2e3) throw new Error("credential_persist_failed");
|
|
1752
|
+
await pause();
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
async revoke(tokens, metadata, authority) {
|
|
1756
|
+
const ctl = new AbortController();
|
|
1757
|
+
const timer = setTimeout(() => ctl.abort(), 3e4);
|
|
1758
|
+
try {
|
|
1759
|
+
const m = metadata ?? await this.wait(authority ? this.metadata(authority, ctl.signal) : this.protocol.metadata(ctl.signal), ctl.signal);
|
|
1760
|
+
if (!m.revocation_endpoint) return "unsupported";
|
|
1761
|
+
const response = await this.wait(this.protocol.fetch(m.revocation_endpoint, {
|
|
1762
|
+
method: "POST",
|
|
1763
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1764
|
+
body: new URLSearchParams({ token: tokens.refresh_token, token_type_hint: "refresh_token" }),
|
|
1765
|
+
signal: ctl.signal
|
|
1766
|
+
}), ctl.signal);
|
|
1767
|
+
return response.ok ? "confirmed" : "failed";
|
|
1768
|
+
} catch {
|
|
1769
|
+
return "failed";
|
|
1770
|
+
} finally {
|
|
1771
|
+
clearTimeout(timer);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
async logout(signal, expected) {
|
|
1775
|
+
let s = await this.read(signal);
|
|
1776
|
+
if (expected && (s.storeInstanceId !== expected.storeInstanceId || s.authSessionId !== expected.authSessionId)) {
|
|
1777
|
+
return {
|
|
1778
|
+
logoutOperationId: uuid(),
|
|
1779
|
+
expectedAuthSessionId: expected.authSessionId,
|
|
1780
|
+
storeInstanceId: expected.storeInstanceId,
|
|
1781
|
+
revision: s.revision,
|
|
1782
|
+
status: "superseded"
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
const session = s.authSessionId, instance = s.storeInstanceId, started = performance.now(), logoutOperationId = uuid();
|
|
1786
|
+
const receipt = () => ({
|
|
1787
|
+
logoutOperationId,
|
|
1788
|
+
expectedAuthSessionId: session,
|
|
1789
|
+
storeInstanceId: instance,
|
|
1790
|
+
revision: s.revision
|
|
1791
|
+
});
|
|
1792
|
+
for (; ; ) {
|
|
1793
|
+
abort(signal);
|
|
1794
|
+
if (s.storeInstanceId !== instance || s.authSessionId !== session)
|
|
1795
|
+
return { ...receipt(), status: "superseded" };
|
|
1796
|
+
if (s.credentialState === "signed_out" && !s.loginAttempt)
|
|
1797
|
+
return { ...receipt(), status: "already_signed_out" };
|
|
1798
|
+
const r = await this.cas(
|
|
1799
|
+
s,
|
|
1800
|
+
{
|
|
1801
|
+
credentialState: "signed_out",
|
|
1802
|
+
authSessionId: null,
|
|
1803
|
+
principal: null,
|
|
1804
|
+
tokenSet: null,
|
|
1805
|
+
refreshOperation: null,
|
|
1806
|
+
loginAttempt: null,
|
|
1807
|
+
lastLoginAttemptId: null,
|
|
1808
|
+
verifiedIdentity: null,
|
|
1809
|
+
reason: null
|
|
1810
|
+
},
|
|
1811
|
+
logoutOperationId,
|
|
1812
|
+
signal
|
|
1813
|
+
);
|
|
1814
|
+
if (r.status === "committed") {
|
|
1815
|
+
await this.notify(r.snapshot);
|
|
1816
|
+
return {
|
|
1817
|
+
...receipt(),
|
|
1818
|
+
revision: r.snapshot.revision,
|
|
1819
|
+
status: "committed",
|
|
1820
|
+
revocation: s.tokenSet ? this.revoke(s.tokenSet, void 0, s) : Promise.resolve("unsupported")
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
if (performance.now() - started >= 2e3) throw new Error("storage_busy");
|
|
1824
|
+
await pause();
|
|
1825
|
+
s = await this.read(signal);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
|
|
1121
1830
|
// src/core/client.ts
|
|
1122
1831
|
init_types();
|
|
1123
1832
|
|
|
@@ -1557,33 +2266,35 @@ async function revokeToken(meta, token, signal, fetchImpl = globalThis.fetch) {
|
|
|
1557
2266
|
}
|
|
1558
2267
|
async function postToken(endpoint, data, signal, fetchImpl = globalThis.fetch) {
|
|
1559
2268
|
const ctl = withTimeout(authTimeoutMs, signal);
|
|
1560
|
-
let resp;
|
|
1561
2269
|
try {
|
|
1562
|
-
resp
|
|
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 = {};
|
|
2270
|
+
let resp;
|
|
1575
2271
|
try {
|
|
1576
|
-
|
|
1577
|
-
|
|
2272
|
+
resp = await fetchImpl(endpoint, {
|
|
2273
|
+
method: "POST",
|
|
2274
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2275
|
+
body: data,
|
|
2276
|
+
signal: ctl.signal
|
|
2277
|
+
});
|
|
2278
|
+
} catch (e) {
|
|
2279
|
+
throw new Error(`token request: ${e instanceof Error ? e.message : String(e)}`);
|
|
1578
2280
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
2281
|
+
if (!resp.ok) {
|
|
2282
|
+
let errBody = {};
|
|
2283
|
+
try {
|
|
2284
|
+
errBody = await resp.json();
|
|
2285
|
+
} catch {
|
|
2286
|
+
}
|
|
2287
|
+
const oauthError = typeof errBody.error === "string" ? errBody.error : "";
|
|
2288
|
+
const errorDescription = typeof errBody.error_description === "string" ? errBody.error_description : "";
|
|
2289
|
+
throw new OAuthTokenEndpointError(resp.status, oauthError, errorDescription);
|
|
2290
|
+
}
|
|
2291
|
+
try {
|
|
2292
|
+
return await resp.json();
|
|
2293
|
+
} catch (e) {
|
|
2294
|
+
throw new Error(`token: decode: ${e instanceof Error ? e.message : String(e)}`);
|
|
2295
|
+
}
|
|
2296
|
+
} finally {
|
|
2297
|
+
ctl.dispose();
|
|
1587
2298
|
}
|
|
1588
2299
|
}
|
|
1589
2300
|
function newTokenSet(resp, clientID, serverURL) {
|
|
@@ -2066,10 +2777,31 @@ function parseHTTPErrorWithHeader(statusCode, body, header) {
|
|
|
2066
2777
|
} else if (typeof top.message === "string") {
|
|
2067
2778
|
message = top.message;
|
|
2068
2779
|
}
|
|
2780
|
+
const contractSource = errObj && typeof errObj === "object" ? errObj : top;
|
|
2069
2781
|
if (typeof top.errorCode === "string") errorCode = top.errorCode;
|
|
2782
|
+
else if (typeof contractSource.errorCode === "string") errorCode = contractSource.errorCode;
|
|
2070
2783
|
if (top.windowKind === "FIVE_HOUR" || top.windowKind === "WEEKLY") windowKind = top.windowKind;
|
|
2071
2784
|
if (typeof top.windowResetAt === "string") windowResetAt = top.windowResetAt;
|
|
2072
2785
|
if (typeof top.windowOverridable === "boolean") windowOverridable = top.windowOverridable;
|
|
2786
|
+
const disposition = contractSource.requestDisposition === "not_accepted" || contractSource.requestDisposition === "accepted" || contractSource.requestDisposition === "unknown" ? contractSource.requestDisposition : void 0;
|
|
2787
|
+
const domains = ["user_auth", "caller_credentials", "account_quota", "account_permission", "provider", "gateway", "transport", "stream_ticket", "protocol"];
|
|
2788
|
+
return new exports.HTTPError(statusCode, {
|
|
2789
|
+
type,
|
|
2790
|
+
message,
|
|
2791
|
+
retryAfter,
|
|
2792
|
+
body: bodyStr,
|
|
2793
|
+
errorCode,
|
|
2794
|
+
windowKind,
|
|
2795
|
+
windowResetAt,
|
|
2796
|
+
windowOverridable,
|
|
2797
|
+
errorContractVersion: contractSource.errorContractVersion === 1 ? 1 : void 0,
|
|
2798
|
+
faultDomain: typeof contractSource.faultDomain === "string" && domains.includes(contractSource.faultDomain) ? contractSource.faultDomain : void 0,
|
|
2799
|
+
requestDisposition: disposition,
|
|
2800
|
+
transportRequestId: typeof contractSource.transportRequestId === "string" ? contractSource.transportRequestId : null,
|
|
2801
|
+
consumeRequestId: typeof contractSource.consumeRequestId === "string" ? contractSource.consumeRequestId : null,
|
|
2802
|
+
providerRequestId: typeof contractSource.providerRequestId === "string" ? contractSource.providerRequestId : null,
|
|
2803
|
+
retryable: contractSource.retryable === true
|
|
2804
|
+
});
|
|
2073
2805
|
}
|
|
2074
2806
|
} catch {
|
|
2075
2807
|
}
|
|
@@ -2131,7 +2863,19 @@ function parseStreamError(data) {
|
|
|
2131
2863
|
if (code === "" && errObj.type) code = errObj.type;
|
|
2132
2864
|
}
|
|
2133
2865
|
}
|
|
2134
|
-
return new exports.StreamError({
|
|
2866
|
+
return new exports.StreamError({
|
|
2867
|
+
code,
|
|
2868
|
+
stage,
|
|
2869
|
+
message,
|
|
2870
|
+
rawError,
|
|
2871
|
+
retryable,
|
|
2872
|
+
errorContractVersion: payload.errorContractVersion,
|
|
2873
|
+
faultDomain: payload.faultDomain,
|
|
2874
|
+
requestDisposition: payload.requestDisposition,
|
|
2875
|
+
transportRequestId: payload.transportRequestId,
|
|
2876
|
+
consumeRequestId: payload.consumeRequestId,
|
|
2877
|
+
providerRequestId: payload.providerRequestId
|
|
2878
|
+
});
|
|
2135
2879
|
}
|
|
2136
2880
|
function isOrderSuccess(status) {
|
|
2137
2881
|
switch (status) {
|
|
@@ -2365,6 +3109,13 @@ var Client = class _Client {
|
|
|
2365
3109
|
tokens = null;
|
|
2366
3110
|
/** token 持久化 */
|
|
2367
3111
|
store;
|
|
3112
|
+
credentialMode;
|
|
3113
|
+
versionedCredentialStore;
|
|
3114
|
+
accessTokenProvider;
|
|
3115
|
+
beforeCredentialInstall;
|
|
3116
|
+
credentialAuthority;
|
|
3117
|
+
credentialRequestOwner;
|
|
3118
|
+
lifecycle = null;
|
|
2368
3119
|
/** fetch 实现 (默认 globalThis.fetch) */
|
|
2369
3120
|
fetchImpl;
|
|
2370
3121
|
/** 互斥锁 (TS 用 Promise chain 替代 sync.Mutex) */
|
|
@@ -2395,6 +3146,7 @@ var Client = class _Client {
|
|
|
2395
3146
|
/** V29 系数缓存 (TTL 8s, listCoefficients 内部用) */
|
|
2396
3147
|
coefCacheData = null;
|
|
2397
3148
|
coefCacheTimeMs = 0;
|
|
3149
|
+
credentialOwnerKey = null;
|
|
2398
3150
|
/** 串行化锁 (替代 Go sync.Mutex) */
|
|
2399
3151
|
coefMu = Promise.resolve();
|
|
2400
3152
|
constructor(cfg = {}) {
|
|
@@ -2405,8 +3157,92 @@ var Client = class _Client {
|
|
|
2405
3157
|
this.oauthMetadataProfile = cfg.oauthMetadataProfile ?? "desktop";
|
|
2406
3158
|
this.browserRefreshMode = cfg.browserRefreshMode ?? "direct";
|
|
2407
3159
|
this.refreshProxyURL = cfg.refreshProxyURL ?? null;
|
|
2408
|
-
this.
|
|
3160
|
+
this.credentialMode = cfg.credentialMode ?? "legacy";
|
|
3161
|
+
this.credentialAuthority = new URL(this.serverURL).origin;
|
|
3162
|
+
if (this.credentialMode === "versioned" && !cfg.versionedCredentialStore) {
|
|
3163
|
+
throw new Error("versionedCredentialStore is required for credentialMode=versioned");
|
|
3164
|
+
}
|
|
3165
|
+
if (cfg.credentialRequestOwner && this.credentialMode !== "versioned") {
|
|
3166
|
+
throw new Error("credentialRequestOwner requires credentialMode=versioned");
|
|
3167
|
+
}
|
|
3168
|
+
if (this.credentialMode === "legacy" && cfg.versionedCredentialStore) {
|
|
3169
|
+
throw new Error("credentialMode=versioned is required when versionedCredentialStore is provided");
|
|
3170
|
+
}
|
|
3171
|
+
if (this.credentialMode === "external" && cfg.versionedCredentialStore) {
|
|
3172
|
+
throw new Error("versionedCredentialStore is incompatible with credentialMode=external");
|
|
3173
|
+
}
|
|
3174
|
+
if (this.credentialMode !== "external" && cfg.accessTokenProvider) {
|
|
3175
|
+
throw new Error("accessTokenProvider requires credentialMode=external");
|
|
3176
|
+
}
|
|
3177
|
+
if (this.credentialMode === "external" && !cfg.accessTokenProvider) {
|
|
3178
|
+
throw new Error("accessTokenProvider is required for credentialMode=external");
|
|
3179
|
+
}
|
|
3180
|
+
if (this.credentialMode !== "versioned" && cfg.beforeCredentialInstall) {
|
|
3181
|
+
throw new Error("beforeCredentialInstall requires credentialMode=versioned");
|
|
3182
|
+
}
|
|
3183
|
+
if (this.credentialMode !== "legacy") {
|
|
3184
|
+
const authority = new URL(this.serverURL).origin;
|
|
3185
|
+
for (const [name, override] of [["apiBaseURL", this.apiBaseURL], ["complianceBaseURL", this.complianceBaseURL]]) {
|
|
3186
|
+
if (override && new URL(override).origin !== authority) {
|
|
3187
|
+
throw new Error(`${name} must use the credential authority ${authority}`);
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
this.accessTokenProvider = cfg.accessTokenProvider;
|
|
3192
|
+
this.beforeCredentialInstall = cfg.beforeCredentialInstall;
|
|
3193
|
+
this.credentialRequestOwner = cfg.credentialRequestOwner ? {
|
|
3194
|
+
storeInstanceId: cfg.credentialRequestOwner.storeInstanceId,
|
|
3195
|
+
authSessionId: cfg.credentialRequestOwner.authSessionId,
|
|
3196
|
+
principal: cfg.credentialRequestOwner.principal ? {
|
|
3197
|
+
issuer: cfg.credentialRequestOwner.principal.issuer,
|
|
3198
|
+
subject: cfg.credentialRequestOwner.principal.subject,
|
|
3199
|
+
organizationId: cfg.credentialRequestOwner.principal.organizationId
|
|
3200
|
+
} : null
|
|
3201
|
+
} : null;
|
|
2409
3202
|
this.fetchImpl = cfg.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
3203
|
+
this.versionedCredentialStore = cfg.versionedCredentialStore ?? null;
|
|
3204
|
+
if (this.versionedCredentialStore) this.lifecycle = new CredentialLifecycle(this.versionedCredentialStore, {
|
|
3205
|
+
serverURL: this.serverURL,
|
|
3206
|
+
fetch: this.fetchImpl,
|
|
3207
|
+
metadata: (signal) => discoverWithProfile(this.serverURL, "desktop", signal, this.fetchImpl),
|
|
3208
|
+
profile: async (tokens, signal) => {
|
|
3209
|
+
const gatewayRoot = this.serverURL.replace(/\/api\/v4$/, "");
|
|
3210
|
+
const profileURL = `${gatewayRoot}/api/oauth/profile`;
|
|
3211
|
+
this.assertCredentialURL(profileURL);
|
|
3212
|
+
const response = await this.fetchImpl(profileURL, { headers: { Authorization: `Bearer ${tokens.access_token}` }, signal });
|
|
3213
|
+
if (!response.ok) {
|
|
3214
|
+
const bodyBytes = response.body ? await readLimited(response.body, maxErrorBodySize) : new Uint8Array();
|
|
3215
|
+
const failure = parseHTTPErrorWithHeader(response.status, bodyBytes, response.headers);
|
|
3216
|
+
const contract = readGatewayErrorContract(failure);
|
|
3217
|
+
const code = contract?.errorCode ?? null;
|
|
3218
|
+
if (code === "INVALID_SCOPE") throw new Error("invalid_scope");
|
|
3219
|
+
if (code === "AUTH_CONTRACT_UNSUPPORTED") throw new Error("auth_contract_unsupported");
|
|
3220
|
+
if (code === "ACCOUNT_NOT_FOUND") throw new Error("account_permission");
|
|
3221
|
+
throw new Error("identity_unavailable");
|
|
3222
|
+
}
|
|
3223
|
+
const body = await response.json();
|
|
3224
|
+
const subject = body.account?.uuid;
|
|
3225
|
+
if (typeof subject !== "string" || !subject) throw new Error("invalid_response");
|
|
3226
|
+
const account = body.account;
|
|
3227
|
+
const organization = body.organization;
|
|
3228
|
+
return {
|
|
3229
|
+
subject,
|
|
3230
|
+
organizationId: organization?.uuid || null,
|
|
3231
|
+
...typeof account.display_name === "string" ? { displayName: account.display_name } : {},
|
|
3232
|
+
...[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") } : {},
|
|
3233
|
+
...typeof account.email === "string" ? { email: account.email } : {},
|
|
3234
|
+
...typeof account.image_url === "string" ? { imageUrl: account.image_url } : {},
|
|
3235
|
+
...typeof account.created_at === "string" ? { accountCreatedAt: account.created_at } : {},
|
|
3236
|
+
...typeof account.requires_phone_binding === "boolean" ? { requiresPhoneBinding: account.requires_phone_binding } : {},
|
|
3237
|
+
...typeof organization?.has_extra_usage_enabled === "boolean" ? { hasExtraUsageEnabled: organization.has_extra_usage_enabled } : {},
|
|
3238
|
+
...typeof organization?.billing_type === "string" ? { billingType: organization.billing_type } : {},
|
|
3239
|
+
...typeof organization?.subscription_created_at === "string" ? { subscriptionCreatedAt: organization.subscription_created_at } : {},
|
|
3240
|
+
...typeof organization?.rate_limit_tier === "string" ? { rateLimitTier: organization.rate_limit_tier } : {},
|
|
3241
|
+
...typeof organization?.name === "string" ? { organizationName: organization.name } : {}
|
|
3242
|
+
};
|
|
3243
|
+
}
|
|
3244
|
+
});
|
|
3245
|
+
this.store = cfg.store ?? defaultTokenStore();
|
|
2410
3246
|
this.retryPolicy = effectivePolicy(cfg.retryPolicy ?? null);
|
|
2411
3247
|
}
|
|
2412
3248
|
/**
|
|
@@ -2415,6 +3251,11 @@ var Client = class _Client {
|
|
|
2415
3251
|
*/
|
|
2416
3252
|
static async create(cfg = {}) {
|
|
2417
3253
|
const c = new _Client(cfg);
|
|
3254
|
+
if (c.credentialMode === "external") return c;
|
|
3255
|
+
if (c.credentialMode === "versioned") {
|
|
3256
|
+
await c.reconcileCredentials();
|
|
3257
|
+
return c;
|
|
3258
|
+
}
|
|
2418
3259
|
try {
|
|
2419
3260
|
const tokens = await c.store.load();
|
|
2420
3261
|
if (tokens) {
|
|
@@ -2428,6 +3269,63 @@ var Client = class _Client {
|
|
|
2428
3269
|
}
|
|
2429
3270
|
return c;
|
|
2430
3271
|
}
|
|
3272
|
+
/** Read the durable authority. Available only in explicit versioned mode. */
|
|
3273
|
+
async getCredentialSnapshot(signal) {
|
|
3274
|
+
if (!this.versionedCredentialStore) {
|
|
3275
|
+
throw new Error("getCredentialSnapshot requires credentialMode=versioned");
|
|
3276
|
+
}
|
|
3277
|
+
return this.versionedCredentialStore.readSnapshot(signal);
|
|
3278
|
+
}
|
|
3279
|
+
/** Reconcile memory from durable state; storage errors never clear confirmed memory. */
|
|
3280
|
+
async reconcileCredentials(signal) {
|
|
3281
|
+
const snapshot = await this.lifecycle.reconcile(signal);
|
|
3282
|
+
this.adoptCredentialOwner(snapshot);
|
|
3283
|
+
this.tokens = snapshot.credentialState === "ready" ? snapshot.tokenSet : null;
|
|
3284
|
+
return snapshot;
|
|
3285
|
+
}
|
|
3286
|
+
ownerKey(snapshot) {
|
|
3287
|
+
return `${snapshot.storeInstanceId}\0${snapshot.authSessionId ?? ""}\0${snapshot.principal?.issuer ?? ""}\0${snapshot.principal?.subject ?? ""}\0${snapshot.principal?.organizationId ?? ""}`;
|
|
3288
|
+
}
|
|
3289
|
+
adoptCredentialOwner(snapshot) {
|
|
3290
|
+
const next = this.ownerKey(snapshot);
|
|
3291
|
+
if (this.credentialOwnerKey !== null && this.credentialOwnerKey !== next) {
|
|
3292
|
+
this.modelCache = [];
|
|
3293
|
+
this.modelCacheTimeMs = 0;
|
|
3294
|
+
this.coefCacheData = null;
|
|
3295
|
+
this.coefCacheTimeMs = 0;
|
|
3296
|
+
}
|
|
3297
|
+
this.credentialOwnerKey = next;
|
|
3298
|
+
}
|
|
3299
|
+
async ensureCredential(signal) {
|
|
3300
|
+
if (!this.lifecycle) return this.ensureToken(signal);
|
|
3301
|
+
const token = await this.lifecycle.ensure(signal, void 0, this.credentialRequestOwner ?? void 0);
|
|
3302
|
+
const snapshot = await this.lifecycle.read(signal);
|
|
3303
|
+
this.adoptCredentialOwner(snapshot);
|
|
3304
|
+
this.tokens = snapshot.credentialState === "ready" ? snapshot.tokenSet : null;
|
|
3305
|
+
return token;
|
|
3306
|
+
}
|
|
3307
|
+
subscribeCredentialState(listener) {
|
|
3308
|
+
if (!this.lifecycle) throw new Error("subscribeCredentialState requires credentialMode=versioned");
|
|
3309
|
+
return this.lifecycle.subscribe(listener);
|
|
3310
|
+
}
|
|
3311
|
+
async retryCredentialIdentity(signal) {
|
|
3312
|
+
const snapshot = await this.getCredentialSnapshot(signal);
|
|
3313
|
+
if (!snapshot.authSessionId) throw new Error("not authorized");
|
|
3314
|
+
const ready = await this.lifecycle.bindIdentity(snapshot.authSessionId, signal);
|
|
3315
|
+
this.adoptCredentialOwner(ready);
|
|
3316
|
+
this.tokens = ready.credentialState === "ready" ? ready.tokenSet : null;
|
|
3317
|
+
return ready;
|
|
3318
|
+
}
|
|
3319
|
+
async logoutCredential(signal, expected) {
|
|
3320
|
+
if (!this.lifecycle) throw new Error("logoutCredential requires credentialMode=versioned");
|
|
3321
|
+
const result = await this.lifecycle.logout(signal, expected);
|
|
3322
|
+
if (result.status === "committed" || result.status === "already_signed_out") {
|
|
3323
|
+
const current = await this.lifecycle.read(signal);
|
|
3324
|
+
this.adoptCredentialOwner(current);
|
|
3325
|
+
this.tokens = null;
|
|
3326
|
+
}
|
|
3327
|
+
return result;
|
|
3328
|
+
}
|
|
2431
3329
|
// ===========================================================================
|
|
2432
3330
|
// 授权生命周期
|
|
2433
3331
|
// ===========================================================================
|
|
@@ -2478,6 +3376,44 @@ var Client = class _Client {
|
|
|
2478
3376
|
return this.loginInternal(appName, scopes, { handler, ...opts }, signal);
|
|
2479
3377
|
}
|
|
2480
3378
|
async loginInternal(appName, scopes, opts, signal) {
|
|
3379
|
+
if (this.lifecycle) {
|
|
3380
|
+
const attempt = await this.lifecycle.reserveLogin(signal);
|
|
3381
|
+
let installHookRejected = false;
|
|
3382
|
+
try {
|
|
3383
|
+
const registration = await register(attempt.metadata, appName, signal, this.fetchImpl);
|
|
3384
|
+
await this.lifecycle.assertLogin(attempt.attemptId, signal);
|
|
3385
|
+
const authorization = await authorize(attempt.metadata, registration.client_id, scopes, { ...opts, handler: opts?.handler ?? void 0, signal });
|
|
3386
|
+
await this.lifecycle.assertLogin(attempt.attemptId, signal);
|
|
3387
|
+
const response = await exchangeCode(attempt.metadata, registration.client_id, authorization.result.code, authorization.result.redirectURI, authorization.verifier, signal, this.fetchImpl);
|
|
3388
|
+
if (!Number.isFinite(response.expires_in) || response.expires_in <= 0) throw new Error("invalid_response");
|
|
3389
|
+
await this.lifecycle.assertLogin(attempt.attemptId, signal);
|
|
3390
|
+
try {
|
|
3391
|
+
await this.beforeCredentialInstall?.({
|
|
3392
|
+
accessToken: response.access_token,
|
|
3393
|
+
attemptId: attempt.attemptId,
|
|
3394
|
+
serverURL: this.serverURL,
|
|
3395
|
+
clientId: registration.client_id
|
|
3396
|
+
}, signal);
|
|
3397
|
+
} catch (error) {
|
|
3398
|
+
installHookRejected = true;
|
|
3399
|
+
throw error;
|
|
3400
|
+
}
|
|
3401
|
+
await this.lifecycle.assertLogin(attempt.attemptId, signal);
|
|
3402
|
+
const ready = await this.lifecycle.installLogin(attempt.attemptId, newTokenSet(response, registration.client_id, this.serverURL), signal);
|
|
3403
|
+
const current = await this.getCredentialSnapshot(signal);
|
|
3404
|
+
if (current.revision !== ready.revision || current.authSessionId !== ready.authSessionId) throw new Error("superseded");
|
|
3405
|
+
this.tokens = ready.tokenSet;
|
|
3406
|
+
opts?.handler?.({ type: EventComplete, attemptId: attempt.attemptId });
|
|
3407
|
+
} catch (error) {
|
|
3408
|
+
const current = await this.getCredentialSnapshot();
|
|
3409
|
+
if (current.loginAttempt?.attemptId === attempt.attemptId || current.lastLoginAttemptId === attempt.attemptId) {
|
|
3410
|
+
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" });
|
|
3411
|
+
}
|
|
3412
|
+
await this.lifecycle.cancelLogin(attempt.attemptId);
|
|
3413
|
+
throw error;
|
|
3414
|
+
}
|
|
3415
|
+
return;
|
|
3416
|
+
}
|
|
2481
3417
|
const handler = opts?.handler ?? void 0;
|
|
2482
3418
|
const emit = (e) => {
|
|
2483
3419
|
if (handler) handler(e);
|
|
@@ -2586,6 +3522,11 @@ var Client = class _Client {
|
|
|
2586
3522
|
}
|
|
2587
3523
|
/** 吊销 token 并清除本地存储 */
|
|
2588
3524
|
async logout(signal) {
|
|
3525
|
+
if (this.lifecycle) {
|
|
3526
|
+
await this.logoutCredential(signal);
|
|
3527
|
+
await this.reconcileCredentials(signal);
|
|
3528
|
+
return;
|
|
3529
|
+
}
|
|
2589
3530
|
const tokens = this.tokens;
|
|
2590
3531
|
let meta = this.meta;
|
|
2591
3532
|
this.tokens = null;
|
|
@@ -2623,6 +3564,13 @@ var Client = class _Client {
|
|
|
2623
3564
|
* 避免应用启动期 "login + 多个 API 调用" 并发场景下 4+ 条 "not authorized" 误报.
|
|
2624
3565
|
*/
|
|
2625
3566
|
async ensureToken(signal) {
|
|
3567
|
+
if (this.credentialMode === "external") {
|
|
3568
|
+
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
3569
|
+
const token = await this.accessTokenProvider(signal);
|
|
3570
|
+
if (typeof token !== "string" || token.length === 0) throw new Error("external access token unavailable");
|
|
3571
|
+
return token;
|
|
3572
|
+
}
|
|
3573
|
+
if (this.lifecycle) return this.lifecycle.ensure(signal, void 0, this.credentialRequestOwner ?? void 0);
|
|
2626
3574
|
let tokens = this.tokens;
|
|
2627
3575
|
const ready = this.tokenReady.promise;
|
|
2628
3576
|
const inFlight = this.loginInFlight;
|
|
@@ -2670,7 +3618,14 @@ var Client = class _Client {
|
|
|
2670
3618
|
);
|
|
2671
3619
|
}
|
|
2672
3620
|
/** 强制刷新 token (用于 401 重试) */
|
|
2673
|
-
async forceRefresh(signal) {
|
|
3621
|
+
async forceRefresh(signal, rejectedToken) {
|
|
3622
|
+
if (this.credentialMode === "external") throw new Error("external credentials cannot be refreshed by the SDK");
|
|
3623
|
+
if (this.lifecycle) {
|
|
3624
|
+
await this.lifecycle.forceRefresh(signal, rejectedToken, this.credentialRequestOwner ?? void 0);
|
|
3625
|
+
const snapshot = await this.lifecycle.read(signal);
|
|
3626
|
+
this.tokens = snapshot.credentialState === "ready" ? snapshot.tokenSet : null;
|
|
3627
|
+
return;
|
|
3628
|
+
}
|
|
2674
3629
|
return this.withMu(
|
|
2675
3630
|
() => this.storeWithLock(async () => {
|
|
2676
3631
|
await this.syncFromDisk();
|
|
@@ -2865,6 +3820,7 @@ var Client = class _Client {
|
|
|
2865
3820
|
*/
|
|
2866
3821
|
async listModelsWithStatus(signal, opts) {
|
|
2867
3822
|
const includeLocked = opts?.includeLocked === true;
|
|
3823
|
+
const requestOwner = this.lifecycle ? this.ownerKey(await this.getCredentialSnapshot(signal)) : null;
|
|
2868
3824
|
const path = includeLocked ? "/managed-models?picker=1" : "/managed-models";
|
|
2869
3825
|
const { result, headers } = await this.doJSONFull(
|
|
2870
3826
|
"GET",
|
|
@@ -2873,6 +3829,11 @@ var Client = class _Client {
|
|
|
2873
3829
|
signal
|
|
2874
3830
|
);
|
|
2875
3831
|
const normalized = normalizeInputModalities(result.data);
|
|
3832
|
+
if (requestOwner !== null) {
|
|
3833
|
+
const current = await this.getCredentialSnapshot(signal);
|
|
3834
|
+
if (this.ownerKey(current) !== requestOwner) throw new Error("superseded");
|
|
3835
|
+
this.adoptCredentialOwner(current);
|
|
3836
|
+
}
|
|
2876
3837
|
if (!includeLocked) {
|
|
2877
3838
|
this.modelCache = normalized;
|
|
2878
3839
|
this.modelCacheTimeMs = Date.now();
|
|
@@ -3292,12 +4253,11 @@ var Client = class _Client {
|
|
|
3292
4253
|
throw classifyTransport("POST " + endpoint, url, e);
|
|
3293
4254
|
}
|
|
3294
4255
|
if (resp.status === 401 && !retried) {
|
|
4256
|
+
const bodyBytes = resp.body ? await readLimited(resp.body, maxErrorBodySize) : new Uint8Array();
|
|
4257
|
+
const authError = parseHTTPErrorWithHeader(resp.status, bodyBytes, resp.headers);
|
|
4258
|
+
if (!isUserAccessTokenRejected(authError)) throw authError;
|
|
3295
4259
|
try {
|
|
3296
|
-
await
|
|
3297
|
-
} catch {
|
|
3298
|
-
}
|
|
3299
|
-
try {
|
|
3300
|
-
await this.forceRefresh(signal);
|
|
4260
|
+
await this.forceRefresh(signal, token);
|
|
3301
4261
|
} catch (refreshErr) {
|
|
3302
4262
|
throw new Error(
|
|
3303
4263
|
`stream: unauthorized and refresh failed: ${refreshErr instanceof Error ? refreshErr.message : String(refreshErr)}`
|
|
@@ -3363,12 +4323,11 @@ var Client = class _Client {
|
|
|
3363
4323
|
throw classifyTransport("POST " + endpoint, url, e);
|
|
3364
4324
|
}
|
|
3365
4325
|
if (resp.status === 401 && !retried) {
|
|
4326
|
+
const bodyBytes = resp.body ? await readLimited(resp.body, maxErrorBodySize) : new Uint8Array();
|
|
4327
|
+
const authError = parseHTTPErrorWithHeader(resp.status, bodyBytes, resp.headers);
|
|
4328
|
+
if (!isUserAccessTokenRejected(authError)) throw authError;
|
|
3366
4329
|
try {
|
|
3367
|
-
await
|
|
3368
|
-
} catch {
|
|
3369
|
-
}
|
|
3370
|
-
try {
|
|
3371
|
-
await this.forceRefresh(signal);
|
|
4330
|
+
await this.forceRefresh(signal, token);
|
|
3372
4331
|
} catch (refreshErr) {
|
|
3373
4332
|
throw new Error(
|
|
3374
4333
|
`messages stream: unauthorized and refresh failed: ${refreshErr instanceof Error ? refreshErr.message : String(refreshErr)}`
|
|
@@ -3458,7 +4417,9 @@ var Client = class _Client {
|
|
|
3458
4417
|
if (!base.endsWith("/api/v4")) {
|
|
3459
4418
|
base += "/api/v4";
|
|
3460
4419
|
}
|
|
3461
|
-
|
|
4420
|
+
const url = base + path;
|
|
4421
|
+
this.assertCredentialURL(url);
|
|
4422
|
+
return url;
|
|
3462
4423
|
}
|
|
3463
4424
|
/**
|
|
3464
4425
|
* Compliance API URL 拼接。
|
|
@@ -3471,7 +4432,14 @@ var Client = class _Client {
|
|
|
3471
4432
|
*/
|
|
3472
4433
|
complianceURL(path) {
|
|
3473
4434
|
const base = this.complianceBaseURL ?? this.serverURL + "/admin-api";
|
|
3474
|
-
|
|
4435
|
+
const url = base + path;
|
|
4436
|
+
this.assertCredentialURL(url);
|
|
4437
|
+
return url;
|
|
4438
|
+
}
|
|
4439
|
+
assertCredentialURL(url) {
|
|
4440
|
+
if (this.credentialMode !== "legacy" && new URL(url).origin !== this.credentialAuthority) {
|
|
4441
|
+
throw new Error("credential request authority changed");
|
|
4442
|
+
}
|
|
3475
4443
|
}
|
|
3476
4444
|
/** GET/POST/... 通用 JSON 调用 (返回 result 已 typed) */
|
|
3477
4445
|
async doJSON(method, path, body, signal) {
|
|
@@ -3500,12 +4468,11 @@ var Client = class _Client {
|
|
|
3500
4468
|
ctl.signal
|
|
3501
4469
|
);
|
|
3502
4470
|
if (resp.status === 401 && !retried) {
|
|
4471
|
+
const bodyBytes = resp.body ? await readLimited(resp.body, maxErrorBodySize) : new Uint8Array();
|
|
4472
|
+
const authError = parseHTTPErrorWithHeader(resp.status, bodyBytes, resp.headers);
|
|
4473
|
+
if (!isUserAccessTokenRejected(authError)) throw authError;
|
|
3503
4474
|
try {
|
|
3504
|
-
await
|
|
3505
|
-
} catch {
|
|
3506
|
-
}
|
|
3507
|
-
try {
|
|
3508
|
-
await this.forceRefresh(ctl.signal);
|
|
4475
|
+
await this.forceRefresh(ctl.signal, token);
|
|
3509
4476
|
} catch (refreshErr) {
|
|
3510
4477
|
throw new Error(
|
|
3511
4478
|
`unauthorized and refresh failed: ${refreshErr instanceof Error ? refreshErr.message : String(refreshErr)}`
|
|
@@ -3570,12 +4537,11 @@ var Client = class _Client {
|
|
|
3570
4537
|
ctl.signal
|
|
3571
4538
|
);
|
|
3572
4539
|
if (resp.status === 401 && !retried) {
|
|
4540
|
+
const bodyBytes = resp.body ? await readLimited(resp.body, maxErrorBodySize) : new Uint8Array();
|
|
4541
|
+
const authError = parseHTTPErrorWithHeader(resp.status, bodyBytes, resp.headers);
|
|
4542
|
+
if (!isUserAccessTokenRejected(authError)) throw authError;
|
|
3573
4543
|
try {
|
|
3574
|
-
await
|
|
3575
|
-
} catch {
|
|
3576
|
-
}
|
|
3577
|
-
try {
|
|
3578
|
-
await this.forceRefresh(ctl.signal);
|
|
4544
|
+
await this.forceRefresh(ctl.signal, token);
|
|
3579
4545
|
} catch (refreshErr) {
|
|
3580
4546
|
throw new Error(
|
|
3581
4547
|
`unauthorized and refresh failed: ${refreshErr instanceof Error ? refreshErr.message : String(refreshErr)}`
|
|
@@ -3650,6 +4616,7 @@ var Client = class _Client {
|
|
|
3650
4616
|
* 6 处原始 fetch() 全部走此 helper
|
|
3651
4617
|
*/
|
|
3652
4618
|
async doRequest(req, signal) {
|
|
4619
|
+
if (new Headers(req.headers).has("Authorization")) this.assertCredentialURL(req.url);
|
|
3653
4620
|
try {
|
|
3654
4621
|
return await this.fetchImpl(req.url, {
|
|
3655
4622
|
method: req.method,
|
|
@@ -4647,12 +5614,11 @@ async function uploadSkillInternal(c, zipData, scope, intent, retried, signal) {
|
|
|
4647
5614
|
throw classifyTransport("POST /skill-store/upload", url, e);
|
|
4648
5615
|
}
|
|
4649
5616
|
if (resp.status === 401 && !retried) {
|
|
5617
|
+
const bodyBytes = resp.body ? await readLimited(resp.body, maxErrorBodySize) : new Uint8Array();
|
|
5618
|
+
const authError = parseHTTPErrorWithHeader(resp.status, bodyBytes, resp.headers);
|
|
5619
|
+
if (!isUserAccessTokenRejected(authError)) throw authError;
|
|
4650
5620
|
try {
|
|
4651
|
-
await
|
|
4652
|
-
} catch {
|
|
4653
|
-
}
|
|
4654
|
-
try {
|
|
4655
|
-
await c.forceRefresh(ctl.signal);
|
|
5621
|
+
await c.forceRefresh(ctl.signal, token);
|
|
4656
5622
|
} catch (refreshErr) {
|
|
4657
5623
|
throw new Error(
|
|
4658
5624
|
`upload: unauthorized and refresh failed: ${refreshErr instanceof Error ? refreshErr.message : String(refreshErr)}`
|
|
@@ -4823,9 +5789,7 @@ Client.prototype.updateNotificationPreference = async function(typeCode, pref, s
|
|
|
4823
5789
|
|
|
4824
5790
|
// src/notifications/ws.ts
|
|
4825
5791
|
Client.prototype.connect = async function(cfg, signal) {
|
|
4826
|
-
|
|
4827
|
-
await this.disconnect();
|
|
4828
|
-
}
|
|
5792
|
+
const oldDisconnect = this.ws ? this.disconnect() : Promise.resolve();
|
|
4829
5793
|
const noop = () => {
|
|
4830
5794
|
};
|
|
4831
5795
|
const filledCfg = {
|
|
@@ -4841,21 +5805,31 @@ Client.prototype.connect = async function(cfg, signal) {
|
|
|
4841
5805
|
const done = new Promise((r) => {
|
|
4842
5806
|
resolveDone = r;
|
|
4843
5807
|
});
|
|
4844
|
-
const
|
|
5808
|
+
const abort2 = new AbortController();
|
|
4845
5809
|
if (signal) {
|
|
4846
|
-
if (signal.aborted)
|
|
4847
|
-
else signal.addEventListener("abort", () =>
|
|
5810
|
+
if (signal.aborted) abort2.abort();
|
|
5811
|
+
else signal.addEventListener("abort", () => abort2.abort());
|
|
4848
5812
|
}
|
|
4849
5813
|
const ws = {
|
|
4850
5814
|
conn: null,
|
|
4851
5815
|
cfg: filledCfg,
|
|
4852
|
-
abort,
|
|
5816
|
+
abort: abort2,
|
|
4853
5817
|
done,
|
|
4854
5818
|
doneResolve: resolveDone,
|
|
4855
|
-
connected: false
|
|
5819
|
+
connected: false,
|
|
5820
|
+
owner: null
|
|
4856
5821
|
};
|
|
4857
|
-
await wsConnectOnce(this, ws);
|
|
4858
5822
|
this.ws = ws;
|
|
5823
|
+
try {
|
|
5824
|
+
await oldDisconnect;
|
|
5825
|
+
await assertCurrent(this, ws);
|
|
5826
|
+
await wsConnectOnce(this, ws);
|
|
5827
|
+
} catch (error) {
|
|
5828
|
+
ws.abort.abort();
|
|
5829
|
+
if (this.ws === ws) this.ws = null;
|
|
5830
|
+
ws.doneResolve();
|
|
5831
|
+
throw error;
|
|
5832
|
+
}
|
|
4859
5833
|
void wsLoop(this, ws);
|
|
4860
5834
|
};
|
|
4861
5835
|
Client.prototype.disconnect = async function() {
|
|
@@ -4893,15 +5867,47 @@ function getWebSocketCtor() {
|
|
|
4893
5867
|
}
|
|
4894
5868
|
return WSCtor;
|
|
4895
5869
|
}
|
|
5870
|
+
function sameOwner(a, b) {
|
|
5871
|
+
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;
|
|
5872
|
+
}
|
|
5873
|
+
async function readOwner(c, signal) {
|
|
5874
|
+
if (c.credentialMode !== "versioned") return null;
|
|
5875
|
+
const snapshot = await c.getCredentialSnapshot(signal);
|
|
5876
|
+
return {
|
|
5877
|
+
storeInstanceId: snapshot.storeInstanceId,
|
|
5878
|
+
authSessionId: snapshot.authSessionId,
|
|
5879
|
+
principal: snapshot.principal
|
|
5880
|
+
};
|
|
5881
|
+
}
|
|
5882
|
+
async function assertCurrent(c, ws) {
|
|
5883
|
+
if (ws.abort.signal.aborted || c.ws !== ws) throw new Error("websocket connection superseded");
|
|
5884
|
+
if (ws.owner !== null && !sameOwner(ws.owner, await readOwner(c, ws.abort.signal))) {
|
|
5885
|
+
ws.abort.abort();
|
|
5886
|
+
throw new Error("websocket credential owner changed");
|
|
5887
|
+
}
|
|
5888
|
+
}
|
|
4896
5889
|
async function wsConnectOnce(c, ws) {
|
|
5890
|
+
const observedOwner = await readOwner(c, ws.abort.signal);
|
|
5891
|
+
if (ws.owner === null) ws.owner = observedOwner;
|
|
5892
|
+
else if (!sameOwner(ws.owner, observedOwner)) throw new Error("websocket credential owner changed");
|
|
5893
|
+
await assertCurrent(c, ws);
|
|
4897
5894
|
const url = wsURL(c);
|
|
4898
5895
|
const WSCtor = getWebSocketCtor();
|
|
4899
|
-
const
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
5896
|
+
const token = await c.ensureToken(ws.abort.signal);
|
|
5897
|
+
await assertCurrent(c, ws);
|
|
5898
|
+
const ticketURL = c.apiURL("/ws/stream-ticket");
|
|
5899
|
+
await assertCurrent(c, ws);
|
|
5900
|
+
const ticketHTTP = await c.doRequest({
|
|
5901
|
+
method: "POST",
|
|
5902
|
+
url: ticketURL,
|
|
5903
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
5904
|
+
}, ws.abort.signal);
|
|
5905
|
+
if (!ticketHTTP.ok) {
|
|
5906
|
+
const body = ticketHTTP.body ? await readLimited(ticketHTTP.body, maxErrorBodySize) : new Uint8Array();
|
|
5907
|
+
throw parseHTTPErrorWithHeader(ticketHTTP.status, body, ticketHTTP.headers);
|
|
5908
|
+
}
|
|
5909
|
+
const ticketResp = await ticketHTTP.json();
|
|
5910
|
+
await assertCurrent(c, ws);
|
|
4905
5911
|
const ticket = ticketResp.data.ticket;
|
|
4906
5912
|
const u = new URL(url);
|
|
4907
5913
|
u.searchParams.set("ticket", ticket);
|
|
@@ -4922,11 +5928,28 @@ async function wsConnectOnce(c, ws) {
|
|
|
4922
5928
|
reject(new Error("dial: handshake timeout"));
|
|
4923
5929
|
}
|
|
4924
5930
|
}, 3e4);
|
|
5931
|
+
const abortHandshake = () => {
|
|
5932
|
+
clearTimeout(handshakeTimer);
|
|
5933
|
+
try {
|
|
5934
|
+
conn.close();
|
|
5935
|
+
} catch {
|
|
5936
|
+
}
|
|
5937
|
+
reject(new Error("websocket connection aborted"));
|
|
5938
|
+
};
|
|
5939
|
+
ws.abort.signal.addEventListener("abort", abortHandshake, { once: true });
|
|
4925
5940
|
conn.addEventListener("open", () => {
|
|
5941
|
+
if (ws.abort.signal.aborted || c.ws !== ws) {
|
|
5942
|
+
try {
|
|
5943
|
+
conn.close();
|
|
5944
|
+
} catch {
|
|
5945
|
+
}
|
|
5946
|
+
return;
|
|
5947
|
+
}
|
|
4926
5948
|
opened = true;
|
|
4927
5949
|
});
|
|
4928
5950
|
conn.addEventListener("error", (e) => {
|
|
4929
5951
|
clearTimeout(handshakeTimer);
|
|
5952
|
+
ws.abort.signal.removeEventListener("abort", abortHandshake);
|
|
4930
5953
|
reject(new Error(`dial: ${e.message ?? "connection error"}`));
|
|
4931
5954
|
});
|
|
4932
5955
|
conn.addEventListener("message", (e) => {
|
|
@@ -4935,6 +5958,7 @@ async function wsConnectOnce(c, ws) {
|
|
|
4935
5958
|
const welcome = JSON.parse(msg);
|
|
4936
5959
|
if (welcome.type !== "welcome") {
|
|
4937
5960
|
clearTimeout(handshakeTimer);
|
|
5961
|
+
ws.abort.signal.removeEventListener("abort", abortHandshake);
|
|
4938
5962
|
try {
|
|
4939
5963
|
conn.close();
|
|
4940
5964
|
} catch {
|
|
@@ -4942,31 +5966,34 @@ async function wsConnectOnce(c, ws) {
|
|
|
4942
5966
|
reject(new Error(`unexpected first message: ${welcome.type}`));
|
|
4943
5967
|
return;
|
|
4944
5968
|
}
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
JSON.stringify({
|
|
4952
|
-
type: "subscribe",
|
|
4953
|
-
topics: ws.cfg.topics
|
|
4954
|
-
})
|
|
4955
|
-
);
|
|
4956
|
-
} catch (sendErr) {
|
|
4957
|
-
ws.conn = null;
|
|
4958
|
-
ws.connected = false;
|
|
5969
|
+
void assertCurrent(c, ws).then(() => {
|
|
5970
|
+
clearTimeout(handshakeTimer);
|
|
5971
|
+
ws.abort.signal.removeEventListener("abort", abortHandshake);
|
|
5972
|
+
ws.conn = conn;
|
|
5973
|
+
ws.connected = true;
|
|
5974
|
+
if (ws.cfg.topics.length > 0) {
|
|
4959
5975
|
try {
|
|
4960
|
-
conn.
|
|
4961
|
-
|
|
5976
|
+
conn.send(
|
|
5977
|
+
JSON.stringify({
|
|
5978
|
+
type: "subscribe",
|
|
5979
|
+
topics: ws.cfg.topics
|
|
5980
|
+
})
|
|
5981
|
+
);
|
|
5982
|
+
} catch (sendErr) {
|
|
5983
|
+
ws.conn = null;
|
|
5984
|
+
ws.connected = false;
|
|
5985
|
+
try {
|
|
5986
|
+
conn.close();
|
|
5987
|
+
} catch {
|
|
5988
|
+
}
|
|
5989
|
+
reject(new Error(`send subscribe: ${sendErr instanceof Error ? sendErr.message : String(sendErr)}`));
|
|
5990
|
+
return;
|
|
4962
5991
|
}
|
|
4963
|
-
reject(new Error(`send subscribe: ${sendErr instanceof Error ? sendErr.message : String(sendErr)}`));
|
|
4964
|
-
return;
|
|
4965
5992
|
}
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
5993
|
+
ws.cfg.onConnect();
|
|
5994
|
+
console.log(`[acosmi-sdk] websocket connected, connId=${welcome.connId ?? ""}`);
|
|
5995
|
+
resolve();
|
|
5996
|
+
}).catch(reject);
|
|
4970
5997
|
} catch (parseErr) {
|
|
4971
5998
|
clearTimeout(handshakeTimer);
|
|
4972
5999
|
try {
|
|
@@ -4981,7 +6008,7 @@ async function wsConnectOnce(c, ws) {
|
|
|
4981
6008
|
async function wsLoop(c, ws) {
|
|
4982
6009
|
try {
|
|
4983
6010
|
while (true) {
|
|
4984
|
-
await wsReadLoop(ws);
|
|
6011
|
+
await wsReadLoop(c, ws);
|
|
4985
6012
|
if (ws.abort.signal.aborted) return;
|
|
4986
6013
|
if (ws.conn) {
|
|
4987
6014
|
try {
|
|
@@ -5012,7 +6039,7 @@ async function wsLoop(c, ws) {
|
|
|
5012
6039
|
ws.doneResolve();
|
|
5013
6040
|
}
|
|
5014
6041
|
}
|
|
5015
|
-
async function wsReadLoop(ws) {
|
|
6042
|
+
async function wsReadLoop(c, ws) {
|
|
5016
6043
|
const conn = ws.conn;
|
|
5017
6044
|
if (!conn) return;
|
|
5018
6045
|
return new Promise((resolve) => {
|
|
@@ -5020,10 +6047,17 @@ async function wsReadLoop(ws) {
|
|
|
5020
6047
|
try {
|
|
5021
6048
|
const data = e.data;
|
|
5022
6049
|
const event = JSON.parse(data);
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
6050
|
+
void assertCurrent(c, ws).then(() => {
|
|
6051
|
+
try {
|
|
6052
|
+
ws.cfg.onEvent(event);
|
|
6053
|
+
} catch {
|
|
6054
|
+
}
|
|
6055
|
+
}).catch(() => {
|
|
6056
|
+
try {
|
|
6057
|
+
conn.close();
|
|
6058
|
+
} catch {
|
|
6059
|
+
}
|
|
6060
|
+
});
|
|
5027
6061
|
} catch {
|
|
5028
6062
|
}
|
|
5029
6063
|
};
|
|
@@ -5236,6 +6270,7 @@ function isTerminalRemoteEvent(ev) {
|
|
|
5236
6270
|
}
|
|
5237
6271
|
|
|
5238
6272
|
// src/agent-runs/client.ts
|
|
6273
|
+
init_errors();
|
|
5239
6274
|
var agentRunsByClient = /* @__PURE__ */ new WeakMap();
|
|
5240
6275
|
Object.defineProperty(Client.prototype, "agentRuns", {
|
|
5241
6276
|
configurable: true,
|
|
@@ -5615,11 +6650,10 @@ var AgentRunsClient = class {
|
|
|
5615
6650
|
}
|
|
5616
6651
|
const resp = await this.client.doRequest({ method, url, headers, body: bodyStr }, signal);
|
|
5617
6652
|
if (resp.status === 401 && opts.retryOn401 && !retried) {
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
await this.client.forceRefresh(signal);
|
|
6653
|
+
const bodyBytes = resp.body ? await readLimited(resp.body, maxErrorBodySize) : new Uint8Array();
|
|
6654
|
+
const authError = parseHTTPErrorWithHeader(resp.status, bodyBytes, resp.headers);
|
|
6655
|
+
if (!isUserAccessTokenRejected(authError)) throw authError;
|
|
6656
|
+
await this.client.forceRefresh(signal, token);
|
|
5623
6657
|
return this.requestRawInner(method, path, body, signal, opts, true);
|
|
5624
6658
|
}
|
|
5625
6659
|
if (resp.status < 200 || resp.status >= 300) {
|
|
@@ -6246,6 +7280,7 @@ function isComplianceBusinessError(err) {
|
|
|
6246
7280
|
}
|
|
6247
7281
|
|
|
6248
7282
|
// src/compliance/client.ts
|
|
7283
|
+
init_errors();
|
|
6249
7284
|
var cache = /* @__PURE__ */ new WeakMap();
|
|
6250
7285
|
Object.defineProperty(Client.prototype, "compliance", {
|
|
6251
7286
|
configurable: true,
|
|
@@ -7062,11 +8097,10 @@ var ComplianceClient = class {
|
|
|
7062
8097
|
}
|
|
7063
8098
|
const resp = await this.client.doRequest({ method, url, headers, body: bodyStr }, signal);
|
|
7064
8099
|
if (resp.status === 401 && opts.retryOn401 && !retried) {
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
await this.client.forceRefresh(signal);
|
|
8100
|
+
const bodyBytes = resp.body ? await readLimited(resp.body, maxErrorBodySize) : new Uint8Array();
|
|
8101
|
+
const authError = parseHTTPErrorWithHeader(resp.status, bodyBytes, resp.headers);
|
|
8102
|
+
if (!isUserAccessTokenRejected(authError)) throw authError;
|
|
8103
|
+
await this.client.forceRefresh(signal, token);
|
|
7070
8104
|
return this.executeJsonInner(method, path, body, signal, opts, true);
|
|
7071
8105
|
}
|
|
7072
8106
|
if (resp.status < 200 || resp.status >= 300) {
|
|
@@ -7987,6 +9021,7 @@ exports.isRegion = isRegion;
|
|
|
7987
9021
|
exports.isSSECommentLine = isSSECommentLine;
|
|
7988
9022
|
exports.isSSLError = isSSLError;
|
|
7989
9023
|
exports.isTerminalRemoteEvent = isTerminalRemoteEvent;
|
|
9024
|
+
exports.isUserAccessTokenRejected = isUserAccessTokenRejected;
|
|
7990
9025
|
exports.isValidTokenSet = isValidTokenSet;
|
|
7991
9026
|
exports.isWindowLimitError = isWindowLimitError;
|
|
7992
9027
|
exports.isWindowLimitStreamError = isWindowLimitStreamError;
|
|
@@ -8004,6 +9039,7 @@ exports.parseNotificationEvent = parseNotificationEvent;
|
|
|
8004
9039
|
exports.parseRemoteControlEvent = parseRemoteControlEvent;
|
|
8005
9040
|
exports.parseSettlement = parseSettlement;
|
|
8006
9041
|
exports.parseSourcesEvent = parseSourcesEvent;
|
|
9042
|
+
exports.readGatewayErrorContract = readGatewayErrorContract;
|
|
8007
9043
|
exports.refreshToken = refreshToken;
|
|
8008
9044
|
exports.register = register;
|
|
8009
9045
|
exports.registerWebOAuthClient = registerWebOAuthClient;
|