a2acalling 0.6.69 → 0.6.70

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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.6.69",
3
- "installed_at": "2026-02-26T05:13:54.868Z",
2
+ "version": "0.6.70",
3
+ "installed_at": "2026-02-26T06:18:02.969Z",
4
4
  "files": [
5
5
  {
6
6
  "path": "CLAUDE.md",
package/CONVENTIONS.md CHANGED
@@ -66,6 +66,7 @@ All modules use CommonJS (`require`/`module.exports`). Each lib file exports a f
66
66
  - Dark theme is the default; uses CSS custom properties for theming
67
67
  - Sidebar navigation with tab switching (Contacts, Calls, Invites, Logs, Settings, Permissions, Health)
68
68
  - Permissions tab uses tier cards with tool toggles and auto-save
69
+ - Drag-and-drop uses event delegation on stable parent containers (`.perm-sidebar` for sidebar items, zone containers for drop targets) — do NOT bind listeners directly to innerHTML-generated elements (A2A-61)
69
70
 
70
71
  ## Network Resilience (A2A-54)
71
72
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.6.69",
3
+ "version": "0.6.70",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/routes/a2a.js CHANGED
@@ -57,6 +57,10 @@ function getCallMonitor(options = {}) {
57
57
  // For production: use Redis or persistent store
58
58
  const rateLimits = new Map();
59
59
 
60
+ // Rate limit eviction constants
61
+ const RATE_LIMIT_MAX_ENTRIES = 1000;
62
+ const RATE_LIMIT_STALE_MS = 24 * 60 * 60 * 1000; // 24 hours
63
+
60
64
  // Constants
61
65
  const MAX_MESSAGE_LENGTH = 10000; // 10KB max message
62
66
  const MAX_TIMEOUT_SECONDS = 300; // 5 min max timeout
@@ -104,6 +108,20 @@ function normalizeRequestMetadata(req) {
104
108
  };
105
109
  }
106
110
 
111
+ /**
112
+ * Timing-safe comparison of two token strings.
113
+ * Returns true if tokens match, false otherwise.
114
+ * Short-circuits (non-timing-safe) only when a value is missing or empty,
115
+ * which is acceptable since the absence of a token is not secret.
116
+ */
117
+ function timingSafeTokenEqual(a, b) {
118
+ if (!a || !b) return false;
119
+ const bufA = Buffer.from(String(a));
120
+ const bufB = Buffer.from(String(b));
121
+ if (bufA.length !== bufB.length) return false;
122
+ return crypto.timingSafeEqual(bufA, bufB);
123
+ }
124
+
107
125
  function checkRateLimit(tokenId, limits = { minute: 10, hour: 100, day: 1000 }) {
108
126
  const now = Date.now();
109
127
  const minute = Math.floor(now / 60000);
@@ -141,6 +159,34 @@ function checkRateLimit(tokenId, limits = { minute: 10, hour: 100, day: 1000 })
141
159
  state.hour.count++;
142
160
  state.day.count++;
143
161
 
162
+ // Evict stale entries when the Map exceeds the size threshold
163
+ if (rateLimits.size > RATE_LIMIT_MAX_ENTRIES) {
164
+ const staleThreshold = now - RATE_LIMIT_STALE_MS;
165
+ let evicted = 0;
166
+ for (const [key, entry] of rateLimits) {
167
+ // An entry is stale if all three bucket timestamps are older than 24h
168
+ const latestBucket = Math.max(
169
+ entry.minute.bucket * 60000,
170
+ entry.hour.bucket * 3600000,
171
+ entry.day.bucket * 86400000
172
+ );
173
+ if (latestBucket < staleThreshold) {
174
+ rateLimits.delete(key);
175
+ evicted++;
176
+ }
177
+ }
178
+ // If no stale entries found, evict the oldest (first-inserted) entries
179
+ if (evicted === 0) {
180
+ const excess = rateLimits.size - RATE_LIMIT_MAX_ENTRIES;
181
+ let removed = 0;
182
+ for (const key of rateLimits.keys()) {
183
+ if (removed >= excess) break;
184
+ rateLimits.delete(key);
185
+ removed++;
186
+ }
187
+ }
188
+ }
189
+
144
190
  return { limited: false };
145
191
  }
146
192
 
@@ -758,7 +804,7 @@ function createRoutes(options = {}) {
758
804
  message: 'Set A2A_ADMIN_TOKEN to access conversation admin routes from non-local addresses'
759
805
  });
760
806
  }
