Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changeset/shared-asserts-treeshake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@exortek/apikey': patch
'@exortek/challenge': patch
'@exortek/crypto': patch
'@exortek/jwe': patch
'@exortek/jwk': patch
'@exortek/jwks': patch
'@exortek/jws': patch
'@exortek/jwt': patch
'@exortek/magic-link': patch
'@exortek/opaque': patch
'@exortek/otp': patch
'@exortek/paseto': patch
'@exortek/password': patch
'@exortek/security': patch
'@exortek/session': patch
---

Smaller bundles — the internal argument-guard helpers are now tree-shakeable.

Each package bundles the guard helpers it uses. They were previously built as
one object holding all fourteen, which a bundler cannot take apart, so every
package shipped all of them regardless of how many it called. They are now
individually importable, and each package pulls in only what it uses.

No API change: the errors, codes and messages raised by argument validation are
identical. Published bundles shrink by roughly 7-18% depending on the package.
38 changes: 23 additions & 15 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,29 +179,37 @@ packages/<name>/

### Argument guards — bind once, import from local `internal/guards.js`

Every package binds `@exortek/shared/asserts` to its own typed error class
exactly once, in `src/internal/guards.js`, and every call site imports the
bound helpers from that local file — **never from `@exortek/shared/asserts`
directly**. This keeps `err instanceof <Package>Error` working for every
argument-shape failure, without every call site paying wrap boilerplate.
Every package binds the `@exortek/shared/asserts` factories it needs to its own
typed error class exactly once, in `src/internal/guards.js`, and every call site
imports the bound helpers from that local file — **never from
`@exortek/shared/asserts` directly**. This keeps `err instanceof <Package>Error`
working for every argument-shape failure, without every call site paying wrap
boilerplate.

```js
// packages/<pkg>/src/internal/guards.js — 5 lines
import { defineGuards } from '@exortek/shared/asserts';
// packages/<pkg>/src/internal/guards.js
import { makeAssertString, makeAssertPositiveInt, makeInvalidArgument } from '@exortek/shared/asserts';
import { CryptoError, ErrorCode } from '../errors.js';
export const { assertString, assertPositiveInt, invalidArgument, parse } =
defineGuards(CryptoError, ErrorCode.INVALID_ARGUMENT);

const wrap = (message, extra) => new CryptoError(ErrorCode.INVALID_ARGUMENT, message, extra);

export const assertString = makeAssertString(wrap);
export const assertPositiveInt = makeAssertPositiveInt(wrap);
export const invalidArgument = makeInvalidArgument(wrap);
```

Rules:

- Only re-export the assert names the package's own callsites use — the
destructure list is not "everything shared exports".
- **Import only the factories the package's own call sites use.** This is not
style — each `make…` import is what a bundler keys on, so an unused import
ships dead code into every subpath bundle. The predecessor of this API handed
back one object holding every guard, and `assertBoolean` reached fifteen
published packages without a single call site.
- `invalidArgument(msg, { cause })` for the `throw invalidArgument('…')` sites
that don't fit the `X must be Y` shape (canonicalisation errors,
cross-field constraint failures).
- `parse(schema, input, path)` is the bridge from
`@exortek/shared/validate` schemas — same bound error, one call.
that don't fit the `X must be Y` shape (canonicalisation errors, cross-field
constraint failures).
- `parse(schema, input, path)` is the bridge from `@exortek/shared/validate`
schemas — same bound error, one call.
- **Path-naming convention** for the assert `name` argument:
`<publicFn>[.options|.config][.<field>]` — e.g. `'createUser.name'`,
`'scrypt.options.r'`, `'pepper.wrap.password'`. Keep it short; the stack
Expand Down
18 changes: 13 additions & 5 deletions packages/apikey/src/internal/guards.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@
* shared module directly.
*/

import { defineGuards } from '@exortek/shared/asserts';
import { makeInvalidArgument, makeParse, makeAssertObject, makeAssertNonEmptyString } from '@exortek/shared/asserts';
import { ApiKeyError, ErrorCode } from '../errors.js';

