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
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// 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 _optionalChain([this, 'access', _ => _.onAppAccessRevoked, 'optionalCall', _2 => _2(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 _optionalChain([this, 'access', _3 => _3.onAppAccessRevoked, 'optionalCall', _4 => _4(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
|
+
var _crypto = require('crypto'); var crypto = _interopRequireWildcard(_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 = _nullishCoalesce(options.maxTimestampAge, () => ( 300));
|
|
162
|
+
this.keysCacheTtl = _nullishCoalesce(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 (e2) {
|
|
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 _optionalChain([h, 'access', _5 => _5.onInviteCreated, 'optionalCall', _6 => _6(event)]);
|
|
336
|
+
case "invite.accepted":
|
|
337
|
+
return _optionalChain([h, 'access', _7 => _7.onInviteAccepted, 'optionalCall', _8 => _8(event)]);
|
|
338
|
+
case "invite.deleted":
|
|
339
|
+
return _optionalChain([h, 'access', _9 => _9.onInviteDeleted, 'optionalCall', _10 => _10(event)]);
|
|
340
|
+
case "invite.expired":
|
|
341
|
+
return _optionalChain([h, 'access', _11 => _11.onInviteExpired, 'optionalCall', _12 => _12(event)]);
|
|
342
|
+
// Subject events (users, service accounts, machines)
|
|
343
|
+
case "subject.created":
|
|
344
|
+
return _optionalChain([h, 'access', _13 => _13.onSubjectCreated, 'optionalCall', _14 => _14(event)]);
|
|
345
|
+
case "subject.updated":
|
|
346
|
+
return _optionalChain([h, 'access', _15 => _15.onSubjectUpdated, 'optionalCall', _16 => _16(event)]);
|
|
347
|
+
case "subject.deleted":
|
|
348
|
+
return _optionalChain([h, 'access', _17 => _17.onSubjectDeleted, 'optionalCall', _18 => _18(event)]);
|
|
349
|
+
case "subject.deactivated":
|
|
350
|
+
return _optionalChain([h, 'access', _19 => _19.onSubjectDeactivated, 'optionalCall', _20 => _20(event)]);
|
|
351
|
+
// Member events
|
|
352
|
+
case "member.joined":
|
|
353
|
+
return _optionalChain([h, 'access', _21 => _21.onMemberJoined, 'optionalCall', _22 => _22(event)]);
|
|
354
|
+
case "member.left":
|
|
355
|
+
return _optionalChain([h, 'access', _23 => _23.onMemberLeft, 'optionalCall', _24 => _24(event)]);
|
|
356
|
+
case "member.role_changed":
|
|
357
|
+
return _optionalChain([h, 'access', _25 => _25.onMemberRoleChanged, 'optionalCall', _26 => _26(event)]);
|
|
358
|
+
case "member.suspended":
|
|
359
|
+
return _optionalChain([h, 'access', _27 => _27.onMemberSuspended, 'optionalCall', _28 => _28(event)]);
|
|
360
|
+
case "member.activated":
|
|
361
|
+
return _optionalChain([h, 'access', _29 => _29.onMemberActivated, 'optionalCall', _30 => _30(event)]);
|
|
362
|
+
// App access events
|
|
363
|
+
case "app_access.granted":
|
|
364
|
+
return _optionalChain([h, 'access', _31 => _31.onAppAccessGranted, 'optionalCall', _32 => _32(event)]);
|
|
365
|
+
case "app_access.revoked":
|
|
366
|
+
return _optionalChain([h, 'access', _33 => _33.onAppAccessRevoked, 'optionalCall', _34 => _34(event)]);
|
|
367
|
+
case "app_access.role_changed":
|
|
368
|
+
return _optionalChain([h, 'access', _35 => _35.onAppAccessRoleChanged, 'optionalCall', _36 => _36(event)]);
|
|
369
|
+
// License events
|
|
370
|
+
case "license.assigned":
|
|
371
|
+
return _optionalChain([h, 'access', _37 => _37.onLicenseAssigned, 'optionalCall', _38 => _38(event)]);
|
|
372
|
+
case "license.revoked":
|
|
373
|
+
return _optionalChain([h, 'access', _39 => _39.onLicenseRevoked, 'optionalCall', _40 => _40(event)]);
|
|
374
|
+
case "license.changed":
|
|
375
|
+
return _optionalChain([h, 'access', _41 => _41.onLicenseChanged, 'optionalCall', _42 => _42(event)]);
|
|
376
|
+
default:
|
|
377
|
+
return _optionalChain([h, 'access', _43 => _43.onUnhandledEvent, 'optionalCall', _44 => _44(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
|
+
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
exports.SYNC_EVENT_TYPES = SYNC_EVENT_TYPES; exports.isInviteEvent = isInviteEvent; exports.isSubjectEvent = isSubjectEvent; exports.isMemberEvent = isMemberEvent; exports.isAppAccessEvent = isAppAccessEvent; exports.isLicenseEvent = isLicenseEvent; exports.InviteEventHandler = InviteEventHandler; exports.SubjectEventHandler = SubjectEventHandler; exports.MemberEventHandler = MemberEventHandler; exports.AppAccessEventHandler = AppAccessEventHandler; exports.LicenseEventHandler = LicenseEventHandler; exports.AuthVitalEventHandler = AuthVitalEventHandler; exports.AuthVitalWebhooks = AuthVitalWebhooks; exports.WebhookRouter = WebhookRouter;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// src/client/oauth.ts
|
|
2
|
+
function encodeState(csrf, codeVerifier) {
|
|
3
|
+
const encodedVerifier = btoa(codeVerifier).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
4
|
+
return `${csrf}:${encodedVerifier}`;
|
|
5
|
+
}
|
|
6
|
+
function decodeState(state) {
|
|
7
|
+
const colonIndex = state.indexOf(":");
|
|
8
|
+
if (colonIndex === -1) return null;
|
|
9
|
+
const csrf = state.substring(0, colonIndex);
|
|
10
|
+
const encodedVerifier = state.substring(colonIndex + 1);
|
|
11
|
+
try {
|
|
12
|
+
const padded = encodedVerifier + "===".slice(0, (4 - encodedVerifier.length % 4) % 4);
|
|
13
|
+
const codeVerifier = atob(padded.replace(/-/g, "+").replace(/_/g, "/"));
|
|
14
|
+
return { csrf, codeVerifier };
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function loginToAuthVital(authVitalHost, options) {
|
|
20
|
+
const screen = options?.screen || "login";
|
|
21
|
+
const page = screen === "signup" ? "/auth/signup" : "/auth/login";
|
|
22
|
+
const url = new URL(`${authVitalHost}${page}`);
|
|
23
|
+
if (options?.clientId) {
|
|
24
|
+
url.searchParams.set("client_id", options.clientId);
|
|
25
|
+
}
|
|
26
|
+
window.location.href = url.toString();
|
|
27
|
+
}
|
|
28
|
+
function signupAtAuthVital(authVitalHost, options) {
|
|
29
|
+
loginToAuthVital(authVitalHost, { ...options, screen: "signup" });
|
|
30
|
+
}
|
|
31
|
+
function generateCodeVerifier() {
|
|
32
|
+
const array = new Uint8Array(32);
|
|
33
|
+
crypto.getRandomValues(array);
|
|
34
|
+
return base64UrlEncode(array);
|
|
35
|
+
}
|
|
36
|
+
async function generateCodeChallenge(verifier) {
|
|
37
|
+
const encoder = new TextEncoder();
|
|
38
|
+
const data = encoder.encode(verifier);
|
|
39
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
40
|
+
return base64UrlEncode(new Uint8Array(digest));
|
|
41
|
+
}
|
|
42
|
+
function base64UrlEncode(buffer) {
|
|
43
|
+
let binary = "";
|
|
44
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
45
|
+
binary += String.fromCharCode(buffer[i]);
|
|
46
|
+
}
|
|
47
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
48
|
+
}
|
|
49
|
+
var LOGIN_ATTEMPT_KEY = "authvital_login_attempt";
|
|
50
|
+
var LOGIN_COOLDOWN_MS = 5e3;
|
|
51
|
+
function canAttemptLogin() {
|
|
52
|
+
const lastAttempt = sessionStorage.getItem(LOGIN_ATTEMPT_KEY);
|
|
53
|
+
if (!lastAttempt) return true;
|
|
54
|
+
return Date.now() - parseInt(lastAttempt, 10) > LOGIN_COOLDOWN_MS;
|
|
55
|
+
}
|
|
56
|
+
function recordLoginAttempt() {
|
|
57
|
+
sessionStorage.setItem(LOGIN_ATTEMPT_KEY, Date.now().toString());
|
|
58
|
+
}
|
|
59
|
+
function clearLoginAttempt() {
|
|
60
|
+
sessionStorage.removeItem(LOGIN_ATTEMPT_KEY);
|
|
61
|
+
}
|
|
62
|
+
async function startAuthorizationFlow(config, params = {}) {
|
|
63
|
+
if (!canAttemptLogin()) {
|
|
64
|
+
throw new Error("Too many login attempts. Please wait a moment and try again.");
|
|
65
|
+
}
|
|
66
|
+
recordLoginAttempt();
|
|
67
|
+
const codeVerifier = generateCodeVerifier();
|
|
68
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
69
|
+
const csrf = params.state || generateCodeVerifier().substring(0, 16);
|
|
70
|
+
const state = encodeState(csrf, codeVerifier);
|
|
71
|
+
const authorizeUrl = new URL(`${config.authVitalHost}/oauth/authorize`);
|
|
72
|
+
authorizeUrl.searchParams.set("client_id", config.clientId);
|
|
73
|
+
authorizeUrl.searchParams.set("redirect_uri", config.redirectUri);
|
|
74
|
+
authorizeUrl.searchParams.set("response_type", "code");
|
|
75
|
+
authorizeUrl.searchParams.set("scope", config.scope || "openid profile email");
|
|
76
|
+
authorizeUrl.searchParams.set("state", state);
|
|
77
|
+
authorizeUrl.searchParams.set("code_challenge", codeChallenge);
|
|
78
|
+
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
79
|
+
if (params.nonce) {
|
|
80
|
+
authorizeUrl.searchParams.set("nonce", params.nonce);
|
|
81
|
+
}
|
|
82
|
+
if (params.screen === "signup") {
|
|
83
|
+
authorizeUrl.searchParams.set("screen", "signup");
|
|
84
|
+
}
|
|
85
|
+
window.location.href = authorizeUrl.toString();
|
|
86
|
+
}
|
|
87
|
+
async function startLogin(options) {
|
|
88
|
+
await startAuthorizationFlow(
|
|
89
|
+
{
|
|
90
|
+
authVitalHost: options.authVitalHost,
|
|
91
|
+
clientId: options.clientId,
|
|
92
|
+
redirectUri: options.redirectUri,
|
|
93
|
+
scope: options.scope
|
|
94
|
+
},
|
|
95
|
+
{ state: options.state }
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
async function startSignup(options) {
|
|
99
|
+
await startAuthorizationFlow(
|
|
100
|
+
{
|
|
101
|
+
authVitalHost: options.authVitalHost,
|
|
102
|
+
clientId: options.clientId,
|
|
103
|
+
redirectUri: options.redirectUri,
|
|
104
|
+
scope: options.scope
|
|
105
|
+
},
|
|
106
|
+
{ state: options.state, screen: "signup" }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
var callbackInProgress = null;
|
|
110
|
+
async function handleCallback(config) {
|
|
111
|
+
if (callbackInProgress) {
|
|
112
|
+
await callbackInProgress;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const params = new URLSearchParams(window.location.search);
|
|
116
|
+
const code = params.get("code");
|
|
117
|
+
const state = params.get("state");
|
|
118
|
+
const error = params.get("error");
|
|
119
|
+
const errorDescription = params.get("error_description");
|
|
120
|
+
if (error) {
|
|
121
|
+
throw new Error(errorDescription || error);
|
|
122
|
+
}
|
|
123
|
+
if (!code) {
|
|
124
|
+
throw new Error("No authorization code received");
|
|
125
|
+
}
|
|
126
|
+
if (!state) {
|
|
127
|
+
throw new Error("No state parameter received");
|
|
128
|
+
}
|
|
129
|
+
const decoded = decodeState(state);
|
|
130
|
+
if (!decoded) {
|
|
131
|
+
throw new Error("Invalid state format - could not extract PKCE verifier");
|
|
132
|
+
}
|
|
133
|
+
callbackInProgress = exchangeCodeForTokens(config, code, decoded.codeVerifier);
|
|
134
|
+
try {
|
|
135
|
+
await callbackInProgress;
|
|
136
|
+
window.history.replaceState({}, "", window.location.pathname);
|
|
137
|
+
clearLoginAttempt();
|
|
138
|
+
} finally {
|
|
139
|
+
callbackInProgress = null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function extractCallbackParams() {
|
|
143
|
+
const params = new URLSearchParams(window.location.search);
|
|
144
|
+
const code = params.get("code");
|
|
145
|
+
const state = params.get("state");
|
|
146
|
+
const error = params.get("error");
|
|
147
|
+
const errorDescription = params.get("error_description");
|
|
148
|
+
if (error) {
|
|
149
|
+
return { code: null, codeVerifier: null, csrf: null, error, errorDescription };
|
|
150
|
+
}
|
|
151
|
+
if (!state) {
|
|
152
|
+
return { code, codeVerifier: null, csrf: null, error: "missing_state", errorDescription: "No state parameter" };
|
|
153
|
+
}
|
|
154
|
+
const decoded = decodeState(state);
|
|
155
|
+
if (!decoded) {
|
|
156
|
+
return { code, codeVerifier: null, csrf: null, error: "invalid_state", errorDescription: "Could not decode state" };
|
|
157
|
+
}
|
|
158
|
+
return { code, codeVerifier: decoded.codeVerifier, csrf: decoded.csrf, error: null, errorDescription: null };
|
|
159
|
+
}
|
|
160
|
+
async function exchangeCodeForTokens(config, code, codeVerifier) {
|
|
161
|
+
const response = await fetch(`${config.authVitalHost}/oauth/token`, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: { "Content-Type": "application/json" },
|
|
164
|
+
credentials: "include",
|
|
165
|
+
// Important: receive and send cookies
|
|
166
|
+
body: JSON.stringify({
|
|
167
|
+
grant_type: "authorization_code",
|
|
168
|
+
code,
|
|
169
|
+
redirect_uri: config.redirectUri,
|
|
170
|
+
client_id: config.clientId,
|
|
171
|
+
code_verifier: codeVerifier
|
|
172
|
+
})
|
|
173
|
+
});
|
|
174
|
+
if (!response.ok) {
|
|
175
|
+
const error = await response.json().catch(() => ({}));
|
|
176
|
+
throw new Error(error.message || "Token exchange failed");
|
|
177
|
+
}
|
|
178
|
+
return response.json();
|
|
179
|
+
}
|
|
180
|
+
async function checkAuthStatus(authVitalHost) {
|
|
181
|
+
try {
|
|
182
|
+
const response = await fetch(`${authVitalHost}/api/auth/me`, {
|
|
183
|
+
method: "GET",
|
|
184
|
+
credentials: "include"
|
|
185
|
+
// Send cookies
|
|
186
|
+
});
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
return { authenticated: false };
|
|
189
|
+
}
|
|
190
|
+
const data = await response.json();
|
|
191
|
+
return {
|
|
192
|
+
authenticated: data.authenticated ?? false,
|
|
193
|
+
user: data.user
|
|
194
|
+
};
|
|
195
|
+
} catch {
|
|
196
|
+
return { authenticated: false };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function logout(authVitalHost, options) {
|
|
200
|
+
sessionStorage.removeItem(LOGIN_ATTEMPT_KEY);
|
|
201
|
+
const logoutUrl = new URL(`${authVitalHost}/api/auth/logout/redirect`);
|
|
202
|
+
if (options?.postLogoutRedirectUri) {
|
|
203
|
+
logoutUrl.searchParams.set("post_logout_redirect_uri", options.postLogoutRedirectUri);
|
|
204
|
+
}
|
|
205
|
+
window.location.href = logoutUrl.toString();
|
|
206
|
+
}
|
|
207
|
+
function decodeJwt(token) {
|
|
208
|
+
try {
|
|
209
|
+
const parts = token.split(".");
|
|
210
|
+
if (parts.length !== 3) return null;
|
|
211
|
+
const payload = parts[1];
|
|
212
|
+
const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
|
|
213
|
+
return JSON.parse(decoded);
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export {
|
|
220
|
+
encodeState,
|
|
221
|
+
decodeState,
|
|
222
|
+
loginToAuthVital,
|
|
223
|
+
signupAtAuthVital,
|
|
224
|
+
generateCodeVerifier,
|
|
225
|
+
generateCodeChallenge,
|
|
226
|
+
startAuthorizationFlow,
|
|
227
|
+
startLogin,
|
|
228
|
+
startSignup,
|
|
229
|
+
handleCallback,
|
|
230
|
+
extractCallbackParams,
|
|
231
|
+
exchangeCodeForTokens,
|
|
232
|
+
checkAuthStatus,
|
|
233
|
+
logout,
|
|
234
|
+
decodeJwt
|
|
235
|
+
};
|