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/core.d.ts CHANGED
@@ -1,52 +1,22 @@
1
- /**
2
- * Connection state + heartbeat.
3
- *
4
- * Tracks whether the runtime is currently receiving updates from a
5
- * trusted origin and fires a callback when the heartbeat times out.
6
- *
7
- * The heartbeat is reset on every valid message and considered dead
8
- * after `timeoutMs` of silence. The host typically responds by
9
- * re-sending the `ready` handshake to attempt reconnection.
10
- *
11
- * @module @core/state
12
- */
1
+ /** The package version, read from package.json so the entries and the inline runtime cannot drift. */
2
+ declare const VERSION: string;
3
+
4
+ /** Connection status and the heartbeat that decides a disconnect. */
13
5
  type ConnectionStatus = 'disconnected' | 'connecting' | 'connected';
14
6
 
15
7
  /**
16
- * The shape of a live-preview inspection snapshot.
17
- *
18
- * This module is types only. The snapshot is assembled by
19
- * `LivePreviewRuntime.inspect()` as a plain object literal, because the inline
20
- * runtime pays for every byte of indirection and the assembly needs access to
21
- * internal state the runtime does not otherwise expose.
22
- *
23
- * A snapshot is a read of what the runtime already holds. It discloses nothing
24
- * that is not already observable on the page: the trusted origins are inside
25
- * the injected script, the field names are `data-payload-field` attributes in
26
- * the DOM, and the counters are derived from those. It is therefore available
27
- * in production rather than gated to development builds — a preview that
28
- * misbehaves only on the deployed site is exactly the case where the
29
- * information is worth having, and a development-only gate would withhold it
30
- * there.
31
- *
32
- * Nothing here is transmitted anywhere. Reading a snapshot performs no I/O.
33
- *
34
- * @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.
35
13
  */
36
14
 
37
15
  /** Origin trust as the runtime currently sees it. */
38
16
  interface InspectionOrigins {
39
- /**
40
- * Origins the runtime is willing to accept messages from, as configured.
41
- * An empty list means no explicit allow-list was given, which is the
42
- * condition under which referrer and localhost heuristics decide instead.
43
- */
17
+ /** Configured origins; empty means the referrer and localhost heuristics decide instead. */
44
18
  readonly trusted: readonly string[];
45
- /**
46
- * The origin the runtime locked onto after its first accepted update, or
47
- * `undefined` while still unlocked. Once locked, every other origin is
48
- * refused for the rest of the session.
49
- */
19
+ /** The origin locked onto by the first accepted update; every other is then refused. */
50
20
  readonly locked: string | undefined;
51
21
  }
52
22
  /** Protocol versions and the capabilities they enable. */
@@ -59,6 +29,10 @@ interface InspectionProtocol {
59
29
  readonly negotiated: number;
60
30
  /** Capability flags enabled at the negotiated version. */
61
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;
62
36
  }
63
37
  /** Revision accounting for the update pipeline. */
