@tokenoftrust/cli 3.9.1 → 3.10.1

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": "3.9.1",
3
+ "version": "3.10.1",
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",
@@ -157,6 +157,7 @@ export function normalizeHotfixResult(data) {
157
157
  artifactDigest: typeof o.artifactDigest === "string" ? o.artifactDigest : null,
158
158
  receiptId: typeof o.receiptId === "string" ? o.receiptId : null,
159
159
  rollbackTarget: o.rollbackTarget && typeof o.rollbackTarget === "object" ? o.rollbackTarget : null,
160
+ retriable: o.retriable === false ? false : o.retriable === true ? true : null,
160
161
  forwardIntegration: fwd
161
162
  ? {
162
163
  ok: fwd.ok === true,
@@ -371,10 +372,15 @@ export function reportHotfixResult(result, { tenant, liveUrl, noOpen, openUrl })
371
372
  result.state === "promote_failed" ||
372
373
  result.state === "record_failed"
373
374
  ) {
375
+ // Same rule as ship: a compare-and-set refusal, or any outcome that says a retry cannot
376
+ // change it, must not be offered as safe to retry.
377
+ const deterministic = result.retriable === false || result.reason === "live_moved";
374
378
  console.error(
375
379
  fail(
376
380
  result.message || `hotfix ${result.state.replace("_", " ")}`,
377
- "re-run `tot hotfix` — the release is idempotent and safe to retry",
381
+ deterministic
382
+ ? "do not re-run `tot hotfix` — this refusal is deterministic and will not change"
383
+ : "re-run `tot hotfix` — the release is idempotent and safe to retry",
378
384
  ),
379
385
  );
380
386
  return 1;
@@ -734,11 +734,12 @@ function reportShipResult(result, { tenant, liveUrl, noOpen, openUrl }) {
734
734
  return 1;
735
735
  }
736
736
  if (result.state === "publishing") {
737
- // Dispatched is not written. Say the pointer moved only once this result says it was.
737
+ // pointerWritten false means the job step has not confirmed a write. For s3-overwrite, CI can
738
+ // write the pointer during awaiting-external, before prove, so do not say the pointer is absent.
738
739
  console.log(
739
740
  result.pointerWritten
740
741
  ? `\n ⏳ ${tenant} is publishing — the live pointer moved, CDN delivery is still confirming.`
741
- : `\n ⏳ ${tenant} is publishing — dispatched; the live pointer is not written yet.`,
742
+ : `\n ⏳ ${tenant} is publishing — dispatched; the live pointer is not confirmed written.`,
742
743
  );
743
744
  if (result.pinnedSha) console.log(` pinned: ${result.pinnedSha}`);
744
745
  if (result.receiptId) console.log(` receipt: ${result.receiptId}`);
@@ -4,7 +4,7 @@
4
4
  * TOT_HOME selects a private home and TOT_PROFILE selects an independent identity.
5
5
  */
6
6
  import {
7
- readFileSync, writeFileSync, mkdirSync, renameSync, chmodSync, existsSync, rmSync, linkSync, unlinkSync,
7
+ readFileSync, writeFileSync, mkdirSync, renameSync, chmodSync, existsSync, rmSync, linkSync, unlinkSync, statSync,
8
8
  } from "node:fs";
9
9
  import { homedir } from "node:os";
10
10
  import { createHash, randomUUID } from "node:crypto";
@@ -55,22 +55,45 @@ export function normalizeAuthority(value) {
55
55
  }
56
56
 
57
57
  /**
58
- * `git config --get` exit 1 means the key is unset. Exit 128 is "not a checkout"
59
- * only when stderr says so. Permission denied and a missing git binary are not
60
- * "no authority" — callers must not fall through to another session.
58
+ * `git config --get` exit 1 means the key is unset only when the config was readable.
59
+ * Exit 128 is "not a checkout" only when stderr says so. Permission denied, at any
60
+ * exit code, and a missing git binary are not "no authority".
61
61
  * @param {unknown} error
62
62
  */
63
63
  export function isAbsentGitConfig(error) {
64
+ const stderr = `${/** @type {{ stderr?: unknown }} */ (error)?.stderr ?? ""}\n${/** @type {{ message?: unknown }} */ (error)?.message ?? ""}`;
65
+ if (/Permission denied|unable to access/i.test(stderr)) return false;
64
66
  const status = error && typeof error === "object" ? /** @type {{ status?: number }} */ (error).status : undefined;
65
67
  if (status === 1) return true;
66
68
  if (status !== 128) return false;
67
- const stderr = `${/** @type {{ stderr?: unknown, message?: unknown }} */ (error).stderr ?? ""}\n${/** @type {{ message?: unknown }} */ (error).message ?? ""}`;
68
- if (/Permission denied|unable to access/i.test(stderr)) return false;
69
69
  return /not a git repository|can only be used inside a git repository/i.test(stderr);
70
70
  }
71
71
 
72
+ /** A mode with no read bits is unreadable even when a privileged process can still open it. */
73
+ function refuseUnreadableGitConfig(cwd) {
74
+ const config = join(cwd, ".git", "config");
75
+ let mode;
76
+ try {
77
+ mode = statSync(config).mode;
78
+ } catch (error) {
79
+ const code = error && typeof error === "object" ? /** @type {{ code?: string }} */ (error).code : undefined;
80
+ if (code === "ENOENT" || code === "ENOTDIR") return;
81
+ if (code === "EACCES") throw gitConfigDenied(config);
82
+ throw error;
83
+ }
84
+ if ((mode & 0o444) === 0) throw gitConfigDenied(config);
85
+ }
86
+
87
+ function gitConfigDenied(config) {
88
+ const denied = new Error(`unable to access '${config}': Permission denied`);
89
+ /** @type {Error & { status?: number, stderr?: string }} */ (denied).status = 128;
90
+ /** @type {Error & { stderr?: string }} */ (denied).stderr = denied.message;
91
+ return denied;
92
+ }
93
+
72
94
  /** Local checkout provenance, never inferred from a shared forge hostname. */
73
95
  export function repositoryAuthority(cwd = process.cwd()) {
96
+ refuseUnreadableGitConfig(cwd);
74
97
  let value;
75
98
  try {
76
99
  value = execFileSync("git", ["-C", cwd, "config", "--local", "--get", MCP_AUTHORITY_KEY], {