@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/dist/stores.cjs
CHANGED
|
@@ -124,11 +124,42 @@ function assertRedisClient(client, methods, wrap) {
|
|
|
124
124
|
* can share a single detection path instead of copy-pasting the same
|
|
125
125
|
* three-way dispatch.
|
|
126
126
|
*
|
|
127
|
-
*
|
|
128
|
-
* `
|
|
127
|
+
* Consumed by `incr-store.js` and `record-store.js` here, and imported
|
|
128
|
+
* directly by the `oauth2`, `opaque` and `session` stores — so treat this
|
|
129
|
+
* module's surface as shared, not private to `@exortek/shared`.
|
|
129
130
|
*/
|
|
130
131
|
|
|
131
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Which calling convention a client expects for the commands whose
|
|
135
|
+
* signatures differ between drivers — Lua (`eval`) and `SET` with a TTL.
|
|
136
|
+
*
|
|
137
|
+
* Most helpers here can dispatch on a method name (`mget` vs `mGet`), but
|
|
138
|
+
* these two share a name and differ only in how arguments are passed, so
|
|
139
|
+
* they need an explicit answer.
|
|
140
|
+
*
|
|
141
|
+
* A real `ioredis` instance reports `constructor.name === 'EventEmitter'`
|
|
142
|
+
* — it extends EventEmitter and no class name survives to the instance —
|
|
143
|
+
* so a constructor-name probe alone never matches it. Detect on the API
|
|
144
|
+
* surface instead (`scanStream` / the `status` string), neither of which
|
|
145
|
+
* node-redis has, and keep the name check as a fallback for wrapped
|
|
146
|
+
* clients. Anything unrecognised is treated as node-redis, whose
|
|
147
|
+
* options-object form is also what `@upstash/redis` accepts.
|
|
148
|
+
*
|
|
149
|
+
* @param {any} client
|
|
150
|
+
* @returns {'ioredis' | 'node-redis'}
|
|
151
|
+
*/
|
|
152
|
+
function detectDialect(client) {
|
|
153
|
+
if (client && (isFunction(client.scanStream) || isString(client.status))) {
|
|
154
|
+
return 'ioredis';
|
|
155
|
+
}
|
|
156
|
+
const name = (client && client.constructor && client.constructor.name) || '';
|
|
157
|
+
if (name === 'Redis' || name === 'Cluster') {
|
|
158
|
+
return 'ioredis';
|
|
159
|
+
}
|
|
160
|
+
return 'node-redis';
|
|
161
|
+
}
|
|
162
|
+
|
|
132
163
|
/**
|
|
133
164
|
* @param {object} client A Redis-compatible client instance.
|
|
134
165
|
* @returns {{
|
|
@@ -142,7 +173,11 @@ function assertRedisClient(client, methods, wrap) {
|
|
|
142
173
|
* }}
|
|
143
174
|
*/
|
|
144
175
|
function createRedisHelpers(client) {
|
|
176
|
+
const dialect = detectDialect(client);
|
|
177
|
+
|
|
145
178
|
return {
|
|
179
|
+
dialect,
|
|
180
|
+
|
|
146
181
|
async mget(keys) {
|
|
147
182
|
if (keys.length === 0) {
|
|
148
183
|
return [];
|
|
@@ -188,9 +223,13 @@ function createRedisHelpers(client) {
|
|
|
188
223
|
|
|
189
224
|
async setWithTTL(key, value, ttlMs) {
|
|
190
225
|
const px = Math.max(1, Math.ceil(ttlMs));
|
|
191
|
-
try
|
|
226
|
+
// Must dispatch on the dialect, not on a try/catch: node-redis accepts
|
|
227
|
+
// the ioredis positional form without complaint and simply stores the
|
|
228
|
+
// key with no expiry, so a catch-based fallback never fires and the TTL
|
|
229
|
+
// is silently dropped.
|
|
230
|
+
if (dialect === 'ioredis') {
|
|
192
231
|
await client.set(key, value, 'PX', px);
|
|
193
|
-
}
|
|
232
|
+
} else {
|
|
194
233
|
await client.set(key, value, { PX: px });
|
|
195
234
|
}
|
|
196
235
|
},
|
|
@@ -590,20 +629,33 @@ function memoryStore() {
|
|
|
590
629
|
* `@exortek/shared/validate`: **schema** for whole options objects,
|
|
591
630
|
* **asserts** for one-liner argument guards at the call site.
|
|
592
631
|
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
632
|
+
* Every guard is exported as a `make…` **factory** taking the binding
|
|
633
|
+
* package's `wrap` and returning the guard itself. A package binds the
|
|
634
|
+
* guards it calls once, in its `internal/guards.js`, so every argument
|
|
635
|
+
* failure throws that package's error class — `err instanceof
|
|
636
|
+
* CryptoError` holds, and users see at a glance which package raised it.
|
|
637
|
+
*
|
|
638
|
+
* // packages/<pkg>/src/internal/guards.js
|
|
639
|
+
* import { makeAssertString, makeInvalidArgument } from '@exortek/shared/asserts';
|
|
640
|
+
* import { CryptoError, ErrorCode } from '../errors.js';
|
|
641
|
+
*
|
|
642
|
+
* const wrap = (message, extra) => new CryptoError(ErrorCode.INVALID_ARGUMENT, message, extra);
|
|
643
|
+
*
|
|
644
|
+
* export const assertString = makeAssertString(wrap);
|
|
645
|
+
* export const invalidArgument = makeInvalidArgument(wrap);
|
|
646
|
+
*
|
|
647
|
+
* Importing each factory by name is what makes the module
|
|
648
|
+
* tree-shakeable. An earlier version handed back one object carrying
|
|
649
|
+
* every guard, so all of them shipped in every package's bundle no
|
|
650
|
+
* matter how few were used — `assertBoolean` reached fifteen published
|
|
651
|
+
* packages without a single call site. Import what you call.
|
|
600
652
|
*
|
|
601
653
|
* **Path naming convention** (used as the `name` argument):
|
|
602
654
|
*
|
|
603
655
|
* `<publicFunction>[.options|.config][.<field>]`
|
|
604
656
|
*
|
|
605
|
-
* - `assertString(name, 'createUser.name')`
|
|
606
|
-
* - `assertPositiveInt(n, 'scrypt.options.r')`
|
|
657
|
+
* - `assertString(name, 'createUser.name')` — top-level arg
|
|
658
|
+
* - `assertPositiveInt(n, 'scrypt.options.r')` — nested option
|
|
607
659
|
* - `assertBytesOrString(pwd, 'pepper.wrap.password')` — method arg
|
|
608
660
|
*
|
|
609
661
|
* Keep the path short: the emitted error is `"<name> must be <desc>"`,
|
|
@@ -612,289 +664,17 @@ function memoryStore() {
|
|
|
612
664
|
* encryptSymmetric()"), not for restating the field.
|
|
613
665
|
*/
|
|
614
666
|
|
|
615
|
-
/**
|
|
616
|
-
* @typedef {{ cause?: unknown }} WrapExtra
|
|
617
|
-
* Extra options forwarded to the wrap function, so `invalidArgument`
|
|
618
|
-
* sites that catch an underlying error can preserve the `cause` chain
|
|
619
|
-
* without falling back to a raw `new PackageError(...)`.
|
|
620
|
-
*
|
|
621
|
-
* @typedef {(message: string, extra?: WrapExtra) => Error} WrapFn
|
|
622
|
-
* Constructs (never throws) the binding package's error, e.g.
|
|
623
|
-
* `(m, { cause } = {}) => new CryptoError(ErrorCode.INVALID_ARGUMENT, m, { cause })`.
|
|
624
|
-
*
|
|
625
|
-
* @typedef {{ hint?: string }} AssertOptions
|
|
626
|
-
* `hint` is appended to the message after an em-dash — use it for
|
|
627
|
-
* actionable guidance ("pass the exact bytes returned by …").
|
|
628
|
-
*
|
|
629
|
-
* @typedef {{ safeParse: (input: unknown, path?: string) => { ok: true, value: unknown } | { ok: false, errors: string[] } }} ParseableSchema
|
|
630
|
-
*/
|
|
631
|
-
|
|
632
|
-
/**
|
|
633
|
-
* Build the failure message. `description` completes the sentence
|
|
634
|
-
* `"<name> must be <description>"`; `hint` (optional) follows an
|
|
635
|
-
* em-dash.
|
|
636
|
-
*
|
|
637
|
-
* @param {string} name
|
|
638
|
-
* @param {string} description
|
|
639
|
-
* @param {string} [hint]
|
|
640
|
-
* @returns {string}
|
|
641
|
-
*/
|
|
642
|
-
function message(name, description, hint) {
|
|
643
|
-
return `${name} must be ${description}${hint ? ` — ${hint}` : ''}`;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
const ENCODINGS = new Set(['hex', 'base64', 'base64url']);
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* Bind the assert set to a package's error class. Call once per
|
|
650
|
-
* package (conventionally in `internal/guards.js`) and import the
|
|
651
|
-
* bound object everywhere:
|
|
652
|
-
*
|
|
653
|
-
* import { bindAsserts } from '@exortek/shared/asserts';
|
|
654
|
-
* import { CryptoError, ErrorCode } from '../errors.js';
|
|
655
|
-
* export const g = bindAsserts((m) => new CryptoError(ErrorCode.INVALID_ARGUMENT, m));
|
|
656
|
-
*
|
|
657
|
-
* // call sites:
|
|
658
|
-
* g.assertPositiveInt(options.iterations, 'options.iterations');
|
|
659
|
-
* const cfg = g.parse(OptionsSchema, options, 'options');
|
|
660
|
-
*
|
|
661
|
-
* @param {WrapFn} wrap
|
|
662
|
-
*/
|
|
663
|
-
function bindAsserts(wrap) {
|
|
664
|
-
/**
|
|
665
|
-
* @param {string} name
|
|
666
|
-
* @param {string} description
|
|
667
|
-
* @param {AssertOptions} [opts]
|
|
668
|
-
* @returns {never}
|
|
669
|
-
*/
|
|
670
|
-
function fail(name, description, opts) {
|
|
671
|
-
throw wrap(message(name, description, opts?.hint));
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
/**
|
|
675
|
-
* Assert that `value` is a plain object (not `null`, not an array,
|
|
676
|
-
* not a primitive).
|
|
677
|
-
* @param {unknown} value
|
|
678
|
-
* @param {string} name
|
|
679
|
-
* @param {AssertOptions} [opts]
|
|
680
|
-
*/
|
|
681
|
-
function assertObject(value, name, opts) {
|
|
682
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
683
|
-
fail(name, 'an object', opts);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
return {
|
|
688
|
-
/**
|
|
689
|
-
* Construct (not throw) the bound error with a free-form message —
|
|
690
|
-
* for `throw g.invalidArgument('…')` sites that don't fit the
|
|
691
|
-
* `X must be Y` shape (canonicalisation errors, cross-field
|
|
692
|
-
* constraint failures). Pass `{ cause }` when the argument-shape
|
|
693
|
-
* failure was triggered by an underlying throw whose chain matters
|
|
694
|
-
* (e.g. `JSON.stringify` on a bigint).
|
|
695
|
-
*
|
|
696
|
-
* @param {string} msg
|
|
697
|
-
* @param {WrapExtra} [extra]
|
|
698
|
-
* @returns {Error}
|
|
699
|
-
*/
|
|
700
|
-
invalidArgument(msg, extra) {
|
|
701
|
-
return wrap(msg, extra);
|
|
702
|
-
},
|
|
703
|
-
|
|
704
|
-
/**
|
|
705
|
-
* Validate `input` against a `@exortek/shared/validate` schema;
|
|
706
|
-
* failures throw the bound error carrying every collected message.
|
|
707
|
-
* The bridge that keeps schema validation on the package's own
|
|
708
|
-
* error surface.
|
|
709
|
-
*
|
|
710
|
-
* @param {ParseableSchema} schema
|
|
711
|
-
* @param {unknown} input
|
|
712
|
-
* @param {string} [path='options']
|
|
713
|
-
* @returns {unknown} the parsed (possibly defaulted) value
|
|
714
|
-
*/
|
|
715
|
-
parse(schema, input, path = 'options') {
|
|
716
|
-
const r = schema.safeParse(input, path);
|
|
717
|
-
if (!r.ok) {
|
|
718
|
-
throw wrap(r.errors.join('; '));
|
|
719
|
-
}
|
|
720
|
-
return r.value;
|
|
721
|
-
},
|
|
722
|
-
|
|
723
|
-
/**
|
|
724
|
-
* Assert that `value` is a non-negative safe integer (`0, 1, 2, …`).
|
|
725
|
-
* @param {unknown} value
|
|
726
|
-
* @param {string} name Argument name to include in the error message.
|
|
727
|
-
* @param {AssertOptions} [opts]
|
|
728
|
-
*/
|
|
729
|
-
assertNonNegativeInt(value, name, opts) {
|
|
730
|
-
if (!Number.isSafeInteger(value) || /** @type {number} */ (value) < 0) {
|
|
731
|
-
fail(name, 'a non-negative safe integer', opts);
|
|
732
|
-
}
|
|
733
|
-
},
|
|
734
|
-
|
|
735
|
-
/**
|
|
736
|
-
* Assert that `value` is a strictly positive safe integer (`1, 2, 3, …`).
|
|
737
|
-
* @param {unknown} value
|
|
738
|
-
* @param {string} name
|
|
739
|
-
* @param {AssertOptions} [opts]
|
|
740
|
-
*/
|
|
741
|
-
assertPositiveInt(value, name, opts) {
|
|
742
|
-
if (!Number.isSafeInteger(value) || /** @type {number} */ (value) <= 0) {
|
|
743
|
-
fail(name, 'a positive integer', opts);
|
|
744
|
-
}
|
|
745
|
-
},
|
|
746
|
-
|
|
747
|
-
/**
|
|
748
|
-
* Assert that `value` fits in a 48-bit unsigned integer (`0 … 2^48 − 1`).
|
|
749
|
-
* Used for Unix millisecond timestamps in UUID v7 / ULID.
|
|
750
|
-
* @param {unknown} value
|
|
751
|
-
* @param {string} name
|
|
752
|
-
* @param {AssertOptions} [opts]
|
|
753
|
-
*/
|
|
754
|
-
assertUint48(value, name, opts) {
|
|
755
|
-
if (
|
|
756
|
-
!Number.isSafeInteger(value) ||
|
|
757
|
-
/** @type {number} */ (value) < 0 ||
|
|
758
|
-
/** @type {number} */ (value) > 0xffffffffffff
|
|
759
|
-
) {
|
|
760
|
-
fail(name, 'a non-negative safe integer ≤ 2^48 − 1 (Unix ms since epoch)', opts);
|
|
761
|
-
}
|
|
762
|
-
},
|
|
763
|
-
|
|
764
|
-
/**
|
|
765
|
-
* Assert that `value` is a string (may be empty).
|
|
766
|
-
* @param {unknown} value
|
|
767
|
-
* @param {string} name
|
|
768
|
-
* @param {AssertOptions} [opts]
|
|
769
|
-
*/
|
|
770
|
-
assertString(value, name, opts) {
|
|
771
|
-
if (typeof value !== 'string') {
|
|
772
|
-
fail(name, 'a string', opts);
|
|
773
|
-
}
|
|
774
|
-
},
|
|
775
|
-
|
|
776
|
-
/**
|
|
777
|
-
* Assert that `value` is a non-empty string.
|
|
778
|
-
* @param {unknown} value
|
|
779
|
-
* @param {string} name
|
|
780
|
-
* @param {AssertOptions} [opts]
|
|
781
|
-
*/
|
|
782
|
-
assertNonEmptyString(value, name, opts) {
|
|
783
|
-
if (typeof value !== 'string' || value.length === 0) {
|
|
784
|
-
fail(name, 'a non-empty string', opts);
|
|
785
|
-
}
|
|
786
|
-
},
|
|
787
|
-
|
|
788
|
-
/**
|
|
789
|
-
* Assert that `value` is a boolean.
|
|
790
|
-
* @param {unknown} value
|
|
791
|
-
* @param {string} name
|
|
792
|
-
* @param {AssertOptions} [opts]
|
|
793
|
-
*/
|
|
794
|
-
assertBoolean(value, name, opts) {
|
|
795
|
-
if (typeof value !== 'boolean') {
|
|
796
|
-
fail(name, 'a boolean', opts);
|
|
797
|
-
}
|
|
798
|
-
},
|
|
799
|
-
|
|
800
|
-
/**
|
|
801
|
-
* Assert that `value` is a function.
|
|
802
|
-
* @param {unknown} value
|
|
803
|
-
* @param {string} name
|
|
804
|
-
* @param {AssertOptions} [opts]
|
|
805
|
-
*/
|
|
806
|
-
assertFunction(value, name, opts) {
|
|
807
|
-
if (typeof value !== 'function') {
|
|
808
|
-
fail(name, 'a function', opts);
|
|
809
|
-
}
|
|
810
|
-
},
|
|
811
|
-
|
|
812
|
-
assertObject,
|
|
813
|
-
|
|
814
|
-
/**
|
|
815
|
-
* Assert that `value` is either `undefined` or a plain object —
|
|
816
|
-
* the "optional options object" pattern.
|
|
817
|
-
* @param {unknown} value
|
|
818
|
-
* @param {string} name
|
|
819
|
-
* @param {AssertOptions} [opts]
|
|
820
|
-
*/
|
|
821
|
-
assertOptionalObject(value, name, opts) {
|
|
822
|
-
if (value === undefined) {
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
assertObject(value, name, opts);
|
|
826
|
-
},
|
|
827
|
-
|
|
828
|
-
/**
|
|
829
|
-
* Assert that `value` is a byte buffer (`Buffer` or `Uint8Array`) —
|
|
830
|
-
* strings are NOT accepted. For already-encoded material where a
|
|
831
|
-
* string would be ambiguous (ciphertext, signatures, raw key bytes).
|
|
832
|
-
* @param {unknown} value
|
|
833
|
-
* @param {string} name
|
|
834
|
-
* @param {AssertOptions} [opts]
|
|
835
|
-
*/
|
|
836
|
-
assertBytes(value, name, opts) {
|
|
837
|
-
if (!Buffer.isBuffer(value) && !(value instanceof Uint8Array)) {
|
|
838
|
-
fail(name, 'a Buffer or Uint8Array', opts);
|
|
839
|
-
}
|
|
840
|
-
},
|
|
841
|
-
|
|
842
|
-
/**
|
|
843
|
-
* Assert that `value` is either a string or a byte buffer (`Buffer`
|
|
844
|
-
* or `Uint8Array`).
|
|
845
|
-
* @param {unknown} value
|
|
846
|
-
* @param {string} name
|
|
847
|
-
* @param {AssertOptions} [opts]
|
|
848
|
-
*/
|
|
849
|
-
assertBytesOrString(value, name, opts) {
|
|
850
|
-
if (typeof value !== 'string' && !Buffer.isBuffer(value) && !(value instanceof Uint8Array)) {
|
|
851
|
-
fail(name, 'a string or Buffer', opts);
|
|
852
|
-
}
|
|
853
|
-
},
|
|
854
|
-
|
|
855
|
-
/**
|
|
856
|
-
* Assert that `encoding` is one of the accepted output/input
|
|
857
|
-
* encodings. Pass `allowBuffer: false` where a `Buffer` output makes
|
|
858
|
-
* no sense (verifying a string signature, decoding a token payload).
|
|
859
|
-
*
|
|
860
|
-
* @param {unknown} encoding
|
|
861
|
-
* @param {string} name
|
|
862
|
-
* @param {AssertOptions & { allowBuffer?: boolean }} [opts]
|
|
863
|
-
*/
|
|
864
|
-
assertEncoding(encoding, name, opts) {
|
|
865
|
-
const allowBuffer = opts?.allowBuffer !== false;
|
|
866
|
-
const valid = (typeof encoding === 'string' && ENCODINGS.has(encoding)) || (allowBuffer && encoding === 'buffer');
|
|
867
|
-
if (!valid) {
|
|
868
|
-
const list = allowBuffer ? "'hex', 'base64', 'base64url', or 'buffer'" : "'hex', 'base64', or 'base64url'";
|
|
869
|
-
fail(name, list, opts);
|
|
870
|
-
}
|
|
871
|
-
},
|
|
872
|
-
};
|
|
873
|
-
}
|
|
874
667
|
|
|
875
668
|
/**
|
|
876
|
-
*
|
|
877
|
-
*
|
|
878
|
-
*
|
|
879
|
-
* `
|
|
880
|
-
*
|
|
881
|
-
*
|
|
882
|
-
* // packages/<pkg>/src/internal/guards.js
|
|
883
|
-
* import { defineGuards } from '@exortek/shared/asserts';
|
|
884
|
-
* import { PasswordError, ErrorCode } from '../errors.js';
|
|
885
|
-
*
|
|
886
|
-
* export const { assertString, invalidArgument, parse } =
|
|
887
|
-
* defineGuards(PasswordError, ErrorCode.INVALID_ARGUMENT);
|
|
669
|
+
* Construct (not throw) the bound error with a free-form message — for
|
|
670
|
+
* `throw invalidArgument('…')` sites that don't fit the `X must be Y`
|
|
671
|
+
* shape (canonicalisation errors, cross-field constraint failures). Pass
|
|
672
|
+
* `{ cause }` when the failure was triggered by an underlying throw whose
|
|
673
|
+
* chain matters (e.g. `JSON.stringify` on a bigint).
|
|
888
674
|
*
|
|
889
|
-
*
|
|
890
|
-
* mapping (an alt code, a decorated message) — it stays available.
|
|
891
|
-
*
|
|
892
|
-
* @param {new (code: string, message: string, options?: { cause?: unknown }) => Error} ErrorClass
|
|
893
|
-
* @param {string} code
|
|
675
|
+
* @type {(wrap: WrapFn) => (msg: string, extra?: WrapExtra) => Error}
|
|
894
676
|
*/
|
|
895
|
-
|
|
896
|
-
return bindAsserts((message, extra) => new ErrorClass(code, message, extra));
|
|
897
|
-
}
|
|
677
|
+
const makeInvalidArgument = wrap => (msg, extra) => wrap(msg, extra);
|
|
898
678
|
|
|
899
679
|
/**
|
|
900
680
|
* Shared base error class — the single error structure behind every
|
|
@@ -1005,10 +785,15 @@ class ApiKeyError extends BaseError {
|
|
|
1005
785
|
*/
|
|
1006
786
|
|
|
1007
787
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
788
|
+
/**
|
|
789
|
+
* Bind every guard this package calls to ApiKeyError, so an argument-shape
|
|
790
|
+
* failure anywhere carries one class and one code to branch on.
|
|
791
|
+
*
|
|
792
|
+
* @type {import('@exortek/shared/asserts').WrapFn}
|
|
793
|
+
*/
|
|
794
|
+
const wrap = (message, extra) => new ApiKeyError(ErrorCode.INVALID_ARGUMENT, message, extra);
|
|
795
|
+
|
|
796
|
+
const invalidArgument = makeInvalidArgument(wrap);
|
|
1012
797
|
|
|
1013
798
|
/**
|
|
1014
799
|
* Redis-backed store for API keys.
|
|
@@ -1112,39 +897,101 @@ function redisStore(client, options = {}) {
|
|
|
1112
897
|
}
|
|
1113
898
|
|
|
1114
899
|
/**
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
1117
|
-
* surfaces at construction time, not on the first `createApiKey` /
|
|
1118
|
-
* `verifyApiKey` call. Sync implementations are wrapped transparently
|
|
1119
|
-
* — return a plain value or a Promise, either works.
|
|
900
|
+
* Family factory for the `customStore(impl)` wrapper every store-shipping
|
|
901
|
+
* package exposes.
|
|
1120
902
|
*
|
|
1121
|
-
*
|
|
903
|
+
* Each package used to hand-roll the same three steps: assert `impl` is an
|
|
904
|
+
* object, assert its required methods are functions, then `Promise.resolve()`
|
|
905
|
+
* -wrap every call so a sync implementation is transparently promoted to the
|
|
906
|
+
* async store interface. Only the method-name arrays and the typed error
|
|
907
|
+
* differ. This collapses all of that into one binding per package.
|
|
1122
908
|
*
|
|
1123
|
-
*
|
|
1124
|
-
* @returns {import('../index.js').ApiKeyStore}
|
|
909
|
+
* Consumers: `apikey`, `challenge`, `magic-link`, `opaque`, `session`.
|
|
1125
910
|
*/
|
|
1126
911
|
|
|
1127
|
-
const REQUIRED = ['put', 'getById', 'update', 'revoke', 'revokeAllForUser', 'listByUser'];
|
|
1128
912
|
|
|
1129
|
-
|
|
913
|
+
/**
|
|
914
|
+
* Assert `impl` is an object exposing every method in `required` as a function.
|
|
915
|
+
* Throws the caller's typed error (via `wrap`) on the first failure. Used on
|
|
916
|
+
* its own by stores that keep a caller's implementation verbatim (jwt / paseto
|
|
917
|
+
* token registries), and internally by {@link createCustomStoreValidator}.
|
|
918
|
+
*
|
|
919
|
+
* @param {unknown} impl
|
|
920
|
+
* @param {readonly string[]} required
|
|
921
|
+
* @param {(message: string) => Error} wrap
|
|
922
|
+
* The package's error factory; its returned error is thrown. A `wrap` that
|
|
923
|
+
* throws directly also works (the outer `throw` is then unreachable).
|
|
924
|
+
* @returns {void}
|
|
925
|
+
*/
|
|
926
|
+
function assertStoreShape(impl, required, wrap) {
|
|
1130
927
|
if (!isObject(impl)) {
|
|
1131
|
-
throw
|
|
928
|
+
throw wrap(`customStore(impl) requires an object with { ${required.join(', ')} } methods`);
|
|
1132
929
|
}
|
|
1133
|
-
for (const name of
|
|
1134
|
-
if (!isFunction(impl[name])) {
|
|
1135
|
-
throw
|
|
930
|
+
for (const name of required) {
|
|
931
|
+
if (!isFunction(/** @type {any} */ (impl)[name])) {
|
|
932
|
+
throw wrap(`customStore: impl.${name} is required and must be a function`);
|
|
1136
933
|
}
|
|
1137
934
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* @param {object} spec
|
|
939
|
+
* @param {readonly string[]} spec.required
|
|
940
|
+
* Methods that must exist on `impl`; missing ones fail at construction time.
|
|
941
|
+
* @param {readonly string[]} [spec.optional]
|
|
942
|
+
* Methods wrapped through only when present (feature-gated store extras).
|
|
943
|
+
* @param {(message: string) => Error} spec.wrap
|
|
944
|
+
* The package's error factory (its `invalidArgument`) — called with a
|
|
945
|
+
* diagnostic message; its returned typed error is thrown so
|
|
946
|
+
* `err instanceof <Pkg>Error` holds. A `wrap` that throws directly also
|
|
947
|
+
* works (the outer `throw` is then unreachable).
|
|
948
|
+
* @param {Record<string, (result: any) => any>} [spec.coerce]
|
|
949
|
+
* Optional per-method post-processor applied to the resolved result — e.g.
|
|
950
|
+
* opaque coerces `set` to `undefined` so a native `Map.set` return value
|
|
951
|
+
* can't leak out as `Promise<Map>`.
|
|
952
|
+
* @returns {(impl: object) => Record<string, (...args: any[]) => Promise<any>>}
|
|
953
|
+
*/
|
|
954
|
+
function createCustomStoreValidator({ required, optional = [], wrap, coerce = {} }) {
|
|
955
|
+
return function customStore(impl) {
|
|
956
|
+
assertStoreShape(impl, required, wrap);
|
|
957
|
+
|
|
958
|
+
/** @type {Record<string, (...args: any[]) => Promise<any>>} */
|
|
959
|
+
const store = {};
|
|
960
|
+
const bind = name => {
|
|
961
|
+
const post = coerce[name];
|
|
962
|
+
store[name] = post
|
|
963
|
+
? (...args) => Promise.resolve(impl[name](...args)).then(post)
|
|
964
|
+
: (...args) => Promise.resolve(impl[name](...args));
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
for (const name of required) {
|
|
968
|
+
bind(name);
|
|
969
|
+
}
|
|
970
|
+
for (const name of optional) {
|
|
971
|
+
if (isFunction(impl[name])) {
|
|
972
|
+
bind(name);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return store;
|
|
1145
976
|
};
|
|
1146
977
|
}
|
|
1147
978
|
|
|
979
|
+
/**
|
|
980
|
+
* Wrap a user-supplied store into the `ApiKeyStore` interface. Validates the
|
|
981
|
+
* required methods at construction time (not on first use) and promotes sync
|
|
982
|
+
* implementations to the async interface.
|
|
983
|
+
*
|
|
984
|
+
* Required: `put`, `getById`, `update`, `revoke`, `revokeAllForUser`, `listByUser`.
|
|
985
|
+
*
|
|
986
|
+
* @param {Partial<import('../index.js').ApiKeyStore>} impl
|
|
987
|
+
* @returns {import('../index.js').ApiKeyStore}
|
|
988
|
+
*/
|
|
989
|
+
|
|
990
|
+
const customStore = createCustomStoreValidator({
|
|
991
|
+
required: ['put', 'getById', 'update', 'revoke', 'revokeAllForUser', 'listByUser'],
|
|
992
|
+
wrap: invalidArgument,
|
|
993
|
+
});
|
|
994
|
+
|
|
1148
995
|
exports.customStore = customStore;
|
|
1149
996
|
exports.memoryStore = memoryStore;
|
|
1150
997
|
exports.redisStore = redisStore;
|