@harperfast/harper 5.2.0-beta.4 → 5.2.0

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 (169) hide show
  1. package/bin/backup.ts +169 -0
  2. package/bin/cliOperations.ts +92 -73
  3. package/bin/harper.ts +25 -6
  4. package/components/Scope.ts +52 -6
  5. package/components/componentLoader.ts +107 -9
  6. package/components/mcp/toolRegistry.ts +10 -0
  7. package/components/mcp/tools/application.ts +12 -5
  8. package/components/mcp/tools/operations.ts +3 -0
  9. package/components/mcp/tools/schemas/operationDescriptions.ts +3 -0
  10. package/components/mcp/tools/schemas/operations.ts +9 -0
  11. package/components/operations.js +1 -0
  12. package/components/operationsValidation.js +32 -2
  13. package/components/scopeMount.ts +150 -0
  14. package/config-root.schema.json +4 -0
  15. package/dataLayer/backupManifest.ts +102 -0
  16. package/dataLayer/blobBackup.ts +286 -0
  17. package/dataLayer/harperBridge/ResourceBridge.ts +43 -0
  18. package/dataLayer/restoreMarker.ts +276 -0
  19. package/dataLayer/rocksdbBackup.ts +1100 -0
  20. package/dist/bin/backup.d.ts +9 -0
  21. package/dist/bin/backup.js +192 -0
  22. package/dist/bin/backup.js.map +1 -0
  23. package/dist/bin/cliOperations.d.ts +13 -0
  24. package/dist/bin/cliOperations.js +89 -70
  25. package/dist/bin/cliOperations.js.map +1 -1
  26. package/dist/bin/harper.d.ts +7 -0
  27. package/dist/bin/harper.js +27 -6
  28. package/dist/bin/harper.js.map +1 -1
  29. package/dist/components/Scope.d.ts +37 -1
  30. package/dist/components/Scope.js +48 -6
  31. package/dist/components/Scope.js.map +1 -1
  32. package/dist/components/componentLoader.d.ts +2 -6
  33. package/dist/components/componentLoader.js +91 -8
  34. package/dist/components/componentLoader.js.map +1 -1
  35. package/dist/components/mcp/toolRegistry.d.ts +7 -0
  36. package/dist/components/mcp/toolRegistry.js +10 -0
  37. package/dist/components/mcp/toolRegistry.js.map +1 -1
  38. package/dist/components/mcp/tools/application.js +11 -5
  39. package/dist/components/mcp/tools/application.js.map +1 -1
  40. package/dist/components/mcp/tools/operations.js +3 -0
  41. package/dist/components/mcp/tools/operations.js.map +1 -1
  42. package/dist/components/mcp/tools/schemas/operationDescriptions.js +2 -0
  43. package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
  44. package/dist/components/mcp/tools/schemas/operations.js +9 -0
  45. package/dist/components/mcp/tools/schemas/operations.js.map +1 -1
  46. package/dist/components/operations.js +2 -0
  47. package/dist/components/operations.js.map +1 -1
  48. package/dist/components/operationsValidation.js +34 -2
  49. package/dist/components/operationsValidation.js.map +1 -1
  50. package/dist/components/scopeMount.d.ts +86 -0
  51. package/dist/components/scopeMount.js +131 -0
  52. package/dist/components/scopeMount.js.map +1 -0
  53. package/dist/dataLayer/backupManifest.d.ts +26 -0
  54. package/dist/dataLayer/backupManifest.js +97 -0
  55. package/dist/dataLayer/backupManifest.js.map +1 -0
  56. package/dist/dataLayer/blobBackup.d.ts +87 -0
  57. package/dist/dataLayer/blobBackup.js +282 -0
  58. package/dist/dataLayer/blobBackup.js.map +1 -0
  59. package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +3 -0
  60. package/dist/dataLayer/harperBridge/ResourceBridge.js +33 -0
  61. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  62. package/dist/dataLayer/restoreMarker.d.ts +122 -0
  63. package/dist/dataLayer/restoreMarker.js +261 -0
  64. package/dist/dataLayer/restoreMarker.js.map +1 -0
  65. package/dist/dataLayer/rocksdbBackup.d.ts +127 -0
  66. package/dist/dataLayer/rocksdbBackup.js +1039 -0
  67. package/dist/dataLayer/rocksdbBackup.js.map +1 -0
  68. package/dist/resources/DatabaseTransaction.js +0 -6
  69. package/dist/resources/DatabaseTransaction.js.map +1 -1
  70. package/dist/resources/ResourceInterface.d.ts +0 -3
  71. package/dist/resources/ResourceInterface.js.map +1 -1
  72. package/dist/resources/Table.js +22 -22
  73. package/dist/resources/Table.js.map +1 -1
  74. package/dist/resources/blob.d.ts +8 -0
  75. package/dist/resources/blob.js +16 -7
  76. package/dist/resources/blob.js.map +1 -1
  77. package/dist/resources/databases.d.ts +42 -1
  78. package/dist/resources/databases.js +276 -40
  79. package/dist/resources/databases.js.map +1 -1
  80. package/dist/resources/transaction.js +0 -3
  81. package/dist/resources/transaction.js.map +1 -1
  82. package/dist/server/REST.js +25 -9
  83. package/dist/server/REST.js.map +1 -1
  84. package/dist/server/fastifyRoutes.js +15 -1
  85. package/dist/server/fastifyRoutes.js.map +1 -1
  86. package/dist/server/itc/serverHandlers.js +7 -1
  87. package/dist/server/itc/serverHandlers.js.map +1 -1
  88. package/dist/server/jobs/jobProcess.js +20 -1
  89. package/dist/server/jobs/jobProcess.js.map +1 -1
  90. package/dist/server/jobs/jobRunner.js +10 -0
  91. package/dist/server/jobs/jobRunner.js.map +1 -1
  92. package/dist/server/jobs/jobs.js +11 -0
  93. package/dist/server/jobs/jobs.js.map +1 -1
  94. package/dist/server/middlewareChain.d.ts +10 -1
  95. package/dist/server/middlewareChain.js +81 -21
  96. package/dist/server/middlewareChain.js.map +1 -1
  97. package/dist/server/serverHelpers/serverHandlers.js +8 -4
  98. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  99. package/dist/server/serverHelpers/serverUtilities.js +11 -0
  100. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  101. package/dist/server/static.js +11 -3
  102. package/dist/server/static.js.map +1 -1
  103. package/dist/utility/OperationFunctionCaller.js +25 -6
  104. package/dist/utility/OperationFunctionCaller.js.map +1 -1
  105. package/dist/utility/hdbTerms.d.ts +11 -1
  106. package/dist/utility/hdbTerms.js +11 -1
  107. package/dist/utility/hdbTerms.js.map +1 -1
  108. package/dist/utility/logging/harper_logger.d.ts +40 -0
  109. package/dist/utility/logging/harper_logger.js +746 -14
  110. package/dist/utility/logging/harper_logger.js.map +1 -1
  111. package/dist/utility/operation_authorization.js +13 -3
  112. package/dist/utility/operation_authorization.js.map +1 -1
  113. package/npm-shrinkwrap.json +3 -2
  114. package/package.json +4 -1
  115. package/resources/DESIGN.md +2 -0
  116. package/resources/DatabaseTransaction.ts +0 -3
  117. package/resources/ResourceInterface.ts +0 -3
  118. package/resources/Table.ts +20 -20
  119. package/resources/blob.ts +16 -6
  120. package/resources/databases.ts +272 -42
  121. package/resources/transaction.ts +0 -3
  122. package/server/DESIGN.md +16 -0
  123. package/server/REST.ts +25 -9
  124. package/server/fastifyRoutes.ts +20 -1
  125. package/server/itc/serverHandlers.js +7 -1
  126. package/server/jobs/jobProcess.ts +18 -1
  127. package/server/jobs/jobRunner.ts +10 -0
  128. package/server/jobs/jobs.ts +11 -0
  129. package/server/middlewareChain.ts +79 -20
  130. package/server/serverHelpers/serverHandlers.js +8 -4
  131. package/server/serverHelpers/serverUtilities.ts +19 -0
  132. package/server/static.ts +12 -3
  133. package/static/defaultConfig.yaml +1 -0
  134. package/studio/web/assets/{Chat-DK3GlWEb.js → Chat-DoVWScmq.js} +2 -2
  135. package/studio/web/assets/{Chat-DK3GlWEb.js.map → Chat-DoVWScmq.js.map} +1 -1
  136. package/studio/web/assets/{FloatingChat-fBcC1Ew_.js → FloatingChat-UZ2NsUOZ.js} +4 -4
  137. package/studio/web/assets/{FloatingChat-fBcC1Ew_.js.map → FloatingChat-UZ2NsUOZ.js.map} +1 -1
  138. package/studio/web/assets/{apiToken-DJo1nakA.js → apiToken-BUI_04o7.js} +2 -2
  139. package/studio/web/assets/{apiToken-DJo1nakA.js.map → apiToken-BUI_04o7.js.map} +1 -1
  140. package/studio/web/assets/{applications-BDfH8urd.js → applications-D03NA7wW.js} +2 -2
  141. package/studio/web/assets/{applications-BDfH8urd.js.map → applications-D03NA7wW.js.map} +1 -1
  142. package/studio/web/assets/{index-BHo3c2Gk.js → index-Bh_CNAHr.js} +6 -6
  143. package/studio/web/assets/index-Bh_CNAHr.js.map +1 -0
  144. package/studio/web/assets/{index.lazy-BTo0y6UM.js → index.lazy-Dx3MpyDC.js} +4 -4
  145. package/studio/web/assets/{index.lazy-BTo0y6UM.js.map → index.lazy-Dx3MpyDC.js.map} +1 -1
  146. package/studio/web/assets/{notifications-CMxvWNnz.js → notifications-0edoFTsb.js} +2 -2
  147. package/studio/web/assets/{notifications-CMxvWNnz.js.map → notifications-0edoFTsb.js.map} +1 -1
  148. package/studio/web/assets/{notifications-D3GoB26g.js → notifications-CwKhipK7.js} +2 -2
  149. package/studio/web/assets/{notifications-D3GoB26g.js.map → notifications-CwKhipK7.js.map} +1 -1
  150. package/studio/web/assets/{profile-Doj5FVDE.js → profile-DUfEPQtx.js} +2 -2
  151. package/studio/web/assets/{profile-Doj5FVDE.js.map → profile-DUfEPQtx.js.map} +1 -1
  152. package/studio/web/assets/{setComponentFile-yinsqJy0.js → setComponentFile-DMPo4UjC.js} +2 -2
  153. package/studio/web/assets/{setComponentFile-yinsqJy0.js.map → setComponentFile-DMPo4UjC.js.map} +1 -1
  154. package/studio/web/assets/{setup-DJwR0BHd.js → setup-B56Oz1_u.js} +2 -2
  155. package/studio/web/assets/{setup-DJwR0BHd.js.map → setup-B56Oz1_u.js.map} +1 -1
  156. package/studio/web/assets/{status-Br_AbsJs.js → status-BAod7p3o.js} +2 -2
  157. package/studio/web/assets/{status-Br_AbsJs.js.map → status-BAod7p3o.js.map} +1 -1
  158. package/studio/web/assets/{swagger-ui-react-02XH5sVf.js → swagger-ui-react-lQrBxfwM.js} +2 -2
  159. package/studio/web/assets/{swagger-ui-react-02XH5sVf.js.map → swagger-ui-react-lQrBxfwM.js.map} +1 -1
  160. package/studio/web/assets/{tsMode-D1DMKY7h.js → tsMode-CrHCRjTK.js} +2 -2
  161. package/studio/web/assets/{tsMode-D1DMKY7h.js.map → tsMode-CrHCRjTK.js.map} +1 -1
  162. package/studio/web/assets/{useEntityRestURL-DtRblRgw.js → useEntityRestURL-DoaBMEvU.js} +2 -2
  163. package/studio/web/assets/{useEntityRestURL-DtRblRgw.js.map → useEntityRestURL-DoaBMEvU.js.map} +1 -1
  164. package/studio/web/index.html +1 -1
  165. package/utility/OperationFunctionCaller.ts +24 -3
  166. package/utility/hdbTerms.ts +11 -1
  167. package/utility/logging/harper_logger.ts +729 -15
  168. package/utility/operation_authorization.ts +31 -3
  169. package/studio/web/assets/index-BHo3c2Gk.js.map +0 -1
