@agentunion/fastaun 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 (129) 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 +13 -1
  55. package/dist/agent-md-schema.js +63 -33
  56. package/dist/agent-md-schema.js.map +1 -1
  57. package/dist/agent-md.d.ts +30 -3
  58. package/dist/agent-md.js +293 -152
  59. package/dist/agent-md.js.map +1 -1
  60. package/dist/aid-store.d.ts +42 -0
  61. package/dist/aid-store.js +224 -44
  62. package/dist/aid-store.js.map +1 -1
  63. package/dist/aid.js +18 -13
  64. package/dist/aid.js.map +1 -1
  65. package/dist/auth.js +78 -2
  66. package/dist/auth.js.map +1 -1
  67. package/dist/cert-utils.d.ts +44 -1
  68. package/dist/cert-utils.js +157 -56
  69. package/dist/cert-utils.js.map +1 -1
  70. package/dist/client/delivery.d.ts +68 -1
  71. package/dist/client/delivery.js +1196 -135
  72. package/dist/client/delivery.js.map +1 -1
  73. package/dist/client/rpc-pipeline.d.ts +5 -1
  74. package/dist/client/rpc-pipeline.js +240 -111
  75. package/dist/client/rpc-pipeline.js.map +1 -1
  76. package/dist/client/v2-e2ee.d.ts +12 -2
  77. package/dist/client/v2-e2ee.js +842 -379
  78. package/dist/client/v2-e2ee.js.map +1 -1
  79. package/dist/client.d.ts +26 -2
  80. package/dist/client.js +420 -75
  81. package/dist/client.js.map +1 -1
  82. package/dist/config.d.ts +4 -0
  83. package/dist/config.js +1 -0
  84. package/dist/config.js.map +1 -1
  85. package/dist/events.d.ts +23 -4
  86. package/dist/events.js +171 -15
  87. package/dist/events.js.map +1 -1
  88. package/dist/facades.d.ts +43 -1
  89. package/dist/facades.js +368 -12
  90. package/dist/facades.js.map +1 -1
  91. package/dist/index.d.ts +7 -4
  92. package/dist/index.js +1 -1
  93. package/dist/index.js.map +1 -1
  94. package/dist/keystore/aid-db.d.ts +7 -2
  95. package/dist/keystore/aid-db.js +194 -43
  96. package/dist/keystore/aid-db.js.map +1 -1
  97. package/dist/keystore/index.d.ts +15 -1
  98. package/dist/keystore/local-token-store.d.ts +10 -2
  99. package/dist/keystore/local-token-store.js +81 -16
  100. package/dist/keystore/local-token-store.js.map +1 -1
  101. package/dist/org-protocol.d.ts +45 -0
  102. package/dist/org-protocol.js +829 -22
  103. package/dist/org-protocol.js.map +1 -1
  104. package/dist/ownership-v21.d.ts +40 -9
  105. package/dist/ownership-v21.js +174 -79
  106. package/dist/ownership-v21.js.map +1 -1
  107. package/dist/ownership-workflow.d.ts +22 -0
  108. package/dist/ownership-workflow.js +861 -66
  109. package/dist/ownership-workflow.js.map +1 -1
  110. package/dist/secret-store/file-store.js +84 -0
  111. package/dist/secret-store/file-store.js.map +1 -1
  112. package/dist/seq-tracker.d.ts +1 -0
  113. package/dist/seq-tracker.js +2 -2
  114. package/dist/seq-tracker.js.map +1 -1
  115. package/dist/tools/cross-sdk-agent.d.ts +67 -3
  116. package/dist/tools/cross-sdk-agent.js +575 -42
  117. package/dist/tools/cross-sdk-agent.js.map +1 -1
  118. package/dist/transport.d.ts +4 -0
  119. package/dist/transport.js +108 -44
  120. package/dist/transport.js.map +1 -1
  121. package/dist/types.d.ts +1 -0
  122. package/dist/v2/e2ee/encrypt-group.js +1 -0
  123. package/dist/v2/e2ee/encrypt-group.js.map +1 -1
  124. package/dist/v2/e2ee/encrypt-p2p.js +1 -0
  125. package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
  126. package/dist/v2/e2ee/types.d.ts +1 -0
  127. package/dist/version.d.ts +1 -1
  128. package/dist/version.js +1 -1
  129. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
- import { updateOwnershipAgentMd } from './agent-md.js';
2
+ import { buildDefaultAgentMd, updateOwnershipAgentMd } from './agent-md.js';
3
3
  import { parseFileFrontmatter, validateAgentMdCertificate, validateAgentMdDocument } from './agent-md-schema.js';
4
- import { buildAgentMdSignatureBlock, certFingerprint, isStrictBase64, parseAgentMdTailSignature, publicKeyFingerprint, publicKeyMatchesFingerprint, signBytes, verifySignatureWithCert } from './cert-utils.js';
4
+ import { certFingerprint, isStrictBase64, parseAgentMdTailSignature, publicKeyMatchesFingerprint, signDocument, verifyDocument } from './cert-utils.js';
5
5
  import { StateError, ValidationError } from './errors.js';
6
6
  const AID_RE = /^[a-z0-9_][a-z0-9_-]*(?:\.[a-z0-9_][a-z0-9_-]*)+$/;
7
7
  export const OWNERSHIP_UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
