payload-live-preview 1.8.0 → 2.0.0-beta.0

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.
Files changed (98) hide show
  1. package/README.md +301 -93
  2. package/dist/adapters/astro/PreviewBoundary.astro +66 -0
  3. package/dist/adapters/astro/RichText.astro +6 -0
  4. package/dist/adapters/astro/index.d.ts +165 -134
  5. package/dist/adapters/astro/index.js +1 -1
  6. package/dist/adapters/astro/index.js.map +1 -1
  7. package/dist/adapters/astro/middleware-entry.d.ts +4 -9
  8. package/dist/adapters/astro/middleware-entry.js +1 -1
  9. package/dist/adapters/astro/middleware-entry.js.map +1 -1
  10. package/dist/adapters/nextjs/index.d.ts +13 -80
  11. package/dist/adapters/nextjs/index.js +1 -1
  12. package/dist/adapters/nextjs/index.js.map +1 -1
  13. package/dist/adapters/nuxt/index.d.ts +46 -106
  14. package/dist/adapters/nuxt/index.js +1 -1
  15. package/dist/adapters/nuxt/index.js.map +1 -1
  16. package/dist/adapters/sveltekit/index.d.ts +10 -75
  17. package/dist/adapters/sveltekit/index.js +1 -1
  18. package/dist/adapters/sveltekit/index.js.map +1 -1
  19. package/dist/client.cjs +2 -0
  20. package/dist/client.cjs.map +1 -0
  21. package/dist/client.d.cts +747 -0
  22. package/dist/client.d.ts +747 -0
  23. package/dist/client.js +2 -0
  24. package/dist/client.js.map +1 -0
  25. package/dist/codegen-astro.d.ts +5 -27
  26. package/dist/codegen-astro.js +1 -1
  27. package/dist/codegen-astro.js.map +1 -1
  28. package/dist/codegen-cli.d.ts +3 -16
  29. package/dist/codegen-cli.js +1 -1
  30. package/dist/codegen-cli.js.map +1 -1
  31. package/dist/codegen.cjs +1 -1
  32. package/dist/codegen.cjs.map +1 -1
  33. package/dist/codegen.d.cts +37 -124
  34. package/dist/codegen.d.ts +37 -124
  35. package/dist/codegen.js +1 -1
  36. package/dist/codegen.js.map +1 -1
  37. package/dist/core.cjs +1 -1
  38. package/dist/core.cjs.map +1 -1
  39. package/dist/core.d.cts +647 -1100
  40. package/dist/core.d.ts +647 -1100
  41. package/dist/core.js +1 -1
  42. package/dist/core.js.map +1 -1
  43. package/dist/doctor-cli.d.ts +3 -28
  44. package/dist/doctor-cli.js +1 -1
  45. package/dist/doctor-cli.js.map +1 -1
  46. package/dist/doctor.d.ts +16 -32
  47. package/dist/doctor.js +1 -1
  48. package/dist/doctor.js.map +1 -1
  49. package/dist/fragment.cjs +2 -0
  50. package/dist/fragment.cjs.map +1 -0
  51. package/dist/fragment.d.cts +333 -0
  52. package/dist/fragment.d.ts +333 -0
  53. package/dist/fragment.js +2 -0
  54. package/dist/fragment.js.map +1 -0
  55. package/dist/index.cjs +1 -1
  56. package/dist/index.cjs.map +1 -1
  57. package/dist/index.d.cts +885 -1531
  58. package/dist/index.d.ts +885 -1531
  59. package/dist/index.js +1 -1
  60. package/dist/index.js.map +1 -1
  61. package/dist/lexical.cjs +2 -0
  62. package/dist/lexical.cjs.map +1 -0
  63. package/dist/lexical.d.cts +90 -0
  64. package/dist/lexical.d.ts +90 -0
  65. package/dist/lexical.js +2 -0
  66. package/dist/lexical.js.map +1 -0
  67. package/dist/middleware-CEPsY1P7.d.ts +40 -0
  68. package/dist/migrate.d.ts +73 -0
  69. package/dist/migrate.js +2 -0
  70. package/dist/migrate.js.map +1 -0
  71. package/dist/options-CEzzfHJu.d.ts +197 -0
  72. package/dist/payload.cjs +1 -1
  73. package/dist/payload.cjs.map +1 -1
  74. package/dist/payload.d.cts +11 -80
  75. package/dist/payload.d.ts +11 -80
  76. package/dist/payload.js +1 -1
  77. package/dist/payload.js.map +1 -1
  78. package/dist/plugins.cjs +2 -0
  79. package/dist/plugins.cjs.map +1 -0
  80. package/dist/plugins.d.cts +509 -0
  81. package/dist/plugins.d.ts +509 -0
  82. package/dist/plugins.js +2 -0
  83. package/dist/plugins.js.map +1 -0
  84. package/dist/{probe-tMHdim5y.d.ts → probe-CYtvj_Lm.d.ts} +43 -77
  85. package/dist/server.cjs +2 -0
  86. package/dist/server.cjs.map +1 -0
  87. package/dist/server.d.cts +435 -0
  88. package/dist/server.d.ts +435 -0
  89. package/dist/server.js +2 -0
  90. package/dist/server.js.map +1 -0
  91. package/dist/structural.cjs +3 -0
  92. package/dist/structural.cjs.map +1 -0
  93. package/dist/structural.d.cts +243 -0
  94. package/dist/structural.d.ts +243 -0
  95. package/dist/structural.js +3 -0
  96. package/dist/structural.js.map +1 -0
  97. package/package.json +86 -7
  98. package/dist/middleware-DD4Hwomt.d.ts +0 -189
package/dist/index.d.ts CHANGED
@@ -1,67 +1,22 @@
1
- /**
2
- * Single source of truth for the library version.
3
- *
4
- * Both the npm package surface (`src/index.ts`, `src/core-entry.ts`)
5
- * and the inline runtime (`src/core/runtime.ts`, which gets bundled by
6
- * esbuild and embedded into `runtime.generated.ts`) read the version
7
- * from here so they never drift.
8
- *
9
- * Imports `package.json` directly — tsc, tsup, and esbuild all inline
10
- * the value at build time when `resolveJsonModule` is enabled.
11
- *
12
- * @module @/version
13
- */
1
+ /** The package version, read from package.json so the entries and the inline runtime cannot drift. */
14
2
  declare const VERSION: string;
15
3
 
16
- /**
17
- * Connection state + heartbeat.
18
- *
19
- * Tracks whether the runtime is currently receiving updates from a
20
- * trusted origin and fires a callback when the heartbeat times out.
21
- *
22
- * The heartbeat is reset on every valid message and considered dead
23
- * after `timeoutMs` of silence. The host typically responds by
24
- * re-sending the `ready` handshake to attempt reconnection.
25
- *
26
- * @module @core/state
27
- */
4
+ /** Connection status and the heartbeat that decides a disconnect. */
28
5
  type ConnectionStatus = 'disconnected' | 'connecting' | 'connected';
29
6
 
30
7
  /**
31
- * The shape of a live-preview inspection snapshot.
32
- *
33
- * This module is types only. The snapshot is assembled by
34
- * `LivePreviewRuntime.inspect()` as a plain object literal, because the inline
35
- * runtime pays for every byte of indirection and the assembly needs access to
36
- * internal state the runtime does not otherwise expose.
37
- *
38
- * A snapshot is a read of what the runtime already holds. It discloses nothing
39
- * that is not already observable on the page: the trusted origins are inside
40
- * the injected script, the field names are `data-payload-field` attributes in
41
- * the DOM, and the counters are derived from those. It is therefore available
42
- * in production rather than gated to development builds — a preview that
43
- * misbehaves only on the deployed site is exactly the case where the
44
- * information is worth having, and a development-only gate would withhold it
45
- * there.
46
- *
47
- * Nothing here is transmitted anywhere. Reading a snapshot performs no I/O.
48
- *
49
- * @module @core/inspection/types
8
+ * Shape of the `inspect()` snapshot. It only re-reads what the page already
9
+ * shows — origins from the injected script, field names from the DOM,
10
+ * counters derived from both — so it stays available in production, where a
11
+ * misbehaving preview is exactly the case worth diagnosing. Reading one
12
+ * performs no I/O and transmits nothing.
50
13
  */
51
14
 
52
15
  /** Origin trust as the runtime currently sees it. */
53
16
  interface InspectionOrigins {
54
- /**
55
- * Origins the runtime is willing to accept messages from, as configured.
56
- * An empty list means no explicit allow-list was given, which is the
57
- * condition under which referrer and localhost heuristics decide instead.
58
- */
17
+ /** Configured origins; empty means the referrer and localhost heuristics decide instead. */
59
18
  readonly trusted: readonly string[];
60
- /**
61
- * The origin the runtime locked onto after its first accepted update, or
62
- * `undefined` while still unlocked. Once locked, every other origin is
63
- * refused for the rest of the session.
64
- */
19
+ /** The origin locked onto by the first accepted update; every other is then refused. */
65
20
  readonly locked: string | undefined;
66
21
  }
67
22
  /** Protocol versions and the capabilities they enable. */
@@ -74,6 +29,10 @@ interface InspectionProtocol {
74
29
  readonly negotiated: number;
75
30
  /** Capability flags enabled at the negotiated version. */
76
31
  readonly capabilities: readonly string[];
32
+ /** The subset of `capabilities` seen on the wire rather than granted by version. */
33
+ readonly observed: readonly string[];
34
+ /** The Payload profile the observed capabilities imply: `unknown`, `payload-2` or `payload-3`. */
35
+ readonly profile: string;
77
36
  }
78
37
  /** Revision accounting for the update pipeline. */
