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

15 KiB
Raw Blame History

Tasks — @suaveplan/jws (RFC 7515 JSON Web Signature: verify + sign)

Every checkbox below MUST reflect work actually done in this change. Do not retroactively tick boxes. 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/jws.md end-to-end
  • 1.2 Read RFC 7515 (JWS), RFC 7517 (JWK), RFC 7518 (JWA), RFC 8037 (CFRG curves in JOSE), RFC 7797 (b64) end-to-end
  • 1.3 Read openspec/AGENTS.md Sections 13, 14, 14a
  • 1.4 Confirm proposal.md covers every public-surface item, every subpath, every dependency
  • 1.5 Confirm specs/jws/spec.md carries one requirement per logical capability with ≥2 scenarios each
  • 1.6 Confirm every requirement has a **Verified by:** line naming concrete it(...) titles in concrete test file paths
  • 1.7 Source-mapping table cites every RFC section consumed

2. Package scaffold

  • 2.1 Create packages/security/jws/ with src/{base64url,serialization,header,algorithms,keys,detached,verify,sign,errors,observability,schemas,__benchmarks__}/
  • 2.2 package.json — @suaveplan/jws@0.0.0, type: module, sideEffects: false, deps @suaveplan/x509, @suaveplan/crypto-curve, @suaveplan/crypto-hash, @suaveplan/crypto-utils, @suaveplan/error, @suaveplan/types, @opentelemetry/api, zod, tslib via catalog refs only (no bare versions)
  • 2.3 package.json exports map declares ., ./serialization/compact, ./serialization/flat, ./serialization/general, ./header, ./algorithms, ./keys, ./detached, ./base64url, ./errors (10 subpaths) — each mapped to its built dist/<feature>/<feature>.js + .d.ts pair
  • 2.4 tsconfig.lib.json extends @suaveplan/typescript-config/library.json
  • 2.5 vite.config.ts multi-entry build (one per subpath) + vitest.config.ts calling createTestConfig() from @suaveplan/testing/config
  • 2.6 README.md with package name, description, bun add install, quick-start example (compactVerify of an Appendix A.1 token), Modules table linking every co-located .md

