@jskit-ai/auth-web 0.1.4
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/package.descriptor.mjs +290 -0
- package/package.json +29 -0
- package/src/client/composables/useDefaultLoginView.js +935 -0
- package/src/client/composables/useDefaultSignOutView.js +113 -0
- package/src/client/index.js +19 -0
- package/src/client/lib/returnToPath.js +20 -0
- package/src/client/lib/surfaceLinkTarget.js +19 -0
- package/src/client/providers/AuthWebClientProvider.js +72 -0
- package/src/client/runtime/authGuardRuntime.js +499 -0
- package/src/client/runtime/authHttpClient.js +19 -0
- package/src/client/runtime/inject.js +43 -0
- package/src/client/runtime/tokens.js +7 -0
- package/src/client/runtime/useLoginView.js +7 -0
- package/src/client/runtime/useSignOut.js +121 -0
- package/src/client/views/AuthProfileMenuLinkItem.vue +83 -0
- package/src/client/views/AuthProfileWidget.vue +100 -0
- package/src/client/views/DefaultLoginView.vue +291 -0
- package/src/client/views/DefaultSignOutView.vue +58 -0
- package/src/server/constants/authActionIds.js +15 -0
- package/src/server/controllers/AuthController.js +183 -0
- package/src/server/providers/AuthRouteServiceProvider.js +31 -0
- package/src/server/providers/AuthWebServiceProvider.js +23 -0
- package/src/server/routes/authRoutes.js +244 -0
- package/src/server/services/AuthWebService.js +126 -0
- package/templates/src/pages/auth/login.vue +17 -0
- package/templates/src/pages/auth/signout.vue +17 -0
- package/templates/src/runtime/authGuardRuntime.js +7 -0
- package/templates/src/runtime/authHttpClient.js +1 -0
- package/templates/src/runtime/useSignOut.js +1 -0
- package/templates/src/views/auth/LoginView.vue +7 -0
- package/templates/src/views/auth/SignOutView.vue +7 -0
- package/test/authGuardRuntime.test.js +361 -0
- package/test/clientBoot.test.js +16 -0
- package/test/clientSurface.test.js +89 -0
- package/test/index.test.js +21 -0
- package/test/logoutFallback.test.js +50 -0
- package/test/providerRuntime.test.js +100 -0
- package/test/returnToPath.test.js +72 -0
- package/test/surfaceLinkTarget.test.js +80 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import { isTransientQueryError } from "@jskit-ai/kernel/shared/support";
|
|
2
|
+
import { AUTH_PATHS } from "@jskit-ai/auth-core/shared/authPaths";
|
|
3
|
+
import { isExternalLinkTarget } from "@jskit-ai/kernel/shared/support/linkPath";
|
|
4
|
+
|
|
5
|
+
const GLOBAL_GUARD_EVALUATOR_KEY = "__JSKIT_WEB_SHELL_GUARD_EVALUATOR__";
|
|
6
|
+
const AUTH_POLICY_AUTHENTICATED = "authenticated";
|
|
7
|
+
const DEFAULT_SESSION_PATH = AUTH_PATHS.SESSION;
|
|
8
|
+
const DEFAULT_LOGIN_ROUTE = "/auth/login";
|
|
9
|
+
const DEFAULT_REFRESH_ON_FOREGROUND = false;
|
|
10
|
+
const DEFAULT_REFRESH_ON_RECONNECT = false;
|
|
11
|
+
const DEFAULT_REALTIME_REFRESH_EVENTS = Object.freeze(["users.bootstrap.changed", "auth.session.changed"]);
|
|
12
|
+
const KEEP_PREVIOUS_AUTH_STATE = Symbol("keepPreviousAuthState");
|
|
13
|
+
const DEFAULT_AUTH_STATE = Object.freeze({
|
|
14
|
+
authenticated: false,
|
|
15
|
+
username: "",
|
|
16
|
+
oauthDefaultProvider: "",
|
|
17
|
+
oauthProviders: Object.freeze([])
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function asGlobalObject() {
|
|
21
|
+
if (typeof globalThis !== "object" || !globalThis) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return globalThis;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizePathname(pathname, fallback = "/") {
|
|
28
|
+
const raw = String(pathname || "").trim();
|
|
29
|
+
if (!raw || !raw.startsWith("/")) {
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
if (raw.startsWith("//")) {
|
|
33
|
+
return fallback;
|
|
34
|
+
}
|
|
35
|
+
return raw;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeLoginRoute(loginRoute, fallback = DEFAULT_LOGIN_ROUTE) {
|
|
39
|
+
const raw = String(loginRoute || "").trim();
|
|
40
|
+
if (isExternalLinkTarget(raw)) {
|
|
41
|
+
if (raw.startsWith("//")) {
|
|
42
|
+
try {
|
|
43
|
+
const baseOrigin =
|
|
44
|
+
typeof window === "object" && window?.location?.origin ? window.location.origin : "http://localhost";
|
|
45
|
+
return new URL(raw, baseOrigin).toString();
|
|
46
|
+
} catch {
|
|
47
|
+
return fallback;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return raw;
|
|
51
|
+
}
|
|
52
|
+
return normalizePathname(raw, fallback);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeOAuthProviders(rawProviders) {
|
|
56
|
+
if (!Array.isArray(rawProviders)) {
|
|
57
|
+
return Object.freeze([]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const providers = rawProviders
|
|
61
|
+
.map((entry) => {
|
|
62
|
+
if (!entry || typeof entry !== "object") {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const id = String(entry.id || "")
|
|
66
|
+
.trim()
|
|
67
|
+
.toLowerCase();
|
|
68
|
+
const label = String(entry.label || id).trim();
|
|
69
|
+
if (!id) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return Object.freeze({ id, label: label || id });
|
|
73
|
+
})
|
|
74
|
+
.filter(Boolean);
|
|
75
|
+
|
|
76
|
+
return Object.freeze(providers);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeAuthState(payload = {}) {
|
|
80
|
+
const oauthProviders = normalizeOAuthProviders(payload.oauthProviders);
|
|
81
|
+
const oauthDefaultProvider = String(payload.oauthDefaultProvider || "")
|
|
82
|
+
.trim()
|
|
83
|
+
.toLowerCase();
|
|
84
|
+
const authenticated = Boolean(payload.authenticated);
|
|
85
|
+
const username = authenticated ? String(payload.username || "").trim() : "";
|
|
86
|
+
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
authenticated,
|
|
89
|
+
username,
|
|
90
|
+
oauthDefaultProvider,
|
|
91
|
+
oauthProviders
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isPlacementRuntime(value) {
|
|
96
|
+
return Boolean(value && typeof value.getContext === "function" && typeof value.setContext === "function");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isAuthGuardRuntime(value) {
|
|
100
|
+
return Boolean(
|
|
101
|
+
value &&
|
|
102
|
+
typeof value.initialize === "function" &&
|
|
103
|
+
typeof value.refresh === "function" &&
|
|
104
|
+
typeof value.getState === "function"
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function applyAuthContext(nextState, placementRuntime) {
|
|
109
|
+
placementRuntime.setContext(
|
|
110
|
+
{
|
|
111
|
+
auth: {
|
|
112
|
+
authenticated: nextState.authenticated,
|
|
113
|
+
oauthDefaultProvider: nextState.oauthDefaultProvider,
|
|
114
|
+
oauthProviders: nextState.oauthProviders
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
source: "auth-web"
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function readSessionState({ sessionPath = DEFAULT_SESSION_PATH, fetchImplementation = globalThis.fetch } = {}) {
|
|
124
|
+
if (typeof fetchImplementation !== "function") {
|
|
125
|
+
return DEFAULT_AUTH_STATE;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const response = await fetchImplementation(sessionPath, {
|
|
130
|
+
method: "GET",
|
|
131
|
+
credentials: "include",
|
|
132
|
+
headers: {
|
|
133
|
+
accept: "application/json"
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
if (isTransientQueryError(response)) {
|
|
139
|
+
return KEEP_PREVIOUS_AUTH_STATE;
|
|
140
|
+
}
|
|
141
|
+
return DEFAULT_AUTH_STATE;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const payload = await response.json();
|
|
145
|
+
return normalizeAuthState(payload);
|
|
146
|
+
} catch {
|
|
147
|
+
return KEEP_PREVIOUS_AUTH_STATE;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function resolveReturnToPath(context, { absolute = false } = {}) {
|
|
152
|
+
if (absolute) {
|
|
153
|
+
const href = String(context?.location?.href || (typeof window === "object" ? window.location?.href : "") || "").trim();
|
|
154
|
+
if (href && isExternalLinkTarget(href)) {
|
|
155
|
+
return href;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const pathname = normalizePathname(
|
|
160
|
+
context?.location?.pathname || (typeof window === "object" ? window.location?.pathname : "") || "",
|
|
161
|
+
"/"
|
|
162
|
+
);
|
|
163
|
+
const search = String(
|
|
164
|
+
context?.location?.search || (typeof window === "object" ? window.location?.search : "") || ""
|
|
165
|
+
).trim();
|
|
166
|
+
if (!search) {
|
|
167
|
+
return pathname;
|
|
168
|
+
}
|
|
169
|
+
return `${pathname}${search}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function toLoginRedirect(loginRoute, context) {
|
|
173
|
+
const normalizedLoginRoute = normalizeLoginRoute(loginRoute, DEFAULT_LOGIN_ROUTE);
|
|
174
|
+
const externalLoginRoute = isExternalLinkTarget(normalizedLoginRoute);
|
|
175
|
+
const returnTo = resolveReturnToPath(context, { absolute: externalLoginRoute });
|
|
176
|
+
if (externalLoginRoute) {
|
|
177
|
+
try {
|
|
178
|
+
const parsed = new URL(
|
|
179
|
+
normalizedLoginRoute,
|
|
180
|
+
typeof window === "object" && window?.location?.origin ? window.location.origin : "http://localhost"
|
|
181
|
+
);
|
|
182
|
+
if (returnTo) {
|
|
183
|
+
parsed.searchParams.set("returnTo", returnTo);
|
|
184
|
+
}
|
|
185
|
+
return parsed.toString();
|
|
186
|
+
} catch {
|
|
187
|
+
return DEFAULT_LOGIN_ROUTE;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const params = new URLSearchParams();
|
|
192
|
+
if (returnTo) {
|
|
193
|
+
params.set("returnTo", returnTo);
|
|
194
|
+
}
|
|
195
|
+
const query = params.toString();
|
|
196
|
+
return query ? `${normalizedLoginRoute}?${query}` : normalizedLoginRoute;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function evaluateAuthGuard({ guard, context, loginRoute, authState = DEFAULT_AUTH_STATE }) {
|
|
200
|
+
const guardPolicy = String(guard?.policy || "")
|
|
201
|
+
.trim()
|
|
202
|
+
.toLowerCase();
|
|
203
|
+
|
|
204
|
+
if (guardPolicy !== AUTH_POLICY_AUTHENTICATED) {
|
|
205
|
+
return {
|
|
206
|
+
allow: true,
|
|
207
|
+
redirectTo: "",
|
|
208
|
+
reason: ""
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (authState.authenticated) {
|
|
213
|
+
return {
|
|
214
|
+
allow: true,
|
|
215
|
+
redirectTo: "",
|
|
216
|
+
reason: ""
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const redirectTo = toLoginRedirect(loginRoute, context);
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
allow: false,
|
|
224
|
+
redirectTo,
|
|
225
|
+
reason: "auth-required"
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function installGuardEvaluator({ loginRoute = DEFAULT_LOGIN_ROUTE, getAuthState }) {
|
|
230
|
+
const root = asGlobalObject();
|
|
231
|
+
if (!root || typeof getAuthState !== "function") {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
root[GLOBAL_GUARD_EVALUATOR_KEY] = ({ guard, context } = {}) => {
|
|
236
|
+
return evaluateAuthGuard({
|
|
237
|
+
guard,
|
|
238
|
+
context,
|
|
239
|
+
loginRoute,
|
|
240
|
+
authState: getAuthState()
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function normalizeRuntimePath(value, fallback) {
|
|
246
|
+
const raw = String(value || "").trim();
|
|
247
|
+
return raw || fallback;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function asEventTarget(value) {
|
|
251
|
+
if (!value || typeof value !== "object") {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
if (typeof value.addEventListener !== "function" || typeof value.removeEventListener !== "function") {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
return value;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function getWindowEventTarget() {
|
|
261
|
+
if (typeof window !== "object" || !window) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
return asEventTarget(window);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function getDocumentEventTarget() {
|
|
268
|
+
if (typeof document !== "object" || !document) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
return asEventTarget(document);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function isDocumentVisible() {
|
|
275
|
+
if (typeof document !== "object" || !document) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
const visibilityState = String(document.visibilityState || "").trim().toLowerCase();
|
|
279
|
+
if (!visibilityState) {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
return visibilityState === "visible";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function asRealtimeSocket(value) {
|
|
286
|
+
if (!value || typeof value !== "object") {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
if (typeof value.on !== "function" || typeof value.off !== "function") {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
return value;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function normalizeRealtimeRefreshEvents(value) {
|
|
296
|
+
const source = Array.isArray(value) ? value : [value];
|
|
297
|
+
const deduped = [];
|
|
298
|
+
|
|
299
|
+
for (const entry of source) {
|
|
300
|
+
const eventName = String(entry || "").trim();
|
|
301
|
+
if (!eventName || deduped.includes(eventName)) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
deduped.push(eventName);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (deduped.length < 1) {
|
|
308
|
+
return DEFAULT_REALTIME_REFRESH_EVENTS;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return Object.freeze(deduped);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function createAuthGuardRuntime({
|
|
315
|
+
placementRuntime = null,
|
|
316
|
+
sessionPath = DEFAULT_SESSION_PATH,
|
|
317
|
+
loginRoute = DEFAULT_LOGIN_ROUTE,
|
|
318
|
+
fetchImplementation = globalThis.fetch,
|
|
319
|
+
refreshOnForeground = DEFAULT_REFRESH_ON_FOREGROUND,
|
|
320
|
+
refreshOnReconnect = DEFAULT_REFRESH_ON_RECONNECT,
|
|
321
|
+
realtimeSocket = null,
|
|
322
|
+
realtimeRefreshEvents = DEFAULT_REALTIME_REFRESH_EVENTS
|
|
323
|
+
} = {}) {
|
|
324
|
+
if (!isPlacementRuntime(placementRuntime)) {
|
|
325
|
+
throw new Error("createAuthGuardRuntime requires a web placement runtime with getContext()/setContext().");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let currentSessionPath = normalizeRuntimePath(sessionPath, DEFAULT_SESSION_PATH);
|
|
329
|
+
let currentLoginRoute = normalizeLoginRoute(loginRoute, DEFAULT_LOGIN_ROUTE);
|
|
330
|
+
const foregroundRefreshEnabled = refreshOnForeground === true;
|
|
331
|
+
const reconnectRefreshEnabled = refreshOnReconnect === true;
|
|
332
|
+
const socket = asRealtimeSocket(realtimeSocket);
|
|
333
|
+
const realtimeEvents = normalizeRealtimeRefreshEvents(realtimeRefreshEvents);
|
|
334
|
+
let authState = DEFAULT_AUTH_STATE;
|
|
335
|
+
let activeRefreshPromise = null;
|
|
336
|
+
let listenersInstalled = false;
|
|
337
|
+
const listeners = new Set();
|
|
338
|
+
|
|
339
|
+
function notifyListeners() {
|
|
340
|
+
for (const listener of listeners) {
|
|
341
|
+
try {
|
|
342
|
+
listener(authState);
|
|
343
|
+
} catch {
|
|
344
|
+
// Ignore listener failures to keep runtime updates safe.
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function getState() {
|
|
350
|
+
return authState;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function subscribe(listener) {
|
|
354
|
+
if (typeof listener !== "function") {
|
|
355
|
+
return () => {};
|
|
356
|
+
}
|
|
357
|
+
listeners.add(listener);
|
|
358
|
+
return () => {
|
|
359
|
+
listeners.delete(listener);
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function refresh({ sessionPath: nextSessionPath } = {}) {
|
|
364
|
+
currentSessionPath = normalizeRuntimePath(nextSessionPath, currentSessionPath);
|
|
365
|
+
if (activeRefreshPromise) {
|
|
366
|
+
return activeRefreshPromise;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
activeRefreshPromise = (async () => {
|
|
370
|
+
const nextAuthState = await readSessionState({
|
|
371
|
+
sessionPath: currentSessionPath,
|
|
372
|
+
fetchImplementation
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
if (nextAuthState === KEEP_PREVIOUS_AUTH_STATE) {
|
|
376
|
+
return authState;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
authState = nextAuthState;
|
|
380
|
+
applyAuthContext(authState, placementRuntime);
|
|
381
|
+
notifyListeners();
|
|
382
|
+
return authState;
|
|
383
|
+
})();
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
return await activeRefreshPromise;
|
|
387
|
+
} finally {
|
|
388
|
+
activeRefreshPromise = null;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function initialize({ sessionPath: nextSessionPath, loginRoute: nextLoginRoute } = {}) {
|
|
393
|
+
currentSessionPath = normalizeRuntimePath(nextSessionPath, currentSessionPath);
|
|
394
|
+
currentLoginRoute = normalizeLoginRoute(nextLoginRoute, currentLoginRoute);
|
|
395
|
+
installGuardEvaluator({
|
|
396
|
+
loginRoute: currentLoginRoute,
|
|
397
|
+
getAuthState: () => authState
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
if (!listenersInstalled) {
|
|
401
|
+
listenersInstalled = true;
|
|
402
|
+
const onReconnect = () => {
|
|
403
|
+
void refresh();
|
|
404
|
+
};
|
|
405
|
+
const onWindowFocus = () => {
|
|
406
|
+
if (foregroundRefreshEnabled) {
|
|
407
|
+
void refresh();
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
const onVisibilityChange = () => {
|
|
411
|
+
if (foregroundRefreshEnabled && isDocumentVisible()) {
|
|
412
|
+
void refresh();
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
const onRealtimeRefresh = () => {
|
|
416
|
+
void refresh();
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const windowTarget = getWindowEventTarget();
|
|
420
|
+
if (windowTarget) {
|
|
421
|
+
if (reconnectRefreshEnabled) {
|
|
422
|
+
windowTarget.addEventListener("online", onReconnect);
|
|
423
|
+
}
|
|
424
|
+
if (foregroundRefreshEnabled) {
|
|
425
|
+
windowTarget.addEventListener("focus", onWindowFocus);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const documentTarget = getDocumentEventTarget();
|
|
430
|
+
if (foregroundRefreshEnabled && documentTarget) {
|
|
431
|
+
documentTarget.addEventListener("visibilitychange", onVisibilityChange);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (socket) {
|
|
435
|
+
for (const eventName of realtimeEvents) {
|
|
436
|
+
socket.on(eventName, onRealtimeRefresh);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return refresh({
|
|
442
|
+
sessionPath: currentSessionPath
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return Object.freeze({
|
|
447
|
+
initialize,
|
|
448
|
+
refresh,
|
|
449
|
+
getState,
|
|
450
|
+
subscribe
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function resolveRuntimeInput(input = {}) {
|
|
455
|
+
if (isAuthGuardRuntime(input)) {
|
|
456
|
+
return {
|
|
457
|
+
runtime: input,
|
|
458
|
+
options: {}
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
463
|
+
throw new TypeError("Auth guard runtime API requires options with runtime/authGuardRuntime.");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const runtime = input.runtime || input.authGuardRuntime || null;
|
|
467
|
+
if (!isAuthGuardRuntime(runtime)) {
|
|
468
|
+
throw new Error("Auth guard runtime API requires runtime/authGuardRuntime from createAuthGuardRuntime().");
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const { runtime: _runtime, authGuardRuntime: _authGuardRuntime, ...options } = input;
|
|
472
|
+
return {
|
|
473
|
+
runtime,
|
|
474
|
+
options
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function initializeAuthGuardRuntime(input = {}) {
|
|
479
|
+
const { runtime, options } = resolveRuntimeInput(input);
|
|
480
|
+
return runtime.initialize(options);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function refreshAuthGuardState(input = {}) {
|
|
484
|
+
const { runtime, options } = resolveRuntimeInput(input);
|
|
485
|
+
return runtime.refresh(options);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function getAuthGuardState(input = {}) {
|
|
489
|
+
const { runtime } = resolveRuntimeInput(input);
|
|
490
|
+
return runtime.getState();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export {
|
|
494
|
+
createAuthGuardRuntime,
|
|
495
|
+
isAuthGuardRuntime,
|
|
496
|
+
initializeAuthGuardRuntime,
|
|
497
|
+
refreshAuthGuardState,
|
|
498
|
+
getAuthGuardState
|
|
499
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createHttpClient } from "@jskit-ai/http-runtime/client";
|
|
2
|
+
import { AUTH_PATHS } from "@jskit-ai/auth-core/shared/authPaths";
|
|
3
|
+
|
|
4
|
+
const authHttpClient = createHttpClient({
|
|
5
|
+
credentials: "include",
|
|
6
|
+
csrf: {
|
|
7
|
+
sessionPath: AUTH_PATHS.SESSION
|
|
8
|
+
}
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
function authHttpRequest(url, options = {}, state = undefined) {
|
|
12
|
+
return authHttpClient.request(url, options, state);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function clearAuthCsrfTokenCache() {
|
|
16
|
+
authHttpClient.clearCsrfTokenCache();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { authHttpRequest, clearAuthCsrfTokenCache };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { inject } from "vue";
|
|
2
|
+
import { isAuthGuardRuntime } from "./authGuardRuntime.js";
|
|
3
|
+
import { AUTH_GUARD_RUNTIME_INJECTION_KEY } from "./tokens.js";
|
|
4
|
+
|
|
5
|
+
const EMPTY_AUTH_GUARD_STATE = Object.freeze({
|
|
6
|
+
authenticated: false,
|
|
7
|
+
username: "",
|
|
8
|
+
oauthDefaultProvider: "",
|
|
9
|
+
oauthProviders: Object.freeze([])
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const EMPTY_AUTH_GUARD_RUNTIME = Object.freeze({
|
|
13
|
+
async initialize() {
|
|
14
|
+
return EMPTY_AUTH_GUARD_STATE;
|
|
15
|
+
},
|
|
16
|
+
async refresh() {
|
|
17
|
+
return EMPTY_AUTH_GUARD_STATE;
|
|
18
|
+
},
|
|
19
|
+
getState() {
|
|
20
|
+
return EMPTY_AUTH_GUARD_STATE;
|
|
21
|
+
},
|
|
22
|
+
subscribe() {
|
|
23
|
+
return () => {};
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function useAuthGuardRuntime({ required = false } = {}) {
|
|
28
|
+
const runtime = inject(AUTH_GUARD_RUNTIME_INJECTION_KEY, null);
|
|
29
|
+
if (isAuthGuardRuntime(runtime)) {
|
|
30
|
+
return runtime;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (required) {
|
|
34
|
+
throw new Error("Auth guard runtime is not available in Vue injection context.");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return EMPTY_AUTH_GUARD_RUNTIME;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
EMPTY_AUTH_GUARD_RUNTIME,
|
|
42
|
+
useAuthGuardRuntime
|
|
43
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { runAuthSignOutFlow } from "@jskit-ai/auth-core/client/signOutFlow";
|
|
2
|
+
import { AUTH_PATHS } from "@jskit-ai/auth-core/shared/authPaths";
|
|
3
|
+
import { isAuthGuardRuntime } from "./authGuardRuntime.js";
|
|
4
|
+
import { authHttpRequest, clearAuthCsrfTokenCache } from "./authHttpClient.js";
|
|
5
|
+
import { useAuthGuardRuntime } from "./inject.js";
|
|
6
|
+
import { normalizeAuthReturnToPath } from "../lib/returnToPath.js";
|
|
7
|
+
|
|
8
|
+
const SIGN_OUT_ENDPOINT = AUTH_PATHS.LOGOUT;
|
|
9
|
+
const SESSION_ENDPOINT = AUTH_PATHS.SESSION;
|
|
10
|
+
|
|
11
|
+
async function readSessionState() {
|
|
12
|
+
try {
|
|
13
|
+
const payload = await authHttpRequest(SESSION_ENDPOINT, { method: "GET" });
|
|
14
|
+
return payload && typeof payload === "object" ? payload : {};
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function waitForSignedOutState({ attempts = 4, delayMs = 120 } = {}) {
|
|
21
|
+
for (let index = 0; index < attempts; index += 1) {
|
|
22
|
+
const session = await readSessionState();
|
|
23
|
+
if (!session?.authenticated) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (index < attempts - 1) {
|
|
28
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createHttpLogoutApi() {
|
|
36
|
+
return {
|
|
37
|
+
async logout() {
|
|
38
|
+
const initialSession = await readSessionState();
|
|
39
|
+
const wasAuthenticated = Boolean(initialSession?.authenticated);
|
|
40
|
+
await authHttpRequest(SIGN_OUT_ENDPOINT, { method: "POST" });
|
|
41
|
+
|
|
42
|
+
const signedOut = await waitForSignedOutState();
|
|
43
|
+
if (signedOut) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!wasAuthenticated) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
throw new Error("Logout did not terminate the server session.");
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function performSignOutRequest({ authGuardRuntime } = {}) {
|
|
57
|
+
if (!isAuthGuardRuntime(authGuardRuntime)) {
|
|
58
|
+
throw new TypeError("performSignOutRequest requires authGuardRuntime from useAuthGuardRuntime().");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await runAuthSignOutFlow({
|
|
62
|
+
authApi: createHttpLogoutApi(),
|
|
63
|
+
clearCsrfTokenCache: clearAuthCsrfTokenCache
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const guardState = await authGuardRuntime.refresh();
|
|
67
|
+
if (guardState?.authenticated) {
|
|
68
|
+
throw new Error("Sign out did not complete because the session is still authenticated.");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return guardState;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createSignOutAction({ currentSurface, goToEntry, authGuardRuntime, returnTo = "", resolveReturnToPath = null } = {}) {
|
|
75
|
+
if (typeof goToEntry !== "function") {
|
|
76
|
+
throw new TypeError("createSignOutAction requires goToEntry().");
|
|
77
|
+
}
|
|
78
|
+
if (!isAuthGuardRuntime(authGuardRuntime)) {
|
|
79
|
+
throw new TypeError("createSignOutAction requires authGuardRuntime from useAuthGuardRuntime().");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return async function signOut() {
|
|
83
|
+
const resolvedByCallback =
|
|
84
|
+
typeof resolveReturnToPath === "function" ? normalizeAuthReturnToPath(resolveReturnToPath(), "") : "";
|
|
85
|
+
const resolvedByOption = normalizeAuthReturnToPath(returnTo, "");
|
|
86
|
+
const resolvedByCurrentSurface = normalizeAuthReturnToPath(currentSurface?.value, "");
|
|
87
|
+
const resolvedReturnToPath = resolvedByCallback || resolvedByOption || resolvedByCurrentSurface || "/";
|
|
88
|
+
const redirectParams = new URLSearchParams({
|
|
89
|
+
returnTo: resolvedReturnToPath
|
|
90
|
+
});
|
|
91
|
+
const redirectRoute = `/auth/login?${redirectParams.toString()}`;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await performSignOutRequest({
|
|
95
|
+
authGuardRuntime
|
|
96
|
+
});
|
|
97
|
+
await goToEntry({ resolvedRoute: redirectRoute });
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (import.meta.env?.DEV) {
|
|
100
|
+
console.error("Sign out request failed.", error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function useSignOut(options = {}) {
|
|
107
|
+
const authGuardRuntime = isAuthGuardRuntime(options?.authGuardRuntime)
|
|
108
|
+
? options.authGuardRuntime
|
|
109
|
+
: useAuthGuardRuntime({
|
|
110
|
+
required: true
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return Object.freeze({
|
|
114
|
+
signOut: createSignOutAction({
|
|
115
|
+
...options,
|
|
116
|
+
authGuardRuntime
|
|
117
|
+
})
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export { useSignOut, createSignOutAction, performSignOutRequest };
|