@agentvault/agentvault 0.9.5 → 0.9.7

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.
@@ -0,0 +1,4 @@
1
+ // Re-export transport utilities from shared @agentvault/crypto package.
2
+ // Plugin code continues importing from ./crypto-helpers.js — no import changes needed.
3
+ export { hexToBytes, bytesToHex, base64ToBytes, bytesToBase64, encryptedMessageToTransport, transportToEncryptedMessage, } from "@agentvault/crypto";
4
+ //# sourceMappingURL=crypto-helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto-helpers.js","sourceRoot":"","sources":["../src/crypto-helpers.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,uFAAuF;AACvF,OAAO,EACL,UAAU,EACV,UAAU,EACV,aAAa,EACb,aAAa,EACb,2BAA2B,EAC3B,2BAA2B,GAE5B,MAAM,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -73,9 +73,9 @@ if (typeof Module.getRandomValue === "undefined") {
73
73
  Module.getRandomValue = randomValuesStandard;
74
74
  } catch (e) {
75
75
  try {
76
- crypto = null;
76
+ crypto2 = null;
77
77
  randomValueNodeJS = function() {
78
- var buf = crypto["randomBytes"](4);
78
+ var buf = crypto2["randomBytes"](4);
79
79
  return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
80
80
  };
81
81
  randomValueNodeJS();
@@ -88,7 +88,7 @@ if (typeof Module.getRandomValue === "undefined") {
88
88
  var window_;
89
89
  var crypto_;
90
90
  var randomValuesStandard;
91
- var crypto;
91
+ var crypto2;
92
92
  var randomValueNodeJS;
93
93
  var _Module = Module;
94
94
  Module.ready = new Promise(function(resolve2, reject) {
@@ -40283,7 +40283,7 @@ Module.ready = new Promise(function(resolve2, reject) {
40283
40283
  try {
40284
40284
  var window_ = "object" === typeof window ? window : self;
40285
40285
  var crypto_ = typeof window_.crypto !== "undefined" ? window_.crypto : window_.msCrypto;
40286
- crypto_ = crypto_ === void 0 ? crypto : crypto_;
40286
+ crypto_ = crypto_ === void 0 ? crypto2 : crypto_;
40287
40287
  var randomValuesStandard = function() {
40288
40288
  var buf = new Uint32Array(1);
40289
40289
  crypto_.getRandomValues(buf);
@@ -40293,9 +40293,9 @@ Module.ready = new Promise(function(resolve2, reject) {
40293
40293
  Module3.getRandomValue = randomValuesStandard;
40294
40294
  } catch (e) {
40295
40295
  try {
40296
- var crypto = null;
40296
+ var crypto2 = null;
40297
40297
  var randomValueNodeJS = function() {
40298
- var buf = crypto["randomBytes"](4);
40298
+ var buf = crypto2["randomBytes"](4);
40299
40299
  return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
40300
40300
  };
40301
40301
  randomValueNodeJS();
@@ -41343,7 +41343,7 @@ Module.ready = new Promise(function(resolve2, reject) {
41343
41343
  try {
41344
41344
  var window_ = "object" === typeof window ? window : self;
41345
41345
  var crypto_ = typeof window_.crypto !== "undefined" ? window_.crypto : window_.msCrypto;
41346
- crypto_ = crypto_ === void 0 ? crypto : crypto_;
41346
+ crypto_ = crypto_ === void 0 ? crypto2 : crypto_;
41347
41347
  var randomValuesStandard = function() {
41348
41348
  var buf = new Uint32Array(1);
41349
41349
  crypto_.getRandomValues(buf);
@@ -41353,9 +41353,9 @@ Module.ready = new Promise(function(resolve2, reject) {
41353
41353
  Module2.getRandomValue = randomValuesStandard;
41354
41354
  } catch (e) {
41355
41355
  try {
41356
- var crypto = null;
41356
+ var crypto2 = null;
41357
41357
  var randomValueNodeJS = function() {
41358
- var buf = crypto["randomBytes"](4);
41358
+ var buf = crypto2["randomBytes"](4);
41359
41359
  return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
41360
41360
  };
41361
41361
  randomValueNodeJS();
@@ -44766,6 +44766,22 @@ var DoubleRatchet = class _DoubleRatchet {
44766
44766
  const currentDhHex = this.state.dhReceivingPublicKey ? libsodium_wrappers_default.to_hex(this.state.dhReceivingPublicKey) : null;
44767
44767
  if (currentDhHex === null && this.state.receivingChain) {
44768
44768
  this.state.dhReceivingPublicKey = message.header.dhPublicKey;
44769
+ } else if (currentDhHex === null && !this.state.receivingChain) {
44770
+ if (message.header.messageNumber === 0) {
44771
+ try {
44772
+ const { messageKey: testKey, nextChainKey: nextChainKey2 } = kdfChainKey(this.state.rootKey);
44773
+ const ad2 = serializeHeader(message.header);
44774
+ const ptBytes = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.ciphertext, ad2, message.nonce, testKey);
44775
+ this.state.dhReceivingPublicKey = message.header.dhPublicKey;
44776
+ this.state.receivingChain = {
44777
+ chainKey: nextChainKey2,
44778
+ messageNumber: 1
44779
+ };
44780
+ return libsodium_wrappers_default.to_string(ptBytes);
44781
+ } catch {
44782
+ }
44783
+ }
44784
+ this.dhRatchetReceive(message.header.dhPublicKey);
44769
44785
  } else if (headerDhHex !== currentDhHex) {
44770
44786
  if (this.state.receivingChain && this.state.dhReceivingPublicKey) {
44771
44787
  this.skipMessages(this.state.receivingChain, message.header.previousChainLength, this.state.dhReceivingPublicKey);
@@ -45149,6 +45165,208 @@ function transportToEncryptedMessage(transport) {
45149
45165
  };
45150
45166
  }
45151
45167
 
45168
+ // ../crypto/dist/telemetry.js
45169
+ function randomHex(byteCount) {
45170
+ const bytes = new Uint8Array(byteCount);
45171
+ crypto.getRandomValues(bytes);
45172
+ let hex = "";
45173
+ for (let i2 = 0; i2 < bytes.length; i2++) {
45174
+ hex += bytes[i2].toString(16).padStart(2, "0");
45175
+ }
45176
+ return hex;
45177
+ }
45178
+ function generateTraceId() {
45179
+ return randomHex(16);
45180
+ }
45181
+ function generateSpanId() {
45182
+ return randomHex(8);
45183
+ }
45184
+ function buildLlmSpan(opts) {
45185
+ const now = Date.now();
45186
+ const attributes = {
45187
+ "ai.agent.llm.model": opts.model,
45188
+ "ai.agent.llm.latency_ms": opts.latencyMs,
45189
+ "ai.agent.llm.tokens_input": opts.tokensInput,
45190
+ "ai.agent.llm.tokens_output": opts.tokensOutput
45191
+ };
45192
+ if (opts.provider !== void 0) {
45193
+ attributes["ai.agent.llm.provider"] = opts.provider;
45194
+ }
45195
+ const isError = opts.status === "error";
45196
+ const status = isError ? { code: 2, ...opts.statusMessage ? { message: opts.statusMessage } : {} } : { code: 0 };
45197
+ return {
45198
+ traceId: opts.traceId ?? generateTraceId(),
45199
+ spanId: opts.spanId ?? generateSpanId(),
45200
+ parentSpanId: opts.parentSpanId,
45201
+ name: "llm.inference",
45202
+ kind: "internal",
45203
+ startTime: now - opts.latencyMs,
45204
+ endTime: now,
45205
+ attributes,
45206
+ status
45207
+ };
45208
+ }
45209
+ function buildToolSpan(opts) {
45210
+ const now = Date.now();
45211
+ const attributes = {
45212
+ "ai.agent.tool.name": opts.toolName,
45213
+ "ai.agent.tool.latency_ms": opts.latencyMs,
45214
+ "ai.agent.tool.success": opts.success
45215
+ };
45216
+ const status = opts.success ? { code: 0 } : { code: 2, ...opts.errorMessage ? { message: opts.errorMessage } : {} };
45217
+ return {
45218
+ traceId: opts.traceId ?? generateTraceId(),
45219
+ spanId: opts.spanId ?? generateSpanId(),
45220
+ parentSpanId: opts.parentSpanId,
45221
+ name: "tool.execute",
45222
+ kind: "internal",
45223
+ startTime: now - opts.latencyMs,
45224
+ endTime: now,
45225
+ attributes,
45226
+ status
45227
+ };
45228
+ }
45229
+ function buildErrorSpan(opts) {
45230
+ const now = Date.now();
45231
+ return {
45232
+ traceId: opts.traceId ?? generateTraceId(),
45233
+ spanId: opts.spanId ?? generateSpanId(),
45234
+ parentSpanId: opts.parentSpanId,
45235
+ name: "error",
45236
+ kind: opts.spanKind ?? "internal",
45237
+ startTime: now,
45238
+ endTime: now,
45239
+ attributes: {
45240
+ "ai.agent.error.type": opts.errorType,
45241
+ "ai.agent.error.message": opts.errorMessage
45242
+ },
45243
+ status: { code: 2, message: opts.errorMessage }
45244
+ };
45245
+ }
45246
+
45247
+ // ../crypto/dist/telemetry-reporter.js
45248
+ function toOtlpAttributes(attrs) {
45249
+ return Object.entries(attrs).map(([key, val]) => {
45250
+ if (typeof val === "string") {
45251
+ return { key, value: { stringValue: val } };
45252
+ }
45253
+ if (typeof val === "boolean") {
45254
+ return { key, value: { boolValue: val } };
45255
+ }
45256
+ if (Number.isInteger(val)) {
45257
+ return { key, value: { intValue: val } };
45258
+ }
45259
+ return { key, value: { doubleValue: val } };
45260
+ });
45261
+ }
45262
+ function spanToOtlp(span) {
45263
+ const otlp = {
45264
+ traceId: span.traceId,
45265
+ spanId: span.spanId,
45266
+ name: span.name,
45267
+ kind: span.kind,
45268
+ startTimeUnixNano: String(span.startTime * 1e6),
45269
+ endTimeUnixNano: String(span.endTime * 1e6),
45270
+ attributes: toOtlpAttributes(span.attributes)
45271
+ };
45272
+ if (span.parentSpanId !== void 0) {
45273
+ otlp.parentSpanId = span.parentSpanId;
45274
+ }
45275
+ if (span.status) {
45276
+ otlp.status = span.status;
45277
+ }
45278
+ if (span.events && span.events.length > 0) {
45279
+ otlp.events = span.events;
45280
+ }
45281
+ return otlp;
45282
+ }
45283
+ var TelemetryReporter = class {
45284
+ _apiBase;
45285
+ _hubId;
45286
+ _authHeader;
45287
+ _fetch;
45288
+ _buffer = [];
45289
+ _timer = null;
45290
+ constructor(config) {
45291
+ this._apiBase = config.apiBase.replace(/\/+$/, "");
45292
+ this._hubId = config.hubId;
45293
+ this._authHeader = config.authHeader;
45294
+ this._fetch = config.fetchImpl ?? globalThis.fetch;
45295
+ }
45296
+ /** Number of spans waiting to be flushed. */
45297
+ get pendingCount() {
45298
+ return this._buffer.length;
45299
+ }
45300
+ // -- Report methods ---------------------------------------------------------
45301
+ /** Record an LLM inference call. */
45302
+ reportLlmCall(opts) {
45303
+ this._buffer.push(buildLlmSpan(opts));
45304
+ }
45305
+ /** Record a tool/function invocation. */
45306
+ reportToolCall(opts) {
45307
+ this._buffer.push(buildToolSpan(opts));
45308
+ }
45309
+ /** Record an error event. */
45310
+ reportError(opts) {
45311
+ this._buffer.push(buildErrorSpan(opts));
45312
+ }
45313
+ /** Record an arbitrary pre-built span. */
45314
+ reportCustomSpan(span) {
45315
+ this._buffer.push(span);
45316
+ }
45317
+ // -- Flush ------------------------------------------------------------------
45318
+ /**
45319
+ * POST all buffered spans to the backend ingest endpoint.
45320
+ *
45321
+ * - On success (HTTP 2xx): clears the buffer.
45322
+ * - On failure: keeps spans in the buffer for retry.
45323
+ * - Never throws — telemetry is best-effort.
45324
+ */
45325
+ async flush() {
45326
+ if (this._buffer.length === 0) {
45327
+ return;
45328
+ }
45329
+ const spans = this._buffer;
45330
+ this._buffer = [];
45331
+ try {
45332
+ const response = await this._fetch(`${this._apiBase}/api/v1/telemetry/ingest`, {
45333
+ method: "POST",
45334
+ headers: {
45335
+ "Content-Type": "application/json",
45336
+ Authorization: this._authHeader
45337
+ },
45338
+ body: JSON.stringify({
45339
+ hub_id: this._hubId,
45340
+ spans: spans.map(spanToOtlp)
45341
+ })
45342
+ });
45343
+ if (!response.ok) {
45344
+ this._buffer = spans.concat(this._buffer);
45345
+ }
45346
+ } catch {
45347
+ this._buffer = spans.concat(this._buffer);
45348
+ }
45349
+ }
45350
+ // -- Auto-flush -------------------------------------------------------------
45351
+ /**
45352
+ * Start a periodic flush timer.
45353
+ * @param intervalMs Flush interval in milliseconds (default 30 000).
45354
+ */
45355
+ startAutoFlush(intervalMs = 3e4) {
45356
+ this.stopAutoFlush();
45357
+ this._timer = setInterval(() => {
45358
+ void this.flush();
45359
+ }, intervalMs);
45360
+ }
45361
+ /** Stop the periodic flush timer. Safe to call when not started. */
45362
+ stopAutoFlush() {
45363
+ if (this._timer !== null) {
45364
+ clearInterval(this._timer);
45365
+ this._timer = null;
45366
+ }
45367
+ }
45368
+ };
45369
+
45152
45370
  // src/state.ts
45153
45371
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
45154
45372
  import { join } from "node:path";
@@ -45292,12 +45510,13 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45292
45510
  _lastWakeTick = Date.now();
45293
45511
  _pendingPollTimer = null;
45294
45512
  _syncMessageIds = null;
45295
- /** Recently handled message IDs via WS — survives reconnects so sync skips them. Max 500. */
45296
- _recentlyHandledIds = /* @__PURE__ */ new Set();
45297
45513
  /** Queued A2A messages for responder channels not yet activated (no first initiator message received). */
45298
45514
  _a2aPendingQueue = {};
45299
45515
  _scanEngine = null;
45300
45516
  _scanRuleSetVersion = 0;
45517
+ _telemetryReporter = null;
45518
+ /** Topic ID from the most recent inbound message — used as fallback for replies. */
45519
+ _lastIncomingTopicId;
45301
45520
  // Liveness detection: server sends app-level {"event":"ping"} every 30s.
45302
45521
  // We check every 30s; if no data received in 90s (3 missed pings), connection is dead.
45303
45522
  static PING_INTERVAL_MS = 3e4;
@@ -45326,6 +45545,10 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45326
45545
  get sessionCount() {
45327
45546
  return this._sessions.size;
45328
45547
  }
45548
+ /** Returns the TelemetryReporter instance (available after WebSocket connect). */
45549
+ get telemetry() {
45550
+ return this._telemetryReporter;
45551
+ }
45329
45552
  async start() {
45330
45553
  this._stopped = false;
45331
45554
  await libsodium_wrappers_default.ready;
@@ -45408,7 +45631,7 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45408
45631
  if (this._sessions.size === 0) {
45409
45632
  throw new Error("No active sessions");
45410
45633
  }
45411
- const topicId = options?.topicId ?? this._persisted?.defaultTopicId;
45634
+ const topicId = options?.topicId ?? this._lastIncomingTopicId ?? this._persisted?.defaultTopicId;
45412
45635
  const messageType = options?.messageType ?? "text";
45413
45636
  const priority = options?.priority ?? "normal";
45414
45637
  const parentSpanId = options?.parentSpanId;
@@ -45870,6 +46093,11 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45870
46093
  clearTimeout(this._reconnectTimer);
45871
46094
  this._reconnectTimer = null;
45872
46095
  }
46096
+ if (this._telemetryReporter) {
46097
+ this._telemetryReporter.stopAutoFlush();
46098
+ await this._telemetryReporter.flush();
46099
+ this._telemetryReporter = null;
46100
+ }
45873
46101
  if (this._ws) {
45874
46102
  this._ws.removeAllListeners();
45875
46103
  this._ws.close();
@@ -46393,26 +46621,20 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
46393
46621
  this._scanEngine = new ScanEngine();
46394
46622
  await this._fetchScanRules();
46395
46623
  }
46624
+ if (!this._telemetryReporter && this._persisted?.deviceJwt && this._persisted?.hubAddress) {
46625
+ this._telemetryReporter = new TelemetryReporter({
46626
+ apiBase: this.config.apiUrl,
46627
+ hubId: this._persisted.hubAddress,
46628
+ authHeader: `Bearer ${this._persisted.deviceJwt}`
46629
+ });
46630
+ this._telemetryReporter.startAutoFlush(3e4);
46631
+ }
46396
46632
  this.emit("ready");
46397
46633
  } catch (openErr) {
46398
46634
  console.error("[SecureChannel] Error in WS open handler:", openErr);
46399
46635
  this.emit("error", openErr);
46400
46636
  }
46401
46637
  });
46402
- const _onUnhandledRejection = (reason) => {
46403
- console.error("[SecureChannel] UNHANDLED REJECTION (would crash process):", reason);
46404
- };
46405
- const _onUncaughtException = (err) => {
46406
- console.error("[SecureChannel] UNCAUGHT EXCEPTION (would crash process):", err);
46407
- };
46408
- process.on("unhandledRejection", _onUnhandledRejection);
46409
- process.on("uncaughtException", _onUncaughtException);
46410
- ws.on("close", (code, reason) => {
46411
- const reasonStr = reason?.toString() || "";
46412
- console.log(`[SecureChannel] WS CLOSED: code=${code} reason=${JSON.stringify(reasonStr)}`);
46413
- process.removeListener("unhandledRejection", _onUnhandledRejection);
46414
- process.removeListener("uncaughtException", _onUncaughtException);
46415
- });
46416
46638
  ws.on("message", async (raw) => {
46417
46639
  this._lastServerMessage = Date.now();
46418
46640
  this._lastWakeTick = Date.now();
@@ -46432,10 +46654,8 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
46432
46654
  return;
46433
46655
  }
46434
46656
  if (data.event === "message") {
46435
- console.log(`[SecureChannel] \u2190 Direct message received: msg=${data.data?.message_id?.slice(0, 8) ?? "?"} conv=${data.data?.conversation_id?.slice(0, 8) ?? "?"}`);
46436
46657
  try {
46437
46658
  await this._handleIncomingMessage(data.data);
46438
- console.log(`[SecureChannel] \u2190 Direct message processed OK: msg=${data.data?.message_id?.slice(0, 8) ?? "?"}`);
46439
46659
  } catch (msgErr) {
46440
46660
  console.error(
46441
46661
  `[SecureChannel] Message handler failed for conv ${data.data?.conversation_id?.slice(0, 8) ?? "?"}...:`,
@@ -46767,9 +46987,7 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
46767
46987
  this.emit("error", err);
46768
46988
  }
46769
46989
  });
46770
- ws.on("close", (code, reason) => {
46771
- const reasonStr = reason?.toString() || "";
46772
- console.log(`[SecureChannel] WS close handler: code=${code} reason=${JSON.stringify(reasonStr)}`);
46990
+ ws.on("close", () => {
46773
46991
  this._stopPing();
46774
46992
  this._stopWakeDetector();
46775
46993
  this._stopPendingPoll();
@@ -46791,11 +47009,6 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
46791
47009
  if (this._syncMessageIds?.has(msgData.message_id)) {
46792
47010
  return;
46793
47011
  }
46794
- this._recentlyHandledIds.add(msgData.message_id);
46795
- if (this._recentlyHandledIds.size > 500) {
46796
- const all = [...this._recentlyHandledIds];
46797
- this._recentlyHandledIds = new Set(all.slice(all.length - 400));
46798
- }
46799
47012
  const convId = msgData.conversation_id;
46800
47013
  const session = this._sessions.get(convId);
46801
47014
  if (!session) {
@@ -46808,14 +47021,7 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
46808
47021
  header_blob: msgData.header_blob,
46809
47022
  ciphertext: msgData.ciphertext
46810
47023
  });
46811
- let plaintext;
46812
- try {
46813
- plaintext = session.ratchet.decrypt(encrypted);
46814
- } catch (decryptErr) {
46815
- console.error(`[SecureChannel] Direct message decrypt FAILED for conv ${convId.slice(0, 8)}...: ${String(decryptErr)}`);
46816
- throw decryptErr;
46817
- }
46818
- console.log(`[SecureChannel] Direct message decrypted OK for conv ${convId.slice(0, 8)}...`);
47024
+ const plaintext = session.ratchet.decrypt(encrypted);
46819
47025
  this._sendAck(msgData.message_id);
46820
47026
  if (!session.activated) {
46821
47027
  session.activated = true;
@@ -46850,6 +47056,7 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
46850
47056
  }
46851
47057
  if (messageType === "message") {
46852
47058
  const topicId = msgData.topic_id;
47059
+ this._lastIncomingTopicId = topicId;
46853
47060
  let attachData;
46854
47061
  if (attachmentInfo) {
46855
47062
  try {
@@ -46908,13 +47115,7 @@ ${messageText}`;
46908
47115
  Promise.resolve(this.config.onMessage?.(emitText, metadata)).catch((err) => {
46909
47116
  console.error("[SecureChannel] onMessage callback error:", err);
46910
47117
  });
46911
- console.log(`[SecureChannel] Relaying sync to ${this._sessions.size - 1} siblings for conv ${convId.slice(0, 8)}...`);
46912
- try {
46913
- await this._relaySyncToSiblings(convId, session.ownerDeviceId, messageText, topicId);
46914
- console.log(`[SecureChannel] Sync relay complete for conv ${convId.slice(0, 8)}...`);
46915
- } catch (relayErr) {
46916
- console.error(`[SecureChannel] Sync relay FAILED: ${String(relayErr)}`);
46917
- }
47118
+ await this._relaySyncToSiblings(convId, session.ownerDeviceId, messageText, topicId);
46918
47119
  }
46919
47120
  if (this._persisted) {
46920
47121
  this._persisted.lastMessageTimestamp = msgData.created_at;
@@ -47039,29 +47240,22 @@ ${messageText}`;
47039
47240
  ts: (/* @__PURE__ */ new Date()).toISOString(),
47040
47241
  topicId
47041
47242
  });
47042
- let relayed = 0;
47043
47243
  for (const [siblingConvId, siblingSession] of this._sessions) {
47044
47244
  if (siblingConvId === sourceConvId) continue;
47045
47245
  if (!siblingSession.activated) continue;
47046
- try {
47047
- const syncEncrypted = siblingSession.ratchet.encrypt(syncPayload);
47048
- const syncTransport = encryptedMessageToTransport(syncEncrypted);
47049
- this._ws.send(
47050
- JSON.stringify({
47051
- event: "message",
47052
- data: {
47053
- conversation_id: siblingConvId,
47054
- header_blob: syncTransport.header_blob,
47055
- ciphertext: syncTransport.ciphertext
47056
- }
47057
- })
47058
- );
47059
- relayed++;
47060
- } catch (err) {
47061
- console.error(`[SecureChannel] Sync send failed for sibling ${siblingConvId.slice(0, 8)}...: ${String(err)}`);
47062
- }
47246
+ const syncEncrypted = siblingSession.ratchet.encrypt(syncPayload);
47247
+ const syncTransport = encryptedMessageToTransport(syncEncrypted);
47248
+ this._ws.send(
47249
+ JSON.stringify({
47250
+ event: "message",
47251
+ data: {
47252
+ conversation_id: siblingConvId,
47253
+ header_blob: syncTransport.header_blob,
47254
+ ciphertext: syncTransport.ciphertext
47255
+ }
47256
+ })
47257
+ );
47063
47258
  }
47064
- console.log(`[SecureChannel] _relaySyncToSiblings: relayed to ${relayed}/${this._sessions.size - 1} siblings`);
47065
47259
  }
47066
47260
  /**
47067
47261
  * Send stored message history to a newly-activated session.
@@ -47163,12 +47357,52 @@ ${messageText}`;
47163
47357
  );
47164
47358
  return;
47165
47359
  }
47166
- const session = this._sessions.get(convId);
47360
+ let session = this._sessions.get(convId);
47167
47361
  if (!session) {
47168
47362
  console.warn(
47169
- `[SecureChannel] No session for room conv ${convId.slice(0, 8)}..., skipping`
47363
+ `[SecureChannel] No session for room conv ${convId.slice(0, 8)}..., fetching room data`
47170
47364
  );
47171
- return;
47365
+ try {
47366
+ const roomRes = await fetch(
47367
+ `${this.config.apiUrl}/api/v1/rooms/${msgData.room_id}`,
47368
+ {
47369
+ headers: {
47370
+ Authorization: `Bearer ${this._persisted.deviceJwt}`
47371
+ }
47372
+ }
47373
+ );
47374
+ if (roomRes.ok) {
47375
+ const roomData = await roomRes.json();
47376
+ await this.joinRoom({
47377
+ roomId: roomData.id,
47378
+ name: roomData.name,
47379
+ members: (roomData.members || []).map((m2) => ({
47380
+ deviceId: m2.device_id,
47381
+ entityType: m2.entity_type,
47382
+ displayName: m2.display_name,
47383
+ identityPublicKey: m2.identity_public_key,
47384
+ ephemeralPublicKey: m2.ephemeral_public_key
47385
+ })),
47386
+ conversations: (roomData.conversations || []).map((c2) => ({
47387
+ id: c2.id,
47388
+ participantA: c2.participant_a,
47389
+ participantB: c2.participant_b
47390
+ }))
47391
+ });
47392
+ session = this._sessions.get(convId);
47393
+ }
47394
+ } catch (fetchErr) {
47395
+ console.error(
47396
+ `[SecureChannel] Failed to fetch room data for ${msgData.room_id}:`,
47397
+ fetchErr
47398
+ );
47399
+ }
47400
+ if (!session) {
47401
+ console.warn(
47402
+ `[SecureChannel] Still no session for room conv ${convId.slice(0, 8)}... after refresh, skipping`
47403
+ );
47404
+ return;
47405
+ }
47172
47406
  }
47173
47407
  const encrypted = transportToEncryptedMessage({
47174
47408
  header_blob: msgData.header_blob,
@@ -47298,7 +47532,6 @@ ${messageText}`;
47298
47532
  const PAGE_SIZE = 200;
47299
47533
  let since = this._persisted.lastMessageTimestamp;
47300
47534
  let totalProcessed = 0;
47301
- let totalSkipped = 0;
47302
47535
  try {
47303
47536
  for (let page = 0; page < MAX_PAGES; page++) {
47304
47537
  const url = `${this.config.apiUrl}/api/v1/devices/${this._deviceId}/messages?since=${encodeURIComponent(since)}&limit=${PAGE_SIZE}`;
@@ -47308,24 +47541,15 @@ ${messageText}`;
47308
47541
  if (!res.ok) break;
47309
47542
  const messages = await res.json();
47310
47543
  if (messages.length === 0) break;
47311
- console.log(`[SecureChannel] Sync page ${page}: ${messages.length} messages since ${since}`);
47312
47544
  for (const msg of messages) {
47313
47545
  if (msg.sender_device_id === this._deviceId) continue;
47314
47546
  if (this._syncMessageIds.has(msg.id)) continue;
47315
47547
  this._syncMessageIds.add(msg.id);
47316
- if (this._recentlyHandledIds.has(msg.id)) {
47317
- this._persisted.lastMessageTimestamp = msg.created_at;
47318
- since = msg.created_at;
47319
- totalSkipped++;
47320
- continue;
47321
- }
47322
47548
  const session = this._sessions.get(msg.conversation_id);
47323
47549
  if (!session) {
47324
47550
  console.warn(
47325
47551
  `[SecureChannel] No session for conversation ${msg.conversation_id} during sync, skipping`
47326
47552
  );
47327
- this._persisted.lastMessageTimestamp = msg.created_at;
47328
- since = msg.created_at;
47329
47553
  continue;
47330
47554
  }
47331
47555
  try {
@@ -47359,6 +47583,9 @@ ${messageText}`;
47359
47583
  topicId
47360
47584
  };
47361
47585
  this.emit("message", messageText, metadata);
47586
+ Promise.resolve(this.config.onMessage?.(messageText, metadata)).catch((err) => {
47587
+ console.error("[SecureChannel] onMessage callback error:", err);
47588
+ });
47362
47589
  }
47363
47590
  this._persisted.lastMessageTimestamp = msg.created_at;
47364
47591
  since = msg.created_at;
@@ -47375,15 +47602,10 @@ ${messageText}`;
47375
47602
  await this._persistState();
47376
47603
  if (messages.length < PAGE_SIZE) break;
47377
47604
  }
47378
- if (totalProcessed > 0 || totalSkipped > 0) {
47379
- console.log(`[SecureChannel] Sync complete: ${totalProcessed} processed, ${totalSkipped} skipped (already handled via WS)`);
47380
- }
47381
- } catch (outerErr) {
47382
- console.warn(`[SecureChannel] Sync interrupted: ${String(outerErr)}`);
47383
- try {
47384
- await this._persistState();
47385
- } catch {
47605
+ if (totalProcessed > 0) {
47606
+ console.log(`[SecureChannel] Synced ${totalProcessed} missed messages`);
47386
47607
  }
47608
+ } catch {
47387
47609
  }
47388
47610
  this._syncMessageIds = null;
47389
47611
  }