@chrischall/mcp-utils 0.15.0 → 0.17.0

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
@@ -32,7 +32,7 @@ import light:
32
32
  | Import | Contents |
33
33
  | --- | --- |
34
34
  | `@chrischall/mcp-utils` | core barrel: `server` + `response` + `errors` + `config` + `fs` + `http` + `concurrency` + `dates` + `zod` + `auth` + `scrape` |
35
- | `@chrischall/mcp-utils/session` | session registry, session store, token manager, cookie-session manager |
35
+ | `@chrischall/mcp-utils/session` | session registry, session store, state persistence, token manager, cookie-session manager |
36
36
  | `@chrischall/mcp-utils/fetchproxy` | fetchproxy transport adapter, bot-wall / retry / concurrency helpers |
37
37
  | `@chrischall/mcp-utils/html` | opt-in HTML scraping helpers (needs `node-html-parser`) |
38
38
  | `@chrischall/mcp-utils/scrape` | convenience alias for the zero-dep `scrape` module (also in the core barrel) |
@@ -485,6 +485,115 @@ Replaces the hand-rolled re-login / single-flight / 401-replay code in
485
485
  `artsonia-mcp`, `canvas-parent-mcp`, `evite-mcp`, `signupgenius-mcp`, and
486
486
  `skylight-mcp`.
487
487
 
