@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.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @exayard/sdk — official TypeScript SDK for the Exayard API.
3
+ *
4
+ * Every API operation is generated from the API spec (apps/api/openapi.json)
5
+ * into src/_generated/ by scripts/generate.mts, and callable by name or as a
6
+ * typed method. The resource groups (exa.projects, exa.files, ...) are a
7
+ * hand-written layer over the same calls, with helpers such as files.upload.
8
+ *
9
+ * Usage:
10
+ *
11
+ * import { Exayard } from '@exayard/sdk'
12
+ * const exa = new Exayard({ apiKey: process.env.EXAYARD_API_KEY, endUser: 'customer-42' })
13
+ * const projects = await exa.projects.list({})
14
+ * const quotes = await exa.api.listVendorQuotes({ id: projectId })
15
+ * const same = await exa.call('list_vendor_quotes', { id: projectId })
16
+ *
17
+ * Design notes:
18
+ * - Keys: `exa_live_` / `exa_test_` app keys and the older `ak_` keys and `xa_app_` secrets all work.
19
+ * - `organizationId` is optional on every call: the key implies the company, or the client's default fills it.
20
+ * - `endUser` (client or per call) is sent as the Exayard-End-User header and recorded on the call's usage.
21
+ * - 429, 5xx and dropped connections are retried (2 retries by default), honouring Retry-After; every
22
+ * POST/PATCH/PUT/DELETE carries one Idempotency-Key that every retry reuses.
23
+ * - Errors are typed by the problem+json `code` (RateLimitError, AuthenticationError, ...), all ExayardError.
24
+ * - Webhook verification: `exa.webhooks.constructEvent(rawBody, signatureHeader, secret)`.
25
+ */
26
+ export { Exayard } from './client';
27
+ export type { ExayardOptions, VendorQuoteStatus, VendorQuoteLineItem, DocumentShare, BidWarning, GeneratedDocument } from './client';
28
+ export { OPERATIONS } from './_generated/operations';
29
+ export type { OperationInput, OperationMethods, OperationName, OperationOutput, OperationParam, OperationSpec } from './_generated/operations';
30
+ export type { CallOptions, WithOptionalOrganization } from './options';
31
+ export { END_USER_HEADER, END_USER_MAX_LENGTH, isValidEndUser } from './options';
32
+ export { APIConnectionError, AuthenticationError, BadRequestError, ConflictError, errorFromProblem, ExayardError, InternalServerError, NotFoundError, PaymentRequiredError, PermissionDeniedError, RateLimitError, UnprocessableEntityError } from './error';
33
+ export type { ExayardErrorBody, ExayardErrorCode } from './error';
34
+ export { constructWebhookEvent, signWebhookPayload, WebhookSignatureError, WEBHOOK_EVENT_TYPES, WEBHOOK_SIGNATURE_HEADER, DEFAULT_WEBHOOK_TOLERANCE_SECONDS } from './webhooks';
35
+ export type { WebhookEvent, WebhookEventType } from './webhooks';
36
+ export { parseServerSentEvents, WebhookListeners } from './listeners';
37
+ export type { ListenerEvent, ListenerEventsOptions, ServerSentEvent, WebhookListener } from './listeners';
38
+ export { DEFAULT_MAX_RETRIES, parseRetryAfter } from './retry';
39
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAClC,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,mBAAmB,EACnB,aAAa,EACb,UAAU,EACV,iBAAiB,EAClB,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AACpD,YAAY,EACV,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,eAAe,EACf,cAAc,EACd,aAAa,EACd,MAAM,yBAAyB,CAAA;AAChC,YAAY,EAAE,WAAW,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAA;AACtE,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAChF,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,YAAY,EACZ,mBAAmB,EACnB,aAAa,EACb,oBAAoB,EACpB,qBAAqB,EACrB,cAAc,EACd,wBAAwB,EACzB,MAAM,SAAS,CAAA;AAChB,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AACjE,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,wBAAwB,EACxB,iCAAiC,EAClC,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAChE,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AACrE,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AACzG,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ /**
3
+ * @exayard/sdk — official TypeScript SDK for the Exayard API.
4
+ *
5
+ * Every API operation is generated from the API spec (apps/api/openapi.json)
6
+ * into src/_generated/ by scripts/generate.mts, and callable by name or as a
7
+ * typed method. The resource groups (exa.projects, exa.files, ...) are a
8
+ * hand-written layer over the same calls, with helpers such as files.upload.
9
+ *
10
+ * Usage:
11
+ *
12
+ * import { Exayard } from '@exayard/sdk'
13
+ * const exa = new Exayard({ apiKey: process.env.EXAYARD_API_KEY, endUser: 'customer-42' })
14
+ * const projects = await exa.projects.list({})
15
+ * const quotes = await exa.api.listVendorQuotes({ id: projectId })
16
+ * const same = await exa.call('list_vendor_quotes', { id: projectId })
17
+ *
18
+ * Design notes:
19
+ * - Keys: `exa_live_` / `exa_test_` app keys and the older `ak_` keys and `xa_app_` secrets all work.
20
+ * - `organizationId` is optional on every call: the key implies the company, or the client's default fills it.
21
+ * - `endUser` (client or per call) is sent as the Exayard-End-User header and recorded on the call's usage.
22
+ * - 429, 5xx and dropped connections are retried (2 retries by default), honouring Retry-After; every
23
+ * POST/PATCH/PUT/DELETE carries one Idempotency-Key that every retry reuses.
24
+ * - Errors are typed by the problem+json `code` (RateLimitError, AuthenticationError, ...), all ExayardError.
25
+ * - Webhook verification: `exa.webhooks.constructEvent(rawBody, signatureHeader, secret)`.
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.parseRetryAfter = exports.DEFAULT_MAX_RETRIES = exports.WebhookListeners = exports.parseServerSentEvents = exports.DEFAULT_WEBHOOK_TOLERANCE_SECONDS = exports.WEBHOOK_SIGNATURE_HEADER = exports.WEBHOOK_EVENT_TYPES = exports.WebhookSignatureError = exports.signWebhookPayload = exports.constructWebhookEvent = exports.UnprocessableEntityError = exports.RateLimitError = exports.PermissionDeniedError = exports.PaymentRequiredError = exports.NotFoundError = exports.InternalServerError = exports.ExayardError = exports.errorFromProblem = exports.ConflictError = exports.BadRequestError = exports.AuthenticationError = exports.APIConnectionError = exports.isValidEndUser = exports.END_USER_MAX_LENGTH = exports.END_USER_HEADER = exports.OPERATIONS = exports.Exayard = void 0;
29
+ var client_1 = require("./client");
30
+ Object.defineProperty(exports, "Exayard", { enumerable: true, get: function () { return client_1.Exayard; } });
31
+ var operations_1 = require("./_generated/operations");
32
+ Object.defineProperty(exports, "OPERATIONS", { enumerable: true, get: function () { return operations_1.OPERATIONS; } });
33
+ var options_1 = require("./options");
34
+ Object.defineProperty(exports, "END_USER_HEADER", { enumerable: true, get: function () { return options_1.END_USER_HEADER; } });
35
+ Object.defineProperty(exports, "END_USER_MAX_LENGTH", { enumerable: true, get: function () { return options_1.END_USER_MAX_LENGTH; } });
36
+ Object.defineProperty(exports, "isValidEndUser", { enumerable: true, get: function () { return options_1.isValidEndUser; } });
37
+ var error_1 = require("./error");
38
+ Object.defineProperty(exports, "APIConnectionError", { enumerable: true, get: function () { return error_1.APIConnectionError; } });
39
+ Object.defineProperty(exports, "AuthenticationError", { enumerable: true, get: function () { return error_1.AuthenticationError; } });
40
+ Object.defineProperty(exports, "BadRequestError", { enumerable: true, get: function () { return error_1.BadRequestError; } });
41
+ Object.defineProperty(exports, "ConflictError", { enumerable: true, get: function () { return error_1.ConflictError; } });
42
+ Object.defineProperty(exports, "errorFromProblem", { enumerable: true, get: function () { return error_1.errorFromProblem; } });
43
+ Object.defineProperty(exports, "ExayardError", { enumerable: true, get: function () { return error_1.ExayardError; } });
44
+ Object.defineProperty(exports, "InternalServerError", { enumerable: true, get: function () { return error_1.InternalServerError; } });
45
+ Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: function () { return error_1.NotFoundError; } });
46
+ Object.defineProperty(exports, "PaymentRequiredError", { enumerable: true, get: function () { return error_1.PaymentRequiredError; } });
47
+ Object.defineProperty(exports, "PermissionDeniedError", { enumerable: true, get: function () { return error_1.PermissionDeniedError; } });
48
+ Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return error_1.RateLimitError; } });
49
+ Object.defineProperty(exports, "UnprocessableEntityError", { enumerable: true, get: function () { return error_1.UnprocessableEntityError; } });
50
+ var webhooks_1 = require("./webhooks");
51
+ Object.defineProperty(exports, "constructWebhookEvent", { enumerable: true, get: function () { return webhooks_1.constructWebhookEvent; } });
52
+ Object.defineProperty(exports, "signWebhookPayload", { enumerable: true, get: function () { return webhooks_1.signWebhookPayload; } });
53
+ Object.defineProperty(exports, "WebhookSignatureError", { enumerable: true, get: function () { return webhooks_1.WebhookSignatureError; } });
54
+ Object.defineProperty(exports, "WEBHOOK_EVENT_TYPES", { enumerable: true, get: function () { return webhooks_1.WEBHOOK_EVENT_TYPES; } });
55
+ Object.defineProperty(exports, "WEBHOOK_SIGNATURE_HEADER", { enumerable: true, get: function () { return webhooks_1.WEBHOOK_SIGNATURE_HEADER; } });
56
+ Object.defineProperty(exports, "DEFAULT_WEBHOOK_TOLERANCE_SECONDS", { enumerable: true, get: function () { return webhooks_1.DEFAULT_WEBHOOK_TOLERANCE_SECONDS; } });
57
+ var listeners_1 = require("./listeners");
58
+ Object.defineProperty(exports, "parseServerSentEvents", { enumerable: true, get: function () { return listeners_1.parseServerSentEvents; } });
59
+ Object.defineProperty(exports, "WebhookListeners", { enumerable: true, get: function () { return listeners_1.WebhookListeners; } });
60
+ var retry_1 = require("./retry");
61
+ Object.defineProperty(exports, "DEFAULT_MAX_RETRIES", { enumerable: true, get: function () { return retry_1.DEFAULT_MAX_RETRIES; } });
62
+ Object.defineProperty(exports, "parseRetryAfter", { enumerable: true, get: function () { return retry_1.parseRetryAfter; } });
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Webhook listeners: receive the company's webhook events over a server-sent event stream instead of at a public
3
+ * endpoint. This is what `exayard listen` runs on; the server protocol is agents/lab/developer-platform/W/LISTEN.md
4
+ * (routes in apps/api/routes/v1/webhook-handlers.ts).
5
+ *
6
+ * const listener = await exa.webhooks.listeners.start({ events: ['bid.won'], name: 'my-laptop' })
7
+ * for await (const event of exa.webhooks.listeners.events(listener.id)) {
8
+ * // event.payload is the exact body a webhook endpoint would receive; sign it with listener.secret to forward it
9
+ * }
10
+ * await exa.webhooks.listeners.stop(listener.id)
11
+ *
12
+ * The server closes each stream after about two minutes with `event: reconnect`; `events()` reconnects at once with
13
+ * Last-Event-ID, so nothing is missed. A stream that ends any other way is reopened after the server's `retry:`
14
+ * interval. A stopped or swept listener answers 404, which `events()` throws as NotFoundError.
15
+ */
16
+ import type { CallOptions } from './options';
17
+ import type { WebhookEventType } from './webhooks';
18
+ export interface WebhookListener {
19
+ id: string;
20
+ /** The whsec_ secret forwarded events are signed with. Returned only by start; keep it in memory. */
21
+ secret: string;
22
+ events: string[];
23
+ name?: string;
24
+ }
25
+ /** One event on a listener's stream. */
26
+ export interface ListenerEvent {
27
+ /** The listener's own sequence number, 1, 2, 3, ...; resume after it with `after`. */
28
+ seq: number;
29
+ eventId: string;
30
+ type: WebhookEventType;
31
+ test: boolean;
32
+ createdAt: number;
33
+ /** The exact JSON body a webhook endpoint would receive. Forward these bytes unchanged and sign exactly them. */
34
+ payload: string;
35
+ }
36
+ /** One parsed server-sent event (the fields of the HTML event-stream format). */
37
+ export interface ServerSentEvent {
38
+ id?: string;
39
+ event?: string;
40
+ data: string;
41
+ retry?: number;
42
+ }
43
+ /**
44
+ * Parse a text/event-stream body into its events. Comment lines (`: keepalive`) are skipped; an event with no
45
+ * data and no `retry:` is skipped too, while a lone `retry:` is passed on so the caller can read the interval.
46
+ */
47
+ export declare function parseServerSentEvents(body: ReadableStream<Uint8Array>): AsyncGenerator<ServerSentEvent>;
48
+ /** What the listener API needs from the client: its request primitive. */
49
+ export interface ListenerTransport {
50
+ request<R>(opts: {
51
+ method: 'GET' | 'POST' | 'DELETE';
52
+ path: string;
53
+ query?: Record<string, string | undefined>;
54
+ body?: unknown;
55
+ headers?: Record<string, string>;
56
+ raw?: boolean;
57
+ options?: CallOptions;
58
+ }): Promise<R>;
59
+ organizationId(named?: string): string | undefined;
60
+ }
61
+ export interface ListenerEventsOptions {
62
+ /** Resume after this sequence number (Last-Event-ID). Unset: from the start of what the listener holds. */
63
+ after?: number;
64
+ organizationId?: string;
65
+ /** Stops the stream. */
66
+ signal?: AbortSignal;
67
+ /** Reconnects in a row that may fail (no answer, or a 5xx after the SDK's own retries) before it gives up. Default 10. */
68
+ maxReconnects?: number;
69
+ /** Called each time the stream is reopened, with the sequence number it resumes after. */
70
+ onReconnect?: (after: number | undefined) => void;
71
+ }
72
+ export declare class WebhookListeners {
73
+ private readonly transport;
74
+ constructor(transport: ListenerTransport);
75
+ /** Start a listener. `events` defaults to every event (`["*"]`). Needs `write:webhooks`. */
76
+ start(body?: {
77
+ events?: string[];
78
+ name?: string;
79
+ organizationId?: string;
80
+ }, opts?: CallOptions): Promise<WebhookListener>;
81
+ /** Send a test event of a concrete type to the listener; it arrives on the stream with `test: true`. */
82
+ sendTest(id: string, body: {
83
+ type: WebhookEventType;
84
+ organizationId?: string;
85
+ }, opts?: CallOptions): Promise<{
86
+ eventId: string;
87
+ seq: number;
88
+ }>;
89
+ /** Stop the listener and delete its events. */
90
+ stop(id: string, query?: {
91
+ organizationId?: string;
92
+ }, opts?: CallOptions): Promise<void>;
93
+ /** Open the stream once and return the raw Response (text/event-stream). Most callers want `events()`. */
94
+ open(id: string, opts?: {
95
+ after?: number;
96
+ organizationId?: string;
97
+ signal?: AbortSignal;
98
+ }): Promise<Response>;
99
+ /**
100
+ * The listener's events, in order, for as long as the listener lives: reconnects on `event: reconnect` and on a
101
+ * dropped stream, resuming after the last event it yielded. Ends when `signal` aborts. Throws NotFoundError when
102
+ * the listener was stopped or swept, and the last error after `maxReconnects` failed reconnects in a row.
103
+ */
104
+ events(id: string, opts?: ListenerEventsOptions): AsyncGenerator<ListenerEvent>;
105
+ }
106
+ //# sourceMappingURL=listeners.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listeners.d.ts","sourceRoot":"","sources":["../src/listeners.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ /**
3
+ * Webhook listeners: receive the company's webhook events over a server-sent event stream instead of at a public
4
+ * endpoint. This is what `exayard listen` runs on; the server protocol is agents/lab/developer-platform/W/LISTEN.md
5
+ * (routes in apps/api/routes/v1/webhook-handlers.ts).
6
+ *
7
+ * const listener = await exa.webhooks.listeners.start({ events: ['bid.won'], name: 'my-laptop' })
8
+ * for await (const event of exa.webhooks.listeners.events(listener.id)) {
9
+ * // event.payload is the exact body a webhook endpoint would receive; sign it with listener.secret to forward it
10
+ * }
11
+ * await exa.webhooks.listeners.stop(listener.id)
12
+ *
13
+ * The server closes each stream after about two minutes with `event: reconnect`; `events()` reconnects at once with
14
+ * Last-Event-ID, so nothing is missed. A stream that ends any other way is reopened after the server's `retry:`
15
+ * interval. A stopped or swept listener answers 404, which `events()` throws as NotFoundError.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.WebhookListeners = void 0;
19
+ exports.parseServerSentEvents = parseServerSentEvents;
20
+ const error_1 = require("./error");
21
+ const retry_1 = require("./retry");
22
+ /**
23
+ * Parse a text/event-stream body into its events. Comment lines (`: keepalive`) are skipped; an event with no
24
+ * data and no `retry:` is skipped too, while a lone `retry:` is passed on so the caller can read the interval.
25
+ */
26
+ async function* parseServerSentEvents(body) {
27
+ const reader = body.getReader();
28
+ const decoder = new TextDecoder();
29
+ let buffer = '';
30
+ let current = { data: [] };
31
+ const flush = () => {
32
+ const out = current;
33
+ current = { data: [] };
34
+ if (out.data.length === 0 && out.retry === undefined && out.event === undefined)
35
+ return null;
36
+ return {
37
+ ...(out.id !== undefined ? { id: out.id } : {}),
38
+ ...(out.event !== undefined ? { event: out.event } : {}),
39
+ data: out.data.join('\n'),
40
+ ...(out.retry !== undefined ? { retry: out.retry } : {})
41
+ };
42
+ };
43
+ const take = function* (line) {
44
+ if (line === '') {
45
+ const event = flush();
46
+ if (event)
47
+ yield event;
48
+ return;
49
+ }
50
+ if (line.startsWith(':'))
51
+ return;
52
+ const colon = line.indexOf(':');
53
+ const field = colon === -1 ? line : line.slice(0, colon);
54
+ let value = colon === -1 ? '' : line.slice(colon + 1);
55
+ if (value.startsWith(' '))
56
+ value = value.slice(1);
57
+ if (field === 'data')
58
+ current.data.push(value);
59
+ else if (field === 'event')
60
+ current.event = value;
61
+ else if (field === 'id')
62
+ current.id = value;
63
+ else if (field === 'retry' && /^\d+$/.test(value))
64
+ current.retry = Number(value);
65
+ };
66
+ try {
67
+ for (;;) {
68
+ const { done, value } = await reader.read();
69
+ if (done)
70
+ break;
71
+ buffer += decoder.decode(value, { stream: true });
72
+ let newline = buffer.search(/\r\n|\r|\n/);
73
+ while (newline !== -1) {
74
+ const line = buffer.slice(0, newline);
75
+ const width = buffer.startsWith('\r\n', newline) ? 2 : 1;
76
+ // A lone \r at the very end may be the first half of \r\n: wait for the next chunk.
77
+ if (width === 1 && buffer[newline] === '\r' && newline === buffer.length - 1)
78
+ break;
79
+ buffer = buffer.slice(newline + width);
80
+ yield* take(line);
81
+ newline = buffer.search(/\r\n|\r|\n/);
82
+ }
83
+ }
84
+ buffer += decoder.decode();
85
+ if (buffer !== '')
86
+ yield* take(buffer);
87
+ // A final event without its blank line is discarded, as the format says.
88
+ }
89
+ finally {
90
+ reader.releaseLock();
91
+ }
92
+ }
93
+ const DEFAULT_RETRY_MS = 1_000;
94
+ const DEFAULT_MAX_RECONNECTS = 10;
95
+ class WebhookListeners {
96
+ transport;
97
+ constructor(transport) {
98
+ this.transport = transport;
99
+ }
100
+ /** Start a listener. `events` defaults to every event (`["*"]`). Needs `write:webhooks`. */
101
+ start(body = {}, opts = {}) {
102
+ return this.transport.request({
103
+ method: 'POST',
104
+ path: '/webhook_listeners',
105
+ body: { ...body, organizationId: this.transport.organizationId(body.organizationId) },
106
+ options: opts
107
+ });
108
+ }
109
+ /** Send a test event of a concrete type to the listener; it arrives on the stream with `test: true`. */
110
+ sendTest(id, body, opts = {}) {
111
+ return this.transport.request({
112
+ method: 'POST',
113
+ path: `/webhook_listeners/${encodeURIComponent(id)}/test`,
114
+ body: { ...body, organizationId: this.transport.organizationId(body.organizationId) },
115
+ options: opts
116
+ });
117
+ }
118
+ /** Stop the listener and delete its events. */
119
+ async stop(id, query = {}, opts = {}) {
120
+ await this.transport.request({
121
+ method: 'DELETE',
122
+ path: `/webhook_listeners/${encodeURIComponent(id)}`,
123
+ query: { organizationId: this.transport.organizationId(query.organizationId) },
124
+ options: opts
125
+ });
126
+ }
127
+ /** Open the stream once and return the raw Response (text/event-stream). Most callers want `events()`. */
128
+ open(id, opts = {}) {
129
+ return this.transport.request({
130
+ method: 'GET',
131
+ path: `/webhook_listeners/${encodeURIComponent(id)}/events`,
132
+ query: { organizationId: this.transport.organizationId(opts.organizationId) },
133
+ headers: {
134
+ Accept: 'text/event-stream',
135
+ ...(opts.after !== undefined ? { 'Last-Event-ID': String(opts.after) } : {})
136
+ },
137
+ raw: true,
138
+ // The stream stays open for minutes: no per-attempt timeout once the answer has started.
139
+ options: { signal: opts.signal, timeoutMs: 30_000 }
140
+ });
141
+ }
142
+ /**
143
+ * The listener's events, in order, for as long as the listener lives: reconnects on `event: reconnect` and on a
144
+ * dropped stream, resuming after the last event it yielded. Ends when `signal` aborts. Throws NotFoundError when
145
+ * the listener was stopped or swept, and the last error after `maxReconnects` failed reconnects in a row.
146
+ */
147
+ async *events(id, opts = {}) {
148
+ let after = opts.after;
149
+ let retryMs = DEFAULT_RETRY_MS;
150
+ let failures = 0;
151
+ let first = true;
152
+ const maxReconnects = opts.maxReconnects ?? DEFAULT_MAX_RECONNECTS;
153
+ while (!opts.signal?.aborted) {
154
+ if (!first)
155
+ opts.onReconnect?.(after);
156
+ first = false;
157
+ let res;
158
+ try {
159
+ res = await this.open(id, { after, organizationId: opts.organizationId, signal: opts.signal });
160
+ }
161
+ catch (err) {
162
+ if (opts.signal?.aborted)
163
+ return;
164
+ if (err instanceof error_1.NotFoundError || (err instanceof error_1.ExayardError && err.status >= 400 && err.status < 500 && err.status !== 429))
165
+ throw err;
166
+ failures += 1;
167
+ if (failures > maxReconnects)
168
+ throw err;
169
+ await sleepUnlessAborted(Math.max(retryMs, (0, retry_1.retryDelayMs)(failures)), opts.signal);
170
+ continue;
171
+ }
172
+ if (!res.body)
173
+ throw new Error('Exayard: the listener stream answered with no body.');
174
+ failures = 0;
175
+ let reconnectAtOnce = false;
176
+ try {
177
+ for await (const message of parseServerSentEvents(res.body)) {
178
+ if (message.retry !== undefined)
179
+ retryMs = message.retry;
180
+ if (message.event === 'reconnect') {
181
+ reconnectAtOnce = true;
182
+ break;
183
+ }
184
+ if (message.event !== 'webhook' || message.data === '')
185
+ continue;
186
+ const event = JSON.parse(message.data);
187
+ after = event.seq;
188
+ yield event;
189
+ }
190
+ }
191
+ catch (err) {
192
+ if (opts.signal?.aborted)
193
+ return;
194
+ if (err instanceof SyntaxError)
195
+ throw err;
196
+ // A read that failed mid-stream: reopen it like any dropped stream.
197
+ }
198
+ finally {
199
+ await res.body.cancel().catch(() => undefined);
200
+ }
201
+ if (!reconnectAtOnce)
202
+ await sleepUnlessAborted(retryMs, opts.signal);
203
+ }
204
+ }
205
+ }
206
+ exports.WebhookListeners = WebhookListeners;
207
+ const sleepUnlessAborted = async (ms, signal) => {
208
+ try {
209
+ await (0, retry_1.sleep)(ms, signal);
210
+ }
211
+ catch {
212
+ // Aborted: the loop condition ends the stream.
213
+ }
214
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Per-request options, the last argument of every SDK call.
3
+ *
4
+ * Hand-written (the generated operation table imports it), so a spec
5
+ * regeneration never changes what a call accepts beyond its input.
6
+ */
7
+ export interface CallOptions {
8
+ /**
9
+ * Sent as Idempotency-Key. A POST, PATCH, PUT or DELETE gets one generated when this is unset, and every retry of
10
+ * that call reuses it, so a retried call never runs twice.
11
+ */
12
+ idempotencyKey?: string;
13
+ /**
14
+ * Your own id for the person or account this call is for, sent as the `Exayard-End-User` header. It is recorded on
15
+ * the call's usage, so the Usage screen and the usage API can break usage down by end user. 1 to 128 printable
16
+ * characters; an opaque id, never an email address. Overrides the client's `endUser` for this call.
17
+ */
18
+ endUser?: string;
19
+ /** Retries after a 429, a 5xx or a dropped connection. Overrides the client's `maxRetries` for this call. */
20
+ maxRetries?: number;
21
+ /** Timeout for each attempt, in milliseconds. Overrides the client's `timeoutMs` for this call. */
22
+ timeoutMs?: number;
23
+ /** Cancels the call, including any retry wait. */
24
+ signal?: AbortSignal;
25
+ }
26
+ /** The request header that names the end user (the server's END_USER_HEADER). */
27
+ export declare const END_USER_HEADER = "Exayard-End-User";
28
+ /** The longest end user id the API accepts, in characters. */
29
+ export declare const END_USER_MAX_LENGTH = 128;
30
+ /** Whether the API accepts this end user id: printable ASCII, space included, 1 to 128 characters. */
31
+ export declare const isValidEndUser: (value: string) => boolean;
32
+ /**
33
+ * The company field is optional on every call: a company key, or an `exa_` key bound to its own company, implies
34
+ * the company, and the client's `organizationId` fills it otherwise. A personal key or sign-in still names one.
35
+ */
36
+ export type WithOptionalOrganization<T> = T extends {
37
+ organizationId: infer O;
38
+ } ? Omit<T, 'organizationId'> & {
39
+ organizationId?: O;
40
+ } : T;
41
+ //# sourceMappingURL=options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,6GAA6G;IAC7G,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mGAAmG;IACnG,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,kDAAkD;IAClD,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB;AAED,iFAAiF;AACjF,eAAO,MAAM,eAAe,qBAAqB,CAAA;AAEjD,8DAA8D;AAC9D,eAAO,MAAM,mBAAmB,MAAM,CAAA;AAItC,sGAAsG;AACtG,eAAO,MAAM,cAAc,UAAW,MAAM,KAAG,OAAuC,CAAA;AAEtF;;;GAGG;AACH,MAAM,MAAM,wBAAwB,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,cAAc,EAAE,MAAM,CAAC,CAAA;CAAE,GAC3E,IAAI,CAAC,CAAC,EAAE,gBAAgB,CAAC,GAAG;IAAE,cAAc,CAAC,EAAE,CAAC,CAAA;CAAE,GAClD,CAAC,CAAA"}
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isValidEndUser = exports.END_USER_MAX_LENGTH = exports.END_USER_HEADER = void 0;
4
+ /** The request header that names the end user (the server's END_USER_HEADER). */
5
+ exports.END_USER_HEADER = 'Exayard-End-User';
6
+ /** The longest end user id the API accepts, in characters. */
7
+ exports.END_USER_MAX_LENGTH = 128;
8
+ const END_USER_PATTERN = new RegExp(`^[\\x20-\\x7E]{1,${exports.END_USER_MAX_LENGTH}}$`);
9
+ /** Whether the API accepts this end user id: printable ASCII, space included, 1 to 128 characters. */
10
+ const isValidEndUser = (value) => END_USER_PATTERN.test(value);
11
+ exports.isValidEndUser = isValidEndUser;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * When the SDK retries a call, and how long it waits first.
3
+ *
4
+ * Retried: 429, every 5xx, and a request that got no answer (dropped connection, per-attempt timeout). Every POST,
5
+ * PATCH, PUT and DELETE carries an Idempotency-Key that each retry reuses, so the server runs the call once at most;
6
+ * the server never stores a 5xx under the key, so a retry after one is a real second attempt.
7
+ */
8
+ /** Retries after the first attempt, unless the client or the call says otherwise. */
9
+ export declare const DEFAULT_MAX_RETRIES = 2;
10
+ /** The longest wait the SDK honours from Retry-After; a longer one is answered as the error itself. */
11
+ export declare const MAX_RETRY_AFTER_SECONDS = 60;
12
+ export declare const isRetryableStatus: (status: number) => boolean;
13
+ /**
14
+ * Retry-After in seconds: delta-seconds or an HTTP date (RFC 9110 10.2.3). Undefined when absent or unreadable.
15
+ */
16
+ export declare const parseRetryAfter: (value: string | null | undefined, now?: number) => number | undefined;
17
+ /**
18
+ * The wait before retry number `attempt` (1 for the first retry): Retry-After when the server sent one, else
19
+ * exponential backoff from 0.5 s, capped at 8 s, with up to 25% jitter taken off.
20
+ */
21
+ export declare const retryDelayMs: (attempt: number, retryAfterSeconds?: number, random?: () => number) => number;
22
+ /** Resolves after `ms`, or rejects at once when `signal` aborts. */
23
+ export declare const sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
24
+ //# sourceMappingURL=retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../src/retry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,qFAAqF;AACrF,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAEpC,uGAAuG;AACvG,eAAO,MAAM,uBAAuB,KAAK,CAAA;AAKzC,eAAO,MAAM,iBAAiB,WAAY,MAAM,KAAG,OAA0C,CAAA;AAE7F;;GAEG;AACH,eAAO,MAAM,eAAe,UAAW,MAAM,GAAG,IAAI,GAAG,SAAS,QAAO,MAAM,KAAgB,MAAM,GAAG,SAQrG,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,YAAY,YAAa,MAAM,sBAAsB,MAAM,WAAU,MAAM,MAAM,KAAiB,MAI9G,CAAA;AAED,oEAAoE;AACpE,eAAO,MAAM,KAAK,OAAQ,MAAM,WAAW,WAAW,KAAG,OAAO,CAAC,IAAI,CAejE,CAAA"}
package/dist/retry.js ADDED
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ /**
3
+ * When the SDK retries a call, and how long it waits first.
4
+ *
5
+ * Retried: 429, every 5xx, and a request that got no answer (dropped connection, per-attempt timeout). Every POST,
6
+ * PATCH, PUT and DELETE carries an Idempotency-Key that each retry reuses, so the server runs the call once at most;
7
+ * the server never stores a 5xx under the key, so a retry after one is a real second attempt.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.sleep = exports.retryDelayMs = exports.parseRetryAfter = exports.isRetryableStatus = exports.MAX_RETRY_AFTER_SECONDS = exports.DEFAULT_MAX_RETRIES = void 0;
11
+ /** Retries after the first attempt, unless the client or the call says otherwise. */
12
+ exports.DEFAULT_MAX_RETRIES = 2;
13
+ /** The longest wait the SDK honours from Retry-After; a longer one is answered as the error itself. */
14
+ exports.MAX_RETRY_AFTER_SECONDS = 60;
15
+ const BASE_DELAY_MS = 500;
16
+ const MAX_BACKOFF_MS = 8_000;
17
+ const isRetryableStatus = (status) => status === 429 || status >= 500;
18
+ exports.isRetryableStatus = isRetryableStatus;
19
+ /**
20
+ * Retry-After in seconds: delta-seconds or an HTTP date (RFC 9110 10.2.3). Undefined when absent or unreadable.
21
+ */
22
+ const parseRetryAfter = (value, now = Date.now()) => {
23
+ if (value === null || value === undefined)
24
+ return undefined;
25
+ const trimmed = value.trim();
26
+ if (trimmed === '')
27
+ return undefined;
28
+ if (/^\d+(\.\d+)?$/.test(trimmed))
29
+ return Number(trimmed);
30
+ const at = Date.parse(trimmed);
31
+ if (Number.isNaN(at))
32
+ return undefined;
33
+ return Math.max(0, (at - now) / 1000);
34
+ };
35
+ exports.parseRetryAfter = parseRetryAfter;
36
+ /**
37
+ * The wait before retry number `attempt` (1 for the first retry): Retry-After when the server sent one, else
38
+ * exponential backoff from 0.5 s, capped at 8 s, with up to 25% jitter taken off.
39
+ */
40
+ const retryDelayMs = (attempt, retryAfterSeconds, random = Math.random) => {
41
+ if (retryAfterSeconds !== undefined)
42
+ return Math.ceil(retryAfterSeconds * 1000);
43
+ const backoff = Math.min(MAX_BACKOFF_MS, BASE_DELAY_MS * 2 ** (attempt - 1));
44
+ return Math.round(backoff * (1 - 0.25 * random()));
45
+ };
46
+ exports.retryDelayMs = retryDelayMs;
47
+ /** Resolves after `ms`, or rejects at once when `signal` aborts. */
48
+ const sleep = (ms, signal) => new Promise((resolve, reject) => {
49
+ if (signal?.aborted) {
50
+ reject(signal.reason ?? new Error('Aborted'));
51
+ return;
52
+ }
53
+ const timer = setTimeout(() => {
54
+ signal?.removeEventListener('abort', onAbort);
55
+ resolve();
56
+ }, ms);
57
+ const onAbort = () => {
58
+ clearTimeout(timer);
59
+ reject(signal?.reason ?? new Error('Aborted'));
60
+ };
61
+ signal?.addEventListener('abort', onAbort, { once: true });
62
+ });
63
+ exports.sleep = sleep;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Webhook signature verification — Stripe-compatible pattern.
3
+ *
4
+ * Exayard signs every delivery (and `exayard listen` signs every forwarded event the same way) with:
5
+ * Exayard-Signature: t=<unix seconds>,v1=<base64 HMAC-SHA256>
6
+ *
7
+ * where the signed payload is `${t}.${rawBody}` and the HMAC key is the UTF-8 bytes of the whole whsec_... secret,
8
+ * returned once when the endpoint (or listener) was created (server: packages/backend/convex/webhooks.ts deliver).
9
+ *
10
+ * import { Exayard } from '@exayard/sdk'
11
+ * const event = await exa.webhooks.constructEvent(rawBody, request.headers.get('Exayard-Signature')!, process.env.EXAYARD_WEBHOOK_SECRET!)
12
+ * switch (event.type) {
13
+ * case 'assessment.completed': ...
14
+ * case 'bid.won': ...
15
+ * }
16
+ *
17
+ * Verify the raw body exactly as received: re-serializing parsed JSON changes the bytes and the signature fails.
18
+ */
19
+ /** Every event type a webhook endpoint or `exayard listen` receives (the server's webhook event catalog). */
20
+ export declare const WEBHOOK_EVENT_TYPES: readonly ['project.created', 'project.updated', 'project.archived', 'assessment.started', 'assessment.completed', 'assessment.cancelled', 'assessment.failed', 'estimate.generated', 'bid.generated', 'bid.submitted', 'bid.won', 'bid.lost', 'bid.accepted', 'bid.declined', 'bid.sent', 'bid.viewed', 'bid.commented', 'file.processed', 'addendum.uploaded', 'quote.requested', 'quote.received', 'quote.accepted', 'quote.rejected', 'quote.expired', 'conversation.started', 'conversation.handoff_requested', 'conversation.closed', 'lead.captured', 'contact.created', 'appointment.booked', 'appointment.rescheduled', 'appointment.cancelled', 'call.completed', 'call.missed', 'review.requested', 'job.written_back', 'payment.captured', 'payment.failed'];
21
+ export type WebhookEventType = (typeof WEBHOOK_EVENT_TYPES)[number];
22
+ export interface WebhookEvent<T = unknown> {
23
+ id: string;
24
+ type: WebhookEventType;
25
+ created: number;
26
+ /** The company the event came from (also sent as the Exayard-Organization-Id header). */
27
+ organizationId: string;
28
+ data: T;
29
+ /** True on a test event (sent from the Developer page, the API, or `exayard listen`). */
30
+ test?: boolean;
31
+ }
32
+ /** The header that carries the signature. */
33
+ export declare const WEBHOOK_SIGNATURE_HEADER = "Exayard-Signature";
34
+ export declare class WebhookSignatureError extends Error {
35
+ readonly code: 'invalid_signature' | 'replay_window_exceeded' | 'malformed_header';
36
+ constructor(code: WebhookSignatureError['code'], message: string);
37
+ }
38
+ /** Default tolerance between the signature's timestamp and now: 5 minutes, as the server documents. */
39
+ export declare const DEFAULT_WEBHOOK_TOLERANCE_SECONDS: number;
40
+ /**
41
+ * The Exayard-Signature header value for a payload: what the server sends with each delivery, and what
42
+ * `exayard listen` sends with each forwarded event. Also useful to sign fixtures in your own tests.
43
+ */
44
+ export declare const signWebhookPayload: (payload: string | Uint8Array, secret: string, options?: {
45
+ timestamp?: number;
46
+ }) => Promise<string>;
47
+ /**
48
+ * Verify a webhook delivery and parse its payload as a typed WebhookEvent.
49
+ *
50
+ * Throws WebhookSignatureError with a stable `code` on failure; answer it with a 400 so the delivery is retried:
51
+ * - malformed_header: Exayard-Signature isn't in t=…,v1=… form
52
+ * - replay_window_exceeded: the timestamp is more than `toleranceSeconds` (default 300) from now
53
+ * - invalid_signature: no v1 digest matches
54
+ */
55
+ export declare const constructWebhookEvent: <T = unknown>(payload: string | Uint8Array, signatureHeader: string | null | undefined, secret: string, options?: {
56
+ now?: number;
57
+ toleranceSeconds?: number;
58
+ }) => Promise<WebhookEvent<T>>;
59
+ //# sourceMappingURL=webhooks.d.ts.map