@agentunion/fastaun 0.4.4 → 0.4.6

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 (64) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/_packed_docs/CHANGELOG.md +41 -0
  3. package/_packed_docs/INDEX.md +2 -2
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +1 -1
  5. package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +73 -84
  6. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +16 -15
  7. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +2 -2
  8. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +22 -5
  9. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +42 -26
  10. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +2 -2
  11. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +61 -35
  12. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +3 -3
  13. package/_packed_docs/sdk/09-message-rpc-manual.md +6 -6
  14. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +6 -4
  15. package/_packed_docs/sdk/INDEX.md +2 -2
  16. package/_packed_docs/sdk/README.md +3 -3
  17. package/dist/agent-md.d.ts +101 -0
  18. package/dist/agent-md.js +778 -0
  19. package/dist/agent-md.js.map +1 -0
  20. package/dist/aid-store.d.ts +7 -39
  21. package/dist/aid-store.js +74 -141
  22. package/dist/aid-store.js.map +1 -1
  23. package/dist/auth.d.ts +17 -32
  24. package/dist/auth.js +42 -295
  25. package/dist/auth.js.map +1 -1
  26. package/dist/client.d.ts +6 -65
  27. package/dist/client.js +213 -913
  28. package/dist/client.js.map +1 -1
  29. package/dist/crypto.d.ts +1 -1
  30. package/dist/crypto.js +1 -1
  31. package/dist/index.d.ts +4 -2
  32. package/dist/index.js +3 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/keystore/aid-db.d.ts +0 -4
  35. package/dist/keystore/aid-db.js +4 -95
  36. package/dist/keystore/aid-db.js.map +1 -1
  37. package/dist/keystore/file.d.ts +8 -3
  38. package/dist/keystore/file.js +103 -24
  39. package/dist/keystore/file.js.map +1 -1
  40. package/dist/keystore/index.d.ts +43 -36
  41. package/dist/keystore/index.js +3 -2
  42. package/dist/keystore/index.js.map +1 -1
  43. package/dist/keystore/local-identity-store.d.ts +70 -0
  44. package/dist/keystore/local-identity-store.js +525 -0
  45. package/dist/keystore/local-identity-store.js.map +1 -0
  46. package/dist/keystore/local-token-store.d.ts +68 -0
  47. package/dist/keystore/local-token-store.js +368 -0
  48. package/dist/keystore/local-token-store.js.map +1 -0
  49. package/dist/register-flow.d.ts +57 -0
  50. package/dist/register-flow.js +433 -0
  51. package/dist/register-flow.js.map +1 -0
  52. package/dist/secret-store/file-store.js +6 -1
  53. package/dist/secret-store/file-store.js.map +1 -1
  54. package/dist/v2/session/keystore.d.ts +5 -0
  55. package/dist/v2/session/keystore.js +21 -3
  56. package/dist/v2/session/keystore.js.map +1 -1
  57. package/dist/version.d.ts +1 -1
  58. package/dist/version.js +1 -1
  59. package/package.json +1 -1
  60. package/_packed_docs/0.4.0_/345/267/256/345/274/202/346/240/270/345/256/236/345/206/263/347/255/226/350/256/260/345/275/225.md +0 -302
  61. package/_packed_docs/AUN_SDK_0.4.0_/350/256/276/350/256/241/345/257/271/346/257/224/345/210/206/346/236/220.md +0 -194
  62. package/_packed_docs/AUN_SDK_/351/207/215/346/236/204/345/256/236/346/226/275/350/256/241/345/210/222.md +0 -596
  63. package/_packed_docs/AUN_SDK_/351/207/215/346/236/204/350/256/276/350/256/241/346/226/271/346/241/210_v3.md +0 -1698
  64. package/_packed_docs/python-sdk-v2-only-changelog.md +0 -189
