@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.
@@ -196,20 +196,33 @@ function fastifyPlugin(fn, options) {
196
196
  * `@exortek/shared/validate`: **schema** for whole options objects,
197
197
  * **asserts** for one-liner argument guards at the call site.
198
198
  *
199
- * The consumer surface is {@link defineGuards} (or the raw
200
- * {@link bindAsserts} for full control). Each package binds the assert
201
- * set to its own typed error class once (in `internal/guards.js`), so
202
- * every argument failure throws that package's class —
203
- * `err instanceof CryptoError` holds and users see at a glance which
204
- * package raised the error. The bound `parse` bridges
205
- * `@exortek/shared/validate` schemas to the same error class.
199
+ * Every guard is exported as a `make…` **factory** taking the binding
200
+ * package's `wrap` and returning the guard itself. A package binds the
201
+ * guards it calls once, in its `internal/guards.js`, so every argument
202
+ * failure throws that package's error class — `err instanceof
203
+ * CryptoError` holds, and users see at a glance which package raised it.
204
+ *
205
+ * // packages/<pkg>/src/internal/guards.js
206
+ * import { makeAssertString, makeInvalidArgument } from '@exortek/shared/asserts';
207
+ * import { CryptoError, ErrorCode } from '../errors.js';
208
+ *
209
+ * const wrap = (message, extra) => new CryptoError(ErrorCode.INVALID_ARGUMENT, message, extra);
210
+ *
211
+ * export const assertString = makeAssertString(wrap);
212
+ * export const invalidArgument = makeInvalidArgument(wrap);
213
+ *
214
+ * Importing each factory by name is what makes the module
215
+ * tree-shakeable. An earlier version handed back one object carrying
216
+ * every guard, so all of them shipped in every package's bundle no
217
+ * matter how few were used — `assertBoolean` reached fifteen published
218
+ * packages without a single call site. Import what you call.
206
219
  *
207
220
  * **Path naming convention** (used as the `name` argument):
208
221
  *
209
222
  * `<publicFunction>[.options|.config][.<field>]`
210
223
  *
211
- * - `assertString(name, 'createUser.name')` — top-level arg
212
- * - `assertPositiveInt(n, 'scrypt.options.r')` — nested option
224
+ * - `assertString(name, 'createUser.name')` — top-level arg
225
+ * - `assertPositiveInt(n, 'scrypt.options.r')` — nested option
213
226
  * - `assertBytesOrString(pwd, 'pepper.wrap.password')` — method arg
214
227
  *
215
228
  * Keep the path short: the emitted error is `"<name> must be <desc>"`,
@@ -218,289 +231,17 @@ function fastifyPlugin(fn, options) {
218
231
  * encryptSymmetric()"), not for restating the field.
219
232
  */
220
233
 
221
- /**
222
- * @typedef {{ cause?: unknown }} WrapExtra
223
- * Extra options forwarded to the wrap function, so `invalidArgument`
224
- * sites that catch an underlying error can preserve the `cause` chain
225
- * without falling back to a raw `new PackageError(...)`.
226
- *
227
- * @typedef {(message: string, extra?: WrapExtra) => Error} WrapFn
228
- * Constructs (never throws) the binding package's error, e.g.
229
- * `(m, { cause } = {}) => new CryptoError(ErrorCode.INVALID_ARGUMENT, m, { cause })`.
230
- *
231
- * @typedef {{ hint?: string }} AssertOptions
232
- * `hint` is appended to the message after an em-dash — use it for
233
- * actionable guidance ("pass the exact bytes returned by …").
234
- *
235
- * @typedef {{ safeParse: (input: unknown, path?: string) => { ok: true, value: unknown } | { ok: false, errors: string[] } }} ParseableSchema
236
- */
237
-
238
- /**
239
- * Build the failure message. `description` completes the sentence
240
- * `"<name> must be <description>"`; `hint` (optional) follows an
241
- * em-dash.
242
- *
243
- * @param {string} name
244
- * @param {string} description
245
- * @param {string} [hint]
246
- * @returns {string}
247
- */
248
- function message(name, description, hint) {
249
- return `${name} must be ${description}${hint ? ` — ${hint}` : ''}`;
250
- }
251
-
252
- const ENCODINGS = new Set(['hex', 'base64', 'base64url']);
253
-
254
- /**
255
- * Bind the assert set to a package's error class. Call once per
256
- * package (conventionally in `internal/guards.js`) and import the
257
- * bound object everywhere:
258
- *
259
- * import { bindAsserts } from '@exortek/shared/asserts';
260
- * import { CryptoError, ErrorCode } from '../errors.js';
261
- * export const g = bindAsserts((m) => new CryptoError(ErrorCode.INVALID_ARGUMENT, m));
262
- *
263
- * // call sites:
264
- * g.assertPositiveInt(options.iterations, 'options.iterations');
265
- * const cfg = g.parse(OptionsSchema, options, 'options');
266
- *
267
- * @param {WrapFn} wrap
268
- */
269
- function bindAsserts(wrap) {
270
- /**
271
- * @param {string} name
272
- * @param {string} description
273
- * @param {AssertOptions} [opts]
274
- * @returns {never}
275
- */
276
- function fail(name, description, opts) {
277
- throw wrap(message(name, description, opts?.hint));
278
- }
279
-
280
- /**
281
- * Assert that `value` is a plain object (not `null`, not an array,
282
- * not a primitive).
283
- * @param {unknown} value
284
- * @param {string} name
285
- * @param {AssertOptions} [opts]
286
- */
287
- function assertObject(value, name, opts) {
288
- if (value === null || typeof value !== 'object' || Array.isArray(value)) {
289
- fail(name, 'an object', opts);
290
- }
291
- }
292
-
293
- return {
294
- /**
295
- * Construct (not throw) the bound error with a free-form message —
296
- * for `throw g.invalidArgument('…')` sites that don't fit the
297
- * `X must be Y` shape (canonicalisation errors, cross-field
298
- * constraint failures). Pass `{ cause }` when the argument-shape
299
- * failure was triggered by an underlying throw whose chain matters
300
- * (e.g. `JSON.stringify` on a bigint).
301
- *
302
- * @param {string} msg
303
- * @param {WrapExtra} [extra]
304
- * @returns {Error}
305
- */
306
- invalidArgument(msg, extra) {
307
- return wrap(msg, extra);
308
- },
309
-
310
- /**
311
- * Validate `input` against a `@exortek/shared/validate` schema;
312
- * failures throw the bound error carrying every collected message.
313
- * The bridge that keeps schema validation on the package's own
314
- * error surface.
315
- *
316
- * @param {ParseableSchema} schema
317
- * @param {unknown} input
318
- * @param {string} [path='options']
319
- * @returns {unknown} the parsed (possibly defaulted) value
320
- */
321
- parse(schema, input, path = 'options') {
322
- const r = schema.safeParse(input, path);
323
- if (!r.ok) {
324
- throw wrap(r.errors.join('; '));
325
- }
326
- return r.value;
327
- },
328
-
329
- /**
330
- * Assert that `value` is a non-negative safe integer (`0, 1, 2, …`).
331
- * @param {unknown} value
332
- * @param {string} name Argument name to include in the error message.
333
- * @param {AssertOptions} [opts]
334
- */
335
- assertNonNegativeInt(value, name, opts) {
336
- if (!Number.isSafeInteger(value) || /** @type {number} */ (value) < 0) {
337
- fail(name, 'a non-negative safe integer', opts);
338
- }
339
- },
340
-
341
- /**
342
- * Assert that `value` is a strictly positive safe integer (`1, 2, 3, …`).
343
- * @param {unknown} value
344
- * @param {string} name
345
- * @param {AssertOptions} [opts]
346
- */
347
- assertPositiveInt(value, name, opts) {
348
- if (!Number.isSafeInteger(value) || /** @type {number} */ (value) <= 0) {
349
- fail(name, 'a positive integer', opts);
350
- }
351
- },
352
-
353
- /**
354
- * Assert that `value` fits in a 48-bit unsigned integer (`0 … 2^48 − 1`).
355
- * Used for Unix millisecond timestamps in UUID v7 / ULID.
356
- * @param {unknown} value
357
- * @param {string} name
358
- * @param {AssertOptions} [opts]
359
- */
360
- assertUint48(value, name, opts) {
361
- if (
362
- !Number.isSafeInteger(value) ||
363
- /** @type {number} */ (value) < 0 ||
364
- /** @type {number} */ (value) > 0xffffffffffff
365
- ) {
366
- fail(name, 'a non-negative safe integer ≤ 2^48 − 1 (Unix ms since epoch)', opts);
367
- }
368
- },
369
-
370
- /**
371
- * Assert that `value` is a string (may be empty).
372
- * @param {unknown} value
373
- * @param {string} name
374
- * @param {AssertOptions} [opts]
375
- */
376
- assertString(value, name, opts) {
377
- if (typeof value !== 'string') {
378
- fail(name, 'a string', opts);
379
- }
380
- },
381
-
382
- /**
383
- * Assert that `value` is a non-empty string.
384
- * @param {unknown} value
385
- * @param {string} name
386
- * @param {AssertOptions} [opts]
387
- */
388
- assertNonEmptyString(value, name, opts) {
389
- if (typeof value !== 'string' || value.length === 0) {
390
- fail(name, 'a non-empty string', opts);
391
- }
392
- },
393
-
394
- /**
395
- * Assert that `value` is a boolean.
396
- * @param {unknown} value
397
- * @param {string} name
398
- * @param {AssertOptions} [opts]
399
- */
400
- assertBoolean(value, name, opts) {
401
- if (typeof value !== 'boolean') {
402
- fail(name, 'a boolean', opts);
403
- }
404
- },
405
-
406
- /**
407
- * Assert that `value` is a function.
408
- * @param {unknown} value
409
- * @param {string} name
410
- * @param {AssertOptions} [opts]
411
- */
412
- assertFunction(value, name, opts) {
413
- if (typeof value !== 'function') {
414
- fail(name, 'a function', opts);
415
- }
416
- },
417
-
418
- assertObject,
419
-
420
- /**
421
- * Assert that `value` is either `undefined` or a plain object —
422
- * the "optional options object" pattern.
423
- * @param {unknown} value
424
- * @param {string} name
425
- * @param {AssertOptions} [opts]
426
- */
427
- assertOptionalObject(value, name, opts) {
428
- if (value === undefined) {
429
- return;
430
- }
431
- assertObject(value, name, opts);
432
- },
433
-
434
- /**
435
- * Assert that `value` is a byte buffer (`Buffer` or `Uint8Array`) —
436
- * strings are NOT accepted. For already-encoded material where a
437
- * string would be ambiguous (ciphertext, signatures, raw key bytes).
438
- * @param {unknown} value
439
- * @param {string} name
440
- * @param {AssertOptions} [opts]
441
- */
442
- assertBytes(value, name, opts) {
443
- if (!Buffer.isBuffer(value) && !(value instanceof Uint8Array)) {
444
- fail(name, 'a Buffer or Uint8Array', opts);
445
- }
446
- },
447
-
448
- /**
449
- * Assert that `value` is either a string or a byte buffer (`Buffer`
450
- * or `Uint8Array`).
451
- * @param {unknown} value
452
- * @param {string} name
453
- * @param {AssertOptions} [opts]
454
- */
455
- assertBytesOrString(value, name, opts) {
456
- if (typeof value !== 'string' && !Buffer.isBuffer(value) && !(value instanceof Uint8Array)) {
457
- fail(name, 'a string or Buffer', opts);
458
- }
459
- },
460
-
461
- /**
462
- * Assert that `encoding` is one of the accepted output/input
463
- * encodings. Pass `allowBuffer: false` where a `Buffer` output makes
464
- * no sense (verifying a string signature, decoding a token payload).
465
- *
466
- * @param {unknown} encoding
467
- * @param {string} name
468
- * @param {AssertOptions & { allowBuffer?: boolean }} [opts]
469
- */
470
- assertEncoding(encoding, name, opts) {
471
- const allowBuffer = opts?.allowBuffer !== false;
472
- const valid = (typeof encoding === 'string' && ENCODINGS.has(encoding)) || (allowBuffer && encoding === 'buffer');
473
- if (!valid) {
474
- const list = allowBuffer ? "'hex', 'base64', 'base64url', or 'buffer'" : "'hex', 'base64', or 'base64url'";
475
- fail(name, list, opts);
476
- }
477
- },
478
- };
479
- }
480
234
 
481
235
  /**
482
- * One-line sugar over {@link bindAsserts} — the overwhelmingly common
483
- * pattern where a package binds every argument-shape failure to its
484
- * own error class + `INVALID_ARGUMENT` code. Emits the standard
485
- * `new ErrorClass(code, message, { cause })` shape, so the underlying
486
- * `BaseError` `{ cause, status, details }` options object is respected.
487
- *
488
- * // packages/<pkg>/src/internal/guards.js
489
- * import { defineGuards } from '@exortek/shared/asserts';
490
- * import { PasswordError, ErrorCode } from '../errors.js';
491
- *
492
- * export const { assertString, invalidArgument, parse } =
493
- * defineGuards(PasswordError, ErrorCode.INVALID_ARGUMENT);
236
+ * Construct (not throw) the bound error with a free-form message — for
237
+ * `throw invalidArgument('…')` sites that don't fit the `X must be Y`
238
+ * shape (canonicalisation errors, cross-field constraint failures). Pass
239
+ * `{ cause }` when the failure was triggered by an underlying throw whose
240
+ * chain matters (e.g. `JSON.stringify` on a bigint).
494
241
  *
495
- * Reach for the raw {@link bindAsserts} when you need a different
496
- * mapping (an alt code, a decorated message) — it stays available.
497
- *
498
- * @param {new (code: string, message: string, options?: { cause?: unknown }) => Error} ErrorClass
499
- * @param {string} code
242
+ * @type {(wrap: WrapFn) => (msg: string, extra?: WrapExtra) => Error}
500
243
  */
501
- function defineGuards(ErrorClass, code) {
502
- return bindAsserts((message, extra) => new ErrorClass(code, message, extra));
503
- }
244
+ const makeInvalidArgument = wrap => (msg, extra) => wrap(msg, extra);
504
245
 
505
246
  /**
506
247
  * Shared base error class — the single error structure behind every
@@ -611,10 +352,15 @@ class ApiKeyError extends BaseError {
611
352
  */
612
353
 
613
354
 
614
- const { invalidArgument} = defineGuards(
615
- ApiKeyError,
616
- ErrorCode.INVALID_ARGUMENT,
617
- );
355
+ /**
356
+ * Bind every guard this package calls to ApiKeyError, so an argument-shape
357
+ * failure anywhere carries one class and one code to branch on.
358
+ *
359
+ * @type {import('@exortek/shared/asserts').WrapFn}
360
+ */
361
+ const wrap = (message, extra) => new ApiKeyError(ErrorCode.INVALID_ARGUMENT, message, extra);
362
+
363
+ const invalidArgument = makeInvalidArgument(wrap);
618
364
 
619
365
  /**
620
366
  * Scope match helpers.
@@ -1,8 +1 @@
1
- export function customStore(impl: any): {
2
- put: (record: any) => Promise<any>;
3
- getById: (id: any) => Promise<any>;
4
- update: (id: any, patch: any) => Promise<any>;
5
- revoke: (id: any, reason: any) => Promise<any>;
6
- revokeAllForUser: (userId: any, reason: any) => Promise<any>;
7
- listByUser: (userId: any) => Promise<any>;
8
- };
1
+ export const customStore: (impl: object) => Record<string, (...args: any[]) => Promise<any>>;
@@ -1,3 +1,79 @@
1
- export { memoryStore } from "./memory.js";
2
- export { redisStore } from "./redis.js";
3
- export { customStore } from "./custom.js";
1
+ type ApiKeyRecord = {
2
+ /**
3
+ * Plaintext lookup key (128-bit b64u).
4
+ */
5
+ id: string;
6
+ /**
7
+ * Storage hash of the secret (43-char b64u).
8
+ */
9
+ hash: string;
10
+ /**
11
+ * The prefix the key was minted with.
12
+ */
13
+ prefix: string;
14
+ userId: string;
15
+ scopes: string[];
16
+ /**
17
+ * Human label ("Production Backend").
18
+ */
19
+ name?: string | undefined;
20
+ /**
21
+ * `'live'` / `'test'` / caller-defined.
22
+ */
23
+ environment?: string | undefined;
24
+ metadata?: Record<string, unknown> | undefined;
25
+ /**
26
+ * ms epoch.
27
+ */
28
+ createdAt: number;
29
+ /**
30
+ * ms epoch; absent = no expiry.
31
+ */
32
+ expiresAt?: number | undefined;
33
+ /**
34
+ * ms epoch when the record was revoked.
35
+ */
36
+ revokedAt?: number | undefined;
37
+ revokedReason?: string | undefined;
38
+ /**
39
+ * ms epoch of the last successful verify.
40
+ */
41
+ lastUsedAt?: number | undefined;
42
+ /**
43
+ * Index into the peppers array that was used at mint. 0 = newest.
44
+ */
45
+ pepperVersion?: number | undefined;
46
+ };
47
+ type ApiKeyStore = {
48
+ put: (record: ApiKeyRecord) => Promise<void>;
49
+ getById: (id: string) => Promise<ApiKeyRecord | null>;
50
+ update: (id: string, patch: Partial<ApiKeyRecord>) => Promise<ApiKeyRecord | null>;
51
+ revoke: (id: string, reason?: string) => Promise<boolean>;
52
+ revokeAllForUser: (userId: string, reason?: string) => Promise<number>;
53
+ listByUser: (userId: string) => Promise<ApiKeyRecord[]>;
54
+ };
55
+
56
+ /**
57
+ * @returns {import('../index.js').ApiKeyStore & { _size: () => number }}
58
+ */
59
+ declare function memoryStore(): ApiKeyStore & {
60
+ _size: () => number;
61
+ };
62
+
63
+ /**
64
+ * @typedef {object} RedisStoreOptions
65
+ * @property {string} [keyPrefix='apikey:']
66
+ */
67
+ /**
68
+ * @param {any} client
69
+ * @param {RedisStoreOptions} [options]
70
+ * @returns {import('../index.js').ApiKeyStore}
71
+ */
72
+ declare function redisStore(client: any, options?: RedisStoreOptions): ApiKeyStore;
73
+ type RedisStoreOptions = {
74
+ keyPrefix?: string | undefined;
75
+ };
76
+
77
+ declare const customStore: (impl: object) => Record<string, (...args: any[]) => Promise<any>>;
78
+
79
+ export { customStore, memoryStore, redisStore };