@tokenoftrust/cli 2.0.19 → 2.0.21
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/clone.mjs +81 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.21",
|
|
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/clone.mjs
CHANGED
|
@@ -275,6 +275,39 @@ export async function run(argv, ctx) {
|
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Is this client_switch failure the EXPECTED "that tenant is not one of your clients"
|
|
280
|
+
* refusal, as opposed to a broken session (transport, auth, malformed envelope, internal
|
|
281
|
+
* error)? The distinction is load-bearing: an expected refusal is a normal step on the
|
|
282
|
+
* staff-preview path and must be tolerated, while anything else means the session itself
|
|
283
|
+
* is unusable and must surface immediately rather than be mistaken for an authorization
|
|
284
|
+
* problem later.
|
|
285
|
+
*
|
|
286
|
+
* Matched on the server's STRUCTURED signal first (`serverCode` / status), because message
|
|
287
|
+
* text is not a contract. The text fallback exists only for servers predating a typed code
|
|
288
|
+
* and is deliberately narrow — when in doubt this returns false, which escalates. Treating
|
|
289
|
+
* an unknown failure as "expected" is the unsafe direction: it would let a dead connection
|
|
290
|
+
* pass for a routine refusal.
|
|
291
|
+
*
|
|
292
|
+
* Pure + exported so it is unit-tested without any I/O.
|
|
293
|
+
* @param {unknown} error
|
|
294
|
+
* @returns {boolean}
|
|
295
|
+
*/
|
|
296
|
+
export function isNotClientTenantRefusal(error) {
|
|
297
|
+
const e = /** @type {any} */ (error);
|
|
298
|
+
if (!e) return false;
|
|
299
|
+
// A transport/protocol-level failure is never an authorization refusal.
|
|
300
|
+
const category = e?.diagnostic?.category ?? e?.category;
|
|
301
|
+
if (category && category !== "tool_error") return false;
|
|
302
|
+
const code = e?.diagnostic?.serverCode ?? e?.serverCode;
|
|
303
|
+
if (typeof code === "string") {
|
|
304
|
+
return code === "not_client_tenant" || code === "over_scoped";
|
|
305
|
+
}
|
|
306
|
+
// Text fallback (pre-typed-code servers) — narrow on purpose.
|
|
307
|
+
const message = typeof e?.message === "string" ? e.message : "";
|
|
308
|
+
return /not one of your client tenants/i.test(message);
|
|
309
|
+
}
|
|
310
|
+
|
|
278
311
|
/**
|
|
279
312
|
* The privileged checkout core, composable in-process (used by `tot clone`
|
|
280
313
|
* and by `tot start`): client_switch → tenant_checkout → optionally clone. The
|
|
@@ -287,21 +320,37 @@ export async function run(argv, ctx) {
|
|
|
287
320
|
* cloned: boolean, dir: string|null, head: string|null }>}
|
|
288
321
|
*/
|
|
289
322
|
export async function checkoutTenant(client, { tenant, tag = "main", cloneDir = null, redact = (s) => s }) {
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
323
|
+
// tenant_checkout is the SOLE AUTHORITATIVE checkout decision. client_switch is a scope
|
|
324
|
+
// convenience whose refusal may be entirely expected: a staff human checking out a
|
|
325
|
+
// merchant tenant under a staff-preview authorization holds no client grant on it, so the
|
|
326
|
+
// switch MUST refuse while the checkout legitimately succeeds. `tot git-credential` mints
|
|
327
|
+
// through this same function, so treating the switch as fatal breaks every fetch and push
|
|
328
|
+
// in such a checkout, not only the clone.
|
|
329
|
+
//
|
|
330
|
+
// Two failure modes are therefore BOTH wrong, and the second is the dangerous one:
|
|
331
|
+
// - discarding the refusal loses the only good explanation when a tenant genuinely is
|
|
332
|
+
// not yours;
|
|
333
|
+
// - letting the refusal OVERRIDE a checkout denial replaces an authoritative answer
|
|
334
|
+
// ("your staff authorization expired / was revoked / does not cover this resource")
|
|
335
|
+
// with grant guidance that sends you chasing the wrong thing entirely.
|
|
336
|
+
// So an EXPECTED switch refusal may never speak over a checkout denial (see below).
|
|
337
|
+
//
|
|
338
|
+
// An UNEXPECTED switch failure — transport, auth, a malformed envelope — is not the
|
|
339
|
+
// not-a-client case at all. It means the session itself is broken, and carrying it
|
|
340
|
+
// silently would let a dead connection masquerade as an authorization problem, so it is
|
|
341
|
+
// raised immediately rather than remembered.
|
|
342
|
+
let expectedSwitchRefusal = null;
|
|
296
343
|
try {
|
|
297
344
|
await client.callTool("client_switch", { tenant });
|
|
298
345
|
} catch (e) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
346
|
+
if (isNotClientTenantRefusal(e)) {
|
|
347
|
+
expectedSwitchRefusal = e;
|
|
348
|
+
} else {
|
|
349
|
+
throw new CliError(redact(String(e?.message || e)), {
|
|
350
|
+
next: "the session could not be scoped — check your connection and run `tot whoami`",
|
|
351
|
+
cause: e,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
305
354
|
}
|
|
306
355
|
// vc-app-binding safety-net (08-18, §5): ensure THIS developer is bound to the
|
|
307
356
|
// storefront version-control app BEFORE checkout, so tenant_checkout can resolve
|
|
@@ -319,13 +368,32 @@ export async function checkoutTenant(client, { tenant, tag = "main", cloneDir =
|
|
|
319
368
|
} catch {
|
|
320
369
|
/* fail-open — see above */
|
|
321
370
|
}
|
|
322
|
-
|
|
371
|
+
let checkout;
|
|
372
|
+
try {
|
|
373
|
+
checkout = await client.callTool("tenant_checkout", { tenant, tag });
|
|
374
|
+
} catch (e) {
|
|
375
|
+
// A THROWN checkout is not a denial — it never rendered an authorization decision. Only
|
|
376
|
+
// here may the expected switch refusal speak, because it is the only explanation either
|
|
377
|
+
// call produced.
|
|
378
|
+
if (expectedSwitchRefusal) {
|
|
379
|
+
throw new CliError(redact(String(expectedSwitchRefusal?.message || expectedSwitchRefusal)), {
|
|
380
|
+
next:
|
|
381
|
+
"confirm you're entitled to this store — `tot grants` lists what your session can " +
|
|
382
|
+
"act on, and `tot login` refreshes a grant issued after you signed in",
|
|
383
|
+
cause: expectedSwitchRefusal,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
throw e;
|
|
387
|
+
}
|
|
323
388
|
|
|
324
389
|
// A non-checkout result (not provisioned / not entitled / failed) must surface
|
|
325
390
|
// the MCP's human message + a concrete next step, NEVER a raw JSON.stringify
|
|
326
391
|
// dump (James's 2026-07-19 first-experience failure).
|
|
327
392
|
const err = checkoutError(checkout);
|
|
328
393
|
if (err) {
|
|
394
|
+
// AUTHORITATIVE: the checkout rendered a decision, so it is the answer — even when the
|
|
395
|
+
// switch also refused. Substituting grant guidance here is what would tell someone whose
|
|
396
|
+
// staff authorization had EXPIRED to go looking at their grants.
|
|
329
397
|
throw new CliError(redact(err.message), { next: err.next });
|
|
330
398
|
}
|
|
331
399
|
const gitRemote = checkout.gitRemote;
|