@@ -184,35 +184,69 @@ async function updateLogSettings() {
184
184
  }
185
185
 
186
186
  /**
187
- * True when the argument is an Error (same-realm or native cross-realm). The try/catch guards
188
- * exotic objects whose prototype is unreachable (e.g. a revoked Proxy, where `instanceof`
189
- * throws) the logger must never throw on any input, and util.format renders those fine raw.
187
+ * True when the argument is an Error (same-realm or native cross-realm component code runs
188
+ * through node:vm, so a VM-created Error fails `instanceof Error` but passes `isNativeError`).
189
+ * Classified via `util.types.isNativeError` alone, never `instanceof Error`: `instanceof` walks
190
+ * the prototype chain via `[[GetPrototypeOf]]`, which for a value like
191
+ * `Object.create(Object.create(proxyAncestor))` eventually reaches `proxyAncestor` and invokes
192
+ * ITS `getPrototypeOf` trap, even though `arg` itself is an ordinary object and not a Proxy - a
193
+ * trap that hangs (an infinite loop) or has side effects would run to completion on whatever path
194
+ * called isErrorLike (e.g. logging a DIFFERENT operation's failure). `isNativeError` checks an
195
+ * internal slot directly, the same realm-independent, prototype-chain-independent mechanism
196
+ * `types.isMap`/`types.isDate`/etc. use elsewhere in this file, so it recognizes same-realm,
197
+ * cross-realm (VM), and Error-subclass instances without ever walking a prototype chain. The
198
+ * try/catch below still guards other exotic objects for other reasons — the logger must never
199
+ * throw on any input, and util.format renders those fine raw. Exported so call sites outside this
200
+ * module (e.g. OperationFunctionCaller, deciding how to log an error-shaped value it didn't itself
201
+ * catch as an Error) can reuse the same classification instead of a weaker local `instanceof`
202
+ * check.
190
203
  */
