@askalf/truecopy 0.7.0 → 0.8.0

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # truecopy
2
2
 
3
- [![marketplace watch](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Faskalf%2Ftruecopy%2Fwatch%2Fbadge.json)](https://github.com/askalf/truecopy/blob/watch/WATCH.md)
3
+ [![marketplace watch](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Faskalf%2Ftruecopy%2Fwatch%2Fbadge.json)](https://github.com/askalf/truecopy/blob/watch/WATCH.md) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/askalf/truecopy/badge)](https://scorecard.dev/viewer/?uri=github.com/askalf/truecopy)
4
4
 
5
5
  > _truecopy — **own your agent skills**. Vet, sign, and pin every skill & MCP server before it runs. Part of **[Own Your Stack](https://github.com/askalf)** — own your AI infrastructure instead of renting it by the token._
6
6
 
@@ -15,7 +15,7 @@ Agents install tools from places you don't control — MCP servers, skill market
15
15
  - **verifies** on every run / in CI that nothing **drifted** — a pinned skill whose bytes changed is a silent update or a supply-chain attack, and `truecopy verify` exits non-zero before it loads
16
16
  - **diffs** exactly what changed since you trusted it
17
17
 
18
- Deterministic and offline. truecopy shares **[warden](https://github.com/askalf/warden)**'s detection — so the two are a pair, not a duplicate: **truecopy vets the tool (provenance); warden contains the call (runtime).** *Vet it → contain it.*
18
+ Deterministic and offline. truecopy shares **[redstamp](https://github.com/askalf/redstamp)**'s detection — so the two are a pair, not a duplicate: **truecopy vets the tool (provenance); redstamp contains the call (runtime).** *Vet it → contain it.*
19
19
 
20
20
  ## Install
21
21
 
@@ -74,7 +74,7 @@ A silently-added, drifted, or poisoned tool is stripped from `tools/list` (the a
74
74
  truecopy guard -- npm start # refuses to launch (exit 1) if any pinned skill drifted or turned poisonous
75
75
  ```
76
76
 
77
- So truecopy spans the whole lifecycle: **scan → pin → verify (CI) → enforce (runtime).** Where [warden](https://github.com/askalf/warden) firewalls what a tool *does*, truecopy-mcp gates which tools *exist*.
77
+ So truecopy spans the whole lifecycle: **scan → pin → verify (CI) → enforce (runtime).** Where [redstamp](https://github.com/askalf/redstamp) firewalls what a tool *does*, truecopy-mcp gates which tools *exist*.
78
78
 
79
79
  ## Gate Claude Code skills
80
80
 
@@ -199,7 +199,7 @@ verify(); // { ok, results: [{ name, status: 'ok
199
199
 
200
200
  ## The agent-security stack
201
201
 
202
- Three composable layers, one defense: **[warden](https://github.com/askalf/warden)** contains the call · **[truecopy](https://github.com/askalf/truecopy)** vets the tool *(you are here)* · **[keeper](https://github.com/askalf/keeper)** holds the keys. Run all three together → **[agent-security-stack](https://github.com/askalf/agent-security-stack)**.
202
+ Three composable layers, one defense: **[redstamp](https://github.com/askalf/redstamp)** contains the call · **[truecopy](https://github.com/askalf/truecopy)** vets the tool *(you are here)* · **[strongroom](https://github.com/askalf/strongroom)** holds the keys. Run all three together → **[agent-security-stack](https://github.com/askalf/agent-security-stack)**.
203
203
 
204
204
  ---
205
205
  Part of **[Own Your Stack](https://github.com/askalf)** — own your AI infrastructure instead of renting it. Built by Thomas Sprayberry.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/truecopy",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "own your agent skills — vet, sign, and pin every skill & MCP server before it runs. The supply-chain gate for AI agents. Part of Own Your Stack.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -5,7 +5,7 @@ import { spawnSync } from 'node:child_process';
5
5
  import fs from 'node:fs';
6
6
  import os from 'node:os';
7
7
  import path from 'node:path';
8
- import { scan, pin, unpin, verify, diff, readLock, ensureKey, keyId, trustKey, untrustKey, listTrust, loadSkill, skillHash, scanSkill } from './index.mjs';
8
+ import { scan, pin, unpin, verify, diff, readLock, resolveLock, ensureKey, keyId, trustKey, untrustKey, listTrust, loadSkill, skillHash, scanSkill } from './index.mjs';
9
9
  import { discoverClaudeSkills, discoverClaudePluginSkills, discoverMarketplaceSkills, resolveClaudeSkill } from './claude.mjs';
10
10
 
11
11
  // This build's version — so `hook install` can PIN the gate command to a git tag
@@ -34,7 +34,9 @@ const sources = (() => {
34
34
  }
35
35
  return out;
36
36
  })();
37
- const lockPath = opt('--lock', 'canon.lock');
37
+ // Default lock: prefer truecopy.lock, transparently fall back to an existing
38
+ // canon.lock (pre-rename repos), else write truecopy.lock. `--lock` overrides.
39
+ const lockPath = resolveLock(opt('--lock', null) || null);
38
40
  const optTrust = () => { const t = opt('--trust', undefined); return typeof t === 'string' ? t : undefined; };
39
41
  // `--json` — machine-readable output for scan/verify/list/diff: one JSON document
40
42
  // on stdout, no ANSI, and the SAME exit codes (the CI gate contract is the exit
@@ -69,40 +71,43 @@ const mark = { ok: c(C.grn, '✓'), clean: c(C.grn, '✓'), flagged: c(C.red, '
69
71
  const findingLine = (f) => ` ${c(C.red, '☠')} ${f.tool}: ${f.flags.join('; ')}`;
70
72
 
71
73
  function usage() {
72
- out(`${c(C.bold, 'canon')} — own your agent skills · vet · sign · pin · verify
73
-
74
- canon scan <source...> poison-scan a skill / MCP manifest / directory
75
- canon add <source...> [--sign] vet + pin into ${lockPath} (refuses poisoned unless --force)
76
- canon scan --claude poison-scan every Claude Code skill (.claude/skills, project + user)
77
- canon add --claude [--sign] vet + pin them all
78
- canon scan --claude-plugins …and every skill shipped by installed marketplace plugins
79
- canon add --claude-plugins vet + pin those under their \`plugin:skill\` invocation name
80
- canon scan --marketplace <dir> poison-scan a CLONED marketplace or plugin repo (you fetch, canon scans)
81
- canon verify [--lock <file>] [--trust <file>] [--require-signed] re-check every pinned skill
74
+ out(`${c(C.bold, 'truecopy')} — own your agent skills · vet · sign · pin · verify
75
+
76
+ truecopy scan <source...> poison-scan a skill / MCP manifest / directory
77
+ truecopy add <source...> [--sign] vet + pin into ${lockPath} (refuses poisoned unless --force)
78
+ truecopy scan --claude poison-scan every Claude Code skill (.claude/skills, project + user)
79
+ truecopy add --claude [--sign] vet + pin them all
80
+ truecopy scan --claude-plugins …and every skill shipped by installed marketplace plugins
81
+ truecopy add --claude-plugins vet + pin those under their \`plugin:skill\` invocation name
82
+ truecopy scan --marketplace <dir> poison-scan a CLONED marketplace or plugin repo (you fetch, truecopy scans)
83
+ truecopy verify [--lock <file>] [--trust <file>] [--require-signed] re-check every pinned skill
82
84
  for drift / poisoning (--require-signed: also fail any entry
83
85
  without a valid signature from a trusted key)
84
- canon diff <source> [--name <n>] show what changed since it was pinned
85
- canon list show the pinned set
86
- canon remove <name...> un-pin a skill — drop its ${lockPath} entry (alias: unpin)
87
- canon guard [--lock <file>] [--require-signed] -- <cmd...> verify the lock, then run <cmd> only if it's clean
86
+ truecopy diff <source> [--name <n>] show what changed since it was pinned
87
+ truecopy list show the pinned set
88
+ truecopy remove <name...> un-pin a skill — drop its ${lockPath} entry (alias: unpin)
89
+ truecopy guard [--lock <file>] [--require-signed] -- <cmd...> verify the lock, then run <cmd> only if it's clean
88
90
  …scan / verify / list / diff take --json: machine-readable stdout, same exit codes
89
91
 
90
- canon key print this machine's public key + id (share it to be trusted)
91
- canon trust add <pubkey> --name <who> [--repo] trust a publisher's key (--repo → commit it to canon.trust)
92
- canon trust list show the trusted signing keys
93
- canon trust remove <id> stop trusting a key
92
+ truecopy key print this machine's public key + id (share it to be trusted)
93
+ truecopy trust add <pubkey> --name <who> [--repo] trust a publisher's key (--repo → commit it to truecopy.trust)
94
+ truecopy trust list show the trusted signing keys
95
+ truecopy trust remove <id> stop trusting a key
94
96
 
95
- canon hook claude [--strict] Claude Code PreToolUse hook: block a pinned skill that
97
+ truecopy hook claude [--strict] Claude Code PreToolUse hook: block a pinned skill that
96
98
  drifted or turned poisonous at the moment it's invoked
97
99
  (--strict: only pinned skills may run at all)
98
- canon hook install [--strict] [--user] [--settings <file>]
100
+ truecopy hook install [--strict] [--user] [--settings <file>]
99
101
  wire that hook into .claude/settings.json (idempotent;
100
102
  project file by default, --user for ~/.claude)
101
103
 
102
- canon-mcp [--lock] [--name] [--strict] -- <mcp-server cmd...>
104
+ truecopy-mcp [--lock] [--name] [--strict] -- <mcp-server cmd...>
103
105
  enforce the lock on a LIVE MCP server: only vetted,
104
106
  unmodified, unpoisoned tools reach the client
105
107
 
108
+ \`canon\` / \`canon-mcp\` remain as back-compat aliases, and an existing canon.lock /
109
+ canon.trust is read automatically (new pins write truecopy.lock / truecopy.trust).
110
+
106
111
  Exit 1 on any flagged / drifted / poisoned result — drop it in CI.`);
107
112
  }
108
113
 
@@ -285,7 +290,7 @@ function runTrust() {
285
290
  try {
286
291
  const repo = !!opt('--repo', false);
287
292
  const r = trustKey(pub, opt('--name', undefined), { repo });
288
- out(`${mark.ok} trusted ${c(C.bold, r.name)} ${c(C.dim, r.id)}${repo ? c(C.dim, ' · canon.trust') : ''}`);
293
+ out(`${mark.ok} trusted ${c(C.bold, r.name)} ${c(C.dim, r.id)}${repo ? c(C.dim, ' · truecopy.trust') : ''}`);
289
294
  return 0;
290
295
  } catch (e) { out(`${c(C.red, '✗')} ${e.message}`); return 1; }
291
296
  }
@@ -329,12 +334,14 @@ function runHookClaude() {
329
334
  const name = payload.tool_input && payload.tool_input.skill;
330
335
  if (!name) return 0;
331
336
 
332
- // lock: explicit --lock > <project>/canon.lock > ./canon.lock (hooks run in the project dir)
337
+ // lock: explicit --lock > truecopy.lock (project/cwd) > canon.lock (back-compat, project/cwd).
338
+ // Hooks run in the project dir; prefer the branded name, still honor a pre-rename canon.lock.
333
339
  const projectDir = process.env.CLAUDE_PROJECT_DIR || payload.cwd || process.cwd();
334
340
  const explicit = opt('--lock', undefined);
335
- const candidates = typeof explicit === 'string' ? [explicit] : [path.join(projectDir, 'canon.lock'), 'canon.lock'];
341
+ const candidates = typeof explicit === 'string' ? [explicit]
342
+ : [path.join(projectDir, 'truecopy.lock'), 'truecopy.lock', path.join(projectDir, 'canon.lock'), 'canon.lock'];
336
343
  const lp = candidates.find((p) => fs.existsSync(p));
337
- if (!lp) return strict ? deny(`no canon.lock — pin your skills first: canon add --claude`) : 0;
344
+ if (!lp) return strict ? deny(`no truecopy.lock — pin your skills first: truecopy add --claude`) : 0;
338
345
 
339
346
  let lock;
340
347
  try { lock = readLock(lp, { mustExist: true }); }
package/src/index.mjs CHANGED
@@ -3,12 +3,12 @@
3
3
  // pairs with warden's runtime firewall: vet it → contain it.
4
4
  import { loadSkill, skillHash } from './skill.mjs';
5
5
  import { scanSkill, detectionInfo } from './scan.mjs';
6
- import { readLock, writeLock, DEFAULT_LOCK } from './lock.mjs';
6
+ import { readLock, writeLock, DEFAULT_LOCK, LEGACY_LOCK, resolveLock } from './lock.mjs';
7
7
  import { signHash, verifyHashSig, keyId, ensureKey } from './sign.mjs';
8
8
  import { loadTrust, trustedSigner, trustKey, untrustKey, listTrust } from './trust.mjs';
9
9
  import { sha256, canonicalJson } from './hash.mjs';
10
10
 
11
- export { loadSkill, skillHash, scanSkill, detectionInfo, readLock, writeLock, DEFAULT_LOCK };
11
+ export { loadSkill, skillHash, scanSkill, detectionInfo, readLock, writeLock, DEFAULT_LOCK, LEGACY_LOCK, resolveLock };
12
12
  export { signHash, verifyHashSig, keyId, ensureKey };
13
13
  export { loadTrust, trustedSigner, trustKey, untrustKey, listTrust };
14
14
  export { claudeSkillRoots, discoverClaudeSkills, discoverClaudePluginSkills, discoverMarketplaceSkills, resolveClaudeSkill } from './claude.mjs';
@@ -35,7 +35,7 @@ export function scan(source) {
35
35
  }
36
36
 
37
37
  /** Pin a vetted skill into the lock. Refuses to pin a flagged skill unless force. */
38
- export function pin(source, { lockPath = DEFAULT_LOCK, sign = false, force = false, name } = {}) {
38
+ export function pin(source, { lockPath = resolveLock(), sign = false, force = false, name } = {}) {
39
39
  const skill = loadSkill(source);
40
40
  const s = scanSkill(skill);
41
41
  if (s.verdict === 'flagged' && !force) return { ok: false, reason: 'flagged', findings: s.findings, skill };
@@ -58,7 +58,7 @@ export function pin(source, { lockPath = DEFAULT_LOCK, sign = false, force = fal
58
58
  * The guided mirror of `pin`: hand-deleting from a signed lock is exactly the
59
59
  * manual editing canon exists to discourage. A no-op removal never writes (so it
60
60
  * can't create an empty lock), and a corrupt lock still fails CLOSED via readLock. */
61
- export function unpin(name, { lockPath = DEFAULT_LOCK } = {}) {
61
+ export function unpin(name, { lockPath = resolveLock() } = {}) {
62
62
  const lock = readLock(lockPath);
63
63
  if (!(name in lock.skills)) return 0;
64
64
  delete lock.skills[name];
@@ -69,7 +69,7 @@ export function unpin(name, { lockPath = DEFAULT_LOCK } = {}) {
69
69
  /** Re-derive every pinned skill and classify it against the lock.
70
70
  * Fails CLOSED on a missing or corrupt lock — a present-but-empty lock stays
71
71
  * ok:true (legitimately nothing pinned). */
72
- export function verify({ lockPath = DEFAULT_LOCK, trustPath, requireSigned = false } = {}) {
72
+ export function verify({ lockPath = resolveLock(), trustPath, requireSigned = false } = {}) {
73
73
  let lock;
74
74
  try { lock = readLock(lockPath, { mustExist: true }); }
75
75
  catch (e) { return { ok: false, error: e.message, results: [] }; }
@@ -129,7 +129,7 @@ function verifyOne(name, entry, trust) {
129
129
  }
130
130
 
131
131
  /** What changed in a source since it was pinned. */
132
- export function diff(source, { lockPath = DEFAULT_LOCK, name } = {}) {
132
+ export function diff(source, { lockPath = resolveLock(), name } = {}) {
133
133
  const skill = loadSkill(source);
134
134
  const key = name || skill.name;
135
135
  const entry = readLock(lockPath).skills[key];
package/src/lock.mjs CHANGED
@@ -1,9 +1,27 @@
1
- // canon.lock — the pinned, vetted set. One entry per trusted skill: where it came
2
- // from, the content hash you trusted, the scan verdict at pin time, and (optional)
3
- // a signature. `verify` re-derives the hash and flags any drift from this file.
1
+ // truecopy.lock — the pinned, vetted set. One entry per trusted skill: where it
2
+ // came from, the content hash you trusted, the scan verdict at pin time, and
3
+ // (optional) a signature. `verify` re-derives the hash and flags any drift from
4
+ // this file.
4
5
  import fs from 'node:fs';
6
+ import path from 'node:path';
5
7
 
6
- export const DEFAULT_LOCK = 'canon.lock';
8
+ // The lock filename was `canon.lock` before the rename. New locks are written as
9
+ // `truecopy.lock`, but an existing `canon.lock` is still read (see resolveLock)
10
+ // so a repo pinned before the rename keeps verifying with zero changes.
11
+ export const DEFAULT_LOCK = 'truecopy.lock';
12
+ export const LEGACY_LOCK = 'canon.lock';
13
+
14
+ /** Pick the lock file when the caller didn't pass one explicitly: prefer the
15
+ * branded `truecopy.lock`, transparently fall back to an existing `canon.lock`,
16
+ * and default to `truecopy.lock` when neither exists (so fresh pins are branded). */
17
+ export function resolveLock(explicit, dir = '.') {
18
+ if (typeof explicit === 'string' && explicit) return explicit;
19
+ for (const name of [DEFAULT_LOCK, LEGACY_LOCK]) {
20
+ const p = path.join(dir, name);
21
+ try { if (fs.existsSync(p)) return p; } catch {}
22
+ }
23
+ return path.join(dir, DEFAULT_LOCK);
24
+ }
7
25
 
8
26
  // `skills` is ALWAYS a NULL-PROTOTYPE map, on every readLock path — so a skill
9
27
  // keyed by a prototype member ("__proto__", "toString", "constructor", …) can't
@@ -27,15 +45,15 @@ export function readLock(p = DEFAULT_LOCK, { mustExist = false } = {}) {
27
45
  try { raw = fs.readFileSync(p, 'utf8'); }
28
46
  catch (e) {
29
47
  if (e && e.code === 'ENOENT') {
30
- if (mustExist) { const err = new Error(`no canon.lock at ${p}`); err.code = 'ELOCKMISSING'; throw err; }
48
+ if (mustExist) { const err = new Error(`no lock file at ${p}`); err.code = 'ELOCKMISSING'; throw err; }
31
49
  return emptyLock();
32
50
  }
33
51
  throw e;
34
52
  }
35
53
  let l;
36
54
  try { l = JSON.parse(raw); }
37
- catch (e) { const err = new Error(`canon.lock at ${p} is present but unparseable: ${e.message}`); err.code = 'ELOCKCORRUPT'; throw err; }
38
- if (!l || typeof l !== 'object' || Array.isArray(l)) { const err = new Error(`canon.lock at ${p} is not a lock object`); err.code = 'ELOCKCORRUPT'; throw err; }
55
+ catch (e) { const err = new Error(`lock file at ${p} is present but unparseable: ${e.message}`); err.code = 'ELOCKCORRUPT'; throw err; }
56
+ if (!l || typeof l !== 'object' || Array.isArray(l)) { const err = new Error(`lock file at ${p} is not a lock object`); err.code = 'ELOCKCORRUPT'; throw err; }
39
57
  return { version: 1, ...l, skills: asSkills(l.skills) };
40
58
  }
41
59
 
package/src/mcp-cli.mjs CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
- // canon-mcp — enforce canon.lock on a live MCP server. Point your MCP client at
3
- // canon-mcp [--lock canon.lock] [--name <pinned>] [--strict] -- <server cmd...>
4
- // Only vetted, unmodified, unpoisoned tools reach the client.
2
+ // truecopy-mcp — enforce the lock on a live MCP server. Point your MCP client at
3
+ // truecopy-mcp [--lock truecopy.lock] [--name <pinned>] [--strict] -- <server cmd...>
4
+ // Only vetted, unmodified, unpoisoned tools reach the client. (`canon-mcp` still
5
+ // works as an alias; an existing canon.lock is read automatically.)
5
6
  import { runGate } from './mcp.mjs';
7
+ import { resolveLock } from './lock.mjs';
6
8
 
7
9
  const argv = process.argv.slice(2);
8
10
  const sep = argv.indexOf('--');
@@ -16,7 +18,7 @@ const opt = (n, d) => {
16
18
  };
17
19
 
18
20
  if (!cmd.length || pre.includes('-h') || pre.includes('--help')) {
19
- process.stderr.write('usage: canon-mcp [--lock canon.lock] [--name <pinned>] [--strict] -- <mcp-server cmd...>\n');
21
+ process.stderr.write('usage: truecopy-mcp [--lock truecopy.lock] [--name <pinned>] [--strict] -- <mcp-server cmd...>\n');
20
22
  process.exit(cmd.length ? 0 : 2);
21
23
  }
22
- runGate({ command: cmd[0], args: cmd.slice(1), lockPath: opt('--lock', 'canon.lock'), name: opt('--name', null) || null, strict: !!opt('--strict', false) });
24
+ runGate({ command: cmd[0], args: cmd.slice(1), lockPath: resolveLock(opt('--lock', null) || null), name: opt('--name', null) || null, strict: !!opt('--strict', false) });
package/src/mcp.mjs CHANGED
@@ -7,7 +7,7 @@
7
7
  import { spawn } from 'node:child_process';
8
8
  import readline from 'node:readline';
9
9
  import { gateTools, toolHash } from './gate.mjs';
10
- import { readLock } from './lock.mjs';
10
+ import { readLock, resolveLock } from './lock.mjs';
11
11
 
12
12
  const blockReply = (id, name) =>
13
13
  JSON.stringify({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: `⛔ canon blocked an un-vetted / drifted tool: ${name}` }], isError: true } });
@@ -79,8 +79,8 @@ export function pickEntry(lock, name) {
79
79
  }
80
80
 
81
81
  /** Spawn the downstream server and wire the two gated streams together. */
82
- export function runGate({ command, args = [], lockPath = 'canon.lock', name = null, strict = false, onWarn } = {}) {
83
- const warn = onWarn || ((m) => process.stderr.write('[canon] ' + m + '\n'));
82
+ export function runGate({ command, args = [], lockPath = resolveLock(), name = null, strict = false, onWarn } = {}) {
83
+ const warn = onWarn || ((m) => process.stderr.write('[truecopy] ' + m + '\n'));
84
84
  const entry = pickEntry(readLock(lockPath), name);
85
85
  if (!entry) warn(`no pinned ${name ? '"' + name + '" ' : ''}MCP entry in ${lockPath} — every tool will be treated as unvetted (canon add it first)`);
86
86
  const child = spawn(command, args, { stdio: ['pipe', 'pipe', 'inherit'] });
package/src/trust.mjs CHANGED
@@ -11,9 +11,25 @@ import os from 'node:os';
11
11
  import path from 'node:path';
12
12
  import { loadKey, keyId } from './sign.mjs';
13
13
 
14
- export const DEFAULT_REPO_TRUST = 'canon.trust';
14
+ // The repo-committed trust file was `canon.trust` before the rename. New writes
15
+ // go to `truecopy.trust`, but an existing `canon.trust` is still read and written
16
+ // (see resolveRepoTrust) for back-compat. The private global store stays at
17
+ // `~/.canon/trust.json` / `$CANON_HOME` — an internal path kept stable so a
18
+ // user's existing trust decisions aren't lost by the rename.
19
+ export const DEFAULT_REPO_TRUST = 'truecopy.trust';
20
+ export const LEGACY_REPO_TRUST = 'canon.trust';
15
21
  const homeStore = () => path.join(process.env.CANON_HOME || os.homedir(), '.canon', 'trust.json');
16
22
 
23
+ /** The repo trust file to read/write: prefer branded `truecopy.trust`, fall back
24
+ * to an existing `canon.trust`, default to `truecopy.trust`. */
25
+ export function resolveRepoTrust(cwd = process.cwd()) {
26
+ for (const name of [DEFAULT_REPO_TRUST, LEGACY_REPO_TRUST]) {
27
+ const p = path.join(cwd, name);
28
+ try { if (fs.existsSync(p)) return p; } catch {}
29
+ }
30
+ return path.join(cwd, DEFAULT_REPO_TRUST);
31
+ }
32
+
17
33
  function readStore(file) {
18
34
  try { const j = JSON.parse(fs.readFileSync(file, 'utf8')); return Array.isArray(j?.keys) ? j.keys : []; }
19
35
  catch { return []; }
@@ -41,7 +57,7 @@ export function loadTrust({ trustPath, cwd = process.cwd() } = {}) {
41
57
  // key belongs in canon.trust if you want to verify what it signed.
42
58
  const self = loadKey({ envAllowed: false }); if (self) add({ name: 'self', publicKey: self.publicKey });
43
59
  for (const k of readStore(homeStore())) add(k);
44
- for (const k of readStore(trustPath || path.join(cwd, DEFAULT_REPO_TRUST))) add(k);
60
+ for (const k of readStore(trustPath || resolveRepoTrust(cwd))) add(k);
45
61
  return map;
46
62
  }
47
63
 
@@ -54,7 +70,7 @@ export function trustedSigner(publicKey, trust) {
54
70
 
55
71
  /** Add a publisher public key to the trust set (global by default, or repo canon.trust). */
56
72
  export function trustKey(publicKey, name, { repo = false, cwd = process.cwd() } = {}) {
57
- const file = repo ? path.join(cwd, DEFAULT_REPO_TRUST) : homeStore();
73
+ const file = repo ? resolveRepoTrust(cwd) : homeStore();
58
74
  const id = keyId(publicKey);
59
75
  if (!id) throw new Error('not a public key');
60
76
  const entry = { id, name: name || id.slice(0, 12), publicKey: String(publicKey).replace(/\r\n/g, '\n').trim() };