@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,372 @@
1
+ import { KERNEL_TOKENS } from "../../shared/support/tokens.js";
2
+ import { ActionRuntimeError } from "../../shared/actions/actionDefinitions.js";
3
+ import { normalizeOpaqueId } from "../../shared/support/normalize.js";
4
+ import { isAppError } from "./errors.js";
5
+ import { resolveDefaultSurfaceId } from "../support/appConfig.js";
6
+
7
+ const API_ERROR_HANDLER_MARKER_TOKEN = "kernel.runtime.apiErrorHandlerRegistered";
8
+
9
+ function resolveLoggerLevel({ configuredLevel = "", nodeEnv = "development", allowedLevels = [] } = {}) {
10
+ const normalizedConfiguredLevel = String(configuredLevel || "")
11
+ .trim()
12
+ .toLowerCase();
13
+ const allowedLevelSet =
14
+ allowedLevels instanceof Set
15
+ ? new Set(allowedLevels)
16
+ : new Set(Array.isArray(allowedLevels) ? allowedLevels : []);
17
+ if (allowedLevelSet.has(normalizedConfiguredLevel)) {
18
+ return normalizedConfiguredLevel;
19
+ }
20
+
21
+ return String(nodeEnv || "").trim().toLowerCase() === "production" ? "info" : "debug";
22
+ }
23
+
24
+ function createFastifyLoggerOptions({
25
+ configuredLevel = "",
26
+ nodeEnv = "development",
27
+ allowedLevels = [],
28
+ redactPaths = [],
29
+ redactCensor = "[REDACTED]"
30
+ } = {}) {
31
+ return {
32
+ level: resolveLoggerLevel({
33
+ configuredLevel,
34
+ nodeEnv,
35
+ allowedLevels
36
+ }),
37
+ redact: {
38
+ paths: Array.isArray(redactPaths) ? redactPaths : [],
39
+ censor: redactCensor
40
+ }
41
+ };
42
+ }
43
+
44
+ function registerRequestLoggingHooks(
45
+ app,
46
+ {
47
+ requestStartedAtSymbol,
48
+ getPathname,
49
+ getSurface,
50
+ observeRequest,
51
+ enableRequestLogs = true,
52
+ defaultSurfaceId = ""
53
+ } = {}
54
+ ) {
55
+ if (!app || typeof app.addHook !== "function") {
56
+ throw new TypeError("registerRequestLoggingHooks requires a Fastify instance.");
57
+ }
58
+
59
+ const startedAtSymbol = requestStartedAtSymbol || Symbol("request_started_at_ns");
60
+ const resolvePathname = typeof getPathname === "function" ? getPathname : () => "/";
61
+ const resolveSurface =
62
+ typeof getSurface === "function"
63
+ ? getSurface
64
+ : () =>
65
+ resolveDefaultSurfaceId(null, {
66
+ defaultSurfaceId
67
+ });
68
+
69
+ app.addHook("onRequest", async (request) => {
70
+ request[startedAtSymbol] = process.hrtime.bigint();
71
+ });
72
+
73
+ app.addHook("onResponse", async (request, reply) => {
74
+ const startedAt = request[startedAtSymbol];
75
+ const durationMs =
76
+ typeof startedAt === "bigint" ? Number(process.hrtime.bigint() - startedAt) / 1_000_000 : Number.NaN;
77
+ const pathnameValue = resolvePathname(request);
78
+ const routeUrl = String(request?.routeOptions?.url || "").trim();
79
+ const surface = resolveSurface(pathnameValue, request);
80
+ const actorId = normalizeOpaqueId(request?.user?.id);
81
+ const logPayload = {
82
+ requestId: String(request?.id || ""),
83
+ method: String(request?.method || ""),
84
+ path: pathnameValue,
85
+ routeUrl,
86
+ surface,
87
+ statusCode: Number(reply?.statusCode || 0),
88
+ durationMs: Number.isFinite(durationMs) ? Number(durationMs.toFixed(3)) : null
89
+ };
90
+
91
+ if (actorId != null) {
92
+ logPayload.actorId = actorId;
93
+ }
94
+
95
+ if (typeof observeRequest === "function") {
96
+ observeRequest({
97
+ method: logPayload.method,
98
+ route: routeUrl || pathnameValue,
99
+ surface,
100
+ statusCode: logPayload.statusCode,
101
+ durationMs
102
+ });
103
+ }
104
+
105
+ if (enableRequestLogs) {
106
+ request.log.info(logPayload, "request.completed");
107
+ }
108
+ });
109
+ }
110
+
111
+ function resolveValidationFieldErrors(error) {
112
+ const issues = Array.isArray(error?.validation) ? error.validation : [];
113
+ const fieldErrors = {};
114
+
115
+ for (const issue of issues) {
116
+ const fieldFromPath = String(issue.instancePath || "")
117
+ .replace(/^\//, "")
118
+ .replace(/\//g, ".");
119
+ const field =
120
+ fieldFromPath || String(issue.params?.missingProperty || issue.params?.additionalProperty || "request").trim();
121
+
122
+ if (!fieldErrors[field]) {
123
+ fieldErrors[field] = issue.message || "Invalid value.";
124
+ }
125
+ }
126
+
127
+ return fieldErrors;
128
+ }
129
+
130
+ function registerApiErrorHandler(
131
+ app,
132
+ {
133
+ isAppError,
134
+ onRecordDbError,
135
+ onCaptureServerError,
136
+ appErrorLogMessage = "AppError 5xx",
137
+ unhandledErrorLogMessage = "Unhandled error"
138
+ } = {}
139
+ ) {
140
+ if (!app || typeof app.setErrorHandler !== "function") {
141
+ throw new TypeError("registerApiErrorHandler requires a Fastify instance.");
142
+ }
143
+ if (typeof isAppError !== "function") {
144
+ throw new TypeError("registerApiErrorHandler requires isAppError.");
145
+ }
146
+
147
+ const recordDbError = typeof onRecordDbError === "function" ? onRecordDbError : () => {};
148
+ const captureServerError = typeof onCaptureServerError === "function" ? onCaptureServerError : () => {};
149
+
150
+ app.setErrorHandler((error, request, reply) => {
151
+ const normalizedErrorCode = String(error?.code || "").trim();
152
+ const isCsrfErrorCode = normalizedErrorCode.startsWith("FST_CSRF_");
153
+ const statusFromError = Number(error?.statusCode || error?.status);
154
+ const statusCode =
155
+ Number.isInteger(statusFromError) && statusFromError >= 400 && statusFromError <= 599 ? statusFromError : 500;
156
+
157
+ if (Array.isArray(error?.validation)) {
158
+ const fieldErrors = resolveValidationFieldErrors(error);
159
+ const validationErrorCode = normalizedErrorCode || "validation_failed";
160
+ reply.code(400).send({
161
+ error: "Validation failed.",
162
+ code: validationErrorCode,
163
+ fieldErrors,
164
+ details: {
165
+ fieldErrors
166
+ }
167
+ });
168
+ return;
169
+ }
170
+
171
+ if (isAppError(error) || error instanceof ActionRuntimeError) {
172
+ if (error.status >= 500) {
173
+ recordDbError(error);
174
+ captureServerError(request, error, error.status);
175
+ app.log.error({ err: error }, appErrorLogMessage);
176
+ }
177
+
178
+ const appErrorCode = normalizedErrorCode || "app_error";
179
+ const payload = {
180
+ error: error.message,
181
+ code: appErrorCode
182
+ };
183
+ if (error.details) {
184
+ payload.details = error.details;
185
+ if (error.details.fieldErrors) {
186
+ payload.fieldErrors = error.details.fieldErrors;
187
+ }
188
+ }
189
+
190
+ if (error.headers && typeof error.headers === "object") {
191
+ Object.entries(error.headers).forEach(([name, value]) => {
192
+ reply.header(name, value);
193
+ });
194
+ }
195
+
196
+ reply.code(error.status).send(payload);
197
+ return;
198
+ }
199
+
200
+ if (error.headers && typeof error.headers === "object") {
201
+ Object.entries(error.headers).forEach(([name, value]) => {
202
+ reply.header(name, value);
203
+ });
204
+ }
205
+
206
+ if (statusCode >= 500) {
207
+ recordDbError(error);
208
+ }
209
+ captureServerError(request, error, statusCode);
210
+ app.log.error({ err: error }, unhandledErrorLogMessage);
211
+
212
+ const message = statusCode >= 500 ? "Internal server error." : String(error?.message || "Request failed.");
213
+ const fallbackErrorCode =
214
+ normalizedErrorCode || (statusCode >= 500 ? "internal_server_error" : "request_failed");
215
+ const payload = {
216
+ error: message,
217
+ code: fallbackErrorCode
218
+ };
219
+ if (isCsrfErrorCode) {
220
+ payload.details = {
221
+ code: normalizedErrorCode
222
+ };
223
+ }
224
+ reply.code(statusCode).send(payload);
225
+ });
226
+ }
227
+
228
+ function ensureApiErrorHandling(
229
+ app,
230
+ {
231
+ fastifyToken = KERNEL_TOKENS.Fastify,
232
+ markerToken = API_ERROR_HANDLER_MARKER_TOKEN,
233
+ isAppError: isAppErrorOverride,
234
+ autoRegister = true,
235
+ ...handlerOptions
236
+ } = {}
237
+ ) {
238
+ if (!app || typeof app.make !== "function" || typeof app.has !== "function" || typeof app.instance !== "function") {
239
+ throw new TypeError("ensureApiErrorHandling requires an application instance.");
240
+ }
241
+
242
+ if (autoRegister === false) {
243
+ return false;
244
+ }
245
+
246
+ const normalizedMarkerToken = String(markerToken || "").trim() || API_ERROR_HANDLER_MARKER_TOKEN;
247
+ if (app.has(normalizedMarkerToken)) {
248
+ return false;
249
+ }
250
+
251
+ const fastify = app.make(fastifyToken);
252
+ if (!fastify || typeof fastify.setErrorHandler !== "function") {
253
+ throw new TypeError("ensureApiErrorHandling requires a Fastify instance.");
254
+ }
255
+
256
+ const appErrorPredicate = typeof isAppErrorOverride === "function" ? isAppErrorOverride : isAppError;
257
+ registerApiErrorHandler(fastify, {
258
+ ...handlerOptions,
259
+ isAppError: appErrorPredicate
260
+ });
261
+ app.instance(normalizedMarkerToken, true);
262
+
263
+ return true;
264
+ }
265
+
266
+ function resolveDatabaseErrorCode(error) {
267
+ const errorCode = String(error?.code || "")
268
+ .trim()
269
+ .toUpperCase();
270
+ if (errorCode && (errorCode.startsWith("ER_") || errorCode.startsWith("SQLITE_") || errorCode.startsWith("PG"))) {
271
+ return errorCode;
272
+ }
273
+
274
+ const sqlState = String(error?.sqlState || error?.sqlstate || "")
275
+ .trim()
276
+ .toUpperCase();
277
+ if (sqlState) {
278
+ return `SQLSTATE_${sqlState}`;
279
+ }
280
+
281
+ const errno = Number(error?.errno);
282
+ if (Number.isInteger(errno)) {
283
+ return `ERRNO_${errno}`;
284
+ }
285
+
286
+ const message = String(error?.message || "").toLowerCase();
287
+ const name = String(error?.name || "").toLowerCase();
288
+ if (message.includes("mysql") || message.includes("sql") || message.includes("knex") || name.includes("mysql")) {
289
+ return "DB_UNKNOWN";
290
+ }
291
+
292
+ return "";
293
+ }
294
+
295
+ function recordDbErrorBestEffort(observabilityService, error) {
296
+ if (!observabilityService || typeof observabilityService.recordDbError !== "function") {
297
+ return;
298
+ }
299
+
300
+ const code = resolveDatabaseErrorCode(error);
301
+ if (!code) {
302
+ return;
303
+ }
304
+
305
+ observabilityService.recordDbError({ code });
306
+ }
307
+
308
+ async function runGracefulShutdown({
309
+ signal = "",
310
+ exitProcess = false,
311
+ exitCode = 0,
312
+ timeoutMs = 10_000,
313
+ appInstance = null,
314
+ stopBackgroundRuntimes = () => {},
315
+ closeDatabase = async () => {},
316
+ logger = console
317
+ } = {}) {
318
+ let forcedExitTimer = null;
319
+
320
+ if (signal) {
321
+ logger.log(`Received ${signal}. Shutting down.`);
322
+ }
323
+
324
+ stopBackgroundRuntimes();
325
+
326
+ if (exitProcess) {
327
+ forcedExitTimer = setTimeout(() => {
328
+ try {
329
+ appInstance?.server?.closeIdleConnections?.();
330
+ appInstance?.server?.closeAllConnections?.();
331
+ } catch {
332
+ // Ignore best-effort force-close failures.
333
+ }
334
+
335
+ logger.error(`Graceful shutdown timed out after ${timeoutMs}ms. Forcing process exit.`);
336
+ process.exit(1);
337
+ }, timeoutMs);
338
+ forcedExitTimer.unref?.();
339
+ }
340
+
341
+ try {
342
+ if (appInstance) {
343
+ await appInstance.close();
344
+ }
345
+ await closeDatabase();
346
+ } catch (error) {
347
+ logger.error("Failed to close server cleanly:", error);
348
+ if (exitProcess) {
349
+ process.exit(1);
350
+ }
351
+ throw error;
352
+ } finally {
353
+ if (forcedExitTimer) {
354
+ clearTimeout(forcedExitTimer);
355
+ }
356
+ }
357
+
358
+ if (exitProcess) {
359
+ process.exit(exitCode);
360
+ }
361
+ }
362
+
363
+ export {
364
+ resolveLoggerLevel,
365
+ createFastifyLoggerOptions,
366
+ registerRequestLoggingHooks,
367
+ registerApiErrorHandler,
368
+ ensureApiErrorHandling,
369
+ resolveDatabaseErrorCode,
370
+ recordDbErrorBestEffort,
371
+ runGracefulShutdown
372
+ };
@@ -0,0 +1,194 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { AppError, isAppError } from "./errors.js";
5
+ import { registerApiErrorHandler, registerRequestLoggingHooks } from "./fastifyBootstrap.js";
6
+
7
+ function createFastifyStub() {
8
+ return {
9
+ errorHandler: null,
10
+ hooks: {},
11
+ log: {
12
+ error() {}
13
+ },
14
+ setErrorHandler(handler) {
15
+ this.errorHandler = handler;
16
+ },
17
+ addHook(name, handler) {
18
+ this.hooks[name] = handler;
19
+ }
20
+ };
21
+ }
22
+
23
+ function createReplyStub() {
24
+ return {
25
+ statusCode: 200,
26
+ payload: undefined,
27
+ headers: {},
28
+ code(value) {
29
+ this.statusCode = Number(value);
30
+ return this;
31
+ },
32
+ header(name, value) {
33
+ this.headers[name] = value;
34
+ return this;
35
+ },
36
+ send(payload) {
37
+ this.payload = payload;
38
+ return this;
39
+ }
40
+ };
41
+ }
42
+
43
+ test("registerApiErrorHandler includes code for validation errors", () => {
44
+ const fastify = createFastifyStub();
45
+ registerApiErrorHandler(fastify, { isAppError });
46
+
47
+ const reply = createReplyStub();
48
+ fastify.errorHandler(
49
+ {
50
+ validation: [
51
+ {
52
+ instancePath: "/email",
53
+ message: "must match format email"
54
+ }
55
+ ]
56
+ },
57
+ {},
58
+ reply
59
+ );
60
+
61
+ assert.equal(reply.statusCode, 400);
62
+ assert.equal(reply.payload.code, "validation_failed");
63
+ assert.deepEqual(reply.payload.fieldErrors, {
64
+ email: "must match format email"
65
+ });
66
+ });
67
+
68
+ test("registerApiErrorHandler includes code for AppError payloads", () => {
69
+ const fastify = createFastifyStub();
70
+ registerApiErrorHandler(fastify, { isAppError });
71
+
72
+ const reply = createReplyStub();
73
+ const error = new AppError(422, "Domain validation failed.", {
74
+ code: "domain_validation_failed",
75
+ details: {
76
+ fieldErrors: {
77
+ email: "invalid"
78
+ }
79
+ }
80
+ });
81
+
82
+ fastify.errorHandler(error, {}, reply);
83
+
84
+ assert.equal(reply.statusCode, 422);
85
+ assert.equal(reply.payload.code, "domain_validation_failed");
86
+ assert.equal(reply.payload.error, "Domain validation failed.");
87
+ assert.deepEqual(reply.payload.fieldErrors, {
88
+ email: "invalid"
89
+ });
90
+ });
91
+
92
+ test("registerApiErrorHandler falls back to app_error code for AppError without code", () => {
93
+ const fastify = createFastifyStub();
94
+ registerApiErrorHandler(fastify, { isAppError });
95
+
96
+ const reply = createReplyStub();
97
+ const error = new AppError(409, "Conflict.");
98
+ error.code = "";
99
+
100
+ fastify.errorHandler(error, {}, reply);
101
+
102
+ assert.equal(reply.statusCode, 409);
103
+ assert.equal(reply.payload.code, "app_error");
104
+ });
105
+
106
+ test("registerApiErrorHandler includes internal_server_error code for unhandled 500 errors", () => {
107
+ const fastify = createFastifyStub();
108
+ registerApiErrorHandler(fastify, { isAppError });
109
+
110
+ const reply = createReplyStub();
111
+ const error = new Error("Unexpected DB failure");
112
+
113
+ fastify.errorHandler(error, {}, reply);
114
+
115
+ assert.equal(reply.statusCode, 500);
116
+ assert.equal(reply.payload.code, "internal_server_error");
117
+ assert.equal(reply.payload.error, "Internal server error.");
118
+ });
119
+
120
+ test("registerApiErrorHandler keeps known error code for non-app errors", () => {
121
+ const fastify = createFastifyStub();
122
+ registerApiErrorHandler(fastify, { isAppError });
123
+
124
+ const reply = createReplyStub();
125
+ const error = new Error("CSRF token invalid");
126
+ error.statusCode = 403;
127
+ error.code = "FST_CSRF_BAD_TOKEN";
128
+
129
+ fastify.errorHandler(error, {}, reply);
130
+
131
+ assert.equal(reply.statusCode, 403);
132
+ assert.equal(reply.payload.code, "FST_CSRF_BAD_TOKEN");
133
+ assert.deepEqual(reply.payload.details, {
134
+ code: "FST_CSRF_BAD_TOKEN"
135
+ });
136
+ });
137
+
138
+ test("registerRequestLoggingHooks uses configured default surface when getSurface is absent", async () => {
139
+ const fastify = createFastifyStub();
140
+ let loggedPayload = null;
141
+
142
+ registerRequestLoggingHooks(fastify, {
143
+ defaultSurfaceId: "home"
144
+ });
145
+
146
+ const request = {
147
+ id: "req-home",
148
+ method: "GET",
149
+ routeOptions: {
150
+ url: "/status"
151
+ },
152
+ log: {
153
+ info(payload) {
154
+ loggedPayload = payload;
155
+ }
156
+ }
157
+ };
158
+ const reply = {
159
+ statusCode: 204
160
+ };
161
+
162
+ await fastify.hooks.onRequest(request);
163
+ await fastify.hooks.onResponse(request, reply);
164
+
165
+ assert.equal(loggedPayload.surface, "home");
166
+ });
167
+
168
+ test("registerRequestLoggingHooks leaves surface empty when no surface default is configured", async () => {
169
+ const fastify = createFastifyStub();
170
+ let loggedPayload = null;
171
+
172
+ registerRequestLoggingHooks(fastify);
173
+
174
+ const request = {
175
+ id: "req-public",
176
+ method: "GET",
177
+ routeOptions: {
178
+ url: "/status"
179
+ },
180
+ log: {
181
+ info(payload) {
182
+ loggedPayload = payload;
183
+ }
184
+ }
185
+ };
186
+ const reply = {
187
+ statusCode: 204
188
+ };
189
+
190
+ await fastify.hooks.onRequest(request);
191
+ await fastify.hooks.onResponse(request, reply);
192
+
193
+ assert.equal(loggedPayload.surface, "");
194
+ });
@@ -0,0 +1,6 @@
1
+ export { AppError, createValidationError } from "./errors.js";
2
+ export { parsePositiveInteger } from "./integers.js";
3
+ export { requireAuth } from "./serviceAuthorization.js";
4
+ export { installServiceRegistrationApi, resolveServiceRegistrations } from "../registries/serviceRegistrationRegistry.js";
5
+ export { registerDomainEventListener } from "../registries/domainEventListenerRegistry.js";
6
+ export { registerBootstrapPayloadContributor } from "../registries/bootstrapPayloadContributorRegistry.js";
@@ -0,0 +1,13 @@
1
+ import { normalizePositiveInteger } from "../../shared/support/normalize.js";
2
+
3
+ function parsePositiveInteger(value) {
4
+ return normalizePositiveInteger(value, {
5
+ fallback: null
6
+ });
7
+ }
8
+
9
+ function normalizeNullablePositiveInteger(value) {
10
+ return parsePositiveInteger(value);
11
+ }
12
+
13
+ export { parsePositiveInteger, normalizeNullablePositiveInteger };