@acosmi/sdk-ts 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +100 -0
- package/README.md +12 -2
- package/dist/browser/{index.js → index.mjs} +192 -59
- package/dist/browser/index.mjs.map +1 -0
- package/dist/{index.js → index.mjs} +192 -59
- package/dist/index.mjs.map +1 -0
- package/dist/node/adapters/{anthropic.js → anthropic.mjs} +2 -2
- package/dist/node/adapters/anthropic.mjs.map +1 -0
- package/dist/node/adapters/{openai.js → openai.mjs} +2 -2
- package/dist/node/adapters/openai.mjs.map +1 -0
- package/dist/node/index.cjs +190 -56
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.d.cts +57 -12
- package/dist/node/index.d.ts +57 -12
- package/dist/node/{index.js → index.mjs} +192 -59
- package/dist/node/index.mjs.map +1 -0
- package/dist/node/sanitize/{index.js → index.mjs} +2 -2
- package/dist/node/sanitize/index.mjs.map +1 -0
- package/package.json +4 -3
- package/dist/browser/index.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/node/adapters/anthropic.js.map +0 -1
- package/dist/node/adapters/openai.js.map +0 -1
- package/dist/node/index.js.map +0 -1
- package/dist/node/sanitize/index.js.map +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@acosmi/sdk-ts` will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.2] — 2026-05-06
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **多进程共享 `~/.acosmi/tokens.json` 撞 `HTTP 400: refresh token not found` 根治** —
|
|
13
|
+
`Client.ensureToken` / `Client.forceRefresh` 在 `withMu` 临界区内从不 reload 磁盘,
|
|
14
|
+
导致 P1 完成 refresh token rotation 写盘后, P2 内存仍持旧 R0,下一次 refresh 必然
|
|
15
|
+
撞网关 400 invalid_grant。CrabCode TUI 多窗口 / `crabclawskill` 并发等典型场景命中。
|
|
16
|
+
|
|
17
|
+
双层修复:
|
|
18
|
+
|
|
19
|
+
- **Layer 1 — reload-before-refresh** (`src/client.ts`):新增 `Client.syncFromDisk()`
|
|
20
|
+
在 `ensureToken` / `forceRefresh` 进入临界区后立刻 `store.load()`,若磁盘
|
|
21
|
+
`refresh_token` 与内存不同则采纳磁盘新版,重判过期 — 未过期直接 fast-return
|
|
22
|
+
(跳过本进程多余 refresh, 同时避免拿已 invalidated 的 R0 撞网关)。
|
|
23
|
+
- **Layer 2 — 跨进程临界区** (`src/store.ts`):`TokenStore` 加可选
|
|
24
|
+
`withLock?<T>(fn): Promise<T>` 方法(向后兼容,自定义 store 不实现自动回退到 L1
|
|
25
|
+
窄窗口);`FileTokenStore` 实现 sidecar `<path>.lock` + `O_EXCL` 创建语义 + 60s
|
|
26
|
+
旧锁回收 + 30s 获取超时 + 30+jitter ms backoff,真正消除残余 TOCTOU。
|
|
27
|
+
`Client.storeWithLock(fn)` helper 把整段 `load → check → refresh → save` 包进
|
|
28
|
+
跨进程临界区。
|
|
29
|
+
|
|
30
|
+
- **`FileTokenStore.save` 改 atomic rename** (`src/store.ts`) — 写到
|
|
31
|
+
`<path>.tmp.<pid>.<ts>.<rand>` 后 `fs.rename` 到正式路径,POSIX 上 `rename(2)`
|
|
32
|
+
同分区原子, Windows 上 `ReplaceFile`。读端永远看到完整旧/新 JSON,不会读到截断半
|
|
33
|
+
文件(`Client.create.store.load` 在另一进程写入中间触发也不会 JSON parse 失败)。
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- **`fileLockDefaults` 公开常量** (`src/store.ts`) — 暴露 `acquireTimeoutMs` (30s) /
|
|
38
|
+
`staleMs` (60s) / `retryBaseMs` (30) / `retryJitterMs` (70),便于测试与诊断。
|
|
39
|
+
|
|
40
|
+
- **回归测试 7 项**:
|
|
41
|
+
- `test/auth/multi-process-refresh.test.ts` (4 项):双 Client 共享 FileTokenStore
|
|
42
|
+
的 rotation 竞态核心回归 / P2 磁盘新 RT 也过期需 refresh / forceRefresh 也走
|
|
43
|
+
syncFromDisk / 无 rotation 时 0 影响 v1.0.1 行为。
|
|
44
|
+
- `test/store/file-token-store.test.ts` (3 项 + 4 子):atomic save 终态完整 /
|
|
45
|
+
并发 save 不混合 / 双 store 实例临界区互斥 / 旧锁自动 break / 错误路径释放锁 /
|
|
46
|
+
in-process 串行化。
|
|
47
|
+
|
|
48
|
+
### Compatibility
|
|
49
|
+
|
|
50
|
+
- **无破坏性变更**:`TokenStore.withLock` 是可选方法,1.0.x 自定义 store 实现 0 改动。
|
|
51
|
+
- **API 兼容**:`Client.ensureToken` / `forceRefresh` 签名不变,`FileTokenStore` 构造器
|
|
52
|
+
不变。
|
|
53
|
+
- **行为兼容**:单进程场景与 v1.0.1 完全一致(磁盘 RT 与内存一致时 syncFromDisk 早返,
|
|
54
|
+
flock 单进程零竞争 ~1ms 开销)。
|
|
55
|
+
|
|
56
|
+
### Notes
|
|
57
|
+
|
|
58
|
+
- **Go SDK 镜像修复待发**:`acosmi-sdk-go` 同根因(`client.go:316-383` `ensureToken` +
|
|
59
|
+
`:2143-2168` `forceRefresh` + `store.go` `FileTokenStore` 缺 flock),将在 v0.19.1
|
|
60
|
+
对齐修复。
|
|
61
|
+
- **NFS / 跨机共享警告**:`O_EXCL` 在 NFS 上不保证原子。FileTokenStore 设计目标是本地
|
|
62
|
+
文件系统(用户家目录)。真要跨机共享 token, 应实现自定义 Keychain / 数据库 store。
|
|
63
|
+
|
|
64
|
+
## [1.0.1] — 2026-05-01
|
|
65
|
+
|
|
66
|
+
> **Released**: sdk 仓 commit `0d8c0a9` + tag `v1.0.1` → release.yml CI 全自动 npm publish。已通过 audit Part 2 实拉验证 (`npm i @acosmi/sdk-ts@1.0.1` consumer 视角 smoke `tsc --noEmit` 全绿,9 处 declare module 在 dist/node/index.d.ts 行 548/571/592/601/645/654/677/720/761 全包名)。
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- **Layer 1 — packaging**:`tsup.config.ts` 三 entry 显式声明 `outExtension: ({ format }) => ({ js: format === 'esm' ? '.mjs' : '.cjs' })`,让产物与 `package.json.exports` 8 处 `.mjs` 引用对账。修复 1.0.0 在 bun / Node ESM 下 `Cannot find module '@acosmi/sdk-ts'`。
|
|
71
|
+
- **Layer 2 — d.ts augmentation**:9 处 `declare module` 由相对路径(`'../client'` / `'./client'`)改为包名 `'@acosmi/sdk-ts'`:
|
|
72
|
+
- `src/client/{wallet,entitlements,packages,notifications,tools,skills}.ts`(6 处)
|
|
73
|
+
- `src/{ws,sanitize-bridge,bug-report}.ts`(3 处)
|
|
74
|
+
|
|
75
|
+
修复后 augmentation 在 consumer 视角合并到 inline `declare class Client`,50+ 方法(`getBalance` / `submitBugReport` / `browseSkills` / `getWalletStats` / `listNotifications` / `chat` 等)在 user 项目可正常 typecheck。
|
|
76
|
+
- **tsconfig.json**:附带加 `baseUrl` + `paths "@acosmi/sdk-ts": ["./src/client.ts"]`,让源码 typecheck 阶段 self-reference 也能合并到 class Client。
|
|
77
|
+
|
|
78
|
+
### Added
|
|
79
|
+
|
|
80
|
+
- **`scripts/smoke-pack.mjs`** + `prepublishOnly` 末尾追加 `&& npm run test:pack`:跨平台 Node 脚本(Windows + Linux/macOS),在 `npm publish` 前从 packed tarball 装临时 consumer 项目跑 `tsc --noEmit`,验证 9 处 augmentation 在 consumer 视角合并成功。拦截"源码 typecheck 过 / packed 产物 broken"模式(1.0.0 翻车的根因机制)。
|
|
81
|
+
|
|
82
|
+
## [1.0.0] — 2026-05-01 [DEPRECATED]
|
|
83
|
+
|
|
84
|
+
> **⚠ DEPRECATED**:双层 broken packaging。请升级到 1.0.1+:`npm install @acosmi/sdk-ts@latest`。
|
|
85
|
+
>
|
|
86
|
+
> 已通过 `npm deprecate @acosmi/sdk-ts@1.0.0 'broken packaging, use 1.0.1+'` 标记,安装时会显示 deprecation warning。
|
|
87
|
+
|
|
88
|
+
### 已知问题(已在 1.0.1 修复)
|
|
89
|
+
|
|
90
|
+
- `package.json.exports` 8 处 `.mjs` 引用指向不存在的文件(tsup 实际输出 `.js + .cjs`)— bun/Node ESM resolver 报 `Cannot find module`,仅 CJS `require()` 可用。
|
|
91
|
+
- 9 处 d.ts augmentation 用相对路径,packed 产物中路径不可解析 — `getBalance` / `submitBugReport` 等 50+ 方法在 consumer typecheck 时 TS2339(`Property X does not exist on type 'Client'`)。
|
|
92
|
+
|
|
93
|
+
### 端口源
|
|
94
|
+
|
|
95
|
+
- `acosmi-sdk-go` v1.0.0 全量端口
|
|
96
|
+
- 36/36 vitest 全绿,源码 typecheck/lint/build 0 错误
|
|
97
|
+
- 翻车机制:`prepublishOnly` 仅跑源码 typecheck/vitest/build,不验证 packed product 在 consumer 视角能否解析
|
|
98
|
+
|
|
99
|
+
[1.0.1]: https://github.com/acosmi/sdk-ts/releases/tag/v1.0.1
|
|
100
|
+
[1.0.0]: https://www.npmjs.com/package/@acosmi/sdk-ts/v/1.0.0
|
package/README.md
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
## 状态
|
|
8
8
|
|
|
9
9
|
- 端口源:[acosmi-sdk-go](https://github.com/acosmi/acosmi-sdk-go) v1.0.0(与 Go SDK 联动稳定测试版)
|
|
10
|
-
-
|
|
11
|
-
- 测试:36/36 vitest 全绿,typecheck/lint/build 0
|
|
10
|
+
- 当前版本:**1.0.1**(稳定测试版,与 Go SDK 联动 1.0.x;1.0.0 已 deprecate due to broken packaging,详见 [CHANGELOG](./CHANGELOG.md))
|
|
11
|
+
- 测试:36/36 vitest 全绿,typecheck/lint/build 0 错误;packed-tarball smoke (`npm run test:pack`) 在 prepublishOnly 闸内
|
|
12
|
+
- 包链接:[npm](https://www.npmjs.com/package/@acosmi/sdk-ts/v/1.0.1) · [tarball](https://registry.npmjs.org/@acosmi/sdk-ts/-/sdk-ts-1.0.1.tgz) · [GitHub Release](https://github.com/acosmi/sdk-ts/releases/tag/v1.0.1) · [provenance](https://registry.npmjs.org/-/npm/v1/attestations/@acosmi%2fsdk-ts@1.0.1)(SLSA v1,CI 自动签)
|
|
12
13
|
|
|
13
14
|
## 安装
|
|
14
15
|
|
|
@@ -242,6 +243,15 @@ npm test
|
|
|
242
243
|
npm run build
|
|
243
244
|
```
|
|
244
245
|
|
|
246
|
+
## 更新历史
|
|
247
|
+
|
|
248
|
+
完整变更日志见 [CHANGELOG.md](./CHANGELOG.md)。
|
|
249
|
+
|
|
250
|
+
| 版本 | 状态 | 概要 |
|
|
251
|
+
| --- | --- | --- |
|
|
252
|
+
| 1.0.1 | 当前稳定版 | 修复 1.0.0 双层 broken packaging:tsup 输出 `.mjs+.cjs` 与 exports 字段对齐;9 处 `declare module` 绑包名 `@acosmi/sdk-ts` 让 d.ts augmentation 在 consumer 视角合并;prepublishOnly 加 packed-tarball 烟测拦截"源码过 / 打包后 broken"。 |
|
|
253
|
+
| 1.0.0 | **deprecated** | 双层 broken:(1) `package.json.exports` 8 处 `.mjs` 引用与 tsup 默认 `.js+.cjs` 错位 → bun/Node ESM `Cannot find module`;(2) 9 处 `declare module` 用相对路径,consumer 视角断链 → 50+ 方法 TS2339。`npm install @acosmi/sdk-ts` 自动跳到 1.0.1。 |
|
|
254
|
+
|
|
245
255
|
## License
|
|
246
256
|
|
|
247
257
|
[MIT](./LICENSE) — Copyright (c) 2026 Acosmi
|
|
@@ -1403,9 +1403,18 @@ function skillScopes() {
|
|
|
1403
1403
|
}
|
|
1404
1404
|
|
|
1405
1405
|
// src/store.ts
|
|
1406
|
+
var fileLockDefaults = {
|
|
1407
|
+
/** 获取锁的超时上限 (毫秒). refresh 流程含网络 < 30s, 30s 已远超正常完成时间. */
|
|
1408
|
+
acquireTimeoutMs: 3e4,
|
|
1409
|
+
/** 旧锁判定阈值 (毫秒). 锁文件 mtime 早于此值视为 stale 进程崩溃残留, 自动 break. */
|
|
1410
|
+
staleMs: 6e4,
|
|
1411
|
+
/** 重试间隔基数 (毫秒). 真实间隔 = base + random(0, jitter). */
|
|
1412
|
+
retryBaseMs: 30,
|
|
1413
|
+
retryJitterMs: 70
|
|
1414
|
+
};
|
|
1406
1415
|
var FileTokenStore = class {
|
|
1407
1416
|
path;
|
|
1408
|
-
/**
|
|
1417
|
+
/** 进程内串行化 (Promise chain) — 与跨进程 flock 配合, 避免单进程内并发持锁产生死锁式互等. */
|
|
1409
1418
|
chain = Promise.resolve();
|
|
1410
1419
|
constructor(path) {
|
|
1411
1420
|
if (typeof process === "undefined" || !process.versions || !process.versions.node) {
|
|
@@ -1424,7 +1433,9 @@ var FileTokenStore = class {
|
|
|
1424
1433
|
this.path = path.join(os.homedir(), ".acosmi", "tokens.json");
|
|
1425
1434
|
return this.path;
|
|
1426
1435
|
}
|
|
1427
|
-
|
|
1436
|
+
/** 进程内串行 (维持 v1.0.1 单进程语义). flock 之外另一层防御: 如果用户自己就是单进程
|
|
1437
|
+
* 并发场景, 不必每次都进 flock 旁路文件 IO. */
|
|
1438
|
+
withChain(fn) {
|
|
1428
1439
|
const next = this.chain.then(fn, fn);
|
|
1429
1440
|
this.chain = next.then(
|
|
1430
1441
|
() => void 0,
|
|
@@ -1432,19 +1443,106 @@ var FileTokenStore = class {
|
|
|
1432
1443
|
);
|
|
1433
1444
|
return next;
|
|
1434
1445
|
}
|
|
1446
|
+
/**
|
|
1447
|
+
* 跨进程临界区. 用 sidecar `<path>.lock` 文件 + O_EXCL 创建语义实现互斥:
|
|
1448
|
+
* - 创建成功 = 持有锁; 写入 pid+timestamp 便于诊断
|
|
1449
|
+
* - 创建失败 (EEXIST) = 别的进程持锁, backoff 重试
|
|
1450
|
+
* - 锁文件 mtime > staleMs = 进程崩溃残留, unlink 后重试
|
|
1451
|
+
* - acquireTimeoutMs 超时 = 抛错 (caller 应作为 transient error 处理, 上层 retry)
|
|
1452
|
+
*
|
|
1453
|
+
* 注意: O_EXCL 在 NFS 上不保证原子; FileTokenStore 适用于本地文件系统 (典型用户家目录).
|
|
1454
|
+
* 真要跨机共享 token, 应实现自定义 Keychain / 数据库 store, 不要用 FileTokenStore.
|
|
1455
|
+
*/
|
|
1456
|
+
async withLock(fn) {
|
|
1457
|
+
const fs = await import('fs/promises');
|
|
1458
|
+
const pathMod = await import('path');
|
|
1459
|
+
const p = await this.resolvePath();
|
|
1460
|
+
const lockPath = `${p}.lock`;
|
|
1461
|
+
const dir = pathMod.dirname(p);
|
|
1462
|
+
await fs.mkdir(dir, { recursive: true, mode: 448 });
|
|
1463
|
+
const startMs = Date.now();
|
|
1464
|
+
let release = null;
|
|
1465
|
+
while (true) {
|
|
1466
|
+
try {
|
|
1467
|
+
const fh = await fs.open(lockPath, "wx", 384);
|
|
1468
|
+
try {
|
|
1469
|
+
await fh.writeFile(`${process.pid}
|
|
1470
|
+
${Date.now()}
|
|
1471
|
+
`, "utf8");
|
|
1472
|
+
} finally {
|
|
1473
|
+
await fh.close();
|
|
1474
|
+
}
|
|
1475
|
+
release = async () => {
|
|
1476
|
+
try {
|
|
1477
|
+
await fs.unlink(lockPath);
|
|
1478
|
+
} catch {
|
|
1479
|
+
}
|
|
1480
|
+
};
|
|
1481
|
+
break;
|
|
1482
|
+
} catch (e) {
|
|
1483
|
+
if (!isAlreadyExistsError(e)) throw e;
|
|
1484
|
+
let stale = false;
|
|
1485
|
+
try {
|
|
1486
|
+
const st = await fs.stat(lockPath);
|
|
1487
|
+
if (Date.now() - st.mtimeMs > fileLockDefaults.staleMs) stale = true;
|
|
1488
|
+
} catch {
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1491
|
+
if (stale) {
|
|
1492
|
+
try {
|
|
1493
|
+
await fs.unlink(lockPath);
|
|
1494
|
+
} catch {
|
|
1495
|
+
}
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
if (Date.now() - startMs > fileLockDefaults.acquireTimeoutMs) {
|
|
1499
|
+
throw new Error(
|
|
1500
|
+
`acquire token file lock timeout (${fileLockDefaults.acquireTimeoutMs}ms): ${lockPath}`
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
const waitMs = fileLockDefaults.retryBaseMs + Math.random() * fileLockDefaults.retryJitterMs;
|
|
1504
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
try {
|
|
1508
|
+
return await fn();
|
|
1509
|
+
} finally {
|
|
1510
|
+
if (release) await release();
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* 写入 token. 流程:
|
|
1515
|
+
* 1. mkdir -p (默认路径目录)
|
|
1516
|
+
* 2. 写入 `<path>.tmp.<pid>`
|
|
1517
|
+
* 3. fsync 后 rename 到正式路径 (POSIX 上 rename(2) 同分区原子, Windows 上 ReplaceFile)
|
|
1518
|
+
*
|
|
1519
|
+
* 选择 atomic rename 而非直接 writeFile: 多进程并发或本进程崩溃时, 读端永远看到的是
|
|
1520
|
+
* 完整的旧/新 JSON, 不会读到截断半文件 (Client.create.store.load 可能在另一进程
|
|
1521
|
+
* 写入中间触发, atomic rename 避免它解析失败).
|
|
1522
|
+
*/
|
|
1435
1523
|
save(tokens) {
|
|
1436
|
-
return this.
|
|
1524
|
+
return this.withChain(async () => {
|
|
1437
1525
|
const fs = await import('fs/promises');
|
|
1438
|
-
const
|
|
1526
|
+
const pathMod = await import('path');
|
|
1439
1527
|
const p = await this.resolvePath();
|
|
1440
|
-
const dir =
|
|
1528
|
+
const dir = pathMod.dirname(p);
|
|
1441
1529
|
await fs.mkdir(dir, { recursive: true, mode: 448 });
|
|
1530
|
+
const tmp = `${p}.tmp.${process.pid}.${Date.now()}.${Math.floor(Math.random() * 1e6)}`;
|
|
1442
1531
|
const data = JSON.stringify(tokens, null, 2);
|
|
1443
|
-
await fs.writeFile(
|
|
1532
|
+
await fs.writeFile(tmp, data, { encoding: "utf8", mode: 384 });
|
|
1533
|
+
try {
|
|
1534
|
+
await fs.rename(tmp, p);
|
|
1535
|
+
} catch (e) {
|
|
1536
|
+
try {
|
|
1537
|
+
await fs.unlink(tmp);
|
|
1538
|
+
} catch {
|
|
1539
|
+
}
|
|
1540
|
+
throw e;
|
|
1541
|
+
}
|
|
1444
1542
|
});
|
|
1445
1543
|
}
|
|
1446
1544
|
load() {
|
|
1447
|
-
return this.
|
|
1545
|
+
return this.withChain(async () => {
|
|
1448
1546
|
const fs = await import('fs/promises');
|
|
1449
1547
|
const p = await this.resolvePath();
|
|
1450
1548
|
try {
|
|
@@ -1459,7 +1557,7 @@ var FileTokenStore = class {
|
|
|
1459
1557
|
});
|
|
1460
1558
|
}
|
|
1461
1559
|
clear() {
|
|
1462
|
-
return this.
|
|
1560
|
+
return this.withChain(async () => {
|
|
1463
1561
|
const fs = await import('fs/promises');
|
|
1464
1562
|
const p = await this.resolvePath();
|
|
1465
1563
|
try {
|
|
@@ -1477,6 +1575,12 @@ function isNotExistError(e) {
|
|
|
1477
1575
|
}
|
|
1478
1576
|
return false;
|
|
1479
1577
|
}
|
|
1578
|
+
function isAlreadyExistsError(e) {
|
|
1579
|
+
if (typeof e === "object" && e !== null && "code" in e) {
|
|
1580
|
+
return e.code === "EEXIST";
|
|
1581
|
+
}
|
|
1582
|
+
return false;
|
|
1583
|
+
}
|
|
1480
1584
|
function newFileTokenStore(path) {
|
|
1481
1585
|
return new FileTokenStore(path);
|
|
1482
1586
|
}
|
|
@@ -2343,61 +2447,67 @@ var Client = class _Client {
|
|
|
2343
2447
|
if (!tokenSetIsExpired(tokens)) {
|
|
2344
2448
|
return tokens.access_token;
|
|
2345
2449
|
}
|
|
2346
|
-
return this.withMu(
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2450
|
+
return this.withMu(
|
|
2451
|
+
() => this.storeWithLock(async () => {
|
|
2452
|
+
await this.syncFromDisk();
|
|
2453
|
+
if (this.tokens == null) {
|
|
2454
|
+
throw new Error("not authorized, call login() first");
|
|
2455
|
+
}
|
|
2456
|
+
if (!tokenSetIsExpired(this.tokens)) {
|
|
2457
|
+
return this.tokens.access_token;
|
|
2458
|
+
}
|
|
2459
|
+
if (this.meta == null) {
|
|
2460
|
+
try {
|
|
2461
|
+
this.meta = await discover(this.serverURL, signal);
|
|
2462
|
+
} catch (e) {
|
|
2463
|
+
throw new Error(
|
|
2464
|
+
`discover for refresh: ${e instanceof Error ? e.message : String(e)}`
|
|
2465
|
+
);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
let tokenResp;
|
|
2354
2469
|
try {
|
|
2355
|
-
|
|
2470
|
+
tokenResp = await refreshToken(this.meta, this.tokens.client_id, this.tokens.refresh_token, signal);
|
|
2356
2471
|
} catch (e) {
|
|
2357
|
-
throw new Error(
|
|
2358
|
-
`discover for refresh: ${e instanceof Error ? e.message : String(e)}`
|
|
2359
|
-
);
|
|
2472
|
+
throw new Error(`refresh token: ${e instanceof Error ? e.message : String(e)}`);
|
|
2360
2473
|
}
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
await this.store.save(this.tokens);
|
|
2371
|
-
} catch (e) {
|
|
2372
|
-
console.warn(`[acosmi-sdk] warning: save refreshed token failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
2373
|
-
}
|
|
2374
|
-
return this.tokens.access_token;
|
|
2375
|
-
});
|
|
2474
|
+
this.tokens = newTokenSet(tokenResp, this.tokens.client_id, this.serverURL);
|
|
2475
|
+
try {
|
|
2476
|
+
await this.store.save(this.tokens);
|
|
2477
|
+
} catch (e) {
|
|
2478
|
+
console.warn(`[acosmi-sdk] warning: save refreshed token failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
2479
|
+
}
|
|
2480
|
+
return this.tokens.access_token;
|
|
2481
|
+
})
|
|
2482
|
+
);
|
|
2376
2483
|
}
|
|
2377
2484
|
/** 强制刷新 token (用于 401 重试) */
|
|
2378
2485
|
async forceRefresh(signal) {
|
|
2379
|
-
return this.withMu(
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
try {
|
|
2394
|
-
await this.store.save(this.tokens);
|
|
2395
|
-
} catch (e) {
|
|
2396
|
-
console.warn(
|
|
2397
|
-
`[acosmi-sdk] warning: save refreshed token failed: ${e instanceof Error ? e.message : String(e)}`
|
|
2486
|
+
return this.withMu(
|
|
2487
|
+
() => this.storeWithLock(async () => {
|
|
2488
|
+
await this.syncFromDisk();
|
|
2489
|
+
if (this.tokens == null) {
|
|
2490
|
+
throw new Error("no tokens to refresh");
|
|
2491
|
+
}
|
|
2492
|
+
if (this.meta == null) {
|
|
2493
|
+
this.meta = await discover(this.serverURL, signal);
|
|
2494
|
+
}
|
|
2495
|
+
const tokenResp = await refreshToken(
|
|
2496
|
+
this.meta,
|
|
2497
|
+
this.tokens.client_id,
|
|
2498
|
+
this.tokens.refresh_token,
|
|
2499
|
+
signal
|
|
2398
2500
|
);
|
|
2399
|
-
|
|
2400
|
-
|
|
2501
|
+
this.tokens = newTokenSet(tokenResp, this.tokens.client_id, this.serverURL);
|
|
2502
|
+
try {
|
|
2503
|
+
await this.store.save(this.tokens);
|
|
2504
|
+
} catch (e) {
|
|
2505
|
+
console.warn(
|
|
2506
|
+
`[acosmi-sdk] warning: save refreshed token failed: ${e instanceof Error ? e.message : String(e)}`
|
|
2507
|
+
);
|
|
2508
|
+
}
|
|
2509
|
+
})
|
|
2510
|
+
);
|
|
2401
2511
|
}
|
|
2402
2512
|
/** 互斥锁 helper (替代 Go sync.Mutex) */
|
|
2403
2513
|
withMu(fn) {
|
|
@@ -2408,6 +2518,29 @@ var Client = class _Client {
|
|
|
2408
2518
|
);
|
|
2409
2519
|
return next;
|
|
2410
2520
|
}
|
|
2521
|
+
/** v1.0.2: 跨进程临界区 helper. store.withLock 可选, 缺省则直接调用 fn (LocalStorage /
|
|
2522
|
+
* InMemory 单进程无需). */
|
|
2523
|
+
async storeWithLock(fn) {
|
|
2524
|
+
const lock = this.store.withLock;
|
|
2525
|
+
if (typeof lock === "function") {
|
|
2526
|
+
return lock.call(this.store, fn);
|
|
2527
|
+
}
|
|
2528
|
+
return fn();
|
|
2529
|
+
}
|
|
2530
|
+
/** v1.0.2: 从磁盘同步 token (refresh 前). 别的进程 rotation 后磁盘 refresh_token 已变,
|
|
2531
|
+
* 本进程内存仍是旧 R0; 不同步直接 refresh 必撞网关 400. load 失败保留内存继续 (容错). */
|
|
2532
|
+
async syncFromDisk() {
|
|
2533
|
+
let onDisk = null;
|
|
2534
|
+
try {
|
|
2535
|
+
onDisk = await this.store.load();
|
|
2536
|
+
} catch {
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
if (!onDisk) return;
|
|
2540
|
+
if (this.tokens == null || onDisk.refresh_token !== this.tokens.refresh_token) {
|
|
2541
|
+
this.tokens = onDisk;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2411
2544
|
// ===========================================================================
|
|
2412
2545
|
// Managed Models
|
|
2413
2546
|
// ===========================================================================
|
|
@@ -3945,6 +4078,6 @@ Client.prototype.getBugReport = async function(bugID, signal) {
|
|
|
3945
4078
|
return resp.data;
|
|
3946
4079
|
};
|
|
3947
4080
|
|
|
3948
|
-
export { AnthropicAdapter, BucketClassCommercial, BucketClassGeneric, BusinessError, Client, LocalStorageTokenStore as DefaultBrowserTokenStore, DefaultRetryPolicy, ErrAuthDenied, ErrBrowserOpen, ErrDiscovery, ErrRegistration, ErrSSLProxy, ErrTimeout, ErrTokenExchange, EventAuthURL, EventComplete, EventError, FileTokenStore, FilterStatusAdminBypass, FilterStatusDisabledByFlag, FilterStatusFallbackMissingUser, FilterStatusFallbackNoBuckets, FilterStatusFallbackTkdistError, FilterStatusFallbackTkdistSkew, FilterStatusInternalBypass, FilterStatusOK, FilterStatusUnknown, HTTPError, InMemoryTokenStore, LocalStorageTokenStore, ModelNotFoundError, NetworkError, OpenAIAdapter, OrderTerminalError, ProviderFormat, RateLimitError, ScopeAI, ScopeAccount, ScopeEntitlements, ScopeModels, ScopeModelsChat, ScopeProfile, ScopeSkillStore, ScopeSkills, ScopeTokenPackages, ScopeTools, ScopeToolsExecute, ScopeWallet, ScopeWalletReadonly, ServerToolTypeWebSearch, StreamError, ThinkingHigh, ThinkingHighMinMaxTokens, ThinkingMax, ThinkingMaxFallbackMaxTokens, ThinkingOff, allScopes, anthropicResponseTextContent, anthropicResponseThinkingContent, anthropicResponseToolUseBlocks, apiResponseBusinessError, apiResponseGetMessage, authorize, bucketInfoIsCommercial, bucketRowIsCommercial, buildBetas, commerceScopes, computeBackoff, defaultRetryable, defaultSafeToRetry, discover, effectivePolicy, exchangeCode, extractAnthropicBlockMeta, getAdapter, getAdapterForModel, isSSLError, modelScopes, newFileTokenStore, newThinkingConfig, newTokenSet, newWebSearchTool, parseNotificationEvent, parseSettlement, parseSourcesEvent, refreshToken, register, revokeToken, sanitize_exports as sanitize, skillScopes, tokenSetIsExpired, uniqueMerge };
|
|
3949
|
-
//# sourceMappingURL=index.
|
|
3950
|
-
//# sourceMappingURL=index.
|
|
4081
|
+
export { AnthropicAdapter, BucketClassCommercial, BucketClassGeneric, BusinessError, Client, LocalStorageTokenStore as DefaultBrowserTokenStore, DefaultRetryPolicy, ErrAuthDenied, ErrBrowserOpen, ErrDiscovery, ErrRegistration, ErrSSLProxy, ErrTimeout, ErrTokenExchange, EventAuthURL, EventComplete, EventError, FileTokenStore, FilterStatusAdminBypass, FilterStatusDisabledByFlag, FilterStatusFallbackMissingUser, FilterStatusFallbackNoBuckets, FilterStatusFallbackTkdistError, FilterStatusFallbackTkdistSkew, FilterStatusInternalBypass, FilterStatusOK, FilterStatusUnknown, HTTPError, InMemoryTokenStore, LocalStorageTokenStore, ModelNotFoundError, NetworkError, OpenAIAdapter, OrderTerminalError, ProviderFormat, RateLimitError, ScopeAI, ScopeAccount, ScopeEntitlements, ScopeModels, ScopeModelsChat, ScopeProfile, ScopeSkillStore, ScopeSkills, ScopeTokenPackages, ScopeTools, ScopeToolsExecute, ScopeWallet, ScopeWalletReadonly, ServerToolTypeWebSearch, StreamError, ThinkingHigh, ThinkingHighMinMaxTokens, ThinkingMax, ThinkingMaxFallbackMaxTokens, ThinkingOff, allScopes, anthropicResponseTextContent, anthropicResponseThinkingContent, anthropicResponseToolUseBlocks, apiResponseBusinessError, apiResponseGetMessage, authorize, bucketInfoIsCommercial, bucketRowIsCommercial, buildBetas, commerceScopes, computeBackoff, defaultRetryable, defaultSafeToRetry, discover, effectivePolicy, exchangeCode, extractAnthropicBlockMeta, fileLockDefaults, getAdapter, getAdapterForModel, isSSLError, modelScopes, newFileTokenStore, newThinkingConfig, newTokenSet, newWebSearchTool, parseNotificationEvent, parseSettlement, parseSourcesEvent, refreshToken, register, revokeToken, sanitize_exports as sanitize, skillScopes, tokenSetIsExpired, uniqueMerge };
|
|
4082
|
+
//# sourceMappingURL=index.mjs.map
|
|
4083
|
+
//# sourceMappingURL=index.mjs.map
|