@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,431 @@
1
+ import { normalizeObject, normalizePositiveInteger, normalizeText } from "../../shared/support/normalize.js";
2
+ import { isContainerToken } from "../../shared/support/tokens.js";
3
+ import { createEntityChangePublisher } from "../runtime/entityChangeEvents.js";
4
+ import {
5
+ assertTaggableApp,
6
+ normalizeNestedEntries,
7
+ registerTaggedSingleton,
8
+ resolveTaggedEntries
9
+ } from "./primitives.js";
10
+
11
+ const SERVICE_REGISTRATION_TAG = Symbol.for("jskit.runtime.services.registrations");
12
+ const ENTITY_CHANGED_EVENT_TYPE = "entity.changed";
13
+ const DEFAULT_REALTIME_AUDIENCE = "event_scope";
14
+ let SERVICE_REGISTRATION_INDEX = 0;
15
+
16
+ function normalizeMethodName(value, { context = "service method" } = {}) {
17
+ const methodName = String(value || "").trim();
18
+ if (!methodName) {
19
+ throw new TypeError(`${context} must be a non-empty string.`);
20
+ }
21
+ return methodName;
22
+ }
23
+
24
+ function createServiceRegistrationToken() {
25
+ SERVICE_REGISTRATION_INDEX += 1;
26
+ return Symbol(`jskit.runtime.services.registration.${SERVICE_REGISTRATION_INDEX}`);
27
+ }
28
+
29
+ function normalizeServiceEventType(value, { context = "service event" } = {}) {
30
+ const normalizedType = normalizeText(value).toLowerCase();
31
+ if (normalizedType !== ENTITY_CHANGED_EVENT_TYPE) {
32
+ throw new TypeError(`${context}.type must be "${ENTITY_CHANGED_EVENT_TYPE}".`);
33
+ }
34
+ return normalizedType;
35
+ }
36
+
37
+ function normalizeServiceEventOperation(value, { context = "service event" } = {}) {
38
+ if (typeof value === "function") {
39
+ return value;
40
+ }
41
+
42
+ const normalizedOperation = normalizeText(value).toLowerCase();
43
+ if (!normalizedOperation) {
44
+ throw new TypeError(`${context}.operation is required.`);
45
+ }
46
+ if (normalizedOperation !== "created" && normalizedOperation !== "updated" && normalizedOperation !== "deleted") {
47
+ throw new TypeError(`${context}.operation must be one of: created, updated, deleted.`);
48
+ }
49
+ return normalizedOperation;
50
+ }
51
+
52
+ function normalizeServiceEventEntityId(value) {
53
+ if (typeof value === "function" || typeof value === "string") {
54
+ return value;
55
+ }
56
+
57
+ const parsed = normalizePositiveInteger(value);
58
+ if (parsed > 0) {
59
+ return parsed;
60
+ }
61
+
62
+ return null;
63
+ }
64
+
65
+ function normalizeRealtimeDispatch(value, { context = "service event.realtime" } = {}) {
66
+ const source = normalizeObject(value);
67
+ if (Object.keys(source).length < 1) {
68
+ return null;
69
+ }
70
+
71
+ const event = normalizeText(source.event);
72
+ if (!event) {
73
+ throw new TypeError(`${context}.event is required.`);
74
+ }
75
+
76
+ const payload = typeof source.payload === "function" ? source.payload : null;
77
+ const audience = normalizeRealtimeAudience(source.audience, {
78
+ context: `${context}.audience`
79
+ });
80
+
81
+ return Object.freeze({
82
+ event,
83
+ payload,
84
+ audience
85
+ });
86
+ }
87
+
88
+ function normalizeRealtimeAudience(value, { context = "service event.realtime.audience" } = {}) {
89
+ if (typeof value === "undefined") {
90
+ return DEFAULT_REALTIME_AUDIENCE;
91
+ }
92
+
93
+ if (typeof value === "function") {
94
+ return value;
95
+ }
96
+
97
+ if (typeof value === "string") {
98
+ const preset = normalizeText(value).toLowerCase();
99
+ if (!preset) {
100
+ throw new TypeError(`${context} must be a non-empty string, function, or object.`);
101
+ }
102
+ return preset;
103
+ }
104
+
105
+ if (value && typeof value === "object" && !Array.isArray(value)) {
106
+ return Object.freeze({
107
+ ...value
108
+ });
109
+ }
110
+
111
+ throw new TypeError(`${context} must be a string, function, or object.`);
112
+ }
113
+
114
+ function normalizeServiceEventSpec(entry, { context = "service event" } = {}) {
115
+ const source = normalizeObject(entry);
116
+
117
+ return Object.freeze({
118
+ type: normalizeServiceEventType(source.type, { context }),
119
+ source: normalizeText(source.source),
120
+ entity: normalizeText(source.entity),
121
+ operation: normalizeServiceEventOperation(source.operation, { context }),
122
+ entityId: normalizeServiceEventEntityId(source.entityId),
123
+ realtime: normalizeRealtimeDispatch(source.realtime, { context: `${context}.realtime` })
124
+ });
125
+ }
126
+
127
+ function normalizeServiceMetadata(value = {}) {
128
+ const source = normalizeObject(value);
129
+ if (Object.hasOwn(source, "permissions")) {
130
+ throw new TypeError("service metadata.permissions is no longer supported. Define permissions on actions.");
131
+ }
132
+ const eventsSource = normalizeObject(source.events);
133
+ const events = {};
134
+
135
+ for (const [methodName, eventEntries] of Object.entries(eventsSource)) {
136
+ const normalizedMethodName = normalizeMethodName(methodName, {
137
+ context: "service metadata.events method"
138
+ });
139
+ const normalizedEventEntries = normalizeNestedEntries(eventEntries).map((entry, index) =>
140
+ normalizeServiceEventSpec(entry, {
141
+ context: `service metadata.events.${normalizedMethodName}[${index}]`
142
+ })
143
+ );
144
+ events[normalizedMethodName] = Object.freeze(normalizedEventEntries);
145
+ }
146
+
147
+ return Object.freeze({
148
+ events: Object.freeze(events)
149
+ });
150
+ }
151
+
152
+ function normalizeServiceEventsForDefinition(serviceDefinition, serviceMetadata) {
153
+ const service = normalizeObject(serviceDefinition);
154
+ const methodNameSet = new Set(
155
+ Object.entries(service)
156
+ .filter(([, value]) => typeof value === "function")
157
+ .map(([name]) => name)
158
+ );
159
+ const declaredEvents = normalizeObject(serviceMetadata.events);
160
+ const normalizedEvents = {};
161
+
162
+ for (const [methodName, events] of Object.entries(declaredEvents)) {
163
+ if (!methodNameSet.has(methodName)) {
164
+ throw new TypeError(`service metadata.events.${methodName} does not match a service method.`);
165
+ }
166
+
167
+ const normalizedEntries = normalizeNestedEntries(events).map((entry, index) =>
168
+ normalizeServiceEventSpec(entry, {
169
+ context: `service metadata.events.${methodName}[${index}]`
170
+ })
171
+ );
172
+
173
+ for (const [index, entry] of normalizedEntries.entries()) {
174
+ if (!entry.source || !entry.entity) {
175
+ throw new TypeError(
176
+ `service metadata.events.${methodName}[${index}] requires source and entity for "${ENTITY_CHANGED_EVENT_TYPE}".`
177
+ );
178
+ }
179
+ }
180
+
181
+ normalizedEvents[methodName] = Object.freeze(normalizedEntries);
182
+ }
183
+
184
+ return Object.freeze(normalizedEvents);
185
+ }
186
+
187
+ function resolveMethodOptions(args = []) {
188
+ if (!Array.isArray(args) || args.length < 1) {
189
+ return {};
190
+ }
191
+
192
+ const candidate = args[args.length - 1];
193
+ if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
194
+ return candidate;
195
+ }
196
+
197
+ return {};
198
+ }
199
+
200
+ function resolveEventOperation(spec, state) {
201
+ if (typeof spec.operation === "function") {
202
+ return spec.operation({
203
+ result: state.result,
204
+ args: state.args,
205
+ options: state.options,
206
+ methodName: state.methodName,
207
+ serviceToken: state.serviceToken
208
+ });
209
+ }
210
+ return spec.operation;
211
+ }
212
+
213
+ function resolveEventEntityId(spec, state) {
214
+ if (typeof spec.entityId === "function") {
215
+ return spec.entityId({
216
+ result: state.result,
217
+ args: state.args,
218
+ options: state.options,
219
+ methodName: state.methodName,
220
+ serviceToken: state.serviceToken
221
+ });
222
+ }
223
+
224
+ if (typeof spec.entityId === "string") {
225
+ return state?.result?.[spec.entityId];
226
+ }
227
+
228
+ if (Number.isInteger(spec.entityId) && spec.entityId > 0) {
229
+ return spec.entityId;
230
+ }
231
+
232
+ return state?.result?.id;
233
+ }
234
+
235
+ function resolveEventMeta(spec, state) {
236
+ const meta = {
237
+ service: Object.freeze({
238
+ token: state.serviceToken,
239
+ method: state.methodName
240
+ })
241
+ };
242
+
243
+ if (spec.realtime) {
244
+ meta.realtime = spec.realtime.payload
245
+ ? Object.freeze({
246
+ event: spec.realtime.event,
247
+ payload: spec.realtime.payload({
248
+ event: state.event,
249
+ result: state.result,
250
+ args: state.args,
251
+ options: state.options,
252
+ methodName: state.methodName,
253
+ serviceToken: state.serviceToken
254
+ })
255
+ })
256
+ : Object.freeze({
257
+ event: spec.realtime.event
258
+ });
259
+ }
260
+
261
+ return Object.freeze(meta);
262
+ }
263
+
264
+ function createServiceMethodEventPublisher(scope, serviceToken, methodName, specs = []) {
265
+ if (!scope || typeof scope.make !== "function" || typeof scope.has !== "function") {
266
+ throw new TypeError("service event publisher requires a scope with has()/make().");
267
+ }
268
+ if (specs.length < 1) {
269
+ return async function publishNoop() {
270
+ return null;
271
+ };
272
+ }
273
+ if (!scope.has("domainEvents")) {
274
+ throw new Error(`app.service(${String(serviceToken)}) requires "domainEvents" binding to emit service events.`);
275
+ }
276
+
277
+ const domainEvents = scope.make("domainEvents");
278
+ const publishers = specs.map((spec) =>
279
+ createEntityChangePublisher({
280
+ domainEvents,
281
+ source: spec.source,
282
+ entity: spec.entity
283
+ })
284
+ );
285
+
286
+ return async function publishServiceMethodEvents(state) {
287
+ for (const [index, spec] of specs.entries()) {
288
+ const publisher = publishers[index];
289
+ const operation = resolveEventOperation(spec, state);
290
+ const entityId = resolveEventEntityId(spec, state);
291
+ const eventMeta = resolveEventMeta(spec, {
292
+ ...state,
293
+ event: spec
294
+ });
295
+ await publisher(operation, entityId, state.options, eventMeta);
296
+ }
297
+ return null;
298
+ };
299
+ }
300
+
301
+ function materializeServiceRegistration(scope, registrationSpec) {
302
+ const service = registrationSpec.factory(scope);
303
+ const normalizedService = normalizeObject(service);
304
+ const events = normalizeServiceEventsForDefinition(normalizedService, registrationSpec.metadata);
305
+ const wrappedService = {};
306
+
307
+ for (const [methodName, method] of Object.entries(normalizedService)) {
308
+ if (typeof method !== "function") {
309
+ continue;
310
+ }
311
+
312
+ const methodEvents = events[methodName] || [];
313
+ const eventPublisher = createServiceMethodEventPublisher(
314
+ scope,
315
+ registrationSpec.serviceToken,
316
+ methodName,
317
+ methodEvents
318
+ );
319
+
320
+ wrappedService[methodName] = function registeredServiceMethod(...args) {
321
+ if (methodEvents.length < 1) {
322
+ return method(...args);
323
+ }
324
+
325
+ const options = resolveMethodOptions(args);
326
+ const result = method(...args);
327
+ const publish = (resolvedResult) =>
328
+ eventPublisher({
329
+ result: resolvedResult,
330
+ args,
331
+ options,
332
+ methodName,
333
+ serviceToken: registrationSpec.serviceToken
334
+ }).then(() => resolvedResult);
335
+
336
+ if (result && typeof result.then === "function") {
337
+ return result.then((resolvedResult) => publish(resolvedResult));
338
+ }
339
+
340
+ return publish(result);
341
+ };
342
+ }
343
+
344
+ Object.defineProperty(wrappedService, "serviceEvents", {
345
+ enumerable: false,
346
+ configurable: false,
347
+ writable: false,
348
+ value: events
349
+ });
350
+ Object.defineProperty(wrappedService, "serviceToken", {
351
+ enumerable: false,
352
+ configurable: false,
353
+ writable: false,
354
+ value: registrationSpec.serviceToken
355
+ });
356
+
357
+ return Object.freeze(wrappedService);
358
+ }
359
+
360
+ function normalizeServiceRegistration(value = {}) {
361
+ const source = normalizeObject(value);
362
+ if (!isContainerToken(source.serviceToken)) {
363
+ throw new TypeError("app.service requires a valid service token.");
364
+ }
365
+ if (typeof source.factory !== "function") {
366
+ throw new TypeError("app.service requires a factory function.");
367
+ }
368
+
369
+ return Object.freeze({
370
+ serviceToken: source.serviceToken,
371
+ factory: source.factory,
372
+ metadata: normalizeServiceMetadata(source.metadata)
373
+ });
374
+ }
375
+
376
+ function registerServiceRegistration(app, token, factory) {
377
+ registerTaggedSingleton(app, token, factory, SERVICE_REGISTRATION_TAG, {
378
+ context: "registerServiceRegistration"
379
+ });
380
+ }
381
+
382
+ function resolveServiceRegistrations(scope) {
383
+ return resolveTaggedEntries(scope, SERVICE_REGISTRATION_TAG)
384
+ .map((entry) => normalizeObject(entry))
385
+ .filter((entry) => Object.keys(entry).length > 0)
386
+ .sort((left, right) => String(left.serviceToken || "").localeCompare(String(right.serviceToken || "")));
387
+ }
388
+
389
+ function installServiceRegistrationApi(app) {
390
+ assertTaggableApp(app, {
391
+ context: "installServiceRegistrationApi"
392
+ });
393
+ if (typeof app.service === "function") {
394
+ return;
395
+ }
396
+
397
+ const registerService = function registerService(serviceToken, factory, metadata = {}) {
398
+ const registration = normalizeServiceRegistration({
399
+ serviceToken,
400
+ factory,
401
+ metadata
402
+ });
403
+
404
+ this.singleton(registration.serviceToken, (scope) => materializeServiceRegistration(scope, registration));
405
+
406
+ const registrationToken = createServiceRegistrationToken();
407
+ registerServiceRegistration(this, registrationToken, () =>
408
+ Object.freeze({
409
+ serviceToken: registration.serviceToken,
410
+ metadata: registration.metadata
411
+ })
412
+ );
413
+
414
+ return this;
415
+ };
416
+
417
+ Object.defineProperty(app, "service", {
418
+ configurable: true,
419
+ writable: true,
420
+ value: registerService
421
+ });
422
+ }
423
+
424
+ export {
425
+ SERVICE_REGISTRATION_TAG,
426
+ normalizeServiceRegistration,
427
+ materializeServiceRegistration,
428
+ registerServiceRegistration,
429
+ resolveServiceRegistrations,
430
+ installServiceRegistrationApi
431
+ };
@@ -0,0 +1,65 @@
1
+ import * as apiRouteRegistration from "./apiRouteRegistration.js";
2
+ import * as bootstrapContributors from "../registries/bootstrapPayloadContributorRegistry.js";
3
+ import * as bootstrapRoutes from "./bootBootstrapRoutes.js";
4
+ import * as canonicalJson from "./canonicalJson.js";
5
+ import * as composition from "./composition.js";
6
+ import * as domainEvents from "../registries/domainEventListenerRegistry.js";
7
+ import * as errors from "./errors.js";
8
+ import * as fastifyBootstrap from "./fastifyBootstrap.js";
9
+ import * as integers from "./integers.js";
10
+ import * as pagination from "./pagination.js";
11
+ import * as realtimeNormalization from "./realtimeNormalization.js";
12
+ import * as requestUrl from "./requestUrl.js";
13
+ import * as routeUtils from "./routeUtils.js";
14
+ import * as runtimeAssembly from "./runtimeAssembly.js";
15
+ import * as runtimeKernel from "./runtimeKernel.js";
16
+ import * as serviceAuthorization from "./serviceAuthorization.js";
17
+ import * as serviceRegistration from "../registries/serviceRegistrationRegistry.js";
18
+ import * as entityChangeEvents from "./entityChangeEvents.js";
19
+ import * as securityAudit from "./securityAudit.js";
20
+ import { KERNEL_TOKENS } from "../../shared/support/tokens.js";
21
+
22
+ const SERVER_RUNTIME_CORE_API = Object.freeze({
23
+ apiRouteRegistration: Object.freeze({ ...apiRouteRegistration }),
24
+ bootstrapContributors: Object.freeze({ ...bootstrapContributors }),
25
+ bootstrapRoutes: Object.freeze({ ...bootstrapRoutes }),
26
+ canonicalJson: Object.freeze({ ...canonicalJson }),
27
+ composition: Object.freeze({ ...composition }),
28
+ domainEvents: Object.freeze({ ...domainEvents }),
29
+ errors: Object.freeze({ ...errors }),
30
+ fastifyBootstrap: Object.freeze({ ...fastifyBootstrap }),
31
+ integers: Object.freeze({ ...integers }),
32
+ pagination: Object.freeze({ ...pagination }),
33
+ realtimeNormalization: Object.freeze({ ...realtimeNormalization }),
34
+ requestUrl: Object.freeze({ ...requestUrl }),
35
+ routeUtils: Object.freeze({ ...routeUtils }),
36
+ runtimeAssembly: Object.freeze({ ...runtimeAssembly }),
37
+ runtimeKernel: Object.freeze({ ...runtimeKernel }),
38
+ serviceAuthorization: Object.freeze({ ...serviceAuthorization }),
39
+ serviceRegistration: Object.freeze({ ...serviceRegistration }),
40
+ entityChangeEvents: Object.freeze({ ...entityChangeEvents }),
41
+ securityAudit: Object.freeze({ ...securityAudit })
42
+ });
43
+
44
+ class ServerRuntimeCoreServiceProvider {
45
+ static id = "runtime.server";
46
+
47
+ register(app) {
48
+ if (!app || typeof app.singleton !== "function" || typeof app.has !== "function") {
49
+ throw new Error("ServerRuntimeCoreServiceProvider requires application singleton()/has().");
50
+ }
51
+
52
+ serviceRegistration.installServiceRegistrationApi(app);
53
+
54
+ app.singleton("runtime.server", () => SERVER_RUNTIME_CORE_API);
55
+ app.singleton("domainEvents", (scope) => domainEvents.createDomainEvents(scope));
56
+ }
57
+
58
+ boot(app) {
59
+ if (app.has(KERNEL_TOKENS.HttpRouter)) {
60
+ bootstrapRoutes.bootBootstrapRoutes(app);
61
+ }
62
+ }
63
+ }
64
+
65
+ export { ServerRuntimeCoreServiceProvider };
@@ -0,0 +1,53 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createContainer } from "../container/index.js";
4
+ import { registerDomainEventListener } from "../registries/domainEventListenerRegistry.js";
5
+ import { ServerRuntimeCoreServiceProvider } from "./ServerRuntimeCoreServiceProvider.js";
6
+
7
+ test("ServerRuntimeCoreServiceProvider registers runtime.server and default domainEvents", () => {
8
+ const app = createContainer();
9
+ const provider = new ServerRuntimeCoreServiceProvider();
10
+ provider.register(app);
11
+
12
+ assert.equal(typeof app.service, "function");
13
+
14
+ const runtimeServer = app.make("runtime.server");
15
+ assert.equal(typeof runtimeServer, "object");
16
+
17
+ const domainEvents = app.make("domainEvents");
18
+ assert.equal(typeof domainEvents.publish, "function");
19
+ });
20
+
21
+ test("ServerRuntimeCoreServiceProvider default domainEvents dispatches registered listeners", async () => {
22
+ const app = createContainer();
23
+ const provider = new ServerRuntimeCoreServiceProvider();
24
+ provider.register(app);
25
+
26
+ const received = [];
27
+ registerDomainEventListener(app, "test.domainEvents.alpha", () => ({
28
+ listenerId: "alpha",
29
+ async handle(payload) {
30
+ received.push(payload);
31
+ }
32
+ }));
33
+
34
+ const domainEvents = app.make("domainEvents");
35
+ const eventPayload = {
36
+ source: "test"
37
+ };
38
+ const result = await domainEvents.publish(eventPayload);
39
+
40
+ assert.equal(result, null);
41
+ assert.deepEqual(received, [eventPayload]);
42
+ });
43
+
44
+ test("ServerRuntimeCoreServiceProvider owns domainEvents binding", async () => {
45
+ const app = createContainer();
46
+ app.singleton("domainEvents", () => ({ publish: async () => "existing" }));
47
+
48
+ const provider = new ServerRuntimeCoreServiceProvider();
49
+ assert.throws(
50
+ () => provider.register(app),
51
+ /Token "domainEvents" is already bound\./
52
+ );
53
+ });
@@ -0,0 +1,109 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ defaultApplyRoutePolicy as applyHttpRoutePolicy,
5
+ normalizeRoutePolicyConfig as normalizeHttpRoutePolicyConfig
6
+ } from "../http/lib/kernel.js";
7
+ import { __testables as runtimeRouteRegistrationTestables } from "./apiRouteRegistration.js";
8
+ import {
9
+ defaultApplyRoutePolicy as applyCanonicalRoutePolicy,
10
+ normalizeRoutePolicyConfig as normalizeCanonicalRoutePolicyConfig
11
+ } from "../support/routePolicyConfig.js";
12
+
13
+ test("route policy mapping is sourced from one canonical mapper in both registration paths", () => {
14
+ assert.equal(applyHttpRoutePolicy, applyCanonicalRoutePolicy);
15
+ assert.equal(runtimeRouteRegistrationTestables.defaultApplyRoutePolicy, applyCanonicalRoutePolicy);
16
+ assert.equal(normalizeHttpRoutePolicyConfig, normalizeCanonicalRoutePolicyConfig);
17
+ assert.equal(runtimeRouteRegistrationTestables.normalizeRoutePolicyConfig, normalizeCanonicalRoutePolicyConfig);
18
+ });
19
+
20
+ test("route policy parity matrix stays aligned across http/runtime registration", () => {
21
+ const ownerResolver = () => 42;
22
+
23
+ const matrix = [
24
+ {
25
+ label: "auth maps to authPolicy",
26
+ route: { auth: "required" },
27
+ expectedConfig: { seed: "x", authPolicy: "required" }
28
+ },
29
+ {
30
+ label: "contextPolicy maps through",
31
+ route: { contextPolicy: "required" },
32
+ expectedConfig: { seed: "x", contextPolicy: "required" }
33
+ },
34
+ {
35
+ label: "surface maps through",
36
+ route: { surface: "admin" },
37
+ expectedConfig: { seed: "x", surface: "admin" }
38
+ },
39
+ {
40
+ label: "visibility is normalized",
41
+ route: { visibility: "USER" },
42
+ expectedConfig: { seed: "x", visibility: "user" }
43
+ },
44
+ {
45
+ label: "permission maps through",
46
+ route: { permission: "workspace.settings.read" },
47
+ expectedConfig: { seed: "x", permission: "workspace.settings.read" }
48
+ },
49
+ {
50
+ label: "ownerParam maps through",
51
+ route: { ownerParam: "userId" },
52
+ expectedConfig: { seed: "x", ownerParam: "userId" }
53
+ },
54
+ {
55
+ label: "userField maps through",
56
+ route: { userField: "accountId" },
57
+ expectedConfig: { seed: "x", userField: "accountId" }
58
+ },
59
+ {
60
+ label: "ownerResolver maps through",
61
+ route: { ownerResolver },
62
+ expectedConfig: { seed: "x", ownerResolver }
63
+ },
64
+ {
65
+ label: "csrfProtection maps through",
66
+ route: { csrfProtection: false },
67
+ expectedConfig: { seed: "x", csrfProtection: false }
68
+ },
69
+ {
70
+ label: "route fields override existing config values",
71
+ routeOptionsConfig: { authPolicy: "public", visibility: "public", seed: "x" },
72
+ route: { auth: "own", visibility: "WORKSPACE" },
73
+ expectedConfig: { seed: "x", authPolicy: "own", visibility: "workspace" }
74
+ },
75
+ {
76
+ label: "unmapped route fields do not alter config",
77
+ route: { rateLimit: { max: 1, timeWindow: "1 second" } },
78
+ expectedConfig: { seed: "x" }
79
+ }
80
+ ];
81
+
82
+ for (const testCase of matrix) {
83
+ const routeOptions = {
84
+ method: "GET",
85
+ url: "/demo",
86
+ config: {
87
+ seed: "x",
88
+ ...(testCase.routeOptionsConfig || {})
89
+ }
90
+ };
91
+ const route = {
92
+ method: "GET",
93
+ path: "/demo",
94
+ ...(testCase.route || {})
95
+ };
96
+
97
+ const canonical = applyCanonicalRoutePolicy(routeOptions, route);
98
+ const fromHttp = applyHttpRoutePolicy(routeOptions, route);
99
+ const fromRuntime = runtimeRouteRegistrationTestables.defaultApplyRoutePolicy(routeOptions, route);
100
+
101
+ assert.deepEqual(
102
+ canonical.config,
103
+ testCase.expectedConfig,
104
+ `canonical mapping failed for case: ${testCase.label}`
105
+ );
106
+ assert.deepEqual(fromHttp, canonical, `http mapping drifted for case: ${testCase.label}`);
107
+ assert.deepEqual(fromRuntime, canonical, `runtime mapping drifted for case: ${testCase.label}`);
108
+ }
109
+ });