@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.
- package/CHANGELOG.md +54 -2
- package/README.md +140 -44
- package/dist/client.d.ts +46 -20
- package/dist/client.js +14 -7
- package/dist/developer-api.d.ts +200 -0
- package/dist/developer-api.js +42 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/secret-redaction.d.ts +2 -0
- package/dist/secret-redaction.js +10 -0
- package/dist/sse.js +39 -29
- package/dist/webhooks.d.ts +430 -0
- package/dist/webhooks.js +410 -0
- package/package.json +1 -1
|
@@ -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>;
|