@exortek/apikey 1.1.1 → 1.1.3
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/CHANGELOG.md +32 -0
- package/README.md +3 -3
- package/dist/index.cjs +37 -291
- package/dist/index.d.ts +145 -18
- package/dist/index.mjs +37 -291
- package/dist/internal/guards.d.ts +2 -2
- package/dist/middleware/express.cjs +38 -292
- package/dist/middleware/express.d.ts +96 -1
- package/dist/middleware/express.mjs +38 -292
- package/dist/middleware/fastify.cjs +38 -292
- package/dist/middleware/fastify.d.ts +3 -1
- package/dist/middleware/fastify.mjs +38 -292
- package/dist/stores/custom.d.ts +1 -8
- package/dist/stores/index.d.ts +79 -3
- package/dist/stores.cjs +164 -317
- package/dist/stores.mjs +164 -317
- package/dist/token.d.ts +2 -1
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# @exortek/apikey
|
|
2
2
|
|
|
3
|
+
## 1.1.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 89aea87: Ship self-contained TypeScript declarations. Every package's emitted `.d.ts` referenced `@exortek/shared`
|
|
8
|
+
(e.g. `import { BaseError } from '@exortek/shared/errors'`), but `@exortek/shared` is a private, never-published
|
|
9
|
+
workspace package that is inlined into each bundle at build time. A TypeScript consumer therefore hit
|
|
10
|
+
`Cannot find module '@exortek/shared/…'` (with `skipLibCheck` off) or silently degraded error-class types like
|
|
11
|
+
`ApiKeyError` — losing its constructor signature and `.code` / `.message` — with `skipLibCheck` on.
|
|
12
|
+
|
|
13
|
+
The build now runs a declaration-bundling pass (`rollup-plugin-dts`) after `tsc`, flattening each entry's `.d.ts` and
|
|
14
|
+
inlining the `@exortek/shared` types so the shipped declarations are fully self-contained. Runtime deps and `node:*`
|
|
15
|
+
stay external. No runtime or API change — types only.
|
|
16
|
+
|
|
17
|
+
## 1.1.2
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- b9e0647: Publish only the package-root README, CHANGELOG and LICENSE.
|
|
22
|
+
|
|
23
|
+
The `files` list matched those names at any depth rather than just the root, so a nested document was published
|
|
24
|
+
alongside them — `@exortek/oauth2` shipped its `examples/README.md`. The entries are now anchored to the package root.
|
|
25
|
+
|
|
26
|
+
- 0a94f13: Smaller bundles — the internal argument-guard helpers are now tree-shakeable.
|
|
27
|
+
|
|
28
|
+
Each package bundles the guard helpers it uses. They were previously built as one object holding all fourteen, which a
|
|
29
|
+
bundler cannot take apart, so every package shipped all of them regardless of how many it called. They are now
|
|
30
|
+
individually importable, and each package pulls in only what it uses.
|
|
31
|
+
|
|
32
|
+
No API change: the errors, codes and messages raised by argument validation are identical. Published bundles shrink by
|
|
33
|
+
roughly 7-18% depending on the package.
|
|
34
|
+
|
|
3
35
|
## 1.1.1
|
|
4
36
|
|
|
5
37
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
> Stripe-style prefixed API keys for Node.js 22+ — 3-segment tokens (`sk_live_<id>_<secret>`), HMAC-SHA256 storage hash with optional pepper rotation, scope allowlists, memory + Redis stores, Express + Fastify middleware. Zero non-`@exortek/*` runtime dependencies.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@exortek/apikey)
|
|
6
|
-
[](https://github.com/ExorTek/auth/actions/workflows/ci.yml)
|
|
7
7
|
[](https://nodejs.org)
|
|
8
8
|
[](https://packagephobia.com/result?p=@exortek/apikey)
|
|
9
9
|
[](./dist/index.d.ts)
|
|
10
|
-
[](
|
|
10
|
+
[](https://github.com/ExorTek/auth/blob/master/LICENSE)
|
|
11
11
|
|
|
12
12
|
📖 **Docs:** [**auth.memet.dev/apikey**](https://auth.memet.dev/apikey)
|
|
13
13
|
|
|
@@ -325,4 +325,4 @@ Expected verify failures do NOT throw — they surface as `{ valid: false, reaso
|
|
|
325
325
|
|
|
326
326
|
## License
|
|
327
327
|
|
|
328
|
-
MIT © ExorTek — see [LICENSE](
|
|
328
|
+
MIT © ExorTek — see [LICENSE](https://github.com/ExorTek/auth/blob/master/LICENSE).
|
package/dist/index.cjs
CHANGED
|
@@ -351,20 +351,33 @@ class ApiKeyError extends BaseError {
|
|
|
351
351
|
* `@exortek/shared/validate`: **schema** for whole options objects,
|
|
352
352
|
* **asserts** for one-liner argument guards at the call site.
|
|
353
353
|
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
354
|
+
* Every guard is exported as a `make…` **factory** taking the binding
|
|
355
|
+
* package's `wrap` and returning the guard itself. A package binds the
|
|
356
|
+
* guards it calls once, in its `internal/guards.js`, so every argument
|
|
357
|
+
* failure throws that package's error class — `err instanceof
|
|
358
|
+
* CryptoError` holds, and users see at a glance which package raised it.
|
|
359
|
+
*
|
|
360
|
+
* // packages/<pkg>/src/internal/guards.js
|
|
361
|
+
* import { makeAssertString, makeInvalidArgument } from '@exortek/shared/asserts';
|
|
362
|
+
* import { CryptoError, ErrorCode } from '../errors.js';
|
|
363
|
+
*
|
|
364
|
+
* const wrap = (message, extra) => new CryptoError(ErrorCode.INVALID_ARGUMENT, message, extra);
|
|
365
|
+
*
|
|
366
|
+
* export const assertString = makeAssertString(wrap);
|
|
367
|
+
* export const invalidArgument = makeInvalidArgument(wrap);
|
|
368
|
+
*
|
|
369
|
+
* Importing each factory by name is what makes the module
|
|
370
|
+
* tree-shakeable. An earlier version handed back one object carrying
|
|
371
|
+
* every guard, so all of them shipped in every package's bundle no
|
|
372
|
+
* matter how few were used — `assertBoolean` reached fifteen published
|
|
373
|
+
* packages without a single call site. Import what you call.
|
|
361
374
|
*
|
|
362
375
|
* **Path naming convention** (used as the `name` argument):
|
|
363
376
|
*
|
|
364
377
|
* `<publicFunction>[.options|.config][.<field>]`
|
|
365
378
|
*
|
|
366
|
-
* - `assertString(name, 'createUser.name')`
|
|
367
|
-
* - `assertPositiveInt(n, 'scrypt.options.r')`
|
|
379
|
+
* - `assertString(name, 'createUser.name')` — top-level arg
|
|
380
|
+
* - `assertPositiveInt(n, 'scrypt.options.r')` — nested option
|
|
368
381
|
* - `assertBytesOrString(pwd, 'pepper.wrap.password')` — method arg
|
|
369
382
|
*
|
|
370
383
|
* Keep the path short: the emitted error is `"<name> must be <desc>"`,
|
|
@@ -373,301 +386,34 @@ class ApiKeyError extends BaseError {
|
|
|
373
386
|
* encryptSymmetric()"), not for restating the field.
|
|
374
387
|
*/
|
|
375
388
|
|
|
376
|
-
/**
|
|
377
|
-
* @typedef {{ cause?: unknown }} WrapExtra
|
|
378
|
-
* Extra options forwarded to the wrap function, so `invalidArgument`
|
|
379
|
-
* sites that catch an underlying error can preserve the `cause` chain
|
|
380
|
-
* without falling back to a raw `new PackageError(...)`.
|
|
381
|
-
*
|
|
382
|
-
* @typedef {(message: string, extra?: WrapExtra) => Error} WrapFn
|
|
383
|
-
* Constructs (never throws) the binding package's error, e.g.
|
|
384
|
-
* `(m, { cause } = {}) => new CryptoError(ErrorCode.INVALID_ARGUMENT, m, { cause })`.
|
|
385
|
-
*
|
|
386
|
-
* @typedef {{ hint?: string }} AssertOptions
|
|
387
|
-
* `hint` is appended to the message after an em-dash — use it for
|
|
388
|
-
* actionable guidance ("pass the exact bytes returned by …").
|
|
389
|
-
*
|
|
390
|
-
* @typedef {{ safeParse: (input: unknown, path?: string) => { ok: true, value: unknown } | { ok: false, errors: string[] } }} ParseableSchema
|
|
391
|
-
*/
|
|
392
389
|
|
|
393
390
|
/**
|
|
394
|
-
*
|
|
395
|
-
* `
|
|
396
|
-
*
|
|
391
|
+
* Construct (not throw) the bound error with a free-form message — for
|
|
392
|
+
* `throw invalidArgument('…')` sites that don't fit the `X must be Y`
|
|
393
|
+
* shape (canonicalisation errors, cross-field constraint failures). Pass
|
|
394
|
+
* `{ cause }` when the failure was triggered by an underlying throw whose
|
|
395
|
+
* chain matters (e.g. `JSON.stringify` on a bigint).
|
|
397
396
|
*
|
|
398
|
-
* @
|
|
399
|
-
* @param {string} description
|
|
400
|
-
* @param {string} [hint]
|
|
401
|
-
* @returns {string}
|
|
397
|
+
* @type {(wrap: WrapFn) => (msg: string, extra?: WrapExtra) => Error}
|
|
402
398
|
*/
|
|
403
|
-
|
|
404
|
-
return `${name} must be ${description}${hint ? ` — ${hint}` : ''}`;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const ENCODINGS = new Set(['hex', 'base64', 'base64url']);
|
|
399
|
+
const makeInvalidArgument = wrap => (msg, extra) => wrap(msg, extra);
|
|
408
400
|
|
|
409
401
|
/**
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
* import { bindAsserts } from '@exortek/shared/asserts';
|
|
415
|
-
* import { CryptoError, ErrorCode } from '../errors.js';
|
|
416
|
-
* export const g = bindAsserts((m) => new CryptoError(ErrorCode.INVALID_ARGUMENT, m));
|
|
417
|
-
*
|
|
418
|
-
* // call sites:
|
|
419
|
-
* g.assertPositiveInt(options.iterations, 'options.iterations');
|
|
420
|
-
* const cfg = g.parse(OptionsSchema, options, 'options');
|
|
421
|
-
*
|
|
422
|
-
* @param {WrapFn} wrap
|
|
402
|
+
* Argument guards bound to `ApiKeyError` — the package-wide binding of
|
|
403
|
+
* `@exortek/shared/asserts`. Import guards from here, never from the
|
|
404
|
+
* shared module directly.
|
|
423
405
|
*/
|
|
424
|
-
function bindAsserts(wrap) {
|
|
425
|
-
/**
|
|
426
|
-
* @param {string} name
|
|
427
|
-
* @param {string} description
|
|
428
|
-
* @param {AssertOptions} [opts]
|
|
429
|
-
* @returns {never}
|
|
430
|
-
*/
|
|
431
|
-
function fail(name, description, opts) {
|
|
432
|
-
throw wrap(message(name, description, opts?.hint));
|
|
433
|
-
}
|
|
434
406
|
|
|
435
|
-
/**
|
|
436
|
-
* Assert that `value` is a plain object (not `null`, not an array,
|
|
437
|
-
* not a primitive).
|
|
438
|
-
* @param {unknown} value
|
|
439
|
-
* @param {string} name
|
|
440
|
-
* @param {AssertOptions} [opts]
|
|
441
|
-
*/
|
|
442
|
-
function assertObject(value, name, opts) {
|
|
443
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
444
|
-
fail(name, 'an object', opts);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
return {
|
|
449
|
-
/**
|
|
450
|
-
* Construct (not throw) the bound error with a free-form message —
|
|
451
|
-
* for `throw g.invalidArgument('…')` sites that don't fit the
|
|
452
|
-
* `X must be Y` shape (canonicalisation errors, cross-field
|
|
453
|
-
* constraint failures). Pass `{ cause }` when the argument-shape
|
|
454
|
-
* failure was triggered by an underlying throw whose chain matters
|
|
455
|
-
* (e.g. `JSON.stringify` on a bigint).
|
|
456
|
-
*
|
|
457
|
-
* @param {string} msg
|
|
458
|
-
* @param {WrapExtra} [extra]
|
|
459
|
-
* @returns {Error}
|
|
460
|
-
*/
|
|
461
|
-
invalidArgument(msg, extra) {
|
|
462
|
-
return wrap(msg, extra);
|
|
463
|
-
},
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Validate `input` against a `@exortek/shared/validate` schema;
|
|
467
|
-
* failures throw the bound error carrying every collected message.
|
|
468
|
-
* The bridge that keeps schema validation on the package's own
|
|
469
|
-
* error surface.
|
|
470
|
-
*
|
|
471
|
-
* @param {ParseableSchema} schema
|
|
472
|
-
* @param {unknown} input
|
|
473
|
-
* @param {string} [path='options']
|
|
474
|
-
* @returns {unknown} the parsed (possibly defaulted) value
|
|
475
|
-
*/
|
|
476
|
-
parse(schema, input, path = 'options') {
|
|
477
|
-
const r = schema.safeParse(input, path);
|
|
478
|
-
if (!r.ok) {
|
|
479
|
-
throw wrap(r.errors.join('; '));
|
|
480
|
-
}
|
|
481
|
-
return r.value;
|
|
482
|
-
},
|
|
483
|
-
|
|
484
|
-
/**
|
|
485
|
-
* Assert that `value` is a non-negative safe integer (`0, 1, 2, …`).
|
|
486
|
-
* @param {unknown} value
|
|
487
|
-
* @param {string} name Argument name to include in the error message.
|
|
488
|
-
* @param {AssertOptions} [opts]
|
|
489
|
-
*/
|
|
490
|
-
assertNonNegativeInt(value, name, opts) {
|
|
491
|
-
if (!Number.isSafeInteger(value) || /** @type {number} */ (value) < 0) {
|
|
492
|
-
fail(name, 'a non-negative safe integer', opts);
|
|
493
|
-
}
|
|
494
|
-
},
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* Assert that `value` is a strictly positive safe integer (`1, 2, 3, …`).
|
|
498
|
-
* @param {unknown} value
|
|
499
|
-
* @param {string} name
|
|
500
|
-
* @param {AssertOptions} [opts]
|
|
501
|
-
*/
|
|
502
|
-
assertPositiveInt(value, name, opts) {
|
|
503
|
-
if (!Number.isSafeInteger(value) || /** @type {number} */ (value) <= 0) {
|
|
504
|
-
fail(name, 'a positive integer', opts);
|
|
505
|
-
}
|
|
506
|
-
},
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Assert that `value` fits in a 48-bit unsigned integer (`0 … 2^48 − 1`).
|
|
510
|
-
* Used for Unix millisecond timestamps in UUID v7 / ULID.
|
|
511
|
-
* @param {unknown} value
|
|
512
|
-
* @param {string} name
|
|
513
|
-
* @param {AssertOptions} [opts]
|
|
514
|
-
*/
|
|
515
|
-
assertUint48(value, name, opts) {
|
|
516
|
-
if (
|
|
517
|
-
!Number.isSafeInteger(value) ||
|
|
518
|
-
/** @type {number} */ (value) < 0 ||
|
|
519
|
-
/** @type {number} */ (value) > 0xffffffffffff
|
|
520
|
-
) {
|
|
521
|
-
fail(name, 'a non-negative safe integer ≤ 2^48 − 1 (Unix ms since epoch)', opts);
|
|
522
|
-
}
|
|
523
|
-
},
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* Assert that `value` is a string (may be empty).
|
|
527
|
-
* @param {unknown} value
|
|
528
|
-
* @param {string} name
|
|
529
|
-
* @param {AssertOptions} [opts]
|
|
530
|
-
*/
|
|
531
|
-
assertString(value, name, opts) {
|
|
532
|
-
if (typeof value !== 'string') {
|
|
533
|
-
fail(name, 'a string', opts);
|
|
534
|
-
}
|
|
535
|
-
},
|
|
536
|
-
|
|
537
|
-
/**
|
|
538
|
-
* Assert that `value` is a non-empty string.
|
|
539
|
-
* @param {unknown} value
|
|
540
|
-
* @param {string} name
|
|
541
|
-
* @param {AssertOptions} [opts]
|
|
542
|
-
*/
|
|
543
|
-
assertNonEmptyString(value, name, opts) {
|
|
544
|
-
if (typeof value !== 'string' || value.length === 0) {
|
|
545
|
-
fail(name, 'a non-empty string', opts);
|
|
546
|
-
}
|
|
547
|
-
},
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* Assert that `value` is a boolean.
|
|
551
|
-
* @param {unknown} value
|
|
552
|
-
* @param {string} name
|
|
553
|
-
* @param {AssertOptions} [opts]
|
|
554
|
-
*/
|
|
555
|
-
assertBoolean(value, name, opts) {
|
|
556
|
-
if (typeof value !== 'boolean') {
|
|
557
|
-
fail(name, 'a boolean', opts);
|
|
558
|
-
}
|
|
559
|
-
},
|
|
560
|
-
|
|
561
|
-
/**
|
|
562
|
-
* Assert that `value` is a function.
|
|
563
|
-
* @param {unknown} value
|
|
564
|
-
* @param {string} name
|
|
565
|
-
* @param {AssertOptions} [opts]
|
|
566
|
-
*/
|
|
567
|
-
assertFunction(value, name, opts) {
|
|
568
|
-
if (typeof value !== 'function') {
|
|
569
|
-
fail(name, 'a function', opts);
|
|
570
|
-
}
|
|
571
|
-
},
|
|
572
|
-
|
|
573
|
-
assertObject,
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* Assert that `value` is either `undefined` or a plain object —
|
|
577
|
-
* the "optional options object" pattern.
|
|
578
|
-
* @param {unknown} value
|
|
579
|
-
* @param {string} name
|
|
580
|
-
* @param {AssertOptions} [opts]
|
|
581
|
-
*/
|
|
582
|
-
assertOptionalObject(value, name, opts) {
|
|
583
|
-
if (value === undefined) {
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
assertObject(value, name, opts);
|
|
587
|
-
},
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* Assert that `value` is a byte buffer (`Buffer` or `Uint8Array`) —
|
|
591
|
-
* strings are NOT accepted. For already-encoded material where a
|
|
592
|
-
* string would be ambiguous (ciphertext, signatures, raw key bytes).
|
|
593
|
-
* @param {unknown} value
|
|
594
|
-
* @param {string} name
|
|
595
|
-
* @param {AssertOptions} [opts]
|
|
596
|
-
*/
|
|
597
|
-
assertBytes(value, name, opts) {
|
|
598
|
-
if (!Buffer.isBuffer(value) && !(value instanceof Uint8Array)) {
|
|
599
|
-
fail(name, 'a Buffer or Uint8Array', opts);
|
|
600
|
-
}
|
|
601
|
-
},
|
|
602
|
-
|
|
603
|
-
/**
|
|
604
|
-
* Assert that `value` is either a string or a byte buffer (`Buffer`
|
|
605
|
-
* or `Uint8Array`).
|
|
606
|
-
* @param {unknown} value
|
|
607
|
-
* @param {string} name
|
|
608
|
-
* @param {AssertOptions} [opts]
|
|
609
|
-
*/
|
|
610
|
-
assertBytesOrString(value, name, opts) {
|
|
611
|
-
if (typeof value !== 'string' && !Buffer.isBuffer(value) && !(value instanceof Uint8Array)) {
|
|
612
|
-
fail(name, 'a string or Buffer', opts);
|
|
613
|
-
}
|
|
614
|
-
},
|
|
615
|
-
|
|
616
|
-
/**
|
|
617
|
-
* Assert that `encoding` is one of the accepted output/input
|
|
618
|
-
* encodings. Pass `allowBuffer: false` where a `Buffer` output makes
|
|
619
|
-
* no sense (verifying a string signature, decoding a token payload).
|
|
620
|
-
*
|
|
621
|
-
* @param {unknown} encoding
|
|
622
|
-
* @param {string} name
|
|
623
|
-
* @param {AssertOptions & { allowBuffer?: boolean }} [opts]
|
|
624
|
-
*/
|
|
625
|
-
assertEncoding(encoding, name, opts) {
|
|
626
|
-
const allowBuffer = opts?.allowBuffer !== false;
|
|
627
|
-
const valid = (typeof encoding === 'string' && ENCODINGS.has(encoding)) || (allowBuffer && encoding === 'buffer');
|
|
628
|
-
if (!valid) {
|
|
629
|
-
const list = allowBuffer ? "'hex', 'base64', 'base64url', or 'buffer'" : "'hex', 'base64', or 'base64url'";
|
|
630
|
-
fail(name, list, opts);
|
|
631
|
-
}
|
|
632
|
-
},
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
407
|
|
|
636
408
|
/**
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
* own error class + `INVALID_ARGUMENT` code. Emits the standard
|
|
640
|
-
* `new ErrorClass(code, message, { cause })` shape, so the underlying
|
|
641
|
-
* `BaseError` `{ cause, status, details }` options object is respected.
|
|
642
|
-
*
|
|
643
|
-
* // packages/<pkg>/src/internal/guards.js
|
|
644
|
-
* import { defineGuards } from '@exortek/shared/asserts';
|
|
645
|
-
* import { PasswordError, ErrorCode } from '../errors.js';
|
|
409
|
+
* Bind every guard this package calls to ApiKeyError, so an argument-shape
|
|
410
|
+
* failure anywhere carries one class and one code to branch on.
|
|
646
411
|
*
|
|
647
|
-
*
|
|
648
|
-
* defineGuards(PasswordError, ErrorCode.INVALID_ARGUMENT);
|
|
649
|
-
*
|
|
650
|
-
* Reach for the raw {@link bindAsserts} when you need a different
|
|
651
|
-
* mapping (an alt code, a decorated message) — it stays available.
|
|
652
|
-
*
|
|
653
|
-
* @param {new (code: string, message: string, options?: { cause?: unknown }) => Error} ErrorClass
|
|
654
|
-
* @param {string} code
|
|
655
|
-
*/
|
|
656
|
-
function defineGuards(ErrorClass, code) {
|
|
657
|
-
return bindAsserts((message, extra) => new ErrorClass(code, message, extra));
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
/**
|
|
661
|
-
* Argument guards bound to `ApiKeyError` — the package-wide binding of
|
|
662
|
-
* `@exortek/shared/asserts`. Import guards from here, never from the
|
|
663
|
-
* shared module directly.
|
|
412
|
+
* @type {import('@exortek/shared/asserts').WrapFn}
|
|
664
413
|
*/
|
|
414
|
+
const wrap = (message, extra) => new ApiKeyError(ErrorCode.INVALID_ARGUMENT, message, extra);
|
|
665
415
|
|
|
666
|
-
|
|
667
|
-
const { invalidArgument} = defineGuards(
|
|
668
|
-
ApiKeyError,
|
|
669
|
-
ErrorCode.INVALID_ARGUMENT,
|
|
670
|
-
);
|
|
416
|
+
const invalidArgument = makeInvalidArgument(wrap);
|
|
671
417
|
|
|
672
418
|
/**
|
|
673
419
|
* Scope match helpers.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a raw key without verifying it. Returns the segments, or
|
|
3
|
+
* `null` when the shape is wrong. Never throws.
|
|
4
|
+
*
|
|
5
|
+
* Callers can use `parseApiKey(key).prefix` to route by key family
|
|
6
|
+
* in a middleware before hitting the store — but the parse result
|
|
7
|
+
* MUST NOT be trusted as authenticated. Only `verifyApiKey` proves
|
|
8
|
+
* the holder possesses the real secret.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} key
|
|
11
|
+
* @returns {{ prefix: string, id: string, secret: string } | null}
|
|
12
|
+
*/
|
|
13
|
+
declare function parseApiKey(key: string): {
|
|
14
|
+
prefix: string;
|
|
15
|
+
id: string;
|
|
16
|
+
secret: string;
|
|
17
|
+
} | null;
|
|
18
|
+
/**
|
|
19
|
+
* Log-safe display for a key. Returns `<prefix>_<first-N-of-id>…<last-M-of-secret>`
|
|
20
|
+
* so the value is recognisable in an audit log without disclosing the
|
|
21
|
+
* secret. The default masks all but the first 6 chars of the id and
|
|
22
|
+
* the last 4 chars of the secret.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} key
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
declare function mask(key: string): string;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Shared base error class — the single error structure behind every
|
|
31
|
+
* `@exortek/*` package's `errors.js`.
|
|
32
|
+
*
|
|
33
|
+
* Every package keeps its own class identity with a one-liner subclass;
|
|
34
|
+
* codes stay per-package frozen maps, status mapping is declared as a
|
|
35
|
+
* static field:
|
|
36
|
+
*
|
|
37
|
+
* import { BaseError } from '@exortek/shared/errors';
|
|
38
|
+
*
|
|
39
|
+
* export const ErrorCode = Object.freeze({
|
|
40
|
+
* INVALID_ARGUMENT: 'INVALID_ARGUMENT',
|
|
41
|
+
* INVALID_TOKEN: 'INVALID_TOKEN',
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* export class JwtError extends BaseError {
|
|
45
|
+
* static statuses = { INVALID_ARGUMENT: 400, INVALID_TOKEN: 401 };
|
|
46
|
+
* static defaultStatus = 500;
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* Instances carry a stable machine-readable `code` (branch on this,
|
|
50
|
+
* never on the message), an optional HTTP `status`, an optional
|
|
51
|
+
* `details` object, and the standard `cause` chain.
|
|
52
|
+
*/
|
|
53
|
+
declare class BaseError extends Error {
|
|
54
|
+
/**
|
|
55
|
+
* Optional `code → HTTP status` map declared on the subclass. When
|
|
56
|
+
* absent the instance carries no `status` at all — for HTTP-agnostic
|
|
57
|
+
* packages like `@exortek/crypto`.
|
|
58
|
+
*
|
|
59
|
+
* @type {Record<string, number> | undefined}
|
|
60
|
+
*/
|
|
61
|
+
static statuses: Record<string, number> | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Fallback status for codes missing from `statuses`.
|
|
64
|
+
*
|
|
65
|
+
* @type {number}
|
|
66
|
+
*/
|
|
67
|
+
static defaultStatus: number;
|
|
68
|
+
/**
|
|
69
|
+
* @param {string} code Stable machine-readable code; branch on this.
|
|
70
|
+
* @param {string} message Human-readable diagnostic. Free-form; may
|
|
71
|
+
* change across versions.
|
|
72
|
+
* @param {{ cause?: unknown, status?: number, details?: Record<string, unknown> }} [options]
|
|
73
|
+
*/
|
|
74
|
+
constructor(code: string, message: string, options?: {
|
|
75
|
+
cause?: unknown;
|
|
76
|
+
status?: number;
|
|
77
|
+
details?: Record<string, unknown>;
|
|
78
|
+
});
|
|
79
|
+
/** @type {string} */
|
|
80
|
+
code: string;
|
|
81
|
+
/** @type {number | undefined} */
|
|
82
|
+
status: number | undefined;
|
|
83
|
+
/** @type {Record<string, unknown> | undefined} */
|
|
84
|
+
details: Record<string, unknown> | undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
declare const ErrorCode: Readonly<{
|
|
88
|
+
INVALID_ARGUMENT: "INVALID_ARGUMENT";
|
|
89
|
+
INVALID_PREFIX: "INVALID_PREFIX";
|
|
90
|
+
INVALID_PEPPER: "INVALID_PEPPER";
|
|
91
|
+
STORE_ERROR: "STORE_ERROR";
|
|
92
|
+
}>;
|
|
93
|
+
/**
|
|
94
|
+
* Every recoverable failure raised by this package. Carries a stable
|
|
95
|
+
* `code` (from {@link ErrorCode}) and a `status` — the HTTP response
|
|
96
|
+
* status a middleware layer would use when translating the error.
|
|
97
|
+
*/
|
|
98
|
+
declare class ApiKeyError extends BaseError {
|
|
99
|
+
static statuses: {
|
|
100
|
+
INVALID_ARGUMENT: number;
|
|
101
|
+
INVALID_PREFIX: number;
|
|
102
|
+
INVALID_PEPPER: number;
|
|
103
|
+
STORE_ERROR: number;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Does `granted` cover a single `required` scope?
|
|
109
|
+
* @param {string[]} granted
|
|
110
|
+
* @param {string} required
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
declare function covers(granted: string[], required: string): boolean;
|
|
114
|
+
/**
|
|
115
|
+
* All required scopes must be covered by granted.
|
|
116
|
+
* @param {string[]} granted
|
|
117
|
+
* @param {string[]} required
|
|
118
|
+
* @returns {boolean}
|
|
119
|
+
*/
|
|
120
|
+
declare function hasAll(granted: string[], required: string[]): boolean;
|
|
121
|
+
/**
|
|
122
|
+
* At least one required scope must be covered.
|
|
123
|
+
* @param {string[]} granted
|
|
124
|
+
* @param {string[]} required
|
|
125
|
+
* @returns {boolean}
|
|
126
|
+
*/
|
|
127
|
+
declare function hasAny(granted: string[], required: string[]): boolean;
|
|
128
|
+
|
|
1
129
|
/**
|
|
2
130
|
* @typedef {object} ApiKeyRecord
|
|
3
131
|
* @property {string} id Plaintext lookup key (128-bit b64u).
|
|
@@ -80,7 +208,7 @@
|
|
|
80
208
|
* @param {CreateApiKeyOptions} options
|
|
81
209
|
* @returns {Promise<CreateApiKeyResult>}
|
|
82
210
|
*/
|
|
83
|
-
|
|
211
|
+
declare function createApiKey(options: CreateApiKeyOptions): Promise<CreateApiKeyResult>;
|
|
84
212
|
/**
|
|
85
213
|
* Verify a raw API key. Returns `{ valid: true, ... }` on success, or
|
|
86
214
|
* `{ valid: false, reason }` on any expected failure. Never throws on
|
|
@@ -96,7 +224,7 @@ export function createApiKey(options: CreateApiKeyOptions): Promise<CreateApiKey
|
|
|
96
224
|
* @param {VerifyApiKeyOptions} options
|
|
97
225
|
* @returns {Promise<VerifyApiKeyResult>}
|
|
98
226
|
*/
|
|
99
|
-
|
|
227
|
+
declare function verifyApiKey(rawKey: string, options: VerifyApiKeyOptions): Promise<VerifyApiKeyResult>;
|
|
100
228
|
/**
|
|
101
229
|
* Revoke an API key by id or by the raw wire key. Returns `true` if
|
|
102
230
|
* a record was actually revoked, `false` if the id was unknown or
|
|
@@ -106,7 +234,7 @@ export function verifyApiKey(rawKey: string, options: VerifyApiKeyOptions): Prom
|
|
|
106
234
|
* @param {{ store: ApiKeyStore, reason?: string }} options
|
|
107
235
|
* @returns {Promise<boolean>}
|
|
108
236
|
*/
|
|
109
|
-
|
|
237
|
+
declare function revokeApiKey(keyOrId: string, options: {
|
|
110
238
|
store: ApiKeyStore;
|
|
111
239
|
reason?: string;
|
|
112
240
|
}): Promise<boolean>;
|
|
@@ -119,7 +247,7 @@ export function revokeApiKey(keyOrId: string, options: {
|
|
|
119
247
|
* @param {{ store: ApiKeyStore, reason?: string }} options
|
|
120
248
|
* @returns {Promise<number>}
|
|
121
249
|
*/
|
|
122
|
-
|
|
250
|
+
declare function revokeAllForUser(userId: string, options: {
|
|
123
251
|
store: ApiKeyStore;
|
|
124
252
|
reason?: string;
|
|
125
253
|
}): Promise<number>;
|
|
@@ -133,7 +261,7 @@ export function revokeAllForUser(userId: string, options: {
|
|
|
133
261
|
* @param {{ store: ApiKeyStore }} options
|
|
134
262
|
* @returns {Promise<ApiKeyRecord[]>}
|
|
135
263
|
*/
|
|
136
|
-
|
|
264
|
+
declare function listApiKeys(userId: string, options: {
|
|
137
265
|
store: ApiKeyStore;
|
|
138
266
|
}): Promise<ApiKeyRecord[]>;
|
|
139
267
|
/**
|
|
@@ -146,12 +274,12 @@ export function listApiKeys(userId: string, options: {
|
|
|
146
274
|
* @param {{ store: ApiKeyStore, peppers: (Buffer | Uint8Array | string)[] }} options
|
|
147
275
|
* @returns {Promise<boolean>}
|
|
148
276
|
*/
|
|
149
|
-
|
|
277
|
+
declare function rehashApiKey(rawKey: string, options: {
|
|
150
278
|
store: ApiKeyStore;
|
|
151
279
|
peppers: (Buffer | Uint8Array | string)[];
|
|
152
280
|
}): Promise<boolean>;
|
|
153
|
-
|
|
154
|
-
|
|
281
|
+
|
|
282
|
+
type ApiKeyRecord = {
|
|
155
283
|
/**
|
|
156
284
|
* Plaintext lookup key (128-bit b64u).
|
|
157
285
|
*/
|
|
@@ -197,7 +325,7 @@ export type ApiKeyRecord = {
|
|
|
197
325
|
*/
|
|
198
326
|
pepperVersion?: number | undefined;
|
|
199
327
|
};
|
|
200
|
-
|
|
328
|
+
type ApiKeyStore = {
|
|
201
329
|
put: (record: ApiKeyRecord) => Promise<void>;
|
|
202
330
|
getById: (id: string) => Promise<ApiKeyRecord | null>;
|
|
203
331
|
update: (id: string, patch: Partial<ApiKeyRecord>) => Promise<ApiKeyRecord | null>;
|
|
@@ -205,7 +333,7 @@ export type ApiKeyStore = {
|
|
|
205
333
|
revokeAllForUser: (userId: string, reason?: string) => Promise<number>;
|
|
206
334
|
listByUser: (userId: string) => Promise<ApiKeyRecord[]>;
|
|
207
335
|
};
|
|
208
|
-
|
|
336
|
+
type CreateApiKeyOptions = {
|
|
209
337
|
/**
|
|
210
338
|
* Stripe-style; `sk_live`, `pk_test`, `svc_prod_v2`.
|
|
211
339
|
*/
|
|
@@ -229,7 +357,7 @@ export type CreateApiKeyOptions = {
|
|
|
229
357
|
*/
|
|
230
358
|
now?: number | undefined;
|
|
231
359
|
};
|
|
232
|
-
|
|
360
|
+
type CreateApiKeyResult = {
|
|
233
361
|
/**
|
|
234
362
|
* Wire token — show ONCE to the caller and never again.
|
|
235
363
|
*/
|
|
@@ -243,7 +371,7 @@ export type CreateApiKeyResult = {
|
|
|
243
371
|
*/
|
|
244
372
|
record: ApiKeyRecord;
|
|
245
373
|
};
|
|
246
|
-
|
|
374
|
+
type VerifyApiKeyOptions = {
|
|
247
375
|
store: ApiKeyStore;
|
|
248
376
|
peppers?: any[] | undefined;
|
|
249
377
|
requiredScopes?: string[] | undefined;
|
|
@@ -257,8 +385,8 @@ export type VerifyApiKeyOptions = {
|
|
|
257
385
|
updateLastUsed?: boolean | undefined;
|
|
258
386
|
now?: number | undefined;
|
|
259
387
|
};
|
|
260
|
-
|
|
261
|
-
|
|
388
|
+
type VerifyApiKeyFailureReason = "malformed" | "not_found" | "expired" | "revoked" | "bad_secret" | "prefix_mismatch" | "missing_scope" | "store_unavailable";
|
|
389
|
+
type VerifyApiKeyResult = {
|
|
262
390
|
valid: true;
|
|
263
391
|
id: string;
|
|
264
392
|
userId: string;
|
|
@@ -272,7 +400,6 @@ export type VerifyApiKeyResult = {
|
|
|
272
400
|
valid: false;
|
|
273
401
|
reason: VerifyApiKeyFailureReason;
|
|
274
402
|
};
|
|
275
|
-
|
|
276
|
-
export { ApiKeyError, ErrorCode
|
|
277
|
-
export {
|
|
278
|
-
export { parseApiKey, mask } from "./token.js";
|
|
403
|
+
|
|
404
|
+
export { ApiKeyError, ErrorCode, covers, createApiKey, hasAll, hasAny, listApiKeys, mask, mask as maskApiKey, parseApiKey, rehashApiKey, revokeAllForUser, revokeApiKey, verifyApiKey };
|
|
405
|
+
export type { ApiKeyRecord, ApiKeyStore, CreateApiKeyOptions, CreateApiKeyResult, VerifyApiKeyFailureReason, VerifyApiKeyOptions, VerifyApiKeyResult };
|