@agentvault/secure-channel 0.4.1 → 0.4.3

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 CHANGED
@@ -33,8 +33,28 @@ export declare class SecureChannel extends EventEmitter {
33
33
  * Encrypt and send a message to ALL owner devices (fanout).
34
34
  * Each session gets the same plaintext encrypted independently.
35
35
  */
36
- send(plaintext: string): Promise<void>;
36
+ send(plaintext: string, options?: {
37
+ topicId?: string;
38
+ }): Promise<void>;
37
39
  stop(): Promise<void>;
40
+ /**
41
+ * Create a new topic within the conversation group.
42
+ * Requires the channel to be initialized with a groupId (from activation).
43
+ */
44
+ createTopic(name: string): Promise<{
45
+ id: string;
46
+ name: string;
47
+ isDefault: boolean;
48
+ }>;
49
+ /**
50
+ * List all topics in the conversation group.
51
+ * Requires the channel to be initialized with a groupId (from activation).
52
+ */
53
+ listTopics(): Promise<Array<{
54
+ id: string;
55
+ name: string;
56
+ isDefault: boolean;
57
+ }>>;
38
58
  private _enroll;
39
59
  private _poll;
40
60
  private _activate;
@@ -1 +1 @@
1
- {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAa3C,OAAO,KAAK,EACV,mBAAmB,EACnB,YAAY,EAKb,MAAM,YAAY,CAAC;AAiDpB,qBAAa,aAAc,SAAQ,YAAY;IAiBjC,OAAO,CAAC,MAAM;IAhB1B,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,sBAAsB,CAAc;IAC5C,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,SAAS,CAGH;IACd,OAAO,CAAC,GAAG,CAA0B;IACrC,OAAO,CAAC,UAAU,CAA8C;IAChE,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,eAAe,CAA8C;IACrE,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAA+B;gBAE7B,MAAM,EAAE,mBAAmB;IAI/C,IAAI,KAAK,IAAI,YAAY,CAExB;IAED,IAAI,QAAQ,IAAI,MAAM,GAAG,IAAI,CAE5B;IAED,IAAI,WAAW,IAAI,MAAM,GAAG,IAAI,CAE/B;IAED,iEAAiE;IACjE,IAAI,cAAc,IAAI,MAAM,GAAG,IAAI,CAElC;IAED,2CAA2C;IAC3C,IAAI,eAAe,IAAI,MAAM,EAAE,CAE9B;IAED,6CAA6C;IAC7C,IAAI,YAAY,IAAI,MAAM,CAEzB;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAsC5B;;OAEG;IACH,OAAO,CAAC,cAAc;IAmBtB;;;OAGG;IACG,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAsCtC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAoBb,OAAO;IAgDrB,OAAO,CAAC,KAAK;YAgCC,SAAS;IA2HvB,OAAO,CAAC,QAAQ;IA4DhB;;;;OAIG;YACW,sBAAsB;IAoFpC;;;OAGG;YACW,oBAAoB;IAmClC;;;OAGG;YACW,uBAAuB;IAkCrC;;;;OAIG;YACW,mBAAmB;IAkEjC;;;OAGG;YACW,mBAAmB;IAwFjC,OAAO,CAAC,kBAAkB;IAgB1B,OAAO,CAAC,SAAS;IAOjB,OAAO,CAAC,YAAY;IAKpB;;;OAGG;YACW,aAAa;CAc5B"}
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAa3C,OAAO,KAAK,EACV,mBAAmB,EACnB,YAAY,EAKb,MAAM,YAAY,CAAC;AAiDpB,qBAAa,aAAc,SAAQ,YAAY;IAiBjC,OAAO,CAAC,MAAM;IAhB1B,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,sBAAsB,CAAc;IAC5C,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,SAAS,CAGH;IACd,OAAO,CAAC,GAAG,CAA0B;IACrC,OAAO,CAAC,UAAU,CAA8C;IAChE,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,eAAe,CAA8C;IACrE,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAA+B;gBAE7B,MAAM,EAAE,mBAAmB;IAI/C,IAAI,KAAK,IAAI,YAAY,CAExB;IAED,IAAI,QAAQ,IAAI,MAAM,GAAG,IAAI,CAE5B;IAED,IAAI,WAAW,IAAI,MAAM,GAAG,IAAI,CAE/B;IAED,iEAAiE;IACjE,IAAI,cAAc,IAAI,MAAM,GAAG,IAAI,CAElC;IAED,2CAA2C;IAC3C,IAAI,eAAe,IAAI,MAAM,EAAE,CAE9B;IAED,6CAA6C;IAC7C,IAAI,YAAY,IAAI,MAAM,CAEzB;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAsC5B;;OAEG;IACH,OAAO,CAAC,cAAc;IAuBtB;;;OAGG;IACG,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAyCtE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB3B;;;OAGG;IACG,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;IAsC1F;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;YAiCtE,OAAO;IAgDrB,OAAO,CAAC,KAAK;YAgCC,SAAS;IAyIvB,OAAO,CAAC,QAAQ;IAsEhB;;;;OAIG;YACW,sBAAsB;IAwFpC;;;OAGG;YACW,oBAAoB;IAqClC;;;OAGG;YACW,uBAAuB;IAkCrC;;;;OAIG;YACW,mBAAmB;IAkEjC;;;OAGG;YACW,mBAAmB;IA4FjC,OAAO,CAAC,kBAAkB;IAiB1B,OAAO,CAAC,SAAS;IAOjB,OAAO,CAAC,YAAY;IAKpB;;;OAGG;YACW,aAAa;CAc5B"}
package/dist/cli.js CHANGED
@@ -45033,7 +45033,7 @@ async function activateDevice(apiUrl2, deviceId) {
45033
45033
  }
45034
45034
 
45035
45035
  // src/channel.ts
45036
- var POLL_INTERVAL_MS = 5e3;
45036
+ var POLL_INTERVAL_MS = 3e3;
45037
45037
  var RECONNECT_BASE_MS = 1e3;
45038
45038
  var RECONNECT_MAX_MS = 3e4;
45039
45039
  function migratePersistedState(raw) {
@@ -45129,17 +45129,21 @@ var SecureChannel = class extends EventEmitter {
45129
45129
  /**
45130
45130
  * Append a message to persistent history for cross-device replay.
45131
45131
  */
45132
- _appendHistory(sender, text) {
45132
+ _appendHistory(sender, text, topicId) {
45133
45133
  if (!this._persisted) return;
45134
45134
  if (!this._persisted.messageHistory) {
45135
45135
  this._persisted.messageHistory = [];
45136
45136
  }
45137
45137
  const maxSize = this.config.maxHistorySize ?? 500;
45138
- this._persisted.messageHistory.push({
45138
+ const entry = {
45139
45139
  sender,
45140
45140
  text,
45141
45141
  ts: (/* @__PURE__ */ new Date()).toISOString()
45142
- });
45142
+ };
45143
+ if (topicId) {
45144
+ entry.topicId = topicId;
45145
+ }
45146
+ this._persisted.messageHistory.push(entry);
45143
45147
  if (this._persisted.messageHistory.length > maxSize) {
45144
45148
  this._persisted.messageHistory = this._persisted.messageHistory.slice(-maxSize);
45145
45149
  }
@@ -45148,11 +45152,12 @@ var SecureChannel = class extends EventEmitter {
45148
45152
  * Encrypt and send a message to ALL owner devices (fanout).
45149
45153
  * Each session gets the same plaintext encrypted independently.
45150
45154
  */
45151
- async send(plaintext) {
45155
+ async send(plaintext, options) {
45152
45156
  if (this._state !== "ready" || this._sessions.size === 0 || !this._ws) {
45153
45157
  throw new Error("Channel is not ready");
45154
45158
  }
45155
- this._appendHistory("agent", plaintext);
45159
+ const topicId = options?.topicId ?? this._persisted?.defaultTopicId;
45160
+ this._appendHistory("agent", plaintext, topicId);
45156
45161
  const messageGroupId = randomUUID();
45157
45162
  for (const [convId, session] of this._sessions) {
45158
45163
  if (!session.activated) {
@@ -45167,7 +45172,8 @@ var SecureChannel = class extends EventEmitter {
45167
45172
  conversation_id: convId,
45168
45173
  header_blob: transport.header_blob,
45169
45174
  ciphertext: transport.ciphertext,
45170
- message_group_id: messageGroupId
45175
+ message_group_id: messageGroupId,
45176
+ topic_id: topicId
45171
45177
  }
45172
45178
  })
45173
45179
  );
@@ -45191,6 +45197,72 @@ var SecureChannel = class extends EventEmitter {
45191
45197
  }
45192
45198
  this._setState("disconnected");
45193
45199
  }
45200
+ // --- Topic management ---
45201
+ /**
45202
+ * Create a new topic within the conversation group.
45203
+ * Requires the channel to be initialized with a groupId (from activation).
45204
+ */
45205
+ async createTopic(name2) {
45206
+ if (!this._persisted?.groupId) {
45207
+ throw new Error("Channel not initialized or groupId unknown");
45208
+ }
45209
+ if (!this._deviceJwt) {
45210
+ throw new Error("Channel not authenticated");
45211
+ }
45212
+ const res = await fetch(`${this.config.apiUrl}/api/v1/topics`, {
45213
+ method: "POST",
45214
+ headers: {
45215
+ "Content-Type": "application/json",
45216
+ Authorization: `Bearer ${this._deviceJwt}`
45217
+ },
45218
+ body: JSON.stringify({
45219
+ group_id: this._persisted.groupId,
45220
+ name: name2,
45221
+ creator_device_id: this._persisted.deviceId
45222
+ })
45223
+ });
45224
+ if (!res.ok) {
45225
+ const detail = await res.text();
45226
+ throw new Error(`Create topic failed (${res.status}): ${detail}`);
45227
+ }
45228
+ const resp = await res.json();
45229
+ const topic = { id: resp.id, name: resp.name, isDefault: resp.is_default };
45230
+ if (!this._persisted.topics) {
45231
+ this._persisted.topics = [];
45232
+ }
45233
+ this._persisted.topics.push(topic);
45234
+ await this._persistState();
45235
+ return topic;
45236
+ }
45237
+ /**
45238
+ * List all topics in the conversation group.
45239
+ * Requires the channel to be initialized with a groupId (from activation).
45240
+ */
45241
+ async listTopics() {
45242
+ if (!this._persisted?.groupId) {
45243
+ throw new Error("Channel not initialized or groupId unknown");
45244
+ }
45245
+ if (!this._deviceJwt) {
45246
+ throw new Error("Channel not authenticated");
45247
+ }
45248
+ const res = await fetch(
45249
+ `${this.config.apiUrl}/api/v1/topics?group_id=${encodeURIComponent(this._persisted.groupId)}`,
45250
+ {
45251
+ headers: {
45252
+ Authorization: `Bearer ${this._deviceJwt}`
45253
+ }
45254
+ }
45255
+ );
45256
+ if (!res.ok) {
45257
+ const detail = await res.text();
45258
+ throw new Error(`List topics failed (${res.status}): ${detail}`);
45259
+ }
45260
+ const resp = await res.json();
45261
+ const topics = resp.map((t2) => ({ id: t2.id, name: t2.name, isDefault: t2.is_default }));
45262
+ this._persisted.topics = topics;
45263
+ await this._persistState();
45264
+ return topics;
45265
+ }
45194
45266
  // --- Internal lifecycle ---
45195
45267
  async _enroll() {
45196
45268
  this._setState("enrolling");
@@ -45317,6 +45389,15 @@ var SecureChannel = class extends EventEmitter {
45317
45389
  sessions,
45318
45390
  messageHistory: this._persisted.messageHistory ?? []
45319
45391
  };
45392
+ if (conversations.length > 0) {
45393
+ const firstConv = conversations[0];
45394
+ if (firstConv.group_id) {
45395
+ this._persisted.groupId = firstConv.group_id;
45396
+ }
45397
+ if (firstConv.default_topic_id) {
45398
+ this._persisted.defaultTopicId = firstConv.default_topic_id;
45399
+ }
45400
+ }
45320
45401
  await saveState(this.config.dataDir, this._persisted);
45321
45402
  if (this.config.webhookUrl) {
45322
45403
  try {
@@ -45354,6 +45435,14 @@ var SecureChannel = class extends EventEmitter {
45354
45435
  }
45355
45436
  _connect() {
45356
45437
  if (this._stopped) return;
45438
+ if (this._ws) {
45439
+ this._ws.removeAllListeners();
45440
+ try {
45441
+ this._ws.close();
45442
+ } catch {
45443
+ }
45444
+ this._ws = null;
45445
+ }
45357
45446
  this._setState("connecting");
45358
45447
  const wsUrl = this.config.apiUrl.replace(/^http/, "ws");
45359
45448
  const url = `${wsUrl}/api/v1/ws?token=${encodeURIComponent(this._deviceJwt)}&device_id=${this._deviceId}`;
@@ -45438,15 +45527,17 @@ var SecureChannel = class extends EventEmitter {
45438
45527
  return;
45439
45528
  }
45440
45529
  if (messageType === "message") {
45441
- this._appendHistory("owner", messageText);
45530
+ const topicId = msgData.topic_id;
45531
+ this._appendHistory("owner", messageText, topicId);
45442
45532
  const metadata = {
45443
45533
  messageId: msgData.message_id,
45444
45534
  conversationId: convId,
45445
- timestamp: msgData.created_at
45535
+ timestamp: msgData.created_at,
45536
+ topicId
45446
45537
  };
45447
45538
  this.emit("message", messageText, metadata);
45448
45539
  this.config.onMessage?.(messageText, metadata);
45449
- await this._relaySyncToSiblings(convId, session.ownerDeviceId, messageText);
45540
+ await this._relaySyncToSiblings(convId, session.ownerDeviceId, messageText, topicId);
45450
45541
  }
45451
45542
  if (this._persisted) {
45452
45543
  this._persisted.lastMessageTimestamp = msgData.created_at;
@@ -45457,13 +45548,14 @@ var SecureChannel = class extends EventEmitter {
45457
45548
  * Relay an owner's message to all sibling sessions as encrypted sync messages.
45458
45549
  * This allows all owner devices to see messages from any single device.
45459
45550
  */
45460
- async _relaySyncToSiblings(sourceConvId, senderOwnerDeviceId, messageText) {
45551
+ async _relaySyncToSiblings(sourceConvId, senderOwnerDeviceId, messageText, topicId) {
45461
45552
  if (!this._ws || this._sessions.size <= 1) return;
45462
45553
  const syncPayload = JSON.stringify({
45463
45554
  type: "sync",
45464
45555
  sender: senderOwnerDeviceId,
45465
45556
  text: messageText,
45466
- ts: (/* @__PURE__ */ new Date()).toISOString()
45557
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
45558
+ topicId
45467
45559
  });
45468
45560
  for (const [siblingConvId, siblingSession] of this._sessions) {
45469
45561
  if (siblingConvId === sourceConvId) continue;
@@ -45613,11 +45705,13 @@ var SecureChannel = class extends EventEmitter {
45613
45705
  messageText = plaintext;
45614
45706
  }
45615
45707
  if (messageType === "message") {
45616
- this._appendHistory("owner", messageText);
45708
+ const topicId = msg.topic_id;
45709
+ this._appendHistory("owner", messageText, topicId);
45617
45710
  const metadata = {
45618
45711
  messageId: msg.id,
45619
45712
  conversationId: msg.conversation_id,
45620
- timestamp: msg.created_at
45713
+ timestamp: msg.created_at,
45714
+ topicId
45621
45715
  };
45622
45716
  this.emit("message", messageText, metadata);
45623
45717
  this.config.onMessage?.(messageText, metadata);
@@ -45634,6 +45728,7 @@ var SecureChannel = class extends EventEmitter {
45634
45728
  }
45635
45729
  _scheduleReconnect() {
45636
45730
  if (this._stopped) return;
45731
+ if (this._reconnectTimer) return;
45637
45732
  const delay = Math.min(
45638
45733
  RECONNECT_BASE_MS * Math.pow(2, this._reconnectAttempt),
45639
45734
  RECONNECT_MAX_MS