@tokenoftrust/cli 2.2.8 → 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.8",
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
 
@@ -180,10 +180,21 @@ export async function mintOrCacheCredential({
180
180
  const tenant = repoName || ctx.tenant;
181
181
  const tag = tagFromRepoName(repoName, tenant);
182
182
 
183
- const cachePath = credentialCachePath(tenant, tag, env);
184
- const cached = readCachedCredential(cachePath);
185
- if (cached) return { username: cached.username, password: cached.password };
186
-
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.
187
198
  const baseUrl = env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
188
199
  const client = createClient(baseUrl);
189
200
  try {
@@ -195,9 +206,7 @@ export async function mintOrCacheCredential({
195
206
  const res = await checkout(client, { tenant, tag, cloneDir: null });
196
207
  const cred = splitAuthedRemote(res.gitRemote || "");
197
208
  if (!cred) return null;
198
- const fresh = { username: cred.username, password: cred.token };
199
- writeCachedCredential(cachePath, fresh);
200
- return fresh;
209
+ return { username: cred.username, password: cred.token };
201
210
  } catch (e) {
202
211
  // Same distinction as the kit-checkout catch above: `checkoutTenant` renders a
203
212
  // CliError when the MCP (or this checkout's own state) has an authoritative answer
@@ -227,34 +236,19 @@ export function kitCheckout(cwd) {
227
236
  }
228
237
 
229
238
  /**
230
- * git reported the credential rejected (`erase`): drop this checkout's cached credential so
231
- * 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
232
243
  * checkout's own origin host, like `get`. Never throws.
233
244
  * @param {{ env?: NodeJS.ProcessEnv, cwd?: string, expectedHost?: string|null }} [opts]
234
245
  */
235
246
  export function forgetCredential({ env = process.env, cwd = process.cwd(), expectedHost = null } = {}) {
236
247
  try {
237
248
  const kit = kitCheckout(cwd);
238
- if (kit) {
239
- if (expectedHost && !hostMatches(kit.originUrl, expectedHost)) return;
240
- dropCachedCredential(kitCredentialCachePath(kit.target, env));
241
- return;
242
- }
243
- const ctx = detectContext(cwd);
244
- if (ctx.mode !== "checkout" || !ctx.tenant || !ctx.workspacePath) return;
245
- let originUrl = null;
246
- try {
247
- originUrl = execFileSync("git", ["-C", ctx.workspacePath, "remote", "get-url", "origin"], {
248
- stdio: ["ignore", "pipe", "pipe"],
249
- }).toString().trim();
250
- } catch { /* no origin */ }
251
- if (expectedHost && !hostMatches(originUrl, expectedHost)) return;
252
- // Mirror `get`'s tenant resolution (remote-derived, falling back to the config
253
- // file) so `erase` drops the SAME cache key `get` wrote, not a stale sibling.
254
- const repoName = originUrl ? repoNameFromRemote(originUrl) : null;
255
- const tenant = repoName || ctx.tenant;
256
- const tag = tagFromRepoName(repoName, tenant);
257
- dropCachedCredential(credentialCachePath(tenant, tag, env));
249
+ if (!kit) return;
250
+ if (expectedHost && !hostMatches(kit.originUrl, expectedHost)) return;
251
+ dropCachedCredential(kitCredentialCachePath(kit.target, env));
258
252
  } catch {
259
253
  /* erase is advisory — never fail git over it */
260
254
  }