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.
Files changed (71) hide show
  1. package/LICENSE +105 -0
  2. package/NOTICE +8 -0
  3. package/README.md +119 -0
  4. package/SECURITY.md +38 -0
  5. package/THIRD_PARTY_NOTICES.md +35 -0
  6. package/dist/bootstrap.d.ts +18 -0
  7. package/dist/bootstrap.js +104 -0
  8. package/dist/bootstrap.js.map +1 -0
  9. package/dist/cli.d.ts +7 -0
  10. package/dist/cli.js +96 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/coordination-api.d.ts +25 -0
  13. package/dist/coordination-api.js +457 -0
  14. package/dist/coordination-api.js.map +1 -0
  15. package/dist/credentials.d.ts +58 -0
  16. package/dist/credentials.js +244 -0
  17. package/dist/credentials.js.map +1 -0
  18. package/dist/enrollment.d.ts +17 -0
  19. package/dist/enrollment.js +122 -0
  20. package/dist/enrollment.js.map +1 -0
  21. package/dist/https-server.d.ts +94 -0
  22. package/dist/https-server.js +814 -0
  23. package/dist/https-server.js.map +1 -0
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.js +2 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/main.d.ts +3 -0
  28. package/dist/main.js +67 -0
  29. package/dist/main.js.map +1 -0
  30. package/dist/migrations.d.ts +8 -0
  31. package/dist/migrations.js +366 -0
  32. package/dist/migrations.js.map +1 -0
  33. package/dist/network-policy.d.ts +13 -0
  34. package/dist/network-policy.js +49 -0
  35. package/dist/network-policy.js.map +1 -0
  36. package/dist/operator-error.d.ts +3 -0
  37. package/dist/operator-error.js +63 -0
  38. package/dist/operator-error.js.map +1 -0
  39. package/dist/principal.d.ts +32 -0
  40. package/dist/principal.js +64 -0
  41. package/dist/principal.js.map +1 -0
  42. package/dist/protocol-draft.d.ts +2 -0
  43. package/dist/protocol-draft.js +31 -0
  44. package/dist/protocol-draft.js.map +1 -0
  45. package/dist/service.d.ts +61 -0
  46. package/dist/service.js +455 -0
  47. package/dist/service.js.map +1 -0
  48. package/dist/start-options.d.ts +2 -0
  49. package/dist/start-options.js +46 -0
  50. package/dist/start-options.js.map +1 -0
  51. package/dist/store.d.ts +327 -0
  52. package/dist/store.js +1729 -0
  53. package/dist/store.js.map +1 -0
  54. package/npm-shrinkwrap.json +1942 -0
  55. package/package.json +60 -0
  56. package/src/bootstrap.ts +127 -0
  57. package/src/cli.ts +102 -0
  58. package/src/coordination-api.ts +508 -0
  59. package/src/credentials.ts +319 -0
  60. package/src/enrollment.ts +156 -0
  61. package/src/https-server.ts +962 -0
  62. package/src/index.ts +3 -0
  63. package/src/main.ts +73 -0
  64. package/src/migrations.ts +394 -0
  65. package/src/network-policy.ts +65 -0
  66. package/src/operator-error.ts +97 -0
  67. package/src/principal.ts +106 -0
  68. package/src/protocol-draft.ts +32 -0
  69. package/src/service.ts +525 -0
  70. package/src/start-options.ts +46 -0
  71. 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
+ }
@@ -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
+ }