Skip to content

Metadata API: GET /api/v1/meta/app?id= with an empty value ignores the filter and answers the caller's full app list #28548

Closed

Description

@objectstack-fleet

domain:devx#1 · sunhaolin · session_017xRBcUWM7N73xRVd64cyAE

QA-source: #28545 · platform-core.builtin-apps-nav-render · observed beside the clauses

An app read filtered by an empty id answers as if no filter were given. A caller who asked for one app by a name that came out blank gets every app it can see. Found by an off-clause probe and reproduced twice, once as a member and once as admin. The per-caller visibility gate runs first, so nothing leaks beyond the caller's own list. This is the same class as #27484 (a blank $searchFields read as absent), which was fixed.

Reproduction (stock showcase at eeec88d1)

  1. Sign in as admin (admin@objectos.ai), then GET /api/v1/meta/app?id= → 200 with [showcase_app, setup, account].
  2. Sign in as a member (for example phone.demo@example.com), then GET /api/v1/meta/app?id= → 200 with [showcase_app, account].
  3. Contrast: GET /api/v1/meta/app?id=%20 → {"items":[]}, and a repeated id parameter → 400.

Expected: an empty id is refused with a 400 naming the parameter, or matches nothing as the whitespace value does.
Actual: the empty value is treated as absent, and the unfiltered list is served.

Mechanism

packages/rest/src/meta-item-read-gate.ts, around lines 3536–3541: const appIdFilter = metaType === 'app' ? query?.id : undefined; followed by if (typeof appIdFilter === 'string' && appIdFilter !== ''). An empty string fails the guard and skips the filter.


Generated by Claude Code

