@tokenoftrust/storefront-runner 2.2.90 → 2.2.91

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.
@@ -80,15 +80,21 @@ export async function versionDeclaredTenantConfig(
80
80
  }
81
81
  }
82
82
 
83
- /** The tenant as it renders in `env`: its record with every declaration applied. */
83
+ /**
84
+ * The tenant as it renders: its record with every declaration applied. A render
85
+ * pinned to one version (a candidate under review) takes that version's
86
+ * declaration; otherwise the environment's channel supplies it.
87
+ */
84
88
  export async function withDeclaredTenantConfig(
85
89
  tenant: TenantConfig,
86
90
  kvGet: (key: string) => Promise<string | null>,
87
- env: CustomizationEnv,
91
+ source: { env: CustomizationEnv; pinnedVersionId?: string },
88
92
  ): Promise<TenantConfig> {
89
93
  const fromCheckout = applyDeclaredTenantConfig(tenant, checkoutDeclaredTenantConfig(tenant.tenant_id));
90
94
  return applyDeclaredTenantConfig(
91
95
  fromCheckout,
92
- await publishedDeclaredTenantConfig(kvGet, tenant.tenant_id, env),
96
+ source.pinnedVersionId
97
+ ? await versionDeclaredTenantConfig(kvGet, tenant.tenant_id, source.pinnedVersionId)
98
+ : await publishedDeclaredTenantConfig(kvGet, tenant.tenant_id, source.env),
93
99
  );
94
100
  }
@@ -235,11 +235,13 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
235
235
  // The tenant as it renders in THIS environment: its record with the declared
236
236
  // `.tot/config.json` fields applied (compliance, siteType, capabilities,
237
237
  // features). Everything below — static bundle, notices, gates, features,
238
- // capabilities — reads this, never the bare record.
238
+ // capabilities — reads this, never the bare record. A candidate-pinned render
239
+ // takes the candidate version's declaration, so a reviewer sees the notices the
240
+ // change under review declares.
239
241
  const tenant = await withDeclaredTenantConfig(
240
242
  tenantRecord,
241
243
  kv ? (key: string) => kv.get(key) : async () => null,
242
- customization.env,
244
+ { env: customization.env, ...(candidatePin ? { pinnedVersionId: candidatePin.versionId } : {}) },
243
245
  );
244
246
  const publicationTimestamp = publicationSource.status === "selected"
245
247
  ? publicationSource.publicationTimestamp
@@ -752,7 +754,7 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
752
754
  // exactly the placements that rendered on THIS page, never a wildcard.
753
755
  const widgetCsp = appWidgetCspAdditions(locals.activeWidgetPlacements ?? []);
754
756
  // Third-party marketing embeds (e.g. a Pipedrive form) the tenant opted into
755
- // via .tot/config.json `embeds:`. Only scan the HTML body when the tenant
757
+ // via its published embeds.json. Only scan the HTML body when the tenant
756
758
  // declared at least one embed — zero cost for everyone else — and add each
757
759
  // provider's origins ONLY if its marker is actually on THIS page.
