Skip to content

aggregator(feed): isFleetFeed never checks subscriptions — one null row crashes /quota #2446

Closed

Description

@dezren39

Problem

isFleetFeed (pm-stack/aggregator/src/feed.ts:207-219) never validates
the optional subscriptions section at all — it checks only version,
generatedAt, quota.pools being an array, and attention being an
array. But the live source serves feed.subscriptions verbatim as the
usage view's flat-subscription rows, and the /quota renderer
dereferences every row unguarded, so one malformed row in a feed that
passes the gate crashes the whole Usage & billing page.

Evidence (verified this run, main 5a34fa70)

  • feed.ts:53-54: FleetFeed.subscriptions?: { name: string; plan: string; renewsAt: string | null }[] (same shape at model.ts:1280).
  • feed.ts:207-219: isFleetFeed contains no subscriptions check of
    any kind — not array-ness, not entry shape.
  • sources/live.ts:808-815: in usage()'s feed branch (taken whenever
    feed.saas is non-empty), the section is passed through verbatim:
    subscriptions: feed.subscriptions ?? [] — no filter or per-entry
    guard, unlike the registry / projects / snapshots consumers in the
    same file, which validate or filter first.
  • render.ts:1816-1826: the subscriptions strip maps rows with
    esc(s.name) / esc(s.plan) / s.renewsAt — a null row throws on
    s.name; a non-array string subscriptions throws on .map.
  • server.ts:486-487: /quota awaits source.usage() and calls
    renderQuota(...) with no catch, so the throw becomes a 500 for the
    entire usage hub page — not just a missing subscriptions strip.
    /api/quota.json (server.ts:656-665) does not crash, but serializes
    the malformed rows verbatim to API consumers.

Laptop Bun probe (~/git/fleet/probe-2026-10-29-laneA-feedcharges/, real
feed.ts + render.ts from main 5a34fa70, md5-identical, Bun 1.4.2;
usage view assembled exactly as live.ts:811-815 assembles it):

subscriptions isFleetFeed renderQuota (the real /quota render)
valid entry (control) true OK — row renders
[null] true throws TypeError: null is not an object (evaluating 's.name')
entry missing name true OK — renders with an empty name (no crash, silently degraded)
"oops" (non-array string) true throws TypeError: usage.subscriptions.map is not a function
[valid, null] mixed true throws — the valid row never renders

Why it matters

subscriptions is the hub's flat/renewal-only strip (Q5): the collector
emits these rows from the subscription records, so a single drifted or
hand-edited feed row takes down /quota entirely instead of dropping
one row, and the JSON twin silently serves the garbage onward. This is
the same failure class as #2427 (attention entries), #2429 (quota
pools), #2439 (chargesAttention) and #2445 (charges) at the same gate —
the fix should stay consistent with whatever layer those pin. This
issue is the subscriptions field specifically, with its own consumer
in renderQuota.

Acceptance criteria

Dedup

gh searches (open + closed) for subscriptions isFleetFeed and feed subscriptions validator returned no covering issue. Siblings at the
same gate: #2427 (attention), #2429 (quota pools), #2439
(chargesAttention), #2445 (charges — filed this run, distinct field and
consumer).

Activity

  1. developing-today commented on Oct 29, 2026

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

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  4. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

  5. 7 remaining items

  6. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done (triage)

  7. added
    queue/queuedHas a queued task in the Supabase agent queue
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    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

    • Agent: repo-worker-code (lane A)
    • Session / ID: queue task 5326
    • Found via: queue claim (small_fix packet, queue repo-health-code-pilot); shape pinned by triage task 3661
    • Work: implemented sanitizeSubscriptions in loadFeed (feed.ts) per the pinned rule — rows kept only with string name/plan and string-or-null renewsAt; non-array section dropped whole; drops counted as subscriptionsDropped alongside quotaDropped. PR opened; laptop tests: feed.test.ts 23/23, quota/quota-contract/usage 68/68.
    • Links: PR aggregator(feed): loadFeed sanitizes feed subscriptions — drop invalid rows, count drops (#2446) #3432 (head 9000a65)
  9. added a commit that references this issue on Nov 1, 2026
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)effort/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/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions