Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,691 @@
## Purpose
`@suaveplan/types` is the types-only foundation of the genesis monorepo. It owns the canonical shape of every cross-package interface (`Clock`, `IdGenerator`, `Rng`, `EventBusLike`, `LoggerLike`, `StorageLike`, `DatabaseLike`, plus the cross-package crypto contract `Authority` / `DomainTag` / `KeySlotPublic` / `AuthorityVerifier` / `AeadOps`) so any package can import and accept them without dragging in concrete implementations. The package emits zero runtime bytes — every `.js` file is a stub — making it safe to depend on from any layer, including the testing-infrastructure packages and any package that would otherwise risk a circular dependency.
## Requirements
### Requirement: Zero runtime code
The `@suaveplan/types` package MUST emit declaration files only. Every built `.js` file MUST be a no-op module stub — the full package's runtime surface is empty. Adding new type aliases, interfaces, or optional members does NOT change runtime emission and MUST keep every `dist/*.js` within the stub-size budget.
**Verified by:** `tests/zero-runtime.test.ts` (per-file ≤ 200-byte budget walk).
#### Scenario: No .js file exceeds the stub-size budget
- **WHEN** the package is built
- **THEN** every `.js` file under `dist/` is ≤ 200 bytes AND contains no function declarations, class declarations, or non-export runtime statements
#### Scenario: No dependencies
- **WHEN** `package.json` is read
- **THEN** `dependencies`, `peerDependencies`, and `optionalDependencies` are all empty or absent
### Requirement: Canonical shared types
The package SHALL export `Clock`, `MonotonicClock`, `IdGenerator`, `Rng`, `MessageResolver`, `EventBusLike`, `LoggerLike`, `StorageLike`, `DatabaseLike`, `CancellableOptions`, `TraceableOptions`, and `DeterministicOptions` as named type/interface exports.
**Verified by:** `tests/spec-conformance.test.ts` (every canonical type asserted via `expectTypeOf`).
#### Scenario: Consumer uses `import type`
- **WHEN** a consumer writes `import type { Clock } from "@suaveplan/types"`
- **THEN** `tsc` accepts the import AND the built bundle contains zero bytes from `@suaveplan/types`
### Requirement: Testing packages may depend on types
The three testing-infrastructure packages (`@suaveplan/testing`, `@suaveplan/testing-dom`, `@suaveplan/testing-e2e`) SHALL be permitted to add `@suaveplan/types` as a `devDependency` — this is the one exception to the otherwise-strict no-`@suaveplan/*`-dep rule — because `@suaveplan/types` emits no runtime and therefore cannot create a circular test/subject dependency. The same carve-out SHALL also permit `@suaveplan/error` to declare `@suaveplan/types` as a runtime dependency for the `MessageResolver` type.
**Verified by:** `tests/spec-conformance.test.ts` (CLAUDE.md rule-8 carve-out check).
#### Scenario: Rule-8 carve-out documented
- **WHEN** `CLAUDE.md` rule 8 is read
- **THEN** it explicitly states that `@suaveplan/types` is the sole exception to the no-workspace-dep rule
### Requirement: Test coverage at 100%
`vitest.config.ts` MUST enforce `statements: 100`, `branches: 100`, `functions: 100`, `lines: 100` — trivial for a package with no runtime. The guard test in `tests/zero-runtime.test.ts` is the one substantive test.
**Verified by:** `vitest.config.ts` thresholds and `bunx turbo test:coverage --filter=@suaveplan/types`.
#### Scenario: Coverage threshold enforced
- **WHEN** `bunx turbo test:coverage --filter=@suaveplan/types` runs
- **THEN** it exits zero with 100/100/100/100 (or zero runtime covered at 100%)
### Requirement: MonotonicClock is high-precision
The package SHALL export `MonotonicClock = () => bigint`, distinct from `Clock`. The contract: a `MonotonicClock` returns nanoseconds since an arbitrary monotonic origin (process start, performance counter, etc.). Successive calls within a single process MUST be strictly non-decreasing. The result is `bigint` so it can express the full 2^64-nanosecond range without precision loss. `MonotonicClock` is NOT comparable across processes or machines and MUST NOT be used for human-readable timestamps or wire-format dates.
**Verified by:** `src/clock.ts` (type alias) and `tests/spec-conformance.test.ts` (`expectTypeOf<MonotonicClock>().toBeFunction()`).
#### Scenario: MonotonicClock returns bigint
- **WHEN** a consumer declares `const tick: MonotonicClock = () => 1n`
- **THEN** `tsc` accepts the assignment AND the return type is `bigint` (not `number`)
#### Scenario: Clock and MonotonicClock are distinct
- **WHEN** a consumer assigns `const c: Clock = () => 1n` (bigint into Clock)
- **THEN** `tsc` rejects (Clock is `() => number`)
### Requirement: Rng has optional bounded operations
The `Rng` interface SHALL declare `nextBytes(n: number): Uint8Array` and `nextIntInRange(min: number, maxExclusive: number): number` as OPTIONAL members. Implementations SHOULD provide them when efficient (CSPRNGs typically expose `getRandomValues` directly; deterministic seeded RNGs can derive both from `nextUint32`). Consumers needing them MUST narrow on presence (`if (rng.nextBytes)`). `nextIntInRange` is contractually expected to use unbiased rejection sampling, not the `% range` shortcut.
**Verified by:** `src/rng.ts` (interface) and `tests/spec-conformance.test.ts` (optional-member presence + minimal-impl assignability).
#### Scenario: nextBytes is optional
- **WHEN** a consumer declares `const r: Rng = { nextUint32: () => 0, nextFloat: () => 0 }`
- **THEN** `tsc` accepts (no `nextBytes` provided)
#### Scenario: nextIntInRange signature
- **WHEN** an implementation provides `nextIntInRange(min, maxExclusive): number`
- **THEN** `tsc` accepts AND `nextIntInRange(0, 10)` returns `number`
### Requirement: StorageLike has optional batch operations
The `StorageLike` interface SHALL declare `getMany(keys)`, `setMany(entries)`, and `deleteMany(keys)` as OPTIONAL methods. Adapters with native multi-key support SHOULD implement them; consumers without them fall back to N parallel `get`/`set`/`delete` calls. Atomicity is NOT guaranteed by the contract — adapters that provide it MUST document it explicitly.
**Verified by:** `src/storage-like.ts` (interface) and `tests/spec-conformance.test.ts`.
#### Scenario: Batch operations are optional
- **WHEN** a minimal `StorageLike` impl provides only `get`/`set`/`delete`/`exists`/`list`/`clear`
- **THEN** `tsc` accepts (no batch methods required)
#### Scenario: getMany returns ordered map
- **WHEN** an adapter implements `getMany(keys)`
- **THEN** the return type is `Promise<ReadonlyMap<string, Uint8Array | undefined>>`
### Requirement: LoggerLike supports child loggers
The `LoggerLike` interface SHALL declare `child(bindings: Readonly<Record<string, unknown>>): LoggerLike` as an OPTIONAL method. Implementations that propagate context (structured loggers, request-scoped trace context) SHOULD provide it. No-op fallbacks MAY omit it.
**Verified by:** `src/logger-like.ts` (interface) and `tests/spec-conformance.test.ts`.
#### Scenario: child is optional
- **WHEN** a no-op `LoggerLike` impl provides only `debug`/`info`/`warn`/`error`
- **THEN** `tsc` accepts (no `child` required)
#### Scenario: child returns a LoggerLike
- **WHEN** a structured impl provides `child({ requestId: "abc" })`
- **THEN** the return type is `LoggerLike` (composable; the result has the same surface)
### Requirement: DatabaseLike requires transactional rollback
Implementations claiming `DatabaseLike` MUST provide genuine commit-on-resolve / rollback-on-throw semantics in `transaction(fn)`. Partial mutations from a thrown `fn` MUST NOT be visible to subsequent queries. Drivers that cannot guarantee rollback MUST expose a narrower interface (e.g. `QueryableLike` covering `query` + `execute` only) instead of claiming `DatabaseLike`. The rollback contract is non-negotiable.
**Verified by:** `src/database-like.ts` (doc-comment) and downstream adapter conformance tests in `@suaveplan/db-sqlite` / equivalent.
#### Scenario: Driver without rollback cannot claim DatabaseLike
- **WHEN** a SurrealDB driver running in a non-transactional mode is wrapped
- **THEN** the wrapper MUST NOT claim to satisfy `DatabaseLike` (no `transaction` impl, narrower interface used instead)
#### Scenario: Transaction body throw causes rollback
- **WHEN** `db.transaction(async (tx) => { await tx.execute("INSERT ..."); throw new Error("x"); })` runs against a conformant impl
- **THEN** the inserted row is NOT visible to a subsequent `db.query("SELECT ...")` call
---
> The requirements below were folded in from the archived change `enterprise-types-cross-cutting` (2026-05-02). They add the cross-cutting interfaces consumed by `@suaveplan/drive`, `@suaveplan/drive-webrtc`, `@suaveplan/network-webrtc`, `@suaveplan/network-libp2p`, and any future P2P plugin so downstream packages do not redeclare local copies and so that no plugin needs to import drive or network directly.
### Requirement: ChunkSource interface
The package SHALL export a `ChunkSource` interface with `id`, `type`, `priority`, and optional `health` fields. It SHALL declare `isAvailable(): Promise<boolean>`, `getChunk(chunkId: string, signal?: AbortSignal): Promise<Uint8Array | null>`, `getChunks(chunkIds: string[], signal?: AbortSignal): Promise<(Uint8Array | null)[]>`, `hasChunk(chunkId: string): Promise<boolean>`, and optional `updateHealth` and `getMetrics` methods.
**Verified by:** `tests/cross-package-compat.test.ts`::"mock P2P source implements ChunkSource without runtime deps" AND `tests/spec-conformance.test.ts`::"ChunkSource.getChunk accepts optional AbortSignal"
#### Scenario: ChunkSource can be implemented by a P2P source
- **WHEN** a class implements `ChunkSource` with all required fields and methods
- **THEN** it compiles without importing any runtime package
#### Scenario: getChunk accepts AbortSignal
- **WHEN** `getChunk` is called with a pre-aborted `AbortSignal`
- **THEN** the implementing source MAY reject immediately or return `null`
### Requirement: ChunkSourceHealth union
The package SHALL export `ChunkSourceHealth` as the union `'healthy' | 'degraded' | 'failed' | 'unknown'`.
**Verified by:** `tests/spec-conformance.test.ts`::"ChunkSourceHealth exhaustiveness covers all four states"
#### Scenario: Exhaustiveness check
- **WHEN** a `switch` statement covers all four literals
- **THEN** TypeScript reports no exhaustiveness errors
### Requirement: ChunkSourceMetrics interface
The package SHALL export `ChunkSourceMetrics` with `avgResponseTime: number`, `successRate: number`, `totalRequests: number`, `failedRequests: number`, and `lastAccess: Date`.
#### Scenario: All five fields satisfy the interface
- **WHEN** an object literal supplies `avgResponseTime`, `successRate`, `totalRequests`, `failedRequests`, and `lastAccess`
- **THEN** the object is assignable to `ChunkSourceMetrics` and TypeScript narrows the field types correctly
**Verified by:** `tests/spec-conformance.test.ts`::"ChunkSourceMetrics has all five required fields"
### Requirement: ChunkRef interface
The package SHALL export `ChunkRef` with `fileId: string`, `version: number`, and `checksum: string`.
#### Scenario: Object with fileId / version / checksum compiles
- **WHEN** a literal `{ fileId, version, checksum }` is asserted as `ChunkRef`
- **THEN** the assertion compiles and TypeScript narrows each field to the documented primitive type
**Verified by:** `tests/spec-conformance.test.ts`::"ChunkRef has fileId, version, and checksum"
### Requirement: Plugin lifecycle interface
The package SHALL export `Plugin<TConfig>` with `name`, `version`, `type: PluginType`, `initialize(config)`, and `destroy()`.
**Verified by:** `tests/spec-conformance.test.ts`::"Plugin generic config type flows through initialize"
#### Scenario: Generic config type inference
- **WHEN** `Plugin<MyConfig>` is implemented
- **THEN** `initialize` receives `MyConfig` and TypeScript narrows correctly
### Requirement: PluginType union
The package SHALL export `PluginType` as `'storage' | 'transform' | 'hook' | 'middleware' | 'kms' | 'dedup' | 'cow'`.
#### Scenario: All seven categories are exhaustive
- **WHEN** a `switch (kind: PluginType)` enumerates every variant
- **THEN** TypeScript's exhaustiveness checker reports no missing case AND a synthetic eighth literal fails type-narrowing
**Verified by:** `tests/spec-conformance.test.ts`::"PluginType exhaustiveness covers all seven categories"
### Requirement: PluginRegistration interface
The package SHALL export `PluginRegistration` with `plugin: Plugin`, `priority: number`, and `enabled: boolean`.
#### Scenario: PluginRegistration references Plugin
- **WHEN** a `PluginRegistration` literal supplies a `Plugin` instance, a `priority` number, and an `enabled` boolean
- **THEN** the literal compiles and the `plugin` field's `name` / `version` / `type` are reachable via `.plugin`
**Verified by:** `tests/spec-conformance.test.ts`::"PluginRegistration references Plugin interface"
### Requirement: DrivePlugin hook-based interface
The package SHALL export `DrivePlugin` with `name: string`, `hooks: Record<string, PluginHook>`, and optional `priority: number`.
#### Scenario: Mock drive plugin compiles without runtime deps
- **WHEN** a consumer in `tests/cross-package-compat.test.ts` declares a `DrivePlugin`-shaped object literal with `name`, `hooks: {}`, and no `@suaveplan/*` runtime imports
- **THEN** the literal is assignable to `DrivePlugin` AND the test file builds with zero `@suaveplan/*` runtime dependencies
**Verified by:** `tests/cross-package-compat.test.ts`::"mock drive plugin implements DrivePlugin without runtime deps"
### Requirement: PluginHook type
The package SHALL export `PluginHook` as `(context: unknown, next: NextFunction) => Promise<unknown>`.
#### Scenario: PluginHook signature matches expected shape
- **WHEN** a function `(context, next) => Promise.resolve(undefined)` is asserted as `PluginHook`
- **THEN** the assertion compiles AND a function with a different arity (e.g. `(ctx) => ...`) fails to type-check
**Verified by:** `tests/spec-conformance.test.ts`::"PluginHook signature matches expected shape"
### Requirement: MiddlewareHandler type
The package SHALL export `MiddlewareHandler` as `(context: MiddlewareContext, next: NextFunction) => Promise<void>`.
**Verified by:** `tests/cross-package-compat.test.ts`::"middleware handler that skips next compiles correctly"
#### Scenario: Middleware can short-circuit
- **WHEN** a middleware handler does not call `next()`
- **THEN** it still satisfies the `MiddlewareHandler` type signature
### Requirement: MiddlewareContext interface
The package SHALL export `MiddlewareContext` with `operation: string`, `path: string`, and an open index signature `[key: string]: unknown` so consumers can extend it with `drive`, `args`, or other properties without circular deps.
**Verified by:** `tests/spec-conformance.test.ts`::"MiddlewareContext accepts extra properties via index signature"
#### Scenario: Consumer extension
- **WHEN** an object literal includes `operation`, `path`, and `drive: myDrive`
- **THEN** it is assignable to `MiddlewareContext`
### Requirement: NextFunction type
The package SHALL export `NextFunction` as `() => Promise<void>`.
#### Scenario: NextFunction is a no-arg promise-returning function
- **WHEN** a no-arg async function is asserted as `NextFunction`
- **THEN** the assertion compiles AND a function that takes any argument fails to type-check
**Verified by:** `tests/spec-conformance.test.ts`::"NextFunction is a no-arg promise-returning function"
### Requirement: HealthStatus interface
The package SHALL export `HealthStatus` with `status: 'healthy' | 'degraded' | 'failed' | 'unknown'`, optional `latencyMs`, `availableSpaceBytes`, `totalSpaceBytes`, `error`, and required `checkedAt: Date`.
**Verified by:** `tests/health-status.test.ts`::"minimal HealthStatus compiles" AND `tests/health-status.test.ts`::"full HealthStatus compiles"
#### Scenario: Minimal health check
- **WHEN** only `status` and `checkedAt` are provided
- **THEN** the object satisfies `HealthStatus`
#### Scenario: Full health check
- **WHEN** all fields including optional ones are provided
- **THEN** the object satisfies `HealthStatus`
### Requirement: Cross-cutting types are zero-runtime
All cross-cutting type exports (`ChunkSource`, `ChunkSourceHealth`, `ChunkSourceMetrics`, `ChunkRef`, `Plugin`, `PluginType`, `PluginRegistration`, `DrivePlugin`, `PluginHook`, `MiddlewareHandler`, `MiddlewareContext`, `NextFunction`, `HealthStatus`) SHALL be declared as `interface`, `type`, or `export type`. No function bodies, no class definitions, and no side effects SHALL be emitted at runtime by these modules.
**Verified by:** `tests/zero-runtime.test.ts`::"all new exports are type-only"
#### Scenario: Bundle analysis
- **WHEN** a bundler tree-shakes a file that imports only `ChunkSource` and `HealthStatus`
- **THEN** zero bytes from `@suaveplan/types` remain in the output
### Requirement: Subpath exports for cross-cutting types
`package.json` SHALL declare individual subpath exports for `./chunk-source`, `./plugin`, `./middleware`, `./health-status`, and `./chunk-ref`.
**Verified by:** `tests/zero-runtime.test.ts`::"subpath imports do not pull sibling modules"
#### Scenario: Direct subpath import
- **WHEN** a consumer imports `ChunkSource` from `@suaveplan/types/chunk-source`
- **THEN** no other module from `@suaveplan/types` is loaded
### Requirement: DomainTag is a branded type alias
The package SHALL expose `type DomainTag = string & { readonly __brand: "DomainTag" }`. The brand is a phantom type — it exists in the type system only and has no runtime representation. Consumers cannot construct a `DomainTag` from an arbitrary string at the type level; they MUST import a named constant (e.g. `DOMAIN_VAULT_V1`, `DOMAIN_PUBLIC_KEYCHAIN_V1`) from `@suaveplan/crypto/domain-tags`. The closed list of registered tag VALUES and the `encodeDomainTag` / `buildSigningInput` runtime helpers stay in `@suaveplan/crypto`.
#### Scenario: type-only import compiles without dragging in `@suaveplan/crypto`
- **WHEN** a package that does NOT depend on `@suaveplan/crypto` imports `DomainTag` from `@suaveplan/types`
- **THEN** TypeScript compilation succeeds AND the dep graph remains acyclic AND the package's bundled output contains zero references to `@suaveplan/crypto`
#### Scenario: arbitrary string is not assignable to DomainTag
- **WHEN** a caller writes `const t: DomainTag = "free-form"`
- **THEN** TypeScript reports a type error (the brand prevents implicit conversion from `string`)
#### Scenario: zero-runtime preserved
- **WHEN** the `@suaveplan/types` package is built and the per-file byte-budget guard runs
- **THEN** the compiled `dist/domain-tag.js` file is ≤ 200 bytes
**Verified by:** `tests/zero-runtime.test.ts`::"every .js file under dist/ is within the stub-size budget"
### Requirement: Authority interface owns the cross-package signing contract
The package SHALL expose `interface Authority` with five members: `sign(domain: DomainTag, slotId: SlotId, payload: Uint8Array, signal?: AbortSignal): Promise<Uint8Array>`, `getPublicSlot(slotId: SlotId): KeySlotPublic<unknown> | undefined`, `hasSlot(slotId: SlotId): boolean`, `dispose(): void`, and `readonly disposed: boolean`. The package SHALL also expose `KeySlotPublic<TLayoutFields = unknown>`, `KeySlotPrivate<TLayoutFields = unknown>` (extends `KeySlotPublic`), and `type SlotId = string`. The `coordinate` field on `KeySlotPublic` is typed as `unknown` because its concrete shape lives in `@suaveplan/crypto`.
#### Scenario: `@suaveplan/crypto::InMemoryAuthority` satisfies the interface
- **WHEN** the existing `InMemoryAuthority` class in `@suaveplan/crypto/authority` declares `implements Authority` against the interface from `@suaveplan/types`
- **THEN** TypeScript structural compatibility check passes WITHOUT any shape change to `InMemoryAuthority`'s methods
#### Scenario: `bitpacker-crypto`'s `SignedBitSchema` accepts an `Authority` from `@suaveplan/types`
- **WHEN** `bitpacker-crypto::defineSignedBitSchema` declares its `pack(data, authority: Authority, ...)` parameter using the `Authority` import from `@suaveplan/types`
- **THEN** a caller passing an `InMemoryAuthority` instance compiles AND the call-site has no transitive dep on `@suaveplan/crypto` for the type itself
#### Scenario: `KeySlotPublic.coordinate` typed as `unknown` keeps the contract narrow
- **WHEN** a verifier reads `slot.coordinate` from a `KeySlotPublic<unknown>`
- **THEN** TypeScript requires the verifier to narrow the value before using it (no implicit `any`)
**Verified by:** `packages/bitpacker/bitpacker-crypto/src/signed/signed-bitschema.ts` (imports `Authority` from `@suaveplan/types`); `packages/core/types/src/authority.md`
### Requirement: AuthorityVerifier function-type for DI
The package SHALL expose `type AuthorityVerifier = (domain: DomainTag, publicSlot: KeySlotPublic<unknown>, payload: Uint8Array, signature: Uint8Array) => boolean`. Returns `true` when the signature verifies, `false` for bytes that simply don't match. Implementations MAY throw on structural boundary errors; primitives that need a boolean answer SHOULD swallow such throws and treat them as `false`.
#### Scenario: `verifyAuthoritySignature` from `@suaveplan/crypto` is assignable to `AuthorityVerifier`
- **WHEN** a caller writes `const v: AuthorityVerifier = verifyAuthoritySignature` (where `verifyAuthoritySignature` is the canonical implementation in `@suaveplan/crypto/authority`)
- **THEN** TypeScript accepts the assignment with no shape adapter needed
#### Scenario: a `bitpacker-crypto` primitive's `verifier` option accepts the canonical impl
- **WHEN** `defineSignedBitSchema(codec, { domain, verifier: verifyAuthoritySignature })` is constructed
- **THEN** TypeScript accepts the configuration AND the primitive's `unpack` / `verify` paths produce the same boolean answers as if the primitive had imported `verifyAuthoritySignature` directly
**Verified by:** `packages/bitpacker/bitpacker-crypto/src/signed/signed-bitschema.test.ts`::"pack signs via Authority under the configured domain"
### Requirement: AeadOps interface for DI'd AEAD operations
The package SHALL expose `interface AeadOps` with two members: `encrypt(algorithm: AeadAlgorithm, key: Uint8Array, plaintext: Uint8Array, options: AeadOpsOptions): Uint8Array` and `decrypt(algorithm, key, ciphertext, options): Uint8Array`. The package SHALL expose `type AeadAlgorithm = "AES-256-GCM" | "ChaCha20-Poly1305" | "XChaCha20-Poly1305"` and `interface AeadOpsOptions { readonly nonce: Uint8Array; readonly aad?: Uint8Array }`. Both methods are synchronous because the canonical `@suaveplan/crypto::aeadEncrypt` / `aeadDecrypt` are synchronous.
#### Scenario: canonical `aeadEncrypt` / `aeadDecrypt` are assignable to `AeadOps`
- **WHEN** a caller writes `const ops: AeadOps = { encrypt: aeadEncrypt, decrypt: aeadDecrypt }` (where the right-hand-side functions come from `@suaveplan/crypto`)
- **THEN** TypeScript accepts the assignment with no shape adapter needed
#### Scenario: `bitpacker-crypto::EncryptedBitSchema` accepts the canonical AEAD bundle
- **WHEN** `defineEncryptedBitSchema(codec, { key, aead: { encrypt: aeadEncrypt, decrypt: aeadDecrypt } })` is constructed and a payload is round-tripped
- **THEN** the payload decrypts to the original value AND the primitive has no transitive dep on `@suaveplan/crypto`
#### Scenario: `AeadAlgorithm` discriminator matches `@suaveplan/crypto`'s union
- **WHEN** the three string literals (`"AES-256-GCM"`, `"ChaCha20-Poly1305"`, `"XChaCha20-Poly1305"`) are used in the type position of `AeadAlgorithm`
- **THEN** they are byte-identical to the strings accepted by `@suaveplan/crypto::aeadEncrypt`'s first parameter
**Verified by:** `packages/bitpacker/bitpacker-crypto/src/encrypted/encrypted-bitschema.test.ts`::"round-trips through XChaCha20-Poly1305"
### Requirement: StreamingAeadEngine cross-package interface
`@suaveplan/types` SHALL export a `StreamingAeadEngine` interface defining the cross-package contract for pluggable streaming-AEAD engines. The interface SHALL declare the engine's stable identity (`id`, `wireId`), its key / nonce / tag dimensions (`keyLength`, `nonceLength`, `nonceBaseLength`, `tagLength`), a `deriveNonce(baseNonce, counter, finalFlag)` method, and async `seal(input)` / `open(input)` methods consuming `StreamingChunkInput` and returning `Uint8Array`.
The canonical implementations (`aesGcmStreamingEngine`, `xchacha20StreamingEngine`) live in `@suaveplan/crypto-cipher` and SHALL satisfy this interface. The high-level streaming façade in `@suaveplan/crypto` SHALL accept any `StreamingAeadEngine` instance — caller-supplied custom engines (HSM-backed, threshold, recording test spies) substitute cleanly for the built-ins.
#### Scenario: interface is types-only
- **WHEN** the `@suaveplan/types` package is built
- **THEN** `streaming-aead-engine.ts` emits zero runtime bytes (the package's byte-size test still passes)
**Verified by:** `packages/core/types/tests/byte-size.test.ts::"types package emits zero runtime bytes"` AND `packages/crypto/crypto-cipher/src/streaming/streaming-aead/streaming-aead.spec-conformance.test.ts::"StreamingAeadEngine contract is stable for custom engines"`
### Requirement: StreamingChunkInput cross-package interface
`@suaveplan/types` SHALL export a `StreamingChunkInput` interface bundling the inputs passed to `StreamingAeadEngine.seal` / `StreamingAeadEngine.open`: `key`, `baseNonce`, `counter` (32-bit), `finalFlag` (`0 | 1`), `data`, optional `aad`. The interface is the cross-package contract for a single chunk's encrypt / decrypt invocation.
#### Scenario: shape is byte-only
- **WHEN** a consumer calls `engine.seal({ key, baseNonce, counter, finalFlag, data })`
- **THEN** the type system enforces every field is a `Uint8Array` or `number` literal — no runtime validation needed at the boundary
**Verified by:** `packages/crypto/crypto-cipher/src/streaming/streaming-aead-aes-gcm/streaming-aead-aes-gcm.test.ts::"seal then open round-trips bytes for a single chunk"` AND `packages/crypto/crypto-cipher/src/streaming/streaming-aead-xchacha20/streaming-aead-xchacha20.test.ts::"seal then open round-trips bytes for a single chunk"`
### Requirement: StreamingChunkTag cross-package interface
`@suaveplan/types` SHALL export a `StreamingChunkTag` interface used by attestation / observability consumers that subscribe to per-chunk callbacks via `EncryptStreamAeadOptions.onChunk` and `DecryptStreamAeadOptions.onChunk`. The interface SHALL carry `counter`, `finalFlag`, and `tag: Uint8Array` (the AEAD authentication tag for the chunk).
#### Scenario: chunk tags are emitted to the onChunk callback
- **WHEN** `decryptStreamAead(source, { onChunk })` runs to completion over a multi-chunk stream
- **THEN** the callback fires once per chunk with a `StreamingChunkTag` carrying the chunk's counter, final-flag, and AEAD tag bytes
**Verified by:** `packages/crypto/crypto-cipher/src/streaming/streaming-aead/streaming-aead.test.ts::"decryptStreamAead reports the parsed header via onHeader"`
### Requirement: StreamingHeader cross-package interface
`@suaveplan/types` SHALL export a `StreamingHeader` interface used by attestation / observability consumers that subscribe to the once-per-stream `onHeader` callback. The interface SHALL carry `engineId`, `wireId`, `version`, `baseNonceLength`, `baseNonce`.
#### Scenario: header is emitted on stream start
- **WHEN** `encryptStreamAead(key, source, { onHeader, baseNonce, engine })` begins iteration
- **THEN** the callback fires exactly once with a `StreamingHeader` whose `engineId` matches the engine in use and whose `baseNonceLength` matches `baseNonce.length`
**Verified by:** `packages/crypto/crypto-cipher/src/streaming/streaming-aead/streaming-aead.test.ts::"decryptStreamAead reports the parsed header via onHeader"`
### Requirement: EncryptStreamAeadOptions cross-package interface
`@suaveplan/types` SHALL export an `EncryptStreamAeadOptions` interface declaring the options bundle accepted by `encryptStreamAead`. Required: `baseNonce: Uint8Array` (caller-supplied; primitives never generate randomness). Optional: `engine`, `signal`, `onHeader`, `onChunk`.
#### Scenario: baseNonce is required at the type level
- **WHEN** a consumer omits `baseNonce` from an `EncryptStreamAeadOptions` literal
- **THEN** the TypeScript compiler reports a missing-property error (no runtime guard required)
**Verified by:** `packages/crypto/crypto-cipher/src/streaming/streaming-aead/streaming-aead.test.ts::"rejects a wrong-length base nonce on encrypt"` AND `packages/crypto/crypto-cipher/src/streaming/streaming-aead/streaming-aead.test.ts::"round-trips a multi-chunk plaintext through encryptStreamAead and decryptStreamAead"`
### Requirement: DecryptStreamAeadOptions cross-package interface
`@suaveplan/types` SHALL export a `DecryptStreamAeadOptions` interface declaring the options bundle accepted by `decryptStreamAead`. All fields are optional: `engine`, `allowUnterminated`, `signal`, `onHeader`, `onChunk`. The decrypt path reads the base nonce from the wire header rather than from caller options, so no `baseNonce` field is exposed.
#### Scenario: allowUnterminated opts into truncated-stream tolerance
- **WHEN** `decryptStreamAead(key, source, { allowUnterminated: true })` is called against a stream whose final terminator frame has been dropped
- **THEN** the call returns silently rather than throwing `STREAM_TAG_MISMATCH`
**Verified by:** `packages/crypto/crypto-cipher/src/streaming/streaming-aead/streaming-aead.test.ts::"decrypt: allowUnterminated returns silently on truncation"`
### Requirement: Backend interface for translation resource loading
The package SHALL export a `Backend` interface (in `src/i18n-backend.ts`, re-exported from `src/index.ts`) that any i18n backend implementation can satisfy without depending on `@suaveplan/i18n` core. The interface SHALL declare:
- `readonly name: string` — backend identifier
- `readonly capabilities: BackendCapabilities` — capability flags
- `read(locale: string, namespace: string): Promise<TranslationRecord>` — mandatory single-namespace load
- `readMulti?(locale: string, namespaces: readonly string[]): Promise<Record<string, TranslationRecord>>` — optional bulk-load optimisation
- `save?(locale: string, namespace: string, record: TranslationRecord): Promise<void>` — optional write surface
- `lastModified?(locale: string, namespace: string): Promise<number | undefined>` — optional cache-busting hook
- `init?(context: BackendInitContext): Promise<void> | void` — optional one-shot setup hook receiving the catalog's init context for watch-capable backends
- `dispose?(): Promise<void> | void` — optional teardown hook called when the catalog is disposed
#### Scenario: minimal in-memory backend satisfies the interface
- **WHEN** a class implements only `name`, `capabilities`, and `read`
- **THEN** `expectTypeOf<typeof MinimalBackend>().toMatchTypeOf<new () => Backend>()` passes
#### Scenario: backend with all optional methods satisfies the interface
- **WHEN** a class implements every optional method (`readMulti`, `save`, `lastModified`, `init`, `dispose`)
- **THEN** `expectTypeOf<typeof FullBackend>().toMatchTypeOf<new () => Backend>()` passes
**Verified by:** `packages/core/types/tests/i18n-backend.test.ts::"minimal Backend implementation typechecks"` AND `packages/core/types/tests/i18n-backend.test.ts::"full Backend implementation typechecks"`
### Requirement: BackendCapabilities flags
The package SHALL export a `BackendCapabilities` interface declaring five required capability flags:
- `readonly canRead: true` — literal `true`; included for symmetry so the matrix is uniform across every dimension
- `readonly canReadMulti: boolean` — true iff `readMulti?` is implemented
- `readonly canSave: boolean` — true iff `save?` is implemented
- `readonly canTrackModification: boolean` — true iff `lastModified?` is implemented
- `readonly canWatch: boolean` — true iff the backend emits reactive `loaded` events via the `init` context
These flags SHALL be readable at runtime so the catalog can route reads to the most efficient backend in a `ChainedBackend`.
#### Scenario: capabilities are required fields
- **WHEN** a `BackendCapabilities` literal omits any of the five flags
- **THEN** TypeScript reports a `Property '<flag>' is missing in type` error
#### Scenario: canRead must be the literal true
- **WHEN** a `BackendCapabilities` literal sets `canRead: false`
- **THEN** TypeScript reports `Type 'false' is not assignable to type 'true'`
**Verified by:** `packages/core/types/tests/i18n-backend.test.ts::"BackendCapabilities requires all five flags"`
### Requirement: BackendInitContext for watch-capable backends
The package SHALL export a `BackendInitContext` interface declaring the context the catalog passes to `Backend.init` at registration:
- `readonly catalogName: string` — stable name of the catalog this backend is being attached to
- `readonly emitLoaded: (locale: string, namespace: string, meta?: { backend: string }) => void` — emit a `loaded` event after the underlying source changes (file watcher fired, hot-reload socket message arrived, etc.)
#### Scenario: catalog name and emit callback are required
- **WHEN** a `BackendInitContext` literal is constructed and `emitLoaded("en", "common", { backend: "memory" })` is invoked
- **THEN** the call typechecks and `catalogName` is readable
**Verified by:** `packages/core/types/tests/i18n-backend.test.ts::"BackendInitContext shape"`
### Requirement: BackendFactory convenience type
The package SHALL export a `BackendFactory<TOptions = unknown>` type alias equal to `(options?: TOptions) => Backend`. Concrete backends export both the class and a factory function (e.g. `class MemoryBackend implements Backend` plus `memoryBackend(opts?: MemoryBackendOptions): Backend`), and the factory's return type is annotated with `BackendFactory<MemoryBackendOptions>`.
#### Scenario: factory returns a Backend value
- **WHEN** a `BackendFactory<TOptions>` is constructed and invoked with options
- **THEN** the return value is structurally a `Backend`
**Verified by:** `packages/core/types/tests/i18n-backend.test.ts::"BackendFactory<TOptions> returns Backend"`
### Requirement: LayoutOption discriminator union
The package SHALL export a `LayoutOption` string-literal union covering every layout shape used by storage- and filesystem-backed i18n backends:
- `"single-key-per-namespace"` — `<prefix>/<locale>/<namespace>.json` → serialised TranslationRecord
- `"single-key-per-locale"` — `<prefix>/<locale>.json` → `Record<namespace, TranslationRecord>`
- `"flat-keys"` — `<prefix>:<locale>:<namespace>:<dotPath>` → leaf string
- `"nested"`, `"flat"`, `"glob"` — filesystem-only layouts
#### Scenario: every documented layout literal is assignable
- **WHEN** every documented literal is assigned to `LayoutOption`
- **THEN** every assignment typechecks
#### Scenario: unknown layout literal is rejected
- **WHEN** a string outside the documented set is assigned to `LayoutOption`
- **THEN** TypeScript reports a string-literal mismatch
**Verified by:** `packages/core/types/tests/i18n-backend.test.ts::"every documented layout literal is assignable"`
### Requirement: TranslationRecord recursive shape
The package SHALL export a `TranslationRecord` type that is `{ readonly [key: string]: string | TranslationRecord }`. The type SHALL allow arbitrary nesting depth so namespaces can be modelled as nested objects (e.g. `{ auth: { login: { title: "Sign in" } } }`).
#### Scenario: leaves are strings
- **WHEN** a `TranslationRecord` literal contains `{ greet: "hi" }`
- **THEN** the literal is assignable to `TranslationRecord`
#### Scenario: nested records are valid
- **WHEN** a `TranslationRecord` literal contains `{ auth: { login: { title: "x" } } }`
- **THEN** the literal is assignable to `TranslationRecord`
#### Scenario: non-string non-record values rejected
- **WHEN** a `TranslationRecord` literal contains `{ count: 42 }`
- **THEN** TypeScript reports `Type 'number' is not assignable to type 'string | TranslationRecord'`
**Verified by:** `packages/core/types/tests/i18n-backend.test.ts::"TranslationRecord allows string leaves"` AND `packages/core/types/tests/i18n-backend.test.ts::"TranslationRecord allows nested records"` AND `packages/core/types/tests/i18n-backend.test.ts::"TranslationRecord rejects non-string non-record values"`
### Requirement: catalog data shapes exposed for cross-package consumption
The package SHALL also export the following catalog-related types so the slim core's re-export bridge (Wave 6) does not have to redeclare them:
- `LocaleNamespace` — `{ readonly locale: string; readonly namespace: string }`
- `MessageLoader` — `(locale: string, namespace?: string) => Promise<TranslationRecord>`
- `MissingKeyHandler` — `(info: { readonly locale: string; readonly namespace: string; readonly key: string }) => string | undefined`
- `LookupResult` — `{ readonly value: string | undefined; readonly fellBackTo: string | undefined }`
- `NamespacedCatalogMap` — `Map<string, TranslationRecord>`
- `TranslationCatalogMap` — `Map<string, NamespacedCatalogMap>`
#### Scenario: every catalog type is importable from `@suaveplan/types`
- **WHEN** a downstream package writes `import type { LocaleNamespace, MessageLoader, MissingKeyHandler, LookupResult, NamespacedCatalogMap, TranslationCatalogMap } from "@suaveplan/types";`
- **THEN** the import resolves with no `Module has no exported member` errors
**Verified by:** `packages/core/types/tests/i18n-backend.test.ts::"all catalog types are exported from the @suaveplan/types root"`
### Requirement: zero runtime contribution from i18n contract
The i18n cross-package contract SHALL NOT add any runtime code. The package's existing byte-size guard SHALL continue to pass.
#### Scenario: byte-size test stays green
- **WHEN** `bunx turbo build --filter=@suaveplan/types` runs
- **THEN** the resulting runtime emits zero bytes (types-only)
**Verified by:** existing byte-size guard in `packages/core/types/tests/` (continues to pass)
### Requirement: TimersLike interface
The package SHALL export a `TimersLike` interface defining a minimal pluggable timer pair compatible with the platform `setTimeout` / `clearTimeout` shape but stripped to the lowest-common-denominator that works under both `globalThis` timers and test-controlled `FakeClock` backends.
```ts
interface TimersLike {
setTimer(callback: () => void, delayMs: number): number;
clearTimer(handle: number): void;
}
```
`setTimer` SHALL schedule `callback` to run after `delayMs` and return an opaque numeric handle. `clearTimer` SHALL cancel a pending timer by handle and SHALL be a no-op for unknown handles (idempotent). Neither method SHALL accept variadic forwarding arguments — callers SHALL close over any state via the `callback` closure. The interface SHALL be types-only with zero runtime emission.
#### Scenario: globalThis-backed default works
- **GIVEN** a `TimersLike` instance backed by `(cb, ms) => globalThis.setTimeout(cb, ms)` and `(h) => globalThis.clearTimeout(h)`
- **WHEN** `setTimer(fn, 10)` is called
- **THEN** `fn` is invoked after at least 10 wall-clock milliseconds and the returned handle is accepted by `clearTimer` without throwing.
#### Scenario: FakeClock-backed implementation is satisfiable
- **GIVEN** a FakeClock-backed `TimersLike` whose `setTimer` registers `(callback, delayMs)` in an internal queue keyed by `clock.now() + delayMs`
- **WHEN** `clock.advance(delayMs)` is called
- **THEN** every registered callback whose deadline has elapsed fires synchronously in deadline order and zero real wall-clock time has elapsed.
#### Scenario: Idempotent clearTimer
- **GIVEN** any `TimersLike` implementation and a handle that has never been issued (or that was already cleared)
- **WHEN** `clearTimer(handle)` is called
- **THEN** it returns without throwing.
**Verified by:** consumers in `@suaveplan/cron` (`SchedulerTimers = TimersLike`) and `@suaveplan/retry` (`RetryConfig.timers`) — both pass 100% Istanbul coverage including FakeClock-driven test cases.
## Requirements (added by add-types-fetch-like — 2026-05-16)
### Requirement: FetchLike matches the WHATWG fetch shape
The package SHALL export a `FetchLike` type with the signature
`(input: string | URL | Request, init?: RequestInit) => Promise<Response>`.
The type SHALL be assignable from `globalThis.fetch` and SHALL accept any function whose first parameter is `string | URL | Request` and second parameter is optional `RequestInit`.
#### Scenario: globalThis.fetch is assignable to FetchLike
- **WHEN** a caller assigns `globalThis.fetch` to a `FetchLike`-typed variable
- **THEN** the assignment typechecks without `as` casts
#### Scenario: narrower input shape still satisfies FetchLike
- **WHEN** a caller assigns `(input: string | URL, init?: RequestInit) => Promise<Response>` to a `FetchLike`-typed variable
- **THEN** the assignment fails typecheck (input parameter is narrower — contravariant)
- **AND** wrapping the function in `(input, init) => narrower(input as string | URL, init)` re-establishes assignability
**Verified by:** `src/fetch-like.test.ts::"globalThis.fetch is assignable to FetchLike"` AND `src/fetch-like.test.ts::"a function matching the full shape is assignable"`
---
### Requirement: FetchLike is exported from the package root and via a `./fetch-like` subpath
The package SHALL re-export `FetchLike` from `src/index.ts` so that `import type { FetchLike } from "@suaveplan/types"` works.
The package SHALL also declare a `./fetch-like` subpath export in `package.json` so that `import type { FetchLike } from "@suaveplan/types/fetch-like"` works.
#### Scenario: root barrel exports FetchLike
- **WHEN** a consumer writes `import type { FetchLike } from "@suaveplan/types"`
- **THEN** the import resolves and typechecks
#### Scenario: subpath export resolves
- **WHEN** a consumer writes `import type { FetchLike } from "@suaveplan/types/fetch-like"`
- **THEN** the import resolves and typechecks
**Verified by:** `src/fetch-like.test.ts::"FetchLike is exported from the package root"`
---
### Requirement: FetchLike sibling documentation meets the docs-check contract
The package SHALL ship `src/fetch-like.md` meeting CLAUDE.md §4 — at minimum ≥200 words excluding code blocks, with sections `Purpose`, `Features`, `Usage: Basic Example`, `Usage: Advanced Example`, `API Reference`, `Implementation Notes`.
#### Scenario: docs-check passes
- **WHEN** `bun run docs:check` is run against the package
- **THEN** the script reports `Docs coverage: N/N (100%)` and exits 0
**Verified by:** the package's CI gate `bun run docs:check`.