@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/README.md +68 -84
- package/dist/{auth-client-CAHMjodm.d.ts → auth-client-Cdkp07ii.d.ts} +14 -6
- package/dist/{auth-sdk-error-DKM7PyKC.d.ts → auth-sdk-error-D3gsfK9d.d.ts} +0 -3
- package/dist/extension.d.ts +16 -0
- package/dist/extension.js +502 -0
- package/dist/extension.js.map +1 -0
- package/dist/index.d.ts +12 -19
- package/dist/index.js +105 -119
- package/dist/index.js.map +1 -1
- package/dist/next.d.ts +4 -27
- package/dist/next.js +125 -383
- package/dist/next.js.map +1 -1
- package/dist/react.d.ts +4 -20
- package/dist/react.js +129 -176
- package/dist/react.js.map +1 -1
- package/docs/api-reference.md +65 -89
- package/docs/architecture.md +28 -46
- package/docs/development-and-ci.md +15 -19
- package/docs/index.md +1 -15
- package/docs/integration-guide.md +46 -81
- package/docs/overview.md +24 -30
- package/package.json +11 -4
- package/react-client.js +3 -0
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-
|
|
4
|
+
// src/adapters/react/auth-provider.tsx
|
|
5
5
|
|
|
6
6
|
// src/constants/auth-endpoints.ts
|
|
7
|
-
var authApiPrefix = "/
|
|
7
|
+
var authApiPrefix = "/oauth";
|
|
8
8
|
var authEndpoints = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|
|
303
|
+
function resolveReturnToPath(returnTo) {
|
|
287
304
|
assertBrowserRuntime();
|
|
288
305
|
const fallbackPath = getCurrentBrowserPath();
|
|
289
|
-
|
|
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
|
|
349
|
-
if (
|
|
350
|
-
|
|
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
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
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
|
|
406
|
-
|
|
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
|
-
|
|
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 = (
|
|
428
|
-
|
|
429
|
-
window.location.
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
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
|
-
|
|
459
|
+
init,
|
|
473
460
|
getSession,
|
|
474
461
|
login,
|
|
475
462
|
logout,
|
|
476
|
-
|
|
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
|
-
|
|
548
|
-
return resolveClient().
|
|
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
|
-
|
|
560
|
-
return resolveClient().
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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 {
|
|
632
|
+
export { AuthProvider, AuthSessionGate, useAuth };
|
|
680
633
|
//# sourceMappingURL=react.js.map
|
|
681
634
|
//# sourceMappingURL=react.js.map
|