@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.
Files changed (154) hide show
  1. package/CHANGELOG.md +176 -78
  2. package/_packed_docs/CHANGELOG.md +176 -78
  3. package/_packed_docs/INDEX.md +288 -269
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +101 -95
  5. package/_packed_docs/agent.md/SCHEMA.md +7 -7
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +372 -331
  12. package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
  13. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +676 -676
  14. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +8 -12
  15. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +445 -0
  16. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
  17. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -0
  18. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
  19. package/_packed_docs/protocol/agent.md/SCHEMA.md +1 -1
  20. 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
  21. 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
  22. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
  23. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
  24. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
  25. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +70 -63
  26. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +43 -40
  27. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +179 -19
  28. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +50 -49
  29. package/_packed_docs/sdk/09-group-rpc-manual.md +261 -254
  30. package/_packed_docs/sdk/09-message-rpc-manual.md +253 -160
  31. package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
  32. 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
  33. 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
  34. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +105 -92
  35. package/_packed_docs/sdk/INDEX.md +197 -167
  36. package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
  37. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
  38. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -0
  39. package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
  40. 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
  41. 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
  42. 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
  43. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -0
  44. package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
  45. package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. package/dist/agent-md-schema.d.ts +16 -4
  55. package/dist/agent-md-schema.d.ts.map +1 -1
  56. package/dist/agent-md-schema.js +65 -30
  57. package/dist/agent-md-schema.js.map +1 -1
  58. package/dist/agent-md.d.ts +35 -3
  59. package/dist/agent-md.d.ts.map +1 -1
  60. package/dist/agent-md.js +384 -150
  61. package/dist/agent-md.js.map +1 -1
  62. package/dist/aid-store.d.ts +59 -0
  63. package/dist/aid-store.d.ts.map +1 -1
  64. package/dist/aid-store.js +202 -16
  65. package/dist/aid-store.js.map +1 -1
  66. package/dist/aid.d.ts +4 -1
  67. package/dist/aid.d.ts.map +1 -1
  68. package/dist/aid.js +24 -42
  69. package/dist/aid.js.map +1 -1
  70. package/dist/auth.d.ts +1 -1
  71. package/dist/auth.d.ts.map +1 -1
  72. package/dist/auth.js +89 -18
  73. package/dist/auth.js.map +1 -1
  74. package/dist/bundle.js +6410 -1848
  75. package/dist/cert-utils.d.ts +50 -1
  76. package/dist/cert-utils.d.ts.map +1 -1
  77. package/dist/cert-utils.js +123 -56
  78. package/dist/cert-utils.js.map +1 -1
  79. package/dist/client/delivery.d.ts +108 -16
  80. package/dist/client/delivery.d.ts.map +1 -1
  81. package/dist/client/delivery.js +1411 -184
  82. package/dist/client/delivery.js.map +1 -1
  83. package/dist/client/rpc-pipeline.d.ts +4 -1
  84. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  85. package/dist/client/rpc-pipeline.js +203 -117
  86. package/dist/client/rpc-pipeline.js.map +1 -1
  87. package/dist/client/v2-e2ee.d.ts +22 -3
  88. package/dist/client/v2-e2ee.d.ts.map +1 -1
  89. package/dist/client/v2-e2ee.js +1312 -684
  90. package/dist/client/v2-e2ee.js.map +1 -1
  91. package/dist/client.d.ts +23 -2
  92. package/dist/client.d.ts.map +1 -1
  93. package/dist/client.js +419 -101
  94. package/dist/client.js.map +1 -1
  95. package/dist/config.d.ts +4 -0
  96. package/dist/config.d.ts.map +1 -1
  97. package/dist/config.js +1 -0
  98. package/dist/config.js.map +1 -1
  99. package/dist/crypto.d.ts.map +1 -1
  100. package/dist/crypto.js +8 -6
  101. package/dist/crypto.js.map +1 -1
  102. package/dist/events.d.ts +24 -3
  103. package/dist/events.d.ts.map +1 -1
  104. package/dist/events.js +171 -29
  105. package/dist/events.js.map +1 -1
  106. package/dist/facades.d.ts +42 -1
  107. package/dist/facades.d.ts.map +1 -1
  108. package/dist/facades.js +368 -12
  109. package/dist/facades.js.map +1 -1
  110. package/dist/index.d.ts +6 -4
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +1 -1
  113. package/dist/index.js.map +1 -1
  114. package/dist/keystore/index.d.ts +14 -1
  115. package/dist/keystore/index.d.ts.map +1 -1
  116. package/dist/keystore/indexeddb-identity-store.d.ts +4 -0
  117. package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
  118. package/dist/keystore/indexeddb-identity-store.js +30 -13
  119. package/dist/keystore/indexeddb-identity-store.js.map +1 -1
  120. package/dist/keystore/indexeddb-shared.d.ts +19 -2
  121. package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
  122. package/dist/keystore/indexeddb-shared.js +193 -6
  123. package/dist/keystore/indexeddb-shared.js.map +1 -1
  124. package/dist/keystore/indexeddb-token-store.d.ts +15 -2
  125. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
  126. package/dist/keystore/indexeddb-token-store.js +308 -27
  127. package/dist/keystore/indexeddb-token-store.js.map +1 -1
  128. package/dist/org-protocol.d.ts +45 -0
  129. package/dist/org-protocol.d.ts.map +1 -1
  130. package/dist/org-protocol.js +827 -21
  131. package/dist/org-protocol.js.map +1 -1
  132. package/dist/ownership-v21.d.ts +43 -9
  133. package/dist/ownership-v21.d.ts.map +1 -1
  134. package/dist/ownership-v21.js +183 -81
  135. package/dist/ownership-v21.js.map +1 -1
  136. package/dist/ownership-workflow.d.ts +36 -1
  137. package/dist/ownership-workflow.d.ts.map +1 -1
  138. package/dist/ownership-workflow.js +610 -89
  139. package/dist/ownership-workflow.js.map +1 -1
  140. package/dist/seq-tracker.d.ts +2 -0
  141. package/dist/seq-tracker.d.ts.map +1 -1
  142. package/dist/seq-tracker.js +3 -2
  143. package/dist/seq-tracker.js.map +1 -1
  144. package/dist/tools/cross-sdk-agent.d.ts +61 -3
  145. package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
  146. package/dist/tools/cross-sdk-agent.js +524 -45
  147. package/dist/tools/cross-sdk-agent.js.map +1 -1
  148. package/dist/transport.d.ts +4 -0
  149. package/dist/transport.d.ts.map +1 -1
  150. package/dist/transport.js +64 -30
  151. package/dist/transport.js.map +1 -1
  152. package/dist/version.d.ts +1 -1
  153. package/dist/version.js +1 -1
  154. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { parseFileFrontmatter, validateAgentMdCertificate, validateAgentMdDocument } from './agent-md-schema.js';
