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.
@@ -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)