@acosmi/sdk-ts 2.19.6 → 2.20.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 +23 -0
- package/README.md +4 -2
- package/dist/browser/index.mjs +100 -53
- package/dist/browser/index.mjs.map +1 -1
- package/dist/index.mjs +100 -53
- package/dist/index.mjs.map +1 -1
- package/dist/node/adapters/anthropic.cjs.map +1 -1
- package/dist/node/adapters/anthropic.d.cts +1 -1
- package/dist/node/adapters/anthropic.d.ts +1 -1
- package/dist/node/adapters/anthropic.mjs.map +1 -1
- package/dist/node/adapters/openai.cjs.map +1 -1
- package/dist/node/adapters/openai.d.cts +2 -2
- package/dist/node/adapters/openai.d.ts +2 -2
- package/dist/node/adapters/openai.mjs.map +1 -1
- package/dist/node/{format-CKpXSSqY.d.cts → format-BBXu82tQ.d.cts} +67 -2
- package/dist/node/{format-CKpXSSqY.d.ts → format-BBXu82tQ.d.ts} +67 -2
- package/dist/node/index.cjs +100 -53
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.d.cts +4 -4
- package/dist/node/index.d.ts +4 -4
- package/dist/node/index.mjs +100 -53
- package/dist/node/index.mjs.map +1 -1
- package/dist/node/{openai-CH0wnJXv.d.ts → openai-BNJe_o8L.d.ts} +1 -1
- package/dist/node/{openai-ByUO-cSR.d.cts → openai-DauG8pfK.d.cts} +1 -1
- package/docs//345/274/200/345/217/221/344/270/216/345/217/221/345/270/203/346/211/213/345/206/214.md +15 -1
- package/package.json +1 -1
|
@@ -256,8 +256,25 @@ interface ManagedModel {
|
|
|
256
256
|
defaultToolIds?: string[];
|
|
257
257
|
/** 该模型对当前用户档位是否被限制策略锁定 (limit_policy allowedModels 不含)。true=展示但置灰+升级引导。enforced 关/解析失败=false。 */
|
|
258
258
|
locked?: boolean;
|
|
259
|
-
/**
|
|
259
|
+
/**
|
|
260
|
+
* 是否属于"免费会员可用"分区 (PLAN:FREE 档 allowedModels 内)。与当前用户档位无关 (仅分类)。
|
|
261
|
+
*
|
|
262
|
+
* **语义不变, 但不再是展示分区的依据**: 订阅双池之后, 「哪些模型先扣精选免费池」由
|
|
263
|
+
* {@link ManagedModel.freePoolEligible} 表达, 与 FREE 档的 allowedModels 是两套独立的集合
|
|
264
|
+
* (两者可以不一致: 某模型可以 `freePoolEligible: true` 而 `freeTier: false`, 反之亦然)。
|
|
265
|
+
* 新代码不要再用本字段分区或打"精选"角标。字段保留只为老客户端零破坏 (方案 D-14)。
|
|
266
|
+
*/
|
|
260
267
|
freeTier?: boolean;
|
|
268
|
+
/**
|
|
269
|
+
* [订阅双池 P5 / D-14] 该模型当前是否有资格先扣**精选免费池 F** (池类 FREE, 或仍在限免期内的 PROMO_FREE)。
|
|
270
|
+
* 网关随 `effective-model-policy` 的 `freePoolModels` 投影下发, 与 {@link QuotaSummaryFreePool.eligibleModels}
|
|
271
|
+
* 同源。用于模型选择器的「精选」角标。
|
|
272
|
+
*
|
|
273
|
+
* **缺席 = 老网关 (还没有双池), 不是 `false`**: `true` = 有资格; `false` = 明确没有 (走付费池 P);
|
|
274
|
+
* `undefined` = 网关不知道这件事。展示端只在 `=== true` 时画角标, 绝不在 `!== false` 时画。
|
|
275
|
+
* 限免到期是随时间翻转的: 同一模型今天 `true`、截止后 `false`, 不要缓存跨过 `promo_until`。
|
|
276
|
+
*/
|
|
277
|
+
freePoolEligible?: boolean;
|
|
261
278
|
/**
|
|
262
279
|
* 上游 gateway 为此模型启用的请求格式列表
|
|
263
280
|
* 取值: "anthropic" | "openai"
|
|
@@ -380,10 +397,42 @@ interface QuotaSummarySubscriptionPool {
|
|
|
380
397
|
*/
|
|
381
398
|
boosterNextExpiresAt?: string;
|
|
382
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* 精选免费池视图 — QuotaSummary.freePool 的形状 (订阅双池 P5 / D-14, SDK 2.20.0 起)。
|
|
402
|
+
*
|
|
403
|
+
* 付费合同里随每个订阅周期一起发放的**精选免费池 F**: 只能被池类为 FREE / 限免中的 PROMO_FREE 的模型
|
|
404
|
+
* 先扣, 用完后同一请求自动溢出扣付费池 P ({@link QuotaSummarySubscriptionPool}, 语义不变)。
|
|
405
|
+
* quota / used / remaining 的单位与 subscriptionPool 一致, 为**微 Credits** (÷1000 = Credits)。
|
|
406
|
+
*
|
|
407
|
+
* **和 `freeTotalEtu` / `freeBuckets` 不是一回事**: 后者是 GENERIC (注册赠送 / 邀请奖励 / 试用)
|
|
408
|
+
* 桶合计, 与订阅合同无关; `freePool` 是付费合同自带的那一半额度。两者不可相加, 也不可互相替代。
|
|
409
|
+
*/
|
|
410
|
+
interface QuotaSummaryFreePool {
|
|
411
|
+
/** 池总额度 (微 Credits) */
|
|
412
|
+
quota: number;
|
|
413
|
+
/** 已用额度 (微 Credits) */
|
|
414
|
+
used: number;
|
|
415
|
+
/** 剩余额度 (微 Credits) */
|
|
416
|
+
remaining: number;
|
|
417
|
+
/** 到期时间 (ISO-8601); 网关未下发时缺失, 口径同 {@link QuotaSummarySubscriptionPool.expiresAt} */
|
|
418
|
+
expiresAt?: string;
|
|
419
|
+
/** 恒为 `'MICRO_CREDIT'` */
|
|
420
|
+
unit: 'MICRO_CREDIT';
|
|
421
|
+
/**
|
|
422
|
+
* 当前有资格扣本池的模型 id 集合 = 池类 FREE ∪ 仍在限免期的 PROMO_FREE (如 `deepseek-flash`)。
|
|
423
|
+
* **可以是空数组**: `[]` = 网关明确说「此刻没有任何模型有资格」, 与字段缺席不是一回事。
|
|
424
|
+
*/
|
|
425
|
+
eligibleModels: string[];
|
|
426
|
+
}
|
|
427
|
+
/** 计量口径: `OFFICIAL_CNY` = 1 Credit = ¥0.000001 官方价 (新合同); `COST_CNY` = 成本口径 (老合同)。 */
|
|
428
|
+
type QuotaSummaryCreditUnit = 'OFFICIAL_CNY' | 'COST_CNY';
|
|
383
429
|
/**
|
|
384
430
|
* GET /api/v4/entitlements/quota-summary 返回体, 调用 client.getQuotaSummary 获取.
|
|
385
431
|
*
|
|
386
432
|
* 设计目的: 个人中心钱包栏一次性展示"我还有多少免费 + 多少付费"+ 各自最近到期时间.
|
|
433
|
+
*
|
|
434
|
+
* 注意「免费」一词有两层含义: `freeTotalEtu` / `freeBuckets` 是 GENERIC 赠送桶 (与合同无关);
|
|
435
|
+
* 订阅合同自带的精选免费池是 {@link QuotaSummary.freePool}。
|
|
387
436
|
*/
|
|
388
437
|
interface QuotaSummary {
|
|
389
438
|
/** GENERIC (免费/赠送) alive 桶 tokenRemaining 求和 */
|
|
@@ -410,6 +459,22 @@ interface QuotaSummary {
|
|
|
410
459
|
* (÷1000 = Credits)。缺席 = 老网关, 或当前没有 alive 付费桶 —— 两种都不可当 0 处理。
|
|
411
460
|
*/
|
|
412
461
|
subscriptionPool?: QuotaSummarySubscriptionPool;
|
|
462
|
+
/**
|
|
463
|
+
* [订阅双池 P5 / D-14, 2.20.0] 精选免费池 F。`subscriptionPool` 的语义**不变**, 恒为付费池 P。
|
|
464
|
+
*
|
|
465
|
+
* **缺席 = 老合同 (没有 F), 或老网关 (还不认识 F) —— 两种都不可当 0 读**: 把缺席画成
|
|
466
|
+
* 「精选额度 0 / 已用满」是在向老合同用户断言一件不存在的事。展示端只在 `freePool !== undefined`
|
|
467
|
+
* 时画第二条进度条。
|
|
468
|
+
*/
|
|
469
|
+
freePool?: QuotaSummaryFreePool;
|
|
470
|
+
/**
|
|
471
|
+
* [订阅双池 P5 / D-14, 2.20.0] 本响应里所有 Credit 数值的计量口径, 让客户端把 Credits 换算成 ¥ 时能标注口径。
|
|
472
|
+
* `OFFICIAL_CNY` = 新合同 (1 Credit = ¥0.000001 官方价); `COST_CNY` = 老合同 (成本口径)。
|
|
473
|
+
*
|
|
474
|
+
* **缺席 = 老网关, 不能当任一取值读**; 尤其不能默认成 `OFFICIAL_CNY` 去乘 ¥ 单价。
|
|
475
|
+
* 类型是闭集, 但网关将来新增口径时旧版 SDK 会原样透出未知字符串 —— 消费端对未知值应按「不换算」处理。
|
|
476
|
+
*/
|
|
477
|
+
creditUnit?: QuotaSummaryCreditUnit;
|
|
413
478
|
}
|
|
414
479
|
/**
|
|
415
480
|
* GET /api/v4/entitlements/window-reset/summary 返回体, 调用 client.getWindowResetSummary 获取.
|
|
@@ -705,4 +770,4 @@ interface ProviderAdapter {
|
|
|
705
770
|
};
|
|
706
771
|
}
|
|
707
772
|
|
|
708
|
-
export {
|
|
773
|
+
export { bucketInfoIsCommercial as $, type SourcesEventParseResult as A, BucketClassCommercial as B, type ChatContentBlock as C, type StreamEvent as D, type EffortConfig as E, type StreamSettlement as F, type GeoLoc as G, ThinkingHigh as H, type ImageGenerationRequest as I, ThinkingHighMinMaxTokens as J, ThinkingLow as K, ThinkingMax as L, type ManagedModel as M, ThinkingMaxFallbackMaxTokens as N, type OutputConfig as O, type ProviderAdapter as P, type QuotaSummary as Q, type RerankDocument as R, type ServerTool as S, type ThinkingConfig as T, ThinkingOff as U, type VideoGenerationRequest as V, type VideoTaskResponse as W, type WebSearchConfig as X, type WebSearchSource as Y, type WindowLimitStatus as Z, type WindowResetSummary as _, BucketClassGeneric as a, bucketRowIsCommercial as a0, classifySourcesEvent as a1, newThinkingConfig as a2, newWebSearchTool as a3, parseSettlement as a4, parseSourcesEvent as a5, type BucketInfo as b, type BucketRow as c, type ChatMessage as d, type ChatRequest as e, type ChatResponse as f, type ChatUsage as g, type EmbeddingData as h, type EmbeddingRequest as i, type EmbeddingResponse as j, type ImageGenerationResponse as k, type InputModality as l, type InputModalityTag as m, type ModelCapabilities as n, type MultimodalContent as o, ProviderFormat as p, type QuotaSummaryCreditUnit as q, type QuotaSummaryFreePool as r, type QuotaSummarySubscriptionPool as s, type RerankQuery as t, type RerankRequest as u, type RerankResponse as v, type RerankResult as w, ServerToolTypeWebSearch as x, type SourcesEvent as y, type SourcesEventIssueCode as z };
|
|
@@ -256,8 +256,25 @@ interface ManagedModel {
|
|
|
256
256
|
defaultToolIds?: string[];
|
|
257
257
|
/** 该模型对当前用户档位是否被限制策略锁定 (limit_policy allowedModels 不含)。true=展示但置灰+升级引导。enforced 关/解析失败=false。 */
|
|
258
258
|
locked?: boolean;
|
|
259
|
-
/**
|
|
259
|
+
/**
|
|
260
|
+
* 是否属于"免费会员可用"分区 (PLAN:FREE 档 allowedModels 内)。与当前用户档位无关 (仅分类)。
|
|
261
|
+
*
|
|
262
|
+
* **语义不变, 但不再是展示分区的依据**: 订阅双池之后, 「哪些模型先扣精选免费池」由
|
|
263
|
+
* {@link ManagedModel.freePoolEligible} 表达, 与 FREE 档的 allowedModels 是两套独立的集合
|
|
264
|
+
* (两者可以不一致: 某模型可以 `freePoolEligible: true` 而 `freeTier: false`, 反之亦然)。
|
|
265
|
+
* 新代码不要再用本字段分区或打"精选"角标。字段保留只为老客户端零破坏 (方案 D-14)。
|
|
266
|
+
*/
|
|
260
267
|
freeTier?: boolean;
|
|
268
|
+
/**
|
|
269
|
+
* [订阅双池 P5 / D-14] 该模型当前是否有资格先扣**精选免费池 F** (池类 FREE, 或仍在限免期内的 PROMO_FREE)。
|
|
270
|
+
* 网关随 `effective-model-policy` 的 `freePoolModels` 投影下发, 与 {@link QuotaSummaryFreePool.eligibleModels}
|
|
271
|
+
* 同源。用于模型选择器的「精选」角标。
|
|
272
|
+
*
|
|
273
|
+
* **缺席 = 老网关 (还没有双池), 不是 `false`**: `true` = 有资格; `false` = 明确没有 (走付费池 P);
|
|
274
|
+
* `undefined` = 网关不知道这件事。展示端只在 `=== true` 时画角标, 绝不在 `!== false` 时画。
|
|
275
|
+
* 限免到期是随时间翻转的: 同一模型今天 `true`、截止后 `false`, 不要缓存跨过 `promo_until`。
|
|
276
|
+
*/
|
|
277
|
+
freePoolEligible?: boolean;
|
|
261
278
|
/**
|
|
262
279
|
* 上游 gateway 为此模型启用的请求格式列表
|
|
263
280
|
* 取值: "anthropic" | "openai"
|
|
@@ -380,10 +397,42 @@ interface QuotaSummarySubscriptionPool {
|
|
|
380
397
|
*/
|
|
381
398
|
boosterNextExpiresAt?: string;
|
|
382
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* 精选免费池视图 — QuotaSummary.freePool 的形状 (订阅双池 P5 / D-14, SDK 2.20.0 起)。
|
|
402
|
+
*
|
|
403
|
+
* 付费合同里随每个订阅周期一起发放的**精选免费池 F**: 只能被池类为 FREE / 限免中的 PROMO_FREE 的模型
|
|
404
|
+
* 先扣, 用完后同一请求自动溢出扣付费池 P ({@link QuotaSummarySubscriptionPool}, 语义不变)。
|
|
405
|
+
* quota / used / remaining 的单位与 subscriptionPool 一致, 为**微 Credits** (÷1000 = Credits)。
|
|
406
|
+
*
|
|
407
|
+
* **和 `freeTotalEtu` / `freeBuckets` 不是一回事**: 后者是 GENERIC (注册赠送 / 邀请奖励 / 试用)
|
|
408
|
+
* 桶合计, 与订阅合同无关; `freePool` 是付费合同自带的那一半额度。两者不可相加, 也不可互相替代。
|
|
409
|
+
*/
|
|
410
|
+
interface QuotaSummaryFreePool {
|
|
411
|
+
/** 池总额度 (微 Credits) */
|
|
412
|
+
quota: number;
|
|
413
|
+
/** 已用额度 (微 Credits) */
|
|
414
|
+
used: number;
|
|
415
|
+
/** 剩余额度 (微 Credits) */
|
|
416
|
+
remaining: number;
|
|
417
|
+
/** 到期时间 (ISO-8601); 网关未下发时缺失, 口径同 {@link QuotaSummarySubscriptionPool.expiresAt} */
|
|
418
|
+
expiresAt?: string;
|
|
419
|
+
/** 恒为 `'MICRO_CREDIT'` */
|
|
420
|
+
unit: 'MICRO_CREDIT';
|
|
421
|
+
/**
|
|
422
|
+
* 当前有资格扣本池的模型 id 集合 = 池类 FREE ∪ 仍在限免期的 PROMO_FREE (如 `deepseek-flash`)。
|
|
423
|
+
* **可以是空数组**: `[]` = 网关明确说「此刻没有任何模型有资格」, 与字段缺席不是一回事。
|
|
424
|
+
*/
|
|
425
|
+
eligibleModels: string[];
|
|
426
|
+
}
|
|
427
|
+
/** 计量口径: `OFFICIAL_CNY` = 1 Credit = ¥0.000001 官方价 (新合同); `COST_CNY` = 成本口径 (老合同)。 */
|
|
428
|
+
type QuotaSummaryCreditUnit = 'OFFICIAL_CNY' | 'COST_CNY';
|
|
383
429
|
/**
|
|
384
430
|
* GET /api/v4/entitlements/quota-summary 返回体, 调用 client.getQuotaSummary 获取.
|
|
385
431
|
*
|
|
386
432
|
* 设计目的: 个人中心钱包栏一次性展示"我还有多少免费 + 多少付费"+ 各自最近到期时间.
|
|
433
|
+
*
|
|
434
|
+
* 注意「免费」一词有两层含义: `freeTotalEtu` / `freeBuckets` 是 GENERIC 赠送桶 (与合同无关);
|
|
435
|
+
* 订阅合同自带的精选免费池是 {@link QuotaSummary.freePool}。
|
|
387
436
|
*/
|
|
388
437
|
interface QuotaSummary {
|
|
389
438
|
/** GENERIC (免费/赠送) alive 桶 tokenRemaining 求和 */
|
|
@@ -410,6 +459,22 @@ interface QuotaSummary {
|
|
|
410
459
|
* (÷1000 = Credits)。缺席 = 老网关, 或当前没有 alive 付费桶 —— 两种都不可当 0 处理。
|
|
411
460
|
*/
|
|
412
461
|
subscriptionPool?: QuotaSummarySubscriptionPool;
|
|
462
|
+
/**
|
|
463
|
+
* [订阅双池 P5 / D-14, 2.20.0] 精选免费池 F。`subscriptionPool` 的语义**不变**, 恒为付费池 P。
|
|
464
|
+
*
|
|
465
|
+
* **缺席 = 老合同 (没有 F), 或老网关 (还不认识 F) —— 两种都不可当 0 读**: 把缺席画成
|
|
466
|
+
* 「精选额度 0 / 已用满」是在向老合同用户断言一件不存在的事。展示端只在 `freePool !== undefined`
|
|
467
|
+
* 时画第二条进度条。
|
|
468
|
+
*/
|
|
469
|
+
freePool?: QuotaSummaryFreePool;
|
|
470
|
+
/**
|
|
471
|
+
* [订阅双池 P5 / D-14, 2.20.0] 本响应里所有 Credit 数值的计量口径, 让客户端把 Credits 换算成 ¥ 时能标注口径。
|
|
472
|
+
* `OFFICIAL_CNY` = 新合同 (1 Credit = ¥0.000001 官方价); `COST_CNY` = 老合同 (成本口径)。
|
|
473
|
+
*
|
|
474
|
+
* **缺席 = 老网关, 不能当任一取值读**; 尤其不能默认成 `OFFICIAL_CNY` 去乘 ¥ 单价。
|
|
475
|
+
* 类型是闭集, 但网关将来新增口径时旧版 SDK 会原样透出未知字符串 —— 消费端对未知值应按「不换算」处理。
|
|
476
|
+
*/
|
|
477
|
+
creditUnit?: QuotaSummaryCreditUnit;
|
|
413
478
|
}
|
|
414
479
|
/**
|
|
415
480
|
* GET /api/v4/entitlements/window-reset/summary 返回体, 调用 client.getWindowResetSummary 获取.
|
|
@@ -705,4 +770,4 @@ interface ProviderAdapter {
|
|
|
705
770
|
};
|
|
706
771
|
}
|
|
707
772
|
|
|
708
|
-
export {
|
|
773
|
+
export { bucketInfoIsCommercial as $, type SourcesEventParseResult as A, BucketClassCommercial as B, type ChatContentBlock as C, type StreamEvent as D, type EffortConfig as E, type StreamSettlement as F, type GeoLoc as G, ThinkingHigh as H, type ImageGenerationRequest as I, ThinkingHighMinMaxTokens as J, ThinkingLow as K, ThinkingMax as L, type ManagedModel as M, ThinkingMaxFallbackMaxTokens as N, type OutputConfig as O, type ProviderAdapter as P, type QuotaSummary as Q, type RerankDocument as R, type ServerTool as S, type ThinkingConfig as T, ThinkingOff as U, type VideoGenerationRequest as V, type VideoTaskResponse as W, type WebSearchConfig as X, type WebSearchSource as Y, type WindowLimitStatus as Z, type WindowResetSummary as _, BucketClassGeneric as a, bucketRowIsCommercial as a0, classifySourcesEvent as a1, newThinkingConfig as a2, newWebSearchTool as a3, parseSettlement as a4, parseSourcesEvent as a5, type BucketInfo as b, type BucketRow as c, type ChatMessage as d, type ChatRequest as e, type ChatResponse as f, type ChatUsage as g, type EmbeddingData as h, type EmbeddingRequest as i, type EmbeddingResponse as j, type ImageGenerationResponse as k, type InputModality as l, type InputModalityTag as m, type ModelCapabilities as n, type MultimodalContent as o, ProviderFormat as p, type QuotaSummaryCreditUnit as q, type QuotaSummaryFreePool as r, type QuotaSummarySubscriptionPool as s, type RerankQuery as t, type RerankRequest as u, type RerankResponse as v, type RerankResult as w, ServerToolTypeWebSearch as x, type SourcesEvent as y, type SourcesEventIssueCode as z };
|
package/dist/node/index.cjs
CHANGED
|
@@ -1097,6 +1097,18 @@ var abort = (signal) => {
|
|
|
1097
1097
|
if (signal?.aborted) throw new Error("aborted");
|
|
1098
1098
|
};
|
|
1099
1099
|
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;
|
|
1100
|
+
var REFRESH_TEMPORARILY_UNAVAILABLE = "refresh_temporarily_unavailable";
|
|
1101
|
+
var REFRESH_RETRY_BASE_MS = 5e3;
|
|
1102
|
+
var REFRESH_RETRY_MAX_MS = 6e4;
|
|
1103
|
+
var REFRESH_RETRY_AFTER_CAP_MS = 3e5;
|
|
1104
|
+
function retryAfterMs(response) {
|
|
1105
|
+
const raw = response.headers.get("retry-after");
|
|
1106
|
+
if (!raw) return null;
|
|
1107
|
+
const seconds = Number(raw);
|
|
1108
|
+
const ms = Number.isFinite(seconds) ? seconds * 1e3 : Date.parse(raw) - Date.now();
|
|
1109
|
+
if (!Number.isFinite(ms)) return null;
|
|
1110
|
+
return Math.min(Math.max(ms, 0), REFRESH_RETRY_AFTER_CAP_MS);
|
|
1111
|
+
}
|
|
1100
1112
|
var CredentialLifecycle = class {
|
|
1101
1113
|
constructor(store, protocol) {
|
|
1102
1114
|
this.store = store;
|
|
@@ -1107,6 +1119,8 @@ var CredentialLifecycle = class {
|
|
|
1107
1119
|
listeners = /* @__PURE__ */ new Set();
|
|
1108
1120
|
notificationKey = "";
|
|
1109
1121
|
observations = /* @__PURE__ */ new Map();
|
|
1122
|
+
/** Last refusal of a refresh token that was not rotated; see `REFRESH_RETRY_BASE_MS`. */
|
|
1123
|
+
refreshDeferral = null;
|
|
1110
1124
|
read(signal) {
|
|
1111
1125
|
return this.store.readSnapshot(signal);
|
|
1112
1126
|
}
|
|
@@ -1296,6 +1310,19 @@ var CredentialLifecycle = class {
|
|
|
1296
1310
|
usable(s) {
|
|
1297
1311
|
return !!s.tokenSet && Date.parse(s.tokenSet.expires_at) > Date.now() + 3e5;
|
|
1298
1312
|
}
|
|
1313
|
+
refreshDeferred(s) {
|
|
1314
|
+
const d = this.refreshDeferral;
|
|
1315
|
+
return !!d && d.refreshToken === s.tokenSet?.refresh_token && Date.now() < d.until;
|
|
1316
|
+
}
|
|
1317
|
+
deferRefresh(refreshToken2, retryAfter) {
|
|
1318
|
+
const failures = this.refreshDeferral?.refreshToken === refreshToken2 ? this.refreshDeferral.failures + 1 : 1;
|
|
1319
|
+
const backoff = Math.min(REFRESH_RETRY_MAX_MS, REFRESH_RETRY_BASE_MS * 2 ** (failures - 1));
|
|
1320
|
+
this.refreshDeferral = {
|
|
1321
|
+
refreshToken: refreshToken2,
|
|
1322
|
+
failures,
|
|
1323
|
+
until: Date.now() + Math.max(backoff, retryAfter ?? 0)
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1299
1326
|
async bindIdentity(sessionId, signal) {
|
|
1300
1327
|
const s = await this.ensureSnapshot(signal, void 0, true);
|
|
1301
1328
|
if (s.authSessionId !== sessionId) throw new Error("superseded");
|
|
@@ -1409,6 +1436,10 @@ var CredentialLifecycle = class {
|
|
|
1409
1436
|
if (live && (refreshed || s.revision !== (rejected?.revision ?? initialRevision) && (rejected !== void 0 || s.tokenSet.access_token !== initialAccess)))
|
|
1410
1437
|
return s;
|
|
1411
1438
|
if (this.usable(s) && !rejected) return s;
|
|
1439
|
+
if (this.refreshDeferred(s)) {
|
|
1440
|
+
if (live && !rejected) return s;
|
|
1441
|
+
throw new Error(REFRESH_TEMPORARILY_UNAVAILABLE);
|
|
1442
|
+
}
|
|
1412
1443
|
const metadata = await this.metadata(s, signal);
|
|
1413
1444
|
abort(signal);
|
|
1414
1445
|
const operationId = uuid();
|
|
@@ -1431,7 +1462,13 @@ var CredentialLifecycle = class {
|
|
|
1431
1462
|
);
|
|
1432
1463
|
if (r.status !== "committed") continue;
|
|
1433
1464
|
const owner = this.refresh(r.snapshot, metadata);
|
|
1434
|
-
|
|
1465
|
+
try {
|
|
1466
|
+
await this.wait(owner, signal);
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
const deferred = error instanceof Error && error.message === REFRESH_TEMPORARILY_UNAVAILABLE;
|
|
1469
|
+
if (rejected || !deferred) throw error;
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1435
1472
|
refreshed = true;
|
|
1436
1473
|
rejected = void 0;
|
|
1437
1474
|
}
|
|
@@ -1472,59 +1509,9 @@ var CredentialLifecycle = class {
|
|
|
1472
1509
|
const s = r.snapshot;
|
|
1473
1510
|
const ctl = new AbortController();
|
|
1474
1511
|
const timeout = setTimeout(() => ctl.abort(), 3e4);
|
|
1475
|
-
let
|
|
1512
|
+
let outcome;
|
|
1476
1513
|
try {
|
|
1477
|
-
|
|
1478
|
-
(async () => {
|
|
1479
|
-
const response = await this.protocol.fetch(metadata.token_endpoint, {
|
|
1480
|
-
method: "POST",
|
|
1481
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1482
|
-
body: new URLSearchParams({
|
|
1483
|
-
grant_type: "refresh_token",
|
|
1484
|
-
client_id: s.tokenSet.client_id,
|
|
1485
|
-
refresh_token: s.tokenSet.refresh_token
|
|
1486
|
-
}),
|
|
1487
|
-
signal: ctl.signal
|
|
1488
|
-
});
|
|
1489
|
-
const body2 = await response.json();
|
|
1490
|
-
return { response, body: body2 };
|
|
1491
|
-
})(),
|
|
1492
|
-
ctl.signal
|
|
1493
|
-
);
|
|
1494
|
-
if (!result.response.ok) {
|
|
1495
|
-
const code = result.body.error;
|
|
1496
|
-
let reason = "refresh_outcome_unknown";
|
|
1497
|
-
let state = "reauth_required";
|
|
1498
|
-
if (code === "invalid_grant") reason = code;
|
|
1499
|
-
else if (["invalid_client", "invalid_scope", "unsupported_grant_type"].includes(code)) {
|
|
1500
|
-
reason = code;
|
|
1501
|
-
state = "configuration_error";
|
|
1502
|
-
} else if (result.body.rotationOutcome === "not_committed" && ["temporarily_unavailable", "server_error"].includes(code)) {
|
|
1503
|
-
await this.commit(s, {
|
|
1504
|
-
credentialState: s.refreshOperation.returnState,
|
|
1505
|
-
refreshOperation: null
|
|
1506
|
-
});
|
|
1507
|
-
throw new Error("refresh_temporarily_unavailable");
|
|
1508
|
-
}
|
|
1509
|
-
await this.commit(s, {
|
|
1510
|
-
credentialState: state,
|
|
1511
|
-
tokenSet: null,
|
|
1512
|
-
refreshOperation: null,
|
|
1513
|
-
reason
|
|
1514
|
-
});
|
|
1515
|
-
throw new Error(reason);
|
|
1516
|
-
}
|
|
1517
|
-
const body = result.body;
|
|
1518
|
-
if (!Number.isFinite(body.expires_in) || body.expires_in <= 0)
|
|
1519
|
-
throw new Error("invalid_response");
|
|
1520
|
-
tokens = {
|
|
1521
|
-
...s.tokenSet,
|
|
1522
|
-
access_token: body.access_token,
|
|
1523
|
-
refresh_token: body.refresh_token,
|
|
1524
|
-
expires_at: new Date(Date.now() + body.expires_in * 1e3).toISOString(),
|
|
1525
|
-
scope: body.scope ?? s.tokenSet.scope
|
|
1526
|
-
};
|
|
1527
|
-
this.validateTokens(tokens);
|
|
1514
|
+
outcome = await this.wait(this.exchange(metadata, s.tokenSet, ctl.signal), ctl.signal);
|
|
1528
1515
|
} catch (error) {
|
|
1529
1516
|
await this.commit(s, {
|
|
1530
1517
|
credentialState: "reauth_required",
|
|
@@ -1536,6 +1523,24 @@ var CredentialLifecycle = class {
|
|
|
1536
1523
|
} finally {
|
|
1537
1524
|
clearTimeout(timeout);
|
|
1538
1525
|
}
|
|
1526
|
+
if (outcome.kind === "not_committed") {
|
|
1527
|
+
this.deferRefresh(s.tokenSet.refresh_token, outcome.retryAfterMs);
|
|
1528
|
+
await this.commit(s, {
|
|
1529
|
+
credentialState: s.refreshOperation.returnState,
|
|
1530
|
+
refreshOperation: null
|
|
1531
|
+
});
|
|
1532
|
+
throw new Error(REFRESH_TEMPORARILY_UNAVAILABLE);
|
|
1533
|
+
}
|
|
1534
|
+
if (outcome.kind === "rejected") {
|
|
1535
|
+
await this.commit(s, {
|
|
1536
|
+
credentialState: outcome.state,
|
|
1537
|
+
tokenSet: null,
|
|
1538
|
+
refreshOperation: null,
|
|
1539
|
+
reason: outcome.reason
|
|
1540
|
+
});
|
|
1541
|
+
throw new Error(outcome.reason);
|
|
1542
|
+
}
|
|
1543
|
+
const tokens = outcome.tokens;
|
|
1539
1544
|
const started = performance.now(), mutationId = uuid();
|
|
1540
1545
|
for (; ; ) {
|
|
1541
1546
|
if (Date.now() >= Date.parse(s.refreshOperation.deadlineAt)) {
|
|
@@ -1558,6 +1563,7 @@ var CredentialLifecycle = class {
|
|
|
1558
1563
|
mutationId
|
|
1559
1564
|
);
|
|
1560
1565
|
if (result.status === "committed") {
|
|
1566
|
+
this.refreshDeferral = null;
|
|
1561
1567
|
await this.notify(result.snapshot);
|
|
1562
1568
|
return;
|
|
1563
1569
|
}
|
|
@@ -1569,6 +1575,47 @@ var CredentialLifecycle = class {
|
|
|
1569
1575
|
await pause();
|
|
1570
1576
|
}
|
|
1571
1577
|
}
|
|
1578
|
+
/**
|
|
1579
|
+
* One refresh-token exchange. Settles with what the authority said; rejects only when there is
|
|
1580
|
+
* no answer or the answer cannot be read, which leaves the rotation outcome unknown.
|
|
1581
|
+
*/
|
|
1582
|
+
async exchange(metadata, current, signal) {
|
|
1583
|
+
const response = await this.protocol.fetch(metadata.token_endpoint, {
|
|
1584
|
+
method: "POST",
|
|
1585
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1586
|
+
body: new URLSearchParams({
|
|
1587
|
+
grant_type: "refresh_token",
|
|
1588
|
+
client_id: current.client_id,
|
|
1589
|
+
refresh_token: current.refresh_token
|
|
1590
|
+
}),
|
|
1591
|
+
signal
|
|
1592
|
+
});
|
|
1593
|
+
if (response.status === 429)
|
|
1594
|
+
return { kind: "not_committed", retryAfterMs: retryAfterMs(response) };
|
|
1595
|
+
const body = await response.json();
|
|
1596
|
+
if (!response.ok) {
|
|
1597
|
+
const code = body.error;
|
|
1598
|
+
if (code === "invalid_grant")
|
|
1599
|
+
return { kind: "rejected", state: "reauth_required", reason: code };
|
|
1600
|
+
if (["invalid_client", "invalid_scope", "unsupported_grant_type"].includes(code))
|
|
1601
|
+
return { kind: "rejected", state: "configuration_error", reason: code };
|
|
1602
|
+
if (body.rotationOutcome === "not_committed" && ["temporarily_unavailable", "server_error"].includes(code))
|
|
1603
|
+
return { kind: "not_committed", retryAfterMs: retryAfterMs(response) };
|
|
1604
|
+
return { kind: "rejected", state: "reauth_required", reason: "refresh_outcome_unknown" };
|
|
1605
|
+
}
|
|
1606
|
+
const tokenResponse = body;
|
|
1607
|
+
if (!Number.isFinite(tokenResponse.expires_in) || tokenResponse.expires_in <= 0)
|
|
1608
|
+
throw new Error("invalid_response");
|
|
1609
|
+
const tokens = {
|
|
1610
|
+
...current,
|
|
1611
|
+
access_token: tokenResponse.access_token,
|
|
1612
|
+
refresh_token: tokenResponse.refresh_token,
|
|
1613
|
+
expires_at: new Date(Date.now() + tokenResponse.expires_in * 1e3).toISOString(),
|
|
1614
|
+
scope: tokenResponse.scope ?? current.scope
|
|
1615
|
+
};
|
|
1616
|
+
this.validateTokens(tokens);
|
|
1617
|
+
return { kind: "tokens", tokens };
|
|
1618
|
+
}
|
|
1572
1619
|
async revoke(tokens, metadata, authority) {
|
|
1573
1620
|
const ctl = new AbortController();
|
|
1574
1621
|
const timer = setTimeout(() => ctl.abort(), 3e4);
|