Skip to content

meta-exporter: subscription-quota timestamps are never range-checked — an ms-scale resets_at exports a year-58790 reset as a successful reading, and an out-of-Date-range value throws a bare RangeError misclassified as fetch-failed #2460

Closed

Description

@dezren39

meta-exporter: subscription-quota timestamps are never range-checked — an ms-scale resets_at exports a year-58790 reset as a successful reading, and an out-of-Date-range value throws a bare RangeError misclassified as fetch-failed

Evidence (verified this run, main 5a34fa70)

pm-stack/meta-exporter/meta-quota-export.mjs, parseSubscriptionQuota (lines 61–97):

  • The numeric validation loop (line 76) checks only Number.isFinite(v) || v < 0 for window_resets_at / weekly_resets_at. There is no upper bound and no plausibility window.
  • iso (line 87) is new Date(epochSecs * 1000).toISOString() — unbounded.
  • as_of is not in the validated field set at all; consoleAsOf (line 90) calls the same iso() whenever num(q.as_of) is finite.

Probe (node, importing the real module from main):

input result
window_resets_at = 1793090405000 (millisecond-scale, finite, ≥ 0 — passes validation) exported window resetAt: "+058790-10-10T04:03:20.000Z" inside a status: "ok" success document
window_resets_at = 1e300 (finite, ≥ 0 — passes validation) bare RangeError: Invalid time value escapes parseSubscriptionQuota — err.status/err.exitCode are undefined, it is not an ExportError
as_of = 1e300 same bare RangeError from the consoleAsOf path

The escaped RangeError lands in main()'s catch-all, which wraps any non-ExportError as fetch-failed (exit 7). Per the file's own contract, shape drift is unparseable (exit 6): the header documents the failure taxonomy and the parser is documented "Throws on any shape drift — fail-closed, never partial zeros."

Why it matters

This exporter's whole design is fail-closed: "a failed tick can never be mistaken for a reading." Both failure modes break that contract:

  1. A units drift at the Meta console API (seconds → milliseconds is the classic one) silently exports a garbage resetAt as a successful reading; the quota gatherer (feat(quota-plane): quota-gatherer service in the PM stack — 15-minute tick, quota-current contract #1151) ingests these windows for reset/burn display, so the corruption propagates downstream as fact.
  2. The RangeError path writes a failure document with the wrong status (fetch-failed instead of unparseable), sending whoever investigates hunting a network/page problem when the payload shape drifted.

Acceptance criteria

  • parseSubscriptionQuota rejects epoch fields (window_resets_at, weekly_resets_at, and as_of when present) that do not produce a valid, plausible date — e.g. outside a sane window around now, or of millisecond magnitude — by throwing ExportError("unparseable", 6, …).
  • No bare RangeError escapes any parse* function: every shape drift surfaces as unparseable / exit 6.
  • New tests in meta-quota-export.test.mjs: ms-scale window_resets_at rejected; huge finite window_resets_at rejected with err.status === "unparseable"; huge as_of rejected (or explicitly pinned to null); the captured-payload test and all existing tests still pass (node --test pm-stack/meta-exporter/).

Dedup

#2175 is the CDP-plumbing timeout (transport, open); #2350 is the entry-point symlink check (open); #1566 (closed) added the window_duration_secs validation only. No open or closed issue covers the epoch/resets_at/as_of range validation. Sibling filed this run: the parsePaygBalance truncation issue (different function, different mechanism).

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
    on Oct 29, 2026
  2. developing-today commented on Oct 29, 2026

    @developing-today
    Contributor
  3. added
    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/pm-stackpm-stack (Pad/Grist) in developing-today/code
    on Oct 29, 2026
  4. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  5. added
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  6. 8 remaining items

  7. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    task/small-fixQueue packet kind: bounded fix with reproduction and targeted tests
    and removed
    task/triageQueue packet kind: verify, reproduce, and route this item
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  8. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  9. added a commit that references this issue on Nov 1, 2026
  10. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

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

Metadata

Metadata

Assignees

Labels

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 jobstatus:donetask/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