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