@log0u7/opencode-mesh 0.2.0 → 0.4.0
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/CHANGELOG.md +150 -0
- package/README.md +46 -13
- package/dist/lib/client.d.ts +2 -0
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/client.js +6 -1
- package/dist/lib/client.js.map +1 -1
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +26 -1
- package/dist/lib/db.js.map +1 -1
- package/dist/lib/discovery.d.ts +3 -0
- package/dist/lib/discovery.d.ts.map +1 -1
- package/dist/lib/discovery.js +33 -7
- package/dist/lib/discovery.js.map +1 -1
- package/dist/lib/identity.d.ts +10 -8
- package/dist/lib/identity.d.ts.map +1 -1
- package/dist/lib/identity.js +132 -32
- package/dist/lib/identity.js.map +1 -1
- package/dist/lib/locks.d.ts.map +1 -1
- package/dist/lib/locks.js +20 -12
- package/dist/lib/locks.js.map +1 -1
- package/dist/lib/peers.d.ts +13 -1
- package/dist/lib/peers.d.ts.map +1 -1
- package/dist/lib/peers.js +114 -18
- package/dist/lib/peers.js.map +1 -1
- package/dist/lib/serve.d.ts +2 -0
- package/dist/lib/serve.d.ts.map +1 -1
- package/dist/lib/serve.js +62 -71
- package/dist/lib/serve.js.map +1 -1
- package/dist/lib/spawn.d.ts +14 -4
- package/dist/lib/spawn.d.ts.map +1 -1
- package/dist/lib/spawn.js +210 -21
- package/dist/lib/spawn.js.map +1 -1
- package/dist/lib/sqlite.d.ts.map +1 -1
- package/dist/lib/sqlite.js +15 -4
- package/dist/lib/sqlite.js.map +1 -1
- package/dist/lib/workers.d.ts +2 -0
- package/dist/lib/workers.d.ts.map +1 -1
- package/dist/lib/workers.js +45 -12
- package/dist/lib/workers.js.map +1 -1
- package/dist/lib/worktree.d.ts +22 -4
- package/dist/lib/worktree.d.ts.map +1 -1
- package/dist/lib/worktree.js +54 -6
- package/dist/lib/worktree.js.map +1 -1
- package/dist/plugin-v2.d.ts.map +1 -1
- package/dist/plugin-v2.js +65 -337
- package/dist/plugin-v2.js.map +1 -1
- package/dist/plugin.d.ts +2 -6
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +119 -265
- package/dist/plugin.js.map +1 -1
- package/dist/tools.d.ts +30 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +315 -0
- package/dist/tools.js.map +1 -0
- package/dist/types/peer.d.ts +2 -0
- package/dist/types/peer.d.ts.map +1 -1
- package/package.json +10 -8
package/dist/lib/identity.js
CHANGED
|
@@ -1,58 +1,158 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomUUID, sign, verify, } from "node:crypto";
|
|
2
2
|
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { hostname as osHostname } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
5
|
export function loadOrCreateIdentity(dataDir) {
|
|
6
6
|
const file = join(dataDir, "node.json");
|
|
7
|
-
const existing =
|
|
7
|
+
const existing = readIdentity(file);
|
|
8
8
|
if (existing) {
|
|
9
|
-
|
|
9
|
+
// A file written by <= 0.2.0 carries either no keys or a dead Ed25519
|
|
10
|
+
// JWK keypair nothing ever used. The node keeps its node_id (peers stay
|
|
11
|
+
// recognised), and the file is rewritten to the canonical shape with a
|
|
12
|
+
// fresh signing keypair so pairing proofs can be produced.
|
|
13
|
+
if (existing.migrated)
|
|
14
|
+
writeIdentity(file, existing.identity);
|
|
15
|
+
return withFingerprint(existing.identity);
|
|
10
16
|
}
|
|
11
|
-
|
|
17
|
+
const identity = {
|
|
18
|
+
...freshKeys(),
|
|
19
|
+
node_id: randomUUID(),
|
|
20
|
+
hostname: hostName(),
|
|
21
|
+
};
|
|
22
|
+
writeIdentity(file, identity);
|
|
23
|
+
return withFingerprint(identity);
|
|
12
24
|
}
|
|
13
|
-
function
|
|
25
|
+
function readIdentity(file) {
|
|
26
|
+
let raw;
|
|
14
27
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
raw = readFileSync(file, "utf8");
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error.code === "ENOENT")
|
|
32
|
+
return null;
|
|
33
|
+
throw new Error(`cannot read identity file ${file}: ${errorMessage(error)}`);
|
|
34
|
+
}
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(raw);
|
|
20
38
|
}
|
|
21
39
|
catch {
|
|
22
|
-
|
|
40
|
+
throw corrupt(file, "is not valid JSON");
|
|
41
|
+
}
|
|
42
|
+
const record = (parsed ?? {});
|
|
43
|
+
if (typeof record.node_id !== "string" || record.node_id.length === 0) {
|
|
44
|
+
throw corrupt(file, "has no node_id");
|
|
45
|
+
}
|
|
46
|
+
const identity = {
|
|
47
|
+
node_id: record.node_id,
|
|
48
|
+
hostname: hostName(record.hostname),
|
|
49
|
+
public_key: typeof record.public_key === "string" ? record.public_key : "",
|
|
50
|
+
private_key: typeof record.private_key === "string" ? record.private_key : "",
|
|
51
|
+
};
|
|
52
|
+
const keysUsable = isSpki(identity.public_key) && isPkcs8(identity.private_key);
|
|
53
|
+
const migrated = !keysUsable || Object.keys(record).some((key) => !KNOWN_KEYS.has(key));
|
|
54
|
+
if (!keysUsable) {
|
|
55
|
+
// Replace dead or missing keys; the node_id above is never regenerated, so
|
|
56
|
+
// peers keep recognising this node through the migration.
|
|
57
|
+
Object.assign(identity, freshKeys());
|
|
23
58
|
}
|
|
59
|
+
return { identity, migrated };
|
|
60
|
+
}
|
|
61
|
+
const KNOWN_KEYS = new Set(["node_id", "hostname", "public_key", "private_key"]);
|
|
62
|
+
// A corrupt identity file would make the node silently mint a new id, breaking
|
|
63
|
+
// every peer that recognises it and orphaning queued mail. Refuse to start and
|
|
64
|
+
// hand the operator a recovery path instead of reminting behind their back.
|
|
65
|
+
function corrupt(file, why) {
|
|
66
|
+
return new Error(`identity file ${file} ${why}; refusing to mint a new node id. ` +
|
|
67
|
+
`Move it aside to start with a fresh identity: mv '${file}' '${file}.bak'`);
|
|
24
68
|
}
|
|
25
|
-
function
|
|
26
|
-
// Bun implements node:crypto, so this works under OpenCode's runtime and Node.
|
|
27
|
-
const { generateKeyPairSync } = require("node:crypto");
|
|
69
|
+
function freshKeys() {
|
|
28
70
|
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
node_id: randomUUID(),
|
|
33
|
-
hostname: hostName(),
|
|
34
|
-
public_key: publicJwk,
|
|
35
|
-
private_key: privateJwk,
|
|
71
|
+
return {
|
|
72
|
+
public_key: Buffer.from(publicKey.export({ type: "spki", format: "der" })).toString("base64"),
|
|
73
|
+
private_key: Buffer.from(privateKey.export({ type: "pkcs8", format: "der" })).toString("base64"),
|
|
36
74
|
};
|
|
37
|
-
|
|
38
|
-
|
|
75
|
+
}
|
|
76
|
+
function isSpki(value) {
|
|
77
|
+
try {
|
|
78
|
+
const key = createPublicKey({
|
|
79
|
+
key: Buffer.from(value, "base64"),
|
|
80
|
+
format: "der",
|
|
81
|
+
type: "spki",
|
|
82
|
+
});
|
|
83
|
+
// Only the algorithm we sign with: a hand-edited RSA key must not pass
|
|
84
|
+
// validation and then break `sign(null, ...)` at send time.
|
|
85
|
+
return key.asymmetricKeyType === "ed25519";
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function isPkcs8(value) {
|
|
92
|
+
try {
|
|
93
|
+
const key = createPrivateKey({
|
|
94
|
+
key: Buffer.from(value, "base64"),
|
|
95
|
+
format: "der",
|
|
96
|
+
type: "pkcs8",
|
|
97
|
+
});
|
|
98
|
+
return key.asymmetricKeyType === "ed25519";
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Pairing proofs: an Ed25519 signature binding the sender's node id to the
|
|
105
|
+
// token it presents. The canonical payload lives next to the signers so the
|
|
106
|
+
// sender and the verifier cannot drift.
|
|
107
|
+
export function pairingProofPayload(nodeId, token) {
|
|
108
|
+
return `opencode-mesh-pair:${nodeId}:${token}`;
|
|
109
|
+
}
|
|
110
|
+
export function signPairingProof(privateKey, payload) {
|
|
111
|
+
const key = createPrivateKey({
|
|
112
|
+
key: Buffer.from(privateKey, "base64"),
|
|
113
|
+
format: "der",
|
|
114
|
+
type: "pkcs8",
|
|
115
|
+
});
|
|
116
|
+
return Buffer.from(sign(null, Buffer.from(payload, "utf8"), key)).toString("base64");
|
|
117
|
+
}
|
|
118
|
+
// The wire form of a pairing proof: `<base64 spki>.<base64 signature>`.
|
|
119
|
+
export function pairingProof(identity, token) {
|
|
120
|
+
return `${identity.public_key}.${signPairingProof(identity.private_key, pairingProofPayload(identity.node_id, token))}`;
|
|
121
|
+
}
|
|
122
|
+
export function verifyPairingProof(publicKey, payload, proof) {
|
|
123
|
+
try {
|
|
124
|
+
const key = createPublicKey({
|
|
125
|
+
key: Buffer.from(publicKey, "base64"),
|
|
126
|
+
format: "der",
|
|
127
|
+
type: "spki",
|
|
128
|
+
});
|
|
129
|
+
return verify(null, Buffer.from(payload, "utf8"), key, Buffer.from(proof, "base64"));
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function writeIdentity(file, identity) {
|
|
136
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
137
|
+
// Atomic write: temp file + rename, so a crash never leaves a truncated record.
|
|
39
138
|
const tmp = `${file}.tmp-${process.pid}`;
|
|
40
139
|
writeFileSync(tmp, JSON.stringify(identity, null, 2), { mode: 0o600 });
|
|
41
140
|
renameSync(tmp, file);
|
|
42
141
|
chmodSync(file, 0o600);
|
|
43
|
-
return identity;
|
|
44
142
|
}
|
|
143
|
+
// The fingerprint stays derived from the node_id (not from the signing key):
|
|
144
|
+
// keys can be replaced on migration without shuffling the operator-facing
|
|
145
|
+
// label for `mesh_status`.
|
|
45
146
|
function withFingerprint(identity) {
|
|
46
|
-
const { createHash } = require("node:crypto");
|
|
47
|
-
// x is the raw public key coordinate in an Ed25519 JWK; hash it for a short
|
|
48
|
-
// operator-verifiable fingerprint.
|
|
49
|
-
const x = identity.public_key.x ?? "";
|
|
50
147
|
return {
|
|
51
148
|
...identity,
|
|
52
|
-
fingerprint: createHash("sha256").update(
|
|
149
|
+
fingerprint: createHash("sha256").update(identity.node_id).digest("hex").slice(0, 16),
|
|
53
150
|
};
|
|
54
151
|
}
|
|
55
|
-
function hostName() {
|
|
56
|
-
return osHostname();
|
|
152
|
+
function hostName(recorded) {
|
|
153
|
+
return typeof recorded === "string" && recorded.length > 0 ? recorded : osHostname();
|
|
154
|
+
}
|
|
155
|
+
function errorMessage(error) {
|
|
156
|
+
return error instanceof Error ? error.message : String(error);
|
|
57
157
|
}
|
|
58
158
|
//# sourceMappingURL=identity.js.map
|
package/dist/lib/identity.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/lib/identity.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/lib/identity.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/locks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locks.d.ts","sourceRoot":"","sources":["../../src/lib/locks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACtC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE7C,wBAAgB,OAAO,CACrB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC9F,OAAO,
|
|
1
|
+
{"version":3,"file":"locks.d.ts","sourceRoot":"","sources":["../../src/lib/locks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACtC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE7C,wBAAgB,OAAO,CACrB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC9F,OAAO,CAiCT;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAM5F;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,EAAE,CAGpE"}
|
package/dist/lib/locks.js
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
export function tryLock(db, input) {
|
|
2
2
|
const expiresAt = input.now + input.ttl_ms;
|
|
3
3
|
db.conn.run("DELETE FROM locks WHERE expires_at <= ?", [input.now]);
|
|
4
|
+
// Claim atomically: a second opencode process sharing this dataDir that races
|
|
5
|
+
// for the same path loses the insert (ON CONFLICT DO NOTHING) instead of
|
|
6
|
+
// throwing a UNIQUE constraint error out of mesh_lock.
|
|
7
|
+
db.conn.run("INSERT INTO locks (path, owner_node, owner_session, expires_at) VALUES (?, ?, ?, ?) ON CONFLICT(path) DO NOTHING", [input.path, input.owner_node, input.owner_session, expiresAt]);
|
|
8
|
+
const claimed = (db.conn.get("SELECT changes() AS changes")?.changes ?? 0) > 0;
|
|
9
|
+
if (claimed) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
4
12
|
const existing = db.conn.get("SELECT * FROM locks WHERE path = ?", [input.path]);
|
|
5
|
-
if (existing) {
|
|
6
|
-
|
|
7
|
-
// Same owner refreshes its claim.
|
|
8
|
-
db.conn.run("UPDATE locks SET expires_at = ?, owner_session = ? WHERE path = ?", [
|
|
9
|
-
expiresAt,
|
|
10
|
-
input.owner_session,
|
|
11
|
-
input.path,
|
|
12
|
-
]);
|
|
13
|
-
return true;
|
|
14
|
-
}
|
|
13
|
+
if (!existing) {
|
|
14
|
+
// Raced with a release between the insert and this read; not granted.
|
|
15
15
|
return false;
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
if (existing.owner_node === input.owner_node) {
|
|
18
|
+
// Same owner refreshes its claim.
|
|
19
|
+
db.conn.run("UPDATE locks SET expires_at = ?, owner_session = ? WHERE path = ?", [
|
|
20
|
+
expiresAt,
|
|
21
|
+
input.owner_session,
|
|
22
|
+
input.path,
|
|
23
|
+
]);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
19
27
|
}
|
|
20
28
|
export function releaseLock(db, input) {
|
|
21
29
|
db.conn.run("DELETE FROM locks WHERE path = ? AND owner_node = ?", [
|
package/dist/lib/locks.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locks.js","sourceRoot":"","sources":["../../src/lib/locks.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,OAAO,CACrB,EAAU,EACV,KAA+F;IAE/F,MAAM,SAAS,GAAG,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC;IAE3C,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,yCAAyC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAEpE,
|
|
1
|
+
{"version":3,"file":"locks.js","sourceRoot":"","sources":["../../src/lib/locks.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,OAAO,CACrB,EAAU,EACV,KAA+F;IAE/F,MAAM,SAAS,GAAG,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC;IAE3C,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,yCAAyC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAEpE,8EAA8E;IAC9E,yEAAyE;IACzE,uDAAuD;IACvD,EAAE,CAAC,IAAI,CAAC,GAAG,CACT,kHAAkH,EAClH,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,aAAa,EAAE,SAAS,CAAC,CAC/D,CAAC;IACF,MAAM,OAAO,GACX,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAsB,6BAA6B,CAAC,EAAE,OAAO,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IACtF,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,QAAQ,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAU,oCAAoC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IAC1F,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,sEAAsE;QACtE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,QAAQ,CAAC,UAAU,KAAK,KAAK,CAAC,UAAU,EAAE,CAAC;QAC7C,kCAAkC;QAClC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,mEAAmE,EAAE;YAC/E,SAAS;YACT,KAAK,CAAC,aAAa;YACnB,KAAK,CAAC,IAAI;SACX,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,EAAU,EAAE,KAA2C;IACjF,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,qDAAqD,EAAE;QACjE,KAAK,CAAC,IAAI;QACV,KAAK,CAAC,UAAU;KACjB,CAAC,CAAC;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAsB,6BAA6B,CAAC,EAAE,OAAO,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;AAC7F,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,EAAU,EAAE,KAAsB;IAC1D,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,yCAAyC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IACpE,OAAO,EAAE,CAAC,IAAI,CAAC,GAAG,CAAU,mCAAmC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;AAClF,CAAC;AASD,SAAS,SAAS,CAAC,GAAY;IAC7B,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,KAAK,EAAE,GAAG,CAAC,aAAa;QACxB,OAAO,EAAE,GAAG,CAAC,UAAU;QACvB,UAAU,EAAE,GAAG,CAAC,UAAU;KAC3B,CAAC;AACJ,CAAC"}
|
package/dist/lib/peers.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { MeshDb } from "./db.js";
|
|
2
2
|
import type { Peer } from "../types/peer.js";
|
|
3
|
+
export declare const PAIRING_TOKEN_TTL_MS: number;
|
|
3
4
|
export type PeerInput = {
|
|
4
5
|
node_id: string;
|
|
5
6
|
hostname: string;
|
|
6
7
|
fingerprint: string;
|
|
7
8
|
addresses?: string[];
|
|
9
|
+
public_key?: string;
|
|
8
10
|
};
|
|
9
11
|
export declare function pairWithToken(db: MeshDb, peer: {
|
|
10
12
|
node_id: string;
|
|
@@ -12,15 +14,25 @@ export declare function pairWithToken(db: MeshDb, peer: {
|
|
|
12
14
|
fingerprint: string;
|
|
13
15
|
token: string;
|
|
14
16
|
addresses?: string[];
|
|
17
|
+
public_key?: string;
|
|
15
18
|
}): void;
|
|
16
19
|
export declare function createPairingToken(db: MeshDb): string;
|
|
17
|
-
export declare function authenticateToken(db: MeshDb, token: string, from: string | null): Peer | null;
|
|
20
|
+
export declare function authenticateToken(db: MeshDb, token: string, from: string | null, proof?: string): Peer | null;
|
|
21
|
+
export declare function prunePendingTokens(db: MeshDb): void;
|
|
18
22
|
export declare function listPeers(db: MeshDb): Peer[];
|
|
23
|
+
export declare function findPeerById(db: MeshDb, nodeId: string): Peer | null;
|
|
19
24
|
export declare function findPeerByToken(db: MeshDb, token: string): Peer | null;
|
|
25
|
+
export declare function findSendablePeer(db: MeshDb, query: string): {
|
|
26
|
+
node_id: string;
|
|
27
|
+
addresses: string[];
|
|
28
|
+
token: string | null;
|
|
29
|
+
} | null;
|
|
30
|
+
export declare function resolvePeerUrl(db: MeshDb, query: string): string | null;
|
|
20
31
|
export declare function upsertDiscoveredPeer(db: MeshDb, peer: {
|
|
21
32
|
node_id: string;
|
|
22
33
|
hostname: string;
|
|
23
34
|
fingerprint: string;
|
|
24
35
|
addresses: string[];
|
|
36
|
+
public_key?: string;
|
|
25
37
|
}): void;
|
|
26
38
|
//# sourceMappingURL=peers.d.ts.map
|
package/dist/lib/peers.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"peers.d.ts","sourceRoot":"","sources":["../../src/lib/peers.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"peers.d.ts","sourceRoot":"","sources":["../../src/lib/peers.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEtC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAI7C,eAAO,MAAM,oBAAoB,QAAa,CAAC;AAE/C,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAMF,wBAAgB,aAAa,CAC3B,EAAE,EAAE,MAAM,EACV,IAAI,EAAE;IACJ,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GACA,IAAI,CAoBN;AAID,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAOrD;AAMD,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,GAAG,IAAI,EACnB,KAAK,CAAC,EAAE,MAAM,GACb,IAAI,GAAG,IAAI,CA0Cb;AAyCD,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAInD;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,EAAE,CAE5C;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAGpE;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAGtE;AAGD,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,GACZ;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,CAGvE;AAKD,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQvE;AAQD,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,MAAM,EACV,IAAI,EAAE;IACJ,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GACA,IAAI,CAkBN"}
|
package/dist/lib/peers.js
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { pairingProofPayload, verifyPairingProof } from "./identity.js";
|
|
3
|
+
// A pairing offer is only useful while the operator can relay it out of band.
|
|
4
|
+
// Past this age the token is dropped instead of becoming a permanent credential.
|
|
5
|
+
export const PAIRING_TOKEN_TTL_MS = 5 * 60_000;
|
|
2
6
|
// Complete pairing with a token the peer generated (shared out of band).
|
|
3
7
|
// Only the pairing call and the peer know it; the server compares on every request.
|
|
8
|
+
// The pinned public_key is preserved when the call provides none (manual pairing):
|
|
9
|
+
// pairing must never be able to erase a proof key it did not attest.
|
|
4
10
|
export function pairWithToken(db, peer) {
|
|
5
11
|
const now = Date.now();
|
|
6
|
-
db.conn.run(`INSERT INTO peers (node_id, hostname, fingerprint, addresses, paired, token, last_seen)
|
|
7
|
-
VALUES (?, ?, ?, ?, 1, ?, ?)
|
|
12
|
+
db.conn.run(`INSERT INTO peers (node_id, hostname, fingerprint, addresses, paired, token, public_key, last_seen)
|
|
13
|
+
VALUES (?, ?, ?, ?, 1, ?, ?, ?)
|
|
8
14
|
ON CONFLICT(node_id) DO UPDATE SET hostname = excluded.hostname,
|
|
9
|
-
fingerprint = excluded.fingerprint, paired = 1, token = excluded.token,
|
|
15
|
+
fingerprint = excluded.fingerprint, paired = 1, token = excluded.token,
|
|
16
|
+
public_key = COALESCE(excluded.public_key, peers.public_key), last_seen = excluded.last_seen`, [
|
|
10
17
|
peer.node_id,
|
|
11
18
|
peer.hostname,
|
|
12
19
|
peer.fingerprint,
|
|
13
20
|
JSON.stringify(peer.addresses ?? []),
|
|
14
21
|
peer.token,
|
|
22
|
+
peer.public_key ?? null,
|
|
15
23
|
now,
|
|
16
24
|
]);
|
|
17
25
|
db.conn.run("DELETE FROM pending_tokens WHERE token = ?", [peer.token]);
|
|
@@ -26,13 +34,16 @@ export function createPairingToken(db) {
|
|
|
26
34
|
]);
|
|
27
35
|
return token;
|
|
28
36
|
}
|
|
29
|
-
// Check a bearer token: known paired peer, or a pending pairing token
|
|
30
|
-
//
|
|
31
|
-
|
|
37
|
+
// Check a bearer token: known paired peer, or a live pending pairing token.
|
|
38
|
+
// The pending path requires the claimed sender to be a node this host already
|
|
39
|
+
// knows, and (the rebind-window fix) a proof signed by the sender's pinned
|
|
40
|
+
// key: a leaked offer alone cannot rotate a paired credential.
|
|
41
|
+
export function authenticateToken(db, token, from, proof) {
|
|
32
42
|
const paired = findPeerByToken(db, token);
|
|
33
43
|
if (paired) {
|
|
34
44
|
return paired;
|
|
35
45
|
}
|
|
46
|
+
prunePendingTokens(db);
|
|
36
47
|
const pending = db.conn.get("SELECT token FROM pending_tokens WHERE token = ?", [token]);
|
|
37
48
|
if (!pending) {
|
|
38
49
|
return null;
|
|
@@ -41,30 +52,114 @@ export function authenticateToken(db, token, from) {
|
|
|
41
52
|
if (from === null || from.length === 0) {
|
|
42
53
|
return null;
|
|
43
54
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
const known = findPeerById(db, from);
|
|
56
|
+
if (!known) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
// Proof of possession: the sender signs opencode-mesh-pair:<node_id>:<token>
|
|
60
|
+
// with the key pinned for it (first bind: the proof carries its own key).
|
|
61
|
+
const attested = attestedProof(known, token, proof);
|
|
62
|
+
if (attested === null) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
pairWithToken(db, {
|
|
66
|
+
node_id: known.node_id,
|
|
67
|
+
hostname: known.hostname,
|
|
68
|
+
fingerprint: known.fingerprint,
|
|
69
|
+
addresses: known.addresses,
|
|
70
|
+
public_key: attested,
|
|
51
71
|
token,
|
|
52
|
-
|
|
53
|
-
|
|
72
|
+
});
|
|
73
|
+
return known;
|
|
74
|
+
}
|
|
75
|
+
// Returns the public key to pin for this binding, or null when the proof does
|
|
76
|
+
// not attest the sender. Rules:
|
|
77
|
+
// - wire form is exactly `<base64 spki>.<base64 sig>`: no extra dots (base64
|
|
78
|
+
// decoders skip '.', so a trailing dot would otherwise verify);
|
|
79
|
+
// - unpaired known node: the proof's key is pinned once its signature checks;
|
|
80
|
+
// - paired node with a pinned key: the proof key must BE the pinned key;
|
|
81
|
+
// - paired node without a pinned key (legacy/manual rows): no remote attested
|
|
82
|
+
// rebind at all. Rotation goes through the operator (re-run mesh_pair).
|
|
83
|
+
function attestedProof(known, token, proof) {
|
|
84
|
+
if (proof === undefined || proof.length === 0) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const separator = proof.indexOf(".");
|
|
88
|
+
if (separator <= 0 || separator === proof.length - 1) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const presentedKey = proof.slice(0, separator);
|
|
92
|
+
const signature = proof.slice(separator + 1);
|
|
93
|
+
if (signature.includes(".")) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const payload = pairingProofPayload(known.node_id, token);
|
|
97
|
+
if (known.paired) {
|
|
98
|
+
// Rotation of a live credential: the pinned key is the only authority.
|
|
99
|
+
if (known.public_key === null || presentedKey !== known.public_key) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else if (known.public_key !== null && presentedKey !== known.public_key) {
|
|
104
|
+
// First bind, but discovery already pinned a different key: spoof guard.
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return verifyPairingProof(presentedKey, payload, signature) ? presentedKey : null;
|
|
108
|
+
}
|
|
109
|
+
// Pairing offers are single-shot credentials with a short life: bulk delete on
|
|
110
|
+
// every check keeps the table bounded without a background timer.
|
|
111
|
+
export function prunePendingTokens(db) {
|
|
112
|
+
db.conn.run("DELETE FROM pending_tokens WHERE created_at <= ?", [
|
|
113
|
+
Date.now() - PAIRING_TOKEN_TTL_MS,
|
|
114
|
+
]);
|
|
54
115
|
}
|
|
55
116
|
export function listPeers(db) {
|
|
56
117
|
return db.conn.all("SELECT * FROM peers ORDER BY hostname").map(rowToPeer);
|
|
57
118
|
}
|
|
119
|
+
export function findPeerById(db, nodeId) {
|
|
120
|
+
const row = db.conn.get("SELECT * FROM peers WHERE node_id = ?", [nodeId]);
|
|
121
|
+
return row ? rowToPeer(row) : null;
|
|
122
|
+
}
|
|
58
123
|
export function findPeerByToken(db, token) {
|
|
59
124
|
const row = db.conn.get("SELECT * FROM peers WHERE token = ? AND paired = 1", [token]);
|
|
60
125
|
return row ? rowToPeer(row) : null;
|
|
61
126
|
}
|
|
127
|
+
// Paired peers only: discovery alone never makes a peer sendable.
|
|
128
|
+
export function findSendablePeer(db, query) {
|
|
129
|
+
const peers = listPeers(db).filter((p) => p.paired);
|
|
130
|
+
return peers.find((p) => p.node_id === query) ?? peers.find((p) => p.hostname === query) ?? null;
|
|
131
|
+
}
|
|
132
|
+
// Peer addresses must carry a port: discovery attaches the advertised port and
|
|
133
|
+
// mesh_pair transmits a full host:port. Returning null instead of guessing the
|
|
134
|
+
// local port keeps a stale address from silently reaching the wrong socket.
|
|
135
|
+
export function resolvePeerUrl(db, query) {
|
|
136
|
+
const peers = listPeers(db).filter((p) => p.paired);
|
|
137
|
+
const peer = peers.find((p) => p.node_id === query) ?? peers.find((p) => p.hostname === query) ?? null;
|
|
138
|
+
// A bare IPv6 literal also ends in ":<digits>", so the bracketed form is the
|
|
139
|
+
// only one that can be told apart unambiguously.
|
|
140
|
+
const address = peer?.addresses.find(hasPort);
|
|
141
|
+
return address !== undefined ? `http://${address}` : null;
|
|
142
|
+
}
|
|
143
|
+
function hasPort(address) {
|
|
144
|
+
// Bracketed IPv6, with an optional zone id (`[fe80::1%eth0]:4399`) and the
|
|
145
|
+
// v4-mapped dotted form (`[::ffff:192.168.1.10]:4399`).
|
|
146
|
+
return /^\[[0-9a-f:.]+(?:%[^\]]+)?\]:\d+$/i.test(address) || /^[^:]+:\d+$/.test(address);
|
|
147
|
+
}
|
|
62
148
|
export function upsertDiscoveredPeer(db, peer) {
|
|
63
149
|
const now = Date.now();
|
|
64
|
-
db.conn.run(`INSERT INTO peers (node_id, hostname, fingerprint, addresses, paired, token, last_seen)
|
|
65
|
-
VALUES (?, ?, ?, ?, 0, NULL, ?)
|
|
150
|
+
db.conn.run(`INSERT INTO peers (node_id, hostname, fingerprint, addresses, paired, token, public_key, last_seen)
|
|
151
|
+
VALUES (?, ?, ?, ?, 0, NULL, ?, ?)
|
|
66
152
|
ON CONFLICT(node_id) DO UPDATE SET hostname = excluded.hostname,
|
|
67
|
-
fingerprint = excluded.fingerprint, addresses = excluded.addresses,
|
|
153
|
+
fingerprint = excluded.fingerprint, addresses = excluded.addresses,
|
|
154
|
+
public_key = CASE WHEN peers.paired = 1 THEN peers.public_key ELSE excluded.public_key END,
|
|
155
|
+
last_seen = excluded.last_seen`, [
|
|
156
|
+
peer.node_id,
|
|
157
|
+
peer.hostname,
|
|
158
|
+
peer.fingerprint,
|
|
159
|
+
JSON.stringify(peer.addresses),
|
|
160
|
+
peer.public_key ?? null,
|
|
161
|
+
now,
|
|
162
|
+
]);
|
|
68
163
|
}
|
|
69
164
|
function rowToPeer(row) {
|
|
70
165
|
return {
|
|
@@ -74,6 +169,7 @@ function rowToPeer(row) {
|
|
|
74
169
|
addresses: parseAddresses(row.addresses),
|
|
75
170
|
paired: row.paired === 1,
|
|
76
171
|
token: row.token,
|
|
172
|
+
public_key: row.public_key,
|
|
77
173
|
last_seen: row.last_seen,
|
|
78
174
|
};
|
|
79
175
|
}
|
package/dist/lib/peers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"peers.js","sourceRoot":"","sources":["../../src/lib/peers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"peers.js","sourceRoot":"","sources":["../../src/lib/peers.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/serve.d.ts
CHANGED
|
@@ -3,10 +3,12 @@ import type { MeshDb } from "./db.js";
|
|
|
3
3
|
export type MeshServerOptions = {
|
|
4
4
|
db: MeshDb;
|
|
5
5
|
port: number;
|
|
6
|
+
bind?: "loopback" | "lan";
|
|
6
7
|
};
|
|
7
8
|
export type MeshServer = {
|
|
8
9
|
server: Server;
|
|
9
10
|
port: number;
|
|
11
|
+
address: string;
|
|
10
12
|
};
|
|
11
13
|
export declare function startMeshServer(options: MeshServerOptions): Promise<MeshServer>;
|
|
12
14
|
//# sourceMappingURL=serve.d.ts.map
|
package/dist/lib/serve.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/lib/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,MAAM,EAAkB,MAAM,WAAW,CAAC;AAGzE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/lib/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,MAAM,EAAkB,MAAM,WAAW,CAAC;AAGzE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAUtC,MAAM,MAAM,iBAAiB,GAAG;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,UAAU,GAAG,KAAK,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAIF,wBAAsB,eAAe,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,CA6BrF"}
|