Skip to content

Goal: Email Integration Phase 5a -- Component Library Updates

Ship five additive helpers in common-module that the Phase 5b Email module (operations) will consume to build its L1/L2/L3/L4 surfaces correctly. Phase 5a is a library-only phase — no runtime infrastructure changes, no consumer wiring. All five helpers are designed to be reusable beyond the email use case; the email module is the first consumer but not the last.

This phase is the common-module-side counterpart of Phase 5b. Phase 5a’s outputs (a sequence of common-module releases) become Phase 5b’s inputs (a single gradle.properties bump). The two phases can be planned in parallel; the only hard sequencing is that Phase 5b’s implementation merge waits on Phase 5a’s GitHub Packages publish of the final release.

  • PDEV-201 — Notifications / E-mail Pipeline: Project umbrella covering Phase 5a (and the broader Email Integration project). All five Phase 5a PRs reference this ticket; manual transitions only (no auto-transition from PR bodies).
RepositoryRolePlanned Changes
common-modulePrimaryFive additive PRs (four Added-only minors plus one Changed major). New packages: lib/api/headers/ (sanitizeHeader), lib/crypto/ (TokenCipher, Hmac), lib/runtime/idempotency/ (RawIdempotencyStore, IdempotencyStore, IdempotencyKeyMinter). Extended: lib/lang/errors/AppError.kt (third top-level branch). Reclassified: 62+ Internal.IncompatibleState construction sites swept against the new Application.ConflictingState / Invocation.GeneralValidation buckets.
documentationPrimaryPhase 5a planning artefacts (this directory), decision-log entries DQ-R1-027 through DQ-R1-031, completion byproducts at phase end.
operationsOut of scope for Phase 5aConsumer-side adoption (lifting the new common-module version, applying the email-module-scoped IncompatibleState sweep, wiring the typed idempotency view) is Phase 5b’s responsibility.
infrastructureOut of scope for Phase 5aAll infrastructure dependencies are already satisfied by Phase 4’s deliverables.

Phase 5a is complete when all of the following are true:

  1. All five common-module releases published. PRs #1, #3, #4, #5 land as Added-only minor releases (9.2.0 -> 9.3.0 -> 9.4.0 -> 9.5.0); PR #2 (the IncompatibleState sweep) lands last as a Changed major release (10.0.0). Each release is available on the workspace’s GitHub Packages repository for consumer gradle.properties bumps.
  2. AppError.Application is live. The hierarchy gains sealed class Application with three subtypes (PreconditionFailed, PolicyRejected, ConflictingState); reportable() returns empty list for the branch; existing tests pass; new tests assert the three subtype constructors and the empty-reportable contract. (DQ-R1-027)
  3. IncompatibleState sweep complete in common-module. Every construction site of Internal.IncompatibleState in common-module/lib/src/main has been audited and either kept (genuine bug-class invariant violation), reclassified to Application.ConflictingState (recoverable application outcome), or reclassified to Invocation.GeneralValidation (caller error). The classification rationale is captured per-bucket in the implementation PR’s description. Existing test suites pass after the sweep. (DQ-R1-028)
  4. sanitizeHeader is live. New package lib/api/headers/ exposes sanitizeHeader(name, value): Result<String?> returning accept / silent-drop / hard-reject outcomes. Unit tests cover the value-rejection cases (control characters, oversize, charset). The composition pattern with HeadersAllowList is documented. (DQ-R1-029)
  5. TokenCipher and Hmac are live. New package lib/crypto/ exposes TokenCipher (the two-axis envelope from DQ-R1-019) and Hmac (HmacSHA256 wrapper). The two existing JDK-Mac call sites (OpaqueId.kt:67, S3AssetService.kt:143) are internally refactored to use Hmac (no external API change). Unit tests cover encrypt/decrypt round-trip across material-version transitions, the auth-tag-mismatch -> Internal.IncompatibleState classification, and the unknown-versionId -> Transient.FailoverFailed classification. (DQ-R1-030)
  6. Idempotency helpers are live. New package lib/runtime/idempotency/ exposes RawIdempotencyStore (native JsonElement boundary, symmetric Req/Res), IdempotencyStore<Req, Res> (typed wrapper via inline fun typedAs()), IdempotencyStoreFactory, and IdempotencyKeyMinter. ContainerizedPostgres integration tests cover the full begin / commit / failPermanent / failTransient lifecycle, the Mismatch detection path (with recordedRequest: JsonElement carried), the InFlight outcome under concurrent begin, and purgeExpired. The Flyway migration creating the idempotency_record table is not shipped by common-module; it ships in Phase 5b’s consumer adoption. (DQ-R1-031)
  7. All five decision-log entries recorded. DQ-R1-027 through DQ-R1-031 exist in the global decision-log.md, each with context, options, recommendation, decision, and applied-to sections.
  8. common-module CHANGELOG carries one entry per PR. Each release line in CHANGELOG.md matches the workspace direct-edit changelog convention (one new entry per PR; descriptive of intent and outcome; not a file enumeration).
  • Per-partition mail surface live across all four active partitions (dev, stage, demo, prod). Phase 4 delivered the platform-side: partition mail sub-zones, per-partition partition-email stack, per-partition EmailEncryptionKey Secrets Manager secret, per-partition DNS-provisioning + SM-fallback IAM roles, runtime-platform-drift workflow.
  • common-module at version 9.x.x — the workspace’s shared Kotlin library; see common-module/CHANGELOG.md on origin. Already in production use by operations. No Phase 5a-relevant API yet.
  • Existing helpers and patterns Phase 5a builds on:
    • cards.arda.common.lib.lang.errors.AppError — the existing two-branch hierarchy (Internal, Invocation) that Phase 5a extends.
    • cards.arda.common.lib.lang.serialization.JsonConfig.standardJson — the canonical kotlinx Json instance Phase 5a’s idempotency helpers and error_payload projection use. Refinable via JsonConfig.refine { ... } for variants (e.g., prettyPrint = false for canonical hashing).
    • cards.arda.common.lib.persistence.keystore.DatabaseBackedMap — the factory shape pattern (inTransaction / inConnection / withTx returning Result<T>) that the idempotency factory mirrors.
    • cards.arda.common.lib.api.rest.server.service.Message — the companion inline operator fun <reified T : Any> invoke(...) factory pattern, reused for the idempotency-store typed wrapper but not for TokenCipher (non-generic; uses plain companion operator fun invoke instead).
    • cards.arda.common.lib.runtime.observability.HeadersAllowList — the deny-by-name observability filter that sanitizeHeader composes downstream of.
    • cards.arda.common.lib.runtime.observability.OpaqueId and cards.arda.common.lib.infra.storage.S3AssetService — the two existing JDK-Mac HmacSHA256 call sites that the new Hmac helper DRYs.
    • cards.arda.common.lib.testing.persistence.ContainerizedPostgres — the integration-test pattern (real PostgreSQL container) for any persistence-shaped helper, used by the idempotency store’s lifecycle tests.