2
- import { buildAgentMdSignatureBlock, certFingerprint, isStrictBase64, parseAgentMdTailSignature, publicKeyFingerprint, publicKeyMatchesFingerprint, signBytes, verifySignatureWithCert } from './cert-utils.js';
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
- if (Number(fields.timestamp) > now + 300)
169
- throw new ValidationError('ownership file timestamp is too far in the future');
170
- if (certificate.chain_valid === false || certificate.revoked === true || typeof certificate.state === 'string' && certificate.state !== 'active_signing')
171
- throw new ValidationError('ownership signing certificate is not currently verified and active_signing', { stringCode: 'invalid' });
172
- if (certificate.chain_valid !== true || certificate.state !== 'active_signing' || certificate.revoked !== false || certificate.revocation_checked !== true || !Number.isSafeInteger(certificate.observed_at) || certificate.observed_at <= 0 || certificate.observed_at > now + 5 || certificate.observed_at < now - 30)
173
- throw new StateError('ownership certificate current status is unavailable', { stringCode: 'unavailable', retryable: true });
174
- validateAgentMdCertificate(certificate.cert_pem, signer, Number(fields.timestamp), now);
175
- if (await certFingerprint(certificate.cert_pem) !== fields.cert_fingerprint)
176
- throw new ValidationError('ownership certificate fingerprint mismatch');
177
- if (fields.public_key_fingerprint && !await publicKeyMatchesFingerprint(certificate.cert_pem, fields.public_key_fingerprint))
178
- throw new ValidationError('ownership public key fingerprint mismatch');
179
- if (!await verifySignatureWithCert(certificate.cert_pem, fields.signature, new TextEncoder().encode(parsed.payload)))
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
- const signature = await signBytes(identity.privateKeyPem, new TextEncoder().encode(parsed.payload));
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 || code === -32602 ? 'unsupported_protocol' : 'unavailable', `Storage ownership request failed: ${String(error)}`);
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' || typeof value.revision !== 'string' || !value.revision || !Number.isSafeInteger(observed) || observed < now - 30 || observed > now + 5 || observed <= 0)
234
- throw this.error('unavailable', 'Storage authoritative namespace, revision or freshness unavailable');
235
- if (value.exists && (typeof value.sha256 !== 'string' || !/^[0-9a-f]{64}$/.test(value.sha256) || typeof value.object_id !== 'string' || !value.object_id || !Number.isSafeInteger(value.version) || Number(value.version) <= 0))
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, revision: value.revision,
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', { ...params, expected_revision: before.revision });
251
+ const body = await this.request('storage.get_object', params);
250
252
  const current = this.metadata(body, ownerAid, path);
