@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,269 @@
1
+ import { Check, Errors, Parse } from "typebox/value";
2
+ import { normalizeObject, normalizeText } from "../../shared/support/normalize.js";
3
+
4
+ function normalizeModuleId(value) {
5
+ const moduleId = normalizeText(value);
6
+ if (!moduleId) {
7
+ throw new TypeError("defineModuleConfig requires a non-empty moduleId.");
8
+ }
9
+ return moduleId;
10
+ }
11
+
12
+ function normalizeSchema(value) {
13
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
14
+ throw new TypeError("defineModuleConfig requires a schema object.");
15
+ }
16
+ return value;
17
+ }
18
+
19
+ function normalizeIssuePath(issue) {
20
+ const fromInstancePath = normalizeText(issue?.instancePath || issue?.path).replace(/^\//, "").replace(/\//g, ".");
21
+ if (fromInstancePath) {
22
+ return fromInstancePath;
23
+ }
24
+
25
+ const missingProperty = normalizeText(issue?.params?.missingProperty);
26
+ if (missingProperty) {
27
+ return missingProperty;
28
+ }
29
+
30
+ const additionalProperties = issue?.params?.additionalProperties;
31
+ if (Array.isArray(additionalProperties) && additionalProperties.length > 0) {
32
+ const firstAdditional = normalizeText(additionalProperties[0]);
33
+ if (firstAdditional) {
34
+ return firstAdditional;
35
+ }
36
+ }
37
+
38
+ const oneAdditional = normalizeText(additionalProperties);
39
+ if (oneAdditional) {
40
+ return oneAdditional;
41
+ }
42
+
43
+ return "(root)";
44
+ }
45
+
46
+ function normalizeIssueMessage(issue) {
47
+ const message = normalizeText(issue?.message || issue?.error || issue?.description);
48
+ return message || "Invalid value.";
49
+ }
50
+
51
+ function normalizeValidationIssues(rawIssues = []) {
52
+ return rawIssues.map((issue) => ({
53
+ path: normalizeIssuePath(issue),
54
+ message: normalizeIssueMessage(issue),
55
+ keyword: normalizeText(issue?.keyword)
56
+ }));
57
+ }
58
+
59
+ function formatIssues(issues = []) {
60
+ return issues
61
+ .slice(0, 8)
62
+ .map((issue) => `${issue.path}: ${issue.message}`)
63
+ .join("; ");
64
+ }
65
+
66
+ function buildInvalidConfigMessage(moduleId, issues = []) {
67
+ const summary = formatIssues(issues);
68
+ if (!summary) {
69
+ return `Invalid config for module "${moduleId}".`;
70
+ }
71
+ return `Invalid config for module "${moduleId}": ${summary}`;
72
+ }
73
+
74
+ function deepFreeze(value, seen = new WeakSet()) {
75
+ if (!value || typeof value !== "object") {
76
+ return value;
77
+ }
78
+
79
+ if (seen.has(value)) {
80
+ return value;
81
+ }
82
+ seen.add(value);
83
+
84
+ for (const key of Object.getOwnPropertyNames(value)) {
85
+ deepFreeze(value[key], seen);
86
+ }
87
+
88
+ return Object.freeze(value);
89
+ }
90
+
91
+ function normalizeCustomValidationIssues(result) {
92
+ if (result == null || result === true) {
93
+ return [];
94
+ }
95
+
96
+ if (result === false) {
97
+ return [
98
+ {
99
+ path: "(custom)",
100
+ message: "Custom module config validation failed."
101
+ }
102
+ ];
103
+ }
104
+
105
+ if (typeof result === "string") {
106
+ return [
107
+ {
108
+ path: "(custom)",
109
+ message: normalizeText(result) || "Custom module config validation failed."
110
+ }
111
+ ];
112
+ }
113
+
114
+ if (Array.isArray(result)) {
115
+ return result
116
+ .map((entry) => {
117
+ if (typeof entry === "string") {
118
+ return {
119
+ path: "(custom)",
120
+ message: normalizeText(entry) || "Custom module config validation failed."
121
+ };
122
+ }
123
+
124
+ if (!entry || typeof entry !== "object") {
125
+ return null;
126
+ }
127
+
128
+ return {
129
+ path: normalizeText(entry.path, { fallback: "(custom)" }),
130
+ message: normalizeText(entry.message, { fallback: "Custom module config validation failed." })
131
+ };
132
+ })
133
+ .filter(Boolean);
134
+ }
135
+
136
+ if (result && typeof result === "object" && Array.isArray(result.issues)) {
137
+ return normalizeCustomValidationIssues(result.issues);
138
+ }
139
+
140
+ return [
141
+ {
142
+ path: "(custom)",
143
+ message: "Custom module config validation failed."
144
+ }
145
+ ];
146
+ }
147
+
148
+ class ModuleConfigError extends Error {
149
+ constructor(message, { moduleId = "", issues = [], cause } = {}) {
150
+ super(String(message || "Module config error."));
151
+ this.name = "ModuleConfigError";
152
+ this.moduleId = normalizeText(moduleId);
153
+ this.issues = Object.freeze(Array.isArray(issues) ? [...issues] : []);
154
+ if (cause !== undefined) {
155
+ this.cause = cause;
156
+ }
157
+ }
158
+ }
159
+
160
+ function validateSchemaOrThrow({ moduleId, schema, value, coerce = false }) {
161
+ if (coerce) {
162
+ try {
163
+ return Parse(schema, value);
164
+ } catch (cause) {
165
+ const issues = normalizeValidationIssues([...Errors(schema, value)]);
166
+ throw new ModuleConfigError(buildInvalidConfigMessage(moduleId, issues), {
167
+ moduleId,
168
+ issues,
169
+ cause
170
+ });
171
+ }
172
+ }
173
+
174
+ if (Check(schema, value)) {
175
+ return value;
176
+ }
177
+
178
+ const issues = normalizeValidationIssues([...Errors(schema, value)]);
179
+ throw new ModuleConfigError(buildInvalidConfigMessage(moduleId, issues), {
180
+ moduleId,
181
+ issues
182
+ });
183
+ }
184
+
185
+ function defineModuleConfig({
186
+ moduleId,
187
+ schema,
188
+ load = ({ env }) => env,
189
+ transform = null,
190
+ validate = null,
191
+ coerce = false,
192
+ freeze = true
193
+ } = {}) {
194
+ const normalizedModuleId = normalizeModuleId(moduleId);
195
+ const normalizedSchema = normalizeSchema(schema);
196
+
197
+ if (typeof load !== "function") {
198
+ throw new TypeError(`defineModuleConfig("${normalizedModuleId}") requires load to be a function.`);
199
+ }
200
+ if (transform != null && typeof transform !== "function") {
201
+ throw new TypeError(`defineModuleConfig("${normalizedModuleId}") requires transform to be a function when provided.`);
202
+ }
203
+ if (validate != null && typeof validate !== "function") {
204
+ throw new TypeError(`defineModuleConfig("${normalizedModuleId}") requires validate to be a function when provided.`);
205
+ }
206
+
207
+ function resolve(options = {}) {
208
+ const source = normalizeObject(options);
209
+ const env = source.env && typeof source.env === "object" ? source.env : process.env;
210
+ const context = source.context;
211
+ const hasRaw = Object.prototype.hasOwnProperty.call(source, "raw");
212
+
213
+ let loadedConfig;
214
+ try {
215
+ loadedConfig = hasRaw ? source.raw : load({ env, context, moduleId: normalizedModuleId });
216
+ } catch (cause) {
217
+ throw new ModuleConfigError(`Failed to load config for module "${normalizedModuleId}".`, {
218
+ moduleId: normalizedModuleId,
219
+ cause
220
+ });
221
+ }
222
+
223
+ let candidate = loadedConfig;
224
+ if (typeof transform === "function") {
225
+ candidate = transform(candidate, { env, context, moduleId: normalizedModuleId });
226
+ }
227
+
228
+ let resolvedConfig = validateSchemaOrThrow({
229
+ moduleId: normalizedModuleId,
230
+ schema: normalizedSchema,
231
+ value: candidate,
232
+ coerce: Boolean(coerce)
233
+ });
234
+
235
+ if (typeof validate === "function") {
236
+ let customValidationResult;
237
+ try {
238
+ customValidationResult = validate(resolvedConfig, { env, context, moduleId: normalizedModuleId });
239
+ } catch (cause) {
240
+ throw new ModuleConfigError(`Invalid config for module "${normalizedModuleId}" from custom validation.`, {
241
+ moduleId: normalizedModuleId,
242
+ cause
243
+ });
244
+ }
245
+
246
+ const issues = normalizeCustomValidationIssues(customValidationResult);
247
+ if (issues.length > 0) {
248
+ throw new ModuleConfigError(buildInvalidConfigMessage(normalizedModuleId, issues), {
249
+ moduleId: normalizedModuleId,
250
+ issues
251
+ });
252
+ }
253
+ }
254
+
255
+ if (freeze) {
256
+ resolvedConfig = deepFreeze(resolvedConfig);
257
+ }
258
+
259
+ return resolvedConfig;
260
+ }
261
+
262
+ return Object.freeze({
263
+ moduleId: normalizedModuleId,
264
+ schema: normalizedSchema,
265
+ resolve
266
+ });
267
+ }
268
+
269
+ export { ModuleConfigError, defineModuleConfig };
@@ -0,0 +1,141 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { Type } from "typebox";
4
+
5
+ import { ModuleConfigError, defineModuleConfig } from "./moduleConfig.js";
6
+
7
+ test("defineModuleConfig resolves valid config and freezes nested objects", () => {
8
+ const moduleConfig = defineModuleConfig({
9
+ moduleId: "contacts",
10
+ schema: Type.Object(
11
+ {
12
+ mode: Type.Union([Type.Literal("standard"), Type.Literal("strict")]),
13
+ maxContacts: Type.Integer({ minimum: 1 }),
14
+ limits: Type.Object({
15
+ inviteExpiryHours: Type.Integer({ minimum: 1, maximum: 168 })
16
+ })
17
+ },
18
+ { additionalProperties: false }
19
+ ),
20
+ load({ env }) {
21
+ return {
22
+ mode: String(env.CONTACTS_MODE || "standard"),
23
+ maxContacts: Number(env.CONTACTS_MAX || 5000),
24
+ limits: {
25
+ inviteExpiryHours: Number(env.CONTACTS_INVITE_EXPIRY_HOURS || 24)
26
+ }
27
+ };
28
+ }
29
+ });
30
+
31
+ const config = moduleConfig.resolve({
32
+ env: {
33
+ CONTACTS_MODE: "strict",
34
+ CONTACTS_MAX: "1000",
35
+ CONTACTS_INVITE_EXPIRY_HOURS: "36"
36
+ }
37
+ });
38
+
39
+ assert.equal(config.mode, "strict");
40
+ assert.equal(config.maxContacts, 1000);
41
+ assert.equal(config.limits.inviteExpiryHours, 36);
42
+ assert.equal(Object.isFrozen(config), true);
43
+ assert.equal(Object.isFrozen(config.limits), true);
44
+ });
45
+
46
+ test("defineModuleConfig reports schema validation issues with module-scoped details", () => {
47
+ const moduleConfig = defineModuleConfig({
48
+ moduleId: "contacts",
49
+ schema: Type.Object(
50
+ {
51
+ maxContacts: Type.Integer({ minimum: 1 })
52
+ },
53
+ { additionalProperties: false }
54
+ )
55
+ });
56
+
57
+ assert.throws(
58
+ () => moduleConfig.resolve({ raw: { maxContacts: 0, unexpected: true } }),
59
+ (error) => {
60
+ assert.equal(error instanceof ModuleConfigError, true);
61
+ assert.equal(error.moduleId, "contacts");
62
+ assert.equal(error.issues.length >= 1, true);
63
+ assert.equal(String(error.message).includes('module "contacts"'), true);
64
+ return true;
65
+ }
66
+ );
67
+ });
68
+
69
+ test("defineModuleConfig supports coercion via TypeBox Parse", () => {
70
+ const moduleConfig = defineModuleConfig({
71
+ moduleId: "contacts",
72
+ coerce: true,
73
+ schema: Type.Object({
74
+ maxContacts: Type.Integer({ minimum: 1 }),
75
+ enabled: Type.Boolean()
76
+ })
77
+ });
78
+
79
+ const config = moduleConfig.resolve({
80
+ raw: {
81
+ maxContacts: "42",
82
+ enabled: "true"
83
+ }
84
+ });
85
+
86
+ assert.equal(config.maxContacts, 42);
87
+ assert.equal(config.enabled, true);
88
+ });
89
+
90
+ test("defineModuleConfig supports custom cross-field validate hook", () => {
91
+ const moduleConfig = defineModuleConfig({
92
+ moduleId: "contacts",
93
+ schema: Type.Object({
94
+ mode: Type.Union([Type.Literal("standard"), Type.Literal("strict")]),
95
+ requireAuditTrail: Type.Boolean()
96
+ }),
97
+ validate(value) {
98
+ if (value.mode === "strict" && value.requireAuditTrail !== true) {
99
+ return [
100
+ {
101
+ path: "requireAuditTrail",
102
+ message: "must be true when mode is strict"
103
+ }
104
+ ];
105
+ }
106
+ return true;
107
+ }
108
+ });
109
+
110
+ assert.throws(
111
+ () =>
112
+ moduleConfig.resolve({
113
+ raw: {
114
+ mode: "strict",
115
+ requireAuditTrail: false
116
+ }
117
+ }),
118
+ /requireAuditTrail: must be true when mode is strict/
119
+ );
120
+ });
121
+
122
+ test("defineModuleConfig rejects invalid module config definitions", () => {
123
+ assert.throws(() => defineModuleConfig({}), /moduleId/);
124
+ assert.throws(
125
+ () =>
126
+ defineModuleConfig({
127
+ moduleId: "contacts",
128
+ schema: null
129
+ }),
130
+ /schema/
131
+ );
132
+ assert.throws(
133
+ () =>
134
+ defineModuleConfig({
135
+ moduleId: "contacts",
136
+ schema: Type.Object({}),
137
+ load: "not-a-function"
138
+ }),
139
+ /load/
140
+ );
141
+ });
@@ -0,0 +1,13 @@
1
+ import { parsePositiveInteger } from "./integers.js";
2
+
3
+ function normalizePagination(pagination = {}, { defaultPage = 1, defaultPageSize = 20, maxPageSize = 100 } = {}) {
4
+ const rawPage = parsePositiveInteger(pagination?.page);
5
+ const rawPageSize = parsePositiveInteger(pagination?.pageSize);
6
+
7
+ return {
8
+ page: rawPage || defaultPage,
9
+ pageSize: Math.max(1, Math.min(maxPageSize, rawPageSize || defaultPageSize))
10
+ };
11
+ }
12
+
13
+ export { normalizePagination };
@@ -0,0 +1,21 @@
1
+ import { normalizePositiveInteger, normalizeText } from "../../shared/support/normalize.js";
2
+
3
+ function normalizePositiveIntegerOrNull(value) {
4
+ const normalized = normalizePositiveInteger(value);
5
+ if (normalized < 1) {
6
+ return null;
7
+ }
8
+
9
+ return normalized;
10
+ }
11
+
12
+ function normalizeScopeKind(value) {
13
+ const normalized = normalizeText(value).toLowerCase();
14
+ if (!normalized || normalized === "global") {
15
+ return "global";
16
+ }
17
+
18
+ return normalized;
19
+ }
20
+
21
+ export { normalizePositiveIntegerOrNull, normalizeScopeKind };
@@ -0,0 +1,38 @@
1
+ const LOCAL_BASE_URL = "http://localhost";
2
+
3
+ export function safeRequestUrl(request) {
4
+ const rawUrl = request?.raw?.url || request?.url || "/";
5
+
6
+ try {
7
+ return new URL(rawUrl, LOCAL_BASE_URL);
8
+ } catch {
9
+ return new URL("/", LOCAL_BASE_URL);
10
+ }
11
+ }
12
+
13
+ export function safePathnameFromRequest(request) {
14
+ return safeRequestUrl(request).pathname;
15
+ }
16
+
17
+ export function resolveClientIpAddress(request) {
18
+ const forwardedFor = String(request?.headers?.["x-forwarded-for"] || "").trim();
19
+ if (forwardedFor) {
20
+ const [firstHop] = forwardedFor.split(",");
21
+ const candidate = String(firstHop || "").trim();
22
+ if (candidate) {
23
+ return candidate;
24
+ }
25
+ }
26
+
27
+ const requestIp = String(request?.ip || "").trim();
28
+ if (requestIp) {
29
+ return requestIp;
30
+ }
31
+
32
+ const socketAddress = String(request?.socket?.remoteAddress || request?.raw?.socket?.remoteAddress || "").trim();
33
+ if (socketAddress) {
34
+ return socketAddress;
35
+ }
36
+
37
+ return "unknown";
38
+ }
@@ -0,0 +1,20 @@
1
+ import { AppError } from "./errors.js";
2
+ import { defaultMissingHandler } from "../support/defaultMissingHandler.js";
3
+
4
+ function normalizeIdempotencyKey(value) {
5
+ const normalized = String(value || "").trim();
6
+ return normalized || "";
7
+ }
8
+
9
+ function requireIdempotencyKey(request) {
10
+ const idempotencyKey = normalizeIdempotencyKey(request?.headers?.["idempotency-key"]);
11
+ if (!idempotencyKey) {
12
+ throw new AppError(400, "Idempotency-Key header is required.", {
13
+ code: "IDEMPOTENCY_KEY_REQUIRED"
14
+ });
15
+ }
16
+
17
+ return idempotencyKey;
18
+ }
19
+
20
+ export { defaultMissingHandler, normalizeIdempotencyKey, requireIdempotencyKey };
@@ -0,0 +1,113 @@
1
+ import { createRuntimeKernel } from "./runtimeKernel.js";
2
+
3
+ function normalizeRuntimeBundles(bundles) {
4
+ const source = Array.isArray(bundles) ? bundles : [];
5
+ return source.filter((entry) => entry && typeof entry === "object");
6
+ }
7
+
8
+ function mergeRuntimeBundles(bundles = []) {
9
+ const normalizedBundles = normalizeRuntimeBundles(bundles);
10
+
11
+ return normalizedBundles.reduce(
12
+ (accumulator, bundle) => ({
13
+ repositoryDefinitions: accumulator.repositoryDefinitions.concat(
14
+ Array.isArray(bundle.repositoryDefinitions) ? bundle.repositoryDefinitions : []
15
+ ),
16
+ serviceDefinitions: accumulator.serviceDefinitions.concat(
17
+ Array.isArray(bundle.serviceDefinitions) ? bundle.serviceDefinitions : []
18
+ ),
19
+ controllerDefinitions: accumulator.controllerDefinitions.concat(
20
+ Array.isArray(bundle.controllerDefinitions) ? bundle.controllerDefinitions : []
21
+ ),
22
+ runtimeServiceIds: accumulator.runtimeServiceIds.concat(
23
+ Array.isArray(bundle.runtimeServiceIds) ? bundle.runtimeServiceIds : []
24
+ )
25
+ }),
26
+ {
27
+ repositoryDefinitions: [],
28
+ serviceDefinitions: [],
29
+ controllerDefinitions: [],
30
+ runtimeServiceIds: []
31
+ }
32
+ );
33
+ }
34
+
35
+ function createRuntimeAssembly({
36
+ bundles = [],
37
+ dependencies = {},
38
+ repositoryDependencies = {},
39
+ serviceDependencies = {},
40
+ controllerDependencies = {}
41
+ } = {}) {
42
+ const runtimeBundle = mergeRuntimeBundles(bundles);
43
+
44
+ return createRuntimeKernel({
45
+ runtimeBundle,
46
+ dependencies,
47
+ repositoryDependencies,
48
+ serviceDependencies,
49
+ controllerDependencies
50
+ });
51
+ }
52
+
53
+ function normalizeRouteModuleDefinitions(definitions) {
54
+ const source = Array.isArray(definitions) ? definitions : [];
55
+ const normalized = [];
56
+ const seenIds = new Set();
57
+
58
+ for (const entry of source) {
59
+ const id = String(entry?.id || "").trim();
60
+ const buildRoutes = entry?.buildRoutes;
61
+ const resolveOptions = entry?.resolveOptions;
62
+
63
+ if (!id) {
64
+ throw new TypeError("Route module definition id is required.");
65
+ }
66
+ if (seenIds.has(id)) {
67
+ throw new TypeError(`Route module definition "${id}" is duplicated.`);
68
+ }
69
+ if (typeof buildRoutes !== "function") {
70
+ throw new TypeError(`Route module definition "${id}" buildRoutes must be a function.`);
71
+ }
72
+ if (resolveOptions != null && typeof resolveOptions !== "function") {
73
+ throw new TypeError(`Route module definition "${id}" resolveOptions must be a function when provided.`);
74
+ }
75
+
76
+ seenIds.add(id);
77
+ normalized.push({
78
+ id,
79
+ buildRoutes,
80
+ resolveOptions
81
+ });
82
+ }
83
+
84
+ return normalized;
85
+ }
86
+
87
+ function buildRoutesFromManifest({ definitions = [], controllers = {}, routeConfig = {}, missingHandler } = {}) {
88
+ const normalizedDefinitions = normalizeRouteModuleDefinitions(definitions);
89
+ const routeList = [];
90
+
91
+ for (const definition of normalizedDefinitions) {
92
+ const resolvedOptions = definition.resolveOptions ? definition.resolveOptions(routeConfig || {}) : {};
93
+ const routes = definition.buildRoutes(controllers || {}, {
94
+ ...(resolvedOptions && typeof resolvedOptions === "object" ? resolvedOptions : {}),
95
+ ...(missingHandler ? { missingHandler } : {})
96
+ });
97
+
98
+ if (!Array.isArray(routes)) {
99
+ throw new TypeError(`Route module definition "${definition.id}" must return an array.`);
100
+ }
101
+
102
+ routeList.push(...routes);
103
+ }
104
+
105
+ return routeList;
106
+ }
107
+
108
+ const __testables = {
109
+ normalizeRuntimeBundles,
110
+ normalizeRouteModuleDefinitions
111
+ };
112
+
113
+ export { mergeRuntimeBundles, createRuntimeAssembly, buildRoutesFromManifest, __testables };
@@ -0,0 +1,55 @@
1
+ import { createRuntimeComposition } from "./composition.js";
2
+
3
+ function normalizeDefinitions(value) {
4
+ return Array.isArray(value) ? value : [];
5
+ }
6
+
7
+ function normalizeRuntimeServiceIds(value) {
8
+ return Array.isArray(value) ? value : [];
9
+ }
10
+
11
+ function normalizeRuntimeBundle(bundle = {}) {
12
+ const source = bundle && typeof bundle === "object" ? bundle : {};
13
+
14
+ return {
15
+ repositoryDefinitions: normalizeDefinitions(source.repositoryDefinitions),
16
+ serviceDefinitions: normalizeDefinitions(source.serviceDefinitions),
17
+ controllerDefinitions: normalizeDefinitions(source.controllerDefinitions),
18
+ runtimeServiceIds: normalizeRuntimeServiceIds(source.runtimeServiceIds)
19
+ };
20
+ }
21
+
22
+ function createRuntimeKernel({
23
+ runtimeBundle,
24
+ dependencies = {},
25
+ repositoryDependencies = {},
26
+ serviceDependencies = {},
27
+ controllerDependencies = {}
28
+ } = {}) {
29
+ const sharedDependencies = dependencies && typeof dependencies === "object" ? dependencies : {};
30
+ const normalizedBundle = normalizeRuntimeBundle(runtimeBundle);
31
+
32
+ return createRuntimeComposition({
33
+ ...normalizedBundle,
34
+ repositoryDependencies: {
35
+ ...sharedDependencies,
36
+ ...(repositoryDependencies || {})
37
+ },
38
+ serviceDependencies: {
39
+ ...sharedDependencies,
40
+ ...(serviceDependencies || {})
41
+ },
42
+ controllerDependencies: {
43
+ ...sharedDependencies,
44
+ ...(controllerDependencies || {})
45
+ }
46
+ });
47
+ }
48
+
49
+ const __testables = {
50
+ normalizeDefinitions,
51
+ normalizeRuntimeServiceIds,
52
+ normalizeRuntimeBundle
53
+ };
54
+
55
+ export { normalizeRuntimeBundle, createRuntimeKernel, __testables };