@jskit-ai/auth-web 0.1.33 → 0.1.35

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.
@@ -1,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  "packageVersion": 1,
3
3
  "packageId": "@jskit-ai/auth-web",
4
- "version": "0.1.33",
4
+ "version": "0.1.35",
5
5
  "kind": "runtime",
6
6
  "description": "Auth web module: Fastify auth routes plus web login/sign-out scaffolds.",
7
7
  "dependsOn": [
@@ -177,8 +177,8 @@ export default Object.freeze({
177
177
  "placements": {
178
178
  "outlets": [
179
179
  {
180
- "host": "auth-profile-menu",
181
- "position": "primary-menu",
180
+ "target": "auth-profile-menu:primary-menu",
181
+ "defaultLinkComponentToken": "auth.web.profile.menu.link-item",
182
182
  "surfaces": ["*"],
183
183
  "source": "src/client/views/AuthProfileWidget.vue"
184
184
  }
@@ -186,8 +186,7 @@ export default Object.freeze({
186
186
  "contributions": [
187
187
  {
188
188
  "id": "auth.profile.widget",
189
- "host": "shell-layout",
190
- "position": "top-right",
189
+ "target": "shell-layout:top-right",
191
190
  "surfaces": ["*"],
192
191
  "order": 1000,
193
192
  "componentToken": "auth.web.profile.widget",
@@ -195,8 +194,7 @@ export default Object.freeze({
195
194
  },
196
195
  {
197
196
  "id": "auth.profile.menu.sign-in",
198
- "host": "auth-profile-menu",
199
- "position": "primary-menu",
197
+ "target": "auth-profile-menu:primary-menu",
200
198
  "surfaces": ["*"],
201
199
  "order": 200,
202
200
  "componentToken": "auth.web.profile.menu.link-item",
@@ -205,8 +203,7 @@ export default Object.freeze({
205
203
  },
206
204
  {
207
205
  "id": "auth.profile.menu.sign-out",
208
- "host": "auth-profile-menu",
209
- "position": "primary-menu",
206
+ "target": "auth-profile-menu:primary-menu",
210
207
  "surfaces": ["*"],
211
208
  "order": 1000,
212
209
  "componentToken": "auth.web.profile.menu.link-item",
@@ -223,10 +220,10 @@ export default Object.freeze({
223
220
  "@tanstack/vue-query": "5.92.12",
224
221
  "@mdi/js": "^7.4.47",
225
222
  "@fastify/type-provider-typebox": "^6.1.0",
226
- "@jskit-ai/auth-core": "0.1.31",
227
- "@jskit-ai/http-runtime": "0.1.31",
228
- "@jskit-ai/kernel": "0.1.32",
229
- "@jskit-ai/shell-web": "0.1.31",
223
+ "@jskit-ai/auth-core": "0.1.33",
224
+ "@jskit-ai/http-runtime": "0.1.33",
225
+ "@jskit-ai/kernel": "0.1.34",
226
+ "@jskit-ai/shell-web": "0.1.33",
230
227
  "vuetify": "^4.0.0"
231
228
  },
232
229
  "dev": {}
@@ -285,7 +282,7 @@ export default Object.freeze({
285
282
  "file": "src/placement.js",
286
283
  "position": "bottom",
287
284
  "skipIfContains": "id: \"auth.profile.widget\"",
288
- "value": "\naddPlacement({\n id: \"auth.profile.widget\",\n host: \"shell-layout\",\n position: \"top-right\",\n surfaces: [\"*\"],\n order: 1000,\n componentToken: \"auth.web.profile.widget\"\n});\n\naddPlacement({\n id: \"auth.profile.menu.sign-in\",\n host: \"auth-profile-menu\",\n position: \"primary-menu\",\n surfaces: [\"*\"],\n order: 200,\n componentToken: \"auth.web.profile.menu.link-item\",\n props: {\n label: \"Sign in\",\n to: \"/auth/login\"\n },\n when: ({ auth }) => !Boolean(auth?.authenticated)\n});\n\naddPlacement({\n id: \"auth.profile.menu.sign-out\",\n host: \"auth-profile-menu\",\n position: \"primary-menu\",\n surfaces: [\"*\"],\n order: 1000,\n componentToken: \"auth.web.profile.menu.link-item\",\n props: {\n label: \"Sign out\",\n to: \"/auth/signout\"\n },\n when: ({ auth }) => Boolean(auth?.authenticated)\n});\n",
285
+ "value": "\naddPlacement({\n id: \"auth.profile.widget\",\n target: \"shell-layout:top-right\",\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:primary-menu\",\n surfaces: [\"*\"],\n order: 200,\n componentToken: \"auth.web.profile.menu.link-item\",\n props: {\n label: \"Sign in\",\n to: \"/auth/login\"\n },\n when: ({ auth }) => !Boolean(auth?.authenticated)\n});\n\naddPlacement({\n id: \"auth.profile.menu.sign-out\",\n target: \"auth-profile-menu:primary-menu\",\n surfaces: [\"*\"],\n order: 1000,\n componentToken: \"auth.web.profile.menu.link-item\",\n props: {\n label: \"Sign out\",\n to: \"/auth/signout\"\n },\n when: ({ auth }) => Boolean(auth?.authenticated)\n});\n",
289
286
  "reason": "Append auth profile placement entries into app-owned placement registry.",
290
287
  "category": "auth-web",
291
288
  "id": "auth-web-placement-block"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/auth-web",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -18,12 +18,12 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@tanstack/vue-query": "^5.90.5",
21
- "@jskit-ai/auth-core": "0.1.31",
21
+ "@jskit-ai/auth-core": "0.1.33",
22
22
  "@mdi/js": "^7.4.47",
23
23
  "@fastify/type-provider-typebox": "^6.1.0",
24
- "@jskit-ai/kernel": "0.1.32",
25
- "@jskit-ai/shell-web": "0.1.31",
24
+ "@jskit-ai/kernel": "0.1.34",
25
+ "@jskit-ai/shell-web": "0.1.33",
26
26
  "vuetify": "^4.0.0",
27
- "@jskit-ai/http-runtime": "0.1.31"
27
+ "@jskit-ai/http-runtime": "0.1.33"
28
28
  }
29
29
  }
@@ -0,0 +1,97 @@
1
+ import { appendQueryString } from "@jskit-ai/kernel/shared/support";
2
+ import { isExternalLinkTarget, splitPathQueryHash } from "@jskit-ai/kernel/shared/support/linkPath";
3
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
4
+ import {
5
+ resolveSurfaceDefinitionFromPlacementContext,
6
+ resolveSurfaceNavigationTargetFromPlacementContext,
7
+ resolveSurfacePathFromPlacementContext
8
+ } from "@jskit-ai/shell-web/client/placement";
9
+
10
+ const ACCOUNT_SURFACE_ID = "account";
11
+ const ACCOUNT_SETTINGS_FALLBACK_PATH = "/account";
12
+
13
+ function resolvePathnameFromLinkTarget(target = "") {
14
+ const normalizedTarget = normalizeText(target);
15
+ if (!normalizedTarget) {
16
+ return "";
17
+ }
18
+
19
+ if (isExternalLinkTarget(normalizedTarget)) {
20
+ try {
21
+ return normalizeText(new URL(normalizedTarget).pathname);
22
+ } catch {
23
+ return "";
24
+ }
25
+ }
26
+
27
+ return normalizeText(splitPathQueryHash(normalizedTarget).pathname);
28
+ }
29
+
30
+ function resolveAccountSettingsPathFromPlacementContext(contextValue = null) {
31
+ const accountSurfaceDefinition = resolveSurfaceDefinitionFromPlacementContext(contextValue, ACCOUNT_SURFACE_ID);
32
+ if (!accountSurfaceDefinition) {
33
+ return ACCOUNT_SETTINGS_FALLBACK_PATH;
34
+ }
35
+
36
+ return resolveSurfacePathFromPlacementContext(contextValue, ACCOUNT_SURFACE_ID, "/");
37
+ }
38
+
39
+ function resolveFallbackReturnTo({
40
+ currentFullPath = "",
41
+ currentPath = "",
42
+ currentHref = "",
43
+ absolute = false
44
+ } = {}) {
45
+ if (absolute) {
46
+ const normalizedCurrentHref = normalizeText(currentHref);
47
+ if (normalizedCurrentHref) {
48
+ return normalizedCurrentHref;
49
+ }
50
+ }
51
+
52
+ return normalizeText(currentFullPath) || normalizeText(currentPath) || "/";
53
+ }
54
+
55
+ function appendAccountReturnToIfNeeded(
56
+ target = "",
57
+ {
58
+ placementContext = null,
59
+ currentFullPath = "",
60
+ currentPath = "",
61
+ currentHref = ""
62
+ } = {}
63
+ ) {
64
+ const normalizedTarget = normalizeText(target);
65
+ if (!normalizedTarget || normalizedTarget.includes("returnTo=")) {
66
+ return normalizedTarget;
67
+ }
68
+
69
+ const targetPathname = resolvePathnameFromLinkTarget(normalizedTarget);
70
+ const accountSettingsPathname = resolvePathnameFromLinkTarget(
71
+ resolveAccountSettingsPathFromPlacementContext(placementContext)
72
+ );
73
+ if (!targetPathname || !accountSettingsPathname || targetPathname !== accountSettingsPathname) {
74
+ return normalizedTarget;
75
+ }
76
+
77
+ const accountSettingsTarget = resolveSurfaceNavigationTargetFromPlacementContext(placementContext, {
78
+ path: normalizedTarget,
79
+ surfaceId: ACCOUNT_SURFACE_ID
80
+ });
81
+ const returnTo = resolveFallbackReturnTo({
82
+ currentFullPath,
83
+ currentPath,
84
+ currentHref,
85
+ absolute: accountSettingsTarget.sameOrigin !== true
86
+ });
87
+ const queryParams = new URLSearchParams({
88
+ returnTo
89
+ });
90
+
91
+ return appendQueryString(normalizedTarget, queryParams.toString());
92
+ }
93
+
94
+ export {
95
+ appendAccountReturnToIfNeeded,
96
+ resolveAccountSettingsPathFromPlacementContext
97
+ };
@@ -1,10 +1,9 @@
1
1
  <script setup>
2
2
  import { computed } from "vue";
3
- import { mdiAccountCogOutline, mdiCogOutline, mdiLogin, mdiLogout } from "@mdi/js";
4
- import {
5
- useWebPlacementContext,
6
- resolveSurfaceNavigationTargetFromPlacementContext
7
- } from "@jskit-ai/shell-web/client/placement";
3
+ import { useRoute } from "vue-router";
4
+ import { useWebPlacementContext } from "@jskit-ai/shell-web/client/placement";
5
+ import ShellMenuLinkItem from "@jskit-ai/shell-web/client/components/ShellMenuLinkItem";
6
+ import { appendAccountReturnToIfNeeded } from "../lib/profileMenuLinkTarget.js";
8
7
 
9
8
  const props = defineProps({
10
9
  label: {
@@ -18,66 +17,37 @@ const props = defineProps({
18
17
  icon: {
19
18
  type: String,
20
19
  default: ""
20
+ },
21
+ disabled: {
22
+ type: Boolean,
23
+ default: false
21
24
  }
22
25
  });
26
+ const route = useRoute();
23
27
  const { context: placementContext } = useWebPlacementContext();
24
28
 
25
- const resolvedNavigationTarget = computed(() => {
26
- const target = String(props.to || "").trim();
27
- if (!target) {
28
- return {
29
- href: "",
30
- sameOrigin: true
31
- };
32
- }
33
-
34
- const navigationTarget = resolveSurfaceNavigationTargetFromPlacementContext(placementContext.value, {
35
- path: target
36
- });
37
- return {
38
- href: navigationTarget.href,
39
- sameOrigin: navigationTarget.sameOrigin
40
- };
41
- });
42
-
43
- const resolvedIcon = computed(() => {
44
- const explicitIcon = String(props.icon || "").trim();
45
- if (explicitIcon) {
46
- return explicitIcon;
47
- }
48
-
49
- const normalizedLabel = String(props.label || "").trim().toLowerCase();
50
- const normalizedTarget = String(props.to || "").trim().toLowerCase();
51
- if (
52
- normalizedLabel.includes("sign in") ||
53
- normalizedTarget.includes("/auth/login")
54
- ) {
55
- return mdiLogin;
29
+ function resolveWindowHref() {
30
+ if (typeof window !== "object" || !window || !window.location) {
31
+ return "/";
56
32
  }
57
-
58
- if (
59
- normalizedLabel.includes("sign out") ||
60
- normalizedTarget.includes("/auth/signout")
61
- ) {
62
- return mdiLogout;
63
- }
64
-
65
- if (normalizedLabel.includes("settings") || normalizedTarget.includes("/settings")) {
66
- if (normalizedTarget.includes("/account")) {
67
- return mdiAccountCogOutline;
68
- }
69
- return mdiCogOutline;
70
- }
71
-
72
- return "";
73
- });
33
+ return String(window.location.href || "").trim() || "/";
34
+ }
35
+
36
+ const resolvedTo = computed(() =>
37
+ appendAccountReturnToIfNeeded(props.to, {
38
+ placementContext: placementContext.value,
39
+ currentFullPath: route?.fullPath,
40
+ currentPath: route?.path,
41
+ currentHref: resolveWindowHref()
42
+ })
43
+ );
74
44
  </script>
75
45
 
76
46
  <template>
77
- <v-list-item
78
- :title="props.label || undefined"
79
- :to="resolvedNavigationTarget.sameOrigin ? resolvedNavigationTarget.href || undefined : undefined"
80
- :href="resolvedNavigationTarget.sameOrigin ? undefined : resolvedNavigationTarget.href || undefined"
81
- :prepend-icon="resolvedIcon || undefined"
47
+ <ShellMenuLinkItem
48
+ :label="props.label"
49
+ :to="resolvedTo"
50
+ :icon="props.icon"
51
+ :disabled="props.disabled"
82
52
  />
83
53
  </template>
@@ -91,8 +91,8 @@ onBeforeUnmount(() => {
91
91
 
92
92
  <v-list min-width="220" density="comfortable" class="py-1">
93
93
  <ShellOutlet
94
- host="auth-profile-menu"
95
- position="primary-menu"
94
+ target="auth-profile-menu:primary-menu"
95
+ default-link-component-token="auth.web.profile.menu.link-item"
96
96
  :context="placementContext"
97
97
  />
98
98
  </v-list>
@@ -0,0 +1,17 @@
1
+ import assert from "node:assert/strict";
2
+ import path from "node:path";
3
+ import test from "node:test";
4
+ import { readFile } from "node:fs/promises";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
8
+ const COMPONENT_PATH = path.resolve(TEST_DIRECTORY, "../src/client/views/AuthProfileMenuLinkItem.vue");
9
+
10
+ test("AuthProfileMenuLinkItem delegates generic menu-link rendering to shell-web and passes disabled through explicitly", async () => {
11
+ const source = await readFile(COMPONENT_PATH, "utf8");
12
+
13
+ assert.match(source, /import ShellMenuLinkItem from "@jskit-ai\/shell-web\/client\/components\/ShellMenuLinkItem"/);
14
+ assert.match(source, /disabled:\s*\{\s*type:\s*Boolean,\s*default:\s*false\s*\}/s);
15
+ assert.match(source, /<ShellMenuLinkItem[\s\S]*:to="resolvedTo"[\s\S]*:disabled="props\.disabled"/);
16
+ assert.doesNotMatch(source, /<v-list-item\b/);
17
+ });
@@ -0,0 +1,64 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { appendAccountReturnToIfNeeded } from "../src/client/lib/profileMenuLinkTarget.js";
4
+
5
+ test("appendAccountReturnToIfNeeded appends relative returnTo for same-origin account settings targets", () => {
6
+ const context = {
7
+ surfaceConfig: {
8
+ defaultSurfaceId: "home",
9
+ surfacesById: {
10
+ home: { id: "home", routeBase: "/home", origin: "https://www.example.com" },
11
+ account: { id: "account", routeBase: "/account", origin: "https://www.example.com" }
12
+ }
13
+ }
14
+ };
15
+
16
+ assert.equal(
17
+ appendAccountReturnToIfNeeded("/account", {
18
+ placementContext: context,
19
+ currentFullPath: "/home/profile?tab=security"
20
+ }),
21
+ "/account?returnTo=%2Fhome%2Fprofile%3Ftab%3Dsecurity"
22
+ );
23
+ });
24
+
25
+ test("appendAccountReturnToIfNeeded appends absolute returnTo for cross-origin account settings targets", () => {
26
+ const context = {
27
+ surfaceConfig: {
28
+ defaultSurfaceId: "home",
29
+ surfacesById: {
30
+ home: { id: "home", routeBase: "/home", origin: "https://www.example.com" },
31
+ account: { id: "account", routeBase: "/account", origin: "https://account.example.com" }
32
+ }
33
+ }
34
+ };
35
+
36
+ assert.equal(
37
+ appendAccountReturnToIfNeeded("/account", {
38
+ placementContext: context,
39
+ currentFullPath: "/home/profile?tab=security",
40
+ currentHref: "https://www.example.com/home/profile?tab=security"
41
+ }),
42
+ "/account?returnTo=https%3A%2F%2Fwww.example.com%2Fhome%2Fprofile%3Ftab%3Dsecurity"
43
+ );
44
+ });
45
+
46
+ test("appendAccountReturnToIfNeeded leaves non-account targets unchanged", () => {
47
+ const context = {
48
+ surfaceConfig: {
49
+ defaultSurfaceId: "home",
50
+ surfacesById: {
51
+ home: { id: "home", routeBase: "/home", origin: "https://www.example.com" },
52
+ account: { id: "account", routeBase: "/account", origin: "https://www.example.com" }
53
+ }
54
+ }
55
+ };
56
+
57
+ assert.equal(
58
+ appendAccountReturnToIfNeeded("/console/settings", {
59
+ placementContext: context,
60
+ currentFullPath: "/home/profile"
61
+ }),
62
+ "/console/settings"
63
+ );
64
+ });