@jskit-ai/kernel 0.1.245 → 0.1.246
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/client/appConfig.js +2 -1
- package/client/asyncModuleRecovery.js +2 -4
- package/client/index.d.ts +13 -75
- package/client/index.js +1 -1
- package/client/logging.js +8 -2
- package/client/moduleBootstrap.js +41 -33
- package/client/moduleBootstrap.test.js +35 -0
- package/client/shellBootstrap.js +1 -8
- package/client/shellBootstrap.test.js +32 -5
- package/client/shellRouting.js +16 -61
- package/client/shellRouting.test.js +30 -1
- package/client/vite/clientBootstrapPlugin.js +0 -6
- package/client/vite/clientBootstrapPlugin.test.js +2 -3
- package/internal/node/installedPackages.js +14 -58
- package/package.json +2 -4
- package/server/actions/actionCatalogue.js +13 -34
- package/{shared → server}/actions/actionContributorHelpers.js +0 -5
- package/{shared → server}/actions/actionDefinitions.js +24 -131
- package/{shared → server}/actions/actionDefinitions.test.js +75 -21
- package/server/actions/actionEvents.js +97 -2
- package/server/actions/actionEvents.test.js +79 -0
- package/{shared → server}/actions/executionContext.js +3 -13
- package/server/actions/idempotency.js +24 -0
- package/server/actions/pipeline.js +87 -0
- package/{shared → server}/actions/policies.js +20 -33
- package/{shared → server}/actions/policies.test.js +6 -6
- package/{shared → server}/actions/registry.js +3 -22
- package/{shared → server}/actions/registry.test.js +70 -32
- package/server/actions/requestMeta.js +17 -0
- package/server/http/capabilityHttpRuntime.js +1 -1
- package/server/http/capabilityHttpRuntime.test.js +56 -0
- package/server/http/errorHandling.js +291 -0
- package/server/{runtime/fastifyBootstrap.test.js → http/errorHandling.test.js} +72 -120
- package/server/http/lib/routeRegistration.js +36 -150
- package/server/http/lib/routeTransport.js +2 -0
- package/server/http/lib/routeValidator.js +5 -47
- package/server/http/lib/routeValidator.test.js +15 -17
- package/server/http/lib/router.js +10 -94
- package/server/platform/providerRuntime/capabilityProviderLoader.js +2 -1
- package/server/platform/providerRuntime/helpers.js +1 -34
- package/server/runtime/integers.js +1 -5
- package/server/runtime/serviceAuthorization.js +4 -9
- package/server/support/appConfig.js +2 -1
- package/server/support/appConfigFiles.js +1 -38
- package/server/support/appConfigFiles.test.js +3 -39
- package/server/support/index.js +1 -20
- package/shared/capabilities/defineProvider.js +1 -1
- package/shared/capabilities/index.js +1 -1
- package/shared/index.js +1 -1
- package/shared/support/index.js +1 -4
- package/shared/support/mobileConfig.js +99 -0
- package/shared/support/mobileConfig.test.js +113 -0
- package/shared/support/normalize.js +0 -108
- package/shared/support/normalize.test.js +0 -134
- package/shared/{actions/actionContributorHelpers.test.js → support/permissions.test.js} +1 -1
- package/shared/support/returnToPath.js +1 -20
- package/shared/support/returnToPath.test.js +1 -36
- package/shared/support/shellLayoutTargets.js +2 -140
- package/shared/support/shellLayoutTargets.test.js +1 -110
- package/shared/support/sorting.js +1 -9
- package/shared/surface/apiPaths.js +1 -82
- package/shared/surface/paths.js +38 -171
- package/shared/surface/registry.js +2 -1
- package/shared/surface/runtime.js +18 -109
- package/shared/surface/runtime.test.js +57 -0
- package/shared/validators/index.js +1 -4
- package/shared/validators/jsonRestSchemaSupport.js +0 -9
- package/shared/validators/recordIdParamsValidator.js +0 -6
- package/shared/validators/schemaDefinitions.js +4 -30
- package/shared/validators/schemaPayloadValidation.js +2 -6
- package/test/barrelExposure.test.js +3 -7
- package/test/exportsContract.test.js +1 -3
- package/client/mobileLaunchRouting.js +0 -231
- package/client/mobileLaunchRouting.test.js +0 -230
- package/server/http/lib/controller.js +0 -183
- package/server/http/lib/controller.test.js +0 -143
- package/server/http/lib/index.js +0 -5
- package/server/runtime/canonicalJson.js +0 -74
- package/server/runtime/entityChangeEvents.js +0 -307
- package/server/runtime/entityChangeEvents.test.js +0 -274
- package/server/runtime/fastifyBootstrap.js +0 -553
- package/server/runtime/moduleConfig.js +0 -223
- package/server/runtime/moduleConfig.test.js +0 -165
- package/server/runtime/pagination.js +0 -13
- package/server/runtime/realtimeNormalization.js +0 -21
- package/server/runtime/routeUtils.js +0 -20
- package/server/runtime/securityAudit.js +0 -269
- package/server/runtime/securityAudit.test.js +0 -41
- package/server/support/pageTargets.js +0 -772
- package/server/support/pageTargets.test.js +0 -659
- package/server/support/shellOutlets.js +0 -576
- package/server/support/shellOutlets.test.js +0 -336
- package/server/surface/index.js +0 -18
- package/shared/actions/audit.js +0 -7
- package/shared/actions/idempotency.js +0 -62
- package/shared/actions/observability.js +0 -10
- package/shared/actions/pipeline.js +0 -309
- package/shared/actions/requestMeta.js +0 -36
- package/shared/actions/textNormalization.js +0 -3
- package/shared/actions/withActionDefaults.js +0 -34
- /package/{shared → server}/actions/executionContext.test.js +0 -0
|
@@ -1,553 +0,0 @@
|
|
|
1
|
-
import { ActionRuntimeError } from "../../shared/actions/actionDefinitions.js";
|
|
2
|
-
import { normalizeOpaqueId, normalizeTransactionOutcome } from "../../shared/support/normalize.js";
|
|
3
|
-
import { resolveDefaultSurfaceId } from "../support/appConfig.js";
|
|
4
|
-
|
|
5
|
-
const JSON_API_CONTENT_TYPE = "application/vnd.api+json";
|
|
6
|
-
const JSON_API_CONTENT_TYPE_PARSER_MARKER = Symbol.for("jskit.fastify.jsonApiContentTypeParserRegistered");
|
|
7
|
-
const BODYLESS_CONTENT_TYPE_NORMALIZER_MARKER = Symbol.for("jskit.fastify.bodylessContentTypeNormalizerRegistered");
|
|
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 createFallbackJsonBodyParser() {
|
|
45
|
-
return function parseJsonBody(_request, body, done) {
|
|
46
|
-
const source = typeof body === "string" ? body.trim() : "";
|
|
47
|
-
if (!source) {
|
|
48
|
-
done(null, {});
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
done(null, JSON.parse(source));
|
|
54
|
-
} catch (error) {
|
|
55
|
-
if (error && typeof error === "object" && !Array.isArray(error)) {
|
|
56
|
-
error.statusCode = 400;
|
|
57
|
-
}
|
|
58
|
-
done(error);
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function registerJsonApiContentTypeParser(fastify) {
|
|
64
|
-
if (!fastify || typeof fastify.addContentTypeParser !== "function") {
|
|
65
|
-
throw new TypeError("registerJsonApiContentTypeParser requires a Fastify instance.");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (fastify[JSON_API_CONTENT_TYPE_PARSER_MARKER]) {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (typeof fastify.hasContentTypeParser === "function" && fastify.hasContentTypeParser(JSON_API_CONTENT_TYPE)) {
|
|
73
|
-
fastify[JSON_API_CONTENT_TYPE_PARSER_MARKER] = true;
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const parser = typeof fastify.getDefaultJsonParser === "function"
|
|
78
|
-
? fastify.getDefaultJsonParser("ignore", "ignore")
|
|
79
|
-
: createFallbackJsonBodyParser();
|
|
80
|
-
|
|
81
|
-
fastify.addContentTypeParser(JSON_API_CONTENT_TYPE, { parseAs: "string" }, parser);
|
|
82
|
-
fastify[JSON_API_CONTENT_TYPE_PARSER_MARKER] = true;
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function hasHeaderValue(headers = {}, key = "") {
|
|
87
|
-
return String(headers?.[key] ?? "").trim().length > 0;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function hasRequestBody(headers = {}) {
|
|
91
|
-
const transferEncoding = String(headers?.["transfer-encoding"] ?? "").trim();
|
|
92
|
-
if (transferEncoding) {
|
|
93
|
-
return true;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const contentLengthHeader = String(headers?.["content-length"] ?? "").trim();
|
|
97
|
-
if (!contentLengthHeader) {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const contentLength = Number(contentLengthHeader);
|
|
102
|
-
if (!Number.isFinite(contentLength)) {
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return contentLength > 0;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function normalizeBodylessContentTypeHeader(request = null) {
|
|
110
|
-
const headers = request?.headers;
|
|
111
|
-
if (!headers || typeof headers !== "object" || Array.isArray(headers)) {
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (!hasHeaderValue(headers, "content-type") && !hasHeaderValue(headers, "Content-Type")) {
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (hasRequestBody(headers)) {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
delete headers["content-type"];
|
|
124
|
-
delete headers["Content-Type"];
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function registerBodylessContentTypeNormalizer(fastify) {
|
|
129
|
-
if (!fastify || typeof fastify.addHook !== "function") {
|
|
130
|
-
throw new TypeError("registerBodylessContentTypeNormalizer requires a Fastify instance.");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (fastify[BODYLESS_CONTENT_TYPE_NORMALIZER_MARKER]) {
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
fastify.addHook("onRequest", async (request) => {
|
|
138
|
-
normalizeBodylessContentTypeHeader(request);
|
|
139
|
-
});
|
|
140
|
-
fastify[BODYLESS_CONTENT_TYPE_NORMALIZER_MARKER] = true;
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function registerRequestLoggingHooks(
|
|
145
|
-
app,
|
|
146
|
-
{
|
|
147
|
-
requestStartedAtSymbol,
|
|
148
|
-
getPathname,
|
|
149
|
-
getSurface,
|
|
150
|
-
observeRequest,
|
|
151
|
-
enableRequestLogs = true,
|
|
152
|
-
defaultSurfaceId = ""
|
|
153
|
-
} = {}
|
|
154
|
-
) {
|
|
155
|
-
if (!app || typeof app.addHook !== "function") {
|
|
156
|
-
throw new TypeError("registerRequestLoggingHooks requires a Fastify instance.");
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const startedAtSymbol = requestStartedAtSymbol || Symbol("request_started_at_ns");
|
|
160
|
-
const resolvePathname = typeof getPathname === "function" ? getPathname : () => "/";
|
|
161
|
-
const resolveSurface =
|
|
162
|
-
typeof getSurface === "function"
|
|
163
|
-
? getSurface
|
|
164
|
-
: () =>
|
|
165
|
-
resolveDefaultSurfaceId(null, {
|
|
166
|
-
defaultSurfaceId
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
app.addHook("onRequest", async (request) => {
|
|
170
|
-
request[startedAtSymbol] = process.hrtime.bigint();
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
app.addHook("onResponse", async (request, reply) => {
|
|
174
|
-
const startedAt = request[startedAtSymbol];
|
|
175
|
-
const durationMs =
|
|
176
|
-
typeof startedAt === "bigint" ? Number(process.hrtime.bigint() - startedAt) / 1_000_000 : Number.NaN;
|
|
177
|
-
const pathnameValue = resolvePathname(request);
|
|
178
|
-
const routeUrl = String(request?.routeOptions?.url || "").trim();
|
|
179
|
-
const surface = resolveSurface(pathnameValue, request);
|
|
180
|
-
const actorId = normalizeOpaqueId(request?.user?.id);
|
|
181
|
-
const logPayload = {
|
|
182
|
-
requestId: String(request?.id || ""),
|
|
183
|
-
method: String(request?.method || ""),
|
|
184
|
-
path: pathnameValue,
|
|
185
|
-
routeUrl,
|
|
186
|
-
surface,
|
|
187
|
-
statusCode: Number(reply?.statusCode || 0),
|
|
188
|
-
durationMs: Number.isFinite(durationMs) ? Number(durationMs.toFixed(3)) : null
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
if (actorId != null) {
|
|
192
|
-
logPayload.actorId = actorId;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (typeof observeRequest === "function") {
|
|
196
|
-
observeRequest({
|
|
197
|
-
method: logPayload.method,
|
|
198
|
-
route: routeUrl || pathnameValue,
|
|
199
|
-
surface,
|
|
200
|
-
statusCode: logPayload.statusCode,
|
|
201
|
-
durationMs
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (enableRequestLogs) {
|
|
206
|
-
request.log.info(logPayload, "request.completed");
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function resolveValidationFieldErrors(error) {
|
|
212
|
-
const issues = Array.isArray(error?.validation) ? error.validation : [];
|
|
213
|
-
const fieldErrors = {};
|
|
214
|
-
|
|
215
|
-
for (const issue of issues) {
|
|
216
|
-
const fieldFromPath = String(issue.instancePath || "")
|
|
217
|
-
.replace(/^\//, "")
|
|
218
|
-
.replace(/\//g, ".");
|
|
219
|
-
const field =
|
|
220
|
-
fieldFromPath || String(issue.params?.missingProperty || issue.params?.additionalProperty || "request").trim();
|
|
221
|
-
|
|
222
|
-
if (!fieldErrors[field]) {
|
|
223
|
-
fieldErrors[field] = issue.message || "Invalid value.";
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return fieldErrors;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function resolveRequestRouteTransport(request) {
|
|
231
|
-
const directTransport =
|
|
232
|
-
request?.routeTransport && typeof request.routeTransport === "object" && !Array.isArray(request.routeTransport)
|
|
233
|
-
? request.routeTransport
|
|
234
|
-
: null;
|
|
235
|
-
if (directTransport) {
|
|
236
|
-
return directTransport;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const configTransport =
|
|
240
|
-
request?.routeOptions?.config?.transport &&
|
|
241
|
-
typeof request.routeOptions.config.transport === "object" &&
|
|
242
|
-
!Array.isArray(request.routeOptions.config.transport)
|
|
243
|
-
? request.routeOptions.config.transport
|
|
244
|
-
: null;
|
|
245
|
-
const runtimeTransport =
|
|
246
|
-
configTransport?.runtime && typeof configTransport.runtime === "object" && !Array.isArray(configTransport.runtime)
|
|
247
|
-
? configTransport.runtime
|
|
248
|
-
: null;
|
|
249
|
-
if (runtimeTransport) {
|
|
250
|
-
return runtimeTransport;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return configTransport;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function applyRouteTransportErrorResponse(reply, request, error, {
|
|
257
|
-
statusCode = 500,
|
|
258
|
-
normalizedErrorCode = "",
|
|
259
|
-
message = error?.message,
|
|
260
|
-
exposeDetails = true
|
|
261
|
-
} = {}) {
|
|
262
|
-
const transport = resolveRequestRouteTransport(request);
|
|
263
|
-
const errorSerializer = transport && typeof transport.error === "function" ? transport.error : null;
|
|
264
|
-
if (!errorSerializer) {
|
|
265
|
-
return false;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const payload = errorSerializer(error, {
|
|
269
|
-
request,
|
|
270
|
-
reply,
|
|
271
|
-
statusCode,
|
|
272
|
-
code: normalizedErrorCode,
|
|
273
|
-
message,
|
|
274
|
-
exposeDetails
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
if (payload && typeof payload.then === "function") {
|
|
278
|
-
throw new TypeError("Route transport error serializer must return synchronously.");
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (transport.contentType) {
|
|
282
|
-
reply.header("Content-Type", transport.contentType);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
reply.code(statusCode).send(payload);
|
|
286
|
-
return true;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function resolveApiErrorStatus(error, code) {
|
|
290
|
-
// JSON REST's public error codes need HTTP classification even without status hints.
|
|
291
|
-
switch (code) {
|
|
292
|
-
case "REST_API_VALIDATION":
|
|
293
|
-
return 422;
|
|
294
|
-
case "REST_API_RESOURCE":
|
|
295
|
-
switch (error?.subtype) {
|
|
296
|
-
case "not_found": return 404;
|
|
297
|
-
case "forbidden": return 403;
|
|
298
|
-
case "conflict": return 409;
|
|
299
|
-
default: return 400;
|
|
300
|
-
}
|
|
301
|
-
case "REST_API_VERSION_CONFLICT":
|
|
302
|
-
return 409;
|
|
303
|
-
case "REST_API_PRECONDITION_FAILED":
|
|
304
|
-
return 412;
|
|
305
|
-
case "REST_API_FIELDSET_INVALID":
|
|
306
|
-
case "REST_API_INCLUDE_INVALID":
|
|
307
|
-
return 400;
|
|
308
|
-
case "REST_API_PAYLOAD":
|
|
309
|
-
return error?.statusCode === 413 ? 413 : 400;
|
|
310
|
-
case "REST_API_TEMPORAL_DATA_INVALID":
|
|
311
|
-
return 500;
|
|
312
|
-
default: {
|
|
313
|
-
const status = Number(error?.statusCode || error?.status);
|
|
314
|
-
return Number.isInteger(status) && status >= 400 && status <= 599 ? status : 500;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function registerApiErrorHandler(
|
|
320
|
-
app,
|
|
321
|
-
{
|
|
322
|
-
isAppError,
|
|
323
|
-
onRecordDbError,
|
|
324
|
-
onCaptureServerError,
|
|
325
|
-
appErrorLogMessage = "AppError 5xx",
|
|
326
|
-
unhandledErrorLogMessage = "Unhandled error"
|
|
327
|
-
} = {}
|
|
328
|
-
) {
|
|
329
|
-
if (!app || typeof app.setErrorHandler !== "function") {
|
|
330
|
-
throw new TypeError("registerApiErrorHandler requires a Fastify instance.");
|
|
331
|
-
}
|
|
332
|
-
if (typeof isAppError !== "function") {
|
|
333
|
-
throw new TypeError("registerApiErrorHandler requires isAppError.");
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const recordDbError = typeof onRecordDbError === "function" ? onRecordDbError : () => {};
|
|
337
|
-
const captureServerError = typeof onCaptureServerError === "function" ? onCaptureServerError : () => {};
|
|
338
|
-
|
|
339
|
-
function shouldExposeAppErrorDetails(errorCode = "") {
|
|
340
|
-
return String(errorCode || "").trim() !== "ACTION_PERMISSION_DENIED";
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
app.setErrorHandler((error, request, reply) => {
|
|
344
|
-
const normalizedErrorCode = String(error?.code || "").trim();
|
|
345
|
-
const transactionOutcome = normalizeTransactionOutcome(error?.transactionOutcome);
|
|
346
|
-
const outcomeFields = transactionOutcome ? { transactionOutcome } : {};
|
|
347
|
-
const isCsrfErrorCode = normalizedErrorCode.startsWith("FST_CSRF_");
|
|
348
|
-
const statusCode = resolveApiErrorStatus(error, normalizedErrorCode);
|
|
349
|
-
|
|
350
|
-
if (Array.isArray(error?.validation)) {
|
|
351
|
-
const fieldErrors = resolveValidationFieldErrors(error);
|
|
352
|
-
const validationErrorCode = normalizedErrorCode || "validation_failed";
|
|
353
|
-
if (applyRouteTransportErrorResponse(reply, request, error, {
|
|
354
|
-
statusCode: 400,
|
|
355
|
-
normalizedErrorCode: validationErrorCode
|
|
356
|
-
})) {
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
reply.code(400).send({
|
|
360
|
-
error: "Validation failed.",
|
|
361
|
-
code: validationErrorCode,
|
|
362
|
-
...outcomeFields,
|
|
363
|
-
fieldErrors,
|
|
364
|
-
details: {
|
|
365
|
-
fieldErrors
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (isAppError(error) || error instanceof ActionRuntimeError) {
|
|
372
|
-
if (applyRouteTransportErrorResponse(reply, request, error, {
|
|
373
|
-
statusCode: error.status,
|
|
374
|
-
normalizedErrorCode: normalizedErrorCode || "app_error",
|
|
375
|
-
exposeDetails: shouldExposeAppErrorDetails(normalizedErrorCode)
|
|
376
|
-
})) {
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
if (error.status >= 500) {
|
|
380
|
-
recordDbError(error);
|
|
381
|
-
captureServerError(request, error, error.status);
|
|
382
|
-
app.log.error({ err: error }, appErrorLogMessage);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const appErrorCode = normalizedErrorCode || "app_error";
|
|
386
|
-
const payload = {
|
|
387
|
-
error: error.message,
|
|
388
|
-
code: appErrorCode,
|
|
389
|
-
...outcomeFields
|
|
390
|
-
};
|
|
391
|
-
if (error.details && shouldExposeAppErrorDetails(appErrorCode)) {
|
|
392
|
-
payload.details = error.details;
|
|
393
|
-
if (error.details.fieldErrors) {
|
|
394
|
-
payload.fieldErrors = error.details.fieldErrors;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (error.headers && typeof error.headers === "object") {
|
|
399
|
-
Object.entries(error.headers).forEach(([name, value]) => {
|
|
400
|
-
reply.header(name, value);
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
reply.code(error.status).send(payload);
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
if (error.headers && typeof error.headers === "object") {
|
|
409
|
-
Object.entries(error.headers).forEach(([name, value]) => {
|
|
410
|
-
reply.header(name, value);
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
if (statusCode >= 500) {
|
|
415
|
-
recordDbError(error);
|
|
416
|
-
}
|
|
417
|
-
captureServerError(request, error, statusCode);
|
|
418
|
-
app.log.error({ err: error }, unhandledErrorLogMessage);
|
|
419
|
-
|
|
420
|
-
const message = statusCode >= 500 ? "Internal server error." : String(error?.message || "Request failed.");
|
|
421
|
-
const fallbackErrorCode =
|
|
422
|
-
normalizedErrorCode || (statusCode >= 500 ? "internal_server_error" : "request_failed");
|
|
423
|
-
const payload = {
|
|
424
|
-
error: message,
|
|
425
|
-
code: fallbackErrorCode,
|
|
426
|
-
...outcomeFields
|
|
427
|
-
};
|
|
428
|
-
if (isCsrfErrorCode) {
|
|
429
|
-
payload.details = {
|
|
430
|
-
code: normalizedErrorCode
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
if (applyRouteTransportErrorResponse(reply, request, error, {
|
|
434
|
-
statusCode,
|
|
435
|
-
normalizedErrorCode: fallbackErrorCode,
|
|
436
|
-
message,
|
|
437
|
-
exposeDetails: statusCode < 500
|
|
438
|
-
})) {
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
reply.code(statusCode).send(payload);
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function resolveDatabaseErrorCode(error) {
|
|
446
|
-
const errorCode = String(error?.code || "")
|
|
447
|
-
.trim()
|
|
448
|
-
.toUpperCase();
|
|
449
|
-
if (errorCode && (errorCode.startsWith("ER_") || errorCode.startsWith("SQLITE_") || errorCode.startsWith("PG"))) {
|
|
450
|
-
return errorCode;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
const sqlState = String(error?.sqlState || error?.sqlstate || "")
|
|
454
|
-
.trim()
|
|
455
|
-
.toUpperCase();
|
|
456
|
-
if (sqlState) {
|
|
457
|
-
return `SQLSTATE_${sqlState}`;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
const errno = Number(error?.errno);
|
|
461
|
-
if (Number.isInteger(errno)) {
|
|
462
|
-
return `ERRNO_${errno}`;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const message = String(error?.message || "").toLowerCase();
|
|
466
|
-
const name = String(error?.name || "").toLowerCase();
|
|
467
|
-
if (message.includes("mysql") || message.includes("sql") || message.includes("knex") || name.includes("mysql")) {
|
|
468
|
-
return "DB_UNKNOWN";
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
return "";
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function recordDbErrorBestEffort(observabilityService, error) {
|
|
475
|
-
if (!observabilityService || typeof observabilityService.recordDbError !== "function") {
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
const code = resolveDatabaseErrorCode(error);
|
|
480
|
-
if (!code) {
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
observabilityService.recordDbError({ code });
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
async function runGracefulShutdown({
|
|
488
|
-
signal = "",
|
|
489
|
-
exitProcess = false,
|
|
490
|
-
exitCode = 0,
|
|
491
|
-
timeoutMs = 10_000,
|
|
492
|
-
appInstance = null,
|
|
493
|
-
stopBackgroundRuntimes = () => {},
|
|
494
|
-
closeDatabase = async () => {},
|
|
495
|
-
logger = console
|
|
496
|
-
} = {}) {
|
|
497
|
-
let forcedExitTimer = null;
|
|
498
|
-
|
|
499
|
-
if (signal) {
|
|
500
|
-
logger.log(`Received ${signal}. Shutting down.`);
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
stopBackgroundRuntimes();
|
|
504
|
-
|
|
505
|
-
if (exitProcess) {
|
|
506
|
-
forcedExitTimer = setTimeout(() => {
|
|
507
|
-
try {
|
|
508
|
-
appInstance?.server?.closeIdleConnections?.();
|
|
509
|
-
appInstance?.server?.closeAllConnections?.();
|
|
510
|
-
} catch {
|
|
511
|
-
// Ignore best-effort force-close failures.
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
logger.error(`Graceful shutdown timed out after ${timeoutMs}ms. Forcing process exit.`);
|
|
515
|
-
process.exit(1);
|
|
516
|
-
}, timeoutMs);
|
|
517
|
-
forcedExitTimer.unref?.();
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
try {
|
|
521
|
-
if (appInstance) {
|
|
522
|
-
await appInstance.close();
|
|
523
|
-
}
|
|
524
|
-
await closeDatabase();
|
|
525
|
-
} catch (error) {
|
|
526
|
-
logger.error("Failed to close server cleanly:", error);
|
|
527
|
-
if (exitProcess) {
|
|
528
|
-
process.exit(1);
|
|
529
|
-
}
|
|
530
|
-
throw error;
|
|
531
|
-
} finally {
|
|
532
|
-
if (forcedExitTimer) {
|
|
533
|
-
clearTimeout(forcedExitTimer);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
if (exitProcess) {
|
|
538
|
-
process.exit(exitCode);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
export {
|
|
543
|
-
resolveLoggerLevel,
|
|
544
|
-
createFastifyLoggerOptions,
|
|
545
|
-
registerJsonApiContentTypeParser,
|
|
546
|
-
normalizeBodylessContentTypeHeader,
|
|
547
|
-
registerBodylessContentTypeNormalizer,
|
|
548
|
-
registerRequestLoggingHooks,
|
|
549
|
-
registerApiErrorHandler,
|
|
550
|
-
resolveDatabaseErrorCode,
|
|
551
|
-
recordDbErrorBestEffort,
|
|
552
|
-
runGracefulShutdown
|
|
553
|
-
};
|