@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/README.md ADDED
@@ -0,0 +1,229 @@
1
+ # @agentvault/secure-channel
2
+
3
+ End-to-end encrypted communication channel for AI agents on the [AgentVault](https://agentvault.chat) platform. Connect your agent to its owner with XChaCha20-Poly1305 encryption and Double Ratchet forward secrecy.
4
+
5
+ ## What's New in v0.4.0
6
+
7
+ **Webhook Notifications** — Your agent can now receive HTTP webhook callbacks when a new message arrives, even when it's not connected via WebSocket. This is ideal for serverless agents, agents that poll on a schedule, or any agent that isn't always online.
8
+
9
+ ### Upgrading from v0.3.x
10
+
11
+ ```bash
12
+ npm install @agentvault/secure-channel@latest
13
+ ```
14
+
15
+ Add `webhookUrl` to your config to enable:
16
+
17
+ ```js
18
+ const channel = new SecureChannel({
19
+ inviteToken: "your-token",
20
+ dataDir: "./agentvault-data",
21
+ apiUrl: "https://api.agentvault.chat",
22
+ webhookUrl: "https://your-server.com/webhook/agentvault", // NEW in 0.4.0
23
+ });
24
+ ```
25
+
26
+ No other code changes required — fully backward-compatible.
27
+
28
+ ---
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ npm install @agentvault/secure-channel
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ### Option 1: CLI (Interactive)
39
+
40
+ Run directly with npx using the invite token from your AgentVault dashboard:
41
+
42
+ ```bash
43
+ npx @agentvault/secure-channel --token=YOUR_INVITE_TOKEN
44
+ ```
45
+
46
+ The CLI will:
47
+ 1. Enroll your agent with the server
48
+ 2. Display a fingerprint for the owner to verify
49
+ 3. Wait for owner approval
50
+ 4. Establish an encrypted channel
51
+ 5. Enter interactive mode where you can send/receive messages
52
+
53
+ **CLI Flags:**
54
+
55
+ | Flag | Default | Description |
56
+ |------|---------|-------------|
57
+ | `--token` | (required on first run) | Invite token from dashboard |
58
+ | `--name` | `"CLI Agent"` | Agent display name |
59
+ | `--data-dir` | `./agentvault-data` | Directory for persistent state |
60
+ | `--api-url` | `https://api.agentvault.chat` | API endpoint |
61
+
62
+ Environment variables (`AGENTVAULT_INVITE_TOKEN`, `AGENTVAULT_AGENT_NAME`, `AGENTVAULT_DATA_DIR`, `AGENTVAULT_API_URL`) work as alternatives to flags.
63
+
64
+ ### Option 2: SDK (Programmatic)
65
+
66
+ ```js
67
+ import { SecureChannel } from "@agentvault/secure-channel";
68
+
69
+ const channel = new SecureChannel({
70
+ inviteToken: "YOUR_INVITE_TOKEN",
71
+ dataDir: "./agentvault-data",
72
+ apiUrl: "https://api.agentvault.chat",
73
+ agentName: "My Agent",
74
+ });
75
+
76
+ channel.on("message", (text, metadata) => {
77
+ console.log(`Received: ${text}`);
78
+ // Echo back
79
+ channel.send(`You said: ${text}`);
80
+ });
81
+
82
+ channel.on("ready", () => {
83
+ console.log("Secure channel established!");
84
+ });
85
+
86
+ await channel.start();
87
+ ```
88
+
89
+ ---
90
+
91
+ ## Webhook Notifications (v0.4.0+)
92
+
93
+ Enable webhook notifications so your agent gets an HTTP POST when a new message arrives — useful for agents that aren't always connected via WebSocket.
94
+
95
+ ### Setup
96
+
97
+ Add `webhookUrl` to your config:
98
+
99
+ ```js
100
+ const channel = new SecureChannel({
101
+ inviteToken: "YOUR_INVITE_TOKEN",
102
+ dataDir: "./agentvault-data",
103
+ apiUrl: "https://api.agentvault.chat",
104
+ webhookUrl: "https://your-server.com/webhook/agentvault",
105
+ });
106
+ ```
107
+
108
+ The webhook URL is registered automatically during device activation. The channel emits a `webhook_registered` event on success:
109
+
110
+ ```js
111
+ channel.on("webhook_registered", ({ url, secret }) => {
112
+ console.log(`Webhook registered at ${url}`);
113
+ // Save the secret to verify incoming webhooks
114
+ });
115
+ ```
116
+
117
+ ### Webhook Payload
118
+
119
+ When the owner sends a message, your webhook endpoint receives:
120
+
121
+ ```http
122
+ POST /webhook/agentvault HTTP/1.1
123
+ Content-Type: application/json
124
+ X-AgentVault-Event: new_message
125
+ X-AgentVault-Signature: sha256=<hmac-hex>
126
+
127
+ {
128
+ "event": "new_message",
129
+ "conversation_id": "uuid",
130
+ "sender_device_id": "uuid",
131
+ "message_id": "uuid",
132
+ "timestamp": "2026-02-17T12:00:00Z"
133
+ }
134
+ ```
135
+
136
+ ### Verifying Webhook Signatures
137
+
138
+ Each webhook includes an HMAC-SHA256 signature in the `X-AgentVault-Signature` header. Verify it using the secret from the `webhook_registered` event:
139
+
140
+ ```js
141
+ import crypto from "crypto";
142
+
143
+ function verifyWebhook(body, signature, secret) {
144
+ const expected = "sha256=" +
145
+ crypto.createHmac("sha256", secret).update(body).digest("hex");
146
+ return crypto.timingSafeEqual(
147
+ Buffer.from(signature),
148
+ Buffer.from(expected),
149
+ );
150
+ }
151
+ ```
152
+
153
+ ---
154
+
155
+ ## Configuration Reference
156
+
157
+ ```ts
158
+ interface SecureChannelConfig {
159
+ // Required
160
+ inviteToken: string; // Invite token from the AgentVault dashboard
161
+ dataDir: string; // Directory for persistent state files
162
+ apiUrl: string; // API endpoint (e.g., "https://api.agentvault.chat")
163
+
164
+ // Optional
165
+ agentName?: string; // Display name (default: "CLI Agent")
166
+ platform?: string; // Platform identifier (e.g., "node")
167
+ maxHistorySize?: number; // Max stored messages for cross-device replay (default: 500)
168
+ webhookUrl?: string; // Webhook URL for new message notifications (v0.4.0+)
169
+
170
+ // Callbacks (alternative to event listeners)
171
+ onMessage?: (text: string, metadata: MessageMetadata) => void;
172
+ onStateChange?: (state: ChannelState) => void;
173
+ }
174
+ ```
175
+
176
+ ## Events
177
+
178
+ | Event | Payload | Description |
179
+ |-------|---------|-------------|
180
+ | `message` | `(text: string, metadata: MessageMetadata)` | Owner sent a message |
181
+ | `ready` | none | WebSocket connected, channel operational |
182
+ | `state` | `(state: ChannelState)` | State transition occurred |
183
+ | `error` | `(error: Error)` | Fatal error |
184
+ | `webhook_registered` | `({ url: string, secret: string })` | Webhook registered (v0.4.0+) |
185
+
186
+ ## Channel States
187
+
188
+ `idle` → `enrolling` → `polling` → `activating` → `connecting` → `ready`
189
+
190
+ If disconnected: `ready` → `disconnected` → `connecting` → `ready` (auto-reconnect)
191
+
192
+ ## API
193
+
194
+ | Method | Description |
195
+ |--------|-------------|
196
+ | `start()` | Initialize, enroll, and connect |
197
+ | `send(text)` | Encrypt and send message to all owner devices |
198
+ | `stop()` | Gracefully disconnect |
199
+
200
+ | Property | Description |
201
+ |----------|-------------|
202
+ | `state` | Current channel state |
203
+ | `deviceId` | Agent's device ID (after enrollment) |
204
+ | `fingerprint` | Device fingerprint for verification |
205
+ | `conversationId` | Primary conversation ID |
206
+ | `conversationIds` | All active conversation IDs (multi-device) |
207
+ | `sessionCount` | Number of active encrypted sessions |
208
+
209
+ ## Multi-Device Support
210
+
211
+ AgentVault supports multiple owner devices (e.g., desktop + mobile). The channel automatically:
212
+ - Maintains independent encrypted sessions per owner device
213
+ - Fans out `send()` to all active sessions
214
+ - Stores message history for cross-device replay (up to `maxHistorySize`)
215
+ - Replays history when a new device connects
216
+
217
+ No additional configuration needed — multi-device is handled transparently.
218
+
219
+ ## Security
220
+
221
+ - **XChaCha20-Poly1305** symmetric encryption (192-bit nonces)
222
+ - **X3DH** key agreement (Ed25519 + X25519)
223
+ - **Double Ratchet** for forward secrecy — old keys deleted after use
224
+ - **Zero-knowledge server** — the server never sees plaintext
225
+ - **HMAC-SHA256** webhook signatures for authenticity verification
226
+
227
+ ## License
228
+
229
+ MIT
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
@@ -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