@agentunion/fastaun 0.5.18 → 0.5.19
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/CHANGELOG.md +44 -2
- package/_packed_docs/CHANGELOG.md +44 -2
- package/_packed_docs/INDEX.md +136 -14
- package/_packed_docs/KITE_DOCS_GUIDE.md +42 -16
- package/_packed_docs/agent.md/SCHEMA.md +245 -205
- package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +42 -15
- package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +4 -4
- package/_packed_docs/protocol/01-/350/272/253/344/273/275/344/270/216/345/207/255/350/257/201/345/215/217/350/256/256-auth.md +45 -134
- package/_packed_docs/protocol/07-/351/224/231/350/257/257/347/240/201/344/270/216/347/212/266/346/200/201/346/234/272.md +5 -20
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +28 -9
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +22 -27
- package/_packed_docs/protocol/agent.md/SCHEMA.md +4 -0
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +5 -2
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +250 -283
- package/_packed_docs/sdk/09-group-rpc-manual.md +46 -14
- package/_packed_docs/sdk/09-message-rpc-manual.md +40 -2
- package/_packed_docs/sdk/09-storage-rpc-manual.md +74 -64
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +25 -5
- package/_packed_docs/sdk/INDEX.md +76 -17
- package/dist/agent-md-schema.d.ts +2 -0
- package/dist/agent-md-schema.js +45 -10
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +47 -2
- package/dist/agent-md.js +493 -33
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +9 -59
- package/dist/aid-store.js +135 -245
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.js +19 -5
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts +6 -1
- package/dist/auth.js +428 -15
- package/dist/auth.js.map +1 -1
- package/dist/cert-utils.d.ts +1 -1
- package/dist/cert-utils.js +19 -7
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +2 -1
- package/dist/client/delivery.js +143 -52
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.js +22 -5
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +1 -0
- package/dist/client/v2-e2ee.js +93 -29
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +84 -1
- package/dist/client.js +749 -28
- package/dist/client.js.map +1 -1
- package/dist/errors.js +4 -3
- package/dist/errors.js.map +1 -1
- package/dist/events.d.ts +5 -1
- package/dist/events.js +47 -6
- package/dist/events.js.map +1 -1
- package/dist/facades.d.ts +83 -3
- package/dist/facades.js +98 -4
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/keystore/aid-db.d.ts +5 -0
- package/dist/keystore/aid-db.js +48 -0
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/index.d.ts +8 -0
- package/dist/keystore/local-identity-store.js +10 -3
- package/dist/keystore/local-identity-store.js.map +1 -1
- package/dist/keystore/local-token-store.d.ts +6 -1
- package/dist/keystore/local-token-store.js +24 -1
- package/dist/keystore/local-token-store.js.map +1 -1
- package/dist/org-protocol.d.ts +98 -0
- package/dist/org-protocol.js +1689 -0
- package/dist/org-protocol.js.map +1 -0
- package/dist/ownership-proof.d.ts +82 -5
- package/dist/ownership-proof.js +327 -31
- package/dist/ownership-proof.js.map +1 -1
- package/dist/ownership-v21.d.ts +178 -0
- package/dist/ownership-v21.js +635 -0
- package/dist/ownership-v21.js.map +1 -0
- package/dist/ownership-workflow.d.ts +82 -0
- package/dist/ownership-workflow.js +872 -0
- package/dist/ownership-workflow.js.map +1 -0
- package/dist/seq-tracker.d.ts +1 -0
- package/dist/seq-tracker.js +10 -1
- package/dist/seq-tracker.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +89 -1
- package/dist/tools/cross-sdk-agent.js +338 -7
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +7 -0
- package/dist/transport.js +108 -4
- package/dist/transport.js.map +1 -1
- package/dist/v2/e2ee/decrypt.d.ts +1 -0
- package/dist/v2/e2ee/decrypt.js +9 -1
- package/dist/v2/e2ee/decrypt.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/agent-md.js
CHANGED
|
@@ -3,13 +3,44 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as http from 'node:http';
|
|
4
4
|
import * as https from 'node:https';
|
|
5
5
|
import * as path from 'node:path';
|
|
6
|
+
import { parseDocument } from 'yaml';
|
|
6
7
|
import { AID } from './aid.js';
|
|
7
8
|
import { certMatchesFingerprint, parseAgentMdTailSignature } from './cert-utils.js';
|
|
8
|
-
import { validateAgentMdDocument, verifyAgentMdUpload } from './agent-md-schema.js';
|
|
9
|
+
import { parseFileFrontmatter, validateAgentMdDocument, verifyAgentMdUpload } from './agent-md-schema.js';
|
|
10
|
+
import { validateOwnershipFrontmatter, verifyOwnershipFrontmatterStatus } from './ownership-proof.js';
|
|
9
11
|
import { AUNError, ClientSignatureError, NotFoundError, StateError, ValidationError, } from './errors.js';
|
|
10
12
|
import { isJsonObject } from './types.js';
|
|
13
|
+
import { ownershipFailure, parseOwnershipFile } from './ownership-v21.js';
|
|
11
14
|
const DEFAULT_HTTP_TIMEOUT_MS = 30_000;
|
|
12
15
|
const AGENT_MD_NEGATIVE_CACHE_TTL_MS = 60_000;
|
|
16
|
+
export function buildOwnershipReleaseAgentMd(content, release) {
|
|
17
|
+
return updateOwnershipAgentMd(content, { ownership_release: release });
|
|
18
|
+
}
|
|
19
|
+
export function buildOwnershipAgentMd(content, owner, proof) {
|
|
20
|
+
return updateOwnershipAgentMd(content, { owner, ownership_proof: proof });
|
|
21
|
+
}
|
|
22
|
+
export function updateOwnershipAgentMd(content, fields) {
|
|
23
|
+
const payload = parseAgentMdTailSignature(String(content ?? '')).payload;
|
|
24
|
+
const match = /^---(?:\r\n|\n|\r)([\s\S]*?)(?:\r\n|\n|\r)---([\s\S]*)$/.exec(payload);
|
|
25
|
+
if (!match)
|
|
26
|
+
throw new ValidationError('agent.md must start with YAML frontmatter');
|
|
27
|
+
const document = parseDocument(match[1], { uniqueKeys: true });
|
|
28
|
+
if (document.errors.length)
|
|
29
|
+
throw new ValidationError(`agent.md frontmatter is invalid: ${document.errors[0].message}`);
|
|
30
|
+
const frontmatter = document.toJS();
|
|
31
|
+
if (!frontmatter || typeof frontmatter !== 'object' || Array.isArray(frontmatter))
|
|
32
|
+
throw new ValidationError('agent.md frontmatter must be a mapping');
|
|
33
|
+
document.delete('owner');
|
|
34
|
+
document.delete('ownership');
|
|
35
|
+
document.delete('ownership_proof');
|
|
36
|
+
document.delete('ownership_release');
|
|
37
|
+
for (const [name, value] of Object.entries(fields))
|
|
38
|
+
document.set(name, value);
|
|
39
|
+
const yamlText = document.toString().replace(/\r\n/g, '\n');
|
|
40
|
+
const updated = `---\n${yamlText.endsWith('\n') ? yamlText : `${yamlText}\n`}---${match[2]}`;
|
|
41
|
+
validateAgentMdDocument(updated, { expectedAid: String(frontmatter.aid ?? '') });
|
|
42
|
+
return updated;
|
|
43
|
+
}
|
|
13
44
|
export function buildDefaultAgentMd(aid, options = {}) {
|
|
14
45
|
const target = String(aid ?? '').trim();
|
|
15
46
|
const type = String(options.type ?? 'agent').trim() || 'agent';
|
|
@@ -22,6 +53,10 @@ export function buildDefaultAgentMd(aid, options = {}) {
|
|
|
22
53
|
`aid: ${JSON.stringify(target)}`,
|
|
23
54
|
`name: ${JSON.stringify(name)}`,
|
|
24
55
|
`type: ${JSON.stringify(type)}`,
|
|
56
|
+
...(options.exttype ? [`exttype: ${JSON.stringify(options.exttype)}`] : []),
|
|
57
|
+
...(options.owner ? [`owner: ${JSON.stringify(options.owner)}`] : []),
|
|
58
|
+
...(options.entityCertFingerprint ? [`entity_cert_fingerprint: ${JSON.stringify(options.entityCertFingerprint)}`] : []),
|
|
59
|
+
...(options.ownershipProof ? [`ownership_proof: ${JSON.stringify(options.ownershipProof)}`] : []),
|
|
25
60
|
'version: "1.0.0"',
|
|
26
61
|
`description: ${JSON.stringify(description)}`,
|
|
27
62
|
...(avatar ? [`avatar: ${JSON.stringify(avatar)}`] : []),
|
|
@@ -122,11 +157,13 @@ function requestText(url, opts) {
|
|
|
122
157
|
const chunks = [];
|
|
123
158
|
res.on('data', (chunk) => chunks.push(chunk));
|
|
124
159
|
res.on('end', () => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
headers: toHeaders(res.headers),
|
|
128
|
-
|
|
129
|
-
|
|
160
|
+
try {
|
|
161
|
+
const bytes = Buffer.concat(chunks);
|
|
162
|
+
resolve({ status, headers: toHeaders(res.headers), text: opts.strictUtf8 ? new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes) : bytes.toString('utf-8') });
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
reject(ownershipFailure(status >= 200 && status < 300 ? 'invalid' : 'unavailable', 'agent.md response is not valid UTF-8', status < 200 || status >= 300));
|
|
166
|
+
}
|
|
130
167
|
});
|
|
131
168
|
res.on('error', reject);
|
|
132
169
|
});
|
|
@@ -148,10 +185,14 @@ export class AgentMdManager {
|
|
|
148
185
|
_discoveryPort;
|
|
149
186
|
_uploadViaGatewayHost;
|
|
150
187
|
_ownerAidGetter;
|
|
188
|
+
_entityIdentityGetter;
|
|
189
|
+
_entityOwnerResolver;
|
|
190
|
+
_entitySignerResolver;
|
|
151
191
|
_currentAidGetter;
|
|
152
192
|
_gatewayResolver;
|
|
153
193
|
_peerResolver;
|
|
154
194
|
_accessTokenResolver;
|
|
195
|
+
_uploadAccessTokenResolver;
|
|
155
196
|
_aidValidator;
|
|
156
197
|
_cache = new Map();
|
|
157
198
|
_downloadInflight = new Map();
|
|
@@ -165,10 +206,14 @@ export class AgentMdManager {
|
|
|
165
206
|
this._discoveryPort = opts.discoveryPort ?? null;
|
|
166
207
|
this._uploadViaGatewayHost = opts.uploadViaGatewayHost ?? false;
|
|
167
208
|
this._ownerAidGetter = opts.ownerAidGetter;
|
|
209
|
+
this._entityIdentityGetter = opts.entityIdentityGetter;
|
|
210
|
+
this._entityOwnerResolver = opts.entityOwnerResolver;
|
|
211
|
+
this._entitySignerResolver = opts.entitySignerResolver;
|
|
168
212
|
this._currentAidGetter = opts.currentAidGetter;
|
|
169
213
|
this._gatewayResolver = opts.gatewayResolver;
|
|
170
214
|
this._peerResolver = opts.peerResolver;
|
|
171
215
|
this._accessTokenResolver = opts.accessTokenResolver;
|
|
216
|
+
this._uploadAccessTokenResolver = opts.uploadAccessTokenResolver;
|
|
172
217
|
this._aidValidator = opts.aidValidator;
|
|
173
218
|
}
|
|
174
219
|
static contentEtag(content) {
|
|
@@ -286,7 +331,7 @@ export class AgentMdManager {
|
|
|
286
331
|
return {};
|
|
287
332
|
}
|
|
288
333
|
}
|
|
289
|
-
async upload(content
|
|
334
|
+
async upload(content) {
|
|
290
335
|
const target = this._ownerAid();
|
|
291
336
|
if (!target)
|
|
292
337
|
throw new ValidationError('uploadAgentMd requires local AID');
|
|
@@ -297,7 +342,9 @@ export class AgentMdManager {
|
|
|
297
342
|
const rawContent = content === undefined || content === null ? this.readContent(target) : String(content);
|
|
298
343
|
if (!rawContent.trim())
|
|
299
344
|
throw new ValidationError('uploadAgentMd requires non-empty content');
|
|
300
|
-
validateAgentMdDocument(rawContent, { expectedAid: target });
|
|
345
|
+
const parsed = validateAgentMdDocument(rawContent, { expectedAid: target });
|
|
346
|
+
if (['group', 'org'].includes(String(parsed.frontmatter.type)))
|
|
347
|
+
return await this.uploadAs(target, rawContent, current);
|
|
301
348
|
const signed = current.signAgentMd(rawContent);
|
|
302
349
|
if (!signed.ok || !signed.data) {
|
|
303
350
|
const message = signed.error?.message ?? 'agent.md signing failed';
|
|
@@ -305,15 +352,387 @@ export class AgentMdManager {
|
|
|
305
352
|
}
|
|
306
353
|
const signedContent = signed.data.signed;
|
|
307
354
|
verifyAgentMdUpload(signedContent, current.certPem, target);
|
|
355
|
+
return await this._uploadPrepared(target, signedContent, current);
|
|
356
|
+
}
|
|
357
|
+
async readOwnershipFresh(aid) {
|
|
358
|
+
const target = this.safeAid(aid);
|
|
359
|
+
const response = await this._ownershipRequest(target, 'GET');
|
|
360
|
+
if (response.status !== 200 && response.status !== 404)
|
|
361
|
+
throw ownershipFailure('unavailable', `NS fresh read failed: HTTP ${response.status}`, true);
|
|
362
|
+
if (headerValue(response.headers, 'X-AUN-Ownership-Status') === 'not_applicable')
|
|
363
|
+
throw ownershipFailure('not_applicable', 'group/org uses its lifecycle owner relationship');
|
|
364
|
+
let type;
|
|
365
|
+
if (response.status === 200) {
|
|
366
|
+
try {
|
|
367
|
+
type = parseFileFrontmatter(parseAgentMdTailSignature(response.text).payload).type;
|
|
368
|
+
}
|
|
369
|
+
catch { }
|
|
370
|
+
}
|
|
371
|
+
if (['group', 'org'].includes(String(type)))
|
|
372
|
+
throw ownershipFailure('not_applicable', 'group/org uses its lifecycle owner relationship');
|
|
373
|
+
const protocol = headerValue(response.headers, 'X-AUN-Ownership-Protocol');
|
|
374
|
+
if (protocol !== '2.1')
|
|
375
|
+
throw ownershipFailure('unsupported_protocol', 'NS does not provide Ownership 2.1 metadata');
|
|
376
|
+
const version = headerValue(response.headers, 'X-AUN-Ownership-Version');
|
|
377
|
+
const lock = headerValue(response.headers, 'X-AUN-Ownership-Lock');
|
|
378
|
+
const etag = headerValue(response.headers, 'ETag');
|
|
379
|
+
if (!/^(?:0|[1-9][0-9]*)$/.test(version) || !Number.isSafeInteger(Number(version)) || !['locked', 'unlocked'].includes(lock) || response.status === 200 && !etag || response.status === 404 && (version !== '0' || lock !== 'unlocked'))
|
|
380
|
+
throw ownershipFailure('unavailable', 'NS fresh metadata is incomplete', true);
|
|
381
|
+
return { sub_aid: target, exists: response.status === 200, content: response.status === 200 ? response.text : '', etag, protocol, relation_version: Number(version), lock: lock, observed_at: Math.floor(Date.now() / 1000) };
|
|
382
|
+
}
|
|
383
|
+
async submitOwnershipRaw(aid, raw, condition) {
|
|
384
|
+
const target = this.safeAid(aid), current = this._currentAid();
|
|
385
|
+
if (!current?.isPrivateKeyValid() || current.aid !== target || this._ownerAid() !== target)
|
|
386
|
+
throw ownershipFailure('forbidden', 'raw agent.md submission requires its own authenticated identity');
|
|
387
|
+
if (!condition || !condition.if_match && condition.if_none_match !== '*' || condition.if_match && condition.if_none_match)
|
|
388
|
+
throw new ValidationError('raw agent.md requires one original condition');
|
|
389
|
+
parseOwnershipFile(raw, 'agent', { expectedSubAid: target });
|
|
390
|
+
verifyAgentMdUpload(raw, current.certPem, target);
|
|
391
|
+
const response = await this._ownershipRequest(target, 'PUT', raw, condition);
|
|
392
|
+
if (response.status === 409 || response.status === 412)
|
|
393
|
+
throw ownershipFailure('conflict', `NS original condition failed: HTTP ${response.status}`);
|
|
394
|
+
if (response.status === 428)
|
|
395
|
+
throw ownershipFailure('unsupported_protocol', 'NS does not support the required original document condition');
|
|
396
|
+
if (response.status === 401 || response.status === 403)
|
|
397
|
+
throw ownershipFailure('forbidden', `NS self submission denied: HTTP ${response.status}`);
|
|
398
|
+
if (response.status < 200 || response.status >= 300)
|
|
399
|
+
throw ownershipFailure('unavailable', `NS raw submission failed: HTTP ${response.status}`, true);
|
|
400
|
+
const fresh = await this.readOwnershipFresh(target);
|
|
401
|
+
if (fresh.content !== raw)
|
|
402
|
+
throw ownershipFailure('unavailable', 'NS raw submission cannot be confirmed from the current document', true);
|
|
403
|
+
this.saveRecord(target, { content: raw, local_etag: AgentMdManager.contentEtag(raw), remote_etag: fresh.etag, fetched_at: Date.now(), checked_at: Date.now(), remote_status: 'found' });
|
|
404
|
+
return fresh;
|
|
405
|
+
}
|
|
406
|
+
async _ownershipRequest(target, method, raw, condition) {
|
|
308
407
|
const gatewayUrl = await this._resolveGateway(target);
|
|
309
|
-
const
|
|
310
|
-
|
|
408
|
+
const headers = { Accept: 'text/markdown', 'Cache-Control': 'no-cache', 'X-AUN-Ownership-Check': '1' };
|
|
409
|
+
if (method === 'PUT') {
|
|
410
|
+
headers['Content-Type'] = 'text/markdown; charset=utf-8';
|
|
411
|
+
headers.Authorization = `Bearer ${await this._uploadAccessToken(target, gatewayUrl)}`;
|
|
412
|
+
if (condition?.if_match)
|
|
413
|
+
headers['If-Match'] = condition.if_match;
|
|
414
|
+
else if (condition?.if_none_match)
|
|
415
|
+
headers['If-None-Match'] = condition.if_none_match;
|
|
416
|
+
}
|
|
417
|
+
const viaGateway = this._uploadViaGatewayHost;
|
|
418
|
+
if (viaGateway)
|
|
419
|
+
headers.Host = target;
|
|
420
|
+
const request = (url) => requestText(url, { method, verifySsl: this._verifySsl, headers, body: raw, timeoutMs: DEFAULT_HTTP_TIMEOUT_MS, redirectsLeft: 0, strictUtf8: true });
|
|
421
|
+
let response = await request(this._url(target, gatewayUrl, viaGateway));
|
|
422
|
+
if (response.status === 404 && !headerValue(response.headers, 'X-AUN-Ownership-Protocol') && headerValue(response.headers, 'X-AUN-Ownership-Status') !== 'not_applicable') {
|
|
423
|
+
const fallback = nameServiceAgentMdUrl(target, gatewayUrl);
|
|
424
|
+
if (fallback && fallback !== this._url(target, gatewayUrl, viaGateway)) {
|
|
425
|
+
headers.Host = target;
|
|
426
|
+
response = await request(fallback);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return response;
|
|
430
|
+
}
|
|
431
|
+
async uploadAs(targetAid, content, signer) {
|
|
432
|
+
const target = this.safeAid(String(targetAid ?? '').trim());
|
|
433
|
+
const rawContent = String(content ?? '');
|
|
434
|
+
if (!rawContent.trim())
|
|
435
|
+
throw new ValidationError('uploadAgentMd requires non-empty content');
|
|
436
|
+
const parsed = validateAgentMdDocument(rawContent, { expectedAid: target });
|
|
437
|
+
if (['group', 'org'].includes(String(parsed.frontmatter.type)) && parsed.signature) {
|
|
438
|
+
const identity = await this._entitySigningIdentity(target, parsed.frontmatter, parsed.signature, signer);
|
|
439
|
+
verifyAgentMdUpload(rawContent, identity.certPem, target);
|
|
440
|
+
return await this._uploadPrepared(target, rawContent, identity);
|
|
441
|
+
}
|
|
442
|
+
if (!signer?.isPrivateKeyValid())
|
|
443
|
+
throw new StateError('uploadAgentMd requires a valid signer identity');
|
|
444
|
+
if (['group', 'org'].includes(String(parsed.frontmatter.type)))
|
|
445
|
+
await this._entityContext(target, parsed.frontmatter, signer);
|
|
446
|
+
const signed = signer.signAgentMd(rawContent);
|
|
447
|
+
if (!signed.ok || !signed.data) {
|
|
448
|
+
const message = signed.error?.message ?? 'agent.md signing failed';
|
|
449
|
+
throw new ClientSignatureError(message);
|
|
450
|
+
}
|
|
451
|
+
const signedContent = signed.data.signed;
|
|
452
|
+
verifyAgentMdUpload(signedContent, signer.certPem, target);
|
|
453
|
+
return await this._uploadPrepared(target, signedContent, signer);
|
|
454
|
+
}
|
|
455
|
+
pendingUpload(aid) {
|
|
456
|
+
const record = this.loadRecord(aid);
|
|
457
|
+
if (record?.pending_operation !== 'replace' || !String(record.pending_content ?? '').trim())
|
|
458
|
+
return null;
|
|
459
|
+
return { content: String(record.pending_content), content_etag: String(record.pending_content_etag ?? ''), last_error: String(record.pending_error ?? ''), condition: record.pending_condition, relation: record.pending_relation, authorization: record.pending_authorization };
|
|
460
|
+
}
|
|
461
|
+
async resumePendingUpload(aid, signer) {
|
|
462
|
+
const pending = this.pendingUpload(aid);
|
|
463
|
+
if (!pending)
|
|
464
|
+
return false;
|
|
465
|
+
const condition = pending.condition;
|
|
466
|
+
if (!condition || Object.keys(condition).length !== 1 || !(typeof condition.if_match === 'string' && condition.if_match || condition.if_none_match === '*'))
|
|
467
|
+
throw ownershipFailure('persistence_unavailable', 'pending agent.md requires its original document condition');
|
|
468
|
+
const parsed = validateAgentMdDocument(pending.content, { expectedAid: aid, validateOwnership: false });
|
|
469
|
+
const meta = parsed.frontmatter;
|
|
470
|
+
if ('ownership_proof' in meta || 'ownership_release' in meta)
|
|
471
|
+
throw ownershipFailure('unsupported_protocol', 'legacy ownership pending agent.md cannot be recovered through ordinary upload');
|
|
472
|
+
try {
|
|
473
|
+
if (['group', 'org'].includes(String(meta.type))) {
|
|
474
|
+
if (!parsed.signature)
|
|
475
|
+
throw ownershipFailure('invalid', 'pending entity agent.md requires a signature');
|
|
476
|
+
const identity = await this._entitySigningIdentity(aid, meta, parsed.signature, signer ?? this._currentAid() ?? undefined);
|
|
477
|
+
const context = await this._entityContext(aid, meta, identity);
|
|
478
|
+
if (!pending.relation?.entity_cert_fingerprint)
|
|
479
|
+
throw ownershipFailure('persistence_unavailable', 'pending entity upload requires original owner and certificate context');
|
|
480
|
+
if (pending.relation.owner_aid !== context.owner_aid)
|
|
481
|
+
throw ownershipFailure('forbidden', 'pending entity upload belongs to a different owner');
|
|
482
|
+
if (pending.relation.entity_cert_fingerprint !== context.entity_cert_fingerprint)
|
|
483
|
+
throw ownershipFailure('conflict', 'pending entity upload belongs to an old rekey generation');
|
|
484
|
+
if (pending.content_etag !== AgentMdManager.contentEtag(pending.content))
|
|
485
|
+
throw ownershipFailure('persistence_unavailable', 'pending entity original bytes are corrupt');
|
|
486
|
+
verifyAgentMdUpload(pending.content, identity.certPem, aid);
|
|
487
|
+
const uploader = this._currentAid();
|
|
488
|
+
if (uploader.aid.toLowerCase() !== aid.toLowerCase())
|
|
489
|
+
this._validateEntityUploadAuthorization(pending.authorization, aid, pending.content, uploader, String(context.entity_cert_fingerprint));
|
|
490
|
+
const remote = await this._readEntityFresh(aid);
|
|
491
|
+
if (remote.exists && remote.content === pending.content && remote.etag === pending.content_etag) {
|
|
492
|
+
this.saveRecord(aid, { content: pending.content, remote_etag: remote.etag, pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
|
|
496
|
+
throw ownershipFailure('conflict', 'pending agent.md original document condition changed');
|
|
497
|
+
await this._uploadPrepared(aid, pending.content, identity, condition);
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
const remote = await this.readOwnershipFresh(aid);
|
|
501
|
+
const fail = (code, reason, retryable = false) => ownershipFailure(code, reason, retryable);
|
|
502
|
+
const candidate = parseOwnershipFile(pending.content, 'agent', { expectedSubAid: aid });
|
|
503
|
+
const relation = pending.relation && Object.keys(pending.relation).length ? pending.relation : undefined;
|
|
504
|
+
const sameStatement = (first, second) => first == null && second == null || first !== null && second !== null && typeof first === 'object' && typeof second === 'object' && ['version', 'declaration_id', 'owner_aid', 'sub_aid', 'relation_version', 'declared_at'].every((field) => first[field] === second[field]);
|
|
505
|
+
if (candidate.statement && !relation)
|
|
506
|
+
throw fail('persistence_unavailable', 'pending ownership body update requires its original relationship context');
|
|
507
|
+
if (relation) {
|
|
508
|
+
if (remote.relation_version !== relation.relation_version || remote.lock !== relation.lock)
|
|
509
|
+
throw fail('conflict', 'pending agent.md original relationship changed');
|
|
510
|
+
if (!sameStatement(candidate.statement, relation.statement))
|
|
511
|
+
throw fail('unsupported_protocol', 'ownership changes require their persisted 2.1 task');
|
|
512
|
+
if (remote.exists && (remote.relation_version > 0 || candidate.statement)) {
|
|
513
|
+
const current = parseOwnershipFile(remote.content, 'agent', { expectedSubAid: aid });
|
|
514
|
+
if (!sameStatement(current.statement, relation.statement) || (remote.owner_aid ?? current.statement?.owner_aid ?? null) !== relation.owner_aid)
|
|
515
|
+
throw fail('conflict', 'pending agent.md relationship statement changed');
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (!relation && (remote.relation_version !== 0 || remote.lock !== 'unlocked'))
|
|
519
|
+
throw fail('unavailable', 'pending agent.md cannot overwrite relationship history', true);
|
|
520
|
+
if (remote.exists && remote.etag === pending.content_etag && remote.content === pending.content) {
|
|
521
|
+
const saved = this.saveRecord(aid, { content: pending.content, remote_etag: remote.etag, pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
|
|
522
|
+
if (!saved.aid)
|
|
523
|
+
throw new StateError('agent.md pending completion persistence failed');
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
|
|
527
|
+
throw ownershipFailure('conflict', 'pending agent.md original document condition changed');
|
|
528
|
+
const identity = signer ?? this._currentAid();
|
|
529
|
+
if (!identity)
|
|
530
|
+
throw new StateError('pending agent.md requires a signer identity');
|
|
531
|
+
verifyAgentMdUpload(pending.content, identity.certPem, aid);
|
|
532
|
+
await this._uploadPrepared(aid, pending.content, identity, condition);
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
this.saveRecord(aid, { pending_error: error instanceof Error ? error.message : String(error) });
|
|
537
|
+
throw error;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async _uploadPrepared(target, signedContent, signer, originalCondition) {
|
|
541
|
+
const meta = validateAgentMdDocument(signedContent, { expectedAid: target }).frontmatter;
|
|
542
|
+
const entity = ['group', 'org'].includes(String(meta.type));
|
|
543
|
+
const entityContext = entity ? await this._entityContext(target, meta, signer) : undefined;
|
|
544
|
+
let condition = originalCondition;
|
|
545
|
+
let relation = entityContext ?? (this.pendingUpload(target))?.relation;
|
|
546
|
+
if (!condition) {
|
|
547
|
+
const fresh = entity ? await this._readEntityFresh(target) : await this.readOwnershipFresh(target);
|
|
548
|
+
condition = fresh.exists ? { if_match: fresh.etag } : { if_none_match: '*' };
|
|
549
|
+
if (!entity) {
|
|
550
|
+
const authority = fresh;
|
|
551
|
+
const candidate = parseOwnershipFile(signedContent, 'agent', { expectedSubAid: target });
|
|
552
|
+
const statement = fresh.exists && (authority.relation_version > 0 || candidate.statement) ? parseOwnershipFile(fresh.content, 'agent', { expectedSubAid: target }).statement : undefined;
|
|
553
|
+
relation = { relation_version: authority.relation_version, lock: authority.lock, owner_aid: authority.owner_aid ?? statement?.owner_aid ?? null, statement: statement ?? null };
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const uploader = this._currentAid();
|
|
557
|
+
const entityOwnerUpload = entity && uploader.aid.toLowerCase() !== target.toLowerCase();
|
|
558
|
+
const entityAuthorization = entityOwnerUpload ? originalCondition ? (this.pendingUpload(target))?.authorization : this._ownerUploadAuthorization(target, signedContent, uploader, String(entityContext.entity_cert_fingerprint)) : '';
|
|
559
|
+
if (entityOwnerUpload) {
|
|
560
|
+
const timestamp = this._validateEntityUploadAuthorization(entityAuthorization, target, signedContent, uploader, String(entityContext.entity_cert_fingerprint));
|
|
561
|
+
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > 300)
|
|
562
|
+
throw ownershipFailure('conflict', 'pending entity V2 authorization expired; explicit upload is required');
|
|
563
|
+
}
|
|
564
|
+
const pending = this.saveRecord(target, { pending_operation: 'replace', pending_target_aid: target, pending_content: signedContent, pending_content_etag: AgentMdManager.contentEtag(signedContent), pending_error: '', pending_condition: condition, pending_relation: relation, pending_authorization: entityAuthorization });
|
|
565
|
+
const savedCondition = pending.pending_condition;
|
|
566
|
+
if (pending.pending_content !== signedContent || savedCondition?.if_match !== condition.if_match || savedCondition?.if_none_match !== condition.if_none_match || pending.pending_authorization !== entityAuthorization)
|
|
567
|
+
throw new StateError('agent.md pending upload persistence failed');
|
|
568
|
+
try {
|
|
569
|
+
const owner = this._currentAid();
|
|
570
|
+
const delegated = target.toLowerCase() !== this._ownerAid().toLowerCase();
|
|
571
|
+
if (delegated && !owner?.isPrivateKeyValid())
|
|
572
|
+
throw new StateError('owner agent.md upload requires a loaded owner identity');
|
|
573
|
+
const authorization = entity ? entityAuthorization : delegated ? this._ownerUploadAuthorization(target, signedContent, owner) : '';
|
|
574
|
+
const result = await this._uploadHttp(target, signedContent, authorization, condition);
|
|
575
|
+
const remoteEtag = String(result.etag ?? '').trim();
|
|
576
|
+
const saved = this.saveRecord(target, {
|
|
577
|
+
content: signedContent, local_etag: AgentMdManager.contentEtag(signedContent), remote_etag: remoteEtag || undefined,
|
|
578
|
+
last_modified: String(result.last_modified ?? result.lastModified ?? '').trim(), fetched_at: Date.now(), checked_at: Date.now(),
|
|
579
|
+
remote_status: remoteEtag ? 'found' : 'unknown', last_error: '',
|
|
580
|
+
pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '',
|
|
581
|
+
});
|
|
582
|
+
if (!saved.aid)
|
|
583
|
+
throw new StateError('agent.md upload completion persistence failed');
|
|
584
|
+
return { ...result };
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
this.saveRecord(target, { pending_error: error instanceof Error ? error.message : String(error) });
|
|
588
|
+
throw error;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
async _readEntityFresh(target) {
|
|
592
|
+
const gateway = await this._resolveGateway(target);
|
|
593
|
+
const headers = { 'Cache-Control': 'no-cache', ...(this._uploadViaGatewayHost ? { Host: target } : {}) };
|
|
594
|
+
const request = (url) => requestText(url, { method: 'GET', verifySsl: this._verifySsl, headers, timeoutMs: DEFAULT_HTTP_TIMEOUT_MS, redirectsLeft: 0, strictUtf8: true });
|
|
595
|
+
const response = await request(this._url(target, gateway, this._uploadViaGatewayHost));
|
|
596
|
+
if (response.status !== 200 && response.status !== 404)
|
|
597
|
+
throw ownershipFailure('unavailable', `entity agent.md fresh read failed: HTTP ${response.status}`, true);
|
|
598
|
+
if (headerValue(response.headers, 'X-AUN-Ownership-Status') !== 'not_applicable' || !['current', 'stale'].includes(headerValue(response.headers, 'X-AUN-Entity-Verification-Status')))
|
|
599
|
+
throw ownershipFailure('unavailable', 'entity authority status metadata is unavailable', true);
|
|
600
|
+
const etag = headerValue(response.headers, 'ETag');
|
|
601
|
+
if (response.status === 200 && !etag)
|
|
602
|
+
throw ownershipFailure('unavailable', 'entity agent.md fresh read has no ETag', true);
|
|
603
|
+
return { exists: response.status === 200, content: response.status === 200 ? response.text : '', etag, owner_aid: headerValue(response.headers, 'X-AUN-Entity-Owner-AID'), entity_cert_fingerprint: headerValue(response.headers, 'X-AUN-Entity-Cert-Fingerprint') };
|
|
604
|
+
}
|
|
605
|
+
async readEntityAuthorityFresh(target) {
|
|
606
|
+
const observed = await this._readEntityFresh(this.safeAid(target));
|
|
607
|
+
if (!observed.owner_aid || !/^sha256:[0-9a-f]{64}$/.test(observed.entity_cert_fingerprint))
|
|
608
|
+
throw ownershipFailure('unavailable', 'entity owner authority metadata is unavailable', true);
|
|
609
|
+
return { owner_aid: observed.owner_aid, entity_cert_fingerprint: observed.entity_cert_fingerprint };
|
|
610
|
+
}
|
|
611
|
+
async _entityContext(target, meta, signer) {
|
|
612
|
+
const owner = String(meta.owner ?? '').trim().toLowerCase();
|
|
613
|
+
const uploader = this._currentAid();
|
|
614
|
+
if (![owner, target.toLowerCase()].includes(signer.aid.toLowerCase()) || !uploader || ![owner, target.toLowerCase()].includes(uploader.aid.toLowerCase()) || uploader.aid.toLowerCase() !== this._ownerAid().toLowerCase())
|
|
615
|
+
throw ownershipFailure('forbidden', 'group/org agent.md requires entity or current owner identity');
|
|
616
|
+
const context = await this._entityCurrentContext(target, meta, true);
|
|
617
|
+
if (signer.aid.toLowerCase() === owner)
|
|
618
|
+
await this._verifyEntityOwnerSigner(signer);
|
|
619
|
+
else if (signer.certFingerprint !== context.entity_cert_fingerprint)
|
|
620
|
+
throw ownershipFailure('conflict', 'entity signing certificate changed');
|
|
621
|
+
if (uploader.aid.toLowerCase() === target.toLowerCase()) {
|
|
622
|
+
if (uploader.certFingerprint !== context.entity_cert_fingerprint)
|
|
623
|
+
throw ownershipFailure('conflict', 'entity uploader certificate changed');
|
|
624
|
+
}
|
|
625
|
+
else if (uploader.certFingerprint !== signer.certFingerprint)
|
|
626
|
+
await this._verifyEntityOwnerSigner(uploader);
|
|
627
|
+
return context;
|
|
628
|
+
}
|
|
629
|
+
async _entitySigningIdentity(target, meta, signature, candidate) {
|
|
630
|
+
const signer = signature.signer_aid ?? target;
|
|
631
|
+
if (candidate?.aid === signer && candidate.certFingerprint === signature.cert_fingerprint)
|
|
632
|
+
return candidate;
|
|
633
|
+
if (signer === target)
|
|
634
|
+
return await this._entityIdentityGetter?.(target, String(meta.type)) ?? await this._resolvePeer(target);
|
|
635
|
+
if (!this._entitySignerResolver)
|
|
636
|
+
throw ownershipFailure('unavailable', 'entity owner signing certificate resolver is unavailable', true);
|
|
637
|
+
return await this._entitySignerResolver(signer, signature.cert_fingerprint);
|
|
638
|
+
}
|
|
639
|
+
async _verifyEntityOwnerSigner(owner) {
|
|
640
|
+
if (!this._entitySignerResolver)
|
|
641
|
+
throw ownershipFailure('unavailable', 'entity owner signing certificate resolver is unavailable', true);
|
|
642
|
+
const fresh = await this._entitySignerResolver(owner.aid, owner.certFingerprint);
|
|
643
|
+
if (fresh.aid.toLowerCase() !== owner.aid.toLowerCase() || fresh.certFingerprint !== owner.certFingerprint) {
|
|
644
|
+
throw ownershipFailure('conflict', 'entity owner signing certificate changed');
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
async _entityCurrentContext(target, meta, uploading = false) {
|
|
648
|
+
const owner = String(meta.owner ?? '').trim().toLowerCase();
|
|
649
|
+
const observed = this._entityOwnerResolver ? null : await this._readEntityFresh(target);
|
|
650
|
+
if (observed && !/^sha256:[0-9a-f]{64}$/.test(observed.entity_cert_fingerprint))
|
|
651
|
+
throw ownershipFailure('unavailable', 'entity authority metadata is unavailable', true);
|
|
652
|
+
const freshOwner = String(this._entityOwnerResolver ? await this._entityOwnerResolver(target, String(meta.type)) : observed?.owner_aid).trim().toLowerCase();
|
|
653
|
+
if (!freshOwner)
|
|
654
|
+
throw ownershipFailure('unavailable', 'entity current owner is unavailable', true);
|
|
655
|
+
if (freshOwner !== owner)
|
|
656
|
+
throw ownershipFailure(uploading ? 'forbidden' : 'conflict', 'entity current owner changed');
|
|
657
|
+
const entity = await this._entityIdentityGetter?.(target, String(meta.type)) ?? await this._resolvePeer(target);
|
|
658
|
+
if (!entity.certFingerprint)
|
|
659
|
+
throw ownershipFailure('unavailable', 'entity current certificate is unavailable', true);
|
|
660
|
+
if (observed && observed.entity_cert_fingerprint !== entity.certFingerprint)
|
|
661
|
+
throw ownershipFailure('conflict', 'entity authority changed during verification');
|
|
662
|
+
if (meta.entity_cert_fingerprint !== entity.certFingerprint)
|
|
663
|
+
throw ownershipFailure('conflict', 'entity agent.md certificate generation changed');
|
|
664
|
+
return { type: meta.type, owner_aid: owner, entity_cert_fingerprint: entity.certFingerprint };
|
|
665
|
+
}
|
|
666
|
+
_validateEntityUploadAuthorization(authorization, target, content, owner, entityFingerprint) {
|
|
667
|
+
const invalid = ownershipFailure('persistence_unavailable', 'pending entity original V2 authorization is missing or invalid');
|
|
668
|
+
try {
|
|
669
|
+
if (typeof authorization !== 'string' || !/^[A-Za-z0-9_-]+={0,2}$/.test(authorization) || authorization.includes('=') && authorization.length % 4 !== 0)
|
|
670
|
+
throw invalid;
|
|
671
|
+
const raw = Buffer.from(authorization, 'base64url');
|
|
672
|
+
if (raw.toString('base64url') !== authorization.replace(/=+$/, ''))
|
|
673
|
+
throw invalid;
|
|
674
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(raw);
|
|
675
|
+
const value = JSON.parse(text);
|
|
676
|
+
const lexical = parseDocument(text, { uniqueKeys: true });
|
|
677
|
+
if (lexical.errors.length || lexical.get('version', true)?.source !== '2' || !/^[1-9][0-9]*$/.test(lexical.get('timestamp', true)?.source ?? ''))
|
|
678
|
+
throw invalid;
|
|
679
|
+
const fields = ['version', 'target_aid', 'uploader_aid', 'body_sha256', 'entity_cert_fingerprint', 'timestamp', 'nonce', 'cert_fingerprint', 'signature'];
|
|
680
|
+
if (!isJsonObject(value) || Object.keys(value).length !== fields.length || fields.some(field => !(field in value)) || value.version !== 2)
|
|
681
|
+
throw invalid;
|
|
682
|
+
const timestamp = value.timestamp, nonce = value.nonce;
|
|
683
|
+
if (typeof timestamp !== 'number' || !Number.isSafeInteger(timestamp) || timestamp <= 0 || typeof nonce !== 'string' || !nonce.trim() || Buffer.byteLength(nonce, 'utf8') > 128)
|
|
684
|
+
throw invalid;
|
|
685
|
+
if (value.target_aid !== target.toLowerCase() || value.uploader_aid !== owner.aid.toLowerCase() || value.body_sha256 !== crypto.createHash('sha256').update(content, 'utf-8').digest('hex') || value.entity_cert_fingerprint !== entityFingerprint || value.cert_fingerprint !== owner.certFingerprint || typeof value.signature !== 'string')
|
|
686
|
+
throw invalid;
|
|
687
|
+
const verified = owner.verify(`AUN-AGENTMD-OWNER-AUTH-V2;${value.uploader_aid};${value.target_aid};${value.body_sha256};${entityFingerprint};${timestamp};${nonce}`, value.signature);
|
|
688
|
+
if (!verified.ok || !verified.data?.valid)
|
|
689
|
+
throw invalid;
|
|
690
|
+
return timestamp;
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
throw invalid;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
_ownerUploadAuthorization(target, signedContent, owner, entityFingerprint = '') {
|
|
697
|
+
const bodySha256 = crypto.createHash('sha256').update(signedContent, 'utf-8').digest('hex');
|
|
698
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
699
|
+
const nonce = crypto.randomBytes(16).toString('hex');
|
|
700
|
+
const canonical = entityFingerprint ? `AUN-AGENTMD-OWNER-AUTH-V2;${owner.aid.toLowerCase()};${target.toLowerCase()};${bodySha256};${entityFingerprint};${timestamp};${nonce}` : `AUN-AGENT-MD-UPLOAD-V1;${owner.aid.toLowerCase()};${target.toLowerCase()};${bodySha256};${timestamp};${nonce}`;
|
|
701
|
+
const signed = owner.sign(canonical);
|
|
702
|
+
if (!signed.ok || !signed.data) {
|
|
703
|
+
const message = signed.error?.message ?? 'owner authorization signing failed';
|
|
704
|
+
throw new ClientSignatureError(message);
|
|
705
|
+
}
|
|
706
|
+
const payload = {
|
|
707
|
+
version: entityFingerprint ? 2 : 1,
|
|
708
|
+
target_aid: target.toLowerCase(),
|
|
709
|
+
uploader_aid: owner.aid.toLowerCase(),
|
|
710
|
+
body_sha256: bodySha256,
|
|
711
|
+
...(entityFingerprint ? { entity_cert_fingerprint: entityFingerprint } : {}),
|
|
712
|
+
timestamp,
|
|
713
|
+
nonce,
|
|
714
|
+
cert_fingerprint: owner.certFingerprint,
|
|
715
|
+
signature: signed.data.signature,
|
|
716
|
+
};
|
|
717
|
+
return Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
718
|
+
}
|
|
719
|
+
async _uploadHttp(target, signedContent, ownerAuthorization = '', condition) {
|
|
720
|
+
if (!condition || Object.keys(condition).length !== 1 || !(typeof condition.if_match === 'string' && condition.if_match || condition.if_none_match === '*'))
|
|
721
|
+
throw ownershipFailure('persistence_unavailable', 'agent.md upload requires its original document condition');
|
|
722
|
+
const gatewayUrl = await this._resolveGateway(target);
|
|
723
|
+
const authorization = String(ownerAuthorization ?? '').trim();
|
|
724
|
+
const entity = ['group', 'org'].includes(String(validateAgentMdDocument(signedContent, { expectedAid: target }).frontmatter.type));
|
|
311
725
|
const uploadHeaders = {
|
|
312
|
-
Authorization: `Bearer ${token}`,
|
|
313
726
|
'Content-Type': 'text/markdown; charset=utf-8',
|
|
727
|
+
...(!entity ? { 'X-AUN-Ownership-Check': '1' } : {}),
|
|
728
|
+
...(condition.if_match ? { 'If-Match': condition.if_match } : { 'If-None-Match': '*' }),
|
|
314
729
|
...(this._uploadViaGatewayHost ? { Host: target } : {}),
|
|
315
|
-
...(
|
|
730
|
+
...(authorization ? { [entity ? 'AUN-AGENTMD-OWNER-AUTHORIZATION' : 'X-AUN-AgentMD-Owner-Authorization']: authorization } : {}),
|
|
316
731
|
};
|
|
732
|
+
if (!authorization || entity) {
|
|
733
|
+
const token = await this._uploadAccessToken(entity ? this._ownerAid() : target, gatewayUrl);
|
|
734
|
+
uploadHeaders.Authorization = `Bearer ${token}`;
|
|
735
|
+
}
|
|
317
736
|
const uploadRequest = (url) => requestText(url, {
|
|
318
737
|
method: 'PUT',
|
|
319
738
|
verifySsl: this._verifySsl,
|
|
@@ -329,11 +748,12 @@ export class AgentMdManager {
|
|
|
329
748
|
if (fallbackUrl && fallbackUrl !== uploadUrl)
|
|
330
749
|
response = await uploadRequest(fallbackUrl);
|
|
331
750
|
}
|
|
332
|
-
if (response.status === 404)
|
|
751
|
+
if (response.status === 404)
|
|
333
752
|
throw new NotFoundError(`agent.md endpoint not found for aid: ${target}`);
|
|
334
|
-
}
|
|
335
753
|
if (response.status < 200 || response.status >= 300) {
|
|
336
754
|
const message = response.text.trim();
|
|
755
|
+
if (entity)
|
|
756
|
+
throw ownershipFailure(response.status === 409 || response.status === 412 ? 'conflict' : response.status === 401 || response.status === 403 ? 'forbidden' : response.status === 400 || response.status === 422 ? 'invalid' : response.status === 428 ? 'unsupported_protocol' : 'unavailable', `upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`, response.status >= 500);
|
|
337
757
|
throw new AUNError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
|
|
338
758
|
}
|
|
339
759
|
let payload;
|
|
@@ -343,21 +763,9 @@ export class AgentMdManager {
|
|
|
343
763
|
catch (err) {
|
|
344
764
|
throw new AUNError(`upload agent.md returned invalid JSON payload: ${err instanceof Error ? err.message : String(err)}`);
|
|
345
765
|
}
|
|
346
|
-
if (!isJsonObject(payload))
|
|
766
|
+
if (!isJsonObject(payload))
|
|
347
767
|
throw new AUNError('upload agent.md returned invalid JSON payload');
|
|
348
|
-
}
|
|
349
|
-
const result = payload;
|
|
350
|
-
this.saveRecord(target, {
|
|
351
|
-
content: signedContent,
|
|
352
|
-
local_etag: AgentMdManager.contentEtag(signedContent),
|
|
353
|
-
remote_etag: String(result.etag ?? '').trim() || undefined,
|
|
354
|
-
last_modified: String(result.last_modified ?? result.lastModified ?? '').trim(),
|
|
355
|
-
fetched_at: Date.now(),
|
|
356
|
-
checked_at: Date.now(),
|
|
357
|
-
remote_status: String(result.etag ?? '').trim() ? 'found' : 'unknown',
|
|
358
|
-
last_error: '',
|
|
359
|
-
});
|
|
360
|
-
return { ...result };
|
|
768
|
+
return { ...payload };
|
|
361
769
|
}
|
|
362
770
|
async download(aid, timeoutMs = DEFAULT_HTTP_TIMEOUT_MS) {
|
|
363
771
|
const target = this.safeAid(String(aid ?? this._ownerAid() ?? '').trim());
|
|
@@ -576,14 +984,14 @@ export class AgentMdManager {
|
|
|
576
984
|
async _resolveGateway(aid) {
|
|
577
985
|
return String(await this._gatewayResolver?.(aid) ?? '');
|
|
578
986
|
}
|
|
579
|
-
async _resolvePeer(aid, certFingerprint) {
|
|
987
|
+
async _resolvePeer(aid, certFingerprint, signedAt) {
|
|
580
988
|
const expectedFp = String(certFingerprint ?? '').trim().toLowerCase();
|
|
581
989
|
const current = this._currentAid();
|
|
582
990
|
if (current && current.aid.toLowerCase() === aid.toLowerCase()) {
|
|
583
991
|
if (!expectedFp || certMatchesFingerprint(current.certPem, expectedFp))
|
|
584
992
|
return current;
|
|
585
993
|
}
|
|
586
|
-
const peer = await this._peerResolver?.(aid, expectedFp || null);
|
|
994
|
+
const peer = await this._peerResolver?.(aid, expectedFp || null, signedAt);
|
|
587
995
|
if (!(peer instanceof AID)) {
|
|
588
996
|
throw new StateError(`agent.md peer resolver did not return AID for ${aid}`);
|
|
589
997
|
}
|
|
@@ -595,6 +1003,13 @@ export class AgentMdManager {
|
|
|
595
1003
|
throw new StateError('authenticate did not return access_token');
|
|
596
1004
|
return token;
|
|
597
1005
|
}
|
|
1006
|
+
async _uploadAccessToken(aid, gatewayUrl) {
|
|
1007
|
+
const resolver = this._uploadAccessTokenResolver ?? this._accessTokenResolver;
|
|
1008
|
+
const token = String(await resolver?.(aid, gatewayUrl) ?? '').trim();
|
|
1009
|
+
if (!token)
|
|
1010
|
+
throw new StateError('authenticate did not return access_token');
|
|
1011
|
+
return token;
|
|
1012
|
+
}
|
|
598
1013
|
async _head(aid) {
|
|
599
1014
|
const target = this.safeAid(aid);
|
|
600
1015
|
const gatewayUrl = await this._resolveGateway(target);
|
|
@@ -647,6 +1062,7 @@ export class AgentMdManager {
|
|
|
647
1062
|
verifySsl: this._verifySsl,
|
|
648
1063
|
headers,
|
|
649
1064
|
timeoutMs,
|
|
1065
|
+
strictUtf8: true,
|
|
650
1066
|
});
|
|
651
1067
|
let reusedCachedNotModified = false;
|
|
652
1068
|
if (response.status === 304 && cached.content !== undefined && cached.content !== null) {
|
|
@@ -667,6 +1083,7 @@ export class AgentMdManager {
|
|
|
667
1083
|
verifySsl: this._verifySsl,
|
|
668
1084
|
headers: { Accept: 'text/markdown' },
|
|
669
1085
|
timeoutMs,
|
|
1086
|
+
strictUtf8: true,
|
|
670
1087
|
});
|
|
671
1088
|
}
|
|
672
1089
|
if (response.status === 404) {
|
|
@@ -678,9 +1095,28 @@ export class AgentMdManager {
|
|
|
678
1095
|
throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
|
|
679
1096
|
}
|
|
680
1097
|
const content = response.text;
|
|
1098
|
+
if (headerValue(response.headers, 'X-AUN-Entity-Verification-Status') === 'stale')
|
|
1099
|
+
throw ownershipFailure('conflict', 'entity agent.md is not from the current lifecycle generation');
|
|
681
1100
|
const parsed = parseAgentMdTailSignature(content);
|
|
1101
|
+
let entity = false;
|
|
1102
|
+
let frontmatter = {};
|
|
1103
|
+
try {
|
|
1104
|
+
frontmatter = parseFileFrontmatter(parsed.payload);
|
|
1105
|
+
entity = ['group', 'org'].includes(String(frontmatter.type));
|
|
1106
|
+
}
|
|
1107
|
+
catch { }
|
|
1108
|
+
if (entity) {
|
|
1109
|
+
try {
|
|
1110
|
+
frontmatter = validateAgentMdDocument(content, { expectedAid: target, validateOwnership: false, requireSignature: true }).frontmatter;
|
|
1111
|
+
}
|
|
1112
|
+
catch (error) {
|
|
1113
|
+
throw ownershipFailure('invalid', error instanceof Error ? error.message : String(error));
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (entity)
|
|
1117
|
+
await this._entityCurrentContext(target, frontmatter);
|
|
682
1118
|
const expectedFp = parsed.fields?.cert_fingerprint || parsed.fields?.public_key_fingerprint || null;
|
|
683
|
-
const peer = await this._resolvePeer(target, expectedFp);
|
|
1119
|
+
const peer = entity ? await this._entitySigningIdentity(target, frontmatter, parsed.fields) : await this._resolvePeer(target, expectedFp, parsed.fields?.timestamp ? Number(parsed.fields.timestamp) : undefined);
|
|
684
1120
|
const verified = peer.verifyAgentMd(content);
|
|
685
1121
|
if (!verified.ok || !verified.data) {
|
|
686
1122
|
const message = verified.error?.message ?? 'agent.md verification failed';
|
|
@@ -690,9 +1126,33 @@ export class AgentMdManager {
|
|
|
690
1126
|
const statusText = String(signature.status ?? 'invalid');
|
|
691
1127
|
const reason = String(signature.reason ?? '').trim();
|
|
692
1128
|
if (statusText === 'invalid') {
|
|
693
|
-
throw new ClientSignatureError(`agent.md verification failed for ${target}${reason ? `: ${reason}` : ''}
|
|
1129
|
+
throw new ClientSignatureError(`agent.md verification failed for ${target}${reason ? `: ${reason}` : ''}`, entity ? { stringCode: 'invalid' } : {});
|
|
1130
|
+
}
|
|
1131
|
+
if (!entity)
|
|
1132
|
+
frontmatter = validateAgentMdDocument(content, { expectedAid: target, validateOwnership: false }).frontmatter;
|
|
1133
|
+
let ownershipStatus;
|
|
1134
|
+
if (entity)
|
|
1135
|
+
ownershipStatus = 'not_applicable';
|
|
1136
|
+
else {
|
|
1137
|
+
try {
|
|
1138
|
+
validateOwnershipFrontmatter(frontmatter, target);
|
|
1139
|
+
const rawProof = (frontmatter.ownership_proof ?? {});
|
|
1140
|
+
const subjectSignature = rawProof.subject_signature;
|
|
1141
|
+
const ownerSignature = rawProof.owner_signature;
|
|
1142
|
+
const release = frontmatter.ownership_release;
|
|
1143
|
+
const subjectStatement = subjectSignature?.statement;
|
|
1144
|
+
const subjectCertPem = subjectSignature ? (await this._resolvePeer(target, String(subjectSignature.cert_fingerprint), isJsonObject(subjectStatement) ? Number(subjectStatement.signed_at) : undefined)).certPem : undefined;
|
|
1145
|
+
const ownerAid = String(ownerSignature ? frontmatter.owner : release?.statement && typeof release.statement === 'object' ? release.statement.owner_aid : '');
|
|
1146
|
+
const ownerFingerprint = String(ownerSignature?.cert_fingerprint ?? release?.cert_fingerprint ?? '');
|
|
1147
|
+
const ownerStatement = (ownerSignature ?? release)?.statement;
|
|
1148
|
+
const ownerCertPem = ownerFingerprint ? (await this._resolvePeer(ownerAid, ownerFingerprint, isJsonObject(ownerStatement) ? Number(ownerStatement.signed_at) : undefined)).certPem : undefined;
|
|
1149
|
+
ownershipStatus = verifyOwnershipFrontmatterStatus(frontmatter, subjectCertPem, ownerCertPem);
|
|
1150
|
+
}
|
|
1151
|
+
catch {
|
|
1152
|
+
ownershipStatus = 'invalid';
|
|
1153
|
+
}
|
|
694
1154
|
}
|
|
695
|
-
const verification = { status: statusText };
|
|
1155
|
+
const verification = { status: statusText, ownership_status: ownershipStatus };
|
|
696
1156
|
if (reason)
|
|
697
1157
|
verification.reason = reason;
|
|
698
1158
|
const responseEtag = headerValue(response.headers, 'etag');
|