@agentunion/fastaun-browser 0.5.20 → 0.5.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/_packed_docs/CHANGELOG.md +47 -0
  3. package/_packed_docs/INDEX.md +453 -440
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +164 -161
  5. 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
  6. 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
  7. 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
  8. 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
  9. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1433 -1392
  10. package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
  11. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +2 -0
  12. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -658
  13. 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
  14. 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
  15. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
  16. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
  17. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
  18. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +4 -1
  19. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +42 -39
  20. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +147 -14
  21. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +48 -47
  22. package/_packed_docs/sdk/09-group-rpc-manual.md +5 -2
  23. package/_packed_docs/sdk/09-message-rpc-manual.md +250 -160
  24. 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
  25. 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
  26. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +134 -130
  27. package/_packed_docs/sdk/INDEX.md +239 -221
  28. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -922
  29. package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +106 -43
  30. package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +180 -9
  31. package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -708
  32. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -476
  33. 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 +1 -1
  34. 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 +2 -2
  35. package/dist/agent-md-schema.d.ts +1 -0
  36. package/dist/agent-md-schema.d.ts.map +1 -1
  37. package/dist/agent-md-schema.js +5 -4
  38. package/dist/agent-md-schema.js.map +1 -1
  39. package/dist/agent-md.d.ts +25 -1
  40. package/dist/agent-md.d.ts.map +1 -1
  41. package/dist/agent-md.js +148 -71
  42. package/dist/agent-md.js.map +1 -1
  43. package/dist/aid-store.d.ts +13 -0
  44. package/dist/aid-store.d.ts.map +1 -1
  45. package/dist/aid-store.js +55 -8
  46. package/dist/aid-store.js.map +1 -1
  47. package/dist/aid.d.ts.map +1 -1
  48. package/dist/aid.js +2 -1
  49. package/dist/aid.js.map +1 -1
  50. package/dist/auth.d.ts.map +1 -1
  51. package/dist/auth.js +4 -0
  52. package/dist/auth.js.map +1 -1
  53. package/dist/bundle.js +4981 -1219
  54. package/dist/cert-utils.d.ts +1 -0
  55. package/dist/cert-utils.d.ts.map +1 -1
  56. package/dist/cert-utils.js +21 -0
  57. package/dist/cert-utils.js.map +1 -1
  58. package/dist/client/delivery.d.ts +101 -16
  59. package/dist/client/delivery.d.ts.map +1 -1
  60. package/dist/client/delivery.js +1172 -160
  61. package/dist/client/delivery.js.map +1 -1
  62. package/dist/client/rpc-pipeline.d.ts +4 -1
  63. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  64. package/dist/client/rpc-pipeline.js +122 -48
  65. package/dist/client/rpc-pipeline.js.map +1 -1
  66. package/dist/client/v2-e2ee.d.ts +22 -3
  67. package/dist/client/v2-e2ee.d.ts.map +1 -1
  68. package/dist/client/v2-e2ee.js +1258 -649
  69. package/dist/client/v2-e2ee.js.map +1 -1
  70. package/dist/client.d.ts +23 -2
  71. package/dist/client.d.ts.map +1 -1
  72. package/dist/client.js +391 -87
  73. package/dist/client.js.map +1 -1
  74. package/dist/config.d.ts +4 -0
  75. package/dist/config.d.ts.map +1 -1
  76. package/dist/config.js +1 -0
  77. package/dist/config.js.map +1 -1
  78. package/dist/events.d.ts +19 -1
  79. package/dist/events.d.ts.map +1 -1
  80. package/dist/events.js +146 -23
  81. package/dist/events.js.map +1 -1
  82. package/dist/facades.d.ts +42 -1
  83. package/dist/facades.d.ts.map +1 -1
  84. package/dist/facades.js +363 -7
  85. package/dist/facades.js.map +1 -1
  86. package/dist/index.d.ts +5 -3
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +1 -1
  89. package/dist/index.js.map +1 -1
  90. package/dist/keystore/index.d.ts +14 -1
  91. package/dist/keystore/index.d.ts.map +1 -1
  92. package/dist/keystore/indexeddb-identity-store.d.ts +4 -0
  93. package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
  94. package/dist/keystore/indexeddb-identity-store.js +30 -13
  95. package/dist/keystore/indexeddb-identity-store.js.map +1 -1
  96. package/dist/keystore/indexeddb-shared.d.ts +19 -2
  97. package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
  98. package/dist/keystore/indexeddb-shared.js +193 -6
  99. package/dist/keystore/indexeddb-shared.js.map +1 -1
  100. package/dist/keystore/indexeddb-token-store.d.ts +15 -2
  101. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
  102. package/dist/keystore/indexeddb-token-store.js +308 -27
  103. package/dist/keystore/indexeddb-token-store.js.map +1 -1
  104. package/dist/org-protocol.d.ts +45 -0
  105. package/dist/org-protocol.d.ts.map +1 -1
  106. package/dist/org-protocol.js +757 -11
  107. package/dist/org-protocol.js.map +1 -1
  108. package/dist/ownership-v21.d.ts +21 -4
  109. package/dist/ownership-v21.d.ts.map +1 -1
  110. package/dist/ownership-v21.js +49 -16
  111. package/dist/ownership-v21.js.map +1 -1
  112. package/dist/ownership-workflow.d.ts +30 -0
  113. package/dist/ownership-workflow.d.ts.map +1 -1
  114. package/dist/ownership-workflow.js +412 -54
  115. package/dist/ownership-workflow.js.map +1 -1
  116. package/dist/seq-tracker.d.ts +2 -0
  117. package/dist/seq-tracker.d.ts.map +1 -1
  118. package/dist/seq-tracker.js +3 -2
  119. package/dist/seq-tracker.js.map +1 -1
  120. package/dist/tools/cross-sdk-agent.d.ts +60 -3
  121. package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
  122. package/dist/tools/cross-sdk-agent.js +433 -37
  123. package/dist/tools/cross-sdk-agent.js.map +1 -1
  124. package/dist/transport.d.ts +3 -0
  125. package/dist/transport.d.ts.map +1 -1
  126. package/dist/transport.js +62 -30
  127. package/dist/transport.js.map +1 -1
  128. package/dist/version.d.ts +1 -1
  129. package/dist/version.js +1 -1
  130. package/package.json +1 -1
