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 01/30] 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 02/30] 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 03/30] 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 04/30] 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 05/30] 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 06/30] 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 07/30] 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 08/30] 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 09/30] 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 10/30] 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 11/30] 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 12/30] 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 13/30] 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 14/30] 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 15/30] 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 16/30] 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 2affc4ec3b2fffaf3d4286043388e32c761a89f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:50:53 +0700 Subject: [PATCH 17/30] fix(android): validate telemetry timestamps --- .../android/telemetry/TelemetryContract.kt | 10 +++++++- .../android/TelemetryContractTest.kt | 23 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) 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 b941ef80..09b739bc 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 @@ -1,5 +1,8 @@ package com.databreeze.android.telemetry +import java.time.Instant +import java.time.format.DateTimeParseException + /** Cross-runtime names and safe record helpers shared with @databreeze/telemetry/v1. */ object TelemetryContract { const val SchemaVersion = 1 @@ -143,9 +146,14 @@ object TelemetryContract { correlation.spanId, correlation.traceFlags, ) + val normalizedTimestamp = try { + Instant.parse(timestamp).toString() + } catch (_: DateTimeParseException) { + throw IllegalArgumentException("invalid telemetry timestamp") + } return TelemetryRecord( SchemaVersion, - timestamp, + normalizedTimestamp, level, event, component, 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..de903e2c 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 @@ -97,4 +97,27 @@ class TelemetryContractTest { assertTrue(headerError.message.orEmpty().contains("not readable")) assertTrue(!headerError.message.orEmpty().contains("provider header cause")) } + + @Test + fun recordRequiresAndNormalizesAnAbsoluteTimestamp() { + val normalized = TelemetryContract.createRecord( + "info", + "sync.completed", + "android", + CorrelationContext(correlationId), + timestamp = "2026-01-01T07:00:00+07:00", + ) + assertEquals("2026-01-01T00:00:00Z", normalized.timestamp) + + val error = assertThrows(IllegalArgumentException::class.java) { + TelemetryContract.createRecord( + "info", + "sync.completed", + "android", + CorrelationContext(correlationId), + timestamp = "tomorrow in a provider timezone", + ) + } + assertEquals("invalid telemetry timestamp", error.message) + } } From ffe37cf33f8176fb799ffb32058baad9f76846df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:51:31 +0700 Subject: [PATCH 18/30] fix(telemetry): isolate exporter failures --- packages/telemetry/src/v1.ts | 6 +++++- packages/telemetry/test/telemetry-v1.test.mjs | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/telemetry/src/v1.ts b/packages/telemetry/src/v1.ts index e74ea2e2..66711b6d 100644 --- a/packages/telemetry/src/v1.ts +++ b/packages/telemetry/src/v1.ts @@ -375,7 +375,11 @@ export function createStructuredLoggerV1(options: StructuredLoggerOptionsV1) { record.spanId = normalized.spanId; if (normalized.traceFlags !== undefined) record.traceFlags = normalized.traceFlags; } - sink(record); + try { + sink(record); + } catch { + // Exporters are best-effort adapters and cannot become product authority. + } return record; }, }; diff --git a/packages/telemetry/test/telemetry-v1.test.mjs b/packages/telemetry/test/telemetry-v1.test.mjs index d4a53d02..d29cfcf4 100644 --- a/packages/telemetry/test/telemetry-v1.test.mjs +++ b/packages/telemetry/test/telemetry-v1.test.mjs @@ -233,3 +233,24 @@ test('structured logger carries normalized trace context into the record', () => assert.equal(record.spanId, '0123456789abcdef'); assert.equal(record.traceFlags, '00'); }); + +test('structured logger isolates exporter outages from product workflows', () => { + const logger = createStructuredLoggerV1({ + component: 'api', + clock: () => new Date('2026-01-01T00:00:00.000Z'), + sink() { + throw new Error('provider cause with customer source value'); + }, + }); + + const record = logger.emit( + 'warn', + 'telemetry.export_failed', + createCorrelationContextV1({ correlationId }), + { outcome: 'degraded', payload: 'must not be serialized' }, + ); + + assert.equal(record.event, 'telemetry.export_failed'); + assert.deepEqual(record.attributes, { outcome: 'degraded' }); + assert.doesNotMatch(JSON.stringify(record), /provider cause|customer source|must not/u); +}); From 5f998f4b8c23a003da192ea919ae4310203a8ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:52:55 +0700 Subject: [PATCH 19/30] fix(engine): preserve telemetry privacy for mappings --- .../engine/src/databreeze_engine/telemetry.py | 4 +-- services/engine/tests/test_telemetry.py | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/services/engine/src/databreeze_engine/telemetry.py b/services/engine/src/databreeze_engine/telemetry.py index b8418ab2..71a41afa 100644 --- a/services/engine/src/databreeze_engine/telemetry.py +++ b/services/engine/src/databreeze_engine/telemetry.py @@ -160,7 +160,7 @@ def _validate_key(key: object) -> str: return key -def sanitize_attributes(attributes: dict[str, Any]) -> dict[str, str | int | float | bool]: +def sanitize_attributes(attributes: Mapping[str, Any]) -> dict[str, str | int | float | bool]: """Return only bounded, allowlisted scalar attributes.""" if not isinstance(attributes, Mapping): @@ -336,7 +336,7 @@ def emit_record( "event": event, "component": component, "correlationId": normalized.correlation_id, - "attributes": sanitize_attributes(dict(attributes or {})), + "attributes": sanitize_attributes(attributes if attributes is not None else {}), } if normalized.trace_id and normalized.span_id: record["traceId"] = normalized.trace_id diff --git a/services/engine/tests/test_telemetry.py b/services/engine/tests/test_telemetry.py index 2860e7a9..15577a6b 100644 --- a/services/engine/tests/test_telemetry.py +++ b/services/engine/tests/test_telemetry.py @@ -1,3 +1,5 @@ +from collections.abc import Iterator, Mapping + import pytest from databreeze_engine.telemetry import ( @@ -128,3 +130,28 @@ def test_engine_accepts_mixed_case_header_names() -> None: ) == context ) + + +def test_engine_record_builder_isolates_hostile_attribute_mappings() -> None: + class HostileAttributes(Mapping[str, object]): + def __getitem__(self, key: str) -> object: + raise RuntimeError("provider source value must not escape") + + def __iter__(self) -> Iterator[str]: + raise RuntimeError("provider source value must not escape") + + def __len__(self) -> int: + return 1 + + def items(self): # type: ignore[override] + raise RuntimeError("provider source value must not escape") + + record = emit_record( + "warn", + "processor.degraded", + "engine", + CorrelationContext("00000000-0000-4000-8000-000000000001"), + HostileAttributes(), + ) + assert record["attributes"] == {} + assert "provider source" not in str(record) From d5bdf2ac872ba5c6740e54387d9651a7707d17a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:53:43 +0700 Subject: [PATCH 20/30] fix(telemetry): bound clock adapter failures --- packages/telemetry/src/v1.ts | 8 +++++++- packages/telemetry/test/telemetry-v1.test.mjs | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/telemetry/src/v1.ts b/packages/telemetry/src/v1.ts index 66711b6d..a6e70786 100644 --- a/packages/telemetry/src/v1.ts +++ b/packages/telemetry/src/v1.ts @@ -361,9 +361,15 @@ export function createStructuredLoggerV1(options: StructuredLoggerOptionsV1) { if (!levelSet.has(level)) throw new Error('Invalid telemetry level'); if (!eventPattern.test(event)) throw new Error('Invalid telemetry event'); const normalized = createCorrelationContextV1(correlation); + let timestamp: string; + try { + timestamp = clock().toISOString(); + } catch { + timestamp = new Date().toISOString(); + } const record: TelemetryRecordV1 = { schemaVersion: TELEMETRY_SCHEMA_VERSION_V1, - timestamp: clock().toISOString(), + timestamp, level, event, component: options.component, diff --git a/packages/telemetry/test/telemetry-v1.test.mjs b/packages/telemetry/test/telemetry-v1.test.mjs index d29cfcf4..cc6e832d 100644 --- a/packages/telemetry/test/telemetry-v1.test.mjs +++ b/packages/telemetry/test/telemetry-v1.test.mjs @@ -254,3 +254,17 @@ test('structured logger isolates exporter outages from product workflows', () => assert.deepEqual(record.attributes, { outcome: 'degraded' }); assert.doesNotMatch(JSON.stringify(record), /provider cause|customer source|must not/u); }); + +test('structured logger uses a safe fallback when a clock adapter fails', () => { + const logger = createStructuredLoggerV1({ + component: 'engine', + clock() { + throw new Error('provider clock cause'); + }, + sink: () => undefined, + }); + + const record = logger.emit('info', 'processor.started', { correlationId }, {}); + assert.match(record.timestamp, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u); + assert.doesNotMatch(JSON.stringify(record), /provider clock cause/u); +}); From 920b2a293bd0b3630de972fd18094c11de671d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:56:28 +0700 Subject: [PATCH 21/30] fix(iam): reject duplicate MFA state identities --- .../adapter/in-memory-mfa-repository.adapter.ts | 5 +++++ .../api/test/features/iam/mfa.service.test.ts | 15 +++++++++++++++ 2 files changed, 20 insertions(+) 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 5ca1c46e..d67720e3 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 @@ -14,6 +14,11 @@ function cloneState(state: MfaStateV1): MfaStateV1 { } function immutableState(existing: MfaStateV1, next: MfaStateV1): boolean { + if ( + new Set(next.factors.map((factor) => factor.id)).size !== next.factors.length || + new Set(next.recoveryCodes.map((code) => code.id)).size !== next.recoveryCodes.length + ) + return false; const existingFactors = new Map(existing.factors.map((factor) => [factor.id, factor])); const existingCodes = new Map(existing.recoveryCodes.map((code) => [code.id, code])); if ( diff --git a/services/api/test/features/iam/mfa.service.test.ts b/services/api/test/features/iam/mfa.service.test.ts index b0574b69..a3cdeef3 100644 --- a/services/api/test/features/iam/mfa.service.test.ts +++ b/services/api/test/features/iam/mfa.service.test.ts @@ -143,4 +143,19 @@ void test('[IAM-012, IAM-014] in-memory MFA state rejects removal and invalid ne }), /IAM_MFA_REVISION_CONFLICT/u, ); + + await assert.rejects( + repository.saveState(userId as never, { + factors: [factor.value, factor.value], + recoveryCodes: [code.value], + }), + /IAM_MFA_REVISION_CONFLICT/u, + ); + await assert.rejects( + repository.saveState(userId as never, { + factors: [factor.value], + recoveryCodes: [code.value, code.value], + }), + /IAM_MFA_REVISION_CONFLICT/u, + ); }); From 1a0978b47f2c9909624e1c32e70e5503c297c4fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:57:40 +0700 Subject: [PATCH 22/30] fix(iam): enforce Prisma MFA identity uniqueness --- .../adapter/prisma-mfa-repository.adapter.ts | 5 ++++ .../iam/prisma-mfa-repository.test.ts | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/services/api/src/features/iam/adapter/prisma-mfa-repository.adapter.ts b/services/api/src/features/iam/adapter/prisma-mfa-repository.adapter.ts index 4af5d094..2384a4e3 100644 --- a/services/api/src/features/iam/adapter/prisma-mfa-repository.adapter.ts +++ b/services/api/src/features/iam/adapter/prisma-mfa-repository.adapter.ts @@ -181,6 +181,11 @@ function recoveryRow(code: RecoveryCodeV1): MfaRecoveryCodeDatabaseRowV1 { } function immutableState(existing: MfaStateV1, next: MfaStateV1): boolean { + if ( + new Set(next.factors.map((factor) => factor.id)).size !== next.factors.length || + new Set(next.recoveryCodes.map((code) => code.id)).size !== next.recoveryCodes.length + ) + return false; const existingFactors = new Map(existing.factors.map((factor) => [factor.id, factor])); const existingCodes = new Map(existing.recoveryCodes.map((code) => [code.id, code])); if ( diff --git a/services/api/test/features/iam/prisma-mfa-repository.test.ts b/services/api/test/features/iam/prisma-mfa-repository.test.ts index daa55d3b..115535be 100644 --- a/services/api/test/features/iam/prisma-mfa-repository.test.ts +++ b/services/api/test/features/iam/prisma-mfa-repository.test.ts @@ -227,6 +227,30 @@ void test('[IAM-012, IAM-014] Prisma MFA persistence rejects a changed stale rev ); }); +void test('[IAM-012, IAM-014] Prisma MFA persistence rejects duplicate state identities', async () => { + const { client } = createDatabase(); + const adapter = new PrismaMfaRepositoryAdapter(client); + const input = state(); + const factor = input.factors[0]; + const code = input.recoveryCodes[0]; + if (!factor || !code) throw new Error('fixture missing MFA state'); + + await assert.rejects( + adapter.saveState(factor.userId, { + factors: [factor, factor], + recoveryCodes: [code], + }), + /IAM_MFA_REVISION_CONFLICT/u, + ); + await assert.rejects( + adapter.saveState(factor.userId, { + factors: [factor], + recoveryCodes: [code, code], + }), + /IAM_MFA_REVISION_CONFLICT/u, + ); +}); + void test('[IAM-012, IAM-016] Prisma MFA persistence rejects a redemption race with compare-and-set', async () => { const { client } = createDatabase(); const adapter = new PrismaMfaRepositoryAdapter(client); From d1eeae2e3ad6b0eea4ed8d8128502a7583b2c5e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 17:58:54 +0700 Subject: [PATCH 23/30] fix(iam): contain MFA verifier failures --- .../features/iam/application/mfa.service.ts | 19 ++++++++++------- .../api/test/features/iam/mfa.service.test.ts | 21 +++++++++++++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/services/api/src/features/iam/application/mfa.service.ts b/services/api/src/features/iam/application/mfa.service.ts index f9f5519c..7d71fbf1 100644 --- a/services/api/src/features/iam/application/mfa.service.ts +++ b/services/api/src/features/iam/application/mfa.service.ts @@ -128,13 +128,18 @@ export class MfaService { const factor = state.factors.find((item) => item.id === factorId); if (!factor) return invalidState(); if (factor.status !== 'PENDING') return invalidState(); - const verified = await this.factorProofVerifier.verify({ - userId, - factorId, - method: factor.method, - secretReference: factor.secretReference, - proof: factorProof, - }); + let verified = false; + try { + verified = await this.factorProofVerifier.verify({ + userId, + factorId, + method: factor.method, + secretReference: factor.secretReference, + proof: factorProof, + }); + } catch { + verified = false; + } if (!verified) return Object.freeze({ accepted: false as const, code: 'FACTOR_PROOF_INVALID' as const }); const transitioned = transitionMfaFactorV1(factor, 'VERIFY', this.clock().toISOString()); diff --git a/services/api/test/features/iam/mfa.service.test.ts b/services/api/test/features/iam/mfa.service.test.ts index a3cdeef3..898e79a9 100644 --- a/services/api/test/features/iam/mfa.service.test.ts +++ b/services/api/test/features/iam/mfa.service.test.ts @@ -46,6 +46,27 @@ void test('[IAM-012, IAM-013, IAM-014] MFA enrollment and verification are revis assert.deepEqual(secondVerify, { accepted: false, code: 'INVALID_STATE' }); }); +void test('[IAM-013] MFA proof-provider failures become a safe verification result', async () => { + const repository = new InMemoryMfaRepositoryAdapter(); + const service = new MfaService( + repository, + { matches: (presented, stored) => presented === stored }, + { verify: () => Promise.reject(new Error('provider secret details must not escape')) }, + () => new Date(at), + ); + const enrolled = await service.enroll({ + id: factorId, + userId, + method: 'TOTP', + secretReference: 'secret-ref:totp:1', + }); + assert.equal(enrolled.accepted, true); + assert.deepEqual(await service.verifyFactor(userId, factorId, '654321'), { + accepted: false, + code: 'FACTOR_PROOF_INVALID', + }); +}); + void test('[IAM-015, IAM-016] recovery code redemption is one-time and does not expose digests', async () => { const repository = new InMemoryMfaRepositoryAdapter(); const code = createRecoveryCodeV1({ id: recoveryId, userId, digest: 'digest-1', createdAt: at }); From f9b67fd9f4b6a1a780cc0b4fa9b53d879aa76d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 18:00:21 +0700 Subject: [PATCH 24/30] fix(iam): contain MFA clock failures --- .../features/iam/application/mfa.service.ts | 20 ++++++++++++++++--- .../api/test/features/iam/mfa.service.test.ts | 20 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/services/api/src/features/iam/application/mfa.service.ts b/services/api/src/features/iam/application/mfa.service.ts index 7d71fbf1..80de25e1 100644 --- a/services/api/src/features/iam/application/mfa.service.ts +++ b/services/api/src/features/iam/application/mfa.service.ts @@ -89,13 +89,23 @@ export class MfaService { private readonly clock: () => Date = () => new Date(), ) {} + private timestamp(): string | undefined { + try { + return this.clock().toISOString(); + } catch { + return undefined; + } + } + public async enroll(input: { readonly id: unknown; readonly userId: unknown; readonly method: unknown; readonly secretReference: unknown; }): Promise> { - const factor = createMfaFactorV1({ ...input, enrolledAt: this.clock().toISOString() }); + const enrolledAt = this.timestamp(); + if (!enrolledAt) return Object.freeze({ accepted: false, code: 'INVALID_TIMESTAMP' }); + const factor = createMfaFactorV1({ ...input, enrolledAt }); if (!factor.accepted) return Object.freeze({ accepted: false, code: factor.code }); return this.repository.withTransaction(async (transaction) => { const state = await transaction.findState(factor.value.userId); @@ -142,7 +152,9 @@ export class MfaService { } if (!verified) return Object.freeze({ accepted: false as const, code: 'FACTOR_PROOF_INVALID' as const }); - const transitioned = transitionMfaFactorV1(factor, 'VERIFY', this.clock().toISOString()); + const verifiedAt = this.timestamp(); + if (!verifiedAt) return Object.freeze({ accepted: false, code: 'INVALID_TIMESTAMP' }); + const transitioned = transitionMfaFactorV1(factor, 'VERIFY', verifiedAt); if (!transitioned.accepted) return Object.freeze({ accepted: false, code: transitioned.code }); const next = Object.freeze({ @@ -162,9 +174,11 @@ export class MfaService { if (!userId) return Object.freeze({ accepted: false, code: 'INVALID_IDENTIFIER' }); return this.repository.withTransaction(async (transaction) => { const state = await transaction.findState(userId); + const redeemedAt = this.timestamp(); + if (!redeemedAt) return Object.freeze({ accepted: false, code: 'INVALID_TIMESTAMP' }); const redeemed = redeemRecoveryCodeV1( state, - { userId, presentedDigest, at: this.clock().toISOString() }, + { userId, presentedDigest, at: redeemedAt }, this.recoveryMatcher, ); if (!redeemed.accepted) return Object.freeze({ accepted: false, code: redeemed.code }); diff --git a/services/api/test/features/iam/mfa.service.test.ts b/services/api/test/features/iam/mfa.service.test.ts index 898e79a9..e196387c 100644 --- a/services/api/test/features/iam/mfa.service.test.ts +++ b/services/api/test/features/iam/mfa.service.test.ts @@ -67,6 +67,26 @@ void test('[IAM-013] MFA proof-provider failures become a safe verification resu }); }); +void test('[IAM-012, IAM-015] MFA clock-provider failures become a stable timestamp result', async () => { + const service = new MfaService( + new InMemoryMfaRepositoryAdapter(), + { matches: (presented, stored) => presented === stored }, + undefined, + () => { + throw new Error('clock provider details must not escape'); + }, + ); + assert.deepEqual( + await service.enroll({ + id: factorId, + userId, + method: 'TOTP', + secretReference: 'secret-ref:totp:1', + }), + { accepted: false, code: 'INVALID_TIMESTAMP' }, + ); +}); + void test('[IAM-015, IAM-016] recovery code redemption is one-time and does not expose digests', async () => { const repository = new InMemoryMfaRepositoryAdapter(); const code = createRecoveryCodeV1({ id: recoveryId, userId, digest: 'digest-1', createdAt: at }); From 445fd246c8eb4c956a843534a4bfa4f5e65f480d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 18:02:02 +0700 Subject: [PATCH 25/30] fix(bua): reject duplicate usage identities --- .../in-memory-entitlement-repository.adapter.ts | 6 ++++++ .../features/bua/entitlement-repository.test.ts | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/services/api/src/features/bua/adapter/in-memory-entitlement-repository.adapter.ts b/services/api/src/features/bua/adapter/in-memory-entitlement-repository.adapter.ts index d5364b93..7ae882eb 100644 --- a/services/api/src/features/bua/adapter/in-memory-entitlement-repository.adapter.ts +++ b/services/api/src/features/bua/adapter/in-memory-entitlement-repository.adapter.ts @@ -140,6 +140,12 @@ export class InMemoryEntitlementRepositoryAdapter implements EntitlementReposito async persistUsageState(context: IamTenantContextV1, state: UsageLedgerStateV1): Promise { await Promise.resolve(); + if ( + new Set(state.entries.map((entry) => entry.entryId)).size !== state.entries.length || + new Set(state.reservations.map((reservation) => reservation.reservationId)).size !== + state.reservations.length + ) + throw new Error('BUA_USAGE_STATE_CONFLICT'); for (const entry of state.entries) { const existing = this.entries.get(entry.entryId); if (existing) { diff --git a/services/api/test/features/bua/entitlement-repository.test.ts b/services/api/test/features/bua/entitlement-repository.test.ts index 451fc208..9e0799d8 100644 --- a/services/api/test/features/bua/entitlement-repository.test.ts +++ b/services/api/test/features/bua/entitlement-repository.test.ts @@ -161,6 +161,23 @@ void test('[BUA-008, BUA-009, BUA-010, BUA-011] usage state persists append-only }), /BUA_IMMUTABLE_USAGE_ENTRY/, ); + const persistedEntry = reserved.value.state.entries[0]; + if (!persistedEntry) throw new Error('fixture entry missing'); + await assert.rejects( + repository.persistUsageState(context(workspaceId), { + ...reserved.value.state, + entries: [persistedEntry, persistedEntry], + }), + /BUA_USAGE_STATE_CONFLICT/u, + ); + if (!activeReservation) throw new Error('fixture reservation missing'); + await assert.rejects( + repository.persistUsageState(context(workspaceId), { + ...reserved.value.state, + reservations: [activeReservation, activeReservation], + }), + /BUA_USAGE_STATE_CONFLICT/u, + ); await assert.rejects( repository.withTransaction(context(workspaceId), async (transaction) => { const second = reserveUsageV1(storedSnapshot, reserved.value.state, { From a7402f9c5c3b6c84301aa57fec705341586b730f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 18:03:16 +0700 Subject: [PATCH 26/30] fix(bua): enforce Prisma usage identity uniqueness --- .../prisma-entitlement-repository.adapter.ts | 6 ++++ .../bua/prisma-entitlement-repository.test.ts | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/services/api/src/features/bua/adapter/prisma-entitlement-repository.adapter.ts b/services/api/src/features/bua/adapter/prisma-entitlement-repository.adapter.ts index e46a1882..a6feb1ec 100644 --- a/services/api/src/features/bua/adapter/prisma-entitlement-repository.adapter.ts +++ b/services/api/src/features/bua/adapter/prisma-entitlement-repository.adapter.ts @@ -585,6 +585,12 @@ class PrismaEntitlementTransactionAdapter implements EntitlementTransactionPortV context: IamTenantContextV1, state: UsageLedgerStateV1, ): Promise { + if ( + new Set(state.entries.map((entry) => entry.entryId)).size !== state.entries.length || + new Set(state.reservations.map((reservation) => reservation.reservationId)).size !== + state.reservations.length + ) + throw new Error('BUA_USAGE_STATE_CONFLICT'); for (const entry of state.entries) { if (!tenantScopeContainsV1(context.tenantScope, entry.tenantScope)) throw new Error('BUA_SCOPE_NARROWING_REQUIRED'); diff --git a/services/api/test/features/bua/prisma-entitlement-repository.test.ts b/services/api/test/features/bua/prisma-entitlement-repository.test.ts index 520cea03..c4ee6119 100644 --- a/services/api/test/features/bua/prisma-entitlement-repository.test.ts +++ b/services/api/test/features/bua/prisma-entitlement-repository.test.ts @@ -251,6 +251,35 @@ void test('[BUA-001, BUA-002, BUA-008, IAM-009] Prisma entitlement adapter persi ); }); +void test('[BUA-008, BUA-011] Prisma entitlement adapter rejects duplicate usage identities', async () => { + const repository = new PrismaEntitlementRepositoryAdapter(client()); + await repository.saveSnapshot(context(workspaceId, 'duplicate-snapshot'), snapshot()); + const service = new EntitlementAdmissionService(repository); + const admitted = await service.admit( + context(workspaceId, 'duplicate-admit'), + admissionInput('duplicate-admit', '1'), + ); + assert.equal(admitted.accepted, true); + if (!admitted.accepted) return; + const entry = admitted.value.state.entries[0]; + const reservation = admitted.value.state.reservations[0]; + if (!entry || !reservation) throw new Error('fixture usage state missing'); + await assert.rejects( + repository.persistUsageState(context(workspaceId, 'duplicate-entry'), { + ...admitted.value.state, + entries: [entry, entry], + }), + /BUA_USAGE_STATE_CONFLICT/u, + ); + await assert.rejects( + repository.persistUsageState(context(workspaceId, 'duplicate-reservation'), { + ...admitted.value.state, + reservations: [reservation, reservation], + }), + /BUA_USAGE_STATE_CONFLICT/u, + ); +}); + void test('[BUA-008, IAM-009] Prisma entitlement adapter round-trips project-scoped usage', async () => { const repository = new PrismaEntitlementRepositoryAdapter(client()); await repository.saveSnapshot(context(workspaceId, 'project-snapshot'), snapshot()); From f4e2f2f301c814488984054456cccf612f80d92a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 18:05:16 +0700 Subject: [PATCH 27/30] feat(api): parse bounded traceparent context --- .../api/src/platform/http/request-context.ts | 37 +++++++++++++++++++ services/api/test/request-context.test.ts | 23 ++++++++++++ 2 files changed, 60 insertions(+) diff --git a/services/api/src/platform/http/request-context.ts b/services/api/src/platform/http/request-context.ts index 20f5f512..361255cd 100644 --- a/services/api/src/platform/http/request-context.ts +++ b/services/api/src/platform/http/request-context.ts @@ -16,11 +16,21 @@ export interface RequestContext { const requestContexts = new WeakMap(); const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; +const traceparentPattern = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i; export type CorrelationHeaderResult = | { readonly accepted: true; readonly correlationId: string } | { readonly accepted: false }; +export type TraceparentHeaderResult = + | { + readonly accepted: true; + readonly traceId?: string; + readonly spanId?: string; + readonly traceFlags?: string; + } + | { readonly accepted: false }; + export interface RequestContextOptions { readonly csrf?: Partial; } @@ -59,6 +69,33 @@ export function parseCorrelationHeader( return { accepted: true, correlationId: value }; } +/** Parses one W3C traceparent without reflecting malformed or provider values. */ +export function parseTraceparentHeader(values: readonly string[]): TraceparentHeaderResult { + if (values.length === 0) return { accepted: true }; + if (values.length !== 1) return { accepted: false }; + const value = values[0]; + if (value === undefined) return { accepted: false }; + const match = traceparentPattern.exec(value); + if (!match) return { accepted: false }; + const [, version, traceId, spanId, traceFlags] = match; + if ( + !version || + version.toLowerCase() === 'ff' || + !traceId || + traceId === '0'.repeat(32) || + !spanId || + spanId === '0'.repeat(16) || + !traceFlags + ) + return { accepted: false }; + return { + accepted: true, + traceId: traceId.toLowerCase(), + spanId: spanId.toLowerCase(), + traceFlags: traceFlags.toLowerCase(), + }; +} + export function getRequestContext(request: FastifyRequest): RequestContext { const context = requestContexts.get(request); if (context === undefined) throw new Error('Request context is unavailable'); diff --git a/services/api/test/request-context.test.ts b/services/api/test/request-context.test.ts index eecae0ed..75fcbc04 100644 --- a/services/api/test/request-context.test.ts +++ b/services/api/test/request-context.test.ts @@ -7,7 +7,9 @@ const suppliedCorrelationId = '123e4567-e89b-42d3-a456-426614174000'; void test('accepts zero or one valid bounded UUID correlation header and rejects ambiguous or unsafe input', async () => { const requestContextModule = await import('../src/platform/http/request-context.js'); const parseCorrelationHeader = requestContextModule.parseCorrelationHeader; + const parseTraceparentHeader = requestContextModule.parseTraceparentHeader; assert.equal(typeof parseCorrelationHeader, 'function'); + assert.equal(typeof parseTraceparentHeader, 'function'); assert.deepEqual(parseCorrelationHeader([], requestId), { accepted: true, @@ -25,4 +27,25 @@ void test('accepts zero or one valid bounded UUID correlation header and rejects ]) { assert.deepEqual(parseCorrelationHeader(values, requestId), { accepted: false }); } + + assert.deepEqual( + parseTraceparentHeader([ + '00-0123456789abcdef0123456789abcdef-0123456789abcdef-01', + ]), + { + accepted: true, + traceId: '0123456789abcdef0123456789abcdef', + spanId: '0123456789abcdef', + traceFlags: '01', + }, + ); + for (const values of [ + ['not-a-traceparent'], + ['00-00000000000000000000000000000000-0123456789abcdef-01'], + ['00-0123456789abcdef0123456789abcdef-0000000000000000-01'], + ['ff-0123456789abcdef0123456789abcdef-0123456789abcdef-01'], + ['00-0123456789abcdef0123456789abcdef-0123456789abcdef-01', '00-0123456789abcdef0123456789abcdef-0123456789abcdef-01'], + ]) { + assert.deepEqual(parseTraceparentHeader(values), { accepted: false }); + } }); From 1986d804e2dc1a0c313f9548a56b721ff68fe03c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 18:06:47 +0700 Subject: [PATCH 28/30] feat(api): propagate validated trace context --- .../api/src/platform/http/request-context.ts | 39 ++++++++++++++++++- services/api/test/http-contract.test.ts | 13 +++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/services/api/src/platform/http/request-context.ts b/services/api/src/platform/http/request-context.ts index 361255cd..2f8de0e7 100644 --- a/services/api/src/platform/http/request-context.ts +++ b/services/api/src/platform/http/request-context.ts @@ -12,6 +12,9 @@ import { createProblem } from './problem-details.js'; export interface RequestContext { readonly correlationId: string; readonly requestId: string; + readonly traceId?: string; + readonly spanId?: string; + readonly traceFlags?: string; } const requestContexts = new WeakMap(); @@ -113,10 +116,15 @@ export function installRequestContext( requestContexts.set(request, context); reply.header('X-Request-Id', context.requestId); const values: string[] = []; + const traceValues: string[] = []; for (let index = 0; index < request.raw.rawHeaders.length; index += 2) { - if (request.raw.rawHeaders[index]?.toLowerCase() === 'x-correlation-id') { + const name = request.raw.rawHeaders[index]?.toLowerCase(); + if (name === 'x-correlation-id') { const value = request.raw.rawHeaders[index + 1]; if (value !== undefined) values.push(value); + } else if (name === 'traceparent') { + const value = request.raw.rawHeaders[index + 1]; + if (value !== undefined) traceValues.push(value); } } const parsed = parseCorrelationHeader(values, requestId); @@ -136,7 +144,34 @@ export function installRequestContext( ); return; } - const acceptedContext = { correlationId: parsed.correlationId, requestId }; + const parsedTrace = parseTraceparentHeader(traceValues); + if (!parsedTrace.accepted) { + reply.header('X-Correlation-Id', requestId); + reply + .code(400) + .type('application/problem+json') + .send( + createProblem({ + code: 'CORRELATION_ID_INVALID', + correlationId: requestId, + messageKey: 'api.error.correlation_id_invalid', + retryable: false, + status: 400, + }), + ); + return; + } + const acceptedContext = { + correlationId: parsed.correlationId, + requestId, + ...(parsedTrace.traceId + ? { + traceId: parsedTrace.traceId, + spanId: parsedTrace.spanId, + traceFlags: parsedTrace.traceFlags, + } + : {}), + }; requestContexts.set(request, acceptedContext); reply.header('X-Correlation-Id', acceptedContext.correlationId); const csrf = evaluateCsrfRequestV1( diff --git a/services/api/test/http-contract.test.ts b/services/api/test/http-contract.test.ts index 1b6ef295..d20e9340 100644 --- a/services/api/test/http-contract.test.ts +++ b/services/api/test/http-contract.test.ts @@ -124,6 +124,19 @@ void test('rejects malformed and multiple correlation values without reflecting }); }); +void test('rejects malformed traceparent values without reflecting the header', async () => { + await withApp({}, async (app) => { + const leakedMarker = '00-00000000000000000000000000000000-0123456789abcdef-01'; + const response = await app.inject({ + method: 'GET', + url: '/v1/health', + headers: { traceparent: leakedMarker }, + }); + assert.equal(response.statusCode, 400); + assert.doesNotMatch(response.body, new RegExp(leakedMarker)); + }); +}); + void test('maps unknown routes to safe Problem Details without exposing the path or query', async () => { await withApp({}, async (app) => { const response = await app.inject({ From 91cfbdccc591fa67763f49642c12b962dd0d1ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 18:09:29 +0700 Subject: [PATCH 29/30] fix(bua): preserve visible inherited usage replays --- ...in-memory-entitlement-repository.adapter.ts | 18 ++++++++++++++---- .../bua/entitlement-repository.test.ts | 4 ++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/services/api/src/features/bua/adapter/in-memory-entitlement-repository.adapter.ts b/services/api/src/features/bua/adapter/in-memory-entitlement-repository.adapter.ts index 7ae882eb..4a81866d 100644 --- a/services/api/src/features/bua/adapter/in-memory-entitlement-repository.adapter.ts +++ b/services/api/src/features/bua/adapter/in-memory-entitlement-repository.adapter.ts @@ -150,7 +150,7 @@ export class InMemoryEntitlementRepositoryAdapter implements EntitlementReposito const existing = this.entries.get(entry.entryId); if (existing) { if (!sameUsageEntryV1(existing, entry)) throw new Error('BUA_IMMUTABLE_USAGE_ENTRY'); - continue; + if (visibleInScope(context.tenantScope, entry.tenantScope)) continue; } if (!scopeAllowsMutation(context, entry.tenantScope)) throw new Error('BUA_SCOPE_NARROWING_REQUIRED'); @@ -168,13 +168,23 @@ export class InMemoryEntitlementRepositoryAdapter implements EntitlementReposito } for (const reservation of state.reservations) { const existing = this.reservations.get(reservation.reservationId); + if (existing) { + if (sameUsageReservationV1(existing, reservation)) { + if (visibleInScope(context.tenantScope, reservation.tenantScope)) continue; + } else if ( + !sameReservationExceptStatus(existing, reservation) || + existing.revision + 1 !== reservation.revision || + !validReservationTransition(existing, reservation) + ) { + throw new Error('BUA_RESERVATION_CONFLICT'); + } + } + if (!scopeAllowsMutation(context, reservation.tenantScope)) + throw new Error('BUA_SCOPE_NARROWING_REQUIRED'); if (!existing) { - if (!scopeAllowsMutation(context, reservation.tenantScope)) - throw new Error('BUA_SCOPE_NARROWING_REQUIRED'); this.reservations.set(reservation.reservationId, cloneReservation(reservation)); continue; } - if (sameUsageReservationV1(existing, reservation)) continue; if ( existing.revision + 1 !== reservation.revision || !sameReservationExceptStatus(existing, reservation) || diff --git a/services/api/test/features/bua/entitlement-repository.test.ts b/services/api/test/features/bua/entitlement-repository.test.ts index 9e0799d8..8bb87691 100644 --- a/services/api/test/features/bua/entitlement-repository.test.ts +++ b/services/api/test/features/bua/entitlement-repository.test.ts @@ -145,6 +145,10 @@ void test('[BUA-008, BUA-009, BUA-010, BUA-011] usage state persists append-only if (!reserved.accepted) return; await repository.persistUsageState(context(workspaceId), reserved.value.state); assert.equal((await repository.listUsageState(context(workspaceId))).entries.length, 1); + await assert.rejects( + repository.persistUsageState(context(siblingWorkspaceId), reserved.value.state), + /BUA_SCOPE_NARROWING_REQUIRED/u, + ); const activeReservation = reserved.value.state.reservations[0]; if (!activeReservation) throw new Error('fixture reservation missing'); await assert.rejects( From 301121287ceb8eea988d4d47d89bcdea4e116c47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Mon, 3 Aug 2026 18:10:08 +0700 Subject: [PATCH 30/30] style(api): format trace context tests --- services/api/test/request-context.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/services/api/test/request-context.test.ts b/services/api/test/request-context.test.ts index 75fcbc04..e2a720d6 100644 --- a/services/api/test/request-context.test.ts +++ b/services/api/test/request-context.test.ts @@ -29,9 +29,7 @@ void test('accepts zero or one valid bounded UUID correlation header and rejects } assert.deepEqual( - parseTraceparentHeader([ - '00-0123456789abcdef0123456789abcdef-0123456789abcdef-01', - ]), + parseTraceparentHeader(['00-0123456789abcdef0123456789abcdef-0123456789abcdef-01']), { accepted: true, traceId: '0123456789abcdef0123456789abcdef', @@ -44,7 +42,10 @@ void test('accepts zero or one valid bounded UUID correlation header and rejects ['00-00000000000000000000000000000000-0123456789abcdef-01'], ['00-0123456789abcdef0123456789abcdef-0000000000000000-01'], ['ff-0123456789abcdef0123456789abcdef-0123456789abcdef-01'], - ['00-0123456789abcdef0123456789abcdef-0123456789abcdef-01', '00-0123456789abcdef0123456789abcdef-0123456789abcdef-01'], + [ + '00-0123456789abcdef0123456789abcdef-0123456789abcdef-01', + '00-0123456789abcdef0123456789abcdef-0123456789abcdef-01', + ], ]) { assert.deepEqual(parseTraceparentHeader(values), { accepted: false }); }