agent-comms 8.15.9 → 8.15.10
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bridges/user/web/server.d.ts.map +1 -1
- package/dist/bridges/user/web/server.js +44 -24
- package/dist/bridges/user/web/server.js.map +1 -1
- package/dist/bridges/user/web/test/web-assets.unit.test.d.ts +5 -0
- package/dist/bridges/user/web/test/web-assets.unit.test.d.ts.map +1 -0
- package/dist/bridges/user/web/test/web-assets.unit.test.js +90 -0
- package/dist/bridges/user/web/test/web-assets.unit.test.js.map +1 -0
- package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.d.ts +2 -0
- package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.d.ts.map +1 -0
- package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.js +75 -0
- package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.js.map +1 -0
- package/dist/bridges/user/web/web-assets.d.ts +28 -0
- package/dist/bridges/user/web/web-assets.d.ts.map +1 -0
- package/dist/bridges/user/web/web-assets.js +67 -0
- package/dist/bridges/user/web/web-assets.js.map +1 -0
- package/package.json +8 -2
- package/src/account-cli.ts +195 -0
- package/src/account-join-cli.ts +191 -0
- package/src/bridges/cc-peer/approval-commands.ts +82 -0
- package/src/bridges/cc-peer/bridge.ts +138 -0
- package/src/bridges/cc-peer/default-front.ts +58 -0
- package/src/bridges/cc-peer/front-controller.ts +160 -0
- package/src/bridges/cc-peer/front-relay.ts +191 -0
- package/src/bridges/cc-peer/front-runtime.ts +167 -0
- package/src/bridges/cc-peer/front.ts +59 -0
- package/src/bridges/cc-peer/reply-aliases.ts +90 -0
- package/src/bridges/cc-peer/run.ts +100 -0
- package/src/bridges/cc-peer/shared-peer.ts +28 -0
- package/src/bridges/claude-code/channel.ts +318 -0
- package/src/bridges/codex/tool.ts +132 -0
- package/src/bridges/mcp/server.ts +137 -0
- package/src/bridges/opencode/plugin.ts +130 -0
- package/src/bridges/pi/extension.ts +330 -0
- package/src/bridges/registry.ts +17 -0
- package/src/bridges/user/cli.ts +87 -0
- package/src/bridges/user/controller.ts +345 -0
- package/src/bridges/user/tui.ts +300 -0
- package/src/bridges/user/web/actions.ts +173 -0
- package/src/bridges/user/web/contract.ts +222 -0
- package/src/bridges/user/web/dist/bundle.js +49 -0
- package/src/bridges/user/web/dist/icons/icon-192x192.svg +9 -0
- package/src/bridges/user/web/dist/icons/icon-512x512.svg +9 -0
- package/src/bridges/user/web/dist/icons/icon-96x96.svg +9 -0
- package/src/bridges/user/web/dist/index.html +21 -0
- package/src/bridges/user/web/dist/manifest.json +26 -0
- package/src/bridges/user/web/dist/mesh-worker.js +64 -0
- package/src/bridges/user/web/dist/sw.js +1 -0
- package/src/bridges/user/web/event-publisher.ts +114 -0
- package/src/bridges/user/web/port-discovery.ts +49 -0
- package/src/bridges/user/web/reads.ts +35 -0
- package/src/bridges/user/web/router.ts +158 -0
- package/src/bridges/user/web/server.ts +782 -0
- package/src/bridges/user/web/vite.config.ts +71 -0
- package/src/bridges/user/web/web-access.ts +404 -0
- package/src/bridges/user/web/web-assets.ts +90 -0
- package/src/bridges/user/web/web-beacon.ts +69 -0
- package/src/bridges/user/web/web-console-static.ts +90 -0
- package/src/cli-main.ts +635 -0
- package/src/cli.ts +13 -0
- package/src/core/account-bundle.ts +264 -0
- package/src/core/account-join.ts +289 -0
- package/src/core/account-ledger-crypto.ts +154 -0
- package/src/core/account-ledger-store.ts +66 -0
- package/src/core/account-ledger.ts +385 -0
- package/src/core/agent-details.ts +216 -0
- package/src/core/agent-list-format.ts +97 -0
- package/src/core/agent-registry.ts +282 -0
- package/src/core/agent-table.ts +58 -0
- package/src/core/atomic-file.ts +46 -0
- package/src/core/bind-retry.ts +38 -0
- package/src/core/bridge-mesh.ts +176 -0
- package/src/core/bridge.ts +718 -0
- package/src/core/capability-ask.ts +162 -0
- package/src/core/capability-bubble-up.ts +237 -0
- package/src/core/comms-store.ts +130 -0
- package/src/core/connection-approval.ts +134 -0
- package/src/core/connection-code-tool.ts +92 -0
- package/src/core/connection-code.ts +296 -0
- package/src/core/coordinator-role.ts +224 -0
- package/src/core/data-directory.ts +32 -0
- package/src/core/data-frame-routing.ts +309 -0
- package/src/core/default-command.ts +12 -0
- package/src/core/delegation-policy.ts +41 -0
- package/src/core/delivery-engine.ts +649 -0
- package/src/core/device-membership-verification.ts +125 -0
- package/src/core/device-membership.ts +130 -0
- package/src/core/directory-admission.ts +155 -0
- package/src/core/discovery-mdns.ts +247 -0
- package/src/core/discovery-tailscale.ts +224 -0
- package/src/core/discovery.ts +303 -0
- package/src/core/display-name.ts +73 -0
- package/src/core/dm-grant-actions.ts +72 -0
- package/src/core/dm-send-delegation.ts +73 -0
- package/src/core/dm-token-verification.ts +76 -0
- package/src/core/election-sessions.ts +135 -0
- package/src/core/error-reporting.ts +85 -0
- package/src/core/first-contact.ts +242 -0
- package/src/core/gateway-trust-actions.ts +184 -0
- package/src/core/gateway-trust.ts +305 -0
- package/src/core/gossip-directory.ts +276 -0
- package/src/core/gossip-extensions.ts +123 -0
- package/src/core/group-proofs.ts +205 -0
- package/src/core/handshake.ts +354 -0
- package/src/core/hub-forwarding.ts +24 -0
- package/src/core/hub-link.ts +176 -0
- package/src/core/hub-session.ts +374 -0
- package/src/core/identity-store.ts +715 -0
- package/src/core/identity.ts +489 -0
- package/src/core/index.ts +65 -0
- package/src/core/issuer-grouping.ts +165 -0
- package/src/core/issuer-identity-file.ts +187 -0
- package/src/core/lan-web-actions.ts +34 -0
- package/src/core/lan-web-uis.ts +55 -0
- package/src/core/ledger-lock.ts +81 -0
- package/src/core/listener-registry.ts +133 -0
- package/src/core/loopback-address.ts +22 -0
- package/src/core/machine-identity.ts +77 -0
- package/src/core/machine-list-groups.ts +53 -0
- package/src/core/mcp-registry-retry.ts +69 -0
- package/src/core/mcp-registry-version.ts +18 -0
- package/src/core/membership-proof.ts +139 -0
- package/src/core/membership-proofs.ts +149 -0
- package/src/core/mesh-graph-trace-tool.ts +81 -0
- package/src/core/mesh-graph.ts +89 -0
- package/src/core/mesh-network-actions.ts +183 -0
- package/src/core/mesh-store-options.ts +23 -0
- package/src/core/mesh-store-shared.ts +134 -0
- package/src/core/mesh-store.ts +1114 -0
- package/src/core/name-claim.ts +145 -0
- package/src/core/naming-actions.ts +90 -0
- package/src/core/naming.ts +138 -0
- package/src/core/nanoid.ts +20 -0
- package/src/core/network-interfaces.ts +24 -0
- package/src/core/package-version.ts +34 -0
- package/src/core/peer-lifecycle.ts +377 -0
- package/src/core/peer-link-error.ts +41 -0
- package/src/core/peer-versions.ts +98 -0
- package/src/core/pending-connection.ts +53 -0
- package/src/core/petnames.ts +121 -0
- package/src/core/pgp-keyserver.ts +32 -0
- package/src/core/pid-lock-file.ts +122 -0
- package/src/core/push-manager.ts +76 -0
- package/src/core/random-id.ts +9 -0
- package/src/core/receiver-set.ts +42 -0
- package/src/core/relay-role.ts +182 -0
- package/src/core/relay-selection.ts +120 -0
- package/src/core/relay-server.ts +198 -0
- package/src/core/relay-uplink.ts +78 -0
- package/src/core/request-timeouts.ts +37 -0
- package/src/core/room-discovery.ts +48 -0
- package/src/core/room-lifecycle.ts +838 -0
- package/src/core/room-lookup.ts +41 -0
- package/src/core/room-messaging.ts +247 -0
- package/src/core/room-notice-log.ts +61 -0
- package/src/core/room-path.ts +84 -0
- package/src/core/room-protocol.ts +893 -0
- package/src/core/room-router.ts +180 -0
- package/src/core/room-token-verification.ts +85 -0
- package/src/core/room-wire-extensions.ts +60 -0
- package/src/core/self-name-claim.ts +67 -0
- package/src/core/send-outcome.ts +69 -0
- package/src/core/shutdown-signals.ts +57 -0
- package/src/core/stale-agent-checker.ts +108 -0
- package/src/core/store.ts +704 -0
- package/src/core/token-text.ts +40 -0
- package/src/core/tool.ts +853 -0
- package/src/core/transport-events.ts +73 -0
- package/src/core/transport-queries.ts +54 -0
- package/src/core/transport.ts +367 -0
- package/src/core/types.ts +550 -0
- package/src/core/user-identity.ts +290 -0
- package/src/core/vapid.ts +81 -0
- package/src/core/version-check.ts +147 -0
- package/src/core/version-report-actions.ts +79 -0
- package/src/core/web-beacon.ts +31 -0
- package/src/core/web-push.ts +322 -0
- package/src/core/wire-mesh-core-version.ts +25 -0
- package/src/core/wire-mesh-identity.ts +36 -0
- package/src/core/wire-mesh-transport-options.ts +82 -0
- package/src/core/wire-mesh-transport.ts +1007 -0
- package/src/core/wire-protocol.ts +153 -0
- package/src/core/ws-dial.ts +191 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Push encryption and VAPID JWT signing (RFC 8291 + RFC 8292).
|
|
3
|
+
*
|
|
4
|
+
* Implements the Web Push protocol using only Node.js built-in `crypto`
|
|
5
|
+
* and `https` — no external dependencies.
|
|
6
|
+
*
|
|
7
|
+
* Encryption flow (RFC 8291):
|
|
8
|
+
* 1. Derive a shared ECDH secret from the subscription's p256dh key
|
|
9
|
+
* 2. HKDF-derive the encryption key and nonce from the shared secret + auth
|
|
10
|
+
* 3. Encrypt the payload with AES-128-GCM
|
|
11
|
+
* 4. Prepend the sender's public key (65 bytes) to the ciphertext
|
|
12
|
+
*
|
|
13
|
+
* VAPID signing (RFC 8292):
|
|
14
|
+
* 1. Build a JWT with sub, exp, and aud claims
|
|
15
|
+
* 2. Sign with ES256 (ECDSA + P-256 + SHA-256)
|
|
16
|
+
* 3. Append the public key and token to the Crypto-Key header
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import * as crypto from "node:crypto";
|
|
20
|
+
import * as https from "node:https";
|
|
21
|
+
import * as http from "node:http";
|
|
22
|
+
import { URL } from "node:url";
|
|
23
|
+
|
|
24
|
+
import type { VapidKeys } from "./vapid.js";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Types
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export interface PushSubscription {
|
|
31
|
+
endpoint: string;
|
|
32
|
+
keys: { p256dh: string; auth: string };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface PushPayload {
|
|
36
|
+
title: string;
|
|
37
|
+
body: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Constants
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
const CONTENT_ENCODING = "aes128gcm";
|
|
44
|
+
const KEY_LENGTH = 16;
|
|
45
|
+
const NONCE_LENGTH = 12;
|
|
46
|
+
const SALT_LENGTH = 16;
|
|
47
|
+
const MAX_PAYLOAD_SIZE = 4078;
|
|
48
|
+
const EC_UNCOMPRESSED_POINT_LENGTH = 65;
|
|
49
|
+
const RECORD_SIZE = 4096;
|
|
50
|
+
const RECORD_SIZE_FIELD_LENGTH = 4;
|
|
51
|
+
const VAPID_JWT_EXPIRY_HOURS = 12;
|
|
52
|
+
const MINUTES_PER_HOUR = 60;
|
|
53
|
+
const SECONDS_PER_MINUTE = 60;
|
|
54
|
+
const MS_PER_SECOND = 1000;
|
|
55
|
+
const HTTPS_DEFAULT_PORT = 443;
|
|
56
|
+
const HTTP_DEFAULT_PORT = 80;
|
|
57
|
+
const HTTP_STATUS_OK_MIN = 200;
|
|
58
|
+
const HTTP_STATUS_OK_MAX = 299;
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Public API
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Send a Web Push notification.
|
|
66
|
+
*
|
|
67
|
+
* Encrypts the payload per RFC 8291, signs with VAPID per RFC 8292,
|
|
68
|
+
* and POSTs to the push service endpoint.
|
|
69
|
+
*/
|
|
70
|
+
export async function sendWebPush(
|
|
71
|
+
subscription: PushSubscription,
|
|
72
|
+
payload: Readonly<PushPayload>,
|
|
73
|
+
vapidKeys: Readonly<VapidKeys>,
|
|
74
|
+
subject: string,
|
|
75
|
+
): Promise<void> {
|
|
76
|
+
const plaintext = JSON.stringify(payload);
|
|
77
|
+
const plaintextBuf = Buffer.from(plaintext, "utf-8");
|
|
78
|
+
|
|
79
|
+
if (plaintextBuf.length > MAX_PAYLOAD_SIZE) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Payload too large: ${String(plaintextBuf.length)} bytes (max ${String(MAX_PAYLOAD_SIZE)})`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const encrypted = encrypt(plaintextBuf, subscription.keys);
|
|
86
|
+
const headers = buildHeaders(
|
|
87
|
+
encrypted,
|
|
88
|
+
subscription.endpoint,
|
|
89
|
+
vapidKeys,
|
|
90
|
+
subject,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
await post(subscription.endpoint, headers, encrypted);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Encryption (RFC 8291)
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
function encrypt(plaintext: Buffer, keys: PushSubscription["keys"]): Buffer {
|
|
101
|
+
// Decode the subscription's p256dh and auth keys
|
|
102
|
+
const recipientPubKey = base64urlDecode(keys.p256dh);
|
|
103
|
+
const authKey = base64urlDecode(keys.auth);
|
|
104
|
+
|
|
105
|
+
// Generate an ephemeral ECDH key pair for this message
|
|
106
|
+
const ecdh = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
|
|
107
|
+
const senderPrivateKey = crypto.createPrivateKey({
|
|
108
|
+
key: ecdh.privateKey.export({ type: "pkcs8", format: "der" }),
|
|
109
|
+
format: "der",
|
|
110
|
+
type: "pkcs8",
|
|
111
|
+
});
|
|
112
|
+
const senderPublicDer = ecdh.publicKey.export({
|
|
113
|
+
type: "spki",
|
|
114
|
+
format: "der",
|
|
115
|
+
});
|
|
116
|
+
const senderPublicKey = senderPublicDer.subarray(
|
|
117
|
+
senderPublicDer.length - EC_UNCOMPRESSED_POINT_LENGTH,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Derive the shared secret via ECDH
|
|
121
|
+
const sharedSecret = crypto.diffieHellman({
|
|
122
|
+
privateKey: senderPrivateKey,
|
|
123
|
+
publicKey: crypto.createPublicKey({
|
|
124
|
+
key: concat(
|
|
125
|
+
Buffer.from(
|
|
126
|
+
"3059301306072a8648ce3d020106082a8648ce3d030107034200",
|
|
127
|
+
"hex",
|
|
128
|
+
),
|
|
129
|
+
recipientPubKey,
|
|
130
|
+
),
|
|
131
|
+
format: "der",
|
|
132
|
+
type: "spki",
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Derive the pseudo-random key (PRK) using HKDF with auth
|
|
137
|
+
const prk = hkdfExtract(authKey, sharedSecret);
|
|
138
|
+
|
|
139
|
+
// Derive the encryption key and nonce
|
|
140
|
+
const keyInfo = concat(
|
|
141
|
+
Buffer.from("Content-Encoding: aes128gcm\0", "utf-8"),
|
|
142
|
+
senderPublicKey,
|
|
143
|
+
);
|
|
144
|
+
const nonceInfo = Buffer.from("Content-Encoding: nonce\0", "utf-8");
|
|
145
|
+
|
|
146
|
+
const encryptionKey = hkdfExpand(prk, keyInfo, KEY_LENGTH);
|
|
147
|
+
const nonce = hkdfExpand(prk, nonceInfo, NONCE_LENGTH);
|
|
148
|
+
|
|
149
|
+
// Generate a random salt
|
|
150
|
+
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
151
|
+
|
|
152
|
+
// Pad the plaintext: plaintext + 2-byte padding delimiter (0x02 0x00 for no padding)
|
|
153
|
+
// RFC 8291 §4: record is plaintext || 0x02 || padding zeros
|
|
154
|
+
const padded = concat(plaintext, Buffer.from([0x02]));
|
|
155
|
+
|
|
156
|
+
// Encrypt with AES-128-GCM
|
|
157
|
+
const cipher = crypto.createCipheriv("aes-128-gcm", encryptionKey, nonce);
|
|
158
|
+
const ciphertext = cipher.update(padded);
|
|
159
|
+
const authTag = cipher.getAuthTag();
|
|
160
|
+
|
|
161
|
+
// RFC 8291 §4: salt (16) || rs (4, big-endian, 4096) || sender pubkey (65) || ciphertext || tag
|
|
162
|
+
const rs = Buffer.alloc(RECORD_SIZE_FIELD_LENGTH);
|
|
163
|
+
rs.writeUInt32BE(RECORD_SIZE);
|
|
164
|
+
|
|
165
|
+
return concat(salt, rs, senderPublicKey, ciphertext, authTag);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// HKDF helpers
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
function hkdfExtract(salt: Buffer, ikm: Buffer): Buffer {
|
|
173
|
+
const hmac = crypto.createHmac("sha256", salt);
|
|
174
|
+
hmac.update(ikm);
|
|
175
|
+
return hmac.digest();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function hkdfExpand(prk: Buffer, info: Buffer, length: number): Buffer {
|
|
179
|
+
const hmac = crypto.createHmac("sha256", prk);
|
|
180
|
+
hmac.update(concat(info, Buffer.from([0x01])));
|
|
181
|
+
const t = hmac.digest();
|
|
182
|
+
return t.subarray(0, length);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// VAPID JWT signing (RFC 8292)
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
function buildVapidJwt(
|
|
190
|
+
audience: string,
|
|
191
|
+
vapidKeys: Readonly<VapidKeys>,
|
|
192
|
+
subject: string,
|
|
193
|
+
): string {
|
|
194
|
+
const header = base64url(Buffer.from('{"alg":"ES256","typ":"JWT"}', "utf-8"));
|
|
195
|
+
|
|
196
|
+
const exp =
|
|
197
|
+
Math.floor(Date.now() / MS_PER_SECOND) +
|
|
198
|
+
VAPID_JWT_EXPIRY_HOURS * MINUTES_PER_HOUR * SECONDS_PER_MINUTE;
|
|
199
|
+
const claims = JSON.stringify({ aud: audience, exp, sub: subject });
|
|
200
|
+
const payload = base64url(Buffer.from(claims, "utf-8"));
|
|
201
|
+
|
|
202
|
+
const signInput = `${header}.${payload}`;
|
|
203
|
+
|
|
204
|
+
// Reconstruct the private key from the raw 32-byte scalar
|
|
205
|
+
// P-256 PKCS8 DER wrapper: 39 bytes header + 32 bytes scalar (with 0x00 prefix)
|
|
206
|
+
const privScalar = base64urlDecode(vapidKeys.privateKey);
|
|
207
|
+
// Build the EC private key in SEC1 DER format
|
|
208
|
+
const sec1Der = concat(
|
|
209
|
+
Buffer.from("30770201010420", "hex"),
|
|
210
|
+
privScalar,
|
|
211
|
+
Buffer.from("a00a06082a8648ce3d030107a14403420004", "hex"),
|
|
212
|
+
base64urlDecode(vapidKeys.publicKey),
|
|
213
|
+
);
|
|
214
|
+
const privateKey = crypto.createPrivateKey({
|
|
215
|
+
key: sec1Der,
|
|
216
|
+
format: "der",
|
|
217
|
+
type: "sec1",
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const signature = crypto.sign("sha256", Buffer.from(signInput), privateKey);
|
|
221
|
+
// ECDSA signature is a DER-encoded SEQUENCE of two INTEGERs.
|
|
222
|
+
// Web Push requires base64url encoding of the raw DER.
|
|
223
|
+
const sigB64 = base64url(signature);
|
|
224
|
+
|
|
225
|
+
return `${signInput}.${sigB64}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// HTTP headers
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
function buildHeaders(
|
|
233
|
+
encrypted: Buffer,
|
|
234
|
+
endpoint: string,
|
|
235
|
+
vapidKeys: Readonly<VapidKeys>,
|
|
236
|
+
subject: string,
|
|
237
|
+
): Record<string, string> {
|
|
238
|
+
const audience = new URL(endpoint).origin;
|
|
239
|
+
const jwt = buildVapidJwt(audience, vapidKeys, subject);
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
"Content-Type": "application/octet-stream",
|
|
243
|
+
"Content-Encoding": CONTENT_ENCODING,
|
|
244
|
+
"Content-Length": String(encrypted.length),
|
|
245
|
+
Authorization: `vapid t=${jwt}, k=${vapidKeys.publicKey}`,
|
|
246
|
+
TTL: "86400",
|
|
247
|
+
Urgency: "high",
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// HTTP POST
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
async function post(
|
|
256
|
+
endpoint: string,
|
|
257
|
+
headers: Readonly<Record<string, string>>,
|
|
258
|
+
body: Buffer,
|
|
259
|
+
): Promise<void> {
|
|
260
|
+
return new Promise((resolve, reject) => {
|
|
261
|
+
const url = new URL(endpoint);
|
|
262
|
+
const isHttps = url.protocol === "https:";
|
|
263
|
+
const mod = isHttps ? https : http;
|
|
264
|
+
const port =
|
|
265
|
+
url.port !== ""
|
|
266
|
+
? url.port
|
|
267
|
+
: String(isHttps ? HTTPS_DEFAULT_PORT : HTTP_DEFAULT_PORT);
|
|
268
|
+
|
|
269
|
+
const req = mod.request(
|
|
270
|
+
{
|
|
271
|
+
hostname: url.hostname,
|
|
272
|
+
port,
|
|
273
|
+
path: url.pathname + url.search,
|
|
274
|
+
method: "POST",
|
|
275
|
+
headers,
|
|
276
|
+
},
|
|
277
|
+
(res) => {
|
|
278
|
+
// Drain the response body
|
|
279
|
+
const chunks: Buffer[] = [];
|
|
280
|
+
res.on("data", (chunk: Buffer) => {
|
|
281
|
+
chunks.push(chunk);
|
|
282
|
+
});
|
|
283
|
+
res.on("end", () => {
|
|
284
|
+
if (
|
|
285
|
+
res.statusCode !== undefined &&
|
|
286
|
+
(res.statusCode < HTTP_STATUS_OK_MIN ||
|
|
287
|
+
res.statusCode > HTTP_STATUS_OK_MAX)
|
|
288
|
+
) {
|
|
289
|
+
const responseBody = Buffer.concat(chunks).toString("utf-8");
|
|
290
|
+
reject(
|
|
291
|
+
new Error(
|
|
292
|
+
`Push service returned ${String(res.statusCode)}: ${responseBody}`,
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
} else {
|
|
296
|
+
resolve();
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
},
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
req.on("error", reject);
|
|
303
|
+
req.write(body);
|
|
304
|
+
req.end();
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// Utilities
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
function base64urlDecode(input: string): Buffer {
|
|
313
|
+
return Buffer.from(input, "base64url");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function base64url(input: Buffer): string {
|
|
317
|
+
return input.toString("base64url");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function concat(...buffers: readonly Buffer[]): Buffer {
|
|
321
|
+
return Buffer.concat(buffers);
|
|
322
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This process's own installed wire-mesh-core dependency version, read directly from its package.json rather than duplicated as a hand-maintained constant -- the local fallback agent-comms#198 calls for once wire-mesh#179 (wire-mesh-core self-advertising CORE_VERSION_GOSSIP_KEY on every self-advert) makes the value moot for a REMOTE peer. own-version.ts (wire-mesh-core's own internal module producing that gossiped value) is not exported as a package subpath, so this side cannot import its OWN_VERSION constant directly -- but it is exactly this side's own installed wire-mesh-core/package.json version, which is exported ("./package.json") and is what own-version.ts itself reads to produce OWN_VERSION in the first place.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { isVersionedPackageJson } from "./package-version.js";
|
|
8
|
+
|
|
9
|
+
let cachedVersion: string | undefined;
|
|
10
|
+
|
|
11
|
+
/** This process's own installed wire-mesh-core version. Resolved via import.meta.resolve (Node's own module resolver) rather than a static JSON import, so it always reads whatever wire-mesh-core is actually installed at runtime -- immune to any bundler that might otherwise inline a JSON import at agent-comms' own build time. Cached after the first successful read, matching getOwnPackageVersion's own convention. */
|
|
12
|
+
export function getWireMeshCoreVersion(): string {
|
|
13
|
+
if (cachedVersion !== undefined) return cachedVersion;
|
|
14
|
+
const packageJsonUrl = import.meta.resolve("wire-mesh-core/package.json");
|
|
15
|
+
const parsed: unknown = JSON.parse(
|
|
16
|
+
readFileSync(fileURLToPath(packageJsonUrl), "utf-8"),
|
|
17
|
+
);
|
|
18
|
+
if (!isVersionedPackageJson(parsed)) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`wire-mesh-core's package.json at ${packageJsonUrl} has no "version" string`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
cachedVersion = parsed.version;
|
|
24
|
+
return cachedVersion;
|
|
25
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapts agent-comms' own PeerIdentity (a PEM-encoded ECDSA P-256 keypair plus a self-signed X.509 certificate, identity.ts's own concern) into wire-mesh-core's IdentityPort -- the shape MeshSession and createTlsTransport both expect. Both sides wrap the exact same keypair; only the envelope differs. The certificate itself plays no part here -- IdentityPort's own device-id is derived straight from the raw public key, identically to how identity.ts's own deriveDeviceId already works, so the two stay consistent by construction rather than by convention.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { webcrypto } from "node:crypto";
|
|
6
|
+
import { createNodeIdentity } from "wire-mesh-core/adapters/node-identity";
|
|
7
|
+
import type { IdentityPort } from "wire-mesh-core/ports/identity";
|
|
8
|
+
import type { PeerIdentity } from "./identity.js";
|
|
9
|
+
import { rawPublicKeyFromPrivateKey } from "./identity.js";
|
|
10
|
+
|
|
11
|
+
/** COSE algorithm identifier for ES256 (P-256 + SHA-256) -- wire-mesh's own identity-key.alg convention. */
|
|
12
|
+
const ES256 = -7;
|
|
13
|
+
|
|
14
|
+
/** Strips a PEM envelope down to its raw DER bytes, copied into a fresh, non-shared, whole-buffer Uint8Array -- Web Crypto's BufferSource parameters reject a view over a SharedArrayBuffer or a sub-range view, neither of which Buffer.from's return type is guaranteed not to be. */
|
|
15
|
+
function pemToDer(pem: string, label: string): Uint8Array<ArrayBuffer> {
|
|
16
|
+
const b64 = pem
|
|
17
|
+
.replace(new RegExp(`-----BEGIN ${label}-----`), "")
|
|
18
|
+
.replace(new RegExp(`-----END ${label}-----`), "")
|
|
19
|
+
.replace(/\s/g, "");
|
|
20
|
+
return Uint8Array.from(Buffer.from(b64, "base64"));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function toIdentityPort(
|
|
24
|
+
identity: Readonly<PeerIdentity>,
|
|
25
|
+
): Promise<IdentityPort> {
|
|
26
|
+
const privateKeyDer = pemToDer(identity.privateKey, "PRIVATE KEY");
|
|
27
|
+
const privateKey = await webcrypto.subtle.importKey(
|
|
28
|
+
"pkcs8",
|
|
29
|
+
privateKeyDer,
|
|
30
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
31
|
+
true,
|
|
32
|
+
["sign"],
|
|
33
|
+
);
|
|
34
|
+
const publicKeyBytes = rawPublicKeyFromPrivateKey(identity.privateKey);
|
|
35
|
+
return createNodeIdentity(privateKey, publicKeyBytes, ES256);
|
|
36
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WireMeshTransport's own constructor options, split out purely to keep wire-mesh-transport.ts under the repo's max-lines cap, the same reason connection-approval.ts, room-router.ts, hub-session.ts, peer-lifecycle.ts, gossip-directory.ts, and listener-registry.ts were each split from that file.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { KeyValueStorage } from "wire-mesh-core/ports/storage";
|
|
6
|
+
import type { AccountLedgerReplica } from "./account-ledger.js";
|
|
7
|
+
import type { GatewayTrustReader } from "./gateway-trust.js";
|
|
8
|
+
import type { RoomVerbHandler } from "./room-router.js";
|
|
9
|
+
import type { AgentStatus } from "./types.js";
|
|
10
|
+
import type { AgentSelfAdvert, HostedRoomAdvert } from "./gossip-extensions.js";
|
|
11
|
+
import type { ListenerPolicy } from "./transport.js";
|
|
12
|
+
|
|
13
|
+
/** roomVerbHandlers, getCurrentPresence, getHostedRooms, dataStorage, getSelfAgentAdvert, and gatewayTrust each match the field-level doc comment on the WireMeshTransport field they back; accountReplication, verifyMembership and roomJoinApprovalTimeoutMs are described by their own doc comments below. pendingConnectionTimeoutMs and presenceReadvertiseIntervalMs default to DEFAULT_PENDING_CONNECTION_TIMEOUT_MS and PRESENCE_READVERTISE_INTERVAL_MS. Every field is optional and they are bundled into this one options type, so a caller needing only one of them never passes `undefined` for the rest. */
|
|
14
|
+
export interface WireMeshTransportOptions {
|
|
15
|
+
roomVerbHandlers?: Partial<Record<string, RoomVerbHandler>>;
|
|
16
|
+
pendingConnectionTimeoutMs?: number | undefined;
|
|
17
|
+
/** How long a room.join this transport sends may await a human decision at the receiving end. Defaults to ROOM_JOIN_APPROVAL_TIMEOUT_MS; a test shortens it. */
|
|
18
|
+
roomJoinApprovalTimeoutMs?: number | undefined;
|
|
19
|
+
getCurrentPresence?: () => AgentStatus | undefined;
|
|
20
|
+
presenceReadvertiseIntervalMs?: number | undefined;
|
|
21
|
+
getHostedRooms?: () => readonly HostedRoomAdvert[];
|
|
22
|
+
dataStorage?: KeyValueStorage;
|
|
23
|
+
getSelfAgentAdvert?: () => AgentSelfAdvert | undefined;
|
|
24
|
+
/** Replicates the account's grant ledger (agent-comms#344) with peers holding the same account: the ledger, read when a data frame for one of its writer logs arrives and on each re-advertise tick, and the check of a peer's gossiped membership proof against this machine's own principal that decides which peers those are. Absent for a caller with no account ledger. */
|
|
25
|
+
accountReplication?: Readonly<{
|
|
26
|
+
getLedger: () => AccountLedgerReplica | undefined;
|
|
27
|
+
isAccountMember: (
|
|
28
|
+
claim: Readonly<{ proof: string; deviceHex: string }>,
|
|
29
|
+
) => Promise<boolean>;
|
|
30
|
+
}>;
|
|
31
|
+
gatewayTrust?: Readonly<GatewayTrustReader>;
|
|
32
|
+
/** The interface the relay this transport serves listens on (agent-comms#342). Defaults to every interface (ALL_INTERFACES_HOST), so the relay reaches the local network; a test serves loopback only. */
|
|
33
|
+
relayListenHost?: string | undefined;
|
|
34
|
+
/** Checks a gossiped membership proof against a principal or machine this side trusts (MembershipProofs.verify). Without it, a directory entry from a device not trusted by id is refused even when it carries a proof. */
|
|
35
|
+
verifyMembership?: (
|
|
36
|
+
claim: Readonly<{ proof: string; deviceHex: string; issuerHex: string }>,
|
|
37
|
+
) => Promise<{ ok: true; expires: number } | { ok: false; reason: string }>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** WireMeshTransport.connectToRemote's own bundled parameters: see MeshTransport's own connectToRemote doc comment (transport.ts) for what each one means. */
|
|
41
|
+
export interface ConnectToRemoteOptions {
|
|
42
|
+
host: string;
|
|
43
|
+
port: number;
|
|
44
|
+
peerId: string;
|
|
45
|
+
dataPort: number;
|
|
46
|
+
name: string;
|
|
47
|
+
fingerprint: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** How an accepted connection is treated, set by the listener that accepted it. fireOnPeerConnected is true only for the data server, whose connections are a known peer's own data dial; requiresApproval quarantines a listener a stranger can dial cold; machineLocal marks a loopback-only listener, whose sessions take part in the coordinator election (election-sessions.ts). */
|
|
51
|
+
export interface AcceptOptions {
|
|
52
|
+
policy: ListenerPolicy | undefined;
|
|
53
|
+
fireOnPeerConnected: boolean;
|
|
54
|
+
requiresApproval: boolean;
|
|
55
|
+
machineLocal: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The data server: bound on loopback, dialled only by peers that already know its port. */
|
|
59
|
+
export const DATA_SERVER_ACCEPT: Readonly<AcceptOptions> = {
|
|
60
|
+
policy: undefined,
|
|
61
|
+
fireOnPeerConnected: true,
|
|
62
|
+
requiresApproval: false,
|
|
63
|
+
machineLocal: true,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** The default well-known port listener: bound on loopback, but dialled cold, so quarantined until an introduce or an approved connect_request. */
|
|
67
|
+
export const DEFAULT_LISTENER_ACCEPT: Readonly<AcceptOptions> = {
|
|
68
|
+
policy: "full",
|
|
69
|
+
fireOnPeerConnected: false,
|
|
70
|
+
requiresApproval: true,
|
|
71
|
+
machineLocal: true,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** A listener an operator added with addListener: on whatever interface they chose, so it may cross machines and never carries coordinator claims. */
|
|
75
|
+
export function operatorListenerAccept(policy: ListenerPolicy): AcceptOptions {
|
|
76
|
+
return {
|
|
77
|
+
policy,
|
|
78
|
+
fireOnPeerConnected: false,
|
|
79
|
+
requiresApproval: true,
|
|
80
|
+
machineLocal: false,
|
|
81
|
+
};
|
|
82
|
+
}
|