@@ -115,6 +115,743 @@ function isRecord(value) {
115
115
  function isInt(value) {
116
116
  return typeof value === 'number' && Number.isInteger(value) && Number.isSafeInteger(value);
117
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
+ }
118
855
  function certificatePem(value) {
119
856
  if (typeof value === 'string' && value.includes('-----BEGIN CERTIFICATE-----'))
120
857
  return value;
@@ -565,12 +1302,12 @@ export function validateCredential(credential, chainDepth = 0, externalProofChai
565
1302
  throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'proof_chain is too deep');
566
1303
  for (let index = 0; index < chain.length; index++) {
567
1304
  const item = chain[index];
568
- validateCredential(item, chainDepth + 1);
569
- if (canonicalText(item.proof_chain ?? []) !== canonicalText(chain.slice(index + 1)))
1305
+ validateCredential(item, chainDepth + 1, chain.slice(index + 1));
1306
+ if (item.proof_chain !== undefined && canonicalText(item.proof_chain) !== canonicalText(chain.slice(index + 1)))
570
1307
  throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'embedded proof_chain conflicts with outer chain');
571
1308
  }
572
1309
  }
573
- 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) !== '[]') {
574
1311
  throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'root proof_chain must be empty');
575
1312
  }
576
1313
  if ('security_level' in credential && !['low', 'medium', 'high'].includes(credential.security_level))
@@ -610,6 +1347,10 @@ export async function createCredential(options, signer) {
610
1347
  throw invalid('issuer does not match signing AID');
611
1348
  const chain = value.proof_chain;
612
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
+ }
613
1354
  if (!('delegation_depth' in value))