export const { invalidArgument, parse, assertObject, assertNonEmptyString } = defineGuards(
ApiKeyError,
ErrorCode.INVALID_ARGUMENT,
);
/**
* Bind every guard this package calls to ApiKeyError, so an argument-shape
* failure anywhere carries one class and one code to branch on.
*
* @type {import('@exortek/shared/asserts').WrapFn}
*/
const wrap = (message, extra) => new ApiKeyError(ErrorCode.INVALID_ARGUMENT, message, extra);

export const invalidArgument = makeInvalidArgument(wrap);
export const parse = makeParse(wrap);
export const assertObject = makeAssertObject(wrap);
export const assertNonEmptyString = makeAssertNonEmptyString(wrap);
25 changes: 20 additions & 5 deletions packages/challenge/src/internal/guards.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,25 @@
* error class for the whole package.
*/

import { defineGuards } from '@exortek/shared/asserts';
import {
makeInvalidArgument,
makeParse,
makeAssertObject,
makeAssertNonEmptyString,
makeAssertBytes,
} from '@exortek/shared/asserts';
import { ChallengeError, ErrorCode } from '../errors.js';

export const { invalidArgument, parse, assertObject, assertNonEmptyString, assertBytes } = defineGuards(
ChallengeError,
ErrorCode.INVALID_ARGUMENT,
);
/**
* Bind every guard this package calls to ChallengeError, so an argument-shape
* failure anywhere carries one class and one code to branch on.
*
* @type {import('@exortek/shared/asserts').WrapFn}
*/
const wrap = (message, extra) => new ChallengeError(ErrorCode.INVALID_ARGUMENT, message, extra);

export const invalidArgument = makeInvalidArgument(wrap);
export const parse = makeParse(wrap);
export const assertObject = makeAssertObject(wrap);
export const assertNonEmptyString = makeAssertNonEmptyString(wrap);
export const assertBytes = makeAssertBytes(wrap);
49 changes: 34 additions & 15 deletions packages/crypto/src/internal/guards.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,42 @@
* error class for the whole package.
*/

import { defineGuards } from '@exortek/shared/asserts';
import {
makeInvalidArgument,
makeParse,
makeAssertNonNegativeInt,
makeAssertPositiveInt,
makeAssertUint48,
makeAssertString,
makeAssertNonEmptyString,
makeAssertObject,
makeAssertOptionalObject,
makeAssertBytes,
makeAssertBytesOrString,
makeAssertEncoding,
} from '@exortek/shared/asserts';
import { CryptoError, ErrorCode } from '../errors.js';

export const {
invalidArgument,
parse,
assertNonNegativeInt,
assertPositiveInt,
assertUint48,
assertString,
assertNonEmptyString,
assertObject,
assertOptionalObject,
assertBytes,
assertBytesOrString,
assertEncoding,
} = defineGuards(CryptoError, ErrorCode.INVALID_ARGUMENT);
/**
* Bind every guard this package calls to CryptoError, so an argument-shape
* failure anywhere carries one class and one code to branch on.
*
* @type {import('@exortek/shared/asserts').WrapFn}
*/
const wrap = (message, extra) => new CryptoError(ErrorCode.INVALID_ARGUMENT, message, extra);

export const invalidArgument = makeInvalidArgument(wrap);
export const parse = makeParse(wrap);
export const assertNonNegativeInt = makeAssertNonNegativeInt(wrap);
export const assertPositiveInt = makeAssertPositiveInt(wrap);
export const assertUint48 = makeAssertUint48(wrap);
export const assertString = makeAssertString(wrap);
export const assertNonEmptyString = makeAssertNonEmptyString(wrap);
export const assertObject = makeAssertObject(wrap);
export const assertOptionalObject = makeAssertOptionalObject(wrap);
export const assertBytes = makeAssertBytes(wrap);
export const assertBytesOrString = makeAssertBytesOrString(wrap);
export const assertEncoding = makeAssertEncoding(wrap);

/**
* Construct (not throw) a `CryptoError(INVALID_KEY, msg)` — for key-shape
Expand Down
17 changes: 12 additions & 5 deletions packages/jwe/src/internal/guards.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@
* error class for the whole package.
*/

