@ghostly-solutions/auth 0.1.0 → 0.2.1

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/react.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { createContext, useMemo, useState, useEffect, useCallback, useContext } from 'react';
2
2
  import { jsx, Fragment } from 'react/jsx-runtime';
3
3
 
4
- // src/adapters/react/auth-callback-handler.tsx
4
+ // src/adapters/react/auth-provider.tsx
5
5
 
6
6
  // src/constants/auth-endpoints.ts
7
- var authApiPrefix = "/v1/auth";
7
+ var authApiPrefix = "/oauth";
8
8
  var authEndpoints = {
9
- loginStart: `${authApiPrefix}/keycloak/login`,
10
- validateKeycloakToken: `${authApiPrefix}/keycloak/validate`,
11
- session: `${authApiPrefix}/me`,
9
+ authorize: `${authApiPrefix}/authorize`,
10
+ session: `${authApiPrefix}/session`,
11
+ refresh: `${authApiPrefix}/refresh`,
12
12
  logout: `${authApiPrefix}/logout`
13
13
  };
14
14
 
@@ -16,7 +16,6 @@ var authEndpoints = {
16
16
  var httpStatus = {
17
17
  ok: 200,
18
18
  noContent: 204,
19
- badRequest: 400,
20
19
  unauthorized: 401
21
20
  };
22
21
 
@@ -36,27 +35,59 @@ var AuthSdkError = class extends Error {
36
35
 
37
36
  // src/types/auth-error-code.ts
38
37
  var authErrorCode = {
39
- callbackMissingToken: "callback_missing_token",
40
- callbackInvalidToken: "callback_invalid_token",
41
- callbackValidationFailed: "callback_validation_failed",
42
38
  unauthorized: "unauthorized",
43
39
  networkError: "network_error",
44
40
  apiError: "api_error",
45
41
  broadcastChannelUnsupported: "broadcast_channel_unsupported"};
46
42
 
43
+ // src/core/api-origin.ts
44
+ var slash = "/";
45
+ function normalizeApiOrigin(apiOrigin) {
46
+ const trimmed = apiOrigin.trim();
47
+ if (!trimmed) {
48
+ throw new AuthSdkError({
49
+ code: authErrorCode.apiError,
50
+ details: null,
51
+ message: "Auth API origin must be a non-empty absolute URL.",
52
+ status: null
53
+ });
54
+ }
55
+ let parsed;
56
+ try {
57
+ parsed = new URL(trimmed);
58
+ } catch (error) {
59
+ throw new AuthSdkError({
60
+ code: authErrorCode.apiError,
61
+ details: error,
62
+ message: "Auth API origin must be a valid absolute URL.",
63
+ status: null
64
+ });
65
+ }
66
+ if (parsed.pathname !== slash || parsed.search || parsed.hash) {
67
+ throw new AuthSdkError({
68
+ code: authErrorCode.apiError,
69
+ details: null,
70
+ message: "Auth API origin must not include path, query, or hash.",
71
+ status: null
72
+ });
73
+ }
74
+ return parsed.origin;
75
+ }
76
+ function resolveApiEndpoint(path, apiOrigin) {
77
+ if (!apiOrigin) {
78
+ return path;
79
+ }
80
+ return `${normalizeApiOrigin(apiOrigin)}${path}`;
81
+ }
82
+
47
83
  // src/constants/auth-keys.ts
48
- var authQueryKeys = {
49
- token: "token"
50
- };
51
- var authStorageKeys = {
52
- returnTo: "ghostly-auth:return-to"
53
- };
54
84
  var authBroadcast = {
55
85
  channelName: "ghostly-auth-channel",
56
86
  sessionUpdatedEvent: "session-updated"
57
87
  };
58
88
  var authRoutes = {
59
- root: "/"};
89
+ root: "/"
90
+ };
60
91
 
61
92
  // src/core/object-guards.ts
62
93
  function isObjectRecord(value) {
@@ -126,19 +157,6 @@ function createBroadcastSync(options) {
126
157
  };
127
158
  }
128
159
 
129
- // src/core/callback-url.ts
130
- function readCallbackToken(url) {
131
- return url.searchParams.get(authQueryKeys.token);
132
- }
133
- function removeCallbackToken(url) {
134
- const nextUrl = new URL(url.toString());
135
- nextUrl.searchParams.delete(authQueryKeys.token);
136
- return nextUrl;
137
- }
138
- function replaceBrowserHistory(url) {
139
- window.history.replaceState(null, "", url.toString());
140
- }
141
-
142
160
  // src/core/http-client.ts
143
161
  var jsonContentType = "application/json";
144
162
  var jsonHeaderName = "content-type";
@@ -234,9 +252,8 @@ function getJson(path) {
234
252
  path
235
253
  });
