Skip to content

aggregator(saas-extract): applySupabaseExport never range/type-checks the export doc — negative used/limit/billedUsd are stamped as live measured data, and an array measuredAt passes the freshness gate and corrupts the card #2508

Closed

Description

@dezren39

Problem

applySupabaseExport (pm-stack/aggregator/src/saas-extract.ts:701) is the trust boundary for the supabase-export file document: live.ts reads the file with JSON.parse (src/sources/live.ts:833-835) and passes the result straight in. The overlay checks shapes only loosely — typeof === "number" && Number.isFinite for metric values and billing, and a Date.parse freshness check for measuredAt — so values the producer could never legitimately emit are accepted and stamped on the Supabase card as live, measured, sourceClass: "api" data:

  1. Negative numbers pass. saas-extract.ts:721-722 accepts any finite used/limit; :741-743 accepts any finite billing.billedUsd. Verified against the real module (main 7d1b081): a fresh, status:"ok" doc with used: -3.25 for db-size yields used = -3.25, percent = -650; billedUsd: -42.5 is stored verbatim (renders as a measured $-42.50 billed line); limit: -0.5 is stored as the metric's limit.
  2. measuredAt is never type-checked. saas-extract.ts:707-708 only requires truthiness and a finite Date.parse(doc.measuredAt). Date.parse coerces its argument, so a JSON array measuredAt: ["2026-10-29T11:59:00.000Z"] passes the freshness gate, and :753 doc.measuredAt.slice(0, 10) then stores the array itself as the service's measuredAt (verified: typeof becomes object). Downstream, freshness() (derive.ts:899) treats its length (1) as a date-only stamp and builds an unparseable string, and tsFor (snapshot-records.ts:37-39) interpolates the array slice into a snapshot record's ts, producing 2026-10-29T11:59:00.000ZT12:00:00Z.

The module's own contract says the opposite: "Missing, failed, unparseable or stale documents change nothing", and the billing branch's comment says a failed billing probe must never render as a measured amount. A corrupted, hand-edited, or older-producer export file currently changes the card — with negative or type-corrupted values presented as the live API read.

House precedent in the same codebase: the quota-current overlay gate rejects a document outright when typeof p.measuredAt !== "string" (src/quota-current.ts:60), and the producer's own parsers reject negative sizes/counts (supabase-export parseDbSize/parseRequestCount).

Evidence

Probe (real applySupabaseExport + extractSaasServices imported from main 7d1b081, Bun): hidden_files/probe-2026-10-29-laneB-supaoverlay/probe.ts

  • Case 1 (negative used/billed): used= -3.25 percent= -650 billedUsd= -42.5 sourceClass= api
  • Case 2 (negative limit): used= 1 limit= -0.5
  • Case 3 (array measuredAt): typeof= object value= ["2026-10-29T11:59:00.000Z"] sourceClass= api
  • Case 4 (control, well-formed fresh doc): applies cleanly, measuredAt a string
  • Case 5 (control, garbage-string measuredAt): correctly rejected, services unchanged

Related but distinct: #2492 is the producer (billedFromAddons in supabase-quota-export.mjs) accepting negatives at write time. This issue is the consumer gate: even with the producer fixed, the overlay reads a file from disk and must not trust values outside the domain the card renders. #2451 (isFleetFeed saas) is the feed gate for a different input and does not cover this overlay.

Why it matters

The Supabase card is the fleet's paid-trigger surface for the muse-dev project (80%-of-Free-limit flag, billed amounts). Negative or corrupted "measured" values suppress or invert the percent math, fabricate a negative billed amount, and poison snapshot history with an unparseable ts — all under an "api / measured" provenance stamp that tells the reader the numbers are trustworthy.

Acceptance criteria

  • applySupabaseExport rejects (changes nothing) or drops the offending metric when used or limit is negative; percent is never computed from a negative used/limit.
  • A negative billing.billedUsd is treated as "no valid billing measurement" (the existing billedUsd = null path), never stored.
  • doc.measuredAt must be a string before the freshness check (mirror quota-current.ts:60); non-string values change nothing.
  • Regression tests in aggregator/test/supabase-export.test.ts for: negative used, negative limit, negative billedUsd, array measuredAt — each leaves the services unchanged (or the metric/billing unset), while the existing fresh-doc tests still pass.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    effort/xsTrivial: expected under 30 minutes of work
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    on Oct 30, 2026
  2. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: filed

    • Agent: new-issue-creator job (new-issue-creator-code-b)
    • Session / ID: cron new-issue-creator-code-b run 2026-10-30T02:03Z
    • Found via: filed this run
    • Task: Triage the applySupabaseExport consumer gate: negative used/limit/billedUsd accepted as live measured data, non-string measuredAt passes the freshness gate via Date.parse coercion and is stored as the service's measuredAt.
    • Expectation: effort xs — pin the validation treatment at the overlay boundary (mirror quota-current.ts:60), add regression tests, then route a small_fix packet.
    • Links: probe hidden_files/probe-2026-10-29-laneB-supaoverlay/probe.ts; issue body file hidden_files/issue-code-supaoverlay-gate-body.md
  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  4. 16 remaining items

  5. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 2, 2026
  6. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  7. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 2, 2026
  8. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Lane E resumed task 5410: PR #4230 head fd268a0 revalidated — MERGEABLE, Codex review on this head completed with no findings, bun test test/supabase-export.test.ts 12/12 documented in the task checkpoint. The pm-aggregator test check is QUEUED (infra stall pattern). Merging per standing merge authority.

  9. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Done: PR #4230 merged (merge commit 6cdfaee). applySupabaseExport now rejects corrupt measurements at the overlay boundary: non-string/invalid measuredAt makes the doc a no-op, negative used/limit rows are dropped, negative billedUsd becomes null while other valid overlay data applies. Codex clean; bun tests 12/12. Closing as complete.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/aggregatorPackage/app: aggregator (pm-stack in this repo)app/pm-stackpm-stack (Pad/Grist) in developing-today/codeeffort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions