@tokenoftrust/cli 1.4.0-rc.20 → 1.4.0-rc.22
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/bin/tot.mjs +52 -54
- package/package.json +6 -1
- package/src/activity.mjs +5 -4
- package/src/app-scaffold.mjs +2 -2
- package/src/auth.mjs +13 -5
- package/src/candidate-state.mjs +2 -2
- package/src/commands/accept.mjs +489 -50
- package/src/commands/app/dev.mjs +7 -3
- package/src/commands/app/index.mjs +2 -2
- package/src/commands/branches.mjs +1 -0
- package/src/commands/cleanup.mjs +2 -1
- package/src/commands/clone.mjs +51 -20
- package/src/commands/dev.mjs +30 -12
- package/src/commands/git-credential.mjs +180 -0
- package/src/commands/go-live.mjs +6 -2
- package/src/commands/grants.mjs +6 -4
- package/src/commands/link.mjs +2 -2
- package/src/commands/login.mjs +3 -4
- package/src/commands/pr.mjs +6 -5
- package/src/commands/preview-doctor.mjs +225 -0
- package/src/commands/preview.mjs +9 -1
- package/src/commands/rollback.mjs +6 -4
- package/src/commands/ship.mjs +19 -1
- package/src/commands/start.mjs +59 -11
- package/src/commands/submit.mjs +526 -68
- package/src/commands/sync.mjs +11 -0
- package/src/commands/validate.mjs +10 -4
- package/src/dev-heartbeat.mjs +2 -1
- package/src/errors.mjs +8 -4
- package/src/git-credential.mjs +185 -0
- package/src/mcp.mjs +6 -1
- package/src/merge-doctor-report.mjs +208 -0
- package/src/oauth.mjs +12 -8
- package/src/obstacle-beacon.cjs +2 -2
- package/src/obstacle.mjs +1 -1
- package/src/plan.mjs +3 -3
- package/src/sample.mjs +3 -3
- package/src/validate.mjs +56 -0
- package/src/viewer-session.mjs +118 -0
package/src/validate.mjs
CHANGED
|
@@ -27,6 +27,38 @@ function mk(level, rule, file, message, fix) {
|
|
|
27
27
|
return { level, rule, file, message, fix };
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// --- git conflict markers ----------------------------------------------------
|
|
31
|
+
// A half-resolved merge/rebase can commit literal conflict markers into content
|
|
32
|
+
// (the incident: `<<<<<<<`/`=======`/`>>>>>>>` in content/home.html slipped past
|
|
33
|
+
// preview as "validated"). These are the default and diff3 marker lines, anchored
|
|
34
|
+
// at line start and exactly 7 chars with a trailing boundary — precise enough that
|
|
35
|
+
// real content never matches. `=======` / `|||||||` ALONE are NOT flagged (a lone
|
|
36
|
+
// `=======` is a common markdown/prose horizontal rule); only the START (`<<<<<<<`)
|
|
37
|
+
// and END (`>>>>>>>`) markers trigger — either one is a near-certain conflict, so
|
|
38
|
+
// we err false-negative-averse and flag on either.
|
|
39
|
+
const CONFLICT_START = /^<{7}(?=[ \t]|$)/;
|
|
40
|
+
const CONFLICT_END = /^>{7}(?=[ \t]|$)/;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Line numbers (1-based) of git conflict markers in `content`. Empty ⇒ none.
|
|
44
|
+
* Pure — exported for focused testing.
|
|
45
|
+
* @param {string} content
|
|
46
|
+
* @returns {number[]}
|
|
47
|
+
*/
|
|
48
|
+
export function detectConflictMarkers(content) {
|
|
49
|
+
if (typeof content !== "string" || (!content.includes("<<<<<<<") && !content.includes(">>>>>>>"))) return [];
|
|
50
|
+
const lines = content.split(/\r?\n/);
|
|
51
|
+
const hits = [];
|
|
52
|
+
for (let i = 0; i < lines.length; i++) {
|
|
53
|
+
if (CONFLICT_START.test(lines[i]) || CONFLICT_END.test(lines[i])) hits.push(i + 1);
|
|
54
|
+
}
|
|
55
|
+
return hits;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Text artifacts a conflict marker can hide in (images/fonts live in public/, not scanned).
|
|
59
|
+
const CONFLICT_SCAN_EXT = new Set([".html", ".htm", ".json", ".md", ".txt", ".css", ".js", ".mjs", ".svg", ".xml"]);
|
|
60
|
+
const hasScanExt = (p) => CONFLICT_SCAN_EXT.has((p.match(/\.[^./\\]+$/) || [""])[0].toLowerCase());
|
|
61
|
+
|
|
30
62
|
// --- canonical `.tot/config.json` shape (the #24/#25 regression guard) --------
|
|
31
63
|
const KNOWN_KINDS = new Set(["file", "tree"]);
|
|
32
64
|
const REQUIRED_WORKSPACES = ["content/", "public/", "theme.json"];
|
|
@@ -539,6 +571,26 @@ export function validateTenant(tenantDir, opts = {}) {
|
|
|
539
571
|
}
|
|
540
572
|
}
|
|
541
573
|
|
|
574
|
+
// 5. git conflict markers — advisory (never blocks), but LOUD: a half-resolved
|
|
575
|
+
// merge/rebase must not slip past as "validated". Scans text artifacts under
|
|
576
|
+
// content/ plus the root config files.
|
|
577
|
+
const conflictScanFiles = [
|
|
578
|
+
...walk(contentDir, hasScanExt),
|
|
579
|
+
...["theme.json", "capabilities.json", "scripts.json", join(".tot", "config.json")]
|
|
580
|
+
.map((f) => join(tenantDir, f))
|
|
581
|
+
.filter((p) => existsSync(p)),
|
|
582
|
+
];
|
|
583
|
+
for (const p of conflictScanFiles) {
|
|
584
|
+
const lines = detectConflictMarkers(readFileSync(p, "utf8"));
|
|
585
|
+
if (lines.length) {
|
|
586
|
+
findings.push(
|
|
587
|
+
mk(WARN, "git-conflict-markers", rel(p),
|
|
588
|
+
`git conflict markers at line(s) ${lines.join(", ")} — looks like an unfinished merge/rebase (the page would still build/serve broken)`,
|
|
589
|
+
"resolve the conflict and remove the <<<<<<< / ======= / >>>>>>> lines before submitting"),
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
542
594
|
const ok = !findings.some((f) => f.level === ERROR);
|
|
543
595
|
return { ok, findings };
|
|
544
596
|
}
|
|
@@ -555,6 +607,10 @@ function buildPageTargetSet(contentDir, pagesDir) {
|
|
|
555
607
|
return set;
|
|
556
608
|
}
|
|
557
609
|
|
|
610
|
+
/**
|
|
611
|
+
* @param {any} href @param {any} file @param {any} scope @param {any} pageTargets
|
|
612
|
+
* @param {(path: string) => boolean} [ownsPlatformRoute]
|
|
613
|
+
*/
|
|
558
614
|
function checkLink(href, file, scope, pageTargets, ownsPlatformRoute = () => false) {
|
|
559
615
|
const out = [];
|
|
560
616
|
if (!href || href.startsWith("#") || href.startsWith("mailto:") || href.startsWith("tel:")) return out;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewer-session transport for the ship surface — the NO-operator-secret path.
|
|
3
|
+
*
|
|
4
|
+
* An invited developer holds a `tot login` MCP session but no operator secret and no
|
|
5
|
+
* storefront cookie, so the old ship-surface transport dead-ended on them. This mints
|
|
6
|
+
* a storefront VIEWER session from the developer's OWN MCP token (POST
|
|
7
|
+
* /api/dev/cli-session on the tenant's own host, which resolves WHO the opaque token
|
|
8
|
+
* is via the MCP and hands back a `tot_session` cookie), then hands the caller a
|
|
9
|
+
* cookie-based transport pointed at the tenant host. Authorization is still the
|
|
10
|
+
* developer's live `ship-on-behalf` grant, enforced server-side at the ship route —
|
|
11
|
+
* this only carries their identity, it grants nothing.
|
|
12
|
+
*
|
|
13
|
+
* Minted FRESH per call (no disk cache): a `tot accept` is interactive + infrequent,
|
|
14
|
+
* and minting-per-call means a revoked session/grant is never honored past its life.
|
|
15
|
+
*/
|
|
16
|
+
import { resolveDeveloperSession, AuthUnavailableError } from "./auth.mjs";
|
|
17
|
+
|
|
18
|
+
const SESSION_COOKIE = "tot_session";
|
|
19
|
+
|
|
20
|
+
/** The tenant's own storefront host — the dev-viewer admission derives the tenant
|
|
21
|
+
* from the request host, so the session + integrate MUST target it (not the generic
|
|
22
|
+
* storefront origin + X-Tot-Owner, which only steers the operator-secret path). */
|
|
23
|
+
function tenantBase(tenant) {
|
|
24
|
+
return `https://${String(tenant || "").trim().toLowerCase()}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Pull `tot_session=<id>` out of a (possibly comma-folded) Set-Cookie header. The
|
|
28
|
+
* id is base64url — no comma/semicolon — so a non-greedy stop-set is unambiguous. */
|
|
29
|
+
export function parseSessionCookie(setCookie) {
|
|
30
|
+
if (!setCookie) return null;
|
|
31
|
+
const m = new RegExp(`${SESSION_COOKIE}=([^;,\\s]+)`).exec(setCookie);
|
|
32
|
+
return m ? m[1] : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a viewer-session transport for `tenant`. Returns
|
|
37
|
+
* { ok:true, base, authHeaders } — base = tenant host; cookie transport
|
|
38
|
+
* { ok:false, message, hint } — a clean, actionable refusal
|
|
39
|
+
* Never throws.
|
|
40
|
+
*
|
|
41
|
+
* @param {{ tenant:string, env?:NodeJS.ProcessEnv, fetchImpl?:typeof fetch,
|
|
42
|
+
* resolveDev?:typeof resolveDeveloperSession }} params
|
|
43
|
+
* @returns {Promise<
|
|
44
|
+
* { ok:true, base:string, authHeaders:Record<string,string> } |
|
|
45
|
+
* { ok:false, message:string, hint:string }
|
|
46
|
+
* >}
|
|
47
|
+
*/
|
|
48
|
+
export async function resolveViewerTransport({
|
|
49
|
+
tenant,
|
|
50
|
+
env = process.env,
|
|
51
|
+
fetchImpl = fetch,
|
|
52
|
+
resolveDev = resolveDeveloperSession,
|
|
53
|
+
}) {
|
|
54
|
+
// The developer's OWN MCP token (read + silently refreshed by the resolver). No
|
|
55
|
+
// client needed — resolveDeveloperSession tolerates a null client.
|
|
56
|
+
let dev;
|
|
57
|
+
try {
|
|
58
|
+
dev = await resolveDev(null, env, { fetchImpl });
|
|
59
|
+
} catch (e) {
|
|
60
|
+
if (e instanceof AuthUnavailableError) {
|
|
61
|
+
return { ok: false, message: e.message, hint: e.hint || "run `tot login`, then re-run." };
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
message: `couldn't read your Token of Trust session: ${e?.message || e}`,
|
|
66
|
+
hint: "run `tot login`, then re-run.",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const base = tenantBase(tenant);
|
|
71
|
+
let res;
|
|
72
|
+
try {
|
|
73
|
+
res = await fetchImpl(`${base}/api/dev/cli-session`, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: { authorization: `Bearer ${dev.token}` },
|
|
76
|
+
});
|
|
77
|
+
} catch (e) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
message: `couldn't reach ${base} to start a session: ${e?.message || e}`,
|
|
81
|
+
hint: "check the --tenant domain / your network, then re-run.",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (res.status === 401) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
message: "your `tot` session wasn't recognized for this store.",
|
|
88
|
+
hint: "run `tot login`, then re-run.",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
message: `couldn't start a session at ${base} (HTTP ${res.status}).`,
|
|
95
|
+
hint: "retry shortly; if it persists, this store may not be set up for CLI publishing yet.",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const cookie = parseSessionCookie(res.headers.get("set-cookie"));
|
|
100
|
+
if (!cookie) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
message: "the store started a session but returned no session cookie.",
|
|
104
|
+
hint: "re-run; if it persists, report it via `tot` feedback.",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
ok: true,
|
|
110
|
+
base,
|
|
111
|
+
authHeaders: {
|
|
112
|
+
cookie: `${SESSION_COOKIE}=${cookie}`,
|
|
113
|
+
// Matches the admin browser client; the server IGNORES this hint and re-reads
|
|
114
|
+
// the live ship-on-behalf grant, so it authorizes nothing on its own.
|
|
115
|
+
"x-tot-capability": "ship-on-behalf",
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|