@norbix.ai/ts 1.0.1 โ 1.1.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/README.md +1 -0
- package/dist/webhooks/index.cjs +267 -0
- package/dist/webhooks/index.cjs.map +1 -0
- package/dist/webhooks/index.d.cts +175 -0
- package/dist/webhooks/index.d.ts +175 -0
- package/dist/webhooks/index.js +255 -0
- package/dist/webhooks/index.js.map +1 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -353,6 +353,7 @@ Default base URL: `https://hub.norbix.ai`. **18 modules ยท 315 endpoints.**
|
|
|
353
353
|
| ๐ฅ `membership` | Roles, policies, users, preferences, integrations, triggers | [`docs/hub/membership.md`](./docs/hub/membership.md) |
|
|
354
354
|
| โฐ `scheduler` | Scheduler module and task management | [`docs/hub/scheduler.md`](./docs/hub/scheduler.md) |
|
|
355
355
|
| ๐ช `webhooks` | Webhook integrations, destinations, tests, module settings | [`docs/hub/webhooks.md`](./docs/hub/webhooks.md) |
|
|
356
|
+
| ๐ฅ `webhooks` (receiver) | Verify & handle inbound Norbix webhook POSTs at your endpoint | [`docs/webhooks-receiver.md`](./docs/webhooks-receiver.md) |
|
|
356
357
|
| ๐ `auth` | Hub-side sign-in flows | [`docs/hub/auth.md`](./docs/hub/auth.md) |
|
|
357
358
|
| ๐ `apikeys` | List + regenerate Hub API keys | [`docs/hub/apikeys.md`](./docs/hub/apikeys.md) |
|
|
358
359
|
| ๐ชช `accessToken` | Refresh-token exchange | [`docs/hub/access_token.md`](./docs/hub/access_token.md) |
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
// src/webhooks/errors.ts
|
|
6
|
+
var NorbixWebhookError = class extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(message, code) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "NorbixWebhookError";
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var NorbixWebhookSignatureError = class extends NorbixWebhookError {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message, "WEBHOOK_SIGNATURE_INVALID");
|
|
17
|
+
this.name = "NorbixWebhookSignatureError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var NorbixWebhookParseError = class extends NorbixWebhookError {
|
|
21
|
+
constructor(message) {
|
|
22
|
+
super(message, "WEBHOOK_PARSE_INVALID");
|
|
23
|
+
this.name = "NorbixWebhookParseError";
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// src/webhooks/events.ts
|
|
28
|
+
var NORBIX_WEBHOOK_EVENT_NAMES = [
|
|
29
|
+
"database.record.inserted",
|
|
30
|
+
"database.record.updated",
|
|
31
|
+
"database.record.deleted",
|
|
32
|
+
"database.record.replaced",
|
|
33
|
+
"database.record.responsibilityChanged",
|
|
34
|
+
"database.records.inserted",
|
|
35
|
+
"database.records.updated",
|
|
36
|
+
"database.records.deleted",
|
|
37
|
+
"membership.user.registered",
|
|
38
|
+
"membership.user.invited",
|
|
39
|
+
"membership.user.verified",
|
|
40
|
+
"membership.user.updated",
|
|
41
|
+
"membership.user.deleted",
|
|
42
|
+
"membership.user.blocked",
|
|
43
|
+
"membership.user.reactivated",
|
|
44
|
+
"files.file.uploaded",
|
|
45
|
+
"files.file.deleted"
|
|
46
|
+
];
|
|
47
|
+
var NORBIX_WEBHOOK_EVENT_GROUPS = [
|
|
48
|
+
{
|
|
49
|
+
group: "database",
|
|
50
|
+
label: "Database",
|
|
51
|
+
events: [
|
|
52
|
+
"database.record.inserted",
|
|
53
|
+
"database.record.updated",
|
|
54
|
+
"database.record.deleted",
|
|
55
|
+
"database.record.replaced",
|
|
56
|
+
"database.record.responsibilityChanged",
|
|
57
|
+
"database.records.inserted",
|
|
58
|
+
"database.records.updated",
|
|
59
|
+
"database.records.deleted"
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
group: "membership",
|
|
64
|
+
label: "Membership",
|
|
65
|
+
events: [
|
|
66
|
+
"membership.user.registered",
|
|
67
|
+
"membership.user.invited",
|
|
68
|
+
"membership.user.verified",
|
|
69
|
+
"membership.user.updated",
|
|
70
|
+
"membership.user.deleted",
|
|
71
|
+
"membership.user.blocked",
|
|
72
|
+
"membership.user.reactivated"
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
group: "files",
|
|
77
|
+
label: "Files",
|
|
78
|
+
events: ["files.file.uploaded", "files.file.deleted"]
|
|
79
|
+
}
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
// src/webhooks/headers.ts
|
|
83
|
+
var NORBIX_WEBHOOK_HEADERS = {
|
|
84
|
+
event: "X-Norbix-Event",
|
|
85
|
+
delivery: "X-Norbix-Delivery",
|
|
86
|
+
idempotencyKey: "Idempotency-Key",
|
|
87
|
+
account: "X-Norbix-Account",
|
|
88
|
+
project: "X-Norbix-Project",
|
|
89
|
+
integration: "X-Norbix-Integration",
|
|
90
|
+
destination: "X-Norbix-Destination",
|
|
91
|
+
signature: "X-Norbix-Signature",
|
|
92
|
+
timestamp: "X-Norbix-Timestamp"
|
|
93
|
+
};
|
|
94
|
+
function headerValue(headers, name) {
|
|
95
|
+
if (headers instanceof Headers) {
|
|
96
|
+
return headers.get(name);
|
|
97
|
+
}
|
|
98
|
+
const direct = headers[name];
|
|
99
|
+
if (direct !== void 0) {
|
|
100
|
+
return Array.isArray(direct) ? direct[0] ?? null : direct;
|
|
101
|
+
}
|
|
102
|
+
const lower = name.toLowerCase();
|
|
103
|
+
const lowerDirect = headers[lower];
|
|
104
|
+
if (lowerDirect !== void 0) {
|
|
105
|
+
return Array.isArray(lowerDirect) ? lowerDirect[0] ?? null : lowerDirect;
|
|
106
|
+
}
|
|
107
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
108
|
+
if (key.toLowerCase() === lower) {
|
|
109
|
+
return Array.isArray(value) ? value[0] ?? null : value ?? null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
function parseNorbixWebhookHeaders(headers) {
|
|
115
|
+
const deliveryId = headerValue(headers, NORBIX_WEBHOOK_HEADERS.delivery) ?? headerValue(headers, NORBIX_WEBHOOK_HEADERS.idempotencyKey);
|
|
116
|
+
return {
|
|
117
|
+
event: headerValue(headers, NORBIX_WEBHOOK_HEADERS.event),
|
|
118
|
+
deliveryId,
|
|
119
|
+
idempotencyKey: headerValue(headers, NORBIX_WEBHOOK_HEADERS.idempotencyKey),
|
|
120
|
+
accountId: headerValue(headers, NORBIX_WEBHOOK_HEADERS.account),
|
|
121
|
+
projectId: headerValue(headers, NORBIX_WEBHOOK_HEADERS.project),
|
|
122
|
+
integrationId: headerValue(headers, NORBIX_WEBHOOK_HEADERS.integration),
|
|
123
|
+
destinationId: headerValue(headers, NORBIX_WEBHOOK_HEADERS.destination),
|
|
124
|
+
signature: headerValue(headers, NORBIX_WEBHOOK_HEADERS.signature),
|
|
125
|
+
timestamp: headerValue(headers, NORBIX_WEBHOOK_HEADERS.timestamp)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function parseNorbixWebhookEnvelope(rawBody) {
|
|
129
|
+
let parsed;
|
|
130
|
+
try {
|
|
131
|
+
parsed = JSON.parse(rawBody);
|
|
132
|
+
} catch {
|
|
133
|
+
throw new NorbixWebhookParseError("Webhook body is not valid JSON");
|
|
134
|
+
}
|
|
135
|
+
if (!parsed || typeof parsed !== "object") {
|
|
136
|
+
throw new NorbixWebhookParseError("Webhook body must be a JSON object");
|
|
137
|
+
}
|
|
138
|
+
const envelope = parsed;
|
|
139
|
+
if (typeof envelope.id !== "string" || !envelope.id) {
|
|
140
|
+
throw new NorbixWebhookParseError("Webhook envelope missing id");
|
|
141
|
+
}
|
|
142
|
+
if (typeof envelope.event !== "string" || !envelope.event) {
|
|
143
|
+
throw new NorbixWebhookParseError("Webhook envelope missing event");
|
|
144
|
+
}
|
|
145
|
+
return envelope;
|
|
146
|
+
}
|
|
147
|
+
function verifyNorbixWebhookSignature(input) {
|
|
148
|
+
const { secret, rawBody, signature, timestamp, toleranceSeconds = 300 } = input;
|
|
149
|
+
if (!signature) {
|
|
150
|
+
return { ok: false, reason: "missing X-Norbix-Signature header" };
|
|
151
|
+
}
|
|
152
|
+
if (!timestamp) {
|
|
153
|
+
return { ok: false, reason: "missing X-Norbix-Timestamp header" };
|
|
154
|
+
}
|
|
155
|
+
if (toleranceSeconds > 0) {
|
|
156
|
+
const sent = Number(timestamp);
|
|
157
|
+
if (!Number.isFinite(sent)) {
|
|
158
|
+
return { ok: false, reason: "X-Norbix-Timestamp is not a number" };
|
|
159
|
+
}
|
|
160
|
+
const ageSeconds = Math.abs(Date.now() / 1e3 - sent);
|
|
161
|
+
if (ageSeconds > toleranceSeconds) {
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
reason: `timestamp outside ${toleranceSeconds}s tolerance (age ${Math.round(ageSeconds)}s)`
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const expected = computeNorbixWebhookSignature(secret, timestamp, rawBody);
|
|
169
|
+
if (!timingSafeEqualUtf8(expected, signature)) {
|
|
170
|
+
return { ok: false, reason: "signature mismatch" };
|
|
171
|
+
}
|
|
172
|
+
return { ok: true };
|
|
173
|
+
}
|
|
174
|
+
function computeNorbixWebhookSignature(secret, timestamp, rawBody) {
|
|
175
|
+
return `sha256=${hmacSha256Hex(secret, `${timestamp}.${rawBody}`)}`;
|
|
176
|
+
}
|
|
177
|
+
function hmacSha256Hex(secret, payload) {
|
|
178
|
+
return crypto.createHmac("sha256", secret).update(payload, "utf8").digest("hex");
|
|
179
|
+
}
|
|
180
|
+
function timingSafeEqualUtf8(a, b) {
|
|
181
|
+
const bufA = Buffer.from(a, "utf8");
|
|
182
|
+
const bufB = Buffer.from(b, "utf8");
|
|
183
|
+
if (bufA.length !== bufB.length) return false;
|
|
184
|
+
return crypto.timingSafeEqual(bufA, bufB);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/webhooks/receiver.ts
|
|
188
|
+
var NorbixWebhookReceiver = class {
|
|
189
|
+
constructor(options = {}) {
|
|
190
|
+
this.options = options;
|
|
191
|
+
}
|
|
192
|
+
options;
|
|
193
|
+
handlers = /* @__PURE__ */ new Map();
|
|
194
|
+
defaultHandler;
|
|
195
|
+
/** Handle a specific event name (e.g. membership.user.registered). */
|
|
196
|
+
on(event, handler) {
|
|
197
|
+
this.handlers.set(event, handler);
|
|
198
|
+
return this;
|
|
199
|
+
}
|
|
200
|
+
/** Fallback when no event-specific handler is registered. */
|
|
201
|
+
onDefault(handler) {
|
|
202
|
+
this.defaultHandler = handler;
|
|
203
|
+
return this;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Verify (when secret configured), parse, and dispatch the delivery.
|
|
207
|
+
* Returns 200-worthy result โ throw NorbixWebhookSignatureError for 401.
|
|
208
|
+
*/
|
|
209
|
+
async handle(input) {
|
|
210
|
+
const deliveryHeaders = parseNorbixWebhookHeaders(input.headers);
|
|
211
|
+
const shouldVerify = input.verify !== false && !!this.options.secret;
|
|
212
|
+
let verified = null;
|
|
213
|
+
if (shouldVerify && this.options.secret) {
|
|
214
|
+
const result = verifyNorbixWebhookSignature({
|
|
215
|
+
secret: this.options.secret,
|
|
216
|
+
rawBody: input.rawBody,
|
|
217
|
+
signature: deliveryHeaders.signature,
|
|
218
|
+
timestamp: deliveryHeaders.timestamp,
|
|
219
|
+
toleranceSeconds: this.options.toleranceSeconds ?? 300
|
|
220
|
+
});
|
|
221
|
+
if (!result.ok) {
|
|
222
|
+
throw new NorbixWebhookSignatureError(result.reason ?? "Invalid signature");
|
|
223
|
+
}
|
|
224
|
+
verified = true;
|
|
225
|
+
}
|
|
226
|
+
const envelope = parseNorbixWebhookEnvelope(input.rawBody);
|
|
227
|
+
const ctx = {
|
|
228
|
+
path: input.path,
|
|
229
|
+
headers: {
|
|
230
|
+
...deliveryHeaders,
|
|
231
|
+
event: deliveryHeaders.event ?? envelope.event,
|
|
232
|
+
deliveryId: deliveryHeaders.deliveryId ?? envelope.id,
|
|
233
|
+
accountId: deliveryHeaders.accountId ?? envelope.accountId,
|
|
234
|
+
projectId: deliveryHeaders.projectId ?? envelope.projectId
|
|
235
|
+
},
|
|
236
|
+
verified
|
|
237
|
+
};
|
|
238
|
+
const handler = this.handlers.get(envelope.event) ?? this.defaultHandler;
|
|
239
|
+
let handled = false;
|
|
240
|
+
if (handler) {
|
|
241
|
+
await handler(envelope, ctx);
|
|
242
|
+
handled = true;
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
received: true,
|
|
246
|
+
event: envelope.event,
|
|
247
|
+
deliveryId: envelope.id,
|
|
248
|
+
verified,
|
|
249
|
+
handled,
|
|
250
|
+
triggerId: envelope.triggerId
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
exports.NORBIX_WEBHOOK_EVENT_GROUPS = NORBIX_WEBHOOK_EVENT_GROUPS;
|
|
256
|
+
exports.NORBIX_WEBHOOK_EVENT_NAMES = NORBIX_WEBHOOK_EVENT_NAMES;
|
|
257
|
+
exports.NORBIX_WEBHOOK_HEADERS = NORBIX_WEBHOOK_HEADERS;
|
|
258
|
+
exports.NorbixWebhookError = NorbixWebhookError;
|
|
259
|
+
exports.NorbixWebhookParseError = NorbixWebhookParseError;
|
|
260
|
+
exports.NorbixWebhookReceiver = NorbixWebhookReceiver;
|
|
261
|
+
exports.NorbixWebhookSignatureError = NorbixWebhookSignatureError;
|
|
262
|
+
exports.computeNorbixWebhookSignature = computeNorbixWebhookSignature;
|
|
263
|
+
exports.parseNorbixWebhookEnvelope = parseNorbixWebhookEnvelope;
|
|
264
|
+
exports.parseNorbixWebhookHeaders = parseNorbixWebhookHeaders;
|
|
265
|
+
exports.verifyNorbixWebhookSignature = verifyNorbixWebhookSignature;
|
|
266
|
+
//# sourceMappingURL=index.cjs.map
|
|
267
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/webhooks/errors.ts","../../src/webhooks/events.ts","../../src/webhooks/headers.ts","../../src/webhooks/parse.ts","../../src/webhooks/receiver.ts"],"names":["createHmac","timingSafeEqual"],"mappings":"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","file":"index.cjs","sourcesContent":["export class NorbixWebhookError extends Error {\n readonly code: string;\n\n constructor(message: string, code: string) {\n super(message);\n this.name = 'NorbixWebhookError';\n this.code = code;\n }\n}\n\nexport class NorbixWebhookSignatureError extends NorbixWebhookError {\n constructor(message: string) {\n super(message, 'WEBHOOK_SIGNATURE_INVALID');\n this.name = 'NorbixWebhookSignatureError';\n }\n}\n\nexport class NorbixWebhookParseError extends NorbixWebhookError {\n constructor(message: string) {\n super(message, 'WEBHOOK_PARSE_INVALID');\n this.name = 'NorbixWebhookParseError';\n }\n}\n","/**\n * Closed catalog of event names a destination may subscribe to.\n * Source: gateway Domain trigger event name value objects.\n */\nexport const NORBIX_WEBHOOK_EVENT_NAMES = [\n 'database.record.inserted',\n 'database.record.updated',\n 'database.record.deleted',\n 'database.record.replaced',\n 'database.record.responsibilityChanged',\n 'database.records.inserted',\n 'database.records.updated',\n 'database.records.deleted',\n 'membership.user.registered',\n 'membership.user.invited',\n 'membership.user.verified',\n 'membership.user.updated',\n 'membership.user.deleted',\n 'membership.user.blocked',\n 'membership.user.reactivated',\n 'files.file.uploaded',\n 'files.file.deleted',\n] as const;\n\nexport type NorbixWebhookEventName = (typeof NORBIX_WEBHOOK_EVENT_NAMES)[number];\n\nexport interface NorbixWebhookEventGroup {\n group: string;\n label: string;\n events: readonly string[];\n}\n\nexport const NORBIX_WEBHOOK_EVENT_GROUPS: NorbixWebhookEventGroup[] = [\n {\n group: 'database',\n label: 'Database',\n events: [\n 'database.record.inserted',\n 'database.record.updated',\n 'database.record.deleted',\n 'database.record.replaced',\n 'database.record.responsibilityChanged',\n 'database.records.inserted',\n 'database.records.updated',\n 'database.records.deleted',\n ],\n },\n {\n group: 'membership',\n label: 'Membership',\n events: [\n 'membership.user.registered',\n 'membership.user.invited',\n 'membership.user.verified',\n 'membership.user.updated',\n 'membership.user.deleted',\n 'membership.user.blocked',\n 'membership.user.reactivated',\n ],\n },\n {\n group: 'files',\n label: 'Files',\n events: ['files.file.uploaded', 'files.file.deleted'],\n },\n];\n","/**\n * Outbound Norbix webhook delivery headers.\n *\n * Source of truth: gateway `WebhookDeliveryClient` (NOT `AuthStatics` /\n * `ConfigureCors`, which define **inbound API** headers like\n * `norbix-account-id` / `norbix-project-id` for studio โ gateway calls).\n *\n * @see gateway/src/Isidos.CodeMash.Services.Webhook/Dispatching/WebhookDeliveryClient.cs\n */\nexport const NORBIX_WEBHOOK_HEADERS = {\n event: 'X-Norbix-Event',\n delivery: 'X-Norbix-Delivery',\n idempotencyKey: 'Idempotency-Key',\n account: 'X-Norbix-Account',\n project: 'X-Norbix-Project',\n integration: 'X-Norbix-Integration',\n destination: 'X-Norbix-Destination',\n signature: 'X-Norbix-Signature',\n timestamp: 'X-Norbix-Timestamp',\n} as const;\n\nexport type NorbixWebhookHeaderName =\n (typeof NORBIX_WEBHOOK_HEADERS)[keyof typeof NORBIX_WEBHOOK_HEADERS];\n","import { createHmac, timingSafeEqual } from 'node:crypto';\n\nimport { NorbixWebhookParseError } from './errors.js';\nimport { NORBIX_WEBHOOK_HEADERS } from './headers.js';\nimport type {\n NorbixWebhookDeliveryHeaders,\n NorbixWebhookEnvelope,\n NorbixWebhookHeaderBag,\n NorbixWebhookVerifyOptions,\n} from './types.js';\n\nfunction headerValue(headers: NorbixWebhookHeaderBag, name: string): string | null {\n if (headers instanceof Headers) {\n return headers.get(name);\n }\n\n const direct = headers[name];\n if (direct !== undefined) {\n return Array.isArray(direct) ? (direct[0] ?? null) : direct;\n }\n\n const lower = name.toLowerCase();\n const lowerDirect = headers[lower];\n if (lowerDirect !== undefined) {\n return Array.isArray(lowerDirect) ? (lowerDirect[0] ?? null) : lowerDirect;\n }\n\n // Some frameworks preserve original casing on header keys.\n for (const [key, value] of Object.entries(headers)) {\n if (key.toLowerCase() === lower) {\n return Array.isArray(value) ? (value[0] ?? null) : (value ?? null);\n }\n }\n\n return null;\n}\n\n/** Read Norbix delivery headers from an incoming HTTP request. */\nexport function parseNorbixWebhookHeaders(\n headers: NorbixWebhookHeaderBag,\n): NorbixWebhookDeliveryHeaders {\n const deliveryId =\n headerValue(headers, NORBIX_WEBHOOK_HEADERS.delivery) ??\n headerValue(headers, NORBIX_WEBHOOK_HEADERS.idempotencyKey);\n\n return {\n event: headerValue(headers, NORBIX_WEBHOOK_HEADERS.event),\n deliveryId,\n idempotencyKey: headerValue(headers, NORBIX_WEBHOOK_HEADERS.idempotencyKey),\n accountId: headerValue(headers, NORBIX_WEBHOOK_HEADERS.account),\n projectId: headerValue(headers, NORBIX_WEBHOOK_HEADERS.project),\n integrationId: headerValue(headers, NORBIX_WEBHOOK_HEADERS.integration),\n destinationId: headerValue(headers, NORBIX_WEBHOOK_HEADERS.destination),\n signature: headerValue(headers, NORBIX_WEBHOOK_HEADERS.signature),\n timestamp: headerValue(headers, NORBIX_WEBHOOK_HEADERS.timestamp),\n };\n}\n\n/** Parse the JSON envelope from the raw POST body. */\nexport function parseNorbixWebhookEnvelope<TData = unknown>(\n rawBody: string,\n): NorbixWebhookEnvelope<TData> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawBody);\n } catch {\n throw new NorbixWebhookParseError('Webhook body is not valid JSON');\n }\n\n if (!parsed || typeof parsed !== 'object') {\n throw new NorbixWebhookParseError('Webhook body must be a JSON object');\n }\n\n const envelope = parsed as Partial<NorbixWebhookEnvelope<TData>>;\n if (typeof envelope.id !== 'string' || !envelope.id) {\n throw new NorbixWebhookParseError('Webhook envelope missing id');\n }\n if (typeof envelope.event !== 'string' || !envelope.event) {\n throw new NorbixWebhookParseError('Webhook envelope missing event');\n }\n\n return envelope as NorbixWebhookEnvelope<TData>;\n}\n\nexport interface NorbixWebhookSignatureVerification {\n ok: boolean;\n reason?: string;\n}\n\n/**\n * Verify X-Norbix-Signature against the raw body.\n * Algorithm (gateway WebhookDeliveryClient.Sign):\n * sha256=<hex> HMAC-SHA256(secret, \"<timestamp>.<rawBody>\")\n */\nexport function verifyNorbixWebhookSignature(\n input: {\n rawBody: string;\n signature?: string | null;\n timestamp?: string | null;\n } & NorbixWebhookVerifyOptions,\n): NorbixWebhookSignatureVerification {\n const { secret, rawBody, signature, timestamp, toleranceSeconds = 300 } = input;\n\n if (!signature) {\n return { ok: false, reason: 'missing X-Norbix-Signature header' };\n }\n if (!timestamp) {\n return { ok: false, reason: 'missing X-Norbix-Timestamp header' };\n }\n\n if (toleranceSeconds > 0) {\n const sent = Number(timestamp);\n if (!Number.isFinite(sent)) {\n return { ok: false, reason: 'X-Norbix-Timestamp is not a number' };\n }\n const ageSeconds = Math.abs(Date.now() / 1000 - sent);\n if (ageSeconds > toleranceSeconds) {\n return {\n ok: false,\n reason: `timestamp outside ${toleranceSeconds}s tolerance (age ${Math.round(ageSeconds)}s)`,\n };\n }\n }\n\n const expected = computeNorbixWebhookSignature(secret, timestamp, rawBody);\n if (!timingSafeEqualUtf8(expected, signature)) {\n return { ok: false, reason: 'signature mismatch' };\n }\n\n return { ok: true };\n}\n\nexport function computeNorbixWebhookSignature(\n secret: string,\n timestamp: string,\n rawBody: string,\n): string {\n return `sha256=${hmacSha256Hex(secret, `${timestamp}.${rawBody}`)}`;\n}\n\nfunction hmacSha256Hex(secret: string, payload: string): string {\n return createHmac('sha256', secret).update(payload, 'utf8').digest('hex');\n}\n\nfunction timingSafeEqualUtf8(a: string, b: string): boolean {\n const bufA = Buffer.from(a, 'utf8');\n const bufB = Buffer.from(b, 'utf8');\n if (bufA.length !== bufB.length) return false;\n return timingSafeEqual(bufA, bufB);\n}\n","import { NorbixWebhookSignatureError } from './errors.js';\nimport {\n parseNorbixWebhookEnvelope,\n parseNorbixWebhookHeaders,\n verifyNorbixWebhookSignature,\n} from './parse.js';\nimport type {\n NorbixWebhookContext,\n NorbixWebhookHandleInput,\n NorbixWebhookHandleResult,\n NorbixWebhookHandler,\n NorbixWebhookReceiverOptions,\n} from './types.js';\n\n/**\n * Register handlers for inbound Norbix webhook deliveries (trigger โ destination POST).\n *\n * Triggers with a `WebhookCall` action publish events to configured destinations.\n * This receiver verifies the HMAC signature, parses the envelope, and dispatches\n * to per-event handlers (similar to Stripe's webhook pattern).\n *\n * @example\n * ```ts\n * const receiver = new NorbixWebhookReceiver({\n * secret: process.env.NORBIX_WEBHOOK_SECRET,\n * });\n *\n * receiver.on('database.record.inserted', async (event) => {\n * console.log('inserted', event.data);\n * });\n *\n * receiver.onDefault(async (event) => {\n * console.log('unhandled', event.event);\n * });\n *\n * await receiver.handle({ rawBody, headers: req.headers });\n * ```\n */\nexport class NorbixWebhookReceiver {\n private readonly handlers = new Map<string, NorbixWebhookHandler>();\n private defaultHandler?: NorbixWebhookHandler;\n\n constructor(private readonly options: NorbixWebhookReceiverOptions = {}) {}\n\n /** Handle a specific event name (e.g. membership.user.registered). */\n on(event: string, handler: NorbixWebhookHandler): this {\n this.handlers.set(event, handler);\n return this;\n }\n\n /** Fallback when no event-specific handler is registered. */\n onDefault(handler: NorbixWebhookHandler): this {\n this.defaultHandler = handler;\n return this;\n }\n\n /**\n * Verify (when secret configured), parse, and dispatch the delivery.\n * Returns 200-worthy result โ throw NorbixWebhookSignatureError for 401.\n */\n async handle(input: NorbixWebhookHandleInput): Promise<NorbixWebhookHandleResult> {\n const deliveryHeaders = parseNorbixWebhookHeaders(input.headers);\n const shouldVerify = input.verify !== false && !!this.options.secret;\n let verified: boolean | null = null;\n\n if (shouldVerify && this.options.secret) {\n const result = verifyNorbixWebhookSignature({\n secret: this.options.secret,\n rawBody: input.rawBody,\n signature: deliveryHeaders.signature,\n timestamp: deliveryHeaders.timestamp,\n toleranceSeconds: this.options.toleranceSeconds ?? 300,\n });\n if (!result.ok) {\n throw new NorbixWebhookSignatureError(result.reason ?? 'Invalid signature');\n }\n verified = true;\n }\n\n const envelope = parseNorbixWebhookEnvelope(input.rawBody);\n const ctx: NorbixWebhookContext = {\n path: input.path,\n headers: {\n ...deliveryHeaders,\n event: deliveryHeaders.event ?? envelope.event,\n deliveryId: deliveryHeaders.deliveryId ?? envelope.id,\n accountId: deliveryHeaders.accountId ?? envelope.accountId,\n projectId: deliveryHeaders.projectId ?? envelope.projectId,\n },\n verified,\n };\n\n const handler = this.handlers.get(envelope.event) ?? this.defaultHandler;\n let handled = false;\n if (handler) {\n await handler(envelope, ctx);\n handled = true;\n }\n\n return {\n received: true,\n event: envelope.event,\n deliveryId: envelope.id,\n verified,\n handled,\n triggerId: envelope.triggerId,\n };\n }\n}\n"]}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
declare class NorbixWebhookError extends Error {
|
|
2
|
+
readonly code: string;
|
|
3
|
+
constructor(message: string, code: string);
|
|
4
|
+
}
|
|
5
|
+
declare class NorbixWebhookSignatureError extends NorbixWebhookError {
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
declare class NorbixWebhookParseError extends NorbixWebhookError {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Closed catalog of event names a destination may subscribe to.
|
|
14
|
+
* Source: gateway Domain trigger event name value objects.
|
|
15
|
+
*/
|
|
16
|
+
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
|
+
type NorbixWebhookEventName = (typeof NORBIX_WEBHOOK_EVENT_NAMES)[number];
|
|
18
|
+
interface NorbixWebhookEventGroup {
|
|
19
|
+
group: string;
|
|
20
|
+
label: string;
|
|
21
|
+
events: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
declare const NORBIX_WEBHOOK_EVENT_GROUPS: NorbixWebhookEventGroup[];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Outbound Norbix webhook delivery headers.
|
|
27
|
+
*
|
|
28
|
+
* Source of truth: gateway `WebhookDeliveryClient` (NOT `AuthStatics` /
|
|
29
|
+
* `ConfigureCors`, which define **inbound API** headers like
|
|
30
|
+
* `norbix-account-id` / `norbix-project-id` for studio โ gateway calls).
|
|
31
|
+
*
|
|
32
|
+
* @see gateway/src/Isidos.CodeMash.Services.Webhook/Dispatching/WebhookDeliveryClient.cs
|
|
33
|
+
*/
|
|
34
|
+
declare const NORBIX_WEBHOOK_HEADERS: {
|
|
35
|
+
readonly event: "X-Norbix-Event";
|
|
36
|
+
readonly delivery: "X-Norbix-Delivery";
|
|
37
|
+
readonly idempotencyKey: "Idempotency-Key";
|
|
38
|
+
readonly account: "X-Norbix-Account";
|
|
39
|
+
readonly project: "X-Norbix-Project";
|
|
40
|
+
readonly integration: "X-Norbix-Integration";
|
|
41
|
+
readonly destination: "X-Norbix-Destination";
|
|
42
|
+
readonly signature: "X-Norbix-Signature";
|
|
43
|
+
readonly timestamp: "X-Norbix-Timestamp";
|
|
44
|
+
};
|
|
45
|
+
type NorbixWebhookHeaderName = (typeof NORBIX_WEBHOOK_HEADERS)[keyof typeof NORBIX_WEBHOOK_HEADERS];
|
|
46
|
+
|
|
47
|
+
/** JSON envelope POSTed to every webhook destination. */
|
|
48
|
+
interface NorbixWebhookEnvelope<TData = unknown> {
|
|
49
|
+
/** Stable delivery id โ dedupe retries on this (also X-Norbix-Delivery). */
|
|
50
|
+
id: string;
|
|
51
|
+
/** Logical event name, e.g. database.record.inserted. */
|
|
52
|
+
event: string;
|
|
53
|
+
/** ISO-8601 UTC timestamp when the event was emitted. */
|
|
54
|
+
createdOn: string;
|
|
55
|
+
accountId: string;
|
|
56
|
+
projectId: string;
|
|
57
|
+
/** Trigger that produced this delivery, if any. */
|
|
58
|
+
triggerId?: string | null;
|
|
59
|
+
/** Module-specific payload (record, user, file metadata, โฆ). */
|
|
60
|
+
data: TData;
|
|
61
|
+
}
|
|
62
|
+
/** Parsed delivery headers on an inbound POST. */
|
|
63
|
+
interface NorbixWebhookDeliveryHeaders {
|
|
64
|
+
event: string | null;
|
|
65
|
+
deliveryId: string | null;
|
|
66
|
+
idempotencyKey: string | null;
|
|
67
|
+
accountId: string | null;
|
|
68
|
+
projectId: string | null;
|
|
69
|
+
integrationId: string | null;
|
|
70
|
+
destinationId: string | null;
|
|
71
|
+
signature: string | null;
|
|
72
|
+
timestamp: string | null;
|
|
73
|
+
}
|
|
74
|
+
interface NorbixWebhookVerifyOptions {
|
|
75
|
+
/** Project signing secret (from hub.webhooks.revealWebhookIntegrationSecret). */
|
|
76
|
+
secret: string;
|
|
77
|
+
/** Reject timestamps older/newer than this many seconds. Default 300. */
|
|
78
|
+
toleranceSeconds?: number;
|
|
79
|
+
}
|
|
80
|
+
interface NorbixWebhookHandleInput {
|
|
81
|
+
/** Exact UTF-8 request body bytes as received (required for signature verify). */
|
|
82
|
+
rawBody: string;
|
|
83
|
+
/** Incoming request headers (Express/Ts.ED/Fetch Headers or plain object). */
|
|
84
|
+
headers: NorbixWebhookHeaderBag;
|
|
85
|
+
/** Optional request path for logging. */
|
|
86
|
+
path?: string;
|
|
87
|
+
/** When false, skip signature verification even if secret is configured. */
|
|
88
|
+
verify?: boolean;
|
|
89
|
+
}
|
|
90
|
+
type NorbixWebhookHeaderBag = Record<string, string | string[] | undefined> | Headers;
|
|
91
|
+
interface NorbixWebhookContext {
|
|
92
|
+
path?: string;
|
|
93
|
+
headers: NorbixWebhookDeliveryHeaders;
|
|
94
|
+
/** true when signature verified; null when verification was skipped. */
|
|
95
|
+
verified: boolean | null;
|
|
96
|
+
}
|
|
97
|
+
interface NorbixWebhookHandleResult {
|
|
98
|
+
received: true;
|
|
99
|
+
event: string;
|
|
100
|
+
deliveryId: string;
|
|
101
|
+
/** true when verified, null when verification was skipped (no secret). */
|
|
102
|
+
verified: boolean | null;
|
|
103
|
+
handled: boolean;
|
|
104
|
+
triggerId?: string | null;
|
|
105
|
+
}
|
|
106
|
+
type NorbixWebhookHandler<TData = unknown> = (envelope: NorbixWebhookEnvelope<TData>, ctx: NorbixWebhookContext) => void | Promise<void>;
|
|
107
|
+
type NorbixWebhookHandlerMap = Partial<Record<NorbixWebhookEventName | string, NorbixWebhookHandler>>;
|
|
108
|
+
interface NorbixWebhookReceiverOptions {
|
|
109
|
+
/** When set, verification runs on every delivery. Omit to skip verify. */
|
|
110
|
+
secret?: string;
|
|
111
|
+
/** Reject timestamps outside this window (seconds). Default 300. */
|
|
112
|
+
toleranceSeconds?: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Read Norbix delivery headers from an incoming HTTP request. */
|
|
116
|
+
declare function parseNorbixWebhookHeaders(headers: NorbixWebhookHeaderBag): NorbixWebhookDeliveryHeaders;
|
|
117
|
+
/** Parse the JSON envelope from the raw POST body. */
|
|
118
|
+
declare function parseNorbixWebhookEnvelope<TData = unknown>(rawBody: string): NorbixWebhookEnvelope<TData>;
|
|
119
|
+
interface NorbixWebhookSignatureVerification {
|
|
120
|
+
ok: boolean;
|
|
121
|
+
reason?: string;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Verify X-Norbix-Signature against the raw body.
|
|
125
|
+
* Algorithm (gateway WebhookDeliveryClient.Sign):
|
|
126
|
+
* sha256=<hex> HMAC-SHA256(secret, "<timestamp>.<rawBody>")
|
|
127
|
+
*/
|
|
128
|
+
declare function verifyNorbixWebhookSignature(input: {
|
|
129
|
+
rawBody: string;
|
|
130
|
+
signature?: string | null;
|
|
131
|
+
timestamp?: string | null;
|
|
132
|
+
} & NorbixWebhookVerifyOptions): NorbixWebhookSignatureVerification;
|
|
133
|
+
declare function computeNorbixWebhookSignature(secret: string, timestamp: string, rawBody: string): string;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Register handlers for inbound Norbix webhook deliveries (trigger โ destination POST).
|
|
137
|
+
*
|
|
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).
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* const receiver = new NorbixWebhookReceiver({
|
|
145
|
+
* secret: process.env.NORBIX_WEBHOOK_SECRET,
|
|
146
|
+
* });
|
|
147
|
+
*
|
|
148
|
+
* receiver.on('database.record.inserted', async (event) => {
|
|
149
|
+
* console.log('inserted', event.data);
|
|
150
|
+
* });
|
|
151
|
+
*
|
|
152
|
+
* receiver.onDefault(async (event) => {
|
|
153
|
+
* console.log('unhandled', event.event);
|
|
154
|
+
* });
|
|
155
|
+
*
|
|
156
|
+
* await receiver.handle({ rawBody, headers: req.headers });
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
declare class NorbixWebhookReceiver {
|
|
160
|
+
private readonly options;
|
|
161
|
+
private readonly handlers;
|
|
162
|
+
private defaultHandler?;
|
|
163
|
+
constructor(options?: NorbixWebhookReceiverOptions);
|
|
164
|
+
/** Handle a specific event name (e.g. membership.user.registered). */
|
|
165
|
+
on(event: string, handler: NorbixWebhookHandler): this;
|
|
166
|
+
/** Fallback when no event-specific handler is registered. */
|
|
167
|
+
onDefault(handler: NorbixWebhookHandler): this;
|
|
168
|
+
/**
|
|
169
|
+
* Verify (when secret configured), parse, and dispatch the delivery.
|
|
170
|
+
* Returns 200-worthy result โ throw NorbixWebhookSignatureError for 401.
|
|
171
|
+
*/
|
|
172
|
+
handle(input: NorbixWebhookHandleInput): Promise<NorbixWebhookHandleResult>;
|
|
173
|
+
}
|
|
174
|
+
|
|
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 };
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
declare class NorbixWebhookError extends Error {
|
|
2
|
+
readonly code: string;
|
|
3
|
+
constructor(message: string, code: string);
|
|
4
|
+
}
|
|
5
|
+
declare class NorbixWebhookSignatureError extends NorbixWebhookError {
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
declare class NorbixWebhookParseError extends NorbixWebhookError {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Closed catalog of event names a destination may subscribe to.
|
|
14
|
+
* Source: gateway Domain trigger event name value objects.
|
|
15
|
+
*/
|
|
16
|
+
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
|
+
type NorbixWebhookEventName = (typeof NORBIX_WEBHOOK_EVENT_NAMES)[number];
|
|
18
|
+
interface NorbixWebhookEventGroup {
|
|
19
|
+
group: string;
|
|
20
|
+
label: string;
|
|
21
|
+
events: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
declare const NORBIX_WEBHOOK_EVENT_GROUPS: NorbixWebhookEventGroup[];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Outbound Norbix webhook delivery headers.
|
|
27
|
+
*
|
|
28
|
+
* Source of truth: gateway `WebhookDeliveryClient` (NOT `AuthStatics` /
|
|
29
|
+
* `ConfigureCors`, which define **inbound API** headers like
|
|
30
|
+
* `norbix-account-id` / `norbix-project-id` for studio โ gateway calls).
|
|
31
|
+
*
|
|
32
|
+
* @see gateway/src/Isidos.CodeMash.Services.Webhook/Dispatching/WebhookDeliveryClient.cs
|
|
33
|
+
*/
|
|
34
|
+
declare const NORBIX_WEBHOOK_HEADERS: {
|
|
35
|
+
readonly event: "X-Norbix-Event";
|
|
36
|
+
readonly delivery: "X-Norbix-Delivery";
|
|
37
|
+
readonly idempotencyKey: "Idempotency-Key";
|
|
38
|
+
readonly account: "X-Norbix-Account";
|
|
39
|
+
readonly project: "X-Norbix-Project";
|
|
40
|
+
readonly integration: "X-Norbix-Integration";
|
|
41
|
+
readonly destination: "X-Norbix-Destination";
|
|
42
|
+
readonly signature: "X-Norbix-Signature";
|
|
43
|
+
readonly timestamp: "X-Norbix-Timestamp";
|
|
44
|
+
};
|
|
45
|
+
type NorbixWebhookHeaderName = (typeof NORBIX_WEBHOOK_HEADERS)[keyof typeof NORBIX_WEBHOOK_HEADERS];
|
|
46
|
+
|
|
47
|
+
/** JSON envelope POSTed to every webhook destination. */
|
|
48
|
+
interface NorbixWebhookEnvelope<TData = unknown> {
|
|
49
|
+
/** Stable delivery id โ dedupe retries on this (also X-Norbix-Delivery). */
|
|
50
|
+
id: string;
|
|
51
|
+
/** Logical event name, e.g. database.record.inserted. */
|
|
52
|
+
event: string;
|
|
53
|
+
/** ISO-8601 UTC timestamp when the event was emitted. */
|
|
54
|
+
createdOn: string;
|
|
55
|
+
accountId: string;
|
|
56
|
+
projectId: string;
|
|
57
|
+
/** Trigger that produced this delivery, if any. */
|
|
58
|
+
triggerId?: string | null;
|
|
59
|
+
/** Module-specific payload (record, user, file metadata, โฆ). */
|
|
60
|
+
data: TData;
|
|
61
|
+
}
|
|
62
|
+
/** Parsed delivery headers on an inbound POST. */
|
|
63
|
+
interface NorbixWebhookDeliveryHeaders {
|
|
64
|
+
event: string | null;
|
|
65
|
+
deliveryId: string | null;
|
|
66
|
+
idempotencyKey: string | null;
|
|
67
|
+
accountId: string | null;
|
|
68
|
+
projectId: string | null;
|
|
69
|
+
integrationId: string | null;
|
|
70
|
+
destinationId: string | null;
|
|
71
|
+
signature: string | null;
|
|
72
|
+
timestamp: string | null;
|
|
73
|
+
}
|
|
74
|
+
interface NorbixWebhookVerifyOptions {
|
|
75
|
+
/** Project signing secret (from hub.webhooks.revealWebhookIntegrationSecret). */
|
|
76
|
+
secret: string;
|
|
77
|
+
/** Reject timestamps older/newer than this many seconds. Default 300. */
|
|
78
|
+
toleranceSeconds?: number;
|
|
79
|
+
}
|
|
80
|
+
interface NorbixWebhookHandleInput {
|
|
81
|
+
/** Exact UTF-8 request body bytes as received (required for signature verify). */
|
|
82
|
+
rawBody: string;
|
|
83
|
+
/** Incoming request headers (Express/Ts.ED/Fetch Headers or plain object). */
|
|
84
|
+
headers: NorbixWebhookHeaderBag;
|
|
85
|
+
/** Optional request path for logging. */
|
|
86
|
+
path?: string;
|
|
87
|
+
/** When false, skip signature verification even if secret is configured. */
|
|
88
|
+
verify?: boolean;
|
|
89
|
+
}
|
|
90
|
+
type NorbixWebhookHeaderBag = Record<string, string | string[] | undefined> | Headers;
|
|
91
|
+
interface NorbixWebhookContext {
|
|
92
|
+
path?: string;
|
|
93
|
+
headers: NorbixWebhookDeliveryHeaders;
|
|
94
|
+
/** true when signature verified; null when verification was skipped. */
|
|
95
|
+
verified: boolean | null;
|
|
96
|
+
}
|
|
97
|
+
interface NorbixWebhookHandleResult {
|
|
98
|
+
received: true;
|
|
99
|
+
event: string;
|
|
100
|
+
deliveryId: string;
|
|
101
|
+
/** true when verified, null when verification was skipped (no secret). */
|
|
102
|
+
verified: boolean | null;
|
|
103
|
+
handled: boolean;
|
|
104
|
+
triggerId?: string | null;
|
|
105
|
+
}
|
|
106
|
+
type NorbixWebhookHandler<TData = unknown> = (envelope: NorbixWebhookEnvelope<TData>, ctx: NorbixWebhookContext) => void | Promise<void>;
|
|
107
|
+
type NorbixWebhookHandlerMap = Partial<Record<NorbixWebhookEventName | string, NorbixWebhookHandler>>;
|
|
108
|
+
interface NorbixWebhookReceiverOptions {
|
|
109
|
+
/** When set, verification runs on every delivery. Omit to skip verify. */
|
|
110
|
+
secret?: string;
|
|
111
|
+
/** Reject timestamps outside this window (seconds). Default 300. */
|
|
112
|
+
toleranceSeconds?: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Read Norbix delivery headers from an incoming HTTP request. */
|
|
116
|
+
declare function parseNorbixWebhookHeaders(headers: NorbixWebhookHeaderBag): NorbixWebhookDeliveryHeaders;
|
|
117
|
+
/** Parse the JSON envelope from the raw POST body. */
|
|
118
|
+
declare function parseNorbixWebhookEnvelope<TData = unknown>(rawBody: string): NorbixWebhookEnvelope<TData>;
|
|
119
|
+
interface NorbixWebhookSignatureVerification {
|
|
120
|
+
ok: boolean;
|
|
121
|
+
reason?: string;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Verify X-Norbix-Signature against the raw body.
|
|
125
|
+
* Algorithm (gateway WebhookDeliveryClient.Sign):
|
|
126
|
+
* sha256=<hex> HMAC-SHA256(secret, "<timestamp>.<rawBody>")
|
|
127
|
+
*/
|
|
128
|
+
declare function verifyNorbixWebhookSignature(input: {
|
|
129
|
+
rawBody: string;
|
|
130
|
+
signature?: string | null;
|
|
131
|
+
timestamp?: string | null;
|
|
132
|
+
} & NorbixWebhookVerifyOptions): NorbixWebhookSignatureVerification;
|
|
133
|
+
declare function computeNorbixWebhookSignature(secret: string, timestamp: string, rawBody: string): string;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Register handlers for inbound Norbix webhook deliveries (trigger โ destination POST).
|
|
137
|
+
*
|
|
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).
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* const receiver = new NorbixWebhookReceiver({
|
|
145
|
+
* secret: process.env.NORBIX_WEBHOOK_SECRET,
|
|
146
|
+
* });
|
|
147
|
+
*
|
|
148
|
+
* receiver.on('database.record.inserted', async (event) => {
|
|
149
|
+
* console.log('inserted', event.data);
|
|
150
|
+
* });
|
|
151
|
+
*
|
|
152
|
+
* receiver.onDefault(async (event) => {
|
|
153
|
+
* console.log('unhandled', event.event);
|
|
154
|
+
* });
|
|
155
|
+
*
|
|
156
|
+
* await receiver.handle({ rawBody, headers: req.headers });
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
declare class NorbixWebhookReceiver {
|
|
160
|
+
private readonly options;
|
|
161
|
+
private readonly handlers;
|
|
162
|
+
private defaultHandler?;
|
|
163
|
+
constructor(options?: NorbixWebhookReceiverOptions);
|
|
164
|
+
/** Handle a specific event name (e.g. membership.user.registered). */
|
|
165
|
+
on(event: string, handler: NorbixWebhookHandler): this;
|
|
166
|
+
/** Fallback when no event-specific handler is registered. */
|
|
167
|
+
onDefault(handler: NorbixWebhookHandler): this;
|
|
168
|
+
/**
|
|
169
|
+
* Verify (when secret configured), parse, and dispatch the delivery.
|
|
170
|
+
* Returns 200-worthy result โ throw NorbixWebhookSignatureError for 401.
|
|
171
|
+
*/
|
|
172
|
+
handle(input: NorbixWebhookHandleInput): Promise<NorbixWebhookHandleResult>;
|
|
173
|
+
}
|
|
174
|
+
|
|
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 };
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'crypto';
|
|
2
|
+
|
|
3
|
+
// src/webhooks/errors.ts
|
|
4
|
+
var NorbixWebhookError = class extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(message, code) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "NorbixWebhookError";
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var NorbixWebhookSignatureError = class extends NorbixWebhookError {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message, "WEBHOOK_SIGNATURE_INVALID");
|
|
15
|
+
this.name = "NorbixWebhookSignatureError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var NorbixWebhookParseError = class extends NorbixWebhookError {
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(message, "WEBHOOK_PARSE_INVALID");
|
|
21
|
+
this.name = "NorbixWebhookParseError";
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// src/webhooks/events.ts
|
|
26
|
+
var NORBIX_WEBHOOK_EVENT_NAMES = [
|
|
27
|
+
"database.record.inserted",
|
|
28
|
+
"database.record.updated",
|
|
29
|
+
"database.record.deleted",
|
|
30
|
+
"database.record.replaced",
|
|
31
|
+
"database.record.responsibilityChanged",
|
|
32
|
+
"database.records.inserted",
|
|
33
|
+
"database.records.updated",
|
|
34
|
+
"database.records.deleted",
|
|
35
|
+
"membership.user.registered",
|
|
36
|
+
"membership.user.invited",
|
|
37
|
+
"membership.user.verified",
|
|
38
|
+
"membership.user.updated",
|
|
39
|
+
"membership.user.deleted",
|
|
40
|
+
"membership.user.blocked",
|
|
41
|
+
"membership.user.reactivated",
|
|
42
|
+
"files.file.uploaded",
|
|
43
|
+
"files.file.deleted"
|
|
44
|
+
];
|
|
45
|
+
var NORBIX_WEBHOOK_EVENT_GROUPS = [
|
|
46
|
+
{
|
|
47
|
+
group: "database",
|
|
48
|
+
label: "Database",
|
|
49
|
+
events: [
|
|
50
|
+
"database.record.inserted",
|
|
51
|
+
"database.record.updated",
|
|
52
|
+
"database.record.deleted",
|
|
53
|
+
"database.record.replaced",
|
|
54
|
+
"database.record.responsibilityChanged",
|
|
55
|
+
"database.records.inserted",
|
|
56
|
+
"database.records.updated",
|
|
57
|
+
"database.records.deleted"
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
group: "membership",
|
|
62
|
+
label: "Membership",
|
|
63
|
+
events: [
|
|
64
|
+
"membership.user.registered",
|
|
65
|
+
"membership.user.invited",
|
|
66
|
+
"membership.user.verified",
|
|
67
|
+
"membership.user.updated",
|
|
68
|
+
"membership.user.deleted",
|
|
69
|
+
"membership.user.blocked",
|
|
70
|
+
"membership.user.reactivated"
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
group: "files",
|
|
75
|
+
label: "Files",
|
|
76
|
+
events: ["files.file.uploaded", "files.file.deleted"]
|
|
77
|
+
}
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
// src/webhooks/headers.ts
|
|
81
|
+
var NORBIX_WEBHOOK_HEADERS = {
|
|
82
|
+
event: "X-Norbix-Event",
|
|
83
|
+
delivery: "X-Norbix-Delivery",
|
|
84
|
+
idempotencyKey: "Idempotency-Key",
|
|
85
|
+
account: "X-Norbix-Account",
|
|
86
|
+
project: "X-Norbix-Project",
|
|
87
|
+
integration: "X-Norbix-Integration",
|
|
88
|
+
destination: "X-Norbix-Destination",
|
|
89
|
+
signature: "X-Norbix-Signature",
|
|
90
|
+
timestamp: "X-Norbix-Timestamp"
|
|
91
|
+
};
|
|
92
|
+
function headerValue(headers, name) {
|
|
93
|
+
if (headers instanceof Headers) {
|
|
94
|
+
return headers.get(name);
|
|
95
|
+
}
|
|
96
|
+
const direct = headers[name];
|
|
97
|
+
if (direct !== void 0) {
|
|
98
|
+
return Array.isArray(direct) ? direct[0] ?? null : direct;
|
|
99
|
+
}
|
|
100
|
+
const lower = name.toLowerCase();
|
|
101
|
+
const lowerDirect = headers[lower];
|
|
102
|
+
if (lowerDirect !== void 0) {
|
|
103
|
+
return Array.isArray(lowerDirect) ? lowerDirect[0] ?? null : lowerDirect;
|
|
104
|
+
}
|
|
105
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
106
|
+
if (key.toLowerCase() === lower) {
|
|
107
|
+
return Array.isArray(value) ? value[0] ?? null : value ?? null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
function parseNorbixWebhookHeaders(headers) {
|
|
113
|
+
const deliveryId = headerValue(headers, NORBIX_WEBHOOK_HEADERS.delivery) ?? headerValue(headers, NORBIX_WEBHOOK_HEADERS.idempotencyKey);
|
|
114
|
+
return {
|
|
115
|
+
event: headerValue(headers, NORBIX_WEBHOOK_HEADERS.event),
|
|
116
|
+
deliveryId,
|
|
117
|
+
idempotencyKey: headerValue(headers, NORBIX_WEBHOOK_HEADERS.idempotencyKey),
|
|
118
|
+
accountId: headerValue(headers, NORBIX_WEBHOOK_HEADERS.account),
|
|
119
|
+
projectId: headerValue(headers, NORBIX_WEBHOOK_HEADERS.project),
|
|
120
|
+
integrationId: headerValue(headers, NORBIX_WEBHOOK_HEADERS.integration),
|
|
121
|
+
destinationId: headerValue(headers, NORBIX_WEBHOOK_HEADERS.destination),
|
|
122
|
+
signature: headerValue(headers, NORBIX_WEBHOOK_HEADERS.signature),
|
|
123
|
+
timestamp: headerValue(headers, NORBIX_WEBHOOK_HEADERS.timestamp)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function parseNorbixWebhookEnvelope(rawBody) {
|
|
127
|
+
let parsed;
|
|
128
|
+
try {
|
|
129
|
+
parsed = JSON.parse(rawBody);
|
|
130
|
+
} catch {
|
|
131
|
+
throw new NorbixWebhookParseError("Webhook body is not valid JSON");
|
|
132
|
+
}
|
|
133
|
+
if (!parsed || typeof parsed !== "object") {
|
|
134
|
+
throw new NorbixWebhookParseError("Webhook body must be a JSON object");
|
|
135
|
+
}
|
|
136
|
+
const envelope = parsed;
|
|
137
|
+
if (typeof envelope.id !== "string" || !envelope.id) {
|
|
138
|
+
throw new NorbixWebhookParseError("Webhook envelope missing id");
|
|
139
|
+
}
|
|
140
|
+
if (typeof envelope.event !== "string" || !envelope.event) {
|
|
141
|
+
throw new NorbixWebhookParseError("Webhook envelope missing event");
|
|
142
|
+
}
|
|
143
|
+
return envelope;
|
|
144
|
+
}
|
|
145
|
+
function verifyNorbixWebhookSignature(input) {
|
|
146
|
+
const { secret, rawBody, signature, timestamp, toleranceSeconds = 300 } = input;
|
|
147
|
+
if (!signature) {
|
|
148
|
+
return { ok: false, reason: "missing X-Norbix-Signature header" };
|
|
149
|
+
}
|
|
150
|
+
if (!timestamp) {
|
|
151
|
+
return { ok: false, reason: "missing X-Norbix-Timestamp header" };
|
|
152
|
+
}
|
|
153
|
+
if (toleranceSeconds > 0) {
|
|
154
|
+
const sent = Number(timestamp);
|
|
155
|
+
if (!Number.isFinite(sent)) {
|
|
156
|
+
return { ok: false, reason: "X-Norbix-Timestamp is not a number" };
|
|
157
|
+
}
|
|
158
|
+
const ageSeconds = Math.abs(Date.now() / 1e3 - sent);
|
|
159
|
+
if (ageSeconds > toleranceSeconds) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
reason: `timestamp outside ${toleranceSeconds}s tolerance (age ${Math.round(ageSeconds)}s)`
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const expected = computeNorbixWebhookSignature(secret, timestamp, rawBody);
|
|
167
|
+
if (!timingSafeEqualUtf8(expected, signature)) {
|
|
168
|
+
return { ok: false, reason: "signature mismatch" };
|
|
169
|
+
}
|
|
170
|
+
return { ok: true };
|
|
171
|
+
}
|
|
172
|
+
function computeNorbixWebhookSignature(secret, timestamp, rawBody) {
|
|
173
|
+
return `sha256=${hmacSha256Hex(secret, `${timestamp}.${rawBody}`)}`;
|
|
174
|
+
}
|
|
175
|
+
function hmacSha256Hex(secret, payload) {
|
|
176
|
+
return createHmac("sha256", secret).update(payload, "utf8").digest("hex");
|
|
177
|
+
}
|
|
178
|
+
function timingSafeEqualUtf8(a, b) {
|
|
179
|
+
const bufA = Buffer.from(a, "utf8");
|
|
180
|
+
const bufB = Buffer.from(b, "utf8");
|
|
181
|
+
if (bufA.length !== bufB.length) return false;
|
|
182
|
+
return timingSafeEqual(bufA, bufB);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/webhooks/receiver.ts
|
|
186
|
+
var NorbixWebhookReceiver = class {
|
|
187
|
+
constructor(options = {}) {
|
|
188
|
+
this.options = options;
|
|
189
|
+
}
|
|
190
|
+
options;
|
|
191
|
+
handlers = /* @__PURE__ */ new Map();
|
|
192
|
+
defaultHandler;
|
|
193
|
+
/** Handle a specific event name (e.g. membership.user.registered). */
|
|
194
|
+
on(event, handler) {
|
|
195
|
+
this.handlers.set(event, handler);
|
|
196
|
+
return this;
|
|
197
|
+
}
|
|
198
|
+
/** Fallback when no event-specific handler is registered. */
|
|
199
|
+
onDefault(handler) {
|
|
200
|
+
this.defaultHandler = handler;
|
|
201
|
+
return this;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Verify (when secret configured), parse, and dispatch the delivery.
|
|
205
|
+
* Returns 200-worthy result โ throw NorbixWebhookSignatureError for 401.
|
|
206
|
+
*/
|
|
207
|
+
async handle(input) {
|
|
208
|
+
const deliveryHeaders = parseNorbixWebhookHeaders(input.headers);
|
|
209
|
+
const shouldVerify = input.verify !== false && !!this.options.secret;
|
|
210
|
+
let verified = null;
|
|
211
|
+
if (shouldVerify && this.options.secret) {
|
|
212
|
+
const result = verifyNorbixWebhookSignature({
|
|
213
|
+
secret: this.options.secret,
|
|
214
|
+
rawBody: input.rawBody,
|
|
215
|
+
signature: deliveryHeaders.signature,
|
|
216
|
+
timestamp: deliveryHeaders.timestamp,
|
|
217
|
+
toleranceSeconds: this.options.toleranceSeconds ?? 300
|
|
218
|
+
});
|
|
219
|
+
if (!result.ok) {
|
|
220
|
+
throw new NorbixWebhookSignatureError(result.reason ?? "Invalid signature");
|
|
221
|
+
}
|
|
222
|
+
verified = true;
|
|
223
|
+
}
|
|
224
|
+
const envelope = parseNorbixWebhookEnvelope(input.rawBody);
|
|
225
|
+
const ctx = {
|
|
226
|
+
path: input.path,
|
|
227
|
+
headers: {
|
|
228
|
+
...deliveryHeaders,
|
|
229
|
+
event: deliveryHeaders.event ?? envelope.event,
|
|
230
|
+
deliveryId: deliveryHeaders.deliveryId ?? envelope.id,
|
|
231
|
+
accountId: deliveryHeaders.accountId ?? envelope.accountId,
|
|
232
|
+
projectId: deliveryHeaders.projectId ?? envelope.projectId
|
|
233
|
+
},
|
|
234
|
+
verified
|
|
235
|
+
};
|
|
236
|
+
const handler = this.handlers.get(envelope.event) ?? this.defaultHandler;
|
|
237
|
+
let handled = false;
|
|
238
|
+
if (handler) {
|
|
239
|
+
await handler(envelope, ctx);
|
|
240
|
+
handled = true;
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
received: true,
|
|
244
|
+
event: envelope.event,
|
|
245
|
+
deliveryId: envelope.id,
|
|
246
|
+
verified,
|
|
247
|
+
handled,
|
|
248
|
+
triggerId: envelope.triggerId
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export { NORBIX_WEBHOOK_EVENT_GROUPS, NORBIX_WEBHOOK_EVENT_NAMES, NORBIX_WEBHOOK_HEADERS, NorbixWebhookError, NorbixWebhookParseError, NorbixWebhookReceiver, NorbixWebhookSignatureError, computeNorbixWebhookSignature, parseNorbixWebhookEnvelope, parseNorbixWebhookHeaders, verifyNorbixWebhookSignature };
|
|
254
|
+
//# sourceMappingURL=index.js.map
|
|
255
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/webhooks/errors.ts","../../src/webhooks/events.ts","../../src/webhooks/headers.ts","../../src/webhooks/parse.ts","../../src/webhooks/receiver.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["export class NorbixWebhookError extends Error {\n readonly code: string;\n\n constructor(message: string, code: string) {\n super(message);\n this.name = 'NorbixWebhookError';\n this.code = code;\n }\n}\n\nexport class NorbixWebhookSignatureError extends NorbixWebhookError {\n constructor(message: string) {\n super(message, 'WEBHOOK_SIGNATURE_INVALID');\n this.name = 'NorbixWebhookSignatureError';\n }\n}\n\nexport class NorbixWebhookParseError extends NorbixWebhookError {\n constructor(message: string) {\n super(message, 'WEBHOOK_PARSE_INVALID');\n this.name = 'NorbixWebhookParseError';\n }\n}\n","/**\n * Closed catalog of event names a destination may subscribe to.\n * Source: gateway Domain trigger event name value objects.\n */\nexport const NORBIX_WEBHOOK_EVENT_NAMES = [\n 'database.record.inserted',\n 'database.record.updated',\n 'database.record.deleted',\n 'database.record.replaced',\n 'database.record.responsibilityChanged',\n 'database.records.inserted',\n 'database.records.updated',\n 'database.records.deleted',\n 'membership.user.registered',\n 'membership.user.invited',\n 'membership.user.verified',\n 'membership.user.updated',\n 'membership.user.deleted',\n 'membership.user.blocked',\n 'membership.user.reactivated',\n 'files.file.uploaded',\n 'files.file.deleted',\n] as const;\n\nexport type NorbixWebhookEventName = (typeof NORBIX_WEBHOOK_EVENT_NAMES)[number];\n\nexport interface NorbixWebhookEventGroup {\n group: string;\n label: string;\n events: readonly string[];\n}\n\nexport const NORBIX_WEBHOOK_EVENT_GROUPS: NorbixWebhookEventGroup[] = [\n {\n group: 'database',\n label: 'Database',\n events: [\n 'database.record.inserted',\n 'database.record.updated',\n 'database.record.deleted',\n 'database.record.replaced',\n 'database.record.responsibilityChanged',\n 'database.records.inserted',\n 'database.records.updated',\n 'database.records.deleted',\n ],\n },\n {\n group: 'membership',\n label: 'Membership',\n events: [\n 'membership.user.registered',\n 'membership.user.invited',\n 'membership.user.verified',\n 'membership.user.updated',\n 'membership.user.deleted',\n 'membership.user.blocked',\n 'membership.user.reactivated',\n ],\n },\n {\n group: 'files',\n label: 'Files',\n events: ['files.file.uploaded', 'files.file.deleted'],\n },\n];\n","/**\n * Outbound Norbix webhook delivery headers.\n *\n * Source of truth: gateway `WebhookDeliveryClient` (NOT `AuthStatics` /\n * `ConfigureCors`, which define **inbound API** headers like\n * `norbix-account-id` / `norbix-project-id` for studio โ gateway calls).\n *\n * @see gateway/src/Isidos.CodeMash.Services.Webhook/Dispatching/WebhookDeliveryClient.cs\n */\nexport const NORBIX_WEBHOOK_HEADERS = {\n event: 'X-Norbix-Event',\n delivery: 'X-Norbix-Delivery',\n idempotencyKey: 'Idempotency-Key',\n account: 'X-Norbix-Account',\n project: 'X-Norbix-Project',\n integration: 'X-Norbix-Integration',\n destination: 'X-Norbix-Destination',\n signature: 'X-Norbix-Signature',\n timestamp: 'X-Norbix-Timestamp',\n} as const;\n\nexport type NorbixWebhookHeaderName =\n (typeof NORBIX_WEBHOOK_HEADERS)[keyof typeof NORBIX_WEBHOOK_HEADERS];\n","import { createHmac, timingSafeEqual } from 'node:crypto';\n\nimport { NorbixWebhookParseError } from './errors.js';\nimport { NORBIX_WEBHOOK_HEADERS } from './headers.js';\nimport type {\n NorbixWebhookDeliveryHeaders,\n NorbixWebhookEnvelope,\n NorbixWebhookHeaderBag,\n NorbixWebhookVerifyOptions,\n} from './types.js';\n\nfunction headerValue(headers: NorbixWebhookHeaderBag, name: string): string | null {\n if (headers instanceof Headers) {\n return headers.get(name);\n }\n\n const direct = headers[name];\n if (direct !== undefined) {\n return Array.isArray(direct) ? (direct[0] ?? null) : direct;\n }\n\n const lower = name.toLowerCase();\n const lowerDirect = headers[lower];\n if (lowerDirect !== undefined) {\n return Array.isArray(lowerDirect) ? (lowerDirect[0] ?? null) : lowerDirect;\n }\n\n // Some frameworks preserve original casing on header keys.\n for (const [key, value] of Object.entries(headers)) {\n if (key.toLowerCase() === lower) {\n return Array.isArray(value) ? (value[0] ?? null) : (value ?? null);\n }\n }\n\n return null;\n}\n\n/** Read Norbix delivery headers from an incoming HTTP request. */\nexport function parseNorbixWebhookHeaders(\n headers: NorbixWebhookHeaderBag,\n): NorbixWebhookDeliveryHeaders {\n const deliveryId =\n headerValue(headers, NORBIX_WEBHOOK_HEADERS.delivery) ??\n headerValue(headers, NORBIX_WEBHOOK_HEADERS.idempotencyKey);\n\n return {\n event: headerValue(headers, NORBIX_WEBHOOK_HEADERS.event),\n deliveryId,\n idempotencyKey: headerValue(headers, NORBIX_WEBHOOK_HEADERS.idempotencyKey),\n accountId: headerValue(headers, NORBIX_WEBHOOK_HEADERS.account),\n projectId: headerValue(headers, NORBIX_WEBHOOK_HEADERS.project),\n integrationId: headerValue(headers, NORBIX_WEBHOOK_HEADERS.integration),\n destinationId: headerValue(headers, NORBIX_WEBHOOK_HEADERS.destination),\n signature: headerValue(headers, NORBIX_WEBHOOK_HEADERS.signature),\n timestamp: headerValue(headers, NORBIX_WEBHOOK_HEADERS.timestamp),\n };\n}\n\n/** Parse the JSON envelope from the raw POST body. */\nexport function parseNorbixWebhookEnvelope<TData = unknown>(\n rawBody: string,\n): NorbixWebhookEnvelope<TData> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawBody);\n } catch {\n throw new NorbixWebhookParseError('Webhook body is not valid JSON');\n }\n\n if (!parsed || typeof parsed !== 'object') {\n throw new NorbixWebhookParseError('Webhook body must be a JSON object');\n }\n\n const envelope = parsed as Partial<NorbixWebhookEnvelope<TData>>;\n if (typeof envelope.id !== 'string' || !envelope.id) {\n throw new NorbixWebhookParseError('Webhook envelope missing id');\n }\n if (typeof envelope.event !== 'string' || !envelope.event) {\n throw new NorbixWebhookParseError('Webhook envelope missing event');\n }\n\n return envelope as NorbixWebhookEnvelope<TData>;\n}\n\nexport interface NorbixWebhookSignatureVerification {\n ok: boolean;\n reason?: string;\n}\n\n/**\n * Verify X-Norbix-Signature against the raw body.\n * Algorithm (gateway WebhookDeliveryClient.Sign):\n * sha256=<hex> HMAC-SHA256(secret, \"<timestamp>.<rawBody>\")\n */\nexport function verifyNorbixWebhookSignature(\n input: {\n rawBody: string;\n signature?: string | null;\n timestamp?: string | null;\n } & NorbixWebhookVerifyOptions,\n): NorbixWebhookSignatureVerification {\n const { secret, rawBody, signature, timestamp, toleranceSeconds = 300 } = input;\n\n if (!signature) {\n return { ok: false, reason: 'missing X-Norbix-Signature header' };\n }\n if (!timestamp) {\n return { ok: false, reason: 'missing X-Norbix-Timestamp header' };\n }\n\n if (toleranceSeconds > 0) {\n const sent = Number(timestamp);\n if (!Number.isFinite(sent)) {\n return { ok: false, reason: 'X-Norbix-Timestamp is not a number' };\n }\n const ageSeconds = Math.abs(Date.now() / 1000 - sent);\n if (ageSeconds > toleranceSeconds) {\n return {\n ok: false,\n reason: `timestamp outside ${toleranceSeconds}s tolerance (age ${Math.round(ageSeconds)}s)`,\n };\n }\n }\n\n const expected = computeNorbixWebhookSignature(secret, timestamp, rawBody);\n if (!timingSafeEqualUtf8(expected, signature)) {\n return { ok: false, reason: 'signature mismatch' };\n }\n\n return { ok: true };\n}\n\nexport function computeNorbixWebhookSignature(\n secret: string,\n timestamp: string,\n rawBody: string,\n): string {\n return `sha256=${hmacSha256Hex(secret, `${timestamp}.${rawBody}`)}`;\n}\n\nfunction hmacSha256Hex(secret: string, payload: string): string {\n return createHmac('sha256', secret).update(payload, 'utf8').digest('hex');\n}\n\nfunction timingSafeEqualUtf8(a: string, b: string): boolean {\n const bufA = Buffer.from(a, 'utf8');\n const bufB = Buffer.from(b, 'utf8');\n if (bufA.length !== bufB.length) return false;\n return timingSafeEqual(bufA, bufB);\n}\n","import { NorbixWebhookSignatureError } from './errors.js';\nimport {\n parseNorbixWebhookEnvelope,\n parseNorbixWebhookHeaders,\n verifyNorbixWebhookSignature,\n} from './parse.js';\nimport type {\n NorbixWebhookContext,\n NorbixWebhookHandleInput,\n NorbixWebhookHandleResult,\n NorbixWebhookHandler,\n NorbixWebhookReceiverOptions,\n} from './types.js';\n\n/**\n * Register handlers for inbound Norbix webhook deliveries (trigger โ destination POST).\n *\n * Triggers with a `WebhookCall` action publish events to configured destinations.\n * This receiver verifies the HMAC signature, parses the envelope, and dispatches\n * to per-event handlers (similar to Stripe's webhook pattern).\n *\n * @example\n * ```ts\n * const receiver = new NorbixWebhookReceiver({\n * secret: process.env.NORBIX_WEBHOOK_SECRET,\n * });\n *\n * receiver.on('database.record.inserted', async (event) => {\n * console.log('inserted', event.data);\n * });\n *\n * receiver.onDefault(async (event) => {\n * console.log('unhandled', event.event);\n * });\n *\n * await receiver.handle({ rawBody, headers: req.headers });\n * ```\n */\nexport class NorbixWebhookReceiver {\n private readonly handlers = new Map<string, NorbixWebhookHandler>();\n private defaultHandler?: NorbixWebhookHandler;\n\n constructor(private readonly options: NorbixWebhookReceiverOptions = {}) {}\n\n /** Handle a specific event name (e.g. membership.user.registered). */\n on(event: string, handler: NorbixWebhookHandler): this {\n this.handlers.set(event, handler);\n return this;\n }\n\n /** Fallback when no event-specific handler is registered. */\n onDefault(handler: NorbixWebhookHandler): this {\n this.defaultHandler = handler;\n return this;\n }\n\n /**\n * Verify (when secret configured), parse, and dispatch the delivery.\n * Returns 200-worthy result โ throw NorbixWebhookSignatureError for 401.\n */\n async handle(input: NorbixWebhookHandleInput): Promise<NorbixWebhookHandleResult> {\n const deliveryHeaders = parseNorbixWebhookHeaders(input.headers);\n const shouldVerify = input.verify !== false && !!this.options.secret;\n let verified: boolean | null = null;\n\n if (shouldVerify && this.options.secret) {\n const result = verifyNorbixWebhookSignature({\n secret: this.options.secret,\n rawBody: input.rawBody,\n signature: deliveryHeaders.signature,\n timestamp: deliveryHeaders.timestamp,\n toleranceSeconds: this.options.toleranceSeconds ?? 300,\n });\n if (!result.ok) {\n throw new NorbixWebhookSignatureError(result.reason ?? 'Invalid signature');\n }\n verified = true;\n }\n\n const envelope = parseNorbixWebhookEnvelope(input.rawBody);\n const ctx: NorbixWebhookContext = {\n path: input.path,\n headers: {\n ...deliveryHeaders,\n event: deliveryHeaders.event ?? envelope.event,\n deliveryId: deliveryHeaders.deliveryId ?? envelope.id,\n accountId: deliveryHeaders.accountId ?? envelope.accountId,\n projectId: deliveryHeaders.projectId ?? envelope.projectId,\n },\n verified,\n };\n\n const handler = this.handlers.get(envelope.event) ?? this.defaultHandler;\n let handled = false;\n if (handler) {\n await handler(envelope, ctx);\n handled = true;\n }\n\n return {\n received: true,\n event: envelope.event,\n deliveryId: envelope.id,\n verified,\n handled,\n triggerId: envelope.triggerId,\n };\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@norbix.ai/ts",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Official TypeScript SDK for Norbix โ one client for both API and Hub. Works in Node and the browser.",
|
|
5
5
|
"author": "UAB Isidos",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,6 +45,11 @@
|
|
|
45
45
|
"import": "./dist/hub/index.js",
|
|
46
46
|
"require": "./dist/hub/index.cjs"
|
|
47
47
|
},
|
|
48
|
+
"./webhooks": {
|
|
49
|
+
"types": "./dist/webhooks/index.d.ts",
|
|
50
|
+
"import": "./dist/webhooks/index.js",
|
|
51
|
+
"require": "./dist/webhooks/index.cjs"
|
|
52
|
+
},
|
|
48
53
|
"./types/api": {
|
|
49
54
|
"types": "./dist/types/api2.dtos.d.ts",
|
|
50
55
|
"import": "./dist/types/api2.dtos.js",
|