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
+ "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
+ };