@tokenoftrust/storefront-runner 4.3.2 → 4.3.4

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": "4.3.2",
3
+ "version": "4.3.4",
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",
@@ -23,6 +23,8 @@ export interface CspOptions {
23
23
  styleSrc?: readonly string[];
24
24
  fontSrc?: readonly string[];
25
25
  imgSrc?: readonly string[];
26
+ /** `<video>`/`<audio>` sources (`media-src`). */
27
+ mediaSrc?: readonly string[];
26
28
  connectSrc?: readonly string[];
27
29
  /** Sandboxed-widget origins. */
28
30
  frameSrc?: readonly string[];
@@ -68,6 +70,9 @@ export function buildCsp(opts: CspOptions): string {
68
70
  // so allowing `data:` here is the same tolerated relaxation as img-src.
69
71
  "font-src": ["'self'", "data:", ...(opts.fontSrc ?? [])],
70
72
  "img-src": ["'self'", "data:", ...(opts.imgSrc ?? [])],
73
+ // Without an explicit media-src, `<video>`/`<audio>` fall back to
74
+ // `default-src 'self'` and every cross-origin clip is refused.
75
+ "media-src": ["'self'", ...(opts.mediaSrc ?? [])],
71
76
  "connect-src": ["'self'", ...(opts.connectSrc ?? [])],
72
77
  "frame-src": ["'self'", ...(opts.frameSrc ?? [])],
73
78
  };
@@ -98,6 +103,10 @@ export const STOREFRONT_CSP_ALLOWLIST = {
98
103
  fontSrc: [],
99
104
  // Product imagery is rehosted to R2 or served from remote CDNs.
100
105
  imgSrc: ["https:"],
106
+ // Tenant video and audio stay at their source URLs (the tenant repo holds no
107
+ // media binaries), so media is granted over https exactly like imagery. Media
108
+ // cannot execute script, so this is the same tolerated relaxation as img-src.
109
+ mediaSrc: ["https:"],
101
110
  connectSrc: [],
102
111
  } as const;
103
112
 
@@ -155,9 +155,9 @@ function pythonJson(mode, pid) {
155
155
  encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 3000,
156
156
  }));
157
157
  } catch (error) {
158
- const detail = `${error.stderr ?? ""}${error.message ?? ""}`;
159
- if (detail.includes("process_children_unbounded")) throw fixedError("refresh_process_children_unbounded");
160
- if (detail.includes("process_gone")) return null;
158
+ const stderr = Buffer.isBuffer(error?.stderr) ? error.stderr.toString("utf8") : String(error?.stderr ?? "");
159
+ if (stderr.includes("process_gone")) return null;
160
+ if (stderr.includes("process_children_unbounded")) throw fixedError("refresh_process_children_unbounded");
161
161
  throw error;
162
162
  }
163
163
  }
