PR #9651 review: CLI reads public pages from other organizations
Summary
superset pages get and pages pull now fall back to the public page.publicView procedure when the organization lookup refuses a page, so "Anyone with the link" pages work from outside their organization (SUPER-3431). Every pages command also accepts a page link. There is no API change; pull from outside downloads the public viewUrl and refuses versions other than the served one.
Verdict
Minor issues The fix works against production and the member path is unchanged, but the outsider pull writes the served document with the Paradev runtime injected, not the stored version, so the same command gives different bytes depending on who runs it.
Findings
Mediumpackages/cli/src/commands/pages/pull/command.ts:56
The fallback downloads page.viewUrl. The usercontent worker rewrites HTML on that path: it injects storage.*.js, tools.*.js, the theme <style> and runtime.js (apps/usercontent/src/index.ts:256-265). A member gets the stored bytes from a presigned blob URL. So pull ("Write a published version's HTML") returns a different document for outsiders. If that copy goes back through pages publish, the worker injects again: injectScriptTag is not idempotent (packages/shared/src/usercontent/inject.ts:44), and the page loads the runtime twice (shown under Evidence).
Fix: add a public procedure, for example page.publicPull({ slug }), with the same visibility, takedown and served-version gates as publicView, that returns a presigned downloadUrl for the served version. It is additive, so released clients are not affected (see trpc-compat). If the PR stays API-free, say so in pull's help and write a note to stderr that the output is the served document, not the source.
Lowpackages/cli/src/commands/pages/publicPage.ts:12
On page.get/page.pull, FORBIDDEN comes only from pageNotFound (packages/trpc/src/router/page/page.ts:191): "This page belongs to X. Switch to that organization to open it." For a public page, the fallback now returns the reduced public view instead, so a member of that other organization silently gets fewer fields (no canEdit, no workspaceLinks) and pull --version N says "Version N is not public" when switching organization would have worked.
Fix: on FORBIDDEN, keep the original message as the hint when the fallback is used (for example CLIError hint, or a stderr note in get), or only fall back on the pageNotFound i18n key.
Lowpackages/cli/src/commands/pages/pageRef.ts:6
The link parser drops the query string. The web viewer treats ?v=N as the version to show (apps/web/src/app/page/[slug]/page.tsx:65), so pages pull "https://app.superset.sh/page/x?v=2" silently writes the served version. pageRef.test.ts:9 encodes that drop.
Fix: return the v parameter from the link parser and use it as the default for pull --version (an explicit --version wins).
Lowpackages/cli/src/commands/pages/get/command.ts:14
pages get --json now returns two shapes: page.get output, or the smaller publicView output (no visibility, servedVersion, latestVersion, canEdit; version instead of servedVersion). A script cannot tell which one it received without probing fields.
Fix: add a discriminator such as access: "public" to the fallback result, or map it onto the page.get field names that exist (servedVersion, visibility: "everyone").
Nitpackages/cli/src/commands/pages/pageRef.ts:2
PAGE_PATH accepts /page/<x> on any host, so https://example.com/page/foo becomes slug foo. Harmless today (it just misses), but checking the host against env.SUPERSET_WEB_URL and the canonical web host gives a clearer error.
Evidence
Surface: CLI only. Ran the CLI from source (bunx cli-framework dev … in packages/cli) against production, signed in as home@superset.sh (org "Paradev"), which is outside the fixture page's organization. Compared with the released CLI 2.0.8 on PATH. No web, desktop or mobile surface changes.
Works: released CLI says "Page not found"; the PR's CLI returns the public page from a link.
$ superset pages get super-3431-the-cli-can-read-anyone-with-the-link-p-xszcbb (released 2.0.8)
Error: Page not found
[exit 1]
$ superset pages get https://app.superset.sh/page/super-3431-the-cli-can-read-anyone-with-the-link-p-xszcbb --json (source @ f70d46d3)
{
"id": "b7ba57b5-957d-4f96-9d50-6abf902a7121",
"slug": "super-3431-the-cli-can-read-anyone-with-the-link-p-xszcbb",
"title": "SUPER-3431: the CLI can read \"Anyone with the link\" pages",
"description": "Test page for the fix: run superset pages get and pull on this page's link from outside its organization",
"url": "https://app.superset.sh/page/super-3431-the-cli-can-read-anyone-with-the-link-p-xszcbb",
"updatedAt": "2026-11-11T23:42:42.336Z",
"version": 5,
"viewUrl": "https://b7ba57b5-957d-4f96-9d50-6abf902a7121.frame.supersetusercontent.com/versions/5/",
"thumbnailUrl": "https://b7ba57b5-957d-4f96-9d50-6abf902a7121.frame.supersetusercontent.com/versions/5/thumbnail.jpg"
}
[exit 0]
Works: pull from outside writes the served HTML. It contains injected runtime tags (finding 1):
$ superset pages pull https://app.superset.sh/page/super-3431-…-xszcbb > page.html (source @ f70d46d3) [exit 0] 13548 bytes written $ grep -n /_superset/ page.html 3: <head><script src="/_superset/storage.1a50vr1.js"></script><script src="/_superset/tools.1c6au4o.js"></script><style>:where(:root), 555: <script src="/_superset/runtime.js"></script></body>
Running the worker's injection on that copy again (what serving a republished copy does) doubles the runtime:
runtime.js tags in pulled public copy: 1 runtime.js tags after the worker serves it again: 2
Works: non-served version refused with a clear hint; missing pages still say "Page not found"; --version 5 (the served one) exits 0.
$ superset pages pull https://app.superset.sh/page/super-3431-the-cli-can-read-anyone-with-the-link-p-xszcbb --version 1 (source @ f70d46d3) Error: Version 1 is not public Hint: From outside its organization, only the version this page serves (5) can be read [exit 1] $ superset pages get no-such-page-xyz (source @ f70d46d3) Error: Page not found [exit 1] $ superset pages get https://app.superset.sh/page/no-such-page-xyz (source @ f70d46d3) Error: Page not found [exit 1]
Unchanged: a page in the caller's own organization still goes through page.get / page.pull, and pull output is byte-identical to the released CLI's.
$ superset pages get prs-merged-per-week-on-the-paradev-leaderboard-c43lah --json | jq '{slug, visibility, has canEdit, has workspaceLinks, servedVersion}' (source @ f70d46d3)
{"slug":"prs-merged-per-week-on-the-paradev-leaderboard-c43lah","visibility":"everyone","hasCanEdit":true,"hasWorkspaceLinks":true,"servedVersion":2}
$ superset pages pull prs-merged-per-week-on-the-paradev-leaderboard-c43lah (source) vs released 2.0.8
byte-identical: yes; /_superset/ tags in output: 0
Not run: the FORBIDDEN case (a public page in another organization the caller belongs to); this account is in one organization only.
Questions for the author
- Is the injected served document an intended result for outsider
pull, or should it match the stored version byte-for-byte like the member path? - Should
pages versionsget the same fallback (a one-entry list with the served version), soversions→pull --versionworks from outside? - For a member of the page's other organization, do you want the public copy or the "switch organization" message? Today the public copy wins without notice.
Test / CI notes
- At
f70d46d3(PR head):bun test src/commands/pages/inpackages/cli: 54 pass, 0 fail.tsc --noEmitforpackages/cli: clean.biome check packages/cli/src/commands/pages: clean. - CI at review time: Lint, Typecheck, Test, Sherif, Version Sync, Vale, Page storage integration, deploy previews all pass. Build CLI (linux-x64), Deploy Marketing and CodeRabbit were still pending. No failures.
- No tRPC, DB or i18n changes. The new CLI strings match the existing English-only
CLIErrormessages inpull.