@agentunion/fastaun-browser 0.5.17 → 0.5.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -0
- package/_packed_docs/CHANGELOG.md +83 -0
- package/_packed_docs/INDEX.md +163 -16
- package/_packed_docs/KITE_DOCS_GUIDE.md +52 -20
- package/_packed_docs/agent.md/SCHEMA.md +266 -207
- 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 +64 -31
- package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +8 -6
- 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 +39 -39
- 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 +1 -1
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +28 -9
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +20 -2
- package/_packed_docs/protocol/agent.md/SCHEMA.md +4 -0
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +5 -2
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +246 -241
- package/_packed_docs/sdk/09-group-rpc-manual.md +46 -14
- package/_packed_docs/sdk/09-message-rpc-manual.md +40 -2
- package/_packed_docs/sdk/09-storage-rpc-manual.md +69 -38
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +25 -5
- package/_packed_docs/sdk/INDEX.md +78 -18
- package/dist/agent-md-schema.d.ts +3 -0
- package/dist/agent-md-schema.d.ts.map +1 -1
- package/dist/agent-md-schema.js +46 -10
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +44 -1
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +518 -26
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +12 -1
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +113 -18
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +17 -1
- package/dist/aid.d.ts.map +1 -1
- package/dist/aid.js +69 -9
- 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 +343 -23
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +37513 -31501
- package/dist/cert-utils.d.ts +1 -1
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +24 -10
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +16 -1
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +198 -58
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +28 -6
- 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 +858 -484
- 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 +1216 -24
- package/dist/client.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 +4 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +41 -1
- package/dist/events.js.map +1 -1
- package/dist/facades.d.ts +84 -2
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +104 -4
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- 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 +1742 -0
- package/dist/org-protocol.js.map +1 -0
- package/dist/ownership-proof.d.ts +118 -0
- package/dist/ownership-proof.d.ts.map +1 -0
- package/dist/ownership-proof.js +283 -0
- package/dist/ownership-proof.js.map +1 -0
- package/dist/ownership-v21.d.ts +177 -0
- package/dist/ownership-v21.d.ts.map +1 -0
- package/dist/ownership-v21.js +609 -0
- package/dist/ownership-v21.js.map +1 -0
- package/dist/ownership-workflow.d.ts +146 -0
- package/dist/ownership-workflow.d.ts.map +1 -0
- package/dist/ownership-workflow.js +1066 -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 +82 -0
- package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
- package/dist/tools/cross-sdk-agent.js +332 -7
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +6 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +88 -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 +5 -5
package/dist/agent-md.js
CHANGED
|
@@ -1,9 +1,43 @@
|
|
|
1
1
|
import { AID } from './aid.js';
|
|
2
|
+
import { parseDocument } from 'yaml';
|
|
2
3
|
import { certMatchesFingerprint, parseAgentMdTailSignature } from './cert-utils.js';
|
|
3
|
-
import { validateAgentMdDocument, verifyAgentMdUpload } from './agent-md-schema.js';
|
|
4
|
+
import { parseFileFrontmatter, validateAgentMdDocument, verifyAgentMdUpload } 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 { 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 = { Accept: 'text/markdown', '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: { '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;
|
|
@@ -292,34 +423,296 @@ export class AgentMdManager {
|
|
|
292
423
|
if (localContent === null || localContent.length === 0) {
|
|
293
424
|
throw new ValidationError('uploadAgentMd requires local agent.md content');
|
|
294
425
|
}
|
|
295
|
-
validateAgentMdDocument(localContent, { expectedAid: target });
|
|
426
|
+
const parsed = validateAgentMdDocument(localContent, { expectedAid: target });
|
|
427
|
+
if (['group', 'org'].includes(String(parsed.frontmatter.type)))
|
|
428
|
+
return await this.uploadAs(target, localContent, current);
|
|
296
429
|
const signedResult = await current.signAgentMd(localContent);
|
|
297
430
|
if (!signedResult.ok) {
|
|
298
431
|
throw new ClientSignatureError(signedResult.error.message || 'agent.md signing failed');
|
|
299
432
|
}
|
|
300
433
|
const signed = signedResult.data.signed;
|
|
301
434
|
await verifyAgentMdUpload(signed, current.certPem, target);
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
|
|
435
|
+
return await this._uploadPrepared(target, signed, current);
|
|
436
|
+
}
|
|
437
|
+
async uploadAs(targetAid, content, signer) {
|
|
438
|
+
const target = this._safeAid(String(targetAid ?? '').trim());
|
|
439
|
+
const rawContent = String(content ?? '');
|
|
440
|
+
if (!rawContent.trim())
|
|
441
|
+
throw new ValidationError('uploadAgentMd requires non-empty content');
|
|
442
|
+
const parsed = validateAgentMdDocument(rawContent, { expectedAid: target });
|
|
443
|
+
if (['group', 'org'].includes(String(parsed.frontmatter.type)) && parsed.signature) {
|
|
444
|
+
const identity = await this._entityDocumentSigner(target, rawContent);
|
|
445
|
+
await verifyAgentMdUpload(rawContent, identity.certPem, target);
|
|
446
|
+
return await this._uploadPrepared(target, rawContent, identity);
|
|
447
|
+
}
|
|
448
|
+
if (!signer?.isPrivateKeyValid())
|
|
449
|
+
throw new StateError('uploadAgentMd requires a valid signer identity');
|
|
450
|
+
const signedResult = await signer.signAgentMd(rawContent);
|
|
451
|
+
if (!signedResult.ok)
|
|
452
|
+
throw new ClientSignatureError(signedResult.error.message || 'agent.md signing failed');
|
|
453
|
+
const signed = signedResult.data.signed;
|
|
454
|
+
await verifyAgentMdUpload(signed, signer.certPem, target);
|
|
455
|
+
return await this._uploadPrepared(target, signed, signer);
|
|
456
|
+
}
|
|
457
|
+
async pendingUpload(aid) {
|
|
458
|
+
const record = await this.loadRecord(aid);
|
|
459
|
+
if (record?.pending_operation !== 'replace' || !String(record.pending_content ?? '').trim())
|
|
460
|
+
return null;
|
|
461
|
+
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 };
|
|
462
|
+
}
|
|
463
|
+
async resumePendingUpload(aid, signer) {
|
|
464
|
+
const pending = await this.pendingUpload(aid);
|
|
465
|
+
if (!pending)
|
|
466
|
+
return false;
|
|
467
|
+
const condition = pending.condition;
|
|
468
|
+
if (!condition || Object.keys(condition).length !== 1 || !(typeof condition.if_match === 'string' && condition.if_match || condition.if_none_match === '*'))
|
|
469
|
+
throw new StateError('pending agent.md requires its original document condition', { stringCode: 'persistence_unavailable' });
|
|
470
|
+
const meta = validateAgentMdDocument(pending.content, { expectedAid: aid, validateOwnership: false }).frontmatter;
|
|
471
|
+
if ('ownership_proof' in meta || 'ownership_release' in meta)
|
|
472
|
+
throw new StateError('legacy ownership pending agent.md cannot be recovered through ordinary upload', { stringCode: 'unsupported_protocol' });
|
|
473
|
+
try {
|
|
474
|
+
if (['group', 'org'].includes(String(meta.type))) {
|
|
475
|
+
const identity = signer ?? this._currentAid();
|
|
476
|
+
if (!identity)
|
|
477
|
+
throw new StateError('entity upload recovery requires current owner', { stringCode: 'forbidden' });
|
|
478
|
+
const context = await this._entityContext(aid, meta, identity);
|
|
479
|
+
if (!pending.relation?.entity_cert_fingerprint)
|
|
480
|
+
throw new StateError('pending entity upload requires original owner and certificate context', { stringCode: 'persistence_unavailable' });
|
|
481
|
+
if (pending.relation.owner_aid !== context.owner_aid)
|
|
482
|
+
throw new StateError('pending entity upload belongs to a different owner', { stringCode: 'forbidden' });
|
|
483
|
+
if (pending.relation.entity_cert_fingerprint !== context.entity_cert_fingerprint)
|
|
484
|
+
throw new StateError('pending entity upload belongs to an old rekey generation', { stringCode: 'conflict' });
|
|
485
|
+
if (pending.content_etag !== await AgentMdManager.contentEtag(pending.content))
|
|
486
|
+
throw new StateError('pending entity original bytes are corrupt', { stringCode: 'persistence_unavailable' });
|
|
487
|
+
const documentSigner = await this._entityDocumentSigner(aid, pending.content);
|
|
488
|
+
await verifyAgentMdUpload(pending.content, documentSigner.certPem, aid);
|
|
489
|
+
if (identity.aid.toLowerCase() !== aid.toLowerCase())
|
|
490
|
+
await this._validateEntityUploadAuthorization(pending.authorization, aid, pending.content, identity, String(context.entity_cert_fingerprint));
|
|
491
|
+
const remote = await this._readEntityFresh(aid);
|
|
492
|
+
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: '' });
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
|
|
497
|
+
throw new StateError('pending agent.md original document condition changed', { stringCode: 'conflict' });
|
|
498
|
+
await this._uploadPrepared(aid, pending.content, identity, condition);
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
const remote = await this.readOwnershipFresh(aid);
|
|
502
|
+
const fail = (code, reason, retryable = false) => new StateError(reason, { stringCode: code, retryable });
|
|
503
|
+
const candidate = parseOwnershipFile(pending.content, 'agent', { expectedSubAid: aid });
|
|
504
|
+
const relation = pending.relation && Object.keys(pending.relation).length ? pending.relation : undefined;
|
|
505
|
+
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]);
|
|
506
|
+
if (candidate.statement && !relation)
|
|
507
|
+
throw fail('persistence_unavailable', 'pending ownership body update requires its original relationship context');
|
|
508
|
+
if (relation) {
|
|
509
|
+
if (remote.relation_version !== relation.relation_version || remote.lock !== relation.lock)
|
|
510
|
+
throw fail('conflict', 'pending agent.md original relationship changed');
|
|
511
|
+
if (!sameStatement(candidate.statement, relation.statement))
|
|
512
|
+
throw fail('unsupported_protocol', 'ownership changes require their persisted 2.1 task');
|
|
513
|
+
if (remote.exists && (remote.relation_version > 0 || candidate.statement)) {
|
|
514
|
+
const current = parseOwnershipFile(remote.content, 'agent', { expectedSubAid: aid });
|
|
515
|
+
if (!sameStatement(current.statement, relation.statement) || (remote.owner_aid ?? current.statement?.owner_aid ?? null) !== relation.owner_aid)
|
|
516
|
+
throw fail('conflict', 'pending agent.md relationship statement changed');
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (!relation && (remote.relation_version !== 0 || remote.lock !== 'unlocked'))
|
|
520
|
+
throw fail('unavailable', 'pending agent.md cannot overwrite relationship history', true);
|
|
521
|
+
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: '' });
|
|
523
|
+
if (!saved.aid)
|
|
524
|
+
throw new StateError('agent.md pending completion persistence failed');
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
|
|
528
|
+
throw new StateError('pending agent.md original document condition changed', { stringCode: 'conflict' });
|
|
529
|
+
const identity = signer ?? this._currentAid();
|
|
530
|
+
if (!identity)
|
|
531
|
+
throw new StateError('pending agent.md requires a signer identity');
|
|
532
|
+
await verifyAgentMdUpload(pending.content, identity.certPem, aid);
|
|
533
|
+
await this._uploadPrepared(aid, pending.content, identity, condition);
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
await this.saveRecord(aid, { pending_error: error instanceof Error ? error.message : String(error) });
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
async _uploadPrepared(target, signed, signer, originalCondition) {
|
|
542
|
+
const meta = validateAgentMdDocument(signed, { expectedAid: target }).frontmatter;
|
|
543
|
+
const entity = ['group', 'org'].includes(String(meta.type));
|
|
544
|
+
const uploader = entity ? this._currentAid() ?? signer : signer;
|
|
545
|
+
const entityContext = entity ? await this._entityContext(target, meta, uploader) : undefined;
|
|
546
|
+
if (entity)
|
|
547
|
+
await verifyAgentMdUpload(signed, (await this._entityDocumentSigner(target, signed)).certPem, target);
|
|
548
|
+
let condition = originalCondition;
|
|
549
|
+
let relation = entityContext ?? (await this.pendingUpload(target))?.relation;
|
|
550
|
+
if (!condition) {
|
|
551
|
+
const fresh = entity ? await this._readEntityFresh(target) : await this.readOwnershipFresh(target);
|
|
552
|
+
condition = fresh.exists ? { if_match: fresh.etag } : { if_none_match: '*' };
|
|
553
|
+
if (!entity) {
|
|
554
|
+
const authority = fresh;
|
|
555
|
+
const candidate = parseOwnershipFile(signed, 'agent', { expectedSubAid: target });
|
|
556
|
+
const statement = fresh.exists && (authority.relation_version > 0 || candidate.statement) ? parseOwnershipFile(fresh.content, 'agent', { expectedSubAid: target }).statement : undefined;
|
|
557
|
+
relation = { relation_version: authority.relation_version, lock: authority.lock, owner_aid: authority.owner_aid ?? statement?.owner_aid ?? null, statement: statement ?? null };
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const ownerUpload = entity && uploader.aid.toLowerCase() !== target.toLowerCase();
|
|
561
|
+
const entityAuthorization = ownerUpload ? originalCondition ? (await this.pendingUpload(target))?.authorization : await this._ownerUploadAuthorization(target, signed, uploader, String(entityContext.entity_cert_fingerprint)) : '';
|
|
562
|
+
if (ownerUpload) {
|
|
563
|
+
const timestamp = await this._validateEntityUploadAuthorization(entityAuthorization, target, signed, uploader, String(entityContext.entity_cert_fingerprint));
|
|
564
|
+
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > 300)
|
|
565
|
+
throw new StateError('pending entity V2 authorization expired; explicit upload is required', { stringCode: 'conflict' });
|
|
566
|
+
}
|
|
567
|
+
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 });
|
|
568
|
+
const savedCondition = pending.pending_condition;
|
|
569
|
+
if (pending.pending_content !== signed || savedCondition?.if_match !== condition.if_match || savedCondition?.if_none_match !== condition.if_none_match || pending.pending_authorization !== entityAuthorization)
|
|
570
|
+
throw new StateError('agent.md pending upload persistence failed');
|
|
571
|
+
try {
|
|
572
|
+
const owner = this._currentAid();
|
|
573
|
+
const delegated = target.toLowerCase() !== this._ownerAid().toLowerCase();
|
|
574
|
+
if (delegated && !owner?.isPrivateKeyValid())
|
|
575
|
+
throw new StateError('owner agent.md upload requires a loaded owner identity');
|
|
576
|
+
const authorization = entity ? entityAuthorization : delegated ? await this._ownerUploadAuthorization(target, signed, owner) : '';
|
|
577
|
+
const result = await this._uploadHttp(target, signed, authorization, condition);
|
|
578
|
+
const remoteEtag = String(result.etag ?? '').trim();
|
|
579
|
+
const saved = await this.saveRecord(target, {
|
|
580
|
+
content: signed, local_etag: await AgentMdManager.contentEtag(signed), remote_etag: remoteEtag || undefined,
|
|
581
|
+
last_modified: String(result.last_modified ?? result.lastModified ?? '').trim(), fetched_at: Date.now(), checked_at: Date.now(),
|
|
582
|
+
remote_status: remoteEtag ? 'found' : 'unknown', last_error: '',
|
|
583
|
+
pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '',
|
|
584
|
+
});
|
|
585
|
+
if (!saved.aid)
|
|
586
|
+
throw new StateError('agent.md upload completion persistence failed');
|
|
587
|
+
return { ...result };
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
await this.saveRecord(target, { pending_error: error instanceof Error ? error.message : String(error) });
|
|
591
|
+
throw error;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
async readEntityAuthorityFresh(target) {
|
|
595
|
+
const fresh = await this._readEntityFresh(this._safeAid(target));
|
|
596
|
+
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
|
+
throw new StateError('entity current owner authority is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
598
|
+
return { owner_aid: this._safeAid(fresh.owner_aid), entity_cert_fingerprint: fresh.entity_cert_fingerprint };
|
|
599
|
+
}
|
|
600
|
+
async _readEntityFresh(target) {
|
|
601
|
+
const gateway = await this._resolveGateway(target);
|
|
602
|
+
const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin('node:http') !== null;
|
|
603
|
+
const headers = { 'Cache-Control': 'no-cache', ...(viaGatewayHost ? { Host: target } : {}) };
|
|
604
|
+
let url = this._url(target, gateway);
|
|
605
|
+
if (viaGatewayHost) {
|
|
606
|
+
const parsed = new URL(gateway);
|
|
607
|
+
const port = parsed.port && parsed.port !== '443' && parsed.port !== '20001' ? `:${parsed.port}` : '';
|
|
608
|
+
url = `${agentMdHttpScheme(gateway)}://${parsed.hostname}${port}/agent.md`;
|
|
609
|
+
}
|
|
610
|
+
let response;
|
|
611
|
+
try {
|
|
612
|
+
response = await fetchWithTimeout(url, { method: 'GET', cache: 'no-store', redirect: 'error', headers });
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
throw new StateError(`entity agent.md fresh read unavailable: ${String(error)}`, { stringCode: 'unavailable', retryable: true });
|
|
616
|
+
}
|
|
617
|
+
if (response.status !== 200 && response.status !== 404)
|
|
618
|
+
throw new StateError(`entity agent.md fresh read failed: HTTP ${response.status}`, { stringCode: 'unavailable', retryable: true });
|
|
619
|
+
const etag = headerValue(response.headers, 'ETag');
|
|
620
|
+
if (response.status === 200 && !etag)
|
|
621
|
+
throw new StateError('entity agent.md fresh read has no ETag', { stringCode: 'unavailable', retryable: true });
|
|
622
|
+
let content = '';
|
|
623
|
+
if (response.status === 200) {
|
|
624
|
+
try {
|
|
625
|
+
content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(await response.arrayBuffer());
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
throw new ValidationError('entity agent.md must be valid UTF-8', { stringCode: 'invalid' });
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const facts = {};
|
|
632
|
+
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']]) {
|
|
633
|
+
const value = headerValue(response.headers, header);
|
|
634
|
+
if (value)
|
|
635
|
+
facts[field] = value;
|
|
636
|
+
}
|
|
637
|
+
return { exists: response.status === 200, content, etag, ...facts };
|
|
638
|
+
}
|
|
639
|
+
async _entityCurrentContext(target, meta, uploading = false) {
|
|
640
|
+
const owner = String(meta.owner ?? '').trim().toLowerCase();
|
|
641
|
+
if (!this._entityOwnerResolver)
|
|
642
|
+
throw new StateError('entity owner resolver is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
643
|
+
const freshOwner = String(await this._entityOwnerResolver(target, String(meta.type))).trim().toLowerCase();
|
|
644
|
+
if (!freshOwner)
|
|
645
|
+
throw new StateError('entity current owner is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
646
|
+
if (freshOwner !== owner)
|
|
647
|
+
throw new StateError('entity current owner changed', { stringCode: uploading ? 'forbidden' : 'conflict' });
|
|
648
|
+
if (!this._entityIdentityGetter)
|
|
649
|
+
throw new StateError('entity current certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
650
|
+
const entity = await this._entityIdentityGetter(target, String(meta.type));
|
|
651
|
+
if (!entity.certFingerprint)
|
|
652
|
+
throw new StateError('entity current certificate is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
653
|
+
if (meta.entity_cert_fingerprint !== entity.certFingerprint)
|
|
654
|
+
throw new StateError('entity agent.md certificate generation changed', { stringCode: 'conflict' });
|
|
655
|
+
return { type: meta.type, owner_aid: owner, entity_cert_fingerprint: entity.certFingerprint };
|
|
656
|
+
}
|
|
657
|
+
async _entityContext(target, meta, signer) {
|
|
658
|
+
const owner = String(meta.owner ?? '').trim().toLowerCase();
|
|
659
|
+
if (![owner, target.toLowerCase()].includes(signer.aid.toLowerCase()) || signer.aid.toLowerCase() !== this._ownerAid().toLowerCase())
|
|
660
|
+
throw new StateError('group/org agent.md requires entity or current owner uploader', { stringCode: 'forbidden' });
|
|
661
|
+
const context = await this._entityCurrentContext(target, meta, true);
|
|
662
|
+
if (signer.aid.toLowerCase() === owner)
|
|
663
|
+
await this._verifyEntityOwnerSigner(signer);
|
|
664
|
+
else if (signer.certFingerprint !== context.entity_cert_fingerprint)
|
|
665
|
+
throw new StateError('entity uploader certificate generation changed', { stringCode: 'conflict' });
|
|
666
|
+
return context;
|
|
667
|
+
}
|
|
668
|
+
async _entityDocumentSigner(target, content) {
|
|
669
|
+
const parsed = validateAgentMdDocument(content, { expectedAid: target, requireSignature: true });
|
|
670
|
+
const signerAid = parsed.signature.signer_aid ?? target;
|
|
671
|
+
let signer;
|
|
672
|
+
if (signerAid.toLowerCase() === target.toLowerCase()) {
|
|
673
|
+
if (!this._entityIdentityGetter)
|
|
674
|
+
throw new StateError('entity current certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
675
|
+
signer = await this._entityIdentityGetter(target, String(parsed.frontmatter.type));
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
if (!this._entitySignerResolver)
|
|
679
|
+
throw new StateError('entity owner signing certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
680
|
+
signer = await this._entitySignerResolver(signerAid, parsed.signature.cert_fingerprint);
|
|
681
|
+
}
|
|
682
|
+
if (signer.aid.toLowerCase() !== signerAid.toLowerCase() || signer.certFingerprint !== parsed.signature.cert_fingerprint)
|
|
683
|
+
throw new StateError('entity signing certificate changed', { stringCode: 'conflict' });
|
|
684
|
+
return signer;
|
|
685
|
+
}
|
|
686
|
+
async _verifyEntityOwnerSigner(owner) {
|
|
687
|
+
if (!this._entitySignerResolver)
|
|
688
|
+
throw new StateError('entity owner signing certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
689
|
+
const fresh = await this._entitySignerResolver(owner.aid, owner.certFingerprint);
|
|
690
|
+
if (fresh.aid.toLowerCase() !== owner.aid.toLowerCase() || fresh.certFingerprint !== owner.certFingerprint) {
|
|
691
|
+
throw new StateError('entity owner signing certificate changed', { stringCode: 'conflict' });
|
|
692
|
+
}
|
|
316
693
|
}
|
|
317
694
|
async download(aid, timeoutMs = DEFAULT_HTTP_TIMEOUT_MS) {
|
|
318
695
|
const target = this._safeAid(String(aid ?? this._ownerAid() ?? '').trim());
|
|
319
696
|
const contentResult = await this._downloadHttp(target, timeoutMs);
|
|
320
697
|
const parsed = parseAgentMdTailSignature(contentResult.content);
|
|
698
|
+
let entity = false;
|
|
699
|
+
try {
|
|
700
|
+
entity = ['group', 'org'].includes(String(parseFileFrontmatter(parsed.payload).type));
|
|
701
|
+
}
|
|
702
|
+
catch { }
|
|
703
|
+
let frontmatter;
|
|
704
|
+
try {
|
|
705
|
+
frontmatter = validateAgentMdDocument(contentResult.content, { expectedAid: target, validateOwnership: false, ...(entity ? { requireSignature: true } : {}) }).frontmatter;
|
|
706
|
+
}
|
|
707
|
+
catch (error) {
|
|
708
|
+
if (entity)
|
|
709
|
+
throw new ValidationError(error instanceof Error ? error.message : String(error), { stringCode: 'invalid' });
|
|
710
|
+
throw error;
|
|
711
|
+
}
|
|
321
712
|
const expectedFp = parsed.fields?.cert_fingerprint || parsed.fields?.public_key_fingerprint || null;
|
|
322
|
-
|
|
713
|
+
if (entity)
|
|
714
|
+
await this._entityCurrentContext(target, frontmatter);
|
|
715
|
+
const peer = entity ? await this._entityDocumentSigner(target, contentResult.content) : await this._resolvePeer(target, expectedFp, parsed.fields?.timestamp ? Number(parsed.fields.timestamp) : undefined);
|
|
323
716
|
const verified = await peer.verifyAgentMd(contentResult.content);
|
|
324
717
|
if (!verified.ok)
|
|
325
718
|
throw new AUNError(verified.error.message);
|
|
@@ -327,9 +720,30 @@ export class AgentMdManager {
|
|
|
327
720
|
const statusText = String(signature.status ?? 'invalid');
|
|
328
721
|
const reason = String(signature.reason ?? '').trim();
|
|
329
722
|
if (statusText === 'invalid') {
|
|
330
|
-
throw new ClientSignatureError(`agent.md verification failed for ${target}${reason ? `: ${reason}` : ''}
|
|
723
|
+
throw new ClientSignatureError(`agent.md verification failed for ${target}${reason ? `: ${reason}` : ''}`, entity ? { stringCode: 'invalid' } : undefined);
|
|
724
|
+
}
|
|
725
|
+
let ownershipStatus;
|
|
726
|
+
if (entity)
|
|
727
|
+
ownershipStatus = 'not_applicable';
|
|
728
|
+
else {
|
|
729
|
+
try {
|
|
730
|
+
validateOwnershipFrontmatter(frontmatter, target);
|
|
731
|
+
const rawProof = (frontmatter.ownership_proof ?? {});
|
|
732
|
+
const subjectSignature = rawProof.subject_signature;
|
|
733
|
+
const ownerSignature = rawProof.owner_signature;
|
|
734
|
+
const release = frontmatter.ownership_release;
|
|
735
|
+
const subjectCertPem = subjectSignature ? (await this._resolvePeer(target, String(subjectSignature.cert_fingerprint), isJsonObject(subjectSignature.statement) ? Number(subjectSignature.statement.signed_at) : undefined)).certPem : undefined;
|
|
736
|
+
const ownerAid = String(ownerSignature ? frontmatter.owner : release?.statement && typeof release.statement === 'object' ? release.statement.owner_aid : '');
|
|
737
|
+
const ownerFingerprint = String(ownerSignature?.cert_fingerprint ?? release?.cert_fingerprint ?? '');
|
|
738
|
+
const ownerStatement = (ownerSignature ?? release)?.statement;
|
|
739
|
+
const ownerCertPem = ownerFingerprint ? (await this._resolvePeer(ownerAid, ownerFingerprint, isJsonObject(ownerStatement) ? Number(ownerStatement.signed_at) : undefined)).certPem : undefined;
|
|
740
|
+
ownershipStatus = await verifyOwnershipFrontmatterStatus(frontmatter, subjectCertPem, ownerCertPem);
|
|
741
|
+
}
|
|
742
|
+
catch {
|
|
743
|
+
ownershipStatus = 'invalid';
|
|
744
|
+
}
|
|
331
745
|
}
|
|
332
|
-
const verification = { status: statusText };
|
|
746
|
+
const verification = { status: statusText, ownership_status: ownershipStatus };
|
|
333
747
|
if (reason)
|
|
334
748
|
verification.reason = reason;
|
|
335
749
|
const localEtag = await AgentMdManager.contentEtag(contentResult.content);
|
|
@@ -690,14 +1104,14 @@ export class AgentMdManager {
|
|
|
690
1104
|
async _resolveGateway(aid) {
|
|
691
1105
|
return String(await this._gatewayResolver?.(aid) ?? '');
|
|
692
1106
|
}
|
|
693
|
-
async _resolvePeer(aid, certFingerprint) {
|
|
1107
|
+
async _resolvePeer(aid, certFingerprint, signedAt) {
|
|
694
1108
|
const expectedFp = String(certFingerprint ?? '').trim().toLowerCase();
|
|
695
1109
|
const current = this._currentAid();
|
|
696
1110
|
if (current && current.aid.toLowerCase() === aid.toLowerCase()) {
|
|
697
1111
|
if (!expectedFp || await certMatchesFingerprint(current.certPem, expectedFp))
|
|
698
1112
|
return current;
|
|
699
1113
|
}
|
|
700
|
-
const peer = await this._peerResolver?.(aid, expectedFp || null);
|
|
1114
|
+
const peer = await this._peerResolver?.(aid, expectedFp || null, signedAt);
|
|
701
1115
|
if (!(peer instanceof AID)) {
|
|
702
1116
|
throw new StateError(`agent.md peer resolver did not return AID for ${aid}`);
|
|
703
1117
|
}
|
|
@@ -709,19 +1123,87 @@ export class AgentMdManager {
|
|
|
709
1123
|
throw new StateError('authenticate did not return access_token');
|
|
710
1124
|
return token;
|
|
711
1125
|
}
|
|
1126
|
+
async _uploadAccessToken(aid, gatewayUrl) {
|
|
1127
|
+
const resolver = this._uploadAccessTokenResolver ?? this._accessTokenResolver;
|
|
1128
|
+
const token = String(await resolver?.(aid, gatewayUrl) ?? '').trim();
|
|
1129
|
+
if (!token)
|
|
1130
|
+
throw new StateError('authenticate did not return access_token');
|
|
1131
|
+
return token;
|
|
1132
|
+
}
|
|
1133
|
+
async _validateEntityUploadAuthorization(authorization, target, content, owner, entityFingerprint) {
|
|
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 = '') {
|
|
1164
|
+
const bodySha256 = await sha256Hex(content);
|
|
1165
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
1166
|
+
const nonce = randomOwnerAuthorizationNonce();
|
|
1167
|
+
const canonical = entityFingerprint ? `AUN-AGENTMD-OWNER-AUTH-V2;${owner.aid.toLowerCase()};${target.toLowerCase()};${bodySha256};${entityFingerprint};${timestamp};${nonce}` : `AUN-AGENT-MD-UPLOAD-V1;${owner.aid.toLowerCase()};${target.toLowerCase()};${bodySha256};${timestamp};${nonce}`;
|
|
1168
|
+
const signed = await owner.sign(canonical);
|
|
1169
|
+
if (!signed.ok || !signed.data) {
|
|
1170
|
+
const message = signed.error?.message || 'owner authorization signing failed';
|
|
1171
|
+
throw new ClientSignatureError(message);
|
|
1172
|
+
}
|
|
1173
|
+
return base64UrlEncode(JSON.stringify({
|
|
1174
|
+
version: entityFingerprint ? 2 : 1,
|
|
1175
|
+
target_aid: target.toLowerCase(),
|
|
1176
|
+
uploader_aid: owner.aid.toLowerCase(),
|
|
1177
|
+
body_sha256: bodySha256,
|
|
1178
|
+
...(entityFingerprint ? { entity_cert_fingerprint: entityFingerprint } : {}),
|
|
1179
|
+
timestamp,
|
|
1180
|
+
nonce,
|
|
1181
|
+
cert_fingerprint: owner.certFingerprint,
|
|
1182
|
+
signature: signed.data.signature,
|
|
1183
|
+
}));
|
|
1184
|
+
}
|
|
712
1185
|
_url(aid, gatewayUrl) {
|
|
713
1186
|
return `${agentMdHttpScheme(gatewayUrl)}://${this._safeAid(aid)}/agent.md`;
|
|
714
1187
|
}
|
|
715
|
-
async _uploadHttp(aid, content) {
|
|
1188
|
+
async _uploadHttp(aid, content, ownerAuthorization = '', condition) {
|
|
1189
|
+
if (!condition || Object.keys(condition).length !== 1 || !(typeof condition.if_match === 'string' && condition.if_match || condition.if_none_match === '*'))
|
|
1190
|
+
throw new StateError('agent.md upload requires its original document condition', { stringCode: 'persistence_unavailable' });
|
|
716
1191
|
const gatewayUrl = await this._resolveGateway(aid);
|
|
717
|
-
const
|
|
1192
|
+
const authorization = String(ownerAuthorization ?? '').trim();
|
|
1193
|
+
const entity = ['group', 'org'].includes(String(validateAgentMdDocument(content, { expectedAid: aid }).frontmatter.type));
|
|
718
1194
|
// 浏览器禁止覆盖 Host;动态 AID URL 由浏览器/Docker 的通配解析路由。
|
|
719
1195
|
const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin('node:http') !== null;
|
|
720
1196
|
const headers = {
|
|
721
|
-
Authorization: `Bearer ${token}`,
|
|
722
1197
|
'Content-Type': 'text/markdown; charset=utf-8',
|
|
1198
|
+
...(!entity ? { 'X-AUN-Ownership-Check': '1' } : {}),
|
|
1199
|
+
...(condition.if_match ? { 'If-Match': condition.if_match } : { 'If-None-Match': '*' }),
|
|
723
1200
|
...(viaGatewayHost ? { Host: this._safeAid(aid) } : {}),
|
|
1201
|
+
...(authorization ? { [entity ? 'AUN-AGENTMD-OWNER-AUTHORIZATION' : 'X-AUN-AgentMD-Owner-Authorization']: authorization } : {}),
|
|
724
1202
|
};
|
|
1203
|
+
if (!authorization || entity) {
|
|
1204
|
+
const token = await this._uploadAccessToken(aid, gatewayUrl);
|
|
1205
|
+
headers.Authorization = `Bearer ${token}`;
|
|
1206
|
+
}
|
|
725
1207
|
let url = this._url(aid, gatewayUrl);
|
|
726
1208
|
if (viaGatewayHost) {
|
|
727
1209
|
try {
|
|
@@ -762,6 +1244,10 @@ export class AgentMdManager {
|
|
|
762
1244
|
}
|
|
763
1245
|
if (!response.ok) {
|
|
764
1246
|
const message = (await response.text()).trim();
|
|
1247
|
+
if (entity) {
|
|
1248
|
+
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';
|
|
1249
|
+
throw new StateError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`, { stringCode: code, retryable: code === 'unavailable' });
|
|
1250
|
+
}
|
|
765
1251
|
throw new AUNError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
|
|
766
1252
|
}
|
|
767
1253
|
const payload = await response.json();
|
|
@@ -803,9 +1289,15 @@ export class AgentMdManager {
|
|
|
803
1289
|
}
|
|
804
1290
|
if (!response.ok) {
|
|
805
1291
|
const message = (await response.text()).trim();
|
|
806
|
-
throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}
|
|
1292
|
+
throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`, response.status >= 500 ? { stringCode: 'unavailable', retryable: true } : undefined);
|
|
1293
|
+
}
|
|
1294
|
+
let content;
|
|
1295
|
+
try {
|
|
1296
|
+
content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(await response.arrayBuffer());
|
|
1297
|
+
}
|
|
1298
|
+
catch {
|
|
1299
|
+
throw new ValidationError('agent.md must be valid UTF-8', { stringCode: 'invalid' });
|
|
807
1300
|
}
|
|
808
|
-
const content = await response.text();
|
|
809
1301
|
return {
|
|
810
1302
|
content,
|
|
811
1303
|
etag: headerValue(response.headers, 'ETag') || cachedEtag,
|