Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
843a85d
fix(iae): forward artifact scan state
BeforeLights Aug 3, 2026
5a9cff1
fix(dso): require sequential capability revisions
BeforeLights Aug 3, 2026
0fc77d6
fix(iae): block quarantined evidence handles
BeforeLights Aug 3, 2026
2886d00
fix(iae): normalize evidence sheet lookup
BeforeLights Aug 3, 2026
4c8bd91
fix(iae): authorize persisted placement scope
BeforeLights Aug 3, 2026
677d981
fix(iam): align in-memory MFA revisions
BeforeLights Aug 3, 2026
924c48b
test(iae): enforce lineage uniqueness in fixture
BeforeLights Aug 3, 2026
55d0c6c
test(iae): bind lineage index assertion
BeforeLights Aug 3, 2026
151524e
fix(sa): group formula gaps by family
BeforeLights Aug 3, 2026
454043b
test(iam): prove bootstrap transaction client
BeforeLights Aug 3, 2026
71acbc9
test(api): prove foundation option forwarding
BeforeLights Aug 3, 2026
32b6ace
test(iae): fail closed on retention authorization
BeforeLights Aug 3, 2026
9702160
fix(iae): derive deletion requester from session
BeforeLights Aug 3, 2026
2fa1e6e
docs(review): record PR 33 dispositions
BeforeLights Aug 3, 2026
cfdb786
style(review): format promotion fixes
BeforeLights Aug 3, 2026
ea7fdab
Merge review fixes for promotion PR #33
BeforeLights Aug 3, 2026
8eccfa4
fix(android): fail closed on hostile telemetry maps
BeforeLights Aug 3, 2026
2affc4e
fix(android): validate telemetry timestamps
BeforeLights Aug 3, 2026
ffe37cf
fix(telemetry): isolate exporter failures
BeforeLights Aug 3, 2026
5f998f4
fix(engine): preserve telemetry privacy for mappings
BeforeLights Aug 3, 2026
d5bdf2a
fix(telemetry): bound clock adapter failures
BeforeLights Aug 3, 2026
920b2a2
fix(iam): reject duplicate MFA state identities
BeforeLights Aug 3, 2026
1a0978b
fix(iam): enforce Prisma MFA identity uniqueness
BeforeLights Aug 3, 2026
d1eeae2
fix(iam): contain MFA verifier failures
BeforeLights Aug 3, 2026
f9b67fd
fix(iam): contain MFA clock failures
BeforeLights Aug 3, 2026
445fd24
fix(bua): reject duplicate usage identities
BeforeLights Aug 3, 2026
a7402f9
fix(bua): enforce Prisma usage identity uniqueness
BeforeLights Aug 3, 2026
f4e2f2f
feat(api): parse bounded traceparent context
BeforeLights Aug 3, 2026
1986d80
feat(api): propagate validated trace context
BeforeLights Aug 3, 2026
91cfbdc
fix(bua): preserve visible inherited usage replays
BeforeLights Aug 3, 2026
3011212
style(api): format trace context tests
BeforeLights Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -50,7 +53,8 @@ object TelemetryContract {

fun sanitizeAttributes(input: Map<String, Any?>): Map<String, Any> {
val result = linkedMapOf<String, Any>()
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"
}
Expand All @@ -62,13 +66,22 @@ object TelemetryContract {
}

fun assertSafeAttributes(input: Map<String, Any?>) {
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<String, Any?>): List<Pair<String, Any?>>? =
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") {
Expand Down Expand Up @@ -133,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,
Expand All @@ -148,9 +166,14 @@ object TelemetryContract {
}

private fun singleHeader(headers: Map<String, List<String>>, 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" } }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -73,4 +74,50 @@ class TelemetryContractTest {
)
}
}

@Test
fun providerBackedMapsFailClosedWithoutLeakingTheirCause() {
val hostileAttributes = object : Map<String, Any?> by emptyMap() {
override val entries: Set<Map.Entry<String, Any?>>
get() = throw IllegalStateException("provider attribute cause")
}
assertEquals(emptyMap<String, Any>(), TelemetryContract.sanitizeAttributes(hostileAttributes))
val attributeError = assertThrows(IllegalArgumentException::class.java) {
TelemetryContract.assertSafeAttributes(hostileAttributes)
}
assertEquals("telemetry attributes are not readable", attributeError.message)

val hostileHeaders = object : Map<String, List<String>> by emptyMap() {
override val entries: Set<Map.Entry<String, List<String>>>
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"))
}

@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)
}
}
37 changes: 37 additions & 0 deletions docs/operations/coderabbit-pr-33-disposition.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 3 additions & 1 deletion packages/domain/src/artifact/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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] ?? '');
Expand Down
7 changes: 7 additions & 0 deletions packages/domain/test/artifact-v1.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
14 changes: 12 additions & 2 deletions packages/telemetry/src/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -375,7 +381,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;
},
};
Expand Down
35 changes: 35 additions & 0 deletions packages/telemetry/test/telemetry-v1.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,38 @@ 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);
});

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);
});
8 changes: 6 additions & 2 deletions services/api/openapi/v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand All @@ -7533,7 +7538,6 @@
"activeApproval",
"legalHold",
"requestId",
"requestedBy",
"requestedAt"
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,17 @@ export class InMemoryEntitlementRepositoryAdapter implements EntitlementReposito

async persistUsageState(context: IamTenantContextV1, state: UsageLedgerStateV1): Promise<void> {
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) {
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');
Expand All @@ -162,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) ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,12 @@ class PrismaEntitlementTransactionAdapter implements EntitlementTransactionPortV
context: IamTenantContextV1,
state: UsageLedgerStateV1,
): Promise<void> {
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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Expand Down Expand Up @@ -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 ||
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
tenantScopeContainsV1,
tenantScopesEqualV1,
type ArtifactScanStateV1,
type ArtifactVersionV1,
type ContentPlacementV1,
Expand Down Expand Up @@ -115,14 +116,17 @@ export class InMemoryArtifactRepositoryAdapter implements ArtifactRepositoryPort

async updatePlacement(context: IamTenantContextV1, placement: ContentPlacementV1): Promise<void> {
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
Expand Down
Loading
Loading