@exortek/apikey 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -63,7 +63,11 @@ const UNIT_MS = Object.freeze({
63
63
  years: 31_536_000_000,
64
64
  });
65
65
 
66
- const DURATION_RE = /^\s*(-?\d+(?:\.\d+)?)\s*([a-z]+)?\s*$/i;
66
+ // Note: no leading/trailing `\s*` — the caller trims first. Two `\s*` runs
67
+ // around the optional unit (`\s*([a-z]+)?\s*`) made this O(n^2) on hostile
68
+ // input like `"1" + " ".repeat(n) + "!"`; trimming plus a single interior
69
+ // `\s*` keeps matching linear.
70
+ const DURATION_RE = /^(-?\d+(?:\.\d+)?)\s*([a-z]+)?$/i;
67
71
 
68
72
  const SUPPORTED_UNITS = 'ms, s, m, h, d, w, y (and long/plural forms)';
69
73
 
@@ -93,7 +97,7 @@ function parseDuration(input) {
93
97
  throw new TypeError(`parseDuration: expected string or number; got ${typeof input}`);
94
98
  }
95
99
 
96
- const match = DURATION_RE.exec(input);
100
+ const match = DURATION_RE.exec(input.trim());
97
101
  if (!match) {
98
102
  throw new Error(
99
103
  `parseDuration: could not parse ${JSON.stringify(input)}. Examples: 900 (ms), '15m', '2h', '7d', '500ms', '30s'.`,
@@ -345,20 +349,33 @@ class ApiKeyError extends BaseError {
345
349
  * `@exortek/shared/validate`: **schema** for whole options objects,
346
350
  * **asserts** for one-liner argument guards at the call site.
347
351
  *
348
- * The consumer surface is {@link defineGuards} (or the raw
349
- * {@link bindAsserts} for full control). Each package binds the assert
350
- * set to its own typed error class once (in `internal/guards.js`), so
351
- * every argument failure throws that package's class —
352
- * `err instanceof CryptoError` holds and users see at a glance which
353
- * package raised the error. The bound `parse` bridges
354
- * `@exortek/shared/validate` schemas to the same error class.
352
+ * Every guard is exported as a `make…` **factory** taking the binding
353
+ * package's `wrap` and returning the guard itself. A package binds the
354
+ * guards it calls once, in its `internal/guards.js`, so every argument
355
+ * failure throws that package's error class — `err instanceof
356
+ * CryptoError` holds, and users see at a glance which package raised it.
357
+ *
358
+ * // packages/<pkg>/src/internal/guards.js
359
+ * import { makeAssertString, makeInvalidArgument } from '@exortek/shared/asserts';
360
+ * import { CryptoError, ErrorCode } from '../errors.js';
361
+ *
362
+ * const wrap = (message, extra) => new CryptoError(ErrorCode.INVALID_ARGUMENT, message, extra);
363
+ *
364
+ * export const assertString = makeAssertString(wrap);
365
+ * export const invalidArgument = makeInvalidArgument(wrap);
366
+ *
367
+ * Importing each factory by name is what makes the module
368
+ * tree-shakeable. An earlier version handed back one object carrying
369
+ * every guard, so all of them shipped in every package's bundle no
370
+ * matter how few were used — `assertBoolean` reached fifteen published
371
+ * packages without a single call site. Import what you call.
355
372
  *
356
373
  * **Path naming convention** (used as the `name` argument):
357
374
  *
358
375
  * `<publicFunction>[.options|.config][.<field>]`
359
376
  *
360
- * - `assertString(name, 'createUser.name')` — top-level arg
361
- * - `assertPositiveInt(n, 'scrypt.options.r')` — nested option
377
+ * - `assertString(name, 'createUser.name')` — top-level arg
378
+ * - `assertPositiveInt(n, 'scrypt.options.r')` — nested option
362
379
  * - `assertBytesOrString(pwd, 'pepper.wrap.password')` — method arg
363
380
  *
364
381
  * Keep the path short: the emitted error is `"<name> must be <desc>"`,
@@ -367,301 +384,34 @@ class ApiKeyError extends BaseError {
367
384
  * encryptSymmetric()"), not for restating the field.
368
385
  */
369
386
 
370
- /**
371
- * @typedef {{ cause?: unknown }} WrapExtra
372
- * Extra options forwarded to the wrap function, so `invalidArgument`
373
- * sites that catch an underlying error can preserve the `cause` chain
374
- * without falling back to a raw `new PackageError(...)`.
375
- *
376
- * @typedef {(message: string, extra?: WrapExtra) => Error} WrapFn
377
- * Constructs (never throws) the binding package's error, e.g.
378
- * `(m, { cause } = {}) => new CryptoError(ErrorCode.INVALID_ARGUMENT, m, { cause })`.
379
- *
380
- * @typedef {{ hint?: string }} AssertOptions
381
- * `hint` is appended to the message after an em-dash — use it for
382
- * actionable guidance ("pass the exact bytes returned by …").
383
- *
384
- * @typedef {{ safeParse: (input: unknown, path?: string) => { ok: true, value: unknown } | { ok: false, errors: string[] } }} ParseableSchema
385
- */
386
387
 
387
388
  /**
388
- * Build the failure message. `description` completes the sentence
389
- * `"<name> must be <description>"`; `hint` (optional) follows an
390
- * em-dash.
389
+ * Construct (not throw) the bound error with a free-form message — for
390
+ * `throw invalidArgument('…')` sites that don't fit the `X must be Y`
391
+ * shape (canonicalisation errors, cross-field constraint failures). Pass
392
+ * `{ cause }` when the failure was triggered by an underlying throw whose
393
+ * chain matters (e.g. `JSON.stringify` on a bigint).
391
394
  *
392
- * @param {string} name
393
- * @param {string} description
394
- * @param {string} [hint]
395
- * @returns {string}
395
+ * @type {(wrap: WrapFn) => (msg: string, extra?: WrapExtra) => Error}
396
396
  */
397
- function message(name, description, hint) {
398
- return `${name} must be ${description}${hint ? ` — ${hint}` : ''}`;
399
- }
400
-
401
- const ENCODINGS = new Set(['hex', 'base64', 'base64url']);
397
+ const makeInvalidArgument = wrap => (msg, extra) => wrap(msg, extra);
402
398
 
403
399
  /**
404
- * Bind the assert set to a package's error class. Call once per
405
- * package (conventionally in `internal/guards.js`) and import the
406
- * bound object everywhere:
407
- *
408
- * import { bindAsserts } from '@exortek/shared/asserts';
409
- * import { CryptoError, ErrorCode } from '../errors.js';
410
- * export const g = bindAsserts((m) => new CryptoError(ErrorCode.INVALID_ARGUMENT, m));
411
- *
412
- * // call sites:
413
- * g.assertPositiveInt(options.iterations, 'options.iterations');
414
- * const cfg = g.parse(OptionsSchema, options, 'options');
415
- *
416
- * @param {WrapFn} wrap
400
+ * Argument guards bound to `ApiKeyError` — the package-wide binding of
401
+ * `@exortek/shared/asserts`. Import guards from here, never from the
402
+ * shared module directly.
417
403
  */
418
- function bindAsserts(wrap) {
419
- /**
420
- * @param {string} name
421
- * @param {string} description
422
- * @param {AssertOptions} [opts]
423
- * @returns {never}
424
- */
425
- function fail(name, description, opts) {
426
- throw wrap(message(name, description, opts?.hint));
427
- }
428
404
 
429
- /**
430
- * Assert that `value` is a plain object (not `null`, not an array,
431
- * not a primitive).
432
- * @param {unknown} value
433
- * @param {string} name
434
- * @param {AssertOptions} [opts]
435
- */
436
- function assertObject(value, name, opts) {
437
- if (value === null || typeof value !== 'object' || Array.isArray(value)) {
438
- fail(name, 'an object', opts);
439
- }
440
- }
441
-
442
- return {
443
- /**
444
- * Construct (not throw) the bound error with a free-form message —
445
- * for `throw g.invalidArgument('…')` sites that don't fit the
446
- * `X must be Y` shape (canonicalisation errors, cross-field
447
- * constraint failures). Pass `{ cause }` when the argument-shape
448
- * failure was triggered by an underlying throw whose chain matters
449
- * (e.g. `JSON.stringify` on a bigint).
450
- *
451
- * @param {string} msg
452
- * @param {WrapExtra} [extra]
453
- * @returns {Error}
454
- */
455
- invalidArgument(msg, extra) {
456
- return wrap(msg, extra);
457
- },
458
-
459
- /**
460
- * Validate `input` against a `@exortek/shared/validate` schema;
461
- * failures throw the bound error carrying every collected message.
462
- * The bridge that keeps schema validation on the package's own
463
- * error surface.
464
- *
465
- * @param {ParseableSchema} schema
466
- * @param {unknown} input
467
- * @param {string} [path='options']
468
- * @returns {unknown} the parsed (possibly defaulted) value
469
- */
470
- parse(schema, input, path = 'options') {
471
- const r = schema.safeParse(input, path);
472
- if (!r.ok) {
473
- throw wrap(r.errors.join('; '));
474
- }
475
- return r.value;
476
- },
477
-
478
- /**
479
- * Assert that `value` is a non-negative safe integer (`0, 1, 2, …`).
480
- * @param {unknown} value
481
- * @param {string} name Argument name to include in the error message.
482
- * @param {AssertOptions} [opts]
483
- */
484
- assertNonNegativeInt(value, name, opts) {
485
- if (!Number.isSafeInteger(value) || /** @type {number} */ (value) < 0) {
486
- fail(name, 'a non-negative safe integer', opts);
487
- }
488
- },
489
-
490
- /**
491
- * Assert that `value` is a strictly positive safe integer (`1, 2, 3, …`).
492
- * @param {unknown} value
493
- * @param {string} name
494
- * @param {AssertOptions} [opts]
495
- */
496
- assertPositiveInt(value, name, opts) {
497
- if (!Number.isSafeInteger(value) || /** @type {number} */ (value) <= 0) {
498
- fail(name, 'a positive integer', opts);
499
- }
500
- },
501
-
502
- /**
503
- * Assert that `value` fits in a 48-bit unsigned integer (`0 … 2^48 − 1`).
504
- * Used for Unix millisecond timestamps in UUID v7 / ULID.
505
- * @param {unknown} value
506
- * @param {string} name
507
- * @param {AssertOptions} [opts]
508
- */
509
- assertUint48(value, name, opts) {
510
- if (
511
- !Number.isSafeInteger(value) ||
512
- /** @type {number} */ (value) < 0 ||
513
- /** @type {number} */ (value) > 0xffffffffffff
514
- ) {
515
- fail(name, 'a non-negative safe integer ≤ 2^48 − 1 (Unix ms since epoch)', opts);
516
- }
517
- },
518
-
519
- /**
520
- * Assert that `value` is a string (may be empty).
521
- * @param {unknown} value
522
- * @param {string} name
523
- * @param {AssertOptions} [opts]
524
- */
525
- assertString(value, name, opts) {
526
- if (typeof value !== 'string') {
527
- fail(name, 'a string', opts);
528
- }
529
- },
530
-
531
- /**
532
- * Assert that `value` is a non-empty string.
533
- * @param {unknown} value
534
- * @param {string} name
535
- * @param {AssertOptions} [opts]
536
- */
537
- assertNonEmptyString(value, name, opts) {
538
- if (typeof value !== 'string' || value.length === 0) {
539
- fail(name, 'a non-empty string', opts);
540
- }
541
- },
542
-
543
- /**
544
- * Assert that `value` is a boolean.
545
- * @param {unknown} value
546
- * @param {string} name
547
- * @param {AssertOptions} [opts]
548
- */
549
- assertBoolean(value, name, opts) {
550
- if (typeof value !== 'boolean') {
551
- fail(name, 'a boolean', opts);
552
- }
553
- },
554
-
555
- /**
556
- * Assert that `value` is a function.
557
- * @param {unknown} value
558
- * @param {string} name
559
- * @param {AssertOptions} [opts]
560
- */
561
- assertFunction(value, name, opts) {
562
- if (typeof value !== 'function') {
563
- fail(name, 'a function', opts);
564
- }
565
- },
566
-
567
- assertObject,
568
-
569
- /**
570
- * Assert that `value` is either `undefined` or a plain object —
571
- * the "optional options object" pattern.
572
- * @param {unknown} value
573
- * @param {string} name
574
- * @param {AssertOptions} [opts]
575
- */
576
- assertOptionalObject(value, name, opts) {
577
- if (value === undefined) {
578
- return;
579
- }
580
- assertObject(value, name, opts);
581
- },
582
-
583
- /**
584
- * Assert that `value` is a byte buffer (`Buffer` or `Uint8Array`) —
585
- * strings are NOT accepted. For already-encoded material where a
586
- * string would be ambiguous (ciphertext, signatures, raw key bytes).
587
- * @param {unknown} value
588
- * @param {string} name
589
- * @param {AssertOptions} [opts]
590
- */
591
- assertBytes(value, name, opts) {
592
- if (!Buffer.isBuffer(value) && !(value instanceof Uint8Array)) {
593
- fail(name, 'a Buffer or Uint8Array', opts);
594
- }
595
- },
596
-
597
- /**
598
- * Assert that `value` is either a string or a byte buffer (`Buffer`
599
- * or `Uint8Array`).
600
- * @param {unknown} value
601
- * @param {string} name
602
- * @param {AssertOptions} [opts]
603
- */
604
- assertBytesOrString(value, name, opts) {
605
- if (typeof value !== 'string' && !Buffer.isBuffer(value) && !(value instanceof Uint8Array)) {
606
- fail(name, 'a string or Buffer', opts);
607
- }
608
- },
609
-
610
- /**
611
- * Assert that `encoding` is one of the accepted output/input
612
- * encodings. Pass `allowBuffer: false` where a `Buffer` output makes
613
- * no sense (verifying a string signature, decoding a token payload).
614
- *
615
- * @param {unknown} encoding
616
- * @param {string} name
617
- * @param {AssertOptions & { allowBuffer?: boolean }} [opts]
618
- */
619
- assertEncoding(encoding, name, opts) {
620
- const allowBuffer = opts?.allowBuffer !== false;
621
- const valid = (typeof encoding === 'string' && ENCODINGS.has(encoding)) || (allowBuffer && encoding === 'buffer');
622
- if (!valid) {
623
- const list = allowBuffer ? "'hex', 'base64', 'base64url', or 'buffer'" : "'hex', 'base64', or 'base64url'";
624
- fail(name, list, opts);
625
- }
626
- },
627
- };
628
- }
629
405
 
630
406
  /**
631
- * One-line sugar over {@link bindAsserts} — the overwhelmingly common
632
- * pattern where a package binds every argument-shape failure to its
633
- * own error class + `INVALID_ARGUMENT` code. Emits the standard
634
- * `new ErrorClass(code, message, { cause })` shape, so the underlying
635
- * `BaseError` `{ cause, status, details }` options object is respected.
636
- *
637
- * // packages/<pkg>/src/internal/guards.js
638
- * import { defineGuards } from '@exortek/shared/asserts';
639
- * import { PasswordError, ErrorCode } from '../errors.js';
407
+ * Bind every guard this package calls to ApiKeyError, so an argument-shape
408
+ * failure anywhere carries one class and one code to branch on.
640
409
  *
641
- * export const { assertString, invalidArgument, parse } =
642
- * defineGuards(PasswordError, ErrorCode.INVALID_ARGUMENT);
643
- *
644
- * Reach for the raw {@link bindAsserts} when you need a different
645
- * mapping (an alt code, a decorated message) — it stays available.
646
- *
647
- * @param {new (code: string, message: string, options?: { cause?: unknown }) => Error} ErrorClass
648
- * @param {string} code
649
- */
650
- function defineGuards(ErrorClass, code) {
651
- return bindAsserts((message, extra) => new ErrorClass(code, message, extra));
652
- }
653
-
654
- /**
655
- * Argument guards bound to `ApiKeyError` — the package-wide binding of
656
- * `@exortek/shared/asserts`. Import guards from here, never from the
657
- * shared module directly.
410
+ * @type {import('@exortek/shared/asserts').WrapFn}
658
411
  */
412
+ const wrap = (message, extra) => new ApiKeyError(ErrorCode.INVALID_ARGUMENT, message, extra);
659
413
 
660
-
661
- const { invalidArgument} = defineGuards(
662
- ApiKeyError,
663
- ErrorCode.INVALID_ARGUMENT,
664
- );
414
+ const invalidArgument = makeInvalidArgument(wrap);
665
415
 
666
416
  /**
667
417
  * Scope match helpers.
@@ -1,4 +1,4 @@
1
1
  export const invalidArgument: (msg: string, extra?: WrapExtra) => Error;
2
2
  export const parse: (schema: ParseableSchema, input: unknown, path?: string) => unknown;
3
- export const assertObject: (value: unknown, name: string, opts?: AssertOptions) => void;
4
- export const assertNonEmptyString: (value: unknown, name: string, opts?: AssertOptions) => void;
3
+ export const assertObject: Function;
4
+ export const assertNonEmptyString: Function;