The Phase 5b Email module is the proximate consumer of these helpers, but each helper is independently useful. Shipping them in common-module (the workspace’s shared library) instead of inside the Email module (a Phase 5b runtime artifact) means:

  • Future modules that need an idempotency store, encrypted-field primitive, sanitised-header reader, or Application-class error reuse the same primitives without a rebuild.
  • Phase 5a and Phase 5b can be reviewed independently. Phase 5b reviewers focus on the Email module’s correctness, not on whether TokenCipher’s envelope format is sound (that’s a Phase 5a review).
  • The interface contract between the two phases is a versioned common-module artifact rather than internal Email-module code, making future cross-consumer compatibility easier to reason about.

Five PRs are sequenced per task-plan.md. Four are Added-only minor bumps (PRs #1, #3, #4, #5 -> 9.2.0, 9.3.0, 9.4.0, 9.5.0) and can be reviewed in any order; the fifth (PR #2 — the IncompatibleState sweep) is Changed and lands last as a major bump (10.0.0). The major-last sequencing means Phase 5b’s consumer adoption PR sees one combined gradle.properties bump to 10.0.0 carrying the full Phase 5a payload.

  • Phase 5b consumer adoption — lifting common-module to 10.0.0, applying the email-module-scoped IncompatibleState sweep, wiring the typed idempotency view to the L3 services in the Email module. Owned by Phase 5b.
  • The idempotency_record Flyway migration — ships in Phase 5b’s consumer adoption (operations), not in common-module. common-module does not own production-side migrations.
  • HKDF exposure as a standalone helper — deferred. HKDF stays internal to TokenCipher in v1. The workbook tracks this as DT-003 (deferred); promote when a non-TokenCipher consumer surfaces.
  • AppError-Application decision-record extraction into per-helper files — the workbook keeps 5a/decisions/dt-002-*.md files for local record-keeping; the canonical decisions live in this directory’s decision-log.md.
  • decision-log.md — entries DQ-R1-027 through DQ-R1-031 for the Phase 5a decisions; DQ-R1-019 for the encryption-envelope source design.
  • design/index.md — canonical Phase 5a design (four helpers in §§ 1-4; § 5 pointer to idempotency).
  • design/idempotency-design.md — carved-out implementation-shaped design for the idempotency helpers.
  • task-plan.md — six PRs (one documentation promotion + five common-module releases), sequencing, worktree strategy, and acceptance criteria.
  • Inherited constraints (DQ-201..208 application-layer set, DQ-012 per-tenant token storage, DQ-R1-019 per-partition encryption key) live in the global decision-log.md; the new Phase 5a entries DQ-R1-027..031 cite them where they apply. The four constraints the design honours implicitly (DQ-204 STS, DQ-206 no-plaintext, DQ-208 tx-ownership, Cross-Universe rule) are restated in design/index.md § 6.4.