@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.
@@ -122,11 +122,17 @@ declare function skillScopes(): string[];
122
122
  * - Save: 写入持久化, 失败抛 Error
123
123
  * - Load: 读取, 不存在返回 null (与 Go IsNotExist 行为一致)
124
124
  * - Clear: 删除, 不存在不抛错 (Logout 后 Clear 不应报错, [RC-11])
125
+ * - withLock (v1.0.2 新增, 可选): 跨进程临界区. Client 在 refresh token rotation
126
+ * 场景下用此包裹 "load → check → refresh → save" 整段, 防多进程共享同一 store
127
+ * (典型: FileTokenStore 默认 ~/.acosmi/tokens.json) 撞 HTTP 400 refresh token
128
+ * not found. 不实现时 Client 自动回退到仅进程内串行 (LocalStorage / InMemory
129
+ * 单进程语义无需此方法).
125
130
  */
126
131
  interface TokenStore {
127
132
  save(tokens: TokenSet): Promise<void>;
128
133
  load(): Promise<TokenSet | null>;
129
134
  clear(): Promise<void>;
135
+ withLock?<T>(fn: () => Promise<T>): Promise<T>;
130
136
  }
131
137
  /**
132
138
  * 基于文件的 token 存储 (开发/测试用)
@@ -136,13 +142,46 @@ interface TokenStore {
136
142
  *
137
143
  * 浏览器环境调用 new FileTokenStore() 会抛错 — 浏览器请用 LocalStorageTokenStore 或 InMemoryTokenStore.
138
144
  */
