16 KiB
OpenSpec Change: complete-rng-unification
- Change ID: complete-rng-unification
- Package:
@suaveplan/types(canonical) + every consumer that locally redefines anRng-shaped type - Design: ./design.md
- Spec deltas:
- ./specs/types/spec.md
- ./specs/testing/spec.md
- ./specs/crypto-core/spec.md
- ./specs/crypto/spec.md
- ./specs/mime/spec.md
- Status: PROPOSED
- Date: 2026-05-17
- Supersedes:
_superseded-narrow-deferrals-2026-05-17/unify-rng-across-packages— the prior draft covered only three packages and did not exhaustively auditRngLike/RandomSource/ByteSourcecollisions across the monorepo.
Why
The 2026-04-27 types-contract-tightening change widened the canonical Rng
interface in @suaveplan/types (src/rng.ts) with two OPTIONAL members
(nextBytes, nextIntInRange) and deferred migrating the packages that
still declared Rng-shaped types locally. A first attempt
(unify-rng-across-packages, superseded on 2026-05-17) only covered three
packages and silently glossed over crypto-core's genuinely incompatible
type Rng = ByteSource aliasing. The naming collision (crypto-core's Rng
is a function (n: number) => Uint8Array; canonical Rng is an object with
nextUint32/nextFloat) means any consumer that imported Rng from
@suaveplan/crypto-core was getting a fundamentally different contract than
a consumer importing Rng from @suaveplan/types.
This change closes the loop exhaustively: every genesis package surveyed
in the repo-wide audit below is migrated to consume the canonical Rng (or
to drop a misleading Rng alias when the local shape is structurally a
distinct concept like a callable byte-source). Type-only; no runtime
behaviour change.
Audit (exhaustive — every match from the mandated grep)
Repo-wide grep:
grep -rln "interface Rng\b\|type Rng\s*=\|class Rng\b\|RngLike\b\|RandomSource\b\|ByteSource\b" \
packages/ --include="*.ts" --exclude-dir=dist --exclude-dir=node_modules
Categorised — see design.md §"Per-package categorisation" for the full
table, including the reasoning for every "no action / structurally
compatible / structurally incompatible" verdict:
| # | Package | File | Local symbol | Verdict | Action |
|---|---|---|---|---|---|
| 1 | @suaveplan/types |
src/rng.ts |
interface Rng |
CANONICAL | None — source of truth. |
| 2 | @suaveplan/testing |
src/context/context.ts |
interface Rng (subset of canonical) |
STRUCTURALLY COMPATIBLE | Replace local interface with re-export of canonical; existing cryptoRng / seededRng keep working. |
| 3 | @suaveplan/crypto-core |
src/primitives/random-bytes/random-bytes.ts |
type Rng = ByteSource (function, not object) |
STRUCTURALLY INCOMPATIBLE | Remove the misleading Rng alias and defaultRng (which is just defaultByteSource under a wrong name); keep ByteSource and defaultByteSource under their real names; add a back-compat re-export of the canonical Rng from @suaveplan/types. |
| 4 | @suaveplan/crypto-core |
src/index.ts, src/primitives/index.ts, src/primitives/random-bytes/index.ts |
re-exports Rng / defaultRng |
DERIVED from #3 | Update barrels — Rng now forwards canonical; defaultRng is removed (a defaultRng() factory belongs in a future @suaveplan/random-adjacent helper, not in crypto-core's primitives surface). |
| 5 | @suaveplan/crypto |
src/index.ts |
re-exports Rng / defaultRng from crypto-core |
DERIVED from #4 | Update barrel to forward the canonical Rng; drop the defaultRng re-export. |
| 6 | @suaveplan/crypto-utils |
src/random/random-bytes/random-bytes.ts |
type ByteSource = (n: number) => Uint8Array |
DISTINCT CONCEPT | No Rng alias declared (clean); keep ByteSource as the package's own callable byte-producer contract. Document in spec that ByteSource is intentionally NOT the canonical Rng. |
| 7 | @suaveplan/mime |
src/detection/from-stream.ts |
type ByteSource = ReadableStream<Uint8Array> | AsyncIterable<Uint8Array> |
DISTINCT CONCEPT, NAME COLLISION | The local type ByteSource shadows @suaveplan/crypto-utils's ByteSource semantically (stream vs callable) and is file-scope only. Rename to StreamByteSource to remove the collision; not exported at package root. |
| 8 | @suaveplan/crypto-rsa |
src/encrypt/default-rng.ts |
function defaultRng(): Rng (imports canonical) |
ALREADY COMPLIANT | No change — already uses import type { Rng } from "@suaveplan/types". |
| 9 | @suaveplan/events-schema |
src/core/attach-event-schemas/attach-event-schemas.types.ts |
interface SchemaRng |
DISTINCT CONCEPT (schema registry namespace, not RNG) | No change — SchemaRng is a schema registry abbreviation, not a random-number generator. Documented in design.md to prevent future agent confusion. |
| 10 | @suaveplan/pseudo-random |
src/types/seeded-rng.types.ts |
interface SeededRng extends Rng |
STRUCTURAL EXTENSION (subtype of canonical) | No change — already extends canonical correctly per the rule subtype that extends Rng. |
Consumers of the local Rng re-exports that need verification post-migration:
@suaveplan/crypto-cli— importsByteSourcefrom@suaveplan/crypto(notRng); unaffected.@suaveplan/vault-sync— importsByteSourcefrom@suaveplan/crypto-utils; unaffected.- Every other
@suaveplan/crypto-coreconsumer usingByteSource(sealed-box, sign, agree, ed25519, ecdsa-p256, x25519, aead/sign/agree-engine.types, keychain, keypair/generate, vault, mnemonic, identity): unaffected —ByteSourceis preserved verbatim.
What changes
For each affected genesis package, the source files that currently declare a
local Rng-shaped type are migrated to:
- Compatible subset (
testing) — replace the localinterface Rng { ... }withimport type { Rng } from "@suaveplan/types"; export type { Rng };plus a prose comment marking the local re-export as a backwards-compatibility alias. - Incompatible alias (
crypto-core) — remove the misleadingtype Rng = ByteSource(anddefaultRng = defaultByteSource) entirely; theRngname now re-exports canonical from@suaveplan/types; the callable-byte-producer concept keeps theByteSourcename (which it already had). - Distinct concept under colliding name (
mime) — rename the file-scopeByteSourcetoStreamByteSource(it has zero external consumers — never exported fromfrom-stream.ts). - Already compliant or structurally extending (
crypto-rsa,pseudo-random) — no edits; documented in design as the reference pattern. - Distinct namespace (
events-schema/SchemaRng) — no edits; documented in design.
Add @suaveplan/types as a workspace:^ dependency where missing
(specifically @suaveplan/testing, which currently has zero
@suaveplan/* runtime deps but is permitted to depend on
@suaveplan/types for types-only consumption per CLAUDE.md rule 8 — the
byte-size guard verifies @suaveplan/types emits zero runtime).
Per-affected-package spec deltas document the canonical contract and the
back-compat forwarder requirement; the types capability spec delta is
the normative single-source-of-truth clause.
Impact
Affected dependents
Run bunx turbo ls --filter=...@suaveplan/types to enumerate the (large)
transitive dependent set. The packages directly affected by THIS change
(i.e. ones whose source files this change edits or whose tests must run
green pre- and post-migration) are:
@suaveplan/types— [x] tests run / [x] docs reviewed / [ ] no action needed (canonical spec clause added)@suaveplan/testing— [x] tests run / [x] docs reviewed / [ ] no action needed (interface → re-export)@suaveplan/crypto-core— [x] tests run / [x] docs reviewed / [ ] no action needed (drop alias + dropdefaultRng; preserveByteSource)@suaveplan/crypto— [x] tests run / [x] docs reviewed / [ ] no action needed (barrel forward update; dropdefaultRngre-export)@suaveplan/mime— [x] tests run / [x] docs reviewed / [ ] no action needed (rename file-scopeByteSource→StreamByteSource)@suaveplan/crypto-rsa— [ ] tests run / [ ] docs reviewed / [x] no action needed (already canonical-compliant)@suaveplan/pseudo-random— [ ] tests run / [ ] docs reviewed / [x] no action needed (SeededRng extends Rngalready)@suaveplan/events-schema— [ ] tests run / [ ] docs reviewed / [x] no action needed (SchemaRngis a schema-registry concept, not an RNG)@suaveplan/crypto-utils— [ ] tests run / [ ] docs reviewed / [x] no action needed (declares onlyByteSource, noRngcollision)
Foundation-dependent sweep for types (per §13 step 10 + §14a):
bunx turbo test --filter=...@suaveplan/types MUST be green before
archival. Foundation-dependent sweep for testing
(bunx turbo test --filter=...@suaveplan/testing) MUST also be green —
every test in the monorepo flows through @suaveplan/testing/runner per
CLAUDE.md rule 11, so any structural break in Rng here cascades.
Source mapping (local → canonical)
| Package | Local file | Local symbol | Local shape | Action |
|---|---|---|---|---|
@suaveplan/testing |
src/context/context.ts:45 |
interface Rng { nextUint32; nextFloat } |
{ nextUint32(): number; nextFloat(): number } |
Replace with export type { Rng } from "@suaveplan/types";. cryptoRng/seededRng still typecheck (canonical adds only OPTIONAL members). |
@suaveplan/crypto-core |
src/primitives/random-bytes/random-bytes.ts:12 |
type Rng = ByteSource |
(n: number) => Uint8Array (function) |
Remove the alias. Remove defaultRng = defaultByteSource (line 36). Add export type { Rng } from "@suaveplan/types"; with prose comment marking it as a back-compat re-export. Keep ByteSource, defaultByteSource, randomBytes, RandomBytesOptions. Update RandomBytesOptions to drop the rng alias field (currently readonly rng?: ByteSource; — this is a legacy alias of byteSource). |
@suaveplan/crypto-core |
src/primitives/random-bytes/index.ts |
barrel | re-exports Rng, defaultRng |
Update — Rng now forwards canonical; drop defaultRng. |
@suaveplan/crypto-core |
src/primitives/index.ts |
barrel | re-exports Rng, defaultRng |
Update — same as above. |
@suaveplan/crypto-core |
src/index.ts |
barrel | re-exports Rng, defaultRng |
Update — same as above. |
@suaveplan/crypto |
src/index.ts |
barrel | re-exports Rng, defaultRng from crypto-core |
Update — forward canonical Rng; drop defaultRng. |
@suaveplan/mime |
src/detection/from-stream.ts:19 |
type ByteSource = ReadableStream<Uint8Array> | AsyncIterable<Uint8Array> |
union of stream types (file-scope) | Rename to StreamByteSource to eliminate the cross-package name collision; update the single internal reference at line 30. |
Verification
- Verified by:
bunx turbo typecheck test --filter=@suaveplan/types - Verified by:
bunx turbo typecheck test --filter=@suaveplan/testing - Verified by:
bunx turbo typecheck test --filter=@suaveplan/crypto-core - Verified by:
bunx turbo typecheck test --filter=@suaveplan/crypto - Verified by:
bunx turbo typecheck test --filter=@suaveplan/mime - Verified by:
bunx turbo test --filter=...@suaveplan/types(foundation dependent sweep) - Verified by:
bunx turbo test --filter=...@suaveplan/testing(every test consumer in the monorepo) - Verified by:
bunx turbo test --filter=...@suaveplan/crypto-core(every crypto-core consumer:crypto,crypto-rsa,crypto-vault,crypto-keys,vault,vault-sync,webauthn, etc.) - Verified by: repo-wide grep
grep -rln "interface Rng\b\|type Rng\s*=\|class Rng\b\|RngLike\b\|RandomSource\b" packages/ --include="*.ts" --exclude-dir=dist --exclude-dir=node_modulesreturns exactly:packages/core/types/src/rng.ts(canonical declaration)packages/random/pseudo-random/src/types/seeded-rng.types.ts(legitimateSeededRng extends Rngsubtype)- back-compat re-export forwarder lines (
export type { Rng } from "@suaveplan/types";) incrypto-core/random-bytes,crypto-core/index,crypto/index,testing/context— never an independent interface body
Out of scope
- Removing the back-compat re-exports. A follow-up change
remove-deprecated-rng-aliases(opened at least one release cycle after this change archives) will delete theRngre-exports from@suaveplan/crypto-core/random-bytes,@suaveplan/crypto-core/index,@suaveplan/crypto/index, and@suaveplan/testing/context, plus thedefaultRngre-export already dropped here. This change ships with one cycle of grace. - Widening the canonical
Rng. If a consumer needs additional members (e.g.nextBigInt, streaming bytes), open a separatetypes-contract-tightening-style change against@suaveplan/types. - Renaming
ByteSource.crypto-utilsandcrypto-coreboth keepByteSourceunder that name; the type-class distinction (callable byte producer vs uniform numeric RNG) is the right separation.mime's file-scopeByteSourceis renamed only because it shadows the cross-package name without semantic overlap. - Replacing
crypto-core'sRandomBytesOptions.rngfield with a deprecation cycle. Therngfield onRandomBytesOptionsis a legacy alias forbyteSource(currentlyoptions.byteSource ?? options.rng ?? defaultByteSource). It has been a misleading shadow of canonicalRngsince it accepts aByteSourcecallable. This change removes it inline (no deprecation window) because keeping it would require shipping a field that takes aByteSourceunder the namerng, contradicting the whole purpose of the unification. Consumers using{ rng: ... }instead of{ byteSource: ... }are flagged in §Migration. - Runtime behaviour changes. This change is type-only; the test suite must remain green pre- and post-migration with zero assertion edits.
Migration
-
Old: consumers importing
Rngfrom@suaveplan/crypto-core,@suaveplan/crypto, or@suaveplan/testing— particularly any consumer that was relying oncrypto-core'sRngbeing callable asrng(n: number) => Uint8Array. -
New: import
Rngfrom@suaveplan/types. Callable byte producers useByteSourcefrom@suaveplan/crypto-coreor@suaveplan/crypto-utils. -
Search command:
grep -rln "from ['\"]@suaveplan/\(crypto-core\|crypto\|testing\)['\"]" packages/ \ | xargs grep -l "Rng\b" 2>/dev/null grep -rn "RandomBytesOptions\|\.rng:\|, rng:" packages/ --include="*.ts" \ --exclude-dir=dist --exclude-dir=node_modules \ | grep -i "byteSource\|crypto-core\|crypto-utils" -
Recommended replacement:
// For an object Rng (nextUint32 / nextFloat / optional nextBytes / nextIntInRange): import type { Rng } from "@suaveplan/types"; // For a callable byte producer (n: number) => Uint8Array: import type { ByteSource } from "@suaveplan/crypto-core"; // or @suaveplan/crypto-utils -
RandomBytesOptions.rngremoval: any callsite passingrandomBytes(n, { rng: ... })to@suaveplan/crypto-coreor@suaveplan/crypto-utilsMUST switch to{ byteSource: ... }. The repo-wide audit (seedesign.md) confirms zero current callsites use this field — the rename is the migration. -
defaultRngremoval:@suaveplan/crypto-coreand@suaveplan/cryptono longer re-exportdefaultRng. UsedefaultByteSource(same value, correct name) instead. Audit confirms zero internal callsites use thedefaultRngre-export today (it was a deprecation-shaped alias).