@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.
@@ -0,0 +1,220 @@
1
+ // One HTTP attempt (headers, timeout, reading the response) and the loop
2
+ // that repeats it under the retry rules in retry.ts.
3
+ import { AllisallError, ApiError, ConnectionError } from "./errors.js";
4
+ import { backoffMs, isRetryableStatus, MAX_RETRY_AFTER_MS, parseRetryAfter, sleep, } from "./retry.js";
5
+ import { uuidv4 } from "./uuid.js";
6
+ /** The message of the error refusing an API key in a browser. */
7
+ export const BROWSER_KEY_MESSAGE = "allisall: refusing to use an API key in a browser, where it is readable by anyone who loads the page. " +
8
+ "Call the API from your server instead, or pass dangerouslyAllowBrowser: true if you accept that risk.";
9
+ /** Reports whether a bearer token is an API key, which starts `aia_`. */
10
+ export function isApiKey(token) {
11
+ return token.startsWith("aia_");
12
+ }
13
+ /** Joins the base URL and an operation's path with exactly one `/`. */
14
+ export function joinURL(base, path) {
15
+ const url = new URL(base.href);
16
+ url.pathname = url.pathname.replace(/\/+$/, "") + path;
17
+ return url.href;
18
+ }
19
+ /**
20
+ * Reports whether every character of a credential, already trimmed, is
21
+ * printable ASCII (U+0021 to U+007E), as a bearer credential is. Anything
22
+ * else, a control character, an inner space or a non-ASCII character,
23
+ * cannot travel in the header. The check never quotes the credential, so
24
+ * no error built from it can leak the secret.
25
+ */
26
+ export function isPrintableCredential(credential) {
27
+ for (let i = 0; i < credential.length; i++) {
28
+ const c = credential.charCodeAt(i);
29
+ if (c < 0x21 || c > 0x7e)
30
+ return false;
31
+ }
32
+ return true;
33
+ }
34
+ async function bearerToken(config) {
35
+ const { credential } = config;
36
+ if (credential.kind === "apiKey")
37
+ return credential.apiKey;
38
+ const raw = await credential.provider();
39
+ const token = typeof raw === "string" ? raw.trim() : raw;
40
+ if (token === "") {
41
+ throw new AllisallError("allisall: the token provider returned an empty token");
42
+ }
43
+ if (typeof token === "string" && !isPrintableCredential(token)) {
44
+ throw new AllisallError("allisall: the token provider returned a token holding a character other than printable ASCII, which cannot be sent");
45
+ }
46
+ if (config.refuseApiKeys && typeof token === "string" && isApiKey(token)) {
47
+ throw new AllisallError(BROWSER_KEY_MESSAGE);
48
+ }
49
+ return token;
50
+ }
51
+ function isObject(value) {
52
+ return typeof value === "object" && value !== null && !Array.isArray(value);
53
+ }
54
+ function parseJSON(text) {
55
+ try {
56
+ const value = JSON.parse(text);
57
+ return value;
58
+ }
59
+ catch {
60
+ return undefined;
61
+ }
62
+ }
63
+ const OPTIONAL_LOCATORS = [
64
+ "pointer",
65
+ "parameter",
66
+ "header",
67
+ "part",
68
+ "column",
69
+ ];
70
+ function readFieldError(value) {
71
+ if (!isObject(value))
72
+ return undefined;
73
+ const { code, detail } = value;
74
+ if (typeof code !== "string" || typeof detail !== "string")
75
+ return undefined;
76
+ const out = { code, detail };
77
+ for (const name of OPTIONAL_LOCATORS) {
78
+ const locator = value[name];
79
+ if (typeof locator === "string")
80
+ out[name] = locator;
81
+ }
82
+ return out;
83
+ }
84
+ const PROBLEM_STRINGS = [
85
+ "type",
86
+ "title",
87
+ "detail",
88
+ "code",
89
+ "request_id",
90
+ ];
91
+ /**
92
+ * Reads an `application/problem+json` body as a problem document. Returns
93
+ * `undefined` for any other body, or one that is not a JSON object. A
94
+ * member of the wrong type is left out on its own, as is a malformed
95
+ * entry of `errors`; the rest of the problem is kept. Members the SDK does
96
+ * not know are ignored.
97
+ */
98
+ export function readProblem(contentType, text) {
99
+ const mediaType = contentType?.split(";")[0]?.trim().toLowerCase();
100
+ if (mediaType !== "application/problem+json")
101
+ return undefined;
102
+ const body = parseJSON(text);
103
+ if (!isObject(body))
104
+ return undefined;
105
+ const problem = {};
106
+ for (const name of PROBLEM_STRINGS) {
107
+ const value = body[name];
108
+ if (typeof value === "string")
109
+ problem[name] = value;
110
+ }
111
+ if (typeof body.status === "number")
112
+ problem.status = body.status;
113
+ if (Array.isArray(body.errors)) {
114
+ problem.errors = body.errors
115
+ .map(readFieldError)
116
+ .filter((e) => e !== undefined);
117
+ }
118
+ return problem;
119
+ }
120
+ function headerOrUndefined(headers, name) {
121
+ return headers.get(name) ?? undefined;
122
+ }
123
+ /**
124
+ * Sends one attempt. Resolves with the parsed `2xx` body; throws an
125
+ * `ApiError` for any other status or an unreadable `2xx` body, a
126
+ * `ConnectionError` when no response arrived, or the caller's abort reason.
127
+ */
128
+ async function attempt(config, url, body, read, token, requestId, signal) {
129
+ const headers = {
130
+ Authorization: `Bearer ${token}`,
131
+ "Content-Type": "application/json",
132
+ "X-Request-Id": requestId,
133
+ };
134
+ if (config.userAgent !== undefined)
135
+ headers["User-Agent"] = config.userAgent;
136
+ const timeout = AbortSignal.timeout(config.timeout);
137
+ const combined = signal === undefined ? timeout : AbortSignal.any([signal, timeout]);
138
+ const doFetch = config.fetch ?? globalThis.fetch;
139
+ let response;
140
+ let text;
141
+ try {
142
+ response = await doFetch(url, {
143
+ method: "POST",
144
+ headers,
145
+ body,
146
+ signal: combined,
147
+ // A redirect is never followed, so Authorization never travels to
148
+ // another location.
149
+ redirect: "manual",
150
+ });
151
+ text = await response.text();
152
+ }
153
+ catch (err) {
154
+ if (signal?.aborted)
155
+ throw signal.reason;
156
+ throw new ConnectionError(requestId, err);
157
+ }
158
+ const status = response.status;
159
+ const responseId = headerOrUndefined(response.headers, "X-Request-Id");
160
+ // A 3xx, or a browser's opaque redirect, whose status is 0.
161
+ if ((status >= 300 && status < 400) || response.type === "opaqueredirect") {
162
+ throw new ApiError({
163
+ status,
164
+ headers: response.headers,
165
+ requestId: responseId,
166
+ message: `allisall: the API answered ${status} with a redirect, which the SDK does not follow`,
167
+ });
168
+ }
169
+ if (status >= 200 && status < 300) {
170
+ const parsed = parseJSON(text);
171
+ const value = parsed === undefined ? undefined : read(parsed);
172
+ if (value !== undefined)
173
+ return value;
174
+ throw new ApiError({
175
+ status,
176
+ headers: response.headers,
177
+ requestId: responseId,
178
+ message: `allisall: the API answered ${status} with a body the SDK could not read`,
179
+ });
180
+ }
181
+ throw new ApiError({
182
+ status,
183
+ problem: readProblem(response.headers.get("Content-Type"), text),
184
+ requestId: responseId,
185
+ retryAfter: parseRetryAfter(response.headers.get("Retry-After"), Date.now()),
186
+ headers: response.headers,
187
+ });
188
+ }
189
+ /**
190
+ * Posts a JSON body, retrying a failure worth repeating. The body is the
191
+ * same bytes on every attempt. Each attempt reads a fresh token and sends
192
+ * a fresh `X-Request-Id`.
193
+ */
194
+ export async function post(config, path, body, read, signal) {
195
+ const url = joinURL(config.baseURL, path);
196
+ for (let retry = 0;; retry++) {
197
+ // An error from the token provider ends the call unwrapped and unretried.
198
+ const token = await bearerToken(config);
199
+ try {
200
+ return await attempt(config, url, body, read, token, uuidv4(), signal);
201
+ }
202
+ catch (err) {
203
+ if (!(err instanceof ApiError || err instanceof ConnectionError))
204
+ throw err;
205
+ const retryable = err instanceof ConnectionError || isRetryableStatus(err.status);
206
+ if (!retryable || retry >= config.maxRetries)
207
+ throw err;
208
+ let wait;
209
+ if (err instanceof ApiError && err.retryAfter !== undefined) {
210
+ if (err.retryAfter > MAX_RETRY_AFTER_MS)
211
+ throw err;
212
+ wait = err.retryAfter;
213
+ }
214
+ else {
215
+ wait = backoffMs(retry + 1);
216
+ }
217
+ await sleep(wait, signal);
218
+ }
219
+ }
220
+ }
@@ -0,0 +1,135 @@
1
+ import type { Decimal } from "./decimal.js";
2
+ /** The known request-level error codes, in the API's catalogue order. */
3
+ export declare const ERROR_CODES: readonly ["invalid_json", "validation_failed", "unauthenticated", "insufficient_scope", "session_required", "account_not_provisioned", "not_found", "no_active_value_model", "method_not_allowed", "idempotency_key_in_use", "client_key_in_use", "agent_key_in_use", "client_has_events", "client_has_calculations", "agent_has_events", "client_key_has_events", "value_model_exists", "value_model_version_stale", "calculation_transition_invalid", "calculation_not_draft", "calculation_not_published", "report_shared", "report_not_shared", "api_key_revoked", "payload_too_large", "uri_too_long", "unsupported_media_type", "idempotency_key_reused", "rate_limited", "internal_error"];
4
+ /** The known field-level error codes, in the API's catalogue order. */
5
+ export declare const FIELD_ERROR_CODES: readonly ["required", "unknown_field", "immutable", "invalid_type", "invalid_format", "invalid_value", "too_long", "too_many", "out_of_range", "too_precise", "duplicate", "too_large", "mutually_exclusive", "unknown_reference", "client_key_required", "currency_mismatch"];
6
+ /** The event categories. */
7
+ export declare const EVENT_CATEGORIES: readonly ["session", "task", "model", "outcome", "exception"];
8
+ /** The members of `EventInput`, in the API's order. */
9
+ export declare const EVENT_INPUT_MEMBERS: readonly ["event_id", "timestamp", "agent_key", "client_key", "category", "event_type", "outcome_type", "quantity", "cost", "tokens_in", "tokens_out", "model", "currency", "metadata"];
10
+ /**
11
+ * A request-level error code. New codes are additive, so any other string
12
+ * is possible too: branch on the codes you handle, and fall back on the
13
+ * HTTP status for the rest.
14
+ */
15
+ export type ErrorCode = (typeof ERROR_CODES)[number] | (string & {});
16
+ /**
17
+ * A field-level error code, as in a rejected ingest row. New codes are
18
+ * additive, so any other string is possible too.
19
+ */
20
+ export type FieldErrorCode = (typeof FIELD_ERROR_CODES)[number] | (string & {});
21
+ /** An event category. */
22
+ export type EventCategory = (typeof EVENT_CATEGORIES)[number];
23
+ /**
24
+ * One event to ingest. Members keep the API's snake_case names. The SDK
25
+ * sends every member the object holds, so a misspelt member reaches the
26
+ * API and is reported as a rejected row rather than dropped.
27
+ */
28
+ export interface EventInput {
29
+ /**
30
+ * A sender-chosen id; a repeat within the workspace is a duplicate. When
31
+ * it is absent, `null` or `""`, the SDK generates a UUID. Set your own
32
+ * when the same event may be sent again in a later call, such as after a
33
+ * queue redelivers a message.
34
+ */
35
+ event_id?: string | null;
36
+ /** When the event happened: a `Date`, or an RFC 3339 instant with an offset. */
37
+ timestamp: Date | string;
38
+ /** The agent that acted. */
39
+ agent_key: string;
40
+ /** The client the event belongs to. */
41
+ client_key?: string | null;
42
+ /** The event's category. */
43
+ category: EventCategory;
44
+ /** What the agent did. */
45
+ event_type: string;
46
+ /** Required when `category` is `outcome`. */
47
+ outcome_type?: string | null;
48
+ /** Absent, `null` or `0` means 1. */
49
+ quantity?: number | null;
50
+ /** In `currency`, or the workspace currency: a `Decimal`, or a finite number. */
51
+ cost?: Decimal | number | null;
52
+ /** Tokens in. */
53
+ tokens_in?: number | null;
54
+ /** Tokens out. */
55
+ tokens_out?: number | null;
56
+ /** The model the agent invoked. */
57
+ model?: string | null;
58
+ /** The currency of `cost`. Absent means the workspace currency. */
59
+ currency?: string | null;
60
+ /** Free-form members, at most 8 KB serialised. */
61
+ metadata?: Record<string, unknown> | null;
62
+ }
63
+ /** One problem with a rejected ingest row. */
64
+ export interface IngestRowError {
65
+ /** The zero-based position of the row in the list passed to `ingest`. */
66
+ index: number;
67
+ /** The field-level code. */
68
+ code: FieldErrorCode;
69
+ /** An RFC 6901 JSON Pointer to the problem, starting `/events/<index>`. */
70
+ pointer: string;
71
+ /** Human text. Its wording may change at any time. */
72
+ detail: string;
73
+ }
74
+ /** The outcome of one ingest batch, as the API sends it. */
75
+ export interface IngestResult {
76
+ accepted: number;
77
+ duplicates: number;
78
+ rejected: number;
79
+ errors: IngestRowError[];
80
+ errors_truncated: boolean;
81
+ }
82
+ /** The combined outcome of one `ingest` call, over the whole list. */
83
+ export interface IngestSummary {
84
+ /** Events stored. */
85
+ accepted: number;
86
+ /** Events already stored, by `event_id` or content hash. */
87
+ duplicates: number;
88
+ /** Events refused, counted once each, including any too large to send. */
89
+ rejected: number;
90
+ /** One entry per problem, in the order of the list passed to `ingest`. */
91
+ errors: IngestRowError[];
92
+ /** Whether any rejected row had more problems than `errors` holds. */
93
+ errors_truncated: boolean;
94
+ /** The number of batches sent. */
95
+ batches: number;
96
+ }
97
+ /** One problem in a request, from a problem document's `errors`. */
98
+ export interface FieldError {
99
+ /** The field-level code. */
100
+ code: FieldErrorCode;
101
+ /** An RFC 6901 JSON Pointer into the request body. */
102
+ pointer?: string;
103
+ /** A query parameter, by name. */
104
+ parameter?: string;
105
+ /** A request header, by name. */
106
+ header?: string;
107
+ /** A multipart part, by its form field name. */
108
+ part?: string;
109
+ /** A column of a CSV part. */
110
+ column?: string;
111
+ /** Human text. Its wording may change at any time. */
112
+ detail: string;
113
+ }
114
+ /**
115
+ * An RFC 9457 problem document, as the API sends it with an error status.
116
+ * The API sends every member but `errors`; each is optional here because a
117
+ * member of the wrong type, such as one from a proxy, is left out rather
118
+ * than failing the whole document.
119
+ */
120
+ export interface Problem {
121
+ /** A URI identifying the problem; it maps one to one onto `code`. */
122
+ type?: string;
123
+ /** The fixed summary for `code`. */
124
+ title?: string;
125
+ /** The response status. */
126
+ status?: number;
127
+ /** Human text about this occurrence. Its wording may change at any time. */
128
+ detail?: string;
129
+ /** The error code. Branch on it. */
130
+ code?: ErrorCode;
131
+ /** The request's id, to quote when reporting the problem. */
132
+ request_id?: string;
133
+ /** Every problem found. Present only with `validation_failed`. */
134
+ errors?: FieldError[];
135
+ }
package/dist/types.js ADDED
@@ -0,0 +1,80 @@
1
+ // The wire types the SDK uses, written by hand from the API's OpenAPI
2
+ // document. A test reads that document and holds every list below to it,
3
+ // until types generated from the document replace this file.
4
+ /** The known request-level error codes, in the API's catalogue order. */
5
+ export const ERROR_CODES = [
6
+ "invalid_json",
7
+ "validation_failed",
8
+ "unauthenticated",
9
+ "insufficient_scope",
10
+ "session_required",
11
+ "account_not_provisioned",
12
+ "not_found",
13
+ "no_active_value_model",
14
+ "method_not_allowed",
15
+ "idempotency_key_in_use",
16
+ "client_key_in_use",
17
+ "agent_key_in_use",
18
+ "client_has_events",
19
+ "client_has_calculations",
20
+ "agent_has_events",
21
+ "client_key_has_events",
22
+ "value_model_exists",
23
+ "value_model_version_stale",
24
+ "calculation_transition_invalid",
25
+ "calculation_not_draft",
26
+ "calculation_not_published",
27
+ "report_shared",
28
+ "report_not_shared",
29
+ "api_key_revoked",
30
+ "payload_too_large",
31
+ "uri_too_long",
32
+ "unsupported_media_type",
33
+ "idempotency_key_reused",
34
+ "rate_limited",
35
+ "internal_error",
36
+ ];
37
+ /** The known field-level error codes, in the API's catalogue order. */
38
+ export const FIELD_ERROR_CODES = [
39
+ "required",
40
+ "unknown_field",
41
+ "immutable",
42
+ "invalid_type",
43
+ "invalid_format",
44
+ "invalid_value",
45
+ "too_long",
46
+ "too_many",
47
+ "out_of_range",
48
+ "too_precise",
49
+ "duplicate",
50
+ "too_large",
51
+ "mutually_exclusive",
52
+ "unknown_reference",
53
+ "client_key_required",
54
+ "currency_mismatch",
55
+ ];
56
+ /** The event categories. */
57
+ export const EVENT_CATEGORIES = [
58
+ "session",
59
+ "task",
60
+ "model",
61
+ "outcome",
62
+ "exception",
63
+ ];
64
+ /** The members of `EventInput`, in the API's order. */
65
+ export const EVENT_INPUT_MEMBERS = [
66
+ "event_id",
67
+ "timestamp",
68
+ "agent_key",
69
+ "client_key",
70
+ "category",
71
+ "event_type",
72
+ "outcome_type",
73
+ "quantity",
74
+ "cost",
75
+ "tokens_in",
76
+ "tokens_out",
77
+ "model",
78
+ "currency",
79
+ "metadata",
80
+ ];
package/dist/uuid.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Returns a new UUID version 4 (RFC 9562 §5.4), lowercase, in the
3
+ * 8-4-4-4-12 form. It uses `crypto.getRandomValues`, which, unlike
4
+ * `crypto.randomUUID`, every browser offers outside secure contexts too.
5
+ */
6
+ export declare function uuidv4(): string;
package/dist/uuid.js ADDED
@@ -0,0 +1,20 @@
1
+ const HEX = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
2
+ /**
3
+ * Returns a new UUID version 4 (RFC 9562 §5.4), lowercase, in the
4
+ * 8-4-4-4-12 form. It uses `crypto.getRandomValues`, which, unlike
5
+ * `crypto.randomUUID`, every browser offers outside secure contexts too.
6
+ */
7
+ export function uuidv4() {
8
+ const bytes = new Uint8Array(16);
9
+ globalThis.crypto.getRandomValues(bytes);
10
+ // Version 4 in the high nibble of byte 6; variant 10 in the top bits of byte 8.
11
+ bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;
12
+ bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
13
+ let out = "";
14
+ for (let i = 0; i < 16; i++) {
15
+ if (i === 4 || i === 6 || i === 8 || i === 10)
16
+ out += "-";
17
+ out += HEX[bytes[i] ?? 0];
18
+ }
19
+ return out;
20
+ }
@@ -0,0 +1,2 @@
1
+ /** The SDK's version. The release tooling updates it; never edit it by hand. */
2
+ export declare const VERSION = "0.1.0";
@@ -0,0 +1,2 @@
1
+ /** The SDK's version. The release tooling updates it; never edit it by hand. */
2
+ export const VERSION = "0.1.0"; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "@allisall/sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "The allisall SDK for TypeScript and JavaScript: send events to the allisall API.",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/All-is-AI/allisall-typescript.git"
9
+ },
10
+ "type": "module",
11
+ "sideEffects": false,
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "types": "./dist/index.d.ts",
19
+ "files": [
20
+ "dist",
21
+ "LICENSE",
22
+ "NOTICE"
23
+ ],
24
+ "engines": {
25
+ "node": ">=22"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.build.json",
29
+ "lint": "biome check . && tsc --noEmit -p tsconfig.json",
30
+ "test": "vitest run && npm run test:consumer",
31
+ "test:consumer": "tsc -p tsconfig.build.json && node test/consumer/extract-readme.mjs && node node_modules/typescript-5.0/bin/tsc -p test/consumer/tsconfig.bundler.json && node node_modules/typescript-5.0/bin/tsc -p test/consumer/tsconfig.node16.json && node node_modules/typescript-5.0/bin/tsc -p test/consumer/tsconfig.node-types.json"
32
+ },
33
+ "devDependencies": {
34
+ "@biomejs/biome": "2.5.4",
35
+ "@types/node": "^22.20.5",
36
+ "typescript": "~5.9.3",
37
+ "typescript-5.0": "npm:typescript@5.0.4",
38
+ "vitest": "^4.1.10",
39
+ "yaml": "2.9.0"
40
+ }
41
+ }