@gosso/client 0.2.0 → 0.4.0-rc.2

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/dist/index.js CHANGED
@@ -1,723 +1,5 @@
1
- const REFRESH_LOCK_TTL_MS = 15_000;
2
- const REFRESH_WAIT_TIMEOUT_MS = 20_000;
3
- const REFRESH_WAIT_POLL_MS = 100;
4
- const REFRESH_WEB_LOCK_NAME = 'gosso-auth-refresh';
5
- const defaultConfig = {
6
- scope: 'openid profile email',
7
- postLoginDefaultPath: '/',
8
- loginPath: '/login',
9
- storagePrefix: 'gosso',
10
- };
11
- function normalizeBaseUrl(url) {
12
- return url.replace(/\/+$/, '');
13
- }
14
- export function generateRandomString(length) {
15
- const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
16
- let text = '';
17
- const cryptoObj = (typeof window !== 'undefined' ? window.crypto : null) || (typeof globalThis !== 'undefined' ? globalThis.crypto : null);
18
- if (cryptoObj && cryptoObj.getRandomValues) {
19
- const array = new Uint8Array(length);
20
- cryptoObj.getRandomValues(array);
21
- for (let i = 0; i < length; i += 1) {
22
- text += possible.charAt(array[i] % possible.length);
23
- }
24
- }
25
- else {
26
- // Fallback for environments lacking CSPRNG
27
- for (let i = 0; i < length; i += 1) {
28
- text += possible.charAt(Math.floor(Math.random() * possible.length));
29
- }
30
- }
31
- return text;
32
- }
33
- async function generateCodeChallenge(verifier) {
34
- const data = new TextEncoder().encode(verifier);
35
- const digest = await window.crypto.subtle.digest('SHA-256', data);
36
- return bufferToBase64URL(digest);
37
- }
38
- function cookieSecureAttribute() {
39
- return typeof location !== 'undefined' && location.protocol === 'https:' ? '; Secure' : '';
40
- }
41
- function getCookieName(baseName) {
42
- return typeof location !== 'undefined' && location.protocol === 'https:' ? `__Secure-${baseName}` : baseName;
43
- }
44
- function readCSRFToken(preferredName) {
45
- for (const raw of document.cookie.split(';')) {
46
- const [name, ...value] = raw.trim().split('=');
47
- if (name === preferredName || name === '__Host-csrf_token' || name === 'csrf_token' || name === 'blog_csrf_token')
48
- return decodeURIComponent(value.join('='));
49
- }
50
- return null;
51
- }
52
- function readClaimsFromAccessToken(accessToken) {
53
- try {
54
- const payloadBase64 = accessToken.split('.')[1];
55
- const base64 = payloadBase64.replace(/-/g, '+').replace(/_/g, '/');
56
- const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
57
- return JSON.parse(decodeURIComponent(escape(atob(padded))));
58
- }
59
- catch {
60
- return null;
61
- }
62
- }
63
- function readRolesFromAccessToken(accessToken) {
64
- const payload = readClaimsFromAccessToken(accessToken);
65
- return Array.isArray(payload?.roles) ? payload.roles : undefined;
66
- }
67
- function readScopeFromAccessToken(accessToken) {
68
- const payload = readClaimsFromAccessToken(accessToken);
69
- return typeof payload?.scope === 'string' ? payload.scope : undefined;
70
- }
71
- function hasAdminAccess(profile, accessToken) {
72
- const hasAdminRole = profile?.roles?.includes('admin') || false;
73
- const scope = accessToken ? readScopeFromAccessToken(accessToken) : profile?.scope;
74
- return hasAdminRole && Boolean(scope?.split(/\s+/).includes('admin'));
75
- }
76
- function parseRefreshLock(raw) {
77
- if (!raw)
78
- return null;
79
- try {
80
- const lock = JSON.parse(raw);
81
- if (typeof lock.owner !== 'string' || typeof lock.expiresAt !== 'number') {
82
- return null;
83
- }
84
- return { owner: lock.owner, expiresAt: lock.expiresAt };
85
- }
86
- catch {
87
- return null;
88
- }
89
- }
90
- function generateRefreshOwner() {
91
- if (window.crypto?.randomUUID) {
92
- return window.crypto.randomUUID();
93
- }
94
- return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
95
- }
96
- async function parseJsonEnvelope(response, fallbackMessage) {
97
- const body = (await response.json());
98
- if (!response.ok) {
99
- throw new Error(body.message || fallbackMessage);
100
- }
101
- return body.data;
102
- }
103
- export function bufferToBase64URL(buffer) {
104
- const bytes = new Uint8Array(buffer);
105
- let binary = '';
106
- for (let i = 0; i < bytes.byteLength; i += 1) {
107
- binary += String.fromCharCode(bytes[i]);
108
- }
109
- return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
110
- }
111
- export function base64URLToBuffer(base64URLString) {
112
- const base64 = base64URLString.replace(/-/g, '+').replace(/_/g, '/');
113
- const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
114
- const binary = atob(padded);
115
- const bytes = new Uint8Array(binary.length);
116
- for (let i = 0; i < binary.length; i += 1) {
117
- bytes[i] = binary.charCodeAt(i);
118
- }
119
- return bytes.buffer;
120
- }
121
- export function createGossoClient(inputConfig) {
122
- const config = {
123
- ...defaultConfig,
124
- ...inputConfig,
125
- issuer: normalizeBaseUrl(inputConfig.issuer),
126
- };
127
- const cookieSession = config.sessionMode === 'cookie';
128
- const flowStorage = cookieSession ? sessionStorage : localStorage;
129
- const fetcher = config.fetchImpl || fetch.bind(window);
130
- const key = (name) => `${config.storagePrefix}:${name}`;
131
- const storageKeys = {
132
- accessToken: key('access_token'),
133
- refreshToken: key('refresh_token'),
134
- userProfile: key('user_profile'),
135
- pkceVerifier: key('pkce_verifier'),
136
- authState: key('auth_state'),
137
- postLoginRedirect: key('post_login_redirect'),
138
- tokenIssuedAt: key('token_issued_at'),
139
- tokenExpiresIn: key('token_expires_in'),
140
- refreshLock: key('auth_refresh_lock'),
141
- };
142
- let refreshPromise = null;
143
- const setCookie = (name, value, maxAgeSeconds) => {
144
- const cookieName = getCookieName(name);
145
- document.cookie = `${cookieName}=${value}; path=/; max-age=${maxAgeSeconds}; SameSite=Lax${cookieSecureAttribute()}`;
146
- };
147
- const deleteCookie = (name) => {
148
- const cookieName = getCookieName(name);
149
- document.cookie = `${cookieName}=; path=/; max-age=-1; SameSite=Lax`;
150
- };
151
- const readProfile = () => {
152
- const storage = cookieSession ? sessionStorage : localStorage;
153
- const profile = storage.getItem(storageKeys.userProfile);
154
- if (!profile)
155
- return null;
156
- try {
157
- return JSON.parse(profile);
158
- }
159
- catch {
160
- return null;
161
- }
162
- };
163
- const getAccessToken = () => cookieSession ? null : localStorage.getItem(storageKeys.accessToken);
164
- const getRefreshToken = () => cookieSession ? null : localStorage.getItem(storageKeys.refreshToken);
165
- const getSnapshot = () => {
166
- const accessToken = getAccessToken();
167
- const refreshToken = getRefreshToken();
168
- const profile = readProfile();
169
- return {
170
- accessToken,
171
- refreshToken,
172
- profile,
173
- loggedIn: cookieSession ? Boolean(profile) : Boolean(accessToken),
174
- isAdmin: hasAdminAccess(profile, accessToken),
175
- };
176
- };
177
- const emitSessionChanged = () => {
178
- config.onSessionChanged?.(getSnapshot());
179
- };
180
- const saveTokenSet = (data) => {
181
- if (cookieSession) {
182
- emitSessionChanged();
183
- return;
184
- }
185
- localStorage.setItem(storageKeys.accessToken, data.access_token);
186
- if (data.refresh_token) {
187
- localStorage.setItem(storageKeys.refreshToken, data.refresh_token);
188
- }
189
- localStorage.setItem(storageKeys.tokenIssuedAt, String(Date.now()));
190
- localStorage.setItem(storageKeys.tokenExpiresIn, String(data.expires_in || 900));
191
- setCookie('access_token', data.access_token, data.expires_in || 900);
192
- emitSessionChanged();
193
- };
194
- const clear = () => {
195
- Object.values(storageKeys).forEach((storageKey) => localStorage.removeItem(storageKey));
196
- Object.values(storageKeys).forEach((storageKey) => sessionStorage.removeItem(storageKey));
197
- deleteCookie('access_token');
198
- emitSessionChanged();
199
- };
200
- const redirectToLogin = () => {
201
- if (config.onAuthRequired) {
202
- config.onAuthRequired();
203
- return;
204
- }
205
- window.location.href = config.loginPath;
206
- };
207
- const tryAcquireRefreshLock = (owner) => {
208
- const now = Date.now();
209
- const current = parseRefreshLock(localStorage.getItem(storageKeys.refreshLock));
210
- if (current && current.expiresAt > now && current.owner !== owner) {
211
- return false;
212
- }
213
- const nextLock = { owner, expiresAt: now + REFRESH_LOCK_TTL_MS };
214
- localStorage.setItem(storageKeys.refreshLock, JSON.stringify(nextLock));
215
- return parseRefreshLock(localStorage.getItem(storageKeys.refreshLock))?.owner === owner;
216
- };
217
- const releaseRefreshLock = (owner) => {
218
- const current = parseRefreshLock(localStorage.getItem(storageKeys.refreshLock));
219
- if (!current || current.owner === owner || current.expiresAt <= Date.now()) {
220
- localStorage.removeItem(storageKeys.refreshLock);
221
- }
222
- };
223
- const waitForRefreshFromAnotherContext = (previousRefreshToken) => {
224
- const startedAt = Date.now();
225
- return new Promise((resolve) => {
226
- let settled = false;
227
- let intervalId;
228
- let timeoutId;
229
- const finish = (token) => {
230
- if (settled)
231
- return;
232
- settled = true;
233
- if (intervalId !== undefined)
234
- window.clearInterval(intervalId);
235
- if (timeoutId !== undefined)
236
- window.clearTimeout(timeoutId);
237
- window.removeEventListener('storage', check);
238
- resolve(token);
239
- };
240
- const check = () => {
241
- const accessToken = getAccessToken();
242
- const refreshToken = getRefreshToken();
243
- const issuedAt = Number(localStorage.getItem(storageKeys.tokenIssuedAt));
244
- if (accessToken && refreshToken && refreshToken !== previousRefreshToken && issuedAt >= startedAt) {
245
- finish(accessToken);
246
- return;
247
- }
248
- const lock = parseRefreshLock(localStorage.getItem(storageKeys.refreshLock));
249
- if (!lock || lock.expiresAt <= Date.now()) {
250
- finish(null);
251
- }
252
- };
253
- window.addEventListener('storage', check);
254
- intervalId = window.setInterval(check, REFRESH_WAIT_POLL_MS);
255
- timeoutId = window.setTimeout(() => finish(null), REFRESH_WAIT_TIMEOUT_MS);
256
- check();
257
- });
258
- };
259
- const requestBrowserRefreshLock = async (callback) => {
260
- const locks = navigator.locks;
261
- if (!locks)
262
- return callback();
263
- return locks.request(REFRESH_WEB_LOCK_NAME, { mode: 'exclusive' }, callback);
264
- };
265
- const performTokenRefresh = async (previousRefreshToken) => {
266
- const latestRefreshToken = getRefreshToken();
267
- if (!latestRefreshToken)
268
- throw new Error('No refresh token found');
269
- if (latestRefreshToken !== previousRefreshToken) {
270
- const latestAccessToken = getAccessToken();
271
- if (latestAccessToken)
272
- return latestAccessToken;
273
- }
274
- const response = await fetcher(`${config.issuer}/api/v1/auth/refresh`, {
275
- method: 'POST',
276
- headers: { 'Content-Type': 'application/json' },
277
- body: JSON.stringify({ refresh_token: latestRefreshToken }),
278
- });
279
- const data = await parseJsonEnvelope(response, 'Token refresh failed');
280
- saveTokenSet(data);
281
- return data.access_token;
282
- };
283
- const refreshAccessToken = async () => {
284
- if (refreshPromise)
285
- return refreshPromise;
286
- refreshPromise = (async () => {
287
- const owner = generateRefreshOwner();
288
- let lockAcquired = false;
289
- try {
290
- const refreshToken = getRefreshToken();
291
- if (!refreshToken)
292
- throw new Error('No refresh token found');
293
- if (navigator.locks) {
294
- return requestBrowserRefreshLock(() => performTokenRefresh(refreshToken));
295
- }
296
- lockAcquired = tryAcquireRefreshLock(owner);
297
- if (!lockAcquired) {
298
- const tokenFromPeer = await waitForRefreshFromAnotherContext(refreshToken);
299
- if (tokenFromPeer)
300
- return tokenFromPeer;
301
- lockAcquired = tryAcquireRefreshLock(owner);
302
- if (!lockAcquired)
303
- throw new Error('Token refresh is already in progress');
304
- }
305
- return performTokenRefresh(refreshToken);
306
- }
307
- finally {
308
- if (lockAcquired)
309
- releaseRefreshLock(owner);
310
- refreshPromise = null;
311
- }
312
- })();
313
- return refreshPromise;
314
- };
315
- const fetchUserProfile = async (accessToken = getAccessToken()) => {
316
- if (cookieSession) {
317
- const [identity, session] = await Promise.all([
318
- fetcher(`${config.issuer}/oidc/userinfo`, { credentials: 'same-origin' }),
319
- config.sessionProfileEndpoint ? fetcher(config.sessionProfileEndpoint, { credentials: 'same-origin' }) : Promise.resolve(null),
320
- ]);
321
- if (!identity.ok || (session && !session.ok))
322
- throw new Error('Failed to fetch user profile');
323
- const data = await identity.json();
324
- if (session)
325
- Object.assign(data, (await session.json()).data || {});
326
- sessionStorage.setItem(storageKeys.userProfile, JSON.stringify(data));
327
- emitSessionChanged();
328
- return data;
329
- }
330
- if (!accessToken)
331
- throw new Error('No access token found');
332
- const response = await fetcher(`${config.issuer}/oidc/userinfo`, {
333
- headers: { Authorization: `Bearer ${accessToken}` },
334
- });
335
- if (!response.ok)
336
- throw new Error('Failed to fetch user profile');
337
- const data = (await response.json());
338
- const roles = readRolesFromAccessToken(accessToken);
339
- if (roles)
340
- data.roles = roles;
341
- const scope = readScopeFromAccessToken(accessToken);
342
- if (scope)
343
- data.scope = scope;
344
- localStorage.setItem(storageKeys.userProfile, JSON.stringify(data));
345
- emitSessionChanged();
346
- return data;
347
- };
348
- const apiFetch = async (url, options = {}) => {
349
- if (cookieSession) {
350
- const headers = new Headers(options.headers || {});
351
- const issuerRequest = url.startsWith(`${config.issuer}/`);
352
- if (!['GET', 'HEAD', 'OPTIONS'].includes((options.method || 'GET').toUpperCase()) && !headers.has('X-CSRF-Token')) {
353
- const csrf = readCSRFToken(issuerRequest ? undefined : config.csrfCookieName);
354
- if (csrf)
355
- headers.set('X-CSRF-Token', csrf);
356
- }
357
- let response = await fetcher(url, { ...options, headers, credentials: 'same-origin' });
358
- if (response.status === 401 && !issuerRequest) {
359
- const csrf = readCSRFToken();
360
- const refreshResponse = await fetcher(`${config.issuer}/api/v1/auth/refresh`, {
361
- method: 'POST',
362
- headers: csrf ? { 'X-CSRF-Token': csrf, 'X-Gosso-Cookie-Session': '1' } : { 'X-Gosso-Cookie-Session': '1' },
363
- credentials: 'same-origin',
364
- });
365
- if (refreshResponse.ok)
366
- response = await fetcher(url, { ...options, headers, credentials: 'same-origin' });
367
- }
368
- if (response.status === 401)
369
- clear();
370
- return response;
371
- }
372
- let token = getAccessToken();
373
- if (!token) {
374
- redirectToLogin();
375
- return new Response(null, { status: 401 });
376
- }
377
- const issuedAt = Number(localStorage.getItem(storageKeys.tokenIssuedAt));
378
- const expiresIn = Number(localStorage.getItem(storageKeys.tokenExpiresIn)) || 900;
379
- if (issuedAt && Date.now() - issuedAt > expiresIn * 1000) {
380
- try {
381
- token = await refreshAccessToken();
382
- }
383
- catch {
384
- clear();
385
- redirectToLogin();
386
- return new Response(null, { status: 401 });
387
- }
388
- }
389
- const headers = new Headers(options.headers || {});
390
- if (token && !headers.has('Authorization')) {
391
- headers.set('Authorization', `Bearer ${token}`);
392
- }
393
- let response = await fetcher(url, { ...options, headers });
394
- if (response.status === 401 && getRefreshToken()) {
395
- try {
396
- const freshToken = await refreshAccessToken();
397
- headers.set('Authorization', `Bearer ${freshToken}`);
398
- response = await fetcher(url, { ...options, headers });
399
- }
400
- catch {
401
- clear();
402
- redirectToLogin();
403
- }
404
- }
405
- return response;
406
- };
407
- const redirectToAuthorize = async (customRedirectUri) => {
408
- const verifier = generateRandomString(64);
409
- const state = generateRandomString(16);
410
- flowStorage.setItem(storageKeys.pkceVerifier, verifier);
411
- flowStorage.setItem(storageKeys.authState, state);
412
- if (customRedirectUri) {
413
- flowStorage.setItem(storageKeys.postLoginRedirect, customRedirectUri);
414
- }
415
- const challenge = await generateCodeChallenge(verifier);
416
- const authUrl = new URL(`${config.issuer}/oauth2/authorize`);
417
- authUrl.searchParams.append('client_id', config.clientId);
418
- authUrl.searchParams.append('response_type', 'code');
419
- authUrl.searchParams.append('redirect_uri', config.redirectUri);
420
- authUrl.searchParams.append('scope', config.scope);
421
- authUrl.searchParams.append('code_challenge', challenge);
422
- authUrl.searchParams.append('code_challenge_method', 'S256');
423
- authUrl.searchParams.append('state', state);
424
- window.location.href = authUrl.toString();
425
- };
426
- const exchangeCodeForToken = async (code, state) => {
427
- const savedState = flowStorage.getItem(storageKeys.authState);
428
- const verifier = flowStorage.getItem(storageKeys.pkceVerifier);
429
- if (state !== savedState)
430
- throw new Error('State mismatch. Potential CSRF attack.');
431
- if (!verifier)
432
- throw new Error('PKCE verifier not found. Authentication flow expired.');
433
- const body = new URLSearchParams();
434
- body.append('grant_type', 'authorization_code');
435
- body.append('client_id', config.clientId);
436
- body.append('code', code);
437
- body.append('code_verifier', verifier);
438
- body.append('redirect_uri', config.redirectUri);
439
- const response = await fetcher(`${config.issuer}/oauth2/token`, {
440
- method: 'POST',
441
- headers: { 'Content-Type': 'application/x-www-form-urlencoded', ...(cookieSession ? { 'X-Gosso-Cookie-Session': '1' } : {}) },
442
- body: body.toString(),
443
- credentials: 'same-origin',
444
- });
445
- if (!response.ok) {
446
- throw new Error(`Token exchange failed: ${await response.text()}`);
447
- }
448
- const data = (await response.json());
449
- if (!cookieSession)
450
- saveTokenSet(data);
451
- flowStorage.removeItem(storageKeys.pkceVerifier);
452
- flowStorage.removeItem(storageKeys.authState);
453
- return data;
454
- };
455
- const handleRedirectCallback = async (code, state) => {
456
- const tokenSet = await exchangeCodeForToken(code, state);
457
- await fetchUserProfile(cookieSession ? undefined : tokenSet.access_token);
458
- const redirectTo = flowStorage.getItem(storageKeys.postLoginRedirect) || config.postLoginDefaultPath;
459
- flowStorage.removeItem(storageKeys.postLoginRedirect);
460
- return { tokenSet, redirectTo };
461
- };
462
- const logout = async (redirectTo = '/') => {
463
- if (cookieSession) {
464
- const csrf = readCSRFToken();
465
- try {
466
- await fetcher(`${config.issuer}/api/v1/auth/logout`, { method: 'POST', headers: csrf ? { 'X-CSRF-Token': csrf } : {}, credentials: 'same-origin', keepalive: true });
467
- }
468
- finally {
469
- clear();
470
- window.location.href = redirectTo;
471
- }
472
- return;
473
- }
474
- const accessToken = getAccessToken();
475
- try {
476
- if (accessToken) {
477
- await fetcher(`${config.issuer}/api/v1/auth/logout`, {
478
- method: 'POST',
479
- headers: { Authorization: `Bearer ${accessToken}` },
480
- credentials: 'same-origin',
481
- keepalive: true,
482
- });
483
- }
484
- }
485
- finally {
486
- clear();
487
- window.location.href = redirectTo;
488
- }
489
- };
490
- const loginWithPassword = async (username, password) => {
491
- const response = await fetcher(`${config.issuer}/api/v1/auth/login`, {
492
- method: 'POST',
493
- headers: { 'Content-Type': 'application/json', ...(cookieSession ? { 'X-Gosso-Cookie-Session': '1' } : {}) },
494
- body: JSON.stringify({ username, password }),
495
- credentials: 'same-origin',
496
- });
497
- const result = await parseJsonEnvelope(response, 'Login failed');
498
- if (!cookieSession && result.access_token) {
499
- saveTokenSet(result);
500
- await fetchUserProfile(result.access_token);
501
- }
502
- else if (cookieSession && !result.requires_mfa) {
503
- await fetchUserProfile();
504
- }
505
- return result;
506
- };
507
- const verifyMfa = async (mfaToken, code, type = 'totp') => {
508
- const response = await fetcher(`${config.issuer}/api/v1/auth/mfa/verify`, {
509
- method: 'POST',
510
- headers: { 'Content-Type': 'application/json', ...(cookieSession ? { 'X-Gosso-Cookie-Session': '1' } : {}) },
511
- body: JSON.stringify({ mfa_token: mfaToken, code, type }),
512
- credentials: 'same-origin',
513
- });
514
- const data = await parseJsonEnvelope(response, 'MFA verification failed');
515
- if (!cookieSession)
516
- saveTokenSet(data);
517
- await fetchUserProfile(cookieSession ? undefined : data.access_token);
518
- return data;
519
- };
520
- const loginWithPasskey = async () => {
521
- const beginRes = await fetcher(`${config.issuer}/api/v1/passkey/login/begin`, {
522
- method: 'POST',
523
- headers: { 'Content-Type': 'application/json' },
524
- body: JSON.stringify({}),
525
- credentials: 'same-origin',
526
- });
527
- const begin = await parseJsonEnvelope(beginRes, 'Failed to begin passkey login');
528
- const options = {
529
- ...begin.options,
530
- challenge: base64URLToBuffer(begin.options.challenge),
531
- allowCredentials: (begin.options.allowCredentials || []).map((cred) => ({
532
- ...cred,
533
- id: base64URLToBuffer(cred.id),
534
- })),
535
- };
536
- const assertion = (await navigator.credentials.get({ publicKey: options }));
537
- if (!assertion?.response)
538
- throw new Error('Passkey authentication cancelled or failed');
539
- const assertionResponse = assertion.response;
540
- const completeRes = await fetcher(`${config.issuer}/api/v1/passkey/login/complete`, {
541
- method: 'POST',
542
- headers: { 'Content-Type': 'application/json', ...(cookieSession ? { 'X-Gosso-Cookie-Session': '1' } : {}) },
543
- body: JSON.stringify({
544
- request_id: begin.request_id,
545
- id: assertion.id,
546
- rawId: bufferToBase64URL(assertion.rawId),
547
- type: assertion.type,
548
- response: {
549
- clientDataJSON: bufferToBase64URL(assertionResponse.clientDataJSON),
550
- authenticatorData: bufferToBase64URL(assertionResponse.authenticatorData),
551
- signature: bufferToBase64URL(assertionResponse.signature),
552
- userHandle: assertionResponse.userHandle ? bufferToBase64URL(assertionResponse.userHandle) : null,
553
- },
554
- }),
555
- });
556
- const data = await parseJsonEnvelope(completeRes, 'Passkey login failed');
557
- if (!cookieSession)
558
- saveTokenSet(data);
559
- await fetchUserProfile(cookieSession ? undefined : data.access_token);
560
- return data;
561
- };
562
- const updateProfile = async (displayName) => {
563
- const response = await apiFetch(`${config.issuer}/api/v1/auth/profile`, {
564
- method: 'PUT',
565
- headers: { 'Content-Type': 'application/json' },
566
- body: JSON.stringify({ display_name: displayName }),
567
- });
568
- await parseJsonEnvelope(response, 'Failed to update profile');
569
- return fetchUserProfile();
570
- };
571
- const changePassword = async (currentPassword, newPassword) => {
572
- const response = await apiFetch(`${config.issuer}/api/v1/auth/password/change`, {
573
- method: 'POST',
574
- headers: { 'Content-Type': 'application/json' },
575
- body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }),
576
- });
577
- await parseJsonEnvelope(response, 'Failed to change password');
578
- };
579
- const requestEmailChange = async (newEmail, password) => {
580
- const response = await apiFetch(`${config.issuer}/api/v1/auth/profile/email/change/request`, {
581
- method: 'POST',
582
- headers: { 'Content-Type': 'application/json' },
583
- body: JSON.stringify({ new_email: newEmail, password }),
584
- });
585
- await parseJsonEnvelope(response, 'Failed to request email verification code');
586
- };
587
- const confirmEmailChange = async (newEmail, code) => {
588
- const response = await apiFetch(`${config.issuer}/api/v1/auth/profile/email/change/confirm`, {
589
- method: 'POST',
590
- headers: { 'Content-Type': 'application/json' },
591
- body: JSON.stringify({ new_email: newEmail, code }),
592
- });
593
- await parseJsonEnvelope(response, 'Failed to confirm email change');
594
- return fetchUserProfile();
595
- };
596
- const getMfaStatus = async () => {
597
- const response = await apiFetch(`${config.issuer}/api/v1/auth/mfa`);
598
- return parseJsonEnvelope(response, 'Failed to load MFA status');
599
- };
600
- const enrollMfa = async () => {
601
- const response = await apiFetch(`${config.issuer}/api/v1/auth/mfa/enroll`, { method: 'POST' });
602
- return parseJsonEnvelope(response, 'Failed to enroll MFA');
603
- };
604
- const activateMfa = async (code) => {
605
- const response = await apiFetch(`${config.issuer}/api/v1/auth/mfa/activate`, {
606
- method: 'POST',
607
- headers: { 'Content-Type': 'application/json' },
608
- body: JSON.stringify({ code }),
609
- });
610
- await parseJsonEnvelope(response, 'Failed to activate MFA');
611
- const codesResponse = await apiFetch(`${config.issuer}/api/v1/auth/mfa/backup-codes`, { method: 'POST' });
612
- const data = await parseJsonEnvelope(codesResponse, 'Failed to generate backup codes');
613
- return data.backup_codes || [];
614
- };
615
- const disableMfa = async (currentPassword) => {
616
- const response = await apiFetch(`${config.issuer}/api/v1/auth/mfa`, {
617
- method: 'DELETE',
618
- headers: { 'Content-Type': 'application/json' },
619
- body: JSON.stringify({ current_password: currentPassword }),
620
- });
621
- await parseJsonEnvelope(response, 'Failed to disable MFA');
622
- };
623
- const generateBackupCodes = async () => {
624
- const response = await apiFetch(`${config.issuer}/api/v1/auth/mfa/backup-codes`, { method: 'POST' });
625
- const data = await parseJsonEnvelope(response, 'Failed to generate backup codes');
626
- return data.backup_codes || [];
627
- };
628
- const listPasskeys = async () => {
629
- const response = await apiFetch(`${config.issuer}/api/v1/passkeys`);
630
- return parseJsonEnvelope(response, 'Failed to load passkeys');
631
- };
632
- const registerPasskey = async (name) => {
633
- const beginRes = await apiFetch(`${config.issuer}/api/v1/passkey/register/begin`, { method: 'POST' });
634
- const begin = await parseJsonEnvelope(beginRes, 'Failed to initialize passkey registration');
635
- const options = {
636
- ...begin.options,
637
- challenge: base64URLToBuffer(begin.options.challenge),
638
- user: {
639
- ...begin.options.user,
640
- id: base64URLToBuffer(begin.options.user.id),
641
- },
642
- excludeCredentials: (begin.options.excludeCredentials || []).map((cred) => ({
643
- ...cred,
644
- id: base64URLToBuffer(cred.id),
645
- })),
646
- };
647
- const credential = (await navigator.credentials.create({ publicKey: options }));
648
- if (!credential?.response)
649
- throw new Error('Passkey registration cancelled or failed');
650
- const attestationResponse = credential.response;
651
- const completeRes = await apiFetch(`${config.issuer}/api/v1/passkey/register/complete?request_id=${begin.request_id}`, {
652
- method: 'POST',
653
- headers: { 'Content-Type': 'application/json' },
654
- body: JSON.stringify({
655
- id: credential.id,
656
- rawId: bufferToBase64URL(credential.rawId),
657
- type: credential.type,
658
- name,
659
- response: {
660
- clientDataJSON: bufferToBase64URL(attestationResponse.clientDataJSON),
661
- attestationObject: bufferToBase64URL(attestationResponse.attestationObject),
662
- transports: typeof attestationResponse.getTransports === 'function' ? attestationResponse.getTransports() : [],
663
- },
664
- }),
665
- });
666
- await parseJsonEnvelope(completeRes, 'Failed to verify passkey registration');
667
- };
668
- const deletePasskey = async (id) => {
669
- const response = await apiFetch(`${config.issuer}/api/v1/passkeys/${id}`, { method: 'DELETE' });
670
- await parseJsonEnvelope(response, 'Failed to remove passkey');
671
- };
672
- const listSessions = async () => {
673
- const response = await apiFetch(`${config.issuer}/api/v1/auth/sessions`);
674
- const sessions = await parseJsonEnvelope(response, 'Failed to load sessions');
675
- return sessions.sort((a, b) => new Date(b.last_active_at).getTime() - new Date(a.last_active_at).getTime());
676
- };
677
- const getCurrentSession = async () => {
678
- const response = await apiFetch(`${config.issuer}/api/v1/auth/session`);
679
- return parseJsonEnvelope(response, 'Failed to load current session');
680
- };
681
- const revokeSession = async (id) => {
682
- const response = await apiFetch(`${config.issuer}/api/v1/auth/sessions/${id}`, { method: 'DELETE' });
683
- await parseJsonEnvelope(response, 'Failed to revoke session');
684
- };
685
- return {
686
- config,
687
- storageKeys,
688
- getAccessToken,
689
- getRefreshToken,
690
- getUserProfile: readProfile,
691
- getSnapshot,
692
- isLoggedIn: () => cookieSession ? Boolean(readProfile()) : Boolean(getAccessToken()),
693
- isAdmin: () => hasAdminAccess(readProfile(), getAccessToken()),
694
- saveTokenSet,
695
- clear,
696
- logout,
697
- redirectToAuthorize,
698
- exchangeCodeForToken,
699
- handleRedirectCallback,
700
- fetchUserProfile,
701
- refreshAccessToken,
702
- apiFetch,
703
- loginWithPassword,
704
- verifyMfa,
705
- loginWithPasskey,
706
- updateProfile,
707
- changePassword,
708
- requestEmailChange,
709
- confirmEmailChange,
710
- getMfaStatus,
711
- enrollMfa,
712
- activateMfa,
713
- disableMfa,
714
- generateBackupCodes,
715
- listPasskeys,
716
- registerPasskey,
717
- deletePasskey,
718
- listSessions,
719
- getCurrentSession,
720
- revokeSession,
721
- };
722
- }
1
+ export { GossoError, ApiError, AuthenticationError, TokenRefreshError, CsrfError, CryptoError, PasskeyError, } from "./errors.js";
2
+ export { normalizeBaseUrl, generateRandomString, bufferToBase64URL, base64URLToBuffer, getCookieName, safeLocalPath, readCookie, CookieSessionRefreshError, readClaimsFromAccessToken, readRolesFromAccessToken, readScopeFromAccessToken, hasAdminAccess, parseRefreshLock, generateRefreshOwner, parseJsonEnvelope, } from "./utils.js";
3
+ export { generateCodeChallenge } from "./pkce.js";
4
+ export { defaultConfig, createGossoClient } from "./client.js";
723
5
  //# sourceMappingURL=index.js.map