Skip to content

aggregator(feed): isFleetFeed never checks saas — one malformed service crashes usage() and /quota, a non-array silently switches the whole usage view to fallback #2451

Closed

Description

@dezren39

Problem

isFleetFeed (pm-stack/aggregator/src/feed.ts:207-219) never validates
the optional saas section at all — it checks only version,
generatedAt, quota.pools being an array, and attention being an
array. But saas is the section that gates the live source's whole
usage branch, and the services it carries are served verbatim into the
attention-flag builder and the /quota service cards, which
dereference every entry unguarded. One malformed entry in a feed that
passes the gate crashes the entire Usage & billing page — inside
usage() itself, before rendering starts.

Evidence (verified this run, main 5a34fa70)

  • feed.ts:42-43: FleetFeed.saas?: SaasService[] (shape at
    model.ts:1121-1147: string id/name/plan, metrics: UsageMetric[], conflicts: string[], paygEstimate object-or-null,
    numeric cadenceDays, etc.).
  • feed.ts:207-219: isFleetFeed contains no saas check of any
    kind — not array-ness, not entry shape.
  • sources/live.ts:808-815: usage()'s feed branch is taken whenever
    feed.saas is truthy with a non-zero length, and the section is
    passed through verbatim: services: feed.saas — no filter or
    per-entry guard, unlike the registry / projects / snapshots
    consumers in the same file, which validate or filter first.
  • sources/live.ts:832: usage() calls
    buildAttentionFlags(feed.quota, partial) while building its return
    value; derive.ts:823-848 dereferences svc.metrics and
    svc.paygEstimate.high per entry — a null entry or an entry missing
    metrics throws here, inside usage(), so every route that awaits
    usage() fails.
  • render.ts:1808-1809,1703-1720: renderQuota filters
    usage.services by s.group and each surviving card dereferences
    svc.metrics.filter(...), svc.conflicts.length,
    svc.billedUsd.toFixed(2), svc.paygEstimate.low|high.toFixed(2) —
    an entry with a valid group but missing conflicts throws in the
    render even though the flag builder passed it.
  • server.ts:486-487 (/quota), :492 (/quota/service/:id — also
    usage.services.find((s) => s.id ...)), :656-665
    (/api/quota.json): all await source.usage() with no catch, so
    the throw becomes a 500 for the whole hub page and both JSON twins.

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

saas isFleetFeed feed branch taken (live.ts:808) buildAttentionFlags (in usage()) renderQuota
valid entry (control) true yes OK OK — card renders
[null] true yes throws TypeError: null is not an object (evaluating 'svc.metrics') throws on s.group
"oops" (non-array string) true yes (.length 4) throws on svc.metrics (iterates chars) throws usage.services.filter is not a function
{...} (non-array object) true no — .length undefined, usage() silently falls back to direct extraction and the feed section is ignored n/a on the live path n/a on the live path
entry missing metrics true yes throws on svc.metrics throws on svc.metrics.filter
entry missing conflicts (valid group) true yes OK throws TypeError: undefined is not an object (evaluating 'svc.conflicts.length')
[valid, null] mixed true yes throws on svc.metrics throws — the valid card never renders

Why it matters

saas is the hub's core section (§2.3): the service cards are the
page, and this section is also the switch that selects the feed path
for the entire usage view — so a single drifted or hand-edited
collector entry doesn't just drop one card, it takes down /quota,
/quota/service/:id and /api/quota.json (and every other usage()
consumer), or — in the non-array-object case — silently discards the
feed's hub sections in favour of a stale direct read with no signal.
This is the same failure class as #2427 (attention), #2429 (quota
pools), #2439 (chargesAttention), #2445 (charges), #2446
(subscriptions) and #2450 (payg) at the same gate — the fix should
stay consistent with whatever layer those pin. This issue is the
saas field specifically, with its own consumers
(buildAttentionFlags + the service cards) and its branch-switch role.

Acceptance criteria

Dedup

gh searches (open + closed) for payg, isFleetFeed, saas feed
and isFleetFeed never checks returned no covering issue for the feed
saas gate. #1119 is a contract-test request for the
/api/quota.json saas/payg schema — related, but it does not track
this verified crash at the feed gate. Siblings at the same gate:
#2427 (attention), #2429 (quota pools), #2439 (chargesAttention),
#2445 (charges), #2446 (subscriptions), #2450 (payg — filed this run)
— each a 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 · picked up

  5. 31 remaining items

  6. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

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

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up (resume)

  9. dezren39 commented on Nov 3, 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/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/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