import { defineGuards } from '@exortek/shared/asserts';
import { makeInvalidArgument, makeAssertObject, makeAssertNonEmptyString } from '@exortek/shared/asserts';
import { JweError, ErrorCode } from './errors.js';

export const { invalidArgument, assertObject, assertNonEmptyString } = defineGuards(
JweError,
ErrorCode.INVALID_ARGUMENT,
);
/**
* Bind every guard this package calls to JweError, so an argument-shape
* failure anywhere carries one class and one code to branch on.
*
* @type {import('@exortek/shared/asserts').WrapFn}
*/
const wrap = (message, extra) => new JweError(ErrorCode.INVALID_ARGUMENT, message, extra);

export const invalidArgument = makeInvalidArgument(wrap);
export const assertObject = makeAssertObject(wrap);
export const assertNonEmptyString = makeAssertNonEmptyString(wrap);
14 changes: 12 additions & 2 deletions packages/jwk/src/internal/guards.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,17 @@
* error class for the whole package.
*/

import { defineGuards } from '@exortek/shared/asserts';
import { makeInvalidArgument, makeParse, makeAssertObject } from '@exortek/shared/asserts';
import { JwkError, ErrorCode } from './errors.js';

export const { invalidArgument, parse, assertObject } = defineGuards(JwkError, ErrorCode.INVALID_ARGUMENT);
/**
* Bind every guard this package calls to JwkError, so an argument-shape
* failure anywhere carries one class and one code to branch on.
*
* @type {import('@exortek/shared/asserts').WrapFn}
*/
const wrap = (message, extra) => new JwkError(ErrorCode.INVALID_ARGUMENT, message, extra);

export const invalidArgument = makeInvalidArgument(wrap);
export const parse = makeParse(wrap);
export const assertObject = makeAssertObject(wrap);
17 changes: 12 additions & 5 deletions packages/jwks/src/internal/guards.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { defineGuards } from '@exortek/shared/asserts';
import { makeAssertNonEmptyString, makeAssertObject, makeInvalidArgument } from '@exortek/shared/asserts';
import { JwksError, ErrorCode } from '../errors.js';

export const { assertNonEmptyString, assertObject, invalidArgument } = defineGuards(
JwksError,
ErrorCode.INVALID_ARGUMENT,
);
/**
* Bind every guard this package calls to JwksError, so an argument-shape
* failure anywhere carries one class and one code to branch on.
*
* @type {import('@exortek/shared/asserts').WrapFn}
*/
const wrap = (message, extra) => new JwksError(ErrorCode.INVALID_ARGUMENT, message, extra);

export const assertNonEmptyString = makeAssertNonEmptyString(wrap);
export const assertObject = makeAssertObject(wrap);
export const invalidArgument = makeInvalidArgument(wrap);
17 changes: 12 additions & 5 deletions packages/jws/src/internal/guards.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@
* error class for the whole package.
*/

import { defineGuards } from '@exortek/shared/asserts';
import { makeInvalidArgument, makeAssertObject, makeAssertNonEmptyString } from '@exortek/shared/asserts';
import { JwsError, ErrorCode } from './errors.js';

export const { invalidArgument, assertObject, assertNonEmptyString } = defineGuards(
JwsError,
ErrorCode.INVALID_ARGUMENT,
);
/**
* Bind every guard this package calls to JwsError, so an argument-shape
* failure anywhere carries one class and one code to branch on.
*
* @type {import('@exortek/shared/asserts').WrapFn}
*/
const wrap = (message, extra) => new JwsError(ErrorCode.INVALID_ARGUMENT, message, extra);

export const invalidArgument = makeInvalidArgument(wrap);
export const assertObject = makeAssertObject(wrap);
export const assertNonEmptyString = makeAssertNonEmptyString(wrap);
25 changes: 20 additions & 5 deletions packages/jwt/src/internal/guards.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,25 @@
* error class for the whole package.
*/

import { defineGuards } from '@exortek/shared/asserts';
import {
makeInvalidArgument,
makeAssertNonEmptyString,
makeAssertObject,
makeAssertPositiveInt,
makeAssertString,
} from '@exortek/shared/asserts';
import { JwtError, ErrorCode } from './errors.js';

