@jskit-ai/kernel 0.1.245 → 0.1.247

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 (102) hide show
  1. package/client/appConfig.js +2 -1
  2. package/client/asyncModuleRecovery.js +2 -4
  3. package/client/index.d.ts +13 -75
  4. package/client/index.js +1 -1
  5. package/client/logging.js +8 -2
  6. package/client/moduleBootstrap.js +41 -33
  7. package/client/moduleBootstrap.test.js +35 -0
  8. package/client/shellBootstrap.js +1 -8
  9. package/client/shellBootstrap.test.js +32 -5
  10. package/client/shellRouting.js +16 -61
  11. package/client/shellRouting.test.js +30 -1
  12. package/client/vite/clientBootstrapPlugin.js +0 -6
  13. package/client/vite/clientBootstrapPlugin.test.js +2 -3
  14. package/internal/node/installedPackages.js +19 -62
  15. package/internal/node/installedPackages.test.js +33 -0
  16. package/package.json +2 -4
  17. package/server/actions/actionCatalogue.js +13 -34
  18. package/{shared → server}/actions/actionContributorHelpers.js +0 -5
  19. package/{shared → server}/actions/actionDefinitions.js +24 -131
  20. package/{shared → server}/actions/actionDefinitions.test.js +75 -21
  21. package/server/actions/actionEvents.js +97 -2
  22. package/server/actions/actionEvents.test.js +79 -0
  23. package/{shared → server}/actions/executionContext.js +3 -13
  24. package/server/actions/idempotency.js +24 -0
  25. package/server/actions/pipeline.js +87 -0
  26. package/{shared → server}/actions/policies.js +20 -33
  27. package/{shared → server}/actions/policies.test.js +6 -6
  28. package/{shared → server}/actions/registry.js +3 -22
  29. package/{shared → server}/actions/registry.test.js +70 -32
  30. package/server/actions/requestMeta.js +17 -0
  31. package/server/http/capabilityHttpRuntime.js +1 -1
  32. package/server/http/capabilityHttpRuntime.test.js +56 -0
  33. package/server/http/errorHandling.js +291 -0
  34. package/server/{runtime/fastifyBootstrap.test.js → http/errorHandling.test.js} +72 -120
  35. package/server/http/lib/routeRegistration.js +36 -150
  36. package/server/http/lib/routeTransport.js +2 -0
  37. package/server/http/lib/routeValidator.js +5 -47
  38. package/server/http/lib/routeValidator.test.js +15 -17
  39. package/server/http/lib/router.js +10 -94
  40. package/server/platform/providerRuntime/capabilityProviderLoader.js +2 -1
  41. package/server/platform/providerRuntime/helpers.js +1 -34
  42. package/server/runtime/integers.js +1 -5
  43. package/server/runtime/serviceAuthorization.js +4 -9
  44. package/server/support/appConfig.js +2 -1
  45. package/server/support/appConfigFiles.js +1 -38
  46. package/server/support/appConfigFiles.test.js +3 -39
  47. package/server/support/index.js +1 -20
  48. package/shared/capabilities/defineProvider.js +1 -1
  49. package/shared/capabilities/index.js +1 -1
  50. package/shared/index.js +1 -1
  51. package/shared/support/index.js +1 -4
  52. package/shared/support/mobileConfig.js +99 -0
  53. package/shared/support/mobileConfig.test.js +113 -0
  54. package/shared/support/normalize.js +0 -108
  55. package/shared/support/normalize.test.js +0 -134
  56. package/shared/{actions/actionContributorHelpers.test.js → support/permissions.test.js} +1 -1
  57. package/shared/support/returnToPath.js +1 -20
  58. package/shared/support/returnToPath.test.js +1 -36
  59. package/shared/support/shellLayoutTargets.js +2 -140
  60. package/shared/support/shellLayoutTargets.test.js +1 -110
  61. package/shared/support/sorting.js +1 -9
  62. package/shared/surface/apiPaths.js +1 -82
  63. package/shared/surface/paths.js +38 -171
  64. package/shared/surface/registry.js +2 -1
  65. package/shared/surface/runtime.js +18 -109
  66. package/shared/surface/runtime.test.js +57 -0
  67. package/shared/validators/index.js +1 -4
  68. package/shared/validators/jsonRestSchemaSupport.js +0 -9
  69. package/shared/validators/recordIdParamsValidator.js +0 -6
  70. package/shared/validators/schemaDefinitions.js +4 -30
  71. package/shared/validators/schemaPayloadValidation.js +2 -6
  72. package/test/barrelExposure.test.js +3 -7
  73. package/test/exportsContract.test.js +1 -3
  74. package/client/mobileLaunchRouting.js +0 -231
  75. package/client/mobileLaunchRouting.test.js +0 -230
  76. package/server/http/lib/controller.js +0 -183
  77. package/server/http/lib/controller.test.js +0 -143
  78. package/server/http/lib/index.js +0 -5
  79. package/server/runtime/canonicalJson.js +0 -74
  80. package/server/runtime/entityChangeEvents.js +0 -307
  81. package/server/runtime/entityChangeEvents.test.js +0 -274
  82. package/server/runtime/fastifyBootstrap.js +0 -553
  83. package/server/runtime/moduleConfig.js +0 -223
  84. package/server/runtime/moduleConfig.test.js +0 -165
  85. package/server/runtime/pagination.js +0 -13
  86. package/server/runtime/realtimeNormalization.js +0 -21
  87. package/server/runtime/routeUtils.js +0 -20
  88. package/server/runtime/securityAudit.js +0 -269
  89. package/server/runtime/securityAudit.test.js +0 -41
  90. package/server/support/pageTargets.js +0 -772
  91. package/server/support/pageTargets.test.js +0 -659
  92. package/server/support/shellOutlets.js +0 -576
  93. package/server/support/shellOutlets.test.js +0 -336
  94. package/server/surface/index.js +0 -18
  95. package/shared/actions/audit.js +0 -7
  96. package/shared/actions/idempotency.js +0 -62
  97. package/shared/actions/observability.js +0 -10
  98. package/shared/actions/pipeline.js +0 -309
  99. package/shared/actions/requestMeta.js +0 -36
  100. package/shared/actions/textNormalization.js +0 -3
  101. package/shared/actions/withActionDefaults.js +0 -34
  102. /package/{shared → server}/actions/executionContext.test.js +0 -0
