a2acalling 0.6.0 → 0.6.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.
@@ -22,6 +22,7 @@ const CONFIG_DIR = process.env.A2A_CONFIG_DIR ||
22
22
  const EXTERNAL_IP_CACHE_FILE = path.join(CONFIG_DIR, 'a2a-external-ip.json');
23
23
 
24
24
  const DEFAULT_SERVICES = [
25
+ 'https://ifconfig.me/ip',
25
26
  'https://api.ipify.org',
26
27
  'https://checkip.amazonaws.com/',
27
28
  'https://icanhazip.com/'
@@ -113,6 +114,7 @@ async function fetchExternalIp(options = {}) {
113
114
  ? options.services
114
115
  : DEFAULT_SERVICES;
115
116
 
117
+ const attempts = [];
116
118
  let lastError = null;
117
119
  for (const serviceUrl of services) {
118
120
  try {
@@ -124,14 +126,18 @@ async function fetchExternalIp(options = {}) {
124
126
  if (!ip) {
125
127
  throw new Error('invalid_ip');
126
128
  }
127
- return { ip, source: serviceUrl };
129
+ attempts.push({ service: serviceUrl, ok: true, statusCode: res.statusCode, ip });
130
+ return { ip, source: serviceUrl, attempts };
128
131
  } catch (err) {
129
132
  lastError = err;
133
+ attempts.push({ service: serviceUrl, ok: false, error: err && err.message ? err.message : 'request_failed' });
130
134
  }
131
135
  }
132
136
 
133
137
  const msg = lastError ? lastError.message : 'unavailable';
134
- throw new Error(`external_ip_unavailable:${msg}`);
138
+ const failure = new Error(`external_ip_unavailable:${msg}`);
139
+ failure.attempts = attempts;
140
+ throw failure;
135
141
  }
136
142
 
137
143
  /**
@@ -168,13 +174,13 @@ async function getExternalIp(options = {}) {
168
174
  }
169
175
 
170
176
  try {
171
- const { ip, source } = await fetchExternalIp({
177
+ const { ip, source, attempts } = await fetchExternalIp({
172
178
  timeoutMs: options.timeoutMs,
173
179
  services: options.services
174
180
  });
175
181
  const checkedAt = new Date(nowMs).toISOString();
176
182
  atomicWriteJson(cacheFile, { ip, checked_at: checkedAt, source });
177
- return { ip, checkedAt, source, fromCache: false, stale: false };
183
+ return { ip, checkedAt, source, fromCache: false, stale: false, attempts: Array.isArray(attempts) ? attempts : null };
178
184
  } catch (err) {
179
185
  if (cached && cached.ip) {
180
186
  const cachedIp = parseIp(cached.ip);
@@ -184,11 +190,17 @@ async function getExternalIp(options = {}) {
184
190
  checkedAt: cached.checked_at || null,
185
191
  source: cached.source || 'cache',
186
192
  fromCache: true,
187
- stale: true
193
+ stale: true,
194
+ error: err && err.message ? err.message : 'external_ip_unavailable',
195
+ attempts: err && Array.isArray(err.attempts) ? err.attempts : null
188
196
  };
189
197
  }
190
198
  }
191
- return { ip: null, error: err.message };
199
+ return {
200
+ ip: null,
201
+ error: err && err.message ? err.message : 'external_ip_unavailable',
202
+ attempts: err && Array.isArray(err.attempts) ? err.attempts : null
203
+ };
192
204
  }
193
205
  }
194
206
 
@@ -197,4 +209,3 @@ module.exports = {
197
209
  fetchExternalIp,
198
210
  getExternalIp
199
211
  };
200
-
@@ -124,14 +124,6 @@ function isPublicIpHostname(hostname) {
124
124
  return false;
125
125
  }
126
126
 
127
- function isLegacyTunnelHostname(hostname) {
128
- const host = String(hostname || '').trim().toLowerCase();
129
- if (!host) return false;
130
- // Legacy: Cloudflare Quick Tunnel hostnames were ephemeral and are now unsupported.
131
- if (host.endsWith('.trycloudflare.com')) return true;
132
- return false;
133
- }
134
-
135
127
  async function resolveInviteHost(options = {}) {
136
128
  const config = options.config || null;
137
129
 
@@ -155,8 +147,7 @@ async function resolveInviteHost(options = {}) {
155
147
  : 'default';
156
148
 
157
149
  const parsed = splitHostPort(candidate);
158
- const candidateIsLegacyTunnel = candidateSource !== 'env' && isLegacyTunnelHostname(parsed.hostname);
159
- const desiredPort = (candidateIsLegacyTunnel ? null : parsed.port) ||
150
+ const desiredPort = parsed.port ||
160
151
  Number.parseInt(String(options.defaultPort || ''), 10) ||
161
152
  readIntEnv('PORT') ||
162
153
  readIntEnv('A2A_PORT') ||
@@ -170,38 +161,28 @@ async function resolveInviteHost(options = {}) {
170
161
  ? options.externalIpTtlMs
171
162
  : undefined;
172
163
 
173
- // If a previous run persisted a legacy Cloudflare Quick Tunnel hostname into config (e.g. trycloudflare),
174
- // treat it like "unroutable" so we don't emit stale/unsupported invite endpoints.
175
- if (candidateIsLegacyTunnel) {
176
- warnings.push(
177
- `Detected legacy Quick Tunnel hostname "${candidateHostWithPort}". Quick Tunnel support was removed. ` +
178
- `Set A2A_HOSTNAME="your-public-host:port" (or wire a reverse proxy) to enable internet-facing invites.`
179
- );
180
- }
181
-
182
164
  const shouldReplaceWithExternalIp = isLocalOrUnroutableHost(parsed.hostname) ||
183
- candidateIsLegacyTunnel || (
165
+ (
184
166
  options.refreshExternalIp && isPublicIpHostname(parsed.hostname)
185
167
  );
186
168
 
187
- if (!shouldReplaceWithExternalIp) {
188
- return {
189
- host: candidateHostWithPort,
190
- source: candidateSource,
191
- originalHost: candidateHostWithPort,
192
- warnings
193
- };
194
- }
195
-
196
- const external = await getExternalIp({
197
- ttlMs,
198
- timeoutMs: options.externalIpTimeoutMs,
199
- services: options.externalIpServices,
200
- cacheFile: options.externalIpCacheFile,
201
- forceRefresh: Boolean(options.forceRefreshExternalIp)
202
- });
203
-
204
- if (external && external.ip) {
169
+ const alwaysLookupExternalIp = Boolean(options.alwaysLookupExternalIp);
170
+ const wantsExternalIp = shouldReplaceWithExternalIp || alwaysLookupExternalIp;
171
+ const warnOnExternalIpFailure = options.warnOnExternalIpFailure !== undefined
172
+ ? Boolean(options.warnOnExternalIpFailure)
173
+ : shouldReplaceWithExternalIp;
174
+
175
+ const external = wantsExternalIp
176
+ ? await getExternalIp({
177
+ ttlMs,
178
+ timeoutMs: options.externalIpTimeoutMs,
179
+ services: options.externalIpServices,
180
+ cacheFile: options.externalIpCacheFile,
181
+ forceRefresh: Boolean(options.forceRefreshExternalIp)
182
+ })
183
+ : null;
184
+
185
+ if (shouldReplaceWithExternalIp && external && external.ip) {
205
186
  const finalHost = formatHostPort(external.ip, desiredPort);
206
187
  if (finalHost !== candidateHostWithPort) {
207
188
  warnings.push(
@@ -213,17 +194,21 @@ async function resolveInviteHost(options = {}) {
213
194
  source: 'external_ip',
214
195
  originalHost: candidateHostWithPort,
215
196
  externalIp: external.ip,
197
+ externalIpInfo: external,
216
198
  warnings
217
199
  };
218
200
  }
219
201
 
220
- warnings.push(
221
- `Invite host "${candidateHostWithPort}" may not be reachable from other machines, and external IP lookup failed. Set A2A_HOSTNAME="your-public-host:port".`
222
- );
202
+ if (wantsExternalIp && (!external || !external.ip) && warnOnExternalIpFailure) {
203
+ warnings.push(
204
+ `Invite host "${candidateHostWithPort}" may not be reachable from other machines, and external IP lookup failed. Set A2A_HOSTNAME="your-public-host:port".`
205
+ );
206
+ }
223
207
  return {
224
208
  host: candidateHostWithPort,
225
209
  source: candidateSource,
226
210
  originalHost: candidateHostWithPort,
211
+ externalIpInfo: external,
227
212
  warnings
228
213
  };
229
214
  }
package/src/lib/logger.js CHANGED
@@ -129,7 +129,28 @@ class LogStore {
129
129
  } catch (err) {
130
130
  // best effort
131
131
  }
132
- this._migrate();
132
+ const ok = this._ensureSchema();
133
+ if (!ok) {
134
+ // Prototyping mode: do not attempt DB migrations; keep the old file and start fresh.
135
+ const backupPath = `${this.dbPath}.legacy.${Date.now()}`;
136
+ try {
137
+ this.db.close();
138
+ } catch (err) {
139
+ // ignore
140
+ }
141
+ fs.renameSync(this.dbPath, backupPath);
142
+ this.db = new Database(this.dbPath);
143
+ try {
144
+ fs.chmodSync(this.dbPath, 0o600);
145
+ } catch (err) {
146
+ // best effort
147
+ }
148
+ const ok2 = this._ensureSchema();
149
+ if (!ok2) {
150
+ this._dbError = 'failed_to_initialize_log_db_schema';
151
+ return null;
152
+ }
153
+ }
133
154
  this._prepareStatements();
134
155
  return this.db;
135
156
  } catch (err) {
@@ -138,7 +159,7 @@ class LogStore {
138
159
  }
139
160
  }
140
161
 
141
- _migrate() {
162
+ _ensureSchema() {
142
163
  this.db.exec(`
143
164
  CREATE TABLE IF NOT EXISTS logs (
144
165
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -167,18 +188,9 @@ class LogStore {
167
188
  `);
168
189
 
169
190
  const columns = this.db.prepare(`PRAGMA table_info(logs)`).all();
170
- const hasErrorCode = columns.some(c => c.name === 'error_code');
171
- const hasStatusCode = columns.some(c => c.name === 'status_code');
172
- const hasHint = columns.some(c => c.name === 'hint');
173
- if (!hasErrorCode) {
174
- this.db.exec(`ALTER TABLE logs ADD COLUMN error_code TEXT`);
175
- }
176
- if (!hasStatusCode) {
177
- this.db.exec(`ALTER TABLE logs ADD COLUMN status_code INTEGER`);
178
- }
179
- if (!hasHint) {
180
- this.db.exec(`ALTER TABLE logs ADD COLUMN hint TEXT`);
181
- }
191
+ const names = new Set(columns.map(c => c && c.name).filter(Boolean));
192
+ const required = ['timestamp', 'level', 'component', 'message', 'error_code', 'status_code', 'hint', 'data'];
193
+ return required.every((name) => names.has(name));
182
194
  }
183
195
 
184
196
  _prepareStatements() {