velocious 1.0.468 → 1.0.470
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 +6 -4
- package/build/configuration-types.js +9 -1
- package/build/configuration.js +6 -1
- package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +139 -4
- package/build/error-reporting/request-details.js +308 -0
- package/build/frontend-model-controller.js +3 -1
- package/build/src/configuration-types.d.ts +23 -1
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +9 -2
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +7 -2
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +43 -0
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +127 -5
- package/build/src/error-reporting/request-details.d.ts +7 -0
- package/build/src/error-reporting/request-details.d.ts.map +1 -0
- package/build/src/error-reporting/request-details.js +278 -0
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +4 -2
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/configuration-types.js +9 -1
- package/src/configuration.js +6 -1
- package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +139 -4
- package/src/error-reporting/request-details.js +308 -0
- package/src/frontend-model-controller.js +3 -1
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import UploadedFile from "../http-server/client/uploaded-file/uploaded-file.js"
|
|
4
|
+
|
|
5
|
+
const REQUEST_DETAILS_BODY_MAX_SERIALIZED_LENGTH = 12000
|
|
6
|
+
const REQUEST_DETAILS_STRING_MAX_LENGTH = 1000
|
|
7
|
+
const REQUEST_DETAILS_ARRAY_MAX_ITEMS = 50
|
|
8
|
+
const REDACTED_REQUEST_DETAILS_VALUE = "[redacted]"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extracts request metadata without retaining the request object.
|
|
12
|
+
* @param {import("../http-server/client/request.js").default | import("../http-server/client/websocket-request.js").default | undefined} request - Request object, when present.
|
|
13
|
+
* @returns {import("../configuration-types.js").ErrorRequestDetails | null} - Request metadata.
|
|
14
|
+
*/
|
|
15
|
+
export function requestDetails(request) {
|
|
16
|
+
if (!request) return null
|
|
17
|
+
|
|
18
|
+
let details
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
details = {
|
|
22
|
+
httpMethod: request.httpMethod(),
|
|
23
|
+
path: request.path()
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const body = requestBodySnapshot(request)
|
|
30
|
+
|
|
31
|
+
if (body !== undefined) {
|
|
32
|
+
return {...details, body}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return details
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Snapshots parsed request params for error reports.
|
|
40
|
+
* @param {import("../http-server/client/request.js").default | import("../http-server/client/websocket-request.js").default} request - Request object.
|
|
41
|
+
* @returns {? | undefined} - Sanitized body snapshot.
|
|
42
|
+
*/
|
|
43
|
+
function requestBodySnapshot(request) {
|
|
44
|
+
if (typeof request.params !== "function") return undefined
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
return requestDetailsBodySnapshot(request.params())
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return {
|
|
50
|
+
__unavailable: true,
|
|
51
|
+
error: error instanceof Error ? error.message : String(error)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Builds a bounded, redacted body snapshot for error reports.
|
|
58
|
+
* @param {?} body - Parsed request body.
|
|
59
|
+
* @returns {?} - Sanitized body snapshot.
|
|
60
|
+
*/
|
|
61
|
+
function requestDetailsBodySnapshot(body) {
|
|
62
|
+
const originalLength = serializedLength(body)
|
|
63
|
+
const sanitized = sanitizeRequestDetailsValue(body)
|
|
64
|
+
const sanitizedLength = serializedLength(sanitized)
|
|
65
|
+
|
|
66
|
+
if (originalLength <= REQUEST_DETAILS_BODY_MAX_SERIALIZED_LENGTH && sanitizedLength <= REQUEST_DETAILS_BODY_MAX_SERIALIZED_LENGTH) {
|
|
67
|
+
return sanitized
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const compactFrontendModelBody = compactFrontendModelRequestBody(sanitized, originalLength)
|
|
71
|
+
|
|
72
|
+
if (serializedLength(compactFrontendModelBody) <= REQUEST_DETAILS_BODY_MAX_SERIALIZED_LENGTH) {
|
|
73
|
+
return compactFrontendModelBody
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return compactRequestDetailsBody(sanitized, originalLength)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Sanitizes a value recursively for request details.
|
|
81
|
+
* @param {?} value - Value to sanitize.
|
|
82
|
+
* @param {string} [key] - Current object key.
|
|
83
|
+
* @param {WeakSet<object>} [seen] - Seen object references.
|
|
84
|
+
* @returns {?} - Sanitized value.
|
|
85
|
+
*/
|
|
86
|
+
function sanitizeRequestDetailsValue(value, key = "", seen = new WeakSet()) {
|
|
87
|
+
if (sensitiveRequestDetailsKey(key)) return REDACTED_REQUEST_DETAILS_VALUE
|
|
88
|
+
if (primitiveRequestDetailsValue(value)) return value
|
|
89
|
+
if (bufferValue(value)) return bufferRequestDetailsSummary(value)
|
|
90
|
+
if (value instanceof UploadedFile) return uploadedFileRequestDetailsSummary(value)
|
|
91
|
+
if (typeof value === "string") return sanitizeRequestDetailsString(value)
|
|
92
|
+
if (Array.isArray(value)) return sanitizeRequestDetailsArray(value, seen)
|
|
93
|
+
if (typeof value === "object") return sanitizeRequestDetailsObject(value, seen)
|
|
94
|
+
|
|
95
|
+
return String(value)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Checks whether a value can be kept as-is.
|
|
100
|
+
* @param {?} value - Value.
|
|
101
|
+
* @returns {boolean} - Whether the value is primitive and safe.
|
|
102
|
+
*/
|
|
103
|
+
function primitiveRequestDetailsValue(value) {
|
|
104
|
+
return value == null || typeof value === "boolean" || typeof value === "number"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Sanitizes a string value.
|
|
109
|
+
* @param {string} value - String value.
|
|
110
|
+
* @returns {string} - Sanitized string.
|
|
111
|
+
*/
|
|
112
|
+
function sanitizeRequestDetailsString(value) {
|
|
113
|
+
if (value.length <= REQUEST_DETAILS_STRING_MAX_LENGTH) return value
|
|
114
|
+
|
|
115
|
+
return `${value.slice(0, REQUEST_DETAILS_STRING_MAX_LENGTH)}... [truncated ${value.length - REQUEST_DETAILS_STRING_MAX_LENGTH} chars]`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Sanitizes an array value.
|
|
120
|
+
* @param {Array<?>} value - Array value.
|
|
121
|
+
* @param {WeakSet<object>} seen - Seen object references.
|
|
122
|
+
* @returns {Array<?>} - Sanitized array.
|
|
123
|
+
*/
|
|
124
|
+
function sanitizeRequestDetailsArray(value, seen) {
|
|
125
|
+
const entries = value
|
|
126
|
+
.slice(0, REQUEST_DETAILS_ARRAY_MAX_ITEMS)
|
|
127
|
+
.map((entry) => sanitizeRequestDetailsValue(entry, "", seen))
|
|
128
|
+
|
|
129
|
+
if (value.length > REQUEST_DETAILS_ARRAY_MAX_ITEMS) {
|
|
130
|
+
entries.push({__omittedItems: value.length - REQUEST_DETAILS_ARRAY_MAX_ITEMS})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return entries
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Sanitizes a plain object-like value.
|
|
138
|
+
* @param {object} value - Object value.
|
|
139
|
+
* @param {WeakSet<object>} seen - Seen object references.
|
|
140
|
+
* @returns {Record<string, ?> | string} - Sanitized object or circular marker.
|
|
141
|
+
*/
|
|
142
|
+
function sanitizeRequestDetailsObject(value, seen) {
|
|
143
|
+
if (seen.has(value)) return "[circular]"
|
|
144
|
+
|
|
145
|
+
seen.add(value)
|
|
146
|
+
|
|
147
|
+
/** @type {Record<string, ?>} */
|
|
148
|
+
const sanitized = {}
|
|
149
|
+
|
|
150
|
+
for (const [entryKey, entryValue] of Object.entries(value)) {
|
|
151
|
+
sanitized[entryKey] = sanitizeRequestDetailsValue(entryValue, entryKey, seen)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
seen.delete(value)
|
|
155
|
+
|
|
156
|
+
return sanitized
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Checks whether a value is a Node.js Buffer.
|
|
161
|
+
* @param {?} value - Value.
|
|
162
|
+
* @returns {value is Buffer} - Whether value is a Buffer.
|
|
163
|
+
*/
|
|
164
|
+
function bufferValue(value) {
|
|
165
|
+
return typeof Buffer !== "undefined" && typeof Buffer.isBuffer === "function" && Buffer.isBuffer(value)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Summarizes raw binary data without serializing bytes.
|
|
170
|
+
* @param {Buffer} buffer - Uploaded or request buffer.
|
|
171
|
+
* @returns {Record<string, ?>} - Request-safe buffer summary.
|
|
172
|
+
*/
|
|
173
|
+
function bufferRequestDetailsSummary(buffer) {
|
|
174
|
+
return {
|
|
175
|
+
__redacted: true,
|
|
176
|
+
__type: "Buffer",
|
|
177
|
+
byteLength: buffer.byteLength
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Summarizes uploaded files without serializing file bytes or temp paths.
|
|
183
|
+
* @param {UploadedFile} uploadedFile - Uploaded file object.
|
|
184
|
+
* @returns {Record<string, ?>} - Request-safe upload summary.
|
|
185
|
+
*/
|
|
186
|
+
function uploadedFileRequestDetailsSummary(uploadedFile) {
|
|
187
|
+
return {
|
|
188
|
+
__redacted: true,
|
|
189
|
+
__type: "UploadedFile",
|
|
190
|
+
contentType: uploadedFile.contentType() ?? null,
|
|
191
|
+
fieldName: uploadedFile.fieldName(),
|
|
192
|
+
filename: uploadedFile.filename(),
|
|
193
|
+
size: uploadedFile.size()
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Checks whether a key should be redacted from request details.
|
|
199
|
+
* @param {string} key - Object key.
|
|
200
|
+
* @returns {boolean} - Whether the value is sensitive.
|
|
201
|
+
*/
|
|
202
|
+
function sensitiveRequestDetailsKey(key) {
|
|
203
|
+
return /authorization|contentBase64|password|secret|sessionToken|token/i.test(key)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Compacts a frontend-model request envelope while preserving debugging shape.
|
|
208
|
+
* @param {?} value - Sanitized body value.
|
|
209
|
+
* @param {number} originalSerializedLength - Byte count before redaction or compaction.
|
|
210
|
+
* @returns {?} - Compacted body value.
|
|
211
|
+
*/
|
|
212
|
+
function compactFrontendModelRequestBody(value, originalSerializedLength) {
|
|
213
|
+
if (!value || typeof value !== "object" || !Array.isArray(/** @type {Record<string, ?>} */ (value).requests)) {
|
|
214
|
+
return compactRequestDetailsBody(value, originalSerializedLength)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
__truncated: true,
|
|
219
|
+
originalSerializedLength,
|
|
220
|
+
requests: /** @type {Array<?>} */ (/** @type {Record<string, ?>} */ (value).requests).map((request) => {
|
|
221
|
+
return compactFrontendModelRequest(request)
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Compacts one frontend-model request entry.
|
|
228
|
+
* @param {?} value - Candidate request entry from the transport envelope.
|
|
229
|
+
* @returns {?} - Compacted request.
|
|
230
|
+
*/
|
|
231
|
+
function compactFrontendModelRequest(value) {
|
|
232
|
+
if (!value || typeof value !== "object") return value
|
|
233
|
+
|
|
234
|
+
const request = /** @type {Record<string, ?>} */ (value)
|
|
235
|
+
/** @type {Record<string, ?>} */
|
|
236
|
+
const compact = {}
|
|
237
|
+
|
|
238
|
+
for (const key of ["requestId", "model", "commandType", "customPath"]) {
|
|
239
|
+
if (request[key] !== undefined) compact[key] = request[key]
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (request.payload !== undefined) {
|
|
243
|
+
compact.payload = compactFrontendModelPayload(request.payload)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return compact
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Compacts a frontend-model payload.
|
|
251
|
+
* @param {?} value - Candidate payload from a request entry.
|
|
252
|
+
* @returns {?} - Compacted payload.
|
|
253
|
+
*/
|
|
254
|
+
function compactFrontendModelPayload(value) {
|
|
255
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value
|
|
256
|
+
|
|
257
|
+
const payload = /** @type {Record<string, ?>} */ (value)
|
|
258
|
+
/** @type {Record<string, ?>} */
|
|
259
|
+
const compact = {}
|
|
260
|
+
|
|
261
|
+
for (const [key, entryValue] of Object.entries(payload)) {
|
|
262
|
+
compact[key] = compactFrontendModelPayloadEntry({key, value: entryValue})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return compact
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Compacts one frontend-model payload entry.
|
|
270
|
+
* @param {{key: string, value: ?}} args - Payload entry.
|
|
271
|
+
* @returns {?} - Compacted value.
|
|
272
|
+
*/
|
|
273
|
+
function compactFrontendModelPayloadEntry({key, value}) {
|
|
274
|
+
if (key !== "attributes" || !value || typeof value !== "object" || Array.isArray(value)) {
|
|
275
|
+
return value
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {__keys: Object.keys(/** @type {Record<string, ?>} */ (value))}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Builds a last-resort compact body summary.
|
|
283
|
+
* @param {?} value - Sanitized body.
|
|
284
|
+
* @param {number} originalSerializedLength - Byte count before redaction or compaction.
|
|
285
|
+
* @returns {Record<string, ?>} - Compact summary.
|
|
286
|
+
*/
|
|
287
|
+
function compactRequestDetailsBody(value, originalSerializedLength) {
|
|
288
|
+
return {
|
|
289
|
+
__truncated: true,
|
|
290
|
+
originalSerializedLength,
|
|
291
|
+
topLevelKeys: value && typeof value === "object" && !Array.isArray(value)
|
|
292
|
+
? Object.keys(/** @type {Record<string, ?>} */ (value))
|
|
293
|
+
: []
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Measures JSON serialized length.
|
|
299
|
+
* @param {?} value - Value to measure.
|
|
300
|
+
* @returns {number} - Serialized length, or Infinity when not serializable.
|
|
301
|
+
*/
|
|
302
|
+
function serializedLength(value) {
|
|
303
|
+
try {
|
|
304
|
+
return JSON.stringify(value).length
|
|
305
|
+
} catch {
|
|
306
|
+
return Infinity
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -8,6 +8,7 @@ import {frontendModelResourcesWithBuiltInsForBackendProject} from "./frontend-mo
|
|
|
8
8
|
import {frontendModelResourceClassFromDefinition, frontendModelResourceConfigurationFromDefinition, frontendModelResourcePath, frontendModelResourcesForBackendProject} from "./frontend-models/resource-definition.js"
|
|
9
9
|
import {FrontendModelQueryError, normalizeGroup as normalizeQueryGroup, normalizeJoins as normalizeQueryJoins, normalizePluck as normalizeQueryPluck, normalizePreload as normalizeQueryPreload, normalizeSearchOperator as normalizeQuerySearchOperator, normalizeSort as normalizeQuerySort} from "./frontend-models/query.js"
|
|
10
10
|
import {assignSafeProperty, deserializeFrontendModelTransportValue, isBackendModelInstance, serializeFrontendModelTransportValue} from "./frontend-models/transport-serialization.js"
|
|
11
|
+
import {requestDetails} from "./error-reporting/request-details.js"
|
|
11
12
|
import RoutesResolver from "./routes/resolver.js"
|
|
12
13
|
import {ValidationError} from "./database/record/index.js"
|
|
13
14
|
import { normalizeDateStringForWrite } from "./database/datetime-storage.js"
|
|
@@ -3183,7 +3184,8 @@ export default class FrontendModelController extends Controller {
|
|
|
3183
3184
|
const errorPayload = {
|
|
3184
3185
|
context: errorContext,
|
|
3185
3186
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
3186
|
-
request: this.getRequest()
|
|
3187
|
+
request: this.getRequest(),
|
|
3188
|
+
requestDetails: requestDetails(this.getRequest())
|
|
3187
3189
|
}
|
|
3188
3190
|
|
|
3189
3191
|
this.getConfiguration().getErrorEvents().emit("framework-error", errorPayload)
|