@@ -1,4 +1,5 @@
1
- import { isRecord, normalizeMobileConfig } from "../shared/support/normalize.js";
1
+ import { isRecord } from "../shared/support/normalize.js";
2
+ import { normalizeMobileConfig } from "../shared/support/mobileConfig.js";
2
3
 
3
4
  const CLIENT_APP_CONFIG_GLOBAL_KEY = "__JSKIT_CLIENT_APP_CONFIG__";
4
5
  const EMPTY_CLIENT_APP_CONFIG = Object.freeze({});
@@ -1,3 +1,5 @@
1
+ import { isRecord } from "../shared/support/normalize.js";
2
+
1
3
  const DYNAMIC_IMPORT_ERROR_PATTERNS = Object.freeze([
2
4
  /ChunkLoadError/iu,
3
5
  /Failed to fetch dynamically imported module/iu,
@@ -7,10 +9,6 @@ const DYNAMIC_IMPORT_ERROR_PATTERNS = Object.freeze([
7
9
  /Unable to preload CSS/iu
8
10
  ]);
9
11
 
10
- function isRecord(value) {
11
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
12
- }
13
-
14
12
  function errorText(error = null) {
15
13
  return String(error?.message || error || "").trim();
16
14
  }
package/client/index.d.ts CHANGED
@@ -6,85 +6,13 @@ export type ClientLogger = {
6
6
  isDebugEnabled?: boolean;
7
7
  };
8
8
 
9
- export type AsyncModuleRecoveryState = {
10
- attempt: number;
11
- error: any;
12
- label: string;
13
- message: string;
14
- retry: (() => any) | null;
15
- stale: boolean;
16
- visible: boolean;
17
- };
18
-
19
- export function createAsyncModuleRecoveryState(options?: {
20
- label?: string;
21
- message?: string;
22
- retry?: (() => any) | null;
23
- }): AsyncModuleRecoveryState;
24
-
25
- export function isDynamicImportError(error?: any): boolean;
26
-
27
- export function dynamicImportErrorMessage(error?: any, options?: {
28
- label?: string;
29
- stale?: boolean;
30
- }): string;
31
-
32
- export function notifyAsyncModuleLoadError(
33
- state: AsyncModuleRecoveryState,
34
- error?: any,
35
- options?: {
36
- label?: string;
37
- message?: string;
38
- retry?: (() => any) | null;
39
- stale?: boolean;
40
- }
41
- ): AsyncModuleRecoveryState;
42
-
43
- export function dismissAsyncModuleRecovery(state: AsyncModuleRecoveryState): boolean;
44
-
45
- export function guardedReloadApp(options?: {
46
- browserWindow?: any;
47
- fetchFn?: ((input: string, init?: Record<string, any>) => Promise<any>) | null;
48
- state?: AsyncModuleRecoveryState | null;
49
- label?: string;
50
- message?: string;
51
- }): Promise<boolean>;
9
+ export * from "./asyncModuleRecovery";
52
10
 
53
- export function installAsyncModuleRecoveryHandlers(options?: {
54
- router?: any;
55
- state: AsyncModuleRecoveryState;
56
- label?: string;
57
- onNotify?: (state: AsyncModuleRecoveryState) => void;
58
- windowObject?: any;
59
- }): Readonly<{
60
- dispose: () => void;
61
- }>;
11
+ export const DEFAULT_GUARD_EVALUATOR_KEY: "__JSKIT_WEB_SHELL_GUARD_EVALUATOR__";
62
12
 
63
13
  export function getClientAppConfig(): Readonly<Record<string, any>>;
64
14
  export function resolveMobileConfig(appConfig?: Record<string, any>): Readonly<Record<string, any>>;
65
15
  export function resolveClientAssetMode(appConfig?: Record<string, any>): string;
66
- export function normalizeIncomingAppUrl(
67
- url?: string,
68
- mobileConfig?: Record<string, any>,
69
- options?: {
70
- currentOrigin?: string;
71
- allowedHttpOrigins?: string[];
72
- }
73
- ): string;
74
- export function registerMobileLaunchRouting(options?: {
75
- router: any;
76
- mobileConfig?: Record<string, any>;
77
- getInitialLaunchUrl?: () => Promise<string> | string;
78
- subscribeToLaunchUrls?: (handler: (url: string) => void) => (() => void) | void;
79
- currentOrigin?: string;
80
- allowedHttpOrigins?: string[];
81
- logger?: ClientLogger;
82
- }): Readonly<{
83
- initialize: () => Promise<string>;
84
- dispose: () => void;
85
- applyIncomingUrl: (url?: string, reason?: string) => Promise<string>;
86
- }>;
87
-
88
16
  export function resolveClientBootstrapDebugEnabled(options?: {
89
17
  env?: Record<string, any>;
90
18
  debugEnabled?: boolean;
@@ -101,7 +29,15 @@ export function createShellRouter(options?: {
101
29
  surfaceMode?: string;
102
30
  fallbackRoute?: any;
103
31
  notFoundComponent?: any;
104
- guard?: false | ((to: any) => any) | Record<string, any>;
32
+ guard?:
33
+ | false
34
+ | ((to: any) => any)
35
+ | {
36
+ webRootAllowed?: "yes" | "no";
37
+ guardEvaluatorKey?: string;
38
+ authenticatedPolicy?: string;
39
+ publicPolicy?: string;
40
+ };
105
41
  /** Defaults to shellScrollBehavior; pass your own function, or false for none. */
106
42
  scrollBehavior?: false | ((to: any, from: any, savedPosition: any) => any);
107
43
  }): Readonly<{
@@ -116,6 +52,7 @@ export function bootstrapClientShellApp(options?: {
116
52
  appConfig?: Record<string, any>;
117
53
  appPlugins?: any[];
118
54
  pinia?: any;
55
+ queryClient?: any;
119
56
  router: any;
120
57
  bootClientModules: (context: any) => Promise<any>;
121
58
  surfaceRuntime: any;
@@ -123,6 +60,7 @@ export function bootstrapClientShellApp(options?: {
123
60
  env?: Record<string, any>;
124
61
  fallbackRoute?: any;
125
62
  logger?: ClientLogger;
63
+ /** Its isDebugEnabled decides whether bootstrap builds debug payloads; without it, a debug method means enabled. */
126
64
  createBootstrapLogger?: (options: {
127
65
  env?: Record<string, any>;
128
66
  logger?: ClientLogger;
package/client/index.js CHANGED
@@ -8,11 +8,11 @@ export {
8
8
  isDynamicImportError,
9
9
  notifyAsyncModuleLoadError
10
10
  } from "./asyncModuleRecovery.js";
11
- export { normalizeIncomingAppUrl, registerMobileLaunchRouting } from "./mobileLaunchRouting.js";
12
11
  export {
13
12
  resolveClientBootstrapDebugEnabled,
14
13
  createSurfaceShellRouter as createShellRouter,
15
14
  shellScrollBehavior,
16
15
  bootstrapClientShellApp
17
16
  } from "./shellBootstrap.js";
17
+ export { DEFAULT_GUARD_EVALUATOR_KEY } from "./shellRouting.js";
18
18
  export { createComponentInteractionEmitter } from "./componentInteraction.js";
package/client/logging.js CHANGED
@@ -1,12 +1,17 @@
1
1
  import { isRecord } from "../shared/support/normalize.js";
2
2
 
3
+ // isDebugEnabled tells callers whether debug output goes anywhere, so they
4
+ // can skip building debug payloads. A logger may state it; otherwise it is
5
+ // true exactly when the logger has its own debug method.
3
6
  function createStructuredLogger(logger = console) {
4
7
  if (isRecord(logger)) {
8
+ const hasDebug = typeof logger.debug === "function";
5
9
  return Object.freeze({
6
10
  info: typeof logger.info === "function" ? logger.info.bind(logger) : console.info.bind(console),
7
11
  warn: typeof logger.warn === "function" ? logger.warn.bind(logger) : console.warn.bind(console),
8
12
  error: typeof logger.error === "function" ? logger.error.bind(logger) : console.error.bind(console),
9
- debug: typeof logger.debug === "function" ? logger.debug.bind(logger) : () => {}
13
+ debug: hasDebug ? logger.debug.bind(logger) : () => {},
14
+ isDebugEnabled: typeof logger.isDebugEnabled === "boolean" ? logger.isDebugEnabled : hasDebug
10
15
  });
11
16
  }
12
17
 
@@ -14,7 +19,8 @@ function createStructuredLogger(logger = console) {
14
19
  info: console.info.bind(console),
15
20
  warn: console.warn.bind(console),
16
21
  error: console.error.bind(console),
17
- debug: () => {}
22
+ debug: () => {},
23
+ isDebugEnabled: false
18
24
  });
19
25
  }
20
26
 
@@ -136,16 +136,18 @@ function registerClientModuleRoutes({
136
136
  surfaceMode
137
137
  });
138
138
  const log = createStructuredLogger(logger);
139
- log.debug(
140
- {
141
- packageId: normalizedPackageId,
142
- source,
143
- surfaceMode: String(surfaceMode || "").trim(),
144
- declaredRoutes: normalizedRoutes.map(toRouteSnapshot),
145
- activeRoutes: activeRoutes.map(toRouteSnapshot)
146
- },
147
- "Client route registration analysis."
148
- );
139
+ if (log.isDebugEnabled) {
140
+ log.debug(
141
+ {
142
+ packageId: normalizedPackageId,
143
+ source,
144
+ surfaceMode: String(surfaceMode || "").trim(),
145
+ declaredRoutes: normalizedRoutes.map(toRouteSnapshot),
146
+ activeRoutes: activeRoutes.map(toRouteSnapshot)
147
+ },
148
+ "Client route registration analysis."
149
+ );
150
+ }
149
151
 
150
152
  const declaredPaths = normalizedRoutes.map((route) => route.path);
151
153
  const activePaths = activeRoutes.map((route) => route.path);
@@ -167,14 +169,16 @@ function registerClientModuleRoutes({
167
169
 
168
170
  seenRoutePaths.add(normalizedPath);
169
171
  router.addRoute(toVueRouteRecord(route));
170
- log.debug(
171
- {
172
- packageId: normalizedPackageId,
173
- source,
174
- route: toRouteSnapshot(route)
175
- },
176
- "Added client route to router."
177
- );
172
+ if (log.isDebugEnabled) {
173
+ log.debug(
174
+ {
175
+ packageId: normalizedPackageId,
176
+ source,
177
+ route: toRouteSnapshot(route)
178
+ },
179
+ "Added client route to router."
180
+ );
181
+ }
178
182
  registeredCount += 1;
179
183
  }
180
184
 
@@ -560,14 +564,16 @@ async function bootClientModules({
560
564
  },
561
565
  "Registered client module routes."
562
566
  );
563
- log.debug(
564
- {
565
- packageId,
566
- source,
567
- routerRoutes: summarizeRouterRoutes(router)
568
- },
569
- "Router route table after client route registration."
570
- );
567
+ if (log.isDebugEnabled) {
568
+ log.debug(
569
+ {
570
+ packageId,
571
+ source,
572
+ routerRoutes: summarizeRouterRoutes(router)
573
+ },
574
+ "Router route table after client route registration."
575
+ );
576
+ }
571
577
  routeResults.push(result);
572
578
  return result;
573
579
  };
@@ -599,13 +605,15 @@ async function bootClientModules({
599
605
  },
600
606
  "Booted JSKIT client modules."
601
607
  );
602
- log.debug(
603
- {
604
- routerRoutes: summarizeRouterRoutes(router),
605
- currentPath: typeof window !== "undefined" ? String(window.location?.pathname || "") : ""
606
- },
607
- "JSKIT client bootstrap final route table."
608
- );
608
+ if (log.isDebugEnabled) {
609
+ log.debug(
610
+ {
611
+ routerRoutes: summarizeRouterRoutes(router),
612
+ currentPath: typeof window !== "undefined" ? String(window.location?.pathname || "") : ""
613
+ },
614
+ "JSKIT client bootstrap final route table."
615
+ );
616
+ }
609
617
  }
610
618
 
611
619
  return Object.freeze({
@@ -433,3 +433,38 @@ test("bootClientModules throws when packageMetadataClientProviders export is mis
433
433
  /packageMetadata provider export "MissingProvider" must be a capability provider/
434
434
  );
435
435
  });
436
+
437
+ test("bootClientModules builds route debug payloads only when the logger's debug is enabled", async () => {
438
+ const surfaceRuntime = createSurfaceRuntimeFixture();
439
+ const clientModules = [
440
+ {
441
+ packageId: "@example/auth",
442
+ packageMetadataUiRoutes: [{ id: "auth.login", path: "/auth/login", scope: "global", autoRegister: false }],
443
+ module: {
444
+ clientRoutes: [{ id: "auth.login", name: "auth-login", path: "/auth/login", scope: "global", component: {} }]
445
+ }
446
+ }
447
+ ];
448
+
449
+ for (const isDebugEnabled of [false, true]) {
450
+ const router = createRouterStub();
451
+ let routeTableReads = 0;
452
+ router.getRoutes = () => {
453
+ routeTableReads += 1;
454
+ return [...router.routes];
455
+ };
456
+ const messages = [];
457
+ await bootClientModules({
458
+ clientModules,
459
+ router,
460
+ surfaceRuntime,
461
+ surfaceMode: "all",
462
+ logger: { info() {}, warn() {}, error() {}, debug: (_payload, message) => messages.push(message), isDebugEnabled }
463
+ });
464
+
465
+ assert.equal(router.routes.length, 1);
466
+ assert.equal(routeTableReads > 0, isDebugEnabled);
467
+ assert.equal(messages.includes("Added client route to router."), isDebugEnabled);
468
+ assert.equal(messages.includes("JSKIT client bootstrap final route table."), isDebugEnabled);
469
+ }
470
+ });
@@ -204,14 +204,7 @@ async function bootstrapClientShellApp({
204
204
  logger: bootstrapLogger
205
205
  });
206
206
 
207
- const isDebugEnabled =
208
- typeof resolvedLogger?.isDebugEnabled === "boolean"
209
- ? resolvedLogger.isDebugEnabled
210
- : resolveClientBootstrapDebugEnabled({
211
- env,
212
- debugEnabled,
213
- debugEnvKey
214
- });
207
+ const { isDebugEnabled } = bootstrapLogger;
215
208
  if (isDebugEnabled) {
216
209
  bootstrapLogger.info(
217
210
  {
@@ -53,11 +53,6 @@ test("createSurfaceShellRouter builds active routes and installs a guard", () =>
53
53
  surfaceMode: "app",
54
54
  notFoundComponent: {},
55
55
  guard: {
56
- surfaceDefinitions: {
57
- app: { id: "app", pagesRoot: "app", requiresAuth: false },
58
- admin: { id: "admin", pagesRoot: "admin", requiresAuth: false }
59
- },
60
- defaultSurfaceId: "app",
61
56
  webRootAllowed: "yes"
62
57
  }
63
58
  });
@@ -132,6 +127,38 @@ test("createClientBootstrapLogger enables debug from env flag", () => {
132
127
  assert.equal(logger.isDebugEnabled, true);
133
128
  });
134
129
 
130
+ test("bootstrapClientShellApp hands hooks and modules one logger that carries its debug flag", async () => {
131
+ for (const debugEnabled of [false, true]) {
132
+ let routeTableReads = 0;
133
+ const seenLoggers = [];
134
+ const router = {
135
+ addRoute() {},
136
+ getRoutes() {
137
+ routeTableReads += 1;
138
+ return [];
139
+ },
140
+ async isReady() {}
141
+ };
142
+ const result = await bootstrapClientShellApp({
143
+ createApp: () => ({ use() {}, mount() {} }),
144
+ router,
145
+ surfaceRuntime: createSurfaceRuntimeFixture(),
146
+ logger: { info() {}, warn() {}, error() {} },
147
+ debugEnabled,
148
+ bootClientModules: async ({ logger }) => {
149
+ seenLoggers.push(logger);
150
+ return {};
151
+ },
152
+ onAfterRouterReady: ({ logger }) => seenLoggers.push(logger)
153
+ });
154
+
155
+ assert.equal(result.debugEnabled, debugEnabled);
156
+ assert.equal(result.logger.isDebugEnabled, debugEnabled);
157
+ assert.deepEqual(seenLoggers, [result.logger, result.logger]);
158
+ assert.equal(routeTableReads > 0, debugEnabled);
159
+ }
160
+ });
161
+
135
162
  test("bootstrapClientShellApp boots modules, reinstalls fallback route, and mounts app", async () => {
136
163
  const calls = [];
137
164
  const logs = [];
@@ -1,4 +1,3 @@
1
- import { deriveSurfaceRouteBaseFromPagesRoot } from "../shared/surface/index.js";
2
1
  import { filterRoutesBySurface } from "../shared/surface/runtime.js";
3
2
  import { isExternalLinkTarget } from "../shared/support/linkPath.js";
4
3
  import { AUTH_POLICY_AUTHENTICATED, AUTH_POLICY_PUBLIC } from "../shared/support/policies.js";
@@ -101,20 +100,6 @@ function resolveSearchFromFullPath(fullPath) {
101
100
  return hashStart < 0 ? rawFullPath.slice(queryStart) : rawFullPath.slice(queryStart, hashStart);
102
101
  }
103
102
 
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
103
  function normalizeWebRootAllowed(value) {
119
104
  const normalizedValue = String(value || "")
120
105
  .trim()
@@ -125,27 +110,7 @@ function normalizeWebRootAllowed(value) {
125
110
  return WEB_ROOT_ALLOW_YES;
126
111
  }
127
112
 
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
- }) {
113
+ function resolveEffectiveRouteGuard({ to, surfaceRuntime, authenticatedPolicy, publicPolicy }) {
149
114
  const routeGuard = resolveRouteGuard(to);
150
115
  const routePolicy = normalizeGuardPolicy(routeGuard?.policy);
151
116
  if (routePolicy) {
@@ -154,13 +119,8 @@ function resolveEffectiveRouteGuard({
154
119
  };
155
120
  }
156
121
 
157
- if (
158
- resolveSurfaceRequiresAuth({
159
- pathname: to?.path || "/",
160
- surfaceRuntime,
161
- surfaceDefinitions
162
- })
163
- ) {
122
+ const surfaceId = surfaceRuntime.resolveSurfaceFromPathname(String(to?.path || "/").trim() || "/");
123
+ if (surfaceRuntime.getSurfaceDefinition(surfaceId)?.requiresAuth) {
164
124
  return {
165
125
  policy: authenticatedPolicy
166
126
  };
@@ -245,31 +205,27 @@ function evaluateShellGuard({ guard, to, guardEvaluatorKey }) {
245
205
 
246
206
  function createShellBeforeEachGuard({
247
207
  surfaceRuntime,
248
- surfaceDefinitions,
249
- defaultSurfaceId,
250
208
  webRootAllowed = WEB_ROOT_ALLOW_YES,
251
209
  guardEvaluatorKey = DEFAULT_GUARD_EVALUATOR_KEY,
252
210
  authenticatedPolicy = AUTH_POLICY_AUTHENTICATED,
253
211
  publicPolicy = AUTH_POLICY_PUBLIC
254
212
  } = {}) {
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.");
213
+ if (
214
+ !surfaceRuntime ||
215
+ typeof surfaceRuntime.resolveSurfaceFromPathname !== "function" ||
216
+ typeof surfaceRuntime.getSurfaceDefinition !== "function"
217
+ ) {
218
+ throw new TypeError(
219
+ "createShellBeforeEachGuard requires surfaceRuntime.resolveSurfaceFromPathname() and getSurfaceDefinition()."
220
+ );
260
221
  }
261
222
 
223
+ const defaultSurfaceRootPath =
224
+ surfaceRuntime.getSurfaceDefinition(surfaceRuntime.DEFAULT_SURFACE_ID)?.routeBase || "/";
225
+ const redirectWebRoot = normalizeWebRootAllowed(webRootAllowed) === WEB_ROOT_ALLOW_NO && defaultSurfaceRootPath !== "/";
226
+
262
227
  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
- ) {
228
+ if (redirectWebRoot && String(to?.path || "/").trim() === "/") {
273
229
  const search = resolveSearchFromFullPath(to?.fullPath || "");
274
230
  const hash = String(to?.hash || "").trim();
275
231
  return `${defaultSurfaceRootPath}${search}${hash}`;
@@ -278,7 +234,6 @@ function createShellBeforeEachGuard({
278
234
  const guard = resolveEffectiveRouteGuard({
279
235
  to,
280
236
  surfaceRuntime,
281
- surfaceDefinitions,
282
237
  authenticatedPolicy,
283
238
  publicPolicy
284
239
  });
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import { createSurfaceRuntime } from "../shared/surface/runtime.js";
4
- import { buildSurfaceAwareRoutes } from "./shellRouting.js";
4
+ import { DEFAULT_GUARD_EVALUATOR_KEY, buildSurfaceAwareRoutes, createShellBeforeEachGuard } from "./shellRouting.js";
5
5
 
6
6
  test("buildSurfaceAwareRoutes keeps declared route paths and filters by surface mode", () => {
7
7
  const surfaceRuntime = createSurfaceRuntime({
@@ -111,3 +111,32 @@ test("buildSurfaceAwareRoutes preserves parent path when nested descendants are
111
111
  const routePaths = routes.map((route) => route.path);
112
112
  assert.equal(routePaths.includes("/account"), true);
113
113
  });
114
+
115
+ test("createShellBeforeEachGuard reads surfaces and the default root from the surface runtime", () => {
116
+ const surfaceRuntime = createSurfaceRuntime({
117
+ defaultSurfaceId: "app",
118
+ surfaces: {
119
+ home: { pagesRoot: "" },
120
+ app: { pagesRoot: "w/app", requiresAuth: true }
121
+ }
122
+ });
123
+ const evaluations = [];
124
+ const previousEvaluator = globalThis[DEFAULT_GUARD_EVALUATOR_KEY];
125
+ globalThis[DEFAULT_GUARD_EVALUATOR_KEY] = ({ guard, context }) => {
126
+ evaluations.push({ policy: guard.policy, pathname: context.location.pathname });
127
+ return { allow: false, redirectTo: "/auth/login" };
128
+ };
129
+
130
+ try {
131
+ const guard = createShellBeforeEachGuard({ surfaceRuntime, webRootAllowed: "no" });
132
+ assert.equal(guard({ path: "/", fullPath: "/?next=1#top", hash: "#top" }), "/w/app?next=1#top");
133
+ assert.equal(guard({ path: "/about", fullPath: "/about", matched: [] }), true);
134
+ assert.equal(guard({ path: "/w/app/projects", fullPath: "/w/app/projects", matched: [] }), "/auth/login");
135
+ assert.deepEqual(evaluations, [{ policy: "authenticated", pathname: "/w/app/projects" }]);
136
+
137
+ const openGuard = createShellBeforeEachGuard({ surfaceRuntime });
138
+ assert.equal(openGuard({ path: "/", fullPath: "/", matched: [] }), true);
139
+ } finally {
140
+ globalThis[DEFAULT_GUARD_EVALUATOR_KEY] = previousEvaluator;
141
+ }
142
+ });
@@ -234,11 +234,6 @@ function resolveInstalledViteProxyEntries(installedPackages = [], { proxyTarget
234
234
  return proxyEntries;
235
235
  }
236
236
 
237
- async function resolveInstalledClientPackageIds(options) {
238
- const modules = await resolveInstalledClientModules(options);
239
- return Object.freeze(modules.map((entry) => entry.packageId));
240
- }
241
-
242
237
  async function resolveLocalScopePackageIds({ appRoot }) {
243
238
  const appPackageJson = await readJsonFile(path.resolve(appRoot, "package.json"), {});
244
239
  return Object.freeze(
@@ -487,7 +482,6 @@ export {
487
482
  resolveCanonicalLocalPackageId,
488
483
  resolveLocalPackageForSpecifier,
489
484
  resolveLocalPackageSources,
490
- resolveInstalledClientPackageIds,
491
485
  resolveLocalScopePackageIds,
492
486
  resolveInstalledClientModules,
493
487
  resolveInstalledViteProxyEntries,
@@ -14,7 +14,6 @@ import {
14
14
  resolveClientOptimizeIncludeSpecifiers,
15
15
  resolveClientOptimizeExcludeSpecifiers,
16
16
  resolveLocalScopePackageIds,
17
- resolveInstalledClientPackageIds,
18
17
  resolveInstalledClientModules,
19
18
  resolveInstalledViteProxyEntries
20
19
  } from "./clientBootstrapPlugin.js";
@@ -260,7 +259,7 @@ test("resolveClientOptimizeIncludeSpecifiers omits excluded package clients", ()
260
259
  assert.deepEqual(include, ["@c/pkg/client"]);
261
260
  });
262
261
 
263
- test("resolveInstalledClientPackageIds returns only installed packages with a client export", async () => {
262
+ test("resolveInstalledClientModules returns only installed packages with a client export", async () => {
264
263
  const tempRoot = await mkdtemp(path.join(os.tmpdir(), "jskit-client-bootstrap-"));
265
264
  const appRoot = tempRoot;
266
265
 
@@ -289,7 +288,7 @@ test("resolveInstalledClientPackageIds returns only installed packages with a cl
289
288
  }
290
289
  });
291
290
 
292
- const packageIds = await resolveInstalledClientPackageIds({ appRoot });
291
+ const packageIds = (await resolveInstalledClientModules({ appRoot })).map((entry) => entry.packageId);
293
292
 
294
293
  assert.deepEqual(packageIds, ["@example/has-client"]);
295
294
  });