@tokenoftrust/cli 2.0.1 → 2.0.2
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/accept.mjs +1 -1
- package/src/commands/preview-doctor.mjs +6 -2
- package/src/commands/submit.mjs +25 -8
- package/src/validate.mjs +13 -0
- package/src/viewer-session.mjs +22 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
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/accept.mjs
CHANGED
|
@@ -526,7 +526,7 @@ export async function runIntegrate(
|
|
|
526
526
|
"x-tot-capability": "ship-on-behalf",
|
|
527
527
|
};
|
|
528
528
|
} else {
|
|
529
|
-
const viewer = await resolveViewer({ tenant, env, fetchImpl });
|
|
529
|
+
const viewer = await resolveViewer({ tenant, env, fetchImpl, storefrontUrl });
|
|
530
530
|
if (!viewer.ok) {
|
|
531
531
|
console.error(fail(viewer.message, viewer.hint));
|
|
532
532
|
return 2;
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* generic storefront origin (from PREVIEW_RECONCILE_SECRET / GRANTS_ADMIN_SECRET
|
|
19
19
|
* / TOT_OPERATOR_SECRET, or `--secret`).
|
|
20
20
|
* - VIEWER SESSION (an invited developer with no secret): mint a `tot_session`
|
|
21
|
-
* from their own `tot login
|
|
21
|
+
* from their own `tot login`, routed through the shared storefront origin under
|
|
22
|
+
* the tenant's path prefix (`resolveViewerTransport`).
|
|
22
23
|
* A caller who cannot see the accept queue cannot get a diagnosis of it — an auth
|
|
23
24
|
* refusal (401/403) is relayed as a clean house-style failure, not a wrapped verdict.
|
|
24
25
|
*
|
|
@@ -126,7 +127,10 @@ export async function runDoctor(
|
|
|
126
127
|
"x-tot-capability": "ship-on-behalf",
|
|
127
128
|
};
|
|
128
129
|
} else {
|
|
129
|
-
|
|
130
|
+
// `--url` steers BOTH transports. It previously reached only the operator-secret
|
|
131
|
+
// branch, so a developer diagnosing their own tenant had no way to redirect the
|
|
132
|
+
// command and the flag read as ignored.
|
|
133
|
+
const viewer = await resolveViewer({ tenant, env, fetchImpl, storefrontUrl });
|
|
130
134
|
if (!viewer.ok) {
|
|
131
135
|
console.error(fail(viewer.message, viewer.hint));
|
|
132
136
|
return 2;
|
package/src/commands/submit.mjs
CHANGED
|
@@ -1356,15 +1356,32 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
|
|
|
1356
1356
|
|
|
1357
1357
|
// 1. validate locally — refuse on errors.
|
|
1358
1358
|
if (!args.skipValidate) {
|
|
1359
|
+
/** Warnings that would otherwise be swallowed, but describe a defect that ships
|
|
1360
|
+
* looking healthy: [rule, headline, what it costs if ignored]. */
|
|
1361
|
+
const LOUD_ADVISORY_RULES = [
|
|
1362
|
+
[
|
|
1363
|
+
"git-conflict-markers",
|
|
1364
|
+
"git conflict markers in submitted content — an unfinished merge/rebase?",
|
|
1365
|
+
"The preview will still build, but it will serve the broken markers. Resolve before shipping.",
|
|
1366
|
+
],
|
|
1367
|
+
[
|
|
1368
|
+
"duplicate-skip-link",
|
|
1369
|
+
"duplicate skip link — your chrome already supplies one",
|
|
1370
|
+
"The page renders fine and every automated check passes; a screen-reader user hears it twice.",
|
|
1371
|
+
],
|
|
1372
|
+
];
|
|
1359
1373
|
const { ok, findings } = validateTenant(workspace, { tenantId: tenant, scope: ctx.config?.scope });
|
|
1360
|
-
// Advisory but LOUD
|
|
1361
|
-
//
|
|
1362
|
-
//
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1374
|
+
// Advisory but LOUD. Warnings are otherwise swallowed on the ok path, which is
|
|
1375
|
+
// wrong for defects that SHIP SILENTLY: the preview builds, the console is clean,
|
|
1376
|
+
// reconcile and compliance pass, and the flaw only surfaces to whoever reads the
|
|
1377
|
+
// rendered page. Those get surfaced here — never blocking, since none of them
|
|
1378
|
+
// makes the candidate unservable.
|
|
1379
|
+
for (const [rule, headline, consequence] of LOUD_ADVISORY_RULES) {
|
|
1380
|
+
const hits = findings.filter((f) => f.rule === rule);
|
|
1381
|
+
if (!hits.length) continue;
|
|
1382
|
+
console.error(`\n⚠ ${headline} (${hits.length} file(s))`);
|
|
1383
|
+
for (const f of hits) console.error(` ⚠ ${f.file} — ${f.message}`);
|
|
1384
|
+
console.error(` ${consequence}\n`);
|
|
1368
1385
|
}
|
|
1369
1386
|
if (!ok) {
|
|
1370
1387
|
const errs = findings.filter((f) => f.level === ERROR);
|
package/src/validate.mjs
CHANGED
|
@@ -452,6 +452,8 @@ function walk(dir, pred) {
|
|
|
452
452
|
const HREF_RE = /\bhref\s*=\s*"([^"]*)"/gi;
|
|
453
453
|
const SRC_RE = /\b(?:src|srcset)\s*=\s*"([^"]*)"/gi;
|
|
454
454
|
const STYLE_OPEN_WITH_ATTRS_RE = /<style\s+[^>]*>/i;
|
|
455
|
+
/** An in-page skip link: an anchor to a #main-ish target, or one carrying a skip class. */
|
|
456
|
+
const SKIP_LINK_RE = /<a\b[^>]*(?:class="[^"]*\bskip[-\w]*\b[^"]*"|href="#(?:main|content|main-content)\b")/i;
|
|
455
457
|
|
|
456
458
|
// --- platform-owned routes (framework surfaces, NOT tenant pages) ------------
|
|
457
459
|
/**
|
|
@@ -643,6 +645,17 @@ export function validateTenant(tenantDir, opts = {}) {
|
|
|
643
645
|
"make it a full <!doctype html> document, add a content/chrome.json with header.variant/header.brand.label/header.nav and footer.variant/footer.columns, or add content/chrome.html with <!--PAGE_BODY-->"),
|
|
644
646
|
);
|
|
645
647
|
}
|
|
648
|
+
// A body-only fragment is spliced INTO the tenant's chrome, and that chrome
|
|
649
|
+
// already supplies the skip link. A fragment that kept its own ships two — an
|
|
650
|
+
// a11y defect no browser reports and reconcile cannot see, so it survives every
|
|
651
|
+
// gate and lands in the assembled page.
|
|
652
|
+
if (!base && !isFullDocument(html) && hasChrome && SKIP_LINK_RE.test(html)) {
|
|
653
|
+
findings.push(
|
|
654
|
+
mk(WARN, "duplicate-skip-link", r,
|
|
655
|
+
"keeps its own skip link, but the tenant's chrome already supplies one — the assembled page will have two",
|
|
656
|
+
"delete the page-level skip link; the chrome's skipLink is the one that renders"),
|
|
657
|
+
);
|
|
658
|
+
}
|
|
646
659
|
if (STYLE_OPEN_WITH_ATTRS_RE.test(html)) {
|
|
647
660
|
findings.push(
|
|
648
661
|
mk(WARN, "style-attrs", r,
|
package/src/viewer-session.mjs
CHANGED
|
@@ -17,11 +17,22 @@ import { resolveDeveloperSession, AuthUnavailableError } from "./auth.mjs";
|
|
|
17
17
|
|
|
18
18
|
const SESSION_COOKIE = "tot_session";
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
/** Default storefront origin serving every tenant under a path prefix. */
|
|
21
|
+
export const DEFAULT_STOREFRONT_ORIGIN = "https://storefront.tokenoftrust.store";
|
|
22
|
+
|
|
23
|
+
/** The tenant's storefront base on the shared worker: `<origin>/<tenant>`.
|
|
24
|
+
*
|
|
25
|
+
* The dev-viewer admission derives the tenant from the REQUEST, not from an
|
|
26
|
+
* X-Tot-Owner header (that only steers the operator-secret path) — but it reads
|
|
27
|
+
* it from the leading path segment, which the middleware strips, so the shared
|
|
28
|
+
* origin serves every tenant. Addressing `https://<tenant>` directly only works
|
|
29
|
+
* for a tenant whose apex has already cut over to the storefront; for everyone
|
|
30
|
+
* else that host is still their legacy site and answers 404 to every
|
|
31
|
+
* `/api/...` path. Routing through the shared origin works in both cases. */
|
|
32
|
+
/** @param {string} tenant @param {string|null} [storefrontUrl] @returns {string} */
|
|
33
|
+
function tenantBase(tenant, storefrontUrl = null) {
|
|
34
|
+
const origin = String(storefrontUrl || DEFAULT_STOREFRONT_ORIGIN).trim().replace(/\/+$/, "");
|
|
35
|
+
return `${origin}/${String(tenant || "").trim().toLowerCase()}`;
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
/** Pull `tot_session=<id>` out of a (possibly comma-folded) Set-Cookie header. The
|
|
@@ -38,8 +49,11 @@ export function parseSessionCookie(setCookie) {
|
|
|
38
49
|
* { ok:false, message, hint } — a clean, actionable refusal
|
|
39
50
|
* Never throws.
|
|
40
51
|
*
|
|
52
|
+
* `storefrontUrl` overrides the storefront origin (the CLI's `--url`), so a
|
|
53
|
+
* developer can point the same command at a non-default deployment.
|
|
54
|
+
*
|
|
41
55
|
* @param {{ tenant:string, env?:NodeJS.ProcessEnv, fetchImpl?:typeof fetch,
|
|
42
|
-
* resolveDev?:typeof resolveDeveloperSession }} params
|
|
56
|
+
* storefrontUrl?:string|null, resolveDev?:typeof resolveDeveloperSession }} params
|
|
43
57
|
* @returns {Promise<
|
|
44
58
|
* { ok:true, base:string, authHeaders:Record<string,string> } |
|
|
45
59
|
* { ok:false, message:string, hint:string }
|
|
@@ -49,6 +63,7 @@ export async function resolveViewerTransport({
|
|
|
49
63
|
tenant,
|
|
50
64
|
env = process.env,
|
|
51
65
|
fetchImpl = fetch,
|
|
66
|
+
storefrontUrl = null,
|
|
52
67
|
resolveDev = resolveDeveloperSession,
|
|
53
68
|
}) {
|
|
54
69
|
// The developer's OWN MCP token (read + silently refreshed by the resolver). No
|
|
@@ -67,7 +82,7 @@ export async function resolveViewerTransport({
|
|
|
67
82
|
};
|
|
68
83
|
}
|
|
69
84
|
|
|
70
|
-
const base = tenantBase(tenant);
|
|
85
|
+
const base = tenantBase(tenant, storefrontUrl);
|
|
71
86
|
let res;
|
|
72
87
|
try {
|
|
73
88
|
res = await fetchImpl(`${base}/api/dev/cli-session`, {
|