@tokenoftrust/cli 2.2.15 → 2.2.17
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 +1 -1
- package/src/commands/dev.mjs +6 -4
- package/src/commands/report.mjs +14 -3
- package/src/declared-config.mjs +5 -1
- package/src/validate.mjs +20 -3
- package/src/visual-parity.mjs +15 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.17",
|
|
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",
|
package/src/commands/dev.mjs
CHANGED
|
@@ -286,9 +286,12 @@ function runMonorepo(ctx, argv) {
|
|
|
286
286
|
*/
|
|
287
287
|
export class NativeArtifactUnavailableError extends Error {}
|
|
288
288
|
|
|
289
|
-
/**
|
|
289
|
+
/**
|
|
290
|
+
* The local landing URL, shared by the native and Docker paths. The store's path is its canonical
|
|
291
|
+
* id, the config's `tenant` — the same id `tot dev` grafts the checkout under.
|
|
292
|
+
*/
|
|
290
293
|
export function deriveUrl(cfg, port) {
|
|
291
|
-
const domain = typeof cfg.
|
|
294
|
+
const domain = typeof cfg.tenant === "string" && cfg.tenant.includes(".") ? cfg.tenant : null;
|
|
292
295
|
return { domain, url: `http://localhost:${port}/${domain ? `${domain}/` : ""}` };
|
|
293
296
|
}
|
|
294
297
|
|
|
@@ -1835,8 +1838,7 @@ export function buildContainerPlan(workspace, args, _ctx) {
|
|
|
1835
1838
|
|
|
1836
1839
|
const image = resolveDevImage(args);
|
|
1837
1840
|
const port = String(args.port || "4321");
|
|
1838
|
-
const domain
|
|
1839
|
-
const url = `http://localhost:${port}/${domain ? `${domain}/` : ""}`;
|
|
1841
|
+
const { domain, url } = deriveUrl(cfg, port);
|
|
1840
1842
|
|
|
1841
1843
|
// docker run equivalent of scripts/dev/docker-compose.pull.yml: bind-mount the
|
|
1842
1844
|
// checkout, map the port, force polling (reliable HMR across the mount), pull
|
package/src/commands/report.mjs
CHANGED
|
@@ -233,10 +233,14 @@ function resolveStore(args, ctx) {
|
|
|
233
233
|
return null;
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
/**
|
|
236
|
+
/**
|
|
237
|
+
* The store's id from its `.tot/config.json` — the id its `/tenants/<id>/` assets bind to. Never the
|
|
238
|
+
* directory's name.
|
|
239
|
+
* @param {string} dir
|
|
240
|
+
*/
|
|
237
241
|
function storeTenant(dir) {
|
|
238
242
|
const config = readJson(join(dir, ".tot", "config.json"));
|
|
239
|
-
return typeof config?.tenant === "string" && config.tenant ? config.tenant :
|
|
243
|
+
return typeof config?.tenant === "string" && config.tenant ? config.tenant : null;
|
|
240
244
|
}
|
|
241
245
|
|
|
242
246
|
/**
|
|
@@ -272,7 +276,14 @@ export function run(argv, ctx, io = {}) {
|
|
|
272
276
|
return 2;
|
|
273
277
|
}
|
|
274
278
|
const storeExists = existsSync(store);
|
|
275
|
-
const tenant = args.tenant ?? (storeExists ? storeTenant(store) :
|
|
279
|
+
const tenant = args.tenant ?? (storeExists ? storeTenant(store) : null);
|
|
280
|
+
if (!tenant) {
|
|
281
|
+
err(`${fail(
|
|
282
|
+
`${store} names no store id`,
|
|
283
|
+
"set \"tenant\" in .tot/config.json to your store's id (the one `tot whoami` lists), or pass --tenant <id>",
|
|
284
|
+
)}\n`);
|
|
285
|
+
return 2;
|
|
286
|
+
}
|
|
276
287
|
const lint = storeExists ? validateTenant(store, { tenantId: tenant, mode: args.workspace ? "workspace" : "monorepo" }) : null;
|
|
277
288
|
const evidence = evidenceFromDir({ pagesDir, stylePath: args.style ? resolve(args.style) : null, lint });
|
|
278
289
|
if (!evidence.measured) {
|
package/src/declared-config.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
export const TOT_CONFIG_KEYS = [
|
|
13
13
|
"$schema", "tenant", "scope", "mappings", "hostPlatform", "sample", "seed",
|
|
14
|
-
"siteType", "compliance", "capabilities", "features",
|
|
14
|
+
"siteType", "compliance", "capabilities", "features", "embeds",
|
|
15
15
|
];
|
|
16
16
|
// Keys other tooling has written that the platform does NOT read, with the key it does.
|
|
17
17
|
const RENAMED_CONFIG_KEYS = { appDomain: "scope", tenantId: "tenant" };
|
|
@@ -74,5 +74,9 @@ export function validateDeclaredConfig(config, file = ".tot/config.json") {
|
|
|
74
74
|
if (config.capabilities !== undefined && !isObject(config.capabilities)) {
|
|
75
75
|
out.push(mk(ERROR, "config-capabilities", file, "`capabilities` must be an object"));
|
|
76
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
|
+
}
|
|
77
81
|
return out;
|
|
78
82
|
}
|
package/src/validate.mjs
CHANGED
|
@@ -224,7 +224,7 @@ function providesChrome(contentDir) {
|
|
|
224
224
|
|
|
225
225
|
const CAPABILITY_KEYS = new Set(["cartCheckout", "ageVerification", "exciseTax"]);
|
|
226
226
|
|
|
227
|
-
// Compact mirror of EMBED_PROVIDERS (
|
|
227
|
+
// Compact mirror of EMBED_PROVIDERS (agency-kit/tools/lib/embed-catalog.mjs)
|
|
228
228
|
// for the embed-CSP shift-left scan. The published `tot` CLI bundle can't reach
|
|
229
229
|
// the repo's public-runtime package at runtime, so this is a copy — but a parity
|
|
230
230
|
// test (packages/public-runtime/tests/embed-catalog-parity.test.ts) pins it to
|
|
@@ -237,6 +237,18 @@ export const KNOWN_EMBEDS = [
|
|
|
237
237
|
markers: ["webforms.pipedrive.com", "pipedriveWebForms"],
|
|
238
238
|
origins: ["https://webforms.pipedrive.com"],
|
|
239
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
|
+
},
|
|
240
252
|
];
|
|
241
253
|
const KNOWN_EMBED_SLUGS = new Set(KNOWN_EMBEDS.map((p) => p.slug));
|
|
242
254
|
const EMBED_SRC_RE = /<(?:script|iframe)\b[^>]*\bsrc\s*=\s*["']([^"']+)["']/gi;
|
|
@@ -1263,10 +1275,15 @@ export function validateTenant(tenantDir, opts = {}) {
|
|
|
1263
1275
|
// provider not opted into embeds.json, or an unknown external <script>/<iframe>
|
|
1264
1276
|
// origin, is blocking; advisory (info) when there's no embeds.json to compare.
|
|
1265
1277
|
{
|
|
1266
|
-
|
|
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;
|
|
1267
1282
|
if (existsSync(embedsPath)) {
|
|
1268
1283
|
const { value } = readJsonSafe(embedsPath);
|
|
1269
|
-
if (Array.isArray(value))
|
|
1284
|
+
if (Array.isArray(value)) {
|
|
1285
|
+
declaredEmbeds = new Set([...(declaredEmbeds ?? []), ...value.filter((s) => typeof s === "string")]);
|
|
1286
|
+
}
|
|
1270
1287
|
}
|
|
1271
1288
|
for (const file of walk(contentDir, (p) => p.endsWith(".html"))) {
|
|
1272
1289
|
const { knownSlugs, unknownOrigins } = scanHtmlForEmbeds(readFileSync(file, "utf8"));
|
package/src/visual-parity.mjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Read-only closure checks for the optional visual-parity migration module.
|
|
3
|
-
* The tenant checkout remains portable: this reads only content/, public
|
|
4
|
-
* .tot/visual-parity.json, never a preview host or a delivery
|
|
3
|
+
* The tenant checkout remains portable: this reads only content/, public/ and theme.json (where
|
|
4
|
+
* `.tot/config.json` maps them) and .tot/visual-parity.json, never a preview host or a delivery
|
|
5
|
+
* provider.
|
|
5
6
|
*/
|
|
6
7
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
7
8
|
import { spawnSync } from "node:child_process";
|
|
8
9
|
import { extname, join, relative } from "node:path";
|
|
10
|
+
import { workspacePathForTenantRelative } from "./tot-repo-mappings.mjs";
|
|
9
11
|
|
|
10
12
|
const ERROR = "error";
|
|
11
13
|
// Public routes implemented by the renderer. Authentication and cart URLs are
|
|
@@ -59,10 +61,10 @@ function parseConfig(tenantDir) {
|
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
/** The checkout's `.tot/config.json`, or null when absent or unreadable. */
|
|
65
|
+
function readTotConfig(tenantDir) {
|
|
63
66
|
try {
|
|
64
|
-
|
|
65
|
-
return typeof value?.tenant === "string" ? value.tenant : null;
|
|
67
|
+
return JSON.parse(readFileSync(join(tenantDir, ".tot", "config.json"), "utf8"));
|
|
66
68
|
} catch { return null; }
|
|
67
69
|
}
|
|
68
70
|
|
|
@@ -278,7 +280,11 @@ export function validateVisualParityClosure(tenantDir, options = {}) {
|
|
|
278
280
|
const findings = [];
|
|
279
281
|
const { value: config, findings: configFindings } = parseConfig(tenantDir);
|
|
280
282
|
findings.push(...configFindings);
|
|
281
|
-
|
|
283
|
+
// Files are found where the checkout's `.tot/config.json` mappings declare them, as `tot validate`
|
|
284
|
+
// and the hosted reconcile find them.
|
|
285
|
+
const totConfig = readTotConfig(tenantDir);
|
|
286
|
+
const located = (rel) => join(tenantDir, workspacePathForTenantRelative(totConfig, rel));
|
|
287
|
+
const pagesDir = located("content/pages");
|
|
282
288
|
const files = walk(pagesDir).filter((file) => file.endsWith(".json")).sort();
|
|
283
289
|
if (files.length === 0) findings.push(finding("parity-pages-missing", "content/pages/", "no page JSON files define the visual-parity route scope", "add the paired authored page records under content/pages/ before closing parity"));
|
|
284
290
|
const pages = [];
|
|
@@ -297,17 +303,17 @@ export function validateVisualParityClosure(tenantDir, options = {}) {
|
|
|
297
303
|
const routes = new Set(["/"]);
|
|
298
304
|
for (const page of pages) routes.add(normalizeRoute(page.value.slug));
|
|
299
305
|
/** @type {{findings:ParityFinding[], tenantId:string|null, publicDir:string, routes:Set<string>, allowlist:string[], decodeAssets:DecodeAsset[]}} */
|
|
300
|
-
const context = { findings, tenantId: tenantId ??
|
|
306
|
+
const context = { findings, tenantId: tenantId ?? (typeof totConfig?.tenant === "string" ? totConfig.tenant : null), publicDir: located("public"), routes, allowlist: Array.isArray(config.externalNavigationAllowlist) ? config.externalNavigationAllowlist : [], decodeAssets: [] };
|
|
301
307
|
if (!context.tenantId) findings.push(finding("parity-tenant-id", ".tot/config.json", "tenant id is required to verify /tenants/<tenant>/ assets", "set tenant in .tot/config.json or pass a resolved tenant id"));
|
|
302
308
|
for (const page of pages) inspectValue(page.value, page.file, [], context);
|
|
303
309
|
for (const name of ["home.json", "chrome.json"]) {
|
|
304
|
-
const path =
|
|
310
|
+
const path = located(`content/${name}`);
|
|
305
311
|
if (existsSync(path)) {
|
|
306
312
|
const value = readStructured(path, `content/${name}`, findings);
|
|
307
313
|
if (value != null) inspectValue(value, `content/${name}`, [], context);
|
|
308
314
|
}
|
|
309
315
|
}
|
|
310
|
-
const themePath =
|
|
316
|
+
const themePath = located("theme.json");
|
|
311
317
|
if (existsSync(themePath)) {
|
|
312
318
|
const value = readStructured(themePath, "theme.json", findings);
|
|
313
319
|
if (value != null) inspectValue(value, "theme.json", [], context);
|