@tokenoftrust/storefront-runner 3.6.1 → 3.7.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "3.6.1",
3
+ "version": "3.7.0",
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",
@@ -24,6 +24,14 @@ export const MAX_ASSET_REGISTRY_BYTES = 32 * 1024 * 1024;
24
24
  export type VerificationMethod = "byte-read" | "create-readback" | "audit";
25
25
  export interface VerifiedFacet { size: number; etag: string; at: string; how: VerificationMethod }
26
26
 
27
+ /**
28
+ * The response policy a stored pool image carries as object metadata (S3 only: CloudFront emits it;
29
+ * the Worker sets R2's itself). `csp` is the digest (`sha256:<hex>`) of the policy that was observed
30
+ * on the object, `etag` the object it was observed on. It describes that stored object, so a
31
+ * different ETag or a different policy digest makes it stale.
32
+ */
33
+ export interface HeadersFacet { csp: string; etag: string; at: string }
34
+
27
35
  export type AssetProvenanceKind = "capture" | "upload" | "repo" | "variant";
28
36
  export interface AssetProvenance {
29
37
  kind: AssetProvenanceKind;
@@ -74,6 +82,7 @@ export interface AssetRecord {
74
82
  /** Entries (`<sha256><ext>`) derived from this one. */
75
83
  renditions?: string[];
76
84
  verified?: VerifiedFacet;
85
+ headers?: HeadersFacet;
77
86
  retention?: RetentionFacet;
78
87
  }
79
88
 
@@ -97,12 +106,14 @@ export interface AssetWrite {
97
106
  recipe?: AssetRecipe;
98
107
  rendition?: string;
99
108
  verified?: VerifiedFacet;
109
+ headers?: HeadersFacet;
100
110
  retention?: { to: RetentionClass; at: string; how: string; actor?: string };
101
111
  }
102
112
 
103
113
  const ENTRY_RE = /^([0-9a-f]{64})(\.[A-Za-z0-9]+)?$/;
104
114
  const POOL_KEY_RE = /^assets\/([0-9a-f]{64})(\.[A-Za-z0-9]+)?$/;
105
115
  const ETAG_RE = /^[A-Za-z0-9-]{1,128}$/;
116
+ const POLICY_DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
106
117
  const TENANT_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/;
107
118
  const ACTOR_RE = /^[A-Za-z0-9._:-]{1,128}$/;
108
119
  const REVISION_RE = /^[A-Za-z0-9._-]{1,128}$/;
@@ -172,6 +183,12 @@ function normalizeVerified(raw: unknown): VerifiedFacet | null {
172
183
  return { size: raw.size as number, etag: raw.etag, at: raw.at, how: raw.how as VerificationMethod };
173
184
  }
174
185
 
186
+ function normalizeHeaders(raw: unknown): HeadersFacet | null {
187
+ if (!isObject(raw) || typeof raw.csp !== "string" || !POLICY_DIGEST_RE.test(raw.csp) ||
188
+ typeof raw.etag !== "string" || !ETAG_RE.test(raw.etag) || !validTime(raw.at)) return null;
189
+ return { csp: raw.csp, etag: raw.etag, at: raw.at };
190
+ }
191
+
175
192
  function normalizeProvenance(raw: unknown): AssetProvenance | null {
176
193
  if (!isObject(raw) || typeof raw.kind !== "string" || !PROVENANCE_KINDS.has(raw.kind) || !validTime(raw.at)) return null;
177
194
  if (raw.locator !== undefined && !validLocator(raw.locator)) return null;
@@ -266,6 +283,8 @@ export function normalizeAssetRecord(entry: string, raw: unknown): AssetRecord |
266
283
  }
267
284
  const verified = normalizeVerified(raw.verified);
268
285
  if (verified) record.verified = verified;
286
+ const headers = normalizeHeaders(raw.headers);
287
+ if (headers) record.headers = headers;
269
288
  const retention = normalizeRetention(raw.retention);
270
289
  if (retention) record.retention = retention;
271
290
  return record;
@@ -305,7 +324,7 @@ export function isEmptyAssetRecord(record: AssetRecord): boolean {
305
324
  * - `mediaType`, `size`, `provenance`, `owner`, `recipe`: the first writer's value stands. These
306
325
  * describe immutable bytes, so a later writer has nothing to add.
307
326
  * - `tenants`, `renditions`: a union.
308
- * - `verified`: the latest verification replaces the earlier one.
327
+ * - `verified`, `headers`: the latest observation replaces the earlier one.
309
328
  * - `retention`: a transition is applied only when the current class allows it
310
329
  * ({@link RETENTION_TRANSITIONS}); a write naming the current class is a no-op. `rejected` reports
311
330
  * a transition that was not allowed.
@@ -341,6 +360,10 @@ export function applyAssetWrite(existing: AssetRecord | undefined, write: AssetW
341
360
  const verified = normalizeVerified(write.verified);
342
361
  if (verified) record.verified = verified;
343
362
  }
363
+ if (write.headers) {
364
+ const headers = normalizeHeaders(write.headers);
365
+ if (headers) record.headers = headers;
366
+ }
344
367
  let rejected: string | undefined;
345
368
  if (write.retention) {
346
369
  const from = record.retention?.class ?? "release";
@@ -59,6 +59,8 @@ export interface ReleaseImageFacts {
59
59
  heldPoolBytes(entry: string): Uint8Array | null;
60
60
  /** True when the release records `entry` as a sealed capture (`capturedOriginals`). */
61
61
  captured(entry: string): boolean;
62
+ /** True when the release declares `entry` a rendition its build left in the pool, proven by recorded evidence. */
63
+ pooledRendition?(entry: string): boolean;
62
64
  /**
63
65
  * A bundle path (decoded, no leading slash): absent from the release, present but not pinned, or
64
66
  * pinned to the pool entry its bytes hash to.
@@ -181,7 +183,7 @@ export function judgeReleaseImages(input: {
181
183
  const pool = POOL_REF.exec(ref);
182
184
  if (!pool) continue;
183
185
  const entry = `${pool[1]}${pool[2] ?? ""}`;
184
- if (!input.facts.heldPoolBytes(entry) && !input.facts.captured(entry)) add("unregistered", ref);
186
+ if (!input.facts.heldPoolBytes(entry) && !input.facts.captured(entry) && !input.facts.pooledRendition?.(entry)) add("unregistered", ref);
185
187
  else if (!supportedPoolEntry(entry, input.facts, input.formats)) add("unsupported-format", ref);
186
188
  else poolEntries.add(entry);
187
189
  } else if (dependency.disposition === "tenant-hosted") {