export const { invalidArgument, assertNonEmptyString, assertObject, assertPositiveInt, assertString } = defineGuards(
JwtError,
ErrorCode.INVALID_ARGUMENT,
);
/**
* Bind every guard this package calls to JwtError, so an argument-shape
* failure anywhere carries one class and one code to branch on.
*
* @type {import('@exortek/shared/asserts').WrapFn}
*/
const wrap = (message, extra) => new JwtError(ErrorCode.INVALID_ARGUMENT, message, extra);

export const invalidArgument = makeInvalidArgument(wrap);
export const assertNonEmptyString = makeAssertNonEmptyString(wrap);
export const assertObject = makeAssertObject(wrap);
export const assertPositiveInt = makeAssertPositiveInt(wrap);
export const assertString = makeAssertString(wrap);
18 changes: 13 additions & 5 deletions packages/magic-link/src/internal/guards.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@
* of `@exortek/shared/asserts`.
*/

import { defineGuards } from '@exortek/shared/asserts';
import { makeInvalidArgument, makeParse, makeAssertObject, makeAssertNonEmptyString } from '@exortek/shared/asserts';
import { MagicLinkError, ErrorCode } from '../errors.js';

export const { invalidArgument, parse, assertObject, assertNonEmptyString } = defineGuards(
MagicLinkError,
ErrorCode.INVALID_ARGUMENT,
);
/**
* Bind every guard this package calls to MagicLinkError, so an argument-shape
* failure anywhere carries one class and one code to branch on.
*
* @type {import('@exortek/shared/asserts').WrapFn}
*/
const wrap = (message, extra) => new MagicLinkError(ErrorCode.INVALID_ARGUMENT, message, extra);

export const invalidArgument = makeInvalidArgument(wrap);
export const parse = makeParse(wrap);
export const assertObject = makeAssertObject(wrap);
export const assertNonEmptyString = makeAssertNonEmptyString(wrap);
23 changes: 18 additions & 5 deletions packages/opaque/src/internal/guards.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,23 @@
* error class for the whole package.
*/

import { defineGuards } from '@exortek/shared/asserts';
import {
makeInvalidArgument,
makeAssertObject,
makeAssertNonEmptyString,
makeAssertPositiveInt,
} from '@exortek/shared/asserts';
import { OpaqueError, ErrorCode } from '../errors.js';

export const { invalidArgument, assertObject, assertNonEmptyString, assertPositiveInt } = defineGuards(
OpaqueError,
ErrorCode.INVALID_ARGUMENT,
);
/**
* Bind every guard this package calls to OpaqueError, so an argument-shape
* failure anywhere carries one class and one code to branch on.
*
* @type {import('@exortek/shared/asserts').WrapFn}
*/
const wrap = (message, extra) => new OpaqueError(ErrorCode.INVALID_ARGUMENT, message, extra);

export const invalidArgument = makeInvalidArgument(wrap);
export const assertObject = makeAssertObject(wrap);
export const assertNonEmptyString = makeAssertNonEmptyString(wrap);
export const assertPositiveInt = makeAssertPositiveInt(wrap);
18 changes: 13 additions & 5 deletions packages/otp/src/internal/guards.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@
* error class for the whole package.
*/

import { defineGuards } from '@exortek/shared/asserts';
import { makeInvalidArgument, makeParse, makeAssertObject, makeAssertNonEmptyString } from '@exortek/shared/asserts';
import { OtpError, ErrorCode } from './errors.js';

export const { invalidArgument, parse, assertObject, assertNonEmptyString } = defineGuards(
OtpError,
ErrorCode.INVALID_ARGUMENT,
);
/**
* Bind every guard this package calls to OtpError, so an argument-shape
* failure anywhere carries one class and one code to branch on.
*
* @type {import('@exortek/shared/asserts').WrapFn}
*/
const wrap = (message, extra) => new OtpError(ErrorCode.INVALID_ARGUMENT, message, extra);

export const invalidArgument = makeInvalidArgument(wrap);
export const parse = makeParse(wrap);
export const assertObject = makeAssertObject(wrap);
export const assertNonEmptyString = makeAssertNonEmptyString(wrap);
Loading