@agentunion/fastaun-browser 0.5.19 → 0.5.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +176 -78
- package/_packed_docs/CHANGELOG.md +176 -78
- package/_packed_docs/INDEX.md +288 -269
- package/_packed_docs/KITE_DOCS_GUIDE.md +101 -95
- package/_packed_docs/agent.md/SCHEMA.md +7 -7
- package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/344/270/216/346/215/242owner/347/274/226/346/216/222/346/226/271/346/241/210.md +530 -0
- package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/345/256/214/346/225/264/350/256/276/350/256/241/344/270/216/345/256/236/346/226/275/350/256/241/345/210/222.md +685 -0
- package/_packed_docs/agent.md/working/agent.md-schema/344/270/216/347/255/276/345/217/221/351/252/214/347/255/276/346/265/201/347/250/213/346/276/204/346/270/205.md +349 -0
- package/_packed_docs/agent.md/working//346/234/254/345/234/260Docker/346/265/213/350/257/225Bug/346/270/205/345/215/225.md +40 -0
- package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +11 -11
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +372 -331
- package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +676 -676
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +8 -12
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +445 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
- package/_packed_docs/protocol/agent.md/SCHEMA.md +1 -1
- package/_packed_docs/sdk/0.5.21-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +368 -0
- package/_packed_docs/sdk/0.5.21-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +350 -0
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +70 -63
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +43 -40
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +179 -19
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +50 -49
- package/_packed_docs/sdk/09-group-rpc-manual.md +261 -254
- package/_packed_docs/sdk/09-message-rpc-manual.md +253 -160
- package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
- package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +71 -0
- package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +88 -0
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +105 -92
- package/_packed_docs/sdk/INDEX.md +197 -167
- package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -0
- package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +363 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +553 -0
- package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -0
- package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -0
- package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
- package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +173 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +211 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/345/256/236/347/216/260/347/247/273/346/244/215/346/214/207/345/215/227.md +331 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +207 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/344/273/243/347/240/201/345/256/241/346/237/245/346/212/245/345/221/212.md +252 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/345/256/236/346/226/275/350/256/241/345/210/222.md +156 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/346/226/271/346/241/210.md +273 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/350/257/246/347/273/206/350/256/276/350/256/241.md +211 -0
- package/dist/agent-md-schema.d.ts +16 -4
- package/dist/agent-md-schema.d.ts.map +1 -1
- package/dist/agent-md-schema.js +65 -30
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +35 -3
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +384 -150
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +59 -0
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +202 -16
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +4 -1
- package/dist/aid.d.ts.map +1 -1
- package/dist/aid.js +24 -42
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +89 -18
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +6410 -1848
- package/dist/cert-utils.d.ts +50 -1
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +123 -56
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +108 -16
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +1411 -184
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +4 -1
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +203 -117
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +22 -3
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +1312 -684
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +23 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +419 -101
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +8 -6
- package/dist/crypto.js.map +1 -1
- package/dist/events.d.ts +24 -3
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +171 -29
- package/dist/events.js.map +1 -1
- package/dist/facades.d.ts +42 -1
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +368 -12
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +14 -1
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.d.ts +4 -0
- package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.js +30 -13
- package/dist/keystore/indexeddb-identity-store.js.map +1 -1
- package/dist/keystore/indexeddb-shared.d.ts +19 -2
- package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
- package/dist/keystore/indexeddb-shared.js +193 -6
- package/dist/keystore/indexeddb-shared.js.map +1 -1
- package/dist/keystore/indexeddb-token-store.d.ts +15 -2
- package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-token-store.js +308 -27
- package/dist/keystore/indexeddb-token-store.js.map +1 -1
- package/dist/org-protocol.d.ts +45 -0
- package/dist/org-protocol.d.ts.map +1 -1
- package/dist/org-protocol.js +827 -21
- package/dist/org-protocol.js.map +1 -1
- package/dist/ownership-v21.d.ts +43 -9
- package/dist/ownership-v21.d.ts.map +1 -1
- package/dist/ownership-v21.js +183 -81
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.d.ts +36 -1
- package/dist/ownership-workflow.d.ts.map +1 -1
- package/dist/ownership-workflow.js +610 -89
- package/dist/ownership-workflow.js.map +1 -1
- package/dist/seq-tracker.d.ts +2 -0
- package/dist/seq-tracker.d.ts.map +1 -1
- package/dist/seq-tracker.js +3 -2
- package/dist/seq-tracker.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +61 -3
- package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
- package/dist/tools/cross-sdk-agent.js +524 -45
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +4 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +64 -30
- package/dist/transport.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/org-protocol.js
CHANGED
|
@@ -27,6 +27,12 @@ const REPRESENTATION_FIELDS = new Set([
|
|
|
27
27
|
]);
|
|
28
28
|
const REPRESENTATION_SIGNATURE_FIELDS = new Set(['cert_fingerprint', 'public_key_fingerprint', 'timestamp', 'signature']);
|
|
29
29
|
const FINGERPRINT_RE = /^sha256:[0-9a-f]{64}$/i;
|
|
30
|
+
const SERVICE_EVENTS = new Set([
|
|
31
|
+
'org.member.join_requested', 'org.member.join_approved', 'org.member.join_rejected',
|
|
32
|
+
'org.member.invite_created', 'org.member.invite_accepted', 'org.member.invite_rejected',
|
|
33
|
+
'org.member.changed', 'org.lifecycle.created', 'org.lifecycle.dissolving',
|
|
34
|
+
'org.lifecycle.dissolved', 'credential.revoked',
|
|
35
|
+
]);
|
|
30
36
|
export class ProtocolError extends Error {
|
|
31
37
|
code;
|
|
32
38
|
retryable;
|
|
@@ -39,25 +45,30 @@ export class ProtocolError extends Error {
|
|
|
39
45
|
}
|
|
40
46
|
export async function verifyServiceEvent(eventName, payload, context) {
|
|
41
47
|
const event = eventName.startsWith('event/') ? eventName.slice(6) : eventName;
|
|
42
|
-
if (!isRecord(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))
|
|
48
|
+
if (!SERVICE_EVENTS.has(event) || !isRecord(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))
|
|
43
49
|
throw invalid('service event payload is invalid');
|
|
44
50
|
normalizeNfc(payload);
|
|
45
51
|
aid(context.targetAid, 'event target');
|
|
46
52
|
aid(payload.responder, 'event responder');
|
|
47
|
-
if (!Array.isArray(payload.target_aids) || !payload.target_aids.length || payload.target_aids.some((target) => typeof target !== 'string' || !AID_RE.test(target))
|
|
53
|
+
if (!Array.isArray(payload.target_aids) || !payload.target_aids.length || payload.target_aids.some((target) => typeof target !== 'string' || !AID_RE.test(target)))
|
|
54
|
+
throw invalid('service event audience is invalid');
|
|
55
|
+
const targetAids = new Set(payload.target_aids.map((target) => aidKey(target)));
|
|
56
|
+
if (targetAids.size !== payload.target_aids.length || !targetAids.has(aidKey(context.targetAid)))
|
|
48
57
|
throw invalid('service event audience is invalid');
|
|
58
|
+
const responder = payload.responder;
|
|
59
|
+
const responderKey = aidKey(responder);
|
|
49
60
|
const now = context.now ?? Math.trunc(Date.now() / 1000);
|
|
50
61
|
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)
|
|
51
62
|
throw invalid('service event time window is invalid');
|
|
52
63
|
let known = true;
|
|
53
64
|
if (event.startsWith('org.')) {
|
|
54
65
|
aid(payload.org_id, 'event organization');
|
|
55
|
-
if (
|
|
66
|
+
if (responderKey !== aidKey('org.' + String(payload.org_id).slice(String(payload.org_id).indexOf('.') + 1)))
|
|
56
67
|
throw invalid('organization event authority is invalid');
|
|
57
68
|
}
|
|
58
69
|
else if (event.startsWith('credential.')) {
|
|
59
70
|
const cid = payload.credentialCid ?? payload.credential_cid;
|
|
60
|
-
if (!
|
|
71
|
+
if (!responderKey.startsWith('credential.') || typeof payload.target !== 'string' || !targetAids.has(aidKey(payload.target)) || typeof cid !== 'string' || !cid || payload.credentialCid !== undefined && payload.credential_cid !== undefined && payload.credentialCid !== payload.credential_cid)
|
|
61
72
|
throw invalid('credential event authority or target is invalid');
|
|
62
73
|
let service;
|
|
63
74
|
try {
|
|
@@ -67,7 +78,7 @@ export async function verifyServiceEvent(eventName, payload, context) {
|
|
|
67
78
|
throw invalid('trusted credential authority is unavailable');
|
|
68
79
|
}
|
|
69
80
|
known = service != null;
|
|
70
|
-
if (known && (typeof service !== 'string' || !service.startsWith('credential.') || service !==
|
|
81
|
+
if (known && (typeof service !== 'string' || !AID_RE.test(service) || !aidKey(service).startsWith('credential.') || aidKey(service) !== responderKey))
|
|
71
82
|
throw invalid('credential event authority differs from the trusted provider');
|
|
72
83
|
}
|
|
73
84
|
else
|
|
@@ -79,8 +90,8 @@ export async function verifyServiceEvent(eventName, payload, context) {
|
|
|
79
90
|
throw new Error('service event signature context is invalid');
|
|
80
91
|
const body = { ...payload };
|
|
81
92
|
delete body.signature;
|
|
82
|
-
const certificate = await context.resolveEventCertificate(
|
|
83
|
-
await verifyAgentSignature(signed, body,
|
|
93
|
+
const certificate = await context.resolveEventCertificate(responderKey, signed.cert_fingerprint);
|
|
94
|
+
await verifyAgentSignature(signed, body, responder, () => certificate, context.now ?? Math.trunc(Date.now() / 1000), false);
|
|
84
95
|
}
|
|
85
96
|
catch {
|
|
86
97
|
throw new ProtocolError('CREDENTIAL_SIGNATURE_INVALID', 'service event signature verification failed');
|
|
@@ -89,6 +100,9 @@ export async function verifyServiceEvent(eventName, payload, context) {
|
|
|
89
100
|
throw invalid('service event time window is invalid');
|
|
90
101
|
return { signature_valid: true, credential_known: known };
|
|
91
102
|
}
|
|
103
|
+
function aidKey(value) {
|
|
104
|
+
return value.trim().toLowerCase();
|
|
105
|
+
}
|
|
92
106
|
export function protocolErrorRetryable(code) {
|
|
93
107
|
return ERROR_RETRYABLE[String(code)] === true;
|
|
94
108
|
}
|
|
@@ -101,6 +115,743 @@ function isRecord(value) {
|
|
|
101
115
|
function isInt(value) {
|
|
102
116
|
return typeof value === 'number' && Number.isInteger(value) && Number.isSafeInteger(value);
|
|
103
117
|
}
|
|
118
|
+
export class CredentialRenewal {
|
|
119
|
+
tail = Promise.resolve();
|
|
120
|
+
ports;
|
|
121
|
+
scanGuard;
|
|
122
|
+
constructor(ports) {
|
|
123
|
+
this.ports = {
|
|
124
|
+
...ports,
|
|
125
|
+
currentAid: () => { this.checkScan(); return ports.currentAid(); },
|
|
126
|
+
load: (aid) => this.scanOperation(() => ports.load(aid)),
|
|
127
|
+
save: (aid, records, guard) => {
|
|
128
|
+
const scan = this.scanGuard;
|
|
129
|
+
return this.scanOperation(() => ports.save(aid, records, scan ? { current: () => scan.current() && (!guard || guard.current()), signal: scan.signal } : guard));
|
|
130
|
+
},
|
|
131
|
+
signer: (aid) => this.scanOperation(() => ports.signer(aid, this.scanGuard)),
|
|
132
|
+
certificate: (aid, fingerprint, signedAt) => this.scanOperation(() => ports.certificate(aid, fingerprint, signedAt)),
|
|
133
|
+
call: (method, params, onSent) => this.scanOperation(() => onSent ? ports.call(method, params, onSent) : ports.call(method, params)),
|
|
134
|
+
send: (to, payload) => this.scanOperation(() => ports.send(to, payload)),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
checkScan() {
|
|
138
|
+
if (this.scanGuard && !this.scanGuard.current())
|
|
139
|
+
throw invalid('credential renewal was superseded');
|
|
140
|
+
}
|
|
141
|
+
async scanOperation(operation) {
|
|
142
|
+
this.checkScan();
|
|
143
|
+
const result = await operation();
|
|
144
|
+
this.checkScan();
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
serial(operation) {
|
|
148
|
+
const result = this.tail.then(operation);
|
|
149
|
+
this.tail = result.catch(() => { });
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
remember(credential, material = {}) {
|
|
153
|
+
return this.serial(() => this.rememberNow(credential, material));
|
|
154
|
+
}
|
|
155
|
+
current(input) {
|
|
156
|
+
return this.serial(async () => {
|
|
157
|
+
const records = await this.ports.load(this.ports.currentAid());
|
|
158
|
+
let cid = typeof input === 'string' ? input : String(input.cid);
|
|
159
|
+
const seen = new Set();
|
|
160
|
+
let record = records[cid];
|
|
161
|
+
while (record?.successor_cid && !seen.has(cid)) {
|
|
162
|
+
seen.add(cid);
|
|
163
|
+
cid = String(record.successor_cid);
|
|
164
|
+
record = records[cid];
|
|
165
|
+
}
|
|
166
|
+
return record ? structuredClone(record) : null;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async rememberNow(credential, material, actor = this.ports.currentAid(), guard) {
|
|
170
|
+
validateCredential(credential);
|
|
171
|
+
if (!material.long_term && !guard)
|
|
172
|
+
return;
|
|
173
|
+
if (guard && !guard.current())
|
|
174
|
+
throw invalid('membership event receiver identity changed');
|
|
175
|
+
const accounts = new Set();
|
|
176
|
+
if (credential.sub === actor)
|
|
177
|
+
accounts.add(actor);
|
|
178
|
+
if (credential.iss === actor || material.issued)
|
|
179
|
+
accounts.add(String(credential.iss));
|
|
180
|
+
for (const aid of accounts) {
|
|
181
|
+
const records = structuredClone(await this.ports.load(aid));
|
|
182
|
+
if (guard && !guard.current())
|
|
183
|
+
throw invalid('membership event receiver identity changed');
|
|
184
|
+
const previous = records[String(credential.cid)] ?? {};
|
|
185
|
+
const defined = Object.fromEntries(Object.entries(material).filter(([, value]) => value !== undefined));
|
|
186
|
+
records[String(credential.cid)] = { ...previous, ...structuredClone(defined), credential: structuredClone(credential), long_term: material.long_term === true };
|
|
187
|
+
if (guard)
|
|
188
|
+
await this.ports.save(aid, records, guard);
|
|
189
|
+
else
|
|
190
|
+
await this.ports.save(aid, records);
|
|
191
|
+
if (guard && !guard.current())
|
|
192
|
+
throw invalid('membership event receiver identity changed');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
importMembershipPackage(payload, guard) {
|
|
196
|
+
const actor = this.ports.currentAid();
|
|
197
|
+
return this.serial(async () => {
|
|
198
|
+
const check = () => { if (!guard.current())
|
|
199
|
+
throw invalid('membership event receiver identity changed'); };
|
|
200
|
+
check();
|
|
201
|
+
const material = payload.membership_package;
|
|
202
|
+
if (!isRecord(material) || Object.keys(material).length !== 5 || !['credential', 'membership_binding', 'registrationReceipt', 'terms', 'org_signer_aid'].every((key) => key in material)
|
|
203
|
+
|| payload.member_status !== 'active' || !isInt(payload.version) || payload.version < 1)
|
|
204
|
+
throw invalid('membership package is invalid');
|
|
205
|
+
const { credential, membership_binding: binding, registrationReceipt: receipt, terms, org_signer_aid: orgSigner } = material;
|
|
206
|
+
if (!isRecord(credential) || !isRecord(binding) || !isRecord(receipt) || !isRecord(terms) || !isRecord(credential.ext)
|
|
207
|
+
|| credential.iss !== payload.org_id || credential.sub !== payload.member_aid || binding.org_id !== payload.org_id || binding.member_aid !== payload.member_aid
|
|
208
|
+
|| binding.membership_id !== payload.membership_id || credential.cid !== payload.credential_cid || digest(terms) !== binding.terms_hash
|
|
209
|
+
|| credential.ext.credential_type !== 'member' || credential.ext.bound_to_membership !== true || credential.ext.org_id !== payload.org_id
|
|
210
|
+
|| credential.ext.membership_id !== binding.membership_id || credential.ext.binding_version !== binding.binding_version || credential.ext.membership_terms_hash !== binding.terms_hash
|
|
211
|
+
|| credential.parent != null || credential.delegate !== false || !Array.isArray(credential.cap) || credential.cap.length !== 1
|
|
212
|
+
|| credential.cap[0].ns !== 'org' || credential.cap[0].action !== 'role.' + String(binding.role) || credential.cap[0].resource !== payload.org_id)
|
|
213
|
+
throw invalid('membership package does not match the event');
|
|
214
|
+
aid(orgSigner, 'membership organization signer');
|
|
215
|
+
validateCredential(credential);
|
|
216
|
+
validateMembershipBinding(binding);
|
|
217
|
+
const certificates = new Map();
|
|
218
|
+
for (const [signerAid, signed] of [[credential.iss, credential.signature], [binding.member_aid, binding.member_signature], [orgSigner, binding.org_signature], ['credential.' + String(payload.org_id).split('.').slice(1).join('.'), receipt.signature]]) {
|
|
219
|
+
if (!isRecord(signed))
|
|
220
|
+
throw invalid('membership package signature is missing');
|
|
221
|
+
const certificate = await this.ports.certificate(String(signerAid), String(signed.cert_fingerprint));
|
|
222
|
+
check();
|
|
223
|
+
certificates.set(String(signed.cert_fingerprint), certificate);
|
|
224
|
+
}
|
|
225
|
+
const resolve = (fingerprint) => certificates.get(fingerprint);
|
|
226
|
+
const now = Math.trunc(Date.now() / 1000);
|
|
227
|
+
await verifyCredentialSignature(credential, resolve, now, String(payload.org_id));
|
|
228
|
+
await verifyMembershipBinding(binding, resolve, now, String(orgSigner), true);
|
|
229
|
+
check();
|
|
230
|
+
const service = 'credential.' + String(payload.org_id).split('.').slice(1).join('.');
|
|
231
|
+
if (!isInt(receipt.registered_at) || !isInt(receipt.exp) || receipt.exp > receipt.registered_at + 300 || receipt.registered_at > now)
|
|
232
|
+
throw invalid('membership receipt window is invalid');
|
|
233
|
+
const late = now >= receipt.exp;
|
|
234
|
+
await verifyRegistrationReceipt(receipt, credential, { request_id: binding.request_id, nonce: binding.nonce, registrar_aid: payload.org_id }, resolve, service, late ? receipt.registered_at : now);
|
|
235
|
+
check();
|
|
236
|
+
const validUntil = Math.min(Number(credential.exp), late ? await this.status(credential, actor, false, receipt.registered_at) : receipt.exp);
|
|
237
|
+
check();
|
|
238
|
+
const completedAt = Math.trunc(Date.now() / 1000);
|
|
239
|
+
await verifyCredentialSignature(credential, resolve, completedAt, String(payload.org_id));
|
|
240
|
+
check();
|
|
241
|
+
if (actor !== payload.member_aid)
|
|
242
|
+
return;
|
|
243
|
+
const issuerCertificate = certificates.get(String(credential.signature.cert_fingerprint));
|
|
244
|
+
await this.rememberNow(credential, { membership_binding: binding, registrationReceipt: receipt, terms, version: payload.version,
|
|
245
|
+
service_aid: 'org.' + String(payload.org_id).split('.').slice(1).join('.'), long_term: credential.exp === Math.floor(parseCertMetadata(issuerCertificate).notAfter.getTime() / 1000) }, actor, { ...guard, current: () => guard.current() && Date.now() / 1000 < validUntil });
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
observe(method, params, result) {
|
|
249
|
+
if (method !== 'credential.register' && !method.startsWith('org.'))
|
|
250
|
+
return Promise.resolve();
|
|
251
|
+
return this.serial(async () => {
|
|
252
|
+
if (!isRecord(result))
|
|
253
|
+
return;
|
|
254
|
+
const invitations = isRecord(params.credential_templates) ? Object.entries(params.credential_templates).map(([aid, credential]) => ({ ...params, credential_template: credential, membership_binding: params.membership_bindings?.[aid] })) : [];
|
|
255
|
+
const values = [params, ...['previous_owner', 'new_owner'].map((key) => params[key]).filter(isRecord), ...invitations];
|
|
256
|
+
for (const value of values) {
|
|
257
|
+
const credential = value.credential_template ?? value.credential;
|
|
258
|
+
if (!isRecord(credential))
|
|
259
|
+
continue;
|
|
260
|
+
const member = isRecord(credential.ext) && credential.ext.credential_type === 'member';
|
|
261
|
+
const actor = this.ports.currentAid();
|
|
262
|
+
const existing = (await this.ports.load(actor))[String(credential.cid)];
|
|
263
|
+
let longTerm = existing?.long_term === true;
|
|
264
|
+
if (!longTerm && isRecord(credential.signature)) {
|
|
265
|
+
const cert = await this.ports.certificate(String(credential.iss), String(credential.signature.cert_fingerprint));
|
|
266
|
+
const parent = Array.isArray(credential.proof_chain) ? credential.proof_chain[0] : undefined;
|
|
267
|
+
longTerm = credential.exp === Math.min(Math.floor(parseCertMetadata(cert).notAfter.getTime() / 1000), credential.parent != null && parent ? Number(parent.exp) : Infinity);
|
|
268
|
+
}
|
|
269
|
+
await this.rememberNow(credential, {
|
|
270
|
+
long_term: longTerm, issued: true,
|
|
271
|
+
...(member ? { membership_binding: result.membership_binding ?? value.membership_binding,
|
|
272
|
+
terms: value.terms ?? (value.membership_binding?.role === 'owner' ? { org_id: credential.iss, owner_aid: credential.sub } : undefined),
|
|
273
|
+
version: result.version ?? value.expected_version,
|
|
274
|
+
service_aid: 'org.' + String(credential.iss).split('.').slice(1).join('.') } : {}),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
resume(guard) {
|
|
280
|
+
return this.serial(async () => {
|
|
281
|
+
this.scanGuard = guard;
|
|
282
|
+
try {
|
|
283
|
+
const actor = this.ports.currentAid();
|
|
284
|
+
const records = await this.ports.load(actor);
|
|
285
|
+
for (const [cid, initial] of Object.entries(records)) {
|
|
286
|
+
if (!isRecord(initial.credential))
|
|
287
|
+
continue;
|
|
288
|
+
try {
|
|
289
|
+
if (initial.response?.registration_request && !initial.response.registrationReceipt && Number(initial.response.registration_request.request_binding?.exp) <= Math.trunc(Date.now() / 1000)) {
|
|
290
|
+
if (!initial.registration_confirmation)
|
|
291
|
+
await this.recoverRegistration(actor, cid, initial, records);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (initial.submission && initial.received_result && !initial.successor_cid)
|
|
295
|
+
await this.receiveNow(String(initial.credential.iss), initial.received_result, undefined, true);
|
|
296
|
+
else if (initial.incoming && initial.incoming.payload.exp > Math.trunc(Date.now() / 1000))
|
|
297
|
+
await this.receiveNow(initial.incoming.from, initial.incoming.payload);
|
|
298
|
+
const record = (await this.ports.load(actor))[cid];
|
|
299
|
+
if (record.commit_pending)
|
|
300
|
+
await this.sendCommit(record, actor);
|
|
301
|
+
if (record.revoke_pending)
|
|
302
|
+
await this.revokePrevious(record, actor);
|
|
303
|
+
const credential = record.credential;
|
|
304
|
+
const now = Math.trunc(Date.now() / 1000);
|
|
305
|
+
if (record.successor_cid || !record.long_term || credential.sub !== actor || now >= credential.exp && !record.membership_binding && !record.pending || credential.exp - now >= (credential.exp - credential.iat) / 2)
|
|
306
|
+
continue;
|
|
307
|
+
let request = record.pending;
|
|
308
|
+
if (request?.membership_binding && request.membership_binding.exp <= now)
|
|
309
|
+
throw invalid('renewal member consent has expired');
|
|
310
|
+
if (!request || request.exp <= now) {
|
|
311
|
+
const signer = await this.ports.signer(actor);
|
|
312
|
+
if (!signer)
|
|
313
|
+
continue;
|
|
314
|
+
request = { type: 'credential.renewal.request', ver: 'aun-credential-renewal/1', action: 'renew', issuer_aid: credential.iss,
|
|
315
|
+
subject_aid: actor, previous_credential_cid: cid, credential_hash: digest(credential), request_id: request?.request_id ?? organizationIdentifier(), nonce: request?.nonce ?? organizationNonce(), iat: now, exp: now + 300,
|
|
316
|
+
...(request?.membership_binding ? { membership_binding: request.membership_binding, expected_version: request.expected_version } : {}) };
|
|
317
|
+
if (record.membership_binding && !request.membership_binding) {
|
|
318
|
+
if (now < credential.exp) {
|
|
319
|
+
const credentials = { [record.membership_binding.role === 'admin' ? 'approval_credential' : 'member_credential']: credential, membership_binding: record.membership_binding };
|
|
320
|
+
const query = await prepareOrganizationRequest('org.member.get', { org_id: credential.iss, member_aid: actor, credentials }, record.service_aid, signer);
|
|
321
|
+
const current = await this.ports.call('org.member.get', query);
|
|
322
|
+
const member = current.member ?? current;
|
|
323
|
+
if (!isInt(member.version) || member.credential_cid !== undefined && member.credential_cid !== cid)
|
|
324
|
+
throw invalid('current membership differs from managed credential');
|
|
325
|
+
record.version = member.version;
|
|
326
|
+
}
|
|
327
|
+
if (!isInt(record.version))
|
|
328
|
+
throw invalid('current membership version is unavailable');
|
|
329
|
+
const binding = { ...record.membership_binding, binding_version: Number(record.membership_binding.binding_version) + 1,
|
|
330
|
+
request_id: request.request_id, nonce: request.nonce, iat: now, exp: now + 3600 };
|
|
331
|
+
delete binding.member_signature;
|
|
332
|
+
delete binding.org_signature;
|
|
333
|
+
request.membership_binding = await signMembershipBinding(binding, signer, 'member');
|
|
334
|
+
request.expected_version = record.version;
|
|
335
|
+
}
|
|
336
|
+
request.signature = await signProtocolPayload(request, signer);
|
|
337
|
+
record.pending = request;
|
|
338
|
+
const latest = await this.ports.load(actor);
|
|
339
|
+
latest[cid] = record;
|
|
340
|
+
await this.ports.save(actor, latest);
|
|
341
|
+
}
|
|
342
|
+
if (this.ports.currentAid() !== actor)
|
|
343
|
+
return;
|
|
344
|
+
const localIssuer = record.membership_binding && await this.ports.signer(String(credential.iss));
|
|
345
|
+
if (localIssuer) {
|
|
346
|
+
await this.receiveNow(actor, request, String(credential.iss));
|
|
347
|
+
const issued = (await this.ports.load(String(credential.iss)))[cid]?.response;
|
|
348
|
+
if (issued)
|
|
349
|
+
await this.receiveNow(String(credential.iss), issued);
|
|
350
|
+
}
|
|
351
|
+
else
|
|
352
|
+
await this.ports.send(String(credential.iss), request);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
const latest = await this.ports.load(actor);
|
|
356
|
+
if (latest[cid]) {
|
|
357
|
+
latest[cid].last_error = error instanceof Error ? error.message : String(error);
|
|
358
|
+
await this.ports.save(actor, latest);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
this.scanGuard = undefined;
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
receive(from, payload) {
|
|
369
|
+
return this.serial(() => this.receiveNow(from, payload));
|
|
370
|
+
}
|
|
371
|
+
async verifyEnvelope(from, payload, expected, replay = false) {
|
|
372
|
+
const now = Math.trunc(Date.now() / 1000);
|
|
373
|
+
if (from !== expected || payload.ver !== 'aun-credential-renewal/1' || !isInt(payload.iat) || !isInt(payload.exp) || payload.iat > (replay ? payload.iat : now) || payload.exp <= (replay ? payload.iat : now) || payload.exp > payload.iat + 300 || !isRecord(payload.signature))
|
|
374
|
+
throw invalid('renewal message identity or validity is invalid');
|
|
375
|
+
const cert = await this.ports.certificate(expected, String(payload.signature.cert_fingerprint));
|
|
376
|
+
const unsigned = { ...payload };
|
|
377
|
+
delete unsigned.signature;
|
|
378
|
+
await verifyAgentSignature(payload.signature, unsigned, expected, () => cert, now);
|
|
379
|
+
}
|
|
380
|
+
async status(credential, actor, allowExpired = false, registrationConfirmedAt, confirmation) {
|
|
381
|
+
const target = 'credential.' + String(credential.iss).split('.').slice(1).join('.');
|
|
382
|
+
const requestedAt = Math.trunc(Date.now() / 1000);
|
|
383
|
+
const query = { credential_cid: credential.cid, request_id: organizationIdentifier(), nonce: organizationNonce(), include_chain: true };
|
|
384
|
+
query.request_binding = buildRequestBinding('credential.status', target, query, String(credential.cid), String(query.nonce), String(query.request_id), requestedAt, requestedAt + 300);
|
|
385
|
+
const proof = await this.ports.call('credential.status', query);
|
|
386
|
+
const now = Math.trunc(Date.now() / 1000);
|
|
387
|
+
if (!isRecord(proof) || proof.ver !== 'aun-credential-status/1' || proof.credentialCid !== credential.cid || proof.credential_hash !== digest(credential)
|
|
388
|
+
|| proof.issuerAid !== credential.iss || proof.subjectAid !== credential.sub || proof.expiresAt !== credential.exp || proof.responder !== target
|
|
389
|
+
|| proof.request_id !== query.request_id || proof.nonce !== query.nonce || proof.target !== actor
|
|
390
|
+
|| !isInt(proof.asOf) || !isInt(proof.proofExpiresAt) || proof.asOf > now + 300 || proof.proofExpiresAt <= proof.asOf || proof.proofExpiresAt <= now || proof.proofExpiresAt > proof.asOf + 300
|
|
391
|
+
|| !isRecord(proof.signature) || proof.signature.timestamp !== proof.asOf)
|
|
392
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'renewal status proof does not match the query');
|
|
393
|
+
const cert = await this.ports.certificate(target, String(proof.signature.cert_fingerprint));
|
|
394
|
+
const unsigned = { ...proof };
|
|
395
|
+
delete unsigned.signature;
|
|
396
|
+
await verifyAgentSignature(proof.signature, unsigned, target, () => cert, now, false);
|
|
397
|
+
if (proof.status !== 'active' && !(allowExpired && proof.status === 'expired' && proof.credentialStatus === 'expired' && Array.isArray(proof.chain) && proof.chain.every((item) => item.status === 'active')))
|
|
398
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'renewal credential is not active');
|
|
399
|
+
if (registrationConfirmedAt !== undefined && (proof.asOf < requestedAt || proof.asOf > now || !isInt(proof.registrationConfirmedAt) || proof.registrationConfirmedAt < registrationConfirmedAt || proof.registrationConfirmedAt > proof.asOf || proof.credentialStatus !== 'active' || proof.proofExpiresAt <= Math.trunc(Date.now() / 1000)))
|
|
400
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'membership registration confirmation is invalid');
|
|
401
|
+
if (confirmation)
|
|
402
|
+
Object.assign(confirmation, structuredClone(proof));
|
|
403
|
+
return proof.proofExpiresAt;
|
|
404
|
+
}
|
|
405
|
+
async receiveNow(from, value, localIssuer, replay = false) {
|
|
406
|
+
if (!isRecord(value) || !['credential.renewal.request', 'credential.renewal.result'].includes(String(value.type)))
|
|
407
|
+
return false;
|
|
408
|
+
const payload = value;
|
|
409
|
+
const isRequest = payload.type === 'credential.renewal.request';
|
|
410
|
+
const actor = localIssuer ?? this.ports.currentAid();
|
|
411
|
+
await this.verifyEnvelope(from, payload, String(isRequest ? payload.subject_aid : payload.issuer_aid), replay);
|
|
412
|
+
if (actor !== (isRequest ? payload.issuer_aid : payload.subject_aid))
|
|
413
|
+
throw invalid('renewal message has a different recipient');
|
|
414
|
+
let records = await this.ports.load(actor);
|
|
415
|
+
const cid = String(payload.previous_credential_cid);
|
|
416
|
+
const record = records[cid];
|
|
417
|
+
if (!record || !record.long_term || payload.credential_hash !== digest(record.credential) || payload.issuer_aid !== record.credential.iss || payload.subject_aid !== record.credential.sub)
|
|
418
|
+
throw invalid('renewal does not match a known grant');
|
|
419
|
+
if (!isRequest && record.successor_cid === payload.credential?.cid) {
|
|
420
|
+
if (record.commit_pending)
|
|
421
|
+
await this.sendCommit(record, actor);
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
if (replay && (!record.submission || digest(record.received_result) !== digest(payload)))
|
|
425
|
+
throw invalid('renewal replay differs from the saved result');
|
|
426
|
+
record.incoming = { from, payload: structuredClone(payload) };
|
|
427
|
+
await this.ports.save(actor, records);
|
|
428
|
+
if (isRequest && payload.action === 'commit') {
|
|
429
|
+
if (!record.response || record.response.request_id !== payload.request_id || record.response.nonce !== payload.nonce || record.response.credential.cid !== payload.new_credential_cid)
|
|
430
|
+
throw invalid('renewal acknowledgement differs from issued result');
|
|
431
|
+
record.revoke_pending = true;
|
|
432
|
+
await this.ports.save(actor, records);
|
|
433
|
+
await this.revokePrevious(record, actor);
|
|
434
|
+
}
|
|
435
|
+
else if (isRequest && payload.action === 'renew') {
|
|
436
|
+
if (payload.exp > payload.iat + 300)
|
|
437
|
+
throw invalid('renewal request window is too long');
|
|
438
|
+
if (record.successor_cid && record.response?.request_id !== payload.request_id)
|
|
439
|
+
throw invalid('grant was already replaced');
|
|
440
|
+
if (!record.response || record.response.request_id !== payload.request_id) {
|
|
441
|
+
await this.status(record.credential, this.ports.currentAid(), !!record.membership_binding);
|
|
442
|
+
const signer = await this.ports.signer(actor);
|
|
443
|
+
if (!signer)
|
|
444
|
+
throw invalid('issuer signing identity is unavailable');
|
|
445
|
+
const now = Math.trunc(Date.now() / 1000);
|
|
446
|
+
const response = { type: 'credential.renewal.result', ver: payload.ver, action: 'issued', issuer_aid: actor, subject_aid: from,
|
|
447
|
+
previous_credential_cid: cid, credential_hash: payload.credential_hash, request_id: payload.request_id, nonce: payload.nonce, iat: now };
|
|
448
|
+
if (record.membership_binding) {
|
|
449
|
+
const binding = payload.membership_binding;
|
|
450
|
+
const previous = record.membership_binding;
|
|
451
|
+
if (!isRecord(binding) || binding.binding_version !== Number(previous.binding_version) + 1 || binding.request_id !== payload.request_id || binding.nonce !== payload.nonce
|
|
452
|
+
|| ['org_id', 'member_aid', 'membership_id', 'member_type', 'category', 'role', 'ownership', 'visibility', 'terms_hash'].some((key) => binding[key] !== previous[key]))
|
|
453
|
+
throw invalid('renewal changes membership terms');
|
|
454
|
+
const memberSignature = binding.member_signature;
|
|
455
|
+
const memberCert = await this.ports.certificate(from, String(memberSignature?.cert_fingerprint));
|
|
456
|
+
const unsigned = { ...binding };
|
|
457
|
+
delete unsigned.member_signature;
|
|
458
|
+
delete unsigned.org_signature;
|
|
459
|
+
await verifyAgentSignature(memberSignature, unsigned, from, () => memberCert, now);
|
|
460
|
+
const prepared = await prepareOrganizationRequest('org.member.renewCredential', { org_id: actor, member_aid: from, previous_credential_cid: cid,
|
|
461
|
+
expected_version: payload.expected_version, terms: record.terms, membership_binding: binding }, record.service_aid, signer, { orgSigner: signer, memberSigner: signer,
|
|
462
|
+
credentialOptions: Object.fromEntries(['security_level', 'revocation_endpoint', 'credential_service'].filter((key) => key in record.credential).map((key) => [key, record.credential[key]])) });
|
|
463
|
+
delete prepared.request_binding;
|
|
464
|
+
response.renewal_params = prepared;
|
|
465
|
+
response.credential = prepared.credential_template;
|
|
466
|
+
response.exp = Math.min(now + 300, Number(binding.exp), Number(response.credential.exp));
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
if (actor !== this.ports.currentAid())
|
|
470
|
+
throw invalid('generic renewal requires the issuer session');
|
|
471
|
+
let options = { ...record.credential };
|
|
472
|
+
for (const key of ['signature', 'cid', 'iat', 'exp'])
|
|
473
|
+
delete options[key];
|
|
474
|
+
options.iat = now;
|
|
475
|
+
options.nbf = Math.max(Number(record.credential.nbf), now);
|
|
476
|
+
if (options.parent) {
|
|
477
|
+
let parent = records[String(options.parent)];
|
|
478
|
+
const seen = new Set();
|
|
479
|
+
while (parent?.successor_cid && !seen.has(String(parent.credential.cid))) {
|
|
480
|
+
seen.add(String(parent.credential.cid));
|
|
481
|
+
parent = records[String(parent.successor_cid)];
|
|
482
|
+
}
|
|
483
|
+
const successor = parent?.credential;
|
|
484
|
+
if (successor) {
|
|
485
|
+
options.parent = successor.cid;
|
|
486
|
+
options.proof_chain = [successor, ...(successor.proof_chain ?? [])];
|
|
487
|
+
options.nbf = Math.max(Number(options.nbf), Number(successor.nbf));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
options.key_version = signer.certFingerprint;
|
|
491
|
+
response.credential = await createCredential(options, signer);
|
|
492
|
+
response.exp = Math.min(now + 300, Number(response.credential.exp));
|
|
493
|
+
const registration = { request_id: payload.request_id, nonce: payload.nonce, registrar_aid: actor, credential: response.credential };
|
|
494
|
+
registration.request_binding = buildRequestBinding('credential.register', 'credential.' + actor.split('.').slice(1).join('.'), registration, String(response.credential.cid), payload.nonce, payload.request_id, now, now + 300);
|
|
495
|
+
response.registration_request = registration;
|
|
496
|
+
record.response = response;
|
|
497
|
+
await this.ports.save(actor, records);
|
|
498
|
+
}
|
|
499
|
+
record.response = response;
|
|
500
|
+
await this.ports.save(actor, records);
|
|
501
|
+
}
|
|
502
|
+
const response = record.response;
|
|
503
|
+
if (response.registration_request && !response.registrationReceipt) {
|
|
504
|
+
if (Number(response.registration_request.request_binding?.exp) <= Math.trunc(Date.now() / 1000)) {
|
|
505
|
+
if (!record.registration_confirmation)
|
|
506
|
+
await this.recoverRegistration(actor, cid, record, records);
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
const registered = await this.ports.call('credential.register', response.registration_request, async () => {
|
|
510
|
+
const sentAt = Math.trunc(Date.now() / 1000);
|
|
511
|
+
if (this.ports.currentAid() !== actor || sentAt >= Number(response.registration_request.request_binding.exp))
|
|
512
|
+
throw invalid('registration expired before it was sent');
|
|
513
|
+
if (this.ports.atomicCheckpointSave?.() !== true)
|
|
514
|
+
return;
|
|
515
|
+
const deadline = Number(response.registration_request.request_binding.exp), hash = digest(response.registration_request);
|
|
516
|
+
record.registration_submitted_at ??= sentAt;
|
|
517
|
+
record.registration_submission_hash = hash;
|
|
518
|
+
await this.ports.save(actor, records, { current: () => this.ports.currentAid() === actor && Date.now() / 1000 < deadline && digest(response.registration_request) === hash });
|
|
519
|
+
});
|
|
520
|
+
await this.receipt(response.credential, registered.registrationReceipt, response.registration_request);
|
|
521
|
+
await this.status(response.credential, actor);
|
|
522
|
+
response.registrationReceipt = registered.registrationReceipt;
|
|
523
|
+
}
|
|
524
|
+
const responseNow = Math.trunc(Date.now() / 1000);
|
|
525
|
+
const responseLimit = Math.min(responseNow + 300, Number(response.credential.exp), ...['membership_binding', 'credential_template', 'registration_authorization'].map((key) => Number(response.renewal_params?.[key]?.exp ?? Infinity)));
|
|
526
|
+
if (responseLimit <= responseNow)
|
|
527
|
+
throw invalid('renewal result authorization has expired');
|
|
528
|
+
if (!response.signature || response.exp <= responseNow || response.exp > responseLimit) {
|
|
529
|
+
response.iat = responseNow;
|
|
530
|
+
response.exp = responseLimit;
|
|
531
|
+
delete response.signature;
|
|
532
|
+
response.signature = await signProtocolPayload(response, (await this.ports.signer(actor)));
|
|
533
|
+
}
|
|
534
|
+
await this.ports.save(actor, records);
|
|
535
|
+
if (!localIssuer)
|
|
536
|
+
await this.ports.send(from, response);
|
|
537
|
+
}
|
|
538
|
+
else if (!isRequest && payload.action === 'issued') {
|
|
539
|
+
if (!record.pending || record.pending.request_id !== payload.request_id || record.pending.nonce !== payload.nonce || record.successor_cid && record.successor_cid !== payload.credential?.cid)
|
|
540
|
+
throw invalid('renewal result does not match pending request');
|
|
541
|
+
const credential = payload.credential;
|
|
542
|
+
if (!isRecord(credential) || credential.cid === cid || ['iss', 'sub', 'aud', 'cap', 'delegate', 'delegation_depth', 'security_level', 'revocation_endpoint', 'credential_service'].some((key) => canonicalText(credential[key] ?? null) !== canonicalText(record.credential[key] ?? null)))
|
|
543
|
+
throw invalid('renewal result changes the grant');
|
|
544
|
+
if (Number(credential.nbf) < Number(record.credential.nbf))
|
|
545
|
+
throw invalid('renewal moves the grant activation earlier');
|
|
546
|
+
const expectedExt = record.membership_binding ? { ...record.credential.ext, binding_version: Number(record.membership_binding.binding_version) + 1 } : record.credential.ext;
|
|
547
|
+
if (canonicalText(credential.ext ?? null) !== canonicalText(expectedExt ?? null))
|
|
548
|
+
throw invalid('renewal result changes grant extensions');
|
|
549
|
+
const credentialSignature = credential.signature;
|
|
550
|
+
const certificate = await this.ports.certificate(from, String(credentialSignature?.cert_fingerprint));
|
|
551
|
+
await verifyCredentialSignature(credential, () => certificate, Math.trunc(Date.now() / 1000));
|
|
552
|
+
const previousChain = credentialChainDocuments(record.credential);
|
|
553
|
+
const renewedChain = credentialChainDocuments(credential);
|
|
554
|
+
if (previousChain.length !== renewedChain.length)
|
|
555
|
+
throw invalid('renewal changes the authorization chain');
|
|
556
|
+
for (let index = 1; index < renewedChain.length; index++) {
|
|
557
|
+
const parent = renewedChain[index], previous = previousChain[index];
|
|
558
|
+
if (Number(parent.nbf) < Number(previous.nbf) || ['iss', 'sub', 'aud', 'cap', 'delegate', 'delegation_depth', 'ext', 'security_level', 'revocation_endpoint', 'credential_service'].some((key) => canonicalText(parent[key] ?? null) !== canonicalText(previous[key] ?? null)))
|
|
559
|
+
throw invalid('renewal changes parent authorization');
|
|
560
|
+
const cert = await this.ports.certificate(String(parent.iss), String(parent.signature.cert_fingerprint));
|
|
561
|
+
await verifyCredentialSignature(parent, () => cert, Math.trunc(Date.now() / 1000), undefined, renewedChain.slice(index + 1));
|
|
562
|
+
await this.status(parent, actor);
|
|
563
|
+
}
|
|
564
|
+
let material = {};
|
|
565
|
+
let recoveryValidUntil = Infinity;
|
|
566
|
+
if (record.membership_binding) {
|
|
567
|
+
const params = payload.renewal_params;
|
|
568
|
+
if (!isRecord(params) || digest(params.credential_template) !== digest(credential) || params.previous_credential_cid !== cid
|
|
569
|
+
|| !isRecord(params.membership_binding) || digest(params.membership_binding.member_signature) !== digest(record.pending.membership_binding.member_signature)
|
|
570
|
+
|| params.expected_version !== record.pending.expected_version || params.org_id !== record.credential.iss || params.member_aid !== actor
|
|
571
|
+
|| digest(params.terms) !== digest(record.terms))
|
|
572
|
+
throw invalid('renewal membership result is invalid');
|
|
573
|
+
const acceptedBinding = { ...params.membership_binding };
|
|
574
|
+
delete acceptedBinding.org_signature;
|
|
575
|
+
if (digest(acceptedBinding) !== digest(record.pending.membership_binding))
|
|
576
|
+
throw invalid('renewal changes the signed member binding');
|
|
577
|
+
const signer = await this.ports.signer(actor);
|
|
578
|
+
if (!signer)
|
|
579
|
+
throw invalid('member signing identity is unavailable');
|
|
580
|
+
const consentEnd = Math.min(Number(payload.exp), Number(params.membership_binding.exp), Number(params.registration_authorization?.exp), Number(credential.exp));
|
|
581
|
+
if (replay && consentEnd <= Math.trunc(Date.now() / 1000)) {
|
|
582
|
+
if (this.ports.atomicCheckpointSave?.() !== true)
|
|
583
|
+
throw invalid('atomic checkpoint persistence is required for renewal recovery');
|
|
584
|
+
if (!isInt(record.submitted_at) || record.submitted_at < payload.iat || record.submitted_at >= consentEnd || record.submission_hash !== digest(record.submission))
|
|
585
|
+
throw invalid('renewal operation was not submitted within its authorization window');
|
|
586
|
+
const original = { ...record.submission };
|
|
587
|
+
delete original.request_binding;
|
|
588
|
+
delete original.client_signature;
|
|
589
|
+
const expected = { ...params };
|
|
590
|
+
delete expected.request_binding;
|
|
591
|
+
delete expected.client_signature;
|
|
592
|
+
if (digest(original) !== digest(expected))
|
|
593
|
+
throw invalid('renewal submission differs from the original result');
|
|
594
|
+
const credentialProof = {};
|
|
595
|
+
const proofExpiresAt = await this.status(credential, actor, false, record.submitted_at, credentialProof);
|
|
596
|
+
const query = await prepareOrganizationRequest('org.member.get', { org_id: credential.iss, member_aid: actor,
|
|
597
|
+
credentials: { [params.membership_binding.role === 'admin' ? 'approval_credential' : 'member_credential']: credential, membership_binding: params.membership_binding } }, record.service_aid, signer);
|
|
598
|
+
const current = await this.ports.call('org.member.get', query);
|
|
599
|
+
const proof = current.membership_status;
|
|
600
|
+
const now = Math.trunc(Date.now() / 1000);
|
|
601
|
+
if (!isRecord(proof) || proof.ver !== 'aun-membership-status/1' || proof.org_id !== credential.iss || proof.member_aid !== actor || proof.credential_cid !== credential.cid
|
|
602
|
+
|| proof.membership_id !== params.membership_binding.membership_id || proof.role !== params.membership_binding.role || proof.version !== Number(params.expected_version) + 1
|
|
603
|
+
|| proof.status !== 'active' || proof.credential_status !== 'active' || proof.responder !== record.service_aid || proof.target !== actor || proof.request_id !== query.request_id || proof.nonce !== query.nonce
|
|
604
|
+
|| !isInt(proof.as_of) || proof.as_of < Number(query.request_binding.iat) || proof.as_of > now || !isInt(proof.expires_at) || proof.expires_at <= now || proof.expires_at > proof.as_of + 300
|
|
605
|
+
|| !isRecord(proof.signature) || proof.signature.timestamp !== proof.as_of || proofExpiresAt <= now)
|
|
606
|
+
throw invalid('renewal membership confirmation is invalid');
|
|
607
|
+
const cert = await this.ports.certificate(record.service_aid, String(proof.signature.cert_fingerprint));
|
|
608
|
+
const unsigned = { ...proof };
|
|
609
|
+
delete unsigned.signature;
|
|
610
|
+
await verifyAgentSignature(proof.signature, unsigned, record.service_aid, () => cert, now, false);
|
|
611
|
+
recoveryValidUntil = Math.min(proofExpiresAt, proof.expires_at, Number(credential.exp));
|
|
612
|
+
material = { membership_binding: params.membership_binding, terms: record.terms, version: proof.version, service_aid: record.service_aid,
|
|
613
|
+
recovery_confirmation: { membership_status: proof, credential_status: credentialProof } };
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
const prepared = record.submission ? structuredClone(record.submission) : await prepareOrganizationRequest('org.member.renewCredential', params, record.service_aid, signer);
|
|
617
|
+
delete prepared.request_binding;
|
|
618
|
+
delete prepared.client_signature;
|
|
619
|
+
const submittedAt = Math.trunc(Date.now() / 1000);
|
|
620
|
+
prepared.request_binding = buildRequestBinding('org.member.renewCredential', record.service_aid, prepared, String(credential.cid), String(prepared.nonce), String(prepared.request_id), submittedAt, Math.min(submittedAt + 300, Number(credential.exp)));
|
|
621
|
+
record.submission = structuredClone(prepared);
|
|
622
|
+
record.received_result = structuredClone(payload);
|
|
623
|
+
records[cid] = record;
|
|
624
|
+
await this.ports.save(actor, records);
|
|
625
|
+
const result = await this.ports.call('org.member.renewCredential', prepared, async () => {
|
|
626
|
+
const sentAt = Math.trunc(Date.now() / 1000);
|
|
627
|
+
if (this.ports.currentAid() !== actor || sentAt >= Math.min(consentEnd, Number(prepared.request_binding.exp)))
|
|
628
|
+
throw invalid('renewal authorization expired before it was sent');
|
|
629
|
+
if (this.ports.atomicCheckpointSave?.() !== true)
|
|
630
|
+
return;
|
|
631
|
+
const deadline = Math.min(consentEnd, Number(prepared.request_binding.exp)), hash = digest(record.submission);
|
|
632
|
+
record.submitted_at ??= sentAt;
|
|
633
|
+
record.submission_hash = hash;
|
|
634
|
+
await this.ports.save(actor, records, { current: () => this.ports.currentAid() === actor && Date.now() / 1000 < deadline && digest(record.submission) === hash });
|
|
635
|
+
});
|
|
636
|
+
await this.receipt(credential, result.registrationReceipt, prepared);
|
|
637
|
+
await this.status(credential, actor);
|
|
638
|
+
material = { membership_binding: result.membership_binding ?? params.membership_binding, terms: record.terms, version: result.version, service_aid: record.service_aid };
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
await this.receipt(credential, payload.registrationReceipt, payload.registration_request);
|
|
643
|
+
await this.status(credential, actor);
|
|
644
|
+
}
|
|
645
|
+
records = await this.ports.load(actor);
|
|
646
|
+
records[String(credential.cid)] = { credential: structuredClone(credential), long_term: true, ...material };
|
|
647
|
+
records[cid].successor_cid = credential.cid;
|
|
648
|
+
records[cid].response = structuredClone(payload);
|
|
649
|
+
records[cid].commit_pending = true;
|
|
650
|
+
const recoveryGuard = { current: () => Date.now() / 1000 >= Number(credential.nbf) && Date.now() / 1000 < Math.min(recoveryValidUntil, Number(credential.exp)) };
|
|
651
|
+
if (!recoveryGuard.current())
|
|
652
|
+
throw invalid('renewal recovery confirmation has expired');
|
|
653
|
+
await this.ports.save(actor, records, recoveryGuard);
|
|
654
|
+
await this.sendCommit(records[cid], actor);
|
|
655
|
+
}
|
|
656
|
+
else
|
|
657
|
+
throw invalid('renewal message action is invalid');
|
|
658
|
+
records = await this.ports.load(actor);
|
|
659
|
+
delete records[cid].incoming;
|
|
660
|
+
await this.ports.save(actor, records);
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
async recoverRegistration(actor, cid, record, records) {
|
|
664
|
+
if (this.ports.atomicCheckpointSave?.() !== true)
|
|
665
|
+
throw invalid('atomic checkpoint persistence is required for registration recovery');
|
|
666
|
+
const response = record.response, request = response.registration_request, binding = request.request_binding;
|
|
667
|
+
if (!isInt(record.registration_submitted_at) || record.registration_submitted_at < binding.iat || record.registration_submitted_at >= binding.exp
|
|
668
|
+
|| record.registration_submission_hash !== digest(request) || digest(request.credential) !== digest(response.credential))
|
|
669
|
+
throw invalid('registration was not sent within its authorization window');
|
|
670
|
+
const credential = response.credential;
|
|
671
|
+
const certificate = await this.ports.certificate(String(credential.iss), String(credential.signature?.cert_fingerprint));
|
|
672
|
+
await verifyCredentialSignature(credential, () => certificate, Math.trunc(Date.now() / 1000));
|
|
673
|
+
const confirmation = {};
|
|
674
|
+
const validUntil = await this.status(credential, actor, false, record.registration_submitted_at, confirmation);
|
|
675
|
+
records[String(credential.cid)] = { credential: structuredClone(credential), long_term: true, issued: true };
|
|
676
|
+
records[cid].registration_confirmation = confirmation;
|
|
677
|
+
const guard = { current: () => Date.now() / 1000 >= Number(credential.nbf) && Date.now() / 1000 < Math.min(validUntil, Number(credential.exp)) };
|
|
678
|
+
if (!guard.current())
|
|
679
|
+
throw invalid('registration recovery confirmation has expired');
|
|
680
|
+
await this.ports.save(actor, records, guard);
|
|
681
|
+
}
|
|
682
|
+
async receipt(credential, receipt, request) {
|
|
683
|
+
const service = 'credential.' + String(credential.iss).split('.').slice(1).join('.');
|
|
684
|
+
if (!isRecord(receipt) || !isRecord(receipt.signature))
|
|
685
|
+
throw invalid('renewal registration receipt is missing');
|
|
686
|
+
const cert = await this.ports.certificate(service, String(receipt.signature.cert_fingerprint));
|
|
687
|
+
await verifyRegistrationReceipt(receipt, credential, request, () => cert, service, Math.trunc(Date.now() / 1000));
|
|
688
|
+
}
|
|
689
|
+
async sendCommit(record, actor) {
|
|
690
|
+
const signer = await this.ports.signer(actor);
|
|
691
|
+
if (!signer)
|
|
692
|
+
throw invalid('renewal holder signing identity is unavailable');
|
|
693
|
+
const now = Math.trunc(Date.now() / 1000);
|
|
694
|
+
const payload = { ...record.pending, action: 'commit', new_credential_cid: record.successor_cid, iat: now, exp: now + 300 };
|
|
695
|
+
delete payload.signature;
|
|
696
|
+
payload.signature = await signProtocolPayload(payload, signer);
|
|
697
|
+
const localIssuer = record.membership_binding && await this.ports.signer(String(record.credential.iss));
|
|
698
|
+
if (localIssuer)
|
|
699
|
+
await this.receiveNow(actor, payload, String(record.credential.iss));
|
|
700
|
+
else
|
|
701
|
+
await this.ports.send(String(record.credential.iss), payload);
|
|
702
|
+
const records = await this.ports.load(actor);
|
|
703
|
+
delete records[String(record.credential.cid)].commit_pending;
|
|
704
|
+
await this.ports.save(actor, records);
|
|
705
|
+
}
|
|
706
|
+
async revokePrevious(record, actor) {
|
|
707
|
+
await this.status(record.response.credential, this.ports.currentAid());
|
|
708
|
+
if (!record.membership_binding && !record.credential.delegate) {
|
|
709
|
+
const now = Math.trunc(Date.now() / 1000);
|
|
710
|
+
const request = { credential_cid: record.credential.cid, registrar_aid: actor,
|
|
711
|
+
request_id: record.response.request_id + '-revoke', nonce: record.response.nonce + '-revoke', reason_code: 'credential_renewed', cascade: false };
|
|
712
|
+
request.request_binding = buildRequestBinding('credential.revoke', 'credential.' + actor.split('.').slice(1).join('.'), request, String(record.credential.cid), String(request.nonce), String(request.request_id), now, now + 300);
|
|
713
|
+
await this.ports.call('credential.revoke', request);
|
|
714
|
+
}
|
|
715
|
+
const records = await this.ports.load(actor);
|
|
716
|
+
const credential = record.response.credential;
|
|
717
|
+
records[String(credential.cid)] = { ...records[String(credential.cid)], credential: structuredClone(credential), long_term: true,
|
|
718
|
+
...(record.membership_binding ? { membership_binding: record.response.renewal_params.membership_binding, terms: record.terms,
|
|
719
|
+
version: Number(record.response.renewal_params.expected_version) + 1, service_aid: record.service_aid } : {}) };
|
|
720
|
+
records[String(record.credential.cid)].successor_cid = record.response.credential.cid;
|
|
721
|
+
delete records[String(record.credential.cid)].revoke_pending;
|
|
722
|
+
await this.ports.save(actor, records);
|
|
723
|
+
}
|
|
724
|
+
revocationAuthority(document, record) {
|
|
725
|
+
const derived = 'credential.' + String(document.iss).split('.').slice(1).join('.');
|
|
726
|
+
const authority = record?.credential_authority ?? derived;
|
|
727
|
+
if (typeof authority !== 'string' || !/^credential\.[^\s/]+$/.test(authority) || !AID_RE.test(String(document.iss)))
|
|
728
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'credential authority is unavailable');
|
|
729
|
+
return authority;
|
|
730
|
+
}
|
|
731
|
+
revocationState(value) {
|
|
732
|
+
if (value == null || isRecord(value) && Object.keys(value).length === 0)
|
|
733
|
+
return { version: 1, authorities: {} };
|
|
734
|
+
if (!isRecord(value) || value.version !== 1 || !isRecord(value.authorities))
|
|
735
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'credential revocation storage is damaged');
|
|
736
|
+
for (const [authority, partition] of Object.entries(value.authorities)) {
|
|
737
|
+
if (!/^credential\.[^\s/]+$/.test(authority) || !isRecord(partition) || !isRecord(partition.revoked) || !isInt(partition.since_event_seq) || partition.since_event_seq < 0
|
|
738
|
+
|| typeof partition.cursor !== 'string' || partition.cursor !== '' && (!/^(0|[1-9][0-9]*)$/.test(partition.cursor) || !isInt(Number(partition.cursor)) || Number(partition.cursor) !== partition.since_event_seq)
|
|
739
|
+
|| !isInt(partition.as_of) || !isInt(partition.expires_at) || partition.expires_at <= partition.as_of
|
|
740
|
+
|| Object.values(partition.revoked).some((sequence) => !isInt(sequence) || sequence <= 0 || sequence > Number(partition.since_event_seq)))
|
|
741
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'credential revocation storage is damaged');
|
|
742
|
+
}
|
|
743
|
+
return value;
|
|
744
|
+
}
|
|
745
|
+
async _assertNotRevoked(credential, proofChain) {
|
|
746
|
+
if (!this.ports.loadRevocations)
|
|
747
|
+
return;
|
|
748
|
+
const actor = this.ports.currentAid();
|
|
749
|
+
const state = this.revocationState(await this.ports.loadRevocations(actor));
|
|
750
|
+
const records = await this.ports.load(actor);
|
|
751
|
+
if (!isRecord(records))
|
|
752
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'managed credential storage is unavailable');
|
|
753
|
+
const documents = [credential, ...(proofChain ?? [])];
|
|
754
|
+
const seen = new Set();
|
|
755
|
+
for (let index = 0; index < documents.length; index++) {
|
|
756
|
+
const document = documents[index];
|
|
757
|
+
if (!isRecord(document) || seen.has(document))
|
|
758
|
+
continue;
|
|
759
|
+
seen.add(document);
|
|
760
|
+
if (seen.size > 128)
|
|
761
|
+
throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'credential chain is too deep');
|
|
762
|
+
const authority = this.revocationAuthority(document, records[String(document.cid)]);
|
|
763
|
+
const revoked = state.authorities[authority]?.revoked;
|
|
764
|
+
if (revoked && Object.hasOwn(revoked, String(document.cid)))
|
|
765
|
+
throw new ProtocolError('CREDENTIAL_REVOKED', 'credential was revoked');
|
|
766
|
+
if (Array.isArray(document.proof_chain))
|
|
767
|
+
documents.push(...document.proof_chain);
|
|
768
|
+
}
|
|
769
|
+
if (this.ports.currentAid() !== actor)
|
|
770
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'credential receiver identity changed');
|
|
771
|
+
}
|
|
772
|
+
_syncRevocations(guard) {
|
|
773
|
+
if (!this.ports.loadRevocations || !this.ports.saveRevocations)
|
|
774
|
+
return Promise.resolve();
|
|
775
|
+
return this.serial(async () => {
|
|
776
|
+
const actor = this.ports.currentAid();
|
|
777
|
+
const check = () => { if (this.ports.currentAid() !== actor || guard && !guard.current())
|
|
778
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'credential revocation synchronization was superseded'); };
|
|
779
|
+
check();
|
|
780
|
+
let state = structuredClone(this.revocationState(await this.ports.loadRevocations(actor)));
|
|
781
|
+
check();
|
|
782
|
+
const records = await this.ports.load(actor);
|
|
783
|
+
check();
|
|
784
|
+
const authorities = new Set();
|
|
785
|
+
if (AID_RE.test(actor))
|
|
786
|
+
authorities.add('credential.' + actor.split('.').slice(1).join('.'));
|
|
787
|
+
for (const record of Object.values(records)) {
|
|
788
|
+
if (!isRecord(record) || !isRecord(record.credential))
|
|
789
|
+
continue;
|
|
790
|
+
validateCredential(record.credential);
|
|
791
|
+
const documents = [record.credential];
|
|
792
|
+
const seen = new Set();
|
|
793
|
+
for (let index = 0; index < documents.length; index++) {
|
|
794
|
+
const document = documents[index];
|
|
795
|
+
if (!isRecord(document) || seen.has(document))
|
|
796
|
+
continue;
|
|
797
|
+
seen.add(document);
|
|
798
|
+
if (seen.size > 128)
|
|
799
|
+
throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'credential chain is too deep');
|
|
800
|
+
authorities.add(this.revocationAuthority(document, document === record.credential ? record : undefined));
|
|
801
|
+
if (Array.isArray(document.proof_chain))
|
|
802
|
+
documents.push(...document.proof_chain);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
for (const authority of authorities) {
|
|
806
|
+
for (let page = 0; page < 32; page++) {
|
|
807
|
+
check();
|
|
808
|
+
const previous = state.authorities[authority] ?? { revoked: {}, since_event_seq: 0, cursor: '' };
|
|
809
|
+
const since = previous.since_event_seq;
|
|
810
|
+
const query = { cursor: previous.cursor || String(since), limit: 1000, request_id: organizationIdentifier(), nonce: organizationNonce() };
|
|
811
|
+
const requestedAt = Math.trunc(Date.now() / 1000);
|
|
812
|
+
query.request_binding = await buildRequestBinding('credential.revocationList', authority, query, '', String(query.nonce), String(query.request_id), requestedAt, requestedAt + 300);
|
|
813
|
+
const proof = await this.ports.call('credential.revocationList', query);
|
|
814
|
+
check();
|
|
815
|
+
const now = Math.trunc(Date.now() / 1000);
|
|
816
|
+
if (!isRecord(proof) || proof.responder !== authority || proof.target !== actor || proof.request_id !== query.request_id || proof.nonce !== query.nonce || proof.issuer_aid !== ''
|
|
817
|
+
|| !Array.isArray(proof.events) || proof.events.length > 1000 || typeof proof.next_cursor !== 'string'
|
|
818
|
+
|| !isInt(proof.as_of) || proof.as_of <= 0 || proof.as_of > now + 300 || !isInt(proof.expires_at) || proof.expires_at <= now || proof.expires_at <= proof.as_of || proof.expires_at > proof.as_of + 300
|
|
819
|
+
|| !isRecord(proof.signature) || proof.signature.timestamp !== proof.as_of)
|
|
820
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'revocation list does not match the query');
|
|
821
|
+
let last = since;
|
|
822
|
+
for (const event of proof.events) {
|
|
823
|
+
if (!isRecord(event) || !isInt(event.event_seq) || event.event_seq <= last || typeof event.cid !== 'string' || !event.cid
|
|
824
|
+
|| typeof event.reason_code !== 'string' || !event.reason_code || !isInt(event.revoked_at) || event.revoked_at < 0 || event.revoked_at > proof.as_of
|
|
825
|
+
|| typeof event.revoked_by_aid !== 'string' || !AID_RE.test(event.revoked_by_aid))
|
|
826
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'revocation event sequence is invalid');
|
|
827
|
+
last = event.event_seq;
|
|
828
|
+
}
|
|
829
|
+
if (proof.next_cursor !== '' && (!/^[1-9][0-9]*$/.test(proof.next_cursor) || !isInt(Number(proof.next_cursor)) || Number(proof.next_cursor) <= since || Number(proof.next_cursor) < last))
|
|
830
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'revocation cursor did not advance');
|
|
831
|
+
const certificate = await this.ports.certificate(authority, String(proof.signature.cert_fingerprint));
|
|
832
|
+
check();
|
|
833
|
+
const unsigned = { ...proof };
|
|
834
|
+
delete unsigned.signature;
|
|
835
|
+
await verifyAgentSignature(proof.signature, unsigned, authority, () => certificate, Math.trunc(Date.now() / 1000), false);
|
|
836
|
+
const next = structuredClone(state);
|
|
837
|
+
const revoked = { ...previous.revoked };
|
|
838
|
+
for (const event of proof.events)
|
|
839
|
+
Object.defineProperty(revoked, String(event.cid), { value: event.event_seq, enumerable: true, configurable: true, writable: true });
|
|
840
|
+
next.authorities[authority] = { revoked, since_event_seq: proof.next_cursor === '' ? last : Number(proof.next_cursor), cursor: proof.next_cursor, as_of: proof.as_of, expires_at: proof.expires_at };
|
|
841
|
+
const commitGuard = { ...guard, current: () => this.ports.currentAid() === actor && (!guard || guard.current()) && Date.now() / 1000 < Number(proof.expires_at) };
|
|
842
|
+
if (!commitGuard.current())
|
|
843
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'revocation list expired before persistence');
|
|
844
|
+
await this.ports.saveRevocations(actor, next, commitGuard);
|
|
845
|
+
if (!commitGuard.current())
|
|
846
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'revocation synchronization was superseded');
|
|
847
|
+
state = next;
|
|
848
|
+
if (proof.next_cursor === '')
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
104
855
|
function certificatePem(value) {
|
|
105
856
|
if (typeof value === 'string' && value.includes('-----BEGIN CERTIFICATE-----'))
|
|
106
857
|
return value;
|
|
@@ -535,6 +1286,8 @@ export function validateCredential(credential, chainDepth = 0, externalProofChai
|
|
|
535
1286
|
if (!isInt(delegationDepth) || delegationDepth < 0)
|
|
536
1287
|
throw invalid('delegation_depth is invalid');
|
|
537
1288
|
const parent = credential.parent;
|
|
1289
|
+
if (credential.proof_chain !== undefined && externalProofChain !== undefined && canonicalText(credential.proof_chain) !== canonicalText(externalProofChain))
|
|
1290
|
+
throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'embedded proof_chain conflicts with external proof_chain');
|
|
538
1291
|
if (parent == null && delegationDepth !== 0)
|
|
539
1292
|
throw invalid('root credential must have delegation_depth 0');
|
|
540
1293
|
if (parent != null) {
|
|
@@ -549,12 +1302,12 @@ export function validateCredential(credential, chainDepth = 0, externalProofChai
|
|
|
549
1302
|
throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'proof_chain is too deep');
|
|
550
1303
|
for (let index = 0; index < chain.length; index++) {
|
|
551
1304
|
const item = chain[index];
|
|
552
|
-
validateCredential(item, chainDepth + 1);
|
|
553
|
-
if (canonicalText(item.proof_chain
|
|
1305
|
+
validateCredential(item, chainDepth + 1, chain.slice(index + 1));
|
|
1306
|
+
if (item.proof_chain !== undefined && canonicalText(item.proof_chain) !== canonicalText(chain.slice(index + 1)))
|
|
554
1307
|
throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'embedded proof_chain conflicts with outer chain');
|
|
555
1308
|
}
|
|
556
1309
|
}
|
|
557
|
-
else if ('proof_chain' in credential && JSON.stringify(credential.proof_chain) !== '[]') {
|
|
1310
|
+
else if ('proof_chain' in credential && JSON.stringify(credential.proof_chain) !== '[]' || parent == null && externalProofChain !== undefined && JSON.stringify(externalProofChain) !== '[]') {
|
|
558
1311
|
throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'root proof_chain must be empty');
|
|
559
1312
|
}
|
|
560
1313
|
if ('security_level' in credential && !['low', 'medium', 'high'].includes(credential.security_level))
|
|
@@ -594,6 +1347,10 @@ export async function createCredential(options, signer) {
|
|
|
594
1347
|
throw invalid('issuer does not match signing AID');
|
|
595
1348
|
const chain = value.proof_chain;
|
|
596
1349
|
const parentDepth = Array.isArray(chain) && isRecord(chain[0]) ? chain[0].delegation_depth ?? 0 : 0;
|
|
1350
|
+
if (!('exp' in value)) {
|
|
1351
|
+
const certExp = Math.floor(parseCertMetadata(signer.certPem).notAfter.getTime() / 1000);
|
|
1352
|
+
value.exp = value.parent != null && Array.isArray(chain) && isRecord(chain[0]) && isInt(chain[0].exp) ? Math.min(certExp, chain[0].exp) : certExp;
|
|
1353
|
+
}
|
|
597
1354
|
if (!('delegation_depth' in value))
|
|
598
1355
|
value.delegation_depth = value.parent !== undefined && value.parent !== null && isInt(parentDepth) ? parentDepth + 1 : 0;
|
|
599
1356
|
if (Array.isArray(value.cap))
|
|
@@ -819,7 +1576,7 @@ export async function prepareOrganizationRequest(method, params, target, signer,
|
|
|
819
1576
|
throw invalid('organization signer or signed member CAP is required');
|
|
820
1577
|
value.credential_template = await createMemberCredential(binding, options.orgSigner, { key_version: options.orgSigner.certFingerprint, ...options.credentialOptions });
|
|
821
1578
|
}
|
|
822
|
-
if (
|
|
1579
|
+
if (!['org.member.inviteAccept', 'org.member.inviteReject'].includes(method) && isRecord(value.credential_template) && !('registration_authorization' in value)) {
|
|
823
1580
|
if (!binding)
|
|
824
1581
|
throw invalid('membership binding is required for registration');
|
|
825
1582
|
const issuerSigner = options.orgSigner ?? (signer.aid === value.credential_template.iss ? signer : undefined);
|
|
@@ -852,11 +1609,18 @@ export async function prepareOrganizationRequest(method, params, target, signer,
|
|
|
852
1609
|
const credentials = isRecord(value.credentials) ? value.credentials : {};
|
|
853
1610
|
if ('credentials' in value && !isRecord(value.credentials))
|
|
854
1611
|
throw invalid('credentials must be an object');
|
|
855
|
-
if (['org.member.join', 'org.member.inviteAccept'].includes(method)) {
|
|
856
|
-
|
|
857
|
-
|
|
1612
|
+
if (['org.member.join', 'org.member.inviteAccept', 'org.member.inviteReject'].includes(method)) {
|
|
1613
|
+
const rejection = method === 'org.member.inviteReject';
|
|
1614
|
+
if (rejection && binding && !('member_aid' in value))
|
|
1615
|
+
value.member_aid = binding.member_aid;
|
|
1616
|
+
if (!binding || ('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) || !isRecord(rejection ? binding.org_signature : binding.member_signature))
|
|
1617
|
+
throw representationBindingInvalid('admission requires the original membership signature');
|
|
1618
|
+
if (rejection && (value.request_id === binding.request_id || value.nonce === binding.nonce))
|
|
1619
|
+
throw representationBindingInvalid('invitation rejection requires a distinct action identity');
|
|
858
1620
|
value.credentials = credentials;
|
|
859
1621
|
const now = Math.trunc(Date.now() / 1000);
|
|
1622
|
+
if (now >= Number(binding.exp))
|
|
1623
|
+
throw representationBindingInvalid('membership binding has expired');
|
|
860
1624
|
let maximum = Math.min(now + 300, Number(binding.exp));
|
|
861
1625
|
for (const field of ['credential_template', 'registration_authorization'])
|
|
862
1626
|
if (field in value) {
|
|
@@ -922,7 +1686,7 @@ export async function createMemberCredential(binding, signer, options) {
|
|
|
922
1686
|
const text = (value) => new TextDecoder().decode(canonicalJson(value));
|
|
923
1687
|
if (!isRecord(options) || Object.entries(fixed).some(([key, value]) => key in options && text(options[key]) !== text(value)) || options.parent !== undefined && options.parent !== null)
|
|
924
1688
|
throw invalid('member credential options conflict with binding');
|
|
925
|
-
return createCredential({
|
|
1689
|
+
return createCredential({ ...options, ...fixed }, signer);
|
|
926
1690
|
}
|
|
927
1691
|
export async function verifyCredential(credential, context) {
|
|
928
1692
|
if (!context || !context.audience || !context.subject)
|
|
@@ -933,6 +1697,22 @@ export async function verifyCredential(credential, context) {
|
|
|
933
1697
|
throw invalid('credential subject does not match holder');
|
|
934
1698
|
const now = context.now ?? Math.trunc(Date.now() / 1000);
|
|
935
1699
|
const documents = credentialChainDocuments(credential, context.proofChain);
|
|
1700
|
+
const resolvedCertificates = [];
|
|
1701
|
+
const resolveCertificate = async (fingerprint) => {
|
|
1702
|
+
const material = await context.resolveCertificate(fingerprint);
|
|
1703
|
+
if (material != null)
|
|
1704
|
+
resolvedCertificates.push(material);
|
|
1705
|
+
return material;
|
|
1706
|
+
};
|
|
1707
|
+
const resolveActorCertificate = context.resolveActorCertificate
|
|
1708
|
+
? async (fingerprint) => {
|
|
1709
|
+
const material = await context.resolveActorCertificate(fingerprint);
|
|
1710
|
+
if (material != null)
|
|
1711
|
+
resolvedCertificates.push(material);
|
|
1712
|
+
return material;
|
|
1713
|
+
}
|
|
1714
|
+
: resolveCertificate;
|
|
1715
|
+
const statusProofs = [];
|
|
936
1716
|
const status = async (cid) => {
|
|
937
1717
|
const document = documents.find((item) => item.cid === cid);
|
|
938
1718
|
let proof;
|
|
@@ -945,6 +1725,7 @@ export async function verifyCredential(credential, context) {
|
|
|
945
1725
|
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'trusted credential status is unavailable');
|
|
946
1726
|
}
|
|
947
1727
|
if (isRecord(proof)) {
|
|
1728
|
+
statusProofs.push(proof);
|
|
948
1729
|
if (typeof proof.credentialCid === 'string' && proof.credentialCid !== cid)
|
|
949
1730
|
throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'registered credential cid differs from the signed document');
|
|
950
1731
|
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_RE.test(proof.responder.slice(11)) || typeof proof.request_id !== 'string' || !proof.request_id || typeof proof.nonce !== 'string' || !proof.nonce || typeof proof.target !== 'string' || !AID_RE.test(proof.target))
|
|
@@ -965,7 +1746,7 @@ export async function verifyCredential(credential, context) {
|
|
|
965
1746
|
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'parent credential status proof is required');
|
|
966
1747
|
return typeof proof === 'string' ? proof : undefined;
|
|
967
1748
|
};
|
|
968
|
-
await verifyCredentialChain(credential,
|
|
1749
|
+
await verifyCredentialChain(credential, resolveCertificate, status, now, context.audience, context.proofChain);
|
|
969
1750
|
if (credentialChainDocuments(credential, context.proofChain).at(-1).iss !== (context.rootIssuer ?? context.audience))
|
|
970
1751
|
throw invalid('credential root issuer is not trusted for this resource');
|
|
971
1752
|
const rate = context.checkRateLimit ? (field, unit, limit) => context.checkRateLimit(String(value.cid), String(value.sub), field, unit, limit) : undefined;
|
|
@@ -985,7 +1766,7 @@ export async function verifyCredential(credential, context) {
|
|
|
985
1766
|
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)
|
|
986
1767
|
throw invalid('member credential capability does not match binding role');
|
|
987
1768
|
}
|
|
988
|
-
await verifyMembershipBinding(binding,
|
|
1769
|
+
await verifyMembershipBinding(binding, resolveCertificate, now, context.orgSigner, true);
|
|
989
1770
|
if (!context.resolveMembershipStatus || await context.resolveMembershipStatus(binding, value) !== true)
|
|
990
1771
|
throw new ProtocolError('MEMBERSHIP_STATE_INVALID', 'trusted current membership status does not permit this operation');
|
|
991
1772
|
}
|
|
@@ -993,6 +1774,7 @@ export async function verifyCredential(credential, context) {
|
|
|
993
1774
|
const params = context.params ?? {};
|
|
994
1775
|
const target = context.for ?? params.for;
|
|
995
1776
|
const actor = context.actor ?? context.subject;
|
|
1777
|
+
let verifiedRepresentation;
|
|
996
1778
|
if (typeof actor !== 'string' || !AID_RE.test(actor))
|
|
997
1779
|
throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'authenticated actor is invalid');
|
|
998
1780
|
if (target !== undefined && (typeof target !== 'string' || !target || !context.actor || Object.hasOwn(params, 'for') && params.for !== target))
|
|
@@ -1004,7 +1786,9 @@ export async function verifyCredential(credential, context) {
|
|
|
1004
1786
|
const proof = context.representationProof ?? packaged;
|
|
1005
1787
|
if (packaged !== undefined && canonicalText(packaged) !== canonicalText(proof))
|
|
1006
1788
|
throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'representation proof differs from the request');
|
|
1007
|
-
|
|
1789
|
+
const representationRequest = { from: actor, for: target, audience: context.target, resource: target, params, binding: context.requestBinding };
|
|
1790
|
+
await verifyRepresentationProof(proof, credential, representationRequest, resolveActorCertificate, now, context.proofChain);
|
|
1791
|
+
verifiedRepresentation = { proof: proof, request: representationRequest };
|
|
1008
1792
|
const subjects = [String(target), ...(context.subject !== actor && context.subject !== target ? [context.subject] : [])];
|
|
1009
1793
|
let allowed = Boolean(context.resolveOwnership);
|
|
1010
1794
|
try {
|
|
@@ -1021,6 +1805,28 @@ export async function verifyCredential(credential, context) {
|
|
|
1021
1805
|
throw new ProtocolError('ROLE_ACCESS_DENIED', 'trusted current ownership does not permit representation');
|
|
1022
1806
|
result.representation_checked = true;
|
|
1023
1807
|
}
|
|
1808
|
+
if (context.now === undefined) {
|
|
1809
|
+
const completedAt = Math.trunc(Date.now() / 1000);
|
|
1810
|
+
for (const [index, document] of documents.entries())
|
|
1811
|
+
validateCredentialAt(document, completedAt, context.audience, documents.slice(index + 1));
|
|
1812
|
+
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);
|
|
1813
|
+
validateRequestBinding(context.requestBinding, context.method, context.target, context.params ?? {}, String(value.cid), context.nonce ?? '', context.requestId ?? '', completedAt);
|
|
1814
|
+
for (const proof of statusProofs) {
|
|
1815
|
+
if (proof.asOf > completedAt + 300 || completedAt >= proof.proofExpiresAt)
|
|
1816
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'trusted credential status proof is unavailable');
|
|
1817
|
+
}
|
|
1818
|
+
for (const material of resolvedCertificates) {
|
|
1819
|
+
const metadata = parseCertMetadata(certificatePem(material));
|
|
1820
|
+
if (completedAt < metadata.notBefore.getTime() / 1000 || completedAt > metadata.notAfter.getTime() / 1000)
|
|
1821
|
+
throw new ProtocolError('CREDENTIAL_SIGNATURE_INVALID', 'certificate is not valid');
|
|
1822
|
+
}
|
|
1823
|
+
if (verifiedRepresentation) {
|
|
1824
|
+
const { proof, request } = verifiedRepresentation;
|
|
1825
|
+
if (completedAt < proof.iat || completedAt >= proof.exp)
|
|
1826
|
+
throw representationBindingInvalid('representation time window is invalid');
|
|
1827
|
+
validateRepresentationContext(proof, value, request, completedAt);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1024
1830
|
return result;
|
|
1025
1831
|
}
|
|
1026
1832
|
export function validateCredentialAt(credential, now, audience, proofChain) {
|
|
@@ -1327,7 +2133,7 @@ export async function verifyCredentialChain(credential, resolveCertificate, reso
|
|
|
1327
2133
|
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'credential status is unavailable');
|
|
1328
2134
|
}
|
|
1329
2135
|
for (const [index, document] of documents.entries())
|
|
1330
|
-
await verifyCredentialSignature(document, resolveCertificate, now, audience, index
|
|
2136
|
+
await verifyCredentialSignature(document, resolveCertificate, now, audience, documents.slice(index + 1));
|
|
1331
2137
|
}
|
|
1332
2138
|
function validateMembershipBindingPayload(binding, allowPartial = false) {
|
|
1333
2139
|
if (!isRecord(binding))
|
|
@@ -1507,7 +2313,7 @@ function orgRequestBindingParams(method, params) {
|
|
|
1507
2313
|
export function buildRequestBinding(method, target, params, credentialCid, nonce, requestId, iat, exp) {
|
|
1508
2314
|
if (![method, target, nonce, requestId].every((value) => typeof value === 'string' && value.length > 0) || typeof credentialCid !== 'string' || (!credentialCid !== (method === 'credential.revocationList')))
|
|
1509
2315
|
throw invalid('request binding identity is invalid');
|
|
1510
|
-
if (!isInt(iat) || !isInt(exp) || exp <= iat)
|
|
2316
|
+
if (!isInt(iat) || !isInt(exp) || exp <= iat || exp > iat + 300)
|
|
1511
2317
|
throw invalid('request binding time window is invalid');
|
|
1512
2318
|
return { request_id: requestId, nonce, target, method, params_hash: digest(orgRequestBindingParams(method, params)), iat, exp, credential_cid: credentialCid };
|
|
1513
2319
|
}
|
|
@@ -1519,7 +2325,7 @@ export function validateRequestBinding(binding, method, target, params, credenti
|
|
|
1519
2325
|
throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'request binding is invalid');
|
|
1520
2326
|
if (binding.request_id !== requestId || binding.nonce !== nonce || binding.target !== target || binding.method !== method || binding.credential_cid !== credentialCid || binding.params_hash !== digest(orgRequestBindingParams(method, params)))
|
|
1521
2327
|
throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'request binding does not match request');
|
|
1522
|
-
if (!isInt(binding.iat) || !isInt(binding.exp) || binding.exp <= binding.iat || !isInt(now) || now < binding.iat || now >= binding.exp)
|
|
2328
|
+
if (!isInt(binding.iat) || !isInt(binding.exp) || binding.exp <= binding.iat || binding.exp > binding.iat + 300 || !isInt(now) || now < binding.iat || now >= binding.exp)
|
|
1523
2329
|
throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'request binding is outside its validity window');
|
|
1524
2330
|
}
|
|
1525
2331
|
function representationBindingInvalid(message) {
|