79
38
  interface InspectionRevisions {
@@ -86,6 +45,19 @@ interface InspectionRevisions {
86
45
  * when the last update is among the abandoned ones.
87
46
  */
88
47
  readonly superseded: number;
48
+ /**
49
+ * Updates whose scheduled writes reached the DOM. What is neither
50
+ * superseded nor completed is in flight or was cancelled; a `superseded`
51
+ * that tracks `accepted` while `completed` stays low is the editor
52
+ * out-typing the pipeline.
53
+ */
54
+ readonly completed: number;
55
+ /**
56
+ * Bindings not scheduled because their value was identical to the one last
57
+ * applied. Cumulative since start; always `0` unless `skipUnchanged` is on.
58
+ * A large number beside a small `accepted` is the optimisation working.
59
+ */
60
+ readonly skippedUnchanged: number;
89
61
  /** Revision currently in flight, or `undefined` when the pipeline is idle. */
90
62
  readonly active: number | undefined;
91
63
  }
@@ -98,22 +70,14 @@ interface InspectionBindings {
98
70
  /** Those field names, sorted. */
99
71
  readonly fieldNames: readonly string[];
100
72
  /**
101
- * Field names that arrived in an update but matched no binding, as far as
102
- * the runtime has observed. This is the list to read when a field refuses
103
- * to update: a name here is a markup problem, a name absent from here and
104
- * from `fieldNames` was never sent.
73
+ * Bound fields some update carried no value for, so the binding kept its old
74
+ * text — why a binding can stay stale while its siblings update. Cumulative.
105
75
  */
76
+ readonly absentFields: readonly string[];
106
77
  /**
107
- * Bound fields that some update since start carried no value for, so the
108
- * binding kept whatever text it already had. Cumulative, like
109
- * `orphanFields`, and the exact opposite of it: a binding with no value
110
- * rather than a value with no binding.
111
- *
112
- * This is why a binding can stay stale while its siblings update. Without
113
- * it the two cases are indistinguishable from the DOM, since neither
114
- * leaves a trace.
78
+ * Field names that arrived but matched no binding — a markup problem.
79
+ * A name in neither this list nor `fieldNames` was never sent. Cumulative.
115
80
  */
116
- readonly absentFields: readonly string[];
117
81
  readonly orphanFields: readonly string[];
118
82
  /** Whether `scopeBindingsByOwner` is active. */
119
83
  readonly ownerScoped: boolean;
@@ -124,11 +88,7 @@ interface InspectionBindings {
124
88
  interface InspectionScheduler {
125
89
  /** Writes buffered for the next flush. */
126
90
  readonly pending: number;
127
- /**
128
- * Writes held back because their element is offscreen and the binding count
129
- * is above `visibilityGateThreshold`. These are applied when the element
130
- * scrolls into view — never, on a page nobody scrolls.
131
- */
91
+ /** Writes held back offscreen above `visibilityGateThreshold`; applied when scrolled into view. */
132
92
  readonly deferred: number;
133
93
  /** The binding count above which the visibility gate starts deferring. */
134
94
  readonly visibilityGateThreshold: number;
@@ -137,23 +97,13 @@ interface InspectionScheduler {
137
97
  /** Stats of the most recent flush, or `undefined` if none has run. */
138
98
  readonly lastFlush: {
139
99
  readonly applied: number;
140
- /**
141
- * Field names the flush applied, in application order.
142
- *
143
- * `applied` is a count, and a count cannot separate "this binding was
144
- * written" from "this binding was never scheduled" — a stale binding
145
- * next to a non-zero count is consistent with both. The names can.
146
- */
100
+ /** Which fields the flush wrote; a count alone cannot say a binding was skipped. */
147
101
  readonly appliedFields: readonly string[];
148
102
  readonly deferred: number;
149
103
  readonly durationMs: number;
150
104
  } | undefined;
151
105
  }
152
- /**
153
- * A point-in-time read of runtime state, for diagnosing a preview that is not
154
- * behaving. Every field is a plain value; the snapshot holds no references
155
- * into runtime internals and does not change after it is returned.
156
- */
106
+ /** A point-in-time read; plain values only, with no references into runtime internals. */
157
107
  interface LivePreviewInspection {
158
108
  /** Package version that produced this snapshot. */
159
109
  readonly version: string;
@@ -168,65 +118,67 @@ interface LivePreviewInspection {
168
118
  readonly scheduler: InspectionScheduler;
169
119
  /** Field types with a registered renderer, sorted. */
170
120
  readonly renderers: readonly string[];
121
+ /** Registered plugins and their live registrations; empty on a bare runtime. */
122
+ readonly plugins: readonly PluginInspection[];
123
+ /** Server-rendered fragment boundaries: whether a handler exists and what happened to renders. */
124
+ readonly fragments: InspectionFragments;
125
+ /** Route refreshes: whether a strategy exists, how many ran, failed, or were stopped by the loop guard. */
126
+ readonly route: InspectionRoute;
127
+ }
128
+ /** The route strategy as `inspect()` reports it. */
129
+ interface InspectionRoute {
130
+ readonly handler: boolean;
131
+ readonly refreshes: number;
132
+ readonly failed: number;
133
+ /** Second refresh requests for one revision, refused with LP0805. */
134
+ readonly loopStopped: number;
135
+ }
136
+ /** The fragment strategy as `inspect()` reports it. */
137
+ interface InspectionFragments {
138
+ /** Whether a fragment handler is configured; without one, boundaries are patched. */
139
+ readonly handler: boolean;
140
+ readonly inFlight: number;
141
+ readonly rendered: number;
142
+ readonly failed: number;
143
+ readonly superseded: number;
144
+ }
145
+ /** One plugin as `inspect().plugins` reports it. */
146
+ interface PluginInspection {
147
+ readonly name: string;
148
+ readonly version: string | undefined;
149
+ /** `initializing` while `init` runs, `active` afterwards, `tearing-down` while `destroy` runs. */
150
+ readonly state: 'initializing' | 'active' | 'tearing-down';
151
+ /** Live registrations owned by the plugin's scope, by kind. */
152
+ readonly registrations: {
153
+ readonly transforms: number;
154
+ readonly renderers: number;
155
+ readonly subscriptions: number;
156
+ readonly cleanups: number;
157
+ };
171
158
  }
172
159
 
173
160
  /**
174
- * Payload Live Preview message protocol.
175
- *
176
- * Mirrors the messages emitted by Payload CMS through
177
- * `window.postMessage`, verified against the shipped admin code
178
- * (`@payloadcms/ui` → `elements/LivePreview/Window`):
179
- *
180
- * - Payload **3.x** sends `{ type, collectionSlug, globalSlug, data,
181
- * locale, externallyUpdatedRelationship }` on every form edit —
182
- * `data` holds **raw form values** (relationships as bare IDs) and
183
- * there is **no** `fieldSchemaJSON`.
184
- * - Payload **2.x** additionally sent `fieldSchemaJSON` with the
185
- * first update (client-side merge era).
186
- * - `previewToken` and `protocolVersion` are extensions of THIS
187
- * library for custom admin senders; stock Payload never sends
188
- * them and ignores them in our `ready` handshake.
189
- *
190
- * @module @/types/payload-protocol
191
- */
192
- /**
193
- * A `payload-live-preview` data update.
194
- *
195
- * `data` is the serialized document state. `fieldSchemaJSON`, when
196
- * present (Payload 2.x or custom senders), allows the schema-driven
197
- * engine to resolve field types without any DOM annotations.
161
+ * Payload's live-preview `postMessage` protocol as the shipped admin sends it:
162
+ * raw form values on every edit, no schema. `previewToken` and
163
+ * `protocolVersion` are extensions of this library for custom senders.
198
164
  */
199
165
  interface PayloadLivePreviewMessage {
200
166
  readonly type: 'payload-live-preview';
201
167
  readonly data?: Record<string, unknown>;
168
+ /** Sent by Payload 2.x and custom senders only. */
202
169
  readonly fieldSchemaJSON?: readonly PayloadFieldSchema[];
203
170
  readonly globalSlug?: string;
204
171
  readonly collectionSlug?: string;
205
172
  readonly locale?: string;
206
173
  readonly ready?: boolean;
207
- /**
208
- * Payload 3.x: most recent relationship-document event (create/update
209
- * in a drawer), or `null`. The official client ignores it too — it is
210
- * modeled here for completeness and consumer event access.
211
- */
174
+ /** Payload 3.x: the most recent relationship-document event (a drawer save), or `null`. */
212
175
  readonly externallyUpdatedRelationship?: PayloadDocumentEventDetail | null;
213
- /**
214
- * Optional preview JWT. ⚠️ Library extension — stock Payload never
215
- * sends one. When `RuntimeOptions.validateToken` is set, the runtime
216
- * requires every data update to carry a token that passes the
217
- * validator; messages without a valid token are dropped.
218
- */
176
+ /** Library extension: the token `validateToken` checks. Stock Payload never sends one. */
219
177
  readonly previewToken?: string;
220
- /**
221
- * Highest protocol version the sender supports. ⚠️ Library extension
222
- * for custom senders — stock Payload sends no version. When absent,
223
- * the receiver assumes v1.
224
- */
178
+ /** Library extension: the highest protocol version the sender supports; absent means v1. */
225
179
  readonly protocolVersion?: number;
226
180
  }
227
- /**
228
- * Payload's `DocumentEvent` shape (admin `providers/DocumentEvents`).
229
- */
181
+ /** Payload's `DocumentEvent` shape (admin `providers/DocumentEvents`). */
230
182
  interface PayloadDocumentEventDetail {
231
183
  readonly entitySlug: string;
232
184
  readonly operation?: 'create' | 'update';
@@ -234,9 +186,7 @@ interface PayloadDocumentEventDetail {
234
186
  readonly updatedAt?: string;
235
187
  readonly [extra: string]: unknown;
236
188
  }
237
- /**
238
- * Parsed, validated message payload exposed to consumers via events.
239
- */
189
+ /** A validated update as events expose it. */
240
190
  interface PayloadLivePreviewData {
241
191
  readonly fields: Record<string, unknown>;
242
192
  readonly schema?: readonly PayloadFieldSchema[];
@@ -244,13 +194,7 @@ interface PayloadLivePreviewData {
244
194
  readonly collectionSlug?: string;
245
195
  readonly locale?: string;
246
196
  }
247
- /**
248
- * The subset of Payload's field-schema description we rely on.
249
- *
250
- * Payload emits the entire field configuration tree but we only need
251
- * the discriminator `type` and the nesting via `fields`/`blocks`.
252
- * Extra properties are preserved opaquely.
253
- */
197
+ /** The subset of Payload's field schema the runtime reads; extra properties are kept opaquely. */
254
198
  interface PayloadFieldSchema {
255
199
  readonly name: string;
256
200
  readonly type: PayloadFieldType;
@@ -271,54 +215,19 @@ interface PayloadBlockSchema {
271
215
  readonly fields: readonly PayloadFieldSchema[];
272
216
  readonly [extra: string]: unknown;
273
217
  }
274
- /**
275
- * The Payload core field types this library recognises (20 as of
276
- * Payload 3.x). `tabs` is treated as a structural container; `group`
277
- * flattens nested fields.
278
- */
218
+ /** Payload's core field types. `tabs` is a structural container; `group` flattens. */
279
219
  type PayloadFieldType = 'text' | 'textarea' | 'richText' | 'email' | 'number' | 'checkbox' | 'date' | 'select' | 'radio' | 'array' | 'blocks' | 'group' | 'tabs' | 'row' | 'collapsible' | 'relationship' | 'upload' | 'point' | 'json' | 'code' | 'ui';
280
- /**
281
- * Payload's `admin.condition` is a function in source, but it cannot
282
- * be transferred over postMessage. The schema arrives with this slot
283
- * either absent or, in future Payload versions, replaced by a
284
- * serialized expression. We model the slot but do not consume it yet.
285
- */
220
+ /** `admin.condition` is a function in source and cannot cross `postMessage`; modelled, not consumed. */
286
221
  type PayloadFieldCondition = (data: unknown, siblingData: unknown) => boolean;
287
222
 
288
223
  /**
289
- * Stable identifiers for everything the runtime reports.
290
- *
291
- * Prose gets reworded; a code does not. A consumer filtering logs, an alert
292
- * rule, or a bug report referring to `LP0301` keeps meaning the same thing
293
- * after the sentence around it is rewritten, and a code is greppable in a way
294
- * that a sentence fragment is not.
295
- *
296
- * Codes are grouped by the question they answer:
297
- *
298
- * - `LP01xx` — configuration and origin trust: the runtime is running, but it
299
- * was set up in a way that will bite.
300
- * - `LP02xx` — bindings and markup: an update had nowhere to land.
301
- * - `LP03xx` — updates and scheduling: the update landed somewhere other than
302
- * the DOM, or not yet.
303
- * - `LP04xx` — rendering: a value reached an element and was refused.
304
- * - `LP05xx` — protocol and messages: something arrived that was not accepted.
305
- * - `LP06xx` — consumer callbacks: code the consumer supplied threw.
306
- * - `LP07xx` — audit findings from `pll doctor`, which reports on a served
307
- * response rather than on a running runtime.
224
+ * Every diagnostic code the runtime and tools emit, so a consumer can branch
225
+ * on `e.code` instead of matching message text. A code is never reused for a
226
+ * different meaning; a retired one stays reserved.
308
227
  *
309
- * A code is part of the public contract. Codes are never reused for a
310
- * different meaning and never renumbered; a retired code stays retired.
311
- * `LP0604` is unassigned: a throwing token validator is deliberately treated
312
- * as a rejection and reported as `LP0502`, so there is nothing distinct to
313
- * report yet. The number stays reserved rather than being handed to something
314
- * else.
315
- *
316
- * The runtime writes these as string literals at each reporting site rather
317
- * than reading them from this record, so the inline build pays for the six
318
- * characters it prints and nothing more. This record exists so consumers can
319
- * refer to a code by name instead of copying a literal.
320
- *
321
- * @module @core/diagnostic-codes
228
+ * Browser-side reporting sites write the literal (`code: 'LP0603'`) rather than
229
+ * reading it from this record: the frozen table would then ship in the inline
230
+ * runtime for the sake of six characters. Server-side tools may import it.
322
231
  */
323
232
  /**
324
233
  * Every diagnostic code the runtime can report, by name.
@@ -334,6 +243,8 @@ declare const DIAGNOSTIC_CODES: Readonly<{
334
243
  readonly NoTrustedOrigin: "LP0101";
335
244
  /** Origin trust rests on `document.referrer`, so any framing site is trusted. */
336
245
  readonly ReferrerOnlyTrust: "LP0102";
246
+ /** A plugin declares a runtime range this runtime does not satisfy; it was refused. */
247
+ readonly PluginIncompatible: "LP0103";
337
248
  /** An update named a field with no binding anchor on the page. */
338
249
  readonly OrphanField: "LP0201";
339
250
  /** Owner scoping is on and the update names no document it could belong to. */
@@ -346,6 +257,14 @@ declare const DIAGNOSTIC_CODES: Readonly<{
346
257
  readonly TextTargetHasChildren: "LP0402";
347
258
  /** A structural container has no array template, so the update was skipped. */
348
259
  readonly MissingArrayTemplate: "LP0403";
260
+ /** A structural item has no `id`; it pairs positionally, so an insert re-renders every row after it. */
261
+ readonly StructuralItemUnkeyed: "LP0404";
262
+ /** Two structural items share a key; later ones pair positionally. */
263
+ readonly StructuralDuplicateKey: "LP0405";
264
+ /** Every structural key changed while the length did not; the source generates keys per message. */
265
+ readonly StructuralUnstableKeys: "LP0406";
266
+ /** A binding asks for a delivery strategy this release does not have; it is left unchanged. */
267
+ readonly UnsupportedStrategy: "LP0407";
349
268
  /** A message was rejected before it reached the update pipeline. */
350
269
  readonly MessageRejected: "LP0501";
351
270
  /** A preview token was rejected. */
@@ -376,251 +295,245 @@ declare const DIAGNOSTIC_CODES: Readonly<{
376
295
  readonly AuditNoBindings: "LP0707";
377
296
  /** The URL did not return an HTML page, so nothing else can be judged. */
378
297
  readonly AuditNotAPage: "LP0708";
298
+ /** A fragment request failed (network, timeout, server error); the boundary was patched instead. */
299
+ readonly FragmentRequestFailed: "LP0801";
300
+ /** The fragment response had the wrong content type or shape; the boundary was patched instead. */
301
+ readonly FragmentResponseInvalid: "LP0802";
302
+ /** The fragment endpoint refused the preview (not authorized); the boundary was patched instead. */
303
+ readonly FragmentUnauthorized: "LP0803";
304
+ /** A fragment response arrived for a revision that was already superseded and was discarded. */
305
+ readonly FragmentSuperseded: "LP0804";
306
+ /** A route refresh was requested again for the same revision; the loop guard stopped it. */
307
+ readonly RouteRefreshLoop: "LP0805";
308
+ /** A boundary asks for the fragment strategy but no fragment handler is configured; it is patched. */
309
+ readonly FragmentStrategyUnavailable: "LP0806";
310
+ /** A readiness row is not yet at its 2.0 value; `pll doctor --v2` reports it. */
311
+ readonly V2ReadinessGap: "LP0709";
312
+ /** The preview runtime is served to anonymous visitors, not only inside the admin frame. */
313
+ readonly RuntimeOnPublicPage: "LP0710";
379
314
  }>;
380
315
  /** A diagnostic code, as it appears in log output and on the `error` event. */
381
316
  type DiagnosticCode = (typeof DIAGNOSTIC_CODES)[keyof typeof DIAGNOSTIC_CODES];
382
317
 
383
318
  /**
384
- * Public event payload shapes for the live preview lifecycle.
385
- *
386
- * The map keys form the union of valid event names. Adding a new event
387
- * is a two-line change: add the entry here and dispatch from the
388
- * relevant module. Consumers get autocompletion and exhaustive checks
389
- * for free because the `EventEmitter` is generic over this map.
390
- *
391
- * @module @events/types
319
+ * How a bound region is brought up to date: the runtime patches it, a server
320
+ * renders a `data-payload-fragment` boundary from the unsaved form state, or
321
+ * the whole route refreshes. Core carries only the seam — which boundaries a
322
+ * strategy owns and what it may do — so a patch-only page pays for nothing
323
+ * more; the orchestration lives in `payload-live-preview/fragment`.
392
324
  */
393
325
 
326
+ /** What produced an update: the runtime's DOM patching, a server-rendered fragment, or a route refresh. */
327
+ type UpdateSource = 'patch' | 'fragment' | 'route';
328
+ /** One revision, as a strategy sees it, with the runtime capabilities it may use. */
329
+ interface FragmentContext {
330
+ readonly root: ParentNode;
331
+ readonly revision: number;
332
+ readonly receivedAt: number;
333
+ readonly fields: Readonly<Record<string, unknown>>;
334
+ readonly locale: string | undefined;
335
+ readonly collectionSlug: string | undefined;
336
+ readonly globalSlug: string | undefined;
337
+ /** Aborted when a newer revision arrives or the runtime stops. */
338
+ readonly signal: AbortSignal;
339
+ /** Whether this revision is still the current one. */
340
+ readonly isCurrent: () => boolean;
341
+ /** Morph server-rendered HTML into a boundary (Trusted Types and the keyed morph apply). */
342
+ readonly morph: (boundary: Element, html: string) => void;
343
+ /** Patch the boundary's own bindings from this revision — the deterministic fallback. */
344
+ readonly patch: (boundary: Element) => void;
345
+ /** Debug log through the runtime's logger, with the diagnostic code. */
346
+ readonly log: (code: DiagnosticCode, detail: string) => void;
347
+ /** Report a boundary rendered. */
348
+ readonly rendered: (boundary: Element, id: string, key: string | undefined) => void;
349
+ /** Report a boundary failed and patched instead; emits the `error` and `fragmentRender` events. */
350
+ readonly failed: (boundary: Element, id: string, key: string | undefined, code: DiagnosticCode, reason: string) => void;
351
+ }
352
+ interface FragmentReport {
353
+ readonly rendered: number;
354
+ readonly failed: number;
355
+ readonly superseded: number;
356
+ }
357
+ /** A fragment strategy: which boundaries it owns for a revision, and how it renders them. */
358
+ interface FragmentStrategy {
359
+ /** The boundaries this update re-renders; their inner bindings are left to the strategy. */
360
+ readonly plan: (root: ParentNode, changedFields: ReadonlySet<string>) => readonly Element[];
361
+ /** Render the planned boundaries; resolves once every one settled. */
362
+ readonly render: (context: FragmentContext, boundaries: readonly Element[]) => Promise<FragmentReport>;
363
+ }
364
+ /** One revision, as the route strategy sees it. */
365
+ interface RouteContext {
366
+ readonly revision: number;
367
+ readonly receivedAt: number;
368
+ /** Aborted when a newer revision arrives or the runtime stops. */
369
+ readonly signal: AbortSignal;
370
+ readonly isCurrent: () => boolean;
371
+ readonly log: (code: DiagnosticCode, detail: string) => void;
372
+ }
373
+ /**
374
+ * Whether a revision needs the whole route re-rendered, and how. After a
375
+ * refresh the runtime rescans and re-applies the revision, so the unsaved
376
+ * state lands on the fresh markup.
377
+ */
378
+ interface RouteStrategy {
379
+ readonly plan: (root: ParentNode, changedFields: ReadonlySet<string>) => boolean;
380
+ readonly refresh: (context: RouteContext) => Promise<'refreshed' | 'failed' | 'superseded'>;
381
+ }
382
+ interface StrategyHandlers {
383
+ readonly fragment?: FragmentStrategy;
384
+ readonly route?: RouteStrategy;
385
+ }
386
+
387
+ /** Public event payloads. The map keys are the valid event names; `EventEmitter` is generic over it. */
388
+
394
389
  interface LivePreviewEventMap {
395
- /**
396
- * Fired once per startup attempt after observers, cache, and message listening
397
- * are active. A later startup failure may roll that attempt back; retrying then
398
- * emits a new `init` event.
399
- */
390
+ /** Once per startup attempt, after observers, cache and message listening are active. A rolled-back attempt emits it again on retry. */
400
391
  readonly init: {
401
392
  readonly timestamp: number;
402
393
  };
403
- /** Fired for the first accepted data-bearing update from a trusted origin. */
394
+ /** The first accepted data-bearing update from a trusted origin. */
404
395
  readonly connect: {
405
396
  readonly origin: string;
406
397
  readonly timestamp: number;
407
398
  };
408
- /**
409
- * Fired when the heartbeat times out or the runtime is destroyed. The
410
- * `'unload'` reason remains in the public union for 1.x producer compatibility;
411
- * the built-in runtime does not currently emit it.
412
- */
399
+ /** The heartbeat timed out, the runtime was destroyed, or the page was suspended for navigation (`'unload'`). */
413
400
  readonly disconnect: {
414
401
  readonly reason: 'timeout' | 'destroy' | 'unload';
415
402
  readonly timestamp: number;
416
403
  };
417
- /**
418
- * Fired before the DOM is mutated for an incoming update.
419
- *
420
- * Handlers may call `cancel()` before their synchronous or asynchronous
421
- * work settles to skip exactly this revision.
422
- */
404
+ /** Before the DOM is mutated for an update; `cancel()` skips exactly this revision. */
423
405
  readonly beforeUpdate: {
424
406
  readonly data: PayloadLivePreviewData;
425
- /** Runtime-populated ordering metadata; optional for 1.x producer compatibility. */
426
407
  readonly revision?: number;
408
+ /** When the runtime accepted the message, Unix milliseconds. */
409
+ readonly receivedAt?: number;
410
+ /** The strategy that produced the update. */
411
+ readonly source?: UpdateSource;
427
412
  readonly cancel: () => void;
428
413
  };
429
- /**
430
- * Fired after each actual, current DOM application batch. Visibility replay
431
- * may produce another event for the same revision; cancelled, obsolete, and
432
- * deferred-only batches produce none.
433
- */
414
+ /** After each applied, still-current batch. Visibility replay may emit another for the same revision; cancelled, obsolete and deferred-only batches emit none. */
434
415
  readonly afterUpdate: {
435
416
  readonly data: PayloadLivePreviewData;
436
417
  readonly updatedCount: number;
437
418
  readonly durationMs: number;
438
- /** Runtime-populated ordering metadata; optional for 1.x producer compatibility. */
439
419
  readonly revision?: number;
420
+ readonly receivedAt?: number;
421
+ readonly source?: UpdateSource;
440
422
  };
441
- /** Fired for each successful, still-current element write. */
423
+ /** Each successful, still-current element write. */
442
424
  readonly elementUpdate: {
443
425
  readonly element: Element;
444
426
  readonly fieldName: string;
445
427
  readonly previousValue: unknown;
446
428
  readonly nextValue: unknown;
447
- /** Runtime-populated ordering metadata; optional for 1.x producer compatibility. */
448
429
  readonly revision?: number;
430
+ readonly receivedAt?: number;
431
+ readonly source?: UpdateSource;
449
432
  };
450
- /** Fired whenever the element cache is rebuilt (initial scan or MutationObserver-triggered). */
433
+ /** The element cache was rebuilt (initial scan or mutation). */
451
434
  readonly cacheRefresh: {
452
435
  readonly elementCount: number;
453
436
  readonly fieldCount: number;
454
437
  readonly durationMs: number;
455
438
  };
456
- /** Fired when a `payload-document-event` message arrives (document save). */
439
+ /** Once per fragment boundary and revision: rendered by the server, or failed (with the LP08xx code) and patched instead. */
440
+ readonly fragmentRender: {
441
+ readonly element: Element;
442
+ readonly id: string;
443
+ readonly key: string | undefined;
444
+ readonly status: 'rendered' | 'failed';
445
+ readonly code?: DiagnosticCode;
446
+ readonly revision: number;
447
+ readonly receivedAt: number;
448
+ };
449
+ /** A `payload-document-event` message arrived (document save). */
457
450
  readonly documentSave: {
458
451
  readonly timestamp: number;
459
452
  };
460
- /**
461
- * Fired on errors that the runtime caught but cannot fully recover from.
462
- *
463
- * The `context` string identifies where the error originated. `code` says
464
- * the same thing in a form that survives rewording — branch on `code`, read
465
- * `context` when you want the human-readable origin.
466
- */
453
+ /** A data update carried `externallyUpdatedRelationship`: a related document changed in an admin drawer, so the update re-renders unconditionally. */
454
+ readonly relationshipUpdate: {
455
+ readonly detail: PayloadDocumentEventDetail;
456
+ readonly timestamp: number;
457
+ };
458
+ /** An error the runtime caught but cannot fully recover from. Branch on `code`; `context` is the human-readable origin. */
467
459
  readonly error: {
468
460
  readonly error: Error;
469
461
  readonly context: string;
470
462
  readonly code: DiagnosticCode;
471
463
  };
472
- /**
473
- * Fired during destroy after browser observers and the message listener are
474
- * removed. Event/plugin listeners remain available for this notification and
475
- * are released by the owning client afterwards.
476
- */
464
+ /** During destroy, after observers and the message listener are removed; listeners are released by the owning client afterwards. */
477
465
  readonly destroy: {
478
466
  readonly timestamp: number;
479
467
  };
480
468
  }
481
- /**
482
- * Handler signature: receives the payload and may return a Promise.
483
- *
484
- * Promise-returning handlers are awaited in registration order — this
485
- * is important for `beforeUpdate.cancel()` semantics.
486
- */
469
+ /** Handlers may return a promise; they are awaited in registration order, which `beforeUpdate.cancel()` relies on. */
487
470
  type EventHandler<TPayload> = (payload: TPayload) => void | Promise<void>;
488
- /** Subscription removal function returned by `on()`/`once()`. */
489
471
  type Unsubscribe = () => void;
490
472
 
491
473
  /**
492
- * Per-instance, type-safe event emitter.
493
- *
494
- * Replaces the legacy module-level singleton. Each `LivePreviewClient`
495
- * instantiates its own emitter so that calling `destroy()` on one
496
- * instance cannot clobber listeners attached to another.
497
- *
498
- * Design notes:
499
- *
500
- * - Generic over an event map type. The default `LivePreviewEventMap`
501
- * captures every event this library emits, but the implementation
502
- * is reusable for any map.
503
- * - Handlers may be `async`. They are awaited sequentially in
504
- * registration order. This is intentional: `beforeUpdate.cancel()`
505
- * and similar synchronization rely on stable ordering.
506
- * - Errors thrown by one handler are logged via `console.error` and
507
- * do not prevent later handlers from running.
508
- * - `once()` semantics: registered into a separate map; the entire
509
- * snapshot for an event is claimed immediately before its dispatch,
510
- * and remains consumed even if individual handlers throw.
511
- *
512
- * @module @events/emitter
474
+ * Per-instance typed emitter: handlers run sequentially in registration order,
475
+ * may be async, and a throwing one is logged and isolated. See ADR 0002.
513
476
  */
514
477
 
515
478
  declare class EventEmitter<TMap extends object = LivePreviewEventMap> {
516
479
  #private;
517
- /**
518
- * Subscribe `handler` to `event`. Returns an unsubscribe function;
519
- * call it to remove the handler. The handler is invoked for every
520
- * subsequent emit until unsubscribed.
521
- */
522
480
  on<E extends keyof TMap>(event: E, handler: EventHandler<TMap[E]>): Unsubscribe;
523
- /**
524
- * Subscribe `handler` for a single emit. After the first dispatch,
525
- * the handler is removed automatically.
526
- */
527
481
  once<E extends keyof TMap>(event: E, handler: EventHandler<TMap[E]>): Unsubscribe;
528
- /**
529
- * Remove `handler` from `event`. Works for both `on`- and
530
- * `once`-registered handlers. No-op when the handler isn't registered.
531
- */
532
482
  off<E extends keyof TMap>(event: E, handler: EventHandler<TMap[E]>): void;
533
- /**
534
- * Dispatch `payload` to every handler registered for `event`.
535
- *
536
- * Handlers run sequentially in registration order — regular ones
537
- * first, then `once` handlers. Async handlers are awaited. Synchronous
538
- * exceptions are caught, logged, and isolated to the offending handler.
539
- */
483
+ /** Dispatch to every handler: regular ones first, then `once` handlers. */
540
484
  emit<E extends keyof TMap>(event: E, payload: TMap[E]): Promise<void>;
541
485
  /**
542
- * Lifecycle-only guarded dispatch. The predicate is checked before and after
543
- * every handler so an async boundary cannot let an obsolete transaction run
544
- * later callbacks. `emit()` intentionally keeps its established behaviour.
545
- *
546
- * Once-handlers are claimed only when dispatch reaches the once phase. Once
547
- * claimed, the whole snapshot remains consumed even if the predicate changes
548
- * while one of its handlers is awaiting, matching normal once semantics.
549
- *
486
+ * Dispatch while `shouldContinue()` holds — checked around every handler, so
487
+ * an obsolete transaction cannot resume across an await.
550
488
  * @internal
551
- * @returns `true` when the complete handler sequence ran while eligible.
552
489
  */
553
490
  emitWhile<E extends keyof TMap>(event: E, payload: TMap[E], shouldContinue: () => boolean): Promise<boolean>;
554
- /**
555
- * Number of handlers currently registered for `event`.
556
- * Sum of `on` and `once` registrations.
557
- */
491
+ /** Registered handlers for `event`, `on` and `once` together. */
558
492
  listenerCount(event: keyof TMap): number;
559
- /**
560
- * Remove every handler. Pass an event name to limit the removal to
561
- * one event; omit it to clear every event on this emitter.
562
- *
563
- * Crucially this only affects the current instance — there is no
564
- * shared global state.
565
- */
493
+ /** Remove every handler, or only those of `event`. */
566
494
  removeAllListeners(event?: keyof TMap): void;
567
- /**
568
- * Snapshot of all event names that currently have at least one
569
- * registered handler. Useful for debugging.
570
- */
495
+ /** Event names with at least one handler. */
571
496
  eventNames(): (keyof TMap)[];
572
497
  }
573
498
 
574
- /**
575
- * Core type definitions shared across all runtime primitives.
576
- *
577
- * @module @core/types
578
- */
499
+ /** Types shared by the runtime primitives. */
579
500
 
580
- /**
581
- * Field types recognized by the live preview renderer.
582
- *
583
- * Extends Payload's field set with `html` and `url` which are not
584
- * Payload field types per se but useful DOM-binding categories.
585
- */
501
+ /** Payload's field set plus the DOM-binding categories `html`, `url`, `image` and `structural-array`. */
586
502
  type FieldType = PayloadFieldType | 'html' | 'url' | 'image' | 'structural-array';
503
+ /** A namespaced project renderer key, so a typo of a built-in type can never become one. */
504
+ type CustomRendererKey = `${string}:${string}`;
505
+ /** What a renderer may be registered under and an element may ask for. */
506
+ type RendererKey = FieldType | CustomRendererKey;
587
507
  /**
588
- * A DOM element that has been registered as a live preview binding.
589
- *
590
- * Stored in the element cache indexed by `fieldName`. Multiple cached
591
- * entries can share a field name when the same field is rendered in
592
- * several places (e.g., a title shown in both the header and the
593
- * page body).
508
+ * A project rich-text renderer, shared by SSR and preview. Its output goes
509
+ * through the sanitizer; call the same `sanitizeHtml()` server-side for byte equality.
594
510
  */
511
+ type RichTextRenderer = (value: unknown, context: {
512
+ readonly fieldName: string;
513
+ readonly element: Element;
514
+ readonly locale: string | undefined;
515
+ }) => string;
516
+ /** One registered binding. Several may share a field name when it renders in several places. */
595
517
  interface CachedElement {
596
518
  /** The bound DOM element. */
597
519
  readonly element: Element;
598
520
  /** Payload field path, e.g., `title` or `hero.subtitle`. */
599
521
  readonly fieldName: string;
600
- /**
601
- * Resolved field type, used to dispatch to the right renderer.
602
- *
603
- * The cache resolves this from DOM attributes (`data-payload-type`,
604
- * tag-based heuristics) at build time. When `explicitFieldType` is
605
- * `false` the lifecycle is allowed to override this with the type
606
- * learned from the schema once `fieldSchemaJSON` arrives.
607
- */
608
- readonly fieldType: FieldType;
609
- /**
610
- * `true` when the field type was set by an explicit
611
- * `data-payload-type` attribute (consumer trumps schema), `false`
612
- * when it was inferred via tag-based heuristics (schema may
613
- * override later). Optional so synthetic bindings (tests / programmatic
614
- * `cache.add()` callers) need not supply it; the cache always
615
- * populates it for DOM-derived bindings.
616
- */
522
+ /** Resolved from attributes and tag heuristics; the schema may override it unless explicit. */
523
+ readonly fieldType: RendererKey;
524
+ /** Whether `data-payload-type` set the type, in which case the schema must not override it. */
617
525
  readonly explicitFieldType?: boolean;
618
- /**
619
- * Optional target attribute: the value is written to this attribute
620
- * instead of the element's content (from `data-payload-attribute`,
621
- * or `bind(field, { attribute })`). Writes are policed — event
622
- * handlers, `style`, and unsafe URLs are refused.
623
- */
526
+ /** Fields this binding depends on (`data-payload-depends`); a change in any re-applies it under `skipUnchanged`. */
527
+ readonly dependsOn?: readonly string[];
528
+ /** Raw `data-payload-strategy` value. */
529
+ readonly strategy?: string;
530
+ /** How the runtime brings this binding up to date; `'unknown'` for an unrecognised strategy. Resolved by the cache. */
531
+ readonly strategyKind?: UpdateSource | 'unknown';
532
+ /** Nearest enclosing `data-payload-fragment` boundary. Resolved by the cache. */
533
+ readonly fragmentBoundary?: Element;
534
+ /** `data-payload-boundary`: an anchor that hides itself while its field is empty. */
535
+ readonly boundary?: boolean;
536
+ /** Write the value into this attribute instead of the content; policed writes. */
624
537
  readonly targetAttribute?: string;
625
538
  /** Optional sibling-field path bound to the element's `href` attribute. */
626
539
  readonly hrefField?: string;
@@ -634,21 +547,10 @@ interface CachedElement {
634
547
  readonly arraySeparator?: string;
635
548
  /** Optional locale code locked onto this element (overrides the global locale). */
636
549
  readonly locale?: string;
637
- /**
638
- * Optional document this binding belongs to, resolved from the nearest
639
- * `data-payload-owner` ancestor (the element itself included).
640
- *
641
- * Only consulted while owner scoping is enabled. Without it a field name is
642
- * the sole identity, so every document rendered on the page competes for the
643
- * same name.
644
- */
550
+ /** The document this binding belongs to, from the nearest `data-payload-owner`. */
645
551
  readonly owner?: string;
646
552
  }
647
- /**
648
- * Context passed to every field renderer. Provides access to the
649
- * entire field tree so renderers can resolve sibling fields (e.g.,
650
- * an `<a>` that pulls `href` from a different field).
651
- */
553
+ /** What a renderer is given; `allFields` lets it resolve a sibling field. */
652
554
  interface RenderContext {
653
555
  /** Full field map for the current update — used for sibling lookups. */
654
556
  readonly allFields: Record<string, unknown>;
@@ -656,315 +558,221 @@ interface RenderContext {
656
558
  readonly locale: string | undefined;
657
559
  /** Optional schema descriptor for the field, when available. */
658
560
  readonly schema: PayloadFieldSchema | undefined;
561
+ /** The project's rich-text renderer, when the client was configured with one. */
562
+ readonly renderRichText?: RichTextRenderer;
659
563
  }
660
564
  /**
661
- * Contract that every field renderer implements.
662
- *
663
- * Renderers are pure DOM-write functions: they receive the cached
664
- * binding plus the new value and apply it to the element. They must
665
- * not throw — failures should be silenced and logged so that one bad
666
- * field cannot stop an entire update. The public renderer result remains
667
- * `void` for 1.x compatibility. Built-in renderers may use an internal exact
668
- * `false` sentinel when they deliberately perform no DOM write; callers must
669
- * not rely on that implementation detail.
565
+ * A renderer writes one value into its element. It must not throw: one bad
566
+ * field may not stop an update. The result stays `void` — the internal
567
+ * no-write sentinel is not part of the contract.
670
568
  */
671
569
  interface FieldRenderer {
672
- readonly name: FieldType;
570
+ readonly name: RendererKey;
673
571
  render(target: CachedElement, value: unknown, context: RenderContext): void;
674
572
  }
675
573
 
676
574
  /**
677
- * Plugin system types.
678
- *
679
- * Each plugin participates in the live-preview lifecycle through a
680
- * scoped, per-instance context. Plugins can register custom field
681
- * renderers, transform incoming values, and subscribe to events —
682
- * the same primitives the built-in renderers use.
683
- *
684
- * The system is intentionally minimal. Anything that can be expressed
685
- * with an event subscription belongs in `events`, not `plugins`.
686
- *
687
- * @module @plugins/types
575
+ * Plugin compatibility claims. The range grammar is the subset a plugin
576
+ * author writes (`*`, `1.2.3`, `^`, `~`, `>=`, `>`, `<`, `<=`, space-joined
577
+ * conjunctions, `||` alternatives); prerelease tags are ignored.
688
578
  */
579
+ interface PluginCompatibility {
580
+ /** A semver range the runtime's `VERSION` must satisfy, e.g. `>=2.0.0`. */
581
+ readonly runtime?: string;
582
+ /** The highest postMessage protocol version the plugin understands. */
583
+ readonly protocol?: number;
584
+ }
585
+
586
+ /** Plugin contract: a scoped, per-instance context for renderers, transforms and events. */
689
587
 
690
588
  /** Idempotent handle that releases one plugin-owned resource. */
691
589
  type PluginDisposer = () => void;
692
- /**
693
- * Event subscriptions scoped to one plugin registration.
694
- *
695
- * This remains nominally compatible with the pre-1.0.4 `EventEmitter` context
696
- * type. Its methods are registration-scoped: even bulk removal affects only
697
- * this plugin's subscriptions, never a consumer's or another plugin's
698
- * listeners.
699
- */
700
- interface PluginEvents extends EventEmitter {
590
+ /** The client's event surface scoped to one registration: bulk removal and introspection see only this plugin's listeners. */
591
+ interface PluginEvents {
701
592
  readonly on: <E extends keyof LivePreviewEventMap>(event: E, handler: EventHandler<LivePreviewEventMap[E]>) => Unsubscribe;
702
593
  readonly once: <E extends keyof LivePreviewEventMap>(event: E, handler: EventHandler<LivePreviewEventMap[E]>) => Unsubscribe;
703
594
  readonly off: <E extends keyof LivePreviewEventMap>(event: E, handler: EventHandler<LivePreviewEventMap[E]>) => void;
704
595
  /** Emit through the owning client's event channel. */
705
596
  readonly emit: <E extends keyof LivePreviewEventMap>(event: E, payload: LivePreviewEventMap[E]) => Promise<void>;
706
- /**
707
- * Guarded emit through the owning client channel. Dispatch stops when the
708
- * caller predicate becomes false or this plugin registration is removed.
709
- */
597
+ /** Guarded emit; dispatch stops when the predicate turns false or this registration is removed. */
710
598
  readonly emitWhile: <E extends keyof LivePreviewEventMap>(event: E, payload: LivePreviewEventMap[E], shouldContinue: () => boolean) => Promise<boolean>;
711
- /** Number of listeners owned by this plugin registration. */
712
599
  readonly listenerCount: (event: keyof LivePreviewEventMap) => number;
713
- /** Remove only listeners owned by this plugin registration. */
714
600
  readonly removeAllListeners: (event?: keyof LivePreviewEventMap) => void;
715
- /** Event names for which this plugin registration owns listeners. */
716
601
  readonly eventNames: () => (keyof LivePreviewEventMap)[];
717
602
  }
718
603
  /**
719
- * Synchronous function that transforms a merged field value while a revision's
720
- * per-binding scheduler entry is prepared. Transforms run in registration
721
- * order and each receives the preceding transform's result. The final value is
722
- * frozen into that entry before later renderer or attribute dispatch;
723
- * `allFields` remains the merged, untransformed update snapshot.
724
- *
725
- * Returning a different shape than the input is allowed, but returning a
726
- * Promise or other thenable is a contract error. If a transform throws or
727
- * returns a thenable, the chain stops, the runtime emits an `error` event, and
728
- * dispatch falls back to the original merged value. That fallback still passes
729
- * through the normal renderer and attribute security controls.
604
+ * Synchronous value transform run in registration order; returning a thenable
605
+ * is a contract error (`LP0602`) that dispatches the untransformed value.
730
606
  */
731
607
  type FieldTransform = (value: unknown, context: {
732
608
  readonly fieldName: string;
733
609
  readonly element: Element;
734
610
  readonly allFields: Record<string, unknown>;
735
611
  }) => unknown;
736
- /**
737
- * Per-instance plugin context. Replaces the legacy singleton context.
738
- *
739
- * Plugins receive a context that is bound to the parent
740
- * `LivePreviewClient`. They can:
741
- *
742
- * - subscribe via `events.on/once`,
743
- * - register field renderers,
744
- * - register value transforms,
745
- * - read the client's (frozen) configuration,
746
- * - log through the client's debug channel.
747
- */
748
612
  interface PluginContext {
749
613
  readonly events: PluginEvents;
750
- /** Register a renderer owned by this plugin registration. */
614
+ /** Register a renderer layer owned by this registration. */
751
615
  readonly registerFieldRenderer: (renderer: FieldRenderer) => void;
752
- /** Register an ordered transform owned by this plugin registration. */
616
+ /** Register an ordered transform owned by this registration. */
753
617
  readonly registerTransform: (fieldName: string, transform: FieldTransform) => void;
754
- /**
755
- * Register any additional synchronous cleanup owned by this registration.
756
- * The manager invokes it during rollback, `unuse()`, or client destruction.
757
- * Optional so pre-1.0.4 structural context mocks remain assignable.
758
- */
618
+ /** Register a synchronous cleanup run on rollback, `unuse()` and client destruction. */
759
619
  readonly registerCleanup?: (cleanup: PluginDisposer) => void;
760
620
  readonly getConfig: () => Readonly<Record<string, unknown>>;
761
621
  readonly log: (...args: unknown[]) => void;
762
622
  }
763
- /**
764
- * Plugin definition. `init` runs once per registration; `destroy`
765
- * runs when the plugin is unregistered or the client is destroyed.
766
- *
767
- * Both hooks may return a promise; the manager awaits them.
768
- */
623
+ /** `init` runs once per registration, `destroy` on removal; both may return a promise. */
769
624
  interface LivePreviewPlugin {
770
625
  readonly name: string;
771
626
  readonly version?: string;
627
+ /** Checked at registration; a plugin this runtime does not fit is refused (`LP0103`). */
628
+ readonly compat?: PluginCompatibility;
772
629
  readonly init: (context: PluginContext) => void | Promise<void>;
773
630
  readonly destroy?: () => void | Promise<void>;
774
631
  }
775
632
 
776
633
  /**
777
- * `LivePreviewClient` configuration shape.
778
- *
779
- * @module @client/config
634
+ * The two defaults profiles, row by row. 2.0 ships `v2`; `defaults: 'v1'` opts
635
+ * back into the 1.x table for a staged migration. Each row belongs to exactly
636
+ * one side — the adapter's request decision or the browser runtime — and a
637
+ * test asserts the profile assigns every readiness row that has an option, so
638
+ * a row added later cannot be forgotten. See ADR 0007.
639
+ */
640
+ /** `'v2'` is the 2.0 default; `'v1'` is the 1.x table. */
641
+ type DefaultsProfile = 'v1' | 'v2';
642
+ /** How the runtime decides which windows may post updates. */
643
+ type EventSourcePolicy = 'any' | 'parent-or-opener';
644
+
645
+ /**
646
+ * `LivePreviewClient` configuration. Defaults are the 2.0 rows; `defaults: 'v1'`
647
+ * restores the 1.x values named per option (ADR 0007).
780
648
  */
649
+
781
650
  interface LivePreviewClientConfig {
782
- /** Explicit additional trusted origins. */
651
+ /** Explicit trusted admin origins. */
783
652
  readonly allowedOrigins?: readonly string[];
784
- /**
785
- * Payload server origin, e.g. `https://cms.example.com`. When set,
786
- * every incoming update is re-fetched through the Payload REST API
787
- * so relationship/upload fields render populated instead of as bare
788
- * IDs (same strategy as the official client). Recommended for
789
- * Payload 3.x.
790
- */
653
+ /** Payload origin; every update is then re-fetched through the REST API so relations arrive populated. Requires `mergeDepth`. */
791
654
  readonly serverURL?: string;
792
- /** REST API route prefix used with `serverURL`. Defaults to `/api`. */
655
+ /** REST route prefix used with `serverURL`. Default `/api`. */
793
656
  readonly apiRoute?: string;
794
- /** Population depth used with `serverURL`. Defaults to `1`. */
657
+ /** Population depth used with `serverURL`. Required with it; `defaults: 'v1'` falls back to `1`. */
795
658
  readonly mergeDepth?: number;
796
- /**
797
- * Custom fetch implementation for the `serverURL` merge request —
798
- * the equivalent of the official client's `requestHandler`. Use it
799
- * to attach auth headers or route through your own proxy. Only
800
- * available on the programmatic client (functions cannot be
801
- * serialised into the inline script).
802
- */
659
+ /** Custom fetch for the `serverURL` merge request: attach auth headers or route through a proxy. */
803
660
  readonly mergeFetch?: typeof fetch;
804
- /** Enable verbose console debug output. Defaults to dev-mode detection. */
661
+ /** Verbose console output. Defaults to dev-mode detection. */
805
662
  readonly debug?: boolean;
806
- /** Debounce window for incoming updates. Defaults to 50 ms. */
663
+ /** Debounce window for incoming updates. Default 50 ms. */
807
664
  readonly debounceMs?: number;
808
- /**
809
- * Heartbeat timeout in ms. Defaults to `0` (disabled) because the
810
- * Payload admin sends messages only on form edits — there is no
811
- * protocol keepalive, so an idle timeout would produce false
812
- * disconnects.
813
- */
665
+ /** Heartbeat timeout in ms; `0` disables it (default), because the admin posts only on edits. */
814
666
  readonly heartbeatMs?: number;
815
- /** IntersectionObserver rootMargin. Defaults to `200px`. */
667
+ /** IntersectionObserver `rootMargin`. Default `200px`. */
816
668
  readonly intersectionRootMargin?: string;
817
- /**
818
- * Restrict each update to the bindings owned by the document it describes.
819
- *
820
- * Ownership is declared in markup with `data-payload-owner`, resolved from
821
- * the nearest ancestor: `global:<slug>`, `collection:<slug>`, or
822
- * `collection:<slug>:<id>`. Payload already sends the edited document's
823
- * identity, so nothing else needs configuring.
824
- *
825
- * Defaults to `false`, which keeps the 1.x behaviour of matching on the
826
- * field name alone. Enable it when one page previews more than one document
827
- * and a field name is therefore no longer a unique identity. While enabled,
828
- * a binding without an owner is never updated.
829
- */
669
+ /** Update only bindings under a matching `data-payload-owner`; an unowned binding is then never updated. Default `false`. */
830
670
  readonly scopeBindingsByOwner?: boolean;
831
- /** Bypass the visibility gate (apply every update). Defaults to `false`. */
671
+ /** Skip a binding whose value did not change. Default `true`; `defaults: 'v1'` sets `false`. */
672
+ readonly skipUnchanged?: boolean;
673
+ /** Scroll the preview to the field being edited when its value changes. Default `false`. */
674
+ readonly revealEditedField?: boolean;
675
+ /** Fields whose change re-applies other bindings regardless of their own value: `{ price: ['priceLabel'] }`. Used with `skipUnchanged`. */
676
+ readonly dependencies?: Readonly<Record<string, readonly string[]>>;
677
+ /** Strategy handlers beyond patching; `createFragmentStrategy()` from `payload-live-preview/fragment` builds one. */
678
+ readonly strategies?: StrategyHandlers;
679
+ /** Apply every update regardless of visibility. Default `false`. */
832
680
  readonly disableVisibilityGate?: boolean;
833
- /** Cache-size threshold above which off-screen updates are queued for replay. Defaults to 50. */
681
+ /** Cache size above which off-screen updates wait for intersection. Default `50`. */
834
682
  readonly visibilityGateThreshold?: number;
835
- /**
836
- * Mount an `aria-live` region for connections, applied updates, and mounted
837
- * heartbeat-timeout disconnects. Destroy releases it synchronously. Default `true`.
838
- */
683
+ /** Mount an `aria-live` region for connections, applied updates and heartbeat disconnects. Default `true`. */
839
684
  readonly enableA11y?: boolean;
840
- /** Locale used to pick A11y announcement strings. Defaults to detected locale. */
685
+ /** Locale of the announcement strings. Defaults to the detected locale. */
841
686
  readonly a11yLocale?: string;
842
- /** Document root, defaults to `document`. */
687
+ /** Document root. Default `document`. */
843
688
  readonly root?: Document | Element;
844
- /** Disable `document.referrer` detection. */
689
+ /** Renderer resolution ahead of the registry; return `undefined` to fall through. */
690
+ readonly resolveRenderer?: (fieldType: RendererKey, target: CachedElement) => FieldRenderer | undefined;
691
+ /** The project's rich-text renderer, shared with SSR. Its output is sanitized. */
692
+ readonly renderRichText?: RichTextRenderer;
693
+ /** Ignore `document.referrer` as an origin source. Default `true`; `defaults: 'v1'` sets `false`. */
845
694
  readonly disableReferrerDetection?: boolean;
846
- /** Disable the localhost dev-mode pattern matcher. */
695
+ /** Disable the dev-mode localhost matcher. Default `false`. */
847
696
  readonly disableLocalhostMatching?: boolean;
848
- /** Auto-start the runtime in the constructor. Defaults to `true`. */
697
+ /** Which windows may post updates. Default `'parent-or-opener'`; `defaults: 'v1'` sets `'any'`. */
698
+ readonly eventSourcePolicy?: EventSourcePolicy;
699
+ /** Sanitizer policy for rich text and HTML writes. Default `'strict'`; `defaults: 'v1'` sets `'compat'`. */
700
+ readonly sanitizerPolicy?: 'compat' | 'strict';
701
+ /** `'v2'` (default) is the 2.0 table; `'v1'` restores every 1.x row at once. Explicit options win. */
702
+ readonly defaults?: DefaultsProfile;
703
+ /** Start the runtime in the constructor. Default `true`. */
849
704
  readonly autoStart?: boolean;
850
- /**
851
- * Optional preview-token validator. When set, every data update
852
- * must carry a `previewToken` field that this function approves.
853
- *
854
- * ⚠️ `previewToken` is an extension of THIS library, not part of the
855
- * stock Payload postMessage protocol — the Payload admin never sends
856
- * one. Only enable this when a custom admin component adds the token
857
- * to outgoing messages; against an unmodified Payload admin it would
858
- * drop every update.
859
- */
705
+ /** Preview-token validator; every update must then carry a `previewToken` it approves. Stock Payload sends none. */
860
706
  readonly validateToken?: (token: string | undefined, origin: string) => boolean | Promise<boolean>;
861
707
  }
862
708
 
863
709
  /**
864
- * Public, high-level `LivePreviewClient`.
865
- *
866
- * The client wraps a `LivePreviewRuntime` with:
867
- *
868
- * - Per-instance event emitter (no singleton).
869
- * - Per-instance plugin manager.
870
- * - Custom-renderer registration.
871
- * - Field-name transforms frozen into revision-bound scheduler entries before
872
- * their later attribute or renderer dispatch.
873
- *
874
- * Consumers who want full programmatic control instantiate
875
- * `LivePreviewClient` directly. Consumers who just want a working
876
- * inline preview should use `generateInlineScript()` instead.
877
- *
878
- * @module @client
710
+ * `LivePreviewClient`: a `LivePreviewRuntime` with its own emitter, plugin
711
+ * manager and renderer registry. A page that only needs a working preview uses
712
+ * `generateInlineScript()` instead.
879
713
  */
880
714
 
881
715
  declare class LivePreviewClient {
882
716
  #private;
883
- constructor(config?: LivePreviewClientConfig);
717
+ constructor(rawConfig?: LivePreviewClientConfig);
884
718
  /**
885
- * Start the runtime. Returns `true` when this client is eligible and runtime
886
- * startup is active or scheduled for DOM readiness. Repeated calls on an
887
- * active client also return `true`; `false` means the client was destroyed or
888
- * the page is outside a preview context. A deferred startup can still fail,
889
- * roll back, and be retried by a later call.
719
+ * Start the runtime: `true` while eligible and started or scheduled for DOM
720
+ * readiness, `false` once destroyed or outside a preview context.
890
721
  */
891
722
  start(): boolean;
892
723
  /**
893
- * Release the message ingress while keeping this client usable.
894
- *
895
- * For the document lifecycle, not for teardown: a back/forward-cache restore
896
- * does not re-run module scripts, so a client that stays attached across
897
- * `pagehide` returns bound to a frozen page and silently stops updating.
898
- * Plugins, renderers and transforms survive; `resume()` brings the same
899
- * client back. Returns `false` when there was nothing running to suspend.
724
+ * Release the message ingress, keeping plugins and renderers, for `pagehide`:
725
+ * a bfcache restore re-runs no script, so an attached client would return
726
+ * bound to a frozen page. `false` when nothing was running.
900
727
  */
901
728
  suspend(): boolean;
902
- /**
903
- * Reacquire after `suspend()`.
904
- *
905
- * Deliberately the same path as `start()`: a restored document needs the
906
- * cache rebuilt, the observers rebound and the handshake rebroadcast, which
907
- * is exactly what starting does. Returns `false` for a destroyed client or
908
- * one that was never started.
909
- */
729
+ /** Reacquire after `suspend()`: the same path as `start()`, because a restored document needs the same rebuild. */
910
730
  resume(): boolean;
911
- /**
912
- * Stop the runtime and tear down every plugin. Idempotent.
913
- */
731
+ /** Stop the runtime and tear down every plugin. Idempotent; concurrent callers share one promise. */
914
732
  destroy(): Promise<void>;
915
- /**
916
- * Register a plugin.
917
- */
918
733
  use(plugin: LivePreviewPlugin): Promise<void>;
919
- /** Unregister a plugin and release all resources owned by that registration. */
734
+ /** Unregister a plugin and release everything its registration owned. Still allowed from a destroy hook during teardown. */
920
735
  unuse(name: string): Promise<void>;
921
- /** Rebuild the element cache manually. */
922
736
  refreshCache(): void;
923
- /** Read-only access to the event emitter for `on`/`once`/`off`. */
924
737
  get events(): EventEmitter;
925
- /** Names of currently registered plugins. */
738
+ /** Names of the registered plugins. */
926
739
  get plugins(): readonly string[];
927
- /** Current connection status. */
928
740
  get status(): 'disconnected' | 'connecting' | 'connected';
929
- /** Number of valid updates received so far. */
930
741
  /**
931
- * Point-in-time read of runtime state, for diagnosing a preview that is not
932
- * updating. Performs no I/O and transmits nothing.
933
- *
934
- * Read `bindings.orphanFields` when a field refuses to update, and
935
- * `scheduler.deferred` together with `scheduler.visibilityGateActive` when
936
- * updates stop below the fold.
742
+ * Point-in-time read of runtime state, no I/O: `bindings.orphanFields` when a
743
+ * field refuses to update, `scheduler.deferred` when updates stop below the fold.
937
744
  */
938
745
  inspect(): LivePreviewInspection;
746
+ /** Number of accepted updates so far. */
939
747
  get updateCount(): number;
940
748
  /** `true` once `destroy()` has been called. */
941
749
  get destroyed(): boolean;
942
750
  }
751
+ /** A started client, or `null` when the page is not a preview context. */
752
+ declare function initLivePreview(config?: LivePreviewClientConfig): LivePreviewClient | null;
753
+
943
754
  /**
944
- * Convenience factory: instantiate the client, returning `null` when
945
- * the page is not currently a preview context (top-level navigation).
946
- * Useful for SSR-style integrations that import this from any context.
755
+ * Admin-side reporter for "reveal the edited section": it names the field the
756
+ * editor's cursor is in so the preview scrolls there without a keystroke.
947
757
  */
948
- declare function initLivePreview(config?: LivePreviewClientConfig): LivePreviewClient | null;
758
+ /** The preview window (usually `iframe.contentWindow`) messages are posted to. */
759
+ interface FocusReportTarget {
760
+ postMessage: (message: unknown, targetOrigin: string) => void;
761
+ }
762
+ interface PreviewFocusMessage {
763
+ readonly type: 'payload-live-preview-focus';
764
+ readonly field: string;
765
+ }
766
+ /** Post one focus report. `targetOrigin` must be the preview's exact origin, never `'*'`. */
767
+ declare function reportPreviewFocus(target: FocusReportTarget, field: string, targetOrigin: string): void;
768
+ /** A reporter bound to a preview window and origin; `resolveTarget` runs per report, so a re-created iframe is addressed freshly. */
769
+ declare function createPreviewFocusReporter(resolveTarget: () => FocusReportTarget | null | undefined, targetOrigin: string): (field: string) => void;
949
770
 
950
771
  /**
951
- * Document-lifecycle owner for the live-preview client.
952
- *
953
- * The runtime learns about messages, not about navigation. Two browser
954
- * behaviours make that gap visible, and both are silent:
955
- *
956
- * - A **back/forward-cache restore** does not re-run module scripts. A client
957
- * that stays attached across `pagehide` comes back bound to a document the
958
- * browser froze and thawed, and simply stops updating.
959
- * - A **soft navigation** replaces page content without a new document. The
960
- * bindings the cache holds are gone, and nothing tells the runtime.
961
- *
962
- * This binds both to `suspend()`/`resume()`/`refreshCache()` in one place, so a
963
- * consumer that uses no framework adapter can own the lifecycle with one call
964
- * instead of re-deriving it — which is what every integration has had to do so
965
- * far.
966
- *
967
- * @module @core/navigation-lifecycle
772
+ * Binds the document lifecycle the runtime cannot see: a back/forward-cache
773
+ * restore re-runs no scripts, so a client that stayed attached comes back
774
+ * bound to a frozen document and quietly stops updating; a soft navigation
775
+ * replaces the content the binding cache describes. Both are silent failures.
968
776
  */
969
777
  /** The part of the client this owner drives. */
970
778
  interface NavigationLifecycleTarget {
@@ -973,30 +781,17 @@ interface NavigationLifecycleTarget {
973
781
  refreshCache(): void;
974
782
  }
975
783
  interface NavigationLifecycleOptions {
976
- /**
977
- * Event target for `pagehide`/`pageshow`. Defaults to `window`.
978
- *
979
- * These fire on the window, not the document, and only there — binding them
980
- * anywhere else silently never fires.
981
- */
784
+ /** Where `pagehide`/`pageshow` fire; they exist only on the window. Defaults to `window`. */
982
785
  readonly windowTarget?: EventTarget;
983
- /**
984
- * Event target for soft-navigation events. Defaults to `document`.
985
- */
786
+ /** Event target for soft-navigation events. Defaults to `document`. */
986
787
  readonly documentTarget?: EventTarget;
987
788
  /**
988
- * Soft-navigation events after which the DOM has been replaced and the
989
- * binding cache has to be rebuilt — for example `astro:page-load`.
990
- *
991
- * Empty by default: the package cannot know which framework is present, and
992
- * guessing would either miss the event or rebuild on an unrelated one.
789
+ * Events after which the DOM was replaced and the cache must be rebuilt, e.g.
790
+ * `astro:page-load`. Empty by default: only the host knows which its router fires.
993
791
  */
994
792
  readonly softNavigationEvents?: readonly string[];
995
793
  }
996
- /**
997
- * Bind the document lifecycle. Returns an unbind function; calling it twice is
998
- * harmless.
999
- */
794
+ /** Returns an unbind function; calling it twice is harmless. */
1000
795
  declare function bindNavigationLifecycle(target: NavigationLifecycleTarget, options?: NavigationLifecycleOptions): () => void;
1001
796
 
1002
797
  interface RuntimeBuildInfo {
@@ -1005,536 +800,417 @@ interface RuntimeBuildInfo {
1005
800
  }
1006
801
 
1007
802
  /**
1008
- * Inline script generator.
1009
- *
1010
- * Wraps the build-time-generated runtime IIFE with consumer-provided
1011
- * configuration. Returns a JavaScript string suitable for embedding
1012
- * via `<script>` in any framework.
1013
- *
1014
- * The runtime body is identical to the one used by the high-level
1015
- * client because both originate from `src/core/runtime.ts`. There is
1016
- * no parallel implementation to drift out of sync.
1017
- *
1018
- * @module @inline/generator
803
+ * The inline script's configuration and its positional wire order: the
804
+ * generator writes the tuple, `src/core/runtime.ts` destructures it, and
805
+ * `INLINE_CONFIG_KEYS` is where they agree. Append only, never reorder.
1019
806
  */
1020
807
 
1021
808
  interface InlineScriptConfig {
1022
- /** Additional trusted origins to merge with auto-detected ones. */
809
+ /** Trusted admin origins, merged with the detected ones. */
1023
810
  readonly allowedOrigins?: readonly string[];
1024
- /**
1025
- * Payload server origin, e.g. `https://cms.example.com`. When set,
1026
- * every incoming update is re-fetched through the Payload REST API
1027
- * (`X-Payload-HTTP-Method-Override: GET`, same strategy as the
1028
- * official client) so relationship and upload fields render
1029
- * populated instead of as bare IDs. Strongly recommended for
1030
- * Payload 3.x. Requires the preview page to be able to reach the
1031
- * API with credentials (same-site cookies or CORS `credentials`).
1032
- */
811
+ /** Payload origin; every update is then re-fetched through the REST API with credentials. Requires `mergeDepth`. */
1033
812
  readonly serverURL?: string;
1034
- /** REST API route prefix used with `serverURL`. Defaults to `/api`. */
813
+ /** REST route prefix used with `serverURL`. Default `/api`. */
1035
814
  readonly apiRoute?: string;
1036
- /** Population depth used with `serverURL`. Defaults to `1`. */
815
+ /** Population depth used with `serverURL`. Required with `serverURL` unless `defaults: 'v1'`. */
1037
816
  readonly mergeDepth?: number;
1038
- /** Enable verbose console logging. Defaults to `false`. */
817
+ /** Verbose console logging. Default `false`. */
1039
818
  readonly debug?: boolean;
1040
- /** Debounce window for incoming updates in ms. Defaults to `50`. */
819
+ /** Debounce window for incoming updates in ms. Default `50`. */
1041
820
  readonly debounceMs?: number;
1042
- /** Enable screen-reader live region. Defaults to `true`. */
821
+ /** Mount the screen-reader live region. Default `true`. */
1043
822
  readonly enableA11y?: boolean;
1044
- /**
1045
- * Heartbeat timeout in ms. Defaults to `0` (disabled). The Payload
1046
- * admin only posts messages on form edits — there is no protocol
1047
- * keepalive — so an idle-based timeout would fire spurious
1048
- * disconnects while the editor pauses. Enable only if your admin
1049
- * setup sends periodic messages.
1050
- */
823
+ /** Heartbeat timeout in ms; `0` disables it (default), because the admin posts only on edits. */
1051
824
  readonly heartbeatMs?: number;
1052
- /**
1053
- * Bypass the visibility-gate optimisation and apply every update
1054
- * regardless of viewport position. Default `false` — the gate kicks
1055
- * in when the cache exceeds `visibilityGateThreshold` entries.
1056
- */
825
+ /** Apply every update regardless of viewport position. Default `false`. */
1057
826
  readonly disableVisibilityGate?: boolean;
1058
- /**
1059
- * Cache-size threshold above which the visibility gate activates.
1060
- * Default `50`. Off-screen elements above this threshold are queued
1061
- * for replay on intersection instead of being updated immediately.
1062
- */
827
+ /** Cache size above which off-screen updates wait for intersection. Default `50`. */
1063
828
  readonly visibilityGateThreshold?: number;
1064
- /**
1065
- * `rootMargin` passed to the `IntersectionObserver` watching the
1066
- * cached elements. Default `'200px'`. Increase to pre-render
1067
- * further off-screen content; decrease for tighter visibility.
1068
- */
829
+ /** `rootMargin` of the IntersectionObserver. Default `'200px'`. */
1069
830
  readonly intersectionRootMargin?: string;
1070
- /**
1071
- * Opt out of `document.referrer`-based origin auto-detection.
1072
- * Useful when the page is loaded under `Referrer-Policy: no-referrer`
1073
- * and you want predictable behaviour. Default `false`.
1074
- */
831
+ /** Ignore `document.referrer` as an origin source. Default `true`; `defaults: 'v1'` sets `false`. */
1075
832
  readonly disableReferrerDetection?: boolean;
1076
- /**
1077
- * Opt out of the dev-mode localhost-pattern matcher (any port on
1078
- * `localhost` / `127.0.0.1`). Useful when running locally against
1079
- * a production-like origin list. Default `false`.
1080
- */
833
+ /** Disable the dev-mode localhost matcher (any port on `localhost`/`127.0.0.1`). Default `false`. */
1081
834
  readonly disableLocalhostMatching?: boolean;
1082
- /**
1083
- * Restrict each update to the bindings owned by the document it describes,
1084
- * declared in markup with `data-payload-owner` and resolved from the nearest
1085
- * ancestor. Default `false`, which keeps matching on the field name alone.
1086
- *
1087
- * Enable it when one page previews more than one document. While enabled a
1088
- * binding without an owner is never updated.
1089
- */
835
+ /** Update only bindings under a matching `data-payload-owner`. Default `false`. */
1090
836
  readonly scopeBindingsByOwner?: boolean;
1091
- /**
1092
- * Retained for 1.x source compatibility, but has no effect here:
1093
- * `generateInlineScript()` returns a script body and creates no tag.
1094
- * Pass the nonce separately to `wrapWithScriptTag(body, { nonce })`.
1095
- *
1096
- * @deprecated Use the second argument of `wrapWithScriptTag()`.
1097
- */
1098
- readonly nonce?: string;
837
+ /** Skip bindings whose value did not change. Default `true`; `defaults: 'v1'` sets `false`. */
838
+ readonly skipUnchanged?: boolean;
839
+ /** Scroll the preview to the field being edited. Default `false`. */
840
+ readonly revealEditedField?: boolean;
841
+ /** Which windows may post updates. Default `'parent-or-opener'`; `defaults: 'v1'` sets `'any'`. */
842
+ readonly eventSourcePolicy?: 'any' | 'parent-or-opener';
843
+ /** Sanitizer policy for rich text and HTML writes. Default `'strict'`; `defaults: 'v1'` sets `'compat'`. */
844
+ readonly sanitizerPolicy?: 'compat' | 'strict';
845
+ /** Same-origin path of a fragment endpoint; the script then carries the fragment client ahead of the runtime (ADR 0011). */
846
+ readonly fragmentEndpoint?: string;
847
+ /** Which defaults the omitted options fall back to. Not serialized: `'v1'` only relaxes the generator's `mergeDepth` check. */
848
+ readonly defaults?: DefaultsProfile;
1099
849
  }
850
+
1100
851
  /**
1101
- * Build an inline script body. The result is a self-contained IIFE
1102
- * with the build-time runtime, prefixed by a configuration block.
1103
- *
1104
- * The string does NOT include the `<script>` tags — use
1105
- * `wrapWithScriptTag()` when you need them.
852
+ * Wraps the build-time runtime IIFE with the consumer's configuration; a page
853
+ * with a fragment endpoint gets the fragment prelude ahead of it.
1106
854
  */
855
+
856
+ /** The inline script body without `<script>` tags; see `wrapWithScriptTag()`. */
1107
857
  declare function generateInlineScript(config?: InlineScriptConfig): string;
1108
- /**
1109
- * Wrap an inline script body in a `<script>` tag. When a nonce is
1110
- * provided it is added as the `nonce` attribute so CSP policies that
1111
- * require `'nonce-...'` accept the script.
1112
- */
858
+ /** Wrap a script body in `<script>`, with the CSP nonce attribute when given. */
1113
859
  declare function wrapWithScriptTag(body: string, options?: {
1114
860
  nonce?: string;
1115
861
  }): string;
1116
- /**
1117
- * Snapshot of the build-time information for diagnostics.
1118
- */
1119
862
  declare function runtimeBuildInfo(): RuntimeBuildInfo;
1120
863
 
1121
- /**
1122
- * HTML-escape primitives.
1123
- *
1124
- * Implementations follow the OWASP XSS Prevention Cheat Sheet: each
1125
- * special character is replaced with its decimal or hex entity. The
1126
- * functions are isomorphic — they have no dependency on the DOM and
1127
- * can run in any JavaScript environment (browser, Node, Deno, Bun).
1128
- *
1129
- * @module @security/escape
1130
- */
1131
- /**
1132
- * Escape every HTML-significant character in `text` so the result is
1133
- * safe to interpolate into any HTML context (element body or attribute).
1134
- *
1135
- * - Coerces non-strings via `String(value)`; pass `null`/`undefined`
1136
- * intentionally — they become `"null"`/`"undefined"`.
1137
- * - Cannot fail; returns `""` for the empty input.
1138
- */
864
+ /** HTML and CSS escaping primitives (OWASP XSS Prevention Cheat Sheet). DOM-free. */
865
+ /** Escape `text` for an element body or a *quoted* attribute value; strings only, and unquoted attributes are not covered. */
1139
866
  declare function escapeHtml(text: string): string;
1140
- /**
1141
- * Escape characters that would break out of a double-quoted HTML
1142
- * attribute value. Stricter than `escapeHtml` for body content but
1143
- * preserves URL characters (`/`, `=`, etc.) that are common in
1144
- * `href` and `src` values.
1145
- *
1146
- * Only call this for values that have already been validated as safe
1147
- * URLs or trusted strings; it does NOT block dangerous protocols.
1148
- */
867
+ /** Escape a value for a *quoted* HTML attribute, leaving URL characters intact; it validates no scheme, so run `isSafeUrl()` on `href`/`src` first. */
1149
868
  declare function escapeHtmlAttribute(value: string): string;
1150
869
 
1151
870
  /**
1152
- * URL safety validation.
1153
- *
1154
- * Single source of truth for URL-protocol decisions used by both the
1155
- * inline runtime and the high-level client. The previous version of
1156
- * this library had two implementations that subtly disagreed; this
1157
- * module replaces both.
1158
- *
1159
- * Policy:
1160
- * - Allow only the protocols in `SAFE_PROTOCOLS`.
1161
- * - Reject every other protocol explicitly, including `javascript:`,
1162
- * `data:`, `vbscript:`, `file:`, `blob:`, and any custom scheme.
1163
- * - Empty strings are NOT considered safe (this is a tightening from
1164
- * the legacy behaviour and avoids producing `<a href="">` markup).
1165
- *
1166
- * @module @security/url-validator
1167
- */
1168
- /**
1169
- * Returns `true` only when `url` is one of the explicitly allowed forms:
1170
- *
1171
- * - Absolute URL with `http:`, `https:`, `mailto:`, or `tel:` protocol
1172
- * - Protocol-relative URL (`//example.com/...`)
1173
- * - Same-origin absolute path (`/foo`)
1174
- * - Hash or query fragment (`#section`, `?q=1`)
1175
- * - Plain relative path (`foo/bar`, `./foo`, `../foo`)
871
+ * URL safety: the one scheme allow-list the runtime, the client and the
872
+ * sanitizer share. Empty strings are not safe (`<a href="">` is never emitted).
1176
873
  */
874
+ /** `true` only for absolute `http`/`https`/`mailto`/`tel` URLs, protocol-relative URLs, paths, `#`/`?` fragments and relative paths. */
1177
875
  declare function isSafeUrl(url: unknown): boolean;
1178
- /**
1179
- * Returns `true` when the URL points to an external HTTP(S) origin.
1180
- * Protocol-relative URLs (`//example.com/...`) count as external —
1181
- * they resolve to another origin and need the same `noopener`
1182
- * hardening as absolute ones.
1183
- *
1184
- * Used by renderers to decide whether to add `target="_blank"` and the
1185
- * `noopener noreferrer` rel attributes to anchors. Inputs that fail
1186
- * `isSafeUrl()` always return `false`.
1187
- */
876
+ /** Whether a safe URL points at another HTTP(S) origin, protocol-relative forms included, and so needs `noopener` hardening. */
1188
877
  declare function isExternalHttpUrl(url: string): boolean;
1189
878
 
879
+ /**
880
+ * `'strict'` (default) strips `id`, `name` and `data-payload-*` and passes
881
+ * other `data-*` only via `allowedDataAttributes`; `'compat'` keeps them.
882
+ */
883
+ type SanitizerPolicyMode = 'compat' | 'strict';
1190
884
  interface SanitizeOptions {
1191
- /** Extra tags to allow beyond the built-in list. Lower-case, untrimmed. */
885
+ /** Overrides the module default set by `setSanitizerPolicy()`. */
886
+ readonly policy?: SanitizerPolicyMode;
887
+ /** `data-*` attributes (full names) that pass under `'strict'`; `data-payload-*` never does. */
888
+ readonly allowedDataAttributes?: readonly string[];
889
+ /**
890
+ * Keep `input`, `button`, `textarea`, `select`, `option` and `label` — only
891
+ * for markup the page author wrote, never for CMS content. `form`,
892
+ * handlers, `style` and unsafe URLs stay dropped.
893
+ */
894
+ readonly allowFormControls?: boolean;
895
+ /** Extra tags to allow. Lower-case, untrimmed. */
1192
896
  readonly additionalAllowedTags?: readonly string[];
1193
- /** Extra per-tag attributes to allow. Tag and attribute names must be lower-case. */
897
+ /**
898
+ * Extra per-tag attributes, lower-case. Cannot re-admit handlers, `style`,
899
+ * or — under `'strict'` — `id`, `name` and `data-payload-*`.
900
+ */
1194
901
  readonly additionalAllowedAttributes?: Readonly<Record<string, readonly string[]>>;
902
+ /**
903
+ * The input is a page-author item template, so `'strict'` keeps the
904
+ * applier's reconciliation attributes (`TEMPLATE_ATTRIBUTES`) and nothing
905
+ * else — every other `data-payload-*` is still stripped, so a template can
906
+ * never add a binding.
907
+ */
908
+ readonly templateMode?: boolean;
1195
909
  }
1196
- /**
1197
- * Minimal `Document`-shaped surface the sanitizer needs. Any DOM
1198
- * implementation (browser, jsdom, linkedom, happy-dom, parse5 + a
1199
- * thin adapter) that produces `<template>`-style elements works.
1200
- */
910
+ /** Set the policy for calls without an explicit `policy`. The runtime sets it from `sanitizerPolicy`. */
911
+ declare function setSanitizerPolicy(mode: SanitizerPolicyMode): void;
912
+ /** The `Document` surface the sanitizer needs; jsdom, linkedom and happy-dom all provide it. */
1201
913
  interface SanitizerDocument {
1202
914
  createElement: (tagName: string) => {
1203
915
  innerHTML: string;
1204
916
  readonly content: ParentNode;
1205
917
  };
1206
918
  }
919
+ /** Supply a `Document` for SSR, e.g. linkedom's `parseHTML(...).document`; `null` clears it. */
920
+ declare function setSanitizerDocument(doc: SanitizerDocument | null): void;
1207
921
  /**
1208
- * Inject a `Document` implementation for the sanitizer to use when
1209
- * `globalThis.document` is unavailable.
1210
- *
1211
- * Server-side renderers (Node, Bun, Deno without DOM globals) can wire
1212
- * this up once at startup with any of the popular pure-JS DOM
1213
- * libraries:
1214
- *
1215
- * ```ts
1216
- * // linkedom — recommended (smallest, fastest)
1217
- * import { parseHTML } from 'linkedom';
1218
- * import { setSanitizerDocument } from 'payload-live-preview';
1219
- * const { document } = parseHTML('<!doctype html><html><body></body></html>');
1220
- * setSanitizerDocument(document);
1221
- * ```
1222
- *
1223
- * ```ts
1224
- * // jsdom — heavier, but already in many SSR pipelines
1225
- * import { JSDOM } from 'jsdom';
1226
- * setSanitizerDocument(new JSDOM().window.document);
1227
- * ```
1228
- *
1229
- * Pass `null` to clear a previous override (mostly useful for tests).
1230
- */
1231
- declare function setSanitizerDocument(doc: SanitizerDocument | null): void;
1232
- /**
1233
- * Sanitize `html` and return safe HTML.
1234
- *
1235
- * The function never returns `undefined`/`null`. On malformed input the
1236
- * browser parser silently recovers and the sanitizer continues from
1237
- * the recovered tree.
922
+ * Sanitize `html`; malformed input is recovered by the parser first.
1238
923
  *
1239
924
  * @throws {SanitizerEnvironmentError} when no DOM is available.
1240
925
  */
1241
926
  declare function sanitizeHtml(html: string, options?: SanitizeOptions): string;
1242
927
 
1243
928
  /**
1244
- * Content-Security-Policy helpers.
1245
- *
1246
- * Two responsibilities:
1247
- *
1248
- * 1. Generate cryptographic nonces so consumers can opt into a strict
1249
- * `script-src 'nonce-...'` policy and avoid `'unsafe-inline'`.
1250
- * 2. Build the `frame-ancestors` directive value, restricting which
1251
- * origins may embed the preview page in an iframe.
1252
- *
1253
- * Both functions are isomorphic — they work in browsers, Node, and
1254
- * edge runtimes that expose Web Crypto via `globalThis.crypto`.
1255
- *
1256
- * @module @security/csp
1257
- */
1258
- /**
1259
- * Minimal Web-Crypto surface area we actually call. The full `Crypto`
1260
- * type drags in browser-only declarations that don't typecheck cleanly
1261
- * against Node's `webcrypto` shape — this narrower interface accepts
1262
- * both without an unsafe cast.
929
+ * Content-Security-Policy helpers: nonce generation, directive builders and
930
+ * the header merge every adapter uses. Isomorphic; needs only Web Crypto.
1263
931
  */
932
+ /** The one Web Crypto method this module calls; accepts Node's `webcrypto` without a cast. */
1264
933
  interface WebCryptoLike {
1265
934
  getRandomValues: <T extends ArrayBufferView | null>(array: T) => T;
1266
935
  }
1267
- /**
1268
- * Inject a Web Crypto implementation to use when `globalThis.crypto`
1269
- * is unavailable.
1270
- *
1271
- * Node.js exposed `globalThis.crypto` globally starting with v19. On
1272
- * Node 18 (still LTS at time of writing) consumers can flip the
1273
- * `--experimental-global-webcrypto` flag *or* call this function once
1274
- * at startup:
1275
- *
1276
- * ```ts
1277
- * import { webcrypto } from 'node:crypto';
1278
- * import { setCspCrypto } from 'payload-live-preview';
1279
- * setCspCrypto(webcrypto as { getRandomValues: typeof webcrypto.getRandomValues });
1280
- * ```
1281
- *
1282
- * Browser bundles never need to call this — `globalThis.crypto` is
1283
- * always present there, and the override only matters when the global
1284
- * is missing.
1285
- *
1286
- * Pass `null` to clear a previous override (mostly useful for tests).
1287
- */
936
+ /** Supply a Web Crypto implementation for runtimes without `globalThis.crypto`; `null` clears it. */
1288
937
  declare function setCspCrypto(crypto: WebCryptoLike | null): void;
1289
- /**
1290
- * Generate a base64url-encoded cryptographic nonce.
1291
- *
1292
- * Uses `crypto.getRandomValues` everywhere — falls back to throwing
1293
- * if no Web Crypto implementation is available, because a predictable
1294
- * nonce would silently defeat the entire CSP.
1295
- *
1296
- * @param bytes Number of random bytes (defaults to 16 → 128-bit entropy).
1297
- */
938
+ /** A base64url nonce of `bytes` random bytes (default 16); throws without Web Crypto, since a predictable nonce silently defeats the CSP. */
1298
939
  declare function generateCspNonce(bytes?: number): string;
1299
940
  interface FrameAncestorsOptions {
1300
941
  readonly self?: boolean;
1301
942
  readonly origins?: readonly string[];
1302
943
  readonly allowNone?: boolean;
1303
944
  }
1304
- /**
1305
- * Build the value for `frame-ancestors` in a `Content-Security-Policy`
1306
- * header.
1307
- *
1308
- * Returns `'none'` if `allowNone: true` and no other sources are
1309
- * provided. Otherwise concatenates `'self'` (if requested) with the
1310
- * given origins, deduplicating and trimming entries.
1311
- *
1312
- * @example
1313
- * `frame-ancestors ${buildFrameAncestors({ self: true, origins: [adminUrl] })}`
1314
- */
945
+ /** The `frame-ancestors` value: `'self'` and the origins, deduplicated; `'none'` when empty. */
1315
946
  declare function buildFrameAncestors(options?: FrameAncestorsOptions): string;
1316
947
  /**
1317
- * Build a `script-src` value that permits a single nonce plus optional
1318
- * extra sources.
1319
- *
1320
- * `'strict-dynamic'` is **opt-in**: under CSP 3 it makes browsers
1321
- * ignore `'self'` and every host source in the directive, so any
1322
- * parser-inserted script without a nonce (framework hydration scripts,
1323
- * consumer `<script>` tags) would be blocked. Only enable it when every
1324
- * script on the page carries the nonce.
948
+ * A `script-src` value permitting one nonce plus optional extra sources.
949
+ * `'strict-dynamic'` is opt-in: it makes CSP 3 browsers ignore `'self'` and
950
+ * host sources, so every script on the page must then carry the nonce.
1325
951
  */
1326
952
  declare function buildScriptSrcWithNonce(nonce: string, options?: {
1327
953
  readonly self?: boolean;
1328
954
  readonly extra?: readonly string[];
1329
955
  readonly strictDynamic?: boolean;
1330
956
  }): string;
1331
- /**
1332
- * How a directive is merged into an existing CSP header value.
1333
- *
1334
- * - `union` (default) — the new sources are added to the existing
1335
- * directive's sources (deduplicated). An existing `'none'` is
1336
- * dropped when real sources are added, since `'none'` may not be
1337
- * combined with other sources.
1338
- * - `replace` — the new value overwrites the existing directive.
1339
- */
957
+ /** `union` (default) adds sources to an existing directive; `replace` overwrites it. */
1340
958
  interface CspDirectiveMerge {
1341
959
  readonly value: string;
1342
960
  readonly mode?: 'union' | 'replace';
1343
961
  }
1344
962
  /**
1345
- * Merge directives into an existing `Content-Security-Policy` header
1346
- * value without clobbering what a consumer (or another middleware)
1347
- * already configured. Directives not mentioned in `additions` pass
1348
- * through untouched; new directives are appended. Existing policy parsing
1349
- * follows CSP3's ASCII-whitespace and first-duplicate-wins rules.
1350
- *
1351
- * This is the single CSP-merge implementation shared by every adapter.
963
+ * Merge directives into an existing `Content-Security-Policy` header value.
964
+ * Commas separate policies (that is how `Headers.get()` joins repeated
965
+ * headers); each is merged, because a browser enforces every policy and
966
+ * widening only the last would leave the others blocking.
1352
967
  */
1353
968
  declare function mergeCspHeader(existing: string, additions: Readonly<Record<string, string | CspDirectiveMerge>>): string;
1354
969
 
1355
970
  /**
1356
- * Server-side preview-intent detection.
1357
- *
1358
- * The live-preview runtime only ever activates inside the Payload
1359
- * admin's iframe, but the *server* still has to decide whether a
1360
- * request expresses preview intent before doing optional preview
1361
- * work. Adapters use this predicate as a delivery optimisation, and
1362
- * consumers with hand-rolled middleware can import it directly.
1363
- *
1364
- * IMPORTANT: every signal inspected here is controlled or triggerable
1365
- * by the client. A positive result is never proof of identity,
1366
- * authentication, or permission to read drafts. Before forwarding
1367
- * credentials, fetching draft data, bypassing caches, changing CSP,
1368
- * or injecting the runtime, verify the request with an application-
1369
- * owned server session or a short-lived signed authorization. Use the
1370
- * same verified decision for all of those effects.
1371
- *
1372
- * Preview intent is present when any of these hold:
1373
- *
1374
- * 1. A preview query parameter is present (`?preview=true`,
1375
- * `?draft=true`, `?livePreview=true` — configurable).
1376
- * 2. `Sec-Fetch-Dest: iframe` — the document is being loaded as an
1377
- * iframe, which is exactly how the Payload admin embeds it.
1378
- * (Sent by all evergreen browsers; absence never *excludes* a
1379
- * request, presence includes it.)
1380
- * 3. The `Referer` points at one of the given admin origins.
1381
- *
1382
- * @module @adapters/shared/preview-request
1383
- */
1384
- /** The individual signals the predicate can consider. */
1385
- type PreviewSignal = 'query' | 'fetch-dest' | 'referer';
1386
- interface PreviewRequestOptions {
1387
- /**
1388
- * Which signals count as preview intent. Default: all three.
1389
- * Restricting this to `['query']` reduces accidental activation, but
1390
- * an explicit `?preview=true` is still client-controlled and must not
1391
- * authorize response changes or privileged data access by itself.
1392
- */
1393
- readonly signals?: readonly PreviewSignal[];
1394
- /**
1395
- * Query parameters (with values `true` or `1`) that signal preview
1396
- * intent. Default: `['preview', 'draft', 'livePreview']`.
1397
- */
1398
- readonly queryParams?: readonly string[];
1399
- /**
1400
- * Treat `Sec-Fetch-Dest: iframe` as a preview-intent signal. Default `true`.
1401
- * Legacy alias for excluding `'fetch-dest'` from `signals`.
1402
- */
1403
- readonly checkFetchDest?: boolean;
1404
- /**
1405
- * Admin origins whose `Referer` signals preview intent, e.g.
1406
- * `['https://cms.example.com']`. Referer matching is not HTTP-request
1407
- * authentication.
1408
- */
1409
- readonly adminOrigins?: readonly string[];
971
+ * The authorized preview context's shape and brand, in the leaf `types` domain
972
+ * so producer and consumers recognise the same context. See ADR 0006.
973
+ */
974
+ /**
975
+ * The brand key, in the type and on the object.
976
+ *
977
+ * This was a `unique symbol` until 2026-08-30. A `unique symbol` is nominal per
978
+ * *declaration*, and the published entries each carry their own declaration
979
+ * file — so `AuthorizedPreviewContext` from the root barrel was a different
980
+ * type from the one in `./server` or in an adapter, and the documented flow
981
+ * (authorize once, hand the context to the adapter hook and to the draft read)
982
+ * did not type-check in any combination a consumer can write.
983
+ *
984
+ * That is the same trap the runtime brand below already avoids by using
985
+ * `Symbol.for` rather than `Symbol()`; the lesson simply had not reached the
986
+ * type. A shared string literal is identical in every bundle for the same
987
+ * reason a registry symbol is.
988
+ *
989
+ * It is not what makes a context trustworthy. `isAuthorizedPreviewContext`
990
+ * decides that, on the registry symbol and on `Object.isFrozen`, and every
991
+ * consumption point calls it — the adapter policy, the draft read and the
992
+ * binding emitter. Writing this key by hand buys nothing.
993
+ */
994
+ declare const AUTHORIZED_PREVIEW_BRAND_KEY = "payload-live-preview.authorized-preview-context";
995
+ type PreviewAuthorizationStrategyName = 'payload-session' | 'signed-token' | 'verifier';
996
+ /** What a context is bound to; every field is optional because every strategy binds differently. */
997
+ interface AuthorizedPreviewScope {
998
+ /** The site origin the credential was issued for. */
999
+ readonly audience?: string;
1000
+ /** The request pathname the credential is valid for. */
1001
+ readonly path?: string;
1002
+ readonly locale?: string;
1410
1003
  }
1004
+ /** The verdict of a successful `authorizePreviewRequest()`: frozen, branded, produced only there. Carry it, do not rebuild it. */
1005
+ interface AuthorizedPreviewContext {
1006
+ readonly [AUTHORIZED_PREVIEW_BRAND_KEY]: true;
1007
+ readonly strategy: PreviewAuthorizationStrategyName;
1008
+ /** The user id, token subject, or whatever the verifier named. */
1009
+ readonly subject: string | undefined;
1010
+ /** Unix milliseconds. */
1011
+ readonly authorizedAt: number;
1012
+ /** Unix milliseconds, or `undefined` when the strategy does not know. */
1013
+ readonly expiresAt: number | undefined;
1014
+ readonly scope: AuthorizedPreviewScope;
1015
+ /** The minimal request material a draft read forwards to Payload — one verified cookie, never the whole `Cookie` header. */
1016
+ readonly payloadHeaders: Readonly<Record<string, string>>;
1017
+ }
1018
+ /** Whether `value` was produced by `authorizePreviewRequest()`; a copy or JSON round trip is not. */
1019
+ declare function isAuthorizedPreviewContext(value: unknown): value is AuthorizedPreviewContext;
1020
+
1411
1021
  /**
1412
- * The minimal request surface the predicate needs. The standard
1413
- * `Request` satisfies it structurally, and server frameworks without
1414
- * fetch-style requests (e.g. Nitro/H3) can supply a tiny shim.
1022
+ * The verdict shape every authorization strategy produces, and the one error
1023
+ * class that is allowed to escape it. Kept apart from the strategies so the
1024
+ * adapters can import the error without pulling in HMAC or session code.
1415
1025
  */
1416
- interface PreviewRequestLike {
1026
+
1027
+ /** Why a request was refused. `'authorized'` is the one non-refusal. */
1028
+ type PreviewAuthorizationOutcome = 'authorized' | 'missing-credential' | 'invalid' | 'expired' | 'wrong-audience' | 'wrong-path' | 'wrong-locale' | 'wrong-purpose' | 'replayed' | 'unavailable';
1029
+ /** The result of `authorizePreviewRequest()`: a verdict, never an exception. */
1030
+ type PreviewAuthorization = {
1031
+ readonly authorized: true;
1032
+ readonly outcome: 'authorized';
1033
+ readonly context: AuthorizedPreviewContext;
1034
+ } | {
1035
+ readonly authorized: false;
1036
+ readonly outcome: Exclude<PreviewAuthorizationOutcome, 'authorized'>;
1037
+ readonly context: null;
1038
+ };
1039
+ /** The request shape every strategy reads: a URL and a header getter. */
1040
+ interface PreviewAuthorizationRequest {
1417
1041
  readonly url: string;
1418
1042
  readonly headers: {
1419
1043
  get(name: string): string | null;
1420
1044
  };
1421
1045
  }
1422
- /**
1423
- * Detect whether `request` carries live-preview intent. See the module
1424
- * docblock for the exact signals and the required authorization
1425
- * boundary. This compatibility name does not imply authentication.
1426
- */
1427
- declare function isPreviewRequest(request: PreviewRequestLike, options?: PreviewRequestOptions): boolean;
1428
1046
 
1429
1047
  /**
1430
- * Draft-aware document fetching for the initial preview render.
1431
- *
1432
- * The live-preview runtime patches the DOM *after* the page loads —
1433
- * the initial server render is the consumer's job. With Payload
1434
- * drafts enabled, an **authorized** preview should render the draft
1435
- * version on first load, otherwise editors see stale published
1436
- * content until their first keystroke.
1437
- *
1438
- * These helpers wrap the corresponding REST queries with `draft` and
1439
- * `depth` options and an explicit nudge to keep `depth` in sync with
1440
- * the runtime's `mergeDepth` — a mismatch makes populated
1441
- * relationships degrade to IDs after the first edit.
1442
- *
1443
- * These helpers do not authenticate or authorize requests.
1444
- * `isPreviewRequest()` detects client-controlled preview intent only.
1445
- * Verify an application-owned server session or short-lived signed
1446
- * authorization first, and use that one decision to gate `draft`, any
1447
- * privileged headers, cache bypass, CSP changes, and runtime injection.
1448
- * Never attach a long-lived service/API key merely because
1449
- * `isPreviewRequest()` returned `true`.
1450
- *
1451
- * Isomorphic and dependency-free: works in Astro frontmatter,
1452
- * SvelteKit `load`, Next.js server components, or any Node/edge
1453
- * runtime with `fetch`.
1454
- *
1455
- * ```ts
1456
- * // Astro frontmatter. `verifyAppPreviewSession` is application-owned,
1457
- * // server-only code; it validates the request and returns only the
1458
- * // minimal Payload session headers needed for this request.
1459
- * const hasPreviewIntent = isPreviewRequest(Astro.request);
1460
- * const authorization = hasPreviewIntent
1461
- * ? await verifyAppPreviewSession(Astro.request)
1462
- * : null;
1463
- * const page = await fetchPreviewDocument<Page>({
1464
- * serverURL: import.meta.env.PAYLOAD_URL,
1465
- * collection: 'pages',
1466
- * where: { slug: { equals: Astro.params.slug } },
1467
- * draft: authorization !== null,
1468
- * ...(authorization === null ? {} : { headers: authorization.payloadHeaders }),
1469
- * });
1470
- * ```
1471
- *
1472
- * @module @preview-fetch
1048
+ * The `payload-session` strategy: forward exactly one named cookie to
1049
+ * `GET <serverURL>/api/<usersSlug>/me` and authorize when a user comes back.
1473
1050
  */
1474
- interface PreviewFetchBaseOptions {
1051
+
1052
+ /** A `fetch`-compatible function, injectable for tests. */
1053
+ type FetchLike = (input: string, init: {
1054
+ readonly headers: Record<string, string>;
1055
+ readonly signal: AbortSignal;
1056
+ }) => Promise<{
1057
+ readonly ok: boolean;
1058
+ readonly status: number;
1059
+ json(): Promise<unknown>;
1060
+ }>;
1061
+ interface PayloadSessionStrategy {
1062
+ readonly type: 'payload-session';
1475
1063
  /** Payload server origin, e.g. `https://cms.example.com`. */
1476
1064
  readonly serverURL: string;
1477
- /** REST API route prefix. Defaults to `/api`. */
1478
- readonly apiRoute?: string;
1479
- /**
1480
- * Population depth. Defaults to `1`. ⚠️ Keep this equal to the
1481
- * runtime's `mergeDepth` — the live-merge re-fetches with that
1482
- * depth, and a mismatch makes nested relationships flip between
1483
- * objects and IDs mid-session.
1484
- */
1485
- readonly depth?: number;
1486
- /**
1487
- * Fetch the draft version. Defaults to `true` for 1.x compatibility.
1488
- * Set this from a verified authorization decision, not directly
1489
- * from `isPreviewRequest()`, whose result only expresses intent.
1490
- */
1491
- readonly draft?: boolean;
1492
- /** Locale to fetch. */
1065
+ /** Auth collection slug; `users` unless the project renamed it. */
1066
+ readonly usersSlug?: string;
1067
+ /** Cookie name; Payload's default is `payload-token`. */
1068
+ readonly cookieName?: string;
1069
+ /** Upper bound for the `/me` round trip. Below 250 ms is raised to 250 ms; default 3000. */
1070
+ readonly timeoutMs?: number;
1071
+ /** Longest cookie value accepted; longer ones are refused as `invalid`. Default 4096. */
1072
+ readonly maxCookieLength?: number;
1073
+ readonly fetch?: FetchLike;
1074
+ /** Clock, Unix milliseconds. Injectable for tests. */
1075
+ readonly now?: () => number;
1076
+ }
1077
+
1078
+ /**
1079
+ * The `signed-token` strategy: a short-lived HMAC-SHA256 token minted by the
1080
+ * Payload side and verified by the site. Format and bindings: ADR 0006 §3.
1081
+ */
1082
+
1083
+ /** The subset of Web Crypto the token strategy needs, injectable for environments that expose it elsewhere. */
1084
+ interface SubtleCryptoLike {
1085
+ readonly subtle: {
1086
+ importKey(format: 'raw', keyData: Uint8Array, algorithm: {
1087
+ readonly name: 'HMAC';
1088
+ readonly hash: 'SHA-256';
1089
+ }, extractable: false, keyUsages: readonly ('sign' | 'verify')[]): Promise<CryptoKey>;
1090
+ sign(algorithm: 'HMAC', key: CryptoKey, data: Uint8Array): Promise<ArrayBuffer>;
1091
+ verify(algorithm: 'HMAC', key: CryptoKey, signature: Uint8Array, data: Uint8Array): Promise<boolean>;
1092
+ };
1093
+ getRandomValues<T extends Uint8Array>(array: T): T;
1094
+ }
1095
+ /** Where a signed token is read from on the request. */
1096
+ type PreviewTokenTransport = {
1097
+ readonly kind: 'query';
1098
+ readonly param?: string;
1099
+ } | {
1100
+ readonly kind: 'header';
1101
+ readonly name?: string;
1102
+ };
1103
+ /**
1104
+ * Optional replay store. Not shipped: a per-process memory is useless behind
1105
+ * a load balancer, so the store is a deployment decision.
1106
+ */
1107
+ interface PreviewTokenReplayStore {
1108
+ isUsed(id: string): Promise<boolean> | boolean;
1109
+ markUsed(id: string, expiresAt: number): Promise<void> | void;
1110
+ }
1111
+ interface SignedTokenStrategy {
1112
+ readonly type: 'signed-token';
1113
+ /** Shared secret, at least 32 bytes; shorter ones are refused on first use. */
1114
+ readonly secret: string | Uint8Array;
1115
+ /** The site origin tokens must name, e.g. `https://www.example.com`. */
1116
+ readonly audience: string;
1117
+ /** Purpose string tokens must carry. Default `live-preview`. */
1118
+ readonly purpose?: string;
1119
+ /** Default: query parameter `previewToken`. */
1120
+ readonly transport?: PreviewTokenTransport;
1121
+ /** Resolve the request's locale for the `loc` binding. Unset: tokens with `loc` are refused as `wrong-locale`. */
1122
+ readonly locale?: (request: PreviewAuthorizationRequest) => string | undefined;
1123
+ readonly replay?: PreviewTokenReplayStore;
1124
+ readonly crypto?: SubtleCryptoLike;
1125
+ readonly now?: () => number;
1126
+ }
1127
+ /** What `issuePreviewToken()` binds a token to. */
1128
+ interface PreviewTokenClaims {
1129
+ /** The site origin the token is for. */
1130
+ readonly audience: string;
1131
+ /** Request pathname the token opens. Recommended; omit only for a single-page preview. */
1132
+ readonly path?: string;
1493
1133
  readonly locale?: string;
1494
- /**
1495
- * Extra request headers. Supply only credentials derived from the
1496
- * current request's verified server-side session (or another
1497
- * short-lived, scoped authorization). Do not attach a long-lived
1498
- * service/API key based on a preview-intent signal.
1499
- */
1500
- readonly headers?: Readonly<Record<string, string>>;
1501
- /** Injectable fetch implementation (tests, custom agents). */
1502
- readonly fetchFn?: typeof fetch;
1503
- }
1504
- /** Payload `where` clause — kept loose on purpose (server validates). */
1505
- type PreviewWhere = Readonly<Record<string, unknown>>;
1506
- interface FetchPreviewDocumentOptions extends PreviewFetchBaseOptions {
1507
- /** Collection slug. */
1508
- readonly collection: string;
1509
- /** Fetch a single document by id. Mutually exclusive with `where`. */
1510
- readonly id?: string | number;
1511
- /**
1512
- * Fetch the first document matching this Payload `where` clause,
1513
- * e.g. `{ slug: { equals: 'about' } }`.
1514
- */
1515
- readonly where?: PreviewWhere;
1134
+ readonly subject?: string;
1135
+ readonly purpose?: string;
1136
+ /** Lifetime in milliseconds. Default ten minutes; capped at one hour. */
1137
+ readonly ttlMs?: number;
1516
1138
  }
1517
- interface FetchPreviewGlobalOptions extends PreviewFetchBaseOptions {
1518
- /** Global slug. */
1519
- readonly global: string;
1139
+ interface IssuePreviewTokenOptions {
1140
+ readonly secret: string | Uint8Array;
1141
+ readonly crypto?: SubtleCryptoLike;
1142
+ readonly now?: () => number;
1520
1143
  }
1144
+ /** Mint a preview token on the Payload side; pair with `buildLivePreviewUrl`. */
1145
+ declare function issuePreviewToken(claims: PreviewTokenClaims, options: IssuePreviewTokenOptions): Promise<string>;
1146
+
1521
1147
  /**
1522
- * Fetch a single collection document (draft-first by default for 1.x
1523
- * compatibility; callers must authorize draft access).
1524
- * Returns `null` when nothing matches or the request fails —
1525
- * loaders should fall back to their regular data path or a 404.
1148
+ * Authorized preview context: the one verdict every privileged preview
1149
+ * decision is keyed on. Intent (`?preview=true`, an iframe destination, a
1150
+ * referer) is client-chosen; this turns a session, a signed token or the
1151
+ * application's own verifier into a branded context. Threat model: ADR 0006.
1526
1152
  */
1527
- declare function fetchPreviewDocument<T = Record<string, unknown>>(options: FetchPreviewDocumentOptions): Promise<T | null>;
1153
+
1154
+ /** Claims a consumer verifier returns for a request it accepts. */
1155
+ interface PreviewVerifierClaims {
1156
+ readonly subject?: string;
1157
+ readonly expiresAt?: number;
1158
+ readonly scope?: AuthorizedPreviewScope;
1159
+ readonly payloadHeaders?: Readonly<Record<string, string>>;
1160
+ }
1161
+ /** The application's own verification: claims authorize, `null` refuses, a throw is `unavailable`. */
1162
+ interface VerifierStrategy {
1163
+ readonly type: 'verifier';
1164
+ readonly verify: (request: PreviewAuthorizationRequest) => Promise<PreviewVerifierClaims | null> | PreviewVerifierClaims | null;
1165
+ readonly now?: () => number;
1166
+ }
1167
+ type PreviewAuthorizationStrategy = PayloadSessionStrategy | SignedTokenStrategy | VerifierStrategy;
1168
+ /** Decide whether `request` is an authorized preview; refusals resolve to `{ authorized: false }` and only a `PreviewConfigurationError` throws. */
1169
+ declare function authorizePreviewRequest(request: PreviewAuthorizationRequest, strategy: PreviewAuthorizationStrategy): Promise<PreviewAuthorization>;
1170
+
1528
1171
  /**
1529
- * Fetch a global (draft-first by default for 1.x compatibility;
1530
- * callers must authorize draft access). Returns `null` on failure.
1172
+ * Trusted Types: every HTML sink routes through one policy, `payload-live-preview`,
1173
+ * whose `createHTML` is the identity — what reaches a sink is already sanitized
1174
+ * or escaped. Without the API, strings pass through untouched.
1531
1175
  */
1532
- declare function fetchPreviewGlobal<T = Record<string, unknown>>(options: FetchPreviewGlobalOptions): Promise<T | null>;
1176
+ /** The subset of the Trusted Types API this module needs. */
1177
+ interface TrustedHtmlPolicyLike {
1178
+ createHTML(input: string): unknown;
1179
+ }
1180
+ declare const TRUSTED_TYPES_POLICY_NAME = "payload-live-preview";
1181
+ /**
1182
+ * Use `policy` for every sink, `null` to assign plain strings, `undefined` to
1183
+ * return to the auto-created package policy.
1184
+ */
1185
+ declare function setTrustedTypesPolicy(policy: TrustedHtmlPolicyLike | null | undefined): void;
1186
+ /** `html` as a `TrustedHTML` when a policy exists, else the string. Typed `string` for `innerHTML`. */
1187
+ declare function trustedHtml(html: string): string;
1533
1188
 
1534
1189
  /**
1535
- * Generic Lexical node. Concrete renderers consume specific subtypes
1536
- * through narrow type guards in their own modules.
1190
+ * Server-side preview-intent detection. Every signal here is client-controlled:
1191
+ * a positive result is a delivery hint, never proof of identity or permission.
1192
+ * Verify with `authorizePreviewRequest()` before anything privileged.
1537
1193
  */
1194
+ /** `query`: `?preview=true`; `fetch-dest`: `Sec-Fetch-Dest: iframe`; `referer`: an admin-origin Referer. */
1195
+ type PreviewSignal = 'query' | 'fetch-dest' | 'referer';
1196
+ interface PreviewRequestOptions {
1197
+ /** Which signals count. Default: all three. `['query']` is the adapters' 2.0 default. */
1198
+ readonly signals?: readonly PreviewSignal[];
1199
+ /** Query parameters (value `true` or `1`) that signal intent. Default `['preview', 'draft', 'livePreview']`. */
1200
+ readonly queryParams?: readonly string[];
1201
+ /** Admin origins whose Referer signals intent, e.g. `['https://cms.example.com']`. */
1202
+ readonly adminOrigins?: readonly string[];
1203
+ }
1204
+ /** The request surface the predicate reads; `Request` satisfies it, Nitro/H3 events need a shim. */
1205
+ interface PreviewRequestLike {
1206
+ readonly url: string;
1207
+ readonly headers: {
1208
+ get(name: string): string | null;
1209
+ };
1210
+ }
1211
+ /** Whether `request` carries live-preview intent. Intent, not authorization. */
1212
+ declare function hasPreviewIntent(request: PreviewRequestLike, options?: PreviewRequestOptions): boolean;
1213
+
1538
1214
  interface LexicalNode {
1539
1215
  readonly type: string;
1540
1216
  readonly version?: number;
@@ -1545,9 +1221,6 @@ interface LexicalNode {
1545
1221
  readonly text?: string;
1546
1222
  readonly [extra: string]: unknown;
1547
1223
  }
1548
- /**
1549
- * Root of a Lexical document.
1550
- */
1551
1224
  interface LexicalRoot {
1552
1225
  readonly root: {
1553
1226
  readonly type?: string;
@@ -1560,107 +1233,44 @@ interface LexicalRoot {
1560
1233
  }
1561
1234
 
1562
1235
  /**
1563
- * Top-level Lexical → HTML renderer.
1564
- *
1565
- * Walks the document tree, dispatching each node to the renderer
1566
- * registered for its `type`. Unknown nodes are handled gracefully:
1567
- * children are rendered (so the user sees content), the wrapper is
1568
- * skipped.
1569
- *
1570
- * @module @lexical/render
1236
+ * Lexical → HTML. Each node dispatches to the renderer registered for its
1237
+ * `type`; an unknown node renders its children so content is never lost.
1571
1238
  */
1572
1239
 
1573
1240
  interface LexicalRenderOptions {
1574
- /**
1575
- * When `true` (default), the resulting HTML is passed through the
1576
- * isomorphic `sanitizeHtml()` for defence-in-depth.
1577
- */
1241
+ /** Pass the result through `sanitizeHtml()` (default `true`). */
1578
1242
  readonly sanitize?: boolean;
1579
1243
  }
1580
- /**
1581
- * Identify whether a value looks like a Lexical root payload.
1582
- */
1583
1244
  declare function isLexicalContent(value: unknown): value is LexicalRoot;
1584
- /**
1585
- * Convert a Lexical root payload to an HTML string. By default the
1586
- * result is sanitised through `sanitizeHtml`. The renderer is
1587
- * security-first; per-node renderers already escape text and validate
1588
- * URLs, but the final sanitiser closes any gap.
1589
- */
1245
+ /** Render a Lexical document to HTML; without `setSanitizerDocument()` the result is unsanitised and warns once. */
1590
1246
  declare function lexicalToHtml(content: LexicalRoot, options?: LexicalRenderOptions): string;
1591
- /**
1592
- * Extract the plain text content of a Lexical document.
1593
- *
1594
- * Paragraphs are separated by `\n`. Used by the text/textarea renderer
1595
- * when a rich-text value is bound to a non-rich-text element.
1596
- */
1247
+ /** Plain text of a document, paragraphs separated by `\n`. */
1597
1248
  declare function lexicalToPlainText(content: LexicalRoot): string;
1598
1249
 
1599
- /**
1600
- * Block-renderer sub-registry.
1601
- *
1602
- * Payload's BlocksFeature emits Lexical `block` nodes with a
1603
- * `blockType` slug. The default `block`-node renderer (in
1604
- * `../nodes/block.ts`) only emits a generic
1605
- * `<div data-block-type="…">` — useful as a hook, but unhelpful when
1606
- * the consumer wants out-of-the-box rendering for common block types.
1607
- *
1608
- * This registry maps `blockType` → renderer. Default renderers ship
1609
- * for the most common Payload block patterns (callout, image,
1610
- * video, code, cta). Custom slugs override defaults via
1611
- * `registerBlockRenderer(slug, renderer)`.
1612
- *
1613
- * @module @lexical/blocks/registry
1614
- */
1250
+ /** Block-renderer registry keyed by Payload block slug; consulted by the `block` and `inlineBlock` nodes. */
1615
1251
 
1616
- /**
1617
- * Context passed to a block renderer. Includes the recursive
1618
- * `renderChildren` function so blocks that house nested Lexical
1619
- * content can delegate back to the main renderer.
1620
- */
1621
1252
  interface BlockRenderContext {
1622
- /** Renders a list of Lexical children to HTML. */
1253
+ /** Render nested Lexical content through the node registry. */
1623
1254
  readonly renderChildren: (children: readonly LexicalNode[]) => string;
1624
1255
  }
1625
- /**
1626
- * Renderer signature. Receives the block node *and* its already-read
1627
- * `fields` payload (the parser pulls these out so individual block
1628
- * renderers don't have to reach into `node['fields']` themselves).
1629
- */
1256
+ /** Receives the block's `fields` (with `blockType`) and returns HTML. */
1630
1257
  type BlockRenderer = (fields: Record<string, unknown>, context: BlockRenderContext) => string;
1631
1258
  /** Register or replace the renderer for `blockType`. */
1632
1259
  declare function registerBlockRenderer(blockType: string, renderer: BlockRenderer): void;
1633
1260
 
1634
1261
  /**
1635
- * `highlight` plugin — flashes an outline on updated elements so the
1636
- * editor can see what changed.
1637
- *
1638
- * Respects `prefers-reduced-motion`. Adds at most one style tag per
1639
- * document.
1640
- *
1641
- * @module @plugins/built-in/highlight
1262
+ * `highlight` plugin: flashes an outline on updated elements. Respects
1263
+ * `prefers-reduced-motion`; the stylesheet is shared per document across
1264
+ * clients through a lease (ADR 0002).
1642
1265
  */
1643
1266
 
1644
1267
  declare const highlightPlugin: LivePreviewPlugin;
1645
1268
 
1646
- /**
1647
- * `debug` plugin — logs every lifecycle event via the plugin context.
1648
- *
1649
- * @module @plugins/built-in/debug
1650
- */
1269
+ /** `debug` plugin: logs every lifecycle event through the plugin context. */
1651
1270
 
1652
1271
  declare const debugPlugin: LivePreviewPlugin;
1653
1272
 
1654
- /**
1655
- * `analytics` plugin — accumulates summary statistics about the
1656
- * update stream. Useful for performance dashboards.
1657
- *
1658
- * The plugin owns its own state (no module singleton) and exposes it
1659
- * via the `getStats` factory passed through the plugin's `init` hook
1660
- * by closing over a local object.
1661
- *
1662
- * @module @plugins/built-in/analytics
1663
- */
1273
+ /** `analytics` plugin: running totals over the update stream, read through `getStats()`. */
1664
1274
 
1665
1275
  interface AnalyticsSnapshot {
1666
1276
  readonly updateCount: number;
@@ -1673,281 +1283,100 @@ declare function createAnalyticsPlugin(): LivePreviewPlugin & {
1673
1283
  };
1674
1284
 
1675
1285
  /**
1676
- * `documentSave` plugin — react to Payload document-save events with
1677
- * out-of-the-box strategies. Most consumers just want one of these:
1678
- *
1679
- * - `'silent'` (default) → only emit the event, do nothing else.
1680
- * - `'reload'` → reload the page, **preserving the scroll
1681
- * position** across the reload (the
1682
- * framework-agnostic analog of Payload's
1683
- * `RefreshRouteOnSave`).
1684
- * - `'revalidate'` → POST to a revalidation endpoint (Astro,
1685
- * Next.js convention) so SSR caches refresh
1686
- * without losing client state.
1687
- * - `'fetch'` → custom async handler supplied by the user.
1688
- *
1689
- * Each strategy is intentionally narrow so consumers can compose them
1690
- * — e.g. revalidate, then if revalidation fails, fall back to reload.
1691
- *
1692
- * @module @plugins/built-in/document-save
1286
+ * `documentSave` plugin: what to do when Payload saves the document.
1287
+ * `'silent'` (default) emits only; `'reload'` reloads and restores the scroll
1288
+ * position; `'revalidate'` POSTs to a revalidation endpoint; `'fetch'` runs
1289
+ * a custom handler.
1693
1290
  */
1694
1291
 
1695
1292
  type DocumentSaveStrategy = 'silent' | 'reload' | 'revalidate' | 'fetch';
1696
- /**
1697
- * A fetch strategy handler. The optional parameter preserves calls through
1698
- * pre-1.0.4 option objects (`options.handler?.()`), while method bivariance
1699
- * also accepts explicitly signal-aware handlers. The built-in plugin always
1700
- * supplies a live `AbortSignal`.
1701
- */
1293
+ /** Handler for the `fetch` strategy; the signal aborts when the plugin is removed. */
1702
1294
  type DocumentSaveHandler = {
1703
1295
  bivarianceHack(signal?: AbortSignal): void | Promise<void>;
1704
1296
  }['bivarianceHack'];
1705
1297
  interface DocumentSavePluginOptions {
1706
- /** Strategy to invoke on each `documentSave` event. Default `'silent'`. */
1298
+ /** Default `'silent'`. */
1707
1299
  readonly strategy?: DocumentSaveStrategy;
1708
1300
  /**
1709
- * For `'revalidate'`: endpoint to POST to. Defaults to `/api/revalidate`.
1710
- * The request body is `{ source: 'payload-live-preview' }` JSON.
1301
+ * `'revalidate'` endpoint, default `/api/revalidate`; the body is
1302
+ * `{ source: 'payload-live-preview' }`. The request is a plain `fetch` from
1303
+ * the preview page: cookies travel with it, there is no CSRF token, and a
1304
+ * bearer secret in `revalidateHeaders` would sit in page JavaScript. The
1305
+ * endpoint must authorise on its own (session cookie, same-origin check).
1711
1306
  */
1712
1307
  readonly revalidateUrl?: string;
1713
- /**
1714
- * For `'revalidate'`: extra headers (e.g., `Authorization: Bearer …`).
1715
- * The `Content-Type: application/json` header is set automatically.
1716
- */
1308
+ /** Extra request headers for `'revalidate'`; `Content-Type` is set already. */
1717
1309
  readonly revalidateHeaders?: Readonly<Record<string, string>>;
1718
- /**
1719
- * For `'fetch'`: async handler called on each event. The plugin
1720
- * awaits it; rejections are logged but don't crash the runtime. The signal
1721
- * aborts when the plugin is removed. Existing zero-argument handlers remain
1722
- * compatible because they may ignore the argument.
1723
- */
1310
+ /** `'fetch'` handler; rejections are logged, never thrown. */
1724
1311
  readonly handler?: DocumentSaveHandler;
1725
- /**
1726
- * Optional fallback strategy when `'revalidate'` POST fails (network
1727
- * error or non-2xx). Set to `'reload'` to hard-refresh the page when
1728
- * SSR-cache invalidation can't be confirmed.
1729
- */
1312
+ /** `'reload'` hard-refreshes when the revalidate request fails or returns non-2xx. */
1730
1313
  readonly onRevalidateFailure?: 'silent' | 'reload';
1731
1314
  }
1732
- /**
1733
- * Build the plugin. Pass it to `client.use(documentSavePlugin({ … }))`.
1734
- */
1735
1315
  declare function documentSavePlugin(options?: DocumentSavePluginOptions): LivePreviewPlugin;
1736
1316
 
1737
1317
  /**
1738
- * Environment introspection helpers.
1739
- *
1740
- * The detection layer needs to know whether the runtime is currently
1741
- * running inside an iframe, in a popup, in development mode, and
1742
- * whether environment variables are accessible. None of these checks
1743
- * is dramatic on its own, but having a single, tested module avoids
1744
- * the duplicated, subtly-different `typeof process` snippets that
1745
- * accumulated in the legacy implementation.
1746
- *
1747
- * @module @detection/environment
1748
- */
1749
- /**
1750
- * Returns `true` when the current window is inside an iframe.
1751
- *
1752
- * Uses a try/catch around `window.self !== window.top` because the
1753
- * comparison throws in cross-origin iframes — and that very throw is
1754
- * itself a definitive "yes, I am in an iframe" signal.
1318
+ * Where the runtime is running: framed, opened, or on a developer's machine.
1319
+ * Nothing reads bundler-injected variables; a Vite consumer passes
1320
+ * `import.meta.env.X` explicitly (docs/astro.md).
1755
1321
  */
1756
1322
  declare function isInIframe(): boolean;
1757
- /**
1758
- * Returns `true` when the current window was opened via `window.open()`
1759
- * and the opener reference is still accessible.
1760
- */
1761
1323
  declare function isInPopup(): boolean;
1762
- /**
1763
- * Returns `true` when running in a preview-capable context: iframe or
1764
- * popup. Anything else (top-level navigation) should skip live preview
1765
- * initialisation entirely.
1766
- */
1324
+ /** An iframe or a popup; top-level navigation never starts the preview. */
1767
1325
  declare function isInPreviewContext(): boolean;
1768
- /**
1769
- * Returns `true` when the runtime appears to be in development mode.
1770
- *
1771
- * Detection order:
1772
- * 1. `process.env.NODE_ENV !== 'production'`
1773
- * 2. `import.meta.env.DEV === true` (Vite / Astro / SvelteKit)
1774
- * 3. `window.location.hostname` is `localhost`/`127.0.0.1`
1775
- *
1776
- * Returns `false` when none of those are available.
1777
- */
1326
+ /** Under Node, `NODE_ENV !== 'production'`; in a browser the hostname, which is the only signal a bundle has. */
1778
1327
  declare function isDevMode(): boolean;
1779
1328
 
1780
1329
  /**
1781
- * Origin detection and matching.
1782
- *
1783
- * Builds the set of trusted Payload admin origins and exposes a fast
1784
- * matcher used by the message bus. Three sources, in precedence order:
1785
- *
1786
- * 1. Explicit origins passed via configuration. Always trusted.
1787
- * 2. `document.referrer` — **only when no explicit origins are
1788
- * configured**. The browser writes the embedder's origin here
1789
- * when the iframe loads; using it as a fallback gives a
1790
- * zero-config path, but it must never widen an explicit
1791
- * allow-list (the referrer is whoever framed the page, which in
1792
- * an attack is the attacker).
1793
- * 3. Localhost pattern (any port on `localhost` / `127.0.0.1`),
1794
- * enabled when development mode is detected. Replaces the
1795
- * legacy hand-rolled port list.
1796
- *
1797
- * Once the parent's origin is confirmed by an accepted data-bearing
1798
- * update, callers can call `lockOrigin()` to drop every other candidate,
1799
- * making subsequent matches exact. A data-less `ready` handshake does
1800
- * not lock the detector.
1801
- *
1802
- * @module @detection/origin
1330
+ * Which admin origins the runtime trusts: explicit ones always, the
1331
+ * `document.referrer` origin only when none is configured, any localhost port
1332
+ * in development. See docs/security.md §1.
1803
1333
  */
1804
1334
  interface OriginDetectorOptions {
1805
- /** Explicit additional origins to trust. Always allowed. */
1335
+ /** Explicit origins to trust. Always allowed. */
1806
1336
  readonly additionalOrigins?: readonly string[];
1807
- /** Set to `false` to opt out of `document.referrer` detection. */
1337
+ /** Set to `false` to ignore `document.referrer`. */
1808
1338
  readonly enableReferrerDetection?: boolean;
1809
- /** Set to `false` to opt out of localhost-pattern matching. */
1339
+ /** Set to `false` to ignore the localhost pattern even in development. */
1810
1340
  readonly enableLocalhostMatching?: boolean;
1811
- /**
1812
- * Override the dev-mode detection. When `undefined`, dev mode is
1813
- * inferred from the runtime (see `isDevMode`).
1814
- */
1341
+ /** Override dev-mode detection. */
1815
1342
  readonly forceDevMode?: boolean;
1816
1343
  /** Override `document.referrer` for tests. */
1817
1344
  readonly referrer?: string;
1818
1345
  }
1819
- /**
1820
- * Holds the resolved origin policy and exposes a matcher.
1821
- *
1822
- * The detector is stateful: it starts with the union of all candidate
1823
- * sources, then narrows down to a single, verified origin once the
1824
- * lifecycle calls `lockOrigin()` for its first accepted data-bearing
1825
- * update.
1826
- */
1346
+ /** The trusted-origin policy: the union of the candidate sources, narrowed to one by `lockOrigin()`. */
1827
1347
  declare class OriginDetector {
1828
1348
  #private;
1829
1349
  constructor(options?: OriginDetectorOptions);
1830
- /**
1831
- * Returns `true` when `origin` is trusted under the current policy.
1832
- *
1833
- * If an origin has been locked, only that exact value matches.
1834
- * Otherwise explicit origins and (in dev mode) localhost-pattern
1835
- * matches are accepted. The referrer-derived origin is a **fallback,
1836
- * not a union member**: it only counts when NO explicit origins are
1837
- * configured. `document.referrer` always names whoever actually
1838
- * framed the page — if it were unioned with the explicit list, any
1839
- * site embedding the page in an iframe would become a trusted
1840
- * postMessage sender despite the consumer having pinned the admin
1841
- * origin explicitly.
1842
- */
1350
+ /** Whether `origin` is trusted: the locked origin alone once locked, otherwise the policy above. */
1843
1351
  matches(origin: string): boolean;
1844
- /**
1845
- * Narrow the trusted set to a single origin after an accepted
1846
- * data-bearing update. Subsequent calls to `matches()` only allow
1847
- * `origin`.
1848
- *
1849
- * Returns `true` if locking succeeded; `false` if the candidate is
1850
- * not currently considered trusted (which would defeat the purpose
1851
- * of locking).
1852
- */
1352
+ /** Narrow trust to `origin`; refused (`false`) when it is not currently trusted. */
1853
1353
  lockOrigin(origin: string): boolean;
1854
- /**
1855
- * Release the origin lock so any allow-listed origin is trusted
1856
- * again until the next accepted data-bearing update. The lifecycle
1857
- * calls this on heartbeat timeout — after the trusted parent disappears,
1858
- * a different (still-allowed) origin should be free to reconnect.
1859
- *
1860
- * Returns the previously-locked origin, or `undefined` when nothing
1861
- * was locked.
1862
- */
1354
+ /** Release the lock (heartbeat timeout) so another allow-listed origin may reconnect. Returns the previous lock. */
1863
1355
  unlockOrigin(): string | undefined;
1864
- /** The currently locked origin, or `undefined`. */
1865
1356
  get lockedOrigin(): string | undefined;
1866
- /**
1867
- * Snapshot of every origin currently allowed by the policy. Used by
1868
- * the message bus to broadcast `ready` messages.
1869
- *
1870
- * Localhost pattern matches are expanded into the common dev ports
1871
- * so the handshake can find a Payload running on its usual port.
1872
- */
1357
+ /** Every origin the `ready` handshake is broadcast to; the localhost pattern expands to the common dev ports. */
1873
1358
  enumerate(): string[];
1874
- /**
1875
- * Reports whether `document.referrer` contributed at least one
1876
- * origin. Useful for surfacing a console warning when running in
1877
- * production without an explicit origin and the referrer is absent
1878
- * (because of a `Referrer-Policy: no-referrer` header).
1879
- */
1359
+ /** Whether `document.referrer` contributed an origin (absent under `Referrer-Policy: no-referrer`). */
1880
1360
  get referrerWasAvailable(): boolean;
1881
- /**
1882
- * Indicates whether the detector is operating in a "production-without-
1883
- * explicit-origin" configuration. When `true`, the host should log a
1884
- * loud warning — silent connection failure is the worst outcome.
1885
- */
1361
+ /** Framed, outside development, with no trusted origin at all: the host should warn (LP0101). */
1886
1362
  get isProductionUnconfigured(): boolean;
1887
- /**
1888
- * `true` when the referrer is the ONLY trust source (no explicit
1889
- * origins, no localhost matching). In that mode any site that frames
1890
- * the page becomes a trusted postMessage sender — `document.referrer`
1891
- * always names the actual embedder. Content is still sanitized, but
1892
- * the origin allow-list no longer restricts who may drive it. Hosts
1893
- * should surface a warning suggesting explicit `allowedOrigins` and
1894
- * a `frame-ancestors` policy in production.
1895
- */
1363
+ /** The referrer is the only trust source, so any framing site may drive the preview (LP0102). */
1896
1364
  get isReferrerOnlyTrust(): boolean;
1897
1365
  }
1898
1366
 
1899
- /**
1900
- * Locale detection.
1901
- *
1902
- * The runtime needs a sensible default locale for date/number renders
1903
- * before Payload tells us the active locale. Order of precedence:
1904
- *
1905
- * 1. The `lang` attribute on `<html>`.
1906
- * 2. `navigator.language`.
1907
- * 3. The hard-coded fallback `'en'`.
1908
- *
1909
- * The previous version of this library defaulted to `'de'` — a German
1910
- * default that surprised consumers in other locales. We now use the
1911
- * browser hint and fall back to English.
1912
- *
1913
- * @module @detection/locale
1914
- */
1915
- /**
1916
- * Returns a BCP-47 locale string suitable for `Intl.*` constructors.
1917
- *
1918
- * Always returns a non-empty string. When no signal is available the
1919
- * fallback (`'en'`) is returned.
1920
- */
1367
+ /** The locale for `Intl` renders before Payload names one: `<html lang>`, then `navigator.language`, then `'en'`. */
1921
1368
  declare function detectInitialLocale(): string;
1922
1369
 
1923
1370
  /**
1924
- * Field-renderer registry.
1925
- *
1926
- * Maps a `FieldType` to the function that mutates a DOM element with
1927
- * the new value. The registry pattern decouples the lifecycle from the
1928
- * concrete renderers, so:
1929
- *
1930
- * - Tests can substitute mock renderers.
1931
- * - Plugins can register custom renderers via the high-level client.
1932
- * - New field types ship without touching `lifecycle.ts`.
1933
- *
1934
- * The explicit aggregator in `@field-types/index` supplies the default
1935
- * renderers as values, so it remains tree-shaking-safe without module import
1936
- * side effects. This registry stores only explicit consumer overrides.
1937
- *
1938
- * @module @field-types/registry
1371
+ * Explicit renderer overrides, layered above the defaults each time a map is
1372
+ * built. The registry never retains the defaults, so stateful ones can be
1373
+ * recreated per client.
1939
1374
  */
1940
1375
 
1941
- /**
1942
- * Register or replace an explicit renderer override for future snapshots.
1943
- */
1376
+ /** Register or replace an override for maps built afterwards. */
1944
1377
  declare function registerBuiltinRenderer(renderer: FieldRenderer): void;
1945
1378
 
1946
- /**
1947
- * Shared shapes for the field-type renderers.
1948
- *
1949
- * @module @field-types/types
1950
- */
1379
+ /** Populated value shapes the field renderers read. */
1951
1380
  interface PayloadMedia {
1952
1381
  readonly id?: string | number;
1953
1382
  readonly url?: string;
@@ -1962,13 +1391,7 @@ interface PayloadMedia {
1962
1391
  readonly height?: number;
1963
1392
  }>>;
1964
1393
  }
1965
- /**
1966
- * Generic relationship payload. The optional `TSlug` parameter is a
1967
- * phantom marker that codegen uses to record the target collection(s)
1968
- * without changing the runtime shape — `PayloadRelationship<'users'>`
1969
- * and `PayloadRelationship<'users' | 'authors'>` behave identically at
1970
- * runtime but carry different types at compile time.
1971
- */
1394
+ /** `TSlug` is a phantom marker codegen uses to record the target collection; the runtime shape is identical. */
1972
1395
  interface PayloadRelationship<TSlug extends string = string> {
1973
1396
  readonly id?: string | number;
1974
1397
  readonly title?: string;
@@ -1979,145 +1402,137 @@ interface PayloadRelationship<TSlug extends string = string> {
1979
1402
  readonly [extra: string]: unknown;
1980
1403
  }
1981
1404
 
1982
- /**
1983
- * Public barrel for built-in field renderers.
1984
- *
1985
- * Each renderer is imported as a named binding (not a side-effect)
1986
- * and assembled into the registry through `buildBuiltinRenderers`.
1987
- * The bundler keeps every reference reachable, so the registry is
1988
- * fully populated at runtime even under `sideEffects: false`.
1989
- *
1990
- * @module @field-types
1991
- */
1405
+ /** Built-in field renderers, assembled as values so `sideEffects: false` bundlers keep them all. */
1992
1406
 
1993
- /**
1994
- * Built-in renderer map. Called once per client/runtime; the
1995
- * `structural-array` renderer gets a fresh instance with its own diff
1996
- * state so nothing is shared between concurrent clients.
1997
- */
1407
+ /** One renderer map per client; the stateful text and structural renderers are created fresh each call. */
1998
1408
  declare function buildBuiltinRenderers(): Readonly<Record<string, FieldRenderer>>;
1999
1409
 
2000
1410
  /**
2001
- * Protocol-version negotiation.
2002
- *
2003
- * ⚠️ Reality check: **stock Payload does not version its live-preview
2004
- * postMessage protocol.** The admin never sends a `protocolVersion`
2005
- * field and ignores extra fields in our `ready` handshake. Observed
2006
- * wire behaviour:
2007
- *
2008
- * - Payload **2.x** sent `fieldSchemaJSON` with the first update
2009
- * (client-side merge era).
2010
- * - Payload **3.x** removed `fieldSchemaJSON` entirely; the official
2011
- * client re-fetches merged documents through the REST API instead.
2012
- * - No Payload version sends `previewToken` or any keepalive.
2013
- *
2014
- * This module therefore only matters for NON-stock admin panels — a
2015
- * custom `window.postMessage` sender can advertise a version and this
2016
- * library will negotiate `min(ours, theirs)` and expose capability
2017
- * flags. Against stock Payload, `theirs` stays `undefined`, negotiation
2018
- * collapses to v1, and nothing is gated on it: schema handling
2019
- * activates whenever `fieldSchemaJSON` is actually present, and token
2020
- * validation only runs when the consumer opts in.
2021
- *
2022
- * Version ladder (this library's own numbering, not Payload's):
2023
- *
2024
- * - **v1** — data + slug + locale (matches stock Payload of any era).
2025
- * - **v2** — remote sends `fieldSchemaJSON` (Payload 2.x behaviour).
2026
- * - **v3** — remote sends `previewToken` (custom senders only).
2027
- * - **v4** — nested-array recursion / richer document events
2028
- * (custom senders only).
2029
- *
2030
- * @module @core/protocol-version
1411
+ * Protocol negotiation. A capability is granted either by the version a peer
1412
+ * announces or by observation, since the stock Payload admin announces none:
1413
+ * what it can do is read off the messages it sends. See ADR 0010.
2031
1414
  */
1415
+ /** Bumped only when this library's understanding of the wire format changes. */
2032
1416
  declare const LIBRARY_PROTOCOL_VERSION = 4;
2033
- /** Capability flag → minimum protocol version it requires. */
2034
- declare const CAPABILITY_REQUIREMENTS: Readonly<Record<string, number>>;
2035
- type ProtocolCapability = keyof typeof CAPABILITY_REQUIREMENTS;
1417
+ /** How a capability becomes active. */
1418
+ type CapabilitySource = 'version' | 'observed';
1419
+ interface CapabilityDeclaration {
1420
+ /** Enabled once the negotiated version reaches this (for announced peers). */
1421
+ readonly since: number;
1422
+ /** The wire signal that grants this capability to a peer announcing no version. */
1423
+ readonly observed?: 'schema' | 'locale' | 'preview-token' | 'document-event' | 'relationship-event';
1424
+ }
1425
+ /** What activates each capability; the prose lives in `CAPABILITY_DOCUMENTATION`. */
1426
+ declare const CAPABILITY_DECLARATIONS: {
1427
+ readonly basic: {
1428
+ readonly since: 1;
1429
+ };
1430
+ readonly 'schema-json': {
1431
+ readonly since: 2;
1432
+ readonly observed: "schema";
1433
+ };
1434
+ readonly locale: {
1435
+ readonly since: 2;
1436
+ readonly observed: "locale";
1437
+ };
1438
+ readonly 'preview-token': {
1439
+ readonly since: 3;
1440
+ readonly observed: "preview-token";
1441
+ };
1442
+ readonly 'nested-arrays': {
1443
+ readonly since: 4;
1444
+ };
1445
+ readonly 'recursive-diffs': {
1446
+ readonly since: 4;
1447
+ };
1448
+ readonly 'document-events': {
1449
+ readonly since: 4;
1450
+ readonly observed: "document-event";
1451
+ };
1452
+ readonly 'relationship-events': {
1453
+ readonly since: 4;
1454
+ readonly observed: "relationship-event";
1455
+ };
1456
+ };
1457
+ type ProtocolCapability = keyof typeof CAPABILITY_DECLARATIONS;
1458
+ /** Every declared capability, in declaration order. */
1459
+ declare const PROTOCOL_CAPABILITIES: readonly ProtocolCapability[];
2036
1460
  interface ProtocolNegotiation {
2037
1461
  /** This library's protocol version. */
2038
1462
  readonly ours: number;
2039
- /** The remote party's version, when known. */
1463
+ /** The remote party's version, when it announced one. */
2040
1464
  readonly theirs: number | undefined;
2041
1465
  /** `min(ours, theirs ?? 1)` — the version both sides actually share. */
2042
1466
  readonly negotiated: number;
2043
- /** Capability flags enabled at the negotiated version. */
2044
- readonly capabilities: ReadonlySet<string>;
1467
+ /** Capabilities active at the negotiated version or seen on the wire. */
1468
+ readonly capabilities: ReadonlySet<ProtocolCapability>;
1469
+ /** The subset that was observed on the wire rather than granted by version. */
1470
+ readonly observed: ReadonlySet<ProtocolCapability>;
2045
1471
  }
2046
- /**
2047
- * Compute the negotiated view of the protocol.
2048
- *
2049
- * `theirs === undefined` collapses to v1 — assume the remote party is
2050
- * an older Payload that does not advertise a version.
2051
- *
2052
- * Non-finite or non-positive `theirs` values fall back to v1 as well —
2053
- * we never trust an obviously-corrupted negotiation byte.
2054
- */
2055
- declare function negotiateProtocol(theirs: number | undefined): ProtocolNegotiation;
1472
+ /** Deterministic; the runtime re-runs it whenever either input changes. */
1473
+ declare function negotiateProtocol(theirs: number | undefined, observed?: Iterable<ProtocolCapability>): ProtocolNegotiation;
2056
1474
  declare function hasCapability(negotiation: ProtocolNegotiation, capability: ProtocolCapability): boolean;
2057
1475
 
2058
1476
  /**
2059
- * Type-level path utilities — used by the typed binding DSL to give
2060
- * callers compile-time autocomplete on field names.
2061
- *
2062
- * The same path strings the runtime reads from `data-payload-field`
2063
- * are typed here, so a typo like `'heroTitlee'` is rejected at compile
2064
- * time *and* the runtime side does not need to change.
2065
- *
2066
- * @module @dsl/paths
1477
+ * What each capability tells the runtime, for `inspect()` and consumer
1478
+ * diagnostics. Kept out of the runtime module so the inline bundle ships the
1479
+ * rules and not this prose. See ADR 0010.
2067
1480
  */
1481
+
1482
+ interface CapabilityDocumentation {
1483
+ /** The runtime behaviour that depends on the capability. */
1484
+ readonly gates: string;
1485
+ /** What the runtime does without it. */
1486
+ readonly fallback: string;
1487
+ }
1488
+ declare const CAPABILITY_DOCUMENTATION: Readonly<Record<ProtocolCapability, CapabilityDocumentation>>;
1489
+
2068
1490
  /**
2069
- * All string-typed top-level keys of `T`. Non-string keys (numeric
2070
- * indices on arrays) are filtered out.
1491
+ * What the peer does, derived from the capabilities its messages showed —
1492
+ * the runtime never asks which Payload version it is talking to. Unknown
1493
+ * until the admin reveals its shape. See ADR 0010.
2071
1494
  */
2072
- type FieldName<T> = Extract<keyof T, string>;
1495
+
1496
+ type ProtocolProfileName = 'unknown' | 'payload-2' | 'payload-3';
1497
+ interface ProtocolProfile {
1498
+ readonly name: ProtocolProfileName;
1499
+ /** Where field types come from. */
1500
+ readonly fieldTyping: 'schema' | 'heuristic';
1501
+ /**
1502
+ * Who populates relationships. Payload 2.x's admin posts populated data
1503
+ * (it merges client-side with `fieldSchemaJSON`); Payload 3.x posts raw
1504
+ * form values and the runtime asks the REST API when `serverURL` is set.
1505
+ */
1506
+ readonly populatesRelationships: 'admin' | 'server';
1507
+ /** Whether the peer reports document saves and relationship edits. */
1508
+ readonly documentEvents: boolean;
1509
+ }
2073
1510
  /**
2074
- * Dotted paths into nested object structures, capped at three levels
2075
- * deep — sufficient for Payload's `group`/`tab`/`array` nesting
2076
- * without ballooning the IDE's IntelliSense list past the point of
2077
- * usefulness.
1511
+ * `schema-json` on the wire is the Payload 2.x signature: 3.x removed
1512
+ * `fieldSchemaJSON`. Document or relationship events are 3.x-only. Until
1513
+ * either shows, the peer is unknown and treated like 3.x for merging — the
1514
+ * conservative choice, because a needless REST merge costs a request while
1515
+ * a missing one loses populated relationships.
2078
1516
  */
1517
+ declare function detectProtocolProfile(observed: ReadonlySet<ProtocolCapability>): ProtocolProfile;
1518
+
1519
+ /** Type-level field paths, so a binding name is checked at compile time against the codegen schema. */
1520
+ /** String keys of `T`. */
1521
+ type FieldName<T> = Extract<keyof T, string>;
1522
+ /** Dotted paths into `T`, capped at three levels so IntelliSense stays usable. */
2079
1523
  type FieldPath<T, Depth extends 0 | 1 | 2 | 3 = 3> = Depth extends 0 ? never : T extends readonly (infer U)[] ? FieldPath<U, Prev<Depth>> : T extends object ? {
2080
1524
  [K in Extract<keyof T, string>]: K | (T[K] extends object ? `${K}.${FieldPath<T[K], Prev<Depth>>}` : never);
2081
1525
  }[Extract<keyof T, string>] : never;
2082
- /** Decrement helper for depth-bounded recursion. */
2083
1526
  type Prev<N extends 0 | 1 | 2 | 3> = N extends 3 ? 2 : N extends 2 ? 1 : N extends 1 ? 0 : 0;
2084
- /**
2085
- * Resolve the value type at a dotted path. Returns `unknown` when the
2086
- * path does not exist on `T`, so consumers can opt into stricter
2087
- * `keyof`-based DSLs when they want absolute type safety.
2088
- */
1527
+ /** The value type at a dotted path, or `unknown` when the path does not exist. */
2089
1528
  type ValueAt<T, P extends string> = P extends `${infer Head}.${infer Rest}` ? Head extends keyof T ? T[Head] extends readonly (infer U)[] ? ValueAt<U, Rest> : T[Head] extends object ? ValueAt<T[Head], Rest> : unknown : unknown : P extends keyof T ? T[P] : unknown;
2090
1529
 
2091
1530
  /**
2092
- * Typed binding helpers — emit the `data-payload-field` attribute pair
2093
- * that the live-preview runtime reads, with compile-time validation
2094
- * of the field name.
2095
- *
2096
- * Two forms are exposed:
2097
- *
2098
- * - **`bind<Schema>('heroTitle')`** — string-literal form. Lightest
2099
- * possible runtime (no Proxy), but a rename in the IDE does NOT
2100
- * follow into the string.
2101
- * - **`bindByPath<Schema>(s => s.heroTitle)`** — proxy form. The
2102
- * callback walks a Proxy that records the access path, returning
2103
- * the same shape as `bind` so the two are interchangeable. Slightly
2104
- * heavier runtime, but rename-safe and discourages typos.
2105
- *
2106
- * Both helpers also accept a richer return — `bind('image', 'src')` —
2107
- * to bind an attribute other than the default `data-payload-field`,
2108
- * matching the cache resolver's `data-payload-attribute` flow.
2109
- *
2110
- * @module @dsl/bind
1531
+ * Typed helpers emitting the `data-payload-*` attributes the runtime reads.
1532
+ * `bindByPath` records the path through a Proxy, so a rename follows.
2111
1533
  */
2112
1534
 
2113
- /**
2114
- * Shape returned by every binding helper — a spreadable HTML attribute
2115
- * record. Astro / JSX / Svelte all accept this style directly:
2116
- *
2117
- * ```astro
2118
- * <h1 {...bind<Homepage>('heroTitle')}>{data.heroTitle}</h1>
2119
- * ```
2120
- */
1535
+ /** A spreadable attribute record: `<h1 {...bind<Homepage>('heroTitle')}>`. */
2121
1536
  interface FieldBindingAttributes {
2122
1537
  readonly 'data-payload-field': string;
2123
1538
  readonly 'data-payload-attribute'?: string;
@@ -2125,139 +1540,78 @@ interface FieldBindingAttributes {
2125
1540
  readonly 'data-payload-richtext'?: string;
2126
1541
  readonly 'data-payload-html'?: string;
2127
1542
  readonly 'data-payload-locale'?: string;
1543
+ readonly 'data-payload-alt'?: string;
1544
+ readonly 'data-payload-href'?: string;
1545
+ readonly 'data-payload-array-template'?: string;
2128
1546
  }
2129
1547
  interface BindOptions {
2130
- /** Override the bound attribute (e.g., `'src'` for `<img>`). Default: textContent. */
1548
+ /** Attribute to write instead of the text content, e.g. `'src'` for `<img>`. */
2131
1549
  readonly attribute?: string;
2132
- /** Explicit field-type override — bypasses schema detection. */
1550
+ /** Explicit field type, bypassing schema detection. */
2133
1551
  readonly type?: string;
2134
- /**
2135
- * Mark the binding as Lexical rich text. Usually detected automatically;
2136
- * set it where the initial render is empty and there is nothing to detect.
2137
- */
1552
+ /** Mark the binding as Lexical rich text; needed only when the initial render is empty. */
2138
1553
  readonly richtext?: boolean;
2139
1554
  /** Render the value as sanitised HTML rather than text. */
2140
1555
  readonly html?: boolean;
2141
1556
  /** Lock this element to one locale, overriding the message locale. */
2142
1557
  readonly locale?: string;
1558
+ /** Field whose value becomes the image `alt`. */
1559
+ readonly alt?: string;
1560
+ /** Field whose value becomes the link `href`. */
1561
+ readonly href?: string;
1562
+ /** Markup per array item with `{{value}}` placeholders. */
1563
+ readonly arrayTemplate?: string;
2143
1564
  }
2144
- /**
2145
- * String-literal binding helper.
2146
- *
2147
- * ```ts
2148
- * import { bind } from 'payload-live-preview';
2149
- * import type { Homepage } from './payload-types';
2150
- *
2151
- * <h1 {...bind<Homepage>('heroTitle')}>{data.heroTitle}</h1>
2152
- * <img {...bind<Homepage>('heroImage', { attribute: 'src' })} />
2153
- * ```
2154
- *
2155
- * The generic `T` is the schema interface emitted by `pll-codegen`.
2156
- * `field` is constrained to `keyof T` so misspellings fail at compile
2157
- * time. When `T` is omitted, the helper accepts any string — useful
2158
- * for incremental adoption.
2159
- */
1565
+ /** `bind<Homepage>('heroTitle')`; without `T` any string is accepted. */
2160
1566
  declare function bind<T = Record<string, unknown>>(field: FieldName<T>, options?: BindOptions): FieldBindingAttributes;
2161
- /**
2162
- * Proxy-path binding helper.
2163
- *
2164
- * ```ts
2165
- * <h1 {...bindByPath<Homepage>(d => d.heroTitle)}>{data.heroTitle}</h1>
2166
- * ```
2167
- *
2168
- * The picker is invoked once with a recording Proxy; the recorded path
2169
- * is joined with `.` and emitted as the binding name. Property access
2170
- * on nested objects and arrays both work — `d => d.slides[0].title`
2171
- * becomes `slides.title` (array indices are normalised away because the
2172
- * runtime resolves bindings against schema, not against positional
2173
- * indices).
2174
- */
1567
+ /** `bindByPath<Homepage>(d => d.slides[0].title)` → `slides.title`; array indices are dropped because bindings resolve against the schema. */
2175
1568
  declare function bindByPath<T = Record<string, unknown>>(picker: (data: T) => unknown, options?: BindOptions): FieldBindingAttributes;
2176
1569
 
2177
1570
  /**
2178
- * Authorization-gated binding emission.
2179
- *
2180
- * Binding attributes are not neutral markup. `data-payload-field` names a CMS
2181
- * field, and `data-payload-owner` names a global, a collection and often a
2182
- * document id. Emitted unconditionally they publish the shape of the content
2183
- * model — and the identity of documents — to every anonymous visitor and
2184
- * crawler of the page.
2185
- *
2186
- * The gate belongs to the application, not to this package: it is the same
2187
- * decision that already controls draft reads and cache policy. What this
2188
- * module provides is a place to apply that decision **once per request** so no
2189
- * individual call site can forget it, and an emission unit that cannot be
2190
- * partially suppressed.
2191
- *
2192
- * That second property matters more than it looks. A binding is not one
2193
- * attribute: a field travels with its type, its locale, its rich-text marker
2194
- * and its owner. Gating only the field name leaves the companions behind,
2195
- * which discloses the taxonomy anyway and leaves the runtime looking at a
2196
- * binding whose field is gone.
2197
- *
2198
- * @module @dsl/preview-bindings
1571
+ * Binding helpers gated on one authorization verdict per request: the
1572
+ * attributes publish the content model, so an unauthorized response carries
1573
+ * none and a binding is suppressed whole, never in part. See ADR 0006.
2199
1574
  */
2200
1575
 
2201
- /** Owner marker naming the document a subtree belongs to. */
2202
1576
  interface OwnerBindingAttributes {
2203
1577
  readonly 'data-payload-owner': string;
2204
1578
  }
2205
- /**
2206
- * The complete absence of binding attributes.
2207
- *
2208
- * Spreading this into markup contributes nothing, which is the point: an
2209
- * unauthorized response must be byte-identical to one that never knew about
2210
- * live preview.
2211
- */
1579
+ /** No attributes at all: an unauthorized response is byte-identical to one that never knew about live preview. */
2212
1580
  type SuppressedBinding = Readonly<Record<string, never>>;
2213
- interface PreviewBindingsOptions {
2214
- /**
2215
- * The application's own authorization verdict for this request.
2216
- *
2217
- * Never derive this from `?preview=true`, an iframe destination or a
2218
- * referer — those are client-controlled intent signals. Use the same
2219
- * verified result that decides whether drafts may be read.
2220
- */
2221
- readonly authorized: boolean;
2222
- /**
2223
- * Document this subtree belongs to, emitted as `data-payload-owner` by
2224
- * {@link PreviewBindings.owner}. Required only when the page previews more
2225
- * than one document and the runtime runs with `scopeBindingsByOwner`.
2226
- */
1581
+ interface PreviewBindingsCommonOptions {
1582
+ /** Document this subtree belongs to, emitted as `data-payload-owner`; needed with `scopeBindingsByOwner`. */
2227
1583
  readonly owner?: string;
2228
1584
  }
2229
- /** Request-scoped binding helpers that carry one authorization decision. */
1585
+ /** The verdict from `authorizePreviewRequest()`, or `null` for a public response. Only a branded context authorizes emission. */
1586
+ interface PreviewBindingsOptions extends PreviewBindingsCommonOptions {
1587
+ readonly authorization: AuthorizedPreviewContext | null;
1588
+ }
1589
+ /** Request-scoped binding helpers carrying one authorization decision. */
2230
1590
  interface PreviewBindings {
2231
- /** The verdict these helpers were built with. */
2232
1591
  readonly authorized: boolean;
2233
- /** Typed field binding, or nothing at all while unauthorized. */
1592
+ /** Typed field binding, or nothing while unauthorized. */
2234
1593
  bind: <T = Record<string, unknown>>(field: FieldName<T>, options?: BindOptions) => FieldBindingAttributes | SuppressedBinding;
2235
- /** Rename-safe field binding, or nothing at all while unauthorized. */
1594
+ /** Rename-safe field binding, or nothing while unauthorized. */
2236
1595
  bindByPath: <T = Record<string, unknown>>(picker: (data: T) => unknown, options?: BindOptions) => FieldBindingAttributes | SuppressedBinding;
2237
- /**
2238
- * Owner marker for the element that owns this subtree, or nothing at all
2239
- * while unauthorized or when no owner was configured.
2240
- */
1596
+ /** Owner marker for this subtree, or nothing while unauthorized or without an owner. */
2241
1597
  owner: () => OwnerBindingAttributes | SuppressedBinding;
2242
1598
  }
1599
+ /** Request-scoped `bind`, `bindByPath` and `owner`, all suppressed unless `authorization` is a real context. */
1600
+ declare function createPreviewBindings(options: PreviewBindingsOptions): PreviewBindings;
1601
+
2243
1602
  /**
2244
- * Build request-scoped binding helpers around one authorization decision.
2245
- *
2246
- * ```ts
2247
- * const preview = createPreviewBindings({
2248
- * authorized: authorization !== null,
2249
- * owner: `global:${slug}`,
2250
- * });
2251
- * ```
2252
- * ```astro
2253
- * <section {...preview.owner()}>
2254
- * <h1 {...preview.bind<Homepage>('heroTitle')}>{data.heroTitle}</h1>
2255
- * </section>
2256
- * ```
2257
- *
2258
- * While unauthorized every helper returns an empty attribute set, so the
2259
- * public response carries no `data-payload-*` at all.
1603
+ * Hydrated islands own their subtree, so the runtime never patches inside one.
1604
+ * Instead every flush dispatches `payload-live-preview:update` on each island
1605
+ * root with the update in `detail`. `data-payload-island="patch"` opts back in.
2260
1606
  */
2261
- declare function createPreviewBindings(options: PreviewBindingsOptions): PreviewBindings;
1607
+ declare const ISLAND_EVENT = "payload-live-preview:update";
1608
+ interface IslandUpdateDetail {
1609
+ readonly fields: Readonly<Record<string, unknown>>;
1610
+ readonly revision: number;
1611
+ readonly receivedAt: number;
1612
+ readonly locale: string | undefined;
1613
+ }
1614
+ /** Whether `element` (or an ancestor) is an island that did not opt into patching. */
1615
+ declare function isInsideIsland(element: Element): boolean;
2262
1616
 
2263
- export { type AnalyticsSnapshot, type BindOptions, type BlockRenderContext, type BlockRenderer, type CachedElement, type ConnectionStatus, type CspDirectiveMerge, DIAGNOSTIC_CODES, type DiagnosticCode, type DocumentSaveHandler, type DocumentSavePluginOptions, type DocumentSaveStrategy, EventEmitter, type EventHandler, type FetchPreviewDocumentOptions, type FetchPreviewGlobalOptions, type FieldBindingAttributes, type FieldName, type FieldPath, type FieldRenderer, type FieldTransform, type FieldType, type FrameAncestorsOptions, type InlineScriptConfig, type InspectionBindings, type InspectionOrigins, type InspectionProtocol, type InspectionRevisions, type InspectionScheduler, LIBRARY_PROTOCOL_VERSION, type LexicalNode, type LexicalRoot, LivePreviewClient, type LivePreviewClientConfig, type LivePreviewEventMap, type LivePreviewInspection, type LivePreviewPlugin, type NavigationLifecycleOptions, type NavigationLifecycleTarget, OriginDetector, type OwnerBindingAttributes, type PayloadFieldSchema, type PayloadFieldType, type PayloadLivePreviewData, type PayloadLivePreviewMessage, type PayloadMedia, type PayloadRelationship, type PluginContext, type PluginDisposer, type PluginEvents, type PreviewBindings, type PreviewBindingsOptions, type PreviewFetchBaseOptions, type PreviewRequestLike, type PreviewRequestOptions, type PreviewWhere, type ProtocolCapability, type ProtocolNegotiation, type RenderContext, type SanitizerDocument, type SuppressedBinding, type Unsubscribe, VERSION, type ValueAt, bind, bindByPath, bindNavigationLifecycle, buildBuiltinRenderers, buildFrameAncestors, buildScriptSrcWithNonce, createAnalyticsPlugin, createPreviewBindings, debugPlugin, detectInitialLocale, documentSavePlugin, escapeHtml, escapeHtmlAttribute, fetchPreviewDocument, fetchPreviewGlobal, generateCspNonce, generateInlineScript, hasCapability, highlightPlugin, initLivePreview, isDevMode, isExternalHttpUrl, isInIframe, isInPopup, isInPreviewContext, isLexicalContent, isPreviewRequest, isSafeUrl, lexicalToHtml, lexicalToPlainText, mergeCspHeader, negotiateProtocol, registerBlockRenderer, registerBuiltinRenderer, runtimeBuildInfo, sanitizeHtml, setCspCrypto, setSanitizerDocument, wrapWithScriptTag };
1617
+ export { AUTHORIZED_PREVIEW_BRAND_KEY, type AnalyticsSnapshot, type AuthorizedPreviewContext, type AuthorizedPreviewScope, type BindOptions, type BlockRenderContext, type BlockRenderer, CAPABILITY_DECLARATIONS, CAPABILITY_DOCUMENTATION, type CachedElement, type CapabilityDeclaration, type CapabilityDocumentation, type CapabilitySource, type ConnectionStatus, type CspDirectiveMerge, type CustomRendererKey, DIAGNOSTIC_CODES, type DefaultsProfile, type DiagnosticCode, type DocumentSaveHandler, type DocumentSavePluginOptions, type DocumentSaveStrategy, EventEmitter, type EventHandler, type EventSourcePolicy, type FetchLike, type FieldBindingAttributes, type FieldName, type FieldPath, type FieldRenderer, type FieldTransform, type FieldType, type FocusReportTarget, type FragmentContext, type FragmentReport, type FragmentStrategy, type FrameAncestorsOptions, ISLAND_EVENT, type InlineScriptConfig, type InspectionBindings, type InspectionFragments, type InspectionOrigins, type InspectionProtocol, type InspectionRevisions, type InspectionRoute, type InspectionScheduler, type IslandUpdateDetail, type IssuePreviewTokenOptions, LIBRARY_PROTOCOL_VERSION, type LexicalNode, type LexicalRoot, LivePreviewClient, type LivePreviewClientConfig, type LivePreviewEventMap, type LivePreviewInspection, type LivePreviewPlugin, type NavigationLifecycleOptions, type NavigationLifecycleTarget, OriginDetector, type OwnerBindingAttributes, PROTOCOL_CAPABILITIES, type PayloadDocumentEventDetail, type PayloadFieldCondition, type PayloadFieldSchema, type PayloadFieldType, type PayloadLivePreviewData, type PayloadLivePreviewMessage, type PayloadMedia, type PayloadRelationship, type PayloadSessionStrategy, type PluginCompatibility, type PluginContext, type PluginDisposer, type PluginEvents, type PluginInspection, type PreviewAuthorization, type PreviewAuthorizationOutcome, type PreviewAuthorizationRequest, type PreviewAuthorizationStrategy, type PreviewAuthorizationStrategyName, type PreviewBindings, type PreviewBindingsCommonOptions, type PreviewBindingsOptions, type PreviewFocusMessage, type PreviewRequestLike, type PreviewRequestOptions, type PreviewTokenClaims, type PreviewTokenReplayStore, type PreviewTokenTransport, type PreviewVerifierClaims, type ProtocolCapability, type ProtocolNegotiation, type ProtocolProfile, type ProtocolProfileName, type RenderContext, type RendererKey, type RichTextRenderer, type RouteContext, type RouteStrategy, type SanitizerDocument, type SanitizerPolicyMode, type SignedTokenStrategy, type StrategyHandlers, type SubtleCryptoLike, type SuppressedBinding, TRUSTED_TYPES_POLICY_NAME, type TrustedHtmlPolicyLike, type Unsubscribe, type UpdateSource, VERSION, type ValueAt, type VerifierStrategy, authorizePreviewRequest, bind, bindByPath, bindNavigationLifecycle, buildBuiltinRenderers, buildFrameAncestors, buildScriptSrcWithNonce, createAnalyticsPlugin, createPreviewBindings, createPreviewFocusReporter, debugPlugin, detectInitialLocale, detectProtocolProfile, documentSavePlugin, escapeHtml, escapeHtmlAttribute, generateCspNonce, generateInlineScript, hasCapability, hasPreviewIntent, highlightPlugin, initLivePreview, isAuthorizedPreviewContext, isDevMode, isExternalHttpUrl, isInIframe, isInPopup, isInPreviewContext, isInsideIsland, isLexicalContent, isSafeUrl, issuePreviewToken, lexicalToHtml, lexicalToPlainText, mergeCspHeader, negotiateProtocol, registerBlockRenderer, registerBuiltinRenderer, reportPreviewFocus, runtimeBuildInfo, sanitizeHtml, setCspCrypto, setSanitizerDocument, setSanitizerPolicy, setTrustedTypesPolicy, trustedHtml, wrapWithScriptTag };