mdkg 0.5.1 → 0.5.2

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,1100 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.GitMaterializeError = exports.GIT_MATERIALIZE_RECEIPT_SCHEMA = exports.GIT_MATERIALIZE_REQUEST_SCHEMA = void 0;
7
+ exports.collectGitMaterializeReceipt = collectGitMaterializeReceipt;
8
+ const node_crypto_1 = __importDefault(require("node:crypto"));
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const node_child_process_1 = require("node:child_process");
12
+ const validate_1 = require("./validate");
13
+ const filesystem_authority_1 = require("../core/filesystem_authority");
14
+ exports.GIT_MATERIALIZE_REQUEST_SCHEMA = "mdkg.git.materialize.request.v1";
15
+ exports.GIT_MATERIALIZE_RECEIPT_SCHEMA = "mdkg.git.materialize.receipt.v1";
16
+ const MAX_REQUEST_BYTES = 64 * 1024;
17
+ const MAX_REF_LENGTH = 512;
18
+ const MAX_REPOSITORY_REF_LENGTH = 2048;
19
+ const MAX_EVIDENCE_REFS = 32;
20
+ const MAX_CAPTURE_BYTES = 16 * 1024;
21
+ const MAX_JSON_DEPTH = 16;
22
+ const MATERIALIZE_VALIDATION_LIMITS = {
23
+ max_files: 100000,
24
+ max_file_bytes: 8 * 1024 * 1024,
25
+ max_total_bytes: 512 * 1024 * 1024,
26
+ max_depth: 64,
27
+ };
28
+ class StrictJsonParser {
29
+ constructor(input) {
30
+ this.input = input;
31
+ this.offset = 0;
32
+ }
33
+ parse() {
34
+ const value = this.parseValue(0);
35
+ this.skipWhitespace();
36
+ if (this.offset !== this.input.length) {
37
+ throw new Error("request must contain exactly one JSON value");
38
+ }
39
+ return value;
40
+ }
41
+ parseValue(depth) {
42
+ if (depth > MAX_JSON_DEPTH) {
43
+ throw new Error(`request JSON exceeds maximum depth ${MAX_JSON_DEPTH}`);
44
+ }
45
+ this.skipWhitespace();
46
+ const token = this.input[this.offset];
47
+ if (token === "{")
48
+ return this.parseObject(depth + 1);
49
+ if (token === "[")
50
+ return this.parseArray(depth + 1);
51
+ if (token === '"')
52
+ return this.parseString();
53
+ if (token === "t")
54
+ return this.parseLiteral("true", true);
55
+ if (token === "f")
56
+ return this.parseLiteral("false", false);
57
+ if (token === "n")
58
+ return this.parseLiteral("null", null);
59
+ if (token === "-" || (token !== undefined && /[0-9]/.test(token)))
60
+ return this.parseNumber();
61
+ throw new Error("request is not valid JSON");
62
+ }
63
+ parseObject(depth) {
64
+ this.offset += 1;
65
+ const result = Object.create(null);
66
+ const keys = new Set();
67
+ this.skipWhitespace();
68
+ if (this.input[this.offset] === "}") {
69
+ this.offset += 1;
70
+ return result;
71
+ }
72
+ while (this.offset < this.input.length) {
73
+ this.skipWhitespace();
74
+ if (this.input[this.offset] !== '"')
75
+ throw new Error("request object keys must be JSON strings");
76
+ const key = this.parseString();
77
+ if (keys.has(key))
78
+ throw new Error(`request contains duplicate field: ${key}`);
79
+ keys.add(key);
80
+ this.skipWhitespace();
81
+ if (this.input[this.offset] !== ":")
82
+ throw new Error("request object field is missing ':'");
83
+ this.offset += 1;
84
+ result[key] = this.parseValue(depth);
85
+ this.skipWhitespace();
86
+ const delimiter = this.input[this.offset];
87
+ if (delimiter === "}") {
88
+ this.offset += 1;
89
+ return result;
90
+ }
91
+ if (delimiter !== ",")
92
+ throw new Error("request object fields must be comma separated");
93
+ this.offset += 1;
94
+ }
95
+ throw new Error("request JSON object is not closed");
96
+ }
97
+ parseArray(depth) {
98
+ this.offset += 1;
99
+ const result = [];
100
+ this.skipWhitespace();
101
+ if (this.input[this.offset] === "]") {
102
+ this.offset += 1;
103
+ return result;
104
+ }
105
+ while (this.offset < this.input.length) {
106
+ result.push(this.parseValue(depth));
107
+ this.skipWhitespace();
108
+ const delimiter = this.input[this.offset];
109
+ if (delimiter === "]") {
110
+ this.offset += 1;
111
+ return result;
112
+ }
113
+ if (delimiter !== ",")
114
+ throw new Error("request array values must be comma separated");
115
+ this.offset += 1;
116
+ }
117
+ throw new Error("request JSON array is not closed");
118
+ }
119
+ parseString() {
120
+ const start = this.offset;
121
+ this.offset += 1;
122
+ let escaped = false;
123
+ while (this.offset < this.input.length) {
124
+ const character = this.input[this.offset];
125
+ if (!escaped && character === '"') {
126
+ this.offset += 1;
127
+ return JSON.parse(this.input.slice(start, this.offset));
128
+ }
129
+ if (!escaped && character === "\\") {
130
+ escaped = true;
131
+ }
132
+ else {
133
+ escaped = false;
134
+ }
135
+ this.offset += 1;
136
+ }
137
+ throw new Error("request JSON string is not closed");
138
+ }
139
+ parseNumber() {
140
+ const rest = this.input.slice(this.offset);
141
+ const match = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/.exec(rest);
142
+ if (!match)
143
+ throw new Error("request contains an invalid JSON number");
144
+ this.offset += match[0].length;
145
+ const value = Number(match[0]);
146
+ if (!Number.isFinite(value))
147
+ throw new Error("request number must be finite");
148
+ return value;
149
+ }
150
+ parseLiteral(literal, value) {
151
+ if (this.input.slice(this.offset, this.offset + literal.length) !== literal) {
152
+ throw new Error("request contains an invalid JSON literal");
153
+ }
154
+ this.offset += literal.length;
155
+ return value;
156
+ }
157
+ skipWhitespace() {
158
+ while (this.offset < this.input.length && /[\t\n\r ]/.test(this.input[this.offset])) {
159
+ this.offset += 1;
160
+ }
161
+ }
162
+ }
163
+ class MaterializeAbort extends Error {
164
+ constructor(reasonCode) {
165
+ super(reasonCode);
166
+ this.reasonCode = reasonCode;
167
+ this.name = "MaterializeAbort";
168
+ }
169
+ }
170
+ class GitMaterializeError extends Error {
171
+ constructor(receipt) {
172
+ super(`git materialize failed: ${receipt.reason_code}`);
173
+ this.receipt = receipt;
174
+ this.name = "GitMaterializeError";
175
+ }
176
+ }
177
+ exports.GitMaterializeError = GitMaterializeError;
178
+ class CancellationScope {
179
+ constructor() {
180
+ this.child = null;
181
+ this.cancelled = false;
182
+ this.onSignal = () => {
183
+ this.cancelled = true;
184
+ this.terminate("SIGTERM");
185
+ };
186
+ }
187
+ start() {
188
+ process.on("SIGINT", this.onSignal);
189
+ process.on("SIGTERM", this.onSignal);
190
+ }
191
+ stop() {
192
+ process.removeListener("SIGINT", this.onSignal);
193
+ process.removeListener("SIGTERM", this.onSignal);
194
+ this.child = null;
195
+ }
196
+ attach(child) {
197
+ this.child = child;
198
+ if (this.cancelled)
199
+ this.terminate("SIGTERM");
200
+ }
201
+ detach(child) {
202
+ if (this.child === child)
203
+ this.child = null;
204
+ }
205
+ throwIfCancelled() {
206
+ if (this.cancelled)
207
+ throw new MaterializeAbort("cancelled");
208
+ }
209
+ terminate(signal) {
210
+ const child = this.child;
211
+ if (!child)
212
+ return;
213
+ terminateChild(child, signal);
214
+ const timer = setTimeout(() => {
215
+ if (child.exitCode !== null || child.pid === undefined)
216
+ return;
217
+ terminateChild(child, "SIGKILL");
218
+ }, 500);
219
+ timer.unref();
220
+ }
221
+ }
222
+ function terminateChild(child, signal) {
223
+ if (child.pid === undefined)
224
+ return;
225
+ try {
226
+ if (process.platform === "win32")
227
+ child.kill(signal);
228
+ else
229
+ process.kill(-child.pid, signal);
230
+ }
231
+ catch {
232
+ try {
233
+ child.kill(signal);
234
+ }
235
+ catch {
236
+ // The process may already have exited.
237
+ }
238
+ }
239
+ }
240
+ function stableValue(value) {
241
+ if (Array.isArray(value))
242
+ return value.map((item) => stableValue(item));
243
+ if (value !== null && typeof value === "object") {
244
+ const input = value;
245
+ const output = {};
246
+ for (const key of Object.keys(input).sort())
247
+ output[key] = stableValue(input[key]);
248
+ return output;
249
+ }
250
+ return value;
251
+ }
252
+ function hashRequestValue(value) {
253
+ return `sha256:${node_crypto_1.default.createHash("sha256").update(JSON.stringify(stableValue(value))).digest("hex")}`;
254
+ }
255
+ function hashRawRequest(value) {
256
+ return `sha256:${node_crypto_1.default.createHash("sha256").update(value).digest("hex")}`;
257
+ }
258
+ function isRecord(value) {
259
+ return typeof value === "object" && value !== null && !Array.isArray(value);
260
+ }
261
+ function requireStringField(value, key, maxLength = MAX_REF_LENGTH) {
262
+ const field = value[key];
263
+ if (typeof field !== "string" || field.length === 0 || field !== field.trim()) {
264
+ throw new Error(`${key} must be a non-empty string without surrounding whitespace`);
265
+ }
266
+ if (field.length > maxLength)
267
+ throw new Error(`${key} exceeds maximum length ${maxLength}`);
268
+ if (/[\0-\x1f\x7f]/.test(field))
269
+ throw new Error(`${key} must not contain control characters`);
270
+ return field;
271
+ }
272
+ function optionalStringField(value, key) {
273
+ if (value[key] === undefined)
274
+ return undefined;
275
+ return requireStringField(value, key);
276
+ }
277
+ function hasUrlCredentials(value) {
278
+ try {
279
+ const parsed = new URL(value);
280
+ return parsed.username.length > 0 || parsed.password.length > 0;
281
+ }
282
+ catch {
283
+ return false;
284
+ }
285
+ }
286
+ function hasSensitiveUrlParameters(value) {
287
+ try {
288
+ const parsed = new URL(value);
289
+ for (const key of parsed.searchParams.keys()) {
290
+ if (/(?:token|secret|password|passwd|credential|signature|api[_-]?key|private[_-]?key)/i.test(key)) {
291
+ return true;
292
+ }
293
+ }
294
+ return false;
295
+ }
296
+ catch {
297
+ return false;
298
+ }
299
+ }
300
+ function assertBoundedRef(label, value) {
301
+ if (/\s/.test(value))
302
+ throw new Error(`${label} must not contain whitespace`);
303
+ if (value.startsWith("-"))
304
+ throw new Error(`${label} must not be option-shaped`);
305
+ if (hasUrlCredentials(value))
306
+ throw new Error(`${label} must not contain embedded credentials`);
307
+ if (hasSensitiveUrlParameters(value))
308
+ throw new Error(`${label} must not contain credential-like URL parameters`);
309
+ }
310
+ const CREDENTIAL_SHAPED_REF_PATTERNS = [
311
+ /^(?:bearer|basic)(?:\s|:)/i,
312
+ /^(?:token|secret|password|passwd|credential|authorization|api[_-]?key|private[_-]?key)(?:$|[:/_-])/i,
313
+ /^(?:gh[pousr]_|github_pat_|xox[baprs]-|sk-(?:proj-)?|sk_(?:live|test)_|rk_(?:live|test)_)/i,
314
+ /^(?:AKIA|ASIA)[A-Z0-9]{16}$/,
315
+ /^eyJ[A-Za-z0-9_-]{6,}\.eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}$/,
316
+ ];
317
+ function assertOpaqueNonSecretRef(label, value) {
318
+ assertBoundedRef(label, value);
319
+ if (value.includes("="))
320
+ throw new Error(`${label} must not be assignment-shaped`);
321
+ if (CREDENTIAL_SHAPED_REF_PATTERNS.some((pattern) => pattern.test(value))) {
322
+ throw new Error(`${label} must be an opaque non-secret identifier`);
323
+ }
324
+ }
325
+ function assertRepositoryRef(value) {
326
+ assertBoundedRef("repository_ref", value);
327
+ if (value.includes("::"))
328
+ throw new Error("repository_ref must not use Git remote helpers");
329
+ const localAbsolute = node_path_1.default.isAbsolute(value) || node_path_1.default.win32.isAbsolute(value);
330
+ const scheme = localAbsolute ? undefined : /^([a-z][a-z0-9+.-]*):/i.exec(value)?.[1]?.toLowerCase();
331
+ if (scheme && !["https", "ssh", "git", "file"].includes(scheme)) {
332
+ throw new Error("repository_ref protocol is not supported; use https, ssh, git, file, SCP-like SSH, or a local path");
333
+ }
334
+ if (scheme) {
335
+ const parsed = new URL(value);
336
+ if (parsed.search.length > 0 || parsed.hash.length > 0) {
337
+ throw new Error("repository_ref URLs must not contain query parameters or fragments");
338
+ }
339
+ }
340
+ }
341
+ function repositoryEvidence(repositoryRef) {
342
+ const refHash = `sha256:${node_crypto_1.default.createHash("sha256").update(repositoryRef).digest("hex")}`;
343
+ const safeLabel = (prefix, value) => {
344
+ const normalized = value.replace(/[^A-Za-z0-9._@-]+/g, "-").replace(/^-+|-+$/g, "");
345
+ return `${prefix}:${(normalized || "repository").slice(0, 120)}`;
346
+ };
347
+ if (node_path_1.default.isAbsolute(repositoryRef) || node_path_1.default.win32.isAbsolute(repositoryRef)) {
348
+ return { transport: "local", label: safeLabel("local", node_path_1.default.basename(repositoryRef)), ref_hash: refHash };
349
+ }
350
+ try {
351
+ const parsed = new URL(repositoryRef);
352
+ const transport = parsed.protocol.slice(0, -1);
353
+ if (transport === "file") {
354
+ const basename = node_path_1.default.posix.basename(parsed.pathname) || "repository";
355
+ return { transport, label: safeLabel("file", basename), ref_hash: refHash };
356
+ }
357
+ const basename = node_path_1.default.posix.basename(parsed.pathname) || "repository";
358
+ return {
359
+ transport,
360
+ label: safeLabel(transport, `${parsed.hostname}-${basename}`),
361
+ ref_hash: refHash,
362
+ };
363
+ }
364
+ catch {
365
+ const scp = /^(?:[^@/:]+@)?([^:]+):(.+)$/.exec(repositoryRef);
366
+ if (scp) {
367
+ return {
368
+ transport: "ssh",
369
+ label: safeLabel("ssh", `${scp[1]}-${node_path_1.default.posix.basename(scp[2])}`),
370
+ ref_hash: refHash,
371
+ };
372
+ }
373
+ return {
374
+ transport: "local",
375
+ label: safeLabel("local", node_path_1.default.basename(repositoryRef)),
376
+ ref_hash: refHash,
377
+ };
378
+ }
379
+ }
380
+ function assertTargetRef(value) {
381
+ if (!value.startsWith("refs/heads/") && !value.startsWith("refs/tags/")) {
382
+ throw new Error("target_ref must be a full refs/heads/* or refs/tags/* ref");
383
+ }
384
+ if (value.length > 1024 ||
385
+ /[\0-\x20\x7f~^:?*\[\\]/.test(value) ||
386
+ value.includes("..") ||
387
+ value.includes("@{") ||
388
+ value.includes("//") ||
389
+ value.endsWith("/") ||
390
+ value.endsWith(".")) {
391
+ throw new Error("target_ref is not a valid full Git ref");
392
+ }
393
+ const components = value.split("/");
394
+ if (components.some((component) => component.startsWith(".") || component.endsWith(".lock"))) {
395
+ throw new Error("target_ref is not a valid full Git ref");
396
+ }
397
+ }
398
+ function assertObjectId(label, value) {
399
+ if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(value)) {
400
+ throw new Error(`${label} must be a full lowercase SHA-1 or SHA-256 object id`);
401
+ }
402
+ }
403
+ function assertDestination(value) {
404
+ if (value.startsWith("-") || value.includes("\\")) {
405
+ throw new Error("destination must be a portable non-option relative path using '/' separators");
406
+ }
407
+ if (node_path_1.default.isAbsolute(value) || node_path_1.default.posix.isAbsolute(value) || node_path_1.default.win32.isAbsolute(value)) {
408
+ throw new Error("destination must be relative");
409
+ }
410
+ const components = value.split("/");
411
+ if (components.some((component) => component === "" || component === "." || component === "..")) {
412
+ throw new Error("destination must not contain empty, current-directory, or parent-directory components");
413
+ }
414
+ }
415
+ function validateRequest(value) {
416
+ if (!isRecord(value))
417
+ throw new Error("request must be a JSON object");
418
+ const allowed = new Set([
419
+ "schema",
420
+ "source_ref",
421
+ "repository_ref",
422
+ "access_ref",
423
+ "auth_capability",
424
+ "target_ref",
425
+ "expected_commit",
426
+ "expected_tree",
427
+ "destination",
428
+ "depth",
429
+ "submodule_policy",
430
+ "project_memory_policy",
431
+ "correlation_ref",
432
+ "evidence_refs",
433
+ ]);
434
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key)).sort();
435
+ if (unknown.length > 0)
436
+ throw new Error(`request contains unknown field(s): ${unknown.join(", ")}`);
437
+ const schema = requireStringField(value, "schema");
438
+ if (schema !== exports.GIT_MATERIALIZE_REQUEST_SCHEMA) {
439
+ throw new Error(`schema must equal ${exports.GIT_MATERIALIZE_REQUEST_SCHEMA}`);
440
+ }
441
+ const sourceRef = requireStringField(value, "source_ref");
442
+ const repositoryRef = requireStringField(value, "repository_ref", MAX_REPOSITORY_REF_LENGTH);
443
+ const accessRef = requireStringField(value, "access_ref");
444
+ const authCapability = requireStringField(value, "auth_capability");
445
+ const targetRef = requireStringField(value, "target_ref", 1024);
446
+ const expectedCommit = requireStringField(value, "expected_commit", 64);
447
+ const expectedTree = optionalStringField(value, "expected_tree");
448
+ const destination = requireStringField(value, "destination", 1024);
449
+ const submodulePolicy = requireStringField(value, "submodule_policy");
450
+ const projectMemoryPolicy = requireStringField(value, "project_memory_policy");
451
+ const correlationRef = optionalStringField(value, "correlation_ref");
452
+ const authValues = [
453
+ "unauthenticated",
454
+ "gh",
455
+ "ssh-agent",
456
+ "credential-helper",
457
+ "git-environment",
458
+ ];
459
+ if (!authValues.includes(authCapability)) {
460
+ throw new Error(`auth_capability must be one of ${authValues.join(", ")}`);
461
+ }
462
+ if (!["deny", "ignore"].includes(submodulePolicy)) {
463
+ throw new Error("submodule_policy must be deny or ignore");
464
+ }
465
+ if (!["required", "optional", "forbidden"].includes(projectMemoryPolicy)) {
466
+ throw new Error("project_memory_policy must be required, optional, or forbidden");
467
+ }
468
+ const depthRaw = value.depth;
469
+ let depth;
470
+ if (depthRaw === "full")
471
+ depth = "full";
472
+ else if (typeof depthRaw === "number" && Number.isSafeInteger(depthRaw) && depthRaw > 0 && depthRaw <= 2147483647) {
473
+ depth = depthRaw;
474
+ }
475
+ else {
476
+ throw new Error('depth must be "full" or a positive integer');
477
+ }
478
+ let evidenceRefs;
479
+ if (value.evidence_refs !== undefined) {
480
+ if (!Array.isArray(value.evidence_refs) || value.evidence_refs.length > MAX_EVIDENCE_REFS) {
481
+ throw new Error(`evidence_refs must be an array with at most ${MAX_EVIDENCE_REFS} values`);
482
+ }
483
+ evidenceRefs = value.evidence_refs.map((item, index) => {
484
+ if (typeof item !== "string")
485
+ throw new Error(`evidence_refs[${index}] must be a string`);
486
+ const wrapped = { value: item };
487
+ const normalized = requireStringField(wrapped, "value");
488
+ assertOpaqueNonSecretRef(`evidence_refs[${index}]`, normalized);
489
+ return normalized;
490
+ });
491
+ if (new Set(evidenceRefs).size !== evidenceRefs.length)
492
+ throw new Error("evidence_refs must be unique");
493
+ }
494
+ assertOpaqueNonSecretRef("source_ref", sourceRef);
495
+ assertRepositoryRef(repositoryRef);
496
+ assertOpaqueNonSecretRef("access_ref", accessRef);
497
+ assertTargetRef(targetRef);
498
+ assertObjectId("expected_commit", expectedCommit);
499
+ if (expectedTree !== undefined)
500
+ assertObjectId("expected_tree", expectedTree);
501
+ if (expectedTree !== undefined && expectedTree.length !== expectedCommit.length) {
502
+ throw new Error("expected_tree and expected_commit must use the same object format");
503
+ }
504
+ assertDestination(destination);
505
+ if (correlationRef !== undefined)
506
+ assertOpaqueNonSecretRef("correlation_ref", correlationRef);
507
+ return {
508
+ schema: exports.GIT_MATERIALIZE_REQUEST_SCHEMA,
509
+ source_ref: sourceRef,
510
+ repository_ref: repositoryRef,
511
+ access_ref: accessRef,
512
+ auth_capability: authCapability,
513
+ target_ref: targetRef,
514
+ expected_commit: expectedCommit,
515
+ ...(expectedTree !== undefined ? { expected_tree: expectedTree } : {}),
516
+ destination,
517
+ depth,
518
+ submodule_policy: submodulePolicy,
519
+ project_memory_policy: projectMemoryPolicy,
520
+ ...(correlationRef !== undefined ? { correlation_ref: correlationRef } : {}),
521
+ ...(evidenceRefs !== undefined ? { evidence_refs: evidenceRefs } : {}),
522
+ };
523
+ }
524
+ function readBoundedStdin() {
525
+ const chunks = [];
526
+ let total = 0;
527
+ return new Promise((resolve, reject) => {
528
+ const readNext = () => {
529
+ const remaining = MAX_REQUEST_BYTES - total;
530
+ const buffer = Buffer.allocUnsafe(Math.min(8 * 1024, remaining + 1));
531
+ node_fs_1.default.read(0, buffer, 0, buffer.length, null, (error, bytesRead) => {
532
+ if (error) {
533
+ reject(error);
534
+ return;
535
+ }
536
+ if (bytesRead === 0) {
537
+ resolve(Buffer.concat(chunks, total));
538
+ return;
539
+ }
540
+ if (bytesRead > remaining) {
541
+ reject(new Error(`request exceeds maximum size ${MAX_REQUEST_BYTES}`));
542
+ return;
543
+ }
544
+ chunks.push(buffer.subarray(0, bytesRead));
545
+ total += bytesRead;
546
+ readNext();
547
+ });
548
+ };
549
+ readNext();
550
+ });
551
+ }
552
+ async function readRequest(requestPath) {
553
+ let buffer;
554
+ if (requestPath === "-") {
555
+ buffer = await readBoundedStdin();
556
+ }
557
+ else {
558
+ const stat = node_fs_1.default.lstatSync(requestPath);
559
+ if (!stat.isFile() || stat.isSymbolicLink())
560
+ throw new Error("--request must name a regular non-linked file or -");
561
+ if (stat.size > MAX_REQUEST_BYTES)
562
+ throw new Error(`request exceeds maximum size ${MAX_REQUEST_BYTES}`);
563
+ buffer = node_fs_1.default.readFileSync(requestPath);
564
+ }
565
+ if (buffer.length > MAX_REQUEST_BYTES)
566
+ throw new Error(`request exceeds maximum size ${MAX_REQUEST_BYTES}`);
567
+ const raw = buffer.toString("utf8");
568
+ return { raw, parsed: new StrictJsonParser(raw).parse() };
569
+ }
570
+ function emptyReceipt(rawHash = null) {
571
+ return {
572
+ schema: exports.GIT_MATERIALIZE_RECEIPT_SCHEMA,
573
+ action: "git.materialize",
574
+ ok: false,
575
+ reason_code: "invalid_request",
576
+ request_hash: rawHash,
577
+ repository: { transport: null, label: null, ref_hash: null },
578
+ source_ref: null,
579
+ access_ref: null,
580
+ correlation_ref: null,
581
+ evidence_refs: [],
582
+ target_ref: null,
583
+ expected_revision: { commit: null, tree: null },
584
+ observed_revision: { commit: null, tree: null, object_format: null },
585
+ policies: {
586
+ auth: {
587
+ capability: null,
588
+ available: false,
589
+ status: "not-evaluated",
590
+ reason_code: "not_evaluated",
591
+ },
592
+ depth: null,
593
+ submodules: {
594
+ policy: null,
595
+ gitmodules_present: false,
596
+ gitlink_count: 0,
597
+ gitlink_hash: null,
598
+ initialized: false,
599
+ },
600
+ project_memory: { policy: null, present: false, valid: null, error_count: 0 },
601
+ },
602
+ destination: { path: null, state: "absent", published: false },
603
+ cleanup: { state: "not-required", temporary_paths_remaining: 0 },
604
+ warnings: [],
605
+ };
606
+ }
607
+ function receiptForRequest(request) {
608
+ return {
609
+ ...emptyReceipt(hashRequestValue(request)),
610
+ repository: repositoryEvidence(request.repository_ref),
611
+ source_ref: request.source_ref,
612
+ access_ref: request.access_ref,
613
+ correlation_ref: request.correlation_ref ?? null,
614
+ evidence_refs: request.evidence_refs ?? [],
615
+ target_ref: request.target_ref,
616
+ expected_revision: {
617
+ commit: request.expected_commit,
618
+ tree: request.expected_tree ?? null,
619
+ },
620
+ policies: {
621
+ auth: {
622
+ capability: request.auth_capability,
623
+ available: false,
624
+ status: "not-evaluated",
625
+ reason_code: "not_evaluated",
626
+ },
627
+ depth: request.depth,
628
+ submodules: {
629
+ policy: request.submodule_policy,
630
+ gitmodules_present: false,
631
+ gitlink_count: 0,
632
+ gitlink_hash: null,
633
+ initialized: false,
634
+ },
635
+ project_memory: {
636
+ policy: request.project_memory_policy,
637
+ present: false,
638
+ valid: null,
639
+ error_count: 0,
640
+ },
641
+ },
642
+ destination: { path: request.destination, state: "absent", published: false },
643
+ };
644
+ }
645
+ function baseGitArgs(hooksPath) {
646
+ return [
647
+ "-c",
648
+ `core.hooksPath=${hooksPath}`,
649
+ "-c",
650
+ "protocol.ext.allow=never",
651
+ "-c",
652
+ "submodule.recurse=false",
653
+ "-c",
654
+ "fetch.recurseSubmodules=false",
655
+ "-c",
656
+ "credential.interactive=never",
657
+ ];
658
+ }
659
+ function gitEnvironment(sanitizedCheckout = false) {
660
+ const env = {
661
+ ...process.env,
662
+ GIT_TERMINAL_PROMPT: "0",
663
+ GCM_INTERACTIVE: "Never",
664
+ };
665
+ if (sanitizedCheckout) {
666
+ env.GIT_CONFIG_GLOBAL = process.platform === "win32" ? "NUL" : "/dev/null";
667
+ env.GIT_CONFIG_SYSTEM = process.platform === "win32" ? "NUL" : "/dev/null";
668
+ env.GIT_ATTR_NOSYSTEM = "1";
669
+ }
670
+ return env;
671
+ }
672
+ async function runProcess(command, args, options) {
673
+ return await new Promise((resolve, reject) => {
674
+ let settled = false;
675
+ let outputLimitExceeded = false;
676
+ let stdout = "";
677
+ let capturedBytes = 0;
678
+ const child = (0, node_child_process_1.spawn)(command, args, {
679
+ cwd: options.cwd,
680
+ env: options.env ?? process.env,
681
+ detached: process.platform !== "win32",
682
+ stdio: ["ignore", "pipe", "pipe"],
683
+ });
684
+ options.cancellation.attach(child);
685
+ const fail = (error) => {
686
+ if (settled)
687
+ return;
688
+ settled = true;
689
+ options.cancellation.detach(child);
690
+ reject(error);
691
+ };
692
+ child.on("error", (error) => fail(error));
693
+ child.stdout?.setEncoding("utf8");
694
+ child.stderr?.setEncoding("utf8");
695
+ child.stdout?.on("data", (chunk) => {
696
+ if (options.onStdoutChunk) {
697
+ options.onStdoutChunk(chunk);
698
+ return;
699
+ }
700
+ if (!options.capture)
701
+ return;
702
+ capturedBytes += Buffer.byteLength(chunk);
703
+ if (capturedBytes > MAX_CAPTURE_BYTES) {
704
+ outputLimitExceeded = true;
705
+ terminateChild(child, "SIGTERM");
706
+ return;
707
+ }
708
+ stdout += chunk;
709
+ });
710
+ child.stderr?.on("data", () => {
711
+ // Git and helper output is intentionally discarded and never enters receipts.
712
+ });
713
+ child.on("close", (status, signal) => {
714
+ if (settled)
715
+ return;
716
+ settled = true;
717
+ options.cancellation.detach(child);
718
+ if (outputLimitExceeded) {
719
+ reject(new MaterializeAbort("output_limit_exceeded"));
720
+ return;
721
+ }
722
+ resolve({ status, stdout, signal });
723
+ });
724
+ });
725
+ }
726
+ async function runGit(cwd, args, hooksPath, cancellation, failureCode, options = {}) {
727
+ cancellation.throwIfCancelled();
728
+ let result;
729
+ try {
730
+ result = await runProcess("git", [...baseGitArgs(hooksPath), ...args], {
731
+ cwd,
732
+ env: gitEnvironment(options.sanitizedCheckout),
733
+ capture: options.capture,
734
+ cancellation,
735
+ onStdoutChunk: options.onStdoutChunk,
736
+ });
737
+ }
738
+ catch (error) {
739
+ if (error instanceof MaterializeAbort)
740
+ throw error;
741
+ if (error.code === "ENOENT")
742
+ throw new MaterializeAbort("git_unavailable");
743
+ throw new MaterializeAbort(failureCode);
744
+ }
745
+ cancellation.throwIfCancelled();
746
+ if (result.status !== 0)
747
+ throw new MaterializeAbort(failureCode);
748
+ return result;
749
+ }
750
+ async function authAvailable(request, root, hooksPath, cancellation) {
751
+ const available = (reasonCode) => ({
752
+ capability: request.auth_capability,
753
+ available: true,
754
+ status: "available",
755
+ reason_code: reasonCode,
756
+ });
757
+ const unavailable = (reasonCode) => ({
758
+ capability: request.auth_capability,
759
+ available: false,
760
+ status: "unavailable",
761
+ reason_code: reasonCode,
762
+ });
763
+ switch (request.auth_capability) {
764
+ case "unauthenticated":
765
+ return available("not_required");
766
+ case "ssh-agent":
767
+ return typeof process.env.SSH_AUTH_SOCK === "string" && process.env.SSH_AUTH_SOCK.length > 0
768
+ ? available("ssh_agent_available")
769
+ : unavailable("ssh_agent_unavailable");
770
+ case "git-environment":
771
+ return ["GIT_ASKPASS", "SSH_ASKPASS", "GIT_SSH", "GIT_SSH_COMMAND"].some((name) => typeof process.env[name] === "string" && process.env[name].length > 0)
772
+ ? available("git_environment_available")
773
+ : unavailable("git_environment_unavailable");
774
+ case "credential-helper": {
775
+ const result = await runProcess("git", [...baseGitArgs(hooksPath), "config", "--get-all", "credential.helper"], { cwd: root, env: gitEnvironment(), cancellation });
776
+ return result.status === 0
777
+ ? available("credential_helper_configured")
778
+ : unavailable("credential_helper_unavailable");
779
+ }
780
+ case "gh": {
781
+ try {
782
+ const result = await runProcess("gh", ["auth", "status"], {
783
+ cwd: root,
784
+ env: process.env,
785
+ cancellation,
786
+ });
787
+ return result.status === 0 ? available("gh_authenticated") : unavailable("gh_unavailable");
788
+ }
789
+ catch (error) {
790
+ if (error instanceof MaterializeAbort)
791
+ throw error;
792
+ return unavailable("gh_unavailable");
793
+ }
794
+ }
795
+ }
796
+ }
797
+ function relativePath(root, absolutePath) {
798
+ return node_path_1.default.relative(root, absolutePath).split(node_path_1.default.sep).join("/");
799
+ }
800
+ function cleanup(root, temporaryPaths, createdParents) {
801
+ let failed = false;
802
+ for (const absolutePath of temporaryPaths) {
803
+ try {
804
+ node_fs_1.default.rmSync(absolutePath, { recursive: true, force: true });
805
+ }
806
+ catch {
807
+ failed = true;
808
+ }
809
+ }
810
+ for (const parent of [...createdParents].reverse()) {
811
+ try {
812
+ (0, filesystem_authority_1.withContainedPathSink)({ root, relativePath: relativePath(root, parent), operation: "delete" }, ({ absolutePath }) => node_fs_1.default.rmdirSync(absolutePath));
813
+ }
814
+ catch (error) {
815
+ if (error.code !== "ENOTEMPTY" && error.code !== "ENOENT") {
816
+ failed = true;
817
+ }
818
+ }
819
+ }
820
+ const remaining = temporaryPaths.filter((item) => node_fs_1.default.existsSync(item)).length;
821
+ return { state: failed || remaining > 0 ? "failed" : "complete", remaining };
822
+ }
823
+ function prepareDestination(root, destination) {
824
+ const descriptor = (0, filesystem_authority_1.withContainedPathSink)({ root, relativePath: destination, operation: "create" }, (item) => item);
825
+ if (node_fs_1.default.existsSync(descriptor.absolutePath))
826
+ throw new MaterializeAbort("destination_exists");
827
+ const parentPath = node_path_1.default.dirname(descriptor.absolutePath);
828
+ const parentRelative = relativePath(root, parentPath);
829
+ const createdParents = [];
830
+ if (parentRelative && parentRelative !== ".") {
831
+ const components = parentRelative.split("/");
832
+ for (let index = 1; index <= components.length; index += 1) {
833
+ const candidateRelative = components.slice(0, index).join("/");
834
+ const candidate = (0, filesystem_authority_1.withContainedPathSink)({ root, relativePath: candidateRelative, operation: "create" }, (item) => item.absolutePath);
835
+ if (!node_fs_1.default.existsSync(candidate))
836
+ createdParents.push(candidate);
837
+ }
838
+ (0, filesystem_authority_1.ensureContainedDirectory)({ root, relativePath: parentRelative });
839
+ }
840
+ const suffix = node_crypto_1.default.randomBytes(10).toString("hex");
841
+ const base = node_path_1.default.basename(descriptor.absolutePath);
842
+ const temporaryPath = node_path_1.default.join(parentPath, `.${base}.mdkg-materialize-${suffix}`);
843
+ const hooksPath = node_path_1.default.join(parentPath, `.${base}.mdkg-hooks-${suffix}`);
844
+ for (const candidate of [temporaryPath, hooksPath]) {
845
+ (0, filesystem_authority_1.withContainedPathSink)({ root, relativePath: relativePath(root, candidate), operation: "create" }, ({ absolutePath }) => {
846
+ if (node_fs_1.default.existsSync(absolutePath))
847
+ throw new MaterializeAbort("destination_exists");
848
+ });
849
+ }
850
+ node_fs_1.default.mkdirSync(hooksPath, { mode: 0o700 });
851
+ return { destinationPath: descriptor.absolutePath, temporaryPath, hooksPath, createdParents };
852
+ }
853
+ function strictIdentity(value, expectedLength, reason) {
854
+ const normalized = value.trim();
855
+ const pattern = expectedLength === 40 ? /^[0-9a-f]{40}$/ : /^[0-9a-f]{64}$/;
856
+ if (!pattern.test(normalized))
857
+ throw new MaterializeAbort(reason);
858
+ return normalized;
859
+ }
860
+ async function inspectSubmodules(repositoryRoot, commit, hooksPath, cancellation) {
861
+ const gitmodulesResult = await runProcess("git", [...baseGitArgs(hooksPath), "cat-file", "-e", `${commit}:.gitmodules`], { cwd: repositoryRoot, env: gitEnvironment(true), cancellation });
862
+ cancellation.throwIfCancelled();
863
+ let gitlinkCount = 0;
864
+ const gitlinkHasher = node_crypto_1.default.createHash("sha256");
865
+ let remainder = "";
866
+ await runGit(repositoryRoot, ["ls-tree", "-r", "--full-tree", "--format=%(objectmode) %(objectname)", commit], hooksPath, cancellation, "git_failed", {
867
+ sanitizedCheckout: true,
868
+ onStdoutChunk: (chunk) => {
869
+ const values = `${remainder}${chunk}`.split("\n");
870
+ remainder = values.pop() ?? "";
871
+ for (const entry of values) {
872
+ if (!entry.startsWith("160000 "))
873
+ continue;
874
+ gitlinkCount += 1;
875
+ gitlinkHasher.update(entry);
876
+ gitlinkHasher.update("\n");
877
+ }
878
+ },
879
+ });
880
+ if (remainder.startsWith("160000 ")) {
881
+ gitlinkCount += 1;
882
+ gitlinkHasher.update(remainder);
883
+ gitlinkHasher.update("\n");
884
+ }
885
+ return {
886
+ gitmodulesPresent: gitmodulesResult.status === 0,
887
+ gitlinkCount,
888
+ gitlinkHash: gitlinkCount > 0 ? `sha256:${gitlinkHasher.digest("hex")}` : null,
889
+ };
890
+ }
891
+ function assertMaterializationValidationLimits(configPath) {
892
+ const stat = node_fs_1.default.lstatSync(configPath);
893
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 256 * 1024) {
894
+ throw new MaterializeAbort("project_memory_invalid");
895
+ }
896
+ let config;
897
+ try {
898
+ config = JSON.parse(node_fs_1.default.readFileSync(configPath, "utf8"));
899
+ }
900
+ catch {
901
+ throw new MaterializeAbort("project_memory_invalid");
902
+ }
903
+ if (!isRecord(config))
904
+ throw new MaterializeAbort("project_memory_invalid");
905
+ const index = isRecord(config.index) ? config.index : null;
906
+ const limits = index && isRecord(index.limits) ? index.limits : null;
907
+ if (!limits)
908
+ return;
909
+ for (const [key, maximum] of Object.entries(MATERIALIZE_VALIDATION_LIMITS)) {
910
+ const value = limits[key];
911
+ if (typeof value === "number" && value > maximum)
912
+ throw new MaterializeAbort("project_memory_invalid");
913
+ }
914
+ }
915
+ function validateProjectMemory(repositoryRoot, policy) {
916
+ const mdkgPath = node_path_1.default.join(repositoryRoot, ".mdkg");
917
+ let stat;
918
+ try {
919
+ stat = node_fs_1.default.lstatSync(mdkgPath);
920
+ }
921
+ catch (error) {
922
+ if (error.code !== "ENOENT")
923
+ throw error;
924
+ }
925
+ const present = stat !== undefined;
926
+ if (policy === "forbidden") {
927
+ return present
928
+ ? {
929
+ evidence: { policy, present: true, valid: false, error_count: 1 },
930
+ failure: "project_memory_forbidden",
931
+ }
932
+ : { evidence: { policy, present: false, valid: null, error_count: 0 } };
933
+ }
934
+ if (!present) {
935
+ return policy === "required"
936
+ ? {
937
+ evidence: { policy, present: false, valid: false, error_count: 1 },
938
+ failure: "project_memory_required",
939
+ }
940
+ : { evidence: { policy, present: false, valid: null, error_count: 0 } };
941
+ }
942
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
943
+ return {
944
+ evidence: { policy, present: true, valid: false, error_count: 1 },
945
+ failure: "project_memory_invalid",
946
+ };
947
+ }
948
+ let validation;
949
+ try {
950
+ assertMaterializationValidationLimits(node_path_1.default.join(mdkgPath, "config.json"));
951
+ validation = (0, validate_1.collectValidateReceipt)({ root: repositoryRoot, json: true });
952
+ }
953
+ catch {
954
+ return {
955
+ evidence: { policy, present: true, valid: false, error_count: 1 },
956
+ failure: "project_memory_invalid",
957
+ };
958
+ }
959
+ if (!validation.ok) {
960
+ return {
961
+ evidence: {
962
+ policy,
963
+ present: true,
964
+ valid: false,
965
+ error_count: Math.max(1, validation.error_count),
966
+ },
967
+ failure: "project_memory_invalid",
968
+ };
969
+ }
970
+ return { evidence: { policy, present: true, valid: true, error_count: 0 } };
971
+ }
972
+ async function collectGitMaterializeReceipt(options) {
973
+ let raw = null;
974
+ let request;
975
+ try {
976
+ const loaded = await readRequest(options.request);
977
+ raw = loaded.raw;
978
+ request = validateRequest(loaded.parsed);
979
+ }
980
+ catch {
981
+ throw new GitMaterializeError(emptyReceipt(raw === null ? null : hashRawRequest(raw)));
982
+ }
983
+ const receipt = receiptForRequest(request);
984
+ const cancellation = new CancellationScope();
985
+ const temporaryPaths = [];
986
+ let createdParents = [];
987
+ let destinationPath = null;
988
+ let accepted = false;
989
+ cancellation.start();
990
+ try {
991
+ let prepared;
992
+ try {
993
+ prepared = prepareDestination(options.root, request.destination);
994
+ }
995
+ catch (error) {
996
+ if (error instanceof MaterializeAbort)
997
+ throw error;
998
+ throw new MaterializeAbort("invalid_request");
999
+ }
1000
+ destinationPath = prepared.destinationPath;
1001
+ createdParents = prepared.createdParents;
1002
+ temporaryPaths.push(prepared.temporaryPath, prepared.hooksPath);
1003
+ receipt.policies.auth = await authAvailable(request, options.root, prepared.hooksPath, cancellation);
1004
+ if (!receipt.policies.auth.available)
1005
+ throw new MaterializeAbort("auth_unavailable");
1006
+ const cloneArgs = [
1007
+ "clone",
1008
+ "--no-checkout",
1009
+ "--no-recurse-submodules",
1010
+ "--origin",
1011
+ "origin",
1012
+ ];
1013
+ if (request.depth !== "full")
1014
+ cloneArgs.push(`--depth=${request.depth}`);
1015
+ cloneArgs.push("--", request.repository_ref, prepared.temporaryPath);
1016
+ await runGit(options.root, cloneArgs, prepared.hooksPath, cancellation, "remote_unavailable");
1017
+ const fetchArgs = ["fetch", "--no-recurse-submodules", "--no-tags"];
1018
+ if (request.depth !== "full")
1019
+ fetchArgs.push(`--depth=${request.depth}`);
1020
+ fetchArgs.push("origin", request.target_ref);
1021
+ await runGit(prepared.temporaryPath, fetchArgs, prepared.hooksPath, cancellation, "target_ref_missing");
1022
+ const commitResult = await runGit(prepared.temporaryPath, ["rev-parse", "--verify", "FETCH_HEAD^{commit}"], prepared.hooksPath, cancellation, "target_ref_missing", { capture: true, sanitizedCheckout: true });
1023
+ const observedCommit = strictIdentity(commitResult.stdout, request.expected_commit.length, "object_format_mismatch");
1024
+ receipt.observed_revision.commit = observedCommit;
1025
+ if (observedCommit !== request.expected_commit)
1026
+ throw new MaterializeAbort("commit_mismatch");
1027
+ const objectFormatResult = await runGit(prepared.temporaryPath, ["rev-parse", "--show-object-format"], prepared.hooksPath, cancellation, "object_format_mismatch", { capture: true, sanitizedCheckout: true });
1028
+ const objectFormat = objectFormatResult.stdout.trim();
1029
+ if (objectFormat !== "sha1" && objectFormat !== "sha256") {
1030
+ throw new MaterializeAbort("object_format_mismatch");
1031
+ }
1032
+ if ((objectFormat === "sha1" ? 40 : 64) !== request.expected_commit.length) {
1033
+ throw new MaterializeAbort("object_format_mismatch");
1034
+ }
1035
+ receipt.observed_revision.object_format = objectFormat;
1036
+ const treeResult = await runGit(prepared.temporaryPath, ["rev-parse", "--verify", `${observedCommit}^{tree}`], prepared.hooksPath, cancellation, "git_failed", { capture: true, sanitizedCheckout: true });
1037
+ const observedTree = strictIdentity(treeResult.stdout, observedCommit.length, "object_format_mismatch");
1038
+ receipt.observed_revision.tree = observedTree;
1039
+ if (request.expected_tree !== undefined && observedTree !== request.expected_tree) {
1040
+ throw new MaterializeAbort("tree_mismatch");
1041
+ }
1042
+ await runGit(prepared.temporaryPath, ["checkout", "--detach", "--force", "--no-recurse-submodules", observedCommit, "--"], prepared.hooksPath, cancellation, "git_failed", { sanitizedCheckout: true });
1043
+ const submodules = await inspectSubmodules(prepared.temporaryPath, observedCommit, prepared.hooksPath, cancellation);
1044
+ receipt.policies.submodules.gitmodules_present = submodules.gitmodulesPresent;
1045
+ receipt.policies.submodules.gitlink_count = submodules.gitlinkCount;
1046
+ receipt.policies.submodules.gitlink_hash = submodules.gitlinkHash;
1047
+ if (request.submodule_policy === "deny" &&
1048
+ (submodules.gitmodulesPresent || submodules.gitlinkCount > 0)) {
1049
+ throw new MaterializeAbort("submodules_denied");
1050
+ }
1051
+ if (request.submodule_policy === "ignore" && submodules.gitlinkCount > 0) {
1052
+ receipt.warnings.push("submodule gitlinks were left uninitialized by policy");
1053
+ }
1054
+ const projectMemory = validateProjectMemory(prepared.temporaryPath, request.project_memory_policy);
1055
+ receipt.policies.project_memory = projectMemory.evidence;
1056
+ if (projectMemory.failure)
1057
+ throw new MaterializeAbort(projectMemory.failure);
1058
+ cancellation.throwIfCancelled();
1059
+ node_fs_1.default.rmSync(prepared.hooksPath, { recursive: true, force: true });
1060
+ if (node_fs_1.default.existsSync(destinationPath))
1061
+ throw new MaterializeAbort("destination_exists");
1062
+ (0, filesystem_authority_1.withContainedPathSink)({ root: options.root, relativePath: request.destination, operation: "create" }, ({ absolutePath }) => {
1063
+ if (node_fs_1.default.existsSync(absolutePath))
1064
+ throw new MaterializeAbort("destination_exists");
1065
+ try {
1066
+ node_fs_1.default.renameSync(prepared.temporaryPath, absolutePath);
1067
+ }
1068
+ catch {
1069
+ throw new MaterializeAbort("atomic_publish_failed");
1070
+ }
1071
+ });
1072
+ accepted = true;
1073
+ receipt.ok = true;
1074
+ receipt.reason_code = "accepted";
1075
+ receipt.destination = { path: request.destination, state: "accepted", published: true };
1076
+ receipt.cleanup = { state: "complete", temporary_paths_remaining: 0 };
1077
+ return receipt;
1078
+ }
1079
+ catch (error) {
1080
+ const reason = error instanceof MaterializeAbort ? error.reasonCode : "internal_error";
1081
+ const cleaned = cleanup(options.root, temporaryPaths, createdParents);
1082
+ receipt.cleanup = {
1083
+ state: temporaryPaths.length === 0 ? "not-required" : cleaned.state,
1084
+ temporary_paths_remaining: cleaned.remaining,
1085
+ };
1086
+ receipt.reason_code = cleaned.state === "failed" ? "cleanup_failed" : reason;
1087
+ receipt.destination = {
1088
+ path: request.destination,
1089
+ state: accepted ? "accepted" : "absent",
1090
+ published: accepted,
1091
+ };
1092
+ if (destinationPath !== null && !accepted && node_fs_1.default.existsSync(destinationPath)) {
1093
+ receipt.reason_code = "atomic_publish_failed";
1094
+ }
1095
+ throw new GitMaterializeError(receipt);
1096
+ }
1097
+ finally {
1098
+ cancellation.stop();
1099
+ }
1100
+ }