761
- if (adminToken !== expected) {
807
+ if (!timingSafeTokenEqual(adminToken, expected)) {
762
808
  return res.status(401).json({ error: 'unauthorized' });
763
809
  }
764
810
  }
@@ -773,7 +819,7 @@ function createRoutes(options = {}) {
773
819
  const conversations = convStore.listConversations({
774
820
  contactId: contact_id,
775
821
  status,
776
- limit: parseInt(limit),
822
+ limit: Math.min(100, Math.max(1, Number.parseInt(String(limit), 10) || 20)),
777
823
  includeMessages: false
778
824
  });
779
825
 
@@ -794,7 +840,7 @@ function createRoutes(options = {}) {
794
840
  message: 'Set A2A_ADMIN_TOKEN to access conversation admin routes from non-local addresses'
795
841
  });
796
842
  }
797
- if (adminToken !== expected) {
843
+ if (!timingSafeTokenEqual(adminToken, expected)) {
798
844
  return res.status(401).json({ error: 'unauthorized' });
799
845
  }
800
846
  }
@@ -806,8 +852,8 @@ function createRoutes(options = {}) {
806
852
 
807
853
  const { recent_messages = 10 } = req.query;
808
854
  const context = convStore.getConversationContext(
809
- req.params.id,
810
- parseInt(recent_messages)
855
+ req.params.id,
856
+ Math.min(50, Math.max(1, Number.parseInt(String(recent_messages), 10) || 10))
811
857
  );
812
858
 
813
859
  if (!context) {
@@ -830,4 +876,11 @@ async function defaultMessageHandler(message, context, options) {
830
876
  };
831
877
  }
832
878
 
833
- module.exports = { createRoutes, checkRateLimit };
879
+ module.exports = {
880
+ createRoutes,
881
+ checkRateLimit,
882
+ timingSafeTokenEqual,
883
+ // Exposed for testing only
884
+ _rateLimits: rateLimits,
885
+ _RATE_LIMIT_MAX_ENTRIES: RATE_LIMIT_MAX_ENTRIES
886
+ };
package/src/server.js CHANGED
@@ -27,6 +27,7 @@ const { buildUnifiedSummaryPrompt } = require('./lib/summary-prompt');
27
27
  const { A2AConfig } = require('./lib/config');
28
28
  const { UpdateManager } = require('./lib/update-manager');
29
29
  const { DashboardEventStore } = require('./lib/dashboard-events');
30
+ const { CallbookStore } = require('./lib/callbook');
30
31
  const { spawn } = require('child_process');
31
32
  const { resolveTurnTimeoutMs } = require('./lib/turn-timeout');
32
33
 
@@ -69,6 +70,8 @@ const agentContext = loadAgentContext();
69
70
  const tokenStore = new TokenStore();
70
71
  const config = new A2AConfig();
71
72
  const eventStore = new DashboardEventStore(tokenStore.configDir);
73
+ // A2A-59: Hoist CallbookStore to server.js so shutdown() can close it
74
+ const callbookStore = new CallbookStore(tokenStore.configDir);
72
75
  const runtime = createRuntimeAdapter({
73
76
  workspaceDir,
74
77
  agentContext,
@@ -878,6 +881,7 @@ app.use('/api/a2a/dashboard', createDashboardApiRouter({
878
881
  agentContext,
879
882
  config,
880
883
  eventStore,
884
+ callbookStore,
881
885
  getUpdateManager: () => updateManager,
882
886
  logger: logger.child({ component: 'a2a.dashboard' })
883
887
  }));
@@ -1081,6 +1085,8 @@ async function startServer() {
1081
1085
  try { serverConvStore.close(); } catch (_) {}
1082
1086
  }
1083
1087
  try { eventStore.close(); } catch (_) {}
1088
+ // A2A-59: Close CallbookStore to flush WAL on shutdown
1089
+ try { callbookStore.close(); } catch (_) {}
1084
1090
  try { closeAllLoggerStores(); } catch (_) {}
1085
1091
  server.close(() => process.exit(0));
1086
1092
  // Force exit after 5s if connections won't close