@tokenoftrust/storefront-runner 2.2.111 → 2.2.113

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.
@@ -220,8 +220,8 @@
220
220
  }
221
221
  }
222
222
 
223
- function showMsg(kind, text) {
224
- const el = $("msg");
223
+ function showMsg(kind, text, targetId) {
224
+ const el = $(targetId || "msg");
225
225
  el.className = "msg " + kind;
226
226
  el.replaceChildren();
227
227
  if (typeof text === "string") {
@@ -242,15 +242,21 @@
242
242
  btn.className = "secondary";
243
243
  btn.textContent = "Provision now?";
244
244
  btn.title = "Create the store repo on this environment, then retry the invite";
245
- btn.addEventListener("click", () => provisionStore(btn));
245
+ btn.addEventListener("click", () => provisionStore(btn, { successHint: "Click Send invite again." }));
246
246
  wrap.appendChild(line);
247
247
  wrap.appendChild(btn);
248
248
  showMsg("err", wrap);
249
249
  }
250
250
 
251
- async function provisionStore(btn) {
251
+ // Shared by the invite dead-end's one-shot "Provision now?" button AND the
252
+ // persistent "Repair store connection" button below — same idempotent
253
+ // POST /api/grants/provision-store, which re-asserts the repo AND its webhooks
254
+ // on every call, even when the repo is already provisioned.
255
+ async function provisionStore(btn, opts) {
256
+ opts = opts || {};
257
+ const targetId = opts.targetId || "msg";
252
258
  if (btn) btn.disabled = true;
253
- showMsg("", "Provisioning store repo…");
259
+ showMsg("", opts.pendingMessage || "Provisioning store repo…", targetId);
254
260
  try {
255
261
  const res = await fetch("/api/grants/provision-store", {
256
262
  method: "POST",
@@ -262,14 +268,15 @@
262
268
  const created = body.created ? "created" : "already present";
263
269
  showMsg(
264
270
  "ok",
265
- "Store repo " + created + " for " + (body.tenant || tenant) +
266
- ". Click Send invite again.",
271
+ "Store repo " + created + " for " + (body.tenant || tenant) + "." +
272
+ (opts.successHint ? " " + opts.successHint : ""),
273
+ targetId,
267
274
  );
268
275
  } else {
269
- showMsg("err", body.error || ("Provision failed (HTTP " + res.status + ")."));
276
+ showMsg("err", body.error || ("Provision failed (HTTP " + res.status + ")."), targetId);
270
277
  }
271
278
  } catch (e) {
272
- showMsg("err", "Provision failed: " + (e && e.message ? e.message : e));
279
+ showMsg("err", "Provision failed: " + (e && e.message ? e.message : e), targetId);
273
280
  } finally {
274
281
  if (btn) btn.disabled = false;
275
282
  }
@@ -330,5 +337,20 @@
330
337
  inviteBtn.addEventListener("click", invite);
331
338
  emailInput.addEventListener("keydown", (e) => { if (e.key === "Enter") invite(); });
332
339
  }
340
+
341
+ // Persistent repair affordance: the invite dead-end's "Provision now?" button only
342
+ // ever appears as a 409 reaction, so once a repo is `provisioned: true` that path
343
+ // can never recur — leaving no self-service way to re-run the same idempotent
344
+ // repair (e.g. when the repo's registered webhook has drifted). This button is
345
+ // always available, independent of invite state.
346
+ const repairBtn = $("repairBtn");
347
+ if (repairBtn) {
348
+ repairBtn.addEventListener("click", () => provisionStore(repairBtn, {
349
+ targetId: "repairMsg",
350
+ pendingMessage: "Repairing store connection…",
351
+ successHint: "Webhooks re-synced.",
352
+ }));
353
+ }
354
+
333
355
  loadMembers();
334
356
  })();
@@ -42,6 +42,7 @@ import {
42
42
  CSP_REPORT_GROUP,
43
43
  CSP_REPORT_PATH,
44
44
  readPublishedArtifact,
45
+ readVersionArtifact,
45
46
  themeArtifactPath,
46
47
  resolveTenantFeatures,
47
48
  resolveCapabilities,
@@ -63,6 +64,7 @@ import {
63
64
  resolveNativeGa4SandboxUrl,
64
65
  } from "@tot/public-runtime";
65
66
  import { readBakedManagedInstalls } from "@/lib/integration/bakedManagedInstalls";
67
+ import { readWithImagePreviewRewriteOverlay } from "@tot/public-runtime/image-preview-rewrite-overlay";
66
68
  import type {
67
69
  CapabilityConfig,
68
70
  ConsentSnapshot,
@@ -451,15 +453,13 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
451
453
  ? `${proto}://${host}${basePath}`
452
454
  : `https://${tenant.canonicalDomain}`;
453
455
 
454
- // Apply a published theme override for the resolved environment (store->app).
455
- // The live domain serves `live`; a gated preview serves `test`. When nothing
456
- // is published the build-time tenant theme is used unchanged.
456
+ // Apply the theme selected for this render. A candidate route reads its exact
457
+ // version; an ordinary route resolves the environment's published channel.
457
458
  //
458
- // Resolution is versioned-aware inside readPublishedArtifact: env maps to a
459
- // publish channel (live->live, test->preview); if the tenant has a channel
460
- // pointer, the artifact is resolved through the immutable site version
461
- // (channel -> versionId -> manifest -> content). Tenants with no channel
462
- // pointer fall back to the legacy per-path selection — no behavior change.
459
+ // readPublishedArtifact maps env to a publish channel (live->live,
460
+ // test->preview) and resolves it through the immutable site version. A
461
+ // candidate must bypass that pointer so a reviewer sees the candidate's own
462
+ // theme, capabilities, and embed opt-in with its content.
463
463
  // Request-scoped read memo: every content read resolves the same pointer and
464
464
  // version manifest, and a catalog render reads one artifact per catalog file.
465
465
  const kvReads = new Map<string, Promise<string | null>>();
@@ -473,6 +473,15 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
473
473
  return read;
474
474
  }
475
475
  : async () => null;
476
+ const readRuntimeArtifact = (path: string) => candidatePin
477
+ ? readWithImagePreviewRewriteOverlay(
478
+ kvGet,
479
+ tenant.tenant_id,
480
+ candidatePin.versionId,
481
+ path,
482
+ () => readVersionArtifact(kvGet, tenant.tenant_id, candidatePin.versionId, path),
483
+ )
484
+ : readPublishedArtifact(kvGet, tenant.tenant_id, customization.env, path);
476
485
 
477
486
  // Self-heal a DANGLING channel pointer: the pointer for this env's channel may
478
487
  // name a version that was never built (or whose manifest was evicted).
@@ -480,7 +489,7 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
480
489
  // the surface; here we do the once-per-request detection to ENQUEUE a rebuild of
481
490
  // the dangling version. Fail-open: a KV hiccup never blocks the render (the reads
482
491
  // below still run).
483
- if (kv) {
492
+ if (kv && !candidatePin) {
484
493
  const channel = channelFromEnv(customization.env);
485
494
  await resolveServableChannelVersion(kvGet, tenant.tenant_id, channel, ({ versionId }) => {
486
495
  recordActivity({
@@ -508,12 +517,7 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
508
517
  draftClaims && draftClaims.tenantId === tenant.tenant_id ? draftClaims : null;
509
518
 
510
519
  let theme = mergeTheme(referenceTheme, tenant.theme_tokens);
511
- const publishedTheme = await readPublishedArtifact(
512
- kvGet,
513
- tenant.tenant_id,
514
- customization.env,
515
- themeArtifactPath(tenant.tenant_id),
516
- );
520
+ const publishedTheme = await readRuntimeArtifact(themeArtifactPath(tenant.tenant_id));
517
521
  if (publishedTheme) {
518
522
  try {
519
523
  theme = mergeTheme(theme, JSON.parse(publishedTheme) as DeepPartial<ThemeTokens>);
@@ -539,12 +543,7 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
539
543
  // commit (localhost=working tree, preview=PR head, live=promoted). So a
540
544
  // capability change is PR-gated and separable per environment.
541
545
  let publishedCaps: Partial<CapabilityConfig> | null = null;
542
- const publishedCapsRaw = await readPublishedArtifact(
543
- kvGet,
544
- tenant.tenant_id,
545
- customization.env,
546
- capabilitiesArtifactPath(tenant.tenant_id),
547
- );
546
+ const publishedCapsRaw = await readRuntimeArtifact(capabilitiesArtifactPath(tenant.tenant_id));
548
547
  if (publishedCapsRaw) {
549
548
  try {
550
549
  publishedCaps = JSON.parse(publishedCapsRaw) as Partial<CapabilityConfig>;
@@ -560,12 +559,7 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
560
559
  // tenant enables a catalogued embed by committing embeds.json — no platform
561
560
  // change — but can never introduce a non-catalog origin.
562
561
  let publishedEmbeds: string[] | null = null;
563
- const publishedEmbedsRaw = await readPublishedArtifact(
564
- kvGet,
565
- tenant.tenant_id,
566
- customization.env,
567
- embedsArtifactPath(tenant.tenant_id),
568
- );
562
+ const publishedEmbedsRaw = await readRuntimeArtifact(embedsArtifactPath(tenant.tenant_id));
569
563
  if (publishedEmbedsRaw) {
570
564
  try {
571
565
  const parsed = JSON.parse(publishedEmbedsRaw);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.2.111",
3
+ "version": "2.2.113",
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",