@chrischall/mcp-utils 0.6.0 → 0.8.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 +148 -9
- package/dist/auth/index.d.ts +15 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +48 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/config/index.d.ts +66 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +97 -0
- package/dist/config/index.js.map +1 -1
- package/dist/errors/index.d.ts +12 -0
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +45 -3
- package/dist/errors/index.js.map +1 -1
- package/dist/fetchproxy/index.d.ts +149 -2
- package/dist/fetchproxy/index.d.ts.map +1 -1
- package/dist/fetchproxy/index.js +179 -2
- package/dist/fetchproxy/index.js.map +1 -1
- package/dist/http/index.d.ts +179 -5
- package/dist/http/index.d.ts.map +1 -1
- package/dist/http/index.js +280 -21
- package/dist/http/index.js.map +1 -1
- package/dist/http/throttle.d.ts +28 -0
- package/dist/http/throttle.d.ts.map +1 -0
- package/dist/http/throttle.js +39 -0
- package/dist/http/throttle.js.map +1 -0
- package/dist/response/index.d.ts +5 -1
- package/dist/response/index.d.ts.map +1 -1
- package/dist/response/index.js +7 -2
- package/dist/response/index.js.map +1 -1
- package/dist/session/index.d.ts +139 -1
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js +215 -9
- package/dist/session/index.js.map +1 -1
- package/dist/zod/index.d.ts +19 -0
- package/dist/zod/index.d.ts.map +1 -1
- package/dist/zod/index.js +27 -0
- package/dist/zod/index.js.map +1 -1
- package/package.json +1 -1
package/dist/http/index.d.ts
CHANGED
|
@@ -16,12 +16,20 @@
|
|
|
16
16
|
* - {@link createApiClient} never embeds the token in a thrown message; a 401
|
|
17
17
|
* yields a fixed "unauthorized" string, not the credential.
|
|
18
18
|
*/
|
|
19
|
-
|
|
19
|
+
export * from './throttle.js';
|
|
20
|
+
/** Retry policy for transient responses (HTTP 429 by default). */
|
|
20
21
|
export interface RetryPolicy {
|
|
21
22
|
/** Number of retries after the initial attempt. The fleet default is 1. */
|
|
22
23
|
count: number;
|
|
23
24
|
/** Delay before each retry, in milliseconds. The fleet default is 2000. */
|
|
24
25
|
delayMs: number;
|
|
26
|
+
/**
|
|
27
|
+
* Statuses that trigger a retry. Defaults to `[429]` — the historical
|
|
28
|
+
* fleet-wide behavior. Add transient 5xx codes (e.g. `[429, 500, 502, 503,
|
|
29
|
+
* 504]`) for upstreams that flake; an exhausted non-429 retried status
|
|
30
|
+
* surfaces through the normal non-2xx path (an {@link ApiError}).
|
|
31
|
+
*/
|
|
32
|
+
statuses?: number[];
|
|
25
33
|
}
|
|
26
34
|
/** Options for {@link createApiClient}. */
|
|
27
35
|
/**
|
|
@@ -46,6 +54,16 @@ export interface ApiClientOptions {
|
|
|
46
54
|
* Optional when {@link ApiClientOptions.tokenManager} is provided.
|
|
47
55
|
*/
|
|
48
56
|
getToken?: () => string | undefined | Promise<string | undefined>;
|
|
57
|
+
/**
|
|
58
|
+
* Send the token in this named request header instead of
|
|
59
|
+
* `Authorization: Bearer <token>`. The raw token is sent verbatim — no
|
|
60
|
+
* `Bearer ` prefix (e.g. `tokenHeader: 'x-goog-api-key'` /
|
|
61
|
+
* `'x-auth-token'` / `'x-api-key'`). Unset keeps the default
|
|
62
|
+
* `Authorization: Bearer` behavior. Applies to both
|
|
63
|
+
* {@link ApiClientOptions.getToken} and
|
|
64
|
+
* {@link ApiClientOptions.tokenManager} tokens.
|
|
65
|
+
*/
|
|
66
|
+
tokenHeader?: string;
|
|
49
67
|
/**
|
|
50
68
|
* A reactive token source (e.g. `TokenManager`). When set, every request is
|
|
51
69
|
* routed through {@link ReactiveTokenSource.withAuth} — proactive refresh +
|
|
@@ -130,6 +148,35 @@ export declare class RequestTimeoutError extends Error {
|
|
|
130
148
|
readonly timeoutMs: number;
|
|
131
149
|
constructor(service: string, timeoutMs: number);
|
|
132
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Thrown by {@link ApiClient.fetchJson} / {@link ApiClient.fetchHtml} for a
|
|
153
|
+
* non-2xx response that isn't a 401/429 (those map to
|
|
154
|
+
* {@link UnauthorizedError} / {@link RateLimitedError}). The message is exactly
|
|
155
|
+
* the redacted, truncated {@link formatApiError} string the client has always
|
|
156
|
+
* thrown — this class only adds the `status` so callers can branch
|
|
157
|
+
* (`err instanceof ApiError && err.status === 404`) instead of regexing the
|
|
158
|
+
* message.
|
|
159
|
+
*/
|
|
160
|
+
export declare class ApiError extends Error {
|
|
161
|
+
readonly status: number;
|
|
162
|
+
constructor(status: number, message: string);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* A status-carrying HTTP error consumers can `throw new` directly — the
|
|
166
|
+
* directly-constructible parallel to {@link ApiError} (which is only thrown
|
|
167
|
+
* *inside* {@link createApiClient}). Extends {@link ApiError} so the same
|
|
168
|
+
* `err instanceof ApiError && err.status === 404` branch works for both, while
|
|
169
|
+
* its own name/`instanceof UpstreamHttpError` distinguishes the manual throws.
|
|
170
|
+
*
|
|
171
|
+
* Consolidates opentable's local `HttpError` and musescore's
|
|
172
|
+
* `MusescoreHttpError` — structural twins (status-carrying, thrown from a
|
|
173
|
+
* transport/bridge code path that doesn't go through `createApiClient`, used to
|
|
174
|
+
* branch on a 404 fallback). Repos that DO route through `createApiClient`
|
|
175
|
+
* already get {@link ApiError} for free and don't need this.
|
|
176
|
+
*/
|
|
177
|
+
export declare class UpstreamHttpError extends ApiError {
|
|
178
|
+
constructor(status: number, message: string);
|
|
179
|
+
}
|
|
133
180
|
/**
|
|
134
181
|
* Build a bearer-auth fetch client with one-shot 429 retry, 401 mapping, 204 /
|
|
135
182
|
* empty-body handling, and redacted error formatting.
|
|
@@ -198,15 +245,19 @@ export interface ParsedLinkHeader {
|
|
|
198
245
|
* Consolidates canvas's pagination Link parser.
|
|
199
246
|
*/
|
|
200
247
|
export declare function parseLinkHeader(header: string | null | undefined): ParsedLinkHeader;
|
|
201
|
-
/**
|
|
202
|
-
|
|
248
|
+
/**
|
|
249
|
+
* A parsed Set-Cookie jar: deduplicated name→value plus a ready `Cookie`
|
|
250
|
+
* header. (Renamed from `CookieJar` to free that name for the stateful
|
|
251
|
+
* {@link CookieJar} class; no fleet consumer imported the type.)
|
|
252
|
+
*/
|
|
253
|
+
export interface ParsedCookieJar {
|
|
203
254
|
/** Surviving cookies, name → value (last value wins, deletions removed). */
|
|
204
255
|
cookies: Record<string, string>;
|
|
205
256
|
/** Pre-joined `name=value; name2=value2` string for the `Cookie` request header. */
|
|
206
257
|
cookieHeader: string;
|
|
207
258
|
}
|
|
208
259
|
/**
|
|
209
|
-
* Parse `Set-Cookie` headers into a deduplicated {@link
|
|
260
|
+
* Parse `Set-Cookie` headers into a deduplicated {@link ParsedCookieJar}.
|
|
210
261
|
*
|
|
211
262
|
* Login responses commonly send deletion markers (`Max-Age=0` or an epoch
|
|
212
263
|
* `Expires`) alongside real cookies; forwarding both the delete and set form of
|
|
@@ -221,7 +272,62 @@ export interface CookieJar {
|
|
|
221
272
|
*
|
|
222
273
|
* Consolidates the IC/signupgenius cookie-jar logic.
|
|
223
274
|
*/
|
|
224
|
-
export declare function parseCookieJar(setCookieHeaders: string[] | string | null | undefined):
|
|
275
|
+
export declare function parseCookieJar(setCookieHeaders: string[] | string | null | undefined): ParsedCookieJar;
|
|
276
|
+
/**
|
|
277
|
+
* Parse a *request* `Cookie:` header (`name=value; name2=value2`) into a
|
|
278
|
+
* name→value map. This is the inbound counterpart to {@link parseCookieJar},
|
|
279
|
+
* which parses *response* `Set-Cookie` headers (with their attributes and
|
|
280
|
+
* deletion semantics) — a `Cookie` header has no attributes, just pairs.
|
|
281
|
+
*
|
|
282
|
+
* Semantics (matching creditkarma's hand-rolled `extractCookieValue` ×3):
|
|
283
|
+
* - splits on `;`, then on the FIRST `=` only, so a value containing `=`
|
|
284
|
+
* (e.g. base64 padding `ab==`, or `x=y=z`) is preserved verbatim,
|
|
285
|
+
* - trims surrounding whitespace from both name and value,
|
|
286
|
+
* - skips fragments with no `=` (bare attributes) or an empty name (`=v`),
|
|
287
|
+
* - keeps an empty value when the name is present (`a=` → `{ a: '' }`),
|
|
288
|
+
* - on a duplicate name, the last value wins.
|
|
289
|
+
*
|
|
290
|
+
* A missing / empty / whitespace-only header yields `{}`.
|
|
291
|
+
*
|
|
292
|
+
* Consolidates creditkarma's `extractCookieValue` (single-name lookup ×3) and
|
|
293
|
+
* the cookie-header pattern-matching in two other repos: `extractCookieValue(h,
|
|
294
|
+
* name)` is `parseCookieHeader(h)[name] ?? null`.
|
|
295
|
+
*/
|
|
296
|
+
export declare function parseCookieHeader(header: string): Record<string, string>;
|
|
297
|
+
/**
|
|
298
|
+
* Anything {@link CookieJar.absorb} can read `Set-Cookie`s from: the array from
|
|
299
|
+
* `Headers.getSetCookie()`, a single (possibly comma-joined) header string, or
|
|
300
|
+
* a `Headers`-like object (prefers `getSetCookie()`, falls back to splitting
|
|
301
|
+
* `get('set-cookie')` safely around `Expires` commas).
|
|
302
|
+
*/
|
|
303
|
+
export type SetCookieSource = string[] | string | null | undefined | {
|
|
304
|
+
getSetCookie?: () => string[];
|
|
305
|
+
get(name: string): string | null;
|
|
306
|
+
};
|
|
307
|
+
/**
|
|
308
|
+
* Stateful cookie jar for multi-step session logins (login page → CSRF prime →
|
|
309
|
+
* credential POST → API calls), built on the {@link parseCookieJar} semantics.
|
|
310
|
+
*
|
|
311
|
+
* Consolidates the five drifted hand-rolled jars across
|
|
312
|
+
* artsonia/canvas-parent/evite/signupgenius/skylight:
|
|
313
|
+
* - `absorb` merges each response's `Set-Cookie`s into the jar — later values
|
|
314
|
+
* override earlier ones by name, and deletion markers (`Max-Age <= 0` or a
|
|
315
|
+
* pre-2000 `Expires`, both comma- and dash-format epochs) **remove** the
|
|
316
|
+
* name from the jar,
|
|
317
|
+
* - empty-value non-deletion cookies are ignored (an existing value survives),
|
|
318
|
+
* - `header()` renders the `Cookie` request-header value in insertion order.
|
|
319
|
+
*/
|
|
320
|
+
export declare class CookieJar {
|
|
321
|
+
private readonly jar;
|
|
322
|
+
/** Merge `Set-Cookie`s into the jar (later wins; deletion markers remove). */
|
|
323
|
+
absorb(setCookies: SetCookieSource): void;
|
|
324
|
+
/** The current value of a cookie, or `undefined` when absent/deleted. */
|
|
325
|
+
get(name: string): string | undefined;
|
|
326
|
+
/** Render the `Cookie` request-header value: `name=value; name2=value2`. */
|
|
327
|
+
header(): string;
|
|
328
|
+
/** Number of cookies currently in the jar. */
|
|
329
|
+
get size(): number;
|
|
330
|
+
}
|
|
225
331
|
/**
|
|
226
332
|
* Decode a JWT's `exp` claim (seconds since epoch). Throws when the structure is
|
|
227
333
|
* invalid or `exp` is missing/non-numeric — the strict variant zola uses for the
|
|
@@ -235,6 +341,17 @@ export declare function decodeJwtExp(token: string): number;
|
|
|
235
341
|
* header.
|
|
236
342
|
*/
|
|
237
343
|
export declare function decodeJwtSessionId(token: string): string | null;
|
|
344
|
+
/**
|
|
345
|
+
* Generic single-claim extractor for a JWT payload. Returns the raw claim
|
|
346
|
+
* value (`unknown` — cast or narrow at the call site), or `undefined` for an
|
|
347
|
+
* undecodable token or an absent claim. Never throws.
|
|
348
|
+
*
|
|
349
|
+
* Generalizes the hand-rolled per-claim readers across the fleet (e.g.
|
|
350
|
+
* creditkarma's `extractGlidFromJwt`): `decodeJwtClaim(token, 'glid')` instead
|
|
351
|
+
* of a bespoke decode + payload-field pluck. Reuses the same base64url payload
|
|
352
|
+
* decode as {@link decodeJwtExp} / {@link decodeJwtSessionId}.
|
|
353
|
+
*/
|
|
354
|
+
export declare function decodeJwtClaim(token: string, claim: string): unknown;
|
|
238
355
|
/** Result of {@link validateJwtExpiry}. */
|
|
239
356
|
export interface JwtExpiryStatus {
|
|
240
357
|
/** True when the token is past its `exp` (or undecodable / lacks `exp`). */
|
|
@@ -254,4 +371,61 @@ export interface JwtExpiryStatus {
|
|
|
254
371
|
* reporting `expired: false`, so callers can refresh proactively.
|
|
255
372
|
*/
|
|
256
373
|
export declare function validateJwtExpiry(token: string, nowMs?: number): JwtExpiryStatus;
|
|
374
|
+
/** Options for {@link runBoundedBatch}. */
|
|
375
|
+
export interface RunBoundedBatchOptions<T, R> {
|
|
376
|
+
/**
|
|
377
|
+
* Overall hard deadline for the WHOLE batch, in milliseconds. When it fires,
|
|
378
|
+
* any item that hasn't settled is filled by {@link onTimeout} and the batch
|
|
379
|
+
* returns immediately — unsettled workers are abandoned (left to settle in
|
|
380
|
+
* the background, never awaited), so a permanently-hung item can't wedge the
|
|
381
|
+
* call past this bound.
|
|
382
|
+
*/
|
|
383
|
+
deadlineMs: number;
|
|
384
|
+
/**
|
|
385
|
+
* Backfill for an item the deadline cut off before it settled. Receives the
|
|
386
|
+
* original `item` and its `index` so the placeholder stays identifiable and
|
|
387
|
+
* re-runnable (the zillow `pending`-row pattern). Called only for unsettled
|
|
388
|
+
* slots.
|
|
389
|
+
*/
|
|
390
|
+
onTimeout: (item: T, index: number) => R;
|
|
391
|
+
/**
|
|
392
|
+
* Backfill for an item whose `worker` REJECTED (threw). A worker error is
|
|
393
|
+
* isolated to its own slot — it never rejects the batch or cascades onto
|
|
394
|
+
* other items. Defaults to `onTimeout(item, index)` (a rejected worker is
|
|
395
|
+
* treated like a timeout) so a caller that doesn't care needn't distinguish;
|
|
396
|
+
* supply `onError` to tell a genuine failure apart from a deadline cutoff.
|
|
397
|
+
*/
|
|
398
|
+
onError?: (item: T, index: number, err: unknown) => R;
|
|
399
|
+
/**
|
|
400
|
+
* Max workers in flight at once. Defaults to unbounded (all items started
|
|
401
|
+
* immediately). Use it to bound fan-out against a rate-limited upstream.
|
|
402
|
+
*/
|
|
403
|
+
concurrency?: number;
|
|
404
|
+
/**
|
|
405
|
+
* Injectable timer (for tests). Defaults to `setTimeout`. Must invoke `cb`
|
|
406
|
+
* after roughly `ms`, returning a handle passed to {@link clearTimer}.
|
|
407
|
+
*/
|
|
408
|
+
setTimer?: (ms: number, cb: () => void) => unknown;
|
|
409
|
+
/** Injectable clear paired with {@link setTimer}. Defaults to `clearTimeout`. */
|
|
410
|
+
clearTimer?: (handle: unknown) => void;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Run `worker` over `items` with an overall hard deadline and a per-item
|
|
414
|
+
* timeout-backfill, returning a full-length, input-ordered array with exactly
|
|
415
|
+
* one result per item.
|
|
416
|
+
*
|
|
417
|
+
* Each item gets an index-addressable slot. Work is raced against `deadlineMs`;
|
|
418
|
+
* when the deadline fires first, every still-unsettled slot is filled by
|
|
419
|
+
* `onTimeout(item, index)` and the batch resolves immediately — the in-flight
|
|
420
|
+
* workers are abandoned (and signalled via their `AbortSignal`) rather than
|
|
421
|
+
* awaited, so a single hung row can't keep the whole call (and the MCP request
|
|
422
|
+
* deadline behind it) pinned open. When everything settles before the deadline,
|
|
423
|
+
* the timer is cleared and every slot holds its real result.
|
|
424
|
+
*
|
|
425
|
+
* Generalises zillow's bulk-tool `runWithDeadline` (`zillow_bulk_get` /
|
|
426
|
+
* `zillow_resolve_addresses`, issues #98/#78): the slot-array + overall-deadline
|
|
427
|
+
* + `pending`-backfill shape, now with the worker + concurrency folded in and an
|
|
428
|
+
* injectable timer for deterministic tests.
|
|
429
|
+
*/
|
|
430
|
+
export declare function runBoundedBatch<T, R>(items: T[], worker: (item: T, signal?: AbortSignal) => Promise<R>, opts: RunBoundedBatchOptions<T, R>): Promise<R[]>;
|
|
257
431
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/http/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"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"}
|
package/dist/http/index.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* yields a fixed "unauthorized" string, not the credential.
|
|
18
18
|
*/
|
|
19
19
|
import { truncateErrorMessage } from '../errors/index.js';
|
|
20
|
+
export * from './throttle.js';
|
|
20
21
|
const DEFAULT_RETRY = { count: 1, delayMs: 2000 };
|
|
21
22
|
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
22
23
|
/** Thrown for an upstream 401. Carries the status so callers can trigger a re-auth. */
|
|
@@ -47,6 +48,44 @@ export class RequestTimeoutError extends Error {
|
|
|
47
48
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
48
49
|
}
|
|
49
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Thrown by {@link ApiClient.fetchJson} / {@link ApiClient.fetchHtml} for a
|
|
53
|
+
* non-2xx response that isn't a 401/429 (those map to
|
|
54
|
+
* {@link UnauthorizedError} / {@link RateLimitedError}). The message is exactly
|
|
55
|
+
* the redacted, truncated {@link formatApiError} string the client has always
|
|
56
|
+
* thrown — this class only adds the `status` so callers can branch
|
|
57
|
+
* (`err instanceof ApiError && err.status === 404`) instead of regexing the
|
|
58
|
+
* message.
|
|
59
|
+
*/
|
|
60
|
+
export class ApiError extends Error {
|
|
61
|
+
status;
|
|
62
|
+
constructor(status, message) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.name = 'ApiError';
|
|
65
|
+
this.status = status;
|
|
66
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A status-carrying HTTP error consumers can `throw new` directly — the
|
|
71
|
+
* directly-constructible parallel to {@link ApiError} (which is only thrown
|
|
72
|
+
* *inside* {@link createApiClient}). Extends {@link ApiError} so the same
|
|
73
|
+
* `err instanceof ApiError && err.status === 404` branch works for both, while
|
|
74
|
+
* its own name/`instanceof UpstreamHttpError` distinguishes the manual throws.
|
|
75
|
+
*
|
|
76
|
+
* Consolidates opentable's local `HttpError` and musescore's
|
|
77
|
+
* `MusescoreHttpError` — structural twins (status-carrying, thrown from a
|
|
78
|
+
* transport/bridge code path that doesn't go through `createApiClient`, used to
|
|
79
|
+
* branch on a 404 fallback). Repos that DO route through `createApiClient`
|
|
80
|
+
* already get {@link ApiError} for free and don't need this.
|
|
81
|
+
*/
|
|
82
|
+
export class UpstreamHttpError extends ApiError {
|
|
83
|
+
constructor(status, message) {
|
|
84
|
+
super(status, message);
|
|
85
|
+
this.name = 'UpstreamHttpError';
|
|
86
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
50
89
|
function hostOf(baseUrl) {
|
|
51
90
|
try {
|
|
52
91
|
return new URL(baseUrl).host;
|
|
@@ -74,7 +113,11 @@ export function createApiClient(opts) {
|
|
|
74
113
|
const sleep = opts.sleep ?? defaultSleep;
|
|
75
114
|
const unauthorized = () => (opts.onUnauthorized ? opts.onUnauthorized() : new UnauthorizedError(service));
|
|
76
115
|
const rateLimited = () => (opts.onRateLimited ? opts.onRateLimited() : new RateLimitedError(service));
|
|
116
|
+
const retryStatuses = retry.statuses ?? [429];
|
|
77
117
|
const timeoutMs = opts.timeout;
|
|
118
|
+
// Default: `Authorization: Bearer <token>`. With `tokenHeader`, the raw
|
|
119
|
+
// token goes in that named header instead (no `Bearer ` prefix).
|
|
120
|
+
const authHeader = (token) => !token ? {} : opts.tokenHeader ? { [opts.tokenHeader]: token } : { Authorization: `Bearer ${token}` };
|
|
78
121
|
// Bound `run` with an AbortController when a timeout is configured, mapping the
|
|
79
122
|
// abort to a RequestTimeoutError. No timeout → run as-is (no signal).
|
|
80
123
|
function withTimeout(run) {
|
|
@@ -106,7 +149,7 @@ export function createApiClient(opts) {
|
|
|
106
149
|
Accept: 'application/json',
|
|
107
150
|
...(hasJsonBody ? { 'Content-Type': 'application/json' } : {}),
|
|
108
151
|
...opts.baseHeaders,
|
|
109
|
-
...(token
|
|
152
|
+
...authHeader(token || undefined),
|
|
110
153
|
...opt.headers,
|
|
111
154
|
},
|
|
112
155
|
...(signal ? { signal } : {}),
|
|
@@ -122,7 +165,7 @@ export function createApiClient(opts) {
|
|
|
122
165
|
// attempt 0 is the initial request; up to `retry.count` further attempts on 429.
|
|
123
166
|
for (;;) {
|
|
124
167
|
const res = await once();
|
|
125
|
-
if (res.status
|
|
168
|
+
if (retryStatuses.includes(res.status) && attempt < retry.count) {
|
|
126
169
|
attempt += 1;
|
|
127
170
|
await sleep(retry.delayMs);
|
|
128
171
|
continue;
|
|
@@ -140,7 +183,7 @@ export function createApiClient(opts) {
|
|
|
140
183
|
return undefined;
|
|
141
184
|
const text = await res.text();
|
|
142
185
|
if (!res.ok) {
|
|
143
|
-
throw new
|
|
186
|
+
throw new ApiError(res.status, formatApiError(res.status, method, path, text, { service }));
|
|
144
187
|
}
|
|
145
188
|
if (text.length === 0)
|
|
146
189
|
return undefined;
|
|
@@ -155,7 +198,7 @@ export function createApiClient(opts) {
|
|
|
155
198
|
throw rateLimited();
|
|
156
199
|
const text = await res.text();
|
|
157
200
|
if (!res.ok) {
|
|
158
|
-
throw new
|
|
201
|
+
throw new ApiError(res.status, formatApiError(res.status, method, path, text, { service }));
|
|
159
202
|
}
|
|
160
203
|
return text;
|
|
161
204
|
}
|
|
@@ -247,10 +290,41 @@ export function parseLinkHeader(header) {
|
|
|
247
290
|
}
|
|
248
291
|
return out;
|
|
249
292
|
}
|
|
250
|
-
const
|
|
251
|
-
const
|
|
293
|
+
const MAX_AGE_RE = /(?:^|;)\s*Max-Age\s*=\s*(-?\d+)\s*(?:;|$)/i;
|
|
294
|
+
const EXPIRES_RE = /(?:^|;)\s*Expires\s*=\s*([^;]+)/i;
|
|
295
|
+
/** `Expires` dates before this year are treated as deletion markers. Fixed (not
|
|
296
|
+
* `Date.now()`-relative) so parsing stays deterministic; real session cookies
|
|
297
|
+
* are never legitimately set to expire in the previous millennium. */
|
|
298
|
+
const EXPIRES_DELETION_YEAR = 2000;
|
|
299
|
+
/**
|
|
300
|
+
* True when a `Set-Cookie` entry is a deletion marker: `Max-Age <= 0`
|
|
301
|
+
* (including the common `Max-Age=-1`), or — when no `Max-Age` is present
|
|
302
|
+
* (RFC 6265 §5.3: `Max-Age` outranks `Expires`) — an `Expires` date that
|
|
303
|
+
* parses to before {@link EXPIRES_DELETION_YEAR}. Both RFC 1123
|
|
304
|
+
* (`Thu, 01 Jan 1970`) and the legacy dash format (`Thu, 01-Jan-1970`) parse.
|
|
305
|
+
*/
|
|
306
|
+
function isDeletionCookie(entry) {
|
|
307
|
+
const maxAge = MAX_AGE_RE.exec(entry);
|
|
308
|
+
if (maxAge)
|
|
309
|
+
return Number(maxAge[1]) <= 0;
|
|
310
|
+
const expires = EXPIRES_RE.exec(entry);
|
|
311
|
+
if (expires && expires[1]) {
|
|
312
|
+
const when = new Date(expires[1].trim());
|
|
313
|
+
if (!Number.isNaN(when.getTime()))
|
|
314
|
+
return when.getUTCFullYear() < EXPIRES_DELETION_YEAR;
|
|
315
|
+
}
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
/** Parse the leading `name=value` pair of a `Set-Cookie` entry (attrs ignored). */
|
|
319
|
+
function parseNameValue(entry) {
|
|
320
|
+
const nameValue = (entry.split(';')[0] ?? '').trim();
|
|
321
|
+
const eqIdx = nameValue.indexOf('=');
|
|
322
|
+
if (eqIdx < 1)
|
|
323
|
+
return null;
|
|
324
|
+
return { name: nameValue.slice(0, eqIdx).trim(), value: nameValue.slice(eqIdx + 1).trim() };
|
|
325
|
+
}
|
|
252
326
|
/**
|
|
253
|
-
* Parse `Set-Cookie` headers into a deduplicated {@link
|
|
327
|
+
* Parse `Set-Cookie` headers into a deduplicated {@link ParsedCookieJar}.
|
|
254
328
|
*
|
|
255
329
|
* Login responses commonly send deletion markers (`Max-Age=0` or an epoch
|
|
256
330
|
* `Expires`) alongside real cookies; forwarding both the delete and set form of
|
|
@@ -266,24 +340,15 @@ const EXPIRES_EPOCH_RE = /(?:^|;)\s*Expires\s*=\s*Thu,\s*01\s*Jan\s*1970/i;
|
|
|
266
340
|
* Consolidates the IC/signupgenius cookie-jar logic.
|
|
267
341
|
*/
|
|
268
342
|
export function parseCookieJar(setCookieHeaders) {
|
|
269
|
-
const entries = setCookieHeaders
|
|
270
|
-
? []
|
|
271
|
-
: Array.isArray(setCookieHeaders)
|
|
272
|
-
? setCookieHeaders
|
|
273
|
-
: splitSetCookie(setCookieHeaders);
|
|
343
|
+
const entries = setCookieEntries(setCookieHeaders);
|
|
274
344
|
const jar = new Map();
|
|
275
345
|
for (const entry of entries) {
|
|
276
|
-
if (
|
|
346
|
+
if (isDeletionCookie(entry))
|
|
277
347
|
continue;
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
if (eqIdx < 1)
|
|
348
|
+
const pair = parseNameValue(entry);
|
|
349
|
+
if (!pair || !pair.value)
|
|
281
350
|
continue;
|
|
282
|
-
|
|
283
|
-
const value = nameValue.slice(eqIdx + 1).trim();
|
|
284
|
-
if (!value)
|
|
285
|
-
continue;
|
|
286
|
-
jar.set(name, value);
|
|
351
|
+
jar.set(pair.name, pair.value);
|
|
287
352
|
}
|
|
288
353
|
const cookies = {};
|
|
289
354
|
for (const [k, v] of jar)
|
|
@@ -291,6 +356,100 @@ export function parseCookieJar(setCookieHeaders) {
|
|
|
291
356
|
const cookieHeader = [...jar.entries()].map(([k, v]) => `${k}=${v}`).join('; ');
|
|
292
357
|
return { cookies, cookieHeader };
|
|
293
358
|
}
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
// parseCookieHeader
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
/**
|
|
363
|
+
* Parse a *request* `Cookie:` header (`name=value; name2=value2`) into a
|
|
364
|
+
* name→value map. This is the inbound counterpart to {@link parseCookieJar},
|
|
365
|
+
* which parses *response* `Set-Cookie` headers (with their attributes and
|
|
366
|
+
* deletion semantics) — a `Cookie` header has no attributes, just pairs.
|
|
367
|
+
*
|
|
368
|
+
* Semantics (matching creditkarma's hand-rolled `extractCookieValue` ×3):
|
|
369
|
+
* - splits on `;`, then on the FIRST `=` only, so a value containing `=`
|
|
370
|
+
* (e.g. base64 padding `ab==`, or `x=y=z`) is preserved verbatim,
|
|
371
|
+
* - trims surrounding whitespace from both name and value,
|
|
372
|
+
* - skips fragments with no `=` (bare attributes) or an empty name (`=v`),
|
|
373
|
+
* - keeps an empty value when the name is present (`a=` → `{ a: '' }`),
|
|
374
|
+
* - on a duplicate name, the last value wins.
|
|
375
|
+
*
|
|
376
|
+
* A missing / empty / whitespace-only header yields `{}`.
|
|
377
|
+
*
|
|
378
|
+
* Consolidates creditkarma's `extractCookieValue` (single-name lookup ×3) and
|
|
379
|
+
* the cookie-header pattern-matching in two other repos: `extractCookieValue(h,
|
|
380
|
+
* name)` is `parseCookieHeader(h)[name] ?? null`.
|
|
381
|
+
*/
|
|
382
|
+
export function parseCookieHeader(header) {
|
|
383
|
+
const out = {};
|
|
384
|
+
if (!header)
|
|
385
|
+
return out;
|
|
386
|
+
for (const part of header.split(';')) {
|
|
387
|
+
const eqIdx = part.indexOf('=');
|
|
388
|
+
if (eqIdx < 0)
|
|
389
|
+
continue; // bare attribute fragment, no `=`
|
|
390
|
+
const name = part.slice(0, eqIdx).trim();
|
|
391
|
+
if (name === '')
|
|
392
|
+
continue; // missing name (leading `=`)
|
|
393
|
+
out[name] = part.slice(eqIdx + 1).trim();
|
|
394
|
+
}
|
|
395
|
+
return out;
|
|
396
|
+
}
|
|
397
|
+
/** Normalize a {@link SetCookieSource} to individual `Set-Cookie` strings. */
|
|
398
|
+
function setCookieEntries(source) {
|
|
399
|
+
if (source == null)
|
|
400
|
+
return [];
|
|
401
|
+
if (Array.isArray(source))
|
|
402
|
+
return source;
|
|
403
|
+
if (typeof source === 'string')
|
|
404
|
+
return splitSetCookie(source);
|
|
405
|
+
if (typeof source.getSetCookie === 'function')
|
|
406
|
+
return source.getSetCookie();
|
|
407
|
+
const joined = source.get('set-cookie');
|
|
408
|
+
return joined ? splitSetCookie(joined) : [];
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Stateful cookie jar for multi-step session logins (login page → CSRF prime →
|
|
412
|
+
* credential POST → API calls), built on the {@link parseCookieJar} semantics.
|
|
413
|
+
*
|
|
414
|
+
* Consolidates the five drifted hand-rolled jars across
|
|
415
|
+
* artsonia/canvas-parent/evite/signupgenius/skylight:
|
|
416
|
+
* - `absorb` merges each response's `Set-Cookie`s into the jar — later values
|
|
417
|
+
* override earlier ones by name, and deletion markers (`Max-Age <= 0` or a
|
|
418
|
+
* pre-2000 `Expires`, both comma- and dash-format epochs) **remove** the
|
|
419
|
+
* name from the jar,
|
|
420
|
+
* - empty-value non-deletion cookies are ignored (an existing value survives),
|
|
421
|
+
* - `header()` renders the `Cookie` request-header value in insertion order.
|
|
422
|
+
*/
|
|
423
|
+
export class CookieJar {
|
|
424
|
+
jar = new Map();
|
|
425
|
+
/** Merge `Set-Cookie`s into the jar (later wins; deletion markers remove). */
|
|
426
|
+
absorb(setCookies) {
|
|
427
|
+
for (const entry of setCookieEntries(setCookies)) {
|
|
428
|
+
const pair = parseNameValue(entry);
|
|
429
|
+
if (!pair)
|
|
430
|
+
continue;
|
|
431
|
+
if (isDeletionCookie(entry)) {
|
|
432
|
+
this.jar.delete(pair.name);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (!pair.value)
|
|
436
|
+
continue;
|
|
437
|
+
this.jar.set(pair.name, pair.value);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/** The current value of a cookie, or `undefined` when absent/deleted. */
|
|
441
|
+
get(name) {
|
|
442
|
+
return this.jar.get(name);
|
|
443
|
+
}
|
|
444
|
+
/** Render the `Cookie` request-header value: `name=value; name2=value2`. */
|
|
445
|
+
header() {
|
|
446
|
+
return [...this.jar.entries()].map(([k, v]) => `${k}=${v}`).join('; ');
|
|
447
|
+
}
|
|
448
|
+
/** Number of cookies currently in the jar. */
|
|
449
|
+
get size() {
|
|
450
|
+
return this.jar.size;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
294
453
|
/** Best-effort split of a single joined `set-cookie` string (no `getSetCookie()`). */
|
|
295
454
|
function splitSetCookie(header) {
|
|
296
455
|
// Split on commas that are NOT part of an Expires date ("Thu, 01 Jan ...").
|
|
@@ -349,6 +508,22 @@ export function decodeJwtSessionId(token) {
|
|
|
349
508
|
const sid = payload['session_id'] ?? payload['sid'];
|
|
350
509
|
return typeof sid === 'string' ? sid : null;
|
|
351
510
|
}
|
|
511
|
+
/**
|
|
512
|
+
* Generic single-claim extractor for a JWT payload. Returns the raw claim
|
|
513
|
+
* value (`unknown` — cast or narrow at the call site), or `undefined` for an
|
|
514
|
+
* undecodable token or an absent claim. Never throws.
|
|
515
|
+
*
|
|
516
|
+
* Generalizes the hand-rolled per-claim readers across the fleet (e.g.
|
|
517
|
+
* creditkarma's `extractGlidFromJwt`): `decodeJwtClaim(token, 'glid')` instead
|
|
518
|
+
* of a bespoke decode + payload-field pluck. Reuses the same base64url payload
|
|
519
|
+
* decode as {@link decodeJwtExp} / {@link decodeJwtSessionId}.
|
|
520
|
+
*/
|
|
521
|
+
export function decodeJwtClaim(token, claim) {
|
|
522
|
+
const payload = decodeJwtPayload(token);
|
|
523
|
+
if (!payload)
|
|
524
|
+
return undefined;
|
|
525
|
+
return payload[claim];
|
|
526
|
+
}
|
|
352
527
|
/** Tokens within this many seconds of `exp` are flagged as near-expiry. */
|
|
353
528
|
const NEAR_EXPIRY_SKEW_SEC = 300;
|
|
354
529
|
/**
|
|
@@ -380,4 +555,88 @@ export function validateJwtExpiry(token, nowMs = Date.now()) {
|
|
|
380
555
|
}
|
|
381
556
|
return { expired: false, expiresIn };
|
|
382
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* Run `worker` over `items` with an overall hard deadline and a per-item
|
|
560
|
+
* timeout-backfill, returning a full-length, input-ordered array with exactly
|
|
561
|
+
* one result per item.
|
|
562
|
+
*
|
|
563
|
+
* Each item gets an index-addressable slot. Work is raced against `deadlineMs`;
|
|
564
|
+
* when the deadline fires first, every still-unsettled slot is filled by
|
|
565
|
+
* `onTimeout(item, index)` and the batch resolves immediately — the in-flight
|
|
566
|
+
* workers are abandoned (and signalled via their `AbortSignal`) rather than
|
|
567
|
+
* awaited, so a single hung row can't keep the whole call (and the MCP request
|
|
568
|
+
* deadline behind it) pinned open. When everything settles before the deadline,
|
|
569
|
+
* the timer is cleared and every slot holds its real result.
|
|
570
|
+
*
|
|
571
|
+
* Generalises zillow's bulk-tool `runWithDeadline` (`zillow_bulk_get` /
|
|
572
|
+
* `zillow_resolve_addresses`, issues #98/#78): the slot-array + overall-deadline
|
|
573
|
+
* + `pending`-backfill shape, now with the worker + concurrency folded in and an
|
|
574
|
+
* injectable timer for deterministic tests.
|
|
575
|
+
*/
|
|
576
|
+
export function runBoundedBatch(items, worker, opts) {
|
|
577
|
+
const slots = Array.from({ length: items.length });
|
|
578
|
+
const settled = new Array(items.length).fill(false);
|
|
579
|
+
// An item whose worker threw: backfilled via onError (default onTimeout) at
|
|
580
|
+
// finish, distinct from a slot the deadline simply never reached.
|
|
581
|
+
const errored = new Array(items.length).fill(false);
|
|
582
|
+
const errorVals = new Array(items.length);
|
|
583
|
+
// Nothing to do — never arm a timer for an empty batch.
|
|
584
|
+
if (items.length === 0)
|
|
585
|
+
return Promise.resolve([]);
|
|
586
|
+
const setTimer = opts.setTimer ?? ((ms, cb) => setTimeout(cb, ms));
|
|
587
|
+
const clearTimer = opts.clearTimer ?? ((h) => clearTimeout(h));
|
|
588
|
+
const controller = new AbortController();
|
|
589
|
+
// Bounded fan-out: a pool of `limit` runners pulling the next index off a
|
|
590
|
+
// shared cursor. limit >= items.length (or unset) means "all at once".
|
|
591
|
+
const limit = opts.concurrency && opts.concurrency > 0
|
|
592
|
+
? Math.min(opts.concurrency, items.length)
|
|
593
|
+
: items.length;
|
|
594
|
+
const runAll = async () => {
|
|
595
|
+
let cursor = 0;
|
|
596
|
+
const runner = async () => {
|
|
597
|
+
for (;;) {
|
|
598
|
+
const index = cursor;
|
|
599
|
+
cursor += 1;
|
|
600
|
+
if (index >= items.length)
|
|
601
|
+
return;
|
|
602
|
+
// Isolate a worker rejection to its own slot: record it and continue
|
|
603
|
+
// pulling the next item, so one bad row neither rejects the batch nor
|
|
604
|
+
// strands the items this runner hasn't reached yet.
|
|
605
|
+
try {
|
|
606
|
+
slots[index] = await worker(items[index], controller.signal);
|
|
607
|
+
settled[index] = true;
|
|
608
|
+
}
|
|
609
|
+
catch (err) {
|
|
610
|
+
errored[index] = true;
|
|
611
|
+
errorVals[index] = err;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
await Promise.all(Array.from({ length: limit }, () => runner()));
|
|
616
|
+
};
|
|
617
|
+
return new Promise((resolve) => {
|
|
618
|
+
let done = false;
|
|
619
|
+
const finish = () => {
|
|
620
|
+
if (done)
|
|
621
|
+
return;
|
|
622
|
+
done = true;
|
|
623
|
+
clearTimer(handle);
|
|
624
|
+
// Signal abandoned workers so a cooperative worker can stop early.
|
|
625
|
+
if (!controller.signal.aborted)
|
|
626
|
+
controller.abort();
|
|
627
|
+
const onError = opts.onError ?? ((item, index) => opts.onTimeout(item, index));
|
|
628
|
+
resolve(items.map((item, index) => {
|
|
629
|
+
if (settled[index])
|
|
630
|
+
return slots[index];
|
|
631
|
+
if (errored[index])
|
|
632
|
+
return onError(item, index, errorVals[index]);
|
|
633
|
+
return opts.onTimeout(item, index);
|
|
634
|
+
}));
|
|
635
|
+
};
|
|
636
|
+
const handle = setTimer(opts.deadlineMs, finish);
|
|
637
|
+
// Abandon (never await) the fan-out on the deadline path; on full settle,
|
|
638
|
+
// finish() clears the timer and returns the real results.
|
|
639
|
+
void runAll().then(finish, finish);
|
|
640
|
+
});
|
|
641
|
+
}
|
|
383
642
|
//# sourceMappingURL=index.js.map
|