@exayard/sdk 0.3.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/client.js ADDED
@@ -0,0 +1,404 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Exayard = void 0;
4
+ const operations_1 = require("./_generated/operations");
5
+ const error_1 = require("./error");
6
+ const listeners_1 = require("./listeners");
7
+ const options_1 = require("./options");
8
+ const retry_1 = require("./retry");
9
+ const webhooks_1 = require("./webhooks");
10
+ const UNSAFE_METHODS = new Set(['POST', 'PATCH', 'PUT', 'DELETE']);
11
+ const randomIdempotencyKey = () => {
12
+ // Good enough for SDK auto-retries; callers needing deterministic keys
13
+ // pass their own. Uses crypto.randomUUID in runtimes that have it and
14
+ // falls back to timestamp+random.
15
+ if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
16
+ return `idem_${crypto.randomUUID().replace(/-/g, '')}`;
17
+ }
18
+ return `idem_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
19
+ };
20
+ const envApiKey = () => {
21
+ if (typeof process === 'undefined' || !process.env)
22
+ return undefined;
23
+ const value = process.env.EXAYARD_API_KEY;
24
+ return value && value.trim() !== '' ? value.trim() : undefined;
25
+ };
26
+ const checkEndUser = (value) => {
27
+ if (value === undefined)
28
+ return undefined;
29
+ if (!(0, options_1.isValidEndUser)(value)) {
30
+ throw new TypeError(`Exayard: endUser must be 1 to ${options_1.END_USER_MAX_LENGTH} printable characters.`);
31
+ }
32
+ return value;
33
+ };
34
+ const transportProblem = (code, status, detail, requestId) => ({
35
+ type: `https://developers.exayard.com/concepts/errors#${code}`,
36
+ title: code === 'transport_error' ? 'Transport Error' : code === 'timeout' ? 'Timeout' : 'Connection Error',
37
+ status,
38
+ detail,
39
+ code,
40
+ doc_url: `https://developers.exayard.com/concepts/errors#${code}`,
41
+ ...(requestId ? { request_id: requestId } : {})
42
+ });
43
+ class Exayard {
44
+ credential;
45
+ baseUrl;
46
+ timeoutMs;
47
+ maxRetries;
48
+ fetchImpl;
49
+ defaultOrganizationId;
50
+ defaultEndUser;
51
+ constructor(opts = {}) {
52
+ const credential = opts.apiKey ?? opts.bearerToken ?? envApiKey();
53
+ if (!credential) {
54
+ throw new Error('Exayard: pass `apiKey` (or set EXAYARD_API_KEY) or `bearerToken` — see https://developers.exayard.com/api-reference.');
55
+ }
56
+ this.credential = credential;
57
+ this.baseUrl = (opts.baseUrl ?? 'https://api.exayard.com/v1').replace(/\/$/, '');
58
+ this.timeoutMs = opts.timeoutMs ?? 60_000;
59
+ this.maxRetries = Math.max(0, opts.maxRetries ?? retry_1.DEFAULT_MAX_RETRIES);
60
+ this.fetchImpl = opts.fetch ?? ((input, init) => fetch(input, init));
61
+ this.defaultOrganizationId = opts.organizationId;
62
+ this.defaultEndUser = checkEndUser(opts.endUser);
63
+ }
64
+ /** The company a call names, else the client's default; undefined lets the key imply it. */
65
+ organizationFor(named) {
66
+ return named ?? this.defaultOrganizationId;
67
+ }
68
+ // One attempt: the per-attempt timeout covers the wait for the answer's headers only, so a stream body is never
69
+ // cut by it; the caller's signal aborts both (its listener stays while the body may still be read).
70
+ async attempt(url, init, timeoutMs, signal) {
71
+ const ac = new AbortController();
72
+ let timedOut = false;
73
+ const onAbort = () => ac.abort(signal?.reason);
74
+ if (signal?.aborted)
75
+ ac.abort(signal.reason);
76
+ signal?.addEventListener('abort', onAbort, { once: true });
77
+ const timer = timeoutMs > 0 ? setTimeout(() => ((timedOut = true), ac.abort()), timeoutMs) : undefined;
78
+ try {
79
+ return await this.fetchImpl(url, { ...init, signal: ac.signal });
80
+ }
81
+ catch (err) {
82
+ signal?.removeEventListener('abort', onAbort);
83
+ if (signal?.aborted)
84
+ throw err;
85
+ throw new error_1.APIConnectionError(timedOut
86
+ ? transportProblem('timeout', 0, `No answer within ${timeoutMs} ms.`)
87
+ : transportProblem('connection_error', 0, err?.message || 'The connection failed.'));
88
+ }
89
+ finally {
90
+ if (timer)
91
+ clearTimeout(timer);
92
+ }
93
+ }
94
+ async toError(res) {
95
+ const retryAfter = (0, retry_1.parseRetryAfter)(res.headers.get('Retry-After'));
96
+ const ct = res.headers.get('Content-Type') ?? '';
97
+ if (ct.includes('problem+json') || ct.includes('application/json')) {
98
+ const body = (await res.json().catch(() => null));
99
+ if (body && typeof body === 'object')
100
+ return (0, error_1.errorFromProblem)({ ...body, status: body.status ?? res.status }, retryAfter);
101
+ }
102
+ // Fallback for non-JSON error pages (shouldn't happen against /v1 but
103
+ // could hit a CDN error page if the service is down).
104
+ const text = await res.text().catch(() => '');
105
+ const body = transportProblem('transport_error', res.status, text.slice(0, 500) || `HTTP ${res.status}`, res.headers.get('X-Request-Id') ?? undefined);
106
+ return (0, error_1.errorFromProblem)({ ...body, code: res.status === 429 ? 'rate_limited' : body.code }, retryAfter);
107
+ }
108
+ // Internal request primitive. Public resource methods compose this.
109
+ async request(opts) {
110
+ const call = opts.options ?? {};
111
+ const url = new URL(`${this.baseUrl}${opts.path}`);
112
+ if (opts.query) {
113
+ for (const [k, v] of Object.entries(opts.query)) {
114
+ if (v !== undefined)
115
+ url.searchParams.set(k, String(v));
116
+ }
117
+ }
118
+ const headers = {
119
+ Accept: 'application/json',
120
+ Authorization: `Bearer ${this.credential}`,
121
+ ...opts.headers
122
+ };
123
+ if (opts.body !== undefined)
124
+ headers['Content-Type'] = 'application/json';
125
+ const endUser = checkEndUser(call.endUser) ?? this.defaultEndUser;
126
+ if (endUser !== undefined)
127
+ headers[options_1.END_USER_HEADER] = endUser;
128
+ // Idempotency-Key for unsafe methods, generated once and sent on every retry, so the server runs the call once
129
+ // at most. A caller's own key wins.
130
+ if (call.idempotencyKey)
131
+ headers['Idempotency-Key'] = call.idempotencyKey;
132
+ else if (UNSAFE_METHODS.has(opts.method))
133
+ headers['Idempotency-Key'] = randomIdempotencyKey();
134
+ const init = {
135
+ method: opts.method,
136
+ headers,
137
+ body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined
138
+ };
139
+ const maxRetries = Math.max(0, call.maxRetries ?? this.maxRetries);
140
+ const timeoutMs = call.timeoutMs ?? this.timeoutMs;
141
+ for (let attempt = 0;; attempt++) {
142
+ let res;
143
+ try {
144
+ res = await this.attempt(url.toString(), init, timeoutMs, call.signal);
145
+ }
146
+ catch (err) {
147
+ if (err instanceof error_1.APIConnectionError && attempt < maxRetries) {
148
+ await (0, retry_1.sleep)((0, retry_1.retryDelayMs)(attempt + 1), call.signal);
149
+ continue;
150
+ }
151
+ throw err;
152
+ }
153
+ if (res.ok) {
154
+ if (opts.raw)
155
+ return res;
156
+ if (res.status === 204)
157
+ return undefined;
158
+ const text = await res.text();
159
+ return (text === '' ? undefined : JSON.parse(text));
160
+ }
161
+ if ((0, retry_1.isRetryableStatus)(res.status) && attempt < maxRetries) {
162
+ const retryAfter = (0, retry_1.parseRetryAfter)(res.headers.get('Retry-After'));
163
+ if (retryAfter === undefined || retryAfter <= retry_1.MAX_RETRY_AFTER_SECONDS) {
164
+ await res.body?.cancel().catch(() => undefined);
165
+ await (0, retry_1.sleep)((0, retry_1.retryDelayMs)(attempt + 1, retryAfter), call.signal);
166
+ continue;
167
+ }
168
+ }
169
+ throw await this.toError(res);
170
+ }
171
+ }
172
+ /**
173
+ * Call any API operation by its name: the spec's operationId, which is also
174
+ * its MCP tool name and its CLI command. The input is the operation's path,
175
+ * query and body fields merged flat, the same input its MCP tool takes.
176
+ */
177
+ call(name, input, opts = {}) {
178
+ const op = operations_1.OPERATIONS[name];
179
+ if (!op)
180
+ throw new Error(`Exayard: no operation named ${String(name)}`);
181
+ const args = { ...input };
182
+ const known = new Set(op.params.map(param => param.name));
183
+ const unknown = Object.keys(args).filter(field => !known.has(field));
184
+ if (unknown.length > 0)
185
+ throw new Error(`Exayard: ${name} does not take ${unknown.join(', ')}`);
186
+ if (known.has('organizationId') && args.organizationId === undefined && this.defaultOrganizationId) {
187
+ args.organizationId = this.defaultOrganizationId;
188
+ }
189
+ let path = op.path;
190
+ const query = {};
191
+ const body = {};
192
+ for (const param of op.params) {
193
+ const value = args[param.name];
194
+ if (param.in === 'path') {
195
+ if (value === undefined || value === null || value === '')
196
+ throw new Error(`Exayard: ${name} needs ${param.name}`);
197
+ path = path.replace(`{${param.name}}`, encodeURIComponent(String(value)));
198
+ }
199
+ else if (value !== undefined) {
200
+ if (param.in === 'query')
201
+ query[param.name] = value;
202
+ else
203
+ body[param.name] = value;
204
+ }
205
+ }
206
+ return this.request({
207
+ method: op.method,
208
+ path,
209
+ query,
210
+ body: op.body ? body : undefined,
211
+ raw: op.response === 'stream',
212
+ options: opts
213
+ });
214
+ }
215
+ /**
216
+ * Every API operation as a typed method, generated from the API spec:
217
+ * `exa.api.listVendorQuotes({ id: projectId })`.
218
+ */
219
+ api = Object.fromEntries(Object.entries(operations_1.OPERATION_METHODS).map(([method, name]) => [
220
+ method,
221
+ (input, opts) => this.call(name, input, opts)
222
+ ]));
223
+ // ---------------------------------------------------------------------------
224
+ // Resource surface — a curated layer over the generated operations for the
225
+ // flows agents and integrations most often hit. Each method is a call() of the
226
+ // operation it names, so its input and output types come from the API spec.
227
+ // Everything else is on `exa.api` and `exa.call`. `organizationId` is optional
228
+ // everywhere: the key implies the company, or the client's default fills it.
229
+ // ---------------------------------------------------------------------------
230
+ me = {
231
+ get: () => this.call('get_me', {})
232
+ };
233
+ projects = {
234
+ // GET /v1/projects returns a raw array; there is no { items, next_cursor }
235
+ // envelope and no limit/cursor pagination yet.
236
+ list: (query) => this.call('list_projects', query),
237
+ get: (id, query = {}) => this.call('get_project', { id, ...query }),
238
+ create: (body, opts = {}) => this.call('create_project', body, opts),
239
+ export: (id, query = {}) => this.call('export_project', { id, ...query }),
240
+ archive: (id, query = {}) => this.call('archive_project', { id, ...query })
241
+ };
242
+ // File upload is a 3-step dance: presign (get an R2 upload URL) → PUT the
243
+ // bytes straight to R2 → confirm (which also kicks off PDF page extraction).
244
+ // `upload` wraps all three. A PDF becomes pages asynchronously after confirm —
245
+ // poll pages.list() until processingStatus is 'complete' before proposing/running.
246
+ // The project decides the organization, so none of the three takes one.
247
+ files = {
248
+ presign: (body) => this.call('presign_file_upload', body),
249
+ confirm: (fileId, r2Key) => this.call('confirm_file_upload', { id: fileId, r2Key }),
250
+ // Convenience: presign → PUT bytes to the presigned R2 URL → confirm.
251
+ // The PUT goes directly to R2 (a different host, self-authenticated by the
252
+ // signed URL) — not through the /v1 request primitive.
253
+ upload: async (body) => {
254
+ const buf = body.bytes instanceof ArrayBuffer ? new Uint8Array(body.bytes) : body.bytes;
255
+ const { fileId, uploadUrl, r2Key } = await this.files.presign({
256
+ projectId: body.projectId,
257
+ filename: body.filename,
258
+ mimeType: body.mimeType,
259
+ fileSize: buf.byteLength,
260
+ folderId: body.folderId
261
+ });
262
+ const put = await this.fetchImpl(uploadUrl, {
263
+ method: 'PUT',
264
+ headers: { 'Content-Type': body.mimeType },
265
+ // Node/undici fetch accepts a Uint8Array body at runtime; the DOM
266
+ // BodyInit type doesn't list it, so cast at this boundary.
267
+ body: buf
268
+ });
269
+ if (!put.ok) {
270
+ throw new error_1.ExayardError({
271
+ type: 'https://developers.exayard.com/concepts/errors#transport_error',
272
+ title: 'Upload Failed',
273
+ status: put.status,
274
+ detail: `PUT to storage failed: HTTP ${put.status}`,
275
+ code: 'transport_error',
276
+ doc_url: 'https://developers.exayard.com/concepts/errors#transport_error'
277
+ });
278
+ }
279
+ await this.files.confirm(fileId, r2Key);
280
+ return { fileId };
281
+ }
282
+ };
283
+ // List a project's pages (grouped by file). Use to discover pageIds + poll
284
+ // each file's processingStatus until extraction finishes.
285
+ pages = {
286
+ list: (projectId, query = {}) => this.call('list_pages', { id: projectId, ...query })
287
+ };
288
+ // AI takeoff assessments. Reads resolve the org from the project/assessment
289
+ // (no organizationId). `run` kicks off an AI run on an existing project's
290
+ // pages — pair with the assessment.completed webhook + latest()/takeoffSummary
291
+ // to pull results once it finishes.
292
+ assessments = {
293
+ list: (projectId) => this.call('list_assessments', { id: projectId }),
294
+ latest: (projectId) => this.call('get_latest_assessment', { id: projectId }),
295
+ get: (id) => this.call('get_assessment', { id }),
296
+ takeoffSummary: (projectId) => this.call('get_takeoff_summary', { id: projectId }),
297
+ // Ask the AI what to measure: a natural-language prompt + pageIds → a
298
+ // proposed `elements` array (id/name/category/hexColor) you can pass
299
+ // straight to run(). The bridge for callers who don't already know the
300
+ // elements.
301
+ propose: (projectId, body) => this.call('propose_analysis', { id: projectId, ...body }),
302
+ // Run an AI takeoff analysis on an existing project and let it complete
303
+ // end-to-end (no human approval step). You specify exactly what to detect
304
+ // via `elements` (each: id, name, category area|linear|count, hexColor) on
305
+ // the given pageIds. Returns { assessmentId, pageIds }; track completion via
306
+ // the assessment.completed webhook + latest()/takeoffSummary.
307
+ //
308
+ // (The auto-detect POST /assessments path pauses at awaiting_approval,
309
+ // which needs a human, so it isn't wrapped here; it is exa.api.createAssessment.)
310
+ run: (projectId, body, opts = {}) => this.call('run_approved_analysis', { id: projectId, ...body }, opts)
311
+ };
312
+ // Generate an estimate from a project's takeoff and persist it as a document.
313
+ // Blocking: the estimator typically runs 30–60s, which is at the default
314
+ // 60s client timeout — raise `timeoutMs` when constructing the client.
315
+ //
316
+ // The saved document is PRIVATE to your organization. `share: true` also
317
+ // publishes it on a public link anyone holding the URL can read, and makes
318
+ // `documentUrl` the public path instead of the signed-in one. Set it only
319
+ // when the customer asked for a shareable link; `documents.setSharing` can
320
+ // share or unshare later either way.
321
+ //
322
+ // (The streaming sibling POST /v1/projects/{id}/estimates/generate persists
323
+ // no document and has no `share` flag; it is exa.api.generateEstimate, which
324
+ // returns the event-stream Response.)
325
+ estimates = {
326
+ create: (projectId, body, opts = {}) => this.call('create_estimate', { id: projectId, ...body }, opts)
327
+ };
328
+ // Same estimator wrapped with bid-formatting instructions, persisted as a
329
+ // bid document. Same blocking timing and same private-by-default `share`
330
+ // semantics as estimates.create. When `measurementContext` is omitted the
331
+ // takeoff is auto-loaded server-side, which can 422 on line items with
332
+ // missing formula variables — retry with `ignoreMissingVariables: true` to
333
+ // skip them (the skipped rows come back in `warnings`).
334
+ bids = {
335
+ create: (projectId, body, opts = {}) => this.call('create_bid', { id: projectId, ...body }, opts)
336
+ };
337
+ // Documents (estimates, bids, and anything else saved to a project) plus the
338
+ // sharing switch. Documents are private to the organization until someone
339
+ // enables sharing; enabling mints a secret and publishes a link readable by
340
+ // anyone on the internet who has the URL — no sign-in, no expiry.
341
+ //
342
+ // Disabling revokes access but KEEPS the secret, so re-enabling later revives
343
+ // the same URL. There is no rotate operation: a leaked link means the
344
+ // document is burned, not that a disable/enable cycle gives you a fresh one.
345
+ documents = {
346
+ // Both list endpoints return a raw array (no { items, next_cursor }
347
+ // envelope), like projects.list. They're here because setSharing needs a
348
+ // document id and only documents you just generated hand you one directly.
349
+ list: (query) => this.call('list_org_documents', query),
350
+ listByProject: (projectId, query = {}) => this.call('list_documents', { id: projectId, ...query }),
351
+ // Current sharing state rides the document body: `shareEnabled` and
352
+ // `shareSecret` come back on GET.
353
+ get: (id, query = {}) => this.call('get_document', { id, ...query }),
354
+ // A document id belonging to another organization answers 404 exactly like
355
+ // an id that doesn't exist.
356
+ setSharing: (id, body, opts = {}) => this.call('set_document_sharing', { id, ...body }, opts)
357
+ };
358
+ // Vendor quotes. Request a quote from a vendor for a project, advance it to
359
+ // `received` once the vendor responds (with priced line items), then record a
360
+ // terminal accepted | rejected | expired decision. Each transition fires a
361
+ // quote.* webhook. Note: there is no product UI for vendor quotes yet — these
362
+ // are visible only via this API and the webhooks.
363
+ quotes = {
364
+ list: (projectId, query) => this.call('list_vendor_quotes', { id: projectId, ...query }),
365
+ get: (id, query = {}) => this.call('get_vendor_quote', { id, ...query }),
366
+ create: (projectId, body, opts = {}) => this.call('create_vendor_quote', { id: projectId, ...body }, opts),
367
+ receive: (id, body, opts = {}) => this.call('receive_vendor_quote', { id, ...body }, opts),
368
+ updateStatus: (id, body, opts = {}) => this.call('update_vendor_quote_status', { id, ...body }, opts)
369
+ };
370
+ help = {
371
+ search: (body) => this.call('search_help_articles', body)
372
+ };
373
+ webhooks = {
374
+ listEndpoints: (query = {}) => this.call('list_webhook_endpoints', query),
375
+ createEndpoint: (body) => this.call('create_webhook_endpoint', body),
376
+ deleteEndpoint: (id, query = {}) => this.call('delete_webhook_endpoint', { id, ...query }),
377
+ listDeliveries: (id, query = {}) => this.call('list_webhook_deliveries', { id, ...query }),
378
+ /** Send a test event of one type to an endpoint, signed as usual, its body marked `"test": true`. */
379
+ sendTestEvent: (id, body, opts = {}) => this.request({
380
+ method: 'POST',
381
+ path: `/webhook_endpoints/${encodeURIComponent(id)}/test`,
382
+ body: { ...body, organizationId: this.organizationFor(body.organizationId) },
383
+ options: opts
384
+ }),
385
+ /** Send a delivery again, as a new delivery with the same event id and body. */
386
+ resendDelivery: (deliveryId, body = {}, opts = {}) => this.request({
387
+ method: 'POST',
388
+ path: `/webhook_deliveries/${encodeURIComponent(deliveryId)}/resend`,
389
+ body: { organizationId: this.organizationFor(body.organizationId) },
390
+ options: opts
391
+ }),
392
+ /** Receive the company's events over a stream, without a public endpoint (what `exayard listen` runs on). */
393
+ listeners: new listeners_1.WebhookListeners({
394
+ request: (opts) => this.request(opts),
395
+ organizationId: named => this.organizationFor(named)
396
+ }),
397
+ /**
398
+ * Parse + verify an inbound webhook delivery: `constructEvent(rawBody, signatureHeader, secret)`. Throws
399
+ * WebhookSignatureError on any signature failure — always catch and return 400 to let us retry.
400
+ */
401
+ constructEvent: webhooks_1.constructWebhookEvent
402
+ };
403
+ }
404
+ exports.Exayard = Exayard;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Typed error surface for SDK consumers.
3
+ *
4
+ * Every non-2xx response from /v1 is RFC 9457 problem+json with a stable `code`. The SDK throws the subclass that
5
+ * code (or, for a code it does not know, the HTTP status) belongs to, so callers can `instanceof` a class or branch
6
+ * on `err.code`. Every class extends ExayardError and carries the problem fields (code, param, doc_url, request_id)
7
+ * plus any extension members.
8
+ */
9
+ export interface ExayardErrorBody {
10
+ type: string;
11
+ title: string;
12
+ status: number;
13
+ detail: string;
14
+ instance?: string;
15
+ code: string;
16
+ param?: string;
17
+ doc_url?: string;
18
+ request_id?: string;
19
+ [extension: string]: unknown;
20
+ }
21
+ /** The problem codes the API documents. Any other string can still arrive: branch on it as a string. */
22
+ export type ExayardErrorCode = 'invalid_request' | 'invalid_end_user' | 'company_required' | 'company_mismatch' | 'unauthenticated' | 'api_key_expired' | 'insufficient_credits' | 'subscription_required' | 'upgrade_required' | 'paid_plan_required' | 'project_limit_reached' | 'usage_limit_reached' | 'forbidden' | 'insufficient_scope' | 'app_not_installed' | 'app_suspended' | 'partner_company_limit_reached' | 'not_found' | 'conflict' | 'idempotency_key_reused' | 'rate_limited' | 'internal_error' | 'transport_error' | 'connection_error' | 'timeout';
23
+ export declare class ExayardError extends Error {
24
+ readonly type: string;
25
+ readonly title: string;
26
+ readonly status: number;
27
+ readonly detail: string;
28
+ readonly instance?: string;
29
+ readonly code: ExayardErrorCode | (string & {});
30
+ readonly param?: string;
31
+ readonly docUrl?: string;
32
+ readonly requestId?: string;
33
+ /** Problem extension members, such as `limit` on usage_limit_reached. */
34
+ readonly extensions: Record<string, unknown>;
35
+ constructor(body: ExayardErrorBody);
36
+ isRateLimited(): boolean;
37
+ isUnauthenticated(): boolean;
38
+ isNotFound(): boolean;
39
+ isInsufficientScope(): boolean;
40
+ isIdempotencyConflict(): boolean;
41
+ }
42
+ /** 400: the request is malformed (invalid_request, invalid_end_user, company_required, company_mismatch). */
43
+ export declare class BadRequestError extends ExayardError {
44
+ }
45
+ /** 401: no credential, or one that is unknown, revoked or expired (unauthenticated, api_key_expired). */
46
+ export declare class AuthenticationError extends ExayardError {
47
+ }
48
+ /** 402: the company's plan or AI usage does not cover the call (insufficient_credits, usage_limit_reached, ...). */
49
+ export declare class PaymentRequiredError extends ExayardError {
50
+ }
51
+ /** 403: the credential may not do this (forbidden, insufficient_scope, app_not_installed, app_suspended). */
52
+ export declare class PermissionDeniedError extends ExayardError {
53
+ }
54
+ /** 404: no such record, or one in another company. */
55
+ export declare class NotFoundError extends ExayardError {
56
+ }
57
+ /** 409: a conflict, or an Idempotency-Key reused with a different request (idempotency_key_reused). */
58
+ export declare class ConflictError extends ExayardError {
59
+ }
60
+ /** 422: the request is well formed but cannot be carried out. */
61
+ export declare class UnprocessableEntityError extends ExayardError {
62
+ }
63
+ /** 429: too many requests. `retryAfter` is the server's Retry-After in seconds, when it sent one. */
64
+ export declare class RateLimitError extends ExayardError {
65
+ readonly retryAfter?: number;
66
+ constructor(body: ExayardErrorBody, retryAfter?: number);
67
+ }
68
+ /** 5xx: the API failed. The SDK has already retried it. */
69
+ export declare class InternalServerError extends ExayardError {
70
+ }
71
+ /** The request never got an answer: a dropped connection or a timeout (code connection_error or timeout). */
72
+ export declare class APIConnectionError extends ExayardError {
73
+ }
74
+ /** The typed error for a problem+json body: by its `code`, else by its status. */
75
+ export declare const errorFromProblem: (body: ExayardErrorBody, retryAfter?: number) => ExayardError;
76
+ //# sourceMappingURL=error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAA;CAC7B;AAED,wGAAwG;AACxG,MAAM,MAAM,gBAAgB,GACxB,iBAAiB,GACjB,kBAAkB,GAClB,kBAAkB,GAClB,kBAAkB,GAClB,iBAAiB,GACjB,iBAAiB,GACjB,sBAAsB,GACtB,uBAAuB,GACvB,kBAAkB,GAClB,oBAAoB,GACpB,uBAAuB,GACvB,qBAAqB,GACrB,WAAW,GACX,oBAAoB,GACpB,mBAAmB,GACnB,eAAe,GACf,+BAA+B,GAC/B,WAAW,GACX,UAAU,GACV,wBAAwB,GACxB,cAAc,GACd,gBAAgB,GAChB,iBAAiB,GACjB,kBAAkB,GAClB,SAAS,CAAA;AAIb,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,gBAAgB,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;IAC/C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAE5C,YAAY,IAAI,EAAE,gBAAgB,EAqBjC;IAGD,aAAa,IAAI,OAAO,CAEvB;IAED,iBAAiB,IAAI,OAAO,CAE3B;IAED,UAAU,IAAI,OAAO,CAEpB;IAED,mBAAmB,IAAI,OAAO,CAE7B;IAED,qBAAqB,IAAI,OAAO,CAE/B;CACF;AAED,6GAA6G;AAC7G,qBAAa,eAAgB,SAAQ,YAAY;CAAG;AACpD,yGAAyG;AACzG,qBAAa,mBAAoB,SAAQ,YAAY;CAAG;AACxD,oHAAoH;AACpH,qBAAa,oBAAqB,SAAQ,YAAY;CAAG;AACzD,6GAA6G;AAC7G,qBAAa,qBAAsB,SAAQ,YAAY;CAAG;AAC1D,sDAAsD;AACtD,qBAAa,aAAc,SAAQ,YAAY;CAAG;AAClD,uGAAuG;AACvG,qBAAa,aAAc,SAAQ,YAAY;CAAG;AAClD,iEAAiE;AACjE,qBAAa,wBAAyB,SAAQ,YAAY;CAAG;AAC7D,qGAAqG;AACrG,qBAAa,cAAe,SAAQ,YAAY;IAC9C,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAE5B,YAAY,IAAI,EAAE,gBAAgB,EAAE,UAAU,CAAC,EAAE,MAAM,EAGtD;CACF;AACD,2DAA2D;AAC3D,qBAAa,mBAAoB,SAAQ,YAAY;CAAG;AACxD,6GAA6G;AAC7G,qBAAa,kBAAmB,SAAQ,YAAY;CAAG;AAwCvD,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,SAAU,gBAAgB,eAAe,MAAM,KAAG,YAK9E,CAAA"}
package/dist/error.js ADDED
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ /**
3
+ * Typed error surface for SDK consumers.
4
+ *
5
+ * Every non-2xx response from /v1 is RFC 9457 problem+json with a stable `code`. The SDK throws the subclass that
6
+ * code (or, for a code it does not know, the HTTP status) belongs to, so callers can `instanceof` a class or branch
7
+ * on `err.code`. Every class extends ExayardError and carries the problem fields (code, param, doc_url, request_id)
8
+ * plus any extension members.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.errorFromProblem = exports.APIConnectionError = exports.InternalServerError = exports.RateLimitError = exports.UnprocessableEntityError = exports.ConflictError = exports.NotFoundError = exports.PermissionDeniedError = exports.PaymentRequiredError = exports.AuthenticationError = exports.BadRequestError = exports.ExayardError = void 0;
12
+ const KNOWN_FIELDS = new Set(['type', 'title', 'status', 'detail', 'instance', 'code', 'param', 'doc_url', 'request_id']);
13
+ class ExayardError extends Error {
14
+ type;
15
+ title;
16
+ status;
17
+ detail;
18
+ instance;
19
+ code;
20
+ param;
21
+ docUrl;
22
+ requestId;
23
+ /** Problem extension members, such as `limit` on usage_limit_reached. */
24
+ extensions;
25
+ constructor(body) {
26
+ // Guard against upstream error pages that don't conform to Problem Details —
27
+ // a missing code/title/detail should still produce a useful message rather
28
+ // than the string "undefined (undefined): undefined".
29
+ const title = body.title ?? 'error';
30
+ const status = typeof body.status === 'number' ? body.status : 0;
31
+ const code = body.code ?? `http_${status || 'unknown'}`;
32
+ const detail = body.detail ?? '';
33
+ const message = detail ? `${title} (${code}): ${detail}` : `HTTP ${status || 'unknown'}: ${title}`;
34
+ super(message);
35
+ this.name = new.target.name;
36
+ this.type = body.type ?? 'about:blank';
37
+ this.title = title;
38
+ this.status = status;
39
+ this.detail = detail;
40
+ this.instance = body.instance;
41
+ this.code = code;
42
+ this.param = body.param;
43
+ this.docUrl = body.doc_url;
44
+ this.requestId = body.request_id;
45
+ this.extensions = Object.fromEntries(Object.entries(body).filter(([key]) => !KNOWN_FIELDS.has(key)));
46
+ }
47
+ // Convenience guards for the most common branching points.
48
+ isRateLimited() {
49
+ return this.code === 'rate_limited' || this.status === 429;
50
+ }
51
+ isUnauthenticated() {
52
+ return this.code === 'unauthenticated' || this.code === 'api_key_expired' || this.status === 401;
53
+ }
54
+ isNotFound() {
55
+ return this.code === 'not_found' || this.status === 404;
56
+ }
57
+ isInsufficientScope() {
58
+ return this.code === 'insufficient_scope' || this.status === 403;
59
+ }
60
+ isIdempotencyConflict() {
61
+ return this.code === 'idempotency_key_reused';
62
+ }
63
+ }
64
+ exports.ExayardError = ExayardError;
65
+ /** 400: the request is malformed (invalid_request, invalid_end_user, company_required, company_mismatch). */
66
+ class BadRequestError extends ExayardError {
67
+ }
68
+ exports.BadRequestError = BadRequestError;
69
+ /** 401: no credential, or one that is unknown, revoked or expired (unauthenticated, api_key_expired). */
70
+ class AuthenticationError extends ExayardError {
71
+ }
72
+ exports.AuthenticationError = AuthenticationError;
73
+ /** 402: the company's plan or AI usage does not cover the call (insufficient_credits, usage_limit_reached, ...). */
74
+ class PaymentRequiredError extends ExayardError {
75
+ }
76
+ exports.PaymentRequiredError = PaymentRequiredError;
77
+ /** 403: the credential may not do this (forbidden, insufficient_scope, app_not_installed, app_suspended). */
78
+ class PermissionDeniedError extends ExayardError {
79
+ }
80
+ exports.PermissionDeniedError = PermissionDeniedError;
81
+ /** 404: no such record, or one in another company. */
82
+ class NotFoundError extends ExayardError {
83
+ }
84
+ exports.NotFoundError = NotFoundError;
85
+ /** 409: a conflict, or an Idempotency-Key reused with a different request (idempotency_key_reused). */
86
+ class ConflictError extends ExayardError {
87
+ }
88
+ exports.ConflictError = ConflictError;
89
+ /** 422: the request is well formed but cannot be carried out. */
90
+ class UnprocessableEntityError extends ExayardError {
91
+ }
92
+ exports.UnprocessableEntityError = UnprocessableEntityError;
93
+ /** 429: too many requests. `retryAfter` is the server's Retry-After in seconds, when it sent one. */
94
+ class RateLimitError extends ExayardError {
95
+ retryAfter;
96
+ constructor(body, retryAfter) {
97
+ super(body);
98
+ this.retryAfter = retryAfter;
99
+ }
100
+ }
101
+ exports.RateLimitError = RateLimitError;
102
+ /** 5xx: the API failed. The SDK has already retried it. */
103
+ class InternalServerError extends ExayardError {
104
+ }
105
+ exports.InternalServerError = InternalServerError;
106
+ /** The request never got an answer: a dropped connection or a timeout (code connection_error or timeout). */
107
+ class APIConnectionError extends ExayardError {
108
+ }
109
+ exports.APIConnectionError = APIConnectionError;
110
+ const BY_CODE = {
111
+ invalid_request: BadRequestError,
112
+ invalid_end_user: BadRequestError,
113
+ company_required: BadRequestError,
114
+ company_mismatch: BadRequestError,
115
+ unauthenticated: AuthenticationError,
116
+ api_key_expired: AuthenticationError,
117
+ insufficient_credits: PaymentRequiredError,
118
+ subscription_required: PaymentRequiredError,
119
+ upgrade_required: PaymentRequiredError,
120
+ paid_plan_required: PaymentRequiredError,
121
+ project_limit_reached: PaymentRequiredError,
122
+ usage_limit_reached: PaymentRequiredError,
123
+ forbidden: PermissionDeniedError,
124
+ insufficient_scope: PermissionDeniedError,
125
+ app_not_installed: PermissionDeniedError,
126
+ app_suspended: PermissionDeniedError,
127
+ partner_company_limit_reached: PermissionDeniedError,
128
+ not_found: NotFoundError,
129
+ conflict: ConflictError,
130
+ idempotency_key_reused: ConflictError,
131
+ rate_limited: RateLimitError,
132
+ internal_error: InternalServerError
133
+ };
134
+ const BY_STATUS = {
135
+ 400: BadRequestError,
136
+ 401: AuthenticationError,
137
+ 402: PaymentRequiredError,
138
+ 403: PermissionDeniedError,
139
+ 404: NotFoundError,
140
+ 409: ConflictError,
141
+ 422: UnprocessableEntityError,
142
+ 429: RateLimitError
143
+ };
144
+ /** The typed error for a problem+json body: by its `code`, else by its status. */
145
+ const errorFromProblem = (body, retryAfter) => {
146
+ const status = typeof body.status === 'number' ? body.status : 0;
147
+ const cls = BY_CODE[body.code] ?? BY_STATUS[status] ?? (status >= 500 ? InternalServerError : ExayardError);
148
+ if (cls === RateLimitError)
149
+ return new RateLimitError(body, retryAfter);
150
+ return new cls(body);
151
+ };
152
+ exports.errorFromProblem = errorFromProblem;