614
1355
  value.delegation_depth = value.parent !== undefined && value.parent !== null && isInt(parentDepth) ? parentDepth + 1 : 0;
615
1356
  if (Array.isArray(value.cap))
@@ -835,7 +1576,7 @@ export async function prepareOrganizationRequest(method, params, target, signer,
835
1576
  throw invalid('organization signer or signed member CAP is required');
836
1577
  value.credential_template = await createMemberCredential(binding, options.orgSigner, { key_version: options.orgSigner.certFingerprint, ...options.credentialOptions });
837
1578
  }
838
- if (method !== 'org.member.inviteAccept' && isRecord(value.credential_template) && !('registration_authorization' in value)) {
1579
+ if (!['org.member.inviteAccept', 'org.member.inviteReject'].includes(method) && isRecord(value.credential_template) && !('registration_authorization' in value)) {
839
1580
  if (!binding)
840
1581
  throw invalid('membership binding is required for registration');
841
1582
  const issuerSigner = options.orgSigner ?? (signer.aid === value.credential_template.iss ? signer : undefined);
@@ -868,9 +1609,14 @@ export async function prepareOrganizationRequest(method, params, target, signer,
868
1609
  const credentials = isRecord(value.credentials) ? value.credentials : {};
869
1610
  if ('credentials' in value && !isRecord(value.credentials))
870
1611
  throw invalid('credentials must be an object');
871
- if (['org.member.join', 'org.member.inviteAccept'].includes(method)) {
872
- if (!binding || !isRecord(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))
873
- throw representationBindingInvalid('admission target must match the signed membership binding');
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');
874
1620
  value.credentials = credentials;
875
1621
  const now = Math.trunc(Date.now() / 1000);
876
1622
  if (now >= Number(binding.exp))
@@ -940,7 +1686,7 @@ export async function createMemberCredential(binding, signer, options) {
940
1686
  const text = (value) => new TextDecoder().decode(canonicalJson(value));
941
1687
  if (!isRecord(options) || Object.entries(fixed).some(([key, value]) => key in options && text(options[key]) !== text(value)) || options.parent !== undefined && options.parent !== null)
942
1688
  throw invalid('member credential options conflict with binding');
943
- return createCredential({ exp: binding.exp, ...options, ...fixed }, signer);
1689
+ return createCredential({ ...options, ...fixed }, signer);
944
1690
  }
945
1691
  export async function verifyCredential(credential, context) {
946
1692
  if (!context || !context.audience || !context.subject)
@@ -1061,8 +1807,8 @@ export async function verifyCredential(credential, context) {
1061
1807
  }
1062
1808
  if (context.now === undefined) {
1063
1809
  const completedAt = Math.trunc(Date.now() / 1000);
1064
- for (const document of documents)
1065
- validateCredentialAt(document, completedAt, context.audience);
1810
+ for (const [index, document] of documents.entries())
1811
+ validateCredentialAt(document, completedAt, context.audience, documents.slice(index + 1));
1066
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);
1067
1813
  validateRequestBinding(context.requestBinding, context.method, context.target, context.params ?? {}, String(value.cid), context.nonce ?? '', context.requestId ?? '', completedAt);
1068
1814
  for (const proof of statusProofs) {
@@ -1387,7 +2133,7 @@ export async function verifyCredentialChain(credential, resolveCertificate, reso
1387
2133
  throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'credential status is unavailable');
1388
2134
  }
1389
2135
  for (const [index, document] of documents.entries())
1390
- await verifyCredentialSignature(document, resolveCertificate, now, audience, index === 0 ? proofChain : undefined);
2136
+ await verifyCredentialSignature(document, resolveCertificate, now, audience, documents.slice(index + 1));
1391
2137
  }
1392
2138
  function validateMembershipBindingPayload(binding, allowPartial = false) {
1393
2139
  if (!isRecord(binding))