@elytracms/next 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2616 @@
1
+ import { ComponentType, ReactNode } from 'react';
2
+ import { z } from 'zod';
3
+ export { z } from 'zod';
4
+ import { Metadata, MetadataRoute } from 'next';
5
+ import { ImageLoader } from 'next/image';
6
+
7
+ /**
8
+ * A structured CMS validation issue. Same conventions as
9
+ * `@elytracms/project-graph`'s `ValidationIssue`: a path locating the offending
10
+ * data, optional document/collection identity, and free-form metadata.
11
+ */
12
+ declare const cmsValidationIssueSchema: z.ZodObject<{
13
+ code: z.ZodEnum<{
14
+ "duplicate-collection": "duplicate-collection";
15
+ "duplicate-field": "duplicate-field";
16
+ "unknown-collection": "unknown-collection";
17
+ "invalid-collection-config": "invalid-collection-config";
18
+ "invalid-field-config": "invalid-field-config";
19
+ "missing-required-field": "missing-required-field";
20
+ "invalid-field-value": "invalid-field-value";
21
+ "unknown-relation-target": "unknown-relation-target";
22
+ "unpublished-relation-target": "unpublished-relation-target";
23
+ "cardinality-violation": "cardinality-violation";
24
+ "unknown-asset": "unknown-asset";
25
+ "duplicate-document": "duplicate-document";
26
+ "unknown-locale": "unknown-locale";
27
+ "missing-localized-value": "missing-localized-value";
28
+ "route-conflict": "route-conflict";
29
+ "redirect-loop": "redirect-loop";
30
+ "unknown-route-target": "unknown-route-target";
31
+ "hierarchy-cycle": "hierarchy-cycle";
32
+ "unknown-version": "unknown-version";
33
+ }>;
34
+ severity: z.ZodEnum<{
35
+ error: "error";
36
+ warning: "warning";
37
+ }>;
38
+ message: z.ZodString;
39
+ path: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
40
+ collectionId: z.ZodOptional<z.ZodString>;
41
+ documentId: z.ZodOptional<z.ZodString>;
42
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
43
+ }, z.core.$strip>;
44
+ type CmsValidationIssue = z.infer<typeof cmsValidationIssueSchema>;
45
+ type CmsPath = ReadonlyArray<string | number>;
46
+
47
+ /** How many related documents a relation field points at. */
48
+ declare const cardinalitySchema: z.ZodEnum<{
49
+ one: "one";
50
+ many: "many";
51
+ }>;
52
+ type Cardinality = z.infer<typeof cardinalitySchema>;
53
+ /** A single allowed option for a `select` field. */
54
+ declare const selectOptionSchema: z.ZodObject<{
55
+ value: z.ZodString;
56
+ label: z.ZodOptional<z.ZodString>;
57
+ }, z.core.$strip>;
58
+ type SelectOption = z.infer<typeof selectOptionSchema>;
59
+ /** A schema of just the shared base attributes — the source for {@link BaseFieldDef}. */
60
+ declare const baseFieldObjectSchema: z.ZodObject<{
61
+ name: z.ZodString;
62
+ localized: z.ZodOptional<z.ZodBoolean>;
63
+ filterable: z.ZodOptional<z.ZodBoolean>;
64
+ context: z.ZodOptional<z.ZodEnum<{
65
+ document: "document";
66
+ prop: "prop";
67
+ both: "both";
68
+ }>>;
69
+ default: z.ZodOptional<z.ZodUnknown>;
70
+ form: z.ZodOptional<z.ZodObject<{
71
+ label: z.ZodOptional<z.ZodString>;
72
+ description: z.ZodOptional<z.ZodString>;
73
+ placeholder: z.ZodOptional<z.ZodString>;
74
+ group: z.ZodOptional<z.ZodString>;
75
+ tab: z.ZodOptional<z.ZodString>;
76
+ order: z.ZodOptional<z.ZodNumber>;
77
+ readOnly: z.ZodOptional<z.ZodBoolean>;
78
+ hidden: z.ZodOptional<z.ZodBoolean>;
79
+ control: z.ZodOptional<z.ZodEnum<{
80
+ input: "input";
81
+ textarea: "textarea";
82
+ color: "color";
83
+ json: "json";
84
+ }>>;
85
+ inlineEditable: z.ZodOptional<z.ZodBoolean>;
86
+ }, z.core.$strip>>;
87
+ validation: z.ZodOptional<z.ZodObject<{
88
+ required: z.ZodOptional<z.ZodBoolean>;
89
+ min: z.ZodOptional<z.ZodNumber>;
90
+ max: z.ZodOptional<z.ZodNumber>;
91
+ pattern: z.ZodOptional<z.ZodString>;
92
+ unique: z.ZodOptional<z.ZodBoolean>;
93
+ }, z.core.$strip>>;
94
+ }, z.core.$strip>;
95
+ /** Attributes every field-def carries, derived from {@link baseFieldShape}. */
96
+ type BaseFieldDef = z.infer<typeof baseFieldObjectSchema>;
97
+ /**
98
+ * A field definition (the shape {@link fieldDefSchema} parses to). Written
99
+ * explicitly rather than via `z.infer` because the `object` variant is recursive
100
+ * — its `fields` reference `FieldDef` itself — and TypeScript cannot infer a
101
+ * self-referential `z.infer`. The schema below is annotated with this type and the
102
+ * two are kept in lock-step (`object-field.test.ts` round-trips against drift).
103
+ *
104
+ * A discriminated union on `type` so relation/asset/select/object carry exactly
105
+ * the extra config they need and nothing more.
106
+ */
107
+ type FieldDef = (BaseFieldDef & {
108
+ type: 'text';
109
+ }) | (BaseFieldDef & {
110
+ type: 'number';
111
+ }) | (BaseFieldDef & {
112
+ type: 'boolean';
113
+ }) | (BaseFieldDef & {
114
+ type: 'date';
115
+ }) | (BaseFieldDef & {
116
+ type: 'select';
117
+ options: SelectOption[];
118
+ multiple?: boolean;
119
+ }) | (BaseFieldDef & {
120
+ type: 'richText';
121
+ allow?: string[];
122
+ }) | (BaseFieldDef & {
123
+ type: 'relation';
124
+ target: string;
125
+ cardinality: Cardinality;
126
+ populate?: boolean;
127
+ strict?: boolean;
128
+ }) | (BaseFieldDef & {
129
+ type: 'asset';
130
+ cardinality: Cardinality;
131
+ accept?: string[];
132
+ }) | (BaseFieldDef & {
133
+ type: 'blocks';
134
+ allow?: string[];
135
+ cardinality: Cardinality;
136
+ }) | (BaseFieldDef & {
137
+ type: 'object';
138
+ fields: FieldDef[];
139
+ cardinality: Cardinality;
140
+ });
141
+
142
+ /**
143
+ * A collection definition: an id, a kind, an ordered list of fields, and
144
+ * optional form metadata. The field whose value identifies a document for
145
+ * routing/display defaults to `title` when present (see `titleFieldOf`).
146
+ */
147
+ declare const collectionDefSchema: z.ZodObject<{
148
+ id: z.ZodString;
149
+ kind: z.ZodDefault<z.ZodEnum<{
150
+ asset: "asset";
151
+ document: "document";
152
+ }>>;
153
+ fields: z.ZodDefault<z.ZodArray<z.ZodType<FieldDef, unknown, z.core.$ZodTypeInternals<FieldDef, unknown>>>>;
154
+ form: z.ZodOptional<z.ZodObject<{
155
+ label: z.ZodOptional<z.ZodString>;
156
+ labelSingular: z.ZodOptional<z.ZodString>;
157
+ description: z.ZodOptional<z.ZodString>;
158
+ groups: z.ZodOptional<z.ZodArray<z.ZodString>>;
159
+ tabs: z.ZodOptional<z.ZodArray<z.ZodString>>;
160
+ }, z.core.$strip>>;
161
+ localized: z.ZodOptional<z.ZodBoolean>;
162
+ titleField: z.ZodOptional<z.ZodString>;
163
+ singleton: z.ZodOptional<z.ZodBoolean>;
164
+ }, z.core.$strip>;
165
+ type CollectionDef = z.infer<typeof collectionDefSchema>;
166
+
167
+ /**
168
+ * A locale code (BCP-47-ish, e.g. `en`, `en-US`, `de`). Kept as a plain
169
+ * non-empty string so unusual locales still parse; validity against a project's
170
+ * declared locale set is checked semantically (EC-018).
171
+ */
172
+ declare const localeSchema: z.ZodString;
173
+ type Locale = z.infer<typeof localeSchema>;
174
+ /**
175
+ * A document identity (EC-017): the stable pointer to a piece of content. A
176
+ * document belongs to exactly one collection and has a stable id. For localized
177
+ * content the *identity* is collection+id; a specific localized variant is
178
+ * addressed by additionally carrying a locale (see `LocaleAwareDocumentRef`).
179
+ */
180
+ declare const documentRefSchema: z.ZodObject<{
181
+ collection: z.ZodString;
182
+ id: z.ZodString;
183
+ }, z.core.$strip>;
184
+ type DocumentRef = z.infer<typeof documentRefSchema>;
185
+ /**
186
+ * A stored document (EC-016/017/018). The live row is the continuous DRAFT
187
+ * (the Sanity model, EC-224). Holds:
188
+ * - identity (`collection` + `id`),
189
+ * - non-localized field values in `values`,
190
+ * - per-locale field values in `localized` (only for localized fields).
191
+ *
192
+ * Publish-ness is NOT a field here: a document is published when an append-only
193
+ * version is pinned via the record's `publishedVersion` pointer (EC-224). The
194
+ * delivery `published` perspective serves that pinned snapshot; this live row
195
+ * is always the working draft.
196
+ */
197
+ declare const documentSchema: z.ZodObject<{
198
+ collection: z.ZodString;
199
+ id: z.ZodString;
200
+ values: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
201
+ localized: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
202
+ defaultLocale: z.ZodOptional<z.ZodString>;
203
+ }, z.core.$strip>;
204
+ type CmsDocument = z.infer<typeof documentSchema>;
205
+
206
+ /**
207
+ * Locale configuration for a project (EC-018): the set of supported locales and
208
+ * the default used for fallback. Provider-neutral.
209
+ */
210
+ declare const localeConfigSchema: z.ZodObject<{
211
+ default: z.ZodString;
212
+ locales: z.ZodArray<z.ZodString>;
213
+ }, z.core.$strip>;
214
+ type LocaleConfig = z.infer<typeof localeConfigSchema>;
215
+
216
+ /** A JSON-serializable value. The project graph contains only these — never functions or runtime handles. */
217
+ type JsonValue = string | number | boolean | null | JsonValue[] | {
218
+ [key: string]: JsonValue;
219
+ };
220
+
221
+ /** A pointer from the graph into a data source via a stable path token. */
222
+ declare const bindingReferenceSchema: z.ZodObject<{
223
+ sourceId: z.ZodString;
224
+ token: z.ZodString;
225
+ mode: z.ZodEnum<{
226
+ object: "object";
227
+ value: "value";
228
+ spread: "spread";
229
+ repeaterItem: "repeaterItem";
230
+ condition: "condition";
231
+ }>;
232
+ }, z.core.$strip>;
233
+ type BindingReference = z.infer<typeof bindingReferenceSchema>;
234
+ /** Gates whether a node renders. The left operand is resolved from a binding. */
235
+ declare const conditionSchema: z.ZodObject<{
236
+ source: z.ZodObject<{
237
+ sourceId: z.ZodString;
238
+ token: z.ZodString;
239
+ mode: z.ZodEnum<{
240
+ object: "object";
241
+ value: "value";
242
+ spread: "spread";
243
+ repeaterItem: "repeaterItem";
244
+ condition: "condition";
245
+ }>;
246
+ }, z.core.$strip>;
247
+ operator: z.ZodEnum<{
248
+ truthy: "truthy";
249
+ exists: "exists";
250
+ eq: "eq";
251
+ neq: "neq";
252
+ gt: "gt";
253
+ gte: "gte";
254
+ lt: "lt";
255
+ lte: "lte";
256
+ }>;
257
+ value: z.ZodOptional<z.ZodType<JsonValue, unknown, z.core.$ZodTypeInternals<JsonValue, unknown>>>;
258
+ }, z.core.$strip>;
259
+ type Condition = z.infer<typeof conditionSchema>;
260
+
261
+ /** A prop value is either a static JSON value or a binding into a data source. */
262
+ declare const propValueSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
263
+ kind: z.ZodLiteral<"static">;
264
+ value: z.ZodType<JsonValue, unknown, z.core.$ZodTypeInternals<JsonValue, unknown>>;
265
+ }, z.core.$strip>, z.ZodObject<{
266
+ kind: z.ZodLiteral<"binding">;
267
+ binding: z.ZodObject<{
268
+ sourceId: z.ZodString;
269
+ token: z.ZodString;
270
+ mode: z.ZodEnum<{
271
+ object: "object";
272
+ value: "value";
273
+ spread: "spread";
274
+ repeaterItem: "repeaterItem";
275
+ condition: "condition";
276
+ }>;
277
+ }, z.core.$strip>;
278
+ }, z.core.$strip>], "kind">;
279
+ type PropValue = z.infer<typeof propValueSchema>;
280
+ /**
281
+ * An instance of a registered component. Recursive via `slots`: each named slot
282
+ * holds an ordered list of child nodes. The reserved slot `children` is the default
283
+ * insertion point.
284
+ */
285
+ interface ComponentNode {
286
+ id: string;
287
+ /** Namespaced registered component id (validated semantically, not at parse time). */
288
+ componentId: string;
289
+ props?: Record<string, PropValue>;
290
+ slots?: Record<string, ComponentNode[]>;
291
+ condition?: Condition;
292
+ }
293
+
294
+ /**
295
+ * Schema version 2 (EC-187): the graph is LAYOUTS-ONLY. Version 1 carried a
296
+ * `pages` array (page-as-graph-node); v2 removes it — a page is now a document
297
+ * in the `page` collection whose `body` composition renders into a layout via
298
+ * `renderCompositionInLayout`. A deployment NOT reseeded to v2 serves a null
299
+ * graph, so the host degrades to its visible fallback rather than crashing.
300
+ */
301
+ declare const PROJECT_GRAPH_SCHEMA_VERSION = 2;
302
+ /**
303
+ * The canonical project graph — LAYOUTS-ONLY (EC-187). The single source of
304
+ * truth for the dev-frame layout scaffolding; page content lives in the `page`
305
+ * collection.
306
+ */
307
+ declare const projectGraphSchema: z.ZodObject<{
308
+ id: z.ZodString;
309
+ schemaVersion: z.ZodNumber;
310
+ metadata: z.ZodOptional<z.ZodObject<{
311
+ name: z.ZodOptional<z.ZodString>;
312
+ }, z.core.$strip>>;
313
+ settingsRef: z.ZodOptional<z.ZodString>;
314
+ layouts: z.ZodDefault<z.ZodArray<z.ZodObject<{
315
+ id: z.ZodString;
316
+ name: z.ZodString;
317
+ root: z.ZodType<ComponentNode, unknown, z.core.$ZodTypeInternals<ComponentNode, unknown>>;
318
+ }, z.core.$strip>>>;
319
+ }, z.core.$strip>;
320
+ type ProjectGraph = z.infer<typeof projectGraphSchema>;
321
+
322
+ /** A structured validation issue carrying a JSON path into the graph. */
323
+ declare const validationIssueSchema: z.ZodObject<{
324
+ code: z.ZodEnum<{
325
+ "route-conflict": "route-conflict";
326
+ "invalid-component-name": "invalid-component-name";
327
+ "unknown-component": "unknown-component";
328
+ "duplicate-node-id": "duplicate-node-id";
329
+ "unknown-prop": "unknown-prop";
330
+ "invalid-prop": "invalid-prop";
331
+ "binding-not-allowed": "binding-not-allowed";
332
+ "unresolved-binding": "unresolved-binding";
333
+ "missing-required-slot": "missing-required-slot";
334
+ "unknown-slot": "unknown-slot";
335
+ "forbidden-component": "forbidden-component";
336
+ "server-only-component": "server-only-component";
337
+ "unknown-layout": "unknown-layout";
338
+ "invalid-container-direction": "invalid-container-direction";
339
+ "invalid-container-alignment": "invalid-container-alignment";
340
+ "invalid-container-gap": "invalid-container-gap";
341
+ "invalid-container-columns": "invalid-container-columns";
342
+ "invalid-container-responsive": "invalid-container-responsive";
343
+ }>;
344
+ severity: z.ZodEnum<{
345
+ error: "error";
346
+ warning: "warning";
347
+ }>;
348
+ message: z.ZodString;
349
+ path: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
350
+ nodeId: z.ZodOptional<z.ZodString>;
351
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<JsonValue, unknown, z.core.$ZodTypeInternals<JsonValue, unknown>>>>;
352
+ }, z.core.$strip>;
353
+ type ValidationIssue = z.infer<typeof validationIssueSchema>;
354
+
355
+ /**
356
+ * Structural views of a component manifest, supplied by the registry. The graph
357
+ * package depends only on these shapes (not on `@elytracms/component-registry`) so it
358
+ * stays free of cycles. The registry's Manifest satisfies these structurally.
359
+ */
360
+ interface PropView {
361
+ /** Whether this prop accepts a binding. Defaults to allowed when omitted. */
362
+ bindable?: boolean;
363
+ /** Optional static-value validator (typically backed by the manifest's Zod schema). */
364
+ validate?: (value: unknown) => boolean;
365
+ }
366
+ interface SlotView {
367
+ name: string;
368
+ required?: boolean;
369
+ /**
370
+ * Allowed child component ids (EC-186). Omit to allow any registered
371
+ * component; when present, a child whose `componentId` is not listed is a
372
+ * `forbidden-component` error. Mirrors the registry's `SlotSpec.allow`.
373
+ */
374
+ allow?: readonly string[];
375
+ }
376
+ interface ManifestView {
377
+ id: string;
378
+ props?: Record<string, PropView>;
379
+ slots?: SlotView[];
380
+ /**
381
+ * Canvas-eligibility (EC-191). `false` marks a server-only / RSC component that
382
+ * is NOT client-renderable, so it may not appear in an editor composition (the
383
+ * canvas is client-rendered). Omitted/true = canvas-eligible. Only enforced when
384
+ * `requireCanvasEligible` is set (i.e. validating a composition value, not a
385
+ * dev-authored page/layout frame).
386
+ */
387
+ clientRenderable?: boolean;
388
+ }
389
+ interface ComponentLookup {
390
+ has(componentId: string): boolean;
391
+ get(componentId: string): ManifestView | undefined;
392
+ }
393
+
394
+ type ParseResult = {
395
+ ok: true;
396
+ graph: ProjectGraph;
397
+ } | {
398
+ ok: false;
399
+ error: z.ZodError;
400
+ };
401
+ /** Parse a project graph from a JSON string or already-parsed value. Never throws on invalid input. */
402
+ declare function parseProjectGraph(input: string | unknown): ParseResult;
403
+
404
+ /**
405
+ * A route record (EC-019, reshaped by EC-187). A path pattern may contain
406
+ * dynamic segments written `:name` (e.g. `/blog/:slug`). A route maps a concrete
407
+ * URL to a **render target = a collection + document** via its `document` ref
408
+ * — `/` → `{ collection: 'page', id: 'home' }`, `/blog/:slug` →
409
+ * `{ collection: 'post', id: ':slug' }`. The catch-all dispatches on the
410
+ * resolved document's collection. There is no `templateId`/`pageId` anymore:
411
+ * page content is a `page`-collection document, not a graph page.
412
+ */
413
+ declare const routeRecordSchema: z.ZodObject<{
414
+ id: z.ZodString;
415
+ pattern: z.ZodString;
416
+ locale: z.ZodOptional<z.ZodString>;
417
+ document: z.ZodOptional<z.ZodObject<{
418
+ collection: z.ZodString;
419
+ id: z.ZodString;
420
+ }, z.core.$strip>>;
421
+ }, z.core.$strip>;
422
+ type RouteRecord = z.infer<typeof routeRecordSchema>;
423
+ /** A redirect record (EC-019/020). */
424
+ declare const redirectRecordSchema: z.ZodObject<{
425
+ id: z.ZodString;
426
+ from: z.ZodString;
427
+ to: z.ZodString;
428
+ permanent: z.ZodDefault<z.ZodBoolean>;
429
+ }, z.core.$strip>;
430
+ type RedirectRecord = z.infer<typeof redirectRecordSchema>;
431
+ /** Result of `resolveUrl` (EC-019). */
432
+ type ResolveStatus = 'ok' | 'redirect' | 'notFound';
433
+
434
+ /**
435
+ * A snapshot of a document at a point in time (EC-021 version history). Stores a
436
+ * full copy of the document's stored state plus bookkeeping. Snapshots are
437
+ * immutable records of prior states.
438
+ */
439
+ declare const documentVersionSchema: z.ZodObject<{
440
+ version: z.ZodNumber;
441
+ snapshot: z.ZodObject<{
442
+ collection: z.ZodString;
443
+ id: z.ZodString;
444
+ values: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
445
+ localized: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
446
+ defaultLocale: z.ZodOptional<z.ZodString>;
447
+ }, z.core.$strip>;
448
+ createdAt: z.ZodNumber;
449
+ label: z.ZodOptional<z.ZodString>;
450
+ }, z.core.$strip>;
451
+ type DocumentVersion = z.infer<typeof documentVersionSchema>;
452
+ /**
453
+ * Draft/published primitives + version history for a single document (EC-021).
454
+ *
455
+ * Holds the live `draft` state, the last `published` state (if any), and an
456
+ * ordered history of recorded versions. All mutations return new states or
457
+ * append history; nothing is silently dropped, and retrieving an unknown
458
+ * version yields a structured issue rather than throwing.
459
+ */
460
+ interface DocumentHistory {
461
+ /** The current working draft. */
462
+ readonly draft: CmsDocument;
463
+ /** The last published snapshot, if the document has ever been published. */
464
+ readonly published: CmsDocument | undefined;
465
+ /** Recorded versions, oldest first. */
466
+ readonly versions: readonly DocumentVersion[];
467
+ }
468
+
469
+ /** Distributive `Omit` that preserves a discriminated union (one variant per member). */
470
+ type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
471
+ /**
472
+ * A component prop is a **field-def** (EC-190 "props as fields", AD-12): the SAME
473
+ * vocabulary the CMS uses for document fields (`@elytracms/cms-core` `FieldDef`) —
474
+ * text/number/boolean/select/date/relation/asset/richText/blocks — minus the
475
+ * `name` (the record key on `ComponentManifest.props` IS the prop name). One
476
+ * schema vocabulary, one inspector/form engine, recursively: a prop of type
477
+ * `blocks` carries a nested composition. Authoring metadata (label, default,
478
+ * inline-editing, control hint) lives on the field-def's `form` and `default`.
479
+ * Use `context: 'prop'` (or `'both'`); document-only attributes (`filterable`,
480
+ * `unique`) are flagged by cms-core's `assertPropFieldDef`, never silently honoured.
481
+ */
482
+ type PropField = DistributiveOmit<FieldDef, 'name'>;
483
+ /** A named child insertion point. */
484
+ interface SlotSpec {
485
+ name: string;
486
+ label?: string;
487
+ required?: boolean;
488
+ /** Restrict which component ids may be inserted (omit to allow any). */
489
+ allow?: string[];
490
+ }
491
+ /** A deterministic preview fixture for a component state. */
492
+ interface ComponentFixture {
493
+ name: string;
494
+ props?: Record<string, JsonValue>;
495
+ /** Named slots rendered with literal child component ids for preview. */
496
+ slotChildren?: Record<string, string[]>;
497
+ }
498
+ /** Declares that a component iterates a list prop over an item slot (e.g. Repeater). */
499
+ interface IterateSpec {
500
+ itemsProp: string;
501
+ itemSlot: string;
502
+ }
503
+ /**
504
+ * Declares that a component renders a composition FIELD VALUE (EC-188, AD-12): the
505
+ * `valueProp` carries a stored composition (one `ComponentNode` root or an ordered
506
+ * list — a `blocks` field's value), which the renderer renders as the component's
507
+ * children via `renderComposition`, with the live `RenderContext`. The mirror of
508
+ * `IterateSpec`: a manifest opts a host component into rendering a composition tree
509
+ * wherever it is placed (e.g. a post template's body), so the same renderer/export
510
+ * pipeline that renders pages renders block fields too.
511
+ */
512
+ interface CompositionSpec {
513
+ valueProp: string;
514
+ }
515
+ /**
516
+ * Declares that a component renders a LISTING — a live, query-resolved set of
517
+ * documents (EC-255, un-parks AD-9 "this section repeats over a collection,
518
+ * filtered, sorted"). The `prop` receives the matching documents (delivery-shaped),
519
+ * injected by the host exactly like a composition's children — NOT a static
520
+ * relation pick the editor maintains by hand. The filter binds the listed
521
+ * collection's `field` to the ref a SIBLING authored prop (`fromProp`) carries:
522
+ * a ProductGrid picks one `category`, and the listing lists `product` where its
523
+ * `categories` (a `many` relation) contains that pick. `sort`/`limit` are the
524
+ * closed AD-3 query envelope. The mirror of {@link CompositionSpec} for a
525
+ * query-resolved prop — so the same renderer/export pipeline that renders pages
526
+ * renders dynamic grids, and adding/removing a member re-renders them.
527
+ */
528
+ interface ListingSpec {
529
+ /** Prop the resolved documents are injected as (read by the implementation). */
530
+ prop: string;
531
+ /** Collection to list (e.g. `product`). */
532
+ collection: string;
533
+ /**
534
+ * The membership filter: the listed collection's `field` must contain the ref
535
+ * the sibling authored prop `fromProp` holds (a relation pick, reduced to its
536
+ * target id). Closed to a single equality/contains term — the AD-3 vocabulary.
537
+ */
538
+ filter: {
539
+ field: string;
540
+ fromProp: string;
541
+ };
542
+ /** Optional single-field sort (ties break by id, like every list — EC-143). */
543
+ sort?: {
544
+ field: string;
545
+ direction?: 'asc' | 'desc';
546
+ };
547
+ /** Optional page-size cap. */
548
+ limit?: number;
549
+ }
550
+ /** The typed manifest each editable component declares (brief §7.2). */
551
+ interface ComponentManifest {
552
+ /** Namespaced id, e.g. `base.primitives.Stack` or `project.MarketingHero`. */
553
+ id: string;
554
+ /** Source namespace: `base.primitives` for platform primitives, `project` for project code. */
555
+ namespace: string;
556
+ title?: string;
557
+ description?: string;
558
+ category?: string;
559
+ /** Editable props as field-defs (EC-190), keyed by prop name. */
560
+ props: Record<string, PropField>;
561
+ slots: SlotSpec[];
562
+ /** Design-token usage hints (e.g. `['color.surface', 'space.gap']`). */
563
+ designTokens?: string[];
564
+ fixtures?: ComponentFixture[];
565
+ iterate?: IterateSpec;
566
+ /** Marks the component as a composition host: `valueProp` holds a block-field tree (EC-188). */
567
+ composition?: CompositionSpec;
568
+ /** Marks the component as a listing host: `prop` receives a live query's documents (EC-255). */
569
+ listing?: ListingSpec;
570
+ /**
571
+ * Canvas-eligibility (EC-191). Editors place components on a **client-rendered**
572
+ * canvas, so a placeable component must be client-renderable (isomorphic React).
573
+ * Defaults to eligible; set `clientRenderable: false` for a server-only / RSC
574
+ * component — it stays usable in dev frames (`page.tsx` / `layout.tsx`) but the
575
+ * blocks panel won't offer it and a composition that contains it is invalid.
576
+ */
577
+ clientRenderable?: boolean;
578
+ }
579
+ /** Identity helper that infers nothing but documents intent at call sites. */
580
+ declare function defineComponent(manifest: ComponentManifest): ComponentManifest;
581
+
582
+ /** Structured issues surfaced by the registry itself (distinct from graph issues). */
583
+ interface ComponentIssue {
584
+ code: 'duplicate-id' | 'invalid-namespace' | 'invalid-id';
585
+ componentId: string;
586
+ message: string;
587
+ }
588
+
589
+ interface ListFilter {
590
+ namespace?: string;
591
+ category?: string;
592
+ /** Case-insensitive substring match over id / title / category. */
593
+ query?: string;
594
+ }
595
+ /**
596
+ * The component registry serves the Builder, renderer, AI tools, export pipeline, and
597
+ * CLI. It distinguishes platform primitives from project components, supports lookup,
598
+ * filtering, required lookups, and reports duplicate ids as structured issues.
599
+ */
600
+ declare class ComponentRegistry {
601
+ private readonly byId;
602
+ private readonly viewCache;
603
+ readonly issues: ComponentIssue[];
604
+ constructor(manifests?: Iterable<ComponentManifest>);
605
+ register(manifest: ComponentManifest): void;
606
+ has(id: string): boolean;
607
+ getManifest(id: string): ComponentManifest | undefined;
608
+ /** Required lookup — throws when the component is not registered. */
609
+ require(id: string): ComponentManifest;
610
+ list(filter?: ListFilter): ComponentManifest[];
611
+ /** Platform primitives (`base.primitives.*`). */
612
+ primitives(): ComponentManifest[];
613
+ /** Project-level components (everything not under `base.primitives.*`). */
614
+ projectComponents(): ComponentManifest[];
615
+ get size(): number;
616
+ /** A `ComponentLookup` view for the project-graph validator. */
617
+ get lookup(): ComponentLookup;
618
+ }
619
+
620
+ /**
621
+ * A single repeater item scope. When set, `repeaterItem`-mode bindings resolve against
622
+ * `item` rather than the root data source (brief §4.6 — uniform binding model).
623
+ */
624
+ interface ItemContext {
625
+ item: unknown;
626
+ index: number;
627
+ }
628
+ /**
629
+ * Resolves a binding to a runtime value. INJECTED by the host (builder preview, export
630
+ * runtime, tests) so the renderer never imports `@elytracms/data-binding`. When an
631
+ * `ItemContext` is active, it is passed through so `repeaterItem` bindings resolve
632
+ * relative to the current item.
633
+ */
634
+ type ResolveBinding = (ref: BindingReference, item?: ItemContext) => unknown;
635
+ /**
636
+ * A resolved asset, ready to hand to an image implementation (EC-195). The
637
+ * delivery-shaped subset of `@elytracms/content`'s `ResolvedAsset` the renderer
638
+ * needs — kept minimal so the renderer takes no dependency on the content or
639
+ * persistence packages. `width`/`height` are intrinsic dimensions (so the host
640
+ * can reserve space and `next/image` can size variants); `null` when unknown.
641
+ */
642
+ interface RenderAsset {
643
+ url: string;
644
+ width: number | null;
645
+ height: number | null;
646
+ alt: string | null;
647
+ /**
648
+ * Normalized focal point `{ x, y }` in 0–1 (EC-230) → CSS `object-position` so
649
+ * an art-directed image keeps its subject in frame when cropped; `null` when
650
+ * none. Kept as a plain pair so the renderer stays free of content/persistence.
651
+ */
652
+ focalPoint: {
653
+ x: number;
654
+ y: number;
655
+ } | null;
656
+ }
657
+ /**
658
+ * A relation target populated to delivery shape (EC-254): identity plus the
659
+ * target document's resolved fields (relations inside stay `{ id, collection }`
660
+ * stubs at depth-1; assets inside resolve to objects). Opaque to the renderer —
661
+ * the host's content layer produces it, the component implementation reads its
662
+ * fields. The minimal structural face is `{ id, collection }`.
663
+ */
664
+ type RelationTarget = {
665
+ id: string;
666
+ collection: string;
667
+ } & Record<string, unknown>;
668
+ /**
669
+ * Maps namespaced component ids to their React implementations. Implementations receive
670
+ * resolved props plus rendered slot children: the default slot `children` maps to React
671
+ * `children`, and every other named slot is passed as a prop named after the slot.
672
+ */
673
+ type ComponentImplementations = Record<string, ComponentType<Record<string, unknown>>> | Map<string, ComponentType<Record<string, unknown>>>;
674
+
675
+ /**
676
+ * One host-repo component: the manifest the builder edits against plus the
677
+ * real React implementation that renders it (vision AD-2 — project components
678
+ * are real code in the host repo, never generated).
679
+ */
680
+ interface HostComponent {
681
+ manifest: ComponentManifest;
682
+ implementation: ComponentType<Record<string, unknown>>;
683
+ }
684
+ /**
685
+ * The registered component surface a host app hands to `<CanvasRenderer />`
686
+ * and the catch-all route helper: a manifest registry plus the implementation
687
+ * map, with registration problems surfaced as structured issues (never
688
+ * thrown — an unknown component renders the EC-015 fallback).
689
+ */
690
+ interface HostComponents {
691
+ registry: ComponentRegistry;
692
+ implementations: ComponentImplementations;
693
+ /** Structural registration issues (duplicate ids, invalid namespaces). */
694
+ issues: readonly ComponentIssue[];
695
+ }
696
+ interface DefineHostComponentsOptions {
697
+ /**
698
+ * Include the platform primitives (`base.primitives.*`) that ship with
699
+ * `@elytracms/runtime-renderer`. Defaults to `true` — graphs authored in the
700
+ * builder assume them.
701
+ */
702
+ includeBasePrimitives?: boolean;
703
+ }
704
+ /**
705
+ * Component registration API for the embedded runtime (EC-144). The host repo
706
+ * supplies real implementations + manifests; base primitives are included by
707
+ * default. A host implementation registered under a primitive id replaces the
708
+ * default implementation (the manifest of the first registration wins, which
709
+ * is the primitive's — by design, so prop schemas stay canonical).
710
+ */
711
+ declare function defineHostComponents(components?: readonly HostComponent[], options?: DefineHostComponentsOptions): HostComponents;
712
+
713
+ /**
714
+ * Which side of the draft/published split a delivery request reads from
715
+ * (EC-021, vision AD-4). `published` never exposes draft content; `draft` is
716
+ * the editor/preview view.
717
+ */
718
+ declare const perspectiveSchema: z.ZodEnum<{
719
+ draft: "draft";
720
+ published: "published";
721
+ }>;
722
+ type Perspective = z.infer<typeof perspectiveSchema>;
723
+ /**
724
+ * Ambient request context (vision AD-4): locale and perspective are set once
725
+ * per request and threaded through every resolution function — never per-call
726
+ * ceremony. The locale config travels along so EC-018 fallback needs no extra
727
+ * arguments either.
728
+ */
729
+ interface ContentContext {
730
+ /** The locale requested for this delivery (EC-018 fallback applies). */
731
+ readonly locale: Locale;
732
+ /** Draft or published perspective (EC-021). */
733
+ readonly perspective: Perspective;
734
+ /** The project's locale configuration (default + supported set). */
735
+ readonly locales: LocaleConfig;
736
+ }
737
+
738
+ /**
739
+ * What delivery resolution accepts as a document source: either a bare stored
740
+ * row (`CmsDocument`, always a working draft — EC-224 retired the `state` flag)
741
+ * or the full draft/published history (`DocumentHistory`, EC-021) whose
742
+ * `published` field carries the pinned snapshot. Lookups hand these in;
743
+ * resolution stays pure — no fetching at this layer.
744
+ */
745
+ type ContentDocumentSource = CmsDocument | DocumentHistory;
746
+
747
+ /**
748
+ * A report entry for one field whose value was served from a different locale
749
+ * than the one requested (EC-018 default-locale fallback). Paths are rooted at
750
+ * the resolved document, e.g. `['title']` or `['author', 'bio']` for a field
751
+ * inside a populated relation.
752
+ */
753
+ interface FieldLocaleFallback {
754
+ path: CmsPath;
755
+ /** The field name (last path segment, repeated for convenience). */
756
+ field: string;
757
+ /** The locale the request asked for. */
758
+ requestedLocale: Locale;
759
+ /** The locale the value was actually read from. */
760
+ sourceLocale: Locale;
761
+ }
762
+
763
+ /**
764
+ * Which backend a persistence adapter is talking to, and whether it's reachable.
765
+ * Lets the Studio surface degraded/offline states without leaking provider types.
766
+ */
767
+ declare const backendKindSchema: z.ZodEnum<{
768
+ convex: "convex";
769
+ "in-memory": "in-memory";
770
+ }>;
771
+ type BackendKind = z.infer<typeof backendKindSchema>;
772
+ interface BackendStatus {
773
+ backend: BackendKind;
774
+ available: boolean;
775
+ detail?: string;
776
+ }
777
+
778
+ /**
779
+ * An immutable record of one saved graph state (brief §7.1, §10). Each save
780
+ * appends a revision; the highest revision is the latest. The graph is stored
781
+ * serialized so reload is provably lossless (serialize → parse round-trip).
782
+ */
783
+ declare const graphRevisionRecordSchema: z.ZodObject<{
784
+ id: z.ZodString;
785
+ projectId: z.ZodString;
786
+ revision: z.ZodNumber;
787
+ serializedGraph: z.ZodString;
788
+ schemaVersion: z.ZodNumber;
789
+ createdAt: z.ZodString;
790
+ label: z.ZodOptional<z.ZodString>;
791
+ message: z.ZodOptional<z.ZodString>;
792
+ }, z.core.$strip>;
793
+ type GraphRevisionRecord = z.infer<typeof graphRevisionRecordSchema>;
794
+ /** A revision paired with its parsed graph. */
795
+ interface LoadedGraph {
796
+ graph: ProjectGraph;
797
+ revision: GraphRevisionRecord;
798
+ }
799
+ interface SaveGraphOptions {
800
+ label?: string;
801
+ message?: string;
802
+ }
803
+ /**
804
+ * Persists project graphs and their revision history (brief §7.1, §10).
805
+ * Provider-neutral: an in-memory and a Convex implementation both satisfy it.
806
+ */
807
+ interface GraphRepository {
808
+ /** Append a new revision for the project's graph and return it. */
809
+ saveGraph(projectId: string, graph: ProjectGraph, options?: SaveGraphOptions): Promise<GraphRevisionRecord>;
810
+ /** Load the latest graph + revision. Throws `RecordNotFoundError` if none. */
811
+ getLatest(projectId: string): Promise<LoadedGraph>;
812
+ /** Load a specific revision. Throws `RecordNotFoundError` if absent. */
813
+ getRevision(projectId: string, revision: number): Promise<LoadedGraph>;
814
+ /** All revisions for a project, newest first. */
815
+ listRevisions(projectId: string): Promise<GraphRevisionRecord[]>;
816
+ /** Whether any graph revision exists for the project. */
817
+ hasGraph(projectId: string): Promise<boolean>;
818
+ }
819
+
820
+ /** The persisted CMS schema for a project: its collection definitions (brief §7.4, §10). */
821
+ declare const cmsSchemaRecordSchema: z.ZodObject<{
822
+ createdAt: z.ZodString;
823
+ updatedAt: z.ZodString;
824
+ projectId: z.ZodString;
825
+ collections: z.ZodArray<z.ZodObject<{
826
+ id: z.ZodString;
827
+ kind: z.ZodDefault<z.ZodEnum<{
828
+ asset: "asset";
829
+ document: "document";
830
+ }>>;
831
+ fields: z.ZodDefault<z.ZodArray<z.ZodType<FieldDef, unknown, z.core.$ZodTypeInternals<FieldDef, unknown>>>>;
832
+ form: z.ZodOptional<z.ZodObject<{
833
+ label: z.ZodOptional<z.ZodString>;
834
+ labelSingular: z.ZodOptional<z.ZodString>;
835
+ description: z.ZodOptional<z.ZodString>;
836
+ groups: z.ZodOptional<z.ZodArray<z.ZodString>>;
837
+ tabs: z.ZodOptional<z.ZodArray<z.ZodString>>;
838
+ }, z.core.$strip>>;
839
+ localized: z.ZodOptional<z.ZodBoolean>;
840
+ titleField: z.ZodOptional<z.ZodString>;
841
+ singleton: z.ZodOptional<z.ZodBoolean>;
842
+ }, z.core.$strip>>;
843
+ }, z.core.$strip>;
844
+ type CmsSchemaRecord = z.infer<typeof cmsSchemaRecordSchema>;
845
+ /**
846
+ * A persisted CMS document. The stored `document` is the continuous working
847
+ * **draft** (every edit updates it); `publishedVersion` pins which recorded
848
+ * version is currently live (the Sanity draft/published model, EC-224). Absent
849
+ * `publishedVersion` means the document has never been published / is unpublished.
850
+ * Localized variants ride along on the `CmsDocument` itself.
851
+ */
852
+ declare const cmsDocumentRecordSchema: z.ZodObject<{
853
+ createdAt: z.ZodString;
854
+ updatedAt: z.ZodString;
855
+ projectId: z.ZodString;
856
+ document: z.ZodObject<{
857
+ collection: z.ZodString;
858
+ id: z.ZodString;
859
+ values: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
860
+ localized: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
861
+ defaultLocale: z.ZodOptional<z.ZodString>;
862
+ }, z.core.$strip>;
863
+ publishedVersion: z.ZodOptional<z.ZodNumber>;
864
+ }, z.core.$strip>;
865
+ type CmsDocumentRecord = z.infer<typeof cmsDocumentRecordSchema>;
866
+ /**
867
+ * An immutable snapshot of a document at one recorded version (EC-224). Append-only,
868
+ * mirroring {@link GraphRevisionRecord} but document-scoped: one row per version,
869
+ * keyed by (projectId, collection, docId, version). Created on publish (the promoted
870
+ * draft) and by an explicit pre-restore snapshot, never on a plain draft edit.
871
+ */
872
+ declare const documentVersionRecordSchema: z.ZodObject<{
873
+ projectId: z.ZodString;
874
+ collection: z.ZodString;
875
+ docId: z.ZodString;
876
+ version: z.ZodNumber;
877
+ snapshot: z.ZodObject<{
878
+ collection: z.ZodString;
879
+ id: z.ZodString;
880
+ values: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
881
+ localized: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
882
+ defaultLocale: z.ZodOptional<z.ZodString>;
883
+ }, z.core.$strip>;
884
+ createdAt: z.ZodString;
885
+ label: z.ZodOptional<z.ZodString>;
886
+ }, z.core.$strip>;
887
+ type DocumentVersionRecord = z.infer<typeof documentVersionRecordSchema>;
888
+ interface SaveDocumentVersionOptions {
889
+ label?: string;
890
+ }
891
+ /**
892
+ * Persists CMS collection schemas and documents (brief §7.4, §10). Localized
893
+ * variants and draft/published state ride along on the `CmsDocument` itself.
894
+ */
895
+ interface CmsRepository {
896
+ /** Persist (replace) the project's collection schema; validates structural shape. */
897
+ saveSchema(projectId: string, collections: readonly CollectionDef[]): Promise<CmsSchemaRecord>;
898
+ getSchema(projectId: string): Promise<CmsSchemaRecord>;
899
+ findSchema(projectId: string): Promise<CmsSchemaRecord | undefined>;
900
+ /** Insert or update a document (keyed by collection + id). */
901
+ upsertDocument(projectId: string, document: CmsDocument): Promise<CmsDocumentRecord>;
902
+ getDocument(projectId: string, ref: DocumentRef): Promise<CmsDocumentRecord>;
903
+ findDocument(projectId: string, ref: DocumentRef): Promise<CmsDocumentRecord | undefined>;
904
+ /** All documents for a project, optionally filtered to one collection. */
905
+ listDocuments(projectId: string, collection?: string): Promise<CmsDocumentRecord[]>;
906
+ deleteDocument(projectId: string, ref: DocumentRef): Promise<void>;
907
+ /**
908
+ * Append a snapshot of a document as a new monotonic version (EC-224). Used by
909
+ * publish (the promoted draft) and the pre-restore snapshot so a rollback is
910
+ * itself undoable. The (collection, docId) is derived from the snapshot.
911
+ */
912
+ saveDocumentVersion(projectId: string, snapshot: CmsDocument, options?: SaveDocumentVersionOptions): Promise<DocumentVersionRecord>;
913
+ /** All recorded versions for a document, newest first. Empty when none. */
914
+ listDocumentVersions(projectId: string, ref: DocumentRef): Promise<DocumentVersionRecord[]>;
915
+ /**
916
+ * Every recorded version across all documents in a project (EC-224). Backs the
917
+ * delivery feeders, which reconstruct each document's `DocumentHistory` from
918
+ * one bulk fetch rather than a per-document query. Order is deterministic
919
+ * (by document key, then version ascending).
920
+ */
921
+ listAllDocumentVersions(projectId: string): Promise<DocumentVersionRecord[]>;
922
+ /** A specific recorded version. Throws `RecordNotFoundError` if absent. */
923
+ getDocumentVersion(projectId: string, ref: DocumentRef, version: number): Promise<DocumentVersionRecord>;
924
+ /**
925
+ * Pin an existing recorded version live (EC-224 publish). Throws
926
+ * `RecordNotFoundError` if the document or the version is unknown.
927
+ */
928
+ publishDocumentVersion(projectId: string, ref: DocumentRef, version: number): Promise<CmsDocumentRecord>;
929
+ /**
930
+ * Clear the live pin (EC-224 unpublish). Throws `PersistenceValidationError`
931
+ * if the document is not currently published.
932
+ */
933
+ unpublishDocument(projectId: string, ref: DocumentRef): Promise<CmsDocumentRecord>;
934
+ }
935
+
936
+ /**
937
+ * Persisted asset metadata (brief §7.4, §8.6, §10). The bytes themselves live in
938
+ * a {@link BlobStorageAdapter}; this record is the queryable metadata that CMS
939
+ * `asset` fields point at by id.
940
+ */
941
+ declare const assetRecordSchema: z.ZodObject<{
942
+ createdAt: z.ZodString;
943
+ updatedAt: z.ZodString;
944
+ id: z.ZodString;
945
+ projectId: z.ZodString;
946
+ filename: z.ZodString;
947
+ contentType: z.ZodString;
948
+ byteSize: z.ZodNumber;
949
+ width: z.ZodOptional<z.ZodNumber>;
950
+ height: z.ZodOptional<z.ZodNumber>;
951
+ alt: z.ZodOptional<z.ZodString>;
952
+ title: z.ZodOptional<z.ZodString>;
953
+ storageKey: z.ZodString;
954
+ url: z.ZodOptional<z.ZodString>;
955
+ focalPoint: z.ZodOptional<z.ZodObject<{
956
+ x: z.ZodNumber;
957
+ y: z.ZodNumber;
958
+ }, z.core.$strip>>;
959
+ }, z.core.$strip>;
960
+ type AssetRecord = z.infer<typeof assetRecordSchema>;
961
+ interface NewAssetInput {
962
+ id?: string;
963
+ filename: string;
964
+ contentType: string;
965
+ byteSize: number;
966
+ width?: number;
967
+ height?: number;
968
+ alt?: string;
969
+ /** Editable display title (EC-152 asset library metadata). */
970
+ title?: string;
971
+ /** Defaults to a key derived from the asset id. */
972
+ storageKey?: string;
973
+ /** Resolvable serve URL for the stored bytes (EC-151). */
974
+ url?: string;
975
+ /** Normalized image focal point `{ x, y }` in 0–1 (EC-230). */
976
+ focalPoint?: {
977
+ x: number;
978
+ y: number;
979
+ };
980
+ }
981
+ /**
982
+ * Storage boundary for asset *bytes* (brief §8.6). Project-owned and swappable
983
+ * (Convex file storage, S3, etc.); the in-memory implementation backs tests and
984
+ * local inspection. Metadata stays in the {@link AssetRepository}.
985
+ */
986
+ interface BlobStorageAdapter {
987
+ put(key: string, data: Uint8Array, contentType?: string): Promise<void>;
988
+ get(key: string): Promise<Uint8Array | undefined>;
989
+ /** A resolvable locator for the blob (e.g. a CDN/file URL). */
990
+ url(key: string): string;
991
+ has(key: string): Promise<boolean>;
992
+ delete(key: string): Promise<void>;
993
+ /**
994
+ * Store bytes under a backend-assigned key and return it (EC-151). Backends
995
+ * like Convex file storage mint their own ids, so callers cannot choose the
996
+ * key up front the way {@link put} assumes. Optional; in-memory implements it
997
+ * deterministically.
998
+ */
999
+ store?(data: Uint8Array, contentType?: string): Promise<string>;
1000
+ /**
1001
+ * Mint a short-lived URL the client can POST bytes to directly (EC-151,
1002
+ * Convex `storage.generateUploadUrl()`). Optional — backends without
1003
+ * client-direct uploads omit it and callers fall back to {@link store}/{@link put}.
1004
+ */
1005
+ createUploadUrl?(): Promise<string>;
1006
+ /**
1007
+ * Resolve the serve URL for a stored blob asynchronously (EC-151, Convex
1008
+ * `storage.getUrl()`). Returns `undefined` when the blob does not exist.
1009
+ * Optional; backends with synchronous stable URLs can rely on {@link url}.
1010
+ */
1011
+ resolveUrl?(key: string): Promise<string | undefined>;
1012
+ }
1013
+ /** Persists asset metadata records (brief §7.4, §8.6, §10). */
1014
+ interface AssetRepository {
1015
+ upsertAsset(projectId: string, input: NewAssetInput): Promise<AssetRecord>;
1016
+ getAsset(projectId: string, id: string): Promise<AssetRecord>;
1017
+ findAsset(projectId: string, id: string): Promise<AssetRecord | undefined>;
1018
+ listAssets(projectId: string): Promise<AssetRecord[]>;
1019
+ removeAsset(projectId: string, id: string): Promise<void>;
1020
+ /** Resolve persisted metadata for the asset ids referenced by CMS fields. */
1021
+ resolveMany(projectId: string, ids: readonly string[]): Promise<Map<string, AssetRecord>>;
1022
+ }
1023
+
1024
+ /**
1025
+ * Publishing state (brief §10). Single-environment (EC-179, vision AD-8): a
1026
+ * deployment IS one environment, so there is exactly ONE publishing-state
1027
+ * record per project. A `published` record pins the graph revision that is
1028
+ * live. Moving a release between environments is a deployment/code-pipeline
1029
+ * concern, not an in-deployment dimension — see `env-is-deployment-boundary`.
1030
+ */
1031
+ declare const publishingStateRecordSchema: z.ZodObject<{
1032
+ createdAt: z.ZodString;
1033
+ updatedAt: z.ZodString;
1034
+ projectId: z.ZodString;
1035
+ status: z.ZodEnum<{
1036
+ draft: "draft";
1037
+ published: "published";
1038
+ }>;
1039
+ publishedRevisionId: z.ZodOptional<z.ZodString>;
1040
+ publishedRevision: z.ZodOptional<z.ZodNumber>;
1041
+ publishedAt: z.ZodOptional<z.ZodString>;
1042
+ }, z.core.$strip>;
1043
+ type PublishingStateRecord = z.infer<typeof publishingStateRecordSchema>;
1044
+ interface PublishInput {
1045
+ revisionId: string;
1046
+ revision: number;
1047
+ }
1048
+ /**
1049
+ * Persists and transitions publishing state (brief §10). Transitions are
1050
+ * validated: publishing requires a revision; unpublishing requires a
1051
+ * currently-published deployment.
1052
+ */
1053
+ interface PublishingRepository {
1054
+ getState(projectId: string): Promise<PublishingStateRecord>;
1055
+ findState(projectId: string): Promise<PublishingStateRecord | undefined>;
1056
+ /** Publish a graph revision. */
1057
+ publish(projectId: string, input: PublishInput): Promise<PublishingStateRecord>;
1058
+ /** Revert to draft (must currently be published). */
1059
+ unpublish(projectId: string): Promise<PublishingStateRecord>;
1060
+ }
1061
+
1062
+ /**
1063
+ * Reference-index substrate (EC-155, vision AD-6 / pillar 3.6).
1064
+ *
1065
+ * On every write the operations layer extracts *outbound* references — relation
1066
+ * fields, rich-text link marks and inline refs, page-graph bindings and
1067
+ * component usage, asset usages — into this index, for draft and published
1068
+ * variants separately. One index powers many features: "used by" panels, safe
1069
+ * unpublish (the queryable answer to Sanity's hidden-draft-reference failure),
1070
+ * component/asset where-used, and broken-link audits (UX lands in Milestone B).
1071
+ *
1072
+ * Records are **derived data**: they carry no timestamps and are deterministic
1073
+ * functions of the indexed content, so backfill is idempotent and two runs over
1074
+ * the same data are byte-for-byte identical.
1075
+ */
1076
+ /** Which content variant an entry was extracted from (the Sanity failure-case axis). */
1077
+ declare const referenceVariantSchema: z.ZodEnum<{
1078
+ draft: "draft";
1079
+ published: "published";
1080
+ }>;
1081
+ type ReferenceVariant = z.infer<typeof referenceVariantSchema>;
1082
+ /**
1083
+ * What kind of entity *holds* the reference. Documents are keyed by their
1084
+ * `documentKey` (`collection:id`); pages/layouts by their graph ids.
1085
+ */
1086
+ declare const referenceSourceTypeSchema: z.ZodEnum<{
1087
+ document: "document";
1088
+ page: "page";
1089
+ layout: "layout";
1090
+ }>;
1091
+ type ReferenceSourceType = z.infer<typeof referenceSourceTypeSchema>;
1092
+ declare const referenceSourceSchema: z.ZodObject<{
1093
+ type: z.ZodEnum<{
1094
+ document: "document";
1095
+ page: "page";
1096
+ layout: "layout";
1097
+ }>;
1098
+ id: z.ZodString;
1099
+ }, z.core.$strip>;
1100
+ type ReferenceSource = z.infer<typeof referenceSourceSchema>;
1101
+ declare const referenceTargetSchema: z.ZodObject<{
1102
+ type: z.ZodEnum<{
1103
+ asset: "asset";
1104
+ document: "document";
1105
+ component: "component";
1106
+ }>;
1107
+ id: z.ZodString;
1108
+ }, z.core.$strip>;
1109
+ type ReferenceTarget = z.infer<typeof referenceTargetSchema>;
1110
+ /**
1111
+ * One outbound reference found *inside* a source, before it is attributed to
1112
+ * that source: the exact field/node path, the target, and which structure
1113
+ * produced it. `path` is a `/`-joined token path into the stored value, e.g.
1114
+ * `values/author`, `localized/en/body/doc/content/0/marks/0`,
1115
+ * `root/slots/children/2/props/items/binding`.
1116
+ */
1117
+ declare const outboundReferenceSchema: z.ZodObject<{
1118
+ path: z.ZodString;
1119
+ target: z.ZodObject<{
1120
+ type: z.ZodEnum<{
1121
+ asset: "asset";
1122
+ document: "document";
1123
+ component: "component";
1124
+ }>;
1125
+ id: z.ZodString;
1126
+ }, z.core.$strip>;
1127
+ kind: z.ZodEnum<{
1128
+ "relation-field": "relation-field";
1129
+ "asset-field": "asset-field";
1130
+ "rich-text-link": "rich-text-link";
1131
+ "rich-text-node": "rich-text-node";
1132
+ "component-usage": "component-usage";
1133
+ "prop-binding": "prop-binding";
1134
+ "condition-binding": "condition-binding";
1135
+ }>;
1136
+ }, z.core.$strip>;
1137
+ type OutboundReference = z.infer<typeof outboundReferenceSchema>;
1138
+ /** An outbound reference attributed to its holding source entity. */
1139
+ declare const referenceEntrySchema: z.ZodObject<{
1140
+ path: z.ZodString;
1141
+ target: z.ZodObject<{
1142
+ type: z.ZodEnum<{
1143
+ asset: "asset";
1144
+ document: "document";
1145
+ component: "component";
1146
+ }>;
1147
+ id: z.ZodString;
1148
+ }, z.core.$strip>;
1149
+ kind: z.ZodEnum<{
1150
+ "relation-field": "relation-field";
1151
+ "asset-field": "asset-field";
1152
+ "rich-text-link": "rich-text-link";
1153
+ "rich-text-node": "rich-text-node";
1154
+ "component-usage": "component-usage";
1155
+ "prop-binding": "prop-binding";
1156
+ "condition-binding": "condition-binding";
1157
+ }>;
1158
+ source: z.ZodObject<{
1159
+ type: z.ZodEnum<{
1160
+ document: "document";
1161
+ page: "page";
1162
+ layout: "layout";
1163
+ }>;
1164
+ id: z.ZodString;
1165
+ }, z.core.$strip>;
1166
+ }, z.core.$strip>;
1167
+ type ReferenceEntry = z.infer<typeof referenceEntrySchema>;
1168
+ /** The persisted index record: entry + project + variant. Derived, timestamp-free. */
1169
+ declare const referenceEntryRecordSchema: z.ZodObject<{
1170
+ path: z.ZodString;
1171
+ target: z.ZodObject<{
1172
+ type: z.ZodEnum<{
1173
+ asset: "asset";
1174
+ document: "document";
1175
+ component: "component";
1176
+ }>;
1177
+ id: z.ZodString;
1178
+ }, z.core.$strip>;
1179
+ kind: z.ZodEnum<{
1180
+ "relation-field": "relation-field";
1181
+ "asset-field": "asset-field";
1182
+ "rich-text-link": "rich-text-link";
1183
+ "rich-text-node": "rich-text-node";
1184
+ "component-usage": "component-usage";
1185
+ "prop-binding": "prop-binding";
1186
+ "condition-binding": "condition-binding";
1187
+ }>;
1188
+ source: z.ZodObject<{
1189
+ type: z.ZodEnum<{
1190
+ document: "document";
1191
+ page: "page";
1192
+ layout: "layout";
1193
+ }>;
1194
+ id: z.ZodString;
1195
+ }, z.core.$strip>;
1196
+ projectId: z.ZodString;
1197
+ variant: z.ZodEnum<{
1198
+ draft: "draft";
1199
+ published: "published";
1200
+ }>;
1201
+ }, z.core.$strip>;
1202
+ type ReferenceEntryRecord = z.infer<typeof referenceEntryRecordSchema>;
1203
+ interface ReferenceLookupOptions {
1204
+ /** Restrict the lookup to one variant; omit for both (draft + published). */
1205
+ variant?: ReferenceVariant;
1206
+ }
1207
+ /**
1208
+ * Persists the reference index (EC-155). Writes are **replace-all-for-source**:
1209
+ * saving an entity recomputes every outbound reference it holds and replaces the
1210
+ * previous set atomically, so edits/deletes/unpublishes can never leave stale
1211
+ * entries behind. Lookups work in both directions with field/node-path
1212
+ * granularity and are variant-aware.
1213
+ */
1214
+ interface ReferenceIndexRepository {
1215
+ /**
1216
+ * Replace all entries held by `source` in `variant` with the given outbound
1217
+ * references. An empty list removes the source's entries entirely.
1218
+ */
1219
+ replaceForSource(projectId: string, variant: ReferenceVariant, source: ReferenceSource, references: readonly OutboundReference[]): Promise<ReferenceEntryRecord[]>;
1220
+ /**
1221
+ * Replace all entries held by sources of the given types in `variant` (the
1222
+ * graph recompute path: one revision yields entries across many pages/layouts,
1223
+ * and pages removed from the graph must drop their entries too).
1224
+ */
1225
+ replaceForSourceTypes(projectId: string, variant: ReferenceVariant, sourceTypes: readonly ReferenceSourceType[], entries: readonly ReferenceEntry[]): Promise<ReferenceEntryRecord[]>;
1226
+ /** Remove all entries held by `source` (in one variant, or both when omitted). */
1227
+ removeForSource(projectId: string, source: ReferenceSource, variant?: ReferenceVariant): Promise<void>;
1228
+ /** Outbound: what does `source` reference? */
1229
+ outbound(projectId: string, source: ReferenceSource, options?: ReferenceLookupOptions): Promise<ReferenceEntryRecord[]>;
1230
+ /** Inbound: what references `target`? (The "used by" / safe-unpublish direction.) */
1231
+ inbound(projectId: string, target: ReferenceTarget, options?: ReferenceLookupOptions): Promise<ReferenceEntryRecord[]>;
1232
+ /** Every entry of a project (optionally one variant), deterministically ordered. */
1233
+ listEntries(projectId: string, variant?: ReferenceVariant): Promise<ReferenceEntryRecord[]>;
1234
+ /** Drop the whole index of a project (backfill starts from a clean slate). */
1235
+ clearProject(projectId: string): Promise<void>;
1236
+ }
1237
+
1238
+ /**
1239
+ * Per-project membership records (EC-150, vision AD-7). The role model is
1240
+ * deliberately minimal for v1 — three roles, project-scoped, no field-level
1241
+ * permissions. Workspace-level grants (who may create projects, default
1242
+ * memberships) land with EC-154; everything here stays project-scoped.
1243
+ *
1244
+ * Roles:
1245
+ * - `viewer` — read-only: queries succeed, every command is denied.
1246
+ * - `editor` — edits and publishes content, but cannot administer the project
1247
+ * (lifecycle, settings, members, CLI tokens).
1248
+ * - `admin` — everything within the project.
1249
+ *
1250
+ * Enforcement lives at the operations chokepoint
1251
+ * (`@elytracms/operations` `createRoleAuthorization`) and server-side in the
1252
+ * Convex functions (`apps/builder/convex/guard.ts`); this module only defines
1253
+ * the storage contract.
1254
+ */
1255
+ declare const projectRoleSchema: z.ZodEnum<{
1256
+ admin: "admin";
1257
+ editor: "editor";
1258
+ viewer: "viewer";
1259
+ }>;
1260
+ type ProjectRole = z.infer<typeof projectRoleSchema>;
1261
+ declare const projectMemberRecordSchema: z.ZodObject<{
1262
+ projectId: z.ZodString;
1263
+ userId: z.ZodString;
1264
+ role: z.ZodEnum<{
1265
+ admin: "admin";
1266
+ editor: "editor";
1267
+ viewer: "viewer";
1268
+ }>;
1269
+ createdAt: z.ZodString;
1270
+ updatedAt: z.ZodString;
1271
+ }, z.core.$strip>;
1272
+ type ProjectMemberRecord = z.infer<typeof projectMemberRecordSchema>;
1273
+ /**
1274
+ * Membership storage boundary. Mirrors the other repository contracts: the
1275
+ * in-memory implementation below and the Convex-backed implementation in
1276
+ * `apps/builder` both satisfy it.
1277
+ */
1278
+ interface MembersRepository {
1279
+ /** Upsert a membership (add a member or change their role). */
1280
+ setMember(projectId: string, userId: string, role: ProjectRole): Promise<ProjectMemberRecord>;
1281
+ /** Look up one membership; `undefined` when the user is not a member. */
1282
+ findMember(projectId: string, userId: string): Promise<ProjectMemberRecord | undefined>;
1283
+ /** All members of a project, ordered by userId for determinism. */
1284
+ listMembers(projectId: string): Promise<ProjectMemberRecord[]>;
1285
+ /** All memberships of a user across projects, ordered by projectId. */
1286
+ listMembershipsForUser(userId: string): Promise<ProjectMemberRecord[]>;
1287
+ /** Remove a membership. Throws `RecordNotFoundError` when absent. */
1288
+ removeMember(projectId: string, userId: string): Promise<void>;
1289
+ }
1290
+
1291
+ /**
1292
+ * Revocable per-project CLI tokens (EC-150). The EC-147 push CLI authenticates
1293
+ * with `Authorization: Bearer <token>`; the builder stores only a SHA-256 hash
1294
+ * of the token (`tokenHash`) — the plaintext is shown exactly once at issuance
1295
+ * and never persisted. Verification (`verifyCliToken` in `@elytracms/operations`)
1296
+ * hashes the presented token and looks it up here; revoked tokens stay stored
1297
+ * (with `revokedAt`) so revocation is auditable and ids stay stable.
1298
+ */
1299
+ declare const cliTokenRecordSchema: z.ZodObject<{
1300
+ id: z.ZodString;
1301
+ label: z.ZodString;
1302
+ tokenHash: z.ZodString;
1303
+ createdAt: z.ZodString;
1304
+ revokedAt: z.ZodOptional<z.ZodString>;
1305
+ }, z.core.$strip>;
1306
+ type CliTokenRecord = z.infer<typeof cliTokenRecordSchema>;
1307
+ interface NewCliTokenInput {
1308
+ label: string;
1309
+ tokenHash: string;
1310
+ }
1311
+ /**
1312
+ * Token storage boundary. The in-memory implementation below and the
1313
+ * Convex-backed implementation in `apps/builder` both satisfy it.
1314
+ */
1315
+ interface CliTokenRepository {
1316
+ /** Persist a freshly issued token (hash only). */
1317
+ createToken(input: NewCliTokenInput): Promise<CliTokenRecord>;
1318
+ /** All tokens of this deployment (revoked included), ordered by id. */
1319
+ listTokens(): Promise<CliTokenRecord[]>;
1320
+ /** Mark a token revoked (idempotent). Throws `RecordNotFoundError` when absent. */
1321
+ revokeToken(tokenId: string): Promise<CliTokenRecord>;
1322
+ /** Look a token up by its hash — the verification path. */
1323
+ findByHash(tokenHash: string): Promise<CliTokenRecord | undefined>;
1324
+ }
1325
+
1326
+ /**
1327
+ * Inputs to the backend validation service (brief §4.11, §10). Any subset may be
1328
+ * provided; only supplied domains are validated.
1329
+ */
1330
+ interface BackendValidationInput {
1331
+ graph?: ProjectGraph;
1332
+ /** Registry lookup for component/prop/slot checks against the graph. */
1333
+ components?: ComponentLookup;
1334
+ /** Known data-source ids for binding resolution. */
1335
+ knownSourceIds?: ReadonlySet<string>;
1336
+ collections?: readonly CollectionDef[];
1337
+ routes?: readonly RouteRecord[];
1338
+ redirects?: readonly RedirectRecord[];
1339
+ defaultLocale?: string;
1340
+ }
1341
+ /** Structured, typed validation results returned to clients (brief §4.11). */
1342
+ interface BackendValidationResult {
1343
+ ok: boolean;
1344
+ /** Graph + binding + component issues (from `@elytracms/project-graph`). */
1345
+ graphIssues: ValidationIssue[];
1346
+ /** Collection/field schema issues (from `@elytracms/cms-core`). */
1347
+ schemaIssues: CmsValidationIssue[];
1348
+ /** Route conflict + redirect loop issues (from `@elytracms/cms-core`). */
1349
+ routeIssues: CmsValidationIssue[];
1350
+ /** Count of error-severity issues across all domains. */
1351
+ errorCount: number;
1352
+ }
1353
+
1354
+ /**
1355
+ * The aggregate persistence boundary (brief §4.10, §10). Core packages depend on
1356
+ * this interface — never on Convex directly. A Convex-backed implementation and
1357
+ * the in-memory implementation below both satisfy it, so the Studio, seeds, and
1358
+ * tests are provider-agnostic.
1359
+ */
1360
+ interface PersistenceAdapter {
1361
+ readonly graphs: GraphRepository;
1362
+ readonly cms: CmsRepository;
1363
+ readonly assets: AssetRepository;
1364
+ readonly publishing: PublishingRepository;
1365
+ /** Reference index — outbound/inbound reference entries per variant (EC-155, AD-6). */
1366
+ readonly references: ReferenceIndexRepository;
1367
+ /** Per-project membership roles — admin/editor/viewer (EC-150, AD-7). */
1368
+ readonly members: MembersRepository;
1369
+ /** Revocable per-project CLI sync tokens, stored hashed (EC-150). */
1370
+ readonly cliTokens: CliTokenRepository;
1371
+ /** Blob storage boundary for asset bytes. */
1372
+ readonly blobs: BlobStorageAdapter;
1373
+ /** Backend identity + reachability, for degraded-state UI. */
1374
+ status(): Promise<BackendStatus>;
1375
+ /** Server-side validation service (brief §4.11, EC-047). */
1376
+ validate(input: BackendValidationInput): BackendValidationResult;
1377
+ }
1378
+
1379
+ /**
1380
+ * The delivery shape of an asset (vision AD-4): a directly usable object, not
1381
+ * an id. Optional metadata that the stored record lacks is `null` (never
1382
+ * `undefined`) so resolved documents stay JSON-stable.
1383
+ */
1384
+ interface ResolvedAsset {
1385
+ /** The asset id the field stored (kept for cache keys and re-lookup). */
1386
+ id: string;
1387
+ /** Resolvable URL for the asset bytes (EC-044 blob storage locator). */
1388
+ url: string;
1389
+ width: number | null;
1390
+ height: number | null;
1391
+ alt: string | null;
1392
+ mimeType: string;
1393
+ /**
1394
+ * Normalized image focal point `{ x, y }` in 0–1 (EC-230), or `null`. Threaded
1395
+ * to the image primitive's CSS `object-position` so a migrated hotspot keeps
1396
+ * the subject in frame when a layout crops the image (`object-fit: cover`).
1397
+ */
1398
+ focalPoint: {
1399
+ x: number;
1400
+ y: number;
1401
+ } | null;
1402
+ }
1403
+
1404
+ /**
1405
+ * A document in delivery shape: identity plus resolved field values, flat —
1406
+ * `post.title`, `post.author.name`, `post.cover.url`. The precise per-collection
1407
+ * shape is what `generateDeliveryTypes` emits (EC-142 codegen).
1408
+ */
1409
+ type ResolvedDocument = {
1410
+ id: string;
1411
+ collection: string;
1412
+ } & Record<string, unknown>;
1413
+ /**
1414
+ * The minimal structural face of any delivery-shaped document — what the typed
1415
+ * accessors (EC-143) constrain their per-collection generics against. The
1416
+ * generated interfaces from `generateDeliveryTypes` (e.g. `Post`) satisfy this
1417
+ * where they cannot satisfy `ResolvedDocument`'s index signature.
1418
+ */
1419
+ interface DeliveryDocument {
1420
+ id: string;
1421
+ collection: string;
1422
+ }
1423
+ /**
1424
+ * Structured information about *how* a document was resolved — most notably
1425
+ * which fields served their value via EC-018 locale fallback.
1426
+ */
1427
+ interface ResolutionInfo {
1428
+ locale: Locale;
1429
+ perspective: Perspective;
1430
+ localeFallbacks: FieldLocaleFallback[];
1431
+ }
1432
+
1433
+ /** A scalar a filter can compare against (relation/asset filters use the id). */
1434
+ type FilterScalar = string | number | boolean;
1435
+ /** One field's filter: every present operator must hold (AND). */
1436
+ interface FilterCondition {
1437
+ eq?: FilterScalar;
1438
+ gt?: FilterScalar;
1439
+ gte?: FilterScalar;
1440
+ in?: readonly FilterScalar[];
1441
+ lt?: FilterScalar;
1442
+ lte?: FilterScalar;
1443
+ }
1444
+ /**
1445
+ * The `where` input: field name → condition. A bare scalar is shorthand for
1446
+ * `{ eq: value }`. Generated per-collection types (`PostWhere`) narrow the
1447
+ * keys to the fields declared `filterable` in the schema.
1448
+ */
1449
+ type WhereInput = Readonly<Record<string, FilterScalar | FilterCondition | undefined>>;
1450
+ type SortDirection = 'asc' | 'desc';
1451
+ /** Single-field sort; ties always break by document id ascending. */
1452
+ interface SortInput {
1453
+ field: string;
1454
+ direction?: SortDirection;
1455
+ }
1456
+ /** The full `listDocuments` query envelope (vision AD-3, rung 3). */
1457
+ interface ListDocumentsQuery<TWhere extends WhereInput = WhereInput> {
1458
+ where?: TWhere;
1459
+ sort?: SortInput;
1460
+ limit?: number;
1461
+ cursor?: string;
1462
+ }
1463
+ /**
1464
+ * Codes for structured accessor query errors (EC-143). These describe *caller*
1465
+ * mistakes (bad filter, bad cursor), as opposed to `CmsValidationIssue`s which
1466
+ * describe content/schema problems. Accessors never throw — invalid queries
1467
+ * come back as an error result carrying these.
1468
+ */
1469
+ type ContentQueryErrorCode = 'unknown-collection' | 'list-not-supported' | 'non-filterable-field' | 'invalid-filter' | 'invalid-sort' | 'invalid-limit' | 'invalid-cursor';
1470
+ interface ContentQueryError {
1471
+ code: ContentQueryErrorCode;
1472
+ message: string;
1473
+ /** Path into the query input, e.g. ['where', 'title'] or ['sort', 'field']. */
1474
+ path: CmsPath;
1475
+ meta?: Record<string, unknown>;
1476
+ }
1477
+
1478
+ /**
1479
+ * Cache-tag metadata carried by every accessor result (EC-143, vision AD-5):
1480
+ * fetch-time tagging of what was *actually returned*, so EC-146 can map each
1481
+ * entry to a `revalidateTag` call. Deterministic by construction — every list
1482
+ * is sorted and deduplicated.
1483
+ */
1484
+ interface CacheTags {
1485
+ /**
1486
+ * Identities of every document whose content is present in the result —
1487
+ * the returned documents themselves plus their depth-1 populated relation
1488
+ * targets. Entries are `documentKey` strings (`collection:id`), sorted.
1489
+ */
1490
+ docs: string[];
1491
+ /**
1492
+ * Collections the accessor read from (the coarse membership tags of AD-5:
1493
+ * create/delete/publish in a collection sweeps them). Sorted.
1494
+ */
1495
+ collections: string[];
1496
+ /**
1497
+ * Normalized URL paths the accessor touched (`resolvePage` only): the
1498
+ * requested path, plus every hop of a redirect chain — editing any hop
1499
+ * changes the result. Sorted.
1500
+ */
1501
+ routes: string[];
1502
+ /**
1503
+ * Asset ids the result *baked* its delivery shape from (EC-227): the page's
1504
+ * page-scoped `ResolvedAsset` set (url/dimensions/alt) + its `seoOgImage`.
1505
+ * Editing the asset record (alt/dimensions/re-upload) must drop the page that
1506
+ * baked it — without this dimension the only invalidation triggers are
1507
+ * unrelated (doc/collection/project), so an asset-only edit serves stale
1508
+ * until one of those fires. Sorted.
1509
+ */
1510
+ assets: string[];
1511
+ /** The locale the result was resolved for. */
1512
+ locale: Locale;
1513
+ }
1514
+
1515
+ /**
1516
+ * The closed-form v1 query surface (EC-143, vision AD-3): `resolvePage`,
1517
+ * `getDocument`, `listDocuments` — no query language. Every result is
1518
+ * delivery-shaped (EC-142) and carries deterministic `CacheTags` for EC-146.
1519
+ *
1520
+ * Pure and synchronous like the rest of the package: the client reads through
1521
+ * a `ContentLookup`; EC-144/EC-145 bind it to Convex/Next later. The ambient
1522
+ * `{ locale, perspective }` context is taken once at creation — never per-call
1523
+ * ceremony (per-call `locale` overrides exist for route-level needs only).
1524
+ */
1525
+ interface ResolvePageResult {
1526
+ /** Route resolution status (EC-019 parity): ok | redirect | notFound. */
1527
+ status: ResolveStatus;
1528
+ /** The matched route record, when status is `ok`. */
1529
+ route: RouteRecord | null;
1530
+ /** Extracted dynamic params, e.g. `{ slug: 'hello-world' }`. */
1531
+ dynamicParams: Record<string, string>;
1532
+ /** True when route matching fell back to the default locale (EC-018). */
1533
+ localeFallback: boolean;
1534
+ /** Terminal redirect target + permanence, when status is `redirect`. */
1535
+ redirect: {
1536
+ target: string;
1537
+ permanent: boolean;
1538
+ } | null;
1539
+ /**
1540
+ * The document this URL resolves to, delivery-shaped (EC-142, EC-187). A
1541
+ * `page`-collection document (its `body` composition renders into a layout)
1542
+ * or a content-collection document (rendered by a dev route component). SEO
1543
+ * for a page is read from this document's flat `seo*` fields — there is no
1544
+ * separate page-graph `seo` block anymore.
1545
+ */
1546
+ document: ResolvedDocument | null;
1547
+ issues: CmsValidationIssue[];
1548
+ tags: CacheTags;
1549
+ }
1550
+ interface GetDocumentResult<TDoc extends DeliveryDocument = ResolvedDocument> {
1551
+ /**
1552
+ * The delivery-shaped document, or `null` when not found / not visible in
1553
+ * the context perspective (correct behavior, not an error — see EC-142).
1554
+ */
1555
+ document: TDoc | null;
1556
+ info: ResolutionInfo | null;
1557
+ issues: CmsValidationIssue[];
1558
+ tags: CacheTags;
1559
+ }
1560
+ type ListDocumentsResult<TDoc extends DeliveryDocument = ResolvedDocument> = {
1561
+ ok: true;
1562
+ documents: TDoc[];
1563
+ /** Cursor for the next page, `null` when this page exhausts the list. */
1564
+ nextCursor: string | null;
1565
+ issues: CmsValidationIssue[];
1566
+ tags: CacheTags;
1567
+ } | {
1568
+ /** The query itself was invalid (never thrown — structured errors). */
1569
+ ok: false;
1570
+ errors: ContentQueryError[];
1571
+ tags: CacheTags;
1572
+ };
1573
+ interface ContentClient {
1574
+ readonly context: ContentContext;
1575
+ /** Structural route issues from construction (conflicts, redirect loops). */
1576
+ readonly routerIssues: readonly CmsValidationIssue[];
1577
+ resolvePage(url: string, locale?: Locale): ResolvePageResult;
1578
+ getDocument<TDoc extends DeliveryDocument = ResolvedDocument>(collection: string, idOrSlug: string, locale?: Locale): GetDocumentResult<TDoc>;
1579
+ listDocuments<TDoc extends DeliveryDocument = ResolvedDocument, TWhere extends WhereInput = WhereInput>(collection: string, query?: ListDocumentsQuery<TWhere>): ListDocumentsResult<TDoc>;
1580
+ /**
1581
+ * Populate a relation reference to depth-1 delivery shape (EC-254), or `null`
1582
+ * when the target is missing or not visible in the active perspective. A
1583
+ * renderer host wires this as `RenderContext.resolveRelation` to populate the
1584
+ * COMPOSITION-NODE relation props of the nodes inside a page's `blocks` field
1585
+ * (e.g. a ProductGrid block's picked `product`s) — which `resolvePage` leaves
1586
+ * raw, since document resolution does not descend into a composition value.
1587
+ */
1588
+ resolveRelation(ref: DocumentRef): ResolvedDocument | null;
1589
+ }
1590
+
1591
+ /**
1592
+ * Binding payloads for the embedded runtime (EC-144): one payload value per
1593
+ * binding `sourceId`. The catch-all route helper provides `document` (the
1594
+ * route-bound resolved document) and `params` (dynamic route params) by
1595
+ * default; hosts add more through the route helper's `payloads` factory
1596
+ * (e.g. a `listDocuments` result for a posts repeater).
1597
+ *
1598
+ * This is a deliberately small, pure resolver — the embedded runtime never
1599
+ * imports `@elytracms/data-binding` (a builder-side package). Semantics mirror
1600
+ * EC-023 tokens: a binding token is a JSON Pointer (RFC 6901) into the
1601
+ * payload; a miss resolves to `undefined`, never a throw (the renderer turns
1602
+ * unresolvable bindings into visible fallback behavior, EC-015).
1603
+ */
1604
+ type BindingPayloads = Record<string, unknown>;
1605
+ /**
1606
+ * Marker key of an {@link SourcePayloadError} payload. A plain string property
1607
+ * (not a Symbol) on purpose: the route helper's cached path JSON-serializes
1608
+ * payloads through `unstable_cache`, and the marker must survive that
1609
+ * round-trip.
1610
+ */
1611
+ declare const SOURCE_PAYLOAD_ERROR_KEY = "__elytraSourcePayloadError";
1612
+ /**
1613
+ * Explicit issue payload for a source whose stored query was REJECTED at
1614
+ * delivery (EC-177 gap 3) — e.g. a sort/filter field that is no longer
1615
+ * filterable in the live schema. Materialization stores this instead of a
1616
+ * silently-absent payload; `createBindingResolver` throws on it, which the
1617
+ * renderer converts into its visible per-node render-error fallback (EC-015).
1618
+ * Validation principle: a broken stored query is a visible state, never an
1619
+ * empty section.
1620
+ */
1621
+ interface SourcePayloadError {
1622
+ [SOURCE_PAYLOAD_ERROR_KEY]: true;
1623
+ sourceId: string;
1624
+ /** Human-readable messages of the structured query errors. */
1625
+ messages: string[];
1626
+ }
1627
+ /** Build the explicit issue payload for one failed source query. */
1628
+ declare function sourcePayloadError(sourceId: string, messages: readonly string[]): SourcePayloadError;
1629
+ /** `true` when a payload value is an explicit {@link SourcePayloadError}. */
1630
+ declare function isSourcePayloadError(value: unknown): value is SourcePayloadError;
1631
+ /**
1632
+ * Resolve a JSON Pointer token within a payload value. Numeric segments index
1633
+ * arrays; string segments index plain objects. Any miss (unknown key,
1634
+ * out-of-range index, traversal through a primitive) yields `undefined`.
1635
+ */
1636
+ declare function resolvePayloadToken(payload: unknown, token: string): unknown;
1637
+ /**
1638
+ * Create the renderer's `ResolveBinding` over a payload map. Mode semantics
1639
+ * (brief §4.6, uniform binding model):
1640
+ *
1641
+ * - `value` / `object` / `condition` — the value at the token within the
1642
+ * payload named by `sourceId`.
1643
+ * - `spread` — same lookup; the renderer spreads the resulting object.
1644
+ * - `repeaterItem` — the token resolves against the active repeater item,
1645
+ * not the root payload.
1646
+ *
1647
+ * Total for data misses: an unknown `sourceId` or missing path resolves to
1648
+ * `undefined` — the renderer (EC-015) degrades visibly instead of crashing.
1649
+ * An explicit {@link SourcePayloadError} payload (a REJECTED stored query, not
1650
+ * merely missing data) throws instead: the renderer catches per node and
1651
+ * renders its visible render-error fallback with the query error message;
1652
+ * conditions treat the throw as `false` (the node hides) — never a page crash.
1653
+ */
1654
+ declare function createBindingResolver(payloads: BindingPayloads): ResolveBinding;
1655
+
1656
+ /**
1657
+ * `<CanvasRenderer />` (EC-144, vision AD-1): an async React Server Component
1658
+ * that renders a resolved page (from `resolvePage`, EC-143) through
1659
+ * `@elytracms/runtime-renderer` with the host app's component registry. EC-015
1660
+ * fallback behavior is identical to the builder: a missing component, invalid
1661
+ * prop, or missing required slot renders a visible fallback — a page never
1662
+ * crashes and never silently omits a node.
1663
+ */
1664
+ interface CanvasRendererProps {
1665
+ /** The `resolvePage` result for the current URL. */
1666
+ result: ResolvePageResult;
1667
+ /** Host component surface from `defineHostComponents`. */
1668
+ components: HostComponents;
1669
+ /**
1670
+ * The full project graph for the active perspective (from
1671
+ * `ContentSource.graph`). Needed for layout composition; without it the
1672
+ * page root renders without its layout.
1673
+ */
1674
+ graph?: ProjectGraph | null;
1675
+ /**
1676
+ * Binding payloads keyed by `sourceId`. Defaults to
1677
+ * `{ document, params }` derived from the result.
1678
+ */
1679
+ payloads?: BindingPayloads;
1680
+ /** Full custom binding resolver — overrides `payloads` when provided. */
1681
+ resolveBinding?: ResolveBinding;
1682
+ /**
1683
+ * Delivery-shaped assets the page may reference (EC-195, from the render
1684
+ * outcome). An `asset`-typed Image prop resolves through these to a url +
1685
+ * intrinsic dimensions; an unknown id degrades to the visible fallback.
1686
+ */
1687
+ assets?: readonly ResolvedAsset[];
1688
+ /**
1689
+ * Populated composition-node `relation` props, keyed `collection:id` (EC-254,
1690
+ * from the render outcome). A block's `relation` prop (e.g. a ProductGrid's
1691
+ * picked products) resolves through these to the populated target; an unknown
1692
+ * key degrades to the block's own empty state. Absent → relation props pass
1693
+ * through unresolved (raw ref), the same degradation as a missing asset list.
1694
+ */
1695
+ relations?: Record<string, RelationTarget>;
1696
+ /**
1697
+ * Documents each `listing` block resolved to, keyed by serialized query (EC-255,
1698
+ * from the render outcome). A ProductGrid keyed to a picked `category` reads its
1699
+ * products through this; an unknown key (or absent map) degrades to the block's
1700
+ * own empty state — the same degradation as a missing relation.
1701
+ */
1702
+ listings?: Record<string, RelationTarget[]>;
1703
+ }
1704
+ /** Default payloads: the route-bound document and the dynamic route params. */
1705
+ declare function defaultBindingPayloads(result: ResolvePageResult): BindingPayloads;
1706
+ /**
1707
+ * Render a resolved page server-side. Async so hosts can compose it like any
1708
+ * other RSC; resolution itself is synchronous (the content was prefetched by
1709
+ * the `ContentSource`).
1710
+ */
1711
+ declare function CanvasRenderer(props: CanvasRendererProps): Promise<ReactNode>;
1712
+
1713
+ /**
1714
+ * The content seam of the embedded runtime (EC-144, vision AD-1/AD-4): the
1715
+ * catch-all route helper consumes this interface, so where the content comes
1716
+ * from is swappable. EC-144 ships the in-memory snapshot bridge
1717
+ * (`createContentSnapshot` over a `PersistenceAdapter`); EC-145/146 plug a
1718
+ * Convex-backed source into the exact same seam.
1719
+ *
1720
+ * Accessors are synchronous over a prefetched `ContentLookup` (vision AD-4);
1721
+ * any fetching happens when the source is loaded, before clients are minted.
1722
+ */
1723
+ /**
1724
+ * One data-source definition the published graph's bindings reference
1725
+ * (EC-166 delivery): the structural shape of an `@elytracms/data-binding`
1726
+ * `DataSource`, declared here so the embedded runtime stays free of
1727
+ * builder-side packages. Only `cms`-kind sources with the EC-166 per-scope id
1728
+ * prefixes (`cms.section.<nodeId>` / `cms.template.<pageId>`) participate in
1729
+ * automatic payload materialization; everything else is ignored.
1730
+ */
1731
+ interface CanvasDataSource {
1732
+ kind: string;
1733
+ id: string;
1734
+ label?: string;
1735
+ /** Kind-specific config; cms query configs parse via `@elytracms/content`. */
1736
+ config?: unknown;
1737
+ }
1738
+ interface ContentSource {
1739
+ /** The project's locale configuration (default + supported set, EC-018). */
1740
+ readonly locales: LocaleConfig;
1741
+ /**
1742
+ * Mint a typed accessor client (EC-143) bound to a perspective. The locale
1743
+ * defaults to the project default; the route helper overrides it per
1744
+ * request from the URL's locale prefix.
1745
+ */
1746
+ client(init: {
1747
+ perspective: Perspective;
1748
+ locale?: Locale;
1749
+ }): ContentClient;
1750
+ /**
1751
+ * The full project graph visible in a perspective — `draft` is the latest
1752
+ * working revision, `published` the revision pinned by publishing state.
1753
+ * `null` when nothing is visible (e.g. never published). The renderer needs
1754
+ * the graph (not just the page) for layout composition.
1755
+ */
1756
+ graph(perspective: Perspective): ProjectGraph | null;
1757
+ /**
1758
+ * The route records the source's clients resolve against (EC-171, additive).
1759
+ * Lets enumerating consumers — the sitemap helper — walk the same route
1760
+ * table `resolvePage` uses, instead of keeping a second list. Optional so
1761
+ * existing custom sources stay valid; without it the sitemap helper needs
1762
+ * explicit routes.
1763
+ */
1764
+ readonly routes?: readonly RouteRecord[];
1765
+ /**
1766
+ * Data-source definitions the graph's bindings reference (EC-166 delivery,
1767
+ * additive). When present, the route helper materializes per-section list
1768
+ * sources automatically by replaying their stored where/sort/limit through
1769
+ * `listDocuments` — see `materializeSourcePayloads`. Optional so existing
1770
+ * custom sources stay valid; absent definitions degrade to unresolved
1771
+ * bindings (the renderer's visible fallbacks), never a crash.
1772
+ */
1773
+ readonly sources?: readonly CanvasDataSource[];
1774
+ /**
1775
+ * The project's delivery-shaped assets (EC-195, additive). Lets the route
1776
+ * helper bake the assets a page may reference into the (serializable) render
1777
+ * outcome, so the renderer can resolve an `asset`-typed Image prop to a usable
1778
+ * url + intrinsic dimensions — and a cached page keeps its images during a
1779
+ * backend outage. Optional so existing custom sources stay valid; without it,
1780
+ * asset-typed props fall back to their raw value (the renderer's visible
1781
+ * degradation), never a crash.
1782
+ */
1783
+ assets?(): readonly ResolvedAsset[];
1784
+ }
1785
+ /**
1786
+ * Merge accessor cache tags into one deterministic set (sorted, deduplicated)
1787
+ * — used by the route helper to combine the `resolvePage` tags with tags
1788
+ * added by the host's payload factory (e.g. a `listDocuments` call). The
1789
+ * merged set is what the EC-146 `onTags` hook receives.
1790
+ */
1791
+ declare function mergeCacheTags(first: CacheTags, ...rest: readonly CacheTags[]): CacheTags;
1792
+
1793
+ /**
1794
+ * Automatic payload materialization for EC-166 repeating sections — the
1795
+ * delivery half of the builder's collection-driven LAYOUTS (EC-187: layouts
1796
+ * are the only graph trees that can bind section sources now). A published
1797
+ * layout may carry bindings against per-scope CMS sources; the renderer needs
1798
+ * the source PAYLOADS:
1799
+ *
1800
+ * - **Repeating sections** (`cms.section.<nodeId>`): the stored source config
1801
+ * carries the closed AD-3 query. For every section source a referenced
1802
+ * layout tree actually binds, the stored where/sort/limit replays through
1803
+ * the request client's `listDocuments` — published perspective and ambient
1804
+ * locale come from the client itself. The list result's `CacheTags` (the
1805
+ * fetch-time doc tags of what was actually returned plus the coarse
1806
+ * collection tag — the EC-146 contract) are surfaced so the route helper
1807
+ * merges them into the request tag set.
1808
+ *
1809
+ * The `cms.template.<pageId>` "current document" source is RETIRED (EC-187):
1810
+ * there is no template page, and a routed page document's `body` uses static
1811
+ * props (v1). A collection detail route's document is read by the dev route
1812
+ * component directly via the route's `document` ref.
1813
+ *
1814
+ * Everything degrades visibly, never a crash:
1815
+ *
1816
+ * - a missing source definition, a non-cms kind, or a malformed config leaves
1817
+ * the payload absent, so the binding resolves to `undefined` and the
1818
+ * renderer shows its EC-015 fallback (an empty list renders the section's
1819
+ * empty state);
1820
+ * - a structured query error (the stored query was REJECTED at delivery, e.g.
1821
+ * a sort field no longer filterable in the live schema) stores an explicit
1822
+ * {@link sourcePayloadError} payload instead — the binding resolver throws
1823
+ * on it and the renderer shows its visible per-node render-error fallback,
1824
+ * not a silently empty section (EC-177 gap 3).
1825
+ */
1826
+ /** Collect every binding `sourceId` referenced in a node tree (props, conditions, slots). */
1827
+ declare function collectBindingSourceIds(node: ComponentNode, into?: Set<string>): Set<string>;
1828
+ interface MaterializeSourcePayloadsInput {
1829
+ /** Accessor client bound to this request's perspective + locale. */
1830
+ client: ContentClient;
1831
+ result: ResolvePageResult;
1832
+ /** The graph for the active perspective (layout trees may bind sections too). */
1833
+ graph: ProjectGraph | null;
1834
+ /** Source definitions from the `ContentSource` (absent ones simply skip). */
1835
+ sources: readonly CanvasDataSource[];
1836
+ }
1837
+ interface MaterializedSourcePayloads {
1838
+ payloads: BindingPayloads;
1839
+ /** `CacheTags` of every accessor call made here (one entry per section list). */
1840
+ tags: CacheTags[];
1841
+ }
1842
+ /**
1843
+ * Materialize the EC-166 source payloads for one resolved page. Pure over the
1844
+ * prefetched source — `listDocuments` is synchronous; tags are returned (not
1845
+ * applied) so the caller owns the merge.
1846
+ */
1847
+ declare function materializeSourcePayloads(input: MaterializeSourcePayloadsInput): MaterializedSourcePayloads;
1848
+
1849
+ interface ContentSnapshotOptions {
1850
+ /** Route records served by `resolvePage` (EC-019). */
1851
+ routes?: readonly RouteRecord[];
1852
+ /** Redirect records served by `resolvePage` (EC-019/020). */
1853
+ redirects?: readonly RedirectRecord[];
1854
+ /** Locale configuration; defaults to English-only. */
1855
+ locales?: LocaleConfig;
1856
+ /**
1857
+ * Data-source definitions the graph's bindings reference (EC-166): section
1858
+ * sources (`cms.section.<nodeId>`) carry their stored where/sort/limit, and
1859
+ * the route helper materializes their payloads automatically.
1860
+ */
1861
+ sources?: readonly CanvasDataSource[];
1862
+ }
1863
+ interface ContentSnapshot extends ContentSource {
1864
+ readonly projectId: string;
1865
+ }
1866
+ /**
1867
+ * Plain prefetched data for {@link createStaticContentSource} (EC-156): the
1868
+ * same inputs `createContentSnapshot` reads from a `PersistenceAdapter`, as
1869
+ * already-loaded values. Everything content-bearing is optional — an absent
1870
+ * piece simply resolves to "not visible", with the runtime's explicit
1871
+ * fallbacks (never a crash).
1872
+ */
1873
+ interface StaticContentData {
1874
+ projectId: string;
1875
+ /** Locale configuration; defaults to English-only. */
1876
+ locales?: LocaleConfig;
1877
+ /** Route records served by `resolvePage` (EC-019). */
1878
+ routes?: readonly RouteRecord[];
1879
+ /** Redirect records served by `resolvePage` (EC-019/020). */
1880
+ redirects?: readonly RedirectRecord[];
1881
+ collections?: readonly CollectionDef[];
1882
+ /**
1883
+ * Stored document sources. Either a bare `CmsDocument` (gated by its own
1884
+ * `state`) or a `DocumentHistory` carrying the live draft + the pinned
1885
+ * published snapshot (EC-224). Perspective gating happens in the pure
1886
+ * accessors (`selectPerspective`, EC-142): a `published` client sees a
1887
+ * history's pinned snapshot (or nothing), and never a bare `draft` row.
1888
+ */
1889
+ documents?: readonly ContentDocumentSource[];
1890
+ assets?: readonly AssetRecord[];
1891
+ /**
1892
+ * The graph visible per perspective: `draft` is the latest working
1893
+ * revision, `published` the revision pinned by publishing state. `null` /
1894
+ * absent means "nothing visible in that perspective".
1895
+ */
1896
+ graphs?: {
1897
+ draft?: ProjectGraph | null;
1898
+ published?: ProjectGraph | null;
1899
+ };
1900
+ /**
1901
+ * Resolve a serve URL for assets without a stored `url`. Defaults to an
1902
+ * explicit unresolved-placeholder scheme (never a silent empty string).
1903
+ */
1904
+ assetUrl?: (asset: AssetRecord) => string;
1905
+ /**
1906
+ * Data-source definitions the graph's bindings reference (EC-166 delivery).
1907
+ * Absent definitions degrade to unresolved bindings, never a crash.
1908
+ */
1909
+ sources?: readonly CanvasDataSource[];
1910
+ }
1911
+ /**
1912
+ * Merge stored route records with derived fallback routes (EC-177 gap 2):
1913
+ * stored records win per pattern+locale; fallback routes only fill patterns
1914
+ * the stored set does not cover. Never XOR — the first stored route must not
1915
+ * drop the implicit per-locale home-page fallback (or vice versa).
1916
+ *
1917
+ * Coverage rules mirror the router's locale matching (EC-018/019): a stored
1918
+ * locale-agnostic route (no `locale`) covers its pattern for every locale; a
1919
+ * locale-specific stored route covers only that locale, so fallbacks for the
1920
+ * other locales still apply.
1921
+ */
1922
+ declare function mergeRouteRecords(stored: readonly RouteRecord[], fallback: readonly RouteRecord[]): RouteRecord[];
1923
+ /**
1924
+ * Assemble a `ContentSource` from plain prefetched data — the pure half of
1925
+ * {@link createContentSnapshot}, shared with hosts that fetched the data
1926
+ * themselves (e.g. the EC-156 live delivery snapshot). Accessor semantics are
1927
+ * byte-identical across both paths: the same `ContentLookup` feeds the same
1928
+ * pure `@elytracms/content` client.
1929
+ */
1930
+ declare function createStaticContentSource(data: StaticContentData): ContentSnapshot;
1931
+ /**
1932
+ * Prefetch a project's content from a `PersistenceAdapter` into an in-memory
1933
+ * `ContentSource`. The snapshot is a point-in-time view: load once per
1934
+ * request (or memoize across requests for fixture-backed sites) — accessors
1935
+ * never reach back to the adapter.
1936
+ */
1937
+ declare function createContentSnapshot(adapter: PersistenceAdapter, projectId: string, options?: ContentSnapshotOptions): Promise<ContentSnapshot>;
1938
+
1939
+ /**
1940
+ * The pure half of the catch-all route helper (EC-144): URL → outcome, with
1941
+ * no Next.js imports — so redirects, 404s, locale handling, payload assembly,
1942
+ * and tag merging are all unit-testable as plain functions. `route.tsx` maps
1943
+ * each outcome onto `redirect()` / `permanentRedirect()` / `notFound()` /
1944
+ * `<CanvasRenderer />`.
1945
+ */
1946
+ /** What the host's payload factory sees for one request. */
1947
+ interface CanvasPayloadContext {
1948
+ /** Accessor client bound to this request's perspective + locale. */
1949
+ client: ContentClient;
1950
+ result: ResolvePageResult;
1951
+ perspective: Perspective;
1952
+ /** The normalized request path (locale prefix stripped). */
1953
+ path: string;
1954
+ params: Record<string, string>;
1955
+ /**
1956
+ * Register the `CacheTags` of any extra accessor call this factory makes
1957
+ * (e.g. `listDocuments`) so they join the request's merged tag set — the
1958
+ * EC-146 invalidation hook sees everything the request actually read.
1959
+ */
1960
+ addTags(tags: CacheTags): void;
1961
+ }
1962
+ /**
1963
+ * Builds the binding payloads for a page render. The defaults
1964
+ * (`document`, `params`) are always present; factory entries win on conflict.
1965
+ */
1966
+ type CanvasPayloadsFactory = (ctx: CanvasPayloadContext) => BindingPayloads | Promise<BindingPayloads>;
1967
+ type CanvasPageOutcome = {
1968
+ kind: 'redirect';
1969
+ target: string;
1970
+ permanent: boolean;
1971
+ tags: CacheTags;
1972
+ } | {
1973
+ kind: 'not-found';
1974
+ tags: CacheTags;
1975
+ } | {
1976
+ kind: 'render';
1977
+ result: ResolvePageResult;
1978
+ graph: ProjectGraph | null;
1979
+ payloads: BindingPayloads;
1980
+ /**
1981
+ * The delivery-shaped assets the page may reference (EC-195). Baked into
1982
+ * the outcome (serializable) so the renderer resolves `asset`-typed Image
1983
+ * props to a url + intrinsic dimensions, and a cached page keeps its
1984
+ * images during a backend outage.
1985
+ */
1986
+ assets: readonly ResolvedAsset[];
1987
+ /**
1988
+ * The composition-node `relation` props this page references, populated to
1989
+ * depth-1 delivery shape and keyed `collection:id` (EC-254). Baked into the
1990
+ * outcome (serializable) so `<CanvasRenderer />` rebuilds
1991
+ * `RenderContext.resolveRelation` from it inside the RSC — a function (the
1992
+ * content client) cannot ride the data cache or cross the RSC boundary, so
1993
+ * the data is baked exactly as the page-scoped `assets` list is.
1994
+ */
1995
+ relations: Record<string, RelationTarget>;
1996
+ /**
1997
+ * The documents each `listing` block on this page resolved to (EC-255),
1998
+ * keyed by serialized query. Baked into the outcome (serializable) so
1999
+ * `<CanvasRenderer />` rebuilds `RenderContext.resolveListing` from it inside
2000
+ * the RSC — like `relations`, because the content client that runs the query
2001
+ * cannot ride the data cache or cross the RSC boundary. A listing depends on
2002
+ * collection MEMBERSHIP (a product joining/leaving the category), so its
2003
+ * invalidation rides the per-query `listDocuments` tags merged into `tags`.
2004
+ */
2005
+ listings: Record<string, RelationTarget[]>;
2006
+ tags: CacheTags;
2007
+ };
2008
+ /** Build the request path from a Next catch-all `slug` param. */
2009
+ declare function pathFromSlug(slug: readonly string[] | undefined): string;
2010
+ /**
2011
+ * Derive the request locale from a leading URL segment (`/de/about` → locale
2012
+ * `de`, path `/about`) and return the remaining path. No prefix (or the
2013
+ * default locale's own content at root) falls back to the default locale.
2014
+ */
2015
+ declare function splitLocalePath(url: string, locales: LocaleConfig): {
2016
+ locale: Locale;
2017
+ path: string;
2018
+ };
2019
+ interface ResolveCanvasPageOptions {
2020
+ payloads?: CanvasPayloadsFactory;
2021
+ /**
2022
+ * Component registry (EC-205): when provided, the baked {@link CanvasPageOutcome}
2023
+ * `assets` are PAGE-SCOPED to exactly the asset ids this page's composition +
2024
+ * layout resolve through `resolveAsset` — not the whole project asset list,
2025
+ * which otherwise rides in every page's cache entry + RSC payload. Omitted →
2026
+ * all source assets (back-compat). Scoping mirrors the renderer's asset
2027
+ * resolution exactly, so no referenced image is ever dropped.
2028
+ */
2029
+ registry?: ComponentRegistry;
2030
+ }
2031
+ /**
2032
+ * Resolve one request URL against a content source: split the locale, run
2033
+ * `resolvePage` in the requested perspective, assemble binding payloads, and
2034
+ * merge every accessor's `CacheTags` into one deterministic set. Pure — all
2035
+ * data was prefetched by the source.
2036
+ */
2037
+ declare function resolveCanvasPage(source: ContentSource, url: string, perspective: Perspective, options?: ResolveCanvasPageOptions): Promise<CanvasPageOutcome>;
2038
+
2039
+ /**
2040
+ * Catch-all route helper (EC-144): the factory a host app calls once in
2041
+ * `app/[[...slug]]/page.tsx` to wire `resolvePage` → `<CanvasRenderer />`,
2042
+ * with redirects answered by Next's `redirect()`/`permanentRedirect()` (per
2043
+ * the route's `permanent` flag), unknown URLs answered by `notFound()` (the
2044
+ * host's 404 renders), and the perspective chosen by Next `draftMode()` —
2045
+ * draft when enabled (via the token-gated preview route), published
2046
+ * otherwise.
2047
+ *
2048
+ * With `cache` configured (EC-146), the published-perspective resolve+render
2049
+ * data work runs inside `unstable_cache`, keyed by URL and tagged with the
2050
+ * fetch-time tag set of what the response actually contained (see
2051
+ * `cache.ts`). Publishing in the studio then invalidates exactly the affected
2052
+ * entries via the signed `/api/revalidate` webhook — and while the content
2053
+ * backend is unreachable, cached entries keep serving because a cache hit
2054
+ * never invokes `loadContent`.
2055
+ */
2056
+ interface CanvasRequestInfo {
2057
+ /** The raw request path (including any locale prefix). */
2058
+ url: string;
2059
+ perspective: Perspective;
2060
+ }
2061
+ interface CanvasCacheOptions {
2062
+ /**
2063
+ * Project scope prefix for every tag (`p:<scope>:…`) — must be the project
2064
+ * id the studio publishes under, so the webhook dispatcher's tags match.
2065
+ * Omit only when the emitter also emits unscoped tags (see `cache.ts`).
2066
+ */
2067
+ scope?: string;
2068
+ }
2069
+ /**
2070
+ * What the route helper tells the content loader about the request it is
2071
+ * loading for (EC-156). Loaders that serve both perspectives from one
2072
+ * prefetched source (the fixture snapshot) can ignore it; a live loader uses
2073
+ * it to fetch the draft-perspective snapshot only for draft-mode requests —
2074
+ * it must NOT call request APIs like `draftMode()` itself, because on the
2075
+ * cached published path the loader runs inside `unstable_cache`, where
2076
+ * dynamic request APIs are unavailable.
2077
+ */
2078
+ interface CanvasContentRequest {
2079
+ perspective: Perspective;
2080
+ }
2081
+ interface CanvasRouteOptions {
2082
+ /**
2083
+ * Async content loader — the seam EC-145/146 plug Convex into. For the
2084
+ * fixture-backed example this memoizes a `createContentSnapshot` call.
2085
+ * Invoked per request; cache/memoize inside the loader as appropriate.
2086
+ * With `cache` configured it only runs on a cache miss (once per
2087
+ * revalidation cycle) — cache hits never touch the content backend.
2088
+ * Receives the request's perspective (see {@link CanvasContentRequest});
2089
+ * zero-arg loaders remain valid.
2090
+ */
2091
+ loadContent: (request: CanvasContentRequest) => Promise<ContentSource>;
2092
+ /** Host component surface from `defineHostComponents`. */
2093
+ components: HostComponents;
2094
+ /** Extra binding payloads per request (e.g. `listDocuments` for a list page). */
2095
+ payloads?: CanvasPayloadsFactory;
2096
+ /**
2097
+ * EC-146 instant publish: cache the published-perspective resolution in
2098
+ * Next's data cache (`unstable_cache`), tagged with the response's own
2099
+ * fetch-time tags, invalidated by the `/api/revalidate` webhook. Draft mode
2100
+ * always bypasses this cache entirely. Tag-driven only — no time-based
2101
+ * revalidation (the AD-5 model). Cached values are JSON-serialized, so
2102
+ * payload factories must return plain data on the cached path.
2103
+ */
2104
+ cache?: CanvasCacheOptions;
2105
+ /**
2106
+ * EC-146 hook: receives the merged `CacheTags` of everything this request
2107
+ * read (the `resolvePage` tags plus any tags the payload factory
2108
+ * registered via `addTags`). On a cache hit these are the stored tags of
2109
+ * the run that wrote the entry.
2110
+ */
2111
+ onTags?: (tags: CacheTags, info: CanvasRequestInfo) => void;
2112
+ /** Override the generated `<head>` metadata for a resolved page. */
2113
+ metadata?: (result: ResolvePageResult, info: CanvasRequestInfo) => Metadata;
2114
+ }
2115
+ interface CanvasRouteProps {
2116
+ params: Promise<{
2117
+ slug?: string[];
2118
+ }>;
2119
+ }
2120
+ interface CanvasRoute {
2121
+ /** The page component: `export default route.Page`. */
2122
+ Page: (props: CanvasRouteProps) => Promise<ReactNode>;
2123
+ /** Next metadata hook: `export const generateMetadata = route.generateMetadata`. */
2124
+ generateMetadata: (props: CanvasRouteProps) => Promise<Metadata>;
2125
+ }
2126
+ /** Create the `{ Page, generateMetadata }` pair for `app/[[...slug]]/page.tsx`. */
2127
+ declare function createCanvasRoute(options: CanvasRouteOptions): CanvasRoute;
2128
+
2129
+ declare function canvasPageMetadata(result: ResolvePageResult, assets?: readonly ResolvedAsset[]): Metadata;
2130
+
2131
+ /**
2132
+ * `sitemap.xml` from published routes (EC-171): walk the same route records
2133
+ * `resolvePage` resolves against, resolve every candidate URL in the
2134
+ * **published** perspective, and emit `MetadataRoute.Sitemap` entries per
2135
+ * locale — Next's `app/sitemap.ts` convention. Pages marked `noindex` (and
2136
+ * URLs that do not resolve, e.g. a page that was never published) are
2137
+ * omitted.
2138
+ *
2139
+ * Dynamic routes (`/blog/:slug`): a route record does not declare which
2140
+ * collection feeds its params, so enumeration is delegated to the host's
2141
+ * {@link SitemapParamsProvider} — it gets a published-perspective accessor
2142
+ * client and returns one param record per concrete URL (typically from
2143
+ * `listDocuments`). **Honest limit:** parameterized routes without a provider
2144
+ * (or provider entries missing a param) are skipped and reported in
2145
+ * `skipped`, never guessed. `lastModified` is omitted — the delivery shape
2146
+ * carries no per-page timestamps today.
2147
+ *
2148
+ * Hierarchy mounts (`/:path*`, EC-218): a page-tree mount self-describes its
2149
+ * URLs, so it is enumerated automatically from the collection's published
2150
+ * documents (composing each page's nested path from its parent chain) — no
2151
+ * params provider entry needed.
2152
+ *
2153
+ * Host usage (`app/sitemap.ts`):
2154
+ *
2155
+ * ```ts
2156
+ * import { createCanvasSitemap } from '@elytracms/next'
2157
+ * import { loadContent } from '../lib/content'
2158
+ *
2159
+ * export default createCanvasSitemap({
2160
+ * loadContent,
2161
+ * baseUrl: 'https://example.com',
2162
+ * params: ({ route, client }) =>
2163
+ * route.id === 'r-post'
2164
+ * ? (client.listDocuments('post').ok ? … : []) // slug per published post
2165
+ * : null,
2166
+ * })
2167
+ * ```
2168
+ */
2169
+ interface SitemapRouteContext {
2170
+ route: RouteRecord;
2171
+ locale: Locale;
2172
+ /** Published-perspective accessor client bound to `locale`. */
2173
+ client: ContentClient;
2174
+ }
2175
+ /**
2176
+ * Enumerate the param sets of one dynamic route — one record per concrete
2177
+ * URL (e.g. `[{ slug: 'hello-world' }, …]`). Return `null`/`undefined` to
2178
+ * skip the route (reported in `skipped`).
2179
+ */
2180
+ type SitemapParamsProvider = (ctx: SitemapRouteContext) => readonly Record<string, string>[] | null | undefined;
2181
+ interface CanvasSitemapOptions {
2182
+ /** Absolute site origin, e.g. `https://example.com` (no trailing slash needed). */
2183
+ baseUrl: string;
2184
+ /** Route records to enumerate; defaults to the source's own route table. */
2185
+ routes?: readonly RouteRecord[];
2186
+ /** Param enumeration for dynamic routes (see {@link SitemapParamsProvider}). */
2187
+ params?: SitemapParamsProvider;
2188
+ }
2189
+ interface SitemapSkippedRoute {
2190
+ routeId: string;
2191
+ pattern: string;
2192
+ reason: string;
2193
+ }
2194
+ interface CanvasSitemapResult {
2195
+ entries: MetadataRoute.Sitemap;
2196
+ /** Routes that could not be enumerated — explicit, never silent. */
2197
+ skipped: SitemapSkippedRoute[];
2198
+ }
2199
+ /**
2200
+ * Enumerate the sitemap entries of one content source (published
2201
+ * perspective). Pure over the prefetched source — synchronous accessors,
2202
+ * deterministic ordering (sorted by URL).
2203
+ */
2204
+ declare function canvasSitemapEntries(source: ContentSource, options: CanvasSitemapOptions): CanvasSitemapResult;
2205
+ interface CreateCanvasSitemapOptions extends CanvasSitemapOptions {
2206
+ /** The same content loader the catch-all route uses (published perspective). */
2207
+ loadContent: (request: {
2208
+ perspective: 'published';
2209
+ }) => Promise<ContentSource>;
2210
+ }
2211
+ /**
2212
+ * The `app/sitemap.ts` one-liner: `export default createCanvasSitemap({ … })`.
2213
+ * Loads the published content source and returns the enumerated entries
2214
+ * (skipped routes are dropped here — use {@link canvasSitemapEntries} to
2215
+ * inspect them).
2216
+ */
2217
+ declare function createCanvasSitemap(options: CreateCanvasSitemapOptions): () => Promise<MetadataRoute.Sitemap>;
2218
+
2219
+ /**
2220
+ * Cache-tag pipeline (EC-146, vision AD-5) — the pure half.
2221
+ *
2222
+ * This module defines (a) the **canonical Next.js tag string format** mapping
2223
+ * the accessor-level `CacheTags` (EC-143) onto `revalidateTag` keys, and (b)
2224
+ * the **tag-discovery caching logic** the catch-all route helper runs inside
2225
+ * `unstable_cache`. No Next.js imports — everything here is unit-testable;
2226
+ * `route.tsx` injects the real `unstable_cache` via {@link CacheWrapper}.
2227
+ *
2228
+ * ## Tag format
2229
+ *
2230
+ * - `doc:<collection>:<id>` — one per document whose content was actually in
2231
+ * the response (fetch-time tagging: includes depth-1 populated relation
2232
+ * targets, and documents inside filtered/dynamic lists *after* the filter
2233
+ * ran).
2234
+ * - `collection:<name>:<locale>` — the coarse membership tag: every response
2235
+ * that read from a collection carries it, so create/delete/newly-matching
2236
+ * documents sweep cached lists without read-set tracking.
2237
+ * - `route:<pattern>:<locale>` — the resolved URL path plus every hop of a
2238
+ * redirect chain.
2239
+ * - `asset:<id>` — one per asset whose delivery shape the page *baked*
2240
+ * (page-scoped images + `seoOgImage`, EC-227); editing that asset record
2241
+ * (alt/dimensions/re-upload) drops exactly the pages that baked it. Not
2242
+ * locale-scoped — an asset record is locale-agnostic.
2243
+ * - `project` — the whole-project sweep tag attached to **every** cached
2244
+ * entry; graph publish/unpublish emits it (a layout or route change can
2245
+ * affect any page, so the honest v1 fallback is a full project sweep).
2246
+ *
2247
+ * ## Project scope
2248
+ *
2249
+ * Every tag is prefixed `p:<projectId>:` when a scope is configured (e.g.
2250
+ * `p:prj_aurora:doc:post:post-1`). The cost is a few bytes per tag and it
2251
+ * keeps multi-project hosts (one Next app serving several builder projects)
2252
+ * from cross-invalidating, so the prefix is **on whenever the host knows its
2253
+ * project id** — decision: include it. The emitter (the studio's webhook
2254
+ * dispatcher, `apps/builder/src/lib/webhooks`) always scopes with the
2255
+ * operation's `projectId`; a host that omits `cache.scope` therefore will not
2256
+ * match studio-emitted tags. Configure `cache.scope` with the same project id
2257
+ * the studio publishes under.
2258
+ *
2259
+ * The studio-side emitter cannot depend on this package (it would drag the
2260
+ * `next` peer dependency into the builder), so
2261
+ * `apps/builder/src/lib/webhooks/tags.ts` mirrors these formatters; tests on
2262
+ * both sides pin the exact literal strings to keep them in lock-step.
2263
+ */
2264
+ /** Apply the optional multi-project scope prefix to one tag. */
2265
+ declare function scopeCacheTag(tag: string, scope?: string): string;
2266
+ /**
2267
+ * Tag for one document identity. `docKey` is the EC-143 `documentKey` string
2268
+ * (`<collection>:<id>`), exactly as found in `CacheTags.docs`.
2269
+ */
2270
+ declare function docCacheTag(docKey: string, scope?: string): string;
2271
+ /** The coarse membership tag of one collection in one locale. */
2272
+ declare function collectionCacheTag(collection: string, locale: string, scope?: string): string;
2273
+ /** Tag for one resolved route path (or redirect-chain hop) in one locale. */
2274
+ declare function routeCacheTag(pattern: string, locale: string, scope?: string): string;
2275
+ /**
2276
+ * The whole-project sweep tag. Attached to every cached page entry; emitted
2277
+ * on graph publish/unpublish (and as the emitter's honest fallback when a
2278
+ * change's precise tag set is unknowable).
2279
+ */
2280
+ declare function projectSweepTag(scope?: string): string;
2281
+ /**
2282
+ * Map an accessor-level `CacheTags` (what one request actually read) onto the
2283
+ * full, deterministic (sorted, deduplicated) set of Next tag strings for the
2284
+ * cache entry — including the project sweep tag.
2285
+ */
2286
+ declare function nextCacheTags(tags: CacheTags, scope?: string): string[];
2287
+ /**
2288
+ * The slice of `unstable_cache` the discovery logic needs, injectable so the
2289
+ * logic is testable against a faithful in-memory model (and so the test model
2290
+ * can mirror the real semantics this design depends on — see below).
2291
+ */
2292
+ interface CacheWrapperOptions {
2293
+ tags: string[];
2294
+ /** `false` = never expire by time (tag-driven invalidation only). */
2295
+ revalidate: number | false;
2296
+ }
2297
+ type CacheWrapper = <T>(cb: () => Promise<T>, keyParts: string[], options: CacheWrapperOptions) => () => Promise<T>;
2298
+ /** What one cached request stores: the outcome plus its own discovered tags. */
2299
+ interface CachedEntry<T> {
2300
+ value: T;
2301
+ /** The Next tag strings discovered at fetch time by the run that wrote this entry. */
2302
+ tags: string[];
2303
+ }
2304
+ /**
2305
+ * Fetch-time tag discovery over `unstable_cache` — the heart of EC-146.
2306
+ *
2307
+ * The problem: `unstable_cache` fixes an entry's tags when the *wrapper* is
2308
+ * created (`options.tags` is validated and **copied** at construction time in
2309
+ * Next 15 — mutating the array during the callback does not propagate), but
2310
+ * the correct tags are only known *after* the loader resolved the page. So a
2311
+ * single wrapper can never both (a) run the loader and (b) store the result
2312
+ * under the tags that run discovered.
2313
+ *
2314
+ * The design ("resolve once, cache with discovered tags, serve cached
2315
+ * thereafter") uses two wrapper constructions per request around **one**
2316
+ * cache entry:
2317
+ *
2318
+ * 1. **Probe pass** — a wrapper keyed by `keyParts` whose callback runs the
2319
+ * loader, captures the result + its discovered tags into the closure, and
2320
+ * then throws an internal sentinel.
2321
+ * - Cache **hit**: the stored entry (written by a previous request with
2322
+ * its full discovered tags) is returned; the callback — and therefore
2323
+ * the loader and any backend access — never runs. This is the outage
2324
+ * story: fully cached routes keep serving when the backend is down.
2325
+ * - Cache **miss**: the loader runs exactly once; the sentinel throw
2326
+ * prevents `unstable_cache` from persisting the entry under the
2327
+ * incomplete baseline tags (thrown callbacks cache nothing).
2328
+ * 2. **Write pass** (miss only) — a second wrapper is constructed *now that
2329
+ * the tags are known*, with `options.tags` = the discovered set, same key.
2330
+ * Its callback just returns the already-loaded entry, so the backend is
2331
+ * not hit again; `unstable_cache` persists the entry under the full
2332
+ * fetch-time tag set. `revalidateTag` on any of those tags drops the
2333
+ * entry and the next request repeats the cycle with fresh data.
2334
+ *
2335
+ * The loader runs **once per revalidation cycle** (per key), never once per
2336
+ * request. Time-based revalidation is intentionally not exposed: a
2337
+ * stale-while-revalidate background refresh would re-run the probe callback
2338
+ * (whose bail aborts the refresh), so this design is tag-driven only —
2339
+ * `revalidate: false` — which is exactly the AD-5 model.
2340
+ *
2341
+ * Concurrency: wrappers and the captured closure are per-request, so there is
2342
+ * no cross-request mutation; two racing first requests both resolve and the
2343
+ * last write wins (deterministic content ⇒ identical entries).
2344
+ */
2345
+ declare function loadCachedEntry<T>(cache: CacheWrapper, keyParts: string[], baselineTags: readonly string[], load: () => Promise<CachedEntry<T>>): Promise<CachedEntry<T>>;
2346
+
2347
+ /**
2348
+ * Pure signature/payload logic of the revalidation webhook receiver (EC-146),
2349
+ * kept free of Next.js imports so it is unit-testable. `revalidate.ts` wires
2350
+ * it into a route handler with `revalidateTag`.
2351
+ *
2352
+ * Scheme: HMAC-SHA256 over the **raw request body** with a shared secret,
2353
+ * carried as `x-elytra-signature: sha256=<hex>`. The emitter (the studio's
2354
+ * webhook dispatcher) signs the exact JSON string it sends; the receiver
2355
+ * verifies over the raw text *before* parsing, with a timing-safe compare.
2356
+ */
2357
+ /** Header carrying the HMAC signature. */
2358
+ declare const REVALIDATE_SIGNATURE_HEADER = "x-elytra-signature";
2359
+ /** The webhook body: which tags to revalidate, and why. */
2360
+ declare const revalidatePayloadSchema: z.ZodObject<{
2361
+ projectId: z.ZodString;
2362
+ event: z.ZodString;
2363
+ tags: z.ZodArray<z.ZodString>;
2364
+ timestamp: z.ZodString;
2365
+ }, z.core.$strip>;
2366
+ type RevalidatePayload = z.infer<typeof revalidatePayloadSchema>;
2367
+ /** Compute the signature header value for a raw body (emitter/test side). */
2368
+ declare function signRevalidateBody(rawBody: string, secret: string): string;
2369
+ type RevalidateEvaluation = {
2370
+ ok: true;
2371
+ payload: RevalidatePayload;
2372
+ } | {
2373
+ ok: false;
2374
+ status: 400 | 401;
2375
+ message: string;
2376
+ };
2377
+ /**
2378
+ * Evaluate one webhook request: verify the HMAC first (auth before parsing),
2379
+ * then validate the payload shape.
2380
+ *
2381
+ * - unconfigured (empty) secret → 401 for every request: the endpoint stays
2382
+ * closed until a secret is deliberately configured (mirrors EC-144 preview);
2383
+ * - missing/malformed/mismatched signature → 401 (timing-safe compare);
2384
+ * - unparseable or schema-invalid body → 400 with no tag revalidated.
2385
+ */
2386
+ declare function evaluateRevalidateRequest(rawBody: string, signature: string | null | undefined, secret: string | undefined): RevalidateEvaluation;
2387
+
2388
+ /**
2389
+ * Revalidation webhook route handler (EC-146): the receiving end of the
2390
+ * publish → live-in-seconds pipeline. The studio's dispatcher POSTs a signed
2391
+ * `{projectId, event, tags, timestamp}` payload; this handler verifies the
2392
+ * HMAC over the raw body and calls `revalidateTag` for each tag, dropping
2393
+ * exactly the cached entries whose fetch-time tag sets contained them.
2394
+ *
2395
+ * Server-only by construction — consumed from an App Router route handler
2396
+ * file, never from client bundles.
2397
+ */
2398
+ interface RevalidateRouteOptions {
2399
+ /**
2400
+ * Shared HMAC secret. An empty/undefined secret rejects every request with
2401
+ * 401 — the endpoint stays closed until deliberately configured.
2402
+ */
2403
+ secret: string | undefined;
2404
+ /**
2405
+ * CORS origin allowed to call this endpoint (e.g. the studio's origin).
2406
+ * The v1 dispatcher is a browser `fetch` from the studio, and the custom
2407
+ * signature header always triggers a CORS preflight — so cross-origin
2408
+ * studio → host dispatch only works when this is set. Server-side emitters
2409
+ * (the Convex action variant, CLIs, CI) need no CORS and may leave it
2410
+ * unset. Never use `*` with a real secret-bearing deployment unless you
2411
+ * accept that any origin may *attempt* deliveries (they still need the
2412
+ * secret to have any effect).
2413
+ */
2414
+ allowOrigin?: string;
2415
+ }
2416
+ interface RevalidateRouteHandlers {
2417
+ POST(request: Request): Promise<Response>;
2418
+ OPTIONS(): Promise<Response>;
2419
+ }
2420
+ /**
2421
+ * Create the webhook route handlers for `app/api/revalidate/route.ts`:
2422
+ *
2423
+ * ```ts
2424
+ * export const { POST, OPTIONS } = createRevalidateRoute({
2425
+ * secret: process.env.REVALIDATE_SECRET,
2426
+ * })
2427
+ * ```
2428
+ *
2429
+ * Unsigned/invalid requests are rejected with 401 (timing-safe HMAC check,
2430
+ * see `evaluateRevalidateRequest`); valid requests revalidate every carried
2431
+ * tag and answer `{ revalidated: tags }`.
2432
+ */
2433
+ declare function createRevalidateRoute(options: RevalidateRouteOptions): RevalidateRouteHandlers;
2434
+
2435
+ /**
2436
+ * Image delivery for the embedded runtime (EC-152).
2437
+ *
2438
+ * ## The honest v1 design
2439
+ *
2440
+ * Stored asset bytes are served from plain blob-storage URLs (Convex file
2441
+ * storage serve URLs in production). Those URLs do **not** transform — there
2442
+ * is no `?w=`/`?q=` resizing service behind them, and no image CDN build-out
2443
+ * in v1. The standard, no-CDN path is therefore `next/image` with its
2444
+ * **default loader**: the host app's own Next.js optimizer fetches the remote
2445
+ * asset URL, resizes/re-encodes it, and serves the variants same-origin from
2446
+ * `/_next/image`. That is exactly what {@link ElytraImage} does.
2447
+ *
2448
+ * For that to work the host app must allowlist the asset origin in
2449
+ * `next.config.mjs`:
2450
+ *
2451
+ * ```js
2452
+ * const nextConfig = {
2453
+ * images: {
2454
+ * remotePatterns: [
2455
+ * // Convex file storage serve URLs:
2456
+ * { protocol: 'https', hostname: '*.convex.cloud' },
2457
+ * ],
2458
+ * },
2459
+ * }
2460
+ * ```
2461
+ *
2462
+ * {@link createAssetImageLoader} exists for the day the asset URLs sit behind
2463
+ * a CDN that *does* honor width/quality query params — it appends them and
2464
+ * nothing more. Pointing it at raw Convex serve URLs would silently serve
2465
+ * full-size bytes while pretending to resize, so it is **not** the default.
2466
+ */
2467
+ /**
2468
+ * The slice of the {@link ResolvedAsset} delivery shape an image needs:
2469
+ * the resolvable URL, intrinsic dimensions for layout stability, and alt
2470
+ * text. Structurally satisfied by every `ResolvedAsset`.
2471
+ */
2472
+ type ElytraImageAsset = Pick<ResolvedAsset, 'url' | 'width' | 'height' | 'alt' | 'focalPoint'>;
2473
+ interface ElytraImageProps {
2474
+ /** The resolved asset (delivery shape). `null` renders nothing. */
2475
+ asset: ElytraImageAsset | null | undefined;
2476
+ /** `sizes` hint forwarded to `next/image` for responsive variants. */
2477
+ sizes?: string;
2478
+ /** Preload hint forwarded to `next/image` (above-the-fold images). */
2479
+ priority?: boolean;
2480
+ /** Quality (1–100) forwarded to `next/image`; Next defaults to 75. */
2481
+ quality?: number;
2482
+ className?: string;
2483
+ /**
2484
+ * Custom `next/image` loader (e.g. from {@link createAssetImageLoader})
2485
+ * when assets sit behind a transforming CDN. Omit for the v1 default:
2486
+ * Next's own optimizer.
2487
+ */
2488
+ loader?: ImageLoader;
2489
+ }
2490
+ /**
2491
+ * Render a resolved asset through `next/image` (EC-152).
2492
+ *
2493
+ * - With known intrinsic dimensions the optimizer serves resized variants
2494
+ * and the reserved width/height prevent layout shift.
2495
+ * - Without dimensions `next/image` cannot render (it requires `width` +
2496
+ * `height` or `fill`), so the component degrades to a plain `<img>` —
2497
+ * un-optimized but visible, never a crash. Records written by the EC-151
2498
+ * upload flow always carry detected dimensions.
2499
+ * - A `null`/empty asset renders nothing (missing assets surface as
2500
+ * structured `unknown-asset` validation issues upstream, not here).
2501
+ */
2502
+ declare function ElytraImage(props: ElytraImageProps): ReactNode;
2503
+ interface AssetImageLoaderOptions {
2504
+ /** Query param name for the requested width. Default `"w"`. */
2505
+ widthParam?: string;
2506
+ /** Query param name for the requested quality. Default `"q"`. */
2507
+ qualityParam?: string;
2508
+ /** Quality used when `next/image` passes none. Default `75`. */
2509
+ defaultQuality?: number;
2510
+ }
2511
+ /**
2512
+ * A `next/image` loader for asset URLs served through a transforming CDN:
2513
+ * it appends width/quality query params to the asset URL and returns it.
2514
+ *
2515
+ * Be honest about what this does: it only *requests* a transform. Plain
2516
+ * Convex file-storage serve URLs ignore these params and return the original
2517
+ * bytes, so this loader is only correct once the asset origin actually
2518
+ * resizes (e.g. an image CDN in front of storage). Until then, use the
2519
+ * default `next/image` loader (see module docs) — that is the v1 path.
2520
+ */
2521
+ declare function createAssetImageLoader(options?: AssetImageLoaderOptions): ImageLoader;
2522
+ /**
2523
+ * Host registration that swaps the `base.primitives.Image` implementation
2524
+ * for {@link ElytraImage} while keeping the primitive's canonical manifest
2525
+ * (the first registration's manifest wins in `defineHostComponents` — by
2526
+ * design, so prop schemas stay canonical; the duplicate-id registry issue it
2527
+ * reports is the documented override signal, not an error):
2528
+ *
2529
+ * ```ts
2530
+ * export const hostComponents = defineHostComponents([
2531
+ * nextImagePrimitive(),
2532
+ * // ...project components
2533
+ * ])
2534
+ * ```
2535
+ */
2536
+ declare function nextImagePrimitive(): HostComponent;
2537
+
2538
+ /**
2539
+ * Pure token/target logic of the draft-preview routes (EC-144), kept free of
2540
+ * Next.js imports so it is unit-testable. `preview.ts` wires it into route
2541
+ * handlers with `draftMode()`.
2542
+ */
2543
+ type PreviewEvaluation = {
2544
+ ok: true;
2545
+ redirectTo: string;
2546
+ } | {
2547
+ ok: false;
2548
+ status: 400 | 401;
2549
+ message: string;
2550
+ };
2551
+ /**
2552
+ * Only same-site path targets are accepted (`/...` but not `//host`), so the
2553
+ * preview endpoint can never be used as an open redirect.
2554
+ */
2555
+ declare function safeRedirectTarget(value: string | null | undefined, fallback?: string): string | null;
2556
+ /**
2557
+ * Evaluate a preview-enable request. Drafts are token-gated (EC-144
2558
+ * acceptance: without the token, drafts are never reachable):
2559
+ *
2560
+ * - an unconfigured (empty) expected token rejects every request — preview
2561
+ * cannot be accidentally left open;
2562
+ * - a missing or mismatched `token` query param is a 401;
2563
+ * - an off-site `redirect` target is a 400.
2564
+ */
2565
+ declare function evaluatePreviewRequest(requestUrl: string | URL, expectedToken: string | undefined): PreviewEvaluation;
2566
+
2567
+ /**
2568
+ * Draft preview route handlers (EC-144): Next.js `draftMode()` integration.
2569
+ * Server-only by construction — these factories are consumed from App Router
2570
+ * route handler files, never from client bundles.
2571
+ */
2572
+ interface PreviewRouteOptions {
2573
+ /**
2574
+ * The shared secret gating draft preview. An empty/undefined token means
2575
+ * every request is rejected with 401 — drafts stay unreachable until a
2576
+ * token is configured.
2577
+ */
2578
+ token: string | undefined;
2579
+ }
2580
+ /** The shape an `app/api/.../route.ts` file re-exports. */
2581
+ interface PreviewRouteHandlers {
2582
+ GET(request: Request): Promise<Response>;
2583
+ }
2584
+ /**
2585
+ * Create the preview-enable route handler for
2586
+ * `app/api/preview/route.ts`:
2587
+ *
2588
+ * ```ts
2589
+ * export const { GET } = createPreviewRoute({ token: process.env.PREVIEW_TOKEN })
2590
+ * ```
2591
+ *
2592
+ * `GET /api/preview?token=...&redirect=/some/path` checks the token, enables
2593
+ * Next draft mode (the catch-all helper then renders the `draft`
2594
+ * perspective), and redirects to the target path. Wrong/missing token → 401.
2595
+ */
2596
+ declare function createPreviewRoute(options: PreviewRouteOptions): PreviewRouteHandlers;
2597
+ /**
2598
+ * Create the preview-disable route handler (e.g.
2599
+ * `app/api/preview/disable/route.ts`). Disabling needs no token — it only
2600
+ * ever reduces visibility back to the published perspective.
2601
+ */
2602
+ declare function createPreviewDisableRoute(): PreviewRouteHandlers;
2603
+
2604
+ /**
2605
+ * @elytracms/next — the embedded runtime (EC-144, vision AD-1): render
2606
+ * builder-managed pages inside the user's own Next.js App Router repo with
2607
+ * the user's own components. Drop `createCanvasRoute` into a catch-all
2608
+ * route, register components with `defineHostComponents`, done.
2609
+ *
2610
+ * Nothing here imports builder-only code (no studio, no operations, no
2611
+ * TanStack) — only the runtime packages: content accessors, the component
2612
+ * registry, and the runtime renderer.
2613
+ */
2614
+ declare const PACKAGE = "@elytracms/next";
2615
+
2616
+ export { type AssetImageLoaderOptions, type AssetRecord, type BindingPayloads, type CacheWrapper, type CacheWrapperOptions, type CachedEntry, type CanvasCacheOptions, type CanvasContentRequest, type CanvasDataSource, type CanvasPageOutcome, type CanvasPayloadContext, type CanvasPayloadsFactory, CanvasRenderer, type CanvasRendererProps, type CanvasRequestInfo, type CanvasRoute, type CanvasRouteOptions, type CanvasRouteProps, type CanvasSitemapOptions, type CanvasSitemapResult, type CmsDocument, type CollectionDef, type ComponentImplementations, type ComponentManifest, type ComponentNode, type ContentSnapshot, type ContentSnapshotOptions, type ContentSource, type CreateCanvasSitemapOptions, type DefineHostComponentsOptions, ElytraImage, type ElytraImageAsset, type ElytraImageProps, type FieldDef, type HostComponent, type HostComponents, type Locale, type LocaleConfig, type MaterializeSourcePayloadsInput, type MaterializedSourcePayloads, PACKAGE, PROJECT_GRAPH_SCHEMA_VERSION, type Perspective, type PreviewEvaluation, type PreviewRouteHandlers, type PreviewRouteOptions, type ProjectGraph, type PropField, REVALIDATE_SIGNATURE_HEADER, type RedirectRecord, type RenderAsset, type ResolveCanvasPageOptions, type RevalidateEvaluation, type RevalidatePayload, type RevalidateRouteHandlers, type RevalidateRouteOptions, type RouteRecord, type SitemapParamsProvider, type SitemapRouteContext, type SitemapSkippedRoute, type SlotSpec, type SourcePayloadError, type StaticContentData, canvasPageMetadata, canvasSitemapEntries, collectBindingSourceIds, collectionCacheTag, createAssetImageLoader, createBindingResolver, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defaultBindingPayloads, defineComponent, defineHostComponents, docCacheTag, documentSchema, evaluatePreviewRequest, evaluateRevalidateRequest, isSourcePayloadError, loadCachedEntry, localeConfigSchema, materializeSourcePayloads, mergeCacheTags, mergeRouteRecords, nextCacheTags, nextImagePrimitive, parseProjectGraph, pathFromSlug, projectSweepTag, redirectRecordSchema, resolveCanvasPage, resolvePayloadToken, revalidatePayloadSchema, routeCacheTag, routeRecordSchema, safeRedirectTarget, scopeCacheTag, signRevalidateBody, sourcePayloadError, splitLocalePath };