@fairfox/polly 0.69.0 → 0.71.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/dist/src/client/index.js +17 -20
- package/dist/src/client/index.js.map +4 -4
- package/dist/src/mesh.js +462 -154
- package/dist/src/mesh.js.map +9 -7
- package/dist/src/peer.js +153 -9
- package/dist/src/peer.js.map +5 -4
- package/dist/src/polly-ui/markdown.js +3 -3
- package/dist/src/polly-ui/markdown.js.map +2 -2
- package/dist/src/shared/lib/mesh-client.d.ts +29 -0
- package/dist/src/shared/lib/mesh-diagnostics.d.ts +129 -0
- package/dist/src/shared/lib/mesh-network-adapter.d.ts +90 -3
- package/dist/src/shared/lib/revocation-summary.d.ts +54 -0
- package/dist/tools/test/src/e2e-mesh/console-allowlist.d.ts +31 -0
- package/dist/tools/test/src/e2e-mesh/index.d.ts +27 -0
- package/dist/tools/test/src/e2e-mesh/index.js +1183 -0
- package/dist/tools/test/src/e2e-mesh/index.js.map +22 -0
- package/dist/tools/test/src/e2e-mesh/keys.d.ts +55 -0
- package/dist/tools/test/src/e2e-mesh/launch-peer.d.ts +98 -0
- package/dist/tools/test/src/e2e-mesh/mesh-assertions.d.ts +53 -0
- package/dist/tools/test/src/e2e-mesh/serve-consumer.d.ts +32 -0
- package/dist/tools/test/src/e2e-mesh/wait-for-convergence.d.ts +38 -0
- package/dist/tools/test/src/e2e-mesh/with-relay.d.ts +53 -0
- package/dist/tools/test/src/visual/index.js +24 -24
- package/dist/tools/test/src/visual/index.js.map +2 -2
- package/dist/tools/verify/src/cli.js +361 -22
- package/dist/tools/verify/src/cli.js.map +6 -6
- package/dist/tools/verify/src/config.d.ts +26 -1
- package/dist/tools/verify/src/config.js +9 -1
- package/dist/tools/verify/src/config.js.map +4 -4
- package/dist/tools/verify/src/primitives/index.d.ts +30 -0
- package/dist/tools/visualize/src/cli.js +43 -1
- package/dist/tools/visualize/src/cli.js.map +3 -3
- package/package.json +11 -8
- package/LICENSE +0 -21
- package/README.md +0 -362
package/dist/src/mesh.js
CHANGED
|
@@ -975,6 +975,30 @@ function base64ToBytes(b64) {
|
|
|
975
975
|
// src/shared/lib/mesh-client.ts
|
|
976
976
|
import { Repo } from "@automerge/automerge-repo/slim";
|
|
977
977
|
|
|
978
|
+
// src/shared/lib/mesh-diagnostics.ts
|
|
979
|
+
var listeners = new Set;
|
|
980
|
+
function emitMeshDiagnostic(diagnostic) {
|
|
981
|
+
const event = { ...diagnostic, timestamp: Date.now() };
|
|
982
|
+
for (const listener of listeners) {
|
|
983
|
+
try {
|
|
984
|
+
listener(event);
|
|
985
|
+
} catch {}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
function subscribeToMeshDiagnostics(listener) {
|
|
989
|
+
listeners.add(listener);
|
|
990
|
+
return () => {
|
|
991
|
+
listeners.delete(listener);
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
function recordMeshDiagnostics() {
|
|
995
|
+
const captured = [];
|
|
996
|
+
const stop = subscribeToMeshDiagnostics((event) => {
|
|
997
|
+
captured.push(event);
|
|
998
|
+
});
|
|
999
|
+
return { events: captured, stop };
|
|
1000
|
+
}
|
|
1001
|
+
|
|
978
1002
|
// src/shared/lib/mesh-network-adapter.ts
|
|
979
1003
|
init_encryption();
|
|
980
1004
|
import {
|
|
@@ -1066,11 +1090,17 @@ function decodeSignedEnvelope(bytes) {
|
|
|
1066
1090
|
|
|
1067
1091
|
// src/shared/lib/mesh-network-adapter.ts
|
|
1068
1092
|
var DEFAULT_MESH_KEY_ID = "polly-mesh-default";
|
|
1093
|
+
var MESH_CONTROL_TYPE = {
|
|
1094
|
+
Sync: 0,
|
|
1095
|
+
Revocation: 1,
|
|
1096
|
+
RevocationSummary: 2
|
|
1097
|
+
};
|
|
1069
1098
|
|
|
1070
1099
|
class MeshNetworkAdapter extends NetworkAdapter {
|
|
1071
1100
|
base;
|
|
1072
1101
|
keyringSource;
|
|
1073
1102
|
encryptionEnabled;
|
|
1103
|
+
onControlMessage;
|
|
1074
1104
|
get keyring() {
|
|
1075
1105
|
return this.keyringSource();
|
|
1076
1106
|
}
|
|
@@ -1079,6 +1109,7 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
1079
1109
|
this.base = options.base;
|
|
1080
1110
|
this.keyringSource = options.keyringSource;
|
|
1081
1111
|
this.encryptionEnabled = options.encryptionEnabled ?? true;
|
|
1112
|
+
this.onControlMessage = options.onControlMessage;
|
|
1082
1113
|
this.base.on("close", () => this.emit("close"));
|
|
1083
1114
|
this.base.on("peer-candidate", (payload) => this.emit("peer-candidate", payload));
|
|
1084
1115
|
this.base.on("peer-disconnected", (payload) => this.emit("peer-disconnected", payload));
|
|
@@ -1112,16 +1143,17 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
1112
1143
|
wrap(message) {
|
|
1113
1144
|
const keyring = this.keyringSource();
|
|
1114
1145
|
const serialised = serialiseMessage(message);
|
|
1146
|
+
const tagged = prependControlTag(MESH_CONTROL_TYPE.Sync, serialised);
|
|
1115
1147
|
let payloadToSign;
|
|
1116
1148
|
if (this.encryptionEnabled) {
|
|
1117
1149
|
const docKey = keyring.documentKeys.get(DEFAULT_MESH_KEY_ID);
|
|
1118
1150
|
if (!docKey) {
|
|
1119
1151
|
throw new Error(`MeshNetworkAdapter: missing document encryption key under id "${DEFAULT_MESH_KEY_ID}". Provision the key in the keyring before sending.`);
|
|
1120
1152
|
}
|
|
1121
|
-
const encrypted = sealEnvelope(
|
|
1153
|
+
const encrypted = sealEnvelope(tagged, DEFAULT_MESH_KEY_ID, docKey);
|
|
1122
1154
|
payloadToSign = encodeEncryptedEnvelope(encrypted);
|
|
1123
1155
|
} else {
|
|
1124
|
-
payloadToSign =
|
|
1156
|
+
payloadToSign = tagged;
|
|
1125
1157
|
}
|
|
1126
1158
|
const signed = signEnvelope(payloadToSign, message.senderId, keyring.identity.secretKey);
|
|
1127
1159
|
const signedBytes = encodeSignedEnvelope(signed);
|
|
@@ -1142,45 +1174,157 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
1142
1174
|
let signed;
|
|
1143
1175
|
try {
|
|
1144
1176
|
signed = decodeSignedEnvelope(message.data);
|
|
1145
|
-
} catch {
|
|
1177
|
+
} catch (err) {
|
|
1178
|
+
emitMeshDiagnostic({
|
|
1179
|
+
kind: "drop:malformed-signed-envelope",
|
|
1180
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
1181
|
+
});
|
|
1146
1182
|
return;
|
|
1147
1183
|
}
|
|
1148
1184
|
const keyring = this.keyringSource();
|
|
1149
1185
|
if (keyring.revokedPeers.has(signed.senderId)) {
|
|
1186
|
+
emitMeshDiagnostic({
|
|
1187
|
+
kind: "drop:revoked-peer",
|
|
1188
|
+
senderId: signed.senderId
|
|
1189
|
+
});
|
|
1150
1190
|
return;
|
|
1151
1191
|
}
|
|
1152
1192
|
const senderKey = keyring.knownPeers.get(signed.senderId);
|
|
1153
1193
|
if (!senderKey) {
|
|
1194
|
+
emitMeshDiagnostic({
|
|
1195
|
+
kind: "drop:unknown-peer",
|
|
1196
|
+
senderId: signed.senderId
|
|
1197
|
+
});
|
|
1154
1198
|
return;
|
|
1155
1199
|
}
|
|
1156
1200
|
let verifiedPayload;
|
|
1157
1201
|
try {
|
|
1158
1202
|
verifiedPayload = openEnvelope2(signed, senderKey);
|
|
1159
|
-
} catch {
|
|
1203
|
+
} catch (err) {
|
|
1204
|
+
emitMeshDiagnostic({
|
|
1205
|
+
kind: "drop:bad-signature",
|
|
1206
|
+
senderId: signed.senderId,
|
|
1207
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
1208
|
+
});
|
|
1160
1209
|
return;
|
|
1161
1210
|
}
|
|
1162
1211
|
if (!this.encryptionEnabled) {
|
|
1163
|
-
return
|
|
1212
|
+
return this.dispatchTaggedPayload(verifiedPayload, signed.senderId);
|
|
1164
1213
|
}
|
|
1165
1214
|
let encrypted;
|
|
1166
1215
|
try {
|
|
1167
1216
|
encrypted = decodeEncryptedEnvelope(verifiedPayload);
|
|
1168
|
-
} catch {
|
|
1217
|
+
} catch (err) {
|
|
1218
|
+
emitMeshDiagnostic({
|
|
1219
|
+
kind: "drop:malformed-encrypted-envelope",
|
|
1220
|
+
senderId: signed.senderId,
|
|
1221
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
1222
|
+
});
|
|
1169
1223
|
return;
|
|
1170
1224
|
}
|
|
1171
1225
|
const docKey = keyring.documentKeys.get(encrypted.documentId);
|
|
1172
1226
|
if (!docKey) {
|
|
1227
|
+
emitMeshDiagnostic({
|
|
1228
|
+
kind: "drop:missing-doc-key",
|
|
1229
|
+
senderId: signed.senderId,
|
|
1230
|
+
documentId: encrypted.documentId
|
|
1231
|
+
});
|
|
1173
1232
|
return;
|
|
1174
1233
|
}
|
|
1175
1234
|
let plaintext;
|
|
1176
1235
|
try {
|
|
1177
1236
|
plaintext = openEnvelope(encrypted, docKey);
|
|
1178
|
-
} catch {
|
|
1237
|
+
} catch (err) {
|
|
1238
|
+
emitMeshDiagnostic({
|
|
1239
|
+
kind: "drop:bad-decryption",
|
|
1240
|
+
senderId: signed.senderId,
|
|
1241
|
+
documentId: encrypted.documentId,
|
|
1242
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
1243
|
+
});
|
|
1179
1244
|
return;
|
|
1180
1245
|
}
|
|
1181
|
-
return
|
|
1246
|
+
return this.dispatchTaggedPayload(plaintext, signed.senderId);
|
|
1247
|
+
}
|
|
1248
|
+
dispatchTaggedPayload(payload, senderId) {
|
|
1249
|
+
if (payload.byteLength < 1) {
|
|
1250
|
+
emitMeshDiagnostic({ kind: "drop:empty-control-payload", senderId });
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
const tag = payload[0];
|
|
1254
|
+
const body = payload.subarray(1);
|
|
1255
|
+
switch (tag) {
|
|
1256
|
+
case MESH_CONTROL_TYPE.Sync:
|
|
1257
|
+
return deserialiseMessage(body);
|
|
1258
|
+
case MESH_CONTROL_TYPE.Revocation:
|
|
1259
|
+
emitMeshDiagnostic({ kind: "ctrl:revocation-received", senderId });
|
|
1260
|
+
this.invokeControlHandler(tag, body, senderId);
|
|
1261
|
+
return;
|
|
1262
|
+
case MESH_CONTROL_TYPE.RevocationSummary:
|
|
1263
|
+
emitMeshDiagnostic({
|
|
1264
|
+
kind: "ctrl:revocation-summary-received",
|
|
1265
|
+
senderId
|
|
1266
|
+
});
|
|
1267
|
+
this.invokeControlHandler(tag, body, senderId);
|
|
1268
|
+
return;
|
|
1269
|
+
default:
|
|
1270
|
+
emitMeshDiagnostic({
|
|
1271
|
+
kind: "drop:unknown-control-type",
|
|
1272
|
+
senderId,
|
|
1273
|
+
tag
|
|
1274
|
+
});
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
invokeControlHandler(tag, body, senderId) {
|
|
1279
|
+
if (!this.onControlMessage)
|
|
1280
|
+
return;
|
|
1281
|
+
try {
|
|
1282
|
+
this.onControlMessage(tag, body, senderId);
|
|
1283
|
+
} catch (err) {
|
|
1284
|
+
emitMeshDiagnostic({
|
|
1285
|
+
kind: "drop:control-handler-threw",
|
|
1286
|
+
senderId,
|
|
1287
|
+
tag,
|
|
1288
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
sendControlMessage(tag, body, targetPeerIds) {
|
|
1293
|
+
if (targetPeerIds.length === 0)
|
|
1294
|
+
return;
|
|
1295
|
+
const keyring = this.keyringSource();
|
|
1296
|
+
const tagged = prependControlTag(tag, body);
|
|
1297
|
+
let payloadToSign;
|
|
1298
|
+
if (this.encryptionEnabled) {
|
|
1299
|
+
const docKey = keyring.documentKeys.get(DEFAULT_MESH_KEY_ID);
|
|
1300
|
+
if (!docKey) {
|
|
1301
|
+
throw new Error(`MeshNetworkAdapter.sendControlMessage: missing document encryption key under id "${DEFAULT_MESH_KEY_ID}".`);
|
|
1302
|
+
}
|
|
1303
|
+
const encrypted = sealEnvelope(tagged, DEFAULT_MESH_KEY_ID, docKey);
|
|
1304
|
+
payloadToSign = encodeEncryptedEnvelope(encrypted);
|
|
1305
|
+
} else {
|
|
1306
|
+
payloadToSign = tagged;
|
|
1307
|
+
}
|
|
1308
|
+
const senderId = this.peerId ?? "";
|
|
1309
|
+
const signed = signEnvelope(payloadToSign, senderId, keyring.identity.secretKey);
|
|
1310
|
+
const signedBytes = encodeSignedEnvelope(signed);
|
|
1311
|
+
for (const targetId of targetPeerIds) {
|
|
1312
|
+
const outer = {
|
|
1313
|
+
type: "sync",
|
|
1314
|
+
senderId,
|
|
1315
|
+
targetId,
|
|
1316
|
+
data: signedBytes
|
|
1317
|
+
};
|
|
1318
|
+
this.base.send(outer);
|
|
1319
|
+
}
|
|
1182
1320
|
}
|
|
1183
1321
|
}
|
|
1322
|
+
function prependControlTag(tag, body) {
|
|
1323
|
+
const out = new Uint8Array(body.byteLength + 1);
|
|
1324
|
+
out[0] = tag;
|
|
1325
|
+
out.set(body, 1);
|
|
1326
|
+
return out;
|
|
1327
|
+
}
|
|
1184
1328
|
function serialiseMessage(message) {
|
|
1185
1329
|
const headerObj = {
|
|
1186
1330
|
type: message.type,
|
|
@@ -3296,6 +3440,193 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
3296
3440
|
}
|
|
3297
3441
|
}
|
|
3298
3442
|
|
|
3443
|
+
// src/shared/lib/revocation.ts
|
|
3444
|
+
var REVOCATION_RECORD_VERSION = 1;
|
|
3445
|
+
var REVOCATION_MAGIC = new Uint8Array([80, 82, 86, 49]);
|
|
3446
|
+
|
|
3447
|
+
class RevocationError extends Error {
|
|
3448
|
+
code;
|
|
3449
|
+
constructor(message, code) {
|
|
3450
|
+
super(message);
|
|
3451
|
+
this.name = "RevocationError";
|
|
3452
|
+
this.code = code;
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
function createRevocation(options) {
|
|
3456
|
+
const now = options.now ? options.now() : Date.now();
|
|
3457
|
+
return {
|
|
3458
|
+
version: REVOCATION_RECORD_VERSION,
|
|
3459
|
+
issuerPeerId: options.issuerPeerId,
|
|
3460
|
+
revokedPeerId: options.revokedPeerId,
|
|
3461
|
+
issuedAt: now,
|
|
3462
|
+
...options.reason === undefined ? {} : { reason: options.reason }
|
|
3463
|
+
};
|
|
3464
|
+
}
|
|
3465
|
+
function applyRevocation(record, keyring) {
|
|
3466
|
+
keyring.revokedPeers.add(record.revokedPeerId);
|
|
3467
|
+
}
|
|
3468
|
+
function revokePeerLocally(peerId, keyring) {
|
|
3469
|
+
keyring.revokedPeers.add(peerId);
|
|
3470
|
+
}
|
|
3471
|
+
function serialiseRevocationPayload(record) {
|
|
3472
|
+
const issuerBytes = new TextEncoder().encode(record.issuerPeerId);
|
|
3473
|
+
const revokedBytes = new TextEncoder().encode(record.revokedPeerId);
|
|
3474
|
+
const reasonBytes = new TextEncoder().encode(record.reason ?? "");
|
|
3475
|
+
const total = REVOCATION_MAGIC.length + 1 + 4 + issuerBytes.length + 4 + revokedBytes.length + 8 + 4 + reasonBytes.length;
|
|
3476
|
+
const out = new Uint8Array(total);
|
|
3477
|
+
let offset = 0;
|
|
3478
|
+
out.set(REVOCATION_MAGIC, offset);
|
|
3479
|
+
offset += REVOCATION_MAGIC.length;
|
|
3480
|
+
out[offset] = record.version;
|
|
3481
|
+
offset += 1;
|
|
3482
|
+
const view = new DataView(out.buffer);
|
|
3483
|
+
view.setUint32(offset, issuerBytes.length, false);
|
|
3484
|
+
offset += 4;
|
|
3485
|
+
out.set(issuerBytes, offset);
|
|
3486
|
+
offset += issuerBytes.length;
|
|
3487
|
+
view.setUint32(offset, revokedBytes.length, false);
|
|
3488
|
+
offset += 4;
|
|
3489
|
+
out.set(revokedBytes, offset);
|
|
3490
|
+
offset += revokedBytes.length;
|
|
3491
|
+
view.setBigUint64(offset, BigInt(record.issuedAt), false);
|
|
3492
|
+
offset += 8;
|
|
3493
|
+
view.setUint32(offset, reasonBytes.length, false);
|
|
3494
|
+
offset += 4;
|
|
3495
|
+
out.set(reasonBytes, offset);
|
|
3496
|
+
return out;
|
|
3497
|
+
}
|
|
3498
|
+
function parseRevocationPayload(bytes) {
|
|
3499
|
+
let offset = 0;
|
|
3500
|
+
if (bytes.length < REVOCATION_MAGIC.length) {
|
|
3501
|
+
throw new RevocationError("Revocation record too short for magic.", "truncated");
|
|
3502
|
+
}
|
|
3503
|
+
for (let i = 0;i < REVOCATION_MAGIC.length; i++) {
|
|
3504
|
+
if (bytes[offset + i] !== REVOCATION_MAGIC[i]) {
|
|
3505
|
+
throw new RevocationError("Revocation record magic mismatch.", "wrong-magic");
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
offset += REVOCATION_MAGIC.length;
|
|
3509
|
+
if (bytes.length < offset + 1) {
|
|
3510
|
+
throw new RevocationError("Revocation record truncated at version.", "truncated");
|
|
3511
|
+
}
|
|
3512
|
+
const version = bytes[offset];
|
|
3513
|
+
offset += 1;
|
|
3514
|
+
if (version !== REVOCATION_RECORD_VERSION) {
|
|
3515
|
+
throw new RevocationError(`Unknown revocation record version: ${version}.`, "unknown-version");
|
|
3516
|
+
}
|
|
3517
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
3518
|
+
if (bytes.length < offset + 4) {
|
|
3519
|
+
throw new RevocationError("Revocation record truncated at issuer length.", "truncated");
|
|
3520
|
+
}
|
|
3521
|
+
const issuerLen = view.getUint32(offset, false);
|
|
3522
|
+
offset += 4;
|
|
3523
|
+
if (bytes.length < offset + issuerLen) {
|
|
3524
|
+
throw new RevocationError("Revocation record truncated at issuer id.", "truncated");
|
|
3525
|
+
}
|
|
3526
|
+
const issuerPeerId = new TextDecoder().decode(bytes.subarray(offset, offset + issuerLen));
|
|
3527
|
+
offset += issuerLen;
|
|
3528
|
+
if (bytes.length < offset + 4) {
|
|
3529
|
+
throw new RevocationError("Revocation record truncated at revoked id length.", "truncated");
|
|
3530
|
+
}
|
|
3531
|
+
const revokedLen = view.getUint32(offset, false);
|
|
3532
|
+
offset += 4;
|
|
3533
|
+
if (bytes.length < offset + revokedLen) {
|
|
3534
|
+
throw new RevocationError("Revocation record truncated at revoked id.", "truncated");
|
|
3535
|
+
}
|
|
3536
|
+
const revokedPeerId = new TextDecoder().decode(bytes.subarray(offset, offset + revokedLen));
|
|
3537
|
+
offset += revokedLen;
|
|
3538
|
+
if (bytes.length < offset + 8) {
|
|
3539
|
+
throw new RevocationError("Revocation record truncated at issuedAt.", "truncated");
|
|
3540
|
+
}
|
|
3541
|
+
const issuedAt = Number(view.getBigUint64(offset, false));
|
|
3542
|
+
offset += 8;
|
|
3543
|
+
if (bytes.length < offset + 4) {
|
|
3544
|
+
throw new RevocationError("Revocation record truncated at reason length.", "truncated");
|
|
3545
|
+
}
|
|
3546
|
+
const reasonLen = view.getUint32(offset, false);
|
|
3547
|
+
offset += 4;
|
|
3548
|
+
if (bytes.length < offset + reasonLen) {
|
|
3549
|
+
throw new RevocationError("Revocation record truncated at reason.", "truncated");
|
|
3550
|
+
}
|
|
3551
|
+
const reason = new TextDecoder().decode(bytes.subarray(offset, offset + reasonLen));
|
|
3552
|
+
offset += reasonLen;
|
|
3553
|
+
return {
|
|
3554
|
+
version,
|
|
3555
|
+
issuerPeerId,
|
|
3556
|
+
revokedPeerId,
|
|
3557
|
+
issuedAt,
|
|
3558
|
+
...reason ? { reason } : {}
|
|
3559
|
+
};
|
|
3560
|
+
}
|
|
3561
|
+
function encodeRevocation(record, issuer) {
|
|
3562
|
+
const payload = serialiseRevocationPayload(record);
|
|
3563
|
+
const envelope = signEnvelope(payload, record.issuerPeerId, issuer.secretKey);
|
|
3564
|
+
return encodeSignedEnvelope(envelope);
|
|
3565
|
+
}
|
|
3566
|
+
function decodeRevocation(bytes, keyring) {
|
|
3567
|
+
const envelope = decodeSignedEnvelope(bytes);
|
|
3568
|
+
const issuerKey = keyring.knownPeers.get(envelope.senderId);
|
|
3569
|
+
if (!issuerKey) {
|
|
3570
|
+
throw new RevocationError(`Revocation issuer ${envelope.senderId} is not in the local keyring.`, "unknown-issuer");
|
|
3571
|
+
}
|
|
3572
|
+
if (keyring.revocationAuthority !== undefined && keyring.revocationAuthority.size > 0 && !keyring.revocationAuthority.has(envelope.senderId)) {
|
|
3573
|
+
throw new RevocationError(`Revocation issuer ${envelope.senderId} is not in the keyring's revocation authority set.`, "unauthorised-issuer");
|
|
3574
|
+
}
|
|
3575
|
+
let payloadBytes;
|
|
3576
|
+
try {
|
|
3577
|
+
payloadBytes = openEnvelope2(envelope, issuerKey);
|
|
3578
|
+
} catch {
|
|
3579
|
+
throw new RevocationError(`Revocation signature failed verification for issuer ${envelope.senderId}.`, "signature-invalid");
|
|
3580
|
+
}
|
|
3581
|
+
const record = parseRevocationPayload(payloadBytes);
|
|
3582
|
+
if (record.issuerPeerId !== envelope.senderId) {
|
|
3583
|
+
throw new RevocationError(`Revocation payload claims issuer ${record.issuerPeerId} but the envelope was signed by ${envelope.senderId}.`, "not-signed-by-issuer");
|
|
3584
|
+
}
|
|
3585
|
+
return record;
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
// src/shared/lib/revocation-summary.ts
|
|
3589
|
+
function encodeRevocationSummary(entries) {
|
|
3590
|
+
const sorted = [...entries].sort(compareSummaryEntries);
|
|
3591
|
+
const json = JSON.stringify(sorted.map((entry) => ({
|
|
3592
|
+
r: entry.revokedPeerId,
|
|
3593
|
+
i: entry.issuerPeerId,
|
|
3594
|
+
t: entry.issuedAt
|
|
3595
|
+
})));
|
|
3596
|
+
return new TextEncoder().encode(json);
|
|
3597
|
+
}
|
|
3598
|
+
function decodeRevocationSummary(bytes) {
|
|
3599
|
+
const text = new TextDecoder().decode(bytes);
|
|
3600
|
+
let raw;
|
|
3601
|
+
try {
|
|
3602
|
+
raw = JSON.parse(text);
|
|
3603
|
+
} catch (err) {
|
|
3604
|
+
throw new Error(`decodeRevocationSummary: invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
3605
|
+
}
|
|
3606
|
+
if (!Array.isArray(raw)) {
|
|
3607
|
+
throw new Error("decodeRevocationSummary: expected an array");
|
|
3608
|
+
}
|
|
3609
|
+
return raw.map((item, index) => {
|
|
3610
|
+
if (!item || typeof item !== "object") {
|
|
3611
|
+
throw new Error(`decodeRevocationSummary: entry ${index} is not an object`);
|
|
3612
|
+
}
|
|
3613
|
+
const e = item;
|
|
3614
|
+
if (typeof e.r !== "string" || typeof e.i !== "string" || typeof e.t !== "number") {
|
|
3615
|
+
throw new Error(`decodeRevocationSummary: entry ${index} has missing or wrong-typed fields`);
|
|
3616
|
+
}
|
|
3617
|
+
return { revokedPeerId: e.r, issuerPeerId: e.i, issuedAt: e.t };
|
|
3618
|
+
});
|
|
3619
|
+
}
|
|
3620
|
+
function compareSummaryEntries(a, b) {
|
|
3621
|
+
if (a.revokedPeerId !== b.revokedPeerId) {
|
|
3622
|
+
return a.revokedPeerId < b.revokedPeerId ? -1 : 1;
|
|
3623
|
+
}
|
|
3624
|
+
if (a.issuerPeerId !== b.issuerPeerId) {
|
|
3625
|
+
return a.issuerPeerId < b.issuerPeerId ? -1 : 1;
|
|
3626
|
+
}
|
|
3627
|
+
return a.issuedAt - b.issuedAt;
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3299
3630
|
// src/shared/lib/mesh-client.ts
|
|
3300
3631
|
async function resolveIceServers(rtc) {
|
|
3301
3632
|
if (rtc?.iceCredentialResolver) {
|
|
@@ -3482,10 +3813,107 @@ async function createMeshClient(options) {
|
|
|
3482
3813
|
});
|
|
3483
3814
|
webrtcAdapterOptions.signaling = signaling;
|
|
3484
3815
|
webrtcAdapter = new MeshWebRTCAdapter(webrtcAdapterOptions);
|
|
3816
|
+
const connectedPeerIds = new Set;
|
|
3817
|
+
let selfRevocation;
|
|
3818
|
+
const localPeerId = options.signaling.peerId;
|
|
3819
|
+
const revocationStore = new Map;
|
|
3820
|
+
function storeRevocation(record, bytes) {
|
|
3821
|
+
const existing = revocationStore.get(record.revokedPeerId);
|
|
3822
|
+
if (existing && existing.entry.issuedAt >= record.issuedAt)
|
|
3823
|
+
return;
|
|
3824
|
+
revocationStore.set(record.revokedPeerId, {
|
|
3825
|
+
bytes,
|
|
3826
|
+
entry: {
|
|
3827
|
+
revokedPeerId: record.revokedPeerId,
|
|
3828
|
+
issuerPeerId: record.issuerPeerId,
|
|
3829
|
+
issuedAt: record.issuedAt
|
|
3830
|
+
}
|
|
3831
|
+
});
|
|
3832
|
+
}
|
|
3833
|
+
const handleRevocationControl = (body, senderId) => {
|
|
3834
|
+
const keyring2 = keyringSource();
|
|
3835
|
+
let record;
|
|
3836
|
+
try {
|
|
3837
|
+
record = decodeRevocation(body, keyring2);
|
|
3838
|
+
} catch (err) {
|
|
3839
|
+
const reason = err instanceof RevocationError ? err.code : err instanceof Error ? err.message : String(err);
|
|
3840
|
+
emitMeshDiagnostic({ kind: "revoke:rejected", senderId, reason });
|
|
3841
|
+
return;
|
|
3842
|
+
}
|
|
3843
|
+
if (record.revokedPeerId === localPeerId) {
|
|
3844
|
+
selfRevocation = record;
|
|
3845
|
+
emitMeshDiagnostic({
|
|
3846
|
+
kind: "revoke:self-targeted",
|
|
3847
|
+
issuerId: record.issuerPeerId,
|
|
3848
|
+
...record.reason !== undefined && { reason: record.reason },
|
|
3849
|
+
issuedAt: record.issuedAt
|
|
3850
|
+
});
|
|
3851
|
+
return;
|
|
3852
|
+
}
|
|
3853
|
+
if (keyring2.revokedPeers.has(record.revokedPeerId)) {
|
|
3854
|
+
storeRevocation(record, body);
|
|
3855
|
+
emitMeshDiagnostic({
|
|
3856
|
+
kind: "revoke:duplicate",
|
|
3857
|
+
revokedPeerId: record.revokedPeerId,
|
|
3858
|
+
issuerId: record.issuerPeerId
|
|
3859
|
+
});
|
|
3860
|
+
return;
|
|
3861
|
+
}
|
|
3862
|
+
applyRevocation(record, keyring2);
|
|
3863
|
+
storeRevocation(record, body);
|
|
3864
|
+
emitMeshDiagnostic({ kind: "revoke:applied", revokedPeerId: record.revokedPeerId });
|
|
3865
|
+
};
|
|
3866
|
+
const handleRevocationSummary = (body, senderId) => {
|
|
3867
|
+
let summary;
|
|
3868
|
+
try {
|
|
3869
|
+
summary = decodeRevocationSummary(body);
|
|
3870
|
+
} catch (err) {
|
|
3871
|
+
emitMeshDiagnostic({
|
|
3872
|
+
kind: "revoke:rejected",
|
|
3873
|
+
senderId,
|
|
3874
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
3875
|
+
});
|
|
3876
|
+
return;
|
|
3877
|
+
}
|
|
3878
|
+
const remoteKeys = new Set(summary.map((entry) => entry.revokedPeerId));
|
|
3879
|
+
const senderPeerId = senderId;
|
|
3880
|
+
for (const stored of revocationStore.values()) {
|
|
3881
|
+
if (remoteKeys.has(stored.entry.revokedPeerId))
|
|
3882
|
+
continue;
|
|
3883
|
+
networkAdapter.sendControlMessage(MESH_CONTROL_TYPE.Revocation, stored.bytes, [senderPeerId]);
|
|
3884
|
+
}
|
|
3885
|
+
};
|
|
3485
3886
|
const networkAdapter = new MeshNetworkAdapter({
|
|
3486
3887
|
base: webrtcAdapter,
|
|
3487
3888
|
keyringSource,
|
|
3488
|
-
encryptionEnabled
|
|
3889
|
+
encryptionEnabled,
|
|
3890
|
+
onControlMessage: (tag, body, senderId) => {
|
|
3891
|
+
if (tag === MESH_CONTROL_TYPE.Revocation) {
|
|
3892
|
+
handleRevocationControl(body, senderId);
|
|
3893
|
+
} else if (tag === MESH_CONTROL_TYPE.RevocationSummary) {
|
|
3894
|
+
handleRevocationSummary(body, senderId);
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
});
|
|
3898
|
+
function sendSummaryTo(peerId) {
|
|
3899
|
+
const entries = [...revocationStore.values()].map((stored) => stored.entry);
|
|
3900
|
+
const body = encodeRevocationSummary(entries);
|
|
3901
|
+
networkAdapter.sendControlMessage(MESH_CONTROL_TYPE.RevocationSummary, body, [peerId]);
|
|
3902
|
+
}
|
|
3903
|
+
networkAdapter.on("peer-candidate", (event) => {
|
|
3904
|
+
connectedPeerIds.add(event.peerId);
|
|
3905
|
+
try {
|
|
3906
|
+
sendSummaryTo(event.peerId);
|
|
3907
|
+
} catch (err) {
|
|
3908
|
+
emitMeshDiagnostic({
|
|
3909
|
+
kind: "revoke:rejected",
|
|
3910
|
+
senderId: event.peerId,
|
|
3911
|
+
reason: `summary-send-failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3912
|
+
});
|
|
3913
|
+
}
|
|
3914
|
+
});
|
|
3915
|
+
networkAdapter.on("peer-disconnected", (event) => {
|
|
3916
|
+
connectedPeerIds.delete(event.peerId);
|
|
3489
3917
|
});
|
|
3490
3918
|
const repo = new Repo({
|
|
3491
3919
|
network: [networkAdapter],
|
|
@@ -3552,6 +3980,30 @@ async function createMeshClient(options) {
|
|
|
3552
3980
|
return;
|
|
3553
3981
|
await webrtcAdapter.refreshAllTransportStats();
|
|
3554
3982
|
},
|
|
3983
|
+
revokePeer: async (targetPeerId, reason) => {
|
|
3984
|
+
const keyring2 = keyringSource();
|
|
3985
|
+
const record = createRevocation({
|
|
3986
|
+
issuer: keyring2.identity,
|
|
3987
|
+
issuerPeerId: localPeerId,
|
|
3988
|
+
revokedPeerId: targetPeerId,
|
|
3989
|
+
...reason !== undefined && { reason }
|
|
3990
|
+
});
|
|
3991
|
+
const bytes = encodeRevocation(record, keyring2.identity);
|
|
3992
|
+
applyRevocation(record, keyring2);
|
|
3993
|
+
storeRevocation(record, bytes);
|
|
3994
|
+
emitMeshDiagnostic({
|
|
3995
|
+
kind: "revoke:issued",
|
|
3996
|
+
revokedPeerId: targetPeerId,
|
|
3997
|
+
issuerId: localPeerId
|
|
3998
|
+
});
|
|
3999
|
+
const targets = [...connectedPeerIds];
|
|
4000
|
+
if (targets.length > 0) {
|
|
4001
|
+
networkAdapter.sendControlMessage(MESH_CONTROL_TYPE.Revocation, bytes, targets);
|
|
4002
|
+
}
|
|
4003
|
+
},
|
|
4004
|
+
get selfRevocation() {
|
|
4005
|
+
return selfRevocation;
|
|
4006
|
+
},
|
|
3555
4007
|
close: async () => {
|
|
3556
4008
|
signaling.close();
|
|
3557
4009
|
webrtcAdapter?.disconnect();
|
|
@@ -3762,150 +4214,6 @@ function randomBytes(n) {
|
|
|
3762
4214
|
crypto.getRandomValues(out);
|
|
3763
4215
|
return out;
|
|
3764
4216
|
}
|
|
3765
|
-
// src/shared/lib/revocation.ts
|
|
3766
|
-
var REVOCATION_RECORD_VERSION = 1;
|
|
3767
|
-
var REVOCATION_MAGIC = new Uint8Array([80, 82, 86, 49]);
|
|
3768
|
-
|
|
3769
|
-
class RevocationError extends Error {
|
|
3770
|
-
code;
|
|
3771
|
-
constructor(message, code) {
|
|
3772
|
-
super(message);
|
|
3773
|
-
this.name = "RevocationError";
|
|
3774
|
-
this.code = code;
|
|
3775
|
-
}
|
|
3776
|
-
}
|
|
3777
|
-
function createRevocation(options) {
|
|
3778
|
-
const now = options.now ? options.now() : Date.now();
|
|
3779
|
-
return {
|
|
3780
|
-
version: REVOCATION_RECORD_VERSION,
|
|
3781
|
-
issuerPeerId: options.issuerPeerId,
|
|
3782
|
-
revokedPeerId: options.revokedPeerId,
|
|
3783
|
-
issuedAt: now,
|
|
3784
|
-
...options.reason === undefined ? {} : { reason: options.reason }
|
|
3785
|
-
};
|
|
3786
|
-
}
|
|
3787
|
-
function applyRevocation(record, keyring) {
|
|
3788
|
-
keyring.revokedPeers.add(record.revokedPeerId);
|
|
3789
|
-
}
|
|
3790
|
-
function revokePeerLocally(peerId, keyring) {
|
|
3791
|
-
keyring.revokedPeers.add(peerId);
|
|
3792
|
-
}
|
|
3793
|
-
function serialiseRevocationPayload(record) {
|
|
3794
|
-
const issuerBytes = new TextEncoder().encode(record.issuerPeerId);
|
|
3795
|
-
const revokedBytes = new TextEncoder().encode(record.revokedPeerId);
|
|
3796
|
-
const reasonBytes = new TextEncoder().encode(record.reason ?? "");
|
|
3797
|
-
const total = REVOCATION_MAGIC.length + 1 + 4 + issuerBytes.length + 4 + revokedBytes.length + 8 + 4 + reasonBytes.length;
|
|
3798
|
-
const out = new Uint8Array(total);
|
|
3799
|
-
let offset = 0;
|
|
3800
|
-
out.set(REVOCATION_MAGIC, offset);
|
|
3801
|
-
offset += REVOCATION_MAGIC.length;
|
|
3802
|
-
out[offset] = record.version;
|
|
3803
|
-
offset += 1;
|
|
3804
|
-
const view = new DataView(out.buffer);
|
|
3805
|
-
view.setUint32(offset, issuerBytes.length, false);
|
|
3806
|
-
offset += 4;
|
|
3807
|
-
out.set(issuerBytes, offset);
|
|
3808
|
-
offset += issuerBytes.length;
|
|
3809
|
-
view.setUint32(offset, revokedBytes.length, false);
|
|
3810
|
-
offset += 4;
|
|
3811
|
-
out.set(revokedBytes, offset);
|
|
3812
|
-
offset += revokedBytes.length;
|
|
3813
|
-
view.setBigUint64(offset, BigInt(record.issuedAt), false);
|
|
3814
|
-
offset += 8;
|
|
3815
|
-
view.setUint32(offset, reasonBytes.length, false);
|
|
3816
|
-
offset += 4;
|
|
3817
|
-
out.set(reasonBytes, offset);
|
|
3818
|
-
return out;
|
|
3819
|
-
}
|
|
3820
|
-
function parseRevocationPayload(bytes) {
|
|
3821
|
-
let offset = 0;
|
|
3822
|
-
if (bytes.length < REVOCATION_MAGIC.length) {
|
|
3823
|
-
throw new RevocationError("Revocation record too short for magic.", "truncated");
|
|
3824
|
-
}
|
|
3825
|
-
for (let i = 0;i < REVOCATION_MAGIC.length; i++) {
|
|
3826
|
-
if (bytes[offset + i] !== REVOCATION_MAGIC[i]) {
|
|
3827
|
-
throw new RevocationError("Revocation record magic mismatch.", "wrong-magic");
|
|
3828
|
-
}
|
|
3829
|
-
}
|
|
3830
|
-
offset += REVOCATION_MAGIC.length;
|
|
3831
|
-
if (bytes.length < offset + 1) {
|
|
3832
|
-
throw new RevocationError("Revocation record truncated at version.", "truncated");
|
|
3833
|
-
}
|
|
3834
|
-
const version = bytes[offset];
|
|
3835
|
-
offset += 1;
|
|
3836
|
-
if (version !== REVOCATION_RECORD_VERSION) {
|
|
3837
|
-
throw new RevocationError(`Unknown revocation record version: ${version}.`, "unknown-version");
|
|
3838
|
-
}
|
|
3839
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
3840
|
-
if (bytes.length < offset + 4) {
|
|
3841
|
-
throw new RevocationError("Revocation record truncated at issuer length.", "truncated");
|
|
3842
|
-
}
|
|
3843
|
-
const issuerLen = view.getUint32(offset, false);
|
|
3844
|
-
offset += 4;
|
|
3845
|
-
if (bytes.length < offset + issuerLen) {
|
|
3846
|
-
throw new RevocationError("Revocation record truncated at issuer id.", "truncated");
|
|
3847
|
-
}
|
|
3848
|
-
const issuerPeerId = new TextDecoder().decode(bytes.subarray(offset, offset + issuerLen));
|
|
3849
|
-
offset += issuerLen;
|
|
3850
|
-
if (bytes.length < offset + 4) {
|
|
3851
|
-
throw new RevocationError("Revocation record truncated at revoked id length.", "truncated");
|
|
3852
|
-
}
|
|
3853
|
-
const revokedLen = view.getUint32(offset, false);
|
|
3854
|
-
offset += 4;
|
|
3855
|
-
if (bytes.length < offset + revokedLen) {
|
|
3856
|
-
throw new RevocationError("Revocation record truncated at revoked id.", "truncated");
|
|
3857
|
-
}
|
|
3858
|
-
const revokedPeerId = new TextDecoder().decode(bytes.subarray(offset, offset + revokedLen));
|
|
3859
|
-
offset += revokedLen;
|
|
3860
|
-
if (bytes.length < offset + 8) {
|
|
3861
|
-
throw new RevocationError("Revocation record truncated at issuedAt.", "truncated");
|
|
3862
|
-
}
|
|
3863
|
-
const issuedAt = Number(view.getBigUint64(offset, false));
|
|
3864
|
-
offset += 8;
|
|
3865
|
-
if (bytes.length < offset + 4) {
|
|
3866
|
-
throw new RevocationError("Revocation record truncated at reason length.", "truncated");
|
|
3867
|
-
}
|
|
3868
|
-
const reasonLen = view.getUint32(offset, false);
|
|
3869
|
-
offset += 4;
|
|
3870
|
-
if (bytes.length < offset + reasonLen) {
|
|
3871
|
-
throw new RevocationError("Revocation record truncated at reason.", "truncated");
|
|
3872
|
-
}
|
|
3873
|
-
const reason = new TextDecoder().decode(bytes.subarray(offset, offset + reasonLen));
|
|
3874
|
-
offset += reasonLen;
|
|
3875
|
-
return {
|
|
3876
|
-
version,
|
|
3877
|
-
issuerPeerId,
|
|
3878
|
-
revokedPeerId,
|
|
3879
|
-
issuedAt,
|
|
3880
|
-
...reason ? { reason } : {}
|
|
3881
|
-
};
|
|
3882
|
-
}
|
|
3883
|
-
function encodeRevocation(record, issuer) {
|
|
3884
|
-
const payload = serialiseRevocationPayload(record);
|
|
3885
|
-
const envelope = signEnvelope(payload, record.issuerPeerId, issuer.secretKey);
|
|
3886
|
-
return encodeSignedEnvelope(envelope);
|
|
3887
|
-
}
|
|
3888
|
-
function decodeRevocation(bytes, keyring) {
|
|
3889
|
-
const envelope = decodeSignedEnvelope(bytes);
|
|
3890
|
-
const issuerKey = keyring.knownPeers.get(envelope.senderId);
|
|
3891
|
-
if (!issuerKey) {
|
|
3892
|
-
throw new RevocationError(`Revocation issuer ${envelope.senderId} is not in the local keyring.`, "unknown-issuer");
|
|
3893
|
-
}
|
|
3894
|
-
if (keyring.revocationAuthority !== undefined && keyring.revocationAuthority.size > 0 && !keyring.revocationAuthority.has(envelope.senderId)) {
|
|
3895
|
-
throw new RevocationError(`Revocation issuer ${envelope.senderId} is not in the keyring's revocation authority set.`, "unauthorised-issuer");
|
|
3896
|
-
}
|
|
3897
|
-
let payloadBytes;
|
|
3898
|
-
try {
|
|
3899
|
-
payloadBytes = openEnvelope2(envelope, issuerKey);
|
|
3900
|
-
} catch {
|
|
3901
|
-
throw new RevocationError(`Revocation signature failed verification for issuer ${envelope.senderId}.`, "signature-invalid");
|
|
3902
|
-
}
|
|
3903
|
-
const record = parseRevocationPayload(payloadBytes);
|
|
3904
|
-
if (record.issuerPeerId !== envelope.senderId) {
|
|
3905
|
-
throw new RevocationError(`Revocation payload claims issuer ${record.issuerPeerId} but the envelope was signed by ${envelope.senderId}.`, "not-signed-by-issuer");
|
|
3906
|
-
}
|
|
3907
|
-
return record;
|
|
3908
|
-
}
|
|
3909
4217
|
export {
|
|
3910
4218
|
wasMeshStateResolved,
|
|
3911
4219
|
verify,
|
|
@@ -3982,4 +4290,4 @@ export {
|
|
|
3982
4290
|
$meshCounter
|
|
3983
4291
|
};
|
|
3984
4292
|
|
|
3985
|
-
//# debugId=
|
|
4293
|
+
//# debugId=E57D210FDE704BCC64756E2164756E21
|