@agentvault/secure-channel 0.2.0 → 0.4.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/channel.d.ts +9 -0
- package/dist/channel.d.ts.map +1 -1
- package/dist/cli.js +153 -42
- package/dist/cli.js.map +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +154 -43
- package/dist/index.js.map +2 -2
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { SecureChannel } from "./channel.js";
|
|
2
|
-
export type { SecureChannelConfig, ChannelState, MessageMetadata, PersistedState, LegacyPersistedState, DeviceSession, } from "./types.js";
|
|
3
|
-
export declare const VERSION = "0.
|
|
2
|
+
export type { SecureChannelConfig, ChannelState, MessageMetadata, PersistedState, LegacyPersistedState, DeviceSession, HistoryEntry, } from "./types.js";
|
|
3
|
+
export declare const VERSION = "0.3.0";
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EACV,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,cAAc,EACd,oBAAoB,EACpB,aAAa,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EACV,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,cAAc,EACd,oBAAoB,EACpB,aAAa,EACb,YAAY,GACb,MAAM,YAAY,CAAC;AACpB,eAAO,MAAM,OAAO,UAAU,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -45052,7 +45052,8 @@ function migratePersistedState(raw) {
|
|
|
45052
45052
|
identityKeypair: legacy.identityKeypair,
|
|
45053
45053
|
ephemeralKeypair: legacy.ephemeralKeypair,
|
|
45054
45054
|
fingerprint: legacy.fingerprint,
|
|
45055
|
-
lastMessageTimestamp: legacy.lastMessageTimestamp
|
|
45055
|
+
lastMessageTimestamp: legacy.lastMessageTimestamp,
|
|
45056
|
+
messageHistory: []
|
|
45056
45057
|
};
|
|
45057
45058
|
}
|
|
45058
45059
|
var SecureChannel = class extends EventEmitter {
|
|
@@ -45099,6 +45100,9 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45099
45100
|
const raw = await loadState(this.config.dataDir);
|
|
45100
45101
|
if (raw) {
|
|
45101
45102
|
this._persisted = migratePersistedState(raw);
|
|
45103
|
+
if (!this._persisted.messageHistory) {
|
|
45104
|
+
this._persisted.messageHistory = [];
|
|
45105
|
+
}
|
|
45102
45106
|
this._deviceId = this._persisted.deviceId;
|
|
45103
45107
|
this._deviceJwt = this._persisted.deviceJwt;
|
|
45104
45108
|
this._primaryConversationId = this._persisted.primaryConversationId;
|
|
@@ -45110,7 +45114,8 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45110
45114
|
const ratchet = DoubleRatchet.deserialize(sessionData.ratchetState);
|
|
45111
45115
|
this._sessions.set(convId, {
|
|
45112
45116
|
ownerDeviceId: sessionData.ownerDeviceId,
|
|
45113
|
-
ratchet
|
|
45117
|
+
ratchet,
|
|
45118
|
+
activated: sessionData.activated ?? false
|
|
45114
45119
|
});
|
|
45115
45120
|
}
|
|
45116
45121
|
}
|
|
@@ -45119,6 +45124,24 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45119
45124
|
}
|
|
45120
45125
|
await this._enroll();
|
|
45121
45126
|
}
|
|
45127
|
+
/**
|
|
45128
|
+
* Append a message to persistent history for cross-device replay.
|
|
45129
|
+
*/
|
|
45130
|
+
_appendHistory(sender, text) {
|
|
45131
|
+
if (!this._persisted) return;
|
|
45132
|
+
if (!this._persisted.messageHistory) {
|
|
45133
|
+
this._persisted.messageHistory = [];
|
|
45134
|
+
}
|
|
45135
|
+
const maxSize = this.config.maxHistorySize ?? 500;
|
|
45136
|
+
this._persisted.messageHistory.push({
|
|
45137
|
+
sender,
|
|
45138
|
+
text,
|
|
45139
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
45140
|
+
});
|
|
45141
|
+
if (this._persisted.messageHistory.length > maxSize) {
|
|
45142
|
+
this._persisted.messageHistory = this._persisted.messageHistory.slice(-maxSize);
|
|
45143
|
+
}
|
|
45144
|
+
}
|
|
45122
45145
|
/**
|
|
45123
45146
|
* Encrypt and send a message to ALL owner devices (fanout).
|
|
45124
45147
|
* Each session gets the same plaintext encrypted independently.
|
|
@@ -45127,8 +45150,12 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45127
45150
|
if (this._state !== "ready" || this._sessions.size === 0 || !this._ws) {
|
|
45128
45151
|
throw new Error("Channel is not ready");
|
|
45129
45152
|
}
|
|
45153
|
+
this._appendHistory("agent", plaintext);
|
|
45130
45154
|
const messageGroupId = randomUUID();
|
|
45131
45155
|
for (const [convId, session] of this._sessions) {
|
|
45156
|
+
if (!session.activated) {
|
|
45157
|
+
continue;
|
|
45158
|
+
}
|
|
45132
45159
|
const encrypted = session.ratchet.encrypt(plaintext);
|
|
45133
45160
|
const transport = encryptedMessageToTransport(encrypted);
|
|
45134
45161
|
this._ws.send(
|
|
@@ -45199,7 +45226,8 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45199
45226
|
publicKey: bytesToHex(ephemeral.publicKey),
|
|
45200
45227
|
privateKey: bytesToHex(ephemeral.privateKey)
|
|
45201
45228
|
},
|
|
45202
|
-
fingerprint: result.fingerprint
|
|
45229
|
+
fingerprint: result.fingerprint,
|
|
45230
|
+
messageHistory: []
|
|
45203
45231
|
};
|
|
45204
45232
|
this._poll();
|
|
45205
45233
|
} catch (err) {
|
|
@@ -45250,36 +45278,73 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45250
45278
|
this._deviceJwt = result.device_jwt;
|
|
45251
45279
|
const identity = this._persisted.identityKeypair;
|
|
45252
45280
|
const ephemeral = this._persisted.ephemeralKeypair;
|
|
45253
|
-
const
|
|
45254
|
-
|
|
45255
|
-
|
|
45256
|
-
|
|
45257
|
-
|
|
45258
|
-
|
|
45259
|
-
|
|
45260
|
-
|
|
45261
|
-
|
|
45262
|
-
|
|
45263
|
-
|
|
45264
|
-
|
|
45265
|
-
|
|
45266
|
-
|
|
45267
|
-
|
|
45268
|
-
|
|
45269
|
-
|
|
45270
|
-
|
|
45281
|
+
const sessions = {};
|
|
45282
|
+
for (const conv of conversations) {
|
|
45283
|
+
const ownerIdentityKey = conv.owner_identity_public_key || result.owner_identity_public_key;
|
|
45284
|
+
const ownerEphemeralKey = conv.owner_ephemeral_public_key || result.owner_ephemeral_public_key || ownerIdentityKey;
|
|
45285
|
+
const sharedSecret = performX3DH({
|
|
45286
|
+
myIdentityPrivate: hexToBytes(identity.privateKey),
|
|
45287
|
+
myEphemeralPrivate: hexToBytes(ephemeral.privateKey),
|
|
45288
|
+
theirIdentityPublic: hexToBytes(ownerIdentityKey),
|
|
45289
|
+
theirEphemeralPublic: hexToBytes(ownerEphemeralKey),
|
|
45290
|
+
isInitiator: false
|
|
45291
|
+
});
|
|
45292
|
+
const ratchet = DoubleRatchet.initReceiver(sharedSecret, {
|
|
45293
|
+
publicKey: hexToBytes(identity.publicKey),
|
|
45294
|
+
privateKey: hexToBytes(identity.privateKey),
|
|
45295
|
+
keyType: "ed25519"
|
|
45296
|
+
});
|
|
45297
|
+
this._sessions.set(conv.conversation_id, {
|
|
45298
|
+
ownerDeviceId: conv.owner_device_id,
|
|
45299
|
+
ratchet,
|
|
45300
|
+
activated: false
|
|
45301
|
+
// Wait for owner's first message before sending to this session
|
|
45302
|
+
});
|
|
45303
|
+
sessions[conv.conversation_id] = {
|
|
45304
|
+
ownerDeviceId: conv.owner_device_id,
|
|
45305
|
+
ratchetState: ratchet.serialize()
|
|
45306
|
+
};
|
|
45307
|
+
console.log(
|
|
45308
|
+
`[SecureChannel] Session initialized for conv ${conv.conversation_id.slice(0, 8)}... (owner ${conv.owner_device_id.slice(0, 8)}..., primary=${conv.is_primary})`
|
|
45309
|
+
);
|
|
45310
|
+
}
|
|
45271
45311
|
this._persisted = {
|
|
45272
45312
|
...this._persisted,
|
|
45273
45313
|
deviceJwt: result.device_jwt,
|
|
45274
45314
|
primaryConversationId: primary.conversation_id,
|
|
45275
|
-
sessions
|
|
45276
|
-
|
|
45277
|
-
ownerDeviceId: primary.owner_device_id,
|
|
45278
|
-
ratchetState: ratchet.serialize()
|
|
45279
|
-
}
|
|
45280
|
-
}
|
|
45315
|
+
sessions,
|
|
45316
|
+
messageHistory: this._persisted.messageHistory ?? []
|
|
45281
45317
|
};
|
|
45282
45318
|
await saveState(this.config.dataDir, this._persisted);
|
|
45319
|
+
if (this.config.webhookUrl) {
|
|
45320
|
+
try {
|
|
45321
|
+
const webhookResp = await fetch(
|
|
45322
|
+
`${this.config.apiUrl}/api/v1/devices/self/webhook`,
|
|
45323
|
+
{
|
|
45324
|
+
method: "PATCH",
|
|
45325
|
+
headers: {
|
|
45326
|
+
"Content-Type": "application/json",
|
|
45327
|
+
Authorization: `Bearer ${this._deviceJwt}`
|
|
45328
|
+
},
|
|
45329
|
+
body: JSON.stringify({ webhook_url: this.config.webhookUrl })
|
|
45330
|
+
}
|
|
45331
|
+
);
|
|
45332
|
+
if (webhookResp.ok) {
|
|
45333
|
+
const webhookData = await webhookResp.json();
|
|
45334
|
+
console.log(
|
|
45335
|
+
`[SecureChannel] Webhook registered: ${this.config.webhookUrl} (secret: ${webhookData.webhook_secret?.slice(0, 8)}...)`
|
|
45336
|
+
);
|
|
45337
|
+
this.emit("webhook_registered", {
|
|
45338
|
+
url: this.config.webhookUrl,
|
|
45339
|
+
secret: webhookData.webhook_secret
|
|
45340
|
+
});
|
|
45341
|
+
} else {
|
|
45342
|
+
console.warn(`[SecureChannel] Webhook registration failed: ${webhookResp.status}`);
|
|
45343
|
+
}
|
|
45344
|
+
} catch (err) {
|
|
45345
|
+
console.warn(`[SecureChannel] Webhook registration error: ${err}`);
|
|
45346
|
+
}
|
|
45347
|
+
}
|
|
45283
45348
|
this._connect();
|
|
45284
45349
|
} catch (err) {
|
|
45285
45350
|
this._handleError(err);
|
|
@@ -45350,6 +45415,10 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45350
45415
|
ciphertext: msgData.ciphertext
|
|
45351
45416
|
});
|
|
45352
45417
|
const plaintext = session.ratchet.decrypt(encrypted);
|
|
45418
|
+
if (!session.activated) {
|
|
45419
|
+
session.activated = true;
|
|
45420
|
+
console.log(`[SecureChannel] Session ${convId.slice(0, 8)}... activated by first owner message`);
|
|
45421
|
+
}
|
|
45353
45422
|
let messageText;
|
|
45354
45423
|
let messageType;
|
|
45355
45424
|
try {
|
|
@@ -45360,7 +45429,14 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45360
45429
|
messageType = "message";
|
|
45361
45430
|
messageText = plaintext;
|
|
45362
45431
|
}
|
|
45432
|
+
if (messageType === "session_init") {
|
|
45433
|
+
console.log(`[SecureChannel] session_init received for ${convId.slice(0, 8)}..., replaying history`);
|
|
45434
|
+
await this._replayHistoryToSession(convId);
|
|
45435
|
+
await this._persistState();
|
|
45436
|
+
return;
|
|
45437
|
+
}
|
|
45363
45438
|
if (messageType === "message") {
|
|
45439
|
+
this._appendHistory("owner", messageText);
|
|
45364
45440
|
const metadata = {
|
|
45365
45441
|
messageId: msgData.message_id,
|
|
45366
45442
|
conversationId: convId,
|
|
@@ -45389,6 +45465,7 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45389
45465
|
});
|
|
45390
45466
|
for (const [siblingConvId, siblingSession] of this._sessions) {
|
|
45391
45467
|
if (siblingConvId === sourceConvId) continue;
|
|
45468
|
+
if (!siblingSession.activated) continue;
|
|
45392
45469
|
const syncEncrypted = siblingSession.ratchet.encrypt(syncPayload);
|
|
45393
45470
|
const syncTransport = encryptedMessageToTransport(syncEncrypted);
|
|
45394
45471
|
this._ws.send(
|
|
@@ -45403,6 +45480,38 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45403
45480
|
);
|
|
45404
45481
|
}
|
|
45405
45482
|
}
|
|
45483
|
+
/**
|
|
45484
|
+
* Send stored message history to a newly-activated session.
|
|
45485
|
+
* Batches all history into a single encrypted message.
|
|
45486
|
+
*/
|
|
45487
|
+
async _replayHistoryToSession(convId) {
|
|
45488
|
+
const session = this._sessions.get(convId);
|
|
45489
|
+
if (!session || !session.activated || !this._ws) return;
|
|
45490
|
+
const history = this._persisted?.messageHistory ?? [];
|
|
45491
|
+
if (history.length === 0) {
|
|
45492
|
+
console.log(`[SecureChannel] No history to replay for ${convId.slice(0, 8)}...`);
|
|
45493
|
+
return;
|
|
45494
|
+
}
|
|
45495
|
+
console.log(
|
|
45496
|
+
`[SecureChannel] Replaying ${history.length} messages to session ${convId.slice(0, 8)}...`
|
|
45497
|
+
);
|
|
45498
|
+
const replayPayload = JSON.stringify({
|
|
45499
|
+
type: "history_replay",
|
|
45500
|
+
messages: history
|
|
45501
|
+
});
|
|
45502
|
+
const encrypted = session.ratchet.encrypt(replayPayload);
|
|
45503
|
+
const transport = encryptedMessageToTransport(encrypted);
|
|
45504
|
+
this._ws.send(
|
|
45505
|
+
JSON.stringify({
|
|
45506
|
+
event: "message",
|
|
45507
|
+
data: {
|
|
45508
|
+
conversation_id: convId,
|
|
45509
|
+
header_blob: transport.header_blob,
|
|
45510
|
+
ciphertext: transport.ciphertext
|
|
45511
|
+
}
|
|
45512
|
+
})
|
|
45513
|
+
);
|
|
45514
|
+
}
|
|
45406
45515
|
/**
|
|
45407
45516
|
* Handle a device_linked event: a new owner device has joined.
|
|
45408
45517
|
* Fetches the new device's public keys, performs X3DH, and initializes
|
|
@@ -45413,27 +45522,20 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45413
45522
|
`[SecureChannel] New owner device linked: ${event.owner_device_id.slice(0, 8)}...`
|
|
45414
45523
|
);
|
|
45415
45524
|
try {
|
|
45416
|
-
|
|
45417
|
-
`${this.config.apiUrl}/api/v1/conversations/${event.conversation_id}/keys`,
|
|
45418
|
-
{
|
|
45419
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}` }
|
|
45420
|
-
}
|
|
45421
|
-
);
|
|
45422
|
-
if (!keysRes.ok) {
|
|
45525
|
+
if (!event.owner_identity_public_key) {
|
|
45423
45526
|
console.error(
|
|
45424
|
-
`[SecureChannel]
|
|
45527
|
+
`[SecureChannel] device_linked event missing owner keys for conv ${event.conversation_id.slice(0, 8)}...`
|
|
45425
45528
|
);
|
|
45426
45529
|
return;
|
|
45427
45530
|
}
|
|
45428
|
-
const keys = await keysRes.json();
|
|
45429
45531
|
const identity = this._persisted.identityKeypair;
|
|
45430
45532
|
const ephemeral = this._persisted.ephemeralKeypair;
|
|
45431
45533
|
const sharedSecret = performX3DH({
|
|
45432
45534
|
myIdentityPrivate: hexToBytes(identity.privateKey),
|
|
45433
45535
|
myEphemeralPrivate: hexToBytes(ephemeral.privateKey),
|
|
45434
|
-
theirIdentityPublic: hexToBytes(
|
|
45536
|
+
theirIdentityPublic: hexToBytes(event.owner_identity_public_key),
|
|
45435
45537
|
theirEphemeralPublic: hexToBytes(
|
|
45436
|
-
|
|
45538
|
+
event.owner_ephemeral_public_key ?? event.owner_identity_public_key
|
|
45437
45539
|
),
|
|
45438
45540
|
isInitiator: false
|
|
45439
45541
|
});
|
|
@@ -45444,15 +45546,18 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45444
45546
|
});
|
|
45445
45547
|
this._sessions.set(event.conversation_id, {
|
|
45446
45548
|
ownerDeviceId: event.owner_device_id,
|
|
45447
|
-
ratchet
|
|
45549
|
+
ratchet,
|
|
45550
|
+
activated: false
|
|
45551
|
+
// Wait for owner's first message
|
|
45448
45552
|
});
|
|
45449
45553
|
this._persisted.sessions[event.conversation_id] = {
|
|
45450
45554
|
ownerDeviceId: event.owner_device_id,
|
|
45451
|
-
ratchetState: ratchet.serialize()
|
|
45555
|
+
ratchetState: ratchet.serialize(),
|
|
45556
|
+
activated: false
|
|
45452
45557
|
};
|
|
45453
45558
|
await this._persistState();
|
|
45454
45559
|
console.log(
|
|
45455
|
-
`[SecureChannel] Session initialized for device ${event.owner_device_id.slice(0, 8)}
|
|
45560
|
+
`[SecureChannel] Session initialized for device ${event.owner_device_id.slice(0, 8)}... (conv ${event.conversation_id.slice(0, 8)}...)`
|
|
45456
45561
|
);
|
|
45457
45562
|
} catch (err) {
|
|
45458
45563
|
console.error(
|
|
@@ -45491,6 +45596,10 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45491
45596
|
ciphertext: msg.ciphertext
|
|
45492
45597
|
});
|
|
45493
45598
|
const plaintext = session.ratchet.decrypt(encrypted);
|
|
45599
|
+
if (!session.activated) {
|
|
45600
|
+
session.activated = true;
|
|
45601
|
+
console.log(`[SecureChannel] Session ${msg.conversation_id.slice(0, 8)}... activated during sync`);
|
|
45602
|
+
}
|
|
45494
45603
|
let messageText;
|
|
45495
45604
|
let messageType;
|
|
45496
45605
|
try {
|
|
@@ -45502,6 +45611,7 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45502
45611
|
messageText = plaintext;
|
|
45503
45612
|
}
|
|
45504
45613
|
if (messageType === "message") {
|
|
45614
|
+
this._appendHistory("owner", messageText);
|
|
45505
45615
|
const metadata = {
|
|
45506
45616
|
messageId: msg.id,
|
|
45507
45617
|
conversationId: msg.conversation_id,
|
|
@@ -45552,7 +45662,8 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45552
45662
|
for (const [convId, session] of this._sessions) {
|
|
45553
45663
|
this._persisted.sessions[convId] = {
|
|
45554
45664
|
ownerDeviceId: session.ownerDeviceId,
|
|
45555
|
-
ratchetState: session.ratchet.serialize()
|
|
45665
|
+
ratchetState: session.ratchet.serialize(),
|
|
45666
|
+
activated: session.activated
|
|
45556
45667
|
};
|
|
45557
45668
|
}
|
|
45558
45669
|
await saveState(this.config.dataDir, this._persisted);
|
|
@@ -45560,7 +45671,7 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45560
45671
|
};
|
|
45561
45672
|
|
|
45562
45673
|
// src/index.ts
|
|
45563
|
-
var VERSION = "0.
|
|
45674
|
+
var VERSION = "0.3.0";
|
|
45564
45675
|
export {
|
|
45565
45676
|
SecureChannel,
|
|
45566
45677
|
VERSION
|