64
38
  interface InspectionRevisions {
@@ -71,6 +45,19 @@ interface InspectionRevisions {
71
45
  * when the last update is among the abandoned ones.
72
46
  */
73
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;
74
61
  /** Revision currently in flight, or `undefined` when the pipeline is idle. */
75
62
  readonly active: number | undefined;
76
63
  }
@@ -83,22 +70,14 @@ interface InspectionBindings {
83
70
  /** Those field names, sorted. */
84
71
  readonly fieldNames: readonly string[];
85
72
  /**
86
- * Field names that arrived in an update but matched no binding, as far as
87
- * the runtime has observed. This is the list to read when a field refuses
88
- * to update: a name here is a markup problem, a name absent from here and
89
- * 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.
90
75
  */
76
+ readonly absentFields: readonly string[];
91
77
  /**
92
- * Bound fields that some update since start carried no value for, so the
93
- * binding kept whatever text it already had. Cumulative, like
94
- * `orphanFields`, and the exact opposite of it: a binding with no value
95
- * rather than a value with no binding.
96
- *
97
- * This is why a binding can stay stale while its siblings update. Without
98
- * it the two cases are indistinguishable from the DOM, since neither
99
- * 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.
100
80
  */
101
- readonly absentFields: readonly string[];
102
81
  readonly orphanFields: readonly string[];
103
82
  /** Whether `scopeBindingsByOwner` is active. */
104
83
  readonly ownerScoped: boolean;
@@ -109,11 +88,7 @@ interface InspectionBindings {
109
88
  interface InspectionScheduler {
110
89
  /** Writes buffered for the next flush. */
111
90
  readonly pending: number;
112
- /**
113
- * Writes held back because their element is offscreen and the binding count
114
- * is above `visibilityGateThreshold`. These are applied when the element
115
- * scrolls into view — never, on a page nobody scrolls.
116
- */
91
+ /** Writes held back offscreen above `visibilityGateThreshold`; applied when scrolled into view. */
117
92
  readonly deferred: number;
118
93
  /** The binding count above which the visibility gate starts deferring. */
119
94
  readonly visibilityGateThreshold: number;
@@ -122,23 +97,13 @@ interface InspectionScheduler {
122
97
  /** Stats of the most recent flush, or `undefined` if none has run. */
123
98
  readonly lastFlush: {
124
99
  readonly applied: number;
125
- /**
126
- * Field names the flush applied, in application order.
127
- *
128
- * `applied` is a count, and a count cannot separate "this binding was
129
- * written" from "this binding was never scheduled" — a stale binding
130
- * next to a non-zero count is consistent with both. The names can.
131
- */
100
+ /** Which fields the flush wrote; a count alone cannot say a binding was skipped. */
132
101
  readonly appliedFields: readonly string[];
133
102
  readonly deferred: number;
134
103
  readonly durationMs: number;
135
104
  } | undefined;
136
105
  }
137
- /**
138
- * A point-in-time read of runtime state, for diagnosing a preview that is not
139
- * behaving. Every field is a plain value; the snapshot holds no references
140
- * into runtime internals and does not change after it is returned.
141
- */
106
+ /** A point-in-time read; plain values only, with no references into runtime internals. */
142
107
  interface LivePreviewInspection {
143
108
  /** Package version that produced this snapshot. */
144
109
  readonly version: string;
@@ -153,65 +118,67 @@ interface LivePreviewInspection {
153
118
  readonly scheduler: InspectionScheduler;
154
119
  /** Field types with a registered renderer, sorted. */
155
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
+ };
156
158
  }
157
159
 
158
160
  /**
159
- * Payload Live Preview message protocol.
160
- *
161
- * Mirrors the messages emitted by Payload CMS through
162
- * `window.postMessage`, verified against the shipped admin code
163
- * (`@payloadcms/ui` → `elements/LivePreview/Window`):
164
- *
165
- * - Payload **3.x** sends `{ type, collectionSlug, globalSlug, data,
166
- * locale, externallyUpdatedRelationship }` on every form edit —
167
- * `data` holds **raw form values** (relationships as bare IDs) and
168
- * there is **no** `fieldSchemaJSON`.
169
- * - Payload **2.x** additionally sent `fieldSchemaJSON` with the
170
- * first update (client-side merge era).
171
- * - `previewToken` and `protocolVersion` are extensions of THIS
172
- * library for custom admin senders; stock Payload never sends
173
- * them and ignores them in our `ready` handshake.
174
- *
175
- * @module @/types/payload-protocol
176
- */
177
- /**
178
- * A `payload-live-preview` data update.
179
- *
180
- * `data` is the serialized document state. `fieldSchemaJSON`, when
181
- * present (Payload 2.x or custom senders), allows the schema-driven
182
- * 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.
183
164
  */
184
165
  interface PayloadLivePreviewMessage {
185
166
  readonly type: 'payload-live-preview';
186
167
  readonly data?: Record<string, unknown>;
168
+ /** Sent by Payload 2.x and custom senders only. */
187
169
  readonly fieldSchemaJSON?: readonly PayloadFieldSchema[];
188
170
  readonly globalSlug?: string;
189
171
  readonly collectionSlug?: string;
190
172
  readonly locale?: string;
191
173
  readonly ready?: boolean;
192
- /**
193
- * Payload 3.x: most recent relationship-document event (create/update
194
- * in a drawer), or `null`. The official client ignores it too — it is
195
- * modeled here for completeness and consumer event access.
196
- */
174
+ /** Payload 3.x: the most recent relationship-document event (a drawer save), or `null`. */
197
175
  readonly externallyUpdatedRelationship?: PayloadDocumentEventDetail | null;
198
- /**
199
- * Optional preview JWT. ⚠️ Library extension — stock Payload never
200
- * sends one. When `RuntimeOptions.validateToken` is set, the runtime
201
- * requires every data update to carry a token that passes the
202
- * validator; messages without a valid token are dropped.
203
- */
176
+ /** Library extension: the token `validateToken` checks. Stock Payload never sends one. */
204
177
  readonly previewToken?: string;
205
- /**
206
- * Highest protocol version the sender supports. ⚠️ Library extension
207
- * for custom senders — stock Payload sends no version. When absent,
208
- * the receiver assumes v1.
209
- */
178
+ /** Library extension: the highest protocol version the sender supports; absent means v1. */
210
179
  readonly protocolVersion?: number;
211
180
  }
212
- /**
213
- * Payload's `DocumentEvent` shape (admin `providers/DocumentEvents`).
214
- */
181
+ /** Payload's `DocumentEvent` shape (admin `providers/DocumentEvents`). */
215
182
  interface PayloadDocumentEventDetail {
216
183
  readonly entitySlug: string;
217
184
  readonly operation?: 'create' | 'update';
@@ -219,9 +186,7 @@ interface PayloadDocumentEventDetail {
219
186
  readonly updatedAt?: string;
220
187
  readonly [extra: string]: unknown;
221
188
  }
222
- /**
223
- * Parsed, validated message payload exposed to consumers via events.
224
- */
189
+ /** A validated update as events expose it. */
225
190
  interface PayloadLivePreviewData {
226
191
  readonly fields: Record<string, unknown>;
227
192
  readonly schema?: readonly PayloadFieldSchema[];
@@ -229,13 +194,7 @@ interface PayloadLivePreviewData {
229
194
  readonly collectionSlug?: string;
230
195
  readonly locale?: string;
231
196
  }
232
- /**
233
- * The subset of Payload's field-schema description we rely on.
234
- *
235
- * Payload emits the entire field configuration tree but we only need
236
- * the discriminator `type` and the nesting via `fields`/`blocks`.
237
- * Extra properties are preserved opaquely.
238
- */
197
+ /** The subset of Payload's field schema the runtime reads; extra properties are kept opaquely. */
239
198
  interface PayloadFieldSchema {
240
199
  readonly name: string;
241
200
  readonly type: PayloadFieldType;
@@ -256,54 +215,19 @@ interface PayloadBlockSchema {
256
215
  readonly fields: readonly PayloadFieldSchema[];
257
216
  readonly [extra: string]: unknown;
258
217
  }
259
- /**
260
- * The Payload core field types this library recognises (20 as of
261
- * Payload 3.x). `tabs` is treated as a structural container; `group`
262
- * flattens nested fields.
263
- */
218
+ /** Payload's core field types. `tabs` is a structural container; `group` flattens. */
264
219
  type PayloadFieldType = 'text' | 'textarea' | 'richText' | 'email' | 'number' | 'checkbox' | 'date' | 'select' | 'radio' | 'array' | 'blocks' | 'group' | 'tabs' | 'row' | 'collapsible' | 'relationship' | 'upload' | 'point' | 'json' | 'code' | 'ui';
265
- /**
266
- * Payload's `admin.condition` is a function in source, but it cannot
267
- * be transferred over postMessage. The schema arrives with this slot
268
- * either absent or, in future Payload versions, replaced by a
269
- * serialized expression. We model the slot but do not consume it yet.
270
- */
220
+ /** `admin.condition` is a function in source and cannot cross `postMessage`; modelled, not consumed. */
271
221
  type PayloadFieldCondition = (data: unknown, siblingData: unknown) => boolean;
272
222
 
273
223
  /**
274
- * Stable identifiers for everything the runtime reports.
275
- *
276
- * Prose gets reworded; a code does not. A consumer filtering logs, an alert
277
- * rule, or a bug report referring to `LP0301` keeps meaning the same thing
278
- * after the sentence around it is rewritten, and a code is greppable in a way
279
- * that a sentence fragment is not.
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.
280
227
  *
281
- * Codes are grouped by the question they answer:
282
- *
283
- * - `LP01xx` — configuration and origin trust: the runtime is running, but it
284
- * was set up in a way that will bite.
285
- * - `LP02xx` — bindings and markup: an update had nowhere to land.
286
- * - `LP03xx` — updates and scheduling: the update landed somewhere other than
287
- * the DOM, or not yet.
288
- * - `LP04xx` — rendering: a value reached an element and was refused.
289
- * - `LP05xx` — protocol and messages: something arrived that was not accepted.
290
- * - `LP06xx` — consumer callbacks: code the consumer supplied threw.
291
- * - `LP07xx` — audit findings from `pll doctor`, which reports on a served
292
- * response rather than on a running runtime.
293
- *
294
- * A code is part of the public contract. Codes are never reused for a
295
- * different meaning and never renumbered; a retired code stays retired.
296
- * `LP0604` is unassigned: a throwing token validator is deliberately treated
297
- * as a rejection and reported as `LP0502`, so there is nothing distinct to
298
- * report yet. The number stays reserved rather than being handed to something
299
- * else.
300
- *
301
- * The runtime writes these as string literals at each reporting site rather
302
- * than reading them from this record, so the inline build pays for the six
303
- * characters it prints and nothing more. This record exists so consumers can
304
- * refer to a code by name instead of copying a literal.
305
- *
306
- * @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.
307
231
  */
308
232
  /**
309
233
  * Every diagnostic code the runtime can report, by name.
@@ -319,6 +243,8 @@ declare const DIAGNOSTIC_CODES: Readonly<{
319
243
  readonly NoTrustedOrigin: "LP0101";
320
244
  /** Origin trust rests on `document.referrer`, so any framing site is trusted. */
321
245
  readonly ReferrerOnlyTrust: "LP0102";
246
+ /** A plugin declares a runtime range this runtime does not satisfy; it was refused. */
247
+ readonly PluginIncompatible: "LP0103";
322
248
  /** An update named a field with no binding anchor on the page. */
323
249
  readonly OrphanField: "LP0201";
324
250
  /** Owner scoping is on and the update names no document it could belong to. */
@@ -331,6 +257,14 @@ declare const DIAGNOSTIC_CODES: Readonly<{
331
257
  readonly TextTargetHasChildren: "LP0402";
332
258
  /** A structural container has no array template, so the update was skipped. */
333
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";
334
268
  /** A message was rejected before it reached the update pipeline. */
335
269
  readonly MessageRejected: "LP0501";
336
270
  /** A preview token was rejected. */
@@ -361,251 +295,245 @@ declare const DIAGNOSTIC_CODES: Readonly<{
361
295
  readonly AuditNoBindings: "LP0707";
362
296
  /** The URL did not return an HTML page, so nothing else can be judged. */
363
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";
364
314
  }>;
365
315
  /** A diagnostic code, as it appears in log output and on the `error` event. */
366
316
  type DiagnosticCode = (typeof DIAGNOSTIC_CODES)[keyof typeof DIAGNOSTIC_CODES];
367
317
 
368
318
  /**
369
- * Public event payload shapes for the live preview lifecycle.
370
- *
371
- * The map keys form the union of valid event names. Adding a new event
372
- * is a two-line change: add the entry here and dispatch from the
373
- * relevant module. Consumers get autocompletion and exhaustive checks
374
- * for free because the `EventEmitter` is generic over this map.
375
- *
376
- * @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`.
324
+ */
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
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. */
378
388
 
379
389
  interface LivePreviewEventMap {
380
- /**
381
- * Fired once per startup attempt after observers, cache, and message listening
382
- * are active. A later startup failure may roll that attempt back; retrying then
383
- * emits a new `init` event.
384
- */
390
+ /** Once per startup attempt, after observers, cache and message listening are active. A rolled-back attempt emits it again on retry. */
385
391
  readonly init: {
386
392
  readonly timestamp: number;
387
393
  };
388
- /** Fired for the first accepted data-bearing update from a trusted origin. */
394
+ /** The first accepted data-bearing update from a trusted origin. */
389
395
  readonly connect: {
390
396
  readonly origin: string;
391
397
  readonly timestamp: number;
392
398
  };
393
- /**
394
- * Fired when the heartbeat times out or the runtime is destroyed. The
395
- * `'unload'` reason remains in the public union for 1.x producer compatibility;
396
- * the built-in runtime does not currently emit it.
397
- */
399
+ /** The heartbeat timed out, the runtime was destroyed, or the page was suspended for navigation (`'unload'`). */
398
400
  readonly disconnect: {
399
401
  readonly reason: 'timeout' | 'destroy' | 'unload';
400
402
  readonly timestamp: number;
401
403
  };
402
- /**
403
- * Fired before the DOM is mutated for an incoming update.
404
- *
405
- * Handlers may call `cancel()` before their synchronous or asynchronous
406
- * work settles to skip exactly this revision.
407
- */
404
+ /** Before the DOM is mutated for an update; `cancel()` skips exactly this revision. */
408
405
  readonly beforeUpdate: {
409
406
  readonly data: PayloadLivePreviewData;
410
- /** Runtime-populated ordering metadata; optional for 1.x producer compatibility. */
411
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;
412
412
  readonly cancel: () => void;
413
413
  };
414
- /**
415
- * Fired after each actual, current DOM application batch. Visibility replay
416
- * may produce another event for the same revision; cancelled, obsolete, and
417
- * deferred-only batches produce none.
418
- */
414
+ /** After each applied, still-current batch. Visibility replay may emit another for the same revision; cancelled, obsolete and deferred-only batches emit none. */
419
415
  readonly afterUpdate: {
420
416
  readonly data: PayloadLivePreviewData;
421
417
  readonly updatedCount: number;
422
418
  readonly durationMs: number;
423
- /** Runtime-populated ordering metadata; optional for 1.x producer compatibility. */
424
419
  readonly revision?: number;
420
+ readonly receivedAt?: number;
421
+ readonly source?: UpdateSource;
425
422
  };
426
- /** Fired for each successful, still-current element write. */
423
+ /** Each successful, still-current element write. */
427
424
  readonly elementUpdate: {
428
425
  readonly element: Element;
429
426
  readonly fieldName: string;
430
427
  readonly previousValue: unknown;
431
428
  readonly nextValue: unknown;
432
- /** Runtime-populated ordering metadata; optional for 1.x producer compatibility. */
433
429
  readonly revision?: number;
430
+ readonly receivedAt?: number;
431
+ readonly source?: UpdateSource;
434
432
  };
435
- /** Fired whenever the element cache is rebuilt (initial scan or MutationObserver-triggered). */
433
+ /** The element cache was rebuilt (initial scan or mutation). */
436
434
  readonly cacheRefresh: {
437
435
  readonly elementCount: number;
438
436
  readonly fieldCount: number;
439
437
  readonly durationMs: number;
440
438
  };
441
- /** 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). */
442
450
  readonly documentSave: {
443
451
  readonly timestamp: number;
444
452
  };
445
- /**
446
- * Fired on errors that the runtime caught but cannot fully recover from.
447
- *
448
- * The `context` string identifies where the error originated. `code` says
449
- * the same thing in a form that survives rewording — branch on `code`, read
450
- * `context` when you want the human-readable origin.
451
- */
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. */
452
459
  readonly error: {
453
460
  readonly error: Error;
454
461
  readonly context: string;
455
462
  readonly code: DiagnosticCode;
456
463
  };
457
- /**
458
- * Fired during destroy after browser observers and the message listener are
459
- * removed. Event/plugin listeners remain available for this notification and
460
- * are released by the owning client afterwards.
461
- */
464
+ /** During destroy, after observers and the message listener are removed; listeners are released by the owning client afterwards. */
462
465
  readonly destroy: {
463
466
  readonly timestamp: number;
464
467
  };
465
468
  }
466
- /**
467
- * Handler signature: receives the payload and may return a Promise.
468
- *
469
- * Promise-returning handlers are awaited in registration order — this
470
- * is important for `beforeUpdate.cancel()` semantics.
471
- */
469
+ /** Handlers may return a promise; they are awaited in registration order, which `beforeUpdate.cancel()` relies on. */
472
470
  type EventHandler<TPayload> = (payload: TPayload) => void | Promise<void>;
473
- /** Subscription removal function returned by `on()`/`once()`. */
474
471
  type Unsubscribe = () => void;
475
472
 
476
473
  /**
477
- * Per-instance, type-safe event emitter.
478
- *
479
- * Replaces the legacy module-level singleton. Each `LivePreviewClient`
480
- * instantiates its own emitter so that calling `destroy()` on one
481
- * instance cannot clobber listeners attached to another.
482
- *
483
- * Design notes:
484
- *
485
- * - Generic over an event map type. The default `LivePreviewEventMap`
486
- * captures every event this library emits, but the implementation
487
- * is reusable for any map.
488
- * - Handlers may be `async`. They are awaited sequentially in
489
- * registration order. This is intentional: `beforeUpdate.cancel()`
490
- * and similar synchronization rely on stable ordering.
491
- * - Errors thrown by one handler are logged via `console.error` and
492
- * do not prevent later handlers from running.
493
- * - `once()` semantics: registered into a separate map; the entire
494
- * snapshot for an event is claimed immediately before its dispatch,
495
- * and remains consumed even if individual handlers throw.
496
- *
497
- * @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.
498
476
  */
499
477
 
500
478
  declare class EventEmitter<TMap extends object = LivePreviewEventMap> {
501
479
  #private;
502
- /**
503
- * Subscribe `handler` to `event`. Returns an unsubscribe function;
504
- * call it to remove the handler. The handler is invoked for every
505
- * subsequent emit until unsubscribed.
506
- */
507
480
  on<E extends keyof TMap>(event: E, handler: EventHandler<TMap[E]>): Unsubscribe;
508
- /**
509
- * Subscribe `handler` for a single emit. After the first dispatch,
510
- * the handler is removed automatically.
511
- */
512
481
  once<E extends keyof TMap>(event: E, handler: EventHandler<TMap[E]>): Unsubscribe;
513
- /**
514
- * Remove `handler` from `event`. Works for both `on`- and
515
- * `once`-registered handlers. No-op when the handler isn't registered.
516
- */
517
482
  off<E extends keyof TMap>(event: E, handler: EventHandler<TMap[E]>): void;
518
- /**
519
- * Dispatch `payload` to every handler registered for `event`.
520
- *
521
- * Handlers run sequentially in registration order — regular ones
522
- * first, then `once` handlers. Async handlers are awaited. Synchronous
523
- * exceptions are caught, logged, and isolated to the offending handler.
524
- */
483
+ /** Dispatch to every handler: regular ones first, then `once` handlers. */
525
484
  emit<E extends keyof TMap>(event: E, payload: TMap[E]): Promise<void>;
526
485
  /**
527
- * Lifecycle-only guarded dispatch. The predicate is checked before and after
528
- * every handler so an async boundary cannot let an obsolete transaction run
529
- * later callbacks. `emit()` intentionally keeps its established behaviour.
530
- *
531
- * Once-handlers are claimed only when dispatch reaches the once phase. Once
532
- * claimed, the whole snapshot remains consumed even if the predicate changes
533
- * while one of its handlers is awaiting, matching normal once semantics.
534
- *
486
+ * Dispatch while `shouldContinue()` holds — checked around every handler, so
487
+ * an obsolete transaction cannot resume across an await.
535
488
  * @internal
536
- * @returns `true` when the complete handler sequence ran while eligible.
537
489
  */
538
490
  emitWhile<E extends keyof TMap>(event: E, payload: TMap[E], shouldContinue: () => boolean): Promise<boolean>;
539
- /**
540
- * Number of handlers currently registered for `event`.
541
- * Sum of `on` and `once` registrations.
542
- */
491
+ /** Registered handlers for `event`, `on` and `once` together. */
543
492
  listenerCount(event: keyof TMap): number;
544
- /**
545
- * Remove every handler. Pass an event name to limit the removal to
546
- * one event; omit it to clear every event on this emitter.
547
- *
548
- * Crucially this only affects the current instance — there is no
549
- * shared global state.
550
- */
493
+ /** Remove every handler, or only those of `event`. */
551
494
  removeAllListeners(event?: keyof TMap): void;
552
- /**
553
- * Snapshot of all event names that currently have at least one
554
- * registered handler. Useful for debugging.
555
- */
495
+ /** Event names with at least one handler. */
556
496
  eventNames(): (keyof TMap)[];
557
497
  }
558
498
 
559
- /**
560
- * Core type definitions shared across all runtime primitives.
561
- *
562
- * @module @core/types
563
- */
499
+ /** Types shared by the runtime primitives. */
564
500
 
565
- /**
566
- * Field types recognized by the live preview renderer.
567
- *
568
- * Extends Payload's field set with `html` and `url` which are not
569
- * Payload field types per se but useful DOM-binding categories.
570
- */
501
+ /** Payload's field set plus the DOM-binding categories `html`, `url`, `image` and `structural-array`. */
571
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;
572
507
  /**
573
- * A DOM element that has been registered as a live preview binding.
574
- *
575
- * Stored in the element cache indexed by `fieldName`. Multiple cached
576
- * entries can share a field name when the same field is rendered in
577
- * several places (e.g., a title shown in both the header and the
578
- * 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.
579
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. */
580
517
  interface CachedElement {
581
518
  /** The bound DOM element. */
582
519
  readonly element: Element;
583
520
  /** Payload field path, e.g., `title` or `hero.subtitle`. */
584
521
  readonly fieldName: string;
585
- /**
586
- * Resolved field type, used to dispatch to the right renderer.
587
- *
588
- * The cache resolves this from DOM attributes (`data-payload-type`,
589
- * tag-based heuristics) at build time. When `explicitFieldType` is
590
- * `false` the lifecycle is allowed to override this with the type
591
- * learned from the schema once `fieldSchemaJSON` arrives.
592
- */
593
- readonly fieldType: FieldType;
594
- /**
595
- * `true` when the field type was set by an explicit
596
- * `data-payload-type` attribute (consumer trumps schema), `false`
597
- * when it was inferred via tag-based heuristics (schema may
598
- * override later). Optional so synthetic bindings (tests / programmatic
599
- * `cache.add()` callers) need not supply it; the cache always
600
- * populates it for DOM-derived bindings.
601
- */
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. */
602
525
  readonly explicitFieldType?: boolean;
603
- /**
604
- * Optional target attribute: the value is written to this attribute
605
- * instead of the element's content (from `data-payload-attribute`,
606
- * or `bind(field, { attribute })`). Writes are policed — event
607
- * handlers, `style`, and unsafe URLs are refused.
608
- */
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. */
609
537
  readonly targetAttribute?: string;
610
538
  /** Optional sibling-field path bound to the element's `href` attribute. */
611
539
  readonly hrefField?: string;
@@ -619,21 +547,10 @@ interface CachedElement {
619
547
  readonly arraySeparator?: string;
620
548
  /** Optional locale code locked onto this element (overrides the global locale). */
621
549
  readonly locale?: string;
622
- /**
623
- * Optional document this binding belongs to, resolved from the nearest
624
- * `data-payload-owner` ancestor (the element itself included).
625
- *
626
- * Only consulted while owner scoping is enabled. Without it a field name is
627
- * the sole identity, so every document rendered on the page competes for the
628
- * same name.
629
- */
550
+ /** The document this binding belongs to, from the nearest `data-payload-owner`. */
630
551
  readonly owner?: string;
631
552
  }
632
- /**
633
- * Context passed to every field renderer. Provides access to the
634
- * entire field tree so renderers can resolve sibling fields (e.g.,
635
- * an `<a>` that pulls `href` from a different field).
636
- */
553
+ /** What a renderer is given; `allFields` lets it resolve a sibling field. */
637
554
  interface RenderContext {
638
555
  /** Full field map for the current update — used for sibling lookups. */
639
556
  readonly allFields: Record<string, unknown>;
@@ -641,702 +558,284 @@ interface RenderContext {
641
558
  readonly locale: string | undefined;
642
559
  /** Optional schema descriptor for the field, when available. */
643
560
  readonly schema: PayloadFieldSchema | undefined;
561
+ /** The project's rich-text renderer, when the client was configured with one. */
562
+ readonly renderRichText?: RichTextRenderer;
644
563
  }
645
564
  /**
646
- * Contract that every field renderer implements.
647
- *
648
- * Renderers are pure DOM-write functions: they receive the cached
649
- * binding plus the new value and apply it to the element. They must
650
- * not throw — failures should be silenced and logged so that one bad
651
- * field cannot stop an entire update. The public renderer result remains
652
- * `void` for 1.x compatibility. Built-in renderers may use an internal exact
653
- * `false` sentinel when they deliberately perform no DOM write; callers must
654
- * 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.
655
568
  */
656
569
  interface FieldRenderer {
657
- readonly name: FieldType;
570
+ readonly name: RendererKey;
658
571
  render(target: CachedElement, value: unknown, context: RenderContext): void;
659
572
  }
660
573
 
661
574
  /**
662
- * Plugin system types.
663
- *
664
- * Each plugin participates in the live-preview lifecycle through a
665
- * scoped, per-instance context. Plugins can register custom field
666
- * renderers, transform incoming values, and subscribe to events —
667
- * the same primitives the built-in renderers use.
668
- *
669
- * The system is intentionally minimal. Anything that can be expressed
670
- * with an event subscription belongs in `events`, not `plugins`.
671
- *
672
- * @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.
673
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. */
674
587
 
675
588
  /** Idempotent handle that releases one plugin-owned resource. */
676
589
  type PluginDisposer = () => void;
677
- /**
678
- * Event subscriptions scoped to one plugin registration.
679
- *
680
- * This remains nominally compatible with the pre-1.0.4 `EventEmitter` context
681
- * type. Its methods are registration-scoped: even bulk removal affects only
682
- * this plugin's subscriptions, never a consumer's or another plugin's
683
- * listeners.
684
- */
685
- 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 {
686
592
  readonly on: <E extends keyof LivePreviewEventMap>(event: E, handler: EventHandler<LivePreviewEventMap[E]>) => Unsubscribe;
687
593
  readonly once: <E extends keyof LivePreviewEventMap>(event: E, handler: EventHandler<LivePreviewEventMap[E]>) => Unsubscribe;
688
594
  readonly off: <E extends keyof LivePreviewEventMap>(event: E, handler: EventHandler<LivePreviewEventMap[E]>) => void;
689
595
  /** Emit through the owning client's event channel. */
690
596
  readonly emit: <E extends keyof LivePreviewEventMap>(event: E, payload: LivePreviewEventMap[E]) => Promise<void>;
691
- /**
692
- * Guarded emit through the owning client channel. Dispatch stops when the
693
- * caller predicate becomes false or this plugin registration is removed.
694
- */
597
+ /** Guarded emit; dispatch stops when the predicate turns false or this registration is removed. */
695
598
  readonly emitWhile: <E extends keyof LivePreviewEventMap>(event: E, payload: LivePreviewEventMap[E], shouldContinue: () => boolean) => Promise<boolean>;
696
- /** Number of listeners owned by this plugin registration. */
697
599
  readonly listenerCount: (event: keyof LivePreviewEventMap) => number;
698
- /** Remove only listeners owned by this plugin registration. */
699
600
  readonly removeAllListeners: (event?: keyof LivePreviewEventMap) => void;
700
- /** Event names for which this plugin registration owns listeners. */
701
601
  readonly eventNames: () => (keyof LivePreviewEventMap)[];
702
602
  }
703
603
  /**
704
- * Synchronous function that transforms a merged field value while a revision's
705
- * per-binding scheduler entry is prepared. Transforms run in registration
706
- * order and each receives the preceding transform's result. The final value is
707
- * frozen into that entry before later renderer or attribute dispatch;
708
- * `allFields` remains the merged, untransformed update snapshot.
709
- *
710
- * Returning a different shape than the input is allowed, but returning a
711
- * Promise or other thenable is a contract error. If a transform throws or
712
- * returns a thenable, the chain stops, the runtime emits an `error` event, and
713
- * dispatch falls back to the original merged value. That fallback still passes
714
- * 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.
715
606
  */
716
607
  type FieldTransform = (value: unknown, context: {
717
608
  readonly fieldName: string;
718
609
  readonly element: Element;
719
610
  readonly allFields: Record<string, unknown>;
720
611
  }) => unknown;
721
- /**
722
- * Per-instance plugin context. Replaces the legacy singleton context.
723
- *
724
- * Plugins receive a context that is bound to the parent
725
- * `LivePreviewClient`. They can:
726
- *
727
- * - subscribe via `events.on/once`,
728
- * - register field renderers,
729
- * - register value transforms,
730
- * - read the client's (frozen) configuration,
731
- * - log through the client's debug channel.
732
- */
733
612
  interface PluginContext {
734
613
  readonly events: PluginEvents;
735
- /** Register a renderer owned by this plugin registration. */
614
+ /** Register a renderer layer owned by this registration. */
736
615
  readonly registerFieldRenderer: (renderer: FieldRenderer) => void;
737
- /** Register an ordered transform owned by this plugin registration. */
616
+ /** Register an ordered transform owned by this registration. */
738
617
  readonly registerTransform: (fieldName: string, transform: FieldTransform) => void;
739
- /**
740
- * Register any additional synchronous cleanup owned by this registration.
741
- * The manager invokes it during rollback, `unuse()`, or client destruction.
742
- * Optional so pre-1.0.4 structural context mocks remain assignable.
743
- */
618
+ /** Register a synchronous cleanup run on rollback, `unuse()` and client destruction. */
744
619
  readonly registerCleanup?: (cleanup: PluginDisposer) => void;
745
620
  readonly getConfig: () => Readonly<Record<string, unknown>>;
746
621
  readonly log: (...args: unknown[]) => void;
747
622
  }
748
- /**
749
- * Plugin definition. `init` runs once per registration; `destroy`
750
- * runs when the plugin is unregistered or the client is destroyed.
751
- *
752
- * Both hooks may return a promise; the manager awaits them.
753
- */
623
+ /** `init` runs once per registration, `destroy` on removal; both may return a promise. */
754
624
  interface LivePreviewPlugin {
755
625
  readonly name: string;
756
626
  readonly version?: string;
627
+ /** Checked at registration; a plugin this runtime does not fit is refused (`LP0103`). */
628
+ readonly compat?: PluginCompatibility;
757
629
  readonly init: (context: PluginContext) => void | Promise<void>;
758
630
  readonly destroy?: () => void | Promise<void>;
759
631
  }
760
632
 
761
633
  /**
762
- * `LivePreviewClient` configuration shape.
763
- *
764
- * @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.
765
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).
648
+ */
649
+
766
650
  interface LivePreviewClientConfig {
767
- /** Explicit additional trusted origins. */
651
+ /** Explicit trusted admin origins. */
768
652
  readonly allowedOrigins?: readonly string[];
769
- /**
770
- * Payload server origin, e.g. `https://cms.example.com`. When set,
771
- * every incoming update is re-fetched through the Payload REST API
772
- * so relationship/upload fields render populated instead of as bare
773
- * IDs (same strategy as the official client). Recommended for
774
- * Payload 3.x.
775
- */
653
+ /** Payload origin; every update is then re-fetched through the REST API so relations arrive populated. Requires `mergeDepth`. */
776
654
  readonly serverURL?: string;
777
- /** REST API route prefix used with `serverURL`. Defaults to `/api`. */
655
+ /** REST route prefix used with `serverURL`. Default `/api`. */
778
656
  readonly apiRoute?: string;
779
- /** Population depth used with `serverURL`. Defaults to `1`. */
657
+ /** Population depth used with `serverURL`. Required with it; `defaults: 'v1'` falls back to `1`. */
780
658
  readonly mergeDepth?: number;
781
- /**
782
- * Custom fetch implementation for the `serverURL` merge request —
783
- * the equivalent of the official client's `requestHandler`. Use it
784
- * to attach auth headers or route through your own proxy. Only
785
- * available on the programmatic client (functions cannot be
786
- * serialised into the inline script).
787
- */
659
+ /** Custom fetch for the `serverURL` merge request: attach auth headers or route through a proxy. */
788
660
  readonly mergeFetch?: typeof fetch;
789
- /** Enable verbose console debug output. Defaults to dev-mode detection. */
661
+ /** Verbose console output. Defaults to dev-mode detection. */
790
662
  readonly debug?: boolean;
791
- /** Debounce window for incoming updates. Defaults to 50 ms. */
663
+ /** Debounce window for incoming updates. Default 50 ms. */
792
664
  readonly debounceMs?: number;
793
- /**
794
- * Heartbeat timeout in ms. Defaults to `0` (disabled) because the
795
- * Payload admin sends messages only on form edits — there is no
796
- * protocol keepalive, so an idle timeout would produce false
797
- * disconnects.
798
- */
665
+ /** Heartbeat timeout in ms; `0` disables it (default), because the admin posts only on edits. */
799
666
  readonly heartbeatMs?: number;
800
- /** IntersectionObserver rootMargin. Defaults to `200px`. */
667
+ /** IntersectionObserver `rootMargin`. Default `200px`. */
801
668
  readonly intersectionRootMargin?: string;
802
- /**
803
- * Restrict each update to the bindings owned by the document it describes.
804
- *
805
- * Ownership is declared in markup with `data-payload-owner`, resolved from
806
- * the nearest ancestor: `global:<slug>`, `collection:<slug>`, or
807
- * `collection:<slug>:<id>`. Payload already sends the edited document's
808
- * identity, so nothing else needs configuring.
809
- *
810
- * Defaults to `false`, which keeps the 1.x behaviour of matching on the
811
- * field name alone. Enable it when one page previews more than one document
812
- * and a field name is therefore no longer a unique identity. While enabled,
813
- * a binding without an owner is never updated.
814
- */
669
+ /** Update only bindings under a matching `data-payload-owner`; an unowned binding is then never updated. Default `false`. */
815
670
  readonly scopeBindingsByOwner?: boolean;
816
- /** 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`. */
817
680
  readonly disableVisibilityGate?: boolean;
818
- /** 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`. */
819
682
  readonly visibilityGateThreshold?: number;
820
- /**
821
- * Mount an `aria-live` region for connections, applied updates, and mounted
822
- * heartbeat-timeout disconnects. Destroy releases it synchronously. Default `true`.
823
- */
683
+ /** Mount an `aria-live` region for connections, applied updates and heartbeat disconnects. Default `true`. */
824
684
  readonly enableA11y?: boolean;
825
- /** Locale used to pick A11y announcement strings. Defaults to detected locale. */
685
+ /** Locale of the announcement strings. Defaults to the detected locale. */
826
686
  readonly a11yLocale?: string;
827
- /** Document root, defaults to `document`. */
687
+ /** Document root. Default `document`. */
828
688
  readonly root?: Document | Element;
829
- /** 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`. */
830
694
  readonly disableReferrerDetection?: boolean;
831
- /** Disable the localhost dev-mode pattern matcher. */
695
+ /** Disable the dev-mode localhost matcher. Default `false`. */
832
696
  readonly disableLocalhostMatching?: boolean;
833
- /** 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`. */
834
704
  readonly autoStart?: boolean;
835
- /**
836
- * Optional preview-token validator. When set, every data update
837
- * must carry a `previewToken` field that this function approves.
838
- *
839
- * ⚠️ `previewToken` is an extension of THIS library, not part of the
840
- * stock Payload postMessage protocol — the Payload admin never sends
841
- * one. Only enable this when a custom admin component adds the token
842
- * to outgoing messages; against an unmodified Payload admin it would
843
- * drop every update.
844
- */
705
+ /** Preview-token validator; every update must then carry a `previewToken` it approves. Stock Payload sends none. */
845
706
  readonly validateToken?: (token: string | undefined, origin: string) => boolean | Promise<boolean>;
846
707
  }
847
708
 
848
709
  /**
849
- * Public, high-level `LivePreviewClient`.
850
- *
851
- * The client wraps a `LivePreviewRuntime` with:
852
- *
853
- * - Per-instance event emitter (no singleton).
854
- * - Per-instance plugin manager.
855
- * - Custom-renderer registration.
856
- * - Field-name transforms frozen into revision-bound scheduler entries before
857
- * their later attribute or renderer dispatch.
858
- *
859
- * Consumers who want full programmatic control instantiate
860
- * `LivePreviewClient` directly. Consumers who just want a working
861
- * inline preview should use `generateInlineScript()` instead.
862
- *
863
- * @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.
864
713
  */
865
714
 
866
715
  declare class LivePreviewClient {
867
716
  #private;
868
- constructor(config?: LivePreviewClientConfig);
717
+ constructor(rawConfig?: LivePreviewClientConfig);
869
718
  /**
870
- * Start the runtime. Returns `true` when this client is eligible and runtime
871
- * startup is active or scheduled for DOM readiness. Repeated calls on an
872
- * active client also return `true`; `false` means the client was destroyed or
873
- * the page is outside a preview context. A deferred startup can still fail,
874
- * 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.
875
721
  */
876
722
  start(): boolean;
877
723
  /**
878
- * Release the message ingress while keeping this client usable.
879
- *
880
- * For the document lifecycle, not for teardown: a back/forward-cache restore
881
- * does not re-run module scripts, so a client that stays attached across
882
- * `pagehide` returns bound to a frozen page and silently stops updating.
883
- * Plugins, renderers and transforms survive; `resume()` brings the same
884
- * 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.
885
727
  */
886
728
  suspend(): boolean;
887
- /**
888
- * Reacquire after `suspend()`.
889
- *
890
- * Deliberately the same path as `start()`: a restored document needs the
891
- * cache rebuilt, the observers rebound and the handshake rebroadcast, which
892
- * is exactly what starting does. Returns `false` for a destroyed client or
893
- * one that was never started.
894
- */
729
+ /** Reacquire after `suspend()`: the same path as `start()`, because a restored document needs the same rebuild. */
895
730
  resume(): boolean;
896
- /**
897
- * Stop the runtime and tear down every plugin. Idempotent.
898
- */
731
+ /** Stop the runtime and tear down every plugin. Idempotent; concurrent callers share one promise. */
899
732
  destroy(): Promise<void>;
900
- /**
901
- * Register a plugin.
902
- */
903
733
  use(plugin: LivePreviewPlugin): Promise<void>;
904
- /** 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. */
905
735
  unuse(name: string): Promise<void>;
906
- /** Rebuild the element cache manually. */
907
736
  refreshCache(): void;
908
- /** Read-only access to the event emitter for `on`/`once`/`off`. */
909
737
  get events(): EventEmitter;
910
- /** Names of currently registered plugins. */
738
+ /** Names of the registered plugins. */
911
739
  get plugins(): readonly string[];
912
- /** Current connection status. */
913
740
  get status(): 'disconnected' | 'connecting' | 'connected';
914
- /** Number of valid updates received so far. */
915
741
  /**
916
- * Point-in-time read of runtime state, for diagnosing a preview that is not
917
- * updating. Performs no I/O and transmits nothing.
918
- *
919
- * Read `bindings.orphanFields` when a field refuses to update, and
920
- * `scheduler.deferred` together with `scheduler.visibilityGateActive` when
921
- * 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.
922
744
  */
923
745
  inspect(): LivePreviewInspection;
746
+ /** Number of accepted updates so far. */
924
747
  get updateCount(): number;
925
748
  /** `true` once `destroy()` has been called. */
926
749
  get destroyed(): boolean;
927
750
  }
928
- /**
929
- * Convenience factory: instantiate the client, returning `null` when
930
- * the page is not currently a preview context (top-level navigation).
931
- * Useful for SSR-style integrations that import this from any context.
932
- */
751
+ /** A started client, or `null` when the page is not a preview context. */
933
752
  declare function initLivePreview(config?: LivePreviewClientConfig): LivePreviewClient | null;
934
753
 
935
- /**
936
- * Environment introspection helpers.
937
- *
938
- * The detection layer needs to know whether the runtime is currently
939
- * running inside an iframe, in a popup, in development mode, and
940
- * whether environment variables are accessible. None of these checks
941
- * is dramatic on its own, but having a single, tested module avoids
942
- * the duplicated, subtly-different `typeof process` snippets that
943
- * accumulated in the legacy implementation.
944
- *
945
- * @module @detection/environment
946
- */
947
- /**
948
- * Returns `true` when the current window is inside an iframe.
949
- *
950
- * Uses a try/catch around `window.self !== window.top` because the
951
- * comparison throws in cross-origin iframes — and that very throw is
952
- * itself a definitive "yes, I am in an iframe" signal.
953
- */
954
- declare function isInIframe(): boolean;
955
- /**
956
- * Returns `true` when the current window was opened via `window.open()`
957
- * and the opener reference is still accessible.
958
- */
959
- declare function isInPopup(): boolean;
960
- /**
961
- * Returns `true` when running in a preview-capable context: iframe or
962
- * popup. Anything else (top-level navigation) should skip live preview
963
- * initialisation entirely.
964
- */
965
- declare function isInPreviewContext(): boolean;
966
- /**
967
- * Returns `true` when the runtime appears to be in development mode.
968
- *
969
- * Detection order:
970
- * 1. `process.env.NODE_ENV !== 'production'`
971
- * 2. `import.meta.env.DEV === true` (Vite / Astro / SvelteKit)
972
- * 3. `window.location.hostname` is `localhost`/`127.0.0.1`
973
- *
974
- * Returns `false` when none of those are available.
975
- */
976
- declare function isDevMode(): boolean;
977
-
978
- /**
979
- * Origin detection and matching.
980
- *
981
- * Builds the set of trusted Payload admin origins and exposes a fast
982
- * matcher used by the message bus. Three sources, in precedence order:
983
- *
984
- * 1. Explicit origins passed via configuration. Always trusted.
985
- * 2. `document.referrer` — **only when no explicit origins are
986
- * configured**. The browser writes the embedder's origin here
987
- * when the iframe loads; using it as a fallback gives a
988
- * zero-config path, but it must never widen an explicit
989
- * allow-list (the referrer is whoever framed the page, which in
990
- * an attack is the attacker).
991
- * 3. Localhost pattern (any port on `localhost` / `127.0.0.1`),
992
- * enabled when development mode is detected. Replaces the
993
- * legacy hand-rolled port list.
994
- *
995
- * Once the parent's origin is confirmed by an accepted data-bearing
996
- * update, callers can call `lockOrigin()` to drop every other candidate,
997
- * making subsequent matches exact. A data-less `ready` handshake does
998
- * not lock the detector.
999
- *
1000
- * @module @detection/origin
1001
- */
1002
- interface OriginDetectorOptions {
1003
- /** Explicit additional origins to trust. Always allowed. */
1004
- readonly additionalOrigins?: readonly string[];
1005
- /** Set to `false` to opt out of `document.referrer` detection. */
1006
- readonly enableReferrerDetection?: boolean;
1007
- /** Set to `false` to opt out of localhost-pattern matching. */
1008
- readonly enableLocalhostMatching?: boolean;
1009
- /**
1010
- * Override the dev-mode detection. When `undefined`, dev mode is
1011
- * inferred from the runtime (see `isDevMode`).
1012
- */
1013
- readonly forceDevMode?: boolean;
1014
- /** Override `document.referrer` for tests. */
1015
- readonly referrer?: string;
1016
- }
1017
- /**
1018
- * Holds the resolved origin policy and exposes a matcher.
1019
- *
1020
- * The detector is stateful: it starts with the union of all candidate
1021
- * sources, then narrows down to a single, verified origin once the
1022
- * lifecycle calls `lockOrigin()` for its first accepted data-bearing
1023
- * update.
1024
- */
1025
- declare class OriginDetector {
1026
- #private;
1027
- constructor(options?: OriginDetectorOptions);
1028
- /**
1029
- * Returns `true` when `origin` is trusted under the current policy.
1030
- *
1031
- * If an origin has been locked, only that exact value matches.
1032
- * Otherwise explicit origins and (in dev mode) localhost-pattern
1033
- * matches are accepted. The referrer-derived origin is a **fallback,
1034
- * not a union member**: it only counts when NO explicit origins are
1035
- * configured. `document.referrer` always names whoever actually
1036
- * framed the page — if it were unioned with the explicit list, any
1037
- * site embedding the page in an iframe would become a trusted
1038
- * postMessage sender despite the consumer having pinned the admin
1039
- * origin explicitly.
1040
- */
1041
- matches(origin: string): boolean;
1042
- /**
1043
- * Narrow the trusted set to a single origin after an accepted
1044
- * data-bearing update. Subsequent calls to `matches()` only allow
1045
- * `origin`.
1046
- *
1047
- * Returns `true` if locking succeeded; `false` if the candidate is
1048
- * not currently considered trusted (which would defeat the purpose
1049
- * of locking).
1050
- */
1051
- lockOrigin(origin: string): boolean;
1052
- /**
1053
- * Release the origin lock so any allow-listed origin is trusted
1054
- * again until the next accepted data-bearing update. The lifecycle
1055
- * calls this on heartbeat timeout — after the trusted parent disappears,
1056
- * a different (still-allowed) origin should be free to reconnect.
1057
- *
1058
- * Returns the previously-locked origin, or `undefined` when nothing
1059
- * was locked.
1060
- */
1061
- unlockOrigin(): string | undefined;
1062
- /** The currently locked origin, or `undefined`. */
1063
- get lockedOrigin(): string | undefined;
1064
- /**
1065
- * Snapshot of every origin currently allowed by the policy. Used by
1066
- * the message bus to broadcast `ready` messages.
1067
- *
1068
- * Localhost pattern matches are expanded into the common dev ports
1069
- * so the handshake can find a Payload running on its usual port.
1070
- */
1071
- enumerate(): string[];
1072
- /**
1073
- * Reports whether `document.referrer` contributed at least one
1074
- * origin. Useful for surfacing a console warning when running in
1075
- * production without an explicit origin and the referrer is absent
1076
- * (because of a `Referrer-Policy: no-referrer` header).
1077
- */
1078
- get referrerWasAvailable(): boolean;
1079
- /**
1080
- * Indicates whether the detector is operating in a "production-without-
1081
- * explicit-origin" configuration. When `true`, the host should log a
1082
- * loud warning — silent connection failure is the worst outcome.
1083
- */
1084
- get isProductionUnconfigured(): boolean;
1085
- /**
1086
- * `true` when the referrer is the ONLY trust source (no explicit
1087
- * origins, no localhost matching). In that mode any site that frames
1088
- * the page becomes a trusted postMessage sender — `document.referrer`
1089
- * always names the actual embedder. Content is still sanitized, but
1090
- * the origin allow-list no longer restricts who may drive it. Hosts
1091
- * should surface a warning suggesting explicit `allowedOrigins` and
1092
- * a `frame-ancestors` policy in production.
1093
- */
1094
- get isReferrerOnlyTrust(): boolean;
1095
- }
1096
-
1097
- /**
1098
- * Locale detection.
1099
- *
1100
- * The runtime needs a sensible default locale for date/number renders
1101
- * before Payload tells us the active locale. Order of precedence:
1102
- *
1103
- * 1. The `lang` attribute on `<html>`.
1104
- * 2. `navigator.language`.
1105
- * 3. The hard-coded fallback `'en'`.
1106
- *
1107
- * The previous version of this library defaulted to `'de'` — a German
1108
- * default that surprised consumers in other locales. We now use the
1109
- * browser hint and fall back to English.
1110
- *
1111
- * @module @detection/locale
1112
- */
1113
- /**
1114
- * Returns a BCP-47 locale string suitable for `Intl.*` constructors.
1115
- *
1116
- * Always returns a non-empty string. When no signal is available the
1117
- * fallback (`'en'`) is returned.
1118
- */
1119
- declare function detectInitialLocale(): string;
1120
-
1121
- /**
1122
- * Single source of truth for the library version.
1123
- *
1124
- * Both the npm package surface (`src/index.ts`, `src/core-entry.ts`)
1125
- * and the inline runtime (`src/core/runtime.ts`, which gets bundled by
1126
- * esbuild and embedded into `runtime.generated.ts`) read the version
1127
- * from here so they never drift.
1128
- *
1129
- * Imports `package.json` directly — tsc, tsup, and esbuild all inline
1130
- * the value at build time when `resolveJsonModule` is enabled.
1131
- *
1132
- * @module @/version
1133
- */
1134
- declare const VERSION: string;
1135
-
1136
- /**
1137
- * HTML-escape primitives.
1138
- *
1139
- * Implementations follow the OWASP XSS Prevention Cheat Sheet: each
1140
- * special character is replaced with its decimal or hex entity. The
1141
- * functions are isomorphic — they have no dependency on the DOM and
1142
- * can run in any JavaScript environment (browser, Node, Deno, Bun).
1143
- *
1144
- * @module @security/escape
1145
- */
1146
- /**
1147
- * Escape every HTML-significant character in `text` so the result is
1148
- * safe to interpolate into any HTML context (element body or attribute).
1149
- *
1150
- * - Coerces non-strings via `String(value)`; pass `null`/`undefined`
1151
- * intentionally — they become `"null"`/`"undefined"`.
1152
- * - Cannot fail; returns `""` for the empty input.
1153
- */
754
+ /** HTML and CSS escaping primitives (OWASP XSS Prevention Cheat Sheet). DOM-free. */
755
+ /** Escape `text` for an element body or a *quoted* attribute value; strings only, and unquoted attributes are not covered. */
1154
756
  declare function escapeHtml(text: string): string;
1155
- /**
1156
- * Escape characters that would break out of a double-quoted HTML
1157
- * attribute value. Stricter than `escapeHtml` for body content but
1158
- * preserves URL characters (`/`, `=`, etc.) that are common in
1159
- * `href` and `src` values.
1160
- *
1161
- * Only call this for values that have already been validated as safe
1162
- * URLs or trusted strings; it does NOT block dangerous protocols.
1163
- */
757
+ /** Escape a value for a *quoted* HTML attribute, leaving URL characters intact; it validates no scheme, so run `isSafeUrl()` on `href`/`src` first. */
1164
758
  declare function escapeHtmlAttribute(value: string): string;
1165
759
 
1166
760
  /**
1167
- * URL safety validation.
1168
- *
1169
- * Single source of truth for URL-protocol decisions used by both the
1170
- * inline runtime and the high-level client. The previous version of
1171
- * this library had two implementations that subtly disagreed; this
1172
- * module replaces both.
1173
- *
1174
- * Policy:
1175
- * - Allow only the protocols in `SAFE_PROTOCOLS`.
1176
- * - Reject every other protocol explicitly, including `javascript:`,
1177
- * `data:`, `vbscript:`, `file:`, `blob:`, and any custom scheme.
1178
- * - Empty strings are NOT considered safe (this is a tightening from
1179
- * the legacy behaviour and avoids producing `<a href="">` markup).
1180
- *
1181
- * @module @security/url-validator
1182
- */
1183
- /**
1184
- * Returns `true` only when `url` is one of the explicitly allowed forms:
1185
- *
1186
- * - Absolute URL with `http:`, `https:`, `mailto:`, or `tel:` protocol
1187
- * - Protocol-relative URL (`//example.com/...`)
1188
- * - Same-origin absolute path (`/foo`)
1189
- * - Hash or query fragment (`#section`, `?q=1`)
1190
- * - Plain relative path (`foo/bar`, `./foo`, `../foo`)
761
+ * URL safety: the one scheme allow-list the runtime, the client and the
762
+ * sanitizer share. Empty strings are not safe (`<a href="">` is never emitted).
1191
763
  */
764
+ /** `true` only for absolute `http`/`https`/`mailto`/`tel` URLs, protocol-relative URLs, paths, `#`/`?` fragments and relative paths. */
1192
765
  declare function isSafeUrl(url: unknown): boolean;
1193
- /**
1194
- * Returns `true` when the URL points to an external HTTP(S) origin.
1195
- * Protocol-relative URLs (`//example.com/...`) count as external —
1196
- * they resolve to another origin and need the same `noopener`
1197
- * hardening as absolute ones.
1198
- *
1199
- * Used by renderers to decide whether to add `target="_blank"` and the
1200
- * `noopener noreferrer` rel attributes to anchors. Inputs that fail
1201
- * `isSafeUrl()` always return `false`.
1202
- */
766
+ /** Whether a safe URL points at another HTTP(S) origin, protocol-relative forms included, and so needs `noopener` hardening. */
1203
767
  declare function isExternalHttpUrl(url: string): boolean;
1204
768
 
769
+ /**
770
+ * `'strict'` (default) strips `id`, `name` and `data-payload-*` and passes
771
+ * other `data-*` only via `allowedDataAttributes`; `'compat'` keeps them.
772
+ */
773
+ type SanitizerPolicyMode = 'compat' | 'strict';
1205
774
  interface SanitizeOptions {
1206
- /** Extra tags to allow beyond the built-in list. Lower-case, untrimmed. */
775
+ /** Overrides the module default set by `setSanitizerPolicy()`. */
776
+ readonly policy?: SanitizerPolicyMode;
777
+ /** `data-*` attributes (full names) that pass under `'strict'`; `data-payload-*` never does. */
778
+ readonly allowedDataAttributes?: readonly string[];
779
+ /**
780
+ * Keep `input`, `button`, `textarea`, `select`, `option` and `label` — only
781
+ * for markup the page author wrote, never for CMS content. `form`,
782
+ * handlers, `style` and unsafe URLs stay dropped.
783
+ */
784
+ readonly allowFormControls?: boolean;
785
+ /** Extra tags to allow. Lower-case, untrimmed. */
1207
786
  readonly additionalAllowedTags?: readonly string[];
1208
- /** Extra per-tag attributes to allow. Tag and attribute names must be lower-case. */
787
+ /**
788
+ * Extra per-tag attributes, lower-case. Cannot re-admit handlers, `style`,
789
+ * or — under `'strict'` — `id`, `name` and `data-payload-*`.
790
+ */
1209
791
  readonly additionalAllowedAttributes?: Readonly<Record<string, readonly string[]>>;
792
+ /**
793
+ * The input is a page-author item template, so `'strict'` keeps the
794
+ * applier's reconciliation attributes (`TEMPLATE_ATTRIBUTES`) and nothing
795
+ * else — every other `data-payload-*` is still stripped, so a template can
796
+ * never add a binding.
797
+ */
798
+ readonly templateMode?: boolean;
1210
799
  }
1211
- /**
1212
- * Minimal `Document`-shaped surface the sanitizer needs. Any DOM
1213
- * implementation (browser, jsdom, linkedom, happy-dom, parse5 + a
1214
- * thin adapter) that produces `<template>`-style elements works.
1215
- */
800
+ /** The `Document` surface the sanitizer needs; jsdom, linkedom and happy-dom all provide it. */
1216
801
  interface SanitizerDocument {
1217
802
  createElement: (tagName: string) => {
1218
803
  innerHTML: string;
1219
804
  readonly content: ParentNode;
1220
805
  };
1221
806
  }
1222
- /**
1223
- * Inject a `Document` implementation for the sanitizer to use when
1224
- * `globalThis.document` is unavailable.
1225
- *
1226
- * Server-side renderers (Node, Bun, Deno without DOM globals) can wire
1227
- * this up once at startup with any of the popular pure-JS DOM
1228
- * libraries:
1229
- *
1230
- * ```ts
1231
- * // linkedom — recommended (smallest, fastest)
1232
- * import { parseHTML } from 'linkedom';
1233
- * import { setSanitizerDocument } from 'payload-live-preview';
1234
- * const { document } = parseHTML('<!doctype html><html><body></body></html>');
1235
- * setSanitizerDocument(document);
1236
- * ```
1237
- *
1238
- * ```ts
1239
- * // jsdom — heavier, but already in many SSR pipelines
1240
- * import { JSDOM } from 'jsdom';
1241
- * setSanitizerDocument(new JSDOM().window.document);
1242
- * ```
1243
- *
1244
- * Pass `null` to clear a previous override (mostly useful for tests).
1245
- */
807
+ /** Supply a `Document` for SSR, e.g. linkedom's `parseHTML(...).document`; `null` clears it. */
1246
808
  declare function setSanitizerDocument(doc: SanitizerDocument | null): void;
1247
809
  /**
1248
- * Sanitize `html` and return safe HTML.
1249
- *
1250
- * The function never returns `undefined`/`null`. On malformed input the
1251
- * browser parser silently recovers and the sanitizer continues from
1252
- * the recovered tree.
810
+ * Sanitize `html`; malformed input is recovered by the parser first.
1253
811
  *
1254
812
  * @throws {SanitizerEnvironmentError} when no DOM is available.
1255
813
  */
1256
814
  declare function sanitizeHtml(html: string, options?: SanitizeOptions): string;
1257
815
 
1258
816
  /**
1259
- * Content-Security-Policy helpers.
1260
- *
1261
- * Two responsibilities:
1262
- *
1263
- * 1. Generate cryptographic nonces so consumers can opt into a strict
1264
- * `script-src 'nonce-...'` policy and avoid `'unsafe-inline'`.
1265
- * 2. Build the `frame-ancestors` directive value, restricting which
1266
- * origins may embed the preview page in an iframe.
1267
- *
1268
- * Both functions are isomorphic — they work in browsers, Node, and
1269
- * edge runtimes that expose Web Crypto via `globalThis.crypto`.
1270
- *
1271
- * @module @security/csp
1272
- */
1273
- /**
1274
- * Minimal Web-Crypto surface area we actually call. The full `Crypto`
1275
- * type drags in browser-only declarations that don't typecheck cleanly
1276
- * against Node's `webcrypto` shape — this narrower interface accepts
1277
- * both without an unsafe cast.
817
+ * Content-Security-Policy helpers: nonce generation, directive builders and
818
+ * the header merge every adapter uses. Isomorphic; needs only Web Crypto.
1278
819
  */
820
+ /** The one Web Crypto method this module calls; accepts Node's `webcrypto` without a cast. */
1279
821
  interface WebCryptoLike {
1280
822
  getRandomValues: <T extends ArrayBufferView | null>(array: T) => T;
1281
823
  }
1282
- /**
1283
- * Inject a Web Crypto implementation to use when `globalThis.crypto`
1284
- * is unavailable.
1285
- *
1286
- * Node.js exposed `globalThis.crypto` globally starting with v19. On
1287
- * Node 18 (still LTS at time of writing) consumers can flip the
1288
- * `--experimental-global-webcrypto` flag *or* call this function once
1289
- * at startup:
1290
- *
1291
- * ```ts
1292
- * import { webcrypto } from 'node:crypto';
1293
- * import { setCspCrypto } from 'payload-live-preview';
1294
- * setCspCrypto(webcrypto as { getRandomValues: typeof webcrypto.getRandomValues });
1295
- * ```
1296
- *
1297
- * Browser bundles never need to call this — `globalThis.crypto` is
1298
- * always present there, and the override only matters when the global
1299
- * is missing.
1300
- *
1301
- * Pass `null` to clear a previous override (mostly useful for tests).
1302
- */
824
+ /** Supply a Web Crypto implementation for runtimes without `globalThis.crypto`; `null` clears it. */
1303
825
  declare function setCspCrypto(crypto: WebCryptoLike | null): void;
1304
- /**
1305
- * Generate a base64url-encoded cryptographic nonce.
1306
- *
1307
- * Uses `crypto.getRandomValues` everywhere — falls back to throwing
1308
- * if no Web Crypto implementation is available, because a predictable
1309
- * nonce would silently defeat the entire CSP.
1310
- *
1311
- * @param bytes Number of random bytes (defaults to 16 → 128-bit entropy).
1312
- */
826
+ /** A base64url nonce of `bytes` random bytes (default 16); throws without Web Crypto, since a predictable nonce silently defeats the CSP. */
1313
827
  declare function generateCspNonce(bytes?: number): string;
1314
828
  interface FrameAncestorsOptions {
1315
829
  readonly self?: boolean;
1316
830
  readonly origins?: readonly string[];
1317
831
  readonly allowNone?: boolean;
1318
832
  }
1319
- /**
1320
- * Build the value for `frame-ancestors` in a `Content-Security-Policy`
1321
- * header.
1322
- *
1323
- * Returns `'none'` if `allowNone: true` and no other sources are
1324
- * provided. Otherwise concatenates `'self'` (if requested) with the
1325
- * given origins, deduplicating and trimming entries.
1326
- *
1327
- * @example
1328
- * `frame-ancestors ${buildFrameAncestors({ self: true, origins: [adminUrl] })}`
1329
- */
833
+ /** The `frame-ancestors` value: `'self'` and the origins, deduplicated; `'none'` when empty. */
1330
834
  declare function buildFrameAncestors(options?: FrameAncestorsOptions): string;
1331
835
  /**
1332
- * Build a `script-src` value that permits a single nonce plus optional
1333
- * extra sources.
1334
- *
1335
- * `'strict-dynamic'` is **opt-in**: under CSP 3 it makes browsers
1336
- * ignore `'self'` and every host source in the directive, so any
1337
- * parser-inserted script without a nonce (framework hydration scripts,
1338
- * consumer `<script>` tags) would be blocked. Only enable it when every
1339
- * script on the page carries the nonce.
836
+ * A `script-src` value permitting one nonce plus optional extra sources.
837
+ * `'strict-dynamic'` is opt-in: it makes CSP 3 browsers ignore `'self'` and
838
+ * host sources, so every script on the page must then carry the nonce.
1340
839
  */
1341
840
  declare function buildScriptSrcWithNonce(nonce: string, options?: {
1342
841
  readonly self?: boolean;
@@ -1345,126 +844,235 @@ declare function buildScriptSrcWithNonce(nonce: string, options?: {
1345
844
  }): string;
1346
845
 
1347
846
  /**
1348
- * Protocol-version negotiation.
1349
- *
1350
- * ⚠️ Reality check: **stock Payload does not version its live-preview
1351
- * postMessage protocol.** The admin never sends a `protocolVersion`
1352
- * field and ignores extra fields in our `ready` handshake. Observed
1353
- * wire behaviour:
1354
- *
1355
- * - Payload **2.x** sent `fieldSchemaJSON` with the first update
1356
- * (client-side merge era).
1357
- * - Payload **3.x** removed `fieldSchemaJSON` entirely; the official
1358
- * client re-fetches merged documents through the REST API instead.
1359
- * - No Payload version sends `previewToken` or any keepalive.
1360
- *
1361
- * This module therefore only matters for NON-stock admin panels — a
1362
- * custom `window.postMessage` sender can advertise a version and this
1363
- * library will negotiate `min(ours, theirs)` and expose capability
1364
- * flags. Against stock Payload, `theirs` stays `undefined`, negotiation
1365
- * collapses to v1, and nothing is gated on it: schema handling
1366
- * activates whenever `fieldSchemaJSON` is actually present, and token
1367
- * validation only runs when the consumer opts in.
1368
- *
1369
- * Version ladder (this library's own numbering, not Payload's):
1370
- *
1371
- * - **v1** — data + slug + locale (matches stock Payload of any era).
1372
- * - **v2** — remote sends `fieldSchemaJSON` (Payload 2.x behaviour).
1373
- * - **v3** — remote sends `previewToken` (custom senders only).
1374
- * - **v4** — nested-array recursion / richer document events
1375
- * (custom senders only).
1376
- *
1377
- * @module @core/protocol-version
847
+ * The authorized preview context's shape and brand, in the leaf `types` domain
848
+ * so producer and consumers recognise the same context. See ADR 0006.
849
+ */
850
+ /**
851
+ * The brand key, in the type and on the object.
852
+ *
853
+ * This was a `unique symbol` until 2026-08-30. A `unique symbol` is nominal per
854
+ * *declaration*, and the published entries each carry their own declaration
855
+ * file — so `AuthorizedPreviewContext` from the root barrel was a different
856
+ * type from the one in `./server` or in an adapter, and the documented flow
857
+ * (authorize once, hand the context to the adapter hook and to the draft read)
858
+ * did not type-check in any combination a consumer can write.
859
+ *
860
+ * That is the same trap the runtime brand below already avoids by using
861
+ * `Symbol.for` rather than `Symbol()`; the lesson simply had not reached the
862
+ * type. A shared string literal is identical in every bundle for the same
863
+ * reason a registry symbol is.
864
+ *
865
+ * It is not what makes a context trustworthy. `isAuthorizedPreviewContext`
866
+ * decides that, on the registry symbol and on `Object.isFrozen`, and every
867
+ * consumption point calls it — the adapter policy, the draft read and the
868
+ * binding emitter. Writing this key by hand buys nothing.
869
+ */
870
+ declare const AUTHORIZED_PREVIEW_BRAND_KEY = "payload-live-preview.authorized-preview-context";
871
+ type PreviewAuthorizationStrategyName = 'payload-session' | 'signed-token' | 'verifier';
872
+ /** What a context is bound to; every field is optional because every strategy binds differently. */
873
+ interface AuthorizedPreviewScope {
874
+ /** The site origin the credential was issued for. */
875
+ readonly audience?: string;
876
+ /** The request pathname the credential is valid for. */
877
+ readonly path?: string;
878
+ readonly locale?: string;
879
+ }
880
+ /** The verdict of a successful `authorizePreviewRequest()`: frozen, branded, produced only there. Carry it, do not rebuild it. */
881
+ interface AuthorizedPreviewContext {
882
+ readonly [AUTHORIZED_PREVIEW_BRAND_KEY]: true;
883
+ readonly strategy: PreviewAuthorizationStrategyName;
884
+ /** The user id, token subject, or whatever the verifier named. */
885
+ readonly subject: string | undefined;
886
+ /** Unix milliseconds. */
887
+ readonly authorizedAt: number;
888
+ /** Unix milliseconds, or `undefined` when the strategy does not know. */
889
+ readonly expiresAt: number | undefined;
890
+ readonly scope: AuthorizedPreviewScope;
891
+ /** The minimal request material a draft read forwards to Payload — one verified cookie, never the whole `Cookie` header. */
892
+ readonly payloadHeaders: Readonly<Record<string, string>>;
893
+ }
894
+ /** Whether `value` was produced by `authorizePreviewRequest()`; a copy or JSON round trip is not. */
895
+ declare function isAuthorizedPreviewContext(value: unknown): value is AuthorizedPreviewContext;
896
+
897
+ /**
898
+ * Where the runtime is running: framed, opened, or on a developer's machine.
899
+ * Nothing reads bundler-injected variables; a Vite consumer passes
900
+ * `import.meta.env.X` explicitly (docs/astro.md).
901
+ */
902
+ declare function isInIframe(): boolean;
903
+ declare function isInPopup(): boolean;
904
+ /** An iframe or a popup; top-level navigation never starts the preview. */
905
+ declare function isInPreviewContext(): boolean;
906
+ /** Under Node, `NODE_ENV !== 'production'`; in a browser the hostname, which is the only signal a bundle has. */
907
+ declare function isDevMode(): boolean;
908
+
909
+ /**
910
+ * Which admin origins the runtime trusts: explicit ones always, the
911
+ * `document.referrer` origin only when none is configured, any localhost port
912
+ * in development. See docs/security.md §1.
1378
913
  */
914
+ interface OriginDetectorOptions {
915
+ /** Explicit origins to trust. Always allowed. */
916
+ readonly additionalOrigins?: readonly string[];
917
+ /** Set to `false` to ignore `document.referrer`. */
918
+ readonly enableReferrerDetection?: boolean;
919
+ /** Set to `false` to ignore the localhost pattern even in development. */
920
+ readonly enableLocalhostMatching?: boolean;
921
+ /** Override dev-mode detection. */
922
+ readonly forceDevMode?: boolean;
923
+ /** Override `document.referrer` for tests. */
924
+ readonly referrer?: string;
925
+ }
926
+ /** The trusted-origin policy: the union of the candidate sources, narrowed to one by `lockOrigin()`. */
927
+ declare class OriginDetector {
928
+ #private;
929
+ constructor(options?: OriginDetectorOptions);
930
+ /** Whether `origin` is trusted: the locked origin alone once locked, otherwise the policy above. */
931
+ matches(origin: string): boolean;
932
+ /** Narrow trust to `origin`; refused (`false`) when it is not currently trusted. */
933
+ lockOrigin(origin: string): boolean;
934
+ /** Release the lock (heartbeat timeout) so another allow-listed origin may reconnect. Returns the previous lock. */
935
+ unlockOrigin(): string | undefined;
936
+ get lockedOrigin(): string | undefined;
937
+ /** Every origin the `ready` handshake is broadcast to; the localhost pattern expands to the common dev ports. */
938
+ enumerate(): string[];
939
+ /** Whether `document.referrer` contributed an origin (absent under `Referrer-Policy: no-referrer`). */
940
+ get referrerWasAvailable(): boolean;
941
+ /** Framed, outside development, with no trusted origin at all: the host should warn (LP0101). */
942
+ get isProductionUnconfigured(): boolean;
943
+ /** The referrer is the only trust source, so any framing site may drive the preview (LP0102). */
944
+ get isReferrerOnlyTrust(): boolean;
945
+ }
946
+
947
+ /** The locale for `Intl` renders before Payload names one: `<html lang>`, then `navigator.language`, then `'en'`. */
948
+ declare function detectInitialLocale(): string;
949
+
950
+ /**
951
+ * Protocol negotiation. A capability is granted either by the version a peer
952
+ * announces or by observation, since the stock Payload admin announces none:
953
+ * what it can do is read off the messages it sends. See ADR 0010.
954
+ */
955
+ /** Bumped only when this library's understanding of the wire format changes. */
1379
956
  declare const LIBRARY_PROTOCOL_VERSION = 4;
1380
- /** Capability flag → minimum protocol version it requires. */
1381
- declare const CAPABILITY_REQUIREMENTS: Readonly<Record<string, number>>;
1382
- type ProtocolCapability = keyof typeof CAPABILITY_REQUIREMENTS;
957
+ /** How a capability becomes active. */
958
+ type CapabilitySource = 'version' | 'observed';
959
+ interface CapabilityDeclaration {
960
+ /** Enabled once the negotiated version reaches this (for announced peers). */
961
+ readonly since: number;
962
+ /** The wire signal that grants this capability to a peer announcing no version. */
963
+ readonly observed?: 'schema' | 'locale' | 'preview-token' | 'document-event' | 'relationship-event';
964
+ }
965
+ /** What activates each capability; the prose lives in `CAPABILITY_DOCUMENTATION`. */
966
+ declare const CAPABILITY_DECLARATIONS: {
967
+ readonly basic: {
968
+ readonly since: 1;
969
+ };
970
+ readonly 'schema-json': {
971
+ readonly since: 2;
972
+ readonly observed: "schema";
973
+ };
974
+ readonly locale: {
975
+ readonly since: 2;
976
+ readonly observed: "locale";
977
+ };
978
+ readonly 'preview-token': {
979
+ readonly since: 3;
980
+ readonly observed: "preview-token";
981
+ };
982
+ readonly 'nested-arrays': {
983
+ readonly since: 4;
984
+ };
985
+ readonly 'recursive-diffs': {
986
+ readonly since: 4;
987
+ };
988
+ readonly 'document-events': {
989
+ readonly since: 4;
990
+ readonly observed: "document-event";
991
+ };
992
+ readonly 'relationship-events': {
993
+ readonly since: 4;
994
+ readonly observed: "relationship-event";
995
+ };
996
+ };
997
+ type ProtocolCapability = keyof typeof CAPABILITY_DECLARATIONS;
998
+ /** Every declared capability, in declaration order. */
999
+ declare const PROTOCOL_CAPABILITIES: readonly ProtocolCapability[];
1383
1000
  interface ProtocolNegotiation {
1384
1001
  /** This library's protocol version. */
1385
1002
  readonly ours: number;
1386
- /** The remote party's version, when known. */
1003
+ /** The remote party's version, when it announced one. */
1387
1004
  readonly theirs: number | undefined;
1388
1005
  /** `min(ours, theirs ?? 1)` — the version both sides actually share. */
1389
1006
  readonly negotiated: number;
1390
- /** Capability flags enabled at the negotiated version. */
1391
- readonly capabilities: ReadonlySet<string>;
1007
+ /** Capabilities active at the negotiated version or seen on the wire. */
1008
+ readonly capabilities: ReadonlySet<ProtocolCapability>;
1009
+ /** The subset that was observed on the wire rather than granted by version. */
1010
+ readonly observed: ReadonlySet<ProtocolCapability>;
1392
1011
  }
1393
- /**
1394
- * Compute the negotiated view of the protocol.
1395
- *
1396
- * `theirs === undefined` collapses to v1 — assume the remote party is
1397
- * an older Payload that does not advertise a version.
1398
- *
1399
- * Non-finite or non-positive `theirs` values fall back to v1 as well —
1400
- * we never trust an obviously-corrupted negotiation byte.
1401
- */
1402
- declare function negotiateProtocol(theirs: number | undefined): ProtocolNegotiation;
1012
+ /** Deterministic; the runtime re-runs it whenever either input changes. */
1013
+ declare function negotiateProtocol(theirs: number | undefined, observed?: Iterable<ProtocolCapability>): ProtocolNegotiation;
1403
1014
  declare function hasCapability(negotiation: ProtocolNegotiation, capability: ProtocolCapability): boolean;
1404
1015
 
1405
1016
  /**
1406
- * Type-level path utilities — used by the typed binding DSL to give
1407
- * callers compile-time autocomplete on field names.
1408
- *
1409
- * The same path strings the runtime reads from `data-payload-field`
1410
- * are typed here, so a typo like `'heroTitlee'` is rejected at compile
1411
- * time *and* the runtime side does not need to change.
1412
- *
1413
- * @module @dsl/paths
1017
+ * What each capability tells the runtime, for `inspect()` and consumer
1018
+ * diagnostics. Kept out of the runtime module so the inline bundle ships the
1019
+ * rules and not this prose. See ADR 0010.
1414
1020
  */
1021
+
1022
+ interface CapabilityDocumentation {
1023
+ /** The runtime behaviour that depends on the capability. */
1024
+ readonly gates: string;
1025
+ /** What the runtime does without it. */
1026
+ readonly fallback: string;
1027
+ }
1028
+ declare const CAPABILITY_DOCUMENTATION: Readonly<Record<ProtocolCapability, CapabilityDocumentation>>;
1029
+
1415
1030
  /**
1416
- * All string-typed top-level keys of `T`. Non-string keys (numeric
1417
- * indices on arrays) are filtered out.
1031
+ * What the peer does, derived from the capabilities its messages showed —
1032
+ * the runtime never asks which Payload version it is talking to. Unknown
1033
+ * until the admin reveals its shape. See ADR 0010.
1418
1034
  */
1419
- type FieldName<T> = Extract<keyof T, string>;
1035
+
1036
+ type ProtocolProfileName = 'unknown' | 'payload-2' | 'payload-3';
1037
+ interface ProtocolProfile {
1038
+ readonly name: ProtocolProfileName;
1039
+ /** Where field types come from. */
1040
+ readonly fieldTyping: 'schema' | 'heuristic';
1041
+ /**
1042
+ * Who populates relationships. Payload 2.x's admin posts populated data
1043
+ * (it merges client-side with `fieldSchemaJSON`); Payload 3.x posts raw
1044
+ * form values and the runtime asks the REST API when `serverURL` is set.
1045
+ */
1046
+ readonly populatesRelationships: 'admin' | 'server';
1047
+ /** Whether the peer reports document saves and relationship edits. */
1048
+ readonly documentEvents: boolean;
1049
+ }
1420
1050
  /**
1421
- * Dotted paths into nested object structures, capped at three levels
1422
- * deep — sufficient for Payload's `group`/`tab`/`array` nesting
1423
- * without ballooning the IDE's IntelliSense list past the point of
1424
- * usefulness.
1051
+ * `schema-json` on the wire is the Payload 2.x signature: 3.x removed
1052
+ * `fieldSchemaJSON`. Document or relationship events are 3.x-only. Until
1053
+ * either shows, the peer is unknown and treated like 3.x for merging — the
1054
+ * conservative choice, because a needless REST merge costs a request while
1055
+ * a missing one loses populated relationships.
1425
1056
  */
1057
+ declare function detectProtocolProfile(observed: ReadonlySet<ProtocolCapability>): ProtocolProfile;
1058
+
1059
+ /** Type-level field paths, so a binding name is checked at compile time against the codegen schema. */
1060
+ /** String keys of `T`. */
1061
+ type FieldName<T> = Extract<keyof T, string>;
1062
+ /** Dotted paths into `T`, capped at three levels so IntelliSense stays usable. */
1426
1063
  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 ? {
1427
1064
  [K in Extract<keyof T, string>]: K | (T[K] extends object ? `${K}.${FieldPath<T[K], Prev<Depth>>}` : never);
1428
1065
  }[Extract<keyof T, string>] : never;
1429
- /** Decrement helper for depth-bounded recursion. */
1430
1066
  type Prev<N extends 0 | 1 | 2 | 3> = N extends 3 ? 2 : N extends 2 ? 1 : N extends 1 ? 0 : 0;
1431
- /**
1432
- * Resolve the value type at a dotted path. Returns `unknown` when the
1433
- * path does not exist on `T`, so consumers can opt into stricter
1434
- * `keyof`-based DSLs when they want absolute type safety.
1435
- */
1067
+ /** The value type at a dotted path, or `unknown` when the path does not exist. */
1436
1068
  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;
1437
1069
 
1438
1070
  /**
1439
- * Typed binding helpers — emit the `data-payload-field` attribute pair
1440
- * that the live-preview runtime reads, with compile-time validation
1441
- * of the field name.
1442
- *
1443
- * Two forms are exposed:
1444
- *
1445
- * - **`bind<Schema>('heroTitle')`** — string-literal form. Lightest
1446
- * possible runtime (no Proxy), but a rename in the IDE does NOT
1447
- * follow into the string.
1448
- * - **`bindByPath<Schema>(s => s.heroTitle)`** — proxy form. The
1449
- * callback walks a Proxy that records the access path, returning
1450
- * the same shape as `bind` so the two are interchangeable. Slightly
1451
- * heavier runtime, but rename-safe and discourages typos.
1452
- *
1453
- * Both helpers also accept a richer return — `bind('image', 'src')` —
1454
- * to bind an attribute other than the default `data-payload-field`,
1455
- * matching the cache resolver's `data-payload-attribute` flow.
1456
- *
1457
- * @module @dsl/bind
1071
+ * Typed helpers emitting the `data-payload-*` attributes the runtime reads.
1072
+ * `bindByPath` records the path through a Proxy, so a rename follows.
1458
1073
  */
1459
1074
 
1460
- /**
1461
- * Shape returned by every binding helper — a spreadable HTML attribute
1462
- * record. Astro / JSX / Svelte all accept this style directly:
1463
- *
1464
- * ```astro
1465
- * <h1 {...bind<Homepage>('heroTitle')}>{data.heroTitle}</h1>
1466
- * ```
1467
- */
1075
+ /** A spreadable attribute record: `<h1 {...bind<Homepage>('heroTitle')}>`. */
1468
1076
  interface FieldBindingAttributes {
1469
1077
  readonly 'data-payload-field': string;
1470
1078
  readonly 'data-payload-attribute'?: string;
@@ -1472,140 +1080,79 @@ interface FieldBindingAttributes {
1472
1080
  readonly 'data-payload-richtext'?: string;
1473
1081
  readonly 'data-payload-html'?: string;
1474
1082
  readonly 'data-payload-locale'?: string;
1083
+ readonly 'data-payload-alt'?: string;
1084
+ readonly 'data-payload-href'?: string;
1085
+ readonly 'data-payload-array-template'?: string;
1475
1086
  }
1476
1087
  interface BindOptions {
1477
- /** Override the bound attribute (e.g., `'src'` for `<img>`). Default: textContent. */
1088
+ /** Attribute to write instead of the text content, e.g. `'src'` for `<img>`. */
1478
1089
  readonly attribute?: string;
1479
- /** Explicit field-type override — bypasses schema detection. */
1090
+ /** Explicit field type, bypassing schema detection. */
1480
1091
  readonly type?: string;
1481
- /**
1482
- * Mark the binding as Lexical rich text. Usually detected automatically;
1483
- * set it where the initial render is empty and there is nothing to detect.
1484
- */
1092
+ /** Mark the binding as Lexical rich text; needed only when the initial render is empty. */
1485
1093
  readonly richtext?: boolean;
1486
1094
  /** Render the value as sanitised HTML rather than text. */
1487
1095
  readonly html?: boolean;
1488
1096
  /** Lock this element to one locale, overriding the message locale. */
1489
1097
  readonly locale?: string;
1098
+ /** Field whose value becomes the image `alt`. */
1099
+ readonly alt?: string;
1100
+ /** Field whose value becomes the link `href`. */
1101
+ readonly href?: string;
1102
+ /** Markup per array item with `{{value}}` placeholders. */
1103
+ readonly arrayTemplate?: string;
1490
1104
  }
1491
- /**
1492
- * String-literal binding helper.
1493
- *
1494
- * ```ts
1495
- * import { bind } from 'payload-live-preview';
1496
- * import type { Homepage } from './payload-types';
1497
- *
1498
- * <h1 {...bind<Homepage>('heroTitle')}>{data.heroTitle}</h1>
1499
- * <img {...bind<Homepage>('heroImage', { attribute: 'src' })} />
1500
- * ```
1501
- *
1502
- * The generic `T` is the schema interface emitted by `pll-codegen`.
1503
- * `field` is constrained to `keyof T` so misspellings fail at compile
1504
- * time. When `T` is omitted, the helper accepts any string — useful
1505
- * for incremental adoption.
1506
- */
1105
+ /** `bind<Homepage>('heroTitle')`; without `T` any string is accepted. */
1507
1106
  declare function bind<T = Record<string, unknown>>(field: FieldName<T>, options?: BindOptions): FieldBindingAttributes;
1508
- /**
1509
- * Proxy-path binding helper.
1510
- *
1511
- * ```ts
1512
- * <h1 {...bindByPath<Homepage>(d => d.heroTitle)}>{data.heroTitle}</h1>
1513
- * ```
1514
- *
1515
- * The picker is invoked once with a recording Proxy; the recorded path
1516
- * is joined with `.` and emitted as the binding name. Property access
1517
- * on nested objects and arrays both work — `d => d.slides[0].title`
1518
- * becomes `slides.title` (array indices are normalised away because the
1519
- * runtime resolves bindings against schema, not against positional
1520
- * indices).
1521
- */
1107
+ /** `bindByPath<Homepage>(d => d.slides[0].title)` → `slides.title`; array indices are dropped because bindings resolve against the schema. */
1522
1108
  declare function bindByPath<T = Record<string, unknown>>(picker: (data: T) => unknown, options?: BindOptions): FieldBindingAttributes;
1523
1109
 
1524
1110
  /**
1525
- * Authorization-gated binding emission.
1526
- *
1527
- * Binding attributes are not neutral markup. `data-payload-field` names a CMS
1528
- * field, and `data-payload-owner` names a global, a collection and often a
1529
- * document id. Emitted unconditionally they publish the shape of the content
1530
- * model — and the identity of documents — to every anonymous visitor and
1531
- * crawler of the page.
1532
- *
1533
- * The gate belongs to the application, not to this package: it is the same
1534
- * decision that already controls draft reads and cache policy. What this
1535
- * module provides is a place to apply that decision **once per request** so no
1536
- * individual call site can forget it, and an emission unit that cannot be
1537
- * partially suppressed.
1538
- *
1539
- * That second property matters more than it looks. A binding is not one
1540
- * attribute: a field travels with its type, its locale, its rich-text marker
1541
- * and its owner. Gating only the field name leaves the companions behind,
1542
- * which discloses the taxonomy anyway and leaves the runtime looking at a
1543
- * binding whose field is gone.
1544
- *
1545
- * @module @dsl/preview-bindings
1111
+ * Binding helpers gated on one authorization verdict per request: the
1112
+ * attributes publish the content model, so an unauthorized response carries
1113
+ * none and a binding is suppressed whole, never in part. See ADR 0006.
1546
1114
  */
1547
1115
 
1548
- /** Owner marker naming the document a subtree belongs to. */
1549
1116
  interface OwnerBindingAttributes {
1550
1117
  readonly 'data-payload-owner': string;
1551
1118
  }
1552
- /**
1553
- * The complete absence of binding attributes.
1554
- *
1555
- * Spreading this into markup contributes nothing, which is the point: an
1556
- * unauthorized response must be byte-identical to one that never knew about
1557
- * live preview.
1558
- */
1119
+ /** No attributes at all: an unauthorized response is byte-identical to one that never knew about live preview. */
1559
1120
  type SuppressedBinding = Readonly<Record<string, never>>;
1560
- interface PreviewBindingsOptions {
1561
- /**
1562
- * The application's own authorization verdict for this request.
1563
- *
1564
- * Never derive this from `?preview=true`, an iframe destination or a
1565
- * referer — those are client-controlled intent signals. Use the same
1566
- * verified result that decides whether drafts may be read.
1567
- */
1568
- readonly authorized: boolean;
1569
- /**
1570
- * Document this subtree belongs to, emitted as `data-payload-owner` by
1571
- * {@link PreviewBindings.owner}. Required only when the page previews more
1572
- * than one document and the runtime runs with `scopeBindingsByOwner`.
1573
- */
1121
+ interface PreviewBindingsCommonOptions {
1122
+ /** Document this subtree belongs to, emitted as `data-payload-owner`; needed with `scopeBindingsByOwner`. */
1574
1123
  readonly owner?: string;
1575
1124
  }
1576
- /** Request-scoped binding helpers that carry one authorization decision. */
1125
+ /** The verdict from `authorizePreviewRequest()`, or `null` for a public response. Only a branded context authorizes emission. */
1126
+ interface PreviewBindingsOptions extends PreviewBindingsCommonOptions {
1127
+ readonly authorization: AuthorizedPreviewContext | null;
1128
+ }
1129
+ /** Request-scoped binding helpers carrying one authorization decision. */
1577
1130
  interface PreviewBindings {
1578
- /** The verdict these helpers were built with. */
1579
1131
  readonly authorized: boolean;
1580
- /** Typed field binding, or nothing at all while unauthorized. */
1132
+ /** Typed field binding, or nothing while unauthorized. */
1581
1133
  bind: <T = Record<string, unknown>>(field: FieldName<T>, options?: BindOptions) => FieldBindingAttributes | SuppressedBinding;
1582
- /** Rename-safe field binding, or nothing at all while unauthorized. */
1134
+ /** Rename-safe field binding, or nothing while unauthorized. */
1583
1135
  bindByPath: <T = Record<string, unknown>>(picker: (data: T) => unknown, options?: BindOptions) => FieldBindingAttributes | SuppressedBinding;
1584
- /**
1585
- * Owner marker for the element that owns this subtree, or nothing at all
1586
- * while unauthorized or when no owner was configured.
1587
- */
1136
+ /** Owner marker for this subtree, or nothing while unauthorized or without an owner. */
1588
1137
  owner: () => OwnerBindingAttributes | SuppressedBinding;
1589
1138
  }
1139
+ /** Request-scoped `bind`, `bindByPath` and `owner`, all suppressed unless `authorization` is a real context. */
1140
+ declare function createPreviewBindings(options: PreviewBindingsOptions): PreviewBindings;
1141
+
1590
1142
  /**
1591
- * Build request-scoped binding helpers around one authorization decision.
1592
- *
1593
- * ```ts
1594
- * const preview = createPreviewBindings({
1595
- * authorized: authorization !== null,
1596
- * owner: `global:${slug}`,
1597
- * });
1598
- * ```
1599
- * ```astro
1600
- * <section {...preview.owner()}>
1601
- * <h1 {...preview.bind<Homepage>('heroTitle')}>{data.heroTitle}</h1>
1602
- * </section>
1603
- * ```
1604
- *
1605
- * While unauthorized every helper returns an empty attribute set, so the
1606
- * public response carries no `data-payload-*` at all.
1143
+ * Hydrated islands own their subtree, so the runtime never patches inside one.
1144
+ * Instead every flush dispatches `payload-live-preview:update` on each island
1145
+ * root with the update in `detail`. `data-payload-island="patch"` opts back in.
1607
1146
  */
1608
- declare function createPreviewBindings(options: PreviewBindingsOptions): PreviewBindings;
1147
+ declare const ISLAND_EVENT = "payload-live-preview:update";
1148
+ interface IslandUpdateDetail {
1149
+ readonly fields: Readonly<Record<string, unknown>>;
1150
+ readonly revision: number;
1151
+ readonly receivedAt: number;
1152
+ readonly locale: string | undefined;
1153
+ }
1154
+ /** Whether `element` (or an ancestor) is an island that did not opt into patching. */
1155
+ declare function isInsideIsland(element: Element): boolean;
1609
1156
 
1610
1157
  /**
1611
1158
  * Narrow public entry — the minimum API for sites that only need to
@@ -1628,4 +1175,4 @@ declare function createPreviewBindings(options: PreviewBindingsOptions): Preview
1628
1175
 
1629
1176
  declare const CORE_ENTRY = true;
1630
1177
 
1631
- export { type BindOptions, CORE_ENTRY, type CachedElement, type ConnectionStatus, DIAGNOSTIC_CODES, type DiagnosticCode, EventEmitter, type EventHandler, type FieldBindingAttributes, type FieldName, type FieldPath, type FieldRenderer, type FieldType, type FrameAncestorsOptions, type InspectionBindings, type InspectionOrigins, type InspectionProtocol, type InspectionRevisions, type InspectionScheduler, LIBRARY_PROTOCOL_VERSION, LivePreviewClient, type LivePreviewClientConfig, type LivePreviewEventMap, type LivePreviewInspection, OriginDetector, type OwnerBindingAttributes, type PayloadFieldSchema, type PayloadFieldType, type PayloadLivePreviewData, type PayloadLivePreviewMessage, type PreviewBindings, type PreviewBindingsOptions, type ProtocolCapability, type ProtocolNegotiation, type RenderContext, type SanitizerDocument, type SuppressedBinding, type Unsubscribe, VERSION, type ValueAt, bind, bindByPath, buildFrameAncestors, buildScriptSrcWithNonce, createPreviewBindings, detectInitialLocale, escapeHtml, escapeHtmlAttribute, generateCspNonce, hasCapability, initLivePreview, isDevMode, isExternalHttpUrl, isInIframe, isInPopup, isInPreviewContext, isSafeUrl, negotiateProtocol, sanitizeHtml, setCspCrypto, setSanitizerDocument };
1178
+ export { AUTHORIZED_PREVIEW_BRAND_KEY, type AuthorizedPreviewContext, type AuthorizedPreviewScope, type BindOptions, CAPABILITY_DECLARATIONS, CAPABILITY_DOCUMENTATION, CORE_ENTRY, type CachedElement, type CapabilityDeclaration, type CapabilityDocumentation, type CapabilitySource, type ConnectionStatus, type CustomRendererKey, DIAGNOSTIC_CODES, type DefaultsProfile, type DiagnosticCode, EventEmitter, type EventHandler, type EventSourcePolicy, type FieldBindingAttributes, type FieldName, type FieldPath, type FieldRenderer, type FieldType, type FragmentContext, type FragmentReport, type FragmentStrategy, type FrameAncestorsOptions, ISLAND_EVENT, type InspectionBindings, type InspectionFragments, type InspectionOrigins, type InspectionProtocol, type InspectionRevisions, type InspectionRoute, type InspectionScheduler, type IslandUpdateDetail, LIBRARY_PROTOCOL_VERSION, LivePreviewClient, type LivePreviewClientConfig, type LivePreviewEventMap, type LivePreviewInspection, OriginDetector, type OwnerBindingAttributes, PROTOCOL_CAPABILITIES, type PayloadDocumentEventDetail, type PayloadFieldCondition, type PayloadFieldSchema, type PayloadFieldType, type PayloadLivePreviewData, type PayloadLivePreviewMessage, type PluginCompatibility, type PluginInspection, type PreviewAuthorizationStrategyName, type PreviewBindings, type PreviewBindingsCommonOptions, type PreviewBindingsOptions, type ProtocolCapability, type ProtocolNegotiation, type ProtocolProfile, type ProtocolProfileName, type RenderContext, type RendererKey, type RichTextRenderer, type RouteContext, type RouteStrategy, type SanitizerDocument, type SanitizerPolicyMode, type StrategyHandlers, type SuppressedBinding, type Unsubscribe, type UpdateSource, VERSION, type ValueAt, bind, bindByPath, buildFrameAncestors, buildScriptSrcWithNonce, createPreviewBindings, detectInitialLocale, detectProtocolProfile, escapeHtml, escapeHtmlAttribute, generateCspNonce, hasCapability, initLivePreview, isAuthorizedPreviewContext, isDevMode, isExternalHttpUrl, isInIframe, isInPopup, isInPreviewContext, isInsideIsland, isSafeUrl, negotiateProtocol, sanitizeHtml, setCspCrypto, setSanitizerDocument };