@elytracms/next 0.0.93 → 0.0.95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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';
@@ -226,8 +227,13 @@ interface ContentSource {
226
227
  * backend outage. Optional so existing custom sources stay valid; without it,
227
228
  * asset-typed props fall back to their raw value (the renderer's visible
228
229
  * degradation), never a crash.
230
+ *
231
+ * EC-632: resolved in `locale` (absent → the source's default locale) — a localized
232
+ * asset-collection field (alt, …) carries its value in that locale, with the default
233
+ * locale as its fallback. The route helper passes the request's locale, so a page bakes
234
+ * its assets in the locale it renders in.
229
235
  */
230
- assets?(): readonly ResolvedAsset[];
236
+ assets?(locale?: Locale): readonly ResolvedAsset[];
231
237
  }
232
238
  /**
233
239
  * Merge accessor cache tags into one deterministic set (sorted, deduplicated)
@@ -306,10 +312,14 @@ interface StaticContentData {
306
312
  documents?: readonly ContentDocumentSource[];
307
313
  assets?: readonly AssetRecord[];
308
314
  /**
309
- * Resolve a serve URL for assets without a stored `url`. Defaults to an
310
- * explicit unresolved-placeholder scheme (never a silent empty string).
315
+ * Resolve a serve URL for assets without a stored `url` (`undefined` when there is
316
+ * none). EC-633: no default — an asset with no URL is no resolved asset (the ONE
317
+ * constructor, `resolvedAsset`, decides it): it renders the canvas's empty state, its
318
+ * field resolves to `null` with an `asset-without-url` issue, and this source names it
319
+ * on `sourceIssues`. The placeholder this used to default to (`asset://unresolved/…`)
320
+ * rendered a broken image on the site.
311
321
  */
312
- assetUrl?: (asset: AssetRecord) => string;
322
+ assetUrl?: (asset: AssetRecord) => string | undefined;
313
323
  /**
314
324
  * EC-585 — the stored rows the host could not read (`parseDeliveredDocuments`), handed
315
325
  * to every client of this source as its `sourceIssues` (and in the results they
@@ -378,6 +388,25 @@ interface CanvasWrapContext extends CanvasRequestInfo {
378
388
  */
379
389
  result: ResolvePageResult;
380
390
  }
