@jskit-ai/kernel 0.1.244 → 0.1.246

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 (101) hide show
  1. package/client/appConfig.js +2 -1
  2. package/client/asyncModuleRecovery.js +2 -4
  3. package/client/index.d.ts +18 -76
  4. package/client/index.js +7 -2
  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 +38 -10
  9. package/client/shellBootstrap.test.js +85 -6
  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 +14 -58
  15. package/package.json +2 -4
  16. package/server/actions/actionCatalogue.js +13 -34
  17. package/{shared → server}/actions/actionContributorHelpers.js +0 -5
  18. package/{shared → server}/actions/actionDefinitions.js +24 -131
  19. package/{shared → server}/actions/actionDefinitions.test.js +75 -21
  20. package/server/actions/actionEvents.js +97 -2
  21. package/server/actions/actionEvents.test.js +79 -0
  22. package/{shared → server}/actions/executionContext.js +3 -13
  23. package/server/actions/idempotency.js +24 -0
  24. package/server/actions/pipeline.js +87 -0
  25. package/{shared → server}/actions/policies.js +20 -33
  26. package/{shared → server}/actions/policies.test.js +6 -6
  27. package/{shared → server}/actions/registry.js +3 -22
  28. package/{shared → server}/actions/registry.test.js +70 -32
  29. package/server/actions/requestMeta.js +17 -0
  30. package/server/http/capabilityHttpRuntime.js +1 -1
  31. package/server/http/capabilityHttpRuntime.test.js +56 -0
  32. package/server/http/errorHandling.js +291 -0
  33. package/server/{runtime/fastifyBootstrap.test.js → http/errorHandling.test.js} +72 -120
  34. package/server/http/lib/routeRegistration.js +36 -150
  35. package/server/http/lib/routeTransport.js +2 -0
  36. package/server/http/lib/routeValidator.js +5 -47
  37. package/server/http/lib/routeValidator.test.js +15 -17
  38. package/server/http/lib/router.js +10 -94
  39. package/server/platform/providerRuntime/capabilityProviderLoader.js +2 -1
  40. package/server/platform/providerRuntime/helpers.js +1 -34
  41. package/server/runtime/integers.js +1 -5
  42. package/server/runtime/serviceAuthorization.js +4 -9
  43. package/server/support/appConfig.js +2 -1
  44. package/server/support/appConfigFiles.js +1 -38
  45. package/server/support/appConfigFiles.test.js +3 -39
  46. package/server/support/index.js +1 -20
  47. package/shared/capabilities/defineProvider.js +1 -1
  48. package/shared/capabilities/index.js +1 -1
  49. package/shared/index.js +1 -1
  50. package/shared/support/index.js +1 -4
  51. package/shared/support/mobileConfig.js +99 -0
  52. package/shared/support/mobileConfig.test.js +113 -0
  53. package/shared/support/normalize.js +0 -108
  54. package/shared/support/normalize.test.js +0 -134
  55. package/shared/{actions/actionContributorHelpers.test.js → support/permissions.test.js} +1 -1
  56. package/shared/support/returnToPath.js +16 -20
  57. package/shared/support/returnToPath.test.js +5 -33
  58. package/shared/support/shellLayoutTargets.js +2 -140
  59. package/shared/support/shellLayoutTargets.test.js +1 -110
  60. package/shared/support/sorting.js +1 -9
  61. package/shared/surface/apiPaths.js +1 -82
  62. package/shared/surface/paths.js +38 -171
  63. package/shared/surface/registry.js +2 -1
  64. package/shared/surface/runtime.js +18 -109
  65. package/shared/surface/runtime.test.js +57 -0
  66. package/shared/validators/index.js +1 -4
  67. package/shared/validators/jsonRestSchemaSupport.js +0 -9
  68. package/shared/validators/recordIdParamsValidator.js +0 -6
  69. package/shared/validators/schemaDefinitions.js +4 -30
  70. package/shared/validators/schemaPayloadValidation.js +2 -6
  71. package/test/barrelExposure.test.js +5 -8
  72. package/test/exportsContract.test.js +1 -3
  73. package/client/mobileLaunchRouting.js +0 -231
  74. package/client/mobileLaunchRouting.test.js +0 -230
  75. package/server/http/lib/controller.js +0 -183
  76. package/server/http/lib/controller.test.js +0 -143
  77. package/server/http/lib/index.js +0 -5
  78. package/server/runtime/canonicalJson.js +0 -74
  79. package/server/runtime/entityChangeEvents.js +0 -307
  80. package/server/runtime/entityChangeEvents.test.js +0 -274
  81. package/server/runtime/fastifyBootstrap.js +0 -553
  82. package/server/runtime/moduleConfig.js +0 -223
  83. package/server/runtime/moduleConfig.test.js +0 -165
  84. package/server/runtime/pagination.js +0 -13
  85. package/server/runtime/realtimeNormalization.js +0 -21
  86. package/server/runtime/routeUtils.js +0 -20
  87. package/server/runtime/securityAudit.js +0 -269
  88. package/server/runtime/securityAudit.test.js +0 -41
  89. package/server/support/pageTargets.js +0 -772
  90. package/server/support/pageTargets.test.js +0 -659
  91. package/server/support/shellOutlets.js +0 -576
  92. package/server/support/shellOutlets.test.js +0 -336
  93. package/server/surface/index.js +0 -18
  94. package/shared/actions/audit.js +0 -7
  95. package/shared/actions/idempotency.js +0 -62
  96. package/shared/actions/observability.js +0 -10
  97. package/shared/actions/pipeline.js +0 -309
  98. package/shared/actions/requestMeta.js +0 -36
  99. package/shared/actions/textNormalization.js +0 -3
  100. package/shared/actions/withActionDefaults.js +0 -34
  101. /package/{shared → server}/actions/executionContext.test.js +0 -0
