diff --git a/prisma/migrations/0336_measurement_source_external/migration.sql b/prisma/migrations/0336_measurement_source_external/migration.sql new file mode 100644 index 000000000..0a8601d7b --- /dev/null +++ b/prisma/migrations/0336_measurement_source_external/migration.sql @@ -0,0 +1,27 @@ +-- Ingest-token write provenance. +-- +-- The `measurements:write` Bearer scope mints a token a home-automation +-- bridge pastes into a rule; until now the rows it pushed were attributed +-- `MANUAL`, the same label a hand-typed reading gets, so afterwards nobody +-- could tell which readings came off the scale. Rows written under that +-- scope carry their own `MeasurementSource` value instead, so they are +-- distinguishable from MANUAL / TELEGRAM / MCP provenance and get an +-- isolated idempotency namespace `(userId, type, EXTERNAL, externalId)`. +-- +-- Purely-additive enum extension; no row touched. `ADD VALUE IF NOT EXISTS` +-- makes a rerun safe. The new value is NOT used elsewhere in this migration, +-- so it is safe to extend the enum in the same step (Postgres only forbids +-- USING a freshly-added value in the same transaction). Mirrors the 0189 +-- extension that added TELEGRAM and the 0207 one that added MCP. The enum is +-- `@@map`-ed to the snake_case type name `measurement_source`. +-- +-- No backfill, deliberately, and none is possible: `measurements` carries no +-- actor or token column and the audit trail never records the credential, so +-- a bridge-written MANUAL row is byte-identical to a hand-typed one. That +-- indistinguishability is the bug being fixed; any heuristic would relabel +-- real hand entries as machine ones. Existing rows stay MANUAL — still +-- editable, and correctly not badged. +-- +-- Reversibility: Postgres cannot remove an enum value, so `EXTERNAL` stays +-- (inert with no rows). +ALTER TYPE "measurement_source" ADD VALUE IF NOT EXISTS 'EXTERNAL'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c6c619041..70acf8e51 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1741,6 +1741,21 @@ enum MeasurementSource { // mirrors this exact spelling so the `/api/sync/changes` delta feed decodes // the rows. Feeds Workout rows only — Strava exposes no daily metrics. STRAVA + // v1.38.x — measurements pushed in through a `measurements:write` Bearer + // token: a home-automation bridge, a smart scale's own uploader, any + // third-party sender the user pointed at their own record. Like TELEGRAM + // and MCP this is a user-driven write rather than a server-owned + // integration, but it does not enter through the cookie/Bearer client + // write path, so it carries its own source label — a hand-typed reading + // and a bridged one were otherwise indistinguishable after the fact. Its + // own value also gives those writes an isolated idempotency namespace + // `(userId, type, EXTERNAL, externalId)`. No client may NAME it: the batch + // + single-POST surfaces refuse any body that asserts a source under this + // scope, and the value is resolved server-side. Unlike the server-owned + // sources above the rows stay value-editable, since the hardware is the + // user's own. iOS DTO must mirror this exact spelling so the + // /api/sync/changes delta feed decodes the rows. + EXTERNAL @@map("measurement_source") }