@elytracms/next 0.0.87 → 0.0.90

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.js CHANGED
@@ -5,16 +5,16 @@ import { isValidElement } from 'react';
5
5
  import { isBlockSequenceValueShape } from '@elytracms/core/rich-text';
6
6
  import { jsx, Fragment } from 'react/jsx-runtime';
7
7
  import { cacheTags, isDocumentHistory, resolvedAsset, createContentClient, createContentContext } from '@elytracms/core/content';
8
- import { documentKey, splitLocalePath, hierarchyMount, composePath, localeUrlPath, mergeRouteRecords, fallbackHomeRoutes, presentationBodyFields, splitPath, documentSchema, redirectRecordSchema } from '@elytracms/core/cms-core';
9
- export { documentSchema, fallbackHomeRoutes, localeConfigSchema, localeUrlPath, mergeRouteRecords, redirectRecordSchema, resolveLocale, routeRecordSchema, splitLocalePath, textDirection } from '@elytracms/core/cms-core';
8
+ import { parseLocaleConfig, documentKey, splitLocalePath, hierarchyMount, composePath, localeUrlPath, mergeRouteRecords, fallbackHomeRoutes, presentationBodyFields, splitPath, documentSchema, redirectRecordSchema } from '@elytracms/core/cms-core';
9
+ export { documentSchema, fallbackHomeRoutes, localeConfigSchema, localeUrlPath, mergeRouteRecords, parseLocaleConfig, redirectRecordSchema, resolveLocale, routeRecordSchema, splitLocalePath, textDirection } from '@elytracms/core/cms-core';
10
10
  import { revalidateTag, unstable_cache } from 'next/cache';
11
11
  import { draftMode } from 'next/headers';
12
12
  import { redirect, notFound, permanentRedirect } from 'next/navigation';
13
13
  import { COMPOSITE_KEY_SEPARATOR } from '@elytracms/core/project-graph';
14
- import { createHmac, timingSafeEqual } from 'crypto';
15
- import { z } from 'zod';
16
- export { z } from 'zod';
14
+ import { verifyRevalidationSignature, readRevalidationBody, REVALIDATE_SIGNATURE_HEADER } from '@elytracms/core/revalidation';
15
+ export { REVALIDATE_SIGNATURE_HEADER, revalidationEventSchema, signRevalidationBody, signedRevalidationRequest } from '@elytracms/core/revalidation';
17
16
  import NextImage from 'next/image';
17
+ export { z } from 'zod';
18
18
 
19
19
  // src/components.ts
20
20
  function defineHostComponents(components = [], options = {}) {
@@ -134,7 +134,7 @@ function mergeCacheTags(first, ...rest) {
134
134
  });
135
135
  }
