@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/LICENSE +21 -0
- package/README.md +139 -0
- package/dist/_generated/operations.d.ts +8467 -0
- package/dist/_generated/operations.d.ts.map +1 -0
- package/dist/_generated/operations.js +2489 -0
- package/dist/client.d.ts +554 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +404 -0
- package/dist/error.d.ts +76 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +152 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/listeners.d.ts +106 -0
- package/dist/listeners.d.ts.map +1 -0
- package/dist/listeners.js +214 -0
- package/dist/options.d.ts +41 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +11 -0
- package/dist/retry.d.ts +24 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +63 -0
- package/dist/webhooks.d.ts +59 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +149 -0
- package/package.json +51 -0
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;
|
package/dist/error.d.ts
ADDED
|
@@ -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;
|