@tokenoftrust/storefront-runner 2.5.4 → 2.5.6

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.
@@ -18,8 +18,25 @@ import type { CatalogImage } from "@tot/public-runtime";
18
18
  import { placeholderDataUri, isUsableImageUrl } from "@/lib/content/shared/placeholder";
19
19
  import { resolveImage } from "@/lib/assets/shared/imageAttrs";
20
20
 
21
+ export type PublicGalleryImage = Pick<CatalogImage,
22
+ "id" | "url" | "alt" | "position" | "width" | "height" | "variant_ids" | "rehosted">;
23
+
24
+ /** Project before Astro serializes props; private provenance stays in the catalog. */
25
+ export function publicGalleryImages(images: readonly CatalogImage[]): PublicGalleryImage[] {
26
+ return images.map((image) => ({
27
+ id: image.id,
28
+ url: image.url,
29
+ position: image.position,
30
+ ...(image.alt !== undefined ? { alt: image.alt } : {}),
31
+ ...(image.width !== undefined ? { width: image.width } : {}),
32
+ ...(image.height !== undefined ? { height: image.height } : {}),
33
+ ...(image.variant_ids !== undefined ? { variant_ids: [...image.variant_ids] } : {}),
34
+ ...(image.rehosted !== undefined ? { rehosted: image.rehosted } : {}),
35
+ }));
36
+ }
37
+
21
38
  interface Props {
22
- images: CatalogImage[];
39
+ images: PublicGalleryImage[];
23
40
  title: string;
24
41
  productHandle: string;
25
42
  activeImageId?: string;
@@ -36,7 +53,7 @@ const isShopifyCdnUrl = (url: string): boolean =>
36
53
  * decision (same as <Img> on the PLP) so rehosted images use the same
37
54
  * provider-neutral shared-CDN URL on Cloudflare and CloudFront.
38
55
  */
39
- function mainImage(img: CatalogImage, title: string): { src: string; srcset?: string } {
56
+ function mainImage(img: PublicGalleryImage, title: string): { src: string; srcset?: string } {
40
57
  return resolveImage({
41
58
  src: img.url,
42
59
  rehosted: img.rehosted,
@@ -53,7 +70,7 @@ function mainImage(img: CatalogImage, title: string): { src: string; srcset?: st
53
70
  * URLs draw a deterministic placeholder.
54
71
  */
55
72
  const THUMB_WIDTH = 160;
56
- function thumbSrc(img: CatalogImage, title: string): string {
73
+ function thumbSrc(img: PublicGalleryImage, title: string): string {
57
74
  if (!isUsableImageUrl(img.url, img.rehosted)) {
58
75
  return placeholderDataUri({
59
76
  seed: img.id || title,
@@ -718,8 +718,10 @@ export class PublishedContentProvider implements ContentProvider {
718
718
  return page ?? ((await this.usesVersionedContent()) ? null : this.base.getPage(slug));
719
719
  }
720
720
  async listPages(): Promise<EditorialPage[]> {
721
+ // Editorial enumeration follows its source, not the generic artifact fallback.
722
+ if (!(await this.usesVersionedContent())) return this.base.listPages();
721
723
  const paths = await this.listArtifactPaths("pages/");
722
- if (paths === null) return this.base.listPages();
724
+ if (paths === null) return [];
723
725
  const pages = await Promise.all(
724
726
  paths
725
727
  .filter((path) => path.endsWith(".json"))
@@ -728,8 +730,10 @@ export class PublishedContentProvider implements ContentProvider {
728
730
  return pages.filter((page): page is EditorialPage => page !== null);
729
731
  }
730
732
 
731
- readArtifact(relative: string): Promise<string | null> {
732
- return this.publishedRaw(relative);
733
+ async readArtifact(relative: string): Promise<string | null> {
734
+ const raw = await this.publishedRaw(relative);
735
+ if (raw !== null) return raw;
736
+ return (await this.usesVersionedContent()) ? null : this.base.readArtifact(relative);
733
737
  }
734
738
 
735
739
  async derivedArtifact(name: string): Promise<DerivedArtifact | null> {
@@ -743,8 +747,9 @@ export class PublishedContentProvider implements ContentProvider {
743
747
  async listArtifactPaths(prefix = ""): Promise<string[] | null> {
744
748
  const root = contentArtifactPath(this.tenantId, "");
745
749
  const paths = await this.versionedPaths();
746
- return paths?.filter((path) => path.startsWith(`${root}${prefix}`))
747
- .map((path) => path.slice(root.length)) ?? null;
750
+ if (paths === null) return this.base.listArtifactPaths(prefix);
751
+ return paths.filter((path) => path.startsWith(`${root}${prefix}`))
752
+ .map((path) => path.slice(root.length));
748
753
  }
749
754
  }
750
755
 
@@ -0,0 +1,28 @@
1
+ import type { APIContext } from "astro";
2
+
3
+ const ROUTES = {
4
+ "pipeline-job": { path: /^\/api\/pipeline\/jobs\/([A-Za-z0-9._:-]{1,128})\/?$/, param: "jobId" },
5
+ "fulfillment-order": { path: /^\/api\/admin\/fulfillment\/orders\/([A-Za-z0-9._:-]{1,128})\/?$/, param: "id" },
6
+ "fulfillment-address": { path: /^\/api\/admin\/fulfillment\/orders\/([A-Za-z0-9._:-]{1,128})\/address\/?$/, param: "id" },
7
+ } as const;
8
+
9
+ export type RoutedApiKind = keyof typeof ROUTES;
10
+
11
+ /** Resolve an opaque resource inside the server-routed tenant, including rewritten endpoints. */
12
+ export function routedApiParam(context: APIContext, kind: RoutedApiKind): string | undefined {
13
+ if (!Object.hasOwn(ROUTES, kind)) return undefined;
14
+ const tenant = context.locals.tenant?.appDomain;
15
+ const prefix = context.locals.apiBasePath;
16
+ if (typeof tenant !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,252}$/.test(tenant)) return undefined;
17
+ if (typeof prefix !== "string" || (prefix !== "" && !/^\/[A-Za-z0-9][A-Za-z0-9.-]{0,252}$/.test(prefix))) return undefined;
18
+ let pathname: string;
19
+ try { pathname = new URL(context.request.url).pathname; } catch { return undefined; }
20
+ if (prefix && !pathname.startsWith(`${prefix}/`)) return undefined;
21
+ const relative = pathname.slice(prefix.length);
22
+ const route = ROUTES[kind];
23
+ const id = route.path.exec(relative)?.[1];
24
+ if (!id || id === "." || id === "..") return undefined;
25
+ const param = context.params[route.param];
26
+ if (param !== undefined && param !== "" && param !== id) return undefined;
27
+ return id;
28
+ }
@@ -1,3 +1,4 @@
1
+ import { routedApiParam } from "@/lib/publication/shared/routedApiParam";
1
2
  /**
2
3
  * Owner-only retrieval of a stored ship-to address.
3
4
  * The read runs only after the owner gate admits the caller.
@@ -24,8 +25,8 @@ export async function GET(context: APIContext): Promise<Response> {
24
25
  },
25
26
  });
26
27
  }
27
- const orderId = context.params.id ?? "";
28
- if (!isOpaqueId(orderId)) return fulfillmentResultResponse({ ok: false, code: "not-found" }, () => ({}));
28
+ const orderId = routedApiParam(context, "fulfillment-address");
29
+ if (!orderId || !isOpaqueId(orderId)) return fulfillmentResultResponse({ ok: false, code: "not-found" }, () => ({}));
29
30
  const opened = await openFulfillment(context);
30
31
  if (!opened.ok) return opened.response;
31
32
  return fulfillmentResultResponse(await opened.service.readAddress(opened.actor, orderId), (address) => address);
@@ -1,3 +1,4 @@
1
+ import { routedApiParam } from "@/lib/publication/shared/routedApiParam";
1
2
  /**
2
3
  * Authenticated owner API for one order's fulfillment commands.
3
4
  * The response is state codes and the fulfillment view. It never returns an address.
@@ -53,8 +54,8 @@ function fail(code: string): Response {
53
54
  export async function GET(context: APIContext): Promise<Response> {
54
55
  const refused = await requireOwner(context);
55
56
  if (refused) return refused;
56
- const orderId = context.params.id ?? "";
57
- if (!isOpaqueId(orderId)) return fail("not-found");
57
+ const orderId = routedApiParam(context, "fulfillment-order");
58
+ if (!orderId || !isOpaqueId(orderId)) return fail("not-found");
58
59
  const opened = await openFulfillment(context);
59
60
  if (!opened.ok) return opened.response;
60
61
  return fulfillmentResultResponse(await opened.service.get(opened.actor, orderId), (view) => view);
@@ -63,8 +64,8 @@ export async function GET(context: APIContext): Promise<Response> {
63
64
  export async function POST(context: APIContext): Promise<Response> {
64
65
  const refused = await requireOwner(context);
65
66
  if (refused) return refused;
66
- const orderId = context.params.id ?? "";
67
- if (!isOpaqueId(orderId)) return fail("not-found");
67
+ const orderId = routedApiParam(context, "fulfillment-order");
68
+ if (!orderId || !isOpaqueId(orderId)) return fail("not-found");
68
69
  const raw = await readJsonBody(context.request);
69
70
  if (!raw.ok) return fulfillmentResultResponse(raw, () => ({}));
70
71
  const parsed = parseFulfillmentBody(raw.value);
@@ -11,7 +11,7 @@ import { homeCrumbLabel, siteTrail } from "@/lib/content/shared/breadcrumbs";
11
11
  import Badge from "@/components/content/storefront/Badge.astro";
12
12
  import ProductCard from "@/components/catalog/storefront/ProductCard.astro";
13
13
  import Section from "@/components/content/storefront/Section.astro";
14
- import ImageGallery from "@/components/catalog/storefront/islands/ImageGallery.tsx";
14
+ import ImageGallery, { publicGalleryImages } from "@/components/catalog/storefront/islands/ImageGallery.tsx";
15
15
  import VariantSelector, { publicSelectorVariants } from "@/components/catalog/storefront/islands/VariantSelector.tsx";
16
16
  import CheckoutComplianceGate from "@/components/compliance/storefront/islands/CheckoutComplianceGate.tsx";
17
17
  import RecentlyViewed from "@/components/catalog/storefront/islands/RecentlyViewed.tsx";
@@ -345,7 +345,7 @@ const structuredData = {
345
345
  <div>
346
346
  <ImageGallery
347
347
  client:idle
348
- images={product.images}
348
+ images={publicGalleryImages(product.images)}
349
349
  title={product.title}
350
350
  productHandle={product.handle}
351
351
  />
@@ -420,7 +420,12 @@ const structuredData = {
420
420
  productTitle={product.title}
421
421
  checkoutCart={checkoutCart}
422
422
  pricing={pricing}
423
- handoffUrl={sampleHandoff ? undefined : cartApiPath}
423
+ handoffUrl={
424
+ !sampleHandoff &&
425
+ (Astro.locals.browserEnvironment.environment === "live" || Astro.locals.browserEnvironment.environment === "local")
426
+ ? cartApiPath
427
+ : undefined
428
+ }
424
429
  />
425
430
  {/* Regulated-tenant checkout ENFORCEMENT gate. Intercepts the
426
431
  managed-cart handoff to clear age + ship-to compliance server-side
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.5.4",
3
+ "version": "2.5.6",
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",