@tokenoftrust/storefront-runner 2.0.54 → 2.0.56

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.
@@ -128,7 +128,56 @@
128
128
  function showMsg(kind, text) {
129
129
  const el = $("msg");
130
130
  el.className = "msg " + kind;
131
- el.textContent = text;
131
+ el.replaceChildren();
132
+ if (typeof text === "string") {
133
+ el.textContent = text;
134
+ return;
135
+ }
136
+ if (text && text.nodeType) el.appendChild(text);
137
+ }
138
+
139
+ function showProvisionPrompt(message) {
140
+ const wrap = document.createElement("div");
141
+ const line = document.createElement("p");
142
+ line.style.margin = "0 0 .6rem";
143
+ line.textContent = message || "This store's repo isn't provisioned yet.";
144
+ const btn = document.createElement("button");
145
+ btn.type = "button";
146
+ btn.id = "provisionBtn";
147
+ btn.className = "secondary";
148
+ btn.textContent = "Provision now?";
149
+ btn.title = "Create the store repo on this environment, then retry the invite";
150
+ btn.addEventListener("click", () => provisionStore(btn));
151
+ wrap.appendChild(line);
152
+ wrap.appendChild(btn);
153
+ showMsg("err", wrap);
154
+ }
155
+
156
+ async function provisionStore(btn) {
157
+ if (btn) btn.disabled = true;
158
+ showMsg("", "Provisioning store repo…");
159
+ try {
160
+ const res = await fetch("/api/grants/provision-store", {
161
+ method: "POST",
162
+ headers: { "content-type": "application/json", accept: "application/json" },
163
+ body: JSON.stringify({ tenant, confirm: true }),
164
+ });
165
+ const body = await res.json().catch(() => ({}));
166
+ if (res.ok) {
167
+ const created = body.created ? "created" : "already present";
168
+ showMsg(
169
+ "ok",
170
+ "Store repo " + created + " for " + (body.tenant || tenant) +
171
+ ". Click Send invite again.",
172
+ );
173
+ } else {
174
+ showMsg("err", body.error || ("Provision failed (HTTP " + res.status + ")."));
175
+ }
176
+ } catch (e) {
177
+ showMsg("err", "Provision failed: " + (e && e.message ? e.message : e));
178
+ } finally {
179
+ if (btn) btn.disabled = false;
180
+ }
132
181
  }
133
182
 
134
183
  async function invite() {
@@ -161,6 +210,11 @@
161
210
  $("email").value = "";
162
211
  if (noteEl) noteEl.value = "";
163
212
  loadMembers();
213
+ } else if (
214
+ res.status === 409 &&
215
+ (body.code === "store-not-provisioned" || body.reason === "storeNotProvisioned")
216
+ ) {
217
+ showProvisionPrompt(body.error);
164
218
  } else {
165
219
  showMsg("err", body.error || ("Invite failed (HTTP " + res.status + ")."));
166
220
  }
@@ -1,40 +1,10 @@
1
- /* ============================================================================
2
- Shared commerce-chrome stylesheet — ONE platform-shipped, token-driven layer
3
- (workstream shared-ecommerce-widgets, unit F2 `shared-chrome-css`).
1
+ /* Shared commerce chrome. Theme values come from the token contract in
2
+ docs/architecture/token-contract.md; tenant-private brand variables and
3
+ hard-coded brand colors do not belong here. Dark surfaces derive from
4
+ --color-text and --color-primary-contrast.
4
5
 
5
- Generalizes the divergent per-tenant `public/pages/mkt.css` files (some tenants
6
- shared a byte-identical base sheet; others carried a richer variant with .mega /
7
- .member-cue / .announce-bar / .age-gate / .trust-marks) into a SINGLE stylesheet
8
- every storefront links and themes via
9
- tokens only. Per decisions raw-chrome-single-stylesheet + materialization-compat.
10
-
11
- CONTRACT-TOKEN-ONLY (docs/architecture/token-contract.md, unit F1). Every value
12
- here reads the shared `--color-*` / `--font-*` / `--radius-*` / `--shadow-*` /
13
- `--container-max` vocabulary and NO tenant-private brand var (never `--navy`,
14
- `--brand-red`, unprefixed `--sale`/`--ink`/`--surface`), and no hard-coded brand
15
- hex. On-dark surfaces (footer, age-gate overlay, cat cards) derive from
16
- `--color-text` (the deep ink surface) + `--color-primary-contrast` via
17
- `color-mix`, so a tenant reskins the whole chrome by supplying tokens/aliases.
18
-
19
- Token mapping from the old per-tenant vars (applied by the integration units
20
- that alias each tenant's brand vocabulary onto the contract):
21
- --navy / --brand-red -> --color-primary (nav active, cart, CTA fill)
22
- --navy-dark -> --color-primary-hover
23
- --sale -> --color-sale (flag / NEW / marquee emphasis)
24
- --surface / --surface-2 / --border / --muted -> --color-{surface,surface-2,border,muted}
25
- --ink / --ink-2 -> --color-text
26
- --brand-blue -> --color-accent (member cue, promo/trust spark)
27
- --container -> --container-max
28
- footer / age-gate ink surfaces -> --color-text (+ --color-primary-contrast)
29
-
30
- DELIVERY: platform static asset, served at /shared/commerce-chrome.css (root,
31
- global — like /brand, /js). It is a LINKED sheet, not inline: commerce routes
32
- (/products, /collections) carry linked <link> sheets but strip inline <style>,
33
- which is exactly the PVC breakage this closes. The canonical chrome partial (F3)
34
- links it; per-tenant chrome retires its copied mkt.css at integration (I1/I2/I3).
35
- rawMarketingHtml.prefixTenantLinks exempts /shared/ so the link survives
36
- path-prefix tenant routing (as it already does for /tenants/).
37
- ========================================================================== */
6
+ This linked platform asset is served at /shared/commerce-chrome.css.
7
+ rawMarketingHtml.prefixTenantLinks leaves /shared/ paths unprefixed. */
38
8
 