3. Implementation — base64url

  • 3.1 src/base64url/base64url.ts — base64urlEncode(bytes): string and base64urlDecode(str): Uint8Array per RFC 7515 Appendix C (no padding, no +//)
  • 3.2 Malformed input throws JWS.BASE64URL_INVALID
  • 3.3 src/base64url/base64url.test.ts — Appendix C round-trip + every malformed-alphabet case

4. Implementation — serializations

  • 4.1 src/serialization/compact.ts — compactDecode, compactVerify, compactSign per RFC 7515 §3.1 + §7.1
  • 4.2 src/serialization/flat.ts — flatDecode, flatVerify, flatSign per RFC 7515 §3.2 + §7.2.2
  • 4.3 src/serialization/general.ts — generalDecode, generalVerify, generalSign per RFC 7515 §3.2 + §7.2.1
  • 4.4 src/serialization/serialization.types.ts — CompactJws, FlatJws, GeneralJws, JwsSignature, JwsSerialization discriminator
  • 4.5 Signing input formation per RFC 7515 §5.1 step 5 (with RFC 7797 b64: false branch)

5. Implementation — headers

  • 5.1 src/header/header.ts — parseHeader, serializeHeader; every RFC 7515 §4.1 registered parameter
  • 5.2 src/header/crit.ts — enforceCrit(header, acceptableCriticalHeaders); empty / non-array / unknown entry rejections
  • 5.3 src/header/b64.ts — RFC 7797 b64 parameter; MUST appear in crit when false; signing input branch

6. Implementation — algorithms

  • 6.1 src/algorithms/registry.ts — AlgorithmRegistry.register / lookup / lookupOrThrow; built-ins registered at module load
  • 6.2 src/algorithms/hmac.ts — HS256/HS384/HS512 per RFC 7518 §3.2; constantTimeEqual over tag bytes
  • 6.3 src/algorithms/rsa.ts — RS256/RS384/RS512 per RFC 7518 §3.3
  • 6.4 src/algorithms/rsa-pss.ts — PS256/PS384/PS512 per RFC 7518 §3.5; salt length = hash output length
  • 6.5 src/algorithms/ecdsa.ts — ES256/ES384/ES512 per RFC 7518 §3.4; fixed-length R||S; ASN.1 DER input rejected
  • 6.6 src/algorithms/eddsa.ts — EdDSA (Ed25519) per RFC 8037 §3.1
  • 6.7 src/algorithms/none.ts — "none" per RFC 7518 §3.6; rejected unless algorithms: ["none"] AND acceptNone: true

7. Implementation — keys

  • 7.1 src/keys/jwk.ts — jwkToCryptoKey(jwk, alg) per RFC 7517 §4 + RFC 7518 §6; kty/alg consistency check
  • 7.2 src/keys/x5c.ts — extractX5cLeafKey; chain delegated to @suaveplan/x509.validateChain; leaf SPKI extraction + alg match
  • 7.3 src/keys/resolver.ts — KeyResolver type + helpers; kid-driven lookup; missing key → KEY_NOT_FOUND

8. Implementation — detached payload + RFC 7797 b64

  • 8.1 src/detached/detached.ts — RFC 7515 §A.5 / Appendix F detached helpers; empty middle segment on sign; detachedPayload injection on verify
  • 8.2 RFC 7797 b64: false path tested with both compact and flat serializations

9. Implementation — verify + sign pipelines

  • 9.1 src/verify/verify-pipeline.ts — parse header → enforce crit → enforce alg allowlist → enforce alg !== "none" unless opted in → resolve key (explicit → jwk → x5c → kid) → recompute signing input → dispatch to algorithm verify → return VerifyResult
  • 9.2 src/sign/sign-pipeline.ts — validate options → resolve algorithm → assemble protected header → compute signing input → dispatch to algorithm sign → assemble serialization
  • 9.3 AbortSignal honored in both pipelines; pre-aborted signal throws JWS.ABORTED; abort between phases throws JWS.ABORTED

10. Implementation — errors, schemas, observability

  • 10.1 src/errors/error-codes.ts — defineCodes("JWS", [...]) 18-code registry: MALFORMED, HEADER_MALFORMED, BASE64URL_INVALID, ALG_UNSUPPORTED, ALG_NOT_ALLOWED, ALG_NONE_DISABLED, SIG_INVALID, SIG_FORMAT_INVALID, JWK_INVALID, X5C_CHAIN_INVALID, KEY_NOT_FOUND, KEY_ALG_MISMATCH, CRIT_UNRECOGNIZED, CRIT_MALFORMED, B64_INVALID, INVALID_OPTIONS, ABORTED, MULTI_SIG_FAILED; JwsError extends SuaveplanError<JwsErrorCode>
  • 10.2 src/observability/metrics.ts — jws.verify_duration_ms{alg,outcome} + jws.sign_duration_ms{alg} histograms; jws.verify_total{alg,outcome} + jws.sign_total{alg,outcome} + jws.error_total{code} counters; outcome bounded to 4 values
  • 10.3 src/schemas/schemas.ts — Zod schemas for JoseHeader, VerifyOptions, SignOptions, FlatJws, GeneralJws, JsonWebKey; validated at every public entry point
  • 10.4 Every code in the registry is thrown by ≥1 production code path; verified by error-codes.test.ts

11. Root barrel + subpath barrels

  • 11.1 src/index.ts — explicit named re-exports (no export * in feature barrels); namespace re-exports allowed for sub-namespaces
  • 11.2 Every <feature>/index.ts uses explicit named re-exports per AGENTS.md §6a
  • 11.3 package.json exports map matches the 10 subpaths declared in §2.3

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

  • 12.1 src/base64url/base64url.test.ts — Appendix C round-trip; malformed alphabet rejection
  • 12.2 src/serialization/compact.test.ts — three-segment parse + assembly; malformed-segment rejection
  • 12.3 src/serialization/compact.spec-conformance.test.ts — RFC 7515 Appendix A.1–A.4 vectors round-trip byte-identically
  • 12.4 src/serialization/flat.test.ts — JSON-flat decode / verify / sign; Appendix A.7 vector
  • 12.5 src/serialization/general.test.ts — multi-signer encode / decode / verify; Appendix A.6 vector
  • 12.6 src/header/header.test.ts — every RFC 7515 §4.1 registered parameter parses; unknown parameter survives round-trip
  • 12.7 src/header/crit.test.ts — empty / non-array / unknown / allowlisted entry behaviours
  • 12.8 src/header/b64.test.ts — b64: false without crit rejected; with crit round-trips; signing input uses raw payload
  • 12.9 src/algorithms/registry.test.ts — built-ins registered at load; register adds; double-register throws
  • 12.10 src/algorithms/hmac.test.ts — HS256/HS384/HS512 IANA vectors; tampered tag rejected
  • 12.11 src/algorithms/rsa.test.ts — RS256/RS384/RS512 Appendix A.2 vector + IANA vectors
  • 12.12 src/algorithms/rsa-pss.test.ts — PS256/PS384/PS512 round-trip; salt-length mismatch rejected
  • 12.13 src/algorithms/ecdsa.test.ts — ES256/ES384/ES512 Appendix A.3 + IANA P-384/P-521 vectors; DER input rejected with SIG_FORMAT_INVALID
  • 12.14 src/algorithms/eddsa.test.ts — RFC 8037 Appendix A.4 vector verifies; round-trip with generated keypair
  • 12.15 src/algorithms/none.test.ts — default rejection; explicit two-flag opt-in accepts; missing either flag throws ALG_NONE_DISABLED
  • 12.16 src/keys/jwk.test.ts — every supported kty parses; alg/kty mismatch throws JWK_INVALID
  • 12.17 src/keys/x5c.test.ts — leaf SPKI extracted; chain delegated to @suaveplan/x509.validateChain with real fixture chain; invalid chain throws X5C_CHAIN_INVALID; leaf-alg mismatch throws KEY_ALG_MISMATCH
  • 12.18 src/keys/resolver.test.ts — resolver awaited when kid set; resolver returning undefined throws KEY_NOT_FOUND
  • 12.19 src/detached/detached.test.ts — Appendix F vectors round-trip; missing detachedPayload at verify throws
  • 12.20 src/verify/verify-pipeline.test.ts — pipeline ordering; allowlist enforced before any crypto runs; crit enforced before alg dispatch
  • 12.21 src/sign/sign-pipeline.test.ts — sign pipeline round-trip per algorithm
  • 12.22 src/errors/error-codes.test.ts — every code in the registry thrown by ≥1 production path; expectTypeOf pins literal union
  • 12.23 src/observability/metrics.test.ts — histograms + counters recorded under in-memory MeterProvider; outcome attribute bounded
  • 12.24 src/schemas/schemas.test.ts — every Zod boundary rejects malformed input
  • 12.25 src/verify/property.test.ts — DEFERRED: property-style fast-check suite not authored; deterministic round-trip tests already exercise sign→decode→verify per algorithm.
  • 12.26 src/verify/abort.test.ts — pre-aborted signal throws JWS.ABORTED; abort between phases throws
  • 12.27 src/index.test.ts — every documented subpath export resolves
  • 12.28 Every it(...) carries { timeout: 5000 } (or { timeout: 30000 } for the property tests)
  • 12.29 Runner-neutral imports — grep -rnE "from ['\"](vitest|bun:test)['\"]" packages/security/jws/src/ returns zero output

13. Documentation

  • 13.1 Sibling .md for every implementation .ts (≥200 words, basic + advanced examples, full API reference) — base64url.md, compact.md, flat.md, general.md, header.md, crit.md, b64.md, registry.md, hmac.md, rsa.md, rsa-pss.md, ecdsa.md, eddsa.md, none.md, jwk.md, x5c.md, resolver.md, detached.md, verify-pipeline.md, sign-pipeline.md, error-codes.md, metrics.md, schemas.md
  • 13.2 README.md links every co-located .md under ## Modules
  • 13.3 Update CLAUDE.md package listing — add the jws paragraph under the existing Security grouping

14. Benchmarks

  • 14.1 src/__benchmarks__/verify.bench.ts — HS256, RS256, ES256, EdDSA verify on 1 KiB payload
  • 14.2 src/__benchmarks__/sign.bench.ts — same four algorithms sign on 1 KiB payload
  • 14.3 src/__benchmarks__/multi-sig.bench.ts — JSON-general decode + verify with 3 signers
  • 14.4 README.md publishes baseline mitata numbers — DEFERRED: benches authored but baseline numbers not pinned in README.

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

  • 15.1 bun run check — Biome 0 errors / 0 warnings / 0 info across the monorepo
  • 15.2 bunx turbo lint typecheck test --filter=@suaveplan/jws — green
  • 15.3 bunx turbo test:coverage --filter=@suaveplan/jws — 100/100/100/100
  • 15.4 bun scripts/openspec-conformance.ts @suaveplan/jws — exit 0, no error findings
  • 15.5 bunx turbo build --filter=@suaveplan/jws — green

16. Pre-archive 7-row checklist (AGENTS.md §13)

  • 16.1 Generics threaded — AlgorithmImpl<K>, KeyResolver, VerifyResult<H>; expectTypeOf assertions on every literal-type claim (JwsErrorCode, JoseAlg, JoseHeader.crit entries)
  • 16.2 Zod schemas wired at every declared boundary (compactVerify, compactSign, flatVerify, flatSign, generalVerify, generalSign, parseHeader, jwkToCryptoKey)
  • 16.3 Every error code in error-codes.ts thrown by ≥1 production path
  • 16.4 Strict-mode behaviors tested both on (acceptNone: false default) and off (acceptNone: true with algorithms: ["none"])
  • 16.5 OTel instruments emitted from production paths and asserted with a real in-memory MeterProvider
  • 16.6 Lifecycle dedup — N/A (verify / sign are stateless; AlgorithmRegistry double-register throws and is tested)
  • 16.7 No retroactive tick — every box above reflects work done in this change

17. Banned-pattern + barrel-shape + runner-import scans

  • 17.1 grep -rn "@deprecated\|TODO\|FIXME\|ts-ignore\|ts-expect-error\|\.skip\|\.only" packages/security/jws/src/ --include="*.ts" — zero output
  • 17.2 Barrel-shape scan per AGENTS.md §13 step 4a — zero output for any <feature>/index.ts
  • 17.3 Runner-import scan per AGENTS.md §13 step 4b — zero output
  • 17.4 Missing-.md scan — zero output
  • 17.5 Bare-version scan — zero output
  • 17.6 Subpath count matches feature module count (10 subpaths: ., ./serialization/compact, ./serialization/flat, ./serialization/general, ./header, ./algorithms, ./keys, ./detached, ./base64url, ./errors)
  • 17.7 Security scan — no hardcoded secrets; test fixture keys generated deterministically via @suaveplan/crypto-curve

18. Root-doc refresh

  • 18.1 bun scripts/folder-reorg/generate-manifest.ts — DEFERRED to follow-up commit
  • 18.2 bun scripts/generate-missing.ts — DEFERRED to follow-up commit
  • 18.3 STATUS.md — DEFERRED to follow-up commit
  • 18.4 SUMMARY.md — DEFERRED to follow-up commit
  • 18.5 bunx markdownlint-cli2 --fix then a second bunx markdownlint-cli2 pass with exit 0 — DEFERRED to follow-up
  • 18.6 git status shows only intentional changes — verified locally; full root-doc refresh deferred

19. Archive

  • 19.1 Fold spec delta into openspec/specs/jws/spec.md (canonical, status ACTIVE)
  • 19.2 Move openspec/changes/enterprise-jws/ → openspec/archive/<yyyy-mm-dd>-enterprise-jws/
  • 19.3 Confirm MANIFEST.md lists @suaveplan/jws after refresh