@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.
Files changed (35) hide show
  1. package/dist/src/client/index.js +17 -20
  2. package/dist/src/client/index.js.map +4 -4
  3. package/dist/src/mesh.js +462 -154
  4. package/dist/src/mesh.js.map +9 -7
  5. package/dist/src/peer.js +153 -9
  6. package/dist/src/peer.js.map +5 -4
  7. package/dist/src/polly-ui/markdown.js +3 -3
  8. package/dist/src/polly-ui/markdown.js.map +2 -2
  9. package/dist/src/shared/lib/mesh-client.d.ts +29 -0
  10. package/dist/src/shared/lib/mesh-diagnostics.d.ts +129 -0
  11. package/dist/src/shared/lib/mesh-network-adapter.d.ts +90 -3
  12. package/dist/src/shared/lib/revocation-summary.d.ts +54 -0
  13. package/dist/tools/test/src/e2e-mesh/console-allowlist.d.ts +31 -0
  14. package/dist/tools/test/src/e2e-mesh/index.d.ts +27 -0
  15. package/dist/tools/test/src/e2e-mesh/index.js +1183 -0
  16. package/dist/tools/test/src/e2e-mesh/index.js.map +22 -0
  17. package/dist/tools/test/src/e2e-mesh/keys.d.ts +55 -0
  18. package/dist/tools/test/src/e2e-mesh/launch-peer.d.ts +98 -0
  19. package/dist/tools/test/src/e2e-mesh/mesh-assertions.d.ts +53 -0
  20. package/dist/tools/test/src/e2e-mesh/serve-consumer.d.ts +32 -0
  21. package/dist/tools/test/src/e2e-mesh/wait-for-convergence.d.ts +38 -0
  22. package/dist/tools/test/src/e2e-mesh/with-relay.d.ts +53 -0
  23. package/dist/tools/test/src/visual/index.js +24 -24
  24. package/dist/tools/test/src/visual/index.js.map +2 -2
  25. package/dist/tools/verify/src/cli.js +361 -22
  26. package/dist/tools/verify/src/cli.js.map +6 -6
  27. package/dist/tools/verify/src/config.d.ts +26 -1
  28. package/dist/tools/verify/src/config.js +9 -1
  29. package/dist/tools/verify/src/config.js.map +4 -4
  30. package/dist/tools/verify/src/primitives/index.d.ts +30 -0
  31. package/dist/tools/visualize/src/cli.js +43 -1
  32. package/dist/tools/visualize/src/cli.js.map +3 -3
  33. package/package.json +11 -8
  34. package/LICENSE +0 -21
  35. 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(serialised, DEFAULT_MESH_KEY_ID, docKey);
1153
+ const encrypted = sealEnvelope(tagged, DEFAULT_MESH_KEY_ID, docKey);
1122
1154
  payloadToSign = encodeEncryptedEnvelope(encrypted);
1123
1155
  } else {
1124
- payloadToSign = serialised;
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 deserialiseMessage(verifiedPayload);
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 deserialiseMessage(plaintext);
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=516309C916BB154F64756E2164756E21
4293
+ //# debugId=E57D210FDE704BCC64756E2164756E21