@zoowork-ai/sdk 0.8.0 → 0.10.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,430 @@
1
+ /**
2
+ * Receiving webhooks: the event vocabulary, and Standard Webhooks verification of a raw body.
3
+ *
4
+ * Engine emits plain [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks),
5
+ * so nothing here is invented. Three headers arrive with every POST:
6
+ *
7
+ * ```text
8
+ * webhook-id: <event id>
9
+ * webhook-timestamp: <unix seconds of THIS send attempt, not of the event>
10
+ * webhook-signature: v1,<base64 HMAC> [v1,<base64 HMAC under the other active secret>]
11
+ *
12
+ * signed_bytes = UTF8(event_id + "." + timestamp + ".") || raw_body
13
+ * signature = HMAC-SHA256(decoded_secret, signed_bytes)
14
+ * secret = "whsec_" + base64(32 bytes)
15
+ * ```
16
+ *
17
+ * Two things the shape above decides for you:
18
+ *
19
+ * - **You need the RAW bytes.** `JSON.parse` then `JSON.stringify` does not round-trip a body
20
+ * byte for byte (key order survives, but whitespace and non-ASCII escaping do not), and the
21
+ * signature covers bytes. Read the body before any JSON middleware touches it — Express
22
+ * `express.json()` and most framework body parsers consume it. {@link unwrapWebhook} parses
23
+ * only after the signature has verified, and never re-serializes.
24
+ * - **The window is checked against `webhook-timestamp`, never against the envelope's
25
+ * `created_at`.** A retry of a two-hour-old event carries a fresh timestamp and verifies; the
26
+ * envelope still says when the fact happened.
27
+ *
28
+ * Two `v1,` entries mean a rotation window is open: the sender signs under every active secret,
29
+ * so pass them all (`secret: [newest, previous]`) and any one match accepts.
30
+ *
31
+ * Zero runtime dependencies, so this is WebCrypto (`crypto.subtle`) rather than `node:crypto` —
32
+ * it runs in Node, Deno, Bun, Workers and the browser, and it is why the API is `async`. The
33
+ * comparison is `crypto.subtle.verify`, which is constant-time; this module never compares
34
+ * signatures with `===`.
35
+ *
36
+ * A receiver that would rather not use this SDK can verify the same bytes with the official
37
+ * Standard Webhooks library for its language (npm `standardwebhooks`, PyPI `standardwebhooks`,
38
+ * and the other ports). `src/__vectors__/webhook-vectors.json` is byte-compatible with them.
39
+ */
40
+ /**
41
+ * Event types the deployed Engine delivers, mirrored from its public projection.
42
+ *
43
+ * A type not in this list is NOT an error — see {@link isKnownWebhookEventType}.
44
+ */
45
+ export declare const WEBHOOK_EVENT_TYPES: readonly ["run.started", "run.finished", "run.yielded", "approval.requested", "approval.resolved", "custom_tool.requested", "custom_tool.resolved", "outcome.evaluated", "session.created", "session.archived", "session.deleted", "schedule.dispatched", "schedule.dispatch_failed", "schedule.skipped", "schedule.finished", "webhook.test"];
46
+ /**
47
+ * Schedule CONFIGURATION changes, as distinct from the fire events above.
48
+ *
49
+ * Subscribe to these types for schedule configuration changes. Deliveries may be reordered.
50
+ */
51
+ export declare const WEBHOOK_SCHEDULE_CONFIG_EVENT_TYPES: readonly ["schedule.created", "schedule.updated", "schedule.paused", "schedule.resumed", "schedule.deleted"];
52
+ export type WebhookEventType = (typeof WEBHOOK_EVENT_TYPES)[number] | (typeof WEBHOOK_SCHEDULE_CONFIG_EVENT_TYPES)[number];
53
+ /**
54
+ * Whether this SDK release knows a type.
55
+ *
56
+ * Engine adds event types within a schema version, so a receiver MUST tolerate one it has never
57
+ * seen: acknowledge it with 2xx and ignore it. Failing an unknown type only makes the sender
58
+ * retry it, and after that, dead-letter it.
59
+ */
60
+ export declare function isKnownWebhookEventType(type: string): type is WebhookEventType;
61
+ /**
62
+ * Attribution and origin, prepended to every event's `data` by the sender.
63
+ *
64
+ * `resource_access` is `internal_only` when the underlying Session is Engine's own rather than
65
+ * one you created through the public API; such a Session is not readable through the public API,
66
+ * so a lookup after the event would 404. `org_id`/`owner_uid` are the credential the endpoint
67
+ * belongs to — compare them if one process serves several tenants.
68
+ */
69
+ export interface WebhookEventAttribution {
70
+ org_id: string;
71
+ owner_uid: string;
72
+ /** Absent only on an owner-scope `webhook.test`, which belongs to no agent. */
73
+ agent_id?: string;
74
+ session_origin?: 'api' | 'schedule' | 'channel' | 'internal';
75
+ run_origin?: 'api' | 'channel' | 'none';
76
+ resource_access?: 'public' | 'internal_only';
77
+ }
78
+ /** One schedule fire folded into a run event's summary. */
79
+ export interface WebhookScheduleRunRef {
80
+ /**
81
+ * The public schedule id — the same id you pass to the schedules API routes
82
+ * (`/schedules/{schedule_id}`). A heartbeat fire uses `heartbeat`.
83
+ */
84
+ schedule_id: string;
85
+ fired_at: string;
86
+ }
87
+ /** What a yielded run is waiting for. */
88
+ export interface WebhookWaitingOnRef {
89
+ kind: 'child_session' | 'async_tool';
90
+ id: string;
91
+ }
92
+ /** Fields the three run events share. */
93
+ export interface WebhookRunEventData extends WebhookEventAttribution {
94
+ session_id: string;
95
+ run_id: string;
96
+ turn?: number;
97
+ /** `public_session_events.id` of the input that started this run — an inbound event id, not a
98
+ * public event id, and absent when nothing inbound triggered the run (a schedule fire). */
99
+ origin_event_id?: string;
100
+ /** `pse1:<seq>` cursor of this fact's session-event projection; feed it to `streamEvents`. */
101
+ event_cursor?: string;
102
+ /** The fires this run served. `schedule_runs_complete: false` means the list was truncated to
103
+ * keep the envelope under its 16 KiB ceiling — read the schedule's runs for the rest. */
104
+ schedule_runs?: WebhookScheduleRunRef[];
105
+ schedule_runs_complete?: false;
106
+ }
107
+ export interface WebhookRunStartedData extends WebhookRunEventData {
108
+ trigger?: string;
109
+ parent_run_id?: string;
110
+ }
111
+ export interface WebhookRunFinishedData extends WebhookRunEventData {
112
+ status?: 'succeeded' | 'failed' | 'aborted';
113
+ terminal_outcome?: string;
114
+ output_type?: string;
115
+ error_class?: string;
116
+ failure_stage?: string;
117
+ }
118
+ /**
119
+ * A run that stopped to wait, not a run that ended.
120
+ *
121
+ * Only a SUCCESSFUL yield becomes `run.yielded`; a failed or aborted turn stays `run.finished`
122
+ * whatever it was waiting for. So the two are exclusive, and neither implies the other follows.
123
+ */
124
+ export interface WebhookRunYieldedData extends WebhookRunEventData {
125
+ waiting_on?: WebhookWaitingOnRef[];
126
+ waiting_on_complete?: false;
127
+ }
128
+ export interface WebhookApprovalEventData extends WebhookEventAttribution {
129
+ approval_id: string;
130
+ session_id: string;
131
+ run_id: string;
132
+ event_cursor?: string;
133
+ }
134
+ export interface WebhookApprovalRequestedData extends WebhookApprovalEventData {
135
+ tool_call_id?: string;
136
+ tool_name?: string;
137
+ timeout_at?: string;
138
+ }
139
+ export interface WebhookApprovalResolvedData extends WebhookApprovalEventData {
140
+ /** `expired` is a timeout; a cancellation reads as `denied`. */
141
+ status?: 'approved' | 'denied' | 'expired';
142
+ }
143
+ export interface WebhookCustomToolEventData extends WebhookEventAttribution {
144
+ /** The custom-tool call id — `custom_tool_use_id` when you post the result back. */
145
+ call_id: string;
146
+ session_id: string;
147
+ run_id: string;
148
+ event_cursor?: string;
149
+ }
150
+ export interface WebhookCustomToolRequestedData extends WebhookCustomToolEventData {
151
+ tool_call_id?: string;
152
+ tool_name?: string;
153
+ timeout_at?: string;
154
+ }
155
+ export interface WebhookCustomToolResolvedData extends WebhookCustomToolEventData {
156
+ status?: 'completed' | 'timeout' | 'cancelled';
157
+ }
158
+ export interface WebhookOutcomeEvaluatedData extends WebhookEventAttribution {
159
+ session_id: string;
160
+ run_id: string;
161
+ event_cursor?: string;
162
+ /** 1-based evaluation round of this run. */
163
+ iteration: number;
164
+ verdict: 'satisfied' | 'failed' | 'needs_revision' | 'evaluator_error';
165
+ /** Present only when the verdict was assumed rather than graded (always with
166
+ * `verdict: 'needs_revision'`). */
167
+ skipped?: true;
168
+ grader_type?: string;
169
+ /**
170
+ * The public schedule id of the run's triggering fire, when one triggered it — the same id
171
+ * you pass to the schedules API routes (`/schedules/{schedule_id}`). A heartbeat fire uses
172
+ * `heartbeat`.
173
+ */
174
+ schedule_id?: string;
175
+ }
176
+ export interface WebhookSessionCreatedData extends WebhookEventAttribution {
177
+ session_id: string;
178
+ parent_session_id?: string;
179
+ }
180
+ export interface WebhookSessionArchivedData extends WebhookEventAttribution {
181
+ session_id: string;
182
+ archived_at?: string;
183
+ }
184
+ export interface WebhookSessionDeletedData extends WebhookEventAttribution {
185
+ session_id: string;
186
+ deleted_at?: string;
187
+ }
188
+ /** Fields every schedule FIRE event shares. `fired_at` identifies the fire. */
189
+ export interface WebhookScheduleFireData extends WebhookEventAttribution {
190
+ /**
191
+ * The public schedule id — the same id you pass to the schedules API routes
192
+ * (`/schedules/{schedule_id}`). A heartbeat fire uses `heartbeat`.
193
+ */
194
+ schedule_id: string;
195
+ fired_at: string;
196
+ job_kind?: string;
197
+ }
198
+ export interface WebhookScheduleDispatchedData extends WebhookScheduleFireData {
199
+ dispatch_mode?: string;
200
+ session_id?: string;
201
+ }
202
+ export interface WebhookScheduleDispatchFailedData extends WebhookScheduleFireData {
203
+ error_class?: string;
204
+ }
205
+ export interface WebhookScheduleSkippedData extends WebhookScheduleFireData {
206
+ reason?: string;
207
+ }
208
+ export interface WebhookScheduleFinishedData extends WebhookScheduleFireData {
209
+ result?: string;
210
+ exit_code?: number;
211
+ reason?: string;
212
+ session_id?: string;
213
+ run_id?: string;
214
+ }
215
+ /**
216
+ * A schedule's configuration changed. See {@link WEBHOOK_SCHEDULE_CONFIG_EVENT_TYPES}: declared,
217
+ * delivered for schedule configuration changes.
218
+ */
219
+ export interface WebhookScheduleConfigData extends WebhookEventAttribution {
220
+ /**
221
+ * The public schedule id — the same id you pass to the schedules API routes
222
+ * (`/schedules/{schedule_id}`). A heartbeat fire uses `heartbeat`.
223
+ */
224
+ schedule_id: string;
225
+ /** Monotonic per schedule; the ordering to trust, since deliveries can overtake each other. */
226
+ resource_version?: number;
227
+ enabled?: boolean;
228
+ changed_fields?: string[];
229
+ }
230
+ export interface WebhookTestData extends WebhookEventAttribution {
231
+ endpoint_id: string;
232
+ }
233
+ /** Each known type's `data` shape. */
234
+ export interface WebhookEventDataByType {
235
+ 'run.started': WebhookRunStartedData;
236
+ 'run.finished': WebhookRunFinishedData;
237
+ 'run.yielded': WebhookRunYieldedData;
238
+ 'approval.requested': WebhookApprovalRequestedData;
239
+ 'approval.resolved': WebhookApprovalResolvedData;
240
+ 'custom_tool.requested': WebhookCustomToolRequestedData;
241
+ 'custom_tool.resolved': WebhookCustomToolResolvedData;
242
+ 'outcome.evaluated': WebhookOutcomeEvaluatedData;
243
+ 'session.created': WebhookSessionCreatedData;
244
+ 'session.archived': WebhookSessionArchivedData;
245
+ 'session.deleted': WebhookSessionDeletedData;
246
+ 'schedule.dispatched': WebhookScheduleDispatchedData;
247
+ 'schedule.dispatch_failed': WebhookScheduleDispatchFailedData;
248
+ 'schedule.skipped': WebhookScheduleSkippedData;
249
+ 'schedule.finished': WebhookScheduleFinishedData;
250
+ 'webhook.test': WebhookTestData;
251
+ 'schedule.created': WebhookScheduleConfigData;
252
+ 'schedule.updated': WebhookScheduleConfigData;
253
+ 'schedule.paused': WebhookScheduleConfigData;
254
+ 'schedule.resumed': WebhookScheduleConfigData;
255
+ 'schedule.deleted': WebhookScheduleConfigData;
256
+ }
257
+ /**
258
+ * A known type's `data`, left open at the edges.
259
+ *
260
+ * The named fields are what the server sends today; a field a later Engine release adds reads
261
+ * back as `unknown` instead of failing to compile. Nothing is ever removed or repurposed within
262
+ * a `schema_version`, so a field that IS named stays what it says.
263
+ */
264
+ export type WebhookEventData<T extends WebhookEventType> = WebhookEventDataByType[T] & Record<string, unknown>;
265
+ /**
266
+ * The delivered envelope, as received.
267
+ *
268
+ * `type` is typed loosely on purpose: a type this release does not know arrives as its raw
269
+ * string rather than as an error. Narrow with {@link knownWebhookEvent} before switching.
270
+ */
271
+ export interface WebhookEvent {
272
+ object: 'event';
273
+ /** `whe_...`; also the `webhook-id` header, and the idempotency key for your own handler —
274
+ * a retry redelivers the SAME id. */
275
+ id: string;
276
+ type: WebhookEventType | (string & {});
277
+ /**
278
+ * Envelope contract version, `1` today. A bump is announced; additive fields are not.
279
+ *
280
+ * Always an INTEGER value — {@link unwrapWebhook} rejects anything else, so a receiver may
281
+ * compare it or switch on it without rounding first.
282
+ */
283
+ schema_version: number;
284
+ /** When the FACT happened, ISO-8601. Not when this attempt was sent — that is the
285
+ * `webhook-timestamp` header, which {@link verifyWebhookSignature} returns. */
286
+ created_at: string;
287
+ data: Record<string, unknown>;
288
+ }
289
+ /** One envelope narrowed to a single known type. */
290
+ export type WebhookEventFor<T extends WebhookEventType> = Omit<WebhookEvent, 'type' | 'data'> & {
291
+ type: T;
292
+ data: WebhookEventData<T>;
293
+ };
294
+ /** Every known type as one discriminated union — `switch (event.type)` narrows `event.data`. */
295
+ export type KnownWebhookEvent = {
296
+ [T in WebhookEventType]: WebhookEventFor<T>;
297
+ }[WebhookEventType];
298
+ /**
299
+ * The envelope as a discriminated union, or `undefined` when this release does not know its type.
300
+ *
301
+ * `undefined` is the forward-compatible path, not a failure: acknowledge the delivery anyway.
302
+ *
303
+ * The value is the same object, re-typed — nothing is copied, normalized or validated. The
304
+ * narrowing is on `type` alone; the `data` shape is the contract's, asserted rather than checked,
305
+ * exactly as the rest of this SDK types API responses. That is sound because the body arrived
306
+ * signed: verification already established that Engine wrote it.
307
+ */
308
+ export declare function knownWebhookEvent(event: WebhookEvent): KnownWebhookEvent | undefined;
309
+ /** Default clock tolerance, in seconds, in EITHER direction and inclusive. */
310
+ export declare const WEBHOOK_TOLERANCE_SECONDS = 300;
311
+ /** The sender's envelope ceiling. A receiver refuses a larger body without hashing it. */
312
+ export declare const WEBHOOK_DEFAULT_MAX_BODY_BYTES: number;
313
+ /**
314
+ * Environment variable read when `secret` is omitted.
315
+ *
316
+ * Holds one secret, or several separated by whitespace or commas for a rotation window. The
317
+ * Python SDK reads it identically, so one deployment's configuration serves both.
318
+ */
319
+ export declare const WEBHOOK_SECRET_ENV = "ZOOWORK_WEBHOOK_SECRET";
320
+ export declare const WEBHOOK_ID_HEADER = "webhook-id";
321
+ export declare const WEBHOOK_TIMESTAMP_HEADER = "webhook-timestamp";
322
+ export declare const WEBHOOK_SIGNATURE_HEADER = "webhook-signature";
323
+ export type WebhookErrorCode = 'invalid_secret' | 'body_too_large' | 'missing_header' | 'invalid_header' | 'timestamp_out_of_window' | 'signature_mismatch' | 'invalid_payload';
324
+ /**
325
+ * Every verification and unwrap failure.
326
+ *
327
+ * Match on {@link ZooworkWebhookError.code}, never on the message. The message names the failing
328
+ * check and at most a header name — never a secret, a signature value, or body bytes — so it is
329
+ * safe to log and to forward to your own error reporting.
330
+ *
331
+ * Separate from `ZooworkError`, which carries the HTTP `status` of an API call the SDK made.
332
+ * Nothing here is an HTTP response; a receiver decides its own status, and the right one is
333
+ * usually 400 for a bad signature and 2xx for anything it means to ignore.
334
+ *
335
+ * Cross-language note: the `code` strings are the contract and are identical in the Python SDK.
336
+ * The class shape is not — there this is a subclass of that SDK's `ZooworkError`, carrying a 400
337
+ * and `retryable=False`, which suits an exception hierarchy built around one base error. Match on
338
+ * `code`, and do not port `instanceof` checks between the two.
339
+ */
340
+ export declare class ZooworkWebhookError extends Error {
341
+ code: WebhookErrorCode;
342
+ constructor(code: WebhookErrorCode, message: string);
343
+ }
344
+ /** fetch `Headers`, or anything else with a case-insensitive `get`. */
345
+ export interface WebhookHeaderSource {
346
+ get(name: string): string | null | undefined;
347
+ }
348
+ /**
349
+ * Node `IncomingHttpHeaders` (lower-cased keys, arrays for repeated headers), a plain object in
350
+ * any casing, or a `Headers`-like source.
351
+ */
352
+ export type WebhookHeaders = Readonly<Record<string, string | readonly string[] | undefined>> | WebhookHeaderSource;
353
+ export interface VerifyWebhookInput {
354
+ headers: WebhookHeaders;
355
+ /** The bytes exactly as received. A string is UTF-8 encoded; never pass re-serialized JSON. */
356
+ rawBody: Uint8Array | string;
357
+ /**
358
+ * The endpoint's `whsec_` secret, or every active secret during a rotation window (order does
359
+ * not matter). A string here is always ONE secret; it is never split.
360
+ *
361
+ * Omitted, it defaults to {@link WEBHOOK_SECRET_ENV} — which MAY list several secrets separated
362
+ * by whitespace or commas, so a rotation needs a configuration change rather than a code
363
+ * change. That variable exists only where `process.env` does, so in a browser, Workers or Deno
364
+ * pass the secret explicitly.
365
+ */
366
+ secret?: string | readonly string[];
367
+ /** Receiver clock in MILLISECONDS, for tests and for replaying a captured delivery. */
368
+ now?: number;
369
+ /** Defaults to {@link WEBHOOK_TOLERANCE_SECONDS}. Widen it only for a known-skewed clock. */
370
+ toleranceSeconds?: number;
371
+ /** Defaults to {@link WEBHOOK_DEFAULT_MAX_BODY_BYTES}. */
372
+ maxBodyBytes?: number;
373
+ }
374
+ export interface VerifiedWebhook {
375
+ /** `webhook-id`, which equals the envelope's `id`. Deduplicate your handler on it. */
376
+ eventId: string;
377
+ /** `webhook-timestamp`, unix SECONDS of this send attempt. */
378
+ timestamp: number;
379
+ }
380
+ export interface SignWebhookInput {
381
+ eventId: string;
382
+ /** Unix SECONDS. */
383
+ timestamp: number;
384
+ body: Uint8Array | string;
385
+ /** One entry per secret; a rotation window double-signs. */
386
+ secret: string | readonly string[];
387
+ }
388
+ /** The three headers a sender sets. */
389
+ export type WebhookSignatureHeaders = {
390
+ [WEBHOOK_ID_HEADER]: string;
391
+ [WEBHOOK_TIMESTAMP_HEADER]: string;
392
+ [WEBHOOK_SIGNATURE_HEADER]: string;
393
+ };
394
+ /**
395
+ * Verify a delivery and return what its headers asserted.
396
+ *
397
+ * Checks run cheapest-first and fail closed: the secrets decode, the body is within its ceiling,
398
+ * the three headers are present and well-formed, the timestamp is inside the tolerance, and only
399
+ * then is every `v1` entry compared against every secret with `crypto.subtle.verify` (a
400
+ * constant-time comparison). Any one match accepts. The body is never parsed here — an
401
+ * unverified body is not yet data. Throws {@link ZooworkWebhookError} on every failure.
402
+ */
403
+ export declare function verifyWebhookSignature(input: VerifyWebhookInput): Promise<VerifiedWebhook>;
404
+ /**
405
+ * Verify a delivery and hand back its envelope.
406
+ *
407
+ * Verification runs FIRST; the body is parsed only once it is authenticated, and it is never
408
+ * re-serialized. The structural check is deliberately minimal — `object`, `id`, `type`,
409
+ * `schema_version`, `created_at`, `data` — because Engine adds fields and event types within a
410
+ * schema version, and rejecting one of those would drop a valid delivery. An unrecognized
411
+ * `type` passes straight through; narrow it with {@link knownWebhookEvent}.
412
+ *
413
+ * `schema_version` IS required, although it is the one field a receiver rarely reads: Engine's
414
+ * projector emits it unconditionally, so an envelope without it is not an envelope, and admitting
415
+ * one would leave {@link WebhookEvent} lying about its own type. It must be a number with an
416
+ * INTEGER VALUE — `1` and `1.0` both pass, since JSON has a single numeric type and they are the
417
+ * same number, while `1.5` is rejected because no release could mean it. The Python SDK checks the
418
+ * same six fields and reaches the same verdict on the same envelope.
419
+ *
420
+ * Throws {@link ZooworkWebhookError} with `invalid_payload` when the body is not an envelope,
421
+ * and with the verification codes before that.
422
+ */
423
+ export declare function unwrapWebhook(input: VerifyWebhookInput): Promise<WebhookEvent>;
424
+ /**
425
+ * Sign a body the way Engine does — the sender's side of the contract.
426
+ *
427
+ * Exported for your own tests: a receiver can build a delivery it controls, rather than pointing
428
+ * its handler at staging to get one. Engine signs real deliveries; nothing here needs to.
429
+ */
430
+ export declare function signWebhook(input: SignWebhookInput): Promise<WebhookSignatureHeaders>;