Implementation Plan: Documint CDN Image Signing
Author: Principal Engineer
Date: 2026-04-14
Status: Draft
Related Issues: Analysis Issue 4, Decision Log FD-23 through FD-26
Task Directory: 3-frontend-implementation/36c-discovery-remediation/
Summary
Section titled “Summary”When items with images are printed, Documint’s render servers fetch the image at its CDN URL. Because CloudFront requires signed cookies (FD-10), and Documint has no mechanism to attach cookies or auth headers, image fetches return HTTP 403 and printed labels render without images.
This plan implements Option A from the analysis: at print time, the operations backend generates a CloudFront signed URL (query-string authentication) with a 15-minute TTL for each item’s product_image. The signed URL replaces the bare CDN URL in the Documint template payload, allowing Documint to fetch the image without any authentication mechanism on its side.
Context
Section titled “Context”- Phase 3.6c Analysis — Issue 4 — problem statement and options explored
- Decision Log FD-22 — dual-mode CDN auth (signed URLs already implemented in the Next.js BFF)
- The BFF’s
signUrl()in/arda-frontend-app/src/server/lib/cloudfront-signer.tsis the reference implementation for the CloudFront signing algorithm - Infrastructure stack (
ImageStorageStackin CDK) already exports the signing key secret ARN and key pair ID as CloudFormation outputs — no infrastructure changes needed
In Scope
Section titled “In Scope”- CloudFront signed URL utility in
common-module(Kotlin port of the BFF’ssignUrl()) - Helm/config wiring to deliver the signing key and key pair ID to the
operationspod - Integration into
ItemPrinter.render()andKanbanCardPrinter.render()to signproduct_imageURLs at print time (labels, breadcrumbs, and kanban cards) - Unit tests for the signer, integration tests for all three print paths
Out of Scope
Section titled “Out of Scope”- Changes to
arda-frontend-app— the BFF’s signed URL/cookie flow is unrelated - Changes to Documint templates —
product_imagealready accepts a URL; signed vs bare is transparent - Changes to
PdfRenderService,DocumintProxy,ItemPrintingService, orPrintLifecycleImpl— signing is encapsulated in the printer classes - S3 or CloudFront distribution configuration — CloudFront natively accepts both signed cookies and signed URLs
Design
Section titled “Design”CloudFront Signed URL Algorithm
Section titled “CloudFront Signed URL Algorithm”The signing algorithm follows the AWS CloudFront signed URL specification using a custom policy:
- Build a JSON custom policy:
{"Statement": [{"Resource": "<image-cdn-url>","Condition": {"DateLessThan": { "AWS:EpochTime": <expiry-epoch-seconds> }}}]}
- Sign the policy with RSA-SHA1 using the CloudFront private key (PEM)
- Encode both policy and signature with CloudFront-safe Base64 (
+to-,/to~,=to_) - Append query parameters:
?Policy=<encoded>&Signature=<encoded>&Key-Pair-Id=<id>
New Utility: CloudFrontUrlSigner (common-module)
Section titled “New Utility: CloudFrontUrlSigner (common-module)”Location: common-module/lib/src/main/kotlin/cards/arda/common/lib/infra/cdn/CloudFrontUrlSigner.kt
class CloudFrontUrlSigner( privateKeyPem: String, private val keyPairId: String, private val defaultTtl: Duration = 15.minutes) { private val privateKey: PrivateKey = parsePem(privateKeyPem) // fails at construction if PEM is invalid
fun sign(resourceUrl: String, ttl: Duration = defaultTtl): Result<String>}Construction:
- Parses PEM private key into
java.security.PrivateKeyat construction time (PKCS#8 viaKeyFactory.getInstance("RSA")) - The CDK lambda (
generate-signing-key.ts:56) generates keys withprivateKeyEncoding: { type: "pkcs8", format: "pem" }— only PKCS#8 format needs to be supported - Throws immediately on malformed PEM — this is a boot-time failure, not a runtime one
sign() returns Result<String>:
- Builds the custom policy JSON
- Signs with
Signature.getInstance("SHA1withRSA") - Encodes with CloudFront-safe Base64
- Returns
Result.successwith the URL + query parameters, orResult.failure(AppError.Infrastructure(...))on crypto errors - Implementation uses
runCatching { ... }.mapError { it.normalizeToAppError() }or equivalent to wrap JCA exceptions
The class is stateless after construction (private key is parsed once). Thread-safe — Signature instances are created per call.
TTL Configuration
Section titled “TTL Configuration”The signed URL TTL defaults to 15 minutes and is configurable through the standard extras config chain:
Default in reference/item/application.conf:
extras { printing: { maxItemsPerRequest: 200 signedUrlTtlMinutes: 15 }}Override via Helm/configmap at deploy time: the configmap surfaces system.reference.item.extras.printing.signedUrlTtlMinutes which overrides the resource default through the standard HOCON merge.
In Module.kt, the value is read alongside maxItemsPerRequest:
val signedUrlTtlMinutes = if (cfg.extras.hasPath("printing.signedUrlTtlMinutes")) cfg.extras.getInt("printing.signedUrlTtlMinutes") else 15The Duration is constructed at module wiring time and passed to the CloudFrontUrlSigner constructor as defaultTtl.
Helm and Configuration Wiring
Section titled “Helm and Configuration Wiring”Two new values need to flow from infrastructure to the operations pod. The read-cloudFormation-values.cmd file uses a DSL consumed by the Gradle helm plugin with three commands: readExport (reads a CloudFormation export value into a Helm value path), readSecretName (reads a CloudFormation export that contains a Secrets Manager ARN and extracts the secret name), and copyValue (copies a literal value).
1. CloudFront Key Pair ID (non-sensitive — configmap):
| Layer | File | Change |
|---|---|---|
| Helm values | read-cloudFormation-values.cmd | Add: readExport .system.reference.item.extras.imageCdnSigningKeyPairId ${PURPOSE}-API-ImageCdnSigningKeyId |
| ConfigMap | templates/configmap.yaml | Add: system.reference.item.extras.imageCdnSigningKeyPairId={{ .Values.system.reference.item.extras.imageCdnSigningKeyPairId | default "" }} |
The CFN export ${PURPOSE}-API-ImageCdnSigningKeyId is published by ImageStorageStack (line 72 of image-storage.ts) and contains the CloudFront public key ID string (not an ARN).
2. CloudFront Signing Key PEM (sensitive — ExternalSecret):
| Layer | File | Change |
|---|---|---|
| Helm values | read-cloudFormation-values.cmd | Add: readSecretName .system.reference.item.extras.imageCdnSigningKeySecretName ${PURPOSE}-API-ImageCdnSigningKeySecretArn |
| ExternalSecret | templates/secrets.yaml | Add ImageCdnSigningKey data entry in the main ExternalSecret, referencing .Values.system.reference.item.extras.imageCdnSigningKeySecretName; surface as system.reference.item.extras.imageCdnSigningKeyPem={{ .ImageCdnSigningKey }} in secrets.properties |
The CFN export ${PURPOSE}-API-ImageCdnSigningKeySecretArn (line 76 of image-storage.ts) is a full Secrets Manager ARN. The readSecretName command extracts the secret name portion (e.g., Alpha002-dev-ImageCdnSigningKey) for use as the ExternalSecret remoteRef.key. This follows the same pattern as pg_admin_secret_name (line 12 of read-cloudFormation-values.cmd, consumed at line 121 of secrets.yaml).
IAM Permissions (Verified)
Section titled “IAM Permissions (Verified)”The secrets reader IAM role (defined in eks-cluster.ts:396-410) grants secretsmanager:GetSecretValue on the resource pattern:
arn:aws:secretsmanager:${region}:${account}:secret:${infraId}-*The signing key secret is named ${fqn}-ImageCdnSigningKey where fqn = ${infraId}-${purpose} (e.g., Alpha002-dev-ImageCdnSigningKey). This matches the Alpha002-* wildcard pattern. No IAM changes are needed.
Note: The operations component’s own secrets use the ${Infrastructure}-${Namespace}-I-* naming convention (e.g., Alpha002-dev-operations-I-DocumintApiKey), but the ExternalSecret remoteRef.key accepts any secret name — the convention is not a constraint. The IAM policy is the binding authority, and it permits access to all ${infraId}-* secrets.
Failure Strategy
Section titled “Failure Strategy”All failures follow the Result return pattern and the single-return-point convention. Failures surface as early as possible:
Boot-time failures (fail the component startup with AppError.Infrastructure):
- Missing or empty
imageCdnSigningKeyPairIdconfig value →AppError.Infrastructure("imageCdnSigningKeyPairId is required") - Missing or empty
imageCdnSigningKeyPemsecret value →AppError.Infrastructure("imageCdnSigningKeyPem is required") - PEM parsing failure (malformed key) →
AppError.Infrastructure("Failed to parse CloudFront signing key: ...")
These are configuration errors that cannot self-heal at runtime. The component must not start if the signing infrastructure is misconfigured.
Print-time failures (propagated via Result.failure to the calling route):
CloudFrontUrlSigner.sign()returnsResult<String>— crypto failures wrap asAppError.Infrastructure("CloudFront URL signing failed: ...")ItemPrinter.render()andKanbanCardPrinter.render()returnResult— signing failures compose viaflatMap(nogetOrThrow)- The calling endpoint maps the failure to an appropriate HTTP error response
No silent degradation: a signing failure means the image will 403 at Documint, so the print request must fail explicitly rather than produce a broken PDF.
Affected Print Paths
Section titled “Affected Print Paths”There are three print types across two modules, served by two printer classes:
| Print Type | Endpoint | Printer Class | product_image Line | Module |
|---|---|---|---|---|
| Labels | POST /item/print-label | ItemPrinter | ItemPrinter.kt:95 | reference/item |
| Breadcrumbs | POST /item/print-breadcrumb | ItemPrinter | ItemPrinter.kt:95 | reference/item |
| Kanban Cards | POST /kanban-card/print-card | KanbanCardPrinter | KanbanCardPrinter.kt:62 | resources/kanban |
Execution traces:
Labels and Breadcrumbs share the same path:
ItemEndpoint.printingRoutes → service.printLabels / service.printBreadcrumbs → ItemPrintingService.Impl.print() → itemPrinter.render(EntityRecord) → ItemPrintInfo.product_image ← SIGN HERE → renderService.renderGroups(groups, ...) → DocumintProxy.render(templateId, grid, live)Kanban Cards:
KanbanCardEndpoint.print-card → service.printCards → PrintLifecycleImpl.printCards() → cardPrinter.render(KanbanCardDetails) → KanbanCardPrintInfo.product_image ← SIGN HERE → printService.renderGroups(groups, ...) → DocumintProxy.render(templateId, grid, live)Both printers produce a product_image field from item.imageUrl?.toString() ?: "". Both need the signer injected.
Module Wiring
Section titled “Module Wiring”The CloudFrontUrlSigner is built once in the item module (where the config values are scoped) and injected into both modules. This enables unit testing with a mock signer in all consumers.
Item module (reference/item/Module.kt):
In itemModule():
- Read
imageCdnSigningKeyPairIdfrom config extras (required — fail startup if missing/blank) - Read
imageCdnSigningKeyPemfrom secrets properties (required — fail startup if missing/blank) - Read
printing.signedUrlTtlMinutesfrom config extras (default: 15) - Instantiate
CloudFrontUrlSigner(pem, keyPairId, ttl)— constructor parses the PEM key; throws on invalid key format - Pass the signer to
ItemPrinter - Return the signer alongside
ItemServiceso callers can pass it to other modules
The item() function return type changes from ItemService to a structure that includes both:
data class ItemModuleResult(val service: ItemService, val urlSigner: CloudFrontUrlSigner)Main.kt wiring:
val itemResult = module { item(cfgProvider, ...) } // returns ItemModuleResultval itemService = itemResult.serviceval urlSigner = itemResult.urlSignerval kanbanService = module { kanban(cfgProvider, ..., itemService, urlSigner) }Kanban module (resources/kanban/Module.kt):
kanban()andkanbanCardModule()gain aurlSigner: CloudFrontUrlSignerparameterKanbanCardPrinteris constructed with the signer (line 77)
The signer is a required dependency in both modules, not optional. If the signing key infrastructure is not deployed, the component fails to start.
Signature changes across the codebase:
Adding CloudFrontUrlSigner as a required constructor parameter to ItemPrinter and KanbanCardPrinter will break all existing call sites and tests that construct these classes. Similarly, the item() return type change and kanban() signature change propagate through Main.kt and any test that wires these modules. The implementation agent must update:
- All existing
ItemPrinter(...)constructor calls in tests - All existing
KanbanCardPrinter(...)constructor calls in tests Main.ktmodule wiringComponentStartupTestand any other integration test that bootstraps these modulesItemService.Implconstructor if it stores/forwards the signer
In tests, the signer can be constructed with a test RSA key or mocked.
Printer Changes
Section titled “Printer Changes”Both printers receive a CloudFrontUrlSigner (non-nullable) as a constructor parameter.
ItemPrinter (reference/item/service/ItemPrinter.kt):
class ItemPrinter( val qrLookupUrlBase: String, private val urlSigner: CloudFrontUrlSigner, private val colorResolver: (ItemColor) -> Result<ColorLookup>)The render() method changes from color(...).map { ... } to color(...).flatMap { ... } so that the signing Result composes within the Result chain without using getOrThrow() (prohibited by kotlin-coding rule 5). The signing call is extracted into a helper that returns Result<String>:
private fun signImageUrl(imageUrl: java.net.URL?): Result<String> = when (imageUrl) { null -> Result.success("") else -> urlSigner.sign(imageUrl.toString()) }In render(), the body changes from:
fun render(itemRd: EntityRecord<Item, ItemMetadata>): Result<ItemPrintInfo> { // ... return color(item, supply).map { colorLookup -> ItemPrintInfo( // ... product_image = item.imageUrl?.toString() ?: "", // ... ) }}to:
fun render(itemRd: EntityRecord<Item, ItemMetadata>): Result<ItemPrintInfo> { // ... return color(item, supply).flatMap { colorLookup -> signImageUrl(item.imageUrl).map { signedUrl -> ItemPrintInfo( // ... product_image = signedUrl, // ... ) } }}KanbanCardPrinter (resources/kanban/business/KanbanCardPrinter.kt) — identical pattern:
class KanbanCardPrinter( val qrLookupUrlBase: String, private val urlSigner: CloudFrontUrlSigner, private val colorResolver: (ItemColor) -> Result<ColorLookup>)Same signImageUrl helper; render() changes color(...).map to color(...).flatMap with nested signImageUrl(...).map.
Error types: CloudFrontUrlSigner.sign() wraps crypto failures in AppError.Infrastructure("CloudFront URL signing failed: ${cause.message}"). Boot-time failures (missing config, invalid PEM) also use AppError.Infrastructure since they are configuration/infrastructure errors, not attributable to caller input.
Task Breakdown
Section titled “Task Breakdown”| # | Task | Repository | Depends On | Acceptance Criteria |
|---|---|---|---|---|
| 0a | Baseline: run all checks in common-module | common-module | — | make build passes; record test count and coverage percentage |
| 0b | Baseline: run all checks in operations | operations | — | make build passes (uses --include-build ../common-module); record test count and coverage percentage |
| 1 | Implement CloudFrontUrlSigner | common-module | 0a | Class signs URLs correctly; constructor fails on invalid PEM; sign() returns Result; thread-safe |
| 2 | Unit tests for CloudFrontUrlSigner | common-module | 1 | Known-answer test (fixed PKCS#8 key + timestamp = expected signature); edge cases (URL with existing query params, empty URL); TTL override; invalid PEM throws at construction |
| 3 | Add Helm config wiring for key pair ID | operations | 0b | readExport for ImageCdnSigningKeyId; configmap surfaces imageCdnSigningKeyPairId |
| 4 | Add Helm secret wiring for signing key PEM | operations | 0b | readSecretName for ImageCdnSigningKeySecretArn; ExternalSecret fetches PEM; secrets.properties surfaces imageCdnSigningKeyPem |
| 5 | Wire CloudFrontUrlSigner into item Module.kt; update Main.kt | operations | 1, 3, 4 | item() returns ItemModuleResult(service, urlSigner); signer instantiated from config at boot; missing config fails startup; Main.kt updated to destructure result and pass signer to kanban; all existing ItemPrinter test call sites updated with test signer |
| 6 | Wire CloudFrontUrlSigner into kanban Module.kt | operations | 5 | kanban() and kanbanCardModule() accept urlSigner param; KanbanCardPrinter receives signer; all existing KanbanCardPrinter test call sites updated with test signer |
| 7 | Update ItemPrinter to sign product_image | operations | 5 | render() signs non-null image URLs; signing failure propagates as Result.failure; null imageUrl produces empty string |
| 8 | Update KanbanCardPrinter to sign product_image | operations | 6 | render() signs non-null image URLs; signing failure propagates as Result.failure; null imageUrl produces empty string |
| 9 | Unit tests for ItemPrinter signing | operations | 7 | Tests verify: signed URL format, signing failure propagation, null imageUrl |
| 10 | Unit tests for KanbanCardPrinter signing | operations | 8 | Tests verify: signed URL format, signing failure propagation, null imageUrl |
| 11 | Integration test: label/breadcrumb print with signed images | operations | 7 | End-to-end print-label call produces ItemPrintInfo with valid signed product_image URLs |
| 12 | Integration test: kanban card print with signed images | operations | 8 | End-to-end print-card call produces KanbanCardPrintInfo with valid signed product_image URLs |
| 13 | Update ComponentStartupTest | operations | 5, 6 | Startup test config includes signing key entries; startup succeeds; test with missing config verifies startup failure |
| 14 | Pre-push: common-module finalization | common-module | 2 | CHANGELOG updated with version [X.Y.Z-jmpicnic-DINTG] under Added category; make build passes; coverage >= baseline from 0a |
| 15 | Push and publish common-module | common-module | 14 | Feature branch pushed; CI publishes library artifact |
| 16 | Pre-push: operations finalization | operations | 9-13, 15 | CHANGELOG updated with version [X.Y.Z-jmpicnic-DINTG] under Added category; arda-common-version in gradle/libs.versions.toml updated to published feature-branch version; settings.gradle.kts includeBuild reverted; all checks pass; coverage >= baseline from 0b |
| 17 | Push operations and create PR | operations | 16 | Feature branch pushed; CI green; PR description includes cross-repo dependency callout: > [!IMPORTANT] This PR depends on common-module #<N>. Please review and merge it first. and dependency on PR #163 |
| 18 | Byproduct generation (Phase 3) | documentation | 17 | byproducts/ created with: changelog.md, learnings.md, suggestions.md, alternatives.md, skipped.md, specification-post.md |
| 19 | Session logging (Phase 4) | documentation | 18 | session/ created with plans, walkthroughs, and intermediate artifacts |
| 20 | Knowledge publication (Phase 5) | repos | 19 | Key learnings from learnings.md incorporated into relevant knowledge-base/ directories |
Worktree Strategy
Section titled “Worktree Strategy”Single agent — no parallel worktrees needed within each repository. Two repositories are modified:
| Worktree | Branch | Base | Repository |
|---|---|---|---|
projects/image-upload-frontend-worktrees/common-module | jmpicnic/image-upload-printing | origin/main | common-module |
projects/image-upload-frontend-worktrees/operations | jmpicnic/image-upload-printing | origin/jmpicnic/multi-pdf-print-and-bugs | operations |
Dependency: The operations PR depends on PR #163 (jmpicnic/multi-pdf-print-and-bugs) merging first. The common-module PR targets main directly.
Build verification: Use composite build for local validation:
cd projects/image-upload-frontend-worktrees/operations./gradlew build --include-build ../common-moduleMerge order: common-module PR merges first (publishes new version), then operations PR (after PR #163 merges and the new common-module version is available).
Build and Release Protocol
Section titled “Build and Release Protocol”Baseline Establishment (Tasks 0a, 0b)
Section titled “Baseline Establishment (Tasks 0a, 0b)”Before any code changes, run all checks in both repositories and record the baseline:
common-module:make build— record test count and coverage percentageoperations:make build— record test count and coverage percentage
All new code must have unit test coverage. Final coverage in both repositories must be equal to or higher than the baseline.
Local Development
Section titled “Local Development”During development, operations depends on the local common-module via composite build. Use the worktree-aware existence check pattern from the release-lifecycle conventions:
// settings.gradle.kts — worktree-aware override (development only)val commonModulePath = file("../common-module")if (commonModulePath.exists()) { includeBuild(commonModulePath)}Or pass via command line without modifying settings.gradle.kts:
./gradlew build --include-build ../common-moduleEither way, this is a development-only convenience and must not be pushed. The if (exists()) guard is safer — it no-ops in CI where the sibling path doesn’t exist — but must still be reverted before push per release-lifecycle push-guard rules.
Pre-Push Checklist — common-module (Task 14)
Section titled “Pre-Push Checklist — common-module (Task 14)”- Update
CHANGELOG.mdwith a version entry using the pattern[X.Y.Z-jmpicnic-DINTG]under theAddedcategory (new capability = minor version bump) - Run
make build— all checks must pass - Verify coverage >= baseline from Task 0a
Publish — common-module (Task 15)
Section titled “Publish — common-module (Task 15)”- Push the
jmpicnic/image-upload-printingbranch - CI publishes the library artifact with the feature-branch version
- Verify the artifact is available in the registry before proceeding
Pre-Push Checklist — operations (Task 16)
Section titled “Pre-Push Checklist — operations (Task 16)”- Update
CHANGELOG.mdwith a version entry using the pattern[X.Y.Z-jmpicnic-DINTG]under theAddedcategory - Update
arda-common-versioningradle/libs.versions.tomlto the published feature-branch version from Task 15 (current:8.1.0; update to theX.Y.Z-jmpicnic-DINTGversion published bycommon-moduleCI) - Revert any
includeBuild("../common-module")additions insettings.gradle.kts— this file must NOT be pushed with the local override - Run
make build(without--include-build) — all checks must pass against the published artifact - Verify coverage >= baseline from Task 0b
Push — operations (Task 17)
Section titled “Push — operations (Task 17)”- Push the
jmpicnic/image-upload-printingbranch - CI runs against the published
common-moduleartifact
Coding Conventions
Section titled “Coding Conventions”The implementation must follow the kotlin-coding and unit-tests-backend skills. Key rules for this task:
- No
getOrThrow()orgetOrNull()— usemap/flatMapto composeResultchains - Single exit point — no multiple
returnstatements; usewhenexpressions andResultchains AppErrorsubclasses for all errors —AppError.Infrastructurefor config/crypto failures- Named arguments at call sites when constructor has multiple
Stringparameters (e.g.,CloudFrontUrlSigner(privateKeyPem = pem, keyPairId = id)) - No
!!— userequireNotNullorwhen/?:withAppError runCatching+normalizeToAppError()for wrapping JCA crypto exceptions- Composite build safety — never push
includeBuildoverrides insettings.gradle.kts - Test harness pattern — use a
Harnessobject when test setup is shared across multiple tests; inline setup is acceptable for simple cases - Named mock variables — never pass
mockk()inline in constructors
Risks and Mitigations
Section titled “Risks and Mitigations”| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Signed URL TTL too short for Documint batch renders | Low | Medium — images 403 on slow renders | 15-minute TTL is generous; Documint typically fetches within seconds. TTL configurable via Helm value for operational tuning |
common-module version not available when operations PR is ready | Low | Low — blocks merge but not development | Use --include-build ../common-module for local dev; coordinate merge timing |
| Signature change blast radius in existing tests | Certain | Low — test compilation failures, not logic errors | All existing tests that construct ItemPrinter or KanbanCardPrinter need a signer parameter. Use a shared test helper that builds a signer with a fixed test RSA key |
Resolved Questions
Section titled “Resolved Questions”| # | Question | Resolution | Decision Log |
|---|---|---|---|
| 1 | IAM permissions for signing key secret | Verified. Secrets reader policy in eks-cluster.ts:409 grants access to arn:...:secret:${infraId}-*. Signing key secret ${infraId}-${purpose}-ImageCdnSigningKey matches. No IAM changes needed. | — |
| 2 | readSecretName helper availability | Verified. readSecretName is a DSL command in the Gradle helm plugin, used at line 12 of read-cloudFormation-values.cmd for pg_admin_secret_name. Same pattern applies for the signing key secret ARN. | — |
| 3 | Failure mode for signing errors | Decided: fail fast. Missing config or secret values fail component startup. Runtime signing errors propagate as Result.failure through the print call chain. No silent degradation. | FD-24 |
| 4 | Signing approach for Documint images | Decided: Option A — CloudFront signed URLs at print time. Options B-E rejected (see decision log for rationale). | FD-23 |
| 5 | Signer placement | Decided: common-module. Shared utility, available to any backend component. | FD-25 |
| 6 | TTL configuration | Decided: configurable. Default 15 min in application.conf extras.printing.signedUrlTtlMinutes, overridable via Helm configmap. | FD-26 |
| 7 | PEM key format | Verified: PKCS#8 only. The CDK lambda (generate-signing-key.ts:56) uses privateKeyEncoding: { type: "pkcs8", format: "pem" }. No need to support PKCS#1. | — |
| 8 | Signer construction location | Decided: item module. Built once in itemModule() (where config is scoped), returned alongside ItemService via ItemModuleResult, injected into kanban module by Main.kt. Enables mock injection in tests. | — |
Copyright: © Arda Systems 2025-2026, All rights reserved