@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.
@@ -135,20 +135,33 @@ function isBuffer(value) {
135
135
  * `@exortek/shared/validate`: **schema** for whole options objects,
136
136
  * **asserts** for one-liner argument guards at the call site.
137
137
  *
138
- * The consumer surface is {@link defineGuards} (or the raw
139
- * {@link bindAsserts} for full control). Each package binds the assert
140
- * set to its own typed error class once (in `internal/guards.js`), so
141
- * every argument failure throws that package's class —
142
- * `err instanceof CryptoError` holds and users see at a glance which
143
- * package raised the error. The bound `parse` bridges
144
- * `@exortek/shared/validate` schemas to the same error class.
138
+ * Every guard is exported as a `make…` **factory** taking the binding
139
+ * package's `wrap` and returning the guard itself. A package binds the
140
+ * guards it calls once, in its `internal/guards.js`, so every argument
141
+ * failure throws that package's error class — `err instanceof
142
+ * CryptoError` holds, and users see at a glance which package raised it.
143
+ *
144
+ * // packages/<pkg>/src/internal/guards.js
145
+ * import { makeAssertString, makeInvalidArgument } from '@exortek/shared/asserts';
146
+ * import { CryptoError, ErrorCode } from '../errors.js';
147
+ *
148
+ * const wrap = (message, extra) => new CryptoError(ErrorCode.INVALID_ARGUMENT, message, extra);
149
+ *
150
+ * export const assertString = makeAssertString(wrap);
151
+ * export const invalidArgument = makeInvalidArgument(wrap);
152
+ *
153
+ * Importing each factory by name is what makes the module
154
+ * tree-shakeable. An earlier version handed back one object carrying
155
+ * every guard, so all of them shipped in every package's bundle no
156
+ * matter how few were used — `assertBoolean` reached fifteen published
157
+ * packages without a single call site. Import what you call.
145
158
  *
146
159
  * **Path naming convention** (used as the `name` argument):
147
160
  *
148
161
  * `<publicFunction>[.options|.config][.<field>]`
149
162
  *
150
- * - `assertString(name, 'createUser.name')` — top-level arg
151
- * - `assertPositiveInt(n, 'scrypt.options.r')` — nested option
163
+ * - `assertString(name, 'createUser.name')` — top-level arg
164
+ * - `assertPositiveInt(n, 'scrypt.options.r')` — nested option
152
165
  * - `assertBytesOrString(pwd, 'pepper.wrap.password')` — method arg
153
166
  *
154
167
  * Keep the path short: the emitted error is `"<name> must be <desc>"`,
@@ -157,289 +170,17 @@ function isBuffer(value) {
157
170
  * encryptSymmetric()"), not for restating the field.
158
171
  */
159
172
 
160
- /**
161
- * @typedef {{ cause?: unknown }} WrapExtra
162
- * Extra options forwarded to the wrap function, so `invalidArgument`
163
- * sites that catch an underlying error can preserve the `cause` chain
164
- * without falling back to a raw `new PackageError(...)`.
165
- *
166
- * @typedef {(message: string, extra?: WrapExtra) => Error} WrapFn
167
- * Constructs (never throws) the binding package's error, e.g.
168
- * `(m, { cause } = {}) => new CryptoError(ErrorCode.INVALID_ARGUMENT, m, { cause })`.
169
- *
170
- * @typedef {{ hint?: string }} AssertOptions
171
- * `hint` is appended to the message after an em-dash — use it for
172
- * actionable guidance ("pass the exact bytes returned by …").
173
- *
174
- * @typedef {{ safeParse: (input: unknown, path?: string) => { ok: true, value: unknown } | { ok: false, errors: string[] } }} ParseableSchema
175
- */
176
173
 
177
174
  /**
178
- * Build the failure message. `description` completes the sentence
179
- * `"<name> must be <description>"`; `hint` (optional) follows an
180
- * em-dash.
175
+ * Construct (not throw) the bound error with a free-form message — for
176
+ * `throw invalidArgument('…')` sites that don't fit the `X must be Y`
177
+ * shape (canonicalisation errors, cross-field constraint failures). Pass
178
+ * `{ cause }` when the failure was triggered by an underlying throw whose
179
+ * chain matters (e.g. `JSON.stringify` on a bigint).
181
180
  *
182
- * @param {string} name
183
- * @param {string} description
184
- * @param {string} [hint]
185
- * @returns {string}
181
+ * @type {(wrap: WrapFn) => (msg: string, extra?: WrapExtra) => Error}
186
182
  */
187
- function message(name, description, hint) {
188
- return `${name} must be ${description}${hint ? ` — ${hint}` : ''}`;
189
- }
190
-
191
- const ENCODINGS = new Set(['hex', 'base64', 'base64url']);
192
-
193
- /**
194
- * Bind the assert set to a package's error class. Call once per
195
- * package (conventionally in `internal/guards.js`) and import the
196
- * bound object everywhere:
197
- *
198
- * import { bindAsserts } from '@exortek/shared/asserts';
199
- * import { CryptoError, ErrorCode } from '../errors.js';
200
- * export const g = bindAsserts((m) => new CryptoError(ErrorCode.INVALID_ARGUMENT, m));
201
- *
202
- * // call sites:
203
- * g.assertPositiveInt(options.iterations, 'options.iterations');
204
- * const cfg = g.parse(OptionsSchema, options, 'options');
205
- *
206
- * @param {WrapFn} wrap
207
- */
208
- function bindAsserts(wrap) {
209
- /**
210
- * @param {string} name
211
- * @param {string} description
212
- * @param {AssertOptions} [opts]
213
- * @returns {never}
214
- */
215
- function fail(name, description, opts) {
216
- throw wrap(message(name, description, opts?.hint));
217
- }
218
-
219
- /**
220
- * Assert that `value` is a plain object (not `null`, not an array,
221
- * not a primitive).
222
- * @param {unknown} value
223
- * @param {string} name
224
- * @param {AssertOptions} [opts]
225
- */
226
- function assertObject(value, name, opts) {
227
- if (value === null || typeof value !== 'object' || Array.isArray(value)) {
228
- fail(name, 'an object', opts);
229
- }
230
- }
231
-
232
- return {
233
- /**
234
- * Construct (not throw) the bound error with a free-form message —
235
- * for `throw g.invalidArgument('…')` sites that don't fit the
236
- * `X must be Y` shape (canonicalisation errors, cross-field
237
- * constraint failures). Pass `{ cause }` when the argument-shape
238
- * failure was triggered by an underlying throw whose chain matters
239
- * (e.g. `JSON.stringify` on a bigint).
240
- *
241
- * @param {string} msg
242
- * @param {WrapExtra} [extra]
243
- * @returns {Error}
244
- */
245
- invalidArgument(msg, extra) {
246
- return wrap(msg, extra);
247
- },
248
-
249
- /**
250
- * Validate `input` against a `@exortek/shared/validate` schema;
251
- * failures throw the bound error carrying every collected message.
252
- * The bridge that keeps schema validation on the package's own
253
- * error surface.
254
- *
255
- * @param {ParseableSchema} schema
256
- * @param {unknown} input
257
- * @param {string} [path='options']
258
- * @returns {unknown} the parsed (possibly defaulted) value
259
- */
260
- parse(schema, input, path = 'options') {
261
- const r = schema.safeParse(input, path);
262
- if (!r.ok) {
263
- throw wrap(r.errors.join('; '));
264
- }
265
- return r.value;
266
- },
267
-
268
- /**
269
- * Assert that `value` is a non-negative safe integer (`0, 1, 2, …`).
270
- * @param {unknown} value
271
- * @param {string} name Argument name to include in the error message.
272
- * @param {AssertOptions} [opts]
273
- */
274
- assertNonNegativeInt(value, name, opts) {
275
- if (!Number.isSafeInteger(value) || /** @type {number} */ (value) < 0) {
276
- fail(name, 'a non-negative safe integer', opts);
277
- }
278
- },
279
-
280
- /**
281
- * Assert that `value` is a strictly positive safe integer (`1, 2, 3, …`).
282
- * @param {unknown} value
283
- * @param {string} name
284
- * @param {AssertOptions} [opts]
285
- */
286
- assertPositiveInt(value, name, opts) {
287
- if (!Number.isSafeInteger(value) || /** @type {number} */ (value) <= 0) {
288
- fail(name, 'a positive integer', opts);
289
- }
290
- },
291
-
292
- /**
293
- * Assert that `value` fits in a 48-bit unsigned integer (`0 … 2^48 − 1`).
294
- * Used for Unix millisecond timestamps in UUID v7 / ULID.
295
- * @param {unknown} value
296
- * @param {string} name
297
- * @param {AssertOptions} [opts]
298
- */
299
- assertUint48(value, name, opts) {
300
- if (
301
- !Number.isSafeInteger(value) ||
302
- /** @type {number} */ (value) < 0 ||
303
- /** @type {number} */ (value) > 0xffffffffffff
304
- ) {
305
- fail(name, 'a non-negative safe integer ≤ 2^48 − 1 (Unix ms since epoch)', opts);
306
- }
307
- },
308
-
309
- /**
310
- * Assert that `value` is a string (may be empty).
311
- * @param {unknown} value
312
- * @param {string} name
313
- * @param {AssertOptions} [opts]
314
- */
315
- assertString(value, name, opts) {
316
- if (typeof value !== 'string') {
317
- fail(name, 'a string', opts);
318
- }
319
- },
320
-
321
- /**
322
- * Assert that `value` is a non-empty string.
323
- * @param {unknown} value
324
- * @param {string} name
325
- * @param {AssertOptions} [opts]
326
- */
327
- assertNonEmptyString(value, name, opts) {
328
- if (typeof value !== 'string' || value.length === 0) {
329
- fail(name, 'a non-empty string', opts);
330
- }
331
- },
332
-
333
- /**
334
- * Assert that `value` is a boolean.
335
- * @param {unknown} value
336
- * @param {string} name
337
- * @param {AssertOptions} [opts]
338
- */
339
- assertBoolean(value, name, opts) {
340
- if (typeof value !== 'boolean') {
341
- fail(name, 'a boolean', opts);
342
- }
343
- },
344
-
345
- /**
346
- * Assert that `value` is a function.
347
- * @param {unknown} value
348
- * @param {string} name
349
- * @param {AssertOptions} [opts]
350
- */
351
- assertFunction(value, name, opts) {
352
- if (typeof value !== 'function') {
353
- fail(name, 'a function', opts);
354
- }
355
- },
356
-
357
- assertObject,
358
-
359
- /**
360
- * Assert that `value` is either `undefined` or a plain object —
361
- * the "optional options object" pattern.
362
- * @param {unknown} value
363
- * @param {string} name
364
- * @param {AssertOptions} [opts]
365
- */
366
- assertOptionalObject(value, name, opts) {
367
- if (value === undefined) {
368
- return;
369
- }
370
- assertObject(value, name, opts);
371
- },
372
-
373
- /**
374
- * Assert that `value` is a byte buffer (`Buffer` or `Uint8Array`) —
375
- * strings are NOT accepted. For already-encoded material where a
376
- * string would be ambiguous (ciphertext, signatures, raw key bytes).
377
- * @param {unknown} value
378
- * @param {string} name
379
- * @param {AssertOptions} [opts]
380
- */
381
- assertBytes(value, name, opts) {
382
- if (!Buffer.isBuffer(value) && !(value instanceof Uint8Array)) {
383
- fail(name, 'a Buffer or Uint8Array', opts);
384
- }
385
- },
386
-
387
- /**
388
- * Assert that `value` is either a string or a byte buffer (`Buffer`
389
- * or `Uint8Array`).
390
- * @param {unknown} value
391
- * @param {string} name
392
- * @param {AssertOptions} [opts]
393
- */
394
- assertBytesOrString(value, name, opts) {
395
- if (typeof value !== 'string' && !Buffer.isBuffer(value) && !(value instanceof Uint8Array)) {
396
- fail(name, 'a string or Buffer', opts);
397
- }
398
- },
399
-
400
- /**
401
- * Assert that `encoding` is one of the accepted output/input
402
- * encodings. Pass `allowBuffer: false` where a `Buffer` output makes
403
- * no sense (verifying a string signature, decoding a token payload).
404
- *
405
- * @param {unknown} encoding
406
- * @param {string} name
407
- * @param {AssertOptions & { allowBuffer?: boolean }} [opts]
408
- */
409
- assertEncoding(encoding, name, opts) {
410
- const allowBuffer = opts?.allowBuffer !== false;
411
- const valid = (typeof encoding === 'string' && ENCODINGS.has(encoding)) || (allowBuffer && encoding === 'buffer');
412
- if (!valid) {
413
- const list = allowBuffer ? "'hex', 'base64', 'base64url', or 'buffer'" : "'hex', 'base64', or 'base64url'";
414
- fail(name, list, opts);
415
- }
416
- },
417
- };
418
- }
419
-
420
- /**
421
- * One-line sugar over {@link bindAsserts} — the overwhelmingly common
422
- * pattern where a package binds every argument-shape failure to its
423
- * own error class + `INVALID_ARGUMENT` code. Emits the standard
424
- * `new ErrorClass(code, message, { cause })` shape, so the underlying
425
- * `BaseError` `{ cause, status, details }` options object is respected.
426
- *
427
- * // packages/<pkg>/src/internal/guards.js
428
- * import { defineGuards } from '@exortek/shared/asserts';
429
- * import { PasswordError, ErrorCode } from '../errors.js';
430
- *
431
- * export const { assertString, invalidArgument, parse } =
432
- * defineGuards(PasswordError, ErrorCode.INVALID_ARGUMENT);
433
- *
434
- * Reach for the raw {@link bindAsserts} when you need a different
435
- * mapping (an alt code, a decorated message) — it stays available.
436
- *
437
- * @param {new (code: string, message: string, options?: { cause?: unknown }) => Error} ErrorClass
438
- * @param {string} code
439
- */
440
- function defineGuards(ErrorClass, code) {
441
- return bindAsserts((message, extra) => new ErrorClass(code, message, extra));
442
- }
183
+ const makeInvalidArgument = wrap => (msg, extra) => wrap(msg, extra);
443
184
 
444
185
  /**
445
186
  * Shared base error class — the single error structure behind every
@@ -550,10 +291,15 @@ class ApiKeyError extends BaseError {
550
291
  */
551
292
 
552
293
 
553
- const { invalidArgument} = defineGuards(
554
- ApiKeyError,
555
- ErrorCode.INVALID_ARGUMENT,
556
- );
294
+ /**
295
+ * Bind every guard this package calls to ApiKeyError, so an argument-shape
296
+ * failure anywhere carries one class and one code to branch on.
297
+ *
298
+ * @type {import('@exortek/shared/asserts').WrapFn}
299
+ */
300
+ const wrap = (message, extra) => new ApiKeyError(ErrorCode.INVALID_ARGUMENT, message, extra);
301
+
302
+ const invalidArgument = makeInvalidArgument(wrap);
557
303
 
558
304
  /**
559
305
  * Scope match helpers.