@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.
- package/apps/storefront/src/config/declaredTenantConfig.ts +9 -3
- package/apps/storefront/src/middleware/renderChain.ts +5 -3
- package/package.json +1 -1
- package/packages/cli/src/declared-config.mjs +78 -0
- package/packages/public-runtime/src/csp.ts +2 -2
- package/packages/public-runtime/src/tenant.ts +8 -2
- package/scripts/dev/standalone-graft-plan.mjs +2 -2
- package/scripts/tenant/validate.mjs +3 -0
|
@@ -80,15 +80,21 @@ export async function versionDeclaredTenantConfig(
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
/**
|
|
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
|
-
|
|
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 .
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
//
|
|
49
|
-
//
|
|
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
|