@norbix.ai/ts 1.1.0 → 1.2.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.
@@ -1,3 +1,5 @@
1
+ import { CodeMashHub2 } from '../types/hub2.dtos.js';
2
+
1
3
  declare class NorbixWebhookError extends Error {
2
4
  readonly code: string;
3
5
  constructor(message: string, code: string);
@@ -15,6 +17,38 @@ declare class NorbixWebhookParseError extends NorbixWebhookError {
15
17
  */
16
18
  declare const NORBIX_WEBHOOK_EVENT_NAMES: readonly ["database.record.inserted", "database.record.updated", "database.record.deleted", "database.record.replaced", "database.record.responsibilityChanged", "database.records.inserted", "database.records.updated", "database.records.deleted", "membership.user.registered", "membership.user.invited", "membership.user.verified", "membership.user.updated", "membership.user.deleted", "membership.user.blocked", "membership.user.reactivated", "files.file.uploaded", "files.file.deleted"];
17
19
  type NorbixWebhookEventName = (typeof NORBIX_WEBHOOK_EVENT_NAMES)[number];
20
+ /**
21
+ * Named event constants — use these instead of raw strings so app code gets
22
+ * autocomplete and is typo-safe.
23
+ *
24
+ * @example
25
+ * receiver.on(NorbixWebhookEvents.Membership.UserRegistered, (user, event) => {});
26
+ */
27
+ declare const NorbixWebhookEvents: {
28
+ readonly Database: {
29
+ readonly RecordInserted: "database.record.inserted";
30
+ readonly RecordUpdated: "database.record.updated";
31
+ readonly RecordDeleted: "database.record.deleted";
32
+ readonly RecordReplaced: "database.record.replaced";
33
+ readonly RecordResponsibilityChanged: "database.record.responsibilityChanged";
34
+ readonly RecordsInserted: "database.records.inserted";
35
+ readonly RecordsUpdated: "database.records.updated";
36
+ readonly RecordsDeleted: "database.records.deleted";
37
+ };
38
+ readonly Membership: {
39
+ readonly UserRegistered: "membership.user.registered";
40
+ readonly UserInvited: "membership.user.invited";
41
+ readonly UserVerified: "membership.user.verified";
42
+ readonly UserUpdated: "membership.user.updated";
43
+ readonly UserDeleted: "membership.user.deleted";
44
+ readonly UserBlocked: "membership.user.blocked";
45
+ readonly UserReactivated: "membership.user.reactivated";
46
+ };
47
+ readonly Files: {
48
+ readonly FileUploaded: "files.file.uploaded";
49
+ readonly FileDeleted: "files.file.deleted";
50
+ };
51
+ };
18
52
  interface NorbixWebhookEventGroup {
19
53
  group: string;
20
54
  label: string;
@@ -44,6 +78,177 @@ declare const NORBIX_WEBHOOK_HEADERS: {
44
78
  };
45
79
  type NorbixWebhookHeaderName = (typeof NORBIX_WEBHOOK_HEADERS)[keyof typeof NORBIX_WEBHOOK_HEADERS];
46
80
 
81
+ /**
82
+ * Webhook payload shapes, organised by the three trigger kinds:
83
+ *
84
+ * - **Entity** — create / delete / single-property state flip. The payload
85
+ * IS the entity; the event name already says what changed.
86
+ * - **Mutation** — an arbitrary edit you must diff. The payload is `{ from, to }`.
87
+ * - **Batch** — many records at once. The payload is an array.
88
+ *
89
+ * Wrapper identifiers (record id, schema name, …) are NOT part of the payload —
90
+ * they are lifted onto `event.metadata` by the receiver. See `event-data.ts`.
91
+ */
92
+ /** A before/after pair for a mutation event. */
93
+ interface NorbixWebhookMutation<T> {
94
+ from: T;
95
+ to: T;
96
+ }
97
+ /** `membership.user.updated` payload — a user mutation. */
98
+ type NorbixWebhookUserUpdated = NorbixWebhookMutation<CodeMashHub2.UserDto>;
99
+ /** `database.record.updated` / `replaced` payload — a document mutation. */
100
+ type NorbixWebhookRecordUpdated<TDocument = Record<string, unknown>> = NorbixWebhookMutation<TDocument>;
101
+ /** `membership.user.invited` payload (no full entity yet — just an email). */
102
+ interface NorbixWebhookUserInvited {
103
+ email: string;
104
+ }
105
+ /** `files.file.deleted` payload (no entity — just the path). */
106
+ interface NorbixWebhookFileDeleted {
107
+ path: string;
108
+ }
109
+ /** Wire payload for single-record database events (insert / delete). */
110
+ interface NorbixWebhookWireDatabaseRecord<TDocument = Record<string, unknown>> {
111
+ schemaName: string;
112
+ integrationId: string;
113
+ id: string;
114
+ document: TDocument;
115
+ schema?: CodeMashHub2.DataSchemaDto | null;
116
+ }
117
+ /** Wire payload for database record updated / replaced. */
118
+ interface NorbixWebhookWireDatabaseRecordUpdated<TDocument = Record<string, unknown>> {
119
+ schemaName: string;
120
+ integrationId: string;
121
+ id: string;
122
+ from: TDocument;
123
+ to: TDocument;
124
+ schema?: CodeMashHub2.DataSchemaDto | null;
125
+ }
126
+ /** Wire payload for database.records.inserted (batch). */
127
+ interface NorbixWebhookWireDatabaseRecordsInserted<TDocument = Record<string, unknown>> {
128
+ schemaName: string;
129
+ integrationId: string;
130
+ ids: string[];
131
+ documents: TDocument[];
132
+ schema?: CodeMashHub2.DataSchemaDto | null;
133
+ }
134
+ /** Wire payload for database.records.updated. */
135
+ interface NorbixWebhookWireDatabaseRecordsUpdated {
136
+ schemaName: string;
137
+ integrationId: string;
138
+ matchedCount: number;
139
+ modifiedCount: number;
140
+ update: Record<string, unknown>;
141
+ schema?: CodeMashHub2.DataSchemaDto | null;
142
+ }
143
+ /** Wire payload for database.records.deleted. */
144
+ interface NorbixWebhookWireDatabaseRecordsDeleted {
145
+ schemaName: string;
146
+ integrationId: string;
147
+ deletedCount: number;
148
+ filter: Record<string, unknown>;
149
+ schema?: CodeMashHub2.DataSchemaDto | null;
150
+ }
151
+ /** Wire payload for database.record.responsibilityChanged. */
152
+ interface NorbixWebhookWireDatabaseRecordResponsibilityChanged {
153
+ schemaName: string;
154
+ integrationId: string;
155
+ id: string;
156
+ fromOwnerId: string;
157
+ toOwnerId: string;
158
+ schema?: CodeMashHub2.DataSchemaDto | null;
159
+ }
160
+ /** Wire payload for membership.user.registered. */
161
+ interface NorbixWebhookWireUserRegistered {
162
+ id: string;
163
+ to: CodeMashHub2.UserDto;
164
+ }
165
+ /** Wire payload for membership.user.verified | blocked | reactivated | updated. */
166
+ interface NorbixWebhookWireUserTransition {
167
+ id: string;
168
+ from?: CodeMashHub2.UserDto | null;
169
+ to: CodeMashHub2.UserDto;
170
+ }
171
+ /** Wire payload for membership.user.deleted. */
172
+ interface NorbixWebhookWireUserDeleted {
173
+ id: string;
174
+ from: CodeMashHub2.UserDto;
175
+ }
176
+ /** Wire payload for files.file.uploaded. */
177
+ interface NorbixWebhookWireFileUploaded {
178
+ integrationId: string;
179
+ file: CodeMashHub2.FileResourceRefDto;
180
+ }
181
+ /** Wire payload for files.file.deleted. */
182
+ interface NorbixWebhookWireFileDeleted {
183
+ integrationId: string;
184
+ path: string;
185
+ }
186
+
187
+ /**
188
+ * The normalised payload the receiver hands to a typed handler, per event name.
189
+ *
190
+ * - Entity events resolve to the entity itself (`UserDto`, `FileResourceRefDto`).
191
+ * - Mutation events resolve to `{ from, to }`.
192
+ * - Batch events resolve to an array.
193
+ *
194
+ * For database events the document type is unknown at the catalog level — pass
195
+ * it as the generic on `on<TDocument>(...)` (see `NorbixWebhookPayload`).
196
+ */
197
+ interface NorbixWebhookPayloadMap<TDocument = Record<string, unknown>> {
198
+ 'membership.user.registered': CodeMashHub2.UserDto;
199
+ 'membership.user.verified': CodeMashHub2.UserDto;
200
+ 'membership.user.blocked': CodeMashHub2.UserDto;
201
+ 'membership.user.reactivated': CodeMashHub2.UserDto;
202
+ 'membership.user.deleted': CodeMashHub2.UserDto;
203
+ 'membership.user.invited': NorbixWebhookUserInvited;
204
+ 'membership.user.updated': NorbixWebhookMutation<CodeMashHub2.UserDto>;
205
+ 'database.record.inserted': TDocument;
206
+ 'database.record.deleted': TDocument;
207
+ 'database.record.updated': NorbixWebhookMutation<TDocument>;
208
+ 'database.record.replaced': NorbixWebhookMutation<TDocument>;
209
+ 'database.records.inserted': TDocument[];
210
+ 'database.records.updated': {
211
+ matchedCount: number;
212
+ modifiedCount: number;
213
+ };
214
+ 'database.records.deleted': {
215
+ deletedCount: number;
216
+ };
217
+ 'database.record.responsibilityChanged': {
218
+ fromOwnerId: string;
219
+ toOwnerId: string;
220
+ };
221
+ 'files.file.uploaded': CodeMashHub2.FileResourceRefDto;
222
+ 'files.file.deleted': NorbixWebhookFileDeleted;
223
+ }
224
+ /** Resolve the typed payload for a known event (with optional document type). */
225
+ type NorbixWebhookPayload<E extends NorbixWebhookEventName, TDocument = Record<string, unknown>> = NorbixWebhookPayloadMap<TDocument>[E];
226
+ /**
227
+ * Identifiers lifted off the wire payload onto `event.metadata`.
228
+ * Fields are present only for events that carry them.
229
+ */
230
+ interface NorbixWebhookEventMetadata {
231
+ /** The mutated / created entity id (membership events). */
232
+ user?: {
233
+ id: string;
234
+ };
235
+ /** Schema info for database.* events. `id` is null until the gateway sends it. */
236
+ schema?: {
237
+ id: string | null;
238
+ name: string;
239
+ };
240
+ /** Single record id for single-record database events. */
241
+ record?: {
242
+ id: string;
243
+ };
244
+ /** Record ids for batch database events. */
245
+ records?: {
246
+ ids: string[];
247
+ };
248
+ /** Integration id for files.* events (and database events that carry it). */
249
+ integrationId?: string;
250
+ }
251
+
47
252
  /** JSON envelope POSTed to every webhook destination. */
48
253
  interface NorbixWebhookEnvelope<TData = unknown> {
49
254
  /** Stable delivery id — dedupe retries on this (also X-Norbix-Delivery). */
@@ -88,6 +293,32 @@ interface NorbixWebhookHandleInput {
88
293
  verify?: boolean;
89
294
  }
90
295
  type NorbixWebhookHeaderBag = Record<string, string | string[] | undefined> | Headers;
296
+ /**
297
+ * Metadata object passed as the 2nd arg to a typed handler. Carries the
298
+ * delivery facts at the top level and payload identifiers under `metadata`.
299
+ */
300
+ interface NorbixWebhookEvent {
301
+ /** Logical event name, e.g. "membership.user.registered". */
302
+ name: string;
303
+ /** Stable delivery id — dedupe retries on this. */
304
+ deliveryId: string;
305
+ /** ISO-8601 UTC emit time. */
306
+ createdOn: string;
307
+ triggerId: string | null;
308
+ /** Present once the gateway sends a correlation header/field; else null. */
309
+ correlationId: string | null;
310
+ accountId: string;
311
+ projectId: string;
312
+ integrationId: string | null;
313
+ destinationId: string | null;
314
+ /** true when signature verified; null when verification was skipped. */
315
+ verified: boolean | null;
316
+ /** Identifiers lifted off the wire payload (entity id, schema, record ids). */
317
+ metadata: NorbixWebhookEventMetadata;
318
+ /** Escape hatch: the raw envelope, if a handler needs an unmapped field. */
319
+ raw: NorbixWebhookEnvelope;
320
+ }
321
+ /** Context passed to raw (`onRaw`) handlers alongside the envelope. */
91
322
  interface NorbixWebhookContext {
92
323
  path?: string;
93
324
  headers: NorbixWebhookDeliveryHeaders;
@@ -103,15 +334,52 @@ interface NorbixWebhookHandleResult {
103
334
  handled: boolean;
104
335
  triggerId?: string | null;
105
336
  }
106
- type NorbixWebhookHandler<TData = unknown> = (envelope: NorbixWebhookEnvelope<TData>, ctx: NorbixWebhookContext) => void | Promise<void>;
107
- type NorbixWebhookHandlerMap = Partial<Record<NorbixWebhookEventName | string, NorbixWebhookHandler>>;
337
+ /** Typed handler first arg is the normalised payload, second is metadata. */
338
+ type NorbixWebhookHandler<TPayload = unknown> = (payload: TPayload, event: NorbixWebhookEvent) => void | Promise<void>;
339
+ /** Raw handler — first arg is the envelope, second is the delivery context. */
340
+ type NorbixWebhookRawHandler<TData = unknown> = (envelope: NorbixWebhookEnvelope<TData>, ctx: NorbixWebhookContext) => void | Promise<void>;
108
341
  interface NorbixWebhookReceiverOptions {
109
- /** When set, verification runs on every delivery. Omit to skip verify. */
342
+ /**
343
+ * Signing secret. When set, verification runs on every delivery.
344
+ * Defaults to `process.env.NORBIX_WEBHOOK_SIGNING_SECRET`. Omit to skip verify.
345
+ */
110
346
  secret?: string;
111
- /** Reject timestamps outside this window (seconds). Default 300. */
347
+ /**
348
+ * Reject timestamps outside this window (seconds).
349
+ * Defaults to `process.env.NORBIX_WEBHOOK_TOLERANCE_SECONDS`, else 300.
350
+ */
112
351
  toleranceSeconds?: number;
352
+ /**
353
+ * Optional guard — when set, deliveries whose envelope projectId does not
354
+ * match are rejected. Defaults to `process.env.NORBIX_PROJECT_ID`.
355
+ */
356
+ projectId?: string;
357
+ /**
358
+ * Optional guard — when set, deliveries whose envelope accountId does not
359
+ * match are rejected. Defaults to `process.env.NORBIX_ACCOUNT_ID`.
360
+ */
361
+ accountId?: string;
113
362
  }
114
363
 
364
+ /** Result of normalising a wire envelope for a typed handler. */
365
+ interface NorbixWebhookNormalized {
366
+ /** The payload-first value handed to a typed handler. */
367
+ payload: unknown;
368
+ /** Identifiers lifted off the wire payload. */
369
+ metadata: NorbixWebhookEventMetadata;
370
+ }
371
+ /**
372
+ * Turn a raw wire envelope into `{ payload, metadata }`.
373
+ *
374
+ * - Entity events → `payload` is the entity (user / document / file).
375
+ * - Mutation events → `payload` is `{ from, to }`.
376
+ * - Batch events → `payload` is the array.
377
+ *
378
+ * Wrapper ids (record id, schema, user id, …) are moved onto `metadata`.
379
+ * Unknown events fall back to `payload = envelope.data`, `metadata = {}`.
380
+ */
381
+ declare function normalizeNorbixWebhook(envelope: NorbixWebhookEnvelope): NorbixWebhookNormalized;
382
+
115
383
  /** Read Norbix delivery headers from an incoming HTTP request. */
116
384
  declare function parseNorbixWebhookHeaders(headers: NorbixWebhookHeaderBag): NorbixWebhookDeliveryHeaders;
117
385
  /** Parse the JSON envelope from the raw POST body. */
@@ -135,41 +403,73 @@ declare function computeNorbixWebhookSignature(secret: string, timestamp: string
135
403
  /**
136
404
  * Register handlers for inbound Norbix webhook deliveries (trigger → destination POST).
137
405
  *
138
- * Triggers with a `WebhookCall` action publish events to configured destinations.
139
- * This receiver verifies the HMAC signature, parses the envelope, and dispatches
140
- * to per-event handlers (similar to Stripe's webhook pattern).
406
+ * Verifies the HMAC signature, parses the envelope, normalises the payload, and
407
+ * dispatches to per-event handlers.
141
408
  *
142
409
  * @example
143
410
  * ```ts
144
- * const receiver = new NorbixWebhookReceiver({
145
- * secret: process.env.NORBIX_WEBHOOK_SECRET,
146
- * });
411
+ * import { NorbixWebhookReceiver, NorbixWebhookEvents } from '@norbix.ai/ts/webhooks';
412
+ * import type { CodeMashHub2 } from '@norbix.ai/ts/types/hub';
147
413
  *
148
- * receiver.on('database.record.inserted', async (event) => {
149
- * console.log('inserted', event.data);
150
- * });
414
+ * const receiver = new NorbixWebhookReceiver(); // reads env
151
415
  *
152
- * receiver.onDefault(async (event) => {
153
- * console.log('unhandled', event.event);
416
+ * // Typed: first arg IS the payload, second is metadata.
417
+ * receiver.on<CodeMashHub2.UserDto>(
418
+ * NorbixWebhookEvents.Membership.UserRegistered,
419
+ * (user, event) => {
420
+ * user.email ?? user.userName;
421
+ * event.metadata.user?.id;
422
+ * },
423
+ * );
424
+ *
425
+ * // Mutation: payload is { from, to }.
426
+ * receiver.on(NorbixWebhookEvents.Membership.UserUpdated, (user, event) => {
427
+ * if (user.from.email !== user.to.email) { ... }
154
428
  * });
155
429
  *
430
+ * // Raw escape hatch: (envelope, ctx).
431
+ * receiver.onRaw(NorbixWebhookEvents.Files.FileUploaded, (envelope, ctx) => {});
432
+ *
156
433
  * await receiver.handle({ rawBody, headers: req.headers });
157
434
  * ```
158
435
  */
159
436
  declare class NorbixWebhookReceiver {
160
- private readonly options;
161
437
  private readonly handlers;
162
438
  private defaultHandler?;
439
+ private readonly config;
163
440
  constructor(options?: NorbixWebhookReceiverOptions);
164
- /** Handle a specific event name (e.g. membership.user.registered). */
441
+ /**
442
+ * Membership user entity events (create / delete / state flip) — `payload`
443
+ * is the user directly. Pass the entity type as the generic, e.g.
444
+ * `on<CodeMashHub2.UserDto>(NorbixWebhookEvents.Membership.UserRegistered, ...)`.
445
+ */
446
+ on<TUser = CodeMashHub2.UserDto>(event: 'membership.user.registered' | 'membership.user.verified' | 'membership.user.blocked' | 'membership.user.reactivated' | 'membership.user.deleted', handler: NorbixWebhookHandler<TUser>): this;
447
+ /** Membership user-updated mutation — `payload` is `{ from, to }`. */
448
+ on<TUser = CodeMashHub2.UserDto>(event: 'membership.user.updated', handler: NorbixWebhookHandler<NorbixWebhookMutation<TUser>>): this;
449
+ /** Database record mutation — pass the document type; `payload` is `{ from, to }`. */
450
+ on<TDocument>(event: 'database.record.updated' | 'database.record.replaced', handler: NorbixWebhookHandler<NorbixWebhookMutation<TDocument>>): this;
451
+ /** Database single-record entity — pass the document type; `payload` is the document. */
452
+ on<TDocument>(event: 'database.record.inserted' | 'database.record.deleted', handler: NorbixWebhookHandler<TDocument>): this;
453
+ /** Database batch insert — pass the document type; `payload` is the array. */
454
+ on<TDocument>(event: 'database.records.inserted', handler: NorbixWebhookHandler<TDocument[]>): this;
455
+ /** Any known catalog event — `payload` typed from the payload map. */
456
+ on<E extends NorbixWebhookEventName>(event: E, handler: NorbixWebhookHandler<NorbixWebhookPayload<E>>): this;
457
+ /** Any event name — untyped payload. */
165
458
  on(event: string, handler: NorbixWebhookHandler): this;
166
- /** Fallback when no event-specific handler is registered. */
167
- onDefault(handler: NorbixWebhookHandler): this;
459
+ /** Register the typed handler for many events at once (skips already-registered). */
460
+ onEach(events: readonly string[], handler: NorbixWebhookHandler): this;
461
+ /** Raw handler for one event — receives the envelope and delivery context. */
462
+ onRaw<TData = unknown>(event: string, handler: NorbixWebhookRawHandler<TData>): this;
463
+ /** Register a raw handler for many events at once (skips already-registered). */
464
+ onEachRaw(events: readonly string[], handler: NorbixWebhookRawHandler): this;
465
+ /** Fallback (raw) when no event-specific handler is registered. */
466
+ onDefault(handler: NorbixWebhookRawHandler): this;
168
467
  /**
169
- * Verify (when secret configured), parse, and dispatch the delivery.
170
- * Returns 200-worthy result throw NorbixWebhookSignatureError for 401.
468
+ * Verify (when secret configured), parse, normalise, and dispatch the delivery.
469
+ * Throws NorbixWebhookSignatureError (401-worthy) on bad signature or guard
470
+ * mismatch; otherwise returns a 200-worthy result.
171
471
  */
172
472
  handle(input: NorbixWebhookHandleInput): Promise<NorbixWebhookHandleResult>;
173
473
  }
174
474
 
175
- export { NORBIX_WEBHOOK_EVENT_GROUPS, NORBIX_WEBHOOK_EVENT_NAMES, NORBIX_WEBHOOK_HEADERS, type NorbixWebhookContext, type NorbixWebhookDeliveryHeaders, type NorbixWebhookEnvelope, NorbixWebhookError, type NorbixWebhookEventGroup, type NorbixWebhookEventName, type NorbixWebhookHandleInput, type NorbixWebhookHandleResult, type NorbixWebhookHandler, type NorbixWebhookHandlerMap, type NorbixWebhookHeaderBag, type NorbixWebhookHeaderName, NorbixWebhookParseError, NorbixWebhookReceiver, type NorbixWebhookReceiverOptions, NorbixWebhookSignatureError, type NorbixWebhookSignatureVerification, type NorbixWebhookVerifyOptions, computeNorbixWebhookSignature, parseNorbixWebhookEnvelope, parseNorbixWebhookHeaders, verifyNorbixWebhookSignature };
475
+ export { NORBIX_WEBHOOK_EVENT_GROUPS, NORBIX_WEBHOOK_EVENT_NAMES, NORBIX_WEBHOOK_HEADERS, type NorbixWebhookContext, type NorbixWebhookDeliveryHeaders, type NorbixWebhookEnvelope, NorbixWebhookError, type NorbixWebhookEvent, type NorbixWebhookEventGroup, type NorbixWebhookEventMetadata, type NorbixWebhookEventName, NorbixWebhookEvents, type NorbixWebhookFileDeleted, type NorbixWebhookHandleInput, type NorbixWebhookHandleResult, type NorbixWebhookHandler, type NorbixWebhookHeaderBag, type NorbixWebhookHeaderName, type NorbixWebhookMutation, type NorbixWebhookNormalized, NorbixWebhookParseError, type NorbixWebhookPayload, type NorbixWebhookPayloadMap, type NorbixWebhookRawHandler, NorbixWebhookReceiver, type NorbixWebhookReceiverOptions, type NorbixWebhookRecordUpdated, NorbixWebhookSignatureError, type NorbixWebhookSignatureVerification, type NorbixWebhookUserInvited, type NorbixWebhookUserUpdated, type NorbixWebhookVerifyOptions, type NorbixWebhookWireDatabaseRecord, type NorbixWebhookWireDatabaseRecordResponsibilityChanged, type NorbixWebhookWireDatabaseRecordUpdated, type NorbixWebhookWireDatabaseRecordsDeleted, type NorbixWebhookWireDatabaseRecordsInserted, type NorbixWebhookWireDatabaseRecordsUpdated, type NorbixWebhookWireFileDeleted, type NorbixWebhookWireFileUploaded, type NorbixWebhookWireUserDeleted, type NorbixWebhookWireUserRegistered, type NorbixWebhookWireUserTransition, computeNorbixWebhookSignature, normalizeNorbixWebhook, parseNorbixWebhookEnvelope, parseNorbixWebhookHeaders, verifyNorbixWebhookSignature };
@@ -42,6 +42,31 @@ var NORBIX_WEBHOOK_EVENT_NAMES = [
42
42
  "files.file.uploaded",
43
43
  "files.file.deleted"
44
44
  ];
45
+ var NorbixWebhookEvents = {
46
+ Database: {
47
+ RecordInserted: "database.record.inserted",
48
+ RecordUpdated: "database.record.updated",
49
+ RecordDeleted: "database.record.deleted",
50
+ RecordReplaced: "database.record.replaced",
51
+ RecordResponsibilityChanged: "database.record.responsibilityChanged",
52
+ RecordsInserted: "database.records.inserted",
53
+ RecordsUpdated: "database.records.updated",
54
+ RecordsDeleted: "database.records.deleted"
55
+ },
56
+ Membership: {
57
+ UserRegistered: "membership.user.registered",
58
+ UserInvited: "membership.user.invited",
59
+ UserVerified: "membership.user.verified",
60
+ UserUpdated: "membership.user.updated",
61
+ UserDeleted: "membership.user.deleted",
62
+ UserBlocked: "membership.user.blocked",
63
+ UserReactivated: "membership.user.reactivated"
64
+ },
65
+ Files: {
66
+ FileUploaded: "files.file.uploaded",
67
+ FileDeleted: "files.file.deleted"
68
+ }
69
+ };
45
70
  var NORBIX_WEBHOOK_EVENT_GROUPS = [
46
71
  {
47
72
  group: "database",
@@ -89,6 +114,71 @@ var NORBIX_WEBHOOK_HEADERS = {
89
114
  signature: "X-Norbix-Signature",
90
115
  timestamp: "X-Norbix-Timestamp"
91
116
  };
117
+
118
+ // src/webhooks/normalize.ts
119
+ function isObject(value) {
120
+ return typeof value === "object" && value !== null;
121
+ }
122
+ function normalizeNorbixWebhook(envelope) {
123
+ const event = envelope.event;
124
+ const data = envelope.data;
125
+ const d = isObject(data) ? data : {};
126
+ if (event.startsWith("database.")) {
127
+ const metadata = {};
128
+ if (typeof d.schemaName === "string") {
129
+ const schema = isObject(d.schema) ? d.schema : null;
130
+ const schemaId = schema && typeof schema.id === "string" ? schema.id : null;
131
+ metadata.schema = { id: schemaId, name: d.schemaName };
132
+ }
133
+ if (typeof d.integrationId === "string") metadata.integrationId = d.integrationId;
134
+ if (typeof d.id === "string") metadata.record = { id: d.id };
135
+ if (Array.isArray(d.ids)) metadata.records = { ids: d.ids };
136
+ switch (event) {
137
+ case "database.record.inserted":
138
+ case "database.record.deleted":
139
+ return { payload: d.document, metadata };
140
+ case "database.record.updated":
141
+ case "database.record.replaced":
142
+ return { payload: { from: d.from, to: d.to }, metadata };
143
+ case "database.records.inserted":
144
+ return { payload: d.documents ?? [], metadata };
145
+ default:
146
+ return { payload: data, metadata };
147
+ }
148
+ }
149
+ if (event.startsWith("membership.")) {
150
+ const metadata = {};
151
+ if (typeof d.id === "string") metadata.user = { id: d.id };
152
+ switch (event) {
153
+ case "membership.user.registered":
154
+ case "membership.user.verified":
155
+ case "membership.user.blocked":
156
+ case "membership.user.reactivated":
157
+ return { payload: d.to, metadata };
158
+ case "membership.user.deleted":
159
+ return { payload: d.from, metadata };
160
+ case "membership.user.updated":
161
+ return { payload: { from: d.from, to: d.to }, metadata };
162
+ case "membership.user.invited":
163
+ return { payload: { email: d.email }, metadata };
164
+ default:
165
+ return { payload: data, metadata };
166
+ }
167
+ }
168
+ if (event.startsWith("files.")) {
169
+ const metadata = {};
170
+ if (typeof d.integrationId === "string") metadata.integrationId = d.integrationId;
171
+ switch (event) {
172
+ case "files.file.uploaded":
173
+ return { payload: d.file, metadata };
174
+ case "files.file.deleted":
175
+ return { payload: { path: d.path }, metadata };
176
+ default:
177
+ return { payload: data, metadata };
178
+ }
179
+ }
180
+ return { payload: data, metadata: {} };
181
+ }
92
182
  function headerValue(headers, name) {
93
183
  if (headers instanceof Headers) {
94
184
  return headers.get(name);
@@ -183,38 +273,77 @@ function timingSafeEqualUtf8(a, b) {
183
273
  }
184
274
 
185
275
  // src/webhooks/receiver.ts
276
+ function readEnv() {
277
+ return typeof process !== "undefined" && process.env ? process.env : {};
278
+ }
279
+ function toNumber(value) {
280
+ if (value == null || value === "") return void 0;
281
+ const n = Number(value);
282
+ return Number.isFinite(n) ? n : void 0;
283
+ }
284
+ function resolveConfig(options) {
285
+ const env = readEnv();
286
+ const tolerance = options.toleranceSeconds ?? toNumber(env.NORBIX_WEBHOOK_TOLERANCE_SECONDS);
287
+ return {
288
+ secret: options.secret ?? env.NORBIX_WEBHOOK_SIGNING_SECRET,
289
+ projectId: options.projectId ?? env.NORBIX_PROJECT_ID,
290
+ accountId: options.accountId ?? env.NORBIX_ACCOUNT_ID,
291
+ toleranceSeconds: Number.isFinite(tolerance) ? tolerance : 300
292
+ };
293
+ }
186
294
  var NorbixWebhookReceiver = class {
187
- constructor(options = {}) {
188
- this.options = options;
189
- }
190
- options;
191
295
  handlers = /* @__PURE__ */ new Map();
192
296
  defaultHandler;
193
- /** Handle a specific event name (e.g. membership.user.registered). */
297
+ config;
298
+ constructor(options = {}) {
299
+ this.config = resolveConfig(options);
300
+ }
301
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- overloads pass narrower handlers
194
302
  on(event, handler) {
195
- this.handlers.set(event, handler);
303
+ this.handlers.set(event, { raw: false, fn: handler });
304
+ return this;
305
+ }
306
+ /** Register the typed handler for many events at once (skips already-registered). */
307
+ onEach(events, handler) {
308
+ for (const event of events) {
309
+ if (!this.handlers.has(event)) this.handlers.set(event, { raw: false, fn: handler });
310
+ }
196
311
  return this;
197
312
  }
198
- /** Fallback when no event-specific handler is registered. */
313
+ /* ---- Raw handlers: (envelope, ctx) ---- */
314
+ /** Raw handler for one event — receives the envelope and delivery context. */
315
+ onRaw(event, handler) {
316
+ this.handlers.set(event, { raw: true, fn: handler });
317
+ return this;
318
+ }
319
+ /** Register a raw handler for many events at once (skips already-registered). */
320
+ onEachRaw(events, handler) {
321
+ for (const event of events) {
322
+ if (!this.handlers.has(event)) this.handlers.set(event, { raw: true, fn: handler });
323
+ }
324
+ return this;
325
+ }
326
+ /** Fallback (raw) when no event-specific handler is registered. */
199
327
  onDefault(handler) {
200
- this.defaultHandler = handler;
328
+ this.defaultHandler = { raw: true, fn: handler };
201
329
  return this;
202
330
  }
203
331
  /**
204
- * Verify (when secret configured), parse, and dispatch the delivery.
205
- * Returns 200-worthy result throw NorbixWebhookSignatureError for 401.
332
+ * Verify (when secret configured), parse, normalise, and dispatch the delivery.
333
+ * Throws NorbixWebhookSignatureError (401-worthy) on bad signature or guard
334
+ * mismatch; otherwise returns a 200-worthy result.
206
335
  */
207
336
  async handle(input) {
208
337
  const deliveryHeaders = parseNorbixWebhookHeaders(input.headers);
209
- const shouldVerify = input.verify !== false && !!this.options.secret;
338
+ const shouldVerify = input.verify !== false && !!this.config.secret;
210
339
  let verified = null;
211
- if (shouldVerify && this.options.secret) {
340
+ if (shouldVerify && this.config.secret) {
212
341
  const result = verifyNorbixWebhookSignature({
213
- secret: this.options.secret,
342
+ secret: this.config.secret,
214
343
  rawBody: input.rawBody,
215
344
  signature: deliveryHeaders.signature,
216
345
  timestamp: deliveryHeaders.timestamp,
217
- toleranceSeconds: this.options.toleranceSeconds ?? 300
346
+ toleranceSeconds: this.config.toleranceSeconds
218
347
  });
219
348
  if (!result.ok) {
220
349
  throw new NorbixWebhookSignatureError(result.reason ?? "Invalid signature");
@@ -222,6 +351,16 @@ var NorbixWebhookReceiver = class {
222
351
  verified = true;
223
352
  }
224
353
  const envelope = parseNorbixWebhookEnvelope(input.rawBody);
354
+ if (this.config.projectId && envelope.projectId !== this.config.projectId) {
355
+ throw new NorbixWebhookSignatureError(
356
+ `delivery projectId ${envelope.projectId} does not match configured ${this.config.projectId}`
357
+ );
358
+ }
359
+ if (this.config.accountId && envelope.accountId !== this.config.accountId) {
360
+ throw new NorbixWebhookSignatureError(
361
+ `delivery accountId ${envelope.accountId} does not match configured ${this.config.accountId}`
362
+ );
363
+ }
225
364
  const ctx = {
226
365
  path: input.path,
227
366
  headers: {
@@ -233,10 +372,29 @@ var NorbixWebhookReceiver = class {
233
372
  },
234
373
  verified
235
374
  };
236
- const handler = this.handlers.get(envelope.event) ?? this.defaultHandler;
375
+ const registration = this.handlers.get(envelope.event) ?? this.defaultHandler;
237
376
  let handled = false;
238
- if (handler) {
239
- await handler(envelope, ctx);
377
+ if (registration) {
378
+ if (registration.raw) {
379
+ await registration.fn(envelope, ctx);
380
+ } else {
381
+ const { payload, metadata } = normalizeNorbixWebhook(envelope);
382
+ const event = {
383
+ name: envelope.event,
384
+ deliveryId: envelope.id,
385
+ createdOn: envelope.createdOn,
386
+ triggerId: envelope.triggerId ?? null,
387
+ correlationId: null,
388
+ accountId: ctx.headers.accountId ?? envelope.accountId,
389
+ projectId: ctx.headers.projectId ?? envelope.projectId,
390
+ integrationId: ctx.headers.integrationId,
391
+ destinationId: ctx.headers.destinationId,
392
+ verified,
393
+ metadata,
394
+ raw: envelope
395
+ };
396
+ await registration.fn(payload, event);
397
+ }
240
398
  handled = true;
241
399
  }
242
400
  return {
@@ -250,6 +408,6 @@ var NorbixWebhookReceiver = class {
250
408
  }
251
409
  };
252
410
 
253
- export { NORBIX_WEBHOOK_EVENT_GROUPS, NORBIX_WEBHOOK_EVENT_NAMES, NORBIX_WEBHOOK_HEADERS, NorbixWebhookError, NorbixWebhookParseError, NorbixWebhookReceiver, NorbixWebhookSignatureError, computeNorbixWebhookSignature, parseNorbixWebhookEnvelope, parseNorbixWebhookHeaders, verifyNorbixWebhookSignature };
411
+ export { NORBIX_WEBHOOK_EVENT_GROUPS, NORBIX_WEBHOOK_EVENT_NAMES, NORBIX_WEBHOOK_HEADERS, NorbixWebhookError, NorbixWebhookEvents, NorbixWebhookParseError, NorbixWebhookReceiver, NorbixWebhookSignatureError, computeNorbixWebhookSignature, normalizeNorbixWebhook, parseNorbixWebhookEnvelope, parseNorbixWebhookHeaders, verifyNorbixWebhookSignature };
254
412
  //# sourceMappingURL=index.js.map
255
413
  //# sourceMappingURL=index.js.map