@8ux-co/eelzap 0.10.2 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/next.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/next.ts","../src/refs.ts"],"sourcesContent":["import { createElement } from 'react'\n\nimport { ZapPreview as ClientZapPreview, type ZapPreviewProps } from './boot/component'\nimport { isPreviewTokenShape } from './refs'\n\n/**\n * `@8ux-co/eelzap/next` — the draft-mode route, `isZapPreview()` and the\n * preview boot for a Next.js site (zap-cms-v2 §2.5 \"Draft values\", §2.7, §3.5).\n *\n * ```ts\n * // app/api/zap-preview/route.ts\n * import { cookies, draftMode } from 'next/headers'\n * import { createDraftModeRoute } from '@8ux-co/eelzap/next'\n *\n * export const GET = createDraftModeRoute({\n * siteKey: 'verdeorigen',\n * apiKey: process.env.ZAP_SECRET_KEY!,\n * draftMode,\n * cookies,\n * })\n * ```\n *\n * and `<ZapPreview siteKey=\"verdeorigen\" />` in the root layout, which tells\n * Zap's editor the route's path (`zap:ready`, §2.5); the editor joins it to the\n * frame's verified origin and loads `{origin}{route}?token=…&path=…`.\n *\n * ## What the route does, in order\n *\n * 1. Reads `token` and `path` from the query. A token not shaped like Zap's\n * (`zpt_` + 43 base64url characters) is refused (401) without a request.\n * 2. Refuses a `path` that could leave the site: it must start with one `/`,\n * carry no backslash or control character, resolve to this request's own\n * origin, and carry no token itself (400). An open redirect on the site's\n * own domain is the classic draft-mode hole; this closes it.\n * 3. Validates the token WITH ZAP (`GET /api/public/v1/preview/token`),\n * sending the site's own API key beside it, and Zap checks the token was\n * minted for the site that key belongs to. Without that binding, any Zap\n * account could mint a token for its own site and switch YOUR site into\n * draft mode, with YOUR server rendering THEIR drafts on your domain. The\n * site key alone cannot carry it: it is unique within a workspace only, so\n * anyone can name a site `verdeorigen` in a workspace of their own (P7\n * audit). The answer's site key is compared too. An unknown, expired or\n * revoked token, or another site's, is refused (401 / 403); Zap\n * unreachable is 502. Nothing is enabled on a refusal.\n * 4. Enables Next's draft mode and stores the token in its own cookie, so the\n * server can read drafts with it (`getValidPreviewToken`, which checks it\n * with Zap again; `getPreviewToken` only checks its shape). Both cookies are\n * `SameSite=None; Secure; Partitioned; HttpOnly`: the page renders inside\n * Zap's frame, a third-party context where anything else is dropped, and\n * Chrome keeps a partitioned (CHIPS) cookie under third-party cookie\n * restrictions. Safari blocks frame cookies regardless; the client's\n * cookieless fallback covers it (`previewHeaders` → `getValidPreviewToken`).\n * 5. Redirects (307) to `path`, WITHOUT the token, `Cache-Control: no-store`,\n * `Referrer-Policy: no-referrer`.\n *\n * `createDraftModeExitRoute` ends the preview: both cookies expired, same\n * attributes, and a redirect through the same `safeRedirectPath`.\n *\n * Next is not imported: `draftMode` and `cookies` are passed in, so the\n * helper works with any Next version that has them and is testable alone.\n */\n\n/** The cookie the token travels in after the exchange. */\nexport const PREVIEW_TOKEN_COOKIE = '__zap_preview'\n/** Next's own draft-mode cookie, re-issued partitioned. */\nexport const NEXT_DRAFT_COOKIE = '__prerender_bypass'\n/** The header the client's cookieless fallback sends (`previewHeaders`). */\nexport const PREVIEW_TOKEN_HEADER = 'x-zap-preview-token'\nexport const DEFAULT_ZAP_ORIGIN = 'https://zap.eel.software'\nexport const TOKEN_VALIDATION_PATH = '/api/public/v1/preview/token'\n/** The header the site's own API key travels in to the validation call. */\nexport const SITE_API_KEY_HEADER = 'X-Zap-Site-Api-Key'\n/** A preview token lives ten minutes (§3.5); its cookie no longer. */\nconst PREVIEW_COOKIE_MAX_AGE = 600\nconst VALIDATION_TIMEOUT_MS = 5000\n\ninterface DraftModeLike {\n enable(): void | Promise<void>\n}\n\ninterface CookieStoreLike {\n get(name: string): { value: string } | undefined\n}\n\nexport interface DraftModeRouteOptions {\n /** The Zap site's key: a token minted for any other site is refused. */\n siteKey: string\n /**\n * One of the site's own API keys (the secret key the server already reads\n * with, or its public key). Zap refuses a token minted for any other site\n * than this key's. Server-side only; it is sent to Zap and nowhere else.\n */\n apiKey: string\n /** `draftMode` from `next/headers`. */\n draftMode: () => DraftModeLike | Promise<DraftModeLike>\n /** `cookies` from `next/headers`, to re-issue Next's draft cookie partitioned. */\n cookies?: () => CookieStoreLike | Promise<CookieStoreLike>\n /** Where Zap answers. Default `https://zap.eel.software`. */\n zapOrigin?: string\n /** Injected in specs. */\n fetch?: typeof fetch\n}\n\nexport type DraftModeRefusal =\n | 'invalid_token'\n | 'invalid_path'\n | 'token_refused'\n | 'wrong_site'\n | 'zap_unreachable'\n\nconst NO_STORE = { 'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer' }\n\nfunction refuse(status: number, error: DraftModeRefusal): Response {\n return Response.json({ error }, { status, headers: NO_STORE })\n}\n\n/** A 307 to `path` (already `safeRedirectPath`ed) that sets `cookies`. */\nfunction redirect(path: string, cookies: string[]): Response {\n const headers = new Headers({ Location: path, ...NO_STORE })\n for (const cookie of cookies) headers.append('Set-Cookie', cookie)\n return new Response(null, { status: 307, headers })\n}\n\nfunction requireKeys(options: { siteKey?: string; apiKey?: string } | undefined): void {\n if (!options?.siteKey || !options.apiKey) {\n throw new Error('eelzap/next: siteKey and apiKey (the site API key) are required')\n }\n}\n\n// eslint-disable-next-line no-control-regex\nconst UNSAFE_PATH_CHARS = /[\\u0000-\\u001f\\u007f\\\\]/\n\n/**\n * The path to redirect to, or null when it could leave the site. Returned\n * as path + query + hash, never absolute, so the `Location` stays on the\n * host the browser asked.\n */\nexport function safeRedirectPath(raw: string | null, requestUrl: string): string | null {\n const value = raw ?? '/'\n if (value.length === 0 || value.length > 2048) return null\n if (!value.startsWith('/') || value.startsWith('//')) return null\n if (UNSAFE_PATH_CHARS.test(value)) return null\n let base: URL\n let target: URL\n try {\n base = new URL(requestUrl)\n target = new URL(value, base)\n } catch {\n return null\n }\n if (target.origin !== base.origin) return null\n // Dot segments can collapse into a leading `//` (`/.//evil.example`,\n // `/a/..//evil.example`), and a `Location` of `//evil.example` is\n // protocol-relative: another host. The resolved path must not start so.\n if (target.pathname.startsWith('//')) return null\n if (/zpt_/i.test(target.search) || /zpt_/i.test(target.hash)) return null\n return `${target.pathname}${target.search}${target.hash}`\n}\n\nfunction cookieHeader(name: string, value: string, maxAge?: number): string {\n const parts = [`${name}=${value}`, 'Path=/', 'HttpOnly', 'Secure', 'SameSite=None', 'Partitioned']\n if (maxAge !== undefined) parts.push(`Max-Age=${maxAge}`)\n return parts.join('; ')\n}\n\ntype ZapValidationOptions = Pick<DraftModeRouteOptions, 'apiKey' | 'zapOrigin' | 'fetch'>\n\ntype ZapVerdict =\n | { ok: true; siteKey: string; expiresAt: number | null }\n | { ok: false; status: number; code: string | null }\n\n/** An expiry Zap may answer beside the site (ISO string or epoch ms), else null. */\nfunction readExpiry(value: unknown): number | null {\n const at = typeof value === 'string' ? Date.parse(value) : value\n return typeof at === 'number' && Number.isFinite(at) ? at : null\n}\n\n/** Ask Zap whether the token is live, and for which site. */\nasync function validateWithZap(token: string, options: ZapValidationOptions): Promise<ZapVerdict> {\n const doFetch = options.fetch ?? fetch\n const origin = new URL(options.zapOrigin ?? DEFAULT_ZAP_ORIGIN).origin\n const response = await doFetch(`${origin}${TOKEN_VALIDATION_PATH}`, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${token}`,\n [SITE_API_KEY_HEADER]: options.apiKey,\n Accept: 'application/json',\n },\n cache: 'no-store',\n redirect: 'error',\n signal: AbortSignal.timeout(VALIDATION_TIMEOUT_MS),\n })\n if (!response.ok) {\n const body = (await response.json().catch(() => null)) as { error?: { code?: unknown } } | null\n const code = typeof body?.error?.code === 'string' ? body.error.code : null\n return { ok: false, status: response.status, code }\n }\n const body = (await response.json().catch(() => null)) as {\n site?: { key?: unknown }\n expiresAt?: unknown\n token?: { expiresAt?: unknown }\n } | null\n const key = body?.site?.key\n return typeof key === 'string'\n ? { ok: true, siteKey: key, expiresAt: readExpiry(body?.expiresAt ?? body?.token?.expiresAt) }\n : { ok: false, status: 502, code: null }\n}\n\nexport function createDraftModeRoute(options: DraftModeRouteOptions) {\n requireKeys(options)\n return async function GET(request: Request): Promise<Response> {\n const url = new URL(request.url)\n const token = url.searchParams.get('token')\n if (!isPreviewTokenShape(token)) return refuse(401, 'invalid_token')\n const path = safeRedirectPath(url.searchParams.get('path'), request.url)\n if (path === null) return refuse(400, 'invalid_path')\n\n let verdict: ZapVerdict\n try {\n verdict = await validateWithZap(token, options)\n } catch {\n return refuse(502, 'zap_unreachable')\n }\n if (!verdict.ok) {\n if (verdict.status >= 500) return refuse(502, 'zap_unreachable')\n return verdict.code === 'WRONG_SITE'\n ? refuse(403, 'wrong_site')\n : refuse(401, 'token_refused')\n }\n if (verdict.siteKey !== options.siteKey) return refuse(403, 'wrong_site')\n\n const draft = await options.draftMode()\n await draft.enable()\n\n const cookies = [cookieHeader(PREVIEW_TOKEN_COOKIE, token, PREVIEW_COOKIE_MAX_AGE)]\n // Next set its own draft cookie when `enable()` ran, without\n // `Partitioned`; the same value again, partitioned, is the copy Chrome\n // keeps inside a third-party frame.\n const bypass = options.cookies ? (await options.cookies()).get(NEXT_DRAFT_COOKIE)?.value : null\n if (bypass && /^[A-Za-z0-9._~-]+$/.test(bypass)) {\n // No longer than the token: past it the page would sit in draft mode\n // with nothing to read drafts with.\n cookies.push(cookieHeader(NEXT_DRAFT_COOKIE, bypass, PREVIEW_COOKIE_MAX_AGE))\n }\n return redirect(path, cookies)\n }\n}\n\ninterface DraftModeExitLike {\n disable(): void | Promise<void>\n}\n\nexport interface DraftModeExitRouteOptions {\n /** `draftMode` from `next/headers`. */\n draftMode: () => DraftModeExitLike | Promise<DraftModeExitLike>\n}\n\n/**\n * The route that ends a preview: `createDraftModeRoute`'s twin.\n *\n * ```ts\n * // app/api/zap-preview/exit/route.ts\n * import { draftMode } from 'next/headers'\n * import { createDraftModeExitRoute } from '@8ux-co/eelzap/next'\n *\n * export const GET = createDraftModeExitRoute({ draftMode })\n * ```\n *\n * Disables Next's draft mode, then expires BOTH cookies the enable route set,\n * with the same `SameSite=None; Secure; Partitioned; HttpOnly; Path=/`: a\n * partitioned cookie is only replaced by a partitioned one, so `disable()`\n * alone (Next's unpartitioned expiry) would leave the framed copy alive.\n * Redirects (307) to `?path=` when `safeRedirectPath` accepts it, else to `/`.\n *\n * Mount it at `{draftRoute}/exit` (`/api/zap-preview/exit` by default): the\n * live-site toolbar's sign-out calls exactly that path to drop the draft\n * cookie a draft session left (ADR 041 amendment 2026-10-06). Without it,\n * signing out still revokes the session's tokens at Zap, and the cookie\n * expires within its ten minutes.\n */\nexport function createDraftModeExitRoute(options: DraftModeExitRouteOptions) {\n if (!options?.draftMode) throw new Error('eelzap/next: draftMode is required')\n return async function GET(request: Request): Promise<Response> {\n const url = new URL(request.url)\n const path = safeRedirectPath(url.searchParams.get('path'), request.url) ?? '/'\n const draft = await options.draftMode()\n await draft.disable()\n return redirect(path, [\n cookieHeader(NEXT_DRAFT_COOKIE, '', 0),\n cookieHeader(PREVIEW_TOKEN_COOKIE, '', 0),\n ])\n }\n}\n\ninterface HeaderSource {\n get(name: string): string | null | undefined\n}\n\n/**\n * The preview token for this request, from the client's header (the\n * cookieless fallback) or the cookie the draft-mode route set; null when\n * neither carries a well-formed one. Pass it to the server SDK as the\n * credential (`createClient({ apiKey: token })`), which reads drafts with it.\n *\n * ```ts\n * import { cookies, headers } from 'next/headers'\n * const token = getPreviewToken({ headers: await headers(), cookies: await cookies() })\n * ```\n */\nexport function getPreviewToken(\n source: Request | { headers?: HeaderSource; cookies?: CookieStoreLike },\n): string | null {\n const request = source instanceof Request\n const fromHeader = source.headers?.get(PREVIEW_TOKEN_HEADER)\n if (isPreviewTokenShape(fromHeader)) return fromHeader\n const fromCookie = request\n ? readCookie(source.headers.get('cookie'), PREVIEW_TOKEN_COOKIE)\n : source.cookies?.get(PREVIEW_TOKEN_COOKIE)?.value\n return isPreviewTokenShape(fromCookie) ? fromCookie : null\n}\n\nexport interface ValidPreviewTokenOptions {\n /** The Zap site's key: a token minted for any other site is refused. */\n siteKey: string\n /** One of the site's own API keys, as for `createDraftModeRoute`. Server-side only. */\n apiKey: string\n /** Where Zap answers. Default `https://zap.eel.software`. */\n zapOrigin?: string\n /** Injected in specs. */\n fetch?: typeof fetch\n}\n\n/** A confirmed token is trusted this long at most, less when it expires sooner. */\nconst VALID_TOKEN_CACHE_MS = 60_000\n/** A refused token is re-asked after this long. */\nconst REFUSED_TOKEN_CACHE_MS = 10_000\nconst TOKEN_CACHE_MAX = 500\nconst tokenCache = new Map<string, { valid: boolean; until: number }>()\n\nfunction rememberVerdict(key: string, valid: boolean, until: number): void {\n tokenCache.delete(key)\n // Full: drop the oldest (a Map iterates in insertion order).\n if (tokenCache.size >= TOKEN_CACHE_MAX) tokenCache.delete(tokenCache.keys().next().value!)\n tokenCache.set(key, { valid, until })\n}\n\n/** Test-only: forget every cached verdict. */\nexport function __resetPreviewTokenCacheForTests(): void {\n tokenCache.clear()\n}\n\n/**\n * `getPreviewToken`, checked with Zap: the token for this request only when\n * Zap confirms it is live AND was minted for this site, else null.\n *\n * `getPreviewToken` accepts any well-formed `x-zap-preview-token` header, and\n * anyone can send one: a token minted for their own Zap site, or one that\n * expired or was revoked. **Use this on every request that serves drafts**,\n * so a stranger's token never makes your server render drafts. It asks\n * `GET /api/public/v1/preview/token` exactly as the draft-mode route does\n * (the token as the bearer, your API key beside it) and compares the site\n * key Zap answers with `siteKey`.\n *\n * Verdicts are cached in memory per token, site and key, at most 500 of\n * them: a confirmed token for 60 seconds or until the expiry Zap answers,\n * whichever comes first; a refused one for 10 seconds. Zap unreachable is\n * null and not cached. The token is never logged.\n *\n * ```ts\n * import { cookies, headers } from 'next/headers'\n * const token = await getValidPreviewToken(\n * { headers: await headers(), cookies: await cookies() },\n * { siteKey: 'verdeorigen', apiKey: process.env.ZAP_SECRET_KEY! },\n * )\n * ```\n */\nexport async function getValidPreviewToken(\n source: Request | { headers?: HeaderSource; cookies?: CookieStoreLike },\n options: ValidPreviewTokenOptions,\n): Promise<string | null> {\n requireKeys(options)\n const token = getPreviewToken(source)\n if (token === null) return null\n\n // Keyed by everything the verdict depends on: one process may serve\n // several sites, and a token valid for one is not valid for another.\n const key = `${options.zapOrigin}\\n${options.siteKey}\\n${options.apiKey}\\n${token}`\n const now = Date.now()\n const cached = tokenCache.get(key)\n if (cached && cached.until > now) return cached.valid ? token : null\n\n let verdict: ZapVerdict\n try {\n verdict = await validateWithZap(token, options)\n } catch {\n return null\n }\n if (!verdict.ok) {\n if (verdict.status < 500) rememberVerdict(key, false, now + REFUSED_TOKEN_CACHE_MS)\n return null\n }\n if (verdict.siteKey !== options.siteKey) {\n rememberVerdict(key, false, now + REFUSED_TOKEN_CACHE_MS)\n return null\n }\n if (verdict.expiresAt !== null && verdict.expiresAt <= now) {\n rememberVerdict(key, false, now + REFUSED_TOKEN_CACHE_MS)\n return null\n }\n const until = Math.min(now + VALID_TOKEN_CACHE_MS, verdict.expiresAt ?? Number.POSITIVE_INFINITY)\n rememberVerdict(key, true, until)\n return token\n}\n\nfunction readCookie(header: string | null, name: string): string | null {\n if (!header) return null\n for (const part of header.split(';')) {\n const [key, ...rest] = part.trim().split('=')\n if (key === name) return rest.join('=')\n }\n return null\n}\n\n/**\n * The path `createDraftModeRoute` is documented at, and the one `ZapPreview`\n * announces to Zap's editor unless told otherwise (§2.5): mount the route at\n * `app/api/zap-preview/route.ts`, or pass `draftRoute` with its path.\n */\nexport const DEFAULT_DRAFT_ROUTE = '/api/zap-preview'\n\n/**\n * The preview boot for a Next.js site (§2.7): render it once, in the root\n * layout. It announces the draft-mode route (`/api/zap-preview` unless\n * `draftRoute` says otherwise; `null` for none) and loads the overlay only in\n * a preview session. Pass `preview={(await draftMode()).isEnabled}` so a\n * draft-mode page in its own tab boots too.\n *\n * ```tsx\n * import { draftMode } from 'next/headers'\n * import { ZapPreview } from '@8ux-co/eelzap/next'\n *\n * <ZapPreview siteKey=\"verdeorigen\" siteId=\"…uuid…\" preview={(await draftMode()).isEnabled} />\n * ```\n *\n * A server component that renders the client boot from `@8ux-co/eelzap/react`.\n */\nexport function ZapPreview(props: ZapPreviewProps) {\n return createElement(ClientZapPreview, {\n ...props,\n draftRoute: props.draftRoute === undefined ? DEFAULT_DRAFT_ROUTE : props.draftRoute,\n })\n}\n\n/**\n * True when this request carries a preview token in the shape Zap mints (the\n * client's header or the draft-mode cookie): a cheap hint for rendering\n * choices (skip a cache, render `ZapPreview` with `preview`). It does NOT\n * prove the token is live or yours; serve drafts only with\n * `getValidPreviewToken`.\n */\nexport function isZapPreview(\n source: Request | { headers?: HeaderSource; cookies?: CookieStoreLike },\n): boolean {\n return getPreviewToken(source) !== null\n}\n","/**\n * The record and field grammar of tags (`.docs/proposals/zap-cms-v2.md` §2.4),\n * in a module of its own so the light entries (`./fields`, `./react`, the\n * boot) can name records without pulling in the preview protocol.\n * No imports, no side effects.\n */\n\n/**\n * A record reference in its canonical string form: `collectionKey/slug` for an\n * entry, `doc:documentKey` for a document (the tag grammar, §2.4).\n */\nexport type RecordRef = string\n\n/** Zap's field key pattern. */\nexport const FIELD_KEY_RE = /^[a-z][a-z0-9_]{0,127}$/\n/** Entry and document references; keys and slugs are `[a-z0-9]+(?:-[a-z0-9]+)*`. */\nconst ENTRY_REF_RE = /^([a-z0-9]+(?:-[a-z0-9]+)*)\\/([a-z0-9]+(?:-[a-z0-9]+)*)$/\nconst DOC_REF_RE = /^doc:([a-z0-9]+(?:-[a-z0-9]+)*)$/\nconst MAX_REF_LENGTH = 300\n\nexport type ParsedRecordRef =\n | { kind: 'entry'; collection: string; slug: string }\n | { kind: 'document'; key: string }\n\nexport function parseRecordRef(value: unknown): ParsedRecordRef | null {\n if (typeof value !== 'string' || value.length > MAX_REF_LENGTH) return null\n const doc = DOC_REF_RE.exec(value)\n if (doc) return { kind: 'document', key: doc[1]! }\n const entry = ENTRY_REF_RE.exec(value)\n if (entry) return { kind: 'entry', collection: entry[1]!, slug: entry[2]! }\n return null\n}\n\nexport function isRecordRef(value: unknown): value is RecordRef {\n return parseRecordRef(value) !== null\n}\n\n/** `zpt_` + 32 bytes in base64url, as Zap mints it (zap-cms-v2 §3.5). */\nexport const PREVIEW_TOKEN_RE = /^zpt_[A-Za-z0-9_-]{43}$/\n\n/** A preview token in the shape Zap mints; says nothing about whether it is live. */\nexport function isPreviewTokenShape(value: unknown): value is string {\n return typeof value === 'string' && PREVIEW_TOKEN_RE.test(value)\n}\n\nexport function isFieldKey(value: unknown): value is string {\n return typeof value === 'string' && FIELD_KEY_RE.test(value)\n}\n\n/**\n * What `zapAttrs` and `onValues` accept: a record reference string\n * (`blog/hola`, `doc:home`) or a delivery API entry (an item carries\n * `collection.key` and `slug`, a document `key`).\n */\nexport type EntryLike =\n | RecordRef\n | {\n slug?: string | null\n key?: string | null\n collection?: { key?: string | null } | null\n localizedSlugs?: Record<string, string | null | undefined> | null\n }\n\n/** Every reference an entry answers to (default slug first, then localized ones). */\nexport function recordRefsOf(entry: EntryLike | null | undefined): RecordRef[] {\n if (!entry) return []\n if (typeof entry === 'string') return isRecordRef(entry) ? [entry] : []\n const refs: RecordRef[] = []\n const add = (ref: string) => {\n if (isRecordRef(ref) && !refs.includes(ref)) refs.push(ref)\n }\n const collection = entry.collection?.key\n if (collection) {\n if (entry.slug) add(`${collection}/${entry.slug}`)\n for (const slug of Object.values(entry.localizedSlugs ?? {})) {\n if (slug) add(`${collection}/${slug}`)\n }\n } else if (entry.key) {\n add(`doc:${entry.key}`)\n }\n return refs\n}\n\n/**\n * The tag for one field, as attributes to spread on the element that renders\n * it: `{ 'data-zap': 'blog/hola#title' }`. Empty when the entry or the field\n * key is not one Zap can name, so a bad call never breaks the page.\n */\nexport function zapAttrs(entry: EntryLike, fieldKey: string): { 'data-zap'?: string } {\n const [ref] = recordRefsOf(entry)\n if (!ref || !isFieldKey(fieldKey)) return {}\n return { 'data-zap': `${ref}#${fieldKey}` }\n}\n"],"mappings":"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","names":["body"]}
1
+ {"version":3,"sources":["../src/next.ts","../src/refs.ts","../src/preview/draft-path.ts"],"sourcesContent":["import { createElement } from 'react'\n\nimport { ZapPreview as ClientZapPreview, type ZapPreviewProps } from './boot/component'\nimport { isPreviewTokenShape } from './refs'\nimport { safeRedirectPath } from './preview/draft-path'\n\nexport { safeRedirectPath } from './preview/draft-path'\n\n/**\n * `@8ux-co/eelzap/next` — the draft-mode route, `isZapPreview()` and the\n * preview boot for a Next.js site (zap-cms-v2 §2.5 \"Draft values\", §2.7, §3.5).\n *\n * ```ts\n * // app/api/zap-preview/route.ts\n * import { cookies, draftMode } from 'next/headers'\n * import { createDraftModeRoute } from '@8ux-co/eelzap/next'\n *\n * export const POST = createDraftModeRoute({\n * siteKey: 'verdeorigen',\n * apiKey: process.env.ZAP_SECRET_KEY!,\n * draftMode,\n * cookies,\n * })\n * export const GET = POST\n * ```\n *\n * and `<ZapPreview siteKey=\"verdeorigen\" />` in the root layout, which tells\n * Zap's editor the route's path (`zap:ready`, §2.5); the editor joins it to the\n * frame's verified origin. Clients advertising `fragment-token` opt into\n * `{origin}{route}#token=…&path=…`; older clients keep query URLs.\n * GET serves a tiny page and a same-origin script that clears the fragment\n * and submits a form POST.\n * Fragments never travel in HTTP request URLs or site request logs.\n *\n * ## What the route does, in order\n *\n * 1. Requires same-origin evidence for POST (Sec-Fetch-Site or Origin), then\n * reads `token` and `path` from the form body. Legacy GET query tokens\n * remain supported. A token not shaped like\n * Zap's\n * (`zpt_` + 43 base64url characters) is refused (401) without a request.\n * 2. Refuses a `path` that could leave the site: it must start with one `/`,\n * carry no backslash or control character, resolve to this request's own\n * origin, and carry no token itself (400). An open redirect on the site's\n * own domain is the classic draft-mode hole; this closes it.\n * 3. Validates the token WITH ZAP (`GET /api/public/v1/preview/token`),\n * sending the site's own API key beside it, and Zap checks the token was\n * minted for the site that key belongs to. Without that binding, any Zap\n * account could mint a token for its own site and switch YOUR site into\n * draft mode, with YOUR server rendering THEIR drafts on your domain. The\n * site key alone cannot carry it: it is unique within a workspace only, so\n * anyone can name a site `verdeorigen` in a workspace of their own (P7\n * audit). The answer's site key is compared too. An unknown, expired or\n * revoked token, or another site's, is refused (401 / 403); Zap\n * unreachable is 502. Nothing is enabled on a refusal.\n * 4. Enables Next's draft mode and stores the token in its own cookie, so the\n * server can read drafts with it (`getValidPreviewToken`, which checks it\n * with Zap again; `getPreviewToken` only checks its shape). Both cookies are\n * `SameSite=None; Secure; Partitioned; HttpOnly`: the page renders inside\n * Zap's frame, a third-party context where anything else is dropped, and\n * Chrome keeps a partitioned (CHIPS) cookie under third-party cookie\n * restrictions. Safari blocks frame cookies regardless; the client's\n * cookieless fallback covers it (`previewHeaders` → `getValidPreviewToken`).\n * 5. Redirects (303 after POST; 307 for legacy GET) to `path`, without the\n * token, `Cache-Control: no-store`,\n * `Referrer-Policy: no-referrer`.\n *\n * `createDraftModeExitRoute` ends the preview: both cookies expired, same\n * attributes, and a redirect through the same `safeRedirectPath`.\n *\n * Next is not imported: `draftMode` and `cookies` are passed in, so the\n * helper works with any Next version that has them and is testable alone.\n */\n\n/** The cookie the token travels in after the exchange. */\nexport const PREVIEW_TOKEN_COOKIE = '__zap_preview'\n/** Next's own draft-mode cookie, re-issued partitioned. */\nexport const NEXT_DRAFT_COOKIE = '__prerender_bypass'\n/** The header the client's cookieless fallback sends (`previewHeaders`). */\nexport const PREVIEW_TOKEN_HEADER = 'x-zap-preview-token'\nexport const DEFAULT_ZAP_ORIGIN = 'https://zap.eel.software'\nexport const TOKEN_VALIDATION_PATH = '/api/public/v1/preview/token'\n/** The header the site's own API key travels in to the validation call. */\nexport const SITE_API_KEY_HEADER = 'X-Zap-Site-Api-Key'\n/** A preview token lives ten minutes (§3.5); its cookie no longer. */\nconst PREVIEW_COOKIE_MAX_AGE = 600\nconst VALIDATION_TIMEOUT_MS = 5000\n\ninterface DraftModeLike {\n enable(): void | Promise<void>\n}\n\ninterface CookieStoreLike {\n get(name: string): { value: string } | undefined\n}\n\nexport interface DraftModeRouteOptions {\n /** The Zap site's key: a token minted for any other site is refused. */\n siteKey: string\n /**\n * One of the site's own API keys (the secret key the server already reads\n * with, or its public key). Zap refuses a token minted for any other site\n * than this key's. Server-side only; it is sent to Zap and nowhere else.\n */\n apiKey: string\n /** `draftMode` from `next/headers`. */\n draftMode: () => DraftModeLike | Promise<DraftModeLike>\n /** `cookies` from `next/headers`, to re-issue Next's draft cookie partitioned. */\n cookies?: () => CookieStoreLike | Promise<CookieStoreLike>\n /** Where Zap answers. Default `https://zap.eel.software`. */\n zapOrigin?: string\n /** Injected in specs. */\n fetch?: typeof fetch\n}\n\nexport type DraftModeRefusal =\n | 'cross_origin'\n | 'invalid_token'\n | 'invalid_path'\n | 'token_refused'\n | 'wrong_site'\n | 'zap_unreachable'\n\nconst NO_STORE = { 'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer' }\n\nfunction refuse(status: number, error: DraftModeRefusal): Response {\n return Response.json({ error }, { status, headers: NO_STORE })\n}\n\n/** A redirect to `path` (already `safeRedirectPath`ed) that sets `cookies`. */\nfunction redirect(path: string, cookies: string[], status = 307): Response {\n const headers = new Headers({ Location: path, ...NO_STORE })\n for (const cookie of cookies) headers.append('Set-Cookie', cookie)\n return new Response(null, { status, headers })\n}\n\nfunction requireKeys(options: { siteKey?: string; apiKey?: string } | undefined): void {\n if (!options?.siteKey || !options.apiKey) {\n throw new Error('eelzap/next: siteKey and apiKey (the site API key) are required')\n }\n}\n\nfunction cookieHeader(name: string, value: string, maxAge?: number): string {\n const parts = [`${name}=${value}`, 'Path=/', 'HttpOnly', 'Secure', 'SameSite=None', 'Partitioned']\n if (maxAge !== undefined) parts.push(`Max-Age=${maxAge}`)\n return parts.join('; ')\n}\n\ntype ZapValidationOptions = Pick<DraftModeRouteOptions, 'apiKey' | 'zapOrigin' | 'fetch'>\n\ntype ZapVerdict =\n | { ok: true; siteKey: string; expiresAt: number | null }\n | { ok: false; status: number; code: string | null }\n\n/** An expiry Zap may answer beside the site (ISO string or epoch ms), else null. */\nfunction readExpiry(value: unknown): number | null {\n const at = typeof value === 'string' ? Date.parse(value) : value\n return typeof at === 'number' && Number.isFinite(at) ? at : null\n}\n\n/** Ask Zap whether the token is live, and for which site. */\nasync function validateWithZap(token: string, options: ZapValidationOptions): Promise<ZapVerdict> {\n const doFetch = options.fetch ?? fetch\n const origin = new URL(options.zapOrigin ?? DEFAULT_ZAP_ORIGIN).origin\n const response = await doFetch(`${origin}${TOKEN_VALIDATION_PATH}`, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${token}`,\n [SITE_API_KEY_HEADER]: options.apiKey,\n Accept: 'application/json',\n },\n cache: 'no-store',\n redirect: 'error',\n signal: AbortSignal.timeout(VALIDATION_TIMEOUT_MS),\n })\n if (!response.ok) {\n const body = (await response.json().catch(() => null)) as { error?: { code?: unknown } } | null\n const code = typeof body?.error?.code === 'string' ? body.error.code : null\n return { ok: false, status: response.status, code }\n }\n const body = (await response.json().catch(() => null)) as {\n site?: { key?: unknown }\n expiresAt?: unknown\n token?: { expiresAt?: unknown }\n } | null\n const key = body?.site?.key\n return typeof key === 'string'\n ? { ok: true, siteKey: key, expiresAt: readExpiry(body?.expiresAt ?? body?.token?.expiresAt) }\n : { ok: false, status: 502, code: null }\n}\n\n/** Static script: the token is read only in the browser, never reflected by the server. */\nconst FRAGMENT_EXCHANGE_SCRIPT = `(() => {\n const params = new URLSearchParams(location.hash.slice(1));\n history.replaceState(null, '', location.pathname);\n const token = params.get('token');\n if (!/^zpt_[A-Za-z0-9_-]{43}$/.test(token || '')) return;\n const form = document.createElement('form');\n form.method = 'POST';\n form.action = location.pathname;\n for (const [name, value] of [['token', token], ['path', params.get('path') ?? '/']]) {\n const input = document.createElement('input');\n input.type = 'hidden';\n input.name = name;\n input.value = value;\n form.appendChild(input);\n }\n document.body.appendChild(form);\n form.submit();\n})();`\n\nfunction fragmentExchangeScript(): Response {\n return new Response(FRAGMENT_EXCHANGE_SCRIPT, {\n headers: { ...NO_STORE, 'Content-Type': 'application/javascript; charset=utf-8' },\n })\n}\n\nfunction fragmentExchangePage(): Response {\n const html = `<!doctype html><html><head><meta charset=\"utf-8\"><title>Zap preview</title></head><body>\n<script src=\"?script=1\"></script><noscript>JavaScript is required to enter Zap preview.</noscript></body></html>`\n return new Response(html, {\n headers: {\n ...NO_STORE,\n 'Content-Type': 'text/html; charset=utf-8',\n 'Content-Security-Policy':\n \"default-src 'none'; script-src 'self'; form-action 'self'; base-uri 'none'\",\n },\n })\n}\n\nexport function createDraftModeRoute(options: DraftModeRouteOptions) {\n requireKeys(options)\n const exchange = async (request: Request): Promise<Response> => {\n let params: URLSearchParams | FormData\n if (request.method === 'POST') {\n const sameOrigin =\n request.headers.get('sec-fetch-site') === 'same-origin' ||\n request.headers.get('origin') === new URL(request.url).origin\n if (!sameOrigin) return refuse(403, 'cross_origin')\n try {\n params = await request.formData()\n } catch {\n return refuse(401, 'invalid_token')\n }\n } else {\n params = new URL(request.url).searchParams\n }\n const token = params.get('token')\n if (!isPreviewTokenShape(token)) return refuse(401, 'invalid_token')\n const rawPath = params.get('path')\n if (rawPath !== null && typeof rawPath !== 'string') return refuse(400, 'invalid_path')\n const path = safeRedirectPath(rawPath, request.url)\n if (path === null) return refuse(400, 'invalid_path')\n\n let verdict: ZapVerdict\n try {\n verdict = await validateWithZap(token, options)\n } catch {\n return refuse(502, 'zap_unreachable')\n }\n if (!verdict.ok) {\n if (verdict.status >= 500) return refuse(502, 'zap_unreachable')\n return verdict.code === 'WRONG_SITE'\n ? refuse(403, 'wrong_site')\n : refuse(401, 'token_refused')\n }\n if (verdict.siteKey !== options.siteKey) return refuse(403, 'wrong_site')\n\n const draft = await options.draftMode()\n await draft.enable()\n\n const cookies = [cookieHeader(PREVIEW_TOKEN_COOKIE, token, PREVIEW_COOKIE_MAX_AGE)]\n // Next set its own draft cookie when `enable()` ran, without\n // `Partitioned`; the same value again, partitioned, is the copy Chrome\n // keeps inside a third-party frame.\n const bypass = options.cookies ? (await options.cookies()).get(NEXT_DRAFT_COOKIE)?.value : null\n if (bypass && /^[A-Za-z0-9._~-]+$/.test(bypass)) {\n // No longer than the token: past it the page would sit in draft mode\n // with nothing to read drafts with.\n cookies.push(cookieHeader(NEXT_DRAFT_COOKIE, bypass, PREVIEW_COOKIE_MAX_AGE))\n }\n return redirect(path, cookies, request.method === 'POST' ? 303 : 307)\n }\n return async function draftModeRoute(request: Request): Promise<Response> {\n if (request.method === 'GET') {\n const params = new URL(request.url).searchParams\n if (params.get('script') === '1') return fragmentExchangeScript()\n if (!params.has('token')) return fragmentExchangePage()\n // Old sites keep using query transport until their SDK opts in.\n return exchange(request)\n }\n if (request.method === 'POST') return exchange(request)\n return new Response(null, { status: 405, headers: { ...NO_STORE, Allow: 'GET, POST' } })\n }\n}\n\ninterface DraftModeExitLike {\n disable(): void | Promise<void>\n}\n\nexport interface DraftModeExitRouteOptions {\n /** `draftMode` from `next/headers`. */\n draftMode: () => DraftModeExitLike | Promise<DraftModeExitLike>\n}\n\n/**\n * The route that ends a preview: `createDraftModeRoute`'s twin.\n *\n * ```ts\n * // app/api/zap-preview/exit/route.ts\n * import { draftMode } from 'next/headers'\n * import { createDraftModeExitRoute } from '@8ux-co/eelzap/next'\n *\n * export const GET = createDraftModeExitRoute({ draftMode })\n * ```\n *\n * Disables Next's draft mode, then expires BOTH cookies the enable route set,\n * with the same `SameSite=None; Secure; Partitioned; HttpOnly; Path=/`: a\n * partitioned cookie is only replaced by a partitioned one, so `disable()`\n * alone (Next's unpartitioned expiry) would leave the framed copy alive.\n * Redirects (307) to `?path=` when `safeRedirectPath` accepts it, else to `/`.\n *\n * Mount it at `{draftRoute}/exit` (`/api/zap-preview/exit` by default): the\n * live-site toolbar's sign-out calls exactly that path to drop the draft\n * cookie a draft session left (ADR 041 amendment 2026-10-06). Without it,\n * signing out still revokes the session's tokens at Zap, and the cookie\n * expires within its ten minutes.\n */\nexport function createDraftModeExitRoute(options: DraftModeExitRouteOptions) {\n if (!options?.draftMode) throw new Error('eelzap/next: draftMode is required')\n return async function GET(request: Request): Promise<Response> {\n const url = new URL(request.url)\n const path = safeRedirectPath(url.searchParams.get('path'), request.url) ?? '/'\n const draft = await options.draftMode()\n await draft.disable()\n return redirect(path, [\n cookieHeader(NEXT_DRAFT_COOKIE, '', 0),\n cookieHeader(PREVIEW_TOKEN_COOKIE, '', 0),\n ])\n }\n}\n\ninterface HeaderSource {\n get(name: string): string | null | undefined\n}\n\n/**\n * The preview token for this request, from the client's header (the\n * cookieless fallback) or the cookie the draft-mode route set; null when\n * neither carries a well-formed one. Pass it to the server SDK as the\n * credential (`createClient({ apiKey: token })`), which reads drafts with it.\n *\n * ```ts\n * import { cookies, headers } from 'next/headers'\n * const token = getPreviewToken({ headers: await headers(), cookies: await cookies() })\n * ```\n */\nexport function getPreviewToken(\n source: Request | { headers?: HeaderSource; cookies?: CookieStoreLike },\n): string | null {\n const request = source instanceof Request\n const fromHeader = source.headers?.get(PREVIEW_TOKEN_HEADER)\n if (isPreviewTokenShape(fromHeader)) return fromHeader\n const fromCookie = request\n ? readCookie(source.headers.get('cookie'), PREVIEW_TOKEN_COOKIE)\n : source.cookies?.get(PREVIEW_TOKEN_COOKIE)?.value\n return isPreviewTokenShape(fromCookie) ? fromCookie : null\n}\n\nexport interface ValidPreviewTokenOptions {\n /** The Zap site's key: a token minted for any other site is refused. */\n siteKey: string\n /** One of the site's own API keys, as for `createDraftModeRoute`. Server-side only. */\n apiKey: string\n /** Where Zap answers. Default `https://zap.eel.software`. */\n zapOrigin?: string\n /** Injected in specs. */\n fetch?: typeof fetch\n}\n\n/** A confirmed token is trusted this long at most, less when it expires sooner. */\nconst VALID_TOKEN_CACHE_MS = 60_000\n/** A refused token is re-asked after this long. */\nconst REFUSED_TOKEN_CACHE_MS = 10_000\nconst TOKEN_CACHE_MAX = 500\nconst tokenCache = new Map<string, { valid: boolean; until: number }>()\n\nfunction rememberVerdict(key: string, valid: boolean, until: number): void {\n tokenCache.delete(key)\n // Full: drop the oldest (a Map iterates in insertion order).\n if (tokenCache.size >= TOKEN_CACHE_MAX) tokenCache.delete(tokenCache.keys().next().value!)\n tokenCache.set(key, { valid, until })\n}\n\n/** Test-only: forget every cached verdict. */\nexport function __resetPreviewTokenCacheForTests(): void {\n tokenCache.clear()\n}\n\n/**\n * `getPreviewToken`, checked with Zap: the token for this request only when\n * Zap confirms it is live AND was minted for this site, else null.\n *\n * `getPreviewToken` accepts any well-formed `x-zap-preview-token` header, and\n * anyone can send one: a token minted for their own Zap site, or one that\n * expired or was revoked. **Use this on every request that serves drafts**,\n * so a stranger's token never makes your server render drafts. It asks\n * `GET /api/public/v1/preview/token` exactly as the draft-mode route does\n * (the token as the bearer, your API key beside it) and compares the site\n * key Zap answers with `siteKey`.\n *\n * Verdicts are cached in memory per token, site and key, at most 500 of\n * them: a confirmed token for 60 seconds or until the expiry Zap answers,\n * whichever comes first; a refused one for 10 seconds. Zap unreachable is\n * null and not cached. The token is never logged.\n *\n * ```ts\n * import { cookies, headers } from 'next/headers'\n * const token = await getValidPreviewToken(\n * { headers: await headers(), cookies: await cookies() },\n * { siteKey: 'verdeorigen', apiKey: process.env.ZAP_SECRET_KEY! },\n * )\n * ```\n */\nexport async function getValidPreviewToken(\n source: Request | { headers?: HeaderSource; cookies?: CookieStoreLike },\n options: ValidPreviewTokenOptions,\n): Promise<string | null> {\n requireKeys(options)\n const token = getPreviewToken(source)\n if (token === null) return null\n\n // Keyed by everything the verdict depends on: one process may serve\n // several sites, and a token valid for one is not valid for another.\n const key = `${options.zapOrigin}\\n${options.siteKey}\\n${options.apiKey}\\n${token}`\n const now = Date.now()\n const cached = tokenCache.get(key)\n if (cached && cached.until > now) return cached.valid ? token : null\n\n let verdict: ZapVerdict\n try {\n verdict = await validateWithZap(token, options)\n } catch {\n return null\n }\n if (!verdict.ok) {\n if (verdict.status < 500) rememberVerdict(key, false, now + REFUSED_TOKEN_CACHE_MS)\n return null\n }\n if (verdict.siteKey !== options.siteKey) {\n rememberVerdict(key, false, now + REFUSED_TOKEN_CACHE_MS)\n return null\n }\n if (verdict.expiresAt !== null && verdict.expiresAt <= now) {\n rememberVerdict(key, false, now + REFUSED_TOKEN_CACHE_MS)\n return null\n }\n const until = Math.min(now + VALID_TOKEN_CACHE_MS, verdict.expiresAt ?? Number.POSITIVE_INFINITY)\n rememberVerdict(key, true, until)\n return token\n}\n\nfunction readCookie(header: string | null, name: string): string | null {\n if (!header) return null\n for (const part of header.split(';')) {\n const [key, ...rest] = part.trim().split('=')\n if (key === name) return rest.join('=')\n }\n return null\n}\n\n/**\n * The path `createDraftModeRoute` is documented at, and the one `ZapPreview`\n * announces to Zap's editor unless told otherwise (§2.5): mount the route at\n * `app/api/zap-preview/route.ts`, or pass `draftRoute` with its path.\n */\nexport const DEFAULT_DRAFT_ROUTE = '/api/zap-preview'\n\n/**\n * The preview boot for a Next.js site (§2.7): render it once, in the root\n * layout. It announces the draft-mode route (`/api/zap-preview` unless\n * `draftRoute` says otherwise; `null` for none) and loads the overlay only in\n * a preview session. Pass `preview={(await draftMode()).isEnabled}` so a\n * draft-mode page in its own tab boots too.\n *\n * ```tsx\n * import { draftMode } from 'next/headers'\n * import { ZapPreview } from '@8ux-co/eelzap/next'\n *\n * <ZapPreview siteKey=\"verdeorigen\" siteId=\"…uuid…\" preview={(await draftMode()).isEnabled} />\n * ```\n *\n * A server component that renders the client boot from `@8ux-co/eelzap/react`.\n */\nexport function ZapPreview(props: ZapPreviewProps) {\n return createElement(ClientZapPreview, {\n ...props,\n draftRoute: props.draftRoute === undefined ? DEFAULT_DRAFT_ROUTE : props.draftRoute,\n })\n}\n\n/**\n * True when this request carries a preview token in the shape Zap mints (the\n * client's header or the draft-mode cookie): a cheap hint for rendering\n * choices (skip a cache, render `ZapPreview` with `preview`). It does NOT\n * prove the token is live or yours; serve drafts only with\n * `getValidPreviewToken`.\n */\nexport function isZapPreview(\n source: Request | { headers?: HeaderSource; cookies?: CookieStoreLike },\n): boolean {\n return getPreviewToken(source) !== null\n}\n","/**\n * The record and field grammar of tags (`.docs/proposals/zap-cms-v2.md` §2.4),\n * in a module of its own so the light entries (`./fields`, `./react`, the\n * boot) can name records without pulling in the preview protocol.\n * No imports, no side effects.\n */\n\n/**\n * A record reference in its canonical string form: `collectionKey/slug` for an\n * entry, `doc:documentKey` for a document (the tag grammar, §2.4).\n */\nexport type RecordRef = string\n\n/** Zap's field key pattern. */\nexport const FIELD_KEY_RE = /^[a-z][a-z0-9_]{0,127}$/\n/** Entry and document references; keys and slugs are `[a-z0-9]+(?:-[a-z0-9]+)*`. */\nconst ENTRY_REF_RE = /^([a-z0-9]+(?:-[a-z0-9]+)*)\\/([a-z0-9]+(?:-[a-z0-9]+)*)$/\nconst DOC_REF_RE = /^doc:([a-z0-9]+(?:-[a-z0-9]+)*)$/\nconst MAX_REF_LENGTH = 300\n\nexport type ParsedRecordRef =\n | { kind: 'entry'; collection: string; slug: string }\n | { kind: 'document'; key: string }\n\nexport function parseRecordRef(value: unknown): ParsedRecordRef | null {\n if (typeof value !== 'string' || value.length > MAX_REF_LENGTH) return null\n const doc = DOC_REF_RE.exec(value)\n if (doc) return { kind: 'document', key: doc[1]! }\n const entry = ENTRY_REF_RE.exec(value)\n if (entry) return { kind: 'entry', collection: entry[1]!, slug: entry[2]! }\n return null\n}\n\nexport function isRecordRef(value: unknown): value is RecordRef {\n return parseRecordRef(value) !== null\n}\n\n/** `zpt_` + 32 bytes in base64url, as Zap mints it (zap-cms-v2 §3.5). */\nexport const PREVIEW_TOKEN_RE = /^zpt_[A-Za-z0-9_-]{43}$/\n\n/** A preview token in the shape Zap mints; says nothing about whether it is live. */\nexport function isPreviewTokenShape(value: unknown): value is string {\n return typeof value === 'string' && PREVIEW_TOKEN_RE.test(value)\n}\n\nexport function isFieldKey(value: unknown): value is string {\n return typeof value === 'string' && FIELD_KEY_RE.test(value)\n}\n\n/**\n * What `zapAttrs` and `onValues` accept: a record reference string\n * (`blog/hola`, `doc:home`) or a delivery API entry (an item carries\n * `collection.key` and `slug`, a document `key`).\n */\nexport type EntryLike =\n | RecordRef\n | {\n slug?: string | null\n key?: string | null\n collection?: { key?: string | null } | null\n localizedSlugs?: Record<string, string | null | undefined> | null\n }\n\n/** Every reference an entry answers to (default slug first, then localized ones). */\nexport function recordRefsOf(entry: EntryLike | null | undefined): RecordRef[] {\n if (!entry) return []\n if (typeof entry === 'string') return isRecordRef(entry) ? [entry] : []\n const refs: RecordRef[] = []\n const add = (ref: string) => {\n if (isRecordRef(ref) && !refs.includes(ref)) refs.push(ref)\n }\n const collection = entry.collection?.key\n if (collection) {\n if (entry.slug) add(`${collection}/${entry.slug}`)\n for (const slug of Object.values(entry.localizedSlugs ?? {})) {\n if (slug) add(`${collection}/${slug}`)\n }\n } else if (entry.key) {\n add(`doc:${entry.key}`)\n }\n return refs\n}\n\n/**\n * The tag for one field, as attributes to spread on the element that renders\n * it: `{ 'data-zap': 'blog/hola#title' }`. Empty when the entry or the field\n * key is not one Zap can name, so a bad call never breaks the page.\n */\nexport function zapAttrs(entry: EntryLike, fieldKey: string): { 'data-zap'?: string } {\n const [ref] = recordRefsOf(entry)\n if (!ref || !isFieldKey(fieldKey)) return {}\n return { 'data-zap': `${ref}#${fieldKey}` }\n}\n","// eslint-disable-next-line no-control-regex\nconst UNSAFE_PATH_CHARS = /[\\u0000-\\u001f\\u007f\\\\]/\n\n/**\n * The path to redirect to, or null when it could leave the site. Returned\n * as path + query + hash, never absolute, so the `Location` stays on the\n * host the browser asked.\n */\nexport function safeRedirectPath(raw: string | null, requestUrl: string): string | null {\n const value = raw ?? '/'\n if (value.length === 0 || value.length > 2048) return null\n if (!value.startsWith('/') || value.startsWith('//')) return null\n if (UNSAFE_PATH_CHARS.test(value)) return null\n let base: URL\n let target: URL\n try {\n base = new URL(requestUrl)\n target = new URL(value, base)\n } catch {\n return null\n }\n if (target.origin !== base.origin) return null\n // Dot segments can collapse into a leading `//` (`/.//evil.example`,\n // `/a/..//evil.example`), and a `Location` of `//evil.example` is\n // protocol-relative: another host. The resolved path must not start so.\n if (target.pathname.startsWith('//')) return null\n // Refuse a token anywhere in the redirect, including percent-encoded ones.\n let decoded = value\n for (let i = 0; i < value.length; i++) {\n if (/zpt_/i.test(decoded)) return null\n const next = decoded.replace(/%([0-9a-f]{2})/gi, (_, hex: string) =>\n String.fromCharCode(Number.parseInt(hex, 16)),\n )\n if (next === decoded) break\n decoded = next\n }\n return `${target.pathname}${target.search}${target.hash}`\n}\n"],"mappings":"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","names":["body"]}
package/dist/preview.d.ts CHANGED
@@ -101,6 +101,22 @@ declare function zapAttrs(entry: EntryLike, fieldKey: string): {
101
101
  * tagged elements, `runaway` when the count kept growing with nothing on
102
102
  * the page explaining it. The page stops writing values; the editor offers
103
103
  * a reload. Sent again after a hello, which a pause before it never reached.
104
+ * - Comparar (#953), two frames of one page side by side, the published one
105
+ * and the draft. Scroll sync, only to a page whose `zap:ready` advertised
106
+ * `scroll-sync`: `zap:scroll-sync` (editor → page) `{ enabled }` starts or
107
+ * stops `zap:scroll` (page → editor) `{ y, x?, anchor?, next?, progress? }`,
108
+ * one per animation frame at most, positions as fractions (0..1) of the
109
+ * scrollable range, `anchor` the last tagged field whose top is at or above
110
+ * the viewport's top (else the first in view), and since 0.11.1 `next` the
111
+ * tagged field below it and `progress` how far the viewport's top is from
112
+ * one to the other; `zap:scroll-to` (editor → page, same shape) places the
113
+ * page there: between the two fields when it has both, else by the anchor,
114
+ * else by the fraction, at most once per frame, and is never echoed back.
115
+ * `next` and `progress` are optional: a 0.11.0 page or editor ignores them.
116
+ * Diff marks, only to a page whose `zap:ready` advertised `diff-marks`:
117
+ * `zap:marks` (editor → page) `{ marks }`, every element of each field
118
+ * marked in its tone, removed (published) or added (draft); ≤ 200 marks,
119
+ * an empty list clears them.
104
120
  */
105
121
  declare const PROTOCOL_SOURCE = "eel-zap";
106
122
  /**
@@ -124,10 +140,14 @@ declare const MAX_TAG_REPORT = 2000;
124
140
  declare const MAX_LABELS = 500;
125
141
  /** Pins drawn at once (`zap:pins`). */
126
142
  declare const MAX_PINS = 50;
143
+ /** Diff marks drawn at once (`zap:marks`), and the longest mark label. */
144
+ declare const MAX_MARKS = 200;
145
+ declare const MAX_MARK_LABEL = 200;
146
+ /** Highest occurrence index a scroll anchor may name (`MAX_TAGGED` in `tags.ts`). */
147
+ declare const MAX_ANCHOR_NTH = 5000;
127
148
  type OverlayMode = 'inspect' | 'select' | 'spot' | 'off';
128
149
  declare const OVERLAY_MODES: readonly OverlayMode[];
129
- type Capability = 'overlay' | 'values' | 'stega' | 'pins' | 'links' | 'refresh';
130
- /** Every capability; a client built from this source advertises all of them. */
150
+ type Capability = 'overlay' | 'values' | 'stega' | 'pins' | 'links' | 'refresh' | 'scroll-sync' | 'diff-marks' | 'fragment-token' | 'structured-values';
131
151
  declare const CAPABILITIES: readonly Capability[];
132
152
 
133
153
  /**
@@ -139,6 +159,12 @@ declare const CAPABILITIES: readonly Capability[];
139
159
  * should show. A bare string, number or boolean is shorthand for `{ text }`.
140
160
  * `{ url }` and `{ email }` (a URL and an EMAIL field, `links` capability) go
141
161
  * to a link's `href` rather than its text.
162
+ *
163
+ * With the `structured-values` capability, a CURRENCY field travels as
164
+ * {@link PreviewCurrencyValue} and an ENUM as {@link PreviewEnumValue}: the
165
+ * delivery API's own shapes, so a site's `price.amountMinor` and
166
+ * `category.value` survive a live merge (`useZapLiveUpdates`). The overlay
167
+ * writes their `text` / `label`.
142
168
  */
143
169
  type PreviewValue = string | number | boolean | null | {
144
170
  text: string;
@@ -154,7 +180,18 @@ type PreviewValue = string | number | boolean | null | {
154
180
  srcset?: string | null;
155
181
  alt?: string | null;
156
182
  };
157
- };
183
+ } | PreviewCurrencyValue | PreviewEnumValue;
184
+ /** A CURRENCY field: the stored minor units, the ISO 4217 code, and the formatted text to show. */
185
+ interface PreviewCurrencyValue {
186
+ amountMinor: number;
187
+ currency: string;
188
+ text: string;
189
+ }
190
+ /** An ENUM field: the option's value and its label (what the page shows). */
191
+ interface PreviewEnumValue {
192
+ value: string;
193
+ label: string;
194
+ }
158
195
  /** Longest EMAIL value accepted (RFC 5321's path limit). */
159
196
  declare const MAX_EMAIL_LENGTH = 320;
160
197
  /** How the page found an element: a stega marker in its text, or a `data-zap` attribute. */
@@ -303,6 +340,48 @@ interface PausedPayload {
303
340
  reason: 'runaway' | 'cap';
304
341
  count: number;
305
342
  }
343
+ /** A tagged field's element: the `nth` element of the field (in `TagIndex.byField` order, 0 first). */
344
+ interface ScrollFieldRef {
345
+ recordRef: RecordRef;
346
+ fieldKey: string;
347
+ nth: number;
348
+ }
349
+ /**
350
+ * A tagged field's element as a scroll anchor, and its top's distance below
351
+ * the viewport's top, CSS px (negative once it scrolled past).
352
+ */
353
+ interface ScrollAnchor extends ScrollFieldRef {
354
+ offset: number;
355
+ }
356
+ /**
357
+ * `zap:scroll` and `zap:scroll-to`: the scroll position as fractions (0..1) of
358
+ * the scrollable range (`x` only when the page scrolls sideways), and the
359
+ * fields around the viewport's top, which align two pages of different
360
+ * lengths better than the fraction does. With `next` and `progress` (0.11.1)
361
+ * the other page puts its top that far between ITS two fields, so the
362
+ * position stays continuous when the anchor moves on to the next field, even
363
+ * where the two pages space their fields differently.
364
+ */
365
+ interface ScrollPayload {
366
+ y: number;
367
+ x?: number;
368
+ anchor?: ScrollAnchor;
369
+ /** The tagged field below the anchor. */
370
+ next?: ScrollFieldRef;
371
+ /** 0 with the anchor at the viewport's top, 1 with `next` there. */
372
+ progress?: number;
373
+ }
374
+ /**
375
+ * One Comparar mark (`zap:marks`): every element of the field gets a box in
376
+ * its tone, red for `removed` (the published page), green for `added` (the
377
+ * draft); `active` (the change picked in the list) is solid, tinted and
378
+ * named with `label`.
379
+ */
380
+ interface DiffMark extends FieldRefPayload {
381
+ tone: 'removed' | 'added';
382
+ label: string;
383
+ active?: boolean;
384
+ }
306
385
  interface PageMessages {
307
386
  'zap:ready': ReadyPayload;
308
387
  'zap:tags': TagSummary[];
@@ -322,6 +401,8 @@ interface PageMessages {
322
401
  id: string;
323
402
  };
324
403
  'zap:paused': PausedPayload;
404
+ /** `scroll-sync`: the page scrolled (while sync is on); never for a `zap:scroll-to`. */
405
+ 'zap:scroll': ScrollPayload;
325
406
  }
326
407
  interface EditorMessages {
327
408
  'zap:hello': HelloPayload;
@@ -343,6 +424,16 @@ interface EditorMessages {
343
424
  'zap:pins': {
344
425
  pins: Pin[];
345
426
  };
427
+ /** `scroll-sync`: start or stop reporting `zap:scroll`. */
428
+ 'zap:scroll-sync': {
429
+ enabled: boolean;
430
+ };
431
+ /** `scroll-sync`: place the page here (the anchor first), without a `zap:scroll` back. */
432
+ 'zap:scroll-to': ScrollPayload;
433
+ /** `diff-marks`: the marks to draw; an empty list clears them. */
434
+ 'zap:marks': {
435
+ marks: DiffMark[];
436
+ };
346
437
  }
347
438
  type PageMessageType = keyof PageMessages;
348
439
  type EditorMessageType = keyof EditorMessages;
@@ -379,6 +470,7 @@ declare function isDraftRoutePath(value: unknown): value is string;
379
470
  declare function isPinId(value: unknown): value is string;
380
471
  /** A pin: id grammar, integer `n` 1..999, and exactly one of `spot` / `dom`. */
381
472
  declare function isPin(value: unknown): value is Pin;
473
+ declare function isDiffMark(value: unknown): value is DiffMark;
382
474
  declare function isPreviewValue(value: unknown): value is PreviewValue;
383
475
  /** The selector cap is in BYTES (§3.3), so a multi-byte selector counts right. */
384
476
  declare function utf8Length(value: string): number;
@@ -409,7 +501,7 @@ declare function parseEditorMessage(data: unknown): ParseResult<EditorMessage>;
409
501
  declare function parsePageMessage(data: unknown): ParseResult<PageMessage>;
410
502
  declare function envelope<T extends string, P>(type: T, session: string, payload: P): Envelope<T, P>;
411
503
 
412
- declare const TAG_SELECTOR = "[data-zap],[data-zap-field]";
504
+ declare const TAG_SELECTOR = "[data-zap],[data-zap-field],[data-zap-site-renders]";
413
505
  interface TaggedElement {
414
506
  element: Element;
415
507
  recordRef: RecordRef;
@@ -420,11 +512,13 @@ interface TaggedElement {
420
512
  html: boolean;
421
513
  /** `attr` for a manual tag, `stega` for a marker found in a text node. */
422
514
  source: TagSource;
515
+ /** `data-zap-site-renders`: the site renders this field here; never written into. */
516
+ siteRenders?: true;
423
517
  /** Stega only: the text node that carries the marker (values are written to it). */
424
518
  textNode?: Text;
425
519
  }
426
520
  interface TagProblem {
427
- attribute: 'data-zap' | 'data-zap-field' | 'data-zap-entry';
521
+ attribute: 'data-zap' | 'data-zap-field' | 'data-zap-entry' | 'data-zap-site-renders';
428
522
  value: string;
429
523
  }
430
524
  type ReadTagResult = {
@@ -506,6 +600,12 @@ declare function observeTags(target: Node, onChange: (page: boolean) => void, op
506
600
  * `off` too with `pinsWhenOff`, the live site's «Navegar»), and are the only
507
601
  * things here that take the pointer besides the capture layer.
508
602
  * A click on one is consumed and reported (`onPin`), never also a spot.
603
+ * - Comparar's diff marks (`setMarks`) are drawn in every mode, `off`
604
+ * included, under the other boxes (boards CompararEscritorio /
605
+ * CompararMovil): a dashed 1.5px outline in the tone, red #FB2C36 for
606
+ * `removed` and green #00C950 for `added`; the active change's is solid
607
+ * 2px, tinted at 8% and named by a chip (#C10007 / #008236) whose label
608
+ * starts with − or +. Divided by the zoom like the outlines.
509
609
  * - Boxes are `position: fixed` from `getBoundingClientRect()` and redrawn on
510
610
  * scroll and resize in one animation frame, so nothing in the site's layout
511
611
  * moves.
@@ -523,6 +623,10 @@ interface OverlayCallbacks {
523
623
  id: string;
524
624
  }): void;
525
625
  }
626
+ /** A diff mark on one element (`DiffMark`, resolved by the runtime). */
627
+ interface OverlayMark extends Pick<DiffMark, 'tone' | 'label' | 'active'> {
628
+ element: Element;
629
+ }
526
630
  /** A pin to draw: at a point of the document, or at an element's top-left corner. */
527
631
  interface OverlayPin {
528
632
  id: string;
@@ -548,7 +652,7 @@ interface OverlayOptions extends OverlayCallbacks {
548
652
  */
549
653
  spotOnLarge?: boolean;
550
654
  }
551
- type BoxKind = 'hover' | 'focus' | 'selected' | 'highlight';
655
+ type BoxKind = 'hover' | 'focus' | 'selected' | 'highlight' | 'mark';
552
656
  declare class Overlay {
553
657
  private readonly options;
554
658
  private host;
@@ -563,6 +667,7 @@ declare class Overlay {
563
667
  private highlighted;
564
668
  private selected;
565
669
  private pins;
670
+ private marks;
566
671
  private pending;
567
672
  private capture;
568
673
  private frame;
@@ -591,6 +696,8 @@ declare class Overlay {
591
696
  highlight(elements: Element[]): void;
592
697
  /** Draw these pins (≤ 50); drops the pending spot pin. */
593
698
  setPins(pins: OverlayPin[]): void;
699
+ /** Draw these diff marks (Comparar), in every mode; an empty list clears them. */
700
+ setMarks(marks: OverlayMark[]): void;
594
701
  /** Drop the pending spot pin. */
595
702
  clearPending(): void;
596
703
  clearSelection(): void;
@@ -600,6 +707,7 @@ declare class Overlay {
600
707
  boxes: Array<{
601
708
  kind: BoxKind;
602
709
  label: string | null;
710
+ tone?: string;
603
711
  }>;
604
712
  pins: HTMLElement[];
605
713
  capture: HTMLElement | null;
@@ -680,7 +788,7 @@ declare function createPageBridge(options: PageBridgeOptions): PageBridge;
680
788
  * Live client (`live.ts`) boots it inside the customer's own page, framed by
681
789
  * Zap's editor by its real URL (§2.7).
682
790
  */
683
- declare const CLIENT_VERSION = "0.10.0";
791
+ declare const CLIENT_VERSION = "0.11.1";
684
792
  interface RuntimeOptions {
685
793
  win?: Window;
686
794
  doc?: Document;
@@ -795,7 +903,9 @@ type ValuesHandler = (patch: Record<string, PreviewValue>, payload: ValuesPayloa
795
903
  /**
796
904
  * Call `handler` with each `zap:values` patch for `entry` (unsaved keystrokes
797
905
  * from the editor), for sites that compute things from values. The tagged
798
- * elements are updated anyway; this is for everything else. Values arrive
906
+ * elements are updated anyway, except those marked `data-zap-site-renders`
907
+ * (`fields(…).derived(key)`), which the site renders from this patch; this is
908
+ * for everything else. Values arrive
799
909
  * formatted for display, exactly as the overlay writes them. Returns an
800
910
  * unsubscribe function. Works before or after `initZap`.
801
911
  */
@@ -977,4 +1087,4 @@ declare function buildAnchor(element: Element, index: TagIndex, pageUrl: string,
977
1087
  */
978
1088
  declare function findAnchorElement(anchor: DomAnchor, index: TagIndex, doc: Document, win: Window): Element | null;
979
1089
 
980
- export { CAPABILITIES, CLIENT_VERSION, type Capability, type ClickPayload, type DomAnchor, type DropReason, type DropStats, EDITOR_MESSAGE_TYPES, type EditorMessage, type EditorMessageType, type EditorMessages, type EntryLike, type Envelope, FIELD_KEY_RE, type FieldRefPayload, type HelloPayload, type InitZapOptions, MAX_ANCHORS, MAX_DRAFT_ROUTE_LENGTH, MAX_EMAIL_LENGTH, MAX_LABELS, MAX_MESSAGE_BYTES, MAX_PINS, MAX_QUOTE_CONTEXT, MAX_QUOTE_EXACT, MAX_SELECTOR_BYTES, MAX_TAG_REPORT, MAX_URL_LENGTH, type MessageTargetLike, type NormalizedRect, OVERLAY_HOST_TAG, OVERLAY_MODES, type ObserveOptions, Overlay, type OverlayCallbacks, type OverlayMode, type OverlayOptions, type OverlayTheme, PAGE_MESSAGE_TYPES, PAGE_UNLOAD_ERROR, PREVIEW_TOKEN_RE, PREVIEW_TOKEN_STORAGE_KEY, PROTOCOL_SOURCE, PROTOCOL_VERSION, type PageAnchor, type PageBridge, type PageBridgeOptions, type PageMessage, type PageMessageType, type PageMessages, type PageRuntime, type ParseResult, type ParsedRecordRef, type PausedPayload, type Pin, type PreviewValue, RICH_TEXT_ALLOWED_ATTRS, RICH_TEXT_ALLOWED_TAGS, RICH_TEXT_DROPPED_TAGS, type ReadTagResult, type ReadyPayload, type RecordRef, type RuntimeConfig, type RuntimeOptions, type SpotAnchor, type SpotPayload, type SpotPoint, TAG_SELECTOR, TagIndex, type TagProblem, type TagSource, type TagSummary, type TaggedElement, type TextQuote, ValueApplier, type ValuesHandler, type ValuesPayload, ZAP_PREVIEW_TOKEN_HEADER, ZAP_PRODUCTION_ORIGIN, type ZapLive, buildAnchor, buildDomAnchor, buildSelector, cleanStega, createPageBridge, cssEscape, developmentZapOrigin, emptyDropStats, envelope, findAnchorElement, getPreviewToken, initZap, isDomAnchor, isDraftRoutePath, isFieldKey, isLocale, isPageAnchor, isPin, isPinId, isPreviewTokenShape, isPreviewValue, isRecordRef, isSafeImageUrl, isSafeSrcset, isSafeUrl, isSession, messageBytes, normalizeText, normalizedRect, observeTags, onValues, parseEditorMessage, parsePageMessage, parseRecordRef, parseTag, previewHeaders, readElementTag, recordRefsOf, richTextToText, safeQueryAll, sanitizeRichText, sanitizeRichTextToString, startRuntime, textQuoteFor, utf8Length, zapAttrs };
1090
+ export { CAPABILITIES, CLIENT_VERSION, type Capability, type ClickPayload, type DiffMark, type DomAnchor, type DropReason, type DropStats, EDITOR_MESSAGE_TYPES, type EditorMessage, type EditorMessageType, type EditorMessages, type EntryLike, type Envelope, FIELD_KEY_RE, type FieldRefPayload, type HelloPayload, type InitZapOptions, MAX_ANCHORS, MAX_ANCHOR_NTH, MAX_DRAFT_ROUTE_LENGTH, MAX_EMAIL_LENGTH, MAX_LABELS, MAX_MARKS, MAX_MARK_LABEL, MAX_MESSAGE_BYTES, MAX_PINS, MAX_QUOTE_CONTEXT, MAX_QUOTE_EXACT, MAX_SELECTOR_BYTES, MAX_TAG_REPORT, MAX_URL_LENGTH, type MessageTargetLike, type NormalizedRect, OVERLAY_HOST_TAG, OVERLAY_MODES, type ObserveOptions, Overlay, type OverlayCallbacks, type OverlayMode, type OverlayOptions, type OverlayTheme, PAGE_MESSAGE_TYPES, PAGE_UNLOAD_ERROR, PREVIEW_TOKEN_RE, PREVIEW_TOKEN_STORAGE_KEY, PROTOCOL_SOURCE, PROTOCOL_VERSION, type PageAnchor, type PageBridge, type PageBridgeOptions, type PageMessage, type PageMessageType, type PageMessages, type PageRuntime, type ParseResult, type ParsedRecordRef, type PausedPayload, type Pin, type PreviewCurrencyValue, type PreviewEnumValue, type PreviewValue, RICH_TEXT_ALLOWED_ATTRS, RICH_TEXT_ALLOWED_TAGS, RICH_TEXT_DROPPED_TAGS, type ReadTagResult, type ReadyPayload, type RecordRef, type RuntimeConfig, type RuntimeOptions, type ScrollAnchor, type ScrollFieldRef, type ScrollPayload, type SpotAnchor, type SpotPayload, type SpotPoint, TAG_SELECTOR, TagIndex, type TagProblem, type TagSource, type TagSummary, type TaggedElement, type TextQuote, ValueApplier, type ValuesHandler, type ValuesPayload, ZAP_PREVIEW_TOKEN_HEADER, ZAP_PRODUCTION_ORIGIN, type ZapLive, buildAnchor, buildDomAnchor, buildSelector, cleanStega, createPageBridge, cssEscape, developmentZapOrigin, emptyDropStats, envelope, findAnchorElement, getPreviewToken, initZap, isDiffMark, isDomAnchor, isDraftRoutePath, isFieldKey, isLocale, isPageAnchor, isPin, isPinId, isPreviewTokenShape, isPreviewValue, isRecordRef, isSafeImageUrl, isSafeSrcset, isSafeUrl, isSession, messageBytes, normalizeText, normalizedRect, observeTags, onValues, parseEditorMessage, parsePageMessage, parseRecordRef, parseTag, previewHeaders, readElementTag, recordRefsOf, richTextToText, safeQueryAll, sanitizeRichText, sanitizeRichTextToString, startRuntime, textQuoteFor, utf8Length, zapAttrs };