191
- function isErrorLike(arg: any): boolean {
204
+ export function isErrorLike(arg: any): boolean {
192
205
  try {
193
- return arg instanceof Error || isNativeError(arg);
206
+ return isNativeError(arg);
194
207
  } catch {
195
208
  return false;
196
209
  }
197
210
  }
198
211
 
212
+ /**
213
+ * True when `arg` needs sanitizeErrorArg's replacement before reaching Console's raw formatting:
214
+ * either it's error-like, or it's a live Proxy that isErrorLike deliberately reports as false (see
215
+ * isErrorLike above) but that Console's default inspect would still reflect on directly - which,
216
+ * if the Proxy happens to wrap an Error, dumps its own-enumerable properties unsanitized (#1734).
217
+ */
218
+ function needsSanitizing(arg: any): boolean {
219
+ return types.isProxy(arg) || isErrorLike(arg);
220
+ }
221
+
222
+ /**
223
+ * Replaces an Error (or a Proxy - live-or-revoked, whatever it wraps) with a safe placeholder
224
+ * before it reaches Console's util.inspect formatting; same Proxy-first ordering and rationale as
225
+ * deepSanitizeErrors' `types.isProxy` check (see there) - reflecting on a Proxy at all, even just
226
+ * to classify it, risks running a hostile trap on the shallow logging path too.
227
+ */
228
+ function sanitizeErrorArg(arg: any) {
229
+ if (types.isProxy(arg)) return labelPlaceholder('[Proxy]');
230
+ return isErrorLike(arg) ? errorForLog(arg) : arg;
231
+ }
232
+
199
233
  /**
200
234
  * Replaces every Error argument with its log-safe errorForLog wrapper before the args reach
201
235
  * Console's util.inspect formatting, which would otherwise dump the error's own-enumerable
202
236
  * properties — where libraries and app code stash credentials (axios config headers, an
203
237
  * hdb_secret for an outbound Authorization header) — into hdb.log (see #1734 and errorForLog).
204
238
  * Called inside each level gate so filtered-out log calls pay nothing beyond the arg scan,
205
- * and only allocates when an Error is actually present. Deliberately shallow: an Error nested
206
- * inside a logged object/array is not rewritten (deep-walking every logged structure is not
207
- * worth the per-call cost, and the #1734 threat is raw thrown errors).
239
+ * and only allocates when an Error (or Proxy needing the same treatment) is actually present.
240
+ * Deliberately shallow: an Error nested inside a logged object/array is not rewritten (deep-
241
+ * walking every logged structure is not worth the per-call cost, and the #1734 threat is raw
242
+ * thrown errors).
208
243
  */
209
244
  function sanitizeErrorArgs(args: any[]) {
210
245
  for (let i = 0; i < args.length; i++) {
211
- if (isErrorLike(args[i])) {
246
+ if (needsSanitizing(args[i])) {
212
247
  const sanitized = args.slice(0, i);
213
248
  for (let j = i; j < args.length; j++) {
214
- const arg = args[j];
215
- sanitized[j] = isErrorLike(arg) ? errorForLog(arg) : arg;
249
+ sanitized[j] = sanitizeErrorArg(args[j]);
216
250
  }
217
251
  return sanitized;
218
252
  }
@@ -296,10 +330,10 @@ class HarperLogger extends Console {
296
330
  super.log(...sanitizeErrorArgs(args));
297
331
  }
298
332
  dir(item, options?) {
299
- super.dir(isErrorLike(item) ? errorForLog(item) : item, options);
333
+ super.dir(sanitizeErrorArg(item), options);
300
334
  }
301
335
  table(data, columns?) {
302
- super.table(Array.isArray(data) ? sanitizeErrorArgs(data) : isErrorLike(data) ? errorForLog(data) : data, columns);
336
+ super.table(Array.isArray(data) ? sanitizeErrorArgs(data) : sanitizeErrorArg(data), columns);
303
337
  }
304
338
  withTag(tag) {
305
339
  return loggerWithTag(tag, true, this);
@@ -344,6 +378,8 @@ module.exports = {
344
378
  startOnMainThread: updateLogSettings,
345
379
  errorToString,
346
380
  errorForLog,
381
+ inspectForLog,
382
+ isErrorLike,
347
383
  disableStdio,
348
384
  externalLogger,
349
385
  };
@@ -985,8 +1021,12 @@ function renderErrorLine(error: any): string {
985
1021
  const base = typeof error?.stack === 'string' ? error.stack : errorToString(error);
986
1022
  return base + loggablePropsSuffix(error);
987
1023
  } catch (err) {
988
- // error?.stack itself can throw on a hostile object even though errorToString cannot.
989
- return `[Unrenderable Error: ${err instanceof Error ? err.message : String(err)}]`;
1024
+ // error?.stack itself can throw on a hostile object (e.g. a getter on a `cause` chain member
1025
+ // that throws a revoked Proxy) - `err` is then whatever was thrown, so it must be rendered via
1026
+ // errorToString, the only renderer in this file explicitly guaranteed never to throw regardless
1027
+ // of input. `err instanceof Error`/`String(err)` are NOT safe here: both throw on a revoked
1028
+ // Proxy, which would defeat this catch's whole purpose.
1029
+ return `[Unrenderable Error: ${errorToString(err)}]`;
990
1030
  }
991
1031
  }
992
1032
 
@@ -1042,6 +1082,678 @@ export function errorForLog(error: any) {
1042
1082
  return { [inspect.custom]: render, toString: render };
1043
1083
  }
1044
1084
 
1085
+ // Bounds deepSanitizeErrors' walk: deep enough to reach any realistic diagnostic payload shape,
1086
+ // capped so a pathological/adversarial structure can't blow the stack.
1087
+ const MAX_SANITIZE_DEPTH = 20;
1088
+
1089
+ // Default per-container breadth cap (used when inspectForLog's caller didn't request a specific
1090
+ // maxArrayLength) and an absolute ceiling on total nodes visited across the WHOLE walk. Per-
1091
+ // container breadth alone isn't enough: a structure that is merely wide at every one of
1092
+ // MAX_SANITIZE_DEPTH levels multiplies out to an astronomical node count, so a global counter is
1093
+ // the actual backstop. Both exist because sanitizing happens BEFORE util.inspect's own
1094
+ // maxArrayLength truncation runs - a huge/sparse array or a huge Map/Set in a structured error
1095
+ // (`new Array(0xffffffff)`, or a hostile component's crafted payload) would otherwise force this
1096
+ // walk to visit billions of entries and wedge the event loop while just trying to log the
1097
+ // *original* error, before inspect ever gets a chance to truncate the output.
1098
+ const DEFAULT_MAX_SANITIZE_ENTRIES = 1000;
1099
+ const MAX_SANITIZE_NODES = 50_000;
1100
+ // Hard ceiling on the per-container breadth budget, regardless of what a caller requests via
1101
+ // inspectForLog's `maxArrayLength` option. Without this, a caller passing an unbounded value
1102
+ // (`Infinity`, or just a very large finite one) sets a container's own loop limit that high too -
1103
+ // so even though deepSanitizeErrors itself starts returning cheap placeholders once
1104
+ // MAX_SANITIZE_NODES is exhausted, the PARENT loop (the one iterating a huge array/Map/Set's
1105
+ // entries) still runs for its full stated limit before that ever kicks in, defeating the budget as
1106
+ // a real ceiling on total work. The one caller in this codebase passes 250; this only bites a
1107
+ // caller that deliberately (or by bug) requests something far larger.
1108
+ const HARD_MAX_SANITIZE_ENTRIES = 10_000;
1109
+
1110
+ interface SanitizeBudget {
1111
+ nodes: number;
1112
+ maxEntries: number;
1113
+ }
1114
+
1115
+ /** A util.inspect-style placeholder rendered without invoking anything, used both for an unread
1116
+ * accessor property and for a budget-truncated container tail. */
1117
+ function labelPlaceholder(label: string) {
1118
+ return { [inspect.custom]: () => label, toString: () => label };
1119
+ }
1120
+
1121
+ /** A util.inspect-style placeholder for an accessor property, describing it without invoking the
1122
+ * getter — see the getter-invocation note on deepSanitizeErrors below. */
1123
+ function accessorPlaceholder(descriptor: PropertyDescriptor) {
1124
+ return labelPlaceholder(
1125
+ descriptor.get && descriptor.set ? '[Getter/Setter]' : descriptor.get ? '[Getter]' : '[Setter]'
1126
+ );
1127
+ }
1128
+
1129
+ /**
1130
+ * Placeholder substituted when a recursive sanitize step on a child throws, instead of falling
1131
+ * back to that child's raw (unsanitized) value. A throw here is not a reason to skip sanitizing -
1132
+ * it is exactly the case a hostile value produces (e.g. a Proxy whose `ownKeys` trap throws once
1133
+ * reached one level down), and the child that triggered it may itself contain an unsanitized
1134
+ * Error. Falling back to raw would silently hand that Error to inspect() at the raised depth,
1135
+ * recreating the #1734 leak this whole walk exists to prevent.
1136
+ */
1137
+ function sanitizeFailurePlaceholder() {
1138
+ return labelPlaceholder('[Unrenderable value: sanitize failed]');
1139
+ }
1140
+
1141
+ // Unique symbol keys for the plain-object breadth-cap markers below, rather than a string key
1142
+ // like the array/Map/Set truncation markers use - a hostile or just plain unlucky object could
1143
+ // have an own string property literally named the same as a string marker, silently colliding
1144
+ // with (and hiding) real data. A locally-scoped Symbol can never collide with an enumerable
1145
+ // string OR pre-existing symbol key on the original value.
1146
+ const KEYS_TRUNCATED_MARKER = Symbol('sanitize: string-keyed properties truncated');
1147
+ const SYMBOLS_TRUNCATED_MARKER = Symbol('sanitize: symbol-keyed properties truncated');
1148
+
1149
+ /**
1150
+ * True for the specific built-ins whose actual data is NOT reachable through their own-enumerable
1151
+ * string/symbol keys, so rebuilding them via a property walk would silently corrupt their
1152
+ * rendering (Object.keys(new Date()) is `[]`; a Buffer's bytes live in a typed-array internal
1153
+ * slot, not enumerable own properties). Everything else - an object literal, a class instance, a
1154
+ * VM cross-realm object of either - IS walked and rebuilt: a class or custom-prototype instance is
1155
+ * just as capable of holding a nested Error as a plain object (`http_resp_msg` is a generic field,
1156
+ * not limited to the known deploy payload), and leaving instances raw would hand the raised inspect
1157
+ * depth below a real, generic secret-leak path. `types.is*` checks an internal slot, not the
1158
+ * prototype chain, so - like isNativeError / types.isMap / types.isSet elsewhere in this file -
1159
+ * this is realm-independent: a VM-created Date is still recognized as opaque.
1160
+ *
1161
+ * Promise is deliberately NOT included here (handled separately in deepSanitizeErrors, see below):
1162
+ * unlike the others, a Promise's resolved/rejected value is not merely internal-slot data with a
1163
+ * fixed rendering, it's arbitrary caller data - and util.inspect renders it directly, own-enumerable
1164
+ * properties and all (`util.inspect(Promise.resolve(errorWithSecretHeader), { depth: 8 })` prints
1165
+ * the header). There is no supported synchronous way to read that value in order to sanitize it, so
1166
+ * the only safe option is to never hand a Promise to inspect() raw.
1167
+ *
1168
+ * An *expando* own-enumerable property stashed directly on one of these (e.g. `const d = new
1169
+ * Date(); d.cause = secretError`) would otherwise leak the same way the Promise case above does -
1170
+ * inspect() renders own-enumerable properties on ANY object, opaque built-ins included. Handled by
1171
+ * hasEnumerableOwnProps/safeOpaqueBuiltinSummary below: the fast, zero-cost, common path (no
1172
+ * expando) returns the value raw and untouched; only a value actually carrying one pays for a safe
1173
+ * replacement.
1174
+ */
1175
+ function isOpaqueBuiltin(value: object): boolean {
1176
+ return (
1177
+ types.isDate(value) ||
1178
+ types.isRegExp(value) ||
1179
+ types.isArrayBufferView(value) || // covers Buffer and every TypedArray/DataView
1180
+ types.isAnyArrayBuffer(value) ||
1181
+ types.isWeakMap(value) ||
1182
+ types.isWeakSet(value) ||
1183
+ types.isBoxedPrimitive(value) // a boxed Boolean/Number/String/Symbol/BigInt wrapper
1184
+ );
1185
+ }
1186
+
1187
+ // %TypedArray%.prototype - the shared abstract superclass prototype every concrete TypedArray
1188
+ // (Uint8Array, Buffer, etc.) inherits `length` from. Read once so hasEnumerableOwnProps can bind
1189
+ // to it explicitly via Reflect.get, the same hijack-avoidance pattern as the Map/Set branches'
1190
+ // `Reflect.get(Map.prototype, 'size', value)` - the intrinsic `length` getter is spec-configurable,
1191
+ // so a hostile value can shadow it with an own `length` property reporting whatever it likes.
1192
+ const TYPED_ARRAY_PROTO = Object.getPrototypeOf(Uint8Array.prototype);
1193
+
1194
+ // Above this many elements, hasEnumerableOwnProps skips its indexed-type check and fails closed
1195
+ // (assumes an expando IS present, safe direction) rather than paying for it - see the comment at
1196
+ // that check for why.
1197
+ const MAX_INDEXED_EXPANDO_CHECK_LENGTH = 10_000;
1198
+
1199
+ /**
1200
+ * True if `value` has any own-enumerable string or symbol property beyond its own intrinsic data -
1201
+ * i.e. an expando - since none of isOpaqueBuiltin's types (nor a bare function) normally carry any.
1202
+ * Checked before deciding whether an opaque built-in/function is safe to hand to inspect() raw.
1203
+ * Errs conservative: if the check itself cannot be completed safely (a hostile `ownKeys`/descriptor
1204
+ * trap), treat that as "has an expando" rather than risk a false "clean" on something we couldn't
1205
+ * actually verify.
1206
+ *
1207
+ * A TypedArray/Buffer's own numeric indices ARE its intrinsic byte/element data, own-enumerable
1208
+ * exactly like any other array - `Object.keys(Buffer.from('hi'))` is `['0', '1']` - so those don't
1209
+ * count as expandos here; only a key beyond `[0, length)` does. A boxed String has the same shape
1210
+ * (`Object.keys(new String('hi'))` is the same `['0', '1']`) and gets the same carve-out; detected
1211
+ * via `types.isStringObject` (an internal-slot check, like `isOpaqueBuiltin`'s `types.is*` checks
1212
+ * above) rather than `Object.prototype.toString.call`, which reads the value's own
1213
+ * `Symbol.toStringTag` property - a hostile value defining that as a getter would have it run
1214
+ * during this supposedly passive classification. DataView has no such index
1215
+ * properties (its data is accessed only via get/set methods), so it's excluded from the carve-out
1216
+ * and goes through the plain `Object.keys(value).length > 0` check like any ordinary object.
1217
+ *
1218
+ * For either indexed shape, checking for an expando against `Object.keys` costs one array
1219
+ * allocation sized to the FULL length just to answer "clean or not" - fine for a small buffer, but
1220
+ * a multi-hundred-MB Buffer or huge boxed string would materialize a matching number of key strings
1221
+ * merely to decide this supposedly-cheap fast path applies, turning it into an attacker-sized scan
1222
+ * on the error-logging path itself. Above MAX_INDEXED_EXPANDO_CHECK_LENGTH elements, skip the check
1223
+ * and FAIL CLOSED (assume it HAS an expando) rather than skip it and assume clean: the safe
1224
+ * direction when we can't verify is always "has an expando" (same as the hostile-trap catch above),
1225
+ * never "verified clean" - failing open here would return e.g. a `Buffer.alloc(10_001)` carrying a
1226
+ * real, ordinary-sized expando raw, unsanitized. Failing closed still avoids the expensive scan
1227
+ * (that's the whole point of the size cap) - it just means a huge but genuinely expando-free
1228
+ * Buffer/string also renders as safeOpaqueBuiltinSummary's generic placeholder above this size,
1229
+ * rather than its full native format. Losing pretty-printing for a buffer/string that large is a
1230
+ * reasonable trade for never guessing "clean" on data we didn't actually check.
1231
+ */
1232
+ function hasEnumerableOwnProps(value: object): boolean {
1233
+ try {
1234
+ const isTypedArray = ArrayBuffer.isView(value) && !types.isDataView(value);
1235
+ const isBoxedString = types.isStringObject(value);
1236
+ if (isTypedArray || isBoxedString) {
1237
+ // The typed-array getter is bound explicitly (see TYPED_ARRAY_PROTO above) to survive a
1238
+ // shadowed own `length`; a boxed String's `length` is a non-configurable, non-writable own
1239
+ // property per spec and can't be shadowed, so a direct read is already safe.
1240
+ const length = isTypedArray ? Reflect.get(TYPED_ARRAY_PROTO, 'length', value) : (value as any).length;
1241
+ if (typeof length !== 'number') return true; // couldn't verify the intrinsic size - conservative
1242
+ if (length > MAX_INDEXED_EXPANDO_CHECK_LENGTH) return true; // too large to check - fail closed, not clean
1243
+ const keys = Object.keys(value);
1244
+ for (const key of keys) {
1245
+ const index = key === '' ? NaN : Number(key);
1246
+ if (!(Number.isInteger(index) && index >= 0 && index < length && String(index) === key)) return true;
1247
+ }
1248
+ } else if (Object.keys(value).length > 0) {
1249
+ return true;
1250
+ }
1251
+ for (const sym of Object.getOwnPropertySymbols(value)) {
1252
+ if (Object.getOwnPropertyDescriptor(value, sym)?.enumerable) return true;
1253
+ }
1254
+ return false;
1255
+ } catch {
1256
+ return true;
1257
+ }
1258
+ }
1259
+
1260
+ /**
1261
+ * Returns a value safe to hand to inspect() in place of an opaque built-in that (unusually) carries
1262
+ * an expando property - reached only via hasEnumerableOwnProps returning true, never on the common
1263
+ * expando-free path. Date/RegExp/WeakMap/WeakSet are cheap to reconstruct byte/value-for-value from
1264
+ * their intrinsic prototype (bound explicitly via .call/Reflect.get, not `value.getTime()` etc,
1265
+ * exactly so an own property shadowing that method - the same class of hijack the Map/Set branches
1266
+ * below guard against - can't run instead of the real accessor); WeakMap/WeakSet never expose their
1267
+ * entries via inspect regardless, so a fresh empty instance loses nothing. Buffer/TypedArray/
1268
+ * DataView/ArrayBuffer/boxed-primitives are left as a bounded type-tag summary instead: safely
1269
+ * reconstructing an exact byte-for-byte or value-for-value copy needs per-subtype branching that
1270
+ * isn't worth it for how rarely one of these ever carries an expando in the first place.
1271
+ */
1272
+ function safeOpaqueBuiltinSummary(value: object): any {
1273
+ try {
1274
+ if (types.isDate(value)) return new Date(Date.prototype.getTime.call(value));
1275
+ if (types.isRegExp(value)) {
1276
+ const source = Reflect.get(RegExp.prototype, 'source', value);
1277
+ // Built manually from each individual flag getter (bound via Reflect.get, same
1278
+ // hijack-avoidance pattern as source/Map/Set elsewhere in this file) rather than reading
1279
+ // the combined `RegExp.prototype.flags` getter: per spec, `flags` synthesizes its result by
1280
+ // reading `this.global`, `this.ignoreCase`, etc. as ordinary property gets on `value` -
1281
+ // each individual flag getter (`global`, `ignoreCase`, ...) instead reads the internal
1282
+ // [[OriginalFlags]] slot directly, the same as `source`. An expando shadowing e.g. `global`
1283
+ // with a hostile getter would otherwise have `flags` invoke it while merely trying to
1284
+ // reconstruct a safe copy of the RegExp.
1285
+ let flags = '';
1286
+ if (Reflect.get(RegExp.prototype, 'hasIndices', value)) flags += 'd';
1287
+ if (Reflect.get(RegExp.prototype, 'global', value)) flags += 'g';
1288
+ if (Reflect.get(RegExp.prototype, 'ignoreCase', value)) flags += 'i';
1289
+ if (Reflect.get(RegExp.prototype, 'multiline', value)) flags += 'm';
1290
+ if (Reflect.get(RegExp.prototype, 'dotAll', value)) flags += 's';
1291
+ if (Reflect.get(RegExp.prototype, 'unicode', value)) flags += 'u';
1292
+ if (Reflect.get(RegExp.prototype, 'unicodeSets', value)) flags += 'v';
1293
+ if (Reflect.get(RegExp.prototype, 'sticky', value)) flags += 'y';
1294
+ return new RegExp(source, flags);
1295
+ }
1296
+ if (types.isWeakMap(value)) return new WeakMap();
1297
+ if (types.isWeakSet(value)) return new WeakSet();
1298
+ } catch {
1299
+ // fall through to the generic tag-only summary below
1300
+ }
1301
+ // A fixed, types.is*-derived tag - never Object.prototype.toString.call(value), which reads the
1302
+ // value's own Symbol.toStringTag and would invoke a hostile getter defined there while merely
1303
+ // picking a label (the same class of bug fixed in isErrorLike's classification - see there). Every
1304
+ // branch here is an internal-slot check, so none of them can run caller-controlled code.
1305
+ let tag = 'value';
1306
+ if (types.isStringObject(value)) tag = 'String';
1307
+ else if (types.isNumberObject(value)) tag = 'Number';
1308
+ else if (types.isBooleanObject(value)) tag = 'Boolean';
1309
+ else if (types.isSymbolObject(value)) tag = 'Symbol';
1310
+ else if (types.isArrayBufferView(value)) tag = 'ArrayBufferView';
1311
+ else if (types.isAnyArrayBuffer(value)) tag = 'ArrayBuffer';
1312
+ return labelPlaceholder(`[${tag} with own properties omitted for safety]`);
1313
+ }
1314
+
1315
+ /** Defines `key` as an own DATA property via defineProperty rather than `target[key] = value`.
1316
+ * Once a sanitized clone's prototype is restored to the original's (see deepSanitizeErrors), a
1317
+ * plain assignment for a key that has an inherited accessor further up that prototype chain would
1318
+ * invoke the INHERITED SETTER instead of creating an own property - running arbitrary code during
1319
+ * what should be a passive render - and a key literally named `__proto__` would hit the legacy
1320
+ * Object.prototype.__proto__ setter and reparent the clone instead of storing a property named
1321
+ * "__proto__". defineProperty always creates/replaces an own property directly, regardless of key
1322
+ * name or what the prototype chain declares. */
1323
+ function defineOwnProperty(target: any, key: string | symbol, value: any) {
1324
+ Object.defineProperty(target, key, { value, writable: true, enumerable: true, configurable: true });
1325
+ }
1326
+
1327
+ /**
1328
+ * True if `proto` (and every prototype above it, up to but excluding Object.prototype/null) is
1329
+ * safe for a stateless sanitized clone to wear - i.e. util.inspect can later render a value with
1330
+ * this prototype without running any caller-controlled code.
1331
+ *
1332
+ * This used to be answered by actually calling `inspect()` on the empty clone as a probe - but
1333
+ * that IS running caller-controlled code, just earlier and silently: `inspect()` walks the full
1334
+ * prototype chain (not just the immediate level) doing its own class-name/tag detection, which
1335
+ * invokes a Proxy ANCESTOR's traps (`types.isProxy(proto)` alone only catches the immediate
1336
+ * level), reads an inherited `Symbol.toStringTag` getter, and - even with `customInspect: false`
1337
+ * suppressing the clone's OWN custom inspector - still leaves that same inspector to be invoked a
1338
+ * second time by the real render later. A hostile trap/getter that never returns wedges the
1339
+ * logging worker regardless of which of those two invocations reaches it.
1340
+ *
1341
+ * So this never invokes anything: `types.isProxy` and `Object.getOwnPropertyNames`/
1342
+ * `getOwnPropertyDescriptor`/`getPrototypeOf` are internal-slot/structural reads on each
1343
+ * non-Proxy level, same as the rest of this file's reflection. A level is rejected if it's a
1344
+ * Proxy (any trap, including on revocation) or defines any OWN accessor (getter/setter) property
1345
+ * - the exact shape of a branded built-in's internal-slot getter (e.g. URL.prototype's
1346
+ * `href`/`protocol`/...), and there is no way to tell "safe getter" from "hostile getter" without
1347
+ * invoking it, so any accessor anywhere in the chain fails closed. An ordinary class's prototype
1348
+ * (only a data `constructor` property, plus perhaps plain methods) passes, so a plain
1349
+ * custom-class instance still renders with its real class name (see the 'Diagnostic' test).
1350
+ */
1351
+ function isSafeToWearPrototype(proto: object): boolean {
1352
+ let level: any = proto;
1353
+ while (level !== null && level !== Object.prototype) {
1354
+ if (types.isProxy(level)) return false;
1355
+ let keys: (string | symbol)[];
1356
+ try {
1357
+ keys = [...Object.getOwnPropertyNames(level), ...Object.getOwnPropertySymbols(level)];
1358
+ } catch {
1359
+ return false;
1360
+ }
1361
+ for (const key of keys) {
1362
+ if (key === 'constructor') continue; // an ordinary data property on every class prototype
1363
+ let descriptor;
1364
+ try {
1365
+ descriptor = Object.getOwnPropertyDescriptor(level, key);
1366
+ } catch {
1367
+ return false;
1368
+ }
1369
+ if (descriptor && (descriptor.get || descriptor.set)) return false;
1370
+ }
1371
+ try {
1372
+ level = Object.getPrototypeOf(level);
1373
+ } catch {
1374
+ return false;
1375
+ }
1376
+ }
1377
+ return true;
1378
+ }
1379
+
1380
+ /**
1381
+ * Recursively walks a plain object/array, replacing every Error-like value found at any depth
1382
+ * with its errorForLog wrapper (see isErrorLike/errorForLog and #1734). sanitizeErrorArgs is
1383
+ * deliberately shallow because it guards the hot, frequent top-level log-call path — this walk is
1384
+ * for inspectForLog's callers instead, which are low-frequency (a caught error's diagnostic
1385
+ * detail), so the extra traversal cost doesn't matter and full coverage does: an Error nested
1386
+ * anywhere inside a value later rendered with a raised inspect depth (see inspectForLog) would
1387
+ * otherwise surface its own-enumerable properties raw.
1388
+ *
1389
+ * Arrays, Map, Set, and every other object EXCEPT the isOpaqueBuiltin exclusions above are rebuilt
1390
+ * so an Error nested inside them at any depth is still reached and sanitized - a raw Error left
1391
+ * unsanitized anywhere in the tree would surface its own-enumerable properties (e.g. an axios
1392
+ * `config.headers.Authorization`) raw once the raised inspect depth below reaches it. A rebuilt
1393
+ * object/class-instance clone has the original's prototype restored (Object.setPrototypeOf) so
1394
+ * inspect() still shows its real class name and picks up any inspect.custom hook defined on the
1395
+ * class's prototype (not an own property, so the symbol walk below wouldn't otherwise see it) -
1396
+ * the clone differs from the original only in which of its OWN enumerable properties got swapped
1397
+ * for a sanitized/placeholder value, same as it would for a plain object.
1398
+ *
1399
+ * Cycle-safe via a WeakMap from original to its (in-progress) clone, registered before recursing
1400
+ * into children, so a cycle resolves to the clone in progress rather than falling back to the raw
1401
+ * original (which would bypass sanitization on the repeated branch) - and depth-capped so a
1402
+ * pathologically deep structure can't blow the stack. Every property read and recursive step is
1403
+ * individually guarded so one hostile getter or exotic nested value can only cost that one
1404
+ * field, not the whole render (inspectForLog's own try/catch around inspect() is still the final
1405
+ * backstop regardless).
1406
+ *
1407
+ * Never invokes an accessor (getter) property, and never resolves an overridable Symbol.iterator:
1408
+ * unlike util.inspect's default (which shows a getter as `[Getter]` without calling it), reading
1409
+ * `value[key]`/`value[sym]` for every own-enumerable key - or iterating an array/Map/Set with
1410
+ * `for...of`, which resolves the value's own-or-inherited `Symbol.iterator` - would run arbitrary
1411
+ * synchronous code, including a subclass instance's overridden iterator, while the logger is just
1412
+ * trying to report the *original* error (a hostile accessor/iterator that loops, blocks, or
1413
+ * mutates state runs regardless). Arrays are walked by own property descriptor per index instead
1414
+ * of `for...of`; Map/Set are walked via their intrinsic prototype methods bound with `.call`,
1415
+ * which reads the internal slot data directly rather than going through the instance's own (or an
1416
+ * overriding subclass's) iterator method. Objects read property descriptors and recurse only into
1417
+ * a data descriptor's value; an accessor gets accessorPlaceholder's label instead. Every own
1418
+ * property is written via defineOwnProperty rather than assignment, so a clone whose prototype was
1419
+ * restored to a class's prototype can't trigger an inherited setter (or the legacy `__proto__`
1420
+ * setter) partway through the walk.
1421
+ *
1422
+ * Breadth-bounded via `budget`, shared across the whole walk (not reset per container): each
1423
+ * container is capped at `budget.maxEntries` entries (with a placeholder noting what was
1424
+ * skipped), and the walk stops sanitizing entirely past `MAX_SANITIZE_NODES` total nodes visited,
1425
+ * regardless of per-container caps - see the constants' comment for why both are needed.
1426
+ */
1427
+ function deepSanitizeErrors(
1428
+ value: any,
1429
+ seen: WeakMap<object, any> = new WeakMap(),
1430
+ depth = 0,
1431
+ budget: SanitizeBudget = { nodes: 0, maxEntries: DEFAULT_MAX_SANITIZE_ENTRIES }
1432
+ ): any {
1433
+ // Functions are `typeof 'function'`, not 'object' - included here (rather than falling through
1434
+ // as if they were a harmless primitive) because a function is just as capable of carrying an
1435
+ // expando own-enumerable property (`fn.cause = secretError`, an ordinary and not-even-unusual
1436
+ // JS pattern) as a plain object is, and inspect() renders those own properties the same way.
1437
+ // Counted first, before even the primitive-leaf check below, so a primitive leaf (string/number/
1438
+ // etc in an array/object, the overwhelmingly common case) consumes exactly as much of the shared
1439
+ // budget as a container does. Without this, only container nodes counted towards
1440
+ // MAX_SANITIZE_NODES while each one's up-to-budget.maxEntries primitive children were free, so
1441
+ // e.g. 50,000 containers of 250 primitive fields each could still walk ~12.5 million values
1442
+ // before this cap ever engaged.
1443
+ if (++budget.nodes > MAX_SANITIZE_NODES) return labelPlaceholder('[Unrenderable value: sanitize budget exceeded]');
1444
+ if (value === null || (typeof value !== 'object' && typeof value !== 'function')) return value;
1445
+ // Checked before isErrorLike/anything else that reflects on `value`: types.isProxy queries the
1446
+ // exotic object's internal slots directly (safe even on a revoked Proxy - it never throws), but
1447
+ // EVERY reflective operation past this point - `instanceof` inside isErrorLike, Array.isArray,
1448
+ // Object.getPrototypeOf, Object.keys - invokes whatever trap the Proxy's handler defines for it.
1449
+ // A throwing trap was already caught by the try/catch below, but a trap that hangs (an infinite
1450
+ // loop) or has side effects runs to completion regardless of any catch - on the very path meant
1451
+ // to log a DIFFERENT failure. There is no privileged way in userland to read a Proxy's target
1452
+ // without going through its traps (the exact reason native util.inspect's default rendering,
1453
+ // which does have that privileged access, isn't reusable here - see the Promise note above for
1454
+ // the same class of "can't safely introspect this" tradeoff), so a Proxy is never reflected on
1455
+ // at all: always a safe, static placeholder, trap or no trap.
1456
+ if (types.isProxy(value)) return labelPlaceholder('[Proxy]');
1457
+ if (isErrorLike(value)) return errorForLog(value);
1458
+ if (seen.has(value)) return seen.get(value);
1459
+ // Depth-capped values are never handed back raw: an Error directly AT this depth is already
1460
+ // caught by isErrorLike above, but a plain container here could still hold an Error somewhere
1461
+ // inside it that we're choosing not to recurse into - handing it back unsanitized would let a
1462
+ // caller-requested depth greater than MAX_SANITIZE_DEPTH (the one caller in this codebase uses
1463
+ // 8, well under it) reach and print that Error's own-enumerable properties raw (#1734).
1464
+ if (depth >= MAX_SANITIZE_DEPTH) return labelPlaceholder('[Unrenderable value: sanitize depth exceeded]');
1465
+
1466
+ let isArray: boolean, isMap: boolean, isSet: boolean, isFunction: boolean;
1467
+ try {
1468
+ isArray = Array.isArray(value);
1469
+ // types.isMap/isSet check an internal slot, not the prototype chain, so (like isNativeError)
1470
+ // they still recognize a Map/Set created in a different realm (component code runs through
1471
+ // node:vm) - `instanceof Map`/`Set` would not, silently leaving that container's contents
1472
+ // (and any Error nested inside) unsanitized at the raised inspect depth below.
1473
+ isMap = !isArray && types.isMap(value);
1474
+ isSet = !isArray && !isMap && types.isSet(value);
1475
+ isFunction = !isArray && !isMap && !isSet && typeof value === 'function';
1476
+ // See the Promise note on isOpaqueBuiltin above: its resolved/rejected value is arbitrary
1477
+ // caller data that inspect() renders directly (own-enumerable properties included), and there
1478
+ // is no supported synchronous way to read it in order to sanitize it - so unlike every other
1479
+ // opaque built-in, a Promise is never handed to inspect() raw, sanitized or not.
1480
+ if (!isArray && !isMap && !isSet && types.isPromise(value)) return labelPlaceholder('[Promise]');
1481
+ if (!isArray && !isMap && !isSet && !isFunction && isOpaqueBuiltin(value)) {
1482
+ // Fast, faithful, zero-cost path for the overwhelming common case: no expando, so the
1483
+ // value is returned exactly as-is and inspect() renders its normal native format. Only a
1484
+ // value that actually carries an expando pays for safeOpaqueBuiltinSummary below.
1485
+ if (!hasEnumerableOwnProps(value)) return value;
1486
+ return safeOpaqueBuiltinSummary(value);
1487
+ }
1488
+ if (isFunction && !hasEnumerableOwnProps(value)) return value; // same fast path, for functions
1489
+ } catch {
1490
+ // The types.isProxy check above already routes every Proxy (revoked or not) to a placeholder
1491
+ // before this block ever runs, so nothing YET IDENTIFIED reaches this catch on real input -
1492
+ // but on the same "never fall back to raw" principle as every other catch in this walk, a
1493
+ // safe placeholder costs nothing here and closes the door on whatever unforeseen way a
1494
+ // future value might still throw here.
1495
+ return sanitizeFailurePlaceholder();
1496
+ }
1497
+ // A function reaching here carries an expando: fall through into the generic object walk below
1498
+ // (Object.keys/getOwnPropertyDescriptor/getOwnPropertySymbols all work the same on a function as
1499
+ // on a plain object) so that expando gets sanitized like any other object's - the clone won't be
1500
+ // callable and inspect() renders it as a plain `Function { ... }` rather than `[Function: name]`,
1501
+ // but that's a safe, honest trade for a case inspect() would otherwise render with a raw,
1502
+ // unsanitized secret alongside it.
1503
+
1504
+ if (isArray) {
1505
+ const clone: any[] = [];
1506
+ seen.set(value, clone);
1507
+ const length = value.length;
1508
+ // Reserve one slot for the truncation marker when overflowing, so the clone's final size is
1509
+ // exactly budget.maxEntries - matching whatever maxArrayLength the caller will inspect() with
1510
+ // - rather than maxEntries + 1, which util.inspect's OWN truncation would then clip anyway,
1511
+ // silently hiding the marker (and the fact that anything was dropped at all) behind its
1512
+ // generic "... N more items" ellipsis.
1513
+ const overflow = length > budget.maxEntries;
1514
+ const limit = overflow ? budget.maxEntries - 1 : length;
1515
+ for (let i = 0; i < limit; i++) {
1516
+ let descriptor;
1517
+ try {
1518
+ descriptor = Object.getOwnPropertyDescriptor(value, i);
1519
+ } catch {
1520
+ continue; // leave index i a hole rather than risk a second throw
1521
+ }
1522
+ if (!descriptor) continue; // a genuine sparse-array hole - leave it, not `undefined`
1523
+ if (descriptor.get || descriptor.set) {
1524
+ clone[i] = accessorPlaceholder(descriptor);
1525
+ continue;
1526
+ }
1527
+ try {
1528
+ clone[i] = deepSanitizeErrors(descriptor.value, seen, depth + 1, budget);
1529
+ } catch {
1530
+ clone[i] = sanitizeFailurePlaceholder();
1531
+ }
1532
+ }
1533
+ if (overflow) clone[limit] = labelPlaceholder(`[${length - limit} more array entries omitted (sanitize budget)]`);
1534
+ return clone;
1535
+ }
1536
+
1537
+ if (isMap) {
1538
+ const clone = new Map();
1539
+ seen.set(value, clone);
1540
+ // Reflect.get with an explicit receiver reads Map.prototype's intrinsic `size` getter bound
1541
+ // to value's internal slot, bypassing an overriding subclass's own `size` the same way the
1542
+ // .call-bound entries() below bypasses an overriding subclass's Symbol.iterator.
1543
+ const size = Reflect.get(Map.prototype, 'size', value);
1544
+ const overflow = size > budget.maxEntries;
1545
+ const limit = overflow ? budget.maxEntries - 1 : size;
1546
+ let count = 0;
1547
+ // Map.prototype.entries bound via .call reads the internal [[MapData]] slot directly,
1548
+ // rather than resolving value's own (or an overriding subclass's) Symbol.iterator.
1549
+ for (const [k, v] of Map.prototype.entries.call(value)) {
1550
+ if (count++ >= limit) break;
1551
+ // Sanitized independently (rather than in one combined try) so a throw sanitizing the key
1552
+ // doesn't also discard an already-sanitized value, or vice versa - each side falls back to
1553
+ // its own placeholder, never to the other's raw counterpart.
1554
+ let sanitizedKey, sanitizedValue;
1555
+ try {
1556
+ sanitizedKey = deepSanitizeErrors(k, seen, depth + 1, budget);
1557
+ } catch {
1558
+ sanitizedKey = sanitizeFailurePlaceholder();
1559
+ }
1560
+ try {
1561
+ sanitizedValue = deepSanitizeErrors(v, seen, depth + 1, budget);
1562
+ } catch {
1563
+ sanitizedValue = sanitizeFailurePlaceholder();
1564
+ }
1565
+ clone.set(sanitizedKey, sanitizedValue);
1566
+ }
1567
+ if (overflow)
1568
+ clone.set(
1569
+ labelPlaceholder('[truncated]'),
1570
+ labelPlaceholder(`[${size - limit} more Map entries omitted (sanitize budget)]`)
1571
+ );
1572
+ return clone;
1573
+ }
1574
+
1575
+ if (isSet) {
1576
+ const clone = new Set();
1577
+ seen.set(value, clone);
1578
+ const size = Reflect.get(Set.prototype, 'size', value);
1579
+ const overflow = size > budget.maxEntries;
1580
+ const limit = overflow ? budget.maxEntries - 1 : size;
1581
+ let count = 0;
1582
+ // Same rationale as the Map branch above: Set.prototype.values via .call, not for...of.
1583
+ for (const item of Set.prototype.values.call(value)) {
1584
+ if (count++ >= limit) break;
1585
+ try {
1586
+ clone.add(deepSanitizeErrors(item, seen, depth + 1, budget));
1587
+ } catch {
1588
+ clone.add(sanitizeFailurePlaceholder());
1589
+ }
1590
+ }
1591
+ if (overflow) clone.add(labelPlaceholder(`[${size - limit} more Set entries omitted (sanitize budget)]`));
1592
+ return clone;
1593
+ }
1594
+
1595
+ const result: Record<string | symbol, any> = {};
1596
+ seen.set(value, result);
1597
+ try {
1598
+ const proto = Object.getPrototypeOf(value);
1599
+ // A branded built-in with private internal slots (URL, Headers, Request/Response, or any
1600
+ // custom class whose own accessors read `#private` state) throws when util.inspect later
1601
+ // renders a value wearing its prototype but lacking its real internal state - and losing it
1602
+ // unguarded would surface all the way up to inspectForLog's outer catch and replace the
1603
+ // ENTIRE structured payload - not just this one nested field - with a single "[Unrenderable
1604
+ // value]", losing phase/install_output/deployment_id and everything else alongside it, for
1605
+ // one URL-shaped field anywhere in the tree.
1606
+ //
1607
+ // This USED to be answered by actually calling `inspect()` on the empty clone as a probe, but
1608
+ // that runs the same caller-controlled code it's trying to guard against, just earlier and
1609
+ // silently: a Proxy ANYWHERE in the ancestor chain (not just the immediate `proto`) still gets
1610
+ // its traps invoked by inspect's own class-name/tag walk, an inherited `Symbol.toStringTag`
1611
+ // getter still gets read, and even `customInspect: false` only stops the PROBE's own call from
1612
+ // invoking a custom inspector - the real render right after still invokes it a second time,
1613
+ // corrupting a stateful/one-shot inspector's actual output. `isSafeToWearPrototype` answers the
1614
+ // same question without invoking anything (see its doc comment); `proto` is restored only when
1615
+ // it says so. Skipped for `null`/`Object.prototype`, the overwhelmingly common case for a
1616
+ // JSON-like diagnostic payload, where no exotic prototype is ever attached and this check would
1617
+ // be pure overhead for a class that could never fail it.
1618
+ if (proto !== null && proto !== Object.prototype && isSafeToWearPrototype(proto)) {
1619
+ Object.setPrototypeOf(result, proto);
1620
+ }
1621
+ } catch {
1622
+ // Fall through with the clone's default Object.prototype - inspect() renders it without the
1623
+ // class name, but every actual property attached below is still safe to render (see above for
1624
+ // why the prototype itself, not the data, is what's unsafe here).
1625
+ }
1626
+ // Object.keys/getOwnPropertySymbols themselves are one unavoidable O(n) pass (a plain object,
1627
+ // unlike Array/Map/Set, has no O(1) size to check before enumerating) - but everything AFTER
1628
+ // that (getOwnPropertyDescriptor + recurse + defineProperty per key) is the expensive part, and
1629
+ // that part IS capped at budget.maxEntries, same as every other container branch, so a
1630
+ // million-key object can't turn a single log call into a million-entry clone.
1631
+ const keys = Object.keys(value);
1632
+ const keysOverflow = keys.length > budget.maxEntries;
1633
+ const keysLimit = keysOverflow ? budget.maxEntries - 1 : keys.length;
1634
+ for (let i = 0; i < keysLimit; i++) {
1635
+ const key = keys[i];
1636
+ let descriptor;
1637
+ try {
1638
+ descriptor = Object.getOwnPropertyDescriptor(value, key);
1639
+ } catch {
1640
+ continue; // a hostile descriptor trap - omit rather than risk a second throw
1641
+ }
1642
+ if (!descriptor) continue; // removed mid-walk by another property's getter side effect
1643
+ if (descriptor.get || descriptor.set) {
1644
+ defineOwnProperty(result, key, accessorPlaceholder(descriptor));
1645
+ continue;
1646
+ }
1647
+ try {
1648
+ defineOwnProperty(result, key, deepSanitizeErrors(descriptor.value, seen, depth + 1, budget));
1649
+ } catch {
1650
+ defineOwnProperty(result, key, sanitizeFailurePlaceholder());
1651
+ }
1652
+ }
1653
+ if (keysOverflow)
1654
+ defineOwnProperty(
1655
+ result,
1656
+ KEYS_TRUNCATED_MARKER,
1657
+ labelPlaceholder(`[${keys.length - keysLimit} more properties omitted (sanitize budget)]`)
1658
+ );
1659
+ const symbols = Object.getOwnPropertySymbols(value);
1660
+ const symbolsOverflow = symbols.length > budget.maxEntries;
1661
+ const symbolsLimit = symbolsOverflow ? budget.maxEntries - 1 : symbols.length;
1662
+ for (let i = 0; i < symbolsLimit; i++) {
1663
+ const sym = symbols[i];
1664
+ let descriptor;
1665
+ try {
1666
+ descriptor = Object.getOwnPropertyDescriptor(value, sym);
1667
+ } catch {
1668
+ continue;
1669
+ }
1670
+ if (!descriptor?.enumerable) continue;
1671
+ if (sym === inspect.custom) {
1672
+ // The custom renderer itself, not data - preserve unchanged (even if defined via an
1673
+ // accessor) rather than replace it with an accessor placeholder. util.inspect's own
1674
+ // top-level render finds this hook via the exact same `value[sym]` property access, so
1675
+ // reading it here isn't new arbitrary-code exposure the way an ordinary data getter
1676
+ // would be - it's the documented render-hook contract, just resolved one level earlier.
1677
+ let original;
1678
+ try {
1679
+ original = value[sym];
1680
+ } catch {
1681
+ continue; // leave unset - inspect() will render the rest of the object without a custom hook
1682
+ }
1683
+ if (typeof original !== 'function') continue; // not a valid hook - nothing to wrap or invoke
1684
+ // The hook itself runs at REAL render time, entirely outside this walk - so whatever it
1685
+ // RETURNS has never been through deepSanitizeErrors. Left as-is, a hook that returns a
1686
+ // closure-captured secret-bearing Error (or any value with one nested inside) would have
1687
+ // that Error rendered raw by the outer util.inspect call, bypassing this sanitizer entirely
1688
+ // (#1994 review). This wraps the hook so the CALL is unchanged (still invoked with `this`
1689
+ // bound to the original untrusted `value`, same as inspect would do directly) but its return
1690
+ // value is sanitized with a fresh depth/budget - independent of this walk's own counters,
1691
+ // which by render time are long since exhausted or out of scope.
1692
+ defineOwnProperty(result, sym, (...args: any[]) => {
1693
+ let rendered;
1694
+ try {
1695
+ rendered = original.apply(value, args);
1696
+ } catch (err) {
1697
+ return `[Unrenderable value: ${errorToString(err)}]`;
1698
+ }
1699
+ return deepSanitizeErrors(rendered, new WeakMap(), 0, { nodes: 0, maxEntries: DEFAULT_MAX_SANITIZE_ENTRIES });
1700
+ });
1701
+ continue;
1702
+ }
1703
+ if (descriptor.get || descriptor.set) {
1704
+ defineOwnProperty(result, sym, accessorPlaceholder(descriptor));
1705
+ continue;
1706
+ }
1707
+ try {
1708
+ defineOwnProperty(result, sym, deepSanitizeErrors(descriptor.value, seen, depth + 1, budget));
1709
+ } catch {
1710
+ defineOwnProperty(result, sym, sanitizeFailurePlaceholder());
1711
+ }
1712
+ }
1713
+ if (symbolsOverflow)
1714
+ defineOwnProperty(
1715
+ result,
1716
+ SYMBOLS_TRUNCATED_MARKER,
1717
+ labelPlaceholder(`[${symbols.length - symbolsLimit} more symbol properties omitted (sanitize budget)]`)
1718
+ );
1719
+ return result;
1720
+ }
1721
+
1722
+ /**
1723
+ * Returns a log-safe lazy wrapper around `util.inspect(value, options)`, for call sites that need
1724
+ * to log a structured, non-Error value with non-default inspect options (e.g. a deeper depth or
1725
+ * higher array/string limits than Console's defaults, to avoid flattening nested diagnostic data —
1726
+ * see harper#1982). Same rationale as errorForLog: the wrapper defers the (potentially expensive,
1727
+ * e.g. large-array) render until the logger's level gate actually writes the entry, and the render
1728
+ * itself can never throw regardless of what `value` is — including a hostile nested
1729
+ * `[util.inspect.custom]` hook — so a formatting failure can never mask the real thing being
1730
+ * logged (e.g. replace the caught operation error with an inspect error). Any Error nested inside
1731
+ * `value` is sanitized via deepSanitizeErrors before rendering, so a raised inspect depth here
1732
+ * can't surface a nested Error's own-enumerable properties (#1734) the way the raw value would.
1733
+ * `options.maxArrayLength`, if given, also bounds sanitization's own per-container breadth (see
1734
+ * deepSanitizeErrors' budget) - a caller raising the render limit is raising how much genuinely
1735
+ * needs to be walked, not just how much of an already-cheap walk gets displayed.
1736
+ */
1737
+ export function inspectForLog(value: any, options?: any) {
1738
+ const render = () => {
1739
+ try {
1740
+ const maxEntries = Number(options?.maxArrayLength);
1741
+ const budget: SanitizeBudget = {
1742
+ nodes: 0,
1743
+ maxEntries: maxEntries > 0 ? Math.min(maxEntries, HARD_MAX_SANITIZE_ENTRIES) : DEFAULT_MAX_SANITIZE_ENTRIES,
1744
+ };
1745
+ return inspect(deepSanitizeErrors(value, new WeakMap(), 0, budget), options);
1746
+ } catch (err) {
1747
+ // errorToString is the guaranteed-never-throw stringifier (unlike `err instanceof Error` or
1748
+ // `String(err)` here, both of which can themselves throw on a hostile value - e.g. a revoked
1749
+ // Proxy thrown by a nested custom-inspect hook - which would otherwise escape this catch and
1750
+ // mask the real operation error being logged).
1751
+ return `[Unrenderable value: ${errorToString(err)}]`;
1752
+ }
1753
+ };
1754
+ return { [inspect.custom]: render, toString: render };
1755
+ }
1756
+
1045
1757
  export function setMainLogger(logger: any) {
1046
1758
  mainLogger = logger;
1047
1759
  }
@@ -1101,4 +1813,6 @@ export default {
1101
1813
  AuthAuditLog,
1102
1814
  errorToString,
1103
1815
  errorForLog,
1816
+ inspectForLog,
1817
+ isErrorLike,
1104
1818
  };