236
254
  }
237
- function postJson(path, body) {
255
+ function postJsonWithoutBody(path) {
238
256
  return request({
239
- body,
240
257
  method: "POST",
241
258
  path
242
259
  });
@@ -283,18 +300,10 @@ function sanitizeReturnTo(value) {
283
300
  function getCurrentBrowserPath() {
284
301
  return `${window.location.pathname}${window.location.search}${window.location.hash}`;
285
302
  }
286
- function saveReturnToPath(returnTo) {
303
+ function resolveReturnToPath(returnTo) {
287
304
  assertBrowserRuntime();
288
305
  const fallbackPath = getCurrentBrowserPath();
289
- const sanitized = sanitizeReturnTo(returnTo ?? fallbackPath);
290
- window.sessionStorage.setItem(authStorageKeys.returnTo, sanitized);
291
- return sanitized;
292
- }
293
- function consumeReturnToPath() {
294
- assertBrowserRuntime();
295
- const value = window.sessionStorage.getItem(authStorageKeys.returnTo);
296
- window.sessionStorage.removeItem(authStorageKeys.returnTo);
297
- return sanitizeReturnTo(value);
306
+ return sanitizeReturnTo(returnTo ?? fallbackPath);
298
307
  }
299
308
 
300
309
  // src/core/session-store.ts
@@ -327,10 +336,6 @@ var SessionStore = class {
327
336
  };
328
337
 
329
338
  // src/core/auth-client.ts
330
- function createPendingRedirectPromise() {
331
- return new Promise(() => {
332
- });
333
- }
334
339
  function createInvalidSessionPayloadError(path) {
335
340
  return new AuthSdkError({
336
341
  code: authErrorCode.apiError,
@@ -345,29 +350,11 @@ function toValidatedSession(payload, path) {
345
350
  }
346
351
  return payload;
347
352
  }
348
- function toCallbackFailure(error) {
349
- if (error instanceof AuthSdkError) {
350
- if (error.status === httpStatus.unauthorized) {
351
- return new AuthSdkError({
352
- code: authErrorCode.callbackInvalidToken,
353
- details: error.details,
354
- message: "Callback JWT is invalid or expired.",
355
- status: error.status
356
- });
357
- }
358
- return new AuthSdkError({
359
- code: authErrorCode.callbackValidationFailed,
360
- details: error.details,
361
- message: "Keycloak callback validation failed.",
362
- status: error.status
363
- });
353
+ function toSessionPayload(payload, path) {
354
+ if (payload === null) {
355
+ return null;
364
356
  }
365
- return new AuthSdkError({
366
- code: authErrorCode.callbackValidationFailed,
367
- details: error,
368
- message: "Keycloak callback validation failed.",
369
- status: null
370
- });
357
+ return toValidatedSession(payload, path);
371
358
  }
372
359
  function createNoopBroadcastSync() {
373
360
  return {
@@ -389,25 +376,53 @@ function createSafeBroadcastSync(onSessionUpdated) {
389
376
  throw error;
390
377
  }
391
378
  }
392
- async function fetchCurrentSessionFromApi() {
393
- const payload = await getJson(authEndpoints.session);
394
- if (payload === null) {
395
- return null;
396
- }
397
- return toValidatedSession(payload, authEndpoints.session);
379
+ function toInitResult(session) {
380
+ return {
381
+ session,
382
+ status: session ? "authenticated" : "unauthenticated"
383
+ };
398
384
  }
399
- function createAuthClient() {
400
- assertBrowserRuntime();
385
+ function createAuthClient(options = {}) {
386
+ let initPromise = null;
387
+ const defaultApplication = options.application?.trim() || "";
401
388
  const sessionStore = new SessionStore();
402
389
  const broadcastSync = createSafeBroadcastSync((session) => {
403
390
  sessionStore.setSession(session);
404
391
  });
405
- const getSession = async (options) => {
406
- const forceRefresh = options?.forceRefresh ?? false;
392
+ const resolveEndpoint = (path) => resolveApiEndpoint(path, options.apiOrigin);
393
+ const loadSession = async () => {
394
+ const payload = await getJson(resolveEndpoint(authEndpoints.session));
395
+ return toSessionPayload(payload, authEndpoints.session);
396
+ };
397
+ const init = async (initOptions) => {
398
+ const forceRefresh = initOptions?.forceRefresh ?? false;
407
399
  if (sessionStore.hasResolvedSession() && !forceRefresh) {
408
- return sessionStore.getSessionIfResolved();
400
+ return toInitResult(sessionStore.getSessionIfResolved());
401
+ }
402
+ if (initPromise) {
403
+ return initPromise;
409
404
  }
410
- const session = await fetchCurrentSessionFromApi();
405
+ initPromise = (async () => {
406
+ const session = await loadSession();
407
+ sessionStore.setSession(session);
408
+ broadcastSync.publishSession(session);
409
+ return toInitResult(session);
410
+ })();
411
+ try {
412
+ return await initPromise;
413
+ } finally {
414
+ initPromise = null;
415
+ }
416
+ };
417
+ const getSession = async (requestOptions) => {
418
+ const result = await init({
419
+ forceRefresh: requestOptions?.forceRefresh ?? false
420
+ });
421
+ return result.session;
422
+ };
423
+ const refresh = async () => {
424
+ const payload = await postJsonWithoutBody(resolveEndpoint(authEndpoints.refresh));
425
+ const session = toSessionPayload(payload, authEndpoints.refresh);
411
426
  sessionStore.setSession(session);
412
427
  broadcastSync.publishSession(session);
413
428
  return session;
@@ -424,106 +439,32 @@ function createAuthClient() {
424
439
  status: httpStatus.unauthorized
425
440
  });
426
441
  };
427
- const login = (options) => {
428
- saveReturnToPath(options?.returnTo);
429
- window.location.assign(authEndpoints.loginStart);
430
- };
431
- const processCallback = async () => {
432
- const currentUrl = new URL(window.location.href);
433
- const token = readCallbackToken(currentUrl);
434
- if (!token) {
435
- throw new AuthSdkError({
436
- code: authErrorCode.callbackMissingToken,
437
- details: null,
438
- message: "Missing callback token query parameter.",
439
- status: httpStatus.badRequest
440
- });
442
+ const login = (loginOptions) => {
443
+ const returnTo = resolveReturnToPath(loginOptions?.returnTo);
444
+ const authorizeUrl = new URL(resolveEndpoint(authEndpoints.authorize), window.location.origin);
445
+ authorizeUrl.searchParams.set("return_to", returnTo);
446
+ const application = loginOptions?.application?.trim() || defaultApplication;
447
+ if (application) {
448
+ authorizeUrl.searchParams.set("app", application);
441
449
  }
442
- const cleanedUrl = removeCallbackToken(currentUrl);
443
- replaceBrowserHistory(cleanedUrl);
444
- try {
445
- const payload = await postJson(
446
- authEndpoints.validateKeycloakToken,
447
- { token }
448
- );
449
- const session = toValidatedSession(payload.session, authEndpoints.validateKeycloakToken);
450
- sessionStore.setSession(session);
451
- broadcastSync.publishSession(session);
452
- return {
453
- redirectTo: consumeReturnToPath(),
454
- session
455
- };
456
- } catch (error) {
457
- throw toCallbackFailure(error);
458
- }
459
- };
460
- const completeCallbackRedirect = async () => {
461
- const result = await processCallback();
462
- window.location.replace(result.redirectTo);
463
- return createPendingRedirectPromise();
450
+ window.location.assign(authorizeUrl.toString());
464
451
  };
465
452
  const logout = async () => {
466
- await postEmpty(authEndpoints.logout);
453
+ await postEmpty(resolveEndpoint(authEndpoints.logout));
467
454
  sessionStore.setSession(null);
468
455
  broadcastSync.publishSession(null);
469
456
  };
470
457
  const subscribe = sessionStore.subscribe.bind(sessionStore);
471
458
  return {
472
- completeCallbackRedirect,
459
+ init,
473
460
  getSession,
474
461
  login,
475
462
  logout,
476
- processCallback,
463
+ refresh,
477
464
  requireSession,
478
465
  subscribe
479
466
  };
480
467
  }
481
- function normalizeAuthError(error) {
482
- if (error instanceof AuthSdkError) {
483
- return error;
484
- }
485
- return new AuthSdkError({
486
- code: "callback_validation_failed",
487
- details: error,
488
- message: "Auth callback redirect failed.",
489
- status: null
490
- });
491
- }
492
- function useAuthCallbackRedirect(options = {}) {
493
- const client = useMemo(() => options.client ?? createAuthClient(), [options.client]);
494
- const [error, setError] = useState(null);
495
- useEffect(() => {
496
- let isActive = true;
497
- void client.completeCallbackRedirect().catch((caughtError) => {
498
- if (!isActive) {
499
- return;
500
- }
501
- setError(normalizeAuthError(caughtError));
502
- });
503
- return () => {
504
- isActive = false;
505
- };
506
- }, [client]);
507
- if (error) {
508
- return {
509
- error,
510
- status: "failed"
511
- };
512
- }
513
- return {
514
- error: null,
515
- status: "processing"
516
- };
517
- }
518
- function AuthCallbackHandler(props) {
519
- const state = useAuthCallbackRedirect({
520
- client: props.client
521
- });
522
- if (state.status === "failed" && state.error) {
523
- return props.renderError(state.error);
524
- }
525
- return /* @__PURE__ */ jsx(Fragment, { children: props.processing });
526
- }
527
468
  var AuthContext = createContext(null);
528
469
  var initialLoadingState = true;
529
470
  function toAuthError(error) {
@@ -544,8 +485,8 @@ function createDeferredAuthClient() {
544
485
  return authClient;
545
486
  };
546
487
  return {
547
- completeCallbackRedirect() {
548
- return resolveClient().completeCallbackRedirect();
488
+ init(options) {
489
+ return resolveClient().init(options);
549
490
  },
550
491
  getSession(options) {
551
492
  return resolveClient().getSession(options);
@@ -556,8 +497,8 @@ function createDeferredAuthClient() {
556
497
  logout() {
557
498
  return resolveClient().logout();
558
499
  },
559
- processCallback() {
560
- return resolveClient().processCallback();
500
+ refresh() {
501
+ return resolveClient().refresh();
561
502
  },
562
503
  requireSession() {
563
504
  return resolveClient().requireSession();
@@ -573,23 +514,35 @@ function createDeferredAuthClient() {
573
514
  }
574
515
  function AuthProvider(props) {
575
516
  const authClient = useMemo(() => props.client ?? createDeferredAuthClient(), [props.client]);
576
- const [session, setSession] = useState(null);
517
+ const [session, setSession] = useState(props.initialSession ?? null);
577
518
  const [error, setError] = useState(null);
578
- const [isLoading, setIsLoading] = useState(initialLoadingState);
519
+ const [isLoading, setIsLoading] = useState(
520
+ props.initialSession === void 0 ? initialLoadingState : false
521
+ );
579
522
  useEffect(() => {
580
523
  let isActive = true;
524
+ const requiresBootstrap = props.initialSession === void 0;
525
+ if (!requiresBootstrap) {
526
+ setSession(props.initialSession ?? null);
527
+ setIsLoading(false);
528
+ }
581
529
  const unsubscribe = authClient.subscribe((nextSession) => {
582
530
  if (!isActive) {
583
531
  return;
584
532
  }
585
533
  setSession(nextSession);
586
- setIsLoading(false);
587
534
  });
588
- void authClient.getSession().then((nextSession) => {
535
+ if (!requiresBootstrap) {
536
+ return () => {
537
+ isActive = false;
538
+ unsubscribe();
539
+ };
540
+ }
541
+ void authClient.init().then((result) => {
589
542
  if (!isActive) {
590
543
  return;
591
544
  }
592
- setSession(nextSession);
545
+ setSession(result.session);
593
546
  }).catch((caughtError) => {
594
547
  if (!isActive) {
595
548
  return;
@@ -605,7 +558,7 @@ function AuthProvider(props) {
605
558
  isActive = false;
606
559
  unsubscribe();
607
560
  };
608
- }, [authClient]);
561
+ }, [authClient, props.initialSession]);
609
562
  const login = useCallback(
610
563
  (options) => {
611
564
  setError(null);
@@ -627,7 +580,7 @@ function AuthProvider(props) {
627
580
  const refresh = useCallback(async () => {
628
581
  setError(null);
629
582
  try {
630
- const nextSession = await authClient.getSession({ forceRefresh: true });
583
+ const nextSession = await authClient.refresh();
631
584
  setSession(nextSession);
632
585
  return nextSession;
633
586
  } catch (caughtError) {
@@ -676,6 +629,6 @@ function AuthSessionGate(props) {
676
629
  return /* @__PURE__ */ jsx(Fragment, { children: props.authorized(auth.session) });
677
630
  }
678
631
 
679
- export { AuthCallbackHandler, AuthProvider, AuthSessionGate, useAuth, useAuthCallbackRedirect };
632
+ export { AuthProvider, AuthSessionGate, useAuth };
680
633
  //# sourceMappingURL=react.js.map
681
634
  //# sourceMappingURL=react.js.map