@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,42 @@
1
+ import { Type } from "typebox";
2
+ import { normalizeObjectInput } from "./inputNormalization.js";
3
+ import { normalizeText } from "../support/normalize.js";
4
+
5
+ function createCursorListValidator(itemValidator) {
6
+ if (!itemValidator || typeof itemValidator !== "object" || Array.isArray(itemValidator)) {
7
+ throw new TypeError("createCursorListValidator requires an item validator object.");
8
+ }
9
+
10
+ if (!Object.hasOwn(itemValidator, "schema")) {
11
+ throw new TypeError("createCursorListValidator requires itemValidator.schema.");
12
+ }
13
+
14
+ const normalizeItem =
15
+ typeof itemValidator.normalize === "function"
16
+ ? itemValidator.normalize
17
+ : function identity(value) {
18
+ return value;
19
+ };
20
+
21
+ return Object.freeze({
22
+ get schema() {
23
+ return Type.Object(
24
+ {
25
+ items: Type.Array(itemValidator.schema),
26
+ nextCursor: Type.Union([Type.String({ minLength: 1 }), Type.Null()])
27
+ },
28
+ { additionalProperties: false }
29
+ );
30
+ },
31
+ normalize(payload = {}) {
32
+ const source = normalizeObjectInput(payload);
33
+
34
+ return {
35
+ items: Array.isArray(source.items) ? source.items.map((entry) => normalizeItem(entry)) : [],
36
+ nextCursor: normalizeText(source.nextCursor) || null
37
+ };
38
+ }
39
+ });
40
+ }
41
+
42
+ export { createCursorListValidator };
@@ -0,0 +1,34 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { Type } from "typebox";
4
+ import { createCursorListValidator } from "./createCursorListValidator.js";
5
+
6
+ test("createCursorListValidator builds a list validator from an item validator", () => {
7
+ const itemValidator = {
8
+ schema: Type.Object(
9
+ {
10
+ id: Type.Integer({ minimum: 1 }),
11
+ label: Type.String({ minLength: 1 })
12
+ },
13
+ { additionalProperties: false }
14
+ ),
15
+ normalize(payload = {}) {
16
+ return {
17
+ id: Number(payload.id),
18
+ label: String(payload.label || "").trim()
19
+ };
20
+ }
21
+ };
22
+
23
+ const listValidator = createCursorListValidator(itemValidator);
24
+ const normalized = listValidator.normalize({
25
+ items: [{ id: "7", label: " member " }],
26
+ nextCursor: " 8 "
27
+ });
28
+
29
+ assert.deepEqual(normalized, {
30
+ items: [{ id: 7, label: "member" }],
31
+ nextCursor: "8"
32
+ });
33
+ assert.equal(listValidator.schema.properties.items.type, "array");
34
+ });
@@ -0,0 +1,31 @@
1
+ import { Type } from "typebox";
2
+ import { normalizeObjectInput } from "./inputNormalization.js";
3
+ import { positiveIntegerValidator } from "./recordIdParamsValidator.js";
4
+
5
+ function normalizeCursorPaginationQuery(input = {}) {
6
+ const source = normalizeObjectInput(input);
7
+ const normalized = {};
8
+
9
+ if (Object.hasOwn(source, "cursor")) {
10
+ normalized.cursor = positiveIntegerValidator.normalize(source.cursor);
11
+ }
12
+
13
+ if (Object.hasOwn(source, "limit")) {
14
+ normalized.limit = positiveIntegerValidator.normalize(source.limit);
15
+ }
16
+
17
+ return normalized;
18
+ }
19
+
20
+ const cursorPaginationQueryValidator = Object.freeze({
21
+ schema: Type.Object(
22
+ {
23
+ cursor: Type.Optional(positiveIntegerValidator.schema),
24
+ limit: Type.Optional(positiveIntegerValidator.schema)
25
+ },
26
+ { additionalProperties: false }
27
+ ),
28
+ normalize: normalizeCursorPaginationQuery
29
+ });
30
+
31
+ export { cursorPaginationQueryValidator };
@@ -0,0 +1,21 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { cursorPaginationQueryValidator } from "./cursorPaginationQueryValidator.js";
4
+
5
+ test("cursorPaginationQueryValidator normalizes numeric strings", () => {
6
+ assert.deepEqual(cursorPaginationQueryValidator.normalize({ cursor: "12", limit: "25" }), {
7
+ cursor: 12,
8
+ limit: 25
9
+ });
10
+ });
11
+
12
+ test("cursorPaginationQueryValidator normalizes invalid values to 0", () => {
13
+ assert.deepEqual(cursorPaginationQueryValidator.normalize({ cursor: "abc", limit: "-1" }), {
14
+ cursor: 0,
15
+ limit: 0
16
+ });
17
+ });
18
+
19
+ test("cursorPaginationQueryValidator keeps absent keys absent", () => {
20
+ assert.deepEqual(cursorPaginationQueryValidator.normalize({}), {});
21
+ });
@@ -0,0 +1,12 @@
1
+ export { normalizeObjectInput } from "./inputNormalization.js";
2
+ export { createCursorListValidator } from "./createCursorListValidator.js";
3
+ export { cursorPaginationQueryValidator } from "./cursorPaginationQueryValidator.js";
4
+ export { mergeObjectSchemas } from "./mergeObjectSchemas.js";
5
+ export { mergeValidators } from "./mergeValidators.js";
6
+ export { nestValidator } from "./nestValidator.js";
7
+ export { recordIdParamsValidator, positiveIntegerValidator } from "./recordIdParamsValidator.js";
8
+ export {
9
+ normalizeRequiredFieldList,
10
+ deriveRequiredFieldsFromSchema,
11
+ deriveResourceRequiredMetadata
12
+ } from "./resourceRequiredMetadata.js";
@@ -0,0 +1,13 @@
1
+ function normalizeObjectInput(value) {
2
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3
+ return {};
4
+ }
5
+
6
+ return {
7
+ ...value
8
+ };
9
+ }
10
+
11
+ export {
12
+ normalizeObjectInput
13
+ };
@@ -0,0 +1,31 @@
1
+ import { Type } from "typebox";
2
+
3
+ function mergeObjectSchemas(schemas) {
4
+ if (!Array.isArray(schemas)) {
5
+ throw new Error("mergeObjectSchemas requires an array of object schemas.");
6
+ }
7
+
8
+ const mergedProperties = {};
9
+
10
+ for (const schema of schemas) {
11
+ if (!schema || typeof schema !== "object" || schema.type !== "object" || typeof schema.properties !== "object") {
12
+ throw new Error("mergeObjectSchemas only supports Type.Object schemas.");
13
+ }
14
+
15
+ for (const [propertyName, propertySchema] of Object.entries(schema.properties)) {
16
+ if (Object.hasOwn(mergedProperties, propertyName) && mergedProperties[propertyName] !== propertySchema) {
17
+ throw new Error(`mergeObjectSchemas cannot merge duplicate property "${propertyName}".`);
18
+ }
19
+
20
+ mergedProperties[propertyName] = propertySchema;
21
+ }
22
+ }
23
+
24
+ return Type.Object(mergedProperties, {
25
+ additionalProperties: false
26
+ });
27
+ }
28
+
29
+ export {
30
+ mergeObjectSchemas
31
+ };
@@ -0,0 +1,67 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { Type } from "typebox";
4
+ import { mergeObjectSchemas } from "./mergeObjectSchemas.js";
5
+
6
+ test("mergeObjectSchemas merges disjoint object schemas", () => {
7
+ const mergedSchema = mergeObjectSchemas([
8
+ Type.Object(
9
+ {
10
+ cursor: Type.Optional(Type.String({ minLength: 1 }))
11
+ },
12
+ { additionalProperties: false }
13
+ ),
14
+ Type.Object(
15
+ {
16
+ search: Type.Optional(Type.String({ minLength: 1 }))
17
+ },
18
+ { additionalProperties: false }
19
+ )
20
+ ]);
21
+
22
+ assert.equal(mergedSchema.type, "object");
23
+ assert.equal(mergedSchema.additionalProperties, false);
24
+ assert.equal(typeof mergedSchema.properties.cursor, "object");
25
+ assert.equal(typeof mergedSchema.properties.search, "object");
26
+ assert.deepEqual(mergedSchema.required || [], []);
27
+ });
28
+
29
+ test("mergeObjectSchemas preserves required fields through merged property definitions", () => {
30
+ const mergedSchema = mergeObjectSchemas([
31
+ Type.Object(
32
+ {
33
+ workspaceSlug: Type.String({ minLength: 1 })
34
+ },
35
+ { additionalProperties: false }
36
+ ),
37
+ Type.Object(
38
+ {
39
+ inviteId: Type.String({ minLength: 1 })
40
+ },
41
+ { additionalProperties: false }
42
+ )
43
+ ]);
44
+
45
+ assert.deepEqual(mergedSchema.required, ["workspaceSlug", "inviteId"]);
46
+ });
47
+
48
+ test("mergeObjectSchemas rejects duplicate properties with different schema objects", () => {
49
+ assert.throws(
50
+ () =>
51
+ mergeObjectSchemas([
52
+ Type.Object(
53
+ {
54
+ workspaceSlug: Type.Optional(Type.String({ minLength: 1 }))
55
+ },
56
+ { additionalProperties: false }
57
+ ),
58
+ Type.Object(
59
+ {
60
+ workspaceSlug: Type.Optional(Type.String({ minLength: 2 }))
61
+ },
62
+ { additionalProperties: false }
63
+ )
64
+ ]),
65
+ /duplicate property "workspaceSlug"/
66
+ );
67
+ });
@@ -0,0 +1,89 @@
1
+ import { mergeObjectSchemas } from "./mergeObjectSchemas.js";
2
+ import { isRecord as isPlainObject } from "../support/normalize.js";
3
+
4
+ function isPromiseLike(value) {
5
+ return Boolean(value) && typeof value.then === "function";
6
+ }
7
+
8
+ function createErrorFactory(createError) {
9
+ if (typeof createError === "function") {
10
+ return createError;
11
+ }
12
+
13
+ return (message) => new Error(message);
14
+ }
15
+
16
+ function mergeValidators(validators = [], options = {}) {
17
+ const sourceValidators = Array.isArray(validators) ? validators : [];
18
+ const context = String(options?.context || "validator").trim() || "validator";
19
+ const requireSchema = options?.requireSchema === true;
20
+ const allowAsyncNormalize = options?.allowAsyncNormalize !== false;
21
+ const requiredSchemaMessage = String(options?.requiredSchemaMessage || `${context}.schema is required.`);
22
+ const normalizeResultMessage = String(options?.normalizeResultMessage || `${context}.normalize must return an object.`);
23
+ const makeError = createErrorFactory(options?.createError);
24
+ const schemas = [];
25
+ const normalizers = [];
26
+
27
+ for (const validator of sourceValidators) {
28
+ if (!isPlainObject(validator)) {
29
+ continue;
30
+ }
31
+
32
+ if (Object.prototype.hasOwnProperty.call(validator, "schema")) {
33
+ schemas.push(validator.schema);
34
+ }
35
+
36
+ if (typeof validator.normalize === "function") {
37
+ normalizers.push(validator.normalize);
38
+ }
39
+ }
40
+
41
+ if (requireSchema && schemas.length < 1) {
42
+ throw makeError(requiredSchemaMessage);
43
+ }
44
+
45
+ const merged = {};
46
+ if (schemas.length === 1) {
47
+ merged.schema = schemas[0];
48
+ } else if (schemas.length > 1) {
49
+ merged.schema = mergeObjectSchemas(schemas);
50
+ }
51
+
52
+ if (normalizers.length === 1) {
53
+ merged.normalize = normalizers[0];
54
+ } else if (normalizers.length > 1) {
55
+ if (allowAsyncNormalize) {
56
+ merged.normalize = async function normalizeMergedValidators(payload, meta) {
57
+ const normalized = {};
58
+
59
+ for (const normalizer of normalizers) {
60
+ const result = await normalizer(payload, meta);
61
+ if (!isPlainObject(result)) {
62
+ throw makeError(normalizeResultMessage);
63
+ }
64
+ Object.assign(normalized, result);
65
+ }
66
+
67
+ return normalized;
68
+ };
69
+ } else {
70
+ merged.normalize = function normalizeMergedValidators(payload, meta) {
71
+ const normalized = {};
72
+
73
+ for (const normalizer of normalizers) {
74
+ const result = normalizer(payload, meta);
75
+ if (isPromiseLike(result) || !isPlainObject(result)) {
76
+ throw makeError(normalizeResultMessage);
77
+ }
78
+ Object.assign(normalized, result);
79
+ }
80
+
81
+ return normalized;
82
+ };
83
+ }
84
+ }
85
+
86
+ return Object.freeze(merged);
87
+ }
88
+
89
+ export { mergeValidators };
@@ -0,0 +1,116 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { Type } from "typebox";
4
+ import { mergeValidators } from "./mergeValidators.js";
5
+
6
+ test("mergeValidators merges schemas and sync normalizers", () => {
7
+ const merged = mergeValidators(
8
+ [
9
+ {
10
+ schema: Type.Object(
11
+ {
12
+ workspaceSlug: Type.Optional(Type.String({ minLength: 1 }))
13
+ },
14
+ { additionalProperties: false }
15
+ ),
16
+ normalize(input = {}) {
17
+ return {
18
+ workspaceSlug: String(input.workspaceSlug || "").trim().toLowerCase()
19
+ };
20
+ }
21
+ },
22
+ {
23
+ schema: Type.Object(
24
+ {
25
+ recordId: Type.Optional(Type.Union([Type.Integer({ minimum: 1 }), Type.String({ minLength: 1 })]))
26
+ },
27
+ { additionalProperties: false }
28
+ ),
29
+ normalize(input = {}) {
30
+ const parsed = Number(input.recordId);
31
+ return {
32
+ recordId: Number.isInteger(parsed) && parsed > 0 ? parsed : 0
33
+ };
34
+ }
35
+ }
36
+ ],
37
+ {
38
+ context: "route params",
39
+ allowAsyncNormalize: false
40
+ }
41
+ );
42
+
43
+ assert.equal(typeof merged.schema, "object");
44
+ assert.equal(typeof merged.normalize, "function");
45
+ assert.deepEqual(merged.normalize({ workspaceSlug: " ACME ", recordId: "42" }), {
46
+ workspaceSlug: "acme",
47
+ recordId: 42
48
+ });
49
+ });
50
+
51
+ test("mergeValidators merges async normalizers for action validators", async () => {
52
+ const merged = mergeValidators(
53
+ [
54
+ {
55
+ schema: Type.Object(
56
+ {
57
+ workspaceSlug: Type.Optional(Type.String({ minLength: 1 }))
58
+ },
59
+ { additionalProperties: false }
60
+ ),
61
+ async normalize(input = {}) {
62
+ return {
63
+ workspaceSlug: String(input.workspaceSlug || "").trim().toLowerCase()
64
+ };
65
+ }
66
+ },
67
+ {
68
+ schema: Type.Object(
69
+ {
70
+ invitesEnabled: Type.Optional(Type.Boolean())
71
+ },
72
+ { additionalProperties: false }
73
+ ),
74
+ normalize(input = {}) {
75
+ return {
76
+ invitesEnabled: input.invitesEnabled === true
77
+ };
78
+ }
79
+ }
80
+ ],
81
+ {
82
+ context: "action input"
83
+ }
84
+ );
85
+
86
+ assert.equal(typeof merged.schema, "object");
87
+ assert.equal(typeof merged.normalize, "function");
88
+ const normalized = await merged.normalize({
89
+ workspaceSlug: " ACME ",
90
+ invitesEnabled: true
91
+ });
92
+ assert.deepEqual(normalized, {
93
+ workspaceSlug: "acme",
94
+ invitesEnabled: true
95
+ });
96
+ });
97
+
98
+ test("mergeValidators enforces schema requirement when requested", () => {
99
+ assert.throws(
100
+ () =>
101
+ mergeValidators(
102
+ [
103
+ {
104
+ normalize() {
105
+ return {};
106
+ }
107
+ }
108
+ ],
109
+ {
110
+ context: "action input",
111
+ requireSchema: true
112
+ }
113
+ ),
114
+ /action input\.schema is required/
115
+ );
116
+ });
@@ -0,0 +1,53 @@
1
+ import { Type } from "typebox";
2
+ import { normalizeObjectInput } from "./inputNormalization.js";
3
+
4
+ function normalizeValidator(validator) {
5
+ if (!validator || typeof validator !== "object" || Array.isArray(validator)) {
6
+ return null;
7
+ }
8
+
9
+ if (!Object.hasOwn(validator, "schema")) {
10
+ return null;
11
+ }
12
+
13
+ return validator;
14
+ }
15
+
16
+ function nestValidator(key, validator, { required = true } = {}) {
17
+ const normalizedKey = String(key || "").trim();
18
+ if (!normalizedKey) {
19
+ throw new TypeError("nestValidator requires a non-empty key.");
20
+ }
21
+
22
+ const normalizedValidator = normalizeValidator(validator);
23
+ if (!normalizedValidator) {
24
+ throw new TypeError(`nestValidator(\"${normalizedKey}\") requires a validator object with schema.`);
25
+ }
26
+
27
+ const keySchema = required ? normalizedValidator.schema : Type.Optional(normalizedValidator.schema);
28
+ const schema = Type.Object(
29
+ {
30
+ [normalizedKey]: keySchema
31
+ },
32
+ { additionalProperties: false }
33
+ );
34
+ const normalizeSection = typeof normalizedValidator.normalize === "function" ? normalizedValidator.normalize : null;
35
+
36
+ return Object.freeze({
37
+ schema,
38
+ async normalize(payload, meta) {
39
+ const source = normalizeObjectInput(payload);
40
+ if (!Object.hasOwn(source, normalizedKey)) {
41
+ return {};
42
+ }
43
+
44
+ const sectionPayload = source[normalizedKey];
45
+ const normalizedSection = normalizeSection ? await normalizeSection(sectionPayload, meta) : sectionPayload;
46
+ return {
47
+ [normalizedKey]: normalizedSection
48
+ };
49
+ }
50
+ });
51
+ }
52
+
53
+ export { nestValidator };
@@ -0,0 +1,60 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { Check } from "typebox/value";
4
+ import { Type } from "typebox";
5
+ import { nestValidator } from "./nestValidator.js";
6
+
7
+ test("nestValidator wraps schema + normalize under one key", async () => {
8
+ const baseValidator = Object.freeze({
9
+ schema: Type.Object(
10
+ {
11
+ name: Type.Optional(Type.String({ minLength: 1 }))
12
+ },
13
+ { additionalProperties: false }
14
+ ),
15
+ normalize(payload = {}) {
16
+ return {
17
+ ...(Object.hasOwn(payload, "name") ? { name: String(payload.name).trim() } : {})
18
+ };
19
+ }
20
+ });
21
+ const validator = nestValidator("payload", baseValidator);
22
+
23
+ const normalized = await validator.normalize({
24
+ payload: {
25
+ name: " Acme "
26
+ }
27
+ });
28
+
29
+ assert.deepEqual(normalized, {
30
+ payload: {
31
+ name: "Acme"
32
+ }
33
+ });
34
+ assert.equal(Check(validator.schema, normalized), true);
35
+ assert.equal(Check(validator.schema, {}), false);
36
+ });
37
+
38
+ test("nestValidator can define optional nested key", async () => {
39
+ const validator = nestValidator(
40
+ "patch",
41
+ Object.freeze({
42
+ schema: Type.Object(
43
+ {
44
+ title: Type.Optional(Type.String({ minLength: 1 }))
45
+ },
46
+ { additionalProperties: false }
47
+ )
48
+ }),
49
+ { required: false }
50
+ );
51
+
52
+ assert.deepEqual(await validator.normalize({}), {});
53
+ assert.equal(Check(validator.schema, {}), true);
54
+ assert.equal(Check(validator.schema, { patch: { title: "X" } }), true);
55
+ });
56
+
57
+ test("nestValidator rejects invalid key and validator", () => {
58
+ assert.throws(() => nestValidator("", { schema: Type.Object({}) }), /requires a non-empty key/);
59
+ assert.throws(() => nestValidator("payload", null), /requires a validator object with schema/);
60
+ });
@@ -0,0 +1,36 @@
1
+ import { Type } from "typebox";
2
+ import { normalizeObjectInput } from "./inputNormalization.js";
3
+ import { normalizePositiveInteger, normalizeText } from "../support/normalize.js";
4
+
5
+ function normalizeRecordId(value) {
6
+ return normalizePositiveInteger(normalizeText(value));
7
+ }
8
+
9
+ const positiveIntegerValidator = Object.freeze({
10
+ schema: Type.Union([
11
+ Type.Integer({ minimum: 1 }),
12
+ Type.String({ minLength: 1, pattern: "^[1-9][0-9]*$" })
13
+ ]),
14
+ normalize: normalizeRecordId
15
+ });
16
+
17
+ const recordIdParamsValidator = Object.freeze({
18
+ schema: Type.Object(
19
+ {
20
+ recordId: Type.Optional(positiveIntegerValidator.schema)
21
+ },
22
+ { additionalProperties: false }
23
+ ),
24
+ normalize(input = {}) {
25
+ const source = normalizeObjectInput(input);
26
+ const normalized = {};
27
+
28
+ if (Object.hasOwn(source, "recordId")) {
29
+ normalized.recordId = normalizeRecordId(source.recordId);
30
+ }
31
+
32
+ return normalized;
33
+ }
34
+ });
35
+
36
+ export { recordIdParamsValidator, positiveIntegerValidator };
@@ -0,0 +1,20 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { recordIdParamsValidator } from "./recordIdParamsValidator.js";
5
+
6
+ test("recordIdParamsValidator normalizes string id to positive integer", () => {
7
+ assert.deepEqual(recordIdParamsValidator.normalize({ recordId: "42" }), {
8
+ recordId: 42
9
+ });
10
+ });
11
+
12
+ test("recordIdParamsValidator normalizes invalid id to 0", () => {
13
+ assert.deepEqual(recordIdParamsValidator.normalize({ recordId: "nope" }), {
14
+ recordId: 0
15
+ });
16
+ });
17
+
18
+ test("recordIdParamsValidator keeps absent key absent", () => {
19
+ assert.deepEqual(recordIdParamsValidator.normalize({}), {});
20
+ });
@@ -0,0 +1,41 @@
1
+ function normalizeRequiredFieldList(value) {
2
+ if (!Array.isArray(value)) {
3
+ return Object.freeze([]);
4
+ }
5
+
6
+ const normalized = value
7
+ .map((entry) => String(entry || "").trim())
8
+ .filter(Boolean);
9
+
10
+ return Object.freeze(Array.from(new Set(normalized)));
11
+ }
12
+
13
+ function deriveRequiredFieldsFromSchema(schema) {
14
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
15
+ return Object.freeze([]);
16
+ }
17
+
18
+ return normalizeRequiredFieldList(schema.required);
19
+ }
20
+
21
+ function deriveResourceRequiredMetadata(resourceSchema) {
22
+ const operations = resourceSchema && typeof resourceSchema === "object"
23
+ ? resourceSchema.operations
24
+ : null;
25
+
26
+ const createSchema = operations?.create?.bodyValidator?.schema;
27
+ const replaceSchema = operations?.replace?.bodyValidator?.schema;
28
+ const patchSchema = operations?.patch?.bodyValidator?.schema;
29
+
30
+ return Object.freeze({
31
+ create: deriveRequiredFieldsFromSchema(createSchema),
32
+ replace: deriveRequiredFieldsFromSchema(replaceSchema),
33
+ patch: deriveRequiredFieldsFromSchema(patchSchema)
34
+ });
35
+ }
36
+
37
+ export {
38
+ normalizeRequiredFieldList,
39
+ deriveRequiredFieldsFromSchema,
40
+ deriveResourceRequiredMetadata
41
+ };