@elytracms/next 0.0.88 → 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
  }
@@ -585,7 +606,7 @@ function createCanvasRoute(options) {
585
606
  return options.metadata ? options.metadata(outcome.result, { url, perspective }) : defaultMetadata(outcome.result, outcome.assets);
586
607
  };
587
608
  const generateMetadata = async (props, parent) => {
588
- const [page, resolvedParent] = await Promise.all([pageMetadata(props), parent ?? null]);
609
+ const [page, resolvedParent] = await Promise.all([pageMetadata(props), parent]);
589
610
  return mergeMetadataOverParent(resolvedParent, page);
590
611
  };
591
612
  return { Page, generateMetadata };
@@ -719,71 +740,33 @@ function createCanvasSitemap(options) {
719
740
  return canvasSitemapEntries(source, options).entries;
720
741
  };
721
742
  }
722
- var REVALIDATE_SIGNATURE_HEADER = "x-elytra-signature";
723
- var revalidatePayloadSchema = z.object({
724
- projectId: z.string().min(1),
725
- /** The operation that fired, e.g. `documents.setState`, `publishing.publish`. */
726
- event: z.string().min(1),
727
- /** Fully-formatted Next tag strings (already scoped) — revalidated verbatim. */
728
- tags: z.array(z.string().min(1)),
729
- /** ISO timestamp of emission. Informational in v1 (no replay window). */
730
- timestamp: z.string().min(1)
731
- });
732
- function signRevalidateBody(rawBody, secret) {
733
- return `sha256=${createHmac("sha256", secret).update(rawBody, "utf8").digest("hex")}`;
734
- }
735
- var SIGNATURE_PREFIX = "sha256=";
736
- var HEX_PATTERN = /^[0-9a-f]+$/;
737
- function evaluateRevalidateRequest(rawBody, signature, secret) {
743
+ async function evaluateRevalidateRequest(rawBody, signature, secret) {
738
744
  if (secret === void 0 || secret.length === 0) {
739
745
  return { ok: false, status: 401, message: "Revalidation webhook is not configured." };
740
746
  }
741
- if (signature === null || signature === void 0 || !signature.startsWith(SIGNATURE_PREFIX)) {
747
+ if (typeof signature !== "string" || signature.length === 0) {
742
748
  return { ok: false, status: 401, message: "Missing or malformed webhook signature." };
743
749
  }
744
- const providedHex = signature.slice(SIGNATURE_PREFIX.length).toLowerCase();
745
- const expectedHex = createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
746
- 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)) {
747
751
  return { ok: false, status: 401, message: "Invalid webhook signature." };
748
752
  }
749
- let parsed;
750
- try {
751
- parsed = JSON.parse(rawBody);
752
- } catch {
753
- return { ok: false, status: 400, message: "Webhook body is not valid JSON." };
754
- }
755
- const payload = revalidatePayloadSchema.safeParse(parsed);
756
- if (!payload.success) {
757
- return { ok: false, status: 400, message: "Webhook payload has an invalid shape." };
758
- }
759
- return { ok: true, payload: payload.data };
760
- }
761
- function corsHeaders(allowOrigin) {
762
- if (allowOrigin === void 0 || allowOrigin.length === 0) return {};
763
- return {
764
- "access-control-allow-origin": allowOrigin,
765
- "access-control-allow-methods": "POST, OPTIONS",
766
- "access-control-allow-headers": `content-type, ${REVALIDATE_SIGNATURE_HEADER}`
767
- };
753
+ const read = readRevalidationBody(rawBody);
754
+ if (!read.ok) return { ok: false, status: 400, message: read.message };
755
+ return { ok: true, event: read.event };
768
756
  }
769
757
  function createRevalidateRoute(options) {
770
- const cors = corsHeaders(options.allowOrigin);
758
+ const project = { scope: options.scope, locales: [...options.locales] };
771
759
  return {
772
760
  async POST(request) {
773
761
  const rawBody = await request.text();
774
762
  const signature = request.headers.get(REVALIDATE_SIGNATURE_HEADER);
775
- const evaluation = evaluateRevalidateRequest(rawBody, signature, options.secret);
763
+ const evaluation = await evaluateRevalidateRequest(rawBody, signature, options.secret);
776
764
  if (!evaluation.ok) {
777
- return Response.json(
778
- { error: evaluation.message },
779
- { status: evaluation.status, headers: cors }
780
- );
765
+ return Response.json({ error: evaluation.message }, { status: evaluation.status });
781
766
  }
782
- for (const tag of evaluation.payload.tags) revalidateTag(tag, { expire: 0 });
783
- return Response.json({ revalidated: evaluation.payload.tags }, { headers: cors });
784
- },
785
- async OPTIONS() {
786
- 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 });
787
770
  }
788
771
  };
789
772
  }
@@ -894,8 +877,7 @@ var ELYTRA_HOST_ENV_KEYS = {
894
877
  convexUrl: "ELYTRA_CONVEX_URL",
895
878
  projectId: "ELYTRA_PROJECT_ID",
896
879
  draftToken: "DRAFT_PREVIEW_TOKEN",
897
- revalidateSecret: "REVALIDATE_SECRET",
898
- studioOrigin: "STUDIO_ORIGIN"
880
+ revalidateSecret: "REVALIDATE_SECRET"
899
881
  };
900
882
  function cleaned(value) {
901
883
  const trimmed = value?.trim();
@@ -1241,11 +1223,10 @@ function createElytraHost(options) {
1241
1223
  return createCanvasRoute({
1242
1224
  loadContent,
1243
1225
  components,
1244
- // Tag scope = the project id the studio publishes under, so its webhook
1245
- // tags match this site's cached entries.
1246
- // EC-543: the mode is in the entry key so no world ever reads another's
1247
- // entries; an explicit override may replace the scope, not drop the mode.
1248
- ...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 } },
1249
1230
  ...rest
1250
1231
  });
1251
1232
  };
@@ -1253,7 +1234,11 @@ function createElytraHost(options) {
1253
1234
  // An unset secret keeps the endpoint CLOSED (401) — the kit never ships a
1254
1235
  // default secret (an auth-shaped no-op is worse than an explicit gap).
1255
1236
  secret: "secret" in overrides ? overrides.secret : env.revalidateSecret,
1256
- ...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
1257
1242
  });
1258
1243
  const previewRoute = () => createPreviewRoute({ token: env.draftToken });
1259
1244
  const previewDisableRoute = () => createPreviewDisableRoute();
@@ -1284,6 +1269,6 @@ function createElytraHost(options) {
1284
1269
  }
1285
1270
  var PACKAGE = "@elytracms/next";
1286
1271
 
1287
- 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, mergeMetadataOverParent, nextCacheTags, nextImagePrimitive, openGraphLocale, 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 };
1288
1273
  //# sourceMappingURL=index.js.map
1289
1274
  //# sourceMappingURL=index.js.map