@tokenoftrust/cli 3.7.1 → 3.7.3
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/bin/tot.mjs +6 -1
- package/package.json +1 -1
- package/src/commands/git-credential.mjs +18 -7
- package/src/git-credential.mjs +9 -4
- package/src/no-gitea-links.test.mjs +2 -0
- package/src/token-store.mjs +30 -4
- package/src/visual-parity.mjs +5 -2
package/bin/tot.mjs
CHANGED
|
@@ -133,7 +133,12 @@ async function dispatch(cmd, rest, ctx) {
|
|
|
133
133
|
// root's preview branch into it. Refuse them there, naming the space's own path.
|
|
134
134
|
if (SPACE_REFUSED_COMMANDS.has(cmd)) {
|
|
135
135
|
const { recordedCheckout } = await import("../src/commands/git-credential.mjs");
|
|
136
|
-
|
|
136
|
+
let space;
|
|
137
|
+
try { space = recordedCheckout(ctx.cwd); }
|
|
138
|
+
catch (error) {
|
|
139
|
+
process.stderr.write(`tot: could not read this checkout's git config, so it is not safe to continue.\n${error instanceof Error ? error.message : error}\n`);
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
137
142
|
if (space?.kind === "publication-space") {
|
|
138
143
|
process.stderr.write(spaceCheckoutRefusal(cmd, space.spaceId));
|
|
139
144
|
return 2;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.3",
|
|
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",
|
|
@@ -128,7 +128,14 @@ export async function mintOrCacheCredential({
|
|
|
128
128
|
} = {}) {
|
|
129
129
|
let authority;
|
|
130
130
|
try { authority = selectedAuthority(env, { cwd }); }
|
|
131
|
-
catch
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (error instanceof Error && error.message.startsWith("invalid MCP authority")) {
|
|
133
|
+
console.error("tot: this checkout has an invalid MCP authority. Run tot login --mcp <url> in this checkout.");
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
console.error(formatError(error));
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
132
139
|
env = { ...env, TOT_SELECTED_MCP_URL: authority, MCP_BASE_URL: authority };
|
|
133
140
|
const report = (e) => {
|
|
134
141
|
console.error(e instanceof AuthUnavailableError
|
|
@@ -142,7 +149,9 @@ export async function mintOrCacheCredential({
|
|
|
142
149
|
`tot: Token of Trust's entitlement service didn't answer — retrying for up to ${Math.round(ms / 1000)}s…`,
|
|
143
150
|
),
|
|
144
151
|
});
|
|
145
|
-
|
|
152
|
+
let recorded;
|
|
153
|
+
try { recorded = recordedCheckout(cwd); }
|
|
154
|
+
catch (e) { report(e); return null; }
|
|
146
155
|
if (recorded?.kind === "invalid") return null; // a half-recorded space mints nothing, never the store's
|
|
147
156
|
if (recorded?.kind === PUBLICATION_SPACE_KIND) {
|
|
148
157
|
if (expectedHost && !hostMatches(recorded.originUrl, expectedHost)) return null;
|
|
@@ -167,7 +176,9 @@ export async function mintOrCacheCredential({
|
|
|
167
176
|
return null;
|
|
168
177
|
}
|
|
169
178
|
}
|
|
170
|
-
|
|
179
|
+
let kit;
|
|
180
|
+
try { kit = kitCheckout(cwd); }
|
|
181
|
+
catch (e) { report(e); return null; }
|
|
171
182
|
if (kit) {
|
|
172
183
|
if (expectedHost && !hostMatches(kit.originUrl, expectedHost)) return null;
|
|
173
184
|
const alias = resolveReadOnlyAlias(kit.target);
|
|
@@ -256,7 +267,8 @@ export async function mintOrCacheCredential({
|
|
|
256
267
|
|
|
257
268
|
/**
|
|
258
269
|
* The publication-space checkout `cwd` is in (or a half-recorded one: `{ kind: "invalid" }`),
|
|
259
|
-
* from the kind `tot clone` recorded; null for anything else.
|
|
270
|
+
* from the kind `tot clone` recorded; null for anything else. An unreadable git
|
|
271
|
+
* config throws — it is not "not a space".
|
|
260
272
|
* @param {string} cwd
|
|
261
273
|
* @returns {{ kind: "publication-space", tenant: string, spaceId: string, repo: string, originUrl: string|null }
|
|
262
274
|
* | { kind: "invalid" } | null}
|
|
@@ -264,8 +276,7 @@ export async function mintOrCacheCredential({
|
|
|
264
276
|
export function recordedCheckout(cwd) {
|
|
265
277
|
const git = (cargs) =>
|
|
266
278
|
execFileSync("git", ["-C", cwd, ...cargs], { stdio: ["ignore", "pipe", "pipe"] }).toString();
|
|
267
|
-
|
|
268
|
-
try { kind = readCheckoutKind(git); } catch { return null; }
|
|
279
|
+
const kind = readCheckoutKind(git);
|
|
269
280
|
if (kind.kind === "invalid") return { kind: "invalid" };
|
|
270
281
|
if (kind.kind !== PUBLICATION_SPACE_KIND) return null;
|
|
271
282
|
let originUrl = null;
|
|
@@ -275,7 +286,7 @@ export function recordedCheckout(cwd) {
|
|
|
275
286
|
|
|
276
287
|
/**
|
|
277
288
|
* The read-only kit checkout `cwd` is in, from the kind `tot clone` recorded, or null when
|
|
278
|
-
* it is not one (a store, a loose dir, not a repo).
|
|
289
|
+
* it is not one (a store, a loose dir, not a repo). An unreadable git config throws.
|
|
279
290
|
* @param {string} cwd
|
|
280
291
|
* @returns {{ target: string, originUrl: string|null }|null}
|
|
281
292
|
*/
|
package/src/git-credential.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { homedir } from "node:os";
|
|
|
21
21
|
import { createHash } from "node:crypto";
|
|
22
22
|
import { join, dirname } from "node:path";
|
|
23
23
|
import { mkdirSync, writeFileSync, chmodSync, rmSync } from "node:fs";
|
|
24
|
-
import { readCredentials, writeCredentials, selectedAuthority, profileSlug, readSessionCredentials } from "./token-store.mjs";
|
|
24
|
+
import { readCredentials, writeCredentials, selectedAuthority, profileSlug, readSessionCredentials, isAbsentGitConfig } from "./token-store.mjs";
|
|
25
25
|
|
|
26
26
|
/** The git config value that routes credential requests through `tot` — the
|
|
27
27
|
* `!` tells git to run this as a shell command (git appends the operation,
|
|
@@ -54,8 +54,9 @@ export function splitAuthedRemote(remoteUrl) {
|
|
|
54
54
|
if (!token) return null;
|
|
55
55
|
const username = u.username ? decodeURIComponent(u.username) : "";
|
|
56
56
|
return { publicUrl: `${u.protocol}//${u.host}${u.pathname}`, username, token };
|
|
57
|
-
} catch {
|
|
58
|
-
return null;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error instanceof TypeError || error instanceof URIError) return null;
|
|
59
|
+
throw error;
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
|
|
@@ -205,7 +206,11 @@ export function recordPublicationSpaceCheckout(git, { tenant, spaceId, repo }) {
|
|
|
205
206
|
*/
|
|
206
207
|
export function readCheckoutKind(git) {
|
|
207
208
|
const get = (key) => {
|
|
208
|
-
try { return String(git(["config", "--local", "--get", key])).trim(); }
|
|
209
|
+
try { return String(git(["config", "--local", "--get", key])).trim(); }
|
|
210
|
+
catch (error) {
|
|
211
|
+
if (isAbsentGitConfig(error)) return "";
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
209
214
|
};
|
|
210
215
|
const kind = get(CHECKOUT_KIND_KEY);
|
|
211
216
|
if (kind === PUBLICATION_SPACE_KIND) {
|
package/src/token-store.mjs
CHANGED
|
@@ -54,6 +54,21 @@ export function normalizeAuthority(value) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
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.
|
|
61
|
+
* @param {unknown} error
|
|
62
|
+
*/
|
|
63
|
+
export function isAbsentGitConfig(error) {
|
|
64
|
+
const status = error && typeof error === "object" ? /** @type {{ status?: number }} */ (error).status : undefined;
|
|
65
|
+
if (status === 1) return true;
|
|
66
|
+
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
|
+
return /not a git repository|can only be used inside a git repository/i.test(stderr);
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
/** Local checkout provenance, never inferred from a shared forge hostname. */
|
|
58
73
|
export function repositoryAuthority(cwd = process.cwd()) {
|
|
59
74
|
let value;
|
|
@@ -61,7 +76,10 @@ export function repositoryAuthority(cwd = process.cwd()) {
|
|
|
61
76
|
value = execFileSync("git", ["-C", cwd, "config", "--local", "--get", MCP_AUTHORITY_KEY], {
|
|
62
77
|
encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
|
|
63
78
|
}).trim();
|
|
64
|
-
} catch {
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (isAbsentGitConfig(error)) return null;
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
65
83
|
return value ? normalizeAuthority(value) : null;
|
|
66
84
|
}
|
|
67
85
|
|
|
@@ -147,8 +165,13 @@ export function defaultCredentialsPath(env = process.env, mcpUrl = undefined) {
|
|
|
147
165
|
export function readSessionCredentials(env = process.env, mcpUrl = undefined) {
|
|
148
166
|
const authority = selectedAuthority(env, { mcpUrl });
|
|
149
167
|
const creds = readCredentials(defaultCredentialsPath(env, authority));
|
|
150
|
-
|
|
151
|
-
|
|
168
|
+
if (!creds || typeof creds.mcpUrl !== "string") return null;
|
|
169
|
+
try {
|
|
170
|
+
return normalizeAuthority(creds.mcpUrl) === authority ? creds : null;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (error instanceof Error && error.message.startsWith("invalid MCP authority")) return null;
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
152
175
|
}
|
|
153
176
|
|
|
154
177
|
/**
|
|
@@ -165,7 +188,10 @@ export function readCredentials(filePath) {
|
|
|
165
188
|
try {
|
|
166
189
|
const parsed = JSON.parse(text);
|
|
167
190
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
168
|
-
} catch {
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (error instanceof SyntaxError) return null;
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
169
195
|
}
|
|
170
196
|
|
|
171
197
|
/**
|
package/src/visual-parity.mjs
CHANGED
|
@@ -61,11 +61,14 @@ function parseConfig(tenantDir) {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
/** The checkout's `.tot/config.json`, or null when absent or
|
|
64
|
+
/** The checkout's `.tot/config.json`, or null when it is absent or malformed. Unreadable throws. */
|
|
65
65
|
function readTotConfig(tenantDir) {
|
|
66
66
|
try {
|
|
67
67
|
return JSON.parse(readFileSync(join(tenantDir, ".tot", "config.json"), "utf8"));
|
|
68
|
-
} catch {
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error?.code === "ENOENT" || error instanceof SyntaxError) return null;
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
function allowedNavigation(value, allowlist) {
|