@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.
- package/README.md +24 -0
- package/_testable/index.js +4 -0
- package/client/appConfig.js +33 -0
- package/client/componentInteraction.js +51 -0
- package/client/componentInteraction.test.js +111 -0
- package/client/descriptorSections.js +75 -0
- package/client/index.d.ts +70 -0
- package/client/index.js +3 -0
- package/client/logging.js +38 -0
- package/client/moduleBootstrap.js +670 -0
- package/client/moduleBootstrap.test.js +403 -0
- package/client/shellBootstrap.js +233 -0
- package/client/shellBootstrap.test.js +185 -0
- package/client/shellRouting.js +321 -0
- package/client/shellRouting.test.js +113 -0
- package/client/vite/clientBootstrapPlugin.js +259 -0
- package/client/vite/clientBootstrapPlugin.test.js +563 -0
- package/client/vite/index.js +3 -0
- package/internal/node/fileSystem.js +21 -0
- package/internal/node/installedPackageDescriptor.js +104 -0
- package/package.json +43 -0
- package/server/actions/ActionRuntimeServiceProvider.js +309 -0
- package/server/actions/ActionRuntimeServiceProvider.test.js +551 -0
- package/server/actions/index.js +8 -0
- package/server/container/ContainerCoreServiceProvider.js +27 -0
- package/server/container/index.js +10 -0
- package/server/exportPolicy.test.js +68 -0
- package/server/http/HttpFastifyServiceProvider.js +25 -0
- package/server/http/_testable/index.js +2 -0
- package/server/http/index.js +1 -0
- package/server/http/lib/controller.js +183 -0
- package/server/http/lib/controller.test.js +143 -0
- package/server/http/lib/errors.js +12 -0
- package/server/http/lib/httpRuntime.js +82 -0
- package/server/http/lib/index.js +18 -0
- package/server/http/lib/kernel.js +15 -0
- package/server/http/lib/kernel.test.js +880 -0
- package/server/http/lib/middlewareRuntime.js +149 -0
- package/server/http/lib/requestActionExecutor.js +258 -0
- package/server/http/lib/requestScope.js +59 -0
- package/server/http/lib/routeRegistration.js +165 -0
- package/server/http/lib/routeSupport.js +45 -0
- package/server/http/lib/routeValidator.js +469 -0
- package/server/http/lib/routeValidator.test.js +474 -0
- package/server/http/lib/router.js +206 -0
- package/server/kernel/KernelCoreServiceProvider.js +27 -0
- package/server/kernel/index.js +10 -0
- package/server/platform/PlatformServerRuntimeServiceProvider.js +30 -0
- package/server/platform/index.js +5 -0
- package/server/platform/providerRuntime/descriptorCatalog.js +170 -0
- package/server/platform/providerRuntime/helpers.js +45 -0
- package/server/platform/providerRuntime/lockfile.js +27 -0
- package/server/platform/providerRuntime/providerLoader.js +283 -0
- package/server/platform/providerRuntime.js +142 -0
- package/server/platform/providerRuntime.test.js +217 -0
- package/server/platform/runtime.js +40 -0
- package/server/platform/surfaceRuntime.js +150 -0
- package/server/platform/surfaceRuntime.test.js +136 -0
- package/server/registries/actionSurfaceSourceRegistry.js +150 -0
- package/server/registries/bootstrapPayloadContributorRegistry.js +41 -0
- package/server/registries/domainEventListenerRegistry.js +61 -0
- package/server/registries/index.js +36 -0
- package/server/registries/primitives.js +63 -0
- package/server/registries/routeVisibilityResolverRegistry.js +87 -0
- package/server/registries/serviceRegistrationRegistry.js +431 -0
- package/server/runtime/ServerRuntimeCoreServiceProvider.js +65 -0
- package/server/runtime/ServerRuntimeCoreServiceProvider.test.js +53 -0
- package/server/runtime/apiRoutePolicyParity.test.js +109 -0
- package/server/runtime/apiRouteRegistration.js +65 -0
- package/server/runtime/bootBootstrapRoutes.js +46 -0
- package/server/runtime/bootBootstrapRoutes.test.js +79 -0
- package/server/runtime/bootstrapContributors.test.js +114 -0
- package/server/runtime/canonicalJson.js +74 -0
- package/server/runtime/composition.js +142 -0
- package/server/runtime/domainEvents.test.js +114 -0
- package/server/runtime/domainRules.js +50 -0
- package/server/runtime/domainRules.test.js +87 -0
- package/server/runtime/entityChangeEvents.js +182 -0
- package/server/runtime/entityChangeEvents.test.js +211 -0
- package/server/runtime/errors.js +68 -0
- package/server/runtime/errors.test.js +73 -0
- package/server/runtime/fastifyBootstrap.js +372 -0
- package/server/runtime/fastifyBootstrap.test.js +194 -0
- package/server/runtime/index.js +6 -0
- package/server/runtime/integers.js +13 -0
- package/server/runtime/moduleConfig.js +269 -0
- package/server/runtime/moduleConfig.test.js +141 -0
- package/server/runtime/pagination.js +13 -0
- package/server/runtime/realtimeNormalization.js +21 -0
- package/server/runtime/requestUrl.js +38 -0
- package/server/runtime/routeUtils.js +20 -0
- package/server/runtime/runtimeAssembly.js +113 -0
- package/server/runtime/runtimeKernel.js +55 -0
- package/server/runtime/securityAudit.js +269 -0
- package/server/runtime/securityAudit.test.js +41 -0
- package/server/runtime/serviceAuthorization.js +113 -0
- package/server/runtime/serviceAuthorization.test.js +100 -0
- package/server/runtime/serviceRegistration.test.js +197 -0
- package/server/support/SupportCoreServiceProvider.js +25 -0
- package/server/support/appConfig.js +37 -0
- package/server/support/appConfig.test.js +94 -0
- package/server/support/defaultMissingHandler.js +7 -0
- package/server/support/index.js +2 -0
- package/server/support/routePolicyConfig.js +51 -0
- package/server/support/symlinkSafeRequire.js +78 -0
- package/server/support/symlinkSafeRequire.test.js +27 -0
- package/server/surface/SurfaceRoutingServiceProvider.js +27 -0
- package/server/surface/index.js +19 -0
- package/shared/actions/actionContributorHelpers.js +34 -0
- package/shared/actions/actionContributorHelpers.test.js +16 -0
- package/shared/actions/actionDefinitions.js +488 -0
- package/shared/actions/actionDefinitions.test.js +212 -0
- package/shared/actions/audit.js +7 -0
- package/shared/actions/executionContext.js +97 -0
- package/shared/actions/executionContext.test.js +66 -0
- package/shared/actions/idempotency.js +62 -0
- package/shared/actions/index.js +2 -0
- package/shared/actions/observability.js +10 -0
- package/shared/actions/pipeline.js +287 -0
- package/shared/actions/policies.js +342 -0
- package/shared/actions/policies.test.js +233 -0
- package/shared/actions/registry.js +187 -0
- package/shared/actions/registry.test.js +381 -0
- package/shared/actions/requestMeta.js +36 -0
- package/shared/actions/textNormalization.js +3 -0
- package/shared/actions/withActionDefaults.js +34 -0
- package/shared/index.js +2 -0
- package/shared/runtime/application.js +323 -0
- package/shared/runtime/container.js +261 -0
- package/shared/runtime/containerErrors.js +22 -0
- package/shared/runtime/index.js +18 -0
- package/shared/runtime/kernelErrors.js +20 -0
- package/shared/runtime/serviceProvider.js +13 -0
- package/shared/support/formatDateTime.js +10 -0
- package/shared/support/formatDateTime.test.js +15 -0
- package/shared/support/index.js +14 -0
- package/shared/support/linkPath.js +67 -0
- package/shared/support/linkPath.test.js +35 -0
- package/shared/support/normalize.js +116 -0
- package/shared/support/normalize.test.js +48 -0
- package/shared/support/packageDescriptor.test.js +121 -0
- package/shared/support/permissions.js +50 -0
- package/shared/support/pickOwnProperties.js +17 -0
- package/shared/support/pickOwnProperties.test.js +25 -0
- package/shared/support/policies.js +11 -0
- package/shared/support/queryPath.js +33 -0
- package/shared/support/queryPath.test.js +19 -0
- package/shared/support/queryResilience.js +34 -0
- package/shared/support/queryResilience.test.js +33 -0
- package/shared/support/returnToPath.js +153 -0
- package/shared/support/returnToPath.test.js +123 -0
- package/shared/support/sorting.js +15 -0
- package/shared/support/tokens.js +23 -0
- package/shared/support/tokens.test.js +17 -0
- package/shared/support/visibility.js +56 -0
- package/shared/support/visibility.test.js +45 -0
- package/shared/surface/apiPaths.js +84 -0
- package/shared/surface/escapeRegExp.js +5 -0
- package/shared/surface/index.js +6 -0
- package/shared/surface/paths.js +273 -0
- package/shared/surface/registry.js +135 -0
- package/shared/surface/registry.test.js +44 -0
- package/shared/surface/runtime.js +357 -0
- package/shared/surface/runtime.test.js +319 -0
- package/shared/validators/createCursorListValidator.js +42 -0
- package/shared/validators/createCursorListValidator.test.js +34 -0
- package/shared/validators/cursorPaginationQueryValidator.js +31 -0
- package/shared/validators/cursorPaginationQueryValidator.test.js +21 -0
- package/shared/validators/index.js +12 -0
- package/shared/validators/inputNormalization.js +13 -0
- package/shared/validators/mergeObjectSchemas.js +31 -0
- package/shared/validators/mergeObjectSchemas.test.js +67 -0
- package/shared/validators/mergeValidators.js +89 -0
- package/shared/validators/mergeValidators.test.js +116 -0
- package/shared/validators/nestValidator.js +53 -0
- package/shared/validators/nestValidator.test.js +60 -0
- package/shared/validators/recordIdParamsValidator.js +36 -0
- package/shared/validators/recordIdParamsValidator.test.js +20 -0
- package/shared/validators/resourceRequiredMetadata.js +41 -0
- package/shared/validators/resourceRequiredMetadata.test.js +49 -0
- package/test/barrelExposure.test.js +106 -0
- package/test/dynamicImportPolicy.test.js +89 -0
- package/test/exportsContract.test.js +168 -0
- package/test/routeInputContractGuard.test.js +78 -0
- 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 };
|