488
+ #### Surviving a restart — `StatePersistence` *(opt-in)*
489
+
490
+ Both managers own a credential only for the life of the process. On a
491
+ scale-to-zero host that means a full login on every cold start — children idle
492
+ out after ten minutes, several services rate-limit the login endpoint, and one
493
+ escalates repeated attempts to a captcha that breaks server-side auth outright.
494
+ Pass `persistence` and the credential survives instead:
495
+
496
+ ```ts
497
+ import {
498
+ TokenManager,
499
+ createFileStatePersistence,
500
+ resolveStateDir,
501
+ type BearerTokens,
502
+ } from '@chrischall/mcp-utils/session';
503
+ import { join } from 'node:path';
504
+
505
+ const tokens = new TokenManager({
506
+ // Function form: run the login ONLY when nothing usable was restored.
507
+ initial: () => loginWithPassword(),
508
+ refresh: (rt) => exchangeRefreshToken(rt),
509
+ persistence: createFileStatePersistence<BearerTokens>({
510
+ filePath: join(resolveStateDir({ subdir: '.acme-mcp' }), 'tokens.json'),
511
+ }),
512
+ });
513
+ ```
514
+
515
+ What that buys, in order of how often it applies: a stored token that is still
516
+ valid costs **nothing**; a stored token that has expired but carries a refresh
517
+ token costs **one refresh** instead of a login; only an empty or unusable store
518
+ runs `initial`. A refresh token revoked between runs is not terminal — the
519
+ stored copy is discarded and the login re-runs, so a stale file cannot brick the
520
+ server. A *transient* refresh failure is treated differently: a `RateLimitedError`,
521
+ a `RequestTimeoutError` or a 5xx `ApiError` surfaces to the caller with the
522
+ refresh token left intact, because destroying a valid credential and burning a
523
+ login on a passing outage is the cost this feature exists to avoid. Override
524
+ `isRefreshRevoked` for a service that signals revocation some other way.
525
+
526
+ `createFileStatePersistence` writes atomically (temp file + rename), leaves the
527
+ file `0600`, and creates any missing directory `0700` — but does **not**
528
+ re-permission a directory that already exists, since a bare `resolveStateDir()`
529
+ is `$HOME` and `mcp-host` creates the data dir before the child starts. It never
530
+ throws: a read-only or full disk degrades to in-memory operation, costing a
531
+ login rather than a failed request. `resolveStateDir` prefers `MCP_DATA_DIR` — the variable `mcp-host`
532
+ injects for a registration with `state.dataDir: true` — then `HOME`, then the OS
533
+ home directory. It reads both through `readEnvVar`, so blank values, the
534
+ `'null'` / `'undefined'` sentinels and unexpanded `${...}` placeholders are all
535
+ treated as unset (`MCP_DATA_DIR=null` would otherwise be a *relative* `./null`
536
+ directory, quietly parking the credential under the process cwd).
537
+
538
+ > On `mcp-host`, set `state.dataDir: true` in the repo's `mint.yaml` when you
539
+ > adopt this. Without it the child's `$HOME` is on the container rootfs, which
540
+ > an idle-stop discards — the runner's unpersisted-state detector will report
541
+ > the omission, but the writes still vanish.
542
+
543
+ `CookieSessionManager` takes the same option, storing `{ session, sessionAt }`
544
+ so `maxAgeMs` keeps counting from the original login. Its `invalidate()` clears
545
+ the stored copy — without that, a session detected as expired would be read back
546
+ off disk and the expiry would loop.
547
+
548
+ #### Capabilities lifted from the hand-rolled stores
549
+
550
+ Four repos (`freshbooks-mcp`, `kiaaccess-mcp`, `alphaportal-mcp`, `vibo-mcp`)
551
+ persisted tokens before this helper existed. Auditing them before migrating
552
+ turned up behaviour the first cut did not have:
553
+
554
+ - **`onPersistError`** — a failed write is swallowed by default, which is right
555
+ when it merely costs a future re-login. It is wrong for a service that rotates
556
+ **single-use** refresh tokens: the old one is already spent upstream, so a new
557
+ one that never reaches disk locks the account out on the next start. Throw
558
+ from the hook to make the write fatal (`freshbooks-mcp`'s case). Accordingly
559
+ `createFileStatePersistence.save` now *reports* a failed write by throwing;
560
+ `load` stays total. A failure raised this way is wrapped in a
561
+ `StatePersistenceError` so it can never be mistaken for a revoked credential —
562
+ the refresh that produced it succeeded, so discarding the stored record would
563
+ destroy the only surviving copy, which is the lockout the option exists to
564
+ prevent.
565
+ - **`boundTo`** — bind a record to the credential that minted it, so a rotated
566
+ password or a re-run OAuth bootstrap discards the cache instead of being
567
+ shadowed by it. Only a salted HMAC digest is written, never the credential, and
568
+ the salt is fresh per write so the same credential never leaves the same
569
+ artifact twice. It is a change-detector, not a password store — pass a
570
+ non-secret discriminator where you have one. (`freshbooks-mcp` tracked this as
571
+ `seededFromEnv`, storing the raw token.)
572
+ - **`createKeyedFileStatePersistence`** — many records in one file, keyed by
573
+ account, each key handed out as a plain `StatePersistence` a manager takes
574
+ directly. Required for any server authenticating as more than one identity,
575
+ and for anything serving several users from one process, where a
576
+ single-record file would hand one user's token to the next. Keys normalize
577
+ trim+lowercase by default, because they are account identities, not origins.
578
+ Writes are whole-file read-modify-write, so two processes saving different
579
+ keys at the same instant can drop one update — the loser re-authenticates
580
+ rather than reading anything wrong, which is the right trade for a credential
581
+ cache and would not be for a general store.
582
+ - **`resolveStateFile({ envVar, subdir, fileName })`** — an env override for the
583
+ path, checked through the same hardened `readEnvVar`. Every one of the four
584
+ had one, and every one used it to keep its test suite off the developer's real
585
+ `$HOME`.
586
+
587
+ Records are written in a small envelope (`{ v: 1, boundTo?, state }`). A bare
588
+ record written by an earlier version is still read, so nothing already on disk
589
+ is lost.
590
+
591
+ Persistence is **opt-in throughout**: a manager constructed without it behaves
592
+ exactly as before, and no credential reaches a disk because a dependency was
593
+ upgraded. The interface is two methods (`load` / `save`, plus an optional
594
+ `clear`), each allowed to be async, so a backend other than the local filesystem
595
+ can be dropped in.
596
+
488
597
  ### `fetchproxy` — transport adapter *(subpath, optional peer)*
489
598
 
490
599
  ```ts
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Session scaffolding for the MCP fleet — four related-but-distinct surfaces
2
+ * Session scaffolding for the MCP fleet — five related-but-distinct surfaces
3
3
  * consolidated behind one subpath (`@chrischall/mcp-utils/session`):
4
4
  *
5
5
  * 1. {@link SessionRegistry} — an *ephemeral, in-memory* registry of signed-in
@@ -12,12 +12,18 @@
12
12
  * (0600 file / 0700 dir), normalized keys, and a most-recently-used "active"
13
13
  * pointer. Used by ofw/creditkarma/honeybook.
14
14
  *
15
- * 3. {@link TokenManager} — a bearer-token lifecycle manager: proactive refresh
16
- * inside a 5-minute skew window, reactive 401-replay, and a single-flight
17
- * semaphore so concurrent callers coalesce into ONE refresh. Used by
18
- * skylight/canvas/creditkarma/honeybook/zola.
15
+ * 3. {@link StatePersistence} — the opt-in seam that lets the two managers
16
+ * below survive a process restart, with {@link createFileStatePersistence}
17
+ * (atomic, 0600) and {@link resolveStateDir} (`MCP_DATA_DIR` `HOME`) as
18
+ * the disk-backed default. Without it a scale-to-zero host re-runs a full
19
+ * login on every cold start, against endpoints that often rate-limit it.
19
20
  *
20
- * 4. {@link CookieSessionManager} — the cookie-session analog of TokenManager:
21
+ * 4. {@link TokenManager} — a bearer-token lifecycle manager: a lazily
22
+ * bootstrapped login, proactive refresh inside a 5-minute skew window,
23
+ * reactive 401-replay, and a single-flight semaphore so concurrent callers
24
+ * coalesce into ONE exchange. Used by skylight/canvas/creditkarma/honeybook/zola.
25
+ *
26
+ * 5. {@link CookieSessionManager} — the cookie-session analog of TokenManager:
21
27
  * a single-flight login + reactive expiry-replay (with heuristic, not just
22
28
  * status-code, expiry detection) + clear-on-settle so a rejected login never
23
29
  * sticks. Used by artsonia/canvas/evite/signupgenius/skylight.
@@ -179,6 +185,212 @@ export declare class SessionStore<T extends Record<string, unknown>> {
179
185
  /** Clear in-memory state without touching disk. Test helper. */
180
186
  resetForTest(): void;
181
187
  }
188
+ /**
189
+ * A place to keep a credential between processes.
190
+ *
191
+ * Why this exists: {@link TokenManager} and {@link CookieSessionManager} own a
192
+ * credential's lifecycle *within* a process, and every fleet server used to
193
+ * throw that credential away on exit — so a cold start re-ran the full login
194
+ * even when a valid refresh token had been minted seconds earlier. On
195
+ * `mcp-host` that is the normal case, not the exception: children idle out
196
+ * after ten minutes and the machine scales to zero behind them. Several
197
+ * services rate-limit the login endpoint, and at least one (per the kiaaccess
198
+ * notes) escalates repeated attempts to a captcha that breaks server-side auth
199
+ * for the account permanently. Re-login is not always a free retry.
200
+ *
201
+ * Deliberately opt-in: a manager given no `persistence` behaves exactly as it
202
+ * did before, and no credential reaches a disk because a package was upgraded.
203
+ *
204
+ * Both methods may be sync or async. The fleet's own implementation
205
+ * ({@link createFileStatePersistence}) is sync — it writes one small file — but
206
+ * the async signature leaves room for a backend that has to go over a wire.
207
+ *
208
+ * **A failing implementation should throw.** Whether losing a write is
209
+ * survivable is the MANAGER's call, not the store's: for most services a failed
210
+ * save costs a re-login, but freshbooks-mcp rotates single-use refresh tokens,
211
+ * so a new token that does not reach disk locks the account out on the next
212
+ * start. The managers catch every call and, by default, swallow it — pass
213
+ * `onPersistError` to observe or to make it fatal.
214
+ */
215
+ export interface StatePersistence<T> {
216
+ /** Read the stored state; `null` when absent, unparseable, or unusable. */
217
+ load(): T | null | Promise<T | null>;
218
+ /** Write state, replacing whatever was there. */
219
+ save(state: T): void | Promise<void>;
220
+ /**
221
+ * Discard the stored state. Optional, but a manager that detects its stored
222
+ * credential is no good calls this — without it, an expired session is read
223
+ * straight back off disk and the expiry loops.
224
+ */
225
+ clear?(): void | Promise<void>;
226
+ }
227
+ /**
228
+ * Wraps a failure that came from WRITING state, so the managers can tell it
229
+ * apart from a failure of the credential itself.
230
+ *
231
+ * This distinction is load-bearing, not cosmetic. `TokenManager` recovers from a
232
+ * rejected refresh by discarding the stored record and re-running the login —
233
+ * correct for a revoked token, catastrophic for a disk error, because the
234
+ * refresh that just SUCCEEDED already burned the old token upstream. Deleting
235
+ * the record at that point is precisely the lockout `onPersistError` exists to
236
+ * prevent, so a persistence failure is never routed into that recovery.
237
+ */
238
+ export declare class StatePersistenceError extends Error {
239
+ readonly cause: unknown;
240
+ constructor(cause: unknown);
241
+ }
242
+ /** Options for {@link createFileStatePersistence}. */
243
+ export interface FileStatePersistenceOptions<T> {
244
+ /** Absolute path to the JSON file. Parent directories are created as needed. */
245
+ filePath: string;
246
+ /**
247
+ * Bind the record to the credential that minted it. Pass the value whose change
248
+ * should invalidate the cache — an env-supplied refresh token, an account id.
249
+ * Only a salted HMAC digest is written, never the value, and the salt is fresh
250
+ * per write. It is a change-detector rather than a password store, so prefer a
251
+ * non-secret discriminator where one exists.
252
+ *
253
+ * Without this a rotated credential is silently shadowed by a cache minted
254
+ * from the old one: the operator re-bootstraps, and the server keeps using
255
+ * what it had. freshbooks-mcp discovered this and tracked it as
256
+ * `seededFromEnv`; this is the generalised, non-secret-storing form.
257
+ *
258
+ * A record with no binding is not accepted when one is required — it cannot
259
+ * be shown to belong to this credential.
260
+ */
261
+ boundTo?: string;
262
+ /**
263
+ * Narrow the parsed JSON to `T`, returning `null` to reject it. Without this
264
+ * any well-formed JSON is handed back and the caller must check the shape —
265
+ * the managers do, but a custom consumer should pass a guard.
266
+ */
267
+ validate?: (raw: unknown) => T | null;
268
+ }
269
+ /**
270
+ * File-backed {@link StatePersistence}. `load` never throws — an absent, corrupt
271
+ * or rejected record is simply `null`. `save` DOES throw on a failed write, so
272
+ * the manager above it can decide what that means.
273
+ *
274
+ * The file is `0600`, re-asserted after
275
+ * the write because `mode` only applies on creation. A directory is created
276
+ * `0700` and re-asserted the same way — but ONLY one this call creates: a bare
277
+ * {@link resolveStateDir} is `$HOME`, and on `mcp-host` the data dir exists
278
+ * before the child starts, so re-permissioning a pre-existing directory would
279
+ * be an invasive side effect of writing one token file rather than hardening.
280
+ *
281
+ * Two differences from {@link SessionStore}, which is why this is its own
282
+ * implementation rather than a wrapper over it. It holds ONE record rather than
283
+ * a keyed collection; and it replaces the file **atomically** — written to a
284
+ * temp file beside it, then renamed over the target — because two children of
285
+ * the same registration can share a data directory, and a half-written token
286
+ * file that parses as valid JSON is worse than none.
287
+ *
288
+ * A load failure (absent, corrupt, rejected by `validate`) returns `null`; a
289
+ * save failure throws, leaving the previous file intact — the atomic replace
290
+ * means a failed write never damages what was already there. On `mcp-host` this
291
+ * belongs under {@link resolveStateDir}, which needs
292
+ * the registration to declare `state.dataDir: true` — the runner's
293
+ * unpersisted-state detector will report the omission rather than let the
294
+ * writes silently vanish on the next idle-stop.
295
+ */
296
+ export declare function createFileStatePersistence<T>(opts: FileStatePersistenceOptions<T>): Required<StatePersistence<T>>;
297
+ /** Options for {@link createKeyedFileStatePersistence}. */
298
+ export interface KeyedFileStatePersistenceOptions<T> {
299
+ /** Absolute path to the shared JSON file. Parent dirs are created as needed. */
300
+ filePath: string;
301
+ /** Narrow a stored record to `T`, returning `null` to reject it. */
302
+ validate?: (raw: unknown) => T | null;
303
+ /**
304
+ * Normalize a key before storing or looking up. Defaults to trim + lowercase,
305
+ * because these keys are account identities (emails, usernames) — NOT origins.
306
+ * kiaaccess-mcp and alphaportal-mcp both had to override `SessionStore`'s
307
+ * origin normalizer for exactly this reason, so it is the default here.
308
+ */
309
+ normalizeKey?: (key: string) => string;
310
+ }
311
+ /** A keyed store: hand each key out as its own {@link StatePersistence}. */
312
+ export interface KeyedStatePersistence<T> {
313
+ /**
314
+ * A single-record view of one key, shaped exactly like the persistence the
315
+ * managers take — so a multi-account server gives each account its own
316
+ * {@link TokenManager} over one shared file.
317
+ */
318
+ forKey(key: string): Required<StatePersistence<T>>;
319
+ /** The normalized keys currently held. */
320
+ keys(): string[];
321
+ }
322
+ /**
323
+ * Many records in one file, keyed by account.
324
+ *
325
+ * {@link createFileStatePersistence} holds exactly one record, which is wrong
326
+ * for any server that authenticates as more than one identity — and actively
327
+ * unsafe for one that serves several users from a single process, where a
328
+ * single-record file would hand one user's token to the next. kiaaccess-mcp and
329
+ * alphaportal-mcp both hand-rolled this over {@link SessionStore}; this is the
330
+ * shared form, with the same atomic-replace and `0600`/`0700` hardening as the
331
+ * single-record store.
332
+ *
333
+ * Reads go through the file each time rather than an in-process cache, so a
334
+ * record written by a SIBLING process is picked up — the property kiaaccess-mcp's
335
+ * "constructed per call" comment exists to preserve.
336
+ *
337
+ * WRITES, though, are whole-file read-modify-write: two processes saving
338
+ * DIFFERENT keys at the same instant can lose one of the two, because each
339
+ * rewrites the map it read. The replace is atomic, so the file is never torn —
340
+ * only a concurrent sibling's update can be dropped, and the loser re-authenticates
341
+ * rather than reading anything wrong. That is acceptable for credential caches
342
+ * (rare writes, self-healing) and would not be for a general-purpose store. If
343
+ * that ever stops being true the fix is a lock file, not a bigger read.
344
+ */
345
+ export declare function createKeyedFileStatePersistence<T>(opts: KeyedFileStatePersistenceOptions<T>): KeyedStatePersistence<T>;
346
+ /** Options for {@link resolveStateDir}. */
347
+ export interface ResolveStateDirOptions {
348
+ /** Environment to read (defaults to `process.env`) — injectable for tests. */
349
+ env?: Record<string, string | undefined>;
350
+ /** Optional service-scoped subdirectory to join onto the base. */
351
+ subdir?: string;
352
+ }
353
+ /**
354
+ * Where a server should keep state that must survive a restart.
355
+ *
356
+ * `MCP_DATA_DIR` first — that is the variable `mcp-host` injects for a
357
+ * registration with `state.dataDir: true`, pointing at a path on the Fly volume
358
+ * keyed by the registration itself (a slot `$HOME` is handed out by arrival
359
+ * order and moves between boots, which is why the data dir is the fix and a
360
+ * bigger rootfs is not). Then `HOME`, then the OS home directory.
361
+ *
362
+ * Blank and unexpanded-placeholder values (`${MCP_DATA_DIR}`, the shape a host
363
+ * config leaves behind when a variable was never substituted) are ignored
364
+ * rather than used as a literal directory name — the same hardening
365
+ * {@link readEnvVar} applies.
366
+ */
367
+ export declare function resolveStateDir(opts?: ResolveStateDirOptions): string;
368
+ /** Options for {@link resolveStateFile}. */
369
+ export interface ResolveStateFileOptions extends ResolveStateDirOptions {
370
+ /**
371
+ * An env var naming the file outright, checked first. Every fleet repo that
372
+ * hand-rolled persistence has one (`KIA_SESSION_FILE`, `VIBO_SESSION_FILE`,
373
+ * `ALPHAPORTAL_SESSION_FILE`) and every one of them uses it to keep its test
374
+ * suite off the developer's real `$HOME` — which is worth having by default
375
+ * rather than rediscovering per repo.
376
+ */
377
+ envVar?: string;
378
+ /** File name inside the resolved directory. */
379
+ fileName: string;
380
+ }
381
+ /**
382
+ * The full path to a state file: `<envVar>` if set, else
383
+ * {@link resolveStateDir}`/<subdir>/<fileName>`.
384
+ *
385
+ * The override goes through the same hardened {@link readEnvVar} as the base, so
386
+ * a host forwarding an unexpanded `${...}` — or the literal `null` — falls back
387
+ * rather than creating a relative directory of that name under the process cwd.
388
+ * The result is always absolute: `~` expands against the same home the fallback
389
+ * uses, and anything relative is resolved, because
390
+ * {@link FileStatePersistenceOptions.filePath} is documented as absolute and a
391
+ * cwd-relative store would move with the process.
392
+ */
393
+ export declare function resolveStateFile(opts: ResolveStateFileOptions): string;
182
394
  /** Refresh proactively this many ms before the access token expires. */
183
395
  export declare const TOKEN_REFRESH_SKEW_MS: number;
184
396
  /** A bearer access token + (optional) refresh token + absolute expiry. */
@@ -198,8 +410,21 @@ export interface RefreshedTokens {
198
410
  }
199
411
  /** Options for {@link TokenManager}. */
200
412
  export interface TokenManagerOptions {
201
- /** Initial tokens (typically from env or a one-shot bootstrap). */
202
- initial: BearerTokens;
413
+ /**
414
+ * The starting tokens — either the tokens themselves, or a **bootstrap
415
+ * function** that mints them (typically a full login).
416
+ *
417
+ * Pass the function form to get the persistence benefit: it is invoked only
418
+ * when {@link TokenManagerOptions.persistence} has nothing usable, so a
419
+ * restart that finds a stored token never logs in at all, and one that finds
420
+ * an expired token with a refresh token spends a refresh instead of a login.
421
+ * It is single-flighted like every other credential operation here, so a
422
+ * burst of first calls hits a rate-limited login endpoint exactly once.
423
+ *
424
+ * The eager object form is unchanged: the caller already paid for the login,
425
+ * so persistence is not consulted and the tokens are used as given.
426
+ */
427
+ initial: BearerTokens | (() => Promise<BearerTokens>);
203
428
  /**
204
429
  * Exchange the current refresh token for fresh tokens. Called at most once
205
430
  * per concurrent burst (the in-flight promise is shared).
@@ -210,36 +435,124 @@ export interface TokenManagerOptions {
210
435
  * refresh). Defaults to {@link TOKEN_REFRESH_SKEW_MS} (5 minutes).
211
436
  */
212
437
  skewMs?: number;
438
+ /**
439
+ * Keep tokens across process restarts. Read once on the bootstrap path
440
+ * (function-form `initial` only), written after every successful bootstrap
441
+ * and refresh, including rotation. Omit for the previous in-memory-only
442
+ * behaviour. See {@link StatePersistence}.
443
+ */
444
+ persistence?: StatePersistence<BearerTokens>;
445
+ /**
446
+ * Decide whether a {@link TokenManagerOptions.refresh} rejection means the
447
+ * credential itself is dead (re-mint via the bootstrap) or the endpoint was
448
+ * merely unreachable (surface it, keep the token).
449
+ *
450
+ * The distinction matters in both directions. Treating a transient failure as
451
+ * revocation deletes a still-VALID refresh token and burns a login against an
452
+ * endpoint that may rate-limit or escalate to a captcha — the exact cost this
453
+ * whole feature exists to avoid. Treating a real revocation as transient
454
+ * leaves the server broken until someone deletes the stored file by hand.
455
+ *
456
+ * The default resolves that by only excusing failures that are transient *by
457
+ * construction* — a {@link RateLimitedError}, a {@link RequestTimeoutError},
458
+ * or an {@link ApiError} with a 5xx status. Anything else is assumed to be a
459
+ * dead credential, which keeps the recover-from-revocation guarantee. Override
460
+ * it for a service that signals revocation some other way (or, conversely, one
461
+ * that answers a live token with a 5xx). Mirrors the permanent-vs-transient
462
+ * split {@link CookieSessionManagerOptions.isPermanentError} already makes.
463
+ */
464
+ isRefreshRevoked?: (err: unknown) => boolean;
465
+ /**
466
+ * Called when a {@link TokenManagerOptions.persistence} write fails.
467
+ *
468
+ * Default: the failure is swallowed and the request proceeds on the
469
+ * in-memory token — right when a lost write merely costs a future re-login.
470
+ * It is WRONG when the service rotates single-use refresh tokens: the old one
471
+ * is already spent upstream, so a new one that never reaches disk locks the
472
+ * account out on the next start. **Throw from this hook to make the write
473
+ * fatal**, with a message naming the recovery (freshbooks-mcp's case).
474
+ */
475
+ onPersistError?: (err: unknown) => void;
476
+ /** Injectable clock (defaults to `Date.now`) — for tests. */
477
+ now?: () => number;
213
478
  }
214
479
  /**
215
480
  * Manages a bearer access token's lifecycle:
216
481
  *
482
+ * - **Lazy bootstrap:** with a function-form {@link TokenManagerOptions.initial}
483
+ * the login runs on first use, and only if {@link TokenManagerOptions.persistence}
484
+ * has no usable token — the difference between a cold start costing a login
485
+ * and costing nothing.
217
486
  * - **Proactive:** {@link TokenManager.getAccessToken} refreshes when the token
218
487
  * is within `skewMs` (default 5 min) of expiry, returning a still-valid token.
219
488
  * - **Reactive:** {@link TokenManager.withAuth} runs a request, and on a `401`
220
489
  * refreshes once and replays exactly once (no infinite loop).
221
- * - **Race-safe:** concurrent refreshes coalesce onto a single in-flight promise
222
- * (semaphore), so a burst of callers triggers exactly ONE token exchange. The
223
- * in-flight promise is cleared on settle so a later refresh can run again.
490
+ * - **Race-safe:** concurrent refreshes (and concurrent bootstraps) coalesce
491
+ * onto a single in-flight promise, so a burst of callers triggers exactly ONE
492
+ * exchange. The in-flight promise is cleared on settle so a later attempt can
493
+ * run again — a rejected bootstrap never sticks.
494
+ * - **Recoverable:** when a refresh fails and a bootstrap function is available,
495
+ * the stored credential is discarded and the login re-runs. A refresh token
496
+ * revoked between two runs of the process must not brick the server.
224
497
  */
225
498
  export declare class TokenManager {
226
- private accessToken;
227
- private refreshToken;
228
- private expiresAt;
499
+ private tokens;
500
+ private readonly bootstrapFn;
229
501
  private readonly refreshFn;
230
502
  private readonly skewMs;
503
+ private readonly persistence;
504
+ private readonly now;
505
+ private readonly isRefreshRevokedFn;
506
+ private readonly onPersistErrorFn;
231
507
  private inFlight;
508
+ private bootstrapInFlight;
509
+ /**
510
+ * Persistence is consulted at most once per process. Without this the
511
+ * revoked-token recovery below re-reads the SAME rejected record — `clear()`
512
+ * is optional on {@link StatePersistence} and its failures are swallowed, so
513
+ * recovery must not depend on it. After the first read the in-memory tokens
514
+ * (or their deliberate absence) are the truth.
515
+ */
516
+ private persistenceRead;
232
517
  constructor(opts: TokenManagerOptions);
233
518
  /** Whether the token is within the skew window of (or past) expiry. */
234
519
  private needsRefresh;
520
+ /**
521
+ * A stored token is worth using when it is still valid, OR when it carries a
522
+ * refresh token — an expired-but-refreshable token still saves the login,
523
+ * which is the expensive half.
524
+ */
525
+ private isUsable;
526
+ /** Read persisted tokens, guarding shape and usability. Never throws. */
527
+ private loadPersisted;
528
+ /**
529
+ * Write tokens. Silent by default (a lost write costs a future login, not this
530
+ * request); throws a {@link StatePersistenceError} when `onPersistError` does.
531
+ */
532
+ private persist;
533
+ /** Discard persisted tokens (a refresh they could not satisfy). Never throws. */
534
+ private clearPersisted;
535
+ /** The current tokens, single-flighting the bootstrap if there are none. */
536
+ private ensureTokens;
537
+ /** One bootstrap attempt: persisted tokens if usable, else the login. */
538
+ private runBootstrap;
235
539
  /**
236
540
  * Single-flight refresh. Concurrent callers share one in-flight promise; it is
237
541
  * cleared on settle (success or failure) so a subsequent refresh can proceed.
238
542
  */
239
543
  refreshNow(): Promise<void>;
544
+ /** One refresh attempt against the current refresh token. */
545
+ private runRefresh;
546
+ /**
547
+ * Recover from a refresh the current credential could not satisfy — commonly
548
+ * a refresh token restored from a previous process and revoked since. Without
549
+ * a bootstrap to fall back on this is terminal; with one, re-minting beats
550
+ * staying broken forever. Shared so the two entry points cannot diverge.
551
+ */
552
+ private reBootstrap;
240
553
  /** Get a valid access token, refreshing proactively inside the skew window. */
241
554
  getAccessToken(): Promise<string>;
242
- /** Current absolute expiry (epoch ms). */
555
+ /** Current absolute expiry (epoch ms), or `0` before the first bootstrap. */
243
556
  getExpiresAt(): number;
244
557
  /**
245
558
  * Run an authenticated request with reactive 401-replay. `call` receives a
@@ -329,6 +642,43 @@ export interface CookieSessionManagerOptions<S, R = Response> {
329
642
  * login failure to the caller instead of the stale response.
330
643
  */
331
644
  onReplayLoginError?: (err: unknown) => void;
645
+ /**
646
+ * Keep the session across process restarts. Read ONCE, on the first login
647
+ * path, and written after every successful login and {@link
648
+ * CookieSessionManager.seed}. {@link CookieSessionManager.invalidate} clears
649
+ * it — without that, a session detected as expired would be read straight
650
+ * back off disk and the expiry would loop.
651
+ *
652
+ * The stored envelope carries the login time alongside the session so
653
+ * {@link CookieSessionManagerOptions.maxAgeMs} keeps counting from the
654
+ * original login rather than restarting at the restore. Omit for the previous
655
+ * in-memory-only behaviour. See {@link StatePersistence}.
656
+ */
657
+ persistence?: StatePersistence<PersistedCookieSession<S>>;
658
+ /**
659
+ * Called when a {@link CookieSessionManagerOptions.persistence} write fails.
660
+ * Swallowed by default — the in-memory session is still usable.
661
+ *
662
+ * Where a throwing hook goes depends on which write failed. The login path
663
+ * awaits its write, so the error reaches a direct {@link CookieSessionManager.ensure}
664
+ * caller — but it is deliberately NOT offered to
665
+ * {@link CookieSessionManagerOptions.isPermanentError} first, because caching a
666
+ * disk error as a permanent config failure would brick every later `ensure()`.
667
+ *
668
+ * Two places it does NOT reach the caller. {@link CookieSessionManager.withSession}'s
669
+ * expiry replay catches `ensure()` and returns the stale response by design, so
670
+ * a "fatal" write there is dropped unless
671
+ * {@link CookieSessionManagerOptions.onReplayLoginError} rethrows too. And the
672
+ * `seed()`/`invalidate()` writes are fire-and-forget onto the ordering chain,
673
+ * whose retained tail swallows.
674
+ */
675
+ onPersistError?: (err: unknown) => void;
676
+ }
677
+ /** What {@link CookieSessionManagerOptions.persistence} stores: a session plus its login time. */
678
+ export interface PersistedCookieSession<S> {
679
+ session: S;
680
+ /** Epoch ms the session was minted or seeded — the `maxAgeMs` clock. */
681
+ sessionAt: number;
332
682
  }
333
683
  /**
334
684
  * Cookie-session analog of {@link TokenManager}: owns a site's cookie-session
@@ -382,6 +732,17 @@ export declare class CookieSessionManager<S = CookieSession, R = Response> {
382
732
  private readonly maxAgeMs;
383
733
  private readonly now;
384
734
  private readonly onReplayLoginErrorFn;
735
+ private readonly persistence;
736
+ private readonly onPersistErrorFn;
737
+ /** Persistence is consulted once per process; a miss must not be re-read. */
738
+ private persistenceRead;
739
+ /**
740
+ * Serializes persistence writes. `seed()` and `invalidate()` are synchronous
741
+ * by contract and so fire-and-forget their save/clear; with an async backend a
742
+ * slow save could otherwise land AFTER the clear that followed it and leave an
743
+ * invalidated session on disk.
744
+ */
745
+ private persistChain;
385
746
  constructor(opts: CookieSessionManagerOptions<S, R>);
386
747
  /** The current session, or `undefined` before the first successful login. */
387
748
  get current(): S | undefined;
@@ -425,6 +786,24 @@ export declare class CookieSessionManager<S = CookieSession, R = Response> {
425
786
  * (new config). Used to recover from a detected session expiry.
426
787
  */
427
788
  invalidate(): void;
789
+ /**
790
+ * The persisted session, if there is one worth using. Read at most once per
791
+ * process — after that the in-memory session (or its absence) is the truth,
792
+ * so an invalidate() cannot be undone by a stale file.
793
+ */
794
+ private restoreFromPersistence;
795
+ /**
796
+ * Append a persistence op to the chain, preserving call order.
797
+ *
798
+ * The RETURNED promise can reject — that is the whole `StatePersistenceError`
799
+ * path, and the awaited login write depends on it. Only the retained chain is
800
+ * swallowed, so one failed write cannot poison every later one.
801
+ */
802
+ private enqueuePersist;
803
+ /** Write the session. Silent unless `onPersistError` throws. */
804
+ private persist;
805
+ /** Discard the persisted session. Never throws. */
806
+ private clearPersisted;
428
807
  /**
429
808
  * Run an authenticated `call` with the current session and reactive
430
809
  * expiry-replay. `call` receives the session and returns a `Response`. If
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/session/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/session/index.ts"],"names":[],"mappings":"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"}