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.
- package/README.md +657 -0
- package/dist/chunk-ETJ5ICJ7.mjs +412 -0
- package/dist/chunk-FXVD4Y5G.js +412 -0
- package/dist/chunk-JNEJMHGA.mjs +235 -0
- package/dist/chunk-JPODZIZT.mjs +95 -0
- package/dist/chunk-QPYBK2J4.js +235 -0
- package/dist/chunk-R4OHZZQP.js +95 -0
- package/dist/index.d.mts +615 -0
- package/dist/index.d.ts +615 -0
- package/dist/index.js +1299 -0
- package/dist/index.mjs +1299 -0
- package/dist/invitations-EFJA5C6L.mjs +22 -0
- package/dist/invitations-LZHJ3AZY.js +22 -0
- package/dist/oauth-K7E7OCWI.js +34 -0
- package/dist/oauth-UAFXEKZ7.mjs +34 -0
- package/dist/server.d.mts +2656 -0
- package/dist/server.d.ts +2656 -0
- package/dist/server.js +2915 -0
- package/dist/server.mjs +2915 -0
- package/dist/webhook-router-DdfXLtHa.d.mts +461 -0
- package/dist/webhook-router-DdfXLtHa.d.ts +461 -0
- package/package.json +71 -0
|
@@ -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
|
+
};
|