@jskit-ai/auth-web 0.1.159 → 0.1.161
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.json +29 -135
- package/patterns/auth-surface/PATTERN.md +84 -0
- package/patterns/auth-surface/example/.vibe64/bin/preview-identity +5 -0
- package/src/client/index.js +1 -4
- package/src/client/providers/AuthWebClientProvider.js +32 -60
- package/src/client/runtime/authClient.js +79 -0
- package/src/client/runtime/authGuardRuntime.js +28 -7
- package/src/server/AuthWebFeature.js +41 -0
- package/src/server/managedPreviewIdentity.js +344 -0
- package/src/server/services/AuthWebService.js +4 -16
- package/test/clientBoot.test.js +3 -3
- package/test/clientSurface.test.js +6 -16
- package/test/managedPreviewIdentity.test.js +89 -0
- package/test/packageMetadataOwnership.test.js +3 -15
- package/test/provider.test.js +30 -220
- package/test/providerRuntime.test.js +92 -307
- package/src/client/providers/bootAuthClientProvider.js +0 -49
- package/src/server/providers/AuthRouteServiceProvider.js +0 -31
- package/src/server/providers/AuthWebServiceProvider.js +0 -38
- /package/{templates → patterns/auth-surface/example}/src/pages/auth/login.vue +0 -0
- /package/{templates → patterns/auth-surface/example}/src/pages/auth/reset-password.vue +0 -0
- /package/{templates → patterns/auth-surface/example}/src/pages/auth/signout.vue +0 -0
- /package/{templates → patterns/auth-surface/example}/src/runtime/authGuardRuntime.js +0 -0
- /package/{templates → patterns/auth-surface/example}/src/runtime/authHttpClient.js +0 -0
- /package/{templates → patterns/auth-surface/example}/src/runtime/useSignOut.js +0 -0
- /package/{templates → patterns/auth-surface/example}/src/views/auth/LoginView.vue +0 -0
- /package/{templates → patterns/auth-surface/example}/src/views/auth/ResetPasswordView.vue +0 -0
- /package/{templates → patterns/auth-surface/example}/src/views/auth/SignOutView.vue +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/auth-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.161",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"./server/controllers/AuthController": "./src/server/controllers/AuthController.js",
|
|
10
10
|
"./server/services/AuthWebService": "./src/server/services/AuthWebService.js",
|
|
11
11
|
"./server/routes/authRoutes": "./src/server/routes/authRoutes.js",
|
|
12
|
+
"./server/AuthWebFeature": "./src/server/AuthWebFeature.js",
|
|
13
|
+
"./server/managedPreviewIdentity": "./src/server/managedPreviewIdentity.js",
|
|
12
14
|
"./client": "./src/client/index.js",
|
|
13
15
|
"./client/views/DefaultLoginView": "./src/client/views/DefaultLoginView.vue",
|
|
14
16
|
"./client/views/DefaultResetPasswordView": "./src/client/views/DefaultResetPasswordView.vue",
|
|
@@ -20,10 +22,10 @@
|
|
|
20
22
|
"./test/playwright": "./src/test/playwright.js"
|
|
21
23
|
},
|
|
22
24
|
"dependencies": {
|
|
23
|
-
"@jskit-ai/auth-core": "0.1.
|
|
24
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
25
|
-
"@jskit-ai/kernel": "0.1.
|
|
26
|
-
"@jskit-ai/shell-web": "0.1.
|
|
25
|
+
"@jskit-ai/auth-core": "0.1.159",
|
|
26
|
+
"@jskit-ai/http-runtime": "0.1.159",
|
|
27
|
+
"@jskit-ai/kernel": "0.1.161",
|
|
28
|
+
"@jskit-ai/shell-web": "0.1.165",
|
|
27
29
|
"@mdi/js": "^7.4.47"
|
|
28
30
|
},
|
|
29
31
|
"peerDependencies": {
|
|
@@ -38,28 +40,23 @@
|
|
|
38
40
|
"kind": "runtime",
|
|
39
41
|
"capabilities": {
|
|
40
42
|
"provides": [
|
|
41
|
-
"auth.
|
|
43
|
+
"auth.web",
|
|
42
44
|
"auth.web-login"
|
|
43
45
|
],
|
|
44
46
|
"requires": [
|
|
45
|
-
"auth.
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"runtime.web-placement"
|
|
47
|
+
"auth.service",
|
|
48
|
+
"runtime.actions",
|
|
49
|
+
"runtime.env",
|
|
50
|
+
"runtime.http"
|
|
50
51
|
]
|
|
51
52
|
},
|
|
52
53
|
"runtime": {
|
|
53
54
|
"server": {
|
|
54
|
-
"providerEntrypoint": "src/server/
|
|
55
|
+
"providerEntrypoint": "src/server/AuthWebFeature.js",
|
|
55
56
|
"providers": [
|
|
56
57
|
{
|
|
57
|
-
"entrypoint": "src/server/
|
|
58
|
-
"export": "
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
"entrypoint": "src/server/providers/AuthRouteServiceProvider.js",
|
|
62
|
-
"export": "AuthRouteServiceProvider"
|
|
58
|
+
"entrypoint": "src/server/AuthWebFeature.js",
|
|
59
|
+
"export": "AuthWebFeature"
|
|
63
60
|
}
|
|
64
61
|
]
|
|
65
62
|
},
|
|
@@ -86,20 +83,19 @@
|
|
|
86
83
|
{
|
|
87
84
|
"subpath": "./test/playwright",
|
|
88
85
|
"summary": "Exports the localhost-only Playwright helper for creating a dev-auth session in the tested browser context."
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"subpath": "./server/managedPreviewIdentity",
|
|
89
|
+
"summary": "Runs the app-owned managed-preview identity command against JSKIT's development auth routes."
|
|
89
90
|
}
|
|
90
91
|
],
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
"
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"auth.login.useLoginView",
|
|
99
|
-
"auth.web.profile.widget",
|
|
100
|
-
"auth.web.profile.menu.link-item"
|
|
101
|
-
]
|
|
102
|
-
}
|
|
92
|
+
"clientCapabilities": [
|
|
93
|
+
"runtime.auth-guard.client",
|
|
94
|
+
"auth.login.component",
|
|
95
|
+
"auth.login.useLoginView",
|
|
96
|
+
"auth.web.profile.widget",
|
|
97
|
+
"auth.web.profile.menu.link-item"
|
|
98
|
+
]
|
|
103
99
|
},
|
|
104
100
|
"server": {
|
|
105
101
|
"routes": [
|
|
@@ -268,7 +264,7 @@
|
|
|
268
264
|
],
|
|
269
265
|
"order": 1000,
|
|
270
266
|
"componentToken": "auth.web.profile.widget",
|
|
271
|
-
"source": "
|
|
267
|
+
"source": "src/client/views/AuthProfileWidget.vue"
|
|
272
268
|
},
|
|
273
269
|
{
|
|
274
270
|
"id": "auth.profile.menu.sign-in",
|
|
@@ -279,7 +275,7 @@
|
|
|
279
275
|
],
|
|
280
276
|
"order": 200,
|
|
281
277
|
"when": "auth.authenticated !== true",
|
|
282
|
-
"source": "
|
|
278
|
+
"source": "patterns/auth-surface/example/src/pages/auth/login.vue"
|
|
283
279
|
},
|
|
284
280
|
{
|
|
285
281
|
"id": "auth.profile.menu.sign-out",
|
|
@@ -290,113 +286,11 @@
|
|
|
290
286
|
],
|
|
291
287
|
"order": 1000,
|
|
292
288
|
"when": "auth.authenticated === true",
|
|
293
|
-
"source": "
|
|
289
|
+
"source": "patterns/auth-surface/example/src/pages/auth/signout.vue"
|
|
294
290
|
}
|
|
295
291
|
]
|
|
296
292
|
}
|
|
297
293
|
}
|
|
298
|
-
},
|
|
299
|
-
"mutations": {
|
|
300
|
-
"dependencies": {
|
|
301
|
-
"runtime": {
|
|
302
|
-
"@mdi/js": "^7.4.47",
|
|
303
|
-
"@jskit-ai/auth-core": "0.1.157",
|
|
304
|
-
"@jskit-ai/http-runtime": "0.1.157",
|
|
305
|
-
"@jskit-ai/kernel": "0.1.159",
|
|
306
|
-
"@jskit-ai/shell-web": "0.1.163"
|
|
307
|
-
},
|
|
308
|
-
"dev": {}
|
|
309
|
-
},
|
|
310
|
-
"packageJson": {
|
|
311
|
-
"scripts": {
|
|
312
|
-
"server:auth": "SERVER_SURFACE=auth node ./bin/server.js",
|
|
313
|
-
"dev:auth": "VITE_SURFACE=auth vite",
|
|
314
|
-
"build:auth": "VITE_SURFACE=auth vite build"
|
|
315
|
-
}
|
|
316
|
-
},
|
|
317
|
-
"procfile": {},
|
|
318
|
-
"files": [
|
|
319
|
-
{
|
|
320
|
-
"from": "templates/src/views/auth/LoginView.vue",
|
|
321
|
-
"to": "src/views/auth/LoginView.vue",
|
|
322
|
-
"ownership": "app",
|
|
323
|
-
"reason": "Install minimal login container that renders the module-provided DefaultLoginView by default.",
|
|
324
|
-
"category": "auth-web",
|
|
325
|
-
"id": "auth-view-login"
|
|
326
|
-
},
|
|
327
|
-
{
|
|
328
|
-
"from": "templates/src/views/auth/SignOutView.vue",
|
|
329
|
-
"to": "src/views/auth/SignOutView.vue",
|
|
330
|
-
"ownership": "app",
|
|
331
|
-
"reason": "Install minimal sign-out container that renders the module-provided SignOutView by default (edit the scaffolded file to customize).",
|
|
332
|
-
"category": "auth-web",
|
|
333
|
-
"id": "auth-view-signout"
|
|
334
|
-
},
|
|
335
|
-
{
|
|
336
|
-
"from": "templates/src/views/auth/ResetPasswordView.vue",
|
|
337
|
-
"to": "src/views/auth/ResetPasswordView.vue",
|
|
338
|
-
"ownership": "app",
|
|
339
|
-
"reason": "Install minimal password reset container that renders the module-provided DefaultResetPasswordView by default.",
|
|
340
|
-
"category": "auth-web",
|
|
341
|
-
"id": "auth-view-reset-password"
|
|
342
|
-
},
|
|
343
|
-
{
|
|
344
|
-
"from": "templates/src/pages/auth/login.vue",
|
|
345
|
-
"to": "src/pages/auth/login.vue",
|
|
346
|
-
"ownership": "app",
|
|
347
|
-
"reason": "Provide an auth-surface /auth/login wrapper that renders the package login view.",
|
|
348
|
-
"category": "auth-web",
|
|
349
|
-
"id": "auth-page-login"
|
|
350
|
-
},
|
|
351
|
-
{
|
|
352
|
-
"from": "templates/src/pages/auth/signout.vue",
|
|
353
|
-
"to": "src/pages/auth/signout.vue",
|
|
354
|
-
"ownership": "app",
|
|
355
|
-
"reason": "Provide an auth-surface /auth/signout wrapper that renders the package sign-out view.",
|
|
356
|
-
"category": "auth-web",
|
|
357
|
-
"id": "auth-page-signout"
|
|
358
|
-
},
|
|
359
|
-
{
|
|
360
|
-
"from": "templates/src/pages/auth/reset-password.vue",
|
|
361
|
-
"to": "src/pages/auth/reset-password.vue",
|
|
362
|
-
"ownership": "app",
|
|
363
|
-
"reason": "Provide an auth-surface /auth/reset-password wrapper that renders the package password reset view.",
|
|
364
|
-
"category": "auth-web",
|
|
365
|
-
"id": "auth-page-reset-password"
|
|
366
|
-
}
|
|
367
|
-
],
|
|
368
|
-
"text": [
|
|
369
|
-
{
|
|
370
|
-
"op": "append-text",
|
|
371
|
-
"file": "config/public.js",
|
|
372
|
-
"position": "bottom",
|
|
373
|
-
"skipIfContains": "config.surfaceDefinitions.auth = {",
|
|
374
|
-
"value": "\nconfig.surfaceDefinitions.auth = {\n id: \"auth\",\n label: \"Auth\",\n pagesRoot: \"auth\",\n enabled: true,\n requiresAuth: false,\n requiresWorkspace: false,\n origin: \"\"\n};\n",
|
|
375
|
-
"reason": "Register auth surface definition in public surface config.",
|
|
376
|
-
"category": "auth-web",
|
|
377
|
-
"id": "auth-web-surface-config-auth"
|
|
378
|
-
},
|
|
379
|
-
{
|
|
380
|
-
"op": "append-text",
|
|
381
|
-
"file": "src/placement.js",
|
|
382
|
-
"position": "bottom",
|
|
383
|
-
"skipIfContains": "id: \"auth.profile.widget\"",
|
|
384
|
-
"value": "\naddPlacement({\n id: \"auth.profile.widget\",\n target: \"shell.status\",\n kind: \"component\",\n surfaces: [\"*\"],\n order: 1000,\n componentToken: \"auth.web.profile.widget\"\n});\n\naddPlacement({\n id: \"auth.profile.menu.sign-in\",\n target: \"auth.profile-menu\",\n kind: \"link\",\n surfaces: [\"*\"],\n order: 200,\n props: {\n label: \"Sign in\",\n to: \"/auth/login\"\n },\n when: ({ auth }) => auth?.authenticated !== true\n});\n\naddPlacement({\n id: \"auth.profile.menu.sign-out\",\n target: \"auth.profile-menu\",\n kind: \"link\",\n surfaces: [\"*\"],\n order: 1000,\n props: {\n label: \"Sign out\",\n to: \"/auth/signout\"\n },\n when: ({ auth }) => auth?.authenticated === true\n});\n",
|
|
385
|
-
"reason": "Append auth profile placement entries into app-owned placement registry.",
|
|
386
|
-
"category": "auth-web",
|
|
387
|
-
"id": "auth-web-placement-block"
|
|
388
|
-
},
|
|
389
|
-
{
|
|
390
|
-
"op": "append-text",
|
|
391
|
-
"file": "src/placementTopology.js",
|
|
392
|
-
"position": "bottom",
|
|
393
|
-
"skipIfContains": "id: \"auth.profile-menu\"",
|
|
394
|
-
"value": "\naddPlacementTopology({\n id: \"auth.profile-menu\",\n description: \"Authenticated profile menu actions.\",\n surfaces: [\"*\"],\n variants: {\n compact: {\n outlet: \"auth-profile-menu:primary-menu\",\n renderers: {\n link: \"auth.web.profile.menu.link-item\"\n }\n },\n medium: {\n outlet: \"auth-profile-menu:primary-menu\",\n renderers: {\n link: \"auth.web.profile.menu.link-item\"\n }\n },\n expanded: {\n outlet: \"auth-profile-menu:primary-menu\",\n renderers: {\n link: \"auth.web.profile.menu.link-item\"\n }\n }\n }\n});\n",
|
|
395
|
-
"reason": "Append auth profile menu topology into the app-owned placement topology.",
|
|
396
|
-
"category": "auth-web",
|
|
397
|
-
"id": "auth-web-profile-menu-topology"
|
|
398
|
-
}
|
|
399
|
-
]
|
|
400
294
|
}
|
|
401
295
|
}
|
|
402
296
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: auth/auth-surface
|
|
3
|
+
title: Authentication surface
|
|
4
|
+
summary: Compose JSKIT authentication routes, views, profile controls, and public surface configuration without generated source.
|
|
5
|
+
keywords: account, auth, login, logout, password, placement, profile, reset, surface
|
|
6
|
+
requires: @jskit-ai/auth-core, @jskit-ai/auth-web, @jskit-ai/shell-web
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Authentication surface
|
|
10
|
+
|
|
11
|
+
## Use when
|
|
12
|
+
|
|
13
|
+
Use this pattern when an application needs browser login, sign-out, password
|
|
14
|
+
reset, and authenticated profile controls through JSKIT's auth runtime.
|
|
15
|
+
|
|
16
|
+
## Do not use when
|
|
17
|
+
|
|
18
|
+
Do not use this pattern for an API-only service, an application that delegates
|
|
19
|
+
all authentication UI to another product, or a second auth stack beside JSKIT.
|
|
20
|
+
|
|
21
|
+
## Product decisions
|
|
22
|
+
|
|
23
|
+
Choose the auth provider, permitted login methods, public routes, post-login
|
|
24
|
+
destination, account language, and which profile actions belong in the shell.
|
|
25
|
+
Those choices must be known before adapting the example.
|
|
26
|
+
|
|
27
|
+
## Framework APIs
|
|
28
|
+
|
|
29
|
+
Use the views, auth guard, HTTP client integration, sign-out runtime, providers,
|
|
30
|
+
and Playwright auth helper exported by `@jskit-ai/auth-web`. Use auth policies
|
|
31
|
+
from `@jskit-ai/auth-core` for server authorization.
|
|
32
|
+
|
|
33
|
+
For a Vibe64-managed preview, copy the app-owned
|
|
34
|
+
`.vibe64/bin/preview-identity` executable and declare it in the project's
|
|
35
|
+
Genesis Launch target. The executable calls the exported managed-preview
|
|
36
|
+
identity library directly; it does not require a JSKIT CLI.
|
|
37
|
+
|
|
38
|
+
Choose local-auth storage through the installed capability provider. A
|
|
39
|
+
database-backed application installs `@jskit-ai/auth-provider-local-db-core`,
|
|
40
|
+
which provides `auth.local.backend`; no environment switch selects it. When no
|
|
41
|
+
backend package provides that capability, local auth deliberately uses its file
|
|
42
|
+
store. Managed preview selectors name an existing auth-backend user whose
|
|
43
|
+
application profile already exists; preview identity never creates or projects
|
|
44
|
+
a user as a side effect.
|
|
45
|
+
|
|
46
|
+
## Invariants
|
|
47
|
+
|
|
48
|
+
- Credentials and provider secrets come from environment values.
|
|
49
|
+
- Auth backend selection comes from the installed provider graph, not an
|
|
50
|
+
`AUTH_LOCAL_BACKEND` environment value.
|
|
51
|
+
- The selected auth backend and the storage containing the application's users
|
|
52
|
+
agree before registration, login, session bootstrap, or preview identity is
|
|
53
|
+
exercised.
|
|
54
|
+
- Public auth routes do not require an existing session.
|
|
55
|
+
- Protected routes use the framework auth policy rather than page-local checks.
|
|
56
|
+
- Login, sign-out, and reset views use the public auth components/composables.
|
|
57
|
+
- Profile placements are conditional on the current authenticated state.
|
|
58
|
+
- Transient mutation failures use the application toast; initial load failures
|
|
59
|
+
remain in the affected surface.
|
|
60
|
+
|
|
61
|
+
## Example files
|
|
62
|
+
|
|
63
|
+
`example/` contains concrete route wrappers, editable view wrappers, and the
|
|
64
|
+
client runtime helpers needed by an authentication surface. It also contains
|
|
65
|
+
the optional app-owned managed-preview identity executable. Compose their
|
|
66
|
+
surface and placement declarations into the application's ordinary config and
|
|
67
|
+
placement files.
|
|
68
|
+
|
|
69
|
+
## Variation points
|
|
70
|
+
|
|
71
|
+
Change provider, routes, labels, page wrappers, permitted login methods, profile
|
|
72
|
+
placements, and post-auth destinations while retaining the runtime contracts.
|
|
73
|
+
|
|
74
|
+
## Verification
|
|
75
|
+
|
|
76
|
+
Exercise failed and successful login, sign-out, reset, protected navigation,
|
|
77
|
+
keyboard interaction, warm-cache return navigation, and browser refresh.
|
|
78
|
+
|
|
79
|
+
## Avoid
|
|
80
|
+
|
|
81
|
+
- storing credentials in source
|
|
82
|
+
- copying auth repositories or session mechanics into the app
|
|
83
|
+
- redirect loops between public and protected surfaces
|
|
84
|
+
- generator commands, mutation metadata, or setup receipts
|
package/src/client/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { AuthWebClientProvider } from "./providers/AuthWebClientProvider.js";
|
|
2
1
|
import DefaultLoginView from "./views/DefaultLoginView.vue";
|
|
3
2
|
import DefaultSignOutView from "./views/DefaultSignOutView.vue";
|
|
4
3
|
import DefaultResetPasswordView from "./views/DefaultResetPasswordView.vue";
|
|
@@ -24,6 +23,4 @@ const routeComponents = Object.freeze({
|
|
|
24
23
|
"auth-reset-password": DefaultResetPasswordView
|
|
25
24
|
});
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
export { routeComponents, clientProviders };
|
|
26
|
+
export { routeComponents };
|
|
@@ -1,68 +1,40 @@
|
|
|
1
|
+
import { defineProvider } from "@jskit-ai/kernel/shared/capabilities";
|
|
1
2
|
import DefaultLoginView from "../views/DefaultLoginView.vue";
|
|
2
3
|
import AuthProfileWidget from "../views/AuthProfileWidget.vue";
|
|
3
4
|
import AuthProfileMenuLinkItem from "../views/AuthProfileMenuLinkItem.vue";
|
|
4
|
-
import { createAuthGuardRuntime } from "../runtime/authGuardRuntime.js";
|
|
5
|
-
import { completeOAuthCallbackFromUrl } from "../runtime/oauthCallbackRuntime.js";
|
|
6
5
|
import { useLoginView } from "../runtime/useLoginView.js";
|
|
7
|
-
import {
|
|
8
|
-
import { resolveSurfaceNavigationTargetFromPlacementContext } from "@jskit-ai/shell-web/client/placement";
|
|
9
|
-
import { resolveAllowedReturnToOriginsFromPlacementContext } from "../lib/returnToPath.js";
|
|
6
|
+
import { createAuthClient } from "../runtime/authClient.js";
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
);
|
|
44
|
-
app.singleton("runtime.auth-guard.client", () => {
|
|
45
|
-
if (!app.has("runtime.web-placement.client")) {
|
|
46
|
-
throw new Error("AuthWebClientProvider requires shell-web placement runtime.");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const placementRuntime = app.make("runtime.web-placement.client");
|
|
50
|
-
const realtimeSocket = app.has("runtime.realtime.client.socket") ? app.make("runtime.realtime.client.socket") : null;
|
|
51
|
-
const loginRouteTarget = resolveSurfaceNavigationTargetFromPlacementContext(placementRuntime.getContext(), {
|
|
52
|
-
path: "/auth/login",
|
|
53
|
-
surfaceId: "auth"
|
|
54
|
-
});
|
|
55
|
-
return createAuthGuardRuntime({
|
|
56
|
-
loginRoute: loginRouteTarget.href,
|
|
57
|
-
placementRuntime,
|
|
58
|
-
realtimeSocket
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async boot(app) {
|
|
64
|
-
await bootAuthClientProvider(app);
|
|
8
|
+
const AuthWebClientProvider = defineProvider({
|
|
9
|
+
id: "auth.web.client",
|
|
10
|
+
requires: {
|
|
11
|
+
components: "client.components",
|
|
12
|
+
pinia: "client.pinia",
|
|
13
|
+
shell: "client.shell",
|
|
14
|
+
vueApp: "client.vue"
|
|
15
|
+
},
|
|
16
|
+
optional: {
|
|
17
|
+
mobile: "client.mobile",
|
|
18
|
+
realtime: "client.realtime"
|
|
19
|
+
},
|
|
20
|
+
provides: {
|
|
21
|
+
auth: "client.auth"
|
|
22
|
+
},
|
|
23
|
+
setup({ components, mobile, pinia, realtime, shell, vueApp }) {
|
|
24
|
+
components.register("auth.login.component", DefaultLoginView);
|
|
25
|
+
components.register("auth.login.useLoginView", useLoginView);
|
|
26
|
+
components.register("auth.web.profile.widget", AuthProfileWidget);
|
|
27
|
+
components.register("auth.web.profile.menu.link-item", AuthProfileMenuLinkItem);
|
|
28
|
+
return {
|
|
29
|
+
auth: createAuthClient({ mobile, pinia, realtime, shell, vueApp })
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
async boot(_dependencies, { outputs }) {
|
|
33
|
+
await outputs.auth.initialize();
|
|
34
|
+
},
|
|
35
|
+
shutdown(_dependencies, { outputs }) {
|
|
36
|
+
outputs.auth.dispose();
|
|
65
37
|
}
|
|
66
|
-
}
|
|
38
|
+
});
|
|
67
39
|
|
|
68
40
|
export { AuthWebClientProvider };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { resolveSurfaceNavigationTargetFromPlacementContext } from "@jskit-ai/shell-web/client/placement";
|
|
2
|
+
|
|
3
|
+
import { resolveAllowedReturnToOriginsFromPlacementContext } from "../lib/returnToPath.js";
|
|
4
|
+
import { createAuthGuardRuntime } from "./authGuardRuntime.js";
|
|
5
|
+
import { completeOAuthCallbackFromUrl } from "./oauthCallbackRuntime.js";
|
|
6
|
+
import { createBrowserOAuthLaunchClient } from "./oauthLaunchClient.js";
|
|
7
|
+
import {
|
|
8
|
+
AUTH_GUARD_RUNTIME_INJECTION_KEY,
|
|
9
|
+
AUTH_OAUTH_LAUNCH_CLIENT_INJECTION_KEY
|
|
10
|
+
} from "./inject.js";
|
|
11
|
+
import { useAuthStore } from "../stores/useAuthStore.js";
|
|
12
|
+
|
|
13
|
+
function createMobileCallbackCompleter() {
|
|
14
|
+
return Object.freeze({
|
|
15
|
+
async completeFromUrl({
|
|
16
|
+
url = "",
|
|
17
|
+
fallbackReturnTo = "/",
|
|
18
|
+
placementContext = null,
|
|
19
|
+
defaultProvider = "",
|
|
20
|
+
request = undefined,
|
|
21
|
+
refreshSession = async () => null
|
|
22
|
+
} = {}) {
|
|
23
|
+
return completeOAuthCallbackFromUrl({
|
|
24
|
+
url,
|
|
25
|
+
fallbackReturnTo,
|
|
26
|
+
allowedReturnToOrigins: resolveAllowedReturnToOriginsFromPlacementContext(placementContext),
|
|
27
|
+
defaultProvider,
|
|
28
|
+
...(typeof request === "function" ? { request } : {}),
|
|
29
|
+
refreshSession
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createAuthClient({ mobile = null, pinia, realtime = null, shell, vueApp } = {}) {
|
|
36
|
+
const placementRuntime = shell.placement;
|
|
37
|
+
const loginRouteTarget = resolveSurfaceNavigationTargetFromPlacementContext(placementRuntime.getContext(), {
|
|
38
|
+
path: "/auth/login",
|
|
39
|
+
surfaceId: "auth"
|
|
40
|
+
});
|
|
41
|
+
const guard = createAuthGuardRuntime({
|
|
42
|
+
loginRoute: loginRouteTarget.href,
|
|
43
|
+
placementRuntime,
|
|
44
|
+
realtimeSocket: realtime?.socket || null
|
|
45
|
+
});
|
|
46
|
+
const mobileCallback = createMobileCallbackCompleter();
|
|
47
|
+
const oauthLaunch = mobile?.oauthLaunch || createBrowserOAuthLaunchClient();
|
|
48
|
+
let stopAuthRefresh = null;
|
|
49
|
+
|
|
50
|
+
async function initialize() {
|
|
51
|
+
if (!pinia) {
|
|
52
|
+
throw new Error("AuthWebClientProvider requires Pinia installed in the client app.");
|
|
53
|
+
}
|
|
54
|
+
const authStore = useAuthStore(pinia);
|
|
55
|
+
authStore.attachRuntime(guard);
|
|
56
|
+
await authStore.initialize();
|
|
57
|
+
stopAuthRefresh = authStore.subscribe(() => {
|
|
58
|
+
void shell.bootstrap.refresh("auth.state");
|
|
59
|
+
});
|
|
60
|
+
vueApp?.provide?.(AUTH_GUARD_RUNTIME_INJECTION_KEY, guard);
|
|
61
|
+
vueApp?.provide?.(AUTH_OAUTH_LAUNCH_CLIENT_INJECTION_KEY, oauthLaunch);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function dispose() {
|
|
65
|
+
stopAuthRefresh?.();
|
|
66
|
+
stopAuthRefresh = null;
|
|
67
|
+
guard.dispose?.();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return Object.freeze({
|
|
71
|
+
dispose,
|
|
72
|
+
guard,
|
|
73
|
+
initialize,
|
|
74
|
+
mobileCallback,
|
|
75
|
+
oauthLaunch
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { createAuthClient, createMobileCallbackCompleter };
|
|
@@ -262,10 +262,10 @@ function evaluateAuthGuard({ guard, context, loginRoute, authState = DEFAULT_AUT
|
|
|
262
262
|
function installGuardEvaluator({ loginRoute = DEFAULT_LOGIN_ROUTE, getAuthState }) {
|
|
263
263
|
const root = asGlobalObject();
|
|
264
264
|
if (!root || typeof getAuthState !== "function") {
|
|
265
|
-
return;
|
|
265
|
+
return () => {};
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
|
|
268
|
+
const evaluator = ({ guard, context } = {}) => {
|
|
269
269
|
return evaluateAuthGuard({
|
|
270
270
|
guard,
|
|
271
271
|
context,
|
|
@@ -273,6 +273,12 @@ function installGuardEvaluator({ loginRoute = DEFAULT_LOGIN_ROUTE, getAuthState
|
|
|
273
273
|
authState: getAuthState()
|
|
274
274
|
});
|
|
275
275
|
};
|
|
276
|
+
root[GLOBAL_GUARD_EVALUATOR_KEY] = evaluator;
|
|
277
|
+
return () => {
|
|
278
|
+
if (root[GLOBAL_GUARD_EVALUATOR_KEY] === evaluator) {
|
|
279
|
+
delete root[GLOBAL_GUARD_EVALUATOR_KEY];
|
|
280
|
+
}
|
|
281
|
+
};
|
|
276
282
|
}
|
|
277
283
|
|
|
278
284
|
function normalizeRuntimePath(value, fallback) {
|
|
@@ -422,6 +428,7 @@ function createAuthGuardRuntime({
|
|
|
422
428
|
let authState = DEFAULT_AUTH_STATE;
|
|
423
429
|
let activeRefreshPromise = null;
|
|
424
430
|
let listenersInstalled = false;
|
|
431
|
+
const cleanup = [];
|
|
425
432
|
const listeners = new Set();
|
|
426
433
|
const subscribe = createListenerSubscription(listeners);
|
|
427
434
|
|
|
@@ -475,13 +482,12 @@ function createAuthGuardRuntime({
|
|
|
475
482
|
return authState;
|
|
476
483
|
}
|
|
477
484
|
|
|
478
|
-
installGuardEvaluator({
|
|
479
|
-
loginRoute: currentLoginRoute,
|
|
480
|
-
getAuthState: () => authState
|
|
481
|
-
});
|
|
482
|
-
|
|
483
485
|
if (!listenersInstalled) {
|
|
484
486
|
listenersInstalled = true;
|
|
487
|
+
cleanup.push(installGuardEvaluator({
|
|
488
|
+
loginRoute: currentLoginRoute,
|
|
489
|
+
getAuthState: () => authState
|
|
490
|
+
}));
|
|
485
491
|
const onReconnect = () => {
|
|
486
492
|
void refresh();
|
|
487
493
|
};
|
|
@@ -503,20 +509,24 @@ function createAuthGuardRuntime({
|
|
|
503
509
|
if (windowTarget) {
|
|
504
510
|
if (reconnectRefreshEnabled) {
|
|
505
511
|
windowTarget.addEventListener("online", onReconnect);
|
|
512
|
+
cleanup.push(() => windowTarget.removeEventListener("online", onReconnect));
|
|
506
513
|
}
|
|
507
514
|
if (foregroundRefreshEnabled) {
|
|
508
515
|
windowTarget.addEventListener("focus", onWindowFocus);
|
|
516
|
+
cleanup.push(() => windowTarget.removeEventListener("focus", onWindowFocus));
|
|
509
517
|
}
|
|
510
518
|
}
|
|
511
519
|
|
|
512
520
|
const documentTarget = getDocumentEventTarget();
|
|
513
521
|
if (foregroundRefreshEnabled && documentTarget) {
|
|
514
522
|
documentTarget.addEventListener("visibilitychange", onVisibilityChange);
|
|
523
|
+
cleanup.push(() => documentTarget.removeEventListener("visibilitychange", onVisibilityChange));
|
|
515
524
|
}
|
|
516
525
|
|
|
517
526
|
if (socket) {
|
|
518
527
|
for (const eventName of realtimeEvents) {
|
|
519
528
|
socket.on(eventName, onRealtimeRefresh);
|
|
529
|
+
cleanup.push(() => socket.off(eventName, onRealtimeRefresh));
|
|
520
530
|
}
|
|
521
531
|
}
|
|
522
532
|
}
|
|
@@ -526,7 +536,18 @@ function createAuthGuardRuntime({
|
|
|
526
536
|
});
|
|
527
537
|
}
|
|
528
538
|
|
|
539
|
+
function dispose() {
|
|
540
|
+
for (const release of cleanup.splice(0, cleanup.length).reverse()) {
|
|
541
|
+
try {
|
|
542
|
+
release();
|
|
543
|
+
} catch {}
|
|
544
|
+
}
|
|
545
|
+
listeners.clear();
|
|
546
|
+
listenersInstalled = false;
|
|
547
|
+
}
|
|
548
|
+
|
|
529
549
|
return Object.freeze({
|
|
550
|
+
dispose,
|
|
530
551
|
initialize,
|
|
531
552
|
refresh,
|
|
532
553
|
getState,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { resolveDevAuthPolicyFromEnv } from "@jskit-ai/auth-core/server/devAuth";
|
|
2
|
+
import { defineFeature } from "@jskit-ai/kernel/server/features";
|
|
3
|
+
import { AuthController } from "./controllers/AuthController.js";
|
|
4
|
+
import { buildRoutes } from "./routes/authRoutes.js";
|
|
5
|
+
import { AuthWebService } from "./services/AuthWebService.js";
|
|
6
|
+
|
|
7
|
+
function devAuthBootstrapEnabled(env) {
|
|
8
|
+
const policy = resolveDevAuthPolicyFromEnv(env);
|
|
9
|
+
return policy.enabled && !policy.isProduction;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const AuthWebFeature = defineFeature({
|
|
13
|
+
id: "auth.web",
|
|
14
|
+
domain: "auth",
|
|
15
|
+
requires: {
|
|
16
|
+
authService: "auth.service",
|
|
17
|
+
env: "runtime.env",
|
|
18
|
+
http: "runtime.http"
|
|
19
|
+
},
|
|
20
|
+
provides: {
|
|
21
|
+
authWeb: "auth.web"
|
|
22
|
+
},
|
|
23
|
+
setup({ authService, env, http }) {
|
|
24
|
+
const service = new AuthWebService({
|
|
25
|
+
authService,
|
|
26
|
+
devAuthBootstrapEnabled: devAuthBootstrapEnabled(env)
|
|
27
|
+
});
|
|
28
|
+
const controller = new AuthController({ service });
|
|
29
|
+
const routes = buildRoutes(controller, {
|
|
30
|
+
includeDevLoginAs: service.isDevLoginAsAvailable()
|
|
31
|
+
});
|
|
32
|
+
for (const route of routes) {
|
|
33
|
+
http.router.register(route.method, route.path, route, route.handler);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
authWeb: Object.freeze({ controller, routes: Object.freeze(routes), service })
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export { AuthWebFeature };
|