@naturalcycles/nodejs-lib 15.121.0 → 15.122.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.
@@ -277,8 +277,15 @@ class Exec2 {
277
277
  }
278
278
  }
279
279
  logFinish(cmd, opt, started, isSuccessful) {
280
- if (isSuccessful && !opt.logFinish)
281
- return;
280
+ if (!opt.logFinish) {
281
+ if (isSuccessful)
282
+ return;
283
+ // Failures are logged even with logFinish: false,
284
+ // unless explicitly silenced with `log: false`
285
+ // (e.g when the caller buffers the output to print it later, like runCheck does)
286
+ if (opt.log === false)
287
+ return;
288
+ }
282
289
  console.log([
283
290
  isSuccessful ? ' ✓' : ' ×',
284
291
  white(opt.name || _substringAfterLast(cmd, '/')),
@@ -182,9 +182,11 @@ export class JWTService {
182
182
  if (err instanceof errors.JOSEError) {
183
183
  return new JWTInvalidError(err.message, errorData);
184
184
  }
185
- if (this.cfg.verifyAlgorithms && err instanceof TypeError) {
185
+ if (this.cfg.verifyAlgorithms && (err instanceof TypeError || err instanceof DOMException)) {
186
186
  // With multiple verifyAlgorithms, a token/key algorithm mismatch is reachable
187
- // by untrusted input, and jose reports it as TypeError - treat it as an invalid token
187
+ // by untrusted input, and jose reports it as TypeError, or (since jose 6.2.6,
188
+ // which prepares keys via Node's KeyObject.toCryptoKey) as DOMException
189
+ // "DataError: Invalid key type" - treat it as an invalid token
188
190
  return new JWTInvalidError(err.message, errorData);
189
191
  }
190
192
  return err;
@@ -2,7 +2,7 @@ import type { ValidationFunction, ValidationFunctionResult } from '@naturalcycle
2
2
  import type { Set2 } from '@naturalcycles/js-lib/object';
3
3
  import type { AnyObject, BaseDBEntity, IANATimezone, Inclusiveness, IsoDate, IsoDateTime, IsoMonth, NumberEnum, StringEnum, StringMap, UnixTimestamp, UnixTimestampMillis } from '@naturalcycles/js-lib/types';
4
4
  import type { StandardJSONSchemaV1, StandardSchemaV1 } from '@standard-schema/spec';
5
- import type { Ajv } from 'ajv';
5
+ import type { Ajv, ValidateFunction } from 'ajv';
6
6
  import { AjvValidationError } from './ajvValidationError.js';
7
7
  export declare const j: {
8
8
  /**
@@ -480,12 +480,12 @@ export declare class AjvSchema<OUT> {
480
480
  * Creates a minimal AjvSchema wrapper from a pre-compiled validate function.
481
481
  * Used internally by JSchema to cache a compatible AjvSchema instance.
482
482
  */
483
- static _wrap<OUT>(schema: JsonSchema<OUT>, compiledFn: any): AjvSchema<OUT>;
483
+ static _wrap<OUT>(schema: JsonSchema<OUT>, compiledFn: ValidateFunction): AjvSchema<OUT>;
484
484
  static isSchemaWithCachedAjvSchema<Base, OUT>(schema: Base): schema is WithCachedAjvSchema<Base, OUT>;
485
485
  static cacheAjvSchema<Base extends AnyObject, OUT>(schema: Base, ajvSchema: AjvSchema<OUT>): WithCachedAjvSchema<Base, OUT>;
486
486
  static requireCachedAjvSchema<Base, OUT>(schema: WithCachedAjvSchema<Base, OUT>): AjvSchema<OUT>;
487
487
  readonly cfg: AjvSchemaCfg;
488
- private _compiledFn;
488
+ private _compiledFn?;
489
489
  private _getValidateFn;
490
490
  /**
491
491
  * It returns the original object just for convenience.
@@ -1174,6 +1174,9 @@ function executeValidation(fn, builtSchema, input, opt = {}, defaultInputName) {
1174
1174
  fn.errors = [
1175
1175
  {
1176
1176
  instancePath: '',
1177
+ schemaPath: '',
1178
+ keyword: 'postValidation',
1179
+ params: {},
1177
1180
  message: err.message,
1178
1181
  },
1179
1182
  ];
@@ -1190,15 +1193,34 @@ function executeValidation(fn, builtSchema, input, opt = {}, defaultInputName) {
1190
1193
  // Build fingerprint before applyImprovementsOnErrorMessages: after it, /items/0/name becomes
1191
1194
  // .items[0].name, embedding the index into the segment and making it harder to strip without regex
1192
1195
  const fingerprint = buildAjvErrorFingerprint(errors[0], inputName, resolveCustomErrorMessage(builtSchema, errors[0]));
1193
- applyImprovementsOnErrorMessages(errors, builtSchema);
1194
- let message = getAjv().errorsText(errors, {
1195
- dataVar,
1196
- separator,
1197
- });
1198
1196
  // Note: if we mutated the input already, e.g stripped unknown properties,
1199
1197
  // the error message Input would contain already mutated object print, such as Input: {}
1200
1198
  // Unless `getOriginalInput` function is provided - then it will be used to preserve the Input pureness.
1201
- const inputStringified = _inspect(opt.getOriginalInput?.() || input, { maxLen: 4000 });
1199
+ const inputForPrint = opt.getOriginalInput?.() || input;
1200
+ // Resolve each error's failing value while its instancePath is still a raw JSON pointer
1201
+ // (e.g `/events/2/name`), which is unambiguous to walk - unlike the dot notation it gets
1202
+ // rewritten into below (a property name may itself contain a dot).
1203
+ // Root errors (empty instancePath) resolve to undefined - there the value is the whole Input,
1204
+ // which is printed anyway.
1205
+ const failingValueByError = new Map(errors.map(e => [
1206
+ e,
1207
+ e.instancePath ? getValueAtJsonPointer(inputForPrint, e.instancePath) : undefined,
1208
+ ]));
1209
+ applyImprovementsOnErrorMessages(errors, builtSchema);
1210
+ // Same line format as Ajv's errorsText().
1211
+ // Each nested error line additionally ends with the narrow failing value itself (`, got: ...`),
1212
+ // so it stays visible even when the Input print below gets truncated for large inputs.
1213
+ let message = errors
1214
+ .map(e => {
1215
+ let line = `${dataVar}${e.instancePath} ${e.message}`;
1216
+ const value = failingValueByError.get(e);
1217
+ if (value !== undefined) {
1218
+ line += `, got: ${_inspect(value, { maxLen: 500 })}`;
1219
+ }
1220
+ return line;
1221
+ })
1222
+ .join(separator);
1223
+ const inputStringified = _inspect(inputForPrint, { maxLen: 4000 });
1202
1224
  message = [message, 'Input: ' + inputStringified].join(separator);
1203
1225
  const err = new AjvValidationError(message, _filterNullishValues({
1204
1226
  errors,
@@ -1209,6 +1231,19 @@ function executeValidation(fn, builtSchema, input, opt = {}, defaultInputName) {
1209
1231
  return [err, output];
1210
1232
  }
1211
1233
  // ==== Error formatting helpers ====
1234
+ /**
1235
+ * Resolves the value at an Ajv error's raw instancePath, which is a JSON pointer, e.g `/events/2/name`.
1236
+ * Returns undefined if the path cannot be resolved (e.g the input was mutated).
1237
+ */
1238
+ function getValueAtJsonPointer(input, pointer) {
1239
+ let value = input;
1240
+ for (const segment of pointer.split('/').slice(1)) {
1241
+ if (value === null || typeof value !== 'object')
1242
+ return undefined;
1243
+ value = value[segment.replaceAll('~1', '/').replaceAll('~0', '~')];
1244
+ }
1245
+ return value;
1246
+ }
1212
1247
  function applyImprovementsOnErrorMessages(errors, schema) {
1213
1248
  if (!errors)
1214
1249
  return;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.121.0",
4
+ "version": "15.122.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@standard-schema/spec": "^1",
@@ -14,8 +14,8 @@
14
14
  "yaml": "^2"
15
15
  },
16
16
  "devDependencies": {
17
- "typescript": "^7",
18
- "@naturalcycles/dev-lib": "0.0.0"
17
+ "@naturalcycles/dev-lib": "0.0.0",
18
+ "typescript": "^7"
19
19
  },
20
20
  "exports": {
21
21
  ".": "./dist/index.js",
@@ -332,7 +332,13 @@ class Exec2 {
332
332
  started: UnixTimestampMillis,
333
333
  isSuccessful: boolean,
334
334
  ): void {
335
- if (isSuccessful && !opt.logFinish) return
335
+ if (!opt.logFinish) {
336
+ if (isSuccessful) return
337
+ // Failures are logged even with logFinish: false,
338
+ // unless explicitly silenced with `log: false`
339
+ // (e.g when the caller buffers the output to print it later, like runCheck does)
340
+ if (opt.log === false) return
341
+ }
336
342
 
337
343
  console.log(
338
344
  [
@@ -379,9 +379,11 @@ export class JWTService<T extends AnyObject = AnyObject> {
379
379
  if (err instanceof errors.JOSEError) {
380
380
  return new JWTInvalidError(err.message, errorData)
381
381
  }
382
- if (this.cfg.verifyAlgorithms && err instanceof TypeError) {
382
+ if (this.cfg.verifyAlgorithms && (err instanceof TypeError || err instanceof DOMException)) {
383
383
  // With multiple verifyAlgorithms, a token/key algorithm mismatch is reachable
384
- // by untrusted input, and jose reports it as TypeError - treat it as an invalid token
384
+ // by untrusted input, and jose reports it as TypeError, or (since jose 6.2.6,
385
+ // which prepares keys via Node's KeyObject.toCryptoKey) as DOMException
386
+ // "DataError: Invalid key type" - treat it as an invalid token
385
387
  return new JWTInvalidError(err.message, errorData)
386
388
  }
387
389
  return err as Error
@@ -27,7 +27,7 @@ import type {
27
27
  } from '@naturalcycles/js-lib/types'
28
28
  import { _objectAssign, _typeCast, JWT_REGEX } from '@naturalcycles/js-lib/types'
29
29
  import type { StandardJSONSchemaV1, StandardSchemaV1 } from '@standard-schema/spec'
30
- import type { Ajv, ErrorObject } from 'ajv'
30
+ import type { Ajv, ErrorObject, ValidateFunction } from 'ajv'
31
31
  import { _inspect } from '../../string/inspect.js'
32
32
  import {
33
33
  BASE64URL_REGEX,
@@ -367,7 +367,7 @@ export class JSchema<OUT, Opt>
367
367
  }
368
368
 
369
369
  private _builtSchema?: JsonSchema
370
- private _compiledFns?: WeakMap<Ajv, any>
370
+ private _compiledFns?: WeakMap<Ajv, ValidateFunction>
371
371
 
372
372
  private _getBuiltSchema(): JsonSchema {
373
373
  if (!this._builtSchema) {
@@ -387,7 +387,7 @@ export class JSchema<OUT, Opt>
387
387
  return this._builtSchema
388
388
  }
389
389
 
390
- private _getCompiled(overrideAjv?: Ajv): { fn: any; builtSchema: JsonSchema } {
390
+ private _getCompiled(overrideAjv?: Ajv): { fn: ValidateFunction; builtSchema: JsonSchema } {
391
391
  const builtSchema = this._getBuiltSchema()
392
392
  const ajv = overrideAjv ?? this._cfg?.ajv ?? getAjv()
393
393
 
@@ -1105,18 +1105,22 @@ export class JObject<OUT extends AnyObject, Opt extends boolean = false> extends
1105
1105
  OUT,
1106
1106
  {
1107
1107
  // required keys
1108
- [K in keyof P as P[K] extends JSchema<any, infer IsOpt>
1109
- ? IsOpt extends true
1110
- ? never
1111
- : K
1112
- : never]: P[K] extends JSchema<infer OUT2, any> ? OUT2 : never
1108
+ [
1109
+ K in keyof P as P[K] extends JSchema<any, infer IsOpt>
1110
+ ? IsOpt extends true
1111
+ ? never
1112
+ : K
1113
+ : never
1114
+ ]: P[K] extends JSchema<infer OUT2, any> ? OUT2 : never
1113
1115
  } & {
1114
1116
  // optional keys
1115
- [K in keyof P as P[K] extends JSchema<any, infer IsOpt>
1116
- ? IsOpt extends true
1117
- ? K
1117
+ [
1118
+ K in keyof P as P[K] extends JSchema<any, infer IsOpt>
1119
+ ? IsOpt extends true
1120
+ ? K
1121
+ : never
1118
1122
  : never
1119
- : never]?: P[K] extends JSchema<infer OUT2, any> ? OUT2 : never
1123
+ ]?: P[K] extends JSchema<infer OUT2, any> ? OUT2 : never
1120
1124
  }
1121
1125
  >
1122
1126
  >,
@@ -1185,17 +1189,21 @@ export class JObjectInfer<
1185
1189
  > extends JBuilder<
1186
1190
  Expand<
1187
1191
  {
1188
- [K in keyof PROPS as PROPS[K] extends JSchema<any, infer IsOpt>
1189
- ? IsOpt extends true
1190
- ? never
1191
- : K
1192
- : never]: PROPS[K] extends JSchema<infer OUT, any> ? OUT : never
1192
+ [
1193
+ K in keyof PROPS as PROPS[K] extends JSchema<any, infer IsOpt>
1194
+ ? IsOpt extends true
1195
+ ? never
1196
+ : K
1197
+ : never
1198
+ ]: PROPS[K] extends JSchema<infer OUT, any> ? OUT : never
1193
1199
  } & {
1194
- [K in keyof PROPS as PROPS[K] extends JSchema<any, infer IsOpt>
1195
- ? IsOpt extends true
1196
- ? K
1200
+ [
1201
+ K in keyof PROPS as PROPS[K] extends JSchema<any, infer IsOpt>
1202
+ ? IsOpt extends true
1203
+ ? K
1204
+ : never
1197
1205
  : never
1198
- : never]?: PROPS[K] extends JSchema<infer OUT, any> ? OUT : never
1206
+ ]?: PROPS[K] extends JSchema<infer OUT, any> ? OUT : never
1199
1207
  }
1200
1208
  >,
1201
1209
  Opt
@@ -1516,7 +1524,7 @@ export class AjvSchema<OUT> {
1516
1524
  private constructor(
1517
1525
  public schema: JsonSchema<OUT>,
1518
1526
  cfg: Partial<AjvSchemaCfg> = {},
1519
- preCompiledFn?: any,
1527
+ preCompiledFn?: ValidateFunction,
1520
1528
  ) {
1521
1529
  this.cfg = {
1522
1530
  lazy: false,
@@ -1585,7 +1593,7 @@ export class AjvSchema<OUT> {
1585
1593
  * Creates a minimal AjvSchema wrapper from a pre-compiled validate function.
1586
1594
  * Used internally by JSchema to cache a compatible AjvSchema instance.
1587
1595
  */
1588
- static _wrap<OUT>(schema: JsonSchema<OUT>, compiledFn: any): AjvSchema<OUT> {
1596
+ static _wrap<OUT>(schema: JsonSchema<OUT>, compiledFn: ValidateFunction): AjvSchema<OUT> {
1589
1597
  return new AjvSchema<OUT>(schema, {}, compiledFn)
1590
1598
  }
1591
1599
 
@@ -1608,9 +1616,9 @@ export class AjvSchema<OUT> {
1608
1616
 
1609
1617
  readonly cfg: AjvSchemaCfg
1610
1618
 
1611
- private _compiledFn: any
1619
+ private _compiledFn?: ValidateFunction
1612
1620
 
1613
- private _getValidateFn(): any {
1621
+ private _getValidateFn(): ValidateFunction {
1614
1622
  this._compiledFn ||= this.cfg.ajv.compile(this.schema as any)
1615
1623
  return this._compiledFn
1616
1624
  }
@@ -1663,7 +1671,7 @@ export class AjvSchema<OUT> {
1663
1671
  const separator = '\n'
1664
1672
 
1665
1673
  function executeValidation<OUT>(
1666
- fn: any,
1674
+ fn: ValidateFunction,
1667
1675
  builtSchema: JsonSchema,
1668
1676
  input: unknown,
1669
1677
  opt: AjvValidationOptions = {},
@@ -1685,6 +1693,9 @@ function executeValidation<OUT>(
1685
1693
  fn.errors = [
1686
1694
  {
1687
1695
  instancePath: '',
1696
+ schemaPath: '',
1697
+ keyword: 'postValidation',
1698
+ params: {},
1688
1699
  message: err.message,
1689
1700
  },
1690
1701
  ]
@@ -1706,22 +1717,45 @@ function executeValidation<OUT>(
1706
1717
  // Build fingerprint before applyImprovementsOnErrorMessages: after it, /items/0/name becomes
1707
1718
  // .items[0].name, embedding the index into the segment and making it harder to strip without regex
1708
1719
  const fingerprint = buildAjvErrorFingerprint(
1709
- errors[0],
1720
+ errors[0]!,
1710
1721
  inputName,
1711
- resolveCustomErrorMessage(builtSchema, errors[0]),
1722
+ resolveCustomErrorMessage(builtSchema, errors[0]!),
1712
1723
  )
1713
1724
 
1714
- applyImprovementsOnErrorMessages(errors, builtSchema)
1715
-
1716
- let message = getAjv().errorsText(errors, {
1717
- dataVar,
1718
- separator,
1719
- })
1720
-
1721
1725
  // Note: if we mutated the input already, e.g stripped unknown properties,
1722
1726
  // the error message Input would contain already mutated object print, such as Input: {}
1723
1727
  // Unless `getOriginalInput` function is provided - then it will be used to preserve the Input pureness.
1724
- const inputStringified = _inspect(opt.getOriginalInput?.() || input, { maxLen: 4000 })
1728
+ const inputForPrint = opt.getOriginalInput?.() || input
1729
+
1730
+ // Resolve each error's failing value while its instancePath is still a raw JSON pointer
1731
+ // (e.g `/events/2/name`), which is unambiguous to walk - unlike the dot notation it gets
1732
+ // rewritten into below (a property name may itself contain a dot).
1733
+ // Root errors (empty instancePath) resolve to undefined - there the value is the whole Input,
1734
+ // which is printed anyway.
1735
+ const failingValueByError = new Map<ErrorObject, unknown>(
1736
+ errors.map(e => [
1737
+ e,
1738
+ e.instancePath ? getValueAtJsonPointer(inputForPrint, e.instancePath) : undefined,
1739
+ ]),
1740
+ )
1741
+
1742
+ applyImprovementsOnErrorMessages(errors, builtSchema)
1743
+
1744
+ // Same line format as Ajv's errorsText().
1745
+ // Each nested error line additionally ends with the narrow failing value itself (`, got: ...`),
1746
+ // so it stays visible even when the Input print below gets truncated for large inputs.
1747
+ let message = errors
1748
+ .map(e => {
1749
+ let line = `${dataVar}${e.instancePath} ${e.message}`
1750
+ const value = failingValueByError.get(e)
1751
+ if (value !== undefined) {
1752
+ line += `, got: ${_inspect(value, { maxLen: 500 })}`
1753
+ }
1754
+ return line
1755
+ })
1756
+ .join(separator)
1757
+
1758
+ const inputStringified = _inspect(inputForPrint, { maxLen: 4000 })
1725
1759
  message = [message, 'Input: ' + inputStringified].join(separator)
1726
1760
 
1727
1761
  const err = new AjvValidationError(
@@ -1738,6 +1772,19 @@ function executeValidation<OUT>(
1738
1772
 
1739
1773
  // ==== Error formatting helpers ====
1740
1774
 
1775
+ /**
1776
+ * Resolves the value at an Ajv error's raw instancePath, which is a JSON pointer, e.g `/events/2/name`.
1777
+ * Returns undefined if the path cannot be resolved (e.g the input was mutated).
1778
+ */
1779
+ function getValueAtJsonPointer(input: unknown, pointer: string): unknown {
1780
+ let value: any = input
1781
+ for (const segment of pointer.split('/').slice(1)) {
1782
+ if (value === null || typeof value !== 'object') return undefined
1783
+ value = value[segment.replaceAll('~1', '/').replaceAll('~0', '~')]
1784
+ }
1785
+ return value
1786
+ }
1787
+
1741
1788
  function applyImprovementsOnErrorMessages(
1742
1789
  errors: ErrorObject[] | null | undefined,
1743
1790
  schema: JsonSchema,
@@ -2146,13 +2193,15 @@ type StripIndexSignatureDeep<T> = T extends readonly unknown[]
2146
2193
  ? T
2147
2194
  : T extends Record<string, any>
2148
2195
  ? {
2149
- [K in keyof T as string extends K
2150
- ? never
2151
- : number extends K
2196
+ [
2197
+ K in keyof T as string extends K
2152
2198
  ? never
2153
- : symbol extends K
2199
+ : number extends K
2154
2200
  ? never
2155
- : K]: StripIndexSignatureDeep<T[K]>
2201
+ : symbol extends K
2202
+ ? never
2203
+ : K
2204
+ ]: StripIndexSignatureDeep<T[K]>
2156
2205
  }
2157
2206
  : T
2158
2207