@@ -183,6 +183,7 @@ function linuxIdentity(pid) {
183
183
  function childPids(pid) {
184
184
  if (process.platform === "darwin") {
185
185
  const listed = pythonJson("children", pid);
186
+ if (listed === null) return [];
186
187
  if (!Array.isArray(listed) || listed.some((child) => !Number.isInteger(child))) throw fixedError("refresh_process_children_unbounded");
187
188
  return listed;
188
189
  }
@@ -200,7 +201,8 @@ function childPids(pid) {
200
201
  export function processIdentity(pid) {
201
202
  if (!Number.isSafeInteger(pid) || pid < 2) throw fixedError("refresh_process_identity_invalid");
202
203
  const info = process.platform === "darwin" ? pythonJson("identity", pid) : linuxIdentity(pid);
203
- if (!info || !Array.isArray(info.argv) || info.argv.some((part) => typeof part !== "string")) throw fixedError("refresh_process_identity_invalid");
204
+ if (info === null) return null;
205
+ if (!Array.isArray(info.argv) || info.argv.some((part) => typeof part !== "string")) throw fixedError("refresh_process_identity_invalid");
204
206
  if (info.uid !== process.getuid?.()) throw fixedError("refresh_process_owner_mismatch");
205
207
  let cwd;
206
208
  if (process.platform === "darwin") {
@@ -320,9 +322,10 @@ function descendantsOf(pid) {
320
322
  for (const child of childPids(parent)) {
321
323
  visit(child);
322
324
  try {
323
- out.push(processIdentity(child));
325
+ const identity = processIdentity(child);
326
+ if (identity) out.push(identity);
324
327
  } catch (error) {
325
- if (error?.message !== "refresh_process_identity_invalid") throw error;
328
+ if (error?.message !== "refresh_process_identity_invalid" && error?.message !== "refresh_process_changed") throw error;
326
329
  }
327
330
  }
328
331
  };
@@ -341,6 +344,7 @@ export const realEffects = {
341
344
  if (git(checkout, "status", "--porcelain", "--untracked-files=no")) throw fixedError("refresh_preview_tracked_dirty");
342
345
  const pid = Number(receipt?.pid ?? options.pid);
343
346
  const owned = processIdentity(pid);
347
+ if (!owned) throw fixedError("refresh_process_changed");
344
348
  const app = realpathSync(join(checkout, "apps/storefront"));
345
349
  const volume = privateIdentity(join(preview, "local-state"));
346
350
  const environment = privateIdentity(join(preview, "apps/storefront/.dev.vars"), true);
@@ -449,9 +453,18 @@ export const realEffects = {
449
453
  || environment.dev !== prior.environment.dev || environment.ino !== prior.environment.ino || environment.uid !== prior.environment.uid
450
454
  || environment.mode !== prior.environment.mode || environment.digest !== prior.environment.digest) throw fixedError("refresh_private_state_changed");
451
455
  },
452
- async stop(owned) {
456
+ async stop(owned, hooks = {}) {
453
457
  const descendants = descendantsOf(owned.pid);
454
- for (const identity of [...descendants, owned]) signalOwned(identity);
458
+ // Test seam: kill a listed descendant after enumeration and before the signal loop.
459
+ if (typeof hooks.afterEnumerate === "function") await hooks.afterEnumerate(descendants);
460
+ for (const identity of [...descendants, owned]) {
461
+ try {
462
+ signalOwned(identity);
463
+ } catch (error) {
464
+ const gone = error?.message === "refresh_process_changed" || error?.code === "ESRCH";
465
+ if (identity === owned || !gone) throw error;
466
+ }
467
+ }
455
468
  for (let attempt = 0; attempt < 50; attempt += 1) {
456
469
  let live = false;
457
470
  for (const identity of [...descendants, owned]) {
@@ -484,17 +497,24 @@ export const realEffects = {
484
497
  child.unref();
485
498
  for (let attempt = 0; attempt < 100; attempt += 1) {
486
499
  if (failed) {
487
- try { if (child.pid) await this.stop(processIdentity(child.pid)); } catch { /* An exited root cannot be signalled. */ }
500
+ try {
501
+ const identity = child.pid ? processIdentity(child.pid) : null;
502
+ if (identity) await this.stop(identity);
503
+ } catch { /* An exited root cannot be signalled. */ }
488
504
  throw fixedError("refresh_start_failed");
489
505
  }
490
506
  try {
491
507
  const identity = processIdentity(child.pid);
508
+ if (!identity) throw fixedError("refresh_process_changed");
492
509
  await fetchLocal("/health", previewBase());
493
510
  return identity;
494
511
  } catch { await delay(200); }
495
512
  }
496
513
  if (child.pid) {
497
- try { await this.stop(processIdentity(child.pid)); } catch { throw fixedError("refresh_start_cleanup_failed"); }
514
+ const identity = processIdentity(child.pid);
515
+ if (identity) {
516
+ try { await this.stop(identity); } catch { throw fixedError("refresh_start_cleanup_failed"); }
517
+ }
498
518
  }
499
519
  throw fixedError("refresh_start_timeout");
500
520
  },