You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
pi-web authenticates everything with one static bearer token (PI_WEB_TOKEN). One secret doing every job badly:
Static: never rotates, no expiry; revocation = re-key every device.
In URLs: WebSocket connects with ?token= (src/app/api.tswsUrl()), initial page load accepts ?token=, and the QR share hands out the permanent token — history/log leakage by design.
In localStorage (pi-web-token): readable by any same-origin script.
Cannot attach to browser-native loads (<img>, <video>, <iframe src>, downloads) — which is why /api/artifacts/ is served before the auth gate (server.ts — serveArtifact runs ahead of isAuthorized). Consequence: anyone with network reach can read .pi/web/artifacts/** of known cwds without the token. With tunneling in the product, that is a real exposure.
One credential for all devices: no per-device visibility or revocation.
Patching the artifact route alone relocates the problem. This RFC replaces the model.
Design: three layers, one per job
Layer 1 — Identity: device sessions with pairing
Every browser is a paired device with a server-side record { id, name, created, lastSeen, revoked }.
Bootstrap: server prints a one-time login URL (short-lived code) to the terminal — the Jupyter pattern. No static env token as the front door.
Additional devices: an authenticated device mints a single-use, short-TTL pairing code/QR (replaces the current permanent-token QR: buzzing someone in once vs copying the house key).
Revocation: per device, from a Devices settings panel (list, rename, last-seen, revoke).
Layer 2 — Transport: HttpOnly cookie, opaque and rotating
Pairing sets an HttpOnly cookie holding an opaque random session id (hash stored server-side — nothing signed to steal; lookup = instant revocation), SameSite=Lax, Secure with localhost exemption, sliding expiry with rotation.
Consumer
Today
Under cookie auth
fetch() API calls
Bearer header
cookie (header dies)
WebSocket upgrade
?token= in URL
cookie + Origin check
<img> in chat, <video>, downloads
exempt route
authenticated ✓
Artifact preview iframe document
exempt route
authenticated ✓ (ancestor chain is the same-site app, so Lax attaches)
Sub-resources inside sandboxed artifact HTML
exempt route
✗ irreducible gap — below
Automation / curl / CI
shared static token
Layer 3
CSRF arrives with cookies and is handled at the same moment: SameSite=Lax + require the existing x-pi-web-client-id header on all mutating routes + Origin validation on WS. Cross-origin pages cannot add custom headers without a CORS preflight that is never granted.
Layer 3 — Programmatic access: managed API tokens
Named, mintable, revocable API tokens (hashed at rest, settings UI), Bearer-in-header only, never in URLs. PI_WEB_TOKEN survives one release as a compat alias, then at most seeds the first pairing.
The irreducible gap, decided deliberately
A sandboxed iframe without allow-same-origin has an opaque origin, which nulls the site-for-cookies chain: Lax/Strict cookies do not attach to its sub-resource fetches. SameSite=None collides with third-party cookie blocking; allow-same-origin is a non-starter (model-authored HTML would gain same-origin access to the app); service workers cannot intercept opaque-origin clients. Every design meets this fork, so choose consciously:
Artifact documents and all user content: always authenticated. The actual exposure is closed.
Artifact HTML referencing sibling artifacts (user data fetched from an opaque origin) becomes unsupported-by-design: inline your data. One line in the artifact docs.
Migration (no flag-day)
Add: device sessions + cookie issuance; all routes accept cookie or legacy token; Devices settings panel; pairing QR.
Switch defaults: frontend drops Bearer/localStorage and WS ?token=; token-share QR becomes pairing QR; PI_WEB_TOKEN only bootstraps a first pairing.
Close the hole: artifact route requires the cookie; /api/web-assets/ ships as the narrow public surface; legacy token acceptance removed. The e2e token.spec.ts suite becomes the pairing/cookie suite.
Residual risks (stated, not hidden)
Extension-rendered HTML in the main DOM stays trusted-by-design (extension trust model, not an auth gap — and why HttpOnly matters).
A hostile process on the same machine was never in this tool's threat model and still is not.
Browser matrix for the opaque-origin cookie behavior should be verified explicitly (Chrome/Safari/Firefox) during phase 3.
Motivation
pi-web authenticates everything with one static bearer token (
PI_WEB_TOKEN). One secret doing every job badly:?token=(src/app/api.tswsUrl()), initial page load accepts?token=, and the QR share hands out the permanent token — history/log leakage by design.localStorage(pi-web-token): readable by any same-origin script.<img>,<video>,<iframe src>, downloads) — which is why/api/artifacts/is served before the auth gate (server.ts—serveArtifactruns ahead ofisAuthorized). Consequence: anyone with network reach can read.pi/web/artifacts/**of known cwds without the token. With tunneling in the product, that is a real exposure.Patching the artifact route alone relocates the problem. This RFC replaces the model.
Design: three layers, one per job
Layer 1 — Identity: device sessions with pairing
Every browser is a paired device with a server-side record
{ id, name, created, lastSeen, revoked }.Layer 2 — Transport: HttpOnly cookie, opaque and rotating
Pairing sets an
HttpOnlycookie holding an opaque random session id (hash stored server-side — nothing signed to steal; lookup = instant revocation),SameSite=Lax,Securewith localhost exemption, sliding expiry with rotation.fetch()API calls?token=in URL<img>in chat,<video>, downloadsCSRF arrives with cookies and is handled at the same moment:
SameSite=Lax+ require the existingx-pi-web-client-idheader on all mutating routes + Origin validation on WS. Cross-origin pages cannot add custom headers without a CORS preflight that is never granted.Layer 3 — Programmatic access: managed API tokens
Named, mintable, revocable API tokens (hashed at rest, settings UI), Bearer-in-header only, never in URLs.
PI_WEB_TOKENsurvives one release as a compat alias, then at most seeds the first pairing.The irreducible gap, decided deliberately
A sandboxed iframe without
allow-same-originhas an opaque origin, which nulls the site-for-cookies chain:Lax/Strictcookies do not attach to its sub-resource fetches.SameSite=Nonecollides with third-party cookie blocking;allow-same-originis a non-starter (model-authored HTML would gain same-origin access to the app); service workers cannot intercept opaque-origin clients. Every design meets this fork, so choose consciously:/api/web-assets/, cf. Vend bundled Mermaid to standalone artifacts via /api/vendor/ (offline diagrams) #27's/api/vendor/): deliberately public. Published OSS bytes at unguessable hashed URLs; nothing to protect. A narrow, stated exemption replacing today's broad accidental one — and exactly what sandboxed chart/diagram artifacts need (RFC: unified extension contribution kernel (contributions × slots × kinds) #82 assets milestone, Vend bundled Mermaid to standalone artifacts via /api/vendor/ (offline diagrams) #27 Mermaid vendoring).Migration (no flag-day)
?token=; token-share QR becomes pairing QR;PI_WEB_TOKENonly bootstraps a first pairing./api/web-assets/ships as the narrow public surface; legacy token acceptance removed. The e2etoken.spec.tssuite becomes the pairing/cookie suite.Residual risks (stated, not hidden)
HttpOnlymatters).Open questions
/api/session-artifacts/needs per-session scoping rules beyond the cookie (viewer-scoped artifacts).Related: #82 (assets milestone auth line item — resolved as a corollary), #27 (vendored libs = same public-assets surface), #45 (multi-user identity).