@elytracms/next 0.0.92 → 0.0.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
+ import { ResolvedAsset } from '@elytracms/core/cms-core';
2
3
 
3
4
  /**
4
5
  * `@elytracms/next/host-image` — the host image-delivery seam for DUAL-RENDERED
@@ -37,26 +38,21 @@ import { ReactNode } from 'react';
37
38
  * second singleton instance into the server entry, disconnecting
38
39
  * `setHostImage` from `getHostImage` — pinned by `package-build.test.ts`).
39
40
  */
40
- /** The resolved-asset slice a host image renderer needs (mirrors `ResolvedAsset`). */
41
- interface HostImageAsset {
42
- url: string;
43
- width: number | null;
44
- height: number | null;
45
- alt: string | null;
46
- focalPoint: {
47
- x: number;
48
- y: number;
49
- } | null;
50
- /**
51
- * LQIP blur-placeholder data URI (EC-420) → next/image blurDataURL. Optional so a
52
- * consumer mapping a `ResolvedAsset` in stays source-compatible; when present the
53
- * delivered image blurs up while the optimized bytes load.
54
- */
55
- lqip?: string | null;
56
- }
41
+ /**
42
+ * The resolved-asset slice a host image renderer needs — DERIVED from `ResolvedAsset`
43
+ * (EC-587; it used to mirror it by hand). `lqip` (EC-420 → next/image blurDataURL) is
44
+ * optional so a caller handing a partial asset stays source-compatible; when present
45
+ * the delivered image blurs up while the optimized bytes load.
46
+ */
47
+ type HostImageAsset = Pick<ResolvedAsset, 'url' | 'width' | 'height' | 'alt' | 'focalPoint'> & Partial<Pick<ResolvedAsset, 'lqip'>>;
57
48
  interface HostImageProps {
58
49
  asset: HostImageAsset;
59
- alt: string;
50
+ /**
51
+ * The image's alt text. Omitted → the ASSET's alt (EC-587: an asset's alt has one
52
+ * home and one reader). Pass it only to override deliberately — e.g. `''` for a
53
+ * decorative image.
54
+ */
55
+ alt?: string;
60
56
  className?: string;
61
57
  loading?: 'eager' | 'lazy';
62
58
  /** Above-the-fold opt-out of lazy-loading (the hero image). */
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/host-image.ts"],"names":[],"mappings":";AAoEA,IAAI,SAAA,GAAsC,IAAA;AAGnC,SAAS,aAAa,QAAA,EAA0C;AACrE,EAAA,SAAA,GAAY,QAAA;AACd;AAGO,SAAS,YAAA,GAAyC;AACvD,EAAA,OAAO,SAAA;AACT;AAiBO,IAAM,yBAAA,GAAgF;AAAA,EAC3F,EAAE,QAAA,EAAU,OAAA,EAAS,QAAA,EAAU,gBAAA;AACjC","file":"host-image.js","sourcesContent":["import type { ReactNode } from 'react'\n\n/**\n * `@elytracms/next/host-image` — the host image-delivery seam for DUAL-RENDERED\n * project components (EC-368, from the consumer-proven EC-271 pattern).\n *\n * The `project.*` page-builder components are dual-rendered: the SAME module\n * renders in the Vite studio canvas AND the Next host (AD-2/AD-11), so it may\n * not statically import `next/image` — that would break the Vite build. And in\n * the Next host these components render as Server Components, where React\n * context is unavailable, so an optimized `<Image>` can't be threaded through a\n * provider either.\n *\n * The seam that satisfies both: a tiny module singleton the host sets once at\n * boot. The studio never sets it (it renders the components via the root\n * `elytra.config`, which never imports the Next host), so the canvas keeps a\n * plain-`<img>` fallback; the Next host injects the `ElytraImage`-backed\n * renderer so content images go through `next/image`'s optimizer:\n *\n * ```ts\n * // frontend/lib/host.ts — imported once by the catch-all route at boot\n * import { elytraHostImage } from '@elytracms/next'\n * import { setHostImage } from '@elytracms/next/host-image'\n *\n * setHostImage(elytraHostImage())\n * ```\n *\n * WHEN (and whether) to register stays consumer-owned — legacy-interop windows\n * are real: a host whose global `next/image` config is still owned by a\n * previous stack (e.g. a Sanity-era custom loader) deliberately skips\n * registration until the conflict clears, and its components keep the honest\n * plain-`<img>` fallback.\n *\n * This module is deliberately its OWN subpath entry importing nothing but React\n * types — safe in Vite, in RSC, and in `next.config.*`. The main\n * `@elytracms/next` entry never imports it at runtime (that would bundle a\n * second singleton instance into the server entry, disconnecting\n * `setHostImage` from `getHostImage` — pinned by `package-build.test.ts`).\n */\n\n/** The resolved-asset slice a host image renderer needs (mirrors `ResolvedAsset`). */\nexport interface HostImageAsset {\n url: string\n width: number | null\n height: number | null\n alt: string | null\n focalPoint: { x: number; y: number } | null\n /**\n * LQIP blur-placeholder data URI (EC-420) → next/image blurDataURL. Optional so a\n * consumer mapping a `ResolvedAsset` in stays source-compatible; when present the\n * delivered image blurs up while the optimized bytes load.\n */\n lqip?: string | null\n}\n\nexport interface HostImageProps {\n asset: HostImageAsset\n alt: string\n className?: string\n loading?: 'eager' | 'lazy'\n /** Above-the-fold opt-out of lazy-loading (the hero image). */\n priority?: boolean\n /** Responsive `sizes` hint so the optimizer serves a column-sized variant. */\n sizes?: string\n}\n\nexport type HostImageRenderer = (props: HostImageProps) => ReactNode\n\nlet hostImage: HostImageRenderer | null = null\n\n/** Host-only: register the optimized image renderer (called once at host boot). */\nexport function setHostImage(renderer: HostImageRenderer | null): void {\n hostImage = renderer\n}\n\n/** The registered host renderer, or `null` in the studio canvas (plain-`<img>` fallback). */\nexport function getHostImage(): HostImageRenderer | null {\n return hostImage\n}\n\n/**\n * The `next.config.*` `images.remotePatterns` preset for Elytra asset delivery\n * (EC-368 image defaults): Convex file-storage serve URLs, where uploaded\n * assets live in connected mode. Without it `next/image` refuses the remote\n * origin and delivery images 400 — spread it into your config:\n *\n * ```js\n * // next.config.mjs\n * import { convexAssetRemotePatterns } from '@elytracms/next/host-image'\n *\n * const nextConfig = {\n * images: { remotePatterns: [...convexAssetRemotePatterns] },\n * }\n * ```\n */\nexport const convexAssetRemotePatterns: readonly { protocol: 'https'; hostname: string }[] = [\n { protocol: 'https', hostname: '*.convex.cloud' },\n]\n"]}
1
+ {"version":3,"sources":["../src/host-image.ts"],"names":[],"mappings":";AAoEA,IAAI,SAAA,GAAsC,IAAA;AAGnC,SAAS,aAAa,QAAA,EAA0C;AACrE,EAAA,SAAA,GAAY,QAAA;AACd;AAGO,SAAS,YAAA,GAAyC;AACvD,EAAA,OAAO,SAAA;AACT;AAiBO,IAAM,yBAAA,GAAgF;AAAA,EAC3F,EAAE,QAAA,EAAU,OAAA,EAAS,QAAA,EAAU,gBAAA;AACjC","file":"host-image.js","sourcesContent":["import type { ReactNode } from 'react'\nimport type { ResolvedAsset } from '@elytracms/core/cms-core'\n\n/**\n * `@elytracms/next/host-image` — the host image-delivery seam for DUAL-RENDERED\n * project components (EC-368, from the consumer-proven EC-271 pattern).\n *\n * The `project.*` page-builder components are dual-rendered: the SAME module\n * renders in the Vite studio canvas AND the Next host (AD-2/AD-11), so it may\n * not statically import `next/image` — that would break the Vite build. And in\n * the Next host these components render as Server Components, where React\n * context is unavailable, so an optimized `<Image>` can't be threaded through a\n * provider either.\n *\n * The seam that satisfies both: a tiny module singleton the host sets once at\n * boot. The studio never sets it (it renders the components via the root\n * `elytra.config`, which never imports the Next host), so the canvas keeps a\n * plain-`<img>` fallback; the Next host injects the `ElytraImage`-backed\n * renderer so content images go through `next/image`'s optimizer:\n *\n * ```ts\n * // frontend/lib/host.ts — imported once by the catch-all route at boot\n * import { elytraHostImage } from '@elytracms/next'\n * import { setHostImage } from '@elytracms/next/host-image'\n *\n * setHostImage(elytraHostImage())\n * ```\n *\n * WHEN (and whether) to register stays consumer-owned — legacy-interop windows\n * are real: a host whose global `next/image` config is still owned by a\n * previous stack (e.g. a Sanity-era custom loader) deliberately skips\n * registration until the conflict clears, and its components keep the honest\n * plain-`<img>` fallback.\n *\n * This module is deliberately its OWN subpath entry importing nothing but React\n * types — safe in Vite, in RSC, and in `next.config.*`. The main\n * `@elytracms/next` entry never imports it at runtime (that would bundle a\n * second singleton instance into the server entry, disconnecting\n * `setHostImage` from `getHostImage` — pinned by `package-build.test.ts`).\n */\n\n/**\n * The resolved-asset slice a host image renderer needs — DERIVED from `ResolvedAsset`\n * (EC-587; it used to mirror it by hand). `lqip` (EC-420 → next/image blurDataURL) is\n * optional so a caller handing a partial asset stays source-compatible; when present\n * the delivered image blurs up while the optimized bytes load.\n */\nexport type HostImageAsset = Pick<ResolvedAsset, 'url' | 'width' | 'height' | 'alt' | 'focalPoint'> &\n Partial<Pick<ResolvedAsset, 'lqip'>>\n\nexport interface HostImageProps {\n asset: HostImageAsset\n /**\n * The image's alt text. Omitted → the ASSET's alt (EC-587: an asset's alt has one\n * home and one reader). Pass it only to override deliberately — e.g. `''` for a\n * decorative image.\n */\n alt?: string\n className?: string\n loading?: 'eager' | 'lazy'\n /** Above-the-fold opt-out of lazy-loading (the hero image). */\n priority?: boolean\n /** Responsive `sizes` hint so the optimizer serves a column-sized variant. */\n sizes?: string\n}\n\nexport type HostImageRenderer = (props: HostImageProps) => ReactNode\n\nlet hostImage: HostImageRenderer | null = null\n\n/** Host-only: register the optimized image renderer (called once at host boot). */\nexport function setHostImage(renderer: HostImageRenderer | null): void {\n hostImage = renderer\n}\n\n/** The registered host renderer, or `null` in the studio canvas (plain-`<img>` fallback). */\nexport function getHostImage(): HostImageRenderer | null {\n return hostImage\n}\n\n/**\n * The `next.config.*` `images.remotePatterns` preset for Elytra asset delivery\n * (EC-368 image defaults): Convex file-storage serve URLs, where uploaded\n * assets live in connected mode. Without it `next/image` refuses the remote\n * origin and delivery images 400 — spread it into your config:\n *\n * ```js\n * // next.config.mjs\n * import { convexAssetRemotePatterns } from '@elytracms/next/host-image'\n *\n * const nextConfig = {\n * images: { remotePatterns: [...convexAssetRemotePatterns] },\n * }\n * ```\n */\nexport const convexAssetRemotePatterns: readonly { protocol: 'https'; hostname: string }[] = [\n { protocol: 'https', hostname: '*.convex.cloud' },\n]\n"]}
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ import { Clock } from '@elytracms/core/determinism';
13
13
  import { ResolvingMetadata, Metadata, ResolvedMetadata, MetadataRoute } from 'next';
14
14
  import { RevalidationEvent } from '@elytracms/core/revalidation';
15
15
  export { REVALIDATE_SIGNATURE_HEADER, RevalidationEvent, RevalidationEventKind, revalidationEventSchema, signRevalidationBody, signedRevalidationRequest } from '@elytracms/core/revalidation';
16
+ import { IngestPayload } from '@elytracms/core/ingest';
16
17
  import { ImageLoader } from 'next/image';
17
18
  import { HostImageRenderer } from './host-image.js';
18
19
  export { z } from 'zod';
@@ -310,6 +311,12 @@ interface StaticContentData {
310
311
  * explicit unresolved-placeholder scheme (never a silent empty string).
311
312
  */
312
313
  assetUrl?: (asset: AssetRecord) => string;
314
+ /**
315
+ * EC-585 — the stored rows the host could not read (`parseDeliveredDocuments`), handed
316
+ * to every client of this source as its `sourceIssues` (and in the results they
317
+ * concern). A malformed row is not delivered, and never silently.
318
+ */
319
+ sourceIssues?: readonly CmsValidationIssue[];
313
320
  }
314
321
  /**
315
322
  * Assemble a `ContentSource` from plain prefetched data — the pure half of
@@ -372,6 +379,25 @@ interface CanvasWrapContext extends CanvasRequestInfo {
372
379
  */
373
380
  result: ResolvePageResult;
374
381
  }
