@tokenoftrust/cli 2.2.14 → 2.2.16

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/cli",
3
- "version": "2.2.14",
3
+ "version": "2.2.16",
4
4
  "description": "Token of Trust developer CLI — clone a tenant store, run it locally with save→reload, and submit it for preview. Installs the `tot` command.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Token of Trust",
@@ -0,0 +1,82 @@
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", "embeds",
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
+ if (config.embeds !== undefined
78
+ && !(Array.isArray(config.embeds) && config.embeds.every((/** @type {unknown} */ s) => typeof s === "string"))) {
79
+ out.push(mk(ERROR, "config-embeds", file, '`embeds` must be an array of embed slugs, e.g. ["youtube"]'));
80
+ }
81
+ return out;
82
+ }
package/src/validate.mjs CHANGED
@@ -17,12 +17,21 @@
17
17
  */
18
18
  import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
19
19
  import { join, relative } from "node:path";
20
+ import { validateDeclaredConfig } from "./declared-config.mjs";
20
21
  import {
21
22
  foreignTenantSegments,
22
23
  parseMappingTarget,
23
24
  workspacePathForTenantRelative,
24
25
  } from "./tot-repo-mappings.mjs";
25
26
 
27
+ export {
28
+ TOT_CONFIG_KEYS,
29
+ COMPLIANCE_KEYS,
30
+ FEATURE_KEYS,
31
+ SITE_TYPES,
32
+ validateDeclaredConfig,
33
+ } from "./declared-config.mjs";
34
+
26
35
  const NUL = String.fromCharCode(0);
27
36
  export const ERROR = "error";
28
37
  export const WARN = "warn";
