@tokenoftrust/cli 2.0.19 → 2.0.20

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/cli",
3
- "version": "2.0.19",
3
+ "version": "2.0.20",
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",
@@ -287,21 +287,26 @@ export async function run(argv, ctx) {
287
287
  * cloned: boolean, dir: string|null, head: string|null }>}
288
288
  */
289
289
  export async function checkoutTenant(client, { tenant, tag = "main", cloneDir = null, redact = (s) => s }) {
290
- // The switch is LOAD-BEARING, so its refusal is the error the developer must see. When
291
- // the tenant is not one of this session's clients the server refuses here with the real
292
- // cause (no grant / a grant issued to a client this identity is not bound to / a grant
293
- // issued after sign-in that needs a re-auth) far more specific than the tenant_checkout
294
- // refusal that follows it. Surfacing the switch's own words, with the server's nextAction,
295
- // stops a genuinely-unentitled tenant from being reported as a scope-selection slip.
290
+ // The switch CARRIES the best diagnosis but is NOT a precondition, and the difference
291
+ // matters in both directions:
292
+ //
293
+ // - Discarding its refusal (the original behaviour) meant a genuinely unentitled tenant
294
+ // surfaced later as tenant_checkout complaining about your PRIOR active tenant, which
295
+ // reads as a scope-selection slip rather than a missing grant.
296
+ // - Treating it as fatal breaks the STAFF-PREVIEW path, where the refusal is expected:
297
+ // a staff human checking out a merchant tenant under an authorization holds no client
298
+ // grant on it, so client_switch MUST fail while tenant_checkout legitimately succeeds
299
+ // (it takes the tenant as an argument and resolves the app from the authorization).
300
+ // `tot git-credential` mints through this same function, so a fatal switch also breaks
301
+ // every fetch and push in such a checkout, not just the initial clone.
302
+ //
303
+ // So: remember it, proceed, and surface it only if the checkout ALSO fails — at which
304
+ // point it is the more specific of the two errors.
305
+ let switchRefusal = null;
296
306
  try {
297
307
  await client.callTool("client_switch", { tenant });
298
308
  } catch (e) {
299
- throw new CliError(redact(String(e?.message || e)), {
300
- next:
301
- "confirm you're entitled to this store — `tot grants` lists what your session can " +
302
- "act on, and `tot login` refreshes a grant issued after you signed in",
303
- cause: e,
304
- });
309
+ switchRefusal = e;
305
310
  }
306
311
  // vc-app-binding safety-net (08-18, §5): ensure THIS developer is bound to the
307
312
  // storefront version-control app BEFORE checkout, so tenant_checkout can resolve
@@ -319,13 +324,37 @@ export async function checkoutTenant(client, { tenant, tag = "main", cloneDir =
319
324
  } catch {
320
325
  /* fail-open — see above */
321
326
  }
322
- const checkout = await client.callTool("tenant_checkout", { tenant, tag });
327
+ let checkout;
328
+ try {
329
+ checkout = await client.callTool("tenant_checkout", { tenant, tag });
330
+ } catch (e) {
331
+ // Both failed: the switch's refusal names the cause more precisely than a thrown
332
+ // checkout, so lead with it.
333
+ if (switchRefusal) {
334
+ throw new CliError(redact(String(switchRefusal?.message || switchRefusal)), {
335
+ next:
336
+ "confirm you're entitled to this store — `tot grants` lists what your session can " +
337
+ "act on, and `tot login` refreshes a grant issued after you signed in",
338
+ cause: switchRefusal,
339
+ });
340
+ }
341
+ throw e;
342
+ }
323
343
 
324
344
  // A non-checkout result (not provisioned / not entitled / failed) must surface
325
345
  // the MCP's human message + a concrete next step, NEVER a raw JSON.stringify
326
346
  // dump (James's 2026-07-19 first-experience failure).
327
347
  const err = checkoutError(checkout);
328
348
  if (err) {
349
+ // Same precedence: an unentitled tenant is better explained by the switch.
350
+ if (switchRefusal) {
351
+ throw new CliError(redact(String(switchRefusal?.message || switchRefusal)), {
352
+ next:
353
+ "confirm you're entitled to this store — `tot grants` lists what your session can " +
354
+ "act on, and `tot login` refreshes a grant issued after you signed in",
355
+ cause: switchRefusal,
356
+ });
357
+ }
329
358
  throw new CliError(redact(err.message), { next: err.next });
330
359
  }
331
360
  const gitRemote = checkout.gitRemote;