136
136
  function createStaticContentSource(data) {
137
- const locales = data.locales;
137
+ const locales = parseLocaleConfig(data.locales);
138
138
  const routes = data.routes ?? [];
139
139
  const redirects = data.redirects ?? [];
140
140
  const assetUrl = data.assetUrl ?? ((asset) => `asset://unresolved/${asset.storageKey}`);
@@ -414,6 +414,27 @@ function nextCacheTags(tags, scope) {
414
414
  for (const assetId of tags.assets) all.add(assetCacheTag(assetId, scope));
415
415
  return [...all].sort();
416
416
  }
417
+ function revalidationTags(event, project) {
418
+ const { scope } = project;
419
+ const tags = /* @__PURE__ */ new Set();
420
+ switch (event.kind) {
421
+ case "publish":
422
+ case "unpublish":
423
+ case "delete":
424
+ tags.add(docCacheTag(`${event.collection}:${event.id}`, scope));
425
+ for (const locale of project.locales) tags.add(collectionCacheTag(event.collection, locale, scope));
426
+ tags.add(projectSweepTag(scope));
427
+ break;
428
+ case "redirects":
429
+ tags.add(projectSweepTag(scope));
430
+ break;
431
+ case "asset":
432
+ tags.add(assetCacheTag(event.id, scope));
433
+ tags.add(projectSweepTag(scope));
434
+ break;
435
+ }
436
+ return [...tags].sort();
437
+ }
417
438
  function canvasCacheKey({ scope, mode, url }) {
418
439
  return ["elytra-route", scope ?? "", mode ?? "", url];
419
440
  }
@@ -467,7 +488,7 @@ function resolveOgImage(value, assets) {
467
488
  return assets.find((asset) => asset.id === value)?.url ?? null;
468
489
  }
469
490
  function openGraphLocale(locale) {
470
- return locale.replace(/-/g, "_");
491
+ return locale.includes("-") ? locale.replace(/-/g, "_") : null;
471
492
  }
472
493
  function canvasPageMetadata(result, assets = []) {
473
494
  const doc = result.document;
@@ -484,17 +505,32 @@ function canvasPageMetadata(result, assets = []) {
484
505
  if (seoDescription) metadata.description = seoDescription;
485
506
  const ogTitle = seoOgTitle ?? title;
486
507
  if (doc) {
508
+ const ogLocale = openGraphLocale(result.tags.locale);
487
509
  metadata.openGraph = {
488
510
  ...ogTitle !== null ? { title: ogTitle } : {},
489
511
  ...seoDescription ? { description: seoDescription } : {},
490
512
  ...seoOgImage ? { images: [{ url: seoOgImage }] } : {},
491
- locale: openGraphLocale(result.tags.locale)
513
+ ...ogLocale !== null ? { locale: ogLocale } : {}
492
514
  };
493
515
  }
494
516
  if (seoNoindex) metadata.robots = { index: false };
495
517
  if (seoCanonical) metadata.alternates = { canonical: seoCanonical };
496
518
  return metadata;
497
519
  }
520
+ function mergeMetadataOverParent(parent, page) {
521
+ if (!parent) return page;
522
+ const merged = { ...page };
523
+ for (const [key, pageValue] of Object.entries(page)) {
524
+ const parentValue = parent[key];
525
+ if (isPlainObject(pageValue) && isPlainObject(parentValue)) {
526
+ merged[key] = { ...parentValue, ...pageValue };
527
+ }
528
+ }
529
+ return merged;
530
+ }
531
+ function isPlainObject(value) {
532
+ return typeof value === "object" && value !== null && !Array.isArray(value);
533
+ }
498
534
  function defaultMetadata(result, assets) {
499
535
  return canvasPageMetadata(result, assets);
500
536
  }
@@ -555,7 +591,7 @@ function createCanvasRoute(options) {
555
591
  }
556
592
  }
557
593
  };
