Files
suaveplan/openspec/archive/2026-05-11-enterprise-tpm-struct/proposal.md
2026-05-27 23:29:53 +01:00

143 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `@suaveplan/tpm-struct` — TPM 2.0 attestation-structure parser
**Package**: `@suaveplan/tpm-struct` (NEW)
**Status**: PROPOSED
**Type**: new-package (genesis-original; greenfield — no upstream port)
**Date**: 2026-05-11
**Path**: `packages/security/tpm-struct/`
## Why
`@suaveplan/webauthn`'s `tpm` attestation-statement verifier (W3C WebAuthn L3 §8.3 — _TPM Attestation Statement Format_) is blocked on a universal-tier TPM 2.0 attestation-structure parser. The verifier MUST decode `TPMS_ATTEST` from the `attestationData` field (TCG TPM 2.0 Library Part 2 §10.12), MUST decode `TPMT_PUBLIC` from the `pubArea` field (TCG Part 2 §12.2), MUST compute the canonical TPM name digest of that public area (TCG Part 1 §16) so it can match it against `authData.attestedCredentialData.credentialPublicKey`, and MUST verify that `extraData` from the attest blob equals the `attToBeSigned` digest the relying party recomputes (WebAuthn L3 §8.3 step 8). External libraries (`tpm-js`, `node-tss`, `tss.js`) either drag in a TPM driver, target Node-only, ship CommonJS with no type information, or implement only a sliver of Part 2 §10/§11/§12. An in-house, spec-cited, zero-dep, buffer-only parser keeps `webauthn` universal-tier and gives every future TPM 2.0 attestation context (AIK enrollment per TCG _Credential Profile EK 2.0_, remote attestation, vTPM provisioning) one canonical implementation with one error registry, one OTel surface, and one set of test vectors.
The same parser is webauthn-agnostic. The public surface decodes TPM 2.0 byte strings; it neither imports nor knows about WebAuthn's `attStmt` map, `authData`, or COSE keys.
## What changes
### New package: `@suaveplan/tpm-struct`
- ADDED: `packages/security/tpm-struct/` package scaffold (`package.json`, `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`, `README.md`).
- ADDED: `src/buffer/buffer.ts` — `TpmBufferReader` (`readUint8`, `readUint16Be`, `readUint32Be`, `readUint64Be: bigint`, `readBytes(n)`, `readTpm2b(): Uint8Array`, `peek(n)`, `remaining()`, `assertEnd()`) and `TpmBufferWriter` (matching write surface plus `writeTpm2b(bytes)`, `toUint8Array()`) per TCG Part 2 §10.1 octet-stream conventions; every read past `bytes.length` raises `TPM_STRUCT.TRUNCATED` carrying cursor + requested-length in `cause`.
- ADDED: `src/buffer/buffer.types.ts` — `TpmReaderOptions`, `TpmWriterOptions`, `Tpm2bReadResult`.
- ADDED: `src/alg/alg.ts` — `TPM_ALG` frozen registry mirroring TCG Part 2 §11.1 (`NULL = 0x0010`, `RSA = 0x0001`, `SHA1 = 0x0004`, `HMAC = 0x0005`, `AES = 0x0006`, `KEYEDHASH = 0x0008`, `SHA256 = 0x000b`, `SHA384 = 0x000c`, `SHA512 = 0x000d`, `XOR = 0x000a`, `RSASSA = 0x0014`, `RSAES = 0x0015`, `RSAPSS = 0x0016`, `OAEP = 0x0017`, `ECDSA = 0x0018`, `ECDH = 0x0019`, `ECDAA = 0x001a`, `SM2 = 0x001b`, `ECSCHNORR = 0x001c`, `ECMQV = 0x001d`, `KDF1_SP800_56A = 0x0020`, `KDF2 = 0x0021`, `KDF1_SP800_108 = 0x0022`, `ECC = 0x0023`, `SYMCIPHER = 0x0025`, `CTR = 0x0040`, `OFB = 0x0041`, `CBC = 0x0042`, `CFB = 0x0043`, `ECB = 0x0044`); `TPM_ECC_CURVE` frozen registry (`NIST_P256 = 0x0003`, `NIST_P384 = 0x0004`, `NIST_P521 = 0x0005`, `BN_P256 = 0x0010`, `BN_P638 = 0x0011`, `SM2_P256 = 0x0020`); `algIdToName(id)` returns the symbolic name; unknown IDs raise `TPM_STRUCT.ALG_UNSUPPORTED`. `eccCurveToName` does the same for curves; unknown raises `TPM_STRUCT.CURVE_UNSUPPORTED`.
- ADDED: `src/alg/alg.types.ts` — `TpmAlgId`, `TpmEccCurve` literal unions.
- ADDED: `src/public/public.ts` — `parseTpmtPublic(bytes): TpmtPublic` per TCG Part 2 §12.2; reads `type` (TPMI_ALG_PUBLIC), `nameAlg` (TPMI_ALG_HASH), `objectAttributes` (32-bit TPMA_OBJECT per §8.3 — exposes raw `uint32` and named-flag accessor), `authPolicy` (TPM2B_DIGEST), `parameters` (`TPMS_RSA_PARMS` per §12.2.3.5 with `symmetric` `TPMT_SYM_DEF_OBJECT`, `scheme` `TPMT_RSA_SCHEME`, `keyBits`, `exponent` (uint32, `0` ⇒ default 65537)), `parameters` (`TPMS_ECC_PARMS` per §12.2.3.6 with `symmetric`, `scheme` `TPMT_ECC_SCHEME`, `curveID` `TPMI_ECC_CURVE`, `kdf` `TPMT_KDF_SCHEME`), `unique` (`TPM2B_PUBLIC_KEY_RSA` for RSA or `TPMS_ECC_POINT` `{ x, y }` of two `TPM2B_ECC_PARAMETER`s for ECC). KEYEDHASH and SYMCIPHER branches decode their `TPMS_*_PARMS` per §12.2.3.3 / §12.2.3.4 and return the same discriminated-union shape.
- ADDED: `src/public/public.types.ts` — `TpmtPublic` discriminator on `type`; `TpmsRsaParms`, `TpmsEccParms`, `TpmsKeyedHashParms`, `TpmsSymCipherParms`, `TpmsEccPoint`, `ObjectAttributes` (typed flag struct + raw `uint32` slot).
- ADDED: `src/public/serialize.ts` — `serializeTpmtPublic(pub): Uint8Array` produces byte-identical re-serialisation of any `parseTpmtPublic` output; powers `computeTpmtPublicName`.
- ADDED: `src/attest/attest.ts` — `parseTpmsAttest(bytes): TpmsAttest` per TCG Part 2 §10.12; validates 4-byte magic `0xff544347` (`TPM_GENERATED_VALUE`, ASCII "TCG\xff" in big-endian); reads `type` (`TPMI_ST_ATTEST` — `TPM_ST_ATTEST_CERTIFY = 0x8017`, `TPM_ST_ATTEST_QUOTE = 0x8018`, `TPM_ST_ATTEST_SESSION_AUDIT = 0x8016`, `TPM_ST_ATTEST_COMMAND_AUDIT = 0x8015`, `TPM_ST_ATTEST_TIME = 0x8019`, `TPM_ST_ATTEST_CREATION = 0x801a`, `TPM_ST_ATTEST_NV = 0x8014`, `TPM_ST_ATTEST_NV_DIGEST = 0x801c`), `qualifiedSigner` (TPM2B_NAME), `extraData` (TPM2B_DATA), `clockInfo` (17 bytes: uint64 clock, uint32 resetCount, uint32 restartCount, uint8 safe), `firmwareVersion` (uint64), and dispatches `attested` to the matching `TPMU_ATTEST` member (per §10.11). For `TPM_ST_ATTEST_CERTIFY` the `attested` is `TPMS_CERTIFY_INFO` (§10.5) — `name` + `qualifiedName` both `TPM2B_NAME`.
- ADDED: `src/attest/attest.types.ts` — `TpmsAttest` discriminator on `type`; `TpmsCertifyInfo`, `TpmsQuoteInfo`, `TpmsSessionAuditInfo`, `TpmsCommandAuditInfo`, `TpmsTimeAttestInfo`, `TpmsCreationInfo`, `TpmsNvCertifyInfo`, `TpmsNvDigestCertifyInfo`, `ClockInfo`.
- ADDED: `src/attest/serialize.ts` — `serializeTpmsAttest(att): Uint8Array` produces byte-identical re-serialisation of any `parseTpmsAttest` output.
- ADDED: `src/name/name.ts` — `computeTpmtPublicName(pub, hash): Uint8Array` per TCG Part 1 §16. Output = `nameAlg (2 BE bytes) || H_nameAlg(serializeTpmtPublic(pub))`. Hash backend injected via `HashLike` (caller supplies a SHA-1/256/384/512 implementation matching `nameAlg`; the package itself imports no hash code). `nameAlg === TPM_ALG_NULL` is rejected with `TPM_STRUCT.NAME_ALG_NULL`.
- ADDED: `src/name/name.types.ts` — `HashLike` interface (`digest(bytes: Uint8Array): Uint8Array`).
- ADDED: `src/extra-data/extra-data.ts` — `extraDataMatches(actual: Uint8Array, expected: Uint8Array, equal: ConstantTimeEqual): boolean`; the comparison function is injected to keep the package zero-runtime-dep on crypto-utils.
- ADDED: `src/extra-data/extra-data.types.ts` — `ConstantTimeEqual` function signature.
- ADDED: `src/schemas/schemas.ts` — Zod boundary schemas: `TpmsAttestSchema`, `TpmtPublicSchema`, `TpmsRsaParmsSchema`, `TpmsEccParmsSchema`, `TpmsKeyedHashParmsSchema`, `TpmsSymCipherParmsSchema`, `TpmsCertifyInfoSchema`, `ClockInfoSchema`, `Tpm2bSchema`, `TpmAlgIdSchema`, `TpmEccCurveSchema`, `ObjectAttributesSchema`. Every public parser validates output through the matching schema; failure raises `TPM_STRUCT.MALFORMED` with Zod issues attached as `cause.issues`.
- ADDED: `src/errors/error-codes.ts` — `defineCodes("TPM_STRUCT", [...])` registry: `MAGIC_INVALID`, `TRUNCATED`, `ALG_UNSUPPORTED`, `CURVE_UNSUPPORTED`, `TYPE_MISMATCH`, `MALFORMED`, `NAME_ALG_NULL`, `ATTEST_TYPE_UNSUPPORTED`, `PARAMETERS_INVALID`, `OBJECT_ATTRIBUTES_RESERVED_NONZERO`. `TpmStructError extends SuaveplanError<TpmStructErrorCode>`.
- ADDED: `src/observability/metrics.ts` — histograms `tpm_struct.parse_tpms_attest_duration_ms{outcome}`, `tpm_struct.parse_tpmt_public_duration_ms{type,outcome}`, `tpm_struct.compute_name_duration_ms{name_alg}`; counters `tpm_struct.parse_total{kind,outcome}`, `tpm_struct.parse_errors_total{code}`, `tpm_struct.alg_total{alg}`. All attributes low-cardinality enums.
- ADDED: `src/diagnostic/diagnostic.ts` — `toDiagnostic(value: TpmsAttest | TpmtPublic): string` human-readable dump with `TPM_ALG` symbolic names inlined; debug-only; never invoked from production hot paths.
- ADDED: `src/__fixtures__/webauthn-tpm-vector.ts` — known-good real-world WebAuthn `tpm`-format attestation byte strings (`attestationData` + `pubArea`) plus expected parsed structures and expected name digest. Used by the WebAuthn integration scenario.
- ADDED: `src/__benchmarks__/parse-tpms-attest.bench.ts`, `parse-tpmt-public-rsa2048.bench.ts`, `parse-tpmt-public-ecc-p256.bench.ts`, `compute-name.bench.ts` — mitata suites.
- ADDED: `src/index.ts` — explicit named re-exports; no `export *` (per `openspec/AGENTS.md` §6a).
### Subpath exports
- `.` — root barrel: `parseTpmsAttest`, `parseTpmtPublic`, `computeTpmtPublicName`, `extraDataMatches`, `TPM_ALG`, `TPM_ECC_CURVE`, error codes.
- `./buffer` — `TpmBufferReader`, `TpmBufferWriter`, types.
- `./alg` — `TPM_ALG`, `TPM_ECC_CURVE`, `algIdToName`, `eccCurveToName`, literal-union types.
- `./public` — `parseTpmtPublic`, `serializeTpmtPublic`, `TpmtPublic`, parameter / point / attribute types.
- `./attest` — `parseTpmsAttest`, `serializeTpmsAttest`, `TpmsAttest`, every `TPMU_ATTEST` member type, `ClockInfo`.
- `./name` — `computeTpmtPublicName`, `HashLike`.
- `./extra-data` — `extraDataMatches`, `ConstantTimeEqual`.
- `./schemas` — Zod boundary schemas.
- `./errors` — `TPM_STRUCT_CODES`, `TpmStructError`, `TpmStructErrorCode`.
- `./diagnostic` — `toDiagnostic`.
### Dependencies
- `@suaveplan/error workspace:^` — `defineCodes`, `SuaveplanError` base.
- `@suaveplan/types workspace:^` — shared interface contracts.
- `@opentelemetry/api catalog:observability` — histograms + counters.
- `tslib catalog:build`.
- `zod catalog:forms` — option + result boundary validation.
- **No** `node:*` imports, **no** `tpm-js` / `node-tss` / `tss.js` / `@noble/*` / `@stablelib/*`. Pure TypeScript over `Uint8Array` + `DataView`.
## Out of scope
- **TPM I/O / TPM commands** — no `TPM2_*` command marshalling, no TSS device driver, no `/dev/tpmrm0` or Windows `Tbsi_*` integration, no TPM2_TOOLS shell wrapping. _Justification_: this package is buffer-only; TPM device interaction is the job of a TSS-shaped package that is not currently planned. Folding it in would require a Node-only target and pull in driver-level concerns that no current consumer needs.
- **TPM key creation / sealing / unsealing** — no `TPM2_Create`, no `TPM2_Load`, no auth-session orchestration. _Justification_: this is a parser/serialiser, not a key manager.
- **TPM session management / HMAC / parameter encryption** — no session contexts, no encryption sessions, no parameter encryption per TCG Part 1 §19. _Justification_: same — buffer parsing only.
- **AIK / EK credential validation chain** — TCG _Credential Profile_ EK 2.0 procedures (EK certificate verification, manufacturer-CA pin) are the WebAuthn verifier's job; this package returns the parsed structures it needs to do that work.
- **TPM 1.2 structures** — TPM 1.2 (`TPM_PUBKEY`, `TPM_QUOTE_INFO`) is out of scope; the WebAuthn `tpm` format mandates TPM 2.0.
- **COSE / CBOR / WebAuthn-format-specific concerns** — `@suaveplan/cbor` handles CBOR, `@suaveplan/webauthn` handles the `attStmt` map structure. This package never sees CBOR bytes.
## Source mapping
This package is genesis-original; no `../suaveplan/enterprise/tpm-struct.md` predecessor exists. The source-of-record is the TCG TPM 2.0 specification (Library Part 2 — Structures, revision 1.59, plus Library Part 1 — Architecture for the name digest) and W3C WebAuthn Level 3 for the consumer contract.
| Requirement | Source | Notes |
|---|---|---|
| `parseTpmsAttest` magic check (`0xff544347` / "TCG\xff") | TCG Part 2 §10.12 `TPMS_ATTEST.magic` + §6.2 `TPM_GENERATED_VALUE` | Big-endian uint32; mismatch ⇒ `MAGIC_INVALID` |
| `parseTpmsAttest` type field | TCG Part 2 §10.12 `type: TPMI_ST_ATTEST` + §10.5 `TPM_ST` constants | Eight-member subset of `TPM_ST_ATTEST_*` |
| `parseTpmsAttest` qualifiedSigner / extraData / clockInfo / firmwareVersion / attested | TCG Part 2 §10.12 field layout | `qualifiedSigner` = `TPM2B_NAME`; `extraData` = `TPM2B_DATA`; `clockInfo` per §10.10 (17 bytes); `firmwareVersion` = uint64 BE |
| `attested` union dispatch | TCG Part 2 §10.11 `TPMU_ATTEST` + §10.5 `TPMS_CERTIFY_INFO` | `TPM_ST_ATTEST_CERTIFY` ⇒ `TPMS_CERTIFY_INFO` (name + qualifiedName) |
| `parseTpmtPublic` type | TCG Part 2 §12.2 `TPMT_PUBLIC.type: TPMI_ALG_PUBLIC` | `RSA` / `ECC` / `KEYEDHASH` / `SYMCIPHER` |
| `parseTpmtPublic` nameAlg | TCG Part 2 §12.2 `nameAlg: TPMI_ALG_HASH` + §11.1 | `SHA-1/256/384/512`; `NULL` rejected for name-digest call site |
| `parseTpmtPublic` objectAttributes | TCG Part 2 §8.3 `TPMA_OBJECT` 32-bit bitfield | Named-flag accessor + raw uint32; reserved bits non-zero ⇒ `OBJECT_ATTRIBUTES_RESERVED_NONZERO` |
| `parseTpmtPublic` authPolicy | TCG Part 2 §12.2 `authPolicy: TPM2B_DIGEST` | Length-prefixed |
| RSA parameters | TCG Part 2 §12.2.3.5 `TPMS_RSA_PARMS` | symmetric / scheme / keyBits / exponent (uint32, `0` ⇒ default 65537 per spec) |
| ECC parameters | TCG Part 2 §12.2.3.6 `TPMS_ECC_PARMS` | symmetric / scheme / curveID (`TPMI_ECC_CURVE`) / kdf; curve lookup via §11.2.5 |
| RSA unique field | TCG Part 2 §12.2 `unique: TPM2B_PUBLIC_KEY_RSA` | Modulus bytes |
| ECC unique field | TCG Part 2 §12.2 `unique: TPMS_ECC_POINT` | `x` + `y` each `TPM2B_ECC_PARAMETER` |
| `computeTpmtPublicName` | TCG Part 1 §16 _Names_ | `nameAlg (2 BE) || H_nameAlg(TPMT_PUBLIC)` |
| `TPM2B` reader | TCG Part 2 §10.1 `TPM2B_*` size-prefixed buffer | UINT16 BE length + exact bytes |
| `TPM_ALG_ID` registry | TCG Part 2 §11.1 ALG_TABLE | 31-entry registry; unknown ⇒ `ALG_UNSUPPORTED` |
| WebAuthn consumer contract (`extraData`, `pubArea` match) | W3C WebAuthn L3 §8.3 steps 4–10 | `extraData === attToBeSigned`; `pubArea name === credentialPublicKey name` |
| EK credential context | TCG _Credential Profile EK 2.0_ v2.3 §3 | Informational — drives the `attested` union shape decisions |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| `defineCodes("TPM_STRUCT", [...])` 10-code registry | Genesis convention (`CLAUDE.md` Quality + `@suaveplan/error`); each code maps to a distinct §10 / §11 / §12 / §16 failure class so callers can branch on root cause. |
| Zod boundary schemas at every public type | Genesis convention (`CLAUDE.md` §11). A buggy reader cannot leak a partially-populated object past the boundary. |
| OTel surface (parse + name histograms; parse-total + parse-errors + alg counters) | Genesis convention — every hot-path package emits histograms + counters. `alg` attribute bounded by §11.1 registry; `code` bounded by the 10-entry code registry. |
| 300-line file budget + 100/100/100/100 coverage + sibling `.md` per impl file | `CLAUDE.md` Quality + Documentation sections. |
| Property tests (round-trip + single-bit-flip) | `CLAUDE.md` "no mocks" rule; round-trip is the only test that proves parser + serialiser agree under arbitrary valid inputs. |
| Mitata benchmarks | `CLAUDE.md` global benchmark requirement; baseline numbers feed README perf table. |
| Hash-injection via `HashLike` | Cross-cutting design — keeps the package universal-tier and free of crypto-engine selection; consumer wires `@suaveplan/crypto-hash` at the call site. |
## Verification approach
Every requirement carries a `**Verified by:**` line in `specs/tpm-struct/spec.md` naming concrete `it(...)` titles inside the test files below.
- `packages/security/tpm-struct/src/buffer/buffer.test.ts` — reader cursor advancement; `readTpm2b` length/payload round-trip; `assertEnd` rejects trailing bytes; out-of-bounds reads raise `TRUNCATED`.
- `packages/security/tpm-struct/src/alg/alg.test.ts` — every §11.1 entry round-trips through `algIdToName`; unknown ID raises `ALG_UNSUPPORTED`; every §11.2.5 curve round-trips through `eccCurveToName`.
- `packages/security/tpm-struct/src/public/public.test.ts` — `parseTpmtPublic` for RSA-2048, ECC P-256, ECC P-384, KEYEDHASH, SYMCIPHER; `objectAttributes` flag round-trip; reserved bits non-zero raises `OBJECT_ATTRIBUTES_RESERVED_NONZERO`.
- `packages/security/tpm-struct/src/public/property.test.ts` — fast-check round-trip: arbitrary valid `TpmtPublic` → `serializeTpmtPublic` → `parseTpmtPublic` byte-identical.
- `packages/security/tpm-struct/src/public/serialize.test.ts` — `serializeTpmtPublic` produces byte-identical output for parsed real-world fixtures.
- `packages/security/tpm-struct/src/attest/attest.test.ts` — magic mismatch raises `MAGIC_INVALID`; every `TPM_ST_ATTEST_*` variant decodes the matching `attested` union member; unknown `type` raises `ATTEST_TYPE_UNSUPPORTED`.
- `packages/security/tpm-struct/src/attest/property.test.ts` — fast-check round-trip: arbitrary valid `TpmsAttest` → `serializeTpmsAttest` → `parseTpmsAttest` byte-identical; single-bit-flip on any field either decodes to a different value or raises a typed `TpmStructError`.
- `packages/security/tpm-struct/src/attest/serialize.test.ts` — `serializeTpmsAttest` byte-identical for parsed real-world fixtures.
- `packages/security/tpm-struct/src/name/name.test.ts` — `computeTpmtPublicName` matches the TCG Part 1 §16 expected output for SHA-1/256/384/512 nameAlg variants; `nameAlg === TPM_ALG_NULL` raises `NAME_ALG_NULL`.
- `packages/security/tpm-struct/src/extra-data/extra-data.test.ts` — `extraDataMatches` true on equal buffers, false on mismatched bytes, false on length mismatch; delegates to the injected `ConstantTimeEqual`.
- `packages/security/tpm-struct/src/schemas/schemas.test.ts` — every Zod schema accepts a known-good fixture and rejects a malformed fixture with `MALFORMED` + Zod issues on `cause`.
- `packages/security/tpm-struct/src/errors/error-codes.test.ts` — every code in the `TPM_STRUCT_CODES` registry is thrown by ≥1 production path; `expectTypeOf<TpmStructErrorCode>().toEqualTypeOf<…>()` pins the literal union.
- `packages/security/tpm-struct/src/observability/metrics.test.ts` — histograms + counters emit under a real in-memory `MeterProvider`; `alg` and `code` attributes match the bounded enums.
- `packages/security/tpm-struct/src/diagnostic/diagnostic.test.ts` — `toDiagnostic` emits symbolic algorithm names; never invokes parser hot paths (no metric emission during diagnostic).
- `packages/security/tpm-struct/src/__integration__/webauthn-vector.test.ts` — known-good WebAuthn `tpm` attestation fixture: `parseTpmsAttest(attestationData)` succeeds; `parseTpmtPublic(pubArea)` succeeds; `computeTpmtPublicName(parsedPub, sha256)` equals the expected fixture digest byte-for-byte.
- `packages/security/tpm-struct/src/index.test.ts` — every documented subpath export resolves.
## Migration impact
None — additive only; this is the first delta of a new package. No existing genesis code imports from `@suaveplan/tpm-struct`. `@suaveplan/webauthn` is the gating consumer and lands in a follow-up change once this package archives; that change SHALL add the `workspace:^` dep edge and wire the `tpm`-attestation verifier.
## Impact
### Affected dependents
`bunx turbo ls --filter=...@suaveplan/tpm-struct` returns no current dependents (the package does not yet exist on `genesis`). The pending consumer is:
- `@suaveplan/webauthn` — [ ] tests run / [ ] docs reviewed / [x] no action needed in THIS change (reason: webauthn is itself pending; the `tpm`-attestation verifier wiring lands in the webauthn migration change once this package archives, per `STATUS.md` and `openspec/AGENTS.md` §0b).
No foundation-package fan-out applies (this is a new universal-tier leaf package; foundation deps `@suaveplan/error` and `@suaveplan/types` are unchanged).