@moonglade/provenance 0.0.0-stage → 2.6.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.
@@ -0,0 +1,410 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.HOOK_USAGE = exports.HOOK_FLAGS = exports.PRE_COMMIT_SCRIPT = exports.HookConflictError = exports.HOOK_MARKER = void 0;
37
+ exports.findGitRoot = findGitRoot;
38
+ exports.resolveHooksDir = resolveHooksDir;
39
+ exports.installGitHook = installGitHook;
40
+ exports.writeProvenanceConfig = writeProvenanceConfig;
41
+ exports.installHooks = installHooks;
42
+ exports.runInstallHooksCli = runInstallHooksCli;
43
+ const fs = __importStar(require("fs/promises"));
44
+ const path = __importStar(require("path"));
45
+ const readline = __importStar(require("readline"));
46
+ const child_process_1 = require("child_process");
47
+ /** Marker line (line 2 of the generated hook). A hook is ours only if its whole content equals a known generated version. */
48
+ exports.HOOK_MARKER = '# moonglade-provenance-hook v1';
49
+ class HookConflictError extends Error {
50
+ hookPath;
51
+ constructor(hookPath) {
52
+ super(`${hookPath} already exists and is not an unmodified hook generated by Moonglade Provenance (it may be foreign or edited). It was left unchanged. Re-run with --force to back it up and replace it.`);
53
+ this.hookPath = hookPath;
54
+ this.name = 'HookConflictError';
55
+ }
56
+ }
57
+ exports.HookConflictError = HookConflictError;
58
+ function git(cwd, args) {
59
+ return new Promise(resolve => {
60
+ (0, child_process_1.execFile)('git', args, { cwd, windowsHide: true }, (err, stdout) => {
61
+ if (err)
62
+ resolve({ ok: false, missing: err.code === 'ENOENT', out: '' });
63
+ else
64
+ resolve({ ok: true, missing: false, out: String(stdout).trim() });
65
+ });
66
+ });
67
+ }
68
+ async function findGitRoot(startDir = process.cwd()) {
69
+ let curr = path.resolve(startDir);
70
+ while (true) {
71
+ try {
72
+ const stat = await fs.stat(path.join(curr, '.git'));
73
+ if (stat.isDirectory())
74
+ return curr;
75
+ }
76
+ catch {
77
+ // not in this dir
78
+ }
79
+ const parent = path.dirname(curr);
80
+ if (parent === curr)
81
+ return null;
82
+ curr = parent;
83
+ }
84
+ }
85
+ /**
86
+ * Hooks directory as git itself resolves it (`git rev-parse --git-path hooks`), so linked worktrees,
87
+ * submodules and core.hooksPath work. Falls back to a `.git` directory only when git is not installed.
88
+ * Returns null when cwd is not inside a repository.
89
+ */
90
+ async function resolveHooksDir(cwd = process.cwd()) {
91
+ const r = await git(cwd, ['rev-parse', '--git-path', 'hooks']);
92
+ if (r.ok)
93
+ return path.resolve(cwd, r.out);
94
+ if (r.missing) {
95
+ const root = await findGitRoot(cwd);
96
+ return root ? path.join(root, '.git', 'hooks') : null;
97
+ }
98
+ return null;
99
+ }
100
+ async function resolveTopLevel(cwd) {
101
+ const r = await git(cwd, ['rev-parse', '--show-toplevel']);
102
+ return r.ok ? path.resolve(cwd, r.out) : path.resolve(cwd);
103
+ }
104
+ /**
105
+ * The generated pre-commit hook. Written for bash (not POSIX sh): it reads NUL-delimited names (`read -d ''`)
106
+ * so file names with spaces, non-ASCII characters (git would quote them otherwise) or newlines are all checked.
107
+ * Fail closed: if the staged list cannot be read, the commit is blocked.
108
+ */
109
+ exports.PRE_COMMIT_SCRIPT = `#!/usr/bin/env bash
110
+ ${exports.HOOK_MARKER}
111
+ # Moonglade Provenance - Git Pre-Commit Verification Gate (requires bash)
112
+ # Blocks the commit when a staged artifact that has a provenance sidecar no longer matches it.
113
+
114
+ echo "Moonglade Provenance: Verifying staged artifacts..."
115
+
116
+ TMP_LIST=$(mktemp) || exit 1
117
+ trap 'rm -f "$TMP_LIST"' EXIT
118
+ if ! git diff --cached --name-only -z --diff-filter=ACMR > "$TMP_LIST"; then
119
+ echo "Moonglade Provenance: could not list staged files; commit aborted."
120
+ exit 1
121
+ fi
122
+
123
+ FAILED=0
124
+ while IFS= read -r -d '' FILE; do
125
+ case "$FILE" in
126
+ *.provenance.json|*.provenance-registry.json) continue ;;
127
+ *.md|*.csv|*.json|*.sql|*.yml|*.yaml) ;;
128
+ *) continue ;;
129
+ esac
130
+ [ -f "$FILE" ] || continue
131
+ [ -f "$FILE.provenance.json" ] || continue
132
+ TARGET="$FILE"
133
+ case "$TARGET" in -*) TARGET="./$TARGET" ;; esac
134
+ if ! npx --no-install @moonglade/provenance badge "$TARGET" --short < /dev/null > /dev/null 2>&1; then
135
+ echo "[PROVENANCE DRIFT] $FILE has mutated or failed verification!"
136
+ FAILED=1
137
+ fi
138
+ done < "$TMP_LIST"
139
+
140
+ if [ "$FAILED" -ne 0 ]; then
141
+ echo ""
142
+ echo "Commit aborted by Moonglade Provenance Gate."
143
+ echo "Please re-seal mutated artifacts before committing:"
144
+ echo " npx @moonglade/provenance seal-provisional <file>"
145
+ echo ""
146
+ exit 1
147
+ fi
148
+
149
+ echo "All staged artifacts verified."
150
+ exit 0
151
+ `;
152
+ /** Generated hook contents this tool has shipped. A hook is ours only if its content equals one of these exactly. */
153
+ const KNOWN_GENERATED_HOOKS = [exports.PRE_COMMIT_SCRIPT];
154
+ function backupStamp() {
155
+ return new Date().toISOString().replace(/\.\d+Z$/, 'Z').replace(/[-:]/g, '');
156
+ }
157
+ /** Copy a file to `<file>.bak-<UTC timestamp>` (exclusive create, counter suffix on collision). */
158
+ async function backupFile(file) {
159
+ const stamp = backupStamp();
160
+ for (let i = 0;; i++) {
161
+ const candidate = `${file}.bak-${stamp}${i ? '-' + i : ''}`;
162
+ try {
163
+ await fs.copyFile(file, candidate, 1 /* COPYFILE_EXCL */);
164
+ return candidate;
165
+ }
166
+ catch (e) {
167
+ if (e.code !== 'EEXIST')
168
+ throw e;
169
+ }
170
+ }
171
+ }
172
+ /**
173
+ * Install one hook script. An existing hook is replaced only if its content equals a known generated version
174
+ * (`known`, default: the shipped hook). Anything else, including our own hook with an edit or a foreign hook
175
+ * that merely mentions the marker, is refused unless `force`, and a forced replace is backed up first.
176
+ */
177
+ async function installGitHook(hooksDir, hookName, scriptContent, opts = {}) {
178
+ await fs.mkdir(hooksDir, { recursive: true });
179
+ const hookPath = path.join(hooksDir, hookName);
180
+ const known = opts.known ?? KNOWN_GENERATED_HOOKS;
181
+ let existing = null;
182
+ try {
183
+ existing = await fs.readFile(hookPath, 'utf8');
184
+ }
185
+ catch (e) {
186
+ if (e.code !== 'ENOENT')
187
+ throw e;
188
+ }
189
+ let action = 'created';
190
+ let backup;
191
+ if (existing !== null) {
192
+ if (existing === scriptContent) {
193
+ action = 'unchanged';
194
+ }
195
+ else if (known.includes(existing)) {
196
+ action = 'updated';
197
+ }
198
+ else {
199
+ if (!opts.force)
200
+ throw new HookConflictError(hookPath);
201
+ action = 'replaced';
202
+ backup = await backupFile(hookPath);
203
+ }
204
+ }
205
+ if (action !== 'unchanged') {
206
+ await fs.writeFile(hookPath, scriptContent, { mode: 0o755 });
207
+ }
208
+ await fs.chmod(hookPath, 0o755);
209
+ return backup ? { path: hookPath, action, backup } : { path: hookPath, action };
210
+ }
211
+ function manifestPayload(selection) {
212
+ const payload = {
213
+ standard: MANIFEST_STANDARD,
214
+ hooks: selection
215
+ };
216
+ if (selection.mcpVerifyRead || selection.mcpAutoSeal) {
217
+ payload.mcpServer = { command: 'npx', args: ['-y', '@moonglade/provenance', 'provenance-gate'] };
218
+ }
219
+ return payload;
220
+ }
221
+ /** Previously recorded choices from an existing manifest, or null when absent or unreadable. */
222
+ async function readRecordedHooks(file) {
223
+ let raw;
224
+ try {
225
+ raw = await fs.readFile(file, 'utf8');
226
+ }
227
+ catch (e) {
228
+ if (e.code === 'ENOENT')
229
+ return { parsed: null, raw: null };
230
+ throw e;
231
+ }
232
+ try {
233
+ const parsed = JSON.parse(raw);
234
+ return { parsed: parsed && typeof parsed === 'object' ? parsed : null, raw };
235
+ }
236
+ catch {
237
+ return { parsed: null, raw };
238
+ }
239
+ }
240
+ const MANIFEST_STANDARD = 'Moonglade Provenance Hook Manifest v1';
241
+ /** True only for a manifest of exactly the shape this tool writes. Anything else (user keys, other standard, bad JSON) is backed up before a rewrite. */
242
+ function isOurManifest(parsed) {
243
+ if (!parsed || parsed.standard !== MANIFEST_STANDARD)
244
+ return false;
245
+ const allowed = new Set(['standard', 'installedAt', 'hooks', 'mcpServer']);
246
+ if (!Object.keys(parsed).every(k => allowed.has(k)))
247
+ return false;
248
+ const hooks = parsed.hooks;
249
+ if (!hooks || typeof hooks !== 'object')
250
+ return false;
251
+ const hk = new Set(['preCommit', 'mcpVerifyRead', 'mcpAutoSeal']);
252
+ return Object.keys(hooks).every(k => hk.has(k) && typeof hooks[k] === 'boolean');
253
+ }
254
+ /**
255
+ * Write the manifest. An existing manifest that is not exactly in the shape this tool writes is backed up first.
256
+ */
257
+ async function writeProvenanceConfig(dir, selection) {
258
+ const configPath = path.join(dir, '.provenance-hooks.json');
259
+ const payload = manifestPayload(selection);
260
+ const { parsed, raw } = await readRecordedHooks(configPath);
261
+ let backup;
262
+ if (raw !== null && !isOurManifest(parsed))
263
+ backup = await backupFile(configPath);
264
+ const out = { standard: payload.standard, installedAt: new Date().toISOString(), ...payload };
265
+ await fs.writeFile(configPath, JSON.stringify(out, null, 2) + '\n', 'utf8');
266
+ return backup ? { path: configPath, backup } : { path: configPath };
267
+ }
268
+ /**
269
+ * Install exactly the selected gates. Nothing is enabled that was not selected. The manifest records the
270
+ * selected options merged with earlier recorded MCP choices (never switched off silently) and with reality:
271
+ * preCommit is true when an unmodified generated pre-commit hook is installed.
272
+ */
273
+ async function installHooks(opts) {
274
+ const cwd = path.resolve(opts.cwd ?? process.cwd());
275
+ const selected = {
276
+ preCommit: opts.preCommit === true,
277
+ mcpVerifyRead: opts.mcpVerifyRead === true,
278
+ mcpAutoSeal: opts.mcpAutoSeal === true
279
+ };
280
+ let hooksDir = null;
281
+ let hook = null;
282
+ if (selected.preCommit) {
283
+ hooksDir = await resolveHooksDir(cwd);
284
+ if (!hooksDir)
285
+ throw new Error(`pre-commit hook requested but ${cwd} is not inside a git repository`);
286
+ hook = await installGitHook(hooksDir, 'pre-commit', exports.PRE_COMMIT_SCRIPT, { force: opts.force });
287
+ }
288
+ else {
289
+ hooksDir = await resolveHooksDir(cwd);
290
+ }
291
+ const top = await resolveTopLevel(cwd);
292
+ const recorded = (await readRecordedHooks(path.join(top, '.provenance-hooks.json'))).parsed?.hooks;
293
+ let installed = false;
294
+ if (hooksDir) {
295
+ try {
296
+ installed = KNOWN_GENERATED_HOOKS.includes(await fs.readFile(path.join(hooksDir, 'pre-commit'), 'utf8'));
297
+ }
298
+ catch { /* no hook */ }
299
+ }
300
+ const selection = {
301
+ preCommit: selected.preCommit || installed,
302
+ mcpVerifyRead: selected.mcpVerifyRead || recorded?.mcpVerifyRead === true,
303
+ mcpAutoSeal: selected.mcpAutoSeal || recorded?.mcpAutoSeal === true
304
+ };
305
+ const manifest = await writeProvenanceConfig(top, selection);
306
+ return { selection, hooksDir, hook, manifestPath: manifest.path, manifestBackup: manifest.backup };
307
+ }
308
+ exports.HOOK_FLAGS = ['--pre-commit', '--mcp-verify-read', '--mcp-auto-seal', '--force'];
309
+ exports.HOOK_USAGE = 'usage: provenance install-hooks [--pre-commit] [--mcp-verify-read] [--mcp-auto-seal] [--force]\n' +
310
+ ' --pre-commit install the git pre-commit verification gate\n' +
311
+ ' --mcp-verify-read record the fail-closed verify-on-read MCP option\n' +
312
+ ' --mcp-auto-seal record the provisional auto-seal-on-agent-write MCP option\n' +
313
+ ' --force replace an existing hook that is not ours, after backing it up to <hook>.bak-<UTC timestamp>\n' +
314
+ 'With no choice flag, the command asks about each option when run in a terminal and does nothing otherwise.';
315
+ /** CLI entry. Returns the process exit code. */
316
+ async function runInstallHooksCli(args, cwd = process.cwd(), io = {}) {
317
+ const unknown = args.filter(a => !exports.HOOK_FLAGS.includes(a));
318
+ if (unknown.length) {
319
+ console.error(`unknown argument: ${unknown.join(' ')}\n${exports.HOOK_USAGE}`);
320
+ return 2;
321
+ }
322
+ const has = (f) => args.includes(f);
323
+ let selection = { preCommit: has('--pre-commit'), mcpVerifyRead: has('--mcp-verify-read'), mcpAutoSeal: has('--mcp-auto-seal') };
324
+ if (!selection.preCommit && !selection.mcpVerifyRead && !selection.mcpAutoSeal) {
325
+ const interactive = io.interactive ?? (Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY));
326
+ if (!interactive) {
327
+ console.error(`no option selected and no terminal to ask.\n${exports.HOOK_USAGE}`);
328
+ return 2;
329
+ }
330
+ try {
331
+ selection = await askInteractively(io);
332
+ }
333
+ catch (e) {
334
+ if (e instanceof PromptCancelled) {
335
+ console.error(`install-hooks ${e.message}.`);
336
+ return 1;
337
+ }
338
+ throw e;
339
+ }
340
+ }
341
+ try {
342
+ const r = await installHooks({ cwd, ...selection, force: has('--force') });
343
+ if (r.hook) {
344
+ console.log(`${r.hook.action}: ${r.hook.path}`);
345
+ if (r.hook.backup)
346
+ console.log(`backup of previous hook: ${r.hook.backup}`);
347
+ }
348
+ console.log(`manifest: ${r.manifestPath}`);
349
+ if (r.manifestBackup)
350
+ console.log(`backup of previous manifest: ${r.manifestBackup}`);
351
+ console.log(`enabled: ${JSON.stringify(r.selection)}`);
352
+ if (selection.mcpVerifyRead || selection.mcpAutoSeal) {
353
+ console.log('MCP configuration snippet:');
354
+ console.log(JSON.stringify({ mcpServers: { 'provenance-gate': { command: 'npx', args: ['-y', '@moonglade/provenance', 'provenance-gate'] } } }, null, 2));
355
+ }
356
+ return 0;
357
+ }
358
+ catch (e) {
359
+ console.error(e.message);
360
+ return 1;
361
+ }
362
+ }
363
+ class PromptCancelled extends Error {
364
+ constructor() {
365
+ super('cancelled, nothing changed');
366
+ this.name = 'PromptCancelled';
367
+ }
368
+ }
369
+ async function askInteractively(io = {}) {
370
+ const rl = readline.createInterface({ input: io.input ?? process.stdin, output: io.output ?? process.stdout });
371
+ // Queue lines from the start so piped input delivered in one chunk is not lost; end of input cancels.
372
+ const lines = [];
373
+ let waiter = null;
374
+ let closed = false;
375
+ rl.on('line', l => { if (waiter) {
376
+ const w = waiter;
377
+ waiter = null;
378
+ w.resolve(l);
379
+ }
380
+ else
381
+ lines.push(l); });
382
+ rl.on('close', () => { closed = true; if (waiter) {
383
+ const w = waiter;
384
+ waiter = null;
385
+ w.reject(new PromptCancelled());
386
+ } });
387
+ const nextLine = () => new Promise((resolve, reject) => {
388
+ if (lines.length)
389
+ return resolve(lines.shift());
390
+ if (closed)
391
+ return reject(new PromptCancelled());
392
+ waiter = { resolve, reject };
393
+ });
394
+ const ask = async (q, def) => {
395
+ rl.setPrompt(`${q} (${def ? 'Y/n' : 'y/N'}): `);
396
+ rl.prompt();
397
+ const t = (await nextLine()).trim().toLowerCase();
398
+ return t === '' ? def : t === 'y' || t === 'yes';
399
+ };
400
+ try {
401
+ return {
402
+ preCommit: await ask('Install the git pre-commit verification gate?', true),
403
+ mcpVerifyRead: await ask('Record the fail-closed verify-on-read MCP option?', false),
404
+ mcpAutoSeal: await ask('Record the provisional auto-seal-on-agent-write MCP option?', false)
405
+ };
406
+ }
407
+ finally {
408
+ rl.close();
409
+ }
410
+ }
@@ -0,0 +1,228 @@
1
+ import * as crypto from 'crypto';
2
+ import { InclusionProof, ArtifactMap } from './merkle';
3
+ import { DTAResult, DTAOptions } from './timestamp';
4
+ import { ArtifactEnvelope } from './artifactEnvelope';
5
+ /**
6
+ * Moonglade Provenance Engine (TypeScript Implementation)
7
+ * Provides Ed25519 micro-signatures, RFC 3161 timestamps, and Merkle root macro-provenance.
8
+ */
9
+ export interface ProvenanceSidecar {
10
+ standard: string;
11
+ artifact: string;
12
+ claims?: Record<string, any>;
13
+ claims_sha256?: string;
14
+ attestations: Array<{
15
+ method: string;
16
+ signer?: string;
17
+ scope?: string;
18
+ sha256?: string;
19
+ target_sha256?: string;
20
+ claims_sha256?: string;
21
+ created?: string;
22
+ git_commit?: string;
23
+ signature?: string;
24
+ tier?: string;
25
+ tsa?: string;
26
+ tsa_time?: string;
27
+ token_b64?: string;
28
+ probes?: any[];
29
+ }>;
30
+ sha256_at_last_write: string;
31
+ updated: string;
32
+ status?: 'provisional' | 'canonical' | 'revoked';
33
+ generator?: {
34
+ agent: string;
35
+ model?: string;
36
+ session?: string;
37
+ };
38
+ parents?: string[];
39
+ envelope?: ArtifactEnvelope;
40
+ }
41
+ export interface VerificationResult {
42
+ verified: boolean;
43
+ status?: 'VERIFIED' | 'INDETERMINATE';
44
+ documentSignatureVerified?: boolean;
45
+ timestampStatus?: 'ABSENT' | 'INVALID' | 'INTEGRITY_ONLY' | 'ANCHORED';
46
+ signer?: string;
47
+ timestamp?: string;
48
+ timestampTier?: string;
49
+ timestampVerified?: boolean;
50
+ timestampSigner?: string;
51
+ error?: string;
52
+ /** How many signature attestations were checked. Zero means an empty bundle. */
53
+ attestationsChecked?: number;
54
+ /** Methods present that this version cannot verify. Reported, never ignored. */
55
+ unknownMethods?: string[];
56
+ /**
57
+ * The scope covered by the verified signature. Returned so a caller can compare it
58
+ * with the location or namespace it expected: the signature binds the scope string,
59
+ * but nothing in this library binds that string to where the file actually is.
60
+ * A caller that does not compare it has not enforced scope.
61
+ */
62
+ scope?: string;
63
+ /** Standard markdown provenance badge representation of this artifact verdict. */
64
+ badge?: string;
65
+ claims?: Record<string, any>;
66
+ claimsSha256?: string;
67
+ }
68
+ export declare const TEXT_EXTENSIONS: Set<string>;
69
+ export declare const BINARY_EXTENSIONS: Set<string>;
70
+ /**
71
+ * Checks if a filename or byte buffer represents text rather than binary data.
72
+ */
73
+ export declare function isTextContent(filename: string, bytes: Buffer | Uint8Array): boolean;
74
+ /**
75
+ * Deterministic JSON Canonicalization Scheme (RFC 8785 subset for generic claims, draft -03 section 3.3).
76
+ * Sorts object keys lexicographically by UTF-16 code units with no whitespace. Numbers must be safe
77
+ * integers; floats, NaN, Infinity and lone surrogates are rejected, exactly as in the Python and Rust engines.
78
+ */
79
+ export declare function canonicalizeJson(value: any): string;
80
+ export interface VerificationPolicy {
81
+ expectedScope?: string;
82
+ requiredSigners?: string[];
83
+ timestampTrustAnchors?: string[];
84
+ }
85
+ export declare const BUNDLE_V2 = "Moonglade Provenance Attestation Bundle v2";
86
+ export declare const BUNDLE_V3 = "Moonglade Provenance Attestation Bundle v3";
87
+ export declare const MAX_ARTIFACT_BYTES: number;
88
+ export declare const MAX_SIDECAR_BYTES: number;
89
+ /** Reject links and oversized inputs. This local tool assumes no concurrent hostile filesystem writer. */
90
+ export declare function readRegularFile(file: string, limit: number): Promise<Buffer>;
91
+ export declare function writeSidecar(file: string, value: ProvenanceSidecar): Promise<void>;
92
+ export declare function validateSidecar(sidecar: any): asserts sidecar is ProvenanceSidecar;
93
+ export declare class ProvenanceEngine {
94
+ /**
95
+ * Canonicalizes an in-memory buffer according to Moonglade Provenance rules.
96
+ * Text documents: strips UTF-8 BOM, normalizes CRLF to LF, and applies Unicode NFC.
97
+ * Binary documents (PDFs, images, binaries): preserves exact byte fidelity.
98
+ */
99
+ static canonicalizeBytes(raw: Buffer, filename?: string): Buffer;
100
+ /**
101
+ * Canonicalize a file's bytes deterministically:
102
+ * For text documents, normalize CRLF to LF, strip UTF-8 BOM, and apply Unicode NFC.
103
+ * For binary documents (PDFs, images, binaries), preserve byte-exact fidelity.
104
+ *
105
+ * This exists so a git checkout on Windows does not invalidate text signatures
106
+ * in the repository, while ensuring binary structures (like PDF xref tables)
107
+ * remain completely untouched.
108
+ */
109
+ static canonicalizeFile(filePath: string): Promise<Buffer>;
110
+ /**
111
+ * Generates an Ed25519 keypair for testing/local identities if none exists.
112
+ */
113
+ static generateEd25519KeyPair(): crypto.KeyPairSyncResult<string, string>;
114
+ /**
115
+ * The bytes that are actually signed.
116
+ *
117
+ * Until v2 the signature covered the document bytes alone, which meant the
118
+ * scope, the signer identity and the timestamp were unauthenticated JSON
119
+ * sitting beside it. Anyone could rewrite the scope in a sidecar and the
120
+ * signature still verified - so the stated property, that a signature made
121
+ * for one scope will not verify under another, was not true here.
122
+ *
123
+ * v2 signs a structured claim instead. Three properties matter:
124
+ *
125
+ * 1. Domain separation - the version string is inside the payload, so a
126
+ * signature over one payload type cannot be replayed as another.
127
+ * 2. Field binding - scope, artifact hash and signer are all covered.
128
+ * 3. Unambiguous framing - newlines delimit fields, and fields are
129
+ * validated to contain none, so a crafted scope cannot smuggle in a
130
+ * forged signer line.
131
+ */
132
+ static readonly PAYLOAD_VERSION = "moonglade-provenance/v2";
133
+ static readonly PAYLOAD_VERSION_V3 = "moonglade-provenance/v3";
134
+ static buildSigningPayload(fields: {
135
+ scope: string;
136
+ sha256: string;
137
+ signer: string;
138
+ }): Buffer;
139
+ static buildSigningPayloadV3(fields: {
140
+ scope: string;
141
+ targetSha256: string;
142
+ signer: string;
143
+ claimsSha256: string;
144
+ }): Buffer;
145
+ /**
146
+ * Build the attestation scope for an artifact.
147
+ *
148
+ * Scope isolation is the stated security property: a signature made for one
149
+ * scope must not be presentable for another. Until 2026-08-30 this used
150
+ * `path.basename(filePath)`, which gave every README.md in the tree the same
151
+ * scope - a collision in any repository with repeated filenames, which is all
152
+ * of them.
153
+ *
154
+ * It is now the path relative to `vaultRoot`, matching the Python
155
+ * implementation's `namespace_for()`.
156
+ *
157
+ * Separators are forced to POSIX. `path.relative` yields backslashes on
158
+ * Windows and forward slashes elsewhere, so without this the same file signed
159
+ * on two machines would carry two different scopes. That is the same class of
160
+ * defect canonicalizeFile exists to prevent for line endings, and it would be
161
+ * careless to fix one and not the other.
162
+ */
163
+ static scopeFor(filePath: string, vaultRoot?: string, explicit?: string): string;
164
+ /**
165
+ * Sign a document and generate the .provenance.json sidecar.
166
+ * Supports both v2 standard bundles and v3 extended bundles with generic claims.
167
+ */
168
+ static sealDocument(filePath: string, privateKeyPem: string, signerIdentity: string, gitCommit?: string, opts?: {
169
+ vaultRoot?: string;
170
+ scope?: string;
171
+ timestamp?: boolean | DTAOptions;
172
+ claims?: Record<string, any>;
173
+ version?: 'v2' | 'v3';
174
+ }): Promise<ProvenanceSidecar>;
175
+ /**
176
+ * Countersign an existing document and append the attestation to the sidecar.
177
+ */
178
+ static countersignDocument(filePath: string, privateKeyPem: string, signerIdentity: string, gitCommit?: string, opts?: {
179
+ vaultRoot?: string;
180
+ scope?: string;
181
+ verificationKeys?: string | Record<string, string>;
182
+ policy?: VerificationPolicy;
183
+ }): Promise<ProvenanceSidecar>;
184
+ /**
185
+ * Attest RFC 3161 timestamping for an artifact via Concurrent Multi-Source DTA.
186
+ */
187
+ static attestTimestamp(filePath: string, opts?: DTAOptions): Promise<DTAResult>;
188
+ /**
189
+ * Verify a document against its sidecar.
190
+ * Supports both v2 and v3 attestation bundles.
191
+ */
192
+ static verifyDocument(filePath: string, publicKeyPem: string | Record<string, string>, policy?: VerificationPolicy): Promise<VerificationResult>;
193
+ }
194
+ /**
195
+ * Vault Macro-Provenance: Merkle Tree Hash Registry
196
+ */
197
+ export declare class ProvenanceVault {
198
+ rootPath: string;
199
+ salt: string;
200
+ constructor(rootPath: string, salt?: string);
201
+ private walk;
202
+ /**
203
+ * Hash every file under the root and build a real Merkle tree over them.
204
+ *
205
+ * Until v2 this returned a flat SHA-256 over the concatenated hashes. That
206
+ * detected change and nothing else - it could not produce an inclusion proof,
207
+ * which is the only reason to use a tree. See merkle.ts.
208
+ *
209
+ * Paths are POSIX-normalised before hashing, so a vault scanned on Windows and
210
+ * on Linux yields the same root. Without that the two would silently disagree,
211
+ * which is the same defect canonicalizeFile prevents for line endings.
212
+ */
213
+ scanAndAudit(): Promise<{
214
+ merkleRoot: string;
215
+ algorithm: string;
216
+ fileCount: number;
217
+ artifacts: ArtifactMap;
218
+ }>;
219
+ /** Audit path for one artifact, by its POSIX path relative to the root. */
220
+ proveInclusion(relPath: string): Promise<InclusionProof | null>;
221
+ }
222
+ export * from './merkle';
223
+ export * from './timestamp';
224
+ export * from './mcp';
225
+ export * from './artifactHook';
226
+ export * from './artifactEnvelope';
227
+ export * from './self-test';
228
+ export * from './badge';