@fairfox/polly 0.70.0 → 0.72.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 (38) hide show
  1. package/dist/src/client/index.js +2 -2
  2. package/dist/src/client/index.js.map +2 -2
  3. package/dist/src/mesh.js +407 -156
  4. package/dist/src/mesh.js.map +9 -8
  5. package/dist/src/peer.js +92 -5
  6. package/dist/src/peer.js.map +4 -4
  7. package/dist/src/polly-ui/ActionInput.d.ts +10 -1
  8. package/dist/src/polly-ui/ActionSelect.d.ts +35 -0
  9. package/dist/src/polly-ui/Cluster.d.ts +35 -0
  10. package/dist/src/polly-ui/Code.d.ts +17 -0
  11. package/dist/src/polly-ui/Text.d.ts +31 -0
  12. package/dist/src/polly-ui/index.css +278 -185
  13. package/dist/src/polly-ui/index.d.ts +5 -1
  14. package/dist/src/polly-ui/index.js +480 -284
  15. package/dist/src/polly-ui/index.js.map +11 -6
  16. package/dist/src/polly-ui/internal/dispatch-action.d.ts +13 -0
  17. package/dist/src/polly-ui/markdown.js +3 -3
  18. package/dist/src/polly-ui/markdown.js.map +2 -2
  19. package/dist/src/polly-ui/styles.css +288 -185
  20. package/dist/src/shared/lib/mesh-client.d.ts +29 -0
  21. package/dist/src/shared/lib/mesh-diagnostics.d.ts +31 -0
  22. package/dist/src/shared/lib/mesh-network-adapter.d.ts +91 -0
  23. package/dist/src/shared/lib/revocation-summary.d.ts +54 -0
  24. package/dist/tools/quality/src/cli.js +6 -2
  25. package/dist/tools/quality/src/cli.js.map +3 -3
  26. package/dist/tools/quality/src/index.js +6 -2
  27. package/dist/tools/quality/src/index.js.map +3 -3
  28. package/dist/tools/test/src/browser/run.js +75 -44
  29. package/dist/tools/test/src/browser/run.js.map +3 -3
  30. package/dist/tools/test/src/e2e-mesh/index.d.ts +1 -1
  31. package/dist/tools/test/src/e2e-mesh/index.js +95 -1
  32. package/dist/tools/test/src/e2e-mesh/index.js.map +4 -4
  33. package/dist/tools/test/src/e2e-mesh/launch-peer.d.ts +29 -1
  34. package/dist/tools/test/src/visual/index.js +24 -24
  35. package/dist/tools/test/src/visual/index.js.map +2 -2
  36. package/dist/tools/verify/src/cli.js +82 -51
  37. package/dist/tools/verify/src/cli.js.map +7 -6
  38. package/package.json +2 -2
package/dist/src/mesh.js CHANGED
@@ -975,12 +975,6 @@ 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-network-adapter.ts
979
- init_encryption();
980
- import {
981
- NetworkAdapter
982
- } from "@automerge/automerge-repo/slim";
983
-
984
978
  // src/shared/lib/mesh-diagnostics.ts
985
979
  var listeners = new Set;