758
760
  // Prefer an opt-in the render handler resolved for a tenant the URL router
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.2.90",
3
+ "version": "2.2.91",
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",
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The `.tot/config.json` keys the platform reads, and the refusal of everything
3
+ * else. Shared by `tot validate` (src/validate.mjs) and the monorepo/dev-loop
4
+ * validator (scripts/tenant/validate.mjs), so both refuse the same keys.
5
+ *
6
+ * Mirrors packages/public-runtime/src/declared-tenant-config.mjs (this package ships
7
+ * to npm on its own); test/declared-config-parity.test.mjs pins the two together, so
8
+ * a field the platform honors can never be refused here, and a key no surface reads
9
+ * can never pass. Dependency-free: the storefront runner copies this file as-is.
10
+ */
11
+
12
+ export const TOT_CONFIG_KEYS = [
13
+ "$schema", "tenant", "scope", "mappings", "hostPlatform", "sample", "seed",
14
+ "siteType", "compliance", "capabilities", "features",
15
+ ];
16
+ // Keys other tooling has written that the platform does NOT read, with the key it does.
17
+ const RENAMED_CONFIG_KEYS = { appDomain: "scope", tenantId: "tenant" };
18
+ export const COMPLIANCE_KEYS = [
19
+ "ruleProfile", "minAge", "nicotineWarning", "shippingRestriction", "pactAct",
20
+ "adultSignature", "prop65", "purchaseLimit", "stateEligibility", "exciseTax",
21
+ ];
22
+ export const FEATURE_KEYS = [
23
+ "catalog", "productSearch", "savedItems", "quickView", "newsletter", "commerceSitemap", "productJsonLd", "ageGate",
24
+ ];
25
+ export const SITE_TYPES = ["commerce", "marketing"];
26
+
27
+ const ERROR = "error";
28
+ const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
29
+
30
+ /** @typedef {{level:string, rule:string, file:string, message:string, fix?:string}} Finding */
31
+ function mk(level, rule, file, message, fix) {
32
+ return { level, rule, file, message, fix };
33
+ }
34
+
35
+ /**
36
+ * Refuse what the platform would silently ignore: an unknown top-level key, an
37
+ * unknown `compliance`/`features` key, or a declared field of the wrong shape. Each
38
+ * one passes JSON parsing and renders nothing — on a regulated store that is a
39
+ * mandated notice that never appears.
40
+ * @param {Record<string, any>} config parsed `.tot/config.json` (an object)
41
+ * @param {string} [file]
42
+ * @returns {Finding[]}
43
+ */
44
+ export function validateDeclaredConfig(config, file = ".tot/config.json") {
45
+ const out = [];
46
+ for (const key of Object.keys(config)) {
47
+ if (!TOT_CONFIG_KEYS.includes(key)) {
48
+ const renamed = RENAMED_CONFIG_KEYS[/** @type {keyof typeof RENAMED_CONFIG_KEYS} */ (key)];
49
+ out.push(mk(ERROR, "config-unknown-key", file,
50
+ `\`${key}\` is not a .tot/config.json key — the platform never reads it`,
51
+ renamed ? `use \`${renamed}\`` : `known keys: ${TOT_CONFIG_KEYS.join(", ")}`));
52
+ }
53
+ }
54
+ if (config.siteType !== undefined && !SITE_TYPES.includes(config.siteType)) {
55
+ out.push(mk(ERROR, "config-site-type", file, `\`siteType\` is "${config.siteType}" — must be one of ${SITE_TYPES.join(", ")}`));
56
+ }
57
+ /** @type {Array<[string, string[]]>} */
58
+ const objectFields = [["compliance", COMPLIANCE_KEYS], ["features", FEATURE_KEYS]];
59
+ for (const [field, known] of objectFields) {
60
+ const value = config[field];
61
+ if (value === undefined) continue;
62
+ if (!isObject(value)) {
63
+ out.push(mk(ERROR, `config-${field}`, file, `\`${field}\` must be an object`));
64
+ continue;
65
+ }
66
+ for (const key of Object.keys(value)) {
67
+ if (!known.includes(key)) {
68
+ out.push(mk(ERROR, `config-${field}-unknown-key`, `${file} ${field}`,
69
+ `\`${field}.${key}\` is not a ${field} key — nothing renders from it`,
70
+ `known keys: ${known.join(", ")}`));
71
+ }
72
+ }
73
+ }
74
+ if (config.capabilities !== undefined && !isObject(config.capabilities)) {
75
+ out.push(mk(ERROR, "config-capabilities", file, "`capabilities` must be an object"));
76
+ }
77
+ return out;
78
+ }
@@ -103,7 +103,7 @@ export const STOREFRONT_CSP_ALLOWLIST = {
103
103
 
104
104
  /**
105
105
  * Curated catalog of known third-party embeds a tenant may opt into. Keyed by a
106
- * stable provider slug the tenant lists in `.tot/config.json` `embeds:`; the
106
+ * stable provider slug the tenant lists in its `embeds.json`; the
107
107
  * platform (never the tenant) owns the exact origins each provider needs, so a
108
108
  * tenant can enable "pipedrive" without ever hand-writing a CSP origin — and a
109
109
  * novel provider requires a reviewed catalog entry here, not a config edit.
@@ -170,7 +170,7 @@ export interface EmbedCspAdditions {
170
170
  * CSP additions for the third-party embeds ACTUALLY rendered on this page.
171
171
  *
172
172
  * An origin is added only when BOTH hold: (1) the tenant opted the provider in
173
- * via `.tot/config.json` `embeds:` (audit trail + platform review of novel
173
+ * via its `embeds.json` (audit trail + platform review of novel
174
174
  * providers), and (2) the provider's marker appears in the rendered HTML (so a
175
175
  * page that doesn't embed the form never carries the origin). This keeps the
176
176
  * strict policy tenant- AND page-scoped — never a blanket global relaxation.
@@ -503,10 +503,16 @@ export interface TenantConfig {
503
503
  * the pages that actually contain it (see `embedCspAdditions`). A provider not
504
504
  * in the catalog needs a reviewed platform addition, not a config edit — so
505
505
  * this stays an auditable allow-list, never an escape hatch to arbitrary hosts.
506
- * Declared in the tenant's platform config alongside `siteType`/`capabilities`.
506
+ * The platform default comes from tenant-embed-defaults.mjs; a tenant opts in
507
+ * itself by committing `embeds.json` (see `resolveEffectiveEmbeds`).
507
508
  */
508
509
  embeds?: string[];
509
- /** Regulated-commerce affordances; undefined for unregulated tenants. */
510
+ /**
511
+ * Regulated-commerce affordances; undefined for unregulated tenants. A tenant
512
+ * declares these in its `.tot/config.json`; every render reads them through
513
+ * `applyDeclaredTenantConfig` (declared-tenant-config.mjs), never off the bare
514
+ * tenant record alone.
515
+ */
510
516
  compliance?: ComplianceConfig;
511
517
  /**
512
518
  * Per-tenant newsletter compliance/sender config. Sourced when a digest is
@@ -45,8 +45,8 @@ export function standaloneGraftPlan(tenant, mappings) {
45
45
  // `.tot/config.json` is never itself one of the declared `mappings` (those map
46
46
  // content/public/theme.json) but IS the tenant's own declared platform config
47
47
  // — siteType/compliance/capabilities/features (see
48
- // apps/storefront/src/config/devTenantSeed.ts TOT_CONFIG_TENANT_FIELDS and
49
- // resolver.ts declaredConfigs()) — the same file
48
+ // packages/public-runtime/src/declared-tenant-config.mjs, applied by
49
+ // apps/storefront/src/config/declaredTenantConfig.ts) — the same file
50
50
  // scripts/tenant/materialize-tenant-content.mjs copies for a Gitea-sourced
51
51
  // tenant. Graft it unconditionally so a regulated store's mandated notices
52
52
  // resolve from the SAME declared config locally as they would once published.
@@ -44,6 +44,7 @@ import {
44
44
  workspacePathForTenantRelative,
45
45
  } from "../../agency-kit/tools/lib/tot-repo-mappings.mjs";
46
46
  import { validateVisualParityClosure } from "../../packages/cli/src/visual-parity.mjs";
47
+ import { validateDeclaredConfig } from "../../packages/cli/src/declared-config.mjs";
47
48
 
48
49
  /** Catalogued embed slugs a tenant may opt into (mirror of EMBED_PROVIDERS). */
49
50
  const KNOWN_EMBED_SLUGS = new Set(KNOWN_EMBEDS.map((p) => p.slug));
@@ -115,6 +116,8 @@ export function validateConfigShape(config, file = ".tot/config.json") {
115
116
  if (typeof config.tenant !== "string" || !config.tenant) {
116
117
  out.push(mk(ERROR, "config-tenant", file, "`tenant` must be a non-empty string"));
117
118
  }
119
+ // The same refusal `tot validate` applies: a key no surface reads never passes.
120
+ out.push(...validateDeclaredConfig(config, file));
118
121
  if (!Array.isArray(config.mappings)) {
119
122
  out.push(mk(ERROR, "config-mappings", file, "`mappings` must be an array", "seed shape: [{workspace,repo,kind}]"));
120
123
  return out; // nothing more to check without the array