@tokenoftrust/storefront-runner 2.5.1 → 2.5.2

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.
@@ -5,7 +5,10 @@
5
5
  * when the sealed revision manifest names the address.
6
6
  */
7
7
  import { sha256Hex } from "@tot/public-runtime";
8
+ import { CAPTURABLE_RASTER_MEDIA_TYPES, PINNED_ASSET_EXTENSION_BY_MEDIA_TYPE } from "@tot/public-runtime/hosted-image-media-types";
9
+ import { durableAssetsStatusKey, durableCaptureRevisionId, durableRequiredStores, parseDurableAssetsStatus } from "@tot/public-runtime/durable-assets";
8
10
 
11
+ const CAPTURE_TYPES = new Set<string>(CAPTURABLE_RASTER_MEDIA_TYPES);
9
12
  const ADDRESS_RE = /^[0-9a-f]{64}$/;
10
13
  /** Hostname-like tenant ids (no scheme, no path separators). */
11
14
  const TENANT_ID_RE = /^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$/i;
@@ -37,10 +40,11 @@ export type CapturedOriginalBucket = {
37
40
  get(key: string): Promise<unknown>;
38
41
  };
39
42
 
40
- type ManifestEntry = {
43
+ export type ManifestEntry = {
41
44
  address: string;
42
45
  byteLength: number;
43
46
  mediaType: string;
47
+ provenance?: { kind: "local" | "remote"; locator: string };
44
48
  };
45
49
 
46
50
  function objectKey(tenantId: string, address: string): string {
@@ -60,12 +64,13 @@ async function readManifestEntries(
60
64
  revisionId: string,
61
65
  ): Promise<ManifestEntry[] | null> {
62
66
  if (!isSafeTenantId(tenantId) || !isSafeRevisionId(revisionId)) return null;
67
+ const object = await bucket.get(originalImageManifestObjectKey(tenantId, revisionId));
68
+ if (!object || typeof object !== "object") return null;
69
+ const body = object as { arrayBuffer?: () => Promise<ArrayBuffer> };
70
+ if (typeof body.arrayBuffer !== "function") return null;
71
+ const bytes = await body.arrayBuffer();
63
72
  try {
64
- const object = await bucket.get(originalImageManifestObjectKey(tenantId, revisionId));
65
- if (!object || typeof object !== "object") return null;
66
- const body = object as { arrayBuffer?: () => Promise<ArrayBuffer> };
67
- if (typeof body.arrayBuffer !== "function") return null;
68
- const parsed = JSON.parse(new TextDecoder().decode(new Uint8Array(await body.arrayBuffer()))) as {
73
+ const parsed = JSON.parse(new TextDecoder().decode(new Uint8Array(bytes))) as {
69
74
  version?: unknown;
70
75
  revisionId?: unknown;
71
76
  entries?: unknown;
@@ -78,11 +83,13 @@ async function readManifestEntries(
78
83
  if (!raw || typeof raw !== "object") return null;
79
84
  const entry = raw as Partial<ManifestEntry>;
80
85
  if (typeof entry.address !== "string" || !ADDRESS_RE.test(entry.address)) return null;
81
- if (typeof entry.byteLength !== "number" || typeof entry.mediaType !== "string") return null;
86
+ if (!Number.isSafeInteger(entry.byteLength) || entry.byteLength! < 1 || entry.byteLength! > 8 * 1024 * 1024 ||
87
+ typeof entry.mediaType !== "string" || !CAPTURE_TYPES.has(entry.mediaType)) return null;
82
88
  entries.push({
83
89
  address: entry.address,
84
- byteLength: entry.byteLength,
85
- mediaType: entry.mediaType,
90
+ byteLength: entry.byteLength!,
91
+ mediaType: entry.mediaType!,
92
+ provenance: entry.provenance,
86
93
  });
87
94
  }
88
95
  return entries;
@@ -91,6 +98,31 @@ async function readManifestEntries(
91
98
  }
92
99
  }
93
100
 
101
+ /** Resolve an active, provider-verified designation through its tenant-scoped sealed capture. */
102
+ export async function readDurableCapturedEntry(input: {
103
+ bucket: CapturedOriginalBucket;
104
+ kv: { get(key: string): Promise<string | null> };
105
+ tenantId: string;
106
+ provider: string | null | undefined;
107
+ address: string;
108
+ }): Promise<ManifestEntry | null> {
109
+ if (!isSafeTenantId(input.tenantId) || !ADDRESS_RE.test(input.address)) return null;
110
+ const required = durableRequiredStores(input.provider);
111
+ if (!required) return null;
112
+ const record = parseDurableAssetsStatus(await input.kv.get(durableAssetsStatusKey(input.tenantId)), input.tenantId);
113
+ if (!record) return null;
114
+ for (const designation of record.entries) {
115
+ if (typeof designation.entry !== "string" || !designation.entry.startsWith(`${input.address}.`) ||
116
+ !Array.isArray(designation.verifiedStores) || !required.every((store) => designation.verifiedStores.includes(store)) ||
117
+ typeof designation.designatedRevision !== "string" || !isSafeRevisionId(designation.designatedRevision)) continue;
118
+ const entries = await readManifestEntries(input.bucket, input.tenantId, durableCaptureRevisionId(designation.designatedRevision));
119
+ const entry = entries?.find((entry) => entry.address === input.address &&
120
+ designation.entry === `${entry.address}${PINNED_ASSET_EXTENSION_BY_MEDIA_TYPE[entry.mediaType]}`);
121
+ if (entry) return entry;
122
+ }
123
+ return null;
124
+ }
125
+
94
126
  /**
95
127
  * Serve a captured original for the exact tenant+revision, or null to fall through.
96
128
  * Missing/unbound/tampered addresses are 404 with no locator or digest in the body.
@@ -98,18 +130,33 @@ async function readManifestEntries(
98
130
  export async function tryServeCapturedOriginal(input: {
99
131
  bucket: CapturedOriginalBucket;
100
132
  tenantId: string;
101
- revisionId: string;
133
+ revisionId?: string;
134
+ durable?: { kv: { get(key: string): Promise<string | null> }; provider: string | null | undefined };
102
135
  pathname: string;
103
136
  method?: string;
104
137
  }): Promise<Response | null> {
105
138
  const parsed = parsePinnedImagePath(input.pathname);
106
139
  if (!parsed) return null;
107
- if (!isSafeTenantId(input.tenantId) || !isSafeRevisionId(input.revisionId)) {
140
+ if (!isSafeTenantId(input.tenantId) || (input.revisionId !== undefined && !isSafeRevisionId(input.revisionId))) {
108
141
  return new Response("Not found", { status: 404, headers: { "cache-control": "no-store" } });
109
142
  }
110
143
 
111
- const entries = await readManifestEntries(input.bucket, input.tenantId, input.revisionId);
112
- const entry = entries?.find((item) => item.address === parsed.address);
144
+ const entries = input.revisionId ? await readManifestEntries(input.bucket, input.tenantId, input.revisionId).catch(() => null) : null;
145
+ let entry = entries?.find((item) => item.address === parsed.address);
146
+ let durable = false;
147
+ if (!entry && input.revisionId) {
148
+ const declared = await readManifestEntries(input.bucket, input.tenantId, durableCaptureRevisionId(input.revisionId)).catch(() => null);
149
+ entry = declared?.find((item) => item.address === parsed.address);
150
+ durable = !!entry;
151
+ }
152
+ if (!entry && input.durable) {
153
+ try {
154
+ entry = await readDurableCapturedEntry({ bucket: input.bucket, tenantId: input.tenantId,
155
+ address: parsed.address, ...input.durable }) ?? undefined;
156
+ durable = !!entry;
157
+ } catch { /* Unreadable durable authority fails closed. */ }
158
+ }
159
+ if (entry && parsed.ext && EXT_TYPE[parsed.ext.toLowerCase()] !== entry.mediaType) entry = undefined;
113
160
  if (!entry) {
114
161
  return new Response("Not found", { status: 404, headers: { "cache-control": "no-store" } });
115
162
  }
@@ -134,8 +181,8 @@ export async function tryServeCapturedOriginal(input: {
134
181
  const contentType = EXT_TYPE[parsed.ext.toLowerCase()] ?? entry.mediaType;
135
182
  const headers: Record<string, string> = {
136
183
  "content-type": contentType,
137
- "cache-control": "public, max-age=31536000, immutable",
138
- "x-tot-original-image": "revision-bound",
184
+ "cache-control": durable ? "no-store" : "public, max-age=31536000, immutable",
185
+ "x-tot-original-image": durable ? "durable" : "revision-bound",
139
186
  "content-length": String(bytes.byteLength),
140
187
  };
141
188
  const method = (input.method ?? "GET").toUpperCase();
@@ -0,0 +1,25 @@
1
+ /** Keep captured-image requests inside the selected tenant on path-routed hosted previews. */
2
+ export async function scopePreviewPinnedImageUrls(response: Response, basePath: string): Promise<Response> {
3
+ if (!basePath || !response.body || !(response.headers.get("content-type") ?? "").includes("text/html")) return response;
4
+ const html = await response.text();
5
+ const scoped = prefixPreviewPinnedImageUrls(html, basePath);
6
+ const headers = new Headers(response.headers);
7
+ headers.delete("content-length");
8
+ headers.delete("etag");
9
+ return new Response(scoped, { status: response.status, statusText: response.statusText, headers });
10
+ }
11
+
12
+ /** Scope only exact captured-image addresses; platform assets and external URLs retain their host. */
13
+ export function prefixPreviewPinnedImageUrls(html: string, basePath: string): string {
14
+ if (!basePath) return html;
15
+ return html.replace(/([\s"'(=,])\/_a\/([0-9a-f]{64}(?:\.[A-Za-z0-9]+)?)(?=[\s"'?#),<>]|$)/g,
16
+ (_match, before: string, entry: string) => `${before}${basePath}/_a/${entry}`);
17
+ }
18
+
19
+ /** Choose asset routing from the resolved request tenant, never a second host-membership check. */
20
+ export function previewPinnedImageBasePath(tenant: {
21
+ tenant_id: string; appDomain?: string;
22
+ }, routing: { tenantId: string | undefined; basePath: string }): string {
23
+ if (routing.tenantId === tenant.tenant_id) return routing.basePath;
24
+ return tenant.appDomain && /^[a-z0-9.-]+$/i.test(tenant.appDomain) ? `/${tenant.appDomain}` : "";
25
+ }
@@ -73,10 +73,12 @@ function routeTierIndexFor(tenant: TenantConfig): RouteTierIndexV1 {
73
73
  * appended after the branch has read or written any edge cache.
74
74
  */
75
75
  export const onRequest = defineMiddleware(async (context, next) => {
76
- const seen = { cdnForwarded: false };
76
+ const seen: { cdnForwarded: boolean; assetBasePath?: string } = { cdnForwarded: false };
77
77
  const response = await serveRequest(context, next, seen);
78
+ const { scopePreviewPinnedImageUrls } = await import("@/lib/assets/preview-pinned-image-urls");
79
+ const scoped = await scopePreviewPinnedImageUrls(response, context.locals.apiBasePath ?? seen.assetBasePath ?? "");
78
80
  return withVisitorRegionCookie(
79
- response,
81
+ scoped,
80
82
  visitorRegionFor(context.request, { cdnForwarded: seen.cdnForwarded, dev: import.meta.env.DEV }),
81
83
  );
82
84
  });
@@ -84,7 +86,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
84
86
  async function serveRequest(
85
87
  context: APIContext,
86
88
  next: MiddlewareNext,
87
- seen: { cdnForwarded: boolean },
89
+ seen: { cdnForwarded: boolean; assetBasePath?: string },
88
90
  ): Promise<Response> {
89
91
  const { request, locals } = context;
90
92
  const url = new URL(request.url);
@@ -163,6 +165,7 @@ async function serveRequest(
163
165
  if (!tenant) {
164
166
  return new Response("Unknown host", { status: 404 });
165
167
  }
168
+ seen.assetBasePath = basePath;
166
169
 
167
170
  // ---- The one route classification -----------------------------------------
168
171
  // An EXPLICITLY unpaid store is never fully public: the ONLY way to reach its
@@ -282,7 +285,7 @@ async function serveRequest(
282
285
  });
283
286
  const capturedRevisionId = candidatePin?.versionId ?? cookieRevision;
284
287
  const originalsBucket =
285
- capturedRevisionId
288
+ (capturedRevisionId || renderPath.startsWith("/_a/"))
286
289
  ? ((await readR2("PRODUCT_IMAGES")) ?? (await readR2("TENANT_ASSETS")) ?? null)
287
290
  : null;
288
291
  const bundled = await tryServeStaticBundle({
@@ -298,11 +301,11 @@ async function serveRequest(
298
301
  hasCandidatePin: !!candidatePin,
299
302
  hasDraftCookie: !!context.cookies.get(DEV_DRAFT_COOKIE)?.value,
300
303
  hasManagedInstalls: false,
301
- ...(capturedRevisionId && originalsBucket
304
+ ...(originalsBucket
302
305
  ? {
303
306
  capturedOriginal: {
304
307
  bucket: originalsBucket as { get(key: string): Promise<unknown> },
305
- revisionId: capturedRevisionId,
308
+ ...(capturedRevisionId ? { revisionId: capturedRevisionId } : {}),
306
309
  },
307
310
  }
308
311
  : {}),
@@ -301,7 +301,7 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
301
301
  });
302
302
  const capturedRevisionId = candidatePin?.versionId ?? cookieRevision;
303
303
  const originalsBucket =
304
- capturedRevisionId
304
+ (capturedRevisionId || renderPath.startsWith("/_a/"))
305
305
  ? ((await readR2("PRODUCT_IMAGES")) ?? (await readR2("TENANT_ASSETS")) ?? null)
306
306
  : null;
307
307
  const bundled = publicationSource.status === "legacy-root" ? await tryServeStaticBundle({
@@ -316,11 +316,11 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
316
316
  hasCandidatePin: !!candidatePin,
317
317
  hasDraftCookie: !!draftCookie,
318
318
  hasManagedInstalls: managedIntegrationDeclarations.length > 0,
319
- ...(capturedRevisionId && originalsBucket
319
+ ...(originalsBucket
320
320
  ? {
321
321
  capturedOriginal: {
322
322
  bucket: originalsBucket as { get(key: string): Promise<unknown> },
323
- revisionId: capturedRevisionId,
323
+ ...(capturedRevisionId ? { revisionId: capturedRevisionId } : {}),
324
324
  },
325
325
  }
326
326
  : {}),
@@ -37,7 +37,7 @@ export interface StaticBundleAttemptInput {
37
37
  */
38
38
  capturedOriginal?: {
39
39
  bucket: { get(key: string): Promise<unknown> };
40
- revisionId: string;
40
+ revisionId?: string;
41
41
  };
42
42
  }
43
43
 
@@ -60,15 +60,17 @@ export async function tryServeStaticBundle(
60
60
  ): Promise<Response | null> {
61
61
  const { request, url, tenant, cache, kv } = input;
62
62
  const method = request.method.toUpperCase();
63
+ const assetPath = input.basePath && url.pathname.startsWith(`${input.basePath}/_a/`)
64
+ ? url.pathname.slice(input.basePath.length) : url.pathname;
63
65
 
64
66
  // Version-independent assets (`/_a/<sha256>`, `/_astro/<name>`) are exact addresses: the same
65
67
  // bytes for every viewer, preview or not, whatever the live pointer says. A page from an
66
68
  // older version — or a preview of one — keeps resolving its own sub-resources.
67
- if ((method === "GET" || method === "HEAD") && versionIndependentAssetKey(url.pathname)) {
69
+ if ((method === "GET" || method === "HEAD") && versionIndependentAssetKey(assetPath)) {
68
70
  const bucket = await readR2("STATIC_BUNDLES");
69
71
  if (bucket) {
70
72
  const pinnedKey = cache && method === "GET"
71
- ? staticBundleCacheKey(tenant.tenant_id, "pinned", url.pathname, "")
73
+ ? staticBundleCacheKey(tenant.tenant_id, "pinned", assetPath, "")
72
74
  : null;
73
75
  if (pinnedKey && cache) {
74
76
  const hit = await getCachedPage(cache, pinnedKey);
@@ -76,7 +78,7 @@ export async function tryServeStaticBundle(
76
78
  }
77
79
  const pinned = await serveVersionIndependentAsset(
78
80
  bucket as unknown as StaticBundleR2BucketLike,
79
- url.pathname,
81
+ assetPath,
80
82
  method,
81
83
  );
82
84
  if (pinned && pinned.ok) {
@@ -84,26 +86,28 @@ export async function tryServeStaticBundle(
84
86
  return pinned;
85
87
  }
86
88
  // `/_a/` miss: try revision-bound captured originals before the pinned 404.
87
- if (input.capturedOriginal && url.pathname.startsWith("/_a/")) {
89
+ if (input.capturedOriginal && assetPath.startsWith("/_a/")) {
88
90
  const { tryServeCapturedOriginal } = await import("@/lib/assets/captured-original-serve");
89
91
  const captured = await tryServeCapturedOriginal({
90
92
  bucket: input.capturedOriginal.bucket,
91
93
  tenantId: tenant.tenant_id,
92
94
  revisionId: input.capturedOriginal.revisionId,
93
- pathname: url.pathname,
95
+ pathname: assetPath,
94
96
  method,
97
+ ...(kv ? { durable: { kv, provider: tenant.serving?.provider } } : {}),
95
98
  });
96
99
  if (captured) return captured;
97
100
  }
98
101
  if (pinned) return pinned;
99
- } else if (input.capturedOriginal && url.pathname.startsWith("/_a/")) {
102
+ } else if (input.capturedOriginal && assetPath.startsWith("/_a/")) {
100
103
  const { tryServeCapturedOriginal } = await import("@/lib/assets/captured-original-serve");
101
104
  const captured = await tryServeCapturedOriginal({
102
105
  bucket: input.capturedOriginal.bucket,
103
106
  tenantId: tenant.tenant_id,
104
107
  revisionId: input.capturedOriginal.revisionId,
105
- pathname: url.pathname,
108
+ pathname: assetPath,
106
109
  method,
110
+ ...(kv ? { durable: { kv, provider: tenant.serving?.provider } } : {}),
107
111
  });
108
112
  if (captured) return captured;
109
113
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.5.1",
3
+ "version": "2.5.2",
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",