@8ux-co/eelzap 0.10.2 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/README.md +52 -9
- package/dist/{chunk-T6S3S5RH.js → chunk-G2JAAZKS.js} +108 -19
- package/dist/chunk-G2JAAZKS.js.map +1 -0
- package/dist/fields.cjs +3 -0
- package/dist/fields.cjs.map +1 -1
- package/dist/fields.d.cts +22 -1
- package/dist/fields.d.ts +22 -1
- package/dist/fields.js +3 -0
- package/dist/fields.js.map +1 -1
- package/dist/index.d.cts +42 -5
- package/dist/index.d.ts +42 -5
- package/dist/next.d.ts +21 -11
- package/dist/next.js +94 -26
- package/dist/next.js.map +1 -1
- package/dist/preview.d.ts +119 -9
- package/dist/preview.js +154 -6
- package/dist/preview.js.map +1 -1
- package/dist/react.d.ts +43 -7
- package/dist/react.js +33 -4
- package/dist/react.js.map +1 -1
- package/dist/{suggest-KLS6B7AM.js → suggest-FDD4MBRP.js} +109 -43
- package/dist/suggest-FDD4MBRP.js.map +1 -0
- package/package.json +70 -33
- package/dist/chunk-T6S3S5RH.js.map +0 -1
- package/dist/suggest-KLS6B7AM.js.map +0 -1
package/dist/fields.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/refs.ts","../src/stega.ts","../src/fields.ts"],"sourcesContent":["/**\n * The record and field grammar of tags (`.docs/proposals/zap-cms-v2.md` §2.4),\n * in a module of its own so the light entries (`./fields`, `./react`, the\n * boot) can name records without pulling in the preview protocol.\n * No imports, no side effects.\n */\n\n/**\n * A record reference in its canonical string form: `collectionKey/slug` for an\n * entry, `doc:documentKey` for a document (the tag grammar, §2.4).\n */\nexport type RecordRef = string\n\n/** Zap's field key pattern. */\nexport const FIELD_KEY_RE = /^[a-z][a-z0-9_]{0,127}$/\n/** Entry and document references; keys and slugs are `[a-z0-9]+(?:-[a-z0-9]+)*`. */\nconst ENTRY_REF_RE = /^([a-z0-9]+(?:-[a-z0-9]+)*)\\/([a-z0-9]+(?:-[a-z0-9]+)*)$/\nconst DOC_REF_RE = /^doc:([a-z0-9]+(?:-[a-z0-9]+)*)$/\nconst MAX_REF_LENGTH = 300\n\nexport type ParsedRecordRef =\n | { kind: 'entry'; collection: string; slug: string }\n | { kind: 'document'; key: string }\n\nexport function parseRecordRef(value: unknown): ParsedRecordRef | null {\n if (typeof value !== 'string' || value.length > MAX_REF_LENGTH) return null\n const doc = DOC_REF_RE.exec(value)\n if (doc) return { kind: 'document', key: doc[1]! }\n const entry = ENTRY_REF_RE.exec(value)\n if (entry) return { kind: 'entry', collection: entry[1]!, slug: entry[2]! }\n return null\n}\n\nexport function isRecordRef(value: unknown): value is RecordRef {\n return parseRecordRef(value) !== null\n}\n\n/** `zpt_` + 32 bytes in base64url, as Zap mints it (zap-cms-v2 §3.5). */\nexport const PREVIEW_TOKEN_RE = /^zpt_[A-Za-z0-9_-]{43}$/\n\n/** A preview token in the shape Zap mints; says nothing about whether it is live. */\nexport function isPreviewTokenShape(value: unknown): value is string {\n return typeof value === 'string' && PREVIEW_TOKEN_RE.test(value)\n}\n\nexport function isFieldKey(value: unknown): value is string {\n return typeof value === 'string' && FIELD_KEY_RE.test(value)\n}\n\n/**\n * What `zapAttrs` and `onValues` accept: a record reference string\n * (`blog/hola`, `doc:home`) or a delivery API entry (an item carries\n * `collection.key` and `slug`, a document `key`).\n */\nexport type EntryLike =\n | RecordRef\n | {\n slug?: string | null\n key?: string | null\n collection?: { key?: string | null } | null\n localizedSlugs?: Record<string, string | null | undefined> | null\n }\n\n/** Every reference an entry answers to (default slug first, then localized ones). */\nexport function recordRefsOf(entry: EntryLike | null | undefined): RecordRef[] {\n if (!entry) return []\n if (typeof entry === 'string') return isRecordRef(entry) ? [entry] : []\n const refs: RecordRef[] = []\n const add = (ref: string) => {\n if (isRecordRef(ref) && !refs.includes(ref)) refs.push(ref)\n }\n const collection = entry.collection?.key\n if (collection) {\n if (entry.slug) add(`${collection}/${entry.slug}`)\n for (const slug of Object.values(entry.localizedSlugs ?? {})) {\n if (slug) add(`${collection}/${slug}`)\n }\n } else if (entry.key) {\n add(`doc:${entry.key}`)\n }\n return refs\n}\n\n/**\n * The tag for one field, as attributes to spread on the element that renders\n * it: `{ 'data-zap': 'blog/hola#title' }`. Empty when the entry or the field\n * key is not one Zap can name, so a bad call never breaks the page.\n */\nexport function zapAttrs(entry: EntryLike, fieldKey: string): { 'data-zap'?: string } {\n const [ref] = recordRefsOf(entry)\n if (!ref || !isFieldKey(fieldKey)) return {}\n return { 'data-zap': `${ref}#${fieldKey}` }\n}\n","/**\n * Stega: invisible field markers on prose values read in preview\n * (`.docs/proposals/zap-cms-v2.md` §2.4, §7.5).\n *\n * Zap appends a marker to TEXT and LONG_TEXT values, and to the last text\n * node of each RICH_TEXT block, ONLY in `preview=1` delivery responses. The\n * preview overlay finds the marker in the page's text nodes and so knows\n * which element renders which field, with no manual tag. Published responses\n * never carry one.\n *\n * ## Format (eelzap stega v1)\n *\n * A marker is the tag the overlay would read from a `data-zap` attribute,\n * `recordRef#fieldKey`, plus `@locale` when the value belongs to a locale:\n *\n * blog-posts/mi-primer-post#title@es\n * doc:home#hero_title@es\n *\n * written as zero-width characters:\n *\n * - four digits, base 4: U+200B (0), U+200C (1), U+200D (2), U+2060 (3);\n * - a prefix of 8 digits, the bytes 0xE5 0xA7 (`3211 2213`);\n * - then every byte of the ASCII payload as 4 digits, most significant first.\n *\n * The payload is printable ASCII (the tag grammar allows nothing else), so\n * every payload byte starts with digit 0 or 1 and the prefix, which starts\n * with 3, can never be read inside a payload. A decoder reads groups of four\n * after the prefix until a group is not a payload byte, then validates the\n * result against the tag grammar; anything that does not parse is ignored. A\n * value never carries two markers (`stegaEncode` refuses to add a second).\n *\n * Why not `@vercel/stega`'s format: it encodes a JSON object (a URL and\n * origin in Sanity's and Vercel's use) and uses U+FEFF, which some tools strip\n * as a byte order mark. Here the decoded marker IS the tag grammar\n * (`data-zap`), so one parser serves both, and U+FEFF is avoided. No\n * dependency either way.\n *\n * Zero dependencies, no side effects: `.` re-exports `cleanStega` and\n * `hasStega`; the encoder is internal (Zap's delivery imports it); the\n * decoder serves `./preview`.\n */\n\nconst DIGITS = ['', '', '', ''] as const\nconst PREFIX = ''\n/** Longest payload a marker may carry, in bytes (ref 300 + field 128 + locale). */\nconst MAX_PAYLOAD = 512\n\n/** `recordRef#fieldKey` with an optional `@locale`: the tag grammar (`refs.ts`). */\nconst PAYLOAD_RE =\n /^(doc:[a-z0-9]+(?:-[a-z0-9]+)*|[a-z0-9]+(?:-[a-z0-9]+)*\\/[a-z0-9]+(?:-[a-z0-9]+)*)#([a-z][a-z0-9_]{0,127})(?:@([A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8}){0,3}))?$/\n/** The prefix, then whole payload bytes (first digit 0 or 1). */\nconst MARKER_RE = /(?:[][]{3})*/g\n\n/** What one marker says: the record, the field, and the locale when known. */\nexport interface StegaInfo {\n /** `collectionKey/slug` or `doc:documentKey`. */\n recordRef: string\n fieldKey: string\n locale?: string | null\n}\n\nexport interface DecodedStega {\n recordRef: string\n fieldKey: string\n locale: string | null\n /** `recordRef#fieldKey`, the `data-zap` tag. */\n tag: string\n}\n\n/** The marker for one field, or `''` when the info does not fit the tag grammar. */\nexport function stegaMarker(info: StegaInfo): string {\n const payload = `${info.recordRef}#${info.fieldKey}${info.locale ? `@${info.locale}` : ''}`\n if (payload.length > MAX_PAYLOAD || !PAYLOAD_RE.test(payload)) return ''\n let out = PREFIX\n for (let i = 0; i < payload.length; i++) {\n const byte = payload.charCodeAt(i)\n out +=\n DIGITS[(byte >> 6) & 3]! +\n DIGITS[(byte >> 4) & 3]! +\n DIGITS[(byte >> 2) & 3]! +\n DIGITS[byte & 3]!\n }\n return out\n}\n\n/**\n * `text` with the field's marker appended. A string that already carries a\n * marker, or info outside the grammar, comes back unchanged.\n */\nexport function stegaEncode(text: string, info: StegaInfo): string {\n if (hasStega(text)) return text\n return text + stegaMarker(info)\n}\n\n/** True when `value` is a string carrying at least one marker. */\nexport function hasStega(value: unknown): boolean {\n return typeof value === 'string' && value.includes(PREFIX)\n}\n\n/** Every valid marker in `text`, in order. Invalid ones are skipped. */\nexport function stegaDecode(text: string): DecodedStega[] {\n if (!text.includes(PREFIX)) return []\n const found: DecodedStega[] = []\n for (const match of text.match(MARKER_RE) ?? []) {\n let payload = ''\n for (let i = PREFIX.length; i + 4 <= match.length; i += 4) {\n let byte = 0\n for (let j = 0; j < 4; j++) byte = byte * 4 + DIGITS.indexOf(match[i + j] as never)\n payload += String.fromCharCode(byte)\n }\n const parsed = PAYLOAD_RE.exec(payload)\n if (!parsed) continue\n found.push({\n recordRef: parsed[1]!,\n fieldKey: parsed[2]!,\n locale: parsed[3] ?? null,\n tag: `${parsed[1]}#${parsed[2]}`,\n })\n }\n return found\n}\n\n/** `text` split into its visible part and the markers it carries, in order. */\nexport function splitStega(text: string): { text: string; markers: string } {\n if (!text.includes(PREFIX)) return { text, markers: '' }\n return { text: text.replace(MARKER_RE, ''), markers: (text.match(MARKER_RE) ?? []).join('') }\n}\n\n/** `text` without any marker. */\nfunction cleanString(text: string): string {\n return text.includes(PREFIX) ? text.replace(MARKER_RE, '') : text\n}\n\n/**\n * `value` with every stega marker removed: a string is cleaned, an array or a\n * plain object is cleaned deeply (into new objects), anything else is\n * returned as is. Use it where a preview value is compared, parsed or put in\n * an attribute, a URL or `<head>`; outside preview it changes nothing.\n */\nexport function cleanStega<T>(value: T): T {\n if (typeof value === 'string') return cleanString(value) as T\n if (Array.isArray(value)) return value.map((item) => cleanStega(item)) as T\n if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {\n const out: Record<string, unknown> = {}\n for (const [key, item] of Object.entries(value)) out[key] = cleanStega(item)\n return out as T\n }\n return value\n}\n","import { recordRefsOf } from './refs'\nimport { cleanStega, hasStega } from './stega'\n\n/**\n * `@8ux-co/eelzap/fields` (`.docs/proposals/zap-cms-v2.md` §2.4, §2.7): typed\n * pick helpers over an item or a document that tag themselves in preview.\n *\n * ```tsx\n * import { fields } from '@8ux-co/eelzap/fields'\n *\n * const f = fields(home) // a HomeDocument from codegen\n * <h1>{f.text('hero_title')}</h1> // carries its stega marker in preview\n * <a href={f.value('cta_url')} {...f.attrs('cta_url')}>…</a>\n * <img {...f.image('hero_image')} /> // src, alt, width, height (+ data-zap in preview)\n *\n * // Numbered slots: `stat_1` … `stat_4`\n * {f.list('stat', 4).map((s) => <p key={s.key}>{s.text()}</p>)}\n * // Slots of several fields: `nav_1_texto` + `nav_1_url` … `nav_5_…`\n * {f.list('nav', 5).filter((nav) => !nav.empty).map((nav) => (\n * <a key={nav.key} href={nav.value('url') ?? '/'} {...nav.attrs('url')}>{nav.text('texto')}</a>\n * ))}\n * ```\n *\n * Keys are typed from the record's `content`, so with codegen types a typo in\n * a field key fails to compile.\n *\n * Outside preview every helper returns plain values and EMPTY attribute\n * objects, so production HTML is byte-identical to reading values directly.\n * In preview (the record's text carries stega, or `{ preview: true }`), text\n * keeps its marker (the overlay finds it with no tag), and the non-text\n * helpers add `data-zap` (a manual tag). Markers never reach an attribute:\n * `value`, `image` and `attrs` clean them.\n */\n\n/** What `fields` reads: an item (`collection.key` + `slug`) or a document (`key`). */\nexport interface FieldsRecord {\n content: object\n slug?: string | null\n key?: string | null\n collection?: { key?: string | null } | null\n}\n\ntype Content<R extends FieldsRecord> = R['content']\ntype Key<R extends FieldsRecord> = keyof Content<R> & string\n/** Field keys whose value is text (string, possibly null). */\ntype TextKey<R extends FieldsRecord> = {\n [K in Key<R>]: NonNullable<Content<R>[K]> extends string ? K : never\n}[Key<R>]\n/**\n * Every numbered prefix in `K`: `stat` for `stat_1`, `nav` for `nav_1_texto`,\n * `hero_stat` for `hero_stat_1`. Walks the key one `_` segment at a time, so a\n * prefix may itself contain `_`.\n */\ntype Prefixes<K extends string, Head extends string = ''> = K extends `${infer P}_${infer Rest}`\n ?\n | (Rest extends `${number}` | `${number}_${string}` ? `${Head}${P}` : never)\n | Prefixes<Rest, `${Head}${P}_`>\n : never\n/** Numbered prefixes of the record (any string for an untyped record). */\ntype ListPrefix<R extends FieldsRecord> = string extends Key<R> ? string : Prefixes<Key<R>>\n/** The suffixes of `prefix_{i}_{suffix}` slots: `texto` and `url` for `nav`. */\ntype SlotSuffix<R extends FieldsRecord, P extends string> =\n string extends Key<R>\n ? string\n : Key<R> extends infer K\n ? K extends `${P}_${number}_${infer S}`\n ? S\n : never\n : never\n/** A slot field's value type; `unknown` for an untyped record. */\ntype SlotValue<R extends FieldsRecord, K> =\n string extends Key<R> ? unknown : Content<R>[Extract<Key<R>, K>]\n\n/** `{ 'data-zap': 'blog/hola#cover' }` in preview, `{}` otherwise. */\nexport type ZapAttrs = { 'data-zap'?: string }\n\nexport interface FieldImage extends ZapAttrs {\n src: string | undefined\n alt: string | undefined\n width: number | undefined\n height: number | undefined\n}\n\n/**\n * One numbered slot of `f.list(prefix, n)`: the same helpers as `fields`,\n * scoped to the slot. With a suffix they read `prefix_{i}_{suffix}`\n * (`nav.text('texto')` is `nav_1_texto`); without one, `prefix_{i}` itself\n * (`stat.text()` is `stat_1`).\n */\nexport interface FieldSlot<R extends FieldsRecord, P extends string> {\n /** `prefix_{i}`, unique in the list: a React key. */\n readonly key: string\n /** 1 for the first slot. */\n readonly index: number\n /** True when every field of the slot is empty (or the slot has no field). */\n readonly empty: boolean\n /** A text field as delivered, with its stega marker in preview. */\n text(suffix?: SlotSuffix<R, P>): string\n /** The single field `prefix_{i}`, with any marker removed. */\n value(): SlotValue<R, `${P}_${number}`>\n /** The slot's field `prefix_{i}_{suffix}`, with any marker removed. */\n value<S extends SlotSuffix<R, P>>(suffix: S): SlotValue<R, `${P}_${number}_${S}`>\n /** `{ 'data-zap': ref#key }` in preview, `{}` otherwise. */\n attrs(suffix?: SlotSuffix<R, P>): ZapAttrs\n /** An image or media field as `<img>` attributes, tagged in preview. */\n image(suffix?: SlotSuffix<R, P>): FieldImage\n}\n\nexport interface Fields<R extends FieldsRecord> {\n /** True when the helpers tag (the record was read in preview). */\n readonly preview: boolean\n /** A text field as delivered: with its stega marker in preview, so the overlay finds it. */\n text<K extends TextKey<R>>(key: K): string\n /** A field's value with any marker removed: safe in attributes, URLs, comparisons. */\n value<K extends Key<R>>(key: K): Content<R>[K]\n /** `{ 'data-zap': ref#key }` in preview, `{}` otherwise: spread it on the element. */\n attrs(key: Key<R>): ZapAttrs\n /** An image or media field as `<img>` attributes, tagged in preview. */\n image(key: Key<R>): FieldImage\n /**\n * Slots `1` … `count` of a numbered prefix, each a `FieldSlot`: fields\n * `prefix_1` … `prefix_n`, or slots of several fields\n * `prefix_{i}_{suffix}` (`nav_1_texto`, `nav_1_url`), read by suffix.\n */\n list<P extends ListPrefix<R>>(prefix: P, count: number): FieldSlot<R, P>[]\n}\n\nexport interface FieldsOptions {\n /** Force preview on or off; default: on when any text value carries stega. */\n preview?: boolean\n}\n\nexport function fields<R extends FieldsRecord>(record: R, options: FieldsOptions = {}): Fields<R> {\n const content = (record?.content ?? {}) as Record<string, unknown>\n const preview = options.preview ?? Object.values(content).some((value) => hasStega(value))\n const [ref] = recordRefsOf(record)\n const attrs = (key: string): ZapAttrs => (preview && ref ? { 'data-zap': `${ref}#${key}` } : {})\n const value = (key: string) => cleanStega(content[key])\n const text = (key: string) => {\n const raw = content[key]\n return typeof raw === 'string' ? (preview ? raw : cleanStega(raw)) : ''\n }\n const image = (key: string): FieldImage => {\n const media = (value(key) ?? {}) as Record<string, unknown>\n const pick = <T>(name: string, type: string) =>\n typeof media[name] === type ? (media[name] as T) : undefined\n return {\n src: pick<string>('url', 'string'),\n alt: pick<string>('alt', 'string'),\n width: pick<number>('width', 'number'),\n height: pick<number>('height', 'number'),\n ...attrs(key),\n }\n }\n\n return {\n preview,\n text,\n value: value as Fields<R>['value'],\n attrs,\n image,\n list: (prefix, count) =>\n Array.from({ length: Math.max(0, Math.floor(count)) }, (_, i) => {\n const key = `${prefix}_${i + 1}`\n const at = (suffix?: string) => (suffix ? `${key}_${suffix}` : key)\n const own = Object.keys(content).filter((k) => k === key || k.startsWith(`${key}_`))\n return {\n key,\n index: i + 1,\n empty: own.every((k) => {\n const v = value(k)\n return v === null || v === undefined || (typeof v === 'string' && !v.trim())\n }),\n text: (suffix?: string) => text(at(suffix)),\n value: (suffix?: string) => value(at(suffix)),\n attrs: (suffix?: string) => attrs(at(suffix)),\n image: (suffix?: string) => image(at(suffix)),\n } as FieldSlot<R, typeof prefix>\n }),\n }\n}\n"],"mappings":"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","names":["value"]}
|
|
1
|
+
{"version":3,"sources":["../src/refs.ts","../src/stega.ts","../src/fields.ts"],"sourcesContent":["/**\n * The record and field grammar of tags (`.docs/proposals/zap-cms-v2.md` §2.4),\n * in a module of its own so the light entries (`./fields`, `./react`, the\n * boot) can name records without pulling in the preview protocol.\n * No imports, no side effects.\n */\n\n/**\n * A record reference in its canonical string form: `collectionKey/slug` for an\n * entry, `doc:documentKey` for a document (the tag grammar, §2.4).\n */\nexport type RecordRef = string\n\n/** Zap's field key pattern. */\nexport const FIELD_KEY_RE = /^[a-z][a-z0-9_]{0,127}$/\n/** Entry and document references; keys and slugs are `[a-z0-9]+(?:-[a-z0-9]+)*`. */\nconst ENTRY_REF_RE = /^([a-z0-9]+(?:-[a-z0-9]+)*)\\/([a-z0-9]+(?:-[a-z0-9]+)*)$/\nconst DOC_REF_RE = /^doc:([a-z0-9]+(?:-[a-z0-9]+)*)$/\nconst MAX_REF_LENGTH = 300\n\nexport type ParsedRecordRef =\n | { kind: 'entry'; collection: string; slug: string }\n | { kind: 'document'; key: string }\n\nexport function parseRecordRef(value: unknown): ParsedRecordRef | null {\n if (typeof value !== 'string' || value.length > MAX_REF_LENGTH) return null\n const doc = DOC_REF_RE.exec(value)\n if (doc) return { kind: 'document', key: doc[1]! }\n const entry = ENTRY_REF_RE.exec(value)\n if (entry) return { kind: 'entry', collection: entry[1]!, slug: entry[2]! }\n return null\n}\n\nexport function isRecordRef(value: unknown): value is RecordRef {\n return parseRecordRef(value) !== null\n}\n\n/** `zpt_` + 32 bytes in base64url, as Zap mints it (zap-cms-v2 §3.5). */\nexport const PREVIEW_TOKEN_RE = /^zpt_[A-Za-z0-9_-]{43}$/\n\n/** A preview token in the shape Zap mints; says nothing about whether it is live. */\nexport function isPreviewTokenShape(value: unknown): value is string {\n return typeof value === 'string' && PREVIEW_TOKEN_RE.test(value)\n}\n\nexport function isFieldKey(value: unknown): value is string {\n return typeof value === 'string' && FIELD_KEY_RE.test(value)\n}\n\n/**\n * What `zapAttrs` and `onValues` accept: a record reference string\n * (`blog/hola`, `doc:home`) or a delivery API entry (an item carries\n * `collection.key` and `slug`, a document `key`).\n */\nexport type EntryLike =\n | RecordRef\n | {\n slug?: string | null\n key?: string | null\n collection?: { key?: string | null } | null\n localizedSlugs?: Record<string, string | null | undefined> | null\n }\n\n/** Every reference an entry answers to (default slug first, then localized ones). */\nexport function recordRefsOf(entry: EntryLike | null | undefined): RecordRef[] {\n if (!entry) return []\n if (typeof entry === 'string') return isRecordRef(entry) ? [entry] : []\n const refs: RecordRef[] = []\n const add = (ref: string) => {\n if (isRecordRef(ref) && !refs.includes(ref)) refs.push(ref)\n }\n const collection = entry.collection?.key\n if (collection) {\n if (entry.slug) add(`${collection}/${entry.slug}`)\n for (const slug of Object.values(entry.localizedSlugs ?? {})) {\n if (slug) add(`${collection}/${slug}`)\n }\n } else if (entry.key) {\n add(`doc:${entry.key}`)\n }\n return refs\n}\n\n/**\n * The tag for one field, as attributes to spread on the element that renders\n * it: `{ 'data-zap': 'blog/hola#title' }`. Empty when the entry or the field\n * key is not one Zap can name, so a bad call never breaks the page.\n */\nexport function zapAttrs(entry: EntryLike, fieldKey: string): { 'data-zap'?: string } {\n const [ref] = recordRefsOf(entry)\n if (!ref || !isFieldKey(fieldKey)) return {}\n return { 'data-zap': `${ref}#${fieldKey}` }\n}\n","/**\n * Stega: invisible field markers on prose values read in preview\n * (`.docs/proposals/zap-cms-v2.md` §2.4, §7.5).\n *\n * Zap appends a marker to TEXT and LONG_TEXT values, and to the last text\n * node of each RICH_TEXT block, ONLY in `preview=1` delivery responses. The\n * preview overlay finds the marker in the page's text nodes and so knows\n * which element renders which field, with no manual tag. Published responses\n * never carry one.\n *\n * ## Format (eelzap stega v1)\n *\n * A marker is the tag the overlay would read from a `data-zap` attribute,\n * `recordRef#fieldKey`, plus `@locale` when the value belongs to a locale:\n *\n * blog-posts/mi-primer-post#title@es\n * doc:home#hero_title@es\n *\n * written as zero-width characters:\n *\n * - four digits, base 4: U+200B (0), U+200C (1), U+200D (2), U+2060 (3);\n * - a prefix of 8 digits, the bytes 0xE5 0xA7 (`3211 2213`);\n * - then every byte of the ASCII payload as 4 digits, most significant first.\n *\n * The payload is printable ASCII (the tag grammar allows nothing else), so\n * every payload byte starts with digit 0 or 1 and the prefix, which starts\n * with 3, can never be read inside a payload. A decoder reads groups of four\n * after the prefix until a group is not a payload byte, then validates the\n * result against the tag grammar; anything that does not parse is ignored. A\n * value never carries two markers (`stegaEncode` refuses to add a second).\n *\n * Why not `@vercel/stega`'s format: it encodes a JSON object (a URL and\n * origin in Sanity's and Vercel's use) and uses U+FEFF, which some tools strip\n * as a byte order mark. Here the decoded marker IS the tag grammar\n * (`data-zap`), so one parser serves both, and U+FEFF is avoided. No\n * dependency either way.\n *\n * Zero dependencies, no side effects: `.` re-exports `cleanStega` and\n * `hasStega`; the encoder is internal (Zap's delivery imports it); the\n * decoder serves `./preview`.\n */\n\nconst DIGITS = ['', '', '', ''] as const\nconst PREFIX = ''\n/** Longest payload a marker may carry, in bytes (ref 300 + field 128 + locale). */\nconst MAX_PAYLOAD = 512\n\n/** `recordRef#fieldKey` with an optional `@locale`: the tag grammar (`refs.ts`). */\nconst PAYLOAD_RE =\n /^(doc:[a-z0-9]+(?:-[a-z0-9]+)*|[a-z0-9]+(?:-[a-z0-9]+)*\\/[a-z0-9]+(?:-[a-z0-9]+)*)#([a-z][a-z0-9_]{0,127})(?:@([A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8}){0,3}))?$/\n/** The prefix, then whole payload bytes (first digit 0 or 1). */\nconst MARKER_RE = /(?:[][]{3})*/g\n\n/** What one marker says: the record, the field, and the locale when known. */\nexport interface StegaInfo {\n /** `collectionKey/slug` or `doc:documentKey`. */\n recordRef: string\n fieldKey: string\n locale?: string | null\n}\n\nexport interface DecodedStega {\n recordRef: string\n fieldKey: string\n locale: string | null\n /** `recordRef#fieldKey`, the `data-zap` tag. */\n tag: string\n}\n\n/** The marker for one field, or `''` when the info does not fit the tag grammar. */\nexport function stegaMarker(info: StegaInfo): string {\n const payload = `${info.recordRef}#${info.fieldKey}${info.locale ? `@${info.locale}` : ''}`\n if (payload.length > MAX_PAYLOAD || !PAYLOAD_RE.test(payload)) return ''\n let out = PREFIX\n for (let i = 0; i < payload.length; i++) {\n const byte = payload.charCodeAt(i)\n out +=\n DIGITS[(byte >> 6) & 3]! +\n DIGITS[(byte >> 4) & 3]! +\n DIGITS[(byte >> 2) & 3]! +\n DIGITS[byte & 3]!\n }\n return out\n}\n\n/**\n * `text` with the field's marker appended. A string that already carries a\n * marker, or info outside the grammar, comes back unchanged.\n */\nexport function stegaEncode(text: string, info: StegaInfo): string {\n if (hasStega(text)) return text\n return text + stegaMarker(info)\n}\n\n/** True when `value` is a string carrying at least one marker. */\nexport function hasStega(value: unknown): boolean {\n return typeof value === 'string' && value.includes(PREFIX)\n}\n\n/** Every valid marker in `text`, in order. Invalid ones are skipped. */\nexport function stegaDecode(text: string): DecodedStega[] {\n if (!text.includes(PREFIX)) return []\n const found: DecodedStega[] = []\n for (const match of text.match(MARKER_RE) ?? []) {\n let payload = ''\n for (let i = PREFIX.length; i + 4 <= match.length; i += 4) {\n let byte = 0\n for (let j = 0; j < 4; j++) byte = byte * 4 + DIGITS.indexOf(match[i + j] as never)\n payload += String.fromCharCode(byte)\n }\n const parsed = PAYLOAD_RE.exec(payload)\n if (!parsed) continue\n found.push({\n recordRef: parsed[1]!,\n fieldKey: parsed[2]!,\n locale: parsed[3] ?? null,\n tag: `${parsed[1]}#${parsed[2]}`,\n })\n }\n return found\n}\n\n/** `text` split into its visible part and the markers it carries, in order. */\nexport function splitStega(text: string): { text: string; markers: string } {\n if (!text.includes(PREFIX)) return { text, markers: '' }\n return { text: text.replace(MARKER_RE, ''), markers: (text.match(MARKER_RE) ?? []).join('') }\n}\n\n/** `text` without any marker. */\nfunction cleanString(text: string): string {\n return text.includes(PREFIX) ? text.replace(MARKER_RE, '') : text\n}\n\n/**\n * `value` with every stega marker removed: a string is cleaned, an array or a\n * plain object is cleaned deeply (into new objects), anything else is\n * returned as is. Use it where a preview value is compared, parsed or put in\n * an attribute, a URL or `<head>`; outside preview it changes nothing.\n */\nexport function cleanStega<T>(value: T): T {\n if (typeof value === 'string') return cleanString(value) as T\n if (Array.isArray(value)) return value.map((item) => cleanStega(item)) as T\n if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {\n const out: Record<string, unknown> = {}\n for (const [key, item] of Object.entries(value)) out[key] = cleanStega(item)\n return out as T\n }\n return value\n}\n","import { recordRefsOf } from './refs'\nimport { cleanStega, hasStega } from './stega'\n\n/**\n * `@8ux-co/eelzap/fields` (`.docs/proposals/zap-cms-v2.md` §2.4, §2.7): typed\n * pick helpers over an item or a document that tag themselves in preview.\n *\n * ```tsx\n * import { fields } from '@8ux-co/eelzap/fields'\n *\n * const f = fields(home) // a HomeDocument from codegen\n * <h1>{f.text('hero_title')}</h1> // carries its stega marker in preview\n * <a href={f.value('cta_url')} {...f.attrs('cta_url')}>…</a>\n * <img {...f.image('hero_image')} /> // src, alt, width, height (+ data-zap in preview)\n *\n * // A value the site renders itself (split, uppercased, formatted):\n * <h1 {...f.derived('title')}>{splitTitle(f.value('title'))}</h1>\n *\n * // Numbered slots: `stat_1` … `stat_4`\n * {f.list('stat', 4).map((s) => <p key={s.key}>{s.text()}</p>)}\n * // Slots of several fields: `nav_1_texto` + `nav_1_url` … `nav_5_…`\n * {f.list('nav', 5).filter((nav) => !nav.empty).map((nav) => (\n * <a key={nav.key} href={nav.value('url') ?? '/'} {...nav.attrs('url')}>{nav.text('texto')}</a>\n * ))}\n * ```\n *\n * Keys are typed from the record's `content`, so with codegen types a typo in\n * a field key fails to compile.\n *\n * Outside preview every helper returns plain values and EMPTY attribute\n * objects, so production HTML is byte-identical to reading values directly.\n * In preview (the record's text carries stega, or `{ preview: true }`), text\n * keeps its marker (the overlay finds it with no tag), and the non-text\n * helpers add `data-zap` (a manual tag). Markers never reach an attribute:\n * `value`, `image` and `attrs` clean them.\n *\n * `derived` is for an element that shows what the site computes from a field\n * (an italic-split title, a long text split into paragraphs, an uppercased\n * label): in preview it adds `data-zap-site-renders`, so the overlay outlines\n * the element and opens the field on a click but never writes into it. The\n * site keeps it current with `useZapLiveUpdates` or `onValues`.\n */\n\n/** What `fields` reads: an item (`collection.key` + `slug`) or a document (`key`). */\nexport interface FieldsRecord {\n content: object\n slug?: string | null\n key?: string | null\n collection?: { key?: string | null } | null\n}\n\ntype Content<R extends FieldsRecord> = R['content']\ntype Key<R extends FieldsRecord> = keyof Content<R> & string\n/** Field keys whose value is text (string, possibly null). */\ntype TextKey<R extends FieldsRecord> = {\n [K in Key<R>]: NonNullable<Content<R>[K]> extends string ? K : never\n}[Key<R>]\n/**\n * Every numbered prefix in `K`: `stat` for `stat_1`, `nav` for `nav_1_texto`,\n * `hero_stat` for `hero_stat_1`. Walks the key one `_` segment at a time, so a\n * prefix may itself contain `_`.\n */\ntype Prefixes<K extends string, Head extends string = ''> = K extends `${infer P}_${infer Rest}`\n ?\n | (Rest extends `${number}` | `${number}_${string}` ? `${Head}${P}` : never)\n | Prefixes<Rest, `${Head}${P}_`>\n : never\n/** Numbered prefixes of the record (any string for an untyped record). */\ntype ListPrefix<R extends FieldsRecord> = string extends Key<R> ? string : Prefixes<Key<R>>\n/** The suffixes of `prefix_{i}_{suffix}` slots: `texto` and `url` for `nav`. */\ntype SlotSuffix<R extends FieldsRecord, P extends string> =\n string extends Key<R>\n ? string\n : Key<R> extends infer K\n ? K extends `${P}_${number}_${infer S}`\n ? S\n : never\n : never\n/** A slot field's value type; `unknown` for an untyped record. */\ntype SlotValue<R extends FieldsRecord, K> =\n string extends Key<R> ? unknown : Content<R>[Extract<Key<R>, K>]\n\n/** `{ 'data-zap': 'blog/hola#cover' }` in preview, `{}` otherwise. */\nexport type ZapAttrs = { 'data-zap'?: string }\n\n/** `{ 'data-zap-site-renders': 'blog/hola#title' }` in preview, `{}` otherwise. */\nexport type ZapSiteRendersAttrs = { 'data-zap-site-renders'?: string }\n\nexport interface FieldImage extends ZapAttrs {\n src: string | undefined\n alt: string | undefined\n width: number | undefined\n height: number | undefined\n}\n\n/**\n * One numbered slot of `f.list(prefix, n)`: the same helpers as `fields`,\n * scoped to the slot. With a suffix they read `prefix_{i}_{suffix}`\n * (`nav.text('texto')` is `nav_1_texto`); without one, `prefix_{i}` itself\n * (`stat.text()` is `stat_1`).\n */\nexport interface FieldSlot<R extends FieldsRecord, P extends string> {\n /** `prefix_{i}`, unique in the list: a React key. */\n readonly key: string\n /** 1 for the first slot. */\n readonly index: number\n /** True when every field of the slot is empty (or the slot has no field). */\n readonly empty: boolean\n /** A text field as delivered, with its stega marker in preview. */\n text(suffix?: SlotSuffix<R, P>): string\n /** The single field `prefix_{i}`, with any marker removed. */\n value(): SlotValue<R, `${P}_${number}`>\n /** The slot's field `prefix_{i}_{suffix}`, with any marker removed. */\n value<S extends SlotSuffix<R, P>>(suffix: S): SlotValue<R, `${P}_${number}_${S}`>\n /** `{ 'data-zap': ref#key }` in preview, `{}` otherwise. */\n attrs(suffix?: SlotSuffix<R, P>): ZapAttrs\n /** `{ 'data-zap-site-renders': ref#key }` in preview: the site renders it, never written into. */\n derived(suffix?: SlotSuffix<R, P>): ZapSiteRendersAttrs\n /** An image or media field as `<img>` attributes, tagged in preview. */\n image(suffix?: SlotSuffix<R, P>): FieldImage\n}\n\nexport interface Fields<R extends FieldsRecord> {\n /** True when the helpers tag (the record was read in preview). */\n readonly preview: boolean\n /** A text field as delivered: with its stega marker in preview, so the overlay finds it. */\n text<K extends TextKey<R>>(key: K): string\n /** A field's value with any marker removed: safe in attributes, URLs, comparisons. */\n value<K extends Key<R>>(key: K): Content<R>[K]\n /** `{ 'data-zap': ref#key }` in preview, `{}` otherwise: spread it on the element. */\n attrs(key: Key<R>): ZapAttrs\n /**\n * `{ 'data-zap-site-renders': ref#key }` in preview, `{}` otherwise: for an\n * element that shows what the site computes from the field. The overlay\n * outlines it and opens the field on a click, and never writes into it.\n */\n derived(key: Key<R>): ZapSiteRendersAttrs\n /** An image or media field as `<img>` attributes, tagged in preview. */\n image(key: Key<R>): FieldImage\n /**\n * Slots `1` … `count` of a numbered prefix, each a `FieldSlot`: fields\n * `prefix_1` … `prefix_n`, or slots of several fields\n * `prefix_{i}_{suffix}` (`nav_1_texto`, `nav_1_url`), read by suffix.\n */\n list<P extends ListPrefix<R>>(prefix: P, count: number): FieldSlot<R, P>[]\n}\n\nexport interface FieldsOptions {\n /** Force preview on or off; default: on when any text value carries stega. */\n preview?: boolean\n}\n\nexport function fields<R extends FieldsRecord>(record: R, options: FieldsOptions = {}): Fields<R> {\n const content = (record?.content ?? {}) as Record<string, unknown>\n const preview = options.preview ?? Object.values(content).some((value) => hasStega(value))\n const [ref] = recordRefsOf(record)\n const attrs = (key: string): ZapAttrs => (preview && ref ? { 'data-zap': `${ref}#${key}` } : {})\n const derived = (key: string): ZapSiteRendersAttrs =>\n preview && ref ? { 'data-zap-site-renders': `${ref}#${key}` } : {}\n const value = (key: string) => cleanStega(content[key])\n const text = (key: string) => {\n const raw = content[key]\n return typeof raw === 'string' ? (preview ? raw : cleanStega(raw)) : ''\n }\n const image = (key: string): FieldImage => {\n const media = (value(key) ?? {}) as Record<string, unknown>\n const pick = <T>(name: string, type: string) =>\n typeof media[name] === type ? (media[name] as T) : undefined\n return {\n src: pick<string>('url', 'string'),\n alt: pick<string>('alt', 'string'),\n width: pick<number>('width', 'number'),\n height: pick<number>('height', 'number'),\n ...attrs(key),\n }\n }\n\n return {\n preview,\n text,\n value: value as Fields<R>['value'],\n attrs,\n derived,\n image,\n list: (prefix, count) =>\n Array.from({ length: Math.max(0, Math.floor(count)) }, (_, i) => {\n const key = `${prefix}_${i + 1}`\n const at = (suffix?: string) => (suffix ? `${key}_${suffix}` : key)\n const own = Object.keys(content).filter((k) => k === key || k.startsWith(`${key}_`))\n return {\n key,\n index: i + 1,\n empty: own.every((k) => {\n const v = value(k)\n return v === null || v === undefined || (typeof v === 'string' && !v.trim())\n }),\n text: (suffix?: string) => text(at(suffix)),\n value: (suffix?: string) => value(at(suffix)),\n attrs: (suffix?: string) => attrs(at(suffix)),\n derived: (suffix?: string) => derived(at(suffix)),\n image: (suffix?: string) => image(at(suffix)),\n } as FieldSlot<R, typeof prefix>\n }),\n }\n}\n"],"mappings":"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","names":["value"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -208,11 +208,16 @@ interface EnumValue {
|
|
|
208
208
|
* A CURRENCY field's value, read and written in this one shape.
|
|
209
209
|
*
|
|
210
210
|
* `amountMinor` is in the currency's minor unit (cents: `4800000` is
|
|
211
|
-
* COP 48.000,00), an integer. `currency` is the ISO 4217 code.
|
|
211
|
+
* COP 48.000,00), an integer. `currency` is the ISO 4217 code. Reads also
|
|
212
|
+
* carry `amount`, the same value in major units (`48000`): `amountMinor`
|
|
213
|
+
* divided by ten to the currency's ISO 4217 exponent (COP and USD 2, JPY 0).
|
|
214
|
+
* Writes ignore it.
|
|
212
215
|
*/
|
|
213
216
|
interface CurrencyValue {
|
|
214
217
|
amountMinor: number;
|
|
215
218
|
currency: string;
|
|
219
|
+
/** Major units, on reads: `amountMinor / 10^exponent`. */
|
|
220
|
+
amount?: number;
|
|
216
221
|
}
|
|
217
222
|
/**
|
|
218
223
|
* Access mode for a media value.
|
|
@@ -410,6 +415,12 @@ interface CollectionSummary {
|
|
|
410
415
|
key: string;
|
|
411
416
|
name: string;
|
|
412
417
|
description: string | null;
|
|
418
|
+
/**
|
|
419
|
+
* «Ruta en tu sitio»: the page an entry appears on, `/blog/{slug}`; `null`
|
|
420
|
+
* when unset. Read with a `secret_` key or a suite credential only; a
|
|
421
|
+
* `public_` key's answer does not carry it.
|
|
422
|
+
*/
|
|
423
|
+
previewPath?: string | null;
|
|
413
424
|
itemCount: number;
|
|
414
425
|
fields?: FieldInfo[];
|
|
415
426
|
sections?: SectionInfo[];
|
|
@@ -734,7 +745,7 @@ type CommentStatus = 'OPEN' | 'RESOLVED';
|
|
|
734
745
|
/** How a change request was resolved; APPLIED also covers "changed by hand". */
|
|
735
746
|
type CommentResolution = 'APPLIED' | 'DISMISSED';
|
|
736
747
|
/** Where the thread was made. The API derives it from the credential. */
|
|
737
|
-
type CommentSource = 'EDITOR' | 'PREVIEW' | 'SITE' | 'API' | 'MCP';
|
|
748
|
+
type CommentSource = 'EDITOR' | 'PREVIEW' | 'SITE' | 'API' | 'MCP' | 'AI';
|
|
738
749
|
type CommentAnchorKind = 'field' | 'dom' | 'spot';
|
|
739
750
|
/** A page-normalised rectangle: every value between 0 and 1. */
|
|
740
751
|
interface CommentRect {
|
|
@@ -955,7 +966,7 @@ interface CommentWriteBase {
|
|
|
955
966
|
type CreateCommentInput = CommentRecord & CommentWriteBase & {
|
|
956
967
|
isChangeRequest?: boolean;
|
|
957
968
|
assigneeId?: string | null;
|
|
958
|
-
/** At most 20; TEXT, LONG_TEXT, NUMBER and
|
|
969
|
+
/** At most 20; TEXT, LONG_TEXT, NUMBER, URL and RICH_TEXT (an HTML string) fields. */
|
|
959
970
|
proposedValues?: CommentProposedValueInput[];
|
|
960
971
|
};
|
|
961
972
|
/** `saveToDraft`: always a change request, no assignee, at least one proposed value. */
|
|
@@ -1124,6 +1135,7 @@ declare class CommentsResource {
|
|
|
1124
1135
|
* string.
|
|
1125
1136
|
*/
|
|
1126
1137
|
type VersionStatus = 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';
|
|
1138
|
+
type VersionActorKind = 'PERSON' | 'ASK_ZAP' | 'API_KEY' | 'MCP' | 'SITE_KEY' | 'AI_JOB';
|
|
1127
1139
|
/**
|
|
1128
1140
|
* One row of an entry's history. What `list`, `createDraft`, `publishDraft`
|
|
1129
1141
|
* and `rollback` answer with: no values.
|
|
@@ -1134,6 +1146,20 @@ interface VersionSummary {
|
|
|
1134
1146
|
status: VersionStatus;
|
|
1135
1147
|
/** The account that made the version, or null for an API key or a deleted account. */
|
|
1136
1148
|
createdById: string | null;
|
|
1149
|
+
/**
|
|
1150
|
+
* What wrote the version, recorded when it was written: `PERSON` (the editor,
|
|
1151
|
+
* and every version from before this field), `ASK_ZAP`, `API_KEY` (a workspace
|
|
1152
|
+
* API key), `MCP` (an OAuth connection such as Claude), `SITE_KEY` (a site
|
|
1153
|
+
* secret key) or `AI_JOB` (the dashboard's bulk site translation).
|
|
1154
|
+
*/
|
|
1155
|
+
actorKind: VersionActorKind;
|
|
1156
|
+
/**
|
|
1157
|
+
* The API key's or connection's name as it was when the version was written
|
|
1158
|
+
* (not renamed or removed with it); null for `PERSON` and `ASK_ZAP`.
|
|
1159
|
+
*/
|
|
1160
|
+
actorName: string | null;
|
|
1161
|
+
/** The OAuth client's display name ("Claude Code") for an MCP write; null otherwise. */
|
|
1162
|
+
actorApp: string | null;
|
|
1137
1163
|
/** The author's display name, or null when the account is gone. */
|
|
1138
1164
|
authorName: string | null;
|
|
1139
1165
|
authorAvatarUrl: string | null;
|
|
@@ -1307,6 +1333,11 @@ interface DocumentListEntry {
|
|
|
1307
1333
|
key: string;
|
|
1308
1334
|
name: string;
|
|
1309
1335
|
description: string | null;
|
|
1336
|
+
/**
|
|
1337
|
+
* «Ruta en tu sitio»: the page this document is, `/nosotros`; `null` when
|
|
1338
|
+
* unset. Read with a `secret_` key or a suite credential only.
|
|
1339
|
+
*/
|
|
1340
|
+
previewPath?: string | null;
|
|
1310
1341
|
status: string;
|
|
1311
1342
|
updatedAt: string;
|
|
1312
1343
|
publishedAt: string | null;
|
|
@@ -1319,6 +1350,12 @@ interface DocumentListEntry {
|
|
|
1319
1350
|
interface DocumentDetail<TContent = Record<string, unknown>> {
|
|
1320
1351
|
key: string;
|
|
1321
1352
|
name: string;
|
|
1353
|
+
/**
|
|
1354
|
+
* «Ruta en tu sitio»: the page this document is, `/nosotros`; `null` when
|
|
1355
|
+
* unset. Read with a `secret_` key or a suite credential only; a `public_`
|
|
1356
|
+
* key's answer does not carry it.
|
|
1357
|
+
*/
|
|
1358
|
+
previewPath?: string | null;
|
|
1322
1359
|
status: string;
|
|
1323
1360
|
locale: string;
|
|
1324
1361
|
meta: DocumentMeta;
|
|
@@ -2424,7 +2461,7 @@ interface WebhookCommentData<TEntry extends WebhookCommentRef = WebhookCommentRe
|
|
|
2424
2461
|
} | null;
|
|
2425
2462
|
comments: TEntry[];
|
|
2426
2463
|
}
|
|
2427
|
-
type WebhookCommentSource = 'EDITOR' | 'PREVIEW' | 'SITE' | 'API' | 'MCP';
|
|
2464
|
+
type WebhookCommentSource = 'EDITOR' | 'PREVIEW' | 'SITE' | 'API' | 'MCP' | 'AI';
|
|
2428
2465
|
/** `data` of `zap.comment.created`. */
|
|
2429
2466
|
type WebhookCommentCreatedData = WebhookCommentData<WebhookCommentRef & {
|
|
2430
2467
|
is_change_request: boolean;
|
|
@@ -2513,4 +2550,4 @@ declare function verifyWebhookSignature(payload: string, headers: WebhookHeaders
|
|
|
2513
2550
|
*/
|
|
2514
2551
|
declare function webhookChanges(payload: WebhookPayload): WebhookChange[];
|
|
2515
2552
|
|
|
2516
|
-
export { type ApiErrorDetail, type ApiErrorPayload, type CacheAdapter, type CacheStrategy, type CachedFetchOptions, type ClientConfig, type ClientDefaults, type CollectionDetail, type CollectionFieldListResponse, CollectionFieldsResource, type CollectionListResponse, type CollectionSectionListResponse, CollectionSectionsResource, type CollectionSummary, CollectionsResource, type CommentAnchor, type CommentAnchorInput, type CommentAnchorKind, type CommentDomAnchor, type CommentDraftAnswer, type CommentFieldAnchorInput, type CommentListOptions, type CommentListResponse, type CommentPersonRef, type CommentProposedValue, type CommentProposedValueInput, type CommentRecord, type CommentRect, type CommentReplyAnswer, type CommentResolution, type CommentSource, type CommentSpotAnchor, type CommentStatus, type CommentTarget, type CommentThread, type CommentThreadAnswer, type CommentVersionRef, type CommentViewport, CommentsResource, type CommonRequestOptions, type ConfirmUploadInput, type CreateCollectionInput, type CreateCommentInput, type CreateDocumentFieldInput, type CreateDocumentInput, type CreateDraftInput, type CreateFieldInput, type CreateItemInput, type CreateSectionInput, type CreateUploadUrlInput, type CurrencyValue, type DeleteDocumentOptions, type DeleteItemOptions, type DeleteOptions, type DeliveryStatus, type DocumentDeleteResponse, type DocumentDetail, type DocumentFieldListResponse, DocumentFieldsResource, type DocumentGetOptions, type DocumentListEntry, type DocumentListOptions, type DocumentListResponse, type DocumentMeta, type DocumentMutationResponse, type DocumentSectionListResponse, DocumentSectionsResource, type DocumentSeo, DocumentSeoResource, type DocumentSeoResponse, DocumentValuesResource, type DocumentValuesResponse, type DocumentVersionDetail, type DocumentVersionSeo, type DocumentVersionSummary, type DocumentVersionValue, DocumentVersionsResource, DocumentsResource, EelZapClient, EelZapError, type EelZapErrorCode, EelZapNetworkError, type EnumValue, type FieldConstraints, type FieldInfo, type FieldOptionInput, type FieldType, type FilterOperatorMap, type FilterValue, type GalleryItemValue, type IdempotencyOptions, type ItemDeleteResponse, type ItemDetail, type ItemFilters, type ItemGetOptions, type ItemListOptions, type ItemListResponse, type ItemMeta, type ItemMutationResponse, ItemQueryBuilder, type ItemSeo, ItemSeoResource, type ItemSeoResponse, type ItemVersionDetail, type ItemVersionSeo, type ItemVersionSlug, type ItemVersionSummary, type ItemVersionValue, ItemVersionsResource, ItemsResource, type JsonObject, type MediaAccess, type MediaDetail, type MediaDetailResponse, type MediaFromUrlInput, type MediaListParams, type MediaListResponse, MediaResource, type MediaSort, type MediaType, type MediaUploadInput, type MediaUsage, type MediaValue, MemoryCacheAdapter, type OnPageComment, type OnPageCommentAnchor, type OnPageComments, type OnPageField, type Pagination, type PreviewOption, type PreviewTokenValidation, PreviewTokensResource, type QueryPrimitive, type ReplyCommentInput, type RetryEvent, type RetryOptions, type SaveCommentToDraftInput, type SectionInfo, type SectionMeta, type Seo, type SeoImage, type SeoInput, type SiteInfo, type SiteListEntry, SiteResource, SitesResource, type ThreadComment, type UpdateCollectionInput, type UpdateCommentInput, type UpdateDocumentDraftInput, type UpdateDocumentFieldInput, type UpdateDocumentInput, type UpdateFieldInput, type UpdateItemDraftInput, type UpdateItemInput, type UpdateMediaInput, type UpdateSectionInput, type UpdateSiteInput, type UploadFile, type UploadUrlResponse, type VerifyWebhookOptions, type VersionDetail, type VersionGalleryItem, type VersionListResponse, type VersionSeo, type VersionStatus, type VersionSummary, type VersionValue, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, WEBHOOK_TOLERANCE_SECONDS, type WebhookAction, type WebhookActor, type WebhookChange, type WebhookCollectionEventData, type WebhookCollectionRef, type WebhookCommentCreatedData, type WebhookCommentData, type WebhookCommentRef, type WebhookCommentReopenedData, type WebhookCommentRepliedData, type WebhookCommentResolvedData, type WebhookCommentSource, type WebhookCommentTarget, type WebhookCommentUpdatedData, type WebhookDocumentEventData, type WebhookEventName, type WebhookEventType, type WebhookHeaders, type WebhookItemEventData, type WebhookMediaEventData, type WebhookPayload, type WebhookSchemaEventData, type WebhookSeoEventData, type WebhookSiteEventData, type WebhookSiteRef, type WebhookSubject, cachedFetch, cleanStega, createClient, getMediaUrl, hasStega, isEelZapError, verifyWebhookSignature, webhookChanges };
|
|
2553
|
+
export { type ApiErrorDetail, type ApiErrorPayload, type CacheAdapter, type CacheStrategy, type CachedFetchOptions, type ClientConfig, type ClientDefaults, type CollectionDetail, type CollectionFieldListResponse, CollectionFieldsResource, type CollectionListResponse, type CollectionSectionListResponse, CollectionSectionsResource, type CollectionSummary, CollectionsResource, type CommentAnchor, type CommentAnchorInput, type CommentAnchorKind, type CommentDomAnchor, type CommentDraftAnswer, type CommentFieldAnchorInput, type CommentListOptions, type CommentListResponse, type CommentPersonRef, type CommentProposedValue, type CommentProposedValueInput, type CommentRecord, type CommentRect, type CommentReplyAnswer, type CommentResolution, type CommentSource, type CommentSpotAnchor, type CommentStatus, type CommentTarget, type CommentThread, type CommentThreadAnswer, type CommentVersionRef, type CommentViewport, CommentsResource, type CommonRequestOptions, type ConfirmUploadInput, type CreateCollectionInput, type CreateCommentInput, type CreateDocumentFieldInput, type CreateDocumentInput, type CreateDraftInput, type CreateFieldInput, type CreateItemInput, type CreateSectionInput, type CreateUploadUrlInput, type CurrencyValue, type DeleteDocumentOptions, type DeleteItemOptions, type DeleteOptions, type DeliveryStatus, type DocumentDeleteResponse, type DocumentDetail, type DocumentFieldListResponse, DocumentFieldsResource, type DocumentGetOptions, type DocumentListEntry, type DocumentListOptions, type DocumentListResponse, type DocumentMeta, type DocumentMutationResponse, type DocumentSectionListResponse, DocumentSectionsResource, type DocumentSeo, DocumentSeoResource, type DocumentSeoResponse, DocumentValuesResource, type DocumentValuesResponse, type DocumentVersionDetail, type DocumentVersionSeo, type DocumentVersionSummary, type DocumentVersionValue, DocumentVersionsResource, DocumentsResource, EelZapClient, EelZapError, type EelZapErrorCode, EelZapNetworkError, type EnumValue, type FieldConstraints, type FieldInfo, type FieldOptionInput, type FieldType, type FilterOperatorMap, type FilterValue, type GalleryItemValue, type IdempotencyOptions, type ItemDeleteResponse, type ItemDetail, type ItemFilters, type ItemGetOptions, type ItemListOptions, type ItemListResponse, type ItemMeta, type ItemMutationResponse, ItemQueryBuilder, type ItemSeo, ItemSeoResource, type ItemSeoResponse, type ItemVersionDetail, type ItemVersionSeo, type ItemVersionSlug, type ItemVersionSummary, type ItemVersionValue, ItemVersionsResource, ItemsResource, type JsonObject, type MediaAccess, type MediaDetail, type MediaDetailResponse, type MediaFromUrlInput, type MediaListParams, type MediaListResponse, MediaResource, type MediaSort, type MediaType, type MediaUploadInput, type MediaUsage, type MediaValue, MemoryCacheAdapter, type OnPageComment, type OnPageCommentAnchor, type OnPageComments, type OnPageField, type Pagination, type PreviewOption, type PreviewTokenValidation, PreviewTokensResource, type QueryPrimitive, type ReplyCommentInput, type RetryEvent, type RetryOptions, type SaveCommentToDraftInput, type SectionInfo, type SectionMeta, type Seo, type SeoImage, type SeoInput, type SiteInfo, type SiteListEntry, SiteResource, SitesResource, type ThreadComment, type UpdateCollectionInput, type UpdateCommentInput, type UpdateDocumentDraftInput, type UpdateDocumentFieldInput, type UpdateDocumentInput, type UpdateFieldInput, type UpdateItemDraftInput, type UpdateItemInput, type UpdateMediaInput, type UpdateSectionInput, type UpdateSiteInput, type UploadFile, type UploadUrlResponse, type VerifyWebhookOptions, type VersionActorKind, type VersionDetail, type VersionGalleryItem, type VersionListResponse, type VersionSeo, type VersionStatus, type VersionSummary, type VersionValue, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, WEBHOOK_TOLERANCE_SECONDS, type WebhookAction, type WebhookActor, type WebhookChange, type WebhookCollectionEventData, type WebhookCollectionRef, type WebhookCommentCreatedData, type WebhookCommentData, type WebhookCommentRef, type WebhookCommentReopenedData, type WebhookCommentRepliedData, type WebhookCommentResolvedData, type WebhookCommentSource, type WebhookCommentTarget, type WebhookCommentUpdatedData, type WebhookDocumentEventData, type WebhookEventName, type WebhookEventType, type WebhookHeaders, type WebhookItemEventData, type WebhookMediaEventData, type WebhookPayload, type WebhookSchemaEventData, type WebhookSeoEventData, type WebhookSiteEventData, type WebhookSiteRef, type WebhookSubject, cachedFetch, cleanStega, createClient, getMediaUrl, hasStega, isEelZapError, verifyWebhookSignature, webhookChanges };
|
package/dist/index.d.ts
CHANGED
|
@@ -208,11 +208,16 @@ interface EnumValue {
|
|
|
208
208
|
* A CURRENCY field's value, read and written in this one shape.
|
|
209
209
|
*
|
|
210
210
|
* `amountMinor` is in the currency's minor unit (cents: `4800000` is
|
|
211
|
-
* COP 48.000,00), an integer. `currency` is the ISO 4217 code.
|
|
211
|
+
* COP 48.000,00), an integer. `currency` is the ISO 4217 code. Reads also
|
|
212
|
+
* carry `amount`, the same value in major units (`48000`): `amountMinor`
|
|
213
|
+
* divided by ten to the currency's ISO 4217 exponent (COP and USD 2, JPY 0).
|
|
214
|
+
* Writes ignore it.
|
|
212
215
|
*/
|
|
213
216
|
interface CurrencyValue {
|
|
214
217
|
amountMinor: number;
|
|
215
218
|
currency: string;
|
|
219
|
+
/** Major units, on reads: `amountMinor / 10^exponent`. */
|
|
220
|
+
amount?: number;
|
|
216
221
|
}
|
|
217
222
|
/**
|
|
218
223
|
* Access mode for a media value.
|
|
@@ -410,6 +415,12 @@ interface CollectionSummary {
|
|
|
410
415
|
key: string;
|
|
411
416
|
name: string;
|
|
412
417
|
description: string | null;
|
|
418
|
+
/**
|
|
419
|
+
* «Ruta en tu sitio»: the page an entry appears on, `/blog/{slug}`; `null`
|
|
420
|
+
* when unset. Read with a `secret_` key or a suite credential only; a
|
|
421
|
+
* `public_` key's answer does not carry it.
|
|
422
|
+
*/
|
|
423
|
+
previewPath?: string | null;
|
|
413
424
|
itemCount: number;
|
|
414
425
|
fields?: FieldInfo[];
|
|
415
426
|
sections?: SectionInfo[];
|
|
@@ -734,7 +745,7 @@ type CommentStatus = 'OPEN' | 'RESOLVED';
|
|
|
734
745
|
/** How a change request was resolved; APPLIED also covers "changed by hand". */
|
|
735
746
|
type CommentResolution = 'APPLIED' | 'DISMISSED';
|
|
736
747
|
/** Where the thread was made. The API derives it from the credential. */
|
|
737
|
-
type CommentSource = 'EDITOR' | 'PREVIEW' | 'SITE' | 'API' | 'MCP';
|
|
748
|
+
type CommentSource = 'EDITOR' | 'PREVIEW' | 'SITE' | 'API' | 'MCP' | 'AI';
|
|
738
749
|
type CommentAnchorKind = 'field' | 'dom' | 'spot';
|
|
739
750
|
/** A page-normalised rectangle: every value between 0 and 1. */
|
|
740
751
|
interface CommentRect {
|
|
@@ -955,7 +966,7 @@ interface CommentWriteBase {
|
|
|
955
966
|
type CreateCommentInput = CommentRecord & CommentWriteBase & {
|
|
956
967
|
isChangeRequest?: boolean;
|
|
957
968
|
assigneeId?: string | null;
|
|
958
|
-
/** At most 20; TEXT, LONG_TEXT, NUMBER and
|
|
969
|
+
/** At most 20; TEXT, LONG_TEXT, NUMBER, URL and RICH_TEXT (an HTML string) fields. */
|
|
959
970
|
proposedValues?: CommentProposedValueInput[];
|
|
960
971
|
};
|
|
961
972
|
/** `saveToDraft`: always a change request, no assignee, at least one proposed value. */
|
|
@@ -1124,6 +1135,7 @@ declare class CommentsResource {
|
|
|
1124
1135
|
* string.
|
|
1125
1136
|
*/
|
|
1126
1137
|
type VersionStatus = 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';
|
|
1138
|
+
type VersionActorKind = 'PERSON' | 'ASK_ZAP' | 'API_KEY' | 'MCP' | 'SITE_KEY' | 'AI_JOB';
|
|
1127
1139
|
/**
|
|
1128
1140
|
* One row of an entry's history. What `list`, `createDraft`, `publishDraft`
|
|
1129
1141
|
* and `rollback` answer with: no values.
|
|
@@ -1134,6 +1146,20 @@ interface VersionSummary {
|
|
|
1134
1146
|
status: VersionStatus;
|
|
1135
1147
|
/** The account that made the version, or null for an API key or a deleted account. */
|
|
1136
1148
|
createdById: string | null;
|
|
1149
|
+
/**
|
|
1150
|
+
* What wrote the version, recorded when it was written: `PERSON` (the editor,
|
|
1151
|
+
* and every version from before this field), `ASK_ZAP`, `API_KEY` (a workspace
|
|
1152
|
+
* API key), `MCP` (an OAuth connection such as Claude), `SITE_KEY` (a site
|
|
1153
|
+
* secret key) or `AI_JOB` (the dashboard's bulk site translation).
|
|
1154
|
+
*/
|
|
1155
|
+
actorKind: VersionActorKind;
|
|
1156
|
+
/**
|
|
1157
|
+
* The API key's or connection's name as it was when the version was written
|
|
1158
|
+
* (not renamed or removed with it); null for `PERSON` and `ASK_ZAP`.
|
|
1159
|
+
*/
|
|
1160
|
+
actorName: string | null;
|
|
1161
|
+
/** The OAuth client's display name ("Claude Code") for an MCP write; null otherwise. */
|
|
1162
|
+
actorApp: string | null;
|
|
1137
1163
|
/** The author's display name, or null when the account is gone. */
|
|
1138
1164
|
authorName: string | null;
|
|
1139
1165
|
authorAvatarUrl: string | null;
|
|
@@ -1307,6 +1333,11 @@ interface DocumentListEntry {
|
|
|
1307
1333
|
key: string;
|
|
1308
1334
|
name: string;
|
|
1309
1335
|
description: string | null;
|
|
1336
|
+
/**
|
|
1337
|
+
* «Ruta en tu sitio»: the page this document is, `/nosotros`; `null` when
|
|
1338
|
+
* unset. Read with a `secret_` key or a suite credential only.
|
|
1339
|
+
*/
|
|
1340
|
+
previewPath?: string | null;
|
|
1310
1341
|
status: string;
|
|
1311
1342
|
updatedAt: string;
|
|
1312
1343
|
publishedAt: string | null;
|
|
@@ -1319,6 +1350,12 @@ interface DocumentListEntry {
|
|
|
1319
1350
|
interface DocumentDetail<TContent = Record<string, unknown>> {
|
|
1320
1351
|
key: string;
|
|
1321
1352
|
name: string;
|
|
1353
|
+
/**
|
|
1354
|
+
* «Ruta en tu sitio»: the page this document is, `/nosotros`; `null` when
|
|
1355
|
+
* unset. Read with a `secret_` key or a suite credential only; a `public_`
|
|
1356
|
+
* key's answer does not carry it.
|
|
1357
|
+
*/
|
|
1358
|
+
previewPath?: string | null;
|
|
1322
1359
|
status: string;
|
|
1323
1360
|
locale: string;
|
|
1324
1361
|
meta: DocumentMeta;
|
|
@@ -2424,7 +2461,7 @@ interface WebhookCommentData<TEntry extends WebhookCommentRef = WebhookCommentRe
|
|
|
2424
2461
|
} | null;
|
|
2425
2462
|
comments: TEntry[];
|
|
2426
2463
|
}
|
|
2427
|
-
type WebhookCommentSource = 'EDITOR' | 'PREVIEW' | 'SITE' | 'API' | 'MCP';
|
|
2464
|
+
type WebhookCommentSource = 'EDITOR' | 'PREVIEW' | 'SITE' | 'API' | 'MCP' | 'AI';
|
|
2428
2465
|
/** `data` of `zap.comment.created`. */
|
|
2429
2466
|
type WebhookCommentCreatedData = WebhookCommentData<WebhookCommentRef & {
|
|
2430
2467
|
is_change_request: boolean;
|
|
@@ -2513,4 +2550,4 @@ declare function verifyWebhookSignature(payload: string, headers: WebhookHeaders
|
|
|
2513
2550
|
*/
|
|
2514
2551
|
declare function webhookChanges(payload: WebhookPayload): WebhookChange[];
|
|
2515
2552
|
|
|
2516
|
-
export { type ApiErrorDetail, type ApiErrorPayload, type CacheAdapter, type CacheStrategy, type CachedFetchOptions, type ClientConfig, type ClientDefaults, type CollectionDetail, type CollectionFieldListResponse, CollectionFieldsResource, type CollectionListResponse, type CollectionSectionListResponse, CollectionSectionsResource, type CollectionSummary, CollectionsResource, type CommentAnchor, type CommentAnchorInput, type CommentAnchorKind, type CommentDomAnchor, type CommentDraftAnswer, type CommentFieldAnchorInput, type CommentListOptions, type CommentListResponse, type CommentPersonRef, type CommentProposedValue, type CommentProposedValueInput, type CommentRecord, type CommentRect, type CommentReplyAnswer, type CommentResolution, type CommentSource, type CommentSpotAnchor, type CommentStatus, type CommentTarget, type CommentThread, type CommentThreadAnswer, type CommentVersionRef, type CommentViewport, CommentsResource, type CommonRequestOptions, type ConfirmUploadInput, type CreateCollectionInput, type CreateCommentInput, type CreateDocumentFieldInput, type CreateDocumentInput, type CreateDraftInput, type CreateFieldInput, type CreateItemInput, type CreateSectionInput, type CreateUploadUrlInput, type CurrencyValue, type DeleteDocumentOptions, type DeleteItemOptions, type DeleteOptions, type DeliveryStatus, type DocumentDeleteResponse, type DocumentDetail, type DocumentFieldListResponse, DocumentFieldsResource, type DocumentGetOptions, type DocumentListEntry, type DocumentListOptions, type DocumentListResponse, type DocumentMeta, type DocumentMutationResponse, type DocumentSectionListResponse, DocumentSectionsResource, type DocumentSeo, DocumentSeoResource, type DocumentSeoResponse, DocumentValuesResource, type DocumentValuesResponse, type DocumentVersionDetail, type DocumentVersionSeo, type DocumentVersionSummary, type DocumentVersionValue, DocumentVersionsResource, DocumentsResource, EelZapClient, EelZapError, type EelZapErrorCode, EelZapNetworkError, type EnumValue, type FieldConstraints, type FieldInfo, type FieldOptionInput, type FieldType, type FilterOperatorMap, type FilterValue, type GalleryItemValue, type IdempotencyOptions, type ItemDeleteResponse, type ItemDetail, type ItemFilters, type ItemGetOptions, type ItemListOptions, type ItemListResponse, type ItemMeta, type ItemMutationResponse, ItemQueryBuilder, type ItemSeo, ItemSeoResource, type ItemSeoResponse, type ItemVersionDetail, type ItemVersionSeo, type ItemVersionSlug, type ItemVersionSummary, type ItemVersionValue, ItemVersionsResource, ItemsResource, type JsonObject, type MediaAccess, type MediaDetail, type MediaDetailResponse, type MediaFromUrlInput, type MediaListParams, type MediaListResponse, MediaResource, type MediaSort, type MediaType, type MediaUploadInput, type MediaUsage, type MediaValue, MemoryCacheAdapter, type OnPageComment, type OnPageCommentAnchor, type OnPageComments, type OnPageField, type Pagination, type PreviewOption, type PreviewTokenValidation, PreviewTokensResource, type QueryPrimitive, type ReplyCommentInput, type RetryEvent, type RetryOptions, type SaveCommentToDraftInput, type SectionInfo, type SectionMeta, type Seo, type SeoImage, type SeoInput, type SiteInfo, type SiteListEntry, SiteResource, SitesResource, type ThreadComment, type UpdateCollectionInput, type UpdateCommentInput, type UpdateDocumentDraftInput, type UpdateDocumentFieldInput, type UpdateDocumentInput, type UpdateFieldInput, type UpdateItemDraftInput, type UpdateItemInput, type UpdateMediaInput, type UpdateSectionInput, type UpdateSiteInput, type UploadFile, type UploadUrlResponse, type VerifyWebhookOptions, type VersionDetail, type VersionGalleryItem, type VersionListResponse, type VersionSeo, type VersionStatus, type VersionSummary, type VersionValue, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, WEBHOOK_TOLERANCE_SECONDS, type WebhookAction, type WebhookActor, type WebhookChange, type WebhookCollectionEventData, type WebhookCollectionRef, type WebhookCommentCreatedData, type WebhookCommentData, type WebhookCommentRef, type WebhookCommentReopenedData, type WebhookCommentRepliedData, type WebhookCommentResolvedData, type WebhookCommentSource, type WebhookCommentTarget, type WebhookCommentUpdatedData, type WebhookDocumentEventData, type WebhookEventName, type WebhookEventType, type WebhookHeaders, type WebhookItemEventData, type WebhookMediaEventData, type WebhookPayload, type WebhookSchemaEventData, type WebhookSeoEventData, type WebhookSiteEventData, type WebhookSiteRef, type WebhookSubject, cachedFetch, cleanStega, createClient, getMediaUrl, hasStega, isEelZapError, verifyWebhookSignature, webhookChanges };
|
|
2553
|
+
export { type ApiErrorDetail, type ApiErrorPayload, type CacheAdapter, type CacheStrategy, type CachedFetchOptions, type ClientConfig, type ClientDefaults, type CollectionDetail, type CollectionFieldListResponse, CollectionFieldsResource, type CollectionListResponse, type CollectionSectionListResponse, CollectionSectionsResource, type CollectionSummary, CollectionsResource, type CommentAnchor, type CommentAnchorInput, type CommentAnchorKind, type CommentDomAnchor, type CommentDraftAnswer, type CommentFieldAnchorInput, type CommentListOptions, type CommentListResponse, type CommentPersonRef, type CommentProposedValue, type CommentProposedValueInput, type CommentRecord, type CommentRect, type CommentReplyAnswer, type CommentResolution, type CommentSource, type CommentSpotAnchor, type CommentStatus, type CommentTarget, type CommentThread, type CommentThreadAnswer, type CommentVersionRef, type CommentViewport, CommentsResource, type CommonRequestOptions, type ConfirmUploadInput, type CreateCollectionInput, type CreateCommentInput, type CreateDocumentFieldInput, type CreateDocumentInput, type CreateDraftInput, type CreateFieldInput, type CreateItemInput, type CreateSectionInput, type CreateUploadUrlInput, type CurrencyValue, type DeleteDocumentOptions, type DeleteItemOptions, type DeleteOptions, type DeliveryStatus, type DocumentDeleteResponse, type DocumentDetail, type DocumentFieldListResponse, DocumentFieldsResource, type DocumentGetOptions, type DocumentListEntry, type DocumentListOptions, type DocumentListResponse, type DocumentMeta, type DocumentMutationResponse, type DocumentSectionListResponse, DocumentSectionsResource, type DocumentSeo, DocumentSeoResource, type DocumentSeoResponse, DocumentValuesResource, type DocumentValuesResponse, type DocumentVersionDetail, type DocumentVersionSeo, type DocumentVersionSummary, type DocumentVersionValue, DocumentVersionsResource, DocumentsResource, EelZapClient, EelZapError, type EelZapErrorCode, EelZapNetworkError, type EnumValue, type FieldConstraints, type FieldInfo, type FieldOptionInput, type FieldType, type FilterOperatorMap, type FilterValue, type GalleryItemValue, type IdempotencyOptions, type ItemDeleteResponse, type ItemDetail, type ItemFilters, type ItemGetOptions, type ItemListOptions, type ItemListResponse, type ItemMeta, type ItemMutationResponse, ItemQueryBuilder, type ItemSeo, ItemSeoResource, type ItemSeoResponse, type ItemVersionDetail, type ItemVersionSeo, type ItemVersionSlug, type ItemVersionSummary, type ItemVersionValue, ItemVersionsResource, ItemsResource, type JsonObject, type MediaAccess, type MediaDetail, type MediaDetailResponse, type MediaFromUrlInput, type MediaListParams, type MediaListResponse, MediaResource, type MediaSort, type MediaType, type MediaUploadInput, type MediaUsage, type MediaValue, MemoryCacheAdapter, type OnPageComment, type OnPageCommentAnchor, type OnPageComments, type OnPageField, type Pagination, type PreviewOption, type PreviewTokenValidation, PreviewTokensResource, type QueryPrimitive, type ReplyCommentInput, type RetryEvent, type RetryOptions, type SaveCommentToDraftInput, type SectionInfo, type SectionMeta, type Seo, type SeoImage, type SeoInput, type SiteInfo, type SiteListEntry, SiteResource, SitesResource, type ThreadComment, type UpdateCollectionInput, type UpdateCommentInput, type UpdateDocumentDraftInput, type UpdateDocumentFieldInput, type UpdateDocumentInput, type UpdateFieldInput, type UpdateItemDraftInput, type UpdateItemInput, type UpdateMediaInput, type UpdateSectionInput, type UpdateSiteInput, type UploadFile, type UploadUrlResponse, type VerifyWebhookOptions, type VersionActorKind, type VersionDetail, type VersionGalleryItem, type VersionListResponse, type VersionSeo, type VersionStatus, type VersionSummary, type VersionValue, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, WEBHOOK_TOLERANCE_SECONDS, type WebhookAction, type WebhookActor, type WebhookChange, type WebhookCollectionEventData, type WebhookCollectionRef, type WebhookCommentCreatedData, type WebhookCommentData, type WebhookCommentRef, type WebhookCommentReopenedData, type WebhookCommentRepliedData, type WebhookCommentResolvedData, type WebhookCommentSource, type WebhookCommentTarget, type WebhookCommentUpdatedData, type WebhookDocumentEventData, type WebhookEventName, type WebhookEventType, type WebhookHeaders, type WebhookItemEventData, type WebhookMediaEventData, type WebhookPayload, type WebhookSchemaEventData, type WebhookSeoEventData, type WebhookSiteEventData, type WebhookSiteRef, type WebhookSubject, cachedFetch, cleanStega, createClient, getMediaUrl, hasStega, isEelZapError, verifyWebhookSignature, webhookChanges };
|
package/dist/next.d.ts
CHANGED
|
@@ -59,6 +59,13 @@ interface ZapBootOptions {
|
|
|
59
59
|
|
|
60
60
|
type ZapPreviewProps = ZapBootOptions;
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The path to redirect to, or null when it could leave the site. Returned
|
|
64
|
+
* as path + query + hash, never absolute, so the `Location` stays on the
|
|
65
|
+
* host the browser asked.
|
|
66
|
+
*/
|
|
67
|
+
declare function safeRedirectPath(raw: string | null, requestUrl: string): string | null;
|
|
68
|
+
|
|
62
69
|
/**
|
|
63
70
|
* `@8ux-co/eelzap/next` — the draft-mode route, `isZapPreview()` and the
|
|
64
71
|
* preview boot for a Next.js site (zap-cms-v2 §2.5 "Draft values", §2.7, §3.5).
|
|
@@ -68,21 +75,29 @@ type ZapPreviewProps = ZapBootOptions;
|
|
|
68
75
|
* import { cookies, draftMode } from 'next/headers'
|
|
69
76
|
* import { createDraftModeRoute } from '@8ux-co/eelzap/next'
|
|
70
77
|
*
|
|
71
|
-
* export const
|
|
78
|
+
* export const POST = createDraftModeRoute({
|
|
72
79
|
* siteKey: 'verdeorigen',
|
|
73
80
|
* apiKey: process.env.ZAP_SECRET_KEY!,
|
|
74
81
|
* draftMode,
|
|
75
82
|
* cookies,
|
|
76
83
|
* })
|
|
84
|
+
* export const GET = POST
|
|
77
85
|
* ```
|
|
78
86
|
*
|
|
79
87
|
* and `<ZapPreview siteKey="verdeorigen" />` in the root layout, which tells
|
|
80
88
|
* Zap's editor the route's path (`zap:ready`, §2.5); the editor joins it to the
|
|
81
|
-
* frame's verified origin
|
|
89
|
+
* frame's verified origin. Clients advertising `fragment-token` opt into
|
|
90
|
+
* `{origin}{route}#token=…&path=…`; older clients keep query URLs.
|
|
91
|
+
* GET serves a tiny page and a same-origin script that clears the fragment
|
|
92
|
+
* and submits a form POST.
|
|
93
|
+
* Fragments never travel in HTTP request URLs or site request logs.
|
|
82
94
|
*
|
|
83
95
|
* ## What the route does, in order
|
|
84
96
|
*
|
|
85
|
-
* 1.
|
|
97
|
+
* 1. Requires same-origin evidence for POST (Sec-Fetch-Site or Origin), then
|
|
98
|
+
* reads `token` and `path` from the form body. Legacy GET query tokens
|
|
99
|
+
* remain supported. A token not shaped like
|
|
100
|
+
* Zap's
|
|
86
101
|
* (`zpt_` + 43 base64url characters) is refused (401) without a request.
|
|
87
102
|
* 2. Refuses a `path` that could leave the site: it must start with one `/`,
|
|
88
103
|
* carry no backslash or control character, resolve to this request's own
|
|
@@ -106,7 +121,8 @@ type ZapPreviewProps = ZapBootOptions;
|
|
|
106
121
|
* Chrome keeps a partitioned (CHIPS) cookie under third-party cookie
|
|
107
122
|
* restrictions. Safari blocks frame cookies regardless; the client's
|
|
108
123
|
* cookieless fallback covers it (`previewHeaders` → `getValidPreviewToken`).
|
|
109
|
-
* 5. Redirects (307) to `path`,
|
|
124
|
+
* 5. Redirects (303 after POST; 307 for legacy GET) to `path`, without the
|
|
125
|
+
* token, `Cache-Control: no-store`,
|
|
110
126
|
* `Referrer-Policy: no-referrer`.
|
|
111
127
|
*
|
|
112
128
|
* `createDraftModeExitRoute` ends the preview: both cookies expired, same
|
|
@@ -151,13 +167,7 @@ interface DraftModeRouteOptions {
|
|
|
151
167
|
/** Injected in specs. */
|
|
152
168
|
fetch?: typeof fetch;
|
|
153
169
|
}
|
|
154
|
-
type DraftModeRefusal = 'invalid_token' | 'invalid_path' | 'token_refused' | 'wrong_site' | 'zap_unreachable';
|
|
155
|
-
/**
|
|
156
|
-
* The path to redirect to, or null when it could leave the site. Returned
|
|
157
|
-
* as path + query + hash, never absolute, so the `Location` stays on the
|
|
158
|
-
* host the browser asked.
|
|
159
|
-
*/
|
|
160
|
-
declare function safeRedirectPath(raw: string | null, requestUrl: string): string | null;
|
|
170
|
+
type DraftModeRefusal = 'cross_origin' | 'invalid_token' | 'invalid_path' | 'token_refused' | 'wrong_site' | 'zap_unreachable';
|
|
161
171
|
declare function createDraftModeRoute(options: DraftModeRouteOptions): (request: Request) => Promise<Response>;
|
|
162
172
|
interface DraftModeExitLike {
|
|
163
173
|
disable(): void | Promise<void>;
|
package/dist/next.js
CHANGED
|
@@ -8,6 +8,36 @@ function isPreviewTokenShape(value) {
|
|
|
8
8
|
return typeof value === "string" && PREVIEW_TOKEN_RE.test(value);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// src/preview/draft-path.ts
|
|
12
|
+
var UNSAFE_PATH_CHARS = /[\u0000-\u001f\u007f\\]/;
|
|
13
|
+
function safeRedirectPath(raw, requestUrl) {
|
|
14
|
+
const value = raw ?? "/";
|
|
15
|
+
if (value.length === 0 || value.length > 2048) return null;
|
|
16
|
+
if (!value.startsWith("/") || value.startsWith("//")) return null;
|
|
17
|
+
if (UNSAFE_PATH_CHARS.test(value)) return null;
|
|
18
|
+
let base;
|
|
19
|
+
let target;
|
|
20
|
+
try {
|
|
21
|
+
base = new URL(requestUrl);
|
|
22
|
+
target = new URL(value, base);
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if (target.origin !== base.origin) return null;
|
|
27
|
+
if (target.pathname.startsWith("//")) return null;
|
|
28
|
+
let decoded = value;
|
|
29
|
+
for (let i = 0; i < value.length; i++) {
|
|
30
|
+
if (/zpt_/i.test(decoded)) return null;
|
|
31
|
+
const next = decoded.replace(
|
|
32
|
+
/%([0-9a-f]{2})/gi,
|
|
33
|
+
(_, hex) => String.fromCharCode(Number.parseInt(hex, 16))
|
|
34
|
+
);
|
|
35
|
+
if (next === decoded) break;
|
|
36
|
+
decoded = next;
|
|
37
|
+
}
|
|
38
|
+
return `${target.pathname}${target.search}${target.hash}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
11
41
|
// src/next.ts
|
|
12
42
|
var PREVIEW_TOKEN_COOKIE = "__zap_preview";
|
|
13
43
|
var NEXT_DRAFT_COOKIE = "__prerender_bypass";
|
|
@@ -21,35 +51,16 @@ var NO_STORE = { "Cache-Control": "no-store", "Referrer-Policy": "no-referrer" }
|
|
|
21
51
|
function refuse(status, error) {
|
|
22
52
|
return Response.json({ error }, { status, headers: NO_STORE });
|
|
23
53
|
}
|
|
24
|
-
function redirect(path, cookies) {
|
|
54
|
+
function redirect(path, cookies, status = 307) {
|
|
25
55
|
const headers = new Headers({ Location: path, ...NO_STORE });
|
|
26
56
|
for (const cookie of cookies) headers.append("Set-Cookie", cookie);
|
|
27
|
-
return new Response(null, { status
|
|
57
|
+
return new Response(null, { status, headers });
|
|
28
58
|
}
|
|
29
59
|
function requireKeys(options) {
|
|
30
60
|
if (!options?.siteKey || !options.apiKey) {
|
|
31
61
|
throw new Error("eelzap/next: siteKey and apiKey (the site API key) are required");
|
|
32
62
|
}
|
|
33
63
|
}
|
|
34
|
-
var UNSAFE_PATH_CHARS = /[\u0000-\u001f\u007f\\]/;
|
|
35
|
-
function safeRedirectPath(raw, requestUrl) {
|
|
36
|
-
const value = raw ?? "/";
|
|
37
|
-
if (value.length === 0 || value.length > 2048) return null;
|
|
38
|
-
if (!value.startsWith("/") || value.startsWith("//")) return null;
|
|
39
|
-
if (UNSAFE_PATH_CHARS.test(value)) return null;
|
|
40
|
-
let base;
|
|
41
|
-
let target;
|
|
42
|
-
try {
|
|
43
|
-
base = new URL(requestUrl);
|
|
44
|
-
target = new URL(value, base);
|
|
45
|
-
} catch {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
if (target.origin !== base.origin) return null;
|
|
49
|
-
if (target.pathname.startsWith("//")) return null;
|
|
50
|
-
if (/zpt_/i.test(target.search) || /zpt_/i.test(target.hash)) return null;
|
|
51
|
-
return `${target.pathname}${target.search}${target.hash}`;
|
|
52
|
-
}
|
|
53
64
|
function cookieHeader(name, value, maxAge) {
|
|
54
65
|
const parts = [`${name}=${value}`, "Path=/", "HttpOnly", "Secure", "SameSite=None", "Partitioned"];
|
|
55
66
|
if (maxAge !== void 0) parts.push(`Max-Age=${maxAge}`);
|
|
@@ -82,13 +93,60 @@ async function validateWithZap(token, options) {
|
|
|
82
93
|
const key = body?.site?.key;
|
|
83
94
|
return typeof key === "string" ? { ok: true, siteKey: key, expiresAt: readExpiry(body?.expiresAt ?? body?.token?.expiresAt) } : { ok: false, status: 502, code: null };
|
|
84
95
|
}
|
|
96
|
+
var FRAGMENT_EXCHANGE_SCRIPT = `(() => {
|
|
97
|
+
const params = new URLSearchParams(location.hash.slice(1));
|
|
98
|
+
history.replaceState(null, '', location.pathname);
|
|
99
|
+
const token = params.get('token');
|
|
100
|
+
if (!/^zpt_[A-Za-z0-9_-]{43}$/.test(token || '')) return;
|
|
101
|
+
const form = document.createElement('form');
|
|
102
|
+
form.method = 'POST';
|
|
103
|
+
form.action = location.pathname;
|
|
104
|
+
for (const [name, value] of [['token', token], ['path', params.get('path') ?? '/']]) {
|
|
105
|
+
const input = document.createElement('input');
|
|
106
|
+
input.type = 'hidden';
|
|
107
|
+
input.name = name;
|
|
108
|
+
input.value = value;
|
|
109
|
+
form.appendChild(input);
|
|
110
|
+
}
|
|
111
|
+
document.body.appendChild(form);
|
|
112
|
+
form.submit();
|
|
113
|
+
})();`;
|
|
114
|
+
function fragmentExchangeScript() {
|
|
115
|
+
return new Response(FRAGMENT_EXCHANGE_SCRIPT, {
|
|
116
|
+
headers: { ...NO_STORE, "Content-Type": "application/javascript; charset=utf-8" }
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function fragmentExchangePage() {
|
|
120
|
+
const html = `<!doctype html><html><head><meta charset="utf-8"><title>Zap preview</title></head><body>
|
|
121
|
+
<script src="?script=1"></script><noscript>JavaScript is required to enter Zap preview.</noscript></body></html>`;
|
|
122
|
+
return new Response(html, {
|
|
123
|
+
headers: {
|
|
124
|
+
...NO_STORE,
|
|
125
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
126
|
+
"Content-Security-Policy": "default-src 'none'; script-src 'self'; form-action 'self'; base-uri 'none'"
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
85
130
|
function createDraftModeRoute(options) {
|
|
86
131
|
requireKeys(options);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
132
|
+
const exchange = async (request) => {
|
|
133
|
+
let params;
|
|
134
|
+
if (request.method === "POST") {
|
|
135
|
+
const sameOrigin = request.headers.get("sec-fetch-site") === "same-origin" || request.headers.get("origin") === new URL(request.url).origin;
|
|
136
|
+
if (!sameOrigin) return refuse(403, "cross_origin");
|
|
137
|
+
try {
|
|
138
|
+
params = await request.formData();
|
|
139
|
+
} catch {
|
|
140
|
+
return refuse(401, "invalid_token");
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
params = new URL(request.url).searchParams;
|
|
144
|
+
}
|
|
145
|
+
const token = params.get("token");
|
|
90
146
|
if (!isPreviewTokenShape(token)) return refuse(401, "invalid_token");
|
|
91
|
-
const
|
|
147
|
+
const rawPath = params.get("path");
|
|
148
|
+
if (rawPath !== null && typeof rawPath !== "string") return refuse(400, "invalid_path");
|
|
149
|
+
const path = safeRedirectPath(rawPath, request.url);
|
|
92
150
|
if (path === null) return refuse(400, "invalid_path");
|
|
93
151
|
let verdict;
|
|
94
152
|
try {
|
|
@@ -108,7 +166,17 @@ function createDraftModeRoute(options) {
|
|
|
108
166
|
if (bypass && /^[A-Za-z0-9._~-]+$/.test(bypass)) {
|
|
109
167
|
cookies.push(cookieHeader(NEXT_DRAFT_COOKIE, bypass, PREVIEW_COOKIE_MAX_AGE));
|
|
110
168
|
}
|
|
111
|
-
return redirect(path, cookies);
|
|
169
|
+
return redirect(path, cookies, request.method === "POST" ? 303 : 307);
|
|
170
|
+
};
|
|
171
|
+
return async function draftModeRoute(request) {
|
|
172
|
+
if (request.method === "GET") {
|
|
173
|
+
const params = new URL(request.url).searchParams;
|
|
174
|
+
if (params.get("script") === "1") return fragmentExchangeScript();
|
|
175
|
+
if (!params.has("token")) return fragmentExchangePage();
|
|
176
|
+
return exchange(request);
|
|
177
|
+
}
|
|
178
|
+
if (request.method === "POST") return exchange(request);
|
|
179
|
+
return new Response(null, { status: 405, headers: { ...NO_STORE, Allow: "GET, POST" } });
|
|
112
180
|
};
|
|
113
181
|
}
|
|
114
182
|
function createDraftModeExitRoute(options) {
|