986
980
  function emitMeshDiagnostic(diagnostic) {
@@ -1005,6 +999,12 @@ function recordMeshDiagnostics() {
1005
999
  return { events: captured, stop };
1006
1000
  }
1007
1001
 
1002
+ // src/shared/lib/mesh-network-adapter.ts
1003
+ init_encryption();
1004
+ import {
1005
+ NetworkAdapter
1006
+ } from "@automerge/automerge-repo/slim";
1007
+
1008
1008
  // src/shared/lib/signing.ts
1009
1009
  import nacl2 from "tweetnacl";
1010
1010
  var PUBLIC_KEY_BYTES = 32;
@@ -1090,11 +1090,17 @@ function decodeSignedEnvelope(bytes) {
1090
1090
 
1091
1091
  // src/shared/lib/mesh-network-adapter.ts
1092
1092
  var DEFAULT_MESH_KEY_ID = "polly-mesh-default";
1093
+ var MESH_CONTROL_TYPE = {
1094
+ Sync: 0,
1095
+ Revocation: 1,
1096
+ RevocationSummary: 2
1097
+ };
1093
1098
 
1094
1099
  class MeshNetworkAdapter extends NetworkAdapter {
1095
1100
  base;
1096
1101
  keyringSource;
1097
1102
  encryptionEnabled;
1103
+ onControlMessage;
1098
1104
  get keyring() {
1099
1105
  return this.keyringSource();
1100
1106
  }
@@ -1103,6 +1109,7 @@ class MeshNetworkAdapter extends NetworkAdapter {
1103
1109
  this.base = options.base;
1104
1110
  this.keyringSource = options.keyringSource;
1105
1111
  this.encryptionEnabled = options.encryptionEnabled ?? true;
1112
+ this.onControlMessage = options.onControlMessage;
1106
1113
  this.base.on("close", () => this.emit("close"));
1107
1114
  this.base.on("peer-candidate", (payload) => this.emit("peer-candidate", payload));
1108
1115
  this.base.on("peer-disconnected", (payload) => this.emit("peer-disconnected", payload));
@@ -1136,16 +1143,17 @@ class MeshNetworkAdapter extends NetworkAdapter {
1136
1143
  wrap(message) {
1137
1144
  const keyring = this.keyringSource();
1138
1145
  const serialised = serialiseMessage(message);
1146
+ const tagged = prependControlTag(MESH_CONTROL_TYPE.Sync, serialised);
1139
1147
  let payloadToSign;
1140
1148
  if (this.encryptionEnabled) {
1141
1149
  const docKey = keyring.documentKeys.get(DEFAULT_MESH_KEY_ID);
1142
1150
  if (!docKey) {
1143
1151
  throw new Error(`MeshNetworkAdapter: missing document encryption key under id "${DEFAULT_MESH_KEY_ID}". Provision the key in the keyring before sending.`);
1144
1152
  }
1145
- const encrypted = sealEnvelope(serialised, DEFAULT_MESH_KEY_ID, docKey);
1153
+ const encrypted = sealEnvelope(tagged, DEFAULT_MESH_KEY_ID, docKey);
1146
1154
  payloadToSign = encodeEncryptedEnvelope(encrypted);
1147
1155
  } else {
1148
- payloadToSign = serialised;
1156
+ payloadToSign = tagged;
1149
1157
  }
1150
1158
  const signed = signEnvelope(payloadToSign, message.senderId, keyring.identity.secretKey);
1151
1159
  const signedBytes = encodeSignedEnvelope(signed);
@@ -1201,7 +1209,7 @@ class MeshNetworkAdapter extends NetworkAdapter {
1201
1209
  return;
1202
1210
  }
1203
1211
  if (!this.encryptionEnabled) {
1204
- return deserialiseMessage(verifiedPayload);
1212
+ return this.dispatchTaggedPayload(verifiedPayload, signed.senderId);
1205
1213
  }
1206
1214
  let encrypted;
1207
1215
  try {
@@ -1235,9 +1243,88 @@ class MeshNetworkAdapter extends NetworkAdapter {
1235
1243
  });
1236
1244
  return;
1237
1245
  }
1238
- 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
+ }
1239
1320
  }
1240
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
+ }
1241
1328
  function serialiseMessage(message) {
1242
1329
  const headerObj = {
1243
1330
  type: message.type,
@@ -3353,6 +3440,193 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
3353
3440
  }
3354
3441
  }
3355
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
+
3356
3630
  // src/shared/lib/mesh-client.ts
3357
3631
  async function resolveIceServers(rtc) {
3358
3632
  if (rtc?.iceCredentialResolver) {
@@ -3539,10 +3813,107 @@ async function createMeshClient(options) {
3539
3813
  });
3540
3814
  webrtcAdapterOptions.signaling = signaling;
3541
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
+ };
3542
3886
  const networkAdapter = new MeshNetworkAdapter({
3543
3887
  base: webrtcAdapter,
3544
3888
  keyringSource,
3545
- 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);
3546
3917
  });
3547
3918
  const repo = new Repo({
3548
3919
  network: [networkAdapter],
@@ -3609,6 +3980,30 @@ async function createMeshClient(options) {
3609
3980
  return;
3610
3981
  await webrtcAdapter.refreshAllTransportStats();
3611
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
+ },
3612
4007
  close: async () => {
3613
4008
  signaling.close();
3614
4009
  webrtcAdapter?.disconnect();
@@ -3819,150 +4214,6 @@ function randomBytes(n) {
3819
4214
  crypto.getRandomValues(out);
3820
4215
  return out;
3821
4216
  }
3822
- // src/shared/lib/revocation.ts
3823
- var REVOCATION_RECORD_VERSION = 1;
3824
- var REVOCATION_MAGIC = new Uint8Array([80, 82, 86, 49]);
3825
-
3826
- class RevocationError extends Error {
3827
- code;
3828
- constructor(message, code) {
3829
- super(message);
3830
- this.name = "RevocationError";
3831
- this.code = code;
3832
- }
3833
- }
3834
- function createRevocation(options) {
3835
- const now = options.now ? options.now() : Date.now();
3836
- return {
3837
- version: REVOCATION_RECORD_VERSION,
3838
- issuerPeerId: options.issuerPeerId,
3839
- revokedPeerId: options.revokedPeerId,
3840
- issuedAt: now,
3841
- ...options.reason === undefined ? {} : { reason: options.reason }
3842
- };
3843
- }
3844
- function applyRevocation(record, keyring) {
3845
- keyring.revokedPeers.add(record.revokedPeerId);
3846
- }
3847
- function revokePeerLocally(peerId, keyring) {
3848
- keyring.revokedPeers.add(peerId);
3849
- }
3850
- function serialiseRevocationPayload(record) {
3851
- const issuerBytes = new TextEncoder().encode(record.issuerPeerId);
3852
- const revokedBytes = new TextEncoder().encode(record.revokedPeerId);
3853
- const reasonBytes = new TextEncoder().encode(record.reason ?? "");
3854
- const total = REVOCATION_MAGIC.length + 1 + 4 + issuerBytes.length + 4 + revokedBytes.length + 8 + 4 + reasonBytes.length;
3855
- const out = new Uint8Array(total);
3856
- let offset = 0;
3857
- out.set(REVOCATION_MAGIC, offset);
3858
- offset += REVOCATION_MAGIC.length;
3859
- out[offset] = record.version;
3860
- offset += 1;
3861
- const view = new DataView(out.buffer);
3862
- view.setUint32(offset, issuerBytes.length, false);
3863
- offset += 4;
3864
- out.set(issuerBytes, offset);
3865
- offset += issuerBytes.length;
3866
- view.setUint32(offset, revokedBytes.length, false);
3867
- offset += 4;
3868
- out.set(revokedBytes, offset);
3869
- offset += revokedBytes.length;
3870
- view.setBigUint64(offset, BigInt(record.issuedAt), false);
3871
- offset += 8;
3872
- view.setUint32(offset, reasonBytes.length, false);
3873
- offset += 4;
3874
- out.set(reasonBytes, offset);
3875
- return out;
3876
- }
3877
- function parseRevocationPayload(bytes) {
3878
- let offset = 0;
3879
- if (bytes.length < REVOCATION_MAGIC.length) {
3880
- throw new RevocationError("Revocation record too short for magic.", "truncated");
3881
- }
3882
- for (let i = 0;i < REVOCATION_MAGIC.length; i++) {
3883
- if (bytes[offset + i] !== REVOCATION_MAGIC[i]) {
3884
- throw new RevocationError("Revocation record magic mismatch.", "wrong-magic");
3885
- }
3886
- }
3887
- offset += REVOCATION_MAGIC.length;
3888
- if (bytes.length < offset + 1) {
3889
- throw new RevocationError("Revocation record truncated at version.", "truncated");
3890
- }
3891
- const version = bytes[offset];
3892
- offset += 1;
3893
- if (version !== REVOCATION_RECORD_VERSION) {
3894
- throw new RevocationError(`Unknown revocation record version: ${version}.`, "unknown-version");
3895
- }
3896
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
3897
- if (bytes.length < offset + 4) {
3898
- throw new RevocationError("Revocation record truncated at issuer length.", "truncated");
3899
- }
3900
- const issuerLen = view.getUint32(offset, false);
3901
- offset += 4;
3902
- if (bytes.length < offset + issuerLen) {
3903
- throw new RevocationError("Revocation record truncated at issuer id.", "truncated");
3904
- }
3905
- const issuerPeerId = new TextDecoder().decode(bytes.subarray(offset, offset + issuerLen));
3906
- offset += issuerLen;
3907
- if (bytes.length < offset + 4) {
3908
- throw new RevocationError("Revocation record truncated at revoked id length.", "truncated");
3909
- }
3910
- const revokedLen = view.getUint32(offset, false);
3911
- offset += 4;
3912
- if (bytes.length < offset + revokedLen) {
3913
- throw new RevocationError("Revocation record truncated at revoked id.", "truncated");
3914
- }
3915
- const revokedPeerId = new TextDecoder().decode(bytes.subarray(offset, offset + revokedLen));
3916
- offset += revokedLen;
3917
- if (bytes.length < offset + 8) {
3918
- throw new RevocationError("Revocation record truncated at issuedAt.", "truncated");
3919
- }
3920
- const issuedAt = Number(view.getBigUint64(offset, false));
3921
- offset += 8;
3922
- if (bytes.length < offset + 4) {
3923
- throw new RevocationError("Revocation record truncated at reason length.", "truncated");
3924
- }
3925
- const reasonLen = view.getUint32(offset, false);
3926
- offset += 4;
3927
- if (bytes.length < offset + reasonLen) {
3928
- throw new RevocationError("Revocation record truncated at reason.", "truncated");
3929
- }
3930
- const reason = new TextDecoder().decode(bytes.subarray(offset, offset + reasonLen));
3931
- offset += reasonLen;
3932
- return {
3933
- version,
3934
- issuerPeerId,
3935
- revokedPeerId,
3936
- issuedAt,
3937
- ...reason ? { reason } : {}
3938
- };
3939
- }
3940
- function encodeRevocation(record, issuer) {
3941
- const payload = serialiseRevocationPayload(record);
3942
- const envelope = signEnvelope(payload, record.issuerPeerId, issuer.secretKey);
3943
- return encodeSignedEnvelope(envelope);
3944
- }
3945
- function decodeRevocation(bytes, keyring) {
3946
- const envelope = decodeSignedEnvelope(bytes);
3947
- const issuerKey = keyring.knownPeers.get(envelope.senderId);
3948
- if (!issuerKey) {
3949
- throw new RevocationError(`Revocation issuer ${envelope.senderId} is not in the local keyring.`, "unknown-issuer");
3950
- }
3951
- if (keyring.revocationAuthority !== undefined && keyring.revocationAuthority.size > 0 && !keyring.revocationAuthority.has(envelope.senderId)) {
3952
- throw new RevocationError(`Revocation issuer ${envelope.senderId} is not in the keyring's revocation authority set.`, "unauthorised-issuer");
3953
- }
3954
- let payloadBytes;
3955
- try {
3956
- payloadBytes = openEnvelope2(envelope, issuerKey);
3957
- } catch {
3958
- throw new RevocationError(`Revocation signature failed verification for issuer ${envelope.senderId}.`, "signature-invalid");
3959
- }
3960
- const record = parseRevocationPayload(payloadBytes);
3961
- if (record.issuerPeerId !== envelope.senderId) {
3962
- throw new RevocationError(`Revocation payload claims issuer ${record.issuerPeerId} but the envelope was signed by ${envelope.senderId}.`, "not-signed-by-issuer");
3963
- }
3964
- return record;
3965
- }
3966
4217
  export {
3967
4218
  wasMeshStateResolved,
3968
4219
  verify,
@@ -4039,4 +4290,4 @@ export {
4039
4290
  $meshCounter
4040
4291
  };
4041
4292
 
4042
- //# debugId=3048B484420F5AE864756E2164756E21
4293
+ //# debugId=E57D210FDE704BCC64756E2164756E21