@agentunion/fastaun-browser 0.5.19 → 0.5.20
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 +129 -78
- package/_packed_docs/CHANGELOG.md +129 -78
- package/_packed_docs/INDEX.md +201 -195
- package/_packed_docs/KITE_DOCS_GUIDE.md +93 -90
- package/_packed_docs/agent.md/SCHEMA.md +7 -7
- 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 +11 -11
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1072 -1072
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +676 -676
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +8 -12
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +443 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +658 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
- package/_packed_docs/protocol/agent.md/SCHEMA.md +1 -1
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +1 -1
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +67 -63
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +1 -1
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +35 -8
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +3 -3
- package/_packed_docs/sdk/09-group-rpc-manual.md +258 -254
- package/_packed_docs/sdk/09-message-rpc-manual.md +7 -4
- package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +61 -52
- package/_packed_docs/sdk/INDEX.md +92 -80
- package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +922 -0
- package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +300 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +382 -0
- package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -0
- package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +476 -0
- package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
- package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +173 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +211 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/345/256/236/347/216/260/347/247/273/346/244/215/346/214/207/345/215/227.md +331 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +207 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/344/273/243/347/240/201/345/256/241/346/237/245/346/212/245/345/221/212.md +252 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/345/256/236/346/226/275/350/256/241/345/210/222.md +156 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/346/226/271/346/241/210.md +273 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/350/257/246/347/273/206/350/256/276/350/256/241.md +211 -0
- package/dist/agent-md-schema.d.ts +15 -4
- package/dist/agent-md-schema.d.ts.map +1 -1
- package/dist/agent-md-schema.js +64 -30
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +10 -2
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +278 -121
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +46 -0
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +149 -10
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +4 -1
- package/dist/aid.d.ts.map +1 -1
- package/dist/aid.js +23 -42
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +85 -18
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +1581 -781
- package/dist/cert-utils.d.ts +49 -1
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +102 -56
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +7 -0
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +243 -28
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +89 -77
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +99 -80
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +31 -17
- package/dist/client.js.map +1 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +8 -6
- package/dist/crypto.js.map +1 -1
- package/dist/events.d.ts +5 -2
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +33 -14
- package/dist/events.js.map +1 -1
- package/dist/facades.js +5 -5
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/org-protocol.d.ts.map +1 -1
- package/dist/org-protocol.js +72 -12
- package/dist/org-protocol.js.map +1 -1
- package/dist/ownership-v21.d.ts +25 -8
- package/dist/ownership-v21.d.ts.map +1 -1
- package/dist/ownership-v21.js +142 -73
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.d.ts +6 -1
- package/dist/ownership-workflow.d.ts.map +1 -1
- package/dist/ownership-workflow.js +221 -58
- package/dist/ownership-workflow.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +2 -1
- package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
- package/dist/tools/cross-sdk-agent.js +91 -8
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +1 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +2 -0
- package/dist/transport.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
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { AID } from './aid.js';
|
|
2
2
|
import { parseDocument } from 'yaml';
|
|
3
|
-
import { certMatchesFingerprint, parseAgentMdTailSignature } from './cert-utils.js';
|
|
4
|
-
import { parseFileFrontmatter, validateAgentMdDocument, verifyAgentMdUpload } from './agent-md-schema.js';
|
|
3
|
+
import { certMatchesFingerprint, normalizeFingerprintHex, parseAgentMdTailSignature, parseAgentMdTailSignatureForRead } from './cert-utils.js';
|
|
4
|
+
import { parseFileFrontmatter, validateAgentMdDocument, validateAgentMdDocumentForRead, verifyAgentMdUpload, verifyAgentMdLegacyBodyUpload } from './agent-md-schema.js';
|
|
5
5
|
import { validateOwnershipFrontmatter, verifyOwnershipFrontmatterStatus } from './ownership-proof.js';
|
|
6
6
|
import { AUNError, ClientSignatureError, NotFoundError, StateError, ValidationError } from './errors.js';
|
|
7
7
|
import { isJsonObject } from './types.js';
|
|
8
|
-
import { parseOwnershipFile } from './ownership-v21.js';
|
|
8
|
+
import { ownershipAid, parseOwnershipFile } from './ownership-v21.js';
|
|
9
9
|
const DEFAULT_HTTP_TIMEOUT_MS = 30_000;
|
|
10
10
|
export function buildOwnershipReleaseAgentMd(content, release) {
|
|
11
11
|
return updateOwnershipAgentMd(content, { ownership_release: release });
|
|
@@ -327,7 +327,7 @@ export class AgentMdManager {
|
|
|
327
327
|
const target = this._safeAid(aid);
|
|
328
328
|
const gatewayUrl = await this._resolveGateway(target);
|
|
329
329
|
const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin('node:http') !== null;
|
|
330
|
-
const headers = { Accept: 'text/markdown', 'X-AUN-Ownership-Check': '1', ...(viaGatewayHost ? { Host: target } : {}) };
|
|
330
|
+
const headers = { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown', 'Cache-Control': 'no-cache', 'X-AUN-Ownership-Check': '1', ...(viaGatewayHost ? { Host: target } : {}) };
|
|
331
331
|
let url = this._url(target, gatewayUrl);
|
|
332
332
|
if (viaGatewayHost) {
|
|
333
333
|
const parsed = new URL(gatewayUrl);
|
|
@@ -380,7 +380,7 @@ export class AgentMdManager {
|
|
|
380
380
|
const token = await this._uploadAccessToken(target, gatewayUrl);
|
|
381
381
|
const response = await fetchWithTimeout(this._url(target, gatewayUrl), {
|
|
382
382
|
method: 'PUT', cache: 'no-store', redirect: 'error', body: content,
|
|
383
|
-
headers: { 'Content-Type': 'text/markdown; charset=utf-8', Authorization: `Bearer ${token}`,
|
|
383
|
+
headers: { 'X-AUN-Signature-Format': 'multi-v1', 'Content-Type': 'text/markdown; charset=utf-8', Authorization: `Bearer ${token}`,
|
|
384
384
|
...(condition.if_match ? { 'If-Match': condition.if_match } : { 'If-None-Match': '*' }) },
|
|
385
385
|
});
|
|
386
386
|
if (!response.ok) {
|
|
@@ -408,30 +408,23 @@ export class AgentMdManager {
|
|
|
408
408
|
if (!current.isPrivateKeyValid()) {
|
|
409
409
|
throw new StateError('uploadAgentMd requires loaded AID with a valid private key');
|
|
410
410
|
}
|
|
411
|
-
|
|
412
|
-
const text = String(content ?? '');
|
|
413
|
-
if (text.length === 0)
|
|
414
|
-
throw new ValidationError('uploadAgentMd requires non-empty content');
|
|
415
|
-
validateAgentMdDocument(text, { expectedAid: target });
|
|
416
|
-
await this.saveRecord(target, {
|
|
417
|
-
content: text,
|
|
418
|
-
local_etag: await AgentMdManager.contentEtag(text),
|
|
419
|
-
fetched_at: Date.now(),
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
const localContent = await this.readContent(target);
|
|
411
|
+
const localContent = content ?? (String((await this.loadRecord(target, true))?.local_draft_content ?? '') || await this.readContent(target));
|
|
423
412
|
if (localContent === null || localContent.length === 0) {
|
|
424
413
|
throw new ValidationError('uploadAgentMd requires local agent.md content');
|
|
425
414
|
}
|
|
426
|
-
const parsed =
|
|
415
|
+
const parsed = this._parseBodyUpdate(localContent, target);
|
|
427
416
|
if (['group', 'org'].includes(String(parsed.frontmatter.type)))
|
|
428
417
|
return await this.uploadAs(target, localContent, current);
|
|
429
|
-
|
|
418
|
+
if (parsed.signatures.length > 1) {
|
|
419
|
+
await this._verifyBodyUpload(target, localContent, current);
|
|
420
|
+
return await this._uploadPrepared(target, localContent, current);
|
|
421
|
+
}
|
|
422
|
+
const signedResult = await current.signAgentMd(this._isLegacyBodyRelation(parsed.frontmatter) ? parsed.payload : localContent);
|
|
430
423
|
if (!signedResult.ok) {
|
|
431
424
|
throw new ClientSignatureError(signedResult.error.message || 'agent.md signing failed');
|
|
432
425
|
}
|
|
433
426
|
const signed = signedResult.data.signed;
|
|
434
|
-
await
|
|
427
|
+
await this._verifyBodyUpload(target, signed, current);
|
|
435
428
|
return await this._uploadPrepared(target, signed, current);
|
|
436
429
|
}
|
|
437
430
|
async uploadAs(targetAid, content, signer) {
|
|
@@ -439,19 +432,33 @@ export class AgentMdManager {
|
|
|
439
432
|
const rawContent = String(content ?? '');
|
|
440
433
|
if (!rawContent.trim())
|
|
441
434
|
throw new ValidationError('uploadAgentMd requires non-empty content');
|
|
442
|
-
const parsed =
|
|
435
|
+
const parsed = this._parseBodyUpdate(rawContent, target);
|
|
436
|
+
if (!['group', 'org'].includes(String(parsed.frontmatter.type)) && parsed.signatures.length > 1) {
|
|
437
|
+
await this._verifyBodyUpload(target, rawContent, signer);
|
|
438
|
+
return await this._uploadPrepared(target, rawContent, signer);
|
|
439
|
+
}
|
|
443
440
|
if (['group', 'org'].includes(String(parsed.frontmatter.type)) && parsed.signature) {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
441
|
+
await verifyAgentMdUpload(rawContent, signer.certPem, target, undefined, async (aid, fingerprint) => {
|
|
442
|
+
if (!this._entitySignerResolver)
|
|
443
|
+
throw new StateError('entity additional signing certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
444
|
+
return await this._entitySignerResolver(aid, fingerprint);
|
|
445
|
+
});
|
|
446
|
+
return await this._uploadPrepared(target, rawContent, signer);
|
|
447
|
+
}
|
|
448
|
+
if (['group', 'org'].includes(String(parsed.frontmatter.type)) && signer.aid !== target) {
|
|
449
|
+
if (!this._entityIdentityGetter)
|
|
450
|
+
throw new StateError('entity signing identity is unavailable');
|
|
451
|
+
signer = await this._entityIdentityGetter(target, String(parsed.frontmatter.type));
|
|
452
|
+
if (signer.aid !== target)
|
|
453
|
+
throw new StateError('entity signing identity does not match target');
|
|
447
454
|
}
|
|
448
455
|
if (!signer?.isPrivateKeyValid())
|
|
449
456
|
throw new StateError('uploadAgentMd requires a valid signer identity');
|
|
450
|
-
const signedResult = await signer.signAgentMd(rawContent);
|
|
457
|
+
const signedResult = await signer.signAgentMd(this._isLegacyBodyRelation(parsed.frontmatter) ? parsed.payload : rawContent);
|
|
451
458
|
if (!signedResult.ok)
|
|
452
459
|
throw new ClientSignatureError(signedResult.error.message || 'agent.md signing failed');
|
|
453
460
|
const signed = signedResult.data.signed;
|
|
454
|
-
await
|
|
461
|
+
await this._verifyBodyUpload(target, signed, signer);
|
|
455
462
|
return await this._uploadPrepared(target, signed, signer);
|
|
456
463
|
}
|
|
457
464
|
async pendingUpload(aid) {
|
|
@@ -467,10 +474,34 @@ export class AgentMdManager {
|
|
|
467
474
|
const condition = pending.condition;
|
|
468
475
|
if (!condition || Object.keys(condition).length !== 1 || !(typeof condition.if_match === 'string' && condition.if_match || condition.if_none_match === '*'))
|
|
469
476
|
throw new StateError('pending agent.md requires its original document condition', { stringCode: 'persistence_unavailable' });
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
477
|
+
const pendingDocument = validateAgentMdDocument(pending.content, { expectedAid: aid, validateOwnership: false });
|
|
478
|
+
const meta = pendingDocument.frontmatter;
|
|
479
|
+
if (this._isLegacyBodyRelation(meta) && pending.relation?.protocol !== 'legacy')
|
|
480
|
+
throw new StateError('legacy ownership pending agent.md requires its original relationship snapshot', { stringCode: 'unsupported_protocol' });
|
|
473
481
|
try {
|
|
482
|
+
if (pending.relation?.protocol === 'legacy') {
|
|
483
|
+
const relation = pending.relation;
|
|
484
|
+
if (!condition.if_match || condition.if_match !== relation.original_etag || typeof relation.original_content !== 'string' || relation.original_content_etag !== await AgentMdManager.contentEtag(relation.original_content) || pending.content_etag !== await AgentMdManager.contentEtag(pending.content))
|
|
485
|
+
throw new StateError('pending legacy original bytes or document condition are corrupt', { stringCode: 'persistence_unavailable' });
|
|
486
|
+
const original = await this._verifyLegacyBodyOriginal(aid, relation.original_content);
|
|
487
|
+
if (!this._isLegacyBodyRelation(original) || !this._sameBodyRelation(this._bodyRelationFields(original), relation.fields))
|
|
488
|
+
throw new StateError('pending legacy original relationship snapshot is corrupt', { stringCode: 'persistence_unavailable' });
|
|
489
|
+
if (!this._sameBodyRelation(this._bodyRelationFields(meta), relation.fields))
|
|
490
|
+
throw new StateError('legacy relationship changes require their original protocol', { stringCode: 'unsupported_protocol' });
|
|
491
|
+
const identity = signer ?? this._currentAid();
|
|
492
|
+
if (!identity || identity.aid.toLowerCase() !== aid.toLowerCase() || this._currentAid()?.aid.toLowerCase() !== aid.toLowerCase())
|
|
493
|
+
throw new StateError('legacy body recovery requires its current subject identity', { stringCode: 'forbidden' });
|
|
494
|
+
await this._verifyBodyUpload(aid, pending.content, identity);
|
|
495
|
+
const remote = await this._readLegacyBodyFresh(aid);
|
|
496
|
+
if (remote.content === pending.content && remote.etag === pending.content_etag) {
|
|
497
|
+
await this.saveRecord(aid, { content: pending.content, remote_etag: remote.etag, local_draft_content: '', pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
if (remote.etag !== condition.if_match || remote.content !== relation.original_content)
|
|
501
|
+
throw new StateError('pending legacy original document changed', { stringCode: 'conflict' });
|
|
502
|
+
await this._uploadPrepared(aid, pending.content, identity, condition);
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
474
505
|
if (['group', 'org'].includes(String(meta.type))) {
|
|
475
506
|
const identity = signer ?? this._currentAid();
|
|
476
507
|
if (!identity)
|
|
@@ -485,12 +516,10 @@ export class AgentMdManager {
|
|
|
485
516
|
if (pending.content_etag !== await AgentMdManager.contentEtag(pending.content))
|
|
486
517
|
throw new StateError('pending entity original bytes are corrupt', { stringCode: 'persistence_unavailable' });
|
|
487
518
|
const documentSigner = await this._entityDocumentSigner(aid, pending.content);
|
|
488
|
-
await
|
|
489
|
-
if (identity.aid.toLowerCase() !== aid.toLowerCase())
|
|
490
|
-
await this._validateEntityUploadAuthorization(pending.authorization, aid, pending.content, identity, String(context.entity_cert_fingerprint));
|
|
519
|
+
await this._verifyEntityDocument(aid, pending.content);
|
|
491
520
|
const remote = await this._readEntityFresh(aid);
|
|
492
521
|
if (remote.exists && remote.content === pending.content && remote.etag === pending.content_etag) {
|
|
493
|
-
await 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
|
+
await this.saveRecord(aid, { content: pending.content, remote_etag: remote.etag, local_draft_content: '', pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
|
|
494
523
|
return true;
|
|
495
524
|
}
|
|
496
525
|
if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
|
|
@@ -498,6 +527,12 @@ export class AgentMdManager {
|
|
|
498
527
|
await this._uploadPrepared(aid, pending.content, identity, condition);
|
|
499
528
|
return true;
|
|
500
529
|
}
|
|
530
|
+
const identity = signer ?? this._currentAid();
|
|
531
|
+
if (pendingDocument.signatures.length > 1) {
|
|
532
|
+
if (!identity)
|
|
533
|
+
throw new StateError('pending agent.md requires a signer identity');
|
|
534
|
+
await this._verifyBodyUpload(aid, pending.content, identity);
|
|
535
|
+
}
|
|
501
536
|
const remote = await this.readOwnershipFresh(aid);
|
|
502
537
|
const fail = (code, reason, retryable = false) => new StateError(reason, { stringCode: code, retryable });
|
|
503
538
|
const candidate = parseOwnershipFile(pending.content, 'agent', { expectedSubAid: aid });
|
|
@@ -519,17 +554,17 @@ export class AgentMdManager {
|
|
|
519
554
|
if (!relation && (remote.relation_version !== 0 || remote.lock !== 'unlocked'))
|
|
520
555
|
throw fail('unavailable', 'pending agent.md cannot overwrite relationship history', true);
|
|
521
556
|
if (remote.exists && remote.etag === pending.content_etag && remote.content === pending.content) {
|
|
522
|
-
const saved = await 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: '' });
|
|
557
|
+
const saved = await this.saveRecord(aid, { content: pending.content, remote_etag: remote.etag, local_draft_content: '', pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
|
|
523
558
|
if (!saved.aid)
|
|
524
559
|
throw new StateError('agent.md pending completion persistence failed');
|
|
525
560
|
return true;
|
|
526
561
|
}
|
|
527
562
|
if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
|
|
528
563
|
throw new StateError('pending agent.md original document condition changed', { stringCode: 'conflict' });
|
|
529
|
-
const identity = signer ?? this._currentAid();
|
|
530
564
|
if (!identity)
|
|
531
565
|
throw new StateError('pending agent.md requires a signer identity');
|
|
532
|
-
|
|
566
|
+
if (pendingDocument.signatures.length <= 1)
|
|
567
|
+
await this._verifyBodyUpload(aid, pending.content, identity);
|
|
533
568
|
await this._uploadPrepared(aid, pending.content, identity, condition);
|
|
534
569
|
return true;
|
|
535
570
|
}
|
|
@@ -538,33 +573,142 @@ export class AgentMdManager {
|
|
|
538
573
|
throw error;
|
|
539
574
|
}
|
|
540
575
|
}
|
|
576
|
+
_parseBodyUpdate(content, target) {
|
|
577
|
+
const parsed = validateAgentMdDocumentForRead(content, { expectedAid: target, validateOwnership: false });
|
|
578
|
+
return validateAgentMdDocument(this._isLegacyBodyRelation(parsed.frontmatter) && parsed.signatures.length <= 1 ? parsed.payload : content, { expectedAid: target, validateOwnership: false });
|
|
579
|
+
}
|
|
580
|
+
_isLegacyBodyRelation(meta) {
|
|
581
|
+
if (['group', 'org'].includes(String(meta.type)))
|
|
582
|
+
return false;
|
|
583
|
+
if (Object.hasOwn(meta, 'ownership')) {
|
|
584
|
+
const ownership = meta.ownership;
|
|
585
|
+
const statement = isJsonObject(ownership) ? ownership.statement : undefined;
|
|
586
|
+
return isJsonObject(statement) && statement.version === 2 && !Object.hasOwn(statement, 'relation_version');
|
|
587
|
+
}
|
|
588
|
+
return ['owner', 'ownership_proof', 'ownership_release'].some((field) => Object.hasOwn(meta, field));
|
|
589
|
+
}
|
|
590
|
+
_bodyRelationFields(meta) {
|
|
591
|
+
return Object.fromEntries(['owner', 'ownership', 'ownership_proof', 'ownership_release'].filter((field) => Object.hasOwn(meta, field)).map((field) => [field, meta[field]]));
|
|
592
|
+
}
|
|
593
|
+
_sameBodyRelation(first, second) {
|
|
594
|
+
const stable = (value) => Array.isArray(value) ? value.map(stable) : isJsonObject(value) ? Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])])) : value;
|
|
595
|
+
return JSON.stringify(stable(first)) === JSON.stringify(stable(second));
|
|
596
|
+
}
|
|
597
|
+
async _readLegacyBodyFresh(target) {
|
|
598
|
+
const gateway = await this._resolveGateway(target);
|
|
599
|
+
const response = await fetchWithTimeout(this._url(target, gateway), { method: 'GET', headers: { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown', 'Cache-Control': 'no-cache' }, cache: 'no-store', redirect: 'error' });
|
|
600
|
+
if (response.status === 404)
|
|
601
|
+
throw new StateError('legacy relationship cannot be created by an ordinary body update', { stringCode: 'unsupported_protocol' });
|
|
602
|
+
const etag = headerValue(response.headers, 'ETag');
|
|
603
|
+
if (response.status !== 200 || !etag)
|
|
604
|
+
throw new StateError('legacy original document or ETag is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
605
|
+
let content;
|
|
606
|
+
try {
|
|
607
|
+
content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(await response.arrayBuffer());
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
throw new ValidationError('agent.md must be valid UTF-8', { stringCode: 'invalid' });
|
|
611
|
+
}
|
|
612
|
+
return { content, etag, last_modified: headerValue(response.headers, 'Last-Modified'), status: response.status };
|
|
613
|
+
}
|
|
614
|
+
async _verifyLegacyBodyOriginal(target, content) {
|
|
615
|
+
const parsed = validateAgentMdDocumentForRead(content, { expectedAid: target, validateOwnership: false, requireSignature: true });
|
|
616
|
+
const peer = await this._resolvePeer(target, parsed.signature.cert_fingerprint, Number(parsed.signature.timestamp));
|
|
617
|
+
const verified = await this._verifyPeerDocument(peer, content);
|
|
618
|
+
if (!verified.ok || verified.data.status !== 'verified')
|
|
619
|
+
throw new ClientSignatureError('legacy original agent.md signature verification failed', { stringCode: 'invalid' });
|
|
620
|
+
return parsed.frontmatter;
|
|
621
|
+
}
|
|
622
|
+
async _verifyPeerDocument(peer, content) {
|
|
623
|
+
const parsed = parseAgentMdTailSignatureForRead(content);
|
|
624
|
+
const additionalCertificates = new Map();
|
|
625
|
+
if (parsed.fields)
|
|
626
|
+
additionalCertificates.set(`${parsed.fields.signer_aid ?? peer.aid};${parsed.fields.cert_fingerprint}`, peer);
|
|
627
|
+
for (const block of parsed.signatures.slice(1))
|
|
628
|
+
additionalCertificates.set(`${block.fields.signer_aid};${block.fields.cert_fingerprint}`, await this._resolvePeer(block.fields.signer_aid, block.fields.cert_fingerprint, Number(block.fields.timestamp)));
|
|
629
|
+
return await peer.verifyAgentMd(content, (signer, fingerprint) => {
|
|
630
|
+
const certificate = additionalCertificates.get(`${signer};${fingerprint}`);
|
|
631
|
+
if (!certificate)
|
|
632
|
+
throw new StateError('additional signing certificate is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
633
|
+
return certificate;
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
async _verifyBodyUpload(target, content, signer) {
|
|
637
|
+
const meta = validateAgentMdDocument(content, { expectedAid: target, validateOwnership: false }).frontmatter;
|
|
638
|
+
await (this._isLegacyBodyRelation(meta) ? verifyAgentMdLegacyBodyUpload : verifyAgentMdUpload)(content, signer.certPem, target, undefined, async (aid, fingerprint, timestamp) => {
|
|
639
|
+
if (this._entitySignerResolver)
|
|
640
|
+
return await this._entitySignerResolver(aid, fingerprint);
|
|
641
|
+
return await this._resolvePeer(aid, fingerprint, timestamp);
|
|
642
|
+
});
|
|
643
|
+
}
|
|
541
644
|
async _uploadPrepared(target, signed, signer, originalCondition) {
|
|
542
|
-
const meta = validateAgentMdDocument(signed, { expectedAid: target }).frontmatter;
|
|
645
|
+
const meta = validateAgentMdDocument(signed, { expectedAid: target, validateOwnership: false }).frontmatter;
|
|
646
|
+
if (this._isLegacyBodyRelation(meta))
|
|
647
|
+
validateOwnershipFrontmatter(Object.fromEntries(['aid', 'type', 'owner'].filter((field) => Object.hasOwn(meta, field)).map((field) => [field, meta[field]])), target);
|
|
648
|
+
if (!['group', 'org'].includes(String(meta.type)))
|
|
649
|
+
await this._verifyBodyUpload(target, signed, signer);
|
|
650
|
+
if (!originalCondition) {
|
|
651
|
+
const draft = await this.saveRecord(target, { local_draft_content: signed });
|
|
652
|
+
if (draft.local_draft_content !== signed)
|
|
653
|
+
throw new StateError('agent.md local draft persistence failed', { stringCode: 'persistence_unavailable' });
|
|
654
|
+
}
|
|
543
655
|
const entity = ['group', 'org'].includes(String(meta.type));
|
|
544
656
|
const uploader = entity ? this._currentAid() ?? signer : signer;
|
|
545
657
|
const entityContext = entity ? await this._entityContext(target, meta, uploader) : undefined;
|
|
546
658
|
if (entity)
|
|
547
|
-
await
|
|
659
|
+
await this._verifyEntityDocument(target, signed);
|
|
548
660
|
let condition = originalCondition;
|
|
549
661
|
let relation = entityContext ?? (await this.pendingUpload(target))?.relation;
|
|
550
662
|
if (!condition) {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
663
|
+
let fresh;
|
|
664
|
+
if (!entity && this._isLegacyBodyRelation(meta))
|
|
665
|
+
fresh = await this._readLegacyBodyFresh(target);
|
|
666
|
+
else {
|
|
667
|
+
try {
|
|
668
|
+
fresh = entity ? await this._readEntityFresh(target) : await this.readOwnershipFresh(target);
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
if (entity || !['unavailable', 'unsupported_protocol'].includes(String(error?.stringCode)))
|
|
672
|
+
throw error;
|
|
673
|
+
const legacy = await this._readLegacyBodyFresh(target);
|
|
674
|
+
const legacyMeta = validateAgentMdDocumentForRead(legacy.content, { expectedAid: target, validateOwnership: false }).frontmatter;
|
|
675
|
+
if (!this._isLegacyBodyRelation(legacyMeta))
|
|
676
|
+
throw error;
|
|
677
|
+
fresh = legacy;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
const freshMeta = !entity && (!('exists' in fresh) || fresh.exists) && fresh.content ? validateAgentMdDocumentForRead(fresh.content, { expectedAid: target, validateOwnership: false }).frontmatter : undefined;
|
|
681
|
+
const legacy = !entity && (this._isLegacyBodyRelation(meta) || freshMeta && this._isLegacyBodyRelation(freshMeta));
|
|
682
|
+
if (legacy) {
|
|
683
|
+
if (!freshMeta || !this._isLegacyBodyRelation(freshMeta) || !this._sameBodyRelation(this._bodyRelationFields(meta), this._bodyRelationFields(freshMeta)))
|
|
684
|
+
throw new StateError('legacy relationship changes require their original protocol', { stringCode: 'unsupported_protocol' });
|
|
685
|
+
if (!fresh.etag)
|
|
686
|
+
throw new StateError('legacy original document ETag is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
687
|
+
await this._verifyLegacyBodyOriginal(target, fresh.content);
|
|
688
|
+
condition = { if_match: fresh.etag };
|
|
689
|
+
relation = { protocol: 'legacy', original_content: fresh.content, original_content_etag: await AgentMdManager.contentEtag(fresh.content), original_etag: fresh.etag, fields: this._bodyRelationFields(freshMeta) };
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
554
692
|
const authority = fresh;
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
693
|
+
condition = authority.exists ? { if_match: fresh.etag } : { if_none_match: '*' };
|
|
694
|
+
if (!entity) {
|
|
695
|
+
const candidate = parseOwnershipFile(signed, 'agent', { expectedSubAid: target });
|
|
696
|
+
const statement = authority.exists && (authority.relation_version > 0 || candidate.statement) ? parseOwnershipFile(fresh.content, 'agent', { expectedSubAid: target }).statement : undefined;
|
|
697
|
+
relation = { relation_version: authority.relation_version, lock: authority.lock, owner_aid: authority.owner_aid ?? statement?.owner_aid ?? null, statement: statement ?? null };
|
|
698
|
+
}
|
|
558
699
|
}
|
|
559
700
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
701
|
+
if (relation?.protocol === 'legacy') {
|
|
702
|
+
if (signer.aid.toLowerCase() !== target.toLowerCase() || this._currentAid()?.aid.toLowerCase() !== target.toLowerCase() || this._currentAid()?.certFingerprint !== signer.certFingerprint)
|
|
703
|
+
throw new StateError('legacy body update requires its current subject identity', { stringCode: 'forbidden' });
|
|
704
|
+
if (!this._isLegacyBodyRelation(meta) || !this._sameBodyRelation(this._bodyRelationFields(meta), relation.fields))
|
|
705
|
+
throw new StateError('legacy relationship changes require their original protocol', { stringCode: 'unsupported_protocol' });
|
|
706
|
+
await this._verifyBodyUpload(target, signed, signer);
|
|
566
707
|
}
|
|
708
|
+
const entityAuthorization = '';
|
|
567
709
|
const pending = await this.saveRecord(target, { pending_operation: 'replace', pending_target_aid: target, pending_content: signed, pending_content_etag: await AgentMdManager.contentEtag(signed), pending_error: '', pending_condition: condition, pending_relation: relation, pending_authorization: entityAuthorization });
|
|
710
|
+
if (relation?.protocol === 'legacy' && !this._sameBodyRelation(pending.pending_relation, relation))
|
|
711
|
+
throw new StateError('legacy original relationship snapshot persistence failed', { stringCode: 'persistence_unavailable' });
|
|
568
712
|
const savedCondition = pending.pending_condition;
|
|
569
713
|
if (pending.pending_content !== signed || savedCondition?.if_match !== condition.if_match || savedCondition?.if_none_match !== condition.if_none_match || pending.pending_authorization !== entityAuthorization)
|
|
570
714
|
throw new StateError('agent.md pending upload persistence failed');
|
|
@@ -573,11 +717,12 @@ export class AgentMdManager {
|
|
|
573
717
|
const delegated = target.toLowerCase() !== this._ownerAid().toLowerCase();
|
|
574
718
|
if (delegated && !owner?.isPrivateKeyValid())
|
|
575
719
|
throw new StateError('owner agent.md upload requires a loaded owner identity');
|
|
576
|
-
const authorization = entity ?
|
|
577
|
-
const result = await this._uploadHttp(target, signed, authorization, condition);
|
|
720
|
+
const authorization = entity ? '' : delegated ? await this._ownerUploadAuthorization(target, signed, owner) : '';
|
|
721
|
+
const result = await this._uploadHttp(target, signed, authorization, condition, relation?.protocol === 'legacy');
|
|
578
722
|
const remoteEtag = String(result.etag ?? '').trim();
|
|
723
|
+
const contentEtag = await AgentMdManager.contentEtag(signed);
|
|
579
724
|
const saved = await this.saveRecord(target, {
|
|
580
|
-
content: signed, local_etag:
|
|
725
|
+
content: signed, local_etag: contentEtag, remote_etag: remoteEtag || undefined, ...(remoteEtag === contentEtag ? { local_draft_content: '' } : {}),
|
|
581
726
|
last_modified: String(result.last_modified ?? result.lastModified ?? '').trim(), fetched_at: Date.now(), checked_at: Date.now(),
|
|
582
727
|
remote_status: remoteEtag ? 'found' : 'unknown', last_error: '',
|
|
583
728
|
pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '',
|
|
@@ -595,12 +740,17 @@ export class AgentMdManager {
|
|
|
595
740
|
const fresh = await this._readEntityFresh(this._safeAid(target));
|
|
596
741
|
if (fresh.ownership_status !== 'not_applicable' || !['current', 'stale'].includes(fresh.entity_status ?? '') || !fresh.owner_aid || !/^sha256:[0-9a-f]{64}$/.test(fresh.entity_cert_fingerprint ?? ''))
|
|
597
742
|
throw new StateError('entity current owner authority is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
598
|
-
|
|
743
|
+
try {
|
|
744
|
+
return { owner_aid: this._safeAid(ownershipAid(fresh.owner_aid, 'owner_aid')), entity_cert_fingerprint: fresh.entity_cert_fingerprint };
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
throw new StateError('entity current owner authority is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
748
|
+
}
|
|
599
749
|
}
|
|
600
750
|
async _readEntityFresh(target) {
|
|
601
751
|
const gateway = await this._resolveGateway(target);
|
|
602
752
|
const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin('node:http') !== null;
|
|
603
|
-
const headers = { 'Cache-Control': 'no-cache', ...(viaGatewayHost ? { Host: target } : {}) };
|
|
753
|
+
const headers = { 'X-AUN-Signature-Format': 'multi-v1', 'X-AUN-Ownership-Check': '1', 'Cache-Control': 'no-cache', ...(viaGatewayHost ? { Host: target } : {}) };
|
|
604
754
|
let url = this._url(target, gateway);
|
|
605
755
|
if (viaGatewayHost) {
|
|
606
756
|
const parsed = new URL(gateway);
|
|
@@ -656,8 +806,8 @@ export class AgentMdManager {
|
|
|
656
806
|
}
|
|
657
807
|
async _entityContext(target, meta, signer) {
|
|
658
808
|
const owner = String(meta.owner ?? '').trim().toLowerCase();
|
|
659
|
-
if (
|
|
660
|
-
throw new StateError('group/org agent.md requires
|
|
809
|
+
if (signer.aid.toLowerCase() !== owner || signer.aid.toLowerCase() !== this._ownerAid().toLowerCase())
|
|
810
|
+
throw new StateError('group/org agent.md requires current owner uploader', { stringCode: 'forbidden' });
|
|
661
811
|
const context = await this._entityCurrentContext(target, meta, true);
|
|
662
812
|
if (signer.aid.toLowerCase() === owner)
|
|
663
813
|
await this._verifyEntityOwnerSigner(signer);
|
|
@@ -683,6 +833,17 @@ export class AgentMdManager {
|
|
|
683
833
|
throw new StateError('entity signing certificate changed', { stringCode: 'conflict' });
|
|
684
834
|
return signer;
|
|
685
835
|
}
|
|
836
|
+
async _verifyEntityDocument(target, content) {
|
|
837
|
+
const primary = await this._entityDocumentSigner(target, content);
|
|
838
|
+
await verifyAgentMdUpload(content, primary.certPem, target, undefined, async (signer, fingerprint) => {
|
|
839
|
+
if (!this._entitySignerResolver)
|
|
840
|
+
throw new StateError('entity additional signing certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
841
|
+
const material = await this._entitySignerResolver(signer, fingerprint);
|
|
842
|
+
if (material.aid !== signer || material.certFingerprint !== fingerprint)
|
|
843
|
+
throw new StateError('entity additional signing certificate changed', { stringCode: 'conflict' });
|
|
844
|
+
return material;
|
|
845
|
+
});
|
|
846
|
+
}
|
|
686
847
|
async _verifyEntityOwnerSigner(owner) {
|
|
687
848
|
if (!this._entitySignerResolver)
|
|
688
849
|
throw new StateError('entity owner signing certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
@@ -694,15 +855,15 @@ export class AgentMdManager {
|
|
|
694
855
|
async download(aid, timeoutMs = DEFAULT_HTTP_TIMEOUT_MS) {
|
|
695
856
|
const target = this._safeAid(String(aid ?? this._ownerAid() ?? '').trim());
|
|
696
857
|
const contentResult = await this._downloadHttp(target, timeoutMs);
|
|
697
|
-
const parsed =
|
|
858
|
+
const parsed = parseAgentMdTailSignatureForRead(contentResult.content);
|
|
698
859
|
let entity = false;
|
|
699
860
|
try {
|
|
700
|
-
entity = ['group', 'org'].includes(String(parseFileFrontmatter(parsed.payload).type));
|
|
861
|
+
entity = ['group', 'org'].includes(String(parseFileFrontmatter(parsed.payload, false, false).type));
|
|
701
862
|
}
|
|
702
863
|
catch { }
|
|
703
864
|
let frontmatter;
|
|
704
865
|
try {
|
|
705
|
-
frontmatter =
|
|
866
|
+
frontmatter = validateAgentMdDocumentForRead(contentResult.content, { expectedAid: target, validateOwnership: false, ...(entity ? { requireSignature: true } : {}) }).frontmatter;
|
|
706
867
|
}
|
|
707
868
|
catch (error) {
|
|
708
869
|
if (entity)
|
|
@@ -710,10 +871,9 @@ export class AgentMdManager {
|
|
|
710
871
|
throw error;
|
|
711
872
|
}
|
|
712
873
|
const expectedFp = parsed.fields?.cert_fingerprint || parsed.fields?.public_key_fingerprint || null;
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const
|
|
716
|
-
const verified = await peer.verifyAgentMd(contentResult.content);
|
|
874
|
+
const signerAid = entity ? (parsed.fields?.signer_aid ?? target) : target;
|
|
875
|
+
const peer = await this._resolvePeer(signerAid, expectedFp, parsed.fields?.timestamp ? Number(parsed.fields.timestamp) : undefined);
|
|
876
|
+
const verified = await this._verifyPeerDocument(peer, contentResult.content);
|
|
717
877
|
if (!verified.ok)
|
|
718
878
|
throw new AUNError(verified.error.message);
|
|
719
879
|
const signature = { ...verified.data };
|
|
@@ -971,7 +1131,7 @@ export class AgentMdManager {
|
|
|
971
1131
|
const root = this.root;
|
|
972
1132
|
await this._writeStorage(this._contentKey(aid), String(content ?? ''), root);
|
|
973
1133
|
}
|
|
974
|
-
async loadRecord(aid) {
|
|
1134
|
+
async loadRecord(aid, strict = false) {
|
|
975
1135
|
const target = String(aid ?? '').trim();
|
|
976
1136
|
if (!target)
|
|
977
1137
|
return null;
|
|
@@ -979,7 +1139,7 @@ export class AgentMdManager {
|
|
|
979
1139
|
const generation = this._storageGeneration;
|
|
980
1140
|
try {
|
|
981
1141
|
const loaded = await this._withLock(async () => {
|
|
982
|
-
const record = await this._readRecordUnlocked(target, root);
|
|
1142
|
+
const record = await this._readRecordUnlocked(target, root, strict);
|
|
983
1143
|
const next = Object.keys(record).length > 0 ? { ...record, aid: target } : { aid: target };
|
|
984
1144
|
try {
|
|
985
1145
|
const content = await this._readStorage(this._contentKey(target), root);
|
|
@@ -1005,6 +1165,8 @@ export class AgentMdManager {
|
|
|
1005
1165
|
return { ...loaded };
|
|
1006
1166
|
}
|
|
1007
1167
|
catch (err) {
|
|
1168
|
+
if (strict)
|
|
1169
|
+
throw err;
|
|
1008
1170
|
this._log.debug(`agent.md cache load skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1009
1171
|
return null;
|
|
1010
1172
|
}
|
|
@@ -1015,10 +1177,12 @@ export class AgentMdManager {
|
|
|
1015
1177
|
return {};
|
|
1016
1178
|
const root = this.root;
|
|
1017
1179
|
const generation = this._storageGeneration;
|
|
1180
|
+
let strictDraft = false;
|
|
1018
1181
|
try {
|
|
1019
1182
|
const inputFields = { ...fields };
|
|
1183
|
+
strictDraft = typeof inputFields.local_draft_content === 'string';
|
|
1020
1184
|
const hasContent = Object.prototype.hasOwnProperty.call(inputFields, 'content') && inputFields.content !== undefined && inputFields.content !== null;
|
|
1021
|
-
if (hasContent) {
|
|
1185
|
+
if (hasContent && !strictDraft) {
|
|
1022
1186
|
const text = String(inputFields.content ?? '');
|
|
1023
1187
|
await this._writeStorage(this._contentKey(target), text, root);
|
|
1024
1188
|
if (!inputFields.local_etag)
|
|
@@ -1028,8 +1192,18 @@ export class AgentMdManager {
|
|
|
1028
1192
|
}
|
|
1029
1193
|
delete inputFields.content;
|
|
1030
1194
|
const record = await this._withLock(async () => {
|
|
1031
|
-
const next = { ...(await this._readRecordUnlocked(target, root)), aid: target };
|
|
1195
|
+
const next = { ...(await this._readRecordUnlocked(target, root, strictDraft)), aid: target };
|
|
1196
|
+
if (hasContent && strictDraft) {
|
|
1197
|
+
const text = String(fields.content ?? '');
|
|
1198
|
+
await this._writeStorage(this._contentKey(target), text, root);
|
|
1199
|
+
if (!inputFields.local_etag)
|
|
1200
|
+
inputFields.local_etag = await AgentMdManager.contentEtag(text);
|
|
1201
|
+
if (!inputFields.fetched_at)
|
|
1202
|
+
inputFields.fetched_at = Date.now();
|
|
1203
|
+
}
|
|
1032
1204
|
for (const [key, value] of Object.entries(inputFields)) {
|
|
1205
|
+
if (key === 'local_draft_content' && value === '' && hasContent && next.local_draft_content !== fields.content)
|
|
1206
|
+
continue;
|
|
1033
1207
|
if (value !== undefined && value !== null)
|
|
1034
1208
|
next[key] = value;
|
|
1035
1209
|
}
|
|
@@ -1045,6 +1219,8 @@ export class AgentMdManager {
|
|
|
1045
1219
|
return { ...loaded };
|
|
1046
1220
|
}
|
|
1047
1221
|
catch (err) {
|
|
1222
|
+
if (strictDraft)
|
|
1223
|
+
throw err;
|
|
1048
1224
|
this._log.debug(`agent.md cache save skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1049
1225
|
return {};
|
|
1050
1226
|
}
|
|
@@ -1105,7 +1281,8 @@ export class AgentMdManager {
|
|
|
1105
1281
|
return String(await this._gatewayResolver?.(aid) ?? '');
|
|
1106
1282
|
}
|
|
1107
1283
|
async _resolvePeer(aid, certFingerprint, signedAt) {
|
|
1108
|
-
const
|
|
1284
|
+
const normalizedFp = normalizeFingerprintHex(certFingerprint);
|
|
1285
|
+
const expectedFp = normalizedFp ? `sha256:${normalizedFp}` : String(certFingerprint ?? '').trim().toLowerCase();
|
|
1109
1286
|
const current = this._currentAid();
|
|
1110
1287
|
if (current && current.aid.toLowerCase() === aid.toLowerCase()) {
|
|
1111
1288
|
if (!expectedFp || await certMatchesFingerprint(current.certPem, expectedFp))
|
|
@@ -1115,6 +1292,8 @@ export class AgentMdManager {
|
|
|
1115
1292
|
if (!(peer instanceof AID)) {
|
|
1116
1293
|
throw new StateError(`agent.md peer resolver did not return AID for ${aid}`);
|
|
1117
1294
|
}
|
|
1295
|
+
if (peer.aid.toLowerCase() !== aid.toLowerCase())
|
|
1296
|
+
throw new ClientSignatureError(`agent.md signer does not match requested AID: ${aid}`, { stringCode: 'invalid' });
|
|
1118
1297
|
return peer;
|
|
1119
1298
|
}
|
|
1120
1299
|
async _accessToken(aid, gatewayUrl) {
|
|
@@ -1130,52 +1309,21 @@ export class AgentMdManager {
|
|
|
1130
1309
|
throw new StateError('authenticate did not return access_token');
|
|
1131
1310
|
return token;
|
|
1132
1311
|
}
|
|
1133
|
-
async
|
|
1134
|
-
const invalid = new StateError('pending entity original V2 authorization is missing or invalid', { stringCode: 'persistence_unavailable' });
|
|
1135
|
-
try {
|
|
1136
|
-
if (typeof authorization !== 'string' || !/^[A-Za-z0-9_-]+={0,2}$/.test(authorization) || authorization.includes('=') && authorization.length % 4 !== 0)
|
|
1137
|
-
throw invalid;
|
|
1138
|
-
const binary = globalThis.atob(authorization.replace(/-/g, '+').replace(/_/g, '/'));
|
|
1139
|
-
const text = new TextDecoder('utf-8', { fatal: true }).decode(Uint8Array.from(binary, char => char.charCodeAt(0)));
|
|
1140
|
-
if (base64UrlEncode(text) !== authorization.replace(/=+$/, ''))
|
|
1141
|
-
throw invalid;
|
|
1142
|
-
const value = JSON.parse(text);
|
|
1143
|
-
const lexical = parseDocument(text, { uniqueKeys: true });
|
|
1144
|
-
if (lexical.errors.length || lexical.get('version', true)?.source !== '2' || !/^[1-9][0-9]*$/.test(lexical.get('timestamp', true)?.source ?? ''))
|
|
1145
|
-
throw invalid;
|
|
1146
|
-
const fields = ['version', 'target_aid', 'uploader_aid', 'body_sha256', 'entity_cert_fingerprint', 'timestamp', 'nonce', 'cert_fingerprint', 'signature'];
|
|
1147
|
-
if (!isJsonObject(value) || Object.keys(value).length !== fields.length || fields.some(field => !(field in value)) || value.version !== 2)
|
|
1148
|
-
throw invalid;
|
|
1149
|
-
const timestamp = value.timestamp, nonce = value.nonce;
|
|
1150
|
-
if (typeof timestamp !== 'number' || !Number.isSafeInteger(timestamp) || timestamp <= 0 || typeof nonce !== 'string' || !nonce.trim() || new TextEncoder().encode(nonce).length > 128)
|
|
1151
|
-
throw invalid;
|
|
1152
|
-
if (value.target_aid !== target.toLowerCase() || value.uploader_aid !== owner.aid.toLowerCase() || value.body_sha256 !== await sha256Hex(content) || value.entity_cert_fingerprint !== entityFingerprint || value.cert_fingerprint !== owner.certFingerprint || typeof value.signature !== 'string')
|
|
1153
|
-
throw invalid;
|
|
1154
|
-
const verified = await owner.verify(`AUN-AGENTMD-OWNER-AUTH-V2;${value.uploader_aid};${value.target_aid};${value.body_sha256};${entityFingerprint};${timestamp};${nonce}`, value.signature);
|
|
1155
|
-
if (!verified.ok || !verified.data?.valid)
|
|
1156
|
-
throw invalid;
|
|
1157
|
-
return timestamp;
|
|
1158
|
-
}
|
|
1159
|
-
catch {
|
|
1160
|
-
throw invalid;
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
async _ownerUploadAuthorization(target, content, owner, entityFingerprint = '') {
|
|
1312
|
+
async _ownerUploadAuthorization(target, content, owner) {
|
|
1164
1313
|
const bodySha256 = await sha256Hex(content);
|
|
1165
1314
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
1166
1315
|
const nonce = randomOwnerAuthorizationNonce();
|
|
1167
|
-
const canonical =
|
|
1316
|
+
const canonical = `AUN-AGENT-MD-UPLOAD-V1;${owner.aid.toLowerCase()};${target.toLowerCase()};${bodySha256};${timestamp};${nonce}`;
|
|
1168
1317
|
const signed = await owner.sign(canonical);
|
|
1169
1318
|
if (!signed.ok || !signed.data) {
|
|
1170
1319
|
const message = signed.error?.message || 'owner authorization signing failed';
|
|
1171
1320
|
throw new ClientSignatureError(message);
|
|
1172
1321
|
}
|
|
1173
1322
|
return base64UrlEncode(JSON.stringify({
|
|
1174
|
-
version:
|
|
1323
|
+
version: 1,
|
|
1175
1324
|
target_aid: target.toLowerCase(),
|
|
1176
1325
|
uploader_aid: owner.aid.toLowerCase(),
|
|
1177
1326
|
body_sha256: bodySha256,
|
|
1178
|
-
...(entityFingerprint ? { entity_cert_fingerprint: entityFingerprint } : {}),
|
|
1179
1327
|
timestamp,
|
|
1180
1328
|
nonce,
|
|
1181
1329
|
cert_fingerprint: owner.certFingerprint,
|
|
@@ -1185,20 +1333,21 @@ export class AgentMdManager {
|
|
|
1185
1333
|
_url(aid, gatewayUrl) {
|
|
1186
1334
|
return `${agentMdHttpScheme(gatewayUrl)}://${this._safeAid(aid)}/agent.md`;
|
|
1187
1335
|
}
|
|
1188
|
-
async _uploadHttp(aid, content, ownerAuthorization = '', condition) {
|
|
1336
|
+
async _uploadHttp(aid, content, ownerAuthorization = '', condition, legacyBodyUpdate = false) {
|
|
1189
1337
|
if (!condition || Object.keys(condition).length !== 1 || !(typeof condition.if_match === 'string' && condition.if_match || condition.if_none_match === '*'))
|
|
1190
1338
|
throw new StateError('agent.md upload requires its original document condition', { stringCode: 'persistence_unavailable' });
|
|
1191
1339
|
const gatewayUrl = await this._resolveGateway(aid);
|
|
1192
1340
|
const authorization = String(ownerAuthorization ?? '').trim();
|
|
1193
|
-
const
|
|
1341
|
+
const meta = validateAgentMdDocument(content, { expectedAid: aid, validateOwnership: false }).frontmatter;
|
|
1342
|
+
const entity = ['group', 'org'].includes(String(meta.type));
|
|
1194
1343
|
// 浏览器禁止覆盖 Host;动态 AID URL 由浏览器/Docker 的通配解析路由。
|
|
1195
1344
|
const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin('node:http') !== null;
|
|
1196
|
-
const headers = {
|
|
1345
|
+
const headers = { 'X-AUN-Signature-Format': 'multi-v1',
|
|
1197
1346
|
'Content-Type': 'text/markdown; charset=utf-8',
|
|
1198
|
-
...(!entity ? { 'X-AUN-Ownership-Check': '1' } : {}),
|
|
1347
|
+
...(!entity && !(legacyBodyUpdate && this._isLegacyBodyRelation(meta)) ? { 'X-AUN-Ownership-Check': '1' } : {}),
|
|
1199
1348
|
...(condition.if_match ? { 'If-Match': condition.if_match } : { 'If-None-Match': '*' }),
|
|
1200
1349
|
...(viaGatewayHost ? { Host: this._safeAid(aid) } : {}),
|
|
1201
|
-
...(authorization ? {
|
|
1350
|
+
...(!entity && authorization ? { 'X-AUN-AgentMD-Owner-Authorization': authorization } : {}),
|
|
1202
1351
|
};
|
|
1203
1352
|
if (!authorization || entity) {
|
|
1204
1353
|
const token = await this._uploadAccessToken(aid, gatewayUrl);
|
|
@@ -1223,7 +1372,7 @@ export class AgentMdManager {
|
|
|
1223
1372
|
}
|
|
1224
1373
|
let response;
|
|
1225
1374
|
try {
|
|
1226
|
-
response = await fetchWithTimeout(url, { method: 'PUT', headers, body: content });
|
|
1375
|
+
response = await fetchWithTimeout(url, { method: 'PUT', headers, body: content, redirect: 'error' });
|
|
1227
1376
|
}
|
|
1228
1377
|
catch (error) {
|
|
1229
1378
|
if (!viaGatewayHost)
|
|
@@ -1231,12 +1380,12 @@ export class AgentMdManager {
|
|
|
1231
1380
|
const fallbackUrl = nameServiceAgentMdUrl(aid, gatewayUrl);
|
|
1232
1381
|
if (!fallbackUrl || fallbackUrl === url)
|
|
1233
1382
|
throw error;
|
|
1234
|
-
response = await fetchWithTimeout(fallbackUrl, { method: 'PUT', headers, body: content });
|
|
1383
|
+
response = await fetchWithTimeout(fallbackUrl, { method: 'PUT', headers, body: content, redirect: 'error' });
|
|
1235
1384
|
}
|
|
1236
1385
|
if (response.status === 404 && viaGatewayHost) {
|
|
1237
1386
|
const fallbackUrl = nameServiceAgentMdUrl(aid, gatewayUrl);
|
|
1238
1387
|
if (fallbackUrl && fallbackUrl !== url) {
|
|
1239
|
-
response = await fetchWithTimeout(fallbackUrl, { method: 'PUT', headers, body: content });
|
|
1388
|
+
response = await fetchWithTimeout(fallbackUrl, { method: 'PUT', headers, body: content, redirect: 'error' });
|
|
1240
1389
|
}
|
|
1241
1390
|
}
|
|
1242
1391
|
if (response.status === 404) {
|
|
@@ -1260,13 +1409,13 @@ export class AgentMdManager {
|
|
|
1260
1409
|
const cached = await this.loadRecord(target) ?? {};
|
|
1261
1410
|
const gatewayUrl = await this._resolveGateway(target);
|
|
1262
1411
|
const url = this._url(target, gatewayUrl);
|
|
1263
|
-
const headers = { Accept: 'text/markdown' };
|
|
1412
|
+
const headers = { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' };
|
|
1264
1413
|
const cachedEtag = String(cached.remote_etag ?? cached.etag ?? cached.local_etag ?? '').trim();
|
|
1265
1414
|
const cachedLastModified = String(cached.last_modified ?? '').trim();
|
|
1266
1415
|
let response = await fetchWithTimeout(url, {
|
|
1267
1416
|
method: 'GET',
|
|
1268
1417
|
headers,
|
|
1269
|
-
redirect: '
|
|
1418
|
+
redirect: 'error',
|
|
1270
1419
|
}, timeoutMs);
|
|
1271
1420
|
if (response.status === 304 && cached.content !== undefined && cached.content !== null) {
|
|
1272
1421
|
return {
|
|
@@ -1279,8 +1428,8 @@ export class AgentMdManager {
|
|
|
1279
1428
|
if (response.status === 304) {
|
|
1280
1429
|
response = await fetchWithTimeout(url, {
|
|
1281
1430
|
method: 'GET',
|
|
1282
|
-
headers: { Accept: 'text/markdown' },
|
|
1283
|
-
redirect: '
|
|
1431
|
+
headers: { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' },
|
|
1432
|
+
redirect: 'error',
|
|
1284
1433
|
}, timeoutMs);
|
|
1285
1434
|
}
|
|
1286
1435
|
if (response.status === 404) {
|
|
@@ -1310,7 +1459,7 @@ export class AgentMdManager {
|
|
|
1310
1459
|
const gatewayUrl = await this._resolveGateway(target);
|
|
1311
1460
|
const response = await fetchWithTimeout(this._url(target, gatewayUrl), {
|
|
1312
1461
|
method: 'HEAD',
|
|
1313
|
-
headers: { Accept: 'text/markdown' },
|
|
1462
|
+
headers: { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' },
|
|
1314
1463
|
}, HEAD_HTTP_TIMEOUT_MS);
|
|
1315
1464
|
const etag = headerValue(response.headers, 'ETag');
|
|
1316
1465
|
const lastModified = headerValue(response.headers, 'Last-Modified');
|
|
@@ -1405,14 +1554,22 @@ export class AgentMdManager {
|
|
|
1405
1554
|
payload.aid = this._safeAid(aid);
|
|
1406
1555
|
await this._writeStorage(this._metaKey(aid), `${JSON.stringify(payload, null, 2)}\n`, root);
|
|
1407
1556
|
}
|
|
1408
|
-
async _readRecordUnlocked(aid, root = this.root) {
|
|
1409
|
-
const raw = await this._readStorage(this._metaKey(aid), root);
|
|
1410
|
-
if (raw === null)
|
|
1411
|
-
return {};
|
|
1557
|
+
async _readRecordUnlocked(aid, root = this.root, strict = false) {
|
|
1412
1558
|
try {
|
|
1413
|
-
|
|
1559
|
+
const raw = await this._readStorage(this._metaKey(aid), root);
|
|
1560
|
+
if (raw === null)
|
|
1561
|
+
return {};
|
|
1562
|
+
const parsed = JSON.parse(raw);
|
|
1563
|
+
if (strict && !isRecord(parsed))
|
|
1564
|
+
throw new StateError('agent.md metadata is damaged', { stringCode: 'persistence_unavailable' });
|
|
1565
|
+
return this._normalizeRecord(aid, parsed);
|
|
1414
1566
|
}
|
|
1415
1567
|
catch (err) {
|
|
1568
|
+
if (strict) {
|
|
1569
|
+
if (err instanceof StateError)
|
|
1570
|
+
throw err;
|
|
1571
|
+
throw new StateError('agent.md metadata is damaged', { stringCode: 'persistence_unavailable' });
|
|
1572
|
+
}
|
|
1416
1573
|
this._log.warn(`agent.md metadata damaged, ignoring: aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1417
1574
|
return {};
|
|
1418
1575
|
}
|