@ghostly-solutions/auth 0.1.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 +127 -0
- package/dist/auth-client-CAHMjodm.d.ts +32 -0
- package/dist/auth-sdk-error-DKM7PyKC.d.ts +26 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +483 -0
- package/dist/index.js.map +1 -0
- package/dist/next.d.ts +47 -0
- package/dist/next.js +899 -0
- package/dist/next.js.map +1 -0
- package/dist/react.d.ts +50 -0
- package/dist/react.js +681 -0
- package/dist/react.js.map +1 -0
- package/docs/api-reference.md +145 -0
- package/docs/architecture.md +62 -0
- package/docs/development-and-ci.md +53 -0
- package/docs/index.md +22 -0
- package/docs/integration-guide.md +142 -0
- package/docs/overview.md +41 -0
- package/package.json +66 -0
package/dist/react.js
ADDED
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import { createContext, useMemo, useState, useEffect, useCallback, useContext } from 'react';
|
|
2
|
+
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/adapters/react/auth-callback-handler.tsx
|
|
5
|
+
|
|
6
|
+
// src/constants/auth-endpoints.ts
|
|
7
|
+
var authApiPrefix = "/v1/auth";
|
|
8
|
+
var authEndpoints = {
|
|
9
|
+
loginStart: `${authApiPrefix}/keycloak/login`,
|
|
10
|
+
validateKeycloakToken: `${authApiPrefix}/keycloak/validate`,
|
|
11
|
+
session: `${authApiPrefix}/me`,
|
|
12
|
+
logout: `${authApiPrefix}/logout`
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// src/constants/http-status.ts
|
|
16
|
+
var httpStatus = {
|
|
17
|
+
ok: 200,
|
|
18
|
+
noContent: 204,
|
|
19
|
+
badRequest: 400,
|
|
20
|
+
unauthorized: 401
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/errors/auth-sdk-error.ts
|
|
24
|
+
var AuthSdkError = class extends Error {
|
|
25
|
+
code;
|
|
26
|
+
details;
|
|
27
|
+
status;
|
|
28
|
+
constructor(payload) {
|
|
29
|
+
super(payload.message);
|
|
30
|
+
this.name = "AuthSdkError";
|
|
31
|
+
this.code = payload.code;
|
|
32
|
+
this.details = payload.details;
|
|
33
|
+
this.status = payload.status;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// src/types/auth-error-code.ts
|
|
38
|
+
var authErrorCode = {
|
|
39
|
+
callbackMissingToken: "callback_missing_token",
|
|
40
|
+
callbackInvalidToken: "callback_invalid_token",
|
|
41
|
+
callbackValidationFailed: "callback_validation_failed",
|
|
42
|
+
unauthorized: "unauthorized",
|
|
43
|
+
networkError: "network_error",
|
|
44
|
+
apiError: "api_error",
|
|
45
|
+
broadcastChannelUnsupported: "broadcast_channel_unsupported"};
|
|
46
|
+
|
|
47
|
+
// src/constants/auth-keys.ts
|
|
48
|
+
var authQueryKeys = {
|
|
49
|
+
token: "token"
|
|
50
|
+
};
|
|
51
|
+
var authStorageKeys = {
|
|
52
|
+
returnTo: "ghostly-auth:return-to"
|
|
53
|
+
};
|
|
54
|
+
var authBroadcast = {
|
|
55
|
+
channelName: "ghostly-auth-channel",
|
|
56
|
+
sessionUpdatedEvent: "session-updated"
|
|
57
|
+
};
|
|
58
|
+
var authRoutes = {
|
|
59
|
+
root: "/"};
|
|
60
|
+
|
|
61
|
+
// src/core/object-guards.ts
|
|
62
|
+
function isObjectRecord(value) {
|
|
63
|
+
return typeof value === "object" && value !== null;
|
|
64
|
+
}
|
|
65
|
+
function isStringValue(value) {
|
|
66
|
+
return typeof value === "string";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/core/session-parser.ts
|
|
70
|
+
function isStringArray(value) {
|
|
71
|
+
return Array.isArray(value) && value.every((entry) => isStringValue(entry));
|
|
72
|
+
}
|
|
73
|
+
function isGhostlySession(value) {
|
|
74
|
+
if (!isObjectRecord(value)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return isStringValue(value.id) && isStringValue(value.username) && (value.firstName === null || isStringValue(value.firstName)) && (value.lastName === null || isStringValue(value.lastName)) && isStringValue(value.email) && isStringValue(value.role) && isStringArray(value.permissions);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/core/broadcast-sync.ts
|
|
81
|
+
function isSessionUpdatedMessage(value) {
|
|
82
|
+
if (!isObjectRecord(value)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if (!isStringValue(value.type)) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
if (value.type !== authBroadcast.sessionUpdatedEvent) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return value.session === null || isGhostlySession(value.session);
|
|
92
|
+
}
|
|
93
|
+
function createUnsupportedBroadcastChannelError() {
|
|
94
|
+
return new AuthSdkError({
|
|
95
|
+
code: authErrorCode.broadcastChannelUnsupported,
|
|
96
|
+
details: null,
|
|
97
|
+
message: "BroadcastChannel is unavailable in this runtime.",
|
|
98
|
+
status: null
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function createBroadcastSync(options) {
|
|
102
|
+
if (typeof BroadcastChannel === "undefined") {
|
|
103
|
+
throw createUnsupportedBroadcastChannelError();
|
|
104
|
+
}
|
|
105
|
+
const channel = new BroadcastChannel(authBroadcast.channelName);
|
|
106
|
+
const onMessage = (event) => {
|
|
107
|
+
const messageEvent = event;
|
|
108
|
+
if (!isSessionUpdatedMessage(messageEvent.data)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
options.onSessionUpdated(messageEvent.data.session);
|
|
112
|
+
};
|
|
113
|
+
channel.addEventListener("message", onMessage);
|
|
114
|
+
return {
|
|
115
|
+
close() {
|
|
116
|
+
channel.removeEventListener("message", onMessage);
|
|
117
|
+
channel.close();
|
|
118
|
+
},
|
|
119
|
+
publishSession(session) {
|
|
120
|
+
const payload = {
|
|
121
|
+
session,
|
|
122
|
+
type: authBroadcast.sessionUpdatedEvent
|
|
123
|
+
};
|
|
124
|
+
channel.postMessage(payload);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
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
|
+
// src/core/http-client.ts
|
|
143
|
+
var jsonContentType = "application/json";
|
|
144
|
+
var jsonHeaderName = "content-type";
|
|
145
|
+
var includeCredentials = "include";
|
|
146
|
+
var noStoreCache = "no-store";
|
|
147
|
+
function toTypedValue(value) {
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
function mapHttpStatusToAuthErrorCode(status) {
|
|
151
|
+
if (status === httpStatus.unauthorized) {
|
|
152
|
+
return authErrorCode.unauthorized;
|
|
153
|
+
}
|
|
154
|
+
return authErrorCode.apiError;
|
|
155
|
+
}
|
|
156
|
+
async function parseJsonPayload(response) {
|
|
157
|
+
try {
|
|
158
|
+
return await response.json();
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function parseErrorPayload(response) {
|
|
164
|
+
const payload = await parseJsonPayload(response);
|
|
165
|
+
if (!isObjectRecord(payload)) {
|
|
166
|
+
return {
|
|
167
|
+
code: null,
|
|
168
|
+
details: null,
|
|
169
|
+
message: null
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const maybeCode = payload.code;
|
|
173
|
+
const maybeMessage = payload.message;
|
|
174
|
+
return {
|
|
175
|
+
code: isStringValue(maybeCode) ? maybeCode : null,
|
|
176
|
+
details: "details" in payload ? payload.details : null,
|
|
177
|
+
message: isStringValue(maybeMessage) ? maybeMessage : null
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function buildApiErrorMessage(method, path) {
|
|
181
|
+
return `Auth API request failed: ${method} ${path}`;
|
|
182
|
+
}
|
|
183
|
+
function buildNetworkErrorMessage(method, path) {
|
|
184
|
+
return `Auth API network failure: ${method} ${path}`;
|
|
185
|
+
}
|
|
186
|
+
async function request(options) {
|
|
187
|
+
const expectedStatus = options.expectedStatus ?? httpStatus.ok;
|
|
188
|
+
const headers = new Headers();
|
|
189
|
+
const hasBody = typeof options.body !== "undefined";
|
|
190
|
+
if (hasBody) {
|
|
191
|
+
headers.set(jsonHeaderName, jsonContentType);
|
|
192
|
+
}
|
|
193
|
+
const requestInit = {
|
|
194
|
+
cache: noStoreCache,
|
|
195
|
+
credentials: includeCredentials,
|
|
196
|
+
headers,
|
|
197
|
+
method: options.method
|
|
198
|
+
};
|
|
199
|
+
if (hasBody) {
|
|
200
|
+
requestInit.body = JSON.stringify(options.body);
|
|
201
|
+
}
|
|
202
|
+
let response;
|
|
203
|
+
try {
|
|
204
|
+
response = await fetch(options.path, requestInit);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
throw new AuthSdkError({
|
|
207
|
+
code: authErrorCode.networkError,
|
|
208
|
+
details: error,
|
|
209
|
+
message: buildNetworkErrorMessage(options.method, options.path),
|
|
210
|
+
status: null
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
if (response.status !== expectedStatus) {
|
|
214
|
+
const parsed = await parseErrorPayload(response);
|
|
215
|
+
throw new AuthSdkError({
|
|
216
|
+
code: mapHttpStatusToAuthErrorCode(response.status),
|
|
217
|
+
details: {
|
|
218
|
+
apiCode: parsed.code,
|
|
219
|
+
apiDetails: parsed.details
|
|
220
|
+
},
|
|
221
|
+
message: parsed.message ?? buildApiErrorMessage(options.method, options.path),
|
|
222
|
+
status: response.status
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (response.status === httpStatus.noContent) {
|
|
226
|
+
return toTypedValue(null);
|
|
227
|
+
}
|
|
228
|
+
const payload = await parseJsonPayload(response);
|
|
229
|
+
return toTypedValue(payload);
|
|
230
|
+
}
|
|
231
|
+
function getJson(path) {
|
|
232
|
+
return request({
|
|
233
|
+
method: "GET",
|
|
234
|
+
path
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function postJson(path, body) {
|
|
238
|
+
return request({
|
|
239
|
+
body,
|
|
240
|
+
method: "POST",
|
|
241
|
+
path
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
function postEmpty(path) {
|
|
245
|
+
return request({
|
|
246
|
+
expectedStatus: httpStatus.noContent,
|
|
247
|
+
method: "POST",
|
|
248
|
+
path
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/core/runtime.ts
|
|
253
|
+
var browserRuntimeErrorMessage = "Browser runtime is required for this auth operation.";
|
|
254
|
+
function isBrowserRuntime() {
|
|
255
|
+
return typeof window !== "undefined";
|
|
256
|
+
}
|
|
257
|
+
function assertBrowserRuntime() {
|
|
258
|
+
if (isBrowserRuntime()) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
throw new AuthSdkError({
|
|
262
|
+
code: authErrorCode.apiError,
|
|
263
|
+
details: null,
|
|
264
|
+
message: browserRuntimeErrorMessage,
|
|
265
|
+
status: null
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/core/return-to-storage.ts
|
|
270
|
+
function sanitizeReturnTo(value) {
|
|
271
|
+
if (!value) {
|
|
272
|
+
return authRoutes.root;
|
|
273
|
+
}
|
|
274
|
+
if (!value.startsWith(authRoutes.root)) {
|
|
275
|
+
return authRoutes.root;
|
|
276
|
+
}
|
|
277
|
+
const protocolRelativePrefix = "//";
|
|
278
|
+
if (value.startsWith(protocolRelativePrefix)) {
|
|
279
|
+
return authRoutes.root;
|
|
280
|
+
}
|
|
281
|
+
return value;
|
|
282
|
+
}
|
|
283
|
+
function getCurrentBrowserPath() {
|
|
284
|
+
return `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
285
|
+
}
|
|
286
|
+
function saveReturnToPath(returnTo) {
|
|
287
|
+
assertBrowserRuntime();
|
|
288
|
+
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);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/core/session-store.ts
|
|
301
|
+
var SessionStore = class {
|
|
302
|
+
listeners = /* @__PURE__ */ new Set();
|
|
303
|
+
resolvedSession = null;
|
|
304
|
+
resolveState = "pending";
|
|
305
|
+
getSessionIfResolved() {
|
|
306
|
+
if (this.resolveState === "pending") {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
return this.resolvedSession;
|
|
310
|
+
}
|
|
311
|
+
hasResolvedSession() {
|
|
312
|
+
return this.resolveState === "resolved";
|
|
313
|
+
}
|
|
314
|
+
setSession(session) {
|
|
315
|
+
this.resolveState = "resolved";
|
|
316
|
+
this.resolvedSession = session;
|
|
317
|
+
for (const listener of this.listeners) {
|
|
318
|
+
listener(session);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
subscribe(listener) {
|
|
322
|
+
this.listeners.add(listener);
|
|
323
|
+
return () => {
|
|
324
|
+
this.listeners.delete(listener);
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// src/core/auth-client.ts
|
|
330
|
+
function createPendingRedirectPromise() {
|
|
331
|
+
return new Promise(() => {
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
function createInvalidSessionPayloadError(path) {
|
|
335
|
+
return new AuthSdkError({
|
|
336
|
+
code: authErrorCode.apiError,
|
|
337
|
+
details: null,
|
|
338
|
+
message: `Auth API response has invalid session shape: ${path}`,
|
|
339
|
+
status: null
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
function toValidatedSession(payload, path) {
|
|
343
|
+
if (!isGhostlySession(payload)) {
|
|
344
|
+
throw createInvalidSessionPayloadError(path);
|
|
345
|
+
}
|
|
346
|
+
return payload;
|
|
347
|
+
}
|
|
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
|
+
});
|
|
364
|
+
}
|
|
365
|
+
return new AuthSdkError({
|
|
366
|
+
code: authErrorCode.callbackValidationFailed,
|
|
367
|
+
details: error,
|
|
368
|
+
message: "Keycloak callback validation failed.",
|
|
369
|
+
status: null
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
function createNoopBroadcastSync() {
|
|
373
|
+
return {
|
|
374
|
+
close() {
|
|
375
|
+
},
|
|
376
|
+
publishSession() {
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function createSafeBroadcastSync(onSessionUpdated) {
|
|
381
|
+
try {
|
|
382
|
+
return createBroadcastSync({
|
|
383
|
+
onSessionUpdated
|
|
384
|
+
});
|
|
385
|
+
} catch (error) {
|
|
386
|
+
if (error instanceof AuthSdkError && error.code === authErrorCode.broadcastChannelUnsupported) {
|
|
387
|
+
return createNoopBroadcastSync();
|
|
388
|
+
}
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
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);
|
|
398
|
+
}
|
|
399
|
+
function createAuthClient() {
|
|
400
|
+
assertBrowserRuntime();
|
|
401
|
+
const sessionStore = new SessionStore();
|
|
402
|
+
const broadcastSync = createSafeBroadcastSync((session) => {
|
|
403
|
+
sessionStore.setSession(session);
|
|
404
|
+
});
|
|
405
|
+
const getSession = async (options) => {
|
|
406
|
+
const forceRefresh = options?.forceRefresh ?? false;
|
|
407
|
+
if (sessionStore.hasResolvedSession() && !forceRefresh) {
|
|
408
|
+
return sessionStore.getSessionIfResolved();
|
|
409
|
+
}
|
|
410
|
+
const session = await fetchCurrentSessionFromApi();
|
|
411
|
+
sessionStore.setSession(session);
|
|
412
|
+
broadcastSync.publishSession(session);
|
|
413
|
+
return session;
|
|
414
|
+
};
|
|
415
|
+
const requireSession = async () => {
|
|
416
|
+
const session = await getSession();
|
|
417
|
+
if (session) {
|
|
418
|
+
return session;
|
|
419
|
+
}
|
|
420
|
+
throw new AuthSdkError({
|
|
421
|
+
code: authErrorCode.unauthorized,
|
|
422
|
+
details: null,
|
|
423
|
+
message: "Authenticated session is required.",
|
|
424
|
+
status: httpStatus.unauthorized
|
|
425
|
+
});
|
|
426
|
+
};
|
|
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
|
+
});
|
|
441
|
+
}
|
|
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();
|
|
464
|
+
};
|
|
465
|
+
const logout = async () => {
|
|
466
|
+
await postEmpty(authEndpoints.logout);
|
|
467
|
+
sessionStore.setSession(null);
|
|
468
|
+
broadcastSync.publishSession(null);
|
|
469
|
+
};
|
|
470
|
+
const subscribe = sessionStore.subscribe.bind(sessionStore);
|
|
471
|
+
return {
|
|
472
|
+
completeCallbackRedirect,
|
|
473
|
+
getSession,
|
|
474
|
+
login,
|
|
475
|
+
logout,
|
|
476
|
+
processCallback,
|
|
477
|
+
requireSession,
|
|
478
|
+
subscribe
|
|
479
|
+
};
|
|
480
|
+
}
|
|
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
|
+
var AuthContext = createContext(null);
|
|
528
|
+
var initialLoadingState = true;
|
|
529
|
+
function toAuthError(error) {
|
|
530
|
+
if (error instanceof AuthSdkError) {
|
|
531
|
+
return error;
|
|
532
|
+
}
|
|
533
|
+
return new AuthSdkError({
|
|
534
|
+
code: "api_error",
|
|
535
|
+
details: error,
|
|
536
|
+
message: "Unexpected auth adapter error.",
|
|
537
|
+
status: null
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
function createDeferredAuthClient() {
|
|
541
|
+
let authClient = null;
|
|
542
|
+
const resolveClient = () => {
|
|
543
|
+
authClient ??= createAuthClient();
|
|
544
|
+
return authClient;
|
|
545
|
+
};
|
|
546
|
+
return {
|
|
547
|
+
completeCallbackRedirect() {
|
|
548
|
+
return resolveClient().completeCallbackRedirect();
|
|
549
|
+
},
|
|
550
|
+
getSession(options) {
|
|
551
|
+
return resolveClient().getSession(options);
|
|
552
|
+
},
|
|
553
|
+
login(options) {
|
|
554
|
+
resolveClient().login(options);
|
|
555
|
+
},
|
|
556
|
+
logout() {
|
|
557
|
+
return resolveClient().logout();
|
|
558
|
+
},
|
|
559
|
+
processCallback() {
|
|
560
|
+
return resolveClient().processCallback();
|
|
561
|
+
},
|
|
562
|
+
requireSession() {
|
|
563
|
+
return resolveClient().requireSession();
|
|
564
|
+
},
|
|
565
|
+
subscribe(listener) {
|
|
566
|
+
if (typeof window === "undefined") {
|
|
567
|
+
return () => {
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
return resolveClient().subscribe(listener);
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
function AuthProvider(props) {
|
|
575
|
+
const authClient = useMemo(() => props.client ?? createDeferredAuthClient(), [props.client]);
|
|
576
|
+
const [session, setSession] = useState(null);
|
|
577
|
+
const [error, setError] = useState(null);
|
|
578
|
+
const [isLoading, setIsLoading] = useState(initialLoadingState);
|
|
579
|
+
useEffect(() => {
|
|
580
|
+
let isActive = true;
|
|
581
|
+
const unsubscribe = authClient.subscribe((nextSession) => {
|
|
582
|
+
if (!isActive) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
setSession(nextSession);
|
|
586
|
+
setIsLoading(false);
|
|
587
|
+
});
|
|
588
|
+
void authClient.getSession().then((nextSession) => {
|
|
589
|
+
if (!isActive) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
setSession(nextSession);
|
|
593
|
+
}).catch((caughtError) => {
|
|
594
|
+
if (!isActive) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
setError(toAuthError(caughtError));
|
|
598
|
+
}).finally(() => {
|
|
599
|
+
if (!isActive) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
setIsLoading(false);
|
|
603
|
+
});
|
|
604
|
+
return () => {
|
|
605
|
+
isActive = false;
|
|
606
|
+
unsubscribe();
|
|
607
|
+
};
|
|
608
|
+
}, [authClient]);
|
|
609
|
+
const login = useCallback(
|
|
610
|
+
(options) => {
|
|
611
|
+
setError(null);
|
|
612
|
+
authClient.login(options);
|
|
613
|
+
},
|
|
614
|
+
[authClient]
|
|
615
|
+
);
|
|
616
|
+
const logout = useCallback(async () => {
|
|
617
|
+
setError(null);
|
|
618
|
+
try {
|
|
619
|
+
await authClient.logout();
|
|
620
|
+
setSession(null);
|
|
621
|
+
} catch (caughtError) {
|
|
622
|
+
const normalizedError = toAuthError(caughtError);
|
|
623
|
+
setError(normalizedError);
|
|
624
|
+
throw normalizedError;
|
|
625
|
+
}
|
|
626
|
+
}, [authClient]);
|
|
627
|
+
const refresh = useCallback(async () => {
|
|
628
|
+
setError(null);
|
|
629
|
+
try {
|
|
630
|
+
const nextSession = await authClient.getSession({ forceRefresh: true });
|
|
631
|
+
setSession(nextSession);
|
|
632
|
+
return nextSession;
|
|
633
|
+
} catch (caughtError) {
|
|
634
|
+
const normalizedError = toAuthError(caughtError);
|
|
635
|
+
setError(normalizedError);
|
|
636
|
+
throw normalizedError;
|
|
637
|
+
}
|
|
638
|
+
}, [authClient]);
|
|
639
|
+
const contextValue = useMemo(
|
|
640
|
+
() => ({
|
|
641
|
+
error,
|
|
642
|
+
isLoading,
|
|
643
|
+
login,
|
|
644
|
+
logout,
|
|
645
|
+
refresh,
|
|
646
|
+
session
|
|
647
|
+
}),
|
|
648
|
+
[error, isLoading, login, logout, refresh, session]
|
|
649
|
+
);
|
|
650
|
+
return /* @__PURE__ */ jsx(AuthContext.Provider, { value: contextValue, children: props.children });
|
|
651
|
+
}
|
|
652
|
+
var missingProviderErrorMessage = "useAuth must be used inside AuthProvider.";
|
|
653
|
+
function useAuth() {
|
|
654
|
+
const context = useContext(AuthContext);
|
|
655
|
+
if (context) {
|
|
656
|
+
return context;
|
|
657
|
+
}
|
|
658
|
+
throw new Error(missingProviderErrorMessage);
|
|
659
|
+
}
|
|
660
|
+
function resolveUnauthorizedContent(input, props) {
|
|
661
|
+
if (typeof input === "function") {
|
|
662
|
+
return input(props);
|
|
663
|
+
}
|
|
664
|
+
return input;
|
|
665
|
+
}
|
|
666
|
+
function AuthSessionGate(props) {
|
|
667
|
+
const auth = useAuth();
|
|
668
|
+
if (auth.isLoading) {
|
|
669
|
+
return /* @__PURE__ */ jsx(Fragment, { children: props.loading ?? null });
|
|
670
|
+
}
|
|
671
|
+
if (!auth.session) {
|
|
672
|
+
return /* @__PURE__ */ jsx(Fragment, { children: resolveUnauthorizedContent(props.unauthorized, {
|
|
673
|
+
login: auth.login
|
|
674
|
+
}) });
|
|
675
|
+
}
|
|
676
|
+
return /* @__PURE__ */ jsx(Fragment, { children: props.authorized(auth.session) });
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export { AuthCallbackHandler, AuthProvider, AuthSessionGate, useAuth, useAuthCallbackRedirect };
|
|
680
|
+
//# sourceMappingURL=react.js.map
|
|
681
|
+
//# sourceMappingURL=react.js.map
|