@parhelia/preview-ticket
v1.0.12951
Published
Opt-in per-person preview for rendering hosts: redeem Parhelia's one-time preview code, keep the ticket server-side, send it on preview reads.
Readme
@parhelia/preview-ticket
Opt-in per-person preview for rendering hosts (heads) framed by the Parhelia editor. The head's server redeems the
one-time code Parhelia puts on the frame URL, keeps the resulting ticket server-side behind a random HttpOnly; Secure;
SameSite=None; Partitioned handle cookie, removes the code with a 303 before anything renders, and sends the ticket on
its preview reads to the Parhelia preview gateway. The ticket never reaches the browser.
Register the head in Parhelia with per-person preview turned on (Settings › Rendering Hosts): its Head settings give the head the preview key and the gateway it reads through.
// proxy.ts (Next 16) — or `as middleware` in middleware.ts (Next 15.5+, Node.js runtime)
export { parheliaPreview as proxy } from "@parhelia/preview-ticket/next";
// App Router server code
import { readPreviewSession } from "@parhelia/preview-ticket/next/app";
const session = await readPreviewSession(); // null without a preview session
// The ticket's credential (derived from the preview key; never the key itself) goes in the CMS SDK's own credential slot.
if (session) fetch(gatewayUrl, { headers: { ...session.headers, Authorization: `Bearer ${session.credential}` } });
// Pages Router
import { readPreviewSessionFor } from "@parhelia/preview-ticket/next";
export const getServerSideProps = async ({ req, res }) => {
const session = await readPreviewSessionFor(req, res);
// ...
};A code is redeemed only in the editor's frame (the editor-bound redemption rule, redemptionContextRefusal): through the
rendering proxy a same-origin frame, framed directly a cross-site frame whose Referer is PARHELIA_ORIGIN (or the
editorOrigin option); anything else is refused by name (preview-frame-required, preview-editor-same-site,
preview-editor-origin-required) without calling Parhelia.
Environment: PARHELIA_PREVIEW_URL (Parhelia's base URL as the head's server reaches it; PARHELIA_ORIGIN when unset), PARHELIA_PREVIEW_KEY (the
host entry's previewKey), optionally PARHELIA_PREVIEW_ALLOW_HTTP=true for a non-loopback http: URL; for the reads,
PARHELIA_PREVIEW_STORE (the store id the gateway serves; PARHELIA_STORE when unset). A session's client takes all of them
from one call:
import { previewGatewayFromEnv } from "@parhelia/preview-ticket";
const gateway = previewGatewayFromEnv(); // { baseUrl, storeId }; throws the named preview-head-not-configured
const client = createClient({ baseUrl: gateway.baseUrl, apiKey: session.credential, headers: session.headers });The key never travels (preview protocol 2). The redemption is signed with the key (signRedemption: its id, a timestamp,
a fresh nonce, the origin of PARHELIA_PREVIEW_URL and the code; Parhelia refuses a tampered, stale, replayed or other-origin
redemption by name), and every gateway read carries session.credential, the ticket's credential derived from the key. A
Parhelia on protocol 1 (the key as a bearer) refuses this package's redemptions, and a protocol 2 Parhelia refuses the bearer
form by name (preview-redemption-unsigned): update the package and Parhelia together.
Entry points:
| Import | For |
|---|---|
| @parhelia/preview-ticket | the framework-neutral core: createPreviewKit, the store interface and in-memory store, redeemPreviewCode, previewGatewayFromEnv, previewGatewayUrl, redactParheliaPreview, ParheliaPreviewError; a head page's named states: previewRefusalOf, answerPreviewRefusal, and DeliveryReadRefused (a head's delivery client's refused read) with deliveryRefusal(inSession) (a named state inside a session only) |
| @parhelia/preview-ticket/next | parheliaPreview, withParheliaPreview, createParheliaPreview (custom store, JSS plugin form), readPreviewSessionFor, previewHeadersFor |
| @parhelia/preview-ticket/next/app | previewHeaders(), readPreviewSession() over cookies() |
| @parhelia/preview-ticket/fetch | a head whose server is a Web fetch handler (TanStack Start, React Router, Hono, Astro middleware): withParheliaPreview(handler, { basePath }), readPreviewSession(request), previewHeaders(request), createParheliaPreviewFetch (custom store) |
| @parhelia/preview-ticket/alpaca | alpacaPreviewReads({ axios, cache, cookies }) for @alpaca-headless App Router heads: routes the package's draft reads to the gateway as the person (resolve() for the page's route read, named refusals, a per-render edit_rev) |
