@agentunion/fastaun-browser 0.5.19 → 0.5.21
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 +176 -78
- package/_packed_docs/CHANGELOG.md +176 -78
- package/_packed_docs/INDEX.md +288 -269
- package/_packed_docs/KITE_DOCS_GUIDE.md +101 -95
- package/_packed_docs/agent.md/SCHEMA.md +7 -7
- package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/344/270/216/346/215/242owner/347/274/226/346/216/222/346/226/271/346/241/210.md +530 -0
- package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/345/256/214/346/225/264/350/256/276/350/256/241/344/270/216/345/256/236/346/226/275/350/256/241/345/210/222.md +685 -0
- package/_packed_docs/agent.md/working/agent.md-schema/344/270/216/347/255/276/345/217/221/351/252/214/347/255/276/346/265/201/347/250/213/346/276/204/346/270/205.md +349 -0
- package/_packed_docs/agent.md/working//346/234/254/345/234/260Docker/346/265/213/350/257/225Bug/346/270/205/345/215/225.md +40 -0
- package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +11 -11
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +372 -331
- package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +676 -676
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +8 -12
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +445 -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 +666 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
- package/_packed_docs/protocol/agent.md/SCHEMA.md +1 -1
- package/_packed_docs/sdk/0.5.21-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +368 -0
- package/_packed_docs/sdk/0.5.21-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +350 -0
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +70 -63
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +43 -40
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +179 -19
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +50 -49
- package/_packed_docs/sdk/09-group-rpc-manual.md +261 -254
- package/_packed_docs/sdk/09-message-rpc-manual.md +253 -160
- package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
- package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +71 -0
- package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +88 -0
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +105 -92
- package/_packed_docs/sdk/INDEX.md +197 -167
- 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 +930 -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 +363 -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 +553 -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 +480 -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 +16 -4
- package/dist/agent-md-schema.d.ts.map +1 -1
- package/dist/agent-md-schema.js +65 -30
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +35 -3
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +384 -150
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +59 -0
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +202 -16
- 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 +24 -42
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +89 -18
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +6410 -1848
- package/dist/cert-utils.d.ts +50 -1
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +123 -56
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +108 -16
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +1411 -184
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +4 -1
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +203 -117
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +22 -3
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +1312 -684
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +23 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +419 -101
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +8 -6
- package/dist/crypto.js.map +1 -1
- package/dist/events.d.ts +24 -3
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +171 -29
- package/dist/events.js.map +1 -1
- package/dist/facades.d.ts +42 -1
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +368 -12
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +14 -1
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.d.ts +4 -0
- package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.js +30 -13
- package/dist/keystore/indexeddb-identity-store.js.map +1 -1
- package/dist/keystore/indexeddb-shared.d.ts +19 -2
- package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
- package/dist/keystore/indexeddb-shared.js +193 -6
- package/dist/keystore/indexeddb-shared.js.map +1 -1
- package/dist/keystore/indexeddb-token-store.d.ts +15 -2
- package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-token-store.js +308 -27
- package/dist/keystore/indexeddb-token-store.js.map +1 -1
- package/dist/org-protocol.d.ts +45 -0
- package/dist/org-protocol.d.ts.map +1 -1
- package/dist/org-protocol.js +827 -21
- package/dist/org-protocol.js.map +1 -1
- package/dist/ownership-v21.d.ts +43 -9
- package/dist/ownership-v21.d.ts.map +1 -1
- package/dist/ownership-v21.js +183 -81
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.d.ts +36 -1
- package/dist/ownership-workflow.d.ts.map +1 -1
- package/dist/ownership-workflow.js +610 -89
- package/dist/ownership-workflow.js.map +1 -1
- package/dist/seq-tracker.d.ts +2 -0
- package/dist/seq-tracker.d.ts.map +1 -1
- package/dist/seq-tracker.js +3 -2
- package/dist/seq-tracker.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +61 -3
- package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
- package/dist/tools/cross-sdk-agent.js +524 -45
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +4 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +64 -30
- package/dist/transport.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/ownership-v21.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseFileFrontmatter, validateAgentMdCertificate, validateAgentMdDocument } from './agent-md-schema.js';
|
|
2
|
-
import {
|
|
2
|
+
import { signDocument, verifyDocument, isStrictBase64, parseAgentMdTailSignature } from './cert-utils.js';
|
|
3
3
|
import { base64ToUint8 } from './crypto.js';
|
|
4
4
|
import { StateError, ValidationError } from './errors.js';
|
|
5
5
|
import { buildDefaultAgentMd, buildOwnershipAgentMdV21 } from './agent-md.js';
|
|
@@ -108,7 +108,9 @@ export function parseOwnershipFile(content, role, context = {}) {
|
|
|
108
108
|
if (raw.length - new TextEncoder().encode(parsed.payload).length > 2048)
|
|
109
109
|
throw new ValidationError('ownership file signature exceeds 2048 bytes');
|
|
110
110
|
const fields = parsed.fields;
|
|
111
|
-
if (fields)
|
|
111
|
+
if (fields?.signer_aid !== undefined)
|
|
112
|
+
throw new ValidationError('ownership main signature contains unknown signer_aid');
|
|
113
|
+
for (const { fields } of parsed.signatures) {
|
|
112
114
|
if (!/^sha256:[0-9a-f]{64}$/.test(fields.cert_fingerprint) || fields.public_key_fingerprint !== undefined && !/^sha256:[0-9a-f]{64}$/.test(fields.public_key_fingerprint))
|
|
113
115
|
throw new ValidationError('ownership file fingerprint is invalid');
|
|
114
116
|
if (!/^[1-9][0-9]*$/.test(fields.timestamp) || !Number.isSafeInteger(Number(fields.timestamp)))
|
|
@@ -121,7 +123,7 @@ export function parseOwnershipFile(content, role, context = {}) {
|
|
|
121
123
|
let action;
|
|
122
124
|
let transfer;
|
|
123
125
|
if (role === 'agent') {
|
|
124
|
-
frontmatter = validateAgentMdDocument(text, { expectedAid: context.expectedSubAid, validateOwnership: false, now: context.now, checkTimestamp: false }).frontmatter;
|
|
126
|
+
frontmatter = validateAgentMdDocument(text, { expectedAid: context.expectedSubAid, validateOwnership: false, validateOwnershipNumbers: true, now: context.now, checkTimestamp: false }).frontmatter;
|
|
125
127
|
requireOwnershipApplicable(frontmatter.type);
|
|
126
128
|
statement = validateOwnershipAgentFrontmatter(frontmatter, context.expectedSubAid);
|
|
127
129
|
if (context.expectedOwnerAid && statement?.owner_aid !== context.expectedOwnerAid)
|
|
@@ -154,7 +156,7 @@ export function parseOwnershipFile(content, role, context = {}) {
|
|
|
154
156
|
}
|
|
155
157
|
else
|
|
156
158
|
throw new ValidationError('ownership file role is invalid');
|
|
157
|
-
return { raw, payload: parsed.payload, frontmatter, signature: fields, statement, action, transfer };
|
|
159
|
+
return { raw, payload: parsed.payload, frontmatter, signature: fields, signatures: parsed.signatures, statement, action, transfer };
|
|
158
160
|
}
|
|
159
161
|
export async function verifyOwnershipFile(content, role, certificate, context = {}) {
|
|
160
162
|
const parsed = parseOwnershipFile(content, role, { ...context, requireSignature: true });
|
|
@@ -165,19 +167,20 @@ export async function verifyOwnershipFile(content, role, certificate, context =
|
|
|
165
167
|
if (context.expectedTargetAid && context.expectedTargetAid !== sub || context.authenticatedAid && context.authenticatedAid !== signer || context.certificateAid && context.certificateAid !== signer || context.resourcePath && context.resourcePath !== `${signer}:/${storagePath}` || context.storagePath && context.storagePath !== storagePath)
|
|
166
168
|
throw new ValidationError('ownership file resource or authenticated identity mismatch');
|
|
167
169
|
const now = Math.trunc(context.now ?? Date.now() / 1000);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (!
|
|
180
|
-
throw new ValidationError('ownership file signature verification failed');
|
|
170
|
+
const verified = await verifyDocument(new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(parsed.raw), async (blockSigner, fingerprint, timestamp) => {
|
|
171
|
+
const material = blockSigner === signer && fingerprint === fields.cert_fingerprint ? certificate : context.certificateResolver ? await context.certificateResolver(blockSigner, fingerprint) : null;
|
|
172
|
+
if (!material)
|
|
173
|
+
throw new StateError('ownership additional signing certificate is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
174
|
+
if (material.chain_valid === false || material.revoked === true || typeof material.state === 'string' && material.state !== 'active_signing')
|
|
175
|
+
throw new ValidationError('ownership signing certificate is not currently verified and active_signing', { stringCode: 'invalid' });
|
|
176
|
+
if (material.chain_valid !== true || material.state !== 'active_signing' || material.revoked !== false || material.revocation_checked !== true || !Number.isSafeInteger(material.observed_at) || material.observed_at <= 0 || material.observed_at > now + 5 || material.observed_at < now - 30)
|
|
177
|
+
throw new StateError('ownership certificate current status is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
178
|
+
validateAgentMdCertificate(material.cert_pem, blockSigner, timestamp, now);
|
|
179
|
+
return material.cert_pem;
|
|
180
|
+
}, { expectedAid: signer, now, requireCurrentValidity: true });
|
|
181
|
+
if (!verified.valid) {
|
|
182
|
+
throw new ValidationError(verified.reason || 'ownership file signature verification failed');
|
|
183
|
+
}
|
|
181
184
|
return parsed;
|
|
182
185
|
}
|
|
183
186
|
export async function signOwnershipFile(identity, content, role, context = {}) {
|
|
@@ -193,8 +196,7 @@ export async function signOwnershipFile(identity, content, role, context = {}) {
|
|
|
193
196
|
throw new ValidationError('ownership payload must end with a newline before signature');
|
|
194
197
|
const now = Math.trunc(context.now ?? Date.now() / 1000);
|
|
195
198
|
validateAgentMdCertificate(identity.certPem, identity.aid, now, now);
|
|
196
|
-
|
|
197
|
-
return parsed.payload + buildAgentMdSignatureBlock(identity.certFingerprint, now, signature, await publicKeyFingerprint(identity.certPem), role === 'agent' ? 'AUN-SIGNATURE-AGENTMD-V2' : 'AUN-SIGNATURE-OWNERSHIP-V2');
|
|
199
|
+
return (await signDocument(identity, parsed.payload, { now, tag: role === 'agent' ? 'AGENTMD-V2' : 'OWNERSHIP-V2' })).signed;
|
|
198
200
|
}
|
|
199
201
|
export async function ownershipSha256(content) {
|
|
200
202
|
const bytes = typeof content === 'string' ? new TextEncoder().encode(content) : content;
|
|
@@ -219,9 +221,9 @@ export class OwnershipStorageAdapter {
|
|
|
219
221
|
}
|
|
220
222
|
catch (error) {
|
|
221
223
|
if (error instanceof StateError && error.stringCode)
|
|
222
|
-
throw error;
|
|
224
|
+
throw error.stringCode === 'forbidden' && (method === 'storage.head_object' || method === 'storage.get_object') ? this.error('unavailable', error.message) : error;
|
|
223
225
|
const code = error?.code;
|
|
224
|
-
throw this.error(code === -32009 ? 'conflict' : code === -32004 ? 'forbidden' : code === -32601
|
|
226
|
+
throw this.error(code === -32009 ? 'conflict' : code === -32004 ? method === 'storage.head_object' || method === 'storage.get_object' ? 'unavailable' : 'forbidden' : code === -32601 ? 'unsupported_protocol' : code === -32602 ? 'invalid' : 'unavailable', `Storage ownership request failed: ${String(error)}`);
|
|
225
227
|
}
|
|
226
228
|
}
|
|
227
229
|
metadata(value, ownerAid, path) {
|
|
@@ -230,13 +232,13 @@ export class OwnershipStorageAdapter {
|
|
|
230
232
|
const service = `st.${ownerAid.slice(ownerAid.indexOf('.') + 1)}`;
|
|
231
233
|
if (Object.hasOwn(value, 'service_aid') && value.service_aid !== service || Object.hasOwn(value, 'authenticated_service_aid') && value.authenticated_service_aid !== service)
|
|
232
234
|
throw this.error('unavailable', 'Storage authoritative service identity unavailable');
|
|
233
|
-
if (value.consistency !== 'authoritative' || Object.hasOwn(value, 'recovery_ready') && value.recovery_ready !== true || value.owner_aid !== ownerAid || value.bucket !== 'default' || value.path !== path || typeof value.exists !== 'boolean' ||
|
|
234
|
-
throw this.error('unavailable', 'Storage authoritative namespace
|
|
235
|
-
if (value.exists && (typeof value.sha256 !== 'string' || !/^[0-9a-f]{64}$/.test(value.sha256)
|
|
235
|
+
if (value.consistency !== 'authoritative' || Object.hasOwn(value, 'recovery_ready') && value.recovery_ready !== true || value.owner_aid !== ownerAid || value.bucket !== 'default' || value.path !== path || typeof value.exists !== 'boolean' || !Number.isSafeInteger(observed) || observed < now - 30 || observed > now + 5 || observed <= 0)
|
|
236
|
+
throw this.error('unavailable', 'Storage authoritative namespace or freshness unavailable');
|
|
237
|
+
if (value.exists && (typeof value.sha256 !== 'string' || !/^[0-9a-f]{64}$/.test(value.sha256)))
|
|
236
238
|
throw this.error('unavailable', 'Storage authoritative object metadata unavailable');
|
|
237
|
-
return { owner_aid: ownerAid, bucket: 'default', path, exists: value.exists,
|
|
239
|
+
return { owner_aid: ownerAid, bucket: 'default', path, exists: value.exists,
|
|
238
240
|
observed_at: observed, sha256: value.exists ? String(value.sha256) : '', content: '', authoritative: true,
|
|
239
|
-
service_aid: service, authenticated_service_aid: service };
|
|
241
|
+
service_aid: service, authenticated_service_aid: service, version: value.exists && Number.isSafeInteger(value.version) && Number(value.version) > 0 ? Number(value.version) : undefined, ...(typeof value.revision === 'string' && value.revision ? { revision: value.revision } : {}) };
|
|
240
242
|
}
|
|
241
243
|
async readFile(ownerAid, path) {
|
|
242
244
|
ownershipAid(ownerAid, 'owner_aid');
|
|
@@ -246,10 +248,10 @@ export class OwnershipStorageAdapter {
|
|
|
246
248
|
const before = this.metadata(beforeRaw, ownerAid, path);
|
|
247
249
|
if (!before.exists)
|
|
248
250
|
return before;
|
|
249
|
-
const body = await this.request('storage.get_object',
|
|
251
|
+
const body = await this.request('storage.get_object', params);
|
|
250
252
|
const current = this.metadata(body, ownerAid, path);
|
|
251
|
-
if (!current.exists || current.
|
|
252
|
-
throw this.error('unavailable', 'Storage ownership body
|
|
253
|
+
if (!current.exists || current.sha256 !== before.sha256 || before.revision !== undefined && current.revision !== before.revision)
|
|
254
|
+
throw this.error('unavailable', 'Storage ownership body changed');
|
|
253
255
|
if (typeof body.content !== 'string')
|
|
254
256
|
throw this.error('unavailable', 'Storage ownership body is unavailable');
|
|
255
257
|
if (body.content !== '' && !isStrictBase64(body.content))
|
|
@@ -266,34 +268,56 @@ export class OwnershipStorageAdapter {
|
|
|
266
268
|
}
|
|
267
269
|
const afterRaw = await this.request('storage.head_object', params);
|
|
268
270
|
const after = this.metadata(afterRaw, ownerAid, path);
|
|
269
|
-
if (!after.exists || after.
|
|
271
|
+
if (!after.exists || after.sha256 !== before.sha256 || before.revision !== undefined && after.revision !== before.revision || Math.trunc(this.deps.now?.() ?? Date.now() / 1000) - began > 30)
|
|
270
272
|
throw this.error('unavailable', 'Storage ownership changed during authoritative read');
|
|
271
273
|
return { ...after, content };
|
|
272
274
|
}
|
|
273
275
|
async readOwnerMd(ownerAid, subAid) {
|
|
274
276
|
return await this.readFile(ownerAid, `sub_aids/${ownershipAid(subAid, 'sub_aid')}/owner.md`);
|
|
275
277
|
}
|
|
276
|
-
async writeOwnerMd(ownerAid, subAid, raw,
|
|
278
|
+
async writeOwnerMd(ownerAid, subAid, raw, original, guard) {
|
|
277
279
|
if (ownerAid !== this.deps.currentAid())
|
|
278
280
|
throw this.error('forbidden', 'only the authenticated owner may publish owner.md');
|
|
279
|
-
|
|
280
|
-
|
|
281
|
+
const path = `sub_aids/${ownershipAid(subAid, 'sub_aid')}/owner.md`;
|
|
282
|
+
const before = await this.readOwnerMd(ownerAid, subAid);
|
|
283
|
+
if (original && (before.exists !== original.exists || before.content !== original.content || before.sha256 !== original.sha256 || original.revision !== undefined && before.revision !== original.revision))
|
|
284
|
+
throw this.error('conflict', 'ownership original owner file was replaced');
|
|
281
285
|
const bytes = new TextEncoder().encode(raw);
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
await
|
|
286
|
+
const expectedVersion = before.exists ? (original ?? before).version : 0;
|
|
287
|
+
if (typeof expectedVersion !== 'number' || !Number.isSafeInteger(expectedVersion) || expectedVersion < (before.exists ? 1 : 0))
|
|
288
|
+
throw this.error('unavailable', 'Storage owner.md version is unavailable');
|
|
289
|
+
await guard?.();
|
|
290
|
+
const session = await this.request('storage.create_upload_session', { owner_aid: ownershipAid(ownerAid, 'owner_aid'), bucket: 'default', object_key: path, size_bytes: bytes.length, content_type: 'text/markdown; charset=utf-8', expected_version: expectedVersion, overwrite: true });
|
|
291
|
+
const uploadURL = String(session.upload_url ?? ''), sessionID = String(session.session_id ?? '');
|
|
292
|
+
if (!uploadURL || !sessionID)
|
|
293
|
+
throw this.error('unavailable', 'Storage owner.md upload session is incomplete');
|
|
294
|
+
const headers = {};
|
|
295
|
+
if (session.headers && typeof session.headers === 'object')
|
|
296
|
+
for (const [key, value] of Object.entries(session.headers))
|
|
297
|
+
if (value !== undefined && value !== null && key.toLowerCase() !== 'content-type')
|
|
298
|
+
headers[key] = String(value);
|
|
299
|
+
headers['Content-Type'] = 'text/markdown; charset=utf-8';
|
|
300
|
+
await guard?.();
|
|
301
|
+
if (this.deps.httpPut)
|
|
302
|
+
await this.deps.httpPut(uploadURL, bytes, headers);
|
|
303
|
+
else {
|
|
304
|
+
const response = await fetch(uploadURL, { method: 'PUT', body: bytes, headers });
|
|
305
|
+
if (!response.ok)
|
|
306
|
+
throw this.error('unavailable', `Storage owner.md HTTP PUT failed: ${response.status}`);
|
|
307
|
+
}
|
|
308
|
+
const complete = { owner_aid: ownerAid, bucket: 'default', object_key: path, session_id: sessionID, size_bytes: bytes.length, sha256: await ownershipSha256(bytes), content_type: 'text/markdown; charset=utf-8', is_private: false, overwrite: true };
|
|
309
|
+
complete.expected_version = expectedVersion;
|
|
310
|
+
await guard?.();
|
|
311
|
+
await this.request('storage.complete_upload', complete);
|
|
286
312
|
return await this.readOwnerMd(ownerAid, subAid);
|
|
287
313
|
}
|
|
288
|
-
async deleteOwnerMd(ownerAid, subAid
|
|
314
|
+
async deleteOwnerMd(ownerAid, subAid) {
|
|
289
315
|
if (ownerAid !== this.deps.currentAid())
|
|
290
316
|
throw this.error('forbidden', 'only the authenticated owner may delete owner.md');
|
|
291
|
-
if (typeof expectedRevision !== 'string' || !expectedRevision)
|
|
292
|
-
throw this.error('invalid', 'ownership expected_revision is required');
|
|
293
317
|
const path = `sub_aids/${ownershipAid(subAid, 'sub_aid')}/owner.md`;
|
|
294
|
-
await this.request('storage.delete_object', { owner_aid: ownershipAid(ownerAid, 'owner_aid'), bucket: 'default', object_key: path
|
|
318
|
+
await this.request('storage.delete_object', { owner_aid: ownershipAid(ownerAid, 'owner_aid'), bucket: 'default', object_key: path });
|
|
295
319
|
const after = this.metadata(await this.request('storage.head_object', { owner_aid: ownerAid, bucket: 'default', object_key: path, consistency: 'authoritative' }), ownerAid, path);
|
|
296
|
-
if (after.exists
|
|
320
|
+
if (after.exists)
|
|
297
321
|
throw this.error('conflict', 'Storage ownership deletion could not be confirmed');
|
|
298
322
|
}
|
|
299
323
|
}
|
|
@@ -318,12 +342,12 @@ export class OwnershipFiles {
|
|
|
318
342
|
if (snapshot.sub_aid !== subAid || Object.hasOwn(snapshot, 'recovery_ready') && snapshot.recovery_ready !== true || !['locked', 'unlocked'].includes(snapshot.lock) || typeof snapshot.exists !== 'boolean' || !Number.isSafeInteger(snapshot.relation_version) || snapshot.relation_version < 0)
|
|
319
343
|
throw new StateError('NS ownership 2.1 authoritative metadata is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
320
344
|
if (snapshot.exists && !snapshot.etag || !snapshot.exists && (snapshot.lock !== 'unlocked' || snapshot.relation_version !== 0))
|
|
321
|
-
throw new StateError('NS ownership document
|
|
345
|
+
throw new StateError('NS ownership document metadata is unavailable');
|
|
322
346
|
this.fresh(snapshot.observed_at);
|
|
323
347
|
}
|
|
324
348
|
async validateOwnerSnapshot(snapshot, ownerAid, subAid) {
|
|
325
349
|
const expectedService = `st.${ownerAid.slice(ownerAid.indexOf('.') + 1)}`;
|
|
326
|
-
if (snapshot.error || snapshot.authoritative !== true || Object.hasOwn(snapshot, 'recovery_ready') && snapshot.recovery_ready !== true || snapshot.owner_aid !== ownerAid || snapshot.bucket !== 'default' || snapshot.path !== `sub_aids/${subAid}/owner.md` || snapshot.service_aid !== expectedService || snapshot.authenticated_service_aid !== expectedService || typeof snapshot.
|
|
350
|
+
if (snapshot.error || snapshot.authoritative !== true || Object.hasOwn(snapshot, 'recovery_ready') && snapshot.recovery_ready !== true || snapshot.owner_aid !== ownerAid || snapshot.bucket !== 'default' || snapshot.path !== `sub_aids/${subAid}/owner.md` || snapshot.service_aid !== expectedService || snapshot.authenticated_service_aid !== expectedService || typeof snapshot.exists !== 'boolean')
|
|
327
351
|
throw new StateError('Storage authoritative ownership object is unavailable');
|
|
328
352
|
this.fresh(snapshot.observed_at);
|
|
329
353
|
if (snapshot.exists && (!/^[0-9a-f]{64}$/.test(snapshot.sha256) || await ownershipSha256(snapshot.content) !== snapshot.sha256))
|
|
@@ -353,36 +377,42 @@ export class OwnershipFiles {
|
|
|
353
377
|
const file = parseOwnershipFile(content, role, { expectedSubAid: subAid, expectedOwnerAid: role === 'owner' ? ownerAid : undefined, now: this.now() });
|
|
354
378
|
const signer = role === 'agent' ? subAid : ownerAid;
|
|
355
379
|
const certificate = await this.deps.resolveCertificate(signer, file.signature.cert_fingerprint, role === 'agent' && file.frontmatter.type === 'group' ? 'group_identity' : undefined);
|
|
356
|
-
return await verifyOwnershipFile(content, role, certificate, { expectedSubAid: subAid, expectedOwnerAid: role === 'owner' ? ownerAid : undefined, certificateAid: signer, now: this.now() });
|
|
380
|
+
return await verifyOwnershipFile(content, role, certificate, { expectedSubAid: subAid, expectedOwnerAid: role === 'owner' ? ownerAid : undefined, certificateAid: signer, certificateResolver: (aid, fingerprint) => this.deps.resolveCertificate(aid, fingerprint), now: this.now() });
|
|
357
381
|
}
|
|
358
382
|
async checkConsensus(subAid = this.deps.currentAid().aid, pendingTarget) {
|
|
359
383
|
const began = this.now();
|
|
360
|
-
const result = { status: 'unavailable', sub_aid: subAid, owner_aid: null, relation_version: 0, lock: 'unlocked', agent_md_etag: '', agent_md_sha256: '',
|
|
384
|
+
const result = { status: 'unavailable', sub_aid: subAid, owner_aid: null, relation_version: 0, lock: 'unlocked', agent_md_etag: '', agent_md_sha256: '', owner_md_sha256: '' };
|
|
361
385
|
try {
|
|
362
386
|
const first = await this.readAgent(subAid);
|
|
363
387
|
Object.assign(result, { relation_version: first.relation_version, lock: first.lock, agent_md_etag: first.etag, agent_md_sha256: first.exists ? await ownershipSha256(first.content) : '' });
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
if (first.lock === 'unlocked' && first.relation_version === 0 && agent?.statement)
|
|
388
|
+
const agent = first.exists ? await this.verify(first.content, 'agent', subAid) : null;
|
|
389
|
+
if ((agent?.statement?.relation_version ?? 0) !== first.relation_version)
|
|
367
390
|
throw new StateError('NS relation metadata does not match agent.md', { stringCode: 'unavailable', retryable: true });
|
|
368
|
-
const ownerAid =
|
|
391
|
+
const ownerAid = agent?.statement?.owner_aid ?? pendingTarget?.owner_aid;
|
|
369
392
|
let owner = null;
|
|
370
393
|
let object = null;
|
|
371
394
|
if (ownerAid) {
|
|
372
395
|
object = await this.readOwner(ownerAid, subAid);
|
|
373
|
-
result.owner_md_revision = object.revision;
|
|
374
396
|
result.owner_md_sha256 = object.exists ? object.sha256 : '';
|
|
397
|
+
result.owner_md = { owner_aid: ownerAid, bucket: 'default', path: `sub_aids/${subAid}/owner.md`, exists: object.exists, content: object.content, sha256: object.sha256, observed_at: object.observed_at };
|
|
375
398
|
if (object.exists)
|
|
376
399
|
owner = await this.verify(object.content, 'owner', subAid, ownerAid);
|
|
377
400
|
}
|
|
378
401
|
const last = await this.readAgent(subAid);
|
|
379
402
|
if (last.relation_version !== first.relation_version || last.etag !== first.etag || last.lock !== first.lock || last.exists !== first.exists || last.owner_aid !== first.owner_aid || first.exists && await ownershipSha256(last.content) !== result.agent_md_sha256)
|
|
380
403
|
throw new StateError('NS ownership changed during verification');
|
|
404
|
+
if (object && ownerAid) {
|
|
405
|
+
const current = await this.readOwner(ownerAid, subAid);
|
|
406
|
+
if (current.exists !== object.exists || current.sha256 !== object.sha256 || current.content !== object.content)
|
|
407
|
+
throw new StateError('Storage ownership changed during verification', { stringCode: 'unavailable', retryable: true });
|
|
408
|
+
}
|
|
381
409
|
this.fresh(began);
|
|
382
410
|
this.fresh(first.observed_at);
|
|
383
411
|
if (object)
|
|
384
412
|
this.fresh(object.observed_at);
|
|
385
|
-
if (
|
|
413
|
+
if (first.lock === 'unlocked' && agent?.statement) {
|
|
414
|
+
if (!object || object.exists && (!owner?.statement || owner.action !== 'release' || !ownershipStatementsMatch(agent.statement, owner.statement)))
|
|
415
|
+
throw new StateError('current ownership files do not confirm release', { stringCode: 'unavailable', retryable: true });
|
|
386
416
|
result.status = 'released';
|
|
387
417
|
}
|
|
388
418
|
else if (first.lock === 'locked') {
|
|
@@ -425,14 +455,37 @@ export class OwnershipFiles {
|
|
|
425
455
|
const snapshot = await this.readAgent(task.sub_aid);
|
|
426
456
|
if (snapshot.relation_version !== task.base_relation_version || snapshot.lock !== (task.operation === 'bind' ? 'unlocked' : 'locked'))
|
|
427
457
|
throw new StateError('ownership relation version conflict', { stringCode: 'ownership_conflict', retryable: false });
|
|
428
|
-
if (!snapshot.exists)
|
|
429
|
-
|
|
458
|
+
if (!snapshot.exists) {
|
|
459
|
+
const base = task.base_agent_md;
|
|
460
|
+
if (task.operation !== 'bind' || base?.exists)
|
|
461
|
+
throw new StateError('ownership original resource is unavailable', { stringCode: 'conflict' });
|
|
462
|
+
if (!base) {
|
|
463
|
+
task.base_agent_md = { exists: false, raw: '', sha256: await ownershipSha256(''), etag: '', statement: null };
|
|
464
|
+
await task.persist();
|
|
465
|
+
}
|
|
466
|
+
}
|
|
430
467
|
if (snapshot.exists) {
|
|
431
|
-
const file =
|
|
468
|
+
const file = await this.verify(snapshot.content, 'agent', task.sub_aid);
|
|
469
|
+
const base = task.base_agent_md;
|
|
470
|
+
if (base && (base.exists !== snapshot.exists || (base.statement ? !file.statement || !['version', 'declaration_id', 'owner_aid', 'sub_aid', 'relation_version', 'declared_at'].every((key) => base.statement[key] === file.statement[key]) : file.statement || base.raw !== snapshot.content || base.etag !== snapshot.etag || base.sha256 !== await ownershipSha256(snapshot.content))))
|
|
471
|
+
throw new StateError('ownership original relation was replaced', { stringCode: 'conflict', retryable: false });
|
|
472
|
+
if (!base) {
|
|
473
|
+
task.base_agent_md = { exists: true, raw: snapshot.content, sha256: await ownershipSha256(snapshot.content), etag: snapshot.etag, statement: file.statement ?? null };
|
|
474
|
+
await task.persist();
|
|
475
|
+
}
|
|
432
476
|
if (snapshot.lock === 'unlocked' && snapshot.relation_version === 0 && file.statement)
|
|
433
477
|
throw new StateError('NS relation metadata does not match agent.md', { stringCode: 'unavailable', retryable: true });
|
|
434
|
-
if (
|
|
478
|
+
if ((file.statement?.relation_version ?? 0) !== snapshot.relation_version || task.operation !== 'bind' && file.statement?.owner_aid !== task.owner_aid)
|
|
435
479
|
throw new StateError('ownership current owner mismatch', { stringCode: 'conflict', retryable: false });
|
|
480
|
+
if (task.operation === 'bind' && file.statement?.owner_aid === task.owner_aid)
|
|
481
|
+
throw new StateError('current owner release requires sub cleanup before binding again', { stringCode: 'conflict', retryable: false });
|
|
482
|
+
if (task.operation === 'bind' && base?.statement && task.base_owner_md) {
|
|
483
|
+
const original = await this.readOwner(base.statement.owner_aid, task.sub_aid), expected = task.base_owner_md;
|
|
484
|
+
if (original.exists !== expected.exists || original.content !== expected.raw || original.sha256 !== expected.sha256)
|
|
485
|
+
throw new StateError('ownership original released owner file was replaced', { stringCode: 'conflict', retryable: false });
|
|
486
|
+
}
|
|
487
|
+
if (task.operation === 'bind' && file.statement && (await this.checkConsensus(task.sub_aid)).status !== 'released')
|
|
488
|
+
throw new StateError('current ownership files do not confirm release', { stringCode: 'unavailable', retryable: true });
|
|
436
489
|
}
|
|
437
490
|
return snapshot;
|
|
438
491
|
}
|
|
@@ -444,6 +497,11 @@ export class OwnershipFiles {
|
|
|
444
497
|
const file = await this.verify(object.content, 'owner', task.sub_aid, task.owner_aid);
|
|
445
498
|
if (file.action !== 'transfer_owner' || file.statement?.relation_version !== task.base_relation_version || !file.transfer || file.transfer.new_owner_aid !== task.new_owner_aid || file.transfer.target_relation_version !== task.target_relation_version)
|
|
446
499
|
throw new StateError('ownership transfer authorization target changed', { stringCode: 'conflict', retryable: false });
|
|
500
|
+
if (task.base_owner_md?.exists) {
|
|
501
|
+
const original = parseOwnershipFile(task.base_owner_md.raw, 'owner', { expectedSubAid: task.sub_aid, expectedOwnerAid: task.owner_aid }).statement;
|
|
502
|
+
if (!original || !['version', 'declaration_id', 'owner_aid', 'sub_aid', 'relation_version', 'declared_at'].every((key) => original[key] === file.statement[key]))
|
|
503
|
+
throw new StateError('ownership original transfer relation changed', { stringCode: 'conflict', retryable: false });
|
|
504
|
+
}
|
|
447
505
|
if (file.transfer.expires_at <= this.now())
|
|
448
506
|
throw new StateError('ownership transfer authorization expired', { stringCode: 'conflict', retryable: false });
|
|
449
507
|
if (task.authorization_sha256 !== undefined && task.authorization_sha256 !== object.sha256 || task.expires_at !== undefined && task.expires_at !== file.transfer.expires_at)
|
|
@@ -460,42 +518,57 @@ export class OwnershipFiles {
|
|
|
460
518
|
await this.readAgent(task.sub_aid);
|
|
461
519
|
const current = await this.readOwner(ownerAid, task.sub_aid);
|
|
462
520
|
let prepared = task.files[key];
|
|
521
|
+
if (!prepared && action !== 'declare' && !current.exists)
|
|
522
|
+
throw new StateError('ownership original owner file is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
523
|
+
if (!prepared && !task.base_owner_md && action !== 'declare') {
|
|
524
|
+
const snapshot = current;
|
|
525
|
+
task.base_owner_md = { exists: snapshot.exists, raw: snapshot.content, sha256: snapshot.sha256 };
|
|
526
|
+
await task.persist();
|
|
527
|
+
}
|
|
463
528
|
if (!prepared) {
|
|
464
529
|
await this.requireBase(task);
|
|
530
|
+
const base = task.base_owner_md;
|
|
531
|
+
if (ownerAid === task.base_agent_md?.statement?.owner_aid && base && (current.exists !== base.exists || current.content !== base.raw || current.sha256 !== base.sha256))
|
|
532
|
+
throw new StateError('ownership original owner file was replaced', { stringCode: 'conflict', retryable: false });
|
|
465
533
|
if (action === 'transfer_owner')
|
|
466
534
|
task.expires_at = this.now() + 3600;
|
|
467
|
-
const
|
|
535
|
+
const original = action === 'declare' || !current.exists ? null : await this.verify(current.content, 'owner', task.sub_aid, ownerAid);
|
|
536
|
+
if (original && (!original.statement || original.statement.relation_version !== task.base_relation_version))
|
|
537
|
+
throw new StateError('ownership original owner relation changed', { stringCode: 'conflict', retryable: false });
|
|
538
|
+
const ownership = { statement: original?.statement ?? this.statement(ownerAid, task.sub_aid, version), action,
|
|
468
539
|
...(action === 'transfer_owner' ? { transfer: { new_owner_aid: task.new_owner_aid, target_relation_version: task.target_relation_version, expires_at: task.expires_at } } : {}) };
|
|
469
540
|
const payload = `---\nowner_aid: ${ownerAid}\nsub_aid: ${task.sub_aid}\nownership: ${JSON.stringify(ownership)}\n---\n`;
|
|
470
541
|
const raw = await signOwnershipFile(identity, payload, 'owner', { now: this.now() });
|
|
471
542
|
await this.verify(raw, 'owner', task.sub_aid, ownerAid);
|
|
472
|
-
prepared = { raw, sha256: await ownershipSha256(raw), condition: {
|
|
543
|
+
prepared = { raw, sha256: await ownershipSha256(raw), condition: {}, original_owner: { exists: current.exists, raw: current.content, sha256: current.sha256 } };
|
|
473
544
|
if (action === 'transfer_owner')
|
|
474
545
|
prepared.expires_at = task.expires_at;
|
|
475
546
|
task.files[key] = prepared;
|
|
476
547
|
await task.persist();
|
|
477
548
|
}
|
|
478
|
-
if (!prepared.condition ||
|
|
479
|
-
throw new StateError('ownership original
|
|
549
|
+
if (!prepared.condition || Object.keys(prepared.condition).length !== 0 || !prepared.original_owner)
|
|
550
|
+
throw new StateError('ownership original owner.md context is unavailable', { stringCode: 'ownership_conflict' });
|
|
551
|
+
if (prepared.original_owner.revision && current.revision !== prepared.original_owner.revision)
|
|
552
|
+
throw new StateError('legacy owner original revision changed', { stringCode: 'conflict', retryable: false });
|
|
480
553
|
if (current.exists && current.sha256 === prepared.sha256 && current.content === prepared.raw) {
|
|
481
|
-
if (prepared.published_revision !== undefined && prepared.published_revision !== current.revision)
|
|
482
|
-
throw new StateError('ownership published file revision changed', { stringCode: 'ownership_conflict' });
|
|
483
|
-
prepared.published_revision = current.revision;
|
|
484
|
-
await task.persist();
|
|
485
554
|
return prepared;
|
|
486
555
|
}
|
|
556
|
+
if (prepared.original_owner.exists && prepared.original_owner.raw === prepared.raw && prepared.original_owner.sha256 === prepared.sha256)
|
|
557
|
+
throw new StateError('ownership adopted owner file was replaced', { stringCode: 'conflict', retryable: false });
|
|
487
558
|
await this.requireBase(task);
|
|
488
|
-
if (current.revision !== prepared.condition.expected_revision)
|
|
489
|
-
throw new StateError('ownership Storage revision conflict', { stringCode: 'ownership_conflict' });
|
|
490
559
|
if (action === 'transfer_owner' && (!task.expires_at || task.expires_at <= this.now()))
|
|
491
560
|
throw new StateError('ownership transfer authorization expired', { stringCode: 'conflict', retryable: false });
|
|
492
561
|
await this.verify(prepared.raw, 'owner', task.sub_aid, ownerAid);
|
|
562
|
+
const latest = await this.readOwner(ownerAid, task.sub_aid), base = prepared.original_owner;
|
|
563
|
+
if (base.revision && latest.revision !== base.revision)
|
|
564
|
+
throw new StateError('legacy owner original revision changed before publication', { stringCode: 'conflict', retryable: false });
|
|
565
|
+
if (latest.exists !== base.exists || latest.content !== base.raw || latest.sha256 !== base.sha256)
|
|
566
|
+
throw new StateError('ownership original owner file was replaced', { stringCode: 'conflict', retryable: false });
|
|
493
567
|
await task.guard?.();
|
|
494
|
-
await this.deps.writeOwnerMd(ownerAid, task.sub_aid, prepared.raw,
|
|
568
|
+
await this.deps.writeOwnerMd(ownerAid, task.sub_aid, prepared.raw, latest, task.guard);
|
|
495
569
|
const saved = await this.readOwner(ownerAid, task.sub_aid);
|
|
496
570
|
if (!saved.exists || saved.sha256 !== prepared.sha256 || saved.content !== prepared.raw)
|
|
497
571
|
throw new StateError('ownership published file could not be confirmed');
|
|
498
|
-
prepared.published_revision = saved.revision;
|
|
499
572
|
await task.persist();
|
|
500
573
|
return prepared;
|
|
501
574
|
}
|
|
@@ -513,15 +586,29 @@ export class OwnershipFiles {
|
|
|
513
586
|
throw new ValidationError('ownership transfer target must differ from current owner');
|
|
514
587
|
return await this.publishOwner(task, 'transfer', 'transfer_owner', task.owner_aid, task.base_relation_version);
|
|
515
588
|
}
|
|
589
|
+
async requireRecoveredOwner(task) {
|
|
590
|
+
if (!task.legacy_recovery)
|
|
591
|
+
return;
|
|
592
|
+
const pinned = task.files.owner, owner = await this.readOwner(task.owner_aid, task.sub_aid);
|
|
593
|
+
if (!task.legacy_recovery.owner_confirmed || !pinned || !owner.exists || owner.content !== pinned.raw || owner.sha256 !== pinned.sha256)
|
|
594
|
+
throw new StateError('ownership recovered owner candidate was replaced', { stringCode: 'conflict', retryable: false });
|
|
595
|
+
const file = await this.verify(owner.content, 'owner', task.sub_aid, task.owner_aid);
|
|
596
|
+
if (file.action !== 'declare' || file.statement?.relation_version !== task.target_relation_version)
|
|
597
|
+
throw new StateError('ownership recovered owner declaration differs', { stringCode: 'conflict', retryable: false });
|
|
598
|
+
}
|
|
516
599
|
async publishAgent(task, cleanup) {
|
|
517
600
|
const identity = this.local(task.sub_aid), key = cleanup ? 'cleanup' : 'agent';
|
|
518
601
|
const current = await this.readAgent(task.sub_aid);
|
|
602
|
+
if (!cleanup)
|
|
603
|
+
await this.requireRecoveredOwner(task);
|
|
519
604
|
let prepared = task.files[key];
|
|
520
605
|
if (!prepared) {
|
|
521
606
|
if (cleanup) {
|
|
522
|
-
if (current.lock !== 'unlocked' || current.relation_version !== task.
|
|
607
|
+
if (current.lock !== 'unlocked' || current.relation_version !== task.base_relation_version)
|
|
523
608
|
throw new StateError('owner_locked or ownership cleanup version conflict');
|
|
524
609
|
const prior = current.exists ? parseOwnershipFile(current.content, 'agent', { expectedSubAid: task.sub_aid, now: this.now() }) : null;
|
|
610
|
+
if (task.expected_agent_statement && (!prior?.statement || !['version', 'declaration_id', 'owner_aid', 'sub_aid', 'relation_version', 'declared_at'].every((field) => prior.statement[field] === task.expected_agent_statement[field])))
|
|
611
|
+
throw new StateError('ownership cleanup historical declaration changed', { stringCode: 'conflict', retryable: false });
|
|
525
612
|
if (prior?.statement && (prior.statement.owner_aid !== task.owner_aid || prior.statement.relation_version !== task.base_relation_version))
|
|
526
613
|
throw new StateError('ownership cleanup historical authorization mismatch', { stringCode: 'conflict', retryable: false });
|
|
527
614
|
}
|
|
@@ -538,7 +625,7 @@ export class OwnershipFiles {
|
|
|
538
625
|
throw new StateError('ownership target declaration mismatch', { stringCode: 'conflict', retryable: false });
|
|
539
626
|
}
|
|
540
627
|
const ownerAid = task.operation === 'transfer' ? task.new_owner_aid : task.owner_aid;
|
|
541
|
-
const payload = buildOwnershipAgentMdV21(current.exists ? current.content : buildDefaultAgentMd(task.sub_aid), cleanup ? undefined : this.statement(ownerAid, task.sub_aid, task.target_relation_version));
|
|
628
|
+
const payload = buildOwnershipAgentMdV21(current.exists ? current.content : buildDefaultAgentMd(task.sub_aid, { type: this.deps.currentAidType?.() ?? 'agent' }), cleanup ? undefined : this.statement(ownerAid, task.sub_aid, task.target_relation_version));
|
|
542
629
|
const raw = await signOwnershipFile(identity, payload, 'agent', { now: this.now() });
|
|
543
630
|
await this.verify(raw, 'agent', task.sub_aid);
|
|
544
631
|
prepared = { raw, sha256: await ownershipSha256(raw), condition: current.exists ? { if_match: current.etag } : { if_none_match: '*' } };
|
|
@@ -546,8 +633,8 @@ export class OwnershipFiles {
|
|
|
546
633
|
await task.persist();
|
|
547
634
|
}
|
|
548
635
|
if (!prepared.condition || !(typeof prepared.condition.if_match === 'string' && prepared.condition.if_match || prepared.condition.if_none_match === '*'))
|
|
549
|
-
throw new StateError('ownership original
|
|
550
|
-
if (current.exists && await ownershipSha256(current.content) === prepared.sha256 && current.content === prepared.raw && current.relation_version === task.target_relation_version) {
|
|
636
|
+
throw new StateError('ownership original resource condition is unavailable', { stringCode: 'ownership_conflict' });
|
|
637
|
+
if (current.exists && await ownershipSha256(current.content) === prepared.sha256 && current.content === prepared.raw && current.relation_version === (cleanup ? 0 : task.target_relation_version)) {
|
|
551
638
|
if (prepared.published_etag !== undefined && prepared.published_etag !== current.etag)
|
|
552
639
|
throw new StateError('ownership published agent ETag changed', { stringCode: 'ownership_conflict' });
|
|
553
640
|
prepared.published_etag = current.etag;
|
|
@@ -557,7 +644,7 @@ export class OwnershipFiles {
|
|
|
557
644
|
if (current.exists ? current.etag !== prepared.condition.if_match : prepared.condition.if_none_match !== '*')
|
|
558
645
|
throw new StateError('ownership agent.md condition conflict', { stringCode: 'ownership_conflict' });
|
|
559
646
|
if (cleanup) {
|
|
560
|
-
if (current.lock !== 'unlocked' || current.relation_version !== task.
|
|
647
|
+
if (current.lock !== 'unlocked' || current.relation_version !== task.base_relation_version)
|
|
561
648
|
throw new StateError('owner_locked or ownership cleanup version conflict');
|
|
562
649
|
}
|
|
563
650
|
else {
|
|
@@ -566,10 +653,12 @@ export class OwnershipFiles {
|
|
|
566
653
|
await this.verifyTransfer(task);
|
|
567
654
|
}
|
|
568
655
|
await this.verify(prepared.raw, 'agent', task.sub_aid);
|
|
656
|
+
if (!cleanup)
|
|
657
|
+
await this.requireRecoveredOwner(task);
|
|
569
658
|
await task.guard?.();
|
|
570
|
-
const saved = await this.deps.submitAgentMdRaw(task.sub_aid, prepared.raw, prepared.condition);
|
|
659
|
+
const saved = await this.deps.submitAgentMdRaw(task.sub_aid, prepared.raw, prepared.condition, task.guard);
|
|
571
660
|
this.validateAgentSnapshot(saved, task.sub_aid);
|
|
572
|
-
if (!saved.exists || await ownershipSha256(saved.content) !== prepared.sha256 || saved.relation_version !== task.target_relation_version)
|
|
661
|
+
if (!saved.exists || await ownershipSha256(saved.content) !== prepared.sha256 || saved.relation_version !== (cleanup ? 0 : task.target_relation_version))
|
|
573
662
|
throw new StateError('NS ownership submitted file could not be confirmed');
|
|
574
663
|
prepared.published_etag = saved.etag;
|
|
575
664
|
await task.persist();
|
|
@@ -581,14 +670,15 @@ export class OwnershipFiles {
|
|
|
581
670
|
this.local(task.owner_aid);
|
|
582
671
|
await this.readAgent(task.sub_aid);
|
|
583
672
|
const prepared = task.files[task.operation === 'release' ? 'release' : 'transfer'];
|
|
584
|
-
if (!prepared
|
|
673
|
+
if (!prepared || prepared.cleaned)
|
|
585
674
|
return;
|
|
675
|
+
const began = this.now();
|
|
586
676
|
const consensus = await this.checkConsensus(task.sub_aid);
|
|
587
677
|
if (consensus.status === 'not_applicable')
|
|
588
678
|
throw new StateError(consensus.reason || 'group/org use their dedicated lifecycle ownership', { stringCode: 'not_applicable', retryable: false });
|
|
589
679
|
if (consensus.status === 'invalid' || consensus.status === 'unavailable')
|
|
590
680
|
throw new StateError(consensus.reason || 'ownership cleanup verification failed', { stringCode: consensus.status, retryable: consensus.status === 'unavailable' });
|
|
591
|
-
if (
|
|
681
|
+
if (task.operation === 'release' ? !(consensus.status === 'unbound' && consensus.relation_version === 0 || consensus.status === 'consensus') : consensus.relation_version !== task.target_relation_version || consensus.status !== 'consensus' || consensus.owner_aid !== task.new_owner_aid)
|
|
592
682
|
throw new StateError('ownership cleanup relation is not confirmed', { stringCode: 'conflict', retryable: false });
|
|
593
683
|
const object = await this.readOwner(task.owner_aid, task.sub_aid);
|
|
594
684
|
if (!object.exists) {
|
|
@@ -596,12 +686,24 @@ export class OwnershipFiles {
|
|
|
596
686
|
await task.persist();
|
|
597
687
|
return;
|
|
598
688
|
}
|
|
599
|
-
if (object.
|
|
600
|
-
throw new StateError('ownership cleanup
|
|
689
|
+
if (object.content !== prepared.raw || object.sha256 !== prepared.sha256)
|
|
690
|
+
throw new StateError('ownership cleanup original owner file was replaced', { stringCode: 'conflict', retryable: false });
|
|
691
|
+
const agent = await this.readAgent(task.sub_aid);
|
|
692
|
+
if (this.now() - began > 30 || agent.etag !== consensus.agent_md_etag || await ownershipSha256(agent.content) !== consensus.agent_md_sha256 || agent.lock !== consensus.lock || agent.relation_version !== consensus.relation_version)
|
|
693
|
+
throw new StateError('ownership cleanup current files changed', { stringCode: 'unavailable', retryable: true });
|
|
601
694
|
if (!this.deps.deleteOwnerMd)
|
|
602
|
-
throw new StateError('ownership
|
|
695
|
+
throw new StateError('ownership owner file delete unsupported');
|
|
696
|
+
const latest = await this.readOwner(task.owner_aid, task.sub_aid);
|
|
697
|
+
if (!latest.exists) {
|
|
698
|
+
prepared.cleaned = true;
|
|
699
|
+
await task.persist();
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (latest.content !== prepared.raw || latest.sha256 !== prepared.sha256)
|
|
703
|
+
throw new StateError('ownership cleanup original owner file was replaced', { stringCode: 'conflict', retryable: false });
|
|
603
704
|
await task.guard?.();
|
|
604
|
-
|
|
705
|
+
this.fresh(began);
|
|
706
|
+
await this.deps.deleteOwnerMd(task.owner_aid, task.sub_aid);
|
|
605
707
|
prepared.cleaned = true;
|
|
606
708
|
await task.persist();
|
|
607
709
|
}
|