@tokenoftrust/cli 2.2.7 → 2.2.9

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.2.7",
3
+ "version": "2.2.9",
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",
@@ -55,7 +55,7 @@ import { repoNameFromRemote, tagFromRepoName } from "./submit.mjs";
55
55
  import { CliError, formatError } from "../errors.mjs";
56
56
  import {
57
57
  parseCredentialInput, formatCredentialOutput, splitAuthedRemote,
58
- credentialCachePath, readCachedCredential, writeCachedCredential,
58
+ readCachedCredential, writeCachedCredential,
59
59
  kitCredentialCachePath, dropCachedCredential, readCheckoutKind, READ_ONLY_KIT_KIND,
60
60
  } from "../git-credential.mjs";
61
61
 
@@ -165,12 +165,36 @@ export async function mintOrCacheCredential({
165
165
  /* fall through — tagFromRepoName degrades to "main" on a null repo name */
166
166
  }
167
167
  if (expectedHost && !hostMatches(originUrl, expectedHost)) return null;
168
- const tag = tagFromRepoName(repoName, ctx.tenant);
169
168
 
170
- const cachePath = credentialCachePath(ctx.tenant, tag, env);
171
- const cached = readCachedCredential(cachePath);
172
- if (cached) return { username: cached.username, password: cached.password };
169
+ // Prefer the tenant encoded in the git remote itself over `.tot/config.json`'s
170
+ // "tenant" field. The remote URL is the forge's OWN record of this checkout's repo
171
+ // (set once by `tenant_checkout`'s returned cloneUrl at clone time); the config file
172
+ // is tenant-owned, committed content a developer's own commit can set wrong at seed
173
+ // time or drift later — exactly what broke every client_switch/tenant_checkout call
174
+ // in fb-1790958790645-7j8gul / fb-1790969588649-zl3xa7. Every real tenant is
175
+ // provisioned with repo === owner (storefront's provisioning enforces this at every
176
+ // call site — buildStaffStoreIdentity, scripts/tenant/provision-gitea-repo.mjs), so
177
+ // the repo name IS the tenant id. Fall back to the config file only when the remote
178
+ // can't be read or parsed (no origin yet) — this never makes the mint MORE likely to
179
+ // fail than before, only less dependent on content the forge itself didn't set.
180
+ const tenant = repoName || ctx.tenant;
181
+ const tag = tagFromRepoName(repoName, tenant);
173
182
 
183
+ // NEVER cache this credential (fb-1790975400361-eg8wxt). The push credential's mint
184
+ // (tot-mcp's checkoutRepo -> mintUserToken) ROTATES: it revokes EVERY prior token for
185
+ // this (developer, tenant) Gitea user on each call -- by design, so a leaked push
186
+ // credential stops working the moment a fresh one is issued (gitea-rest.ts:1559-1569).
187
+ // A local cache keyed only by (tenant, tag) -- not by checkout directory -- has no way
188
+ // to learn that ANOTHER checkout, another machine, or even a retry just rotated the
189
+ // token it is holding: it only knows its own TTL, so it keeps serving a credential
190
+ // Gitea already revoked until that TTL expires. That produced exactly the reported
191
+ // symptom -- a brand-new clone's first fetch fails "Failed to authenticate user" until
192
+ // the cache is cleared, because an earlier clone's mint had already rotated this one
193
+ // out from under it. The read-only kit-credential path avoids this by minting with
194
+ // `rotate: false` (gitea-rest.ts:571, "would yank a concurrent fetch's token") -- that
195
+ // option isn't available here without weakening the push credential's own leak-safety
196
+ // property, so the fix is on this side: mint fresh every time instead of trusting a
197
+ // local TTL the server can invalidate out of band.
174
198
  const baseUrl = env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
175
199
  const client = createClient(baseUrl);
176
200
  try {
@@ -179,12 +203,10 @@ export async function mintOrCacheCredential({
179
203
  return null; // not signed in (or session unrefreshable) — nothing this helper can do
180
204
  }
181
205
  try {
182
- const res = await checkout(client, { tenant: ctx.tenant, tag, cloneDir: null });
206
+ const res = await checkout(client, { tenant, tag, cloneDir: null });
183
207
  const cred = splitAuthedRemote(res.gitRemote || "");
184
208
  if (!cred) return null;
185
- const fresh = { username: cred.username, password: cred.token };
186
- writeCachedCredential(cachePath, fresh);
187
- return fresh;
209
+ return { username: cred.username, password: cred.token };
188
210
  } catch (e) {
189
211
  // Same distinction as the kit-checkout catch above: `checkoutTenant` renders a
190
212
  // CliError when the MCP (or this checkout's own state) has an authoritative answer
@@ -214,30 +236,19 @@ export function kitCheckout(cwd) {
214
236
  }
215
237
 
216
238
  /**
217
- * git reported the credential rejected (`erase`): drop this checkout's cached credential so
218
- * the next `get` mints a fresh one instead of replaying the rejected token. Scoped to the
239
+ * git reported a credential rejected (`erase`). Only the read-only kit credential is ever
240
+ * cached (see `mintOrCacheCredential` — the tenant push credential is never cached, because
241
+ * its mint rotates server-side out from under any local TTL), so this only has work to do
242
+ * for a kit checkout; a tenant checkout's `erase` is a correct no-op. Scoped to the
219
243
  * checkout's own origin host, like `get`. Never throws.
220
244
  * @param {{ env?: NodeJS.ProcessEnv, cwd?: string, expectedHost?: string|null }} [opts]
221
245
  */
222
246
  export function forgetCredential({ env = process.env, cwd = process.cwd(), expectedHost = null } = {}) {
223
247
  try {
224
248
  const kit = kitCheckout(cwd);
225
- if (kit) {
226
- if (expectedHost && !hostMatches(kit.originUrl, expectedHost)) return;
227
- dropCachedCredential(kitCredentialCachePath(kit.target, env));
228
- return;
229
- }
230
- const ctx = detectContext(cwd);
231
- if (ctx.mode !== "checkout" || !ctx.tenant || !ctx.workspacePath) return;
232
- let originUrl = null;
233
- try {
234
- originUrl = execFileSync("git", ["-C", ctx.workspacePath, "remote", "get-url", "origin"], {
235
- stdio: ["ignore", "pipe", "pipe"],
236
- }).toString().trim();
237
- } catch { /* no origin */ }
238
- if (expectedHost && !hostMatches(originUrl, expectedHost)) return;
239
- const tag = tagFromRepoName(originUrl ? repoNameFromRemote(originUrl) : null, ctx.tenant);
240
- dropCachedCredential(credentialCachePath(ctx.tenant, tag, env));
249
+ if (!kit) return;
250
+ if (expectedHost && !hostMatches(kit.originUrl, expectedHost)) return;
251
+ dropCachedCredential(kitCredentialCachePath(kit.target, env));
241
252
  } catch {
242
253
  /* erase is advisory — never fail git over it */
243
254
  }