391
+ /**
392
+ * EC-618 — what the route's `analytics` slot is told about the page it renders on: the
393
+ * resolved document (the content model, not the URL — slug changes and locale paths then
394
+ * attribute correctly), the locale it rendered in, the request path and the status. Plain
395
+ * serializable values, so the slot may be a client component.
396
+ */
397
+ interface CanvasBeaconProps {
398
+ /** The request path as served (including any locale prefix). */
399
+ path: string;
400
+ /** 200: the route renders the slot only on a rendered page (a 404 never reaches it). */
401
+ status: 200;
402
+ /** The locale the page rendered in. */
403
+ locale: string;
404
+ /** The resolved document's collection and id — absent when the URL resolved to no document. */
405
+ collection?: string;
406
+ documentId?: string;
407
+ }
408
+ /** PURE core (EC-618): the slot's props from a render outcome's resolution and the request info. */
409
+ declare function canvasBeaconProps(result: ResolvePageResult, info: CanvasRequestInfo): CanvasBeaconProps;
381
410
  type CanvasPageOutcome = {
382
411
  kind: 'redirect';
383
412
  target: string;
@@ -729,6 +758,21 @@ interface CanvasRouteOptions {
729
758
  * the default.
730
759
  */
731
760
  wrap?: (children: ReactNode, ctx: CanvasWrapContext) => ReactNode;
761
+ /**
762
+ * The ANALYTICS slot (EC-618): a component the route renders inside every `render`
763
+ * outcome, AFTER the page (and outside `wrap`, so a wrapper cannot drop it), with the
764
+ * resolved document, the locale, the path and the status ({@link CanvasBeaconProps}).
765
+ * Published perspective only — a draft preview is an editor at work, not a visit.
766
+ *
767
+ * The engine supplies the slot and nothing else: its value is the beacon of the
768
+ * analytics addon, `@elytracms/analytics/next` — **unpublished, pending counsel's
769
+ * verdict on its consent-free claim** (EC-618) — or a host's own component. Absent, the
770
+ * route renders exactly what it rendered before, and sends nothing.
771
+ *
772
+ * A not-found page never reaches this slot (`notFound()` renders the host's
773
+ * `not-found.tsx`), so a 404 beacon lives in that file.
774
+ */
775
+ analytics?: ComponentType<CanvasBeaconProps>;
732
776
  }
733
777
  interface CanvasRouteProps {
734
778
  params: Promise<{
@@ -1058,6 +1102,177 @@ interface RevalidateRouteHandlers {
1058
1102
  */
1059
1103
  declare function createRevalidateRoute(options: RevalidateRouteOptions): RevalidateRouteHandlers;
1060
1104
 
1105
+ /**
1106
+ * EC-617 — the pure decision core of the PUBLIC INGEST route (`createIngestRoute`, the
1107
+ * Next wrapper in `ingest.ts`): no Next imports, no network, an injected clock — the
1108
+ * house "pure core + thin wrapper" shape of `evaluateRevalidateRequest` /
1109
+ * `createRevalidateRoute`.
1110
+ *
1111
+ * The route is a site's one public write path (ruling EC-565 § 6c). It runs, in order:
1112
+ *
1113
+ * 1. **method** — `POST` only (405);
1114
+ * 2. **configured** — the server-held `INGEST_SECRET` and the receiver URL are set (503
1115
+ * otherwise: the route stays closed until deliberately configured, never a no-op);
1116
+ * 3. **same-origin** — a browser request from another origin (`Origin` / `Sec-Fetch-Site`)
1117
+ * is refused (403). The route emits no CORS headers and reads no cookie;
1118
+ * 4. **rate limit** — a fixed window per CLIENT KEY (429, `Retry-After`);
1119
+ * 5. **size** — the raw body capped at `maxBodyBytes` (413), checked before parsing;
1120
+ * 6. **shape** — JSON in the ingest envelope (400, with the issues);
1121
+ * 7. **bot gate** — the host's own check, an injected seam (403, `{ gate: 'bot' }`): the
1122
+ * engine supplies the slot, never the vendor;
1123
+ * 8. **enrichment** (EC-618) — an optional seam an ADDON fills (the analytics addon derives
1124
+ * a country and a visitor's sketch coordinates here, where the request is): it returns the
1125
+ * payload to forward, which is re-validated against the envelope and the receiver's cap
1126
+ * (500 — the site's own fault — when it throws or returns something invalid). Absent, the
1127
+ * route is exactly what it was;
1128
+ *
1129
+ * then the wrapper signs the payload with the server-held secret and forwards it to the
1130
+ * one Convex receiver, relaying its answer. Only the PAYLOAD is forwarded — never a request
1131
+ * header — so whatever an enrichment reads from the request (the client address, the user
1132
+ * agent, a geo header) stays in this request's memory; only what it derives travels.
1133
+ */
1134
+ /** The answer the core gives the wrapper. */
1135
+ type IngestEvaluation = {
1136
+ ok: true;
1137
+ payload: IngestPayload;
1138
+ } | {
1139
+ ok: false;
1140
+ status: 400 | 403 | 405 | 413 | 429 | 500 | 503;
1141
+ body: Record<string, unknown>;
1142
+ headers?: Record<string, string>;
1143
+ };
1144
+ /** What the host's bot gate answers (a Turnstile / hCaptcha / honeypot check the HOST implements). */
1145
+ type BotGateVerdict = {
1146
+ allow: true;
1147
+ } | {
1148
+ allow: false;
1149
+ reason: string;
1150
+ };
1151
+ /**
1152
+ * The bot-gate seam: called with the request and the parsed payload, AFTER the cheap
1153
+ * checks (so a flood never reaches a paid verification API). Absent → no gate.
1154
+ */
1155
+ type IngestBotGate = (input: {
1156
+ request: Request;
1157
+ payload: IngestPayload;
1158
+ }) => BotGateVerdict | Promise<BotGateVerdict>;
1159
+ /**
1160
+ * EC-618 — the enrichment seam: called with the request and the parsed payload LAST (after
1161
+ * the bot gate), it returns the payload the route signs and forwards. The engine supplies
1162
+ * the slot, never the code: an addon fills it (`@elytracms/analytics/next`'s
1163
+ * `analyticsEnrichment`), and a host may compose its own. Whatever it reads from the
1164
+ * request is held for this request only — the route forwards the returned payload and
1165
+ * nothing else. Absent → no enrichment, the payload forwarded as parsed.
1166
+ */
1167
+ type IngestEnrich = (input: {
1168
+ request: Request;
1169
+ payload: IngestPayload;
1170
+ }) => IngestPayload | Promise<IngestPayload>;
1171
+ /**
1172
+ * The fixed-window limiter — the AI route's pattern (EC-602), keyed per client. Holds,
1173
+ * in memory only, one `{ windowStart, count }` per key seen in the current window; expired
1174
+ * windows are dropped as the clock moves, so the map is bounded by the clients of one
1175
+ * window. Each server instance holds its own windows (a serverless platform runs several),
1176
+ * so the limit is per instance — the same documented limitation as the AI route.
1177
+ */
1178
+ interface FixedWindowLimiter {
1179
+ /** Count one request for `key`; `retryAfterSeconds` when it is over the limit. */
1180
+ hit(key: string): {
1181
+ allowed: true;
1182
+ } | {
1183
+ allowed: false;
1184
+ retryAfterSeconds: number;
1185
+ };
1186
+ /** How many keys the limiter currently holds (for the bound test). */
1187
+ size(): number;
1188
+ }
1189
+ declare function createFixedWindowLimiter(options: {
1190
+ limit: number;
1191
+ windowMs?: number;
1192
+ now?: () => number;
1193
+ }): FixedWindowLimiter;
1194
+ /**
1195
+ * The DEFAULT client key: the first hop of `x-forwarded-for` (what Vercel and most proxies
1196
+ * set), else `x-real-ip`, else one shared `unknown` bucket. An IP address is personal
1197
+ * data: it is held ONLY as a key of the in-memory window above, for at most one window,
1198
+ * and is never stored, logged or forwarded to the receiver. A host that wants a different
1199
+ * key (a hashed one, a session id) passes `clientKey`.
1200
+ */
1201
+ declare function defaultIngestClientKey(request: Request): string;
1202
+ /** Is this a cross-origin BROWSER request? (A request with no browser headers is not judged here.) */
1203
+ declare function isCrossOrigin(request: Request): boolean;
1204
+ interface IngestEvaluationInput {
1205
+ request: Request;
1206
+ /** The raw body, read by the wrapper up to `maxBodyBytes + 1` bytes (`undefined` when over). */
1207
+ rawBody: string | undefined;
1208
+ configured: boolean;
1209
+ limiter: FixedWindowLimiter;
1210
+ clientKey: (request: Request) => string;
1211
+ botGate?: IngestBotGate;
1212
+ enrich?: IngestEnrich;
1213
+ }
1214
+ /** Evaluate one request (see the module header for the order). */
1215
+ declare function evaluateIngestRequest(input: IngestEvaluationInput): Promise<IngestEvaluation>;
1216
+ /**
1217
+ * Read a request body as text, at most `maxBytes` (counted in UTF-8 bytes). `undefined`
1218
+ * when it is longer — the over-size body is never buffered whole. A declared
1219
+ * `Content-Length` over the cap is refused before a byte is read.
1220
+ */
1221
+ declare function readCappedBody(request: Request, maxBytes?: number): Promise<string | undefined>;
1222
+
1223
+ /**
1224
+ * EC-617 — the PUBLIC INGEST route: a site's one write path into its project (form
1225
+ * submissions, analytics events, upload URLs), mounted SAME-ORIGIN in the site's own
1226
+ * App Router:
1227
+ *
1228
+ * ```ts
1229
+ * // frontend/app/api/ingest/route.ts — through the delivery kit
1230
+ * export const { POST } = elytra.ingestRoute()
1231
+ * ```
1232
+ *
1233
+ * The decisions are the pure core's (`evaluateIngestRequest`: method, configured,
1234
+ * same-origin, rate limit, size, shape, the host's bot gate); this wrapper then SIGNS the
1235
+ * payload with the server-held `INGEST_SECRET` (it never reaches a browser) and forwards it
1236
+ * to the deployment's ONE receiver (`<deployment>.convex.site/elytra/ingest`), relaying the
1237
+ * receiver's answer. A receiver that rejects the signature is a SITE misconfiguration
1238
+ * (the two secrets differ), answered 502 with that sentence — never a 401 a visitor could
1239
+ * read as their fault.
1240
+ *
1241
+ * Uploads leave same-origin by design: an `upload` payload asks the receiver for ONE
1242
+ * short-lived storage URL (the request rides this route's gate, limiter and bot gate), and
1243
+ * the browser then sends the bytes straight to that URL; the submission that follows claims
1244
+ * the stored file, which is checked against what the upload request declared.
1245
+ */
1246
+ interface IngestRouteOptions {
1247
+ /** The server-held HMAC secret (= the deployment's `ELYTRA_INGEST_SECRET`). Unset → 503. */
1248
+ secret: string | undefined;
1249
+ /** The deployment's HTTP origin (`https://<name>.convex.site`); the receiver path is appended. Unset → 503. */
1250
+ siteUrl: string | undefined;
1251
+ /** Cap on the raw JSON body, ≤ the receiver's `INGEST_MAX_BODY_BYTES` (default). */
1252
+ maxBodyBytes?: number;
1253
+ /** Requests per client key per minute (default 30). */
1254
+ maxRequestsPerMinute?: number;
1255
+ /** The rate limiter's key for a request (default: the client IP, in memory only — see `defaultIngestClientKey`). */
1256
+ clientKey?: (request: Request) => string;
1257
+ /** The host's bot check (Turnstile, hCaptcha, a honeypot …). The engine supplies the slot, never the vendor. */
1258
+ botGate?: IngestBotGate;
1259
+ /**
1260
+ * EC-618 — an addon's enrichment, run last: it returns the payload to sign and forward
1261
+ * (the analytics addon adds a country and the visitor's sketch coordinates, derived from
1262
+ * the request here and never forwarded raw). Absent → the payload is forwarded as parsed.
1263
+ */
1264
+ enrich?: IngestEnrich;
1265
+ /** Injected clock (tests). */
1266
+ now?: () => number;
1267
+ /** Injected transport (tests). */
1268
+ fetchImpl?: typeof fetch;
1269
+ }
1270
+ interface IngestRouteHandlers {
1271
+ POST(request: Request): Promise<Response>;
1272
+ }
1273
+ /** Create the ingest route handler (see the module header). */
1274
+ declare function createIngestRoute(options: IngestRouteOptions): IngestRouteHandlers;
1275
+
1061
1276
  /**
1062
1277
  * Image delivery for the embedded runtime (EC-152).
1063
1278
  *
@@ -1253,6 +1468,8 @@ declare function elytraHostImage(options?: ElytraHostImageOptions): HostImageRen
1253
1468
  * | | equal the Convex deployment's own env var) |
1254
1469
  * | `REVALIDATE_SECRET` | Instant-publish webhook HMAC secret (must equal the |
1255
1470
  * | | deployment's `CANVAS_REVALIDATE_SECRET`) |
1471
+ * | `INGEST_SECRET` | Public-ingest HMAC secret (EC-617; must equal the |
1472
+ * | | deployment's `ELYTRA_INGEST_SECRET`) |
1256
1473
  *
1257
1474
  * EC-579 retired `STUDIO_ORIGIN` (the CORS origin of the studio's deleted browser
1258
1475
  * dispatcher): the one emitter is the Convex deployment, server to server.
@@ -1263,12 +1480,14 @@ declare const ELYTRA_HOST_ENV_KEYS: {
1263
1480
  readonly projectId: "ELYTRA_PROJECT_ID";
1264
1481
  readonly draftToken: "DRAFT_PREVIEW_TOKEN";
1265
1482
  readonly revalidateSecret: "REVALIDATE_SECRET";
1483
+ readonly ingestSecret: "INGEST_SECRET";
1266
1484
  };
1267
1485
  interface ElytraHostEnv {
1268
1486
  convexUrl?: string;
1269
1487
  projectId?: string;
1270
1488
  draftToken?: string;
1271
1489
  revalidateSecret?: string;
1490
+ ingestSecret?: string;
1272
1491
  }
1273
1492
  /**
1274
1493
  * Resolve the kit's env bag from a raw env record (`process.env` by default at
@@ -1585,6 +1804,12 @@ interface HostCanvasRouteOptions {
1585
1804
  * invalidation — a disagreement would drop nothing, silently.
1586
1805
  */
1587
1806
  published?: CanvasRouteOptions['published'];
1807
+ /**
1808
+ * EC-618 — the analytics slot (see `CanvasRouteOptions.analytics`): its value is an
1809
+ * addon's beacon (`@elytracms/analytics/next`, unpublished pending counsel). Absent →
1810
+ * nothing renders and nothing is sent.
1811
+ */
1812
+ analytics?: CanvasRouteOptions['analytics'];
1588
1813
  }
1589
1814
  interface LoadGlobalsOptions {
1590
1815
  /** Perspective of the chrome read. Default `'published'` (live is published-only). */
@@ -1705,6 +1930,15 @@ interface ElytraHost<Collections extends readonly CollectionDef[] = readonly Col
1705
1930
  previewRoute(): PreviewRouteHandlers;
1706
1931
  /** The draft-mode DISABLE route (no token — it only reduces visibility). */
1707
1932
  previewDisableRoute(): PreviewRouteHandlers;
1933
+ /**
1934
+ * EC-617 — the PUBLIC INGEST route for `app/api/ingest/route.ts`: the site's one write path
1935
+ * (form submissions, analytics events, upload URLs), signed with `INGEST_SECRET` and
1936
+ * forwarded to this deployment's receiver (its HTTP origin derived from
1937
+ * `ELYTRA_CONVEX_URL`; pass `siteUrl` for a self-hosted backend). Unset secret or no
1938
+ * backend = 503, closed. The rate limit, the client key and the bot gate are the host's to
1939
+ * tune — the engine supplies the slots, never a vendor.
1940
+ */
1941
+ ingestRoute(options?: Partial<Omit<IngestRouteOptions, 'secret'>>): IngestRouteHandlers;
1708
1942
  /**
1709
1943
  * Does the page for this catch-all `slug` OPEN full-bleed? Manifest-driven
1710
1944
  * (`presentation.fullBleed`, EC-368) and draft-mode aware — the host lays out
@@ -1766,6 +2000,12 @@ interface LiveContentOptions {
1766
2000
  interface DeliverySnapshotPayload {
1767
2001
  projectId: string;
1768
2002
  redirects?: unknown[];
2003
+ /**
2004
+ * EC-628 — the stored redirects the deployment withheld because it could not read them
2005
+ * (`invalid-redirect` issues from the one live rule). Absent from a deployment older than
2006
+ * EC-628, which serves such rows in `redirects` instead (and this host names them there).
2007
+ */
2008
+ redirectIssues?: unknown[];
1769
2009
  documents: unknown[];
1770
2010
  assets: unknown[];
1771
2011
  }
@@ -1870,4 +2110,4 @@ declare function evaluatePreviewRequest(requestUrl: string | URL, expectedToken:
1870
2110
  */
1871
2111
  declare const PACKAGE = "@elytracms/next";
1872
2112
 
1873
- 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 };
2113
+ 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 };