@voidly/agent-sdk 2.2.0 → 3.0.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/index.d.mts +112 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +478 -70
- package/dist/index.mjs +478 -70
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -3996,6 +3996,8 @@ var FLAG_PADDED = 1;
|
|
|
3996
3996
|
var FLAG_SEALED = 2;
|
|
3997
3997
|
var FLAG_RATCHET = 4;
|
|
3998
3998
|
var FLAG_PQ = 8;
|
|
3999
|
+
var FLAG_DH_RATCHET = 16;
|
|
4000
|
+
var FLAG_DENIABLE = 32;
|
|
3999
4001
|
function makeProtoHeader(flags, ratchetStep2) {
|
|
4000
4002
|
return new Uint8Array([PROTO_MARKER, flags, ratchetStep2 >> 8 & 255, ratchetStep2 & 255]);
|
|
4001
4003
|
}
|
|
@@ -4007,6 +4009,30 @@ function parseProtoHeader(data) {
|
|
|
4007
4009
|
content: data.slice(4)
|
|
4008
4010
|
};
|
|
4009
4011
|
}
|
|
4012
|
+
async function kdfRK(rootKey, dhOutput) {
|
|
4013
|
+
const combined = new Uint8Array(rootKey.length + dhOutput.length);
|
|
4014
|
+
combined.set(rootKey, 0);
|
|
4015
|
+
combined.set(dhOutput, rootKey.length);
|
|
4016
|
+
if (typeof globalThis.crypto?.subtle !== "undefined") {
|
|
4017
|
+
const prk2 = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", combined.buffer));
|
|
4018
|
+
const newRootKey2 = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", new Uint8Array([...prk2, 1]).buffer));
|
|
4019
|
+
const newChainKey2 = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", new Uint8Array([...prk2, 2]).buffer));
|
|
4020
|
+
return { newRootKey: newRootKey2, newChainKey: newChainKey2 };
|
|
4021
|
+
}
|
|
4022
|
+
const { createHash } = await import("crypto");
|
|
4023
|
+
const prk = new Uint8Array(createHash("sha256").update(Buffer.from(combined)).digest());
|
|
4024
|
+
const newRootKey = new Uint8Array(createHash("sha256").update(Buffer.from([...prk, 1])).digest());
|
|
4025
|
+
const newChainKey = new Uint8Array(createHash("sha256").update(Buffer.from([...prk, 2])).digest());
|
|
4026
|
+
return { newRootKey, newChainKey };
|
|
4027
|
+
}
|
|
4028
|
+
async function hmacSha256(key, data) {
|
|
4029
|
+
if (typeof globalThis.crypto?.subtle !== "undefined") {
|
|
4030
|
+
const cryptoKey = await globalThis.crypto.subtle.importKey("raw", key.buffer, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
4031
|
+
return new Uint8Array(await globalThis.crypto.subtle.sign("HMAC", cryptoKey, data.buffer));
|
|
4032
|
+
}
|
|
4033
|
+
const { createHmac } = await import("crypto");
|
|
4034
|
+
return new Uint8Array(createHmac("sha256", Buffer.from(key)).update(Buffer.from(data)).digest());
|
|
4035
|
+
}
|
|
4010
4036
|
var MAX_SKIP = 200;
|
|
4011
4037
|
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
4012
4038
|
function toBase58(bytes) {
|
|
@@ -4026,6 +4052,8 @@ function toBase58(bytes) {
|
|
|
4026
4052
|
}
|
|
4027
4053
|
var VoidlyAgent = class _VoidlyAgent {
|
|
4028
4054
|
constructor(identity, config) {
|
|
4055
|
+
this._signedPrekey = null;
|
|
4056
|
+
this._signedPrekeyId = 0;
|
|
4029
4057
|
this._pinnedDids = /* @__PURE__ */ new Set();
|
|
4030
4058
|
this._listeners = /* @__PURE__ */ new Set();
|
|
4031
4059
|
this._conversations = /* @__PURE__ */ new Map();
|
|
@@ -4047,6 +4075,10 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4047
4075
|
this.requireSignatures = config?.requireSignatures || false;
|
|
4048
4076
|
this.timeout = config?.timeout ?? 3e4;
|
|
4049
4077
|
this.postQuantum = config?.postQuantum !== false;
|
|
4078
|
+
this.deniable = config?.deniable || false;
|
|
4079
|
+
this.doubleRatchet = config?.doubleRatchet !== false;
|
|
4080
|
+
this.jitterMs = config?.jitterMs || 0;
|
|
4081
|
+
this.longPoll = config?.longPoll !== false;
|
|
4050
4082
|
this.mlkemPublicKey = identity.mlkemPublicKey || null;
|
|
4051
4083
|
this.mlkemSecretKey = identity.mlkemSecretKey || null;
|
|
4052
4084
|
}
|
|
@@ -4066,11 +4098,18 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4066
4098
|
const kem = new MlKem768();
|
|
4067
4099
|
[mlkemPk, mlkemSk] = await kem.generateKeyPair();
|
|
4068
4100
|
}
|
|
4101
|
+
const signedPrekeyPair = import_tweetnacl.default.box.keyPair();
|
|
4102
|
+
const signedPrekeyId = 1;
|
|
4103
|
+
const prekeySignature = import_tweetnacl.default.sign.detached(signedPrekeyPair.publicKey, signingKeyPair.secretKey);
|
|
4069
4104
|
const regBody = {
|
|
4070
4105
|
name: options.name,
|
|
4071
4106
|
capabilities: options.capabilities,
|
|
4072
4107
|
signing_public_key: (0, import_tweetnacl_util.encodeBase64)(signingKeyPair.publicKey),
|
|
4073
|
-
encryption_public_key: (0, import_tweetnacl_util.encodeBase64)(encryptionKeyPair.publicKey)
|
|
4108
|
+
encryption_public_key: (0, import_tweetnacl_util.encodeBase64)(encryptionKeyPair.publicKey),
|
|
4109
|
+
// X3DH signed prekey
|
|
4110
|
+
signed_prekey_public: (0, import_tweetnacl_util.encodeBase64)(signedPrekeyPair.publicKey),
|
|
4111
|
+
signed_prekey_signature: (0, import_tweetnacl_util.encodeBase64)(prekeySignature),
|
|
4112
|
+
signed_prekey_id: signedPrekeyId
|
|
4074
4113
|
};
|
|
4075
4114
|
if (mlkemPk) {
|
|
4076
4115
|
regBody.mlkem_public_key = (0, import_tweetnacl_util.encodeBase64)(mlkemPk);
|
|
@@ -4086,7 +4125,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4086
4125
|
throw new Error(`Registration failed: ${errMsg}`);
|
|
4087
4126
|
}
|
|
4088
4127
|
const data = await res.json();
|
|
4089
|
-
|
|
4128
|
+
const agent = new _VoidlyAgent({
|
|
4090
4129
|
did: data.did,
|
|
4091
4130
|
apiKey: data.api_key,
|
|
4092
4131
|
signingKeyPair,
|
|
@@ -4094,6 +4133,9 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4094
4133
|
mlkemPublicKey: mlkemPk,
|
|
4095
4134
|
mlkemSecretKey: mlkemSk
|
|
4096
4135
|
}, config);
|
|
4136
|
+
agent._signedPrekey = signedPrekeyPair;
|
|
4137
|
+
agent._signedPrekeyId = signedPrekeyId;
|
|
4138
|
+
return agent;
|
|
4097
4139
|
}
|
|
4098
4140
|
/**
|
|
4099
4141
|
* Restore an agent from saved credentials.
|
|
@@ -4153,17 +4195,55 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4153
4195
|
const sendChainKey = (0, import_tweetnacl_util.decodeBase64)(rs.sendChainKey);
|
|
4154
4196
|
const recvChainKey = (0, import_tweetnacl_util.decodeBase64)(rs.recvChainKey);
|
|
4155
4197
|
if (sendChainKey.length !== 32 || recvChainKey.length !== 32) continue;
|
|
4156
|
-
|
|
4198
|
+
const state = {
|
|
4157
4199
|
sendChainKey,
|
|
4158
4200
|
sendStep: rs.sendStep || 0,
|
|
4159
4201
|
recvChainKey,
|
|
4160
4202
|
recvStep: rs.recvStep || 0,
|
|
4161
4203
|
skippedKeys: /* @__PURE__ */ new Map()
|
|
4162
|
-
}
|
|
4204
|
+
};
|
|
4205
|
+
if (rs.rootKey) {
|
|
4206
|
+
try {
|
|
4207
|
+
state.rootKey = (0, import_tweetnacl_util.decodeBase64)(rs.rootKey);
|
|
4208
|
+
if (state.rootKey.length !== 32) state.rootKey = void 0;
|
|
4209
|
+
} catch {
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
if (rs.dhSendSecretKey && rs.dhSendPublicKey) {
|
|
4213
|
+
try {
|
|
4214
|
+
const sk = (0, import_tweetnacl_util.decodeBase64)(rs.dhSendSecretKey);
|
|
4215
|
+
const pk = (0, import_tweetnacl_util.decodeBase64)(rs.dhSendPublicKey);
|
|
4216
|
+
if (sk.length === 32 && pk.length === 32) {
|
|
4217
|
+
state.dhSendKeyPair = { publicKey: pk, secretKey: sk };
|
|
4218
|
+
}
|
|
4219
|
+
} catch {
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
if (rs.dhRecvPubKey) {
|
|
4223
|
+
try {
|
|
4224
|
+
const pk = (0, import_tweetnacl_util.decodeBase64)(rs.dhRecvPubKey);
|
|
4225
|
+
if (pk.length === 32) state.dhRecvPubKey = pk;
|
|
4226
|
+
} catch {
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
if (rs.prevSendStep !== void 0) state.prevSendStep = rs.prevSendStep;
|
|
4230
|
+
state.dhSkippedKeys = /* @__PURE__ */ new Map();
|
|
4231
|
+
agent._ratchetStates.set(pairId, state);
|
|
4163
4232
|
} catch {
|
|
4164
4233
|
}
|
|
4165
4234
|
}
|
|
4166
4235
|
}
|
|
4236
|
+
if (creds.signedPrekeySecret && creds.signedPrekeyPublic) {
|
|
4237
|
+
try {
|
|
4238
|
+
const sk = (0, import_tweetnacl_util.decodeBase64)(creds.signedPrekeySecret);
|
|
4239
|
+
const pk = (0, import_tweetnacl_util.decodeBase64)(creds.signedPrekeyPublic);
|
|
4240
|
+
if (sk.length === 32 && pk.length === 32) {
|
|
4241
|
+
agent._signedPrekey = { publicKey: pk, secretKey: sk };
|
|
4242
|
+
agent._signedPrekeyId = creds.signedPrekeyId || 0;
|
|
4243
|
+
}
|
|
4244
|
+
} catch {
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4167
4247
|
return agent;
|
|
4168
4248
|
}
|
|
4169
4249
|
/**
|
|
@@ -4173,12 +4253,20 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4173
4253
|
exportCredentials() {
|
|
4174
4254
|
const ratchetStates = {};
|
|
4175
4255
|
for (const [pairId, state] of this._ratchetStates) {
|
|
4176
|
-
|
|
4256
|
+
const rs = {
|
|
4177
4257
|
sendChainKey: (0, import_tweetnacl_util.encodeBase64)(state.sendChainKey),
|
|
4178
4258
|
sendStep: state.sendStep,
|
|
4179
4259
|
recvChainKey: (0, import_tweetnacl_util.encodeBase64)(state.recvChainKey),
|
|
4180
4260
|
recvStep: state.recvStep
|
|
4181
4261
|
};
|
|
4262
|
+
if (state.rootKey) rs.rootKey = (0, import_tweetnacl_util.encodeBase64)(state.rootKey);
|
|
4263
|
+
if (state.dhSendKeyPair) {
|
|
4264
|
+
rs.dhSendSecretKey = (0, import_tweetnacl_util.encodeBase64)(state.dhSendKeyPair.secretKey);
|
|
4265
|
+
rs.dhSendPublicKey = (0, import_tweetnacl_util.encodeBase64)(state.dhSendKeyPair.publicKey);
|
|
4266
|
+
}
|
|
4267
|
+
if (state.dhRecvPubKey) rs.dhRecvPubKey = (0, import_tweetnacl_util.encodeBase64)(state.dhRecvPubKey);
|
|
4268
|
+
if (state.prevSendStep !== void 0) rs.prevSendStep = state.prevSendStep;
|
|
4269
|
+
ratchetStates[pairId] = rs;
|
|
4182
4270
|
}
|
|
4183
4271
|
return {
|
|
4184
4272
|
did: this.did,
|
|
@@ -4189,7 +4277,12 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4189
4277
|
encryptionPublicKey: (0, import_tweetnacl_util.encodeBase64)(this.encryptionKeyPair.publicKey),
|
|
4190
4278
|
...Object.keys(ratchetStates).length > 0 ? { ratchetStates } : {},
|
|
4191
4279
|
...this.mlkemPublicKey ? { mlkemPublicKey: (0, import_tweetnacl_util.encodeBase64)(this.mlkemPublicKey) } : {},
|
|
4192
|
-
...this.mlkemSecretKey ? { mlkemSecretKey: (0, import_tweetnacl_util.encodeBase64)(this.mlkemSecretKey) } : {}
|
|
4280
|
+
...this.mlkemSecretKey ? { mlkemSecretKey: (0, import_tweetnacl_util.encodeBase64)(this.mlkemSecretKey) } : {},
|
|
4281
|
+
...this._signedPrekey ? {
|
|
4282
|
+
signedPrekeySecret: (0, import_tweetnacl_util.encodeBase64)(this._signedPrekey.secretKey),
|
|
4283
|
+
signedPrekeyPublic: (0, import_tweetnacl_util.encodeBase64)(this._signedPrekey.publicKey),
|
|
4284
|
+
signedPrekeyId: this._signedPrekeyId
|
|
4285
|
+
} : {}
|
|
4193
4286
|
};
|
|
4194
4287
|
}
|
|
4195
4288
|
/**
|
|
@@ -4250,9 +4343,10 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4250
4343
|
const pairId = `${this.did}:${recipientDid}`;
|
|
4251
4344
|
let state = this._ratchetStates.get(pairId);
|
|
4252
4345
|
let pqCiphertext = null;
|
|
4346
|
+
let dhRatchetPub = null;
|
|
4253
4347
|
if (!state) {
|
|
4254
4348
|
const x25519Shared = import_tweetnacl.default.box.before(recipientPubKey, this.encryptionKeyPair.secretKey);
|
|
4255
|
-
let
|
|
4349
|
+
let initialKey;
|
|
4256
4350
|
if (this.postQuantum && profile.mlkem_public_key) {
|
|
4257
4351
|
try {
|
|
4258
4352
|
const recipientPqPk = (0, import_tweetnacl_util.decodeBase64)(profile.mlkem_public_key);
|
|
@@ -4262,22 +4356,44 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4262
4356
|
const combined = new Uint8Array(x25519Shared.length + pqShared.length);
|
|
4263
4357
|
combined.set(x25519Shared, 0);
|
|
4264
4358
|
combined.set(pqShared, x25519Shared.length);
|
|
4265
|
-
|
|
4359
|
+
initialKey = new Uint8Array(await crypto.subtle.digest("SHA-256", combined));
|
|
4266
4360
|
} catch {
|
|
4267
|
-
|
|
4361
|
+
initialKey = x25519Shared;
|
|
4268
4362
|
}
|
|
4269
4363
|
} else {
|
|
4270
|
-
|
|
4364
|
+
initialKey = x25519Shared;
|
|
4365
|
+
}
|
|
4366
|
+
if (this.doubleRatchet) {
|
|
4367
|
+
const dhSendKeyPair = import_tweetnacl.default.box.keyPair();
|
|
4368
|
+
const dhOutput = import_tweetnacl.default.box.before(recipientPubKey, dhSendKeyPair.secretKey);
|
|
4369
|
+
const { newRootKey, newChainKey } = await kdfRK(initialKey, dhOutput);
|
|
4370
|
+
dhRatchetPub = dhSendKeyPair.publicKey;
|
|
4371
|
+
state = {
|
|
4372
|
+
sendChainKey: newChainKey,
|
|
4373
|
+
sendStep: 0,
|
|
4374
|
+
recvChainKey: initialKey,
|
|
4375
|
+
// Will be updated on first receive
|
|
4376
|
+
recvStep: 0,
|
|
4377
|
+
skippedKeys: /* @__PURE__ */ new Map(),
|
|
4378
|
+
// Double Ratchet state
|
|
4379
|
+
rootKey: newRootKey,
|
|
4380
|
+
dhSendKeyPair,
|
|
4381
|
+
dhRecvPubKey: void 0,
|
|
4382
|
+
prevSendStep: 0,
|
|
4383
|
+
dhSkippedKeys: /* @__PURE__ */ new Map()
|
|
4384
|
+
};
|
|
4385
|
+
} else {
|
|
4386
|
+
state = {
|
|
4387
|
+
sendChainKey: initialKey,
|
|
4388
|
+
sendStep: 0,
|
|
4389
|
+
recvChainKey: initialKey,
|
|
4390
|
+
recvStep: 0,
|
|
4391
|
+
skippedKeys: /* @__PURE__ */ new Map()
|
|
4392
|
+
};
|
|
4271
4393
|
}
|
|
4272
|
-
state = {
|
|
4273
|
-
sendChainKey: chainKey,
|
|
4274
|
-
sendStep: 0,
|
|
4275
|
-
recvChainKey: chainKey,
|
|
4276
|
-
// Will be synced on first receive
|
|
4277
|
-
recvStep: 0,
|
|
4278
|
-
skippedKeys: /* @__PURE__ */ new Map()
|
|
4279
|
-
};
|
|
4280
4394
|
this._ratchetStates.set(pairId, state);
|
|
4395
|
+
} else if (state.rootKey && state.dhSendKeyPair) {
|
|
4396
|
+
dhRatchetPub = state.dhSendKeyPair.publicKey;
|
|
4281
4397
|
}
|
|
4282
4398
|
const { nextChainKey, messageKey } = await ratchetStep(state.sendChainKey);
|
|
4283
4399
|
state.sendChainKey = nextChainKey;
|
|
@@ -4287,6 +4403,8 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4287
4403
|
if (usePadding) flags |= FLAG_PADDED;
|
|
4288
4404
|
if (useSealed) flags |= FLAG_SEALED;
|
|
4289
4405
|
if (pqCiphertext) flags |= FLAG_PQ;
|
|
4406
|
+
if (dhRatchetPub) flags |= FLAG_DH_RATCHET;
|
|
4407
|
+
if (this.deniable) flags |= FLAG_DENIABLE;
|
|
4290
4408
|
const header = makeProtoHeader(flags, currentStep);
|
|
4291
4409
|
const messageBytes = new Uint8Array(header.length + contentBytes.length);
|
|
4292
4410
|
messageBytes.set(header, 0);
|
|
@@ -4296,6 +4414,10 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4296
4414
|
if (!ciphertext) {
|
|
4297
4415
|
throw new Error("Encryption failed");
|
|
4298
4416
|
}
|
|
4417
|
+
if (this.jitterMs > 0) {
|
|
4418
|
+
const jitter = Math.random() * this.jitterMs;
|
|
4419
|
+
await new Promise((r) => setTimeout(r, jitter));
|
|
4420
|
+
}
|
|
4299
4421
|
const envelopeObj = {
|
|
4300
4422
|
from: this.did,
|
|
4301
4423
|
to: recipientDid,
|
|
@@ -4307,8 +4429,18 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4307
4429
|
if (pqCiphertext) {
|
|
4308
4430
|
envelopeObj.pq_ciphertext = (0, import_tweetnacl_util.encodeBase64)(pqCiphertext);
|
|
4309
4431
|
}
|
|
4432
|
+
if (dhRatchetPub) {
|
|
4433
|
+
envelopeObj.dh_ratchet_key = (0, import_tweetnacl_util.encodeBase64)(dhRatchetPub);
|
|
4434
|
+
envelopeObj.pn = state.prevSendStep || 0;
|
|
4435
|
+
}
|
|
4310
4436
|
const envelopeData = JSON.stringify(envelopeObj);
|
|
4311
|
-
|
|
4437
|
+
let signature;
|
|
4438
|
+
if (this.deniable) {
|
|
4439
|
+
const sharedSecret = import_tweetnacl.default.box.before(recipientPubKey, this.encryptionKeyPair.secretKey);
|
|
4440
|
+
signature = await hmacSha256(sharedSecret, (0, import_tweetnacl_util.decodeUTF8)(envelopeData));
|
|
4441
|
+
} else {
|
|
4442
|
+
signature = import_tweetnacl.default.sign.detached((0, import_tweetnacl_util.decodeUTF8)(envelopeData), this.signingKeyPair.secretKey);
|
|
4443
|
+
}
|
|
4312
4444
|
const payload = {
|
|
4313
4445
|
to: recipientDid,
|
|
4314
4446
|
ciphertext: (0, import_tweetnacl_util.encodeBase64)(ciphertext),
|
|
@@ -4389,6 +4521,9 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4389
4521
|
let rawPlaintext = null;
|
|
4390
4522
|
let envelopeRatchetStep = 0;
|
|
4391
4523
|
let envelopePqCiphertext = null;
|
|
4524
|
+
let envelopeDhRatchetKey = null;
|
|
4525
|
+
let envelopePn = 0;
|
|
4526
|
+
let envelopeDeniable = false;
|
|
4392
4527
|
if (msg.envelope) {
|
|
4393
4528
|
try {
|
|
4394
4529
|
const env = JSON.parse(msg.envelope);
|
|
@@ -4398,6 +4533,12 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4398
4533
|
if (typeof env.pq_ciphertext === "string") {
|
|
4399
4534
|
envelopePqCiphertext = env.pq_ciphertext;
|
|
4400
4535
|
}
|
|
4536
|
+
if (typeof env.dh_ratchet_key === "string") {
|
|
4537
|
+
envelopeDhRatchetKey = env.dh_ratchet_key;
|
|
4538
|
+
}
|
|
4539
|
+
if (typeof env.pn === "number") {
|
|
4540
|
+
envelopePn = env.pn;
|
|
4541
|
+
}
|
|
4401
4542
|
} catch {
|
|
4402
4543
|
}
|
|
4403
4544
|
}
|
|
@@ -4406,7 +4547,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4406
4547
|
let state = this._ratchetStates.get(pairId);
|
|
4407
4548
|
if (!state) {
|
|
4408
4549
|
const x25519Shared = import_tweetnacl.default.box.before(senderEncPub, this.encryptionKeyPair.secretKey);
|
|
4409
|
-
let
|
|
4550
|
+
let initialKey;
|
|
4410
4551
|
if (envelopePqCiphertext && this.mlkemSecretKey) {
|
|
4411
4552
|
try {
|
|
4412
4553
|
const pqCt = (0, import_tweetnacl_util.decodeBase64)(envelopePqCiphertext);
|
|
@@ -4415,26 +4556,80 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4415
4556
|
const combined = new Uint8Array(x25519Shared.length + pqShared.length);
|
|
4416
4557
|
combined.set(x25519Shared, 0);
|
|
4417
4558
|
combined.set(pqShared, x25519Shared.length);
|
|
4418
|
-
|
|
4559
|
+
initialKey = new Uint8Array(await crypto.subtle.digest("SHA-256", combined));
|
|
4419
4560
|
} catch {
|
|
4420
|
-
|
|
4561
|
+
initialKey = x25519Shared;
|
|
4421
4562
|
}
|
|
4422
4563
|
} else {
|
|
4423
|
-
|
|
4564
|
+
initialKey = x25519Shared;
|
|
4565
|
+
}
|
|
4566
|
+
if (envelopeDhRatchetKey && this.doubleRatchet) {
|
|
4567
|
+
const senderDhPub = (0, import_tweetnacl_util.decodeBase64)(envelopeDhRatchetKey);
|
|
4568
|
+
const dhOutput = import_tweetnacl.default.box.before(senderDhPub, this.encryptionKeyPair.secretKey);
|
|
4569
|
+
const { newRootKey, newChainKey } = await kdfRK(initialKey, dhOutput);
|
|
4570
|
+
state = {
|
|
4571
|
+
sendChainKey: initialKey,
|
|
4572
|
+
sendStep: 0,
|
|
4573
|
+
recvChainKey: newChainKey,
|
|
4574
|
+
recvStep: 0,
|
|
4575
|
+
skippedKeys: /* @__PURE__ */ new Map(),
|
|
4576
|
+
rootKey: newRootKey,
|
|
4577
|
+
dhSendKeyPair: void 0,
|
|
4578
|
+
dhRecvPubKey: senderDhPub,
|
|
4579
|
+
prevSendStep: 0,
|
|
4580
|
+
dhSkippedKeys: /* @__PURE__ */ new Map()
|
|
4581
|
+
};
|
|
4582
|
+
} else {
|
|
4583
|
+
state = {
|
|
4584
|
+
sendChainKey: initialKey,
|
|
4585
|
+
sendStep: 0,
|
|
4586
|
+
recvChainKey: initialKey,
|
|
4587
|
+
recvStep: 0,
|
|
4588
|
+
skippedKeys: /* @__PURE__ */ new Map()
|
|
4589
|
+
};
|
|
4424
4590
|
}
|
|
4425
|
-
state = {
|
|
4426
|
-
sendChainKey: chainKey,
|
|
4427
|
-
// Our sending chain to this peer
|
|
4428
|
-
sendStep: 0,
|
|
4429
|
-
recvChainKey: chainKey,
|
|
4430
|
-
// Their sending chain (our receiving)
|
|
4431
|
-
recvStep: 0,
|
|
4432
|
-
skippedKeys: /* @__PURE__ */ new Map()
|
|
4433
|
-
};
|
|
4434
4591
|
this._ratchetStates.set(pairId, state);
|
|
4592
|
+
} else if (envelopeDhRatchetKey && state.rootKey) {
|
|
4593
|
+
const senderDhPub = (0, import_tweetnacl_util.decodeBase64)(envelopeDhRatchetKey);
|
|
4594
|
+
const currentDhRecv = state.dhRecvPubKey;
|
|
4595
|
+
if (!currentDhRecv || (0, import_tweetnacl_util.encodeBase64)(senderDhPub) !== (0, import_tweetnacl_util.encodeBase64)(currentDhRecv)) {
|
|
4596
|
+
if (envelopePn > state.recvStep) {
|
|
4597
|
+
let ck = state.recvChainKey;
|
|
4598
|
+
for (let i = state.recvStep + 1; i <= envelopePn && i - state.recvStep <= MAX_SKIP; i++) {
|
|
4599
|
+
const { nextChainKey, messageKey: skippedMk } = await ratchetStep(ck);
|
|
4600
|
+
const skipKey = `${currentDhRecv ? (0, import_tweetnacl_util.encodeBase64)(currentDhRecv) : "init"}:${i}`;
|
|
4601
|
+
if (!state.dhSkippedKeys) state.dhSkippedKeys = /* @__PURE__ */ new Map();
|
|
4602
|
+
state.dhSkippedKeys.set(skipKey, skippedMk);
|
|
4603
|
+
ck = nextChainKey;
|
|
4604
|
+
if (state.dhSkippedKeys.size > MAX_SKIP) {
|
|
4605
|
+
const oldest = state.dhSkippedKeys.keys().next().value;
|
|
4606
|
+
if (oldest !== void 0) state.dhSkippedKeys.delete(oldest);
|
|
4607
|
+
}
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
state.dhRecvPubKey = senderDhPub;
|
|
4611
|
+
const myKey = state.dhSendKeyPair || this.encryptionKeyPair;
|
|
4612
|
+
const dhOutput1 = import_tweetnacl.default.box.before(senderDhPub, myKey.secretKey);
|
|
4613
|
+
const kdf1 = await kdfRK(state.rootKey, dhOutput1);
|
|
4614
|
+
state.rootKey = kdf1.newRootKey;
|
|
4615
|
+
state.recvChainKey = kdf1.newChainKey;
|
|
4616
|
+
state.recvStep = 0;
|
|
4617
|
+
state.prevSendStep = state.sendStep;
|
|
4618
|
+
state.dhSendKeyPair = import_tweetnacl.default.box.keyPair();
|
|
4619
|
+
state.sendStep = 0;
|
|
4620
|
+
const dhOutput2 = import_tweetnacl.default.box.before(senderDhPub, state.dhSendKeyPair.secretKey);
|
|
4621
|
+
const kdf2 = await kdfRK(state.rootKey, dhOutput2);
|
|
4622
|
+
state.rootKey = kdf2.newRootKey;
|
|
4623
|
+
state.sendChainKey = kdf2.newChainKey;
|
|
4624
|
+
}
|
|
4435
4625
|
}
|
|
4436
4626
|
const targetStep = envelopeRatchetStep;
|
|
4437
|
-
|
|
4627
|
+
const dhSkipKey = envelopeDhRatchetKey ? `${envelopeDhRatchetKey}:${targetStep}` : `init:${targetStep}`;
|
|
4628
|
+
if (state.dhSkippedKeys?.has(dhSkipKey)) {
|
|
4629
|
+
const mk = state.dhSkippedKeys.get(dhSkipKey);
|
|
4630
|
+
rawPlaintext = import_tweetnacl.default.secretbox.open(ciphertext, nonce, mk);
|
|
4631
|
+
state.dhSkippedKeys.delete(dhSkipKey);
|
|
4632
|
+
} else if (state.skippedKeys.has(targetStep)) {
|
|
4438
4633
|
const mk = state.skippedKeys.get(targetStep);
|
|
4439
4634
|
rawPlaintext = import_tweetnacl.default.secretbox.open(ciphertext, nonce, mk);
|
|
4440
4635
|
state.skippedKeys.delete(targetStep);
|
|
@@ -4497,7 +4692,6 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4497
4692
|
}
|
|
4498
4693
|
let signatureValid = false;
|
|
4499
4694
|
try {
|
|
4500
|
-
const senderSignPub = (0, import_tweetnacl_util.decodeBase64)(msg.sender_signing_key);
|
|
4501
4695
|
const signatureBytes = (0, import_tweetnacl_util.decodeBase64)(msg.signature);
|
|
4502
4696
|
const envelopeStr = msg.envelope || JSON.stringify({
|
|
4503
4697
|
from: senderDid,
|
|
@@ -4506,11 +4700,22 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4506
4700
|
nonce: msg.nonce,
|
|
4507
4701
|
ciphertext_hash: await sha256(msg.ciphertext)
|
|
4508
4702
|
});
|
|
4509
|
-
|
|
4510
|
-
(
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4703
|
+
if (signatureBytes.length === 32) {
|
|
4704
|
+
const sharedSecret = import_tweetnacl.default.box.before(senderEncPub, this.encryptionKeyPair.secretKey);
|
|
4705
|
+
const expectedHmac = await hmacSha256(sharedSecret, (0, import_tweetnacl_util.decodeUTF8)(envelopeStr));
|
|
4706
|
+
if (expectedHmac.length === signatureBytes.length) {
|
|
4707
|
+
let diff = 0;
|
|
4708
|
+
for (let i = 0; i < expectedHmac.length; i++) diff |= expectedHmac[i] ^ signatureBytes[i];
|
|
4709
|
+
signatureValid = diff === 0;
|
|
4710
|
+
}
|
|
4711
|
+
} else {
|
|
4712
|
+
const senderSignPub = (0, import_tweetnacl_util.decodeBase64)(msg.sender_signing_key);
|
|
4713
|
+
signatureValid = import_tweetnacl.default.sign.detached.verify(
|
|
4714
|
+
(0, import_tweetnacl_util.decodeUTF8)(envelopeStr),
|
|
4715
|
+
signatureBytes,
|
|
4716
|
+
senderSignPub
|
|
4717
|
+
);
|
|
4718
|
+
}
|
|
4514
4719
|
} catch {
|
|
4515
4720
|
signatureValid = false;
|
|
4516
4721
|
}
|
|
@@ -4704,22 +4909,35 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4704
4909
|
async rotateKeys() {
|
|
4705
4910
|
const newSigningKeyPair = import_tweetnacl.default.sign.keyPair();
|
|
4706
4911
|
const newEncryptionKeyPair = import_tweetnacl.default.box.keyPair();
|
|
4912
|
+
const newSignedPrekey = import_tweetnacl.default.box.keyPair();
|
|
4913
|
+
const newSignedPrekeyId = (this._signedPrekeyId || 0) + 1;
|
|
4914
|
+
const signedPrekeySignature = import_tweetnacl.default.sign.detached(newSignedPrekey.publicKey, newSigningKeyPair.secretKey);
|
|
4915
|
+
const body = {
|
|
4916
|
+
signing_public_key: (0, import_tweetnacl_util.encodeBase64)(newSigningKeyPair.publicKey),
|
|
4917
|
+
encryption_public_key: (0, import_tweetnacl_util.encodeBase64)(newEncryptionKeyPair.publicKey),
|
|
4918
|
+
signed_prekey_public: (0, import_tweetnacl_util.encodeBase64)(newSignedPrekey.publicKey),
|
|
4919
|
+
signed_prekey_signature: (0, import_tweetnacl_util.encodeBase64)(signedPrekeySignature),
|
|
4920
|
+
signed_prekey_id: newSignedPrekeyId
|
|
4921
|
+
};
|
|
4922
|
+
if (this.postQuantum && this.mlkemPublicKey) {
|
|
4923
|
+
body.mlkem_public_key = this.mlkemPublicKey;
|
|
4924
|
+
}
|
|
4707
4925
|
const res = await this._timedFetch(`${this.baseUrl}/v1/agent/rotate-keys`, {
|
|
4708
4926
|
method: "POST",
|
|
4709
4927
|
headers: {
|
|
4710
4928
|
"Content-Type": "application/json",
|
|
4711
4929
|
"X-Agent-Key": this.apiKey
|
|
4712
4930
|
},
|
|
4713
|
-
body: JSON.stringify(
|
|
4714
|
-
signing_public_key: (0, import_tweetnacl_util.encodeBase64)(newSigningKeyPair.publicKey),
|
|
4715
|
-
encryption_public_key: (0, import_tweetnacl_util.encodeBase64)(newEncryptionKeyPair.publicKey)
|
|
4716
|
-
})
|
|
4931
|
+
body: JSON.stringify(body)
|
|
4717
4932
|
});
|
|
4718
4933
|
if (!res.ok) {
|
|
4719
4934
|
throw new Error("Key rotation failed");
|
|
4720
4935
|
}
|
|
4721
4936
|
this.signingKeyPair = newSigningKeyPair;
|
|
4722
4937
|
this.encryptionKeyPair = newEncryptionKeyPair;
|
|
4938
|
+
this._signedPrekey = newSignedPrekey;
|
|
4939
|
+
this._signedPrekeyId = newSignedPrekeyId;
|
|
4940
|
+
await this.uploadPrekeys(10);
|
|
4723
4941
|
}
|
|
4724
4942
|
// ─── Channels (Encrypted AI Forum) ──────────────────────────────────────────
|
|
4725
4943
|
/**
|
|
@@ -5597,6 +5815,158 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
5597
5815
|
return res.json();
|
|
5598
5816
|
}
|
|
5599
5817
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
5818
|
+
// X3DH — Async Key Agreement (prekey bundles for offline agents)
|
|
5819
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
5820
|
+
/**
|
|
5821
|
+
* Upload prekey bundle for X3DH async key agreement.
|
|
5822
|
+
* Other agents can fetch your prekeys and establish encrypted sessions
|
|
5823
|
+
* even while you're offline.
|
|
5824
|
+
*
|
|
5825
|
+
* @param count Number of one-time prekeys to upload (default: 20)
|
|
5826
|
+
*/
|
|
5827
|
+
async uploadPrekeys(count = 20) {
|
|
5828
|
+
const prekeys = [];
|
|
5829
|
+
for (let i = 0; i < count; i++) {
|
|
5830
|
+
const kp = import_tweetnacl.default.box.keyPair();
|
|
5831
|
+
prekeys.push({
|
|
5832
|
+
id: Date.now() + i,
|
|
5833
|
+
public_key: (0, import_tweetnacl_util.encodeBase64)(kp.publicKey),
|
|
5834
|
+
secretKey: kp.secretKey
|
|
5835
|
+
});
|
|
5836
|
+
}
|
|
5837
|
+
const newPrekey = import_tweetnacl.default.box.keyPair();
|
|
5838
|
+
this._signedPrekeyId++;
|
|
5839
|
+
const prekeySignature = import_tweetnacl.default.sign.detached(newPrekey.publicKey, this.signingKeyPair.secretKey);
|
|
5840
|
+
this._signedPrekey = newPrekey;
|
|
5841
|
+
const res = await this._timedFetch(`${this.baseUrl}/v1/agent/prekeys`, {
|
|
5842
|
+
method: "POST",
|
|
5843
|
+
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
5844
|
+
body: JSON.stringify({
|
|
5845
|
+
prekeys: prekeys.map((pk) => ({ id: pk.id, public_key: pk.public_key })),
|
|
5846
|
+
signed_prekey: {
|
|
5847
|
+
public_key: (0, import_tweetnacl_util.encodeBase64)(newPrekey.publicKey),
|
|
5848
|
+
signature: (0, import_tweetnacl_util.encodeBase64)(prekeySignature),
|
|
5849
|
+
id: this._signedPrekeyId
|
|
5850
|
+
}
|
|
5851
|
+
})
|
|
5852
|
+
});
|
|
5853
|
+
if (!res.ok) throw new Error(`Prekey upload failed: ${res.status}`);
|
|
5854
|
+
return await res.json();
|
|
5855
|
+
}
|
|
5856
|
+
/**
|
|
5857
|
+
* Fetch another agent's prekey bundle for X3DH key agreement.
|
|
5858
|
+
* Use this to establish an encrypted session with an offline agent.
|
|
5859
|
+
*/
|
|
5860
|
+
async fetchPrekeys(did) {
|
|
5861
|
+
const res = await this._timedFetch(`${this.baseUrl}/v1/agent/prekeys/${encodeURIComponent(did)}`);
|
|
5862
|
+
if (!res.ok) return null;
|
|
5863
|
+
return await res.json();
|
|
5864
|
+
}
|
|
5865
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
5866
|
+
// CLIENT-SIDE CHANNEL ENCRYPTION
|
|
5867
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
5868
|
+
/**
|
|
5869
|
+
* Create a channel with client-side encryption.
|
|
5870
|
+
* The channel symmetric key is generated locally and encrypted per-member.
|
|
5871
|
+
* The relay NEVER sees the plaintext channel key — true E2E for groups.
|
|
5872
|
+
*/
|
|
5873
|
+
async createEncryptedChannel(options) {
|
|
5874
|
+
const channelKey = import_tweetnacl.default.randomBytes(32);
|
|
5875
|
+
const res = await this._timedFetch(`${this.baseUrl}/v1/agent/channels`, {
|
|
5876
|
+
method: "POST",
|
|
5877
|
+
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
5878
|
+
body: JSON.stringify(options)
|
|
5879
|
+
});
|
|
5880
|
+
if (!res.ok) {
|
|
5881
|
+
const err = await res.json().catch(() => ({}));
|
|
5882
|
+
throw new Error(`Channel creation failed: ${err.error || res.statusText}`);
|
|
5883
|
+
}
|
|
5884
|
+
const channel = await res.json();
|
|
5885
|
+
const selfNonce = import_tweetnacl.default.randomBytes(import_tweetnacl.default.box.nonceLength);
|
|
5886
|
+
const selfEncrypted = import_tweetnacl.default.box(channelKey, selfNonce, this.encryptionKeyPair.publicKey, this.encryptionKeyPair.secretKey);
|
|
5887
|
+
await this.memorySet("channel-keys", channel.id, {
|
|
5888
|
+
key: (0, import_tweetnacl_util.encodeBase64)(channelKey),
|
|
5889
|
+
nonce: (0, import_tweetnacl_util.encodeBase64)(selfNonce)
|
|
5890
|
+
});
|
|
5891
|
+
return { ...channel, channelKey };
|
|
5892
|
+
}
|
|
5893
|
+
/**
|
|
5894
|
+
* Post a client-side encrypted message to a channel.
|
|
5895
|
+
* Uses the channel's shared symmetric key — relay never sees plaintext.
|
|
5896
|
+
*
|
|
5897
|
+
* @param channelId Channel ID
|
|
5898
|
+
* @param message Plaintext message
|
|
5899
|
+
* @param channelKey 32-byte symmetric channel key (from createEncryptedChannel or received via invite)
|
|
5900
|
+
*/
|
|
5901
|
+
async postEncrypted(channelId, message, channelKey) {
|
|
5902
|
+
const nonce = import_tweetnacl.default.randomBytes(import_tweetnacl.default.secretbox.nonceLength);
|
|
5903
|
+
const ciphertext = import_tweetnacl.default.secretbox((0, import_tweetnacl_util.decodeUTF8)(message), nonce, channelKey);
|
|
5904
|
+
const sigPayload = new Uint8Array([...ciphertext, ...nonce]);
|
|
5905
|
+
const signature = import_tweetnacl.default.sign.detached(sigPayload, this.signingKeyPair.secretKey);
|
|
5906
|
+
const res = await this._timedFetch(`${this.baseUrl}/v1/agent/channels/${channelId}/messages`, {
|
|
5907
|
+
method: "POST",
|
|
5908
|
+
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
5909
|
+
body: JSON.stringify({
|
|
5910
|
+
message: JSON.stringify({
|
|
5911
|
+
v: 3,
|
|
5912
|
+
ct: (0, import_tweetnacl_util.encodeBase64)(ciphertext),
|
|
5913
|
+
n: (0, import_tweetnacl_util.encodeBase64)(nonce),
|
|
5914
|
+
sig: (0, import_tweetnacl_util.encodeBase64)(signature),
|
|
5915
|
+
from: this.did
|
|
5916
|
+
})
|
|
5917
|
+
})
|
|
5918
|
+
});
|
|
5919
|
+
if (!res.ok) throw new Error(`Post failed: ${res.status}`);
|
|
5920
|
+
return await res.json();
|
|
5921
|
+
}
|
|
5922
|
+
/**
|
|
5923
|
+
* Read and decrypt channel messages using client-side channel key.
|
|
5924
|
+
* Ignores server-side encryption entirely — true E2E.
|
|
5925
|
+
*
|
|
5926
|
+
* @param channelId Channel ID
|
|
5927
|
+
* @param channelKey 32-byte symmetric channel key
|
|
5928
|
+
*/
|
|
5929
|
+
async readEncrypted(channelId, channelKey, options) {
|
|
5930
|
+
const raw = await this.readChannel(channelId, options);
|
|
5931
|
+
const decrypted = [];
|
|
5932
|
+
for (const msg of raw.messages) {
|
|
5933
|
+
try {
|
|
5934
|
+
const parsed = JSON.parse(typeof msg.content === "string" ? msg.content : msg.message || "");
|
|
5935
|
+
if (parsed.v === 3 && parsed.ct && parsed.n) {
|
|
5936
|
+
const ct = (0, import_tweetnacl_util.decodeBase64)(parsed.ct);
|
|
5937
|
+
const nonce = (0, import_tweetnacl_util.decodeBase64)(parsed.n);
|
|
5938
|
+
const plain = import_tweetnacl.default.secretbox.open(ct, nonce, channelKey);
|
|
5939
|
+
if (plain) {
|
|
5940
|
+
let sigValid = false;
|
|
5941
|
+
if (parsed.sig && parsed.from) {
|
|
5942
|
+
try {
|
|
5943
|
+
const profile = await this.getIdentity(parsed.from);
|
|
5944
|
+
if (profile) {
|
|
5945
|
+
const sigPayload = new Uint8Array([...ct, ...nonce]);
|
|
5946
|
+
sigValid = import_tweetnacl.default.sign.detached.verify(
|
|
5947
|
+
sigPayload,
|
|
5948
|
+
(0, import_tweetnacl_util.decodeBase64)(parsed.sig),
|
|
5949
|
+
(0, import_tweetnacl_util.decodeBase64)(profile.signing_public_key)
|
|
5950
|
+
);
|
|
5951
|
+
}
|
|
5952
|
+
} catch {
|
|
5953
|
+
}
|
|
5954
|
+
}
|
|
5955
|
+
decrypted.push({
|
|
5956
|
+
id: msg.id,
|
|
5957
|
+
from: parsed.from || msg.author || "unknown",
|
|
5958
|
+
content: (0, import_tweetnacl_util.encodeUTF8)(plain),
|
|
5959
|
+
timestamp: msg.created_at || msg.timestamp,
|
|
5960
|
+
signatureValid: sigValid
|
|
5961
|
+
});
|
|
5962
|
+
}
|
|
5963
|
+
}
|
|
5964
|
+
} catch {
|
|
5965
|
+
}
|
|
5966
|
+
}
|
|
5967
|
+
return { messages: decrypted, count: decrypted.length };
|
|
5968
|
+
}
|
|
5969
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
5600
5970
|
// LISTEN — Event-Driven Message Receiving
|
|
5601
5971
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
5602
5972
|
/**
|
|
@@ -5663,20 +6033,54 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
5663
6033
|
this.ping().catch(() => {
|
|
5664
6034
|
});
|
|
5665
6035
|
}
|
|
6036
|
+
const useLongPoll = this.longPoll;
|
|
5666
6037
|
const poll = async () => {
|
|
5667
6038
|
if (!active || options.signal?.aborted) {
|
|
5668
6039
|
handle.stop();
|
|
5669
6040
|
return;
|
|
5670
6041
|
}
|
|
5671
6042
|
try {
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
6043
|
+
let messages;
|
|
6044
|
+
if (useLongPoll) {
|
|
6045
|
+
const params = new URLSearchParams({ timeout: "25" });
|
|
6046
|
+
if (lastSeen) params.set("since", lastSeen);
|
|
6047
|
+
if (options.from) params.set("from", options.from);
|
|
6048
|
+
const res = await this._timedFetch(`${this.baseUrl}/v1/agent/receive/poll?${params}`, {
|
|
6049
|
+
headers: { "X-Agent-Key": this.apiKey }
|
|
6050
|
+
});
|
|
6051
|
+
if (res.ok) {
|
|
6052
|
+
const data = await res.json();
|
|
6053
|
+
messages = [];
|
|
6054
|
+
for (const raw of data.messages) {
|
|
6055
|
+
try {
|
|
6056
|
+
if (this._seenMessageIds.has(raw.id)) continue;
|
|
6057
|
+
this._seenMessageIds.add(raw.id);
|
|
6058
|
+
} catch {
|
|
6059
|
+
}
|
|
6060
|
+
}
|
|
6061
|
+
if (data.messages.length > 0) {
|
|
6062
|
+
messages = await this.receive({
|
|
6063
|
+
since: lastSeen,
|
|
6064
|
+
from: options.from,
|
|
6065
|
+
threadId: options.threadId,
|
|
6066
|
+
messageType: options.messageType,
|
|
6067
|
+
unreadOnly,
|
|
6068
|
+
limit: 50
|
|
6069
|
+
});
|
|
6070
|
+
}
|
|
6071
|
+
} else {
|
|
6072
|
+
messages = [];
|
|
6073
|
+
}
|
|
6074
|
+
} else {
|
|
6075
|
+
messages = await this.receive({
|
|
6076
|
+
since: lastSeen,
|
|
6077
|
+
from: options.from,
|
|
6078
|
+
threadId: options.threadId,
|
|
6079
|
+
messageType: options.messageType,
|
|
6080
|
+
unreadOnly,
|
|
6081
|
+
limit: 50
|
|
6082
|
+
});
|
|
6083
|
+
}
|
|
5680
6084
|
if (messages.length > 0) {
|
|
5681
6085
|
consecutiveEmpty = 0;
|
|
5682
6086
|
if (adaptive) currentInterval = Math.max(interval / 2, 500);
|
|
@@ -5694,7 +6098,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
5694
6098
|
lastSeen = messages[messages.length - 1].timestamp;
|
|
5695
6099
|
} else {
|
|
5696
6100
|
consecutiveEmpty++;
|
|
5697
|
-
if (adaptive && consecutiveEmpty > 3) {
|
|
6101
|
+
if (adaptive && consecutiveEmpty > 3 && !useLongPoll) {
|
|
5698
6102
|
currentInterval = Math.min(currentInterval * 1.5, interval * 4);
|
|
5699
6103
|
}
|
|
5700
6104
|
}
|
|
@@ -5703,7 +6107,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
5703
6107
|
currentInterval = Math.min(currentInterval * 2, interval * 8);
|
|
5704
6108
|
}
|
|
5705
6109
|
if (active && !options.signal?.aborted) {
|
|
5706
|
-
timer = setTimeout(poll, currentInterval);
|
|
6110
|
+
timer = setTimeout(poll, useLongPoll ? 100 : currentInterval);
|
|
5707
6111
|
}
|
|
5708
6112
|
};
|
|
5709
6113
|
poll();
|
|
@@ -5903,30 +6307,35 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
5903
6307
|
return {
|
|
5904
6308
|
relayCanSee: [
|
|
5905
6309
|
"Your DID (public identifier)",
|
|
5906
|
-
"Who you message (recipient DIDs)",
|
|
5907
|
-
"When you message (timestamps)",
|
|
6310
|
+
...this.sealedSender ? [] : ["Who you message (recipient DIDs)"],
|
|
6311
|
+
...this.jitterMs > 0 ? ["Approximate timing (jittered timestamps)"] : ["When you message (timestamps)"],
|
|
5908
6312
|
"Message types (text, task-request, etc.)",
|
|
5909
6313
|
"Thread structure (which messages are replies)",
|
|
5910
|
-
"Channel membership",
|
|
6314
|
+
"Channel membership (but NOT channel message content with client-side encryption)",
|
|
5911
6315
|
"Capability registrations",
|
|
5912
6316
|
"Online/offline status",
|
|
5913
6317
|
"Approximate message size (even with padding, bounded to power-of-2)"
|
|
5914
6318
|
],
|
|
5915
6319
|
relayCannotSee: [
|
|
5916
|
-
"Message content (E2E encrypted \u2014 nacl.
|
|
6320
|
+
"Message content (E2E encrypted \u2014 nacl.box with Double Ratchet per-message keys)",
|
|
5917
6321
|
"Private keys (generated and stored client-side only)",
|
|
5918
6322
|
"Memory values (encrypted CLIENT-SIDE with nacl.secretbox before relay storage)",
|
|
5919
|
-
"Past message keys (hash ratchet
|
|
5920
|
-
|
|
6323
|
+
"Past message keys (forward secrecy \u2014 hash ratchet + DH ratchet, old keys deleted)",
|
|
6324
|
+
"Future message keys (post-compromise recovery via DH ratchet \u2014 compromise heals after one round-trip)",
|
|
6325
|
+
"Channel message content (client-side nacl.secretbox encryption \u2014 relay stores only ciphertext)",
|
|
6326
|
+
...this.sealedSender ? ["Sender identity (sealed inside ciphertext)"] : [],
|
|
6327
|
+
...this.deniable ? ["Who authored a message (HMAC is symmetric \u2014 either party could have produced it)"] : []
|
|
5921
6328
|
],
|
|
5922
6329
|
protections: [
|
|
5923
|
-
"Hash ratchet forward secrecy \u2014 per-message key derivation, old keys deleted",
|
|
6330
|
+
...this.doubleRatchet ? ["Double Ratchet (Signal Protocol) \u2014 DH ratchet for post-compromise recovery + hash ratchet for forward secrecy"] : ["Hash ratchet forward secrecy \u2014 per-message key derivation, old keys deleted"],
|
|
5924
6331
|
...this.postQuantum && this.mlkemPublicKey ? ["ML-KEM-768 + X25519 hybrid key exchange (NIST FIPS 203 post-quantum, harvest-now-decrypt-later resistant)"] : [],
|
|
6332
|
+
"X3DH async key agreement (signed prekeys + one-time prekeys for offline session establishment)",
|
|
5925
6333
|
"X25519 key exchange + XSalsa20-Poly1305 authenticated encryption",
|
|
5926
|
-
"Ed25519 signatures on every message (envelope + ciphertext hash)",
|
|
6334
|
+
...this.deniable ? ["Deniable authentication (HMAC-SHA256 with shared DH secret \u2014 both parties can produce the MAC)"] : ["Ed25519 signatures on every message (envelope + ciphertext hash)"],
|
|
5927
6335
|
"TOFU key pinning (MitM detection on key change)",
|
|
5928
6336
|
"Client-side memory encryption (relay never sees plaintext values)",
|
|
5929
|
-
"
|
|
6337
|
+
"Client-side channel encryption (nacl.secretbox \u2014 relay never sees channel plaintext)",
|
|
6338
|
+
"Protocol version header (deterministic padding/sealing/ratchet detection, no heuristics)",
|
|
5930
6339
|
"Identity cache (reduced key lookups, 5-min TTL)",
|
|
5931
6340
|
"Message deduplication (track seen message IDs)",
|
|
5932
6341
|
"Request timeouts (AbortController on all HTTP, configurable)",
|
|
@@ -5935,20 +6344,19 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
5935
6344
|
...this.sealedSender ? ["Sealed sender (relay cannot see who sent a message)"] : [],
|
|
5936
6345
|
...this.requireSignatures ? ["Strict signature enforcement (reject unsigned/invalid messages)"] : [],
|
|
5937
6346
|
...this.fallbackRelays.length > 0 ? [`Multi-relay fallback (${this.fallbackRelays.length} backup relays)`] : [],
|
|
6347
|
+
...this.jitterMs > 0 ? [`Timing jitter (random ${this.jitterMs}ms delay \u2014 metadata timing protection)`] : [],
|
|
6348
|
+
...this.longPoll ? ["Long-poll transport (25s server-held connection \u2014 near-real-time delivery)"] : [],
|
|
5938
6349
|
"Auto-retry with exponential backoff",
|
|
5939
6350
|
"Offline message queue",
|
|
5940
6351
|
"did:key interoperability (W3C standard DID format)"
|
|
5941
6352
|
],
|
|
5942
6353
|
gaps: [
|
|
5943
|
-
"No DH ratchet yet \u2014 hash ratchet only (no post-compromise recovery, planned for v3)",
|
|
5944
6354
|
...!this.postQuantum || !this.mlkemPublicKey ? ["No post-quantum protection \u2014 enable postQuantum option and re-register"] : [],
|
|
5945
|
-
"
|
|
5946
|
-
"
|
|
5947
|
-
"
|
|
5948
|
-
"
|
|
5949
|
-
"No
|
|
5950
|
-
"Polling-based (no WebSocket real-time transport)",
|
|
5951
|
-
"Ratchet state is in-memory (lost on process restart \u2014 export credentials to persist)"
|
|
6355
|
+
"Single relay architecture (no onion routing, no mix network \u2014 mitigated by multi-relay fallback)",
|
|
6356
|
+
"Ratchet state is in-memory (lost on process restart \u2014 export credentials to persist)",
|
|
6357
|
+
...!this.deniable ? ["Ed25519 signatures are non-repudiable \u2014 enable deniable option for HMAC auth"] : [],
|
|
6358
|
+
...!this.doubleRatchet ? ["Hash ratchet only \u2014 enable doubleRatchet option for post-compromise recovery"] : [],
|
|
6359
|
+
...this.jitterMs === 0 ? ["No timing jitter \u2014 enable jitterMs option for metadata protection"] : []
|
|
5952
6360
|
]
|
|
5953
6361
|
};
|
|
5954
6362
|
}
|