@jskit-ai/kernel 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.
Files changed (185) hide show
  1. package/README.md +24 -0
  2. package/_testable/index.js +4 -0
  3. package/client/appConfig.js +33 -0
  4. package/client/componentInteraction.js +51 -0
  5. package/client/componentInteraction.test.js +111 -0
  6. package/client/descriptorSections.js +75 -0
  7. package/client/index.d.ts +70 -0
  8. package/client/index.js +3 -0
  9. package/client/logging.js +38 -0
  10. package/client/moduleBootstrap.js +670 -0
  11. package/client/moduleBootstrap.test.js +403 -0
  12. package/client/shellBootstrap.js +233 -0
  13. package/client/shellBootstrap.test.js +185 -0
  14. package/client/shellRouting.js +321 -0
  15. package/client/shellRouting.test.js +113 -0
  16. package/client/vite/clientBootstrapPlugin.js +259 -0
  17. package/client/vite/clientBootstrapPlugin.test.js +563 -0
  18. package/client/vite/index.js +3 -0
  19. package/internal/node/fileSystem.js +21 -0
  20. package/internal/node/installedPackageDescriptor.js +104 -0
  21. package/package.json +43 -0
  22. package/server/actions/ActionRuntimeServiceProvider.js +309 -0
  23. package/server/actions/ActionRuntimeServiceProvider.test.js +551 -0
  24. package/server/actions/index.js +8 -0
  25. package/server/container/ContainerCoreServiceProvider.js +27 -0
  26. package/server/container/index.js +10 -0
  27. package/server/exportPolicy.test.js +68 -0
  28. package/server/http/HttpFastifyServiceProvider.js +25 -0
  29. package/server/http/_testable/index.js +2 -0
  30. package/server/http/index.js +1 -0
  31. package/server/http/lib/controller.js +183 -0
  32. package/server/http/lib/controller.test.js +143 -0
  33. package/server/http/lib/errors.js +12 -0
  34. package/server/http/lib/httpRuntime.js +82 -0
  35. package/server/http/lib/index.js +18 -0
  36. package/server/http/lib/kernel.js +15 -0
  37. package/server/http/lib/kernel.test.js +880 -0
  38. package/server/http/lib/middlewareRuntime.js +149 -0
  39. package/server/http/lib/requestActionExecutor.js +258 -0
  40. package/server/http/lib/requestScope.js +59 -0
  41. package/server/http/lib/routeRegistration.js +165 -0
  42. package/server/http/lib/routeSupport.js +45 -0
  43. package/server/http/lib/routeValidator.js +469 -0
  44. package/server/http/lib/routeValidator.test.js +474 -0
  45. package/server/http/lib/router.js +206 -0
  46. package/server/kernel/KernelCoreServiceProvider.js +27 -0
  47. package/server/kernel/index.js +10 -0
  48. package/server/platform/PlatformServerRuntimeServiceProvider.js +30 -0
  49. package/server/platform/index.js +5 -0
  50. package/server/platform/providerRuntime/descriptorCatalog.js +170 -0
  51. package/server/platform/providerRuntime/helpers.js +45 -0
  52. package/server/platform/providerRuntime/lockfile.js +27 -0
  53. package/server/platform/providerRuntime/providerLoader.js +283 -0
  54. package/server/platform/providerRuntime.js +142 -0
  55. package/server/platform/providerRuntime.test.js +217 -0
  56. package/server/platform/runtime.js +40 -0
  57. package/server/platform/surfaceRuntime.js +150 -0
  58. package/server/platform/surfaceRuntime.test.js +136 -0
  59. package/server/registries/actionSurfaceSourceRegistry.js +150 -0
  60. package/server/registries/bootstrapPayloadContributorRegistry.js +41 -0
  61. package/server/registries/domainEventListenerRegistry.js +61 -0
  62. package/server/registries/index.js +36 -0
  63. package/server/registries/primitives.js +63 -0
  64. package/server/registries/routeVisibilityResolverRegistry.js +87 -0
  65. package/server/registries/serviceRegistrationRegistry.js +431 -0
  66. package/server/runtime/ServerRuntimeCoreServiceProvider.js +65 -0
  67. package/server/runtime/ServerRuntimeCoreServiceProvider.test.js +53 -0
  68. package/server/runtime/apiRoutePolicyParity.test.js +109 -0
  69. package/server/runtime/apiRouteRegistration.js +65 -0
  70. package/server/runtime/bootBootstrapRoutes.js +46 -0
  71. package/server/runtime/bootBootstrapRoutes.test.js +79 -0
  72. package/server/runtime/bootstrapContributors.test.js +114 -0
  73. package/server/runtime/canonicalJson.js +74 -0
  74. package/server/runtime/composition.js +142 -0
  75. package/server/runtime/domainEvents.test.js +114 -0
  76. package/server/runtime/domainRules.js +50 -0
  77. package/server/runtime/domainRules.test.js +87 -0
  78. package/server/runtime/entityChangeEvents.js +182 -0
  79. package/server/runtime/entityChangeEvents.test.js +211 -0
  80. package/server/runtime/errors.js +68 -0
  81. package/server/runtime/errors.test.js +73 -0
  82. package/server/runtime/fastifyBootstrap.js +372 -0
  83. package/server/runtime/fastifyBootstrap.test.js +194 -0
  84. package/server/runtime/index.js +6 -0
  85. package/server/runtime/integers.js +13 -0
  86. package/server/runtime/moduleConfig.js +269 -0
  87. package/server/runtime/moduleConfig.test.js +141 -0
  88. package/server/runtime/pagination.js +13 -0
  89. package/server/runtime/realtimeNormalization.js +21 -0
  90. package/server/runtime/requestUrl.js +38 -0
  91. package/server/runtime/routeUtils.js +20 -0
  92. package/server/runtime/runtimeAssembly.js +113 -0
  93. package/server/runtime/runtimeKernel.js +55 -0
  94. package/server/runtime/securityAudit.js +269 -0
  95. package/server/runtime/securityAudit.test.js +41 -0
  96. package/server/runtime/serviceAuthorization.js +113 -0
  97. package/server/runtime/serviceAuthorization.test.js +100 -0
  98. package/server/runtime/serviceRegistration.test.js +197 -0
  99. package/server/support/SupportCoreServiceProvider.js +25 -0
  100. package/server/support/appConfig.js +37 -0
  101. package/server/support/appConfig.test.js +94 -0
  102. package/server/support/defaultMissingHandler.js +7 -0
  103. package/server/support/index.js +2 -0
  104. package/server/support/routePolicyConfig.js +51 -0
  105. package/server/support/symlinkSafeRequire.js +78 -0
  106. package/server/support/symlinkSafeRequire.test.js +27 -0
  107. package/server/surface/SurfaceRoutingServiceProvider.js +27 -0
  108. package/server/surface/index.js +19 -0
  109. package/shared/actions/actionContributorHelpers.js +34 -0
  110. package/shared/actions/actionContributorHelpers.test.js +16 -0
  111. package/shared/actions/actionDefinitions.js +488 -0
  112. package/shared/actions/actionDefinitions.test.js +212 -0
  113. package/shared/actions/audit.js +7 -0
  114. package/shared/actions/executionContext.js +97 -0
  115. package/shared/actions/executionContext.test.js +66 -0
  116. package/shared/actions/idempotency.js +62 -0
  117. package/shared/actions/index.js +2 -0
  118. package/shared/actions/observability.js +10 -0
  119. package/shared/actions/pipeline.js +287 -0
  120. package/shared/actions/policies.js +342 -0
  121. package/shared/actions/policies.test.js +233 -0
  122. package/shared/actions/registry.js +187 -0
  123. package/shared/actions/registry.test.js +381 -0
  124. package/shared/actions/requestMeta.js +36 -0
  125. package/shared/actions/textNormalization.js +3 -0
  126. package/shared/actions/withActionDefaults.js +34 -0
  127. package/shared/index.js +2 -0
  128. package/shared/runtime/application.js +323 -0
  129. package/shared/runtime/container.js +261 -0
  130. package/shared/runtime/containerErrors.js +22 -0
  131. package/shared/runtime/index.js +18 -0
  132. package/shared/runtime/kernelErrors.js +20 -0
  133. package/shared/runtime/serviceProvider.js +13 -0
  134. package/shared/support/formatDateTime.js +10 -0
  135. package/shared/support/formatDateTime.test.js +15 -0
  136. package/shared/support/index.js +14 -0
  137. package/shared/support/linkPath.js +67 -0
  138. package/shared/support/linkPath.test.js +35 -0
  139. package/shared/support/normalize.js +116 -0
  140. package/shared/support/normalize.test.js +48 -0
  141. package/shared/support/packageDescriptor.test.js +121 -0
  142. package/shared/support/permissions.js +50 -0
  143. package/shared/support/pickOwnProperties.js +17 -0
  144. package/shared/support/pickOwnProperties.test.js +25 -0
  145. package/shared/support/policies.js +11 -0
  146. package/shared/support/queryPath.js +33 -0
  147. package/shared/support/queryPath.test.js +19 -0
  148. package/shared/support/queryResilience.js +34 -0
  149. package/shared/support/queryResilience.test.js +33 -0
  150. package/shared/support/returnToPath.js +153 -0
  151. package/shared/support/returnToPath.test.js +123 -0
  152. package/shared/support/sorting.js +15 -0
  153. package/shared/support/tokens.js +23 -0
  154. package/shared/support/tokens.test.js +17 -0
  155. package/shared/support/visibility.js +56 -0
  156. package/shared/support/visibility.test.js +45 -0
  157. package/shared/surface/apiPaths.js +84 -0
  158. package/shared/surface/escapeRegExp.js +5 -0
  159. package/shared/surface/index.js +6 -0
  160. package/shared/surface/paths.js +273 -0
  161. package/shared/surface/registry.js +135 -0
  162. package/shared/surface/registry.test.js +44 -0
  163. package/shared/surface/runtime.js +357 -0
  164. package/shared/surface/runtime.test.js +319 -0
  165. package/shared/validators/createCursorListValidator.js +42 -0
  166. package/shared/validators/createCursorListValidator.test.js +34 -0
  167. package/shared/validators/cursorPaginationQueryValidator.js +31 -0
  168. package/shared/validators/cursorPaginationQueryValidator.test.js +21 -0
  169. package/shared/validators/index.js +12 -0
  170. package/shared/validators/inputNormalization.js +13 -0
  171. package/shared/validators/mergeObjectSchemas.js +31 -0
  172. package/shared/validators/mergeObjectSchemas.test.js +67 -0
  173. package/shared/validators/mergeValidators.js +89 -0
  174. package/shared/validators/mergeValidators.test.js +116 -0
  175. package/shared/validators/nestValidator.js +53 -0
  176. package/shared/validators/nestValidator.test.js +60 -0
  177. package/shared/validators/recordIdParamsValidator.js +36 -0
  178. package/shared/validators/recordIdParamsValidator.test.js +20 -0
  179. package/shared/validators/resourceRequiredMetadata.js +41 -0
  180. package/shared/validators/resourceRequiredMetadata.test.js +49 -0
  181. package/test/barrelExposure.test.js +106 -0
  182. package/test/dynamicImportPolicy.test.js +89 -0
  183. package/test/exportsContract.test.js +168 -0
  184. package/test/routeInputContractGuard.test.js +78 -0
  185. package/test/surfaceIndependence.test.js +109 -0
