From d0d37fcb87220747fb8cf89c60f755d070d5181a Mon Sep 17 00:00:00 2001 From: eyeveil <64274427+eyeveil@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:20:46 +0100 Subject: [PATCH 1/4] fix(server): linking names an untrusted peer certificate Linking to an https peer whose certificate this machine does not trust reported that nothing there answered as T3 Code. The descriptor probe dropped the TLS error with Effect.option, so the real cause was lost. The probe now fails with the TLS code, and status and token checks still treat that address as not answering. --- apps/server/src/peer/PeerLinks.test.ts | 41 ++++++++++++++ apps/server/src/peer/PeerLinks.ts | 53 +++++++++++++++---- .../src/peer/fixtures/localhost-cert.pem | 17 ++++++ .../src/peer/fixtures/localhost-key.pem | 28 ++++++++++ 4 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 apps/server/src/peer/fixtures/localhost-cert.pem create mode 100644 apps/server/src/peer/fixtures/localhost-key.pem diff --git a/apps/server/src/peer/PeerLinks.test.ts b/apps/server/src/peer/PeerLinks.test.ts index f16a91f26c0ad..71e0ca285da55 100644 --- a/apps/server/src/peer/PeerLinks.test.ts +++ b/apps/server/src/peer/PeerLinks.test.ts @@ -1,3 +1,7 @@ +// @effect-diagnostics-next-line nodeBuiltinImport:off - The HTTPS test server needs certificate bytes. +import * as NodeFS from "node:fs"; +// @effect-diagnostics-next-line nodeBuiltinImport:off - A peer with a certificate this machine does not trust. +import * as NodeHttps from "node:https"; import { NodeHttpServer } from "@effect/platform-node"; import { type ExecutionEnvironmentDescriptor, OrchestratorMcpFailure } from "@t3tools/contracts"; import { expect, it } from "@effect/vitest"; @@ -526,3 +530,40 @@ it.effect("linking a picked machine that answers nowhere names it and links noth }), ).pipe(Effect.provide(NodeHttpServer.layerTest)), ); + +it.effect("names an untrusted certificate instead of calling the peer something else", () => + Effect.scoped( + Effect.gen(function* () { + const server = yield* Layer.build( + Layer.fresh( + NodeHttpServer.layer( + () => + NodeHttps.createServer({ + key: NodeFS.readFileSync(new URL("./fixtures/localhost-key.pem", import.meta.url)), + cert: NodeFS.readFileSync( + new URL("./fixtures/localhost-cert.pem", import.meta.url), + ), + }), + { port: 0, host: "127.0.0.1" }, + ), + ), + ); + const url = yield* Effect.gen(function* () { + yield* HttpServer.serve(Effect.succeed(HttpServerResponse.empty({ status: 404 }))).pipe( + Layer.build, + ); + const address = (yield* HttpServer.HttpServer).address; + if (address._tag === "UnixPathAddress") return yield* Effect.die("expected a TCP address"); + return `https://127.0.0.1:${address.port}`; + }).pipe(Effect.provideContext(server)); + const a = yield* makeA(laptop); + const failed = yield* a.links + .link({ url, pairingCode: "unused", access: "auto" }) + .pipe(Effect.flip); + expect(failed.reason).toBe("unreachable"); + expect(failed.message).toContain("certificate"); + expect(failed.message).toContain("DEPTH_ZERO_SELF_SIGNED_CERT"); + expect(yield* a.links.list).toEqual([]); + }), + ), +); diff --git a/apps/server/src/peer/PeerLinks.ts b/apps/server/src/peer/PeerLinks.ts index f47ea7d006a36..1cf4965b552d7 100644 --- a/apps/server/src/peer/PeerLinks.ts +++ b/apps/server/src/peer/PeerLinks.ts @@ -5,6 +5,7 @@ import { EnvironmentHttpApi, EnvironmentId, type ExecutionEnvironmentCapabilities, + type ExecutionEnvironmentDescriptor, MCP_PEER_LINK_SOFTWARE_ID, type PeerLink, type PeerLinkCreateInput, @@ -21,6 +22,7 @@ import * as Base64Url from "effect/encoding/Base64Url"; import * as Hex from "effect/encoding/Hex"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; +import * as Predicate from "effect/Predicate"; import * as Result from "effect/Result"; import * as Schema from "effect/Schema"; import * as SqlClient from "effect/sql/SqlClient"; @@ -42,6 +44,21 @@ const LINK_REDIRECT_URI = "http://127.0.0.1/t3code-peer-link"; const probeTimeout = (origin: string) => isLocalLoopbackHost(new URL(origin).hostname) ? Duration.seconds(3) : Duration.seconds(10); +/** Node's TLS verification code in a failed request's cause chain, if any. */ +const certificateErrorCode = (error: unknown) => { + let current = error; + for (let depth = 0; depth < 6 && Predicate.isObject(current); depth++) { + if ( + Predicate.hasProperty(current, "code") && + typeof current.code === "string" && + /CERT|SELF_SIGNED|UNABLE_TO_(GET|VERIFY)/.test(current.code) + ) + return current.code; + current = Predicate.hasProperty(current, "cause") ? current.cause : undefined; + } + return undefined; +}; + /** A usable link: where to reach the peer and the token to send there. */ export interface ResolvedPeerLink { readonly link: PeerLink; @@ -188,12 +205,34 @@ const make = Effect.gen(function* () { Effect.provideService(HttpClient.HttpClient, peerHttpClient), ); - /** The environment answering at `origin`, or nothing when it does not answer as T3 Code. */ + /** + * The environment answering at `origin`, or nothing when it does not answer + * as T3 Code. A certificate this machine does not trust fails instead, since + * the peer may well be there. + */ const describe = (origin: string) => clientFor(origin).pipe( Effect.flatMap((client) => client.metadata.descriptor()), Effect.timeout(probeTimeout(origin)), - Effect.option, + Effect.asSome, + Effect.catch((error) => { + const code = certificateErrorCode(error); + return code === undefined + ? Effect.succeed(Option.none()) + : Effect.fail( + new PeerLinkError({ + reason: "unreachable", + message: `${origin} presented a certificate this machine does not trust (${code}).`, + }), + ); + }), + ); + + /** Whether `url` answers as `environmentId`, counting an untrusted certificate as no. */ + const answersAs = (url: string, environmentId: EnvironmentId) => + describe(url).pipe( + Effect.orElseSucceed(() => Option.none()), + Effect.map((found) => Option.filter(found, (peer) => peer.environmentId === environmentId)), ); const toLink = Effect.fn("PeerLinks.toLink")(function* (row: PeerLinkRow) { @@ -489,11 +528,7 @@ const make = Effect.gen(function* () { DateTime.isLessThanOrEqualTo(link.expiresAt, now) ? Effect.succeed("expired") : Effect.findFirst(link.urls, (url) => - describe(url).pipe( - Effect.map( - (found) => Option.isSome(found) && found.value.environmentId === link.environmentId, - ), - ), + answersAs(url, link.environmentId).pipe(Effect.map(Option.isSome)), ).pipe( Effect.map((answered): PeerLinkStatus => Option.isSome(answered) ? "reachable" : "unreachable", @@ -560,9 +595,9 @@ const make = Effect.gen(function* () { // reused LAN address or a moved hostname never receives it. let fenced = true; const url = yield* Effect.findFirst(link.urls, (candidate) => - describe(candidate).pipe( + answersAs(candidate, environmentId).pipe( Effect.map((found) => { - if (Option.isNone(found) || found.value.environmentId !== environmentId) return false; + if (Option.isNone(found)) return false; fenced = linkLimitsHold(found.value); return true; }), diff --git a/apps/server/src/peer/fixtures/localhost-cert.pem b/apps/server/src/peer/fixtures/localhost-cert.pem new file mode 100644 index 0000000000000..acebbe7794bcc --- /dev/null +++ b/apps/server/src/peer/fixtures/localhost-cert.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICyTCCAbGgAwIBAgIJAOpcH/7sbrKbMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV +BAMMCWxvY2FsaG9zdDAeFw0yNjEwMjgxMzIwMDJaFw0zNjEwMjUxMzIwMDJaMBQx +EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBANHdJw88xLujStCPbzRi9ESeQ68eLHbOYCWEgViNAmzU4HvMINRxLtIFcIbK +E8AZ+zRauNSJTbQdvBLxYmbzmQZb9tzQZ28VqlkaXDRvExLhbJe9csxbRExcwEbN +tBXhyEztlSgJSGP+a22GYsGBljCIaOC6HtpbJo1/GBwhKlJfucGuwr2aT/njUaby +BNFaam1M44jCIqmQ1WifxMZxDzlegjKbBeis6q/2aA+EygQ4OxdjaADFj6+ZQsxm +MoEbNKni4mERPKfTM9irh8UoZDCTFHLHqTUQkzIr6dTO10ljZOLCxCmmKiyeUXH6 +Q+IFJYxXk3k5OZ+3qo1/RqOQelcCAwEAAaMeMBwwGgYDVR0RBBMwEYIJbG9jYWxo +b3N0hwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQCRpA0t5fnHYhOvm7b1McZV10le +WXaJIw17ksV0BfJyZhWm2ru8udEJ6qtzSnxrHhBBHUXUJVPSJfbgV+UFcpTPnjGZ +RZzYr7Oq7K/2VyRUCRMQRv4ONo3wbh91DoN6llFiX6VYpa1kuVRscs12jPXomZ9W +Ca1jVTqr9Ty8izHCqVhNgb8xcvEONLCj8Ikeeej4cVl7gW6538UJj0LpQGv2suUR +0dAFo0tSE7KqcsPPsfdOiWWSWhy8r7unCdU/ySmIDK2/tOsQ56vkAl/0zbNVTxck +gSZOEpfqpWCDxg0txC08dQ7+Gh6MdomPXTb/Zxw9pHU3EjV14tQtcDkFa5VS +-----END CERTIFICATE----- diff --git a/apps/server/src/peer/fixtures/localhost-key.pem b/apps/server/src/peer/fixtures/localhost-key.pem new file mode 100644 index 0000000000000..58e81fe0333c3 --- /dev/null +++ b/apps/server/src/peer/fixtures/localhost-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDR3ScPPMS7o0rQ +j280YvREnkOvHix2zmAlhIFYjQJs1OB7zCDUcS7SBXCGyhPAGfs0WrjUiU20HbwS +8WJm85kGW/bc0GdvFapZGlw0bxMS4WyXvXLMW0RMXMBGzbQV4chM7ZUoCUhj/mtt +hmLBgZYwiGjguh7aWyaNfxgcISpSX7nBrsK9mk/541Gm8gTRWmptTOOIwiKpkNVo +n8TGcQ85XoIymwXorOqv9mgPhMoEODsXY2gAxY+vmULMZjKBGzSp4uJhETyn0zPY +q4fFKGQwkxRyx6k1EJMyK+nUztdJY2TiwsQppiosnlFx+kPiBSWMV5N5OTmft6qN +f0ajkHpXAgMBAAECggEBAMrmKBdhR0uqinCt3HQnn8afxQyP/ak3ylk7jNqSKmAS +SJ8oMi1y6DOBstN+mC3IcMijVcOWpBuaC/W8jsi+h5pQW5INo5MQvUu8Yp0HLse4 +Toc+ULknHBdALV3HD2+AhWlq4iR9JRIYLsELhntT0GNH3y42hBHUwqdW03aO0h4k +UHPDtXBA0qWEwLUxtDt6dMWblaBc679FVpQflmj1zjVvcir8hgU5jE8B52zXo0AW +2QtrKZe0ND1TwCmAg61Ox2mZ1MUBFR6pLuKAwTpob6lFoN9OV1x5QETEN6Cz8/db +sgEw9YbVLMY3Gpk379POTd8Bq+CG7ryjdXm6voJDhUECgYEA/wXcR1dv4FLG+i52 +utGL2Uc2B4iLzGkVpzm49mpI8yYr7pJ/PBNgkHLEwLzMp6Rybm5tbEWdK3EF+A+b +mZ+KeO6rLcc6QityvkhXL3QfmVX0QFA0DDYRVMJw99fLJgVsR+5FU+angqD/zskp +jKS+iH4CKk3DINbalsMPpHwUmnECgYEA0qr/cgisNtpqpC32CPQ/ud3Oq7pSQ0tL +8S1dmJ0wJzaLFaXZ3Pofgd81S9Lx/ZvTsYQveO17+h/aZyKzWk5YC5smIPdCdyln +iiVu7Ep/n+5njIuxDlzqMvpoIEcyEnYO2SfWiCP8oLpVVnMNn21QbsPp4RvOVuS7 +jPZBin2EdUcCgYAx/8MlVZZAo9oRGOT8UbMagWSta28FP6EYLS//V+0/hr9cQeqi +K6ZmC6pZo2p4f7xTvGDFtoxPBRTHgr/eCbKLbTewfPwGFyCi/+ngCuNbG8gMh+z8 +a+lP9eNSJEPXC38kRynefj1fVhNxRm1X/sDAX/wqsR5NpPIsVsgNVqpvcQKBgQCq +Cj+5KWrns5BUCI0IWS1+tpv0XKW9j28yse1UG7wcSpgk1wNsq672wtVf6FpO25IZ +PJqcvwNMtp0wlf+yKn5Cj+5MuYc6a38ZMHHB47fzvRhzhuVYtIKMtzWIqC6wW/5M +JS2yIWntybjL2H3sau17GkMS4cDAvkrr0nDAeeOKTQKBgHJKIpkP7fjowXIaO70N +OqajkGxaeuhhfGPLJlmroi15hlijNiTBNtcMzm8K+Wo5kHDp4efG+2As8bS3cXBW +arc3zQVEqEEi9dluVfWc1yb2jeUw+W+b+ZohZqAkIih6Fb3wllk522zRDQLY0Yz9 +yGsIbsE2QjJ+w5GzVul7zaix +-----END PRIVATE KEY----- From ce9210e8e1cdf2c2d3f28e85b6db89bfa254ebf8 Mon Sep 17 00:00:00 2001 From: eyeveil <64274427+eyeveil@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:40:05 +0100 Subject: [PATCH 2/4] test(server): resolve skips an untrusted peer address Name the answersAs span and cover resolve and link status when a stored link lists an untrusted address before the peer. --- apps/server/src/peer/PeerLinks.test.ts | 64 ++++++++++++++++---------- apps/server/src/peer/PeerLinks.ts | 12 +++-- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/apps/server/src/peer/PeerLinks.test.ts b/apps/server/src/peer/PeerLinks.test.ts index 71e0ca285da55..be8e8054cc333 100644 --- a/apps/server/src/peer/PeerLinks.test.ts +++ b/apps/server/src/peer/PeerLinks.test.ts @@ -531,39 +531,55 @@ it.effect("linking a picked machine that answers nowhere names it and links noth ).pipe(Effect.provide(NodeHttpServer.layerTest)), ); +/** An https listener whose certificate this machine does not trust. */ +const serveUntrusted = Layer.build( + Layer.fresh( + NodeHttpServer.layer( + () => + NodeHttps.createServer({ + key: NodeFS.readFileSync(new URL("./fixtures/localhost-key.pem", import.meta.url)), + cert: NodeFS.readFileSync(new URL("./fixtures/localhost-cert.pem", import.meta.url)), + }), + { port: 0, host: "127.0.0.1" }, + ), + ), +).pipe( + Effect.flatMap((server) => + Effect.gen(function* () { + yield* HttpServer.serve(Effect.succeed(HttpServerResponse.empty({ status: 404 }))).pipe( + Layer.build, + ); + const address = (yield* HttpServer.HttpServer).address; + if (address._tag === "UnixPathAddress") return yield* Effect.die("expected a TCP address"); + return `https://127.0.0.1:${address.port}`; + }).pipe(Effect.provideContext(server)), + ), +); + it.effect("names an untrusted certificate instead of calling the peer something else", () => Effect.scoped( Effect.gen(function* () { - const server = yield* Layer.build( - Layer.fresh( - NodeHttpServer.layer( - () => - NodeHttps.createServer({ - key: NodeFS.readFileSync(new URL("./fixtures/localhost-key.pem", import.meta.url)), - cert: NodeFS.readFileSync( - new URL("./fixtures/localhost-cert.pem", import.meta.url), - ), - }), - { port: 0, host: "127.0.0.1" }, - ), - ), - ); - const url = yield* Effect.gen(function* () { - yield* HttpServer.serve(Effect.succeed(HttpServerResponse.empty({ status: 404 }))).pipe( - Layer.build, - ); - const address = (yield* HttpServer.HttpServer).address; - if (address._tag === "UnixPathAddress") return yield* Effect.die("expected a TCP address"); - return `https://127.0.0.1:${address.port}`; - }).pipe(Effect.provideContext(server)); + const untrusted = yield* serveUntrusted; const a = yield* makeA(laptop); const failed = yield* a.links - .link({ url, pairingCode: "unused", access: "auto" }) + .link({ url: untrusted, pairingCode: "unused", access: "auto" }) .pipe(Effect.flip); expect(failed.reason).toBe("unreachable"); expect(failed.message).toContain("certificate"); expect(failed.message).toContain("DEPTH_ZERO_SELF_SIGNED_CERT"); expect(yield* a.links.list).toEqual([]); + + // A stored link tries its next address past an untrusted one. + const b = yield* serveB(box); + const pairing = yield* b.auth.issuePairingCredential(); + yield* a.links.link({ url: b.url, pairingCode: pairing.credential, access: "auto" }); + yield* a.sql` + UPDATE peer_environment_links + SET urls_json = ${JSON.stringify([untrusted, b.url])} + WHERE environment_id = ${box.environmentId} + `; + expect((yield* a.links.resolve(box.environmentId)).url).toBe(b.url); + expect((yield* a.links.list).map((link) => link.status)).toEqual(["reachable"]); }), - ), + ).pipe(Effect.provide(NodeHttpServer.layerTest)), ); diff --git a/apps/server/src/peer/PeerLinks.ts b/apps/server/src/peer/PeerLinks.ts index 1cf4965b552d7..527e2f6b9fd8d 100644 --- a/apps/server/src/peer/PeerLinks.ts +++ b/apps/server/src/peer/PeerLinks.ts @@ -229,11 +229,13 @@ const make = Effect.gen(function* () { ); /** Whether `url` answers as `environmentId`, counting an untrusted certificate as no. */ - const answersAs = (url: string, environmentId: EnvironmentId) => - describe(url).pipe( - Effect.orElseSucceed(() => Option.none()), - Effect.map((found) => Option.filter(found, (peer) => peer.environmentId === environmentId)), - ); + const answersAs = Effect.fn("PeerLinks.answersAs")(function* ( + url: string, + environmentId: EnvironmentId, + ) { + const found = yield* describe(url).pipe(Effect.orElseSucceed(() => Option.none())); + return Option.filter(found, (peer) => peer.environmentId === environmentId); + }); const toLink = Effect.fn("PeerLinks.toLink")(function* (row: PeerLinkRow) { return { From fd95b42e5dfb2bd9d2e3879c722659a0f162a8e3 Mon Sep 17 00:00:00 2001 From: eyeveil <64274427+eyeveil@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:54:19 +0100 Subject: [PATCH 3/4] fix(server): name more TLS verification codes for peer links --- apps/server/src/peer/PeerLinks.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/server/src/peer/PeerLinks.ts b/apps/server/src/peer/PeerLinks.ts index 527e2f6b9fd8d..2f6b8cc1dc74b 100644 --- a/apps/server/src/peer/PeerLinks.ts +++ b/apps/server/src/peer/PeerLinks.ts @@ -51,7 +51,9 @@ const certificateErrorCode = (error: unknown) => { if ( Predicate.hasProperty(current, "code") && typeof current.code === "string" && - /CERT|SELF_SIGNED|UNABLE_TO_(GET|VERIFY)/.test(current.code) + /CERT|SELF_SIGNED|UNABLE_TO_(GET|VERIFY)|INVALID_(CA|PURPOSE)|PATH_LENGTH_EXCEEDED|HOSTNAME_MISMATCH/.test( + current.code, + ) ) return current.code; current = Predicate.hasProperty(current, "cause") ? current.cause : undefined; From 07d1da329565cdea2cac07374634c00bb3fa0cfa Mon Sep 17 00:00:00 2001 From: eyeveil <64274427+eyeveil@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:04:40 +0100 Subject: [PATCH 4/4] fix(server): neutral wording for rejected peer certificates A hostname mismatch is not an untrusted certificate, so the error now says the machine rejected the certificate. Cover the TLS code matcher for each recognized code and a nested cause. --- apps/server/src/peer/PeerLinks.test.ts | 21 ++++++++++++++++++++- apps/server/src/peer/PeerLinks.ts | 6 +++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/apps/server/src/peer/PeerLinks.test.ts b/apps/server/src/peer/PeerLinks.test.ts index be8e8054cc333..11d2c04419b46 100644 --- a/apps/server/src/peer/PeerLinks.test.ts +++ b/apps/server/src/peer/PeerLinks.test.ts @@ -565,7 +565,7 @@ it.effect("names an untrusted certificate instead of calling the peer something .link({ url: untrusted, pairingCode: "unused", access: "auto" }) .pipe(Effect.flip); expect(failed.reason).toBe("unreachable"); - expect(failed.message).toContain("certificate"); + expect(failed.message).toContain("certificate this machine rejected"); expect(failed.message).toContain("DEPTH_ZERO_SELF_SIGNED_CERT"); expect(yield* a.links.list).toEqual([]); @@ -583,3 +583,22 @@ it.effect("names an untrusted certificate instead of calling the peer something }), ).pipe(Effect.provide(NodeHttpServer.layerTest)), ); + +it("reads Node's TLS verification code from anywhere in a cause chain", () => { + for (const code of [ + "DEPTH_ZERO_SELF_SIGNED_CERT", + "UNABLE_TO_VERIFY_LEAF_SIGNATURE", + "CERT_HAS_EXPIRED", + "ERR_TLS_CERT_ALTNAME_INVALID", + "HOSTNAME_MISMATCH", + "INVALID_CA", + "INVALID_PURPOSE", + "PATH_LENGTH_EXCEEDED", + ]) { + expect(PeerLinks.certificateErrorCode({ code })).toBe(code); + } + const wrapped = { message: "fetch failed", cause: { cause: { code: "INVALID_CA" } } }; + expect(PeerLinks.certificateErrorCode(wrapped)).toBe("INVALID_CA"); + expect(PeerLinks.certificateErrorCode({ cause: { code: "ECONNREFUSED" } })).toBeUndefined(); + expect(PeerLinks.certificateErrorCode(new Error("timed out"))).toBeUndefined(); +}); diff --git a/apps/server/src/peer/PeerLinks.ts b/apps/server/src/peer/PeerLinks.ts index 2f6b8cc1dc74b..909e812faff42 100644 --- a/apps/server/src/peer/PeerLinks.ts +++ b/apps/server/src/peer/PeerLinks.ts @@ -45,7 +45,7 @@ const probeTimeout = (origin: string) => isLocalLoopbackHost(new URL(origin).hostname) ? Duration.seconds(3) : Duration.seconds(10); /** Node's TLS verification code in a failed request's cause chain, if any. */ -const certificateErrorCode = (error: unknown) => { +export const certificateErrorCode = (error: unknown) => { let current = error; for (let depth = 0; depth < 6 && Predicate.isObject(current); depth++) { if ( @@ -209,7 +209,7 @@ const make = Effect.gen(function* () { /** * The environment answering at `origin`, or nothing when it does not answer - * as T3 Code. A certificate this machine does not trust fails instead, since + * as T3 Code. A certificate this machine rejects fails instead, since * the peer may well be there. */ const describe = (origin: string) => @@ -224,7 +224,7 @@ const make = Effect.gen(function* () { : Effect.fail( new PeerLinkError({ reason: "unreachable", - message: `${origin} presented a certificate this machine does not trust (${code}).`, + message: `${origin} presented a certificate this machine rejected (${code}).`, }), ); }),