borgmcp-server 0.1.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.
- package/LICENSE +105 -0
- package/NOTICE +8 -0
- package/README.md +119 -0
- package/SECURITY.md +38 -0
- package/THIRD_PARTY_NOTICES.md +35 -0
- package/dist/bootstrap.d.ts +18 -0
- package/dist/bootstrap.js +104 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +96 -0
- package/dist/cli.js.map +1 -0
- package/dist/coordination-api.d.ts +25 -0
- package/dist/coordination-api.js +457 -0
- package/dist/coordination-api.js.map +1 -0
- package/dist/credentials.d.ts +58 -0
- package/dist/credentials.js +244 -0
- package/dist/credentials.js.map +1 -0
- package/dist/enrollment.d.ts +17 -0
- package/dist/enrollment.js +122 -0
- package/dist/enrollment.js.map +1 -0
- package/dist/https-server.d.ts +94 -0
- package/dist/https-server.js +814 -0
- package/dist/https-server.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.js +67 -0
- package/dist/main.js.map +1 -0
- package/dist/migrations.d.ts +8 -0
- package/dist/migrations.js +366 -0
- package/dist/migrations.js.map +1 -0
- package/dist/network-policy.d.ts +13 -0
- package/dist/network-policy.js +49 -0
- package/dist/network-policy.js.map +1 -0
- package/dist/operator-error.d.ts +3 -0
- package/dist/operator-error.js +63 -0
- package/dist/operator-error.js.map +1 -0
- package/dist/principal.d.ts +32 -0
- package/dist/principal.js +64 -0
- package/dist/principal.js.map +1 -0
- package/dist/protocol-draft.d.ts +2 -0
- package/dist/protocol-draft.js +31 -0
- package/dist/protocol-draft.js.map +1 -0
- package/dist/service.d.ts +61 -0
- package/dist/service.js +455 -0
- package/dist/service.js.map +1 -0
- package/dist/start-options.d.ts +2 -0
- package/dist/start-options.js +46 -0
- package/dist/start-options.js.map +1 -0
- package/dist/store.d.ts +327 -0
- package/dist/store.js +1729 -0
- package/dist/store.js.map +1 -0
- package/npm-shrinkwrap.json +1942 -0
- package/package.json +60 -0
- package/src/bootstrap.ts +127 -0
- package/src/cli.ts +102 -0
- package/src/coordination-api.ts +508 -0
- package/src/credentials.ts +319 -0
- package/src/enrollment.ts +156 -0
- package/src/https-server.ts +962 -0
- package/src/index.ts +3 -0
- package/src/main.ts +73 -0
- package/src/migrations.ts +394 -0
- package/src/network-policy.ts +65 -0
- package/src/operator-error.ts +97 -0
- package/src/principal.ts +106 -0
- package/src/protocol-draft.ts +32 -0
- package/src/service.ts +525 -0
- package/src/start-options.ts +46 -0
- package/src/store.ts +2316 -0
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "borgmcp-server",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Self-hosted Borg coordination server",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Byte-Ventures/borg-mcp-server.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Byte-Ventures/borg-mcp-server/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/Byte-Ventures/borg-mcp-server#readme",
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=22.12.0",
|
|
21
|
+
"npm": ">=10.0.0"
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"borg-mcp-server": "./dist/main.js"
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"src",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"NOTICE",
|
|
37
|
+
"README.md",
|
|
38
|
+
"SECURITY.md",
|
|
39
|
+
"THIRD_PARTY_NOTICES.md",
|
|
40
|
+
"npm-shrinkwrap.json"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -p tsconfig.build.json",
|
|
44
|
+
"check": "npm run build && npm run typecheck && npm test",
|
|
45
|
+
"pretest": "npm run build",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:watch": "vitest",
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"verify:artifact": "node scripts/verify-packed-artifact.mjs"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "22.20.1",
|
|
53
|
+
"borgmcp-shared": "0.3.0",
|
|
54
|
+
"typescript": "7.0.2",
|
|
55
|
+
"vitest": "4.1.10"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"selfsigned": "5.5.0"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/bootstrap.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID, X509Certificate } from "node:crypto";
|
|
2
|
+
import { lstat, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { generate } from "selfsigned";
|
|
5
|
+
|
|
6
|
+
import { CredentialAuthority, CredentialDigester } from "./credentials.js";
|
|
7
|
+
import { openStore } from "./store.js";
|
|
8
|
+
|
|
9
|
+
export interface BootstrapResult {
|
|
10
|
+
readonly serverId: string;
|
|
11
|
+
readonly caFingerprint: string;
|
|
12
|
+
readonly recoveryCredential: string;
|
|
13
|
+
readonly initialInvitation: string;
|
|
14
|
+
readonly paths: {
|
|
15
|
+
readonly database: string;
|
|
16
|
+
readonly digestKey: string;
|
|
17
|
+
readonly caKey: string;
|
|
18
|
+
readonly caCertificate: string;
|
|
19
|
+
readonly serverKey: string;
|
|
20
|
+
readonly serverCertificate: string;
|
|
21
|
+
readonly config: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function bootstrapServer(
|
|
26
|
+
dataDirectory: string,
|
|
27
|
+
bindHost = "127.0.0.1",
|
|
28
|
+
clock: () => Date = () => new Date(),
|
|
29
|
+
): Promise<BootstrapResult> {
|
|
30
|
+
const directory = resolve(dataDirectory);
|
|
31
|
+
const paths = {
|
|
32
|
+
database: join(directory, "borg.db"),
|
|
33
|
+
digestKey: join(directory, "credential-digest.key"),
|
|
34
|
+
caKey: join(directory, "ca.key"),
|
|
35
|
+
caCertificate: join(directory, "ca.crt"),
|
|
36
|
+
serverKey: join(directory, "server.key"),
|
|
37
|
+
serverCertificate: join(directory, "server.crt"),
|
|
38
|
+
config: join(directory, "server.json"),
|
|
39
|
+
};
|
|
40
|
+
const ca = await generate([{ name: "commonName", value: "Borg Local CA" }], {
|
|
41
|
+
algorithm: "sha256",
|
|
42
|
+
keyType: "ec",
|
|
43
|
+
extensions: [
|
|
44
|
+
{ name: "basicConstraints", cA: true, pathLenConstraint: 0, critical: true },
|
|
45
|
+
{ name: "keyUsage", keyCertSign: true, cRLSign: true, critical: true },
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
const server = await generate([{ name: "commonName", value: "Borg Local Server" }], {
|
|
49
|
+
algorithm: "sha256",
|
|
50
|
+
keyType: "ec",
|
|
51
|
+
ca: { key: ca.private, cert: ca.cert },
|
|
52
|
+
extensions: [
|
|
53
|
+
{ name: "basicConstraints", cA: false, critical: true },
|
|
54
|
+
{ name: "keyUsage", digitalSignature: true, keyAgreement: true, critical: true },
|
|
55
|
+
{ name: "extKeyUsage", serverAuth: true },
|
|
56
|
+
{ name: "subjectAltName", altNames: [{ type: 7, ip: bindHost }] },
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
const serverId = randomUUID();
|
|
60
|
+
const caCertificate = new X509Certificate(ca.cert);
|
|
61
|
+
const caFingerprint = createHash("sha256")
|
|
62
|
+
.update(caCertificate.publicKey.export({ type: "spki", format: "der" }))
|
|
63
|
+
.digest("hex");
|
|
64
|
+
const digestKey = randomBytes(32);
|
|
65
|
+
const runtime = await openStore({ path: paths.database, clock });
|
|
66
|
+
try {
|
|
67
|
+
await Promise.all([
|
|
68
|
+
writePrivate(paths.digestKey, digestKey),
|
|
69
|
+
writePrivate(paths.caKey, ca.private),
|
|
70
|
+
writePrivate(paths.caCertificate, ca.cert),
|
|
71
|
+
writePrivate(paths.serverKey, server.private),
|
|
72
|
+
writePrivate(paths.serverCertificate, server.cert),
|
|
73
|
+
writePrivate(paths.config, JSON.stringify({
|
|
74
|
+
server_id: serverId,
|
|
75
|
+
ca_spki_sha256: caFingerprint,
|
|
76
|
+
bind_host: bindHost,
|
|
77
|
+
}, null, 2)),
|
|
78
|
+
]);
|
|
79
|
+
const digester = new CredentialDigester(digestKey);
|
|
80
|
+
digestKey.fill(0);
|
|
81
|
+
try {
|
|
82
|
+
const authority = new CredentialAuthority(runtime.credentials, digester, clock);
|
|
83
|
+
const recoveryCredential = authority.createRecoveryCredential();
|
|
84
|
+
const initialInvitation = authority.createBootstrapInvitation(15 * 60_000);
|
|
85
|
+
return {
|
|
86
|
+
serverId,
|
|
87
|
+
caFingerprint,
|
|
88
|
+
recoveryCredential,
|
|
89
|
+
initialInvitation,
|
|
90
|
+
paths,
|
|
91
|
+
};
|
|
92
|
+
} finally {
|
|
93
|
+
digester.destroy();
|
|
94
|
+
}
|
|
95
|
+
} finally {
|
|
96
|
+
digestKey.fill(0);
|
|
97
|
+
runtime.close();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function writePrivate(path: string, value: string | Buffer): Promise<void> {
|
|
102
|
+
await writeFile(path, value, { flag: "wx", mode: 0o600 });
|
|
103
|
+
if (((await stat(path)).mode & 0o777) !== 0o600) {
|
|
104
|
+
throw new Error("Bootstrap file permissions are not private.");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function loadDigestKey(path: string): Promise<Buffer> {
|
|
109
|
+
const key = await loadPrivateFile(path, "Credential digest key");
|
|
110
|
+
if (key.length !== 32) {
|
|
111
|
+
key.fill(0);
|
|
112
|
+
throw new Error("Credential digest key is invalid or not private.");
|
|
113
|
+
}
|
|
114
|
+
return key;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function loadTlsPrivateKey(path: string): Promise<Buffer> {
|
|
118
|
+
return loadPrivateFile(path, "TLS private key");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function loadPrivateFile(path: string, label: string): Promise<Buffer> {
|
|
122
|
+
const metadata = await lstat(path);
|
|
123
|
+
if (!metadata.isFile() || (metadata.mode & 0o077) !== 0) {
|
|
124
|
+
throw new Error(`${label} is invalid or not private.`);
|
|
125
|
+
}
|
|
126
|
+
return readFile(path);
|
|
127
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { ServerService } from "./service.js";
|
|
2
|
+
|
|
3
|
+
export interface CliIo {
|
|
4
|
+
readonly stdout: (message: string) => void;
|
|
5
|
+
readonly stderr: (message: string) => void;
|
|
6
|
+
readonly readSecret?: (prompt: string) => Promise<string>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const usage = `Usage: borg-mcp-server <command> [options]
|
|
10
|
+
|
|
11
|
+
Commands:
|
|
12
|
+
setup Prepare an offline server installation
|
|
13
|
+
start Start the server process
|
|
14
|
+
client-rotate <client-id> Rotate one client credential offline
|
|
15
|
+
client-revoke <client-id> Revoke one client and its credentials offline
|
|
16
|
+
client-grant <client-id> <cube-id> <read|write|manage> Set one offline cube grant
|
|
17
|
+
client-ungrant <client-id> <cube-id> Remove one offline cube grant
|
|
18
|
+
client-invite Create one ordinary client invitation using a private recovery prompt
|
|
19
|
+
owner-invite Replace the unclaimed owner invitation using a private recovery prompt
|
|
20
|
+
help Show this help
|
|
21
|
+
|
|
22
|
+
Start options:
|
|
23
|
+
--host <ip> Explicit bind address (default: 127.0.0.1)
|
|
24
|
+
--port <number> Listen port (default: 7091)
|
|
25
|
+
--lan Consent to this start on a private LAN address
|
|
26
|
+
|
|
27
|
+
TLS files:
|
|
28
|
+
BORG_SERVER_DATA_DIR (default: ~/.borg/server), or explicit
|
|
29
|
+
BORG_SERVER_TLS_KEY_FILE, BORG_SERVER_TLS_CERT_FILE, and BORG_SERVER_TLS_CA_FILE
|
|
30
|
+
|
|
31
|
+
Stop the server before running offline client administration commands.`;
|
|
32
|
+
|
|
33
|
+
export async function runCli(
|
|
34
|
+
args: readonly string[],
|
|
35
|
+
service: ServerService,
|
|
36
|
+
io: CliIo,
|
|
37
|
+
): Promise<number> {
|
|
38
|
+
const [command, ...extraArgs] = args;
|
|
39
|
+
|
|
40
|
+
switch (command) {
|
|
41
|
+
case "setup":
|
|
42
|
+
if (extraArgs.length !== 0) return invalidArguments(io);
|
|
43
|
+
if (service.setup === undefined) {
|
|
44
|
+
io.stderr("Server setup is unavailable.");
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
const result = await service.setup();
|
|
48
|
+
io.stdout(`Server setup complete.\nRecovery credential (shown once): ${result.recoveryCredential}\nInitial enrollment invitation (shown once): ${result.initialInvitation}`);
|
|
49
|
+
return 0;
|
|
50
|
+
case "start":
|
|
51
|
+
await service.start(extraArgs);
|
|
52
|
+
return 0;
|
|
53
|
+
case "client-rotate":
|
|
54
|
+
if (extraArgs.length !== 1 || service.rotateClient === undefined) return invalidArguments(io);
|
|
55
|
+
io.stdout(`Client credential rotated (shown once): ${await service.rotateClient(extraArgs[0]!)}`);
|
|
56
|
+
return 0;
|
|
57
|
+
case "client-revoke":
|
|
58
|
+
if (extraArgs.length !== 1 || service.revokeClient === undefined) return invalidArguments(io);
|
|
59
|
+
await service.revokeClient(extraArgs[0]!);
|
|
60
|
+
io.stdout("Client revoked.");
|
|
61
|
+
return 0;
|
|
62
|
+
case "client-grant": {
|
|
63
|
+
if (extraArgs.length !== 3 || service.grantClient === undefined) return invalidArguments(io);
|
|
64
|
+
const access = extraArgs[2];
|
|
65
|
+
if (access !== "read" && access !== "write" && access !== "manage") return invalidArguments(io);
|
|
66
|
+
await service.grantClient(extraArgs[0]!, extraArgs[1]!, access);
|
|
67
|
+
io.stdout("Client cube grant updated.");
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
case "client-ungrant":
|
|
71
|
+
if (extraArgs.length !== 2 || service.ungrantClient === undefined) return invalidArguments(io);
|
|
72
|
+
await service.ungrantClient(extraArgs[0]!, extraArgs[1]!);
|
|
73
|
+
io.stdout("Client cube grant removed.");
|
|
74
|
+
return 0;
|
|
75
|
+
case "client-invite":
|
|
76
|
+
case "owner-invite": {
|
|
77
|
+
if (extraArgs.length !== 0 || io.readSecret === undefined) return invalidArguments(io);
|
|
78
|
+
const operation = command === "client-invite"
|
|
79
|
+
? service.createClientInvitation
|
|
80
|
+
: service.replaceOwnerInvitation;
|
|
81
|
+
if (operation === undefined) return invalidArguments(io);
|
|
82
|
+
const recovery = await io.readSecret("Recovery credential: ");
|
|
83
|
+
const invitation = await operation(recovery);
|
|
84
|
+
io.stdout(`Enrollment invitation (shown once): ${invitation}`);
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
case "help":
|
|
88
|
+
case "--help":
|
|
89
|
+
case "-h":
|
|
90
|
+
case undefined:
|
|
91
|
+
io.stdout(usage);
|
|
92
|
+
return 0;
|
|
93
|
+
default:
|
|
94
|
+
io.stderr("Unknown command.");
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function invalidArguments(io: CliIo): 1 {
|
|
100
|
+
io.stderr("Invalid command arguments.");
|
|
101
|
+
return 1;
|
|
102
|
+
}
|