382
+ /**
383
+ * EC-618 — what the route's `analytics` slot is told about the page it renders on: the
384
+ * resolved document (the content model, not the URL — slug changes and locale paths then
385
+ * attribute correctly), the locale it rendered in, the request path and the status. Plain
386
+ * serializable values, so the slot may be a client component.
387
+ */
388
+ interface CanvasBeaconProps {
389
+ /** The request path as served (including any locale prefix). */
390
+ path: string;
391
+ /** 200: the route renders the slot only on a rendered page (a 404 never reaches it). */
392
+ status: 200;
393
+ /** The locale the page rendered in. */
394
+ locale: string;
395
+ /** The resolved document's collection and id — absent when the URL resolved to no document. */
396
+ collection?: string;
397
+ documentId?: string;
398
+ }
399
+ /** PURE core (EC-618): the slot's props from a render outcome's resolution and the request info. */
400
+ declare function canvasBeaconProps(result: ResolvePageResult, info: CanvasRequestInfo): CanvasBeaconProps;
375
401
  type CanvasPageOutcome = {
376
402
  kind: 'redirect';
377
403
  target: string;
@@ -723,6 +749,21 @@ interface CanvasRouteOptions {
723
749
  * the default.
724
750
  */
725
751
  wrap?: (children: ReactNode, ctx: CanvasWrapContext) => ReactNode;
752
+ /**
753
+ * The ANALYTICS slot (EC-618): a component the route renders inside every `render`
754
+ * outcome, AFTER the page (and outside `wrap`, so a wrapper cannot drop it), with the
755
+ * resolved document, the locale, the path and the status ({@link CanvasBeaconProps}).
756
+ * Published perspective only — a draft preview is an editor at work, not a visit.
757
+ *
758
+ * The engine supplies the slot and nothing else: its value is the beacon of the
759
+ * analytics addon, `@elytracms/analytics/next` — **unpublished, pending counsel's
760
+ * verdict on its consent-free claim** (EC-618) — or a host's own component. Absent, the
761
+ * route renders exactly what it rendered before, and sends nothing.
762
+ *
763
+ * A not-found page never reaches this slot (`notFound()` renders the host's
764
+ * `not-found.tsx`), so a 404 beacon lives in that file.
765
+ */
766
+ analytics?: ComponentType<CanvasBeaconProps>;
726
767
  }
727
768
  interface CanvasRouteProps {
728
769
  params: Promise<{
@@ -1052,6 +1093,177 @@ interface RevalidateRouteHandlers {
1052
1093
  */
1053
1094
  declare function createRevalidateRoute(options: RevalidateRouteOptions): RevalidateRouteHandlers;
1054
1095
 
1096
+ /**
1097
+ * EC-617 — the pure decision core of the PUBLIC INGEST route (`createIngestRoute`, the
1098
+ * Next wrapper in `ingest.ts`): no Next imports, no network, an injected clock — the
1099
+ * house "pure core + thin wrapper" shape of `evaluateRevalidateRequest` /
1100
+ * `createRevalidateRoute`.
1101
+ *
1102
+ * The route is a site's one public write path (ruling EC-565 § 6c). It runs, in order:
1103
+ *
1104
+ * 1. **method** — `POST` only (405);
1105
+ * 2. **configured** — the server-held `INGEST_SECRET` and the receiver URL are set (503
1106
+ * otherwise: the route stays closed until deliberately configured, never a no-op);
1107
+ * 3. **same-origin** — a browser request from another origin (`Origin` / `Sec-Fetch-Site`)
1108
+ * is refused (403). The route emits no CORS headers and reads no cookie;
1109
+ * 4. **rate limit** — a fixed window per CLIENT KEY (429, `Retry-After`);
1110
+ * 5. **size** — the raw body capped at `maxBodyBytes` (413), checked before parsing;
1111
+ * 6. **shape** — JSON in the ingest envelope (400, with the issues);
1112
+ * 7. **bot gate** — the host's own check, an injected seam (403, `{ gate: 'bot' }`): the
1113
+ * engine supplies the slot, never the vendor;
1114
+ * 8. **enrichment** (EC-618) — an optional seam an ADDON fills (the analytics addon derives
1115
+ * a country and a visitor's sketch coordinates here, where the request is): it returns the
1116
+ * payload to forward, which is re-validated against the envelope and the receiver's cap
1117
+ * (500 — the site's own fault — when it throws or returns something invalid). Absent, the
1118
+ * route is exactly what it was;
1119
+ *
1120
+ * then the wrapper signs the payload with the server-held secret and forwards it to the
1121
+ * one Convex receiver, relaying its answer. Only the PAYLOAD is forwarded — never a request
1122
+ * header — so whatever an enrichment reads from the request (the client address, the user
1123
+ * agent, a geo header) stays in this request's memory; only what it derives travels.
1124
+ */
1125
+ /** The answer the core gives the wrapper. */
1126
+ type IngestEvaluation = {
1127
+ ok: true;
1128
+ payload: IngestPayload;
1129
+ } | {
1130
+ ok: false;
1131
+ status: 400 | 403 | 405 | 413 | 429 | 500 | 503;
1132
+ body: Record<string, unknown>;
1133
+ headers?: Record<string, string>;
1134
+ };
1135
+ /** What the host's bot gate answers (a Turnstile / hCaptcha / honeypot check the HOST implements). */
1136
+ type BotGateVerdict = {
1137
+ allow: true;
1138
+ } | {
1139
+ allow: false;
1140
+ reason: string;
1141
+ };
1142
+ /**
1143
+ * The bot-gate seam: called with the request and the parsed payload, AFTER the cheap
1144
+ * checks (so a flood never reaches a paid verification API). Absent → no gate.
1145
+ */
1146
+ type IngestBotGate = (input: {
1147
+ request: Request;
1148
+ payload: IngestPayload;
1149
+ }) => BotGateVerdict | Promise<BotGateVerdict>;
1150
+ /**
1151
+ * EC-618 — the enrichment seam: called with the request and the parsed payload LAST (after
1152
+ * the bot gate), it returns the payload the route signs and forwards. The engine supplies
1153
+ * the slot, never the code: an addon fills it (`@elytracms/analytics/next`'s
1154
+ * `analyticsEnrichment`), and a host may compose its own. Whatever it reads from the
1155
+ * request is held for this request only — the route forwards the returned payload and
1156
+ * nothing else. Absent → no enrichment, the payload forwarded as parsed.
1157
+ */
1158
+ type IngestEnrich = (input: {
1159
+ request: Request;
1160
+ payload: IngestPayload;
1161
+ }) => IngestPayload | Promise<IngestPayload>;
1162
+ /**
1163
+ * The fixed-window limiter — the AI route's pattern (EC-602), keyed per client. Holds,
1164
+ * in memory only, one `{ windowStart, count }` per key seen in the current window; expired
1165
+ * windows are dropped as the clock moves, so the map is bounded by the clients of one
1166
+ * window. Each server instance holds its own windows (a serverless platform runs several),
1167
+ * so the limit is per instance — the same documented limitation as the AI route.
1168
+ */
1169
+ interface FixedWindowLimiter {
1170
+ /** Count one request for `key`; `retryAfterSeconds` when it is over the limit. */
1171
+ hit(key: string): {
1172
+ allowed: true;
1173
+ } | {
1174
+ allowed: false;
1175
+ retryAfterSeconds: number;
1176
+ };
1177
+ /** How many keys the limiter currently holds (for the bound test). */
1178
+ size(): number;
1179
+ }
1180
+ declare function createFixedWindowLimiter(options: {
1181
+ limit: number;
1182
+ windowMs?: number;
1183
+ now?: () => number;
1184
+ }): FixedWindowLimiter;
1185
+ /**
1186
+ * The DEFAULT client key: the first hop of `x-forwarded-for` (what Vercel and most proxies
1187
+ * set), else `x-real-ip`, else one shared `unknown` bucket. An IP address is personal
1188
+ * data: it is held ONLY as a key of the in-memory window above, for at most one window,
1189
+ * and is never stored, logged or forwarded to the receiver. A host that wants a different
1190
+ * key (a hashed one, a session id) passes `clientKey`.
1191
+ */
1192
+ declare function defaultIngestClientKey(request: Request): string;
1193
+ /** Is this a cross-origin BROWSER request? (A request with no browser headers is not judged here.) */
1194
+ declare function isCrossOrigin(request: Request): boolean;
1195
+ interface IngestEvaluationInput {
1196
+ request: Request;
1197
+ /** The raw body, read by the wrapper up to `maxBodyBytes + 1` bytes (`undefined` when over). */
1198
+ rawBody: string | undefined;
1199
+ configured: boolean;
1200
+ limiter: FixedWindowLimiter;
1201
+ clientKey: (request: Request) => string;
1202
+ botGate?: IngestBotGate;
1203
+ enrich?: IngestEnrich;
1204
+ }
1205
+ /** Evaluate one request (see the module header for the order). */
1206
+ declare function evaluateIngestRequest(input: IngestEvaluationInput): Promise<IngestEvaluation>;
1207
+ /**
1208
+ * Read a request body as text, at most `maxBytes` (counted in UTF-8 bytes). `undefined`
1209
+ * when it is longer — the over-size body is never buffered whole. A declared
1210
+ * `Content-Length` over the cap is refused before a byte is read.
1211
+ */
1212
+ declare function readCappedBody(request: Request, maxBytes?: number): Promise<string | undefined>;
1213
+
1214
+ /**
1215
+ * EC-617 — the PUBLIC INGEST route: a site's one write path into its project (form
1216
+ * submissions, analytics events, upload URLs), mounted SAME-ORIGIN in the site's own
1217
+ * App Router:
1218
+ *
1219
+ * ```ts
1220
+ * // frontend/app/api/ingest/route.ts — through the delivery kit
1221
+ * export const { POST } = elytra.ingestRoute()
1222
+ * ```
1223
+ *
1224
+ * The decisions are the pure core's (`evaluateIngestRequest`: method, configured,
1225
+ * same-origin, rate limit, size, shape, the host's bot gate); this wrapper then SIGNS the
1226
+ * payload with the server-held `INGEST_SECRET` (it never reaches a browser) and forwards it
1227
+ * to the deployment's ONE receiver (`<deployment>.convex.site/elytra/ingest`), relaying the
1228
+ * receiver's answer. A receiver that rejects the signature is a SITE misconfiguration
1229
+ * (the two secrets differ), answered 502 with that sentence — never a 401 a visitor could
1230
+ * read as their fault.
1231
+ *
1232
+ * Uploads leave same-origin by design: an `upload` payload asks the receiver for ONE
1233
+ * short-lived storage URL (the request rides this route's gate, limiter and bot gate), and
1234
+ * the browser then sends the bytes straight to that URL; the submission that follows claims
1235
+ * the stored file, which is checked against what the upload request declared.
1236
+ */
1237
+ interface IngestRouteOptions {
1238
+ /** The server-held HMAC secret (= the deployment's `ELYTRA_INGEST_SECRET`). Unset → 503. */
1239
+ secret: string | undefined;
1240
+ /** The deployment's HTTP origin (`https://<name>.convex.site`); the receiver path is appended. Unset → 503. */
1241
+ siteUrl: string | undefined;
1242
+ /** Cap on the raw JSON body, ≤ the receiver's `INGEST_MAX_BODY_BYTES` (default). */
1243
+ maxBodyBytes?: number;
1244
+ /** Requests per client key per minute (default 30). */
1245
+ maxRequestsPerMinute?: number;
1246
+ /** The rate limiter's key for a request (default: the client IP, in memory only — see `defaultIngestClientKey`). */
1247
+ clientKey?: (request: Request) => string;
1248
+ /** The host's bot check (Turnstile, hCaptcha, a honeypot …). The engine supplies the slot, never the vendor. */
1249
+ botGate?: IngestBotGate;
1250
+ /**
1251
+ * EC-618 — an addon's enrichment, run last: it returns the payload to sign and forward
1252
+ * (the analytics addon adds a country and the visitor's sketch coordinates, derived from
1253
+ * the request here and never forwarded raw). Absent → the payload is forwarded as parsed.
1254
+ */
1255
+ enrich?: IngestEnrich;
1256
+ /** Injected clock (tests). */
1257
+ now?: () => number;
1258
+ /** Injected transport (tests). */
1259
+ fetchImpl?: typeof fetch;
1260
+ }
1261
+ interface IngestRouteHandlers {
1262
+ POST(request: Request): Promise<Response>;
1263
+ }
1264
+ /** Create the ingest route handler (see the module header). */
1265
+ declare function createIngestRoute(options: IngestRouteOptions): IngestRouteHandlers;
1266
+
1055
1267
  /**
1056
1268
  * Image delivery for the embedded runtime (EC-152).
1057
1269
  *
@@ -1247,6 +1459,8 @@ declare function elytraHostImage(options?: ElytraHostImageOptions): HostImageRen
1247
1459
  * | | equal the Convex deployment's own env var) |
1248
1460
  * | `REVALIDATE_SECRET` | Instant-publish webhook HMAC secret (must equal the |
1249
1461
  * | | deployment's `CANVAS_REVALIDATE_SECRET`) |
1462
+ * | `INGEST_SECRET` | Public-ingest HMAC secret (EC-617; must equal the |
1463
+ * | | deployment's `ELYTRA_INGEST_SECRET`) |
1250
1464
  *
1251
1465
  * EC-579 retired `STUDIO_ORIGIN` (the CORS origin of the studio's deleted browser
1252
1466
  * dispatcher): the one emitter is the Convex deployment, server to server.
@@ -1257,12 +1471,14 @@ declare const ELYTRA_HOST_ENV_KEYS: {
1257
1471
  readonly projectId: "ELYTRA_PROJECT_ID";
1258
1472
  readonly draftToken: "DRAFT_PREVIEW_TOKEN";
1259
1473
  readonly revalidateSecret: "REVALIDATE_SECRET";
1474
+ readonly ingestSecret: "INGEST_SECRET";
1260
1475
  };
1261
1476
  interface ElytraHostEnv {
1262
1477
  convexUrl?: string;
1263
1478
  projectId?: string;
1264
1479
  draftToken?: string;
1265
1480
  revalidateSecret?: string;
1481
+ ingestSecret?: string;
1266
1482
  }
1267
1483
  /**
1268
1484
  * Resolve the kit's env bag from a raw env record (`process.env` by default at
@@ -1579,6 +1795,12 @@ interface HostCanvasRouteOptions {
1579
1795
  * invalidation — a disagreement would drop nothing, silently.
1580
1796
  */
1581
1797
  published?: CanvasRouteOptions['published'];
1798
+ /**
1799
+ * EC-618 — the analytics slot (see `CanvasRouteOptions.analytics`): its value is an
1800
+ * addon's beacon (`@elytracms/analytics/next`, unpublished pending counsel). Absent →
1801
+ * nothing renders and nothing is sent.
1802
+ */
1803
+ analytics?: CanvasRouteOptions['analytics'];
1582
1804
  }
1583
1805
  interface LoadGlobalsOptions {
1584
1806
  /** Perspective of the chrome read. Default `'published'` (live is published-only). */
@@ -1699,6 +1921,15 @@ interface ElytraHost<Collections extends readonly CollectionDef[] = readonly Col
1699
1921
  previewRoute(): PreviewRouteHandlers;
1700
1922
  /** The draft-mode DISABLE route (no token — it only reduces visibility). */
1701
1923
  previewDisableRoute(): PreviewRouteHandlers;
1924
+ /**
1925
+ * EC-617 — the PUBLIC INGEST route for `app/api/ingest/route.ts`: the site's one write path
1926
+ * (form submissions, analytics events, upload URLs), signed with `INGEST_SECRET` and
1927
+ * forwarded to this deployment's receiver (its HTTP origin derived from
1928
+ * `ELYTRA_CONVEX_URL`; pass `siteUrl` for a self-hosted backend). Unset secret or no
1929
+ * backend = 503, closed. The rate limit, the client key and the bot gate are the host's to
1930
+ * tune — the engine supplies the slots, never a vendor.
1931
+ */
1932
+ ingestRoute(options?: Partial<Omit<IngestRouteOptions, 'secret'>>): IngestRouteHandlers;
1702
1933
  /**
1703
1934
  * Does the page for this catch-all `slug` OPEN full-bleed? Manifest-driven
1704
1935
  * (`presentation.fullBleed`, EC-368) and draft-mode aware — the host lays out
@@ -1864,4 +2095,4 @@ declare function evaluatePreviewRequest(requestUrl: string | URL, expectedToken:
1864
2095
  */
1865
2096
  declare const PACKAGE = "@elytracms/next";
1866
2097
 
1867
- export { type AssetImageLoaderOptions, type BakedListing, CANVAS_VALIDATION_SLUG, type CanvasCacheKey, type CanvasContentRequest, type CanvasPageOutcome, CanvasRenderer, type CanvasRendererProps, type CanvasRequestInfo, type CanvasRoute, type CanvasRouteOptions, type CanvasRouteProps, type CanvasSitemapOptions, type CanvasSitemapResult, type CanvasWrapContext, type ContentSnapshot, type ContentSnapshotOptions, type ContentSource, type CreateCanvasSitemapOptions, type DeliveryPresentation, ELYTRA_HOST_ENV_KEYS, type ElytraContentMode, type ElytraHost, type ElytraHostEnv, type ElytraHostImageOptions, type ElytraHostOptions, ElytraImage, type ElytraImageAsset, type ElytraImageProps, type GlobalsConventions, type HostCanvasRouteOptions, type HostComponent, type HostComponents, type LiveContentOptions, type LoadGlobalsOptions, type NextImageOptions, PACKAGE, type PreviewEvaluation, type PreviewRouteHandlers, type PreviewRouteOptions, type ResolveCanvasPageOptions, type ResolveGlobalsOptions, type ResolvedLink, type ResolvedNavEntry, type ResolvedNavLink, type RevalidateEvaluation, type RevalidateRouteHandlers, type RevalidateRouteOptions, type RevalidationProject, type SeoMetadataContext, type SiteGlobals, type SitemapSkippedRoute, type StaticContentData, assembleLiveSource, bodyItemsOf, canvasSitemapEntries, collectionCacheTag, createAssetImageLoader, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createElytraHost, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defineHostComponents, docCacheTag, elytraHostImage, evaluatePreviewRequest, evaluateRevalidateRequest, firstBlockFullBleed, loadLiveContent, mergeCacheTags, mergeMetadataOverParent, nextCacheTags, openGraphLocale, pathFromSlug, projectSweepTag, requestPerspective, resolveCanvasPage, resolveGlobals, resolveHostEnv, revalidationTags, routeCacheTag, safeRedirectTarget, scopeCacheTag, seoContextOf, seoMetadata, seoObjectOf, withNextImage };
2098
+ export { type AssetImageLoaderOptions, type BakedListing, type BotGateVerdict, CANVAS_VALIDATION_SLUG, type CanvasBeaconProps, type CanvasCacheKey, type CanvasContentRequest, type CanvasPageOutcome, CanvasRenderer, type CanvasRendererProps, type CanvasRequestInfo, type CanvasRoute, type CanvasRouteOptions, type CanvasRouteProps, type CanvasSitemapOptions, type CanvasSitemapResult, type CanvasWrapContext, type ContentSnapshot, type ContentSnapshotOptions, type ContentSource, type CreateCanvasSitemapOptions, type DeliveryPresentation, ELYTRA_HOST_ENV_KEYS, type ElytraContentMode, type ElytraHost, type ElytraHostEnv, type ElytraHostImageOptions, type ElytraHostOptions, ElytraImage, type ElytraImageAsset, type ElytraImageProps, type FixedWindowLimiter, type GlobalsConventions, type HostCanvasRouteOptions, type HostComponent, type HostComponents, type IngestBotGate, type IngestEnrich, type IngestEvaluation, type IngestRouteHandlers, type IngestRouteOptions, type LiveContentOptions, type LoadGlobalsOptions, type NextImageOptions, PACKAGE, type PreviewEvaluation, type PreviewRouteHandlers, type PreviewRouteOptions, type ResolveCanvasPageOptions, type ResolveGlobalsOptions, type ResolvedLink, type ResolvedNavEntry, type ResolvedNavLink, type RevalidateEvaluation, type RevalidateRouteHandlers, type RevalidateRouteOptions, type RevalidationProject, type SeoMetadataContext, type SiteGlobals, type SitemapSkippedRoute, type StaticContentData, assembleLiveSource, bodyItemsOf, canvasBeaconProps, canvasSitemapEntries, collectionCacheTag, createAssetImageLoader, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createElytraHost, createFixedWindowLimiter, createIngestRoute, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defaultIngestClientKey, defineHostComponents, docCacheTag, elytraHostImage, evaluateIngestRequest, evaluatePreviewRequest, evaluateRevalidateRequest, firstBlockFullBleed, isCrossOrigin, loadLiveContent, mergeCacheTags, mergeMetadataOverParent, nextCacheTags, openGraphLocale, pathFromSlug, projectSweepTag, readCappedBody, requestPerspective, resolveCanvasPage, resolveGlobals, resolveHostEnv, revalidationTags, routeCacheTag, safeRedirectTarget, scopeCacheTag, seoContextOf, seoMetadata, seoObjectOf, withNextImage };