@typeship-ax/cli 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/api.json +5597 -3010
- package/api.md +433 -54
- package/dist/cli-agent.d.ts +9 -1
- package/dist/cli-agent.d.ts.map +1 -1
- package/dist/cli-agent.js +26 -9
- package/dist/cli.js +83 -232
- package/dist/core/http.d.ts +6 -92
- package/dist/core/http.d.ts.map +1 -1
- package/dist/core/http.js +70 -209
- package/dist/core/pagination.d.ts.map +1 -1
- package/dist/core/pagination.js +6 -34
- package/dist/dates.d.ts +0 -2
- package/dist/dates.d.ts.map +1 -1
- package/dist/dates.js +0 -1
- package/dist/docs.d.ts +11 -0
- package/dist/docs.d.ts.map +1 -0
- package/dist/docs.js +114 -0
- package/dist/errors.d.ts +27 -27
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +7 -7
- package/dist/index.d.ts +19 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -13
- package/dist/ops.d.ts +5 -0
- package/dist/ops.d.ts.map +1 -1
- package/dist/ops.js +31 -17
- package/dist/resources/account.d.ts +2 -2
- package/dist/resources/account.d.ts.map +1 -1
- package/dist/resources/api-keys.d.ts +10 -5
- package/dist/resources/api-keys.d.ts.map +1 -1
- package/dist/resources/api-keys.js +3 -1
- package/dist/resources/definition-revisions.d.ts +58 -0
- package/dist/resources/definition-revisions.d.ts.map +1 -0
- package/dist/resources/definition-revisions.js +110 -0
- package/dist/resources/definitions.d.ts +24 -0
- package/dist/resources/definitions.d.ts.map +1 -0
- package/dist/resources/definitions.js +51 -0
- package/dist/resources/generate.d.ts +5 -5
- package/dist/resources/generate.d.ts.map +1 -1
- package/dist/resources/generate.js +3 -3
- package/dist/resources/generations.d.ts +3 -3
- package/dist/resources/generations.d.ts.map +1 -1
- package/dist/resources/generations.js +1 -1
- package/dist/resources/projects.d.ts +66 -26
- package/dist/resources/projects.d.ts.map +1 -1
- package/dist/resources/projects.js +87 -13
- package/dist/resources/targets.d.ts +86 -0
- package/dist/resources/targets.d.ts.map +1 -0
- package/dist/resources/targets.js +184 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +119 -62
- package/dist/types.d.ts +1761 -222
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -3
- package/package.json +1 -1
- package/src/cli-agent.ts +28 -11
- package/src/cli.ts +89 -233
- package/src/core/http.ts +75 -293
- package/src/core/pagination.ts +6 -30
- package/src/dates.ts +0 -1
- package/src/docs.ts +101 -0
- package/src/errors.ts +30 -30
- package/src/index.ts +23 -13
- package/src/ops.ts +43 -17
- package/src/resources/account.ts +3 -3
- package/src/resources/api-keys.ts +22 -7
- package/src/resources/definition-revisions.ts +198 -0
- package/src/resources/definitions.ts +97 -0
- package/src/resources/generate.ts +6 -6
- package/src/resources/generations.ts +4 -4
- package/src/resources/projects.ts +182 -37
- package/src/resources/targets.ts +346 -0
- package/src/schemas.ts +119 -62
- package/src/types.ts +1947 -281
- package/dist/resources/spec-revisions.d.ts +0 -47
- package/dist/resources/spec-revisions.d.ts.map +0 -1
- package/dist/resources/spec-revisions.js +0 -90
- package/src/resources/spec-revisions.ts +0 -150
package/src/core/http.ts
CHANGED
|
@@ -17,7 +17,12 @@ export interface RequestOptions {
|
|
|
17
17
|
export interface ResponseMeta {
|
|
18
18
|
status: number;
|
|
19
19
|
headers: Headers;
|
|
20
|
-
/**
|
|
20
|
+
/** Parsed wire body before it is narrowed to the generated response type.
|
|
21
|
+
* Use this escape hatch for additive fields or variants introduced after
|
|
22
|
+
* this generator edition. */
|
|
23
|
+
rawBody?: unknown;
|
|
24
|
+
/** Request identifier from the JSON response body, or from headers for
|
|
25
|
+
* raw and bodyless responses. */
|
|
21
26
|
requestId?: string;
|
|
22
27
|
}
|
|
23
28
|
|
|
@@ -37,15 +42,6 @@ export interface RequestContext {
|
|
|
37
42
|
attempt: number;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
/** One server-sent event from a text/event-stream response. */
|
|
41
|
-
export interface SseEvent {
|
|
42
|
-
/** The `event:` field; undefined for unnamed events. */
|
|
43
|
-
event?: string;
|
|
44
|
-
/** The `id:` field, when the server sends one. */
|
|
45
|
-
id?: string;
|
|
46
|
-
/** Concatenated `data:` lines. Parse as JSON if your API sends JSON. */
|
|
47
|
-
data: string;
|
|
48
|
-
}
|
|
49
45
|
|
|
50
46
|
/**
|
|
51
47
|
* Every SDK call returns a discriminated result instead of throwing.
|
|
@@ -85,97 +81,6 @@ export class UnexpectedApiError extends ApiError<number, unknown> {
|
|
|
85
81
|
}
|
|
86
82
|
}
|
|
87
83
|
|
|
88
|
-
/** A 200 response whose GraphQL payload carried errors. */
|
|
89
|
-
export class GraphQLRequestError extends Error {
|
|
90
|
-
/** The raw errors array from the GraphQL response. */
|
|
91
|
-
readonly errors: { message?: string; path?: unknown[]; extensions?: unknown }[];
|
|
92
|
-
readonly response: ResponseMeta;
|
|
93
|
-
|
|
94
|
-
constructor(errors: unknown[], response: ResponseMeta) {
|
|
95
|
-
const first = (errors[0] as { message?: string } | undefined)?.message;
|
|
96
|
-
super(first ?? "GraphQL request returned errors");
|
|
97
|
-
this.name = "GraphQLRequestError";
|
|
98
|
-
this.errors = errors as GraphQLRequestError["errors"];
|
|
99
|
-
this.response = response;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
|
-
// GraphQL selections — typed field picking over the schema's result types
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
|
-
|
|
107
|
-
type Primitive = string | number | boolean | bigint | symbol | null | undefined;
|
|
108
|
-
type Unwrap<T> = NonNullable<T> extends readonly (infer U)[] ? Unwrap<U> : NonNullable<T>;
|
|
109
|
-
type IsUnion<T, U = T> = T extends unknown ? ([U] extends [T] ? false : true) : never;
|
|
110
|
-
type TypeNameOf<T> = T extends { __typename?: infer N } ? Extract<N, string> : never;
|
|
111
|
-
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* A selection over a GraphQL result type `T`: `true` picks a leaf field, a
|
|
115
|
-
* nested object picks inside an object field, and `on` picks per concrete
|
|
116
|
-
* type when `T` is a union or interface (`{ on: { Transaction: { amount: true } } }`).
|
|
117
|
-
* Keys are checked against the schema, so a typo is a compile error.
|
|
118
|
-
*/
|
|
119
|
-
export type Selection<T> = Unwrap<T> extends Primitive ? never : SelectionObject<Unwrap<T>>;
|
|
120
|
-
|
|
121
|
-
type FieldSelection<V> = Unwrap<V> extends Primitive ? true : SelectionObject<Unwrap<V>>;
|
|
122
|
-
|
|
123
|
-
type SelectionObject<T> = {
|
|
124
|
-
[K in Extract<keyof T, string> as K extends "__typename" ? never : K]?: FieldSelection<T[K]>;
|
|
125
|
-
} & { __typename?: true } & (IsUnion<T> extends true
|
|
126
|
-
? { on?: { [N in TypeNameOf<T>]?: SelectionObject<Extract<T, { __typename?: N }>> } }
|
|
127
|
-
: { on?: never });
|
|
128
|
-
|
|
129
|
-
/** The result type a {@link Selection} `S` produces from result type `T`:
|
|
130
|
-
* exactly the fields picked, nullability and lists preserved, union members
|
|
131
|
-
* narrowed by `__typename`. */
|
|
132
|
-
export type Selected<T, S> = T extends readonly (infer U)[]
|
|
133
|
-
? Selected<U, S>[]
|
|
134
|
-
: T extends Primitive
|
|
135
|
-
? T
|
|
136
|
-
: S extends object
|
|
137
|
-
? Prettify<SelectedMember<T, S>>
|
|
138
|
-
: never;
|
|
139
|
-
|
|
140
|
-
type SelectedMember<T, S> = (S extends { on?: infer O }
|
|
141
|
-
? O extends object
|
|
142
|
-
? TypeNameOf<T> extends keyof O
|
|
143
|
-
? PickSelected<T, NonNullable<O[TypeNameOf<T>]>>
|
|
144
|
-
: {}
|
|
145
|
-
: {}
|
|
146
|
-
: {}) &
|
|
147
|
-
PickSelected<T, S>;
|
|
148
|
-
|
|
149
|
-
type PickSelected<T, S> = {
|
|
150
|
-
-readonly [K in keyof S as K extends "on" ? never : K extends keyof T ? (S[K] extends true | object ? K : never) : never]-?: K extends "__typename"
|
|
151
|
-
? NonNullable<T[K & keyof T]>
|
|
152
|
-
: S[K] extends true
|
|
153
|
-
? Exclude<T[K & keyof T], undefined>
|
|
154
|
-
: Selected<Exclude<T[K & keyof T], undefined>, S[K]>;
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
/** A selection object or a raw selection set (`"{ id name }"`). */
|
|
158
|
-
export type SelectionInput = string | Record<string, unknown>;
|
|
159
|
-
|
|
160
|
-
/** Serialize a selection object to a GraphQL selection set. Strings pass
|
|
161
|
-
* through untouched. */
|
|
162
|
-
export function selectionToString(selection: SelectionInput): string {
|
|
163
|
-
if (typeof selection === "string") return selection;
|
|
164
|
-
const parts: string[] = [];
|
|
165
|
-
for (const [key, value] of Object.entries(selection)) {
|
|
166
|
-
if (!value) continue;
|
|
167
|
-
if (key === "on" && typeof value === "object") {
|
|
168
|
-
for (const [typeName, sub] of Object.entries(value as Record<string, unknown>)) {
|
|
169
|
-
if (sub && typeof sub === "object") parts.push("... on " + typeName + " " + selectionToString(sub as Record<string, unknown>));
|
|
170
|
-
}
|
|
171
|
-
} else if (value === true) {
|
|
172
|
-
parts.push(key);
|
|
173
|
-
} else if (typeof value === "object") {
|
|
174
|
-
parts.push(key + " " + selectionToString(value as Record<string, unknown>));
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return "{ " + (parts.length > 0 ? parts.join(" ") : "__typename") + " }";
|
|
178
|
-
}
|
|
179
84
|
|
|
180
85
|
// ---------------------------------------------------------------------------
|
|
181
86
|
// Optional runtime validation — zero-dependency, schema table in schemas.ts
|
|
@@ -341,14 +246,9 @@ export interface CoreRequest {
|
|
|
341
246
|
errors?: Record<string, ErrorCtor>;
|
|
342
247
|
/** Idempotent requests are retried automatically. */
|
|
343
248
|
idempotent?: boolean;
|
|
344
|
-
/** Success body is text/event-stream: yield SseEvents instead of parsing. */
|
|
345
|
-
stream?: boolean;
|
|
346
249
|
/** Header name auto-filled with one UUID per call (stable across retries)
|
|
347
250
|
* when the caller doesn't supply a value. */
|
|
348
251
|
idempotencyKey?: string;
|
|
349
|
-
/** GraphQL: unwrap body.data[field] and turn body.errors into a
|
|
350
|
-
* GraphQLRequestError. */
|
|
351
|
-
graphqlField?: string;
|
|
352
252
|
/** Key into the schemas table for optional runtime validation. */
|
|
353
253
|
schemaKey?: string;
|
|
354
254
|
/** Operation-level retry policy (x-typeship-retries), merged over the
|
|
@@ -509,16 +409,20 @@ export class HttpCore {
|
|
|
509
409
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
510
410
|
let response: Response;
|
|
511
411
|
const attemptStarted = Date.now();
|
|
412
|
+
const emitResponseDebug = (value: Response, body?: unknown) => this.config.debug?.({
|
|
413
|
+
method: req.method,
|
|
414
|
+
path: req.path,
|
|
415
|
+
status: value.status,
|
|
416
|
+
durationMs: Date.now() - attemptStarted,
|
|
417
|
+
attempt: attempt + 1,
|
|
418
|
+
requestId:
|
|
419
|
+
requestIdFromBody(body) ??
|
|
420
|
+
value.headers.get("request-id") ??
|
|
421
|
+
value.headers.get("x-request-id") ??
|
|
422
|
+
undefined,
|
|
423
|
+
});
|
|
512
424
|
try {
|
|
513
425
|
response = await this.send(req, timeoutMs, attempt, autoIdempotencyKey);
|
|
514
|
-
this.config.debug?.({
|
|
515
|
-
method: req.method,
|
|
516
|
-
path: req.path,
|
|
517
|
-
status: response.status,
|
|
518
|
-
durationMs: Date.now() - attemptStarted,
|
|
519
|
-
attempt: attempt + 1,
|
|
520
|
-
requestId: response.headers.get("x-request-id") ?? response.headers.get("request-id") ?? undefined,
|
|
521
|
-
});
|
|
522
426
|
} catch (cause) {
|
|
523
427
|
lastError = cause;
|
|
524
428
|
this.config.debug?.({
|
|
@@ -541,13 +445,11 @@ export class HttpCore {
|
|
|
541
445
|
}
|
|
542
446
|
|
|
543
447
|
if (response.ok) {
|
|
544
|
-
if (req.stream) {
|
|
545
|
-
return { ok: true, data: sseEvents(response) as T, response: meta(response) };
|
|
546
|
-
}
|
|
547
448
|
let data: T;
|
|
548
449
|
try {
|
|
549
450
|
data = (await parseBody(response, req.method)) as T;
|
|
550
451
|
} catch (cause) {
|
|
452
|
+
emitResponseDebug(response);
|
|
551
453
|
const error = new TransportError(
|
|
552
454
|
"The response body read was aborted before completing",
|
|
553
455
|
cause,
|
|
@@ -555,9 +457,13 @@ export class HttpCore {
|
|
|
555
457
|
await this.config.onError?.(error, { method: req.method, path: req.path });
|
|
556
458
|
return { ok: false, error, response: meta(response) };
|
|
557
459
|
}
|
|
558
|
-
|
|
460
|
+
emitResponseDebug(response, data);
|
|
461
|
+
const responseMeta = meta(response, data);
|
|
462
|
+
let responseData: unknown = data;
|
|
463
|
+
let shouldValidateResponse = responseData !== undefined;
|
|
464
|
+
if (opSchemas?.res && this.config.validate!.responses && shouldValidateResponse) {
|
|
559
465
|
const violations: Violation[] = [];
|
|
560
|
-
validateAgainstSchema(
|
|
466
|
+
validateAgainstSchema(responseData, opSchemas.res, "response", violations, this.config.schemaDefs);
|
|
561
467
|
if (violations.length > 0) {
|
|
562
468
|
const validationError = new ValidationError("response", violations);
|
|
563
469
|
if (this.config.validate!.mode === "warn") {
|
|
@@ -565,21 +471,11 @@ export class HttpCore {
|
|
|
565
471
|
} else {
|
|
566
472
|
const error = validationError as unknown as E;
|
|
567
473
|
await this.config.onError?.(error, { method: req.method, path: req.path });
|
|
568
|
-
return { ok: false, error, response:
|
|
474
|
+
return { ok: false, error, response: responseMeta };
|
|
569
475
|
}
|
|
570
476
|
}
|
|
571
477
|
}
|
|
572
|
-
|
|
573
|
-
const payload = data as { data?: Record<string, unknown>; errors?: unknown[] } | undefined;
|
|
574
|
-
const responseMeta = meta(response);
|
|
575
|
-
if (Array.isArray(payload?.errors) && payload.errors.length > 0) {
|
|
576
|
-
const gqlError = new GraphQLRequestError(payload.errors, responseMeta) as unknown as E;
|
|
577
|
-
await this.config.onError?.(gqlError, { method: req.method, path: req.path });
|
|
578
|
-
return { ok: false, error: gqlError, response: responseMeta };
|
|
579
|
-
}
|
|
580
|
-
return { ok: true, data: payload?.data?.[req.graphqlField] as T, response: responseMeta };
|
|
581
|
-
}
|
|
582
|
-
return { ok: true, data, response: meta(response) };
|
|
478
|
+
return { ok: true, data, response: responseMeta };
|
|
583
479
|
}
|
|
584
480
|
|
|
585
481
|
// 429 is safe to retry regardless of idempotency; other retryable
|
|
@@ -588,7 +484,15 @@ export class HttpCore {
|
|
|
588
484
|
retryableStatuses.has(response.status) &&
|
|
589
485
|
(retryAllowed || response.status === 429);
|
|
590
486
|
if (attempt < maxRetries && retryableStatus) {
|
|
591
|
-
|
|
487
|
+
const delay = retryAfterMs(response) ?? backoff(attempt, policy);
|
|
488
|
+
let retryBody: unknown;
|
|
489
|
+
try {
|
|
490
|
+
retryBody = await parseBody(response, req.method);
|
|
491
|
+
} catch {
|
|
492
|
+
retryBody = undefined;
|
|
493
|
+
}
|
|
494
|
+
emitResponseDebug(response, retryBody);
|
|
495
|
+
await sleep(delay);
|
|
592
496
|
continue;
|
|
593
497
|
}
|
|
594
498
|
|
|
@@ -598,7 +502,8 @@ export class HttpCore {
|
|
|
598
502
|
} catch {
|
|
599
503
|
body = undefined; // error responses keep their status even if the body read aborts
|
|
600
504
|
}
|
|
601
|
-
|
|
505
|
+
emitResponseDebug(response, body);
|
|
506
|
+
const responseMeta = meta(response, body);
|
|
602
507
|
const Ctor =
|
|
603
508
|
req.errors?.[String(response.status)] ??
|
|
604
509
|
req.errors?.[String(Math.floor(response.status / 100)) + "XX"] ??
|
|
@@ -645,28 +550,14 @@ export class HttpCore {
|
|
|
645
550
|
};
|
|
646
551
|
await this.config.onRequest?.(context);
|
|
647
552
|
|
|
648
|
-
// Streaming responses are exempt from the attempt timeout once headers
|
|
649
|
-
// arrive (an event stream may stay open far longer than timeoutMs);
|
|
650
|
-
// everything else keeps the timeout armed through the body read, so a
|
|
651
|
-
// stalled body aborts instead of hanging.
|
|
652
553
|
const signals: AbortSignal[] = [];
|
|
653
|
-
|
|
654
|
-
if (req.stream) {
|
|
655
|
-
const headersTimeout = new AbortController();
|
|
656
|
-
const timer = setTimeout(
|
|
657
|
-
() => headersTimeout.abort(new DOMException("Timed out waiting for response headers", "TimeoutError")),
|
|
658
|
-
timeoutMs,
|
|
659
|
-
);
|
|
660
|
-
(timer as { unref?: () => void }).unref?.();
|
|
661
|
-
clearStreamTimeout = () => clearTimeout(timer);
|
|
662
|
-
signals.push(headersTimeout.signal);
|
|
663
|
-
} else {
|
|
554
|
+
{
|
|
664
555
|
signals.push(AbortSignal.timeout(timeoutMs));
|
|
665
556
|
}
|
|
666
557
|
if (req.options?.signal) signals.push(req.options.signal);
|
|
667
558
|
|
|
668
559
|
try {
|
|
669
|
-
const signal =
|
|
560
|
+
const signal = composeSignals(signals);
|
|
670
561
|
const response = this.manualRedirects
|
|
671
562
|
? await this.followRedirects(context, body, signal)
|
|
672
563
|
: await this.config.fetch(context.url, {
|
|
@@ -678,7 +569,6 @@ export class HttpCore {
|
|
|
678
569
|
await this.config.onResponse?.(response, context);
|
|
679
570
|
return response;
|
|
680
571
|
} finally {
|
|
681
|
-
clearStreamTimeout?.();
|
|
682
572
|
}
|
|
683
573
|
}
|
|
684
574
|
|
|
@@ -765,55 +655,6 @@ async function resolveAuthValue(value: AuthValue): Promise<string> {
|
|
|
765
655
|
return typeof value === "function" ? await value() : value;
|
|
766
656
|
}
|
|
767
657
|
|
|
768
|
-
/** Parse a text/event-stream body into SseEvents, lazily. */
|
|
769
|
-
async function* sseEvents(response: Response): AsyncGenerator<SseEvent, void, undefined> {
|
|
770
|
-
if (!response.body) return;
|
|
771
|
-
const reader = response.body.getReader();
|
|
772
|
-
const decoder = new TextDecoder();
|
|
773
|
-
let buffer = "";
|
|
774
|
-
let dataLines: string[] = [];
|
|
775
|
-
let eventName: string | undefined;
|
|
776
|
-
let eventId: string | undefined;
|
|
777
|
-
|
|
778
|
-
const flush = (): SseEvent | undefined => {
|
|
779
|
-
if (dataLines.length === 0) return undefined;
|
|
780
|
-
const event: SseEvent = { data: dataLines.join("\n") };
|
|
781
|
-
if (eventName !== undefined) event.event = eventName;
|
|
782
|
-
if (eventId !== undefined) event.id = eventId;
|
|
783
|
-
dataLines = [];
|
|
784
|
-
eventName = undefined;
|
|
785
|
-
return event;
|
|
786
|
-
};
|
|
787
|
-
|
|
788
|
-
try {
|
|
789
|
-
while (true) {
|
|
790
|
-
const { done, value } = await reader.read();
|
|
791
|
-
if (done) break;
|
|
792
|
-
buffer += decoder.decode(value, { stream: true });
|
|
793
|
-
let newline: number;
|
|
794
|
-
while ((newline = buffer.indexOf("\n")) !== -1) {
|
|
795
|
-
const hasCr = newline > 0 && buffer[newline - 1] === "\r";
|
|
796
|
-
const line = buffer.slice(0, hasCr ? newline - 1 : newline);
|
|
797
|
-
buffer = buffer.slice(newline + 1);
|
|
798
|
-
if (line === "") {
|
|
799
|
-
const event = flush();
|
|
800
|
-
if (event) yield event;
|
|
801
|
-
} else if (line.startsWith("data:")) {
|
|
802
|
-
dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
803
|
-
} else if (line.startsWith("event:")) {
|
|
804
|
-
eventName = line.slice(6).replace(/^ /, "");
|
|
805
|
-
} else if (line.startsWith("id:")) {
|
|
806
|
-
eventId = line.slice(3).replace(/^ /, "");
|
|
807
|
-
}
|
|
808
|
-
// comments (":") and "retry:" are intentionally ignored
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
const last = flush();
|
|
812
|
-
if (last) yield last;
|
|
813
|
-
} finally {
|
|
814
|
-
reader.releaseLock();
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
658
|
|
|
818
659
|
/** Default rendering for debug events (the boolean debug:true sink). */
|
|
819
660
|
export function formatDebugEvent(name: string, event: DebugEvent): string {
|
|
@@ -833,74 +674,6 @@ export function bearerAuth(token: AuthValue): AuthValue {
|
|
|
833
674
|
return "Bearer " + token;
|
|
834
675
|
}
|
|
835
676
|
|
|
836
|
-
export interface ClientCredentialsConfig {
|
|
837
|
-
clientId: string;
|
|
838
|
-
clientSecret: string;
|
|
839
|
-
tokenUrl: string;
|
|
840
|
-
scopes?: string[];
|
|
841
|
-
/** Extra token-request parameters, e.g. the "audience" some
|
|
842
|
-
* authorization servers require for API-valid access tokens. */
|
|
843
|
-
tokenParams?: Record<string, string>;
|
|
844
|
-
/** How credentials reach the token endpoint. Default "post"
|
|
845
|
-
* (client_secret_post, form fields); "basic" sends an Authorization
|
|
846
|
-
* header (client_secret_basic). */
|
|
847
|
-
authMethod?: "post" | "basic";
|
|
848
|
-
fetchImpl?: typeof fetch;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
/**
|
|
852
|
-
* OAuth2 client-credentials token source: fetches from the token URL,
|
|
853
|
-
* caches until expiry (60s early refresh), and shares one in-flight
|
|
854
|
-
* request across concurrent callers. Returned function plugs in as an
|
|
855
|
-
* Authorization AuthValue, resolved before every attempt.
|
|
856
|
-
*/
|
|
857
|
-
export function oauthClientCredentials(config: ClientCredentialsConfig): () => Promise<string> {
|
|
858
|
-
let token: string | undefined;
|
|
859
|
-
let expiresAt = 0;
|
|
860
|
-
let inflight: Promise<string> | undefined;
|
|
861
|
-
const fetchImpl = config.fetchImpl ?? fetch;
|
|
862
|
-
|
|
863
|
-
async function fetchToken(): Promise<string> {
|
|
864
|
-
const params = new URLSearchParams({ grant_type: "client_credentials" });
|
|
865
|
-
if (config.scopes !== undefined && config.scopes.length > 0) {
|
|
866
|
-
params.set("scope", config.scopes.join(" "));
|
|
867
|
-
}
|
|
868
|
-
for (const [key, value] of Object.entries(config.tokenParams ?? {})) {
|
|
869
|
-
params.set(key, value);
|
|
870
|
-
}
|
|
871
|
-
const headers: Record<string, string> = {
|
|
872
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
873
|
-
Accept: "application/json",
|
|
874
|
-
};
|
|
875
|
-
if (config.authMethod === "basic") {
|
|
876
|
-
headers["Authorization"] = "Basic " + toBase64(config.clientId + ":" + config.clientSecret);
|
|
877
|
-
} else {
|
|
878
|
-
params.set("client_id", config.clientId);
|
|
879
|
-
params.set("client_secret", config.clientSecret);
|
|
880
|
-
}
|
|
881
|
-
const response = await fetchImpl(config.tokenUrl, { method: "POST", headers, body: params.toString() });
|
|
882
|
-
const body = (await response.json().catch(() => null)) as
|
|
883
|
-
| { access_token?: string; expires_in?: number; error?: string }
|
|
884
|
-
| null;
|
|
885
|
-
if (!response.ok || typeof body?.access_token !== "string") {
|
|
886
|
-
throw new TransportError(
|
|
887
|
-
"OAuth token request failed (HTTP " + response.status + (body?.error ? ": " + body.error : "") + ")",
|
|
888
|
-
body,
|
|
889
|
-
);
|
|
890
|
-
}
|
|
891
|
-
token = body.access_token;
|
|
892
|
-
expiresAt = body.expires_in !== undefined
|
|
893
|
-
? Date.now() + body.expires_in * 1000 - 60_000
|
|
894
|
-
: Number.MAX_SAFE_INTEGER;
|
|
895
|
-
return token;
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
return async () => {
|
|
899
|
-
if (token !== undefined && Date.now() < expiresAt) return "Bearer " + token;
|
|
900
|
-
inflight ??= fetchToken().finally(() => { inflight = undefined; });
|
|
901
|
-
return "Bearer " + (await inflight);
|
|
902
|
-
};
|
|
903
|
-
}
|
|
904
677
|
|
|
905
678
|
/**
|
|
906
679
|
* Bracket-style deep encoding shared by query strings and form bodies:
|
|
@@ -925,31 +698,8 @@ function serializeBody(req: CoreRequest): { body: NonNullable<RequestInit["body"
|
|
|
925
698
|
switch (req.bodyKind ?? "json") {
|
|
926
699
|
case "json":
|
|
927
700
|
return { body: JSON.stringify(req.body), contentType: "application/json" };
|
|
928
|
-
case "form": {
|
|
929
|
-
const params = new URLSearchParams();
|
|
930
|
-
for (const [k, v] of Object.entries(req.body as Record<string, unknown>)) {
|
|
931
|
-
appendDeep(params, k, v);
|
|
932
|
-
}
|
|
933
|
-
// URLSearchParams sets its own content type with the charset suffix.
|
|
934
|
-
return { body: params };
|
|
935
|
-
}
|
|
936
|
-
case "multipart": {
|
|
937
|
-
const form = new FormData();
|
|
938
|
-
for (const [k, v] of Object.entries(req.body as Record<string, unknown>)) {
|
|
939
|
-
if (v === undefined || v === null) continue;
|
|
940
|
-
form.append(
|
|
941
|
-
k,
|
|
942
|
-
v instanceof Blob ? v : typeof v === "object" ? JSON.stringify(v) : String(v),
|
|
943
|
-
);
|
|
944
|
-
}
|
|
945
|
-
// Let fetch set the boundary header.
|
|
946
|
-
return { body: form };
|
|
947
|
-
}
|
|
948
|
-
case "text":
|
|
949
|
-
return { body: String(req.body), contentType: "text/plain" };
|
|
950
|
-
case "binary":
|
|
951
|
-
return { body: req.body as NonNullable<RequestInit["body"]> };
|
|
952
701
|
}
|
|
702
|
+
throw new Error("Unsupported request body kind: " + String(req.bodyKind));
|
|
953
703
|
}
|
|
954
704
|
|
|
955
705
|
async function parseBody(response: Response, method: string): Promise<unknown> {
|
|
@@ -968,17 +718,49 @@ async function parseBody(response: Response, method: string): Promise<unknown> {
|
|
|
968
718
|
}
|
|
969
719
|
}
|
|
970
720
|
|
|
971
|
-
function
|
|
721
|
+
function requestIdFromBody(body: unknown): string | undefined {
|
|
722
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return undefined;
|
|
723
|
+
const value = (body as Record<string, unknown>).request_id ?? (body as Record<string, unknown>).requestId;
|
|
724
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function meta(response: Response, body?: unknown): ResponseMeta {
|
|
972
728
|
return {
|
|
973
729
|
status: response.status,
|
|
974
730
|
headers: response.headers,
|
|
731
|
+
...(body !== undefined ? { rawBody: body } : {}),
|
|
975
732
|
requestId:
|
|
976
|
-
|
|
733
|
+
requestIdFromBody(body) ??
|
|
977
734
|
response.headers.get("request-id") ??
|
|
735
|
+
response.headers.get("x-request-id") ??
|
|
978
736
|
undefined,
|
|
979
737
|
};
|
|
980
738
|
}
|
|
981
739
|
|
|
740
|
+
/** AbortSignal.any reached Node in 18.17. Keep the package's Node 18 floor
|
|
741
|
+
* honest for earlier 18.x releases and for web runtimes without it. */
|
|
742
|
+
function composeSignals(signals: AbortSignal[]): AbortSignal {
|
|
743
|
+
const nativeAny = (AbortSignal as typeof AbortSignal & { any?: (items: AbortSignal[]) => AbortSignal }).any;
|
|
744
|
+
if (nativeAny) return nativeAny.call(AbortSignal, signals);
|
|
745
|
+
const controller = new AbortController();
|
|
746
|
+
const listeners = new Map<AbortSignal, () => void>();
|
|
747
|
+
const abortFrom = (signal: AbortSignal) => {
|
|
748
|
+
for (const [item, listener] of listeners) item.removeEventListener("abort", listener);
|
|
749
|
+
listeners.clear();
|
|
750
|
+
controller.abort(signal.reason);
|
|
751
|
+
};
|
|
752
|
+
for (const signal of signals) {
|
|
753
|
+
if (signal.aborted) {
|
|
754
|
+
abortFrom(signal);
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
const listener = () => abortFrom(signal);
|
|
758
|
+
listeners.set(signal, listener);
|
|
759
|
+
signal.addEventListener("abort", listener, { once: true });
|
|
760
|
+
}
|
|
761
|
+
return controller.signal;
|
|
762
|
+
}
|
|
763
|
+
|
|
982
764
|
function retryAfterMs(response: Response): number | undefined {
|
|
983
765
|
const header = response.headers.get("retry-after");
|
|
984
766
|
if (!header) return undefined;
|
package/src/core/pagination.ts
CHANGED
|
@@ -74,26 +74,11 @@ export class Page<Item, E = unknown> {
|
|
|
74
74
|
case "cursor": {
|
|
75
75
|
const next = getPath(this.body, config.nextCursorField!);
|
|
76
76
|
if (next === undefined || next === null || next === "") return null;
|
|
77
|
+
if (next === params[config.cursorParam!]) return null;
|
|
77
78
|
return { ...params, [config.cursorParam!]: next };
|
|
78
79
|
}
|
|
79
|
-
case "cursorFromLastId": {
|
|
80
|
-
if (items.length === 0) return null;
|
|
81
|
-
const last = items[items.length - 1] as Record<string, unknown>;
|
|
82
|
-
const id = last?.[config.idField ?? "id"];
|
|
83
|
-
if (id === undefined || id === null) return null;
|
|
84
|
-
return { ...params, [config.cursorParam!]: id };
|
|
85
|
-
}
|
|
86
|
-
case "page": {
|
|
87
|
-
if (!this.looksLikeMore(items)) return null;
|
|
88
|
-
const current = Number(params[config.pageParam!] ?? 1);
|
|
89
|
-
return { ...params, [config.pageParam!]: current + 1 };
|
|
90
|
-
}
|
|
91
|
-
case "offset": {
|
|
92
|
-
if (!this.looksLikeMore(items)) return null;
|
|
93
|
-
const current = Number(params[config.offsetParam!] ?? 0);
|
|
94
|
-
return { ...params, [config.offsetParam!]: current + items.length };
|
|
95
|
-
}
|
|
96
80
|
}
|
|
81
|
+
return null;
|
|
97
82
|
}
|
|
98
83
|
|
|
99
84
|
/** Fetch the next page, or null when this is the last one. Throws the typed error on failure. */
|
|
@@ -159,26 +144,17 @@ export function paginate<Item, E>(
|
|
|
159
144
|
req: CoreRequest,
|
|
160
145
|
config: PageConfig,
|
|
161
146
|
): PagePromise<Item, E> {
|
|
162
|
-
const isGraphql =
|
|
147
|
+
const isGraphql = false
|
|
148
|
+
;
|
|
163
149
|
const fetchPage: FetchPage<Item, E> = async (params) => {
|
|
164
|
-
|
|
165
|
-
? {
|
|
166
|
-
...req,
|
|
167
|
-
body: {
|
|
168
|
-
...(req.body as Record<string, unknown>),
|
|
169
|
-
variables: { ...((req.body as { variables?: Record<string, unknown> })?.variables ?? {}), ...params },
|
|
170
|
-
},
|
|
171
|
-
}
|
|
172
|
-
: { ...req, query: params };
|
|
150
|
+
let nextReq: CoreRequest = { ...req, query: params };
|
|
173
151
|
const result = await core.request<unknown, E>(nextReq);
|
|
174
152
|
if (!result.ok) return result;
|
|
175
153
|
const page = new Page<Item, E>(fetchPage, config, params, result.data, result.response);
|
|
176
154
|
return { ok: true, data: page, response: result.response };
|
|
177
155
|
};
|
|
178
156
|
const initial: Record<string, unknown> = {};
|
|
179
|
-
|
|
180
|
-
? ((req.body as { variables?: Record<string, unknown> })?.variables ?? {})
|
|
181
|
-
: (req.query ?? {});
|
|
157
|
+
let seed = req.query ?? {};
|
|
182
158
|
for (const [k, v] of Object.entries(seed)) {
|
|
183
159
|
if (v !== undefined) initial[k] = v;
|
|
184
160
|
}
|
package/src/dates.ts
CHANGED
|
@@ -29,7 +29,6 @@ const CALENDAR_UNITS = new Set(["mo", "month", "y", "yr", "year"]);
|
|
|
29
29
|
const HINT = "Use a signed form: -7d or -P7D for seven days ago, +7d or +P7D for seven days ahead; or an absolute ISO 8601 value.";
|
|
30
30
|
|
|
31
31
|
/** The forms a date-shaped argument accepts, for descriptions and help. */
|
|
32
|
-
export const RELATIVE_DATE_HINT = "Accepts ISO 8601 or a relative form: -P7D, -7d, 7 days ago, today, now.";
|
|
33
32
|
|
|
34
33
|
function shift(base: Date, amount: number, unit: string): Date {
|
|
35
34
|
const d = new Date(base.getTime());
|
package/src/docs.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/** Shared documentation-fetch boundary for generated CLI and MCP surfaces. */
|
|
2
|
+
|
|
3
|
+
export const MAX_DOCS_TEXT_BYTES = 2_000_000;
|
|
4
|
+
|
|
5
|
+
export function resolveDocsPageUrl(base: string | null, pathOrFile: string): string | null {
|
|
6
|
+
if (base === null) return null;
|
|
7
|
+
try {
|
|
8
|
+
const baseUrl = new URL(base);
|
|
9
|
+
if ((baseUrl.protocol !== "https:" && baseUrl.protocol !== "http:") || baseUrl.username || baseUrl.password) return null;
|
|
10
|
+
const target = /^https?:\/\//.test(pathOrFile)
|
|
11
|
+
? new URL(pathOrFile)
|
|
12
|
+
: new URL(pathOrFile.replace(/^\/+/, ""), baseUrl.toString().replace(/\/+$/, "") + "/");
|
|
13
|
+
return target.origin === baseUrl.origin && !target.username && !target.password ? target.toString() : null;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Resolve conventional docs files while honoring an explicitly configured
|
|
20
|
+
* llms.txt location. Guide links may live on either the docs-site origin or
|
|
21
|
+
* the index origin; no other origin is accepted. */
|
|
22
|
+
export function resolveDocsContentUrl(
|
|
23
|
+
base: string | null,
|
|
24
|
+
indexUrl: string | null,
|
|
25
|
+
pathOrFile: string,
|
|
26
|
+
): string | null {
|
|
27
|
+
const exactIndex = safeHttpUrl(indexUrl);
|
|
28
|
+
if (pathOrFile === "llms.txt" && exactIndex) return exactIndex;
|
|
29
|
+
if (pathOrFile === "llms-full.txt" && exactIndex) {
|
|
30
|
+
try { return new URL("llms-full.txt", exactIndex).toString(); } catch { return null; }
|
|
31
|
+
}
|
|
32
|
+
const primary = resolveDocsPageUrl(base, pathOrFile);
|
|
33
|
+
if (primary) return primary;
|
|
34
|
+
if (!exactIndex) return null;
|
|
35
|
+
try {
|
|
36
|
+
const target = /^https?:\/\//.test(pathOrFile)
|
|
37
|
+
? new URL(pathOrFile)
|
|
38
|
+
: new URL(pathOrFile.replace(/^\/+/, ""), exactIndex);
|
|
39
|
+
const allowed = new URL(exactIndex);
|
|
40
|
+
return target.origin === allowed.origin && !target.username && !target.password ? target.toString() : null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function safeHttpUrl(value: string | null): string | null {
|
|
47
|
+
if (!value) return null;
|
|
48
|
+
try {
|
|
49
|
+
const parsed = new URL(value);
|
|
50
|
+
return (parsed.protocol === "https:" || parsed.protocol === "http:") && !parsed.username && !parsed.password
|
|
51
|
+
? parsed.toString()
|
|
52
|
+
: null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Markdown-preferred fetch with same-origin redirects, one deadline, and a
|
|
59
|
+
* streaming byte cap. Returns null for every invalid or failed read. */
|
|
60
|
+
export async function fetchDocsText(url: string): Promise<string | null> {
|
|
61
|
+
try {
|
|
62
|
+
const initial = new URL(url);
|
|
63
|
+
if ((initial.protocol !== "https:" && initial.protocol !== "http:") || initial.username || initial.password) return null;
|
|
64
|
+
const allowedOrigin = initial.origin;
|
|
65
|
+
let current = initial.toString();
|
|
66
|
+
const signal = AbortSignal.timeout(10_000);
|
|
67
|
+
for (let redirects = 0; redirects <= 3; redirects += 1) {
|
|
68
|
+
const response = await fetch(current, { headers: { Accept: "text/markdown, text/plain, */*" }, redirect: "manual", signal });
|
|
69
|
+
if (response.status >= 300 && response.status < 400) {
|
|
70
|
+
const location = response.headers.get("location");
|
|
71
|
+
if (!location || redirects === 3) return null;
|
|
72
|
+
const next = new URL(location, current);
|
|
73
|
+
if (next.origin !== allowedOrigin || next.username || next.password) return null;
|
|
74
|
+
current = next.toString();
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (!response.ok) return null;
|
|
78
|
+
const declared = Number(response.headers.get("content-length"));
|
|
79
|
+
if (Number.isFinite(declared) && declared > MAX_DOCS_TEXT_BYTES) return null;
|
|
80
|
+
if (!response.body) return "";
|
|
81
|
+
const reader = response.body.getReader();
|
|
82
|
+
const decoder = new TextDecoder();
|
|
83
|
+
let bytes = 0;
|
|
84
|
+
let text = "";
|
|
85
|
+
for (;;) {
|
|
86
|
+
const chunk = await reader.read();
|
|
87
|
+
if (chunk.done) break;
|
|
88
|
+
bytes += chunk.value.byteLength;
|
|
89
|
+
if (bytes > MAX_DOCS_TEXT_BYTES) {
|
|
90
|
+
await reader.cancel();
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
94
|
+
}
|
|
95
|
+
return text + decoder.decode();
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|