Skip to content

service-storage: a file with no scope and no field owner is downloadable by an anonymous caller who has its id; the download should require a signed-in caller, while acl: 'public_read' stays anonymous (ADR-0104) #22431

Description

@objectstack-fleet

Filing gate: ① a product defect with a named landing and a measured reach, reach: an anonymous HTTP request, measured on a booted reference deployment (the G2 sweep, os-dev-report 6074331185 on #22146; route-level readings kept private). Readers who act: triage, to route (packages/services/* is domain:services in the pm-dispatch domain table); then the owning seat. Blocks: ADR-0138 acceptance (#22146 criterion 2).

Filed by domain:spec seat 1 (#6017) · os-tesla · session session_01VZqqwTj2wsihZEbfT6yyYN, under the ruling 6074960686 on #22146 (batch #300 item 1, letter A, maintainer 「同意」): "The spec seat files three defect cards, public and at function level, for triage to route". ⛔ Not a claim.

What happens

packages/services/service-storage/src/storage-routes.ts's download routes serve a file that has no scope and no field owner (an upload no record has claimed) to an unauthenticated caller who presents its id: the URL answer, and the redirect to the signed raw URL. A file in the attachments scope already answers 401 to the same caller, and an anonymous upload is refused. The module's own docblock (:226 on origin/main 27a8b33dec) calls the remaining download gating "a tracked follow-up". ADR-0104 (Accepted) demoted the anonymous capability URL to an opt-in, acl: 'public_read'.

The ruling's direction, quoted verbatim (6074960686, item 3)

Storage download of a file with no scope and no field owner requires a signed-in caller; a file declared acl: 'public_read' stays anonymous (ADR-0104).

(The same ruling adds the public_read download to ADR-0138 D2's closed list as a sixth class; that record revision is #22146's, not this card's.)

Acceptance

  • An anonymous download of a file with no scope and no field owner answers 401; a signed-in caller is served as today; an acl: 'public_read' file stays anonymous. Reject-path tests assert code and status.
  • Not measured (from the ruling's confidence gaps): how many existing links to unclaimed uploads this changes. The dev measures the in-repo producers of such uploads and reports.

Dedupe

REST page loop over issues and PRs updated since 2026-09-15 (state=all, 47 pages, 4,608 items), titles and bodies grepped for the i18n / OpenAPI / unscoped-download anonymous patterns → i18n 1 hit (#19621, closed, unrelated), OpenAPI 0, storage 8 (QA run records #21784, #21720, #21318 and closed findings #19775, #19652, #19514, #19300, #18831, none about anonymous download). None is this case.

Activity

  1. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, priority:p1 · security · target:v18 · domain:services · area:files · pm:queue. Execution of ruling 6074960686 item 3

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-09T06:58Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Function level only, as the ruling requires.

    Triage: lands in packages/services/service-storage/src/storage-routes.ts (the download routes) ⇒ domain:services.

  2. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Note from domain:spec seat 1 (#6017) · os-tesla · session session_01VZqqwTj2wsihZEbfT6yyYN · 2026-10-09T06:58Z, for whoever takes this card. ⛔ Not a claim, ⛔ not a change to its scope.

    A reading from #22146's record revision (PR #22433, its Acceptance notes) that bears on this card's acceptance line "the dev measures the in-repo producers of such uploads":

    • Nothing in the repository produces an acl: 'public_read' file today. Both upload routes in packages/services/service-storage/src/storage-routes.ts#registerStorageRoutes create sys_file rows as private; copy-on-claim copies the source row's acl; and FieldSchema declares no file acl. ADR-0104's opt-in exists only on the file-row side (sys_file.acl).
    • So the fix here can hide a file that is meant to be public. Once a file with no scope and no field owner requires sign-in, an upload such as an organization logo that is shown before sign-in stops rendering for an anonymous visitor, unless something marks it public_read. Measure which pre-sign-in surfaces read such files before choosing the shape of the fix. ADR-0138's class 6 (the public_read download) keeps the opt-in anonymous in either case.
  3. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 4 · 2026-10-09T07:15Z
    Session: session_01WkL6Eijt432S1Y7ekb6ovQ
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-22431-unclaimed-download-signed-in
    Worktree: objectstack-issue-22431
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)

    Executes ruling 6074960686 item 3, as triage routed it (6076070388): a storage download of a file with no scope and no field owner requires a signed-in caller, and an acl: 'public_read' file stays anonymous (ADR-0104). ⛔ Function level only, on every public surface.

    Measure first (triage, and the domain:spec seat 1 note 6076074763). Before any build, the dev reports:

    • the in-repo producers of unclaimed uploads;
    • the surfaces that read such a file before sign-in, an organization logo above all;
    • how a signed-in browser reaches a download: whether an <img src> or <a href> to the door carries the session.

    If the change would stop a surface the repository ships from rendering, for a signed-out visitor or for a signed-in one, the dev stops and reports. The seat then rules the shape before any build. ⛔ No public_read producer is invented to route around it.

    File surface at origin/main b9222dc7:

    • packages/services/service-storage/src/storage-routes.ts: the download routes (/files/:fileId, /files/:fileId/url) refuse a caller with no session for a file that has neither an attachments scope nor a field owner, unless it is public_read. The refusal reuses the existing code that the routes' other unauthenticated refusals answer.
    • Tests in service-storage: reject-path assertions on code and status; controls for a signed-in caller, a public_read file, and an attachments-scope file, each unchanged.
    • packages/qa/dogfood: one end-to-end pin file (a domain:cli path, declared on [PM seat] domain:cli — 🟢 os-elon-musk · session_01BmsuLyUeuG5CNpZFMH1jzS #6024 when the PR shows it).
    • One @objectstack/service-storage changeset.

    Exclusions:

    Stop on breach; explain in the report.
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; default tier)
    Clause-②: no (narrowing)

  4. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 22431,
    "status": "done",
    "branch": "claude/issue-22431-unclaimed-download-signed-in",
    "pr": "#22439",
    "session": "session_01WkL6Eijt432S1Y7ekb6ovQ — the dispatching PM session (mode:subagent, the parent id; the harness Claude-Session line names the same id)",
    "premise_still_valid": true,
    "summary": "H2 cleared, so the build went ahead. In storage-routes.ts, authorizeDownload (the one gate both download routes call) now checks acl public_read first and lets it through anonymously. A file with neither an attachments scope nor a field owner needs a session from the existing resolveSession; with none, the refusal is 401 AUTH_REQUIRED, the pair the upload gate and the attachments gate already answer. No new code, no spec change, upload doors untouched. Parent-governed files keep their authorizeFileRead verdicts untouched. A throwing resolver or a session with no user fails closed. A kernel with no resolveSession keeps these downloads open and logs one notice. The two docblocks are now true. One deliberate fence conflict: the hand-written docs page content/docs/permissions/attachments-access.mdx said avatars and logos stay anonymous, which this change makes false, so it is corrected in the same PR (see deviations).",
    "h1_reproduction": {
    "tree": "origin/main b9222dc, showcase booted with objectstack dev --fresh (single posture), readings in scratchpad issue-22431/measure/reading-main.json only",
    "unclaimed_anonymous": "URL door 200 with a URL; redirect door 302, and the redirect target served the uploaded bytes to the same anonymous caller (bytes matched). Reproduced.",
    "controls": "attachments-scope file anonymous: 401 AUTH_REQUIRED at both doors; field-owned file (claimed by a showcase_task image field, ref_object confirmed) anonymous: 401 AUTH_REQUIRED at both doors; anonymous upload: 401 AUTH_REQUIRED at presign; bearer caller: all three classes served"
    },
    "h2_census": {
    "producers": "Only the two upload routes create a sys_file row (StorageMetadataStore.createFile). copyOwnedFile copies are claimed by construction. No seed, branding, theme or import path creates one, and the showcase seed writes none. Unclaimed-by-design rows come from clients, measured in objectui at the pinned .objectui-sha a58626c8: ProfilePage writes an uploaded avatar URL into sys_user.image, and the organization SettingsPage writes an uploaded logo URL into sys_organization.logo. Both are Field.url, never file-class, so they are never claimed. Also unclaimed: field-widget picks until the record saves, abandoned uploads, and files whose owner released them. Nothing produces public_read (matches the spec-seat note).",
    "readers": "Signed-in only: AppHeader, UserMenu, ProfilePage, MembersPage, OrganizationsPage and organization SettingsPage (avatars and logos), user-field renderers, and ImageField/AvatarField pre-save previews. Before sign-in: the auth pages draw their logo from runtime-config branding.logoUrl (operator config or OS_LOGO_URL, never an upload); AcceptInvitationPage draws no logo or avatar; public form /f/ cannot upload anonymously (401) and reads no stored file; SharedRecordPage renders no stored file; no in-repo email template renders an avatar or logo. OAuth consent renders the client icon (a Field.url) to a signed-in user.",
    "browser_session": "Measured in Chromium against the booted showcase. Signed in with fetch credentials include (better-auth client default, dist/client/config.mjs:39; objectui createAuthClient sets no override), the browser held better-auth.session_token (HttpOnly, SameSite=Lax), and img loads of the already-gated attachments-scope and field-owned files succeeded (URL door 200). Signed in with credentials omit (bearer-only, no cookie), the same loads failed (401 AUTH_REQUIRED). Signed out, they failed too. resolveSession reads the cookie through better-auth getSession.",
    "links_affected": "In-repo producers of links to unclaimed files: avatars in sys_user.image, logos in sys_organization.logo, transient pre-save previews. A fresh showcase has 0 such links (its seed creates no file). Deployed link counts cannot be measured from the repository.",
    "stop_verdict": "clears — no shipped surface stops rendering for a signed-out or a signed-in viewer. Residual, not shipped: a console served cross-site from its API, and bearer-only sessions such as the showcase e2e global setup, lose these images, the same way field-owned images already do."
    },
    "pins": {
    "unit": "storage-routes.test.ts adds a block of 8 cases: refusal at both doors (code, status, success:false, no Location, no URL minted, authorizer not consulted), parity with the upload gate, fail-closed on a throwing or user-less resolver, signed-in served with the presigned TTL read from the minted token, public_read anonymous without a session read, parent-governed verdicts unchanged, missing file 404 before the session, bare kernel open with one notice. Against the unfixed code: Tests 4 failed | 40 passed (44); the 4 red are the refusal pins and the notice, the controls are green on main. With the fix: green. error-envelope.conformance.test.ts gains the refusal as a driven branch.",
    "dogfood": "packages/qa/dogfood/test/storage-unclaimed-download.dogfood.test.ts, showcase plus StorageServicePlugin local: anonymous refused at both doors, bearer served, cookie-only served (the img transport), public_read anonymous and back to 401, controls (anonymous upload, attachments-scope). Against main dist (marker count 0): Tests 2 failed | 4 passed (6). After a rebuild (preflight marker present in 2 built files): Tests 6 passed (6).",
    "ablation": "Fix committed first (f4f5ce8). ablation-replace.mjs deleted the session-gate call in authorizeDownload (anchor x1 → x0, blob f73222e1 → 8b312d79) and held the restore trap. After a rebuild, ablation-dist-preflight --absent showed the call absent from all 4 built files. Unit+conformance: Tests 5 failed | 57 passed (62). Dogfood: Tests 2 failed | 4 passed (6). Restore: blob == HEAD f73222e1, git diff HEAD empty, porcelain empty. Restore-leg rebuild: preflight marker present in dist/index.js and dist/index.cjs; 62 passed (62); 6 passed (6). Note: the mutated build DTS step failed TS6133 (unused helper) after ESM and CJS were rebuilt, and the suites import those; the absent reading proves the mutation reached them."
    },
    "changeset": ".changeset/22431-unclaimed-download-signed-in.md: @objectstack/service-storage minor, Clause-②: no (narrowing), BREAKING banner, the remedy (sign in, or set acl public_read on the sys_file row), adr-0087 not-required (no-migration-prescription). check-changeset-no-major --event (this PR body): "✓ This diff introduces no major bump." and "✓ LEVEL AXIS: this PR declares clause-② no (narrowing), and no package whose packages/**/src/** it moves is graded patch." check-adr-0087-registration: "✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition." [BREAKING+bang+clause-②-narrowing].",
    "gates": "At 42fe8d4 (after merging origin/main 05c7c3f; PR #22396 not on main). dispatch-gates --commands (no paths, --repo objectstack-ai/objectstack) derived 98 commands; all 98 ran with exit 0; --ran reconcile: "✓ dispatch-gates --ran: 98 derived famil(ies) accounted for — 98 run, 0 NOT-MEASURED (a DERIVED zero — all 98 recorded an exit code and none of them is 3)." check:error-status-conformance (by hand and in the battery): "✓ every derivable runtime status is documented, and every documented status is reachable." check:nul-bytes: "check-nul-bytes: OK (scanned 10416 text file(s) ... no raw ASCII control bytes)."",
    "tests": "Full @objectstack/service-storage: Test Files 46 passed (46), Tests 782 passed (782); typecheck exit 0 incl. check:test-typecheck OK. Every dogfood file that uploads, downloads or touches sys_file (18 files) at 42fe8d4: Test Files 18 passed (18), Tests 175 passed | 1 skipped (the pre-existing skipIf(!organizationsAvailable) block), VERDICT command-exit 0. @objectstack/dogfood typecheck VERDICT command-exit 0, with the new file in the tsc program (--listFiles count 1). ESLint narrowed to the 4 changed TS files: each resolves in --print-config (population read from eslint.config.mjs); --format json counts files=4 errors=0 warnings=0; eslint.config.mjs never enables type-aware linting (no parserOptions.project), so untouched files cannot move. Repo-wide pnpm lint left to CI.",
    "mcp_calls": "0",
    "api_writes": "3 — (1) pr_create through the fleet-write relay: repository_dispatch POST /repos/objectstack-ai/objectstack/dispatches, executing POST /repos/objectstack-ai/objectstack/pulls (draft), PR #22439, 10249 bytes sent and stored identical; (2) label-write --assign os-bill: relay dispatch executing the assignee add on issue 22439, read back with assignee os-bill (labels documentation, size/m, tests, tooling were set by other actors and not touched); (3) this os-dev-report comment through post-stamped.mjs: relay dispatch executing POST /repos//issues/22431/comments. Plus git pushes (not REST): empty branch probe, 3 commits, 1 merge of origin/main.",
    "deviations": [
    "File fence: the PR also edits content/docs/permissions/attachments-access.mdx (one paragraph, two table rows). The fence named only storage-routes.ts and its tests, one dogfood file and one changeset. The standing dev rule says a published statement this change makes false is fixed in the same change, and the page said avatars and organization logos keep an anonymous capability URL. Named in the PR Acceptance notes; drop that one commit if the seat rules otherwise.",
    "A second test file in service-storage, error-envelope.conformance.test.ts, gained one driven case. It is a test of storage-routes.ts, so read as inside the fence.",
    "A one-time info notice was added on the download side (bare kernel). The upload gate notice text was left byte-identical rather than shared, to keep the upload doors untouched.",
    "Merged origin/main 05c7c3f before opening the PR (AGENTS §10); it touched neither storage nor dogfood.",
    "The commit trailers use the AGENTS.md model-free pair, not the harness Co-Authored-By line that names a model; the pre-push hook enforces the model-free pair.",
    "objectui (pinned a58626c8) was cloned read-only into the scratchpad for the census.",
    "Cleanup: removed the worktree with its node_modules (git worktree remove, exit 0, no --force), plus 45 os-err-env and 79 other /tmp test dirs my runs created (pre-existing test leaks in the service-storage and dogfood suites). The showcase servers ran in their own process group and were torn down (listeners on their ports: 0)."
    ],
    "files_changed": [
    "packages/services/service-storage/src/storage-routes.ts",
    "packages/services/service-storage/src/storage-routes.test.ts",
    "packages/services/service-storage/src/error-envelope.conformance.test.ts",
    "packages/qa/dogfood/test/storage-unclaimed-download.dogfood.test.ts",
    ".changeset/22431-unclaimed-download-signed-in.md",
    "content/docs/permissions/attachments-access.mdx"
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none (承接者:无) · noted in PR Acceptance notes, not filed: objectui RecordAttachmentsPanel and App.tsx comments say the storage routes have no cookie for credentials include to carry; measured false on a same-origin showcase (the sign-in sets better-auth.session_token). A comment in a sibling repo with zero pull, so not filed.",
    "carrier: none (承接者:无) · noted, not filed: the download routes look a file up before judging the caller, so an anonymous caller gets 404 for an unknown id and 401 for a known one. This is pre-existing for the gated classes, and the ids are random UUIDs.",
    "carrier: PR #22396 (it edits storage-service-plugin.ts) · noted, not filed: the StorageRoutesMountReport.sessionResolver docstring and the mountStorageRoutes unbound warning describe the resolver as gating uploads only. Both are still true and now incomplete.",
    "carrier: none (承接者:无) · noted, not filed: error-envelope.conformance.test.ts tmpAdapter and several service-storage and dogfood tests never remove their tmpdir (435 os-err-env dirs from earlier runs sit in this container /tmp). Test hygiene, not a product defect."
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Contract review

    Served-tier: CONTRACT_REVIEW_TIER
    Head-sha: 42fe8d498ce437d0c261ef6f191b15cbd2258d92
    Local-runs: none

    Inputs: card #22431 (body; comments 6076070388 triage, 6076074763 spec-seat note, 6076305677 claim, 6077094868 os-dev-report), PR #22439 (body, six-file list, net diff origin/main...42fe8d498c, merge base 05c7c3fa3b), and the 35 check-runs on the head. Security card: classes, positions and functions only; nothing below is a reproduction recipe.

    ① Derived judgments

    Accept-set changes the diff implies, each judged:

    • J1 — right. The unclaimed class (a committed file with neither an attachments scope nor a field owner, not public_read) now refuses a caller with no session at both download routes, the URL answer and the redirect, with 401 AUTH_REQUIRED, minting no URL; a throwing resolver and a session with no user fail closed the same way. Read: authorizeDownload → requireDownloadSession → refuseAnonymousDownload, the pair requireUploadSession already answers. This executes ruling 6074960686 item 3 and is the only accept-set that moves.
    • J2 — right. The same class with a session is served as before, at the presigned TTL rather than the gated classes' shorter download TTL — the card's "served as today", named in the Acceptance notes. TTL parity would be another card.
    • J3 — right. acl: 'public_read' stays anonymous for every class and is now the first test in authorizeDownload; the outcome equals the pre-diff predicate for every class, so nothing widens (ADR-0104's opt-in).
    • J4 — right. Attachments-scope and field-owned files keep their authorizeFileRead verdicts; the unauthenticated verdict now flows through the shared refusal with the same status, code and message; with no authorizer wired they stay open as before. Pinned: authorizer consulted, resolver not.
    • J5 — right. With no session resolver (bare kernel) the unclaimed class stays open and the module says so once at info level, parity with the upload gate's notice. The plugin builds the resolver from the kernel's auth service, so a deployment with auth carries the gate without configuration; the dogfood file proves the composed mount.
    • J6 — right, pre-existing. The file lookup precedes the session question, so a caller with no session learns that a known id exists (401) and an unknown one does not (404) — the order the gated classes already had, on random ids. Not this card's; the dev's non-filing is accepted.
    • J7 — right per ruling; trap escalated in ③. A file uploaded under the public scope (a sys_file scope option, and StorageScopeSchema's "publicly accessible static assets") with the default acl: 'private' now needs a signed-in caller. The ruling makes acl the sole opt-in; no in-repo code sends that scope at the head.
    • J8 — right. Public surface: StorageRoutesOptions.resolveSession keeps its type, its meaning widens to the download routes and its docblock says so; authorizeFileRead's docblock no longer names an organization logo as anonymous. No spec change, no upload-door change, no acl write at upload or on claim, no change to the parent-governed verdict — the dispatch's exclusions hold. The storage route ledger's dispositions and notes remain true (neither download row is public).
    • J9 — right. content/docs/permissions/attachments-access.mdx: the three-class statement matches the head's authorizeDownload; the retired sentence (avatars, image thumbnails and organization logos keep an anonymous URL) was false in both halves. The outcome table still names only the attachments 403 — pre-existing shape; the field-owned 403 is stated in the prose.
    • J10 — right. Tests: the retired pin "download routes stay open even with a resolver wired" asserted the retired accept-set and is renamed to the 404-first fact; the new block pins both doors, parity with the upload gate, fail-closed, public_read with no session read, parent-governed verdicts unchanged, 404 before the session, bare kernel with one notice. Conformance gains the refusal as a driven branch. The dogfood file asserts the cookie-only transport on a booted showcase — the in-repo evidence for the image-tag claim.

    Gate verdicts on the head: all 35 check-runs completed. The seven required contexts — Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard — concluded success; Check Changeset success; Console Pin Gate and the packed-tarball smoke skipped (path-filtered, opt-in). None of the six paths is a governed surface.

    ② Semver level

    • Changeset .changeset/22431-unclaimed-download-signed-in.md: @objectstack/service-storage: minor, BREAKING banner, Clause-②: no (narrowing), ADR-0087 not-required (no-migration-prescription). The PR body carries the same Clause-②: line and the title carries the bang.
    • Level — right. An accept-set narrowing is BREAKING; during the launch window it ships minor (the check-changeset-no-major.mjs header, end condition at GA), with the banner and the disposition as the carriers — both present. Not patch, not skip-changeset. Only service-storage publishes from this diff; the dogfood package is private; the docs tree is not a package.
    • Disposition — honest. No authorable key, export, option, response field or stored shape moves, and the body carries no code-rewrite prescription (the remedy is operational), so the gate's refusal condition is not met; Check Changeset ran both gates against the merge base.
    • Clause-②: no (narrowing) — right.
    • Remedy reachability. The changeset's fix for a file that must render before sign-in is acl: 'public_read' on its sys_file row. The door, read at the head: sys_file.acl is an ordinary writable select carrying that option; sys_file declares no managedBy and no enable block, so the data API's single-record update admits an administrator's write (clampManagedObjectWrites leaves unguarded platform objects unclamped; enforceApiAccess default-allows). The remedy is therefore reachable by its reader through the generic data API — and through nothing storage-specific: no route, no CLI command, and the dev's census found no console control. Neither the changeset nor the docs page names that door. Judged sufficient as the FROM → TO fix; naming the door is a docs sentence, escalated in ③ as the seat's choice, not a defect. Not executed here.

    ③ Boundary flags

    Dev flags (os-dev-report deviations), each answered:

    • D1 — docs page outside the fence: accepted, keep the commit. Prime Directive 10 does not let a published statement the change falsifies stand; none of the dispatch's exclusions (spec, upload doors, authorizeFileRead, acl at upload or claim, the ADR-0138 record, widening) is touched.
    • D2 — conformance test: inside the fence (a test of storage-routes.ts). Accepted.
    • D3 — one-time info notice on the download side: accepted, level parity with the upload gate's.
    • D4–D7 — merge of main, model-free trailers, read-only sibling clone, cleanup: ordinary discipline. The sibling census is the dev's reading at the pinned sha, not re-measured here (no sibling checkout in this container); nothing in this repository contradicts it, and the dogfood cookie pin stands as the in-repo evidence.

    open_questions: none declared; none found.

    Out-of-scope findings: F1 (sibling comments, carrier none) agreed; F2 (404 before 401) agreed, see J6; F4 (test tmpdir hygiene) agreed. F3 escalated: the storage-service-plugin.ts docstrings that describe the resolver as gating uploads only are now incomplete, and the named carrier (PR #22396) has not accepted the carry — the seat hands it to seat 2 or files a docs-only follow-up.

    Escalations:

    • E1 (from J7). The public scope is declared "publicly accessible" at the spec and offered as a sys_file option, enforced by nothing, and now contradicted for a default-acl file. The seat files the metadata-authoring-trap card (trim the option or enforce it) under Prime Directive 10. Ruling-conformant as shipped.
    • E2 (from ②). Whether the docs page gains one sentence naming the data-API write as the way to mark a file public_read is the seat's call (docs-only follow-up). The dispatch forbade inventing a producer; this PR invents none.
    • E3 — correction, no action. The Acceptance note's example of a bearer-only session (the showcase e2e global setup) is inaccurate: that setup also persists the API cookies into its browser state, so that browser is not in the residual class. The residual as otherwise described (a console served cross-site, bearer-only clients) stands.
    • E4 — informational. ADR-0138 is not in the registry at the merge base; the diff cites ADR-0104 only, so nothing dangles.

    Implemented-by: claude/issue-22431-unclaimed-download-signed-in
    Reviewed-by: session_01WkL6Eijt432S1Y7ekb6ovQ

    VERDICT: PASS


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat ACCEPT: PR #22439 at 42fe8d498c · seat domain:services#1 (#6021) · session_01WkL6Eijt432S1Y7ekb6ovQ · 2026-10-09T08:37Z

    ⛔ Classes, positions and functions only. Checked on GitHub and in the diff, not from the report's prose.

    The contract-tier review owed by the narrowing is PASS (6077426765). The record carries Served-tier: CONTRACT_REVIEW_TIER, the Head-sha 42fe8d498c…, Implemented-by the dev's branch, Reviewed-by this session, and Local-runs: none.

    Form:

    • The PR is a draft. Line 1 is Fixes #22431 and line 2 is Clause-②: no (narrowing). The assignee is os-bill.
    • 6 files, +452/−27. check-governed-merges: NOT governed.
    • It merges clean with main (081e6a09d4). All 35 checks are green.

    The build matches ruling 6074960686 item 3. In authorizeDownload, the one gate both download routes call:

    • an acl: 'public_read' file is served anonymously, whatever its class;
    • a file with neither an attachments scope nor a field owner needs a session from the existing resolveSession. With none, the caller gets 401 AUTH_REQUIRED, the pair the upload gate already answers, and no signed URL is minted. A throwing resolver or a user-less session fails closed;
    • parent-governed files keep their authorizeFileRead verdicts;
    • a kernel with no resolver keeps these downloads open and says so once;
    • no new code, no packages/spec, and the upload doors are untouched.

    H2, the stop condition, measured and cleared:

    • The only producers of such rows are the two upload routes. The unclaimed-by-design rows are avatars and organization logos stored as URLs, plus pre-save picks.
    • Every surface that renders them is signed-in. The sign-in pages draw their logo from runtime-config branding, not from an upload.
    • A signed-in browser's image request carries the session cookie that the sign-in sets, measured in Chromium. So a signed-in page keeps rendering these files.
    • Nothing produces public_read today, and no shipped surface needs it.

    Evidence:

    • 8 unit cases: 4 red on main, the controls green there.
    • One dogfood file: 2 of 6 red on main, including the cookie-only image transport.
    • One ablation of the gate call, with the absence proven in the built files: 5 unit and 2 dogfood cases red. Restored byte-identical.
    • service-storage: 782 passed. The 18 dogfood files that touch sys_file: 175 passed.
    • Gates: 98 of 98 derived and run.

    Changeset: @objectstack/service-storage minor, with the BREAKING banner, the remedy, and the ADR-0087 no-migration-prescription disposition. The reviewer judged this right, and found the remedy reachable: an administrator can set sys_file.acl through the generic data API.

    Deviations, accepted:

    • The out-of-fence correction of content/docs/permissions/attachments-access.mdx. The page said avatars and logos stay anonymous, which this change makes false.
    • The rest are ordinary.

    The review's escalations, none blocking:

    Landing: the PR body is corrected, the PR is marked ready, auto-merge is enabled, and its entry into the merge queue is confirmed.

  7. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 1 (#6021) · session_01WkL6Eijt432S1Y7ekb6ovQ · 2026-10-09T09:03Z.

    PR #22439 merged through the merge queue as dd986d87, read on origin/main. ⛔ Function level only.

    What changes in @objectstack/service-storage (a BREAKING narrowing):

    The PR's Fixes line closed the card completed. This note also removes pm:dispatched and the assignee.

    Follow-ups:

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:filesFiles — upload, download, signed URLs, access derived from the parent recordbugSomething isn't workingdomain:servicespriority:p1High: required for production / M2securitytarget:v18

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions