@agentvault/secure-channel 0.4.0 → 0.4.2

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.js CHANGED
@@ -45127,17 +45127,21 @@ var SecureChannel = class extends EventEmitter {
45127
45127
  /**
45128
45128
  * Append a message to persistent history for cross-device replay.
45129
45129
  */
45130
- _appendHistory(sender, text) {
45130
+ _appendHistory(sender, text, topicId) {
45131
45131
  if (!this._persisted) return;
45132
45132
  if (!this._persisted.messageHistory) {
45133
45133
  this._persisted.messageHistory = [];
45134
45134
  }
45135
45135
  const maxSize = this.config.maxHistorySize ?? 500;
45136
- this._persisted.messageHistory.push({
45136
+ const entry = {
45137
45137
  sender,
45138
45138
  text,
45139
45139
  ts: (/* @__PURE__ */ new Date()).toISOString()
45140
- });
45140
+ };
45141
+ if (topicId) {
45142
+ entry.topicId = topicId;
45143
+ }
45144
+ this._persisted.messageHistory.push(entry);
45141
45145
  if (this._persisted.messageHistory.length > maxSize) {
45142
45146
  this._persisted.messageHistory = this._persisted.messageHistory.slice(-maxSize);
45143
45147
  }
@@ -45146,11 +45150,12 @@ var SecureChannel = class extends EventEmitter {
45146
45150
  * Encrypt and send a message to ALL owner devices (fanout).
45147
45151
  * Each session gets the same plaintext encrypted independently.
45148
45152
  */
45149
- async send(plaintext) {
45153
+ async send(plaintext, options) {
45150
45154
  if (this._state !== "ready" || this._sessions.size === 0 || !this._ws) {
45151
45155
  throw new Error("Channel is not ready");
45152
45156
  }
45153
- this._appendHistory("agent", plaintext);
45157
+ const topicId = options?.topicId ?? this._persisted?.defaultTopicId;
45158
+ this._appendHistory("agent", plaintext, topicId);
45154
45159
  const messageGroupId = randomUUID();
45155
45160
  for (const [convId, session] of this._sessions) {
45156
45161
  if (!session.activated) {
@@ -45165,7 +45170,8 @@ var SecureChannel = class extends EventEmitter {
45165
45170
  conversation_id: convId,
45166
45171
  header_blob: transport.header_blob,
45167
45172
  ciphertext: transport.ciphertext,
45168
- message_group_id: messageGroupId
45173
+ message_group_id: messageGroupId,
45174
+ topic_id: topicId
45169
45175
  }
45170
45176
  })
45171
45177
  );
@@ -45189,6 +45195,72 @@ var SecureChannel = class extends EventEmitter {
45189
45195
  }
45190
45196
  this._setState("disconnected");
45191
45197
  }
45198
+ // --- Topic management ---
45199
+ /**
45200
+ * Create a new topic within the conversation group.
45201
+ * Requires the channel to be initialized with a groupId (from activation).
45202
+ */
45203
+ async createTopic(name) {
45204
+ if (!this._persisted?.groupId) {
45205
+ throw new Error("Channel not initialized or groupId unknown");
45206
+ }
45207
+ if (!this._deviceJwt) {
45208
+ throw new Error("Channel not authenticated");
45209
+ }
45210
+ const res = await fetch(`${this.config.apiUrl}/api/v1/topics`, {
45211
+ method: "POST",
45212
+ headers: {
45213
+ "Content-Type": "application/json",
45214
+ Authorization: `Bearer ${this._deviceJwt}`
45215
+ },
45216
+ body: JSON.stringify({
45217
+ group_id: this._persisted.groupId,
45218
+ name,
45219
+ creator_device_id: this._persisted.deviceId
45220
+ })
45221
+ });
45222
+ if (!res.ok) {
45223
+ const detail = await res.text();
45224
+ throw new Error(`Create topic failed (${res.status}): ${detail}`);
45225
+ }
45226
+ const resp = await res.json();
45227
+ const topic = { id: resp.id, name: resp.name, isDefault: resp.is_default };
45228
+ if (!this._persisted.topics) {
45229
+ this._persisted.topics = [];
45230
+ }
45231
+ this._persisted.topics.push(topic);
45232
+ await this._persistState();
45233
+ return topic;
45234
+ }
45235
+ /**
45236
+ * List all topics in the conversation group.
45237
+ * Requires the channel to be initialized with a groupId (from activation).
45238
+ */
45239
+ async listTopics() {
45240
+ if (!this._persisted?.groupId) {
45241
+ throw new Error("Channel not initialized or groupId unknown");
45242
+ }
45243
+ if (!this._deviceJwt) {
45244
+ throw new Error("Channel not authenticated");
45245
+ }
45246
+ const res = await fetch(
45247
+ `${this.config.apiUrl}/api/v1/topics?group_id=${encodeURIComponent(this._persisted.groupId)}`,
45248
+ {
45249
+ headers: {
45250
+ Authorization: `Bearer ${this._deviceJwt}`
45251
+ }
45252
+ }
45253
+ );
45254
+ if (!res.ok) {
45255
+ const detail = await res.text();
45256
+ throw new Error(`List topics failed (${res.status}): ${detail}`);
45257
+ }
45258
+ const resp = await res.json();
45259
+ const topics = resp.map((t2) => ({ id: t2.id, name: t2.name, isDefault: t2.is_default }));
45260
+ this._persisted.topics = topics;
45261
+ await this._persistState();
45262
+ return topics;
45263
+ }
45192
45264
  // --- Internal lifecycle ---
45193
45265
  async _enroll() {
45194
45266
  this._setState("enrolling");
@@ -45315,6 +45387,15 @@ var SecureChannel = class extends EventEmitter {
45315
45387
  sessions,
45316
45388
  messageHistory: this._persisted.messageHistory ?? []
45317
45389
  };
45390
+ if (conversations.length > 0) {
45391
+ const firstConv = conversations[0];
45392
+ if (firstConv.group_id) {
45393
+ this._persisted.groupId = firstConv.group_id;
45394
+ }
45395
+ if (firstConv.default_topic_id) {
45396
+ this._persisted.defaultTopicId = firstConv.default_topic_id;
45397
+ }
45398
+ }
45318
45399
  await saveState(this.config.dataDir, this._persisted);
45319
45400
  if (this.config.webhookUrl) {
45320
45401
  try {
@@ -45352,6 +45433,14 @@ var SecureChannel = class extends EventEmitter {
45352
45433
  }
45353
45434
  _connect() {
45354
45435
  if (this._stopped) return;
45436
+ if (this._ws) {
45437
+ this._ws.removeAllListeners();
45438
+ try {
45439
+ this._ws.close();
45440
+ } catch {
45441
+ }
45442
+ this._ws = null;
45443
+ }
45355
45444
  this._setState("connecting");
45356
45445
  const wsUrl = this.config.apiUrl.replace(/^http/, "ws");
45357
45446
  const url = `${wsUrl}/api/v1/ws?token=${encodeURIComponent(this._deviceJwt)}&device_id=${this._deviceId}`;
@@ -45436,15 +45525,17 @@ var SecureChannel = class extends EventEmitter {
45436
45525
  return;
45437
45526
  }
45438
45527
  if (messageType === "message") {
45439
- this._appendHistory("owner", messageText);
45528
+ const topicId = msgData.topic_id;
45529
+ this._appendHistory("owner", messageText, topicId);
45440
45530
  const metadata = {
45441
45531
  messageId: msgData.message_id,
45442
45532
  conversationId: convId,
45443
- timestamp: msgData.created_at
45533
+ timestamp: msgData.created_at,
45534
+ topicId
45444
45535
  };
45445
45536
  this.emit("message", messageText, metadata);
45446
45537
  this.config.onMessage?.(messageText, metadata);
45447
- await this._relaySyncToSiblings(convId, session.ownerDeviceId, messageText);
45538
+ await this._relaySyncToSiblings(convId, session.ownerDeviceId, messageText, topicId);
45448
45539
  }
45449
45540
  if (this._persisted) {
45450
45541
  this._persisted.lastMessageTimestamp = msgData.created_at;
@@ -45455,13 +45546,14 @@ var SecureChannel = class extends EventEmitter {
45455
45546
  * Relay an owner's message to all sibling sessions as encrypted sync messages.
45456
45547
  * This allows all owner devices to see messages from any single device.
45457
45548
  */
45458
- async _relaySyncToSiblings(sourceConvId, senderOwnerDeviceId, messageText) {
45549
+ async _relaySyncToSiblings(sourceConvId, senderOwnerDeviceId, messageText, topicId) {
45459
45550
  if (!this._ws || this._sessions.size <= 1) return;
45460
45551
  const syncPayload = JSON.stringify({
45461
45552
  type: "sync",
45462
45553
  sender: senderOwnerDeviceId,
45463
45554
  text: messageText,
45464
- ts: (/* @__PURE__ */ new Date()).toISOString()
45555
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
45556
+ topicId
45465
45557
  });
45466
45558
  for (const [siblingConvId, siblingSession] of this._sessions) {
45467
45559
  if (siblingConvId === sourceConvId) continue;
@@ -45611,11 +45703,13 @@ var SecureChannel = class extends EventEmitter {
45611
45703
  messageText = plaintext;
45612
45704
  }
45613
45705
  if (messageType === "message") {
45614
- this._appendHistory("owner", messageText);
45706
+ const topicId = msg.topic_id;
45707
+ this._appendHistory("owner", messageText, topicId);
45615
45708
  const metadata = {
45616
45709
  messageId: msg.id,
45617
45710
  conversationId: msg.conversation_id,
45618
- timestamp: msg.created_at
45711
+ timestamp: msg.created_at,
45712
+ topicId
45619
45713
  };
45620
45714
  this.emit("message", messageText, metadata);
45621
45715
  this.config.onMessage?.(messageText, metadata);
@@ -45632,6 +45726,7 @@ var SecureChannel = class extends EventEmitter {
45632
45726
  }
45633
45727
  _scheduleReconnect() {
45634
45728
  if (this._stopped) return;
45729
+ if (this._reconnectTimer) return;
45635
45730
  const delay = Math.min(
45636
45731
  RECONNECT_BASE_MS * Math.pow(2, this._reconnectAttempt),
45637
45732
  RECONNECT_MAX_MS