Activity

  1. added
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    bugSomething isn't working
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Nov 1, 2026
  2. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 可对外的 API | none | P②

    Triage: p3 · bug · domain:server · area:api · 2026-11-01T05:56Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

    Claim: PM loop round 1 · card thread · 2026-11-01T06:55Z
    Session: session_01FEkQ5kE5BrPUPNMMyv6oCH (the seat); the card thread is session_01FEkQ5pQoUJCqKk6SJQb6Qx
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-cq2t0t (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#3
    Ruling-ref: the maintainer, 2026-11-01T03:46Z: 「server 车道现在就是你负责,任务有一大堆啊,都可以派,包括 p3」, with 2026-10-26T10:29Z 「p1 p2 处理完了,就可以处理 p3」, and 2026-11-01T06:39Z 「server 队列还是很多任务,再开一个席位吧」. Triage 6554161006 grades p3 and names #27484 as the same class.
    File surface: packages/rest/src/meta-item-read-gate.ts (the app id filter, and identical empty-as-absent filters in the same gate), pins that assert the old answer, tests, and a changeset.
    Container & model: S/M, mode:thread, model: opus (--tier names no path mandate: PM's call, default), effort: high
    Clause-②: no (narrowing) — expected if the #27484 precedent is a 400 refusal: a request that answered 200 is refused, so a contract review at CONTRACT_REVIEW_TIER is owed before the queue. If the dev follows a "matches nothing" precedent instead, the dev declares plain no.
    Metadata-protocol: none (expected; the dev measures and declares)
    Responsibility: the code is the REST meta read gate · the platform path is the per-caller visibility gate, which already runs first (no leak beyond the caller list) · reached by an API client whose app name came out blank
    Thread-read: 6554161006
    Premise: holds (read at origin/main 310cb96d7: if (typeof appIdFilter === 'string' && appIdFilter !== '') skips the filter for an empty string); the dev re-measures first
    Serial constraints cleared: none found; no open PR on packages/rest/src/meta-item-read-gate.ts at this read
    Resume-from: none — fresh from origin/main

    State: → pm:dispatched, assigned to os-project-manager, recorded by this claim. Order: pm-orders/server/order-28548.md (project folder).

  4. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-project-manager · session_01FEkQ5pQoUJCqKk6SJQb6Qx
    os-dev-report
    {
    "issue": 28548,
    "status": "done",
    "branch": "claude/project-thread-cq2t0t",
    "pr": "#28602",
    "session": "session_01FEkQ5pQoUJCqKk6SJQb6Qx",
    "premise_still_valid": true,
    "summary": "Premise held at origin/main 310cb96: createMetaListAnswer step 3 skipped the app filter for an empty id (typeof appIdFilter === 'string' && appIdFilter !== ''). An empty ?id= on /meta/app and /meta/apps is now refused 400 VALIDATION_ERROR with param 'id' and the message "Query parameter 'id' names no app; ..." by refuseEmptyAppIdFilter(), thrown in step 3 after the per-caller gate (step 2 keeps its place), so RestServer's handleRouteError and the dispatcher's errorFromThrown each shape it. Followed the precedent PR #27670 set for #27484 (400 VALIDATION_ERROR naming the parameter, not 'matches nothing'). A non-empty id (including ' '), an absent id, the repeated-id 400 and ?id= on other types are unchanged. One pin flipped on purpose (meta-app-publish-gate.test.ts 'an ABSENT id' asserted the empty id served the full list); no producer sends an empty id. The card assignee was set by the seat and not touched by this dev.",
    "tests": "Head 9246901. Red first on base code with the new tests: rest meta-app-publish-gate.test.ts 'an EMPTY id is refused' failed (expected 200 to be 400; 1 failed / 25 passed); runtime meta-list-projection-parity.test.ts row A 'the reference moves' failed (1 failed / 906 passed). Green after fix + rest rebuild: 26/26 and 907/907. pnpm --filter @objectstack/rest test :: exit 0 (Test Files 362 passed, Tests 6906 passed / 332 skipped). rest typecheck :: exit 0; runtime typecheck :: exit 0 (after building @objectstack/driver-turso, a test-only import whose missing dist gave TS2307 on the first run). eslint --no-inline-config packages/rest packages/runtime :: exit 0. Ablation: ablation-replace.mjs on packages/rest/src/meta-item-read-gate.ts, anchor the const appIdFilter line, replacement restoring the old skip with marker ABLATE_28548; anchor 1->0, blob 24ba18a4 -> acc770c9; rebuilt rest; ablation-dist-preflight hit dist/index.js + dist/index.cjs; red: rest case (caller holding [studio.access]: expected 200 to be 400) and runtime row A case; restored blob == HEAD 24ba18a4, git diff HEAD empty, rebuilt, preflight --absent clean over 6 files, tree clean.",
    "gates": "Local subset on 9246901: dispatch-gates --commands --repo objectstack-ai/objectstack derived 60 families; all 60 exit 0, exit codes captured before any pipe. First-pass non-verdicts re-run: check:dual-build-cjs-loads exit 3 (PREREQUISITE NOT MET, missing dists) -> exit 0 after building the remaining 7 dists; check:type-check-debt exit 99 (lock held by the dogfood battery) -> exit 0 under os-verify-lock. --ran reconcile: 60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN. CI families: PR #28602 check-runs, in_progress at report time.",
    "line_budget": "changed lines (dispatch-gates): +93 / -10 over 4 files, under the 3000 human-merge threshold",
    "files_changed": [
    ".changeset/28548-empty-app-id-refused.md",
    "packages/rest/src/meta-app-publish-gate.test.ts",
    "packages/rest/src/meta-item-read-gate.ts",
    "packages/runtime/src/domains/meta-list-projection-parity.test.ts"
    ],
    "deviations": "test:repo is not run as a whole locally; its one changed file (runtime meta-list-projection-parity.test.ts, repo project) was run (907/907), the rest belongs to CI Test Core. The commit and PR declare Clause-②: no (narrowing) and Metadata-protocol: none as the claim expected; the seat owes the contract review before the queue. No labels sent with pr_create: the order names none and the diff ships (@objectstack/rest patch changeset), so no skip-changeset.",
    "dogfood_battery": "9246901a · VERDICT command-exit 0 · Test Files 364 passed, 3 skipped (367) · Tests 2934 passed, 28 skipped (2962) · no failures",
    "mcp_calls": "0 — no MCP GitHub calls",
    "api_writes": "2 — git push of claude/project-thread-cq2t0t (not REST); relay pr_create (POST /repos/objectstack-ai/objectstack/dispatches, run 41285692629, opened #28602 draft, no assignees, no labels); this os-dev-report comment through post-stamped.mjs is the third write. Write budget spent: 3 of 3.",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: a · reach: unit-level only, no live door probe (createMetaListAnswer step 4, packages/rest/src/meta-item-read-gate.ts) · GET /meta/view?object= with an empty value fails the truthiness test query?.object and lists every view, unsorted and including views bound to no object, as if no filter were sent: the same empty-as-absent class as this card, a different guard spelling, outcome not ruled by the card, no producer sends an empty object · dedupe words: empty object filter view list, meta view object blank, view switcher empty object",
    "carrier: none · noted in PR #28602 Acceptance notes, not filed"
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

    Claim amendment to 6554674218 · 2026-11-01T07:43Z · file surface increment, declared

    The dev's report 6555083587 found the same class one guard further down: GET /meta/view?object= with an empty value fails the truthiness test query?.object (createMetaListAnswer step 4, packages/rest/src/meta-item-read-gate.ts) and lists every view as if no filter were sent. The seat folds it into PR #28602 instead of filing a card: same defect class, same file, mechanical, unclaimed, same gate family. Outcome: the same 400 refusal naming object, after the visibility gate.

    • File surface adds: the view object filter in the same gate and its tests.
    • Clause-②: no (narrowing) is unchanged; the contract review runs on the head that carries the fold.
  6. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-project-manager · session_01FEkQ5pQoUJCqKk6SJQb6Qx
    os-dev-report
    {
    "issue": 28548,
    "status": "done",
    "branch": "claude/project-thread-cq2t0t",
    "pr": "#28602",
    "session": "session_01FEkQ5pQoUJCqKk6SJQb6Qx",
    "premise_still_valid": true,
    "summary": "Patch round on the seat's direction (fold the empty ?object= finding). New head 9c45ac5 on top of 9246901. createMetaListAnswer step 4 now refuses an empty ?object= on /meta/view and /meta/views with 400 VALIDATION_ERROR, param 'object', message "Query parameter 'object' names no object; ...", through refuseEmptyListFilter (the helper the empty app id now shares, renamed from refuseEmptyAppIdFilter), after the per-caller gate. A named object still switches; ?object= on other types is still ignored. First round unchanged: empty ?id= on /meta/app and /meta/apps refused the same way, precedent PR #27670 for #27484. The PR body is NOT patched by the dev (os-dev.md: body written once); the v2 body text for the seat to write is at pm-orders/server/28548-pr-body-v2.md, and the changeset sentence is updated in the commit.",
    "tests": "Head 9c45ac5. Red on 9246901 with the new object tests: rest 'an EMPTY view object is refused' (view: expected 200 to be 400; 1 failed / 26 passed); runtime parity row A on the new ?object= assertion (1 failed / 934 passed). Green after fix + rest rebuild: meta-app-publish-gate + meta-view-dashboard-audience-gate 46/46; meta-list-projection-parity + dispatcher-query-arity 968/968. pnpm --filter @objectstack/rest test :: exit 0 (362 files, 6907 passed / 332 skipped). rest + runtime typecheck :: exit 0. eslint --no-inline-config packages/rest packages/runtime :: exit 0. Ablation object leg: anchor the new step-4 refusal line, replacement gating it behind (globalThis as any).ABLATE_28548B === 'on' so the empty value falls to the old truthiness skip; anchor 1->0, blob 35301758 -> 15af187f; rebuilt; preflight hit dist/index.js + dist/index.cjs; red on both suites (as above); restored blob == HEAD 35301758, git diff HEAD empty, rebuilt, --absent clean, tree clean. The id leg from round 1 (on 9246901) stands.",
    "gates": "Local subset on 9c45ac5: 60 derived families, all 60 exit 0, codes captured before any pipe. First-pass non-verdicts re-run after the parallel dogfood closure build settled: check:dts-closure exit 1 (dists being rewritten) -> exit 0 (239/239 declarations); check:dual-build-cjs-loads exit 3 (missing dists) -> exit 0; check:type-check-debt exit 99 (lock held by dogfood) -> exit 0. --ran: 60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN. CI families: PR #28602 check-runs on 9c45ac5, in_progress at report time.",
    "line_budget": "changed lines (dispatch-gates): +128 / -12 over 4 files, under the 3000 human-merge threshold",
    "files_changed": [
    ".changeset/28548-empty-app-id-refused.md",
    "packages/rest/src/meta-app-publish-gate.test.ts",
    "packages/rest/src/meta-item-read-gate.ts",
    "packages/runtime/src/domains/meta-list-projection-parity.test.ts"
    ],
    "deviations": "PR body not updated by the dev, against the seat's ask: os-dev.md says the dev writes the PR body once at pr_create and never PATCHes it; the seat writes later changes. v2 body at pm-orders/server/28548-pr-body-v2.md; suggested title: fix(rest)!: an empty ?id= on the app list or ?object= on the view list is refused 400 naming the parameter instead of read as absent. Comment 6555454646 was first posted with the stale round-1 report and edited in place. test:repo is not run whole locally; its one changed file was run. No labels on the PR (rest ships, patch changeset).",
    "dogfood_battery": "9c45ac50 · VERDICT command-exit 0 · Test Files 364 passed, 3 skipped (367) · Tests 2934 passed, 28 skipped (2962) · no failures (also green on 9246901)",
    "mcp_calls": "0 — no MCP GitHub calls",
    "api_writes": "This round: 3 — git push 9246901..9c45ac5 (not REST); comment 6555454646 through post-stamped.mjs, which first carried a stale copy of the round-1 report by a script slip and was then corrected in place by one relay comment_edit to this text. Round 1: push, relay pr_create (#28602), report comment 6555083587. PR body not patched.",
    "open_questions": [],
    "out_of_scope_findings": []
    }


    Generated by Claude Code

  7. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

    ACCEPT → PR #28602 comment 6555784146 · head 70a3f972 · 2026-11-01T08:56Z · landing sent (ready + auto-merge; contract review PASS 6555574011 on 9c45ac50, carried by a changeset-only move). The empty ?object= on the view list rides the same PR per claim amendment 6555101824.

  8. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

    Landing record · #28548 · PR #28602

    domain:server seat 3 · session_01FEkQ5kE5BrPUPNMMyv6oCH · 2026-11-01T10:10Z

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-28602-*' 0 refs (released)
    Ancestry git merge-base --is-ancestor f3f7b629 origin/main yes
    Content git grep -c refuseEmptyListFilter origin/main -- packages/rest/src/meta-item-read-gate.ts 5 hits
    • Landed: merged as f3f7b629; Fixes #28548 closed the card. An empty ?id= on the app list and an empty ?object= on the view list now answer 400 VALIDATION_ERROR naming the parameter, on REST and the runtime dispatcher alike, instead of the caller's whole visible list. Observation kept from contract review 6555574011: an empty ?package= still reads as no scope (not this card).
    • Claim 6554674218 closed by the merge. Resume-from: main at f3f7b629 or later.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpm:dispatchedpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions