@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.
- package/README.md +24 -0
- package/_testable/index.js +4 -0
- package/client/appConfig.js +33 -0
- package/client/componentInteraction.js +51 -0
- package/client/componentInteraction.test.js +111 -0
- package/client/descriptorSections.js +75 -0
- package/client/index.d.ts +70 -0
- package/client/index.js +3 -0
- package/client/logging.js +38 -0
- package/client/moduleBootstrap.js +670 -0
- package/client/moduleBootstrap.test.js +403 -0
- package/client/shellBootstrap.js +233 -0
- package/client/shellBootstrap.test.js +185 -0
- package/client/shellRouting.js +321 -0
- package/client/shellRouting.test.js +113 -0
- package/client/vite/clientBootstrapPlugin.js +259 -0
- package/client/vite/clientBootstrapPlugin.test.js +563 -0
- package/client/vite/index.js +3 -0
- package/internal/node/fileSystem.js +21 -0
- package/internal/node/installedPackageDescriptor.js +104 -0
- package/package.json +43 -0
- package/server/actions/ActionRuntimeServiceProvider.js +309 -0
- package/server/actions/ActionRuntimeServiceProvider.test.js +551 -0
- package/server/actions/index.js +8 -0
- package/server/container/ContainerCoreServiceProvider.js +27 -0
- package/server/container/index.js +10 -0
- package/server/exportPolicy.test.js +68 -0
- package/server/http/HttpFastifyServiceProvider.js +25 -0
- package/server/http/_testable/index.js +2 -0
- package/server/http/index.js +1 -0
- package/server/http/lib/controller.js +183 -0
- package/server/http/lib/controller.test.js +143 -0
- package/server/http/lib/errors.js +12 -0
- package/server/http/lib/httpRuntime.js +82 -0
- package/server/http/lib/index.js +18 -0
- package/server/http/lib/kernel.js +15 -0
- package/server/http/lib/kernel.test.js +880 -0
- package/server/http/lib/middlewareRuntime.js +149 -0
- package/server/http/lib/requestActionExecutor.js +258 -0
- package/server/http/lib/requestScope.js +59 -0
- package/server/http/lib/routeRegistration.js +165 -0
- package/server/http/lib/routeSupport.js +45 -0
- package/server/http/lib/routeValidator.js +469 -0
- package/server/http/lib/routeValidator.test.js +474 -0
- package/server/http/lib/router.js +206 -0
- package/server/kernel/KernelCoreServiceProvider.js +27 -0
- package/server/kernel/index.js +10 -0
- package/server/platform/PlatformServerRuntimeServiceProvider.js +30 -0
- package/server/platform/index.js +5 -0
- package/server/platform/providerRuntime/descriptorCatalog.js +170 -0
- package/server/platform/providerRuntime/helpers.js +45 -0
- package/server/platform/providerRuntime/lockfile.js +27 -0
- package/server/platform/providerRuntime/providerLoader.js +283 -0
- package/server/platform/providerRuntime.js +142 -0
- package/server/platform/providerRuntime.test.js +217 -0
- package/server/platform/runtime.js +40 -0
- package/server/platform/surfaceRuntime.js +150 -0
- package/server/platform/surfaceRuntime.test.js +136 -0
- package/server/registries/actionSurfaceSourceRegistry.js +150 -0
- package/server/registries/bootstrapPayloadContributorRegistry.js +41 -0
- package/server/registries/domainEventListenerRegistry.js +61 -0
- package/server/registries/index.js +36 -0
- package/server/registries/primitives.js +63 -0
- package/server/registries/routeVisibilityResolverRegistry.js +87 -0
- package/server/registries/serviceRegistrationRegistry.js +431 -0
- package/server/runtime/ServerRuntimeCoreServiceProvider.js +65 -0
- package/server/runtime/ServerRuntimeCoreServiceProvider.test.js +53 -0
- package/server/runtime/apiRoutePolicyParity.test.js +109 -0
- package/server/runtime/apiRouteRegistration.js +65 -0
- package/server/runtime/bootBootstrapRoutes.js +46 -0
- package/server/runtime/bootBootstrapRoutes.test.js +79 -0
- package/server/runtime/bootstrapContributors.test.js +114 -0
- package/server/runtime/canonicalJson.js +74 -0
- package/server/runtime/composition.js +142 -0
- package/server/runtime/domainEvents.test.js +114 -0
- package/server/runtime/domainRules.js +50 -0
- package/server/runtime/domainRules.test.js +87 -0
- package/server/runtime/entityChangeEvents.js +182 -0
- package/server/runtime/entityChangeEvents.test.js +211 -0
- package/server/runtime/errors.js +68 -0
- package/server/runtime/errors.test.js +73 -0
- package/server/runtime/fastifyBootstrap.js +372 -0
- package/server/runtime/fastifyBootstrap.test.js +194 -0
- package/server/runtime/index.js +6 -0
- package/server/runtime/integers.js +13 -0
- package/server/runtime/moduleConfig.js +269 -0
- package/server/runtime/moduleConfig.test.js +141 -0
- package/server/runtime/pagination.js +13 -0
- package/server/runtime/realtimeNormalization.js +21 -0
- package/server/runtime/requestUrl.js +38 -0
- package/server/runtime/routeUtils.js +20 -0
- package/server/runtime/runtimeAssembly.js +113 -0
- package/server/runtime/runtimeKernel.js +55 -0
- package/server/runtime/securityAudit.js +269 -0
- package/server/runtime/securityAudit.test.js +41 -0
- package/server/runtime/serviceAuthorization.js +113 -0
- package/server/runtime/serviceAuthorization.test.js +100 -0
- package/server/runtime/serviceRegistration.test.js +197 -0
- package/server/support/SupportCoreServiceProvider.js +25 -0
- package/server/support/appConfig.js +37 -0
- package/server/support/appConfig.test.js +94 -0
- package/server/support/defaultMissingHandler.js +7 -0
- package/server/support/index.js +2 -0
- package/server/support/routePolicyConfig.js +51 -0
- package/server/support/symlinkSafeRequire.js +78 -0
- package/server/support/symlinkSafeRequire.test.js +27 -0
- package/server/surface/SurfaceRoutingServiceProvider.js +27 -0
- package/server/surface/index.js +19 -0
- package/shared/actions/actionContributorHelpers.js +34 -0
- package/shared/actions/actionContributorHelpers.test.js +16 -0
- package/shared/actions/actionDefinitions.js +488 -0
- package/shared/actions/actionDefinitions.test.js +212 -0
- package/shared/actions/audit.js +7 -0
- package/shared/actions/executionContext.js +97 -0
- package/shared/actions/executionContext.test.js +66 -0
- package/shared/actions/idempotency.js +62 -0
- package/shared/actions/index.js +2 -0
- package/shared/actions/observability.js +10 -0
- package/shared/actions/pipeline.js +287 -0
- package/shared/actions/policies.js +342 -0
- package/shared/actions/policies.test.js +233 -0
- package/shared/actions/registry.js +187 -0
- package/shared/actions/registry.test.js +381 -0
- package/shared/actions/requestMeta.js +36 -0
- package/shared/actions/textNormalization.js +3 -0
- package/shared/actions/withActionDefaults.js +34 -0
- package/shared/index.js +2 -0
- package/shared/runtime/application.js +323 -0
- package/shared/runtime/container.js +261 -0
- package/shared/runtime/containerErrors.js +22 -0
- package/shared/runtime/index.js +18 -0
- package/shared/runtime/kernelErrors.js +20 -0
- package/shared/runtime/serviceProvider.js +13 -0
- package/shared/support/formatDateTime.js +10 -0
- package/shared/support/formatDateTime.test.js +15 -0
- package/shared/support/index.js +14 -0
- package/shared/support/linkPath.js +67 -0
- package/shared/support/linkPath.test.js +35 -0
- package/shared/support/normalize.js +116 -0
- package/shared/support/normalize.test.js +48 -0
- package/shared/support/packageDescriptor.test.js +121 -0
- package/shared/support/permissions.js +50 -0
- package/shared/support/pickOwnProperties.js +17 -0
- package/shared/support/pickOwnProperties.test.js +25 -0
- package/shared/support/policies.js +11 -0
- package/shared/support/queryPath.js +33 -0
- package/shared/support/queryPath.test.js +19 -0
- package/shared/support/queryResilience.js +34 -0
- package/shared/support/queryResilience.test.js +33 -0
- package/shared/support/returnToPath.js +153 -0
- package/shared/support/returnToPath.test.js +123 -0
- package/shared/support/sorting.js +15 -0
- package/shared/support/tokens.js +23 -0
- package/shared/support/tokens.test.js +17 -0
- package/shared/support/visibility.js +56 -0
- package/shared/support/visibility.test.js +45 -0
- package/shared/surface/apiPaths.js +84 -0
- package/shared/surface/escapeRegExp.js +5 -0
- package/shared/surface/index.js +6 -0
- package/shared/surface/paths.js +273 -0
- package/shared/surface/registry.js +135 -0
- package/shared/surface/registry.test.js +44 -0
- package/shared/surface/runtime.js +357 -0
- package/shared/surface/runtime.test.js +319 -0
- package/shared/validators/createCursorListValidator.js +42 -0
- package/shared/validators/createCursorListValidator.test.js +34 -0
- package/shared/validators/cursorPaginationQueryValidator.js +31 -0
- package/shared/validators/cursorPaginationQueryValidator.test.js +21 -0
- package/shared/validators/index.js +12 -0
- package/shared/validators/inputNormalization.js +13 -0
- package/shared/validators/mergeObjectSchemas.js +31 -0
- package/shared/validators/mergeObjectSchemas.test.js +67 -0
- package/shared/validators/mergeValidators.js +89 -0
- package/shared/validators/mergeValidators.test.js +116 -0
- package/shared/validators/nestValidator.js +53 -0
- package/shared/validators/nestValidator.test.js +60 -0
- package/shared/validators/recordIdParamsValidator.js +36 -0
- package/shared/validators/recordIdParamsValidator.test.js +20 -0
- package/shared/validators/resourceRequiredMetadata.js +41 -0
- package/shared/validators/resourceRequiredMetadata.test.js +49 -0
- package/test/barrelExposure.test.js +106 -0
- package/test/dynamicImportPolicy.test.js +89 -0
- package/test/exportsContract.test.js +168 -0
- package/test/routeInputContractGuard.test.js +78 -0
- 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
|
+
});
|