pgserve 2.4.0 → 2.6.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.
Files changed (36) hide show
  1. package/README.md +5 -8
  2. package/bin/pgserve-wrapper.cjs +23 -0
  3. package/bin/postgres-server.js +28 -0
  4. package/package.json +2 -1
  5. package/scripts/aggregate-manifest.sh +184 -0
  6. package/scripts/assemble-tarball.sh +191 -0
  7. package/scripts/audit-redaction-lint.js +349 -0
  8. package/scripts/build-binary.sh +213 -0
  9. package/scripts/fetch-postgres-bins.sh +234 -0
  10. package/scripts/postinstall.cjs +102 -18
  11. package/scripts/verify-published-artifacts.sh +211 -0
  12. package/src/audit/audit.js +134 -0
  13. package/src/cli-install.cjs +258 -26
  14. package/src/commands/doctor.js +465 -0
  15. package/src/commands/gc.js +276 -0
  16. package/src/commands/provision.js +396 -0
  17. package/src/commands/trust.js +187 -0
  18. package/src/commands/verify.js +360 -0
  19. package/src/cosign/cache-token.js +328 -0
  20. package/src/cosign/schema.js +97 -0
  21. package/src/cosign/trust-list.js +81 -0
  22. package/src/cosign/trust-store.js +250 -0
  23. package/src/cosign/verify-binary.js +277 -0
  24. package/src/gc/audit-log.js +150 -0
  25. package/src/gc/orphan-detection.js +190 -0
  26. package/src/gc/queries.js +193 -0
  27. package/src/lib/pg-query.js +145 -0
  28. package/src/lib/runtime-json.js +181 -0
  29. package/src/provision/advisory-lock.js +91 -0
  30. package/src/provision/db-naming.js +130 -0
  31. package/src/provision/fingerprint.js +144 -0
  32. package/src/schema/pgserve-meta.js +120 -0
  33. package/src/security/blocked-versions.js +103 -0
  34. package/src/upgrade/index.js +5 -0
  35. package/src/upgrade/steps/binary-cache-flush.js +2 -2
  36. package/src/upgrade/steps/cosign-meta-migration.js +123 -0
