@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.
- package/apps/storefront/src/components/catalog/storefront/islands/ImageGallery.tsx +20 -3
- package/apps/storefront/src/lib/content/storyblok/provider.ts +10 -5
- package/apps/storefront/src/lib/publication/shared/routedApiParam.ts +28 -0
- package/apps/storefront/src/pages/api/admin/fulfillment/orders/[id]/address.ts +3 -2
- package/apps/storefront/src/pages/api/admin/fulfillment/orders/[id].ts +5 -4
- package/apps/storefront/src/pages/products/[handle].astro +8 -3
- package/package.json +1 -1
|
@@ -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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
747
|
-
|
|
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
|
|
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
|
|
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
|
|
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={
|
|
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.
|
|
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",
|