@@ -0,0 +1,185 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createSurfaceRuntime } from "../shared/surface/runtime.js";
4
+ import {
5
+ bootstrapClientShellApp,
6
+ createClientBootstrapLogger,
7
+ createSurfaceShellRouter
8
+ } from "./shellBootstrap.js";
9
+ import { getClientAppConfig } from "./appConfig.js";
10
+
11
+ function createSurfaceRuntimeFixture() {
12
+ return createSurfaceRuntime({
13
+ allMode: "all",
14
+ surfaces: {
15
+ app: { id: "app", pagesRoot: "app", enabled: true },
16
+ admin: { id: "admin", pagesRoot: "admin", enabled: true }
17
+ },
18
+ defaultSurfaceId: "app"
19
+ });
20
+ }
21
+
22
+ test("createSurfaceShellRouter builds active routes and installs a guard", () => {
23
+ const surfaceRuntime = createSurfaceRuntimeFixture();
24
+ const guards = [];
25
+ const routerState = {
26
+ options: null
27
+ };
28
+
29
+ const { router, activeRoutes, fallbackRoute } = createSurfaceShellRouter({
30
+ createRouter(options) {
31
+ routerState.options = options;
32
+ return {
33
+ beforeEach(guard) {
34
+ guards.push(guard);
35
+ }
36
+ };
37
+ },
38
+ history: { kind: "memory" },
39
+ routes: [
40
+ {
41
+ path: "/app/home",
42
+ name: "app-home",
43
+ component: {}
44
+ },
45
+ {
46
+ path: "/admin/home",
47
+ name: "admin-home",
48
+ component: {}
49
+ }
50
+ ],
51
+ surfaceRuntime,
52
+ surfaceMode: "app",
53
+ notFoundComponent: {},
54
+ guard: {
55
+ surfaceDefinitions: {
56
+ app: { id: "app", pagesRoot: "app", requiresAuth: false },
57
+ admin: { id: "admin", pagesRoot: "admin", requiresAuth: false }
58
+ },
59
+ defaultSurfaceId: "app",
60
+ webRootAllowed: "yes"
61
+ }
62
+ });
63
+
64
+ assert.ok(router);
65
+ assert.equal(guards.length, 1);
66
+ assert.equal(routerState.options.routes.length, activeRoutes.length);
67
+ assert.equal(activeRoutes.some((route) => route.path === "/app/home"), true);
68
+ assert.equal(activeRoutes.some((route) => route.path === "/admin/home"), false);
69
+ assert.equal(activeRoutes.some((route) => route.path === "/:pathMatch(.*)*"), true);
70
+ assert.equal(fallbackRoute.path, "/:pathMatch(.*)*");
71
+ });
72
+
73
+ test("createClientBootstrapLogger enables debug from env flag", () => {
74
+ const logger = createClientBootstrapLogger({
75
+ env: { VITE_JSKIT_CLIENT_DEBUG: "1" }
76
+ });
77
+
78
+ assert.equal(typeof logger.info, "function");
79
+ assert.equal(typeof logger.debug, "function");
80
+ assert.equal(logger.isDebugEnabled, true);
81
+ });
82
+
83
+ test("bootstrapClientShellApp boots modules, reinstalls fallback route, and mounts app", async () => {
84
+ const calls = [];
85
+ const logs = [];
86
+ const surfaceRuntime = createSurfaceRuntimeFixture();
87
+ const plugin = { name: "vuetify-like-plugin" };
88
+ const fallbackRoute = {
89
+ name: "not-found",
90
+ path: "/:pathMatch(.*)*",
91
+ component: {}
92
+ };
93
+
94
+ const app = {
95
+ used: [],
96
+ mountedAt: "",
97
+ use(entry) {
98
+ this.used.push(entry);
99
+ return this;
100
+ },
101
+ mount(selector) {
102
+ this.mountedAt = selector;
103
+ calls.push(`mount:${selector}`);
104
+ }
105
+ };
106
+
107
+ const router = {
108
+ routes: [fallbackRoute],
109
+ addRoute(route) {
110
+ calls.push(`add:${route.name || route.path}`);
111
+ this.routes.push(route);
112
+ },
113
+ hasRoute(name) {
114
+ return this.routes.some((route) => String(route?.name || "") === String(name || ""));
115
+ },
116
+ removeRoute(name) {
117
+ calls.push(`remove:${name}`);
118
+ this.routes = this.routes.filter((route) => String(route?.name || "") !== String(name || ""));
119
+ },
120
+ getRoutes() {
121
+ return [...this.routes];
122
+ },
123
+ async isReady() {
124
+ calls.push("isReady");
125
+ }
126
+ };
127
+
128
+ const result = await bootstrapClientShellApp({
129
+ createApp(rootComponent) {
130
+ calls.push(`createApp:${String(rootComponent || "")}`);
131
+ return app;
132
+ },
133
+ rootComponent: "RootComponent",
134
+ appConfig: {
135
+ crud: {
136
+ contacts: {
137
+ namespace: "crm",
138
+ visibility: "workspace"
139
+ }
140
+ }
141
+ },
142
+ appPlugins: [plugin],
143
+ router,
144
+ bootClientModules: async (context) => {
145
+ calls.push("bootClientModules");
146
+ assert.equal(context.app, app);
147
+ assert.equal(context.router, router);
148
+ assert.equal(typeof context.logger.debug, "function");
149
+ return {
150
+ modules: ["@example/main"],
151
+ providerCount: 1,
152
+ routeCount: 3
153
+ };
154
+ },
155
+ surfaceRuntime,
156
+ surfaceMode: "app",
157
+ env: { VITE_JSKIT_CLIENT_DEBUG: "1" },
158
+ fallbackRoute,
159
+ logger: {
160
+ info(payload, message) {
161
+ logs.push({ payload, message });
162
+ },
163
+ warn() {},
164
+ error() {}
165
+ },
166
+ onAfterModulesBootstrapped(context) {
167
+ calls.push(`after:${context.clientBootstrap.routeCount}`);
168
+ }
169
+ });
170
+
171
+ assert.equal(result.debugEnabled, true);
172
+ assert.deepEqual(getClientAppConfig().crud?.contacts, {
173
+ namespace: "crm",
174
+ visibility: "workspace"
175
+ });
176
+ assert.equal(app.used[0], plugin);
177
+ assert.equal(app.used[1], router);
178
+ assert.equal(app.mountedAt, "#app");
179
+ assert.equal(logs.length, 1);
180
+ assert.equal(calls.includes("bootClientModules"), true);
181
+ assert.equal(calls.includes("remove:not-found"), true);
182
+ assert.equal(calls.includes("add:not-found"), true);
183
+ assert.equal(calls.includes("isReady"), true);
184
+ assert.equal(calls.includes("after:3"), true);
185
+ });
@@ -0,0 +1,321 @@
1
+ import { deriveSurfaceRouteBaseFromPagesRoot } from "../shared/surface/index.js";
2
+ import { filterRoutesBySurface } from "../shared/surface/runtime.js";
3
+ import { isExternalLinkTarget } from "../shared/support/linkPath.js";
4
+ import { AUTH_POLICY_AUTHENTICATED, AUTH_POLICY_PUBLIC } from "../shared/support/policies.js";
5
+
6
+ const DEFAULT_GUARD_EVALUATOR_KEY = "__JSKIT_WEB_SHELL_GUARD_EVALUATOR__";
7
+ const WEB_ROOT_ALLOW_YES = "yes";
8
+ const WEB_ROOT_ALLOW_NO = "no";
9
+
10
+ function createFallbackNotFoundRoute(component) {
11
+ if (!component) {
12
+ throw new Error("createFallbackNotFoundRoute requires a component.");
13
+ }
14
+
15
+ return Object.freeze({
16
+ path: "/:pathMatch(.*)*",
17
+ name: "not-found",
18
+ component,
19
+ meta: {
20
+ jskit: {
21
+ scope: "global"
22
+ }
23
+ }
24
+ });
25
+ }
26
+
27
+ function buildSurfaceAwareRoutes({
28
+ routes = [],
29
+ surfaceRuntime,
30
+ surfaceMode,
31
+ fallbackRoute,
32
+ notFoundComponent
33
+ } = {}) {
34
+ const effectiveFallback =
35
+ fallbackRoute ||
36
+ (notFoundComponent ? createFallbackNotFoundRoute(notFoundComponent) : null);
37
+
38
+ if (!effectiveFallback) {
39
+ throw new TypeError("buildSurfaceAwareRoutes requires fallbackRoute or notFoundComponent.");
40
+ }
41
+ return filterRoutesBySurface([...routes, effectiveFallback], {
42
+ surfaceRuntime,
43
+ surfaceMode
44
+ });
45
+ }
46
+
47
+ function normalizeGuardPolicy(value) {
48
+ return String(value || "")
49
+ .trim()
50
+ .toLowerCase();
51
+ }
52
+
53
+ function resolveRouteGuardFromMeta(meta) {
54
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
55
+ return null;
56
+ }
57
+
58
+ if (meta.guard && typeof meta.guard === "object" && !Array.isArray(meta.guard)) {
59
+ return meta.guard;
60
+ }
61
+
62
+ if (meta.jskit && typeof meta.jskit === "object" && !Array.isArray(meta.jskit)) {
63
+ const jskitGuard = meta.jskit.guard;
64
+ if (jskitGuard && typeof jskitGuard === "object" && !Array.isArray(jskitGuard)) {
65
+ return jskitGuard;
66
+ }
67
+ }
68
+
69
+ return null;
70
+ }
71
+
72
+ function resolveRouteGuard(to) {
73
+ const matched = Array.isArray(to?.matched) ? to.matched : [];
74
+ for (let index = matched.length - 1; index >= 0; index -= 1) {
75
+ const routeRecord = matched[index] && typeof matched[index] === "object" ? matched[index] : null;
76
+ if (!routeRecord) {
77
+ continue;
78
+ }
79
+
80
+ if (routeRecord.guard && typeof routeRecord.guard === "object" && !Array.isArray(routeRecord.guard)) {
81
+ return routeRecord.guard;
82
+ }
83
+
84
+ const metaGuard = resolveRouteGuardFromMeta(routeRecord.meta);
85
+ if (metaGuard) {
86
+ return metaGuard;
87
+ }
88
+ }
89
+
90
+ return null;
91
+ }
92
+
93
+ function resolveSearchFromFullPath(fullPath) {
94
+ const rawFullPath = String(fullPath || "").trim();
95
+ const queryStart = rawFullPath.indexOf("?");
96
+ if (queryStart < 0) {
97
+ return "";
98
+ }
99
+
100
+ const hashStart = rawFullPath.indexOf("#", queryStart);
101
+ return hashStart < 0 ? rawFullPath.slice(queryStart) : rawFullPath.slice(queryStart, hashStart);
102
+ }
103
+
104
+ function resolveSurfaceDefinition(surfaceDefinitions, surfaceId) {
105
+ const normalizedSurfaceId = String(surfaceId || "")
106
+ .trim()
107
+ .toLowerCase();
108
+ if (!normalizedSurfaceId) {
109
+ return null;
110
+ }
111
+ const definition = surfaceDefinitions[normalizedSurfaceId];
112
+ if (!definition || typeof definition !== "object") {
113
+ return null;
114
+ }
115
+ return definition;
116
+ }
117
+
118
+ function normalizeWebRootAllowed(value) {
119
+ const normalizedValue = String(value || "")
120
+ .trim()
121
+ .toLowerCase();
122
+ if (normalizedValue === WEB_ROOT_ALLOW_YES || normalizedValue === WEB_ROOT_ALLOW_NO) {
123
+ return normalizedValue;
124
+ }
125
+ return WEB_ROOT_ALLOW_YES;
126
+ }
127
+
128
+ function resolveDefaultSurfaceRootPath({ surfaceDefinitions, defaultSurfaceId }) {
129
+ const defaultSurface = resolveSurfaceDefinition(surfaceDefinitions, defaultSurfaceId);
130
+ return String(
131
+ defaultSurface?.routeBase || deriveSurfaceRouteBaseFromPagesRoot(defaultSurface?.pagesRoot || "")
132
+ ).trim() || "/";
133
+ }
134
+
135
+ function resolveSurfaceRequiresAuth({ pathname, surfaceRuntime, surfaceDefinitions }) {
136
+ const normalizedPathname = String(pathname || "/").trim() || "/";
137
+ const surfaceId = surfaceRuntime.resolveSurfaceFromPathname(normalizedPathname);
138
+ const surfaceDefinition = resolveSurfaceDefinition(surfaceDefinitions, surfaceId);
139
+ return Boolean(surfaceDefinition?.requiresAuth);
140
+ }
141
+
142
+ function resolveEffectiveRouteGuard({
143
+ to,
144
+ surfaceRuntime,
145
+ surfaceDefinitions,
146
+ authenticatedPolicy = AUTH_POLICY_AUTHENTICATED,
147
+ publicPolicy = AUTH_POLICY_PUBLIC
148
+ }) {
149
+ const routeGuard = resolveRouteGuard(to);
150
+ const routePolicy = normalizeGuardPolicy(routeGuard?.policy);
151
+ if (routePolicy) {
152
+ return {
153
+ policy: routePolicy
154
+ };
155
+ }
156
+
157
+ if (
158
+ resolveSurfaceRequiresAuth({
159
+ pathname: to?.path || "/",
160
+ surfaceRuntime,
161
+ surfaceDefinitions
162
+ })
163
+ ) {
164
+ return {
165
+ policy: authenticatedPolicy
166
+ };
167
+ }
168
+
169
+ return {
170
+ policy: publicPolicy
171
+ };
172
+ }
173
+
174
+ function resolveGuardEvaluator(guardEvaluatorKey) {
175
+ if (typeof globalThis !== "object" || !globalThis) {
176
+ return null;
177
+ }
178
+
179
+ const evaluator = globalThis[guardEvaluatorKey];
180
+ if (typeof evaluator !== "function") {
181
+ return null;
182
+ }
183
+ return evaluator;
184
+ }
185
+
186
+ function normalizeGuardOutcome(outcome) {
187
+ if (outcome === false) {
188
+ return {
189
+ allow: false,
190
+ redirectTo: "",
191
+ reason: ""
192
+ };
193
+ }
194
+
195
+ if (outcome == null || outcome === true || typeof outcome !== "object" || Array.isArray(outcome)) {
196
+ return {
197
+ allow: true,
198
+ redirectTo: "",
199
+ reason: ""
200
+ };
201
+ }
202
+
203
+ return {
204
+ allow: outcome.allow !== false,
205
+ redirectTo: String(outcome.redirectTo || "").trim(),
206
+ reason: String(outcome.reason || "").trim()
207
+ };
208
+ }
209
+
210
+ function evaluateShellGuard({ guard, to, guardEvaluatorKey }) {
211
+ const evaluator = resolveGuardEvaluator(guardEvaluatorKey);
212
+ if (!evaluator) {
213
+ return {
214
+ allow: true,
215
+ redirectTo: "",
216
+ reason: ""
217
+ };
218
+ }
219
+
220
+ const pathname = String(to?.path || "/").trim() || "/";
221
+ const search = resolveSearchFromFullPath(to?.fullPath || "");
222
+
223
+ try {
224
+ return normalizeGuardOutcome(
225
+ evaluator({
226
+ guard,
227
+ phase: "route",
228
+ context: {
229
+ to,
230
+ location: {
231
+ pathname,
232
+ search
233
+ }
234
+ }
235
+ })
236
+ );
237
+ } catch {
238
+ return {
239
+ allow: false,
240
+ redirectTo: "",
241
+ reason: "guard-evaluator-error"
242
+ };
243
+ }
244
+ }
245
+
246
+ function createShellBeforeEachGuard({
247
+ surfaceRuntime,
248
+ surfaceDefinitions,
249
+ defaultSurfaceId,
250
+ webRootAllowed = WEB_ROOT_ALLOW_YES,
251
+ guardEvaluatorKey = DEFAULT_GUARD_EVALUATOR_KEY,
252
+ authenticatedPolicy = AUTH_POLICY_AUTHENTICATED,
253
+ publicPolicy = AUTH_POLICY_PUBLIC
254
+ } = {}) {
255
+ if (!surfaceRuntime || typeof surfaceRuntime.resolveSurfaceFromPathname !== "function") {
256
+ throw new TypeError("createShellBeforeEachGuard requires surfaceRuntime.resolveSurfaceFromPathname().");
257
+ }
258
+ if (!surfaceDefinitions || typeof surfaceDefinitions !== "object" || Array.isArray(surfaceDefinitions)) {
259
+ throw new TypeError("createShellBeforeEachGuard requires a surfaceDefinitions object.");
260
+ }
261
+
262
+ return (to) => {
263
+ const normalizedWebRootAllowed = normalizeWebRootAllowed(webRootAllowed);
264
+ const defaultSurfaceRootPath = resolveDefaultSurfaceRootPath({
265
+ surfaceDefinitions,
266
+ defaultSurfaceId
267
+ });
268
+ if (
269
+ normalizedWebRootAllowed === WEB_ROOT_ALLOW_NO &&
270
+ String(to?.path || "/").trim() === "/" &&
271
+ defaultSurfaceRootPath !== "/"
272
+ ) {
273
+ const search = resolveSearchFromFullPath(to?.fullPath || "");
274
+ const hash = String(to?.hash || "").trim();
275
+ return `${defaultSurfaceRootPath}${search}${hash}`;
276
+ }
277
+
278
+ const guard = resolveEffectiveRouteGuard({
279
+ to,
280
+ surfaceRuntime,
281
+ surfaceDefinitions,
282
+ authenticatedPolicy,
283
+ publicPolicy
284
+ });
285
+ if (guard.policy !== authenticatedPolicy) {
286
+ return true;
287
+ }
288
+
289
+ const outcome = evaluateShellGuard({
290
+ guard,
291
+ to,
292
+ guardEvaluatorKey
293
+ });
294
+ if (outcome.allow) {
295
+ return true;
296
+ }
297
+
298
+ if (outcome.redirectTo) {
299
+ if (isExternalLinkTarget(outcome.redirectTo)) {
300
+ if (typeof window === "object" && window?.location && typeof window.location.assign === "function") {
301
+ window.location.assign(outcome.redirectTo);
302
+ }
303
+ return false;
304
+ }
305
+ return outcome.redirectTo;
306
+ }
307
+
308
+ return false;
309
+ };
310
+ }
311
+
312
+ export {
313
+ AUTH_POLICY_AUTHENTICATED,
314
+ AUTH_POLICY_PUBLIC,
315
+ WEB_ROOT_ALLOW_YES,
316
+ WEB_ROOT_ALLOW_NO,
317
+ DEFAULT_GUARD_EVALUATOR_KEY,
318
+ createFallbackNotFoundRoute,
319
+ buildSurfaceAwareRoutes,
320
+ createShellBeforeEachGuard
321
+ };
@@ -0,0 +1,113 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createSurfaceRuntime } from "../shared/surface/runtime.js";
4
+ import { buildSurfaceAwareRoutes } from "./shellRouting.js";
5
+
6
+ test("buildSurfaceAwareRoutes keeps declared route paths and filters by surface mode", () => {
7
+ const surfaceRuntime = createSurfaceRuntime({
8
+ defaultSurfaceId: "app",
9
+ surfaces: {
10
+ app: { id: "app", pagesRoot: "app", enabled: true, requiresWorkspace: false },
11
+ coffie: { id: "coffie", pagesRoot: "coffie", enabled: true, requiresWorkspace: true },
12
+ console: { id: "console", pagesRoot: "console", enabled: true, requiresWorkspace: false }
13
+ }
14
+ });
15
+
16
+ const routes = buildSurfaceAwareRoutes({
17
+ routes: [
18
+ {
19
+ path: "/app/home",
20
+ name: "app-home",
21
+ component: {}
22
+ },
23
+ {
24
+ path: "/coffie",
25
+ name: "coffie-home",
26
+ component: {}
27
+ },
28
+ {
29
+ path: "/coffie/members",
30
+ name: "coffie-members",
31
+ component: {}
32
+ },
33
+ {
34
+ path: "/coffie/workspaces",
35
+ name: "coffie-workspaces",
36
+ component: {}
37
+ },
38
+ {
39
+ path: "/console/settings",
40
+ name: "console-settings",
41
+ component: {}
42
+ },
43
+ {
44
+ path: "/auth/login",
45
+ name: "auth-login",
46
+ scope: "global",
47
+ component: {}
48
+ },
49
+ {
50
+ path: "/",
51
+ name: "root-global",
52
+ component: {},
53
+ meta: {
54
+ jskit: {
55
+ scope: "global"
56
+ }
57
+ }
58
+ }
59
+ ],
60
+ surfaceRuntime,
61
+ surfaceMode: "all",
62
+ notFoundComponent: {}
63
+ });
64
+
65
+ const routePaths = routes.map((route) => route.path);
66
+ assert.equal(routePaths.includes("/coffie"), true);
67
+ assert.equal(routePaths.includes("/coffie/members"), true);
68
+ assert.equal(routePaths.includes("/coffie/workspaces"), true);
69
+ assert.equal(routePaths.includes("/app/home"), true);
70
+ assert.equal(routePaths.includes("/console/settings"), true);
71
+ assert.equal(routePaths.includes("/auth/login"), true);
72
+ assert.equal(routePaths.includes("/"), true);
73
+ });
74
+
75
+ test("buildSurfaceAwareRoutes preserves parent path when nested descendants are global", () => {
76
+ const surfaceRuntime = createSurfaceRuntime({
77
+ defaultSurfaceId: "app",
78
+ surfaces: {
79
+ app: { id: "app", pagesRoot: "w/[workspaceSlug]", enabled: true, requiresWorkspace: true },
80
+ admin: { id: "admin", pagesRoot: "w/[workspaceSlug]/admin", enabled: true, requiresWorkspace: true }
81
+ }
82
+ });
83
+
84
+ const routes = buildSurfaceAwareRoutes({
85
+ routes: [
86
+ {
87
+ path: "/account",
88
+ children: [
89
+ {
90
+ path: "settings",
91
+ children: [
92
+ {
93
+ path: "",
94
+ component: {},
95
+ meta: {
96
+ jskit: {
97
+ scope: "global"
98
+ }
99
+ }
100
+ }
101
+ ]
102
+ }
103
+ ]
104
+ }
105
+ ],
106
+ surfaceRuntime,
107
+ surfaceMode: "all",
108
+ notFoundComponent: {}
109
+ });
110
+
111
+ const routePaths = routes.map((route) => route.path);
112
+ assert.equal(routePaths.includes("/account"), true);
113
+ });