Skip to content

aggregator(feed): isSubscriptionRow accepts any string as renewsAt — 'banana', '2026-02-30' and '2026-13-01' pass sanitizeSubscriptions and render verbatim as renewal dates #3513

Closed

Description

@dezren39

Problem

isSubscriptionRow (pm-stack/aggregator/src/feed.ts:472-479) — the gate behind sanitizeSubscriptions (feed.ts:486-493), applied at the loadFeed boundary (feed.ts:507) — accepts any string as renewsAt:

(typeof v.renewsAt === "string" || v.renewsAt === null)

The only producer never emits such a value: extractSubscriptions (pm-stack/aggregator/src/saas-extract.ts:1063-1064) sets renewsAt only when parseIsoDate(...) !== null, otherwise null ("never a verbatim renewsAt"). The shared strict primitive already exists in the same aggregator: isIsoDate (src/validate.ts:113-115), which rejects impossible calendar dates instead of rolling them over.

The renderer trusts the gate: render.ts:2039 interpolates the string verbatim (escaped only) as · renews ${esc(s.renewsAt)}.

Verified this run (laptop probe, real code)

Probe: ~/git/fleet/probe-2026-11-01-laneB-subsdate/ on the laptop — verbatim copy of pm-stack/aggregator at main 5abd39a (feed.ts sha256 cf7ea03d…, validate.ts sha256 1f72539a…, matched to the sandbox clone; probe copy differs only by the #3452 closing brace, without which feed.ts does not parse). bun probe.ts output (probe.log):

  • control {renewsAt:"2026-12-01"} → isSubscriptionRow = true (correct)
  • control {renewsAt:null} → true (correct)
  • renewsAt:"banana" → true (isIsoDate = false)
  • renewsAt:"2026-02-30" → true (isIsoDate = false)
  • renewsAt:"2026-13-01" → true (isIsoDate = false)
  • renewsAt:"12/01/2026" → true (isIsoDate = false)
  • renewsAt:"2026-12-01banana" → true (isIsoDate = false)
  • sanitizeSubscriptions over the mixed set: all 7 rows kept, dropped: 0

So a corrupt or hand-edited feed renders renews banana, renews 2026-02-30 (a date that does not exist) etc. on /quota as if they were measured renewal dates, and /api/quota.json serves them verbatim — the exact falsified-date class the sibling gates were fixed for (#3458 charges at, #3428 burn from/to, #3427 quota resetsAt).

Why it matters

The subscriptions strip is renewal-only guidance: a bogus or impossible renewal date is presented as fact. The gate's own comment (feed.ts:469-471) says the row must be "safe to hand to the renderers" — a non-date renewsAt is not.

Acceptance criteria

  • isSubscriptionRow requires renewsAt to be null or a string passing isIsoDate (validate.ts) — "banana", "2026-02-30", "2026-13-01", "12/01/2026", and "2026-12-01banana" rows are dropped by sanitizeSubscriptions and counted in dropped.
  • Valid ISO dates and null rows pass unchanged (producer output from extractSubscriptions is unaffected).
  • Regression tests cover the five rejected spellings plus the two controls, following the existing feed-gate test pattern.

Dedup

Searched open + closed for isSubscriptionRow, subscriptions renewsAt: #2446 (closed) was the null-row crash that created this gate — it never checked the date's validity; #2206 (closed) fixed the producer side in saas-extract. No issue covers the feed gate accepting a non-date renewsAt. Distinct fields from #3458/#3428/#3427 (other gates, same file).

Activity

  1. developing-today commented on Nov 1, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/lowNice to have; do when convenient
    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/sSmall: expected under 2 hours of work
    on Nov 1, 2026
  3. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (lane B, slug new-issue-creator-code-b)
    • Session / ID: cron new-issue-creator-code-b, run 2026-11-01 04:50 CST
    • Found via: filed this run
    • Task: Triage the isSubscriptionRow renewsAt date gate (feed.ts:472) — gate accepts any string; producer only emits isIsoDate-valid or null
    • Expectation: effort/s; done = renewsAt must be null or isIsoDate-valid, bad rows dropped+counted by sanitizeSubscriptions, regression tests for the five rejected spellings
    • Links: queue packet packets/packet-3513-triage.json (hidden_files)
  4. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

    • Agent: repo-worker-code (OpenChamber scheduled lane)
    • Session / ID: OpenCode session ses_e5e64cc9646ffeeArQEaEu392o; queue task 5473 (triage, attempt 1)
    • Found via: claimed from repo-health-code-pilot queue
    • Task: Confirm the renewsAt date-gate gap on current main and route the fix packet with regression criteria.
  5. removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 2, 2026
  6. 6 remaining items

  7. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 2, 2026
  8. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Picked up small fix (queue task 6140, openchamber scheduled lane). Plan: gate renewsAt in isSubscriptionRow with the existing isIsoDate validator, add feed-gate regression tests, verify with bun test.

  9. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Done: fix implemented and verified locally, PR #4474 opened (queue task 6140 completed as pr_opened). CI pending on the PR — merge once checks are green per the standing criteria.

  10. added a commit that references this issue on Nov 3, 2026
  11. developing-today commented on Nov 3, 2026

    @developing-today
    Contributor

    agent-note:v1
    state: triaged (coverage pass)
    Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as triage at priority 30 (priority/low). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
    Found via: coverage pass 2026-11-03

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/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientqueue/queuedHas a queued task in the Supabase agent queuesource/creatorFiled by the new-issue-creator jobstatus/needs-triageNot yet triaged; needs a first looktask/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