@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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/boot/local.ts","../src/preview/site/load.ts","../src/preview/site/boot.ts","../src/preview/page-bridge.ts","../src/preview/sanitize.ts","../src/preview/values.ts","../src/preview/runtime.ts","../src/preview/live.ts"],"sourcesContent":["/**\n * Development origins: `localhost`, `127.0.0.1`, `[::1]` and any `*.localhost`\n * name (which browsers resolve to the machine itself and treat as secure\n * contexts), over http OR https, any port: a site in development runs on\n * either.\n *\n * Shared by the boot (`./boot.ts`, which every visitor runs, so one regex and\n * no more) and the overlay (`../preview/live.ts`), so the two never disagree\n * about what counts as local.\n */\nexport const LOCAL_ORIGIN =\n /^https?:\\/\\/(?:(?:[a-z\\d-]+\\.)*localhost|127\\.0\\.0\\.1|\\[::1\\])(?::\\d+)?$/i\n\n/** True for a development hostname (no scheme, no port). */\nexport const isLocalHost = (hostname: string): boolean => LOCAL_ORIGIN.test(`http://${hostname}`)\n","import type { ChunkModule, ChunkName } from './config'\n\n/**\n * The npm build's chunk loader: a dynamic `import()`, which the site's bundler\n * (and this package's own ESM build, `tsup` with splitting) turns into a\n * separate file fetched only when called. The script-tag build swaps this\n * module for `load-cdn.ts` at bundle time (`scripts/bundle-config.mjs`).\n */\nexport function loadChunk(name: ChunkName, _base?: string | null): Promise<ChunkModule> {\n return name === 'signin' ? import('./signin') : import('./suggest')\n}\n","import {\n PENDING_STORAGE_KEY,\n SIGNIN_CHANNEL,\n siteClientId,\n STATE_PREFIX,\n type CallbackParams,\n type ChunkHandle,\n type ChunkModule,\n type ChunkName,\n type SiteContext,\n type StartOptions,\n} from './config'\nimport { loadChunk } from './load'\nimport { DEFAULT_SHORTCUT, isShortcut } from './shortcut'\nimport { readSession } from './token'\n\n/**\n * The core's part of Live suggestion mode (zap-cms-v2 §3.4, ADR 041), for a\n * page that is NOT framed by Zap. It draws nothing and loads nothing until one\n * of these holds:\n *\n * 1. **This page is the OAuth callback**: `{origin}/?code&state&iss` with a\n * `state` this client minted and `iss` = the compiled-in issuer. In the\n * sign-in popup the code goes to the opener over\n * `BroadcastChannel('eel-zap')` and the popup closes; after a full-page\n * sign-in (the popup was blocked) the `signin` chunk finishes here. Either\n * way the code leaves the address bar at once.\n * 2. **This tab holds a live token** for this site (or a refresh token that\n * can renew it, `session.ts`): the `suggest` chunk.\n * 3. **The person asks**: `?zap` in the URL, or the shortcut (Shift Z by\n * default, never while typing in a field or composing, `shortcut.ts`): the\n * `signin` chunk, which shows the launcher.\n *\n * Ordinary visitors meet none of these, so they get one `keydown` listener\n * and nothing else. Whatever the callback carries is checked again by the\n * `signin` chunk (state, issuer, the PKCE verifier only the opener holds);\n * this file only decides whether a page looks like ours.\n */\n\nexport interface SiteModeOptions {\n win: Window\n siteId: string\n zapOrigin: string\n authOrigin: string\n locale?: 'es' | 'en'\n /** The letter pressed with Shift to open the launcher; false turns the shortcut off. */\n shortcut?: string | false\n /** Script-tag build: the core's own URL, the chunks are its siblings. */\n chunkBase?: string | null\n /** Open at once: the boot already saw the trigger (the shortcut). */\n open?: boolean\n /** The site's draft-mode route, for «Editar» on an untagged page (`draft-session.ts`). */\n draftRoute?: string | null\n}\n\n/** The callback params when this page is one of ours, else null. */\nexport function readCallback(win: Window, authOrigin: string): CallbackParams | null {\n const { pathname, search } = win.location\n if (pathname !== '/' || !search) return null\n const params = new URLSearchParams(search)\n const code = params.get('code')\n const state = params.get('state')\n const iss = params.get('iss')\n if (!code || !state?.startsWith(STATE_PREFIX) || iss !== authOrigin) return null\n return { code, state, iss }\n}\n\nfunction pendingRedirect(win: Window): boolean {\n try {\n return JSON.parse(win.sessionStorage.getItem(PENDING_STORAGE_KEY) ?? '{}').mode === 'redirect'\n } catch {\n return false\n }\n}\n\nfunction pageLocale(doc: Document): 'es' | 'en' {\n return /^en\\b/i.test(doc.documentElement.lang) ? 'en' : 'es'\n}\n\nexport function startSiteMode(options: SiteModeOptions): () => void {\n const { win, siteId } = options\n const doc = win.document\n const key = options.shortcut === undefined ? DEFAULT_SHORTCUT : options.shortcut\n let current: ChunkHandle | null = null\n let opening = false\n\n const ctx: SiteContext = {\n win,\n doc,\n siteId,\n zapOrigin: options.zapOrigin,\n authOrigin: options.authOrigin,\n clientId: siteClientId(options.zapOrigin, siteId),\n redirectUri: `${win.location.origin}/`,\n locale: options.locale ?? pageLocale(doc),\n shortcut: key,\n draftRoute: options.draftRoute ?? null,\n async open(name: ChunkName, startOptions?: StartOptions) {\n opening = true\n try {\n const module: ChunkModule = await loadChunk(name, options.chunkBase)\n current?.destroy()\n current = module.start(ctx, startOptions)\n } catch (error) {\n console.warn('eelzap: suggestions failed', error)\n } finally {\n opening = false\n }\n },\n close() {\n current?.destroy()\n current = null\n },\n }\n\n const callback = readCallback(win, options.authOrigin)\n if (callback) {\n // The code and state must not stay in the address bar or the history.\n win.history.replaceState(win.history.state, '', '/')\n if (pendingRedirect(win)) {\n void ctx.open('signin', { callback })\n } else {\n try {\n const channel = new BroadcastChannel(SIGNIN_CHANNEL)\n channel.postMessage({ type: 'eelzap:callback', ...callback })\n channel.close()\n } finally {\n win.close()\n }\n }\n return ctx.close\n }\n\n const launch = () => {\n if (current || opening) return\n void ctx.open(readSession(win, siteId) ? 'suggest' : 'signin', { reason: 'trigger' })\n }\n\n if (readSession(win, siteId)) void ctx.open('suggest')\n else if (options.open || new URLSearchParams(win.location.search).has('zap')) launch()\n\n const onKey = (event: KeyboardEvent) => {\n if (key && isShortcut(event, key)) launch()\n }\n win.addEventListener('keydown', onKey, true)\n\n return () => {\n win.removeEventListener('keydown', onKey, true)\n ctx.close()\n }\n}\n","import {\n emptyDropStats,\n envelope,\n MAX_MESSAGE_BYTES,\n messageBytes,\n parseEditorMessage,\n type DropReason,\n type DropStats,\n type EditorMessage,\n type PageMessages,\n type PageMessageType,\n} from './protocol'\n\n/**\n * The PAGE side of the bridge (§2.6 \"Origins\").\n *\n * A message from the editor is accepted only when ALL hold:\n *\n * 1. `event.source === parent` — the window that framed us, nothing else\n * (another frame, a popup, the page itself posting to itself).\n * 2. `event.origin` is one of `editorOrigins` (the Zap origins the client\n * trusts). `'null'` (an opaque origin) is never accepted.\n * 3. It parses as a v1 envelope of a known editor to page type, under 256 KB.\n * 4. Its `session` matches. Before `zap:hello` the session is unknown, and\n * nothing but a hello is accepted; the first valid hello fixes it. After\n * that, every message must carry it; a second hello with another session\n * is refused.\n *\n * Every drop is counted in `stats` by reason; nothing throws.\n *\n * Neither side ever posts with `'*'`. The page answers the origin that said\n * hello (before it, each accepted candidate; the browser drops a message whose\n * target is not the parent's real origin). The editor posts only to the exact\n * page origin that said `zap:ready` (`editor.ts`).\n */\n\nexport interface MessageTargetLike {\n postMessage(message: unknown, targetOrigin: string): void\n}\n\nexport interface PageBridgeOptions {\n win: Window\n /** `window.parent` in production; injected in specs. */\n parent: MessageTargetLike\n editorOrigins: readonly string[]\n /**\n * Where to post BEFORE the hello names the editor's origin. Defaults\n * to `editorOrigins`. Every entry must itself be an accepted origin; the\n * browser drops a message whose target is not the parent's real origin.\n */\n preHelloTargets?(): readonly string[]\n onMessage(message: EditorMessage): void\n onDrop?(reason: DropReason): void\n}\n\nexport interface PageBridge {\n post<K extends PageMessageType>(type: K, payload: PageMessages[K]): boolean\n readonly session: string | null\n readonly stats: DropStats\n destroy(): void\n}\n\nexport function createPageBridge(options: PageBridgeOptions): PageBridge {\n const stats = emptyDropStats()\n let session: string | null = null\n let helloOrigin: string | null = null\n const origins = options.editorOrigins.filter((o) => typeof o === 'string' && o !== '')\n\n const isAccepted = (origin: string) =>\n typeof origin === 'string' && origin !== 'null' && origins.includes(origin)\n\n const drop = (reason: DropReason) => {\n stats[reason]++\n options.onDrop?.(reason)\n }\n\n const listener = (event: MessageEvent) => {\n if (event.source !== (options.parent as unknown)) return drop('wrong-source')\n if (!isAccepted(event.origin)) return drop('wrong-origin')\n const parsed = parseEditorMessage(event.data)\n if (!parsed.ok) return drop(parsed.reason)\n const message = parsed.message\n\n if (message.type === 'zap:hello') {\n if (message.payload.session !== message.session) return drop('invalid-payload')\n if (session !== null && message.session !== session) return drop('wrong-session')\n if (session === null) session = message.session\n helloOrigin = event.origin\n } else if (session === null || message.session !== session || helloOrigin === null) {\n // Nothing but a hello is accepted before the hello.\n return drop('wrong-session')\n }\n options.onMessage(message)\n }\n\n options.win.addEventListener('message', listener)\n\n return {\n post(type, payload) {\n const message = envelope(type, session ?? '', payload)\n if (messageBytes(message) > MAX_MESSAGE_BYTES) return false\n // Never '*': after the hello we answer the origin that said hello;\n // before it, each allowed origin (a non-matching target is dropped by\n // the browser, so only the real parent receives it).\n const targets = helloOrigin\n ? [helloOrigin]\n : (options.preHelloTargets?.() ?? origins).filter(isAccepted)\n for (const origin of targets) options.parent.postMessage(message, origin)\n return targets.length > 0\n },\n get session() {\n return session\n },\n stats,\n destroy() {\n options.win.removeEventListener('message', listener)\n },\n }\n}\n","/**\n * The allowlist sanitiser for rich-text substitution (§2.4 \"Draft values\",\n * §7.2 \"Rich text substitution\").\n *\n * ## Why this is not \"the delivery API's allowlist\"\n *\n * The spec says to sanitise \"with the same allowlist the delivery API\n * applies\". The delivery API applies NONE: `apps/zap/src/lib/cms/rich-text-media.ts`\n * strips `<script>…</script>` and nothing else, on purpose (its header: an\n * allowlist would drop `data-media-id` and change bytes, flipping every\n * delivery ETag). The suite's real allowlist is `@eel/ui-web`'s\n * `sanitizeRichTextHtml`, a regex sanitiser for Swarm/Wrap editor HTML whose\n * `src` rule admits only the suite's own file route — Zap rich text points at\n * R2 media URLs, so reusing it verbatim would blank every image.\n *\n * So the allowlist lives HERE, as the one source of truth for HTML that Zap\n * puts into a customer page: the tags and attributes Zap's rich-text editor\n * emits (Tiptap StarterKit + link, underline, text-align, the media image\n * with `data-media-id`), nothing else. Constants are exported so a server-side\n * twin, if one is ever needed, imports them rather than copying.\n *\n * ## DOM-based, not regex\n *\n * The page is a real browser, so the HTML is parsed by the browser's own\n * parser into an INERT `<template>` (no script runs, no image loads, no\n * handler fires while parsing) and the resulting tree is walked: disallowed\n * elements are dropped with their content (script-like) or unwrapped\n * (formatting-like), every attribute not on the list is removed, and URL\n * attributes are re-parsed with `URL` so `java\\tscript:` and friends are\n * judged by the scheme the browser would actually use.\n */\n\n/** Elements kept as they are. Everything else is unwrapped or dropped. */\nexport const RICH_TEXT_ALLOWED_TAGS: ReadonlySet<string> = new Set([\n 'p',\n 'br',\n 'strong',\n 'b',\n 'em',\n 'i',\n 'u',\n 's',\n 'code',\n 'pre',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'ul',\n 'ol',\n 'li',\n 'blockquote',\n 'hr',\n 'a',\n 'span',\n 'img',\n])\n\n/** Elements removed WITH their content: executable, embedding or form markup. */\nexport const RICH_TEXT_DROPPED_TAGS: ReadonlySet<string> = new Set([\n 'script',\n 'style',\n 'template',\n 'iframe',\n 'frame',\n 'frameset',\n 'object',\n 'embed',\n 'applet',\n 'noscript',\n 'svg',\n 'math',\n 'form',\n 'input',\n 'button',\n 'select',\n 'textarea',\n 'link',\n 'meta',\n 'base',\n 'title',\n 'head',\n])\n\n/** Attributes kept, per element (`*` applies to every allowed element). */\nexport const RICH_TEXT_ALLOWED_ATTRS: Readonly<Record<string, ReadonlySet<string>>> = {\n '*': new Set(['title', 'style']),\n a: new Set(['href', 'target', 'rel']),\n img: new Set(['src', 'alt', 'width', 'height', 'data-media-id']),\n}\n\n/** `style` survives only as text alignment (the TextAlign extension). */\nconst ALLOWED_STYLE = /^\\s*text-align\\s*:\\s*(left|right|center|justify)\\s*;?\\s*$/i\nconst LINK_SCHEMES = new Set(['http:', 'https:', 'mailto:', 'tel:'])\nconst IMAGE_SCHEMES = new Set(['http:', 'https:'])\nconst DIMENSION = /^\\d{1,5}$/\nconst MEDIA_ID = /^[A-Za-z0-9-]{1,64}$/\n\n/**\n * Is `value` a URL on one of `schemes` once the browser parses it? Relative\n * URLs resolve against the document's base (the customer page), which is\n * always http(s).\n */\nexport function isSafeUrl(value: string, schemes: ReadonlySet<string>, base?: string): boolean {\n try {\n const url = new URL(value, base ?? 'https://base.invalid/')\n return schemes.has(url.protocol)\n } catch {\n return false\n }\n}\n\n/** A link target the page may carry: http(s), mailto or tel. */\nexport function isSafeLinkUrl(value: string, base?: string): boolean {\n return isSafeUrl(value, LINK_SCHEMES, base)\n}\n\nexport function isSafeImageUrl(value: string, base?: string): boolean {\n return isSafeUrl(value, IMAGE_SCHEMES, base)\n}\n\n/** Every candidate URL in a `srcset` must be a safe image URL. */\nexport function isSafeSrcset(value: string, base?: string): boolean {\n const candidates = value.split(',').map((part) => part.trim().split(/\\s+/)[0] ?? '')\n return candidates.length > 0 && candidates.every((url) => url !== '' && isSafeImageUrl(url, base))\n}\n\nfunction cleanAttributes(element: Element, tag: string, base: string | undefined): void {\n const allowed = RICH_TEXT_ALLOWED_ATTRS[tag]\n const global = RICH_TEXT_ALLOWED_ATTRS['*']!\n for (const attribute of Array.from(element.attributes)) {\n const name = attribute.name.toLowerCase()\n const value = attribute.value\n let keep = global.has(name) || (allowed?.has(name) ?? false)\n if (keep && name === 'style') keep = ALLOWED_STYLE.test(value)\n if (keep && name === 'href') keep = isSafeUrl(value, LINK_SCHEMES, base)\n if (keep && name === 'src') keep = isSafeImageUrl(value, base)\n if (keep && name === 'target') keep = value === '_blank'\n if (keep && (name === 'width' || name === 'height')) keep = DIMENSION.test(value)\n if (keep && name === 'data-media-id') keep = MEDIA_ID.test(value)\n if (!keep) element.removeAttribute(attribute.name)\n }\n if (tag === 'a' && element.getAttribute('target') === '_blank') {\n element.setAttribute('rel', 'noopener noreferrer')\n } else if (tag === 'a') {\n element.removeAttribute('rel')\n }\n}\n\nfunction walk(parent: ParentNode, base: string | undefined): void {\n for (const node of Array.from(parent.childNodes)) {\n if (node.nodeType === 3 /* TEXT */) continue\n if (node.nodeType !== 1 /* ELEMENT */) {\n // Comments, processing instructions, CDATA: never kept.\n node.parentNode?.removeChild(node)\n continue\n }\n const element = node as Element\n const tag = element.localName.toLowerCase()\n const foreign = element.namespaceURI !== 'http://www.w3.org/1999/xhtml'\n if (foreign || RICH_TEXT_DROPPED_TAGS.has(tag)) {\n element.remove()\n continue\n }\n walk(element, base)\n if (!RICH_TEXT_ALLOWED_TAGS.has(tag)) {\n // Unknown formatting (div, section, font, …): keep the text, drop the tag.\n element.replaceWith(...Array.from(element.childNodes))\n continue\n }\n cleanAttributes(element, tag, base)\n // An image whose src did not survive is dropped, not left broken.\n if (tag === 'img' && !element.hasAttribute('src')) element.remove()\n }\n}\n\n/**\n * Sanitise `html` into a fragment owned by `doc`, ready for\n * `element.replaceChildren(fragment)`. Never sets `innerHTML` on a live node.\n */\nexport function sanitizeRichText(\n html: string,\n doc: Document,\n base: string | undefined = doc.baseURI,\n): DocumentFragment {\n const template = doc.createElement('template')\n template.innerHTML = html\n const content = template.content\n walk(content, base)\n return content\n}\n\n/** The sanitised HTML as a string (for tests and for comparisons). */\nexport function sanitizeRichTextToString(html: string, doc: Document): string {\n const holder = doc.createElement('div')\n holder.appendChild(sanitizeRichText(html, doc))\n return holder.innerHTML\n}\n\n/** Plain text of a rich-text value, for an element that did not opt into HTML. */\nexport function richTextToText(html: string, doc: Document): string {\n // Through the sanitiser first, so the text of a dropped <script> or <style>\n // never shows up as page copy.\n return sanitizeRichText(html, doc).textContent ?? ''\n}\n","import { cleanStega, splitStega } from '../stega'\nimport type { PreviewValue, ValuesPayload } from './protocol'\nimport {\n isSafeImageUrl,\n isSafeLinkUrl,\n isSafeSrcset,\n richTextToText,\n sanitizeRichText,\n} from './sanitize'\nimport type { TagIndex, TaggedElement } from './tags'\n\n/**\n * Draft value substitution (`zap:values`, §2.4 \"Draft values\").\n *\n * What is touched, and nothing else:\n *\n * - text (`string`, `number`, `boolean`, `{ text }`) → `textContent`; on an\n * `<img>` it becomes `alt` (an image tagged with an alt-text field).\n * - `{ image }` → `src`, `srcset` and `alt` of the element when it is an\n * `<img>`, else of its first descendant `<img>`; `<source>` siblings in a\n * `<picture>` lose `srcset` so the browser actually shows the new `src`.\n * URLs must be http(s) (relative resolves against the page).\n * - `{ html }` → sanitised children (never `innerHTML` on the live node), ONLY\n * when the element carries `data-zap-html`; otherwise its plain text.\n * - `{ url }` (a URL field) and `{ email }` (an EMAIL field, as `mailto:`) →\n * the `href` of the element when it is an `<a>`, else of its first\n * descendant `<a>`; the link's label stays the site's. With no link, the\n * address is the text. Only http(s), mailto and tel links. (An image field\n * is `{ image }`, above: its `src`, never its text.)\n * - `null` → empty text; an image keeps what it had.\n *\n * Never an attribute other than `href`, `src`, `srcset` and `alt`, never a\n * style, never an event handler.\n *\n * An element found by a stega marker (`source: 'stega'`) gets text only, and\n * only in the text node that carries the marker, which keeps it: the rest of\n * the element (other text, other fields, markup) is the site's. Rich text and\n * images found that way are left to the site's `onValues` handler.\n *\n * ## Formatting is the editor's job\n *\n * Numbers, dates and enum labels arrive already formatted (`PreviewValue`\n * says why): the browser core has no locale data, no field types and a size\n * budget, and the editor already has the suite's formatters.\n *\n * ## Re-application\n *\n * The last value per field is remembered, so when the site re-renders a tagged\n * node (hydration, client routing) `reapply()` writes it again. An element is\n * rewritten only when it differs from what we would write, so our own writes\n * — which the mutation observer also sees — settle after one pass.\n */\n\ninterface Desired {\n locale: string\n value: PreviewValue\n}\n\nexport class ValueApplier {\n private desired = new Map<string, Desired>()\n private appliedHtml = new WeakMap<Element, { source: string; output: string }>()\n\n constructor(\n private readonly index: TagIndex,\n private readonly doc: Document,\n ) {}\n\n /** Remember and apply a patch. Returns how many elements were written. */\n apply(payload: ValuesPayload): number {\n let written = 0\n for (const [fieldKey, raw] of Object.entries(payload.patch)) {\n // Never a marker from a value: a stega text node keeps ITS markers and\n // appends the value, so a value carrying markers (text copied from a\n // preview into the form) would add more on every pass, and each new\n // marker is one more tagged entry at the next scan — the index and the\n // node grow without bound until the tab runs out of memory.\n const value = cleanStega(raw)\n this.desired.set(`${payload.recordRef}#${fieldKey}`, { locale: payload.locale, value })\n for (const tagged of this.index.byField(payload.recordRef, fieldKey)) {\n if (this.write(tagged, payload.locale, value)) written++\n }\n }\n return written\n }\n\n /** Write every remembered value again where the page drifted from it. */\n reapply(): number {\n let written = 0\n for (const tagged of this.index.elements) {\n const desired = this.desired.get(tagged.tag)\n if (desired && this.write(tagged, desired.locale, desired.value)) written++\n }\n return written\n }\n\n private write(tagged: TaggedElement, locale: string, value: PreviewValue): boolean {\n // An element pinned to another locale shows that locale's copy; leave it.\n if (tagged.locale && tagged.locale !== locale) return false\n const element = tagged.element\n\n if (tagged.textNode) {\n const text = plainText(value)\n if (text === null) return false\n const node = tagged.textNode\n // The node is the field's own text; keep the whitespace a template put before it.\n const split = splitStega(node.data)\n const next = /^\\s*/.exec(split.text)![0] + text + split.markers\n if (node.data === next) return false\n node.data = next\n return true\n }\n\n if (value !== null && typeof value === 'object' && 'image' in value) {\n return writeImage(element, value.image, this.doc.baseURI)\n }\n\n if (value !== null && typeof value === 'object' && 'html' in value) {\n if (tagged.html) return this.writeHtml(element, value.html)\n return writeText(element, richTextToText(value.html, this.doc))\n }\n\n if (value !== null && typeof value === 'object' && ('url' in value || 'email' in value)) {\n return writeLink(element, value, this.doc.baseURI)\n }\n\n const text = plainText(value) ?? ''\n if (isImg(element)) return setAttr(element, 'alt', text)\n return writeText(element, text)\n }\n\n private writeHtml(element: Element, html: string): boolean {\n // Unchanged when the source is the one we last wrote AND the node still\n // holds exactly our output (the site did not re-render it since).\n const last = this.appliedHtml.get(element)\n if (last && last.source === html && last.output === element.innerHTML) return false\n element.replaceChildren(sanitizeRichText(html, this.doc))\n this.appliedHtml.set(element, { source: html, output: element.innerHTML })\n return true\n }\n}\n\n/** A value as plain text, or null for the shapes that are not text (html, image). */\nfunction plainText(value: PreviewValue): string | null {\n if (value === null || typeof value !== 'object') return value === null ? '' : String(value)\n const v = value as { text?: string; url?: string; email?: string }\n return v.text ?? v.url ?? v.email ?? null\n}\n\n/** A URL or EMAIL value: the `href` of the link, never its label. */\nfunction writeLink(element: Element, value: { url: string } | { email: string }, base: string) {\n const email = 'email' in value\n const address = email ? value.email : value.url\n const link = element.localName === 'a' ? element : element.querySelector('a')\n if (!link) return writeText(element, address)\n const href = email && address ? `mailto:${address}` : address\n return isSafeLinkUrl(href, base) && setAttr(link, 'href', href)\n}\n\nfunction isImg(element: Element): element is HTMLImageElement {\n return element.localName === 'img'\n}\n\nfunction writeText(element: Element, text: string): boolean {\n if (element.textContent === text) return false\n element.textContent = text\n return true\n}\n\nfunction setAttr(element: Element, name: string, value: string): boolean {\n if (element.getAttribute(name) === value) return false\n element.setAttribute(name, value)\n return true\n}\n\nfunction writeImage(\n element: Element,\n image: { src?: string | null; srcset?: string | null; alt?: string | null },\n base: string,\n): boolean {\n const img = isImg(element) ? element : element.querySelector('img')\n if (!img) return false\n let changed = false\n if (image.src && isSafeImageUrl(image.src, base)) {\n changed = setAttr(img, 'src', image.src) || changed\n const picture = img.parentElement?.localName === 'picture' ? img.parentElement : null\n if (picture) {\n for (const source of Array.from(picture.children)) {\n if (source.localName === 'source' && source.hasAttribute('srcset')) {\n source.removeAttribute('srcset')\n changed = true\n }\n }\n }\n if (image.srcset === undefined || image.srcset === null) {\n // A stale srcset would win over the new src.\n if (img.hasAttribute('srcset')) {\n img.removeAttribute('srcset')\n changed = true\n }\n }\n }\n if (image.srcset && isSafeSrcset(image.srcset, base)) {\n changed = setAttr(img, 'srcset', image.srcset) || changed\n }\n if (typeof image.alt === 'string') changed = setAttr(img, 'alt', image.alt) || changed\n return changed\n}\n","import { buildAnchor, findAnchorElement } from './anchor'\nimport { Overlay } from './overlay'\nimport { createPageBridge, type MessageTargetLike, type PageBridge } from './page-bridge'\nimport {\n CAPABILITIES,\n PAGE_UNLOAD_ERROR,\n type EditorMessage,\n type PausedPayload,\n type TagSummary,\n} from './protocol'\nimport { observeTags, TagIndex, tagGuard, type TagProblem } from './tags'\nimport { ValueApplier } from './values'\n\n/**\n * The page runtime: index, overlay, values and bridge, wired together. The\n * Live client (`live.ts`) boots it inside the customer's own page, framed by\n * Zap's editor by its real URL (§2.7).\n */\n\nexport const CLIENT_VERSION = '0.10.0'\n\nexport interface RuntimeOptions {\n win?: Window\n doc?: Document\n parent?: MessageTargetLike\n /** Rescan throttle. Default 250 ms. */\n throttleMs?: number\n}\n\nexport interface PageRuntime {\n readonly bridge: PageBridge\n readonly index: TagIndex\n readonly overlay: Overlay\n /** Force a rescan + report now (specs, and after a navigation); nothing once paused. */\n refresh(): void\n destroy(): void\n}\n\nconst MAX_PROBLEM_REPORTS = 20\n\n/**\n * What one runtime needs beyond the DOM: who it talks to and how the page\n * behaves. The Live client builds it from the origins it trusts (`live.ts`).\n */\nexport interface RuntimeConfig {\n editorOrigins: readonly string[]\n /** Where `zap:ready` goes before the hello names the editor. */\n preHelloTargets?(): readonly string[]\n /** The site's draft-mode route path, announced in `zap:ready`. */\n draftRoute?: string | null\n /** The page's real URL (`location.href`; it moves on client-side routing). */\n pageUrl(): string\n /**\n * Nothing is added to the page (no overlay element, no listeners) until\n * a verified `zap:hello`, so a page framed by anyone else stays untouched (§7.3 \"Overlay as an attack surface on public pages\").\n */\n lazyOverlay?: boolean\n /**\n * A last say on a message the bridge accepted (the hello must name this\n * site). False ignores it, before the runtime touches the page.\n */\n accept?(message: EditorMessage): boolean\n /** Called after the runtime handled an editor message itself. */\n onMessage?(message: EditorMessage): void\n}\n\nexport function startRuntime(config: RuntimeConfig, options: RuntimeOptions = {}): PageRuntime {\n const win = options.win ?? window\n const doc = options.doc ?? win.document\n const parent = options.parent ?? (win.parent as MessageTargetLike)\n const index = new TagIndex(doc)\n const values = new ValueApplier(index, doc)\n let lastReport = ''\n const reportedProblems = new Set<string>()\n let stopObserving: () => void = () => {}\n // The runaway guard (`tagGuard`): once paused, nothing is scanned or written.\n const guard = tagGuard()\n let paused: PausedPayload | null = null\n /**\n * `localStorage['eelzap:debug']` set (any value) on the site's origin:\n * `console.debug('[zap:fields]', …)` for every tags report and its cause,\n * and every editor message dropped (a wrong session drops them all).\n */\n let debug = false\n try {\n debug = !!win.localStorage.getItem('eelzap:debug')\n } catch {\n // Blocked storage: no logs.\n }\n const log = (...args: unknown[]) => debug && console.debug('[zap:fields]', ...args)\n\n const overlay = new Overlay({\n doc,\n win,\n index,\n onInspect: (tagged) => {\n const { x, y, width: w, height: h } = tagged.element.getBoundingClientRect()\n bridge.post('zap:click', {\n recordRef: tagged.recordRef,\n fieldKey: tagged.fieldKey,\n rect: { x, y, w, h },\n })\n },\n onSelect: (elements) =>\n bridge.post('zap:select', {\n anchors: elements.map((element) => buildAnchor(element, index, config.pageUrl(), doc, win)),\n }),\n onSpot: (anchor) => bridge.post('zap:spot', { ...anchor, pageUrl: config.pageUrl() }),\n onPin: (pin) => bridge.post('zap:pin', pin),\n // Zap's one comment toggle: a click on the page or a wrapper is a spot.\n spotOnLarge: true,\n })\n\n const bridge = createPageBridge({\n win,\n parent,\n editorOrigins: config.editorOrigins,\n preHelloTargets: config.preHelloTargets,\n // Any window may post to the page: only drops from the editor are news.\n onDrop: (reason) => reason !== 'wrong-source' && log('drop', reason),\n onMessage: (message) => {\n if (config.accept && !config.accept(message)) return\n handle(message)\n config.onMessage?.(message)\n },\n })\n\n function handle(message: EditorMessage): void {\n switch (message.type) {\n case 'zap:hello': {\n const hello = message.payload\n greeted = true\n overlay.mount()\n overlay.setLabels(hello.labels, hello.foreign)\n overlay.setTheme(hello.theme)\n if (hello.zoom !== undefined) overlay.setZoom(hello.zoom)\n overlay.setMode(hello.mode)\n // The editor may have missed the load-time report (it was not\n // listening yet), so the hello is answered with the current tags.\n report('hello', true)\n // A pause before the hello never reached the editor.\n if (paused) bridge.post('zap:paused', paused)\n return\n }\n case 'zap:values':\n if (!paused) values.apply(message.payload)\n return\n case 'zap:focus-field':\n overlay.focusField(\n index.byField(message.payload.recordRef, message.payload.fieldKey).map((t) => t.element),\n )\n return\n case 'zap:mode':\n overlay.setMode(message.payload)\n return\n case 'zap:zoom':\n overlay.setZoom(message.payload.zoom)\n return\n case 'zap:refresh':\n log('refresh')\n win.location.reload()\n return\n case 'zap:highlight': {\n const found = message.payload.anchors\n .map((anchor) => findAnchorElement(anchor, index, doc, win))\n .filter((element): element is Element => element !== null)\n overlay.highlight(found)\n return\n }\n case 'zap:pins': {\n const pins = []\n for (const { id, n, spot, dom } of message.payload.pins) {\n // A dom pin whose element is gone is not drawn (like a highlight).\n const element = dom ? findAnchorElement(dom, index, doc, win) : null\n if (spot) pins.push({ id, n, spot })\n else if (element) pins.push({ id, n, element })\n }\n overlay.setPins(pins)\n return\n }\n }\n }\n\n function report(why: string, force = false): void {\n const summary: TagSummary[] = index.summary()\n const key = JSON.stringify(summary)\n if (force || key !== lastReport) {\n lastReport = key\n bridge.post('zap:tags', summary)\n log('tags', why, summary.length, config.pageUrl(), bridge.session)\n }\n reportProblems(index.problems)\n }\n\n function reportProblems(problems: TagProblem[]): void {\n for (const problem of problems) {\n const key = `${problem.attribute}=${problem.value}`\n if (reportedProblems.has(key) || reportedProblems.size >= MAX_PROBLEM_REPORTS) continue\n reportedProblems.add(key)\n bridge.post('zap:error', { code: 'TAG_INVALID', detail: key.slice(0, 500) })\n }\n }\n\n // A client-side route change (pushState) moves the page without a load; it\n // shows up as DOM mutations, so the rescan notices the new URL and tells the\n // editor. A real navigation loads a new document, which says ready again.\n let lastUrl = config.pageUrl()\n /** Rescan; false once the guard paused the page (it stops observing, writes nothing more). */\n function scan(page: boolean): boolean {\n if (paused) return false\n index.scan()\n const count = index.elements.length\n const reason = guard(count, page)\n if (!reason) return true\n paused = { reason, count }\n stopObserving()\n console.warn('eelzap: preview paused', paused)\n bridge.post('zap:paused', paused)\n return false\n }\n\n function refresh(page = true): void {\n if (!scan(page)) return\n values.reapply()\n report(page ? 'mutation' : 'reapply')\n const url = config.pageUrl()\n if (url !== lastUrl) {\n lastUrl = url\n bridge.post('zap:navigate', { url })\n }\n }\n\n // Listeners go in NOW, at script start (or, with `lazyOverlay`, the overlay's\n // at the verified hello). When the frame navigates, the editor hears of it\n // before the next document can receive anything (see PAGE_UNLOAD_ERROR).\n const onPageHide = () => {\n bridge.post('zap:error', { code: PAGE_UNLOAD_ERROR })\n }\n // A document restored from the back-forward cache never loads again: it\n // says ready again, and the editor answers with its hello.\n const onPageShow = (event: PageTransitionEvent) => {\n if (event.persisted) {\n log('pageshow')\n announce()\n }\n }\n /** Whether a hello arrived: until then, `zap:ready` is said again. */\n let greeted = false\n const retries: number[] = []\n function announce(): void {\n log('ready', config.pageUrl())\n bridge.post('zap:ready', {\n version: CLIENT_VERSION,\n capabilities: [...CAPABILITIES],\n pageUrl: config.pageUrl(),\n draftRoute: config.draftRoute ?? null,\n })\n }\n\n if (!config.lazyOverlay) overlay.install()\n win.addEventListener('pagehide', onPageHide)\n win.addEventListener('pageshow', onPageShow)\n\n function boot(): void {\n if (!config.lazyOverlay) overlay.mount()\n if (scan(true)) {\n stopObserving = observeTags(doc.documentElement, refresh, {\n throttleMs: options.throttleMs,\n ignore: (node) => {\n const host = overlay.hostElement\n return !!host && (node === host || host.contains(node))\n },\n })\n }\n announce()\n report('ready', true)\n // An editor whose listener mounted after this ready (a soft navigation in\n // Zap) never answers it: say it again until a hello comes.\n for (const ms of [500, 1500, 4000])\n retries.push(win.setTimeout(() => greeted || announce(), ms))\n }\n\n if (doc.readyState === 'loading') {\n doc.addEventListener('DOMContentLoaded', boot, { once: true })\n } else {\n boot()\n }\n\n return {\n bridge,\n index,\n overlay,\n refresh,\n destroy() {\n for (const id of retries) win.clearTimeout(id)\n stopObserving()\n win.removeEventListener('pagehide', onPageHide)\n win.removeEventListener('pageshow', onPageShow)\n overlay.destroy()\n bridge.destroy()\n },\n }\n}\n","import {\n isDraftRoutePath,\n isPreviewTokenShape,\n type EditorMessage,\n type PreviewValue,\n type RecordRef,\n type ValuesPayload,\n} from './protocol'\nimport { isLocalHost } from '../boot/local'\nimport { NEST_PRODUCTION_ORIGIN } from './site/config'\nimport { startSiteMode } from './site/boot'\nimport { startRuntime, type PageRuntime, type RuntimeOptions } from './runtime'\n\n/**\n * The Live client («En vivo», `.docs/proposals/zap-cms-v2.md` §2.5–2.7): the\n * page runtime (`runtime.ts`: tag index, overlay, value substitution, bridge),\n * booted by the customer's site itself, which Zap's editor frames by its real\n * URL.\n *\n * ```ts\n * import { initZap, onValues, zapAttrs } from '@8ux-co/eelzap/preview'\n *\n * initZap({ siteKey: 'verdeorigen' })\n * <h1 {...zapAttrs(post, 'title')}>{post.content.title}</h1>\n * onValues(post, (patch) => …) // optional: values the site computes from\n * ```\n *\n * ## Who the client listens to (§2.6 \"Origins\")\n *\n * A message from the editor is accepted only when ALL hold, and every other\n * message is dropped and counted (`page-bridge.ts`):\n *\n * 1. `event.source === window.parent`;\n * 2. `event.origin` is the production editor (`https://zap.eel.software`)\n * or the development origin opted into below. Zap's own preview\n * deployments are not editors: no deployment-specific origin is compiled\n * into a public bundle;\n * 3. it parses as a v1 envelope under 256 KB;\n * 4. after the first `zap:hello`, its `session` matches.\n *\n * And the hello itself must name THIS site (`siteKey`); a hello for another\n * site is ignored, and nothing is drawn or stored for it.\n *\n * ## Development: `zapOrigin`\n *\n * `zapOrigin` is the only way to trust another origin, and it is honoured only\n * for a local Zap (`localhost`, `127.0.0.1`, `[::1]` or a `*.localhost` name,\n * over http or https, any port) AND only while the page itself runs on one of\n * those hosts. The boot sets it to the Zap it loaded this file from when that\n * is a local Zap framing a local page, so the site need not pass it. A\n * production page never trusts a local editor, even when the option is left in\n * the site's code; any other value is ignored with a console warning. The\n * production editor needs no option.\n *\n * ## Not framed by Zap, nothing happens\n *\n * Outside a frame the client does nothing at all. Inside a frame it posts one\n * `zap:ready` to the candidate parent origin (and the browser drops it unless\n * the parent really is that Zap origin), and adds nothing to the page — no\n * element, no listener beyond `message` — until a verified hello arrives.\n */\n\n/** The production editor. Always trusted. */\nexport const ZAP_PRODUCTION_ORIGIN = 'https://zap.eel.software'\n\n/** The header the cookieless fallback sends the preview token in (§2.5). */\nexport const ZAP_PREVIEW_TOKEN_HEADER = 'x-zap-preview-token'\n/** Where the client keeps it, per tab and per frame. */\nexport const PREVIEW_TOKEN_STORAGE_KEY = 'eelzap:preview-token'\n\nfunction parseUrl(value: string): URL | null {\n try {\n return new URL(value)\n } catch {\n return null\n }\n}\n\n/**\n * The extra origin `zapOrigin` adds, or null. See \"Development\" above: a\n * local Zap, from a local page, nothing else.\n */\nexport function developmentZapOrigin(\n zapOrigin: string | undefined,\n pageHostname: string,\n): string | null {\n if (!zapOrigin) return null\n const url = parseUrl(zapOrigin)\n if (!url || (url.protocol !== 'http:' && url.protocol !== 'https:')) return null\n if (url.origin === ZAP_PRODUCTION_ORIGIN) return null\n if (!isLocalHost(url.hostname) || !isLocalHost(pageHostname)) return null\n return url.origin\n}\n\n// ── Record references ───────────────────────────────────────────────────────\n\nexport { recordRefsOf, zapAttrs, type EntryLike } from '../refs'\nimport { recordRefsOf, type EntryLike } from '../refs'\nexport { cleanStega } from '../stega'\n\n// ── Values ──────────────────────────────────────────────────────────────────\n\nexport type ValuesHandler = (patch: Record<string, PreviewValue>, payload: ValuesPayload) => void\n\ninterface Subscription {\n refs: RecordRef[]\n handler: ValuesHandler\n}\n\nconst subscriptions = new Set<Subscription>()\n\n/**\n * Call `handler` with each `zap:values` patch for `entry` (unsaved keystrokes\n * from the editor), for sites that compute things from values. The tagged\n * elements are updated anyway; this is for everything else. Values arrive\n * formatted for display, exactly as the overlay writes them. Returns an\n * unsubscribe function. Works before or after `initZap`.\n */\nexport function onValues(entry: EntryLike, handler: ValuesHandler): () => void {\n return subscribeRefs(recordRefsOf(entry), handler)\n}\n\n/** `onValues` for references already resolved (the React hook). */\nexport function subscribeRefs(refs: readonly RecordRef[], handler: ValuesHandler): () => void {\n const subscription: Subscription = { refs: [...refs], handler }\n subscriptions.add(subscription)\n return () => {\n subscriptions.delete(subscription)\n }\n}\n\n/**\n * The window event every `zap:values` patch is also announced with, so code\n * bundled apart from this client (`useZapLiveUpdates` and `onValues` in\n * `@8ux-co/eelzap/react`, which never bundle the overlay) hears it.\n */\nexport const VALUES_EVENT = 'eelzap:values'\n\nfunction dispatchValues(payload: ValuesPayload, win?: Window): void {\n win?.dispatchEvent(new CustomEvent(VALUES_EVENT, { detail: payload }))\n for (const subscription of [...subscriptions]) {\n if (!subscription.refs.includes(payload.recordRef)) continue\n try {\n subscription.handler(payload.patch, payload)\n } catch (error) {\n // A site handler that throws must not take the bridge down with it.\n console.error('eelzap: onValues handler threw', error)\n }\n }\n}\n\n// ── Preview token (cookieless fallback) ─────────────────────────────────────\n\nfunction sessionStore(win: Window | undefined): Storage | null {\n try {\n return win?.sessionStorage ?? null\n } catch {\n return null\n }\n}\n\n/**\n * The preview token the editor handed this frame, or null. For the\n * cookieless fallback: send it to your own server with `previewHeaders()`,\n * where `getPreviewToken` from `@8ux-co/eelzap/next` reads it.\n */\nexport function getPreviewToken(win: Window | undefined = globalWindow()): string | null {\n try {\n const value = sessionStore(win)?.getItem(PREVIEW_TOKEN_STORAGE_KEY) ?? null\n return isPreviewTokenShape(value) ? value : null\n } catch {\n return null\n }\n}\n\n/** `{ 'x-zap-preview-token': token }` while this frame has one, else `{}`. */\nexport function previewHeaders(win?: Window): Record<string, string> {\n const token = getPreviewToken(win ?? globalWindow())\n return token ? { [ZAP_PREVIEW_TOKEN_HEADER]: token } : {}\n}\n\nfunction forgetPreviewToken(win: Window): void {\n try {\n sessionStore(win)?.removeItem(PREVIEW_TOKEN_STORAGE_KEY)\n } catch {\n // Nothing stored.\n }\n}\n\nfunction globalWindow(): Window | undefined {\n return typeof window === 'undefined' ? undefined : window\n}\n\n// ── initZap ─────────────────────────────────────────────────────────────────\n\nexport interface InitZapOptions {\n /** The Zap site's key (Configuración del sitio). A hello for another site is ignored. */\n siteKey: string\n /**\n * The Zap site's id. Turns on Live suggestion mode outside Zap's frame\n * (§3.4): it names the site's OAuth client. Without it the client only\n * talks to Zap's editor.\n */\n siteId?: string\n /** A local Zap, for development only (see \"Development\" above). */\n zapOrigin?: string\n /** A local Nest (the OAuth issuer), for development only, under the same rule as `zapOrigin`. */\n authOrigin?: string\n /** The letter pressed with Shift to open «Editar» and «Comentar». Default `Z`; false turns it off. */\n shortcut?: string | false\n /** The language of the suggestion UI; default from `<html lang>`, Spanish otherwise. */\n locale?: 'es' | 'en'\n /** Script-tag build only: the core's own URL, so its chunks load from beside it. */\n chunkBase?: string | null\n /**\n * The site's draft-mode route, a path such as `/api/zap-preview`, announced\n * to the editor in `zap:ready` (§2.5). Anything that is not a plain path is\n * dropped (`isDraftRoutePath`).\n */\n draftRoute?: string | null\n /** The boot saw the shortcut: open the site tools at once. */\n open?: boolean\n}\n\nexport interface ZapLive {\n /** Framed and listening; false outside a frame or after `destroy`. */\n readonly active: boolean\n /** The underlying runtime, null when inactive (tests and debugging). */\n readonly runtime: PageRuntime | null\n destroy(): void\n}\n\nlet current: ZapLive | null = null\n\nconst INERT: ZapLive = {\n active: false,\n runtime: null,\n destroy() {},\n}\n\n/**\n * Start the Live client. Call once per page, as early as convenient; a second\n * call returns the first one's handle (a script tag and a bundled import on\n * the same page do not start two clients).\n */\nexport function initZap(options: InitZapOptions, runtimeOptions: RuntimeOptions = {}): ZapLive {\n if (current?.active) return current\n const win = runtimeOptions.win ?? globalWindow()\n if (!win) return INERT\n const siteKey = typeof options?.siteKey === 'string' ? options.siteKey.trim() : ''\n if (!siteKey || siteKey.length > 128) {\n console.warn('eelzap: no site key')\n return INERT\n }\n const parent = runtimeOptions.parent ?? (win.parent as Window | null)\n const dev = developmentZapOrigin(options.zapOrigin, win.location.hostname)\n if (options.zapOrigin && !dev && safeOrigin(options.zapOrigin) !== ZAP_PRODUCTION_ORIGIN) {\n console.warn('eelzap: zapOrigin is for a local Zap on a local page')\n }\n if (!parent || (parent as unknown) === win) return startSuggestions(options, win, dev)\n\n const exact = dev ? [ZAP_PRODUCTION_ORIGIN, dev] : [ZAP_PRODUCTION_ORIGIN]\n\n let helloOk = false\n const draftRoute = isDraftRoutePath(options.draftRoute) ? options.draftRoute : null\n const runtime = startRuntime(\n {\n editorOrigins: exact,\n preHelloTargets: () => {\n const candidate = parentOriginHint(win)\n return candidate && exact.includes(candidate) ? [candidate] : exact\n },\n draftRoute,\n pageUrl: () => win.location.href,\n lazyOverlay: true,\n accept: (message) => {\n if (message.type === 'zap:hello') {\n helloOk = message.payload.siteKey === siteKey\n return helloOk\n }\n return helloOk\n },\n onMessage: (message: EditorMessage) => {\n if (message.type === 'zap:hello') {\n // A hello without a token (drafts off, or a new session) forgets the\n // last one: a stale token must not keep serving drafts.\n try {\n const token = message.payload.previewToken\n const store = sessionStore(win)\n const last = store?.getItem(PREVIEW_TOKEN_STORAGE_KEY)\n if (token) store?.setItem(PREVIEW_TOKEN_STORAGE_KEY, token)\n else forgetPreviewToken(win)\n // The editor renewed the draft session ahead of its end: the new\n // token goes through the site's draft route in the background,\n // which sets the draft cookies again without navigating, so the\n // next reload still reads drafts. The redirect is not followed.\n if (token && last && last !== token && draftRoute) {\n const query = new URLSearchParams({ token, path: win.location.pathname })\n void win\n .fetch(`${draftRoute}?${query}`, { credentials: 'same-origin', redirect: 'manual' })\n .catch(() => {})\n }\n } catch {\n // Storage blocked: the draft-mode cookie is the only path left.\n }\n } else if (message.type === 'zap:values') {\n dispatchValues(message.payload, win)\n }\n },\n },\n { ...runtimeOptions, win, parent: parent as unknown as RuntimeOptions['parent'] },\n )\n\n const handle: ZapLive = {\n get active() {\n return current === handle\n },\n runtime,\n destroy() {\n runtime.destroy()\n forgetPreviewToken(win)\n if (current === handle) current = null\n },\n }\n current = handle\n return handle\n}\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\nlet stopSuggestions: (() => void) | null = null\n\n/**\n * Not framed: Live suggestion mode (§3.4, `site/boot.ts`), when the site\n * passed its id. Zap is production or the local dev opt-in, never a preview\n * deployment; Nest likewise (`authOrigin` follows the `zapOrigin` rule).\n */\nfunction startSuggestions(options: InitZapOptions, win: Window, devZap: string | null): ZapLive {\n const siteId = typeof options.siteId === 'string' ? options.siteId.trim() : ''\n if (stopSuggestions || !UUID_RE.test(siteId)) return INERT\n const local = isLocalHost(win.location.hostname)\n const nest = parseUrl(options.authOrigin ?? '')\n const devNest = local && nest && isLocalHost(nest.hostname) ? nest.origin : null\n const stop = startSiteMode({\n win,\n siteId,\n zapOrigin: devZap ?? ZAP_PRODUCTION_ORIGIN,\n authOrigin: devNest ?? NEST_PRODUCTION_ORIGIN,\n shortcut: options.shortcut,\n locale: options.locale,\n chunkBase: options.chunkBase,\n open: options.open,\n draftRoute: isDraftRoutePath(options.draftRoute) ? options.draftRoute : null,\n })\n stopSuggestions = stop\n return {\n active: false,\n runtime: null,\n destroy() {\n stop()\n if (stopSuggestions === stop) stopSuggestions = null\n },\n }\n}\n\nfunction safeOrigin(value: string): string {\n return parseUrl(value)?.origin ?? ''\n}\n\n/**\n * Who framed us, as far as the page can tell before the hello: Chrome and\n * Safari expose `location.ancestorOrigins`; otherwise the referrer, which the\n * editor sends as its origin (`referrerpolicy=\"origin\"`). Only a hint: the\n * browser delivers `zap:ready` only if the parent really is that origin.\n */\nfunction parentOriginHint(win: Window): string | null {\n const ancestors = (win.location as Location & { ancestorOrigins?: DOMStringList }).ancestorOrigins\n if (ancestors && ancestors.length > 0) return ancestors[0] ?? null\n const referrer = win.document.referrer\n return referrer ? safeOrigin(referrer) || null : null\n}\n\n/** Specs only: forget the page's client and every `onValues` subscription. */\nexport function __resetLiveForTests(): void {\n current?.destroy()\n current = null\n stopSuggestions?.()\n stopSuggestions = null\n subscriptions.clear()\n}\n"],"mappings":"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","names":["current","text"]}
1
+ {"version":3,"sources":["../src/boot/local.ts","../src/preview/site/load.ts","../src/preview/site/boot.ts","../src/preview/page-bridge.ts","../src/preview/scroll.ts","../src/preview/sanitize.ts","../src/preview/values.ts","../src/preview/runtime.ts","../src/preview/live.ts"],"sourcesContent":["/**\n * Development origins: `localhost`, `127.0.0.1`, `[::1]` and any `*.localhost`\n * name (which browsers resolve to the machine itself and treat as secure\n * contexts), over http OR https, any port: a site in development runs on\n * either.\n *\n * Shared by the boot (`./boot.ts`, which every visitor runs, so one regex and\n * no more) and the overlay (`../preview/live.ts`), so the two never disagree\n * about what counts as local.\n */\nexport const LOCAL_ORIGIN =\n /^https?:\\/\\/(?:(?:[a-z\\d-]+\\.)*localhost|127\\.0\\.0\\.1|\\[::1\\])(?::\\d+)?$/i\n\n/** True for a development hostname (no scheme, no port). */\nexport const isLocalHost = (hostname: string): boolean => LOCAL_ORIGIN.test(`http://${hostname}`)\n","import type { ChunkModule, ChunkName } from './config'\n\n/**\n * The npm build's chunk loader: a dynamic `import()`, which the site's bundler\n * (and this package's own ESM build, `tsup` with splitting) turns into a\n * separate file fetched only when called. The script-tag build swaps this\n * module for `load-cdn.ts` at bundle time (`scripts/bundle-config.mjs`).\n */\nexport function loadChunk(name: ChunkName, _base?: string | null): Promise<ChunkModule> {\n return name === 'signin' ? import('./signin') : import('./suggest')\n}\n","import {\n PENDING_STORAGE_KEY,\n SIGNIN_CHANNEL,\n siteClientId,\n STATE_PREFIX,\n type CallbackParams,\n type ChunkHandle,\n type ChunkModule,\n type ChunkName,\n type SiteContext,\n type StartOptions,\n} from './config'\nimport { loadChunk } from './load'\nimport { DEFAULT_SHORTCUT, isShortcut } from './shortcut'\nimport { readSession } from './token'\n\n/**\n * The core's part of Live suggestion mode (zap-cms-v2 §3.4, ADR 041), for a\n * page that is NOT framed by Zap. It draws nothing and loads nothing until one\n * of these holds:\n *\n * 1. **This page is the OAuth callback**: `{origin}/?code&state&iss` with a\n * `state` this client minted and `iss` = the compiled-in issuer. In the\n * sign-in popup the code goes to the opener over\n * `BroadcastChannel('eel-zap')` and the popup closes; after a full-page\n * sign-in (the popup was blocked) the `signin` chunk finishes here. Either\n * way the code leaves the address bar at once.\n * 2. **This tab holds a live token** for this site (or a refresh token that\n * can renew it, `session.ts`): the `suggest` chunk.\n * 3. **The person asks**: `?zap` in the URL, or the shortcut (Shift Z by\n * default, never while typing in a field or composing, `shortcut.ts`): the\n * `signin` chunk, which shows the launcher.\n *\n * Ordinary visitors meet none of these, so they get one `keydown` listener\n * and nothing else. Whatever the callback carries is checked again by the\n * `signin` chunk (state, issuer, the PKCE verifier only the opener holds);\n * this file only decides whether a page looks like ours.\n */\n\nexport interface SiteModeOptions {\n win: Window\n siteId: string\n zapOrigin: string\n authOrigin: string\n locale?: 'es' | 'en'\n /** The letter pressed with Shift to open the launcher; false turns the shortcut off. */\n shortcut?: string | false\n /** Script-tag build: the core's own URL, the chunks are its siblings. */\n chunkBase?: string | null\n /** Open at once: the boot already saw the trigger (the shortcut). */\n open?: boolean\n /** The site's draft-mode route, for «Editar» on an untagged page (`draft-session.ts`). */\n draftRoute?: string | null\n}\n\n/** The callback params when this page is one of ours, else null. */\nexport function readCallback(win: Window, authOrigin: string): CallbackParams | null {\n const { pathname, search } = win.location\n if (pathname !== '/' || !search) return null\n const params = new URLSearchParams(search)\n const code = params.get('code')\n const state = params.get('state')\n const iss = params.get('iss')\n if (!code || !state?.startsWith(STATE_PREFIX) || iss !== authOrigin) return null\n return { code, state, iss }\n}\n\nfunction pendingRedirect(win: Window): boolean {\n try {\n return JSON.parse(win.sessionStorage.getItem(PENDING_STORAGE_KEY) ?? '{}').mode === 'redirect'\n } catch {\n return false\n }\n}\n\nfunction pageLocale(doc: Document): 'es' | 'en' {\n return /^en\\b/i.test(doc.documentElement.lang) ? 'en' : 'es'\n}\n\nexport function startSiteMode(options: SiteModeOptions): () => void {\n const { win, siteId } = options\n const doc = win.document\n const key = options.shortcut === undefined ? DEFAULT_SHORTCUT : options.shortcut\n let current: ChunkHandle | null = null\n let opening = false\n\n const ctx: SiteContext = {\n win,\n doc,\n siteId,\n zapOrigin: options.zapOrigin,\n authOrigin: options.authOrigin,\n clientId: siteClientId(options.zapOrigin, siteId),\n redirectUri: `${win.location.origin}/`,\n locale: options.locale ?? pageLocale(doc),\n shortcut: key,\n draftRoute: options.draftRoute ?? null,\n async open(name: ChunkName, startOptions?: StartOptions) {\n opening = true\n try {\n const module: ChunkModule = await loadChunk(name, options.chunkBase)\n current?.destroy()\n current = module.start(ctx, startOptions)\n } catch (error) {\n console.warn('eelzap: suggestions failed', error)\n } finally {\n opening = false\n }\n },\n close() {\n current?.destroy()\n current = null\n },\n }\n\n const callback = readCallback(win, options.authOrigin)\n if (callback) {\n // The code and state must not stay in the address bar or the history.\n win.history.replaceState(win.history.state, '', '/')\n if (pendingRedirect(win)) {\n void ctx.open('signin', { callback })\n } else {\n try {\n const channel = new BroadcastChannel(SIGNIN_CHANNEL)\n channel.postMessage({ type: 'eelzap:callback', ...callback })\n channel.close()\n } finally {\n win.close()\n }\n }\n return ctx.close\n }\n\n const launch = () => {\n if (current || opening) return\n void ctx.open(readSession(win, siteId) ? 'suggest' : 'signin', { reason: 'trigger' })\n }\n\n if (readSession(win, siteId)) void ctx.open('suggest')\n else if (options.open || new URLSearchParams(win.location.search).has('zap')) launch()\n\n const onKey = (event: KeyboardEvent) => {\n if (key && isShortcut(event, key)) launch()\n }\n win.addEventListener('keydown', onKey, true)\n\n return () => {\n win.removeEventListener('keydown', onKey, true)\n ctx.close()\n }\n}\n","import {\n emptyDropStats,\n envelope,\n MAX_MESSAGE_BYTES,\n messageBytes,\n parseEditorMessage,\n type DropReason,\n type DropStats,\n type EditorMessage,\n type PageMessages,\n type PageMessageType,\n} from './protocol'\n\n/**\n * The PAGE side of the bridge (§2.6 \"Origins\").\n *\n * A message from the editor is accepted only when ALL hold:\n *\n * 1. `event.source === parent` — the window that framed us, nothing else\n * (another frame, a popup, the page itself posting to itself).\n * 2. `event.origin` is one of `editorOrigins` (the Zap origins the client\n * trusts). `'null'` (an opaque origin) is never accepted.\n * 3. It parses as a v1 envelope of a known editor to page type, under 256 KB.\n * 4. Its `session` matches. Before `zap:hello` the session is unknown, and\n * nothing but a hello is accepted; the first valid hello fixes it. After\n * that, every message must carry it; a second hello with another session\n * is refused.\n *\n * Every drop is counted in `stats` by reason; nothing throws.\n *\n * Neither side ever posts with `'*'`. The page answers the origin that said\n * hello (before it, each accepted candidate; the browser drops a message whose\n * target is not the parent's real origin). The editor posts only to the exact\n * page origin that said `zap:ready` (`editor.ts`).\n */\n\nexport interface MessageTargetLike {\n postMessage(message: unknown, targetOrigin: string): void\n}\n\nexport interface PageBridgeOptions {\n win: Window\n /** `window.parent` in production; injected in specs. */\n parent: MessageTargetLike\n editorOrigins: readonly string[]\n /**\n * Where to post BEFORE the hello names the editor's origin. Defaults\n * to `editorOrigins`. Every entry must itself be an accepted origin; the\n * browser drops a message whose target is not the parent's real origin.\n */\n preHelloTargets?(): readonly string[]\n onMessage(message: EditorMessage): void\n onDrop?(reason: DropReason): void\n}\n\nexport interface PageBridge {\n post<K extends PageMessageType>(type: K, payload: PageMessages[K]): boolean\n readonly session: string | null\n readonly stats: DropStats\n destroy(): void\n}\n\nexport function createPageBridge(options: PageBridgeOptions): PageBridge {\n const stats = emptyDropStats()\n let session: string | null = null\n let helloOrigin: string | null = null\n const origins = options.editorOrigins.filter((o) => typeof o === 'string' && o !== '')\n\n const isAccepted = (origin: string) =>\n typeof origin === 'string' && origin !== 'null' && origins.includes(origin)\n\n const drop = (reason: DropReason) => {\n stats[reason]++\n options.onDrop?.(reason)\n }\n\n const listener = (event: MessageEvent) => {\n if (event.source !== (options.parent as unknown)) return drop('wrong-source')\n if (!isAccepted(event.origin)) return drop('wrong-origin')\n const parsed = parseEditorMessage(event.data)\n if (!parsed.ok) return drop(parsed.reason)\n const message = parsed.message\n\n if (message.type === 'zap:hello') {\n if (message.payload.session !== message.session) return drop('invalid-payload')\n if (session !== null && message.session !== session) return drop('wrong-session')\n if (session === null) session = message.session\n helloOrigin = event.origin\n } else if (session === null || message.session !== session || helloOrigin === null) {\n // Nothing but a hello is accepted before the hello.\n return drop('wrong-session')\n }\n options.onMessage(message)\n }\n\n options.win.addEventListener('message', listener)\n\n return {\n post(type, payload) {\n const message = envelope(type, session ?? '', payload)\n if (messageBytes(message) > MAX_MESSAGE_BYTES) return false\n // Never '*': after the hello we answer the origin that said hello;\n // before it, each allowed origin (a non-matching target is dropped by\n // the browser, so only the real parent receives it).\n const targets = helloOrigin\n ? [helloOrigin]\n : (options.preHelloTargets?.() ?? origins).filter(isAccepted)\n for (const origin of targets) options.parent.postMessage(message, origin)\n return targets.length > 0\n },\n get session() {\n return session\n },\n stats,\n destroy() {\n options.win.removeEventListener('message', listener)\n },\n }\n}\n","import type { ScrollFieldRef, ScrollPayload } from './protocol'\nimport type { TagIndex, TaggedElement } from './tags'\n\n/**\n * Scroll sync for Comparar (#953, `scroll-sync` capability): two frames of\n * one page, the published one and the draft, scrolled together by the editor.\n *\n * - While enabled, each scroll of THIS window becomes one `zap:scroll` per\n * animation frame at most: the position as fractions of the scrollable\n * range; as `anchor` the last tagged element whose top is at or above the\n * viewport's top (the first in view while the page is above them all); as\n * `next` the one whose top is the nearest below it, and `progress`, how far\n * the viewport's top is from the anchor's to next's. Identical consecutive\n * reports are sent once.\n * - `scrollTo` puts the viewport's top at `progress` between this page's own\n * anchor and next (0.11.1): the two pages space their fields differently,\n * and placing by the field NEAREST the top made the follower jump each time\n * that field changed. Between the two, the position is continuous: at\n * progress 1 it is next's top, which is where progress 0 of the following\n * pair starts. Without both elements, by the anchor's offset; without the\n * anchor, by the fraction.\n * - `scrollTo` keeps the latest target and applies it once per animation\n * frame, instantly: messages arrive several or none per frame, and applying\n * each made the follower stutter; a smooth scroll (a site's\n * `scroll-behavior: smooth`) would still be animating when the next one\n * lands. The scroll events it causes are never reported: the position it\n * left is remembered, and a report at that position is dropped, so the\n * other frame never answers its own command (no feedback loop). The next\n * scroll away from it is the user's and is reported.\n * - No listener until the editor enables it: a page framed by anyone else\n * stays untouched.\n */\nexport interface ScrollSync {\n setEnabled(enabled: boolean): void\n scrollTo(target: ScrollPayload): void\n destroy(): void\n}\n\nconst round = (value: number) => Math.round(value * 1e4) / 1e4\n\nexport function createScrollSync(\n win: Window,\n doc: Document,\n index: TagIndex,\n post: (payload: ScrollPayload) => void,\n): ScrollSync {\n let enabled = false\n let frame: number | null = null\n let last = ''\n /** Where the last `scrollTo` left the page; a scroll event there is ours. */\n let placed: string | null = null\n /** The `scrollTo` waiting for the next frame; a newer one replaces it. */\n let target: ScrollPayload | null = null\n const at = () => `${Math.round(win.scrollX)},${Math.round(win.scrollY)}`\n const range = (size: number, view: number) => Math.max(0, size - view)\n const nextFrame = (fn: () => void) =>\n (typeof win.requestAnimationFrame === 'function'\n ? win.requestAnimationFrame(fn)\n : win.setTimeout(fn, 16)) as number\n\n function fieldRef(tagged: TaggedElement): ScrollFieldRef {\n const { recordRef, fieldKey } = tagged\n return { recordRef, fieldKey, nth: index.byField(recordRef, fieldKey).indexOf(tagged) }\n }\n\n /** A field ref's element top in THIS page's viewport, if it has that element. */\n function topOf(ref: ScrollFieldRef | undefined): number | undefined {\n const element = ref && index.byField(ref.recordRef, ref.fieldKey)[ref.nth]?.element\n return element?.isConnected ? element.getBoundingClientRect().top : undefined\n }\n\n function measure(): ScrollPayload {\n const root = doc.documentElement\n const fraction = (pos: number, size: number, view: number) => {\n const max = range(size, view)\n return max > 0 ? round(Math.min(1, Math.max(0, pos / max))) : 0\n }\n const payload: ScrollPayload = { y: fraction(win.scrollY, root.scrollHeight, win.innerHeight) }\n if (range(root.scrollWidth, win.innerWidth) > 0) {\n payload.x = fraction(win.scrollX, root.scrollWidth, win.innerWidth)\n }\n // Above: the highest top at or above the viewport's; below: the lowest under it.\n let above: TaggedElement | null = null\n let below: TaggedElement | null = null\n let up = -Infinity\n let down = Infinity\n for (const tagged of index.elements) {\n const { top, height } = tagged.element.getBoundingClientRect()\n if (height <= 0) continue\n if (top > 0) {\n if (top < down) {\n below = tagged\n down = top\n }\n } else if (top > up) {\n above = tagged\n up = top\n }\n }\n // Above them all: the first in view, by its offset.\n const anchor = above ?? (down < win.innerHeight ? below : null)\n if (anchor) {\n payload.anchor = { ...fieldRef(anchor), offset: Math.round(above ? up : down) }\n if (above && below) {\n payload.next = fieldRef(below)\n payload.progress = round(-up / (down - up))\n }\n }\n return payload\n }\n\n function flush(): void {\n frame = null\n if (!enabled) return\n if (at() === placed) return\n placed = null\n const payload = measure()\n const key = JSON.stringify(payload)\n if (key === last) return\n last = key\n post(payload)\n }\n\n /** The latest `scrollTo`, once per frame. */\n function apply(): void {\n const next = target\n target = null\n if (!next) return\n const root = doc.documentElement\n const { y, x, anchor, progress } = next\n let top = y * range(root.scrollHeight, win.innerHeight)\n const from = topOf(anchor)\n const to = topOf(next.next)\n if (from !== undefined) {\n top =\n win.scrollY +\n from +\n (to !== undefined && progress !== undefined ? progress * (to - from) : -anchor!.offset)\n }\n const left = x === undefined ? win.scrollX : x * range(root.scrollWidth, win.innerWidth)\n try {\n // `instant`, never the site's `scroll-behavior: smooth`: an animation\n // would fight the next frame's target.\n win.scrollTo({ top, left, behavior: 'instant' as ScrollBehavior })\n } catch {\n // A browser without `instant`: the default behaviour.\n win.scrollTo(left, top)\n }\n placed = at()\n last = JSON.stringify(measure())\n }\n\n const onScroll = () => {\n if (!enabled || frame !== null) return\n frame = nextFrame(flush)\n }\n\n return {\n setEnabled(next) {\n if (next === enabled) return\n enabled = next\n if (next) {\n win.addEventListener('scroll', onScroll, { passive: true })\n // Where the page is now, so the editor can align the other frame.\n last = ''\n placed = null\n flush()\n } else win.removeEventListener('scroll', onScroll)\n },\n scrollTo(next) {\n if (!target) nextFrame(apply)\n target = next\n },\n destroy() {\n enabled = false\n target = null\n win.removeEventListener('scroll', onScroll)\n },\n }\n}\n","/**\n * The allowlist sanitiser for rich-text substitution (§2.4 \"Draft values\",\n * §7.2 \"Rich text substitution\").\n *\n * ## Why this is not \"the delivery API's allowlist\"\n *\n * The spec says to sanitise \"with the same allowlist the delivery API\n * applies\". The delivery API applies NONE: `apps/zap/src/lib/cms/rich-text-media.ts`\n * strips `<script>…</script>` and nothing else, on purpose (its header: an\n * allowlist would drop `data-media-id` and change bytes, flipping every\n * delivery ETag). The suite's real allowlist is `@eel/ui-web`'s\n * `sanitizeRichTextHtml`, a regex sanitiser for Swarm/Wrap editor HTML whose\n * `src` rule admits only the suite's own file route — Zap rich text points at\n * R2 media URLs, so reusing it verbatim would blank every image.\n *\n * So the allowlist lives HERE, as the one source of truth for HTML that Zap\n * puts into a customer page: the tags and attributes Zap's rich-text editor\n * emits (Tiptap StarterKit + link, underline, text-align, the media image\n * with `data-media-id`), nothing else. Constants are exported so a server-side\n * twin, if one is ever needed, imports them rather than copying.\n *\n * ## DOM-based, not regex\n *\n * The page is a real browser, so the HTML is parsed by the browser's own\n * parser into an INERT `<template>` (no script runs, no image loads, no\n * handler fires while parsing) and the resulting tree is walked: disallowed\n * elements are dropped with their content (script-like) or unwrapped\n * (formatting-like), every attribute not on the list is removed, and URL\n * attributes are re-parsed with `URL` so `java\\tscript:` and friends are\n * judged by the scheme the browser would actually use.\n */\n\n/** Elements kept as they are. Everything else is unwrapped or dropped. */\nexport const RICH_TEXT_ALLOWED_TAGS: ReadonlySet<string> = new Set([\n 'p',\n 'br',\n 'strong',\n 'b',\n 'em',\n 'i',\n 'u',\n 's',\n 'code',\n 'pre',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'ul',\n 'ol',\n 'li',\n 'blockquote',\n 'hr',\n 'a',\n 'span',\n 'img',\n])\n\n/** Elements removed WITH their content: executable, embedding or form markup. */\nexport const RICH_TEXT_DROPPED_TAGS: ReadonlySet<string> = new Set([\n 'script',\n 'style',\n 'template',\n 'iframe',\n 'frame',\n 'frameset',\n 'object',\n 'embed',\n 'applet',\n 'noscript',\n 'svg',\n 'math',\n 'form',\n 'input',\n 'button',\n 'select',\n 'textarea',\n 'link',\n 'meta',\n 'base',\n 'title',\n 'head',\n])\n\n/** Attributes kept, per element (`*` applies to every allowed element). */\nexport const RICH_TEXT_ALLOWED_ATTRS: Readonly<Record<string, ReadonlySet<string>>> = {\n '*': new Set(['title', 'style']),\n a: new Set(['href', 'target', 'rel']),\n img: new Set(['src', 'alt', 'width', 'height', 'data-media-id']),\n}\n\n/** `style` survives only as text alignment (the TextAlign extension). */\nconst ALLOWED_STYLE = /^\\s*text-align\\s*:\\s*(left|right|center|justify)\\s*;?\\s*$/i\nconst LINK_SCHEMES = new Set(['http:', 'https:', 'mailto:', 'tel:'])\nconst IMAGE_SCHEMES = new Set(['http:', 'https:'])\nconst DIMENSION = /^\\d{1,5}$/\nconst MEDIA_ID = /^[A-Za-z0-9-]{1,64}$/\n\n/**\n * Is `value` a URL on one of `schemes` once the browser parses it? Relative\n * URLs resolve against the document's base (the customer page), which is\n * always http(s).\n */\nexport function isSafeUrl(value: string, schemes: ReadonlySet<string>, base?: string): boolean {\n try {\n const url = new URL(value, base ?? 'https://base.invalid/')\n return schemes.has(url.protocol)\n } catch {\n return false\n }\n}\n\n/** A link target the page may carry: http(s), mailto or tel. */\nexport function isSafeLinkUrl(value: string, base?: string): boolean {\n return isSafeUrl(value, LINK_SCHEMES, base)\n}\n\nexport function isSafeImageUrl(value: string, base?: string): boolean {\n return isSafeUrl(value, IMAGE_SCHEMES, base)\n}\n\n/** Every candidate URL in a `srcset` must be a safe image URL. */\nexport function isSafeSrcset(value: string, base?: string): boolean {\n const candidates = value.split(',').map((part) => part.trim().split(/\\s+/)[0] ?? '')\n return candidates.length > 0 && candidates.every((url) => url !== '' && isSafeImageUrl(url, base))\n}\n\nfunction cleanAttributes(element: Element, tag: string, base: string | undefined): void {\n const allowed = RICH_TEXT_ALLOWED_ATTRS[tag]\n const global = RICH_TEXT_ALLOWED_ATTRS['*']!\n for (const attribute of Array.from(element.attributes)) {\n const name = attribute.name.toLowerCase()\n const value = attribute.value\n let keep = global.has(name) || (allowed?.has(name) ?? false)\n if (keep && name === 'style') keep = ALLOWED_STYLE.test(value)\n if (keep && name === 'href') keep = isSafeUrl(value, LINK_SCHEMES, base)\n if (keep && name === 'src') keep = isSafeImageUrl(value, base)\n if (keep && name === 'target') keep = value === '_blank'\n if (keep && (name === 'width' || name === 'height')) keep = DIMENSION.test(value)\n if (keep && name === 'data-media-id') keep = MEDIA_ID.test(value)\n if (!keep) element.removeAttribute(attribute.name)\n }\n if (tag === 'a' && element.getAttribute('target') === '_blank') {\n element.setAttribute('rel', 'noopener noreferrer')\n } else if (tag === 'a') {\n element.removeAttribute('rel')\n }\n}\n\nfunction walk(parent: ParentNode, base: string | undefined): void {\n for (const node of Array.from(parent.childNodes)) {\n if (node.nodeType === 3 /* TEXT */) continue\n if (node.nodeType !== 1 /* ELEMENT */) {\n // Comments, processing instructions, CDATA: never kept.\n node.parentNode?.removeChild(node)\n continue\n }\n const element = node as Element\n const tag = element.localName.toLowerCase()\n const foreign = element.namespaceURI !== 'http://www.w3.org/1999/xhtml'\n if (foreign || RICH_TEXT_DROPPED_TAGS.has(tag)) {\n element.remove()\n continue\n }\n walk(element, base)\n if (!RICH_TEXT_ALLOWED_TAGS.has(tag)) {\n // Unknown formatting (div, section, font, …): keep the text, drop the tag.\n element.replaceWith(...Array.from(element.childNodes))\n continue\n }\n cleanAttributes(element, tag, base)\n // An image whose src did not survive is dropped, not left broken.\n if (tag === 'img' && !element.hasAttribute('src')) element.remove()\n }\n}\n\n/**\n * Sanitise `html` into a fragment owned by `doc`, ready for\n * `element.replaceChildren(fragment)`. Never sets `innerHTML` on a live node.\n */\nexport function sanitizeRichText(\n html: string,\n doc: Document,\n base: string | undefined = doc.baseURI,\n): DocumentFragment {\n const template = doc.createElement('template')\n template.innerHTML = html\n const content = template.content\n walk(content, base)\n return content\n}\n\n/** The sanitised HTML as a string (for tests and for comparisons). */\nexport function sanitizeRichTextToString(html: string, doc: Document): string {\n const holder = doc.createElement('div')\n holder.appendChild(sanitizeRichText(html, doc))\n return holder.innerHTML\n}\n\n/** Plain text of a rich-text value, for an element that did not opt into HTML. */\nexport function richTextToText(html: string, doc: Document): string {\n // Through the sanitiser first, so the text of a dropped <script> or <style>\n // never shows up as page copy.\n return sanitizeRichText(html, doc).textContent ?? ''\n}\n","import { cleanStega, splitStega } from '../stega'\nimport type { PreviewValue, ValuesPayload } from './protocol'\nimport {\n isSafeImageUrl,\n isSafeLinkUrl,\n isSafeSrcset,\n richTextToText,\n sanitizeRichText,\n} from './sanitize'\nimport type { TagIndex, TaggedElement } from './tags'\n\n/**\n * Draft value substitution (`zap:values`, §2.4 \"Draft values\").\n *\n * What is touched, and nothing else:\n *\n * - text (`string`, `number`, `boolean`, `{ text }`) → `textContent`; on an\n * `<img>` it becomes `alt` (an image tagged with an alt-text field).\n * - `{ image }` → `src`, `srcset` and `alt` of the element when it is an\n * `<img>`, else of its first descendant `<img>`; `<source>` siblings in a\n * `<picture>` lose `srcset` so the browser actually shows the new `src`.\n * URLs must be http(s) (relative resolves against the page).\n * - `{ html }` → sanitised children (never `innerHTML` on the live node), ONLY\n * when the element carries `data-zap-html`; otherwise its plain text.\n * - `{ url }` (a URL field) and `{ email }` (an EMAIL field, as `mailto:`) →\n * the `href` of the element when it is an `<a>`, else of its first\n * descendant `<a>`; the link's label stays the site's. With no link, the\n * address is the text. Only http(s), mailto and tel links. (An image field\n * is `{ image }`, above: its `src`, never its text.)\n * - `null` → empty text; an image keeps what it had.\n *\n * Never an attribute other than `href`, `src`, `srcset` and `alt`, never a\n * style, never an event handler.\n *\n * An element found by a stega marker (`source: 'stega'`) gets text only, and\n * only in the text node that carries the marker, which keeps it: the rest of\n * the element (other text, other fields, markup) is the site's. Rich text and\n * images found that way are left to the site's `onValues` handler.\n *\n * An element tagged `data-zap-site-renders` is never written, whatever the\n * value: the site derives what it shows there (an italic-split title, text\n * split into paragraphs, an uppercased label) and re-renders it from\n * `onValues` / `useZapLiveUpdates`. Writing the whole value over it would\n * undo the site's own markup and fight its next render.\n *\n * The element opts in; a subscription does not. Skipping every text write\n * for a record the page subscribed to (card #1110) was rejected: the React\n * hook lives in another bundle and only listens to a window event, so the\n * overlay cannot see who subscribed; and subscribing says nothing about\n * which fields the site re-renders (a client island reading one field would\n * freeze every server-rendered field of the same record).\n *\n * ## Formatting is the editor's job\n *\n * Numbers, dates and enum labels arrive already formatted (`PreviewValue`\n * says why): the browser core has no locale data, no field types and a size\n * budget, and the editor already has the suite's formatters.\n *\n * ## Re-application\n *\n * The last value per field is remembered, so when the site re-renders a tagged\n * node (hydration, client routing) `reapply()` writes it again. An element is\n * rewritten only when it differs from what we would write, so our own writes\n * — which the mutation observer also sees — settle after one pass.\n */\n\ninterface Desired {\n locale: string\n value: PreviewValue\n}\n\nexport class ValueApplier {\n private desired = new Map<string, Desired>()\n private appliedHtml = new WeakMap<Element, { source: string; output: string }>()\n\n constructor(\n private readonly index: TagIndex,\n private readonly doc: Document,\n ) {}\n\n /** Remember and apply a patch. Returns how many elements were written. */\n apply(payload: ValuesPayload): number {\n let written = 0\n for (const [fieldKey, raw] of Object.entries(payload.patch)) {\n // Never a marker from a value: a stega text node keeps ITS markers and\n // appends the value, so a value carrying markers (text copied from a\n // preview into the form) would add more on every pass, and each new\n // marker is one more tagged entry at the next scan — the index and the\n // node grow without bound until the tab runs out of memory.\n const value = cleanStega(raw)\n this.desired.set(`${payload.recordRef}#${fieldKey}`, { locale: payload.locale, value })\n for (const tagged of this.index.byField(payload.recordRef, fieldKey)) {\n if (this.write(tagged, payload.locale, value)) written++\n }\n }\n return written\n }\n\n /** Write every remembered value again where the page drifted from it. */\n reapply(): number {\n let written = 0\n for (const tagged of this.index.elements) {\n const desired = this.desired.get(tagged.tag)\n if (desired && this.write(tagged, desired.locale, desired.value)) written++\n }\n return written\n }\n\n private write(tagged: TaggedElement, locale: string, value: PreviewValue): boolean {\n // An element pinned to another locale shows that locale's copy; leave it.\n if (tagged.locale && tagged.locale !== locale) return false\n // The site renders this one itself (`data-zap-site-renders`).\n if (tagged.siteRenders) return false\n const element = tagged.element\n\n if (tagged.textNode) {\n const text = plainText(value)\n if (text === null) return false\n const node = tagged.textNode\n // The node is the field's own text; keep the whitespace a template put before it.\n const split = splitStega(node.data)\n const next = /^\\s*/.exec(split.text)![0] + text + split.markers\n if (node.data === next) return false\n node.data = next\n return true\n }\n\n if (value !== null && typeof value === 'object' && 'image' in value) {\n return writeImage(element, value.image, this.doc.baseURI)\n }\n\n if (value !== null && typeof value === 'object' && 'html' in value) {\n if (tagged.html) return this.writeHtml(element, value.html)\n return writeText(element, richTextToText(value.html, this.doc))\n }\n\n if (value !== null && typeof value === 'object' && ('url' in value || 'email' in value)) {\n return writeLink(element, value, this.doc.baseURI)\n }\n\n const text = plainText(value) ?? ''\n if (isImg(element)) return setAttr(element, 'alt', text)\n return writeText(element, text)\n }\n\n private writeHtml(element: Element, html: string): boolean {\n // Unchanged when the source is the one we last wrote AND the node still\n // holds exactly our output (the site did not re-render it since).\n const last = this.appliedHtml.get(element)\n if (last && last.source === html && last.output === element.innerHTML) return false\n element.replaceChildren(sanitizeRichText(html, this.doc))\n this.appliedHtml.set(element, { source: html, output: element.innerHTML })\n return true\n }\n}\n\n/**\n * A value as plain text, or null for the shapes that are not text (html,\n * image). A structured CURRENCY shows its `text`, an ENUM its `label`.\n */\nfunction plainText(value: PreviewValue): string | null {\n if (value === null || typeof value !== 'object') return value === null ? '' : String(value)\n const v = value as { text?: string; url?: string; email?: string; label?: string }\n return v.text ?? v.label ?? v.url ?? v.email ?? null\n}\n\n/** A URL or EMAIL value: the `href` of the link, never its label. */\nfunction writeLink(element: Element, value: { url: string } | { email: string }, base: string) {\n const email = 'email' in value\n const address = email ? value.email : value.url\n const link = element.localName === 'a' ? element : element.querySelector('a')\n if (!link) return writeText(element, address)\n const href = email && address ? `mailto:${address}` : address\n return isSafeLinkUrl(href, base) && setAttr(link, 'href', href)\n}\n\nfunction isImg(element: Element): element is HTMLImageElement {\n return element.localName === 'img'\n}\n\nfunction writeText(element: Element, text: string): boolean {\n if (element.textContent === text) return false\n element.textContent = text\n return true\n}\n\nfunction setAttr(element: Element, name: string, value: string): boolean {\n if (element.getAttribute(name) === value) return false\n element.setAttribute(name, value)\n return true\n}\n\nfunction writeImage(\n element: Element,\n image: { src?: string | null; srcset?: string | null; alt?: string | null },\n base: string,\n): boolean {\n const img = isImg(element) ? element : element.querySelector('img')\n if (!img) return false\n let changed = false\n if (image.src && isSafeImageUrl(image.src, base)) {\n changed = setAttr(img, 'src', image.src) || changed\n const picture = img.parentElement?.localName === 'picture' ? img.parentElement : null\n if (picture) {\n for (const source of Array.from(picture.children)) {\n if (source.localName === 'source' && source.hasAttribute('srcset')) {\n source.removeAttribute('srcset')\n changed = true\n }\n }\n }\n if (image.srcset === undefined || image.srcset === null) {\n // A stale srcset would win over the new src.\n if (img.hasAttribute('srcset')) {\n img.removeAttribute('srcset')\n changed = true\n }\n }\n }\n if (image.srcset && isSafeSrcset(image.srcset, base)) {\n changed = setAttr(img, 'srcset', image.srcset) || changed\n }\n if (typeof image.alt === 'string') changed = setAttr(img, 'alt', image.alt) || changed\n return changed\n}\n","import { buildAnchor, findAnchorElement } from './anchor'\nimport { Overlay } from './overlay'\nimport { createPageBridge, type MessageTargetLike, type PageBridge } from './page-bridge'\nimport {\n CAPABILITIES,\n PAGE_UNLOAD_ERROR,\n type DiffMark,\n type EditorMessage,\n type PausedPayload,\n type TagSummary,\n} from './protocol'\nimport { createScrollSync } from './scroll'\nimport { observeTags, TagIndex, tagGuard, type TagProblem } from './tags'\nimport { ValueApplier } from './values'\n\n/**\n * The page runtime: index, overlay, values and bridge, wired together. The\n * Live client (`live.ts`) boots it inside the customer's own page, framed by\n * Zap's editor by its real URL (§2.7).\n */\n\nexport const CLIENT_VERSION = '0.11.1'\n\nexport interface RuntimeOptions {\n win?: Window\n doc?: Document\n parent?: MessageTargetLike\n /** Rescan throttle. Default 250 ms. */\n throttleMs?: number\n}\n\nexport interface PageRuntime {\n readonly bridge: PageBridge\n readonly index: TagIndex\n readonly overlay: Overlay\n /** Force a rescan + report now (specs, and after a navigation); nothing once paused. */\n refresh(): void\n destroy(): void\n}\n\nconst MAX_PROBLEM_REPORTS = 20\n\n/**\n * What one runtime needs beyond the DOM: who it talks to and how the page\n * behaves. The Live client builds it from the origins it trusts (`live.ts`).\n */\nexport interface RuntimeConfig {\n editorOrigins: readonly string[]\n /** Where `zap:ready` goes before the hello names the editor. */\n preHelloTargets?(): readonly string[]\n /** The site's draft-mode route path, announced in `zap:ready`. */\n draftRoute?: string | null\n /** The page's real URL (`location.href`; it moves on client-side routing). */\n pageUrl(): string\n /**\n * Nothing is added to the page (no overlay element, no listeners) until\n * a verified `zap:hello`, so a page framed by anyone else stays untouched (§7.3 \"Overlay as an attack surface on public pages\").\n */\n lazyOverlay?: boolean\n /**\n * A last say on a message the bridge accepted (the hello must name this\n * site). False ignores it, before the runtime touches the page.\n */\n accept?(message: EditorMessage): boolean\n /** Called after the runtime handled an editor message itself. */\n onMessage?(message: EditorMessage): void\n}\n\nexport function startRuntime(config: RuntimeConfig, options: RuntimeOptions = {}): PageRuntime {\n const win = options.win ?? window\n const doc = options.doc ?? win.document\n const parent = options.parent ?? (win.parent as MessageTargetLike)\n const index = new TagIndex(doc)\n const values = new ValueApplier(index, doc)\n let lastReport = ''\n const reportedProblems = new Set<string>()\n let stopObserving: () => void = () => {}\n // The runaway guard (`tagGuard`): once paused, nothing is scanned or written.\n const guard = tagGuard()\n let paused: PausedPayload | null = null\n /**\n * `localStorage['eelzap:debug']` set (any value) on the site's origin:\n * `console.debug('[zap:fields]', …)` for every tags report and its cause,\n * and every editor message dropped (a wrong session drops them all).\n */\n let debug = false\n try {\n debug = !!win.localStorage.getItem('eelzap:debug')\n } catch {\n // Blocked storage: no logs.\n }\n const log = (...args: unknown[]) => debug && console.debug('[zap:fields]', ...args)\n\n const overlay = new Overlay({\n doc,\n win,\n index,\n onInspect: (tagged) => {\n const { x, y, width: w, height: h } = tagged.element.getBoundingClientRect()\n bridge.post('zap:click', {\n recordRef: tagged.recordRef,\n fieldKey: tagged.fieldKey,\n rect: { x, y, w, h },\n })\n },\n onSelect: (elements) =>\n bridge.post('zap:select', {\n anchors: elements.map((element) => buildAnchor(element, index, config.pageUrl(), doc, win)),\n }),\n onSpot: (anchor) => bridge.post('zap:spot', { ...anchor, pageUrl: config.pageUrl() }),\n onPin: (pin) => bridge.post('zap:pin', pin),\n // Zap's one comment toggle: a click on the page or a wrapper is a spot.\n spotOnLarge: true,\n })\n\n const bridge = createPageBridge({\n win,\n parent,\n editorOrigins: config.editorOrigins,\n preHelloTargets: config.preHelloTargets,\n // Any window may post to the page: only drops from the editor are news.\n onDrop: (reason) => reason !== 'wrong-source' && log('drop', reason),\n onMessage: (message) => {\n if (config.accept && !config.accept(message)) return\n handle(message)\n config.onMessage?.(message)\n },\n })\n\n const scroll = createScrollSync(win, doc, index, (payload) => bridge.post('zap:scroll', payload))\n /** Comparar's marks, by field: re-resolved to elements after every rescan. */\n let marks: DiffMark[] = []\n const drawMarks = () =>\n overlay.setMarks(\n marks.flatMap(({ recordRef, fieldKey, tone, label, active }) =>\n index\n .byField(recordRef, fieldKey)\n .map(({ element }) => ({ element, tone, label, active: !!active })),\n ),\n )\n\n function handle(message: EditorMessage): void {\n switch (message.type) {\n case 'zap:hello': {\n const hello = message.payload\n greeted = true\n overlay.mount()\n overlay.setLabels(hello.labels, hello.foreign)\n overlay.setTheme(hello.theme)\n if (hello.zoom !== undefined) overlay.setZoom(hello.zoom)\n overlay.setMode(hello.mode)\n // The editor may have missed the load-time report (it was not\n // listening yet), so the hello is answered with the current tags.\n report('hello', true)\n // A pause before the hello never reached the editor.\n if (paused) bridge.post('zap:paused', paused)\n return\n }\n case 'zap:values':\n if (!paused) values.apply(message.payload)\n return\n case 'zap:focus-field':\n overlay.focusField(\n index.byField(message.payload.recordRef, message.payload.fieldKey).map((t) => t.element),\n )\n return\n case 'zap:mode':\n overlay.setMode(message.payload)\n return\n case 'zap:zoom':\n overlay.setZoom(message.payload.zoom)\n return\n case 'zap:refresh':\n log('refresh')\n win.location.reload()\n return\n case 'zap:highlight': {\n const found = message.payload.anchors\n .map((anchor) => findAnchorElement(anchor, index, doc, win))\n .filter((element): element is Element => element !== null)\n overlay.highlight(found)\n return\n }\n case 'zap:pins': {\n const pins = []\n for (const { id, n, spot, dom } of message.payload.pins) {\n // A dom pin whose element is gone is not drawn (like a highlight).\n const element = dom ? findAnchorElement(dom, index, doc, win) : null\n if (spot) pins.push({ id, n, spot })\n else if (element) pins.push({ id, n, element })\n }\n overlay.setPins(pins)\n return\n }\n case 'zap:scroll-sync':\n scroll.setEnabled(message.payload.enabled)\n return\n case 'zap:scroll-to':\n scroll.scrollTo(message.payload)\n return\n case 'zap:marks':\n marks = message.payload.marks\n drawMarks()\n return\n }\n }\n\n function report(why: string, force = false): void {\n const summary: TagSummary[] = index.summary()\n const key = JSON.stringify(summary)\n if (force || key !== lastReport) {\n lastReport = key\n bridge.post('zap:tags', summary)\n log('tags', why, summary.length, config.pageUrl(), bridge.session)\n }\n reportProblems(index.problems)\n }\n\n function reportProblems(problems: TagProblem[]): void {\n for (const problem of problems) {\n const key = `${problem.attribute}=${problem.value}`\n if (reportedProblems.has(key) || reportedProblems.size >= MAX_PROBLEM_REPORTS) continue\n reportedProblems.add(key)\n bridge.post('zap:error', { code: 'TAG_INVALID', detail: key.slice(0, 500) })\n }\n }\n\n // A client-side route change (pushState) moves the page without a load; it\n // shows up as DOM mutations, so the rescan notices the new URL and tells the\n // editor. A real navigation loads a new document, which says ready again.\n let lastUrl = config.pageUrl()\n /** Rescan; false once the guard paused the page (it stops observing, writes nothing more). */\n function scan(page: boolean): boolean {\n if (paused) return false\n index.scan()\n const count = index.elements.length\n const reason = guard(count, page)\n if (!reason) return true\n paused = { reason, count }\n stopObserving()\n console.warn('eelzap: preview paused', paused)\n bridge.post('zap:paused', paused)\n return false\n }\n\n function refresh(page = true): void {\n if (!scan(page)) return\n values.reapply()\n if (marks.length) drawMarks()\n report(page ? 'mutation' : 'reapply')\n const url = config.pageUrl()\n if (url !== lastUrl) {\n lastUrl = url\n bridge.post('zap:navigate', { url })\n }\n }\n\n // Listeners go in NOW, at script start (or, with `lazyOverlay`, the overlay's\n // at the verified hello). When the frame navigates, the editor hears of it\n // before the next document can receive anything (see PAGE_UNLOAD_ERROR).\n const onPageHide = () => {\n bridge.post('zap:error', { code: PAGE_UNLOAD_ERROR })\n }\n // A document restored from the back-forward cache never loads again: it\n // says ready again, and the editor answers with its hello.\n const onPageShow = (event: PageTransitionEvent) => {\n if (event.persisted) {\n log('pageshow')\n announce()\n }\n }\n /** Whether a hello arrived: until then, `zap:ready` is said again. */\n let greeted = false\n const retries: number[] = []\n function announce(): void {\n log('ready', config.pageUrl())\n bridge.post('zap:ready', {\n version: CLIENT_VERSION,\n capabilities: [...CAPABILITIES],\n pageUrl: config.pageUrl(),\n draftRoute: config.draftRoute ?? null,\n })\n }\n\n if (!config.lazyOverlay) overlay.install()\n win.addEventListener('pagehide', onPageHide)\n win.addEventListener('pageshow', onPageShow)\n\n function boot(): void {\n if (!config.lazyOverlay) overlay.mount()\n if (scan(true)) {\n stopObserving = observeTags(doc.documentElement, refresh, {\n throttleMs: options.throttleMs,\n ignore: (node) => {\n const host = overlay.hostElement\n return !!host && (node === host || host.contains(node))\n },\n })\n }\n announce()\n report('ready', true)\n // An editor whose listener mounted after this ready (a soft navigation in\n // Zap) never answers it: say it again until a hello comes.\n for (const ms of [500, 1500, 4000])\n retries.push(win.setTimeout(() => greeted || announce(), ms))\n }\n\n if (doc.readyState === 'loading') {\n doc.addEventListener('DOMContentLoaded', boot, { once: true })\n } else {\n boot()\n }\n\n return {\n bridge,\n index,\n overlay,\n refresh,\n destroy() {\n for (const id of retries) win.clearTimeout(id)\n stopObserving()\n scroll.destroy()\n win.removeEventListener('pagehide', onPageHide)\n win.removeEventListener('pageshow', onPageShow)\n overlay.destroy()\n bridge.destroy()\n },\n }\n}\n","import {\n isDraftRoutePath,\n isPreviewTokenShape,\n type EditorMessage,\n type PreviewValue,\n type RecordRef,\n type ValuesPayload,\n} from './protocol'\nimport { isLocalHost } from '../boot/local'\nimport { NEST_PRODUCTION_ORIGIN } from './site/config'\nimport { startSiteMode } from './site/boot'\nimport { startRuntime, type PageRuntime, type RuntimeOptions } from './runtime'\n\n/**\n * The Live client («En vivo», `.docs/proposals/zap-cms-v2.md` §2.5–2.7): the\n * page runtime (`runtime.ts`: tag index, overlay, value substitution, bridge),\n * booted by the customer's site itself, which Zap's editor frames by its real\n * URL.\n *\n * ```ts\n * import { initZap, onValues, zapAttrs } from '@8ux-co/eelzap/preview'\n *\n * initZap({ siteKey: 'verdeorigen' })\n * <h1 {...zapAttrs(post, 'title')}>{post.content.title}</h1>\n * onValues(post, (patch) => …) // optional: values the site computes from\n * ```\n *\n * ## Who the client listens to (§2.6 \"Origins\")\n *\n * A message from the editor is accepted only when ALL hold, and every other\n * message is dropped and counted (`page-bridge.ts`):\n *\n * 1. `event.source === window.parent`;\n * 2. `event.origin` is the production editor (`https://zap.eel.software`)\n * or the development origin opted into below. Zap's own preview\n * deployments are not editors: no deployment-specific origin is compiled\n * into a public bundle;\n * 3. it parses as a v1 envelope under 256 KB;\n * 4. after the first `zap:hello`, its `session` matches.\n *\n * And the hello itself must name THIS site (`siteKey`); a hello for another\n * site is ignored, and nothing is drawn or stored for it.\n *\n * ## Development: `zapOrigin`\n *\n * `zapOrigin` is the only way to trust another origin, and it is honoured only\n * for a local Zap (`localhost`, `127.0.0.1`, `[::1]` or a `*.localhost` name,\n * over http or https, any port) AND only while the page itself runs on one of\n * those hosts. The boot sets it to the Zap it loaded this file from when that\n * is a local Zap framing a local page, so the site need not pass it. A\n * production page never trusts a local editor, even when the option is left in\n * the site's code; any other value is ignored with a console warning. The\n * production editor needs no option.\n *\n * ## Not framed by Zap, nothing happens\n *\n * Outside a frame the client does nothing at all. Inside a frame it posts one\n * `zap:ready` to the candidate parent origin (and the browser drops it unless\n * the parent really is that Zap origin), and adds nothing to the page — no\n * element, no listener beyond `message` — until a verified hello arrives.\n */\n\n/** The production editor. Always trusted. */\nexport const ZAP_PRODUCTION_ORIGIN = 'https://zap.eel.software'\n\n/** The header the cookieless fallback sends the preview token in (§2.5). */\nexport const ZAP_PREVIEW_TOKEN_HEADER = 'x-zap-preview-token'\n/** Where the client keeps it, per tab and per frame. */\nexport const PREVIEW_TOKEN_STORAGE_KEY = 'eelzap:preview-token'\n\nfunction parseUrl(value: string): URL | null {\n try {\n return new URL(value)\n } catch {\n return null\n }\n}\n\n/**\n * The extra origin `zapOrigin` adds, or null. See \"Development\" above: a\n * local Zap, from a local page, nothing else.\n */\nexport function developmentZapOrigin(\n zapOrigin: string | undefined,\n pageHostname: string,\n): string | null {\n if (!zapOrigin) return null\n const url = parseUrl(zapOrigin)\n if (!url || (url.protocol !== 'http:' && url.protocol !== 'https:')) return null\n if (url.origin === ZAP_PRODUCTION_ORIGIN) return null\n if (!isLocalHost(url.hostname) || !isLocalHost(pageHostname)) return null\n return url.origin\n}\n\n// ── Record references ───────────────────────────────────────────────────────\n\nexport { recordRefsOf, zapAttrs, type EntryLike } from '../refs'\nimport { recordRefsOf, type EntryLike } from '../refs'\nexport { cleanStega } from '../stega'\n\n// ── Values ──────────────────────────────────────────────────────────────────\n\nexport type ValuesHandler = (patch: Record<string, PreviewValue>, payload: ValuesPayload) => void\n\ninterface Subscription {\n refs: RecordRef[]\n handler: ValuesHandler\n}\n\nconst subscriptions = new Set<Subscription>()\n\n/**\n * Call `handler` with each `zap:values` patch for `entry` (unsaved keystrokes\n * from the editor), for sites that compute things from values. The tagged\n * elements are updated anyway, except those marked `data-zap-site-renders`\n * (`fields(…).derived(key)`), which the site renders from this patch; this is\n * for everything else. Values arrive\n * formatted for display, exactly as the overlay writes them. Returns an\n * unsubscribe function. Works before or after `initZap`.\n */\nexport function onValues(entry: EntryLike, handler: ValuesHandler): () => void {\n return subscribeRefs(recordRefsOf(entry), handler)\n}\n\n/** `onValues` for references already resolved (the React hook). */\nexport function subscribeRefs(refs: readonly RecordRef[], handler: ValuesHandler): () => void {\n const subscription: Subscription = { refs: [...refs], handler }\n subscriptions.add(subscription)\n return () => {\n subscriptions.delete(subscription)\n }\n}\n\n/**\n * The window event every `zap:values` patch is also announced with, so code\n * bundled apart from this client (`useZapLiveUpdates` and `onValues` in\n * `@8ux-co/eelzap/react`, which never bundle the overlay) hears it.\n */\nexport const VALUES_EVENT = 'eelzap:values'\n\nfunction dispatchValues(payload: ValuesPayload, win?: Window): void {\n win?.dispatchEvent(new CustomEvent(VALUES_EVENT, { detail: payload }))\n for (const subscription of [...subscriptions]) {\n if (!subscription.refs.includes(payload.recordRef)) continue\n try {\n subscription.handler(payload.patch, payload)\n } catch (error) {\n // A site handler that throws must not take the bridge down with it.\n console.error('eelzap: onValues handler threw', error)\n }\n }\n}\n\n// ── Preview token (cookieless fallback) ─────────────────────────────────────\n\nfunction sessionStore(win: Window | undefined): Storage | null {\n try {\n return win?.sessionStorage ?? null\n } catch {\n return null\n }\n}\n\n/**\n * The preview token the editor handed this frame, or null. For the\n * cookieless fallback: send it to your own server with `previewHeaders()`,\n * where `getPreviewToken` from `@8ux-co/eelzap/next` reads it.\n */\nexport function getPreviewToken(win: Window | undefined = globalWindow()): string | null {\n try {\n const value = sessionStore(win)?.getItem(PREVIEW_TOKEN_STORAGE_KEY) ?? null\n return isPreviewTokenShape(value) ? value : null\n } catch {\n return null\n }\n}\n\n/** `{ 'x-zap-preview-token': token }` while this frame has one, else `{}`. */\nexport function previewHeaders(win?: Window): Record<string, string> {\n const token = getPreviewToken(win ?? globalWindow())\n return token ? { [ZAP_PREVIEW_TOKEN_HEADER]: token } : {}\n}\n\nfunction forgetPreviewToken(win: Window): void {\n try {\n sessionStore(win)?.removeItem(PREVIEW_TOKEN_STORAGE_KEY)\n } catch {\n // Nothing stored.\n }\n}\n\nfunction globalWindow(): Window | undefined {\n return typeof window === 'undefined' ? undefined : window\n}\n\n// ── initZap ─────────────────────────────────────────────────────────────────\n\nexport interface InitZapOptions {\n /** The Zap site's key (Configuración del sitio). A hello for another site is ignored. */\n siteKey: string\n /**\n * The Zap site's id. Turns on Live suggestion mode outside Zap's frame\n * (§3.4): it names the site's OAuth client. Without it the client only\n * talks to Zap's editor.\n */\n siteId?: string\n /** A local Zap, for development only (see \"Development\" above). */\n zapOrigin?: string\n /** A local Nest (the OAuth issuer), for development only, under the same rule as `zapOrigin`. */\n authOrigin?: string\n /** The letter pressed with Shift to open «Editar» and «Comentar». Default `Z`; false turns it off. */\n shortcut?: string | false\n /** The language of the suggestion UI; default from `<html lang>`, Spanish otherwise. */\n locale?: 'es' | 'en'\n /** Script-tag build only: the core's own URL, so its chunks load from beside it. */\n chunkBase?: string | null\n /**\n * The site's draft-mode route, a path such as `/api/zap-preview`, announced\n * to the editor in `zap:ready` (§2.5). Anything that is not a plain path is\n * dropped (`isDraftRoutePath`).\n */\n draftRoute?: string | null\n /** The boot saw the shortcut: open the site tools at once. */\n open?: boolean\n}\n\nexport interface ZapLive {\n /** Framed and listening; false outside a frame or after `destroy`. */\n readonly active: boolean\n /** The underlying runtime, null when inactive (tests and debugging). */\n readonly runtime: PageRuntime | null\n destroy(): void\n}\n\nlet current: ZapLive | null = null\n\nconst INERT: ZapLive = {\n active: false,\n runtime: null,\n destroy() {},\n}\n\n/**\n * Start the Live client. Call once per page, as early as convenient; a second\n * call returns the first one's handle (a script tag and a bundled import on\n * the same page do not start two clients).\n */\nexport function initZap(options: InitZapOptions, runtimeOptions: RuntimeOptions = {}): ZapLive {\n if (current?.active) return current\n const win = runtimeOptions.win ?? globalWindow()\n if (!win) return INERT\n const siteKey = typeof options?.siteKey === 'string' ? options.siteKey.trim() : ''\n if (!siteKey || siteKey.length > 128) {\n console.warn('eelzap: no site key')\n return INERT\n }\n const parent = runtimeOptions.parent ?? (win.parent as Window | null)\n const dev = developmentZapOrigin(options.zapOrigin, win.location.hostname)\n if (options.zapOrigin && !dev && safeOrigin(options.zapOrigin) !== ZAP_PRODUCTION_ORIGIN) {\n console.warn('eelzap: zapOrigin is for a local Zap on a local page')\n }\n if (!parent || (parent as unknown) === win) return startSuggestions(options, win, dev)\n\n const exact = dev ? [ZAP_PRODUCTION_ORIGIN, dev] : [ZAP_PRODUCTION_ORIGIN]\n\n let helloOk = false\n const draftRoute = isDraftRoutePath(options.draftRoute) ? options.draftRoute : null\n const runtime = startRuntime(\n {\n editorOrigins: exact,\n preHelloTargets: () => {\n const candidate = parentOriginHint(win)\n return candidate && exact.includes(candidate) ? [candidate] : exact\n },\n draftRoute,\n pageUrl: () => win.location.href,\n lazyOverlay: true,\n accept: (message) => {\n if (message.type === 'zap:hello') {\n helloOk = message.payload.siteKey === siteKey\n return helloOk\n }\n return helloOk\n },\n onMessage: (message: EditorMessage) => {\n if (message.type === 'zap:hello') {\n // A hello without a token (drafts off, or a new session) forgets the\n // last one: a stale token must not keep serving drafts.\n try {\n const token = message.payload.previewToken\n const store = sessionStore(win)\n const last = store?.getItem(PREVIEW_TOKEN_STORAGE_KEY)\n if (token) store?.setItem(PREVIEW_TOKEN_STORAGE_KEY, token)\n else forgetPreviewToken(win)\n // The editor renewed the draft session ahead of its end: the new\n // token goes through the site's draft route in the background,\n // which sets the draft cookies again without navigating, so the\n // next reload still reads drafts. The redirect is not followed.\n if (token && last && last !== token && draftRoute) {\n const body = new URLSearchParams({ token, path: win.location.pathname })\n void win\n .fetch(draftRoute, {\n method: 'POST',\n body,\n credentials: 'same-origin',\n redirect: 'manual',\n })\n .catch(() => {})\n }\n } catch {\n // Storage blocked: the draft-mode cookie is the only path left.\n }\n } else if (message.type === 'zap:values') {\n dispatchValues(message.payload, win)\n }\n },\n },\n { ...runtimeOptions, win, parent: parent as unknown as RuntimeOptions['parent'] },\n )\n\n const handle: ZapLive = {\n get active() {\n return current === handle\n },\n runtime,\n destroy() {\n runtime.destroy()\n forgetPreviewToken(win)\n if (current === handle) current = null\n },\n }\n current = handle\n return handle\n}\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\nlet stopSuggestions: (() => void) | null = null\n\n/**\n * Not framed: Live suggestion mode (§3.4, `site/boot.ts`), when the site\n * passed its id. Zap is production or the local dev opt-in, never a preview\n * deployment; Nest likewise (`authOrigin` follows the `zapOrigin` rule).\n */\nfunction startSuggestions(options: InitZapOptions, win: Window, devZap: string | null): ZapLive {\n const siteId = typeof options.siteId === 'string' ? options.siteId.trim() : ''\n if (stopSuggestions || !UUID_RE.test(siteId)) return INERT\n const local = isLocalHost(win.location.hostname)\n const nest = parseUrl(options.authOrigin ?? '')\n const devNest = local && nest && isLocalHost(nest.hostname) ? nest.origin : null\n const stop = startSiteMode({\n win,\n siteId,\n zapOrigin: devZap ?? ZAP_PRODUCTION_ORIGIN,\n authOrigin: devNest ?? NEST_PRODUCTION_ORIGIN,\n shortcut: options.shortcut,\n locale: options.locale,\n chunkBase: options.chunkBase,\n open: options.open,\n draftRoute: isDraftRoutePath(options.draftRoute) ? options.draftRoute : null,\n })\n stopSuggestions = stop\n return {\n active: false,\n runtime: null,\n destroy() {\n stop()\n if (stopSuggestions === stop) stopSuggestions = null\n },\n }\n}\n\nfunction safeOrigin(value: string): string {\n return parseUrl(value)?.origin ?? ''\n}\n\n/**\n * Who framed us, as far as the page can tell before the hello: Chrome and\n * Safari expose `location.ancestorOrigins`; otherwise the referrer, which the\n * editor sends as its origin (`referrerpolicy=\"origin\"`). Only a hint: the\n * browser delivers `zap:ready` only if the parent really is that origin.\n */\nfunction parentOriginHint(win: Window): string | null {\n const ancestors = (win.location as Location & { ancestorOrigins?: DOMStringList }).ancestorOrigins\n if (ancestors && ancestors.length > 0) return ancestors[0] ?? null\n const referrer = win.document.referrer\n return referrer ? safeOrigin(referrer) || null : null\n}\n\n/** Specs only: forget the page's client and every `onValues` subscription. */\nexport function __resetLiveForTests(): void {\n current?.destroy()\n current = null\n stopSuggestions?.()\n stopSuggestions = null\n subscriptions.clear()\n}\n"],"mappings":"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","names":["current","text"]}
package/dist/react.d.ts CHANGED
@@ -44,6 +44,12 @@ declare function zapAttrs(entry: EntryLike, fieldKey: string): {
44
44
  * should show. A bare string, number or boolean is shorthand for `{ text }`.
45
45
  * `{ url }` and `{ email }` (a URL and an EMAIL field, `links` capability) go
46
46
  * to a link's `href` rather than its text.
47
+ *
48
+ * With the `structured-values` capability, a CURRENCY field travels as
49
+ * {@link PreviewCurrencyValue} and an ENUM as {@link PreviewEnumValue}: the
50
+ * delivery API's own shapes, so a site's `price.amountMinor` and
51
+ * `category.value` survive a live merge (`useZapLiveUpdates`). The overlay
52
+ * writes their `text` / `label`.
47
53
  */
48
54
  type PreviewValue = string | number | boolean | null | {
49
55
  text: string;
@@ -59,7 +65,18 @@ type PreviewValue = string | number | boolean | null | {
59
65
  srcset?: string | null;
60
66
  alt?: string | null;
61
67
  };
62
- };
68
+ } | PreviewCurrencyValue | PreviewEnumValue;
69
+ /** A CURRENCY field: the stored minor units, the ISO 4217 code, and the formatted text to show. */
70
+ interface PreviewCurrencyValue {
71
+ amountMinor: number;
72
+ currency: string;
73
+ text: string;
74
+ }
75
+ /** An ENUM field: the option's value and its label (what the page shows). */
76
+ interface PreviewEnumValue {
77
+ value: string;
78
+ label: string;
79
+ }
63
80
  interface ValuesPayload {
64
81
  recordRef: RecordRef;
65
82
  locale: string;
@@ -128,14 +145,22 @@ declare function bootZapPreview(options: ZapBootOptions, win?: Window): () => vo
128
145
  type ZapPreviewProps = ZapBootOptions;
129
146
  /**
130
147
  * The preview boot as a React component (`@8ux-co/eelzap/react` and, with a
131
- * default draft route, `@8ux-co/eelzap/next`). Renders nothing; after mount it
132
- * runs `bootZapPreview`, which loads the overlay only in a preview session.
148
+ * default draft route, `@8ux-co/eelzap/next`). After mount it runs
149
+ * `bootZapPreview`, which loads the overlay only in a preview session.
150
+ *
151
+ * Its only markup is the install marker: one `<meta>`, rendered on the server
152
+ * too, which React 19 hoists into `<head>`. It carries no record tag and no
153
+ * stega, so a published page stays untagged (ADR 041 amendment 2026-10-06).
133
154
  *
134
155
  * ```tsx
135
156
  * <ZapPreview siteKey="verdeorigen" siteId="…uuid…" />
136
157
  * ```
137
158
  */
138
- declare function ZapPreview(props: ZapPreviewProps): react.FunctionComponentElement<react.FragmentProps>;
159
+ declare function ZapPreview(props: ZapPreviewProps): react.DetailedReactHTMLElement<{
160
+ name: string;
161
+ content: string;
162
+ 'data-site': string;
163
+ }, HTMLElement>;
139
164
 
140
165
  /**
141
166
  * `@8ux-co/eelzap/react` (zap-cms-v2 §2.5, §2.7): the preview boot for React
@@ -157,11 +182,22 @@ declare function ZapPreview(props: ZapPreviewProps): react.FunctionComponentElem
157
182
  * values) or any object keyed by field (the object itself gets them). Values
158
183
  * arrive formatted for display, as the overlay writes them: text, a URL and an
159
184
  * email as a string, rich text as its sanitised HTML, an image as
160
- * `{ url, alt }` merged into what the field held. The boot must run somewhere on the page (`ZapPreview`);
161
- * without it, or outside Zap, the hook never changes anything.
185
+ * `{ url, alt }` merged into what the field held, a CURRENCY as
186
+ * `{ amountMinor, currency, amount, text }` and an ENUM as `{ value, label }`.
187
+ * The boot must run somewhere on the page (`ZapPreview`); without it, or
188
+ * outside Zap, the hook never changes anything.
162
189
  */
163
190
  type Patch = Record<string, PreviewValue>;
164
- /** One display value as a plain field value, merged into what the field held. */
191
+ /**
192
+ * One display value as a field value, merged into what the field held.
193
+ *
194
+ * A structured CURRENCY (`{ amountMinor, currency, text }`) and ENUM
195
+ * (`{ value, label }`) come through in the delivery API's shapes, so code
196
+ * reading `price.amountMinor`, `price.amount` or `category.value` keeps
197
+ * working while the editor types. CURRENCY also keeps `text`, the figure
198
+ * formatted the way Zap formats it, and recomputes `amount` (major units)
199
+ * from the ISO 4217 exponent.
200
+ */
165
201
  declare function toFieldValue(value: PreviewValue, previous: unknown): unknown;
166
202
  /** `entry` with `patch` merged into its `content` (or into itself). Pure. */
167
203
  declare function mergeLiveValues<T>(entry: T, patch: Patch): T;
package/dist/react.js CHANGED
@@ -3,6 +3,16 @@
3
3
  // src/react.ts
4
4
  import * as React from "react";
5
5
 
6
+ // src/currency-digits.ts
7
+ var ZERO = " BIF CLP DJF GNF ISK JPY KMF KRW PYG RWF UGX UYI VND VUV XAF XOF XPF ";
8
+ var THREE = " BHD IQD JOD KWD LYD OMR TND ";
9
+ var FOUR = " CLF UYW ";
10
+ function currencyDigits(code) {
11
+ const padded = ` ${(code ?? "").trim().toUpperCase()} `;
12
+ if (padded.length !== 5) return 2;
13
+ return ZERO.includes(padded) ? 0 : THREE.includes(padded) ? 3 : FOUR.includes(padded) ? 4 : 2;
14
+ }
15
+
6
16
  // src/refs.ts
7
17
  var FIELD_KEY_RE = /^[a-z][a-z0-9_]{0,127}$/;
8
18
  var ENTRY_REF_RE = /^([a-z0-9]+(?:-[a-z0-9]+)*)\/([a-z0-9]+(?:-[a-z0-9]+)*)$/;
@@ -59,8 +69,8 @@ function isShortcut(event, key) {
59
69
  var LOCAL_ORIGIN = /^https?:\/\/(?:(?:[a-z\d-]+\.)*localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/i;
60
70
 
61
71
  // src/boot/release.ts
62
- var PREVIEW_RELEASE_PATH = "/js/preview/preview.v1.702c7d7c0a8cbcdf.js";
63
- var PREVIEW_RELEASE_INTEGRITY = "sha384-MgY0PpmzRzbEMRNyUdmdJ7RkqIu/o9sSTAGuMxvh6Z7WkKGHVGVgchwOQ2HfwX90";
72
+ var PREVIEW_RELEASE_PATH = "/js/preview/preview.v1.ae804b72342eda4a.js";
73
+ var PREVIEW_RELEASE_INTEGRITY = "sha384-glp6U0Mz3+ALlGM3ow3OFm1neCdGalPxjzbsUqiBEcpchMOWuX0en/P0+nChQUBK";
64
74
 
65
75
  // src/boot/boot.ts
66
76
  var BOOT_ATTRIBUTES = {
@@ -132,19 +142,38 @@ function bootZapPreview(options, win = window) {
132
142
  }
133
143
 
134
144
  // src/boot/component.ts
135
- import { createElement, Fragment, useEffect } from "react";
145
+ import { createElement, useEffect } from "react";
146
+
147
+ // src/version.ts
148
+ var CLIENT_VERSION = "0.11.1";
149
+
150
+ // src/boot/component.ts
151
+ var CLIENT_MARKER_NAME = "zap-client";
136
152
  function ZapPreview(props) {
137
153
  const { siteKey, siteId, draftRoute, preview, shortcut, zapOrigin, authOrigin } = props;
138
154
  useEffect(
139
155
  () => bootZapPreview({ siteKey, siteId, draftRoute, preview, shortcut, zapOrigin, authOrigin }),
140
156
  [siteKey, siteId, draftRoute, preview, shortcut, zapOrigin, authOrigin]
141
157
  );
142
- return createElement(Fragment);
158
+ return createElement("meta", {
159
+ name: CLIENT_MARKER_NAME,
160
+ content: CLIENT_VERSION,
161
+ "data-site": siteKey
162
+ });
143
163
  }
144
164
 
145
165
  // src/react.ts
146
166
  function toFieldValue(value, previous) {
147
167
  if (value === null || typeof value !== "object") return value;
168
+ if ("amountMinor" in value) {
169
+ return {
170
+ amountMinor: value.amountMinor,
171
+ currency: value.currency,
172
+ amount: Number(`${value.amountMinor}e-${currencyDigits(value.currency)}`),
173
+ text: value.text
174
+ };
175
+ }
176
+ if ("label" in value) return { value: value.value, label: value.label };
148
177
  if ("text" in value) return value.text;
149
178
  if ("html" in value) return value.html;
150
179
  if ("url" in value) return value.url;