@agentunion/fastaun-browser 0.5.18 → 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 +172 -79
- package/_packed_docs/CHANGELOG.md +172 -79
- package/_packed_docs/INDEX.md +244 -116
- package/_packed_docs/KITE_DOCS_GUIDE.md +98 -69
- package/_packed_docs/agent.md/SCHEMA.md +245 -205
- 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/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/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1072 -1072
- 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 +687 -668
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +19 -28
- 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 +4 -0
- 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 +68 -61
- 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 +279 -285
- 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 +271 -235
- package/_packed_docs/sdk/09-message-rpc-manual.md +47 -6
- package/_packed_docs/sdk/09-storage-rpc-manual.md +70 -68
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +63 -34
- package/_packed_docs/sdk/INDEX.md +105 -34
- 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 +17 -3
- package/dist/agent-md-schema.d.ts.map +1 -1
- package/dist/agent-md-schema.js +94 -26
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +54 -3
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +711 -65
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +58 -46
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +254 -248
- 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 +27 -33
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts +4 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +426 -39
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +38179 -32054
- package/dist/cert-utils.d.ts +50 -2
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +105 -47
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +22 -1
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +389 -78
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +103 -77
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +1 -0
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +182 -89
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +100 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1220 -25
- 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/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -2
- package/dist/errors.js.map +1 -1
- package/dist/events.d.ts +9 -2
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +72 -13
- package/dist/events.js.map +1 -1
- package/dist/facades.d.ts +82 -2
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +100 -6
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +17 -0
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb-shared.d.ts +10 -1
- package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
- package/dist/keystore/indexeddb-shared.js +111 -0
- package/dist/keystore/indexeddb-shared.js.map +1 -1
- package/dist/keystore/indexeddb-token-store.d.ts +7 -1
- package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-token-store.js +30 -1
- package/dist/keystore/indexeddb-token-store.js.map +1 -1
- package/dist/org-protocol.d.ts +98 -0
- package/dist/org-protocol.d.ts.map +1 -0
- package/dist/org-protocol.js +1802 -0
- package/dist/org-protocol.js.map +1 -0
- package/dist/ownership-proof.d.ts +93 -5
- package/dist/ownership-proof.d.ts.map +1 -1
- package/dist/ownership-proof.js +239 -53
- package/dist/ownership-proof.js.map +1 -1
- package/dist/ownership-v21.d.ts +194 -0
- package/dist/ownership-v21.d.ts.map +1 -0
- package/dist/ownership-v21.js +678 -0
- package/dist/ownership-v21.js.map +1 -0
- package/dist/ownership-workflow.d.ts +151 -0
- package/dist/ownership-workflow.d.ts.map +1 -0
- package/dist/ownership-workflow.js +1229 -0
- package/dist/ownership-workflow.js.map +1 -0
- package/dist/seq-tracker.d.ts +1 -0
- package/dist/seq-tracker.d.ts.map +1 -1
- package/dist/seq-tracker.js +10 -2
- package/dist/seq-tracker.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +83 -0
- package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
- package/dist/tools/cross-sdk-agent.js +415 -7
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +7 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +90 -2
- package/dist/transport.js.map +1 -1
- package/dist/v2/e2ee/decrypt.d.ts +1 -0
- package/dist/v2/e2ee/decrypt.d.ts.map +1 -1
- 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
|
@@ -1,9 +1,43 @@
|
|
|
1
1
|
import { AID } from './aid.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { parseDocument } from 'yaml';
|
|
3
|
+
import { certMatchesFingerprint, normalizeFingerprintHex, parseAgentMdTailSignature, parseAgentMdTailSignatureForRead } from './cert-utils.js';
|
|
4
|
+
import { parseFileFrontmatter, validateAgentMdDocument, validateAgentMdDocumentForRead, verifyAgentMdUpload, verifyAgentMdLegacyBodyUpload } from './agent-md-schema.js';
|
|
5
|
+
import { validateOwnershipFrontmatter, verifyOwnershipFrontmatterStatus } from './ownership-proof.js';
|
|
4
6
|
import { AUNError, ClientSignatureError, NotFoundError, StateError, ValidationError } from './errors.js';
|
|
5
7
|
import { isJsonObject } from './types.js';
|
|
8
|
+
import { ownershipAid, parseOwnershipFile } from './ownership-v21.js';
|
|
6
9
|
const DEFAULT_HTTP_TIMEOUT_MS = 30_000;
|
|
10
|
+
export function buildOwnershipReleaseAgentMd(content, release) {
|
|
11
|
+
return updateOwnershipAgentMd(content, { ownership_release: release });
|
|
12
|
+
}
|
|
13
|
+
export function buildOwnershipAgentMd(content, owner, proof) {
|
|
14
|
+
return updateOwnershipAgentMd(content, { owner, ownership_proof: proof });
|
|
15
|
+
}
|
|
16
|
+
export function buildOwnershipAgentMdV21(content, statement) {
|
|
17
|
+
return updateOwnershipAgentMd(content, statement ? { owner: statement.owner_aid, ownership: { statement } } : {});
|
|
18
|
+
}
|
|
19
|
+
function updateOwnershipAgentMd(content, fields) {
|
|
20
|
+
const payload = parseAgentMdTailSignature(String(content ?? '')).payload;
|
|
21
|
+
const match = /^---(?:\r\n|\n|\r)([\s\S]*?)(?:\r\n|\n|\r)---([\s\S]*)$/.exec(payload);
|
|
22
|
+
if (!match)
|
|
23
|
+
throw new ValidationError('agent.md must start with YAML frontmatter');
|
|
24
|
+
const document = parseDocument(match[1], { uniqueKeys: true });
|
|
25
|
+
if (document.errors.length)
|
|
26
|
+
throw new ValidationError(`agent.md frontmatter is invalid: ${document.errors[0].message}`);
|
|
27
|
+
const frontmatter = document.toJS();
|
|
28
|
+
if (!frontmatter || typeof frontmatter !== 'object' || Array.isArray(frontmatter))
|
|
29
|
+
throw new ValidationError('agent.md frontmatter must be a mapping');
|
|
30
|
+
document.delete('owner');
|
|
31
|
+
document.delete('ownership_proof');
|
|
32
|
+
document.delete('ownership_release');
|
|
33
|
+
document.delete('ownership');
|
|
34
|
+
for (const [name, value] of Object.entries(fields))
|
|
35
|
+
document.set(name, value);
|
|
36
|
+
const yamlText = document.toString().replace(/\r\n/g, '\n');
|
|
37
|
+
const updated = `---\n${yamlText.endsWith('\n') ? yamlText : `${yamlText}\n`}---${match[2]}`;
|
|
38
|
+
validateAgentMdDocument(updated, { expectedAid: String(frontmatter.aid ?? '') });
|
|
39
|
+
return updated;
|
|
40
|
+
}
|
|
7
41
|
export function buildDefaultAgentMd(aid, options = {}) {
|
|
8
42
|
const target = String(aid ?? '').trim();
|
|
9
43
|
const type = String(options.type ?? 'agent').trim() || 'agent';
|
|
@@ -16,6 +50,10 @@ export function buildDefaultAgentMd(aid, options = {}) {
|
|
|
16
50
|
`aid: ${JSON.stringify(target)}`,
|
|
17
51
|
`name: ${JSON.stringify(name)}`,
|
|
18
52
|
`type: ${JSON.stringify(type)}`,
|
|
53
|
+
...(options.exttype ? [`exttype: ${JSON.stringify(options.exttype)}`] : []),
|
|
54
|
+
...(options.owner ? [`owner: ${JSON.stringify(options.owner)}`] : []),
|
|
55
|
+
...(options.entityCertFingerprint ? [`entity_cert_fingerprint: ${JSON.stringify(options.entityCertFingerprint)}`] : []),
|
|
56
|
+
...(options.ownershipProof ? [`ownership_proof: ${JSON.stringify(options.ownershipProof)}`] : []),
|
|
19
57
|
'version: "1.0.0"',
|
|
20
58
|
`description: ${JSON.stringify(description)}`,
|
|
21
59
|
...(avatar ? [`avatar: ${JSON.stringify(avatar)}`] : []),
|
|
@@ -44,6 +82,24 @@ function nodeBuiltin(name) {
|
|
|
44
82
|
return null;
|
|
45
83
|
}
|
|
46
84
|
}
|
|
85
|
+
async function sha256Hex(value) {
|
|
86
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(value));
|
|
87
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
88
|
+
}
|
|
89
|
+
function randomOwnerAuthorizationNonce() {
|
|
90
|
+
if (typeof globalThis.crypto.randomUUID === 'function')
|
|
91
|
+
return globalThis.crypto.randomUUID().replaceAll('-', '');
|
|
92
|
+
const bytes = new Uint8Array(16);
|
|
93
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
94
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
95
|
+
}
|
|
96
|
+
function base64UrlEncode(value) {
|
|
97
|
+
const bytes = new TextEncoder().encode(value);
|
|
98
|
+
let binary = '';
|
|
99
|
+
for (const byte of bytes)
|
|
100
|
+
binary += String.fromCharCode(byte);
|
|
101
|
+
return globalThis.btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
102
|
+
}
|
|
47
103
|
function requestHeaders(init) {
|
|
48
104
|
const out = {};
|
|
49
105
|
try {
|
|
@@ -231,7 +287,11 @@ export class AgentMdManager {
|
|
|
231
287
|
_gatewayResolver;
|
|
232
288
|
_peerResolver;
|
|
233
289
|
_accessTokenResolver;
|
|
290
|
+
_uploadAccessTokenResolver;
|
|
234
291
|
_aidValidator;
|
|
292
|
+
_entityOwnerResolver;
|
|
293
|
+
_entityIdentityGetter;
|
|
294
|
+
_entitySignerResolver;
|
|
235
295
|
_cache = new Map();
|
|
236
296
|
_memoryStorage = new Map();
|
|
237
297
|
_fetchInflight = new Set();
|
|
@@ -250,7 +310,11 @@ export class AgentMdManager {
|
|
|
250
310
|
this._gatewayResolver = opts.gatewayResolver;
|
|
251
311
|
this._peerResolver = opts.peerResolver;
|
|
252
312
|
this._accessTokenResolver = opts.accessTokenResolver;
|
|
313
|
+
this._uploadAccessTokenResolver = opts.uploadAccessTokenResolver;
|
|
253
314
|
this._aidValidator = opts.aidValidator;
|
|
315
|
+
this._entityOwnerResolver = opts.entityOwnerResolver;
|
|
316
|
+
this._entityIdentityGetter = opts.entityIdentityGetter;
|
|
317
|
+
this._entitySignerResolver = opts.entitySignerResolver;
|
|
254
318
|
}
|
|
255
319
|
get root() {
|
|
256
320
|
return this._agentMdPath || this._defaultRoot();
|
|
@@ -259,6 +323,73 @@ export class AgentMdManager {
|
|
|
259
323
|
this._aunPath = String(aunPath ?? '');
|
|
260
324
|
this.setRoot(null);
|
|
261
325
|
}
|
|
326
|
+
async readOwnershipFresh(aid) {
|
|
327
|
+
const target = this._safeAid(aid);
|
|
328
|
+
const gatewayUrl = await this._resolveGateway(target);
|
|
329
|
+
const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin('node:http') !== null;
|
|
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
|
+
let url = this._url(target, gatewayUrl);
|
|
332
|
+
if (viaGatewayHost) {
|
|
333
|
+
const parsed = new URL(gatewayUrl);
|
|
334
|
+
const port = parsed.port && parsed.port !== '443' && parsed.port !== '20001' ? `:${parsed.port}` : '';
|
|
335
|
+
url = `${agentMdHttpScheme(gatewayUrl)}://${parsed.hostname}${port}/agent.md`;
|
|
336
|
+
}
|
|
337
|
+
const read = (targetUrl) => fetchWithTimeout(targetUrl, {
|
|
338
|
+
method: 'GET', cache: 'no-store', redirect: 'error',
|
|
339
|
+
headers,
|
|
340
|
+
});
|
|
341
|
+
let response = await read(url);
|
|
342
|
+
if (response.status === 404 && !headerValue(response.headers, 'X-AUN-Ownership-Protocol') && headerValue(response.headers, 'X-AUN-Ownership-Status') !== 'not_applicable' && viaGatewayHost) {
|
|
343
|
+
const fallback = nameServiceAgentMdUrl(target, gatewayUrl);
|
|
344
|
+
if (fallback && fallback !== url)
|
|
345
|
+
response = await read(fallback);
|
|
346
|
+
}
|
|
347
|
+
if (!response.ok && response.status !== 404)
|
|
348
|
+
throw new StateError(`NS ownership check unavailable: HTTP ${response.status}`, { stringCode: 'unavailable', retryable: true });
|
|
349
|
+
if (headerValue(response.headers, 'X-AUN-Ownership-Status') === 'not_applicable')
|
|
350
|
+
throw new StateError('group/org uses its lifecycle owner relationship', { stringCode: 'not_applicable' });
|
|
351
|
+
const protocol = headerValue(response.headers, 'X-AUN-Ownership-Protocol');
|
|
352
|
+
const version = headerValue(response.headers, 'X-AUN-Ownership-Version');
|
|
353
|
+
const lock = headerValue(response.headers, 'X-AUN-Ownership-Lock');
|
|
354
|
+
const etag = headerValue(response.headers, 'ETag');
|
|
355
|
+
if (protocol !== '2.1')
|
|
356
|
+
throw new StateError('NS ownership protocol metadata unavailable', { stringCode: 'unsupported_protocol', retryable: true });
|
|
357
|
+
if (!/^(0|[1-9][0-9]*)$/.test(version) || !Number.isSafeInteger(Number(version)) || !['locked', 'unlocked'].includes(lock))
|
|
358
|
+
throw new StateError('NS ownership relationship metadata unavailable', { stringCode: 'unavailable', retryable: true });
|
|
359
|
+
if (response.status === 404 && (Number(version) !== 0 || lock !== 'unlocked') || response.ok && !etag)
|
|
360
|
+
throw new StateError('NS ownership document metadata unavailable', { stringCode: 'unavailable', retryable: true });
|
|
361
|
+
let content = '';
|
|
362
|
+
if (response.ok) {
|
|
363
|
+
try {
|
|
364
|
+
content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(await response.arrayBuffer());
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
throw new ValidationError('NS ownership file must be valid UTF-8', { stringCode: 'invalid' });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return { sub_aid: target, exists: response.ok, content, etag, protocol, relation_version: Number(version), lock: lock, observed_at: Math.trunc(Date.now() / 1000) };
|
|
371
|
+
}
|
|
372
|
+
async submitOwnershipRaw(aid, content, condition) {
|
|
373
|
+
const target = this._safeAid(aid);
|
|
374
|
+
if (this._currentAid()?.aid !== target)
|
|
375
|
+
throw new ValidationError('ownership upload requires its own identity', { stringCode: 'forbidden' });
|
|
376
|
+
parseOwnershipFile(content, 'agent', { expectedSubAid: target });
|
|
377
|
+
if (Boolean(condition.if_match) === Boolean(condition.if_none_match === '*'))
|
|
378
|
+
throw new ValidationError('ownership upload requires its original document condition');
|
|
379
|
+
const gatewayUrl = await this._resolveGateway(target);
|
|
380
|
+
const token = await this._uploadAccessToken(target, gatewayUrl);
|
|
381
|
+
const response = await fetchWithTimeout(this._url(target, gatewayUrl), {
|
|
382
|
+
method: 'PUT', cache: 'no-store', redirect: 'error', body: content,
|
|
383
|
+
headers: { 'X-AUN-Signature-Format': 'multi-v1', 'Content-Type': 'text/markdown; charset=utf-8', Authorization: `Bearer ${token}`,
|
|
384
|
+
...(condition.if_match ? { 'If-Match': condition.if_match } : { 'If-None-Match': '*' }) },
|
|
385
|
+
});
|
|
386
|
+
if (!response.ok) {
|
|
387
|
+
const reason = (await response.text()).trim();
|
|
388
|
+
const code = response.status === 409 || response.status === 412 ? 'conflict' : response.status === 401 || response.status === 403 ? 'forbidden' : response.status === 428 ? 'unsupported_protocol' : response.status >= 500 ? 'unavailable' : 'invalid';
|
|
389
|
+
throw new StateError(`ownership agent.md upload failed: HTTP ${response.status}${reason ? ` ${reason}` : ''}`, { stringCode: /owner_locked/.test(reason) ? 'owner_locked' : code, retryable: code === 'unavailable' });
|
|
390
|
+
}
|
|
391
|
+
return await this.readOwnershipFresh(target);
|
|
392
|
+
}
|
|
262
393
|
setRoot(root) {
|
|
263
394
|
this._agentMdPath = String(root ?? '').trim() || this._defaultRoot();
|
|
264
395
|
this._storageGeneration += 1;
|
|
@@ -268,7 +399,7 @@ export class AgentMdManager {
|
|
|
268
399
|
this._snapshotLoads.clear();
|
|
269
400
|
return this._agentMdPath;
|
|
270
401
|
}
|
|
271
|
-
async upload(content
|
|
402
|
+
async upload(content) {
|
|
272
403
|
const target = this._ownerAid();
|
|
273
404
|
const current = this._currentAid();
|
|
274
405
|
if (!target || !current) {
|
|
@@ -277,59 +408,502 @@ export class AgentMdManager {
|
|
|
277
408
|
if (!current.isPrivateKeyValid()) {
|
|
278
409
|
throw new StateError('uploadAgentMd requires loaded AID with a valid private key');
|
|
279
410
|
}
|
|
280
|
-
|
|
281
|
-
const text = String(content ?? '');
|
|
282
|
-
if (text.length === 0)
|
|
283
|
-
throw new ValidationError('uploadAgentMd requires non-empty content');
|
|
284
|
-
validateAgentMdDocument(text, { expectedAid: target });
|
|
285
|
-
await this.saveRecord(target, {
|
|
286
|
-
content: text,
|
|
287
|
-
local_etag: await AgentMdManager.contentEtag(text),
|
|
288
|
-
fetched_at: Date.now(),
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
const localContent = await this.readContent(target);
|
|
411
|
+
const localContent = content ?? (String((await this.loadRecord(target, true))?.local_draft_content ?? '') || await this.readContent(target));
|
|
292
412
|
if (localContent === null || localContent.length === 0) {
|
|
293
413
|
throw new ValidationError('uploadAgentMd requires local agent.md content');
|
|
294
414
|
}
|
|
295
|
-
|
|
296
|
-
|
|
415
|
+
const parsed = this._parseBodyUpdate(localContent, target);
|
|
416
|
+
if (['group', 'org'].includes(String(parsed.frontmatter.type)))
|
|
417
|
+
return await this.uploadAs(target, localContent, current);
|
|
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);
|
|
297
423
|
if (!signedResult.ok) {
|
|
298
424
|
throw new ClientSignatureError(signedResult.error.message || 'agent.md signing failed');
|
|
299
425
|
}
|
|
300
426
|
const signed = signedResult.data.signed;
|
|
301
|
-
await
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
427
|
+
await this._verifyBodyUpload(target, signed, current);
|
|
428
|
+
return await this._uploadPrepared(target, signed, current);
|
|
429
|
+
}
|
|
430
|
+
async uploadAs(targetAid, content, signer) {
|
|
431
|
+
const target = this._safeAid(String(targetAid ?? '').trim());
|
|
432
|
+
const rawContent = String(content ?? '');
|
|
433
|
+
if (!rawContent.trim())
|
|
434
|
+
throw new ValidationError('uploadAgentMd requires non-empty content');
|
|
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
|
+
}
|
|
440
|
+
if (['group', 'org'].includes(String(parsed.frontmatter.type)) && parsed.signature) {
|
|
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');
|
|
454
|
+
}
|
|
455
|
+
if (!signer?.isPrivateKeyValid())
|
|
456
|
+
throw new StateError('uploadAgentMd requires a valid signer identity');
|
|
457
|
+
const signedResult = await signer.signAgentMd(this._isLegacyBodyRelation(parsed.frontmatter) ? parsed.payload : rawContent);
|
|
458
|
+
if (!signedResult.ok)
|
|
459
|
+
throw new ClientSignatureError(signedResult.error.message || 'agent.md signing failed');
|
|
460
|
+
const signed = signedResult.data.signed;
|
|
461
|
+
await this._verifyBodyUpload(target, signed, signer);
|
|
462
|
+
return await this._uploadPrepared(target, signed, signer);
|
|
463
|
+
}
|
|
464
|
+
async pendingUpload(aid) {
|
|
465
|
+
const record = await this.loadRecord(aid);
|
|
466
|
+
if (record?.pending_operation !== 'replace' || !String(record.pending_content ?? '').trim())
|
|
467
|
+
return null;
|
|
468
|
+
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 };
|
|
469
|
+
}
|
|
470
|
+
async resumePendingUpload(aid, signer) {
|
|
471
|
+
const pending = await this.pendingUpload(aid);
|
|
472
|
+
if (!pending)
|
|
473
|
+
return false;
|
|
474
|
+
const condition = pending.condition;
|
|
475
|
+
if (!condition || Object.keys(condition).length !== 1 || !(typeof condition.if_match === 'string' && condition.if_match || condition.if_none_match === '*'))
|
|
476
|
+
throw new StateError('pending agent.md requires its original document condition', { stringCode: 'persistence_unavailable' });
|
|
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' });
|
|
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
|
+
}
|
|
505
|
+
if (['group', 'org'].includes(String(meta.type))) {
|
|
506
|
+
const identity = signer ?? this._currentAid();
|
|
507
|
+
if (!identity)
|
|
508
|
+
throw new StateError('entity upload recovery requires current owner', { stringCode: 'forbidden' });
|
|
509
|
+
const context = await this._entityContext(aid, meta, identity);
|
|
510
|
+
if (!pending.relation?.entity_cert_fingerprint)
|
|
511
|
+
throw new StateError('pending entity upload requires original owner and certificate context', { stringCode: 'persistence_unavailable' });
|
|
512
|
+
if (pending.relation.owner_aid !== context.owner_aid)
|
|
513
|
+
throw new StateError('pending entity upload belongs to a different owner', { stringCode: 'forbidden' });
|
|
514
|
+
if (pending.relation.entity_cert_fingerprint !== context.entity_cert_fingerprint)
|
|
515
|
+
throw new StateError('pending entity upload belongs to an old rekey generation', { stringCode: 'conflict' });
|
|
516
|
+
if (pending.content_etag !== await AgentMdManager.contentEtag(pending.content))
|
|
517
|
+
throw new StateError('pending entity original bytes are corrupt', { stringCode: 'persistence_unavailable' });
|
|
518
|
+
const documentSigner = await this._entityDocumentSigner(aid, pending.content);
|
|
519
|
+
await this._verifyEntityDocument(aid, pending.content);
|
|
520
|
+
const remote = await this._readEntityFresh(aid);
|
|
521
|
+
if (remote.exists && remote.content === pending.content && remote.etag === pending.content_etag) {
|
|
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: '' });
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
|
|
526
|
+
throw new StateError('pending agent.md original document condition changed', { stringCode: 'conflict' });
|
|
527
|
+
await this._uploadPrepared(aid, pending.content, identity, condition);
|
|
528
|
+
return true;
|
|
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
|
+
}
|
|
536
|
+
const remote = await this.readOwnershipFresh(aid);
|
|
537
|
+
const fail = (code, reason, retryable = false) => new StateError(reason, { stringCode: code, retryable });
|
|
538
|
+
const candidate = parseOwnershipFile(pending.content, 'agent', { expectedSubAid: aid });
|
|
539
|
+
const relation = pending.relation && Object.keys(pending.relation).length ? pending.relation : undefined;
|
|
540
|
+
const sameStatement = (first, second) => first == null && second == null || isJsonObject(first) && isJsonObject(second) && ['version', 'declaration_id', 'owner_aid', 'sub_aid', 'relation_version', 'declared_at'].every((field) => first[field] === second[field]);
|
|
541
|
+
if (candidate.statement && !relation)
|
|
542
|
+
throw fail('persistence_unavailable', 'pending ownership body update requires its original relationship context');
|
|
543
|
+
if (relation) {
|
|
544
|
+
if (remote.relation_version !== relation.relation_version || remote.lock !== relation.lock)
|
|
545
|
+
throw fail('conflict', 'pending agent.md original relationship changed');
|
|
546
|
+
if (!sameStatement(candidate.statement, relation.statement))
|
|
547
|
+
throw fail('unsupported_protocol', 'ownership changes require their persisted 2.1 task');
|
|
548
|
+
if (remote.exists && (remote.relation_version > 0 || candidate.statement)) {
|
|
549
|
+
const current = parseOwnershipFile(remote.content, 'agent', { expectedSubAid: aid });
|
|
550
|
+
if (!sameStatement(current.statement, relation.statement) || (remote.owner_aid ?? current.statement?.owner_aid ?? null) !== relation.owner_aid)
|
|
551
|
+
throw fail('conflict', 'pending agent.md relationship statement changed');
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (!relation && (remote.relation_version !== 0 || remote.lock !== 'unlocked'))
|
|
555
|
+
throw fail('unavailable', 'pending agent.md cannot overwrite relationship history', true);
|
|
556
|
+
if (remote.exists && remote.etag === pending.content_etag && remote.content === pending.content) {
|
|
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: '' });
|
|
558
|
+
if (!saved.aid)
|
|
559
|
+
throw new StateError('agent.md pending completion persistence failed');
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
|
|
563
|
+
throw new StateError('pending agent.md original document condition changed', { stringCode: 'conflict' });
|
|
564
|
+
if (!identity)
|
|
565
|
+
throw new StateError('pending agent.md requires a signer identity');
|
|
566
|
+
if (pendingDocument.signatures.length <= 1)
|
|
567
|
+
await this._verifyBodyUpload(aid, pending.content, identity);
|
|
568
|
+
await this._uploadPrepared(aid, pending.content, identity, condition);
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
catch (error) {
|
|
572
|
+
await this.saveRecord(aid, { pending_error: error instanceof Error ? error.message : String(error) });
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
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
|
+
}
|
|
644
|
+
async _uploadPrepared(target, signed, signer, originalCondition) {
|
|
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
|
+
}
|
|
655
|
+
const entity = ['group', 'org'].includes(String(meta.type));
|
|
656
|
+
const uploader = entity ? this._currentAid() ?? signer : signer;
|
|
657
|
+
const entityContext = entity ? await this._entityContext(target, meta, uploader) : undefined;
|
|
658
|
+
if (entity)
|
|
659
|
+
await this._verifyEntityDocument(target, signed);
|
|
660
|
+
let condition = originalCondition;
|
|
661
|
+
let relation = entityContext ?? (await this.pendingUpload(target))?.relation;
|
|
662
|
+
if (!condition) {
|
|
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 {
|
|
692
|
+
const authority = fresh;
|
|
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
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
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);
|
|
707
|
+
}
|
|
708
|
+
const entityAuthorization = '';
|
|
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' });
|
|
712
|
+
const savedCondition = pending.pending_condition;
|
|
713
|
+
if (pending.pending_content !== signed || savedCondition?.if_match !== condition.if_match || savedCondition?.if_none_match !== condition.if_none_match || pending.pending_authorization !== entityAuthorization)
|
|
714
|
+
throw new StateError('agent.md pending upload persistence failed');
|
|
715
|
+
try {
|
|
716
|
+
const owner = this._currentAid();
|
|
717
|
+
const delegated = target.toLowerCase() !== this._ownerAid().toLowerCase();
|
|
718
|
+
if (delegated && !owner?.isPrivateKeyValid())
|
|
719
|
+
throw new StateError('owner agent.md upload requires a loaded owner identity');
|
|
720
|
+
const authorization = entity ? '' : delegated ? await this._ownerUploadAuthorization(target, signed, owner) : '';
|
|
721
|
+
const result = await this._uploadHttp(target, signed, authorization, condition, relation?.protocol === 'legacy');
|
|
722
|
+
const remoteEtag = String(result.etag ?? '').trim();
|
|
723
|
+
const contentEtag = await AgentMdManager.contentEtag(signed);
|
|
724
|
+
const saved = await this.saveRecord(target, {
|
|
725
|
+
content: signed, local_etag: contentEtag, remote_etag: remoteEtag || undefined, ...(remoteEtag === contentEtag ? { local_draft_content: '' } : {}),
|
|
726
|
+
last_modified: String(result.last_modified ?? result.lastModified ?? '').trim(), fetched_at: Date.now(), checked_at: Date.now(),
|
|
727
|
+
remote_status: remoteEtag ? 'found' : 'unknown', last_error: '',
|
|
728
|
+
pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '',
|
|
729
|
+
});
|
|
730
|
+
if (!saved.aid)
|
|
731
|
+
throw new StateError('agent.md upload completion persistence failed');
|
|
732
|
+
return { ...result };
|
|
733
|
+
}
|
|
734
|
+
catch (error) {
|
|
735
|
+
await this.saveRecord(target, { pending_error: error instanceof Error ? error.message : String(error) });
|
|
736
|
+
throw error;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
async readEntityAuthorityFresh(target) {
|
|
740
|
+
const fresh = await this._readEntityFresh(this._safeAid(target));
|
|
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 ?? ''))
|
|
742
|
+
throw new StateError('entity current owner authority is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
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
|
+
}
|
|
749
|
+
}
|
|
750
|
+
async _readEntityFresh(target) {
|
|
751
|
+
const gateway = await this._resolveGateway(target);
|
|
752
|
+
const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin('node:http') !== null;
|
|
753
|
+
const headers = { 'X-AUN-Signature-Format': 'multi-v1', 'X-AUN-Ownership-Check': '1', 'Cache-Control': 'no-cache', ...(viaGatewayHost ? { Host: target } : {}) };
|
|
754
|
+
let url = this._url(target, gateway);
|
|
755
|
+
if (viaGatewayHost) {
|
|
756
|
+
const parsed = new URL(gateway);
|
|
757
|
+
const port = parsed.port && parsed.port !== '443' && parsed.port !== '20001' ? `:${parsed.port}` : '';
|
|
758
|
+
url = `${agentMdHttpScheme(gateway)}://${parsed.hostname}${port}/agent.md`;
|
|
759
|
+
}
|
|
760
|
+
let response;
|
|
761
|
+
try {
|
|
762
|
+
response = await fetchWithTimeout(url, { method: 'GET', cache: 'no-store', redirect: 'error', headers });
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
throw new StateError(`entity agent.md fresh read unavailable: ${String(error)}`, { stringCode: 'unavailable', retryable: true });
|
|
766
|
+
}
|
|
767
|
+
if (response.status !== 200 && response.status !== 404)
|
|
768
|
+
throw new StateError(`entity agent.md fresh read failed: HTTP ${response.status}`, { stringCode: 'unavailable', retryable: true });
|
|
769
|
+
const etag = headerValue(response.headers, 'ETag');
|
|
770
|
+
if (response.status === 200 && !etag)
|
|
771
|
+
throw new StateError('entity agent.md fresh read has no ETag', { stringCode: 'unavailable', retryable: true });
|
|
772
|
+
let content = '';
|
|
773
|
+
if (response.status === 200) {
|
|
774
|
+
try {
|
|
775
|
+
content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(await response.arrayBuffer());
|
|
776
|
+
}
|
|
777
|
+
catch {
|
|
778
|
+
throw new ValidationError('entity agent.md must be valid UTF-8', { stringCode: 'invalid' });
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
const facts = {};
|
|
782
|
+
for (const [field, header] of [['ownership_status', 'X-AUN-Ownership-Status'], ['entity_status', 'X-AUN-Entity-Verification-Status'], ['owner_aid', 'X-AUN-Entity-Owner-AID'], ['entity_cert_fingerprint', 'X-AUN-Entity-Cert-Fingerprint']]) {
|
|
783
|
+
const value = headerValue(response.headers, header);
|
|
784
|
+
if (value)
|
|
785
|
+
facts[field] = value;
|
|
786
|
+
}
|
|
787
|
+
return { exists: response.status === 200, content, etag, ...facts };
|
|
788
|
+
}
|
|
789
|
+
async _entityCurrentContext(target, meta, uploading = false) {
|
|
790
|
+
const owner = String(meta.owner ?? '').trim().toLowerCase();
|
|
791
|
+
if (!this._entityOwnerResolver)
|
|
792
|
+
throw new StateError('entity owner resolver is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
793
|
+
const freshOwner = String(await this._entityOwnerResolver(target, String(meta.type))).trim().toLowerCase();
|
|
794
|
+
if (!freshOwner)
|
|
795
|
+
throw new StateError('entity current owner is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
796
|
+
if (freshOwner !== owner)
|
|
797
|
+
throw new StateError('entity current owner changed', { stringCode: uploading ? 'forbidden' : 'conflict' });
|
|
798
|
+
if (!this._entityIdentityGetter)
|
|
799
|
+
throw new StateError('entity current certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
800
|
+
const entity = await this._entityIdentityGetter(target, String(meta.type));
|
|
801
|
+
if (!entity.certFingerprint)
|
|
802
|
+
throw new StateError('entity current certificate is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
803
|
+
if (meta.entity_cert_fingerprint !== entity.certFingerprint)
|
|
804
|
+
throw new StateError('entity agent.md certificate generation changed', { stringCode: 'conflict' });
|
|
805
|
+
return { type: meta.type, owner_aid: owner, entity_cert_fingerprint: entity.certFingerprint };
|
|
806
|
+
}
|
|
807
|
+
async _entityContext(target, meta, signer) {
|
|
808
|
+
const owner = String(meta.owner ?? '').trim().toLowerCase();
|
|
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' });
|
|
811
|
+
const context = await this._entityCurrentContext(target, meta, true);
|
|
812
|
+
if (signer.aid.toLowerCase() === owner)
|
|
813
|
+
await this._verifyEntityOwnerSigner(signer);
|
|
814
|
+
else if (signer.certFingerprint !== context.entity_cert_fingerprint)
|
|
815
|
+
throw new StateError('entity uploader certificate generation changed', { stringCode: 'conflict' });
|
|
816
|
+
return context;
|
|
817
|
+
}
|
|
818
|
+
async _entityDocumentSigner(target, content) {
|
|
819
|
+
const parsed = validateAgentMdDocument(content, { expectedAid: target, requireSignature: true });
|
|
820
|
+
const signerAid = parsed.signature.signer_aid ?? target;
|
|
821
|
+
let signer;
|
|
822
|
+
if (signerAid.toLowerCase() === target.toLowerCase()) {
|
|
823
|
+
if (!this._entityIdentityGetter)
|
|
824
|
+
throw new StateError('entity current certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
825
|
+
signer = await this._entityIdentityGetter(target, String(parsed.frontmatter.type));
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
if (!this._entitySignerResolver)
|
|
829
|
+
throw new StateError('entity owner signing certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
830
|
+
signer = await this._entitySignerResolver(signerAid, parsed.signature.cert_fingerprint);
|
|
831
|
+
}
|
|
832
|
+
if (signer.aid.toLowerCase() !== signerAid.toLowerCase() || signer.certFingerprint !== parsed.signature.cert_fingerprint)
|
|
833
|
+
throw new StateError('entity signing certificate changed', { stringCode: 'conflict' });
|
|
834
|
+
return signer;
|
|
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;
|
|
314
845
|
});
|
|
315
|
-
|
|
846
|
+
}
|
|
847
|
+
async _verifyEntityOwnerSigner(owner) {
|
|
848
|
+
if (!this._entitySignerResolver)
|
|
849
|
+
throw new StateError('entity owner signing certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
850
|
+
const fresh = await this._entitySignerResolver(owner.aid, owner.certFingerprint);
|
|
851
|
+
if (fresh.aid.toLowerCase() !== owner.aid.toLowerCase() || fresh.certFingerprint !== owner.certFingerprint) {
|
|
852
|
+
throw new StateError('entity owner signing certificate changed', { stringCode: 'conflict' });
|
|
853
|
+
}
|
|
316
854
|
}
|
|
317
855
|
async download(aid, timeoutMs = DEFAULT_HTTP_TIMEOUT_MS) {
|
|
318
856
|
const target = this._safeAid(String(aid ?? this._ownerAid() ?? '').trim());
|
|
319
857
|
const contentResult = await this._downloadHttp(target, timeoutMs);
|
|
320
|
-
const parsed =
|
|
858
|
+
const parsed = parseAgentMdTailSignatureForRead(contentResult.content);
|
|
859
|
+
let entity = false;
|
|
860
|
+
try {
|
|
861
|
+
entity = ['group', 'org'].includes(String(parseFileFrontmatter(parsed.payload, false, false).type));
|
|
862
|
+
}
|
|
863
|
+
catch { }
|
|
864
|
+
let frontmatter;
|
|
865
|
+
try {
|
|
866
|
+
frontmatter = validateAgentMdDocumentForRead(contentResult.content, { expectedAid: target, validateOwnership: false, ...(entity ? { requireSignature: true } : {}) }).frontmatter;
|
|
867
|
+
}
|
|
868
|
+
catch (error) {
|
|
869
|
+
if (entity)
|
|
870
|
+
throw new ValidationError(error instanceof Error ? error.message : String(error), { stringCode: 'invalid' });
|
|
871
|
+
throw error;
|
|
872
|
+
}
|
|
321
873
|
const expectedFp = parsed.fields?.cert_fingerprint || parsed.fields?.public_key_fingerprint || null;
|
|
322
|
-
const
|
|
323
|
-
const
|
|
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);
|
|
324
877
|
if (!verified.ok)
|
|
325
878
|
throw new AUNError(verified.error.message);
|
|
326
879
|
const signature = { ...verified.data };
|
|
327
880
|
const statusText = String(signature.status ?? 'invalid');
|
|
328
881
|
const reason = String(signature.reason ?? '').trim();
|
|
329
882
|
if (statusText === 'invalid') {
|
|
330
|
-
throw new ClientSignatureError(`agent.md verification failed for ${target}${reason ? `: ${reason}` : ''}
|
|
883
|
+
throw new ClientSignatureError(`agent.md verification failed for ${target}${reason ? `: ${reason}` : ''}`, entity ? { stringCode: 'invalid' } : undefined);
|
|
884
|
+
}
|
|
885
|
+
let ownershipStatus;
|
|
886
|
+
if (entity)
|
|
887
|
+
ownershipStatus = 'not_applicable';
|
|
888
|
+
else {
|
|
889
|
+
try {
|
|
890
|
+
validateOwnershipFrontmatter(frontmatter, target);
|
|
891
|
+
const rawProof = (frontmatter.ownership_proof ?? {});
|
|
892
|
+
const subjectSignature = rawProof.subject_signature;
|
|
893
|
+
const ownerSignature = rawProof.owner_signature;
|
|
894
|
+
const release = frontmatter.ownership_release;
|
|
895
|
+
const subjectCertPem = subjectSignature ? (await this._resolvePeer(target, String(subjectSignature.cert_fingerprint), isJsonObject(subjectSignature.statement) ? Number(subjectSignature.statement.signed_at) : undefined)).certPem : undefined;
|
|
896
|
+
const ownerAid = String(ownerSignature ? frontmatter.owner : release?.statement && typeof release.statement === 'object' ? release.statement.owner_aid : '');
|
|
897
|
+
const ownerFingerprint = String(ownerSignature?.cert_fingerprint ?? release?.cert_fingerprint ?? '');
|
|
898
|
+
const ownerStatement = (ownerSignature ?? release)?.statement;
|
|
899
|
+
const ownerCertPem = ownerFingerprint ? (await this._resolvePeer(ownerAid, ownerFingerprint, isJsonObject(ownerStatement) ? Number(ownerStatement.signed_at) : undefined)).certPem : undefined;
|
|
900
|
+
ownershipStatus = await verifyOwnershipFrontmatterStatus(frontmatter, subjectCertPem, ownerCertPem);
|
|
901
|
+
}
|
|
902
|
+
catch {
|
|
903
|
+
ownershipStatus = 'invalid';
|
|
904
|
+
}
|
|
331
905
|
}
|
|
332
|
-
const verification = { status: statusText };
|
|
906
|
+
const verification = { status: statusText, ownership_status: ownershipStatus };
|
|
333
907
|
if (reason)
|
|
334
908
|
verification.reason = reason;
|
|
335
909
|
const localEtag = await AgentMdManager.contentEtag(contentResult.content);
|
|
@@ -557,7 +1131,7 @@ export class AgentMdManager {
|
|
|
557
1131
|
const root = this.root;
|
|
558
1132
|
await this._writeStorage(this._contentKey(aid), String(content ?? ''), root);
|
|
559
1133
|
}
|
|
560
|
-
async loadRecord(aid) {
|
|
1134
|
+
async loadRecord(aid, strict = false) {
|
|
561
1135
|
const target = String(aid ?? '').trim();
|
|
562
1136
|
if (!target)
|
|
563
1137
|
return null;
|
|
@@ -565,7 +1139,7 @@ export class AgentMdManager {
|
|
|
565
1139
|
const generation = this._storageGeneration;
|
|
566
1140
|
try {
|
|
567
1141
|
const loaded = await this._withLock(async () => {
|
|
568
|
-
const record = await this._readRecordUnlocked(target, root);
|
|
1142
|
+
const record = await this._readRecordUnlocked(target, root, strict);
|
|
569
1143
|
const next = Object.keys(record).length > 0 ? { ...record, aid: target } : { aid: target };
|
|
570
1144
|
try {
|
|
571
1145
|
const content = await this._readStorage(this._contentKey(target), root);
|
|
@@ -591,6 +1165,8 @@ export class AgentMdManager {
|
|
|
591
1165
|
return { ...loaded };
|
|
592
1166
|
}
|
|
593
1167
|
catch (err) {
|
|
1168
|
+
if (strict)
|
|
1169
|
+
throw err;
|
|
594
1170
|
this._log.debug(`agent.md cache load skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
595
1171
|
return null;
|
|
596
1172
|
}
|
|
@@ -601,10 +1177,12 @@ export class AgentMdManager {
|
|
|
601
1177
|
return {};
|
|
602
1178
|
const root = this.root;
|
|
603
1179
|
const generation = this._storageGeneration;
|
|
1180
|
+
let strictDraft = false;
|
|
604
1181
|
try {
|
|
605
1182
|
const inputFields = { ...fields };
|
|
1183
|
+
strictDraft = typeof inputFields.local_draft_content === 'string';
|
|
606
1184
|
const hasContent = Object.prototype.hasOwnProperty.call(inputFields, 'content') && inputFields.content !== undefined && inputFields.content !== null;
|
|
607
|
-
if (hasContent) {
|
|
1185
|
+
if (hasContent && !strictDraft) {
|
|
608
1186
|
const text = String(inputFields.content ?? '');
|
|
609
1187
|
await this._writeStorage(this._contentKey(target), text, root);
|
|
610
1188
|
if (!inputFields.local_etag)
|
|
@@ -614,8 +1192,18 @@ export class AgentMdManager {
|
|
|
614
1192
|
}
|
|
615
1193
|
delete inputFields.content;
|
|
616
1194
|
const record = await this._withLock(async () => {
|
|
617
|
-
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
|
+
}
|
|
618
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;
|
|
619
1207
|
if (value !== undefined && value !== null)
|
|
620
1208
|
next[key] = value;
|
|
621
1209
|
}
|
|
@@ -631,6 +1219,8 @@ export class AgentMdManager {
|
|
|
631
1219
|
return { ...loaded };
|
|
632
1220
|
}
|
|
633
1221
|
catch (err) {
|
|
1222
|
+
if (strictDraft)
|
|
1223
|
+
throw err;
|
|
634
1224
|
this._log.debug(`agent.md cache save skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
635
1225
|
return {};
|
|
636
1226
|
}
|
|
@@ -690,17 +1280,20 @@ export class AgentMdManager {
|
|
|
690
1280
|
async _resolveGateway(aid) {
|
|
691
1281
|
return String(await this._gatewayResolver?.(aid) ?? '');
|
|
692
1282
|
}
|
|
693
|
-
async _resolvePeer(aid, certFingerprint) {
|
|
694
|
-
const
|
|
1283
|
+
async _resolvePeer(aid, certFingerprint, signedAt) {
|
|
1284
|
+
const normalizedFp = normalizeFingerprintHex(certFingerprint);
|
|
1285
|
+
const expectedFp = normalizedFp ? `sha256:${normalizedFp}` : String(certFingerprint ?? '').trim().toLowerCase();
|
|
695
1286
|
const current = this._currentAid();
|
|
696
1287
|
if (current && current.aid.toLowerCase() === aid.toLowerCase()) {
|
|
697
1288
|
if (!expectedFp || await certMatchesFingerprint(current.certPem, expectedFp))
|
|
698
1289
|
return current;
|
|
699
1290
|
}
|
|
700
|
-
const peer = await this._peerResolver?.(aid, expectedFp || null);
|
|
1291
|
+
const peer = await this._peerResolver?.(aid, expectedFp || null, signedAt);
|
|
701
1292
|
if (!(peer instanceof AID)) {
|
|
702
1293
|
throw new StateError(`agent.md peer resolver did not return AID for ${aid}`);
|
|
703
1294
|
}
|
|
1295
|
+
if (peer.aid.toLowerCase() !== aid.toLowerCase())
|
|
1296
|
+
throw new ClientSignatureError(`agent.md signer does not match requested AID: ${aid}`, { stringCode: 'invalid' });
|
|
704
1297
|
return peer;
|
|
705
1298
|
}
|
|
706
1299
|
async _accessToken(aid, gatewayUrl) {
|
|
@@ -709,22 +1302,57 @@ export class AgentMdManager {
|
|
|
709
1302
|
throw new StateError('authenticate did not return access_token');
|
|
710
1303
|
return token;
|
|
711
1304
|
}
|
|
1305
|
+
async _uploadAccessToken(aid, gatewayUrl) {
|
|
1306
|
+
const resolver = this._uploadAccessTokenResolver ?? this._accessTokenResolver;
|
|
1307
|
+
const token = String(await resolver?.(aid, gatewayUrl) ?? '').trim();
|
|
1308
|
+
if (!token)
|
|
1309
|
+
throw new StateError('authenticate did not return access_token');
|
|
1310
|
+
return token;
|
|
1311
|
+
}
|
|
1312
|
+
async _ownerUploadAuthorization(target, content, owner) {
|
|
1313
|
+
const bodySha256 = await sha256Hex(content);
|
|
1314
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
1315
|
+
const nonce = randomOwnerAuthorizationNonce();
|
|
1316
|
+
const canonical = `AUN-AGENT-MD-UPLOAD-V1;${owner.aid.toLowerCase()};${target.toLowerCase()};${bodySha256};${timestamp};${nonce}`;
|
|
1317
|
+
const signed = await owner.sign(canonical);
|
|
1318
|
+
if (!signed.ok || !signed.data) {
|
|
1319
|
+
const message = signed.error?.message || 'owner authorization signing failed';
|
|
1320
|
+
throw new ClientSignatureError(message);
|
|
1321
|
+
}
|
|
1322
|
+
return base64UrlEncode(JSON.stringify({
|
|
1323
|
+
version: 1,
|
|
1324
|
+
target_aid: target.toLowerCase(),
|
|
1325
|
+
uploader_aid: owner.aid.toLowerCase(),
|
|
1326
|
+
body_sha256: bodySha256,
|
|
1327
|
+
timestamp,
|
|
1328
|
+
nonce,
|
|
1329
|
+
cert_fingerprint: owner.certFingerprint,
|
|
1330
|
+
signature: signed.data.signature,
|
|
1331
|
+
}));
|
|
1332
|
+
}
|
|
712
1333
|
_url(aid, gatewayUrl) {
|
|
713
1334
|
return `${agentMdHttpScheme(gatewayUrl)}://${this._safeAid(aid)}/agent.md`;
|
|
714
1335
|
}
|
|
715
|
-
async _uploadHttp(aid, content,
|
|
1336
|
+
async _uploadHttp(aid, content, ownerAuthorization = '', condition, legacyBodyUpdate = false) {
|
|
1337
|
+
if (!condition || Object.keys(condition).length !== 1 || !(typeof condition.if_match === 'string' && condition.if_match || condition.if_none_match === '*'))
|
|
1338
|
+
throw new StateError('agent.md upload requires its original document condition', { stringCode: 'persistence_unavailable' });
|
|
716
1339
|
const gatewayUrl = await this._resolveGateway(aid);
|
|
717
|
-
const
|
|
1340
|
+
const authorization = String(ownerAuthorization ?? '').trim();
|
|
1341
|
+
const meta = validateAgentMdDocument(content, { expectedAid: aid, validateOwnership: false }).frontmatter;
|
|
1342
|
+
const entity = ['group', 'org'].includes(String(meta.type));
|
|
718
1343
|
// 浏览器禁止覆盖 Host;动态 AID URL 由浏览器/Docker 的通配解析路由。
|
|
719
1344
|
const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin('node:http') !== null;
|
|
720
|
-
const headers = {
|
|
721
|
-
Authorization: `Bearer ${token}`,
|
|
1345
|
+
const headers = { 'X-AUN-Signature-Format': 'multi-v1',
|
|
722
1346
|
'Content-Type': 'text/markdown; charset=utf-8',
|
|
1347
|
+
...(!entity && !(legacyBodyUpdate && this._isLegacyBodyRelation(meta)) ? { 'X-AUN-Ownership-Check': '1' } : {}),
|
|
1348
|
+
...(condition.if_match ? { 'If-Match': condition.if_match } : { 'If-None-Match': '*' }),
|
|
723
1349
|
...(viaGatewayHost ? { Host: this._safeAid(aid) } : {}),
|
|
1350
|
+
...(!entity && authorization ? { 'X-AUN-AgentMD-Owner-Authorization': authorization } : {}),
|
|
724
1351
|
};
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
headers
|
|
1352
|
+
if (!authorization || entity) {
|
|
1353
|
+
const token = await this._uploadAccessToken(aid, gatewayUrl);
|
|
1354
|
+
headers.Authorization = `Bearer ${token}`;
|
|
1355
|
+
}
|
|
728
1356
|
let url = this._url(aid, gatewayUrl);
|
|
729
1357
|
if (viaGatewayHost) {
|
|
730
1358
|
try {
|
|
@@ -744,7 +1372,7 @@ export class AgentMdManager {
|
|
|
744
1372
|
}
|
|
745
1373
|
let response;
|
|
746
1374
|
try {
|
|
747
|
-
response = await fetchWithTimeout(url, { method: 'PUT', headers, body: content });
|
|
1375
|
+
response = await fetchWithTimeout(url, { method: 'PUT', headers, body: content, redirect: 'error' });
|
|
748
1376
|
}
|
|
749
1377
|
catch (error) {
|
|
750
1378
|
if (!viaGatewayHost)
|
|
@@ -752,12 +1380,12 @@ export class AgentMdManager {
|
|
|
752
1380
|
const fallbackUrl = nameServiceAgentMdUrl(aid, gatewayUrl);
|
|
753
1381
|
if (!fallbackUrl || fallbackUrl === url)
|
|
754
1382
|
throw error;
|
|
755
|
-
response = await fetchWithTimeout(fallbackUrl, { method: 'PUT', headers, body: content });
|
|
1383
|
+
response = await fetchWithTimeout(fallbackUrl, { method: 'PUT', headers, body: content, redirect: 'error' });
|
|
756
1384
|
}
|
|
757
1385
|
if (response.status === 404 && viaGatewayHost) {
|
|
758
1386
|
const fallbackUrl = nameServiceAgentMdUrl(aid, gatewayUrl);
|
|
759
1387
|
if (fallbackUrl && fallbackUrl !== url) {
|
|
760
|
-
response = await fetchWithTimeout(fallbackUrl, { method: 'PUT', headers, body: content });
|
|
1388
|
+
response = await fetchWithTimeout(fallbackUrl, { method: 'PUT', headers, body: content, redirect: 'error' });
|
|
761
1389
|
}
|
|
762
1390
|
}
|
|
763
1391
|
if (response.status === 404) {
|
|
@@ -765,6 +1393,10 @@ export class AgentMdManager {
|
|
|
765
1393
|
}
|
|
766
1394
|
if (!response.ok) {
|
|
767
1395
|
const message = (await response.text()).trim();
|
|
1396
|
+
if (entity) {
|
|
1397
|
+
const code = response.status === 409 || response.status === 412 ? 'conflict' : response.status === 401 || response.status === 403 ? 'forbidden' : response.status === 428 ? 'unsupported_protocol' : response.status >= 500 ? 'unavailable' : 'invalid';
|
|
1398
|
+
throw new StateError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`, { stringCode: code, retryable: code === 'unavailable' });
|
|
1399
|
+
}
|
|
768
1400
|
throw new AUNError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
|
|
769
1401
|
}
|
|
770
1402
|
const payload = await response.json();
|
|
@@ -777,13 +1409,13 @@ export class AgentMdManager {
|
|
|
777
1409
|
const cached = await this.loadRecord(target) ?? {};
|
|
778
1410
|
const gatewayUrl = await this._resolveGateway(target);
|
|
779
1411
|
const url = this._url(target, gatewayUrl);
|
|
780
|
-
const headers = { Accept: 'text/markdown' };
|
|
1412
|
+
const headers = { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' };
|
|
781
1413
|
const cachedEtag = String(cached.remote_etag ?? cached.etag ?? cached.local_etag ?? '').trim();
|
|
782
1414
|
const cachedLastModified = String(cached.last_modified ?? '').trim();
|
|
783
1415
|
let response = await fetchWithTimeout(url, {
|
|
784
1416
|
method: 'GET',
|
|
785
1417
|
headers,
|
|
786
|
-
redirect: '
|
|
1418
|
+
redirect: 'error',
|
|
787
1419
|
}, timeoutMs);
|
|
788
1420
|
if (response.status === 304 && cached.content !== undefined && cached.content !== null) {
|
|
789
1421
|
return {
|
|
@@ -796,8 +1428,8 @@ export class AgentMdManager {
|
|
|
796
1428
|
if (response.status === 304) {
|
|
797
1429
|
response = await fetchWithTimeout(url, {
|
|
798
1430
|
method: 'GET',
|
|
799
|
-
headers: { Accept: 'text/markdown' },
|
|
800
|
-
redirect: '
|
|
1431
|
+
headers: { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' },
|
|
1432
|
+
redirect: 'error',
|
|
801
1433
|
}, timeoutMs);
|
|
802
1434
|
}
|
|
803
1435
|
if (response.status === 404) {
|
|
@@ -806,9 +1438,15 @@ export class AgentMdManager {
|
|
|
806
1438
|
}
|
|
807
1439
|
if (!response.ok) {
|
|
808
1440
|
const message = (await response.text()).trim();
|
|
809
|
-
throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}
|
|
1441
|
+
throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`, response.status >= 500 ? { stringCode: 'unavailable', retryable: true } : undefined);
|
|
1442
|
+
}
|
|
1443
|
+
let content;
|
|
1444
|
+
try {
|
|
1445
|
+
content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(await response.arrayBuffer());
|
|
1446
|
+
}
|
|
1447
|
+
catch {
|
|
1448
|
+
throw new ValidationError('agent.md must be valid UTF-8', { stringCode: 'invalid' });
|
|
810
1449
|
}
|
|
811
|
-
const content = await response.text();
|
|
812
1450
|
return {
|
|
813
1451
|
content,
|
|
814
1452
|
etag: headerValue(response.headers, 'ETag') || cachedEtag,
|
|
@@ -821,7 +1459,7 @@ export class AgentMdManager {
|
|
|
821
1459
|
const gatewayUrl = await this._resolveGateway(target);
|
|
822
1460
|
const response = await fetchWithTimeout(this._url(target, gatewayUrl), {
|
|
823
1461
|
method: 'HEAD',
|
|
824
|
-
headers: { Accept: 'text/markdown' },
|
|
1462
|
+
headers: { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' },
|
|
825
1463
|
}, HEAD_HTTP_TIMEOUT_MS);
|
|
826
1464
|
const etag = headerValue(response.headers, 'ETag');
|
|
827
1465
|
const lastModified = headerValue(response.headers, 'Last-Modified');
|
|
@@ -916,14 +1554,22 @@ export class AgentMdManager {
|
|
|
916
1554
|
payload.aid = this._safeAid(aid);
|
|
917
1555
|
await this._writeStorage(this._metaKey(aid), `${JSON.stringify(payload, null, 2)}\n`, root);
|
|
918
1556
|
}
|
|
919
|
-
async _readRecordUnlocked(aid, root = this.root) {
|
|
920
|
-
const raw = await this._readStorage(this._metaKey(aid), root);
|
|
921
|
-
if (raw === null)
|
|
922
|
-
return {};
|
|
1557
|
+
async _readRecordUnlocked(aid, root = this.root, strict = false) {
|
|
923
1558
|
try {
|
|
924
|
-
|
|
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);
|
|
925
1566
|
}
|
|
926
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
|
+
}
|
|
927
1573
|
this._log.warn(`agent.md metadata damaged, ignoring: aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
928
1574
|
return {};
|
|
929
1575
|
}
|