Compare commits
5
Commits
feat/one-bus
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10e8191717 | ||
|
|
d703cebff4 | ||
|
|
46b56d53a6 | ||
|
|
d4a2802342 | ||
|
|
8acfa7a07d |
+16
-9
@@ -19,7 +19,7 @@
|
|||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import net from "node:net";
|
import net from "node:net";
|
||||||
import tls from "node:tls";
|
import tls from "node:tls";
|
||||||
import { connect as natsConnect, headers as natsHeaders, StringCodec, type JsMsg, type Subscription } from "nats";
|
import { connect as natsConnect, headers as natsHeaders, StringCodec, type JsMsg, type Subscription, type TlsOptions } from "nats";
|
||||||
import type { Broker, Envelope, EventHeaders } from "@novox/mesh-sdk/messaging";
|
import type { Broker, Envelope, EventHeaders } from "@novox/mesh-sdk/messaging";
|
||||||
|
|
||||||
const sc = StringCodec();
|
const sc = StringCodec();
|
||||||
@@ -85,6 +85,10 @@ export async function connectNats(
|
|||||||
pass: cred.password,
|
pass: cred.password,
|
||||||
name: `${cred.node ?? "?"}.${self}`,
|
name: `${cred.node ?? "?"}.${self}`,
|
||||||
tls: cred.fingerprint ? await pinnedTls(cred.url, cred.fingerprint) : undefined,
|
tls: cred.fingerprint ? await pinnedTls(cred.url, cred.fingerprint) : undefined,
|
||||||
|
// Its own inbox, not a random one: every user's inbox is private to it (design 25 §4), and the
|
||||||
|
// grant names `_INBOX.<user>.>` — a reply space the client invented would be refused, and with
|
||||||
|
// it every pull for the next message and every answer to a tool call.
|
||||||
|
inboxPrefix: cred.user ? `_INBOX.${cred.user}` : undefined,
|
||||||
// Reconnect forever: the bus being restarted is an upgrade, not a reason for every module on
|
// Reconnect forever: the bus being restarted is an upgrade, not a reason for every module on
|
||||||
// the mesh to exit. `close()` stays the only thing that ends the connection.
|
// the mesh to exit. `close()` stays the only thing that ends the connection.
|
||||||
maxReconnectAttempts: -1,
|
maxReconnectAttempts: -1,
|
||||||
@@ -298,14 +302,15 @@ function normalizeFingerprint(fingerprint: string): string {
|
|||||||
* A certificate authority is not consulted: the mesh issued this and knows its fingerprint,
|
* A certificate authority is not consulted: the mesh issued this and knows its fingerprint,
|
||||||
* which is stronger than trusting whoever a machine's trust store happens to contain.
|
* which is stronger than trusting whoever a machine's trust store happens to contain.
|
||||||
*
|
*
|
||||||
* **A constraint on the mesh, not a detail of this file.** Pinning the exact certificate makes
|
* **The pin is the only check.** What comes back is handed to the client as its TLS options, and
|
||||||
* hostname verification redundant in principle, but the NATS client exposes no hook to replace
|
* the client's transport spreads them into Node's own `tls.connect` — so the pinned certificate
|
||||||
* it — its TLS options are file paths and PEM strings, with no verify callback. So the
|
* is the one authority the handshake accepts, and the hostname check beside it is replaced with
|
||||||
* certificate the mesh issues the bus **must carry a subject-alternative name matching the
|
* one that always passes. Pinning the exact certificate makes verifying its name redundant, and
|
||||||
* address nodes dial it by**. The fingerprint check below still happens and is still the real
|
* the bus's certificate names the seat (`mesh-broker`), not the address a machine happens to
|
||||||
* guarantee; what cannot be switched off is the check *beside* it.
|
* dial it by: every module on the mesh met "does not match certificate's altnames" the first time
|
||||||
|
* it reached the handshake (2026-09-28).
|
||||||
*/
|
*/
|
||||||
async function pinnedTls(rawUrl: string, fingerprint: string): Promise<{ ca: string }> {
|
async function pinnedTls(rawUrl: string, fingerprint: string): Promise<TlsOptions> {
|
||||||
const url = new URL(rawUrl.includes("://") ? rawUrl : `nats://${rawUrl}`);
|
const url = new URL(rawUrl.includes("://") ? rawUrl : `nats://${rawUrl}`);
|
||||||
const port = url.port ? Number(url.port) : 4222;
|
const port = url.port ? Number(url.port) : 4222;
|
||||||
// **The bus speaks first, in the clear.** A NATS server sends its INFO line before TLS begins,
|
// **The bus speaks first, in the clear.** A NATS server sends its INFO line before TLS begins,
|
||||||
@@ -342,7 +347,9 @@ async function pinnedTls(rawUrl: string, fingerprint: string): Promise<{ ca: str
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const pem = `-----BEGIN CERTIFICATE-----\n${certificate.raw.toString("base64").replace(/(.{64})/g, "$1\n")}\n-----END CERTIFICATE-----\n`;
|
const pem = `-----BEGIN CERTIFICATE-----\n${certificate.raw.toString("base64").replace(/(.{64})/g, "$1\n")}\n-----END CERTIFICATE-----\n`;
|
||||||
return { ca: pem };
|
// Node's option, not the client's: the transport passes the whole object on. `undefined` from
|
||||||
|
// checkServerIdentity is "the name is fine"; the pin above already decided the rest.
|
||||||
|
return { ca: pem, checkServerIdentity: () => undefined } as TlsOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The mesh's topic matching: `*` is one token, `#` the rest. This is the module's vocabulary —
|
/** The mesh's topic matching: `*` is one token, `#` the rest. This is the module's vocabulary —
|
||||||
|
|||||||
Reference in New Issue
Block a user