@tokenoftrust/storefront-runner 2.2.115 → 2.2.116

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.
@@ -306,6 +306,16 @@ export interface HomeContent {
306
306
  blocks: (HomeBlock | MarketingBlock)[];
307
307
  }
308
308
 
309
+ /** Minimum shape the home renderer can consume without dereferencing invalid blocks. */
310
+ export function hasHomeContentShape(value: unknown): boolean {
311
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
312
+ const blocks = (value as { blocks?: unknown }).blocks;
313
+ return Array.isArray(blocks) && blocks.every((block) =>
314
+ !!block && typeof block === "object" && !Array.isArray(block) &&
315
+ typeof (block as { component?: unknown }).component === "string",
316
+ );
317
+ }
318
+
309
319
  export interface EditorialPage {
310
320
  slug: string;
311
321
  title: string;
@@ -18,6 +18,7 @@ import type {
18
18
  EditorialPage,
19
19
  HomeContent,
20
20
  } from "./content-model.js";
21
+ import { hasHomeContentShape } from "./content-model.js";
21
22
  import type { ChromeConfig } from "../chrome/model.js";
22
23
  import type { CustomizationEnv, KvGet, TenantConfig } from "@tot/public-runtime";
23
24
  import {
@@ -730,19 +731,14 @@ export class PublishedContentProvider implements ContentProvider {
730
731
  }
731
732
 
732
733
  // ---------------------------------------------------------------------------
733
- // VersionPinnedContentProvider — pins content to ONE exact immutable
734
- // `versionId`, bypassing channel/pointer resolution entirely (the candidate/
735
- // revision preview's read path). Structurally the version-pinned twin of
736
- // PublishedContentProvider (channel -> versionId vs. versionId direct); every
737
- // method mirrors it 1:1 so a candidate's OWN pages/blog/products/etc. render
738
- // through the exact same generic-content plumbing as a live/preview page,
739
- // pinned to the exact commit under review instead of the shared channel.
734
+ // VersionPinnedContentProvider reads only ONE immutable version. An absent file
735
+ // stays absent, so a candidate cannot render bundled tenant content that its
736
+ // version manifest does not contain.
740
737
  // ---------------------------------------------------------------------------
741
738
 
742
739
  export class VersionPinnedContentProvider implements ContentProvider {
743
740
  readonly perRequestArtifactReads = true;
744
741
  constructor(
745
- private readonly base: ContentProvider,
746
742
  private readonly tenantId: string,
747
743
  private readonly get: KvGet,
748
744
  private readonly versionId: string,
@@ -770,24 +766,24 @@ export class VersionPinnedContentProvider implements ContentProvider {
770
766
  }
771
767
 
772
768
  async getHome(): Promise<HomeContent> {
773
- return (await this.pinned<HomeContent>("home.json")) ?? this.base.getHome();
769
+ const home = await this.pinned<HomeContent>("home.json");
770
+ if (!home || !hasHomeContentShape(home)) throw new Error("Pinned version has no renderable home.json");
771
+ return home;
774
772
  }
775
773
  async getHomeHtml(): Promise<string | null> {
776
774
  const raw = await this.pinnedRaw("home.html");
777
- return raw !== null ? stampTrackingIdsAtPublish(raw) : this.base.getHomeHtml();
775
+ return raw !== null ? stampTrackingIdsAtPublish(raw) : null;
778
776
  }
779
777
  async getPageHtml(slug: string): Promise<string | null> {
780
778
  const rel = `pages-html/${normalizeRawPageSlug(slug)}.html`;
781
779
  const raw = await this.pinnedRaw(rel);
782
- return raw !== null ? stampTrackingIdsAtPublish(raw) : this.base.getPageHtml(slug);
780
+ return raw !== null ? stampTrackingIdsAtPublish(raw) : null;
783
781
  }
784
782
  listPageHtmlSlugs(): Promise<string[]> {
785
783
  return this.listArtifactPaths("pages-html/").then((paths) =>
786
- paths === null
787
- ? this.base.listPageHtmlSlugs()
788
- : paths
789
- .filter((path) => path.endsWith(".html"))
790
- .map((path) => path.slice("pages-html/".length, -".html".length)),
784
+ (paths ?? [])
785
+ .filter((path) => path.endsWith(".html"))
786
+ .map((path) => path.slice("pages-html/".length, -".html".length)),
791
787
  );
792
788
  }
793
789
  async getChromeHtml(currentPath?: string): Promise<string | null> {
@@ -798,20 +794,19 @@ export class VersionPinnedContentProvider implements ContentProvider {
798
794
  const assignments = parseChromeAssignments(await this.pinned<unknown>("chrome-assignments.json"));
799
795
  return renderRawChrome(config, currentPath, assignments);
800
796
  }
801
- return this.base.getChromeHtml(currentPath);
797
+ return null;
802
798
  }
803
799
  async getChrome(): Promise<ChromeConfig> {
804
800
  const config = parseChromeConfig(await this.pinned<unknown>("chrome.json"));
805
- return config ?? this.base.getChrome();
801
+ return config ?? EMPTY_CHROME_CONFIG;
806
802
  }
807
803
  async getPage(slug: string): Promise<EditorialPage | null> {
808
- return (await this.pinned<EditorialPage>(`pages/${slug}.json`)) ?? this.base.getPage(slug);
804
+ return this.pinned<EditorialPage>(`pages/${slug}.json`);
809
805
  }
810
806
  async listPages(): Promise<EditorialPage[]> {
811
807
  const paths = await this.listArtifactPaths("pages/");
812
- if (paths === null) return this.base.listPages();
813
808
  const pages = await Promise.all(
814
- paths.filter((path) => path.endsWith(".json")).map(async (path) => this.pinned<EditorialPage>(path)),
809
+ (paths ?? []).filter((path) => path.endsWith(".json")).map(async (path) => this.pinned<EditorialPage>(path)),
815
810
  );
816
811
  return pages.filter((page): page is EditorialPage => page !== null);
817
812
  }
@@ -923,15 +918,15 @@ export function createContentProvider(
923
918
  pin?: ContentVersionPin,
924
919
  ): ContentProvider {
925
920
  // Content dirs are keyed by tenant_id (the adopter writes content/<tenant>/).
921
+ if (pin) {
922
+ return new VersionPinnedContentProvider(tenant.tenant_id, pin.get, pin.versionId);
923
+ }
926
924
  const local = new LocalContentProvider(tenant.tenant_id);
927
925
  const token = tenant.storyblok_space_token;
928
926
  const usable = token && !PLACEHOLDER_TOKEN_RE.test(token);
929
927
  const base: ContentProvider = usable
930
928
  ? new StoryblokContentProvider(token, local)
931
929
  : local;
932
- if (pin) {
933
- return new VersionPinnedContentProvider(base, tenant.tenant_id, pin.get, pin.versionId);
934
- }
935
930
  return customization
936
931
  ? new PublishedContentProvider(base, tenant.tenant_id, customization)
937
932
  : base;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.2.115",
3
+ "version": "2.2.116",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",