39
9
  /* --- Layout --------------------------------------------------------------- */
40
10
  /* Clip (not hidden) so a closed off-canvas mobile drawer never creates a
@@ -132,7 +102,7 @@ body { overflow-x: clip; }
132
102
  .site-header { position: sticky; top: 0; z-index: 100; background: var(--color-surface); border-bottom: 1px solid var(--color-border); }
133
103
  .site-header .header-row { gap: clamp(12px, 3vw, 40px); }
134
104
  .header-row { display: flex; flex-wrap: wrap; align-items: center; gap: clamp(12px, 2.5vw, 32px); padding-block: 14px; }
135
- /* Logo: image mark (bigd/giant) or text wordmark (PVC) both supported. */
105
+ /* Logo: image mark and text wordmark are both supported. */
136
106
  /* object-fit is load-bearing, not polish: with height pinned and width auto, a logo whose
137
107
  box is capped by max-width is otherwise STRETCHED to fill it. Measured 2026-09-21 on a
138
108
  real tenant: a 500x196 logo (ratio 2.55) rendered at 154x34 (ratio 4.53) on mobile.
@@ -196,11 +166,11 @@ body { overflow-x: clip; }
196
166
  /* --- Primary nav + mega-menu ---------------------------------------------- */
197
167
  .primary-nav { flex: 1; }
198
168
  .primary-nav > ul { display: flex; flex-wrap: wrap; align-items: center; gap: clamp(10px, 1.6vw, 22px); }
199
- /* Flat links (bigd/giant) */
169
+ /* Flat primary links */
200
170
  .primary-nav a { font-weight: 600; font-size: .92rem; color: var(--color-text); padding: 6px 2px; border-bottom: 2px solid transparent; transition: color .15s, border-color .15s; }
201
171
  .primary-nav a:hover { color: var(--color-primary); border-color: var(--color-primary); }
202
172
  .primary-nav a.is-flag { color: var(--color-sale); }
203
- /* Nav items with dropdowns (PVC mega) */
173
+ /* Nav items with dropdowns */
204
174
  .primary-nav .nav-item { position: relative; }
205
175
  .primary-nav .nav-item > a { display: inline-flex; align-items: center; gap: 5px; font-weight: 600; font-size: .92rem; color: var(--color-text); padding: 14px 2px; border-bottom: 2px solid transparent; transition: color .15s, border-color .15s; }
206
176
  .primary-nav .nav-item > a:hover, .primary-nav .nav-item:focus-within > a { color: var(--color-primary); border-color: var(--color-primary); }
@@ -211,7 +181,7 @@ body { overflow-x: clip; }
211
181
  display: inline-block; background: var(--color-sale); color: var(--color-primary-contrast); font-size: .58rem; font-weight: 800;
212
182
  letter-spacing: .06em; padding: 2px 5px; border-radius: var(--radius-sm); margin-left: 6px; vertical-align: middle;
213
183
  }
214
- /* Second-row nav bar (bigd) + nav-bar container (PVC) */
184
+ /* Second-row nav bar and its container */
215
185
  .primary-nav-bar { border-top: 1px solid var(--color-border); background: var(--color-surface); }