@@ -108,7 +108,10 @@ 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 item of parsed.signatures) {
114
+ const fields = item.fields;
112
115
  if (!/^sha256:[0-9a-f]{64}$/.test(fields.cert_fingerprint) || 'public_key_fingerprint' in fields && !/^sha256:[0-9a-f]{64}$/.test(fields.public_key_fingerprint))
113
116
  throw new ValidationError('ownership file fingerprint is invalid');
114
117
  if (!/^[1-9][0-9]*$/.test(fields.timestamp) || !Number.isSafeInteger(Number(fields.timestamp)))
@@ -121,7 +124,7 @@ export function parseOwnershipFile(content, role, context = {}) {
121
124
  let action;
122
125
  let transfer;
123
126
  if (role === 'agent') {
124
- frontmatter = validateAgentMdDocument(parsed.payload, { expectedAid: context.expectedSubAid, validateOwnership: false, now: context.now }).frontmatter;
127
+ frontmatter = validateAgentMdDocument(parsed.payload, { expectedAid: context.expectedSubAid, validateOwnership: true, now: context.now }).frontmatter;
125
128
  requireOwnershipApplicable(frontmatter.type);
126
129
  statement = validateOwnershipAgentFrontmatter(frontmatter, context.expectedSubAid);
127
130
  if (context.expectedOwnerAid && statement && statement.owner_aid !== context.expectedOwnerAid)
@@ -187,8 +190,18 @@ export function verifyOwnershipFile(content, role, certificate, context = {}) {
187
190
  throw new ValidationError('ownership certificate fingerprint mismatch');
188
191
  if ('public_key_fingerprint' in fields && !publicKeyMatchesFingerprint(certificate.cert_pem, fields.public_key_fingerprint))
189
192
  throw new ValidationError('ownership public key fingerprint mismatch');
190
- if (!verifySignatureWithCert(certificate.cert_pem, Buffer.from(fields.signature, 'base64'), Buffer.from(parsed.payload, 'utf8')))
191
- throw new ValidationError('ownership file signature verification failed');
193
+ const additional = new Map();
194
+ for (const item of parseAgentMdTailSignature(new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(parsed.raw)).signatures.slice(1)) {
195
+ const fields = item.fields, extra = context.certificateResolver?.(fields.signer_aid, fields.cert_fingerprint, Number(fields.timestamp));
196
+ if (extra && (extra.chain_valid === false || extra.revoked === true || typeof extra.state === 'string' && extra.state !== 'active_signing'))
197
+ throw new ValidationError('ownership additional signing certificate is not currently verified and active_signing', { stringCode: 'invalid' });
198
+ if (!extra || extra.chain_valid !== true || extra.revoked !== false || extra.revocation_checked !== true || extra.state !== 'active_signing' || !Number.isSafeInteger(extra.observed_at) || extra.observed_at <= 0 || extra.observed_at > now + 5 || extra.observed_at < now - 30 || !extra.cert_pem?.trim())
199
+ throw new StateError('ownership additional certificate current status is unavailable', { stringCode: 'unavailable', retryable: true });
200
+ additional.set(`${fields.signer_aid};${fields.cert_fingerprint}`, extra.cert_pem);
201
+ }
202
+ const verified = verifyDocument(new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(parsed.raw), { expectedAid: signer, primaryCertPem: certificate.cert_pem, requireCurrentValidity: true, now, certificateResolver: (aid, fingerprint) => additional.get(`${aid};${fingerprint}`) });
203
+ if (!verified.valid)
204
+ throw new ValidationError(verified.reason ?? 'ownership file signature verification failed');
192
205
  return parsed;
193
206
  }
194
207
  export function signOwnershipFile(identity, content, role, context = {}) {
@@ -204,8 +217,7 @@ export function signOwnershipFile(identity, content, role, context = {}) {
204
217
  throw new ValidationError('ownership payload must end with a newline before signature');
205
218
  const now = Math.trunc(context.now ?? Date.now() / 1000);
206
219
  validateAgentMdCertificate(identity.certPem, identity.aid, now, { now });
207
- const signature = signBytes(identity.privateKeyPem, Buffer.from(parsed.payload, 'utf8')).toString('base64');
208
- return parsed.payload + buildAgentMdSignatureBlock(identity.certFingerprint, now, signature, publicKeyFingerprint(identity.certPem), role === 'agent' ? 'AUN-SIGNATURE-AGENTMD-V2' : 'AUN-SIGNATURE-OWNERSHIP-V2');
220
+ return signDocument(identity, parsed.payload, { timestamp: now, tag: role === 'agent' ? 'AGENTMD-V2' : 'OWNERSHIP-V2' }).signed;
209
221
  }
210
222
  export function ownershipSha256(content) {
211
223
  return createHash('sha256').update(content).digest('hex');
@@ -225,14 +237,13 @@ export class OwnershipFiles {
225
237
  async checkConsensus(subAid, target) {
226
238
  ownershipAid(subAid, 'sub_aid');
227
239
  const began = this.deps.now();
228
- 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: '' };
240
+ const result = { status: 'unavailable', sub_aid: subAid, owner_aid: null, relation_version: 0, lock: 'unlocked', agent_md_etag: '', agent_md_sha256: '', owner_md_sha256: '' };
229
241
  try {
230
242
  const before = await this.readAgent(subAid);
231
243
  Object.assign(result, { relation_version: before.relation_version, lock: before.lock, agent_md_etag: before.etag, agent_md_sha256: before.exists ? ownershipSha256(before.content) : '' });
232
- const released = before.lock === 'unlocked' && before.relation_version > 0;
233
- const agent = before.exists && !released ? await this.verifyFile(before.content, 'agent', subAid) : undefined;
244
+ const agent = before.exists ? await this.verifyFile(before.content, 'agent', subAid) : undefined;
234
245
  const statement = agent?.statement;
235
- if (before.lock === 'unlocked' && before.relation_version === 0 && statement)
246
+ if (before.relation_version !== (statement?.relation_version ?? 0))
236
247
  throw ownershipFailure('unavailable', 'NS relation metadata does not match agent.md', true);
237
248
  if (before.lock === 'locked' && (!statement || statement.relation_version !== before.relation_version))
238
249
  throw ownershipFailure('unavailable', 'NS locked relation does not match agent.md', true);
@@ -240,29 +251,37 @@ export class OwnershipFiles {
240
251
  if (before.owner_aid !== undefined && before.owner_aid !== currentOwner)
241
252
  throw ownershipFailure('unavailable', 'NS owner metadata does not match agent.md', true);
242
253
  result.owner_aid = currentOwner;
243
- result.status = before.lock === 'unlocked' ? before.relation_version === 0 ? 'unbound' : 'released' : 'single_sided';
244
- const ownerAid = released ? undefined : currentOwner ?? target?.owner_aid ?? statement?.owner_aid;
254
+ result.status = statement ? 'single_sided' : 'unbound';
255
+ const ownerAid = statement?.owner_aid ?? target?.owner_aid;
256
+ let ownerBefore;
245
257
  if (ownerAid) {
246
258
  const owner = await this.readOwner(ownerAid, subAid);
247
- result.owner_md_revision = owner.revision;
248
- result.owner_md_sha256 = owner.sha256 ?? '';
259
+ ownerBefore = owner;
260
+ result.owner_md_sha256 = owner.exists ? owner.sha256 ?? '' : '';
261
+ result.owner_md = { owner_aid: ownerAid, bucket: 'default', path: `sub_aids/${subAid}/owner.md`, exists: owner.exists, content: owner.content, sha256: owner.sha256 ?? '', observed_at: owner.observed_at };
249
262
  if (owner.exists) {
250
263
  const declaration = await this.verifyFile(owner.content, 'owner', subAid, ownerAid);
251
- if (before.lock === 'locked') {
264
+ if (statement) {
252
265
  if (declaration.statement.relation_version !== before.relation_version)
253
266
  throw new ValidationError('ownership relation_version mismatch');
254
- if (declaration.action === 'release')
255
- throw ownershipFailure('unavailable', 'NS has not confirmed the current release', true);
256
- result.status = 'consensus';
267
+ result.status = declaration.action === 'release' ? 'released' : 'consensus';
257
268
  }
258
269
  else if (target && declaration.action === 'declare' && declaration.statement.relation_version === target.relation_version && target.relation_version === before.relation_version + 1) {
259
270
  result.status = 'single_sided';
260
271
  }
261
272
  }
262
- else if (before.lock === 'locked')
263
- throw ownershipFailure('unavailable', 'NS has not confirmed owner object absence', true);
273
+ else if (statement)
274
+ result.status = 'released';
264
275
  }
276
+ if (statement && before.lock !== (result.status === 'released' ? 'unlocked' : 'locked'))
277
+ throw ownershipFailure('unavailable', 'NS lock does not match current ownership materials', true);
278
+ result.owner_aid = result.status === 'consensus' ? statement.owner_aid : null;
265
279
  const after = await this.readAgent(subAid);
280
+ if (ownerAid && ownerBefore) {
281
+ const ownerAfter = await this.readOwner(ownerAid, subAid);
282
+ if (ownerBefore.exists !== ownerAfter.exists || ownerBefore.content !== ownerAfter.content || ownerBefore.sha256 !== ownerAfter.sha256)
283
+ throw ownershipFailure('unavailable', 'owner.md changed during consensus check', true);
284
+ }
266
285
  if (before.exists !== after.exists || before.etag !== after.etag || before.relation_version !== after.relation_version || before.lock !== after.lock || before.content !== after.content || before.owner_aid !== after.owner_aid || this.deps.now() - began > 30)
267
286
  throw ownershipFailure('unavailable', 'NS relation changed or verification window elapsed during consensus check', true);
268
287
  return result;
@@ -316,11 +335,15 @@ export class OwnershipFiles {
316
335
  return this.publishAgent(task, async (agent) => {
317
336
  if (agent.lock === 'locked')
318
337
  throw ownershipFailure('owner_locked', 'NS has not confirmed release');
319
- if (agent.relation_version !== task.target_relation_version)
320
- throw ownershipFailure('conflict', 'release relation_version changed');
321
338
  const parsed = parseOwnershipFile(agent.content, 'agent', { expectedSubAid: task.sub_aid });
339
+ if (!parsed.statement && agent.relation_version === 0)
340
+ return {};
341
+ if (agent.relation_version !== task.base_relation_version)
342
+ throw ownershipFailure('conflict', 'release relation_version changed');
322
343
  if (parsed.statement && (parsed.statement.owner_aid !== task.owner_aid || parsed.statement.relation_version !== task.base_relation_version))
323
344
  throw ownershipFailure('conflict', 'agent.md no longer contains the released relation');
345
+ if (task.expected_agent_statement && Object.keys(task.expected_agent_statement).some((field) => task.expected_agent_statement[field] !== parsed.statement?.[field]))
346
+ throw ownershipFailure('conflict', 'agent.md no longer contains the approved statement');
324
347
  return {};
325
348
  }, true);
326
349
  }
@@ -338,8 +361,8 @@ export class OwnershipFiles {
338
361
  async cleanupOwnerFile(task) {
339
362
  this.requireLocal(task.owner_aid);
340
363
  const source = this.savedFile(task, task.operation === 'release' ? 'release' : 'transfer');
341
- if (!source?.published_revision)
342
- throw ownershipFailure('conflict', 'original published owner revision is unavailable');
364
+ if (!source)
365
+ throw ownershipFailure('conflict', 'original owner file is unavailable');
343
366
  const consensus = await this.checkConsensus(task.sub_aid);
344
367
  if (consensus.status === 'not_applicable')
345
368
  throw ownershipFailure('not_applicable', consensus.reason ?? 'group/org use their dedicated lifecycle ownership');
@@ -347,19 +370,26 @@ export class OwnershipFiles {
347
370
  throw new ValidationError(consensus.reason ?? 'ownership consensus is invalid', { stringCode: 'invalid' });
348
371
  if (consensus.status === 'unavailable')
349
372
  throw ownershipFailure(consensus.code ?? 'unavailable', consensus.reason ?? 'ownership consensus is unavailable', true);
350
- if (consensus.relation_version !== task.target_relation_version || task.operation === 'release' && consensus.status !== 'released' || task.operation === 'transfer' && (consensus.status !== 'consensus' || consensus.owner_aid !== task.new_owner_aid))
351
- throw ownershipFailure('conflict', 'NS has not confirmed this task target');
373
+ if (task.operation === 'release' && consensus.status === 'released')
374
+ return;
375
+ if (task.operation === 'release' ? consensus.status !== 'unbound' && consensus.status !== 'consensus' : consensus.relation_version !== task.target_relation_version || consensus.status !== 'consensus' || consensus.owner_aid !== task.new_owner_aid)
376
+ throw ownershipFailure('conflict', 'current materials do not permit this task cleanup');
352
377
  const current = await this.readOwner(task.owner_aid, task.sub_aid);
353
- if (current.exists && (current.revision !== source.published_revision || current.sha256 !== source.hash))
354
- throw ownershipFailure('conflict', 'owner file revision changed before cleanup');
378
+ if (current.exists && (current.content !== source.raw || current.sha256 !== source.hash))
379
+ throw ownershipFailure('conflict', 'owner file changed before cleanup');
355
380
  if (!task.files.cleanup) {
356
- task.files.cleanup = { raw: source.raw, hash: source.hash, condition: { expected_revision: source.published_revision } };
381
+ task.files.cleanup = { raw: source.raw, hash: source.hash, condition: {} };
357
382
  await task.persist();
358
383
  }
359
384
  else
360
385
  this.savedFile(task, 'cleanup');
361
- if (current.exists)
362
- await this.deps.deleteOwnerMd(task.owner_aid, task.sub_aid, source.published_revision);
386
+ const beforeDelete = await this.readOwner(task.owner_aid, task.sub_aid);
387
+ if (beforeDelete.exists && (beforeDelete.content !== source.raw || beforeDelete.sha256 !== source.hash))
388
+ throw ownershipFailure('conflict', 'owner file changed before cleanup');
389
+ if (beforeDelete.exists) {
390
+ task.assertActive?.();
391
+ await this.deps.deleteOwnerMd(task.owner_aid, task.sub_aid);
392
+ }
363
393
  task.files.cleanup.deleted = true;
364
394
  await task.persist();
365
395
  }
@@ -379,7 +409,7 @@ export class OwnershipFiles {
379
409
  async readOwner(ownerAid, subAid) {
380
410
  const value = await this.deps.readOwnerMd(ownerAid, subAid);
381
411
  const now = this.deps.now();
382
- if (value.owner_aid !== ownerAid || value.sub_aid !== undefined && value.sub_aid !== subAid || value.bucket !== 'default' || value.path !== `sub_aids/${subAid}/owner.md` || typeof value.exists !== 'boolean' || typeof value.revision !== 'string' || !value.revision || !Number.isSafeInteger(value.observed_at) || value.observed_at > now + 5 || value.observed_at < now - 30 || value.authoritative !== true || value.recovery_ready !== undefined && value.recovery_ready !== true || value.service_aid !== `st.${ownerAid.slice(ownerAid.indexOf('.') + 1)}` || value.service_aid !== value.authenticated_service_aid || typeof value.content !== 'string' || value.exists && !/^[0-9a-f]{64}$/.test(value.sha256 ?? ''))
412
+ if (value.owner_aid !== ownerAid || value.sub_aid !== undefined && value.sub_aid !== subAid || value.bucket !== 'default' || value.path !== `sub_aids/${subAid}/owner.md` || typeof value.exists !== 'boolean' || !Number.isSafeInteger(value.observed_at) || value.observed_at > now + 5 || value.observed_at < now - 30 || value.authoritative !== true || value.recovery_ready !== undefined && value.recovery_ready !== true || value.service_aid !== `st.${ownerAid.slice(ownerAid.indexOf('.') + 1)}` || value.service_aid !== value.authenticated_service_aid || typeof value.content !== 'string' || value.exists && !/^[0-9a-f]{64}$/.test(value.sha256 ?? ''))
383
413
  throw ownershipFailure('unavailable', 'Storage authoritative observation is incomplete or changed', true);
384
414
  if (value.exists && value.sha256 !== ownershipSha256(value.content))
385
415
  throw ownershipFailure('unavailable', 'Storage complete file digest does not match authoritative observation', true);
@@ -396,13 +426,19 @@ export class OwnershipFiles {
396
426
  const parsed = parseOwnershipFile(raw, role, context);
397
427
  const signer = role === 'agent' ? subAid : String(parsed.frontmatter.owner_aid);
398
428
  const certificate = await this.deps.resolveCertificate(signer, parsed.signature.cert_fingerprint, role === 'agent' && parsed.frontmatter.type === 'group' ? 'group_identity' : undefined);
399
- return verifyOwnershipFile(raw, role, certificate, { ...context, now: this.deps.now() });
429
+ const additional = new Map();
430
+ for (const item of parseAgentMdTailSignature(raw).signatures.slice(1))
431
+ additional.set(`${item.fields.signer_aid};${item.fields.cert_fingerprint}`, await this.deps.resolveCertificate(item.fields.signer_aid, item.fields.cert_fingerprint));
432
+ return verifyOwnershipFile(raw, role, certificate, { ...context, now: this.deps.now(), certificateResolver: (aid, fingerprint) => additional.get(`${aid};${fingerprint}`) });
400
433
  }
401
434
  async requireTransfer(task) {
402
435
  const owner = await this.readOwner(task.owner_aid, task.sub_aid);
403
436
  if (!owner.exists)
404
437
  throw ownershipFailure('unavailable', 'original owner authorization is missing', true);
405
438
  const authorization = await this.verifyFile(owner.content, 'owner', task.sub_aid, task.owner_aid);
439
+ const saved = task.files.transfer;
440
+ if (saved && (saved.raw !== owner.content || saved.hash !== owner.sha256))
441
+ throw ownershipFailure('conflict', 'fixed transfer authorization changed');
406
442
  if (authorization.action !== 'transfer_owner' || authorization.statement.relation_version !== task.base_relation_version || authorization.transfer.new_owner_aid !== task.new_owner_aid || authorization.transfer.target_relation_version !== task.target_relation_version)
407
443
  throw ownershipFailure('conflict', 'transfer authorization does not match this task');
408
444
  const expiresAt = authorization.transfer.expires_at;
@@ -429,17 +465,26 @@ export class OwnershipFiles {
429
465
  }
430
466
  async assertBase(task, agent) {
431
467
  ownershipInteger(task.base_relation_version, 'base_relation_version', 0);
432
- if (ownershipInteger(task.target_relation_version, 'target_relation_version') !== task.base_relation_version + 1 || agent.relation_version !== task.base_relation_version)
468
+ const target = ownershipInteger(task.target_relation_version, 'target_relation_version');
469
+ if (target !== task.base_relation_version + 1 && !(task.operation === 'release' && target === task.base_relation_version) || agent.relation_version !== task.base_relation_version)
433
470
  throw ownershipFailure('conflict', 'NS relation_version changed');
434
- if (!agent.exists)
435
- throw ownershipFailure('unavailable', 'initial agent.md is not published', true);
436
- const parsed = agent.lock === 'unlocked' && agent.relation_version > 0 ? parseOwnershipFile(agent.content, 'agent', { expectedSubAid: task.sub_aid }) : await this.verifyFile(agent.content, 'agent', task.sub_aid);
437
- if (agent.lock === 'unlocked' && agent.relation_version === 0 && parsed.statement)
471
+ if (!agent.exists) {
472
+ if (task.operation !== 'bind' || task.base_relation_version !== 0 || agent.lock !== 'unlocked' || task.base_agent_md?.exists)
473
+ throw ownershipFailure('conflict', 'original agent.md is missing');
474
+ return;
475
+ }
476
+ const parsed = await this.verifyFile(agent.content, 'agent', task.sub_aid);
477
+ const base = task.base_agent_md;
478
+ if (base && (agent.exists !== base.exists || (base.statement ? !parsed.statement || Object.keys(base.statement).some((field) => base.statement[field] !== parsed.statement[field]) : parsed.statement || agent.content !== base.raw || ownershipSha256(agent.content) !== base.sha256)))
479
+ throw ownershipFailure('conflict', 'the original ownership statement was replaced');
480
+ if (agent.relation_version !== (parsed.statement?.relation_version ?? 0))
438
481
  throw ownershipFailure('unavailable', 'NS relation metadata does not match agent.md', true);
439
482
  if (task.operation === 'bind' && parsed.frontmatter.type === 'human')
440
483
  throw ownershipFailure('invalid', 'human cannot declare an owner');
441
484
  if (task.operation === 'bind' && agent.lock !== 'unlocked')
442
485
  throw ownershipFailure('owner_locked', 'an existing owner relation is locked');
486
+ if (task.operation === 'bind' && parsed.statement?.owner_aid === this.targetOwner(task))
487
+ throw ownershipFailure('conflict', 'clear the released agent.md statement before binding the same owner');
443
488
  if (task.operation !== 'bind' && (agent.lock !== 'locked' || parsed.statement?.owner_aid !== task.owner_aid || parsed.statement.relation_version !== task.base_relation_version))
444
489
  throw ownershipFailure('conflict', 'the original relation is no longer current');
445
490
  }
@@ -451,49 +496,66 @@ export class OwnershipFiles {
451
496
  }
452
497
  savedFile(task, key) {
453
498
  const saved = task.files[key];
454
- if (saved && (typeof saved.raw !== 'string' || saved.hash !== ownershipSha256(saved.raw) || !this.validSavedCondition(key, saved.condition) || saved.published_revision !== undefined && (typeof saved.published_revision !== 'string' || !saved.published_revision)))
499
+ if (saved && (typeof saved.raw !== 'string' || saved.hash !== ownershipSha256(saved.raw) || !this.validSavedCondition(key, saved.condition) || saved.published_etag !== undefined && (typeof saved.published_etag !== 'string' || !saved.published_etag)))
455
500
  throw ownershipFailure('persistence_unavailable', 'saved ownership file is corrupt');
501
+ const original = saved?.original_owner;
502
+ if (saved && key !== 'agent' && key !== 'cleanup' && (!original || typeof original.exists !== 'boolean' || typeof original.raw !== 'string' || original.sha256 !== (original.exists ? ownershipSha256(original.raw) : '') || !original.exists && original.raw !== ''))
503
+ throw ownershipFailure('persistence_unavailable', 'original owner.md evidence is corrupt');
456
504
  return saved;
457
505
  }
458
506
  validSavedCondition(key, condition) {
459
- if (!condition || typeof condition !== 'object' || Object.keys(condition).length !== 1)
507
+ if (!condition || typeof condition !== 'object')
508
+ return false;
509
+ if (key !== 'agent')
510
+ return Object.keys(condition).length === 0;
511
+ if (Object.keys(condition).length !== 1)
460
512
  return false;
461
513
  if (key === 'agent')
462
514
  return typeof condition.if_match === 'string' && condition.if_match.length > 0 || condition.if_none_match === '*';
463
- return typeof condition.expected_revision === 'string' && condition.expected_revision.length > 0;
515
+ return false;
464
516
  }
465
517
  async publishOwner(task, key, ownerAid, prepare) {
466
518
  let saved = this.savedFile(task, key);
467
519
  await this.readAgent(task.sub_aid);
468
520
  const current = await this.readOwner(ownerAid, task.sub_aid);
469
521
  if (!saved) {
522
+ const base = task.base_owner_md;
523
+ if (base && ownerAid === task.base_agent_md?.statement?.owner_aid && (current.exists !== base.exists || current.content !== base.raw || (current.exists ? current.sha256 : '') !== base.sha256))
524
+ throw ownershipFailure('conflict', 'original owner.md material changed before publication');
470
525
  const ownership = await prepare(current);
471
526
  const raw = signOwnershipFile(this.deps.currentAid(), `---\nowner_aid: ${ownerAid}\nsub_aid: ${task.sub_aid}\nownership: ${JSON.stringify(ownership)}\n---\n`, 'owner', { now: this.deps.now(), expectedOwnerAid: ownerAid, expectedSubAid: task.sub_aid });
472
- saved = { raw, hash: ownershipSha256(raw), condition: { expected_revision: current.revision } };
527
+ saved = { raw, hash: ownershipSha256(raw), condition: {}, original_owner: { exists: current.exists, raw: current.content, sha256: current.exists ? ownershipSha256(current.content) : '' } };
473
528
  if (key === 'transfer')
474
529
  saved.expires_at = ownership.transfer.expires_at;
475
530
  task.files[key] = saved;
476
531
  await task.persist();
477
532
  }
478
533
  await this.verifyFile(saved.raw, 'owner', task.sub_aid, ownerAid);
534
+ if (saved.original_owner?.revision && current.revision !== saved.original_owner.revision)
535
+ throw ownershipFailure('conflict', 'legacy owner original revision changed');
479
536
  if (current.exists && current.sha256 === saved.hash && current.content === saved.raw) {
480
- if (saved.published_revision !== undefined && current.revision !== saved.published_revision)
481
- throw ownershipFailure('conflict', 'owner file published revision changed');
482
- saved.published_revision = current.revision;
483
537
  await task.persist();
484
538
  return current;
485
539
  }
486
- if (current.revision !== saved.condition.expected_revision)
487
- throw ownershipFailure('conflict', 'owner file original revision changed');
540
+ const original = saved.original_owner;
541
+ if (!original || typeof original.exists !== 'boolean' || typeof original.raw !== 'string' || original.sha256 !== (original.exists ? ownershipSha256(original.raw) : ''))
542
+ throw ownershipFailure('persistence_unavailable', 'owner file original material is missing or corrupt');
543
+ if (current.exists !== original.exists || current.content !== original.raw || (current.exists ? current.sha256 : '') !== original.sha256)
544
+ throw ownershipFailure('conflict', 'owner file original material changed');
488
545
  await this.assertBase(task, await this.readAgent(task.sub_aid));
489
546
  if (key === 'owner' && task.operation === 'transfer')
490
547
  await this.requireTransfer(task);
491
548
  if (key === 'transfer' && saved.expires_at <= this.deps.now())
492
549
  throw ownershipFailure('conflict', 'transfer authorization expired');
493
- const published = await this.deps.writeOwnerMd(ownerAid, task.sub_aid, saved.raw, saved.condition.expected_revision);
494
- if (!published.exists || !published.revision || published.sha256 !== saved.hash || published.content !== saved.raw)
550
+ const beforeWrite = await this.readOwner(ownerAid, task.sub_aid);
551
+ if (original.revision && beforeWrite.revision !== original.revision)
552
+ throw ownershipFailure('conflict', 'legacy owner original revision changed before publication');
553
+ if (beforeWrite.exists !== original.exists || beforeWrite.content !== original.raw || (beforeWrite.exists ? beforeWrite.sha256 : '') !== original.sha256)
554
+ throw ownershipFailure('conflict', 'owner file original material changed before publication');
555
+ task.assertActive?.();
556
+ const published = await this.deps.writeOwnerMd(ownerAid, task.sub_aid, saved.raw, beforeWrite, task.assertActive);
557
+ if (!published.exists || published.sha256 !== saved.hash || published.content !== saved.raw)
495
558
  throw ownershipFailure('unavailable', 'owner publication could not be verified', true);
496
- saved.published_revision = published.revision;
497
559
  await task.persist();
498
560
  return published;
499
561
  }
@@ -504,24 +566,24 @@ export class OwnershipFiles {
504
566
  const fields = await prepare(current);
505
567
  if (cleanup && !parseOwnershipFile(current.content, 'agent', { now: this.deps.now() }).statement)
506
568
  return current;
507
- const payload = updateOwnershipAgentMd(current.content, fields);
569
+ const payload = updateOwnershipAgentMd(current.exists ? current.content : buildDefaultAgentMd(task.sub_aid, { type: this.deps.currentType?.() ?? 'agent' }), fields);
508
570
  const raw = signOwnershipFile(this.deps.currentAid(), payload.endsWith('\n') || payload.endsWith('\r') ? payload : `${payload}\n`, 'agent', { now: this.deps.now(), expectedSubAid: task.sub_aid });
509
571
  saved = { raw, hash: ownershipSha256(raw), condition: current.exists ? { if_match: current.etag } : { if_none_match: '*' } };
510
572
  task.files.agent = saved;
511
573
  await task.persist();
512
574
  }
513
575
  await this.verifyFile(saved.raw, 'agent', task.sub_aid);
514
- if (current.exists && ownershipSha256(current.content) === saved.hash && current.content === saved.raw && current.relation_version === task.target_relation_version && current.lock === (cleanup ? 'unlocked' : 'locked')) {
515
- if (saved.published_revision !== undefined && current.etag !== saved.published_revision)
576
+ if (current.exists && ownershipSha256(current.content) === saved.hash && current.content === saved.raw && current.relation_version === (cleanup ? 0 : task.target_relation_version) && current.lock === (cleanup ? 'unlocked' : 'locked')) {
577
+ if (saved.published_etag !== undefined && current.etag !== saved.published_etag)
516
578
  throw ownershipFailure('conflict', 'agent.md published ETag changed');
517
- saved.published_revision = current.etag;
579
+ saved.published_etag = current.etag;
518
580
  await task.persist();
519
581
  return current;
520
582
  }
521
- if (current.etag !== saved.condition.if_match)
583
+ if (saved.condition.if_none_match === '*' ? current.exists : !current.exists || current.etag !== saved.condition.if_match)
522
584
  throw ownershipFailure('conflict', 'agent.md original ETag condition changed');
523
585
  if (cleanup) {
524
- if (current.lock !== 'unlocked' || current.relation_version !== task.target_relation_version)
586
+ if (current.lock !== 'unlocked' || current.relation_version !== task.base_relation_version)
525
587
  throw ownershipFailure('conflict', 'release target is no longer current');
526
588
  }
527
589
  else {
@@ -532,13 +594,17 @@ export class OwnershipFiles {
532
594
  if (!target.exists)
533
595
  throw ownershipFailure('unavailable', 'target owner declaration is missing', true);
534
596
  const declaration = await this.verifyFile(target.content, 'owner', task.sub_aid, this.targetOwner(task));
597
+ const savedOwner = task.files.owner;
598
+ if (savedOwner && (savedOwner.raw !== target.content || savedOwner.hash !== target.sha256))
599
+ throw ownershipFailure('conflict', 'fixed target owner declaration changed');
535
600
  if (declaration.action !== 'declare' || declaration.statement.relation_version !== task.target_relation_version)
536
601
  throw ownershipFailure('conflict', 'target owner declaration changed');
537
602
  }
538
- const published = await this.deps.submitAgentMdRaw(task.sub_aid, saved.raw, saved.condition);
539
- if (published.content !== saved.raw || published.relation_version !== task.target_relation_version || published.lock !== (cleanup ? 'unlocked' : 'locked'))
603
+ task.assertActive?.();
604
+ const published = await this.deps.submitAgentMdRaw(task.sub_aid, saved.raw, saved.condition, task.assertActive);
605
+ if (published.content !== saved.raw || published.relation_version !== (cleanup ? 0 : task.target_relation_version) || published.lock !== (cleanup ? 'unlocked' : 'locked'))
540
606
  throw ownershipFailure('unavailable', 'agent.md publication could not be verified', true);
541
- saved.published_revision = published.etag;
607
+ saved.published_etag = published.etag;
542
608
  await task.persist();
543
609
  return published;
544
610
  }
@@ -557,21 +623,24 @@ export class OwnershipStorageAdapter {
557
623
  return result;
558
624
  }
559
625
  catch (error) {
626
+ const reading = method === 'storage.head_object' || method === 'storage.get_object';
560
627
  const code = error && typeof error === 'object' ? error.code : undefined;
561
628
  if (error instanceof ValidationError && code !== -32601 || error instanceof StateError && error.stringCode === 'invalid')
562
629
  throw new ValidationError(error.message, { stringCode: 'invalid' });
563
630
  if (error instanceof StateError && ['unavailable', 'unsupported_protocol', 'conflict'].includes(error.stringCode))
564
631
  throw ownershipFailure(error.stringCode, error.message, error.stringCode !== 'conflict');
632
+ if (reading && error instanceof StateError && error.stringCode === 'forbidden')
633
+ throw ownershipFailure('unavailable', error.message, true);
565
634
  if (error instanceof StateError && error.stringCode)
566
635
  throw error;
567
- throw ownershipFailure(code === -32009 ? 'conflict' : code === -32004 ? 'forbidden' : code === -32601 ? 'unsupported_protocol' : 'unavailable', error instanceof Error ? error.message : String(error), ![-32009, -32004].includes(code ?? 0));
636
+ throw ownershipFailure(code === -32009 ? 'conflict' : code === -32004 ? reading ? 'unavailable' : 'forbidden' : code === -32601 ? 'unsupported_protocol' : 'unavailable', error instanceof Error ? error.message : String(error), code !== -32009 && (reading || code !== -32004));
568
637
  }
569
638
  }
570
639
  metadata(value, ownerAid, path) {
571
640
  const now = this.now();
572
641
  const expectedService = `st.${ownerAid.slice(ownerAid.indexOf('.') + 1)}`;
573
- if (value.consistency !== 'authoritative' || value.owner_aid !== ownerAid || value.bucket !== 'default' || value.path !== path || typeof value.exists !== 'boolean' || typeof value.revision !== 'string' || !value.revision || !Number.isSafeInteger(value.observed_at) || Number(value.observed_at) < now - 30 || Number(value.observed_at) > now + 5 || value.recovery_ready !== undefined && value.recovery_ready !== true || value.service_aid !== undefined && value.service_aid !== expectedService || value.authenticated_service_aid !== undefined && value.authenticated_service_aid !== expectedService || 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) < 1))
574
- throw ownershipFailure('unavailable', 'Storage authoritative namespace, revision or freshness metadata is incomplete', true);
642
+ if (value.consistency !== 'authoritative' || value.owner_aid !== ownerAid || value.bucket !== 'default' || value.path !== path || typeof value.exists !== 'boolean' || !Number.isSafeInteger(value.observed_at) || Number(value.observed_at) < now - 30 || Number(value.observed_at) > now + 5 || value.recovery_ready !== undefined && value.recovery_ready !== true || value.service_aid !== undefined && value.service_aid !== expectedService || value.authenticated_service_aid !== undefined && value.authenticated_service_aid !== expectedService || value.exists && (typeof value.sha256 !== 'string' || !/^[0-9a-f]{64}$/.test(value.sha256)))
643
+ throw ownershipFailure('unavailable', 'Storage authoritative namespace or freshness metadata is incomplete', true);
575
644
  }
576
645
  async readObject(ownerAid, path) {
577
646
  ownershipAid(ownerAid, 'owner_aid');
@@ -582,9 +651,9 @@ export class OwnershipStorageAdapter {
582
651
  let content = '';
583
652
  let final = before;
584
653
  if (before.exists) {
585
- const object = await this.call('storage.get_object', { ...params, expected_revision: before.revision });
654
+ const object = await this.call('storage.get_object', params);
586
655
  this.metadata(object, ownerAid, path);
587
- if (object.exists !== true || object.revision !== before.revision || object.sha256 !== before.sha256 || object.object_id !== before.object_id || object.version !== before.version || typeof object.content !== 'string')
656
+ if (object.exists !== true || object.sha256 !== before.sha256 || before.revision !== undefined && object.revision !== before.revision || typeof object.content !== 'string')
588
657
  throw ownershipFailure('unavailable', 'Storage object changed or full Base64 content is unavailable', true);
589
658
  if (object.content !== '' && !isStrictBase64(object.content))
590
659
  throw ownershipFailure('unavailable', 'Storage Base64 content is unavailable', true);
@@ -599,33 +668,59 @@ export class OwnershipStorageAdapter {
599
668
  }
600
669
  final = await this.call('storage.head_object', params);
601
670
  this.metadata(final, ownerAid, path);
602
- if (final.exists !== true || final.revision !== before.revision || final.sha256 !== before.sha256 || final.object_id !== before.object_id || final.version !== before.version || this.now() - began > 30)
603
- throw ownershipFailure('unavailable', 'Storage revision changed during authoritative read', true);
671
+ if (final.exists !== true || final.sha256 !== before.sha256 || before.revision !== undefined && final.revision !== before.revision || this.now() - began > 30)
672
+ throw ownershipFailure('unavailable', 'Storage material changed during authoritative read', true);
604
673
  }
605
674
  const service = `st.${ownerAid.slice(ownerAid.indexOf('.') + 1)}`;
606
- return { owner_aid: ownerAid, bucket: 'default', path, exists: Boolean(final.exists), revision: String(final.revision), observed_at: Number(final.observed_at),
607
- content, sha256: final.exists ? String(final.sha256) : undefined, authoritative: true, recovery_ready: final.recovery_ready, service_aid: service, authenticated_service_aid: service };
675
+ return { owner_aid: ownerAid, bucket: 'default', path, exists: Boolean(final.exists), observed_at: Number(final.observed_at),
676
+ content, sha256: final.exists ? String(final.sha256) : undefined, authoritative: true, recovery_ready: final.recovery_ready, service_aid: service, authenticated_service_aid: service, version: final.exists && Number.isSafeInteger(final.version) && Number(final.version) > 0 ? final.version : undefined, ...(typeof final.revision === 'string' && final.revision ? { revision: final.revision } : {}) };
608
677
  }
609
678
  async readOwnerMd(ownerAid, subAid) {
610
679
  ownershipAid(subAid, 'sub_aid');
611
680
  return this.readObject(ownerAid, `sub_aids/${subAid}/owner.md`);
612
681
  }
613
- async writeOwnerMd(ownerAid, subAid, raw, originalRevision) {
682
+ async writeOwnerMd(ownerAid, subAid, raw, original, assertActive) {
614
683
  this.requireLocal(ownerAid);
615
684
  ownershipAid(subAid, 'sub_aid');
616
- if (typeof originalRevision !== 'string' || !originalRevision)
617
- throw ownershipFailure('invalid', 'original Storage revision is required');
618
- await this.call('storage.put_object', { owner_aid: ownerAid, bucket: 'default', object_key: `sub_aids/${subAid}/owner.md`, content: Buffer.from(raw, 'utf8').toString('base64'), content_type: 'text/markdown; charset=utf-8', is_private: false, overwrite: true, expected_revision: originalRevision });
685
+ const path = `sub_aids/${subAid}/owner.md`;
686
+ const before = await this.readOwnerMd(ownerAid, subAid);
687
+ if (original && (before.exists !== original.exists || before.content !== original.content || (before.exists ? before.sha256 : '') !== (original.exists ? original.sha256 : '') || original.revision !== undefined && before.revision !== original.revision))
688
+ throw ownershipFailure('conflict', 'owner file original material changed');
689
+ const bytes = new TextEncoder().encode(raw);
690
+ const expectedVersion = (original ?? before).exists ? (original ?? before).version : 0;
691
+ if (typeof expectedVersion !== 'number' || !Number.isSafeInteger(expectedVersion) || expectedVersion < (before.exists ? 1 : 0))
692
+ throw ownershipFailure('unavailable', 'Storage owner.md version is unavailable', true);
693
+ assertActive?.();
694
+ const session = await this.call('storage.create_upload_session', { owner_aid: ownerAid, bucket: 'default', object_key: path, size_bytes: bytes.length, content_type: 'text/markdown; charset=utf-8', expected_version: expectedVersion, overwrite: true });
695
+ const uploadURL = String(session.upload_url ?? ''), sessionID = String(session.session_id ?? '');
696
+ if (!uploadURL || !sessionID)
697
+ throw ownershipFailure('unavailable', 'Storage owner.md upload session is incomplete', true);
698
+ const headers = {};
699
+ if (session.headers && typeof session.headers === 'object')
700
+ for (const [key, value] of Object.entries(session.headers))
701
+ if (value !== undefined && value !== null && key.toLowerCase() !== 'content-type')
702
+ headers[key] = String(value);
703
+ headers['Content-Type'] = 'text/markdown; charset=utf-8';
704
+ assertActive?.();
705
+ if (this.deps.httpPut)
706
+ await this.deps.httpPut(uploadURL, bytes, headers);
707
+ else {
708
+ const response = await fetch(uploadURL, { method: 'PUT', body: bytes, headers });
709
+ if (!response.ok)
710
+ throw ownershipFailure('unavailable', `Storage owner.md HTTP PUT failed: ${response.status}`, true);
711
+ }
712
+ const complete = { owner_aid: ownerAid, bucket: 'default', object_key: path, session_id: sessionID, size_bytes: bytes.length, sha256: ownershipSha256(bytes), content_type: 'text/markdown; charset=utf-8', is_private: false, overwrite: true };
713
+ complete.expected_version = expectedVersion;
714
+ assertActive?.();
715
+ await this.call('storage.complete_upload', complete);
619
716
  return this.readOwnerMd(ownerAid, subAid);
620
717
  }
621
- async deleteOwnerMd(ownerAid, subAid, originalRevision) {
718
+ async deleteOwnerMd(ownerAid, subAid) {
622
719
  this.requireLocal(ownerAid);
623
720
  ownershipAid(subAid, 'sub_aid');
624
- if (typeof originalRevision !== 'string' || !originalRevision)
625
- throw ownershipFailure('invalid', 'original Storage revision is required');
626
- await this.call('storage.delete_object', { owner_aid: ownerAid, bucket: 'default', object_key: `sub_aids/${subAid}/owner.md`, expected_revision: originalRevision });
721
+ await this.call('storage.delete_object', { owner_aid: ownerAid, bucket: 'default', object_key: `sub_aids/${subAid}/owner.md` });
627
722
  if ((await this.readOwnerMd(ownerAid, subAid)).exists)
628
- throw ownershipFailure('unavailable', 'Storage conditional deletion could not be confirmed', true);
723
+ throw ownershipFailure('unavailable', 'Storage deletion could not be confirmed', true);
629
724
  }
630
725
  requireLocal(ownerAid) {
631
726
  if (ownershipAid(ownerAid, 'owner_aid') !== this.deps.currentAid())