@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.
- package/CHANGELOG.md +129 -78
- package/_packed_docs/CHANGELOG.md +129 -78
- package/_packed_docs/INDEX.md +201 -195
- package/_packed_docs/KITE_DOCS_GUIDE.md +93 -90
- package/_packed_docs/agent.md/SCHEMA.md +7 -7
- package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +11 -11
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1072 -1072
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +676 -676
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +8 -12
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +443 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +658 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
- package/_packed_docs/protocol/agent.md/SCHEMA.md +1 -1
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +1 -1
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +67 -63
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +1 -1
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +35 -8
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +3 -3
- package/_packed_docs/sdk/09-group-rpc-manual.md +258 -254
- package/_packed_docs/sdk/09-message-rpc-manual.md +7 -4
- package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +61 -52
- package/_packed_docs/sdk/INDEX.md +92 -80
- package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +922 -0
- package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +300 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +382 -0
- package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -0
- package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +476 -0
- package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
- package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +173 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +211 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/345/256/236/347/216/260/347/247/273/346/244/215/346/214/207/345/215/227.md +331 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +207 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/344/273/243/347/240/201/345/256/241/346/237/245/346/212/245/345/221/212.md +252 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/345/256/236/346/226/275/350/256/241/345/210/222.md +156 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/346/226/271/346/241/210.md +273 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/350/257/246/347/273/206/350/256/276/350/256/241.md +211 -0
- package/dist/agent-md-schema.d.ts +15 -4
- package/dist/agent-md-schema.d.ts.map +1 -1
- package/dist/agent-md-schema.js +64 -30
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +10 -2
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +278 -121
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +46 -0
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +149 -10
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +4 -1
- package/dist/aid.d.ts.map +1 -1
- package/dist/aid.js +23 -42
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +85 -18
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +1581 -781
- package/dist/cert-utils.d.ts +49 -1
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +102 -56
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +7 -0
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +243 -28
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +89 -77
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +99 -80
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +31 -17
- package/dist/client.js.map +1 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +8 -6
- package/dist/crypto.js.map +1 -1
- package/dist/events.d.ts +5 -2
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +33 -14
- package/dist/events.js.map +1 -1
- package/dist/facades.js +5 -5
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/org-protocol.d.ts.map +1 -1
- package/dist/org-protocol.js +72 -12
- package/dist/org-protocol.js.map +1 -1
- package/dist/ownership-v21.d.ts +25 -8
- package/dist/ownership-v21.d.ts.map +1 -1
- package/dist/ownership-v21.js +142 -73
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.d.ts +6 -1
- package/dist/ownership-workflow.d.ts.map +1 -1
- package/dist/ownership-workflow.js +221 -58
- package/dist/ownership-workflow.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +2 -1
- package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
- package/dist/tools/cross-sdk-agent.js +91 -8
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +1 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +2 -0
- package/dist/transport.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/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:
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
9222
|
-
|
|
9223
|
-
|
|
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
|
|
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
|
|
9243
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
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
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
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
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
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 ?
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
9452
|
-
const peer =
|
|
9453
|
-
const verified = await
|
|
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
|
|
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
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
10220
|
-
if (!/^[
|
|
10221
|
-
if (!
|
|
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
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
|
|
10266
|
-
|
|
10267
|
-
|
|
10268
|
-
|
|
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
|
-
|
|
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
|
|
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" ||
|
|
10315
|
-
if (value.exists && (typeof value.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(value.sha256)
|
|
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",
|
|
10506
|
+
const body = await this.request("storage.get_object", params2);
|
|
10338
10507
|
const current = this.metadata(body, ownerAid, path);
|
|
10339
|
-
if (!current.exists || current.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
10363
|
-
|
|
10364
|
-
await this.request("storage.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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: "",
|
|
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
|
|
10432
|
-
|
|
10433
|
-
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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: {
|
|
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 ||
|
|
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.
|
|
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
|
|
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 (
|
|
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.
|
|
10631
|
-
|
|
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.
|
|
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
|
-
|
|
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",
|
|
10711
|
-
if (entity &&
|
|
10712
|
-
if (
|
|
10713
|
-
if (
|
|
10714
|
-
|
|
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
|
|
10722
|
-
|
|
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
|
|
10725
|
-
|
|
10726
|
-
|
|
10727
|
-
|
|
10728
|
-
|
|
10729
|
-
|
|
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()
|
|
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
|
-
|
|
10844
|
-
|
|
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 =
|
|
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
|
|
10912
|
-
|
|
10913
|
-
|
|
10914
|
-
|
|
10915
|
-
|
|
10916
|
-
|
|
10917
|
-
|
|
10918
|
-
|
|
10919
|
-
|
|
10920
|
-
|
|
10921
|
-
|
|
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))
|
|
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 (
|
|
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 (!
|
|
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 !==
|
|
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(
|
|
11170
|
-
await verifyAgentSignature(signed, body,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
15840
|
-
|
|
15841
|
-
const
|
|
15842
|
-
|
|
15843
|
-
|
|
15844
|
-
|
|
15845
|
-
|
|
15846
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 ||
|
|
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.
|
|
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
|
-
|
|
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) ||
|
|
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 ?
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
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)
|
|
17957
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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" ||
|
|
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
|
|
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
|
-
|
|
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 && !
|
|
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
|
|
18659
|
+
if (prepared) {
|
|
18181
18660
|
const object = await this.deps.files.readOwner(task.owner_aid, task.sub_aid);
|
|
18182
|
-
if (object.exists && object.sha256
|
|
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"
|
|
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 ||
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
22063
|
-
|
|
22064
|
-
|
|
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
|
-
() =>
|
|
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
|
-
|
|
22106
|
-
|
|
22107
|
-
|
|
22108
|
-
client.
|
|
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)
|
|
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
|
-
|
|
22246
|
-
|
|
22247
|
-
|
|
22248
|
-
|
|
22249
|
-
this.
|
|
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.
|
|
22932
|
-
|
|
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.
|
|
23010
|
-
|
|
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
|
-
|
|
23018
|
-
|
|
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 = {
|
|
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
|
|
24975
|
-
if (
|
|
25747
|
+
const activeJobs = [...state.activeByNamespace.values()];
|
|
25748
|
+
if (activeJobs.length === 0) {
|
|
24976
25749
|
this.drainPullGate(state);
|
|
24977
25750
|
return;
|
|
24978
25751
|
}
|
|
24979
|
-
|
|
24980
|
-
|
|
24981
|
-
|
|
24982
|
-
|
|
24983
|
-
|
|
24984
|
-
|
|
24985
|
-
|
|
24986
|
-
|
|
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?.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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?.
|
|
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))?.
|
|
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
|
-
|
|
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 &&
|
|
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.
|
|
25288
|
-
if (!job
|
|
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.
|
|
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.
|
|
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
|
-
|
|
25327
|
-
|
|
25328
|
-
|
|
25329
|
-
|
|
25330
|
-
|
|
25331
|
-
|
|
25332
|
-
|
|
25333
|
-
|
|
25334
|
-
|
|
25335
|
-
|
|
25336
|
-
|
|
25337
|
-
|
|
25338
|
-
|
|
25339
|
-
|
|
25340
|
-
|
|
25341
|
-
job.suspended
|
|
25342
|
-
|
|
25343
|
-
|
|
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
|
-
|
|
25356
|
-
|
|
25357
|
-
|
|
25358
|
-
|
|
25359
|
-
|
|
25360
|
-
|
|
25361
|
-
|
|
25362
|
-
|
|
25363
|
-
|
|
25364
|
-
|
|
25365
|
-
|
|
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,
|
|
32375
|
-
|
|
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 ||
|
|
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
|
-
|
|
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
|
|
33599
|
-
inlineAckGeneration
|
|
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
|
-
|
|
33694
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33929
|
-
|
|
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
|
-
|
|
33958
|
-
|
|
33959
|
-
|
|
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
|
|
34356
|
-
inlineAckGeneration
|
|
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
|
-
|
|
34439
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34742
|
-
|
|
34743
|
-
|
|
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
|
|
37536
|
-
deleteOwnerMd: (owner, sub
|
|
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
|
|
38237
|
-
|
|
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
|
-
|
|
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}`);
|