@@ -0,0 +1,778 @@
1
+ import * as crypto from 'node:crypto';
2
+ import * as fs from 'node:fs';
3
+ import * as http from 'node:http';
4
+ import * as https from 'node:https';
5
+ import * as path from 'node:path';
6
+ import { AID } from './aid.js';
7
+ import { AUNError, ClientSignatureError, NotFoundError, StateError, ValidationError, } from './errors.js';
8
+ import { isJsonObject } from './types.js';
9
+ const DEFAULT_HTTP_TIMEOUT_MS = 30_000;
10
+ const HEAD_HTTP_TIMEOUT_MS = 15_000;
11
+ const DOWNLOAD_CONCURRENCY = 8;
12
+ const noopLogger = {
13
+ error: () => { },
14
+ warn: () => { },
15
+ info: () => { },
16
+ debug: () => { },
17
+ };
18
+ function headerValue(headers, name) {
19
+ const target = name.toLowerCase();
20
+ for (const [key, value] of Object.entries(headers)) {
21
+ if (key.toLowerCase() === target)
22
+ return String(value ?? '').trim();
23
+ }
24
+ return '';
25
+ }
26
+ function toHeaders(headers) {
27
+ const out = {};
28
+ for (const [key, value] of Object.entries(headers)) {
29
+ if (typeof value === 'string')
30
+ out[key] = value;
31
+ else if (Array.isArray(value))
32
+ out[key] = value[0] ?? '';
33
+ }
34
+ return out;
35
+ }
36
+ function agentMdSchemeFromGateway(gatewayUrl) {
37
+ return String(gatewayUrl ?? '').trim().toLowerCase().startsWith('ws://') ? 'http' : 'https';
38
+ }
39
+ function requestText(url, opts) {
40
+ const redirectsLeft = opts.redirectsLeft ?? 3;
41
+ return new Promise((resolve, reject) => {
42
+ const parsed = new URL(url);
43
+ const mod = parsed.protocol === 'https:' ? https : http;
44
+ const requestOptions = {
45
+ method: opts.method,
46
+ headers: opts.headers,
47
+ timeout: opts.timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS,
48
+ };
49
+ if (!opts.verifySsl)
50
+ requestOptions.rejectUnauthorized = false;
51
+ const req = mod.request(url, requestOptions, (res) => {
52
+ const status = res.statusCode ?? 0;
53
+ const location = typeof res.headers.location === 'string' ? res.headers.location : '';
54
+ if ([301, 302, 303, 307, 308].includes(status) && location && redirectsLeft > 0) {
55
+ res.resume();
56
+ const nextUrl = new URL(location, url).toString();
57
+ requestText(nextUrl, { ...opts, redirectsLeft: redirectsLeft - 1 }).then(resolve, reject);
58
+ return;
59
+ }
60
+ const chunks = [];
61
+ res.on('data', (chunk) => chunks.push(chunk));
62
+ res.on('end', () => {
63
+ resolve({
64
+ status,
65
+ headers: toHeaders(res.headers),
66
+ text: Buffer.concat(chunks).toString('utf-8'),
67
+ });
68
+ });
69
+ res.on('error', reject);
70
+ });
71
+ req.on('error', reject);
72
+ req.on('timeout', () => {
73
+ req.destroy();
74
+ reject(new AUNError(`agent.md request timed out after ${opts.timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS}ms`));
75
+ });
76
+ if (opts.body !== undefined)
77
+ req.write(opts.body, 'utf-8');
78
+ req.end();
79
+ });
80
+ }
81
+ export class AgentMdManager {
82
+ _aunPath;
83
+ _verifySsl;
84
+ _log;
85
+ _discoveryPort;
86
+ _ownerAidGetter;
87
+ _currentAidGetter;
88
+ _gatewayResolver;
89
+ _peerResolver;
90
+ _accessTokenResolver;
91
+ _aidValidator;
92
+ _cache = new Map();
93
+ _downloadInflight = new Map();
94
+ _downloadActive = 0;
95
+ _downloadWaiters = [];
96
+ constructor(opts) {
97
+ this._aunPath = String(opts.aunPath ?? '');
98
+ this._verifySsl = opts.verifySsl ?? true;
99
+ this._log = opts.logger ?? noopLogger;
100
+ this._discoveryPort = opts.discoveryPort ?? null;
101
+ this._ownerAidGetter = opts.ownerAidGetter;
102
+ this._currentAidGetter = opts.currentAidGetter;
103
+ this._gatewayResolver = opts.gatewayResolver;
104
+ this._peerResolver = opts.peerResolver;
105
+ this._accessTokenResolver = opts.accessTokenResolver;
106
+ this._aidValidator = opts.aidValidator;
107
+ }
108
+ static contentEtag(content) {
109
+ return `"${crypto.createHash('sha256').update(String(content ?? ''), 'utf-8').digest('hex')}"`;
110
+ }
111
+ get root() {
112
+ const root = path.join(this._aunPath, 'AIDs');
113
+ fs.mkdirSync(root, { recursive: true });
114
+ return root;
115
+ }
116
+ setAunPath(aunPath) {
117
+ this._aunPath = String(aunPath ?? '');
118
+ this._cache.clear();
119
+ this._downloadInflight.clear();
120
+ }
121
+ safeAid(aid) {
122
+ const target = String(aid ?? '').trim();
123
+ if (!target || target.includes('/') || target.includes('\\') || target.includes('\0')) {
124
+ throw new ValidationError('agent.md aid is empty or contains path separators');
125
+ }
126
+ this._aidValidator?.(target);
127
+ return target;
128
+ }
129
+ filePath(aid) {
130
+ return path.join(this.root, this.safeAid(aid), 'agent.md');
131
+ }
132
+ metaPath(aid) {
133
+ return path.join(this.root, this.safeAid(aid), 'agentmd.json');
134
+ }
135
+ readContent(aid) {
136
+ return fs.readFileSync(this.filePath(aid), 'utf-8');
137
+ }
138
+ writeContent(aid, content) {
139
+ const filePath = this.filePath(aid);
140
+ this._atomicWriteText(filePath, String(content ?? ''));
141
+ return filePath;
142
+ }
143
+ loadRecord(aid) {
144
+ const target = String(aid ?? '').trim();
145
+ if (!target)
146
+ return null;
147
+ try {
148
+ const record = this._withRecordLock(target, () => this._readRecordUnlocked(target));
149
+ const loaded = Object.keys(record).length > 0 ? { ...record, aid: target } : { aid: target };
150
+ try {
151
+ const content = this.readContent(target);
152
+ loaded.content = content;
153
+ loaded.local_etag = AgentMdManager.contentEtag(content);
154
+ }
155
+ catch (err) {
156
+ if (fs.existsSync(this.metaPath(target))) {
157
+ this._log.warn(`agent.md content read failed: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
158
+ }
159
+ }
160
+ if (Object.keys(loaded).length <= 1)
161
+ return null;
162
+ if (Object.keys(record).length === 0 && typeof loaded.content === 'string') {
163
+ this._withRecordLock(target, () => this._writeRecordUnlocked(target, {
164
+ aid: target,
165
+ local_etag: loaded.local_etag,
166
+ updated_at: Date.now(),
167
+ }));
168
+ }
169
+ this._cache.set(target, { ...loaded });
170
+ return { ...loaded };
171
+ }
172
+ catch (err) {
173
+ this._log.debug(`agent.md cache load skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
174
+ return null;
175
+ }
176
+ }
177
+ saveRecord(aid, fields) {
178
+ const target = String(aid ?? '').trim();
179
+ if (!target)
180
+ return {};
181
+ try {
182
+ const inputFields = { ...fields };
183
+ const hasContent = Object.prototype.hasOwnProperty.call(inputFields, 'content') && inputFields.content !== undefined && inputFields.content !== null;
184
+ let savedTo = '';
185
+ const record = this._withRecordLock(target, () => {
186
+ if (hasContent) {
187
+ const content = String(inputFields.content ?? '');
188
+ savedTo = this.writeContent(target, content);
189
+ if (!inputFields.local_etag)
190
+ inputFields.local_etag = AgentMdManager.contentEtag(content);
191
+ if (!inputFields.fetched_at)
192
+ inputFields.fetched_at = Date.now();
193
+ }
194
+ delete inputFields.content;
195
+ const next = { ...this._readRecordUnlocked(target), aid: target };
196
+ for (const [key, value] of Object.entries(inputFields)) {
197
+ if (value !== undefined && value !== null)
198
+ next[key] = value;
199
+ }
200
+ next.updated_at = Date.now();
201
+ this._writeRecordUnlocked(target, next);
202
+ return next;
203
+ });
204
+ const loaded = { ...record };
205
+ if (hasContent) {
206
+ loaded.content = String(fields.content ?? '');
207
+ if (savedTo)
208
+ loaded.saved_to = savedTo;
209
+ }
210
+ else {
211
+ const current = this.loadRecord(target);
212
+ if (current && typeof current.content === 'string')
213
+ loaded.content = current.content;
214
+ }
215
+ this._cache.set(target, { ...loaded });
216
+ return { ...loaded };
217
+ }
218
+ catch (err) {
219
+ this._log.debug(`agent.md cache save skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
220
+ return {};
221
+ }
222
+ }
223
+ async upload(content) {
224
+ const target = this._ownerAid();
225
+ if (!target)
226
+ throw new ValidationError('uploadAgentMd requires local AID');
227
+ const current = this._currentAid();
228
+ if (!current?.isPrivateKeyValid()) {
229
+ throw new StateError('uploadAgentMd requires loaded AID with a valid private key');
230
+ }
231
+ const rawContent = content === undefined || content === null ? this.readContent(target) : String(content);
232
+ if (!rawContent.trim())
233
+ throw new ValidationError('uploadAgentMd requires non-empty content');
234
+ const signed = current.signAgentMd(rawContent);
235
+ if (!signed.ok || !signed.data) {
236
+ const message = signed.error?.message ?? 'agent.md signing failed';
237
+ throw new ClientSignatureError(message);
238
+ }
239
+ const signedContent = signed.data.signed;
240
+ const gatewayUrl = await this._resolveGateway(target);
241
+ const token = await this._accessToken(target, gatewayUrl);
242
+ const response = await requestText(this._url(target, gatewayUrl), {
243
+ method: 'PUT',
244
+ verifySsl: this._verifySsl,
245
+ headers: {
246
+ Authorization: `Bearer ${token}`,
247
+ 'Content-Type': 'text/markdown; charset=utf-8',
248
+ },
249
+ body: signedContent,
250
+ timeoutMs: DEFAULT_HTTP_TIMEOUT_MS,
251
+ redirectsLeft: 0,
252
+ });
253
+ if (response.status === 404) {
254
+ throw new NotFoundError(`agent.md endpoint not found for aid: ${target}`);
255
+ }
256
+ if (response.status < 200 || response.status >= 300) {
257
+ const message = response.text.trim();
258
+ throw new AUNError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
259
+ }
260
+ let payload;
261
+ try {
262
+ payload = JSON.parse(response.text || '{}');
263
+ }
264
+ catch (err) {
265
+ throw new AUNError(`upload agent.md returned invalid JSON payload: ${err instanceof Error ? err.message : String(err)}`);
266
+ }
267
+ if (!isJsonObject(payload)) {
268
+ throw new AUNError('upload agent.md returned invalid JSON payload');
269
+ }
270
+ const result = payload;
271
+ this.saveRecord(target, {
272
+ content: signedContent,
273
+ local_etag: AgentMdManager.contentEtag(signedContent),
274
+ remote_etag: String(result.etag ?? '').trim() || undefined,
275
+ last_modified: String(result.last_modified ?? result.lastModified ?? '').trim(),
276
+ fetched_at: Date.now(),
277
+ checked_at: Date.now(),
278
+ remote_status: String(result.etag ?? '').trim() ? 'found' : 'unknown',
279
+ last_error: '',
280
+ });
281
+ return { ...result };
282
+ }
283
+ async download(aid, timeoutMs = DEFAULT_HTTP_TIMEOUT_MS) {
284
+ const target = this.safeAid(String(aid ?? this._ownerAid() ?? '').trim());
285
+ const existing = this._downloadInflight.get(target);
286
+ if (existing)
287
+ return await existing;
288
+ const task = (async () => {
289
+ const release = await this._acquireDownloadSlot();
290
+ try {
291
+ return await this._downloadOnce(target, timeoutMs);
292
+ }
293
+ finally {
294
+ release();
295
+ }
296
+ })();
297
+ this._downloadInflight.set(target, task);
298
+ task.finally(() => {
299
+ if (this._downloadInflight.get(target) === task)
300
+ this._downloadInflight.delete(target);
301
+ }).catch(() => undefined);
302
+ return await task;
303
+ }
304
+ async check(aid, ttlDays = 1) {
305
+ const target = this.safeAid(aid);
306
+ const before = this.loadRecord(target) ?? {};
307
+ const localEtag = String(before.local_etag ?? '').trim();
308
+ const localFound = !!(Object.keys(before).length > 0 && (String(before.content ?? '') || localEtag));
309
+ const remoteEtagCached = String(before.remote_etag ?? before.etag ?? '').trim();
310
+ const lastModifiedCached = String(before.last_modified ?? '').trim();
311
+ const checkedAtCached = Number(before.checked_at ?? before.fetched_at ?? 0) || 0;
312
+ const cachedInSync = !!(localFound && localEtag && remoteEtagCached && localEtag === remoteEtagCached);
313
+ if (cachedInSync && AgentMdManager.checkedAtFresh(checkedAtCached, ttlDays)) {
314
+ return {
315
+ aid: target,
316
+ local_found: true,
317
+ remote_found: true,
318
+ local_etag: localEtag,
319
+ remote_etag: remoteEtagCached,
320
+ in_sync: true,
321
+ needs_update: false,
322
+ last_modified: lastModifiedCached,
323
+ status: 200,
324
+ cached: true,
325
+ verify_status: String(before.verify_status ?? ''),
326
+ verify_error: String(before.verify_error ?? ''),
327
+ ttl_days: Number(ttlDays) || 0,
328
+ };
329
+ }
330
+ const remoteMissingCached = String(before.remote_status ?? '') === 'missing';
331
+ if (!localFound && !remoteEtagCached && remoteMissingCached && AgentMdManager.checkedAtFresh(checkedAtCached, ttlDays)) {
332
+ return {
333
+ aid: target,
334
+ local_found: false,
335
+ remote_found: false,
336
+ local_etag: '',
337
+ remote_etag: '',
338
+ in_sync: false,
339
+ needs_update: false,
340
+ last_modified: '',
341
+ status: 404,
342
+ cached: true,
343
+ verify_status: '',
344
+ verify_error: '',
345
+ ttl_days: Number(ttlDays) || 0,
346
+ };
347
+ }
348
+ let remote;
349
+ try {
350
+ remote = await this._head(target);
351
+ }
352
+ catch (err) {
353
+ if (err instanceof NotFoundError) {
354
+ remote = { aid: target, found: false, etag: '', last_modified: '', content_length: 0, status: 404 };
355
+ }
356
+ else {
357
+ this.saveRecord(target, { checked_at: Date.now(), remote_status: 'error', last_error: err instanceof Error ? err.message : String(err) });
358
+ throw err;
359
+ }
360
+ }
361
+ const remoteFound = !!remote.found;
362
+ const remoteEtag = String(remote.etag ?? '').trim();
363
+ const lastModified = String(remote.last_modified ?? '').trim();
364
+ const saved = this.loadRecord(target) ?? before;
365
+ const inSync = !!(localFound && remoteFound && localEtag && remoteEtag && localEtag === remoteEtag);
366
+ return {
367
+ aid: target,
368
+ local_found: localFound,
369
+ remote_found: remoteFound,
370
+ local_etag: localEtag,
371
+ remote_etag: remoteEtag,
372
+ in_sync: inSync,
373
+ needs_update: !!(remoteFound && !inSync),
374
+ last_modified: lastModified,
375
+ status: remote.status,
376
+ cached: false,
377
+ verify_status: String(saved.verify_status ?? before.verify_status ?? ''),
378
+ verify_error: String(saved.verify_error ?? before.verify_error ?? ''),
379
+ ttl_days: Number(ttlDays) || 0,
380
+ };
381
+ }
382
+ observeMeta(aid, etag = '', lastModified = '', source = '') {
383
+ const target = String(aid ?? '').trim();
384
+ const remoteEtag = String(etag ?? '').trim();
385
+ const remoteLastModified = String(lastModified ?? '').trim();
386
+ if (!target || (!remoteEtag && !remoteLastModified))
387
+ return;
388
+ const before = this.loadRecord(target) ?? {};
389
+ const same = (!remoteEtag || String(before.remote_etag ?? '').trim() === remoteEtag) &&
390
+ (!remoteLastModified || String(before.last_modified ?? '').trim() === remoteLastModified);
391
+ let record = { ...before };
392
+ if (!same || Object.keys(before).length === 0) {
393
+ const fields = { observed_at: Date.now(), remote_status: 'found' };
394
+ if (remoteEtag)
395
+ fields.remote_etag = remoteEtag;
396
+ if (remoteLastModified)
397
+ fields.last_modified = remoteLastModified;
398
+ record = this.saveRecord(target, fields) || record;
399
+ }
400
+ this._scheduleDownloadIfMissing(target, record, source);
401
+ this._log.debug(`agent.md meta observed: aid=${target} etag=${remoteEtag || '-'} last_modified=${remoteLastModified || '-'} source=${source || '-'}`);
402
+ }
403
+ observeRpcMeta(meta, ownerAid) {
404
+ if (!meta || typeof meta !== 'object')
405
+ return;
406
+ const owner = String(ownerAid ?? this._ownerAid() ?? '').trim();
407
+ const etag = String(meta.agent_md_etag ?? '').trim();
408
+ if (etag && owner)
409
+ this.observeMeta(owner, etag, '', 'rpc.self');
410
+ const etags = meta.agent_md_etags;
411
+ if (isJsonObject(etags)) {
412
+ for (const key of ['requester', 'peer', 'receiver', 'target', 'to', 'sender', 'from']) {
413
+ const item = etags[key];
414
+ if (!isJsonObject(item))
415
+ continue;
416
+ const obj = item;
417
+ this.observeMeta(String(obj.aid ?? ''), String(obj.etag ?? ''), String(obj.last_modified ?? obj.lastModified ?? ''), `rpc.${key}`);
418
+ }
419
+ }
420
+ }
421
+ observeEnvelope(envelope) {
422
+ if (!isJsonObject(envelope))
423
+ return;
424
+ const env = envelope;
425
+ if (!isJsonObject(env.agent_md))
426
+ return;
427
+ const agentMd = env.agent_md;
428
+ if (!isJsonObject(agentMd.sender))
429
+ return;
430
+ const sender = agentMd.sender;
431
+ let senderAid = String(sender.aid ?? '').trim();
432
+ if (!senderAid) {
433
+ const aad = isJsonObject(env.aad) ? env.aad : {};
434
+ senderAid = String(aad.from ?? env.from ?? '').trim();
435
+ }
436
+ this.observeMeta(senderAid, String(sender.etag ?? ''), String(sender.last_modified ?? sender.lastModified ?? ''), 'envelope');
437
+ }
438
+ eventSnapshot(aid) {
439
+ const target = String(aid ?? this._ownerAid() ?? '').trim();
440
+ if (!target)
441
+ return null;
442
+ const record = this.loadRecord(target) ?? {};
443
+ const localEtag = String(record.local_etag ?? '').trim();
444
+ const remoteEtag = String(record.remote_etag ?? record.etag ?? '').trim();
445
+ if (!localEtag && !remoteEtag)
446
+ return null;
447
+ return { local_etag: localEtag, remote_etag: remoteEtag };
448
+ }
449
+ static checkedAtFresh(checkedAtMs, ttlDays) {
450
+ const days = Number(ttlDays || 0);
451
+ if (!Number.isFinite(days) || days <= 0)
452
+ return false;
453
+ if (!Number.isFinite(checkedAtMs) || checkedAtMs <= 0)
454
+ return false;
455
+ return Date.now() - checkedAtMs <= days * 86400000;
456
+ }
457
+ _ownerAid() {
458
+ return String(this._ownerAidGetter?.() ?? '').trim();
459
+ }
460
+ _currentAid() {
461
+ return this._currentAidGetter?.() ?? null;
462
+ }
463
+ _url(aid, gatewayUrl) {
464
+ let host = this.safeAid(aid);
465
+ if (this._discoveryPort && !host.includes(':'))
466
+ host = `${host}:${this._discoveryPort}`;
467
+ return `${agentMdSchemeFromGateway(gatewayUrl)}://${host}/agent.md`;
468
+ }
469
+ async _resolveGateway(aid) {
470
+ return String(await this._gatewayResolver?.(aid) ?? '');
471
+ }
472
+ async _resolvePeer(aid) {
473
+ const current = this._currentAid();
474
+ if (current?.aid === aid)
475
+ return current;
476
+ const peer = await this._peerResolver?.(aid);
477
+ if (!(peer instanceof AID)) {
478
+ throw new StateError(`agent.md peer resolver did not return AID for ${aid}`);
479
+ }
480
+ return peer;
481
+ }
482
+ async _accessToken(aid, gatewayUrl) {
483
+ const token = String(await this._accessTokenResolver?.(aid, gatewayUrl) ?? '').trim();
484
+ if (!token)
485
+ throw new StateError('authenticate did not return access_token');
486
+ return token;
487
+ }
488
+ async _head(aid) {
489
+ const target = this.safeAid(aid);
490
+ const gatewayUrl = await this._resolveGateway(target);
491
+ const response = await requestText(this._url(target, gatewayUrl), {
492
+ method: 'HEAD',
493
+ verifySsl: this._verifySsl,
494
+ headers: { Accept: 'text/markdown' },
495
+ timeoutMs: HEAD_HTTP_TIMEOUT_MS,
496
+ });
497
+ if (response.status === 404) {
498
+ this.saveRecord(target, {
499
+ remote_etag: '',
500
+ last_modified: '',
501
+ checked_at: Date.now(),
502
+ remote_status: 'missing',
503
+ last_error: '',
504
+ });
505
+ throw new NotFoundError(`agent.md not found for aid: ${target}`);
506
+ }
507
+ if (response.status < 200 || response.status >= 300) {
508
+ throw new AUNError(`head agent.md failed: HTTP ${response.status}`);
509
+ }
510
+ const contentLength = Number.parseInt(headerValue(response.headers, 'content-length') || '0', 10) || 0;
511
+ const data = {
512
+ aid: target,
513
+ found: true,
514
+ etag: headerValue(response.headers, 'etag'),
515
+ last_modified: headerValue(response.headers, 'last-modified'),
516
+ content_length: contentLength,
517
+ status: response.status,
518
+ };
519
+ this.saveRecord(target, {
520
+ remote_etag: data.etag,
521
+ last_modified: data.last_modified,
522
+ checked_at: Date.now(),
523
+ remote_status: 'found',
524
+ last_error: '',
525
+ });
526
+ return data;
527
+ }
528
+ async _downloadOnce(target, timeoutMs) {
529
+ this._log.debug(`downloadAgentMd enter: aid=${target}`);
530
+ const gatewayUrl = await this._resolveGateway(target);
531
+ const cached = this.loadRecord(target) ?? {};
532
+ const headers = { Accept: 'text/markdown' };
533
+ const etag = String(cached.remote_etag ?? cached.etag ?? cached.local_etag ?? '').trim();
534
+ const lastModified = String(cached.last_modified ?? '').trim();
535
+ let response = await requestText(this._url(target, gatewayUrl), {
536
+ method: 'GET',
537
+ verifySsl: this._verifySsl,
538
+ headers,
539
+ timeoutMs,
540
+ });
541
+ let reusedCachedNotModified = false;
542
+ if (response.status === 304 && cached.content !== undefined && cached.content !== null) {
543
+ reusedCachedNotModified = true;
544
+ response = {
545
+ ...response,
546
+ text: String(cached.content),
547
+ headers: {
548
+ ...response.headers,
549
+ ...(etag ? { ETag: etag } : {}),
550
+ ...(lastModified ? { 'Last-Modified': lastModified } : {}),
551
+ },
552
+ };
553
+ }
554
+ else if (response.status === 304) {
555
+ response = await requestText(this._url(target, gatewayUrl), {
556
+ method: 'GET',
557
+ verifySsl: this._verifySsl,
558
+ headers: { Accept: 'text/markdown' },
559
+ timeoutMs,
560
+ });
561
+ }
562
+ if (response.status === 404) {
563
+ this.saveRecord(target, { remote_status: 'missing', checked_at: Date.now(), last_error: '' });
564
+ throw new NotFoundError(`agent.md not found for aid: ${target}`);
565
+ }
566
+ if ((response.status < 200 || response.status >= 300) && !reusedCachedNotModified) {
567
+ const message = response.text.trim();
568
+ throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
569
+ }
570
+ const content = response.text;
571
+ const peer = await this._resolvePeer(target);
572
+ const verified = peer.verifyAgentMd(content);
573
+ if (!verified.ok || !verified.data) {
574
+ const message = verified.error?.message ?? 'agent.md verification failed';
575
+ throw new AUNError(message);
576
+ }
577
+ const signature = { ...verified.data };
578
+ const statusText = String(signature.status ?? 'invalid');
579
+ const reason = String(signature.reason ?? '').trim();
580
+ const verification = { status: statusText };
581
+ if (reason)
582
+ verification.reason = reason;
583
+ const responseEtag = headerValue(response.headers, 'etag');
584
+ const responseLastModified = headerValue(response.headers, 'last-modified');
585
+ const localEtag = AgentMdManager.contentEtag(content);
586
+ const saved = this.saveRecord(target, {
587
+ content,
588
+ local_etag: localEtag,
589
+ remote_etag: responseEtag,
590
+ last_modified: responseLastModified,
591
+ fetched_at: Date.now(),
592
+ checked_at: Date.now(),
593
+ remote_status: 'found',
594
+ verify_status: statusText,
595
+ verify_error: reason,
596
+ last_error: '',
597
+ });
598
+ const owner = this._ownerAid();
599
+ let inSync = null;
600
+ if (target === owner) {
601
+ const remote = responseEtag || String(saved.remote_etag ?? '');
602
+ inSync = localEtag && remote ? localEtag === remote : false;
603
+ }
604
+ this._log.debug(`downloadAgentMd exit: aid=${target} status=${statusText}`);
605
+ return {
606
+ aid: target,
607
+ content,
608
+ verification,
609
+ signature,
610
+ cert_pem: peer.certPem,
611
+ etag: responseEtag,
612
+ last_modified: responseLastModified,
613
+ status: response.status,
614
+ in_sync: inSync,
615
+ saved_to: String(saved.saved_to ?? this.filePath(target)),
616
+ save_error: null,
617
+ };
618
+ }
619
+ _hasLocalContent(aid, record) {
620
+ if (record && typeof record.content === 'string' && record.content.length > 0)
621
+ return true;
622
+ try {
623
+ return fs.existsSync(this.filePath(aid));
624
+ }
625
+ catch {
626
+ return false;
627
+ }
628
+ }
629
+ _scheduleDownloadIfMissing(aid, record, source = '') {
630
+ const target = String(aid ?? '').trim();
631
+ if (!target || this._hasLocalContent(target, record))
632
+ return;
633
+ if (this._downloadInflight.has(target))
634
+ return;
635
+ void this.download(target).catch((err) => {
636
+ this.saveRecord(target, {
637
+ last_error: err instanceof Error ? err.message : String(err),
638
+ remote_status: 'found',
639
+ });
640
+ this._log.debug(`agent.md auto download failed: aid=${target} source=${source || '-'} err=${err instanceof Error ? err.message : String(err)}`);
641
+ });
642
+ }
643
+ async _acquireDownloadSlot() {
644
+ if (this._downloadActive < DOWNLOAD_CONCURRENCY) {
645
+ this._downloadActive += 1;
646
+ return () => this._releaseDownloadSlot();
647
+ }
648
+ await new Promise((resolve) => {
649
+ this._downloadWaiters.push(resolve);
650
+ });
651
+ return () => this._releaseDownloadSlot();
652
+ }
653
+ _releaseDownloadSlot() {
654
+ const next = this._downloadWaiters.shift();
655
+ if (next) {
656
+ next();
657
+ return;
658
+ }
659
+ if (this._downloadActive > 0)
660
+ this._downloadActive -= 1;
661
+ }
662
+ _atomicWriteText(filePath, content) {
663
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
664
+ const tmp = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
665
+ let fd = null;
666
+ try {
667
+ fd = fs.openSync(tmp, 'w');
668
+ fs.writeFileSync(fd, content, 'utf-8');
669
+ fs.fsyncSync(fd);
670
+ fs.closeSync(fd);
671
+ fd = null;
672
+ fs.renameSync(tmp, filePath);
673
+ try {
674
+ const dirFd = fs.openSync(path.dirname(filePath), 'r');
675
+ try {
676
+ fs.fsyncSync(dirFd);
677
+ }
678
+ finally {
679
+ fs.closeSync(dirFd);
680
+ }
681
+ }
682
+ catch {
683
+ // best-effort fsync
684
+ }
685
+ }
686
+ finally {
687
+ if (fd !== null) {
688
+ try {
689
+ fs.closeSync(fd);
690
+ }
691
+ catch { /* ignore */ }
692
+ }
693
+ if (fs.existsSync(tmp)) {
694
+ try {
695
+ fs.unlinkSync(tmp);
696
+ }
697
+ catch { /* ignore */ }
698
+ }
699
+ }
700
+ }
701
+ _sleepSync(ms) {
702
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
703
+ }
704
+ _withRecordLock(aid, fn) {
705
+ const lockPath = path.join(path.dirname(this.metaPath(aid)), 'agentmd.json.lock');
706
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
707
+ const deadline = Date.now() + 5000;
708
+ let fd = null;
709
+ while (fd === null) {
710
+ try {
711
+ fd = fs.openSync(lockPath, 'wx');
712
+ fs.writeFileSync(fd, `${process.pid}\n`, 'utf-8');
713
+ }
714
+ catch (err) {
715
+ if (err?.code !== 'EEXIST' || Date.now() >= deadline)
716
+ throw err;
717
+ try {
718
+ const st = fs.statSync(lockPath);
719
+ if (Date.now() - st.mtimeMs > 30000)
720
+ fs.unlinkSync(lockPath);
721
+ }
722
+ catch {
723
+ // ignore stale lock cleanup failures
724
+ }
725
+ this._sleepSync(25);
726
+ }
727
+ }
728
+ try {
729
+ return fn();
730
+ }
731
+ finally {
732
+ if (fd !== null) {
733
+ try {
734
+ fs.closeSync(fd);
735
+ }
736
+ catch { /* ignore */ }
737
+ }
738
+ try {
739
+ fs.unlinkSync(lockPath);
740
+ }
741
+ catch { /* ignore */ }
742
+ }
743
+ }
744
+ _writeRecordUnlocked(aid, record) {
745
+ const payload = {};
746
+ for (const [key, value] of Object.entries(record)) {
747
+ if (key !== 'content' && value !== undefined && value !== null)
748
+ payload[key] = value;
749
+ }
750
+ payload.aid = this.safeAid(aid);
751
+ this._atomicWriteText(this.metaPath(aid), `${JSON.stringify(payload, null, 2)}\n`);
752
+ }
753
+ _readRecordUnlocked(aid) {
754
+ const filePath = this.metaPath(aid);
755
+ if (!fs.existsSync(filePath))
756
+ return {};
757
+ try {
758
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
759
+ if (!isJsonObject(parsed))
760
+ return {};
761
+ const record = {};
762
+ for (const [key, value] of Object.entries(parsed)) {
763
+ if (key !== 'content')
764
+ record[key] = value;
765
+ }
766
+ record.aid = this.safeAid(String(record.aid ?? aid));
767
+ for (const key of ['fetched_at', 'observed_at', 'checked_at', 'updated_at']) {
768
+ record[key] = Number(record[key] ?? 0) || 0;
769
+ }
770
+ return record;
771
+ }
772
+ catch (err) {
773
+ this._log.warn(`agent.md metadata damaged, ignoring: aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
774
+ return {};
775
+ }
776
+ }
777
+ }
778
+ //# sourceMappingURL=agent-md.js.map