558
- const generateMetadata = async (props) => {
594
+ const pageMetadata = async (props) => {
559
595
  const { slug } = await props.params;
560
596
  const url = pathFromSlug(slug);
561
597
  const perspective = await requestPerspective();
@@ -569,6 +605,10 @@ function createCanvasRoute(options) {
569
605
  if (outcome.kind !== "render") return {};
570
606
  return options.metadata ? options.metadata(outcome.result, { url, perspective }) : defaultMetadata(outcome.result, outcome.assets);
571
607
  };
608
+ const generateMetadata = async (props, parent) => {
609
+ const [page, resolvedParent] = await Promise.all([pageMetadata(props), parent]);
610
+ return mergeMetadataOverParent(resolvedParent, page);
611
+ };
572
612
  return { Page, generateMetadata };
573
613
  }
574
614
  function paramNamesOf(pattern) {
@@ -700,71 +740,33 @@ function createCanvasSitemap(options) {
700
740
  return canvasSitemapEntries(source, options).entries;
701
741
  };
702
742
  }
703
- var REVALIDATE_SIGNATURE_HEADER = "x-elytra-signature";
704
- var revalidatePayloadSchema = z.object({
705
- projectId: z.string().min(1),
706
- /** The operation that fired, e.g. `documents.setState`, `publishing.publish`. */
707
- event: z.string().min(1),
708
- /** Fully-formatted Next tag strings (already scoped) — revalidated verbatim. */
709
- tags: z.array(z.string().min(1)),
710
- /** ISO timestamp of emission. Informational in v1 (no replay window). */
711
- timestamp: z.string().min(1)
712
- });
713
- function signRevalidateBody(rawBody, secret) {
714
- return `sha256=${createHmac("sha256", secret).update(rawBody, "utf8").digest("hex")}`;
715
- }
716
- var SIGNATURE_PREFIX = "sha256=";
717
- var HEX_PATTERN = /^[0-9a-f]+$/;
718
- function evaluateRevalidateRequest(rawBody, signature, secret) {
743
+ async function evaluateRevalidateRequest(rawBody, signature, secret) {
719
744
  if (secret === void 0 || secret.length === 0) {
720
745
  return { ok: false, status: 401, message: "Revalidation webhook is not configured." };
721
746
  }
722
- if (signature === null || signature === void 0 || !signature.startsWith(SIGNATURE_PREFIX)) {
747
+ if (typeof signature !== "string" || signature.length === 0) {
723
748
  return { ok: false, status: 401, message: "Missing or malformed webhook signature." };
724
749
  }
725
- const providedHex = signature.slice(SIGNATURE_PREFIX.length).toLowerCase();
726
- const expectedHex = createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
727
- if (providedHex.length !== expectedHex.length || !HEX_PATTERN.test(providedHex) || !timingSafeEqual(Buffer.from(providedHex, "hex"), Buffer.from(expectedHex, "hex"))) {
750
+ if (!await verifyRevalidationSignature(rawBody, signature, secret)) {
728
751
  return { ok: false, status: 401, message: "Invalid webhook signature." };
729
752
  }
730
- let parsed;
731
- try {
732
- parsed = JSON.parse(rawBody);
733
- } catch {
734
- return { ok: false, status: 400, message: "Webhook body is not valid JSON." };
735
- }
736
- const payload = revalidatePayloadSchema.safeParse(parsed);
737
- if (!payload.success) {
738
- return { ok: false, status: 400, message: "Webhook payload has an invalid shape." };
739
- }
740
- return { ok: true, payload: payload.data };
741
- }
742
- function corsHeaders(allowOrigin) {
743
- if (allowOrigin === void 0 || allowOrigin.length === 0) return {};
744
- return {
745
- "access-control-allow-origin": allowOrigin,
746
- "access-control-allow-methods": "POST, OPTIONS",
747
- "access-control-allow-headers": `content-type, ${REVALIDATE_SIGNATURE_HEADER}`
748
- };
753
+ const read = readRevalidationBody(rawBody);
754
+ if (!read.ok) return { ok: false, status: 400, message: read.message };
755
+ return { ok: true, event: read.event };
749
756
  }
750
757
  function createRevalidateRoute(options) {
751
- const cors = corsHeaders(options.allowOrigin);
758
+ const project = { scope: options.scope, locales: [...options.locales] };
752
759
  return {
753
760
  async POST(request) {
754
761
  const rawBody = await request.text();
755
762
  const signature = request.headers.get(REVALIDATE_SIGNATURE_HEADER);
756
- const evaluation = evaluateRevalidateRequest(rawBody, signature, options.secret);
763
+ const evaluation = await evaluateRevalidateRequest(rawBody, signature, options.secret);
757
764
  if (!evaluation.ok) {
758
- return Response.json(
759
- { error: evaluation.message },
760
- { status: evaluation.status, headers: cors }
761
- );
765
+ return Response.json({ error: evaluation.message }, { status: evaluation.status });
762
766
  }
763
- for (const tag of evaluation.payload.tags) revalidateTag(tag, { expire: 0 });
764
- return Response.json({ revalidated: evaluation.payload.tags }, { headers: cors });
765
- },
766
- async OPTIONS() {
767
- return new Response(null, { status: 204, headers: cors });
767
+ const tags = revalidationTags(evaluation.event, project);
768
+ for (const tag of tags) revalidateTag(tag, { expire: 0 });
769
+ return Response.json({ revalidated: tags });
768
770
  }
769
771
  };
770
772
  }
@@ -875,8 +877,7 @@ var ELYTRA_HOST_ENV_KEYS = {
875
877
  convexUrl: "ELYTRA_CONVEX_URL",
876
878
  projectId: "ELYTRA_PROJECT_ID",
877
879
  draftToken: "DRAFT_PREVIEW_TOKEN",
878
- revalidateSecret: "REVALIDATE_SECRET",
879
- studioOrigin: "STUDIO_ORIGIN"
880
+ revalidateSecret: "REVALIDATE_SECRET"
880
881
  };
881
882
  function cleaned(value) {
882
883
  const trimmed = value?.trim();
@@ -1222,11 +1223,10 @@ function createElytraHost(options) {
1222
1223
  return createCanvasRoute({
1223
1224
  loadContent,
1224
1225
  components,
1225
- // Tag scope = the project id the studio publishes under, so its webhook
1226
- // tags match this site's cached entries.
1227
- // EC-543: the mode is in the entry key so no world ever reads another's
1228
- // entries; an explicit override may replace the scope, not drop the mode.
1229
- ...cache === false ? {} : { cache: { mode: contentMode, ...cache ?? { scope: contentScope } } },
1226
+ // Tag scope = `contentScope`, the SAME value `revalidateRoute` derives its
1227
+ // invalidation tags with (EC-579) — one scope, so they cannot disagree.
1228
+ // EC-543: the mode is in the entry key so no world ever reads another's entries.
1229
+ ...cache === false ? {} : { cache: { mode: contentMode, scope: contentScope } },
1230
1230
  ...rest
1231
1231
  });
1232
1232
  };
@@ -1234,7 +1234,11 @@ function createElytraHost(options) {
1234
1234
  // An unset secret keeps the endpoint CLOSED (401) — the kit never ships a
1235
1235
  // default secret (an auth-shaped no-op is worse than an explicit gap).
1236
1236
  secret: "secret" in overrides ? overrides.secret : env.revalidateSecret,
1237
- ...overrides.allowOrigin !== void 0 ? { allowOrigin: overrides.allowOrigin } : env.studioOrigin !== void 0 ? { allowOrigin: env.studioOrigin } : {}
1237
+ // EC-579: the receiver's OWN project — the scope its cache entries are written
1238
+ // under (`canvasRoute` above) and every locale it serves. The emitter sends only
1239
+ // the event, so this is the one place the scope and locales are read for it.
1240
+ scope: contentScope,
1241
+ locales: options.locales.locales
1238
1242
  });
1239
1243
  const previewRoute = () => createPreviewRoute({ token: env.draftToken });
1240
1244
  const previewDisableRoute = () => createPreviewDisableRoute();
@@ -1265,6 +1269,6 @@ function createElytraHost(options) {
1265
1269
  }
1266
1270
  var PACKAGE = "@elytracms/next";
1267
1271
 
1268
- export { CanvasRenderer, ELYTRA_HOST_ENV_KEYS, ElytraImage, PACKAGE, REVALIDATE_SIGNATURE_HEADER, assembleLiveSource, bodyItemsOf, canvasPageMetadata, canvasSitemapEntries, collectionCacheTag, createAssetImageLoader, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createElytraHost, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defineHostComponents, docCacheTag, elytraHostImage, evaluatePreviewRequest, evaluateRevalidateRequest, firstBlockFullBleed, loadCachedEntry, loadLiveContent, mergeCacheTags, nextCacheTags, nextImagePrimitive, pathFromSlug, projectSweepTag, resolveCanvasPage, resolveGlobals, resolveHostEnv, revalidatePayloadSchema, routeCacheTag, safeRedirectTarget, scopeCacheTag, signRevalidateBody };
1272
+ export { CanvasRenderer, ELYTRA_HOST_ENV_KEYS, ElytraImage, PACKAGE, assembleLiveSource, bodyItemsOf, canvasPageMetadata, canvasSitemapEntries, collectionCacheTag, createAssetImageLoader, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createElytraHost, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defineHostComponents, docCacheTag, elytraHostImage, evaluatePreviewRequest, evaluateRevalidateRequest, firstBlockFullBleed, loadCachedEntry, loadLiveContent, mergeCacheTags, mergeMetadataOverParent, nextCacheTags, nextImagePrimitive, openGraphLocale, pathFromSlug, projectSweepTag, resolveCanvasPage, resolveGlobals, resolveHostEnv, revalidationTags, routeCacheTag, safeRedirectTarget, scopeCacheTag };
1269
1273
  //# sourceMappingURL=index.js.map
1270
1274
  //# sourceMappingURL=index.js.map