216
186
  .primary-nav-bar .container { padding-block: 4px; }
217
187
  .primary-nav-bar .primary-nav > ul { gap: clamp(10px, 1.4vw, 20px); }
@@ -352,7 +322,7 @@ a.article-card__tag:hover { text-decoration: underline; }
352
322
  .footer-col ul { display: flex; flex-direction: column; gap: 9px; }
353
323
  .footer-col a { color: color-mix(in srgb, var(--color-primary-contrast) 78%, transparent); font-size: .92rem; }
354
324
  .footer-col a:hover { color: var(--color-primary-contrast); text-decoration: underline; }
355
- /* Newsletter signup (PVC) */
325
+ /* Newsletter signup */
356
326
  .footer-news { margin-top: 14px; }
357
327
  .footer-news__row { display: flex; gap: 8px; }
358
328
  .footer-news input[type="email"] { flex: 1; min-width: 0; border: 1px solid color-mix(in srgb, var(--color-primary-contrast) 22%, transparent); border-radius: var(--radius-md); padding: 10px 12px; font-size: .88rem; background: color-mix(in srgb, var(--color-text) 88%, var(--color-primary-contrast)); color: var(--color-primary-contrast); }
@@ -391,7 +361,7 @@ a.article-card__tag:hover { text-decoration: underline; }
391
361
  }
392
362
  .pay-badges { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
393
363
  .pay-badges span { font-size: .62rem; font-weight: 700; letter-spacing: .04em; color: color-mix(in srgb, var(--color-primary-contrast) 82%, transparent); border: 1px solid color-mix(in srgb, var(--color-primary-contrast) 22%, transparent); border-radius: var(--radius-sm); padding: 4px 7px; }
394
- /* Trust marks strip (PVC) */
364
+ /* Trust marks strip */
395
365
  .trust-marks { display: flex; flex-wrap: wrap; gap: 8px 18px; margin-top: 30px; padding-top: 20px; border-top: 1px solid color-mix(in srgb, var(--color-primary-contrast) 12%, transparent); }
396
366
  .trust-marks span { display: inline-flex; align-items: center; gap: 7px; font-size: .82rem; font-weight: 600; color: color-mix(in srgb, var(--color-primary-contrast) 82%, transparent); }
397
367
  .trust-marks svg { width: 17px; height: 17px; stroke: var(--color-accent); fill: none; stroke-width: 1.8; }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * TENANT_CACHE projection for staff/operator store provision.
3
+ *
4
+ * Lives under src/config so pages/lib may call it without crossing the
5
+ * tenancy-resolver-fence (only config/middleware may import kvTenantResolver).
6
+ */
7
+ import type { TenantConfig } from "@tot/public-runtime";
8
+ import { putTenant } from "@/config/kvTenantResolver";
9
+
10
+ export async function projectProvisionedTenant(
11
+ kv: KVNamespace,
12
+ config: TenantConfig,
13
+ repoMeta: { owner: string; repo: string; cloneUrl: string | null },
14
+ ): Promise<void> {
15
+ await putTenant(kv, config);
16
+ await kv.put(
17
+ `repo:${config.tenant_id}`,
18
+ JSON.stringify({
19
+ owner: repoMeta.owner,
20
+ repo: repoMeta.repo,
21
+ cloneUrl: repoMeta.cloneUrl,
22
+ }),
23
+ );
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.0.54",
3
+ "version": "2.0.56",
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",
@@ -479,6 +479,14 @@ export const ACTION_CATALOG = {
479
479
  argsAllow: ["resource", "capability"], renderedAllow: [],
480
480
  desc: "A ToT-staff operator entered a vendor tenant (impersonation boundary).",
481
481
  },
482
+ "dashboard.store_provision": {
483
+ // grants/provision-store.ts — "Provision now?" from the Team invite dead-end when
484
+ // the store repo is not yet provisioned. Same near-side admission as team-invite
485
+ // (membership → merchant/dev, staff selection → admin). Args are counts/ids only.
486
+ actors: ["admin", "merchant", "dev"], source: "ui", sinks: LIFECYCLE,
487
+ argsAllow: ["resource"], renderedAllow: ["created", "repo"],
488
+ desc: "A store's forge repo was provisioned from the Team console.",
489
+ },
482
490
  "cache.purged": {
483
491
  // cache-purge.ts — bumps a tenant's edge page-cache version (the ETL load step calls
484
492
  // it per tenant post-migration). Bearer-secret service call (no session) → emitted