@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 +110 -1
- package/dist/session/index.d.ts +394 -15
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js +689 -42
- package/dist/session/index.js.map +1 -1
- package/package.json +1 -1
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
|
package/dist/session/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Session scaffolding for the MCP fleet —
|
|
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
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
202
|
-
|
|
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
|
|
222
|
-
*
|
|
223
|
-
* in-flight promise is cleared on settle so a later
|
|
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
|
|
227
|
-
private
|
|
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":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/session/index.ts"],"names":[],"mappings":"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"}
|