@agentvault/agentvault 0.23.1 → 0.23.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/index.js CHANGED
@@ -64267,145 +64267,140 @@ var init_channel = __esm({
64267
64267
  }
64268
64268
  const raw = await loadState(this.config.dataDir);
64269
64269
  if (raw) {
64270
- await backupState(this.config.dataDir);
64271
- this._persisted = migratePersistedState(raw);
64272
- if (!this._persisted.messageHistory) {
64273
- this._persisted.messageHistory = [];
64274
- }
64275
- this._deviceId = this._persisted.deviceId;
64276
- this._deviceJwt = this._persisted.deviceJwt;
64277
- this._primaryConversationId = this._persisted.primaryConversationId;
64278
- this._fingerprint = this._persisted.fingerprint;
64279
- this._lastInboundRoomId = this._persisted.lastInboundRoomId;
64280
- if (this._persisted.seenMessageIds) {
64281
- this._seenMessageIds = new Set(this._persisted.seenMessageIds.slice(-_SecureChannel.SEEN_MSG_MAX));
64282
- }
64283
- if (this._persisted.seenA2AMessageIds) {
64284
- this._a2aSeenMessageIds = new Set(this._persisted.seenA2AMessageIds.slice(-_SecureChannel.A2A_SEEN_MAX));
64285
- }
64286
- if (this._persisted?.rooms) {
64287
- const roomConvIds = /* @__PURE__ */ new Set();
64288
- for (const room of Object.values(this._persisted.rooms)) {
64289
- for (const cid of room.conversationIds || []) {
64290
- roomConvIds.add(cid);
64291
- }
64292
- }
64293
- let cleanedCount = 0;
64294
- for (const cid of roomConvIds) {
64295
- if (this._persisted.sessions[cid]) {
64296
- delete this._persisted.sessions[cid];
64297
- cleanedCount++;
64298
- }
64299
- }
64300
- if (cleanedCount > 0) {
64301
- console.log(`[SecureChannel] Cleaned ${cleanedCount} stale DR room sessions`);
64270
+ await this._hydrateFromPersisted(raw);
64271
+ this._connect();
64272
+ return;
64273
+ }
64274
+ const restored = await restoreState(this.config.dataDir);
64275
+ if (restored) {
64276
+ console.log("[SecureChannel] Restored state from backup");
64277
+ const restoredRaw = await loadState(this.config.dataDir);
64278
+ if (restoredRaw) {
64279
+ await this._hydrateFromPersisted(restoredRaw);
64280
+ this._connect();
64281
+ return;
64282
+ }
64283
+ }
64284
+ await this._enroll();
64285
+ }
64286
+ /**
64287
+ * Hydrate all in-memory state from persisted creds. The single source of
64288
+ * truth for both the primary boot and the .bak-restore boot, so the two can
64289
+ * never drift (the drift is what caused #416: the backup branch used to skip
64290
+ * MLS group loading and dropped owner MLS DMs). Callers invoke _connect()
64291
+ * after this resolves.
64292
+ */
64293
+ async _hydrateFromPersisted(raw) {
64294
+ await backupState(this.config.dataDir);
64295
+ this._persisted = migratePersistedState(raw);
64296
+ if (!this._persisted.messageHistory) {
64297
+ this._persisted.messageHistory = [];
64298
+ }
64299
+ this._deviceId = this._persisted.deviceId;
64300
+ this._deviceJwt = this._persisted.deviceJwt;
64301
+ this._primaryConversationId = this._persisted.primaryConversationId;
64302
+ this._fingerprint = this._persisted.fingerprint;
64303
+ this._lastInboundRoomId = this._persisted.lastInboundRoomId;
64304
+ if (this._persisted.seenMessageIds) {
64305
+ this._seenMessageIds = new Set(this._persisted.seenMessageIds.slice(-_SecureChannel.SEEN_MSG_MAX));
64306
+ }
64307
+ if (this._persisted.seenA2AMessageIds) {
64308
+ this._a2aSeenMessageIds = new Set(this._persisted.seenA2AMessageIds.slice(-_SecureChannel.A2A_SEEN_MAX));
64309
+ }
64310
+ if (this._persisted?.rooms) {
64311
+ const roomConvIds = /* @__PURE__ */ new Set();
64312
+ for (const room of Object.values(this._persisted.rooms)) {
64313
+ for (const cid of room.conversationIds || []) {
64314
+ roomConvIds.add(cid);
64302
64315
  }
64303
64316
  }
64304
- for (const [convId, sessionData] of Object.entries(
64305
- this._persisted.sessions
64306
- )) {
64307
- if (sessionData.ratchetState) {
64308
- const ratchet = DoubleRatchet.deserialize(sessionData.ratchetState);
64309
- this._sessions.set(convId, {
64310
- ownerDeviceId: sessionData.ownerDeviceId,
64311
- ratchet,
64312
- activated: sessionData.activated ?? false,
64313
- epoch: sessionData.epoch
64314
- });
64317
+ let cleanedCount = 0;
64318
+ for (const cid of roomConvIds) {
64319
+ if (this._persisted.sessions[cid]) {
64320
+ delete this._persisted.sessions[cid];
64321
+ cleanedCount++;
64315
64322
  }
64316
64323
  }
64317
- if (this._persisted.conversationGroupIds) {
64318
- this._sessionGroupIds = new Map(Object.entries(this._persisted.conversationGroupIds));
64319
- console.log(`[SecureChannel] Restored ${this._sessionGroupIds.size} conversation\u2192group mappings`);
64324
+ if (cleanedCount > 0) {
64325
+ console.log(`[SecureChannel] Cleaned ${cleanedCount} stale DR room sessions`);
64320
64326
  }
64321
- if (this._persisted.rooms) {
64322
- for (const [roomId, room] of Object.entries(this._persisted.rooms)) {
64323
- const mlsGroupId = room.mlsGroupId;
64324
- if (mlsGroupId) {
64325
- try {
64326
- const mlsState = await loadMlsState(this.config.dataDir, mlsGroupId);
64327
- if (mlsState) {
64328
- const mgr = new MLSGroupManager();
64329
- mgr.importState(JSON.parse(mlsState));
64330
- this._mlsGroups.set(roomId, mgr);
64331
- }
64332
- } catch (mlsErr) {
64333
- console.warn(`[SecureChannel] Failed to restore MLS state for room ${roomId.slice(0, 8)}:`, mlsErr);
64334
- }
64335
- }
64336
- }
64327
+ }
64328
+ for (const [convId, sessionData] of Object.entries(
64329
+ this._persisted.sessions
64330
+ )) {
64331
+ if (sessionData.ratchetState) {
64332
+ const ratchet = DoubleRatchet.deserialize(sessionData.ratchetState);
64333
+ this._sessions.set(convId, {
64334
+ ownerDeviceId: sessionData.ownerDeviceId,
64335
+ ratchet,
64336
+ activated: sessionData.activated ?? false,
64337
+ epoch: sessionData.epoch
64338
+ });
64337
64339
  }
64338
- for (const [convId, entry] of Object.entries(this._persisted.mlsConversations ?? {})) {
64339
- if (entry.mlsGroupId) {
64340
+ }
64341
+ if (this._persisted.conversationGroupIds) {
64342
+ this._sessionGroupIds = new Map(Object.entries(this._persisted.conversationGroupIds));
64343
+ console.log(`[SecureChannel] Restored ${this._sessionGroupIds.size} conversation\u2192group mappings`);
64344
+ }
64345
+ if (this._persisted.rooms) {
64346
+ for (const [roomId, room] of Object.entries(this._persisted.rooms)) {
64347
+ const mlsGroupId = room.mlsGroupId;
64348
+ if (mlsGroupId) {
64340
64349
  try {
64341
- const mlsState = await loadMlsState(this.config.dataDir, entry.mlsGroupId);
64350
+ const mlsState = await loadMlsState(this.config.dataDir, mlsGroupId);
64342
64351
  if (mlsState) {
64343
64352
  const mgr = new MLSGroupManager();
64344
64353
  mgr.importState(JSON.parse(mlsState));
64345
- this._mlsGroups.set(`conv:${convId}`, mgr);
64354
+ this._mlsGroups.set(roomId, mgr);
64346
64355
  }
64347
64356
  } catch (mlsErr) {
64348
- console.warn(`[SecureChannel] Failed to restore MLS state for conv ${convId.slice(0, 8)}:`, mlsErr);
64357
+ console.warn(`[SecureChannel] Failed to restore MLS state for room ${roomId.slice(0, 8)}:`, mlsErr);
64349
64358
  }
64350
64359
  }
64351
64360
  }
64352
- for (const [gid, entry] of Object.entries(this._persisted.mlsGroups ?? {})) {
64353
- if (!entry.mlsGroupId) continue;
64361
+ }
64362
+ for (const [convId, entry] of Object.entries(this._persisted.mlsConversations ?? {})) {
64363
+ if (entry.mlsGroupId) {
64354
64364
  try {
64355
- const stateJson = await loadMlsState(this.config.dataDir, entry.mlsGroupId);
64356
- if (stateJson) {
64365
+ const mlsState = await loadMlsState(this.config.dataDir, entry.mlsGroupId);
64366
+ if (mlsState) {
64357
64367
  const mgr = new MLSGroupManager();
64358
- mgr.importState(JSON.parse(stateJson));
64359
- this._mlsGroups.set(`1to1-group:${gid}`, mgr);
64360
- console.log(`[SecureChannel] Loaded shared MLS group for ${gid.slice(0, 8)} (epoch=${mgr.epoch})`);
64368
+ mgr.importState(JSON.parse(mlsState));
64369
+ this._mlsGroups.set(`conv:${convId}`, mgr);
64361
64370
  }
64362
- } catch (loadErr) {
64363
- console.warn(`[SecureChannel] Failed to load shared MLS group ${gid.slice(0, 8)}:`, loadErr);
64371
+ } catch (mlsErr) {
64372
+ console.warn(`[SecureChannel] Failed to restore MLS state for conv ${convId.slice(0, 8)}:`, mlsErr);
64364
64373
  }
64365
64374
  }
64366
- for (const [channelId, entry] of Object.entries(this._persisted.a2aChannels ?? {})) {
64367
- if (entry.mlsGroupId) {
64368
- try {
64369
- const mlsState = await loadMlsState(this.config.dataDir, entry.mlsGroupId);
64370
- if (mlsState) {
64371
- const mgr = new MLSGroupManager();
64372
- mgr.importState(JSON.parse(mlsState));
64373
- this._mlsGroups.set(`a2a:${channelId}`, mgr);
64374
- }
64375
- } catch (mlsErr) {
64376
- console.warn(`[SecureChannel] Failed to restore MLS state for A2A ${channelId.slice(0, 8)}:`, mlsErr);
64377
- }
64375
+ }
64376
+ for (const [gid, entry] of Object.entries(this._persisted.mlsGroups ?? {})) {
64377
+ if (!entry.mlsGroupId) continue;
64378
+ try {
64379
+ const stateJson = await loadMlsState(this.config.dataDir, entry.mlsGroupId);
64380
+ if (stateJson) {
64381
+ const mgr = new MLSGroupManager();
64382
+ mgr.importState(JSON.parse(stateJson));
64383
+ this._mlsGroups.set(`1to1-group:${gid}`, mgr);
64384
+ console.log(`[SecureChannel] Loaded shared MLS group for ${gid.slice(0, 8)} (epoch=${mgr.epoch})`);
64378
64385
  }
64386
+ } catch (loadErr) {
64387
+ console.warn(`[SecureChannel] Failed to load shared MLS group ${gid.slice(0, 8)}:`, loadErr);
64379
64388
  }
64380
- this._connect();
64381
- return;
64382
64389
  }
64383
- const restored = await restoreState(this.config.dataDir);
64384
- if (restored) {
64385
- console.log("[SecureChannel] Restored state from backup");
64386
- const restoredRaw = await loadState(this.config.dataDir);
64387
- if (restoredRaw) {
64388
- this._persisted = migratePersistedState(restoredRaw);
64389
- if (!this._persisted.messageHistory) this._persisted.messageHistory = [];
64390
- this._deviceId = this._persisted.deviceId;
64391
- this._deviceJwt = this._persisted.deviceJwt;
64392
- this._primaryConversationId = this._persisted.primaryConversationId;
64393
- this._fingerprint = this._persisted.fingerprint;
64394
- for (const [convId, sd] of Object.entries(this._persisted.sessions)) {
64395
- if (sd.ratchetState) {
64396
- this._sessions.set(convId, {
64397
- ownerDeviceId: sd.ownerDeviceId,
64398
- ratchet: DoubleRatchet.deserialize(sd.ratchetState),
64399
- activated: sd.activated ?? false,
64400
- epoch: sd.epoch
64401
- });
64390
+ for (const [channelId, entry] of Object.entries(this._persisted.a2aChannels ?? {})) {
64391
+ if (entry.mlsGroupId) {
64392
+ try {
64393
+ const mlsState = await loadMlsState(this.config.dataDir, entry.mlsGroupId);
64394
+ if (mlsState) {
64395
+ const mgr = new MLSGroupManager();
64396
+ mgr.importState(JSON.parse(mlsState));
64397
+ this._mlsGroups.set(`a2a:${channelId}`, mgr);
64402
64398
  }
64399
+ } catch (mlsErr) {
64400
+ console.warn(`[SecureChannel] Failed to restore MLS state for A2A ${channelId.slice(0, 8)}:`, mlsErr);
64403
64401
  }
64404
- this._connect();
64405
- return;
64406
64402
  }
64407
64403
  }
64408
- await this._enroll();
64409
64404
  }
64410
64405
  /**
64411
64406
  * Fetch scan rules from the server and load them into the ScanEngine.
@@ -64745,7 +64740,7 @@ var init_channel = __esm({
64745
64740
  */
64746
64741
  sendActivitySpan(spanData) {
64747
64742
  if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
64748
- const pluginVersion = true ? "0.23.1" : "0.0.0-dev";
64743
+ const pluginVersion = true ? "0.23.3" : "0.0.0-dev";
64749
64744
  const agentName = this.config.agentName ?? "Agent";
64750
64745
  const resource = {
64751
64746
  "service.name": "agentvault-agent",
@@ -66362,7 +66357,7 @@ var init_channel = __esm({
66362
66357
  agentVersion: this.config.agentVersion ?? "0.0.0",
66363
66358
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
66364
66359
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
66365
- pluginVersion: true ? "0.23.1" : "0.0.0-dev"
66360
+ pluginVersion: true ? "0.23.3" : "0.0.0-dev"
66366
66361
  });
66367
66362
  this._telemetryReporter.startAutoFlush(3e4);
66368
66363
  }
@@ -66438,7 +66433,7 @@ var init_channel = __esm({
66438
66433
  return;
66439
66434
  }
66440
66435
  if (data.event === "device_revoked") {
66441
- this._handleDeviceRevoked();
66436
+ await this._handleDeviceRevoked();
66442
66437
  return;
66443
66438
  }
66444
66439
  if (data.event === "device_linked") {
@@ -66672,7 +66667,7 @@ var init_channel = __esm({
66672
66667
  agentVersion: this.config.agentVersion ?? "0.0.0",
66673
66668
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
66674
66669
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
66675
- pluginVersion: true ? "0.23.1" : "0.0.0-dev"
66670
+ pluginVersion: true ? "0.23.3" : "0.0.0-dev"
66676
66671
  });
66677
66672
  this._telemetryReporter.startAutoFlush(3e4);
66678
66673
  }
@@ -69837,22 +69832,74 @@ ${messageText}`;
69837
69832
  this._setState("error");
69838
69833
  this.emit("error", err);
69839
69834
  }
69835
+ // Phase 2 — rolling-window timestamps of device_revoked self-heal reconnects,
69836
+ // used as a war cap so a takeover war or a server status/WS inconsistency can't
69837
+ // thrash forever.
69838
+ _revokeRecoveries = [];
69839
+ static REVOKE_RECOVERY_MAX = 3;
69840
+ static REVOKE_RECOVERY_WINDOW_MS = 12e4;
69840
69841
  /**
69841
- * Handle a WS `device_revoked` event. Surfaces a terminal error so
69842
- * attachLifecycle stops the channel — but does NOT delete credentials.
69842
+ * Handle a WS `device_revoked` event.
69843
69843
  *
69844
- * This signal fires both for a genuine owner revoke AND for a transient
69845
- * session-takeover of a device that is still ACTIVE on the server (the error
69846
- * text even reads "or another session has taken over"). Deleting
69847
- * agentvault.json here as this path used to via clearState() strands a
69848
- * still-valid identity: the primary creds vanish, only the .bak survives, and
69849
- * the bridge's token-gate then demands a fresh invite token to restart.
69850
- * Keeping the creds means a restart reconnects via restoreState() when the
69851
- * device is still valid, and fails cleanly (device_revoked again, no data
69852
- * lost) when it isn't. Re-enrollment, which SHOULD wipe creds, goes through
69853
- * the explicit setup --force / _forceReEnroll path in start().
69844
+ * The signal is ambiguous: the server sends it both for a genuine owner revoke
69845
+ * AND for a transient/racy session-takeover of a device that is still ACTIVE
69846
+ * (the error text even reads "or another session has taken over" — loopita's
69847
+ * case, where the device stayed ACTIVE on the server). So we do NOT terminate
69848
+ * blindly. We ask the PUBLIC /devices/{id}/status endpoint (no JWT needed, so
69849
+ * it answers even when our device JWT is being rejected):
69850
+ * - ACTIVE -> reconnect in place (transient); stay online.
69851
+ * - non-ACTIVE -> terminal (genuinely revoked; operator must re-enroll).
69852
+ * - unreachable/429 -> reconnect (inconclusive; a network blip shouldn't
69853
+ * permanently kill a healthy agent the WS is down too,
69854
+ * so normal backoff handles it, and the war cap bounds
69855
+ * thrash if the device really is dead).
69856
+ *
69857
+ * A rolling-window cap (REVOKE_RECOVERY_MAX self-heals per WINDOW_MS) gives up
69858
+ * (terminal) on a repeating loop. Never clears credentials in any branch —
69859
+ * re-enrollment, which SHOULD wipe creds, goes through setup --force /
69860
+ * _forceReEnroll in start().
69854
69861
  */
69855
- _handleDeviceRevoked() {
69862
+ async _handleDeviceRevoked() {
69863
+ const now = Date.now();
69864
+ this._revokeRecoveries = this._revokeRecoveries.filter(
69865
+ (t2) => now - t2 < _SecureChannel.REVOKE_RECOVERY_WINDOW_MS
69866
+ );
69867
+ if (this._revokeRecoveries.length >= _SecureChannel.REVOKE_RECOVERY_MAX) {
69868
+ console.warn(
69869
+ "[SecureChannel] device_revoked self-healed too many times in a short window \u2014 giving up (terminal)"
69870
+ );
69871
+ this._handleError(new Error("Device was revoked"));
69872
+ return;
69873
+ }
69874
+ const deviceId = this._deviceId ?? this._persisted?.deviceId;
69875
+ if (!deviceId) {
69876
+ this._handleError(new Error("Device was revoked"));
69877
+ return;
69878
+ }
69879
+ let reconnect;
69880
+ try {
69881
+ const status = await pollDeviceStatus(this.config.apiUrl, deviceId);
69882
+ reconnect = status.rateLimited === true || status.status === "ACTIVE";
69883
+ if (!reconnect) {
69884
+ console.warn(
69885
+ `[SecureChannel] device_revoked confirmed by server (status=${status.status}) \u2014 terminal`
69886
+ );
69887
+ }
69888
+ } catch (err) {
69889
+ console.warn(
69890
+ "[SecureChannel] device_revoked status check failed; reconnecting (inconclusive):",
69891
+ err
69892
+ );
69893
+ reconnect = true;
69894
+ }
69895
+ if (reconnect) {
69896
+ this._revokeRecoveries.push(now);
69897
+ console.log(
69898
+ "[SecureChannel] device_revoked but device not confirmed dead \u2014 reconnecting (self-heal)"
69899
+ );
69900
+ this._scheduleReconnect();
69901
+ return;
69902
+ }
69856
69903
  this._handleError(new Error("Device was revoked"));
69857
69904
  }
69858
69905
  /**
@@ -96803,7 +96850,7 @@ var init_index = __esm({
96803
96850
  init_skill_invoker();
96804
96851
  await init_skill_telemetry();
96805
96852
  await init_policy_enforcer();
96806
- VERSION = true ? "0.23.1" : "0.0.0-dev";
96853
+ VERSION = true ? "0.23.3" : "0.0.0-dev";
96807
96854
  }
96808
96855
  });
96809
96856
  await init_index();