@rebilly/api 0.0.0 → 0.1.1

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/dist/core.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { createCoreClient, type CoreClient, type CoreClientOptions, } from './create-core-client.ts';
2
+ export { apiServerUrl, type ClientOptions } from './api-client-options.ts';
3
+ export * from './runtime/factory.ts';
4
+ export type * from './core-client.ts';
5
+ export { OPERATIONS } from './core-client.ts';
package/dist/core.js ADDED
@@ -0,0 +1,7 @@
1
+ import { ApiError, TimeoutError } from "./runtime/errors.js";
2
+ import { defaultRetryOn } from "./runtime/retry.js";
3
+ import { createClient } from "./runtime/factory.js";
4
+ import { OPERATIONS } from "./core-client.js";
5
+ import { apiServerUrl } from "./api-client-options.js";
6
+ import { createCoreClient } from "./create-core-client.js";
7
+ export { ApiError, OPERATIONS, TimeoutError, apiServerUrl, createClient, createCoreClient, defaultRetryOn };
@@ -0,0 +1,8 @@
1
+ import { createClient, Ops, OperationId, OperationPath, OperationTag } from './core-client.ts';
2
+ import { ClientOptions } from './api-client-options.ts';
3
+ import { OperationContext } from './runtime/types.ts';
4
+ export type CoreClient = ReturnType<typeof createClient<Ops, OperationId, OperationPath, OperationTag>>;
5
+ export type CoreClientOptions = ClientOptions<OperationContext<OperationId, OperationPath, OperationTag>> & {
6
+ apiKey?: string;
7
+ };
8
+ export declare function createCoreClient({ apiKey, ...options }: CoreClientOptions): CoreClient;
@@ -0,0 +1,9 @@
1
+ import { createClient } from "./runtime/factory.js";
2
+ import { OPERATIONS } from "./core-client.js";
3
+ import { clientConfig, secretKeyAuth } from "./api-client-options.js";
4
+ //#region src/create-core-client.ts
5
+ function createCoreClient({ apiKey, ...options }) {
6
+ return createClient(OPERATIONS, clientConfig(options, secretKeyAuth(apiKey)));
7
+ }
8
+ //#endregion
9
+ export { createCoreClient };
@@ -0,0 +1,6 @@
1
+ import { createClient, Ops, OperationId, OperationPath, OperationTag } from './storefront-client.ts';
2
+ import { ClientOptions, StorefrontCredentials } from './api-client-options.ts';
3
+ import { OperationContext } from './runtime/types.ts';
4
+ export type StorefrontClient = ReturnType<typeof createClient<Ops, OperationId, OperationPath, OperationTag>>;
5
+ export type StorefrontClientOptions = StorefrontCredentials & ClientOptions<OperationContext<OperationId, OperationPath, OperationTag>>;
6
+ export declare function createStorefrontClient({ publishableKey, jwt, ...options }: StorefrontClientOptions): StorefrontClient;
@@ -0,0 +1,12 @@
1
+ import { createClient } from "./runtime/factory.js";
2
+ import { clientConfig, storefrontAuth } from "./api-client-options.js";
3
+ import { OPERATIONS } from "./storefront-client.js";
4
+ //#region src/create-storefront-client.ts
5
+ function createStorefrontClient({ publishableKey, jwt, ...options }) {
6
+ return createClient(OPERATIONS, clientConfig(options, storefrontAuth({
7
+ publishableKey,
8
+ jwt
9
+ })));
10
+ }
11
+ //#endregion
12
+ export { createStorefrontClient };
@@ -0,0 +1,14 @@
1
+ import { ClientConfig, SecuritySpec } from './types.ts';
2
+ /**
3
+ * Build the auth headers/query for one operation's `security` OR-alternatives from the
4
+ * instance credentials (`config.auth`) — capability module, wired into `createClient`.
5
+ * The first alternative whose schemes (an AND-set) are all configured is applied, so
6
+ * "bearer OR apiKey" works with either credential and never sends both. When none is
7
+ * fully configured, the first alternative's configured schemes are still sent (the
8
+ * server rejects the request, mirroring the previous behavior).
9
+ * Cookie-borne apiKeys fold into a single `Cookie` header joined with `; `.
10
+ */
11
+ export declare function resolveAuth(security: readonly (readonly SecuritySpec[])[], config: ClientConfig): Promise<{
12
+ headers: Record<string, string>;
13
+ query: Record<string, string>;
14
+ }>;
@@ -0,0 +1,53 @@
1
+ //#region src/runtime/auth.ts
2
+ /** Resolve a credential: a literal passes through; a function is awaited per request. */
3
+ async function resolveToken(provider) {
4
+ return typeof provider === "function" ? await provider() : provider;
5
+ }
6
+ /** UTF-8-safe base64: bare `btoa` throws on non-Latin-1 credentials (RFC 7617 allows UTF-8). */
7
+ function encodeBase64(text) {
8
+ let binary = "";
9
+ for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);
10
+ return btoa(binary);
11
+ }
12
+ /** Whether a credential for this scheme is configured on the instance. */
13
+ function isConfigured(scheme, config) {
14
+ if (scheme.kind === "apiKey") return config.auth?.apiKey?.[scheme.scheme] !== void 0;
15
+ if (scheme.kind === "bearer") return config.auth?.bearer !== void 0;
16
+ return config.auth?.basic !== void 0;
17
+ }
18
+ /**
19
+ * Build the auth headers/query for one operation's `security` OR-alternatives from the
20
+ * instance credentials (`config.auth`) — capability module, wired into `createClient`.
21
+ * The first alternative whose schemes (an AND-set) are all configured is applied, so
22
+ * "bearer OR apiKey" works with either credential and never sends both. When none is
23
+ * fully configured, the first alternative's configured schemes are still sent (the
24
+ * server rejects the request, mirroring the previous behavior).
25
+ * Cookie-borne apiKeys fold into a single `Cookie` header joined with `; `.
26
+ */
27
+ async function resolveAuth(security, config) {
28
+ const alternative = security.find((schemes) => schemes.every((scheme) => isConfigured(scheme, config))) ?? security[0] ?? [];
29
+ const headers = {};
30
+ const query = {};
31
+ const cookies = [];
32
+ for (const scheme of alternative) if (scheme.kind === "apiKey") {
33
+ const provider = config.auth?.apiKey?.[scheme.scheme];
34
+ if (provider === void 0) continue;
35
+ const value = await resolveToken(provider);
36
+ if (scheme.in === "header") headers[scheme.name] = value;
37
+ else if (scheme.in === "query") query[scheme.name] = value;
38
+ else cookies.push(`${scheme.name}=${encodeURIComponent(value)}`);
39
+ } else if (scheme.kind === "bearer") {
40
+ const provider = config.auth?.bearer;
41
+ if (provider !== void 0) headers.Authorization = `Bearer ${await resolveToken(provider)}`;
42
+ } else {
43
+ const basic = config.auth?.basic;
44
+ if (basic !== void 0) headers.Authorization = `Basic ${encodeBase64(`${basic.username}:${basic.password}`)}`;
45
+ }
46
+ if (cookies.length > 0) headers.Cookie = cookies.join("; ");
47
+ return {
48
+ headers,
49
+ query
50
+ };
51
+ }
52
+ //#endregion
53
+ export { resolveAuth };
@@ -0,0 +1,50 @@
1
+ import { SendCapabilities } from './send.ts';
2
+ import { Client, ClientConfig, OperationContext, OperationDescriptor, OpsShape, PaginationSpec, QueryValue, RequestOptions, SecuritySpec, ServerSentEvent, SseOptions } from './types.ts';
3
+ /**
4
+ * The optional behaviors `createClientCore` can dispatch to but never statically
5
+ * imports. The package's public `createClient` wires the full set; the future
6
+ * inline-mode assembler wires only the capabilities a spec needs.
7
+ */
8
+ export type Capabilities = SendCapabilities & {
9
+ resolveAuth?: (security: readonly (readonly SecuritySpec[])[], config: ClientConfig) => Promise<{
10
+ headers: Record<string, string>;
11
+ query: Record<string, string>;
12
+ }>;
13
+ sse?: (config: ClientConfig, op: OperationContext, prepare: () => Promise<{
14
+ url: string;
15
+ init: SseOptions;
16
+ }>, dataKind: 'json' | 'text') => AsyncGenerator<ServerSentEvent<unknown>>;
17
+ paginate?: {
18
+ pages: (call: (args?: OperationArgs, init?: RequestOptions) => Promise<unknown>, spec: PaginationSpec, args?: OperationArgs, init?: RequestOptions) => AsyncGenerator<unknown>;
19
+ items: (call: (args?: OperationArgs, init?: RequestOptions) => Promise<unknown>, spec: PaginationSpec, args?: OperationArgs, init?: RequestOptions) => AsyncGenerator<unknown>;
20
+ pagesByLink: (call: (args?: OperationArgs, init?: RequestOptions) => Promise<{
21
+ page: unknown;
22
+ linkHeader: string | null;
23
+ url: string;
24
+ }>, spec: PaginationSpec, args?: OperationArgs, init?: RequestOptions) => AsyncGenerator<unknown>;
25
+ itemsByLink: (call: (args?: OperationArgs, init?: RequestOptions) => Promise<{
26
+ page: unknown;
27
+ linkHeader: string | null;
28
+ url: string;
29
+ }>, spec: PaginationSpec, args?: OperationArgs, init?: RequestOptions) => AsyncGenerator<unknown>;
30
+ };
31
+ };
32
+ /**
33
+ * One call's inputs, namespaced by transport layer. `argsStyle: 'flat'` clients accept the
34
+ * merged form instead (every parameter and body property at one level) — `namespaceArgs`
35
+ * converts it to this shape before anything downstream reads it.
36
+ */
37
+ export type OperationArgs = {
38
+ path?: Record<string, unknown>;
39
+ query?: Record<string, QueryValue>;
40
+ body?: unknown;
41
+ headers?: Record<string, unknown>;
42
+ cookies?: Record<string, unknown>;
43
+ } & Record<string, unknown>;
44
+ /**
45
+ * Build a typed instance client over operation descriptors: one real bound method per
46
+ * operation (attached by a construction-time loop — no Proxy), plus the core members
47
+ * (`configure`/`use`/`auth`), which are assigned AFTER the loop so they win any name
48
+ * collision with an operation. All behavior dispatches through the capability seam.
49
+ */
50
+ export declare function createClientCore<Ops extends OpsShape, Id extends string = string, Path extends string = string, Tag extends string = string>(operations: Record<string, OperationDescriptor>, initial?: ClientConfig<OperationContext<Id, Path, Tag>>, caps?: Capabilities): Client<Ops, OperationContext<Id, Path, Tag>>;
@@ -0,0 +1,345 @@
1
+ import { ApiError } from "./errors.js";
2
+ import { parse, readError } from "./parse.js";
3
+ import { middlewareChain, send, toHeaderRecord } from "./send.js";
4
+ import { buildUrl, substitutePath } from "./url.js";
5
+ //#region src/runtime/create-client.ts
6
+ /** The five layer keys, and the only top-level keys a namespaced call may carry. */
7
+ var LAYERS = [
8
+ "path",
9
+ "query",
10
+ "body",
11
+ "headers",
12
+ "cookies"
13
+ ];
14
+ /** Where a declared parameter's `in` value puts it. */
15
+ var LAYER_OF = {
16
+ path: "path",
17
+ query: "query",
18
+ header: "headers",
19
+ cookie: "cookies"
20
+ };
21
+ /**
22
+ * Merged (`argsStyle: 'flat'`) args → the namespaced shape. A key that names a declared
23
+ * parameter goes to that parameter's layer; anything else is a property of the request
24
+ * body, which is how a flat call spells an object body. `body` stays reserved for the
25
+ * operations a flat call cannot merge (an array, a scalar, or a binary body).
26
+ */
27
+ function namespaceArgs(op, args) {
28
+ const layers = {};
29
+ let body;
30
+ let properties;
31
+ const layerOfParam = new Map((op.params ?? []).map((param) => [param.name, param.in]));
32
+ for (const [key, value] of Object.entries(args)) {
33
+ const layer = LAYER_OF[layerOfParam.get(key) ?? ""];
34
+ if (layer !== void 0) (layers[layer] ??= {})[key] = value;
35
+ else if (key === "body" && op.body !== void 0) body = value;
36
+ else if (op.body !== void 0) (properties ??= {})[key] = value;
37
+ else throw new TypeError(`Unknown argument "${key}" for operation "${op.id}": it names no declared parameter, and the operation takes no request body.`);
38
+ }
39
+ const namespaced = {};
40
+ if (layers.path) namespaced.path = layers.path;
41
+ if (layers.query) namespaced.query = layers.query;
42
+ if (layers.headers) namespaced.headers = layers.headers;
43
+ if (layers.cookies) namespaced.cookies = layers.cookies;
44
+ if (properties !== void 0) namespaced.body = properties;
45
+ else if (body !== void 0) namespaced.body = body;
46
+ return namespaced;
47
+ }
48
+ /** The response reader implied by the descriptor (before any per-call `parseAs` override). */
49
+ /**
50
+ * The `Accept` header matching how the response will be read — a blob/text operation
51
+ * must not ask for `application/json` (a content-negotiating server would 406 or
52
+ * answer with a JSON error body instead of the payload). Caller `init.headers` and
53
+ * `config.headers` still override.
54
+ */
55
+ function acceptFor(kind) {
56
+ if (kind === "text") return "text/*";
57
+ if (kind === "blob" || kind === "arrayBuffer" || kind === "stream" || kind === "formData") return "*/*";
58
+ return "application/json";
59
+ }
60
+ function kindFor(op) {
61
+ if (op.responseKind === "void" || op.responseKind === "blob" || op.responseKind === "text") return op.responseKind;
62
+ return "auto";
63
+ }
64
+ /**
65
+ * The call's inputs in namespaced form, converting first on a flat-style client. An
66
+ * operation the generator marked `argsStyle: 'grouped'` is already namespaced — its names
67
+ * could not be merged, so its input type never offered the flat shape.
68
+ */
69
+ function inputOf(op, args, config) {
70
+ return config.argsStyle === "flat" && op.argsStyle !== "grouped" ? namespaceArgs(op, args) : args;
71
+ }
72
+ /** Route the namespaced args to the request pieces. */
73
+ function splitArgs(op, args) {
74
+ for (const key of Object.keys(args)) if (!LAYERS.includes(key)) throw new TypeError(`Unknown argument "${key}" for operation "${op.id}". Inputs are grouped by layer: ${LAYERS.join(", ")}.`);
75
+ return {
76
+ path: args.path ?? {},
77
+ query: args.query,
78
+ body: args.body,
79
+ headers: args.headers,
80
+ cookies: args.cookies
81
+ };
82
+ }
83
+ /**
84
+ * The query-serialization hints for the descriptor's query params. A spec is built only
85
+ * when the param deviates from the OpenAPI defaults (`form` + `explode: true`, encoded),
86
+ * and always fully resolved — so `explode: false` or `allowReserved` alone (no `style`)
87
+ * are honored, and an omitted `explode` keeps the exploded default.
88
+ */
89
+ function queryStyles(op) {
90
+ let styles;
91
+ for (const param of op.params ?? []) {
92
+ if (param.in !== "query") continue;
93
+ if (!(param.style !== void 0 && param.style !== "form" || param.explode === false || param.allowReserved === true)) continue;
94
+ styles ??= {};
95
+ styles[param.name] = {
96
+ style: param.style ?? "form",
97
+ explode: param.explode ?? true,
98
+ allowReserved: param.allowReserved
99
+ };
100
+ }
101
+ return styles;
102
+ }
103
+ /** Stringify caller-supplied extra headers, skipping empty entries. */
104
+ function stringHeaders(headers) {
105
+ const out = {};
106
+ for (const [key, value] of Object.entries(headers ?? {})) if (value !== void 0 && value !== null) out[key] = String(value);
107
+ return out;
108
+ }
109
+ /** Build the request pieces an attempt needs: the final URL and the merged per-call init. */
110
+ async function prepareRequest(config, op, args, init, caps) {
111
+ const { path, query, body, headers, cookies } = splitArgs(op, args);
112
+ const authed = op.security?.length && caps.resolveAuth ? await caps.resolveAuth(op.security, config) : {
113
+ headers: {},
114
+ query: {}
115
+ };
116
+ const cookiePairs = Object.entries(cookies ?? {}).filter(([, value]) => value !== void 0 && value !== null).map(([cookieName, value]) => `${cookieName}=${encodeURIComponent(String(value))}`);
117
+ if (cookiePairs.length > 0) authed.headers.Cookie = [authed.headers.Cookie, ...cookiePairs].filter(Boolean).join("; ");
118
+ const fullQuery = {
119
+ ...query,
120
+ ...authed.query
121
+ };
122
+ return {
123
+ url: buildUrl(config.serverUrl ?? "", substitutePath(op.path, path), Object.keys(fullQuery).length > 0 ? fullQuery : void 0, queryStyles(op)),
124
+ init: {
125
+ ...init,
126
+ method: op.method.toUpperCase(),
127
+ headers: {
128
+ ...authed.headers,
129
+ ...stringHeaders(headers),
130
+ ...toHeaderRecord(init.headers)
131
+ }
132
+ },
133
+ body
134
+ };
135
+ }
136
+ /** Coerce a single declared response header value; omit when absent or unparsable. */
137
+ function coerceResponseHeader(raw, type) {
138
+ if (raw === null) return void 0;
139
+ if (type === "number") {
140
+ if (raw.trim() === "") return void 0;
141
+ const value = Number(raw);
142
+ return Number.isFinite(value) ? value : void 0;
143
+ }
144
+ if (type === "boolean") {
145
+ const value = raw.trim().toLowerCase();
146
+ if (value === "true") return true;
147
+ if (value === "false") return false;
148
+ return;
149
+ }
150
+ return raw;
151
+ }
152
+ /** Build the camelCase declared-header bag for a throw-mode envelope. */
153
+ function readEnvelopeHeaders(response, specs) {
154
+ const headers = {};
155
+ for (const spec of specs ?? []) {
156
+ const value = coerceResponseHeader(response.headers.get(spec.name), spec.type);
157
+ if (value !== void 0) headers[spec.key] = value;
158
+ }
159
+ return headers;
160
+ }
161
+ /** One non-SSE call: send, then branch on the configured error mode. */
162
+ async function execute(config, op, args, init, caps) {
163
+ const prepared = await prepareRequest(config, op, args, init, caps);
164
+ const opCtx = {
165
+ id: op.id,
166
+ path: op.path,
167
+ tags: [...op.tags ?? []]
168
+ };
169
+ const { parseAs, envelope, ...sendInit } = prepared.init;
170
+ const readKind = parseAs ?? kindFor(op);
171
+ const { response, context } = await send(config, opCtx, prepared.url, sendInit, prepared.body, op.body, caps, acceptFor(readKind));
172
+ if (config.errorMode === "result") {
173
+ if (!response.ok) return {
174
+ data: void 0,
175
+ error: await readError(response),
176
+ response
177
+ };
178
+ return {
179
+ data: await parse(response, readKind),
180
+ error: void 0,
181
+ response
182
+ };
183
+ }
184
+ if (!response.ok) {
185
+ let error = new ApiError(context.url, response.status, response.statusText, await readError(response));
186
+ for (const mw of middlewareChain(config)) if (mw.onError) error = await mw.onError(error, context);
187
+ throw error;
188
+ }
189
+ const data = await parse(response, readKind);
190
+ if (envelope === true) return {
191
+ data,
192
+ headers: readEnvelopeHeaders(response, op.responseHeaders),
193
+ response
194
+ };
195
+ return data;
196
+ }
197
+ /** The paginate capability, or a descriptive throw when a paginated op is iterated unwired. */
198
+ function paginateCapability(caps, op) {
199
+ if (!caps.paginate) throw new Error(`Pagination capability not wired: cannot iterate operation "${op.id}"`);
200
+ return caps.paginate;
201
+ }
202
+ /**
203
+ * The per-page call the iterators drive: the method itself in throw mode; in result
204
+ * mode a wrapper that unwraps the `{ data, error, response }` envelope — the page
205
+ * pointers are data-rooted — rethrowing a failed page as `ApiError` (iteration is
206
+ * error-mode-agnostic; the throw-mode-only `onError` middleware hook is not invoked).
207
+ */
208
+ function pageCall(method, config) {
209
+ const callWithoutEnvelope = (args, init) => {
210
+ if (!init || init.envelope === void 0) return method(args, init);
211
+ const { envelope: _envelope, ...pageInit } = init;
212
+ return method(args, pageInit);
213
+ };
214
+ if (config.errorMode !== "result") return callWithoutEnvelope;
215
+ return async (args, init) => {
216
+ const envelope = await callWithoutEnvelope(args, init);
217
+ if (!envelope.response.ok) {
218
+ const { response } = envelope;
219
+ throw new ApiError(response.url, response.status, response.statusText, envelope.error);
220
+ }
221
+ return envelope.data;
222
+ };
223
+ }
224
+ /**
225
+ * The per-page call the `link`-style iterators drive: like `execute`, but returning the
226
+ * parsed page together with the raw `Link` header and the page's own URL (for resolving
227
+ * a relative `rel="next"` target). Error-mode-agnostic like all iteration: a failed
228
+ * page throws `ApiError` even on result-mode clients.
229
+ */
230
+ function linkPageCall(config, op, caps) {
231
+ return async (args = {}, init = {}) => {
232
+ const prepared = await prepareRequest(config, op, args, init, caps);
233
+ const { parseAs, envelope: _envelope, ...sendInit } = prepared.init;
234
+ const readKind = parseAs ?? kindFor(op);
235
+ const opCtx = {
236
+ id: op.id,
237
+ path: op.path,
238
+ tags: [...op.tags ?? []]
239
+ };
240
+ const { response } = await send(config, opCtx, prepared.url, sendInit, prepared.body, op.body, caps, acceptFor(readKind));
241
+ if (!response.ok) throw new ApiError(prepared.url, response.status, response.statusText, await readError(response));
242
+ return {
243
+ page: await parse(response, readKind),
244
+ linkHeader: response.headers.get("link"),
245
+ url: response.url === "" ? prepared.url : response.url
246
+ };
247
+ };
248
+ }
249
+ /**
250
+ * Build a typed instance client over operation descriptors: one real bound method per
251
+ * operation (attached by a construction-time loop — no Proxy), plus the core members
252
+ * (`configure`/`use`/`auth`), which are assigned AFTER the loop so they win any name
253
+ * collision with an operation. All behavior dispatches through the capability seam.
254
+ */
255
+ function createClientCore(operations, initial = {}, caps = {}) {
256
+ const given = initial;
257
+ const config = {
258
+ ...given,
259
+ middleware: [...given.middleware ?? []]
260
+ };
261
+ const client = {};
262
+ for (const [name, op] of Object.entries(operations)) if (op.responseKind === "sse") {
263
+ const method = (given = {}, init = {}) => {
264
+ const args = inputOf(op, given, config);
265
+ if (!caps.sse) throw new Error(`SSE capability not wired: cannot stream operation "${op.id}"`);
266
+ const stream = caps.sse;
267
+ return (async function* () {
268
+ const opCtx = {
269
+ id: op.id,
270
+ path: op.path,
271
+ tags: [...op.tags ?? []]
272
+ };
273
+ const prepare = async () => {
274
+ const prepared = await prepareRequest(config, op, args, init, caps);
275
+ return {
276
+ url: prepared.url,
277
+ init: prepared.init,
278
+ body: prepared.body
279
+ };
280
+ };
281
+ yield* stream(config, opCtx, prepare, op.sseDataKind ?? "text");
282
+ })();
283
+ };
284
+ Object.defineProperty(method, "name", { value: name });
285
+ Object.defineProperty(method, "operationId", { value: op.id });
286
+ client[name] = method;
287
+ } else {
288
+ const raw = (args = {}, init = {}) => execute(config, op, args, init, caps);
289
+ const method = (args = {}, init = {}) => raw(inputOf(op, args, config), init);
290
+ Object.defineProperty(method, "name", { value: name });
291
+ Object.defineProperty(method, "operationId", { value: op.id });
292
+ const spec = op.pagination;
293
+ client[name] = spec === void 0 ? method : spec.style === "link" || spec.style === "body-link" ? Object.assign(method, {
294
+ pages: (args, init) => paginateCapability(caps, op).pagesByLink(linkPageCall(config, op, caps), spec, inputOf(op, args ?? {}, config), init),
295
+ items: (args, init) => paginateCapability(caps, op).itemsByLink(linkPageCall(config, op, caps), spec, inputOf(op, args ?? {}, config), init)
296
+ }) : Object.assign(method, {
297
+ pages: (args, init) => paginateCapability(caps, op).pages(pageCall(raw, config), spec, inputOf(op, args ?? {}, config), init),
298
+ items: (args, init) => paginateCapability(caps, op).items(pageCall(raw, config), spec, inputOf(op, args ?? {}, config), init)
299
+ });
300
+ }
301
+ client.configure = (next) => {
302
+ const { errorMode: _fixedMode, argsStyle: _fixedStyle, auth, ...rest } = next;
303
+ Object.assign(config, rest);
304
+ if (auth) config.auth = {
305
+ ...config.auth,
306
+ ...auth,
307
+ ...auth.apiKey ? { apiKey: {
308
+ ...config.auth?.apiKey,
309
+ ...auth.apiKey
310
+ } } : {}
311
+ };
312
+ };
313
+ client.use = (...middleware) => {
314
+ config.middleware = [...config.middleware ?? [], ...middleware];
315
+ };
316
+ client.auth = {
317
+ bearer(token) {
318
+ config.auth = {
319
+ ...config.auth,
320
+ bearer: token
321
+ };
322
+ },
323
+ basic(username, password) {
324
+ config.auth = {
325
+ ...config.auth,
326
+ basic: {
327
+ username,
328
+ password
329
+ }
330
+ };
331
+ },
332
+ apiKey(scheme, value) {
333
+ config.auth = {
334
+ ...config.auth,
335
+ apiKey: {
336
+ ...config.auth?.apiKey,
337
+ [scheme]: value
338
+ }
339
+ };
340
+ }
341
+ };
342
+ return client;
343
+ }
344
+ //#endregion
345
+ export { createClientCore };
@@ -0,0 +1,18 @@
1
+ /** The error thrown (throw mode) for a non-2xx response, carrying the decoded error body. */
2
+ export declare class ApiError extends Error {
3
+ readonly url: string;
4
+ readonly status: number;
5
+ readonly statusText: string;
6
+ readonly body: unknown;
7
+ constructor(url: string, status: number, statusText: string, body: unknown);
8
+ }
9
+ /** The error thrown when a request attempt exceeds the configured `timeout` — carries
10
+ * the context a log line needs (which operation, what budget, which attempt). */
11
+ export declare class TimeoutError extends Error {
12
+ readonly operationId: string;
13
+ readonly timeout: number;
14
+ readonly attempt: number;
15
+ constructor(operationId: string, timeout: number, attempt: number);
16
+ }
17
+ /** The error to throw for an aborted request: the caller's abort reason when it is an Error. */
18
+ export declare function abortError(signal: AbortSignal): globalThis.Error;
@@ -0,0 +1,38 @@
1
+ //#region src/runtime/errors.ts
2
+ /** The error thrown (throw mode) for a non-2xx response, carrying the decoded error body. */
3
+ var ApiError = class extends Error {
4
+ url;
5
+ status;
6
+ statusText;
7
+ body;
8
+ constructor(url, status, statusText, body) {
9
+ super(`Request failed with status ${status}`);
10
+ this.name = "ApiError";
11
+ this.url = url;
12
+ this.status = status;
13
+ this.statusText = statusText;
14
+ this.body = body;
15
+ }
16
+ };
17
+ /** The error thrown when a request attempt exceeds the configured `timeout` — carries
18
+ * the context a log line needs (which operation, what budget, which attempt). */
19
+ var TimeoutError = class extends Error {
20
+ operationId;
21
+ timeout;
22
+ attempt;
23
+ constructor(operationId, timeout, attempt) {
24
+ super(`Request "${operationId}" timed out after ${timeout} ms (attempt ${attempt})`);
25
+ this.name = "TimeoutError";
26
+ this.operationId = operationId;
27
+ this.timeout = timeout;
28
+ this.attempt = attempt;
29
+ }
30
+ };
31
+ /** The error to throw for an aborted request: the caller's abort reason when it is an Error. */
32
+ function abortError(signal) {
33
+ const reason = signal.reason;
34
+ if (reason instanceof Error) return reason;
35
+ return new DOMException("The operation was aborted.", "AbortError");
36
+ }
37
+ //#endregion
38
+ export { ApiError, TimeoutError, abortError };
@@ -0,0 +1,11 @@
1
+ import { Client, ClientConfig, OperationContext, OperationDescriptor, OpsShape } from './types.ts';
2
+ /**
3
+ * The client factory: `createClientCore` wired with the capabilities this API needs.
4
+ * Exported so apps can build additional instances (per-tenant, per-environment) over
5
+ * the same `OPERATIONS`/`Ops`. The trailing string params carry the wiring's literal
6
+ * unions (`OperationId`/`OperationPath`/`OperationTag`) into `ctx.operation`.
7
+ */
8
+ export declare function createClient<Ops extends OpsShape, Id extends string = string, Path extends string = string, Tag extends string = string>(operations: Record<string, OperationDescriptor>, config?: ClientConfig<OperationContext<Id, Path, Tag>>): Client<Ops, OperationContext<Id, Path, Tag>>;
9
+ export { ApiError, TimeoutError } from './errors.ts';
10
+ export { defaultRetryOn } from './retry.ts';
11
+ export type * from './types.ts';
@@ -0,0 +1,16 @@
1
+ import { ApiError, TimeoutError } from "./errors.js";
2
+ import { defaultRetryOn } from "./retry.js";
3
+ import { createClientCore } from "./create-client.js";
4
+ import { resolveAuth } from "./auth.js";
5
+ //#region src/runtime/factory.ts
6
+ /**
7
+ * The client factory: `createClientCore` wired with the capabilities this API needs.
8
+ * Exported so apps can build additional instances (per-tenant, per-environment) over
9
+ * the same `OPERATIONS`/`Ops`. The trailing string params carry the wiring's literal
10
+ * unions (`OperationId`/`OperationPath`/`OperationTag`) into `ctx.operation`.
11
+ */
12
+ function createClient(operations, config) {
13
+ return createClientCore(operations, config, { resolveAuth });
14
+ }
15
+ //#endregion
16
+ export { ApiError, TimeoutError, createClient, defaultRetryOn };
@@ -0,0 +1,12 @@
1
+ import { ParseAs } from './types.ts';
2
+ /**
3
+ * Read the response body per `kind`. `'auto'` negotiates from the content type
4
+ * (JSON, then `text/*`, then Blob); `204` responses read nothing. A `'void'`
5
+ * operation (no declared 2xx content) still returns a JSON body the server
6
+ * actually sends: the static type stays `void`, but silently dropping real data
7
+ * behind a spec gap is the worse failure — consumers can reach it with a cast
8
+ * while the API description catches up.
9
+ */
10
+ export declare function parse(response: Response, kind: ParseAs | 'void'): Promise<unknown>;
11
+ /** Best-effort decode of a non-2xx body (JSON when declared, else text; undefined on failure). */
12
+ export declare function readError(response: Response): Promise<unknown>;
@@ -0,0 +1,41 @@
1
+ //#region src/runtime/parse.ts
2
+ /**
3
+ * Read the response body per `kind`. `'auto'` negotiates from the content type
4
+ * (JSON, then `text/*`, then Blob); `204` responses read nothing. A `'void'`
5
+ * operation (no declared 2xx content) still returns a JSON body the server
6
+ * actually sends: the static type stays `void`, but silently dropping real data
7
+ * behind a spec gap is the worse failure — consumers can reach it with a cast
8
+ * while the API description catches up.
9
+ */
10
+ async function parse(response, kind) {
11
+ if (kind === "void") {
12
+ if (response.status === 204 || response.status === 205 || response.status === 304) return;
13
+ if (!(response.headers.get("content-type") ?? "").toLowerCase().includes("json")) return void 0;
14
+ const text = await response.text().catch(() => "");
15
+ if (text === "") return void 0;
16
+ try {
17
+ return JSON.parse(text);
18
+ } catch {
19
+ return;
20
+ }
21
+ }
22
+ if (response.status === 204) return void 0;
23
+ if (kind === "stream") return response.body;
24
+ if (kind === "blob") return response.blob();
25
+ if (kind === "arrayBuffer") return response.arrayBuffer();
26
+ if (kind === "formData") return response.formData();
27
+ if (kind === "text") return response.text();
28
+ if (kind === "json") return response.json();
29
+ const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
30
+ if (contentType.includes("json")) return response.json();
31
+ if (contentType.startsWith("text/")) return response.text();
32
+ const blob = await response.blob();
33
+ return blob.size > 0 ? blob : void 0;
34
+ }
35
+ /** Best-effort decode of a non-2xx body (JSON when declared, else text; undefined on failure). */
36
+ async function readError(response) {
37
+ if ((response.headers.get("content-type") ?? "").toLowerCase().includes("json")) return response.json().catch(() => void 0);
38
+ return response.text().catch(() => void 0);
39
+ }
40
+ //#endregion
41
+ export { parse, readError };