@@ -0,0 +1,113 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { normalizeMobileCallbackPath, normalizeMobileConfig } from "./mobileConfig.js";
4
+
5
+ test("normalizeMobileCallbackPath normalizes auth callback paths", () => {
6
+ assert.equal(normalizeMobileCallbackPath("auth/login"), "/auth/login");
7
+ assert.equal(normalizeMobileCallbackPath("/auth/login"), "/auth/login");
8
+ assert.equal(
9
+ normalizeMobileCallbackPath("", {
10
+ fallback: "/login/callback"
11
+ }),
12
+ "/login/callback"
13
+ );
14
+ });
15
+
16
+ test("normalizeMobileConfig normalizes the mobile config contract", () => {
17
+ const mobileConfig = normalizeMobileConfig({
18
+ enabled: true,
19
+ strategy: " Capacitor ",
20
+ appId: "com.example.convict",
21
+ appName: "Convict",
22
+ assetMode: " dev_server ",
23
+ devServerUrl: " http://192.168.1.10:5173 ",
24
+ apiBaseUrl: " https://api.example.com ",
25
+ auth: {
26
+ callbackPath: "auth/login",
27
+ customScheme: " Convict ",
28
+ appLinkDomains: ["App.Example.com", " app.example.com "]
29
+ },
30
+ android: {
31
+ packageName: "com.example.convict",
32
+ minSdk: "28",
33
+ targetSdk: "35",
34
+ versionCode: "4",
35
+ versionName: "2.0.0"
36
+ }
37
+ });
38
+
39
+ assert.deepEqual(mobileConfig, {
40
+ enabled: true,
41
+ strategy: "capacitor",
42
+ appId: "com.example.convict",
43
+ appName: "Convict",
44
+ assetMode: "dev_server",
45
+ devServerUrl: "http://192.168.1.10:5173",
46
+ apiBaseUrl: "https://api.example.com",
47
+ auth: {
48
+ callbackPath: "/auth/login",
49
+ customScheme: "convict",
50
+ appLinkDomains: ["app.example.com"]
51
+ },
52
+ android: {
53
+ packageName: "com.example.convict",
54
+ minSdk: 28,
55
+ targetSdk: 35,
56
+ versionCode: 4,
57
+ versionName: "2.0.0"
58
+ }
59
+ });
60
+ });
61
+
62
+ test("normalizeMobileConfig applies defaults for missing fields", () => {
63
+ const mobileConfig = normalizeMobileConfig({
64
+ auth: {
65
+ appLinkDomains: "app.example.com"
66
+ },
67
+ android: {
68
+ minSdk: "bad",
69
+ targetSdk: 0,
70
+ versionCode: "",
71
+ versionName: ""
72
+ }
73
+ });
74
+
75
+ assert.deepEqual(mobileConfig, {
76
+ enabled: false,
77
+ strategy: "",
78
+ appId: "",
79
+ appName: "",
80
+ assetMode: "bundled",
81
+ devServerUrl: "",
82
+ apiBaseUrl: "",
83
+ auth: {
84
+ callbackPath: "/auth/login",
85
+ customScheme: "",
86
+ appLinkDomains: ["app.example.com"]
87
+ },
88
+ android: {
89
+ packageName: "",
90
+ minSdk: 26,
91
+ targetSdk: 35,
92
+ versionCode: 1,
93
+ versionName: "1.0.0"
94
+ }
95
+ });
96
+ });
97
+
98
+ test("normalizeMobileConfig preserves explicit false values from string input", () => {
99
+ const mobileConfig = normalizeMobileConfig({
100
+ enabled: "false"
101
+ });
102
+
103
+ assert.equal(mobileConfig.enabled, false);
104
+ });
105
+
106
+ test("normalizeMobileConfig rejects invalid assetMode values", () => {
107
+ assert.throws(
108
+ () => normalizeMobileConfig({
109
+ assetMode: "weird"
110
+ }),
111
+ /config\.mobile\.assetMode must be "bundled" or "dev_server"\./u
112
+ );
113
+ });
@@ -134,20 +134,6 @@ function normalizeIfInSource(source, normalized, fieldName, normalizer = (value)
134
134
  return normalized;
135
135
  }
