@agentvault/claude-bridge 0.5.3 → 0.5.4

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.
Files changed (2) hide show
  1. package/dist/index.js +105 -9
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10,10 +10,19 @@ var __export = (target, all) => {
10
10
  };
11
11
 
12
12
  // src/config.ts
13
- import { existsSync } from "node:fs";
13
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
14
14
  import { join as join5 } from "node:path";
15
+ function hasRecoverableBackup(dataDir) {
16
+ try {
17
+ const parsed = JSON.parse(readFileSync2(join5(dataDir, BACKUP_FILE), "utf-8"));
18
+ return !!(parsed && parsed.deviceId && parsed.deviceJwt && parsed.sessions && Object.keys(parsed.sessions).length > 0);
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
15
23
  function hasPersistedCreds(dataDir) {
16
- return CRED_FILES.some((f7) => existsSync(join5(dataDir, f7)));
24
+ if (CRED_FILES.some((f7) => existsSync(join5(dataDir, f7)))) return true;
25
+ return hasRecoverableBackup(dataDir);
17
26
  }
18
27
  function slugify2(name) {
19
28
  return name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
@@ -78,11 +87,12 @@ function loadConfig(env, argv = []) {
78
87
  armRoom
79
88
  };
80
89
  }
81
- var CRED_FILES;
90
+ var CRED_FILES, BACKUP_FILE;
82
91
  var init_config = __esm({
83
92
  "src/config.ts"() {
84
93
  "use strict";
85
94
  CRED_FILES = ["agentvault.json", "secure-channel.json"];
95
+ BACKUP_FILE = "agentvault.json.bak";
86
96
  }
87
97
  });
88
98
 
@@ -64550,6 +64560,11 @@ function migratePersistedState(raw) {
64550
64560
  messageHistory: []
64551
64561
  };
64552
64562
  }
64563
+ function _isSelfHubAddress(ownHubAddress, target) {
64564
+ const own = (ownHubAddress ?? "").trim().toLowerCase();
64565
+ const tgt = (target ?? "").trim().toLowerCase();
64566
+ return own.length > 0 && own === tgt;
64567
+ }
64553
64568
  var ROOM_AGENT_TYPES;
64554
64569
  var CREDENTIAL_MESSAGE_TYPES;
64555
64570
  var CATCHUP_MESSAGE_TYPES;
@@ -64621,6 +64636,15 @@ var init_channel = __esm2({
64621
64636
  _ackTimer = null;
64622
64637
  _stopped = false;
64623
64638
  _persisted = null;
64639
+ // AgentVault display_name, resolved from GET /hub/identities (#499, A1).
64640
+ // hub_address alone isn't enough — its random suffix (wren-8789ee6d…) differs
64641
+ // from what the owner types (Wren). Empty until resolved; the DM preamble is
64642
+ // then omitted rather than asserting a wrong name. Re-resolved on every
64643
+ // connect (picks up renames; retries after a failed fetch).
64644
+ _ownDisplayName = "";
64645
+ // Dedupes concurrent resolves (connect-trigger + lazy DM-trigger) so a burst
64646
+ // of DMs can't stampede /hub/identities.
64647
+ _resolvingOwnDisplayName = null;
64624
64648
  _httpServer = null;
64625
64649
  _mcpServer = null;
64626
64650
  _pollFallbackTimer = null;
@@ -64652,7 +64676,7 @@ var init_channel = __esm2({
64652
64676
  _pendingMlsKpBundle;
64653
64677
  /**
64654
64678
  * Pool of pending KeyPackage bundles this device has published but not yet
64655
- * consumed by a Welcome. The backend now consumes published KeyPackages FIFO,
64679
+ * consumed by a Welcome. The backend consumes published KeyPackages newest-first (#508),
64656
64680
  * so a given room's creator may have been served ANY of these — `_handleMlsWelcome`
64657
64681
  * trial-decrypts the incoming Welcome against every bundle in the pool (plus the
64658
64682
  * connect KP and the persisted per-group bundle). Capped at `_KP_POOL_TARGET`;
@@ -64890,6 +64914,50 @@ var init_channel = __esm2({
64890
64914
  get deviceId() {
64891
64915
  return this._deviceId;
64892
64916
  }
64917
+ /** This agent's AgentVault display_name (#499, A1). "" until resolved. */
64918
+ get ownDisplayName() {
64919
+ return this._ownDisplayName;
64920
+ }
64921
+ /** This agent's AgentVault hub_address (#499, A1). "" until known. */
64922
+ get ownHubAddress() {
64923
+ return this._persisted?.hubAddress ?? "";
64924
+ }
64925
+ /** Public best-effort trigger to (re)resolve this agent's display_name.
64926
+ * Called lazily from the 1:1 DM path when the name is still unknown, so a
64927
+ * resolve that failed at connect (or a fresh agent that hadn't registered
64928
+ * yet) is retried without waiting for the next WS reconnect (#499, A1). */
64929
+ ensureOwnIdentityResolved() {
64930
+ return this._resolveOwnDisplayName();
64931
+ }
64932
+ /** Resolve this agent's own display_name from GET /api/v1/hub/identities
64933
+ * (matches its own device_id). Deduped — concurrent callers share one
64934
+ * in-flight fetch. Refreshes the cached value on success (picks up a rename);
64935
+ * on failure leaves the prior value intact. Best-effort — never throws.
64936
+ * Mirrors Hermes RestClient.get_own_identity (#499, Component 0). */
64937
+ _resolveOwnDisplayName() {
64938
+ if (this._resolvingOwnDisplayName) return this._resolvingOwnDisplayName;
64939
+ const p2 = (async () => {
64940
+ const jwt22 = this._persisted?.deviceJwt ?? this._deviceJwt;
64941
+ const deviceId = this._persisted?.deviceId ?? this._deviceId;
64942
+ if (!jwt22 || !deviceId) return;
64943
+ try {
64944
+ const res = await fetch(`${this.config.apiUrl}/api/v1/hub/identities`, {
64945
+ headers: { Authorization: `Bearer ${jwt22}` }
64946
+ });
64947
+ if (!res.ok) return;
64948
+ const list = await res.json();
64949
+ if (!Array.isArray(list)) return;
64950
+ const mine = list.find((x22) => x22?.device_id === deviceId);
64951
+ if (mine?.display_name) this._ownDisplayName = String(mine.display_name);
64952
+ } catch {
64953
+ }
64954
+ })();
64955
+ this._resolvingOwnDisplayName = p2;
64956
+ void p2.finally(() => {
64957
+ if (this._resolvingOwnDisplayName === p2) this._resolvingOwnDisplayName = null;
64958
+ });
64959
+ return p2;
64960
+ }
64893
64961
  get fingerprint() {
64894
64962
  return this._fingerprint;
64895
64963
  }
@@ -66413,6 +66481,11 @@ var init_channel = __esm2({
66413
66481
  if (!this._persisted?.hubAddress) {
66414
66482
  throw new Error("This agent does not have a hub address assigned");
66415
66483
  }
66484
+ if (_isSelfHubAddress(this._persisted.hubAddress, responderHubAddress)) {
66485
+ throw new Error(
66486
+ "Refusing to open an A2A channel to this agent's own hub address (self-reference). The owner is addressing you by your own name \u2014 reply to them directly instead of opening a channel to yourself."
66487
+ );
66488
+ }
66416
66489
  if (!this._deviceJwt) {
66417
66490
  throw new Error("Channel not authenticated");
66418
66491
  }
@@ -66493,6 +66566,11 @@ var init_channel = __esm2({
66493
66566
  * Falls back to plaintext for channels without a session (legacy/pre-encryption).
66494
66567
  */
66495
66568
  async sendToAgent(hubAddress, text, opts) {
66569
+ if (_isSelfHubAddress(this._persisted?.hubAddress ?? "", hubAddress)) {
66570
+ throw new Error(
66571
+ "Refusing to send an A2A message to this agent's own hub address (self-reference) \u2014 reply to the owner directly."
66572
+ );
66573
+ }
66496
66574
  if (!this._persisted?.a2aChannels) {
66497
66575
  try {
66498
66576
  await this.listA2AChannels();
@@ -67117,8 +67195,7 @@ var init_channel = __esm2({
67117
67195
  return;
67118
67196
  }
67119
67197
  if (data.event === "device_revoked") {
67120
- await clearState(this.config.dataDir);
67121
- this._handleError(new Error("Device was revoked"));
67198
+ this._handleDeviceRevoked();
67122
67199
  return;
67123
67200
  }
67124
67201
  if (data.event === "device_linked") {
@@ -67342,6 +67419,7 @@ var init_channel = __esm2({
67342
67419
  this._persisted.agentHubId = data.data.hub_id;
67343
67420
  this._persisted.agentRole = data.data.agent_role ?? "peer";
67344
67421
  if (changed) this._persistState();
67422
+ void this._resolveOwnDisplayName();
67345
67423
  if (!this._telemetryReporter && this._persisted.deviceJwt && this._persisted.hubId) {
67346
67424
  this._telemetryReporter = new TelemetryReporter({
67347
67425
  apiBase: this.config.apiUrl,
@@ -70516,6 +70594,24 @@ ${messageText}`;
70516
70594
  this._setState("error");
70517
70595
  this.emit("error", err);
70518
70596
  }
70597
+ /**
70598
+ * Handle a WS `device_revoked` event. Surfaces a terminal error so
70599
+ * attachLifecycle stops the channel — but does NOT delete credentials.
70600
+ *
70601
+ * This signal fires both for a genuine owner revoke AND for a transient
70602
+ * session-takeover of a device that is still ACTIVE on the server (the error
70603
+ * text even reads "or another session has taken over"). Deleting
70604
+ * agentvault.json here — as this path used to via clearState() — strands a
70605
+ * still-valid identity: the primary creds vanish, only the .bak survives, and
70606
+ * the bridge's token-gate then demands a fresh invite token to restart.
70607
+ * Keeping the creds means a restart reconnects via restoreState() when the
70608
+ * device is still valid, and fails cleanly (device_revoked again, no data
70609
+ * lost) when it isn't. Re-enrollment, which SHOULD wipe creds, goes through
70610
+ * the explicit setup --force / _forceReEnroll path in start().
70611
+ */
70612
+ _handleDeviceRevoked() {
70613
+ this._handleError(new Error("Device was revoked"));
70614
+ }
70519
70615
  /**
70520
70616
  * Persist all ratchet session states to disk.
70521
70617
  * Syncs live ratchet states back into the persisted sessions map.
@@ -132488,7 +132584,7 @@ var ArmingState = class {
132488
132584
  };
132489
132585
 
132490
132586
  // src/approve-cli.ts
132491
- import { mkdirSync as mkdirSync2, writeFileSync, readFileSync as readFileSync3, readdirSync as readdirSync2, rmSync as rmSync2, existsSync as existsSync3 } from "node:fs";
132587
+ import { mkdirSync as mkdirSync2, writeFileSync, readFileSync as readFileSync4, readdirSync as readdirSync2, rmSync as rmSync2, existsSync as existsSync3 } from "node:fs";
132492
132588
  import { join as join6 } from "node:path";
132493
132589
  var APPROVALS_SUBDIR = "arm-approvals";
132494
132590
  var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
@@ -132521,7 +132617,7 @@ function drainApprovals(dataDir) {
132521
132617
  if (!ID_RE.test(name)) continue;
132522
132618
  let roomId = "";
132523
132619
  try {
132524
- roomId = readFileSync3(join6(dir, name), "utf8").trim();
132620
+ roomId = readFileSync4(join6(dir, name), "utf8").trim();
132525
132621
  } catch {
132526
132622
  }
132527
132623
  out.push({ requestId: name, roomId: ID_RE.test(roomId) ? roomId : "" });
@@ -132770,7 +132866,7 @@ async function main() {
132770
132866
  "[bridge] warning: passing the invite token on the command line is visible to other local users via 'ps'. Prefer: AV_INVITE_TOKEN=\u2026 npx @agentvault/claude-bridge"
132771
132867
  );
132772
132868
  }
132773
- console.error(`[bridge] version: ${true ? "0.5.3" : "dev"}`);
132869
+ console.error(`[bridge] version: ${true ? "0.5.4" : "dev"}`);
132774
132870
  console.error(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
132775
132871
  if (cfg.worker) {
132776
132872
  console.error(`[bridge] WORKER MODE \u2014 workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvault/claude-bridge",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "type": "module",
5
5
  "description": "AgentVault Claude Bridge — daemon for bridging a Claude agent into secure E2E-encrypted AgentVault 1:1 direct messages and rooms.",
6
6
  "main": "dist/index.js",