Files
2026-05-27 23:29:53 +01:00

22 KiB
Raw Permalink Blame History

Tasks — @suaveplan/crypto-rsa (RSA primitives — RFC 8017 + RFC 5208 + RFC 7518)

Every checkbox MUST reflect work actually done in this change. Do not retroactively tick boxes for work that was already in main when this change opened. Do not tick a box for a test that doesn't exist or doesn't pin the requirement it claims to verify.

1. Spec authoring

  • 1.1 Read ../suaveplan/enterprise/crypto-rsa.md end-to-end
  • 1.2 Read RFC 8017 §4 (data conversion primitives), §5 (cryptographic primitives, incl. §5.1.2 CRT), §7 (RSAES encryption schemes), §8 (RSASSA signature schemes), §9 (encoding methods EMSA-PSS + EMSA-PKCS1-v1_5), §B.2.1 (MGF1), Appendix A (ASN.1 modules), Appendix C (test vectors)
  • 1.3 Read RFC 5208 §5 (PrivateKeyInfo) and RFC 5958 §2 (OneAsymmetricKey successor)
  • 1.4 Read RFC 5280 §1.2 + §4.1 + Appendix A (RSA OIDs 1.2.840.113549.1.1.{1,5,10,11,12,13} + SubjectPublicKeyInfo)
  • 1.5 Read RFC 7517 §4.2 + §4.3 + §4.4 + §6.3, RFC 7518 §3.3 + §3.5 + §6.3, RFC 7638 §3.2 (JWK / JWA / Thumbprint)
  • 1.6 Read RFC 7468 §3, §5, §10, §11 (PEM textual encoding for PUBLIC KEY, RSA PRIVATE KEY, PRIVATE KEY)
  • 1.7 Read NIST FIPS 186-5 §5 + Appendix B.3.1 + Appendix A.1 (RSA DSA, probable-prime generation, key-pair parameters)
  • 1.8 Read NIST SP 800-56B Rev. 2 §6.3 (key-pair generation guidance)
  • 1.9 Read Google Wycheproof rsa_signature_*.json, rsa_signature_pss_*.json, rsa_oaep_*.json schemas + a sample of the acceptable test cases
  • 1.10 Read NIST CAVP RSA test-vector format (RSAVS / RSA2VS)
  • 1.11 Read RFC 5990 (RSA-KEM) — confirm out-of-scope rationale
  • 1.12 Read W3C Web Cryptography API §RSA-OAEP / RSASSA-PKCS1-v1_5 / RSA-PSS sections for subtle engine adapter
  • 1.13 Read Bleichenbacher 1998 (CRYPTO '98), Manger 2001 (CRYPTO 2001), Bleichenbacher 2006 (signature forgery), Kocher 1996 (timing attacks) — confirm hardening approach
  • 1.14 Read openspec/AGENTS.md Sections 13, 14, 14a
  • 1.15 Read CLAUDE.md (root + crypto family entries — crypto-cipher, crypto-curve, crypto-hash, crypto-utils, crypto-vault)
  • 1.16 Draft proposal.md from openspec/templates/proposal.md
  • 1.17 Fill in ## Source mapping table — every requirement cites an RFC / FIPS / NIST / Wycheproof / enterprise/crypto-rsa.md line range
  • 1.18 Draft specs/crypto-rsa/spec.md from openspec/templates/spec-delta.md
  • 1.19 Every requirement in the spec delta carries a **Verified by:** line naming concrete it(...) titles

2. Package scaffold

  • 2.1 packages/crypto/crypto-rsa/ directory + src/{asn1,keys,keygen,policy,math,encoding,sign,verify,encrypt,decrypt,jwk,fingerprint,pem,blinding,engine,handle,profile,errors,schemas,observability,__benchmarks__,__fixtures__}/ skeleton
  • 2.2 package.json — @suaveplan/crypto-rsa@0.0.0, type: "module", sideEffects: false, deps pinned via workspace + catalog refs
  • 2.3 tsconfig.lib.json extends @suaveplan/typescript-config/library
  • 2.4 vite.config.ts + vitest.config.ts (use createTestConfig() from @suaveplan/testing/config; provider: 'istanbul'; thresholds 100/100/100/100)
  • 2.5 README.md with package description, install command, sign + verify quick-start, encrypt + decrypt quick-start, keygen quick-start, FIPS profile quick-start, Modules table linking every co-located .md
  • 2.6 Subpath exports map covers ., ./keys, ./keygen, ./sign, ./verify, ./encrypt, ./decrypt, ./encoding, ./jwk, ./fingerprint, ./pem, ./blinding, ./engine, ./handle, ./profile, ./policy, ./schemas, ./observability, ./errors

3. Implementation — ASN.1 / DER

  • 3.1 src/asn1/reader.ts — readInteger, readSequence, readObjectIdentifier, readBitString, readOctetString, readNull, readContextSpecific
  • 3.2 src/asn1/writer.ts — matching DER writer
  • 3.3 Sibling .md for reader.ts + writer.ts

4. Implementation — canonical key shapes

  • 4.1 src/keys/keys.types.ts — RsaPublicKey { n, e, bitLength }, RsaPrivateKey { n, e, d, p, q, dp, dq, qInv, bitLength }
  • 4.2 src/schemas/schemas.ts — RsaPublicKeySchema, RsaPrivateKeySchema, RsaParseOptionsSchema, RsaSerializeOptionsSchema, RsaSignOptionsSchema, RsaVerifyOptionsSchema, RsaPssOptionsSchema, RsaOaepOptionsSchema, HashAlgorithmSchema

5. Implementation — key parsing

  • 5.1 src/keys/parse-pkcs1-public.ts — RFC 8017 Appendix A.1.1
  • 5.2 src/keys/parse-pkcs1-private.ts — RFC 8017 Appendix A.1.2
  • 5.3 src/keys/parse-spki.ts — RFC 5280 + RFC 8017 Appendix A.1.1; rejects non-rsaEncryption OID
  • 5.4 src/keys/parse-pkcs8.ts — RFC 5208 / RFC 5958
  • 5.5 src/keys/parse-jwk.ts — RFC 7517 §6.3; rejects kty !== "RSA"
  • 5.6 src/keys/parse.ts — façade dispatching on { format }

6. Implementation — key serialisation

  • 6.1 src/keys/serialize-pkcs1-public.ts
  • 6.2 src/keys/serialize-pkcs1-private.ts
  • 6.3 src/keys/serialize-spki.ts
  • 6.4 src/keys/serialize-pkcs8.ts
  • 6.5 src/keys/serialize-jwk.ts — base64url per RFC 7515 §2 (no padding)
  • 6.6 src/keys/serialize.ts — façade
  • 6.7 Round-trip invariant: parse → serialise → parse equals original

7. Implementation — modulus-length policy

  • 7.1 src/policy/modulus.ts — enforceMinBitLength(key, { allowLegacy? }); default floor 2048; allowLegacy: true permits 1024; below 1024 always rejects with CRYPTO_RSA.MODULUS_TOO_SHORT

8. Implementation — modular arithmetic

  • 8.1 src/math/modpow.ts — wraps @noble/curves/abstract/modular.pow; NEVER BigInt ** BigInt % N
  • 8.2 src/math/i2osp.ts — Integer-to-Octet-String per RFC 8017 §4.1
  • 8.3 src/math/os2ip.ts — Octet-String-to-Integer per RFC 8017 §4.2
  • 8.4 src/math/crt.ts — RFC 8017 §5.1.2 second-form (m = m2 + h*q)

9. Implementation — MGF1 + encoding methods

  • 9.1 src/encoding/mgf1.ts — RFC 8017 §B.2.1; throws MGF_FAILURE on maskLen > 2^32 * hLen
  • 9.2 src/encoding/emsa-pkcs1-v1_5.ts — RFC 8017 §9.2 with the four DigestInfo prefix constants (SHA-1 / 256 / 384 / 512) literal-byte verified
  • 9.3 src/encoding/emsa-pss-encode.ts — RFC 8017 §9.1.1
  • 9.4 src/encoding/emsa-pss-verify.ts — RFC 8017 §9.1.2; constant-time compare via @suaveplan/crypto-utils

10. Implementation — signatures

  • 10.1 src/sign/pkcs1v15.ts — RFC 8017 §8.2.1 (CRT-aware)
  • 10.2 src/verify/pkcs1v15.ts — RFC 8017 §8.2.2; constant-time encoded-message compare
  • 10.3 src/sign/pss.ts — RFC 8017 §8.1.1 with injectable Rng from @suaveplan/types
  • 10.4 src/verify/pss.ts — RFC 8017 §8.1.2

11. Implementation — encryption

  • 11.1 src/encrypt/pkcs1v15.ts — RFC 8017 §7.2.1
  • 11.2 src/decrypt/pkcs1v15.ts — RFC 8017 §7.2.2; combined CRYPTO_RSA.DECRYPT_FAILED (Bleichenbacher defence)
  • 11.3 src/encrypt/oaep.ts — RFC 8017 §7.1.1
  • 11.4 src/decrypt/oaep.ts — RFC 8017 §7.1.2; combined error

12. Implementation — JWK thumbprint

  • 12.1 src/jwk/thumbprint.ts — RFC 7638 §3.2 with RSA member ordering e, kty, n
  • 12.2 Worked-example match against RFC 7638 §3.1 reference "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"

12a. Implementation — SPKI fingerprint

  • 12a.1 src/fingerprint/spki.ts — computeSpkiFingerprint(publicKey, { hash? = "SHA-256" }): Uint8Array
  • 12a.2 src/fingerprint/format.ts — formatFingerprint(bytes, format) for hex / base64 / base64url / colon-hex
  • 12a.3 Failure to derive SPKI bytes throws CRYPTO_RSA.FINGERPRINT_FAILED

12b. Implementation — PEM framing (RFC 7468)

  • 12b.1 src/pem/parse-public.ts — accepts -----BEGIN PUBLIC KEY----- (SPKI) + -----BEGIN RSA PUBLIC KEY----- (PKCS#1)
  • 12b.2 src/pem/parse-private.ts — accepts -----BEGIN PRIVATE KEY----- (PKCS#8) + -----BEGIN RSA PRIVATE KEY----- (PKCS#1)
  • 12b.3 src/pem/serialize-public.ts + src/pem/serialize-private.ts — emit 64-char-line base64 body per RFC 7468 §3
  • 12b.4 Mismatched BEGIN / END, unknown label, or non-base64 body throws CRYPTO_RSA.PEM_MALFORMED

12c. Implementation — key generation

  • 12c.1 src/keygen/miller-rabin.ts — isProbablePrime(n, rng, rounds? = 64): boolean per NIST FIPS 186-5 Appendix B.3.1
  • 12c.2 src/keygen/exponent.ts — validatePublicExponent(e) (odd, 1 < e < 2^256, coprime with λ(n))
  • 12c.3 src/keygen/generate.ts — generateRsaKeyPair({ modulusBits?, publicExponent?, rng, primes?, signal?, profile? }): Promise<RsaPrivateKey>; defaults modulusBits: 3072, publicExponent: 65537n, primes: 2, profile: "permissive"
  • 12c.4 src/keygen/multi-prime.ts — multi-prime support per RFC 8017 §3 (primes ∈ {3, 4, 5}); serialises to PKCS#1 RSAPrivateKey version 1 with non-empty otherPrimeInfos
  • 12c.5 AbortSignal threaded into prime-search loop; abort interrupts within ≤ 100 ms wall-clock and throws CRYPTO_RSA.ABORTED
  • 12c.6 Miller-Rabin exhaustion throws CRYPTO_RSA.KEYGEN_FAILED
  • 12c.7 Deterministic mode produces byte-identical keys under identical seeded rng

12d. Implementation — blinding (RFC 8017 §5.1.2)

  • 12d.1 src/blinding/blinding.ts — blindMessage(m, key, rng): { mPrime, rInverse } + unblindResult(s, rInverse, key): bigint
  • 12d.2 Wired into every rsaSign* / rsaDecrypt* call by default
  • 12d.3 Opt-out via { blinding: false } emits crypto_rsa.blinding_disabled_total{operation} counter increment
  • 12d.4 Mitata bench blinded_vs_unblinded_2048 confirms < 5% overhead

12e. Implementation — RsaCryptoEngine adapter

  • 12e.1 src/engine/engine.types.ts — RsaCryptoEngine interface (sign, verify, encrypt, decrypt, modPow)
  • 12e.2 src/engine/noble.ts — default engine over @noble/curves/abstract/modular.pow
  • 12e.3 src/engine/subtle.ts — globalThis.crypto.subtle adapter; non-extractable keys by default; { extractable: true } opt-in emits warning counter
  • 12e.4 src/engine/select.ts — defaultEngine(operation, key) auto-selects subtle when available else noble; emits crypto_rsa.engine_selected_total{engine}
  • 12e.5 subtle requested but unavailable throws CRYPTO_RSA.ENGINE_UNAVAILABLE

12f. Implementation — HSM / PKCS#11 bridge

  • 12f.1 src/handle/handle.types.ts — RsaKeyHandle + RsaKeyHandleProvider interfaces
  • 12f.2 src/handle/registry.ts — registerKeyHandleProvider(provider) + resolveKeyHandle(key)
  • 12f.3 src/handle/software.ts — SoftwareRsaKeyHandle reference implementation

12g. Implementation — FIPS 186-5 profile

  • 12g.1 src/profile/profile.ts — enforceProfile(input, { profile }) with permissive and fips-186-5 modes
  • 12g.2 fips-186-5 rejects modulusBits ∉ {2048, 3072, 4096}, publicExponent ∉ [2^16+1, 2^256-1] or even, PSS saltLength ≠ hLen, hash === "SHA-1" in sign / verify — each throws CRYPTO_RSA.PROFILE_VIOLATION with rule attribute

12h. Implementation — JWK key_ops / use / alg enforcement

  • 12h.1 src/keys/jwk-policy.ts — enforceJwkPolicy(jwk, operation)
  • 12h.2 Wired into every public surface (rsaSign*, rsaVerify*, rsaEncrypt*, rsaDecrypt*)
  • 12h.3 Cross-policy violation throws CRYPTO_RSA.KEY_OP_NOT_PERMITTED

12i. Implementation — modulus-size DoS guard

  • 12i.1 src/policy/modulus.ts extended with enforceMaxBitLength(key, { allowOversize? }) rejecting bitLength > 16384 with CRYPTO_RSA.MODULUS_TOO_LARGE

13. Implementation — errors + observability

  • 13.1 src/errors/errors.ts — defineCodes("CRYPTO_RSA", ["MODULUS_TOO_SHORT", "MODULUS_TOO_LARGE", "INVALID_KEY", "INVALID_SIGNATURE", "SIGNATURE_INVALID", "UNSUPPORTED_HASH", "UNSUPPORTED_FORMAT", "DECRYPT_FAILED", "MGF_FAILURE", "KEYGEN_FAILED", "PRIME_INVALID", "KEY_OP_NOT_PERMITTED", "PROFILE_VIOLATION", "ENGINE_UNAVAILABLE", "PEM_MALFORMED", "FINGERPRINT_FAILED", "ABORTED"]); CryptoRsaError extends SuaveplanError<CryptoRsaErrorCode>
  • 13.2 Every code (17) thrown by ≥1 production path in src/
  • 13.3 src/observability/metrics.ts emits:
    • histograms crypto_rsa.verify_duration_ms{scheme,hash,engine} / sign_duration_ms{scheme,hash,engine} / encrypt_duration_ms{scheme,hash,engine} / decrypt_duration_ms{scheme,hash,engine} / keygen_duration_ms{modulus_bits,primes} / modpow_duration_ms{operation,blinded}
    • counters crypto_rsa.outcome_total{operation,outcome} / crypto_rsa.errors_total{code} / crypto_rsa.engine_selected_total{engine} / crypto_rsa.fingerprint_total{format} / crypto_rsa.profile_violation_total{rule} / crypto_rsa.blinding_disabled_total{operation}

14. Root barrel + subpath exports

  • 14.1 src/index.ts — explicit named re-exports (no export *)
  • 14.2 package.json exports map matches subpath plan in §2.6
  • 14.3 Verify subpath count matches feature module count (§13 step 8)

15. Test-vector generator + fixtures

16. Tests (runner-neutral imports — @suaveplan/testing/runner)

  • 16.1 keys/parse-pkcs1-public.test.ts — RFC 8017 Appendix A.1.1 round-trip; malformed → INVALID_KEY
  • 16.2 keys/parse-pkcs1-private.test.ts — Appendix A.1.2 round-trip
  • 16.3 keys/parse-spki.test.ts — rsaEncryption OID accept; wrong OID reject
  • 16.4 keys/parse-pkcs8.test.ts — RFC 5208 + RFC 5958 shapes accept
  • 16.5 keys/parse-jwk.test.ts — kty: "RSA" accept; kty: "EC" reject (UNSUPPORTED_FORMAT)
  • 16.6 keys/serialize-*.test.ts — every parse path's inverse, byte-exact
  • 16.7 keys/roundtrip.spec-conformance.test.ts — property: random valid key → serialise → parse → equal
  • 16.8 policy/modulus.test.ts — 2048-bit accept; 1024-bit reject under defaults; 1024-bit accept under allowLegacy: true; 512-bit always reject
  • 16.9 math/modpow.test.ts — matches naive BigInt reference on small inputs; source-grep gate BigInt \*\* returns zero
  • 16.10 math/crt.test.ts — CRT output equals naive c^d mod n on Appendix C vectors
  • 16.11 encoding/mgf1.test.ts — Appendix C MGF1 outputs match; oversize maskLen throws MGF_FAILURE
  • 16.12 encoding/emsa-pkcs1-v1_5.test.ts — SHA-1 / 256 / 384 / 512 DigestInfo prefixes match RFC 8017 §9.2 note 1 byte-exactly
  • 16.13 encoding/emsa-pss-encode.test.ts + encoding/emsa-pss-verify.test.ts — Appendix C round-trip; bit-flipped encoding rejects
  • 16.14 verify/pkcs1v15.test.ts — Appendix C accept; SHA-384 / 512 / 1 accept; bit-flipped reject
  • 16.15 verify/pss.test.ts — Appendix C accept; custom saltLength; custom mgfHash; bit-flipped reject
  • 16.16 sign/pkcs1v15.test.ts — Appendix C reference signature matches (CRT path)
  • 16.17 sign/pss.test.ts — round-trip across SHA-256 / 384 / 512; deterministic with fixed RNG
  • 16.18 encrypt/pkcs1v15.test.ts + decrypt/pkcs1v15.test.ts — Appendix C round-trip; bad padding → DECRYPT_FAILED indistinguishable from value-error
  • 16.19 encrypt/oaep.test.ts + decrypt/oaep.test.ts — Appendix C round-trip; non-empty label round-trip
  • 16.20 jwk/thumbprint.test.ts — RFC 7638 §3.1 worked example matches
  • 16.21 fingerprint/spki.test.ts + fingerprint/format.test.ts — SHA-256 over RFC 8017 Appendix C SPKI; four output formats produce expected strings
  • 16.22 pem/parse-public.test.ts + pem/parse-private.test.ts + pem/serialize-public.test.ts + pem/serialize-private.test.ts — all four armor labels round-trip; malformed armor → PEM_MALFORMED
  • 16.23 keygen/miller-rabin.test.ts — known primes accepted, composites rejected; rounds: 64 honoured
  • 16.24 keygen/generate.test.ts — sign-then-verify round-trip on modulusBits ∈ {2048, 3072}; deterministic mode byte-identical under seeded RNG; AbortSignal interrupts within 100 ms → ABORTED; Miller-Rabin exhaustion → KEYGEN_FAILED
  • 16.25 keygen/multi-prime.test.ts — primes: 3 produces valid 3-prime key with non-empty otherPrimeInfos; sign-then-verify round-trip
  • 16.26 keygen/exponent.test.ts — even e rejects; e === 2n rejects; e === 65537n accepts
  • 16.27 blinding/blinding.test.ts — blindMessage → private-op → unblindResult equals naive private-op on Appendix C ciphertexts; opt-out emits blinding_disabled_total increment
  • 16.28 engine/select.test.ts — subtle selected when present; noble fallback when absent; explicit subtle against unavailable → ENGINE_UNAVAILABLE
  • 16.29 engine/noble.test.ts + engine/subtle.test.ts — both engines produce verify-equivalent outputs on Appendix C inputs
  • 16.30 handle/registry.test.ts + handle/software.test.ts — provider register / resolve round-trip; SoftwareRsaKeyHandle matches direct call
  • 16.31 profile/profile.test.ts — fips-186-5 rejects 1024-bit, e = 3, hash: "SHA-1" in sign, PSS saltLength: 0; permissive accepts all
  • 16.32 keys/jwk-policy.test.ts — key_ops / use / alg cross-policy rejects with KEY_OP_NOT_PERMITTED
  • 16.33 policy/modulus-max.test.ts — 16384-bit accepts; 32768-bit rejects with MODULUS_TOO_LARGE; 32768-bit accepts under allowOversize: true
  • 16.34 wycheproof/replay.test.ts — every valid accepts; every invalid rejects; every acceptable documented
  • 16.35 cavp/replay.test.ts — every NIST CAVP RSA vector replayed
  • 16.36 errors/errors.test.ts — every code (17) in the registry thrown by ≥1 production path (audit table)
  • 16.37 observability/metrics.test.ts — every histogram + counter asserted with real in-memory MeterProvider, including engine_selected_total, fingerprint_total, profile_violation_total, blinding_disabled_total
  • 16.38 schemas/schemas.test.ts — Zod boundary rejects malformed inputs with the right error code
  • 16.39 crypto-rsa.spec-conformance.test.ts — Appendix C end-to-end; property test (sign / verify, encrypt / decrypt, parse / serialise round-trips across every format); verifiable-randomness determinism property; BigInt \*\* grep gate; every "MUST reject" path
  • 16.40 index.test.ts — every documented subpath resolves
  • 16.41 Every it(...) carries { timeout: 5000 } (or 30000 for 4096-bit modpow / keygen)

17. Documentation

  • 17.1 Sibling .md for every implementation .ts (≥200 words, basic + advanced example, full API reference)
  • 17.2 README links every co-located .md under ## Modules
  • 17.3 Update root CLAUDE.md package listing under "Crypto family"

18. Benchmarks

  • 18.1 src/__benchmarks__/crypto-rsa.bench.ts — mitata covering:
    • verify_pkcs1_sha256_{2048,3072,4096} × engine ∈ {noble, subtle}
    • verify_pss_sha256_{2048,3072,4096} × engine
    • sign_pkcs1_sha256_{2048,3072,4096} × engine
    • sign_pss_sha256_{2048,3072,4096} × engine
    • encrypt_oaep_sha256_{2048,3072,4096} × engine
    • decrypt_oaep_sha256_{2048,3072,4096} × engine
    • generate_{2048,3072,4096} (noble only)
    • parse_{pkcs1,pkcs8,spki,jwk,pem}_2048
    • crt_vs_naive_2048 ratio
    • blinded_vs_unblinded_2048 ratio
  • 18.2 Baseline numbers published in README
  • 18.3 CRT path measurably ≥ 2× faster than non-CRT path on the 2048-bit sign benchmark
  • 18.4 Blinding overhead < 5% of unblinded private-op latency on the 2048-bit bench

19. Pre-archive verification (the 5-gate triple)

  • 19.1 bun run check — Biome 0 errors / 0 warnings / 0 info
  • 19.2 bunx turbo lint typecheck test --filter=@suaveplan/crypto-rsa — green
  • 19.3 bunx turbo test:coverage --filter=@suaveplan/crypto-rsa — 100/100/100/100
  • 19.4 bun scripts/openspec-conformance.ts @suaveplan/crypto-rsa — exit 0, no error findings
  • 19.5 bunx turbo build --filter=@suaveplan/crypto-rsa — green

20. Pre-archive 7-row checklist (AGENTS.md Section 13)

  • 20.1 Generics threaded; expectTypeOf assertions on RsaPublicKey / RsaPrivateKey discriminants and on options-object literal types
  • 20.2 Zod schemas wired at every declared boundary (RsaPublicKeySchema, RsaPrivateKeySchema, every options schema)
  • 20.3 Every error code in errors/errors.ts thrown by at least one production path
  • 20.4 Strict-mode behaviors tested both on and off (allowLegacy modulus-policy on + off; CRT vs naive private-op equivalence)
  • 20.5 OTel instruments emitted from production paths and asserted with a real in-memory MeterProvider
  • 20.6 Lifecycle dedup — N/A (sign / verify / encrypt / decrypt are stateless; document explicitly)
  • 20.7 No retroactive tick — every box above reflects work done in this change

21. Root-doc refresh + markdownlint (§13b)

Deferred — root docs (MANIFEST.md, MISSING.yml, STATUS.md, SUMMARY.md, CLAUDE.md) are refreshed by the orchestrator after parallel crypto-family migrations land.

  • 21.1 bun scripts/folder-reorg/generate-manifest.ts — refresh MANIFEST.md (orchestrator)
  • 21.2 bun scripts/generate-missing.ts — refresh MISSING.yml (orchestrator)
  • 21.3 STATUS.md — move crypto-rsa from Pending → Completed (orchestrator)
  • 21.4 SUMMARY.md — add per-package paragraph (orchestrator)
  • 21.5 bunx markdownlint-cli2 --fix — apply auto-fixes (orchestrator)
  • 21.6 bunx markdownlint-cli2 (no --fix) — exit 0 (orchestrator)

22. Archive

  • 22.1 Fold spec delta into openspec/specs/crypto-rsa/spec.md
  • 22.2 Move openspec/changes/enterprise-crypto-rsa/ → openspec/archive/<yyyy-mm-dd>-enterprise-crypto-rsa/
  • 22.3 Refresh MANIFEST.md if package set or deps changed (orchestrator)