136
136
 
137
- function normalizeIfPresent(value, normalizer = (entry) => entry) {
138
- if (typeof normalizer !== "function") {
139
- throw new TypeError("normalizeIfPresent requires normalizer function.");
140
- }
141
- return value == null ? value : normalizer(value);
142
- }
143
-
144
- function normalizeOrNull(value, normalizer = (entry) => entry) {
145
- if (typeof normalizer !== "function") {
146
- throw new TypeError("normalizeOrNull requires normalizer function.");
147
- }
148
- return value == null ? null : normalizer(value);
149
- }
150
-
151
137
  function isRecord(value) {
152
138
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
153
139
  }
@@ -260,94 +246,6 @@ function normalizeOneOf(value, allowedValues = [], fallback = "") {
260
246
  return supported[0] || "";
261
247
  }
262
248
 
263
- const MOBILE_ASSET_MODES = Object.freeze(["bundled", "dev_server"]);
264
- const MOBILE_STRATEGIES = Object.freeze(["capacitor"]);
265
- const DEFAULT_MOBILE_AUTH_CONFIG = Object.freeze({
266
- callbackPath: "/auth/login",
267
- customScheme: "",
268
- appLinkDomains: Object.freeze([])
269
- });
270
- const DEFAULT_MOBILE_ANDROID_CONFIG = Object.freeze({
271
- packageName: "",
272
- minSdk: 26,
273
- targetSdk: 35,
274
- versionCode: 1,
275
- versionName: "1.0.0"
276
- });
277
-
278
- function normalizeMobileStrategy(value = "", { fallback = "" } = {}) {
279
- const normalized = normalizeLowerText(value);
280
- return MOBILE_STRATEGIES.includes(normalized) ? normalized : normalizeLowerText(fallback);
281
- }
282
-
283
- function normalizeMobileAssetMode(value = "", { fallback = "bundled" } = {}) {
284
- const normalized = normalizeLowerText(value);
285
- if (!normalized) {
286
- return normalizeLowerText(fallback, {
287
- fallback: "bundled"
288
- });
289
- }
290
- if (!MOBILE_ASSET_MODES.includes(normalized)) {
291
- throw new TypeError('config.mobile.assetMode must be "bundled" or "dev_server".');
292
- }
293
- return normalized;
294
- }
295
-
296
- function normalizeMobileCallbackPath(value = "", { fallback = "/auth/login" } = {}) {
297
- const normalized = normalizeText(value, {
298
- fallback
299
- });
300
- if (!normalized) {
301
- return "";
302
- }
303
-
304
- return normalized.startsWith("/") ? normalized : `/${normalized}`;
305
- }
306
-
307
- function normalizeMobileConfig(source = {}) {
308
- const mobile = normalizeObject(source);
309
- const auth = normalizeObject(mobile.auth);
310
- const android = normalizeObject(mobile.android);
311
-
312
- return Object.freeze({
313
- enabled: hasValue(mobile.enabled) ? normalizeBoolean(mobile.enabled) : false,
314
- strategy: normalizeMobileStrategy(mobile.strategy),
315
- appId: normalizeText(mobile.appId),
316
- appName: normalizeText(mobile.appName),
317
- assetMode: normalizeMobileAssetMode(mobile.assetMode),
318
- devServerUrl: normalizeText(mobile.devServerUrl),
319
- apiBaseUrl: normalizeText(mobile.apiBaseUrl),
320
- auth: Object.freeze({
321
- callbackPath: normalizeMobileCallbackPath(auth.callbackPath, {
322
- fallback: DEFAULT_MOBILE_AUTH_CONFIG.callbackPath
323
- }),
324
- customScheme: normalizeLowerText(auth.customScheme),
325
- appLinkDomains: Object.freeze([
326
- ...new Set(
327
- normalizeUniqueTextList(auth.appLinkDomains, {
328
- acceptSingle: true
329
- }).map((entry) => normalizeLowerText(entry))
330
- )
331
- ])
332
- }),
333
- android: Object.freeze({
334
- packageName: normalizeText(android.packageName),
335
- minSdk: normalizePositiveInteger(android.minSdk, {
336
- fallback: DEFAULT_MOBILE_ANDROID_CONFIG.minSdk
337
- }),
338
- targetSdk: normalizePositiveInteger(android.targetSdk, {
339
- fallback: DEFAULT_MOBILE_ANDROID_CONFIG.targetSdk
340
- }),
341
- versionCode: normalizePositiveInteger(android.versionCode, {
342
- fallback: DEFAULT_MOBILE_ANDROID_CONFIG.versionCode
343
- }),
344
- versionName: normalizeText(android.versionName, {
345
- fallback: DEFAULT_MOBILE_ANDROID_CONFIG.versionName
346
- })
347
- })
348
- });
349
- }
350
-
351
249
  function ensureNonEmptyText(value, label = "value") {
352
250
  const normalized = normalizeText(value);
353
251
  if (!normalized) {
@@ -368,8 +266,6 @@ export {
368
266
  normalizeQueryToken,
369
267
  normalizeObject,
370
268
  normalizeIfInSource,
371
- normalizeIfPresent,
372
- normalizeOrNull,
373
269
  isRecord,
374
270
  normalizeArray,
375
271
  normalizeUniqueTextList,
@@ -379,9 +275,5 @@ export {
379
275
  normalizeRecordId,
380
276
  normalizeOpaqueId,
381
277
  normalizeOneOf,
382
- normalizeMobileAssetMode,
383
- normalizeMobileCallbackPath,
384
- normalizeMobileConfig,
385
- normalizeMobileStrategy,
386
278
  ensureNonEmptyText
387
279
  };
@@ -7,10 +7,6 @@ import {
7
7
  normalizeFiniteInteger,
8
8
  normalizeFiniteNumber,
9
9
  normalizeIfInSource,
10
- normalizeIfPresent,
11
- normalizeMobileCallbackPath,
12
- normalizeMobileConfig,
13
- normalizeOrNull,
14
10
  normalizeRecordId,
15
11
  normalizeOpaqueId,
16
12
  normalizePositiveInteger,
@@ -166,26 +162,6 @@ test("normalizeIfInSource validates target and function arguments", () => {
166
162
  );
167
163
  });
168
164
 
169
- test("normalizeIfPresent normalizes non-nullish values and preserves nullish values", () => {
170
- assert.equal(normalizeIfPresent(" Ada ", normalizeText), "Ada");
171
- assert.equal(normalizeIfPresent(null, normalizeText), null);
172
- assert.equal(normalizeIfPresent(undefined, normalizeText), undefined);
173
- assert.throws(
174
- () => normalizeIfPresent("Ada", null),
175
- /requires normalizer function/
176
- );
177
- });
178
-
179
- test("normalizeOrNull normalizes non-nullish values and coerces nullish to null", () => {
180
- assert.equal(normalizeOrNull(" Ada ", normalizeText), "Ada");
181
- assert.equal(normalizeOrNull(null, normalizeText), null);
182
- assert.equal(normalizeOrNull(undefined, normalizeText), null);
183
- assert.throws(
184
- () => normalizeOrNull("Ada", null),
185
- /requires normalizer function/
186
- );
187
- });
188
-
189
165
  test("normalizeRecordId accepts canonical string, safe integer, and bigint identifiers", () => {
190
166
  const unsafeNumericId = Number(9007199254740993n);
191
167
  assert.equal(normalizeRecordId(" 7 "), "7");
@@ -224,113 +200,3 @@ test("normalizeUniqueTextList trims, dedupes, and supports optional single value
224
200
  ["one"]
225
201
  );
226
202
  });
227
-
228
- test("normalizeMobileCallbackPath normalizes auth callback paths", () => {
229
- assert.equal(normalizeMobileCallbackPath("auth/login"), "/auth/login");
230
- assert.equal(normalizeMobileCallbackPath("/auth/login"), "/auth/login");
231
- assert.equal(
232
- normalizeMobileCallbackPath("", {
233
- fallback: "/login/callback"
234
- }),
235
- "/login/callback"
236
- );
237
- });
238
-
239
- test("normalizeMobileConfig normalizes the mobile config contract", () => {
240
- const mobileConfig = normalizeMobileConfig({
241
- enabled: true,
242
- strategy: " Capacitor ",
243
- appId: "com.example.convict",
244
- appName: "Convict",
245
- assetMode: " dev_server ",
246
- devServerUrl: " http://192.168.1.10:5173 ",
247
- apiBaseUrl: " https://api.example.com ",
248
- auth: {
249
- callbackPath: "auth/login",
250
- customScheme: " Convict ",
251
- appLinkDomains: ["App.Example.com", " app.example.com "]
252
- },
253
- android: {
254
- packageName: "com.example.convict",
255
- minSdk: "28",
256
- targetSdk: "35",
257
- versionCode: "4",
258
- versionName: "2.0.0"
259
- }
260
- });
261
-
262
- assert.deepEqual(mobileConfig, {
263
- enabled: true,
264
- strategy: "capacitor",
265
- appId: "com.example.convict",
266
- appName: "Convict",
267
- assetMode: "dev_server",
268
- devServerUrl: "http://192.168.1.10:5173",
269
- apiBaseUrl: "https://api.example.com",
270
- auth: {
271
- callbackPath: "/auth/login",
272
- customScheme: "convict",
273
- appLinkDomains: ["app.example.com"]
274
- },
275
- android: {
276
- packageName: "com.example.convict",
277
- minSdk: 28,
278
- targetSdk: 35,
279
- versionCode: 4,
280
- versionName: "2.0.0"
281
- }
282
- });
283
- });
284
-
285
- test("normalizeMobileConfig applies defaults for missing fields", () => {
286
- const mobileConfig = normalizeMobileConfig({
287
- auth: {
288
- appLinkDomains: "app.example.com"
289
- },
290
- android: {
291
- minSdk: "bad",
292
- targetSdk: 0,
293
- versionCode: "",
294
- versionName: ""
295
- }
296
- });
297
-
298
- assert.deepEqual(mobileConfig, {
299
- enabled: false,
300
- strategy: "",
301
- appId: "",
302
- appName: "",
303
- assetMode: "bundled",
304
- devServerUrl: "",
305
- apiBaseUrl: "",
306
- auth: {
307
- callbackPath: "/auth/login",
308
- customScheme: "",
309
- appLinkDomains: ["app.example.com"]
310
- },
311
- android: {
312
- packageName: "",
313
- minSdk: 26,
314
- targetSdk: 35,
315
- versionCode: 1,
316
- versionName: "1.0.0"
317
- }
318
- });
319
- });
320
-
321
- test("normalizeMobileConfig preserves explicit false values from string input", () => {
322
- const mobileConfig = normalizeMobileConfig({
323
- enabled: "false"
324
- });
325
-
326
- assert.equal(mobileConfig.enabled, false);
327
- });
328
-
329
- test("normalizeMobileConfig rejects invalid assetMode values", () => {
330
- assert.throws(
331
- () => normalizeMobileConfig({
332
- assetMode: "weird"
333
- }),
334
- /config\.mobile\.assetMode must be "bundled" or "dev_server"\./u
335
- );
336
- });
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
 
4
- import { hasPermission } from "./actionContributorHelpers.js";
4
+ import { hasPermission } from "./permissions.js";
5
5
 
6
6
  test("hasPermission allows wildcard and direct matches", () => {
7
7
  assert.equal(hasPermission(["*"], "workspace.billing.manage"), true);
@@ -1,5 +1,17 @@
1
1
  import { normalizePathname } from "../surface/paths.js";
2
2
 
3
+ const SAME_ORIGIN_PROBE = "http://return-to.invalid";
4
+
5
+ // Browsers read "/\\host" and "/<tab>/host" as another site, so a path counts
6
+ // only if it resolves on the page's own origin.
7
+ function staysOnSameOrigin(path) {
8
+ try {
9
+ return new URL(path, SAME_ORIGIN_PROBE).origin === SAME_ORIGIN_PROBE;
10
+ } catch {
11
+ return false;
12
+ }
13
+ }
14
+
3
15
  function normalizeHttpOrigin(value = "") {
4
16
  const rawValue = String(value || "").trim();
5
17
  if (!rawValue) {
@@ -77,24 +89,6 @@ function normalizeBlockedPathnames(blockedPathnames = []) {
77
89
  return normalizedPathnames;
78
90
  }
79
91
 
80
- function resolveAllowedOriginsFromPlacementContext(contextValue = null, { includeWindowOrigin = true } = {}) {
81
- const seedOrigins = [];
82
-
83
- if (includeWindowOrigin && typeof window === "object" && window?.location?.origin) {
84
- appendNormalizedOrigin(seedOrigins, window.location.origin);
85
- }
86
-
87
- const surfaceConfig =
88
- contextValue && typeof contextValue === "object" && contextValue.surfaceConfig && typeof contextValue.surfaceConfig === "object"
89
- ? contextValue.surfaceConfig
90
- : {};
91
- const surfacesById =
92
- surfaceConfig.surfacesById && typeof surfaceConfig.surfacesById === "object" ? surfaceConfig.surfacesById : {};
93
- return resolveAllowedOriginsFromSurfaceDefinitions(surfacesById, {
94
- seedOrigins
95
- });
96
- }
97
-
98
92
  function normalizeReturnToPath(
99
93
  value,
100
94
  {
@@ -113,6 +107,9 @@ function normalizeReturnToPath(
113
107
  const normalizedBlockedPathnames = normalizeBlockedPathnames(blockedPathnames);
114
108
 
115
109
  if (rawValue.startsWith("/") && !rawValue.startsWith("//")) {
110
+ if (!staysOnSameOrigin(rawValue)) {
111
+ return fallback;
112
+ }
116
113
  const normalizedPathname = normalizePathname(rawValue);
117
114
  if (normalizedBlockedPathnames.includes(normalizedPathname)) {
118
115
  return fallback;
@@ -148,6 +145,5 @@ export {
148
145
  normalizeAllowedOrigins,
149
146
  normalizeHttpOrigin,
150
147
  normalizeReturnToPath,
151
- resolveAllowedOriginsFromSurfaceDefinitions,
152
- resolveAllowedOriginsFromPlacementContext
148
+ resolveAllowedOriginsFromSurfaceDefinitions
153
149
  };
@@ -5,8 +5,7 @@ import {
5
5
  normalizeAllowedOrigins,
6
6
  normalizeHttpOrigin,
7
7
  normalizeReturnToPath,
8
- resolveAllowedOriginsFromSurfaceDefinitions,
9
- resolveAllowedOriginsFromPlacementContext
8
+ resolveAllowedOriginsFromSurfaceDefinitions
10
9
  } from "./returnToPath.js";
11
10
 
12
11
  test("normalizeHttpOrigin keeps valid http origins only", () => {
@@ -88,36 +87,9 @@ test("resolveAllowedOriginsFromSurfaceDefinitions includes seed origins and surf
88
87
  ]);
89
88
  });
90
89
 
91
- test("resolveAllowedOriginsFromPlacementContext collects current and surface origins", () => {
92
- const originalWindow = globalThis.window;
93
- globalThis.window = {
94
- location: {
95
- origin: "https://current.example.com"
96
- }
97
- };
98
-
99
- try {
100
- const origins = resolveAllowedOriginsFromPlacementContext({
101
- surfaceConfig: {
102
- surfacesById: {
103
- home: { origin: "https://home.example.com" },
104
- app: { origin: "https://app.example.com" },
105
- duplicate: { origin: "https://home.example.com" },
106
- invalid: { origin: "mailto:hello@example.com" }
107
- }
108
- }
109
- });
110
-
111
- assert.deepEqual(origins, [
112
- "https://current.example.com",
113
- "https://home.example.com",
114
- "https://app.example.com"
115
- ]);
116
- } finally {
117
- if (typeof originalWindow === "undefined") {
118
- delete globalThis.window;
119
- } else {
120
- globalThis.window = originalWindow;
121
- }
90
+ test("normalizeReturnToPath refuses paths a browser reads as another site", () => {
91
+ for (const value of ["/\\evil.example", "/\\/evil.example/x", "/\t/evil.example", "/\n/evil.example", "\\\\evil.example"]) {
92
+ assert.equal(normalizeReturnToPath(value, { fallback: "/home" }), "/home", JSON.stringify(value));
122
93
  }
94
+ assert.equal(normalizeReturnToPath("/a/b?c=1#d", { fallback: "/home" }), "/a/b?c=1#d");
123
95
  });
@@ -3,28 +3,9 @@ import {
3
3
  normalizeText
4
4
  } from "./normalize.js";
5
5
 
6
- const SHELL_OUTLET_TAG_PATTERN = /<ShellOutlet\b([^>]*)\/?>/g;
7
- const ATTRIBUTE_PATTERN = /([:@]?[A-Za-z_][A-Za-z0-9_-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'))?/g;
8
6
  const PLACEMENT_LAYOUT_CLASSES = Object.freeze(["compact", "medium", "expanded"]);
9
7
  const WEB_PLACEMENT_SURFACE_ANY = "*";
10
8
 
11
- function parseTagAttributes(attributesSource = "") {
12
- const attributes = {};
13
- const source = String(attributesSource || "");
14
- for (const match of source.matchAll(ATTRIBUTE_PATTERN)) {
15
- const attributeName = normalizeText(match[1]);
16
- if (!attributeName) {
17
- continue;
18
- }
19
-
20
- const hasValue = match[2] != null || match[3] != null;
21
- const attributeValue = hasValue ? String(match[2] ?? match[3] ?? "") : true;
22
- attributes[attributeName] = attributeValue;
23
- }
24
-
25
- return attributes;
26
- }
27
-
28
9
  function normalizeShellOutletTargetId(value = "") {
29
10
  const normalizedValue = normalizeText(value);
30
11
  if (!normalizedValue) {
@@ -250,126 +231,9 @@ function normalizePlacementTopologyDefinition(
250
231
  });
251
232
  }
252
233
 
253
- function resolveShellOutletTargetParts(
254
- {
255
- target = ""
256
- } = {}
257
- ) {
258
- const normalizedTargetId = normalizeShellOutletTargetId(target);
259
- if (!normalizedTargetId) {
260
- return null;
261
- }
262
-
263
- return Object.freeze({
264
- id: normalizedTargetId
265
- });
266
- }
267
-
268
- function findShellOutletTargetById(targets = [], targetId = "") {
269
- const entries = Array.isArray(targets) ? targets : [];
270
- const normalizedTargetId = normalizeShellOutletTargetId(targetId);
271
- if (!normalizedTargetId) {
272
- return null;
273
- }
274
-
275
- return entries.find((entry) => normalizeShellOutletTargetId(entry?.id) === normalizedTargetId) || null;
276
- }
277
-
278
- function describeShellOutletTargets(targets = []) {
279
- return (Array.isArray(targets) ? targets : [])
280
- .map((entry) => normalizeShellOutletTargetId(entry?.id))
281
- .filter(Boolean)
282
- .join(", ");
283
- }
284
-
285
- function isDefaultAttributeEnabled(value) {
286
- if (value === true) {
287
- return true;
288
- }
289
-
290
- const normalized = normalizeText(value).toLowerCase();
291
- if (!normalized) {
292
- return true;
293
- }
294
-
295
- return normalized !== "false" && normalized !== "0" && normalized !== "no" && normalized !== "off";
296
- }
297
-
298
- function normalizeShellOutletTargetRecord(
299
- value = {},
300
- {
301
- context = "shell layout"
302
- } = {}
303
- ) {
304
- const record = normalizeObject(value);
305
- const resolvedContext = normalizeText(context) || "shell layout";
306
- const targetParts = resolveShellOutletTargetParts(
307
- {
308
- target: record.target
309
- },
310
- { context: resolvedContext }
311
- );
312
- if (!targetParts) {
313
- return null;
314
- }
315
-
316
- return Object.freeze({
317
- ...targetParts,
318
- default:
319
- Object.hasOwn(record, "default") &&
320
- isDefaultAttributeEnabled(record.default)
321
- });
322
- }
323
-
324
- function discoverShellOutletTargetsFromVueSource(
325
- source = "",
326
- {
327
- context = "shell layout",
328
- enforceSingleDefault = true
329
- } = {}
330
- ) {
331
- const sourceText = String(source || "");
332
- const resolvedContext = normalizeText(context) || "shell layout";
333
- const targetById = new Map();
334
- let defaultTargetId = "";
335
-
336
- for (const tagMatch of sourceText.matchAll(SHELL_OUTLET_TAG_PATTERN)) {
337
- const attributes = parseTagAttributes(tagMatch[1]);
338
- const normalizedTarget = normalizeShellOutletTargetRecord(attributes, {
339
- context: resolvedContext
340
- });
341
- if (!normalizedTarget) {
342
- continue;
343
- }
344
- if (targetById.has(normalizedTarget.id)) {
345
- throw new Error(`${resolvedContext} contains duplicate ShellOutlet target "${normalizedTarget.id}".`);
346
- }
347
-
348
- if (normalizedTarget.default) {
349
- if (enforceSingleDefault === true && defaultTargetId) {
350
- throw new Error(
351
- `${resolvedContext} defines multiple default ShellOutlet targets: "${defaultTargetId}" and "${normalizedTarget.id}".`
352
- );
353
- }
354
- if (!defaultTargetId) {
355
- defaultTargetId = normalizedTarget.id;
356
- }
357
- }
358
-
359
- targetById.set(normalizedTarget.id, normalizedTarget);
360
- }
361
-
362
- return Object.freeze({
363
- targets: Object.freeze([...targetById.values()]),
364
- defaultTargetId
365
- });
366
- }
367
-
368
234
  export {
369
235
  PLACEMENT_LAYOUT_CLASSES,
370
- describeShellOutletTargets,
371
- discoverShellOutletTargetsFromVueSource,
372
- findShellOutletTargetById,
236
+ WEB_PLACEMENT_SURFACE_ANY,
373
237
  normalizePlacementKind,
374
238
  normalizePlacementLayoutClass,
375
239
  normalizePlacementOwnerId,
@@ -380,7 +244,5 @@ export {
380
244
  normalizePlacementTopologyVariant,
381
245
  normalizeSemanticPlacementId,
382
246
  normalizeShellOutletTargetId,
383
- normalizeShellOutletTargetRecord,
384
- resolvePlacementTargetReference,
385
- resolveShellOutletTargetParts
247
+ resolvePlacementTargetReference
386
248
  };