@@ -0,0 +1,187 @@
1
+ /**
2
+ * `pgserve trust` — manage the cosign trust list.
3
+ *
4
+ * pgserve singleton (v2.4) — `pgserve-singleton-no-proxy` wish, Group 3.
5
+ *
6
+ * Subverbs:
7
+ * pgserve trust list show hardcoded + user entries
8
+ * pgserve trust add <id> [flags] add a user entry
9
+ * pgserve trust remove <id> remove a user entry (refuses hardcoded)
10
+ *
11
+ * `add` flags (all required except where noted):
12
+ * --issuer <url> OIDC issuer URL
13
+ * --identity-regexp <regex> sigstore --certificate-identity-regexp value
14
+ * --publisher <name> package.json `pgserve.publisher` (optional)
15
+ * --description <text> human-readable summary (optional)
16
+ *
17
+ * Output modes:
18
+ * default human-readable table / status line
19
+ * --json emit a JSON object on stdout instead
20
+ *
21
+ * Exit codes:
22
+ * 0 success
23
+ * 1 user error (bad flags, unknown id, hardcoded id collision)
24
+ * 2 trust store on disk is malformed and must be repaired by hand
25
+ */
26
+
27
+ import { listAllTrust, addUserTrust, removeUserTrust } from '../cosign/trust-store.js';
28
+
29
+ const USAGE = `Usage: pgserve trust <list|add|remove> [args]
30
+
31
+ list show hardcoded + user entries
32
+ add <id> --issuer <url> --identity-regexp <re> [--publisher <name>] [--description <text>]
33
+ remove <id> remove a user entry (refuses hardcoded)
34
+
35
+ Common: --json emit JSON instead of human-readable output`;
36
+
37
+ function parseFlags(argv) {
38
+ const out = { positional: [], json: false, flags: {} };
39
+ for (let i = 0; i < argv.length; i++) {
40
+ const a = argv[i];
41
+ if (a === '--json') {
42
+ out.json = true;
43
+ continue;
44
+ }
45
+ if (a === '--help' || a === '-h') {
46
+ out.flags.help = true;
47
+ continue;
48
+ }
49
+ if (a.startsWith('--')) {
50
+ const name = a.slice(2);
51
+ const next = argv[i + 1];
52
+ if (next === undefined || next.startsWith('--')) {
53
+ out.flags[name] = true;
54
+ } else {
55
+ out.flags[name] = next;
56
+ i++;
57
+ }
58
+ continue;
59
+ }
60
+ out.positional.push(a);
61
+ }
62
+ return out;
63
+ }
64
+
65
+ function emit(json, payload, humanLine) {
66
+ if (json) {
67
+ process.stdout.write(JSON.stringify(payload) + '\n');
68
+ } else if (humanLine) {
69
+ process.stdout.write(humanLine + '\n');
70
+ }
71
+ }
72
+
73
+ function emitErr(json, code, message) {
74
+ if (json) {
75
+ process.stdout.write(JSON.stringify({ ok: false, error: { code, message } }) + '\n');
76
+ } else {
77
+ process.stderr.write(`pgserve trust: ${message}\n`);
78
+ }
79
+ }
80
+
81
+ function cmdList(opts) {
82
+ let entries;
83
+ try {
84
+ entries = listAllTrust();
85
+ } catch (err) {
86
+ emitErr(opts.json, err.code || 'ETRUSTSTORE', err.message);
87
+ return 2;
88
+ }
89
+ if (opts.json) {
90
+ emit(true, { ok: true, entries }, null);
91
+ return 0;
92
+ }
93
+ if (entries.length === 0) {
94
+ emit(false, null, 'pgserve trust: no entries');
95
+ return 0;
96
+ }
97
+ const widthId = Math.max(2, ...entries.map((e) => e.id.length));
98
+ const widthSrc = Math.max(6, ...entries.map((e) => e.source.length));
99
+ const widthPub = Math.max(9, ...entries.map((e) => (e.publisher || '').length));
100
+ const header = `${'id'.padEnd(widthId)} ${'source'.padEnd(widthSrc)} ${'publisher'.padEnd(widthPub)} identityRegexp`;
101
+ process.stdout.write(`${header}\n`);
102
+ process.stdout.write(`${'-'.repeat(header.length)}\n`);
103
+ for (const e of entries) {
104
+ process.stdout.write(
105
+ `${e.id.padEnd(widthId)} ${e.source.padEnd(widthSrc)} ${(e.publisher || '').padEnd(widthPub)} ${e.identityRegexp}\n`,
106
+ );
107
+ }
108
+ return 0;
109
+ }
110
+
111
+ function cmdAdd(opts) {
112
+ const id = opts.positional[1]; // [0]='add', [1]=id
113
+ if (!id) {
114
+ emitErr(opts.json, 'EUSAGE', `add requires an <id> argument\n\n${USAGE}`);
115
+ return 1;
116
+ }
117
+ const issuer = opts.flags.issuer;
118
+ const identityRegexp = opts.flags['identity-regexp'];
119
+ if (typeof issuer !== 'string' || !issuer) {
120
+ emitErr(opts.json, 'EUSAGE', '--issuer <url> is required');
121
+ return 1;
122
+ }
123
+ if (typeof identityRegexp !== 'string' || !identityRegexp) {
124
+ emitErr(opts.json, 'EUSAGE', '--identity-regexp <regex> is required');
125
+ return 1;
126
+ }
127
+ const candidate = {
128
+ id,
129
+ issuer,
130
+ identityRegexp,
131
+ publisher: typeof opts.flags.publisher === 'string' ? opts.flags.publisher : '',
132
+ description: typeof opts.flags.description === 'string' ? opts.flags.description : '',
133
+ };
134
+ let entry;
135
+ try {
136
+ entry = addUserTrust(candidate);
137
+ } catch (err) {
138
+ emitErr(opts.json, err.code || 'ETRUSTADD', err.message);
139
+ return err.code === 'ETRUSTSTORE' ? 2 : 1;
140
+ }
141
+ emit(opts.json, { ok: true, entry }, `pgserve trust: added "${entry.id}"`);
142
+ return 0;
143
+ }
144
+
145
+ function cmdRemove(opts) {
146
+ const id = opts.positional[1];
147
+ if (!id) {
148
+ emitErr(opts.json, 'EUSAGE', `remove requires an <id> argument\n\n${USAGE}`);
149
+ return 1;
150
+ }
151
+ let removed;
152
+ try {
153
+ removed = removeUserTrust(id);
154
+ } catch (err) {
155
+ emitErr(opts.json, err.code || 'ETRUSTREMOVE', err.message);
156
+ return err.code === 'ETRUSTSTORE' ? 2 : 1;
157
+ }
158
+ if (!removed) {
159
+ emitErr(opts.json, 'ENOENT', `no user trust entry with id "${id}"`);
160
+ return 1;
161
+ }
162
+ emit(opts.json, { ok: true, removed: id }, `pgserve trust: removed "${id}"`);
163
+ return 0;
164
+ }
165
+
166
+ export async function runTrust(argv = []) {
167
+ const opts = parseFlags(argv);
168
+ if (opts.flags.help || opts.positional.length === 0) {
169
+ process.stdout.write(USAGE + '\n');
170
+ return opts.flags.help ? 0 : 1;
171
+ }
172
+ const verb = opts.positional[0];
173
+ switch (verb) {
174
+ case 'list':
175
+ return cmdList(opts);
176
+ case 'add':
177
+ return cmdAdd(opts);
178
+ case 'remove':
179
+ case 'rm':
180
+ return cmdRemove(opts);
181
+ default:
182
+ emitErr(opts.json, 'EUSAGE', `unknown subverb "${verb}"\n\n${USAGE}`);
183
+ return 1;
184
+ }
185
+ }
186
+
187
+ export const __testInternals = Object.freeze({ parseFlags });
@@ -0,0 +1,360 @@
1
+ /**
2
+ * `pgserve verify <binary-path>` — cosign-keyless-OIDC verification.
3
+ *
4
+ * pgserve singleton (v2.4) — `pgserve-singleton-no-proxy` wish, Group 4.
5
+ *
6
+ * Flow:
7
+ * 1. Resolve target binary, compute realpath + sha256 + size + mtime.
8
+ * 2. Look up the HMAC-signed cache at `$XDG_STATE_HOME/pgserve/verified/
9
+ * <fingerprint>.token`. If valid (HMAC matches, sliding expiry not
10
+ * lapsed, binary attestation matches mtime/size) → short-circuit.
11
+ * 3. Otherwise call `verifyBinary()` (cosign verify-blob against the
12
+ * hardcoded trust list per `src/cosign/trust-list.js`).
13
+ * 4. On success: persist the cache token (mode 0600). On failure: emit
14
+ * a diagnostic and exit non-zero.
15
+ *
16
+ * Flags:
17
+ * --json — emit machine-readable result on stdout
18
+ * --skip-sigstore — bypass cosign and consult the operator's
19
+ * offline trust file. Refuses unless the file
20
+ * records at least one offline-cosign-key entry
21
+ * (managed by G3's `pgserve trust add`).
22
+ * --bundle <path> — override the sigstore bundle sidecar path
23
+ * --cosign-bin <path> — override the cosign executable
24
+ * --allow-fetch — let cosign be fetched if missing on PATH
25
+ * --no-cache — never read or write the verified-cache token
26
+ *
27
+ * Exit codes:
28
+ * 0 — verified (fresh or cache hit)
29
+ * 2 — verification failed (cosign rejected, tampered binary, ...)
30
+ * 3 — invocation problem (--skip-sigstore without pretrusted key,
31
+ * missing binary, missing bundle, no cosign on PATH, ...)
32
+ */
33
+
34
+ import fs from 'node:fs';
35
+ import os from 'node:os';
36
+ import path from 'node:path';
37
+
38
+ import {
39
+ buildTokenPayload,
40
+ computeBinaryAttestation,
41
+ deleteCacheToken,
42
+ getStateDir,
43
+ readCacheToken,
44
+ touchCacheToken,
45
+ writeCacheToken,
46
+ } from '../cosign/cache-token.js';
47
+ import { sha256File, verifyBinary } from '../cosign/verify-binary.js';
48
+
49
+ const EXIT_OK = 0;
50
+ const EXIT_VERIFY_FAILED = 2;
51
+ const EXIT_INVOCATION = 3;
52
+
53
+ /**
54
+ * Compute the cache fingerprint for a binary. We use the realpath +
55
+ * sha256 first 32 chars so two distinct binaries get distinct cache
56
+ * entries even if they share a directory layout, while keeping the
57
+ * filename short enough to be readable in `ls`.
58
+ */
59
+ export function computeFingerprint(binaryRealpath, sha256) {
60
+ return `${path.basename(binaryRealpath).replace(/[^A-Za-z0-9._-]/g, '_')}.${sha256.slice(0, 16)}`;
61
+ }
62
+
63
+ function getTrustFilePath() {
64
+ if (process.env.PGSERVE_TRUST_FILE) return process.env.PGSERVE_TRUST_FILE;
65
+ const home = process.env.HOME || os.homedir();
66
+ return path.join(home, '.pgserve', 'trust', 'identities.json');
67
+ }
68
+
69
+ /**
70
+ * Load the operator-managed offline trust file. G3's `pgserve trust add
71
+ * --offline-cosign-key` writes to this path; in G4 we only consume it.
72
+ *
73
+ * Expected shape:
74
+ * {
75
+ * offlineKeys: [
76
+ * { id: '<short-id>', publisher: '<package>', keyFingerprint: '...',
77
+ * addedAt: '<ISO>' },
78
+ * ...
79
+ * ]
80
+ * }
81
+ */
82
+ function readOfflineTrust() {
83
+ const file = getTrustFilePath();
84
+ if (!fs.existsSync(file)) return { ok: false, reason: 'trust-file-missing', file };
85
+ let raw;
86
+ try {
87
+ raw = fs.readFileSync(file, 'utf8');
88
+ } catch (err) {
89
+ return { ok: false, reason: 'trust-file-unreadable', detail: err.message, file };
90
+ }
91
+ let doc;
92
+ try {
93
+ doc = JSON.parse(raw);
94
+ } catch (err) {
95
+ return { ok: false, reason: 'trust-file-malformed', detail: err.message, file };
96
+ }
97
+ const keys = Array.isArray(doc?.offlineKeys) ? doc.offlineKeys : null;
98
+ if (!keys || keys.length === 0) {
99
+ return { ok: false, reason: 'no-offline-keys', file };
100
+ }
101
+ return { ok: true, keys, file };
102
+ }
103
+
104
+ function parseArgs(args) {
105
+ const opts = {
106
+ binaryPath: null,
107
+ json: false,
108
+ skipSigstore: false,
109
+ bundlePath: null,
110
+ cosignBin: null,
111
+ allowFetch: false,
112
+ noCache: false,
113
+ };
114
+ for (let i = 0; i < args.length; i++) {
115
+ const a = args[i];
116
+ if (a === '--json') opts.json = true;
117
+ else if (a === '--skip-sigstore') opts.skipSigstore = true;
118
+ else if (a === '--allow-fetch') opts.allowFetch = true;
119
+ else if (a === '--no-cache') opts.noCache = true;
120
+ else if (a === '--bundle') opts.bundlePath = args[++i];
121
+ else if (a === '--cosign-bin') opts.cosignBin = args[++i];
122
+ else if (a === '--help' || a === '-h') {
123
+ printHelp(process.stdout);
124
+ return { exit: EXIT_OK };
125
+ } else if (a.startsWith('-')) {
126
+ process.stderr.write(`pgserve verify: unknown option ${JSON.stringify(a)}\n`);
127
+ return { exit: EXIT_INVOCATION };
128
+ } else if (opts.binaryPath === null) {
129
+ opts.binaryPath = a;
130
+ } else {
131
+ process.stderr.write(`pgserve verify: unexpected positional argument ${JSON.stringify(a)}\n`);
132
+ return { exit: EXIT_INVOCATION };
133
+ }
134
+ }
135
+ if (!opts.binaryPath) {
136
+ printHelp(process.stderr);
137
+ return { exit: EXIT_INVOCATION };
138
+ }
139
+ return { opts };
140
+ }
141
+
142
+ function printHelp(stream) {
143
+ stream.write(`pgserve verify <binary-path> [options]
144
+
145
+ Verify a binary against the cosign keyless OIDC trust list. On success,
146
+ persists an HMAC-signed cache token so subsequent invocations short-circuit
147
+ the cosign call until the binary changes (mtime/size) or the sliding
148
+ expiry lapses (1h idle / 7d max).
149
+
150
+ Options:
151
+ --json Emit a machine-readable JSON result on stdout
152
+ --skip-sigstore Bypass cosign — requires \`pgserve trust add\` (G3)
153
+ --bundle <path> Override the sigstore bundle sidecar path
154
+ (default: <binary>.bundle)
155
+ --cosign-bin <path> Override the cosign executable
156
+ --allow-fetch Allow downloading cosign if missing
157
+ --no-cache Never read or write the verified-cache token
158
+ --help, -h Show this help
159
+
160
+ Exit codes:
161
+ 0 Verified (fresh or cache hit)
162
+ 2 Verification failed
163
+ 3 Invocation problem (missing binary/bundle/cosign/pretrusted key)
164
+ `);
165
+ }
166
+
167
+ function emit({ json }, payload) {
168
+ if (json) {
169
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
170
+ return;
171
+ }
172
+ if (payload.ok) {
173
+ const tag = payload.cached ? 'cached' : 'verified';
174
+ process.stdout.write(`pgserve verify: ${tag} ${payload.binary} as ${payload.identity} (${payload.tier})\n`);
175
+ if (payload.cached === false) {
176
+ process.stdout.write(`pgserve verify: cache token written → ${payload.cacheFile}\n`);
177
+ }
178
+ return;
179
+ }
180
+ process.stderr.write(`pgserve verify: FAILED — ${payload.reason}${payload.detail ? `: ${payload.detail}` : ''}\n`);
181
+ if (payload.identityChain && payload.identityChain.length > 0) {
182
+ process.stderr.write(`pgserve verify: trust roots tried: ${JSON.stringify(payload.identityChain)}\n`);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Run the verify command. `argv` is the bare argument list AFTER the
188
+ * `verify` token. Returns an integer exit code.
189
+ */
190
+ export function runVerify(argv) {
191
+ const parsed = parseArgs(argv);
192
+ if (parsed.exit !== undefined) return parsed.exit;
193
+ const opts = parsed.opts;
194
+
195
+ const binaryPath = path.resolve(opts.binaryPath);
196
+ if (!fs.existsSync(binaryPath)) {
197
+ emit(opts, { ok: false, reason: 'binary-missing', detail: binaryPath });
198
+ return EXIT_INVOCATION;
199
+ }
200
+
201
+ let attestation;
202
+ try {
203
+ attestation = computeBinaryAttestation(binaryPath);
204
+ } catch (err) {
205
+ emit(opts, { ok: false, reason: 'binary-attestation-failed', detail: err.message });
206
+ return EXIT_INVOCATION;
207
+ }
208
+ const sha256 = sha256File(binaryPath);
209
+ const fingerprint = computeFingerprint(attestation.realpath, sha256);
210
+
211
+ // ── Cache lookup ─────────────────────────────────────────────────────
212
+ if (!opts.noCache) {
213
+ const cache = readCacheToken(fingerprint, { binaryAttestation: attestation });
214
+ if (cache.ok) {
215
+ // PR #79 P1 security fix: honor the requested tier strictly. Without
216
+ // this gate, a token written under `--skip-sigstore` (tier:self_signed)
217
+ // would be accepted on a subsequent run WITHOUT `--skip-sigstore`,
218
+ // letting the operator bypass cosign verification entirely. The fix:
219
+ // - default invocation (no --skip-sigstore) requires tier:cosign_signed
220
+ // - --skip-sigstore invocation requires tier:self_signed
221
+ // Mismatched-tier cache hits are treated as cache misses (fall through
222
+ // to re-verify under the requested tier).
223
+ const cachedTier = cache.payload.tier;
224
+ const expectedTier = opts.skipSigstore ? 'self_signed' : 'cosign_signed';
225
+ if (cachedTier === expectedTier) {
226
+ // Tier matches — bump lastUsedAt and return.
227
+ touchCacheToken(cache.payload, {});
228
+ emit(opts, {
229
+ ok: true,
230
+ cached: true,
231
+ binary: binaryPath,
232
+ identity: cache.payload.identity,
233
+ tier: cachedTier,
234
+ sha256: cache.payload.sha256 || sha256,
235
+ cacheFile: cache.file,
236
+ });
237
+ return EXIT_OK;
238
+ }
239
+ // Tier mismatch — fall through. Do NOT delete the cache token: the
240
+ // existing token is valid for its own tier; we just need a fresh
241
+ // verification under the currently-requested tier.
242
+ }
243
+ // Stale binary attestation invalidates the cache so the new fingerprint
244
+ // wins. We delete defensively when the binary changed under us.
245
+ if (cache.reason === 'binary-changed') {
246
+ deleteCacheToken(fingerprint, {});
247
+ }
248
+ }
249
+
250
+ // ── --skip-sigstore path ─────────────────────────────────────────────
251
+ if (opts.skipSigstore) {
252
+ const trust = readOfflineTrust();
253
+ if (!trust.ok) {
254
+ emit(opts, {
255
+ ok: false,
256
+ reason: 'skip-sigstore-without-pretrusted-key',
257
+ detail:
258
+ `--skip-sigstore requires an offline trust entry. None found (${trust.reason}). `
259
+ + 'Operators must run `pgserve trust add --offline-cosign-key '
260
+ + '<key-file> --identity <id>` once Group 3 of the singleton wish ships. '
261
+ + `Trust file path: ${trust.file}`,
262
+ });
263
+ return EXIT_INVOCATION;
264
+ }
265
+ // Operator vouched for the binary via an offline-cosign-key entry; we
266
+ // record it as `self_signed` tier (NOT cosign_signed — this is a less
267
+ // strong attestation than a Sigstore OIDC chain).
268
+ const identity = trust.keys[0].id;
269
+ const payload = buildTokenPayload({
270
+ fingerprint,
271
+ binary: attestation,
272
+ identity,
273
+ tier: 'self_signed',
274
+ sha256,
275
+ });
276
+ let cacheFile = null;
277
+ if (!opts.noCache) {
278
+ try {
279
+ cacheFile = writeCacheToken(payload, {});
280
+ } catch (err) {
281
+ emit(opts, { ok: false, reason: 'cache-write-failed', detail: err.message });
282
+ return EXIT_VERIFY_FAILED;
283
+ }
284
+ }
285
+ emit(opts, {
286
+ ok: true,
287
+ cached: false,
288
+ binary: binaryPath,
289
+ identity,
290
+ tier: 'self_signed',
291
+ sha256,
292
+ cacheFile,
293
+ skipSigstore: true,
294
+ });
295
+ return EXIT_OK;
296
+ }
297
+
298
+ // ── Cosign path ──────────────────────────────────────────────────────
299
+ const result = verifyBinary(binaryPath, {
300
+ cosignBin: opts.cosignBin || process.env.PGSERVE_COSIGN_BIN || undefined,
301
+ bundlePath: opts.bundlePath || undefined,
302
+ allowFetch: opts.allowFetch === true,
303
+ });
304
+
305
+ if (!result.ok) {
306
+ emit(opts, {
307
+ ok: false,
308
+ reason: result.reason,
309
+ detail: result.detail,
310
+ identityChain: result.identityChain,
311
+ });
312
+ if (result.reason === 'binary-missing'
313
+ || result.reason === 'binary-unreadable'
314
+ || result.reason === 'binary-not-a-file'
315
+ || result.reason === 'bundle-missing'
316
+ || result.reason === 'cosign-missing'
317
+ || result.reason === 'empty-trust-list'
318
+ || result.reason === 'invalid-args') {
319
+ return EXIT_INVOCATION;
320
+ }
321
+ return EXIT_VERIFY_FAILED;
322
+ }
323
+
324
+ let cacheFile = null;
325
+ if (!opts.noCache) {
326
+ try {
327
+ const payload = buildTokenPayload({
328
+ fingerprint,
329
+ binary: attestation,
330
+ identity: result.identity,
331
+ tier: result.tier,
332
+ sha256: result.sha256,
333
+ });
334
+ cacheFile = writeCacheToken(payload, {});
335
+ } catch (err) {
336
+ emit(opts, { ok: false, reason: 'cache-write-failed', detail: err.message });
337
+ return EXIT_VERIFY_FAILED;
338
+ }
339
+ }
340
+ emit(opts, {
341
+ ok: true,
342
+ cached: false,
343
+ binary: binaryPath,
344
+ identity: result.identity,
345
+ publisher: result.publisher,
346
+ tier: result.tier,
347
+ sha256: result.sha256,
348
+ cacheFile,
349
+ bundle: result.bundle,
350
+ cosignBin: result.cosignBin,
351
+ });
352
+ return EXIT_OK;
353
+ }
354
+
355
+ // Convenience export so tests can introspect paths without re-implementing.
356
+ export const _internals = {
357
+ computeFingerprint,
358
+ getStateDir,
359
+ getTrustFilePath,
360
+ };