From b3d000086cd786c3cdf9c812f48cf96f0bd7f213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 15:20:48 +0700 Subject: [PATCH 01/73] fix(local): require successful bootstrap readiness --- tools/repo-cli/src/local-services.mjs | 54 +++++++++++++++++-- .../test/local-infrastructure.test.mjs | 25 ++++++++- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/tools/repo-cli/src/local-services.mjs b/tools/repo-cli/src/local-services.mjs index 9ceb993e..e7d2bf93 100644 --- a/tools/repo-cli/src/local-services.mjs +++ b/tools/repo-cli/src/local-services.mjs @@ -18,6 +18,7 @@ const services = [ 'otel-collector', 'otel-collector-health', ]; +const completionServices = ['minio-init']; const logServices = [...services, 'minio-init']; const hostPorts = [ { service: 'postgres', key: 'POSTGRES_PORT', fallback: 5432 }, @@ -142,6 +143,15 @@ export function composeOperationTimeoutMs(waitSeconds) { return (waitSeconds + 30) * 1000; } +export function classifyCompletionStatus(status, exitCode) { + const detail = `${status}/${exitCode}`; + if (status === 'exited') { + return exitCode === 0 ? { state: 'complete', detail } : { state: 'failed', detail }; + } + if (status === 'dead' || status === 'removing') return { state: 'failed', detail }; + return { state: 'pending', detail: status }; +} + function requireDocker() { const result = spawnSync('docker', ['info', '--format', '{{.ServerVersion}}'], { cwd: repositoryRoot, @@ -249,24 +259,60 @@ function inspectHealth(service, values) { return { state, health, detail: `${state}/${health}` }; } +function inspectCompletion(service, values) { + const idResult = runDocker([...composeArgs(values), 'ps', '-aq', service], { + allowFailure: true, + }); + const id = idResult.stdout.trim(); + if (!id) return { state: 'pending', detail: 'no container' }; + const inspect = runDocker(['inspect', '--format', '{{.State.Status}}|{{.State.ExitCode}}', id], { + allowFailure: true, + }); + const inspection = inspect.stdout?.trim(); + if (inspect.error || inspect.status !== 0 || !inspection) { + return { state: 'pending', detail: 'inspect unavailable' }; + } + const [status, rawExitCode] = inspection.split('|'); + return classifyCompletionStatus(status, Number(rawExitCode)); +} + async function waitForReady(values, waitSeconds) { const deadline = Date.now() + waitSeconds * 1000; let last = new Map(); + let lastCompletions = new Map(); while (Date.now() <= deadline) { last = new Map(services.map((service) => [service, inspectHealth(service, values)])); + lastCompletions = new Map( + completionServices.map((service) => [service, inspectCompletion(service, values)]), + ); + const failedCompletion = [...lastCompletions.entries()].find( + ([, result]) => result.state === 'failed', + ); + if (failedCompletion) { + fail(`${failedCompletion[0]} failed (${failedCompletion[1].detail})`); + } if ( - [...last.values()].every(({ state, health }) => state === 'running' && health === 'healthy') + [...last.values()].every( + ({ state, health }) => state === 'running' && health === 'healthy', + ) && + [...lastCompletions.values()].every(({ state }) => state === 'complete') ) { - console.log(`Local services ready (${services.join(', ')}).`); + console.log(`Local services ready (${[...services, ...completionServices].join(', ')}).`); return; } - const summary = services.map((service) => `${service}=${last.get(service).detail}`).join(' '); + const summary = [ + ...services.map((service) => `${service}=${last.get(service).detail}`), + ...completionServices.map((service) => `${service}=${lastCompletions.get(service).detail}`), + ].join(' '); process.stdout.write(`Waiting for local services: ${summary}\r`); await delay(1000); } console.error('\nLocal services did not become ready:'); for (const service of services) console.error(`- ${service}: ${last.get(service)?.detail ?? 'unknown'}`); + for (const service of completionServices) { + console.error(`- ${service}: ${lastCompletions.get(service)?.detail ?? 'unknown'}`); + } fail(`readiness timeout after ${waitSeconds}s`); } @@ -368,6 +414,8 @@ export async function main(argv = process.argv.slice(2)) { if (command === 'status') { for (const service of services) console.log(`${service}: ${inspectHealth(service, values).detail}`); + for (const service of completionServices) + console.log(`${service}: ${inspectCompletion(service, values).detail}`); return; } if (command === 'logs') { diff --git a/tools/repo-cli/test/local-infrastructure.test.mjs b/tools/repo-cli/test/local-infrastructure.test.mjs index 49fe61db..8e920810 100644 --- a/tools/repo-cli/test/local-infrastructure.test.mjs +++ b/tools/repo-cli/test/local-infrastructure.test.mjs @@ -5,7 +5,7 @@ import { spawnSync } from 'node:child_process'; import test from 'node:test'; import { fileURLToPath } from 'node:url'; -import { composeOperationTimeoutMs } from '../src/local-services.mjs'; +import { classifyCompletionStatus, composeOperationTimeoutMs } from '../src/local-services.mjs'; const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..'); const read = (relativePath) => readFileSync(path.join(repositoryRoot, relativePath), 'utf8'); @@ -16,6 +16,29 @@ test('local lifecycle grants image pulls the bounded readiness window plus teard assert.equal(composeOperationTimeoutMs(3600), 3_630_000); }); +test('local readiness requires successful completion jobs', () => { + assert.deepEqual(classifyCompletionStatus('created', 0), { + state: 'pending', + detail: 'created', + }); + assert.deepEqual(classifyCompletionStatus('running', 0), { + state: 'pending', + detail: 'running', + }); + assert.deepEqual(classifyCompletionStatus('exited', 0), { + state: 'complete', + detail: 'exited/0', + }); + assert.deepEqual(classifyCompletionStatus('exited', 2), { + state: 'failed', + detail: 'exited/2', + }); + assert.deepEqual(classifyCompletionStatus('dead', 137), { + state: 'failed', + detail: 'dead/137', + }); +}); + test('local compose defines pinned, healthy disposable dependencies', () => { const compose = read('infrastructure/local/compose.yml'); const envExample = read('infrastructure/local/.env.example'); From 5dfa1d2e64816c6ef94cfd63684d21deb4ba8e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 15:23:13 +0700 Subject: [PATCH 02/73] docs(foundation): verify live local infrastructure --- ...ndation-local-infrastructure-2026-08-02.md | 34 +++++++++++++++++++ .../002-complete-execution-orchestration.md | 2 +- docs/plans/004-luna-max-execution-plan.md | 8 ++--- docs/plans/execution-orchestration.json | 9 +++-- .../test/execution-orchestration.test.mjs | 2 +- 5 files changed, 44 insertions(+), 11 deletions(-) diff --git a/docs/operations/foundation-local-infrastructure-2026-08-02.md b/docs/operations/foundation-local-infrastructure-2026-08-02.md index 90cf4adc..39d6d226 100644 --- a/docs/operations/foundation-local-infrastructure-2026-08-02.md +++ b/docs/operations/foundation-local-infrastructure-2026-08-02.md @@ -54,3 +54,37 @@ Environment-gated: Revert the lifecycle commit and retain the prior static Compose checks. No containers, named volumes, host files, or credentials are modified by the repository changes. + +## Live verification closure + +Observed at (UTC): 2026-08-03T08:21:34Z + +Environment: + +- Docker Engine `29.5.3` +- Docker Desktop `4.77.0` +- Docker Compose `5.1.4` +- Windows host with Linux containers + +The live run first reproduced and corrected four startup gaps: a nonexistent +MinIO client tag, a fixed 30-second image-pull timeout, CRLF bytes in the +Linux-mounted bootstrap script, and readiness that ignored a failed +`minio-init` completion job. The corrected lifecycle then passed: + +- `node --test tools/repo-cli/test/local-infrastructure.test.mjs` +- `node tools/repo-cli/src/local-services.mjs check` +- `node tools/repo-cli/src/local-services.mjs start --wait-seconds=600` +- health checks for PostgreSQL, Redis, MinIO, Mailpit, OpenTelemetry, and its + HTTP health companion +- successful `minio-init` completion with both private buckets created +- a PostgreSQL catalog query finding all 19 module-owned schemas +- `restart-check --wait-seconds=600` +- `persistence-check --wait-seconds=600`, including sentinel cleanup +- an intentional duplicate-port preflight that failed closed +- an intentional impossible disk threshold that failed closed +- `stop --wait-seconds=600`, followed by inspection of all four preserved named + volumes and the stopped containers + +FND-003 is verified. Rollback remains source-only: revert the focused lifecycle +commits. The validation left stopped containers and named volumes intact and +did not delete local development data. diff --git a/docs/plans/002-complete-execution-orchestration.md b/docs/plans/002-complete-execution-orchestration.md index 40b8dc2f..2d205f71 100644 --- a/docs/plans/002-complete-execution-orchestration.md +++ b/docs/plans/002-complete-execution-orchestration.md @@ -66,7 +66,7 @@ This plan was reconciled on 2026-08-02 from remote `dev` at `783a4710c0aa2a2808d Merged PRs 1–23 establish substantial engineering, IAM/AUD/BUA, IAE/DSM, JRA, and DSO code. PR 19 delivered the normal 73-commit foundation batch to `dev`; PR 20 promoted it to `main`; PRs 21–23 carried validated promotion-review fixes back through `dev`. Plans 010–050 must therefore start with evidence reconciliation, not blind reimplementation. Plans 060–500 remain unverified and must be treated as planned until their gates pass. -The active execution packet is `B01` in `004-luna-max-execution-plan.md`, starting with `FND-003` on `feat/foundation-identity-completion`. The packet preserves the requested 30–99 commit rule, targets about 70 commits, and carries the implementation forward without opening a documentation-only PR. +The active execution packet is `B01` in `004-luna-max-execution-plan.md`, continuing with `FND-004` after live verification closed `FND-003`. The packet preserves the requested 30–50 commit target and exceptional 79-commit ceiling, and carries implementation forward without opening a documentation-only PR. The hashes above are an audit anchor, not a branch lock. Every session must fetch and recompute live state; update the ledger checkpoint only as part of a committed task/PR handoff so session-local observations do not create meaningless dirty files. diff --git a/docs/plans/004-luna-max-execution-plan.md b/docs/plans/004-luna-max-execution-plan.md index 0bb9e8be..e6e63723 100644 --- a/docs/plans/004-luna-max-execution-plan.md +++ b/docs/plans/004-luna-max-execution-plan.md @@ -64,7 +64,7 @@ Each batch may require multiple normal integration PR slices before its exit gat | Batch | Branch | Tasks | Dependencies | Commit budget | Exit gate | |---|---|---|---|---|---| -| `B01` | `feat/foundation-identity-reconciliation` | `FND-003..007`, all Plan 020 tasks | Verified `FND-001/002` | 30–50 target; exceptional ceiling 79 | Foundation external gates recorded; IAM/AUD/BUA obligations reconciled and completed | +| `B01` | `feat/foundation-identity-reconciliation` | `FND-004..007`, all Plan 020 tasks | Verified `FND-001..003` | 30–50 target; exceptional ceiling 79 | Foundation external gates recorded; IAM/AUD/BUA obligations reconciled and completed | | `B02` | `feat/artifacts-datasets-completion` | All Plan 030 tasks | `B01` | 30–50 target; exceptional ceiling 79 | Immutable artifact/evidence/dataset foundations verified | | `B03` | `feat/jobs-processing-completion` | All Plan 040 tasks | `B02` | 30–50 target; exceptional ceiling 79 | Signed typed jobs execute locally/cloud with approvals and durable recovery | | `B04` | `feat/devices-sync-completion` | All Plan 050 tasks | `B03` | 30–50 target; exceptional ceiling 79 | Desktop/Android sync, offline, conflict, transfer, and revocation gates pass | @@ -145,10 +145,10 @@ corepack pnpm orchestration:check corepack pnpm requirements:check ``` -Then resume `FND-003`: +Live Docker verification closed `FND-003` on 2026-08-03. Resume `FND-004`: -1. Run the Docker-capable checks in `docs/operations/foundation-local-infrastructure-2026-08-02.md` when Docker Desktop/Compose v2 is available. -2. If Docker remains unavailable, preserve `FND-003` as incomplete, finish only credential-independent `FND-004..007` evidence, and record the external gate. Do not claim foundation verification. +1. Run pinned OpenTofu formatting, initialization without a backend, and validation for the portable AWS modules without applying infrastructure. +2. If OpenTofu remains unavailable, preserve `FND-004` as implemented but unverified, record the exact external gate, and continue only credential-independent `FND-005..007` reconciliation. 3. Reconcile Plans 020–050 against merged code before implementing any missing behavior. For `B01`, complete Plan 020 only after the remaining foundation boundaries are explicit. 4. End every session with the handoff record from `003-luna-handoff-runbook.md`, including exact branch/HEAD, open PRs, checks, task/batch status, rollback points, and safest next command. diff --git a/docs/plans/execution-orchestration.json b/docs/plans/execution-orchestration.json index 9f334dfd..4049b3c5 100644 --- a/docs/plans/execution-orchestration.json +++ b/docs/plans/execution-orchestration.json @@ -54,7 +54,7 @@ "post-ga-planned", "blocked" ], - "nextTaskId": "FND-003", + "nextTaskId": "FND-004", "activeBatchId": "B01", "taskState": { "FND-001": { @@ -82,8 +82,8 @@ "note": "Room, WorkManager, Keystore, bilingual resources, generated contracts/tokens, backup/network policy, account isolation, and process-recreation evidence are complete. No product requirement status was promoted." }, "FND-003": { - "status": "in-progress", - "commit": "783a4710c0aa2a2808d78ad7f0643e6731150bd7", + "status": "verified", + "commit": "b3d000086cd786c3cdf9c812f48cf96f0bd7f213", "evidence": [ "infrastructure/local/compose.yml", "infrastructure/local/README.md", @@ -92,7 +92,7 @@ "tools/repo-cli/test/local-infrastructure.test.mjs", "docs/operations/foundation-local-infrastructure-2026-08-02.md" ], - "note": "Static Compose/bootstrap/lifecycle checks, daemon-free config/preflight, bounded local diagnostics/log retention, AWS safety checks, telemetry redaction tests, and infrastructure path-aware CI are integrated and promoted. Live Docker health, port-collision, disk-pressure, and restart-persistence evidence remains pending because the Docker daemon was unavailable in the implementation environment." + "note": "Docker Engine 29.5.3 and Compose 5.1.4 live verification passed on 2026-08-03: all long-running services became healthy, MinIO initialization exited successfully, all 19 module schemas existed, restart and Redis persistence checks passed, collision and disk-pressure probes failed closed, and safe stop preserved containers and named volumes." }, "FND-004": { "status": "implemented", @@ -150,7 +150,6 @@ "commitBudget": { "minimum": 30, "target": 45, "maximum": 79 }, "maximumChangedFiles": 260, "taskIds": [ - "FND-003", "FND-004", "FND-005", "FND-006", diff --git a/tools/repo-cli/test/execution-orchestration.test.mjs b/tools/repo-cli/test/execution-orchestration.test.mjs index d0ebbc11..9c8f1d09 100644 --- a/tools/repo-cli/test/execution-orchestration.test.mjs +++ b/tools/repo-cli/test/execution-orchestration.test.mjs @@ -205,7 +205,7 @@ test('repository checker validates the committed orchestration package', () => { test('ledger records verified task evidence before advancing the next task', () => { const ledger = readJson('docs/plans/execution-orchestration.json'); - assert.equal(ledger.nextTaskId, 'FND-003'); + assert.equal(ledger.nextTaskId, 'FND-004'); assert.equal(ledger.activeBatchId, 'B01'); assert.equal(ledger.checkpoint.remoteDev, '783a4710c0aa2a2808d78ad7f0643e6731150bd7'); assert.equal(ledger.checkpoint.remoteMain, '3ed3d77d0281ef239d0509c81ded447d8fffd213'); From 5952e37f074d0e2cbc1142c4735120cd2085c32d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 15:24:57 +0700 Subject: [PATCH 03/73] build(infra): pin the OpenTofu validation release --- infrastructure/aws/.opentofu-version | 1 + infrastructure/aws/README.md | 5 +++++ tools/repo-cli/src/check-aws-infrastructure.mjs | 9 +++++++++ tools/repo-cli/test/aws-infrastructure.test.mjs | 7 +++++++ 4 files changed, 22 insertions(+) create mode 100644 infrastructure/aws/.opentofu-version diff --git a/infrastructure/aws/.opentofu-version b/infrastructure/aws/.opentofu-version new file mode 100644 index 00000000..e0a6b34f --- /dev/null +++ b/infrastructure/aws/.opentofu-version @@ -0,0 +1 @@ +1.12.5 diff --git a/infrastructure/aws/README.md b/infrastructure/aws/README.md index aa816b61..787569d2 100644 --- a/infrastructure/aws/README.md +++ b/infrastructure/aws/README.md @@ -20,6 +20,11 @@ repository. ## Validate without applying +The repository pins OpenTofu `1.12.5` in `.opentofu-version`. Use that exact +native CLI release, or the official +`ghcr.io/opentofu/opentofu:1.12.5` container when a host installation is not +available. Do not use a floating container tag for validation evidence. + ```text pnpm infra:check cd infrastructure/aws/environments/alpha diff --git a/tools/repo-cli/src/check-aws-infrastructure.mjs b/tools/repo-cli/src/check-aws-infrastructure.mjs index 5cbf42f2..a713aa73 100644 --- a/tools/repo-cli/src/check-aws-infrastructure.mjs +++ b/tools/repo-cli/src/check-aws-infrastructure.mjs @@ -9,6 +9,7 @@ const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)) const infrastructureRoot = path.join(repositoryRoot, 'infrastructure', 'aws'); const requiredFiles = [ 'README.md', + '.opentofu-version', 'environments/alpha/main.tf', 'environments/alpha/variables.tf', 'environments/alpha/versions.tf', @@ -28,6 +29,14 @@ for (const relativePath of requiredFiles) { if (!existsSync(path.join(infrastructureRoot, relativePath))) fail(`missing ${relativePath}`); } +const opentofuVersion = readFileSync( + path.join(infrastructureRoot, '.opentofu-version'), + 'utf8', +).trim(); +if (!/^\d+\.\d+\.\d+$/u.test(opentofuVersion)) { + fail('the OpenTofu version pin must be one exact semantic version'); +} + const allTerraform = requiredFiles .filter((relativePath) => relativePath.endsWith('.tf')) .map((relativePath) => readFileSync(path.join(infrastructureRoot, relativePath), 'utf8')) diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index 3c3e5af8..f42ff064 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -8,6 +8,13 @@ import { fileURLToPath } from 'node:url'; const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..'); const read = (relativePath) => readFileSync(path.join(repositoryRoot, relativePath), 'utf8'); +test('AWS validation pins one OpenTofu CLI and official container release', () => { + const version = read('infrastructure/aws/.opentofu-version').trim(); + const readme = read('infrastructure/aws/README.md'); + assert.equal(version, '1.12.5'); + assert.match(readme, /ghcr\.io\/opentofu\/opentofu:1\.12\.5/u); +}); + test('AWS foundation has reusable modules and safe alpha composition', () => { for (const relativePath of [ 'infrastructure/aws/modules/network/main.tf', From 815d18075f8593c95293e429e11e3c33c681e743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 15:25:40 +0700 Subject: [PATCH 04/73] style(infra): normalize OpenTofu sources --- infrastructure/aws/environments/alpha/main.tf | 54 +++++++++---------- .../aws/environments/alpha/variables.tf | 6 +-- infrastructure/aws/modules/compute/main.tf | 28 +++++----- .../aws/modules/compute/variables.tf | 8 +-- infrastructure/aws/modules/data/main.tf | 54 +++++++++---------- infrastructure/aws/modules/data/variables.tf | 16 +++--- infrastructure/aws/modules/security/main.tf | 8 +-- 7 files changed, 87 insertions(+), 87 deletions(-) diff --git a/infrastructure/aws/environments/alpha/main.tf b/infrastructure/aws/environments/alpha/main.tf index 21062cfa..567a3611 100644 --- a/infrastructure/aws/environments/alpha/main.tf +++ b/infrastructure/aws/environments/alpha/main.tf @@ -5,23 +5,23 @@ locals { } module "network" { - source = "../../modules/network" - name = var.name - region = var.aws_region - azs = var.availability_zones - vpc_cidr = var.vpc_cidr - enable_nat_gateway = var.enable_nat_gateway - tags = local.tags -} - -module "security" { - source = "../../modules/security" + source = "../../modules/network" name = var.name region = var.aws_region - github_repository = var.github_repository + azs = var.availability_zones + vpc_cidr = var.vpc_cidr + enable_nat_gateway = var.enable_nat_gateway tags = local.tags } +module "security" { + source = "../../modules/security" + name = var.name + region = var.aws_region + github_repository = var.github_repository + tags = local.tags +} + module "web" { source = "../../modules/web" name = var.name @@ -32,22 +32,22 @@ module "web" { } module "data" { - source = "../../modules/data" - name = var.name - environment = var.environment - private_subnet_ids = module.network.private_subnet_ids - database_security_group_id = module.network.database_security_group_id - cache_security_group_id = module.network.cache_security_group_id - kms_key_arn = module.security.kms_key_arn - enable_database = var.enable_database - database_instance_class = var.database_instance_class - backup_retention_period = var.backup_retention_period - deletion_protection = var.deletion_protection - database_multi_az = var.database_multi_az - redis_num_cache_clusters = var.redis_num_cache_clusters + source = "../../modules/data" + name = var.name + environment = var.environment + private_subnet_ids = module.network.private_subnet_ids + database_security_group_id = module.network.database_security_group_id + cache_security_group_id = module.network.cache_security_group_id + kms_key_arn = module.security.kms_key_arn + enable_database = var.enable_database + database_instance_class = var.database_instance_class + backup_retention_period = var.backup_retention_period + deletion_protection = var.deletion_protection + database_multi_az = var.database_multi_az + redis_num_cache_clusters = var.redis_num_cache_clusters redis_automatic_failover_enabled = var.redis_automatic_failover_enabled - redis_multi_az_enabled = var.redis_multi_az_enabled - tags = local.tags + redis_multi_az_enabled = var.redis_multi_az_enabled + tags = local.tags } module "compute" { diff --git a/infrastructure/aws/environments/alpha/variables.tf b/infrastructure/aws/environments/alpha/variables.tf index 370cf456..fd5ad55f 100644 --- a/infrastructure/aws/environments/alpha/variables.tf +++ b/infrastructure/aws/environments/alpha/variables.tf @@ -17,8 +17,8 @@ variable "name" { } variable "availability_zones" { - type = list(string) - default = ["ap-southeast-1a", "ap-southeast-1b"] + type = list(string) + default = ["ap-southeast-1a", "ap-southeast-1b"] } variable "vpc_cidr" { @@ -38,7 +38,7 @@ variable "enable_cloudfront" { } variable "enable_database" { - type = bool + type = bool description = "Create managed RDS/ElastiCache resources; disabled by default to prevent accidental recurring spend." default = false } diff --git a/infrastructure/aws/modules/compute/main.tf b/infrastructure/aws/modules/compute/main.tf index 7e9c14cb..a45e29f1 100644 --- a/infrastructure/aws/modules/compute/main.tf +++ b/infrastructure/aws/modules/compute/main.tf @@ -74,15 +74,15 @@ resource "aws_iam_role" "task" { locals { api_container = { - name = "api" - image = var.api_image - essential = true + name = "api" + image = var.api_image + essential = true readonlyRootFilesystem = true - privileged = false - user = "10001" - stopTimeout = 30 - cpu = var.api_cpu - memory = var.api_memory + privileged = false + user = "10001" + stopTimeout = 30 + cpu = var.api_cpu + memory = var.api_memory portMappings = [{ containerPort = 3000 hostPort = 3000 @@ -135,13 +135,13 @@ resource "aws_ecs_task_definition" "worker" { execution_role_arn = aws_iam_role.execution.arn task_role_arn = aws_iam_role.task.arn container_definitions = jsonencode([{ - name = "worker" - image = var.worker_image - essential = true + name = "worker" + image = var.worker_image + essential = true readonlyRootFilesystem = true - privileged = false - user = "10001" - stopTimeout = 30 + privileged = false + user = "10001" + stopTimeout = 30 logConfiguration = { logDriver = "awslogs" options = { diff --git a/infrastructure/aws/modules/compute/variables.tf b/infrastructure/aws/modules/compute/variables.tf index e54afd04..d3367240 100644 --- a/infrastructure/aws/modules/compute/variables.tf +++ b/infrastructure/aws/modules/compute/variables.tf @@ -41,13 +41,13 @@ variable "private_egress_enabled" { } variable "api_image" { - type = string - default = "ghcr.io/databreeze/api:dev" + type = string + default = "ghcr.io/databreeze/api:dev" } variable "worker_image" { - type = string - default = "ghcr.io/databreeze/worker:dev" + type = string + default = "ghcr.io/databreeze/worker:dev" } variable "enable_services" { diff --git a/infrastructure/aws/modules/data/main.tf b/infrastructure/aws/modules/data/main.tf index dc731da4..aec67aea 100644 --- a/infrastructure/aws/modules/data/main.tf +++ b/infrastructure/aws/modules/data/main.tf @@ -12,35 +12,35 @@ resource "aws_db_subnet_group" "this" { resource "aws_db_instance" "postgres" { count = var.enable_database ? 1 : 0 - identifier = "databreeze-${var.name}" - engine = "postgres" - engine_version = "17.5" - instance_class = var.database_instance_class - allocated_storage = 20 - max_allocated_storage = 100 - storage_type = "gp3" - storage_encrypted = true - kms_key_id = var.kms_key_arn - db_name = var.database_name - username = var.database_username - port = 5432 - manage_master_user_password = true - master_user_secret_kms_key_id = var.kms_key_arn - db_subnet_group_name = aws_db_subnet_group.this[0].name - vpc_security_group_ids = [var.database_security_group_id] - publicly_accessible = false - multi_az = var.database_multi_az - backup_retention_period = var.backup_retention_period - backup_window = "17:00-17:30" - maintenance_window = "sun:18:00-sun:18:30" - deletion_protection = var.deletion_protection - skip_final_snapshot = false - final_snapshot_identifier = "databreeze-${var.name}-final" - auto_minor_version_upgrade = true - copy_tags_to_snapshot = true + identifier = "databreeze-${var.name}" + engine = "postgres" + engine_version = "17.5" + instance_class = var.database_instance_class + allocated_storage = 20 + max_allocated_storage = 100 + storage_type = "gp3" + storage_encrypted = true + kms_key_id = var.kms_key_arn + db_name = var.database_name + username = var.database_username + port = 5432 + manage_master_user_password = true + master_user_secret_kms_key_id = var.kms_key_arn + db_subnet_group_name = aws_db_subnet_group.this[0].name + vpc_security_group_ids = [var.database_security_group_id] + publicly_accessible = false + multi_az = var.database_multi_az + backup_retention_period = var.backup_retention_period + backup_window = "17:00-17:30" + maintenance_window = "sun:18:00-sun:18:30" + deletion_protection = var.deletion_protection + skip_final_snapshot = false + final_snapshot_identifier = "databreeze-${var.name}-final" + auto_minor_version_upgrade = true + copy_tags_to_snapshot = true performance_insights_enabled = var.environment == "production" performance_insights_kms_key_id = var.environment == "production" ? var.kms_key_arn : null - tags = merge(local.common_tags, { Name = "databreeze-${var.name}" }) + tags = merge(local.common_tags, { Name = "databreeze-${var.name}" }) } resource "aws_elasticache_subnet_group" "this" { diff --git a/infrastructure/aws/modules/data/variables.tf b/infrastructure/aws/modules/data/variables.tf index 64411c18..520040a0 100644 --- a/infrastructure/aws/modules/data/variables.tf +++ b/infrastructure/aws/modules/data/variables.tf @@ -41,13 +41,13 @@ variable "database_instance_class" { } variable "database_name" { - type = string - default = "databreeze" + type = string + default = "databreeze" } variable "database_username" { - type = string - default = "databreeze" + type = string + default = "databreeze" } variable "backup_retention_period" { @@ -57,8 +57,8 @@ variable "backup_retention_period" { } variable "deletion_protection" { - type = bool - default = false + type = bool + default = false } variable "database_multi_az" { @@ -90,6 +90,6 @@ variable "redis_engine_version" { } variable "tags" { - type = map(string) - default = {} + type = map(string) + default = {} } diff --git a/infrastructure/aws/modules/security/main.tf b/infrastructure/aws/modules/security/main.tf index 93312fe6..620c2b09 100644 --- a/infrastructure/aws/modules/security/main.tf +++ b/infrastructure/aws/modules/security/main.tf @@ -22,7 +22,7 @@ data "aws_iam_policy_document" "platform_key" { actions = ["kms:Decrypt", "kms:DescribeKey", "kms:Encrypt", "kms:GenerateDataKey*", "kms:ReEncrypt*"] resources = ["*"] principals { - type = "Service" + type = "Service" identifiers = [ "logs.${var.region}.amazonaws.com", "s3.amazonaws.com", @@ -37,7 +37,7 @@ data "aws_iam_policy_document" "platform_key" { condition { test = "StringLike" variable = "kms:ViaService" - values = [ + values = [ "logs.${var.region}.amazonaws.com", "s3.${var.region}.amazonaws.com", "secretsmanager.${var.region}.amazonaws.com" @@ -141,8 +141,8 @@ resource "aws_iam_role_policy" "github_deploy" { policy = jsonencode({ Version = "2012-10-17" Statement = [{ - Effect = "Allow" - Action = ["s3:GetObject", "s3:PutObject", "s3:ListBucket"] + Effect = "Allow" + Action = ["s3:GetObject", "s3:PutObject", "s3:ListBucket"] Resource = [ "arn:aws:s3:::databreeze-${var.name}-web", "arn:aws:s3:::databreeze-${var.name}-web/*" From 57e7c79061f25f9cbcf2946260a16a2f2576a13c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 15:30:17 +0700 Subject: [PATCH 05/73] build(infra): lock the AWS provider selection --- .gitattributes | 1 + infrastructure/aws/README.md | 2 ++ .../environments/alpha/.terraform.lock.hcl | 27 +++++++++++++++++++ .../repo-cli/src/check-aws-infrastructure.mjs | 15 +++++++---- .../repo-cli/test/aws-infrastructure.test.mjs | 15 ++++++++--- 5 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 infrastructure/aws/environments/alpha/.terraform.lock.hcl diff --git a/.gitattributes b/.gitattributes index f321e0e1..5c49c819 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,6 +11,7 @@ *.yaml text eol=lf *.yml text eol=lf *.toml text eol=lf +*.hcl text eol=lf *.ts text eol=lf *.tsx text eol=lf *.css text eol=lf diff --git a/infrastructure/aws/README.md b/infrastructure/aws/README.md index 787569d2..f0501bea 100644 --- a/infrastructure/aws/README.md +++ b/infrastructure/aws/README.md @@ -24,6 +24,8 @@ The repository pins OpenTofu `1.12.5` in `.opentofu-version`. Use that exact native CLI release, or the official `ghcr.io/opentofu/opentofu:1.12.5` container when a host installation is not available. Do not use a floating container tag for validation evidence. +The alpha composition commits `.terraform.lock.hcl`; initialization uses it +read-only so provider selections cannot drift during a validation run. ```text pnpm infra:check diff --git a/infrastructure/aws/environments/alpha/.terraform.lock.hcl b/infrastructure/aws/environments/alpha/.terraform.lock.hcl new file mode 100644 index 00000000..cf3388ee --- /dev/null +++ b/infrastructure/aws/environments/alpha/.terraform.lock.hcl @@ -0,0 +1,27 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/hashicorp/aws" { + version = "6.0.0" + constraints = "6.0.0" + hashes = [ + "h1:1/CeThA/HYnTU2Zm4PEZA4735jHfP7L6LHOUl3+yFwE=", + "h1:6q9f4g92JlbaNWWOcviq2ZXugvrpTs0BKVWvMe9AZks=", + "h1:B10Faphm0kK6BlKlBTkZmaSM/fDlFsuT4QiPwf9+6zA=", + "h1:C+a4crNE3xwGk8Zn0p/4iz2JfuKtiZKNfYBRx3cWI48=", + "h1:F7kc3XB2ssSExi7YIpkAvJsHHYypBXeNZ2LzKPX5ZLg=", + "h1:J4hBxGTSRJAu+jcIbmyBB53KeHTXCgPDNC8eJrdjTIE=", + "h1:Od4Gd1YhRPD3mAsW9JiMwC5tEmkLWbuGac9C8OwZXuY=", + "h1:YUwtc4UmeSvIZetaDrxkAHuGK2pENco7XhfWIiGm67E=", + "h1:idz7G1K9QxXJ+Cz+0tPoJz5ApAatmzH0ktnlKSJGulQ=", + "zh:44c81d55a1844333a50fb36dd51938201fbd4e8a3da71880c7df11bc0eaa251a", + "zh:4d206f13982f539704998c76c2083bdb95f63c9a4d3ac8b4d2d152c4d874efca", + "zh:5e5e6b4cf921abf55c69b7ed33450a98de8bed611082c4272e9bba81a965d81c", + "zh:5fe449164de2f3507bada48e94dc07e192e24b644ebdb431fcd4e09168cab46c", + "zh:66efb8c840cedc830dee28994040d84eb74c353b97e86a33874f22471ec21deb", + "zh:b1e93ddf1557c84ddddba2a67ef908de2ac75414af7d0cc7f9cef86401c36a71", + "zh:b850aa20bdc8d63dca39f7b3f6b313649a28a78f66a9f79ef24f2a9b6c9b2247", + "zh:e58ace0225a8750d82f557bd54225898a784783dae6a378fcb7fffcf7b589315", + "zh:ffcadc0505dd7510f3fb14df5e4b33c2786df8039a902f0fcd2cdca81514b282", + ] +} diff --git a/tools/repo-cli/src/check-aws-infrastructure.mjs b/tools/repo-cli/src/check-aws-infrastructure.mjs index a713aa73..225b8176 100644 --- a/tools/repo-cli/src/check-aws-infrastructure.mjs +++ b/tools/repo-cli/src/check-aws-infrastructure.mjs @@ -11,6 +11,7 @@ const requiredFiles = [ 'README.md', '.opentofu-version', 'environments/alpha/main.tf', + 'environments/alpha/.terraform.lock.hcl', 'environments/alpha/variables.tf', 'environments/alpha/versions.tf', 'modules/network/main.tf', @@ -106,11 +107,15 @@ if (tofu.error?.code === 'ENOENT') { const tofuDataDirectory = mkdtempSync(path.join(os.tmpdir(), 'databreeze-tofu-')); const tofuEnvironment = { ...process.env, TF_DATA_DIR: tofuDataDirectory }; try { - const init = spawnSync('tofu', ['init', '-backend=false', '-input=false', '-no-color'], { - cwd: alphaDirectory, - env: tofuEnvironment, - encoding: 'utf8', - }); + const init = spawnSync( + 'tofu', + ['init', '-backend=false', '-input=false', '-lockfile=readonly', '-no-color'], + { + cwd: alphaDirectory, + env: tofuEnvironment, + encoding: 'utf8', + }, + ); if (init.status !== 0) { console.error(init.stdout || init.stderr); process.exitCode = init.status ?? 1; diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index f42ff064..f8e8eb4a 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -66,11 +66,11 @@ test('AWS sources expose encryption, private data, and OIDC boundaries without s 'master_user_secret_kms_key_id', 'block_public_policy', 'storage_encrypted', - 'manage_master_user_password = true', - 'publicly_accessible = false', - 'transit_encryption_enabled = true', ]) assert.match(sources, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); + assert.match(sources, /manage_master_user_password\s*=\s*true/u); + assert.match(sources, /publicly_accessible\s*=\s*false/u); + assert.match(sources, /transit_encryption_enabled\s*=\s*true/u); assert.doesNotMatch(sources, /AKIA[0-9A-Z]{16}|BEGIN (RSA|OPENSSH) PRIVATE KEY/); assert.doesNotMatch(sources, /ingress[\s\S]*?cidr_blocks\s*=\s*\["0\.0\.0\.0\/0"\]/u); assert.doesNotMatch(sources, /principals[\s\S]*?identifiers\s*=\s*\[[^\]]*"\*"/u); @@ -96,12 +96,21 @@ test('AWS validation script is non-applying and reports missing OpenTofu clearly ); const source = read('tools/repo-cli/src/check-aws-infrastructure.mjs'); assert.match(source, /init', '-backend=false/); + assert.match(source, /'-lockfile=readonly'/u); assert.match(source, /validate', '-no-color/); assert.match(source, /process\.exitCode \?\? 0/); assert.match(source, /missing required safety boundary/u); assert.doesNotMatch(source, /tofu',\s*\['apply'/u); }); +test('AWS provider selection is locked for reproducible validation', () => { + const lock = read('infrastructure/aws/environments/alpha/.terraform.lock.hcl'); + assert.match(lock, /registry\.opentofu\.org\/hashicorp\/aws/u); + assert.match(lock, /version\s+=\s+"6\.0\.0"/u); + assert.match(lock, /constraints\s+=\s+"6\.0\.0"/u); + assert.match(read('.gitattributes'), /^\*\.hcl text eol=lf$/m); +}); + test('AWS production profile enables recovery and prevents public data paths', () => { const production = read('infrastructure/aws/environments/alpha/production.tfvars.example'); const versions = read('infrastructure/aws/environments/alpha/versions.tf'); From 651425abb3dd42e9e31852f31732c4c6a05e7545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 15:33:30 +0700 Subject: [PATCH 06/73] feat(infra): add containerized OpenTofu validation --- package.json | 1 + tools/repo-cli/src/validate-aws-opentofu.mjs | 111 ++++++++++++++++++ .../repo-cli/test/aws-infrastructure.test.mjs | 20 ++++ 3 files changed, 132 insertions(+) create mode 100644 tools/repo-cli/src/validate-aws-opentofu.mjs diff --git a/package.json b/package.json index fb21c49a..2cc9bbc7 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "local:smoke": "node tools/repo-cli/src/local-services-smoke.mjs", "local:services": "node tools/repo-cli/src/local-services.mjs", "infra:check": "node tools/repo-cli/src/check-aws-infrastructure.mjs", + "infra:validate": "node tools/repo-cli/src/validate-aws-opentofu.mjs", "orchestration:check": "node tools/repo-cli/src/check-execution-orchestration.mjs", "repo:bootstrap": "corepack pnpm install --frozen-lockfile", "repo:build": "corepack pnpm build", diff --git a/tools/repo-cli/src/validate-aws-opentofu.mjs b/tools/repo-cli/src/validate-aws-opentofu.mjs new file mode 100644 index 00000000..0c6f3ad6 --- /dev/null +++ b/tools/repo-cli/src/validate-aws-opentofu.mjs @@ -0,0 +1,111 @@ +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..'); +const infrastructureRoot = path.join(repositoryRoot, 'infrastructure', 'aws'); +const alphaDirectory = '/workspace/environments/alpha'; +const containerDataDirectory = '/tmp/databreeze-tofu'; + +function usage() { + console.log(`Usage: pnpm infra:validate + +Runs format, backend-disabled initialization, and validation through the +official pinned OpenTofu container. The command never plans or applies +infrastructure and removes its isolated provider cache on completion.`); +} + +function fail(message) { + throw new Error(`AWS OpenTofu validation: ${message}`); +} + +function runDocker(args, timeout = 600_000) { + const result = spawnSync('docker', args, { + cwd: repositoryRoot, + stdio: 'inherit', + timeout, + }); + if (result.error?.code === 'ENOENT') fail('Docker CLI is not installed or not on PATH'); + if (result.error?.code === 'ETIMEDOUT') fail(`docker ${args[0]} timed out after ${timeout}ms`); + if (result.error || result.status !== 0) { + fail(`docker ${args[0]} failed with status ${result.status ?? 'unknown'}`); + } +} + +function removeValidationDirectory(directory) { + const temporaryRoot = path.resolve(os.tmpdir()); + const resolved = path.resolve(directory); + if ( + !resolved.startsWith(`${temporaryRoot}${path.sep}`) || + !path.basename(resolved).startsWith('databreeze-tofu-') + ) { + fail('refusing to remove a provider cache outside the bounded temporary directory'); + } + rmSync(resolved, { recursive: true, force: true }); +} + +export function main(argv = process.argv.slice(2)) { + if (argv.includes('--help') || argv.includes('-h')) { + usage(); + return; + } + if (argv.length > 0) fail(`unknown argument: ${argv[0]}`); + + const version = readFileSync(path.join(infrastructureRoot, '.opentofu-version'), 'utf8').trim(); + if (!/^\d+\.\d+\.\d+$/u.test(version)) fail('version pin is not an exact semantic version'); + const image = `ghcr.io/opentofu/opentofu:${version}`; + const sourceMount = `type=bind,source=${infrastructureRoot},target=/workspace`; + const validationDirectory = mkdtempSync(path.join(os.tmpdir(), 'databreeze-tofu-')); + const dataMount = `type=bind,source=${validationDirectory},target=${containerDataDirectory}`; + + try { + runDocker([ + 'run', + '--rm', + '--mount', + sourceMount, + image, + 'fmt', + '-check', + '-recursive', + '/workspace', + ]); + const base = [ + 'run', + '--rm', + '--workdir', + alphaDirectory, + '--mount', + sourceMount, + '--mount', + dataMount, + '--env', + 'TF_DATA_DIR=/tmp/databreeze-tofu', + image, + ]; + runDocker([ + ...base, + 'init', + '-backend=false', + '-input=false', + '-lockfile=readonly', + '-no-color', + ]); + runDocker([...base, 'validate', '-no-color']); + } finally { + removeValidationDirectory(validationDirectory); + } + + console.log(`AWS OpenTofu ${version} container validation passed without planning or applying.`); +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + try { + main(); + } catch (error) { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; + } +} diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index f8e8eb4a..b1d5ea57 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -15,6 +15,26 @@ test('AWS validation pins one OpenTofu CLI and official container release', () = assert.match(readme, /ghcr\.io\/opentofu\/opentofu:1\.12\.5/u); }); +test('AWS container validation command is pinned, isolated, and non-applying', () => { + const script = path.join(repositoryRoot, 'tools/repo-cli/src/validate-aws-opentofu.mjs'); + const help = spawnSync(process.execPath, [script, '--help'], { + cwd: repositoryRoot, + encoding: 'utf8', + }); + assert.equal(help.status, 0, help.stderr); + assert.match(help.stdout, /official pinned OpenTofu container/u); + const source = read('tools/repo-cli/src/validate-aws-opentofu.mjs'); + assert.match(source, /'fmt',\s*'-check',\s*'-recursive'/u); + assert.match(source, /'init',\s*'-backend=false',\s*'-input=false',\s*'-lockfile=readonly'/u); + assert.match(source, /'validate', '-no-color'/u); + assert.match(source, /TF_DATA_DIR=\/tmp\/databreeze-tofu/u); + assert.doesNotMatch(source, /['"]apply['"]/u); + assert.match( + read('package.json'), + /"infra:validate": "node tools\/repo-cli\/src\/validate-aws-opentofu\.mjs"/u, + ); +}); + test('AWS foundation has reusable modules and safe alpha composition', () => { for (const relativePath of [ 'infrastructure/aws/modules/network/main.tf', From c18c7b0a65b5bfae0c5991223bfbee527018cf52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 15:43:20 +0700 Subject: [PATCH 07/73] test(infra): exercise credential-free alpha planning --- infrastructure/aws/README.md | 3 ++ .../alpha/tests/alpha-plan.tofutest.hcl | 41 +++++++++++++++++++ .../repo-cli/src/check-aws-infrastructure.mjs | 1 + tools/repo-cli/src/validate-aws-opentofu.mjs | 1 + .../repo-cli/test/aws-infrastructure.test.mjs | 1 + 5 files changed, 47 insertions(+) create mode 100644 infrastructure/aws/environments/alpha/tests/alpha-plan.tofutest.hcl diff --git a/infrastructure/aws/README.md b/infrastructure/aws/README.md index f0501bea..7255c9a0 100644 --- a/infrastructure/aws/README.md +++ b/infrastructure/aws/README.md @@ -26,6 +26,9 @@ native CLI release, or the official available. Do not use a floating container tag for validation evidence. The alpha composition commits `.terraform.lock.hcl`; initialization uses it read-only so provider selections cannot drift during a validation run. +`pnpm infra:validate` also executes a mocked plan test for the safe alpha +defaults, exercising plan-time evaluation without AWS credentials or remote +side effects. ```text pnpm infra:check diff --git a/infrastructure/aws/environments/alpha/tests/alpha-plan.tofutest.hcl b/infrastructure/aws/environments/alpha/tests/alpha-plan.tofutest.hcl new file mode 100644 index 00000000..42f9ee88 --- /dev/null +++ b/infrastructure/aws/environments/alpha/tests/alpha-plan.tofutest.hcl @@ -0,0 +1,41 @@ +mock_provider "aws" { + mock_data "aws_caller_identity" { + defaults = { + account_id = "123456789012" + arn = "arn:aws:iam::123456789012:root" + user_id = "123456789012" + } + } + + mock_data "aws_iam_policy_document" { + defaults = { + json = "{\"Version\":\"2012-10-17\",\"Statement\":[]}" + } + } + + mock_resource "aws_iam_role" { + defaults = { + arn = "arn:aws:iam::123456789012:role/databreeze-mock" + id = "databreeze-mock" + } + } +} + +run "safe_alpha_defaults_plan" { + command = plan + + assert { + condition = output.region == "ap-southeast-1" + error_message = "The alpha plan must remain in the approved Singapore region." + } + + assert { + condition = !var.enable_nat_gateway && !var.enable_database && !var.enable_ecs_services + error_message = "The credential-free alpha plan must keep recurring-cost services disabled." + } + + assert { + condition = !var.enable_cloudfront && var.github_repository == "" + error_message = "The alpha plan must not create public distribution or deployment trust by default." + } +} diff --git a/tools/repo-cli/src/check-aws-infrastructure.mjs b/tools/repo-cli/src/check-aws-infrastructure.mjs index 225b8176..8d12bd54 100644 --- a/tools/repo-cli/src/check-aws-infrastructure.mjs +++ b/tools/repo-cli/src/check-aws-infrastructure.mjs @@ -12,6 +12,7 @@ const requiredFiles = [ '.opentofu-version', 'environments/alpha/main.tf', 'environments/alpha/.terraform.lock.hcl', + 'environments/alpha/tests/alpha-plan.tofutest.hcl', 'environments/alpha/variables.tf', 'environments/alpha/versions.tf', 'modules/network/main.tf', diff --git a/tools/repo-cli/src/validate-aws-opentofu.mjs b/tools/repo-cli/src/validate-aws-opentofu.mjs index 0c6f3ad6..82cae21d 100644 --- a/tools/repo-cli/src/validate-aws-opentofu.mjs +++ b/tools/repo-cli/src/validate-aws-opentofu.mjs @@ -94,6 +94,7 @@ export function main(argv = process.argv.slice(2)) { '-no-color', ]); runDocker([...base, 'validate', '-no-color']); + runDocker([...base, 'test', '-no-color']); } finally { removeValidationDirectory(validationDirectory); } diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index b1d5ea57..91b34f58 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -27,6 +27,7 @@ test('AWS container validation command is pinned, isolated, and non-applying', ( assert.match(source, /'fmt',\s*'-check',\s*'-recursive'/u); assert.match(source, /'init',\s*'-backend=false',\s*'-input=false',\s*'-lockfile=readonly'/u); assert.match(source, /'validate', '-no-color'/u); + assert.match(source, /'test', '-no-color'/u); assert.match(source, /TF_DATA_DIR=\/tmp\/databreeze-tofu/u); assert.doesNotMatch(source, /['"]apply['"]/u); assert.match( From cc8703286f213fbf63d26b14534195881c66b8d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 15:44:14 +0700 Subject: [PATCH 08/73] docs(foundation): verify portable AWS validation --- .../foundation-aws-opentofu-2026-08-03.md | 48 +++++++++++++++++++ .../002-complete-execution-orchestration.md | 2 +- docs/plans/004-luna-max-execution-plan.md | 8 ++-- docs/plans/execution-orchestration.json | 16 ++++--- .../test/execution-orchestration.test.mjs | 2 +- 5 files changed, 64 insertions(+), 12 deletions(-) create mode 100644 docs/operations/foundation-aws-opentofu-2026-08-03.md diff --git a/docs/operations/foundation-aws-opentofu-2026-08-03.md b/docs/operations/foundation-aws-opentofu-2026-08-03.md new file mode 100644 index 00000000..0541dc10 --- /dev/null +++ b/docs/operations/foundation-aws-opentofu-2026-08-03.md @@ -0,0 +1,48 @@ +# FND-004 portable AWS validation evidence + +Observed at (UTC): 2026-08-03T08:43:33Z + +Task: `FND-004 — Close portable AWS foundation gaps` + +## Verified boundary + +- OpenTofu is pinned to `1.12.5`; the official container image is + `ghcr.io/opentofu/opentofu:1.12.5`. +- The alpha composition locks the signed `hashicorp/aws` provider at `6.0.0` + and initialization treats the lock file as read-only. +- Official formatting covers every module, environment file, and OpenTofu test. +- Backend-disabled initialization and configuration validation run with provider + data isolated outside the repository. +- A mocked plan exercises the Singapore alpha composition without AWS + credentials or provider API calls. It verifies that NAT, managed data, ECS + services, CloudFront, and GitHub deployment trust remain disabled by default. +- Static tests continue to verify private networking, encryption, recovery, + least-privilege OIDC scope, production image digests, destroy protection, and + the absence of credentials or state backends. + +## Commands and results + +Passed: + +- `node --test tools/repo-cli/test/aws-infrastructure.test.mjs` +- `corepack pnpm infra:check` +- `corepack pnpm infra:validate` +- OpenTofu `fmt -check -recursive` +- OpenTofu `init -backend=false -input=false -lockfile=readonly -no-color` +- OpenTofu `validate -no-color` +- OpenTofu `test -no-color`: one mocked plan passed + +The first provider download ended with `unexpected EOF`; a fresh isolated retry +installed the same locked, signed provider successfully. No source or lock drift +was accepted from that transient failure. + +## Safety and rollback + +No AWS credentials were loaded, no remote state backend was configured, and no +real plan or apply command ran. Provider caches were created under a guarded +temporary directory and removed after validation. + +Rollback is source-only: revert the version pin, provider lock, formatter, +runner, and plan-test commits together, then restore FND-004 to `implemented` in +the execution ledger. Reverting does not change any AWS resource because this +slice created none. diff --git a/docs/plans/002-complete-execution-orchestration.md b/docs/plans/002-complete-execution-orchestration.md index 2d205f71..e4af716f 100644 --- a/docs/plans/002-complete-execution-orchestration.md +++ b/docs/plans/002-complete-execution-orchestration.md @@ -66,7 +66,7 @@ This plan was reconciled on 2026-08-02 from remote `dev` at `783a4710c0aa2a2808d Merged PRs 1–23 establish substantial engineering, IAM/AUD/BUA, IAE/DSM, JRA, and DSO code. PR 19 delivered the normal 73-commit foundation batch to `dev`; PR 20 promoted it to `main`; PRs 21–23 carried validated promotion-review fixes back through `dev`. Plans 010–050 must therefore start with evidence reconciliation, not blind reimplementation. Plans 060–500 remain unverified and must be treated as planned until their gates pass. -The active execution packet is `B01` in `004-luna-max-execution-plan.md`, continuing with `FND-004` after live verification closed `FND-003`. The packet preserves the requested 30–50 commit target and exceptional 79-commit ceiling, and carries implementation forward without opening a documentation-only PR. +The active execution packet is `B01` in `004-luna-max-execution-plan.md`, continuing with `FND-005` after live verification closed `FND-003` and containerized OpenTofu verification closed `FND-004`. The packet preserves the requested 30–50 commit target and exceptional 79-commit ceiling, and carries implementation forward without opening a documentation-only PR. The hashes above are an audit anchor, not a branch lock. Every session must fetch and recompute live state; update the ledger checkpoint only as part of a committed task/PR handoff so session-local observations do not create meaningless dirty files. diff --git a/docs/plans/004-luna-max-execution-plan.md b/docs/plans/004-luna-max-execution-plan.md index e6e63723..6d3e2693 100644 --- a/docs/plans/004-luna-max-execution-plan.md +++ b/docs/plans/004-luna-max-execution-plan.md @@ -64,7 +64,7 @@ Each batch may require multiple normal integration PR slices before its exit gat | Batch | Branch | Tasks | Dependencies | Commit budget | Exit gate | |---|---|---|---|---|---| -| `B01` | `feat/foundation-identity-reconciliation` | `FND-004..007`, all Plan 020 tasks | Verified `FND-001..003` | 30–50 target; exceptional ceiling 79 | Foundation external gates recorded; IAM/AUD/BUA obligations reconciled and completed | +| `B01` | `feat/foundation-identity-reconciliation` | `FND-005..007`, all Plan 020 tasks | Verified `FND-001..004` | 30–50 target; exceptional ceiling 79 | Foundation external gates recorded; IAM/AUD/BUA obligations reconciled and completed | | `B02` | `feat/artifacts-datasets-completion` | All Plan 030 tasks | `B01` | 30–50 target; exceptional ceiling 79 | Immutable artifact/evidence/dataset foundations verified | | `B03` | `feat/jobs-processing-completion` | All Plan 040 tasks | `B02` | 30–50 target; exceptional ceiling 79 | Signed typed jobs execute locally/cloud with approvals and durable recovery | | `B04` | `feat/devices-sync-completion` | All Plan 050 tasks | `B03` | 30–50 target; exceptional ceiling 79 | Desktop/Android sync, offline, conflict, transfer, and revocation gates pass | @@ -145,10 +145,10 @@ corepack pnpm orchestration:check corepack pnpm requirements:check ``` -Live Docker verification closed `FND-003` on 2026-08-03. Resume `FND-004`: +Live Docker and containerized OpenTofu verification closed `FND-003` and `FND-004` on 2026-08-03. Resume `FND-005`: -1. Run pinned OpenTofu formatting, initialization without a backend, and validation for the portable AWS modules without applying infrastructure. -2. If OpenTofu remains unavailable, preserve `FND-004` as implemented but unverified, record the exact external gate, and continue only credential-independent `FND-005..007` reconciliation. +1. Reconcile correlation propagation, allowlisted telemetry, and safe diagnostics across TypeScript, Kotlin, and Python against the merged implementation. +2. Preserve any hosted or platform-specific telemetry gap as explicit evidence; do not promote the task solely from static source presence. 3. Reconcile Plans 020–050 against merged code before implementing any missing behavior. For `B01`, complete Plan 020 only after the remaining foundation boundaries are explicit. 4. End every session with the handoff record from `003-luna-handoff-runbook.md`, including exact branch/HEAD, open PRs, checks, task/batch status, rollback points, and safest next command. diff --git a/docs/plans/execution-orchestration.json b/docs/plans/execution-orchestration.json index 4049b3c5..e1e404dc 100644 --- a/docs/plans/execution-orchestration.json +++ b/docs/plans/execution-orchestration.json @@ -54,7 +54,7 @@ "post-ga-planned", "blocked" ], - "nextTaskId": "FND-004", + "nextTaskId": "FND-005", "activeBatchId": "B01", "taskState": { "FND-001": { @@ -95,15 +95,20 @@ "note": "Docker Engine 29.5.3 and Compose 5.1.4 live verification passed on 2026-08-03: all long-running services became healthy, MinIO initialization exited successfully, all 19 module schemas existed, restart and Redis persistence checks passed, collision and disk-pressure probes failed closed, and safe stop preserved containers and named volumes." }, "FND-004": { - "status": "implemented", - "commit": "3ed3d77d0281ef239d0509c81ded447d8fffd213", + "status": "verified", + "commit": "c18c7b0a65b5bfae0c5991223bfbee527018cf52", "evidence": [ "infrastructure/aws/README.md", + "infrastructure/aws/.opentofu-version", "infrastructure/aws/environments/alpha/main.tf", + "infrastructure/aws/environments/alpha/.terraform.lock.hcl", + "infrastructure/aws/environments/alpha/tests/alpha-plan.tofutest.hcl", + "tools/repo-cli/src/validate-aws-opentofu.mjs", "tools/repo-cli/src/check-aws-infrastructure.mjs", - "tools/repo-cli/test/aws-infrastructure.test.mjs" + "tools/repo-cli/test/aws-infrastructure.test.mjs", + "docs/operations/foundation-aws-opentofu-2026-08-03.md" ], - "note": "Portable AWS modules and static safety checks are promoted. Pinned OpenTofu fmt/init/validate and any reviewed plan/apply evidence remain external gates." + "note": "OpenTofu 1.12.5 official-container formatting, backend-disabled locked initialization, validation, and one credential-free mocked alpha plan passed on 2026-08-03. Static safety tests cover encryption, private networking, OIDC scope, recovery, destroy protection, and production image digests; no AWS credentials, remote state, real plan, or apply were used." }, "FND-005": { "status": "implemented", @@ -150,7 +155,6 @@ "commitBudget": { "minimum": 30, "target": 45, "maximum": 79 }, "maximumChangedFiles": 260, "taskIds": [ - "FND-004", "FND-005", "FND-006", "FND-007", diff --git a/tools/repo-cli/test/execution-orchestration.test.mjs b/tools/repo-cli/test/execution-orchestration.test.mjs index 9c8f1d09..5132f4e6 100644 --- a/tools/repo-cli/test/execution-orchestration.test.mjs +++ b/tools/repo-cli/test/execution-orchestration.test.mjs @@ -205,7 +205,7 @@ test('repository checker validates the committed orchestration package', () => { test('ledger records verified task evidence before advancing the next task', () => { const ledger = readJson('docs/plans/execution-orchestration.json'); - assert.equal(ledger.nextTaskId, 'FND-004'); + assert.equal(ledger.nextTaskId, 'FND-005'); assert.equal(ledger.activeBatchId, 'B01'); assert.equal(ledger.checkpoint.remoteDev, '783a4710c0aa2a2808d78ad7f0643e6731150bd7'); assert.equal(ledger.checkpoint.remoteMain, '3ed3d77d0281ef239d0509c81ded447d8fffd213'); From 68e0e4aef2ea0aad672d2abbed1776694488bcd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:28:58 +0700 Subject: [PATCH 09/73] fix(iae): bind admission to repository artifacts --- .../application/artifact-admission.service.ts | 2 +- .../iae/artifact-admission.service.test.ts | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/services/api/src/features/iae/application/artifact-admission.service.ts b/services/api/src/features/iae/application/artifact-admission.service.ts index b36451d8..8f9bd581 100644 --- a/services/api/src/features/iae/application/artifact-admission.service.ts +++ b/services/api/src/features/iae/application/artifact-admission.service.ts @@ -31,7 +31,7 @@ export class ArtifactAdmissionService { return this.repository.withTransaction(context, async (transaction) => { const artifact = await transaction.findVersion(context, versionId); if (!artifact) return Object.freeze({ accepted: false, code: 'ARTIFACT_NOT_FOUND' as const }); - const admission = finalizeArtifactAdmissionV1({ artifact, ...input }); + const admission = finalizeArtifactAdmissionV1({ ...input, artifact }); if (!admission.accepted) return admission; const updated = await transaction.updateVersionStatus( context, diff --git a/services/api/test/features/iae/artifact-admission.service.test.ts b/services/api/test/features/iae/artifact-admission.service.test.ts index 5c494f34..9c62c684 100644 --- a/services/api/test/features/iae/artifact-admission.service.test.ts +++ b/services/api/test/features/iae/artifact-admission.service.test.ts @@ -59,3 +59,51 @@ void test('IAE-009/010 admission updates only the status projection after scanne }); assert.deepEqual(rejected, { accepted: false, code: 'DIGEST_MISMATCH' }); }); + +void test('IAE-009 admission never lets request input replace the repository artifact', async () => { + const repository = new InMemoryArtifactRepositoryAdapter(); + const service = new ArtifactAdmissionService(repository); + const artifact = createArtifactVersionV1({ + artifactId: '55555555-5555-4555-8555-555555555555', + versionId: '66666666-6666-4666-8666-666666666666', + tenantScope: context.tenantScope, + sourceKind: 'FILE', + dataMode: 'Hybrid', + contentSha256: 'a'.repeat(64), + byteSize: 4, + mediaType: 'text/csv', + displayName: 'orders.csv', + createdAt: '2026-08-02T00:00:00.000Z', + status: 'QUARANTINED', + }); + const attackerArtifact = createArtifactVersionV1({ + artifactId: '77777777-7777-4777-8777-777777777777', + versionId: '88888888-8888-4888-8888-888888888888', + tenantScope: context.tenantScope, + sourceKind: 'FILE', + dataMode: 'Hybrid', + contentSha256: 'b'.repeat(64), + byteSize: 4, + mediaType: 'text/csv', + displayName: 'attacker.csv', + createdAt: '2026-08-02T00:00:00.000Z', + status: 'QUARANTINED', + }); + assert.equal(artifact.accepted, true); + assert.equal(attackerArtifact.accepted, true); + if (!artifact.accepted || !attackerArtifact.accepted) return; + await repository.saveVersion(context, artifact.value); + + const untrustedInput = { + actualSha256: 'b'.repeat(64), + actualByteSize: 4, + detectedMediaType: 'text/csv', + scanState: 'CLEAN' as const, + maxByteSize: 100, + artifact: attackerArtifact.value, + }; + const result = await service.admit(context, artifact.value.versionId, untrustedInput); + + assert.deepEqual(result, { accepted: false, code: 'DIGEST_MISMATCH' }); + assert.equal((await repository.findVersion(context, artifact.value.versionId))?.status, 'QUARANTINED'); +}); From 069c0cdb8600d4a1ec7d131ae99c592845da26ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:29:42 +0700 Subject: [PATCH 10/73] fix(engine): bound spreadsheet XML reads --- .../processors/spreadsheet_auditor.py | 13 ++++++++++--- services/engine/tests/test_spreadsheet_auditor.py | 13 +++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/services/engine/src/databreeze_engine/processors/spreadsheet_auditor.py b/services/engine/src/databreeze_engine/processors/spreadsheet_auditor.py index afaa4270..97829224 100644 --- a/services/engine/src/databreeze_engine/processors/spreadsheet_auditor.py +++ b/services/engine/src/databreeze_engine/processors/spreadsheet_auditor.py @@ -80,6 +80,13 @@ def _xml(data: bytes) -> Xml.Element: raise SpreadsheetAuditError("MALFORMED_XML") from None +def _xml_member(archive: zipfile.ZipFile, name: str) -> Xml.Element: + """Read at most one byte beyond the XML budget before rejecting a member.""" + with archive.open(name, "r") as member: + data = member.read(_MAX_XML_BYTES + 1) + return _xml(data) + + def _column_number(column: str) -> int: value = 0 for character in column.upper(): @@ -140,8 +147,8 @@ def _relationships(root: Xml.Element) -> dict[str, str]: def _sheet_targets(archive: zipfile.ZipFile) -> list[tuple[str, str]]: - workbook = _xml(archive.read("xl/workbook.xml")) - relationships = _relationships(_xml(archive.read("xl/_rels/workbook.xml.rels"))) + workbook = _xml_member(archive, "xl/workbook.xml") + relationships = _relationships(_xml_member(archive, "xl/_rels/workbook.xml.rels")) sheets: list[tuple[str, str]] = [] for sheet in workbook.findall(f"{{{_SHEET_NS}}}sheets/{{{_SHEET_NS}}}sheet"): name = sheet.attrib.get("name") @@ -213,7 +220,7 @@ def audit_workbook( for sheet_name, target in targets: if target not in names: raise SpreadsheetAuditError("INVALID_ARCHIVE") - root = _xml(archive.read(target)) + root = _xml_member(archive, target) max_row = 0 max_column = 0 cells: list[tuple[str, str | None]] = [] diff --git a/services/engine/tests/test_spreadsheet_auditor.py b/services/engine/tests/test_spreadsheet_auditor.py index ffc8e7c9..759a05d8 100644 --- a/services/engine/tests/test_spreadsheet_auditor.py +++ b/services/engine/tests/test_spreadsheet_auditor.py @@ -106,6 +106,19 @@ def test_audit_rejects_archive_traversal_and_cell_resource_exhaustion() -> None: audit_workbook(_workbook(), max_cells=1) +def test_audit_streams_xml_members_through_a_bounded_reader( + monkeypatch: pytest.MonkeyPatch, +) -> None: + def reject_unbounded_read(*_args: object, **_kwargs: object) -> bytes: + raise AssertionError("ZipFile.read must not decompress untrusted XML without a bound") + + monkeypatch.setattr(zipfile.ZipFile, "read", reject_unbounded_read) + + result = audit_workbook(_workbook()) + + assert result.sheets[0].name == "Inventory" + + def test_manifest_adds_opaque_identities_without_source_values() -> None: result = audit_workbook(_workbook()) manifest = build_spreadsheet_audit_manifest( From 718b40664630eb3de7e29de4cc0ed7c084d764a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:30:16 +0700 Subject: [PATCH 11/73] test(iae): emulate Prisma uniqueness in fixtures --- .../features/iae/prisma-artifact-export-repository.test.ts | 3 +++ .../features/iae/prisma-artifact-intake-repository.test.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/services/api/test/features/iae/prisma-artifact-export-repository.test.ts b/services/api/test/features/iae/prisma-artifact-export-repository.test.ts index 10482b69..3e03102f 100644 --- a/services/api/test/features/iae/prisma-artifact-export-repository.test.ts +++ b/services/api/test/features/iae/prisma-artifact-export-repository.test.ts @@ -53,6 +53,9 @@ void test('IAE-018 Prisma export adapter preserves immutable manifests and scope artifactExportManifestRecord: { create({ data }) { const row = { ...data }; + if (rows.has(row.id)) { + throw Object.assign(new Error('fixture unique constraint violation'), { code: 'P2002' }); + } rows.set(row.id, row); return Promise.resolve(row); }, diff --git a/services/api/test/features/iae/prisma-artifact-intake-repository.test.ts b/services/api/test/features/iae/prisma-artifact-intake-repository.test.ts index d16113f5..8bc8ff36 100644 --- a/services/api/test/features/iae/prisma-artifact-intake-repository.test.ts +++ b/services/api/test/features/iae/prisma-artifact-intake-repository.test.ts @@ -56,6 +56,9 @@ function client(rows: ArtifactIntakeDatabaseRowV1[]): ArtifactIntakeDatabaseClie create(input) { const created = { ...input.data }; const persisted = { ...created } as ArtifactIntakeDatabaseRowV1; + if (rows.some((candidate) => candidate.id === persisted.id)) { + throw Object.assign(new Error('fixture unique constraint violation'), { code: 'P2002' }); + } rows.push(persisted); return Promise.resolve(persisted); }, From 5ed2cb470d39a6f395195b07dc6aab2f348c9012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:30:46 +0700 Subject: [PATCH 12/73] fix(engine): tolerate sparse quality state counts --- .../processors/dataset_quality.py | 6 +++--- services/engine/tests/test_dataset_quality.py | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/services/engine/src/databreeze_engine/processors/dataset_quality.py b/services/engine/src/databreeze_engine/processors/dataset_quality.py index 264fa8f1..dbab8c50 100644 --- a/services/engine/src/databreeze_engine/processors/dataset_quality.py +++ b/services/engine/src/databreeze_engine/processors/dataset_quality.py @@ -57,9 +57,9 @@ def _required_count(profile: DatasetProfile, field: str) -> int | None: for summary in profile.fields: if summary.field == field: return ( - summary.stateCounts["MISSING"] - + summary.stateCounts["NULL"] - + summary.stateCounts["BLANK"] + summary.stateCounts.get("MISSING", 0) + + summary.stateCounts.get("NULL", 0) + + summary.stateCounts.get("BLANK", 0) ) return None diff --git a/services/engine/tests/test_dataset_quality.py b/services/engine/tests/test_dataset_quality.py index 30b454ec..f02115f9 100644 --- a/services/engine/tests/test_dataset_quality.py +++ b/services/engine/tests/test_dataset_quality.py @@ -45,6 +45,26 @@ def test_missing_profiled_field_is_disclosed_and_error_blocks() -> None: assert result.findings[0].occurrenceCount == 1 +def test_required_quality_treats_omitted_zero_state_counts_as_zero() -> None: + profile = profile_records([{"code": "A"}], ["code"]) + summary = profile.fields[0].model_copy(update={"stateCounts": {"VALUE": 1}}) + sparse_profile = profile.model_copy(update={"fields": (summary,)}) + + result = evaluate_required_fields( + sparse_profile, + [ + { + "ruleId": "00000000-0000-4000-8000-000000000003", + "field": "code", + "severity": "ERROR", + } + ], + ) + + assert result.qualityState == "PASS" + assert result.findings == () + + def test_invalid_rule_shape_fails_closed() -> None: profile = profile_records([{"code": "A"}], ["code"]) try: From 96553d0b242833e139c909c6eb832208e0f7f950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:31:35 +0700 Subject: [PATCH 13/73] fix(sa): reject duplicate blocked reasons --- services/api/src/features/sa/api/spreadsheet-audit.dto.ts | 2 ++ .../test/features/sa/spreadsheet-audit.controller.test.ts | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/services/api/src/features/sa/api/spreadsheet-audit.dto.ts b/services/api/src/features/sa/api/spreadsheet-audit.dto.ts index c562acba..455cf261 100644 --- a/services/api/src/features/sa/api/spreadsheet-audit.dto.ts +++ b/services/api/src/features/sa/api/spreadsheet-audit.dto.ts @@ -2,6 +2,7 @@ import { Type } from 'class-transformer'; import { ArrayMaxSize, ArrayMinSize, + ArrayUnique, IsArray, IsIn, IsInt, @@ -107,6 +108,7 @@ export class CreateSpreadsheetAuditResultDto { @ApiProperty({ enum: ['MACRO', 'EXTERNAL_LINK', 'UNSUPPORTED_XML'], isArray: true }) @IsArray() @ArrayMaxSize(3) + @ArrayUnique() @IsIn(['MACRO', 'EXTERNAL_LINK', 'UNSUPPORTED_XML'], { each: true }) blockedReasons!: Array<'MACRO' | 'EXTERNAL_LINK' | 'UNSUPPORTED_XML'>; diff --git a/services/api/test/features/sa/spreadsheet-audit.controller.test.ts b/services/api/test/features/sa/spreadsheet-audit.controller.test.ts index 5f0dca75..77b5681b 100644 --- a/services/api/test/features/sa/spreadsheet-audit.controller.test.ts +++ b/services/api/test/features/sa/spreadsheet-audit.controller.test.ts @@ -68,6 +68,13 @@ void test('SA-001/SA-004 HTTP stores value-free audit results and rejects source delete rejectedWithoutCorrelation['correlationId']; assert.doesNotMatch(JSON.stringify(rejectedWithoutCorrelation), /SUM|42|sourceValue/iu); + const duplicateReason = await app.inject({ + method: 'POST', + url: '/v1/spreadsheet-audits', + payload: { ...payload, blockedReasons: ['MACRO', 'MACRO'] }, + }); + assert.equal(duplicateReason.statusCode, 400); + const created = await app.inject({ method: 'POST', url: '/v1/spreadsheet-audits', From 6d67783a97c004b78fbafe09cd25279520715625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:32:44 +0700 Subject: [PATCH 14/73] fix(sa): serialize in-memory audit writes --- ...ry-spreadsheet-audit-repository.adapter.ts | 27 +++++++++++++++-- .../sa/spreadsheet-audit.service.test.ts | 30 +++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/services/api/src/features/sa/adapter/in-memory-spreadsheet-audit-repository.adapter.ts b/services/api/src/features/sa/adapter/in-memory-spreadsheet-audit-repository.adapter.ts index 43ff005f..c3634fd6 100644 --- a/services/api/src/features/sa/adapter/in-memory-spreadsheet-audit-repository.adapter.ts +++ b/services/api/src/features/sa/adapter/in-memory-spreadsheet-audit-repository.adapter.ts @@ -26,6 +26,13 @@ export class InMemorySpreadsheetAuditRepositoryAdapter implements SpreadsheetAud private transactionTail: Promise = Promise.resolve(); public async save(context: IamTenantContextV1, result: SpreadsheetAuditResultV1): Promise { + await this.withTransaction(context, (transaction) => transaction.save(context, result)); + } + + private async saveUnlocked( + context: IamTenantContextV1, + result: SpreadsheetAuditResultV1, + ): Promise { await Promise.resolve(); if (!tenantScopeContainsV1(context.tenantScope, result.tenantScope)) throw new Error('SA_SCOPE_NARROWING_REQUIRED'); @@ -38,6 +45,13 @@ export class InMemorySpreadsheetAuditRepositoryAdapter implements SpreadsheetAud public async find( context: IamTenantContextV1, auditId: SpreadsheetAuditResultV1['auditId'], + ): Promise { + return this.findUnlocked(context, auditId); + } + + private async findUnlocked( + context: IamTenantContextV1, + auditId: SpreadsheetAuditResultV1['auditId'], ): Promise { await Promise.resolve(); const result = this.results.get(auditId); @@ -47,6 +61,13 @@ export class InMemorySpreadsheetAuditRepositoryAdapter implements SpreadsheetAud public async list( context: IamTenantContextV1, artifactVersionId: SpreadsheetAuditResultV1['artifactVersionId'], + ): Promise { + return this.listUnlocked(context, artifactVersionId); + } + + private async listUnlocked( + context: IamTenantContextV1, + artifactVersionId: SpreadsheetAuditResultV1['artifactVersionId'], ): Promise { await Promise.resolve(); return [...this.results.values()] @@ -72,9 +93,9 @@ export class InMemorySpreadsheetAuditRepositoryAdapter implements SpreadsheetAud const before = new Map(this.results); try { return await work({ - save: this.save.bind(this), - find: this.find.bind(this), - list: this.list.bind(this), + save: this.saveUnlocked.bind(this), + find: this.findUnlocked.bind(this), + list: this.listUnlocked.bind(this), }); } catch (error) { this.results = before; diff --git a/services/api/test/features/sa/spreadsheet-audit.service.test.ts b/services/api/test/features/sa/spreadsheet-audit.service.test.ts index 1b9aa151..875c9f4a 100644 --- a/services/api/test/features/sa/spreadsheet-audit.service.test.ts +++ b/services/api/test/features/sa/spreadsheet-audit.service.test.ts @@ -1,6 +1,8 @@ import { strict as assert } from 'node:assert'; import test from 'node:test'; +import { createSpreadsheetAuditResultV1 } from '@databreeze/domain/spreadsheet-audit/v1'; + import { InMemorySpreadsheetAuditRepositoryAdapter } from '../../../src/features/sa/adapter/in-memory-spreadsheet-audit-repository.adapter.js'; import { SpreadsheetAuditService } from '../../../src/features/sa/application/spreadsheet-audit.service.js'; import { createIamTenantContextV1 } from '../../../src/features/iam/application/tenant-context.js'; @@ -88,3 +90,31 @@ void test('[SA-005] service hides results from a different organization', async code: 'AUDIT_NOT_FOUND', }); }); + +void test('SA repository serializes public saves after a rolling-back transaction', async () => { + const repository = new InMemorySpreadsheetAuditRepositoryAdapter(); + const created = createSpreadsheetAuditResultV1(input); + assert.equal(created.accepted, true); + if (!created.accepted) return; + let enterTransaction!: () => void; + const transactionEntered = new Promise((resolve) => { + enterTransaction = resolve; + }); + let releaseTransaction!: () => void; + const transactionRelease = new Promise((resolve) => { + releaseTransaction = resolve; + }); + const rollingBack = repository.withTransaction(context, async () => { + enterTransaction(); + await transactionRelease; + throw new Error('ROLLBACK'); + }); + await transactionEntered; + + const saving = repository.save(context, created.value); + releaseTransaction(); + await assert.rejects(rollingBack, /ROLLBACK/u); + await saving; + + assert.deepEqual(await repository.find(context, created.value.auditId), created.value); +}); From 8b31681127c332a02e8f2ba6f30448c96dfdcb52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:33:41 +0700 Subject: [PATCH 15/73] fix(sa): require strict UTC audit timestamps --- services/api/src/features/sa/api/spreadsheet-audit.dto.ts | 3 ++- .../test/features/sa/spreadsheet-audit.controller.test.ts | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/services/api/src/features/sa/api/spreadsheet-audit.dto.ts b/services/api/src/features/sa/api/spreadsheet-audit.dto.ts index 455cf261..5d8bfd12 100644 --- a/services/api/src/features/sa/api/spreadsheet-audit.dto.ts +++ b/services/api/src/features/sa/api/spreadsheet-audit.dto.ts @@ -118,6 +118,7 @@ export class CreateSpreadsheetAuditResultDto { processorVersion!: string; @ApiProperty({ format: 'date-time' }) - @IsISO8601() + @IsISO8601({ strict: true, strictSeparator: true }) + @Matches(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u) createdAt!: string; } diff --git a/services/api/test/features/sa/spreadsheet-audit.controller.test.ts b/services/api/test/features/sa/spreadsheet-audit.controller.test.ts index 77b5681b..17a4e22c 100644 --- a/services/api/test/features/sa/spreadsheet-audit.controller.test.ts +++ b/services/api/test/features/sa/spreadsheet-audit.controller.test.ts @@ -75,6 +75,13 @@ void test('SA-001/SA-004 HTTP stores value-free audit results and rejects source }); assert.equal(duplicateReason.statusCode, 400); + const nonUtcTimestamp = await app.inject({ + method: 'POST', + url: '/v1/spreadsheet-audits', + payload: { ...payload, createdAt: '2026-08-04T07:00:00.000+07:00' }, + }); + assert.equal(nonUtcTimestamp.statusCode, 400); + const created = await app.inject({ method: 'POST', url: '/v1/spreadsheet-audits', From 3bfe6006965194d071597cd755b473bba23c41fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:35:05 +0700 Subject: [PATCH 16/73] fix(api): publish bounded collection contracts --- services/api/openapi/v1.json | 24 +++++++++++++++---- .../features/dsm/api/dataset-quality.dto.ts | 4 ++-- .../features/dsm/api/dataset-version.dto.ts | 2 +- .../features/dsm/api/governed-dataset.dto.ts | 4 +++- .../api/src/features/dsm/api/mapping.dto.ts | 7 ++++-- .../features/iae/api/artifact-export.dto.ts | 2 +- services/api/test/openapi.test.ts | 13 ++++++++++ 7 files changed, 45 insertions(+), 11 deletions(-) diff --git a/services/api/openapi/v1.json b/services/api/openapi/v1.json index a5706833..8df95684 100644 --- a/services/api/openapi/v1.json +++ b/services/api/openapi/v1.json @@ -7566,7 +7566,12 @@ "type": "object", "properties": { "manifestId": { "type": "string", "format": "uuid" }, - "versionIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, + "versionIds": { + "minItems": 1, + "maxItems": 1024, + "type": "array", + "items": { "type": "string", "format": "uuid" } + }, "approvalState": { "type": "string", "enum": ["NOT_REQUIRED", "PENDING", "APPROVED", "REJECTED"] @@ -7716,6 +7721,7 @@ "versionId": { "type": "string", "format": "uuid" }, "name": { "type": "string", "maxLength": 200 }, "fields": { + "maxItems": 256, "type": "array", "items": { "$ref": "#/components/schemas/GovernedDatasetFieldDto" } }, @@ -7759,7 +7765,11 @@ "versionId": { "type": "string", "format": "uuid" }, "sourceSchemaVersionId": { "type": "string", "format": "uuid" }, "targetSchemaVersionId": { "type": "string", "format": "uuid" }, - "steps": { "type": "array", "items": { "$ref": "#/components/schemas/MappingStepDto" } }, + "steps": { + "maxItems": 512, + "type": "array", + "items": { "$ref": "#/components/schemas/MappingStepDto" } + }, "createdAt": { "type": "string", "format": "date-time" }, "canonicalHash": { "type": "string", "pattern": "^[0-9a-f]{64}$" } }, @@ -7785,7 +7795,7 @@ "properties": { "versionId": { "type": "string", "format": "uuid" }, "schemaVersionId": { "type": "string", "format": "uuid" }, - "rules": { "type": "array", "items": { "type": "object" } }, + "rules": { "maxItems": 512, "type": "array", "items": { "type": "object" } }, "createdAt": { "type": "string", "format": "date-time" }, "canonicalHash": { "type": "string", "pattern": "^[0-9a-f]{64}$" } }, @@ -7832,6 +7842,7 @@ "properties": { "datasetId": { "type": "string", "format": "uuid" }, "inputArtifactVersionIds": { + "maxItems": 1024, "type": "array", "items": { "type": "string", "format": "uuid" } }, @@ -7903,7 +7914,11 @@ "severity": { "type": "string", "enum": ["INFO", "WARNING", "ERROR"] }, "messageCode": { "type": "string", "minLength": 1, "maxLength": 96 }, "occurrenceCount": { "type": "number", "minimum": 0 }, - "evidenceIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, + "evidenceIds": { + "maxItems": 128, + "type": "array", + "items": { "type": "string", "format": "uuid" } + }, "detailHash": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, "subject": { "$ref": "#/components/schemas/DatasetQualityFindingSubjectDto" }, "actual": { "$ref": "#/components/schemas/DatasetQualitySafeValueDto" }, @@ -7933,6 +7948,7 @@ "enum": ["PASS", "PASS_WITH_WARNINGS", "BLOCKED", "INCOMPLETE"] }, "findings": { + "maxItems": 512, "type": "array", "items": { "$ref": "#/components/schemas/DatasetQualityFindingDto" } }, diff --git a/services/api/src/features/dsm/api/dataset-quality.dto.ts b/services/api/src/features/dsm/api/dataset-quality.dto.ts index d9293b70..91cc51d1 100644 --- a/services/api/src/features/dsm/api/dataset-quality.dto.ts +++ b/services/api/src/features/dsm/api/dataset-quality.dto.ts @@ -100,7 +100,7 @@ export class DatasetQualityFindingDto { @Max(Number.MAX_SAFE_INTEGER) occurrenceCount!: number; - @ApiProperty({ type: [String], format: 'uuid' }) + @ApiProperty({ type: [String], format: 'uuid', maxItems: 128 }) @IsArray() @ArrayMaxSize(128) @IsUUID('4', { each: true }) @@ -162,7 +162,7 @@ export class RegisterDatasetQualityResultDto { @IsIn(['PASS', 'PASS_WITH_WARNINGS', 'BLOCKED', 'INCOMPLETE']) qualityState!: 'PASS' | 'PASS_WITH_WARNINGS' | 'BLOCKED' | 'INCOMPLETE'; - @ApiProperty({ type: [DatasetQualityFindingDto] }) + @ApiProperty({ type: [DatasetQualityFindingDto], maxItems: 512 }) @IsArray() @ArrayMaxSize(512) @ValidateNested({ each: true }) diff --git a/services/api/src/features/dsm/api/dataset-version.dto.ts b/services/api/src/features/dsm/api/dataset-version.dto.ts index ba6b0cc5..aa555ec4 100644 --- a/services/api/src/features/dsm/api/dataset-version.dto.ts +++ b/services/api/src/features/dsm/api/dataset-version.dto.ts @@ -18,7 +18,7 @@ export class RegisterDatasetVersionDto { @IsUUID() datasetId!: string; - @ApiProperty({ format: 'uuid', type: [String] }) + @ApiProperty({ format: 'uuid', type: [String], maxItems: 1024 }) @IsArray() @ArrayMaxSize(1024) @IsUUID('4', { each: true }) diff --git a/services/api/src/features/dsm/api/governed-dataset.dto.ts b/services/api/src/features/dsm/api/governed-dataset.dto.ts index 44093518..d1273a26 100644 --- a/services/api/src/features/dsm/api/governed-dataset.dto.ts +++ b/services/api/src/features/dsm/api/governed-dataset.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { + ArrayMaxSize, IsArray, IsBoolean, IsIn, @@ -80,8 +81,9 @@ export class CreateGovernedDatasetDto { @MaxLength(200) name!: string; - @ApiProperty({ type: [GovernedDatasetFieldDto] }) + @ApiProperty({ type: [GovernedDatasetFieldDto], maxItems: 256 }) @IsArray() + @ArrayMaxSize(256) @ValidateNested({ each: true }) @Type(() => GovernedDatasetFieldDto) fields!: GovernedDatasetFieldDto[]; diff --git a/services/api/src/features/dsm/api/mapping.dto.ts b/services/api/src/features/dsm/api/mapping.dto.ts index 3b613e6a..3c8e1ed0 100644 --- a/services/api/src/features/dsm/api/mapping.dto.ts +++ b/services/api/src/features/dsm/api/mapping.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { + ArrayMaxSize, IsArray, IsIn, IsISO8601, @@ -54,8 +55,9 @@ export class CreateMappingDto { @IsUUID() targetSchemaVersionId!: string; - @ApiProperty({ type: [MappingStepDto] }) + @ApiProperty({ type: [MappingStepDto], maxItems: 512 }) @IsArray() + @ArrayMaxSize(512) @ValidateNested({ each: true }) @Type(() => MappingStepDto) steps!: MappingStepDto[]; @@ -80,8 +82,9 @@ export class CreateRuleSetDto { @IsUUID() schemaVersionId!: string; - @ApiProperty({ type: [Object] }) + @ApiProperty({ type: [Object], maxItems: 512 }) @IsArray() + @ArrayMaxSize(512) @IsObject({ each: true }) rules!: Record[]; diff --git a/services/api/src/features/iae/api/artifact-export.dto.ts b/services/api/src/features/iae/api/artifact-export.dto.ts index 4c0fa9a9..14584399 100644 --- a/services/api/src/features/iae/api/artifact-export.dto.ts +++ b/services/api/src/features/iae/api/artifact-export.dto.ts @@ -6,7 +6,7 @@ export class CreateArtifactExportDto { @IsUUID() manifestId!: string; - @ApiProperty({ type: [String], format: 'uuid' }) + @ApiProperty({ type: [String], format: 'uuid', minItems: 1, maxItems: 1024 }) @IsArray() @ArrayMinSize(1) @ArrayMaxSize(1024) diff --git a/services/api/test/openapi.test.ts b/services/api/test/openapi.test.ts index 93ddb713..673e829d 100644 --- a/services/api/test/openapi.test.ts +++ b/services/api/test/openapi.test.ts @@ -186,6 +186,19 @@ void test('generates deterministic versioned OpenAPI with safe headers, errors, 'refreshToken' ]; assert.equal(refreshToken?.['writeOnly'], undefined); + for (const [schemaName, propertyName, maxItems] of [ + ['CreateArtifactExportDto', 'versionIds', 1024], + ['CreateGovernedDatasetDto', 'fields', 256], + ['CreateMappingDto', 'steps', 512], + ['CreateRuleSetDto', 'rules', 512], + ['RegisterDatasetVersionDto', 'inputArtifactVersionIds', 1024], + ['DatasetQualityFindingDto', 'evidenceIds', 128], + ['RegisterDatasetQualityResultDto', 'findings', 512], + ] as const) { + const schema = firstDocument.components?.schemas?.[schemaName] as Record; + const property = (schema['properties'] as Record>)[propertyName]; + assert.equal(property?.['maxItems'], maxItems, `${schemaName}.${propertyName} must be bounded`); + } for (const operation of operations(firstDocument)) { const headerNames = (operation.parameters ?? []) From fd508f9e1877232df0bbb893e1c0482ca59cc60e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:35:26 +0700 Subject: [PATCH 17/73] test(iae): verify intake transition revisions --- .../features/iae/prisma-artifact-intake-repository.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/services/api/test/features/iae/prisma-artifact-intake-repository.test.ts b/services/api/test/features/iae/prisma-artifact-intake-repository.test.ts index 8bc8ff36..9d918797 100644 --- a/services/api/test/features/iae/prisma-artifact-intake-repository.test.ts +++ b/services/api/test/features/iae/prisma-artifact-intake-repository.test.ts @@ -174,10 +174,9 @@ void test('[IAE-013] Prisma adapter persists only validated state transitions wi { ...context(workspaceId, 'transition-update'), expectedRevision: 1 }, { ...item, state: 'ROUTED', revision: 2 }, ); - assert.equal( - (await repository.find(context(workspaceId, 'transition-read'), itemId))?.state, - 'ROUTED', - ); + const transitioned = await repository.find(context(workspaceId, 'transition-read'), itemId); + assert.equal(transitioned?.state, 'ROUTED'); + assert.equal(transitioned?.revision, 2); await assert.rejects( repository.save( { ...context(workspaceId, 'transition-stale'), expectedRevision: 1 }, From 812e0c5c864d216c60aad4317ea6810264590a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:35:54 +0700 Subject: [PATCH 18/73] test(iae): harden inbox content leak assertions --- services/api/test/features/iae/inbox.controller.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/api/test/features/iae/inbox.controller.test.ts b/services/api/test/features/iae/inbox.controller.test.ts index fbc4f7e8..f0768333 100644 --- a/services/api/test/features/iae/inbox.controller.test.ts +++ b/services/api/test/features/iae/inbox.controller.test.ts @@ -49,7 +49,7 @@ void test('[IAE-001, IAM-009] HTTP inbox listing uses the configured tenant cont const response = await app.inject({ method: 'GET', url: '/v1/artifacts/inbox' }); assert.equal(response.statusCode, 200); assert.deepEqual(response.json(), [created.accepted ? created.value : undefined]); - assert.doesNotMatch(response.body, /opaque|path|byte|excerpt/u); + assert.doesNotMatch(response.body, /opaque|path|byte|excerpt/iu); } finally { await app.close(); } @@ -103,7 +103,7 @@ void test('[IAE-013] HTTP inbox metadata patch uses a revision precondition and const body: unknown = JSON.parse(accepted.body); assert.ok(typeof body === 'object' && body !== null && 'accepted' in body); assert.equal((body as { readonly accepted: boolean }).accepted, true); - assert.doesNotMatch(accepted.body, /path|source|byte|excerpt/u); + assert.doesNotMatch(accepted.body, /path|source|byte|excerpt/iu); } finally { await app.close(); } From 42ff542882e59c36b40f217e0310a4843a804ad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:36:41 +0700 Subject: [PATCH 19/73] fix(api): document readiness problems by media type --- services/api/openapi/v1.json | 6 ++++-- services/api/src/features/system/api/health.controller.ts | 6 +++++- services/api/test/openapi.test.ts | 3 +++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/services/api/openapi/v1.json b/services/api/openapi/v1.json index 8df95684..d0abe932 100644 --- a/services/api/openapi/v1.json +++ b/services/api/openapi/v1.json @@ -147,10 +147,12 @@ } }, "503": { - "description": "", "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } + "application/problem+json": { + "schema": { "$ref": "#/components/schemas/ProblemDetails" } + } }, + "description": "", "headers": { "X-Correlation-Id": { "description": "Stable UUID that correlates related requests and errors.", diff --git a/services/api/src/features/system/api/health.controller.ts b/services/api/src/features/system/api/health.controller.ts index 37fe04a3..13800384 100644 --- a/services/api/src/features/system/api/health.controller.ts +++ b/services/api/src/features/system/api/health.controller.ts @@ -27,7 +27,11 @@ export class HealthController { @ApiOkResponse({ schema: { properties: { status: { enum: ['ready'], type: 'string' } }, type: 'object' }, }) - @ApiServiceUnavailableResponse({ schema: { $ref: '#/components/schemas/ProblemDetails' } }) + @ApiServiceUnavailableResponse({ + content: { + 'application/problem+json': { schema: { $ref: '#/components/schemas/ProblemDetails' } }, + }, + }) async readinessStatus(): Promise<{ readonly status: 'ready' }> { try { if (await this.readiness.check()) return { status: 'ready' }; diff --git a/services/api/test/openapi.test.ts b/services/api/test/openapi.test.ts index 673e829d..4af56f15 100644 --- a/services/api/test/openapi.test.ts +++ b/services/api/test/openapi.test.ts @@ -15,6 +15,7 @@ interface ParameterLike { interface ResponseLike { readonly $ref?: string; + readonly content?: Record; readonly headers?: Record; } @@ -242,6 +243,8 @@ void test('generates deterministic versioned OpenAPI with safe headers, errors, assert.ok(auditRead?.responses['200'], `${path} must document its successful response`); assert.ok(auditRead.responses['503'], `${path} must document audit persistence outages`); } + const readiness = firstDocument.paths['/health/ready']?.get as OperationLike | undefined; + assert.ok(readiness?.responses['503']?.content?.['application/problem+json']); const served = await first.app.inject({ method: 'GET', url: '/v1/openapi.json' }); assert.equal(served.statusCode, 200); From f4af924edff8f6b0be3b4003b4a1e62d785348bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:37:27 +0700 Subject: [PATCH 20/73] fix(iae): disclose expired upload transfers --- .../features/iae/application/artifact-upload.service.ts | 7 ++++++- .../api/test/features/iae/artifact-upload.service.test.ts | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/services/api/src/features/iae/application/artifact-upload.service.ts b/services/api/src/features/iae/application/artifact-upload.service.ts index 211e2b96..821c91e9 100644 --- a/services/api/src/features/iae/application/artifact-upload.service.ts +++ b/services/api/src/features/iae/application/artifact-upload.service.ts @@ -17,7 +17,10 @@ import type { ArtifactUploadStorageResultV1, } from './artifact-upload-storage.port.js'; -export type ArtifactUploadServiceErrorV1 = 'UPLOAD_NOT_FOUND' | 'UPLOAD_SCOPE_NARROWING_REQUIRED'; +export type ArtifactUploadServiceErrorV1 = + | 'UPLOAD_NOT_FOUND' + | 'UPLOAD_SCOPE_NARROWING_REQUIRED' + | 'UPLOAD_SESSION_EXPIRED'; export type ArtifactUploadServiceResultV1 = | ArtifactUploadResultV1 | ArtifactUploadStorageResultV1 @@ -129,6 +132,8 @@ export class ArtifactUploadService { ): Promise> { const session = await this.repository.find(context, sessionId); if (!session) return Object.freeze({ accepted: false, code: 'UPLOAD_NOT_FOUND' as const }); + if (session.state === 'EXPIRED') + return Object.freeze({ accepted: false, code: 'UPLOAD_SESSION_EXPIRED' as const }); return this.storage.issuePartTransfer(context, session, partNumber); } diff --git a/services/api/test/features/iae/artifact-upload.service.test.ts b/services/api/test/features/iae/artifact-upload.service.test.ts index 1340927a..8c85ddae 100644 --- a/services/api/test/features/iae/artifact-upload.service.test.ts +++ b/services/api/test/features/iae/artifact-upload.service.test.ts @@ -97,5 +97,5 @@ void test('IAE-014 expiration revokes storage-side partial state before persisti assert.equal(expired.value.state, 'EXPIRED'); assert.equal(storage.abortCalls, 1); const transfer = await service.issuePartTransfer(context, created.value.sessionId, 1); - assert.deepEqual(transfer, { accepted: false, code: 'UPLOAD_STORAGE_NOT_READY' }); + assert.deepEqual(transfer, { accepted: false, code: 'UPLOAD_SESSION_EXPIRED' }); }); From e5c49765485a1d86b9e0e286c270f78ae1991bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:38:16 +0700 Subject: [PATCH 21/73] fix(domain): validate normalized export text --- packages/domain/src/artifact-export/v1.ts | 9 +++--- .../domain/test/artifact-export-v1.test.mjs | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/packages/domain/src/artifact-export/v1.ts b/packages/domain/src/artifact-export/v1.ts index 316e95f6..77f9180a 100644 --- a/packages/domain/src/artifact-export/v1.ts +++ b/packages/domain/src/artifact-export/v1.ts @@ -64,11 +64,10 @@ function timestamp(input: unknown): StrictUtcTimestampV1 | undefined { } function text(input: unknown): string | undefined { - return typeof input === 'string' && - input.length > 0 && - input.length <= 128 && - !/\p{Cc}/u.test(input) - ? input.normalize('NFC').trim() + if (typeof input !== 'string') return undefined; + const normalized = input.normalize('NFC').trim(); + return normalized.length > 0 && normalized.length <= 128 && !/\p{Cc}/u.test(normalized) + ? normalized : undefined; } diff --git a/packages/domain/test/artifact-export-v1.test.mjs b/packages/domain/test/artifact-export-v1.test.mjs index 8e6cfb3b..e6546f63 100644 --- a/packages/domain/test/artifact-export-v1.test.mjs +++ b/packages/domain/test/artifact-export-v1.test.mjs @@ -56,3 +56,33 @@ void test('[IAE-018] export manifests preserve hashes, evidence references, and { accepted: false, code: 'DUPLICATE_IDENTIFIER' }, ); }); + +void test('[IAE-018] processor versions are validated after normalization and trimming', () => { + const base = { + manifestId: '00000000-0000-4000-8000-000000000726', + tenantScope: scope, + entries: [ + { + versionId: '00000000-0000-4000-8000-000000000727', + contentSha256: 'a'.repeat(64), + byteSize: 10, + evidenceIds: [], + processorVersions: [' '], + }, + ], + approvalState: 'PENDING', + createdAt: '2026-01-03T00:00:00.000Z', + canonicalHash: 'b'.repeat(64), + }; + assert.deepEqual(createArtifactExportManifestV1(base), { + accepted: false, + code: 'INVALID_ENTRY', + }); + + const trimmed = createArtifactExportManifestV1({ + ...base, + entries: [{ ...base.entries[0], processorVersions: [`${' '.repeat(128)}v1`] }], + }); + assert.equal(trimmed.accepted, true); + if (trimmed.accepted) assert.deepEqual(trimmed.value.entries[0].processorVersions, ['v1']); +}); From 533e7b7a68594b0bfd3f8b45718545c0d31c2aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:38:34 +0700 Subject: [PATCH 22/73] test(domain): verify aggregate governance exports --- packages/domain/test/built-public-api-smoke.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/domain/test/built-public-api-smoke.mjs b/packages/domain/test/built-public-api-smoke.mjs index 88cd28a4..06821858 100644 --- a/packages/domain/test/built-public-api-smoke.mjs +++ b/packages/domain/test/built-public-api-smoke.mjs @@ -64,6 +64,8 @@ const [ assert.equal(aggregate.PERMISSION_SCHEMA_VERSION_V1, 1); assert.equal(aggregate.AUTHORIZATION_SCHEMA_VERSION_V1, 1); +assert.equal(aggregate.ARTIFACT_RETENTION_SCHEMA_VERSION_V1, 1); +assert.equal(aggregate.ARTIFACT_EXPORT_SCHEMA_VERSION_V1, 1); assert.equal(permissions.PERMISSION_SCHEMA_VERSION_V1, 1); assert.equal(typeof tenantScope.parseTenantScopeV1, 'function'); assert.equal(typeof authorization.createScopedAuthorizationEvaluatorV1, 'function'); From eec8df5b616239ba96f67ab496c737ceeb04437a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:38:50 +0700 Subject: [PATCH 23/73] test(sa): assert value-free finding payloads --- packages/domain/test/spreadsheet-audit-v1.test.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/domain/test/spreadsheet-audit-v1.test.mjs b/packages/domain/test/spreadsheet-audit-v1.test.mjs index 44ed6045..eb0c0957 100644 --- a/packages/domain/test/spreadsheet-audit-v1.test.mjs +++ b/packages/domain/test/spreadsheet-audit-v1.test.mjs @@ -34,8 +34,8 @@ void test('[SA-001, SA-004] audit results retain exact value-free evidence coord assert.equal(result.accepted, true); if (!result.accepted) return; assert.equal(result.value.findings[0]?.address, 'C1'); - assert.equal(Object.hasOwn(result.value, 'formula'), false); - assert.equal(Object.hasOwn(result.value, 'sourceValue'), false); + assert.equal(Object.hasOwn(result.value.findings[0], 'formula'), false); + assert.equal(Object.hasOwn(result.value.findings[0], 'sourceValue'), false); }); void test('[SA-005] findings cannot reference an unknown sheet or duplicate IDs', () => { From 6173abf58fab4712241a3fc82c73fcb622d55000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:39:24 +0700 Subject: [PATCH 24/73] fix(domain): classify premature upload expiry --- packages/domain/src/artifact-upload/v1.ts | 2 +- packages/domain/test/artifact-upload-v1.test.mjs | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/domain/src/artifact-upload/v1.ts b/packages/domain/src/artifact-upload/v1.ts index b1e37cc2..1d72423f 100644 --- a/packages/domain/src/artifact-upload/v1.ts +++ b/packages/domain/src/artifact-upload/v1.ts @@ -246,7 +246,7 @@ export function expireArtifactUploadSessionV1( const timestampValue = timestamp(now); if (!timestampValue) return rejected('INVALID_TIMESTAMP'); if (session.state !== 'OPEN') return rejected('INVALID_STATE'); - if (Date.parse(timestampValue) < Date.parse(session.expiresAt)) return rejected('EXPIRED'); + if (Date.parse(timestampValue) < Date.parse(session.expiresAt)) return rejected('INVALID_TIMESTAMP'); return accepted( Object.freeze({ ...session, state: 'EXPIRED' as const, revision: session.revision + 1 }), ); diff --git a/packages/domain/test/artifact-upload-v1.test.mjs b/packages/domain/test/artifact-upload-v1.test.mjs index d40778a0..be6a5c1c 100644 --- a/packages/domain/test/artifact-upload-v1.test.mjs +++ b/packages/domain/test/artifact-upload-v1.test.mjs @@ -4,6 +4,7 @@ import test from 'node:test'; import { completeArtifactUploadSessionV1, createArtifactUploadSessionV1, + expireArtifactUploadSessionV1, recordArtifactUploadPartV1, } from '../dist/artifact-upload/v1.js'; @@ -60,3 +61,14 @@ void test('[IAE-014] upload sessions require every bounded part before completio 'COMPLETED', ); }); + +void test('[IAE-014] upload sessions reject a premature expiration timestamp', () => { + const created = createArtifactUploadSessionV1(base); + assert.equal(created.accepted, true); + if (!created.accepted) return; + + assert.deepEqual(expireArtifactUploadSessionV1(created.value, base.createdAt), { + accepted: false, + code: 'INVALID_TIMESTAMP', + }); +}); From 3f769e2c5e5519a5cd8b41c7187677ffd2426c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:40:05 +0700 Subject: [PATCH 25/73] fix(sa): preserve finding validation errors --- packages/domain/src/spreadsheet-audit/v1.ts | 43 +++++++++++-------- .../domain/test/spreadsheet-audit-v1.test.mjs | 13 ++++++ 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/packages/domain/src/spreadsheet-audit/v1.ts b/packages/domain/src/spreadsheet-audit/v1.ts index 7f170e1b..bde1fb31 100644 --- a/packages/domain/src/spreadsheet-audit/v1.ts +++ b/packages/domain/src/spreadsheet-audit/v1.ts @@ -123,8 +123,9 @@ function sheet(input: unknown): SpreadsheetAuditSheetV1 | undefined { return Object.freeze({ sheetId, name, maxRow, maxColumn, formulaCount }); } -function finding(input: unknown): SpreadsheetAuditFindingV1 | undefined { - if (typeof input !== 'object' || input === null || Array.isArray(input)) return undefined; +function finding(input: unknown): SpreadsheetAuditResultValidationV1 { + if (typeof input !== 'object' || input === null || Array.isArray(input)) + return rejected('INVALID_IDENTIFIER'); const record = input as Record; const findingId = identifier(record['findingId']); const sheetId = identifier(record['sheetId']); @@ -132,18 +133,24 @@ function finding(input: unknown): SpreadsheetAuditFindingV1 | undefined { const kind = record['kind']; const severity = record['severity']; const formulaFingerprint = hash(record['formulaFingerprint']); - if (!findingId || !sheetId || !address || !/^[A-Z]{1,3}[1-9][0-9]*$/u.test(address.toUpperCase())) - return undefined; - if (kind !== 'FORMULA_FAMILY_OUTLIER' && kind !== 'FORMULA_GAP') return undefined; - if (severity !== 'INFO' && severity !== 'WARNING' && severity !== 'ERROR') return undefined; - if (!formulaFingerprint) return undefined; + if (!findingId || !sheetId) return rejected('INVALID_IDENTIFIER'); + if (!address || !/^[A-Z]{1,3}[1-9][0-9]*$/u.test(address.toUpperCase())) + return rejected('INVALID_COORDINATE'); + if (kind !== 'FORMULA_FAMILY_OUTLIER' && kind !== 'FORMULA_GAP') + return rejected('INVALID_KIND'); + if (severity !== 'INFO' && severity !== 'WARNING' && severity !== 'ERROR') + return rejected('INVALID_SEVERITY'); + if (!formulaFingerprint) return rejected('INVALID_HASH'); return Object.freeze({ - findingId, - sheetId, - address: address.toUpperCase(), - kind: kind as SpreadsheetAuditFindingKindV1, - severity: severity as SpreadsheetAuditSeverityV1, - formulaFingerprint, + accepted: true, + value: Object.freeze({ + findingId, + sheetId, + address: address.toUpperCase(), + kind: kind as SpreadsheetAuditFindingKindV1, + severity: severity as SpreadsheetAuditSeverityV1, + formulaFingerprint, + }), }); } @@ -181,10 +188,12 @@ export function createSpreadsheetAuditResultV1(input: { return rejected('DUPLICATE_SHEET'); if (!Array.isArray(input.findings) || input.findings.length > 10_000) return rejected('INVALID_COUNT'); - const findings = input.findings.map(finding); - if (findings.some((candidate): candidate is undefined => candidate === undefined)) - return rejected('INVALID_COUNT'); - const validFindings = findings as SpreadsheetAuditFindingV1[]; + const validFindings: SpreadsheetAuditFindingV1[] = []; + for (const candidate of input.findings) { + const parsed = finding(candidate); + if (!parsed.accepted) return parsed; + validFindings.push(parsed.value); + } if (new Set(validFindings.map((candidate) => candidate.findingId)).size !== validFindings.length) return rejected('DUPLICATE_IDENTIFIER'); const sheetsById = new Map(validSheets.map((candidate) => [candidate.sheetId, candidate])); diff --git a/packages/domain/test/spreadsheet-audit-v1.test.mjs b/packages/domain/test/spreadsheet-audit-v1.test.mjs index eb0c0957..4c68ce00 100644 --- a/packages/domain/test/spreadsheet-audit-v1.test.mjs +++ b/packages/domain/test/spreadsheet-audit-v1.test.mjs @@ -71,3 +71,16 @@ void test('[SA-006] findings must stay inside the exact sheet geometry', () => { { accepted: false, code: 'INVALID_COORDINATE' }, ); }); + +void test('[SA-004] finding validation preserves structural error codes', () => { + for (const [finding, code] of [ + [{ ...base.findings[0], address: 'not-a-cell' }, 'INVALID_COORDINATE'], + [{ ...base.findings[0], severity: 'CRITICAL' }, 'INVALID_SEVERITY'], + [{ ...base.findings[0], kind: 'UNKNOWN' }, 'INVALID_KIND'], + ]) { + assert.deepEqual(createSpreadsheetAuditResultV1({ ...base, findings: [finding] }), { + accepted: false, + code, + }); + } +}); From adb45efc01c905e3b1e71eab0ec078e220ae8812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:40:26 +0700 Subject: [PATCH 26/73] test(domain): guard completed upload results --- packages/domain/test/artifact-upload-v1.test.mjs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/domain/test/artifact-upload-v1.test.mjs b/packages/domain/test/artifact-upload-v1.test.mjs index be6a5c1c..5ac5db56 100644 --- a/packages/domain/test/artifact-upload-v1.test.mjs +++ b/packages/domain/test/artifact-upload-v1.test.mjs @@ -53,13 +53,13 @@ void test('[IAE-014] upload sessions require every bounded part before completio }); assert.equal(second.accepted, true); if (!second.accepted) return; - assert.equal( - completeArtifactUploadSessionV1(second.value, { - assembledSha256: base.expectedSha256, - expectedRevision: 3, - }).value.state, - 'COMPLETED', - ); + const completed = completeArtifactUploadSessionV1(second.value, { + assembledSha256: base.expectedSha256, + expectedRevision: 3, + }); + assert.equal(completed.accepted, true); + if (!completed.accepted) return; + assert.equal(completed.value.state, 'COMPLETED'); }); void test('[IAE-014] upload sessions reject a premature expiration timestamp', () => { From d477368dbe2616146dfd7711955965e405fc50d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:40:47 +0700 Subject: [PATCH 27/73] fix(domain): classify premature unlock expiry --- packages/domain/src/protected-document/v1.ts | 2 +- packages/domain/test/protected-document-v1.test.mjs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/domain/src/protected-document/v1.ts b/packages/domain/src/protected-document/v1.ts index 31481aaa..185ff610 100644 --- a/packages/domain/src/protected-document/v1.ts +++ b/packages/domain/src/protected-document/v1.ts @@ -196,7 +196,7 @@ export function expireProtectedDocumentUnlockRequestV1( const timestampValue = timestamp(now); if (!timestampValue) return rejected('INVALID_TIMESTAMP'); if (request.state !== 'REQUESTED') return rejected('INVALID_STATE'); - if (Date.parse(timestampValue) < Date.parse(request.expiresAt)) return rejected('EXPIRED'); + if (Date.parse(timestampValue) < Date.parse(request.expiresAt)) return rejected('INVALID_STATE'); return accepted( Object.freeze({ ...request, state: 'EXPIRED' as const, revision: request.revision + 1 }), ); diff --git a/packages/domain/test/protected-document-v1.test.mjs b/packages/domain/test/protected-document-v1.test.mjs index 6cde0b01..89797d78 100644 --- a/packages/domain/test/protected-document-v1.test.mjs +++ b/packages/domain/test/protected-document-v1.test.mjs @@ -52,6 +52,10 @@ void test('[IAE-015] device-keychain requests require a device and expire withou const created = createProtectedDocumentUnlockRequestV1(base); assert.equal(created.accepted, true); if (!created.accepted) return; + assert.deepEqual( + expireProtectedDocumentUnlockRequestV1(created.value, '2026-08-02T00:29:59.999Z'), + { accepted: false, code: 'INVALID_STATE' }, + ); const expired = expireProtectedDocumentUnlockRequestV1(created.value, '2026-08-02T00:30:00.000Z'); assert.equal(expired.accepted, true); if (expired.accepted) assert.equal(expired.value.state, 'EXPIRED'); From afb3fdc9ca1730d6963f2c820b0aaa243adcc290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:41:18 +0700 Subject: [PATCH 28/73] fix(dsm): enforce dataset profile row budgets --- packages/domain/src/dataset-profile/v1.ts | 1 + packages/domain/test/dataset-profile-v1.test.mjs | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/packages/domain/src/dataset-profile/v1.ts b/packages/domain/src/dataset-profile/v1.ts index 2bf20713..7a78f032 100644 --- a/packages/domain/src/dataset-profile/v1.ts +++ b/packages/domain/src/dataset-profile/v1.ts @@ -151,6 +151,7 @@ export function createDatasetProfileV1(input: { const maxBytes = limit(limitRecord['maxBytes'], 1024 * 1024 * 1024 * 1024); const maxDurationMs = limit(limitRecord['maxDurationMs'], 86_400_000); if (!maxRows || !maxBytes || !maxDurationMs) return rejected('INVALID_LIMITS'); + if (rowCountScanned > maxRows) return rejected('INVALID_COUNT'); if (!profileFingerprint) return rejected('INVALID_HASH'); if (!createdAt) return rejected('INVALID_TIMESTAMP'); return accepted( diff --git a/packages/domain/test/dataset-profile-v1.test.mjs b/packages/domain/test/dataset-profile-v1.test.mjs index c5729e61..9854de9f 100644 --- a/packages/domain/test/dataset-profile-v1.test.mjs +++ b/packages/domain/test/dataset-profile-v1.test.mjs @@ -56,4 +56,11 @@ void test('[DSM-011] complete profiles reject sample-only fields and impossible createDatasetProfileV1({ ...base, completeness: 'COMPLETE', samplingSeed: 'a'.repeat(64) }), { accepted: false, code: 'INVALID_SAMPLING' }, ); + assert.deepEqual( + createDatasetProfileV1({ + ...base, + resourceLimits: { ...base.resourceLimits, maxRows: base.rowCountScanned - 1 }, + }), + { accepted: false, code: 'INVALID_COUNT' }, + ); }); From 3311f2a9be6598487e2c9883af3b624258d96f2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:42:20 +0700 Subject: [PATCH 29/73] fix(sa): support complete XLSX row geometry --- packages/domain/src/spreadsheet-audit/v1.ts | 2 +- packages/domain/test/spreadsheet-audit-v1.test.mjs | 7 +++++++ services/api/openapi/v1.json | 2 +- .../api/src/features/sa/api/spreadsheet-audit.dto.ts | 4 ++-- services/api/test/openapi.test.ts | 9 +++++++++ 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/domain/src/spreadsheet-audit/v1.ts b/packages/domain/src/spreadsheet-audit/v1.ts index bde1fb31..a09feaea 100644 --- a/packages/domain/src/spreadsheet-audit/v1.ts +++ b/packages/domain/src/spreadsheet-audit/v1.ts @@ -119,7 +119,7 @@ function sheet(input: unknown): SpreadsheetAuditSheetV1 | undefined { formulaCount === undefined ) return undefined; - if (maxRow > 1_000_000 || maxColumn > 16_384 || formulaCount > 1_000_000) return undefined; + if (maxRow > 1_048_576 || maxColumn > 16_384 || formulaCount > 1_000_000) return undefined; return Object.freeze({ sheetId, name, maxRow, maxColumn, formulaCount }); } diff --git a/packages/domain/test/spreadsheet-audit-v1.test.mjs b/packages/domain/test/spreadsheet-audit-v1.test.mjs index 4c68ce00..65e9b100 100644 --- a/packages/domain/test/spreadsheet-audit-v1.test.mjs +++ b/packages/domain/test/spreadsheet-audit-v1.test.mjs @@ -70,6 +70,13 @@ void test('[SA-006] findings must stay inside the exact sheet geometry', () => { }), { accepted: false, code: 'INVALID_COORDINATE' }, ); + assert.equal( + createSpreadsheetAuditResultV1({ + ...base, + sheets: [{ ...base.sheets[0], maxRow: 1_048_576 }], + }).accepted, + true, + ); }); void test('[SA-004] finding validation preserves structural error codes', () => { diff --git a/services/api/openapi/v1.json b/services/api/openapi/v1.json index d0abe932..bda203ac 100644 --- a/services/api/openapi/v1.json +++ b/services/api/openapi/v1.json @@ -8372,7 +8372,7 @@ "properties": { "sheetId": { "type": "string", "format": "uuid" }, "name": { "type": "string", "maxLength": 128 }, - "maxRow": { "type": "number", "minimum": 0, "maximum": 1000000 }, + "maxRow": { "type": "number", "minimum": 0, "maximum": 1048576 }, "maxColumn": { "type": "number", "minimum": 0, "maximum": 16384 }, "formulaCount": { "type": "number", "minimum": 0, "maximum": 1000000 } }, diff --git a/services/api/src/features/sa/api/spreadsheet-audit.dto.ts b/services/api/src/features/sa/api/spreadsheet-audit.dto.ts index 5d8bfd12..bae916fe 100644 --- a/services/api/src/features/sa/api/spreadsheet-audit.dto.ts +++ b/services/api/src/features/sa/api/spreadsheet-audit.dto.ts @@ -29,10 +29,10 @@ export class SpreadsheetAuditSheetDto { @MaxLength(128) name!: string; - @ApiProperty({ minimum: 0, maximum: 1_000_000 }) + @ApiProperty({ minimum: 0, maximum: 1_048_576 }) @IsInt() @Min(0) - @Max(1_000_000) + @Max(1_048_576) maxRow!: number; @ApiProperty({ minimum: 0, maximum: 16_384 }) diff --git a/services/api/test/openapi.test.ts b/services/api/test/openapi.test.ts index 4af56f15..909b811b 100644 --- a/services/api/test/openapi.test.ts +++ b/services/api/test/openapi.test.ts @@ -200,6 +200,15 @@ void test('generates deterministic versioned OpenAPI with safe headers, errors, const property = (schema['properties'] as Record>)[propertyName]; assert.equal(property?.['maxItems'], maxItems, `${schemaName}.${propertyName} must be bounded`); } + const spreadsheetSheet = firstDocument.components?.schemas?.[ + 'SpreadsheetAuditSheetDto' + ] as Record; + assert.equal( + (spreadsheetSheet['properties'] as Record>)['maxRow']?.[ + 'maximum' + ], + 1_048_576, + ); for (const operation of operations(firstDocument)) { const headerNames = (operation.parameters ?? []) From 4a4c781e87df39e11017f490d4e32142d3d6f16f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:42:47 +0700 Subject: [PATCH 30/73] refactor(iae): simplify inbox revision context --- services/api/src/features/iae/api/inbox.controller.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/api/src/features/iae/api/inbox.controller.ts b/services/api/src/features/iae/api/inbox.controller.ts index 5f627ecb..a6284cca 100644 --- a/services/api/src/features/iae/api/inbox.controller.ts +++ b/services/api/src/features/iae/api/inbox.controller.ts @@ -88,8 +88,7 @@ export class InboxController { return Object.freeze({ accepted: false, code: 'INVALID_IDENTIFIER' as const }); if (expectedRevision === undefined) return Object.freeze({ accepted: false, code: 'INVALID_METADATA' as const }); - const mutationContext = - expectedRevision === undefined ? context : Object.freeze({ ...context, expectedRevision }); + const mutationContext = Object.freeze({ ...context, expectedRevision }); return this.intake.updateMetadata(mutationContext, parsedId.value, { ...(Object.hasOwn(input, 'assigneeId') ? { assigneeId: input.assigneeId } : {}), ...(Object.hasOwn(input, 'labels') ? { labels: input.labels } : {}), From 34495ddd3ba0aafdfdd6c95b727de79b565e12c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:43:46 +0700 Subject: [PATCH 31/73] fix(iae): harden export manifest persistence --- ...isma-artifact-export-repository.adapter.ts | 20 ++++++++- .../prisma-artifact-export-repository.test.ts | 41 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/services/api/src/features/iae/adapter/prisma-artifact-export-repository.adapter.ts b/services/api/src/features/iae/adapter/prisma-artifact-export-repository.adapter.ts index 1fb55e4b..3f6f9b05 100644 --- a/services/api/src/features/iae/adapter/prisma-artifact-export-repository.adapter.ts +++ b/services/api/src/features/iae/adapter/prisma-artifact-export-repository.adapter.ts @@ -93,6 +93,15 @@ function visible(context: TenantScopeV1, row: ArtifactExportDatabaseRowV1): bool return tenantScopeContainsV1(context, candidate) || tenantScopeContainsV1(candidate, context); } +function isUniqueConstraintViolation(error: unknown): boolean { + return ( + typeof error === 'object' && + error !== null && + 'code' in error && + (error as { readonly code?: unknown }).code === 'P2002' + ); +} + class PrismaArtifactExportTransactionAdapter implements ArtifactExportTransactionPortV1 { public constructor(private readonly client: ArtifactExportDatabaseClientV1) {} @@ -106,12 +115,19 @@ class PrismaArtifactExportTransactionAdapter implements ArtifactExportTransactio where: { id: manifest.manifestId }, }); if (existing !== null) { + if (!visible(context.tenantScope, existing)) + throw new Error('IAE_IMMUTABLE_EXPORT_MANIFEST'); const current = rowToDomain(existing); if (JSON.stringify(current) !== JSON.stringify(manifest)) throw new Error('IAE_IMMUTABLE_EXPORT_MANIFEST'); return; } - await this.client.artifactExportManifestRecord.create({ data: domainToCreate(manifest) }); + try { + await this.client.artifactExportManifestRecord.create({ data: domainToCreate(manifest) }); + } catch (error) { + if (isUniqueConstraintViolation(error)) throw new Error('IAE_IMMUTABLE_EXPORT_MANIFEST'); + throw error; + } } public async find( @@ -142,7 +158,7 @@ export class PrismaArtifactExportRepositoryAdapter implements ArtifactExportRepo } public save(context: IamTenantContextV1, manifest: ArtifactExportManifestV1): Promise { - return new PrismaArtifactExportTransactionAdapter(this.client).save(context, manifest); + return this.withTransaction(context, (transaction) => transaction.save(context, manifest)); } public find( diff --git a/services/api/test/features/iae/prisma-artifact-export-repository.test.ts b/services/api/test/features/iae/prisma-artifact-export-repository.test.ts index 3e03102f..d2820d45 100644 --- a/services/api/test/features/iae/prisma-artifact-export-repository.test.ts +++ b/services/api/test/features/iae/prisma-artifact-export-repository.test.ts @@ -49,6 +49,7 @@ if (!manifest.accepted) throw new Error('fixture manifest invalid'); void test('IAE-018 Prisma export adapter preserves immutable manifests and scopes reads', async () => { const rows = new Map(); + let transactions = 0; const client: ArtifactExportDatabaseClientV1 = { artifactExportManifestRecord: { create({ data }) { @@ -64,6 +65,7 @@ void test('IAE-018 Prisma export adapter preserves immutable manifests and scope }, }, $transaction(work) { + transactions += 1; return work(client); }, }; @@ -72,4 +74,43 @@ void test('IAE-018 Prisma export adapter preserves immutable manifests and scope await repository.save(context, manifest.value); assert.deepEqual(await repository.find(context, manifest.value.manifestId), manifest.value); assert.equal(rows.size, 1); + assert.equal(transactions, 2); +}); + +void test('IAE-018 Prisma export adapter hides colliding tenants and translates create races', async () => { + const hiddenRow: ArtifactExportDatabaseRowV1 = { + id: manifest.value.manifestId, + scopeType: 'workspace', + organizationId: '77777777-7777-4777-8777-777777777777', + workspaceId: '88888888-8888-4888-8888-888888888888', + projectId: null, + entries: 'must-not-be-parsed', + approvalState: 'PENDING', + createdAt: new Date('2026-08-02T00:00:00.000Z'), + canonicalHash: 'b'.repeat(64), + }; + const hiddenClient: ArtifactExportDatabaseClientV1 = { + artifactExportManifestRecord: { + create: () => Promise.reject(new Error('unexpected create')), + findUnique: () => Promise.resolve(hiddenRow), + }, + $transaction: (work) => work(hiddenClient), + }; + await assert.rejects( + new PrismaArtifactExportRepositoryAdapter(hiddenClient).save(context, manifest.value), + /IAE_IMMUTABLE_EXPORT_MANIFEST/u, + ); + + const raceClient: ArtifactExportDatabaseClientV1 = { + artifactExportManifestRecord: { + create: () => + Promise.reject(Object.assign(new Error('unique constraint violation'), { code: 'P2002' })), + findUnique: () => Promise.resolve(null), + }, + $transaction: (work) => work(raceClient), + }; + await assert.rejects( + new PrismaArtifactExportRepositoryAdapter(raceClient).save(context, manifest.value), + /IAE_IMMUTABLE_EXPORT_MANIFEST/u, + ); }); From 5c0b1d431e5db7914551ff640126269c59c7c9e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:45:14 +0700 Subject: [PATCH 32/73] fix(api): map unavailable tenant context safely --- .../src/platform/http/problem-details.filter.ts | 2 +- .../http/request-tenant-context.port.ts | 14 +++++++++++++- .../http/session-tenant-context.adapter.ts | 17 ++++------------- services/api/test/http-contract.test.ts | 7 +++++++ 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/services/api/src/platform/http/problem-details.filter.ts b/services/api/src/platform/http/problem-details.filter.ts index 41c82576..a4597b8e 100644 --- a/services/api/src/platform/http/problem-details.filter.ts +++ b/services/api/src/platform/http/problem-details.filter.ts @@ -13,7 +13,7 @@ import { MfaProblemError } from '../../features/iam/application/mfa-problem.erro import { EntitlementProblemError } from '../../features/bua/application/entitlement-problem.error.js'; import { DeviceIdentityProblemError } from '../../features/iam/application/device-identity-problem.error.js'; import { AuditProblemError } from '../../features/aud/application/audit-problem.error.js'; -import { RequestTenantContextProblemError } from './session-tenant-context.adapter.js'; +import { RequestTenantContextProblemError } from './request-tenant-context.port.js'; import { NotReadyError } from '../../features/system/application/not-ready.error.js'; import { InputValidationException } from './input-validation.exception.js'; import { createProblem, type ProblemInput } from './problem-details.js'; diff --git a/services/api/src/platform/http/request-tenant-context.port.ts b/services/api/src/platform/http/request-tenant-context.port.ts index 05ae6ade..53b3a9a8 100644 --- a/services/api/src/platform/http/request-tenant-context.port.ts +++ b/services/api/src/platform/http/request-tenant-context.port.ts @@ -2,6 +2,18 @@ import type { IamTenantContextV1 } from '../../features/iam/application/tenant-c export const REQUEST_TENANT_CONTEXT = Symbol('REQUEST_TENANT_CONTEXT'); +export type RequestTenantContextProblemCodeV1 = + | 'AUTHENTICATION_FAILED' + | 'AUTHENTICATION_UNAVAILABLE' + | 'CONTEXT_INVALID'; + +export class RequestTenantContextProblemError extends Error { + constructor(readonly code: RequestTenantContextProblemCodeV1) { + super(code); + this.name = 'RequestTenantContextProblemError'; + } +} + /** Resolves an already-authenticated request to a scoped IAM context. */ export interface RequestTenantContextPortV1 { resolve(request: unknown): Promise; @@ -12,6 +24,6 @@ export class UnavailableRequestTenantContextAdapter implements RequestTenantCont public async resolve(request: unknown): Promise { void request; await Promise.resolve(); - throw new Error('AUTHENTICATED_CONTEXT_UNAVAILABLE'); + throw new RequestTenantContextProblemError('AUTHENTICATION_UNAVAILABLE'); } } diff --git a/services/api/src/platform/http/session-tenant-context.adapter.ts b/services/api/src/platform/http/session-tenant-context.adapter.ts index a0852f39..00199e9b 100644 --- a/services/api/src/platform/http/session-tenant-context.adapter.ts +++ b/services/api/src/platform/http/session-tenant-context.adapter.ts @@ -2,21 +2,12 @@ import { randomUUID } from 'node:crypto'; import { type AuthenticatedPrincipalV1 } from '../../features/iam/application/authentication.port.js'; import { createIamTenantContextV1 } from '../../features/iam/application/tenant-context.js'; -import type { RequestTenantContextPortV1 } from './request-tenant-context.port.js'; +import { + RequestTenantContextProblemError, + type RequestTenantContextPortV1, +} from './request-tenant-context.port.js'; import { getRequestContext } from './request-context.js'; -export type RequestTenantContextProblemCodeV1 = - | 'AUTHENTICATION_FAILED' - | 'AUTHENTICATION_UNAVAILABLE' - | 'CONTEXT_INVALID'; - -export class RequestTenantContextProblemError extends Error { - constructor(readonly code: RequestTenantContextProblemCodeV1) { - super(code); - this.name = 'RequestTenantContextProblemError'; - } -} - type HeaderValueV1 = string | readonly string[] | undefined; const SAFE_METHODS_V1 = new Set(['GET', 'HEAD', 'OPTIONS']); diff --git a/services/api/test/http-contract.test.ts b/services/api/test/http-contract.test.ts index 364c38cf..1b6ef295 100644 --- a/services/api/test/http-contract.test.ts +++ b/services/api/test/http-contract.test.ts @@ -83,6 +83,13 @@ void test('reports ready only through the injectable readiness port and minimize ); }); +void test('maps an unconfigured tenant context provider to authentication unavailability', async () => { + await withApp({}, async (app) => { + const response = await app.inject({ method: 'GET', url: '/v1/artifacts/inbox' }); + assertProblem(response, 503, 'AUTHENTICATION_UNAVAILABLE'); + }); +}); + void test('propagates one valid correlation UUID while generating a distinct request UUID', async () => { await withApp({}, async (app) => { const response = await app.inject({ From b6603eb0e2761f16f99022224b3b5fa96a53dd6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:46:00 +0700 Subject: [PATCH 33/73] fix(iae): require integer admission byte sizes --- services/api/openapi/v1.json | 2 +- .../src/features/iae/api/artifact-admission.dto.ts | 5 ++--- .../iae/artifact-admission.controller.test.ts | 13 +++++++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/services/api/openapi/v1.json b/services/api/openapi/v1.json index bda203ac..16922a66 100644 --- a/services/api/openapi/v1.json +++ b/services/api/openapi/v1.json @@ -7646,7 +7646,7 @@ "type": "object", "properties": { "actualSha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, - "actualByteSize": { "type": "number", "minimum": 0 }, + "actualByteSize": { "type": "integer", "minimum": 0 }, "detectedMediaType": { "type": "string" }, "scanState": { "type": "string", "enum": ["PENDING", "CLEAN", "MALICIOUS", "FAILED"] }, "maxByteSize": { "type": "number", "minimum": 0 }, diff --git a/services/api/src/features/iae/api/artifact-admission.dto.ts b/services/api/src/features/iae/api/artifact-admission.dto.ts index c79647e2..36751eba 100644 --- a/services/api/src/features/iae/api/artifact-admission.dto.ts +++ b/services/api/src/features/iae/api/artifact-admission.dto.ts @@ -3,7 +3,6 @@ import { IsISO8601, IsIn, IsInt, - IsNumber, IsOptional, IsString, Min, @@ -15,8 +14,8 @@ export class AdmitArtifactDto { @Matches(/^[0-9a-f]{64}$/u) actualSha256!: string; - @ApiProperty({ minimum: 0 }) - @IsNumber() + @ApiProperty({ type: 'integer', minimum: 0 }) + @IsInt() @Min(0) actualByteSize!: number; diff --git a/services/api/test/features/iae/artifact-admission.controller.test.ts b/services/api/test/features/iae/artifact-admission.controller.test.ts index 6dae83c3..a0380e0e 100644 --- a/services/api/test/features/iae/artifact-admission.controller.test.ts +++ b/services/api/test/features/iae/artifact-admission.controller.test.ts @@ -47,6 +47,19 @@ void test('IAE-009/010 admission HTTP endpoint persists clean status without sou requestTenantContext, }); try { + const fractional = await app.inject({ + method: 'POST', + url: `/v1/artifact-versions/${artifact.value.versionId}/admit`, + payload: { + actualSha256: 'a'.repeat(64), + actualByteSize: 4.5, + detectedMediaType: 'text/csv', + scanState: 'CLEAN', + maxByteSize: 100, + }, + }); + assert.equal(fractional.statusCode, 400); + const response = await app.inject({ method: 'POST', url: `/v1/artifact-versions/${artifact.value.versionId}/admit`, From c59b5b0e1412725175d5255cfd01369a539b00ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:47:35 +0700 Subject: [PATCH 34/73] fix(iae): map export rejections to HTTP problems --- .../iae/api/artifact-export.controller.ts | 9 ++- .../artifact-export-problem.error.ts | 10 ++++ .../platform/http/problem-details.filter.ts | 13 ++++ .../iae/artifact-export.controller.test.ts | 59 +++++++++++++++++++ 4 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 services/api/src/features/iae/application/artifact-export-problem.error.ts create mode 100644 services/api/test/features/iae/artifact-export.controller.test.ts diff --git a/services/api/src/features/iae/api/artifact-export.controller.ts b/services/api/src/features/iae/api/artifact-export.controller.ts index c1a44bfb..f03f17e5 100644 --- a/services/api/src/features/iae/api/artifact-export.controller.ts +++ b/services/api/src/features/iae/api/artifact-export.controller.ts @@ -15,6 +15,7 @@ import { type ArtifactExportRepositoryPortV1, } from '../application/artifact-export-repository.port.js'; import { ArtifactExportService } from '../application/artifact-export.service.js'; +import { ArtifactExportProblemError } from '../application/artifact-export-problem.error.js'; import { CreateArtifactExportDto } from './artifact-export.dto.js'; import { REQUEST_TENANT_CONTEXT, @@ -41,13 +42,17 @@ export class ArtifactExportController { @ApiBody({ type: CreateArtifactExportDto }) async create(@Req() request: unknown, @Body() input: CreateArtifactExportDto): Promise { const context = await this.requestContext.resolve(request); - return this.exports.create(context, input); + const result = await this.exports.create(context, input); + if (!result.accepted) throw new ArtifactExportProblemError(result.code); + return result; } @Get(':manifestId') @ApiOperation({ summary: 'Read an immutable artifact verification manifest' }) async get(@Req() request: unknown, @Param('manifestId') manifestId: string): Promise { const context = await this.requestContext.resolve(request); - return this.exports.find(context, manifestId); + const result = await this.exports.find(context, manifestId); + if (!result.accepted) throw new ArtifactExportProblemError(result.code); + return result; } } diff --git a/services/api/src/features/iae/application/artifact-export-problem.error.ts b/services/api/src/features/iae/application/artifact-export-problem.error.ts new file mode 100644 index 00000000..b6ca8624 --- /dev/null +++ b/services/api/src/features/iae/application/artifact-export-problem.error.ts @@ -0,0 +1,10 @@ +import type { ArtifactExportErrorCodeV1 } from '@databreeze/domain/artifact-export/v1'; + +export type ArtifactExportProblemCodeV1 = ArtifactExportErrorCodeV1 | 'ARTIFACT_NOT_FOUND'; + +export class ArtifactExportProblemError extends Error { + public constructor(readonly code: ArtifactExportProblemCodeV1) { + super(code); + this.name = 'ArtifactExportProblemError'; + } +} diff --git a/services/api/src/platform/http/problem-details.filter.ts b/services/api/src/platform/http/problem-details.filter.ts index a4597b8e..febe07a1 100644 --- a/services/api/src/platform/http/problem-details.filter.ts +++ b/services/api/src/platform/http/problem-details.filter.ts @@ -13,6 +13,7 @@ import { MfaProblemError } from '../../features/iam/application/mfa-problem.erro import { EntitlementProblemError } from '../../features/bua/application/entitlement-problem.error.js'; import { DeviceIdentityProblemError } from '../../features/iam/application/device-identity-problem.error.js'; import { AuditProblemError } from '../../features/aud/application/audit-problem.error.js'; +import { ArtifactExportProblemError } from '../../features/iae/application/artifact-export-problem.error.js'; import { RequestTenantContextProblemError } from './request-tenant-context.port.js'; import { NotReadyError } from '../../features/system/application/not-ready.error.js'; import { InputValidationException } from './input-validation.exception.js'; @@ -105,6 +106,18 @@ function describe(error: unknown, correlationId: string): ProblemInput { status: HttpStatus.SERVICE_UNAVAILABLE, }; } + if (error instanceof ArtifactExportProblemError) { + const notFound = error.code === 'ARTIFACT_NOT_FOUND'; + return { + code: error.code, + correlationId, + messageKey: notFound + ? 'api.error.artifact_export_not_found' + : 'api.error.artifact_export_invalid', + retryable: false, + status: notFound ? HttpStatus.NOT_FOUND : HttpStatus.BAD_REQUEST, + }; + } if (error instanceof RequestTenantContextProblemError) { const invalidContext = error.code === 'CONTEXT_INVALID'; const unavailable = error.code === 'AUTHENTICATION_UNAVAILABLE'; diff --git a/services/api/test/features/iae/artifact-export.controller.test.ts b/services/api/test/features/iae/artifact-export.controller.test.ts new file mode 100644 index 00000000..8662d6ea --- /dev/null +++ b/services/api/test/features/iae/artifact-export.controller.test.ts @@ -0,0 +1,59 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { createApiApplication } from '../../../src/bootstrap.js'; +import { InMemoryArtifactExportRepositoryAdapter } from '../../../src/features/iae/adapter/in-memory-artifact-export-repository.adapter.js'; +import { InMemoryArtifactLineageRepositoryAdapter } from '../../../src/features/iae/adapter/in-memory-artifact-lineage-repository.adapter.js'; +import { InMemoryArtifactRepositoryAdapter } from '../../../src/features/iae/adapter/in-memory-artifact-repository.adapter.js'; +import { createIamTenantContextV1 } from '../../../src/features/iam/application/tenant-context.js'; + +const contextResult = createIamTenantContextV1({ + actorId: '11111111-1111-4111-8111-111111111111', + tenantScope: { + scopeType: 'workspace', + organizationId: '22222222-2222-4222-8222-222222222222', + workspaceId: '33333333-3333-4333-8333-333333333333', + }, + authorizationEpoch: 1, + correlationId: '44444444-4444-4444-8444-444444444444', + idempotencyKey: 'artifact-export-http', +}); +if (!contextResult.accepted) throw new Error('fixture context invalid'); +const context = contextResult.value; + +void test('IAE-018 export HTTP maps rejected service outcomes to problem responses', async () => { + const { app } = await createApiApplication({ + artifactExportRepository: new InMemoryArtifactExportRepositoryAdapter(), + artifactLineageRepository: new InMemoryArtifactLineageRepositoryAdapter(), + artifactRepository: new InMemoryArtifactRepositoryAdapter(), + requestTenantContext: { resolve: () => Promise.resolve(context) }, + }); + try { + const invalid = await app.inject({ method: 'GET', url: '/v1/artifacts/exports/not-a-uuid' }); + assert.equal(invalid.statusCode, 400); + assert.match(String(invalid.headers['content-type']), /^application\/problem\+json/u); + assert.equal((invalid.json() as { code: string }).code, 'INVALID_IDENTIFIER'); + + const missing = await app.inject({ + method: 'GET', + url: '/v1/artifacts/exports/55555555-5555-4555-8555-555555555555', + }); + assert.equal(missing.statusCode, 404); + assert.equal((missing.json() as { code: string }).code, 'ARTIFACT_NOT_FOUND'); + + const missingSource = await app.inject({ + method: 'POST', + url: '/v1/artifacts/exports', + payload: { + manifestId: '66666666-6666-4666-8666-666666666666', + versionIds: ['77777777-7777-4777-8777-777777777777'], + approvalState: 'PENDING', + createdAt: '2026-08-04T00:00:00.000Z', + }, + }); + assert.equal(missingSource.statusCode, 404); + assert.equal((missingSource.json() as { code: string }).code, 'ARTIFACT_NOT_FOUND'); + } finally { + await app.close(); + } +}); From ea3c4ed3aa3abf3980b7b7e21c8a2e90638eb513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:48:52 +0700 Subject: [PATCH 35/73] fix(iae): require strict UTC governance dates --- .../iae/api/artifact-retention.dto.ts | 25 +++++++++++++------ .../src/features/iae/api/inbox-item.dto.ts | 4 ++- .../iae/artifact-retention.controller.test.ts | 18 +++++++++++++ .../features/iae/inbox.controller.test.ts | 16 ++++++++++++ 4 files changed, 54 insertions(+), 9 deletions(-) diff --git a/services/api/src/features/iae/api/artifact-retention.dto.ts b/services/api/src/features/iae/api/artifact-retention.dto.ts index be4ed98c..d1ca10fc 100644 --- a/services/api/src/features/iae/api/artifact-retention.dto.ts +++ b/services/api/src/features/iae/api/artifact-retention.dto.ts @@ -1,25 +1,32 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsISO8601, IsInt, IsUUID, Min } from 'class-validator'; +import { IsBoolean, IsISO8601, IsInt, IsUUID, Matches, Min } from 'class-validator'; + +const strictUtcTimestamp = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u; export class RetentionEvaluationDto { @ApiProperty({ format: 'date-time' }) - @IsISO8601() + @IsISO8601({ strict: true, strictSeparator: true }) + @Matches(strictUtcTimestamp) evaluatedAt!: string; @ApiProperty({ format: 'date-time' }) - @IsISO8601() + @IsISO8601({ strict: true, strictSeparator: true }) + @Matches(strictUtcTimestamp) workspaceRetentionUntil!: string; @ApiProperty({ format: 'date-time' }) - @IsISO8601() + @IsISO8601({ strict: true, strictSeparator: true }) + @Matches(strictUtcTimestamp) resourceRetentionUntil!: string; @ApiProperty({ format: 'date-time' }) - @IsISO8601() + @IsISO8601({ strict: true, strictSeparator: true }) + @Matches(strictUtcTimestamp) auditRetentionUntil!: string; @ApiProperty({ format: 'date-time' }) - @IsISO8601() + @IsISO8601({ strict: true, strictSeparator: true }) + @Matches(strictUtcTimestamp) recoveryWindowUntil!: string; @ApiProperty() @@ -41,13 +48,15 @@ export class CreateArtifactDeletionRequestDto extends RetentionEvaluationDto { requestedBy!: string; @ApiProperty({ format: 'date-time' }) - @IsISO8601() + @IsISO8601({ strict: true, strictSeparator: true }) + @Matches(strictUtcTimestamp) requestedAt!: string; } export class AuthorizeArtifactDeletionRequestDto extends RetentionEvaluationDto { @ApiProperty({ format: 'date-time' }) - @IsISO8601() + @IsISO8601({ strict: true, strictSeparator: true }) + @Matches(strictUtcTimestamp) approvedAt!: string; @ApiProperty() diff --git a/services/api/src/features/iae/api/inbox-item.dto.ts b/services/api/src/features/iae/api/inbox-item.dto.ts index 011d826a..44d7713a 100644 --- a/services/api/src/features/iae/api/inbox-item.dto.ts +++ b/services/api/src/features/iae/api/inbox-item.dto.ts @@ -9,6 +9,7 @@ import { IsOptional, IsString, IsUUID, + Matches, MaxLength, Min, MinLength, @@ -65,7 +66,8 @@ export class UpdateInboxMetadataDto { required: false, }) @IsOptional() - @IsISO8601() + @IsISO8601({ strict: true, strictSeparator: true }) + @Matches(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u) dueAt?: string | null; @ApiProperty({ minimum: 1, required: false }) diff --git a/services/api/test/features/iae/artifact-retention.controller.test.ts b/services/api/test/features/iae/artifact-retention.controller.test.ts index 7492de79..cd965517 100644 --- a/services/api/test/features/iae/artifact-retention.controller.test.ts +++ b/services/api/test/features/iae/artifact-retention.controller.test.ts @@ -59,6 +59,24 @@ void test('[IAE-016, IAM-009] retention HTTP binds requester to the authenticate requestTenantContext, }); try { + const nonUtc = await app.inject({ + method: 'POST', + url: `/v1/artifact-versions/${versionId}/deletion-requests`, + payload: { + requestId, + requestedBy: actorId, + requestedAt: '2026-08-02T08:00:00.000+07:00', + evaluatedAt: '2026-08-02T01:00:00.000Z', + workspaceRetentionUntil: '2026-07-01T00:00:00.000Z', + resourceRetentionUntil: '2026-07-01T00:00:00.000Z', + auditRetentionUntil: '2026-07-01T00:00:00.000Z', + recoveryWindowUntil: '2026-07-01T00:00:00.000Z', + activeApproval: false, + legalHold: false, + }, + }); + assert.equal(nonUtc.statusCode, 400); + const response = await app.inject({ method: 'POST', url: `/v1/artifact-versions/${versionId}/deletion-requests`, diff --git a/services/api/test/features/iae/inbox.controller.test.ts b/services/api/test/features/iae/inbox.controller.test.ts index f0768333..8f7d3db4 100644 --- a/services/api/test/features/iae/inbox.controller.test.ts +++ b/services/api/test/features/iae/inbox.controller.test.ts @@ -104,6 +104,22 @@ void test('[IAE-013] HTTP inbox metadata patch uses a revision precondition and assert.ok(typeof body === 'object' && body !== null && 'accepted' in body); assert.equal((body as { readonly accepted: boolean }).accepted, true); assert.doesNotMatch(accepted.body, /path|source|byte|excerpt/iu); + + const nonUtc = await app.inject({ + method: 'PATCH', + url: `/v1/artifacts/inbox/${inboxItemId}`, + headers: { 'if-match': '2' }, + payload: { dueAt: '2026-01-02T07:00:00.000+07:00' }, + }); + assert.equal(nonUtc.statusCode, 400); + + const cleared = await app.inject({ + method: 'PATCH', + url: `/v1/artifacts/inbox/${inboxItemId}`, + headers: { 'if-match': '2' }, + payload: { dueAt: null }, + }); + assert.equal(cleared.statusCode, 200); } finally { await app.close(); } From bc9e2665098b920413178bc4cc9901ac9b199953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:50:04 +0700 Subject: [PATCH 36/73] fix(dsm): translate immutable create races --- .../prisma-dataset-profile-repository.adapter.ts | 9 ++++++++- .../prisma-dataset-quality-repository.adapter.ts | 9 ++++++++- .../prisma-dataset-version-repository.adapter.ts | 9 ++++++++- services/api/src/features/dsm/adapter/prisma-error.ts | 8 ++++++++ .../dsm/prisma-dataset-profile-repository.test.ts | 11 ++++++++++- .../dsm/prisma-dataset-quality-repository.test.ts | 11 ++++++++++- .../dsm/prisma-dataset-version-repository.test.ts | 11 ++++++++++- 7 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 services/api/src/features/dsm/adapter/prisma-error.ts diff --git a/services/api/src/features/dsm/adapter/prisma-dataset-profile-repository.adapter.ts b/services/api/src/features/dsm/adapter/prisma-dataset-profile-repository.adapter.ts index f5f8c195..1c8bce64 100644 --- a/services/api/src/features/dsm/adapter/prisma-dataset-profile-repository.adapter.ts +++ b/services/api/src/features/dsm/adapter/prisma-dataset-profile-repository.adapter.ts @@ -13,6 +13,7 @@ import type { DatasetProfileRepositoryPortV1, DatasetProfileTransactionPortV1, } from '../application/dataset-profile-repository.port.js'; +import { isPrismaUniqueConstraintViolationV1 } from './prisma-error.js'; export interface DatasetProfileDatabaseRowV1 { readonly id: string; @@ -155,7 +156,13 @@ class PrismaDatasetProfileTransactionAdapter implements DatasetProfileTransactio throw new Error('DSM_IMMUTABLE_DATASET_PROFILE'); return; } - await this.client.datasetProfileRecord.create({ data: domainToCreate(profile) }); + try { + await this.client.datasetProfileRecord.create({ data: domainToCreate(profile) }); + } catch (error) { + if (isPrismaUniqueConstraintViolationV1(error)) + throw new Error('DSM_IMMUTABLE_DATASET_PROFILE'); + throw error; + } } public async find( diff --git a/services/api/src/features/dsm/adapter/prisma-dataset-quality-repository.adapter.ts b/services/api/src/features/dsm/adapter/prisma-dataset-quality-repository.adapter.ts index b3becaec..a7a63425 100644 --- a/services/api/src/features/dsm/adapter/prisma-dataset-quality-repository.adapter.ts +++ b/services/api/src/features/dsm/adapter/prisma-dataset-quality-repository.adapter.ts @@ -13,6 +13,7 @@ import type { DatasetQualityRepositoryPortV1, DatasetQualityTransactionPortV1, } from '../application/dataset-quality-repository.port.js'; +import { isPrismaUniqueConstraintViolationV1 } from './prisma-error.js'; export interface DatasetQualityDatabaseRowV1 { readonly id: string; @@ -129,7 +130,13 @@ class PrismaDatasetQualityTransactionAdapter implements DatasetQualityTransactio throw new Error('DSM_IMMUTABLE_QUALITY_RESULT'); return; } - await this.client.datasetQualityResultRecord.create({ data: domainToCreate(result) }); + try { + await this.client.datasetQualityResultRecord.create({ data: domainToCreate(result) }); + } catch (error) { + if (isPrismaUniqueConstraintViolationV1(error)) + throw new Error('DSM_IMMUTABLE_QUALITY_RESULT'); + throw error; + } } public async find( diff --git a/services/api/src/features/dsm/adapter/prisma-dataset-version-repository.adapter.ts b/services/api/src/features/dsm/adapter/prisma-dataset-version-repository.adapter.ts index 55cb6c03..e9c3bed1 100644 --- a/services/api/src/features/dsm/adapter/prisma-dataset-version-repository.adapter.ts +++ b/services/api/src/features/dsm/adapter/prisma-dataset-version-repository.adapter.ts @@ -13,6 +13,7 @@ import type { DatasetVersionRepositoryPortV1, DatasetVersionTransactionPortV1, } from '../application/dataset-version-repository.port.js'; +import { isPrismaUniqueConstraintViolationV1 } from './prisma-error.js'; export interface DatasetVersionDatabaseRowV1 { readonly id: string; @@ -133,7 +134,13 @@ class PrismaDatasetVersionTransactionAdapter implements DatasetVersionTransactio throw new Error('DSM_IMMUTABLE_DATASET_VERSION'); return; } - await this.client.datasetVersionRecord.create({ data: domainToCreate(version) }); + try { + await this.client.datasetVersionRecord.create({ data: domainToCreate(version) }); + } catch (error) { + if (isPrismaUniqueConstraintViolationV1(error)) + throw new Error('DSM_IMMUTABLE_DATASET_VERSION'); + throw error; + } } public async find( diff --git a/services/api/src/features/dsm/adapter/prisma-error.ts b/services/api/src/features/dsm/adapter/prisma-error.ts new file mode 100644 index 00000000..61e6c184 --- /dev/null +++ b/services/api/src/features/dsm/adapter/prisma-error.ts @@ -0,0 +1,8 @@ +export function isPrismaUniqueConstraintViolationV1(error: unknown): boolean { + return ( + typeof error === 'object' && + error !== null && + 'code' in error && + (error as { readonly code?: unknown }).code === 'P2002' + ); +} diff --git a/services/api/test/features/dsm/prisma-dataset-profile-repository.test.ts b/services/api/test/features/dsm/prisma-dataset-profile-repository.test.ts index 2488ab5d..45ae8ad8 100644 --- a/services/api/test/features/dsm/prisma-dataset-profile-repository.test.ts +++ b/services/api/test/features/dsm/prisma-dataset-profile-repository.test.ts @@ -37,10 +37,15 @@ function context() { return result.value; } -function client(rows: DatasetProfileDatabaseRowV1[]): DatasetProfileDatabaseClientV1 { +function client( + rows: DatasetProfileDatabaseRowV1[], + createConflict = false, +): DatasetProfileDatabaseClientV1 { return { datasetProfileRecord: { create({ data }) { + if (createConflict) + throw Object.assign(new Error('unique constraint violation'), { code: 'P2002' }); const persisted = { ...data } as DatasetProfileDatabaseRowV1; rows.push(persisted); return Promise.resolve(persisted); @@ -93,4 +98,8 @@ void test('[DSM-011, IAM-009] Prisma profile adapter persists immutable disclosu created.value, ]); assert.equal(rows.length, 1); + await assert.rejects( + new PrismaDatasetProfileRepositoryAdapter(client([], true)).save(tenantContext, created.value), + /DSM_IMMUTABLE_DATASET_PROFILE/u, + ); }); diff --git a/services/api/test/features/dsm/prisma-dataset-quality-repository.test.ts b/services/api/test/features/dsm/prisma-dataset-quality-repository.test.ts index 00c82ead..b53840ee 100644 --- a/services/api/test/features/dsm/prisma-dataset-quality-repository.test.ts +++ b/services/api/test/features/dsm/prisma-dataset-quality-repository.test.ts @@ -37,10 +37,15 @@ function context() { return result.value; } -function client(rows: DatasetQualityDatabaseRowV1[]): DatasetQualityDatabaseClientV1 { +function client( + rows: DatasetQualityDatabaseRowV1[], + createConflict = false, +): DatasetQualityDatabaseClientV1 { return { datasetQualityResultRecord: { create({ data }) { + if (createConflict) + throw Object.assign(new Error('unique constraint violation'), { code: 'P2002' }); const persisted = { ...data } as DatasetQualityDatabaseRowV1; rows.push(persisted); return Promise.resolve(persisted); @@ -92,4 +97,8 @@ void test('[DSM-011, DSM-013, IAM-009] Prisma quality adapter persists immutable created.value, ]); assert.equal(rows.length, 1); + await assert.rejects( + new PrismaDatasetQualityRepositoryAdapter(client([], true)).save(tenantContext, created.value), + /DSM_IMMUTABLE_QUALITY_RESULT/u, + ); }); diff --git a/services/api/test/features/dsm/prisma-dataset-version-repository.test.ts b/services/api/test/features/dsm/prisma-dataset-version-repository.test.ts index 89fb7262..e76b5da4 100644 --- a/services/api/test/features/dsm/prisma-dataset-version-repository.test.ts +++ b/services/api/test/features/dsm/prisma-dataset-version-repository.test.ts @@ -37,10 +37,15 @@ function context() { return result.value; } -function client(rows: DatasetVersionDatabaseRowV1[]): DatasetVersionDatabaseClientV1 { +function client( + rows: DatasetVersionDatabaseRowV1[], + createConflict = false, +): DatasetVersionDatabaseClientV1 { return { datasetVersionRecord: { create({ data }) { + if (createConflict) + throw Object.assign(new Error('unique constraint violation'), { code: 'P2002' }); const persisted = { ...data } as DatasetVersionDatabaseRowV1; rows.push(persisted); return Promise.resolve(persisted); @@ -91,4 +96,8 @@ void test('[DSM-002, DSM-003, IAM-009] Prisma dataset version adapter is immutab assert.deepEqual(await repository.find(tenantContext, versionId), created.value); assert.deepEqual(await repository.list(tenantContext, created.value.datasetId), [created.value]); assert.equal(rows.length, 1); + await assert.rejects( + new PrismaDatasetVersionRepositoryAdapter(client([], true)).save(tenantContext, created.value), + /DSM_IMMUTABLE_DATASET_VERSION/u, + ); }); From e634d6553555fc879c4340a4b1836bdbfa9dbfa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:51:27 +0700 Subject: [PATCH 37/73] fix(dsm): constrain quality values to scalars --- .../features/dsm/api/dataset-quality.dto.ts | 17 +++++++++-- .../dsm/dataset-quality.controller.test.ts | 29 +++++++++++++++++++ .../iae/artifact-export.controller.test.ts | 11 +++++-- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/services/api/src/features/dsm/api/dataset-quality.dto.ts b/services/api/src/features/dsm/api/dataset-quality.dto.ts index 91cc51d1..3d6032b3 100644 --- a/services/api/src/features/dsm/api/dataset-quality.dto.ts +++ b/services/api/src/features/dsm/api/dataset-quality.dto.ts @@ -2,7 +2,6 @@ import { Type } from 'class-transformer'; import { ApiProperty } from '@nestjs/swagger'; import { ArrayMaxSize, - Allow, IsArray, IsIn, IsInt, @@ -15,8 +14,22 @@ import { Min, MinLength, ValidateNested, + Validate, + ValidatorConstraint, + type ValidatorConstraintInterface, } from 'class-validator'; +@ValidatorConstraint({ name: 'isDatasetQualityScalar', async: false }) +class DatasetQualityScalarConstraint implements ValidatorConstraintInterface { + validate(value: unknown): boolean { + return ( + typeof value === 'string' || + typeof value === 'boolean' || + (typeof value === 'number' && Number.isFinite(value)) + ); + } +} + export class DatasetQualitySafeValueDto { @ApiProperty({ enum: [ @@ -55,7 +68,7 @@ export class DatasetQualitySafeValueDto { oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], }) @IsOptional() - @Allow() + @Validate(DatasetQualityScalarConstraint) value?: string | number | boolean; } diff --git a/services/api/test/features/dsm/dataset-quality.controller.test.ts b/services/api/test/features/dsm/dataset-quality.controller.test.ts index 8d090ccf..5225478b 100644 --- a/services/api/test/features/dsm/dataset-quality.controller.test.ts +++ b/services/api/test/features/dsm/dataset-quality.controller.test.ts @@ -111,6 +111,35 @@ void test('[DSM-013] quality DTO rejects unsupported source-bearing fields and m }, }); assert.equal(response.statusCode, 400); + + const nestedValue = await app.inject({ + method: 'POST', + url: '/v1/dataset-quality-results', + payload: { + resultId, + datasetId: '00000000-0000-4000-8000-000000000927', + datasetVersionId, + ruleSetVersionId: '00000000-0000-4000-8000-000000000928', + profileFingerprint: 'a'.repeat(64), + rowCountScanned: 1, + qualityState: 'BLOCKED', + findings: [ + { + findingId: '00000000-0000-4000-8000-000000000929', + ruleId: '00000000-0000-4000-8000-000000000930', + severity: 'ERROR', + messageCode: 'INVALID_VALUE', + occurrenceCount: 1, + evidenceIds: [], + detailHash: 'b'.repeat(64), + actual: { kind: 'TEXT', value: { source: 'must-not-be-accepted' } }, + }, + ], + resultFingerprint: 'c'.repeat(64), + createdAt: '2026-01-01T00:00:00.000Z', + }, + }); + assert.equal(nestedValue.statusCode, 400); } finally { await app.close(); } diff --git a/services/api/test/features/iae/artifact-export.controller.test.ts b/services/api/test/features/iae/artifact-export.controller.test.ts index 8662d6ea..63c9c7e4 100644 --- a/services/api/test/features/iae/artifact-export.controller.test.ts +++ b/services/api/test/features/iae/artifact-export.controller.test.ts @@ -21,6 +21,11 @@ const contextResult = createIamTenantContextV1({ if (!contextResult.accepted) throw new Error('fixture context invalid'); const context = contextResult.value; +function problemCode(body: string): unknown { + const parsed: unknown = JSON.parse(body); + return typeof parsed === 'object' && parsed !== null && 'code' in parsed ? parsed.code : undefined; +} + void test('IAE-018 export HTTP maps rejected service outcomes to problem responses', async () => { const { app } = await createApiApplication({ artifactExportRepository: new InMemoryArtifactExportRepositoryAdapter(), @@ -32,14 +37,14 @@ void test('IAE-018 export HTTP maps rejected service outcomes to problem respons const invalid = await app.inject({ method: 'GET', url: '/v1/artifacts/exports/not-a-uuid' }); assert.equal(invalid.statusCode, 400); assert.match(String(invalid.headers['content-type']), /^application\/problem\+json/u); - assert.equal((invalid.json() as { code: string }).code, 'INVALID_IDENTIFIER'); + assert.equal(problemCode(invalid.body), 'INVALID_IDENTIFIER'); const missing = await app.inject({ method: 'GET', url: '/v1/artifacts/exports/55555555-5555-4555-8555-555555555555', }); assert.equal(missing.statusCode, 404); - assert.equal((missing.json() as { code: string }).code, 'ARTIFACT_NOT_FOUND'); + assert.equal(problemCode(missing.body), 'ARTIFACT_NOT_FOUND'); const missingSource = await app.inject({ method: 'POST', @@ -52,7 +57,7 @@ void test('IAE-018 export HTTP maps rejected service outcomes to problem respons }, }); assert.equal(missingSource.statusCode, 404); - assert.equal((missingSource.json() as { code: string }).code, 'ARTIFACT_NOT_FOUND'); + assert.equal(problemCode(missingSource.body), 'ARTIFACT_NOT_FOUND'); } finally { await app.close(); } From 83fbeec557eb3444c6a81b100820370efd24019c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 16:54:03 +0700 Subject: [PATCH 38/73] docs(review): record PR 31 dispositions --- .../coderabbit-pr-31-disposition.md | 50 +++++++++++++++++++ packages/domain/src/artifact-upload/v1.ts | 3 +- packages/domain/src/spreadsheet-audit/v1.ts | 3 +- ...isma-artifact-export-repository.adapter.ts | 3 +- .../iae/api/artifact-admission.dto.ts | 10 +--- .../iae/artifact-admission.service.test.ts | 5 +- .../iae/artifact-export.controller.test.ts | 4 +- services/api/test/openapi.test.ts | 10 +++- 8 files changed, 70 insertions(+), 18 deletions(-) create mode 100644 docs/operations/coderabbit-pr-31-disposition.md diff --git a/docs/operations/coderabbit-pr-31-disposition.md b/docs/operations/coderabbit-pr-31-disposition.md new file mode 100644 index 00000000..2c2715ec --- /dev/null +++ b/docs/operations/coderabbit-pr-31-disposition.md @@ -0,0 +1,50 @@ +# CodeRabbit PR 31 Disposition + +Date: 2026-08-03 +Promotion PR: [#31](https://github.com/DatabreezeService/databreeze-platform/pull/31) +Automatic review ID: `4842552845` +Reviewed range: `8695eed4bd5b988af9f4bea17e724ef5e1ac101d..688896af0af45281f8d9f379837d95abed04ac6c` + +CodeRabbit ran once automatically on the promotion PR. No manual review or rerun was requested. All 32 code findings were reproduced against the current `dev` state: 29 were accepted and fixed with regression coverage, and 3 were rejected after checking the later invariants and public result types. + +| ID | Finding | Disposition | Evidence | +|---|---|---|---| +| I-01 | Request input could replace the repository-loaded artifact during admission. | Accepted and fixed. The trusted artifact is applied last and a runtime-key injection regression test proves the stored version remains authoritative. | `68e0e4a` | +| I-02 | XLSX XML members were fully decompressed before the size check. | Accepted and fixed. XML members now use a bounded `ZipExtFile` read and tests reject use of unbounded `ZipFile.read`. | `069c0cd` | +| O-01 | Prisma intake and export fixtures accepted duplicate primary keys. | Accepted and fixed. Both fixtures now emulate Prisma `P2002` behavior. | `718b406` | +| M-01 | Sparse quality `stateCounts` could raise `KeyError`. | Accepted and fixed with zero defaults and a sparse-profile regression test. | `5ed2cb4` | +| M-02 | Spreadsheet `blockedReasons` accepted duplicates. | Accepted and fixed with `ArrayUnique`. | `96553d0` | +| M-03 | A direct in-memory spreadsheet-audit save could be discarded by transaction rollback. | Accepted and fixed. Public saves use the transaction queue and callbacks use unwrapped helpers. | `6d67783` | +| M-04 | Spreadsheet-audit `createdAt` accepted non-UTC timestamps. | Accepted and fixed with strict ISO validation and an uppercase-`Z` timestamp pattern. | `8b31681` | +| M-05 | Several request arrays lacked matching runtime and OpenAPI bounds. | Accepted and fixed for version IDs, fields, mapping steps, rules, artifact inputs, evidence IDs, and findings. | `3bfe600` | +| M-06 | The intake transition test did not verify the persisted revision. | Accepted and fixed. | `fd508f9` | +| M-07 | Inbox content-leak assertions were case-sensitive. | Accepted and fixed. | `812e0c5` | +| M-08 | Readiness 503 responses documented the wrong media type. | Accepted and fixed as `application/problem+json`, with a generated-contract assertion. | `42ff542` | +| M-09 | Expired upload transfer requests were reported as generic storage unavailability. | Accepted and fixed with `UPLOAD_SESSION_EXPIRED`. | `f4af924` | +| M-10 | Export processor-version text was validated before normalization and trimming. | Accepted and fixed; empty normalized text is rejected and valid trimmed text is retained. | `e5c4976` | +| M-11 | Aggregate public API smoke coverage omitted retention and export schema versions. | Accepted and fixed. | `533e7b7` | +| M-12 | The dataset-profile negative test allegedly mixed a sampling error with its count error. | Rejected. `samplingMethod` is required for both completeness modes; the fixture removes only the sample seed when switching to `COMPLETE`, so the first negative case already isolates `INVALID_COUNT`. Clearing `samplingMethod` would create the ambiguity the comment sought to remove. | `packages/domain/test/dataset-profile-v1.test.mjs` | +| M-13 | The spreadsheet value-free test inspected the manifest root rather than the finding. | Accepted and fixed. | `eec8df5` | +| M-14 | Premature upload expiration returned `EXPIRED`. | Accepted and fixed as `INVALID_TIMESTAMP`. | `6173abf` | +| M-15 | Spreadsheet finding parser errors collapsed into `INVALID_COUNT`. | Accepted and fixed. Coordinate, kind, severity, identifier, and hash errors now retain their structural codes. | `3f769e2` | +| M-16 | The upload completion test read `.value` without proving acceptance. | Accepted and fixed. | `adb45ef` | +| M-17 | Premature protected-document expiration returned `EXPIRED`. | Accepted and fixed as `INVALID_STATE`. | `d477368` | +| M-18 | Dataset profiles allowed `rowCountScanned` above `resourceLimits.maxRows`. | Accepted and fixed. | `afb3fdc` | +| M-19 | Spreadsheet `maxRow` stopped below the XLSX row limit. | Accepted and fixed across domain validation, DTO validation, and generated OpenAPI at 1,048,576. | `3311f2a` | +| M-20 | Inbox mutation context contained an unreachable conditional branch. | Accepted and simplified after the existing undefined guard. | `4a4c781` | +| M-21 | Prisma export saves lacked visibility-safe collision handling, transaction-wrapped direct saves, and create-race translation. | Accepted and fixed with tenant-safe checks and stable immutable-manifest errors. | `34495dd` | +| M-22 | Artifact-lineage lookup should use `findMany` to select a visible row. | Rejected against current `dev`. Later commits `68e69df` and `6431c9a` enforce one globally unique lineage per derived version; the unique lookup then checks tenant visibility. `findMany` would weaken that invariant and conceal duplicate persisted state. | `services/api/src/features/iae/adapter/prisma-artifact-lineage-repository.adapter.ts` | +| M-23 | Retention and content-placement service-only error unions omitted domain result codes. | Rejected. `ArtifactRetentionServiceResultV1` already includes `ArtifactRetentionResultV1`, and `ContentPlacementServiceResultV1` already includes `ArtifactResultV1`; both public unions therefore expose the cited codes without duplicating them in their service-only error aliases. | Service result type definitions | +| M-24 | The default request-tenant-context adapter produced a generic 500. | Accepted and fixed. The shared problem error now maps the unconfigured provider to retryable `AUTHENTICATION_UNAVAILABLE`/503. | `5c0b1d4` | +| M-25 | Artifact admission accepted fractional byte sizes at the DTO boundary. | Accepted and fixed with integer runtime validation and OpenAPI type. | `b6603eb` | +| M-26 | Artifact-export controllers returned failed service envelopes with HTTP 200. | Accepted and fixed. Invalid requests map to 400 problems and missing resources to 404 problems. | `c59b5b0` | +| M-27 | Retention and inbox date-time DTOs accepted date-only or offset values. | Accepted and fixed with strict ISO/UTC validation while preserving nullable inbox `dueAt`. | `ea3c4ed` | +| M-28 | DSM immutable repositories leaked Prisma create races. | Accepted and fixed for dataset profiles, quality results, and dataset versions by translating `P2002` into their stable immutable error codes. | `bc9e266` | +| M-29 | Dataset quality safe values accepted objects and arrays despite the scalar OpenAPI contract. | Accepted and fixed with a finite scalar validator and an object-injection regression test. | `e634d65` | + +## Release handling + +- Fixes are applied through a dedicated PR to `dev`; CodeRabbit is not invoked on that PR. +- After the fix PR merges, the two critical inline discussions receive the fixing commit references and the promotion PR receives a link to this disposition. +- PR #31 remains a historical promotion slice. It receives no second CodeRabbit run and is merged only after the repair PR and required checks pass. +- The generic docstring-coverage warning was not treated as a code finding: it did not identify a changed runtime defect, and bulk comments would add noise without improving the reviewed behavior. diff --git a/packages/domain/src/artifact-upload/v1.ts b/packages/domain/src/artifact-upload/v1.ts index 1d72423f..be8120af 100644 --- a/packages/domain/src/artifact-upload/v1.ts +++ b/packages/domain/src/artifact-upload/v1.ts @@ -246,7 +246,8 @@ export function expireArtifactUploadSessionV1( const timestampValue = timestamp(now); if (!timestampValue) return rejected('INVALID_TIMESTAMP'); if (session.state !== 'OPEN') return rejected('INVALID_STATE'); - if (Date.parse(timestampValue) < Date.parse(session.expiresAt)) return rejected('INVALID_TIMESTAMP'); + if (Date.parse(timestampValue) < Date.parse(session.expiresAt)) + return rejected('INVALID_TIMESTAMP'); return accepted( Object.freeze({ ...session, state: 'EXPIRED' as const, revision: session.revision + 1 }), ); diff --git a/packages/domain/src/spreadsheet-audit/v1.ts b/packages/domain/src/spreadsheet-audit/v1.ts index a09feaea..bd1c5ea1 100644 --- a/packages/domain/src/spreadsheet-audit/v1.ts +++ b/packages/domain/src/spreadsheet-audit/v1.ts @@ -136,8 +136,7 @@ function finding(input: unknown): SpreadsheetAuditResultValidationV1 { diff --git a/services/api/test/openapi.test.ts b/services/api/test/openapi.test.ts index 909b811b..8d4c061a 100644 --- a/services/api/test/openapi.test.ts +++ b/services/api/test/openapi.test.ts @@ -197,8 +197,14 @@ void test('generates deterministic versioned OpenAPI with safe headers, errors, ['RegisterDatasetQualityResultDto', 'findings', 512], ] as const) { const schema = firstDocument.components?.schemas?.[schemaName] as Record; - const property = (schema['properties'] as Record>)[propertyName]; - assert.equal(property?.['maxItems'], maxItems, `${schemaName}.${propertyName} must be bounded`); + const property = (schema['properties'] as Record>)[ + propertyName + ]; + assert.equal( + property?.['maxItems'], + maxItems, + `${schemaName}.${propertyName} must be bounded`, + ); } const spreadsheetSheet = firstDocument.components?.schemas?.[ 'SpreadsheetAuditSheetDto' From 843a85d8a21030b1349e2645800a8f3068aeff91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:26:20 +0700 Subject: [PATCH 39/73] fix(iae): forward artifact scan state --- .../prisma-artifact-repository.adapter.ts | 2 ++ .../iae/prisma-artifact-repository.test.ts | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/services/api/src/features/iae/adapter/prisma-artifact-repository.adapter.ts b/services/api/src/features/iae/adapter/prisma-artifact-repository.adapter.ts index fa87c2c5..1077557d 100644 --- a/services/api/src/features/iae/adapter/prisma-artifact-repository.adapter.ts +++ b/services/api/src/features/iae/adapter/prisma-artifact-repository.adapter.ts @@ -429,11 +429,13 @@ export class PrismaArtifactRepositoryAdapter implements ArtifactRepositoryPortV1 context: IamTenantContextV1, versionId: ArtifactVersionV1['versionId'], status: ArtifactVersionV1['status'], + scanState?: ArtifactScanStateV1, ): Promise { return new PrismaArtifactTransactionAdapter(this.client).updateVersionStatus( context, versionId, status, + scanState, ); } public savePlacement(context: IamTenantContextV1, placement: ContentPlacementV1): Promise { diff --git a/services/api/test/features/iae/prisma-artifact-repository.test.ts b/services/api/test/features/iae/prisma-artifact-repository.test.ts index 05fbd6a7..0eaaa700 100644 --- a/services/api/test/features/iae/prisma-artifact-repository.test.ts +++ b/services/api/test/features/iae/prisma-artifact-repository.test.ts @@ -239,6 +239,39 @@ void test('[IAE-009, IAE-010] Prisma artifact status transitions reject a scan-s assert.equal(versions[0]?.status, 'ACTIVE'); }); +void test('[IAE-009, IAE-010] direct Prisma artifact status updates persist the supplied scan state', async () => { + const createdAt = parseStrictUtcTimestampV1('2026-01-01T00:00:00.000Z'); + assert.equal(createdAt.accepted, true); + if (!createdAt.accepted) throw new Error('fixture timestamp rejected'); + const artifact = createArtifactVersionV1({ + artifactId, + versionId, + tenantScope: { scopeType: 'workspace', organizationId, workspaceId }, + sourceKind: 'FILE', + dataMode: 'Hybrid', + contentSha256: 'b'.repeat(64), + byteSize: 8, + mediaType: 'text/csv', + displayName: 'orders.csv', + createdAt: createdAt.value, + }); + assert.equal(artifact.accepted, true); + if (!artifact.accepted) throw new Error('fixture artifact rejected'); + const versions: ArtifactVersionDatabaseRowV1[] = []; + const repository = new PrismaArtifactRepositoryAdapter(client(versions, [], [])); + await repository.saveVersion(context('scan-version'), artifact.value); + + const clean = await repository.updateVersionStatus( + context('scan-clean'), + versionId, + 'ACTIVE', + 'CLEAN', + ); + + assert.equal(clean?.scanState, 'CLEAN'); + assert.equal(versions[0]?.scanState, 'CLEAN'); +}); + void test('[IAE-020, DSO-006] Prisma placement adapter rejects a stale revision after a concurrent update', async () => { const createdAt = parseStrictUtcTimestampV1('2026-01-01T00:00:00.000Z'); assert.equal(createdAt.accepted, true); From 5a9cff1022b72d6ce8f0058f1830481ea41d40bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:27:14 +0700 Subject: [PATCH 40/73] fix(dso): require sequential capability revisions --- ...ma-device-capability-repository.adapter.ts | 2 ++ ...risma-device-capability-repository.test.ts | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/services/api/src/features/dso/adapter/prisma-device-capability-repository.adapter.ts b/services/api/src/features/dso/adapter/prisma-device-capability-repository.adapter.ts index 13bbfdd3..0f07a51b 100644 --- a/services/api/src/features/dso/adapter/prisma-device-capability-repository.adapter.ts +++ b/services/api/src/features/dso/adapter/prisma-device-capability-repository.adapter.ts @@ -298,6 +298,7 @@ class PrismaDeviceCapabilityTransactionAdapter implements DeviceCapabilityTransa const current = await this.findCapability(context, capability.capabilityId); if (!current) throw new Error('DSO_CAPABILITY_NOT_FOUND'); if (current.revision !== expectedRevision) throw new Error('DSO_REVISION_CONFLICT'); + if (capability.revision !== expectedRevision + 1) throw new Error('DSO_REVISION_CONFLICT'); if ( current.deviceId !== capability.deviceId || current.organizationId !== capability.organizationId || @@ -326,6 +327,7 @@ class PrismaDeviceCapabilityTransactionAdapter implements DeviceCapabilityTransa const current = await this.findGrant(context, grant.grantId); if (!current) throw new Error('DSO_GRANT_NOT_FOUND'); if (current.revision !== expectedRevision) throw new Error('DSO_REVISION_CONFLICT'); + if (grant.revision !== expectedRevision + 1) throw new Error('DSO_REVISION_CONFLICT'); if ( current.deviceId !== grant.deviceId || current.organizationId !== grant.organizationId || diff --git a/services/api/test/features/dso/prisma-device-capability-repository.test.ts b/services/api/test/features/dso/prisma-device-capability-repository.test.ts index 6b1f0967..a7a22f66 100644 --- a/services/api/test/features/dso/prisma-device-capability-repository.test.ts +++ b/services/api/test/features/dso/prisma-device-capability-repository.test.ts @@ -215,3 +215,26 @@ void test('[DSO-005, DSO-016] Prisma capability and grant replacements reject da /DSO_REVISION_CONFLICT/u, ); }); + +void test('[DSO-005, DSO-016] Prisma capability and grant replacements require one revision step', async () => { + const repository = new PrismaDeviceCapabilityRepositoryAdapter(client()); + await repository.saveCapability(context(workspaceId, 'cap-step-save'), capability()); + await assert.rejects( + repository.replaceCapability( + context(workspaceId, 'cap-step-replace'), + { ...capability(), status: 'PAUSED', revision: 1 }, + 1, + ), + /DSO_REVISION_CONFLICT/u, + ); + + await repository.saveGrant(context(workspaceId, 'grant-step-save'), grant()); + await assert.rejects( + repository.replaceGrant( + context(workspaceId, 'grant-step-replace'), + { ...grant(), status: 'REVOKED', revision: 3 }, + 1, + ), + /DSO_REVISION_CONFLICT/u, + ); +}); From 0fc77d6edb043299c19cd9b80e066fff90c163fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:28:06 +0700 Subject: [PATCH 41/73] fix(iae): block quarantined evidence handles --- .../iae/application/artifact.service.ts | 6 ++++- .../features/iae/artifact.service.test.ts | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/services/api/src/features/iae/application/artifact.service.ts b/services/api/src/features/iae/application/artifact.service.ts index c5d5f6cc..7b6ae72b 100644 --- a/services/api/src/features/iae/application/artifact.service.ts +++ b/services/api/src/features/iae/application/artifact.service.ts @@ -94,7 +94,11 @@ export class ArtifactService { if (!evidence) return undefined; const version = await transaction.findVersion(context, versionId); if (!version) return undefined; - if (version.status === 'DELETED' || evidence.sourceState !== 'AVAILABLE') + if ( + version.status === 'DELETED' || + version.status === 'QUARANTINED' || + evidence.sourceState !== 'AVAILABLE' + ) return Object.freeze({ evidence, version, action: 'UNAVAILABLE' as const }); const placements = await transaction.listPlacements(context, version.versionId); const cloud = placements.find( diff --git a/services/api/test/features/iae/artifact.service.test.ts b/services/api/test/features/iae/artifact.service.test.ts index 26a7754e..2057b2c6 100644 --- a/services/api/test/features/iae/artifact.service.test.ts +++ b/services/api/test/features/iae/artifact.service.test.ts @@ -131,3 +131,29 @@ void test('[IAE-005, IAE-006] evidence resolution returns an opaque device actio undefined, ); }); + +void test('[IAE-009, IAE-010] quarantined artifact evidence never resolves to an open handle', async () => { + const repository = new InMemoryArtifactRepositoryAdapter(); + const service = new ArtifactService(repository); + const registered = await service.register( + context(workspaceId, 'resolve-quarantined'), + input('Hybrid'), + ); + assert.equal(registered.accepted, true); + if (!registered.accepted || !registered.value.evidence) return; + await repository.updateVersionStatus( + context(workspaceId, 'quarantine-version'), + registered.value.version.versionId, + 'QUARANTINED', + 'FAILED', + ); + + const resolved = await service.resolveEvidence( + context(workspaceId, 'resolve-quarantined-read'), + registered.value.version.versionId, + registered.value.evidence.evidenceId, + ); + + assert.equal(resolved?.action, 'UNAVAILABLE'); + assert.equal('placementReference' in (resolved ?? {}), false); +}); From 2886d00dd6fb121707a4bf0623817a1e21625241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:28:49 +0700 Subject: [PATCH 42/73] fix(iae): normalize evidence sheet lookup --- packages/domain/src/artifact/v1.ts | 4 +++- packages/domain/test/artifact-v1.test.mjs | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/domain/src/artifact/v1.ts b/packages/domain/src/artifact/v1.ts index df85eaba..5bccada6 100644 --- a/packages/domain/src/artifact/v1.ts +++ b/packages/domain/src/artifact/v1.ts @@ -385,7 +385,9 @@ export function validateEvidenceCoordinateV1( if (!isEvidenceGeometry(geometry)) return rejected('INVALID_COORDINATE'); if (coordinate.kind === 'CELL') { if (geometry.kind !== 'SPREADSHEET') return rejected('COORDINATE_OUT_OF_BOUNDS'); - const sheet = geometry.sheets.find((candidate) => candidate.name === coordinate.sheet); + const sheet = geometry.sheets.find( + (candidate) => boundedText(candidate.name, 255) === coordinate.sheet, + ); const address = /^\$?([A-Z]{1,3})\$?([1-9][0-9]*)$/u.exec(coordinate.address.toUpperCase()); if (!sheet || !address) return rejected('COORDINATE_OUT_OF_BOUNDS'); const column = spreadsheetColumnNumber(address[1] ?? ''); diff --git a/packages/domain/test/artifact-v1.test.mjs b/packages/domain/test/artifact-v1.test.mjs index aac412b5..e3863382 100644 --- a/packages/domain/test/artifact-v1.test.mjs +++ b/packages/domain/test/artifact-v1.test.mjs @@ -114,6 +114,13 @@ void test('[IAE-006] evidence coordinates are validated against exact source geo ), { accepted: true, value: true }, ); + assert.deepEqual( + validateEvidenceCoordinateV1( + { kind: 'CELL', sheet: 'Sheet1', address: 'B4' }, + { kind: 'SPREADSHEET', sheets: [{ name: ' Sheet1 ', maxRow: 10, maxColumn: 3 }] }, + ), + { accepted: true, value: true }, + ); assert.deepEqual( validateEvidenceCoordinateV1( { kind: 'CELL', sheet: 'Sheet1', address: 'D4' }, From 4c8bd91088b25b90d970e77008b0ab8c9b802519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:30:39 +0700 Subject: [PATCH 43/73] fix(iae): authorize persisted placement scope --- .../in-memory-artifact-repository.adapter.ts | 8 ++- .../prisma-artifact-repository.adapter.ts | 6 ++ .../features/iae/artifact-repository.test.ts | 27 +++++++++ .../iae/prisma-artifact-repository.test.ts | 55 ++++++++++++++++++- 4 files changed, 92 insertions(+), 4 deletions(-) diff --git a/services/api/src/features/iae/adapter/in-memory-artifact-repository.adapter.ts b/services/api/src/features/iae/adapter/in-memory-artifact-repository.adapter.ts index 84f41586..1195a493 100644 --- a/services/api/src/features/iae/adapter/in-memory-artifact-repository.adapter.ts +++ b/services/api/src/features/iae/adapter/in-memory-artifact-repository.adapter.ts @@ -1,5 +1,6 @@ import { tenantScopeContainsV1, + tenantScopesEqualV1, type ArtifactScanStateV1, type ArtifactVersionV1, type ContentPlacementV1, @@ -115,14 +116,17 @@ export class InMemoryArtifactRepositoryAdapter implements ArtifactRepositoryPort async updatePlacement(context: IamTenantContextV1, placement: ContentPlacementV1): Promise { await Promise.resolve(); - if (!scopeAllowsMutation(context, placement.tenantScope)) - throw new Error('IAE_SCOPE_NARROWING_REQUIRED'); const existing = this.placements.get(placement.placementId); if (!existing) throw new Error('IAE_PLACEMENT_NOT_FOUND'); + if (!scopeAllowsMutation(context, existing.tenantScope)) + throw new Error('IAE_SCOPE_NARROWING_REQUIRED'); + if (!scopeAllowsMutation(context, placement.tenantScope)) + throw new Error('IAE_SCOPE_NARROWING_REQUIRED'); if (JSON.stringify(existing) === JSON.stringify(placement)) return; if (placement.revision !== existing.revision + 1) throw new Error('IAE_REVISION_CONFLICT'); if ( existing.artifactVersionId !== placement.artifactVersionId || + !tenantScopesEqualV1(existing.tenantScope, placement.tenantScope) || existing.kind !== placement.kind || existing.opaqueReference !== placement.opaqueReference || existing.contentSha256 !== placement.contentSha256 diff --git a/services/api/src/features/iae/adapter/prisma-artifact-repository.adapter.ts b/services/api/src/features/iae/adapter/prisma-artifact-repository.adapter.ts index 1077557d..506330c7 100644 --- a/services/api/src/features/iae/adapter/prisma-artifact-repository.adapter.ts +++ b/services/api/src/features/iae/adapter/prisma-artifact-repository.adapter.ts @@ -10,6 +10,7 @@ import { import { parseTenantScopeV1, tenantScopeContainsV1, + tenantScopesEqualV1, type TenantScopeV1, } from '@databreeze/domain/tenant-scope/v1'; @@ -336,17 +337,22 @@ class PrismaArtifactTransactionAdapter implements ArtifactTransactionPortV1 { where: { id: placement.placementId }, }); if (existing === null) throw new Error('IAE_PLACEMENT_NOT_FOUND'); + if (!tenantScopeContainsV1(context.tenantScope, rowScope(existing))) + throw new Error('IAE_SCOPE_NARROWING_REQUIRED'); if (!tenantScopeContainsV1(context.tenantScope, placement.tenantScope)) throw new Error('IAE_SCOPE_NARROWING_REQUIRED'); const versionRow = await this.client.artifactVersion.findUnique({ where: { id: placement.artifactVersionId }, }); if (versionRow === null) throw new Error('IAE_VERSION_NOT_FOUND'); + if (!tenantScopeContainsV1(context.tenantScope, rowScope(versionRow))) + throw new Error('IAE_SCOPE_NARROWING_REQUIRED'); const current = rowToPlacement(existing, rowToVersion(versionRow)); if (JSON.stringify(current) === JSON.stringify(placement)) return; if (placement.revision !== current.revision + 1) throw new Error('IAE_REVISION_CONFLICT'); if ( current.artifactVersionId !== placement.artifactVersionId || + !tenantScopesEqualV1(current.tenantScope, placement.tenantScope) || current.kind !== placement.kind || current.opaqueReference !== placement.opaqueReference || current.contentSha256 !== placement.contentSha256 diff --git a/services/api/test/features/iae/artifact-repository.test.ts b/services/api/test/features/iae/artifact-repository.test.ts index 9af96dba..663849ff 100644 --- a/services/api/test/features/iae/artifact-repository.test.ts +++ b/services/api/test/features/iae/artifact-repository.test.ts @@ -83,6 +83,33 @@ void test('[IAE-003, IAE-004] versions are immutable and placements require matc assert.equal((await repository.listPlacements(context(workspaceId), stored.versionId)).length, 1); }); +void test('[IAE-003, IAM-009] placement updates authorize the persisted workspace scope', async () => { + const repository = new InMemoryArtifactRepositoryAdapter(); + const stored = version(otherWorkspaceId); + await repository.saveVersion(context(otherWorkspaceId), stored); + const placement = createContentPlacementV1({ + placementId: '00000000-0000-4000-8000-000000000024', + artifactVersion: stored, + tenantScope: stored.tenantScope, + kind: 'CLOUD', + opaqueReference: 'cloud-reference_5678', + contentSha256: stored.contentSha256, + }); + assert.equal(placement.accepted, true); + if (!placement.accepted) return; + await repository.savePlacement(context(otherWorkspaceId), placement.value); + + await assert.rejects( + repository.updatePlacement(context(workspaceId), { + ...placement.value, + tenantScope: context(workspaceId).tenantScope, + available: false, + revision: 2, + }), + /IAE_SCOPE_NARROWING_REQUIRED/u, + ); +}); + void test('[IAE-001, IAM-009] transaction rollback does not leak a staged artifact', async () => { const repository = new InMemoryArtifactRepositoryAdapter(); const stored = version(workspaceId); diff --git a/services/api/test/features/iae/prisma-artifact-repository.test.ts b/services/api/test/features/iae/prisma-artifact-repository.test.ts index 0eaaa700..e99d0ea3 100644 --- a/services/api/test/features/iae/prisma-artifact-repository.test.ts +++ b/services/api/test/features/iae/prisma-artifact-repository.test.ts @@ -28,15 +28,16 @@ function id(value: string): StableIdentifierV1 { } const organizationId = id('00000000-0000-4000-8000-000000000501'); const workspaceId = id('00000000-0000-4000-8000-000000000502'); +const siblingWorkspaceId = id('00000000-0000-4000-8000-000000000509'); const artifactId = id('00000000-0000-4000-8000-000000000503'); const versionId = id('00000000-0000-4000-8000-000000000504'); const placementId = id('00000000-0000-4000-8000-000000000505'); const evidenceId = id('00000000-0000-4000-8000-000000000506'); -function context(key: string) { +function contextForWorkspace(candidateWorkspaceId: StableIdentifierV1, key: string) { const result = createIamTenantContextV1({ actorId: '00000000-0000-4000-8000-000000000507', - tenantScope: { scopeType: 'workspace', organizationId, workspaceId }, + tenantScope: { scopeType: 'workspace', organizationId, workspaceId: candidateWorkspaceId }, authorizationEpoch: 1, correlationId: '00000000-0000-4000-8000-000000000508', idempotencyKey: key, @@ -46,6 +47,10 @@ function context(key: string) { return result.value; } +function context(key: string) { + return contextForWorkspace(workspaceId, key); +} + function client( versions: ArtifactVersionDatabaseRowV1[], placements: ContentPlacementDatabaseRowV1[], @@ -323,3 +328,49 @@ void test('[IAE-020, DSO-006] Prisma placement adapter rejects a stale revision assert.equal(placements[0]?.available, false); assert.equal(placements[0]?.revision, 2); }); + +void test('[IAE-003, IAM-009] Prisma placement updates authorize the persisted workspace scope', async () => { + const createdAt = parseStrictUtcTimestampV1('2026-01-01T00:00:00.000Z'); + assert.equal(createdAt.accepted, true); + if (!createdAt.accepted) throw new Error('fixture timestamp rejected'); + const artifact = createArtifactVersionV1({ + artifactId, + versionId, + tenantScope: { scopeType: 'workspace', organizationId, workspaceId: siblingWorkspaceId }, + sourceKind: 'FILE', + dataMode: 'Hybrid', + contentSha256: 'c'.repeat(64), + byteSize: 8, + mediaType: 'text/csv', + displayName: 'orders.csv', + createdAt: createdAt.value, + }); + assert.equal(artifact.accepted, true); + if (!artifact.accepted) throw new Error('fixture artifact rejected'); + const placement = createContentPlacementV1({ + placementId, + artifactVersion: artifact.value, + tenantScope: artifact.value.tenantScope, + kind: 'CLOUD', + opaqueReference: 'opaque-reference-5678', + contentSha256: artifact.value.contentSha256, + }); + assert.equal(placement.accepted, true); + if (!placement.accepted) throw new Error('fixture placement rejected'); + const repository = new PrismaArtifactRepositoryAdapter(client([], [], [])); + await repository.saveVersion(contextForWorkspace(siblingWorkspaceId, 'scope-version'), artifact.value); + await repository.savePlacement( + contextForWorkspace(siblingWorkspaceId, 'scope-placement'), + placement.value, + ); + + await assert.rejects( + repository.updatePlacement(context('scope-forgery'), { + ...placement.value, + tenantScope: context('scope-forgery-input').tenantScope, + available: false, + revision: 2, + }), + /IAE_SCOPE_NARROWING_REQUIRED/u, + ); +}); From 677d981341aed132ff3f7e578a88e6b73aacdc26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:31:56 +0700 Subject: [PATCH 44/73] fix(iam): align in-memory MFA revisions --- .../in-memory-mfa-repository.adapter.ts | 12 +++++ .../api/test/features/iam/mfa.service.test.ts | 50 ++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/services/api/src/features/iam/adapter/in-memory-mfa-repository.adapter.ts b/services/api/src/features/iam/adapter/in-memory-mfa-repository.adapter.ts index 7ec25f9b..5ca1c46e 100644 --- a/services/api/src/features/iam/adapter/in-memory-mfa-repository.adapter.ts +++ b/services/api/src/features/iam/adapter/in-memory-mfa-repository.adapter.ts @@ -16,6 +16,16 @@ function cloneState(state: MfaStateV1): MfaStateV1 { function immutableState(existing: MfaStateV1, next: MfaStateV1): boolean { const existingFactors = new Map(existing.factors.map((factor) => [factor.id, factor])); const existingCodes = new Map(existing.recoveryCodes.map((code) => [code.id, code])); + if ( + existing.factors.some((factor) => !next.factors.some((candidate) => candidate.id === factor.id)) + ) + return false; + if ( + existing.recoveryCodes.some( + (code) => !next.recoveryCodes.some((candidate) => candidate.id === code.id), + ) + ) + return false; for (const factor of next.factors) { const prior = existingFactors.get(factor.id); if ( @@ -29,6 +39,7 @@ function immutableState(existing: MfaStateV1, next: MfaStateV1): boolean { factor.revision !== prior.revision + 1 ) return false; + if (!prior && factor.revision !== 1) return false; } for (const code of next.recoveryCodes) { const prior = existingCodes.get(code.id); @@ -39,6 +50,7 @@ function immutableState(existing: MfaStateV1, next: MfaStateV1): boolean { code.revision !== prior.revision + 1 ) return false; + if (!prior && code.revision !== 1) return false; } return true; } diff --git a/services/api/test/features/iam/mfa.service.test.ts b/services/api/test/features/iam/mfa.service.test.ts index 4e00f2f0..c0fe2a67 100644 --- a/services/api/test/features/iam/mfa.service.test.ts +++ b/services/api/test/features/iam/mfa.service.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { createRecoveryCodeV1 } from '@databreeze/domain/mfa/v1'; +import { createMfaFactorV1, createRecoveryCodeV1 } from '@databreeze/domain/mfa/v1'; import { InMemoryMfaRepositoryAdapter } from '../../../src/features/iam/adapter/in-memory-mfa-repository.adapter.js'; import { constantTimeRecoveryCodeMatchV1 } from '../../../src/features/iam/iam.module.js'; @@ -94,3 +94,51 @@ void test('[IAM-015] default recovery-code matching compares normalized bytes sa assert.equal(constantTimeRecoveryCodeMatchV1('digest-1', 'digest-2'), false); assert.equal(constantTimeRecoveryCodeMatchV1('digest-1', 'digest-10'), false); }); + +void test('[IAM-012, IAM-014] in-memory MFA state rejects removal and invalid new revisions', async () => { + const factor = createMfaFactorV1({ + id: factorId, + userId, + method: 'TOTP', + secretReference: 'secret-ref:totp:1', + enrolledAt: at, + }); + const code = createRecoveryCodeV1({ id: recoveryId, userId, digest: 'digest-1', createdAt: at }); + assert.equal(factor.accepted, true); + assert.equal(code.accepted, true); + if (!factor.accepted || !code.accepted) return; + const repository = new InMemoryMfaRepositoryAdapter(); + await repository.saveState(userId as never, { + factors: [factor.value], + recoveryCodes: [code.value], + }); + + await assert.rejects( + repository.saveState(userId as never, { factors: [], recoveryCodes: [code.value] }), + /IAM_MFA_REVISION_CONFLICT/u, + ); + await assert.rejects( + repository.saveState(userId as never, { factors: [factor.value], recoveryCodes: [] }), + /IAM_MFA_REVISION_CONFLICT/u, + ); + await assert.rejects( + repository.saveState(userId as never, { + factors: [{ ...factor.value, id: '00000000-0000-4000-8000-000000000004' as never, revision: 2 }], + recoveryCodes: [code.value], + }), + /IAM_MFA_REVISION_CONFLICT/u, + ); + await assert.rejects( + repository.saveState(userId as never, { + factors: [factor.value], + recoveryCodes: [ + { + ...code.value, + id: '00000000-0000-4000-8000-000000000005' as never, + revision: 2, + }, + ], + }), + /IAM_MFA_REVISION_CONFLICT/u, + ); +}); From 924c48b1b554039414dc913411457181d88fe167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:32:33 +0700 Subject: [PATCH 45/73] test(iae): enforce lineage uniqueness in fixture --- ...prisma-artifact-lineage-repository.test.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/services/api/test/features/iae/prisma-artifact-lineage-repository.test.ts b/services/api/test/features/iae/prisma-artifact-lineage-repository.test.ts index 37622836..53dc9c68 100644 --- a/services/api/test/features/iae/prisma-artifact-lineage-repository.test.ts +++ b/services/api/test/features/iae/prisma-artifact-lineage-repository.test.ts @@ -37,6 +37,16 @@ function client(rows: ArtifactLineageDatabaseRowV1[]): ArtifactLineageDatabaseCl return { artifactLineageRecord: { create({ data }) { + if ( + rows.some( + (candidate) => + candidate.id === data.id || + candidate.derivedArtifactVersionId === data.derivedArtifactVersionId, + ) + ) + return Promise.reject( + Object.assign(new Error('fixture unique constraint'), { code: 'P2002' }), + ); rows.push({ ...data }); return Promise.resolve({ ...data }); }, @@ -82,3 +92,20 @@ void test('IAE-007 Prisma lineage adapter preserves immutable lineage and source assert.deepEqual(await repository.listBySource(context, sourceVersionId), [lineage]); assert.equal(rows.length, 1); }); + +void test('IAE-007 lineage test storage enforces one record per derived artifact version', async () => { + const rows: ArtifactLineageDatabaseRowV1[] = []; + const database = client(rows); + const repository = new PrismaArtifactLineageRepositoryAdapter(database); + await repository.save(context, lineage); + const persisted = rows[0]; + if (!persisted) throw new Error('fixture lineage was not persisted'); + + await assert.rejects( + database.artifactLineageRecord.create({ + data: { ...persisted, id: '88888888-8888-4888-8888-888888888888' }, + }), + (error: unknown) => error instanceof Error && 'code' in error && error.code === 'P2002', + ); + assert.equal(rows.length, 1); +}); From 55d0c6c8d7ed70fff0ce6cb526fc528360fac2ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:32:56 +0700 Subject: [PATCH 46/73] test(iae): bind lineage index assertion --- services/api/test/prisma-foundation.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/api/test/prisma-foundation.test.mjs b/services/api/test/prisma-foundation.test.mjs index f71da3a0..1422edc1 100644 --- a/services/api/test/prisma-foundation.test.mjs +++ b/services/api/test/prisma-foundation.test.mjs @@ -502,7 +502,7 @@ test('the schema diff and centrally ordered migration inventory establish platfo ); assert.match( lineageUniquenessMigration, - /CREATE UNIQUE INDEX "artifact_lineage_derived_version_key"/, + /CREATE UNIQUE INDEX "artifact_lineage_derived_version_key"\s+ON "iae"\."artifact_lineage"\("derived_artifact_version_id"\);/u, ); const sessionScopeMigration = await readFile( path.join(migrationsDirectory, inventory[33], 'migration.sql'), From 151524e4b6c533829560bec20b4a2981ff472583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:33:41 +0700 Subject: [PATCH 47/73] fix(sa): group formula gaps by family --- .../processors/spreadsheet_auditor.py | 47 +++++++++---------- .../engine/tests/test_spreadsheet_auditor.py | 16 +++++++ 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/services/engine/src/databreeze_engine/processors/spreadsheet_auditor.py b/services/engine/src/databreeze_engine/processors/spreadsheet_auditor.py index 97829224..724aeae4 100644 --- a/services/engine/src/databreeze_engine/processors/spreadsheet_auditor.py +++ b/services/engine/src/databreeze_engine/processors/spreadsheet_auditor.py @@ -258,32 +258,31 @@ def audit_workbook( cells_by_column.setdefault(column, {})[row] = formula gap_keys: set[tuple[str, str]] = set() for column, rows in cells_by_column.items(): - formula_rows = sorted(row for row, formula in rows.items() if formula is not None) - for previous_row, next_row in itertools.pairwise(formula_rows): - if next_row - previous_row <= 1: - continue - previous_formula = rows[previous_row] - next_formula = rows[next_row] - if previous_formula is None or next_formula is None: - continue - previous_family = _normalized_formula(previous_formula) - if previous_family != _normalized_formula(next_formula): - continue - populated_rows = sorted(row for row in rows if previous_row < row < next_row) - for row in populated_rows: - address = f"{_column_name(column)}{row}" - key = (address, previous_family) - if key in gap_keys: + rows_by_family: dict[str, list[int]] = {} + for row, formula in rows.items(): + if formula is not None: + rows_by_family.setdefault(_normalized_formula(formula), []).append(row) + for family, formula_rows in rows_by_family.items(): + for previous_row, next_row in itertools.pairwise(sorted(formula_rows)): + if next_row - previous_row <= 1: continue - gap_keys.add(key) - findings.append( - SpreadsheetFinding( - sheet=sheet_name, - address=address, - kind="FORMULA_GAP", - formulaFingerprint=_fingerprint(previous_family), - ) + populated_rows = sorted( + row for row in rows if previous_row < row < next_row ) + for row in populated_rows: + address = f"{_column_name(column)}{row}" + key = (address, family) + if key in gap_keys: + continue + gap_keys.add(key) + findings.append( + SpreadsheetFinding( + sheet=sheet_name, + address=address, + kind="FORMULA_GAP", + formulaFingerprint=_fingerprint(family), + ) + ) summaries.append( SpreadsheetSheetSummary( name=sheet_name, diff --git a/services/engine/tests/test_spreadsheet_auditor.py b/services/engine/tests/test_spreadsheet_auditor.py index 759a05d8..9762953b 100644 --- a/services/engine/tests/test_spreadsheet_auditor.py +++ b/services/engine/tests/test_spreadsheet_auditor.py @@ -17,6 +17,7 @@ def _workbook( macro: bool = False, external_link: bool = False, formula_gap: bool = False, + mixed_formula_gap: bool = False, absolute_reference: bool = False, ) -> bytes: workbook = ( @@ -34,6 +35,12 @@ def _workbook( b'SUM(B2:C2)3' b'SUM(B3:C3)3' ) + elif mixed_formula_gap: + sheet_rows = ( + b'SUM(B1:C1)3' + b'B2*C29' + b'SUM(B3:C3)3' + ) elif formula_gap: sheet_rows = ( b'SUM(B1:C1)3' @@ -80,6 +87,15 @@ def test_audit_reports_a_formula_gap_without_returning_the_intervening_value() - assert all("value" not in finding.model_dump() for finding in result.findings) +def test_audit_pairs_matching_formula_families_across_an_intervening_family() -> None: + result = audit_workbook(_workbook(mixed_formula_gap=True)) + assert [ + (finding.address, finding.kind) + for finding in result.findings + if finding.kind == "FORMULA_GAP" + ] == [("A2", "FORMULA_GAP")] + + def test_formula_family_normalization_preserves_absolute_references() -> None: result = audit_workbook(_workbook(absolute_reference=True)) assert [(finding.address, finding.kind) for finding in result.findings] == [ From 454043bd72a7a8982dcfa9888bf26643c1592eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:34:22 +0700 Subject: [PATCH 48/73] test(iam): prove bootstrap transaction client --- ...isma-identity-bootstrap-repository.test.ts | 58 +++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/services/api/test/features/iam/prisma-identity-bootstrap-repository.test.ts b/services/api/test/features/iam/prisma-identity-bootstrap-repository.test.ts index f19500ee..f8454d07 100644 --- a/services/api/test/features/iam/prisma-identity-bootstrap-repository.test.ts +++ b/services/api/test/features/iam/prisma-identity-bootstrap-repository.test.ts @@ -41,6 +41,7 @@ function createDatabase(): { readonly projects: Map; readonly memberships: Map; readonly transactionCalls: { value: number }; + readonly transactionWriteCalls: { value: number }; } { const users = new Map([ [ @@ -61,6 +62,7 @@ function createDatabase(): { const projects = new Map(); const memberships = new Map(); const transactionCalls = { value: 0 }; + const transactionWriteCalls = { value: 0 }; const client = { userIdentity: { findUnique: async ({ where }: { readonly where: { readonly id: string } }) => @@ -126,8 +128,39 @@ function createDatabase(): { projects: new Map(projects), memberships: new Map(memberships), }; + const transaction = { + ...client, + organizationIdentity: { + ...client.organizationIdentity, + create: async (input: { readonly data: OrganizationIdentityDatabaseRowV1 }) => { + transactionWriteCalls.value += 1; + return client.organizationIdentity.create(input); + }, + }, + workspaceIdentity: { + ...client.workspaceIdentity, + create: async (input: { readonly data: WorkspaceIdentityDatabaseRowV1 }) => { + transactionWriteCalls.value += 1; + return client.workspaceIdentity.create(input); + }, + }, + projectIdentity: { + ...client.projectIdentity, + create: async (input: { readonly data: ProjectIdentityDatabaseRowV1 }) => { + transactionWriteCalls.value += 1; + return client.projectIdentity.create(input); + }, + }, + membershipIdentity: { + ...client.membershipIdentity, + create: async (input: { readonly data: MembershipIdentityDatabaseRowV1 }) => { + transactionWriteCalls.value += 1; + return client.membershipIdentity.create(input); + }, + }, + } as IdentityBootstrapDatabaseClientV1; try { - return await work(client); + return await work(transaction); } catch (error) { organizations.clear(); workspaces.clear(); @@ -141,12 +174,28 @@ function createDatabase(): { } }, } as unknown as IdentityBootstrapDatabaseClientV1; - return { client, users, organizations, workspaces, projects, memberships, transactionCalls }; + return { + client, + users, + organizations, + workspaces, + projects, + memberships, + transactionCalls, + transactionWriteCalls, + }; } void test('[IAM-001, IAM-009, IAM-011] Prisma bootstrap persists and reconstructs a personal owner hierarchy', async () => { - const { client, organizations, workspaces, projects, memberships, transactionCalls } = - createDatabase(); + const { + client, + organizations, + workspaces, + projects, + memberships, + transactionCalls, + transactionWriteCalls, + } = createDatabase(); const adapter = new PrismaIdentityBootstrapRepositoryAdapter(client); const validated = bootstrapPersonalOrganizationV1(input); assert.equal(validated.accepted, true); @@ -154,6 +203,7 @@ void test('[IAM-001, IAM-009, IAM-011] Prisma bootstrap persists and reconstruct await adapter.save(validated.value); assert.equal(transactionCalls.value, 1); + assert.equal(transactionWriteCalls.value, 4); assert.equal(organizations.size, 1); assert.equal(workspaces.size, 1); assert.equal(projects.size, 1); From 71acbc9b7c3cb616d3b6176dfc714c808ab32727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:35:03 +0700 Subject: [PATCH 49/73] test(api): prove foundation option forwarding --- .../foundation-module-composition.test.ts | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/services/api/test/features/foundation-module-composition.test.ts b/services/api/test/features/foundation-module-composition.test.ts index 5c346ed7..9cc318c2 100644 --- a/services/api/test/features/foundation-module-composition.test.ts +++ b/services/api/test/features/foundation-module-composition.test.ts @@ -40,12 +40,38 @@ function moduleTypes(): readonly unknown[] { } void test('[AUD-001, BUA-001] API application options expose durable module adapters', () => { + const auditRepository = {} as never; + const entitlementRepository = {} as never; const options = { - auditRepository: {} as never, - entitlementRepository: {} as never, + auditRepository, + entitlementRepository, } satisfies ApiApplicationOptions; const registered = AppModule.register(options); assert.equal(registered.module, AppModule); + for (const [moduleType, token, expected] of [ + [AudModule, AUDIT_REPOSITORY_PORT, auditRepository], + [BuaModule, ENTITLEMENT_REPOSITORY_PORT, entitlementRepository], + ] as const) { + const child = registered.imports?.find( + (candidate) => + typeof candidate === 'object' && + candidate !== null && + 'module' in candidate && + candidate.module === moduleType, + ); + assert.ok(child && typeof child === 'object' && 'providers' in child); + if (!child || typeof child !== 'object' || !('providers' in child)) return; + const provider = child.providers?.find( + (candidate) => + typeof candidate === 'object' && + candidate !== null && + 'provide' in candidate && + candidate.provide === token, + ); + assert.ok(provider && 'useValue' in provider); + if (!provider || !('useValue' in provider)) return; + assert.equal(provider.useValue, expected); + } }); void test('[IAM-001, AUD-001, BUA-001] application composition includes identity, audit, and entitlements modules', () => { From 32b6aceb47b0190ff9a999a130bb4049c26077e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:35:30 +0700 Subject: [PATCH 50/73] test(iae): fail closed on retention authorization --- .../api/test/features/iae/artifact-retention.service.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/api/test/features/iae/artifact-retention.service.test.ts b/services/api/test/features/iae/artifact-retention.service.test.ts index 621f40f4..e239a9dd 100644 --- a/services/api/test/features/iae/artifact-retention.service.test.ts +++ b/services/api/test/features/iae/artifact-retention.service.test.ts @@ -106,5 +106,7 @@ void test('[IAE-016, IAE-021] retention service preserves blocked requests and a assert.equal(authorized.value.state, 'AUTHORIZED'); const found = await service.find(tenantContext, authorized.value.requestId); assert.deepEqual(found, authorized); + } else { + assert.fail('expected artifact deletion authorization to succeed'); } }); From 9702160900603848b2ac7864db08e139c7778f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:37:07 +0700 Subject: [PATCH 51/73] fix(iae): derive deletion requester from session --- services/api/openapi/v1.json | 8 ++++++-- .../src/features/iae/api/artifact-retention.dto.ts | 13 +++++++++---- .../iae/artifact-retention.controller.test.ts | 1 - services/api/test/openapi.test.ts | 11 +++++++++++ 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/services/api/openapi/v1.json b/services/api/openapi/v1.json index 16922a66..da8befd7 100644 --- a/services/api/openapi/v1.json +++ b/services/api/openapi/v1.json @@ -7521,7 +7521,12 @@ "activeApproval": { "type": "boolean" }, "legalHold": { "type": "boolean" }, "requestId": { "type": "string", "format": "uuid" }, - "requestedBy": { "type": "string", "format": "uuid" }, + "requestedBy": { + "type": "string", + "format": "uuid", + "deprecated": true, + "description": "Ignored. Attribution always uses the authenticated actor." + }, "requestedAt": { "type": "string", "format": "date-time" } }, "required": [ @@ -7533,7 +7538,6 @@ "activeApproval", "legalHold", "requestId", - "requestedBy", "requestedAt" ] }, diff --git a/services/api/src/features/iae/api/artifact-retention.dto.ts b/services/api/src/features/iae/api/artifact-retention.dto.ts index d1ca10fc..688a9142 100644 --- a/services/api/src/features/iae/api/artifact-retention.dto.ts +++ b/services/api/src/features/iae/api/artifact-retention.dto.ts @@ -1,5 +1,5 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsISO8601, IsInt, IsUUID, Matches, Min } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsBoolean, IsISO8601, IsInt, IsOptional, IsUUID, Matches, Min } from 'class-validator'; const strictUtcTimestamp = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u; @@ -43,9 +43,14 @@ export class CreateArtifactDeletionRequestDto extends RetentionEvaluationDto { @IsUUID() requestId!: string; - @ApiProperty({ format: 'uuid' }) + @ApiPropertyOptional({ + format: 'uuid', + deprecated: true, + description: 'Ignored. Attribution always uses the authenticated actor.', + }) + @IsOptional() @IsUUID() - requestedBy!: string; + requestedBy?: string; @ApiProperty({ format: 'date-time' }) @IsISO8601({ strict: true, strictSeparator: true }) diff --git a/services/api/test/features/iae/artifact-retention.controller.test.ts b/services/api/test/features/iae/artifact-retention.controller.test.ts index cd965517..19bea3f5 100644 --- a/services/api/test/features/iae/artifact-retention.controller.test.ts +++ b/services/api/test/features/iae/artifact-retention.controller.test.ts @@ -82,7 +82,6 @@ void test('[IAE-016, IAM-009] retention HTTP binds requester to the authenticate url: `/v1/artifact-versions/${versionId}/deletion-requests`, payload: { requestId, - requestedBy: '00000000-0000-4000-8000-000000000739', requestedAt: '2026-08-02T01:00:00.000Z', evaluatedAt: '2026-08-02T01:00:00.000Z', workspaceRetentionUntil: '2026-07-01T00:00:00.000Z', diff --git a/services/api/test/openapi.test.ts b/services/api/test/openapi.test.ts index 8d4c061a..13a6b8f8 100644 --- a/services/api/test/openapi.test.ts +++ b/services/api/test/openapi.test.ts @@ -187,6 +187,17 @@ void test('generates deterministic versioned OpenAPI with safe headers, errors, 'refreshToken' ]; assert.equal(refreshToken?.['writeOnly'], undefined); + const deletionRequest = firstDocument.components?.schemas?.[ + 'CreateArtifactDeletionRequestDto' + ] as Record; + assert.equal( + (deletionRequest['required'] as readonly string[]).includes('requestedBy'), + false, + ); + const requestedBy = ( + deletionRequest['properties'] as Record> + )['requestedBy']; + assert.equal(requestedBy?.['deprecated'], true); for (const [schemaName, propertyName, maxItems] of [ ['CreateArtifactExportDto', 'versionIds', 1024], ['CreateGovernedDatasetDto', 'fields', 256], From 2fa1e6e31cf9cf8572d4f78d8b5c371c58f2c59a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:37:34 +0700 Subject: [PATCH 52/73] docs(review): record PR 33 dispositions --- .../coderabbit-pr-33-disposition.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/operations/coderabbit-pr-33-disposition.md diff --git a/docs/operations/coderabbit-pr-33-disposition.md b/docs/operations/coderabbit-pr-33-disposition.md new file mode 100644 index 00000000..a0ce307b --- /dev/null +++ b/docs/operations/coderabbit-pr-33-disposition.md @@ -0,0 +1,37 @@ +# CodeRabbit disposition for promotion PR 33 + +Promotion PR [#33](https://github.com/DatabreezeService/databreeze-platform/pull/33) +received exactly one automatic CodeRabbit review (`4843018511`) for the +historical range `56011dc633fe8d999d96a6ea26fdc64319447a8e..12d92716ad287544e0d6149925e0496273306d51`. +The review contained seven inline findings and eight review-body findings. Each +claim was reproduced against current `dev` before disposition. CodeRabbit was +not invoked again. + +| ID | Claim | Disposition | Evidence | +|---|---|---|---| +| CR33-01 | Spreadsheet evidence lookup compared canonical coordinates with an unnormalized geometry name. | Accepted and fixed. | `2886d00`; domain regression for a whitespace-normalized sheet name. | +| CR33-02 | Placement mutation authorized the caller-supplied scope instead of the persisted placement scope. | Accepted and fixed in both adapters. | `4c8bd91`; Prisma and in-memory sibling-workspace mutation regressions. | +| CR33-03 | In-memory MFA state allowed record removal and invalid initial revisions. | Accepted and fixed to match the Prisma invariants. | `677d981`; factor and recovery-code removal/new-revision regressions. | +| CR33-04 | Prisma MFA updates were not revision-conditional. | Rejected as already resolved on current `dev`. | `e668bd4` uses `updateMany` with the prior revision and requires `count === 1` for factors and recovery codes; existing race tests pass. | +| CR33-05 | The lineage repository test double did not enforce the derived-version unique constraint. | Accepted and fixed. | `924c48b`; the fake reports a Prisma-style `P2002` and retains one row. | +| CR33-06 | The migration test asserted only the lineage index name. | Accepted and fixed. | `55d0c6c`; the assertion binds the unique index, schema-qualified relation, and column. | +| CR33-07 | Formula-gap detection paired rows before grouping by formula family. | Accepted and fixed. | `151524e`; a different intervening formula now produces the expected value-free gap finding. | +| CR33-08 | The public Prisma artifact adapter dropped an optional scan state. | Accepted and fixed. | `843a85d`; direct adapter regression proves `PENDING` to `CLEAN` persistence. | +| CR33-09 | Capability and grant replacements did not require exactly one revision step. | Accepted and fixed. | `5a9cff1`; invalid same/skipped revisions fail with `DSO_REVISION_CONFLICT`. | +| CR33-10 | Quarantined evidence could resolve to a live placement handle. | Accepted and fixed. | `0fc77d6`; quarantined cloud evidence resolves only to `UNAVAILABLE`. | +| CR33-11 | The bootstrap test passed the base client as its transaction client. | Accepted and strengthened. | `454043b`; a distinct transaction client records all four hierarchy writes. | +| CR33-12 | The application composition test did not prove audit and entitlement option forwarding. | Accepted and strengthened. | `71acbc9`; child-module providers retain the exact repository identities. | +| CR33-13 | The lineage unique index should be built concurrently. | Rejected for this migration stage. | Plan 010 introduces no customer workflow or production data migration; ADR-0002 uses ordinary Prisma SQL migrations. `CREATE INDEX CONCURRENTLY` cannot run in Prisma's ordinary transactional migration path, while the production expand/migrate/verify/contract gate remains in Plan 400. | +| CR33-14 | A retention test could pass without asserting failed authorization. | Accepted and strengthened. | `32b6ace`; the unexpected result branch now fails explicitly. | +| CR33-15 | `requestedBy` remained required although attribution uses the authenticated actor. | Accepted and fixed compatibly. | `9702160`; the field is optional/deprecated, omission succeeds, generated OpenAPI records authenticated attribution. | + +The generic docstring-coverage warning is informational rather than a repository +gate: DataBreeze has no accepted 80% docstring requirement, and adding comments +solely to satisfy an external heuristic would not repair behavior. Existing +documentation and lint/type/test gates remain authoritative. + +The accepted changes are collected on `fix/coderabbit-promotion-33`. They are +not pushed directly into the historical promotion branch, so PR 33's reviewed +commit range remains immutable. They will enter `dev` through the next +30–50-commit feature batch and reach `main` through a later single-review +promotion slice. From cfdb786023cddca75d7d36d797233c9401ac8e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:38:24 +0700 Subject: [PATCH 53/73] style(review): format promotion fixes --- .../features/iae/prisma-artifact-repository.test.ts | 5 ++++- services/api/test/features/iam/mfa.service.test.ts | 4 +++- services/api/test/openapi.test.ts | 11 ++++------- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/services/api/test/features/iae/prisma-artifact-repository.test.ts b/services/api/test/features/iae/prisma-artifact-repository.test.ts index e99d0ea3..c53ba6a8 100644 --- a/services/api/test/features/iae/prisma-artifact-repository.test.ts +++ b/services/api/test/features/iae/prisma-artifact-repository.test.ts @@ -358,7 +358,10 @@ void test('[IAE-003, IAM-009] Prisma placement updates authorize the persisted w assert.equal(placement.accepted, true); if (!placement.accepted) throw new Error('fixture placement rejected'); const repository = new PrismaArtifactRepositoryAdapter(client([], [], [])); - await repository.saveVersion(contextForWorkspace(siblingWorkspaceId, 'scope-version'), artifact.value); + await repository.saveVersion( + contextForWorkspace(siblingWorkspaceId, 'scope-version'), + artifact.value, + ); await repository.savePlacement( contextForWorkspace(siblingWorkspaceId, 'scope-placement'), placement.value, diff --git a/services/api/test/features/iam/mfa.service.test.ts b/services/api/test/features/iam/mfa.service.test.ts index c0fe2a67..b0574b69 100644 --- a/services/api/test/features/iam/mfa.service.test.ts +++ b/services/api/test/features/iam/mfa.service.test.ts @@ -123,7 +123,9 @@ void test('[IAM-012, IAM-014] in-memory MFA state rejects removal and invalid ne ); await assert.rejects( repository.saveState(userId as never, { - factors: [{ ...factor.value, id: '00000000-0000-4000-8000-000000000004' as never, revision: 2 }], + factors: [ + { ...factor.value, id: '00000000-0000-4000-8000-000000000004' as never, revision: 2 }, + ], recoveryCodes: [code.value], }), /IAM_MFA_REVISION_CONFLICT/u, diff --git a/services/api/test/openapi.test.ts b/services/api/test/openapi.test.ts index 13a6b8f8..abe78db0 100644 --- a/services/api/test/openapi.test.ts +++ b/services/api/test/openapi.test.ts @@ -190,13 +190,10 @@ void test('generates deterministic versioned OpenAPI with safe headers, errors, const deletionRequest = firstDocument.components?.schemas?.[ 'CreateArtifactDeletionRequestDto' ] as Record; - assert.equal( - (deletionRequest['required'] as readonly string[]).includes('requestedBy'), - false, - ); - const requestedBy = ( - deletionRequest['properties'] as Record> - )['requestedBy']; + assert.equal((deletionRequest['required'] as readonly string[]).includes('requestedBy'), false); + const requestedBy = (deletionRequest['properties'] as Record>)[ + 'requestedBy' + ]; assert.equal(requestedBy?.['deprecated'], true); for (const [schemaName, propertyName, maxItems] of [ ['CreateArtifactExportDto', 'versionIds', 1024], From 8eccfa41a9067f03927a14edd5e066979b80f715 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:49:59 +0700 Subject: [PATCH 54/73] fix(android): fail closed on hostile telemetry maps --- .../android/telemetry/TelemetryContract.kt | 25 +++++++++++++++---- .../android/TelemetryContractTest.kt | 24 ++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/apps/android/app/src/main/java/com/databreeze/android/telemetry/TelemetryContract.kt b/apps/android/app/src/main/java/com/databreeze/android/telemetry/TelemetryContract.kt index d8153e40..b941ef80 100644 --- a/apps/android/app/src/main/java/com/databreeze/android/telemetry/TelemetryContract.kt +++ b/apps/android/app/src/main/java/com/databreeze/android/telemetry/TelemetryContract.kt @@ -50,7 +50,8 @@ object TelemetryContract { fun sanitizeAttributes(input: Map): Map { val result = linkedMapOf() - input.forEach { (key, value) -> + val entries = readAttributeEntries(input) ?: return emptyMap() + entries.forEach { (key, value) -> require(key.matches(Regex("^[A-Za-z][A-Za-z0-9]{0,63}$"))) { "invalid telemetry key" } @@ -62,13 +63,22 @@ object TelemetryContract { } fun assertSafeAttributes(input: Map) { - input.forEach { (key, value) -> + val entries = readAttributeEntries(input) + ?: throw IllegalArgumentException("telemetry attributes are not readable") + entries.forEach { (key, value) -> require(key in SafeAttributeKeys && safeScalar(key, value) != null) { "telemetry attribute is not allowed: $key" } } } + private fun readAttributeEntries(input: Map): List>? = + try { + input.entries.map { entry -> entry.key to entry.value } + } catch (_: Exception) { + null + } + private fun safeScalar(key: String, value: Any?): Any? { if (key == "sampled") return value as? Boolean if (key in numericKeys || key == "status") { @@ -148,9 +158,14 @@ object TelemetryContract { } private fun singleHeader(headers: Map>, name: String): String? { - val values = headers.entries - .filter { it.key.lowercase() == name } - .flatMap { it.value } + val entries = try { + headers.entries.map { entry -> entry.key to entry.value.toList() } + } catch (_: Exception) { + throw IllegalArgumentException("telemetry headers are not readable") + } + val values = entries + .filter { it.first.lowercase() == name } + .flatMap { it.second } require(values.size <= 1) { "ambiguous telemetry $name header" } return values.singleOrNull()?.also { require(it.isNotEmpty()) { "empty telemetry $name header" } } } diff --git a/apps/android/app/src/test/java/com/databreeze/android/TelemetryContractTest.kt b/apps/android/app/src/test/java/com/databreeze/android/TelemetryContractTest.kt index 6ad0952d..f3b9cfa5 100644 --- a/apps/android/app/src/test/java/com/databreeze/android/TelemetryContractTest.kt +++ b/apps/android/app/src/test/java/com/databreeze/android/TelemetryContractTest.kt @@ -4,6 +4,7 @@ import com.databreeze.android.telemetry.CorrelationContext import com.databreeze.android.telemetry.TelemetryContract import org.junit.Assert.assertEquals import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue import org.junit.Test class TelemetryContractTest { @@ -73,4 +74,27 @@ class TelemetryContractTest { ) } } + + @Test + fun providerBackedMapsFailClosedWithoutLeakingTheirCause() { + val hostileAttributes = object : Map by emptyMap() { + override val entries: Set> + get() = throw IllegalStateException("provider attribute cause") + } + assertEquals(emptyMap(), TelemetryContract.sanitizeAttributes(hostileAttributes)) + val attributeError = assertThrows(IllegalArgumentException::class.java) { + TelemetryContract.assertSafeAttributes(hostileAttributes) + } + assertEquals("telemetry attributes are not readable", attributeError.message) + + val hostileHeaders = object : Map> by emptyMap() { + override val entries: Set>> + get() = throw IllegalStateException("provider header cause") + } + val headerError = assertThrows(IllegalArgumentException::class.java) { + TelemetryContract.correlationFromHeaders(hostileHeaders) + } + assertTrue(headerError.message.orEmpty().contains("not readable")) + assertTrue(!headerError.message.orEmpty().contains("provider header cause")) + } } From 2ff90188af2057475d2c4eb9ad9093d0c9afe512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 19:46:40 +0700 Subject: [PATCH 55/73] fix(infra): tighten OpenTofu semantic version checks --- tools/repo-cli/src/check-aws-infrastructure.mjs | 2 +- tools/repo-cli/src/validate-aws-opentofu.mjs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/repo-cli/src/check-aws-infrastructure.mjs b/tools/repo-cli/src/check-aws-infrastructure.mjs index 8d12bd54..74473d41 100644 --- a/tools/repo-cli/src/check-aws-infrastructure.mjs +++ b/tools/repo-cli/src/check-aws-infrastructure.mjs @@ -35,7 +35,7 @@ const opentofuVersion = readFileSync( path.join(infrastructureRoot, '.opentofu-version'), 'utf8', ).trim(); -if (!/^\d+\.\d+\.\d+$/u.test(opentofuVersion)) { +if (!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u.test(opentofuVersion)) { fail('the OpenTofu version pin must be one exact semantic version'); } diff --git a/tools/repo-cli/src/validate-aws-opentofu.mjs b/tools/repo-cli/src/validate-aws-opentofu.mjs index 82cae21d..833c9791 100644 --- a/tools/repo-cli/src/validate-aws-opentofu.mjs +++ b/tools/repo-cli/src/validate-aws-opentofu.mjs @@ -54,7 +54,8 @@ export function main(argv = process.argv.slice(2)) { if (argv.length > 0) fail(`unknown argument: ${argv[0]}`); const version = readFileSync(path.join(infrastructureRoot, '.opentofu-version'), 'utf8').trim(); - if (!/^\d+\.\d+\.\d+$/u.test(version)) fail('version pin is not an exact semantic version'); + if (!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u.test(version)) + fail('version pin is not an exact semantic version'); const image = `ghcr.io/opentofu/opentofu:${version}`; const sourceMount = `type=bind,source=${infrastructureRoot},target=/workspace`; const validationDirectory = mkdtempSync(path.join(os.tmpdir(), 'databreeze-tofu-')); From 1ff27ff5eac3f5085313f3fb97add9c33e599ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 19:47:01 +0700 Subject: [PATCH 56/73] test(infra): cover strict OpenTofu version pins --- tools/repo-cli/test/aws-infrastructure.test.mjs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index 91b34f58..eecbee6f 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -15,6 +15,19 @@ test('AWS validation pins one OpenTofu CLI and official container release', () = assert.match(readme, /ghcr\.io\/opentofu\/opentofu:1\.12\.5/u); }); +test('AWS validators accept strict semantic versions without leading zero components', () => { + const strictSemanticVersion = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u; + for (const version of ['0.0.0', '1.2.3', '10.20.30']) + assert.equal(strictSemanticVersion.test(version), true, version); + for (const version of ['01.2.3', '1.02.3', '1.2.03', '1.2', 'v1.2.3']) + assert.equal(strictSemanticVersion.test(version), false, version); + + const expectedLiteral = + '/^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$/u'; + assert.ok(read('tools/repo-cli/src/check-aws-infrastructure.mjs').includes(expectedLiteral)); + assert.ok(read('tools/repo-cli/src/validate-aws-opentofu.mjs').includes(expectedLiteral)); +}); + test('AWS container validation command is pinned, isolated, and non-applying', () => { const script = path.join(repositoryRoot, 'tools/repo-cli/src/validate-aws-opentofu.mjs'); const help = spawnSync(process.execPath, [script, '--help'], { From 7be00e38c48ac273b1d63bc837180da7d1f811fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 19:47:10 +0700 Subject: [PATCH 57/73] fix(infra): make OpenTofu validation mounts read-only --- infrastructure/aws/README.md | 2 +- tools/repo-cli/src/validate-aws-opentofu.mjs | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/infrastructure/aws/README.md b/infrastructure/aws/README.md index 7255c9a0..3f3e50f9 100644 --- a/infrastructure/aws/README.md +++ b/infrastructure/aws/README.md @@ -33,7 +33,7 @@ side effects. ```text pnpm infra:check cd infrastructure/aws/environments/alpha -tofu init -backend=false +tofu init -backend=false -lockfile=readonly tofu validate tofu plan -var-file=terraform.tfvars ``` diff --git a/tools/repo-cli/src/validate-aws-opentofu.mjs b/tools/repo-cli/src/validate-aws-opentofu.mjs index 833c9791..97e20f1d 100644 --- a/tools/repo-cli/src/validate-aws-opentofu.mjs +++ b/tools/repo-cli/src/validate-aws-opentofu.mjs @@ -12,9 +12,9 @@ const containerDataDirectory = '/tmp/databreeze-tofu'; function usage() { console.log(`Usage: pnpm infra:validate -Runs format, backend-disabled initialization, and validation through the -official pinned OpenTofu container. The command never plans or applies -infrastructure and removes its isolated provider cache on completion.`); +Runs format, backend-disabled initialization, validation, and a mocked plan +test through the official pinned OpenTofu container. The command does not +apply infrastructure and removes its isolated provider cache on completion.`); } function fail(message) { @@ -57,7 +57,7 @@ export function main(argv = process.argv.slice(2)) { if (!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u.test(version)) fail('version pin is not an exact semantic version'); const image = `ghcr.io/opentofu/opentofu:${version}`; - const sourceMount = `type=bind,source=${infrastructureRoot},target=/workspace`; + const sourceMount = `type=bind,source=${infrastructureRoot},target=/workspace,readonly`; const validationDirectory = mkdtempSync(path.join(os.tmpdir(), 'databreeze-tofu-')); const dataMount = `type=bind,source=${validationDirectory},target=${containerDataDirectory}`; @@ -100,7 +100,9 @@ export function main(argv = process.argv.slice(2)) { removeValidationDirectory(validationDirectory); } - console.log(`AWS OpenTofu ${version} container validation passed without planning or applying.`); + console.log( + `AWS OpenTofu ${version} container validation passed format, validation, and mocked plan tests without applying infrastructure.`, + ); } if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { From 238f6195ea81df50e79a5d2fafc3fceab3a88a7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 19:47:19 +0700 Subject: [PATCH 58/73] test(infra): assert OpenTofu safety wording and mounts --- tools/repo-cli/test/aws-infrastructure.test.mjs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index eecbee6f..9d2c6ea9 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -36,17 +36,24 @@ test('AWS container validation command is pinned, isolated, and non-applying', ( }); assert.equal(help.status, 0, help.stderr); assert.match(help.stdout, /official pinned OpenTofu container/u); + assert.match(help.stdout, /mocked plan test/u); + assert.match(help.stdout, /does not\s+apply infrastructure/u); const source = read('tools/repo-cli/src/validate-aws-opentofu.mjs'); assert.match(source, /'fmt',\s*'-check',\s*'-recursive'/u); assert.match(source, /'init',\s*'-backend=false',\s*'-input=false',\s*'-lockfile=readonly'/u); assert.match(source, /'validate', '-no-color'/u); assert.match(source, /'test', '-no-color'/u); + assert.match(source, /target=\/workspace,readonly/u); assert.match(source, /TF_DATA_DIR=\/tmp\/databreeze-tofu/u); assert.doesNotMatch(source, /['"]apply['"]/u); assert.match( read('package.json'), /"infra:validate": "node tools\/repo-cli\/src\/validate-aws-opentofu\.mjs"/u, ); + assert.match( + read('infrastructure/aws/README.md'), + /tofu init -backend=false -lockfile=readonly/u, + ); }); test('AWS foundation has reusable modules and safe alpha composition', () => { From 1790f0d77de599d78c8651204ef9ce3aef77ee77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 19:47:41 +0700 Subject: [PATCH 59/73] fix(api): align OpenAPI timestamp and integer schemas --- .../features/iae/api/artifact-retention.dto.ts | 15 ++++++++------- .../api/src/features/iae/api/inbox-item.dto.ts | 7 ++++++- .../src/features/sa/api/spreadsheet-audit.dto.ts | 9 +++++---- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/services/api/src/features/iae/api/artifact-retention.dto.ts b/services/api/src/features/iae/api/artifact-retention.dto.ts index 688a9142..4162e6a8 100644 --- a/services/api/src/features/iae/api/artifact-retention.dto.ts +++ b/services/api/src/features/iae/api/artifact-retention.dto.ts @@ -2,29 +2,30 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsBoolean, IsISO8601, IsInt, IsOptional, IsUUID, Matches, Min } from 'class-validator'; const strictUtcTimestamp = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u; +const strictUtcTimestampPattern = '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$'; export class RetentionEvaluationDto { - @ApiProperty({ format: 'date-time' }) + @ApiProperty({ format: 'date-time', pattern: strictUtcTimestampPattern }) @IsISO8601({ strict: true, strictSeparator: true }) @Matches(strictUtcTimestamp) evaluatedAt!: string; - @ApiProperty({ format: 'date-time' }) + @ApiProperty({ format: 'date-time', pattern: strictUtcTimestampPattern }) @IsISO8601({ strict: true, strictSeparator: true }) @Matches(strictUtcTimestamp) workspaceRetentionUntil!: string; - @ApiProperty({ format: 'date-time' }) + @ApiProperty({ format: 'date-time', pattern: strictUtcTimestampPattern }) @IsISO8601({ strict: true, strictSeparator: true }) @Matches(strictUtcTimestamp) resourceRetentionUntil!: string; - @ApiProperty({ format: 'date-time' }) + @ApiProperty({ format: 'date-time', pattern: strictUtcTimestampPattern }) @IsISO8601({ strict: true, strictSeparator: true }) @Matches(strictUtcTimestamp) auditRetentionUntil!: string; - @ApiProperty({ format: 'date-time' }) + @ApiProperty({ format: 'date-time', pattern: strictUtcTimestampPattern }) @IsISO8601({ strict: true, strictSeparator: true }) @Matches(strictUtcTimestamp) recoveryWindowUntil!: string; @@ -52,14 +53,14 @@ export class CreateArtifactDeletionRequestDto extends RetentionEvaluationDto { @IsUUID() requestedBy?: string; - @ApiProperty({ format: 'date-time' }) + @ApiProperty({ format: 'date-time', pattern: strictUtcTimestampPattern }) @IsISO8601({ strict: true, strictSeparator: true }) @Matches(strictUtcTimestamp) requestedAt!: string; } export class AuthorizeArtifactDeletionRequestDto extends RetentionEvaluationDto { - @ApiProperty({ format: 'date-time' }) + @ApiProperty({ format: 'date-time', pattern: strictUtcTimestampPattern }) @IsISO8601({ strict: true, strictSeparator: true }) @Matches(strictUtcTimestamp) approvedAt!: string; diff --git a/services/api/src/features/iae/api/inbox-item.dto.ts b/services/api/src/features/iae/api/inbox-item.dto.ts index 44d7713a..5706245e 100644 --- a/services/api/src/features/iae/api/inbox-item.dto.ts +++ b/services/api/src/features/iae/api/inbox-item.dto.ts @@ -17,6 +17,8 @@ import { import type { InboxPriorityV1 } from '@databreeze/domain/artifact-intake/v1'; +const strictUtcTimestampPattern = '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$'; + /** IAE-001: content-free, idempotent intake registration request. */ export class CreateInboxItemDto { @ApiProperty({ format: 'uuid' }) @@ -62,7 +64,10 @@ export class UpdateInboxMetadataDto { priority?: InboxPriorityV1; @ApiProperty({ - oneOf: [{ type: 'string', format: 'date-time' }, { type: 'null' }], + oneOf: [ + { type: 'string', format: 'date-time', pattern: strictUtcTimestampPattern }, + { type: 'null' }, + ], required: false, }) @IsOptional() diff --git a/services/api/src/features/sa/api/spreadsheet-audit.dto.ts b/services/api/src/features/sa/api/spreadsheet-audit.dto.ts index bae916fe..26c80228 100644 --- a/services/api/src/features/sa/api/spreadsheet-audit.dto.ts +++ b/services/api/src/features/sa/api/spreadsheet-audit.dto.ts @@ -18,6 +18,7 @@ import { import { ApiProperty } from '@nestjs/swagger'; const sha256Pattern = '^[0-9a-f]{64}$'; +const strictUtcTimestampPattern = '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$'; export class SpreadsheetAuditSheetDto { @ApiProperty({ format: 'uuid' }) @@ -29,19 +30,19 @@ export class SpreadsheetAuditSheetDto { @MaxLength(128) name!: string; - @ApiProperty({ minimum: 0, maximum: 1_048_576 }) + @ApiProperty({ type: 'integer', minimum: 0, maximum: 1_048_576 }) @IsInt() @Min(0) @Max(1_048_576) maxRow!: number; - @ApiProperty({ minimum: 0, maximum: 16_384 }) + @ApiProperty({ type: 'integer', minimum: 0, maximum: 16_384 }) @IsInt() @Min(0) @Max(16_384) maxColumn!: number; - @ApiProperty({ minimum: 0, maximum: 1_000_000 }) + @ApiProperty({ type: 'integer', minimum: 0, maximum: 1_000_000 }) @IsInt() @Min(0) @Max(1_000_000) @@ -117,7 +118,7 @@ export class CreateSpreadsheetAuditResultDto { @MaxLength(128) processorVersion!: string; - @ApiProperty({ format: 'date-time' }) + @ApiProperty({ format: 'date-time', pattern: strictUtcTimestampPattern }) @IsISO8601({ strict: true, strictSeparator: true }) @Matches(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u) createdAt!: string; From 8622586bfc2177a5102079f55b53e6e5d6c4b526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 19:48:19 +0700 Subject: [PATCH 60/73] chore(api): regenerate OpenAPI schema --- services/api/openapi/v1.json | 95 +++++++++++++++++++++++++++++------- 1 file changed, 78 insertions(+), 17 deletions(-) diff --git a/services/api/openapi/v1.json b/services/api/openapi/v1.json index da8befd7..6a3a1102 100644 --- a/services/api/openapi/v1.json +++ b/services/api/openapi/v1.json @@ -7478,7 +7478,16 @@ "assigneeId": { "oneOf": [{ "type": "string", "format": "uuid" }, { "type": "null" }] }, "labels": { "maxItems": 32, "type": "array", "items": { "type": "string" } }, "priority": { "type": "string", "enum": ["LOW", "NORMAL", "HIGH", "URGENT"] }, - "dueAt": { "oneOf": [{ "type": "string", "format": "date-time" }, { "type": "null" }] }, + "dueAt": { + "oneOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$" + }, + { "type": "null" } + ] + }, "expectedRevision": { "type": "number", "minimum": 1 } } }, @@ -7513,11 +7522,31 @@ "CreateArtifactDeletionRequestDto": { "type": "object", "properties": { - "evaluatedAt": { "type": "string", "format": "date-time" }, - "workspaceRetentionUntil": { "type": "string", "format": "date-time" }, - "resourceRetentionUntil": { "type": "string", "format": "date-time" }, - "auditRetentionUntil": { "type": "string", "format": "date-time" }, - "recoveryWindowUntil": { "type": "string", "format": "date-time" }, + "evaluatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$" + }, + "workspaceRetentionUntil": { + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$" + }, + "resourceRetentionUntil": { + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$" + }, + "auditRetentionUntil": { + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$" + }, + "recoveryWindowUntil": { + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$" + }, "activeApproval": { "type": "boolean" }, "legalHold": { "type": "boolean" }, "requestId": { "type": "string", "format": "uuid" }, @@ -7527,7 +7556,11 @@ "deprecated": true, "description": "Ignored. Attribution always uses the authenticated actor." }, - "requestedAt": { "type": "string", "format": "date-time" } + "requestedAt": { + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$" + } }, "required": [ "evaluatedAt", @@ -7544,14 +7577,38 @@ "AuthorizeArtifactDeletionRequestDto": { "type": "object", "properties": { - "evaluatedAt": { "type": "string", "format": "date-time" }, - "workspaceRetentionUntil": { "type": "string", "format": "date-time" }, - "resourceRetentionUntil": { "type": "string", "format": "date-time" }, - "auditRetentionUntil": { "type": "string", "format": "date-time" }, - "recoveryWindowUntil": { "type": "string", "format": "date-time" }, + "evaluatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$" + }, + "workspaceRetentionUntil": { + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$" + }, + "resourceRetentionUntil": { + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$" + }, + "auditRetentionUntil": { + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$" + }, + "recoveryWindowUntil": { + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$" + }, "activeApproval": { "type": "boolean" }, "legalHold": { "type": "boolean" }, - "approvedAt": { "type": "string", "format": "date-time" }, + "approvedAt": { + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$" + }, "mfaSatisfied": { "type": "boolean" }, "expectedRevision": { "type": "number", "minimum": 1 } }, @@ -8376,9 +8433,9 @@ "properties": { "sheetId": { "type": "string", "format": "uuid" }, "name": { "type": "string", "maxLength": 128 }, - "maxRow": { "type": "number", "minimum": 0, "maximum": 1048576 }, - "maxColumn": { "type": "number", "minimum": 0, "maximum": 16384 }, - "formulaCount": { "type": "number", "minimum": 0, "maximum": 1000000 } + "maxRow": { "type": "integer", "minimum": 0, "maximum": 1048576 }, + "maxColumn": { "type": "integer", "minimum": 0, "maximum": 16384 }, + "formulaCount": { "type": "integer", "minimum": 0, "maximum": 1000000 } }, "required": ["sheetId", "name", "maxRow", "maxColumn", "formulaCount"] }, @@ -8416,7 +8473,11 @@ "items": { "type": "string", "enum": ["MACRO", "EXTERNAL_LINK", "UNSUPPORTED_XML"] } }, "processorVersion": { "type": "string", "maxLength": 128 }, - "createdAt": { "type": "string", "format": "date-time" } + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$" + } }, "required": [ "auditId", From 4aecc999e0d6ee1c8ab07923cfd240807039c1ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 19:48:41 +0700 Subject: [PATCH 61/73] test(api): lock OpenAPI validator parity --- services/api/test/openapi.test.ts | 54 +++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/services/api/test/openapi.test.ts b/services/api/test/openapi.test.ts index abe78db0..7d7a719f 100644 --- a/services/api/test/openapi.test.ts +++ b/services/api/test/openapi.test.ts @@ -7,6 +7,8 @@ import { createApiApplication } from '../src/bootstrap.js'; import { CLIENT_VERSION_PATTERN_SOURCE } from '../src/features/system/api/client-compatibility.dto.js'; const httpMethods = ['delete', 'get', 'head', 'options', 'patch', 'post', 'put', 'trace'] as const; +const strictUtcTimestampPattern = + '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$'; interface ParameterLike { readonly in?: string; @@ -217,12 +219,52 @@ void test('generates deterministic versioned OpenAPI with safe headers, errors, const spreadsheetSheet = firstDocument.components?.schemas?.[ 'SpreadsheetAuditSheetDto' ] as Record; - assert.equal( - (spreadsheetSheet['properties'] as Record>)['maxRow']?.[ - 'maximum' - ], - 1_048_576, - ); + const spreadsheetSheetProperties = spreadsheetSheet['properties'] as Record< + string, + Record + >; + assert.equal(spreadsheetSheetProperties['maxRow']?.['type'], 'integer'); + assert.equal(spreadsheetSheetProperties['maxColumn']?.['type'], 'integer'); + assert.equal(spreadsheetSheetProperties['formulaCount']?.['type'], 'integer'); + assert.equal(spreadsheetSheetProperties['maxRow']?.['maximum'], 1_048_576); + + for (const schemaName of [ + 'CreateArtifactDeletionRequestDto', + 'AuthorizeArtifactDeletionRequestDto', + ] as const) { + const schema = firstDocument.components?.schemas?.[schemaName] as Record; + const properties = schema['properties'] as Record>; + for (const propertyName of [ + 'evaluatedAt', + 'workspaceRetentionUntil', + 'resourceRetentionUntil', + 'auditRetentionUntil', + 'recoveryWindowUntil', + schemaName === 'CreateArtifactDeletionRequestDto' ? 'requestedAt' : 'approvedAt', + ]) { + assert.equal( + properties[propertyName]?.['pattern'], + strictUtcTimestampPattern, + `${schemaName}.${propertyName} must document the strict UTC timestamp`, + ); + } + } + const inboxProperties = ( + firstDocument.components?.schemas?.['UpdateInboxMetadataDto'] as Record + )['properties'] as Record>; + const dueAtStringSchema = (inboxProperties['dueAt']?.['oneOf'] as readonly Record< + string, + unknown + >[]).find((candidate) => candidate['type'] === 'string'); + assert.equal(dueAtStringSchema?.['pattern'], strictUtcTimestampPattern); + const auditResult = firstDocument.components?.schemas?.[ + 'CreateSpreadsheetAuditResultDto' + ] as Record; + const auditResultProperties = auditResult['properties'] as Record< + string, + Record + >; + assert.equal(auditResultProperties['createdAt']?.['pattern'], strictUtcTimestampPattern); for (const operation of operations(firstDocument)) { const headerNames = (operation.parameters ?? []) From dff8f01bc77e15316138a61852b2a632caae2ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 19:49:03 +0700 Subject: [PATCH 62/73] fix(iae): map duplicate lineage conflicts --- ...risma-artifact-lineage-repository.adapter.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/services/api/src/features/iae/adapter/prisma-artifact-lineage-repository.adapter.ts b/services/api/src/features/iae/adapter/prisma-artifact-lineage-repository.adapter.ts index 1338e4d7..c03f8e15 100644 --- a/services/api/src/features/iae/adapter/prisma-artifact-lineage-repository.adapter.ts +++ b/services/api/src/features/iae/adapter/prisma-artifact-lineage-repository.adapter.ts @@ -96,6 +96,15 @@ function visible(context: TenantScopeV1, row: ArtifactLineageDatabaseRowV1): boo return tenantScopeContainsV1(context, candidate) || tenantScopeContainsV1(candidate, context); } +function isUniqueConstraintViolation(error: unknown): boolean { + return ( + typeof error === 'object' && + error !== null && + 'code' in error && + (error as { readonly code?: unknown }).code === 'P2002' + ); +} + class PrismaArtifactLineageTransactionAdapter implements ArtifactLineageTransactionPortV1 { public constructor(private readonly client: ArtifactLineageDatabaseClientV1) {} @@ -110,7 +119,13 @@ class PrismaArtifactLineageTransactionAdapter implements ArtifactLineageTransact throw new Error('IAE_IMMUTABLE_LINEAGE'); return; } - await this.client.artifactLineageRecord.create({ data: domainToCreate(lineage) }); + try { + await this.client.artifactLineageRecord.create({ data: domainToCreate(lineage) }); + } catch (error) { + if (isUniqueConstraintViolation(error)) + throw new Error('IAE_DERIVED_LINEAGE_CONFLICT', { cause: error }); + throw error; + } } public async findByDerived( From 2ded1f71e7be93f637d05bdc883d7fad26e34717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 19:49:15 +0700 Subject: [PATCH 63/73] test(iae): cover repository lineage conflict mapping --- .../iae/prisma-artifact-lineage-repository.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/services/api/test/features/iae/prisma-artifact-lineage-repository.test.ts b/services/api/test/features/iae/prisma-artifact-lineage-repository.test.ts index 53dc9c68..a86148f4 100644 --- a/services/api/test/features/iae/prisma-artifact-lineage-repository.test.ts +++ b/services/api/test/features/iae/prisma-artifact-lineage-repository.test.ts @@ -101,6 +101,16 @@ void test('IAE-007 lineage test storage enforces one record per derived artifact const persisted = rows[0]; if (!persisted) throw new Error('fixture lineage was not persisted'); + const conflictingLineageResult = createArtifactLineageV1({ + ...lineage, + lineageId: '88888888-8888-4888-8888-888888888888', + }); + if (!conflictingLineageResult.accepted) throw new Error('fixture conflict lineage invalid'); + await assert.rejects( + repository.save(context, conflictingLineageResult.value), + /IAE_DERIVED_LINEAGE_CONFLICT/u, + ); + await assert.rejects( database.artifactLineageRecord.create({ data: { ...persisted, id: '88888888-8888-4888-8888-888888888888' }, From c3c34f717f7682b70e8333da46aac6ee9de393ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 19:49:31 +0700 Subject: [PATCH 64/73] docs(ops): record PR 37 review disposition --- .../coderabbit-pr-37-disposition.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 docs/operations/coderabbit-pr-37-disposition.md diff --git a/docs/operations/coderabbit-pr-37-disposition.md b/docs/operations/coderabbit-pr-37-disposition.md new file mode 100644 index 00000000..43fd8f6c --- /dev/null +++ b/docs/operations/coderabbit-pr-37-disposition.md @@ -0,0 +1,22 @@ +# CodeRabbit PR #37 disposition + +Review run: `d034f311-c043-4139-8372-ed69b774a83f` +Review policy: one automatic review run for this promotion PR; no rerun. + +All seven inline findings were valid and are addressed below. The review also +included a general walkthrough/docstring coverage warning; it was not adopted +because the repository has no accepted coverage threshold for that warning and +the promotion gate is the executable repository check plus focused tests. + +| Comment | Disposition | Fix commit | Evidence | +| --- | --- | --- | --- | +| OpenTofu README init should use `-lockfile=readonly` | Accepted | `7be00e3` | `infrastructure/aws/README.md` documents the locked native initialization command. | +| OpenAPI timestamp and integer schemas did not match runtime validation | Accepted | `1790f0d`, `8622586`, `4aecc99` | DTO patterns/types, regenerated `services/api/openapi/v1.json`, and parity assertions in `services/api/test/openapi.test.ts`. | +| Duplicate derived lineage P2002 escaped the repository port | Accepted | `dff8f01`, `2ded1f7` | P2002 maps to `IAE_DERIVED_LINEAGE_CONFLICT`; repository-save race test covers the path. | +| OpenTofu version checks allowed leading-zero components | Accepted | `2ff9018`, `1ff27ff` | Both validators use strict SemVer components; acceptance/rejection cases are tested. | +| Validation help/success wording understated the mocked plan test | Accepted | `7be00e3`, `238f619` | Help and source assertions describe mocked plan testing and no apply. | +| OpenTofu source mount was writable | Accepted | `7be00e3`, `238f619` | The container source bind is explicitly `readonly`; the test asserts the mount. | +| Infrastructure test did not verify the read-only mount | Accepted | `238f619` | Test asserts the mount, README lockfile option, and safety wording. | + +No review comment authorized applying infrastructure or changing provider +boundaries; those remain outside this promotion slice. From 2201a3f735f96d79a1590e36da2f4fea9a2bc339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 19:51:23 +0700 Subject: [PATCH 65/73] fix(test): accept wrapped OpenTofu help text --- tools/repo-cli/test/aws-infrastructure.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index 9d2c6ea9..8f1158c9 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -36,7 +36,7 @@ test('AWS container validation command is pinned, isolated, and non-applying', ( }); assert.equal(help.status, 0, help.stderr); assert.match(help.stdout, /official pinned OpenTofu container/u); - assert.match(help.stdout, /mocked plan test/u); + assert.match(help.stdout, /mocked plan\s+test/u); assert.match(help.stdout, /does not\s+apply infrastructure/u); const source = read('tools/repo-cli/src/validate-aws-opentofu.mjs'); assert.match(source, /'fmt',\s*'-check',\s*'-recursive'/u); From c2d0bf0f9e1226476472b98d62c7aea037608845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 19:53:10 +0700 Subject: [PATCH 66/73] style: format review fixes --- services/api/test/openapi.test.ts | 10 ++++------ tools/repo-cli/test/aws-infrastructure.test.mjs | 3 +-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/services/api/test/openapi.test.ts b/services/api/test/openapi.test.ts index 7d7a719f..eb4cb9e1 100644 --- a/services/api/test/openapi.test.ts +++ b/services/api/test/openapi.test.ts @@ -7,8 +7,7 @@ import { createApiApplication } from '../src/bootstrap.js'; import { CLIENT_VERSION_PATTERN_SOURCE } from '../src/features/system/api/client-compatibility.dto.js'; const httpMethods = ['delete', 'get', 'head', 'options', 'patch', 'post', 'put', 'trace'] as const; -const strictUtcTimestampPattern = - '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$'; +const strictUtcTimestampPattern = '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$'; interface ParameterLike { readonly in?: string; @@ -252,10 +251,9 @@ void test('generates deterministic versioned OpenAPI with safe headers, errors, const inboxProperties = ( firstDocument.components?.schemas?.['UpdateInboxMetadataDto'] as Record )['properties'] as Record>; - const dueAtStringSchema = (inboxProperties['dueAt']?.['oneOf'] as readonly Record< - string, - unknown - >[]).find((candidate) => candidate['type'] === 'string'); + const dueAtStringSchema = ( + inboxProperties['dueAt']?.['oneOf'] as readonly Record[] + ).find((candidate) => candidate['type'] === 'string'); assert.equal(dueAtStringSchema?.['pattern'], strictUtcTimestampPattern); const auditResult = firstDocument.components?.schemas?.[ 'CreateSpreadsheetAuditResultDto' diff --git a/tools/repo-cli/test/aws-infrastructure.test.mjs b/tools/repo-cli/test/aws-infrastructure.test.mjs index 8f1158c9..1f8690cf 100644 --- a/tools/repo-cli/test/aws-infrastructure.test.mjs +++ b/tools/repo-cli/test/aws-infrastructure.test.mjs @@ -22,8 +22,7 @@ test('AWS validators accept strict semantic versions without leading zero compon for (const version of ['01.2.3', '1.02.3', '1.2.03', '1.2', 'v1.2.3']) assert.equal(strictSemanticVersion.test(version), false, version); - const expectedLiteral = - '/^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$/u'; + const expectedLiteral = '/^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$/u'; assert.ok(read('tools/repo-cli/src/check-aws-infrastructure.mjs').includes(expectedLiteral)); assert.ok(read('tools/repo-cli/src/validate-aws-opentofu.mjs').includes(expectedLiteral)); }); From 3d270124293dd80d1d510e7df20a0b59b2dc69d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 20:02:14 +0700 Subject: [PATCH 67/73] fix(dsm): reject null quality values --- .../features/dsm/api/dataset-quality.dto.ts | 3 +- .../dsm/dataset-quality.controller.test.ts | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/services/api/src/features/dsm/api/dataset-quality.dto.ts b/services/api/src/features/dsm/api/dataset-quality.dto.ts index 3d6032b3..0f225b8e 100644 --- a/services/api/src/features/dsm/api/dataset-quality.dto.ts +++ b/services/api/src/features/dsm/api/dataset-quality.dto.ts @@ -13,6 +13,7 @@ import { MaxLength, Min, MinLength, + ValidateIf, ValidateNested, Validate, ValidatorConstraint, @@ -67,7 +68,7 @@ export class DatasetQualitySafeValueDto { required: false, oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], }) - @IsOptional() + @ValidateIf((_object, value) => value !== undefined) @Validate(DatasetQualityScalarConstraint) value?: string | number | boolean; } diff --git a/services/api/test/features/dsm/dataset-quality.controller.test.ts b/services/api/test/features/dsm/dataset-quality.controller.test.ts index 5225478b..f0a279d7 100644 --- a/services/api/test/features/dsm/dataset-quality.controller.test.ts +++ b/services/api/test/features/dsm/dataset-quality.controller.test.ts @@ -140,6 +140,35 @@ void test('[DSM-013] quality DTO rejects unsupported source-bearing fields and m }, }); assert.equal(nestedValue.statusCode, 400); + + const nullValue = await app.inject({ + method: 'POST', + url: '/v1/dataset-quality-results', + payload: { + resultId, + datasetId: '00000000-0000-4000-8000-000000000927', + datasetVersionId, + ruleSetVersionId: '00000000-0000-4000-8000-000000000928', + profileFingerprint: 'a'.repeat(64), + rowCountScanned: 1, + qualityState: 'BLOCKED', + findings: [ + { + findingId: '00000000-0000-4000-8000-000000000929', + ruleId: '00000000-0000-4000-8000-000000000930', + severity: 'ERROR', + messageCode: 'INVALID_VALUE', + occurrenceCount: 1, + evidenceIds: [], + detailHash: 'b'.repeat(64), + actual: { kind: 'TEXT', value: null }, + }, + ], + resultFingerprint: 'c'.repeat(64), + createdAt: '2026-01-01T00:00:00.000Z', + }, + }); + assert.equal(nullValue.statusCode, 400); } finally { await app.close(); } From ffcaffbc5c19621c9b656203d5d17d6f34911d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 20:02:40 +0700 Subject: [PATCH 68/73] fix(dsm): guard tenant visibility before persisted decoding --- .../prisma-dataset-profile-repository.adapter.ts | 2 ++ .../prisma-dataset-quality-repository.adapter.ts | 2 ++ .../prisma-dataset-version-repository.adapter.ts | 2 ++ .../dsm/prisma-dataset-profile-repository.test.ts | 15 +++++++++++++++ .../dsm/prisma-dataset-quality-repository.test.ts | 15 +++++++++++++++ .../dsm/prisma-dataset-version-repository.test.ts | 15 +++++++++++++++ 6 files changed, 51 insertions(+) diff --git a/services/api/src/features/dsm/adapter/prisma-dataset-profile-repository.adapter.ts b/services/api/src/features/dsm/adapter/prisma-dataset-profile-repository.adapter.ts index 1c8bce64..674c07b0 100644 --- a/services/api/src/features/dsm/adapter/prisma-dataset-profile-repository.adapter.ts +++ b/services/api/src/features/dsm/adapter/prisma-dataset-profile-repository.adapter.ts @@ -152,6 +152,8 @@ class PrismaDatasetProfileTransactionAdapter implements DatasetProfileTransactio where: { id: profile.profileId }, }); if (existing !== null) { + if (!visible(context.tenantScope, existing)) + throw new Error('DSM_IMMUTABLE_DATASET_PROFILE'); if (JSON.stringify(rowToDomain(existing)) !== JSON.stringify(profile)) throw new Error('DSM_IMMUTABLE_DATASET_PROFILE'); return; diff --git a/services/api/src/features/dsm/adapter/prisma-dataset-quality-repository.adapter.ts b/services/api/src/features/dsm/adapter/prisma-dataset-quality-repository.adapter.ts index a7a63425..953e9f70 100644 --- a/services/api/src/features/dsm/adapter/prisma-dataset-quality-repository.adapter.ts +++ b/services/api/src/features/dsm/adapter/prisma-dataset-quality-repository.adapter.ts @@ -126,6 +126,8 @@ class PrismaDatasetQualityTransactionAdapter implements DatasetQualityTransactio where: { id: result.resultId }, }); if (existing !== null) { + if (!visible(context.tenantScope, existing)) + throw new Error('DSM_IMMUTABLE_QUALITY_RESULT'); if (JSON.stringify(rowToDomain(existing)) !== JSON.stringify(result)) throw new Error('DSM_IMMUTABLE_QUALITY_RESULT'); return; diff --git a/services/api/src/features/dsm/adapter/prisma-dataset-version-repository.adapter.ts b/services/api/src/features/dsm/adapter/prisma-dataset-version-repository.adapter.ts index e9c3bed1..f39fa9da 100644 --- a/services/api/src/features/dsm/adapter/prisma-dataset-version-repository.adapter.ts +++ b/services/api/src/features/dsm/adapter/prisma-dataset-version-repository.adapter.ts @@ -130,6 +130,8 @@ class PrismaDatasetVersionTransactionAdapter implements DatasetVersionTransactio where: { id: version.versionId }, }); if (existing !== null) { + if (!visible(context.tenantScope, existing)) + throw new Error('DSM_IMMUTABLE_DATASET_VERSION'); if (JSON.stringify(rowToDomain(existing)) !== JSON.stringify(version)) throw new Error('DSM_IMMUTABLE_DATASET_VERSION'); return; diff --git a/services/api/test/features/dsm/prisma-dataset-profile-repository.test.ts b/services/api/test/features/dsm/prisma-dataset-profile-repository.test.ts index 45ae8ad8..7c4fb87e 100644 --- a/services/api/test/features/dsm/prisma-dataset-profile-repository.test.ts +++ b/services/api/test/features/dsm/prisma-dataset-profile-repository.test.ts @@ -98,6 +98,21 @@ void test('[DSM-011, IAM-009] Prisma profile adapter persists immutable disclosu created.value, ]); assert.equal(rows.length, 1); + const persisted = rows[0]; + if (!persisted) throw new Error('fixture profile was not persisted'); + await assert.rejects( + new PrismaDatasetProfileRepositoryAdapter( + client([ + { + ...persisted, + organizationId: '00000000-0000-4000-8000-000000000771', + workspaceId: '00000000-0000-4000-8000-000000000772', + rowCountScanned: -1, + }, + ]), + ).save(tenantContext, created.value), + /DSM_IMMUTABLE_DATASET_PROFILE/u, + ); await assert.rejects( new PrismaDatasetProfileRepositoryAdapter(client([], true)).save(tenantContext, created.value), /DSM_IMMUTABLE_DATASET_PROFILE/u, diff --git a/services/api/test/features/dsm/prisma-dataset-quality-repository.test.ts b/services/api/test/features/dsm/prisma-dataset-quality-repository.test.ts index b53840ee..fc764b43 100644 --- a/services/api/test/features/dsm/prisma-dataset-quality-repository.test.ts +++ b/services/api/test/features/dsm/prisma-dataset-quality-repository.test.ts @@ -97,6 +97,21 @@ void test('[DSM-011, DSM-013, IAM-009] Prisma quality adapter persists immutable created.value, ]); assert.equal(rows.length, 1); + const persisted = rows[0]; + if (!persisted) throw new Error('fixture quality result was not persisted'); + await assert.rejects( + new PrismaDatasetQualityRepositoryAdapter( + client([ + { + ...persisted, + organizationId: '00000000-0000-4000-8000-000000000911', + workspaceId: '00000000-0000-4000-8000-000000000912', + rowCountScanned: -1, + }, + ]), + ).save(tenantContext, created.value), + /DSM_IMMUTABLE_QUALITY_RESULT/u, + ); await assert.rejects( new PrismaDatasetQualityRepositoryAdapter(client([], true)).save(tenantContext, created.value), /DSM_IMMUTABLE_QUALITY_RESULT/u, diff --git a/services/api/test/features/dsm/prisma-dataset-version-repository.test.ts b/services/api/test/features/dsm/prisma-dataset-version-repository.test.ts index e76b5da4..3aaa7efc 100644 --- a/services/api/test/features/dsm/prisma-dataset-version-repository.test.ts +++ b/services/api/test/features/dsm/prisma-dataset-version-repository.test.ts @@ -96,6 +96,21 @@ void test('[DSM-002, DSM-003, IAM-009] Prisma dataset version adapter is immutab assert.deepEqual(await repository.find(tenantContext, versionId), created.value); assert.deepEqual(await repository.list(tenantContext, created.value.datasetId), [created.value]); assert.equal(rows.length, 1); + const persisted = rows[0]; + if (!persisted) throw new Error('fixture version was not persisted'); + await assert.rejects( + new PrismaDatasetVersionRepositoryAdapter( + client([ + { + ...persisted, + organizationId: '00000000-0000-4000-8000-000000000821', + workspaceId: '00000000-0000-4000-8000-000000000822', + rowCount: -1, + }, + ]), + ).save(tenantContext, created.value), + /DSM_IMMUTABLE_DATASET_VERSION/u, + ); await assert.rejects( new PrismaDatasetVersionRepositoryAdapter(client([], true)).save(tenantContext, created.value), /DSM_IMMUTABLE_DATASET_VERSION/u, From 63240725bdfeb28674f187b89ac1d2f23891c8c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 20:02:52 +0700 Subject: [PATCH 69/73] test(android): assert telemetry cause suppression --- .../test/java/com/databreeze/android/TelemetryContractTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/android/app/src/test/java/com/databreeze/android/TelemetryContractTest.kt b/apps/android/app/src/test/java/com/databreeze/android/TelemetryContractTest.kt index f3b9cfa5..d948ca2e 100644 --- a/apps/android/app/src/test/java/com/databreeze/android/TelemetryContractTest.kt +++ b/apps/android/app/src/test/java/com/databreeze/android/TelemetryContractTest.kt @@ -86,6 +86,7 @@ class TelemetryContractTest { TelemetryContract.assertSafeAttributes(hostileAttributes) } assertEquals("telemetry attributes are not readable", attributeError.message) + assertTrue(attributeError.cause == null) val hostileHeaders = object : Map> by emptyMap() { override val entries: Set>> @@ -96,5 +97,6 @@ class TelemetryContractTest { } assertTrue(headerError.message.orEmpty().contains("not readable")) assertTrue(!headerError.message.orEmpty().contains("provider header cause")) + assertTrue(headerError.cause == null) } } From 7255556902d3e7d3d82a45367eb516d1a16810b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 20:03:16 +0700 Subject: [PATCH 70/73] refactor(api): centralize Prisma unique constraint checks --- .../prisma-dataset-profile-repository.adapter.ts | 2 +- .../prisma-dataset-quality-repository.adapter.ts | 2 +- .../prisma-dataset-version-repository.adapter.ts | 2 +- .../prisma-artifact-export-repository.adapter.ts | 13 +++---------- .../prisma-artifact-lineage-repository.adapter.ts | 12 ++---------- .../dsm/adapter => platform}/prisma-error.ts | 1 + 6 files changed, 9 insertions(+), 23 deletions(-) rename services/api/src/{features/dsm/adapter => platform}/prisma-error.ts (76%) diff --git a/services/api/src/features/dsm/adapter/prisma-dataset-profile-repository.adapter.ts b/services/api/src/features/dsm/adapter/prisma-dataset-profile-repository.adapter.ts index 674c07b0..883d5d35 100644 --- a/services/api/src/features/dsm/adapter/prisma-dataset-profile-repository.adapter.ts +++ b/services/api/src/features/dsm/adapter/prisma-dataset-profile-repository.adapter.ts @@ -13,7 +13,7 @@ import type { DatasetProfileRepositoryPortV1, DatasetProfileTransactionPortV1, } from '../application/dataset-profile-repository.port.js'; -import { isPrismaUniqueConstraintViolationV1 } from './prisma-error.js'; +import { isPrismaUniqueConstraintViolationV1 } from '../../../platform/prisma-error.js'; export interface DatasetProfileDatabaseRowV1 { readonly id: string; diff --git a/services/api/src/features/dsm/adapter/prisma-dataset-quality-repository.adapter.ts b/services/api/src/features/dsm/adapter/prisma-dataset-quality-repository.adapter.ts index 953e9f70..9e1d3fc6 100644 --- a/services/api/src/features/dsm/adapter/prisma-dataset-quality-repository.adapter.ts +++ b/services/api/src/features/dsm/adapter/prisma-dataset-quality-repository.adapter.ts @@ -13,7 +13,7 @@ import type { DatasetQualityRepositoryPortV1, DatasetQualityTransactionPortV1, } from '../application/dataset-quality-repository.port.js'; -import { isPrismaUniqueConstraintViolationV1 } from './prisma-error.js'; +import { isPrismaUniqueConstraintViolationV1 } from '../../../platform/prisma-error.js'; export interface DatasetQualityDatabaseRowV1 { readonly id: string; diff --git a/services/api/src/features/dsm/adapter/prisma-dataset-version-repository.adapter.ts b/services/api/src/features/dsm/adapter/prisma-dataset-version-repository.adapter.ts index f39fa9da..52d9a763 100644 --- a/services/api/src/features/dsm/adapter/prisma-dataset-version-repository.adapter.ts +++ b/services/api/src/features/dsm/adapter/prisma-dataset-version-repository.adapter.ts @@ -13,7 +13,7 @@ import type { DatasetVersionRepositoryPortV1, DatasetVersionTransactionPortV1, } from '../application/dataset-version-repository.port.js'; -import { isPrismaUniqueConstraintViolationV1 } from './prisma-error.js'; +import { isPrismaUniqueConstraintViolationV1 } from '../../../platform/prisma-error.js'; export interface DatasetVersionDatabaseRowV1 { readonly id: string; diff --git a/services/api/src/features/iae/adapter/prisma-artifact-export-repository.adapter.ts b/services/api/src/features/iae/adapter/prisma-artifact-export-repository.adapter.ts index 91c18626..324647cb 100644 --- a/services/api/src/features/iae/adapter/prisma-artifact-export-repository.adapter.ts +++ b/services/api/src/features/iae/adapter/prisma-artifact-export-repository.adapter.ts @@ -14,6 +14,7 @@ import type { ArtifactExportRepositoryPortV1, ArtifactExportTransactionPortV1, } from '../application/artifact-export-repository.port.js'; +import { isPrismaUniqueConstraintViolationV1 } from '../../../platform/prisma-error.js'; export interface ArtifactExportDatabaseRowV1 { readonly id: string; @@ -93,15 +94,6 @@ function visible(context: TenantScopeV1, row: ArtifactExportDatabaseRowV1): bool return tenantScopeContainsV1(context, candidate) || tenantScopeContainsV1(candidate, context); } -function isUniqueConstraintViolation(error: unknown): boolean { - return ( - typeof error === 'object' && - error !== null && - 'code' in error && - (error as { readonly code?: unknown }).code === 'P2002' - ); -} - class PrismaArtifactExportTransactionAdapter implements ArtifactExportTransactionPortV1 { public constructor(private readonly client: ArtifactExportDatabaseClientV1) {} @@ -124,7 +116,8 @@ class PrismaArtifactExportTransactionAdapter implements ArtifactExportTransactio try { await this.client.artifactExportManifestRecord.create({ data: domainToCreate(manifest) }); } catch (error) { - if (isUniqueConstraintViolation(error)) throw new Error('IAE_IMMUTABLE_EXPORT_MANIFEST'); + if (isPrismaUniqueConstraintViolationV1(error)) + throw new Error('IAE_IMMUTABLE_EXPORT_MANIFEST'); throw error; } } diff --git a/services/api/src/features/iae/adapter/prisma-artifact-lineage-repository.adapter.ts b/services/api/src/features/iae/adapter/prisma-artifact-lineage-repository.adapter.ts index c03f8e15..b8b1d405 100644 --- a/services/api/src/features/iae/adapter/prisma-artifact-lineage-repository.adapter.ts +++ b/services/api/src/features/iae/adapter/prisma-artifact-lineage-repository.adapter.ts @@ -13,6 +13,7 @@ import type { ArtifactLineageRepositoryPortV1, ArtifactLineageTransactionPortV1, } from '../application/artifact-lineage-repository.port.js'; +import { isPrismaUniqueConstraintViolationV1 } from '../../../platform/prisma-error.js'; export interface ArtifactLineageDatabaseRowV1 { readonly id: string; @@ -96,15 +97,6 @@ function visible(context: TenantScopeV1, row: ArtifactLineageDatabaseRowV1): boo return tenantScopeContainsV1(context, candidate) || tenantScopeContainsV1(candidate, context); } -function isUniqueConstraintViolation(error: unknown): boolean { - return ( - typeof error === 'object' && - error !== null && - 'code' in error && - (error as { readonly code?: unknown }).code === 'P2002' - ); -} - class PrismaArtifactLineageTransactionAdapter implements ArtifactLineageTransactionPortV1 { public constructor(private readonly client: ArtifactLineageDatabaseClientV1) {} @@ -122,7 +114,7 @@ class PrismaArtifactLineageTransactionAdapter implements ArtifactLineageTransact try { await this.client.artifactLineageRecord.create({ data: domainToCreate(lineage) }); } catch (error) { - if (isUniqueConstraintViolation(error)) + if (isPrismaUniqueConstraintViolationV1(error)) throw new Error('IAE_DERIVED_LINEAGE_CONFLICT', { cause: error }); throw error; } diff --git a/services/api/src/features/dsm/adapter/prisma-error.ts b/services/api/src/platform/prisma-error.ts similarity index 76% rename from services/api/src/features/dsm/adapter/prisma-error.ts rename to services/api/src/platform/prisma-error.ts index 61e6c184..ffe305db 100644 --- a/services/api/src/features/dsm/adapter/prisma-error.ts +++ b/services/api/src/platform/prisma-error.ts @@ -1,3 +1,4 @@ +/** Returns true for Prisma's bounded unique-constraint error shape. */ export function isPrismaUniqueConstraintViolationV1(error: unknown): boolean { return ( typeof error === 'object' && From 7da3e7719ff58493e233fb6a6fe925859939f469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 20:05:41 +0700 Subject: [PATCH 71/73] fix(iae): revalidate upload transfers after issuance --- ...isma-dataset-profile-repository.adapter.ts | 3 +- ...isma-dataset-quality-repository.adapter.ts | 3 +- ...isma-dataset-version-repository.adapter.ts | 3 +- .../application/artifact-upload.service.ts | 28 +++++++-- .../iae/artifact-upload.service.test.ts | 63 +++++++++++++++++++ 5 files changed, 89 insertions(+), 11 deletions(-) diff --git a/services/api/src/features/dsm/adapter/prisma-dataset-profile-repository.adapter.ts b/services/api/src/features/dsm/adapter/prisma-dataset-profile-repository.adapter.ts index 883d5d35..69bb22b0 100644 --- a/services/api/src/features/dsm/adapter/prisma-dataset-profile-repository.adapter.ts +++ b/services/api/src/features/dsm/adapter/prisma-dataset-profile-repository.adapter.ts @@ -152,8 +152,7 @@ class PrismaDatasetProfileTransactionAdapter implements DatasetProfileTransactio where: { id: profile.profileId }, }); if (existing !== null) { - if (!visible(context.tenantScope, existing)) - throw new Error('DSM_IMMUTABLE_DATASET_PROFILE'); + if (!visible(context.tenantScope, existing)) throw new Error('DSM_IMMUTABLE_DATASET_PROFILE'); if (JSON.stringify(rowToDomain(existing)) !== JSON.stringify(profile)) throw new Error('DSM_IMMUTABLE_DATASET_PROFILE'); return; diff --git a/services/api/src/features/dsm/adapter/prisma-dataset-quality-repository.adapter.ts b/services/api/src/features/dsm/adapter/prisma-dataset-quality-repository.adapter.ts index 9e1d3fc6..712142c0 100644 --- a/services/api/src/features/dsm/adapter/prisma-dataset-quality-repository.adapter.ts +++ b/services/api/src/features/dsm/adapter/prisma-dataset-quality-repository.adapter.ts @@ -126,8 +126,7 @@ class PrismaDatasetQualityTransactionAdapter implements DatasetQualityTransactio where: { id: result.resultId }, }); if (existing !== null) { - if (!visible(context.tenantScope, existing)) - throw new Error('DSM_IMMUTABLE_QUALITY_RESULT'); + if (!visible(context.tenantScope, existing)) throw new Error('DSM_IMMUTABLE_QUALITY_RESULT'); if (JSON.stringify(rowToDomain(existing)) !== JSON.stringify(result)) throw new Error('DSM_IMMUTABLE_QUALITY_RESULT'); return; diff --git a/services/api/src/features/dsm/adapter/prisma-dataset-version-repository.adapter.ts b/services/api/src/features/dsm/adapter/prisma-dataset-version-repository.adapter.ts index 52d9a763..3f879888 100644 --- a/services/api/src/features/dsm/adapter/prisma-dataset-version-repository.adapter.ts +++ b/services/api/src/features/dsm/adapter/prisma-dataset-version-repository.adapter.ts @@ -130,8 +130,7 @@ class PrismaDatasetVersionTransactionAdapter implements DatasetVersionTransactio where: { id: version.versionId }, }); if (existing !== null) { - if (!visible(context.tenantScope, existing)) - throw new Error('DSM_IMMUTABLE_DATASET_VERSION'); + if (!visible(context.tenantScope, existing)) throw new Error('DSM_IMMUTABLE_DATASET_VERSION'); if (JSON.stringify(rowToDomain(existing)) !== JSON.stringify(version)) throw new Error('DSM_IMMUTABLE_DATASET_VERSION'); return; diff --git a/services/api/src/features/iae/application/artifact-upload.service.ts b/services/api/src/features/iae/application/artifact-upload.service.ts index 821c91e9..a3603c3d 100644 --- a/services/api/src/features/iae/application/artifact-upload.service.ts +++ b/services/api/src/features/iae/application/artifact-upload.service.ts @@ -130,11 +130,29 @@ export class ArtifactUploadService { sessionId: ArtifactUploadSessionV1['sessionId'], partNumber: number, ): Promise> { - const session = await this.repository.find(context, sessionId); - if (!session) return Object.freeze({ accepted: false, code: 'UPLOAD_NOT_FOUND' as const }); - if (session.state === 'EXPIRED') - return Object.freeze({ accepted: false, code: 'UPLOAD_SESSION_EXPIRED' as const }); - return this.storage.issuePartTransfer(context, session, partNumber); + return this.repository.withTransaction(context, async (transaction) => { + const session = await transaction.find(context, sessionId); + if (!session) return Object.freeze({ accepted: false, code: 'UPLOAD_NOT_FOUND' as const }); + if (session.state === 'EXPIRED') + return Object.freeze({ accepted: false, code: 'UPLOAD_SESSION_EXPIRED' as const }); + const transfer = await this.storage.issuePartTransfer(context, session, partNumber); + if (!transfer.accepted) return transfer; + + const current = await transaction.find(context, sessionId); + if (!current) { + await this.storage.abort(context, session); + return Object.freeze({ accepted: false, code: 'UPLOAD_NOT_FOUND' as const }); + } + if (current.state === 'EXPIRED') { + await this.storage.abort(context, current); + return Object.freeze({ accepted: false, code: 'UPLOAD_SESSION_EXPIRED' as const }); + } + if (current.state !== 'OPEN' || current.revision !== session.revision) { + await this.storage.abort(context, current); + return Object.freeze({ accepted: false, code: 'REVISION_CONFLICT' as const }); + } + return transfer; + }); } private async mutate( diff --git a/services/api/test/features/iae/artifact-upload.service.test.ts b/services/api/test/features/iae/artifact-upload.service.test.ts index 8c85ddae..9b7dc9e1 100644 --- a/services/api/test/features/iae/artifact-upload.service.test.ts +++ b/services/api/test/features/iae/artifact-upload.service.test.ts @@ -1,7 +1,16 @@ import { strict as assert } from 'node:assert'; import test from 'node:test'; +import { + createArtifactUploadSessionV1, + expireArtifactUploadSessionV1, + type ArtifactUploadSessionV1, +} from '@databreeze/domain/artifact-upload/v1'; import { createIamTenantContextV1 } from '../../../src/features/iam/application/tenant-context.js'; +import type { + ArtifactUploadRepositoryPortV1, + ArtifactUploadTransactionPortV1, +} from '../../../src/features/iae/application/artifact-upload-repository.port.js'; import { ArtifactUploadService } from '../../../src/features/iae/application/artifact-upload.service.js'; import { InMemoryArtifactUploadRepositoryAdapter } from '../../../src/features/iae/adapter/in-memory-artifact-upload-repository.adapter.js'; import { InMemoryArtifactUploadStorageAdapter } from '../../../src/features/iae/adapter/in-memory-artifact-upload-storage.adapter.js'; @@ -17,6 +26,32 @@ class TrackingStorageAdapter extends InMemoryArtifactUploadStorageAdapter { } } +class RevalidatingUploadRepository implements ArtifactUploadRepositoryPortV1 { + private reads = 0; + + public constructor( + private readonly open: ArtifactUploadSessionV1, + private readonly expired: ArtifactUploadSessionV1, + ) {} + + public async save(): Promise {} + + public find(): Promise { + this.reads += 1; + return Promise.resolve(this.reads === 1 ? this.open : this.expired); + } + + public withTransaction( + _context: Parameters[0], + work: (transaction: ArtifactUploadTransactionPortV1) => Promise, + ): Promise { + return work({ + save: this.save.bind(this), + find: this.find.bind(this), + }); + } +} + const contextResult = createIamTenantContextV1({ actorId: '11111111-1111-4111-8111-111111111111', tenantScope: { @@ -99,3 +134,31 @@ void test('IAE-014 expiration revokes storage-side partial state before persisti const transfer = await service.issuePartTransfer(context, created.value.sessionId, 1); assert.deepEqual(transfer, { accepted: false, code: 'UPLOAD_SESSION_EXPIRED' }); }); + +void test('IAE-014 transfer issuance revalidates the session before returning a grant', async () => { + const created = createArtifactUploadSessionV1({ + sessionId: '99999999-9999-4999-8999-999999999991', + artifactId: '99999999-9999-4999-8999-999999999992', + tenantScope: context.tenantScope, + expectedSha256: 'a'.repeat(64), + expectedByteSize: 4, + mediaType: 'application/octet-stream', + partSize: 4, + createdAt: '2026-08-02T00:00:00.000Z', + expiresAt: '2026-08-02T01:00:00.000Z', + }); + assert.equal(created.accepted, true); + if (!created.accepted) return; + const expired = expireArtifactUploadSessionV1(created.value, created.value.expiresAt); + assert.equal(expired.accepted, true); + if (!expired.accepted) return; + + const service = new ArtifactUploadService( + new RevalidatingUploadRepository(created.value, expired.value), + new InMemoryArtifactUploadStorageAdapter(), + ); + assert.deepEqual(await service.issuePartTransfer(context, created.value.sessionId, 1), { + accepted: false, + code: 'UPLOAD_SESSION_EXPIRED', + }); +}); From 0d0c57f8d44b5ed294235a63f3436997d81d2f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 20:06:05 +0700 Subject: [PATCH 72/73] docs(ops): record PR 37 follow-up dispositions --- .../coderabbit-pr-37-disposition.md | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/operations/coderabbit-pr-37-disposition.md b/docs/operations/coderabbit-pr-37-disposition.md index 43fd8f6c..b67ef324 100644 --- a/docs/operations/coderabbit-pr-37-disposition.md +++ b/docs/operations/coderabbit-pr-37-disposition.md @@ -3,10 +3,13 @@ Review run: `d034f311-c043-4139-8372-ed69b774a83f` Review policy: one automatic review run for this promotion PR; no rerun. -All seven inline findings were valid and are addressed below. The review also -included a general walkthrough/docstring coverage warning; it was not adopted -because the repository has no accepted coverage threshold for that warning and -the promotion gate is the executable repository check plus focused tests. +All seven inline findings were valid and are addressed below. The same review +body also contained three outside-diff findings and two nitpicks. Those were +verified against the current code and addressed where they described an +observable correctness, isolation, or test gap. The general walkthrough/ +docstring coverage warning was not adopted because the repository has no +accepted coverage threshold for that warning and the promotion gate is the +executable repository check plus focused tests. | Comment | Disposition | Fix commit | Evidence | | --- | --- | --- | --- | @@ -18,5 +21,15 @@ the promotion gate is the executable repository check plus focused tests. | OpenTofu source mount was writable | Accepted | `7be00e3`, `238f619` | The container source bind is explicitly `readonly`; the test asserts the mount. | | Infrastructure test did not verify the read-only mount | Accepted | `238f619` | Test asserts the mount, README lockfile option, and safety wording. | +## Outside-diff and nitpick follow-ups + +| Finding | Disposition | Fix commit | Evidence | +| --- | --- | --- | --- | +| `DatasetQualitySafeValueDto.value` skipped validation for `null` | Accepted | `3d27012` | `ValidateIf` keeps `undefined` optional while rejecting `null`; the controller test covers the nested null payload. | +| DSM version/profile/quality saves decoded a sibling row before checking visibility | Accepted | `ffcaffb` | All three Prisma adapters check tenant visibility before `rowToDomain`; malformed sibling-row tests expect the stable immutable error. | +| Upload transfer issuance could use a stale session around expiry | Accepted | `7da3e77` | Issuance runs in the repository transaction, re-reads the session, aborts the storage grant on state/revision change, and has a simulated expiry race test. | +| Android telemetry tests did not assert exception causes were absent | Accepted | `6324072` | Tests assert both provider-backed exception causes are `null`. | +| Prisma P2002 predicates were duplicated across feature adapters | Accepted | `7255556` | The predicate now lives in `src/platform/prisma-error.ts`; DSM and IAE adapters share it. | + No review comment authorized applying infrastructure or changing provider boundaries; those remain outside this promotion slice. From 41af8fd1353b9fce76cda660c7f2f6883b1a261d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 20:14:13 +0700 Subject: [PATCH 73/73] fix(api): close final promotion review findings --- .../coderabbit-pr-37-disposition.md | 11 ++++ services/api/openapi/v1.json | 2 +- ...sma-artifact-lineage-repository.adapter.ts | 28 +++++++- .../iae/api/artifact-admission.dto.ts | 2 +- services/api/src/platform/prisma-error.ts | 12 ++++ ...prisma-artifact-lineage-repository.test.ts | 64 ++++++++++++++++++- services/api/test/openapi.test.ts | 5 ++ 7 files changed, 117 insertions(+), 7 deletions(-) diff --git a/docs/operations/coderabbit-pr-37-disposition.md b/docs/operations/coderabbit-pr-37-disposition.md index b67ef324..bd1b1dda 100644 --- a/docs/operations/coderabbit-pr-37-disposition.md +++ b/docs/operations/coderabbit-pr-37-disposition.md @@ -3,6 +3,10 @@ Review run: `d034f311-c043-4139-8372-ed69b774a83f` Review policy: one automatic review run for this promotion PR; no rerun. +After the fixes were pushed, the CodeRabbit app automatically queued a +follow-up status check (`6fd78011-f34d-414f-a9a8-2a696e772375`). No review was +manually invoked; its two additional findings were verified and included below. + All seven inline findings were valid and are addressed below. The same review body also contained three outside-diff findings and two nitpicks. Those were verified against the current code and addressed where they described an @@ -31,5 +35,12 @@ executable repository check plus focused tests. | Android telemetry tests did not assert exception causes were absent | Accepted | `6324072` | Tests assert both provider-backed exception causes are `null`. | | Prisma P2002 predicates were duplicated across feature adapters | Accepted | `7255556` | The predicate now lives in `src/platform/prisma-error.ts`; DSM and IAE adapters share it. | +## Automatic follow-up findings + +| Finding | Disposition | Fix commit | Evidence | +| --- | --- | --- | --- | +| Lineage P2002 handling treated every unique conflict as a derived-version conflict | Accepted | final review-fix commit | P2002 `meta.target` distinguishes `id` from `derivedArtifactVersionId`; same-ID races re-run immutable comparison and have a regression fixture. | +| `AdmitArtifactDto.maxByteSize` was documented as a number despite `@IsInt()` | Accepted | final review-fix commit | DTO metadata, regenerated OpenAPI, and assertions now document both byte-size fields as integers. | + No review comment authorized applying infrastructure or changing provider boundaries; those remain outside this promotion slice. diff --git a/services/api/openapi/v1.json b/services/api/openapi/v1.json index 6a3a1102..f790caa8 100644 --- a/services/api/openapi/v1.json +++ b/services/api/openapi/v1.json @@ -7710,7 +7710,7 @@ "actualByteSize": { "type": "integer", "minimum": 0 }, "detectedMediaType": { "type": "string" }, "scanState": { "type": "string", "enum": ["PENDING", "CLEAN", "MALICIOUS", "FAILED"] }, - "maxByteSize": { "type": "number", "minimum": 0 }, + "maxByteSize": { "type": "integer", "minimum": 0 }, "scannedAt": { "type": "string", "format": "date-time" } }, "required": [ diff --git a/services/api/src/features/iae/adapter/prisma-artifact-lineage-repository.adapter.ts b/services/api/src/features/iae/adapter/prisma-artifact-lineage-repository.adapter.ts index b8b1d405..10988589 100644 --- a/services/api/src/features/iae/adapter/prisma-artifact-lineage-repository.adapter.ts +++ b/services/api/src/features/iae/adapter/prisma-artifact-lineage-repository.adapter.ts @@ -13,7 +13,10 @@ import type { ArtifactLineageRepositoryPortV1, ArtifactLineageTransactionPortV1, } from '../application/artifact-lineage-repository.port.js'; -import { isPrismaUniqueConstraintViolationV1 } from '../../../platform/prisma-error.js'; +import { + isPrismaUniqueConstraintViolationV1, + prismaUniqueConstraintTargetV1, +} from '../../../platform/prisma-error.js'; export interface ArtifactLineageDatabaseRowV1 { readonly id: string; @@ -114,8 +117,27 @@ class PrismaArtifactLineageTransactionAdapter implements ArtifactLineageTransact try { await this.client.artifactLineageRecord.create({ data: domainToCreate(lineage) }); } catch (error) { - if (isPrismaUniqueConstraintViolationV1(error)) - throw new Error('IAE_DERIVED_LINEAGE_CONFLICT', { cause: error }); + if (isPrismaUniqueConstraintViolationV1(error)) { + const target = prismaUniqueConstraintTargetV1(error); + if (target?.includes('derivedArtifactVersionId')) + throw new Error('IAE_DERIVED_LINEAGE_CONFLICT', { cause: error }); + + const racedById = await this.client.artifactLineageRecord.findUnique({ + where: { id: lineage.lineageId }, + }); + if (racedById !== null) { + if (JSON.stringify(rowToDomain(racedById)) !== JSON.stringify(lineage)) + throw new Error('IAE_IMMUTABLE_LINEAGE', { cause: error }); + return; + } + if (target?.includes('id')) throw error; + + const racedByDerived = await this.client.artifactLineageRecord.findUnique({ + where: { derivedArtifactVersionId: lineage.derivedArtifactVersionId }, + }); + if (racedByDerived !== null) + throw new Error('IAE_DERIVED_LINEAGE_CONFLICT', { cause: error }); + } throw error; } } diff --git a/services/api/src/features/iae/api/artifact-admission.dto.ts b/services/api/src/features/iae/api/artifact-admission.dto.ts index 840a03fd..ea13b87a 100644 --- a/services/api/src/features/iae/api/artifact-admission.dto.ts +++ b/services/api/src/features/iae/api/artifact-admission.dto.ts @@ -19,7 +19,7 @@ export class AdmitArtifactDto { @IsIn(['PENDING', 'CLEAN', 'MALICIOUS', 'FAILED']) scanState!: 'PENDING' | 'CLEAN' | 'MALICIOUS' | 'FAILED'; - @ApiProperty({ minimum: 0 }) + @ApiProperty({ type: 'integer', minimum: 0 }) @IsInt() @Min(0) maxByteSize!: number; diff --git a/services/api/src/platform/prisma-error.ts b/services/api/src/platform/prisma-error.ts index ffe305db..be028f21 100644 --- a/services/api/src/platform/prisma-error.ts +++ b/services/api/src/platform/prisma-error.ts @@ -7,3 +7,15 @@ export function isPrismaUniqueConstraintViolationV1(error: unknown): boolean { (error as { readonly code?: unknown }).code === 'P2002' ); } + +export function prismaUniqueConstraintTargetV1(error: unknown): readonly string[] | undefined { + if (!isPrismaUniqueConstraintViolationV1(error)) return undefined; + const target = + typeof error === 'object' && error !== null && 'meta' in error + ? (error as { readonly meta?: { readonly target?: unknown } }).meta?.target + : undefined; + return Array.isArray(target) && + target.every((field): field is string => typeof field === 'string') + ? target + : undefined; +} diff --git a/services/api/test/features/iae/prisma-artifact-lineage-repository.test.ts b/services/api/test/features/iae/prisma-artifact-lineage-repository.test.ts index a86148f4..b4299cda 100644 --- a/services/api/test/features/iae/prisma-artifact-lineage-repository.test.ts +++ b/services/api/test/features/iae/prisma-artifact-lineage-repository.test.ts @@ -43,10 +43,21 @@ function client(rows: ArtifactLineageDatabaseRowV1[]): ArtifactLineageDatabaseCl candidate.id === data.id || candidate.derivedArtifactVersionId === data.derivedArtifactVersionId, ) - ) + ) { + const duplicate = rows.find( + (candidate) => + candidate.id === data.id || + candidate.derivedArtifactVersionId === data.derivedArtifactVersionId, + ); return Promise.reject( - Object.assign(new Error('fixture unique constraint'), { code: 'P2002' }), + Object.assign(new Error('fixture unique constraint'), { + code: 'P2002', + meta: { + target: duplicate?.id === data.id ? ['id'] : ['derivedArtifactVersionId'], + }, + }), ); + } rows.push({ ...data }); return Promise.resolve({ ...data }); }, @@ -93,6 +104,55 @@ void test('IAE-007 Prisma lineage adapter preserves immutable lineage and source assert.equal(rows.length, 1); }); +void test('IAE-007 same-id lineage races preserve immutable identity errors', async () => { + const persisted: ArtifactLineageDatabaseRowV1 = { + id: lineage.lineageId, + scopeType: 'workspace', + organizationId: context.tenantScope.organizationId, + workspaceId: + context.tenantScope.scopeType === 'organization' ? null : context.tenantScope.workspaceId, + projectId: null, + derivedArtifactVersionId: lineage.derivedArtifactVersionId, + sourceVersionIds: lineage.sourceArtifactVersionIds, + processorVersion: 'different-processor@1', + recipeVersion: null, + coordinateLineage: lineage.coordinateLineage, + }; + let initialLookup = true; + const raceClient: ArtifactLineageDatabaseClientV1 = { + artifactLineageRecord: { + create() { + return Promise.reject( + Object.assign(new Error('fixture id race'), { + code: 'P2002', + meta: { target: ['id'] }, + }), + ); + }, + findUnique({ where }) { + if ('id' in where) { + if (initialLookup) { + initialLookup = false; + return Promise.resolve(null); + } + return Promise.resolve(persisted); + } + return Promise.resolve(null); + }, + findMany() { + return Promise.resolve([]); + }, + }, + $transaction(work) { + return work(this); + }, + }; + await assert.rejects( + new PrismaArtifactLineageRepositoryAdapter(raceClient).save(context, lineage), + /IAE_IMMUTABLE_LINEAGE/u, + ); +}); + void test('IAE-007 lineage test storage enforces one record per derived artifact version', async () => { const rows: ArtifactLineageDatabaseRowV1[] = []; const database = client(rows); diff --git a/services/api/test/openapi.test.ts b/services/api/test/openapi.test.ts index eb4cb9e1..4d9cb6bd 100644 --- a/services/api/test/openapi.test.ts +++ b/services/api/test/openapi.test.ts @@ -263,6 +263,11 @@ void test('generates deterministic versioned OpenAPI with safe headers, errors, Record >; assert.equal(auditResultProperties['createdAt']?.['pattern'], strictUtcTimestampPattern); + const admissionProperties = ( + firstDocument.components?.schemas?.['AdmitArtifactDto'] as Record + )['properties'] as Record>; + assert.equal(admissionProperties['actualByteSize']?.['type'], 'integer'); + assert.equal(admissionProperties['maxByteSize']?.['type'], 'integer'); for (const operation of operations(firstDocument)) { const headerNames = (operation.parameters ?? [])