@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.
Files changed (28) hide show
  1. package/package.json +29 -135
  2. package/patterns/auth-surface/PATTERN.md +84 -0
  3. package/patterns/auth-surface/example/.vibe64/bin/preview-identity +5 -0
  4. package/src/client/index.js +1 -4
  5. package/src/client/providers/AuthWebClientProvider.js +32 -60
  6. package/src/client/runtime/authClient.js +79 -0
  7. package/src/client/runtime/authGuardRuntime.js +28 -7
  8. package/src/server/AuthWebFeature.js +41 -0
  9. package/src/server/managedPreviewIdentity.js +344 -0
  10. package/src/server/services/AuthWebService.js +4 -16
  11. package/test/clientBoot.test.js +3 -3
  12. package/test/clientSurface.test.js +6 -16
  13. package/test/managedPreviewIdentity.test.js +89 -0
  14. package/test/packageMetadataOwnership.test.js +3 -15
  15. package/test/provider.test.js +30 -220
  16. package/test/providerRuntime.test.js +92 -307
  17. package/src/client/providers/bootAuthClientProvider.js +0 -49
  18. package/src/server/providers/AuthRouteServiceProvider.js +0 -31
  19. package/src/server/providers/AuthWebServiceProvider.js +0 -38
  20. /package/{templates → patterns/auth-surface/example}/src/pages/auth/login.vue +0 -0
  21. /package/{templates → patterns/auth-surface/example}/src/pages/auth/reset-password.vue +0 -0
  22. /package/{templates → patterns/auth-surface/example}/src/pages/auth/signout.vue +0 -0
  23. /package/{templates → patterns/auth-surface/example}/src/runtime/authGuardRuntime.js +0 -0
  24. /package/{templates → patterns/auth-surface/example}/src/runtime/authHttpClient.js +0 -0
  25. /package/{templates → patterns/auth-surface/example}/src/runtime/useSignOut.js +0 -0
  26. /package/{templates → patterns/auth-surface/example}/src/views/auth/LoginView.vue +0 -0
  27. /package/{templates → patterns/auth-surface/example}/src/views/auth/ResetPasswordView.vue +0 -0
  28. /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.159",
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.157",
24
- "@jskit-ai/http-runtime": "0.1.157",
25
- "@jskit-ai/kernel": "0.1.159",
26
- "@jskit-ai/shell-web": "0.1.163",
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.server-routes",
43
+ "auth.web",
42
44
  "auth.web-login"
43
45
  ],
44
46
  "requires": [
45
- "auth.access",
46
- "auth.provider",
47
- "validators.http",
48
- "auth.policy",
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/providers/AuthWebServiceProvider.js",
55
+ "providerEntrypoint": "src/server/AuthWebFeature.js",
55
56
  "providers": [
56
57
  {
57
- "entrypoint": "src/server/providers/AuthWebServiceProvider.js",
58
- "export": "AuthWebServiceProvider"
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
- "containerTokens": {
92
- "server": [
93
- "auth.web.service"
94
- ],
95
- "client": [
96
- "runtime.auth-guard.client",
97
- "auth.login.component",
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": "mutations.text#auth-web-placement-block"
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": "mutations.text#auth-web-placement-block"
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": "mutations.text#auth-web-placement-block"
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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runManagedPreviewIdentityCommand } from "@jskit-ai/auth-web/server/managedPreviewIdentity";
4
+
5
+ process.exitCode = await runManagedPreviewIdentityCommand();
@@ -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
- const clientProviders = Object.freeze([AuthWebClientProvider]);
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 { bootAuthClientProvider } from "./bootAuthClientProvider.js";
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
- class AuthWebClientProvider {
12
- static id = "auth.web.client";
13
-
14
- register(app) {
15
- if (!app || typeof app.singleton !== "function") {
16
- throw new Error("AuthWebClientProvider requires application singleton().");
17
- }
18
-
19
- app.singleton("auth.login.component", () => DefaultLoginView);
20
- app.singleton("auth.login.useLoginView", () => useLoginView);
21
- app.singleton("auth.web.profile.widget", () => AuthProfileWidget);
22
- app.singleton("auth.web.profile.menu.link-item", () => AuthProfileMenuLinkItem);
23
- app.singleton("auth.mobile-callback.client", () =>
24
- Object.freeze({
25
- async completeFromUrl({
26
- url = "",
27
- fallbackReturnTo = "/",
28
- placementContext = null,
29
- defaultProvider = "",
30
- request = undefined,
31
- refreshSession = async () => null
32
- } = {}) {
33
- return completeOAuthCallbackFromUrl({
34
- url,
35
- fallbackReturnTo,
36
- allowedReturnToOrigins: resolveAllowedReturnToOriginsFromPlacementContext(placementContext),
37
- defaultProvider,
38
- ...(typeof request === "function" ? { request } : {}),
39
- refreshSession
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
- root[GLOBAL_GUARD_EVALUATOR_KEY] = ({ guard, context } = {}) => {
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 };