@mh-gg/git 0.1.1-alpha.20260626T104441232Z

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,257 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const crypto = require("node:crypto");
4
+ const { canonicalJson } = require("@mh-gg/event/canonicalJson");
5
+ const { parseMatterhornOrigin, originSlug } = require("./originUrl.cjs");
6
+ const { authForDraft, loadUserSigningProfile } = require("./signing.cjs");
7
+ const { dataRootFor, git, gitDirFromEnv, writeJsonFile } = require("./localMirror.cjs");
8
+ const { isMatterhornPointer, materializeCasObjectOutbox, metadataForPointer } = require("./cas.cjs");
9
+
10
+ const GIT_PLUGIN_ID = "com.matterhorn.examples.git";
11
+
12
+ function stableId(prefix, value) {
13
+ return `${prefix}_${crypto.createHash("sha256").update(canonicalJson(value)).digest("hex").slice(0, 24)}`;
14
+ }
15
+
16
+ function outboxDirFor(originInput, options = {}) {
17
+ const origin = parseMatterhornOrigin(originInput);
18
+ const gitDir = options.gitDir || gitDirFromEnv(options.cwd);
19
+ return path.join(dataRootFor(gitDir), "outbox", originSlug(origin));
20
+ }
21
+
22
+ function refUpdateDraft({ origin, ref, oldOid, newOid, profile, message, now = Date.now() }) {
23
+ const parsedOrigin = parseMatterhornOrigin(origin);
24
+ const signing = profile || loadUserSigningProfile(parsedOrigin.signing);
25
+ const payload = {
26
+ repoId: parsedOrigin.repo,
27
+ roomName: parsedOrigin.room,
28
+ ref,
29
+ oldOid: oldOid || null,
30
+ newOid: newOid || null,
31
+ expectedOldOid: oldOid || null,
32
+ fastForwardOnly: true,
33
+ source: "git-remote-matterhorn",
34
+ signingProfile: signing.profile,
35
+ message: message || `Update ${ref} from git-remote-matterhorn`
36
+ };
37
+ const draft = {
38
+ id: stableId("git_ref", { payload, actor: signing.actor, now }),
39
+ type: "git.ref.update",
40
+ pluginId: GIT_PLUGIN_ID,
41
+ payload,
42
+ createdAt: now,
43
+ actor: signing.actor
44
+ };
45
+ draft.auth = authForDraft(signing, draft);
46
+ return draft;
47
+ }
48
+
49
+ async function postJsonTo(target, body, label = "Matterhorn POST") {
50
+ const response = await fetch(target, {
51
+ method: "POST",
52
+ headers: { "content-type": "application/json" },
53
+ body: JSON.stringify(body)
54
+ });
55
+ const text = await response.text();
56
+ let parsed;
57
+ try { parsed = text ? JSON.parse(text) : {}; } catch { parsed = { ok: false, reason: text }; }
58
+ if (!response.ok || parsed.ok === false) {
59
+ const error = new Error(parsed.reason || `${label} failed with ${response.status}`);
60
+ error.response = parsed;
61
+ throw error;
62
+ }
63
+ return parsed;
64
+ }
65
+
66
+ async function postJson(url, body) {
67
+ const target = url.endsWith("/matterhorn/operation") ? url : `${url.replace(/\/$/, "")}/matterhorn/operation`;
68
+ return postJsonTo(target, body, "Matterhorn operation POST");
69
+ }
70
+
71
+ async function postGitObject(url, envelope) {
72
+ const target = url.endsWith("/matterhorn/git/object") ? url : `${url.replace(/\/$/, "")}/matterhorn/git/object`;
73
+ return postJsonTo(target, envelope, "Matterhorn Git object POST");
74
+ }
75
+
76
+ function backendFor(origin, options = {}) {
77
+ return options.backend || origin.backend || process.env.MATTERHORN_GIT_CONTROL_URL;
78
+ }
79
+
80
+ async function publishDraft(originInput, draft, options = {}) {
81
+ const origin = parseMatterhornOrigin(originInput);
82
+ const backend = backendFor(origin, options);
83
+ if (backend) return { mode: "http", response: await postJson(backend, draft) };
84
+ const dir = outboxDirFor(origin, options);
85
+ const file = path.join(dir, `${draft.id}.json`);
86
+ writeJsonFile(file, draft);
87
+ return { mode: "outbox", file };
88
+ }
89
+
90
+ async function publishChangedRefs(originInput, beforeRefs, afterRefs, options = {}) {
91
+ const origin = parseMatterhornOrigin(originInput);
92
+ const profile = loadUserSigningProfile(origin.signing, options);
93
+ const changes = [];
94
+ const allRefs = new Set([...Object.keys(beforeRefs || {}), ...Object.keys(afterRefs || {})]);
95
+ for (const ref of [...allRefs].sort()) {
96
+ const oldOid = beforeRefs?.[ref] || null;
97
+ const newOid = afterRefs?.[ref] || null;
98
+ if (oldOid === newOid) continue;
99
+ const draft = refUpdateDraft({ origin, ref, oldOid, newOid, profile, message: options.message });
100
+ const result = await publishDraft(origin, draft, options);
101
+ changes.push({ ref, oldOid, newOid, draft, result });
102
+ }
103
+ return changes;
104
+ }
105
+
106
+ function manifestPublishDraft({ origin, metadata, profile, now = Date.now() }) {
107
+ const parsedOrigin = parseMatterhornOrigin(origin);
108
+ const signing = profile || loadUserSigningProfile(parsedOrigin.signing);
109
+ const payload = {
110
+ repoId: parsedOrigin.repo,
111
+ epoch: metadata.epoch,
112
+ manifestId: metadata.manifestId,
113
+ objectId: metadata.objectId,
114
+ clearSize: metadata.clearSize,
115
+ chunkCount: metadata.chunkCount,
116
+ pointerProfile: metadata.pointerProfile
117
+ };
118
+ const draft = {
119
+ id: stableId("git_manifest", { payload, actor: signing.actor }),
120
+ type: "git.manifest.publish",
121
+ pluginId: GIT_PLUGIN_ID,
122
+ payload,
123
+ createdAt: now,
124
+ actor: signing.actor
125
+ };
126
+ draft.auth = authForDraft(signing, draft);
127
+ return draft;
128
+ }
129
+
130
+ function chunkAdvertiseDraft({ origin, chunks, epoch, profile, batch = 0, now = Date.now() }) {
131
+ const parsedOrigin = parseMatterhornOrigin(origin);
132
+ const signing = profile || loadUserSigningProfile(parsedOrigin.signing);
133
+ const payload = { repoId: parsedOrigin.repo, epoch, chunks };
134
+ const draft = {
135
+ id: stableId("git_chunks", { payload, actor: signing.actor, batch }),
136
+ type: "git.chunk.advertise",
137
+ pluginId: GIT_PLUGIN_ID,
138
+ payload,
139
+ createdAt: now,
140
+ actor: signing.actor
141
+ };
142
+ draft.auth = authForDraft(signing, draft);
143
+ return draft;
144
+ }
145
+
146
+ async function publishPointerMetadata(originInput, pointerText, options = {}) {
147
+ const origin = parseMatterhornOrigin(originInput);
148
+ const profile = loadUserSigningProfile(origin.signing, options);
149
+ const metadata = metadataForPointer(pointerText, origin, options);
150
+ const results = [];
151
+ if (options.stageObjects !== false) {
152
+ const staged = materializeCasObjectOutbox(origin, pointerText, options);
153
+ const backend = backendFor(origin, options);
154
+ if (backend) {
155
+ const uploads = [];
156
+ for (const file of staged.files) uploads.push(await postGitObject(backend, JSON.parse(fs.readFileSync(file, "utf8"))));
157
+ results.push({ kind: "objects", mode: "http-object", dir: staged.dir, files: staged.files, uploads });
158
+ } else {
159
+ results.push({ kind: "objects", mode: "cas-outbox", dir: staged.dir, files: staged.files });
160
+ }
161
+ }
162
+ const manifestDraft = manifestPublishDraft({ origin, metadata, profile, now: options.now || Date.now() });
163
+ results.push({ kind: "manifest", draft: manifestDraft, result: await publishDraft(origin, manifestDraft, options) });
164
+ const chunks = metadata.chunks || [];
165
+ for (let i = 0; i < chunks.length; i += 500) {
166
+ const batch = chunks.slice(i, i + 500);
167
+ if (!batch.length) continue;
168
+ const chunkDraft = chunkAdvertiseDraft({ origin, chunks: batch, epoch: metadata.epoch, profile, batch: i / 500, now: options.now || Date.now() });
169
+ results.push({ kind: "chunks", draft: chunkDraft, result: await publishDraft(origin, chunkDraft, options) });
170
+ }
171
+ return { metadata, results };
172
+ }
173
+
174
+ function treeEntriesForRef(mirrorPath, oid) {
175
+ try {
176
+ const tree = git(["--git-dir", mirrorPath, "ls-tree", "-r", "-z", oid], { encoding: "buffer" });
177
+ return (Buffer.isBuffer(tree) ? tree.toString("utf8") : String(tree)).split("\0");
178
+ } catch (matterhornIgnoredError) {
179
+ globalThis.__matterhornIgnoredError?.(matterhornIgnoredError, "packages/matterhorn-git/src/controlPlane.cjs");
180
+ return [];
181
+ }
182
+ }
183
+
184
+ function parseBlobTreeEntry(entry) {
185
+ if (!entry) return null;
186
+ const match = entry.match(/^([0-7]{6})\s+(\S+)\s+([0-9a-f]{40,64})\t(.+)$/);
187
+ if (!match || match[2] !== "blob") return null;
188
+ return { blob: match[3], filepath: match[4] };
189
+ }
190
+
191
+ function smallBlobText(mirrorPath, blob) {
192
+ try {
193
+ const size = Number(git(["--git-dir", mirrorPath, "cat-file", "-s", blob]).trim());
194
+ if (!Number.isFinite(size) || size > 4096) return null;
195
+ return git(["--git-dir", mirrorPath, "cat-file", "-p", blob]);
196
+ } catch (matterhornIgnoredError) {
197
+ globalThis.__matterhornIgnoredError?.(matterhornIgnoredError, "packages/matterhorn-git/src/controlPlane.cjs");
198
+ return null;
199
+ }
200
+ }
201
+
202
+ function rememberPointerBlob(pointers, seenBlobs, mirrorPath, entry) {
203
+ const parsed = parseBlobTreeEntry(entry);
204
+ if (!parsed || seenBlobs.has(parsed.blob)) return;
205
+ seenBlobs.add(parsed.blob);
206
+ const text = smallBlobText(mirrorPath, parsed.blob);
207
+ if (text && isMatterhornPointer(text)) pointers.set(text, { pointer: text, blob: parsed.blob, filepath: parsed.filepath });
208
+ }
209
+
210
+ function scanMirrorPointers(mirrorPath, refs = {}) {
211
+ const pointers = new Map();
212
+ const seenBlobs = new Set();
213
+ const objectIds = Object.values(refs || {}).filter(Boolean);
214
+ for (const oid of objectIds) {
215
+ for (const entry of treeEntriesForRef(mirrorPath, oid)) rememberPointerBlob(pointers, seenBlobs, mirrorPath, entry);
216
+ }
217
+ return [...pointers.values()].sort((a, b) => a.filepath.localeCompare(b.filepath));
218
+ }
219
+
220
+ async function publishPointersForMirrorRefs(originInput, mirrorPath, refs = {}, options = {}) {
221
+ const origin = parseMatterhornOrigin(originInput);
222
+ const pointers = scanMirrorPointers(mirrorPath, refs);
223
+ const published = [];
224
+ for (const pointer of pointers) {
225
+ try {
226
+ const result = await publishPointerMetadata(origin, pointer.pointer, options);
227
+ published.push({ ...pointer, ...result });
228
+ } catch (error) {
229
+ if (options.allowMissingPointers) published.push({ ...pointer, error });
230
+ else throw error;
231
+ }
232
+ }
233
+ return published;
234
+ }
235
+
236
+ function listOutbox(originInput, options = {}) {
237
+ const origin = parseMatterhornOrigin(originInput);
238
+ const dir = outboxDirFor(origin, options);
239
+ if (!fs.existsSync(dir)) return [];
240
+ return fs.readdirSync(dir).filter((name) => name.endsWith(".json")).sort().map((name) => path.join(dir, name));
241
+ }
242
+
243
+ module.exports = {
244
+ GIT_PLUGIN_ID,
245
+ chunkAdvertiseDraft,
246
+ listOutbox,
247
+ manifestPublishDraft,
248
+ outboxDirFor,
249
+ publishChangedRefs,
250
+ publishPointerMetadata,
251
+ publishPointersForMirrorRefs,
252
+ postGitObject,
253
+ publishDraft,
254
+ refUpdateDraft,
255
+ scanMirrorPointers,
256
+ stableId
257
+ };
package/src/crypto.cjs ADDED
@@ -0,0 +1,213 @@
1
+ const crypto = require("node:crypto");
2
+ const { canonicalJson } = require("@mh-gg/event/canonicalJson");
3
+
4
+ const XCHACHA20_POLY1305_ALG = "xchacha20-poly1305-ietf";
5
+ const TAG_BYTES = 16;
6
+ const NONCE_BYTES = 24;
7
+
8
+ function toBuffer(value, label = "value") {
9
+ if (Buffer.isBuffer(value)) return value;
10
+ if (value instanceof Uint8Array) return Buffer.from(value);
11
+ if (typeof value === "string") return Buffer.from(value, "utf8");
12
+ throw new Error(`${label} must be a Buffer, Uint8Array, or string`);
13
+ }
14
+
15
+ function base64url(buffer) {
16
+ return Buffer.from(buffer).toString("base64url");
17
+ }
18
+
19
+ function fromBase64url(value, label = "base64url") {
20
+ try {
21
+ return Buffer.from(String(value || ""), "base64url");
22
+ } catch {
23
+ throw new Error(`${label} must be base64url encoded`);
24
+ }
25
+ }
26
+
27
+ function parseSecret(value, label = "secret") {
28
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array) return Buffer.from(value);
29
+ const text = String(value || "").trim();
30
+ if (!text) throw new Error(`${label} is required`);
31
+ if (/^[0-9a-fA-F]{64}$/.test(text)) return Buffer.from(text, "hex");
32
+ try {
33
+ const decoded = Buffer.from(text, "base64url");
34
+ if (decoded.length >= 32) return decoded.subarray(0, 32);
35
+ } catch (matterhornIgnoredError) {
36
+ globalThis.__matterhornIgnoredError?.(matterhornIgnoredError, "packages/matterhorn-git/src/crypto.cjs");
37
+ }
38
+ try {
39
+ const decoded = Buffer.from(text, "base64");
40
+ if (decoded.length >= 32) return decoded.subarray(0, 32);
41
+ } catch (matterhornIgnoredError) {
42
+ globalThis.__matterhornIgnoredError?.(matterhornIgnoredError, "packages/matterhorn-git/src/crypto.cjs");
43
+ }
44
+ return crypto.createHash("sha256").update(text).digest();
45
+ }
46
+
47
+ function randomBytes(size) {
48
+ return crypto.randomBytes(size);
49
+ }
50
+
51
+ function sha256Hex(...parts) {
52
+ const hash = crypto.createHash("sha256");
53
+ for (const part of parts) hash.update(toBuffer(part));
54
+ return hash.digest("hex");
55
+ }
56
+
57
+ function hmacSha256Hex(key, ...parts) {
58
+ const hmac = crypto.createHmac("sha256", toBuffer(key, "key"));
59
+ for (const part of parts) hmac.update(toBuffer(part));
60
+ return hmac.digest("hex");
61
+ }
62
+
63
+ function hmacSha256Buffer(key, ...parts) {
64
+ const hmac = crypto.createHmac("sha256", toBuffer(key, "key"));
65
+ for (const part of parts) hmac.update(toBuffer(part));
66
+ return hmac.digest();
67
+ }
68
+
69
+ function deriveSubkey(root, context, size = 32, salt = "matterhorn-git-v1") {
70
+ const derived = crypto.hkdfSync("sha256", toBuffer(root, "root"), toBuffer(salt), toBuffer(context), size);
71
+ return Buffer.from(derived);
72
+ }
73
+
74
+ function deriveEpochSubkeys(epochRoot, { repoId, epoch }) {
75
+ const context = `repo=${repoId};epoch=${Number(epoch) || 1}`;
76
+ return {
77
+ chunkId: deriveSubkey(epochRoot, `rk-git chunk-id v1;${context}`),
78
+ chunkEnc: deriveSubkey(epochRoot, `rk-git chunk-enc v1;${context}`),
79
+ manifestId: deriveSubkey(epochRoot, `rk-git manifest-id v1;${context}`),
80
+ manifestEnc: deriveSubkey(epochRoot, `rk-git manifest-enc v1;${context}`)
81
+ };
82
+ }
83
+
84
+ function readU32LE(buffer, offset) {
85
+ return buffer.readUInt32LE(offset) >>> 0;
86
+ }
87
+
88
+ function writeU32LE(value, buffer, offset) {
89
+ buffer.writeUInt32LE(value >>> 0, offset);
90
+ }
91
+
92
+ function rotl32(value, bits) {
93
+ return ((value << bits) | (value >>> (32 - bits))) >>> 0;
94
+ }
95
+
96
+ function quarterRound(x, a, b, c, d) {
97
+ x[a] = (x[a] + x[b]) >>> 0; x[d] = rotl32(x[d] ^ x[a], 16);
98
+ x[c] = (x[c] + x[d]) >>> 0; x[b] = rotl32(x[b] ^ x[c], 12);
99
+ x[a] = (x[a] + x[b]) >>> 0; x[d] = rotl32(x[d] ^ x[a], 8);
100
+ x[c] = (x[c] + x[d]) >>> 0; x[b] = rotl32(x[b] ^ x[c], 7);
101
+ }
102
+
103
+ function hChaCha20(keyInput, nonceInput) {
104
+ const key = toBuffer(keyInput, "key");
105
+ const nonce = toBuffer(nonceInput, "nonce");
106
+ if (key.length !== 32) throw new Error("XChaCha20 key must be 32 bytes");
107
+ if (nonce.length !== 16) throw new Error("HChaCha20 nonce must be 16 bytes");
108
+ const constants = Buffer.from("expand 32-byte k", "ascii");
109
+ const state = new Uint32Array(16);
110
+ for (let i = 0; i < 4; i += 1) state[i] = readU32LE(constants, i * 4);
111
+ for (let i = 0; i < 8; i += 1) state[4 + i] = readU32LE(key, i * 4);
112
+ for (let i = 0; i < 4; i += 1) state[12 + i] = readU32LE(nonce, i * 4);
113
+ for (let round = 0; round < 10; round += 1) {
114
+ quarterRound(state, 0, 4, 8, 12);
115
+ quarterRound(state, 1, 5, 9, 13);
116
+ quarterRound(state, 2, 6, 10, 14);
117
+ quarterRound(state, 3, 7, 11, 15);
118
+ quarterRound(state, 0, 5, 10, 15);
119
+ quarterRound(state, 1, 6, 11, 12);
120
+ quarterRound(state, 2, 7, 8, 13);
121
+ quarterRound(state, 3, 4, 9, 14);
122
+ }
123
+ const out = Buffer.alloc(32);
124
+ writeU32LE(state[0], out, 0);
125
+ writeU32LE(state[1], out, 4);
126
+ writeU32LE(state[2], out, 8);
127
+ writeU32LE(state[3], out, 12);
128
+ writeU32LE(state[12], out, 16);
129
+ writeU32LE(state[13], out, 20);
130
+ writeU32LE(state[14], out, 24);
131
+ writeU32LE(state[15], out, 28);
132
+ return out;
133
+ }
134
+
135
+ function xNonceToIetf(keyInput, nonceInput) {
136
+ const key = toBuffer(keyInput, "key");
137
+ const nonce = toBuffer(nonceInput, "nonce");
138
+ if (key.length !== 32) throw new Error("XChaCha20 key must be 32 bytes");
139
+ if (nonce.length !== NONCE_BYTES) throw new Error("XChaCha20 nonce must be 24 bytes");
140
+ const subkey = hChaCha20(key, nonce.subarray(0, 16));
141
+ const ietfNonce = Buffer.concat([Buffer.alloc(4, 0), nonce.subarray(16, 24)]);
142
+ return { subkey, ietfNonce };
143
+ }
144
+
145
+ function canonicalAad(value) {
146
+ return Buffer.from(canonicalJson(value), "utf8");
147
+ }
148
+
149
+ function xChaCha20Poly1305Seal({ key, nonce = randomBytes(NONCE_BYTES), plaintext, aad = Buffer.alloc(0) }) {
150
+ const actualNonce = toBuffer(nonce, "nonce");
151
+ const { subkey, ietfNonce } = xNonceToIetf(key, actualNonce);
152
+ const cipher = crypto.createCipheriv("chacha20-poly1305", subkey, ietfNonce, { authTagLength: TAG_BYTES });
153
+ const aadBuffer = toBuffer(aad, "aad");
154
+ if (aadBuffer.length) cipher.setAAD(aadBuffer, { plaintextLength: toBuffer(plaintext, "plaintext").length });
155
+ const ciphertext = Buffer.concat([cipher.update(toBuffer(plaintext, "plaintext")), cipher.final()]);
156
+ const tag = cipher.getAuthTag();
157
+ return { alg: XCHACHA20_POLY1305_ALG, nonce: actualNonce, ciphertext, tag };
158
+ }
159
+
160
+ function xChaCha20Poly1305Open({ key, nonce, ciphertext, tag, aad = Buffer.alloc(0) }) {
161
+ const { subkey, ietfNonce } = xNonceToIetf(key, nonce);
162
+ const decipher = crypto.createDecipheriv("chacha20-poly1305", subkey, ietfNonce, { authTagLength: TAG_BYTES });
163
+ const aadBuffer = toBuffer(aad, "aad");
164
+ if (aadBuffer.length) decipher.setAAD(aadBuffer, { plaintextLength: toBuffer(ciphertext, "ciphertext").length });
165
+ decipher.setAuthTag(toBuffer(tag, "tag"));
166
+ return Buffer.concat([decipher.update(toBuffer(ciphertext, "ciphertext")), decipher.final()]);
167
+ }
168
+
169
+ function chunkAad({ repoId, epoch, chunkPlainId, clearLen, chunker, alg = "rk-git-chunks-v1" }) {
170
+ return canonicalAad({
171
+ kind: "matterhorn.git.chunk-object.v1",
172
+ repoId,
173
+ keyEpoch: Number(epoch),
174
+ chunkPlainId,
175
+ clearLen: Number(clearLen),
176
+ chunker,
177
+ aead: XCHACHA20_POLY1305_ALG,
178
+ alg
179
+ });
180
+ }
181
+
182
+ function manifestAad({ repoId, epoch, manifestId, alg = "rk-git-chunks-v1" }) {
183
+ return canonicalAad({
184
+ kind: "matterhorn.git.file-manifest.v1",
185
+ repoId,
186
+ keyEpoch: Number(epoch),
187
+ manifestId,
188
+ aead: XCHACHA20_POLY1305_ALG,
189
+ alg
190
+ });
191
+ }
192
+
193
+ module.exports = {
194
+ NONCE_BYTES,
195
+ TAG_BYTES,
196
+ XCHACHA20_POLY1305_ALG,
197
+ base64url,
198
+ canonicalAad,
199
+ chunkAad,
200
+ deriveEpochSubkeys,
201
+ deriveSubkey,
202
+ fromBase64url,
203
+ hChaCha20,
204
+ hmacSha256Buffer,
205
+ hmacSha256Hex,
206
+ manifestAad,
207
+ parseSecret,
208
+ randomBytes,
209
+ sha256Hex,
210
+ toBuffer,
211
+ xChaCha20Poly1305Open,
212
+ xChaCha20Poly1305Seal
213
+ };
package/src/index.cjs ADDED
@@ -0,0 +1,11 @@
1
+ module.exports = {
2
+ ...require("./chunker.cjs"),
3
+ ...require("./crypto.cjs"),
4
+ ...require("./cas.cjs"),
5
+ ...require("./controlPlane.cjs"),
6
+ ...require("./localMirror.cjs"),
7
+ ...require("./manifest.cjs"),
8
+ ...require("./originUrl.cjs"),
9
+ ...require("./remoteHelper.cjs"),
10
+ ...require("./signing.cjs")
11
+ };
@@ -0,0 +1,121 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const { spawnSync } = require("node:child_process");
4
+ const { originSlug, parseMatterhornOrigin } = require("./originUrl.cjs");
5
+
6
+ function gitDirFromEnv(cwd = process.cwd()) {
7
+ if (process.env.GIT_DIR) return path.resolve(cwd, process.env.GIT_DIR);
8
+ const dotGit = path.join(cwd, ".git");
9
+ if (fs.existsSync(dotGit)) return dotGit;
10
+ const result = spawnSync("git", ["rev-parse", "--git-dir"], { cwd, encoding: "utf8", windowsHide: true });
11
+ if (result.status === 0) return path.resolve(cwd, result.stdout.trim());
12
+ return dotGit;
13
+ }
14
+
15
+ function dataRootFor(gitDir) {
16
+ return path.resolve(process.env.MATTERHORN_GIT_DATA_DIR || path.join(gitDir, "matterhorn-sdk"));
17
+ }
18
+
19
+ function git(args, options = {}) {
20
+ const result = spawnSync("git", args, {
21
+ cwd: options.cwd || process.cwd(),
22
+ encoding: options.encoding || "utf8",
23
+ stdio: options.stdio || "pipe",
24
+ env: { ...process.env, ...(options.env || {}) },
25
+ windowsHide: true
26
+ });
27
+ if (result.status !== 0) {
28
+ const message = result.stderr || result.stdout || result.error?.message || `git ${args.join(" ")} failed`;
29
+ const error = new Error(message.trim());
30
+ error.result = result;
31
+ throw error;
32
+ }
33
+ return result.stdout;
34
+ }
35
+
36
+ function gitConfigGet(name, options = {}) {
37
+ try { return git(["config", "--get", name], { cwd: options.cwd }).trim(); } catch { return undefined; }
38
+ }
39
+
40
+ function configuredOrigin(options = {}) {
41
+ const explicit = options.origin || process.env.MATTERHORN_GIT_ORIGIN;
42
+ if (explicit) return explicit;
43
+ const matterhornOrigin = gitConfigGet("matterhorn.origin", options);
44
+ if (matterhornOrigin) return matterhornOrigin;
45
+ const remote = gitConfigGet("remote.origin.url", options);
46
+ if (remote && (remote.startsWith("matterhorn::") || remote.startsWith("matterhorn://") || remote.startsWith("matterhorn+"))) return remote;
47
+ throw new Error("Matterhorn Git origin is not configured. Run matterhorn-git install-filter <matterhorn-origin> or set MATTERHORN_GIT_ORIGIN.");
48
+ }
49
+
50
+ function mirrorPathFor(origin, options = {}) {
51
+ const gitDir = options.gitDir || gitDirFromEnv(options.cwd);
52
+ const root = dataRootFor(gitDir);
53
+ return path.join(root, "mirrors", originSlug(origin));
54
+ }
55
+
56
+ function ensureLocalMirror(originInput, options = {}) {
57
+ const origin = parseMatterhornOrigin(originInput);
58
+ const mirrorPath = mirrorPathFor(origin, options);
59
+ if (!fs.existsSync(path.join(mirrorPath, "HEAD"))) {
60
+ fs.mkdirSync(path.dirname(mirrorPath), { recursive: true, mode: 0o700 });
61
+ git(["init", "--bare", mirrorPath], { cwd: options.cwd });
62
+ git(["--git-dir", mirrorPath, "symbolic-ref", "HEAD", `refs/heads/${options.defaultBranch || "main"}`], { cwd: options.cwd });
63
+ }
64
+ return { origin, mirrorPath };
65
+ }
66
+
67
+ function refsFileFor(originInput, options = {}) {
68
+ const origin = parseMatterhornOrigin(originInput);
69
+ const gitDir = options.gitDir || gitDirFromEnv(options.cwd);
70
+ const root = dataRootFor(gitDir);
71
+ return path.join(root, "state", originSlug(origin), "refs.json");
72
+ }
73
+
74
+ function readJsonFile(file, fallback) {
75
+ try {
76
+ return JSON.parse(fs.readFileSync(file, "utf8"));
77
+ } catch {
78
+ return fallback;
79
+ }
80
+ }
81
+
82
+ function writeJsonFile(file, value) {
83
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
84
+ fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
85
+ }
86
+
87
+ function listMirrorRefs(mirrorPath) {
88
+ const stdout = git(["--git-dir", mirrorPath, "for-each-ref", "--format=%(objectname) %(refname)", "refs/heads", "refs/tags"], { cwd: process.cwd() });
89
+ const refs = {};
90
+ for (const line of stdout.split(/\r?\n/)) {
91
+ const trimmed = line.trim();
92
+ if (!trimmed) continue;
93
+ const [oid, ...nameParts] = trimmed.split(" ");
94
+ const name = nameParts.join(" ");
95
+ if (oid && name) refs[name] = oid;
96
+ }
97
+ return refs;
98
+ }
99
+
100
+ function readSavedRefs(originInput, options = {}) {
101
+ return readJsonFile(refsFileFor(originInput, options), { refs: {} }).refs || {};
102
+ }
103
+
104
+ function saveRefs(originInput, refs, options = {}) {
105
+ writeJsonFile(refsFileFor(originInput, options), { version: 1, refs, updatedAt: Date.now() });
106
+ }
107
+
108
+ module.exports = {
109
+ configuredOrigin,
110
+ dataRootFor,
111
+ gitConfigGet,
112
+ ensureLocalMirror,
113
+ git,
114
+ gitDirFromEnv,
115
+ listMirrorRefs,
116
+ mirrorPathFor,
117
+ readSavedRefs,
118
+ refsFileFor,
119
+ saveRefs,
120
+ writeJsonFile
121
+ };
@@ -0,0 +1,60 @@
1
+ const crypto = require("node:crypto");
2
+ const { canonicalJson } = require("@mh-gg/event/canonicalJson");
3
+
4
+ function hmacHex(key, value) {
5
+ const secret = Buffer.isBuffer(key) || key instanceof Uint8Array ? Buffer.from(key) : Buffer.from(String(key || "matterhorn-git-dev-key"));
6
+ return crypto.createHmac("sha256", secret).update(canonicalJson(value)).digest("hex");
7
+ }
8
+
9
+ function createLogicalManifestIdentity(input = {}, key = input.key) {
10
+ const material = {
11
+ kind: "matterhorn.git.logical-manifest.v1",
12
+ repoId: input.repoId,
13
+ epoch: input.epoch,
14
+ fileMode: input.fileMode || "100644",
15
+ clearSize: Number(input.clearSize || 0),
16
+ chunker: input.chunker || { name: "fastcdc", version: "v1" },
17
+ chunks: (input.chunks || []).map((chunk, index) => ({
18
+ index: Number.isFinite(chunk.index) ? chunk.index : index,
19
+ chunkPlainId: chunk.chunkPlainId || chunk.plainId,
20
+ clearLen: Number(chunk.clearLen || chunk.length || 0)
21
+ }))
22
+ };
23
+ return `hmac-sha256:${hmacHex(key, material)}`;
24
+ }
25
+
26
+ function createPointer(input = {}) {
27
+ const lines = [
28
+ "version https://matterhorn.gg/git-chunks/v1",
29
+ `repo ${input.repoId}`,
30
+ `epoch ${input.epoch}`,
31
+ `manifest-id ${input.manifestId}`,
32
+ `clear-size ${Number(input.clearSize || 0)}`,
33
+ `chunk-count ${Number(input.chunkCount || 0)}`,
34
+ `profile ${input.profile || "dedup-same-epoch/encrypted-manifest/no-padding"}`,
35
+ `alg ${input.alg || "rk-git-chunks-v1"}`
36
+ ];
37
+ return `${lines.join("\n")}\n`;
38
+ }
39
+
40
+ function parsePointer(text) {
41
+ const fields = {};
42
+ for (const line of String(text || "").split(/\r?\n/)) {
43
+ const trimmed = line.trim();
44
+ if (!trimmed) continue;
45
+ const [key, ...rest] = trimmed.split(/\s+/);
46
+ fields[key] = rest.join(" ");
47
+ }
48
+ if (fields.version !== "https://matterhorn.gg/git-chunks/v1") throw new Error("Not a Matterhorn Git pointer");
49
+ return {
50
+ repoId: fields.repo,
51
+ epoch: Number(fields.epoch),
52
+ manifestId: fields["manifest-id"],
53
+ clearSize: Number(fields["clear-size"] || 0),
54
+ chunkCount: Number(fields["chunk-count"] || 0),
55
+ profile: fields.profile,
56
+ alg: fields.alg
57
+ };
58
+ }
59
+
60
+ module.exports = { createLogicalManifestIdentity, createPointer, parsePointer };