@tokenoftrust/cli 2.4.8 → 2.4.10
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/admission-hint.mjs +36 -0
- package/src/auth.mjs +6 -2
- package/src/commands/accept.mjs +4 -3
- package/src/commands/preview-doctor.mjs +2 -1
- package/src/commands/revert.mjs +3 -2
- package/src/commands/ship.mjs +39 -13
- package/src/viewer-session.mjs +83 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.10",
|
|
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,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The next step to print when the storefront refuses a call with 401/403.
|
|
3
|
+
*
|
|
4
|
+
* The server's admission rule (apps/storefront lib/pipeline/admission.ts) names exactly what is
|
|
5
|
+
* missing in `admission.missing[].grant`, and its reason is already in the refusal's `error`.
|
|
6
|
+
* The hint follows that answer instead of restating a policy of its own: a client copy of the
|
|
7
|
+
* rule goes stale — every refusal here used to say "you need a live ship-on-behalf grant", which
|
|
8
|
+
* is wrong for an unpaid store, where an invited developer needs no grant at all.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** The grant names a refusal body reports missing, or `[]` when it names none. */
|
|
12
|
+
export function missingGrantsOf(body) {
|
|
13
|
+
const missing = body && typeof body === "object" ? body.admission?.missing : undefined;
|
|
14
|
+
return Array.isArray(missing)
|
|
15
|
+
? missing.map((m) => m?.grant).filter((g) => typeof g === "string")
|
|
16
|
+
: [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** One actionable next step for a refused call, from the grants the server named. */
|
|
20
|
+
export function refusalHint(status, missingGrants = []) {
|
|
21
|
+
if (status === 401 || missingGrants.includes("session")) return "run `tot login`, then re-run";
|
|
22
|
+
if (missingGrants.includes("tenant-binding")) {
|
|
23
|
+
return "your sign-in is for a different store — check --tenant, then re-run";
|
|
24
|
+
}
|
|
25
|
+
if (missingGrants.includes("tenant-standing")) {
|
|
26
|
+
return "ask the store owner to invite you to this store from its Team page, then re-run";
|
|
27
|
+
}
|
|
28
|
+
if (missingGrants.includes("ship-on-behalf")) {
|
|
29
|
+
return "this store's live site is active, so ask the store owner for a ship-on-behalf grant from its Team page, then re-run";
|
|
30
|
+
}
|
|
31
|
+
if (missingGrants.includes("owner-standing")) return "only the store owner can do this";
|
|
32
|
+
if (missingGrants.includes("console-capability")) {
|
|
33
|
+
return "the operator secret is acting with preview-access — set X-Tot-Capability: ship-on-behalf (or ACCEPT_OPERATOR_CAPABILITY), then re-run";
|
|
34
|
+
}
|
|
35
|
+
return "ask the store owner for access to this store — or use an operator secret authorised for it";
|
|
36
|
+
}
|
package/src/auth.mjs
CHANGED
|
@@ -136,7 +136,7 @@ export function warnLegacyOperatorEnv(env = process.env, warn = (m) => console.e
|
|
|
136
136
|
* @param {(import("./mcp.mjs").createMcpClient extends (...a:any)=>infer R ? R : any)|null} client
|
|
137
137
|
* @param {NodeJS.ProcessEnv} env
|
|
138
138
|
* @param {{ fetchImpl?: typeof fetch, now?: number }} [deps] injectable for tests
|
|
139
|
-
* @returns {Promise<{ identity: "developer", appDomain: null, token: string, email?: string|null }>}
|
|
139
|
+
* @returns {Promise<{ identity: "developer", appDomain: null, token: string, email?: string|null, mcpUrl?: string|null }>}
|
|
140
140
|
*/
|
|
141
141
|
export async function resolveDeveloperSession(client, env, deps = {}) {
|
|
142
142
|
const { fetchImpl = fetch, now = Date.now() } = deps;
|
|
@@ -204,6 +204,10 @@ export async function resolveDeveloperSession(client, env, deps = {}) {
|
|
|
204
204
|
appDomain: null,
|
|
205
205
|
token: creds.accessToken,
|
|
206
206
|
email: emailFromJwt(creds.accessToken),
|
|
207
|
+
// The MCP this developer is signed in to — so a caller can tell the human
|
|
208
|
+
// which MCP their OWN session is against when a store reports it trusts a
|
|
209
|
+
// different one (the tier-mismatch case: signed in to qa, store trusts prod).
|
|
210
|
+
mcpUrl: creds.mcpUrl || null,
|
|
207
211
|
};
|
|
208
212
|
}
|
|
209
213
|
|
|
@@ -228,7 +232,7 @@ export async function resolveDeveloperSession(client, env, deps = {}) {
|
|
|
228
232
|
*
|
|
229
233
|
* @param {ReturnType<import("./mcp.mjs").createMcpClient>} client
|
|
230
234
|
* @param {{ env?: NodeJS.ProcessEnv, initialize?: () => Promise<any>, prefer?: string }} [opts]
|
|
231
|
-
* @returns {Promise<{ identity: "developer", appDomain: null, token: string, email?: string|null }>}
|
|
235
|
+
* @returns {Promise<{ identity: "developer", appDomain: null, token: string, email?: string|null, mcpUrl?: string|null }>}
|
|
232
236
|
*/
|
|
233
237
|
export async function establishSession(client, opts = {}) {
|
|
234
238
|
const env = opts.env || process.env;
|
package/src/commands/accept.mjs
CHANGED
|
@@ -47,8 +47,9 @@ import { createMcpClient } from "../mcp.mjs";
|
|
|
47
47
|
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
48
48
|
import { scopeToTenant } from "../tenant-scope.mjs";
|
|
49
49
|
// No-operator-secret path: mint a viewer session from the developer's OWN `tot`
|
|
50
|
-
// login and integrate as themselves (server
|
|
50
|
+
// login and integrate as themselves (the server's admission rule decides).
|
|
51
51
|
import { resolveViewerTransport } from "../viewer-session.mjs";
|
|
52
|
+
import { missingGrantsOf, refusalHint } from "../admission-hint.mjs";
|
|
52
53
|
// The doctor PUSH: on a failed accept/integrate, auto-append the COMPACT
|
|
53
54
|
// "here's what's blocking you" summary over the SAME transport we just used, so a
|
|
54
55
|
// developer doesn't have to remember to run `tot preview doctor` themselves.
|
|
@@ -529,7 +530,7 @@ export async function runIntegrate(
|
|
|
529
530
|
// - OPERATOR SECRET (operators/CI): Bearer + X-Tot-Owner on the generic host.
|
|
530
531
|
// - VIEWER SESSION (an invited developer, no secret): mint a `tot_session` from
|
|
531
532
|
// their OWN `tot` login on the TENANT'S host and send it as a cookie. The
|
|
532
|
-
// server
|
|
533
|
+
// server's admission rule authorizes either way.
|
|
533
534
|
// No `content-type` here: it's added per-POST; the mergeable preflight GET wants none.
|
|
534
535
|
let authHeaders;
|
|
535
536
|
if (secret) {
|
|
@@ -688,7 +689,7 @@ function reportIntegrateHttpError(result, res, { tenant, label }) {
|
|
|
688
689
|
fail(
|
|
689
690
|
`the integration queue refused the request: ${msg}`,
|
|
690
691
|
res.status === 401 || res.status === 403
|
|
691
|
-
?
|
|
692
|
+
? refusalHint(res.status, missingGrantsOf(result.raw))
|
|
692
693
|
: res.status === 404
|
|
693
694
|
? `check that ${label} has a built candidate in ${tenant}'s queue (\`tot pr list --tenant ${tenant}\`)`
|
|
694
695
|
: "check --tenant / --url / --pr, then re-run",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
*/
|
|
39
39
|
import { fail } from "../errors.mjs";
|
|
40
40
|
import { resolveOperatorSecret } from "./ship.mjs";
|
|
41
|
+
import { missingGrantsOf, refusalHint } from "../admission-hint.mjs";
|
|
41
42
|
import { resolveViewerTransport } from "../viewer-session.mjs";
|
|
42
43
|
import {
|
|
43
44
|
DOCTOR_PATH,
|
|
@@ -165,7 +166,7 @@ export async function runDoctor(
|
|
|
165
166
|
console.error(
|
|
166
167
|
fail(
|
|
167
168
|
`not authorized to diagnose ${tenant}'s merges: ${data?.error || `HTTP ${res.status}`}`,
|
|
168
|
-
|
|
169
|
+
refusalHint(res.status, missingGrantsOf(data)),
|
|
169
170
|
),
|
|
170
171
|
);
|
|
171
172
|
return 2;
|
package/src/commands/revert.mjs
CHANGED
|
@@ -45,8 +45,9 @@ import { planForAction, printPlanAndConfirm } from "../plan.mjs";
|
|
|
45
45
|
// speak ONE operator-auth contract.
|
|
46
46
|
import { resolveOperatorSecret } from "./ship.mjs";
|
|
47
47
|
// No-operator-secret path: mint a viewer session from the developer's OWN `tot`
|
|
48
|
-
// login. The server still gates the request
|
|
48
|
+
// login. The server's admission rule still gates the request.
|
|
49
49
|
import { resolveViewerTransport } from "../viewer-session.mjs";
|
|
50
|
+
import { missingGrantsOf, refusalHint } from "../admission-hint.mjs";
|
|
50
51
|
import { isJobAccepted, waitForPipelineJob } from "../pipeline-job.mjs";
|
|
51
52
|
|
|
52
53
|
const DEFAULT_STOREFRONT_URL = "https://storefront.tokenoftrust.store";
|
|
@@ -337,7 +338,7 @@ export async function runRevert(
|
|
|
337
338
|
fail(
|
|
338
339
|
`the revert request was refused: ${msg}`,
|
|
339
340
|
res.status === 401 || res.status === 403
|
|
340
|
-
?
|
|
341
|
+
? refusalHint(res.status, missingGrantsOf(data))
|
|
341
342
|
: "check --tenant / --url / --preview, then re-run",
|
|
342
343
|
),
|
|
343
344
|
);
|
package/src/commands/ship.mjs
CHANGED
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
*
|
|
48
48
|
* TRANSPORT: operators/CI may use a Bearer operator secret. A signed-in owner or
|
|
49
49
|
* developer instead mints a storefront viewer session from their OWN `tot login`
|
|
50
|
-
* identity; the server
|
|
50
|
+
* identity; the server's admission rule decides who may ship (owner; a live
|
|
51
|
+
* `ship-on-behalf` grant; or, while the store is unpaid, any developer invited to it).
|
|
51
52
|
*/
|
|
52
53
|
import { createHash } from "node:crypto";
|
|
53
54
|
import { fail } from "../errors.mjs";
|
|
@@ -61,6 +62,7 @@ import { emitActivity } from "../activity.mjs";
|
|
|
61
62
|
// `tot preview doctor` run.
|
|
62
63
|
import { autoSurfaceDoctor } from "../merge-doctor-report.mjs";
|
|
63
64
|
import { resolveViewerTransport } from "../viewer-session.mjs";
|
|
65
|
+
import { missingGrantsOf, refusalHint } from "../admission-hint.mjs";
|
|
64
66
|
|
|
65
67
|
const DEFAULT_STOREFRONT_URL = "https://storefront.tokenoftrust.store";
|
|
66
68
|
|
|
@@ -75,11 +77,13 @@ const USAGE = `tot ship — publish the tenant's CURRENT GREEN AGGREGATE to live
|
|
|
75
77
|
accept this revision's overridable quality findings,
|
|
76
78
|
record the required reason, then continue publishing
|
|
77
79
|
tot ship --no-open don't open the live URL in your browser
|
|
80
|
+
tot ship --verbose print each request's URL, HTTP status, and the
|
|
81
|
+
server's error code — never a token or cookie
|
|
78
82
|
|
|
79
|
-
Authentication:
|
|
80
|
-
|
|
81
|
-
secret via
|
|
82
|
-
TOT_OPERATOR_SECRET, or --secret.
|
|
83
|
+
Authentication: use your own \`tot login\` session if you own the store, hold
|
|
84
|
+
a live ship-on-behalf grant, or — while the store is unpaid — were invited to
|
|
85
|
+
it as a developer. Operators/CI may instead provide a secret via
|
|
86
|
+
PREVIEW_RECONCILE_SECRET, GRANTS_ADMIN_SECRET, TOT_OPERATOR_SECRET, or --secret.
|
|
83
87
|
|
|
84
88
|
\`tot ship\` has ONE meaning: it publishes the tenant's shared preview
|
|
85
89
|
aggregate — the batch of PRs that integrated cleanly and went GREEN — to
|
|
@@ -107,6 +111,7 @@ export function parseShipArgs(argv) {
|
|
|
107
111
|
url: null,
|
|
108
112
|
secret: null,
|
|
109
113
|
acceptQualityReason: null,
|
|
114
|
+
verbose: false,
|
|
110
115
|
help: false,
|
|
111
116
|
};
|
|
112
117
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -117,6 +122,7 @@ export function parseShipArgs(argv) {
|
|
|
117
122
|
else if (t === "--url") a.url = argv[++i];
|
|
118
123
|
else if (t === "--secret") a.secret = argv[++i];
|
|
119
124
|
else if (t === "--accept-quality-findings") a.acceptQualityReason = argv[++i];
|
|
125
|
+
else if (t === "--verbose") a.verbose = true;
|
|
120
126
|
else if (t === "--help" || t === "-h") a.help = true;
|
|
121
127
|
}
|
|
122
128
|
return a;
|
|
@@ -422,9 +428,9 @@ async function readJsonSafe(res) {
|
|
|
422
428
|
*
|
|
423
429
|
* @param {{ tenant:string, secret:string, storefrontUrl?:string|null,
|
|
424
430
|
* yes?:boolean, noOpen?:boolean, acceptQualityReason?:string|null,
|
|
425
|
-
* env?:NodeJS.ProcessEnv }} params
|
|
431
|
+
* verbose?:boolean, env?:NodeJS.ProcessEnv }} params
|
|
426
432
|
* @param {{ fetch?:typeof fetch, confirmPlan?:typeof printPlanAndConfirm,
|
|
427
|
-
* openUrl?:(u:string)=>boolean, progress?:boolean,
|
|
433
|
+
* openUrl?:(u:string)=>boolean, progress?:boolean, log?:(s:string)=>void,
|
|
428
434
|
* resolveViewerTransport?:typeof resolveViewerTransport }} [deps]
|
|
429
435
|
* @returns {Promise<number>} process exit code
|
|
430
436
|
*/
|
|
@@ -436,6 +442,7 @@ export async function runShip(
|
|
|
436
442
|
yes = false,
|
|
437
443
|
noOpen = false,
|
|
438
444
|
acceptQualityReason = null,
|
|
445
|
+
verbose = false,
|
|
439
446
|
env = process.env,
|
|
440
447
|
},
|
|
441
448
|
deps = {},
|
|
@@ -443,6 +450,9 @@ export async function runShip(
|
|
|
443
450
|
const fetchImpl = deps.fetch || globalThis.fetch;
|
|
444
451
|
const confirmPlan = deps.confirmPlan || printPlanAndConfirm;
|
|
445
452
|
const resolveViewer = deps.resolveViewerTransport || resolveViewerTransport;
|
|
453
|
+
// --verbose: request URL + HTTP status + the server's error code only — NEVER
|
|
454
|
+
// a token, secret, or cookie value.
|
|
455
|
+
const vlog = verbose ? (deps.log || ((s) => console.error(`[verbose] ${s}`))) : () => {};
|
|
446
456
|
let base = (storefrontUrl || DEFAULT_STOREFRONT_URL).trim().replace(/\/+$/, "");
|
|
447
457
|
let authHeaders;
|
|
448
458
|
if (secret) {
|
|
@@ -452,7 +462,7 @@ export async function runShip(
|
|
|
452
462
|
"x-tot-capability": "ship-on-behalf",
|
|
453
463
|
};
|
|
454
464
|
} else {
|
|
455
|
-
const viewer = await resolveViewer({ tenant, env, fetchImpl, storefrontUrl });
|
|
465
|
+
const viewer = await resolveViewer({ tenant, env, fetchImpl, storefrontUrl, verbose, log: vlog });
|
|
456
466
|
if (!viewer.ok) {
|
|
457
467
|
console.error(fail(viewer.message, viewer.hint));
|
|
458
468
|
return 2;
|
|
@@ -471,6 +481,7 @@ export async function runShip(
|
|
|
471
481
|
|
|
472
482
|
// 1. GET the read-only plan: the pinned sha + artifact digest + included PRs +
|
|
473
483
|
// rollback target + paywall verdict. Zero side effects.
|
|
484
|
+
vlog(`GET ${base}/api/changes/ship`);
|
|
474
485
|
let planRes;
|
|
475
486
|
try {
|
|
476
487
|
planRes = await fetchImpl(`${base}/api/changes/ship`, { method: "GET", headers: authHeaders });
|
|
@@ -480,21 +491,35 @@ export async function runShip(
|
|
|
480
491
|
);
|
|
481
492
|
return 1;
|
|
482
493
|
}
|
|
494
|
+
vlog(`ship plan → HTTP ${planRes.status}`);
|
|
483
495
|
const planData = await readJsonSafe(planRes);
|
|
484
|
-
if (!planRes.ok
|
|
485
|
-
|
|
486
|
-
|
|
496
|
+
if (verbose && !planRes.ok) {
|
|
497
|
+
const code = planData?.error || planData?.admission?.missing?.map((m) => m?.grant).join(",") || null;
|
|
498
|
+
if (code) vlog(`ship plan error: ${code}`);
|
|
499
|
+
}
|
|
500
|
+
if (planRes.status === 401 || planRes.status === 403) {
|
|
501
|
+
// The admission rule's own refusal (body shape `{ ok:false, error,
|
|
502
|
+
// admission:{ missing } }`) — name exactly what's missing instead of a
|
|
503
|
+
// generic "check the operator secret", which is actively wrong for the
|
|
504
|
+
// viewer-session transport (no operator secret involved at all).
|
|
487
505
|
const msg = planData?.error || `HTTP ${planRes.status}`;
|
|
488
506
|
console.error(
|
|
489
507
|
fail(
|
|
490
508
|
`the ship plan was refused: ${msg}`,
|
|
491
|
-
|
|
509
|
+
secret
|
|
492
510
|
? "check the operator secret and that it's authorised for this tenant"
|
|
493
|
-
:
|
|
511
|
+
: refusalHint(planRes.status, missingGrantsOf(planData)),
|
|
494
512
|
),
|
|
495
513
|
);
|
|
496
514
|
return 1;
|
|
497
515
|
}
|
|
516
|
+
if (!planRes.ok && planData?.ok !== false) {
|
|
517
|
+
// A transport-level refusal the endpoint didn't explain in its own shape
|
|
518
|
+
// (tenant-resolution/network) — surface it honestly.
|
|
519
|
+
const msg = planData?.error || `HTTP ${planRes.status}`;
|
|
520
|
+
console.error(fail(`the ship plan was refused: ${msg}`, "check --tenant / --url, then re-run"));
|
|
521
|
+
return 1;
|
|
522
|
+
}
|
|
498
523
|
const plan = normalizeShipPlan(planData);
|
|
499
524
|
// The tenant's mounted publication spaces ride the same release (see the file header).
|
|
500
525
|
const spaces = (await readSpaceRelease({ base, authHeaders, fetchImpl })) ?? { ready: [], waiting: [] };
|
|
@@ -725,6 +750,7 @@ export async function run(argv, ctx) {
|
|
|
725
750
|
yes: args.yes,
|
|
726
751
|
noOpen: args.noOpen,
|
|
727
752
|
acceptQualityReason: args.acceptQualityReason,
|
|
753
|
+
verbose: args.verbose,
|
|
728
754
|
env,
|
|
729
755
|
},
|
|
730
756
|
{ openUrl: (u) => openBrowser(u) },
|
package/src/viewer-session.mjs
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* a storefront VIEWER session from the developer's OWN MCP token (POST
|
|
7
7
|
* /api/dev/cli-session on the tenant's own host, which resolves WHO the opaque token
|
|
8
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
|
|
10
|
-
*
|
|
11
|
-
* this only carries their identity, it
|
|
9
|
+
* cookie-based transport pointed at the tenant host. Authorization is the server's
|
|
10
|
+
* admission rule at the ship route (owner; a live `ship-on-behalf` grant; or, while the
|
|
11
|
+
* store is unpaid, any developer invited to it) — this only carries their identity, it
|
|
12
|
+
* grants nothing.
|
|
12
13
|
*
|
|
13
14
|
* Minted FRESH per call (no disk cache): a `tot accept` is interactive + infrequent,
|
|
14
15
|
* and minting-per-call means a revoked session/grant is never honored past its life.
|
|
@@ -43,6 +44,66 @@ export function parseSessionCookie(setCookie) {
|
|
|
43
44
|
return m ? m[1] : null;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
/** @param {string|null|undefined} url @returns {string|null} */
|
|
48
|
+
function originOf(url) {
|
|
49
|
+
if (!url) return null;
|
|
50
|
+
try {
|
|
51
|
+
return new URL(url).origin;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Turn a non-2xx `/api/dev/cli-session` response into one clean, actionable
|
|
59
|
+
* `{ message, hint }` — the ONE place this CLI interprets that endpoint's refusal
|
|
60
|
+
* shape, so ship/preview-token (both routed through `resolveViewerTransport`)
|
|
61
|
+
* never duplicate or drift from each other.
|
|
62
|
+
*
|
|
63
|
+
* - 401 `unresolved_token` carrying `trustedMcp`, where the CLI's OWN signed-in
|
|
64
|
+
* MCP (`signedInMcpUrl`, from `resolveDeveloperSession`) is a DIFFERENT origin
|
|
65
|
+
* — the genuine tier mismatch (signed in to qa, store trusts prod, or vice
|
|
66
|
+
* versa): say exactly which two origins disagree and the exact fix.
|
|
67
|
+
* - Any other 401 — a real "you're not recognized here" — keeps the plain
|
|
68
|
+
* login hint, but still surfaces the server's error code so a report isn't
|
|
69
|
+
* just "it didn't work".
|
|
70
|
+
* - 502/503 — the SEAM is broken (unconfigured, unreachable, malformed), which
|
|
71
|
+
* is never a login problem: say so, with the server's code, and suggest a
|
|
72
|
+
* retry instead of `tot login`.
|
|
73
|
+
*
|
|
74
|
+
* @param {number} status
|
|
75
|
+
* @param {Record<string, unknown>|null} body
|
|
76
|
+
* @param {string|null|undefined} signedInMcpUrl — this CLI's own `creds.mcpUrl`
|
|
77
|
+
* @returns {{ message: string, hint: string }}
|
|
78
|
+
*/
|
|
79
|
+
export function describeCliSessionRefusal(status, body, signedInMcpUrl) {
|
|
80
|
+
const error = body && typeof body.error === "string" ? body.error : null;
|
|
81
|
+
const code = body && typeof body.code === "string" ? body.code : null;
|
|
82
|
+
const trustedMcp = body && typeof body.trustedMcp === "string" ? body.trustedMcp : null;
|
|
83
|
+
|
|
84
|
+
if (status === 502 || status === 503) {
|
|
85
|
+
return {
|
|
86
|
+
message: `couldn't verify your session — the store's identity check failed${code ? ` (${code})` : ""}.`,
|
|
87
|
+
hint: "retry shortly; if it persists, report it with `tot feedback`.",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (error === "unresolved_token" && trustedMcp) {
|
|
92
|
+
const signedInOrigin = originOf(signedInMcpUrl);
|
|
93
|
+
if (signedInOrigin && signedInOrigin !== trustedMcp) {
|
|
94
|
+
return {
|
|
95
|
+
message: `you're signed in to ${signedInOrigin}, but this store trusts ${trustedMcp}.`,
|
|
96
|
+
hint: `run \`tot login --mcp ${trustedMcp}\` (or unset TOT_MCP_URL), then re-run.`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
message: `your \`tot\` session wasn't recognized for this store${error && error !== "unresolved_token" ? ` (${error})` : ""}.`,
|
|
103
|
+
hint: "run `tot login`, then re-run.",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
46
107
|
/**
|
|
47
108
|
* Resolve a viewer-session transport for `tenant`. Returns
|
|
48
109
|
* { ok:true, base, authHeaders } — base = tenant host; cookie transport
|
|
@@ -53,7 +114,8 @@ export function parseSessionCookie(setCookie) {
|
|
|
53
114
|
* developer can point the same command at a non-default deployment.
|
|
54
115
|
*
|
|
55
116
|
* @param {{ tenant:string, env?:NodeJS.ProcessEnv, fetchImpl?:typeof fetch,
|
|
56
|
-
* storefrontUrl?:string|null, resolveDev?:typeof resolveDeveloperSession
|
|
117
|
+
* storefrontUrl?:string|null, resolveDev?:typeof resolveDeveloperSession,
|
|
118
|
+
* verbose?:boolean, log?:(s:string)=>void }} params
|
|
57
119
|
* @returns {Promise<
|
|
58
120
|
* { ok:true, base:string, authHeaders:Record<string,string> } |
|
|
59
121
|
* { ok:false, message:string, hint:string }
|
|
@@ -65,7 +127,12 @@ export async function resolveViewerTransport({
|
|
|
65
127
|
fetchImpl = fetch,
|
|
66
128
|
storefrontUrl = null,
|
|
67
129
|
resolveDev = resolveDeveloperSession,
|
|
130
|
+
verbose = false,
|
|
131
|
+
log = (s) => console.error(`[verbose] ${s}`),
|
|
68
132
|
}) {
|
|
133
|
+
// --verbose: request URL + HTTP status + the server's error code only — NEVER
|
|
134
|
+
// the bearer token or the minted session cookie.
|
|
135
|
+
const vlog = verbose ? log : () => {};
|
|
69
136
|
// The developer's OWN MCP token (read + silently refreshed by the resolver). No
|
|
70
137
|
// client needed — resolveDeveloperSession tolerates a null client.
|
|
71
138
|
let dev;
|
|
@@ -95,6 +162,7 @@ export async function resolveViewerTransport({
|
|
|
95
162
|
// cross-origin without a CORS preflight this worker never answers, so the
|
|
96
163
|
// anti-CSRF property is preserved exactly. Spoofing an `Origin` header instead
|
|
97
164
|
// would assert a browser context that does not exist and protect nothing.
|
|
165
|
+
vlog(`POST ${base}/api/dev/cli-session`);
|
|
98
166
|
res = await fetchImpl(`${base}/api/dev/cli-session`, {
|
|
99
167
|
method: "POST",
|
|
100
168
|
headers: {
|
|
@@ -110,12 +178,17 @@ export async function resolveViewerTransport({
|
|
|
110
178
|
hint: "check the --tenant domain / your network, then re-run.",
|
|
111
179
|
};
|
|
112
180
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
181
|
+
vlog(`cli-session → HTTP ${res.status}`);
|
|
182
|
+
if (res.status === 401 || res.status === 502 || res.status === 503) {
|
|
183
|
+
let body = null;
|
|
184
|
+
try {
|
|
185
|
+
body = await res.json();
|
|
186
|
+
} catch {
|
|
187
|
+
/* non-JSON error body — describeCliSessionRefusal falls back cleanly */
|
|
188
|
+
}
|
|
189
|
+
if (body?.error || body?.code) vlog(`cli-session error: ${[body.error, body.code].filter(Boolean).join(" / ")}`);
|
|
190
|
+
const { message, hint } = describeCliSessionRefusal(res.status, body, dev.mcpUrl);
|
|
191
|
+
return { ok: false, message, hint };
|
|
119
192
|
}
|
|
120
193
|
if (!res.ok) {
|
|
121
194
|
return {
|