@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.
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/bin/git-remote-matterhorn.cjs +7 -0
- package/bin/matterhorn-git.cjs +297 -0
- package/package.json +38 -0
- package/src/cas/pointer.cjs +121 -0
- package/src/cas/storage.cjs +389 -0
- package/src/cas.cjs +26 -0
- package/src/chunker.cjs +67 -0
- package/src/controlPlane.cjs +257 -0
- package/src/crypto.cjs +213 -0
- package/src/index.cjs +11 -0
- package/src/localMirror.cjs +121 -0
- package/src/manifest.cjs +60 -0
- package/src/originUrl.cjs +163 -0
- package/src/remoteHelper.cjs +85 -0
- package/src/signing.cjs +110 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
const DEFAULT_TRANSPORT = "matterhorn-sdk";
|
|
2
|
+
|
|
3
|
+
function decode(value) {
|
|
4
|
+
return decodeURIComponent(String(value || ""));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function encode(value) {
|
|
8
|
+
return encodeURIComponent(String(value || ""));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function cleanText(value, label, max = 180) {
|
|
12
|
+
const text = String(value || "").trim();
|
|
13
|
+
if (!text) throw new Error(`${label} is required`);
|
|
14
|
+
if (text.length > max) throw new Error(`${label} is too long`);
|
|
15
|
+
return text;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseSemiAddress(address) {
|
|
19
|
+
const out = {};
|
|
20
|
+
for (const part of String(address || "").split(";")) {
|
|
21
|
+
if (!part.trim()) continue;
|
|
22
|
+
const index = part.indexOf("=");
|
|
23
|
+
if (index === -1) {
|
|
24
|
+
if (!out.repo) out.repo = decode(part.trim());
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const key = part.slice(0, index).trim();
|
|
28
|
+
const value = part.slice(index + 1).trim();
|
|
29
|
+
if (key) out[key] = decode(value);
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseFriendlyPlus(value) {
|
|
35
|
+
const input = String(value || "").trim();
|
|
36
|
+
const match = input.match(/^matterhorn\+([^+:/?#]+)\+([^:/?#]+):\/\/?([^?#]*)(?:\?([^#]*))?$/);
|
|
37
|
+
if (!match) return undefined;
|
|
38
|
+
const params = new URLSearchParams(match[4] || "");
|
|
39
|
+
return {
|
|
40
|
+
transport: DEFAULT_TRANSPORT,
|
|
41
|
+
room: decode(match[1]),
|
|
42
|
+
signing: decode(match[2]),
|
|
43
|
+
repo: decode((match[3] || "").replace(/^\/+/, "")),
|
|
44
|
+
backend: params.get("backend") || undefined,
|
|
45
|
+
relay: params.get("relay") || undefined
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseCompactAddress(address) {
|
|
50
|
+
const input = String(address || "").trim();
|
|
51
|
+
const [head, ...rest] = input.split(";");
|
|
52
|
+
const match = head.match(/^matterhorn\+([^+\/]+)\+([^\/]+)\/?(.*)$/);
|
|
53
|
+
if (!match) return undefined;
|
|
54
|
+
const params = parseSemiAddress(rest.join(";"));
|
|
55
|
+
return {
|
|
56
|
+
transport: DEFAULT_TRANSPORT,
|
|
57
|
+
room: decode(match[1]),
|
|
58
|
+
signing: decode(match[2]),
|
|
59
|
+
repo: decode((match[3] || "").replace(/^\/+/, "")),
|
|
60
|
+
backend: params.backend,
|
|
61
|
+
relay: params.relay
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseMatterhornOrigin(value, defaults = {}) {
|
|
66
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
67
|
+
const parsed = {
|
|
68
|
+
transport: DEFAULT_TRANSPORT,
|
|
69
|
+
room: cleanText(value.room || value.roomName || defaults.room, "room"),
|
|
70
|
+
repo: cleanText(value.repo || value.repository || defaults.repo, "repo"),
|
|
71
|
+
signing: cleanText(value.signing || value.userSigning || value.profile || defaults.signing || "default", "signing profile"),
|
|
72
|
+
backend: value.backend || defaults.backend,
|
|
73
|
+
relay: value.relay || defaults.relay
|
|
74
|
+
};
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const raw = String(value || "").trim();
|
|
79
|
+
if (!raw) throw new Error("Matterhorn origin URL is required");
|
|
80
|
+
|
|
81
|
+
const friendly = parseFriendlyPlus(raw);
|
|
82
|
+
if (friendly) return finalize(friendly, defaults);
|
|
83
|
+
|
|
84
|
+
const bareCompact = raw.startsWith("matterhorn+") ? parseCompactAddress(raw) : undefined;
|
|
85
|
+
if (bareCompact) return finalize(bareCompact, defaults);
|
|
86
|
+
|
|
87
|
+
if (raw.startsWith("matterhorn::")) {
|
|
88
|
+
const address = raw.slice("matterhorn::".length);
|
|
89
|
+
const compact = parseCompactAddress(address);
|
|
90
|
+
if (compact) return finalize(compact, defaults);
|
|
91
|
+
return finalize({ transport: DEFAULT_TRANSPORT, ...parseSemiAddress(address) }, defaults);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (raw.startsWith("matterhorn://")) {
|
|
95
|
+
const url = new URL(raw);
|
|
96
|
+
const parts = url.pathname.replace(/^\/+/, "").split("/").filter(Boolean);
|
|
97
|
+
return finalize({
|
|
98
|
+
transport: DEFAULT_TRANSPORT,
|
|
99
|
+
room: url.hostname,
|
|
100
|
+
repo: parts.join("/") || url.searchParams.get("repo"),
|
|
101
|
+
signing: url.searchParams.get("signing") || url.searchParams.get("profile"),
|
|
102
|
+
backend: url.searchParams.get("backend") || undefined,
|
|
103
|
+
relay: url.searchParams.get("relay") || undefined
|
|
104
|
+
}, defaults);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return finalize({ transport: DEFAULT_TRANSPORT, ...parseSemiAddress(raw) }, defaults);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function finalize(input, defaults) {
|
|
111
|
+
const parsed = {
|
|
112
|
+
transport: DEFAULT_TRANSPORT,
|
|
113
|
+
room: cleanText(input.room || input.roomName || defaults.room, "room"),
|
|
114
|
+
repo: cleanText(input.repo || input.repository || defaults.repo, "repo"),
|
|
115
|
+
signing: cleanText(input.signing || input.userSigning || input.profile || defaults.signing || "default", "signing profile"),
|
|
116
|
+
backend: input.backend || defaults.backend,
|
|
117
|
+
relay: input.relay || defaults.relay
|
|
118
|
+
};
|
|
119
|
+
return parsed;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function formatMatterhornOrigin(input) {
|
|
123
|
+
const origin = parseMatterhornOrigin(input);
|
|
124
|
+
const parts = [
|
|
125
|
+
`room=${encode(origin.room)}`,
|
|
126
|
+
`repo=${encode(origin.repo)}`,
|
|
127
|
+
`signing=${encode(origin.signing)}`
|
|
128
|
+
];
|
|
129
|
+
if (origin.backend) parts.push(`backend=${encode(origin.backend)}`);
|
|
130
|
+
if (origin.relay) parts.push(`relay=${encode(origin.relay)}`);
|
|
131
|
+
return `matterhorn::${parts.join(";")}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function formatCompactMatterhornOrigin(input) {
|
|
135
|
+
const origin = parseMatterhornOrigin(input);
|
|
136
|
+
const tail = origin.repo ? `/${encode(origin.repo)}` : "";
|
|
137
|
+
const params = [];
|
|
138
|
+
if (origin.backend) params.push(`backend=${encode(origin.backend)}`);
|
|
139
|
+
if (origin.relay) params.push(`relay=${encode(origin.relay)}`);
|
|
140
|
+
return `matterhorn::matterhorn+${encode(origin.room)}+${encode(origin.signing)}${tail}${params.length ? `;${params.join(";")}` : ""}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function formatFriendlyOrigin(input) {
|
|
144
|
+
const origin = parseMatterhornOrigin(input);
|
|
145
|
+
const query = new URLSearchParams();
|
|
146
|
+
if (origin.backend) query.set("backend", origin.backend);
|
|
147
|
+
if (origin.relay) query.set("relay", origin.relay);
|
|
148
|
+
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
149
|
+
return `matterhorn+${encode(origin.room)}+${encode(origin.signing)}://${encode(origin.repo)}${suffix}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function originSlug(input) {
|
|
153
|
+
const origin = parseMatterhornOrigin(input);
|
|
154
|
+
return `${origin.room}--${origin.repo}--${origin.signing}`.replace(/[^A-Za-z0-9_.-]+/g, "_").slice(0, 180) || "matterhorn-origin";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
formatCompactMatterhornOrigin,
|
|
159
|
+
formatFriendlyOrigin,
|
|
160
|
+
formatMatterhornOrigin,
|
|
161
|
+
originSlug,
|
|
162
|
+
parseMatterhornOrigin
|
|
163
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const { spawn } = require("node:child_process");
|
|
2
|
+
const readline = require("node:readline");
|
|
3
|
+
const { ensureLocalMirror, listMirrorRefs, readSavedRefs, saveRefs } = require("./localMirror.cjs");
|
|
4
|
+
const { parseMatterhornOrigin } = require("./originUrl.cjs");
|
|
5
|
+
const { publishChangedRefs, publishPointersForMirrorRefs } = require("./controlPlane.cjs");
|
|
6
|
+
|
|
7
|
+
function writeLine(line = "") {
|
|
8
|
+
process.stdout.write(`${line}\n`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function log(options, message) {
|
|
12
|
+
if (options.quiet) return;
|
|
13
|
+
process.stderr.write(`[matterhorn-git] ${message}\n`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseHelperArgs(argv = process.argv.slice(2)) {
|
|
17
|
+
const remoteName = argv[0];
|
|
18
|
+
const url = argv[1] || argv[0];
|
|
19
|
+
return { remoteName, url };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function connectService(service, mirrorPath, origin, beforeRefs, options = {}) {
|
|
23
|
+
if (service !== "git-upload-pack" && service !== "git-receive-pack") {
|
|
24
|
+
writeLine("fallback");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
writeLine("");
|
|
28
|
+
const child = spawn(service, [mirrorPath], { stdio: ["pipe", "pipe", "inherit"] });
|
|
29
|
+
process.stdin.pipe(child.stdin);
|
|
30
|
+
child.stdout.pipe(process.stdout);
|
|
31
|
+
const code = await new Promise((resolve) => child.on("close", resolve));
|
|
32
|
+
process.stdin.unpipe(child.stdin);
|
|
33
|
+
if (service === "git-receive-pack") {
|
|
34
|
+
const afterRefs = listMirrorRefs(mirrorPath);
|
|
35
|
+
try {
|
|
36
|
+
const pointerPublishes = await publishPointersForMirrorRefs(origin, mirrorPath, afterRefs, options);
|
|
37
|
+
for (const item of pointerPublishes) log(options, `published pointer metadata for ${item.filepath}`);
|
|
38
|
+
const changes = await publishChangedRefs(origin, beforeRefs, afterRefs, options);
|
|
39
|
+
saveRefs(origin, afterRefs, options);
|
|
40
|
+
for (const change of changes) {
|
|
41
|
+
const result = change.result.mode === "http" ? "posted" : `queued ${change.result.file}`;
|
|
42
|
+
log(options, `${change.ref} ${change.oldOid || "0000000"} -> ${change.newOid || "deleted"}: ${result}`);
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
log(options, `control-plane publish failed: ${error.message || error}`);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
process.exit(code || process.exitCode || 0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function runRemoteHelper(argv = process.argv.slice(2), options = {}) {
|
|
53
|
+
const { url } = parseHelperArgs(argv);
|
|
54
|
+
const origin = parseMatterhornOrigin(url || process.env.MATTERHORN_GIT_ORIGIN);
|
|
55
|
+
const { mirrorPath } = ensureLocalMirror(origin, options);
|
|
56
|
+
const savedRefs = readSavedRefs(origin, options);
|
|
57
|
+
const mirrorRefs = listMirrorRefs(mirrorPath);
|
|
58
|
+
const beforeRefs = Object.keys(savedRefs).length ? savedRefs : mirrorRefs;
|
|
59
|
+
|
|
60
|
+
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity, terminal: false });
|
|
61
|
+
for await (const line of rl) {
|
|
62
|
+
const command = line.trim();
|
|
63
|
+
if (!command) break;
|
|
64
|
+
if (command === "capabilities") {
|
|
65
|
+
writeLine("connect");
|
|
66
|
+
writeLine("option");
|
|
67
|
+
writeLine("");
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (command.startsWith("option ")) {
|
|
71
|
+
const [, name] = command.split(/\s+/, 3);
|
|
72
|
+
if (name === "verbosity" || name === "progress") writeLine("ok");
|
|
73
|
+
else writeLine("unsupported");
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (command.startsWith("connect ")) {
|
|
77
|
+
await connectService(command.slice("connect ".length).trim(), mirrorPath, origin, beforeRefs, options);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
writeLine(`error unsupported command ${command.replace(/\s+/g, " ")}`);
|
|
81
|
+
writeLine("");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { parseHelperArgs, runRemoteHelper };
|
package/src/signing.cjs
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const os = require("node:os");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const crypto = require("node:crypto");
|
|
5
|
+
const { gitDirFromEnv, writeJsonFile } = require("./localMirror.cjs");
|
|
6
|
+
|
|
7
|
+
function safeName(value) {
|
|
8
|
+
return String(value || "default").replace(/[^A-Za-z0-9_.-]+/g, "_").slice(0, 120) || "default";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function homeProfilePath(profile) {
|
|
12
|
+
return path.join(os.homedir(), ".matterhorn", "git", "signing", `${safeName(profile)}.json`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function repoProfilePath(profile, options = {}) {
|
|
16
|
+
const gitDir = options.gitDir || gitDirFromEnv(options.cwd);
|
|
17
|
+
return path.join(gitDir, "matterhorn-sdk", "signing", `${safeName(profile)}.json`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function loadJson(file) {
|
|
21
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function defaultActor(profile) {
|
|
25
|
+
const username = process.env.USER || process.env.USERNAME || "member";
|
|
26
|
+
const memberId = profile && profile !== "default" ? profile : username;
|
|
27
|
+
return {
|
|
28
|
+
memberId,
|
|
29
|
+
deviceId: `git-${os.hostname().replace(/[^A-Za-z0-9_.-]+/g, "_")}`,
|
|
30
|
+
role: "member",
|
|
31
|
+
displayName: memberId
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function loadUserSigningProfile(profile = "default", options = {}) {
|
|
36
|
+
if (process.env.MATTERHORN_GIT_SIGNING_PROFILE_JSON) {
|
|
37
|
+
return normalizeProfile(JSON.parse(process.env.MATTERHORN_GIT_SIGNING_PROFILE_JSON), profile);
|
|
38
|
+
}
|
|
39
|
+
const explicit = options.file || process.env.MATTERHORN_GIT_SIGNING_PROFILE_FILE;
|
|
40
|
+
const candidates = [explicit, repoProfilePath(profile, options), homeProfilePath(profile)].filter(Boolean);
|
|
41
|
+
for (const file of candidates) {
|
|
42
|
+
if (fs.existsSync(file)) return normalizeProfile({ ...loadJson(file), file }, profile);
|
|
43
|
+
}
|
|
44
|
+
return normalizeProfile({ profile, actor: defaultActor(profile), devUnsigned: process.env.MATTERHORN_GIT_DEV_UNSIGNED === "1" }, profile);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeProfile(input = {}, fallbackProfile = "default") {
|
|
48
|
+
const profile = input.profile || input.name || fallbackProfile || "default";
|
|
49
|
+
const actor = input.actor || defaultActor(profile);
|
|
50
|
+
return {
|
|
51
|
+
profile,
|
|
52
|
+
actor: {
|
|
53
|
+
memberId: actor.memberId || profile,
|
|
54
|
+
deviceId: actor.deviceId || `git-${profile}`,
|
|
55
|
+
role: actor.role || "member",
|
|
56
|
+
displayName: actor.displayName || actor.name || actor.memberId || profile
|
|
57
|
+
},
|
|
58
|
+
auth: input.auth,
|
|
59
|
+
grant: input.grant,
|
|
60
|
+
privateKeyPem: input.privateKeyPem || input.privateKey,
|
|
61
|
+
file: input.file,
|
|
62
|
+
devUnsigned: input.devUnsigned === true
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createDevSigningProfile(profile, input = {}, options = {}) {
|
|
67
|
+
const actor = {
|
|
68
|
+
...defaultActor(profile),
|
|
69
|
+
memberId: input.memberId || input.member || profile,
|
|
70
|
+
role: input.role || "member",
|
|
71
|
+
displayName: input.displayName || input.name || input.memberId || profile
|
|
72
|
+
};
|
|
73
|
+
const secret = crypto.randomBytes(32).toString("base64url");
|
|
74
|
+
const data = {
|
|
75
|
+
version: 1,
|
|
76
|
+
profile,
|
|
77
|
+
actor,
|
|
78
|
+
auth: {
|
|
79
|
+
kind: "matterhorn.git-dev-signing.v1",
|
|
80
|
+
credentialId: `git-dev-${safeName(profile)}`,
|
|
81
|
+
secret
|
|
82
|
+
},
|
|
83
|
+
warning: "Development profile only. Production should use a Matterhorn operation role-key or device signing profile."
|
|
84
|
+
};
|
|
85
|
+
const file = options.repo ? repoProfilePath(profile, options) : homeProfilePath(profile);
|
|
86
|
+
writeJsonFile(file, data);
|
|
87
|
+
return { file, profile: normalizeProfile({ ...data, file }, profile) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function authForDraft(profile, draft) {
|
|
91
|
+
if (profile.auth?.kind === "matterhorn.git-dev-signing.v1" && profile.auth.secret) {
|
|
92
|
+
const payload = JSON.stringify({ type: draft.type, payload: draft.payload, actor: profile.actor, id: draft.id });
|
|
93
|
+
return {
|
|
94
|
+
kind: profile.auth.kind,
|
|
95
|
+
credentialId: profile.auth.credentialId,
|
|
96
|
+
signature: crypto.createHmac("sha256", profile.auth.secret).update(payload).digest("base64url")
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (profile.auth) return profile.auth;
|
|
100
|
+
if (profile.devUnsigned) return { kind: "matterhorn.git-dev-unsigned.v1", credentialId: `dev-unsigned-${profile.actor.memberId}`, unsigned: true };
|
|
101
|
+
throw new Error("No git signing profile configured. Run: matterhorn-git profile init <name>, or set MATTERHORN_GIT_DEV_UNSIGNED=1 for local development.");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
authForDraft,
|
|
106
|
+
createDevSigningProfile,
|
|
107
|
+
homeProfilePath,
|
|
108
|
+
loadUserSigningProfile,
|
|
109
|
+
repoProfilePath
|
|
110
|
+
};
|