PR #9652 review: page.publicPull
Summary
Adds a public tRPC query page.publicPull({ slug }) that returns a presigned URL to the stored HTML of a page shared with "Anyone with the link". Today the only public read is page.publicView, which points at the usercontent worker, and the worker injects the theme, storage, tools and runtime scripts. A CLI that pulls from there and republishes would inject the runtime twice. The visibility and takedown check moves into isPubliclyServed(), and publicView and publicPull now share a loadPublicPage() lookup.
Verdict
Minor issues The approach is right and the change is additive only, so it is tRPC-compatible. The one thing worth changing before merge: the presigned URL lives for an hour on an unauthenticated endpoint, so a takedown does not take effect at once.
Findings
minorpackages/trpc/src/router/page/page.ts:1355: presigned URL outlives takedown and visibility changes
presignedGetUrl(row.storageKey) uses the default expiry of 1 hour (packages/trpc/src/lib/r2.ts:216). This procedure needs no auth, so anyone can mint a URL to the private bucket while a page is public, and that URL still works for up to an hour after a moderator takes the page down or the owner flips it to org. The worker's public path does not have this gap: it revalidates on every request (no-cache / PUBLIC_REVOCABLE, apps/usercontent/src/index.ts:189-201). For members, pull has the same 1-hour URL, but there the caller is an authenticated reader.
Fix: pass a short expiry. The CLI fetches the URL at once, so something like presignedGetUrl(row.storageKey, 60) is enough.
nitpackages/trpc/src/router/page/page.ts:1341-1367: version-row and presign block copied from pull
The pageVersions select, the presign try/catch, the log line and the pageContentIsNotAvailable error are the same as pull at page.ts:1243-1274. The only difference is how a missing row is reported (null here, NOT_FOUND in pull).
Fix: extract a helper, e.g. versionDownload(pageId, version): Promise<{ row, downloadUrl } | null>, next to latestVersionNumber. pull throws on null; publicPull returns it. The expiry from the finding above then lives in one place as a parameter.
nitpackages/trpc/src/router/page/shared-version.test.ts:44: only the predicate is tested
The new tests cover isPubliclyServed. Nothing covers the procedure itself: null for org, just_me and taken-down pages, the pinned sharedVersion, and a downloadUrl that points at the served version's key. I ran exactly that against a local Postgres (see Evidence) and it behaves correctly. If you want it in the repo, add a case to publish.integration.ts. Note that its r2 mock lacks listObjects, which lib/organization-plugins/bundles.ts imports through the router, so that file does not load at this head. This PR did not cause that gap. I added the mock locally to run it.
Evidence
Surface: API only (one new public tRPC query). No UI changes, so there are no screenshots.
1. Procedure against a local database (449c7068)
A throwaway integration test (not committed) built on the publish.integration.ts harness: Postgres 17 + the local neon HTTP proxy, all migrations applied, and r2 mocked as an in-memory store. It publishes v1 and v2, calls the procedure as an anonymous caller, and flips visibility, the pin and the takedown. Results: org page → null; everyone page → v2 with a downloadUrl whose key holds the stored bytes, not injected HTML; pinned sharedVersion=1 → version 1; publicView output unchanged; taken down → null for both; unknown slug → null.
bun test v1.4.3 (c6da4a4d3)
src/router/page/public-pull.review.integration.ts:
org page -> null
everyone page -> {
id: "fe8522cb-9a2f-4529-afbc-925a90f7b1e6",
slug: "public-pull-review-pn8yna",
title: "Public Pull Review",
version: 2,
contentType: "text/html",
sizeBytes: 11,
sha256: "3a6666dacddd1fc67d6d96e5f3315a628eaeeb9b325fd148e4510d199118ac1c",
downloadUrl: "https://storage.test/pages/fe8522cb-9a2f-4529-afbc-925a90f7b1e6/versions/2/index.html",
}
bytes behind downloadUrl -> <h1>v2</h1>
pinned v1 -> 1
publicView (unchanged) -> {
id: "fe8522cb-9a2f-4529-afbc-925a90f7b1e6",
slug: "public-pull-review-pn8yna",
title: "Public Pull Review",
description: null,
url: "https://app.superset.test/page/public-pull-review-pn8yna",
updatedAt: 2026-11-12T00:25:32.347Z,
version: 1,
viewUrl: "https://fe8522cb-9a2f-4529-afbc-925a90f7b1e6.usercontent.superset.test/versions/1/",
thumbnailUrl: null,
}
taken down -> null
missing slug -> null
1 pass
0 fail
4 expect() calls
Ran 1 test across 1 file. [9.16s]
2. Deployed preview API (api-pr-9652-superset.vercel.app)
The preview deploy has the route, and it returns null for an unknown slug. I did not test a real public page on preview, because I do not know of a public page in the preview database branch.
$ curl -G https://api-pr-9652-superset.vercel.app/api/trpc/page.publicPull --data-urlencode 'input={"json":{"slug":"does-not-exist-review-zz"}}'
{"result":{"data":{"json":null}}}
$ curl -G https://api-pr-9652-superset.vercel.app/api/trpc/page.publicView --data-urlencode 'input={"json":{"slug":"does-not-exist-review-zz"}}'
{"result":{"data":{"json":null}}}
Not run: the presigned URL against real R2, and the CLI from #9651 end to end.
Questions for the author
- The presigned R2 response is served as
text/htmlfrom the R2 domain, with no CSP. WithpublicPull, anyone can get one without an account. Is it worth addingResponseContentDisposition: "attachment"to theGetObjectCommandfor this path, so the link is a download and not a page someone can host? - Multi-file pages:
publicPullreturns only the entry document (same aspull). Does #9651 need the assets beside it too, or is the entry HTML enough for a republish? - A presign failure throws
NOT_FOUND, but every other miss returnsnull. Does the CLI handle both?
Test / CI notes
- All at head
449c706813e1. bun testinpackages/trpc: 899 pass, 0 fail.src/router/page: 170 pass.bun run typecheck(packages/trpc): clean.biome checkon the 3 changed files: clean.- GitHub CI: all checks pass (Lint, Typecheck, Test, Sherif, Page storage integration, Build CLI, Deploy API/Web/Docs/Marketing previews). cubic was skipped.
- tRPC compatibility: a new procedure only. No existing input, output or error code changes. The PR body says #9651 (the CLI caller) merges after this deploys, which matches rule 9 in
packages/trpc/AGENTS.md.