authvital-sdk 0.1.1-dev.3.cefb119.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,412 @@
1
+ // src/webhooks/types.ts
2
+ var SYNC_EVENT_TYPES = {
3
+ // Invitations
4
+ INVITE_CREATED: "invite.created",
5
+ INVITE_ACCEPTED: "invite.accepted",
6
+ INVITE_DELETED: "invite.deleted",
7
+ INVITE_EXPIRED: "invite.expired",
8
+ // Subjects (users, service accounts, etc.)
9
+ SUBJECT_CREATED: "subject.created",
10
+ SUBJECT_UPDATED: "subject.updated",
11
+ SUBJECT_DELETED: "subject.deleted",
12
+ SUBJECT_DEACTIVATED: "subject.deactivated",
13
+ // Memberships
14
+ MEMBER_JOINED: "member.joined",
15
+ MEMBER_LEFT: "member.left",
16
+ MEMBER_ROLE_CHANGED: "member.role_changed",
17
+ MEMBER_SUSPENDED: "member.suspended",
18
+ MEMBER_ACTIVATED: "member.activated",
19
+ // App Access
20
+ APP_ACCESS_GRANTED: "app_access.granted",
21
+ APP_ACCESS_REVOKED: "app_access.revoked",
22
+ APP_ACCESS_ROLE_CHANGED: "app_access.role_changed",
23
+ // Licenses
24
+ LICENSE_ASSIGNED: "license.assigned",
25
+ LICENSE_REVOKED: "license.revoked",
26
+ LICENSE_CHANGED: "license.changed"
27
+ };
28
+ function isInviteEvent(event) {
29
+ return event.type.startsWith("invite.");
30
+ }
31
+ function isSubjectEvent(event) {
32
+ return event.type.startsWith("subject.");
33
+ }
34
+ function isMemberEvent(event) {
35
+ return event.type.startsWith("member.");
36
+ }
37
+ function isAppAccessEvent(event) {
38
+ return event.type.startsWith("app_access.");
39
+ }
40
+ function isLicenseEvent(event) {
41
+ return event.type.startsWith("license.");
42
+ }
43
+
44
+ // src/webhooks/event-interfaces.ts
45
+ var InviteEventHandler = class {
46
+ onInviteCreated(_event) {
47
+ }
48
+ onInviteAccepted(_event) {
49
+ }
50
+ onInviteDeleted(_event) {
51
+ }
52
+ onInviteExpired(_event) {
53
+ }
54
+ };
55
+ var SubjectEventHandler = class {
56
+ onSubjectCreated(_event) {
57
+ }
58
+ onSubjectUpdated(_event) {
59
+ }
60
+ onSubjectDeleted(_event) {
61
+ }
62
+ onSubjectDeactivated(_event) {
63
+ }
64
+ };
65
+ var MemberEventHandler = class {
66
+ onMemberJoined(_event) {
67
+ }
68
+ onMemberLeft(_event) {
69
+ }
70
+ onMemberRoleChanged(_event) {
71
+ }
72
+ onMemberSuspended(_event) {
73
+ }
74
+ onMemberActivated(_event) {
75
+ }
76
+ };
77
+ var AppAccessEventHandler = class {
78
+ onAppAccessGranted(_event) {
79
+ }
80
+ onAppAccessRevoked(_event) {
81
+ }
82
+ /**
83
+ * @deprecated Use onAppAccessRevoked instead. This method exists for backwards compatibility.
84
+ */
85
+ onAppAccessDeactivated(event) {
86
+ return this.onAppAccessRevoked?.(event);
87
+ }
88
+ onAppAccessRoleChanged(_event) {
89
+ }
90
+ };
91
+ var LicenseEventHandler = class {
92
+ onLicenseAssigned(_event) {
93
+ }
94
+ onLicenseRevoked(_event) {
95
+ }
96
+ onLicenseChanged(_event) {
97
+ }
98
+ };
99
+ var AuthVitalEventHandler = class {
100
+ // Invite events
101
+ onInviteCreated(_event) {
102
+ }
103
+ onInviteAccepted(_event) {
104
+ }
105
+ onInviteDeleted(_event) {
106
+ }
107
+ onInviteExpired(_event) {
108
+ }
109
+ // Subject events
110
+ onSubjectCreated(_event) {
111
+ }
112
+ onSubjectUpdated(_event) {
113
+ }
114
+ onSubjectDeleted(_event) {
115
+ }
116
+ onSubjectDeactivated(_event) {
117
+ }
118
+ // Member events
119
+ onMemberJoined(_event) {
120
+ }
121
+ onMemberLeft(_event) {
122
+ }
123
+ onMemberRoleChanged(_event) {
124
+ }
125
+ onMemberSuspended(_event) {
126
+ }
127
+ onMemberActivated(_event) {
128
+ }
129
+ // App access events
130
+ onAppAccessGranted(_event) {
131
+ }
132
+ onAppAccessRevoked(_event) {
133
+ }
134
+ /**
135
+ * @deprecated Use onAppAccessRevoked instead. This method exists for backwards compatibility.
136
+ */
137
+ onAppAccessDeactivated(event) {
138
+ return this.onAppAccessRevoked?.(event);
139
+ }
140
+ onAppAccessRoleChanged(_event) {
141
+ }
142
+ // License events
143
+ onLicenseAssigned(_event) {
144
+ }
145
+ onLicenseRevoked(_event) {
146
+ }
147
+ onLicenseChanged(_event) {
148
+ }
149
+ // Catch-all
150
+ onUnhandledEvent(_event) {
151
+ }
152
+ };
153
+
154
+ // src/webhooks/webhook-handler.ts
155
+ import * as crypto from "crypto";
156
+ var AuthVitalWebhooks = class {
157
+ constructor(options) {
158
+ this.keysCache = /* @__PURE__ */ new Map();
159
+ this.keysCacheExpiry = 0;
160
+ this.jwksUrl = options.jwksUrl;
161
+ this.maxTimestampAge = options.maxTimestampAge ?? 300;
162
+ this.keysCacheTtl = options.keysCacheTtl ?? 36e5;
163
+ }
164
+ /**
165
+ * Verify and parse a webhook payload
166
+ *
167
+ * @param body - The raw request body (string or object)
168
+ * @param headers - The request headers (can pass full headers object)
169
+ * @returns The parsed and verified event
170
+ * @throws Error if verification fails
171
+ */
172
+ async verifyAndParse(body, headers) {
173
+ const normalizedHeaders = this.normalizeHeaders(headers);
174
+ const signature = normalizedHeaders["x-authvital-signature"];
175
+ const keyId = normalizedHeaders["x-authvital-key-id"];
176
+ const timestamp = normalizedHeaders["x-authvital-timestamp"];
177
+ if (!signature) {
178
+ throw new Error("Missing X-AuthVital-Signature header");
179
+ }
180
+ if (!keyId) {
181
+ throw new Error("Missing X-AuthVital-Key-Id header");
182
+ }
183
+ if (!timestamp) {
184
+ throw new Error("Missing X-AuthVital-Timestamp header");
185
+ }
186
+ const timestampNum = parseInt(timestamp, 10);
187
+ if (isNaN(timestampNum)) {
188
+ throw new Error("Invalid timestamp format");
189
+ }
190
+ const now = Math.floor(Date.now() / 1e3);
191
+ const age = now - timestampNum;
192
+ if (age > this.maxTimestampAge) {
193
+ throw new Error(`Timestamp too old: ${age}s (max ${this.maxTimestampAge}s)`);
194
+ }
195
+ if (age < -60) {
196
+ throw new Error("Timestamp is in the future");
197
+ }
198
+ const bodyString = typeof body === "string" ? body : JSON.stringify(body);
199
+ const signedPayload = `${timestamp}.${bodyString}`;
200
+ const publicKey = await this.getPublicKey(keyId);
201
+ const isValid = crypto.verify(
202
+ "RSA-SHA256",
203
+ Buffer.from(signedPayload),
204
+ publicKey,
205
+ Buffer.from(signature, "base64")
206
+ );
207
+ if (!isValid) {
208
+ throw new Error("Invalid signature");
209
+ }
210
+ const event = typeof body === "string" ? JSON.parse(body) : body;
211
+ return event;
212
+ }
213
+ /**
214
+ * Verify signature only (without parsing)
215
+ * Useful if you want to handle parsing yourself
216
+ */
217
+ async verify(body, signature, keyId, timestamp) {
218
+ try {
219
+ const timestampNum = parseInt(timestamp, 10);
220
+ const now = Math.floor(Date.now() / 1e3);
221
+ const age = now - timestampNum;
222
+ if (age > this.maxTimestampAge || age < -60) {
223
+ return false;
224
+ }
225
+ const bodyString = typeof body === "string" ? body : JSON.stringify(body);
226
+ const signedPayload = `${timestamp}.${bodyString}`;
227
+ const publicKey = await this.getPublicKey(keyId);
228
+ return crypto.verify(
229
+ "RSA-SHA256",
230
+ Buffer.from(signedPayload),
231
+ publicKey,
232
+ Buffer.from(signature, "base64")
233
+ );
234
+ } catch {
235
+ return false;
236
+ }
237
+ }
238
+ /**
239
+ * Get a public key from the JWKS endpoint
240
+ */
241
+ async getPublicKey(kid) {
242
+ const now = Date.now();
243
+ if (this.keysCache.has(kid) && now < this.keysCacheExpiry) {
244
+ return this.keysCache.get(kid);
245
+ }
246
+ const response = await fetch(this.jwksUrl);
247
+ if (!response.ok) {
248
+ throw new Error(`Failed to fetch JWKS: ${response.status}`);
249
+ }
250
+ const jwks = await response.json();
251
+ this.keysCache.clear();
252
+ this.keysCacheExpiry = now + this.keysCacheTtl;
253
+ for (const key of jwks.keys) {
254
+ if (key.kty === "RSA" && key.n && key.e) {
255
+ const publicKey2 = crypto.createPublicKey({
256
+ key: {
257
+ kty: key.kty,
258
+ n: key.n,
259
+ e: key.e
260
+ },
261
+ format: "jwk"
262
+ });
263
+ this.keysCache.set(key.kid, publicKey2);
264
+ }
265
+ }
266
+ const publicKey = this.keysCache.get(kid);
267
+ if (!publicKey) {
268
+ throw new Error(`Key not found: ${kid}`);
269
+ }
270
+ return publicKey;
271
+ }
272
+ /**
273
+ * Normalize headers to lowercase keys and string values
274
+ */
275
+ normalizeHeaders(headers) {
276
+ const normalized = {};
277
+ for (const [key, value] of Object.entries(headers)) {
278
+ const normalizedKey = key.toLowerCase();
279
+ normalized[normalizedKey] = Array.isArray(value) ? value[0] : value;
280
+ }
281
+ return normalized;
282
+ }
283
+ /**
284
+ * Clear the key cache (useful for testing or key rotation)
285
+ */
286
+ clearCache() {
287
+ this.keysCache.clear();
288
+ this.keysCacheExpiry = 0;
289
+ }
290
+ };
291
+
292
+ // src/webhooks/webhook-router.ts
293
+ var WebhookRouter = class {
294
+ constructor(options) {
295
+ const authVitalHost = options.authVitalHost || process.env.AV_HOST;
296
+ if (!authVitalHost) {
297
+ throw new Error(
298
+ "AuthVital URL is required. Either pass authVitalHost in options or set AV_HOST environment variable."
299
+ );
300
+ }
301
+ const jwksUrl = `${authVitalHost.replace(/\/$/, "")}/.well-known/jwks.json`;
302
+ this.verifier = new AuthVitalWebhooks({
303
+ jwksUrl,
304
+ maxTimestampAge: options.maxTimestampAge,
305
+ keysCacheTtl: options.keysCacheTtl
306
+ });
307
+ this.handler = options.handler;
308
+ }
309
+ /**
310
+ * Handle a webhook request
311
+ */
312
+ async handle(body, headers) {
313
+ try {
314
+ const event = await this.verifier.verifyAndParse(body, headers);
315
+ await this.dispatch(event);
316
+ return {
317
+ status: 200,
318
+ body: { success: true, message: `Processed ${event.type}` }
319
+ };
320
+ } catch (error) {
321
+ return {
322
+ status: 400,
323
+ body: { success: false, error: error.message || "Webhook processing failed" }
324
+ };
325
+ }
326
+ }
327
+ /**
328
+ * Dispatch event to the appropriate handler method
329
+ */
330
+ async dispatch(event) {
331
+ const h = this.handler;
332
+ switch (event.type) {
333
+ // Invite events
334
+ case "invite.created":
335
+ return h.onInviteCreated?.(event);
336
+ case "invite.accepted":
337
+ return h.onInviteAccepted?.(event);
338
+ case "invite.deleted":
339
+ return h.onInviteDeleted?.(event);
340
+ case "invite.expired":
341
+ return h.onInviteExpired?.(event);
342
+ // Subject events (users, service accounts, machines)
343
+ case "subject.created":
344
+ return h.onSubjectCreated?.(event);
345
+ case "subject.updated":
346
+ return h.onSubjectUpdated?.(event);
347
+ case "subject.deleted":
348
+ return h.onSubjectDeleted?.(event);
349
+ case "subject.deactivated":
350
+ return h.onSubjectDeactivated?.(event);
351
+ // Member events
352
+ case "member.joined":
353
+ return h.onMemberJoined?.(event);
354
+ case "member.left":
355
+ return h.onMemberLeft?.(event);
356
+ case "member.role_changed":
357
+ return h.onMemberRoleChanged?.(event);
358
+ case "member.suspended":
359
+ return h.onMemberSuspended?.(event);
360
+ case "member.activated":
361
+ return h.onMemberActivated?.(event);
362
+ // App access events
363
+ case "app_access.granted":
364
+ return h.onAppAccessGranted?.(event);
365
+ case "app_access.revoked":
366
+ return h.onAppAccessRevoked?.(event);
367
+ case "app_access.role_changed":
368
+ return h.onAppAccessRoleChanged?.(event);
369
+ // License events
370
+ case "license.assigned":
371
+ return h.onLicenseAssigned?.(event);
372
+ case "license.revoked":
373
+ return h.onLicenseRevoked?.(event);
374
+ case "license.changed":
375
+ return h.onLicenseChanged?.(event);
376
+ default:
377
+ return h.onUnhandledEvent?.(event);
378
+ }
379
+ }
380
+ /**
381
+ * Express middleware handler
382
+ */
383
+ expressHandler() {
384
+ return async (req, res) => {
385
+ const result = await this.handle(req.body, req.headers);
386
+ res.status(result.status).json(result.body);
387
+ };
388
+ }
389
+ /**
390
+ * Get the underlying verifier
391
+ */
392
+ getVerifier() {
393
+ return this.verifier;
394
+ }
395
+ };
396
+
397
+ export {
398
+ SYNC_EVENT_TYPES,
399
+ isInviteEvent,
400
+ isSubjectEvent,
401
+ isMemberEvent,
402
+ isAppAccessEvent,
403
+ isLicenseEvent,
404
+ InviteEventHandler,
405
+ SubjectEventHandler,
406
+ MemberEventHandler,
407
+ AppAccessEventHandler,
408
+ LicenseEventHandler,
409
+ AuthVitalEventHandler,
410
+ AuthVitalWebhooks,
411
+ WebhookRouter
412
+ };