@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,67 @@
1
+ import { normalizePathname } from "../surface/paths.js";
2
+
3
+ const EXTERNAL_LINK_PATTERN = /^[A-Za-z][A-Za-z0-9+.-]*:/;
4
+
5
+ function isExternalLinkTarget(target = "") {
6
+ const normalizedTarget = String(target || "").trim();
7
+ if (!normalizedTarget) {
8
+ return false;
9
+ }
10
+ if (normalizedTarget.startsWith("//")) {
11
+ return true;
12
+ }
13
+ return EXTERNAL_LINK_PATTERN.test(normalizedTarget);
14
+ }
15
+
16
+ function splitPathQueryHash(target = "") {
17
+ const normalizedTarget = String(target || "").trim();
18
+ if (!normalizedTarget) {
19
+ return Object.freeze({
20
+ pathname: "",
21
+ search: "",
22
+ hash: ""
23
+ });
24
+ }
25
+
26
+ const hashIndex = normalizedTarget.indexOf("#");
27
+ const beforeHash = hashIndex >= 0 ? normalizedTarget.slice(0, hashIndex) : normalizedTarget;
28
+ const hash = hashIndex >= 0 ? normalizedTarget.slice(hashIndex) : "";
29
+ const queryIndex = beforeHash.indexOf("?");
30
+ const pathname = queryIndex >= 0 ? beforeHash.slice(0, queryIndex) : beforeHash;
31
+ const search = queryIndex >= 0 ? beforeHash.slice(queryIndex) : "";
32
+
33
+ return Object.freeze({
34
+ pathname,
35
+ search,
36
+ hash
37
+ });
38
+ }
39
+
40
+ function resolveLinkPath(basePath = "/", relativePath = "/") {
41
+ const normalizedRelativePath = String(relativePath || "").trim();
42
+ if (isExternalLinkTarget(normalizedRelativePath)) {
43
+ return normalizedRelativePath;
44
+ }
45
+
46
+ const normalizedBasePath = normalizePathname(basePath || "/");
47
+ if (!normalizedRelativePath) {
48
+ return normalizedBasePath;
49
+ }
50
+
51
+ const { pathname, search, hash } = splitPathQueryHash(normalizedRelativePath);
52
+ if (!pathname) {
53
+ return `${normalizedBasePath}${search}${hash}`;
54
+ }
55
+
56
+ const normalizedPathname = normalizePathname(pathname.startsWith("/") ? pathname : `/${pathname}`);
57
+ const resolvedPathname =
58
+ normalizedBasePath === "/"
59
+ ? normalizedPathname
60
+ : normalizedPathname === "/"
61
+ ? normalizedBasePath
62
+ : `${normalizedBasePath}${normalizedPathname}`;
63
+
64
+ return `${resolvedPathname}${search}${hash}`;
65
+ }
66
+
67
+ export { isExternalLinkTarget, splitPathQueryHash, resolveLinkPath };
@@ -0,0 +1,35 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { isExternalLinkTarget, splitPathQueryHash, resolveLinkPath } from "./linkPath.js";
4
+
5
+ test("isExternalLinkTarget detects absolute external targets", () => {
6
+ assert.equal(isExternalLinkTarget("https://example.com"), true);
7
+ assert.equal(isExternalLinkTarget("mailto:test@example.com"), true);
8
+ assert.equal(isExternalLinkTarget("//cdn.example.com/lib.js"), true);
9
+ assert.equal(isExternalLinkTarget("/contacts"), false);
10
+ });
11
+
12
+ test("splitPathQueryHash separates pathname, search, and hash", () => {
13
+ assert.deepEqual(splitPathQueryHash("/members?tab=all#list"), {
14
+ pathname: "/members",
15
+ search: "?tab=all",
16
+ hash: "#list"
17
+ });
18
+ });
19
+
20
+ test("resolveLinkPath composes basePath and relative path", () => {
21
+ assert.equal(resolveLinkPath("/admin/w/acme", "/contacts"), "/admin/w/acme/contacts");
22
+ assert.equal(resolveLinkPath("/admin/w/acme", "contacts/2"), "/admin/w/acme/contacts/2");
23
+ assert.equal(resolveLinkPath("/admin/w/acme", "/"), "/admin/w/acme");
24
+ assert.equal(resolveLinkPath("/", "/contacts"), "/contacts");
25
+ });
26
+
27
+ test("resolveLinkPath preserves query and hash", () => {
28
+ assert.equal(resolveLinkPath("/admin/w/acme", "/contacts?sort=asc#top"), "/admin/w/acme/contacts?sort=asc#top");
29
+ assert.equal(resolveLinkPath("/admin/w/acme", "?tab=profile"), "/admin/w/acme?tab=profile");
30
+ assert.equal(resolveLinkPath("/admin/w/acme", "#details"), "/admin/w/acme#details");
31
+ });
32
+
33
+ test("resolveLinkPath keeps external targets unchanged", () => {
34
+ assert.equal(resolveLinkPath("/admin/w/acme", "https://example.com/docs"), "https://example.com/docs");
35
+ });
@@ -0,0 +1,116 @@
1
+ function normalizeText(value, { fallback = "" } = {}) {
2
+ const normalized = String(value || "").trim();
3
+ return normalized || fallback;
4
+ }
5
+
6
+ function normalizeLowerText(value, { fallback = "" } = {}) {
7
+ return normalizeText(value, {
8
+ fallback
9
+ }).toLowerCase();
10
+ }
11
+
12
+ function normalizeQueryToken(value, { fallback = "__none__" } = {}) {
13
+ const normalized = normalizeText(value).toLowerCase();
14
+ return normalized || fallback;
15
+ }
16
+
17
+ function normalizeObject(value, { fallback = {} } = {}) {
18
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
19
+ return { ...fallback };
20
+ }
21
+ return value;
22
+ }
23
+
24
+ function isRecord(value) {
25
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
26
+ }
27
+
28
+ function normalizeArray(value) {
29
+ return Array.isArray(value) ? value : [];
30
+ }
31
+
32
+ function normalizeInteger(value, { fallback = 0, min = null, max = null } = {}) {
33
+ const numeric = Number(value);
34
+ if (!Number.isFinite(numeric)) {
35
+ return fallback;
36
+ }
37
+
38
+ let next = Math.trunc(numeric);
39
+ if (Number.isFinite(min) && next < min) {
40
+ next = min;
41
+ }
42
+ if (Number.isFinite(max) && next > max) {
43
+ next = max;
44
+ }
45
+ return next;
46
+ }
47
+
48
+ function normalizePositiveInteger(value, { fallback = 0 } = {}) {
49
+ const numeric = Number(value);
50
+ if (!Number.isInteger(numeric) || numeric < 1) {
51
+ return fallback;
52
+ }
53
+
54
+ return numeric;
55
+ }
56
+
57
+ function normalizeOpaqueId(value, { fallback = null } = {}) {
58
+ if (value == null) {
59
+ return fallback;
60
+ }
61
+
62
+ if (typeof value === "string") {
63
+ const normalized = value.trim();
64
+ return normalized || fallback;
65
+ }
66
+
67
+ if (typeof value === "number") {
68
+ return Number.isFinite(value) ? value : fallback;
69
+ }
70
+
71
+ if (typeof value === "bigint") {
72
+ return String(value);
73
+ }
74
+
75
+ return fallback;
76
+ }
77
+
78
+ function normalizeOneOf(value, allowedValues = [], fallback = "") {
79
+ const normalized = normalizeText(value).toLowerCase();
80
+ const supported = Array.isArray(allowedValues)
81
+ ? allowedValues.map((entry) => normalizeText(entry).toLowerCase()).filter(Boolean)
82
+ : [];
83
+
84
+ if (supported.includes(normalized)) {
85
+ return normalized;
86
+ }
87
+
88
+ const normalizedFallback = normalizeText(fallback).toLowerCase();
89
+ if (normalizedFallback) {
90
+ return normalizedFallback;
91
+ }
92
+
93
+ return supported[0] || "";
94
+ }
95
+
96
+ function ensureNonEmptyText(value, label = "value") {
97
+ const normalized = normalizeText(value);
98
+ if (!normalized) {
99
+ throw new TypeError(`${label} is required.`);
100
+ }
101
+ return normalized;
102
+ }
103
+
104
+ export {
105
+ normalizeText,
106
+ normalizeLowerText,
107
+ normalizeQueryToken,
108
+ normalizeObject,
109
+ isRecord,
110
+ normalizeArray,
111
+ normalizeInteger,
112
+ normalizePositiveInteger,
113
+ normalizeOpaqueId,
114
+ normalizeOneOf,
115
+ ensureNonEmptyText
116
+ };
@@ -0,0 +1,48 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ normalizeOpaqueId,
5
+ normalizePositiveInteger,
6
+ normalizeOneOf,
7
+ normalizeQueryToken
8
+ } from "./normalize.js";
9
+
10
+ test("normalizeQueryToken trims, lowercases, and falls back when empty", () => {
11
+ assert.equal(normalizeQueryToken(" Admin "), "admin");
12
+ assert.equal(normalizeQueryToken(""), "__none__");
13
+ assert.equal(normalizeQueryToken(null), "__none__");
14
+ assert.equal(normalizeQueryToken(" ", { fallback: "surface" }), "surface");
15
+ });
16
+
17
+ test("normalizeOneOf returns normalized value when supported", () => {
18
+ assert.equal(normalizeOneOf(" Compact ", ["compact", "comfortable"], "comfortable"), "compact");
19
+ });
20
+
21
+ test("normalizeOneOf returns fallback when value is unsupported", () => {
22
+ assert.equal(normalizeOneOf("wide", ["compact", "comfortable"], "comfortable"), "comfortable");
23
+ });
24
+
25
+ test("normalizeOneOf falls back to first allowed value when fallback is empty", () => {
26
+ assert.equal(normalizeOneOf("", ["compact", "comfortable"], ""), "compact");
27
+ });
28
+
29
+ test("normalizePositiveInteger normalizes valid integers and applies fallback", () => {
30
+ assert.equal(normalizePositiveInteger("12"), 12);
31
+ assert.equal(normalizePositiveInteger(0), 0);
32
+ assert.equal(normalizePositiveInteger(-1), 0);
33
+ assert.equal(
34
+ normalizePositiveInteger("abc", {
35
+ fallback: null
36
+ }),
37
+ null
38
+ );
39
+ });
40
+
41
+ test("normalizeOpaqueId preserves opaque identifiers", () => {
42
+ assert.equal(normalizeOpaqueId(" user-123 "), "user-123");
43
+ assert.equal(normalizeOpaqueId(7), 7);
44
+ assert.equal(normalizeOpaqueId(0), 0);
45
+ assert.equal(normalizeOpaqueId(10n), "10");
46
+ assert.equal(normalizeOpaqueId(""), null);
47
+ assert.equal(normalizeOpaqueId(null), null);
48
+ });
@@ -0,0 +1,121 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+
7
+ import { loadInstalledPackageDescriptor, resolveDescriptorPathForInstalledPackage } from "../../internal/node/installedPackageDescriptor.js";
8
+
9
+ async function createFixtureRoot(prefix) {
10
+ return mkdtemp(path.join(tmpdir(), prefix));
11
+ }
12
+
13
+ async function writeDescriptorFile(filePath, descriptorSource) {
14
+ await mkdir(path.dirname(filePath), { recursive: true });
15
+ await writeFile(filePath, descriptorSource, "utf8");
16
+ }
17
+
18
+ test("resolveDescriptorPathForInstalledPackage returns empty string when descriptor is missing and required=false", async () => {
19
+ const appRoot = await createFixtureRoot("kernel-package-descriptor-missing-");
20
+ try {
21
+ const descriptorPath = await resolveDescriptorPathForInstalledPackage({
22
+ appRoot,
23
+ packageId: "@example/missing",
24
+ installedPackageState: {},
25
+ required: false
26
+ });
27
+ assert.equal(descriptorPath, "");
28
+ } finally {
29
+ await rm(appRoot, { recursive: true, force: true });
30
+ }
31
+ });
32
+
33
+ test("resolveDescriptorPathForInstalledPackage throws when descriptor is missing and required=true", async () => {
34
+ const appRoot = await createFixtureRoot("kernel-package-descriptor-required-");
35
+ try {
36
+ await assert.rejects(
37
+ () =>
38
+ resolveDescriptorPathForInstalledPackage({
39
+ appRoot,
40
+ packageId: "@example/missing",
41
+ installedPackageState: {},
42
+ required: true
43
+ }),
44
+ /Unable to resolve package descriptor for @example\/missing\./
45
+ );
46
+ } finally {
47
+ await rm(appRoot, { recursive: true, force: true });
48
+ }
49
+ });
50
+
51
+ test("resolveDescriptorPathForInstalledPackage resolves descriptor using source.packagePath", async () => {
52
+ const appRoot = await createFixtureRoot("kernel-package-descriptor-package-path-");
53
+ try {
54
+ const descriptorPath = path.join(appRoot, "packages", "local-example", "package.descriptor.mjs");
55
+ await writeDescriptorFile(descriptorPath, "export default { packageId: \"@local/example\" };\n");
56
+
57
+ const resolvedDescriptorPath = await resolveDescriptorPathForInstalledPackage({
58
+ appRoot,
59
+ packageId: "@local/example",
60
+ installedPackageState: {
61
+ source: {
62
+ packagePath: "packages/local-example"
63
+ }
64
+ },
65
+ required: true
66
+ });
67
+ assert.equal(resolvedDescriptorPath, descriptorPath);
68
+ } finally {
69
+ await rm(appRoot, { recursive: true, force: true });
70
+ }
71
+ });
72
+
73
+ test("resolveDescriptorPathForInstalledPackage resolves descriptor using jskit-cli descriptorPath fallback", async () => {
74
+ const appRoot = await createFixtureRoot("kernel-package-descriptor-jskit-root-");
75
+ try {
76
+ const descriptorPath = path.join(
77
+ appRoot,
78
+ "node_modules",
79
+ "@jskit-ai",
80
+ "jskit-cli",
81
+ "descriptors",
82
+ "example.descriptor.mjs"
83
+ );
84
+ await writeDescriptorFile(descriptorPath, "export default { packageId: \"@example/test\" };\n");
85
+
86
+ const resolvedDescriptorPath = await resolveDescriptorPathForInstalledPackage({
87
+ appRoot,
88
+ packageId: "@example/test",
89
+ installedPackageState: {
90
+ source: {
91
+ descriptorPath: "descriptors/example.descriptor.mjs"
92
+ }
93
+ },
94
+ required: true
95
+ });
96
+ assert.equal(resolvedDescriptorPath, descriptorPath);
97
+ } finally {
98
+ await rm(appRoot, { recursive: true, force: true });
99
+ }
100
+ });
101
+
102
+ test("loadInstalledPackageDescriptor loads descriptor payload once resolved", async () => {
103
+ const appRoot = await createFixtureRoot("kernel-package-descriptor-load-");
104
+ try {
105
+ const descriptorPath = path.join(appRoot, "node_modules", "@example", "ready", "package.descriptor.mjs");
106
+ await writeDescriptorFile(descriptorPath, "export default { packageId: \"@example/ready\", version: \"1.0.0\" };\n");
107
+
108
+ const descriptorRecord = await loadInstalledPackageDescriptor({
109
+ appRoot,
110
+ packageId: "@example/ready",
111
+ installedPackageState: {},
112
+ required: true
113
+ });
114
+
115
+ assert.equal(descriptorRecord.descriptorPath, descriptorPath);
116
+ assert.equal(descriptorRecord.descriptor.packageId, "@example/ready");
117
+ assert.equal(descriptorRecord.descriptor.version, "1.0.0");
118
+ } finally {
119
+ await rm(appRoot, { recursive: true, force: true });
120
+ }
121
+ });
@@ -0,0 +1,50 @@
1
+ import { normalizeText } from "./normalize.js";
2
+
3
+ function normalizePermissionList(value) {
4
+ const source = Array.isArray(value) ? value : [value];
5
+ const unique = new Set();
6
+
7
+ for (const entry of source) {
8
+ const normalizedEntry = normalizeText(entry);
9
+ if (!normalizedEntry) {
10
+ continue;
11
+ }
12
+ unique.add(normalizedEntry);
13
+ }
14
+
15
+ return [...unique];
16
+ }
17
+
18
+ function hasPermission(permissionSet = [], permission = "") {
19
+ const requiredPermission = normalizeText(permission);
20
+ if (!requiredPermission) {
21
+ return true;
22
+ }
23
+
24
+ const permissions = normalizePermissionList(permissionSet);
25
+ if (permissions.includes("*") || permissions.includes(requiredPermission)) {
26
+ return true;
27
+ }
28
+
29
+ for (const grantedPermission of permissions) {
30
+ if (!grantedPermission.endsWith(".*")) {
31
+ continue;
32
+ }
33
+
34
+ const wildcardNamespace = grantedPermission.slice(0, -2);
35
+ if (!wildcardNamespace) {
36
+ continue;
37
+ }
38
+
39
+ if (requiredPermission.startsWith(`${wildcardNamespace}.`)) {
40
+ return true;
41
+ }
42
+ }
43
+
44
+ return false;
45
+ }
46
+
47
+ export {
48
+ normalizePermissionList,
49
+ hasPermission
50
+ };
@@ -0,0 +1,17 @@
1
+ function pickOwnProperties(source, keys) {
2
+ if (!source || typeof source !== "object" || Array.isArray(source)) {
3
+ throw new TypeError("pickOwnProperties requires a plain object source.");
4
+ }
5
+
6
+ const patch = {};
7
+
8
+ for (const key of keys) {
9
+ if (Object.hasOwn(source, key)) {
10
+ patch[key] = source[key];
11
+ }
12
+ }
13
+
14
+ return patch;
15
+ }
16
+
17
+ export { pickOwnProperties };
@@ -0,0 +1,25 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { pickOwnProperties } from "./pickOwnProperties.js";
4
+
5
+ test("pickOwnProperties preserves only explicitly present keys", () => {
6
+ const source = {
7
+ theme: "dark",
8
+ avatarUrl: "",
9
+ invitesEnabled: false,
10
+ ignored: "x"
11
+ };
12
+
13
+ const patch = pickOwnProperties(source, ["theme", "avatarUrl", "invitesEnabled", "missing"]);
14
+
15
+ assert.deepEqual(patch, {
16
+ theme: "dark",
17
+ avatarUrl: "",
18
+ invitesEnabled: false
19
+ });
20
+ });
21
+
22
+ test("pickOwnProperties requires caller-normalized object input", () => {
23
+ assert.throws(() => pickOwnProperties(null, ["theme"]), TypeError);
24
+ assert.throws(() => pickOwnProperties([], ["theme"]), TypeError);
25
+ });
@@ -0,0 +1,11 @@
1
+ const AUTH_POLICY_PUBLIC = "public";
2
+ const AUTH_POLICY_AUTHENTICATED = "authenticated";
3
+ const ROUTE_VISIBILITY_PUBLIC = AUTH_POLICY_PUBLIC;
4
+ const ROUTE_VISIBILITY_USER = "user";
5
+
6
+ export {
7
+ AUTH_POLICY_PUBLIC,
8
+ AUTH_POLICY_AUTHENTICATED,
9
+ ROUTE_VISIBILITY_PUBLIC,
10
+ ROUTE_VISIBILITY_USER
11
+ };
@@ -0,0 +1,33 @@
1
+ function splitPathAndHash(path = "") {
2
+ const normalizedPath = String(path || "").trim();
3
+ const hashIndex = normalizedPath.indexOf("#");
4
+ if (hashIndex < 0) {
5
+ return {
6
+ pathWithoutHash: normalizedPath,
7
+ hash: ""
8
+ };
9
+ }
10
+
11
+ return {
12
+ pathWithoutHash: normalizedPath.slice(0, hashIndex),
13
+ hash: normalizedPath.slice(hashIndex)
14
+ };
15
+ }
16
+
17
+ function appendQueryString(path = "", queryString = "") {
18
+ const normalizedPath = String(path || "").trim();
19
+ if (!normalizedPath) {
20
+ return "";
21
+ }
22
+
23
+ const normalizedQuery = String(queryString || "").trim().replace(/^\?+/, "");
24
+ if (!normalizedQuery) {
25
+ return normalizedPath;
26
+ }
27
+
28
+ const { pathWithoutHash, hash } = splitPathAndHash(normalizedPath);
29
+ const separator = pathWithoutHash.includes("?") ? "&" : "?";
30
+ return `${pathWithoutHash}${separator}${normalizedQuery}${hash}`;
31
+ }
32
+
33
+ export { appendQueryString };
@@ -0,0 +1,19 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { appendQueryString } from "./queryPath.js";
4
+
5
+ test("appendQueryString appends query to a path without existing search", () => {
6
+ assert.equal(appendQueryString("/api/conversations", "cursor=2"), "/api/conversations?cursor=2");
7
+ });
8
+
9
+ test("appendQueryString appends query to a path with existing search", () => {
10
+ assert.equal(appendQueryString("/api/conversations?status=open", "cursor=2"), "/api/conversations?status=open&cursor=2");
11
+ });
12
+
13
+ test("appendQueryString preserves hash fragments", () => {
14
+ assert.equal(appendQueryString("/settings/account#profile", "returnTo=%2Fdashboard"), "/settings/account?returnTo=%2Fdashboard#profile");
15
+ });
16
+
17
+ test("appendQueryString ignores empty query strings", () => {
18
+ assert.equal(appendQueryString("/api/conversations", ""), "/api/conversations");
19
+ });
@@ -0,0 +1,34 @@
1
+ const TRANSIENT_QUERY_ERROR_STATUSES = Object.freeze(new Set([0, 502, 503, 504]));
2
+ const MAX_TRANSIENT_QUERY_RETRIES = 2;
3
+ const MAX_TRANSIENT_RETRY_DELAY_MS = 3000;
4
+
5
+ function normalizeQueryErrorStatus(error) {
6
+ const status = Number(error?.status || error?.statusCode || 0);
7
+ return Number.isInteger(status) ? status : 0;
8
+ }
9
+
10
+ function isTransientQueryError(error) {
11
+ return TRANSIENT_QUERY_ERROR_STATUSES.has(normalizeQueryErrorStatus(error));
12
+ }
13
+
14
+ function shouldRetryTransientQueryFailure(failureCount, error) {
15
+ if (!isTransientQueryError(error)) {
16
+ return false;
17
+ }
18
+ return Number(failureCount) < MAX_TRANSIENT_QUERY_RETRIES;
19
+ }
20
+
21
+ function transientQueryRetryDelay(attemptIndex) {
22
+ const normalizedAttempt = Number.isInteger(Number(attemptIndex)) ? Number(attemptIndex) : 1;
23
+ return Math.min(1000 * 2 ** Math.max(0, normalizedAttempt - 1), MAX_TRANSIENT_RETRY_DELAY_MS);
24
+ }
25
+
26
+ export {
27
+ TRANSIENT_QUERY_ERROR_STATUSES,
28
+ MAX_TRANSIENT_QUERY_RETRIES,
29
+ MAX_TRANSIENT_RETRY_DELAY_MS,
30
+ normalizeQueryErrorStatus,
31
+ isTransientQueryError,
32
+ shouldRetryTransientQueryFailure,
33
+ transientQueryRetryDelay
34
+ };
@@ -0,0 +1,33 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ isTransientQueryError,
5
+ shouldRetryTransientQueryFailure,
6
+ transientQueryRetryDelay,
7
+ normalizeQueryErrorStatus
8
+ } from "./queryResilience.js";
9
+
10
+ test("normalizeQueryErrorStatus reads status and statusCode", () => {
11
+ assert.equal(normalizeQueryErrorStatus({ status: 503 }), 503);
12
+ assert.equal(normalizeQueryErrorStatus({ statusCode: 504 }), 504);
13
+ assert.equal(normalizeQueryErrorStatus({}), 0);
14
+ });
15
+
16
+ test("isTransientQueryError checks transient statuses", () => {
17
+ assert.equal(isTransientQueryError({ status: 503 }), true);
18
+ assert.equal(isTransientQueryError({ status: 0 }), true);
19
+ assert.equal(isTransientQueryError({ status: 401 }), false);
20
+ });
21
+
22
+ test("shouldRetryTransientQueryFailure allows capped transient retries only", () => {
23
+ assert.equal(shouldRetryTransientQueryFailure(0, { status: 503 }), true);
24
+ assert.equal(shouldRetryTransientQueryFailure(1, { status: 502 }), true);
25
+ assert.equal(shouldRetryTransientQueryFailure(2, { status: 503 }), false);
26
+ assert.equal(shouldRetryTransientQueryFailure(0, { status: 422 }), false);
27
+ });
28
+
29
+ test("transientQueryRetryDelay is bounded exponential", () => {
30
+ assert.equal(transientQueryRetryDelay(1), 1000);
31
+ assert.equal(transientQueryRetryDelay(2), 2000);
32
+ assert.equal(transientQueryRetryDelay(5), 3000);
33
+ });