@cortexkit/anthropic-auth-core 1.1.1 → 1.1.3

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @cortexkit/anthropic-auth-core
2
2
 
3
- Shared Anthropic OAuth, quota, cache, fast mode, relay, dump, and request-signing helpers used by CortexKit's OpenCode and Pi integrations.
3
+ Shared Anthropic OAuth, quota, cache, cache-keepalive, fast mode, relay, dump, and request-signing helpers used by CortexKit's OpenCode and Pi integrations.
4
4
 
5
5
  User-facing packages:
6
6
 
@@ -62,6 +62,11 @@ export type AccountStorage = {
62
62
  claudeFast?: {
63
63
  enabled?: boolean;
64
64
  };
65
+ cacheKeep?: {
66
+ enabled?: boolean;
67
+ startHour?: number;
68
+ endHour?: number;
69
+ };
65
70
  relay?: {
66
71
  enabled?: boolean;
67
72
  url?: string;
@@ -84,6 +89,14 @@ export type AccountRefreshError = {
84
89
  export declare function getAccountStoragePath(): string;
85
90
  export declare function loadAccounts(path?: string): Promise<AccountStorage | null>;
86
91
  export declare function saveAccounts(storage: AccountStorage, path?: string): Promise<void>;
92
+ export declare function acquireRefreshFileLock(options: {
93
+ name: string;
94
+ ttlMs: number;
95
+ path?: string;
96
+ now?: () => number;
97
+ }): Promise<{
98
+ release: () => Promise<void>;
99
+ } | null>;
87
100
  export declare function isCache1hPersistentlyEnabled(storage: AccountStorage | null): boolean;
88
101
  export declare function getCache1hPersistentMode(storage: AccountStorage | null): Cache1hMode;
89
102
  export declare function setCache1hPersistentEnabled(enabled: boolean, mode?: Cache1hMode, path?: string): Promise<AccountStorage>;
@@ -92,6 +105,8 @@ export declare function isDumpPersistentlyEnabled(storage: AccountStorage | null
92
105
  export declare function setDumpPersistentEnabled(enabled: boolean, path?: string): Promise<AccountStorage>;
93
106
  export declare function isFastModePersistentlyEnabled(storage: AccountStorage | null): boolean;
94
107
  export declare function setFastModePersistentEnabled(enabled: boolean, path?: string): Promise<AccountStorage>;
108
+ export declare function setCacheKeepPersistentWindow(startHour: number, endHour: number, path?: string): Promise<AccountStorage>;
109
+ export declare function setCacheKeepPersistentEnabled(enabled: boolean, path?: string): Promise<AccountStorage>;
95
110
  export declare function shouldFallbackStatus(status: number, storage: AccountStorage | null): boolean;
96
111
  export declare function getRefreshIntervalMs(storage: AccountStorage | null): number;
97
112
  export declare function hashRefreshToken(refreshToken: string): string;
package/dist/accounts.js CHANGED
@@ -1,13 +1,15 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
- import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { ClaudeOAuthRefreshError, refreshClaudeOAuthToken } from "./auth.js";
6
6
  import { CACHE_1H_MODES, CLAUDE_CODE_VERSION, DEFAULT_CACHE_1H_MODE, } from "./constants.js";
7
+ import { log } from "./logger.js";
7
8
  export const ACCOUNT_FILE_NAME = 'anthropic-auth.json';
8
9
  export const QUOTA_URL = 'https://api.anthropic.com/api/oauth/usage';
9
10
  const DEFAULT_FALLBACK_ON = [401, 403, 429];
10
- const DEFAULT_REFRESH_BEFORE_EXPIRY_MINUTES = 30;
11
+ const MIN_REFRESH_BEFORE_EXPIRY_MINUTES = 240;
12
+ const DEFAULT_REFRESH_BEFORE_EXPIRY_MINUTES = MIN_REFRESH_BEFORE_EXPIRY_MINUTES;
11
13
  const DEFAULT_REFRESH_INTERVAL_MINUTES = 10;
12
14
  const MIN_REFRESH_RETRY_DELAY_MS = 5 * 60_000;
13
15
  const MAX_REFRESH_RETRY_DELAY_MS = 60 * 60_000;
@@ -116,6 +118,7 @@ function normalizeStorage(value) {
116
118
  claudeCache: isRecord(value.claudeCache) ? value.claudeCache : undefined,
117
119
  dump: isRecord(value.dump) ? value.dump : undefined,
118
120
  claudeFast: isRecord(value.claudeFast) ? value.claudeFast : undefined,
121
+ cacheKeep: isRecord(value.cacheKeep) ? value.cacheKeep : undefined,
119
122
  relay: isRecord(value.relay) ? value.relay : undefined,
120
123
  accounts: value.accounts
121
124
  .map(normalizeAccount)
@@ -157,6 +160,51 @@ export async function saveAccounts(storage, path = getAccountStoragePath()) {
157
160
  });
158
161
  await rename(tempPath, path);
159
162
  }
163
+ export async function acquireRefreshFileLock(options) {
164
+ const accountPath = options.path ?? getAccountStoragePath();
165
+ const lockDir = `${accountPath}.${options.name}.lock`;
166
+ const ownerPath = join(lockDir, 'owner.json');
167
+ const ownerId = randomUUID();
168
+ const now = options.now ?? Date.now;
169
+ async function tryAcquire() {
170
+ try {
171
+ await mkdir(lockDir, { mode: 0o700 });
172
+ await writeFile(ownerPath, `${JSON.stringify({ ownerId, expiresAt: now() + options.ttlMs })}\n`, { encoding: 'utf8', mode: 0o600 });
173
+ return true;
174
+ }
175
+ catch (error) {
176
+ if (error.code !== 'EEXIST')
177
+ throw error;
178
+ return false;
179
+ }
180
+ }
181
+ if (!(await tryAcquire())) {
182
+ try {
183
+ const owner = JSON.parse(await readFile(ownerPath, 'utf8'));
184
+ if (Number(owner?.expiresAt) > now())
185
+ return null;
186
+ }
187
+ catch {
188
+ // Broken owner metadata is treated as stale.
189
+ }
190
+ await rm(lockDir, { recursive: true, force: true }).catch(() => { });
191
+ if (!(await tryAcquire()))
192
+ return null;
193
+ }
194
+ return {
195
+ release: async () => {
196
+ try {
197
+ const owner = JSON.parse(await readFile(ownerPath, 'utf8'));
198
+ if (owner?.ownerId !== ownerId)
199
+ return;
200
+ }
201
+ catch {
202
+ return;
203
+ }
204
+ await rm(lockDir, { recursive: true, force: true }).catch(() => { });
205
+ },
206
+ };
207
+ }
160
208
  export function isCache1hPersistentlyEnabled(storage) {
161
209
  return storage?.claudeCache?.enabled === true;
162
210
  }
@@ -229,6 +277,33 @@ export async function setFastModePersistentEnabled(enabled, path = getAccountSto
229
277
  await saveAccounts(storage, path);
230
278
  return storage;
231
279
  }
280
+ export async function setCacheKeepPersistentWindow(startHour, endHour, path = getAccountStoragePath()) {
281
+ const storage = (await loadAccounts(path)) ?? {
282
+ version: 1,
283
+ main: { type: 'opencode', provider: 'anthropic' },
284
+ accounts: [],
285
+ };
286
+ storage.cacheKeep = {
287
+ enabled: true,
288
+ startHour,
289
+ endHour,
290
+ };
291
+ await saveAccounts(storage, path);
292
+ return storage;
293
+ }
294
+ export async function setCacheKeepPersistentEnabled(enabled, path = getAccountStoragePath()) {
295
+ const storage = (await loadAccounts(path)) ?? {
296
+ version: 1,
297
+ main: { type: 'opencode', provider: 'anthropic' },
298
+ accounts: [],
299
+ };
300
+ storage.cacheKeep = {
301
+ ...(storage.cacheKeep ?? {}),
302
+ enabled,
303
+ };
304
+ await saveAccounts(storage, path);
305
+ return storage;
306
+ }
232
307
  function getFallbackStatuses(storage) {
233
308
  return storage?.fallbackOn?.length ? storage.fallbackOn : DEFAULT_FALLBACK_ON;
234
309
  }
@@ -255,7 +330,7 @@ function refreshEnabled(storage) {
255
330
  function refreshBeforeExpiryMs(storage) {
256
331
  const minutes = storage?.refresh?.refreshBeforeExpiryMinutes ??
257
332
  DEFAULT_REFRESH_BEFORE_EXPIRY_MINUTES;
258
- return Math.max(0, minutes) * 60_000;
333
+ return Math.max(MIN_REFRESH_BEFORE_EXPIRY_MINUTES, minutes) * 60_000;
259
334
  }
260
335
  export function getRefreshIntervalMs(storage) {
261
336
  const minutes = storage?.refresh?.intervalMinutes ?? DEFAULT_REFRESH_INTERVAL_MINUTES;
@@ -278,7 +353,11 @@ function isTransientRefreshError(error) {
278
353
  error.code === 'UND_ERR_CONNECT_TIMEOUT')));
279
354
  }
280
355
  export function buildRefreshOperationError(input) {
281
- const retryCount = (input.previous?.retryCount ?? 0) + 1;
356
+ const tokenHash = hashRefreshToken(input.refreshToken);
357
+ const previousRetryCount = input.previous?.tokenHash === tokenHash
358
+ ? (input.previous.retryCount ?? 0)
359
+ : 0;
360
+ const retryCount = previousRetryCount + 1;
282
361
  const delay = isTransientRefreshError(input.error)
283
362
  ? Math.min(MAX_REFRESH_RETRY_DELAY_MS, MIN_REFRESH_RETRY_DELAY_MS * 2 ** Math.min(retryCount - 1, 6))
284
363
  : NON_TRANSIENT_REFRESH_RETRY_DELAY_MS;
@@ -287,7 +366,7 @@ export function buildRefreshOperationError(input) {
287
366
  checkedAt: input.now,
288
367
  nextRetryAt: input.now + delay,
289
368
  retryCount,
290
- tokenHash: hashRefreshToken(input.refreshToken),
369
+ tokenHash,
291
370
  };
292
371
  }
293
372
  export function refreshBackoffActive(error, refreshToken, now) {
@@ -525,13 +604,28 @@ export class FallbackAccountManager {
525
604
  if (!tokenNeedsRefresh(account, storage, this.now()))
526
605
  continue;
527
606
  if (refreshBackoffActive(account.lastRefreshError, account.refresh, this.now())) {
607
+ log('[refresh] fallback oauth skipped backoff', {
608
+ accountId: account.id,
609
+ nextRetryAt: account.lastRefreshError?.nextRetryAt,
610
+ retryCount: account.lastRefreshError?.retryCount,
611
+ });
528
612
  continue;
529
613
  }
530
614
  try {
615
+ log('[refresh] fallback oauth background due', {
616
+ accountId: account.id,
617
+ expiresInMs: account.expires
618
+ ? account.expires - this.now()
619
+ : undefined,
620
+ });
531
621
  await this.refreshAccount(account, storage);
532
622
  changed = true;
533
623
  }
534
624
  catch (error) {
625
+ log('[refresh] fallback oauth background failed', {
626
+ accountId: account.id,
627
+ error: error instanceof Error ? error.message : String(error),
628
+ });
535
629
  recordRefreshError(account, error, this.now());
536
630
  updateStoredAccount(storage, account);
537
631
  changed = true;
@@ -636,6 +730,13 @@ export class FallbackAccountManager {
636
730
  }
637
731
  const sourceAccount = latestAccount ?? account;
638
732
  const refreshToken = sourceAccount.refresh;
733
+ log('[refresh] fallback oauth refresh request start', {
734
+ accountId: sourceAccount.id,
735
+ force: options.force === true,
736
+ expiresInMs: sourceAccount.expires
737
+ ? sourceAccount.expires - this.now()
738
+ : undefined,
739
+ });
639
740
  const refreshed = await refreshClaudeOAuthToken({
640
741
  refreshToken,
641
742
  fetchImpl: this.fetchImpl,
@@ -648,6 +749,12 @@ export class FallbackAccountManager {
648
749
  refreshed.expires - refreshed.expiresIn * 1000;
649
750
  sourceAccount.lastRefreshError = undefined;
650
751
  updateStoredAccount(storage, sourceAccount);
752
+ log('[refresh] fallback oauth refresh succeeded', {
753
+ accountId: sourceAccount.id,
754
+ expiresInMs: sourceAccount.expires
755
+ ? sourceAccount.expires - this.now()
756
+ : undefined,
757
+ });
651
758
  return sourceAccount;
652
759
  }
653
760
  async refreshAccountQuota(account, storage) {
@@ -0,0 +1,92 @@
1
+ import type { AccountStorage } from './accounts.ts';
2
+ export declare const CLAUDE_CACHE_KEEP_COMMAND_NAME = "claude-cachekeep";
3
+ export declare const CACHE_KEEP_TTL_MS: number;
4
+ export declare const CACHE_KEEP_PREWARM_LEAD_MS: number;
5
+ export declare const CACHE_KEEP_TICK_MS = 60000;
6
+ export declare const CACHE_KEEP_EXTENDED_TTL_BETA = "extended-cache-ttl-2025-04-11";
7
+ export type CacheKeepWindow = {
8
+ startHour: number;
9
+ endHour: number;
10
+ };
11
+ export type CacheKeepCommandAction = {
12
+ type: 'status';
13
+ } | {
14
+ type: 'disable';
15
+ } | {
16
+ type: 'window';
17
+ startHour: number;
18
+ endHour: number;
19
+ } | {
20
+ type: 'usage';
21
+ };
22
+ export type CacheKeepStatus = {
23
+ enabled: boolean;
24
+ window?: CacheKeepWindow;
25
+ trackedSessions?: number;
26
+ nextPrewarmAt?: number;
27
+ hybridActive?: boolean;
28
+ };
29
+ export declare function parseCacheKeepCommandAction(input: string): CacheKeepCommandAction;
30
+ export declare function getCacheKeepWindow(storage: AccountStorage | null): CacheKeepWindow | undefined;
31
+ export declare function isCacheKeepPersistentlyEnabled(storage: AccountStorage | null): boolean;
32
+ export declare function isCacheKeepHybridActive(storage: AccountStorage | null): boolean;
33
+ export declare function isWithinCacheKeepWindow(window: CacheKeepWindow | undefined, now?: Date): boolean;
34
+ export declare function localDayKey(now?: Date): string;
35
+ export declare function buildCacheKeepStatusSummary(status: CacheKeepStatus): string;
36
+ export declare function executeCacheKeepCommand(input: {
37
+ argumentsText: string;
38
+ enabled?: boolean;
39
+ window?: CacheKeepWindow;
40
+ trackedSessions?: number;
41
+ nextPrewarmAt?: number;
42
+ hybridActive?: boolean;
43
+ }): string;
44
+ export declare function buildCacheKeepPrewarmBody(bodyText: string): Promise<{
45
+ ok: true;
46
+ bodyText: string;
47
+ } | {
48
+ ok: false;
49
+ reason: string;
50
+ }>;
51
+ export type CacheKeepTarget = {
52
+ id: string;
53
+ url: string;
54
+ headers: Record<string, string>;
55
+ bodyText: string;
56
+ cacheExpiresAt: number;
57
+ dayKey: string;
58
+ };
59
+ export declare class CacheKeepManager {
60
+ private readonly options;
61
+ private readonly targets;
62
+ private timer;
63
+ constructor(options: {
64
+ loadStorage: () => Promise<AccountStorage | null>;
65
+ fetchImpl?: typeof fetch;
66
+ now?: () => number;
67
+ prepareHeaders?: (headers: Headers, target: CacheKeepTarget) => Promise<Headers> | Headers;
68
+ log?: (message: string, data?: Record<string, unknown>) => void;
69
+ });
70
+ start(): void;
71
+ stop(): void;
72
+ stats(window?: CacheKeepWindow, now?: number): {
73
+ trackedSessions: number;
74
+ nextPrewarmAt: number | undefined;
75
+ };
76
+ track(input: {
77
+ sessionId?: string | null;
78
+ url: string;
79
+ headers: Headers;
80
+ bodyText: string;
81
+ storage: AccountStorage | null;
82
+ cacheMode: string;
83
+ }): {
84
+ tracked: boolean;
85
+ reason: string;
86
+ } | {
87
+ tracked: boolean;
88
+ reason?: undefined;
89
+ };
90
+ tick(): Promise<void>;
91
+ private prewarm;
92
+ }
@@ -0,0 +1,297 @@
1
+ import { CCH_PATTERN, signRequestBody } from "./cch.js";
2
+ import { orderClaudeCodeBody } from "./claude-code.js";
3
+ export const CLAUDE_CACHE_KEEP_COMMAND_NAME = 'claude-cachekeep';
4
+ export const CACHE_KEEP_TTL_MS = 60 * 60_000;
5
+ export const CACHE_KEEP_PREWARM_LEAD_MS = 5 * 60_000;
6
+ export const CACHE_KEEP_TICK_MS = 60_000;
7
+ export const CACHE_KEEP_EXTENDED_TTL_BETA = 'extended-cache-ttl-2025-04-11';
8
+ const STATUS_TITLE = '## Claude Cache Keep Status';
9
+ const ENABLED_TITLE = '## Claude Cache Keep Enabled';
10
+ const DISABLED_TITLE = '## Claude Cache Keep Disabled';
11
+ const USAGE_TITLE = '## Claude Cache Keep Usage';
12
+ const USAGE = 'Usage: `/claude-cachekeep`, `/claude-cachekeep off`, or `/claude-cachekeep HH-HH`.';
13
+ function padHour(hour) {
14
+ return String(hour).padStart(2, '0');
15
+ }
16
+ export function parseCacheKeepCommandAction(input) {
17
+ const trimmed = input.trim();
18
+ if (!trimmed)
19
+ return { type: 'status' };
20
+ if (trimmed === 'off')
21
+ return { type: 'disable' };
22
+ const match = /^(\d{1,2})-(\d{1,2})$/.exec(trimmed);
23
+ if (!match)
24
+ return { type: 'usage' };
25
+ const startHour = Number(match[1]);
26
+ const endHour = Number(match[2]);
27
+ if (!Number.isInteger(startHour) ||
28
+ !Number.isInteger(endHour) ||
29
+ startHour < 0 ||
30
+ startHour > 23 ||
31
+ endHour < 0 ||
32
+ endHour > 23 ||
33
+ startHour === endHour) {
34
+ return { type: 'usage' };
35
+ }
36
+ return { type: 'window', startHour, endHour };
37
+ }
38
+ export function getCacheKeepWindow(storage) {
39
+ const startHour = Number(storage?.cacheKeep?.startHour);
40
+ const endHour = Number(storage?.cacheKeep?.endHour);
41
+ if (!Number.isInteger(startHour) ||
42
+ !Number.isInteger(endHour) ||
43
+ startHour < 0 ||
44
+ startHour > 23 ||
45
+ endHour < 0 ||
46
+ endHour > 23 ||
47
+ startHour === endHour) {
48
+ return undefined;
49
+ }
50
+ return { startHour, endHour };
51
+ }
52
+ export function isCacheKeepPersistentlyEnabled(storage) {
53
+ return storage?.cacheKeep?.enabled === true && !!getCacheKeepWindow(storage);
54
+ }
55
+ export function isCacheKeepHybridActive(storage) {
56
+ return (isCacheKeepPersistentlyEnabled(storage) &&
57
+ storage?.claudeCache?.enabled === true &&
58
+ storage.claudeCache.mode === 'hybrid');
59
+ }
60
+ export function isWithinCacheKeepWindow(window, now = new Date()) {
61
+ if (!window)
62
+ return false;
63
+ const hour = now.getHours();
64
+ if (window.startHour < window.endHour) {
65
+ return hour >= window.startHour && hour < window.endHour;
66
+ }
67
+ return hour >= window.startHour || hour < window.endHour;
68
+ }
69
+ export function localDayKey(now = new Date()) {
70
+ return `${now.getFullYear()}-${padHour(now.getMonth() + 1)}-${padHour(now.getDate())}`;
71
+ }
72
+ function localWindowKey(window, now = new Date()) {
73
+ if (window &&
74
+ window.startHour > window.endHour &&
75
+ now.getHours() < window.endHour) {
76
+ const previous = new Date(now);
77
+ previous.setDate(previous.getDate() - 1);
78
+ return localDayKey(previous);
79
+ }
80
+ return localDayKey(now);
81
+ }
82
+ export function buildCacheKeepStatusSummary(status) {
83
+ const window = status.window
84
+ ? `${padHour(status.window.startHour)}-${padHour(status.window.endHour)}`
85
+ : 'not configured';
86
+ const lines = [
87
+ `Cache keep: ${status.enabled ? 'enabled' : 'disabled'}`,
88
+ `Window: ${window}`,
89
+ 'Mode requirement: `/claude-cache mode hybrid` must be active.',
90
+ ];
91
+ if (typeof status.hybridActive === 'boolean') {
92
+ lines.push(`Hybrid active: ${status.hybridActive ? 'yes' : 'no'}`);
93
+ }
94
+ if (typeof status.trackedSessions === 'number') {
95
+ lines.push(`Tracked sessions: ${status.trackedSessions}`);
96
+ }
97
+ if (status.nextPrewarmAt) {
98
+ lines.push(`Next prewarm: ${new Date(status.nextPrewarmAt).toLocaleString()}`);
99
+ }
100
+ return lines.join('\n');
101
+ }
102
+ export function executeCacheKeepCommand(input) {
103
+ const action = parseCacheKeepCommandAction(input.argumentsText);
104
+ const status = {
105
+ enabled: input.enabled ?? false,
106
+ window: input.window,
107
+ trackedSessions: input.trackedSessions,
108
+ nextPrewarmAt: input.nextPrewarmAt,
109
+ hybridActive: input.hybridActive,
110
+ };
111
+ if (action.type === 'status') {
112
+ return [STATUS_TITLE, '', buildCacheKeepStatusSummary(status)].join('\n');
113
+ }
114
+ if (action.type === 'disable') {
115
+ return [
116
+ DISABLED_TITLE,
117
+ '',
118
+ buildCacheKeepStatusSummary({ ...status, enabled: false }),
119
+ ].join('\n');
120
+ }
121
+ if (action.type === 'window') {
122
+ return [
123
+ ENABLED_TITLE,
124
+ '',
125
+ buildCacheKeepStatusSummary({
126
+ ...status,
127
+ enabled: true,
128
+ window: { startHour: action.startHour, endHour: action.endHour },
129
+ }),
130
+ ].join('\n');
131
+ }
132
+ return [USAGE_TITLE, '', USAGE, '', buildCacheKeepStatusSummary(status)].join('\n');
133
+ }
134
+ function hasExplicitCacheControl(value) {
135
+ if (!value || typeof value !== 'object')
136
+ return false;
137
+ if (Array.isArray(value))
138
+ return value.some(hasExplicitCacheControl);
139
+ const record = value;
140
+ const cacheControl = record.cache_control;
141
+ if (cacheControl &&
142
+ typeof cacheControl === 'object' &&
143
+ cacheControl.type === 'ephemeral') {
144
+ return true;
145
+ }
146
+ return Object.values(record).some(hasExplicitCacheControl);
147
+ }
148
+ export async function buildCacheKeepPrewarmBody(bodyText) {
149
+ let body;
150
+ try {
151
+ body = JSON.parse(bodyText);
152
+ }
153
+ catch {
154
+ return { ok: false, reason: 'body is not valid JSON' };
155
+ }
156
+ if (!hasExplicitCacheControl(body)) {
157
+ return { ok: false, reason: 'body has no explicit cache breakpoints' };
158
+ }
159
+ const warm = structuredClone(body);
160
+ warm.max_tokens = 0;
161
+ delete warm.stream;
162
+ const thinking = warm.thinking;
163
+ if (thinking?.type === 'enabled')
164
+ delete warm.thinking;
165
+ const outputConfig = warm.output_config;
166
+ if (outputConfig?.format)
167
+ delete warm.output_config;
168
+ const toolChoice = warm.tool_choice;
169
+ if (toolChoice?.type === 'tool' || toolChoice?.type === 'any') {
170
+ delete warm.tool_choice;
171
+ }
172
+ const unsignedBodyText = JSON.stringify(orderClaudeCodeBody(warm)).replace(CCH_PATTERN, 'cch=00000;');
173
+ const signedBodyText = await signRequestBody(unsignedBodyText);
174
+ return { ok: true, bodyText: signedBodyText };
175
+ }
176
+ export class CacheKeepManager {
177
+ options;
178
+ targets = new Map();
179
+ timer = null;
180
+ constructor(options) {
181
+ this.options = options;
182
+ }
183
+ start() {
184
+ if (this.timer)
185
+ return;
186
+ this.timer = setInterval(() => {
187
+ void this.tick().catch((error) => {
188
+ this.options.log?.('[cachekeep] tick failed', {
189
+ error: error instanceof Error ? error.message : String(error),
190
+ });
191
+ });
192
+ }, CACHE_KEEP_TICK_MS);
193
+ if ('unref' in this.timer)
194
+ this.timer.unref();
195
+ }
196
+ stop() {
197
+ if (this.timer)
198
+ clearInterval(this.timer);
199
+ this.timer = null;
200
+ }
201
+ stats(window, now = this.options.now?.() ?? Date.now()) {
202
+ const today = localWindowKey(window, new Date(now));
203
+ const targets = [...this.targets.values()].filter((target) => target.dayKey === today);
204
+ const nextPrewarmAt = targets.length
205
+ ? Math.min(...targets.map((target) => target.cacheExpiresAt - CACHE_KEEP_PREWARM_LEAD_MS))
206
+ : undefined;
207
+ return { trackedSessions: targets.length, nextPrewarmAt };
208
+ }
209
+ track(input) {
210
+ if (!input.sessionId)
211
+ return { tracked: false, reason: 'missing session id' };
212
+ if (!isCacheKeepPersistentlyEnabled(input.storage)) {
213
+ return { tracked: false, reason: 'cachekeep disabled' };
214
+ }
215
+ if (input.cacheMode !== 'hybrid') {
216
+ return { tracked: false, reason: 'cache mode is not hybrid' };
217
+ }
218
+ const now = this.options.now?.() ?? Date.now();
219
+ const window = getCacheKeepWindow(input.storage);
220
+ if (!isWithinCacheKeepWindow(window, new Date(now))) {
221
+ return { tracked: false, reason: 'outside configured window' };
222
+ }
223
+ const headers = {};
224
+ input.headers.forEach((value, key) => {
225
+ headers[key] = value;
226
+ });
227
+ this.targets.set(input.sessionId, {
228
+ id: input.sessionId,
229
+ url: input.url,
230
+ headers,
231
+ bodyText: input.bodyText,
232
+ cacheExpiresAt: now + CACHE_KEEP_TTL_MS,
233
+ dayKey: localWindowKey(window, new Date(now)),
234
+ });
235
+ this.start();
236
+ return { tracked: true };
237
+ }
238
+ async tick() {
239
+ const storage = await this.options.loadStorage();
240
+ const window = getCacheKeepWindow(storage);
241
+ const now = this.options.now?.() ?? Date.now();
242
+ const today = localWindowKey(window, new Date(now));
243
+ for (const [id, target] of this.targets) {
244
+ if (target.dayKey !== today)
245
+ this.targets.delete(id);
246
+ }
247
+ if (!this.targets.size) {
248
+ this.stop();
249
+ return;
250
+ }
251
+ if (!isCacheKeepHybridActive(storage))
252
+ return;
253
+ if (!isWithinCacheKeepWindow(window, new Date(now)))
254
+ return;
255
+ const dueAt = now + CACHE_KEEP_PREWARM_LEAD_MS;
256
+ for (const target of this.targets.values()) {
257
+ if (target.cacheExpiresAt > dueAt)
258
+ continue;
259
+ await this.prewarm(target, now);
260
+ }
261
+ }
262
+ async prewarm(target, now) {
263
+ const prewarm = await buildCacheKeepPrewarmBody(target.bodyText);
264
+ if (!prewarm.ok) {
265
+ this.options.log?.('[cachekeep] prewarm skipped', {
266
+ session: target.id,
267
+ reason: prewarm.reason,
268
+ });
269
+ this.targets.delete(target.id);
270
+ return;
271
+ }
272
+ const fetchImpl = this.options.fetchImpl ?? fetch;
273
+ const prewarmTarget = { ...target, bodyText: prewarm.bodyText };
274
+ const headers = this.options.prepareHeaders
275
+ ? await this.options.prepareHeaders(new Headers(target.headers), prewarmTarget)
276
+ : new Headers(target.headers);
277
+ headers.delete('content-length');
278
+ headers.delete('transfer-encoding');
279
+ const response = await fetchImpl(target.url, {
280
+ method: 'POST',
281
+ headers,
282
+ body: prewarm.bodyText,
283
+ });
284
+ if (!response.ok) {
285
+ this.options.log?.('[cachekeep] prewarm failed', {
286
+ session: target.id,
287
+ status: response.status,
288
+ body: await response.text().catch(() => ''),
289
+ });
290
+ target.cacheExpiresAt = now + CACHE_KEEP_PREWARM_LEAD_MS + 5 * 60_000;
291
+ return;
292
+ }
293
+ await response.body?.cancel().catch(() => { });
294
+ target.cacheExpiresAt = now + CACHE_KEEP_TTL_MS;
295
+ this.options.log?.('[cachekeep] prewarm succeeded', { session: target.id });
296
+ }
297
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './accounts.ts';
2
2
  export * from './auth.ts';
3
3
  export * from './cache1h.ts';
4
+ export * from './cachekeep.ts';
4
5
  export * from './cch.ts';
5
6
  export * from './claude-code.ts';
6
7
  export * from './constants.ts';
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./accounts.js";
2
2
  export * from "./auth.js";
3
3
  export * from "./cache1h.js";
4
+ export * from "./cachekeep.js";
4
5
  export * from "./cch.js";
5
6
  export * from "./claude-code.js";
6
7
  export * from "./constants.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/anthropic-auth-core",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",