@agentunion/fastaun-browser 0.5.19 → 0.5.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +129 -78
  2. package/_packed_docs/CHANGELOG.md +129 -78
  3. package/_packed_docs/INDEX.md +201 -195
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +93 -90
  5. package/_packed_docs/agent.md/SCHEMA.md +7 -7
  6. 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
  7. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1072 -1072
  8. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +676 -676
  9. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +8 -12
  10. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +443 -0
  11. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
  12. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +658 -0
  13. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
  14. package/_packed_docs/protocol/agent.md/SCHEMA.md +1 -1
  15. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +1 -1
  16. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +67 -63
  17. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +1 -1
  18. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +35 -8
  19. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +3 -3
  20. package/_packed_docs/sdk/09-group-rpc-manual.md +258 -254
  21. package/_packed_docs/sdk/09-message-rpc-manual.md +7 -4
  22. package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
  23. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +61 -52
  24. package/_packed_docs/sdk/INDEX.md +92 -80
  25. package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
  26. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
  27. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +922 -0
  28. package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
  29. package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +300 -0
  30. package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +382 -0
  31. 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
  32. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +476 -0
  33. package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
  34. package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. package/dist/agent-md-schema.d.ts +15 -4
  44. package/dist/agent-md-schema.d.ts.map +1 -1
  45. package/dist/agent-md-schema.js +64 -30
  46. package/dist/agent-md-schema.js.map +1 -1
  47. package/dist/agent-md.d.ts +10 -2
  48. package/dist/agent-md.d.ts.map +1 -1
  49. package/dist/agent-md.js +278 -121
  50. package/dist/agent-md.js.map +1 -1
  51. package/dist/aid-store.d.ts +46 -0
  52. package/dist/aid-store.d.ts.map +1 -1
  53. package/dist/aid-store.js +149 -10
  54. package/dist/aid-store.js.map +1 -1
  55. package/dist/aid.d.ts +4 -1
  56. package/dist/aid.d.ts.map +1 -1
  57. package/dist/aid.js +23 -42
  58. package/dist/aid.js.map +1 -1
  59. package/dist/auth.d.ts +1 -1
  60. package/dist/auth.d.ts.map +1 -1
  61. package/dist/auth.js +85 -18
  62. package/dist/auth.js.map +1 -1
  63. package/dist/bundle.js +1581 -781
  64. package/dist/cert-utils.d.ts +49 -1
  65. package/dist/cert-utils.d.ts.map +1 -1
  66. package/dist/cert-utils.js +102 -56
  67. package/dist/cert-utils.js.map +1 -1
  68. package/dist/client/delivery.d.ts +7 -0
  69. package/dist/client/delivery.d.ts.map +1 -1
  70. package/dist/client/delivery.js +243 -28
  71. package/dist/client/delivery.js.map +1 -1
  72. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  73. package/dist/client/rpc-pipeline.js +89 -77
  74. package/dist/client/rpc-pipeline.js.map +1 -1
  75. package/dist/client/v2-e2ee.d.ts.map +1 -1
  76. package/dist/client/v2-e2ee.js +99 -80
  77. package/dist/client/v2-e2ee.js.map +1 -1
  78. package/dist/client.d.ts.map +1 -1
  79. package/dist/client.js +31 -17
  80. package/dist/client.js.map +1 -1
  81. package/dist/crypto.d.ts.map +1 -1
  82. package/dist/crypto.js +8 -6
  83. package/dist/crypto.js.map +1 -1
  84. package/dist/events.d.ts +5 -2
  85. package/dist/events.d.ts.map +1 -1
  86. package/dist/events.js +33 -14
  87. package/dist/events.js.map +1 -1
  88. package/dist/facades.js +5 -5
  89. package/dist/facades.js.map +1 -1
  90. package/dist/index.d.ts +1 -1
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js.map +1 -1
  93. package/dist/org-protocol.d.ts.map +1 -1
  94. package/dist/org-protocol.js +72 -12
  95. package/dist/org-protocol.js.map +1 -1
  96. package/dist/ownership-v21.d.ts +25 -8
  97. package/dist/ownership-v21.d.ts.map +1 -1
  98. package/dist/ownership-v21.js +142 -73
  99. package/dist/ownership-v21.js.map +1 -1
  100. package/dist/ownership-workflow.d.ts +6 -1
  101. package/dist/ownership-workflow.d.ts.map +1 -1
  102. package/dist/ownership-workflow.js +221 -58
  103. package/dist/ownership-workflow.js.map +1 -1
  104. package/dist/tools/cross-sdk-agent.d.ts +2 -1
  105. package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
  106. package/dist/tools/cross-sdk-agent.js +91 -8
  107. package/dist/tools/cross-sdk-agent.js.map +1 -1
  108. package/dist/transport.d.ts +1 -0
  109. package/dist/transport.d.ts.map +1 -1
  110. package/dist/transport.js +2 -0
  111. package/dist/transport.js.map +1 -1
  112. package/dist/version.d.ts +1 -1
  113. package/dist/version.js +1 -1
  114. package/package.json +1 -1
package/dist/bundle.js CHANGED
@@ -64,19 +64,18 @@ function p1363ToDer(p1363) {
64
64
  return der;
65
65
  }
66
66
  function derToP1363(der, coordLen = 32) {
67
- if (der[0] !== 48) throw new Error("invalid DER signature: missing SEQUENCE");
67
+ if (der.length < 8 || der[0] !== 48 || der[1] > 127 || der[1] !== der.length - 2) throw new Error("invalid DER signature: invalid SEQUENCE");
68
68
  let offset = 2;
69
- if (der[1] & 128) {
70
- const lenBytes = der[1] & 127;
71
- offset = 2 + lenBytes;
72
- }
73
69
  if (der[offset] !== 2) throw new Error("invalid DER signature: missing r");
74
70
  const rLen = der[offset + 1];
75
71
  let r = der.slice(offset + 2, offset + 2 + rLen);
72
+ if (!rLen || rLen > 33 || offset + 2 + rLen > der.length) throw new Error("invalid DER signature: invalid r");
76
73
  offset += 2 + rLen;
77
74
  if (der[offset] !== 2) throw new Error("invalid DER signature: missing s");
78
75
  const sLen = der[offset + 1];
79
76
  let s = der.slice(offset + 2, offset + 2 + sLen);
77
+ if (!sLen || sLen > 33 || offset + 2 + sLen !== der.length) throw new Error("invalid DER signature: invalid s");
78
+ if (r.every((byte) => byte === 0) || s.every((byte) => byte === 0) || r[0] === 0 && r.length > 1 && !(r[1] & 128) || s[0] === 0 && s.length > 1 && !(s[1] & 128) || r[0] & 128 || s[0] & 128) throw new Error("invalid DER signature: non-canonical integer");
80
79
  while (r.length > 1 && r[0] === 0) r = r.slice(1);
81
80
  while (s.length > 1 && s[0] === 0) s = s.slice(1);
82
81
  if (r.length > coordLen || s.length > coordLen) {
@@ -460,7 +459,7 @@ var init_indexeddb_store = __esm({
460
459
  });
461
460
 
462
461
  // src/version.ts
463
- var VERSION = "0.5.19";
462
+ var VERSION = "0.5.20";
464
463
 
465
464
  // src/types.ts
466
465
  var ConnectionState = /* @__PURE__ */ ((ConnectionState2) => {
@@ -828,7 +827,8 @@ var Subscription = class {
828
827
  }
829
828
  };
830
829
  var EventDispatcher = class {
831
- constructor() {
830
+ constructor(_readStateIdentity = () => "") {
831
+ this._readStateIdentity = _readStateIdentity;
832
832
  __publicField(this, "_organizationEventIds", /* @__PURE__ */ new Set());
833
833
  __publicField(this, "_eventIdentity", "");
834
834
  __publicField(this, "_log", _noopLog);
@@ -841,6 +841,9 @@ var EventDispatcher = class {
841
841
  setEventIdentity(aid2) {
842
842
  this._eventIdentity = aid2;
843
843
  }
844
+ _identityScoped(event) {
845
+ return event === "read_state_sync" || event === "message.read_changed" || event === "group.read_changed" || event === "_raw.message.read_changed" || event === "_raw.group.read_changed";
846
+ }
844
847
  /**
845
848
  * 订阅事件,返回订阅句柄。
846
849
  *
@@ -898,7 +901,9 @@ var EventDispatcher = class {
898
901
  reject = fail;
899
902
  });
900
903
  const queuedPayload = cloneEventPayload(payload);
901
- state.queue.push({ run: () => this.dispatchNow(state, event, queuedPayload, true), resolve, reject });
904
+ const identity = this._identityScoped(event) ? this._readStateIdentity() : null;
905
+ const guard = identity === null ? void 0 : () => this._readStateIdentity() === identity;
906
+ state.queue.push({ run: () => this.dispatchNow(state, event, queuedPayload, true, guard), resolve, reject, guard });
902
907
  if (state.drainPromise === null) {
903
908
  state.drainPromise = new Promise((done) => {
904
909
  state.drainResolve = done;
@@ -907,7 +912,7 @@ var EventDispatcher = class {
907
912
  this.scheduleDrain(state);
908
913
  await completed;
909
914
  }
910
- async _publish(state, event, payload) {
915
+ async _publish(state, event, payload, guard) {
911
916
  if (state.closed || state.closing) return;
912
917
  if (state === this._app && (event.startsWith("org.member.") || event.startsWith("org.lifecycle.") || event.startsWith("credential.")) && payload && typeof payload === "object" && !Array.isArray(payload)) {
913
918
  const value = payload;
@@ -922,9 +927,12 @@ var EventDispatcher = class {
922
927
  const itemDone = new Promise((resolve) => {
923
928
  resolveItem = resolve;
924
929
  });
930
+ const identity = this._identityScoped(event) ? this._readStateIdentity() : null;
931
+ const effectiveGuard = guard ?? (identity === null ? void 0 : () => this._readStateIdentity() === identity);
925
932
  state.queue.push({
926
- run: () => this.dispatchNow(state, event, payload),
927
- resolve: resolveItem
933
+ run: () => this.dispatchNow(state, event, payload, false, effectiveGuard),
934
+ resolve: resolveItem,
935
+ guard: effectiveGuard
928
936
  });
929
937
  if (state.handlerDepth > 0) return;
930
938
  if (state.drainPromise === null) {
@@ -937,15 +945,15 @@ var EventDispatcher = class {
937
945
  await itemDone;
938
946
  }
939
947
  /** 将事件放入异步 FIFO 队列,不等待处理器执行完成。 */
940
- enqueue(event, payload) {
941
- this._enqueue(this._app, event, payload);
948
+ enqueue(event, payload, guard) {
949
+ this._enqueue(this._app, event, payload, guard);
942
950
  }
943
951
  /** 将协议事件放入独立异步 FIFO 队列,不等待处理器执行完成。 */
944
- enqueueProtocol(event, payload) {
945
- this._enqueue(this._protocol, event, payload);
952
+ enqueueProtocol(event, payload, guard) {
953
+ this._enqueue(this._protocol, event, payload, guard);
946
954
  }
947
- _enqueue(state, event, payload) {
948
- const publish = state === this._protocol ? this.publishProtocol(event, payload) : this.publish(event, payload);
955
+ _enqueue(state, event, payload, guard) {
956
+ const publish = guard !== void 0 ? this._publish(state, event, payload, guard) : state === this._protocol ? this.publishProtocol(event, payload) : this.publish(event, payload);
949
957
  void publish.catch((exc) => {
950
958
  this._log.warn(`event ${event} enqueue failed:`, exc);
951
959
  });
@@ -1020,7 +1028,7 @@ var EventDispatcher = class {
1020
1028
  while (state.queue.length > 0) {
1021
1029
  const item = state.queue.shift();
1022
1030
  try {
1023
- await item.run();
1031
+ if (!item.guard || item.guard()) await item.run();
1024
1032
  } catch (exc) {
1025
1033
  this._log.warn("event dispatch failed:", exc);
1026
1034
  item.reject?.(exc);
@@ -1070,12 +1078,13 @@ var EventDispatcher = class {
1070
1078
  state.handlerDepth -= 1;
1071
1079
  }
1072
1080
  }
1073
- async dispatchNow(state, event, payload, strict = false) {
1081
+ async dispatchNow(state, event, payload, strict = false, guard) {
1074
1082
  const handlers = [...state.handlers.get(event) ?? []];
1075
1083
  const applicationPayload = state === this._protocol && this._app.handlers.has(event) ? cloneEventPayload(payload) : void 0;
1076
1084
  let failed = false;
1077
1085
  let failure2;
1078
1086
  for (const handler of handlers) {
1087
+ if (guard && !guard()) break;
1079
1088
  let result;
1080
1089
  state.handlerDepth += 1;
1081
1090
  try {
@@ -1101,7 +1110,7 @@ var EventDispatcher = class {
1101
1110
  }
1102
1111
  }
1103
1112
  if (applicationPayload !== void 0) {
1104
- this.enqueue(event, applicationPayload);
1113
+ this._enqueue(this._app, event, applicationPayload, guard);
1105
1114
  }
1106
1115
  if (strict && failed) throw failure2;
1107
1116
  }
@@ -1434,6 +1443,292 @@ var PRIVATE_KEY_NOT_VALID = "PRIVATE_KEY_NOT_VALID";
1434
1443
 
1435
1444
  // src/cert-utils.ts
1436
1445
  init_crypto();
1446
+ var AGENT_MD_SIGNATURE_RE = /^<!-- AUN-SIGNATURE(?:-[A-Z0-9-]+)?(?:\r\n|\n|\r)(?<body>[\s\S]*?)(?:\r\n|\n|\r)-->\s*$/;
1447
+ var FINGERPRINT_HEX_RE = /^[0-9a-f]+$/;
1448
+ function isStrictBase64(value) {
1449
+ const text3 = String(value ?? "");
1450
+ if (!text3 || text3.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(text3)) return false;
1451
+ try {
1452
+ return btoa(atob(text3)) === text3;
1453
+ } catch {
1454
+ return false;
1455
+ }
1456
+ }
1457
+ function normalizeFingerprintHex(value) {
1458
+ let text3 = String(value ?? "").trim().toLowerCase();
1459
+ if (text3.startsWith("sha256:")) text3 = text3.slice("sha256:".length);
1460
+ text3 = text3.replace(/:/g, "");
1461
+ if (![16, 64].includes(text3.length) || !FINGERPRINT_HEX_RE.test(text3)) return "";
1462
+ return text3;
1463
+ }
1464
+ async function publicKeyFingerprint(certPem) {
1465
+ const spkiB64 = publicKeyDerB64(certPem);
1466
+ if (!spkiB64) return "";
1467
+ const spkiBytes = base64ToUint8(spkiB64);
1468
+ const spkiInput = spkiBytes.buffer.slice(
1469
+ spkiBytes.byteOffset,
1470
+ spkiBytes.byteOffset + spkiBytes.byteLength
1471
+ );
1472
+ const digest2 = await crypto.subtle.digest("SHA-256", spkiInput);
1473
+ const hex = Array.from(new Uint8Array(digest2)).map((b) => b.toString(16).padStart(2, "0")).join("");
1474
+ return `sha256:${hex}`;
1475
+ }
1476
+ async function certMatchesFingerprint(certPem, fingerprint) {
1477
+ const expected = normalizeFingerprintHex(fingerprint);
1478
+ if (!expected) return false;
1479
+ const derHex = (await certificateSha256Fingerprint(certPem)).slice("sha256:".length);
1480
+ const spkiHex = (await publicKeyFingerprint(certPem)).slice("sha256:".length);
1481
+ if (expected.length === 16) {
1482
+ return derHex.slice(0, 16) === expected || spkiHex.slice(0, 16) === expected;
1483
+ }
1484
+ return derHex === expected || spkiHex === expected;
1485
+ }
1486
+ async function publicKeyMatchesFingerprint(certPem, fingerprint) {
1487
+ const expected = normalizeFingerprintHex(fingerprint);
1488
+ if (!expected) return false;
1489
+ const spki = await publicKeyFingerprint(certPem);
1490
+ const spkiHex = spki ? spki.slice("sha256:".length) : "";
1491
+ if (expected.length === 16) return spkiHex.slice(0, 16) === expected;
1492
+ return spkiHex === expected;
1493
+ }
1494
+ function parseAgentMdTailSignature(content) {
1495
+ return parseAgentMdTailSignatureForPurpose(content);
1496
+ }
1497
+ function parseAgentMdTailSignatureForRead(content) {
1498
+ return parseAgentMdTailSignatureForPurpose(content, true);
1499
+ }
1500
+ function parseAgentMdTailSignatureForPurpose(content, reading = false) {
1501
+ if (new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(new TextEncoder().encode(content)) !== content) return { payload: content, fields: null, signatures: [], parseError: "document must be valid UTF-8" };
1502
+ const positions = Array.from(content.matchAll(/<!-- AUN-SIGNATURE/g), (match) => match.index);
1503
+ const result = { payload: positions.length ? content.slice(0, positions[0]) : content, fields: null, signatures: [] };
1504
+ const fail = (reason) => ({ ...result, fields: null, parseError: reason });
1505
+ for (let index = 0; index < positions.length; index++) {
1506
+ const start = positions[index];
1507
+ if (start > 0 && !/[\r\n]/.test(content[start - 1])) return fail("agent.md signature block must start on a new line");
1508
+ const tail = content.slice(start, positions[index + 1] ?? content.length), match = tail.match(AGENT_MD_SIGNATURE_RE);
1509
+ if (!match) return fail("malformed signature block");
1510
+ const fields = {};
1511
+ for (const rawLine of match.groups?.body?.split(/\r\n|\n|\r/) ?? []) {
1512
+ const line = rawLine.trim();
1513
+ if (!line) continue;
1514
+ const colon = line.indexOf(":");
1515
+ if (colon < 0) return fail("malformed signature field");
1516
+ const key = line.slice(0, colon).trim().toLowerCase();
1517
+ if (Object.hasOwn(fields, key)) return fail(`duplicate signature field: ${key}`);
1518
+ fields[key] = line.slice(colon + 1).trim();
1519
+ }
1520
+ for (const field of ["cert_fingerprint", "timestamp", "signature"]) if (!fields[field]) return fail(`signature block missing ${field}`);
1521
+ if (Object.keys(fields).some((key) => !["cert_fingerprint", "public_key_fingerprint", "timestamp", "signature", "signer_aid"].includes(key))) return fail("signature block contains unknown fields");
1522
+ if (index > 0 && !fields.signer_aid) return fail("additional signature requires signer_aid");
1523
+ if (fields.signer_aid !== void 0 && !/^[A-Za-z0-9_][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_][A-Za-z0-9_-]*)+$/.test(fields.signer_aid)) return fail("invalid signer_aid");
1524
+ const compatible = reading && index === 0;
1525
+ if (compatible ? !normalizeFingerprintHex(fields.cert_fingerprint) : !/^sha256:[0-9a-f]{64}$/.test(fields.cert_fingerprint)) return fail("invalid cert_fingerprint");
1526
+ if (fields.public_key_fingerprint !== void 0 && (compatible ? !normalizeFingerprintHex(fields.public_key_fingerprint) : !/^sha256:[0-9a-f]{64}$/.test(fields.public_key_fingerprint))) return fail("invalid public_key_fingerprint");
1527
+ if (!(compatible ? /^[0-9]+$/ : /^[1-9][0-9]*$/).test(fields.timestamp) || !Number.isSafeInteger(Number(fields.timestamp)) || Number(fields.timestamp) <= 0) return fail("invalid timestamp");
1528
+ if (!isStrictBase64(fields.signature)) return fail("invalid signature base64");
1529
+ result.signatures.push({ ...fields, fields, tag: tail.slice("<!-- AUN-SIGNATURE".length, tail.search(/[\r\n]/)).replace(/^-/, ""), start, end: start + tail.lastIndexOf("-->") + 3, signedPayload: content.slice(0, start) });
1530
+ }
1531
+ if (new TextEncoder().encode(content.slice(positions[0] ?? content.length)).length > 2048) return fail("document signatures exceed 2048 bytes");
1532
+ result.fields = result.signatures[0]?.fields ?? null;
1533
+ return result;
1534
+ }
1535
+ async function signDocument(identity, content, options = {}) {
1536
+ if (new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(new TextEncoder().encode(content)) !== content) throw new Error("document must be valid UTF-8");
1537
+ const parsed = parseAgentMdTailSignature(content);
1538
+ if (parsed.parseError) throw new Error(parsed.parseError);
1539
+ const now = Math.trunc(options.now ?? Date.now() / 1e3), meta = parseCertMetadata(identity.certPem);
1540
+ if (meta.subject.trim().toLowerCase() !== identity.aid.trim().toLowerCase()) throw new Error("signing certificate subject does not match AID");
1541
+ if (!identity.isCertValid() || !identity.isPrivateKeyValid() || !identity.privateKeyPem || now < meta.notBefore.getTime() / 1e3 || now > meta.notAfter.getTime() / 1e3) throw new Error("signing identity is not currently valid");
1542
+ let prefix = options.append ? content : parsed.payload;
1543
+ if (prefix && !/[\r\n]$/.test(prefix)) {
1544
+ if (!options.append || !parsed.signatures.length) throw new Error("document payload must end with a newline before signature");
1545
+ prefix += "\n";
1546
+ }
1547
+ const signature2 = await signBytes(identity.privateKeyPem, new TextEncoder().encode(prefix));
1548
+ const tag = options.tag ? `AUN-SIGNATURE-${options.tag}` : "AUN-SIGNATURE";
1549
+ let block = buildAgentMdSignatureBlock(identity.certFingerprint, now, signature2, await publicKeyFingerprint(identity.certPem), tag);
1550
+ if (options.signerAid || options.append && parsed.signatures.length) block = block.replace("cert_fingerprint:", `signer_aid: ${identity.aid}
1551
+ cert_fingerprint:`);
1552
+ const signed = prefix + block, result = parseAgentMdTailSignature(signed);
1553
+ if (result.parseError) throw new Error(result.parseError);
1554
+ return { signed, signature: signature2, signatures: result.signatures };
1555
+ }
1556
+ async function verifyDocument(content, resolver, options = {}) {
1557
+ if (new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(new TextEncoder().encode(content)) !== content) return { valid: false, status: "invalid", payload: content, signatures: [], reason: "document must be valid UTF-8" };
1558
+ const parsed = options.reading ? parseAgentMdTailSignatureForRead(content) : parseAgentMdTailSignature(content);
1559
+ const output = { valid: false, status: "invalid", payload: parsed.payload, signatures: parsed.signatures, reason: parsed.parseError };
1560
+ if (parsed.parseError) return output;
1561
+ if (!parsed.signatures.length) return { ...output, status: "unsigned" };
1562
+ const now = options.now ?? Date.now() / 1e3;
1563
+ for (let index = 0; index < parsed.signatures.length; index++) {
1564
+ const block = parsed.signatures[index], fields = block.fields, signer = fields.signer_aid ?? options.expectedAid ?? extractAgentMdAid(parsed.payload);
1565
+ const material = await resolver(signer, fields.cert_fingerprint, Number(fields.timestamp)), certPem = typeof material === "string" ? material : material.certPem;
1566
+ const meta = parseCertMetadata(certPem), timestamp2 = Number(fields.timestamp);
1567
+ const subjectMatches = (index !== 0 || !options.expectedAid || signer.toLowerCase() === options.expectedAid.toLowerCase()) && (!signer || meta.subject.trim().toLowerCase() === signer.trim().toLowerCase());
1568
+ const certMatches = options.reading && index === 0 ? await certMatchesFingerprint(certPem, fields.cert_fingerprint) : await certificateSha256Fingerprint(certPem) === fields.cert_fingerprint;
1569
+ block.valid = subjectMatches && certMatches && timestamp2 >= meta.notBefore.getTime() / 1e3 && timestamp2 <= meta.notAfter.getTime() / 1e3 && timestamp2 <= now + 300 && (!options.requireCurrentValidity || now >= meta.notBefore.getTime() / 1e3 && now <= meta.notAfter.getTime() / 1e3) && (!fields.public_key_fingerprint || await publicKeyMatchesFingerprint(certPem, fields.public_key_fingerprint)) && await verifySignatureWithCert(certPem, fields.signature, new TextEncoder().encode(block.signedPayload));
1570
+ if (!block.valid) block.reason = timestamp2 < meta.notBefore.getTime() / 1e3 || timestamp2 > meta.notAfter.getTime() / 1e3 ? "signature timestamp is outside certificate validity" : "signature verification failed";
1571
+ }
1572
+ output.valid = parsed.signatures.every((block) => block.valid);
1573
+ output.status = output.valid ? "verified" : "invalid";
1574
+ output.reason = parsed.signatures.find((block) => !block.valid)?.reason;
1575
+ return output;
1576
+ }
1577
+ function buildAgentMdSignatureBlock(certFingerprint, timestamp2, signatureB64, publicKeyFp = "", tag = "AUN-SIGNATURE") {
1578
+ if (!/^AUN-SIGNATURE(?:-[A-Z0-9-]+)?$/.test(tag)) throw new Error("invalid signature TAG");
1579
+ const lines = [
1580
+ `<!-- ${tag}`,
1581
+ `cert_fingerprint: ${certFingerprint}`
1582
+ ];
1583
+ if (publicKeyFp) lines.push(`public_key_fingerprint: ${publicKeyFp}`);
1584
+ lines.push(`timestamp: ${Math.trunc(timestamp2)}`, `signature: ${signatureB64}`, "-->");
1585
+ return lines.join("\n");
1586
+ }
1587
+ function extractAgentMdAid(payload) {
1588
+ const lines = payload.replace(/^\ufeff/, "").split(/\r?\n/);
1589
+ if (!lines.length || lines[0].trim() !== "---") return "";
1590
+ for (const line of lines.slice(1)) {
1591
+ const t = line.trim();
1592
+ if (t === "---") break;
1593
+ if (t.startsWith("aid:")) {
1594
+ let v = t.slice(4).trim();
1595
+ if (v.length >= 2 && v[0] === v[v.length - 1] && (v[0] === '"' || v[0] === "'")) v = v.slice(1, -1);
1596
+ return v.trim();
1597
+ }
1598
+ }
1599
+ return "";
1600
+ }
1601
+ function readDerLength(data, offset) {
1602
+ if (offset >= data.length) return null;
1603
+ const first = data[offset];
1604
+ if (first < 128) return { value: first, lenBytes: 1 };
1605
+ const numBytes = first & 127;
1606
+ if (numBytes === 0 || numBytes > 4) return null;
1607
+ let value = 0;
1608
+ for (let i = 0; i < numBytes; i++) value = value << 8 | data[offset + 1 + i];
1609
+ return { value, lenBytes: 1 + numBytes };
1610
+ }
1611
+ function readDerTlv(data, offset) {
1612
+ const len = readDerLength(data, offset + 1);
1613
+ if (!len) return null;
1614
+ const valueStart = offset + 1 + len.lenBytes;
1615
+ const valueEnd = valueStart + len.value;
1616
+ if (valueEnd > data.length) return null;
1617
+ return { tag: data[offset], valueStart, valueEnd, fullEnd: valueEnd };
1618
+ }
1619
+ function publicKeyDerB64(certPem) {
1620
+ const certDer = new Uint8Array(pemToArrayBuffer(certPem));
1621
+ const cert = readDerTlv(certDer, 0);
1622
+ if (!cert || cert.tag !== 48) return "";
1623
+ const tbs = readDerTlv(certDer, cert.valueStart);
1624
+ if (!tbs || tbs.tag !== 48) return "";
1625
+ let pos = tbs.valueStart;
1626
+ if (certDer[pos] === 160) pos = readDerTlv(certDer, pos)?.fullEnd ?? pos;
1627
+ for (let i = 0; i < 5; i++) pos = readDerTlv(certDer, pos)?.fullEnd ?? pos;
1628
+ const spki = readDerTlv(certDer, pos);
1629
+ if (!spki || spki.tag !== 48) return "";
1630
+ return uint8ToBase64(certDer.slice(pos, spki.fullEnd));
1631
+ }
1632
+ function extractCommonName(der, nameStart, nameEnd) {
1633
+ let pos = nameStart;
1634
+ while (pos < nameEnd) {
1635
+ const set2 = readDerTlv(der, pos);
1636
+ if (!set2 || set2.tag !== 49) break;
1637
+ let rpos = set2.valueStart;
1638
+ while (rpos < set2.valueEnd) {
1639
+ const seq2 = readDerTlv(der, rpos);
1640
+ if (!seq2 || seq2.tag !== 48) break;
1641
+ const oid = readDerTlv(der, seq2.valueStart);
1642
+ if (oid && oid.tag === 6) {
1643
+ const isCN = oid.valueEnd - oid.valueStart === 3 && der[oid.valueStart] === 85 && der[oid.valueStart + 1] === 4 && der[oid.valueStart + 2] === 3;
1644
+ if (isCN) {
1645
+ const val = readDerTlv(der, oid.fullEnd);
1646
+ if (val) return new TextDecoder().decode(der.slice(val.valueStart, val.valueEnd));
1647
+ }
1648
+ }
1649
+ rpos = seq2.fullEnd;
1650
+ }
1651
+ pos = set2.fullEnd;
1652
+ }
1653
+ return "";
1654
+ }
1655
+ function parseDerTime(der, offset) {
1656
+ const tlv = readDerTlv(der, offset);
1657
+ if (!tlv) return null;
1658
+ const raw = new TextDecoder().decode(der.slice(tlv.valueStart, tlv.valueEnd));
1659
+ if (tlv.tag === 23) {
1660
+ const yy = parseInt(raw.slice(0, 2), 10);
1661
+ const year = yy >= 50 ? 1900 + yy : 2e3 + yy;
1662
+ const ms = Date.UTC(
1663
+ year,
1664
+ parseInt(raw.slice(2, 4), 10) - 1,
1665
+ parseInt(raw.slice(4, 6), 10),
1666
+ parseInt(raw.slice(6, 8), 10),
1667
+ parseInt(raw.slice(8, 10), 10),
1668
+ parseInt(raw.slice(10, 12), 10)
1669
+ );
1670
+ return Number.isNaN(ms) ? null : new Date(ms);
1671
+ }
1672
+ if (tlv.tag === 24) {
1673
+ const ms = Date.UTC(
1674
+ parseInt(raw.slice(0, 4), 10),
1675
+ parseInt(raw.slice(4, 6), 10) - 1,
1676
+ parseInt(raw.slice(6, 8), 10),
1677
+ parseInt(raw.slice(8, 10), 10),
1678
+ parseInt(raw.slice(10, 12), 10),
1679
+ parseInt(raw.slice(12, 14), 10)
1680
+ );
1681
+ return Number.isNaN(ms) ? null : new Date(ms);
1682
+ }
1683
+ return null;
1684
+ }
1685
+ function parseCertMetadata(certPem) {
1686
+ const fallback = { subject: "", issuer: "", notBefore: /* @__PURE__ */ new Date(0), notAfter: /* @__PURE__ */ new Date(0) };
1687
+ try {
1688
+ const der = new Uint8Array(pemToArrayBuffer(certPem));
1689
+ const cert = readDerTlv(der, 0);
1690
+ if (!cert || cert.tag !== 48) return fallback;
1691
+ const tbs = readDerTlv(der, cert.valueStart);
1692
+ if (!tbs || tbs.tag !== 48) return fallback;
1693
+ let pos = tbs.valueStart;
1694
+ if (der[pos] === 160) pos = readDerTlv(der, pos)?.fullEnd ?? pos;
1695
+ pos = readDerTlv(der, pos)?.fullEnd ?? pos;
1696
+ pos = readDerTlv(der, pos)?.fullEnd ?? pos;
1697
+ const issuer = readDerTlv(der, pos);
1698
+ if (!issuer || issuer.tag !== 48) return fallback;
1699
+ const issuerCN = extractCommonName(der, issuer.valueStart, issuer.valueEnd);
1700
+ pos = issuer.fullEnd;
1701
+ const validity = readDerTlv(der, pos);
1702
+ if (!validity || validity.tag !== 48) return fallback;
1703
+ const notBefore = parseDerTime(der, validity.valueStart);
1704
+ const nb = readDerTlv(der, validity.valueStart);
1705
+ const notAfter = nb ? parseDerTime(der, nb.fullEnd) : null;
1706
+ pos = validity.fullEnd;
1707
+ const subject = readDerTlv(der, pos);
1708
+ if (!subject || subject.tag !== 48) return fallback;
1709
+ const subjectCN = extractCommonName(der, subject.valueStart, subject.valueEnd);
1710
+ return {
1711
+ subject: subjectCN,
1712
+ issuer: issuerCN,
1713
+ notBefore: notBefore ?? /* @__PURE__ */ new Date(0),
1714
+ notAfter: notAfter ?? /* @__PURE__ */ new Date(0)
1715
+ };
1716
+ } catch {
1717
+ return fallback;
1718
+ }
1719
+ }
1720
+ async function signBytes(privateKeyPem, payload) {
1721
+ const key = await importPrivateKeyEcdsa(privateKeyPem);
1722
+ return uint8ToBase64(await ecdsaSignDer(key, payload));
1723
+ }
1724
+ async function verifySignatureWithCert(certPem, signatureB64, data) {
1725
+ try {
1726
+ const key = await importCertPublicKeyEcdsa(certPem);
1727
+ return await ecdsaVerifyDer(key, base64ToUint8(signatureB64), data);
1728
+ } catch {
1729
+ return false;
1730
+ }
1731
+ }
1437
1732
 
1438
1733
  // node_modules/yaml/browser/dist/nodes/identity.js
1439
1734
  var ALIAS = Symbol.for("yaml.alias");
@@ -7683,265 +7978,6 @@ function parseDocument(source, options = {}) {
7683
7978
  return doc;
7684
7979
  }
7685
7980
 
7686
- // src/cert-utils.ts
7687
- var AGENT_MD_SIGNATURE_MARKER = "<!-- AUN-SIGNATURE";
7688
- var AGENT_MD_SIGNATURE_RE = /^<!-- AUN-SIGNATURE(?:-[A-Z0-9][A-Z0-9-]*)?(?:\r\n|\n|\r)(?<body>[\s\S]*?)(?:\r\n|\n|\r)-->\s*$/;
7689
- var FINGERPRINT_HEX_RE = /^[0-9a-f]+$/;
7690
- function isStrictBase64(value) {
7691
- const text3 = String(value ?? "");
7692
- if (!text3 || text3.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(text3)) return false;
7693
- try {
7694
- return btoa(atob(text3)) === text3;
7695
- } catch {
7696
- return false;
7697
- }
7698
- }
7699
- function normalizeFingerprintHex(value) {
7700
- let text3 = String(value ?? "").trim().toLowerCase();
7701
- if (text3.startsWith("sha256:")) text3 = text3.slice("sha256:".length);
7702
- text3 = text3.replace(/:/g, "");
7703
- if (![16, 64].includes(text3.length) || !FINGERPRINT_HEX_RE.test(text3)) return "";
7704
- return text3;
7705
- }
7706
- async function publicKeyFingerprint(certPem) {
7707
- const spkiB64 = publicKeyDerB64(certPem);
7708
- if (!spkiB64) return "";
7709
- const spkiBytes = base64ToUint8(spkiB64);
7710
- const spkiInput = spkiBytes.buffer.slice(
7711
- spkiBytes.byteOffset,
7712
- spkiBytes.byteOffset + spkiBytes.byteLength
7713
- );
7714
- const digest2 = await crypto.subtle.digest("SHA-256", spkiInput);
7715
- const hex = Array.from(new Uint8Array(digest2)).map((b) => b.toString(16).padStart(2, "0")).join("");
7716
- return `sha256:${hex}`;
7717
- }
7718
- async function certMatchesFingerprint(certPem, fingerprint) {
7719
- const expected = normalizeFingerprintHex(fingerprint);
7720
- if (!expected) return false;
7721
- const derHex = (await certificateSha256Fingerprint(certPem)).slice("sha256:".length);
7722
- const spkiHex = (await publicKeyFingerprint(certPem)).slice("sha256:".length);
7723
- if (expected.length === 16) {
7724
- return derHex.slice(0, 16) === expected || spkiHex.slice(0, 16) === expected;
7725
- }
7726
- return derHex === expected || spkiHex === expected;
7727
- }
7728
- async function publicKeyMatchesFingerprint(certPem, fingerprint) {
7729
- const expected = normalizeFingerprintHex(fingerprint);
7730
- if (!expected) return false;
7731
- const spki = await publicKeyFingerprint(certPem);
7732
- const spkiHex = spki ? spki.slice("sha256:".length) : "";
7733
- if (expected.length === 16) return spkiHex.slice(0, 16) === expected;
7734
- return spkiHex === expected;
7735
- }
7736
- function parseAgentMdTailSignature(content) {
7737
- const idx = content.lastIndexOf(AGENT_MD_SIGNATURE_MARKER);
7738
- if (idx < 0) return { payload: content, fields: null };
7739
- if (content.indexOf(AGENT_MD_SIGNATURE_MARKER) !== idx) {
7740
- return { payload: content.slice(0, idx), fields: null, parseError: "agent.md must contain exactly one signature block" };
7741
- }
7742
- if (idx > 0 && content[idx - 1] !== "\n" && content[idx - 1] !== "\r") {
7743
- return { payload: content.slice(0, idx), fields: null, parseError: "agent.md signature block must start on a new line" };
7744
- }
7745
- const tail = content.slice(idx);
7746
- const match = tail.match(AGENT_MD_SIGNATURE_RE);
7747
- if (!match) return { payload: content.slice(0, idx), fields: null, parseError: "malformed signature block" };
7748
- const fields = {};
7749
- for (const rawLine of match.groups?.body?.split(/\r\n|\n|\r/) ?? []) {
7750
- const line = rawLine.trim();
7751
- if (!line) continue;
7752
- const colon = line.indexOf(":");
7753
- if (colon < 0) return { payload: content.slice(0, idx), fields: null, parseError: `malformed signature field: ${line}` };
7754
- const key = line.slice(0, colon).trim().toLowerCase();
7755
- if (Object.prototype.hasOwnProperty.call(fields, key)) {
7756
- return { payload: content.slice(0, idx), fields: null, parseError: `duplicate signature field: ${key}` };
7757
- }
7758
- fields[key] = line.slice(colon + 1).trim();
7759
- }
7760
- for (const req of ["cert_fingerprint", "timestamp", "signature"]) {
7761
- if (!fields[req]) return { payload: content.slice(0, idx), fields: null, parseError: `signature block missing ${req}` };
7762
- }
7763
- let entity = false;
7764
- const frontmatter = content.slice(0, idx).replace(/^\ufeff/, "").match(/^---(?:\r\n|\n|\r)([\s\S]*?)(?:\r\n|\n|\r)---(?:\r\n|\n|\r)/);
7765
- if (frontmatter) {
7766
- try {
7767
- const doc = parseDocument(frontmatter[1]);
7768
- entity = !doc.errors.length && ["group", "org"].includes(String(doc.get("type")));
7769
- } catch {
7770
- }
7771
- }
7772
- if (Object.keys(fields).some((key) => !["cert_fingerprint", "public_key_fingerprint", "timestamp", "signature", ...entity ? ["signer_aid"] : []].includes(key))) {
7773
- return { payload: content.slice(0, idx), fields: null, parseError: "signature block contains unknown fields" };
7774
- }
7775
- if (!/^sha256:[0-9a-f]{64}$/.test(fields.cert_fingerprint)) {
7776
- return { payload: content.slice(0, idx), fields: null, parseError: "invalid cert_fingerprint" };
7777
- }
7778
- if (fields.public_key_fingerprint !== void 0 && !/^sha256:[0-9a-f]{64}$/.test(fields.public_key_fingerprint)) {
7779
- return { payload: content.slice(0, idx), fields: null, parseError: "invalid public_key_fingerprint" };
7780
- }
7781
- if (!/^[1-9][0-9]*$/.test(fields.timestamp) || !Number.isSafeInteger(Number(fields.timestamp))) {
7782
- return { payload: content.slice(0, idx), fields: null, parseError: "invalid timestamp" };
7783
- }
7784
- if (!isStrictBase64(fields.signature)) {
7785
- return { payload: content.slice(0, idx), fields: null, parseError: "invalid signature base64" };
7786
- }
7787
- return { payload: content.slice(0, idx), fields };
7788
- }
7789
- function buildAgentMdSignatureBlock(certFingerprint, timestamp2, signatureB64, publicKeyFp = "", tag = "AUN-SIGNATURE") {
7790
- if (!/^AUN-SIGNATURE(?:-[A-Z0-9][A-Z0-9-]*)?$/.test(tag)) throw new Error("invalid signature TAG");
7791
- const lines = [
7792
- `<!-- ${tag}`,
7793
- `cert_fingerprint: ${certFingerprint}`
7794
- ];
7795
- if (publicKeyFp) lines.push(`public_key_fingerprint: ${publicKeyFp}`);
7796
- lines.push(`timestamp: ${Math.trunc(timestamp2)}`, `signature: ${signatureB64}`, "-->");
7797
- return lines.join("\n");
7798
- }
7799
- function extractAgentMdAid(payload) {
7800
- const lines = payload.replace(/^\ufeff/, "").split(/\r?\n/);
7801
- if (!lines.length || lines[0].trim() !== "---") return "";
7802
- for (const line of lines.slice(1)) {
7803
- const t = line.trim();
7804
- if (t === "---") break;
7805
- if (t.startsWith("aid:")) {
7806
- let v = t.slice(4).trim();
7807
- if (v.length >= 2 && v[0] === v[v.length - 1] && (v[0] === '"' || v[0] === "'")) v = v.slice(1, -1);
7808
- return v.trim();
7809
- }
7810
- }
7811
- return "";
7812
- }
7813
- function readDerLength(data, offset) {
7814
- if (offset >= data.length) return null;
7815
- const first = data[offset];
7816
- if (first < 128) return { value: first, lenBytes: 1 };
7817
- const numBytes = first & 127;
7818
- if (numBytes === 0 || numBytes > 4) return null;
7819
- let value = 0;
7820
- for (let i = 0; i < numBytes; i++) value = value << 8 | data[offset + 1 + i];
7821
- return { value, lenBytes: 1 + numBytes };
7822
- }
7823
- function readDerTlv(data, offset) {
7824
- const len = readDerLength(data, offset + 1);
7825
- if (!len) return null;
7826
- const valueStart = offset + 1 + len.lenBytes;
7827
- const valueEnd = valueStart + len.value;
7828
- if (valueEnd > data.length) return null;
7829
- return { tag: data[offset], valueStart, valueEnd, fullEnd: valueEnd };
7830
- }
7831
- function publicKeyDerB64(certPem) {
7832
- const certDer = new Uint8Array(pemToArrayBuffer(certPem));
7833
- const cert = readDerTlv(certDer, 0);
7834
- if (!cert || cert.tag !== 48) return "";
7835
- const tbs = readDerTlv(certDer, cert.valueStart);
7836
- if (!tbs || tbs.tag !== 48) return "";
7837
- let pos = tbs.valueStart;
7838
- if (certDer[pos] === 160) pos = readDerTlv(certDer, pos)?.fullEnd ?? pos;
7839
- for (let i = 0; i < 5; i++) pos = readDerTlv(certDer, pos)?.fullEnd ?? pos;
7840
- const spki = readDerTlv(certDer, pos);
7841
- if (!spki || spki.tag !== 48) return "";
7842
- return uint8ToBase64(certDer.slice(pos, spki.fullEnd));
7843
- }
7844
- function extractCommonName(der, nameStart, nameEnd) {
7845
- let pos = nameStart;
7846
- while (pos < nameEnd) {
7847
- const set2 = readDerTlv(der, pos);
7848
- if (!set2 || set2.tag !== 49) break;
7849
- let rpos = set2.valueStart;
7850
- while (rpos < set2.valueEnd) {
7851
- const seq2 = readDerTlv(der, rpos);
7852
- if (!seq2 || seq2.tag !== 48) break;
7853
- const oid = readDerTlv(der, seq2.valueStart);
7854
- if (oid && oid.tag === 6) {
7855
- const isCN = oid.valueEnd - oid.valueStart === 3 && der[oid.valueStart] === 85 && der[oid.valueStart + 1] === 4 && der[oid.valueStart + 2] === 3;
7856
- if (isCN) {
7857
- const val = readDerTlv(der, oid.fullEnd);
7858
- if (val) return new TextDecoder().decode(der.slice(val.valueStart, val.valueEnd));
7859
- }
7860
- }
7861
- rpos = seq2.fullEnd;
7862
- }
7863
- pos = set2.fullEnd;
7864
- }
7865
- return "";
7866
- }
7867
- function parseDerTime(der, offset) {
7868
- const tlv = readDerTlv(der, offset);
7869
- if (!tlv) return null;
7870
- const raw = new TextDecoder().decode(der.slice(tlv.valueStart, tlv.valueEnd));
7871
- if (tlv.tag === 23) {
7872
- const yy = parseInt(raw.slice(0, 2), 10);
7873
- const year = yy >= 50 ? 1900 + yy : 2e3 + yy;
7874
- const ms = Date.UTC(
7875
- year,
7876
- parseInt(raw.slice(2, 4), 10) - 1,
7877
- parseInt(raw.slice(4, 6), 10),
7878
- parseInt(raw.slice(6, 8), 10),
7879
- parseInt(raw.slice(8, 10), 10),
7880
- parseInt(raw.slice(10, 12), 10)
7881
- );
7882
- return Number.isNaN(ms) ? null : new Date(ms);
7883
- }
7884
- if (tlv.tag === 24) {
7885
- const ms = Date.UTC(
7886
- parseInt(raw.slice(0, 4), 10),
7887
- parseInt(raw.slice(4, 6), 10) - 1,
7888
- parseInt(raw.slice(6, 8), 10),
7889
- parseInt(raw.slice(8, 10), 10),
7890
- parseInt(raw.slice(10, 12), 10),
7891
- parseInt(raw.slice(12, 14), 10)
7892
- );
7893
- return Number.isNaN(ms) ? null : new Date(ms);
7894
- }
7895
- return null;
7896
- }
7897
- function parseCertMetadata(certPem) {
7898
- const fallback = { subject: "", issuer: "", notBefore: /* @__PURE__ */ new Date(0), notAfter: /* @__PURE__ */ new Date(0) };
7899
- try {
7900
- const der = new Uint8Array(pemToArrayBuffer(certPem));
7901
- const cert = readDerTlv(der, 0);
7902
- if (!cert || cert.tag !== 48) return fallback;
7903
- const tbs = readDerTlv(der, cert.valueStart);
7904
- if (!tbs || tbs.tag !== 48) return fallback;
7905
- let pos = tbs.valueStart;
7906
- if (der[pos] === 160) pos = readDerTlv(der, pos)?.fullEnd ?? pos;
7907
- pos = readDerTlv(der, pos)?.fullEnd ?? pos;
7908
- pos = readDerTlv(der, pos)?.fullEnd ?? pos;
7909
- const issuer = readDerTlv(der, pos);
7910
- if (!issuer || issuer.tag !== 48) return fallback;
7911
- const issuerCN = extractCommonName(der, issuer.valueStart, issuer.valueEnd);
7912
- pos = issuer.fullEnd;
7913
- const validity = readDerTlv(der, pos);
7914
- if (!validity || validity.tag !== 48) return fallback;
7915
- const notBefore = parseDerTime(der, validity.valueStart);
7916
- const nb = readDerTlv(der, validity.valueStart);
7917
- const notAfter = nb ? parseDerTime(der, nb.fullEnd) : null;
7918
- pos = validity.fullEnd;
7919
- const subject = readDerTlv(der, pos);
7920
- if (!subject || subject.tag !== 48) return fallback;
7921
- const subjectCN = extractCommonName(der, subject.valueStart, subject.valueEnd);
7922
- return {
7923
- subject: subjectCN,
7924
- issuer: issuerCN,
7925
- notBefore: notBefore ?? /* @__PURE__ */ new Date(0),
7926
- notAfter: notAfter ?? /* @__PURE__ */ new Date(0)
7927
- };
7928
- } catch {
7929
- return fallback;
7930
- }
7931
- }
7932
- async function signBytes(privateKeyPem, payload) {
7933
- const key = await importPrivateKeyEcdsa(privateKeyPem);
7934
- return uint8ToBase64(await ecdsaSignDer(key, payload));
7935
- }
7936
- async function verifySignatureWithCert(certPem, signatureB64, data) {
7937
- try {
7938
- const key = await importCertPublicKeyEcdsa(certPem);
7939
- return await ecdsaVerifyDer(key, base64ToUint8(signatureB64), data);
7940
- } catch {
7941
- return false;
7942
- }
7943
- }
7944
-
7945
7981
  // src/v2/crypto/canonical.ts
7946
7982
  var encoder = new TextEncoder();
7947
7983
  var MAX_SAFE_JSON_INTEGER = 9007199254740991;
@@ -9108,7 +9144,7 @@ var AgentMdManager = class _AgentMdManager {
9108
9144
  const target = this._safeAid(aid2);
9109
9145
  const gatewayUrl = await this._resolveGateway(target);
9110
9146
  const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin("node:http") !== null;
9111
- const headers = { Accept: "text/markdown", "X-AUN-Ownership-Check": "1", ...viaGatewayHost ? { Host: target } : {} };
9147
+ const headers = { "X-AUN-Signature-Format": "multi-v1", Accept: "text/markdown", "Cache-Control": "no-cache", "X-AUN-Ownership-Check": "1", ...viaGatewayHost ? { Host: target } : {} };
9112
9148
  let url = this._url(target, gatewayUrl);
9113
9149
  if (viaGatewayHost) {
9114
9150
  const parsed = new URL(gatewayUrl);
@@ -9158,6 +9194,7 @@ var AgentMdManager = class _AgentMdManager {
9158
9194
  redirect: "error",
9159
9195
  body: content,
9160
9196
  headers: {
9197
+ "X-AUN-Signature-Format": "multi-v1",
9161
9198
  "Content-Type": "text/markdown; charset=utf-8",
9162
9199
  Authorization: `Bearer ${token}`,
9163
9200
  ...condition.if_match ? { "If-Match": condition.if_match } : { "If-None-Match": "*" }
@@ -9188,45 +9225,50 @@ var AgentMdManager = class _AgentMdManager {
9188
9225
  if (!current.isPrivateKeyValid()) {
9189
9226
  throw new StateError("uploadAgentMd requires loaded AID with a valid private key");
9190
9227
  }
9191
- if (content !== void 0 && content !== null) {
9192
- const text3 = String(content ?? "");
9193
- if (text3.length === 0) throw new ValidationError("uploadAgentMd requires non-empty content");
9194
- validateAgentMdDocument(text3, { expectedAid: target });
9195
- await this.saveRecord(target, {
9196
- content: text3,
9197
- local_etag: await _AgentMdManager.contentEtag(text3),
9198
- fetched_at: Date.now()
9199
- });
9200
- }
9201
- const localContent = await this.readContent(target);
9228
+ const localContent = content ?? (String((await this.loadRecord(target, true))?.local_draft_content ?? "") || await this.readContent(target));
9202
9229
  if (localContent === null || localContent.length === 0) {
9203
9230
  throw new ValidationError("uploadAgentMd requires local agent.md content");
9204
9231
  }
9205
- const parsed = validateAgentMdDocument(localContent, { expectedAid: target });
9232
+ const parsed = this._parseBodyUpdate(localContent, target);
9206
9233
  if (["group", "org"].includes(String(parsed.frontmatter.type))) return await this.uploadAs(target, localContent, current);
9207
- const signedResult = await current.signAgentMd(localContent);
9234
+ if (parsed.signatures.length > 1) {
9235
+ await this._verifyBodyUpload(target, localContent, current);
9236
+ return await this._uploadPrepared(target, localContent, current);
9237
+ }
9238
+ const signedResult = await current.signAgentMd(this._isLegacyBodyRelation(parsed.frontmatter) ? parsed.payload : localContent);
9208
9239
  if (!signedResult.ok) {
9209
9240
  throw new ClientSignatureError(signedResult.error.message || "agent.md signing failed");
9210
9241
  }
9211
9242
  const signed = signedResult.data.signed;
9212
- await verifyAgentMdUpload(signed, current.certPem, target);
9243
+ await this._verifyBodyUpload(target, signed, current);
9213
9244
  return await this._uploadPrepared(target, signed, current);
9214
9245
  }
9215
9246
  async uploadAs(targetAid, content, signer) {
9216
9247
  const target = this._safeAid(String(targetAid ?? "").trim());
9217
9248
  const rawContent = String(content ?? "");
9218
9249
  if (!rawContent.trim()) throw new ValidationError("uploadAgentMd requires non-empty content");
9219
- const parsed = validateAgentMdDocument(rawContent, { expectedAid: target });
9250
+ const parsed = this._parseBodyUpdate(rawContent, target);
9251
+ if (!["group", "org"].includes(String(parsed.frontmatter.type)) && parsed.signatures.length > 1) {
9252
+ await this._verifyBodyUpload(target, rawContent, signer);
9253
+ return await this._uploadPrepared(target, rawContent, signer);
9254
+ }
9220
9255
  if (["group", "org"].includes(String(parsed.frontmatter.type)) && parsed.signature) {
9221
- const identity = await this._entityDocumentSigner(target, rawContent);
9222
- await verifyAgentMdUpload(rawContent, identity.certPem, target);
9223
- return await this._uploadPrepared(target, rawContent, identity);
9256
+ await verifyAgentMdUpload(rawContent, signer.certPem, target, void 0, async (aid2, fingerprint) => {
9257
+ if (!this._entitySignerResolver) throw new StateError("entity additional signing certificate resolver is unavailable", { stringCode: "unavailable", retryable: true });
9258
+ return await this._entitySignerResolver(aid2, fingerprint);
9259
+ });
9260
+ return await this._uploadPrepared(target, rawContent, signer);
9261
+ }
9262
+ if (["group", "org"].includes(String(parsed.frontmatter.type)) && signer.aid !== target) {
9263
+ if (!this._entityIdentityGetter) throw new StateError("entity signing identity is unavailable");
9264
+ signer = await this._entityIdentityGetter(target, String(parsed.frontmatter.type));
9265
+ if (signer.aid !== target) throw new StateError("entity signing identity does not match target");
9224
9266
  }
9225
9267
  if (!signer?.isPrivateKeyValid()) throw new StateError("uploadAgentMd requires a valid signer identity");
9226
- const signedResult = await signer.signAgentMd(rawContent);
9268
+ const signedResult = await signer.signAgentMd(this._isLegacyBodyRelation(parsed.frontmatter) ? parsed.payload : rawContent);
9227
9269
  if (!signedResult.ok) throw new ClientSignatureError(signedResult.error.message || "agent.md signing failed");
9228
9270
  const signed = signedResult.data.signed;
9229
- await verifyAgentMdUpload(signed, signer.certPem, target);
9271
+ await this._verifyBodyUpload(target, signed, signer);
9230
9272
  return await this._uploadPrepared(target, signed, signer);
9231
9273
  }
9232
9274
  async pendingUpload(aid2) {
@@ -9239,9 +9281,28 @@ var AgentMdManager = class _AgentMdManager {
9239
9281
  if (!pending) return false;
9240
9282
  const condition = pending.condition;
9241
9283
  if (!condition || Object.keys(condition).length !== 1 || !(typeof condition.if_match === "string" && condition.if_match || condition.if_none_match === "*")) throw new StateError("pending agent.md requires its original document condition", { stringCode: "persistence_unavailable" });
9242
- const meta = validateAgentMdDocument(pending.content, { expectedAid: aid2, validateOwnership: false }).frontmatter;
9243
- if ("ownership_proof" in meta || "ownership_release" in meta) throw new StateError("legacy ownership pending agent.md cannot be recovered through ordinary upload", { stringCode: "unsupported_protocol" });
9284
+ const pendingDocument = validateAgentMdDocument(pending.content, { expectedAid: aid2, validateOwnership: false });
9285
+ const meta = pendingDocument.frontmatter;
9286
+ if (this._isLegacyBodyRelation(meta) && pending.relation?.protocol !== "legacy") throw new StateError("legacy ownership pending agent.md requires its original relationship snapshot", { stringCode: "unsupported_protocol" });
9244
9287
  try {
9288
+ if (pending.relation?.protocol === "legacy") {
9289
+ const relation2 = pending.relation;
9290
+ if (!condition.if_match || condition.if_match !== relation2.original_etag || typeof relation2.original_content !== "string" || relation2.original_content_etag !== await _AgentMdManager.contentEtag(relation2.original_content) || pending.content_etag !== await _AgentMdManager.contentEtag(pending.content)) throw new StateError("pending legacy original bytes or document condition are corrupt", { stringCode: "persistence_unavailable" });
9291
+ const original = await this._verifyLegacyBodyOriginal(aid2, relation2.original_content);
9292
+ if (!this._isLegacyBodyRelation(original) || !this._sameBodyRelation(this._bodyRelationFields(original), relation2.fields)) throw new StateError("pending legacy original relationship snapshot is corrupt", { stringCode: "persistence_unavailable" });
9293
+ if (!this._sameBodyRelation(this._bodyRelationFields(meta), relation2.fields)) throw new StateError("legacy relationship changes require their original protocol", { stringCode: "unsupported_protocol" });
9294
+ const identity2 = signer ?? this._currentAid();
9295
+ if (!identity2 || identity2.aid.toLowerCase() !== aid2.toLowerCase() || this._currentAid()?.aid.toLowerCase() !== aid2.toLowerCase()) throw new StateError("legacy body recovery requires its current subject identity", { stringCode: "forbidden" });
9296
+ await this._verifyBodyUpload(aid2, pending.content, identity2);
9297
+ const remote2 = await this._readLegacyBodyFresh(aid2);
9298
+ if (remote2.content === pending.content && remote2.etag === pending.content_etag) {
9299
+ await this.saveRecord(aid2, { content: pending.content, remote_etag: remote2.etag, local_draft_content: "", pending_operation: "", pending_content: "", pending_content_etag: "", pending_error: "", pending_condition: {}, pending_relation: {}, pending_authorization: "" });
9300
+ return true;
9301
+ }
9302
+ if (remote2.etag !== condition.if_match || remote2.content !== relation2.original_content) throw new StateError("pending legacy original document changed", { stringCode: "conflict" });
9303
+ await this._uploadPrepared(aid2, pending.content, identity2, condition);
9304
+ return true;
9305
+ }
9245
9306
  if (["group", "org"].includes(String(meta.type))) {
9246
9307
  const identity2 = signer ?? this._currentAid();
9247
9308
  if (!identity2) throw new StateError("entity upload recovery requires current owner", { stringCode: "forbidden" });
@@ -9251,17 +9312,21 @@ var AgentMdManager = class _AgentMdManager {
9251
9312
  if (pending.relation.entity_cert_fingerprint !== context.entity_cert_fingerprint) throw new StateError("pending entity upload belongs to an old rekey generation", { stringCode: "conflict" });
9252
9313
  if (pending.content_etag !== await _AgentMdManager.contentEtag(pending.content)) throw new StateError("pending entity original bytes are corrupt", { stringCode: "persistence_unavailable" });
9253
9314
  const documentSigner = await this._entityDocumentSigner(aid2, pending.content);
9254
- await verifyAgentMdUpload(pending.content, documentSigner.certPem, aid2);
9255
- if (identity2.aid.toLowerCase() !== aid2.toLowerCase()) await this._validateEntityUploadAuthorization(pending.authorization, aid2, pending.content, identity2, String(context.entity_cert_fingerprint));
9315
+ await this._verifyEntityDocument(aid2, pending.content);
9256
9316
  const remote2 = await this._readEntityFresh(aid2);
9257
9317
  if (remote2.exists && remote2.content === pending.content && remote2.etag === pending.content_etag) {
9258
- await this.saveRecord(aid2, { content: pending.content, remote_etag: remote2.etag, pending_operation: "", pending_content: "", pending_content_etag: "", pending_error: "", pending_condition: {}, pending_relation: {}, pending_authorization: "" });
9318
+ await this.saveRecord(aid2, { content: pending.content, remote_etag: remote2.etag, local_draft_content: "", pending_operation: "", pending_content: "", pending_content_etag: "", pending_error: "", pending_condition: {}, pending_relation: {}, pending_authorization: "" });
9259
9319
  return true;
9260
9320
  }
9261
9321
  if (condition.if_match && (!remote2.exists || remote2.etag !== condition.if_match) || condition.if_none_match === "*" && remote2.exists) throw new StateError("pending agent.md original document condition changed", { stringCode: "conflict" });
9262
9322
  await this._uploadPrepared(aid2, pending.content, identity2, condition);
9263
9323
  return true;
9264
9324
  }
9325
+ const identity = signer ?? this._currentAid();
9326
+ if (pendingDocument.signatures.length > 1) {
9327
+ if (!identity) throw new StateError("pending agent.md requires a signer identity");
9328
+ await this._verifyBodyUpload(aid2, pending.content, identity);
9329
+ }
9265
9330
  const remote = await this.readOwnershipFresh(aid2);
9266
9331
  const fail = (code, reason, retryable = false) => new StateError(reason, { stringCode: code, retryable });
9267
9332
  const candidate = parseOwnershipFile(pending.content, "agent", { expectedSubAid: aid2 });
@@ -9278,14 +9343,13 @@ var AgentMdManager = class _AgentMdManager {
9278
9343
  }
9279
9344
  if (!relation && (remote.relation_version !== 0 || remote.lock !== "unlocked")) throw fail("unavailable", "pending agent.md cannot overwrite relationship history", true);
9280
9345
  if (remote.exists && remote.etag === pending.content_etag && remote.content === pending.content) {
9281
- const saved = await this.saveRecord(aid2, { content: pending.content, remote_etag: remote.etag, pending_operation: "", pending_content: "", pending_content_etag: "", pending_error: "", pending_condition: {}, pending_relation: {}, pending_authorization: "" });
9346
+ const saved = await this.saveRecord(aid2, { content: pending.content, remote_etag: remote.etag, local_draft_content: "", pending_operation: "", pending_content: "", pending_content_etag: "", pending_error: "", pending_condition: {}, pending_relation: {}, pending_authorization: "" });
9282
9347
  if (!saved.aid) throw new StateError("agent.md pending completion persistence failed");
9283
9348
  return true;
9284
9349
  }
9285
9350
  if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === "*" && remote.exists) throw new StateError("pending agent.md original document condition changed", { stringCode: "conflict" });
9286
- const identity = signer ?? this._currentAid();
9287
9351
  if (!identity) throw new StateError("pending agent.md requires a signer identity");
9288
- await verifyAgentMdUpload(pending.content, identity.certPem, aid2);
9352
+ if (pendingDocument.signatures.length <= 1) await this._verifyBodyUpload(aid2, pending.content, identity);
9289
9353
  await this._uploadPrepared(aid2, pending.content, identity, condition);
9290
9354
  return true;
9291
9355
  } catch (error) {
@@ -9293,44 +9357,134 @@ var AgentMdManager = class _AgentMdManager {
9293
9357
  throw error;
9294
9358
  }
9295
9359
  }
9360
+ _parseBodyUpdate(content, target) {
9361
+ const parsed = validateAgentMdDocumentForRead(content, { expectedAid: target, validateOwnership: false });
9362
+ return validateAgentMdDocument(this._isLegacyBodyRelation(parsed.frontmatter) && parsed.signatures.length <= 1 ? parsed.payload : content, { expectedAid: target, validateOwnership: false });
9363
+ }
9364
+ _isLegacyBodyRelation(meta) {
9365
+ if (["group", "org"].includes(String(meta.type))) return false;
9366
+ if (Object.hasOwn(meta, "ownership")) {
9367
+ const ownership = meta.ownership;
9368
+ const statement = isJsonObject(ownership) ? ownership.statement : void 0;
9369
+ return isJsonObject(statement) && statement.version === 2 && !Object.hasOwn(statement, "relation_version");
9370
+ }
9371
+ return ["owner", "ownership_proof", "ownership_release"].some((field) => Object.hasOwn(meta, field));
9372
+ }
9373
+ _bodyRelationFields(meta) {
9374
+ return Object.fromEntries(["owner", "ownership", "ownership_proof", "ownership_release"].filter((field) => Object.hasOwn(meta, field)).map((field) => [field, meta[field]]));
9375
+ }
9376
+ _sameBodyRelation(first, second) {
9377
+ const stable = (value) => Array.isArray(value) ? value.map(stable) : isJsonObject(value) ? Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])])) : value;
9378
+ return JSON.stringify(stable(first)) === JSON.stringify(stable(second));
9379
+ }
9380
+ async _readLegacyBodyFresh(target) {
9381
+ const gateway = await this._resolveGateway(target);
9382
+ const response = await fetchWithTimeout(this._url(target, gateway), { method: "GET", headers: { "X-AUN-Signature-Format": "multi-v1", Accept: "text/markdown", "Cache-Control": "no-cache" }, cache: "no-store", redirect: "error" });
9383
+ if (response.status === 404) throw new StateError("legacy relationship cannot be created by an ordinary body update", { stringCode: "unsupported_protocol" });
9384
+ const etag = headerValue(response.headers, "ETag");
9385
+ if (response.status !== 200 || !etag) throw new StateError("legacy original document or ETag is unavailable", { stringCode: "unavailable", retryable: true });
9386
+ let content;
9387
+ try {
9388
+ content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(await response.arrayBuffer());
9389
+ } catch {
9390
+ throw new ValidationError("agent.md must be valid UTF-8", { stringCode: "invalid" });
9391
+ }
9392
+ return { content, etag, last_modified: headerValue(response.headers, "Last-Modified"), status: response.status };
9393
+ }
9394
+ async _verifyLegacyBodyOriginal(target, content) {
9395
+ const parsed = validateAgentMdDocumentForRead(content, { expectedAid: target, validateOwnership: false, requireSignature: true });
9396
+ const peer = await this._resolvePeer(target, parsed.signature.cert_fingerprint, Number(parsed.signature.timestamp));
9397
+ const verified = await this._verifyPeerDocument(peer, content);
9398
+ if (!verified.ok || verified.data.status !== "verified") throw new ClientSignatureError("legacy original agent.md signature verification failed", { stringCode: "invalid" });
9399
+ return parsed.frontmatter;
9400
+ }
9401
+ async _verifyPeerDocument(peer, content) {
9402
+ const parsed = parseAgentMdTailSignatureForRead(content);
9403
+ const additionalCertificates = /* @__PURE__ */ new Map();
9404
+ if (parsed.fields) additionalCertificates.set(`${parsed.fields.signer_aid ?? peer.aid};${parsed.fields.cert_fingerprint}`, peer);
9405
+ for (const block of parsed.signatures.slice(1)) additionalCertificates.set(`${block.fields.signer_aid};${block.fields.cert_fingerprint}`, await this._resolvePeer(block.fields.signer_aid, block.fields.cert_fingerprint, Number(block.fields.timestamp)));
9406
+ return await peer.verifyAgentMd(content, (signer, fingerprint) => {
9407
+ const certificate = additionalCertificates.get(`${signer};${fingerprint}`);
9408
+ if (!certificate) throw new StateError("additional signing certificate is unavailable", { stringCode: "unavailable", retryable: true });
9409
+ return certificate;
9410
+ });
9411
+ }
9412
+ async _verifyBodyUpload(target, content, signer) {
9413
+ const meta = validateAgentMdDocument(content, { expectedAid: target, validateOwnership: false }).frontmatter;
9414
+ await (this._isLegacyBodyRelation(meta) ? verifyAgentMdLegacyBodyUpload : verifyAgentMdUpload)(content, signer.certPem, target, void 0, async (aid2, fingerprint, timestamp2) => {
9415
+ if (this._entitySignerResolver) return await this._entitySignerResolver(aid2, fingerprint);
9416
+ return await this._resolvePeer(aid2, fingerprint, timestamp2);
9417
+ });
9418
+ }
9296
9419
  async _uploadPrepared(target, signed, signer, originalCondition) {
9297
- const meta = validateAgentMdDocument(signed, { expectedAid: target }).frontmatter;
9420
+ const meta = validateAgentMdDocument(signed, { expectedAid: target, validateOwnership: false }).frontmatter;
9421
+ if (this._isLegacyBodyRelation(meta)) validateOwnershipFrontmatter(Object.fromEntries(["aid", "type", "owner"].filter((field) => Object.hasOwn(meta, field)).map((field) => [field, meta[field]])), target);
9422
+ if (!["group", "org"].includes(String(meta.type))) await this._verifyBodyUpload(target, signed, signer);
9423
+ if (!originalCondition) {
9424
+ const draft = await this.saveRecord(target, { local_draft_content: signed });
9425
+ if (draft.local_draft_content !== signed) throw new StateError("agent.md local draft persistence failed", { stringCode: "persistence_unavailable" });
9426
+ }
9298
9427
  const entity = ["group", "org"].includes(String(meta.type));
9299
9428
  const uploader = entity ? this._currentAid() ?? signer : signer;
9300
9429
  const entityContext = entity ? await this._entityContext(target, meta, uploader) : void 0;
9301
- if (entity) await verifyAgentMdUpload(signed, (await this._entityDocumentSigner(target, signed)).certPem, target);
9430
+ if (entity) await this._verifyEntityDocument(target, signed);
9302
9431
  let condition = originalCondition;
9303
9432
  let relation = entityContext ?? (await this.pendingUpload(target))?.relation;
9304
9433
  if (!condition) {
9305
- const fresh = entity ? await this._readEntityFresh(target) : await this.readOwnershipFresh(target);
9306
- condition = fresh.exists ? { if_match: fresh.etag } : { if_none_match: "*" };
9307
- if (!entity) {
9434
+ let fresh;
9435
+ if (!entity && this._isLegacyBodyRelation(meta)) fresh = await this._readLegacyBodyFresh(target);
9436
+ else {
9437
+ try {
9438
+ fresh = entity ? await this._readEntityFresh(target) : await this.readOwnershipFresh(target);
9439
+ } catch (error) {
9440
+ if (entity || !["unavailable", "unsupported_protocol"].includes(String(error?.stringCode))) throw error;
9441
+ const legacy2 = await this._readLegacyBodyFresh(target);
9442
+ const legacyMeta = validateAgentMdDocumentForRead(legacy2.content, { expectedAid: target, validateOwnership: false }).frontmatter;
9443
+ if (!this._isLegacyBodyRelation(legacyMeta)) throw error;
9444
+ fresh = legacy2;
9445
+ }
9446
+ }
9447
+ const freshMeta = !entity && (!("exists" in fresh) || fresh.exists) && fresh.content ? validateAgentMdDocumentForRead(fresh.content, { expectedAid: target, validateOwnership: false }).frontmatter : void 0;
9448
+ const legacy = !entity && (this._isLegacyBodyRelation(meta) || freshMeta && this._isLegacyBodyRelation(freshMeta));
9449
+ if (legacy) {
9450
+ if (!freshMeta || !this._isLegacyBodyRelation(freshMeta) || !this._sameBodyRelation(this._bodyRelationFields(meta), this._bodyRelationFields(freshMeta))) throw new StateError("legacy relationship changes require their original protocol", { stringCode: "unsupported_protocol" });
9451
+ if (!fresh.etag) throw new StateError("legacy original document ETag is unavailable", { stringCode: "unavailable", retryable: true });
9452
+ await this._verifyLegacyBodyOriginal(target, fresh.content);
9453
+ condition = { if_match: fresh.etag };
9454
+ relation = { protocol: "legacy", original_content: fresh.content, original_content_etag: await _AgentMdManager.contentEtag(fresh.content), original_etag: fresh.etag, fields: this._bodyRelationFields(freshMeta) };
9455
+ } else {
9308
9456
  const authority = fresh;
9309
- const candidate = parseOwnershipFile(signed, "agent", { expectedSubAid: target });
9310
- const statement = fresh.exists && (authority.relation_version > 0 || candidate.statement) ? parseOwnershipFile(fresh.content, "agent", { expectedSubAid: target }).statement : void 0;
9311
- relation = { relation_version: authority.relation_version, lock: authority.lock, owner_aid: authority.owner_aid ?? statement?.owner_aid ?? null, statement: statement ?? null };
9457
+ condition = authority.exists ? { if_match: fresh.etag } : { if_none_match: "*" };
9458
+ if (!entity) {
9459
+ const candidate = parseOwnershipFile(signed, "agent", { expectedSubAid: target });
9460
+ const statement = authority.exists && (authority.relation_version > 0 || candidate.statement) ? parseOwnershipFile(fresh.content, "agent", { expectedSubAid: target }).statement : void 0;
9461
+ relation = { relation_version: authority.relation_version, lock: authority.lock, owner_aid: authority.owner_aid ?? statement?.owner_aid ?? null, statement: statement ?? null };
9462
+ }
9312
9463
  }
9313
9464
  }
9314
- const ownerUpload = entity && uploader.aid.toLowerCase() !== target.toLowerCase();
9315
- const entityAuthorization = ownerUpload ? originalCondition ? (await this.pendingUpload(target))?.authorization : await this._ownerUploadAuthorization(target, signed, uploader, String(entityContext.entity_cert_fingerprint)) : "";
9316
- if (ownerUpload) {
9317
- const timestamp2 = await this._validateEntityUploadAuthorization(entityAuthorization, target, signed, uploader, String(entityContext.entity_cert_fingerprint));
9318
- if (Math.abs(Math.floor(Date.now() / 1e3) - timestamp2) > 300) throw new StateError("pending entity V2 authorization expired; explicit upload is required", { stringCode: "conflict" });
9465
+ if (relation?.protocol === "legacy") {
9466
+ if (signer.aid.toLowerCase() !== target.toLowerCase() || this._currentAid()?.aid.toLowerCase() !== target.toLowerCase() || this._currentAid()?.certFingerprint !== signer.certFingerprint) throw new StateError("legacy body update requires its current subject identity", { stringCode: "forbidden" });
9467
+ if (!this._isLegacyBodyRelation(meta) || !this._sameBodyRelation(this._bodyRelationFields(meta), relation.fields)) throw new StateError("legacy relationship changes require their original protocol", { stringCode: "unsupported_protocol" });
9468
+ await this._verifyBodyUpload(target, signed, signer);
9319
9469
  }
9470
+ const entityAuthorization = "";
9320
9471
  const pending = await this.saveRecord(target, { pending_operation: "replace", pending_target_aid: target, pending_content: signed, pending_content_etag: await _AgentMdManager.contentEtag(signed), pending_error: "", pending_condition: condition, pending_relation: relation, pending_authorization: entityAuthorization });
9472
+ if (relation?.protocol === "legacy" && !this._sameBodyRelation(pending.pending_relation, relation)) throw new StateError("legacy original relationship snapshot persistence failed", { stringCode: "persistence_unavailable" });
9321
9473
  const savedCondition = pending.pending_condition;
9322
9474
  if (pending.pending_content !== signed || savedCondition?.if_match !== condition.if_match || savedCondition?.if_none_match !== condition.if_none_match || pending.pending_authorization !== entityAuthorization) throw new StateError("agent.md pending upload persistence failed");
9323
9475
  try {
9324
9476
  const owner = this._currentAid();
9325
9477
  const delegated = target.toLowerCase() !== this._ownerAid().toLowerCase();
9326
9478
  if (delegated && !owner?.isPrivateKeyValid()) throw new StateError("owner agent.md upload requires a loaded owner identity");
9327
- const authorization = entity ? entityAuthorization : delegated ? await this._ownerUploadAuthorization(target, signed, owner) : "";
9328
- const result = await this._uploadHttp(target, signed, authorization, condition);
9479
+ const authorization = entity ? "" : delegated ? await this._ownerUploadAuthorization(target, signed, owner) : "";
9480
+ const result = await this._uploadHttp(target, signed, authorization, condition, relation?.protocol === "legacy");
9329
9481
  const remoteEtag = String(result.etag ?? "").trim();
9482
+ const contentEtag = await _AgentMdManager.contentEtag(signed);
9330
9483
  const saved = await this.saveRecord(target, {
9331
9484
  content: signed,
9332
- local_etag: await _AgentMdManager.contentEtag(signed),
9485
+ local_etag: contentEtag,
9333
9486
  remote_etag: remoteEtag || void 0,
9487
+ ...remoteEtag === contentEtag ? { local_draft_content: "" } : {},
9334
9488
  last_modified: String(result.last_modified ?? result.lastModified ?? "").trim(),
9335
9489
  fetched_at: Date.now(),
9336
9490
  checked_at: Date.now(),
@@ -9354,12 +9508,16 @@ var AgentMdManager = class _AgentMdManager {
9354
9508
  async readEntityAuthorityFresh(target) {
9355
9509
  const fresh = await this._readEntityFresh(this._safeAid(target));
9356
9510
  if (fresh.ownership_status !== "not_applicable" || !["current", "stale"].includes(fresh.entity_status ?? "") || !fresh.owner_aid || !/^sha256:[0-9a-f]{64}$/.test(fresh.entity_cert_fingerprint ?? "")) throw new StateError("entity current owner authority is unavailable", { stringCode: "unavailable", retryable: true });
9357
- return { owner_aid: this._safeAid(fresh.owner_aid), entity_cert_fingerprint: fresh.entity_cert_fingerprint };
9511
+ try {
9512
+ return { owner_aid: this._safeAid(ownershipAid(fresh.owner_aid, "owner_aid")), entity_cert_fingerprint: fresh.entity_cert_fingerprint };
9513
+ } catch {
9514
+ throw new StateError("entity current owner authority is unavailable", { stringCode: "unavailable", retryable: true });
9515
+ }
9358
9516
  }
9359
9517
  async _readEntityFresh(target) {
9360
9518
  const gateway = await this._resolveGateway(target);
9361
9519
  const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin("node:http") !== null;
9362
- const headers = { "Cache-Control": "no-cache", ...viaGatewayHost ? { Host: target } : {} };
9520
+ const headers = { "X-AUN-Signature-Format": "multi-v1", "X-AUN-Ownership-Check": "1", "Cache-Control": "no-cache", ...viaGatewayHost ? { Host: target } : {} };
9363
9521
  let url = this._url(target, gateway);
9364
9522
  if (viaGatewayHost) {
9365
9523
  const parsed = new URL(gateway);
@@ -9404,7 +9562,7 @@ var AgentMdManager = class _AgentMdManager {
9404
9562
  }
9405
9563
  async _entityContext(target, meta, signer) {
9406
9564
  const owner = String(meta.owner ?? "").trim().toLowerCase();
9407
- if (![owner, target.toLowerCase()].includes(signer.aid.toLowerCase()) || signer.aid.toLowerCase() !== this._ownerAid().toLowerCase()) throw new StateError("group/org agent.md requires entity or current owner uploader", { stringCode: "forbidden" });
9565
+ if (signer.aid.toLowerCase() !== owner || signer.aid.toLowerCase() !== this._ownerAid().toLowerCase()) throw new StateError("group/org agent.md requires current owner uploader", { stringCode: "forbidden" });
9408
9566
  const context = await this._entityCurrentContext(target, meta, true);
9409
9567
  if (signer.aid.toLowerCase() === owner) await this._verifyEntityOwnerSigner(signer);
9410
9568
  else if (signer.certFingerprint !== context.entity_cert_fingerprint) throw new StateError("entity uploader certificate generation changed", { stringCode: "conflict" });
@@ -9424,6 +9582,15 @@ var AgentMdManager = class _AgentMdManager {
9424
9582
  if (signer.aid.toLowerCase() !== signerAid.toLowerCase() || signer.certFingerprint !== parsed.signature.cert_fingerprint) throw new StateError("entity signing certificate changed", { stringCode: "conflict" });
9425
9583
  return signer;
9426
9584
  }
9585
+ async _verifyEntityDocument(target, content) {
9586
+ const primary = await this._entityDocumentSigner(target, content);
9587
+ await verifyAgentMdUpload(content, primary.certPem, target, void 0, async (signer, fingerprint) => {
9588
+ if (!this._entitySignerResolver) throw new StateError("entity additional signing certificate resolver is unavailable", { stringCode: "unavailable", retryable: true });
9589
+ const material = await this._entitySignerResolver(signer, fingerprint);
9590
+ if (material.aid !== signer || material.certFingerprint !== fingerprint) throw new StateError("entity additional signing certificate changed", { stringCode: "conflict" });
9591
+ return material;
9592
+ });
9593
+ }
9427
9594
  async _verifyEntityOwnerSigner(owner) {
9428
9595
  if (!this._entitySignerResolver) throw new StateError("entity owner signing certificate resolver is unavailable", { stringCode: "unavailable", retryable: true });
9429
9596
  const fresh = await this._entitySignerResolver(owner.aid, owner.certFingerprint);
@@ -9434,23 +9601,23 @@ var AgentMdManager = class _AgentMdManager {
9434
9601
  async download(aid2, timeoutMs = DEFAULT_HTTP_TIMEOUT_MS) {
9435
9602
  const target = this._safeAid(String(aid2 ?? this._ownerAid() ?? "").trim());
9436
9603
  const contentResult = await this._downloadHttp(target, timeoutMs);
9437
- const parsed = parseAgentMdTailSignature(contentResult.content);
9604
+ const parsed = parseAgentMdTailSignatureForRead(contentResult.content);
9438
9605
  let entity = false;
9439
9606
  try {
9440
- entity = ["group", "org"].includes(String(parseFileFrontmatter(parsed.payload).type));
9607
+ entity = ["group", "org"].includes(String(parseFileFrontmatter(parsed.payload, false, false).type));
9441
9608
  } catch {
9442
9609
  }
9443
9610
  let frontmatter;
9444
9611
  try {
9445
- frontmatter = validateAgentMdDocument(contentResult.content, { expectedAid: target, validateOwnership: false, ...entity ? { requireSignature: true } : {} }).frontmatter;
9612
+ frontmatter = validateAgentMdDocumentForRead(contentResult.content, { expectedAid: target, validateOwnership: false, ...entity ? { requireSignature: true } : {} }).frontmatter;
9446
9613
  } catch (error) {
9447
9614
  if (entity) throw new ValidationError(error instanceof Error ? error.message : String(error), { stringCode: "invalid" });
9448
9615
  throw error;
9449
9616
  }
9450
9617
  const expectedFp = parsed.fields?.cert_fingerprint || parsed.fields?.public_key_fingerprint || null;
9451
- if (entity) await this._entityCurrentContext(target, frontmatter);
9452
- const peer = entity ? await this._entityDocumentSigner(target, contentResult.content) : await this._resolvePeer(target, expectedFp, parsed.fields?.timestamp ? Number(parsed.fields.timestamp) : void 0);
9453
- const verified = await peer.verifyAgentMd(contentResult.content);
9618
+ const signerAid = entity ? parsed.fields?.signer_aid ?? target : target;
9619
+ const peer = await this._resolvePeer(signerAid, expectedFp, parsed.fields?.timestamp ? Number(parsed.fields.timestamp) : void 0);
9620
+ const verified = await this._verifyPeerDocument(peer, contentResult.content);
9454
9621
  if (!verified.ok) throw new AUNError(verified.error.message);
9455
9622
  const signature2 = { ...verified.data };
9456
9623
  const statusText = String(signature2.status ?? "invalid");
@@ -9704,14 +9871,14 @@ var AgentMdManager = class _AgentMdManager {
9704
9871
  const root = this.root;
9705
9872
  await this._writeStorage(this._contentKey(aid2), String(content ?? ""), root);
9706
9873
  }
9707
- async loadRecord(aid2) {
9874
+ async loadRecord(aid2, strict = false) {
9708
9875
  const target = String(aid2 ?? "").trim();
9709
9876
  if (!target) return null;
9710
9877
  const root = this.root;
9711
9878
  const generation = this._storageGeneration;
9712
9879
  try {
9713
9880
  const loaded = await this._withLock(async () => {
9714
- const record = await this._readRecordUnlocked(target, root);
9881
+ const record = await this._readRecordUnlocked(target, root, strict);
9715
9882
  const next = Object.keys(record).length > 0 ? { ...record, aid: target } : { aid: target };
9716
9883
  try {
9717
9884
  const content = await this._readStorage(this._contentKey(target), root);
@@ -9731,6 +9898,7 @@ var AgentMdManager = class _AgentMdManager {
9731
9898
  if (generation === this._storageGeneration) this._cache.set(target, { ...loaded });
9732
9899
  return { ...loaded };
9733
9900
  } catch (err) {
9901
+ if (strict) throw err;
9734
9902
  this._log.debug(`agent.md cache load skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
9735
9903
  return null;
9736
9904
  }
@@ -9740,10 +9908,12 @@ var AgentMdManager = class _AgentMdManager {
9740
9908
  if (!target) return {};
9741
9909
  const root = this.root;
9742
9910
  const generation = this._storageGeneration;
9911
+ let strictDraft = false;
9743
9912
  try {
9744
9913
  const inputFields = { ...fields };
9914
+ strictDraft = typeof inputFields.local_draft_content === "string";
9745
9915
  const hasContent = Object.prototype.hasOwnProperty.call(inputFields, "content") && inputFields.content !== void 0 && inputFields.content !== null;
9746
- if (hasContent) {
9916
+ if (hasContent && !strictDraft) {
9747
9917
  const text3 = String(inputFields.content ?? "");
9748
9918
  await this._writeStorage(this._contentKey(target), text3, root);
9749
9919
  if (!inputFields.local_etag) inputFields.local_etag = await _AgentMdManager.contentEtag(text3);
@@ -9751,8 +9921,15 @@ var AgentMdManager = class _AgentMdManager {
9751
9921
  }
9752
9922
  delete inputFields.content;
9753
9923
  const record = await this._withLock(async () => {
9754
- const next = { ...await this._readRecordUnlocked(target, root), aid: target };
9924
+ const next = { ...await this._readRecordUnlocked(target, root, strictDraft), aid: target };
9925
+ if (hasContent && strictDraft) {
9926
+ const text3 = String(fields.content ?? "");
9927
+ await this._writeStorage(this._contentKey(target), text3, root);
9928
+ if (!inputFields.local_etag) inputFields.local_etag = await _AgentMdManager.contentEtag(text3);
9929
+ if (!inputFields.fetched_at) inputFields.fetched_at = Date.now();
9930
+ }
9755
9931
  for (const [key, value] of Object.entries(inputFields)) {
9932
+ if (key === "local_draft_content" && value === "" && hasContent && next.local_draft_content !== fields.content) continue;
9756
9933
  if (value !== void 0 && value !== null) next[key] = value;
9757
9934
  }
9758
9935
  next.updated_at = Date.now();
@@ -9764,6 +9941,7 @@ var AgentMdManager = class _AgentMdManager {
9764
9941
  if (generation === this._storageGeneration) this._cache.set(target, { ...loaded });
9765
9942
  return { ...loaded };
9766
9943
  } catch (err) {
9944
+ if (strictDraft) throw err;
9767
9945
  this._log.debug(`agent.md cache save skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
9768
9946
  return {};
9769
9947
  }
@@ -9820,7 +9998,8 @@ var AgentMdManager = class _AgentMdManager {
9820
9998
  return String(await this._gatewayResolver?.(aid2) ?? "");
9821
9999
  }
9822
10000
  async _resolvePeer(aid2, certFingerprint, signedAt) {
9823
- const expectedFp = String(certFingerprint ?? "").trim().toLowerCase();
10001
+ const normalizedFp = normalizeFingerprintHex(certFingerprint);
10002
+ const expectedFp = normalizedFp ? `sha256:${normalizedFp}` : String(certFingerprint ?? "").trim().toLowerCase();
9824
10003
  const current = this._currentAid();
9825
10004
  if (current && current.aid.toLowerCase() === aid2.toLowerCase()) {
9826
10005
  if (!expectedFp || await certMatchesFingerprint(current.certPem, expectedFp)) return current;
@@ -9829,6 +10008,7 @@ var AgentMdManager = class _AgentMdManager {
9829
10008
  if (!(peer instanceof AID)) {
9830
10009
  throw new StateError(`agent.md peer resolver did not return AID for ${aid2}`);
9831
10010
  }
10011
+ if (peer.aid.toLowerCase() !== aid2.toLowerCase()) throw new ClientSignatureError(`agent.md signer does not match requested AID: ${aid2}`, { stringCode: "invalid" });
9832
10012
  return peer;
9833
10013
  }
9834
10014
  async _accessToken(aid2, gatewayUrl) {
@@ -9842,44 +10022,21 @@ var AgentMdManager = class _AgentMdManager {
9842
10022
  if (!token) throw new StateError("authenticate did not return access_token");
9843
10023
  return token;
9844
10024
  }
9845
- async _validateEntityUploadAuthorization(authorization, target, content, owner, entityFingerprint) {
9846
- const invalid2 = new StateError("pending entity original V2 authorization is missing or invalid", { stringCode: "persistence_unavailable" });
9847
- try {
9848
- if (typeof authorization !== "string" || !/^[A-Za-z0-9_-]+={0,2}$/.test(authorization) || authorization.includes("=") && authorization.length % 4 !== 0) throw invalid2;
9849
- const binary2 = globalThis.atob(authorization.replace(/-/g, "+").replace(/_/g, "/"));
9850
- const text3 = new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(binary2, (char) => char.charCodeAt(0)));
9851
- if (base64UrlEncode(text3) !== authorization.replace(/=+$/, "")) throw invalid2;
9852
- const value = JSON.parse(text3);
9853
- const lexical = parseDocument(text3, { uniqueKeys: true });
9854
- if (lexical.errors.length || lexical.get("version", true)?.source !== "2" || !/^[1-9][0-9]*$/.test(lexical.get("timestamp", true)?.source ?? "")) throw invalid2;
9855
- const fields = ["version", "target_aid", "uploader_aid", "body_sha256", "entity_cert_fingerprint", "timestamp", "nonce", "cert_fingerprint", "signature"];
9856
- if (!isJsonObject(value) || Object.keys(value).length !== fields.length || fields.some((field) => !(field in value)) || value.version !== 2) throw invalid2;
9857
- const timestamp2 = value.timestamp, nonce = value.nonce;
9858
- if (typeof timestamp2 !== "number" || !Number.isSafeInteger(timestamp2) || timestamp2 <= 0 || typeof nonce !== "string" || !nonce.trim() || new TextEncoder().encode(nonce).length > 128) throw invalid2;
9859
- if (value.target_aid !== target.toLowerCase() || value.uploader_aid !== owner.aid.toLowerCase() || value.body_sha256 !== await sha256Hex2(content) || value.entity_cert_fingerprint !== entityFingerprint || value.cert_fingerprint !== owner.certFingerprint || typeof value.signature !== "string") throw invalid2;
9860
- const verified = await owner.verify(`AUN-AGENTMD-OWNER-AUTH-V2;${value.uploader_aid};${value.target_aid};${value.body_sha256};${entityFingerprint};${timestamp2};${nonce}`, value.signature);
9861
- if (!verified.ok || !verified.data?.valid) throw invalid2;
9862
- return timestamp2;
9863
- } catch {
9864
- throw invalid2;
9865
- }
9866
- }
9867
- async _ownerUploadAuthorization(target, content, owner, entityFingerprint = "") {
10025
+ async _ownerUploadAuthorization(target, content, owner) {
9868
10026
  const bodySha256 = await sha256Hex2(content);
9869
10027
  const timestamp2 = Math.floor(Date.now() / 1e3);
9870
10028
  const nonce = randomOwnerAuthorizationNonce();
9871
- const canonical = entityFingerprint ? `AUN-AGENTMD-OWNER-AUTH-V2;${owner.aid.toLowerCase()};${target.toLowerCase()};${bodySha256};${entityFingerprint};${timestamp2};${nonce}` : `AUN-AGENT-MD-UPLOAD-V1;${owner.aid.toLowerCase()};${target.toLowerCase()};${bodySha256};${timestamp2};${nonce}`;
10029
+ const canonical = `AUN-AGENT-MD-UPLOAD-V1;${owner.aid.toLowerCase()};${target.toLowerCase()};${bodySha256};${timestamp2};${nonce}`;
9872
10030
  const signed = await owner.sign(canonical);
9873
10031
  if (!signed.ok || !signed.data) {
9874
10032
  const message = signed.error?.message || "owner authorization signing failed";
9875
10033
  throw new ClientSignatureError(message);
9876
10034
  }
9877
10035
  return base64UrlEncode(JSON.stringify({
9878
- version: entityFingerprint ? 2 : 1,
10036
+ version: 1,
9879
10037
  target_aid: target.toLowerCase(),
9880
10038
  uploader_aid: owner.aid.toLowerCase(),
9881
10039
  body_sha256: bodySha256,
9882
- ...entityFingerprint ? { entity_cert_fingerprint: entityFingerprint } : {},
9883
10040
  timestamp: timestamp2,
9884
10041
  nonce,
9885
10042
  cert_fingerprint: owner.certFingerprint,
@@ -9889,18 +10046,20 @@ var AgentMdManager = class _AgentMdManager {
9889
10046
  _url(aid2, gatewayUrl) {
9890
10047
  return `${agentMdHttpScheme(gatewayUrl)}://${this._safeAid(aid2)}/agent.md`;
9891
10048
  }
9892
- async _uploadHttp(aid2, content, ownerAuthorization = "", condition) {
10049
+ async _uploadHttp(aid2, content, ownerAuthorization = "", condition, legacyBodyUpdate = false) {
9893
10050
  if (!condition || Object.keys(condition).length !== 1 || !(typeof condition.if_match === "string" && condition.if_match || condition.if_none_match === "*")) throw new StateError("agent.md upload requires its original document condition", { stringCode: "persistence_unavailable" });
9894
10051
  const gatewayUrl = await this._resolveGateway(aid2);
9895
10052
  const authorization = String(ownerAuthorization ?? "").trim();
9896
- const entity = ["group", "org"].includes(String(validateAgentMdDocument(content, { expectedAid: aid2 }).frontmatter.type));
10053
+ const meta = validateAgentMdDocument(content, { expectedAid: aid2, validateOwnership: false }).frontmatter;
10054
+ const entity = ["group", "org"].includes(String(meta.type));
9897
10055
  const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin("node:http") !== null;
9898
10056
  const headers = {
10057
+ "X-AUN-Signature-Format": "multi-v1",
9899
10058
  "Content-Type": "text/markdown; charset=utf-8",
9900
- ...!entity ? { "X-AUN-Ownership-Check": "1" } : {},
10059
+ ...!entity && !(legacyBodyUpdate && this._isLegacyBodyRelation(meta)) ? { "X-AUN-Ownership-Check": "1" } : {},
9901
10060
  ...condition.if_match ? { "If-Match": condition.if_match } : { "If-None-Match": "*" },
9902
10061
  ...viaGatewayHost ? { Host: this._safeAid(aid2) } : {},
9903
- ...authorization ? { [entity ? "AUN-AGENTMD-OWNER-AUTHORIZATION" : "X-AUN-AgentMD-Owner-Authorization"]: authorization } : {}
10062
+ ...!entity && authorization ? { "X-AUN-AgentMD-Owner-Authorization": authorization } : {}
9904
10063
  };
9905
10064
  if (!authorization || entity) {
9906
10065
  const token = await this._uploadAccessToken(aid2, gatewayUrl);
@@ -9921,17 +10080,17 @@ var AgentMdManager = class _AgentMdManager {
9921
10080
  }
9922
10081
  let response;
9923
10082
  try {
9924
- response = await fetchWithTimeout(url, { method: "PUT", headers, body: content });
10083
+ response = await fetchWithTimeout(url, { method: "PUT", headers, body: content, redirect: "error" });
9925
10084
  } catch (error) {
9926
10085
  if (!viaGatewayHost) throw error;
9927
10086
  const fallbackUrl = nameServiceAgentMdUrl(aid2, gatewayUrl);
9928
10087
  if (!fallbackUrl || fallbackUrl === url) throw error;
9929
- response = await fetchWithTimeout(fallbackUrl, { method: "PUT", headers, body: content });
10088
+ response = await fetchWithTimeout(fallbackUrl, { method: "PUT", headers, body: content, redirect: "error" });
9930
10089
  }
9931
10090
  if (response.status === 404 && viaGatewayHost) {
9932
10091
  const fallbackUrl = nameServiceAgentMdUrl(aid2, gatewayUrl);
9933
10092
  if (fallbackUrl && fallbackUrl !== url) {
9934
- response = await fetchWithTimeout(fallbackUrl, { method: "PUT", headers, body: content });
10093
+ response = await fetchWithTimeout(fallbackUrl, { method: "PUT", headers, body: content, redirect: "error" });
9935
10094
  }
9936
10095
  }
9937
10096
  if (response.status === 404) {
@@ -9954,13 +10113,13 @@ var AgentMdManager = class _AgentMdManager {
9954
10113
  const cached = await this.loadRecord(target) ?? {};
9955
10114
  const gatewayUrl = await this._resolveGateway(target);
9956
10115
  const url = this._url(target, gatewayUrl);
9957
- const headers = { Accept: "text/markdown" };
10116
+ const headers = { "X-AUN-Signature-Format": "multi-v1", Accept: "text/markdown" };
9958
10117
  const cachedEtag = String(cached.remote_etag ?? cached.etag ?? cached.local_etag ?? "").trim();
9959
10118
  const cachedLastModified = String(cached.last_modified ?? "").trim();
9960
10119
  let response = await fetchWithTimeout(url, {
9961
10120
  method: "GET",
9962
10121
  headers,
9963
- redirect: "follow"
10122
+ redirect: "error"
9964
10123
  }, timeoutMs);
9965
10124
  if (response.status === 304 && cached.content !== void 0 && cached.content !== null) {
9966
10125
  return {
@@ -9973,8 +10132,8 @@ var AgentMdManager = class _AgentMdManager {
9973
10132
  if (response.status === 304) {
9974
10133
  response = await fetchWithTimeout(url, {
9975
10134
  method: "GET",
9976
- headers: { Accept: "text/markdown" },
9977
- redirect: "follow"
10135
+ headers: { "X-AUN-Signature-Format": "multi-v1", Accept: "text/markdown" },
10136
+ redirect: "error"
9978
10137
  }, timeoutMs);
9979
10138
  }
9980
10139
  if (response.status === 404) {
@@ -10003,7 +10162,7 @@ var AgentMdManager = class _AgentMdManager {
10003
10162
  const gatewayUrl = await this._resolveGateway(target);
10004
10163
  const response = await fetchWithTimeout(this._url(target, gatewayUrl), {
10005
10164
  method: "HEAD",
10006
- headers: { Accept: "text/markdown" }
10165
+ headers: { "X-AUN-Signature-Format": "multi-v1", Accept: "text/markdown" }
10007
10166
  }, HEAD_HTTP_TIMEOUT_MS);
10008
10167
  const etag = headerValue(response.headers, "ETag");
10009
10168
  const lastModified = headerValue(response.headers, "Last-Modified");
@@ -10095,12 +10254,18 @@ var AgentMdManager = class _AgentMdManager {
10095
10254
  await this._writeStorage(this._metaKey(aid2), `${JSON.stringify(payload, null, 2)}
10096
10255
  `, root);
10097
10256
  }
10098
- async _readRecordUnlocked(aid2, root = this.root) {
10099
- const raw = await this._readStorage(this._metaKey(aid2), root);
10100
- if (raw === null) return {};
10257
+ async _readRecordUnlocked(aid2, root = this.root, strict = false) {
10101
10258
  try {
10102
- return this._normalizeRecord(aid2, JSON.parse(raw));
10259
+ const raw = await this._readStorage(this._metaKey(aid2), root);
10260
+ if (raw === null) return {};
10261
+ const parsed = JSON.parse(raw);
10262
+ if (strict && !isRecord(parsed)) throw new StateError("agent.md metadata is damaged", { stringCode: "persistence_unavailable" });
10263
+ return this._normalizeRecord(aid2, parsed);
10103
10264
  } catch (err) {
10265
+ if (strict) {
10266
+ if (err instanceof StateError) throw err;
10267
+ throw new StateError("agent.md metadata is damaged", { stringCode: "persistence_unavailable" });
10268
+ }
10104
10269
  this._log.warn(`agent.md metadata damaged, ignoring: aid=${aid2} err=${err instanceof Error ? err.message : String(err)}`);
10105
10270
  return {};
10106
10271
  }
@@ -10215,10 +10380,11 @@ function parseOwnershipFile(content, role, context = {}) {
10215
10380
  if (new TextEncoder().encode(parsed.payload).length > 4096) throw new ValidationError("ownership file payload exceeds 4096 bytes");
10216
10381
  if (raw.length - new TextEncoder().encode(parsed.payload).length > 2048) throw new ValidationError("ownership file signature exceeds 2048 bytes");
10217
10382
  const fields = parsed.fields;
10218
- if (fields) {
10219
- if (!/^sha256:[0-9a-f]{64}$/.test(fields.cert_fingerprint) || fields.public_key_fingerprint !== void 0 && !/^sha256:[0-9a-f]{64}$/.test(fields.public_key_fingerprint)) throw new ValidationError("ownership file fingerprint is invalid");
10220
- if (!/^[1-9][0-9]*$/.test(fields.timestamp) || !Number.isSafeInteger(Number(fields.timestamp))) throw new ValidationError("ownership file timestamp must be a positive integer");
10221
- if (!strictDerSignature(fields.signature)) throw new ValidationError("ownership file signature must be strict Base64 DER");
10383
+ if (fields?.signer_aid !== void 0) throw new ValidationError("ownership main signature contains unknown signer_aid");
10384
+ for (const { fields: fields2 } of parsed.signatures) {
10385
+ if (!/^sha256:[0-9a-f]{64}$/.test(fields2.cert_fingerprint) || fields2.public_key_fingerprint !== void 0 && !/^sha256:[0-9a-f]{64}$/.test(fields2.public_key_fingerprint)) throw new ValidationError("ownership file fingerprint is invalid");
10386
+ if (!/^[1-9][0-9]*$/.test(fields2.timestamp) || !Number.isSafeInteger(Number(fields2.timestamp))) throw new ValidationError("ownership file timestamp must be a positive integer");
10387
+ if (!strictDerSignature(fields2.signature)) throw new ValidationError("ownership file signature must be strict Base64 DER");
10222
10388
  }
10223
10389
  let frontmatter;
10224
10390
  let statement;
@@ -10249,7 +10415,7 @@ function parseOwnershipFile(content, role, context = {}) {
10249
10415
  transfer = value;
10250
10416
  }
10251
10417
  } else throw new ValidationError("ownership file role is invalid");
10252
- return { raw, payload: parsed.payload, frontmatter, signature: fields, statement, action, transfer };
10418
+ return { raw, payload: parsed.payload, frontmatter, signature: fields, signatures: parsed.signatures, statement, action, transfer };
10253
10419
  }
10254
10420
  async function verifyOwnershipFile(content, role, certificate, context = {}) {
10255
10421
  const parsed = parseOwnershipFile(content, role, { ...context, requireSignature: true });
@@ -10259,13 +10425,17 @@ async function verifyOwnershipFile(content, role, certificate, context = {}) {
10259
10425
  const storagePath = role === "agent" ? "agent.md" : `sub_aids/${sub}/owner.md`;
10260
10426
  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) throw new ValidationError("ownership file resource or authenticated identity mismatch");
10261
10427
  const now = Math.trunc(context.now ?? Date.now() / 1e3);
10262
- if (Number(fields.timestamp) > now + 300) throw new ValidationError("ownership file timestamp is too far in the future");
10263
- if (certificate.chain_valid === false || certificate.revoked === true || typeof certificate.state === "string" && certificate.state !== "active_signing") throw new ValidationError("ownership signing certificate is not currently verified and active_signing", { stringCode: "invalid" });
10264
- 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) throw new StateError("ownership certificate current status is unavailable", { stringCode: "unavailable", retryable: true });
10265
- validateAgentMdCertificate(certificate.cert_pem, signer, Number(fields.timestamp), now);
10266
- if (await certificateSha256Fingerprint(certificate.cert_pem) !== fields.cert_fingerprint) throw new ValidationError("ownership certificate fingerprint mismatch");
10267
- if (fields.public_key_fingerprint && !await publicKeyMatchesFingerprint(certificate.cert_pem, fields.public_key_fingerprint)) throw new ValidationError("ownership public key fingerprint mismatch");
10268
- if (!await verifySignatureWithCert(certificate.cert_pem, fields.signature, new TextEncoder().encode(parsed.payload))) throw new ValidationError("ownership file signature verification failed");
10428
+ const verified = await verifyDocument(new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(parsed.raw), async (blockSigner, fingerprint, timestamp2) => {
10429
+ const material = blockSigner === signer && fingerprint === fields.cert_fingerprint ? certificate : context.certificateResolver ? await context.certificateResolver(blockSigner, fingerprint) : null;
10430
+ if (!material) throw new StateError("ownership additional signing certificate is unavailable", { stringCode: "unavailable", retryable: true });
10431
+ if (material.chain_valid === false || material.revoked === true || typeof material.state === "string" && material.state !== "active_signing") throw new ValidationError("ownership signing certificate is not currently verified and active_signing", { stringCode: "invalid" });
10432
+ 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) throw new StateError("ownership certificate current status is unavailable", { stringCode: "unavailable", retryable: true });
10433
+ validateAgentMdCertificate(material.cert_pem, blockSigner, timestamp2, now);
10434
+ return material.cert_pem;
10435
+ }, { expectedAid: signer, now, requireCurrentValidity: true });
10436
+ if (!verified.valid) {
10437
+ throw new ValidationError(verified.reason || "ownership file signature verification failed");
10438
+ }
10269
10439
  return parsed;
10270
10440
  }
10271
10441
  async function signOwnershipFile(identity, content, role, context = {}) {
@@ -10277,8 +10447,7 @@ async function signOwnershipFile(identity, content, role, context = {}) {
10277
10447
  if (!parsed.payload.endsWith("\n") && !parsed.payload.endsWith("\r")) throw new ValidationError("ownership payload must end with a newline before signature");
10278
10448
  const now = Math.trunc(context.now ?? Date.now() / 1e3);
10279
10449
  validateAgentMdCertificate(identity.certPem, identity.aid, now, now);
10280
- const signature2 = await signBytes(identity.privateKeyPem, new TextEncoder().encode(parsed.payload));
10281
- return parsed.payload + buildAgentMdSignatureBlock(identity.certFingerprint, now, signature2, await publicKeyFingerprint(identity.certPem), role === "agent" ? "AUN-SIGNATURE-AGENTMD-V2" : "AUN-SIGNATURE-OWNERSHIP-V2");
10450
+ return (await signDocument(identity, parsed.payload, { now, tag: role === "agent" ? "AGENTMD-V2" : "OWNERSHIP-V2" })).signed;
10282
10451
  }
10283
10452
  async function ownershipSha256(content) {
10284
10453
  const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
@@ -10301,9 +10470,9 @@ var OwnershipStorageAdapter = class {
10301
10470
  if (!result || typeof result !== "object" || Array.isArray(result)) throw this.error("unavailable", "Storage authoritative result unavailable");
10302
10471
  return result;
10303
10472
  } catch (error) {
10304
- if (error instanceof StateError && error.stringCode) throw error;
10473
+ if (error instanceof StateError && error.stringCode) throw error.stringCode === "forbidden" && (method === "storage.head_object" || method === "storage.get_object") ? this.error("unavailable", error.message) : error;
10305
10474
  const code = error?.code;
10306
- throw this.error(code === -32009 ? "conflict" : code === -32004 ? "forbidden" : code === -32601 || code === -32602 ? "unsupported_protocol" : "unavailable", `Storage ownership request failed: ${String(error)}`);
10475
+ 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)}`);
10307
10476
  }
10308
10477
  }
10309
10478
  metadata(value, ownerAid, path) {
@@ -10311,20 +10480,20 @@ var OwnershipStorageAdapter = class {
10311
10480
  const observed = value.observed_at;
10312
10481
  const service = `st.${ownerAid.slice(ownerAid.indexOf(".") + 1)}`;
10313
10482
  if (Object.hasOwn(value, "service_aid") && value.service_aid !== service || Object.hasOwn(value, "authenticated_service_aid") && value.authenticated_service_aid !== service) throw this.error("unavailable", "Storage authoritative service identity unavailable");
10314
- 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) throw this.error("unavailable", "Storage authoritative namespace, revision or freshness unavailable");
10315
- 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)) throw this.error("unavailable", "Storage authoritative object metadata unavailable");
10483
+ 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) throw this.error("unavailable", "Storage authoritative namespace or freshness unavailable");
10484
+ if (value.exists && (typeof value.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(value.sha256))) throw this.error("unavailable", "Storage authoritative object metadata unavailable");
10316
10485
  return {
10317
10486
  owner_aid: ownerAid,
10318
10487
  bucket: "default",
10319
10488
  path,
10320
10489
  exists: value.exists,
10321
- revision: value.revision,
10322
10490
  observed_at: observed,
10323
10491
  sha256: value.exists ? String(value.sha256) : "",
10324
10492
  content: "",
10325
10493
  authoritative: true,
10326
10494
  service_aid: service,
10327
- authenticated_service_aid: service
10495
+ authenticated_service_aid: service,
10496
+ version: value.exists && Number.isSafeInteger(value.version) && Number(value.version) > 0 ? Number(value.version) : void 0
10328
10497
  };
10329
10498
  }
10330
10499
  async readFile(ownerAid, path) {
@@ -10334,9 +10503,9 @@ var OwnershipStorageAdapter = class {
10334
10503
  const beforeRaw = await this.request("storage.head_object", params2);
10335
10504
  const before = this.metadata(beforeRaw, ownerAid, path);
10336
10505
  if (!before.exists) return before;
10337
- const body = await this.request("storage.get_object", { ...params2, expected_revision: before.revision });
10506
+ const body = await this.request("storage.get_object", params2);
10338
10507
  const current = this.metadata(body, ownerAid, path);
10339
- if (!current.exists || current.revision !== before.revision || current.sha256 !== before.sha256 || body.object_id !== beforeRaw.object_id || body.version !== beforeRaw.version) throw this.error("unavailable", "Storage ownership body revision changed");
10508
+ if (!current.exists || current.sha256 !== before.sha256) throw this.error("unavailable", "Storage ownership body changed");
10340
10509
  if (typeof body.content !== "string") throw this.error("unavailable", "Storage ownership body is unavailable");
10341
10510
  if (body.content !== "" && !isStrictBase64(body.content)) throw this.error("unavailable", "Storage ownership body is not strict Base64");
10342
10511
  const bytes = base64ToUint8(body.content);
@@ -10349,28 +10518,43 @@ var OwnershipStorageAdapter = class {
10349
10518
  }
10350
10519
  const afterRaw = await this.request("storage.head_object", params2);
10351
10520
  const after = this.metadata(afterRaw, ownerAid, path);
10352
- 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() / 1e3) - began > 30) throw this.error("unavailable", "Storage ownership changed during authoritative read");
10521
+ if (!after.exists || after.sha256 !== before.sha256 || Math.trunc(this.deps.now?.() ?? Date.now() / 1e3) - began > 30) throw this.error("unavailable", "Storage ownership changed during authoritative read");
10353
10522
  return { ...after, content };
10354
10523
  }
10355
10524
  async readOwnerMd(ownerAid, subAid) {
10356
10525
  return await this.readFile(ownerAid, `sub_aids/${ownershipAid(subAid, "sub_aid")}/owner.md`);
10357
10526
  }
10358
- async writeOwnerMd(ownerAid, subAid, raw, expectedRevision) {
10527
+ async writeOwnerMd(ownerAid, subAid, raw) {
10359
10528
  if (ownerAid !== this.deps.currentAid()) throw this.error("forbidden", "only the authenticated owner may publish owner.md");
10360
- if (typeof expectedRevision !== "string" || !expectedRevision) throw this.error("invalid", "ownership expected_revision is required");
10529
+ const path = `sub_aids/${ownershipAid(subAid, "sub_aid")}/owner.md`;
10530
+ const before = await this.readOwnerMd(ownerAid, subAid);
10361
10531
  const bytes = new TextEncoder().encode(raw);
10362
- let binary2 = "";
10363
- for (const byte of bytes) binary2 += String.fromCharCode(byte);
10364
- 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(binary2), content_type: "text/markdown", is_private: false, overwrite: true, expected_revision: expectedRevision });
10532
+ const expectedVersion = before.exists ? before.version : 0;
10533
+ if (typeof expectedVersion !== "number" || !Number.isSafeInteger(expectedVersion) || expectedVersion < (before.exists ? 1 : 0)) throw this.error("unavailable", "Storage owner.md version is unavailable");
10534
+ 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 });
10535
+ const uploadURL = String(session.upload_url ?? ""), sessionID = String(session.session_id ?? "");
10536
+ if (!uploadURL || !sessionID) throw this.error("unavailable", "Storage owner.md upload session is incomplete");
10537
+ const headers = {};
10538
+ if (session.headers && typeof session.headers === "object") {
10539
+ for (const [key, value] of Object.entries(session.headers)) if (value !== void 0 && value !== null && key.toLowerCase() !== "content-type") headers[key] = String(value);
10540
+ }
10541
+ headers["Content-Type"] = "text/markdown; charset=utf-8";
10542
+ if (this.deps.httpPut) await this.deps.httpPut(uploadURL, bytes, headers);
10543
+ else {
10544
+ const response = await fetch(uploadURL, { method: "PUT", body: bytes, headers });
10545
+ if (!response.ok) throw this.error("unavailable", `Storage owner.md HTTP PUT failed: ${response.status}`);
10546
+ }
10547
+ 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 };
10548
+ complete.expected_version = expectedVersion;
10549
+ await this.request("storage.complete_upload", complete);
10365
10550
  return await this.readOwnerMd(ownerAid, subAid);
10366
10551
  }
10367
- async deleteOwnerMd(ownerAid, subAid, expectedRevision) {
10552
+ async deleteOwnerMd(ownerAid, subAid) {
10368
10553
  if (ownerAid !== this.deps.currentAid()) throw this.error("forbidden", "only the authenticated owner may delete owner.md");
10369
- if (typeof expectedRevision !== "string" || !expectedRevision) throw this.error("invalid", "ownership expected_revision is required");
10370
10554
  const path = `sub_aids/${ownershipAid(subAid, "sub_aid")}/owner.md`;
10371
- await this.request("storage.delete_object", { owner_aid: ownershipAid(ownerAid, "owner_aid"), bucket: "default", object_key: path, expected_revision: expectedRevision });
10555
+ await this.request("storage.delete_object", { owner_aid: ownershipAid(ownerAid, "owner_aid"), bucket: "default", object_key: path });
10372
10556
  const after = this.metadata(await this.request("storage.head_object", { owner_aid: ownerAid, bucket: "default", object_key: path, consistency: "authoritative" }), ownerAid, path);
10373
- if (after.exists || after.revision === expectedRevision) throw this.error("conflict", "Storage ownership deletion could not be confirmed");
10557
+ if (after.exists) throw this.error("conflict", "Storage ownership deletion could not be confirmed");
10374
10558
  }
10375
10559
  };
10376
10560
  function requireOwnershipApplicable(type) {
@@ -10390,12 +10574,12 @@ var OwnershipFiles = class {
10390
10574
  validateAgentSnapshot(snapshot, subAid) {
10391
10575
  if (snapshot.protocol !== "2.1") throw new StateError("NS ownership 2.1 protocol capability is unavailable", { stringCode: "unsupported_protocol", retryable: true });
10392
10576
  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) throw new StateError("NS ownership 2.1 authoritative metadata is unavailable", { stringCode: "unavailable", retryable: true });
10393
- if (snapshot.exists && !snapshot.etag || !snapshot.exists && (snapshot.lock !== "unlocked" || snapshot.relation_version !== 0)) throw new StateError("NS ownership document or ledger is unavailable");
10577
+ if (snapshot.exists && !snapshot.etag || !snapshot.exists && (snapshot.lock !== "unlocked" || snapshot.relation_version !== 0)) throw new StateError("NS ownership document metadata is unavailable");
10394
10578
  this.fresh(snapshot.observed_at);
10395
10579
  }
10396
10580
  async validateOwnerSnapshot(snapshot, ownerAid, subAid) {
10397
10581
  const expectedService = `st.${ownerAid.slice(ownerAid.indexOf(".") + 1)}`;
10398
- 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") throw new StateError("Storage authoritative ownership object is unavailable");
10582
+ 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") throw new StateError("Storage authoritative ownership object is unavailable");
10399
10583
  this.fresh(snapshot.observed_at);
10400
10584
  if (snapshot.exists && (!/^[0-9a-f]{64}$/.test(snapshot.sha256) || await ownershipSha256(snapshot.content) !== snapshot.sha256)) throw new StateError("Storage ownership body and metadata differ", { stringCode: "unavailable", retryable: true });
10401
10585
  }
@@ -10420,32 +10604,35 @@ var OwnershipFiles = class {
10420
10604
  const file = parseOwnershipFile(content, role, { expectedSubAid: subAid, expectedOwnerAid: role === "owner" ? ownerAid : void 0, now: this.now() });
10421
10605
  const signer = role === "agent" ? subAid : ownerAid;
10422
10606
  const certificate = await this.deps.resolveCertificate(signer, file.signature.cert_fingerprint, role === "agent" && file.frontmatter.type === "group" ? "group_identity" : void 0);
10423
- return await verifyOwnershipFile(content, role, certificate, { expectedSubAid: subAid, expectedOwnerAid: role === "owner" ? ownerAid : void 0, certificateAid: signer, now: this.now() });
10607
+ return await verifyOwnershipFile(content, role, certificate, { expectedSubAid: subAid, expectedOwnerAid: role === "owner" ? ownerAid : void 0, certificateAid: signer, certificateResolver: (aid2, fingerprint) => this.deps.resolveCertificate(aid2, fingerprint), now: this.now() });
10424
10608
  }
10425
10609
  async checkConsensus(subAid = this.deps.currentAid().aid, pendingTarget) {
10426
10610
  const began = this.now();
10427
- 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: "" };
10611
+ const result = { status: "unavailable", sub_aid: subAid, owner_aid: null, relation_version: 0, lock: "unlocked", agent_md_etag: "", agent_md_sha256: "", owner_md_sha256: "" };
10428
10612
  try {
10429
10613
  const first = await this.readAgent(subAid);
10430
10614
  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) : "" });
10431
- const released = first.lock === "unlocked" && first.relation_version > 0;
10432
- const agent = !released && first.exists ? await this.verify(first.content, "agent", subAid) : null;
10433
- if (first.lock === "unlocked" && first.relation_version === 0 && agent?.statement) throw new StateError("NS relation metadata does not match agent.md", { stringCode: "unavailable", retryable: true });
10434
- const ownerAid = released ? void 0 : agent?.statement?.owner_aid ?? pendingTarget?.owner_aid;
10615
+ const agent = first.exists ? await this.verify(first.content, "agent", subAid) : null;
10616
+ if ((agent?.statement?.relation_version ?? 0) !== first.relation_version) throw new StateError("NS relation metadata does not match agent.md", { stringCode: "unavailable", retryable: true });
10617
+ const ownerAid = agent?.statement?.owner_aid ?? pendingTarget?.owner_aid;
10435
10618
  let owner = null;
10436
10619
  let object = null;
10437
10620
  if (ownerAid) {
10438
10621
  object = await this.readOwner(ownerAid, subAid);
10439
- result.owner_md_revision = object.revision;
10440
10622
  result.owner_md_sha256 = object.exists ? object.sha256 : "";
10441
10623
  if (object.exists) owner = await this.verify(object.content, "owner", subAid, ownerAid);
10442
10624
  }
10443
10625
  const last = await this.readAgent(subAid);
10444
10626
  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) throw new StateError("NS ownership changed during verification");
10627
+ if (object && ownerAid) {
10628
+ const current = await this.readOwner(ownerAid, subAid);
10629
+ if (current.exists !== object.exists || current.sha256 !== object.sha256 || current.content !== object.content) throw new StateError("Storage ownership changed during verification", { stringCode: "unavailable", retryable: true });
10630
+ }
10445
10631
  this.fresh(began);
10446
10632
  this.fresh(first.observed_at);
10447
10633
  if (object) this.fresh(object.observed_at);
10448
- if (released) {
10634
+ if (first.lock === "unlocked" && agent?.statement) {
10635
+ if (!object || object.exists && (!owner?.statement || owner.action !== "release" || !ownershipStatementsMatch(agent.statement, owner.statement))) throw new StateError("current ownership files do not confirm release", { stringCode: "unavailable", retryable: true });
10449
10636
  result.status = "released";
10450
10637
  } else if (first.lock === "locked") {
10451
10638
  if (!agent?.statement || agent.statement.relation_version !== first.relation_version || first.owner_aid !== void 0 && first.owner_aid !== agent.statement.owner_aid) throw new StateError("ownership current files do not match the NS lock", { stringCode: "unavailable", retryable: true });
@@ -10479,9 +10666,21 @@ var OwnershipFiles = class {
10479
10666
  if (snapshot.relation_version !== task.base_relation_version || snapshot.lock !== (task.operation === "bind" ? "unlocked" : "locked")) throw new StateError("ownership relation version conflict", { stringCode: "ownership_conflict", retryable: false });
10480
10667
  if (!snapshot.exists) throw new StateError("initial agent.md is not published", { stringCode: "unavailable", retryable: true });
10481
10668
  if (snapshot.exists) {
10482
- 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);
10669
+ const file = await this.verify(snapshot.content, "agent", task.sub_aid);
10670
+ const base = task.base_agent_md;
10671
+ 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)))) throw new StateError("ownership original relation was replaced", { stringCode: "conflict", retryable: false });
10672
+ if (!base) {
10673
+ task.base_agent_md = { exists: true, raw: snapshot.content, sha256: await ownershipSha256(snapshot.content), etag: snapshot.etag, statement: file.statement ?? null };
10674
+ await task.persist();
10675
+ }
10483
10676
  if (snapshot.lock === "unlocked" && snapshot.relation_version === 0 && file.statement) throw new StateError("NS relation metadata does not match agent.md", { stringCode: "unavailable", retryable: true });
10484
- if (task.operation !== "bind" && file.statement?.owner_aid !== task.owner_aid || task.operation === "bind" && file.statement && file.statement.relation_version >= snapshot.relation_version) throw new StateError("ownership current owner mismatch", { stringCode: "conflict", retryable: false });
10677
+ if ((file.statement?.relation_version ?? 0) !== snapshot.relation_version || task.operation !== "bind" && file.statement?.owner_aid !== task.owner_aid) throw new StateError("ownership current owner mismatch", { stringCode: "conflict", retryable: false });
10678
+ if (task.operation === "bind" && file.statement?.owner_aid === task.owner_aid) throw new StateError("current owner release requires sub cleanup before binding again", { stringCode: "conflict", retryable: false });
10679
+ if (task.operation === "bind" && base?.statement && task.base_owner_md) {
10680
+ const original = await this.readOwner(base.statement.owner_aid, task.sub_aid), expected = task.base_owner_md;
10681
+ if (original.exists !== expected.exists || original.content !== expected.raw || original.sha256 !== expected.sha256) throw new StateError("ownership original released owner file was replaced", { stringCode: "conflict", retryable: false });
10682
+ }
10683
+ if (task.operation === "bind" && file.statement && (await this.checkConsensus(task.sub_aid)).status !== "released") throw new StateError("current ownership files do not confirm release", { stringCode: "unavailable", retryable: true });
10485
10684
  }
10486
10685
  return snapshot;
10487
10686
  }
@@ -10491,6 +10690,10 @@ var OwnershipFiles = class {
10491
10690
  if (!object.exists) throw new StateError("ownership transfer authorization is unavailable");
10492
10691
  const file = await this.verify(object.content, "owner", task.sub_aid, task.owner_aid);
10493
10692
  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) throw new StateError("ownership transfer authorization target changed", { stringCode: "conflict", retryable: false });
10693
+ if (task.base_owner_md?.exists) {
10694
+ const original = parseOwnershipFile(task.base_owner_md.raw, "owner", { expectedSubAid: task.sub_aid, expectedOwnerAid: task.owner_aid }).statement;
10695
+ if (!original || !["version", "declaration_id", "owner_aid", "sub_aid", "relation_version", "declared_at"].every((key) => original[key] === file.statement[key])) throw new StateError("ownership original transfer relation changed", { stringCode: "conflict", retryable: false });
10696
+ }
10494
10697
  if (file.transfer.expires_at <= this.now()) throw new StateError("ownership transfer authorization expired", { stringCode: "conflict", retryable: false });
10495
10698
  if (task.authorization_sha256 !== void 0 && task.authorization_sha256 !== object.sha256 || task.expires_at !== void 0 && task.expires_at !== file.transfer.expires_at) throw new StateError("ownership transfer authorization changed", { stringCode: "conflict", retryable: false });
10496
10699
  if (task.authorization_sha256 === void 0 || task.expires_at === void 0) {
@@ -10505,11 +10708,21 @@ var OwnershipFiles = class {
10505
10708
  await this.readAgent(task.sub_aid);
10506
10709
  const current = await this.readOwner(ownerAid, task.sub_aid);
10507
10710
  let prepared = task.files[key];
10711
+ if (!prepared && action !== "declare" && !current.exists) throw new StateError("ownership original owner file is unavailable", { stringCode: "unavailable", retryable: true });
10712
+ if (!prepared && !task.base_owner_md && action !== "declare") {
10713
+ const snapshot = current;
10714
+ task.base_owner_md = { exists: snapshot.exists, raw: snapshot.content, sha256: snapshot.sha256 };
10715
+ await task.persist();
10716
+ }
10508
10717
  if (!prepared) {
10509
10718
  await this.requireBase(task);
10719
+ const base2 = task.base_owner_md;
10720
+ if (ownerAid === task.base_agent_md?.statement?.owner_aid && base2 && (current.exists !== base2.exists || current.content !== base2.raw || current.sha256 !== base2.sha256)) throw new StateError("ownership original owner file was replaced", { stringCode: "conflict", retryable: false });
10510
10721
  if (action === "transfer_owner") task.expires_at = this.now() + 3600;
10722
+ const original = action === "declare" || !current.exists ? null : await this.verify(current.content, "owner", task.sub_aid, ownerAid);
10723
+ if (original && (!original.statement || original.statement.relation_version !== task.base_relation_version)) throw new StateError("ownership original owner relation changed", { stringCode: "conflict", retryable: false });
10511
10724
  const ownership = {
10512
- statement: this.statement(ownerAid, task.sub_aid, version),
10725
+ statement: original?.statement ?? this.statement(ownerAid, task.sub_aid, version),
10513
10726
  action,
10514
10727
  ...action === "transfer_owner" ? { transfer: { new_owner_aid: task.new_owner_aid, target_relation_version: task.target_relation_version, expires_at: task.expires_at } } : {}
10515
10728
  };
@@ -10521,27 +10734,24 @@ ownership: ${JSON.stringify(ownership)}
10521
10734
  `;
10522
10735
  const raw = await signOwnershipFile(identity, payload, "owner", { now: this.now() });
10523
10736
  await this.verify(raw, "owner", task.sub_aid, ownerAid);
10524
- prepared = { raw, sha256: await ownershipSha256(raw), condition: { expected_revision: current.revision } };
10737
+ prepared = { raw, sha256: await ownershipSha256(raw), condition: {}, original_owner: { exists: current.exists, raw: current.content, sha256: current.sha256 } };
10525
10738
  if (action === "transfer_owner") prepared.expires_at = task.expires_at;
10526
10739
  task.files[key] = prepared;
10527
10740
  await task.persist();
10528
10741
  }
10529
- if (!prepared.condition || typeof prepared.condition.expected_revision !== "string" || !prepared.condition.expected_revision) throw new StateError("ownership original Storage revision is unavailable", { stringCode: "ownership_conflict" });
10742
+ if (!prepared.condition || Object.keys(prepared.condition).length !== 0 || !prepared.original_owner) throw new StateError("ownership original owner.md context is unavailable", { stringCode: "ownership_conflict" });
10530
10743
  if (current.exists && current.sha256 === prepared.sha256 && current.content === prepared.raw) {
10531
- if (prepared.published_revision !== void 0 && prepared.published_revision !== current.revision) throw new StateError("ownership published file revision changed", { stringCode: "ownership_conflict" });
10532
- prepared.published_revision = current.revision;
10533
- await task.persist();
10534
10744
  return prepared;
10535
10745
  }
10536
10746
  await this.requireBase(task);
10537
- if (current.revision !== prepared.condition.expected_revision) throw new StateError("ownership Storage revision conflict", { stringCode: "ownership_conflict" });
10538
10747
  if (action === "transfer_owner" && (!task.expires_at || task.expires_at <= this.now())) throw new StateError("ownership transfer authorization expired", { stringCode: "conflict", retryable: false });
10539
10748
  await this.verify(prepared.raw, "owner", task.sub_aid, ownerAid);
10540
10749
  await task.guard?.();
10541
- await this.deps.writeOwnerMd(ownerAid, task.sub_aid, prepared.raw, prepared.condition.expected_revision);
10750
+ const latest = await this.readOwner(ownerAid, task.sub_aid), base = prepared.original_owner;
10751
+ if (latest.exists !== base.exists || latest.content !== base.raw || latest.sha256 !== base.sha256) throw new StateError("ownership original owner file was replaced", { stringCode: "conflict", retryable: false });
10752
+ await this.deps.writeOwnerMd(ownerAid, task.sub_aid, prepared.raw);
10542
10753
  const saved = await this.readOwner(ownerAid, task.sub_aid);
10543
10754
  if (!saved.exists || saved.sha256 !== prepared.sha256 || saved.content !== prepared.raw) throw new StateError("ownership published file could not be confirmed");
10544
- prepared.published_revision = saved.revision;
10545
10755
  await task.persist();
10546
10756
  return prepared;
10547
10757
  }
@@ -10565,6 +10775,7 @@ ownership: ${JSON.stringify(ownership)}
10565
10775
  if (cleanup) {
10566
10776
  if (current.lock !== "unlocked" || current.relation_version !== task.target_relation_version) throw new StateError("owner_locked or ownership cleanup version conflict");
10567
10777
  const prior = current.exists ? parseOwnershipFile(current.content, "agent", { expectedSubAid: task.sub_aid, now: this.now() }) : null;
10778
+ 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]))) throw new StateError("ownership cleanup historical declaration changed", { stringCode: "conflict", retryable: false });
10568
10779
  if (prior?.statement && (prior.statement.owner_aid !== task.owner_aid || prior.statement.relation_version !== task.base_relation_version)) throw new StateError("ownership cleanup historical authorization mismatch", { stringCode: "conflict", retryable: false });
10569
10780
  } else {
10570
10781
  await this.requireBase(task);
@@ -10584,7 +10795,7 @@ ownership: ${JSON.stringify(ownership)}
10584
10795
  await task.persist();
10585
10796
  }
10586
10797
  if (!prepared.condition || !(typeof prepared.condition.if_match === "string" && prepared.condition.if_match || prepared.condition.if_none_match === "*")) throw new StateError("ownership original agent.md condition is unavailable", { stringCode: "ownership_conflict" });
10587
- if (current.exists && await ownershipSha256(current.content) === prepared.sha256 && current.content === prepared.raw && current.relation_version === task.target_relation_version) {
10798
+ if (current.exists && await ownershipSha256(current.content) === prepared.sha256 && current.content === prepared.raw && current.relation_version === (cleanup ? 0 : task.target_relation_version)) {
10588
10799
  if (prepared.published_etag !== void 0 && prepared.published_etag !== current.etag) throw new StateError("ownership published agent ETag changed", { stringCode: "ownership_conflict" });
10589
10800
  prepared.published_etag = current.etag;
10590
10801
  await task.persist();
@@ -10601,7 +10812,7 @@ ownership: ${JSON.stringify(ownership)}
10601
10812
  await task.guard?.();
10602
10813
  const saved = await this.deps.submitAgentMdRaw(task.sub_aid, prepared.raw, prepared.condition);
10603
10814
  this.validateAgentSnapshot(saved, task.sub_aid);
10604
- if (!saved.exists || await ownershipSha256(saved.content) !== prepared.sha256 || saved.relation_version !== task.target_relation_version) throw new StateError("NS ownership submitted file could not be confirmed");
10815
+ if (!saved.exists || await ownershipSha256(saved.content) !== prepared.sha256 || saved.relation_version !== (cleanup ? 0 : task.target_relation_version)) throw new StateError("NS ownership submitted file could not be confirmed");
10605
10816
  prepared.published_etag = saved.etag;
10606
10817
  await task.persist();
10607
10818
  return prepared;
@@ -10616,21 +10827,32 @@ ownership: ${JSON.stringify(ownership)}
10616
10827
  this.local(task.owner_aid);
10617
10828
  await this.readAgent(task.sub_aid);
10618
10829
  const prepared = task.files[task.operation === "release" ? "release" : "transfer"];
10619
- if (!prepared?.published_revision || prepared.cleaned) return;
10830
+ if (!prepared || prepared.cleaned) return;
10831
+ const began = this.now();
10620
10832
  const consensus = await this.checkConsensus(task.sub_aid);
10621
10833
  if (consensus.status === "not_applicable") throw new StateError(consensus.reason || "group/org use their dedicated lifecycle ownership", { stringCode: "not_applicable", retryable: false });
10622
10834
  if (consensus.status === "invalid" || consensus.status === "unavailable") throw new StateError(consensus.reason || "ownership cleanup verification failed", { stringCode: consensus.status, retryable: consensus.status === "unavailable" });
10623
- if (consensus.relation_version !== task.target_relation_version || (task.operation === "release" ? consensus.status !== "released" : consensus.status !== "consensus" || consensus.owner_aid !== task.new_owner_aid)) throw new StateError("ownership cleanup relation is not confirmed", { stringCode: "conflict", retryable: false });
10835
+ 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) throw new StateError("ownership cleanup relation is not confirmed", { stringCode: "conflict", retryable: false });
10624
10836
  const object = await this.readOwner(task.owner_aid, task.sub_aid);
10625
10837
  if (!object.exists) {
10626
10838
  prepared.cleaned = true;
10627
10839
  await task.persist();
10628
10840
  return;
10629
10841
  }
10630
- if (object.revision !== prepared.published_revision || object.sha256 !== prepared.sha256) throw new StateError("ownership cleanup revision conflict");
10631
- if (!this.deps.deleteOwnerMd) throw new StateError("ownership conditional delete unsupported");
10842
+ if (object.content !== prepared.raw || object.sha256 !== prepared.sha256) throw new StateError("ownership cleanup original owner file was replaced", { stringCode: "conflict", retryable: false });
10843
+ const agent = await this.readAgent(task.sub_aid);
10844
+ 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) throw new StateError("ownership cleanup current files changed", { stringCode: "unavailable", retryable: true });
10845
+ if (!this.deps.deleteOwnerMd) throw new StateError("ownership owner file delete unsupported");
10632
10846
  await task.guard?.();
10633
- await this.deps.deleteOwnerMd(task.owner_aid, task.sub_aid, prepared.published_revision);
10847
+ const latest = await this.readOwner(task.owner_aid, task.sub_aid);
10848
+ if (!latest.exists) {
10849
+ prepared.cleaned = true;
10850
+ await task.persist();
10851
+ return;
10852
+ }
10853
+ if (latest.content !== prepared.raw || latest.sha256 !== prepared.sha256) throw new StateError("ownership cleanup original owner file was replaced", { stringCode: "conflict", retryable: false });
10854
+ this.fresh(began);
10855
+ await this.deps.deleteOwnerMd(task.owner_aid, task.sub_aid);
10634
10856
  prepared.cleaned = true;
10635
10857
  await task.persist();
10636
10858
  }
@@ -10640,13 +10862,16 @@ ownership: ${JSON.stringify(ownership)}
10640
10862
  var AID_RE2 = /^[A-Za-z0-9_][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_][A-Za-z0-9_-]*)+$/;
10641
10863
  var SEMVER_RE = /^\d+\.\d+\.\d+$/;
10642
10864
  var AGENT_MD_MAX_SIGNATURE_BYTES = 2048;
10865
+ function aidEqual(left, right) {
10866
+ return typeof left === "string" && typeof right === "string" && left.toLowerCase() === right.toLowerCase();
10867
+ }
10643
10868
  function assertValidUtf8Text(content) {
10644
10869
  const encoded = new TextEncoder().encode(content);
10645
10870
  if (new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(encoded) !== content) {
10646
10871
  throw new ValidationError("agent.md must be valid UTF-8 text");
10647
10872
  }
10648
10873
  }
10649
- function parseFileFrontmatter(payload) {
10874
+ function parseFileFrontmatter(payload, allowEmpty = false, validateOwnershipNumbers = true) {
10650
10875
  const lines = payload.replace(/^\ufeff/, "").split(/\r\n|\n|\r/);
10651
10876
  if (lines[0]?.trim() !== "---") throw new ValidationError("agent.md must start with YAML frontmatter");
10652
10877
  const end = lines.slice(1).findIndex((line) => line.trim() === "---");
@@ -10657,30 +10882,44 @@ function parseFileFrontmatter(payload) {
10657
10882
  if (node && typeof node === "object" && (isAlias(node) || "anchor" in node && node.anchor || "tag" in node && node.tag)) throw new ValidationError("YAML tags, anchors and aliases are forbidden");
10658
10883
  });
10659
10884
  if (doc.warnings.length) throw new ValidationError(`agent.md frontmatter is invalid: ${doc.warnings[0].message}`);
10660
- for (const path of [
10885
+ for (const path of validateOwnershipNumbers ? [
10661
10886
  ["ownership", "statement", "version"],
10662
10887
  ["ownership", "statement", "relation_version"],
10663
10888
  ["ownership", "statement", "declared_at"],
10664
10889
  ["ownership", "transfer", "target_relation_version"],
10665
10890
  ["ownership", "transfer", "expires_at"]
10666
- ]) {
10891
+ ] : []) {
10667
10892
  const scalar = doc.getIn(path, true);
10668
10893
  if (scalar && typeof scalar.value === "number" && !/^(?:0|[1-9][0-9]*)$/.test(scalar.source ?? "")) throw new ValidationError(`${path.join(".")} must be a YAML integer`);
10669
10894
  }
10670
10895
  const result = doc.toJS();
10896
+ if (allowEmpty && doc.contents === null) return {};
10671
10897
  if (!result || typeof result !== "object" || Array.isArray(result)) throw new ValidationError("agent.md frontmatter must be a mapping");
10672
10898
  return result;
10673
10899
  }
10674
10900
  function validateAgentMdDocument(content, opts = {}) {
10901
+ return validateAgentMdDocumentForPurpose(content, opts);
10902
+ }
10903
+ function validateAgentMdDocumentForRead(content, opts = {}, fallbackAid = opts.expectedAid) {
10904
+ const parsed = validateAgentMdDocumentForPurpose(content, opts, true, fallbackAid);
10905
+ if (["group", "org"].includes(String(parsed.frontmatter.type)) && !(parsed.frontmatter.type === "group" && parsed.signatures.length === 1 && !Object.hasOwn(parsed.frontmatter, "entity_cert_fingerprint") && (parsed.signature?.signer_aid === void 0 || aidEqual(parsed.signature.signer_aid, parsed.frontmatter.aid)))) return validateAgentMdDocument(content, opts);
10906
+ return parsed;
10907
+ }
10908
+ function validateAgentMdDocumentForPurpose(content, opts, reading = false, fallbackAid) {
10675
10909
  if (!String(content ?? "").trim()) throw new ValidationError("agent.md content cannot be empty");
10676
10910
  assertValidUtf8Text(String(content));
10677
- if ((String(content).match(/<!-- AUN-SIGNATURE/g) ?? []).length > 1) throw new ValidationError("agent.md must contain exactly one signature block");
10678
- const parsed = parseAgentMdTailSignature(String(content));
10911
+ const parsed = (reading ? parseAgentMdTailSignatureForRead : parseAgentMdTailSignature)(String(content));
10679
10912
  if (parsed.parseError) throw new ValidationError(parsed.parseError);
10680
10913
  if (opts.requireSignature && !parsed.fields) throw new ValidationError("agent.md signature is required");
10681
10914
  if (new TextEncoder().encode(parsed.payload).byteLength > (opts.maxPayloadBytes ?? 4096)) throw new ValidationError("agent.md payload exceeds 4096 bytes");
10682
10915
  if (parsed.fields && new TextEncoder().encode(String(content)).byteLength - new TextEncoder().encode(parsed.payload).byteLength > AGENT_MD_MAX_SIGNATURE_BYTES) throw new ValidationError(`agent.md signature block exceeds ${AGENT_MD_MAX_SIGNATURE_BYTES} bytes`);
10683
- const meta = parseFileFrontmatter(parsed.payload);
10916
+ const meta = reading && parsed.fields && parsed.payload.replace(/^\ufeff/, "").split(/\r\n|\n|\r/, 1)[0]?.trim() !== "---" ? { aid: fallbackAid } : parseFileFrontmatter(parsed.payload, reading && !!parsed.fields, !reading);
10917
+ if (reading && parsed.fields) {
10918
+ if (!Object.hasOwn(meta, "aid")) meta.aid = fallbackAid;
10919
+ if (!Object.hasOwn(meta, "name")) meta.name = meta.aid;
10920
+ if (!Object.hasOwn(meta, "type")) meta.type = "agent";
10921
+ if (!Object.hasOwn(meta, "version")) meta.version = "1.0.0";
10922
+ }
10684
10923
  for (const key of ["aid", "name", "type", "version"]) if (!(key in meta)) throw new ValidationError(`agent.md frontmatter missing ${key}`);
10685
10924
  if (typeof meta.aid !== "string" || !AID_RE2.test(meta.aid)) throw new ValidationError("agent.md frontmatter aid has invalid format");
10686
10925
  if (opts.expectedAid && meta.aid.toLowerCase() !== opts.expectedAid.trim().toLowerCase()) throw new ValidationError("agent.md frontmatter aid does not match local AID");
@@ -10697,36 +10936,49 @@ function validateAgentMdDocument(content, opts = {}) {
10697
10936
  if ("exttype" in meta && typeof meta.exttype !== "string") throw new ValidationError("agent.md frontmatter exttype must be a string");
10698
10937
  if (meta.tags !== void 0 && (!Array.isArray(meta.tags) || meta.tags.some((item) => typeof item !== "string"))) throw new ValidationError("agent.md tags must be an array of strings");
10699
10938
  const entity = ["group", "org"].includes(String(meta.type));
10939
+ const legacyEntity = reading && meta.type === "group" && !Object.hasOwn(meta, "entity_cert_fingerprint") && parsed.signatures.length === 1 && parsed.fields && (parsed.fields.signer_aid === void 0 || aidEqual(parsed.fields.signer_aid, meta.aid));
10700
10940
  if (entity) {
10701
- if (typeof meta.owner !== "string" || !AID_RE2.test(meta.owner)) throw new ValidationError("group/org agent.md requires owner");
10702
- if (typeof meta.entity_cert_fingerprint !== "string" || !/^sha256:[0-9a-f]{64}$/.test(meta.entity_cert_fingerprint)) throw new ValidationError("group/org agent.md requires entity_cert_fingerprint");
10703
- if (["ownership", "ownership_proof", "ownership_release"].some((field) => Object.hasOwn(meta, field))) throw new ValidationError("group/org agent.md cannot use Ownership 2.1");
10941
+ if ((!legacyEntity || Object.hasOwn(meta, "owner")) && (typeof meta.owner !== "string" || !AID_RE2.test(meta.owner))) throw new ValidationError("group/org agent.md requires owner");
10942
+ if ((!legacyEntity || Object.hasOwn(meta, "entity_cert_fingerprint")) && (typeof meta.entity_cert_fingerprint !== "string" || !/^sha256:[0-9a-f]{64}$/.test(meta.entity_cert_fingerprint))) throw new ValidationError("group/org agent.md requires entity_cert_fingerprint");
10943
+ if (!legacyEntity && ["ownership", "ownership_proof", "ownership_release"].some((field) => Object.hasOwn(meta, field))) throw new ValidationError("group/org agent.md cannot use Ownership 2.1");
10704
10944
  } else if (opts.validateOwnership ?? true) {
10705
10945
  if ("ownership" in meta || "owner" in meta && !("ownership_proof" in meta)) validateOwnershipAgentFrontmatter(meta, opts.expectedAid);
10706
10946
  else validateOwnershipFrontmatter(meta, opts.expectedAid);
10707
10947
  }
10708
10948
  const fields = parsed.fields;
10709
10949
  if (fields) {
10710
- if (Object.keys(fields).some((key) => !["cert_fingerprint", "public_key_fingerprint", "timestamp", "signature", ...entity ? ["signer_aid"] : []].includes(key))) throw new ValidationError("agent.md signature contains unknown fields");
10711
- if (entity && ![meta.aid, meta.owner].includes(fields.signer_aid ?? meta.aid)) throw new ValidationError("group/org agent.md signer must be entity or owner");
10712
- if (!/^sha256:[0-9a-f]{64}$/.test(fields.cert_fingerprint)) throw new ValidationError("agent.md cert_fingerprint is invalid");
10713
- if (fields.public_key_fingerprint !== void 0 && !/^sha256:[0-9a-f]{64}$/.test(fields.public_key_fingerprint)) throw new ValidationError("agent.md public_key_fingerprint is invalid");
10714
- if (!/^[1-9][0-9]*$/.test(fields.timestamp) || !Number.isSafeInteger(Number(fields.timestamp)) || !isStrictBase64(fields.signature)) throw new ValidationError("agent.md signature fields are invalid");
10950
+ if (Object.keys(fields).some((key) => !["cert_fingerprint", "public_key_fingerprint", "timestamp", "signature", "signer_aid"].includes(key))) throw new ValidationError("agent.md signature contains unknown fields");
10951
+ if (!entity && fields.signer_aid !== void 0) throw new ValidationError("ordinary agent.md main signature contains unknown signer_aid");
10952
+ if (entity && (legacyEntity ? fields.signer_aid !== void 0 && !aidEqual(fields.signer_aid, meta.aid) : !aidEqual(fields.signer_aid, meta.aid) || fields.cert_fingerprint !== meta.entity_cert_fingerprint)) throw new ValidationError("group/org agent.md primary signer must be entity");
10953
+ if (entity && parsed.signatures.slice(1).some((block) => !aidEqual(block.fields.signer_aid, meta.owner))) throw new ValidationError("group/org agent.md additional signer must be current owner");
10954
+ const fingerprintPattern = /^sha256:[0-9a-f]{64}$/;
10955
+ if (reading ? !normalizeFingerprintHex(fields.cert_fingerprint) : !fingerprintPattern.test(fields.cert_fingerprint)) throw new ValidationError("agent.md cert_fingerprint is invalid");
10956
+ if (fields.public_key_fingerprint !== void 0 && (reading ? !normalizeFingerprintHex(fields.public_key_fingerprint) : !fingerprintPattern.test(fields.public_key_fingerprint))) throw new ValidationError("agent.md public_key_fingerprint is invalid");
10957
+ if (!(reading ? /^[0-9]+$/ : /^[1-9][0-9]*$/).test(fields.timestamp) || !Number.isSafeInteger(Number(fields.timestamp)) || Number(fields.timestamp) <= 0 || !isStrictBase64(fields.signature)) throw new ValidationError("agent.md signature fields are invalid");
10715
10958
  const now = Math.trunc(opts.now ?? Date.now() / 1e3);
10716
10959
  const ts = Number(fields.timestamp);
10717
10960
  if ((opts.checkTimestamp ?? true) && ts > now + 300) throw new ValidationError("agent.md signature timestamp is too far in the future");
10718
10961
  }
10719
- return { payload: parsed.payload, frontmatter: meta, signature: fields };
10962
+ return { payload: parsed.payload, frontmatter: meta, signature: fields, signatures: parsed.signatures };
10963
+ }
10964
+ async function verifyAgentMdUpload(content, certPem, expectedAid, now, resolver) {
10965
+ return verifyAgentMdUploadForPurpose(content, certPem, expectedAid, now, true, resolver);
10720
10966
  }
10721
- async function verifyAgentMdUpload(content, certPem, expectedAid, now) {
10722
- const parsed = validateAgentMdDocument(content, { expectedAid, requireSignature: true, now });
10967
+ async function verifyAgentMdLegacyBodyUpload(content, certPem, expectedAid, now, resolver) {
10968
+ return verifyAgentMdUploadForPurpose(content, certPem, expectedAid, now, false, resolver);
10969
+ }
10970
+ async function verifyAgentMdUploadForPurpose(content, certPem, expectedAid, now, validateOwnership = true, resolver) {
10971
+ const parsed = validateAgentMdDocument(content, { expectedAid, requireSignature: true, now, validateOwnership });
10723
10972
  const fields = parsed.signature;
10724
- const signer = ["group", "org"].includes(String(parsed.frontmatter.type)) ? fields.signer_aid ?? expectedAid : expectedAid;
10725
- validateAgentMdCertificate(certPem, signer, Number(fields.timestamp), now);
10726
- const actualCertFingerprint = (await certificateSha256Fingerprint(certPem)).toLowerCase();
10727
- if (actualCertFingerprint !== fields.cert_fingerprint.trim().toLowerCase()) throw new ValidationError("agent.md certificate fingerprint mismatch");
10728
- if (fields.public_key_fingerprint && !await publicKeyMatchesFingerprint(certPem, fields.public_key_fingerprint)) throw new ValidationError("agent.md public key fingerprint mismatch");
10729
- if (!await verifySignatureWithCert(certPem, fields.signature, new TextEncoder().encode(parsed.payload))) throw new ValidationError("agent.md signature verification failed");
10973
+ const verified = await verifyDocument(content, async (signer, fingerprint, timestamp2) => {
10974
+ const material = signer.toLowerCase() === expectedAid.toLowerCase() && await certificateSha256Fingerprint(certPem) === fingerprint ? certPem : resolver ? await resolver(signer, fingerprint, timestamp2) : "";
10975
+ const pem = typeof material === "string" ? material : material.certPem;
10976
+ validateAgentMdCertificate(pem, signer, timestamp2, now);
10977
+ return pem;
10978
+ }, { expectedAid, now, requireCurrentValidity: true });
10979
+ if (!verified.valid) {
10980
+ throw new ValidationError(verified.reason || "agent.md signature verification failed");
10981
+ }
10730
10982
  }
10731
10983
  function validateAgentMdCertificate(certPem, expectedAid, timestamp2, now, requireCurrentValidity = true) {
10732
10984
  const metadata = parseCertMetadata(certPem);
@@ -10735,7 +10987,7 @@ function validateAgentMdCertificate(certPem, expectedAid, timestamp2, now, requi
10735
10987
  const current = Math.trunc(now ?? Date.now() / 1e3);
10736
10988
  if (![notBefore, notAfter, current, timestamp2].every(Number.isFinite)) throw new ValidationError("agent.md signing certificate is invalid");
10737
10989
  if (requireCurrentValidity && (current < notBefore || current > notAfter)) throw new ValidationError("agent.md signing certificate is expired or not yet valid");
10738
- if (metadata.subject.trim().toLowerCase() !== expectedAid.trim().toLowerCase()) throw new ValidationError("agent.md signing certificate subject does not match AID");
10990
+ if (requireCurrentValidity ? !aidEqual(metadata.subject, expectedAid) : !aidEqual(metadata.subject.trim(), expectedAid.trim())) throw new ValidationError("agent.md signing certificate subject does not match AID");
10739
10991
  if (timestamp2 < notBefore || timestamp2 > notAfter) throw new ValidationError("agent.md signature timestamp is outside certificate validity");
10740
10992
  }
10741
10993
 
@@ -10834,23 +11086,13 @@ var AID = class _AID {
10834
11086
  if (payload && !payload.endsWith("\n") && !payload.endsWith("\r")) {
10835
11087
  return resultErr(SIGNATURE_OPERATION_ERROR, "agent.md payload must end with a newline before signature");
10836
11088
  }
10837
- const signature2 = await signBytes(this.privateKeyPem, new TextEncoder().encode(payload));
10838
11089
  let frontmatter = {};
10839
11090
  try {
10840
- frontmatter = parseFileFrontmatter(payload);
11091
+ frontmatter = parseFileFrontmatter(payload, true, false);
10841
11092
  } catch {
10842
11093
  }
10843
- const entity = ["group", "org"].includes(String(frontmatter.type));
10844
- if (entity && ![frontmatter.aid, frontmatter.owner].includes(this.aid)) return resultErr(SIGNATURE_OPERATION_ERROR, "group/org agent.md requires entity or current owner signer");
10845
- let block = buildAgentMdSignatureBlock(
10846
- this.certFingerprint,
10847
- Date.now() / 1e3,
10848
- signature2,
10849
- await publicKeyFingerprint(this.certPem)
10850
- );
10851
- if (entity) block = block.replace("cert_fingerprint:", `signer_aid: ${this.aid}
10852
- cert_fingerprint:`);
10853
- return resultOk({ signed: payload + block });
11094
+ if (["group", "org"].includes(String(frontmatter.type)) && frontmatter.aid !== this.aid) return resultErr(SIGNATURE_OPERATION_ERROR, "group/org agent.md requires entity primary signer");
11095
+ return resultOk(await signDocument(this, payload, { signerAid: ["group", "org"].includes(String(frontmatter.type)) }));
10854
11096
  } catch (exc) {
10855
11097
  return resultErr(SIGNATURE_OPERATION_ERROR, String(exc), exc);
10856
11098
  }
@@ -10889,37 +11131,26 @@ cert_fingerprint:`);
10889
11131
  return resultErr(VERIFICATION_OPERATION_ERROR, String(exc), exc);
10890
11132
  }
10891
11133
  }
10892
- async verifyAgentMd(content) {
11134
+ async verifyAgentMd(content, certificateResolver) {
10893
11135
  try {
10894
11136
  const rawContent = String(content ?? "");
10895
11137
  let parsed;
10896
11138
  try {
10897
- parsed = validateAgentMdDocument(rawContent, { validateOwnership: false });
10898
- const signer = ["group", "org"].includes(String(parsed.frontmatter.type)) ? parsed.signature?.signer_aid ?? parsed.frontmatter.aid : parsed.frontmatter.aid;
10899
- if (String(signer).toLowerCase() !== this.aid.toLowerCase()) throw new Error("agent.md signer does not match certificate identity");
10900
- } catch (exc) {
10901
- const payload2 = parseAgentMdTailSignature(rawContent).payload;
10902
- return resultOk({ status: "invalid", payload: payload2, aid: this.aid, reason: exc instanceof Error ? exc.message : String(exc) });
10903
- }
10904
- const { payload, signature: fields } = parsed;
10905
- if (!fields) return resultOk({ status: "unsigned", payload });
10906
- try {
10907
- validateAgentMdCertificate(this.certPem, this.aid, Number(fields.timestamp), void 0, false);
11139
+ parsed = validateAgentMdDocumentForRead(rawContent, { validateOwnership: false }, this.aid);
10908
11140
  } catch (exc) {
10909
- return resultOk({ status: "invalid", payload, aid: this.aid, reason: exc instanceof Error ? exc.message : String(exc) });
10910
- }
10911
- const payloadAid = extractAgentMdAid(payload);
10912
- const entity = ["group", "org"].includes(String(parsed.frontmatter.type));
10913
- if (!entity && payloadAid && payloadAid.toLowerCase() !== this.aid.toLowerCase()) return resultOk({ status: "invalid", payload, aid: payloadAid, reason: "aid mismatch" });
10914
- if (!await certMatchesFingerprint(this.certPem, fields.cert_fingerprint)) {
10915
- return resultOk({ status: "invalid", payload, aid: this.aid, reason: "certificate fingerprint mismatch" });
10916
- }
10917
- const publicKeyFp = String(fields.public_key_fingerprint ?? "").trim();
10918
- if (publicKeyFp && !await publicKeyMatchesFingerprint(this.certPem, publicKeyFp)) {
10919
- return resultOk({ status: "invalid", payload, aid: this.aid, reason: "public key fingerprint mismatch" });
10920
- }
10921
- const valid = await verifySignatureWithCert(this.certPem, fields.signature, new TextEncoder().encode(payload));
10922
- return resultOk(valid ? { status: "verified", payload, aid: entity ? String(parsed.frontmatter.aid) : this.aid, cert_fingerprint: fields.cert_fingerprint, public_key_fingerprint: publicKeyFp, timestamp: Number(fields.timestamp) } : { status: "invalid", payload, aid: this.aid, cert_fingerprint: fields.cert_fingerprint, timestamp: Number(fields.timestamp), reason: "signature verification failed" });
11141
+ return resultOk({ status: "invalid", payload: parseAgentMdTailSignature(rawContent).payload, aid: this.aid, reason: exc instanceof Error ? exc.message : String(exc) });
11142
+ }
11143
+ const resolver = async (signer, fingerprint, timestamp2) => {
11144
+ if (signer.toLowerCase() === this.aid.toLowerCase() && (await certMatchesFingerprint(this.certPem, fingerprint) || !certificateResolver)) return this.certPem;
11145
+ if (certificateResolver) return await certificateResolver(signer, fingerprint, timestamp2);
11146
+ const store = this._aidStore;
11147
+ const resolved = await store?.resolve?.(signer, { skipAgentMd: true });
11148
+ if (!resolved?.ok) throw new Error("additional signing certificate is unavailable");
11149
+ return resolved.data.aid;
11150
+ };
11151
+ const verified = await verifyDocument(rawContent, resolver, { expectedAid: this.aid, reading: !["group", "org"].includes(String(parsed.frontmatter.type)) || parsed.frontmatter.type === "group" && !Object.hasOwn(parsed.frontmatter, "entity_cert_fingerprint") && parsed.signatures.length === 1 });
11152
+ const fields = parsed.signature;
11153
+ return resultOk({ ...verified, aid: this.aid, cert_fingerprint: fields?.cert_fingerprint, public_key_fingerprint: fields?.public_key_fingerprint, timestamp: fields ? Number(fields.timestamp) : void 0 });
10923
11154
  } catch (exc) {
10924
11155
  return resultErr(VERIFICATION_OPERATION_ERROR, String(exc), exc);
10925
11156
  }
@@ -11125,6 +11356,19 @@ var REPRESENTATION_FIELDS = /* @__PURE__ */ new Set([
11125
11356
  ]);
11126
11357
  var REPRESENTATION_SIGNATURE_FIELDS = /* @__PURE__ */ new Set(["cert_fingerprint", "public_key_fingerprint", "timestamp", "signature"]);
11127
11358
  var FINGERPRINT_RE2 = /^sha256:[0-9a-f]{64}$/i;
11359
+ var SERVICE_EVENTS = /* @__PURE__ */ new Set([
11360
+ "org.member.join_requested",
11361
+ "org.member.join_approved",
11362
+ "org.member.join_rejected",
11363
+ "org.member.invite_created",
11364
+ "org.member.invite_accepted",
11365
+ "org.member.invite_rejected",
11366
+ "org.member.changed",
11367
+ "org.lifecycle.created",
11368
+ "org.lifecycle.dissolving",
11369
+ "org.lifecycle.dissolved",
11370
+ "credential.revoked"
11371
+ ]);
11128
11372
  var ProtocolError = class extends Error {
11129
11373
  constructor(code, message = code) {
11130
11374
  super(`${code}: ${message}`);
@@ -11137,20 +11381,24 @@ var ProtocolError = class extends Error {
11137
11381
  };
11138
11382
  async function verifyServiceEvent(eventName, payload, context) {
11139
11383
  const event = eventName.startsWith("event/") ? eventName.slice(6) : eventName;
11140
- if (!isRecord2(payload) || !context || !["type", "event", "event_id", "responder", "target_aids", "as_of", "expires_at", "signature"].every((field) => field in payload) || payload.type !== event || payload.event !== event || typeof payload.event_id !== "string" || !payload.event_id || ["_route", "_federation", "_trace", "member_aids", "inline_message"].some((field) => field in payload)) throw invalid("service event payload is invalid");
11384
+ if (!SERVICE_EVENTS.has(event) || !isRecord2(payload) || !context || !["type", "event", "event_id", "responder", "target_aids", "as_of", "expires_at", "signature"].every((field) => field in payload) || payload.type !== event || payload.event !== event || typeof payload.event_id !== "string" || !payload.event_id || ["_route", "_federation", "_trace", "member_aids", "inline_message"].some((field) => field in payload)) throw invalid("service event payload is invalid");
11141
11385
  normalizeNfc(payload);
11142
11386
  aid(context.targetAid, "event target");
11143
11387
  aid(payload.responder, "event responder");
11144
- if (!Array.isArray(payload.target_aids) || !payload.target_aids.length || payload.target_aids.some((target) => typeof target !== "string" || !AID_RE3.test(target)) || new Set(payload.target_aids).size !== payload.target_aids.length || !payload.target_aids.includes(context.targetAid)) throw invalid("service event audience is invalid");
11388
+ if (!Array.isArray(payload.target_aids) || !payload.target_aids.length || payload.target_aids.some((target) => typeof target !== "string" || !AID_RE3.test(target))) throw invalid("service event audience is invalid");
11389
+ const targetAids = new Set(payload.target_aids.map((target) => aidKey(target)));
11390
+ if (targetAids.size !== payload.target_aids.length || !targetAids.has(aidKey(context.targetAid))) throw invalid("service event audience is invalid");
11391
+ const responder = payload.responder;
11392
+ const responderKey = aidKey(responder);
11145
11393
  const now = context.now ?? Math.trunc(Date.now() / 1e3);
11146
11394
  if (!isInt(now) || !isInt(payload.as_of) || !isInt(payload.expires_at) || payload.as_of <= 0 || payload.expires_at <= payload.as_of || payload.expires_at > payload.as_of + 300 || payload.as_of > now + 300 || now >= payload.expires_at) throw invalid("service event time window is invalid");
11147
11395
  let known = true;
11148
11396
  if (event.startsWith("org.")) {
11149
11397
  aid(payload.org_id, "event organization");
11150
- if (payload.responder !== "org." + String(payload.org_id).slice(String(payload.org_id).indexOf(".") + 1)) throw invalid("organization event authority is invalid");
11398
+ if (responderKey !== aidKey("org." + String(payload.org_id).slice(String(payload.org_id).indexOf(".") + 1))) throw invalid("organization event authority is invalid");
11151
11399
  } else if (event.startsWith("credential.")) {
11152
11400
  const cid = payload.credentialCid ?? payload.credential_cid;
11153
- if (!String(payload.responder).startsWith("credential.") || typeof payload.target !== "string" || !payload.target_aids.includes(payload.target) || typeof cid !== "string" || !cid || payload.credentialCid !== void 0 && payload.credential_cid !== void 0 && payload.credentialCid !== payload.credential_cid) throw invalid("credential event authority or target is invalid");
11401
+ if (!responderKey.startsWith("credential.") || typeof payload.target !== "string" || !targetAids.has(aidKey(payload.target)) || typeof cid !== "string" || !cid || payload.credentialCid !== void 0 && payload.credential_cid !== void 0 && payload.credentialCid !== payload.credential_cid) throw invalid("credential event authority or target is invalid");
11154
11402
  let service;
11155
11403
  try {
11156
11404
  service = await context.resolveCredentialService?.(cid);
@@ -11158,7 +11406,7 @@ async function verifyServiceEvent(eventName, payload, context) {
11158
11406
  throw invalid("trusted credential authority is unavailable");
11159
11407
  }
11160
11408
  known = service != null;
11161
- if (known && (typeof service !== "string" || !service.startsWith("credential.") || service !== payload.responder)) throw invalid("credential event authority differs from the trusted provider");
11409
+ if (known && (typeof service !== "string" || !AID_RE3.test(service) || !aidKey(service).startsWith("credential.") || aidKey(service) !== responderKey)) throw invalid("credential event authority differs from the trusted provider");
11162
11410
  } else throw invalid("service event namespace is invalid");
11163
11411
  try {
11164
11412
  signature(payload.signature);
@@ -11166,14 +11414,17 @@ async function verifyServiceEvent(eventName, payload, context) {
11166
11414
  if (signed.timestamp !== payload.as_of || typeof context.resolveEventCertificate !== "function") throw new Error("service event signature context is invalid");
11167
11415
  const body = { ...payload };
11168
11416
  delete body.signature;
11169
- const certificate = await context.resolveEventCertificate(payload.responder, signed.cert_fingerprint);
11170
- await verifyAgentSignature(signed, body, payload.responder, () => certificate, context.now ?? Math.trunc(Date.now() / 1e3), false);
11417
+ const certificate = await context.resolveEventCertificate(responderKey, signed.cert_fingerprint);
11418
+ await verifyAgentSignature(signed, body, responder, () => certificate, context.now ?? Math.trunc(Date.now() / 1e3), false);
11171
11419
  } catch {
11172
11420
  throw new ProtocolError("CREDENTIAL_SIGNATURE_INVALID", "service event signature verification failed");
11173
11421
  }
11174
11422
  if ((context.now ?? Math.trunc(Date.now() / 1e3)) >= payload.expires_at) throw invalid("service event time window is invalid");
11175
11423
  return { signature_valid: true, credential_known: known };
11176
11424
  }
11425
+ function aidKey(value) {
11426
+ return value.trim().toLowerCase();
11427
+ }
11177
11428
  function protocolErrorRetryable(code) {
11178
11429
  return ERROR_RETRYABLE[String(code)] === true;
11179
11430
  }
@@ -11512,6 +11763,7 @@ function validateCredential(credential, chainDepth = 0, externalProofChain) {
11512
11763
  const delegationDepth = "delegation_depth" in credential ? credential.delegation_depth : 0;
11513
11764
  if (!isInt(delegationDepth) || delegationDepth < 0) throw invalid("delegation_depth is invalid");
11514
11765
  const parent = credential.parent;
11766
+ if (credential.proof_chain !== void 0 && externalProofChain !== void 0 && canonicalText(credential.proof_chain) !== canonicalText(externalProofChain)) throw new ProtocolError("CREDENTIAL_CHAIN_INVALID", "embedded proof_chain conflicts with external proof_chain");
11515
11767
  if (parent == null && delegationDepth !== 0) throw invalid("root credential must have delegation_depth 0");
11516
11768
  if (parent != null) {
11517
11769
  if (!("delegation_depth" in credential)) throw invalid("child delegation_depth is required");
@@ -11754,6 +12006,7 @@ async function prepareOrganizationRequest(method, params2, target, signer, optio
11754
12006
  if (!binding || !isRecord2(binding.member_signature) || ("for" in value ? value.for : signer.aid) !== binding.member_aid || value.member_aid !== binding.member_aid || binding.org_id !== value.org_id || target !== "org." + String(binding.org_id).slice(String(binding.org_id).indexOf(".") + 1)) throw representationBindingInvalid("admission target must match the signed membership binding");
11755
12007
  value.credentials = credentials;
11756
12008
  const now = Math.trunc(Date.now() / 1e3);
12009
+ if (now >= Number(binding.exp)) throw representationBindingInvalid("membership binding has expired");
11757
12010
  let maximum = Math.min(now + 300, Number(binding.exp));
11758
12011
  for (const field of ["credential_template", "registration_authorization"]) if (field in value) {
11759
12012
  const material = value[field];
@@ -11819,6 +12072,18 @@ async function verifyCredential(credential, context) {
11819
12072
  if (value.sub !== context.subject) throw invalid("credential subject does not match holder");
11820
12073
  const now = context.now ?? Math.trunc(Date.now() / 1e3);
11821
12074
  const documents = credentialChainDocuments(credential, context.proofChain);
12075
+ const resolvedCertificates = [];
12076
+ const resolveCertificate = async (fingerprint) => {
12077
+ const material = await context.resolveCertificate(fingerprint);
12078
+ if (material != null) resolvedCertificates.push(material);
12079
+ return material;
12080
+ };
12081
+ const resolveActorCertificate = context.resolveActorCertificate ? async (fingerprint) => {
12082
+ const material = await context.resolveActorCertificate(fingerprint);
12083
+ if (material != null) resolvedCertificates.push(material);
12084
+ return material;
12085
+ } : resolveCertificate;
12086
+ const statusProofs = [];
11822
12087
  const status = async (cid) => {
11823
12088
  const document2 = documents.find((item) => item.cid === cid);
11824
12089
  let proof;
@@ -11829,6 +12094,7 @@ async function verifyCredential(credential, context) {
11829
12094
  throw new ProtocolError("CREDENTIAL_STATUS_UNKNOWN", "trusted credential status is unavailable");
11830
12095
  }
11831
12096
  if (isRecord2(proof)) {
12097
+ statusProofs.push(proof);
11832
12098
  if (typeof proof.credentialCid === "string" && proof.credentialCid !== cid) throw new ProtocolError("CREDENTIAL_CHAIN_INVALID", "registered credential cid differs from the signed document");
11833
12099
  if (proof.ver !== "aun-credential-status/1" || proof.credentialCid !== cid || typeof proof.credential_hash !== "string" || !DIGEST_RE.test(proof.credential_hash) || !isInt(proof.asOf) || !isInt(proof.proofExpiresAt) || proof.asOf > now + 300 || proof.proofExpiresAt <= proof.asOf || proof.proofExpiresAt > proof.asOf + 300 || now >= proof.proofExpiresAt || typeof proof.responder !== "string" || !proof.responder.startsWith("credential.") || !AID_RE3.test(proof.responder.slice(11)) || typeof proof.request_id !== "string" || !proof.request_id || typeof proof.nonce !== "string" || !proof.nonce || typeof proof.target !== "string" || !AID_RE3.test(proof.target)) throw new ProtocolError("CREDENTIAL_STATUS_UNKNOWN", "trusted credential status proof is unavailable");
11834
12100
  try {
@@ -11843,7 +12109,7 @@ async function verifyCredential(credential, context) {
11843
12109
  if (document2 !== value) throw new ProtocolError("CREDENTIAL_STATUS_UNKNOWN", "parent credential status proof is required");
11844
12110
  return typeof proof === "string" ? proof : void 0;
11845
12111
  };
11846
- await verifyCredentialChain(credential, context.resolveCertificate, status, now, context.audience, context.proofChain);
12112
+ await verifyCredentialChain(credential, resolveCertificate, status, now, context.audience, context.proofChain);
11847
12113
  if (credentialChainDocuments(credential, context.proofChain).at(-1).iss !== (context.rootIssuer ?? context.audience)) throw invalid("credential root issuer is not trusted for this resource");
11848
12114
  const rate = context.checkRateLimit ? (field, unit, limit) => context.checkRateLimit(String(value.cid), String(value.sub), field, unit, limit) : void 0;
11849
12115
  await validateCredentialCapability(credential, { ns: context.ns, action: context.action, resource: context.resource, params: context.params, units: context.units, now, client_ip: context.clientIp, proof_chain: context.proofChain }, rate);
@@ -11858,13 +12124,14 @@ async function verifyCredential(credential, context) {
11858
12124
  const caps = value.cap;
11859
12125
  if (value.iss !== binding.org_id || !value.aud.includes(binding.org_id) || caps.length !== 1 || caps[0].ns !== "org" || caps[0].action !== "role." + String(binding.role) || caps[0].resource !== binding.org_id) throw invalid("member credential capability does not match binding role");
11860
12126
  }
11861
- await verifyMembershipBinding(binding, context.resolveCertificate, now, context.orgSigner, true);
12127
+ await verifyMembershipBinding(binding, resolveCertificate, now, context.orgSigner, true);
11862
12128
  if (!context.resolveMembershipStatus || await context.resolveMembershipStatus(binding, value) !== true) throw new ProtocolError("MEMBERSHIP_STATE_INVALID", "trusted current membership status does not permit this operation");
11863
12129
  }
11864
12130
  const result = { signature_valid: true, chain_valid: true, status_checked: true, capability_checked: true, request_bound: true };
11865
12131
  const params2 = context.params ?? {};
11866
12132
  const target = context.for ?? params2.for;
11867
12133
  const actor = context.actor ?? context.subject;
12134
+ let verifiedRepresentation;
11868
12135
  if (typeof actor !== "string" || !AID_RE3.test(actor)) throw new ProtocolError("CREDENTIAL_REQUEST_BINDING_INVALID", "authenticated actor is invalid");
11869
12136
  if (target !== void 0 && (typeof target !== "string" || !target || !context.actor || Object.hasOwn(params2, "for") && params2.for !== target)) throw new ProtocolError("CREDENTIAL_REQUEST_BINDING_INVALID", "authenticated representation context is required");
11870
12137
  if (target !== void 0 && target !== actor || actor !== context.subject) {
@@ -11872,7 +12139,9 @@ async function verifyCredential(credential, context) {
11872
12139
  const packaged = isRecord2(params2.credentials) ? params2.credentials.representation_proof : void 0;
11873
12140
  const proof = context.representationProof ?? packaged;
11874
12141
  if (packaged !== void 0 && canonicalText(packaged) !== canonicalText(proof)) throw new ProtocolError("CREDENTIAL_REQUEST_BINDING_INVALID", "representation proof differs from the request");
11875
- await verifyRepresentationProof(proof, credential, { from: actor, for: target, audience: context.target, resource: target, params: params2, binding: context.requestBinding }, context.resolveActorCertificate ?? context.resolveCertificate, now, context.proofChain);
12142
+ const representationRequest = { from: actor, for: target, audience: context.target, resource: target, params: params2, binding: context.requestBinding };
12143
+ await verifyRepresentationProof(proof, credential, representationRequest, resolveActorCertificate, now, context.proofChain);
12144
+ verifiedRepresentation = { proof, request: representationRequest };
11876
12145
  const subjects = [String(target), ...context.subject !== actor && context.subject !== target ? [context.subject] : []];
11877
12146
  let allowed = Boolean(context.resolveOwnership);
11878
12147
  try {
@@ -11884,6 +12153,24 @@ async function verifyCredential(credential, context) {
11884
12153
  if (!allowed) throw new ProtocolError("ROLE_ACCESS_DENIED", "trusted current ownership does not permit representation");
11885
12154
  result.representation_checked = true;
11886
12155
  }
12156
+ if (context.now === void 0) {
12157
+ const completedAt = Math.trunc(Date.now() / 1e3);
12158
+ for (const document2 of documents) validateCredentialAt(document2, completedAt, context.audience);
12159
+ await validateCredentialCapability(credential, { ns: context.ns, action: context.action, resource: context.resource, params: context.params, units: context.units, now: completedAt, client_ip: context.clientIp, proof_chain: context.proofChain }, () => true);
12160
+ validateRequestBinding(context.requestBinding, context.method, context.target, context.params ?? {}, String(value.cid), context.nonce ?? "", context.requestId ?? "", completedAt);
12161
+ for (const proof of statusProofs) {
12162
+ if (proof.asOf > completedAt + 300 || completedAt >= proof.proofExpiresAt) throw new ProtocolError("CREDENTIAL_STATUS_UNKNOWN", "trusted credential status proof is unavailable");
12163
+ }
12164
+ for (const material of resolvedCertificates) {
12165
+ const metadata = parseCertMetadata(certificatePem(material));
12166
+ if (completedAt < metadata.notBefore.getTime() / 1e3 || completedAt > metadata.notAfter.getTime() / 1e3) throw new ProtocolError("CREDENTIAL_SIGNATURE_INVALID", "certificate is not valid");
12167
+ }
12168
+ if (verifiedRepresentation) {
12169
+ const { proof, request } = verifiedRepresentation;
12170
+ if (completedAt < proof.iat || completedAt >= proof.exp) throw representationBindingInvalid("representation time window is invalid");
12171
+ validateRepresentationContext(proof, value, request, completedAt);
12172
+ }
12173
+ }
11887
12174
  return result;
11888
12175
  }
11889
12176
  function validateCredentialAt(credential, now, audience, proofChain) {
@@ -12270,7 +12557,7 @@ function orgRequestBindingParams(method, params2) {
12270
12557
  }
12271
12558
  function buildRequestBinding(method, target, params2, credentialCid, nonce, requestId, iat, exp) {
12272
12559
  if (![method, target, nonce, requestId].every((value) => typeof value === "string" && value.length > 0) || typeof credentialCid !== "string" || !credentialCid !== (method === "credential.revocationList")) throw invalid("request binding identity is invalid");
12273
- if (!isInt(iat) || !isInt(exp) || exp <= iat) throw invalid("request binding time window is invalid");
12560
+ if (!isInt(iat) || !isInt(exp) || exp <= iat || exp > iat + 300) throw invalid("request binding time window is invalid");
12274
12561
  return { request_id: requestId, nonce, target, method, params_hash: digest(orgRequestBindingParams(method, params2)), iat, exp, credential_cid: credentialCid };
12275
12562
  }
12276
12563
  function validateRequestBinding(binding, method, target, params2, credentialCid, nonce, requestId, now) {
@@ -12278,7 +12565,7 @@ function validateRequestBinding(binding, method, target, params2, credentialCid,
12278
12565
  if (!isRecord2(binding) || Object.keys(binding).length !== fields.length || Object.keys(binding).some((key) => !fields.includes(key))) throw new ProtocolError("CREDENTIAL_REQUEST_BINDING_INVALID", "request binding is invalid");
12279
12566
  if (![binding.request_id, binding.nonce, binding.target, binding.method, binding.params_hash].every((value) => typeof value === "string" && value.length > 0) || typeof binding.credential_cid !== "string" || !binding.credential_cid !== (method === "credential.revocationList")) throw new ProtocolError("CREDENTIAL_REQUEST_BINDING_INVALID", "request binding is invalid");
12280
12567
  if (binding.request_id !== requestId || binding.nonce !== nonce || binding.target !== target || binding.method !== method || binding.credential_cid !== credentialCid || binding.params_hash !== digest(orgRequestBindingParams(method, params2))) throw new ProtocolError("CREDENTIAL_REQUEST_BINDING_INVALID", "request binding does not match request");
12281
- if (!isInt(binding.iat) || !isInt(binding.exp) || binding.exp <= binding.iat || !isInt(now) || now < binding.iat || now >= binding.exp) throw new ProtocolError("CREDENTIAL_REQUEST_BINDING_INVALID", "request binding is outside its validity window");
12568
+ if (!isInt(binding.iat) || !isInt(binding.exp) || binding.exp <= binding.iat || binding.exp > binding.iat + 300 || !isInt(now) || now < binding.iat || now >= binding.exp) throw new ProtocolError("CREDENTIAL_REQUEST_BINDING_INVALID", "request binding is outside its validity window");
12282
12569
  }
12283
12570
  function representationBindingInvalid(message) {
12284
12571
  return new ProtocolError("CREDENTIAL_REQUEST_BINDING_INVALID", message);
@@ -13010,6 +13297,7 @@ var RPCTransport = class {
13010
13297
  __publicField(this, "_backgroundRpcQueue", []);
13011
13298
  __publicField(this, "_drainingRpcQueue", false);
13012
13299
  __publicField(this, "_onForegroundDrained");
13300
+ __publicField(this, "_onRpcResponse");
13013
13301
  __publicField(this, "_sendChain", Promise.resolve());
13014
13302
  // Gateway 在 RPC envelope 注入 _meta 字段(与 result 同级),由 client 层 observer 接收。
13015
13303
  // 注入失败 / 字段缺失时 observer 不会被调用,不影响业务路径。
@@ -13748,6 +14036,7 @@ var RPCTransport = class {
13748
14036
  this._pendingBackground.delete(rpcId);
13749
14037
  pending.resolve(message);
13750
14038
  this._drainRpcQueue();
14039
+ this._onRpcResponse?.();
13751
14040
  } else {
13752
14041
  withLogContext(this._log, { direction: "inbound", peerAid: "" }).warn("[aun_core.transport] recv unknown rpc response (maybe arrived after timeout): id=" + rpcId);
13753
14042
  }
@@ -15836,19 +16125,68 @@ function parseCertDer(pem) {
15836
16125
  const spkiBytes = der.slice(spkiFullStart, spkiFullEnd);
15837
16126
  pos = spkiFullEnd;
15838
16127
  let isCA = false;
15839
- const extensionsSearchStart = pos;
15840
- const extensionsSearchEnd = tbsEnd;
15841
- const bcOidBytes = new Uint8Array([85, 29, 19]);
15842
- for (let i = extensionsSearchStart; i < extensionsSearchEnd - 3; i++) {
15843
- if (der[i] === bcOidBytes[0] && der[i + 1] === bcOidBytes[1] && der[i + 2] === bcOidBytes[2]) {
15844
- const searchEnd = Math.min(i + 20, extensionsSearchEnd);
15845
- for (let j = i + 3; j < searchEnd - 2; j++) {
15846
- if (der[j] === 1 && der[j + 1] === 1 && der[j + 2] === 255) {
16128
+ let pathLenConstraint;
16129
+ let keyCertSign;
16130
+ const extensionTlv = (offset, end, tag) => {
16131
+ if (offset + 2 > end || der[offset + 1] === 128 || der[offset + 1] > 132) throw new AuthError("invalid certificate extension DER");
16132
+ const value = readTlv(der, offset);
16133
+ if (value[1] + value[2] > end || value[2] < 0 || tag !== void 0 && value[0] !== tag) throw new AuthError("invalid certificate extension DER");
16134
+ return value;
16135
+ };
16136
+ const seenExtensions = /* @__PURE__ */ new Set();
16137
+ while (pos < tbsEnd) {
16138
+ const [tag, start, length] = extensionTlv(pos, tbsEnd);
16139
+ pos = start + length;
16140
+ if (tag === 129 || tag === 130) continue;
16141
+ if (tag !== 163) throw new AuthError("invalid certificate extensions");
16142
+ const [, sequenceStart, sequenceLength] = extensionTlv(start, pos, 48);
16143
+ if (sequenceStart + sequenceLength !== pos) throw new AuthError("invalid certificate extensions");
16144
+ let extensionPos = sequenceStart;
16145
+ while (extensionPos < pos) {
16146
+ const [, extensionStart, extensionLength] = extensionTlv(extensionPos, pos, 48);
16147
+ const extensionEnd = extensionStart + extensionLength;
16148
+ const [, oidStart, oidLength] = extensionTlv(extensionStart, extensionEnd, 6);
16149
+ const oid = parseOid(der.slice(oidStart, oidStart + oidLength));
16150
+ if (seenExtensions.has(oid)) throw new AuthError("duplicate certificate extension");
16151
+ seenExtensions.add(oid);
16152
+ let valuePos = oidStart + oidLength;
16153
+ let critical = false;
16154
+ if (der[valuePos] === 1) {
16155
+ const [, boolStart, boolLength] = extensionTlv(valuePos, extensionEnd, 1);
16156
+ if (boolLength !== 1 || der[boolStart] !== 255) throw new AuthError("invalid certificate extension critical flag");
16157
+ critical = true;
16158
+ valuePos = boolStart + boolLength;
16159
+ }
16160
+ const [, valueStart, valueLength] = extensionTlv(valuePos, extensionEnd, 4);
16161
+ if (valueStart + valueLength !== extensionEnd) throw new AuthError("invalid certificate extension value");
16162
+ if (oid === "2.5.29.19") {
16163
+ const [, constraintStart, constraintLength] = extensionTlv(valueStart, extensionEnd, 48);
16164
+ const constraintEnd = constraintStart + constraintLength;
16165
+ if (constraintEnd !== extensionEnd) throw new AuthError("invalid BasicConstraints");
16166
+ let constraintPos = constraintStart;
16167
+ if (der[constraintPos] === 1) {
16168
+ const [, boolStart, boolLength] = extensionTlv(constraintPos, constraintEnd, 1);
16169
+ if (boolLength !== 1 || der[boolStart] !== 255) throw new AuthError("invalid BasicConstraints CA flag");
15847
16170
  isCA = true;
15848
- break;
16171
+ constraintPos = boolStart + boolLength;
16172
+ }
16173
+ if (constraintPos < constraintEnd) {
16174
+ const [, integerStart, integerLength] = extensionTlv(constraintPos, constraintEnd, 2);
16175
+ if (!isCA || !integerLength || der[integerStart] & 128 || integerLength > 1 && der[integerStart] === 0 && !(der[integerStart + 1] & 128)) throw new AuthError("invalid BasicConstraints path length");
16176
+ pathLenConstraint = 0;
16177
+ for (let index = integerStart; index < integerStart + integerLength; index++) pathLenConstraint = pathLenConstraint * 256 + der[index];
16178
+ if (!Number.isSafeInteger(pathLenConstraint)) throw new AuthError("invalid BasicConstraints path length");
16179
+ constraintPos = integerStart + integerLength;
15849
16180
  }
16181
+ if (constraintPos !== constraintEnd) throw new AuthError("invalid BasicConstraints");
16182
+ } else if (oid === "2.5.29.15") {
16183
+ const [, bitsStart, bitsLength] = extensionTlv(valueStart, extensionEnd, 3);
16184
+ if (bitsStart + bitsLength !== extensionEnd || bitsLength < 2 || der[bitsStart] > 7 || der[bitsStart + bitsLength - 1] & (1 << der[bitsStart]) - 1) throw new AuthError("invalid KeyUsage");
16185
+ keyCertSign = Boolean(der[bitsStart + 1] & 4);
16186
+ } else if (critical) {
16187
+ throw new AuthError(`unsupported critical certificate extension: ${oid}`);
15850
16188
  }
15851
- break;
16189
+ extensionPos = extensionEnd;
15852
16190
  }
15853
16191
  }
15854
16192
  const sigAlg2Pos = tbsEnd;
@@ -15868,6 +16206,8 @@ function parseCertDer(pem) {
15868
16206
  notBefore,
15869
16207
  notAfter,
15870
16208
  isCA,
16209
+ pathLenConstraint,
16210
+ keyCertSign,
15871
16211
  spkiBytes
15872
16212
  };
15873
16213
  }
@@ -16448,7 +16788,7 @@ var _AuthFlow = class _AuthFlow {
16448
16788
  clearTimeout(timeout);
16449
16789
  }
16450
16790
  }
16451
- async verifyPeerCertificate(gatewayUrl, certPem, expectedAid, signedAt, strictRevocation = false) {
16791
+ async verifyPeerCertificate(gatewayUrl, certPem, expectedAid, signedAt, strictRevocation = false, agentMdRead = false) {
16452
16792
  const tStart = Date.now();
16453
16793
  this._log.debug(`verifyPeerCertificate enter: aid=${expectedAid} gateway=${gatewayUrl}`);
16454
16794
  try {
@@ -16456,7 +16796,7 @@ var _AuthFlow = class _AuthFlow {
16456
16796
  if (signedAt !== void 0) {
16457
16797
  ensureCertTimeValid(cert, "peer certificate", signedAt);
16458
16798
  await this._verifyAuthCertChain(gatewayUrl, cert, expectedAid, signedAt);
16459
- if (cert.subjectCN !== expectedAid) throw new AuthError(`peer cert CN mismatch: expected ${expectedAid}`);
16799
+ if (agentMdRead ? cert.subjectCN.trim().toLowerCase() !== expectedAid.trim().toLowerCase() : cert.subjectCN !== expectedAid) throw new AuthError(`peer cert CN mismatch: expected ${expectedAid}`);
16460
16800
  return;
16461
16801
  }
16462
16802
  const now = Date.now() / 1e3;
@@ -16863,7 +17203,7 @@ var _AuthFlow = class _AuthFlow {
16863
17203
  if (!fresh && this._gatewayCaVerified.get(cacheKey)) {
16864
17204
  const issuer = chain[0];
16865
17205
  ensureCertTimeValid(issuer, "Issuer CA", now);
16866
- if (!issuer.isCA) throw new AuthError("Issuer CA is not marked as CA (fast path)");
17206
+ if (!issuer.isCA || issuer.keyCertSign === false) throw new AuthError("Issuer CA is not marked as CA (fast path)");
16867
17207
  if (!arraysEqual(authCert.issuerRaw, issuer.subjectRaw)) {
16868
17208
  throw new AuthError("auth certificate issuer mismatch");
16869
17209
  }
@@ -16878,9 +17218,11 @@ var _AuthFlow = class _AuthFlow {
16878
17218
  if (!arraysEqual(current.issuerRaw, caCert.subjectRaw)) {
16879
17219
  throw new AuthError(`auth certificate issuer mismatch at chain level ${i}`);
16880
17220
  }
16881
- if (!caCert.isCA) {
17221
+ if (!caCert.isCA || caCert.keyCertSign === false) {
16882
17222
  throw new AuthError(`CA certificate[${i}] is not marked as CA`);
16883
17223
  }
17224
+ const subordinateCas = chain.slice(0, i).filter((cert) => cert.isCA).length;
17225
+ if (caCert.pathLenConstraint !== void 0 && subordinateCas > caCert.pathLenConstraint) throw new AuthError(`CA certificate[${i}] path length constraint exceeded`);
16884
17226
  await verifyCertSignature(caCert.spkiBytes, current);
16885
17227
  current = caCert;
16886
17228
  }
@@ -17413,7 +17755,7 @@ function parseOwnershipMessage(content) {
17413
17755
  ownershipAid(message.sub_aid, "sub_aid");
17414
17756
  ownershipAid(message.owner_aid, "owner_aid");
17415
17757
  ownershipInteger(message.base_relation_version, "base_relation_version", 0);
17416
- if (ownershipInteger(message.target_relation_version, "target_relation_version") !== message.base_relation_version + 1) throw new ValidationError("ownership target version must be base plus one");
17758
+ if (ownershipInteger(message.target_relation_version, "target_relation_version") !== message.base_relation_version + (message.operation === "release" ? 0 : 1)) throw new ValidationError("ownership target version does not match operation");
17417
17759
  if (message.operation === "transfer") {
17418
17760
  if (ownershipAid(message.new_owner_aid, "new_owner_aid") === message.owner_aid) throw new ValidationError("transfer target must differ from current owner");
17419
17761
  } else if ("new_owner_aid" in message) throw new ValidationError("new_owner_aid is only allowed for transfer");
@@ -17470,7 +17812,8 @@ function validateOwnershipMessage(message, context) {
17470
17812
  if (ns.protocol !== "2.1") throw failure("unsupported_protocol", "NS ownership protocol is unavailable");
17471
17813
  const base = ns.relation_version === message.base_relation_version && (message.operation === "bind" ? ns.lock === "unlocked" && !ns.owner_aid : ns.lock === "locked" && ns.owner_aid === message.owner_aid);
17472
17814
  const target = ns.relation_version === message.target_relation_version && (message.operation === "release" ? ns.lock === "unlocked" && !ns.owner_aid : ns.lock === "locked" && ns.owner_aid === targetOwner(message));
17473
- if (message.action !== "operation.stopped" && (["relation.confirmed", "sub.cleaned"].includes(message.action) ? !target : !base && !target)) throw failure("conflict", "ownership current NS relation does not match this task");
17815
+ const cleaned = message.action === "sub.cleaned" && Boolean(task) && ns.relation_version === 0 && ns.lock === "unlocked" && !ns.owner_aid;
17816
+ if (message.action !== "operation.stopped" && (["relation.confirmed", "sub.cleaned"].includes(message.action) ? !target && !cleaned : !base && !target)) throw failure("conflict", "ownership current NS relation does not match this task");
17474
17817
  }
17475
17818
  }
17476
17819
  function ownershipTaskResult(task) {
@@ -17531,12 +17874,30 @@ var OwnershipWorkflow = class {
17531
17874
  const task = snapshot.record;
17532
17875
  const localAid = this.deps.currentAid();
17533
17876
  try {
17877
+ if (!Number.isSafeInteger(snapshot.revision) || snapshot.revision < 1) throw new Error("invalid persisted task revision");
17534
17878
  if (task.schema_version !== 1 || !["pending", "completed", "rejected", "stopped", "error"].includes(task.status) || !["pending", "accept", "reject"].includes(task.local_decision) || typeof task.step !== "string") throw new Error("invalid task state");
17535
17879
  parseOwnershipMessage({ ...this.message(task, task.operation === "bind" ? "bind.request" : task.operation === "transfer" ? "transfer.request" : "relation.confirmed") });
17536
- if (!participants(task).includes(task.initiator_aid) || JSON.stringify(task.local_roles) !== JSON.stringify(ownershipLocalRoles(task, localAid)) || !task.local_roles.length) throw new Error("invalid local task identity");
17880
+ if (!participants(task).includes(task.initiator_aid) || task.operation !== "bind" && task.initiator_aid !== task.owner_aid || JSON.stringify(task.local_roles) !== JSON.stringify(ownershipLocalRoles(task, localAid)) || !task.local_roles.length) throw new Error("invalid local task identity");
17537
17881
  if (!task.approvals || typeof task.approvals !== "object" || Array.isArray(task.approvals) || Object.entries(task.approvals).some(([aid2, decision]) => !participants(task).includes(aid2) || !["pending", "accept", "reject"].includes(decision))) throw new Error("invalid task approvals");
17538
17882
  if (JSON.stringify(Object.keys(task.approvals).sort()) !== JSON.stringify(participants(task).sort()) || task.approvals[task.initiator_aid] !== "accept") throw new Error("incomplete task approvals or initiator decision");
17539
17883
  if (!task.files || typeof task.files !== "object" || Array.isArray(task.files) || !Array.isArray(task.inbox) || !Array.isArray(task.received) || !Array.isArray(task.outbox) || !Array.isArray(task.sent) || !task.hints || typeof task.hints !== "object" || typeof task.request_validated !== "boolean") throw new Error("invalid task journals");
17884
+ const base = task.base_agent_md;
17885
+ if (!base && (task.request_validated || Object.keys(task.files).length || task.local_decision !== "pending")) throw new Error("missing original agent.md context");
17886
+ if (base) {
17887
+ if (typeof base.exists !== "boolean" || typeof base.raw !== "string" || typeof base.etag !== "string" || base.exists && !base.etag || base.sha256 !== await ownershipSha256(base.raw)) throw new Error("invalid original agent.md bytes or ETag");
17888
+ const statement = base.exists ? parseOwnershipFile(base.raw, "agent", { expectedSubAid: task.sub_aid }).statement : void 0;
17889
+ if ((statement?.relation_version ?? 0) !== task.base_relation_version || statement && task.operation !== "bind" && statement.owner_aid !== task.owner_aid || (statement ? !this.sameStatement(statement, base.statement ?? void 0) : base.statement !== null)) throw new Error("invalid original agent.md statement");
17890
+ }
17891
+ if (task.base_owner_md) {
17892
+ const prior = task.base_owner_md;
17893
+ if (typeof prior.exists !== "boolean" || typeof prior.raw !== "string" || prior.sha256 !== (prior.exists ? await ownershipSha256(prior.raw) : "") || !prior.exists && prior.raw !== "") throw new Error("invalid original owner.md context");
17894
+ if (base?.statement && !prior.exists && (task.operation === "transfer" || task.operation === "release" && task.local_roles.includes("owner"))) throw new Error("missing original owner.md relation");
17895
+ if (prior.exists) {
17896
+ const file = parseOwnershipFile(prior.raw, "owner", { expectedSubAid: task.sub_aid });
17897
+ if (!file.statement || file.statement.owner_aid !== base?.statement?.owner_aid || file.statement.relation_version !== base?.statement?.relation_version) throw new Error("invalid original owner.md statement");
17898
+ }
17899
+ }
17900
+ if (base?.statement && !task.base_owner_md && !(task.operation === "release" && task.local_roles.includes("sub") && !task.local_roles.includes("owner") && task.cleanup_authorization_operation_id && !task.files.cleanup)) throw new Error("missing original owner.md context");
17540
17901
  if (task.approvals[localAid] !== task.local_decision) throw new Error("task decision and approval differ");
17541
17902
  if (task.status === "completed" && (task.verified_relation_version !== task.target_relation_version || task.operation !== "release" && task.local_decision !== "accept")) throw new Error("invalid completion evidence or local approval");
17542
17903
  const transferEvidenceRequired = task.operation === "transfer" && task.local_roles.some((role) => role === "sub" || role === "new_owner") && (task.status === "completed" || this.ongoing(task) && (Boolean(task.files.owner || task.files.agent || task.hints["transfer.ready"]) || task.authorization_sha256 !== void 0));
@@ -17546,7 +17907,9 @@ var OwnershipWorkflow = class {
17546
17907
  const agentFile = key === "agent" || key === "cleanup";
17547
17908
  if (!["agent", "owner", "release", "transfer", "cleanup"].includes(key) || !prepared || typeof prepared.raw !== "string" || !/^[0-9a-f]{64}$/.test(prepared.sha256) || await ownershipSha256(prepared.raw) !== prepared.sha256 || !prepared.condition || typeof prepared.condition !== "object" || Array.isArray(prepared.condition)) throw new Error("invalid prepared file bytes");
17548
17909
  const condition = prepared.condition;
17549
- if (Object.keys(condition).length !== 1 || (agentFile ? !(typeof condition.if_match === "string" && condition.if_match || condition.if_none_match === "*") : typeof condition.expected_revision !== "string" || !condition.expected_revision)) throw new Error("invalid prepared file condition");
17910
+ if (agentFile ? Object.keys(condition).length !== 1 || !(typeof condition.if_match === "string" && condition.if_match || condition.if_none_match === "*") : Object.keys(condition).length !== 0) throw new Error("invalid prepared file condition");
17911
+ const original = prepared.original_owner;
17912
+ if (!agentFile && (!original || typeof original.exists !== "boolean" || typeof original.raw !== "string" || original.sha256 !== (original.exists ? await ownershipSha256(original.raw) : "") || !original.exists && original.raw !== "")) throw new Error("invalid prepared owner original");
17550
17913
  if (key === "cleanup" && task.operation !== "release" || key === "release" && task.operation !== "release" || key === "transfer" && task.operation !== "transfer" || ["agent", "owner"].includes(key) && task.operation === "release") throw new Error("invalid prepared file operation");
17551
17914
  const ownerAid = key === "owner" || key === "agent" ? targetOwner(task) : task.owner_aid;
17552
17915
  const file = parseOwnershipFile(prepared.raw, agentFile ? "agent" : "owner", { expectedSubAid: task.sub_aid, expectedOwnerAid: key === "cleanup" ? void 0 : ownerAid });
@@ -17554,7 +17917,7 @@ var OwnershipWorkflow = class {
17554
17917
  if (key === "cleanup" ? Boolean(file.statement) : file.statement?.relation_version !== (key === "owner" || key === "agent" ? task.target_relation_version : task.base_relation_version)) throw new Error("invalid prepared file generation");
17555
17918
  const fileExpiry = prepared.expires_at;
17556
17919
  if (!agentFile && file.action !== (key === "owner" ? "declare" : key === "release" ? "release" : "transfer_owner") || key === "transfer" && (typeof fileExpiry !== "number" || !Number.isSafeInteger(fileExpiry) || fileExpiry <= 0 || fileExpiry !== task.expires_at || file.transfer?.new_owner_aid !== task.new_owner_aid || file.transfer?.target_relation_version !== task.target_relation_version || file.transfer?.expires_at !== fileExpiry) || key !== "transfer" && fileExpiry !== void 0) throw new Error("invalid prepared file authorization");
17557
- if (prepared.published_revision !== void 0 && (agentFile || typeof prepared.published_revision !== "string" || !prepared.published_revision) || prepared.published_etag !== void 0 && (!agentFile || typeof prepared.published_etag !== "string" || !prepared.published_etag) || prepared.cleaned !== void 0 && typeof prepared.cleaned !== "boolean") throw new Error("invalid prepared file result");
17920
+ if (prepared.published_etag !== void 0 && (!agentFile || typeof prepared.published_etag !== "string" || !prepared.published_etag) || prepared.cleaned !== void 0 && typeof prepared.cleaned !== "boolean") throw new Error("invalid prepared file result");
17558
17921
  }
17559
17922
  if (task.release_cleanup !== void 0) {
17560
17923
  const cleanup = task.release_cleanup;
@@ -17565,14 +17928,21 @@ var OwnershipWorkflow = class {
17565
17928
  }
17566
17929
  if (task.status === "completed") {
17567
17930
  const consensus = task.verified_consensus;
17568
- if (!consensus || consensus.protocol !== "2.1" || consensus.sub_aid !== task.sub_aid || consensus.relation_version !== task.target_relation_version || typeof consensus.agent_md_etag !== "string" || !consensus.agent_md_etag || !/^[0-9a-f]{64}$/.test(consensus.agent_md_sha256)) throw new Error("invalid persisted consensus evidence");
17931
+ const cleaned = task.operation === "release" && consensus?.status === "unbound" && consensus.relation_version === 0 && Boolean(await this.cleanupEvidence(task, consensus));
17932
+ if (!consensus || consensus.protocol !== "2.1" || consensus.sub_aid !== task.sub_aid || !cleaned && consensus.relation_version !== task.target_relation_version || typeof consensus.agent_md_etag !== "string" || !consensus.agent_md_etag || !/^[0-9a-f]{64}$/.test(consensus.agent_md_sha256)) throw new Error("invalid persisted consensus evidence");
17569
17933
  if (task.operation === "release") {
17570
- if (consensus.status !== "released" || consensus.lock !== "unlocked" || consensus.owner_aid !== null || task.local_roles.includes("owner") && !task.files.release) throw new Error("invalid persisted release evidence");
17934
+ if (!cleaned && consensus.status !== "released" || consensus.lock !== "unlocked" || consensus.owner_aid !== null || task.local_roles.includes("owner") && !task.files.release) throw new Error("invalid persisted release evidence");
17571
17935
  } else {
17572
- if (consensus.status !== "consensus" || consensus.lock !== "locked" || consensus.owner_aid !== targetOwner(task) || typeof consensus.owner_md_revision !== "string" || !consensus.owner_md_revision || !/^[0-9a-f]{64}$/.test(consensus.owner_md_sha256)) throw new Error("invalid persisted binding evidence");
17936
+ if (consensus.status !== "consensus" || consensus.lock !== "locked" || consensus.owner_aid !== targetOwner(task) || !/^[0-9a-f]{64}$/.test(consensus.owner_md_sha256)) throw new Error("invalid persisted binding evidence");
17573
17937
  if (task.local_roles.includes("sub") && task.files.agent?.sha256 !== consensus.agent_md_sha256 || task.local_roles.includes(task.operation === "transfer" ? "new_owner" : "owner") && task.files.owner?.sha256 !== consensus.owner_md_sha256 || task.operation === "transfer" && task.local_roles.includes("owner") && !task.files.transfer) throw new Error("persisted consensus differs from local original files");
17574
17938
  }
17575
17939
  }
17940
+ for (const key of task.received) {
17941
+ if (typeof key !== "string" || key.split("|").length !== 2) throw new Error("invalid received journal");
17942
+ const [fromAid, action] = key.split("|");
17943
+ const message = parseOwnershipMessage(this.message(task, action));
17944
+ validateOwnershipMessage(message, { authenticated_from_aid: fromAid, local_aid: localAid, task });
17945
+ }
17576
17946
  for (const item of task.inbox) {
17577
17947
  parseOwnershipMessage(item.payload);
17578
17948
  validateOwnershipMessage(item.payload, { authenticated_from_aid: item.from_aid, local_aid: localAid, task });
@@ -17580,6 +17950,9 @@ var OwnershipWorkflow = class {
17580
17950
  }
17581
17951
  for (const item of task.outbox) {
17582
17952
  parseOwnershipMessage(item.payload);
17953
+ const action = item.payload.action;
17954
+ if (["bind.accept", "transfer.accept", "owner.ready", "transfer.ready"].includes(action) && task.local_decision !== "accept" || ["bind.reject", "transfer.reject"].includes(action) && task.local_decision !== "reject") throw new Error("invalid outbox local approval");
17955
+ if (["bind.accept", "transfer.accept", "bind.reject", "transfer.reject"].includes(action) && !task.request_validated) throw new Error("invalid outbox received request");
17583
17956
  validateOwnershipMessage(item.payload, { authenticated_from_aid: localAid, local_aid: item.to_aid, task });
17584
17957
  if (item.key !== `${item.to_aid}|${item.payload.action}` || typeof item.message_id !== "string" || !/^m-[0-9a-f]{32}$/.test(item.message_id)) throw new Error("invalid outbox target");
17585
17958
  }
@@ -17705,7 +18078,7 @@ var OwnershipWorkflow = class {
17705
18078
  const saved = await this.load(operationId, aid2);
17706
18079
  if (!saved && !initial) throw failure("operation_not_found", "ownership operation was not found");
17707
18080
  if (!saved && initial) {
17708
- const existing = (await this.records(aid2)).find((task) => task.sub_aid === subAid && this.ongoing(task));
18081
+ const existing = (await this.records(aid2, subAid)).find((task) => task.sub_aid === subAid && this.ongoing(task));
17709
18082
  if (existing) {
17710
18083
  if (!this.sameIntent(existing, initial) || existing.base_relation_version !== initial.base_relation_version || existing.target_relation_version !== initial.target_relation_version) throw failure("conflict", "another local operation for this sub is pending");
17711
18084
  try {
@@ -17741,42 +18114,55 @@ var OwnershipWorkflow = class {
17741
18114
  sameIntent(task, intent) {
17742
18115
  return task.initiator_aid === intent.initiator_aid && task.operation === intent.operation && task.sub_aid === intent.sub_aid && task.owner_aid === intent.owner_aid && task.new_owner_aid === intent.new_owner_aid;
17743
18116
  }
17744
- async records(aid2) {
18117
+ async records(aid2, subAid) {
17745
18118
  let snapshots;
17746
18119
  try {
17747
18120
  snapshots = await this.storage().listOwnershipTasks(aid2);
17748
18121
  } catch (error) {
17749
18122
  throw failure("persistence_unavailable", `ownership task list failed: ${String(error)}`);
17750
18123
  }
17751
- return await Promise.all(snapshots.map((snapshot) => this.record(snapshot)));
18124
+ return await Promise.all(snapshots.filter((snapshot) => !subAid || typeof snapshot.record.sub_aid !== "string" || snapshot.record.sub_aid === subAid).map((snapshot) => this.record(snapshot)));
17752
18125
  }
17753
18126
  async observe(subAid) {
17754
18127
  this.requireApplicable(subAid);
17755
18128
  const snapshot = await this.deps.files.readAgent(subAid);
17756
- const file = snapshot.exists ? snapshot.lock === "unlocked" && snapshot.relation_version > 0 ? parseOwnershipFile(snapshot.content, "agent", { expectedSubAid: subAid }) : await this.deps.files.verify(snapshot.content, "agent", subAid) : null;
18129
+ const file = snapshot.exists ? await this.deps.files.verify(snapshot.content, "agent", subAid) : null;
17757
18130
  if (file) requireOwnershipApplicable(file.frontmatter.type);
17758
- if (snapshot.lock === "unlocked" && snapshot.relation_version === 0 && file?.statement) throw failure("unavailable", "NS relation metadata does not match agent.md", true);
18131
+ if ((file?.statement?.relation_version ?? 0) !== snapshot.relation_version) throw failure("unavailable", "NS relation metadata does not match agent.md", true);
17759
18132
  if (snapshot.lock === "locked" && (!file?.statement || file.statement.relation_version !== snapshot.relation_version || snapshot.owner_aid !== void 0 && snapshot.owner_aid !== file.statement.owner_aid)) throw failure("unavailable", "NS lock and current signed agent.md differ", true);
17760
18133
  return { ...snapshot, file, owner_aid: snapshot.lock === "locked" ? file.statement.owner_aid : null };
17761
18134
  }
17762
18135
  isBase(task, ns) {
17763
- return ns.relation_version === task.base_relation_version && (task.operation === "bind" ? ns.lock === "unlocked" : ns.lock === "locked" && ns.owner_aid === task.owner_aid);
18136
+ const base = task.base_agent_md;
18137
+ return ns.relation_version === task.base_relation_version && (task.operation === "bind" ? ns.lock === "unlocked" : ns.lock === "locked" && ns.owner_aid === task.owner_aid) && (!base || base.exists === ns.exists && (base.statement ? this.sameStatement(base.statement, ns.file?.statement) : !ns.file?.statement && base.raw === ns.content && base.etag === ns.etag));
18138
+ }
18139
+ async captureBase(task, ns) {
18140
+ if (!task.base_agent_md) task.base_agent_md = { exists: ns.exists, raw: ns.content, sha256: await ownershipSha256(ns.content), etag: ns.etag, statement: ns.file?.statement ?? null };
18141
+ if (!task.base_owner_md) {
18142
+ const prior = ns.file?.statement ? await this.deps.files.readOwner(ns.file.statement.owner_aid, task.sub_aid) : null;
18143
+ task.base_owner_md = prior ? { exists: prior.exists, raw: prior.content, sha256: prior.sha256 } : { exists: false, raw: "", sha256: "" };
18144
+ }
17764
18145
  }
17765
18146
  isTarget(task, ns) {
17766
18147
  return ns.relation_version === task.target_relation_version && (task.operation === "release" ? ns.lock === "unlocked" : ns.lock === "locked" && ns.owner_aid === targetOwner(task));
17767
18148
  }
17768
- fileTask(context) {
18149
+ fileTask(context, expectedStatement) {
17769
18150
  const task = {
17770
18151
  ...context.task,
17771
18152
  files: context.task.files,
18153
+ expected_agent_statement: expectedStatement,
17772
18154
  persist: async () => {
17773
18155
  context.task.expires_at = task.expires_at;
17774
18156
  context.task.authorization_sha256 = task.authorization_sha256;
18157
+ context.task.base_agent_md = task.base_agent_md;
18158
+ context.task.base_owner_md = task.base_owner_md;
17775
18159
  await this.save(context);
17776
18160
  },
17777
18161
  guard: async () => {
17778
18162
  context.task.expires_at = task.expires_at;
17779
18163
  context.task.authorization_sha256 = task.authorization_sha256;
18164
+ context.task.base_agent_md = task.base_agent_md;
18165
+ context.task.base_owner_md = task.base_owner_md;
17780
18166
  await this.save(context);
17781
18167
  }
17782
18168
  };
@@ -17808,7 +18194,7 @@ var OwnershipWorkflow = class {
17808
18194
  }
17809
18195
  }
17810
18196
  const intent = { initiator_aid: aid2, operation, sub_aid: subAid, owner_aid: ownerAid, new_owner_aid: newOwnerAid };
17811
- for (const pending of (await this.records(aid2)).filter((task2) => task2.sub_aid === subAid && this.ongoing(task2))) {
18197
+ for (const pending of (await this.records(aid2, subAid)).filter((task2) => task2.sub_aid === subAid && this.ongoing(task2))) {
17812
18198
  if (this.sameIntent(pending, intent)) {
17813
18199
  const result = await this.advance(subAid, pending.operation_id, void 0, events, void 0, guard);
17814
18200
  if (result.status !== "stopped" || result.code !== "conflict" || !/relation version or target was replaced/.test(result.reason ?? "")) return result;
@@ -17824,15 +18210,17 @@ var OwnershipWorkflow = class {
17824
18210
  if (!ns.exists) throw failure("unavailable", "initial agent.md is not published", true);
17825
18211
  if (ns.lock === "locked") throw failure("owner_locked", "the sub AID has a current owner lock");
17826
18212
  if (ns.file?.frontmatter.type === "human") throw failure("invalid", "human cannot declare an owner");
18213
+ if (ns.file?.statement?.owner_aid === ownerAid) throw failure("conflict", "current owner release requires sub cleanup before binding again");
17827
18214
  } else {
17828
18215
  if (aid2 !== ownerAid || ns.lock !== "locked" || ns.owner_aid !== aid2) throw failure("forbidden", "only the current original owner may release or transfer");
17829
18216
  const consensus = await this.deps.files.checkConsensus(subAid);
17830
18217
  if (consensus.status !== "consensus" || consensus.owner_aid !== aid2 || consensus.relation_version !== ns.relation_version) throw failure(consensus.status === "invalid" ? "invalid" : "unavailable", "the current owner relation could not be verified", consensus.status !== "invalid");
17831
18218
  }
17832
- ownershipInteger(ns.relation_version + 1, "target_relation_version");
18219
+ const targetVersion = ownershipInteger(ns.relation_version + (operation === "release" ? 0 : 1), "target_relation_version");
17833
18220
  const operationId = requestedId ?? crypto.randomUUID();
17834
- const payload = { type: OWNERSHIP_SDK_MESSAGE_TYPE, operation_id: operationId, operation, action: operation === "bind" ? "bind.request" : operation === "transfer" ? "transfer.request" : "relation.confirmed", sub_aid: subAid, owner_aid: ownerAid, ...newOwnerAid ? { new_owner_aid: newOwnerAid } : {}, base_relation_version: ns.relation_version, target_relation_version: ns.relation_version + 1 };
18221
+ const payload = { type: OWNERSHIP_SDK_MESSAGE_TYPE, operation_id: operationId, operation, action: operation === "bind" ? "bind.request" : operation === "transfer" ? "transfer.request" : "relation.confirmed", sub_aid: subAid, owner_aid: ownerAid, ...newOwnerAid ? { new_owner_aid: newOwnerAid } : {}, base_relation_version: ns.relation_version, target_relation_version: targetVersion };
17835
18222
  const task = this.create(payload, aid2, "accept");
18223
+ await this.captureBase(task, ns);
17836
18224
  if (!task.local_roles.length) throw failure("forbidden", "the local AID is not an ownership participant");
17837
18225
  if (aid2 !== this.deps.currentAid()) throw failure("forbidden", "ownership current identity changed");
17838
18226
  return await this.advance(subAid, operationId, task, events, async (context) => {
@@ -17953,11 +18341,23 @@ var OwnershipWorkflow = class {
17953
18341
  return true;
17954
18342
  }
17955
18343
  async confirmSubCleaned(task) {
17956
- const began = this.now(), before = await this.deps.files.readAgent(task.sub_aid), after = await this.deps.files.readAgent(task.sub_aid);
17957
- if (this.now() - began > 30 || before.exists !== after.exists || before.etag !== after.etag || before.relation_version !== after.relation_version || before.lock !== after.lock || before.owner_aid !== after.owner_aid || await ownershipSha256(before.content) !== await ownershipSha256(after.content)) throw failure("unavailable", "NS changed during sub cleanup verification", true);
17958
- if (!before.exists || before.relation_version !== task.target_relation_version || before.lock !== "unlocked" || before.owner_aid || !before.etag) return false;
18344
+ const began = this.now(), before = await this.deps.files.readAgent(task.sub_aid);
18345
+ if (!before.exists || before.relation_version !== 0 || before.lock !== "unlocked" || before.owner_aid || !before.etag) return false;
17959
18346
  const file = parseOwnershipFile(before.content, "agent", { expectedSubAid: task.sub_aid });
17960
- return !Object.hasOwn(file.frontmatter, "owner") && !Object.hasOwn(file.frontmatter, "ownership");
18347
+ if (Object.hasOwn(file.frontmatter, "owner") || Object.hasOwn(file.frontmatter, "ownership")) return false;
18348
+ if (!task.base_agent_md?.statement || task.base_agent_md.statement.owner_aid !== task.owner_aid || task.base_agent_md.statement.sub_aid !== task.sub_aid || task.base_agent_md.statement.relation_version !== task.base_relation_version) return false;
18349
+ let ownerHash = "";
18350
+ if (task.local_roles.includes("owner") && task.files.release) {
18351
+ const original = task.files.release, object = await this.deps.files.readOwner(task.owner_aid, task.sub_aid);
18352
+ if (object.exists && object.content === original.raw && object.sha256 === original.sha256) {
18353
+ const file2 = await this.deps.files.verify(object.content, "owner", task.sub_aid, task.owner_aid);
18354
+ if (file2.action === "release" && file2.statement && ["version", "owner_aid", "sub_aid", "relation_version"].every((key) => file2.statement[key] === task.base_agent_md.statement[key])) ownerHash = object.sha256;
18355
+ }
18356
+ }
18357
+ const confirmed = Boolean(await this.cleanupEvidence(task, { agent_md_etag: before.etag, agent_md_sha256: await ownershipSha256(before.content), owner_md_sha256: ownerHash }, before.content));
18358
+ const after = await this.deps.files.readAgent(task.sub_aid);
18359
+ if (this.now() - began > 30 || before.exists !== after.exists || before.etag !== after.etag || before.relation_version !== after.relation_version || before.lock !== after.lock || before.owner_aid !== after.owner_aid || before.content !== after.content) throw failure("unavailable", "NS changed during sub cleanup verification", true);
18360
+ return confirmed;
17961
18361
  }
17962
18362
  async resume(guard) {
17963
18363
  guard?.();
@@ -17969,12 +18369,13 @@ var OwnershipWorkflow = class {
17969
18369
  }
17970
18370
  for (const snapshot of snapshots) {
17971
18371
  guard?.();
17972
- const task = await this.record(snapshot);
17973
- if (["rejected", "stopped"].includes(task.status) && !task.outbox.length && !task.inbox.length || task.status === "completed" && !task.cleanup_pending && !task.outbox.length && !task.inbox.length && !(["bind", "transfer"].includes(task.operation) && task.local_roles.includes("sub") && task.local_decision === "accept" && !task.release_cleanup?.cleaned)) continue;
17974
18372
  try {
18373
+ const task = await this.record(snapshot);
18374
+ if (["rejected", "stopped"].includes(task.status) && !task.outbox.length && !task.inbox.length || task.status === "completed" && !task.cleanup_pending && !task.outbox.length && !task.inbox.length && !(["bind", "transfer"].includes(task.operation) && task.local_roles.includes("sub") && task.local_decision === "accept" && !task.release_cleanup?.cleaned)) continue;
17975
18375
  await this.execute(task.sub_aid, task.operation_id, void 0, void 0, guard);
17976
18376
  } catch (error) {
17977
- if (ownershipErrorCode(error) !== "conflict") throw error;
18377
+ guard?.();
18378
+ if (ownershipErrorCode(error) !== "conflict") await this.deps.emit("ownership.error", { operation_id: snapshot.record.operation_id, sub_aid: snapshot.record.sub_aid, code: ownershipErrorCode(error), reason: String(error), retryable: false });
17978
18379
  }
17979
18380
  }
17980
18381
  }
@@ -17988,6 +18389,21 @@ var OwnershipWorkflow = class {
17988
18389
  }
17989
18390
  const entry = task.inbox[0];
17990
18391
  const ns = await this.observe(task.sub_aid);
18392
+ if (!task.base_agent_md && !task.request_validated && task.local_decision === "pending" && !Object.keys(task.files).length) {
18393
+ if (this.isBase(task, ns) || task.operation === "release" && ns.relation_version === task.base_relation_version && ns.file?.statement?.owner_aid === task.owner_aid) await this.captureBase(task, ns);
18394
+ else if (task.operation === "release" && ns.relation_version === 0 && !ns.file?.statement && task.local_roles.includes("sub")) {
18395
+ for (const candidate of await this.records(context.aid, task.sub_aid)) {
18396
+ const original = candidate.files.agent, cleanup = candidate.release_cleanup?.files.cleanup;
18397
+ if (!["bind", "transfer"].includes(candidate.operation) || candidate.local_decision !== "accept" || !candidate.local_roles.includes("sub") || targetOwner(candidate) !== task.owner_aid || candidate.target_relation_version !== task.base_relation_version || !original || !cleanup || cleanup.raw !== ns.content || cleanup.sha256 !== await ownershipSha256(ns.content) || cleanup.published_etag !== void 0 && cleanup.published_etag !== ns.etag) continue;
18398
+ const statement = parseOwnershipFile(original.raw, "agent", { expectedSubAid: task.sub_aid }).statement;
18399
+ if (!statement || !cleanup.condition?.if_match) continue;
18400
+ task.base_agent_md = { exists: true, raw: original.raw, sha256: original.sha256, etag: cleanup.condition.if_match, statement };
18401
+ task.cleanup_authorization_operation_id = candidate.operation_id;
18402
+ break;
18403
+ }
18404
+ }
18405
+ if (task.base_agent_md) await this.save(context);
18406
+ }
17991
18407
  try {
17992
18408
  validateOwnershipMessage(entry.payload, { authenticated_from_aid: entry.from_aid, local_aid: context.aid, task, ns });
17993
18409
  } catch {
@@ -18002,6 +18418,11 @@ var OwnershipWorkflow = class {
18002
18418
  task.request_validated = true;
18003
18419
  } else if (["bind.accept", "transfer.accept", "bind.reject", "transfer.reject"].includes(action)) {
18004
18420
  const decision = action.endsWith(".accept") ? "accept" : "reject";
18421
+ if (decision === "reject" && await this.complete(context, ns)) {
18422
+ task.inbox.shift();
18423
+ await this.save(context);
18424
+ continue;
18425
+ }
18005
18426
  if (!task.approvals[entry.from_aid] || task.approvals[entry.from_aid] === "pending") {
18006
18427
  task.approvals[entry.from_aid] = decision;
18007
18428
  if (decision === "reject") {
@@ -18032,7 +18453,7 @@ var OwnershipWorkflow = class {
18032
18453
  }
18033
18454
  } else if (action === "sub.cleaned") {
18034
18455
  const file = ns.file;
18035
- if (ns.relation_version === task.target_relation_version && ns.lock === "unlocked" && !ns.owner_aid && file && !file.statement && !Object.hasOwn(file.frontmatter, "owner") && !Object.hasOwn(file.frontmatter, "ownership")) task.hints[action] = true;
18456
+ if (ns.relation_version === 0 && ns.lock === "unlocked" && !ns.owner_aid && file && !file.statement && !Object.hasOwn(file.frontmatter, "owner") && !Object.hasOwn(file.frontmatter, "ownership")) task.hints[action] = true;
18036
18457
  }
18037
18458
  task.inbox.shift();
18038
18459
  await this.save(context);
@@ -18044,7 +18465,8 @@ var OwnershipWorkflow = class {
18044
18465
  const action = item.payload.action;
18045
18466
  const notification = ["relation.confirmed", "sub.cleaned"].includes(action);
18046
18467
  const terminal = ["bind.reject", "transfer.reject", "operation.stopped"].includes(action);
18047
- if (!terminal && (notification ? !this.isTarget(task, ns) : !this.isBase(task, ns) || task.status === "completed" || task.status === "rejected" || task.status === "stopped")) {
18468
+ const cleaned = action === "sub.cleaned" && task.status === "completed" && await this.confirmSubCleaned(task);
18469
+ if (!terminal && (notification ? !this.isTarget(task, ns) && !cleaned : !this.isBase(task, ns) || task.status === "completed" || task.status === "rejected" || task.status === "stopped")) {
18048
18470
  task.outbox = task.outbox.filter((row) => row.key !== item.key);
18049
18471
  await this.save(context);
18050
18472
  continue;
@@ -18065,15 +18487,35 @@ var OwnershipWorkflow = class {
18065
18487
  }
18066
18488
  async complete(context, ns) {
18067
18489
  const task = context.task;
18068
- if (!this.isTarget(task, ns)) return false;
18490
+ const began = Date.now(), cleaned = task.operation === "release" && ns.exists && ns.relation_version === 0 && ns.lock === "unlocked" && !ns.file?.statement;
18491
+ if (!this.isTarget(task, ns) && !cleaned) return false;
18069
18492
  let consensus;
18070
18493
  if (task.operation === "release") {
18071
18494
  consensus = await this.deps.files.checkConsensus(task.sub_aid);
18072
- if (consensus.status !== "released" || consensus.relation_version !== task.target_relation_version || consensus.lock !== ns.lock || consensus.owner_aid !== ns.owner_aid || consensus.agent_md_etag !== ns.etag) throw failure(consensus.status === "invalid" ? "invalid" : "unavailable", "ownership release could not be freshly confirmed", consensus.status !== "invalid");
18495
+ if ((cleaned ? consensus.status !== "unbound" || consensus.relation_version !== 0 : consensus.status !== "released" || consensus.relation_version !== task.target_relation_version) || consensus.lock !== ns.lock || consensus.owner_aid !== ns.owner_aid || consensus.agent_md_etag !== ns.etag || consensus.agent_md_sha256 !== await ownershipSha256(ns.content)) throw failure(consensus.status === "invalid" ? "invalid" : "unavailable", "ownership release could not be freshly confirmed", consensus.status !== "invalid");
18496
+ if (!cleaned && task.files.release && consensus.owner_md_sha256) {
18497
+ const source = task.files.release;
18498
+ if (source.sha256 !== consensus.owner_md_sha256) throw failure("conflict", "current release material does not belong to this task");
18499
+ }
18500
+ if (cleaned) {
18501
+ const source = task.files.release;
18502
+ if (task.local_roles.includes("owner") && source) {
18503
+ const object = await this.deps.files.readOwner(task.owner_aid, task.sub_aid);
18504
+ if (object.exists && object.content === source.raw && object.sha256 === source.sha256) {
18505
+ await this.deps.files.verify(object.content, "owner", task.sub_aid, task.owner_aid);
18506
+ consensus.owner_md_sha256 = object.sha256;
18507
+ }
18508
+ }
18509
+ const authorization = await this.cleanupEvidence(task, consensus, ns.content);
18510
+ if (!authorization) throw failure("conflict", "no local cleanup original can confirm this release task");
18511
+ const after = await this.observe(task.sub_aid);
18512
+ if (Date.now() - began > 3e4 || after.etag !== ns.etag || after.content !== ns.content || !after.exists || after.relation_version !== 0 || after.lock !== "unlocked" || after.owner_aid) throw failure("unavailable", "NS changed during cleanup confirmation", true);
18513
+ if (authorization !== task.operation_id) task.cleanup_authorization_operation_id = authorization;
18514
+ }
18073
18515
  const prior = ns.file?.statement;
18074
- if (prior && (prior.owner_aid !== task.owner_aid || prior.relation_version !== task.base_relation_version)) throw failure("conflict", "ownership released document does not match this generation");
18516
+ if (prior && (prior.owner_aid !== task.owner_aid || prior.relation_version !== task.base_relation_version || !this.sameStatement(prior, task.base_agent_md?.statement ?? void 0))) throw failure("conflict", "ownership released document does not match this generation");
18075
18517
  if (task.local_roles.includes("owner") && !task.files.release) throw failure("conflict", "ownership completion lacks this task original owner file");
18076
- if (prior && task.local_roles.includes("sub") && !task.local_roles.includes("owner") && !await this.authorizeCleanup(context)) {
18518
+ if (prior && task.local_roles.includes("sub") && !task.local_roles.includes("owner") && !await this.authorizeCleanup(context, prior)) {
18077
18519
  task.status = "pending";
18078
18520
  task.step = "cleanup_action_required";
18079
18521
  task.cleanup_pending = true;
@@ -18090,11 +18532,10 @@ var OwnershipWorkflow = class {
18090
18532
  if (task.local_roles.includes("sub") && (!task.files.agent || task.files.agent.sha256 !== consensus.agent_md_sha256)) throw failure("conflict", "NS accepted agent.md does not match this task original bytes");
18091
18533
  if (task.local_roles.includes(task.operation === "transfer" ? "new_owner" : "owner") && task.files.owner?.sha256 !== consensus.owner_md_sha256) throw failure("conflict", "Storage owner.md does not match this task original bytes");
18092
18534
  if (task.local_roles.includes("sub") && task.files.agent.published_etag !== void 0 && task.files.agent.published_etag !== consensus.agent_md_etag && task.verified_relation_version === void 0) throw failure("conflict", "NS accepted agent.md revision changed");
18093
- if (task.local_roles.includes(task.operation === "transfer" ? "new_owner" : "owner") && task.files.owner.published_revision !== void 0 && task.files.owner.published_revision !== consensus.owner_md_revision && task.verified_relation_version === void 0) throw failure("conflict", "Storage owner.md revision changed");
18094
18535
  }
18095
18536
  task.status = "completed";
18096
18537
  task.step = task.operation === "release" ? "released" : "confirmed";
18097
- task.verified_relation_version = consensus.relation_version;
18538
+ task.verified_relation_version = task.target_relation_version;
18098
18539
  task.verified_consensus = { ...consensus, protocol: "2.1" };
18099
18540
  delete task.code;
18100
18541
  delete task.reason;
@@ -18109,9 +18550,34 @@ var OwnershipWorkflow = class {
18109
18550
  } else await this.save(context);
18110
18551
  return true;
18111
18552
  }
18112
- async authorizeCleanup(context) {
18553
+ async cleanupEvidence(task, evidence, raw) {
18554
+ const release = task.files.release;
18555
+ if (task.local_roles.includes("owner") && release && release.sha256 === evidence.owner_md_sha256 && release.sha256 === await ownershipSha256(release.raw)) return task.operation_id;
18556
+ if (!task.local_roles.includes("sub")) return void 0;
18557
+ const matches = async (prepared) => Boolean(prepared && prepared.sha256 === evidence.agent_md_sha256 && prepared.sha256 === await ownershipSha256(prepared.raw) && (raw === void 0 || prepared.raw === raw) && (prepared.published_etag === void 0 || prepared.published_etag === evidence.agent_md_etag));
18558
+ if (await matches(task.files.cleanup)) return task.operation_id;
18559
+ let history;
18560
+ try {
18561
+ history = await this.storage().listOwnershipTasks(this.deps.currentAid());
18562
+ } catch (error) {
18563
+ throw failure("persistence_unavailable", `ownership cleanup history read failed: ${String(error)}`);
18564
+ }
18565
+ for (const snapshot of history) {
18566
+ if (!["bind", "transfer"].includes(String(snapshot.record.operation))) continue;
18567
+ if (snapshot.record.sub_aid !== task.sub_aid) continue;
18568
+ const candidate = await this.record(snapshot);
18569
+ if (task.cleanup_authorization_operation_id && candidate.operation_id !== task.cleanup_authorization_operation_id || candidate.local_decision !== "accept" || !candidate.local_roles.includes("sub") || candidate.sub_aid !== task.sub_aid || targetOwner(candidate) !== task.owner_aid || candidate.target_relation_version !== task.base_relation_version || !await matches(candidate.release_cleanup?.files.cleanup)) continue;
18570
+ const statement = candidate.files.agent && parseOwnershipFile(candidate.files.agent.raw, "agent", { expectedSubAid: task.sub_aid }).statement;
18571
+ if (statement?.owner_aid === task.owner_aid && statement.sub_aid === task.sub_aid && statement.relation_version === task.base_relation_version && this.sameStatement(statement, task.base_agent_md?.statement ?? void 0)) return candidate.operation_id;
18572
+ }
18573
+ return void 0;
18574
+ }
18575
+ sameStatement(left, right) {
18576
+ return Boolean(left && right && ["version", "declaration_id", "owner_aid", "sub_aid", "relation_version", "declared_at"].every((key) => left[key] === right[key]));
18577
+ }
18578
+ async authorizeCleanup(context, statement) {
18113
18579
  const task = context.task;
18114
- const matches = (record) => ["bind", "transfer"].includes(record.operation) && record.sub_aid === task.sub_aid && record.target_relation_version === task.base_relation_version && record.local_decision === "accept" && record.local_roles.includes("sub") && targetOwner(record) === task.owner_aid;
18580
+ const matches = (record) => ["bind", "transfer"].includes(record.operation) && record.sub_aid === task.sub_aid && record.target_relation_version === task.base_relation_version && record.local_decision === "accept" && record.local_roles.includes("sub") && targetOwner(record) === task.owner_aid && Boolean(record.files.agent && this.sameStatement(parseOwnershipFile(record.files.agent.raw, "agent", { expectedSubAid: task.sub_aid }).statement, statement));
18115
18581
  if (task.cleanup_authorization_operation_id) {
18116
18582
  const saved = await this.load(task.cleanup_authorization_operation_id);
18117
18583
  if (!saved || !matches(await this.record(saved))) throw failure("persistence_unavailable", "ownership cleanup historical authorization is damaged");
@@ -18123,7 +18589,7 @@ var OwnershipWorkflow = class {
18123
18589
  } catch (error) {
18124
18590
  throw failure("persistence_unavailable", `ownership cleanup history read failed: ${String(error)}`);
18125
18591
  }
18126
- const authorized = (await Promise.all(history.map((row) => this.record(row)))).find(matches);
18592
+ const authorized = (await Promise.all(history.filter((row) => row.record.sub_aid === task.sub_aid).map((row) => this.record(row)))).find(matches);
18127
18593
  if (!authorized) return false;
18128
18594
  task.cleanup_authorization_operation_id = authorized.operation_id;
18129
18595
  await this.save(context);
@@ -18131,11 +18597,19 @@ var OwnershipWorkflow = class {
18131
18597
  }
18132
18598
  async resumeSubCleanup(context, ns) {
18133
18599
  const task = context.task;
18134
- if (!["bind", "transfer"].includes(task.operation) || !task.local_roles.includes("sub") || task.local_decision !== "accept" || ns.lock !== "unlocked" || ns.relation_version !== task.target_relation_version + 1 || task.release_cleanup?.cleaned) return;
18600
+ if (!["bind", "transfer"].includes(task.operation) || !task.local_roles.includes("sub") || task.local_decision !== "accept" || ns.lock !== "unlocked" || task.release_cleanup?.cleaned) return;
18601
+ if (ns.relation_version === 0 && !ns.file?.statement) {
18602
+ const prepared = task.release_cleanup?.files.cleanup;
18603
+ if (!prepared || !prepared.condition?.if_match || prepared.raw !== ns.content || prepared.sha256 !== await ownershipSha256(ns.content) || prepared.published_etag !== void 0 && prepared.published_etag !== ns.etag) throw failure("conflict", "historical cleanup original file was not confirmed");
18604
+ task.release_cleanup.cleaned = true;
18605
+ await this.save(context);
18606
+ return;
18607
+ }
18608
+ if (ns.relation_version !== task.target_relation_version) return;
18135
18609
  const consensus = await this.deps.files.checkConsensus(task.sub_aid);
18136
18610
  if (consensus.status !== "released" || consensus.relation_version !== ns.relation_version || consensus.agent_md_etag !== ns.etag || consensus.agent_md_sha256 !== await ownershipSha256(ns.content)) throw failure(consensus.status === "invalid" ? "invalid" : "unavailable", "NS changed during historical cleanup observation", consensus.status !== "invalid");
18137
18611
  const statement = ns.file?.statement;
18138
- if (statement && (statement.owner_aid !== targetOwner(task) || statement.relation_version !== task.target_relation_version)) throw failure("conflict", "historical cleanup fields belong to another relation");
18612
+ if (!statement || !task.files.agent || !this.sameStatement(statement, parseOwnershipFile(task.files.agent.raw, "agent", { expectedSubAid: task.sub_aid }).statement)) throw failure("conflict", "historical cleanup fields belong to another relation");
18139
18613
  if (!task.release_cleanup) {
18140
18614
  task.release_cleanup = { files: {}, cleaned: false };
18141
18615
  await this.save(context);
@@ -18147,6 +18621,7 @@ var OwnershipWorkflow = class {
18147
18621
  base_relation_version: task.target_relation_version,
18148
18622
  target_relation_version: ns.relation_version,
18149
18623
  files: task.release_cleanup.files,
18624
+ expected_agent_statement: statement,
18150
18625
  persist: () => this.save(context),
18151
18626
  guard: () => this.save(context)
18152
18627
  });
@@ -18157,7 +18632,9 @@ var OwnershipWorkflow = class {
18157
18632
  async cleanup(context, ns) {
18158
18633
  const task = context.task;
18159
18634
  if (!task.cleanup_pending || task.cleanup_done) return;
18160
- if (!this.isTarget(task, ns)) {
18635
+ const releaseOwner = task.operation === "release" && task.local_roles.includes("owner");
18636
+ const cleaned = task.operation === "release" && ns.relation_version === 0 && ns.lock === "unlocked" && !ns.file?.statement;
18637
+ if (!this.isTarget(task, ns) && !cleaned && !releaseOwner) {
18161
18638
  task.cleanup_pending = false;
18162
18639
  task.step = "cleanup_superseded";
18163
18640
  await this.save(context);
@@ -18165,24 +18642,23 @@ var OwnershipWorkflow = class {
18165
18642
  }
18166
18643
  if (task.operation === "release" && task.local_roles.includes("sub")) {
18167
18644
  const prior = ns.file?.statement;
18168
- if (prior && !await this.authorizeCleanup(context)) {
18645
+ if (prior && !this.sameStatement(prior, task.base_agent_md?.statement ?? void 0)) throw failure("conflict", "ownership cleanup original relation was replaced");
18646
+ if (prior && !await this.authorizeCleanup(context, prior)) {
18169
18647
  task.step = "cleanup_action_required";
18170
18648
  await this.save(context);
18171
18649
  this.event(context, "ownership.action_required");
18172
18650
  return;
18173
18651
  }
18174
18652
  if (prior && (prior.owner_aid !== task.owner_aid || prior.relation_version !== task.base_relation_version)) throw failure("conflict", "ownership cleanup current fields differ");
18175
- if (prior) await this.deps.files.revokeOwneraid(this.fileTask(context));
18653
+ if (prior || task.files.cleanup) await this.deps.files.revokeOwneraid(this.fileTask(context, prior));
18176
18654
  this.enqueue(task, task.owner_aid, "sub.cleaned");
18177
18655
  }
18178
18656
  if (task.local_roles.includes("owner") && (task.operation === "release" || task.operation === "transfer")) {
18657
+ if (releaseOwner && this.isTarget(task, ns) && ns.file?.statement) return;
18179
18658
  const prepared = task.files[task.operation === "release" ? "release" : "transfer"];
18180
- if (prepared && !prepared.published_revision) {
18659
+ if (prepared) {
18181
18660
  const object = await this.deps.files.readOwner(task.owner_aid, task.sub_aid);
18182
- if (object.exists && object.sha256 === prepared.sha256 && object.content === prepared.raw) {
18183
- prepared.published_revision = object.revision;
18184
- await this.save(context);
18185
- }
18661
+ if (object.exists && (object.sha256 !== prepared.sha256 || object.content !== prepared.raw)) throw failure("conflict", "ownership cleanup original owner file has been replaced");
18186
18662
  }
18187
18663
  await this.deps.files.cleanupOwnerFile(this.fileTask(context));
18188
18664
  }
@@ -18216,7 +18692,7 @@ var OwnershipWorkflow = class {
18216
18692
  delete task.reason;
18217
18693
  task.retryable = true;
18218
18694
  if (await this.complete(context, ns)) {
18219
- if (task.status === "completed" && task.operation === "release" && task.local_roles.includes("sub")) {
18695
+ if (task.status === "completed" && task.operation === "release") {
18220
18696
  await this.cleanup(context, ns);
18221
18697
  ns = await this.observe(task.sub_aid);
18222
18698
  }
@@ -18303,6 +18779,21 @@ var OwnershipWorkflow = class {
18303
18779
  };
18304
18780
 
18305
18781
  // src/aid-store.ts
18782
+ async function readSigningInput(input, inputType = "text") {
18783
+ if (inputType === "text") {
18784
+ if (typeof input === "string" || input instanceof Uint8Array) return input;
18785
+ throw new Error("text input requires a string or Uint8Array");
18786
+ }
18787
+ if (typeof input === "string" || input instanceof Uint8Array) throw new Error("browser file input does not support filesystem paths");
18788
+ const file = "getFile" in input ? await input.getFile() : input;
18789
+ if (typeof file.arrayBuffer === "function") return new Uint8Array(await file.arrayBuffer());
18790
+ return await new Promise((resolve, reject) => {
18791
+ const reader = new FileReader();
18792
+ reader.onload = () => resolve(new Uint8Array(reader.result));
18793
+ reader.onerror = () => reject(reader.error);
18794
+ reader.readAsArrayBuffer(file);
18795
+ });
18796
+ }
18306
18797
  function _derReadLength(data, offset) {
18307
18798
  if (offset >= data.length) return null;
18308
18799
  const first = data[offset];
@@ -18573,6 +19064,8 @@ var AIDStore = class {
18573
19064
  const fp = String(certFingerprint ?? "").trim().toLowerCase();
18574
19065
  const cachedCert = await this._keystore.loadCert(target, fp || void 0);
18575
19066
  if (cachedCert) {
19067
+ const gatewayUrl2 = await this._resolveGateway(target);
19068
+ await this._verifyDownloadedPeerCertificate(gatewayUrl2, target, cachedCert, fp, signedAt);
18576
19069
  return await AID.create({ aid: target, aunPath: this.aunPath, certPem: cachedCert, privateKeyPem: null, certValid: true, privateKeyValid: false });
18577
19070
  }
18578
19071
  if (fp) {
@@ -18599,6 +19092,58 @@ var AIDStore = class {
18599
19092
  const closeTokens = this._tokenStore.close;
18600
19093
  if (typeof closeTokens === "function") closeTokens.call(this._tokenStore);
18601
19094
  }
19095
+ async sign(aid2, input, options = {}) {
19096
+ try {
19097
+ if (options.inputType !== void 0 && !["text", "file"].includes(options.inputType) || options.format !== void 0 && !["raw", "document"].includes(options.format)) throw new Error("unsupported signing inputType or format");
19098
+ if (options.append && options.format !== "document") throw new Error("append requires document format");
19099
+ if (options.inPlace && (options.inputType !== "file" || options.format !== "document" || typeof input !== "object" || !("createWritable" in input) || typeof input.createWritable !== "function")) throw new Error("inPlace document signing requires a writable file handle");
19100
+ const loaded = options.identity ? resultOk({ aid: options.identity }) : await this.load(aid2);
19101
+ if (!loaded.ok) return loaded;
19102
+ if (loaded.data.aid.aid !== aid2 || parseCertMetadata(loaded.data.aid.certPem).subject.toLowerCase() !== aid2.toLowerCase()) throw new Error("signing identity does not match AID");
19103
+ const raw = await readSigningInput(input, options.inputType);
19104
+ if (typeof raw === "string" && new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(new TextEncoder().encode(raw)) !== raw) throw new Error("signing input must be valid UTF-8");
19105
+ if (options.format !== "document") return await loaded.data.aid.sign(raw);
19106
+ const content = typeof raw === "string" ? raw : new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(raw);
19107
+ const result = await signDocument(loaded.data.aid, content, options);
19108
+ if (options.inPlace) {
19109
+ const writable = await input.createWritable();
19110
+ await writable.write(result.signed);
19111
+ await writable.close();
19112
+ }
19113
+ return resultOk(result);
19114
+ } catch (exc) {
19115
+ return resultErr(SIGNATURE_OPERATION_ERROR, String(exc), exc);
19116
+ }
19117
+ }
19118
+ async verify(aid2, input, signatureOrOptions = {}, options = {}) {
19119
+ try {
19120
+ const opts = typeof signatureOrOptions === "string" ? options : signatureOrOptions;
19121
+ if (opts.inputType !== void 0 && !["text", "file"].includes(opts.inputType) || opts.format !== void 0 && !["raw", "document"].includes(opts.format)) throw new Error("unsupported verification inputType or format");
19122
+ if (opts.identity && (opts.identity.aid !== aid2 || parseCertMetadata(opts.identity.certPem).subject.toLowerCase() !== aid2.toLowerCase())) throw new Error("verification identity does not match AID");
19123
+ const raw = await readSigningInput(input, opts.inputType);
19124
+ if (typeof raw === "string" && new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(new TextEncoder().encode(raw)) !== raw) throw new Error("verification input must be valid UTF-8");
19125
+ if (opts.format === "document") {
19126
+ const content = typeof raw === "string" ? raw : new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(raw);
19127
+ const resolver = opts.certificateResolver ?? (async (signer, fingerprint) => {
19128
+ if (opts.identity?.aid === signer) return opts.identity;
19129
+ const resolved = await this.load(signer);
19130
+ if (!resolved.ok) throw new Error(resolved.error.message);
19131
+ return resolved.data.aid;
19132
+ });
19133
+ return resultOk(await verifyDocument(content, resolver, { expectedAid: aid2 }));
19134
+ }
19135
+ if (typeof signatureOrOptions !== "string") throw new Error("raw verification requires a detached signature");
19136
+ const supplied = !opts.identity && opts.certificateResolver ? await opts.certificateResolver(aid2, "", 0) : void 0;
19137
+ if (supplied && typeof supplied !== "string" && "aid" in supplied && supplied.aid !== aid2) throw new Error("verification certificate identity does not match AID");
19138
+ const suppliedPem = typeof supplied === "string" ? supplied : supplied?.certPem;
19139
+ const loaded = opts.identity ? resultOk({ aid: opts.identity }) : suppliedPem ? resultOk({ aid: await AID.create({ aid: aid2, aunPath: this.aunPath, certPem: suppliedPem, privateKeyPem: null, certValid: true, privateKeyValid: false }) }) : await this.load(aid2);
19140
+ if (!loaded.ok) return loaded;
19141
+ if (loaded.data.aid.aid !== aid2 || parseCertMetadata(loaded.data.aid.certPem).subject.toLowerCase() !== aid2.toLowerCase()) throw new Error("verification identity does not match AID");
19142
+ return await loaded.data.aid.verify(raw, signatureOrOptions);
19143
+ } catch (exc) {
19144
+ return resultErr(VERIFICATION_OPERATION_ERROR, String(exc), exc);
19145
+ }
19146
+ }
18602
19147
  async load(aid2) {
18603
19148
  const target = String(aid2 ?? "").trim();
18604
19149
  const certPem = await this._keystore.loadCert(target);
@@ -18717,14 +19262,14 @@ var AIDStore = class {
18717
19262
  this._registerFlow.validateAidName(target);
18718
19263
  const type = String(options?.type ?? "").trim(), owner = String(options?.owner ?? "").trim().toLowerCase();
18719
19264
  const entity = ["group", "org"].includes(type);
18720
- if (!type || type === "human" && owner || (type === "agent" || entity) && !owner) throw new ValidationError("register requires type; human forbids owner; agent/group/org requires owner");
19265
+ if (!type || type === "human" && owner || entity && !owner) throw new ValidationError("register requires type; human forbids owner; group/org requires owner");
18721
19266
  if (owner) validateAIDFormat(owner, "owner");
18722
19267
  const previousRaw = await this._tokenStore.getMetadata(target, "registration_intent");
18723
19268
  const previous = previousRaw ? parseOwnershipRegistrationIntent(previousRaw, target) : null;
18724
19269
  const exttype = String(options?.exttype ?? "").trim();
18725
19270
  if (previous && (previous.type !== type || previous.owner !== owner || previous.exttype !== exttype)) throw new StateError("ownership registration intent changed", { stringCode: "conflict" });
18726
19271
  const intent = previous ?? (entity ? { schema_version: 1, sub_aid: target, type, owner, exttype, protocol: "entity", pending: false } : { schema_version: 1, sub_aid: target, type, owner, exttype, operation_id: crypto.randomUUID(), pending: Boolean(owner) });
18727
- await this._tokenStore.setMetadata(target, "registration_intent", JSON.stringify(intent));
19272
+ if (type !== "agent" || owner) await this._tokenStore.setMetadata(target, "registration_intent", JSON.stringify(intent));
18728
19273
  const gatewayUrl = await this._resolveGateway(target);
18729
19274
  const result = await this._registerFlow.registerAid(gatewayUrl, target);
18730
19275
  if (result.cert) {
@@ -18780,7 +19325,11 @@ var AIDStore = class {
18780
19325
  let peer;
18781
19326
  let certFromCache = false;
18782
19327
  const cached = await this.load(target);
18783
- if (cached.ok && !forceRefresh) {
19328
+ const localIdentity = !cached.ok && cached.error.code === CERT_EXPIRED && !forceRefresh && !skipAgentMd ? await this._expiredIdentityForAgentMdRead(target) : null;
19329
+ if (localIdentity) {
19330
+ peer = localIdentity;
19331
+ certFromCache = true;
19332
+ } else if (cached.ok && !forceRefresh) {
18784
19333
  peer = cached.data.aid;
18785
19334
  certFromCache = true;
18786
19335
  } else {
@@ -18796,6 +19345,15 @@ var AIDStore = class {
18796
19345
  }
18797
19346
  const source = { cert_from_cache: certFromCache, agent_md_fetched: false };
18798
19347
  if (skipAgentMd) return resultOk({ aid: peer, source });
19348
+ if (!forceRefresh && (localIdentity || peer.isPrivateKeyValid() && peer.privateKeyPem)) {
19349
+ const record = await this._agentMdManager.loadRecord(target, true);
19350
+ const draft = String(record?.local_draft_content ?? "");
19351
+ const local = draft || await this._agentMdManager.readContent(target);
19352
+ if (local) {
19353
+ const etag = await AgentMdManager.contentEtag(local);
19354
+ return resultOk({ aid: peer, source, agent_md: { aid: target, content: local, verification: { status: "unverified" }, signature: { status: "unverified" }, cert_pem: peer.certPem, etag: draft ? "" : etag, last_modified: String(record?.last_modified ?? ""), status: 0, in_sync: draft ? false : etag === record?.remote_etag, saved_to: null, save_error: null } });
19355
+ }
19356
+ }
18799
19357
  const agentMd = await this.downloadAgentMd(target, opts?.timeout);
18800
19358
  if (!agentMd.ok) return agentMd;
18801
19359
  source.agent_md_fetched = true;
@@ -18804,6 +19362,21 @@ var AIDStore = class {
18804
19362
  return resultErr(NETWORK_ERROR, String(exc), exc);
18805
19363
  }
18806
19364
  }
19365
+ async _expiredIdentityForAgentMdRead(target) {
19366
+ const record = await this._agentMdManager.loadRecord(target, true);
19367
+ if (!String(record?.local_draft_content ?? "") && !await this._agentMdManager.readContent(target)) return null;
19368
+ const certPem = await this._keystore.loadCert(target);
19369
+ const keyPair = await this._keystore.loadKeyPair(target);
19370
+ if (!certPem || !keyPair?.private_key_pem || parseCertCN(certPem) !== target) return null;
19371
+ try {
19372
+ const privateKey = await importPrivateKeyEcdsa(String(keyPair.private_key_pem));
19373
+ const probe = new TextEncoder().encode("aun-aidstore-private-key-self-test");
19374
+ if (!await ecdsaVerifyDer(await importCertPublicKeyEcdsa(certPem), await ecdsaSignDer(privateKey, probe), probe)) return null;
19375
+ return await AID.create({ aid: target, aunPath: this.aunPath, certPem, privateKeyPem: null, certValid: false, privateKeyValid: false, deviceId: this.deviceId, slotId: this.slotId, verifySsl: this._verifySsl, aidStore: this });
19376
+ } catch {
19377
+ return null;
19378
+ }
19379
+ }
18807
19380
  async downloadAgentMd(aid2, timeoutMs = 3e4) {
18808
19381
  try {
18809
19382
  return resultOk(await this._agentMdManager.download(aid2, timeoutMs));
@@ -18824,7 +19397,7 @@ var AIDStore = class {
18824
19397
  throw new ValidationError(`certificate fingerprint mismatch for aid: ${aid2}`);
18825
19398
  }
18826
19399
  if (signedAt === void 0) await this._peerAuth.verifyPeerCertificate(gatewayUrl, certPem, aid2);
18827
- else await this._peerAuth.verifyPeerCertificate(gatewayUrl, certPem, aid2, signedAt);
19400
+ else await this._peerAuth.verifyPeerCertificate(gatewayUrl, certPem, aid2, signedAt, false, true);
18828
19401
  }
18829
19402
  async _resolveEntityAgentMdOwner(aid2, type) {
18830
19403
  const authority = await this._agentMdManager.readEntityAuthorityFresh(aid2);
@@ -18873,10 +19446,13 @@ var AIDStore = class {
18873
19446
  return resultErr(loaded.error.code, loaded.error.message);
18874
19447
  }
18875
19448
  const current = loaded.data.aid;
18876
- const raw = content ?? await this._agentMdManager.readContent(target);
18877
- const parsed = validateAgentMdDocument(String(raw ?? ""), { expectedAid: target });
19449
+ const raw = content ?? (String((await this._agentMdManager.loadRecord(target, true))?.local_draft_content ?? "") || await this._agentMdManager.readContent(target));
19450
+ const tail = parseAgentMdTailSignatureForRead(String(raw ?? ""));
19451
+ if (tail.parseError) throw new ValidationError(tail.parseError);
19452
+ const base = validateAgentMdDocument(tail.payload, { expectedAid: target, validateOwnership: false });
19453
+ const entity = ["group", "org"].includes(String(base.frontmatter.type));
19454
+ const parsed = entity ? validateAgentMdDocument(String(raw ?? ""), { expectedAid: target }) : base;
18878
19455
  const meta = parsed.frontmatter;
18879
- const entity = ["group", "org"].includes(String(meta.type));
18880
19456
  if ((!current.isPrivateKeyValid() || !current.privateKeyPem) && !(entity && parsed.signature)) {
18881
19457
  return resultErr(PRIVATE_KEY_NOT_VALID, `uploadAgentMd requires local AID with a valid private key: ${target}`);
18882
19458
  }
@@ -18906,7 +19482,16 @@ var AIDStore = class {
18906
19482
  accessTokenResolver: async () => this._uploadAgentMdToken(uploader, await this._resolveGateway(uploaderTarget)),
18907
19483
  aidValidator: (value) => this._registerFlow.validateAidName(value)
18908
19484
  });
18909
- return resultOk(entity ? await manager.uploadAs(target, String(raw), current) : await manager.upload(raw));
19485
+ if (entity) {
19486
+ let signed = String(raw);
19487
+ if (!parsed.signature) signed = (await signDocument(current, signed, { signerAid: true })).signed;
19488
+ if (options.coSignerAid) {
19489
+ if (options.coSignerAid !== meta.owner) throw new ValidationError("entity additional signer must be current owner");
19490
+ signed = (await signDocument(uploader, signed, { append: true })).signed;
19491
+ }
19492
+ return resultOk(await manager.uploadAs(target, signed, current));
19493
+ }
19494
+ return resultOk(await manager.upload(raw));
18910
19495
  } catch (exc) {
18911
19496
  return agentMdFailure(exc);
18912
19497
  }
@@ -20182,6 +20767,9 @@ var MessageDeliveryEngine = class {
20182
20767
  __publicField(this, "pendingPullNoProgressAcks", null);
20183
20768
  __publicField(this, "onlineUnreadHintTargets", null);
20184
20769
  __publicField(this, "onlineUnreadHintOwners", null);
20770
+ __publicField(this, "backgroundPullDemands", /* @__PURE__ */ new Map());
20771
+ __publicField(this, "backgroundPullTimer", null);
20772
+ __publicField(this, "backgroundPullRecoveryQueued", false);
20185
20773
  __publicField(this, "realtimeAcking", null);
20186
20774
  __publicField(this, "pendingP2PInlineAcks", null);
20187
20775
  __publicField(this, "pendingGroupInlineAcks", null);
@@ -20209,6 +20797,10 @@ var MessageDeliveryEngine = class {
20209
20797
  this.pendingPullNoProgressAcks = null;
20210
20798
  this.onlineUnreadHintTargets = null;
20211
20799
  this.onlineUnreadHintOwners = null;
20800
+ this.backgroundPullDemands.clear();
20801
+ if (this.backgroundPullTimer !== null) clearTimeout(this.backgroundPullTimer);
20802
+ this.backgroundPullTimer = null;
20803
+ this.backgroundPullRecoveryQueued = false;
20212
20804
  this.realtimeTailResults = null;
20213
20805
  this.realtimeAcking = null;
20214
20806
  this.pendingP2PInlineAcks = null;
@@ -20362,8 +20954,94 @@ var MessageDeliveryEngine = class {
20362
20954
  onPullGateIdle(ns) {
20363
20955
  if (!ns) return;
20364
20956
  this.schedulePendingPullIfNeeded(ns, "pull-gate-idle");
20957
+ this.requestBackgroundPullRecovery();
20958
+ }
20959
+ recordBackgroundPullDemand(ns, throughSeq = 0, probe = false) {
20960
+ const demand = this.backgroundPullDemands.get(ns) ?? {
20961
+ throughSeq: 0,
20962
+ hasMore: false,
20963
+ probe: false,
20964
+ afterSeq: -1,
20965
+ retryAt: 0
20966
+ };
20967
+ demand.throughSeq = Math.max(demand.throughSeq, positiveSafeSequenceHint(throughSeq));
20968
+ demand.probe || (demand.probe = probe);
20969
+ this.backgroundPullDemands.set(ns, demand);
20970
+ return demand;
20971
+ }
20972
+ observeBackgroundPullResponse(ns, afterSeq, result, limit = 50) {
20973
+ if (!ns || this.runtime.client._sessionOptions?.background_sync === false) return;
20974
+ const cursor = isJsonObject(result.cursor) ? result.cursor : null;
20975
+ const rows = Array.isArray(result.messages) ? result.messages : Array.isArray(result.events) ? result.events : [];
20976
+ let throughSeq = Math.max(
20977
+ positiveSafeSequenceHint(cursor?.latest_seq),
20978
+ positiveSafeSequenceHint(result.latest_seq),
20979
+ positiveSafeSequenceHint(result.latest_message_seq),
20980
+ positiveSafeSequenceHint(result.latest_event_seq)
20981
+ );
20982
+ for (const row of rows) {
20983
+ if (!isJsonObject(row) || this.isVolatileMessage(row)) continue;
20984
+ throughSeq = Math.max(throughSeq, positiveSafeSequenceHint(row.seq ?? row.event_seq));
20985
+ }
20986
+ const demand = this.recordBackgroundPullDemand(ns, throughSeq);
20987
+ if (afterSeq >= demand.afterSeq) {
20988
+ demand.afterSeq = afterSeq;
20989
+ demand.hasMore = result.has_more === true || positiveSafeSequenceHint(result.remaining) > 0 || !Object.prototype.hasOwnProperty.call(result, "has_more") && limit > 0 && rows.length >= limit;
20990
+ demand.probe = false;
20991
+ }
20992
+ this.requestBackgroundPullRecovery();
20993
+ }
20994
+ finishBackgroundPull(ns, afterSeq, failed) {
20995
+ const demand = this.backgroundPullDemands.get(ns);
20996
+ if (demand) {
20997
+ const state = this.syncState(ns);
20998
+ const ack = state.ack;
20999
+ if (ack < state.tail - 1) demand.throughSeq = Math.max(demand.throughSeq, state.tail - 1);
21000
+ demand.probe = failed;
21001
+ if (!demand.probe && !demand.hasMore && ack >= demand.throughSeq) this.backgroundPullDemands.delete(ns);
21002
+ else demand.retryAt = failed || ack <= afterSeq || ack <= (this.pendingPullNoProgressAcks?.get(ns) ?? -1) ? Date.now() + P2P_GAP_FILL_RETRY_BASE_MS : 0;
21003
+ }
21004
+ this.requestBackgroundPullRecovery();
21005
+ }
21006
+ requestBackgroundPullRecovery() {
21007
+ if (this.backgroundPullRecoveryQueued) return;
21008
+ const generation = this.captureInlineGeneration();
21009
+ this.backgroundPullRecoveryQueued = true;
21010
+ void Promise.resolve().then(() => {
21011
+ if (!this.isInlineGenerationCurrent(generation)) return;
21012
+ this.backgroundPullRecoveryQueued = false;
21013
+ const client = this.runtime.client;
21014
+ if (!client._aid || client._sessionOptions?.background_sync === false || client.state !== "ready" /* READY */ || client._closing) return;
21015
+ if (this.backgroundPullTimer !== null) clearTimeout(this.backgroundPullTimer);
21016
+ this.backgroundPullTimer = null;
21017
+ let retryAt = 0;
21018
+ for (const [ns, demand] of this.backgroundPullDemands) {
21019
+ if (ns.startsWith("p2p:") && ns !== `p2p:${client._aid}`) continue;
21020
+ const dedupKey = ns.startsWith("p2p:") ? `p2p_pull:${ns}` : ns.startsWith("group_event:") ? `group_event_pull:${ns}` : `group_pull:${ns}`;
21021
+ if (client._gapFillDone.has(dedupKey) || this.onlineUnreadHintOwners?.has(ns) || this.realtimeSyncing?.has(ns) || client._rpcPipeline?.hasPullActivity?.(ns, false)) continue;
21022
+ const ack = this.syncState(ns).ack;
21023
+ if (!demand.probe && !demand.hasMore && ack >= demand.throughSeq) {
21024
+ this.backgroundPullDemands.delete(ns);
21025
+ continue;
21026
+ }
21027
+ if (demand.retryAt > Date.now()) {
21028
+ retryAt = retryAt === 0 ? demand.retryAt : Math.min(retryAt, demand.retryAt);
21029
+ continue;
21030
+ }
21031
+ if (ns.startsWith("p2p:")) client._safeAsync(this.fillP2pGap());
21032
+ else if (ns.startsWith("group_event:")) client._safeAsync(this.fillGroupEventGap(ns.slice("group_event:".length)));
21033
+ else if (ns.startsWith("group:")) client._safeAsync(this.fillGroupGap(ns.slice("group:".length)));
21034
+ }
21035
+ if (retryAt > 0) {
21036
+ this.backgroundPullTimer = globalThis.setTimeout(() => {
21037
+ this.backgroundPullTimer = null;
21038
+ if (this.isInlineGenerationCurrent(generation)) this.requestBackgroundPullRecovery();
21039
+ }, Math.max(0, retryAt - Date.now()));
21040
+ }
21041
+ });
20365
21042
  }
20366
21043
  onForegroundPullsDrained() {
21044
+ this.requestBackgroundPullRecovery();
20367
21045
  const client = this.runtime.client;
20368
21046
  if (!client._aid || client._sessionOptions?.background_sync === false || client.state !== "ready" /* READY */ || client._closing) return;
20369
21047
  const ns = `p2p:${client._aid}`;
@@ -20373,6 +21051,7 @@ var MessageDeliveryEngine = class {
20373
21051
  client._safeAsync(this.fillP2pGap());
20374
21052
  }
20375
21053
  onPullWorkSettled() {
21054
+ this.requestBackgroundPullRecovery();
20376
21055
  const pipeline = this.runtime.client._rpcPipeline;
20377
21056
  if (pipeline?.hasAnyPullActivity?.() === true) return;
20378
21057
  void this.flushPullDeliveryChanges().finally(() => this.syncStopped("pull_drained"));
@@ -21974,7 +22653,13 @@ var MessageDeliveryEngine = class {
21974
22653
  return;
21975
22654
  }
21976
22655
  }
22656
+ const generation = this.captureInlineGeneration();
22657
+ const dedupKey = `group_pull:${ns}`;
22658
+ this.recordBackgroundPullDemand(ns, positiveSafeSequenceHint(notification.seq), true);
22659
+ if (client._gapFillDone.has(dedupKey)) return;
22660
+ client._gapFillDone.add(dedupKey);
21977
22661
  let rawCompleted = false;
22662
+ let failed = false;
21978
22663
  try {
21979
22664
  const key = client._rpcPipeline.pullGateKeyForCall("group.pull", {
21980
22665
  group_id: groupId,
@@ -21990,15 +22675,23 @@ var MessageDeliveryEngine = class {
21990
22675
  limit: 50,
21991
22676
  _rpc_background: true
21992
22677
  }, { background: true });
22678
+ if (!this.isInlineGenerationCurrent(generation)) throw new Error("pull invalidated");
21993
22679
  rawCompleted = true;
22680
+ if (isJsonObject(result)) this.observeBackgroundPullResponse(ns, afterSeq, result, 50);
21994
22681
  await this.commitPlainForwardPage(ns, afterSeq, "group.pull", result, groupId);
21995
22682
  this.prunePushedSeqs(ns);
21996
22683
  }, true);
21997
22684
  } catch (exc) {
22685
+ failed = true;
21998
22686
  client._clientLog.warn(`auto pull group message commit/ack failed:${String(exc)}`);
21999
22687
  if (!rawCompleted) {
22000
22688
  await client._publishAppEvent("group.message_created", notification, "push");
22001
22689
  }
22690
+ } finally {
22691
+ if (this.isInlineGenerationCurrent(generation)) {
22692
+ client._gapFillDone.delete(dedupKey);
22693
+ this.finishBackgroundPull(ns, afterSeq, failed);
22694
+ }
22002
22695
  }
22003
22696
  }
22004
22697
  scheduleP2pGapRetry(ns, retryAttempt) {
@@ -22025,6 +22718,7 @@ var MessageDeliveryEngine = class {
22025
22718
  }
22026
22719
  async fillP2pGap(retryAttempt = 0) {
22027
22720
  const client = this.runtime.client;
22721
+ const generation = this.captureInlineGeneration();
22028
22722
  if (client._sessionOptions?.background_sync === false) return;
22029
22723
  if (client.state !== "ready" /* READY */ || client._closing) return;
22030
22724
  if (!client._aid) return;
@@ -22032,9 +22726,11 @@ var MessageDeliveryEngine = class {
22032
22726
  const afterSeq = client._seqTracker.getContiguousSeq(ns);
22033
22727
  const dedupKey = `p2p_pull:${ns}`;
22034
22728
  if (client._gapFillDone.has(dedupKey)) return;
22729
+ this.recordBackgroundPullDemand(ns, 0, true);
22035
22730
  client._gapFillDone.add(dedupKey);
22036
22731
  this.runtime.delivery.setGapFillActive(true);
22037
22732
  let filled = 0;
22733
+ let failed = false;
22038
22734
  try {
22039
22735
  const key = client._rpcPipeline.pullGateKeyForCall("message.pull", { after_seq: afterSeq, limit: 50 });
22040
22736
  const messages = await client._rpcPipeline.runPullSerialized(
@@ -22052,20 +22748,25 @@ var MessageDeliveryEngine = class {
22052
22748
  this.prunePushedSeqs(ns);
22053
22749
  client._clientLog.warn(`P2P message gap-fill V2 unavailable, used plain pull fallback:${String(formatDeliveryError(exc))}`);
22054
22750
  } catch (fallbackExc) {
22751
+ failed = true;
22055
22752
  client._clientLog.warn(`P2P message gap-fill plain fallback failed:${String(formatDeliveryError(fallbackExc))}`);
22056
- this.scheduleP2pGapRetry(ns, retryAttempt + 1);
22057
22753
  }
22058
22754
  } else {
22755
+ failed = true;
22059
22756
  client._clientLog.warn(`P2P message gap-fill failed:${String(formatDeliveryError(exc))}`);
22060
22757
  }
22061
22758
  } finally {
22062
- client._gapFillDone.delete(dedupKey);
22063
- this.runtime.delivery.setGapFillActive(false);
22064
- this.schedulePendingPullIfNeeded(ns, "gap-fill-complete");
22759
+ if (this.isInlineGenerationCurrent(generation)) {
22760
+ client._gapFillDone.delete(dedupKey);
22761
+ this.runtime.delivery.setGapFillActive(false);
22762
+ this.schedulePendingPullIfNeeded(ns, "gap-fill-complete");
22763
+ this.finishBackgroundPull(ns, afterSeq, failed);
22764
+ }
22065
22765
  }
22066
22766
  }
22067
22767
  async fillP2pGapWithPlainPull(ns, afterSeq) {
22068
22768
  const client = this.runtime.client;
22769
+ const generation = this.captureInlineGeneration();
22069
22770
  const params2 = {
22070
22771
  after_seq: afterSeq,
22071
22772
  limit: 50,
@@ -22074,11 +22775,14 @@ var MessageDeliveryEngine = class {
22074
22775
  _rpc_background: true
22075
22776
  };
22076
22777
  const raw = await client._rpcPipeline.rawCall("message.pull", params2, { background: true });
22778
+ if (!this.isInlineGenerationCurrent(generation)) throw new Error("pull invalidated");
22779
+ if (isJsonObject(raw)) this.observeBackgroundPullResponse(ns, afterSeq, raw, 50);
22077
22780
  const page = await this.commitPlainForwardPage(ns, afterSeq, "message.pull", raw);
22078
22781
  return page.publishedCount;
22079
22782
  }
22080
22783
  async fillGroupGap(groupId) {
22081
22784
  const client = this.runtime.client;
22785
+ const generation = this.captureInlineGeneration();
22082
22786
  if (client._sessionOptions?.background_sync === false) return;
22083
22787
  if (client.state !== "ready" /* READY */ || client._closing) return;
22084
22788
  groupId = String(groupId ?? "").trim();
@@ -22087,27 +22791,47 @@ var MessageDeliveryEngine = class {
22087
22791
  const afterSeq = client._seqTracker.getContiguousSeq(ns);
22088
22792
  const dedupKey = `group_pull:${ns}`;
22089
22793
  if (client._gapFillDone.has(dedupKey)) return;
22794
+ this.recordBackgroundPullDemand(ns, 0, true);
22090
22795
  client._gapFillDone.add(dedupKey);
22091
22796
  this.runtime.delivery.setGapFillActive(true);
22092
22797
  let filled = 0;
22798
+ let failed = false;
22093
22799
  try {
22094
22800
  const key = client._rpcPipeline.pullGateKeyForCall("group.pull", { group_id: groupId, after_seq: afterSeq, limit: 50 });
22095
22801
  const messages = await client._rpcPipeline.runPullSerialized(
22096
22802
  key,
22097
- () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true }),
22803
+ async () => {
22804
+ if (client._v2Session) return await client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true });
22805
+ const result = await client._rpcPipeline.rawCall("group.pull", {
22806
+ group_id: groupId,
22807
+ after_message_seq: afterSeq,
22808
+ device_id: client._deviceId,
22809
+ slot_id: client._slotId,
22810
+ limit: 50,
22811
+ _rpc_background: true
22812
+ }, { background: true });
22813
+ if (!this.isInlineGenerationCurrent(generation)) throw new Error("pull invalidated");
22814
+ if (isJsonObject(result)) this.observeBackgroundPullResponse(ns, afterSeq, result, 50);
22815
+ await this.commitPlainForwardPage(ns, afterSeq, "group.pull", result, groupId);
22816
+ return isJsonObject(result) && Array.isArray(result.messages) ? result.messages : [];
22817
+ },
22098
22818
  true
22099
22819
  );
22100
22820
  filled = messages.length;
22101
22821
  this.prunePushedSeqs(ns);
22102
22822
  } catch (exc) {
22823
+ failed = true;
22103
22824
  client._clientLog.warn(`group message gap-fill failed:${String(exc)}`);
22104
22825
  } finally {
22105
- client._gapFillDone.delete(dedupKey);
22106
- this.runtime.delivery.setGapFillActive(false);
22107
- if (filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
22108
- client._safeAsync(this.fillGroupGap(groupId));
22826
+ if (this.isInlineGenerationCurrent(generation)) {
22827
+ client._gapFillDone.delete(dedupKey);
22828
+ this.runtime.delivery.setGapFillActive(false);
22829
+ if (filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
22830
+ this.recordBackgroundPullDemand(ns, 0, true);
22831
+ }
22832
+ this.schedulePendingPullIfNeeded(ns, "gap-fill-complete");
22833
+ this.finishBackgroundPull(ns, afterSeq, failed);
22109
22834
  }
22110
- this.schedulePendingPullIfNeeded(ns, "gap-fill-complete");
22111
22835
  }
22112
22836
  }
22113
22837
  async fillGroupEventGap(groupId, options) {
@@ -22123,6 +22847,7 @@ var MessageDeliveryEngine = class {
22123
22847
  const singlePage = options?.singlePage === true;
22124
22848
  const dedupKey = `group_event_pull:${ns}`;
22125
22849
  if (client._gapFillDone.has(dedupKey)) return;
22850
+ this.recordBackgroundPullDemand(ns, 0, true);
22126
22851
  client._gapFillDone.add(dedupKey);
22127
22852
  this.runtime.delivery.setGapFillActive(true);
22128
22853
  let continuationAfterSeq = 0;
@@ -22142,6 +22867,8 @@ var MessageDeliveryEngine = class {
22142
22867
  _rpc_background: true
22143
22868
  };
22144
22869
  const result = await client.call("group.pull_events", request);
22870
+ if (!this.isInlineGenerationCurrent(generation)) throw new Error("pull invalidated");
22871
+ if (isJsonObject(result)) this.observeBackgroundPullResponse(ns, nextAfterSeq, result, 50);
22145
22872
  if (!isJsonObject(result)) return;
22146
22873
  const events = result.events;
22147
22874
  if (!Array.isArray(events)) return;
@@ -22191,7 +22918,10 @@ var MessageDeliveryEngine = class {
22191
22918
  const deliveryThrough = Math.max(pageContigBefore, contiguousSeq ?? 0, effectiveFloor, ...eventSeqs);
22192
22919
  await this.drainOrderedMessages(ns, void 0, false, false, void 0, "pull", deliveryThrough);
22193
22920
  if (!this.isInlineGenerationCurrent(generation)) throw new Error("pull invalidated");
22194
- if (hasDissolvedEvent) break;
22921
+ if (hasDissolvedEvent) {
22922
+ this.backgroundPullDemands.delete(ns);
22923
+ break;
22924
+ }
22195
22925
  if (contiguousSeq === void 0) client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq);
22196
22926
  else client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq, contiguousSeq);
22197
22927
  if (effectiveFloor > client._seqTracker.getContiguousSeq(ns)) {
@@ -22242,14 +22972,11 @@ var MessageDeliveryEngine = class {
22242
22972
  failed = true;
22243
22973
  client._clientLog.warn(`group event gap-fill failed:${String(exc)}`);
22244
22974
  } finally {
22245
- client._gapFillDone.delete(dedupKey);
22246
- this.runtime.delivery.setGapFillActive(false);
22247
- if (!client._rpcPipeline) this.syncStopped(failed ? "aborted" : "pull_drained");
22248
- if (singlePage && continuationAfterSeq > afterSeq) {
22249
- this.enqueueOnlineUnreadEventHint({
22250
- group_id: groupId,
22251
- _online_hint_after_event_seq: continuationAfterSeq
22252
- });
22975
+ if (this.isInlineGenerationCurrent(generation)) {
22976
+ client._gapFillDone.delete(dedupKey);
22977
+ this.runtime.delivery.setGapFillActive(false);
22978
+ if (!client._rpcPipeline) this.syncStopped(failed ? "aborted" : "pull_drained");
22979
+ this.finishBackgroundPull(ns, afterSeq, failed);
22253
22980
  }
22254
22981
  }
22255
22982
  }
@@ -22301,6 +23028,9 @@ var MessageDeliveryEngine = class {
22301
23028
  const groupId = String(data.group_id ?? "").trim();
22302
23029
  if (!groupId) return;
22303
23030
  const ns = `group:${groupId}`;
23031
+ const generation = this.captureInlineGeneration();
23032
+ const afterSeq = this.syncState(ns).ack;
23033
+ this.recordBackgroundPullDemand(ns, positiveSafeSequenceHint(data.seq), true);
22304
23034
  const targets = this.onlineUnreadHintTargets ?? /* @__PURE__ */ new Map();
22305
23035
  const owners = this.onlineUnreadHintOwners ?? /* @__PURE__ */ new Set();
22306
23036
  this.onlineUnreadHintTargets = targets;
@@ -22309,20 +23039,26 @@ var MessageDeliveryEngine = class {
22309
23039
  if (owners.has(ns)) return;
22310
23040
  owners.add(ns);
22311
23041
  client._safeAsync((async () => {
23042
+ let failed = false;
23043
+ let attemptAfterSeq = afterSeq;
22312
23044
  try {
22313
23045
  while (true) {
22314
23046
  const throughSeq = targets.get(ns) ?? 0;
22315
23047
  const ackBefore = this.syncState(ns).ack;
23048
+ attemptAfterSeq = ackBefore;
22316
23049
  await this.runGroupForwardRecovery(groupId, ns, 50, true, throughSeq);
23050
+ if (!this.isInlineGenerationCurrent(generation)) return;
22317
23051
  const ackAfter = this.syncState(ns).ack;
22318
23052
  const target = targets.get(ns) ?? 0;
22319
23053
  if (ackAfter >= target || ackAfter <= ackBefore) return;
22320
23054
  }
22321
23055
  } catch (exc) {
23056
+ failed = true;
22322
23057
  client._clientLog?.debug(`online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
22323
23058
  } finally {
22324
23059
  if (this.onlineUnreadHintTargets === targets) targets.delete(ns);
22325
23060
  if (this.onlineUnreadHintOwners === owners) owners.delete(ns);
23061
+ if (this.isInlineGenerationCurrent(generation)) this.finishBackgroundPull(ns, attemptAfterSeq, failed);
22326
23062
  }
22327
23063
  })());
22328
23064
  }
@@ -22330,6 +23066,7 @@ var MessageDeliveryEngine = class {
22330
23066
  const client = this.runtime.client;
22331
23067
  const groupId = String(data.group_id ?? "").trim();
22332
23068
  if (!groupId || client._sessionOptions?.background_sync === false) return;
23069
+ this.recordBackgroundPullDemand(`group_event:${groupId}`, positiveSafeSequenceHint(data.event_seq), true);
22333
23070
  const rawAfter = Number(data._online_hint_after_event_seq ?? 0);
22334
23071
  const afterSeq = Number.isSafeInteger(rawAfter) && rawAfter > 0 ? rawAfter : 0;
22335
23072
  client._safeAsync(this.fillGroupEventGap(groupId, {
@@ -22917,19 +23654,26 @@ var MessageDeliveryEngine = class {
22917
23654
  }
22918
23655
  return;
22919
23656
  }
23657
+ if (client._sessionOptions?.background_sync !== false) this.recordBackgroundPullDemand(ns, seq2);
22920
23658
  const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
22921
23659
  if (pullActive || !this.beginRealtimeSync(ns)) {
22922
23660
  this.recordPendingPull(ns, seq2);
22923
23661
  client._clientLog?.debug(`V2 group push queued behind in-flight realtime sync: ns=${ns} seq=${seq2}`);
22924
23662
  return;
22925
23663
  }
23664
+ const generation = this.captureInlineGeneration();
23665
+ let failed = false;
22926
23666
  try {
22927
23667
  await this.runGroupRealtimeSync(groupId, ns, seq2, 50, false);
22928
23668
  } catch (exc) {
23669
+ failed = true;
22929
23670
  client._clientLog.warn(`V2 group push auto-sync failed: group=${groupId} err=${formatDeliveryError(exc)}`);
22930
23671
  } finally {
22931
- this.endRealtimeSync(ns);
22932
- this.schedulePendingPullIfNeeded(ns, "push-auto-pull-complete");
23672
+ if (this.isInlineGenerationCurrent(generation)) {
23673
+ this.endRealtimeSync(ns);
23674
+ this.schedulePendingPullIfNeeded(ns, "push-auto-pull-complete");
23675
+ this.finishBackgroundPull(ns, before.ack, failed);
23676
+ }
22933
23677
  }
22934
23678
  }
22935
23679
  async onV2PushNotification(data) {
@@ -22995,28 +23739,51 @@ var MessageDeliveryEngine = class {
22995
23739
  }
22996
23740
  return;
22997
23741
  }
23742
+ if (client._sessionOptions?.background_sync !== false) this.recordBackgroundPullDemand(ns, pushSeq);
22998
23743
  const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
22999
23744
  if (pullActive || !this.beginRealtimeSync(ns)) {
23000
23745
  this.recordPendingPull(ns, pushSeq);
23001
23746
  client._clientLog?.debug(`V2 P2P push queued behind in-flight realtime sync: ns=${ns} seq=${pushSeq}`);
23002
23747
  return;
23003
23748
  }
23749
+ const generation = this.captureInlineGeneration();
23750
+ let failed = false;
23004
23751
  try {
23005
23752
  await this.runP2PRealtimeSync(ns, pushSeq, 50, false);
23006
23753
  } catch (exc) {
23754
+ failed = true;
23007
23755
  client._clientLog.warn(`V2 P2P push auto-sync failed: ${formatDeliveryError(exc)}`);
23008
23756
  } finally {
23009
- this.endRealtimeSync(ns);
23010
- this.schedulePendingPullIfNeeded(ns, "push-auto-pull-complete");
23757
+ if (this.isInlineGenerationCurrent(generation)) {
23758
+ this.endRealtimeSync(ns);
23759
+ this.schedulePendingPullIfNeeded(ns, "push-auto-pull-complete");
23760
+ this.finishBackgroundPull(ns, before.ack, failed);
23761
+ }
23011
23762
  }
23012
23763
  }
23013
23764
  enqueueOnlineUnreadP2pHint(data) {
23014
23765
  const client = this.runtime.client;
23015
- if (!client._aid || !client._v2Session) return;
23766
+ if (!client._aid || !client._v2Session || client._sessionOptions?.background_sync === false) return;
23016
23767
  const ns = `p2p:${client._aid}`;
23017
- client._safeAsync(this.runP2PForwardRecovery(ns, 50, true).catch((exc) => {
23018
- client._clientLog?.debug(`P2P online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
23019
- }));
23768
+ const generation = this.captureInlineGeneration();
23769
+ const afterSeq = this.syncState(ns).ack;
23770
+ this.recordBackgroundPullDemand(ns, positiveSafeSequenceHint(data.seq), true);
23771
+ const owners = this.onlineUnreadHintOwners ?? /* @__PURE__ */ new Set();
23772
+ this.onlineUnreadHintOwners = owners;
23773
+ if (owners.has(ns)) return;
23774
+ owners.add(ns);
23775
+ client._safeAsync((async () => {
23776
+ let failed = false;
23777
+ try {
23778
+ await this.runP2PForwardRecovery(ns, 50, true);
23779
+ } catch (exc) {
23780
+ failed = true;
23781
+ client._clientLog?.debug(`P2P online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
23782
+ } finally {
23783
+ if (this.onlineUnreadHintOwners === owners) owners.delete(ns);
23784
+ if (this.isInlineGenerationCurrent(generation)) this.finishBackgroundPull(ns, afterSeq, failed);
23785
+ }
23786
+ })());
23020
23787
  }
23021
23788
  async restoreSeqTrackerState() {
23022
23789
  const client = this.runtime.client;
@@ -24382,6 +25149,7 @@ async function signingCertFingerprint(certPem) {
24382
25149
  }
24383
25150
  var PULL_GATE_STALE_MS = 35e3;
24384
25151
  var PULL_GATE_OPERATION_TIMEOUT_MS = 35e3;
25152
+ var PULL_GATE_MAX_ACTIVE = 4;
24385
25153
  function sameIdentityAdmissionRejection(error, original) {
24386
25154
  const errorCode2 = Number(error?.code);
24387
25155
  const originalCode = Number(original?.code);
@@ -24880,6 +25648,11 @@ var RpcPipeline = class {
24880
25648
  new TextEncoder().encode(paramsJson)
24881
25649
  );
24882
25650
  const paramsHash = Array.from(new Uint8Array(paramsHashBuf)).map((b) => b.toString(16).padStart(2, "0")).join("");
25651
+ if (params2.client_signature !== void 0) {
25652
+ const existing = params2.client_signature;
25653
+ if (!isJsonObject(existing) || existing.aid !== aid2 || existing.cert_fingerprint !== await signingCertFingerprint(certPem) || existing.params_hash !== paramsHash || typeof existing.timestamp !== "string" || !/^\d+$/.test(existing.timestamp) || typeof existing.signature !== "string" || !await verifySignatureWithCert(certPem, existing.signature, new TextEncoder().encode(`${method}|${aid2}|${existing.timestamp}|${paramsHash}`))) throw new Error("\u5DF2\u6709\u5BA2\u6237\u7AEF\u7B7E\u540D\u4E0E\u8BF7\u6C42\u5185\u5BB9\u4E0D\u4E00\u81F4");
25654
+ return;
25655
+ }
24883
25656
  const signData = new TextEncoder().encode(`${method}|${aid2}|${ts}|${paramsHash}`);
24884
25657
  const cryptoKey = await signingPrivateKey(currentAid.privateKeyPem);
24885
25658
  const sigP1363 = await crypto.subtle.sign(
@@ -24943,7 +25716,7 @@ var RpcPipeline = class {
24943
25716
  const name = this.pullGateName(key);
24944
25717
  let state = this.pullGateStates.get(name);
24945
25718
  if (!state) {
24946
- state = { active: null, foreground: [], background: [], byKey: /* @__PURE__ */ new Map(), lifecycle: /* @__PURE__ */ new Map(), watchdog: null };
25719
+ state = { activeByNamespace: /* @__PURE__ */ new Map(), foreground: [], background: [], byKey: /* @__PURE__ */ new Map(), lifecycle: /* @__PURE__ */ new Map(), watchdog: null };
24947
25720
  this.pullGateStates.set(name, state);
24948
25721
  }
24949
25722
  this.startPullGateWatchdog(state, name);
@@ -24971,19 +25744,21 @@ var RpcPipeline = class {
24971
25744
  }
24972
25745
  }
24973
25746
  checkPullGate(state, name) {
24974
- const active = state.active;
24975
- if (!active) {
25747
+ const activeJobs = [...state.activeByNamespace.values()];
25748
+ if (activeJobs.length === 0) {
24976
25749
  this.drainPullGate(state);
24977
25750
  return;
24978
25751
  }
24979
- if (!active.pullingStartedAt || Date.now() - active.pullingStartedAt < this.pullGateOperationTimeoutMs()) return;
24980
- if (active.timedOut) return;
24981
- active.timedOut = true;
24982
- active.invalidated = true;
24983
- active.cancel?.();
24984
- const err = new TimeoutError(`pull gate timeout: ${active.namespace}`, { retryable: true });
24985
- this.runtime.client._clientLog?.warn(`pull gate watchdog timeout: gate=${name} key=${active.key}`);
24986
- active.reject(err);
25752
+ for (const active of activeJobs) {
25753
+ if (!active.pullingStartedAt || Date.now() - active.pullingStartedAt < this.pullGateOperationTimeoutMs()) continue;
25754
+ if (active.timedOut) continue;
25755
+ active.timedOut = true;
25756
+ active.invalidated = true;
25757
+ active.cancel?.();
25758
+ const err = new TimeoutError(`pull gate timeout: ${active.namespace}`, { retryable: true });
25759
+ this.runtime.client._clientLog?.warn(`pull gate watchdog timeout: gate=${name} key=${active.key}`);
25760
+ active.reject(err);
25761
+ }
24987
25762
  }
24988
25763
  realtimePullNamespace(key) {
24989
25764
  for (const marker of ["|tail|", "|forward|"]) {
@@ -25001,7 +25776,7 @@ var RpcPipeline = class {
25001
25776
  const key = gateKey || this.pullGateKeyForCall(method, params2);
25002
25777
  if (typeof cancel !== "function" || !key) return request;
25003
25778
  const state = this.pullGateStates.get(this.pullGateName(key));
25004
- const active = state?.active;
25779
+ const active = [...state?.activeByNamespace.values() ?? []].find((candidate) => candidate.key === key);
25005
25780
  if (!active || active.key !== key || active.timedOut || active.invalidated) return request;
25006
25781
  const boundCancel = () => cancel.call(request);
25007
25782
  active.cancel = boundCancel;
@@ -25027,7 +25802,7 @@ var RpcPipeline = class {
25027
25802
  if (!job) return false;
25028
25803
  return this.realtimePullNamespace(job.key) === normalized;
25029
25804
  };
25030
- return matches(state.active) || state.foreground.some(matches) || state.background.some(matches);
25805
+ return [...state.activeByNamespace.values()].some(matches) || state.foreground.some(matches) || state.background.some(matches);
25031
25806
  }
25032
25807
  hasPullActivity(namespace, includePending = true) {
25033
25808
  const ns = String(namespace ?? "").trim();
@@ -25048,12 +25823,12 @@ var RpcPipeline = class {
25048
25823
  const state = this.pullGateStates.get(this.pullGateName(ns));
25049
25824
  if (!state) return false;
25050
25825
  const matches = (job) => job !== null && job.namespace === ns;
25051
- return matches(state.active) || state.foreground.some(matches) || state.background.some(matches);
25826
+ return [...state.activeByNamespace.values()].some(matches) || state.foreground.some(matches) || state.background.some(matches);
25052
25827
  }
25053
25828
  hasAnyPullActivity() {
25054
25829
  if (this.pullWorkTokens.size > 0) return true;
25055
25830
  for (const state of this.pullGateStates.values()) {
25056
- if (state.active || state.foreground.length > 0 || state.background.length > 0) return true;
25831
+ if (state.activeByNamespace.size > 0 || state.foreground.length > 0 || state.background.length > 0) return true;
25057
25832
  }
25058
25833
  return false;
25059
25834
  }
@@ -25089,7 +25864,7 @@ var RpcPipeline = class {
25089
25864
  }
25090
25865
  const key = `${normalized}|inline|seq=${seq2}`;
25091
25866
  const state = this.pullGateState(key);
25092
- if (state.active !== null || state.foreground.length > 0 || state.background.length > 0) {
25867
+ if (state.activeByNamespace.has(normalized) || state.foreground.some((job) => job.namespace === normalized) || state.background.some((job) => job.namespace === normalized)) {
25093
25868
  return { acquired: false };
25094
25869
  }
25095
25870
  const guard = {
@@ -25121,7 +25896,7 @@ var RpcPipeline = class {
25121
25896
  return { acquired: true, value: await operation() };
25122
25897
  } finally {
25123
25898
  this.inlineRealtimeScopes.delete(normalized);
25124
- if (state.active === guard) state.active = null;
25899
+ if (state.activeByNamespace.get(normalized) === guard) state.activeByNamespace.delete(normalized);
25125
25900
  this.drainPullGate(state);
25126
25901
  this.runtime.client._delivery?.onPullGateIdle?.(normalized);
25127
25902
  this.armQueuedPullTimers(state);
@@ -25131,7 +25906,7 @@ var RpcPipeline = class {
25131
25906
  if (!key) return 0;
25132
25907
  if (this.inlineRealtimeScopes.has(this.pullScopeKey(key))) return null;
25133
25908
  const serialized = this.pullGateStates.get(this.pullGateName(key));
25134
- if (serialized?.active?.inline === true) return null;
25909
+ if ([...serialized?.activeByNamespace.values() ?? []].some((job) => job.inline === true)) return null;
25135
25910
  const client = this.runtime.client;
25136
25911
  const now = Date.now();
25137
25912
  const gate = client._pullGates.get(key) ?? { inflight: false, startedAt: 0, token: 0 };
@@ -25157,7 +25932,7 @@ var RpcPipeline = class {
25157
25932
  isPullNamespaceCurrent(namespace) {
25158
25933
  const ns = this.pullScopeKey(namespace);
25159
25934
  if (!ns) return true;
25160
- const active = this.pullGateStates.get(this.pullGateName(ns))?.active;
25935
+ const active = this.pullGateStates.get(this.pullGateName(ns))?.activeByNamespace.get(ns);
25161
25936
  return !active || active.namespace !== ns || !active.timedOut && !active.invalidated;
25162
25937
  }
25163
25938
  invalidatePulls() {
@@ -25166,7 +25941,7 @@ var RpcPipeline = class {
25166
25941
  const waits = [];
25167
25942
  for (const state of this.pullGateStates.values()) {
25168
25943
  const jobs = /* @__PURE__ */ new Set();
25169
- if (state.active) jobs.add(state.active);
25944
+ for (const active of state.activeByNamespace.values()) jobs.add(active);
25170
25945
  for (const job of state.foreground) jobs.add(job);
25171
25946
  for (const job of state.background) jobs.add(job);
25172
25947
  for (const job of jobs) {
@@ -25223,7 +25998,7 @@ var RpcPipeline = class {
25223
25998
  const candidate = state.byKey.get(key);
25224
25999
  const existing = candidate && !candidate.timedOut && !candidate.invalidated ? candidate : null;
25225
26000
  if (existing) {
25226
- if (!background && existing.background && existing !== state.active) {
26001
+ if (!background && existing.background && ![...state.activeByNamespace.values()].includes(existing)) {
25227
26002
  const index = state.background.indexOf(existing);
25228
26003
  if (index >= 0) state.background.splice(index, 1);
25229
26004
  existing.background = false;
@@ -25284,8 +26059,8 @@ var RpcPipeline = class {
25284
26059
  if (!key) return;
25285
26060
  const state = this.pullGateState(key);
25286
26061
  const namespace = this.pullScopeKey(key);
25287
- const job = state.active?.key === key ? state.active : null;
25288
- if (!job || state.active !== job) return;
26062
+ const job = [...state.activeByNamespace.values()].find((candidate) => candidate.key === key) ?? null;
26063
+ if (!job) return;
25289
26064
  const replacementKey = String(nextKey || key);
25290
26065
  if (state.byKey.get(job.key) === job) state.byKey.delete(job.key);
25291
26066
  job.key = replacementKey;
@@ -25297,7 +26072,7 @@ var RpcPipeline = class {
25297
26072
  job.resume = new Promise((resolve) => {
25298
26073
  job.resumeResolve = resolve;
25299
26074
  });
25300
- state.active = null;
26075
+ state.activeByNamespace.delete(namespace);
25301
26076
  (job.background ? state.background : state.foreground).push(job);
25302
26077
  this.drainPullGate(state);
25303
26078
  if (job.resume) {
@@ -25314,56 +26089,57 @@ var RpcPipeline = class {
25314
26089
  const ns = this.pullScopeKey(namespace);
25315
26090
  if (!ns) return;
25316
26091
  const state = this.pullGateStates.get(this.pullGateName(ns));
25317
- if (!state || state.active?.namespace !== ns) return;
26092
+ if (!state || !state.activeByNamespace.has(ns)) return;
25318
26093
  state.lifecycle.set(ns, lifecycle);
25319
26094
  }
25320
26095
  drainPullGate(state) {
25321
- if (state.active) return;
25322
26096
  const takeRunnable = (queue) => {
25323
- const index = queue.findIndex((candidate) => !this.inlineRealtimeScopes.has(candidate.namespace));
26097
+ const index = queue.findIndex((candidate) => !this.inlineRealtimeScopes.has(candidate.namespace) && !state.activeByNamespace.has(candidate.namespace));
25324
26098
  return index >= 0 ? queue.splice(index, 1)[0] : void 0;
25325
26099
  };
25326
- const foregroundJob = takeRunnable(state.foreground);
25327
- const foregroundDrained = foregroundJob !== void 0 && state.foreground.length === 0;
25328
- const job = foregroundJob ?? takeRunnable(state.background);
25329
- if (!job) return;
25330
- state.active = job;
25331
- this.runtime.client._delivery?.onPullStarted?.(job.namespace);
25332
- if (foregroundDrained) this.runtime.client._delivery?.onForegroundPullsDrained?.();
25333
- if (job.timer) clearTimeout(job.timer);
25334
- job.pullingStartedAt = Date.now();
25335
- job.timedOut = false;
25336
- state.lifecycle.set(job.namespace, "pulling");
25337
- const resumeResolve = job.resumeResolve;
25338
- job.resume = null;
25339
- job.resumeResolve = null;
25340
- if (job.suspended) {
25341
- job.suspended = false;
25342
- resumeResolve?.();
25343
- return;
25344
- }
25345
- job.running = true;
25346
- let failed = false;
25347
- void this.executePullOperation(job.operation, job.background).then(
25348
- (value) => {
25349
- if (!job.timedOut && !job.invalidated) job.resolve(value);
25350
- },
25351
- (error) => {
25352
- failed = true;
25353
- if (!job.timedOut && !job.invalidated) job.reject(error);
26100
+ while (state.activeByNamespace.size < PULL_GATE_MAX_ACTIVE) {
26101
+ const foregroundJob = takeRunnable(state.foreground);
26102
+ const foregroundDrained = foregroundJob !== void 0 && state.foreground.length === 0;
26103
+ const job = foregroundJob ?? takeRunnable(state.background);
26104
+ if (!job) break;
26105
+ state.activeByNamespace.set(job.namespace, job);
26106
+ this.runtime.client._delivery?.onPullStarted?.(job.namespace);
26107
+ if (foregroundDrained) this.runtime.client._delivery?.onForegroundPullsDrained?.();
26108
+ if (job.timer) clearTimeout(job.timer);
26109
+ job.pullingStartedAt = Date.now();
26110
+ job.timedOut = false;
26111
+ state.lifecycle.set(job.namespace, "pulling");
26112
+ const resumeResolve = job.resumeResolve;
26113
+ job.resume = null;
26114
+ job.resumeResolve = null;
26115
+ if (job.suspended) {
26116
+ job.suspended = false;
26117
+ resumeResolve?.();
26118
+ continue;
25354
26119
  }
25355
- ).finally(() => {
25356
- this.pullWorkTokens.delete(job.workToken);
25357
- state.lifecycle.set(job.namespace, "idle");
25358
- if (state.active === job) state.active = null;
25359
- if (state.byKey.get(job.key) === job) state.byKey.delete(job.key);
25360
- this.drainPullGate(state);
25361
- job.settledResolve?.();
25362
- job.settledResolve = null;
25363
- if (!job.invalidated) this.runtime.client._delivery?.onPullGateIdle?.(job.namespace);
25364
- if (failed || job.invalidated) this.runtime.client._delivery?.onPullAborted?.();
25365
- this.runtime.client._delivery?.onPullWorkSettled?.();
25366
- });
26120
+ job.running = true;
26121
+ let failed = false;
26122
+ void this.executePullOperation(job.operation, job.background).then(
26123
+ (value) => {
26124
+ if (!job.timedOut && !job.invalidated) job.resolve(value);
26125
+ },
26126
+ (error) => {
26127
+ failed = true;
26128
+ if (!job.timedOut && !job.invalidated) job.reject(error);
26129
+ }
26130
+ ).finally(() => {
26131
+ this.pullWorkTokens.delete(job.workToken);
26132
+ state.lifecycle.set(job.namespace, "idle");
26133
+ if (state.activeByNamespace.get(job.namespace) === job) state.activeByNamespace.delete(job.namespace);
26134
+ if (state.byKey.get(job.key) === job) state.byKey.delete(job.key);
26135
+ this.drainPullGate(state);
26136
+ job.settledResolve?.();
26137
+ job.settledResolve = null;
26138
+ if (!job.invalidated) this.runtime.client._delivery?.onPullGateIdle?.(job.namespace);
26139
+ if (failed || job.invalidated) this.runtime.client._delivery?.onPullAborted?.();
26140
+ this.runtime.client._delivery?.onPullWorkSettled?.();
26141
+ });
26142
+ }
25367
26143
  }
25368
26144
  armQueuedPullTimers(_state) {
25369
26145
  }
@@ -27499,8 +28275,8 @@ function stripNil2(params2, keepNull = false) {
27499
28275
  }
27500
28276
  return out;
27501
28277
  }
27502
- function splitAidStore(params2) {
27503
- const out = stripNil2(params2);
28278
+ function splitAidStore(params2, keepNull = false) {
28279
+ const out = stripNil2(params2, keepNull);
27504
28280
  const aidStore = out.aidStore ?? out.aid_store;
27505
28281
  delete out.aidStore;
27506
28282
  delete out.aid_store;
@@ -28230,7 +29006,7 @@ var CredentialFacade = class extends RpcFacade {
28230
29006
  };
28231
29007
  var OrgLifecycleFacade = class extends RpcFacade {
28232
29008
  create(params2) {
28233
- const split = splitAidStore(params2);
29009
+ const split = splitAidStore(params2, true);
28234
29010
  return split.aidStore && this.client.createOrg ? this.client.createOrg(split.params, { aidStore: split.aidStore }) : this.call("org.lifecycle.create", split.params);
28235
29011
  }
28236
29012
  get(params2) {
@@ -28249,14 +29025,14 @@ var OrgLifecycleFacade = class extends RpcFacade {
28249
29025
  return this.call("org.lifecycle.resume", params2);
28250
29026
  }
28251
29027
  dissolve(params2) {
28252
- const split = splitAidStore(params2);
29028
+ const split = splitAidStore(params2, true);
28253
29029
  return split.aidStore && this.client.dissolveOrg ? this.client.dissolveOrg(split.params, { aidStore: split.aidStore }) : this.call("org.lifecycle.dissolve", split.params);
28254
29030
  }
28255
29031
  transfer(params2) {
28256
29032
  return this.call("org.lifecycle.transfer", params2);
28257
29033
  }
28258
29034
  transferAccept(params2) {
28259
- const split = splitAidStore(params2);
29035
+ const split = splitAidStore(params2, true);
28260
29036
  return split.aidStore && this.client.completeOrgTransfer ? this.client.completeOrgTransfer(split.params, { aidStore: split.aidStore }) : this.call("org.lifecycle.transferAccept", split.params);
28261
29037
  }
28262
29038
  signTransferAcceptance(proposal, signer) {
@@ -32371,10 +33147,8 @@ function deduplicateForwardPageMessages(value, afterSeq) {
32371
33147
  }
32372
33148
  return [...bySeq.entries()].sort(([a], [b]) => a - b).map(([, row]) => row);
32373
33149
  }
32374
- function contiguousPullSeq(afterSeq, seqs, proof) {
32375
- let contiguous = seqs.length > 0 ? Math.max(afterSeq, ...seqs) : afterSeq;
32376
- const serverProof = safeNonNegativeSeq(proof);
32377
- return serverProof !== null ? Math.max(contiguous, serverProof) : contiguous;
33150
+ function contiguousPullSeq(afterSeq, seqs, _proof) {
33151
+ return seqs.length > 0 ? Math.max(afterSeq, ...seqs) : afterSeq;
32378
33152
  }
32379
33153
  function deduplicatePulledResults(messages) {
32380
33154
  const seen = /* @__PURE__ */ new Set();
@@ -32399,7 +33173,7 @@ function shouldContinueForwardPage({
32399
33173
  observedUpper
32400
33174
  }) {
32401
33175
  if (rawCount <= 0 || nextAfter <= nextAfterSeq) return false;
32402
- if (hasMore || serverHead > nextAfter || pendingAckSeq > 0 || observedUpper > nextAfter) return true;
33176
+ if (hasMore || serverHead > nextAfter || observedUpper > nextAfter) return true;
32403
33177
  return !hasMoreField && limit > 0 && rawCount >= limit;
32404
33178
  }
32405
33179
  function validateTailPage(result, afterSeq, pageLimit) {
@@ -33565,13 +34339,13 @@ var V2E2EECoordinator = class {
33565
34339
  let pageCount = 0;
33566
34340
  let lastAutoAckSeq = 0;
33567
34341
  let pendingAckSeq = deferredForwardAck;
34342
+ let committedAckSeq = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
33568
34343
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
33569
34344
  let pipelineRunning = false;
33570
34345
  let pipelineError = null;
33571
- const pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
34346
+ let pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
33572
34347
  let pipelineProducer = null;
33573
34348
  let pipelineStopped = false;
33574
- let pipelineResume = null;
33575
34349
  const startPipeline = (startAfterSeq, startVolatileCursor) => {
33576
34350
  if (pipelineRunning || pageCount >= maxPages || limit < V2_PULL_MAX_PAGE_LIMIT) return;
33577
34351
  pipelineRunning = true;
@@ -33580,29 +34354,29 @@ var V2E2EECoordinator = class {
33580
34354
  let producerVolatileCursor = startVolatileCursor;
33581
34355
  for (let produced = pageCount; produced < maxPages; produced += 1) {
33582
34356
  if (pipelineStopped) break;
34357
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
33583
34358
  const requestedVolatileCursor = producerVolatileCursor;
34359
+ const ackUpToSeq = Math.min(pendingAckSeq, committedAckSeq);
34360
+ const inlineAckGeneration = ackUpToSeq > 0 ? client._delivery?.pendingP2PInlineAckGeneration?.(ns) ?? null : null;
33584
34361
  const result = await client._callRawV2Rpc("message.v2.pull", {
33585
34362
  after_seq: producerAfterSeq,
33586
34363
  limit,
33587
34364
  ...producerVolatileCursor ? { after_volatile_cursor: producerVolatileCursor } : {},
33588
- ...opts?.force ? { force: true } : {}
34365
+ ...opts?.force ? { force: true } : {},
34366
+ ...ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}
33589
34367
  }, pullGateKey);
33590
34368
  if (pipelineStopped) break;
34369
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
33591
34370
  const rawPageMessages = Array.isArray(result?.messages) ? result.messages : [];
33592
- const completed = new Promise((resolve) => {
33593
- pipelineResume = resolve;
33594
- });
33595
34371
  const enqueued = await pipelineQueue.put({
33596
34372
  result,
33597
34373
  afterSeq: producerAfterSeq,
33598
- ackUpToSeq: 0,
33599
- inlineAckGeneration: null,
34374
+ ackUpToSeq,
34375
+ inlineAckGeneration,
33600
34376
  requestedVolatileCursor,
33601
34377
  weight: rawPageMessages.length
33602
34378
  }, rawPageMessages.length);
33603
34379
  if (!enqueued || pipelineStopped) break;
33604
- await completed;
33605
- if (pipelineStopped) break;
33606
34380
  const volatileMessages = client._delivery.volatileMessages(rawPageMessages);
33607
34381
  if (volatileMessages.length > 0) break;
33608
34382
  const messages = deduplicateForwardPageMessages(rawPageMessages, producerAfterSeq);
@@ -33632,8 +34406,6 @@ var V2E2EECoordinator = class {
33632
34406
  };
33633
34407
  const stopPipeline = async () => {
33634
34408
  pipelineStopped = true;
33635
- pipelineResume?.();
33636
- pipelineResume = null;
33637
34409
  pipelineQueue.close();
33638
34410
  if (pipelineProducer) {
33639
34411
  const producer = pipelineProducer;
@@ -33679,20 +34451,21 @@ var V2E2EECoordinator = class {
33679
34451
  continue;
33680
34452
  }
33681
34453
  }
34454
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
34455
+ if (!opts?.force) client._delivery.observeBackgroundPullResponse?.(ns, nextAfterSeq, result, limit);
33682
34456
  if (ackUpToSeq > 0) {
33683
34457
  const actualAckSeq = client._delivery?.resolveP2PPullAckSeq?.(result, ackUpToSeq) ?? 0;
33684
34458
  if (actualAckSeq >= ackUpToSeq) {
33685
34459
  if (pendingInlineGeneration !== null) {
33686
34460
  client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
33687
34461
  }
33688
- pendingAckSeq = 0;
34462
+ if (pendingAckSeq <= ackUpToSeq) pendingAckSeq = 0;
33689
34463
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
33690
34464
  this.clearForwardAck(ns, ackUpToSeq);
33691
34465
  } else {
33692
34466
  client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
33693
- await this.confirmForwardP2PAck(ackUpToSeq, pendingInlineGeneration ?? void 0);
33694
- pendingAckSeq = 0;
33695
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
34467
+ pendingAckSeq = Math.max(pendingAckSeq, ackUpToSeq);
34468
+ this.recordForwardAck(ns, ackUpToSeq);
33696
34469
  }
33697
34470
  }
33698
34471
  const pageMessages = Array.isArray(result?.messages) ? result.messages : [];
@@ -33717,6 +34490,17 @@ var V2E2EECoordinator = class {
33717
34490
  seqs,
33718
34491
  result.contiguous_seq
33719
34492
  );
34493
+ if (!pipelineRunning && volatileMessages.length === 0 && shouldContinueForwardPage({
34494
+ rawCount: messages.length,
34495
+ nextAfterSeq,
34496
+ nextAfter: pageMaxSeq,
34497
+ limit,
34498
+ hasMoreField,
34499
+ hasMore,
34500
+ serverHead,
34501
+ pendingAckSeq: 0,
34502
+ observedUpper: typeof client._seqTracker.getMaxSeenSeq === "function" ? client._seqTracker.getMaxSeenSeq(ns) : pageMaxSeq
34503
+ })) startPipeline(pageMaxSeq, volatileCursor);
33720
34504
  const deferredKeyFetches = /* @__PURE__ */ new Map();
33721
34505
  let blockedSeq = 0;
33722
34506
  const pageDecryptedBefore = decrypted.length;
@@ -33855,6 +34639,7 @@ var V2E2EECoordinator = class {
33855
34639
  if (pageCount === 1 && deferredServerCursor > 0) this.clearForwardCursor(ns, ackSeq);
33856
34640
  }
33857
34641
  client._delivery.completePendingTailWindow?.(ns);
34642
+ committedAckSeq = ackSeq;
33858
34643
  } catch (exc) {
33859
34644
  if (Number(client._v2RuntimeGeneration ?? 0) === generation && (inlineGeneration === void 0 || client._delivery.isInlineGenerationCurrent(inlineGeneration)) && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false && pageTrackerSnapshot !== void 0 && typeof client._seqTracker.restoreNamespaceSnapshot === "function") {
33860
34645
  client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, pageTrackerSnapshot);
@@ -33872,15 +34657,7 @@ var V2E2EECoordinator = class {
33872
34657
  const ackNeeded = ackSeq > 0 && ackSeq > lastAutoAckSeq && (hasServerAckSeq && ackSeq > serverAckSeq || contigAdvanced && ackSeq > knownServerAckSeq);
33873
34658
  if (ackNeeded) {
33874
34659
  this.recordForwardAck(ns, ackSeq);
33875
- const nextAfter2 = Math.max(pageContiguousSeq, nextAfterSeq);
33876
- const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter2 > nextAfterSeq;
33877
- if (canContinuePage && !pipelineRunning) {
33878
- pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
33879
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
33880
- } else {
33881
- await this.confirmForwardP2PAck(ackSeq);
33882
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
33883
- }
34660
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
33884
34661
  }
33885
34662
  }
33886
34663
  if (ns) client._delivery.onPullPage?.(
@@ -33924,15 +34701,14 @@ var V2E2EECoordinator = class {
33924
34701
  observedUpper: typeof client._seqTracker.getMaxSeenSeq === "function" ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter
33925
34702
  });
33926
34703
  if (!shouldContinue) break;
33927
- if (pipelinePage) {
33928
- pipelineResume?.();
33929
- pipelineResume = null;
34704
+ if (pipelinePage && volatileMessages.length > 0) {
34705
+ await stopPipeline();
34706
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
34707
+ pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
34708
+ pipelineStopped = false;
34709
+ pipelineError = null;
33930
34710
  }
33931
34711
  if (shouldContinueForPipeline && !volatileAdvanced && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
33932
- if (pendingAckSeq > 0) {
33933
- await this.confirmForwardP2PAck(pendingAckSeq);
33934
- pendingAckSeq = 0;
33935
- }
33936
34712
  startPipeline(nextAfter, volatileCursor);
33937
34713
  }
33938
34714
  if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
@@ -33946,7 +34722,7 @@ var V2E2EECoordinator = class {
33946
34722
  pullGateKey = nextKey;
33947
34723
  }
33948
34724
  const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
33949
- if (tailDelayMs > 0) await v2Sleep(tailDelayMs);
34725
+ if (!pipelineRunning && tailDelayMs > 0) await v2Sleep(tailDelayMs);
33950
34726
  nextAfterSeq = nextAfter;
33951
34727
  }
33952
34728
  } finally {
@@ -33954,10 +34730,11 @@ var V2E2EECoordinator = class {
33954
34730
  }
33955
34731
  if (pageCount >= maxPages) {
33956
34732
  client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
33957
- if (pendingAckSeq > 0) {
33958
- await this.confirmForwardP2PAck(pendingAckSeq);
33959
- pendingAckSeq = 0;
33960
- }
34733
+ }
34734
+ if (pendingAckSeq > 0) {
34735
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
34736
+ await this.confirmForwardP2PAck(pendingAckSeq);
34737
+ pendingAckSeq = 0;
33961
34738
  }
33962
34739
  client._delivery.onPullWorkSettled?.();
33963
34740
  return deduplicatePulledResults(decrypted);
@@ -34027,15 +34804,18 @@ var V2E2EECoordinator = class {
34027
34804
  }
34028
34805
  async confirmForwardP2PAck(upToSeq, inlineGeneration) {
34029
34806
  const ns = this.client._aid ? `p2p:${this.client._aid}` : "";
34807
+ const generation = Number(this.client._v2RuntimeGeneration ?? 0);
34808
+ const deliveryGeneration = this.client._delivery.captureInlineGeneration?.();
34030
34809
  try {
34031
34810
  const result = await this.ackV2ForForward(upToSeq, inlineGeneration);
34811
+ this.ensureDeliveryCurrent(ns, generation, deliveryGeneration);
34032
34812
  const actualAckSeq = this.client._delivery?.resolveForwardAckSeq?.(result, upToSeq) ?? 0;
34033
34813
  if (actualAckSeq < upToSeq) {
34034
34814
  throw new E2EEError(`message.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
34035
34815
  }
34036
34816
  this.clearForwardAck(ns, upToSeq);
34037
34817
  } catch (exc) {
34038
- this.recordForwardAck(ns, upToSeq);
34818
+ if (Number(this.client._v2RuntimeGeneration ?? 0) === generation && (deliveryGeneration === void 0 || this.client._delivery.isInlineGenerationCurrent(deliveryGeneration)) && this.client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false) this.recordForwardAck(ns, upToSeq);
34039
34819
  throw exc;
34040
34820
  }
34041
34821
  }
@@ -34323,13 +35103,13 @@ var V2E2EECoordinator = class {
34323
35103
  let pageCount = 0;
34324
35104
  let lastAutoAckSeq = 0;
34325
35105
  let pendingAckSeq = deferredForwardAck;
35106
+ let committedAckSeq = ownsCursor ? client._seqTracker.getContiguousSeq(ns) : 0;
34326
35107
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
34327
35108
  let pipelineRunning = false;
34328
35109
  let pipelineError = null;
34329
35110
  const pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
34330
35111
  let pipelineProducer = null;
34331
35112
  let pipelineStopped = false;
34332
- let pipelineResume = null;
34333
35113
  const startPipeline = (startAfterSeq) => {
34334
35114
  if (pipelineRunning || pageCount >= maxPages || limit < V2_PULL_MAX_PAGE_LIMIT || !ownsCursor) return;
34335
35115
  pipelineRunning = true;
@@ -34337,29 +35117,29 @@ var V2E2EECoordinator = class {
34337
35117
  let producerAfterSeq = startAfterSeq;
34338
35118
  for (let produced = pageCount; produced < maxPages; produced += 1) {
34339
35119
  if (pipelineStopped) break;
35120
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
35121
+ const ackUpToSeq = Math.min(pendingAckSeq, committedAckSeq);
35122
+ const inlineAckGeneration = ackUpToSeq > 0 ? client._delivery?.pendingGroupInlineAckGeneration?.(ns) ?? null : null;
34340
35123
  const result = await client._callRawV2Rpc("group.v2.pull", withExplicitGroupAid({
34341
35124
  group_id: wireGroupId,
34342
35125
  after_seq: producerAfterSeq,
34343
35126
  limit,
34344
35127
  ...cursorParams,
34345
- ...opts?.force ? { force: true } : {}
35128
+ ...opts?.force ? { force: true } : {},
35129
+ ...ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}
34346
35130
  }, groupAid), pullGateKey);
34347
35131
  if (pipelineStopped) break;
35132
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
34348
35133
  const pageMessages = Array.isArray(result?.messages) ? result.messages : [];
34349
- const completed = new Promise((resolve) => {
34350
- pipelineResume = resolve;
34351
- });
34352
35134
  const enqueued = await pipelineQueue.put({
34353
35135
  result,
34354
35136
  afterSeq: producerAfterSeq,
34355
- ackUpToSeq: 0,
34356
- inlineAckGeneration: null,
35137
+ ackUpToSeq,
35138
+ inlineAckGeneration,
34357
35139
  requestedVolatileCursor: "",
34358
35140
  weight: pageMessages.length
34359
35141
  }, pageMessages.length);
34360
35142
  if (!enqueued || pipelineStopped) break;
34361
- await completed;
34362
- if (pipelineStopped) break;
34363
35143
  const messages = deduplicateForwardPageMessages(pageMessages, producerAfterSeq);
34364
35144
  const seqs = messages.map((msg) => Number(msg.seq ?? 0)).filter((seq2) => Number.isFinite(seq2) && seq2 > 0);
34365
35145
  const nextAfter = Math.max(contiguousPullSeq(producerAfterSeq, seqs, result.contiguous_seq), producerAfterSeq);
@@ -34386,8 +35166,6 @@ var V2E2EECoordinator = class {
34386
35166
  };
34387
35167
  const stopPipeline = async () => {
34388
35168
  pipelineStopped = true;
34389
- pipelineResume?.();
34390
- pipelineResume = null;
34391
35169
  pipelineQueue.close();
34392
35170
  if (pipelineProducer) {
34393
35171
  const producer = pipelineProducer;
@@ -34424,20 +35202,21 @@ var V2E2EECoordinator = class {
34424
35202
  ...ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}
34425
35203
  }, groupAid), pullGateKey);
34426
35204
  }
35205
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
35206
+ if (ownsCursor && !opts?.force) client._delivery.observeBackgroundPullResponse?.(ns, nextAfterSeq, result, limit);
34427
35207
  if (ackUpToSeq > 0) {
34428
35208
  const actualAckSeq = client._delivery?.resolveGroupPullAckSeq?.(result, ackUpToSeq) ?? 0;
34429
35209
  if (actualAckSeq >= ackUpToSeq) {
34430
35210
  if (pendingInlineGeneration !== null) {
34431
35211
  client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
34432
35212
  }
34433
- pendingAckSeq = 0;
35213
+ if (pendingAckSeq <= ackUpToSeq) pendingAckSeq = 0;
34434
35214
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
34435
35215
  this.clearForwardAck(ns, ackUpToSeq);
34436
35216
  } else {
34437
35217
  client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
34438
- await this.confirmForwardGroupAck(gid, ackUpToSeq, groupAid);
34439
- pendingAckSeq = 0;
34440
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
35218
+ pendingAckSeq = Math.max(pendingAckSeq, ackUpToSeq);
35219
+ this.recordForwardAck(ns, ackUpToSeq);
34441
35220
  }
34442
35221
  }
34443
35222
  const resultGroupAid = String(result.group_aid ?? result.groupAid ?? "").trim();
@@ -34461,6 +35240,17 @@ var V2E2EECoordinator = class {
34461
35240
  seqs,
34462
35241
  result.contiguous_seq
34463
35242
  );
35243
+ if (!pipelineRunning && shouldContinueForwardPage({
35244
+ rawCount: messages.length,
35245
+ nextAfterSeq,
35246
+ nextAfter: pageMaxSeq,
35247
+ limit,
35248
+ hasMoreField,
35249
+ hasMore,
35250
+ serverHead,
35251
+ pendingAckSeq: 0,
35252
+ observedUpper: typeof client._seqTracker.getMaxSeenSeq === "function" ? client._seqTracker.getMaxSeenSeq(ns) : pageMaxSeq
35253
+ })) startPipeline(pageMaxSeq);
34464
35254
  const deferredKeyFetches = /* @__PURE__ */ new Map();
34465
35255
  let blockedSeq = 0;
34466
35256
  const pageDecryptedBefore = decrypted.length;
@@ -34642,6 +35432,7 @@ var V2E2EECoordinator = class {
34642
35432
  if (pageCount === 1 && deferredServerCursor > 0) this.clearForwardCursor(ns, ackSeq);
34643
35433
  }
34644
35434
  client._delivery.completePendingTailWindow?.(ns);
35435
+ committedAckSeq = ackSeq;
34645
35436
  } catch (exc) {
34646
35437
  if (Number(client._v2RuntimeGeneration ?? 0) === generation && (inlineGeneration === void 0 || client._delivery.isInlineGenerationCurrent(inlineGeneration)) && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false && pageTrackerSnapshot !== void 0 && typeof client._seqTracker.restoreNamespaceSnapshot === "function") {
34647
35438
  client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, pageTrackerSnapshot);
@@ -34670,15 +35461,7 @@ var V2E2EECoordinator = class {
34670
35461
  const ackNeeded = ackSeq > 0 && ackSeq > lastAutoAckSeq && ownsCursor && (hasServerCursor && ackSeq > cursorCurrentSeq || contigAdvanced && ackSeq > knownServerCursorSeq);
34671
35462
  if (ackNeeded) {
34672
35463
  this.recordForwardAck(ns, ackSeq);
34673
- const nextAfter2 = Math.max(pageContiguousSeq, nextAfterSeq);
34674
- const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter2 > nextAfterSeq && ownsCursor;
34675
- if (canContinuePage && !pipelineRunning) {
34676
- pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
34677
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
34678
- } else {
34679
- await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
34680
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
34681
- }
35464
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
34682
35465
  }
34683
35466
  const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
34684
35467
  if (!ownsCursor) break;
@@ -34707,15 +35490,7 @@ var V2E2EECoordinator = class {
34707
35490
  observedUpper: typeof client._seqTracker.getMaxSeenSeq === "function" ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter
34708
35491
  });
34709
35492
  if (!shouldContinue) break;
34710
- if (pipelinePage) {
34711
- pipelineResume?.();
34712
- pipelineResume = null;
34713
- }
34714
35493
  if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
34715
- if (pendingAckSeq > 0) {
34716
- await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
34717
- pendingAckSeq = 0;
34718
- }
34719
35494
  startPipeline(nextAfter);
34720
35495
  }
34721
35496
  if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
@@ -34730,7 +35505,7 @@ var V2E2EECoordinator = class {
34730
35505
  pullGateKey = nextKey;
34731
35506
  }
34732
35507
  const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
34733
- if (tailDelayMs > 0) await v2Sleep(tailDelayMs);
35508
+ if (!pipelineRunning && tailDelayMs > 0) await v2Sleep(tailDelayMs);
34734
35509
  nextAfterSeq = nextAfter;
34735
35510
  }
34736
35511
  } finally {
@@ -34738,25 +35513,29 @@ var V2E2EECoordinator = class {
34738
35513
  }
34739
35514
  if (pageCount >= maxPages) {
34740
35515
  client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
34741
- if (pendingAckSeq > 0) {
34742
- await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
34743
- pendingAckSeq = 0;
34744
- }
35516
+ }
35517
+ if (pendingAckSeq > 0) {
35518
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
35519
+ await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
35520
+ pendingAckSeq = 0;
34745
35521
  }
34746
35522
  client._delivery.onPullWorkSettled?.();
34747
35523
  return deduplicatePulledResults(decrypted);
34748
35524
  }
34749
35525
  async confirmForwardGroupAck(groupId, upToSeq, groupAid) {
34750
35526
  const ns = `group:${String(groupId ?? "").trim()}`;
35527
+ const generation = Number(this.client._v2RuntimeGeneration ?? 0);
35528
+ const deliveryGeneration = this.client._delivery.captureInlineGeneration?.();
34751
35529
  try {
34752
35530
  const result = await this.ackGroupV2ForForward(groupId, upToSeq, groupAid);
35531
+ this.ensureDeliveryCurrent(ns, generation, deliveryGeneration);
34753
35532
  const actualAckSeq = this.client._delivery?.resolveForwardAckSeq?.(result, upToSeq) ?? 0;
34754
35533
  if (actualAckSeq < upToSeq) {
34755
35534
  throw new E2EEError(`group.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
34756
35535
  }
34757
35536
  this.clearForwardAck(ns, upToSeq);
34758
35537
  } catch (exc) {
34759
- this.recordForwardAck(ns, upToSeq);
35538
+ if (Number(this.client._v2RuntimeGeneration ?? 0) === generation && (deliveryGeneration === void 0 || this.client._delivery.isInlineGenerationCurrent(deliveryGeneration)) && this.client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false) this.recordForwardAck(ns, upToSeq);
34760
35539
  throw exc;
34761
35540
  }
34762
35541
  }
@@ -35292,11 +36071,19 @@ var V2E2EECoordinator = class {
35292
36071
  });
35293
36072
  }
35294
36073
  }
36074
+ const targetDeviceId = String(opts.deviceId ?? "").trim();
35295
36075
  if (peerDevices.length === 0) {
36076
+ if (targetDeviceId && useCache) {
36077
+ this.deleteBootstrapCacheEntry(to);
36078
+ return this.buildV2P2PEnvelope({ ...opts, useCache: false });
36079
+ }
35296
36080
  throw new E2EEError(`V2 bootstrap: no devices found for ${to}`);
35297
36081
  }
35298
- const targetDeviceId = String(opts.deviceId ?? "").trim();
35299
36082
  if (targetDeviceId && !peerDevices.some((dev) => String(dev.device_id ?? "").trim() === targetDeviceId)) {
36083
+ if (useCache) {
36084
+ this.deleteBootstrapCacheEntry(to);
36085
+ return this.buildV2P2PEnvelope({ ...opts, useCache: false });
36086
+ }
35300
36087
  throw new E2EEError(`V2 bootstrap: target device ${targetDeviceId} not found for ${to}`);
35301
36088
  }
35302
36089
  let targetSet = useCache ? this.getTargetSetCache(to) : void 0;
@@ -35377,6 +36164,10 @@ var V2E2EECoordinator = class {
35377
36164
  return isPeerTarget || target.role === "self_sync";
35378
36165
  });
35379
36166
  if (!peerTargetFound) {
36167
+ if (useCache) {
36168
+ this.deleteBootstrapCacheEntry(to);
36169
+ return this.buildV2P2PEnvelope({ ...opts, useCache: false });
36170
+ }
35380
36171
  throw new E2EEError(`V2 bootstrap: target device ${targetDeviceId} not found for ${to}`);
35381
36172
  }
35382
36173
  }
@@ -37074,7 +37865,7 @@ var _AUNClient = class _AUNClient {
37074
37865
  this._logDiscovery = this._logger.for("aun_core.discovery");
37075
37866
  this._logEvents = this._logger.for("aun_core.events");
37076
37867
  this._clientLog.info(`AUNClient initialized: debug=${_debug} aunPath=${this.configModel.aunPath} aid=${initAid ?? "-"}`);
37077
- this._dispatcher = new EventDispatcher();
37868
+ this._dispatcher = new EventDispatcher(() => this._ensureReadStateIdentity());
37078
37869
  this._discovery = new GatewayDiscovery();
37079
37870
  this._tokenStore = new IndexedDBTokenStore();
37080
37871
  this._slotId = inputAid?.slotId || "default";
@@ -37113,7 +37904,7 @@ var _AUNClient = class _AUNClient {
37113
37904
  },
37114
37905
  auth: () => this._auth,
37115
37906
  tokenStore: () => this._tokenStore,
37116
- fetchPeerCert: (target, certFingerprint, signedAt) => this._fetchPeerCert(target, certFingerprint || void 0, void 0, signedAt),
37907
+ fetchPeerCert: (target, certFingerprint, signedAt) => this._fetchPeerCert(target, certFingerprint || void 0, void 0, signedAt, true),
37117
37908
  entityOwnerResolver: (target, type) => this._resolveEntityAgentMdOwner(target, type),
37118
37909
  entityIdentityGetter: (target, type) => this._freshEntityCertificate(target, type),
37119
37910
  entitySignerResolver: (target, fingerprint) => this._freshEntitySigner(target, fingerprint)
@@ -37125,6 +37916,7 @@ var _AUNClient = class _AUNClient {
37125
37916
  this._transport.setServiceEventVerifier((event, payload) => this._verifyIncomingServiceEvent(event, payload));
37126
37917
  this._transport.setProtocolDisconnectCallback((error, closeCode) => this._handleTransportDisconnect(error, closeCode));
37127
37918
  this._transport._onForegroundDrained = () => this._delivery.onForegroundPullsDrained();
37919
+ this._transport._onRpcResponse = () => this._delivery.requestBackgroundPullRecovery();
37128
37920
  this._transport.setProtocolMetaObserver(
37129
37921
  (meta) => this._observeRpcMeta(meta).catch((exc) => {
37130
37922
  this._clientLog.debug(`rpc meta observer skipped: ${String(exc)}`);
@@ -37522,7 +38314,7 @@ var _AUNClient = class _AUNClient {
37522
38314
  }
37523
38315
  _ownershipV21() {
37524
38316
  if (!this._ownershipWorkflow) {
37525
- const storage = new OwnershipStorageAdapter({ currentAid: () => this._aid ?? "", call: (method, params2) => this.call(method, params2) });
38317
+ const storage = new OwnershipStorageAdapter({ currentAid: () => this._aid ?? "", call: (method, params2) => this.call(method, params2), httpPut: (url, data, headers) => this.storage.lowlevel.httpPut(url, data, headers) });
37526
38318
  const files = new OwnershipFiles({
37527
38319
  currentAidType: () => this.getType(),
37528
38320
  currentAid: () => {
@@ -37532,8 +38324,8 @@ var _AUNClient = class _AUNClient {
37532
38324
  readAgentMdFresh: (aid2) => this._agentMdManager.readOwnershipFresh(aid2),
37533
38325
  submitAgentMdRaw: (aid2, raw, condition) => this._agentMdManager.submitOwnershipRaw(aid2, raw, condition),
37534
38326
  readOwnerMd: (owner, sub) => storage.readOwnerMd(owner, sub),
37535
- writeOwnerMd: (owner, sub, raw, revision) => storage.writeOwnerMd(owner, sub, raw, revision),
37536
- deleteOwnerMd: (owner, sub, revision) => storage.deleteOwnerMd(owner, sub, revision),
38327
+ writeOwnerMd: (owner, sub, raw) => storage.writeOwnerMd(owner, sub, raw),
38328
+ deleteOwnerMd: (owner, sub) => storage.deleteOwnerMd(owner, sub),
37537
38329
  resolveCertificate: async (aid2, fingerprint, keyPurpose) => this._auth.resolveOwnershipCertificate(await this._resolveOwnershipCertificateGateway(aid2), aid2, fingerprint, keyPurpose)
37538
38330
  });
37539
38331
  this._ownershipWorkflow = new OwnershipWorkflow({
@@ -38065,7 +38857,7 @@ var _AUNClient = class _AUNClient {
38065
38857
  },
38066
38858
  auth: () => this._auth,
38067
38859
  tokenStore: () => this._tokenStore,
38068
- fetchPeerCert: (target, certFingerprint, signedAt) => this._fetchPeerCert(target, certFingerprint || void 0, void 0, signedAt),
38860
+ fetchPeerCert: (target, certFingerprint, signedAt) => this._fetchPeerCert(target, certFingerprint || void 0, void 0, signedAt, true),
38069
38861
  entityOwnerResolver: (target, type) => this._resolveEntityAgentMdOwner(target, type),
38070
38862
  entityIdentityGetter: (target, type) => this._freshEntityCertificate(target, type),
38071
38863
  entitySignerResolver: (target, fingerprint) => this._freshEntitySigner(target, fingerprint)
@@ -38077,6 +38869,7 @@ var _AUNClient = class _AUNClient {
38077
38869
  this._transport.setServiceEventVerifier((event, payload) => this._verifyIncomingServiceEvent(event, payload));
38078
38870
  this._transport.setProtocolDisconnectCallback((error, closeCode) => this._handleTransportDisconnect(error, closeCode));
38079
38871
  this._transport._onForegroundDrained = () => this._delivery.onForegroundPullsDrained();
38872
+ this._transport._onRpcResponse = () => this._delivery.requestBackgroundPullRecovery();
38080
38873
  this._transport.setProtocolMetaObserver(
38081
38874
  (meta) => this._observeRpcMeta(meta).catch((exc) => {
38082
38875
  this._clientLog.debug(`rpc meta observer skipped: ${String(exc)}`);
@@ -38206,6 +38999,7 @@ var _AUNClient = class _AUNClient {
38206
38999
  const groupAid = message.slice(groupMDPrefix.length).trim();
38207
39000
  const store = this._aidStore;
38208
39001
  if (!groupAid || !store) return false;
39002
+ if (String((await (store._agentMdManager ?? this._agentMdManager).loadRecord(groupAid, true))?.local_draft_content ?? "").trim()) return false;
38209
39003
  const loaded = await store.load(groupAid);
38210
39004
  if (!loaded.ok || !loaded.data?.aid?.isPrivateKeyValid()) return false;
38211
39005
  const ownerAid = String(this._aid ?? this._currentAid?.aid ?? "").trim();
@@ -38220,6 +39014,7 @@ var _AUNClient = class _AUNClient {
38220
39014
  if (!message.toLowerCase().startsWith(agentPrefix)) return false;
38221
39015
  const target = message.slice(agentPrefix.length).trim();
38222
39016
  if (!target || target.toLowerCase() !== String(this._aid ?? "").trim().toLowerCase()) return false;
39017
+ if (String((await this._agentMdManager.loadRecord(target, true))?.local_draft_content ?? "").trim()) return false;
38223
39018
  const content = await this._agentMdManager.readContent(target) ?? buildDefaultAgentMd(target);
38224
39019
  if (!this._ordinaryAdmissionAgentMd(content, target)) return false;
38225
39020
  await this._agentMdManager.upload(content);
@@ -38233,10 +39028,11 @@ var _AUNClient = class _AUNClient {
38233
39028
  const aid2 = String(this._aid ?? this._currentAid?.aid ?? "").trim();
38234
39029
  if (!aid2) return;
38235
39030
  try {
38236
- const pending = await this._agentMdManager.pendingUpload(aid2);
38237
- if (pending) {
39031
+ const draft = String((await this._agentMdManager.loadRecord(aid2, true))?.local_draft_content ?? "").trim();
39032
+ const pending = draft ? null : await this._agentMdManager.pendingUpload(aid2);
39033
+ if (!draft && pending) {
38238
39034
  if (this._ordinaryAdmissionAgentMd(pending.content, aid2)) await this._agentMdManager.resumePendingUpload(aid2);
38239
- } else {
39035
+ } else if (!draft) {
38240
39036
  const checked = await this._agentMdManager.check(aid2, 0);
38241
39037
  if (!checked.remote_found) {
38242
39038
  const content = await this._agentMdManager.readContent(aid2) ?? buildDefaultAgentMd(aid2);
@@ -38280,6 +39076,7 @@ var _AUNClient = class _AUNClient {
38280
39076
  await this.bindGroupAid({ group_id: groupId, group_aid: groupAid }, { aidStore: store });
38281
39077
  return;
38282
39078
  }
39079
+ if (String((await (store._agentMdManager ?? this._agentMdManager).loadRecord(groupAid, true))?.local_draft_content ?? "").trim()) return;
38283
39080
  if (await this._resumeGroupAgentMdUpload(store, groupAid)) return;
38284
39081
  const checked = await this._agentMdManager.check(groupAid);
38285
39082
  if (!checked.remote_found) {
@@ -38358,6 +39155,7 @@ var _AUNClient = class _AUNClient {
38358
39155
  await this.completeOrgTransfer(transfer.source_params, { aidStore: store });
38359
39156
  continue;
38360
39157
  }
39158
+ if (String((await (store._agentMdManager ?? this._agentMdManager).loadRecord(aid2, true))?.local_draft_content ?? "").trim()) continue;
38361
39159
  const pending = await store._agentMdManager.pendingUpload(aid2);
38362
39160
  if (!pending) continue;
38363
39161
  const fields = validateAgentMdDocument(pending.content, { expectedAid: aid2 }).frontmatter;
@@ -38373,6 +39171,10 @@ var _AUNClient = class _AUNClient {
38373
39171
  const orgAid = String(params2.org_id ?? "").trim();
38374
39172
  const transferId = String(params2.transfer_id ?? "").trim();
38375
39173
  if (!store || !owner?.isPrivateKeyValid() || !orgAid || !transferId) throw new ValidationError("completeOrgTransfer requires aidStore, org_id, transfer_id and current accepting owner");
39174
+ for (const field of ["previous_owner", "new_owner"]) {
39175
+ const material = params2[field];
39176
+ if (!isJsonObject(material) || !isJsonObject(material.membership_binding) || !isJsonObject(material.terms) || field === "previous_owner" && !isJsonObject(material.credential_template)) throw new StateError("Org transfer requires both replacement membership bindings, terms and previous-owner credential", { stringCode: "invalid", retryable: false });
39177
+ }
38376
39178
  this._aidStore = store;
38377
39179
  const localStore = store._keystore;
38378
39180
  const tokenStore = store._tokenStore;
@@ -38380,10 +39182,6 @@ var _AUNClient = class _AUNClient {
38380
39182
  const raw = await tokenStore.getMetadata(orgAid, "org_identity_transfer");
38381
39183
  let pending = raw ? JSON.parse(raw) : void 0;
38382
39184
  if (pending && (pending.transfer_id !== transferId || pending.owner_aid !== owner.aid || pending.input_hash !== inputHash)) throw new StateError("pending Org transfer context changed", { stringCode: "conflict", retryable: false });
38383
- for (const field of ["previous_owner", "new_owner"]) {
38384
- const material = params2[field];
38385
- if (!isJsonObject(material) || !isJsonObject(material.membership_binding) || !isJsonObject(material.terms) || field === "previous_owner" && !isJsonObject(material.credential_template)) throw new StateError("Org transfer requires both replacement membership bindings, terms and previous-owner credential", { stringCode: "invalid", retryable: false });
38386
- }
38387
39185
  const pendingKey = `org-transfer:${orgAid}:${transferId}`;
38388
39186
  let keyPair = await localStore.loadPendingGroupBind(pendingKey);
38389
39187
  if (!keyPair) {
@@ -38726,7 +39524,8 @@ var _AUNClient = class _AUNClient {
38726
39524
  { uploaderAid, entityOwnerResolver: (aid2, type) => this._resolveEntityAgentMdOwner(aid2, type), entityIdentityGetter: (aid2, type) => this._freshEntityCertificate(aid2, type), entitySignerResolver: (aid2, fingerprint) => this._freshEntitySigner(aid2, fingerprint) }
38727
39525
  );
38728
39526
  if (!uploaded.ok) {
38729
- throw new ValidationError(`group agent.md upload failed: ${uploaded.error.message}`);
39527
+ const code = ["invalid", "unavailable", "forbidden", "conflict", "persistence_unavailable"].includes(uploaded.error.code) ? uploaded.error.code : uploaded.error.code === "NETWORK_ERROR" ? "unavailable" : "invalid";
39528
+ throw new StateError(`group agent.md upload failed: ${uploaded.error.message}`, { stringCode: code, retryable: code === "unavailable" });
38730
39529
  }
38731
39530
  }
38732
39531
  async _freshEntityCertificate(aid2, type = "group") {
@@ -39806,11 +40605,11 @@ ${invitee}` : "";
39806
40605
  if (version !== this._serviceEventIdentityVersion || targetAid !== (this._aid || this._currentAid?.aid || "")) throw new ValidationError("service event receiver identity changed");
39807
40606
  return verified;
39808
40607
  }
39809
- async _fetchPeerCert(aid2, certFingerprint, timeoutMs = 5e3, signedAt) {
40608
+ async _fetchPeerCert(aid2, certFingerprint, timeoutMs = 5e3, signedAt, agentMdRead = false) {
39810
40609
  const tStart = Date.now();
39811
40610
  this._clientLog.debug(`_fetchPeerCert enter: aid=${aid2} fingerprint=${certFingerprint ?? "<none>"}`);
39812
40611
  try {
39813
- const cacheKey = certCacheKey(aid2, certFingerprint) + (signedAt === void 0 ? "" : `:signed:${signedAt}`);
40612
+ const cacheKey = certCacheKey(aid2, certFingerprint) + (signedAt === void 0 ? "" : `:signed:${signedAt}`) + (agentMdRead ? ":agentmd" : "");
39814
40613
  const cached = this._certCache.get(cacheKey);
39815
40614
  const now = Date.now() / 1e3;
39816
40615
  if (cached && now < cached.refreshAfter) {
@@ -39848,6 +40647,7 @@ ${invitee}` : "";
39848
40647
  }
39849
40648
  try {
39850
40649
  if (signedAt === void 0) await this._auth.verifyPeerCertificate(peerGatewayUrl, certPem, aid2);
40650
+ else if (agentMdRead) await this._auth.verifyPeerCertificate(peerGatewayUrl, certPem, aid2, signedAt, false, true);
39851
40651
  else await this._auth.verifyPeerCertificate(peerGatewayUrl, certPem, aid2, signedAt);
39852
40652
  } catch (exc) {
39853
40653
  throw new ValidationError(`peer cert verification failed for ${aid2}: ${exc}`);