@allisall/sdk 0.0.0-stage → 0.1.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/dist/errors.js ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * The base of every error the SDK throws. Catch it to handle any SDK
3
+ * failure; catch `ApiError`, `ConnectionError` or `IngestError` to handle
4
+ * one condition.
5
+ */
6
+ export class AllisallError extends Error {
7
+ name = "AllisallError";
8
+ }
9
+ /**
10
+ * An error response from the API, or a successful response whose body was
11
+ * not the expected shape. Branch on `code`; for a code you do not
12
+ * recognise, fall back on `status`.
13
+ */
14
+ export class ApiError extends AllisallError {
15
+ name = "ApiError";
16
+ /** The HTTP status. */
17
+ status;
18
+ /** The problem's `code`. Absent when the body was not a problem document. */
19
+ code;
20
+ /** The whole problem document. Absent when the body was not one. */
21
+ problem;
22
+ /** The problem's `request_id`, or else the response's `X-Request-Id`. */
23
+ requestId;
24
+ /**
25
+ * The response's `Retry-After`, in milliseconds from when the response
26
+ * arrived. Absent when the response had none, or one that did not parse
27
+ * or named a time in the past.
28
+ */
29
+ retryAfter;
30
+ /** The response headers. */
31
+ headers;
32
+ constructor(init) {
33
+ super(init.message ?? apiErrorMessage(init.status, init.problem));
34
+ this.status = init.status;
35
+ this.problem = init.problem;
36
+ this.code = init.problem?.code;
37
+ const problemId = init.problem?.request_id;
38
+ this.requestId =
39
+ problemId !== undefined && problemId !== "" ? problemId : init.requestId;
40
+ this.retryAfter = init.retryAfter;
41
+ this.headers = init.headers;
42
+ }
43
+ }
44
+ function apiErrorMessage(status, problem) {
45
+ const parts = [`allisall: the API answered ${status}`];
46
+ if (problem?.code !== undefined)
47
+ parts.push(problem.code);
48
+ if (problem?.detail !== undefined)
49
+ parts.push(problem.detail);
50
+ return parts.join(": ");
51
+ }
52
+ /**
53
+ * A request that got no response: the connection failed, was reset, or the
54
+ * attempt timed out. `cause` holds the underlying error.
55
+ */
56
+ export class ConnectionError extends AllisallError {
57
+ name = "ConnectionError";
58
+ /** The `X-Request-Id` the SDK sent on the last attempt, to quote when reporting it. */
59
+ requestId;
60
+ constructor(requestId, cause) {
61
+ super(`allisall: no response from the API (request ${requestId}): ${causeText(cause)}`, { cause });
62
+ this.requestId = requestId;
63
+ }
64
+ }
65
+ function causeText(cause) {
66
+ return cause instanceof Error ? cause.message : String(cause);
67
+ }
68
+ /**
69
+ * A batch of an `ingest` call failed, so the call stopped and sent nothing
70
+ * after it. `cause` holds what failed the batch: an `ApiError`, a
71
+ * `ConnectionError`, the caller's cancellation, or the error from getting
72
+ * the token: the token provider's own error, unchanged, or the
73
+ * `AllisallError` refusing a provider's API key in a browser. To recover,
74
+ * send `remaining` again: every event in it keeps the `event_id` it was
75
+ * sent with, so any the failed batch stored come back as duplicates.
76
+ */
77
+ export class IngestError extends AllisallError {
78
+ name = "IngestError";
79
+ /** The combined outcome of the batches that succeeded before the failed one. */
80
+ summary;
81
+ /**
82
+ * Copies of every event from the failed batch onward, each with its
83
+ * `event_id`, less any event too large to send: those are rows of
84
+ * `summary.errors` and never in `remaining`. To map `remaining` back to
85
+ * your list, walk it from `remainingFrom` and skip the `index` of every
86
+ * `summary.errors` row at or past `remainingFrom`. With no such row,
87
+ * `remaining[i]` is your event `remainingFrom + i`.
88
+ */
89
+ remaining;
90
+ /** The index, in the list passed to `ingest`, of the first event in `remaining`. */
91
+ remainingFrom;
92
+ constructor(cause, summary, remaining, remainingFrom) {
93
+ super(`allisall: an ingest batch failed and the call stopped; send the ${remaining.length} event(s) in remaining, from index ${remainingFrom}, again: ${causeText(cause)}`, { cause });
94
+ this.summary = summary;
95
+ this.remaining = remaining;
96
+ this.remainingFrom = remainingFrom;
97
+ }
98
+ }
@@ -0,0 +1,5 @@
1
+ export { Allisall, type ClientOptions, type RequestOptions } from "./client.js";
2
+ export { type Decimal, decimal } from "./decimal.js";
3
+ export { AllisallError, ApiError, ConnectionError, IngestError, } from "./errors.js";
4
+ export type { FetchLike, TokenProvider } from "./transport.js";
5
+ export type { ErrorCode, EventCategory, EventInput, FieldError, FieldErrorCode, IngestRowError, IngestSummary, Problem, } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ // The public surface of @allisall/sdk.
2
+ export { Allisall } from "./client.js";
3
+ export { decimal } from "./decimal.js";
4
+ export { AllisallError, ApiError, ConnectionError, IngestError, } from "./errors.js";
@@ -0,0 +1,28 @@
1
+ import type { EventInput, IngestResult, IngestSummary } from "./types.js";
2
+ /** The most events one batch holds. */
3
+ export declare const MAX_BATCH_EVENTS = 500;
4
+ /** The API's body limit for a batch, in bytes, inclusive. */
5
+ export declare const MAX_BODY_BYTES = 5000000;
6
+ /** The largest event that fits in a batch alone. */
7
+ export declare const MAX_EVENT_BYTES: number;
8
+ /** Sends one batch body of `count` events and resolves with the API's outcome for it. */
9
+ export type SendBatch = (body: Uint8Array, count: number, signal: AbortSignal | undefined) => Promise<IngestResult>;
10
+ /**
11
+ * Encodes one event as compact JSON. Every member the object holds is
12
+ * written, in its own order; a member set to `undefined` is omitted, as
13
+ * `JSON.stringify` omits it.
14
+ */
15
+ export declare function encodeEvent(event: EventInput, index: number): string;
16
+ /**
17
+ * Reads a `2xx` ingest body for a batch of `count` events as an
18
+ * `IngestResult`, or `undefined` when it is not one. A row error whose
19
+ * `index` is not a position in the batch makes the body unreadable, as a
20
+ * body of the wrong shape is.
21
+ */
22
+ export declare function readIngestResult(value: unknown, count: number): IngestResult | undefined;
23
+ /**
24
+ * Sends any number of events in as many batches as the limits require,
25
+ * one after another, and combines their outcomes. Stops at the first batch
26
+ * that fails, throwing an `IngestError`.
27
+ */
28
+ export declare function ingest(events: readonly EventInput[], send: SendBatch, signal: AbortSignal | undefined): Promise<IngestSummary>;
package/dist/ingest.js ADDED
@@ -0,0 +1,247 @@
1
+ // The ingest helper: assigning ids, serialising, splitting into batches,
2
+ // sending them one at a time, and combining the outcomes.
3
+ import { isDecimalText } from "./decimal.js";
4
+ import { IngestError } from "./errors.js";
5
+ import { uuidv4 } from "./uuid.js";
6
+ /** The most events one batch holds. */
7
+ export const MAX_BATCH_EVENTS = 500;
8
+ /** The API's body limit for a batch, in bytes, inclusive. */
9
+ export const MAX_BODY_BYTES = 5_000_000;
10
+ /** The bytes of `{"events":[` and `]}` around a batch's events. */
11
+ const ENVELOPE_BYTES = 13;
12
+ /** The largest event that fits in a batch alone. */
13
+ export const MAX_EVENT_BYTES = MAX_BODY_BYTES - ENVELOPE_BYTES;
14
+ const encoder = new TextEncoder();
15
+ const PREFIX = encoder.encode('{"events":[');
16
+ const SUFFIX = encoder.encode("]}");
17
+ const COMMA = 0x2c;
18
+ // A UTF-16 surrogate without its partner, which UTF-8 cannot encode.
19
+ const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
20
+ function checkText(text, index) {
21
+ if (LONE_SURROGATE.test(text)) {
22
+ throw new TypeError(`allisall: event ${index} holds text that is not valid Unicode (a lone surrogate)`);
23
+ }
24
+ }
25
+ /** `JSON.stringify`, refusing text that is not valid Unicode. */
26
+ function stringify(value, index) {
27
+ const text = JSON.stringify(value, (key, inner) => {
28
+ checkText(key, index);
29
+ if (typeof inner === "string")
30
+ checkText(inner, index);
31
+ return inner;
32
+ });
33
+ return text;
34
+ }
35
+ const NUMERIC_MEMBERS = new Set([
36
+ "quantity",
37
+ "cost",
38
+ "tokens_in",
39
+ "tokens_out",
40
+ ]);
41
+ function encodeMember(name, value, index) {
42
+ if (typeof value === "number" && NUMERIC_MEMBERS.has(name)) {
43
+ if (!Number.isFinite(value)) {
44
+ throw new TypeError(`allisall: event ${index} has ${name} ${value}; it must be a finite number`);
45
+ }
46
+ return JSON.stringify(value);
47
+ }
48
+ if (name === "cost" && typeof value === "string") {
49
+ // A Decimal is written unquoted, so its syntax is checked again here:
50
+ // an unbranded string from untyped code must not inject JSON.
51
+ if (!isDecimalText(value)) {
52
+ throw new TypeError(`allisall: event ${index} has cost ${JSON.stringify(value)}, which is not a decimal; build it with decimal()`);
53
+ }
54
+ return value;
55
+ }
56
+ if (name === "timestamp" && value instanceof Date) {
57
+ if (Number.isNaN(value.getTime())) {
58
+ throw new TypeError(`allisall: event ${index} has an invalid Date as its timestamp`);
59
+ }
60
+ return JSON.stringify(value.toISOString());
61
+ }
62
+ return stringify(value, index);
63
+ }
64
+ /**
65
+ * Encodes one event as compact JSON. Every member the object holds is
66
+ * written, in its own order; a member set to `undefined` is omitted, as
67
+ * `JSON.stringify` omits it.
68
+ */
69
+ export function encodeEvent(event, index) {
70
+ const members = [];
71
+ for (const [name, value] of Object.entries(event)) {
72
+ if (value === undefined)
73
+ continue;
74
+ const text = encodeMember(name, value, index);
75
+ if (text === undefined)
76
+ continue;
77
+ members.push(`${stringify(name, index)}:${text}`);
78
+ }
79
+ return `{${members.join(",")}}`;
80
+ }
81
+ function needsId(id) {
82
+ return id === undefined || id === null || id === "";
83
+ }
84
+ function prepare(events) {
85
+ return events.map((original, index) => {
86
+ if (typeof original !== "object" ||
87
+ original === null ||
88
+ Array.isArray(original)) {
89
+ throw new TypeError(`allisall: event ${index} is not an object`);
90
+ }
91
+ const event = { ...original };
92
+ if (needsId(event.event_id))
93
+ event.event_id = uuidv4();
94
+ return { index, event, bytes: encoder.encode(encodeEvent(event, index)) };
95
+ });
96
+ }
97
+ function tooLarge(p) {
98
+ return {
99
+ index: p.index,
100
+ code: "too_large",
101
+ pointer: `/events/${p.index}`,
102
+ detail: `The event serialises to ${p.bytes.length} bytes and was not sent. ` +
103
+ `The SDK sends an event of at most ${MAX_EVENT_BYTES} bytes, so that a batch holding it alone ` +
104
+ `stays within the API's body limit of ${MAX_BODY_BYTES} bytes.`,
105
+ };
106
+ }
107
+ /**
108
+ * Packs events into batches in input order. A batch closes at 500 events,
109
+ * or before an event that would take its body over 5,000,000 bytes. An
110
+ * event too large to send alone joins no batch and becomes a row error.
111
+ */
112
+ function split(prepared) {
113
+ const batches = [];
114
+ const rejected = [];
115
+ let current;
116
+ for (const p of prepared) {
117
+ const size = p.bytes.length;
118
+ if (size > MAX_EVENT_BYTES) {
119
+ rejected.push(tooLarge(p));
120
+ continue;
121
+ }
122
+ if (current !== undefined &&
123
+ (current.events.length >= MAX_BATCH_EVENTS ||
124
+ current.size + 1 + size > MAX_BODY_BYTES)) {
125
+ current = undefined;
126
+ }
127
+ if (current === undefined) {
128
+ current = { events: [p], size: ENVELOPE_BYTES + size };
129
+ batches.push(current);
130
+ }
131
+ else {
132
+ current.events.push(p);
133
+ current.size += 1 + size;
134
+ }
135
+ }
136
+ return { batches, rejected };
137
+ }
138
+ /** Writes a batch as `{"events":[e1,e2,…,en]}`, reusing each event's bytes. */
139
+ function body(batch) {
140
+ const out = new Uint8Array(batch.size);
141
+ out.set(PREFIX, 0);
142
+ let at = PREFIX.length;
143
+ batch.events.forEach((p, i) => {
144
+ if (i > 0)
145
+ out[at++] = COMMA;
146
+ out.set(p.bytes, at);
147
+ at += p.bytes.length;
148
+ });
149
+ out.set(SUFFIX, at);
150
+ return out;
151
+ }
152
+ const POINTER_ROW = /^\/events\/(\d+)(?=\/|$)/;
153
+ /** Rewrites a batch's row error to the caller's list. */
154
+ function rewrite(error, batch) {
155
+ const toCaller = (i) => batch.events[i]?.index ?? i;
156
+ const pointer = error.pointer.replace(POINTER_ROW, (_, row) => `/events/${toCaller(Number(row))}`);
157
+ return { ...error, index: toCaller(error.index), pointer };
158
+ }
159
+ /**
160
+ * Reads a `2xx` ingest body for a batch of `count` events as an
161
+ * `IngestResult`, or `undefined` when it is not one. A row error whose
162
+ * `index` is not a position in the batch makes the body unreadable, as a
163
+ * body of the wrong shape is.
164
+ */
165
+ export function readIngestResult(value, count) {
166
+ if (typeof value !== "object" || value === null)
167
+ return undefined;
168
+ const accepted = Reflect.get(value, "accepted");
169
+ const duplicates = Reflect.get(value, "duplicates");
170
+ const rejected = Reflect.get(value, "rejected");
171
+ const errors = Reflect.get(value, "errors");
172
+ const truncated = Reflect.get(value, "errors_truncated");
173
+ if (typeof accepted !== "number" ||
174
+ typeof duplicates !== "number" ||
175
+ typeof rejected !== "number" ||
176
+ typeof truncated !== "boolean" ||
177
+ !Array.isArray(errors)) {
178
+ return undefined;
179
+ }
180
+ const rows = [];
181
+ for (const e of errors) {
182
+ if (typeof e !== "object" || e === null)
183
+ return undefined;
184
+ const index = Reflect.get(e, "index");
185
+ const code = Reflect.get(e, "code");
186
+ const pointer = Reflect.get(e, "pointer");
187
+ const detail = Reflect.get(e, "detail");
188
+ if (typeof index !== "number" ||
189
+ typeof code !== "string" ||
190
+ typeof pointer !== "string" ||
191
+ typeof detail !== "string") {
192
+ return undefined;
193
+ }
194
+ if (!Number.isInteger(index) || index < 0 || index >= count) {
195
+ return undefined;
196
+ }
197
+ rows.push({ index, code, pointer, detail });
198
+ }
199
+ return {
200
+ accepted,
201
+ duplicates,
202
+ rejected,
203
+ errors: rows,
204
+ errors_truncated: truncated,
205
+ };
206
+ }
207
+ /**
208
+ * Sends any number of events in as many batches as the limits require,
209
+ * one after another, and combines their outcomes. Stops at the first batch
210
+ * that fails, throwing an `IngestError`.
211
+ */
212
+ export async function ingest(events, send, signal) {
213
+ const { batches, rejected } = split(prepare(events));
214
+ const summary = {
215
+ accepted: 0,
216
+ duplicates: 0,
217
+ rejected: rejected.length,
218
+ errors: [...rejected],
219
+ errors_truncated: false,
220
+ batches: 0,
221
+ };
222
+ const finish = () => {
223
+ // Stable, so a row's entries keep the order the API gave them.
224
+ summary.errors.sort((a, b) => a.index - b.index);
225
+ return summary;
226
+ };
227
+ for (const [i, batch] of batches.entries()) {
228
+ let result;
229
+ try {
230
+ result = await send(body(batch), batch.events.length, signal);
231
+ }
232
+ catch (err) {
233
+ // Every failure of a batch is wrapped, the token provider's included,
234
+ // so the caller keeps the summary and the generated ids in remaining.
235
+ const remaining = batches.slice(i).flatMap((b) => b.events);
236
+ throw new IngestError(err, finish(), remaining.map((p) => p.event), remaining[0]?.index ?? 0);
237
+ }
238
+ summary.accepted += result.accepted;
239
+ summary.duplicates += result.duplicates;
240
+ summary.rejected += result.rejected;
241
+ summary.errors_truncated ||= result.errors_truncated;
242
+ for (const error of result.errors)
243
+ summary.errors.push(rewrite(error, batch));
244
+ summary.batches++;
245
+ }
246
+ return finish();
247
+ }
@@ -0,0 +1,18 @@
1
+ /** The longest `Retry-After` the SDK honours. A longer one ends the call. */
2
+ export declare const MAX_RETRY_AFTER_MS = 60000;
3
+ /** The statuses worth repeating: a proxy's `408`, `429`, `500`, and a gateway's `502`, `503`, `504`. */
4
+ export declare function isRetryableStatus(status: number): boolean;
5
+ /**
6
+ * The wait before retry `n` (1 for the first retry) when the response
7
+ * carried no usable `Retry-After`: `min(0.5 s × 2^(n−1), 8 s)`, multiplied
8
+ * by a random factor between 0.75 and 1.
9
+ */
10
+ export declare function backoffMs(retry: number): number;
11
+ /**
12
+ * Parses a `Retry-After` value, delay-seconds or an HTTP-date (RFC 9110
13
+ * §10.2.3), into milliseconds from `now`. Returns `undefined` for a value
14
+ * that does not parse or names a time in the past.
15
+ */
16
+ export declare function parseRetryAfter(value: string | null, now: number): number | undefined;
17
+ /** Waits `ms` milliseconds, or rejects with the signal's reason when it aborts first. */
18
+ export declare function sleep(ms: number, signal: AbortSignal | undefined): Promise<void>;
package/dist/retry.js ADDED
@@ -0,0 +1,105 @@
1
+ // The retry decision and the wait between attempts.
2
+ /** The longest `Retry-After` the SDK honours. A longer one ends the call. */
3
+ export const MAX_RETRY_AFTER_MS = 60_000;
4
+ const INITIAL_BACKOFF_MS = 500;
5
+ const MAX_BACKOFF_MS = 8_000;
6
+ /** The statuses worth repeating: a proxy's `408`, `429`, `500`, and a gateway's `502`, `503`, `504`. */
7
+ export function isRetryableStatus(status) {
8
+ return (status === 408 ||
9
+ status === 429 ||
10
+ status === 500 ||
11
+ status === 502 ||
12
+ status === 503 ||
13
+ status === 504);
14
+ }
15
+ /**
16
+ * The wait before retry `n` (1 for the first retry) when the response
17
+ * carried no usable `Retry-After`: `min(0.5 s × 2^(n−1), 8 s)`, multiplied
18
+ * by a random factor between 0.75 and 1.
19
+ */
20
+ export function backoffMs(retry) {
21
+ const base = Math.min(INITIAL_BACKOFF_MS * 2 ** (retry - 1), MAX_BACKOFF_MS);
22
+ return base * (1 - Math.random() * 0.25);
23
+ }
24
+ const MONTHS = [
25
+ "Jan",
26
+ "Feb",
27
+ "Mar",
28
+ "Apr",
29
+ "May",
30
+ "Jun",
31
+ "Jul",
32
+ "Aug",
33
+ "Sep",
34
+ "Oct",
35
+ "Nov",
36
+ "Dec",
37
+ ];
38
+ // RFC 9110 §5.6.7: IMF-fixdate, and the two obsolete forms a recipient must accept.
39
+ const IMF_FIXDATE = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d{2}) ([A-Z][a-z]{2}) (\d{4}) (\d{2}):(\d{2}):(\d{2}) GMT$/;
40
+ const RFC850_DATE = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\d{2})-([A-Z][a-z]{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2}) GMT$/;
41
+ const ASCTIME_DATE = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) ([A-Z][a-z]{2}) ([ \d]\d) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/;
42
+ function utc(year, month, day, hour, minute, second) {
43
+ const m = MONTHS.indexOf(month);
44
+ if (m < 0)
45
+ return undefined;
46
+ const ms = Date.UTC(year, m, Number(day.trim()), Number(hour), Number(minute), Number(second));
47
+ return Number.isNaN(ms) ? undefined : ms;
48
+ }
49
+ function parseHttpDate(value, now) {
50
+ let match = IMF_FIXDATE.exec(value);
51
+ if (match !== null) {
52
+ const [, day = "", month = "", year = "", h = "", m = "", s = ""] = match;
53
+ return utc(Number(year), month, day, h, m, s);
54
+ }
55
+ match = RFC850_DATE.exec(value);
56
+ if (match !== null) {
57
+ const [, day = "", month = "", yy = "", h = "", m = "", s = ""] = match;
58
+ // RFC 9110 §5.6.7: a two-digit year more than 50 years in the future is in the past century.
59
+ const thisYear = new Date(now).getUTCFullYear();
60
+ let year = 2000 + Number(yy);
61
+ if (year > thisYear + 50)
62
+ year -= 100;
63
+ return utc(year, month, day, h, m, s);
64
+ }
65
+ match = ASCTIME_DATE.exec(value);
66
+ if (match !== null) {
67
+ const [, month = "", day = "", h = "", m = "", s = "", year = ""] = match;
68
+ return utc(Number(year), month, day, h, m, s);
69
+ }
70
+ return undefined;
71
+ }
72
+ /**
73
+ * Parses a `Retry-After` value, delay-seconds or an HTTP-date (RFC 9110
74
+ * §10.2.3), into milliseconds from `now`. Returns `undefined` for a value
75
+ * that does not parse or names a time in the past.
76
+ */
77
+ export function parseRetryAfter(value, now) {
78
+ if (value === null)
79
+ return undefined;
80
+ const text = value.trim();
81
+ if (/^\d+$/.test(text))
82
+ return Number(text) * 1000;
83
+ const at = parseHttpDate(text, now);
84
+ if (at === undefined || at < now)
85
+ return undefined;
86
+ return at - now;
87
+ }
88
+ /** Waits `ms` milliseconds, or rejects with the signal's reason when it aborts first. */
89
+ export function sleep(ms, signal) {
90
+ return new Promise((resolve, reject) => {
91
+ if (signal?.aborted) {
92
+ reject(signal.reason);
93
+ return;
94
+ }
95
+ const onAbort = () => {
96
+ clearTimeout(timer);
97
+ reject(signal?.reason);
98
+ };
99
+ const timer = setTimeout(() => {
100
+ signal?.removeEventListener("abort", onAbort);
101
+ resolve();
102
+ }, ms);
103
+ signal?.addEventListener("abort", onAbort, { once: true });
104
+ });
105
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Reports whether the SDK runs in a browser: a window, where `document`
3
+ * is defined, or a Web Worker, where `globalThis` is an instance of
4
+ * `WorkerGlobalScope`. Node, Deno, Bun and edge runtimes define neither.
5
+ */
6
+ export declare function isBrowser(): boolean;
7
+ /**
8
+ * Reads an environment variable from `globalThis.process.env`, where it
9
+ * exists. Returns `undefined` for a variable that is unset or empty.
10
+ */
11
+ export declare function readEnv(name: string): string | undefined;
12
+ /**
13
+ * The `User-Agent` to send: `allisall-ts/<version> node/<node version>`.
14
+ * A server runtime that reports no Node version, such as an edge runtime,
15
+ * sends `allisall-ts/<version>` alone. A browser sends none.
16
+ */
17
+ export declare function userAgent(): string | undefined;
@@ -0,0 +1,60 @@
1
+ // What the SDK learns about the runtime it runs in: whether it is a
2
+ // browser, the environment, and the User-Agent. Globals are read through
3
+ // `globalThis` by name, so a runtime without them, such as a browser
4
+ // without `process`, is never touched.
5
+ import { VERSION } from "./version.js";
6
+ function globalValue(name) {
7
+ const value = Reflect.get(globalThis, name);
8
+ return value;
9
+ }
10
+ function isObject(value) {
11
+ return typeof value === "object" && value !== null;
12
+ }
13
+ /**
14
+ * Reports whether the SDK runs in a browser: a window, where `document`
15
+ * is defined, or a Web Worker, where `globalThis` is an instance of
16
+ * `WorkerGlobalScope`. Node, Deno, Bun and edge runtimes define neither.
17
+ */
18
+ export function isBrowser() {
19
+ if (globalValue("document") !== undefined)
20
+ return true;
21
+ const scope = globalValue("WorkerGlobalScope");
22
+ return typeof scope === "function" && globalThis instanceof scope;
23
+ }
24
+ /**
25
+ * Reads an environment variable from `globalThis.process.env`, where it
26
+ * exists. Returns `undefined` for a variable that is unset or empty.
27
+ */
28
+ export function readEnv(name) {
29
+ const process = globalValue("process");
30
+ if (!isObject(process))
31
+ return undefined;
32
+ const env = Reflect.get(process, "env");
33
+ if (!isObject(env))
34
+ return undefined;
35
+ const value = Reflect.get(env, name);
36
+ return typeof value === "string" && value !== "" ? value : undefined;
37
+ }
38
+ function nodeVersion() {
39
+ const process = globalValue("process");
40
+ if (!isObject(process))
41
+ return undefined;
42
+ const versions = Reflect.get(process, "versions");
43
+ if (!isObject(versions))
44
+ return undefined;
45
+ const node = Reflect.get(versions, "node");
46
+ return typeof node === "string" && node !== "" ? node : undefined;
47
+ }
48
+ /**
49
+ * The `User-Agent` to send: `allisall-ts/<version> node/<node version>`.
50
+ * A server runtime that reports no Node version, such as an edge runtime,
51
+ * sends `allisall-ts/<version>` alone. A browser sends none.
52
+ */
53
+ export function userAgent() {
54
+ if (isBrowser())
55
+ return undefined;
56
+ const node = nodeVersion();
57
+ return node === undefined
58
+ ? `allisall-ts/${VERSION}`
59
+ : `allisall-ts/${VERSION} node/${node}`;
60
+ }
@@ -0,0 +1,53 @@
1
+ import type { Problem } from "./types.js";
2
+ /** A `fetch`-compatible function. */
3
+ export type FetchLike = (url: string, init: RequestInit) => Promise<Response>;
4
+ /** A callback returning the bearer token to send, called before every attempt. */
5
+ export type TokenProvider = () => string | Promise<string>;
6
+ /** Where the bearer token comes from. */
7
+ export type Credential = {
8
+ readonly kind: "apiKey";
9
+ readonly apiKey: string;
10
+ } | {
11
+ readonly kind: "provider";
12
+ readonly provider: TokenProvider;
13
+ };
14
+ /** Everything an attempt needs, fixed when the client is constructed. */
15
+ export interface TransportConfig {
16
+ readonly baseURL: URL;
17
+ readonly credential: Credential;
18
+ readonly timeout: number;
19
+ readonly maxRetries: number;
20
+ readonly fetch: FetchLike | undefined;
21
+ /** Whether a token that is an API key is refused: a browser without the opt-in. */
22
+ readonly refuseApiKeys: boolean;
23
+ /** The `User-Agent` to send; `undefined` in a browser. */
24
+ readonly userAgent: string | undefined;
25
+ }
26
+ /** The message of the error refusing an API key in a browser. */
27
+ export declare const BROWSER_KEY_MESSAGE: string;
28
+ /** Reports whether a bearer token is an API key, which starts `aia_`. */
29
+ export declare function isApiKey(token: string): boolean;
30
+ /** Joins the base URL and an operation's path with exactly one `/`. */
31
+ export declare function joinURL(base: URL, path: string): string;
32
+ /**
33
+ * Reports whether every character of a credential, already trimmed, is
34
+ * printable ASCII (U+0021 to U+007E), as a bearer credential is. Anything
35
+ * else, a control character, an inner space or a non-ASCII character,
36
+ * cannot travel in the header. The check never quotes the credential, so
37
+ * no error built from it can leak the secret.
38
+ */
39
+ export declare function isPrintableCredential(credential: string): boolean;
40
+ /**
41
+ * Reads an `application/problem+json` body as a problem document. Returns
42
+ * `undefined` for any other body, or one that is not a JSON object. A
43
+ * member of the wrong type is left out on its own, as is a malformed
44
+ * entry of `errors`; the rest of the problem is kept. Members the SDK does
45
+ * not know are ignored.
46
+ */
47
+ export declare function readProblem(contentType: string | null, text: string): Problem | undefined;
48
+ /**
49
+ * Posts a JSON body, retrying a failure worth repeating. The body is the
50
+ * same bytes on every attempt. Each attempt reads a fresh token and sends
51
+ * a fresh `X-Request-Id`.
52
+ */
53
+ export declare function post<T>(config: TransportConfig, path: string, body: Uint8Array, read: (body: unknown) => T | undefined, signal: AbortSignal | undefined): Promise<T>;