145
+ /** 跨进程文件锁配置 (v1.0.2). 公开常量是为了让 caller / 测试可观察, 不建议生产代码改. */
146
+ declare const fileLockDefaults: {
147
+ /** 获取锁的超时上限 (毫秒). refresh 流程含网络 < 30s, 30s 已远超正常完成时间. */
148
+ readonly acquireTimeoutMs: 30000;
149
+ /** 旧锁判定阈值 (毫秒). 锁文件 mtime 早于此值视为 stale 进程崩溃残留, 自动 break. */
150
+ readonly staleMs: 60000;
151
+ /** 重试间隔基数 (毫秒). 真实间隔 = base + random(0, jitter). */
152
+ readonly retryBaseMs: 30;
153
+ readonly retryJitterMs: 70;
154
+ };
139
155
  declare class FileTokenStore implements TokenStore {
140
156
  private path;
141
- /** 简单串行化锁 (替代 Go sync.Mutex) */
157
+ /** 进程内串行化 (Promise chain) — 与跨进程 flock 配合, 避免单进程内并发持锁产生死锁式互等. */
142
158
  private chain;
143
159
  constructor(path?: string);
144
160
  private resolvePath;
145
- private withLock;
161
+ /** 进程内串行 (维持 v1.0.1 单进程语义). flock 之外另一层防御: 如果用户自己就是单进程
162
+ * 并发场景, 不必每次都进 flock 旁路文件 IO. */
163
+ private withChain;
164
+ /**
165
+ * 跨进程临界区. 用 sidecar `<path>.lock` 文件 + O_EXCL 创建语义实现互斥:
166
+ * - 创建成功 = 持有锁; 写入 pid+timestamp 便于诊断
167
+ * - 创建失败 (EEXIST) = 别的进程持锁, backoff 重试
168
+ * - 锁文件 mtime > staleMs = 进程崩溃残留, unlink 后重试
169
+ * - acquireTimeoutMs 超时 = 抛错 (caller 应作为 transient error 处理, 上层 retry)
170
+ *
171
+ * 注意: O_EXCL 在 NFS 上不保证原子; FileTokenStore 适用于本地文件系统 (典型用户家目录).
172
+ * 真要跨机共享 token, 应实现自定义 Keychain / 数据库 store, 不要用 FileTokenStore.
173
+ */
174
+ withLock<T>(fn: () => Promise<T>): Promise<T>;
175
+ /**
176
+ * 写入 token. 流程:
177
+ * 1. mkdir -p (默认路径目录)
178
+ * 2. 写入 `<path>.tmp.<pid>`
179
+ * 3. fsync 后 rename 到正式路径 (POSIX 上 rename(2) 同分区原子, Windows 上 ReplaceFile)
180
+ *
181
+ * 选择 atomic rename 而非直接 writeFile: 多进程并发或本进程崩溃时, 读端永远看到的是
182
+ * 完整的旧/新 JSON, 不会读到截断半文件 (Client.create.store.load 可能在另一进程
183
+ * 写入中间触发, atomic rename 避免它解析失败).
184
+ */
146
185
  save(tokens: TokenSet): Promise<void>;
147
186
  load(): Promise<TokenSet | null>;
148
187
  clear(): Promise<void>;
@@ -391,6 +430,12 @@ declare class Client {
391
430
  forceRefresh(signal?: AbortSignal): Promise<void>;
392
431
  /** 互斥锁 helper (替代 Go sync.Mutex) */
393
432
  private withMu;
433
+ /** v1.0.2: 跨进程临界区 helper. store.withLock 可选, 缺省则直接调用 fn (LocalStorage /
434
+ * InMemory 单进程无需). */
435
+ private storeWithLock;
436
+ /** v1.0.2: 从磁盘同步 token (refresh 前). 别的进程 rotation 后磁盘 refresh_token 已变,
437
+ * 本进程内存仍是旧 R0; 不同步直接 refresh 必撞网关 400. load 失败保留内存继续 (容错). */
438
+ private syncFromDisk;
394
439
  /**
395
440
  * 获取可用的托管模型列表.
396
441
  *
@@ -545,7 +590,7 @@ declare class Client {
545
590
  }, signal?: AbortSignal): Promise<Response>;
546
591
  }
547
592
 
548
- declare module '../client' {
593
+ declare module '@acosmi/sdk-ts' {
549
594
  interface Client {
550
595
  /** 查询当前用户的权益余额 (聚合) */
551
596
  getBalance(signal?: AbortSignal): Promise<EntitlementBalance>;
@@ -568,7 +613,7 @@ declare module '../client' {
568
613
  }
569
614
  }
570
615
 
571
- declare module '../client' {
616
+ declare module '@acosmi/sdk-ts' {
572
617
  interface Client {
573
618
  /** 获取商城流量包列表 (兼容 yudao 分页和直接数组两种格式) */
574
619
  listTokenPackages(signal?: AbortSignal): Promise<TokenPackage[]>;
@@ -589,7 +634,7 @@ declare module '../client' {
589
634
  }
590
635
  }
591
636
 
592
- declare module '../client' {
637
+ declare module '@acosmi/sdk-ts' {
593
638
  interface Client {
594
639
  /** 获取钱包统计 (余额/月消费/月充值) */
595
640
  getWalletStats(signal?: AbortSignal): Promise<WalletStats>;
@@ -598,7 +643,7 @@ declare module '../client' {
598
643
  }
599
644
  }
600
645
 
601
- declare module '../client' {
646
+ declare module '@acosmi/sdk-ts' {
602
647
  interface Client {
603
648
  /** 浏览技能商店 (公共端点, 无需认证) */
604
649
  browseSkillStore(query: SkillStoreQuery, signal?: AbortSignal): Promise<SkillStoreItem[]>;
@@ -642,7 +687,7 @@ declare module '../client' {
642
687
  }
643
688
  }
644
689
 
645
- declare module '../client' {
690
+ declare module '@acosmi/sdk-ts' {
646
691
  interface Client {
647
692
  /** 获取当前用户租户下的所有工具 (Skill 优先 + Plugin 兜底) */
648
693
  listTools(signal?: AbortSignal): Promise<ToolView[]>;
@@ -651,7 +696,7 @@ declare module '../client' {
651
696
  }
652
697
  }
653
698
 
654
- declare module '../client' {
699
+ declare module '@acosmi/sdk-ts' {
655
700
  interface Client {
656
701
  /** 分页查询通知列表 */
657
702
  listNotifications(page: number, pageSize: number, typeFilter: string, signal?: AbortSignal): Promise<NotificationList>;
@@ -674,7 +719,7 @@ declare module '../client' {
674
719
  }
675
720
  }
676
721
 
677
- declare module './client' {
722
+ declare module '@acosmi/sdk-ts' {
678
723
  interface Client {
679
724
  /**
680
725
  * 配置请求前的底线防御 (体积 / deny-list / 深度)。
@@ -717,7 +762,7 @@ interface WSConfig {
717
762
  /** 是否自动重连 (默认 true) */
718
763
  autoReconnect?: boolean;
719
764
  }
720
- declare module './client' {
765
+ declare module '@acosmi/sdk-ts' {
721
766
  interface Client {
722
767
  /**
723
768
  * 建立 WebSocket 长连接 — 等待首次连接成功或 abort。
@@ -758,7 +803,7 @@ interface BugView {
758
803
  transcript?: unknown[];
759
804
  extras?: Record<string, unknown>;
760
805
  }
761
- declare module './client' {
806
+ declare module '@acosmi/sdk-ts' {
762
807
  interface Client {
763
808
  /**
764
809
  * 上报一份 CrabCode bug 报告.
@@ -788,4 +833,4 @@ declare module './client' {
788
833
  }
789
834
  }
790
835
 
791
- export { AnthropicResponse, type AuthorizeResult, BalanceDetail, type BlockMeta, type BugReportResult, type BugView, CertificationStatus, ChatRequest, ChatResponse, Client, ClientRegistration, type Config, ConsumeRecordPage, DefaultRetryPolicy, DeviceRegistration, EntitlementBalance, EntitlementItem, ErrAuthDenied, ErrBrowserOpen, ErrDiscovery, ErrRegistration, ErrSSLProxy, ErrTimeout, ErrTokenExchange, EventAuthURL, EventComplete, EventError, FileTokenStore, type FilterStatus, FilterStatusAdminBypass, FilterStatusDisabledByFlag, FilterStatusFallbackMissingUser, FilterStatusFallbackNoBuckets, FilterStatusFallbackTkdistError, FilterStatusFallbackTkdistSkew, FilterStatusInternalBypass, FilterStatusOK, FilterStatusUnknown, GenerateSkillRequest, GenerateSkillResult, InMemoryTokenStore, LocalStorageTokenStore, type LoginErrCode, type LoginEvent, type LoginEventType, type LoginOptions, ManagedModel, ModelBucket, ModelByQuotaResponse, ModelCapabilities, ModelCoefficient, NotificationList, NotificationPreference, OptimizeSkillRequest, OptimizeSkillResult, Order, OrderStatus, PayPayload, ProviderAdapter, QuotaSummary, type RetryPolicy, type RetryRequestInfo, ScopeAI, ScopeAccount, ScopeEntitlements, ScopeModels, ScopeModelsChat, ScopeProfile, ScopeSkillStore, ScopeSkills, ScopeTokenPackages, ScopeTools, ScopeToolsExecute, ScopeWallet, ScopeWalletReadonly, ServerMetadata, SkillBrowseListResponse, SkillBrowseResponse, SkillStoreItem, SkillStoreQuery, SkillSummary, SourcesEvent, StreamEvent, StreamSettlement, TokenPackage, TokenResponse, TokenSet, type TokenStore, ToolView, Transaction, type WSConfig, WSEvent, WalletStats, allScopes, authorize, buildBetas, commerceScopes, computeBackoff, defaultRetryable, defaultSafeToRetry, discover, effectivePolicy, exchangeCode, extractAnthropicBlockMeta, isSSLError, modelScopes, newFileTokenStore, newTokenSet, refreshToken, register, revokeToken, skillScopes, uniqueMerge };
836
+ export { AnthropicResponse, type AuthorizeResult, BalanceDetail, type BlockMeta, type BugReportResult, type BugView, CertificationStatus, ChatRequest, ChatResponse, Client, ClientRegistration, type Config, ConsumeRecordPage, DefaultRetryPolicy, DeviceRegistration, EntitlementBalance, EntitlementItem, ErrAuthDenied, ErrBrowserOpen, ErrDiscovery, ErrRegistration, ErrSSLProxy, ErrTimeout, ErrTokenExchange, EventAuthURL, EventComplete, EventError, FileTokenStore, type FilterStatus, FilterStatusAdminBypass, FilterStatusDisabledByFlag, FilterStatusFallbackMissingUser, FilterStatusFallbackNoBuckets, FilterStatusFallbackTkdistError, FilterStatusFallbackTkdistSkew, FilterStatusInternalBypass, FilterStatusOK, FilterStatusUnknown, GenerateSkillRequest, GenerateSkillResult, InMemoryTokenStore, LocalStorageTokenStore, type LoginErrCode, type LoginEvent, type LoginEventType, type LoginOptions, ManagedModel, ModelBucket, ModelByQuotaResponse, ModelCapabilities, ModelCoefficient, NotificationList, NotificationPreference, OptimizeSkillRequest, OptimizeSkillResult, Order, OrderStatus, PayPayload, ProviderAdapter, QuotaSummary, type RetryPolicy, type RetryRequestInfo, ScopeAI, ScopeAccount, ScopeEntitlements, ScopeModels, ScopeModelsChat, ScopeProfile, ScopeSkillStore, ScopeSkills, ScopeTokenPackages, ScopeTools, ScopeToolsExecute, ScopeWallet, ScopeWalletReadonly, ServerMetadata, SkillBrowseListResponse, SkillBrowseResponse, SkillStoreItem, SkillStoreQuery, SkillSummary, SourcesEvent, StreamEvent, StreamSettlement, TokenPackage, TokenResponse, TokenSet, type TokenStore, ToolView, Transaction, type WSConfig, WSEvent, WalletStats, allScopes, authorize, buildBetas, commerceScopes, computeBackoff, defaultRetryable, defaultSafeToRetry, discover, effectivePolicy, exchangeCode, extractAnthropicBlockMeta, fileLockDefaults, isSSLError, modelScopes, newFileTokenStore, newTokenSet, refreshToken, register, revokeToken, skillScopes, uniqueMerge };
@@ -122,11 +122,17 @@ declare function skillScopes(): string[];
122
122
  * - Save: 写入持久化, 失败抛 Error
123
123
  * - Load: 读取, 不存在返回 null (与 Go IsNotExist 行为一致)
124
124
  * - Clear: 删除, 不存在不抛错 (Logout 后 Clear 不应报错, [RC-11])
125
+ * - withLock (v1.0.2 新增, 可选): 跨进程临界区. Client 在 refresh token rotation
126
+ * 场景下用此包裹 "load → check → refresh → save" 整段, 防多进程共享同一 store
127
+ * (典型: FileTokenStore 默认 ~/.acosmi/tokens.json) 撞 HTTP 400 refresh token
128
+ * not found. 不实现时 Client 自动回退到仅进程内串行 (LocalStorage / InMemory
129
+ * 单进程语义无需此方法).
125
130
  */
126
131
  interface TokenStore {
127
132
  save(tokens: TokenSet): Promise<void>;
128
133
  load(): Promise<TokenSet | null>;
129
134
  clear(): Promise<void>;
135
+ withLock?<T>(fn: () => Promise<T>): Promise<T>;
130
136
  }
131
137
  /**
132
138
  * 基于文件的 token 存储 (开发/测试用)
@@ -136,13 +142,46 @@ interface TokenStore {
136
142
  *
137
143
  * 浏览器环境调用 new FileTokenStore() 会抛错 — 浏览器请用 LocalStorageTokenStore 或 InMemoryTokenStore.
138
144
  */
145
+ /** 跨进程文件锁配置 (v1.0.2). 公开常量是为了让 caller / 测试可观察, 不建议生产代码改. */
146
+ declare const fileLockDefaults: {
147
+ /** 获取锁的超时上限 (毫秒). refresh 流程含网络 < 30s, 30s 已远超正常完成时间. */
148
+ readonly acquireTimeoutMs: 30000;
149
+ /** 旧锁判定阈值 (毫秒). 锁文件 mtime 早于此值视为 stale 进程崩溃残留, 自动 break. */
150
+ readonly staleMs: 60000;
151
+ /** 重试间隔基数 (毫秒). 真实间隔 = base + random(0, jitter). */
152
+ readonly retryBaseMs: 30;
153
+ readonly retryJitterMs: 70;
154
+ };
139
155
  declare class FileTokenStore implements TokenStore {
140
156
  private path;
141
- /** 简单串行化锁 (替代 Go sync.Mutex) */
157
+ /** 进程内串行化 (Promise chain) — 与跨进程 flock 配合, 避免单进程内并发持锁产生死锁式互等. */
142
158
  private chain;
143
159
  constructor(path?: string);
144
160
  private resolvePath;
145
- private withLock;
161
+ /** 进程内串行 (维持 v1.0.1 单进程语义). flock 之外另一层防御: 如果用户自己就是单进程
162
+ * 并发场景, 不必每次都进 flock 旁路文件 IO. */
163
+ private withChain;
164
+ /**
165
+ * 跨进程临界区. 用 sidecar `<path>.lock` 文件 + O_EXCL 创建语义实现互斥:
166
+ * - 创建成功 = 持有锁; 写入 pid+timestamp 便于诊断
167
+ * - 创建失败 (EEXIST) = 别的进程持锁, backoff 重试
168
+ * - 锁文件 mtime > staleMs = 进程崩溃残留, unlink 后重试
169
+ * - acquireTimeoutMs 超时 = 抛错 (caller 应作为 transient error 处理, 上层 retry)
170
+ *
171
+ * 注意: O_EXCL 在 NFS 上不保证原子; FileTokenStore 适用于本地文件系统 (典型用户家目录).
172
+ * 真要跨机共享 token, 应实现自定义 Keychain / 数据库 store, 不要用 FileTokenStore.
173
+ */
174
+ withLock<T>(fn: () => Promise<T>): Promise<T>;
175
+ /**
176
+ * 写入 token. 流程:
177
+ * 1. mkdir -p (默认路径目录)
178
+ * 2. 写入 `<path>.tmp.<pid>`
179
+ * 3. fsync 后 rename 到正式路径 (POSIX 上 rename(2) 同分区原子, Windows 上 ReplaceFile)
180
+ *
181
+ * 选择 atomic rename 而非直接 writeFile: 多进程并发或本进程崩溃时, 读端永远看到的是
182
+ * 完整的旧/新 JSON, 不会读到截断半文件 (Client.create.store.load 可能在另一进程
183
+ * 写入中间触发, atomic rename 避免它解析失败).
184
+ */
146
185
  save(tokens: TokenSet): Promise<void>;
147
186
  load(): Promise<TokenSet | null>;
148
187
  clear(): Promise<void>;
@@ -391,6 +430,12 @@ declare class Client {
391
430
  forceRefresh(signal?: AbortSignal): Promise<void>;
392
431
  /** 互斥锁 helper (替代 Go sync.Mutex) */
393
432
  private withMu;
433
+ /** v1.0.2: 跨进程临界区 helper. store.withLock 可选, 缺省则直接调用 fn (LocalStorage /
434
+ * InMemory 单进程无需). */
435
+ private storeWithLock;
436
+ /** v1.0.2: 从磁盘同步 token (refresh 前). 别的进程 rotation 后磁盘 refresh_token 已变,
437
+ * 本进程内存仍是旧 R0; 不同步直接 refresh 必撞网关 400. load 失败保留内存继续 (容错). */
438
+ private syncFromDisk;
394
439
  /**
395
440
  * 获取可用的托管模型列表.
396
441
  *
@@ -545,7 +590,7 @@ declare class Client {
545
590
  }, signal?: AbortSignal): Promise<Response>;
546
591
  }
547
592
 
548
- declare module '../client' {
593
+ declare module '@acosmi/sdk-ts' {
549
594
  interface Client {
550
595
  /** 查询当前用户的权益余额 (聚合) */
551
596
  getBalance(signal?: AbortSignal): Promise<EntitlementBalance>;
@@ -568,7 +613,7 @@ declare module '../client' {
568
613
  }
569
614
  }
570
615
 
571
- declare module '../client' {
616
+ declare module '@acosmi/sdk-ts' {
572
617
  interface Client {
573
618
  /** 获取商城流量包列表 (兼容 yudao 分页和直接数组两种格式) */
574
619
  listTokenPackages(signal?: AbortSignal): Promise<TokenPackage[]>;
@@ -589,7 +634,7 @@ declare module '../client' {
589
634
  }
590
635
  }
591
636
 
592
- declare module '../client' {
637
+ declare module '@acosmi/sdk-ts' {
593
638
  interface Client {
594
639
  /** 获取钱包统计 (余额/月消费/月充值) */
595
640
  getWalletStats(signal?: AbortSignal): Promise<WalletStats>;
@@ -598,7 +643,7 @@ declare module '../client' {
598
643
  }
599
644
  }
600
645
 
601
- declare module '../client' {
646
+ declare module '@acosmi/sdk-ts' {
602
647
  interface Client {
603
648
  /** 浏览技能商店 (公共端点, 无需认证) */
604
649
  browseSkillStore(query: SkillStoreQuery, signal?: AbortSignal): Promise<SkillStoreItem[]>;
@@ -642,7 +687,7 @@ declare module '../client' {
642
687
  }
643
688
  }
644
689
 
645
- declare module '../client' {
690
+ declare module '@acosmi/sdk-ts' {
646
691
  interface Client {
647
692
  /** 获取当前用户租户下的所有工具 (Skill 优先 + Plugin 兜底) */
648
693
  listTools(signal?: AbortSignal): Promise<ToolView[]>;
@@ -651,7 +696,7 @@ declare module '../client' {
651
696
  }
652
697
  }
653
698
 
654
- declare module '../client' {
699
+ declare module '@acosmi/sdk-ts' {
655
700
  interface Client {
656
701
  /** 分页查询通知列表 */
657
702
  listNotifications(page: number, pageSize: number, typeFilter: string, signal?: AbortSignal): Promise<NotificationList>;
@@ -674,7 +719,7 @@ declare module '../client' {
674
719
  }
675
720
  }
676
721
 
677
- declare module './client' {
722
+ declare module '@acosmi/sdk-ts' {
678
723
  interface Client {
679
724
  /**
680
725
  * 配置请求前的底线防御 (体积 / deny-list / 深度)。
@@ -717,7 +762,7 @@ interface WSConfig {
717
762
  /** 是否自动重连 (默认 true) */
718
763
  autoReconnect?: boolean;
719
764
  }
720
- declare module './client' {
765
+ declare module '@acosmi/sdk-ts' {
721
766
  interface Client {
722
767
  /**
723
768
  * 建立 WebSocket 长连接 — 等待首次连接成功或 abort。
@@ -758,7 +803,7 @@ interface BugView {
758
803
  transcript?: unknown[];
759
804
  extras?: Record<string, unknown>;
760
805
  }
761
- declare module './client' {
806
+ declare module '@acosmi/sdk-ts' {
762
807
  interface Client {
763
808
  /**
764
809
  * 上报一份 CrabCode bug 报告.
@@ -788,4 +833,4 @@ declare module './client' {
788
833
  }
789
834
  }
790
835
 
791
- export { AnthropicResponse, type AuthorizeResult, BalanceDetail, type BlockMeta, type BugReportResult, type BugView, CertificationStatus, ChatRequest, ChatResponse, Client, ClientRegistration, type Config, ConsumeRecordPage, DefaultRetryPolicy, DeviceRegistration, EntitlementBalance, EntitlementItem, ErrAuthDenied, ErrBrowserOpen, ErrDiscovery, ErrRegistration, ErrSSLProxy, ErrTimeout, ErrTokenExchange, EventAuthURL, EventComplete, EventError, FileTokenStore, type FilterStatus, FilterStatusAdminBypass, FilterStatusDisabledByFlag, FilterStatusFallbackMissingUser, FilterStatusFallbackNoBuckets, FilterStatusFallbackTkdistError, FilterStatusFallbackTkdistSkew, FilterStatusInternalBypass, FilterStatusOK, FilterStatusUnknown, GenerateSkillRequest, GenerateSkillResult, InMemoryTokenStore, LocalStorageTokenStore, type LoginErrCode, type LoginEvent, type LoginEventType, type LoginOptions, ManagedModel, ModelBucket, ModelByQuotaResponse, ModelCapabilities, ModelCoefficient, NotificationList, NotificationPreference, OptimizeSkillRequest, OptimizeSkillResult, Order, OrderStatus, PayPayload, ProviderAdapter, QuotaSummary, type RetryPolicy, type RetryRequestInfo, ScopeAI, ScopeAccount, ScopeEntitlements, ScopeModels, ScopeModelsChat, ScopeProfile, ScopeSkillStore, ScopeSkills, ScopeTokenPackages, ScopeTools, ScopeToolsExecute, ScopeWallet, ScopeWalletReadonly, ServerMetadata, SkillBrowseListResponse, SkillBrowseResponse, SkillStoreItem, SkillStoreQuery, SkillSummary, SourcesEvent, StreamEvent, StreamSettlement, TokenPackage, TokenResponse, TokenSet, type TokenStore, ToolView, Transaction, type WSConfig, WSEvent, WalletStats, allScopes, authorize, buildBetas, commerceScopes, computeBackoff, defaultRetryable, defaultSafeToRetry, discover, effectivePolicy, exchangeCode, extractAnthropicBlockMeta, isSSLError, modelScopes, newFileTokenStore, newTokenSet, refreshToken, register, revokeToken, skillScopes, uniqueMerge };
836
+ export { AnthropicResponse, type AuthorizeResult, BalanceDetail, type BlockMeta, type BugReportResult, type BugView, CertificationStatus, ChatRequest, ChatResponse, Client, ClientRegistration, type Config, ConsumeRecordPage, DefaultRetryPolicy, DeviceRegistration, EntitlementBalance, EntitlementItem, ErrAuthDenied, ErrBrowserOpen, ErrDiscovery, ErrRegistration, ErrSSLProxy, ErrTimeout, ErrTokenExchange, EventAuthURL, EventComplete, EventError, FileTokenStore, type FilterStatus, FilterStatusAdminBypass, FilterStatusDisabledByFlag, FilterStatusFallbackMissingUser, FilterStatusFallbackNoBuckets, FilterStatusFallbackTkdistError, FilterStatusFallbackTkdistSkew, FilterStatusInternalBypass, FilterStatusOK, FilterStatusUnknown, GenerateSkillRequest, GenerateSkillResult, InMemoryTokenStore, LocalStorageTokenStore, type LoginErrCode, type LoginEvent, type LoginEventType, type LoginOptions, ManagedModel, ModelBucket, ModelByQuotaResponse, ModelCapabilities, ModelCoefficient, NotificationList, NotificationPreference, OptimizeSkillRequest, OptimizeSkillResult, Order, OrderStatus, PayPayload, ProviderAdapter, QuotaSummary, type RetryPolicy, type RetryRequestInfo, ScopeAI, ScopeAccount, ScopeEntitlements, ScopeModels, ScopeModelsChat, ScopeProfile, ScopeSkillStore, ScopeSkills, ScopeTokenPackages, ScopeTools, ScopeToolsExecute, ScopeWallet, ScopeWalletReadonly, ServerMetadata, SkillBrowseListResponse, SkillBrowseResponse, SkillStoreItem, SkillStoreQuery, SkillSummary, SourcesEvent, StreamEvent, StreamSettlement, TokenPackage, TokenResponse, TokenSet, type TokenStore, ToolView, Transaction, type WSConfig, WSEvent, WalletStats, allScopes, authorize, buildBetas, commerceScopes, computeBackoff, defaultRetryable, defaultSafeToRetry, discover, effectivePolicy, exchangeCode, extractAnthropicBlockMeta, fileLockDefaults, isSSLError, modelScopes, newFileTokenStore, newTokenSet, refreshToken, register, revokeToken, skillScopes, uniqueMerge };
@@ -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
- /** 简单串行化锁 (替代 Go sync.Mutex) */
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
- withLock(fn) {
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.withLock(async () => {
1524
+ return this.withChain(async () => {
1437
1525
  const fs = await import('fs/promises');
1438
- const path = await import('path');
1526
+ const pathMod = await import('path');
1439
1527
  const p = await this.resolvePath();
1440
- const dir = path.dirname(p);
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(p, data, { encoding: "utf8", mode: 384 });
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.withLock(async () => {
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.withLock(async () => {
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(async () => {
2347
- if (this.tokens == null) {
2348
- throw new Error("not authorized, call login() first");
2349
- }
2350
- if (!tokenSetIsExpired(this.tokens)) {
2351
- return this.tokens.access_token;
2352
- }
2353
- if (this.meta == null) {
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
- this.meta = await discover(this.serverURL, signal);
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
- let tokenResp;
2363
- try {
2364
- tokenResp = await refreshToken(this.meta, this.tokens.client_id, this.tokens.refresh_token, signal);
2365
- } catch (e) {
2366
- throw new Error(`refresh token: ${e instanceof Error ? e.message : String(e)}`);
2367
- }
2368
- this.tokens = newTokenSet(tokenResp, this.tokens.client_id, this.serverURL);
2369
- try {
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(async () => {
2380
- if (this.tokens == null) {
2381
- throw new Error("no tokens to refresh");
2382
- }
2383
- if (this.meta == null) {
2384
- this.meta = await discover(this.serverURL, signal);
2385
- }
2386
- const tokenResp = await refreshToken(
2387
- this.meta,
2388
- this.tokens.client_id,
2389
- this.tokens.refresh_token,
2390
- signal
2391
- );
2392
- this.tokens = newTokenSet(tokenResp, this.tokens.client_id, this.serverURL);
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, 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.js.map
3950
- //# sourceMappingURL=index.js.map
4081
+ export { AnthropicAdapter, BucketClassCommercial, BucketClassGeneric, BusinessError, Client, 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