@@ -68,70 +77,6 @@ const hasScanExt = (p) => CONFLICT_SCAN_EXT.has((p.match(/\.[^./\\]+$/) || [""])
68
77
  const KNOWN_KINDS = new Set(["file", "tree"]);
69
78
  const REQUIRED_WORKSPACES = ["content/", "public/", "theme.json"];
70
79
 
71
- // The keys the platform reads in `.tot/config.json`. Mirrors
72
- // packages/public-runtime/src/declared-tenant-config.mjs (this package ships to npm
73
- // on its own); test/declared-config-parity.test.mjs pins the two together, so a
74
- // field the platform honors can never be refused here, and a key no surface reads
75
- // can never pass.
76
- export const TOT_CONFIG_KEYS = [
77
- "$schema", "tenant", "scope", "mappings", "hostPlatform", "sample", "seed",
78
- "siteType", "compliance", "capabilities", "features",
79
- ];
80
- // Keys other tooling has written that the platform does NOT read, with the key it does.
81
- const RENAMED_CONFIG_KEYS = { appDomain: "scope", tenantId: "tenant" };
82
- export const COMPLIANCE_KEYS = [
83
- "ruleProfile", "minAge", "nicotineWarning", "shippingRestriction", "pactAct",
84
- "adultSignature", "prop65", "purchaseLimit", "stateEligibility", "exciseTax",
85
- ];
86
- export const FEATURE_KEYS = [
87
- "catalog", "productSearch", "savedItems", "quickView", "newsletter", "commerceSitemap", "productJsonLd", "ageGate",
88
- ];
89
- export const SITE_TYPES = ["commerce", "marketing"];
90
-
91
- const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
92
-
93
- /**
94
- * Refuse what the platform would silently ignore: an unknown top-level key, an
95
- * unknown `compliance`/`features` key, or a declared field of the wrong shape. Each
96
- * one passes JSON parsing and renders nothing — on a regulated store that is a
97
- * mandated notice that never appears.
98
- * @returns {Finding[]}
99
- */
100
- export function validateDeclaredConfig(config, file = ".tot/config.json") {
101
- const out = [];
102
- for (const key of Object.keys(config)) {
103
- if (!TOT_CONFIG_KEYS.includes(key)) {
104
- const renamed = RENAMED_CONFIG_KEYS[key];
105
- out.push(mk(ERROR, "config-unknown-key", file,
106
- `\`${key}\` is not a .tot/config.json key — the platform never reads it`,
107
- renamed ? `use \`${renamed}\`` : `known keys: ${TOT_CONFIG_KEYS.join(", ")}`));
108
- }
109
- }
110
- if (config.siteType !== undefined && !SITE_TYPES.includes(config.siteType)) {
111
- out.push(mk(ERROR, "config-site-type", file, `\`siteType\` is "${config.siteType}" — must be one of ${SITE_TYPES.join(", ")}`));
112
- }
113
- /** @type {Array<[string, string[]]>} */
114
- const objectFields = [["compliance", COMPLIANCE_KEYS], ["features", FEATURE_KEYS]];
115
- for (const [field, known] of objectFields) {
116
- const value = config[field];
117
- if (value === undefined) continue;
118
- if (!isObject(value)) {
119
- out.push(mk(ERROR, `config-${field}`, file, `\`${field}\` must be an object`));
120
- continue;
121
- }
122
- for (const key of Object.keys(value)) {
123
- if (!known.includes(key)) {
124
- out.push(mk(ERROR, `config-${field}-unknown-key`, `${file} ${field}`,
125
- `\`${field}.${key}\` is not a ${field} key — nothing renders from it`,
126
- `known keys: ${known.join(", ")}`));
127
- }
128
- }
129
- }
130
- if (config.capabilities !== undefined && !isObject(config.capabilities)) {
131
- out.push(mk(ERROR, "config-capabilities", file, "`capabilities` must be an object"));
132
- }
133
- return out;
134
- }
135
80
 
136
81
  /**
137
82
  * Validate a parsed `.tot/config.json` against the shape the reconcile pipeline
@@ -279,7 +224,7 @@ function providesChrome(contentDir) {
279
224
 
280
225
  const CAPABILITY_KEYS = new Set(["cartCheckout", "ageVerification", "exciseTax"]);
281
226
 
282
- // Compact mirror of EMBED_PROVIDERS (packages/public-runtime/src/embed-catalog.mjs)
227
+ // Compact mirror of EMBED_PROVIDERS (agency-kit/tools/lib/embed-catalog.mjs)
283
228
  // for the embed-CSP shift-left scan. The published `tot` CLI bundle can't reach
284
229
  // the repo's public-runtime package at runtime, so this is a copy — but a parity
285
230
  // test (packages/public-runtime/tests/embed-catalog-parity.test.ts) pins it to
@@ -292,6 +237,18 @@ export const KNOWN_EMBEDS = [
292
237
  markers: ["webforms.pipedrive.com", "pipedriveWebForms"],
293
238
  origins: ["https://webforms.pipedrive.com"],
294
239
  },
240
+ {
241
+ slug: "youtube",
242
+ label: "YouTube video (privacy-enhanced)",
243
+ markers: ["www.youtube-nocookie.com/embed/"],
244
+ origins: ["https://www.youtube-nocookie.com"],
245
+ },
246
+ {
247
+ slug: "vimeo",
248
+ label: "Vimeo video",
249
+ markers: ["player.vimeo.com/video/"],
250
+ origins: ["https://player.vimeo.com"],
251
+ },
295
252
  ];
296
253
  const KNOWN_EMBED_SLUGS = new Set(KNOWN_EMBEDS.map((p) => p.slug));
297
254
  const EMBED_SRC_RE = /<(?:script|iframe)\b[^>]*\bsrc\s*=\s*["']([^"']+)["']/gi;
@@ -1318,10 +1275,15 @@ export function validateTenant(tenantDir, opts = {}) {
1318
1275
  // provider not opted into embeds.json, or an unknown external <script>/<iframe>
1319
1276
  // origin, is blocking; advisory (info) when there's no embeds.json to compare.
1320
1277
  {
1321
- let declaredEmbeds = null;
1278
+ // The opt-in is the `embeds` .tot/config.json declares, plus an embeds.json.
1279
+ let declaredEmbeds = Array.isArray(config?.embeds)
1280
+ ? new Set(config.embeds.filter((/** @type {unknown} */ s) => typeof s === "string"))
1281
+ : null;
1322
1282
  if (existsSync(embedsPath)) {
1323
1283
  const { value } = readJsonSafe(embedsPath);
1324
- if (Array.isArray(value)) declaredEmbeds = new Set(value.filter((s) => typeof s === "string"));
1284
+ if (Array.isArray(value)) {
1285
+ declaredEmbeds = new Set([...(declaredEmbeds ?? []), ...value.filter((s) => typeof s === "string")]);
1286
+ }
1325
1287
  }
1326
1288
  for (const file of walk(contentDir, (p) => p.endsWith(".html"))) {
1327
1289
  const { knownSlugs, unknownOrigins } = scanHtmlForEmbeds(readFileSync(file, "utf8"));