@tokenoftrust/cli 1.2.1 → 1.2.3
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 +1 -5
- package/package.json +1 -2
- package/src/auth.mjs +66 -1
- package/src/commands/checkout.mjs +42 -4
- package/src/commands/dev.mjs +58 -18
- package/src/commands/feedback.mjs +8 -3
- package/src/commands/start.mjs +98 -30
- package/src/commands/submit.mjs +8 -8
- package/src/commands/whoami.mjs +10 -5
- package/src/sample.mjs +6 -1
- package/template/sample-store/content/chrome.html +0 -152
- package/template/sample-store/content/chrome.json +0 -94
- package/template/sample-store/content/home.html +0 -194
- package/template/sample-store/content/home.json +0 -50
- package/template/sample-store/content/pages/about.json +0 -10
- package/template/sample-store/content/pages/privacy.json +0 -6
- package/template/sample-store/content/pages/shipping-returns.json +0 -6
- package/template/sample-store/content/pages-html/blogs/news.html +0 -68
- package/template/sample-store/content/pages-html/pages/about-us.html +0 -95
- package/template/sample-store/content/pages-html/pages/contact-us.html +0 -68
- package/template/sample-store/content/pages-html/pages/privacy-policy.html +0 -65
- package/template/sample-store/content/pages-html/pages/shipping-returns.html +0 -77
- package/template/sample-store/content/themes/giant-navy.json +0 -73
- package/template/sample-store/public/img/hero-suicide-bunny.jpg +0 -0
- package/template/sample-store/public/img/hero.webp +0 -0
- package/template/sample-store/public/img/og.jpg +0 -0
- package/template/sample-store/public/logo-wordmark.png +0 -0
- package/template/sample-store/public/pages/home.css +0 -120
- package/template/sample-store/public/pages/mkt.css +0 -185
- package/template/sample-store/public/pages/page.css +0 -155
- package/template/sample-store/public/themes/giant-navy.css +0 -76
- package/template/sample-store/theme.json +0 -39
package/bin/tot.mjs
CHANGED
|
@@ -5,14 +5,12 @@
|
|
|
5
5
|
* One command a developer learns to go from an invite to a running store:
|
|
6
6
|
*
|
|
7
7
|
* tot start invite → running store in one command ← built (orchestrates the below, value-first)
|
|
8
|
-
* tot start --sample FREE local preview — no login, no MCP ← built (zero-login quickstart; MCP is the upsell)
|
|
9
8
|
* tot login sign in to Token of Trust (OAuth) ← built (MCP OAuth PKCE loopback; caches ~/.tot/credentials.json)
|
|
10
9
|
* tot logout sign out (clear the cached session) ← built (deletes ~/.tot/credentials.json; local-only, no server revoke)
|
|
11
10
|
* tot whoami who you're signed in as ← built
|
|
12
11
|
* tot checkout [<tenant>] clone a store you can build on ← built
|
|
13
12
|
* tot validate lint your store before you submit ← built
|
|
14
13
|
* tot dev run your store locally with save→reload ← built (monorepo: host astro; standalone: runs the published runner image)
|
|
15
|
-
* tot dev --sample run a FREE bundled sample store locally ← built (zero-login, no MCP; compliance rendering still shows)
|
|
16
14
|
* tot submit submit your store for preview ← built (validate + push preview ref; MCP preview_status read-back)
|
|
17
15
|
* tot doctor check this machine is ready
|
|
18
16
|
* tot ideas copy-paste AI prompts that reliably wow
|
|
@@ -24,7 +22,7 @@
|
|
|
24
22
|
* from anywhere else. Every command receives the detected context.
|
|
25
23
|
*
|
|
26
24
|
* This package is `@tokenoftrust/cli` (bin `tot`). Its heaviest command, `tot dev`,
|
|
27
|
-
*
|
|
25
|
+
* fetches the published storefront runner; `tot checkout/validate/submit` are
|
|
28
26
|
* pure Node. Dependency-free by design so `npm i -g @tokenoftrust/cli` stays light.
|
|
29
27
|
*/
|
|
30
28
|
import { readFileSync } from "node:fs";
|
|
@@ -49,14 +47,12 @@ function usage() {
|
|
|
49
47
|
tot — Token of Trust developer CLI
|
|
50
48
|
|
|
51
49
|
tot start invite → running store in one command (start here)
|
|
52
|
-
tot start --sample free local preview — no login, no account needed
|
|
53
50
|
tot login sign in to Token of Trust
|
|
54
51
|
tot logout sign out (clear the cached session)
|
|
55
52
|
tot whoami show who you're signed in as
|
|
56
53
|
tot checkout [<tenant>] clone a store you can build on
|
|
57
54
|
tot validate lint your store before you submit
|
|
58
55
|
tot dev run your store locally with save→reload
|
|
59
|
-
tot dev --sample run a free bundled sample store (no login, no MCP)
|
|
60
56
|
tot submit submit your store for preview
|
|
61
57
|
tot doctor check this machine is ready
|
|
62
58
|
tot ideas copy-paste AI prompts that reliably wow
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "Token of Trust developer CLI — check out 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",
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
"files": [
|
|
26
26
|
"bin",
|
|
27
27
|
"src",
|
|
28
|
-
"template",
|
|
29
28
|
"README.md",
|
|
30
29
|
"LICENSE"
|
|
31
30
|
],
|
package/src/auth.mjs
CHANGED
|
@@ -132,5 +132,70 @@ export async function resolveDeveloperSession(client, env, deps = {}) {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
if (client && typeof client.setToken === "function") client.setToken(creds.accessToken);
|
|
135
|
-
return {
|
|
135
|
+
return {
|
|
136
|
+
identity: "developer",
|
|
137
|
+
appDomain: null,
|
|
138
|
+
token: creds.accessToken,
|
|
139
|
+
email: emailFromJwt(creds.accessToken),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Establish a validated session on `client`, attaching auth in the CORRECT order
|
|
145
|
+
* relative to the MCP handshake — the one thing every command must get right:
|
|
146
|
+
*
|
|
147
|
+
* developer → attach the cached bearer via setToken() BEFORE client.initialize(),
|
|
148
|
+
* so the server binds the session to this identity AT initialize time.
|
|
149
|
+
* An init-then-attach order leaves the session anonymous for its whole
|
|
150
|
+
* life on a server that only binds identity at initialize — this is the
|
|
151
|
+
* invited-developer "no stores you can build on" dead-end.
|
|
152
|
+
* operator → client.initialize() FIRST, then credential_validate, which is an
|
|
153
|
+
* in-session tool call that requires a completed handshake.
|
|
154
|
+
*
|
|
155
|
+
* Every command calls THIS instead of hand-ordering initialize() + resolveSession(),
|
|
156
|
+
* so the ordering rule lives in exactly one place. `opts.initialize` lets a caller
|
|
157
|
+
* inject its own handshake (to wrap the unreachable-MCP error, or pass custom
|
|
158
|
+
* clientInfo); it defaults to `() => client.initialize()`.
|
|
159
|
+
*
|
|
160
|
+
* @param {ReturnType<import("./mcp.mjs").createMcpClient>} client
|
|
161
|
+
* @param {{ env?: NodeJS.ProcessEnv, prefer?: "operator"|"developer",
|
|
162
|
+
* initialize?: () => Promise<any> }} [opts]
|
|
163
|
+
* @returns {Promise<{ identity: "operator"|"developer", appDomain: string|null, email?: string|null }>}
|
|
164
|
+
*/
|
|
165
|
+
export async function establishSession(client, opts = {}) {
|
|
166
|
+
const env = opts.env || process.env;
|
|
167
|
+
const prefer = opts.prefer || (hasOperatorCreds(env) ? "operator" : "developer");
|
|
168
|
+
const initialize = opts.initialize || (() => client.initialize());
|
|
169
|
+
|
|
170
|
+
if (prefer === "developer") {
|
|
171
|
+
// Attach the bearer, THEN handshake — the handshake carries the Authorization
|
|
172
|
+
// header so the server binds this identity at initialize time.
|
|
173
|
+
const session = await resolveDeveloperSession(client, env);
|
|
174
|
+
await initialize();
|
|
175
|
+
return session;
|
|
176
|
+
}
|
|
177
|
+
// Operator: handshake FIRST, then credential_validate (an in-session tool call).
|
|
178
|
+
await initialize();
|
|
179
|
+
return resolveSession(client, { env, prefer });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Best-effort email from an OAuth access token when it's a JWT — for DISPLAY ONLY
|
|
184
|
+
* (self-diagnosing dead-ends: "signed in as <email> via <origin>"), never for trust.
|
|
185
|
+
* Decodes the unverified payload segment; returns null for an opaque token or one
|
|
186
|
+
* with no email-ish claim. Never throws.
|
|
187
|
+
* @param {string} token
|
|
188
|
+
* @returns {string|null}
|
|
189
|
+
*/
|
|
190
|
+
export function emailFromJwt(token) {
|
|
191
|
+
try {
|
|
192
|
+
const seg = String(token).split(".")[1];
|
|
193
|
+
if (!seg) return null;
|
|
194
|
+
const json = JSON.parse(
|
|
195
|
+
Buffer.from(seg.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8"),
|
|
196
|
+
);
|
|
197
|
+
return json.email || json.preferred_username || null;
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
136
201
|
}
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
* `tot` performs NO privileged forge work itself — the MCP owns that.
|
|
14
14
|
*
|
|
15
15
|
* Auth is resolved through src/auth.mjs: operator creds (our dogfooding / CI) or
|
|
16
|
-
* the developer's own ToT identity
|
|
16
|
+
* the developer's own ToT identity. This command doesn't care
|
|
17
17
|
* which — it just needs a validated session.
|
|
18
18
|
*
|
|
19
19
|
* Dependency-free (global fetch + `git` via child_process).
|
|
20
20
|
*/
|
|
21
21
|
import { execFileSync } from "node:child_process";
|
|
22
22
|
import { createMcpClient } from "../mcp.mjs";
|
|
23
|
-
import {
|
|
23
|
+
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
24
24
|
import { CliError, fail, formatError } from "../errors.mjs";
|
|
25
25
|
|
|
26
26
|
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
@@ -94,8 +94,9 @@ export async function run(argv, ctx) {
|
|
|
94
94
|
const client = createMcpClient(baseUrl);
|
|
95
95
|
|
|
96
96
|
try {
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
// Attach auth in the right order relative to the handshake (developer bearer
|
|
98
|
+
// BEFORE initialize; operator credential_validate after) — see establishSession.
|
|
99
|
+
const session = await establishSession(client, {
|
|
99
100
|
env,
|
|
100
101
|
prefer: args.identity || undefined,
|
|
101
102
|
});
|
|
@@ -218,10 +219,47 @@ export function normalizeStores(list) {
|
|
|
218
219
|
.filter((r) => r.id);
|
|
219
220
|
}
|
|
220
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Detect whether a `client_list` tool result is an ERROR result rather than a
|
|
224
|
+
* genuinely empty-but-successful store list — so callers surface it (identity +
|
|
225
|
+
* origin + reason, per fb-1783905718950-f45zg9) instead of collapsing it to an
|
|
226
|
+
* empty list and dead-ending at "ask for an invite". callTool unwraps a tool
|
|
227
|
+
* result to its structuredContent / parsed text, so an error surfaces as a
|
|
228
|
+
* status other than ok/success, an `error`/`isError` field, or an unparseable
|
|
229
|
+
* `raw` text blob. A response that carries a resolvable store array (even empty)
|
|
230
|
+
* is always a success. Returns a short human reason, or null when it's not an
|
|
231
|
+
* error. Pure + exported so it's unit-tested without any I/O.
|
|
232
|
+
* @param {unknown} list
|
|
233
|
+
* @returns {string|null}
|
|
234
|
+
*/
|
|
235
|
+
export function storeListError(list) {
|
|
236
|
+
if (list == null || typeof list !== "object" || Array.isArray(list)) return null;
|
|
237
|
+
// A resolvable store array present → it succeeded, never an error.
|
|
238
|
+
if (Array.isArray(list.clients) || Array.isArray(list.tenants)) return null;
|
|
239
|
+
const msg =
|
|
240
|
+
list.message ||
|
|
241
|
+
(typeof list.error === "string" ? list.error : list.error?.message) ||
|
|
242
|
+
null;
|
|
243
|
+
if (list.isError) return msg || "the store list request returned an error";
|
|
244
|
+
if (typeof list.status === "string" && !/^(ok|success)$/i.test(list.status)) {
|
|
245
|
+
return msg || `the store list request returned status "${list.status}"`;
|
|
246
|
+
}
|
|
247
|
+
if (list.error) return msg || "the store list request returned an error";
|
|
248
|
+
if (typeof list.raw === "string" && list.raw.trim()) return list.raw.trim();
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
221
252
|
function printClientList(list) {
|
|
253
|
+
const err = storeListError(list);
|
|
222
254
|
const stores = normalizeStores(list);
|
|
255
|
+
if (err && stores.length === 0) {
|
|
256
|
+
console.log(`\nCouldn't list your stores: ${err}`);
|
|
257
|
+
console.log("Run `tot whoami` to check your session, or `tot login` again.");
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
223
260
|
if (stores.length === 0) {
|
|
224
261
|
console.log("\nNo stores available to build on for this identity yet.");
|
|
262
|
+
console.log("If you were just invited, it may still be propagating — try again in a minute.");
|
|
225
263
|
if (list && !Array.isArray(list)) console.log(JSON.stringify(list, null, 2));
|
|
226
264
|
return;
|
|
227
265
|
}
|
package/src/commands/dev.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* monorepo or --workspace graft mode). Fast host loop for platform devs.
|
|
9
9
|
*
|
|
10
10
|
* checkout — a standalone tenant checkout has no runner. `tot dev` DOWNLOADS the
|
|
11
|
-
* published,
|
|
11
|
+
* published, RUNNER ARTIFACT (a plain tarball — the exact
|
|
12
12
|
* same tree Docker used to bake into the image, see
|
|
13
13
|
* scripts/build/build-runner.mjs) and runs it NATIVELY: no Docker, no
|
|
14
14
|
* container. Cached once per version under ~/.tot/cache/renderer/, so
|
|
@@ -40,7 +40,7 @@ import { Readable } from "node:stream";
|
|
|
40
40
|
import { pipeline } from "node:stream/promises";
|
|
41
41
|
import { setTimeout as delay } from "node:timers/promises";
|
|
42
42
|
import { createMcpClient } from "../mcp.mjs";
|
|
43
|
-
import {
|
|
43
|
+
import { establishSession } from "../auth.mjs";
|
|
44
44
|
import { CliError, fail, formatError } from "../errors.mjs";
|
|
45
45
|
import { openBrowser, waitForServer } from "../open.mjs";
|
|
46
46
|
import {
|
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
resolveRendererSource as resolveLocalRendererSource, SAMPLE_DIR_NAME,
|
|
49
49
|
} from "../sample.mjs";
|
|
50
50
|
|
|
51
|
-
/** The published
|
|
51
|
+
/** The published runner image (--docker fallback). Override with --image / TOT_DEV_IMAGE. */
|
|
52
52
|
const DEFAULT_DEV_IMAGE =
|
|
53
53
|
"242086487598.dkr.ecr.us-east-1.amazonaws.com/tot-dev:latest";
|
|
54
54
|
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
@@ -147,16 +147,56 @@ async function runStandalone(workspace, args, ctx) {
|
|
|
147
147
|
return await runNative(workspace, args, ctx);
|
|
148
148
|
} catch (e) {
|
|
149
149
|
if (e instanceof NativeArtifactUnavailableError) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
// Entitled artifact unavailable — stay NATIVE on the public npm runner
|
|
151
|
+
// (@tokenoftrust/storefront-runner, the SAME renderer bits), so
|
|
152
|
+
// `tot dev` never needs Docker just because dev_renderer_artifact isn't
|
|
153
|
+
// configured. Docker is the last resort only if the public runner is also
|
|
154
|
+
// unreachable (offline).
|
|
155
|
+
try {
|
|
156
|
+
console.error(`~ entitled renderer unavailable (${e.message}) — using the public runner (no Docker).`);
|
|
157
|
+
return await runNativePublic(workspace, args, ctx);
|
|
158
|
+
} catch (e2) {
|
|
159
|
+
if (e2 instanceof NativeArtifactUnavailableError) {
|
|
160
|
+
console.error(`~ public runner unavailable (${e2.message}) — falling back to the Docker runner.`);
|
|
161
|
+
return runContainer(workspace, args, ctx);
|
|
162
|
+
}
|
|
163
|
+
console.error(formatError(e2));
|
|
164
|
+
return e2 instanceof CliError ? (e2.exitCode ?? 2) : 2;
|
|
165
|
+
}
|
|
154
166
|
}
|
|
155
167
|
console.error(formatError(e));
|
|
156
168
|
return e instanceof CliError ? (e.exitCode ?? 2) : 2;
|
|
157
169
|
}
|
|
158
170
|
}
|
|
159
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Native boot against the PUBLIC/local renderer (no MCP, no
|
|
174
|
+
* entitlement) — the fallback for a standalone checkout when the entitled
|
|
175
|
+
* artifact isn't available. Mirrors runNative but resolves the renderer via
|
|
176
|
+
* ensureSampleRenderer (env override → cached runner → in-tree monorepo →
|
|
177
|
+
* public npm), which throws NativeArtifactUnavailableError when even the public
|
|
178
|
+
* runner can't be fetched (offline) so the caller can drop to Docker.
|
|
179
|
+
*/
|
|
180
|
+
async function runNativePublic(workspace, args, ctx) {
|
|
181
|
+
const cfg = readWorkspaceConfig(workspace);
|
|
182
|
+
if (!cfg) {
|
|
183
|
+
throw new CliError(`${workspace} isn't a tenant checkout (no readable .tot/config.json)`, {
|
|
184
|
+
next: "tot checkout <tenant> --clone <dir> (produces a runnable checkout)",
|
|
185
|
+
exitCode: 2,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const port = String(args.port || "4321");
|
|
189
|
+
const { url } = deriveUrl(cfg, port);
|
|
190
|
+
let runnerDir;
|
|
191
|
+
try {
|
|
192
|
+
runnerDir = await ensureSampleRenderer(args, ctx);
|
|
193
|
+
} catch (e) {
|
|
194
|
+
throw new NativeArtifactUnavailableError(String(e?.message || e));
|
|
195
|
+
}
|
|
196
|
+
printDevBanner({ tenant: cfg.tenant || null, url });
|
|
197
|
+
return bootNative(runnerDir, workspace, port, url, args);
|
|
198
|
+
}
|
|
199
|
+
|
|
160
200
|
function runMonorepo(ctx, argv) {
|
|
161
201
|
const script = join(ctx.repoRoot, "scripts", "tot-dev.mjs");
|
|
162
202
|
if (!existsSync(script)) {
|
|
@@ -274,7 +314,7 @@ export function printSampleBanner({ url }) {
|
|
|
274
314
|
}
|
|
275
315
|
|
|
276
316
|
/**
|
|
277
|
-
* Resolve WHERE to get the
|
|
317
|
+
* Resolve WHERE to get the runner tarball for this run — the seam that
|
|
278
318
|
* splits the entitled and the public (zero-login sample) delivery paths. Returns
|
|
279
319
|
* `{ kind, version, url, strip, cacheKey }`; callers download + extract + install +
|
|
280
320
|
* cache it uniformly (installRunnerTarball).
|
|
@@ -297,7 +337,7 @@ export async function resolveRendererSource(args, { client } = {}) {
|
|
|
297
337
|
}
|
|
298
338
|
|
|
299
339
|
/**
|
|
300
|
-
* PUBLIC, un-entitled source for sample / zero-login mode: the
|
|
340
|
+
* PUBLIC, un-entitled source for sample / zero-login mode: the runner
|
|
301
341
|
* straight from the public npm registry — NO MCP call, NO entitlement. Reads the
|
|
302
342
|
* package's registry metadata (unauthenticated JSON) for the tarball URL + version.
|
|
303
343
|
* Pin with `--renderer-version` / TOT_RUNNER_VERSION, else the `latest` dist-tag.
|
|
@@ -328,8 +368,9 @@ export async function resolveEntitledRendererSource(args, { client: providedClie
|
|
|
328
368
|
const baseUrl = args.mcp || process.env.MCP_BASE_URL || process.env.TOT_MCP_URL || DEFAULT_MCP_URL;
|
|
329
369
|
const client = providedClient || createMcpClient(baseUrl);
|
|
330
370
|
if (!providedClient) {
|
|
331
|
-
|
|
332
|
-
|
|
371
|
+
// Auth before the entitlement-gated tool call, in handshake order (developer
|
|
372
|
+
// bearer pre-initialize) — see establishSession.
|
|
373
|
+
await establishSession(client, { env: process.env });
|
|
333
374
|
}
|
|
334
375
|
const res = await client.callTool("dev_renderer_artifact", {});
|
|
335
376
|
if (!res?.url || !res?.version) {
|
|
@@ -339,7 +380,7 @@ export async function resolveEntitledRendererSource(args, { client: providedClie
|
|
|
339
380
|
}
|
|
340
381
|
|
|
341
382
|
/**
|
|
342
|
-
* Get the
|
|
383
|
+
* Get the renderer artifact for this host — a signed URL from the MCP
|
|
343
384
|
* (dev_renderer_artifact, same entitlement gate as the Docker pull token),
|
|
344
385
|
* downloaded + extracted + `pnpm install`'d ONCE per version under
|
|
345
386
|
* ~/.tot/cache/renderer/<version>/. Later runs of the same version reuse the
|
|
@@ -360,8 +401,8 @@ export async function ensureRendererArtifact(args, { client: providedClient } =
|
|
|
360
401
|
try {
|
|
361
402
|
const client = providedClient || createMcpClient(baseUrl);
|
|
362
403
|
if (!providedClient) {
|
|
363
|
-
|
|
364
|
-
await
|
|
404
|
+
// Auth in handshake order before the entitlement-gated tool call.
|
|
405
|
+
await establishSession(client, { env: process.env });
|
|
365
406
|
}
|
|
366
407
|
const res = await client.callTool("dev_renderer_artifact", {});
|
|
367
408
|
if (!res?.url || !res?.version) {
|
|
@@ -751,8 +792,7 @@ export function isPrivateRegistryImage(image) {
|
|
|
751
792
|
* Entitlement-gated registry login: ask the MCP for a short-lived pull credential
|
|
752
793
|
* (`dev_image_pull_token`) and `docker login` with it. Best-effort — if the MCP,
|
|
753
794
|
* the session, or the tool isn't available yet, warn softly and continue (an
|
|
754
|
-
* existing `docker login`, or the docker pull error, takes over).
|
|
755
|
-
* half of ~/.tot-mcp/handoffs/2026-07-09-tot-mcp-dev-image-pull-token.md.
|
|
795
|
+
* existing `docker login`, or the docker pull error, takes over).
|
|
756
796
|
*
|
|
757
797
|
* Exported + accepts an already-authenticated `client` (C1: `tot start` reuses
|
|
758
798
|
* its own session and overlaps this with the checkout clone instead of paying
|
|
@@ -765,8 +805,8 @@ export async function ensureRegistryLogin(image, args, { client: providedClient
|
|
|
765
805
|
try {
|
|
766
806
|
const client = providedClient || createMcpClient(baseUrl);
|
|
767
807
|
if (!providedClient) {
|
|
768
|
-
|
|
769
|
-
await
|
|
808
|
+
// Auth in handshake order before the entitlement-gated tool call.
|
|
809
|
+
await establishSession(client, { env: process.env });
|
|
770
810
|
}
|
|
771
811
|
const tok = await client.callTool("dev_image_pull_token", {});
|
|
772
812
|
const username = tok?.username || "AWS";
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { createInterface } from "node:readline/promises";
|
|
24
24
|
import { createMcpClient } from "../mcp.mjs";
|
|
25
|
-
import {
|
|
25
|
+
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
26
26
|
import { readActivity, formatActivity } from "../activity-log.mjs";
|
|
27
27
|
import { fail } from "../errors.mjs";
|
|
28
28
|
|
|
@@ -145,8 +145,13 @@ export async function run(argv) {
|
|
|
145
145
|
const mcpUrl = args.mcp || env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
|
|
146
146
|
const client = createMcpClient(mcpUrl);
|
|
147
147
|
try {
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
// Attach auth in the right order relative to the handshake (developer bearer
|
|
149
|
+
// pre-initialize, operator credential_validate post-initialize). We keep the
|
|
150
|
+
// feedback-specific clientInfo by injecting our own initialize.
|
|
151
|
+
await establishSession(client, {
|
|
152
|
+
env,
|
|
153
|
+
initialize: () => client.initialize({ name: "tot-cli", version: "feedback" }),
|
|
154
|
+
});
|
|
150
155
|
const payload = {
|
|
151
156
|
type: args.type,
|
|
152
157
|
category: args.category,
|
package/src/commands/start.mjs
CHANGED
|
@@ -50,12 +50,12 @@ import { createInterface } from "node:readline/promises";
|
|
|
50
50
|
|
|
51
51
|
import { detectContext } from "../context.mjs";
|
|
52
52
|
import { createMcpClient } from "../mcp.mjs";
|
|
53
|
-
import {
|
|
53
|
+
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
54
54
|
import { CliError, fail, formatError, exitCodeFor } from "../errors.mjs";
|
|
55
55
|
import { openBrowser, waitForServer } from "../open.mjs";
|
|
56
56
|
import { defaultLastTenantPath, readLastTenant, writeLastTenant } from "../last-tenant.mjs";
|
|
57
57
|
import { collectChecks } from "./doctor.mjs";
|
|
58
|
-
import { normalizeStores, checkoutTenant } from "./checkout.mjs";
|
|
58
|
+
import { normalizeStores, storeListError, checkoutTenant } from "./checkout.mjs";
|
|
59
59
|
import {
|
|
60
60
|
buildContainerPlan, spawnDevContainer, dockerAvailable, tryStartDocker,
|
|
61
61
|
resolveDevImage, isPrivateRegistryImage, ensureRegistryLogin,
|
|
@@ -93,12 +93,9 @@ const USAGE = `tot start — go from invite to a running store in one command
|
|
|
93
93
|
|
|
94
94
|
tot start preflight → sign in → pick your store → check out →
|
|
95
95
|
run it → open your browser → connect Claude
|
|
96
|
-
tot start --sample the FREE local preview — no login, no MCP, no account:
|
|
97
|
-
scaffold + run a sample store, then offer the MCP upsell
|
|
98
96
|
Options:
|
|
99
97
|
--tenant <id> use this store (skips auto-pick/prompt; remembered for next time)
|
|
100
98
|
--port <n> host port for the dev server (default 4321)
|
|
101
|
-
--sample zero-login free taste (see above); also what a no-session run offers
|
|
102
99
|
--docker use the Docker runner instead of the native runtime (F3
|
|
103
100
|
default); also the automatic fallback if the native
|
|
104
101
|
artifact can't be fetched
|
|
@@ -183,8 +180,8 @@ export async function run(argv, ctx) {
|
|
|
183
180
|
const session = await tryResolveSession(client, env, args);
|
|
184
181
|
if (decideStartMode({ sampleFlag: false, hasSession: !!session }) === "sample") {
|
|
185
182
|
if (!(await confirmSampleFallback(args))) {
|
|
186
|
-
throw new CliError("no Token of Trust session
|
|
187
|
-
next: "tot login (then re-run `tot start`)
|
|
183
|
+
throw new CliError("no Token of Trust session", {
|
|
184
|
+
next: "tot login (then re-run `tot start`)",
|
|
188
185
|
});
|
|
189
186
|
}
|
|
190
187
|
return await runSampleStart(args, ctx, env, startedAt);
|
|
@@ -193,11 +190,17 @@ export async function run(argv, ctx) {
|
|
|
193
190
|
// Authenticated path. Preflight (F) now that we know we're taking it — the
|
|
194
191
|
// local machine checks (node/git/Docker) that the authed loop needs.
|
|
195
192
|
await preflight(ctx, env);
|
|
196
|
-
console.log(` ✓ signed in as ${session
|
|
193
|
+
console.log(` ✓ signed in as ${describeIdentity(session)} via ${mcpOrigin(baseUrl)}`);
|
|
197
194
|
|
|
198
195
|
// 3. store — auto-pick, use --tenant, use the remembered one, or choose (A4).
|
|
199
|
-
|
|
200
|
-
|
|
196
|
+
// Keep the raw client_list so a tool ERROR (unauthenticated / not-entitled) is
|
|
197
|
+
// surfaced with who + where, not collapsed to an empty list (fb-...f45zg9).
|
|
198
|
+
const listResp = await client.callTool("client_list", {});
|
|
199
|
+
const stores = normalizeStores(listResp);
|
|
200
|
+
const tenant = await resolveTenant(stores, args, env, baseUrl, {
|
|
201
|
+
session,
|
|
202
|
+
listErr: storeListError(listResp),
|
|
203
|
+
});
|
|
201
204
|
|
|
202
205
|
// 4. checkout → ./<tenant> (reuse an existing checkout on a re-run),
|
|
203
206
|
// OVERLAPPED (C1) with prefetching the runner: the native artifact by
|
|
@@ -213,7 +216,7 @@ export async function run(argv, ctx) {
|
|
|
213
216
|
const runtime = { useDocker: args.docker, runnerDir: null };
|
|
214
217
|
await Promise.all([
|
|
215
218
|
ensureCheckout(client, tenant, dir, env),
|
|
216
|
-
prefetchRuntime(client, devArgs, env, runtime),
|
|
219
|
+
prefetchRuntime(client, devArgs, env, runtime, ctx),
|
|
217
220
|
]);
|
|
218
221
|
|
|
219
222
|
// 5. dev — native by default (F3), Docker on --docker or the fallback
|
|
@@ -340,14 +343,24 @@ async function runSampleStart(args, ctx, env, startedAt) {
|
|
|
340
343
|
* AuthUnavailableError (no creds) or a CliError (can't reach the MCP).
|
|
341
344
|
*/
|
|
342
345
|
async function loginStep(client, env, args) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
346
|
+
// establishSession attaches auth in the right order relative to the handshake:
|
|
347
|
+
// the developer bearer BEFORE initialize (so the server binds this identity at
|
|
348
|
+
// initialize time — otherwise the session is anonymous and client_list is empty),
|
|
349
|
+
// the operator credential_validate after it. We inject our own initialize so the
|
|
350
|
+
// unreachable-MCP case still surfaces the actionable CliError below.
|
|
351
|
+
return establishSession(client, {
|
|
352
|
+
env,
|
|
353
|
+
prefer: args.identity || undefined,
|
|
354
|
+
initialize: async () => {
|
|
355
|
+
try {
|
|
356
|
+
await client.initialize();
|
|
357
|
+
} catch (e) {
|
|
358
|
+
throw new CliError(`can't reach the Token of Trust MCP at ${client.mcpUrl} (${String(e?.message || e)})`, {
|
|
359
|
+
next: "check your network, then re-run — or point elsewhere with --mcp <url>",
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
});
|
|
351
364
|
}
|
|
352
365
|
|
|
353
366
|
/**
|
|
@@ -402,11 +415,16 @@ async function preflight(ctx, env) {
|
|
|
402
415
|
* Prefetch the runner (C1) so it's overlapped with checkout instead of paid
|
|
403
416
|
* for serially afterward: the native renderer artifact by default (F3 — no
|
|
404
417
|
* Docker prerequisite), or the Docker registry pull credential on --docker.
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
418
|
+
*
|
|
419
|
+
* On a native (entitled-artifact) fetch failure the runtime stays NATIVE: it
|
|
420
|
+
* falls back to the PUBLIC npm runner (@tokenoftrust/storefront-runner) — the
|
|
421
|
+
* SAME renderer bits the entitled artifact ships, just delivered over
|
|
422
|
+
* public npm instead of a signed URL — so `tot dev`/`tot start` never require
|
|
423
|
+
* Docker just because `dev_renderer_artifact` isn't configured on a deployment.
|
|
424
|
+
* Docker is the LAST resort, only when the public runner is also unreachable
|
|
425
|
+
* (offline) — flipping `runtime.useDocker` so the caller's dev step picks it up.
|
|
408
426
|
*/
|
|
409
|
-
async function prefetchRuntime(client, devArgs, env, runtime) {
|
|
427
|
+
async function prefetchRuntime(client, devArgs, env, runtime, ctx) {
|
|
410
428
|
if (runtime.useDocker) {
|
|
411
429
|
await prefetchDockerLogin(client, devArgs, env);
|
|
412
430
|
return;
|
|
@@ -415,9 +433,15 @@ async function prefetchRuntime(client, devArgs, env, runtime) {
|
|
|
415
433
|
runtime.runnerDir = await ensureRendererArtifact(devArgs, { client });
|
|
416
434
|
} catch (e) {
|
|
417
435
|
if (!(e instanceof NativeArtifactUnavailableError)) throw e;
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
436
|
+
// Entitled artifact unavailable — stay native on the public runner (no Docker).
|
|
437
|
+
try {
|
|
438
|
+
console.log(` ~ entitled renderer unavailable (${e.message}) — using the public runner (no Docker).`);
|
|
439
|
+
runtime.runnerDir = await ensureSampleRenderer(devArgs, ctx, { env });
|
|
440
|
+
} catch (e2) {
|
|
441
|
+
console.log(` ~ public runner unavailable (${e2?.message || e2}) — falling back to the Docker runner.`);
|
|
442
|
+
runtime.useDocker = true;
|
|
443
|
+
await prefetchDockerLogin(client, devArgs, env);
|
|
444
|
+
}
|
|
421
445
|
}
|
|
422
446
|
}
|
|
423
447
|
|
|
@@ -436,12 +460,58 @@ async function prefetchDockerLogin(client, devArgs, env) {
|
|
|
436
460
|
if (!dockerAvailable()) await tryStartDocker();
|
|
437
461
|
}
|
|
438
462
|
|
|
463
|
+
/** A human label for a resolved session — the email when we could read it from
|
|
464
|
+
* the OAuth token, else the operator app domain, else the identity kind. */
|
|
465
|
+
function describeIdentity(session) {
|
|
466
|
+
if (!session) return "an unknown identity";
|
|
467
|
+
if (session.email) return session.email;
|
|
468
|
+
if (session.identity === "operator") {
|
|
469
|
+
return session.appDomain ? `operator (${session.appDomain})` : "operator";
|
|
470
|
+
}
|
|
471
|
+
return "your developer identity";
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** The MCP host for user-facing messages (never the full /mcp URL). */
|
|
475
|
+
function mcpOrigin(baseUrl) {
|
|
476
|
+
try {
|
|
477
|
+
return new URL(baseUrl).host;
|
|
478
|
+
} catch {
|
|
479
|
+
return String(baseUrl);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Build the CliError for an authenticated session that surfaced NO usable store,
|
|
485
|
+
* making the dead-end SELF-DIAGNOSING (fb-1783905718950-f45zg9): always report who
|
|
486
|
+
* we're signed in as and against which MCP, and distinguish an ERRORED client_list
|
|
487
|
+
* (surface the reason — likely an auth/entitlement problem) from a genuinely empty
|
|
488
|
+
* result (invite may still be propagating, or you need one). Points at `tot whoami`.
|
|
489
|
+
*/
|
|
490
|
+
function noStoresError({ session, baseUrl, listErr }) {
|
|
491
|
+
const who = describeIdentity(session);
|
|
492
|
+
const origin = mcpOrigin(baseUrl);
|
|
493
|
+
if (listErr) {
|
|
494
|
+
return new CliError(
|
|
495
|
+
`signed in as ${who} via ${origin}, but listing your stores failed: ${listErr}`,
|
|
496
|
+
{ next: "run `tot whoami` to check your session, or `tot login` again — then re-run `tot start`" },
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
return new CliError(
|
|
500
|
+
`signed in as ${who} via ${origin}, but you have no stores to build on yet`,
|
|
501
|
+
{
|
|
502
|
+
next:
|
|
503
|
+
"if you were just invited, it may still be propagating — try again in a minute; " +
|
|
504
|
+
"otherwise ask your Token of Trust contact for a store invite (see `tot whoami`)",
|
|
505
|
+
},
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
439
509
|
/**
|
|
440
510
|
* Resolve the tenant to work on from the (normalized) store list, args, and
|
|
441
511
|
* the remembered last tenant (A4) — then remember whatever was decided so the
|
|
442
512
|
* next bare `tot start` doesn't have to ask again.
|
|
443
513
|
*/
|
|
444
|
-
async function resolveTenant(stores, args, env, baseUrl) {
|
|
514
|
+
async function resolveTenant(stores, args, env, baseUrl, { session = null, listErr = null } = {}) {
|
|
445
515
|
const lastTenantPath = defaultLastTenantPath(env);
|
|
446
516
|
const pick = pickTenant(stores, {
|
|
447
517
|
explicit: args.tenant || null,
|
|
@@ -450,9 +520,7 @@ async function resolveTenant(stores, args, env, baseUrl) {
|
|
|
450
520
|
|
|
451
521
|
let tenant;
|
|
452
522
|
if (pick.kind === "none") {
|
|
453
|
-
throw
|
|
454
|
-
next: "ask your Token of Trust contact for a store invite, then re-run — or `tot start --sample` for the free local preview",
|
|
455
|
-
});
|
|
523
|
+
throw noStoresError({ session, baseUrl, listErr });
|
|
456
524
|
} else if (pick.kind === "explicit") {
|
|
457
525
|
tenant = pick.tenant;
|
|
458
526
|
console.log(` → your store: ${tenant} (--tenant)`);
|
package/src/commands/submit.mjs
CHANGED
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
* 3. report back — reconcile result + compliance verdict + the preview URL — from the MCP.
|
|
8
8
|
*
|
|
9
9
|
* This is submit-for-PREVIEW, not ship-to-live (`change_accept` / a future `tot ship`
|
|
10
|
-
* is the separate ship gate). Step 3 calls the MCP `preview_status` read-back
|
|
11
|
-
*
|
|
12
|
-
* just pushed it returns { status, reconcile, compliance, previewUrl } and we poll it
|
|
10
|
+
* is the separate ship gate). Step 3 calls the MCP `preview_status` read-back:
|
|
11
|
+
* given the commit just pushed it returns { status, reconcile, compliance, previewUrl } and we poll it
|
|
13
12
|
* while reconcile is pending. If that tool isn't present (older MCP) the command still
|
|
14
13
|
* validates + pushes and reports "reconcile pending" — degrading visibly, never a crash.
|
|
15
14
|
*
|
|
@@ -33,7 +32,7 @@
|
|
|
33
32
|
import { execFileSync } from "node:child_process";
|
|
34
33
|
import { setTimeout as delay } from "node:timers/promises";
|
|
35
34
|
import { createMcpClient } from "../mcp.mjs";
|
|
36
|
-
import {
|
|
35
|
+
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
37
36
|
import { validateTenant, ERROR } from "../validate.mjs";
|
|
38
37
|
import { openBrowser } from "../open.mjs";
|
|
39
38
|
import { fail } from "../errors.mjs";
|
|
@@ -138,8 +137,9 @@ export async function run(argv, ctx) {
|
|
|
138
137
|
const baseUrl = args.mcp || env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
|
|
139
138
|
const client = createMcpClient(baseUrl);
|
|
140
139
|
try {
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
// Attach auth before the first server call (developer bearer pre-initialize,
|
|
141
|
+
// operator credential_validate post-initialize) — see establishSession.
|
|
142
|
+
await establishSession(client, { env, prefer: args.identity || undefined });
|
|
143
143
|
// Set the active tenant so preview_status reads the right scope (it keys on
|
|
144
144
|
// the session's tenant + the commit — no tenant arg of its own).
|
|
145
145
|
await client.callTool("client_switch", { tenant });
|
|
@@ -230,8 +230,8 @@ const CHECK_GLYPH = { pass: "✓", fail: "✗", warn: "⚠", skip: "~" };
|
|
|
230
230
|
|
|
231
231
|
/**
|
|
232
232
|
* Print one per-rule compliance check: id/label, required-ness, why it didn't
|
|
233
|
-
* pass (detail), and a concrete fix suggestion (hint) when there is one. This
|
|
234
|
-
*
|
|
233
|
+
* pass (detail), and a concrete fix suggestion (hint) when there is one. This
|
|
234
|
+
* beats a bare pass/fail — an agent (or a human) reading `tot submit`
|
|
235
235
|
* output should know exactly what to change and why, with no dashboard round-trip.
|
|
236
236
|
*/
|
|
237
237
|
function reportComplianceCheck(c) {
|