251
- if (!current.exists || current.revision !== before.revision || current.sha256 !== before.sha256 || body.object_id !== beforeRaw.object_id || body.version !== beforeRaw.version)
252
- throw this.error('unavailable', 'Storage ownership body revision changed');
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.revision !== before.revision || after.sha256 !== before.sha256 || afterRaw.object_id !== beforeRaw.object_id || afterRaw.version !== beforeRaw.version || Math.trunc(this.deps.now?.() ?? Date.now() / 1000) - began > 30)
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, expectedRevision) {
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
- if (typeof expectedRevision !== 'string' || !expectedRevision)
280
- throw this.error('invalid', 'ownership expected_revision is required');
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
- let binary = '';
283
- for (const byte of bytes)
284
- binary += String.fromCharCode(byte);
285
- await this.request('storage.put_object', { owner_aid: ownershipAid(ownerAid, 'owner_aid'), bucket: 'default', object_key: `sub_aids/${ownershipAid(subAid, 'sub_aid')}/owner.md`, content: btoa(binary), content_type: 'text/markdown', is_private: false, overwrite: true, expected_revision: expectedRevision });
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, expectedRevision) {
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, expected_revision: expectedRevision });
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 || after.revision === expectedRevision)
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 or ledger is unavailable');
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.revision !== 'string' || !snapshot.revision || typeof snapshot.exists !== 'boolean')
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: '', owner_md_revision: '', owner_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 released = first.lock === 'unlocked' && first.relation_version > 0;
365
- const agent = !released && first.exists ? await this.verify(first.content, 'agent', subAid) : null;
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 = released ? undefined : agent?.statement?.owner_aid ?? pendingTarget?.owner_aid;
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 (released) {
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
- throw new StateError('initial agent.md is not published', { stringCode: 'unavailable', retryable: true });
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 = task.operation === 'bind' && snapshot.relation_version > 0 ? parseOwnershipFile(snapshot.content, 'agent', { expectedSubAid: task.sub_aid }) : await this.verify(snapshot.content, 'agent', task.sub_aid);
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 (task.operation !== 'bind' && file.statement?.owner_aid !== task.owner_aid || task.operation === 'bind' && file.statement && file.statement.relation_version >= snapshot.relation_version)
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 ownership = { statement: this.statement(ownerAid, task.sub_aid, version), action,
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: { expected_revision: current.revision } };
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 || typeof prepared.condition.expected_revision !== 'string' || !prepared.condition.expected_revision)
479
- throw new StateError('ownership original Storage revision is unavailable', { stringCode: 'ownership_conflict' });
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, prepared.condition.expected_revision);
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.target_relation_version)
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 agent.md condition is unavailable', { stringCode: 'ownership_conflict' });
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.target_relation_version)
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?.published_revision || prepared.cleaned)
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 (consensus.relation_version !== task.target_relation_version || (task.operation === 'release' ? consensus.status !== 'released' : consensus.status !== 'consensus' || consensus.owner_aid !== task.new_owner_aid))
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.revision !== prepared.published_revision || object.sha256 !== prepared.sha256)
600
- throw new StateError('ownership cleanup revision conflict');
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 conditional delete unsupported');
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
- await this.deps.deleteOwnerMd(task.owner_aid, task.sub_aid, prepared.published_revision);
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
  }