@agentunion/fastaun 0.5.17 → 0.5.19

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 (95) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/_packed_docs/CHANGELOG.md +83 -0
  3. package/_packed_docs/INDEX.md +163 -16
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +52 -20
  5. package/_packed_docs/agent.md/SCHEMA.md +266 -207
  6. package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +64 -31
  7. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +8 -6
  8. package/_packed_docs/protocol/01-/350/272/253/344/273/275/344/270/216/345/207/255/350/257/201/345/215/217/350/256/256-auth.md +39 -39
  9. package/_packed_docs/protocol/07-/351/224/231/350/257/257/347/240/201/344/270/216/347/212/266/346/200/201/346/234/272.md +1 -1
  10. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +28 -9
  11. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +20 -2
  12. package/_packed_docs/protocol/agent.md/SCHEMA.md +4 -0
  13. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +5 -2
  14. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +246 -241
  15. package/_packed_docs/sdk/09-group-rpc-manual.md +46 -14
  16. package/_packed_docs/sdk/09-message-rpc-manual.md +40 -2
  17. package/_packed_docs/sdk/09-storage-rpc-manual.md +69 -38
  18. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +25 -5
  19. package/_packed_docs/sdk/INDEX.md +78 -18
  20. package/dist/agent-md-schema.d.ts +2 -0
  21. package/dist/agent-md-schema.js +46 -9
  22. package/dist/agent-md-schema.js.map +1 -1
  23. package/dist/agent-md.d.ts +46 -1
  24. package/dist/agent-md.js +492 -30
  25. package/dist/agent-md.js.map +1 -1
  26. package/dist/aid-store.d.ts +9 -1
  27. package/dist/aid-store.js +139 -18
  28. package/dist/aid-store.js.map +1 -1
  29. package/dist/aid.d.ts +17 -1
  30. package/dist/aid.js +67 -7
  31. package/dist/aid.js.map +1 -1
  32. package/dist/auth.d.ts +6 -1
  33. package/dist/auth.js +428 -15
  34. package/dist/auth.js.map +1 -1
  35. package/dist/cert-utils.d.ts +1 -1
  36. package/dist/cert-utils.js +19 -7
  37. package/dist/cert-utils.js.map +1 -1
  38. package/dist/client/delivery.d.ts +4 -1
  39. package/dist/client/delivery.js +264 -68
  40. package/dist/client/delivery.js.map +1 -1
  41. package/dist/client/rpc-pipeline.js +29 -14
  42. package/dist/client/rpc-pipeline.js.map +1 -1
  43. package/dist/client/v2-e2ee.d.ts +8 -1
  44. package/dist/client/v2-e2ee.js +893 -529
  45. package/dist/client/v2-e2ee.js.map +1 -1
  46. package/dist/client.d.ts +87 -1
  47. package/dist/client.js +816 -35
  48. package/dist/client.js.map +1 -1
  49. package/dist/errors.js +4 -3
  50. package/dist/errors.js.map +1 -1
  51. package/dist/events.d.ts +5 -1
  52. package/dist/events.js +47 -6
  53. package/dist/events.js.map +1 -1
  54. package/dist/facades.d.ts +85 -3
  55. package/dist/facades.js +104 -4
  56. package/dist/facades.js.map +1 -1
  57. package/dist/index.d.ts +9 -4
  58. package/dist/index.js +3 -1
  59. package/dist/index.js.map +1 -1
  60. package/dist/keystore/aid-db.d.ts +5 -0
  61. package/dist/keystore/aid-db.js +48 -0
  62. package/dist/keystore/aid-db.js.map +1 -1
  63. package/dist/keystore/index.d.ts +8 -0
  64. package/dist/keystore/local-identity-store.js +10 -3
  65. package/dist/keystore/local-identity-store.js.map +1 -1
  66. package/dist/keystore/local-token-store.d.ts +6 -1
  67. package/dist/keystore/local-token-store.js +24 -1
  68. package/dist/keystore/local-token-store.js.map +1 -1
  69. package/dist/org-protocol.d.ts +98 -0
  70. package/dist/org-protocol.js +1689 -0
  71. package/dist/org-protocol.js.map +1 -0
  72. package/dist/ownership-proof.d.ts +117 -0
  73. package/dist/ownership-proof.js +436 -0
  74. package/dist/ownership-proof.js.map +1 -0
  75. package/dist/ownership-v21.d.ts +178 -0
  76. package/dist/ownership-v21.js +635 -0
  77. package/dist/ownership-v21.js.map +1 -0
  78. package/dist/ownership-workflow.d.ts +82 -0
  79. package/dist/ownership-workflow.js +872 -0
  80. package/dist/ownership-workflow.js.map +1 -0
  81. package/dist/seq-tracker.d.ts +1 -0
  82. package/dist/seq-tracker.js +10 -1
  83. package/dist/seq-tracker.js.map +1 -1
  84. package/dist/tools/cross-sdk-agent.d.ts +89 -1
  85. package/dist/tools/cross-sdk-agent.js +338 -7
  86. package/dist/tools/cross-sdk-agent.js.map +1 -1
  87. package/dist/transport.d.ts +7 -0
  88. package/dist/transport.js +108 -4
  89. package/dist/transport.js.map +1 -1
  90. package/dist/v2/e2ee/decrypt.d.ts +1 -0
  91. package/dist/v2/e2ee/decrypt.js +9 -1
  92. package/dist/v2/e2ee/decrypt.js.map +1 -1
  93. package/dist/version.d.ts +1 -1
  94. package/dist/version.js +1 -1
  95. package/package.json +8 -8
@@ -0,0 +1,1689 @@
1
+ import { canonicalJson as baseCanonicalJson } from './v2/crypto/canonical.js';
2
+ import { createHash, randomBytes, randomUUID, X509Certificate } from 'node:crypto';
3
+ import { certCommonName, certFingerprint, publicKeyFingerprint, verifySignatureWithCert } from './cert-utils.js';
4
+ import { validateAgentMdCertificate } from './agent-md-schema.js';
5
+ const AID_RE = /^[^\s/]+\.[^\s/]+$/;
6
+ const DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
7
+ const BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/;
8
+ const ERROR_RETRYABLE = { CREDENTIAL_STATUS_UNKNOWN: true };
9
+ const CREDENTIAL_FIELDS = new Set([
10
+ 'ver', 'cid', 'iat', 'iss', 'sub', 'aud', 'nbf', 'exp', 'cap', 'delegate',
11
+ 'delegation_depth', 'parent', 'proof_chain', 'key_version', 'security_level',
12
+ 'revocation_endpoint', 'credential_service', 'ext', 'signature',
13
+ ]);
14
+ const CAP_FIELDS = new Set(['ns', 'action', 'resource', 'constraints']);
15
+ const SIGNATURE_FIELDS = new Set(['cert_fingerprint', 'public_key_fingerprint', 'timestamp', 'signature']);
16
+ const BINDING_FIELDS = new Set([
17
+ 'ver', 'membership_id', 'binding_version', 'org_id', 'member_aid', 'member_type',
18
+ 'category', 'role', 'ownership', 'visibility', 'terms_hash', 'request_id', 'nonce',
19
+ 'iat', 'exp', 'member_signature', 'org_signature',
20
+ ]);
21
+ const RECEIPT_FIELDS = new Set([
22
+ 'ver', 'receipt_id', 'request_id', 'credential_cid', 'credential_hash',
23
+ 'registrar_aid', 'service_aid', 'status', 'registered_at', 'exp', 'nonce', 'signature', 'registration_authorization',
24
+ ]);
25
+ const REPRESENTATION_FIELDS = new Set([
26
+ 'issuer', 'represented', 'from', 'for', 'audience', 'credential_cid', 'method',
27
+ 'resource', 'params_hash', 'iat', 'exp', 'nonce', 'signature',
28
+ ]);
29
+ const REPRESENTATION_SIGNATURE_FIELDS = new Set(['cert_fingerprint', 'public_key_fingerprint', 'timestamp', 'signature']);
30
+ const FINGERPRINT_RE = /^sha256:[0-9a-f]{64}$/i;
31
+ export class ProtocolError extends Error {
32
+ code;
33
+ retryable;
34
+ constructor(code, message = code) {
35
+ super(`${code}: ${message}`);
36
+ this.name = 'ProtocolError';
37
+ this.code = code;
38
+ this.retryable = protocolErrorRetryable(code);
39
+ }
40
+ }
41
+ function certificatePem(material) {
42
+ if (!material)
43
+ throw new Error('certificate is missing');
44
+ let value = material instanceof Uint8Array ? Buffer.from(material) : material;
45
+ if (typeof value === 'string' && !value.includes('-----BEGIN CERTIFICATE-----')) {
46
+ if (!value || value.length % 4 !== 0 || !BASE64_RE.test(value) || Buffer.from(value, 'base64').toString('base64') !== value)
47
+ throw new Error('certificate Base64 DER is invalid');
48
+ value = Buffer.from(value, 'base64');
49
+ }
50
+ return new X509Certificate(value).toString();
51
+ }
52
+ export async function verifyServiceEvent(eventName, payload, context) {
53
+ if (!isRecord(payload) || ['_route', '_federation', '_trace', 'member_aids', 'inline_message'].some((key) => Object.hasOwn(payload, key)) || payload.type !== eventName || payload.event !== eventName || typeof payload.event_id !== 'string' || !payload.event_id || typeof payload.responder !== 'string' || !AID_RE.test(payload.responder))
54
+ throw invalid('service event identity is invalid');
55
+ normalizeNfc(payload);
56
+ const now = context.now ?? Math.trunc(Date.now() / 1000);
57
+ 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)
58
+ throw invalid('service event time window is invalid');
59
+ if (!Array.isArray(payload.target_aids) || !payload.target_aids.length || payload.target_aids.some((target) => typeof target !== 'string' || !AID_RE.test(target)) || new Set(payload.target_aids).size !== payload.target_aids.length || !payload.target_aids.includes(context.targetAid))
60
+ throw invalid('service event target is invalid');
61
+ let known = eventName.startsWith('org.');
62
+ if (known) {
63
+ if (typeof payload.org_id !== 'string' || !AID_RE.test(payload.org_id) || payload.responder !== 'org.' + payload.org_id.slice(payload.org_id.indexOf('.') + 1))
64
+ throw invalid('organization event authority is invalid');
65
+ }
66
+ else if (eventName.startsWith('credential.')) {
67
+ const cid = payload.credential_cid ?? payload.credentialCid;
68
+ if (typeof cid !== 'string' || !cid || payload.credential_cid !== undefined && payload.credentialCid !== undefined && payload.credential_cid !== payload.credentialCid || !payload.responder.startsWith('credential.') || typeof payload.target !== 'string' || !payload.target_aids.includes(payload.target))
69
+ throw invalid('credential event authority is invalid');
70
+ const expected = await context.resolveCredentialService?.(cid);
71
+ known = expected != null;
72
+ if (known && expected !== payload.responder)
73
+ throw invalid('credential event provider does not match trusted authority');
74
+ }
75
+ else
76
+ throw invalid('service event namespace is invalid');
77
+ signature(payload.signature);
78
+ const signed = payload.signature;
79
+ if (signed.timestamp !== payload.as_of)
80
+ throw invalid('service event signature time is invalid');
81
+ try {
82
+ const cert = certificatePem(await context.resolveEventCertificate(payload.responder, String(signed.cert_fingerprint)));
83
+ const value = { ...payload };
84
+ delete value.signature;
85
+ verifyAgentSignature(signed, value, payload.responder, () => cert, context.now ?? Math.trunc(Date.now() / 1000), false);
86
+ }
87
+ catch {
88
+ throw new ProtocolError('CREDENTIAL_SIGNATURE_INVALID', 'service event signature is invalid');
89
+ }
90
+ if ((context.now ?? Math.trunc(Date.now() / 1000)) >= payload.expires_at)
91
+ throw invalid('service event time window is invalid');
92
+ return { signature_valid: true, credential_known: known };
93
+ }
94
+ export function protocolErrorRetryable(code) {
95
+ return ERROR_RETRYABLE[String(code)] === true;
96
+ }
97
+ function invalid(message) {
98
+ return new ProtocolError('CREDENTIAL_INVALID', message);
99
+ }
100
+ function isRecord(value) {
101
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
102
+ }
103
+ function isInt(value) {
104
+ return typeof value === 'number' && Number.isInteger(value) && Number.isSafeInteger(value);
105
+ }
106
+ function required(obj, fields) {
107
+ const missing = [...fields].filter((field) => !(field in obj));
108
+ if (missing.length > 0)
109
+ throw invalid(`missing fields: ${missing.sort().join(',')}`);
110
+ }
111
+ function onlyKnown(obj, allowed) {
112
+ const unknown = Object.keys(obj).filter((field) => !allowed.has(field));
113
+ if (unknown.length > 0)
114
+ throw invalid(`unknown fields: ${unknown.sort().join(',')}`);
115
+ }
116
+ function aid(value, field) {
117
+ if (typeof value !== 'string' || value.length === 0 || !AID_RE.test(value)) {
118
+ throw invalid(`${field} must be an AID`);
119
+ }
120
+ }
121
+ function signature(value, field = 'signature') {
122
+ if (!isRecord(value) || !['cert_fingerprint', 'timestamp', 'signature'].every((key) => key in value) || Object.keys(value).some((key) => !SIGNATURE_FIELDS.has(key))) {
123
+ throw invalid(`${field} is invalid`);
124
+ }
125
+ if (typeof value.cert_fingerprint !== 'string' || !FINGERPRINT_RE.test(value.cert_fingerprint)) {
126
+ throw invalid(`${field} is invalid`);
127
+ }
128
+ if (value.public_key_fingerprint !== undefined && (typeof value.public_key_fingerprint !== 'string' || !FINGERPRINT_RE.test(value.public_key_fingerprint)))
129
+ throw invalid(`${field} is invalid`);
130
+ if (!isInt(value.timestamp) || value.timestamp < 0)
131
+ throw invalid(`${field} is invalid`);
132
+ if (typeof value.signature !== 'string' || !strictDerBase64(value.signature))
133
+ throw invalid(`${field} is invalid`);
134
+ }
135
+ function strictDerBase64(value) {
136
+ if (!value || value.length % 4 !== 0 || !BASE64_RE.test(value))
137
+ return false;
138
+ let decoded;
139
+ try {
140
+ decoded = Buffer.from(value, 'base64');
141
+ }
142
+ catch {
143
+ return false;
144
+ }
145
+ if (decoded.toString('base64') !== value || decoded.length < 8 || decoded[0] !== 0x30)
146
+ return false;
147
+ const outer = derLength(decoded, 1);
148
+ if (!outer || outer.length !== decoded.length - outer.pos)
149
+ return false;
150
+ let pos = outer.pos;
151
+ for (let i = 0; i < 2; i++) {
152
+ if (pos >= decoded.length || decoded[pos] !== 0x02)
153
+ return false;
154
+ const item = derLength(decoded, pos + 1);
155
+ if (!item || item.length < 1 || item.length > 33 || item.pos + item.length > decoded.length)
156
+ return false;
157
+ const first = decoded[item.pos];
158
+ if (first === 0 && (item.length === 1 || decoded[item.pos + 1] < 0x80))
159
+ return false;
160
+ if ((first & 0x80) !== 0)
161
+ return false;
162
+ pos = item.pos + item.length;
163
+ }
164
+ return pos === decoded.length;
165
+ }
166
+ function derLength(data, pos) {
167
+ if (pos >= data.length)
168
+ return null;
169
+ const first = data[pos];
170
+ pos += 1;
171
+ if (first < 0x80)
172
+ return { length: first, pos };
173
+ const count = first & 0x7f;
174
+ if (count === 0 || count > 2 || pos + count > data.length || data[pos] === 0)
175
+ return null;
176
+ let length = 0;
177
+ for (let i = 0; i < count; i++)
178
+ length = length * 256 + data[pos + i];
179
+ if (length < 0x80)
180
+ return null;
181
+ return { length, pos: pos + count };
182
+ }
183
+ function normalizeNfc(value) {
184
+ if (typeof value === 'string') {
185
+ for (const char of value)
186
+ if (char.codePointAt(0) >= 0xd800 && char.codePointAt(0) <= 0xdfff)
187
+ throw invalid('isolated surrogate is invalid');
188
+ return value.normalize('NFC');
189
+ }
190
+ if (Array.isArray(value))
191
+ return value.map(normalizeNfc);
192
+ if (value !== null && typeof value === 'object') {
193
+ const out = Object.create(null);
194
+ for (const [key, item] of Object.entries(value)) {
195
+ const normalized = normalizeNfc(key);
196
+ if (Object.hasOwn(out, normalized))
197
+ throw invalid('duplicate NFC object key');
198
+ out[normalized] = normalizeNfc(item);
199
+ }
200
+ return out;
201
+ }
202
+ if (value === null || typeof value === 'boolean')
203
+ return value;
204
+ if (typeof value === 'number' && Number.isFinite(value) && Math.abs(value) <= Number.MAX_SAFE_INTEGER)
205
+ return value;
206
+ throw invalid('unsafe or unsupported JSON value');
207
+ }
208
+ export function canonicalJson(value) {
209
+ return baseCanonicalJson(normalizeNfc(value));
210
+ }
211
+ export function validateOrgRequestJSON(value) {
212
+ if (typeof value === 'string') {
213
+ for (const char of value)
214
+ if (char.codePointAt(0) >= 0xd800 && char.codePointAt(0) <= 0xdfff)
215
+ throw invalid('isolated surrogate is invalid');
216
+ }
217
+ else if (Array.isArray(value)) {
218
+ for (const item of value)
219
+ validateOrgRequestJSON(item);
220
+ }
221
+ else if (value !== null && typeof value === 'object') {
222
+ for (const [key, item] of Object.entries(value)) {
223
+ validateOrgRequestJSON(key);
224
+ validateOrgRequestJSON(item);
225
+ }
226
+ }
227
+ }
228
+ const CONSTRAINT_OPERATORS = { max_value: 'lte', min_value: 'gte', allowed_values: 'in', denied_values: 'not_in', range: 'between', regex: 'match', path_prefix: 'prefix', time_window: 'between', rate_limit: 'lte', ip_whitelist: 'in' };
229
+ function canonicalPath(raw) {
230
+ if (typeof raw !== 'string')
231
+ return undefined;
232
+ let value;
233
+ try {
234
+ value = decodeURIComponent(raw);
235
+ }
236
+ catch {
237
+ return undefined;
238
+ }
239
+ if (!value.startsWith('/') || /[\\\x00%]/u.test(value) || value.includes('//') || value.split('/').some((part) => part === '.' || part === '..'))
240
+ return undefined;
241
+ return value.length > 1 ? value.replace(/\/$/u, '') : value;
242
+ }
243
+ function constraintIP(raw) {
244
+ if (typeof raw !== 'string' || raw.includes('%'))
245
+ return undefined;
246
+ const parts = raw.split('/');
247
+ if (parts.length > 2)
248
+ return undefined;
249
+ let value = parts[0];
250
+ const ipv4 = (text) => {
251
+ const octets = text.split('.');
252
+ if (octets.length !== 4 || octets.some((part) => !/^(0|[1-9][0-9]{0,2})$/u.test(part) || Number(part) > 255))
253
+ return undefined;
254
+ return octets.map(Number);
255
+ };
256
+ let bits;
257
+ let address = 0n;
258
+ if (!value.includes(':')) {
259
+ const octets = ipv4(value);
260
+ if (!octets)
261
+ return undefined;
262
+ bits = 32;
263
+ for (const octet of octets)
264
+ address = address * 256n + BigInt(octet);
265
+ }
266
+ else {
267
+ bits = 128;
268
+ if (value.includes('.')) {
269
+ const end = value.lastIndexOf(':');
270
+ const octets = ipv4(value.slice(end + 1));
271
+ if (!octets)
272
+ return undefined;
273
+ value = value.slice(0, end + 1) + ((octets[0] * 256 + octets[1]).toString(16)) + ':' + ((octets[2] * 256 + octets[3]).toString(16));
274
+ }
275
+ const halves = value.split('::');
276
+ if (halves.length > 2)
277
+ return undefined;
278
+ const left = halves[0] ? halves[0].split(':') : [];
279
+ const right = halves.length === 2 && halves[1] ? halves[1].split(':') : [];
280
+ if ([...left, ...right].some((part) => !/^[0-9a-fA-F]{1,4}$/u.test(part)))
281
+ return undefined;
282
+ if (halves.length === 1 && left.length !== 8 || halves.length === 2 && left.length + right.length >= 8)
283
+ return undefined;
284
+ const groups = halves.length === 1 ? left : [...left, ...Array(8 - left.length - right.length).fill('0'), ...right];
285
+ for (const group of groups)
286
+ address = address * 65536n + BigInt(Number.parseInt(group, 16));
287
+ }
288
+ const prefix = parts.length === 1 ? bits : /^[0-9]+$/u.test(parts[1]) ? Number(parts[1]) : -1;
289
+ if (!Number.isInteger(prefix) || prefix < 0 || prefix > bits)
290
+ return undefined;
291
+ const shift = BigInt(bits - prefix);
292
+ return { bits, prefix, address: (address >> shift) << shift };
293
+ }
294
+ function validConstraintRegex(value) {
295
+ if (typeof value !== 'string' || new TextEncoder().encode(value).length > 256 || value.length < 2 || !value.startsWith('^') || !value.endsWith('$'))
296
+ return false;
297
+ let inside = false;
298
+ let repeats = 0;
299
+ for (let index = 1; index < value.length - 1; index++) {
300
+ const char = value[index];
301
+ if (char === '\\') {
302
+ index++;
303
+ if (index >= value.length - 1 || !'dDwW\\[]{}*+?^$-.nrtf'.includes(value[index]) || value[index] === '-' && !inside)
304
+ return false;
305
+ continue;
306
+ }
307
+ if (char === '[') {
308
+ if (inside)
309
+ return false;
310
+ inside = true;
311
+ continue;
312
+ }
313
+ if (char === ']') {
314
+ if (!inside)
315
+ return false;
316
+ inside = false;
317
+ continue;
318
+ }
319
+ if (inside) {
320
+ if (['&&', '--', '~~'].includes(value.slice(index, index + 2)))
321
+ return false;
322
+ continue;
323
+ }
324
+ if ('()|.^$'.includes(char))
325
+ return false;
326
+ if ('*+?'.includes(char))
327
+ repeats++;
328
+ if (char === '{') {
329
+ const end = value.indexOf('}', index);
330
+ if (end < 0)
331
+ return false;
332
+ const bounds = value.slice(index + 1, end).split(',');
333
+ if (bounds.length > 2 || bounds.some((bound) => !/^[0-9]+$/u.test(bound) || Number(bound) > 128) || bounds.length === 2 && Number(bounds[0]) > Number(bounds[1]))
334
+ return false;
335
+ index = end;
336
+ repeats++;
337
+ }
338
+ if (repeats > 1)
339
+ return false;
340
+ }
341
+ try {
342
+ new RegExp(value, 'u');
343
+ }
344
+ catch {
345
+ return false;
346
+ }
347
+ return !inside;
348
+ }
349
+ function validateCapability(capability) {
350
+ const constraints = capability.constraints;
351
+ if (!Array.isArray(constraints) || constraints.length > 64)
352
+ throw invalid('capability constraints are invalid');
353
+ const groups = new Map();
354
+ const number = (value) => typeof value === 'number' && Number.isFinite(value);
355
+ for (const item of constraints) {
356
+ if (!isRecord(item) || !['type', 'field', 'value'].every((field) => field in item) || Object.keys(item).some((field) => !['type', 'field', 'value', 'unit', 'operator'].includes(field)))
357
+ throw invalid('constraint fields are invalid');
358
+ const kind = item.type;
359
+ const field = item.field;
360
+ const unit = item.unit ?? '';
361
+ if (typeof kind !== 'string' || !Object.hasOwn(CONSTRAINT_OPERATORS, kind) || typeof field !== 'string' || field.length > 128 || !/^[A-Za-z_][A-Za-z0-9_.]*$/u.test(field) || field.includes('..') || field.endsWith('.'))
362
+ throw invalid('constraint type or field is invalid');
363
+ if ('operator' in item && item.operator !== CONSTRAINT_OPERATORS[kind])
364
+ throw invalid('constraint operator is invalid');
365
+ if ('unit' in item && (typeof unit !== 'string' || unit.length === 0 || new TextEncoder().encode(unit).length > 32))
366
+ throw invalid('constraint unit is invalid');
367
+ const key = field + '\x00' + constraintUnit(item);
368
+ let group = groups.get(key);
369
+ if (!group) {
370
+ group = { lower: -Infinity, upper: Infinity, denied: new Set(), timeFrom: '00:00', timeTo: '24:00', field };
371
+ groups.set(key, group);
372
+ }
373
+ const value = item.value;
374
+ switch (kind) {
375
+ case 'max_value':
376
+ case 'min_value':
377
+ case 'rate_limit':
378
+ if (!number(value))
379
+ throw invalid('constraint value must be numeric');
380
+ if (kind === 'rate_limit') {
381
+ if (!Number.isSafeInteger(value) || value <= 0 || !['', 'second', 'minute', 'hour', 'day'].includes(unit))
382
+ throw invalid('rate limit is invalid');
383
+ }
384
+ else if (kind === 'max_value')
385
+ group.upper = Math.min(group.upper, value);
386
+ else
387
+ group.lower = Math.max(group.lower, value);
388
+ break;
389
+ case 'range':
390
+ if (!Array.isArray(value) || value.length !== 2 || !number(value[0]) || !number(value[1]) || value[0] > value[1])
391
+ throw invalid('range is invalid');
392
+ group.lower = Math.max(group.lower, value[0]);
393
+ group.upper = Math.min(group.upper, value[1]);
394
+ break;
395
+ case 'allowed_values':
396
+ case 'denied_values': {
397
+ if (!Array.isArray(value) || value.length < 1 || value.length > 64 || unit || value.some((entry) => entry !== null && typeof entry === 'object'))
398
+ throw invalid('enumeration constraint is invalid');
399
+ const values = new Set(value.map(canonicalText));
400
+ if (values.size !== value.length)
401
+ throw invalid('enumeration contains duplicates');
402
+ if (kind === 'denied_values')
403
+ for (const entry of values)
404
+ group.denied.add(entry);
405
+ else if (!group.allowed)
406
+ group.allowed = values;
407
+ else
408
+ for (const entry of group.allowed)
409
+ if (!values.has(entry))
410
+ group.allowed.delete(entry);
411
+ break;
412
+ }
413
+ case 'regex':
414
+ if (unit || !validConstraintRegex(value))
415
+ throw invalid('regex is outside the supported subset');
416
+ break;
417
+ case 'path_prefix': {
418
+ const path = canonicalPath(value);
419
+ if (unit || typeof value !== 'string' || new TextEncoder().encode(value).length > 1024 || path === undefined || path !== (value.replace(/\/$/u, '') || '/'))
420
+ throw invalid('path prefix is invalid');
421
+ if (group.path === undefined || group.path === '/' || path === group.path || path.startsWith(group.path + '/'))
422
+ group.path = path;
423
+ else if (path !== '/' && !group.path.startsWith(path + '/'))
424
+ throw invalid('capability path prefixes conflict');
425
+ break;
426
+ }
427
+ case 'time_window':
428
+ if (!Array.isArray(value) || value.length !== 2 || typeof value[0] !== 'string' || typeof value[1] !== 'string' || !['', 'UTC'].includes(unit) || !/^([01][0-9]|2[0-3]):[0-5][0-9]$/u.test(value[0]) || !(/^([01][0-9]|2[0-3]):[0-5][0-9]$/u.test(value[1]) || value[1] === '24:00') || value[0] >= value[1])
429
+ throw invalid('time window is invalid');
430
+ if (value[0] > group.timeFrom)
431
+ group.timeFrom = value[0];
432
+ if (value[1] < group.timeTo)
433
+ group.timeTo = value[1];
434
+ break;
435
+ case 'ip_whitelist': {
436
+ if (!Array.isArray(value) || value.length < 1 || value.length > 64 || unit || value.some((entry) => !constraintIP(entry)))
437
+ throw invalid('IP whitelist is invalid');
438
+ const networks = value.map((entry) => constraintIP(entry));
439
+ if (!group.ips)
440
+ group.ips = networks;
441
+ else {
442
+ const intersection = new Map();
443
+ for (const left of group.ips)
444
+ for (const right of networks) {
445
+ if (left.bits !== right.bits)
446
+ continue;
447
+ const prefix = Math.min(left.prefix, right.prefix);
448
+ const shift = BigInt(left.bits - prefix);
449
+ if (left.address >> shift === right.address >> shift) {
450
+ const narrower = left.prefix >= right.prefix ? left : right;
451
+ intersection.set(`${narrower.bits}/${narrower.prefix}/${narrower.address}`, narrower);
452
+ }
453
+ }
454
+ group.ips = [...intersection.values()];
455
+ }
456
+ break;
457
+ }
458
+ }
459
+ }
460
+ for (const group of groups.values()) {
461
+ if (group.lower > group.upper || group.timeFrom >= group.timeTo || group.ips?.length === 0)
462
+ throw invalid('capability constraints conflict');
463
+ if (group.allowed) {
464
+ for (const entry of group.allowed)
465
+ if (group.denied.has(entry) || constraints.some((item) => isRecord(item) && item.field === group.field && item.type !== 'rate_limit' && !constraintSatisfied(item, parseJsonStrict(entry))))
466
+ group.allowed.delete(entry);
467
+ if (!group.allowed.size)
468
+ throw invalid('capability constraints conflict');
469
+ }
470
+ }
471
+ const ns = capability.ns;
472
+ const action = capability.action;
473
+ const resource = capability.resource;
474
+ if (/[*?]/u.test(ns) || /[*?]/u.test(action) && action !== '*' || /[*?]/u.test(resource) && resource !== '*' || action === '*' && resource === '*')
475
+ throw invalid('wildcard scope is invalid');
476
+ if (action === '*') {
477
+ const bounded = constraints.some((item) => isRecord(item) && item.type === 'allowed_values' && item.field === 'operation' && item.value.every((entry) => ['get', 'list', 'read', 'usage'].includes(entry)));
478
+ if (!bounded || ns === 'org')
479
+ throw invalid('wildcard action requires an explicit read-only set');
480
+ }
481
+ if (resource === '*') {
482
+ const bounded = constraints.some((item) => isRecord(item) && item.field === 'resource' && ['path_prefix', 'allowed_values'].includes(item.type));
483
+ if (!bounded || !['get', 'list', 'read', 'usage'].includes(action.split('.').at(-1)))
484
+ throw invalid('wildcard resource requires a bounded read-only scope');
485
+ }
486
+ }
487
+ export function validateCredential(credential, chainDepth = 0, externalProofChain) {
488
+ if (!isRecord(credential))
489
+ throw invalid('credential must be an object');
490
+ normalizeNfc(credential);
491
+ onlyKnown(credential, CREDENTIAL_FIELDS);
492
+ required(credential, new Set(['ver', 'cid', 'iat', 'iss', 'sub', 'aud', 'nbf', 'exp', 'cap', 'key_version', 'signature']));
493
+ if (credential.ver !== 'aun-cap/1' || typeof credential.cid !== 'string' || credential.cid.length === 0) {
494
+ throw invalid('credential version or cid is invalid');
495
+ }
496
+ aid(credential.iss, 'iss');
497
+ aid(credential.sub, 'sub');
498
+ if (!isInt(credential.iat) || !isInt(credential.nbf) || !isInt(credential.exp))
499
+ throw invalid('credential timestamps must be integers');
500
+ if (credential.iat > credential.nbf || credential.exp <= credential.nbf)
501
+ throw invalid('credential time window is invalid');
502
+ if (!Array.isArray(credential.aud) || credential.aud.length === 0)
503
+ throw invalid('aud must be a non-empty array');
504
+ for (const audience of credential.aud)
505
+ aid(audience, 'aud');
506
+ if (!Array.isArray(credential.cap) || credential.cap.length === 0)
507
+ throw invalid('cap must be a non-empty array');
508
+ for (const capability of credential.cap) {
509
+ if (!isRecord(capability) || Object.keys(capability).length !== CAP_FIELDS.size || Object.keys(capability).some((key) => !CAP_FIELDS.has(key)))
510
+ throw invalid('cap item is invalid');
511
+ for (const field of ['ns', 'action', 'resource'])
512
+ if (typeof capability[field] !== 'string' || capability[field].length === 0)
513
+ throw invalid('cap item is invalid');
514
+ if (!Array.isArray(capability.constraints))
515
+ throw invalid('cap constraints must be an array');
516
+ validateCapability(capability);
517
+ }
518
+ if ('delegate' in credential && typeof credential.delegate !== 'boolean')
519
+ throw invalid('delegate must be boolean');
520
+ const delegationDepth = Object.hasOwn(credential, 'delegation_depth') ? credential.delegation_depth : 0;
521
+ if (!isInt(delegationDepth) || delegationDepth < 0)
522
+ throw invalid('delegation_depth is invalid');
523
+ const parent = credential.parent;
524
+ if (parent == null && delegationDepth !== 0)
525
+ throw invalid('root credential must have delegation_depth 0');
526
+ if (parent != null) {
527
+ if (!Object.hasOwn(credential, 'delegation_depth'))
528
+ throw invalid('child delegation_depth is required');
529
+ if (typeof parent !== 'string' || parent.length === 0)
530
+ throw invalid('parent is invalid');
531
+ const chain = credential.proof_chain ?? externalProofChain;
532
+ if (!Array.isArray(chain) || chain.length === 0)
533
+ throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'proof_chain is required');
534
+ if (chain.length > 8 || chainDepth >= 8)
535
+ throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'proof_chain is too deep');
536
+ for (let index = 0; index < chain.length; index++) {
537
+ const item = chain[index];
538
+ validateCredential(item, chainDepth + 1);
539
+ if (canonicalText(item.proof_chain ?? []) !== canonicalText(chain.slice(index + 1)))
540
+ throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'embedded proof_chain conflicts with outer chain');
541
+ }
542
+ }
543
+ else if ('proof_chain' in credential && JSON.stringify(credential.proof_chain) !== '[]') {
544
+ throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'root proof_chain must be empty');
545
+ }
546
+ if (!['low', 'medium', 'high'].includes(Object.hasOwn(credential, 'security_level') ? credential.security_level : 'medium'))
547
+ throw invalid('security_level is invalid');
548
+ if (typeof credential.key_version !== 'string' || credential.key_version.length === 0)
549
+ throw invalid('key_version is invalid');
550
+ if ('ext' in credential && (!isRecord(credential.ext) || Object.keys(credential.ext).some((key) => typeof key !== 'string')))
551
+ throw invalid('ext is invalid');
552
+ const ext = (credential.ext ?? {});
553
+ if (ext.credential_type === 'archive') {
554
+ aid(ext.org_id, 'archive org_id');
555
+ if ('bound_to_membership' in ext && ext.bound_to_membership !== false || !Array.isArray(ext.audit_fields) || !ext.audit_fields.length || ext.audit_fields.some((field) => typeof field !== 'string' || !field))
556
+ throw invalid('archive credential requires independent, explicit audit fields');
557
+ const window = ext.audit_window;
558
+ const start = isRecord(window) ? auditScopeTime(window.from) : undefined;
559
+ const end = isRecord(window) ? auditScopeTime(window.to) : undefined;
560
+ if (start === undefined || end === undefined || end < start)
561
+ throw invalid('archive audit window must have explicit bounds');
562
+ if (!credential.aud.includes(ext.org_id) || credential.cap.some((cap) => !isRecord(cap) || cap.ns !== 'org' || cap.action !== 'role.auditor' || cap.resource !== ext.org_id))
563
+ throw invalid('archive credential must grant only organization audit reads');
564
+ }
565
+ signature(credential.signature);
566
+ }
567
+ export function createCredential(options, signer) {
568
+ if (!isRecord(options) || 'signature' in options)
569
+ throw invalid('credential creation requires an unsigned object');
570
+ const value = structuredClone(options);
571
+ const now = Math.trunc(Date.now() / 1000);
572
+ const defaults = { ver: 'aun-cap/1', cid: 'cred-' + randomUUID().replace(/-/g, ''), iat: now, iss: signer.aid, delegate: false, proof_chain: [] };
573
+ for (const [key, item] of Object.entries(defaults))
574
+ if (!(key in value))
575
+ value[key] = item;
576
+ if (!('nbf' in value))
577
+ value.nbf = value.iat;
578
+ if (value.iss !== signer.aid)
579
+ throw invalid('issuer does not match signing AID');
580
+ const chain = value.proof_chain;
581
+ const parentDepth = Array.isArray(chain) && isRecord(chain[0]) ? chain[0].delegation_depth ?? 0 : 0;
582
+ if (!('delegation_depth' in value))
583
+ value.delegation_depth = value.parent !== undefined && value.parent !== null && isInt(parentDepth) ? parentDepth + 1 : 0;
584
+ if (Array.isArray(value.cap))
585
+ for (const item of value.cap)
586
+ if (isRecord(item) && !('constraints' in item))
587
+ item.constraints = [];
588
+ if (!isInt(value.iat) || !isInt(value.exp) || value.iat > now || value.exp <= now)
589
+ throw invalid('credential signing time is outside its validity window');
590
+ const result = signer.sign(Buffer.from(canonicalJson(value)));
591
+ if (!result.ok)
592
+ throw new ProtocolError('CREDENTIAL_SIGNATURE_INVALID', 'credential signing failed');
593
+ value.signature = { cert_fingerprint: signer.certFingerprint, timestamp: now, signature: result.data.signature };
594
+ credentialChainDocuments(value);
595
+ verifyCredentialSignature(value, (fingerprint) => fingerprint === signer.certFingerprint ? signer.certPem : null);
596
+ return value;
597
+ }
598
+ function organizationIdentifier() { return randomUUID().replace(/-/g, ''); }
599
+ function organizationNonce() { return randomBytes(32).toString('base64url'); }
600
+ export function signMembershipBinding(binding, signer, party) {
601
+ validateMembershipBindingPayload(binding, true);
602
+ if (party !== 'member' && party !== 'org' || party === 'member' && binding.member_aid !== signer.aid)
603
+ throw invalid('membership signer does not match party');
604
+ const value = structuredClone(binding);
605
+ const payload = { ...value };
606
+ delete payload.member_signature;
607
+ delete payload.org_signature;
608
+ value[party + '_signature'] = signProtocolPayload(payload, signer);
609
+ return value;
610
+ }
611
+ function signProtocolPayload(payload, signer) {
612
+ const now = Math.trunc(Date.now() / 1000);
613
+ const result = signer.sign(Buffer.from(canonicalJson(payload)));
614
+ if (!result.ok)
615
+ throw new ProtocolError('CREDENTIAL_SIGNATURE_INVALID', 'protocol signing failed');
616
+ const signature = { cert_fingerprint: signer.certFingerprint, timestamp: now, signature: result.data.signature };
617
+ try {
618
+ verifyAgentSignature(signature, payload, signer.aid, () => signer.certPem, now);
619
+ }
620
+ catch {
621
+ throw new ProtocolError('CREDENTIAL_SIGNATURE_INVALID', 'protocol signing identity is invalid');
622
+ }
623
+ return signature;
624
+ }
625
+ export function createMembershipBinding(options, memberSigner, orgSigner) {
626
+ const value = structuredClone(options);
627
+ const terms = 'terms' in value ? value.terms : {};
628
+ delete value.terms;
629
+ if (!isRecord(terms) || 'member_signature' in value || 'org_signature' in value)
630
+ throw invalid('membership creation requires unsigned terms');
631
+ const now = Math.trunc(Date.now() / 1000);
632
+ const defaults = { ver: 'aun-membership-binding/1', membership_id: 'mem-' + organizationIdentifier(), binding_version: 1, member_type: 'custom', category: 'member', role: 'member', ownership: 'personal', visibility: 'members', request_id: organizationIdentifier(), nonce: organizationNonce(), iat: now, exp: now + 3600, terms_hash: digest(terms) };
633
+ for (const [key, item] of Object.entries(defaults))
634
+ if (!(key in value))
635
+ value[key] = item;
636
+ if (value.terms_hash !== digest(terms) || !['owner', 'admin', 'member', 'observer'].includes(String(value.role)))
637
+ throw invalid('membership terms or role is invalid');
638
+ validateMembershipBindingPayload(value, true);
639
+ let binding = value;
640
+ if (memberSigner)
641
+ binding = signMembershipBinding(binding, memberSigner, 'member');
642
+ if (orgSigner)
643
+ binding = signMembershipBinding(binding, orgSigner, 'org');
644
+ return binding;
645
+ }
646
+ export function createRepresentationProof(credential, params, binding, signer, proofChain) {
647
+ const now = Math.trunc(Date.now() / 1000);
648
+ const target = params.for;
649
+ validateCredentialAt(credential, now, undefined, proofChain);
650
+ if (typeof target !== 'string' || !AID_RE.test(target) || target === signer.aid)
651
+ throw invalid('representation requires another target AID');
652
+ validateRequestBinding(binding, String(binding.method), String(binding.target), params, String(credential.cid), String(params.nonce), String(params.request_id), now);
653
+ const proof = { issuer: signer.aid, from: signer.aid, represented: credential.sub, for: target, audience: binding.target, resource: target, method: binding.method, params_hash: binding.params_hash, iat: binding.iat, exp: binding.exp, nonce: binding.nonce, credential_cid: credential.cid };
654
+ proof.signature = signProtocolPayload(proof, signer);
655
+ return proof;
656
+ }
657
+ export function signTransferAcceptance(proposal, signer) {
658
+ if (!isRecord(proposal) || proposal.ver !== 'aun-org-transfer/1' || proposal.target_member_aid !== signer.aid || !isInt(proposal.exp) || proposal.exp <= Math.trunc(Date.now() / 1000))
659
+ throw invalid('transfer proposal does not match the accepting identity');
660
+ const payload = { ...proposal };
661
+ delete payload.signature;
662
+ return { transfer_id: proposal.transfer_id, proposal_hash: digest(payload), target_signature: signProtocolPayload(payload, signer) };
663
+ }
664
+ export function prepareOrganizationRequest(method, params, target, signer, options = {}) {
665
+ if (!method.startsWith('org.') || !AID_RE.test(target))
666
+ throw invalid('organization method and service AID are required');
667
+ let value = structuredClone(params);
668
+ const memberSigner = options.memberSigner ?? signer;
669
+ let binding = isRecord(value.membership_binding) ? value.membership_binding : undefined;
670
+ if (method === 'org.member.inviteAccept' && value.for !== undefined && value.for !== signer.aid && (!binding || !('org_signature' in binding) || !('credential_template' in value) || !('registration_authorization' in value)))
671
+ throw invalid('represented invitation acceptance requires original organization materials');
672
+ if ('membership_binding' in value)
673
+ validateMembershipBindingPayload(value.membership_binding, true);
674
+ const anchored = ['org.lifecycle.create', 'org.member.join', 'org.member.approveJoin', 'org.member.inviteAccept', 'org.member.renewCredential'].includes(method);
675
+ for (const key of ['request_id', 'nonce']) {
676
+ if (anchored && binding) {
677
+ if (key in value && value[key] !== binding[key])
678
+ throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'request identity differs from membership binding');
679
+ value[key] ??= binding[key];
680
+ }
681
+ if (!(key in value))
682
+ value[key] = key === 'nonce' ? organizationNonce() : organizationIdentifier();
683
+ if (typeof value[key] !== 'string' || !value[key])
684
+ throw invalid('request binding identity is invalid');
685
+ }
686
+ if (method === 'org.lifecycle.create') {
687
+ if (!('owner_aid' in value))
688
+ value.owner_aid = signer.aid;
689
+ if (value.owner_aid !== signer.aid)
690
+ throw invalid('initial owner must match the authenticated signer');
691
+ if (!binding) {
692
+ if (!options.orgSigner || options.orgSigner.aid !== value.org_id)
693
+ throw invalid('organization signing identity is required');
694
+ binding = createMembershipBinding({ org_id: value.org_id, member_aid: signer.aid, role: 'owner', member_type: 'human', terms: { org_id: value.org_id, owner_aid: signer.aid }, request_id: value.request_id, nonce: value.nonce }, signer, options.orgSigner);
695
+ value.membership_binding = binding;
696
+ }
697
+ }
698
+ else if (method === 'org.member.join') {
699
+ if (!('member_aid' in value))
700
+ value.member_aid = 'for' in value ? value.for : memberSigner.aid;
701
+ for (const [key, fallback] of Object.entries({ member_type: 'custom', category: 'member', requested_role: 'member', ownership: 'personal', visibility: 'members', terms: {} }))
702
+ if (!(key in value))
703
+ value[key] = binding?.[key === 'requested_role' ? 'role' : key] ?? fallback;
704
+ if (!('terms_hash' in value))
705
+ value.terms_hash = digest(value.terms);
706
+ if (!binding) {
707
+ const material = {};
708
+ for (const key of ['org_id', 'member_aid', 'member_type', 'category', 'ownership', 'visibility', 'terms', 'request_id', 'nonce'])
709
+ material[key] = value[key];
710
+ material.role = value.requested_role;
711
+ if ('exp' in value)
712
+ material.exp = value.exp;
713
+ binding = createMembershipBinding(material, memberSigner);
714
+ }
715
+ else if (!('member_signature' in binding))
716
+ binding = signMembershipBinding(binding, memberSigner, 'member');
717
+ value.membership_binding = binding;
718
+ value.member_signature = binding.member_signature;
719
+ value.exp = binding.exp;
720
+ }
721
+ else if (method === 'org.member.invite') {
722
+ const targets = value.targets;
723
+ if (!Array.isArray(targets) || !targets.length || targets.some((aid) => typeof aid !== 'string' || !AID_RE.test(aid)) || new Set(targets).size !== targets.length)
724
+ throw invalid('invite targets must be distinct concrete AIDs');
725
+ for (const [key, fallback] of Object.entries({ member_type: 'custom', category: 'member', role: 'member', ownership: 'personal', visibility: 'members', terms: {} }))
726
+ if (!(key in value))
727
+ value[key] = binding?.[key] ?? fallback;
728
+ if (!('terms_hash' in value))
729
+ value.terms_hash = digest(value.terms);
730
+ const materials = 'membership_bindings' in value ? value.membership_bindings : (targets.length === 1 && binding ? { [String(targets[0])]: binding } : {});
731
+ if (!isRecord(materials) || Object.keys(materials).some((aid) => !targets.includes(aid)))
732
+ throw invalid('invite bindings do not match targets');
733
+ for (const targetAid of targets) {
734
+ let material = materials[targetAid];
735
+ if (!isRecord(material) || !('org_signature' in material)) {
736
+ if (!options.orgSigner || options.orgSigner.aid !== value.org_id)
737
+ throw invalid('organization signing identity is required');
738
+ if (material === undefined) {
739
+ const fields = { member_aid: targetAid };
740
+ for (const key of ['org_id', 'member_type', 'category', 'role', 'ownership', 'visibility', 'terms'])
741
+ fields[key] = value[key];
742
+ if (targets.length === 1)
743
+ fields.nonce = value.nonce;
744
+ if ('exp' in value)
745
+ fields.exp = value.exp;
746
+ material = createMembershipBinding(fields, undefined, options.orgSigner);
747
+ }
748
+ else
749
+ material = signMembershipBinding(material, options.orgSigner, 'org');
750
+ }
751
+ validateMembershipBindingPayload(material, true);
752
+ materials[targetAid] = material;
753
+ }
754
+ if (targets.length === 1) {
755
+ const nonce = materials[String(targets[0])].nonce;
756
+ if ('nonce' in params && params.nonce !== nonce)
757
+ throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'request nonce differs from invite binding');
758
+ value.nonce = nonce;
759
+ }
760
+ value.membership_bindings = materials;
761
+ }
762
+ else if (method === 'org.member.approveJoin' && binding) {
763
+ if (!('org_signature' in binding))
764
+ binding = signMembershipBinding(binding, signer, 'org');
765
+ value.membership_binding = binding;
766
+ value.org_signature = binding.org_signature;
767
+ if (!('approved_role' in value))
768
+ value.approved_role = binding.role;
769
+ if (!('join_request_id' in value))
770
+ value.join_request_id = binding.request_id;
771
+ }
772
+ else if (method === 'org.member.inviteAccept' && binding) {
773
+ if (!('member_signature' in binding))
774
+ binding = signMembershipBinding(binding, memberSigner, 'member');
775
+ value.membership_binding = binding;
776
+ value.member_signature = binding.member_signature;
777
+ if (!('member_aid' in value))
778
+ value.member_aid = binding.member_aid;
779
+ if (!('terms_hash' in value))
780
+ value.terms_hash = binding.terms_hash;
781
+ }
782
+ if (method === 'org.member.renewCredential') {
783
+ if (!binding)
784
+ throw invalid('replacement membership binding is required');
785
+ if (!('member_signature' in binding))
786
+ binding = signMembershipBinding(binding, memberSigner, 'member');
787
+ if (!('org_signature' in binding)) {
788
+ if (!options.orgSigner || options.orgSigner.aid !== value.org_id)
789
+ throw invalid('organization signing identity is required');
790
+ binding = signMembershipBinding(binding, options.orgSigner, 'org');
791
+ }
792
+ value.membership_binding = binding;
793
+ if (!('member_aid' in value))
794
+ value.member_aid = binding.member_aid;
795
+ }
796
+ const materials = isRecord(value.membership_bindings) ? value.membership_bindings : {};
797
+ const signedBinding = method === 'org.member.invite' ? (Object.keys(materials).length === 1 ? Object.values(materials)[0] : undefined) : binding;
798
+ for (const field of ['member_signature', 'org_signature']) {
799
+ if (field in params && (!isRecord(signedBinding) || !(field in signedBinding) || canonicalText(params[field]) !== canonicalText(signedBinding[field])))
800
+ throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'outer signature differs from membership binding');
801
+ }
802
+ if (['org.lifecycle.create', 'org.member.approveJoin', 'org.member.renewCredential'].includes(method) && !('credential_template' in value)) {
803
+ if (!options.orgSigner || !binding)
804
+ throw invalid('organization signer or signed member CAP is required');
805
+ value.credential_template = createMemberCredential(binding, options.orgSigner, { key_version: options.orgSigner.certFingerprint, ...options.credentialOptions });
806
+ }
807
+ if (method !== 'org.member.inviteAccept' && isRecord(value.credential_template) && !('registration_authorization' in value)) {
808
+ if (!binding)
809
+ throw invalid('membership binding is required for registration');
810
+ const issuerSigner = options.orgSigner ?? (signer.aid === value.credential_template.iss ? signer : undefined);
811
+ value.registration_authorization = createRegistrationAuthorization(value.credential_template, binding, target, issuerSigner);
812
+ }
813
+ if (method === 'org.member.invite' && options.orgSigner) {
814
+ const templates = isRecord(value.credential_templates) ? value.credential_templates : {};
815
+ const authorizations = isRecord(value.registration_authorizations) ? value.registration_authorizations : {};
816
+ for (const [aid, material] of Object.entries(materials)) {
817
+ if (!(aid in templates))
818
+ templates[aid] = createMemberCredential(material, options.orgSigner, { key_version: options.orgSigner.certFingerprint, ...options.credentialOptions });
819
+ if (!(aid in authorizations))
820
+ authorizations[aid] = createRegistrationAuthorization(templates[aid], material, target, options.orgSigner);
821
+ }
822
+ value.credential_templates = templates;
823
+ value.registration_authorizations = authorizations;
824
+ }
825
+ if (method === 'org.lifecycle.transferAccept' && value.stage !== 'rekey') {
826
+ for (const field of ['previous_owner', 'new_owner']) {
827
+ const material = value[field];
828
+ if (isRecord(material) && !('registration_authorization' in material)) {
829
+ if (!isRecord(material.credential_template) || !isRecord(material.membership_binding))
830
+ throw invalid('transfer registration materials are required');
831
+ const issuerSigner = options.orgSigner ?? (signer.aid === material.credential_template.iss ? signer : undefined);
832
+ material.registration_authorization = createRegistrationAuthorization(material.credential_template, material.membership_binding, target, issuerSigner);
833
+ }
834
+ }
835
+ }
836
+ value = orgRequestParams(method, value);
837
+ const credentials = isRecord(value.credentials) ? value.credentials : {};
838
+ if ('credentials' in value && !isRecord(value.credentials))
839
+ throw invalid('credentials must be an object');
840
+ if (['org.member.join', 'org.member.inviteAccept'].includes(method)) {
841
+ 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(binding.member_signature))
842
+ throw representationBindingInvalid('admission requires the target member signature');
843
+ value.credentials = credentials;
844
+ const now = Math.trunc(Date.now() / 1000);
845
+ let exp = Math.min(now + 300, Number(binding.exp));
846
+ for (const field of ['credential_template', 'registration_authorization'])
847
+ if (field in value) {
848
+ const material = value[field];
849
+ if (!isRecord(material) || !isInt(material.exp))
850
+ throw invalid('admission material expiry is invalid');
851
+ exp = Math.min(exp, material.exp);
852
+ }
853
+ const request = Object.hasOwn(value, 'request_binding') ? value.request_binding : { ver: 'aun-membership-admission-request/1', request_id: value.request_id, nonce: value.nonce, target, method, params_hash: digest(orgRequestBindingParams(method, value)), membership_id: binding.membership_id, iat: now, exp };
854
+ if (!isRecord(request) || Object.keys(request).length !== 9 || Object.keys(request).some((key) => !['ver', 'request_id', 'nonce', 'target', 'method', 'params_hash', 'membership_id', 'iat', 'exp'].includes(key)) || request.ver !== 'aun-membership-admission-request/1' || request.request_id !== value.request_id || request.nonce !== value.nonce || request.target !== target || request.method !== method || request.membership_id !== binding.membership_id || request.params_hash !== digest(orgRequestBindingParams(method, value)) || !isInt(request.iat) || !isInt(request.exp) || request.iat < 0 || request.iat > now || request.iat < Number(binding.iat) || request.exp <= now || request.exp > request.iat + 300 || request.exp > exp)
855
+ throw representationBindingInvalid('admission request binding does not match request');
856
+ value.request_binding = request;
857
+ if (value.for === undefined || value.for === signer.aid)
858
+ return value;
859
+ const proof = { ver: 'aun-membership-admission-representation/1', issuer: signer.aid, from: signer.aid, represented: value.for, for: value.for, audience: target, resource: value.for, org_id: value.org_id, membership_id: binding.membership_id, method, params_hash: request.params_hash, request_id: request.request_id, nonce: request.nonce, iat: request.iat, exp: request.exp };
860
+ if (Object.hasOwn(credentials, 'representation_proof')) {
861
+ const supplied = credentials.representation_proof;
862
+ if (!isRecord(supplied) || canonicalText(Object.fromEntries(Object.entries(supplied).filter(([key]) => key !== 'signature'))) !== canonicalText(proof))
863
+ throw representationBindingInvalid('admission representation differs from request');
864
+ signature(supplied.signature);
865
+ try {
866
+ verifyAgentSignature(supplied.signature, proof, signer.aid, () => signer.certPem, now);
867
+ }
868
+ catch {
869
+ throw new ProtocolError('CREDENTIAL_SIGNATURE_INVALID', 'admission representation signature is invalid');
870
+ }
871
+ }
872
+ else
873
+ credentials.representation_proof = { ...proof, signature: signProtocolPayload(proof, signer) };
874
+ return value;
875
+ }
876
+ const credential = ['member_credential', 'approval_credential', 'audit_credential'].map((key) => credentials[key]).find(isRecord) ?? value.credential_template;
877
+ if (isRecord(credential)) {
878
+ validateCredential(credential, 0, value.proof_chain);
879
+ const now = Math.trunc(Date.now() / 1000);
880
+ if (!('request_binding' in value))
881
+ value.request_binding = buildRequestBinding(method, target, value, credential.cid, value.nonce, value.request_id, now, Math.min(now + 300, credential.exp));
882
+ validateRequestBinding(value.request_binding, method, target, value, credential.cid, value.nonce, value.request_id, now);
883
+ if (value.for && value.for !== signer.aid && !('representation_proof' in credentials))
884
+ value.credentials = { ...credentials, representation_proof: createRepresentationProof(credential, value, value.request_binding, signer, value.proof_chain) };
885
+ }
886
+ else if (value.for && value.for !== signer.aid)
887
+ throw invalid('representation requires a current holder CAP');
888
+ return value;
889
+ }
890
+ function createRegistrationAuthorization(credential, binding, relay, signer) {
891
+ if (!signer || signer.aid !== credential.iss)
892
+ throw new ProtocolError('CREDENTIAL_AUTHENTICATION_INVALID', 'issuer signing identity or registration authorization is required');
893
+ const now = Math.trunc(Date.now() / 1000);
894
+ const value = { ver: 'aun-credential-registration-authorization/1', issuer_aid: credential.iss, credential_cid: credential.cid, credential_hash: digest(credential), request_id: binding.request_id, nonce: binding.nonce, target: 'credential.' + relay.slice(relay.indexOf('.') + 1), relay_aid: relay, iat: now, exp: binding.exp, revocation_authority: { aid: relay, org_id: binding.org_id, credential_cid: credential.cid, registration_request_id: binding.request_id, reason_codes: ['request_failed', 'membership_ended', 'organization_dissolved', 'org_ownership_transfer', 'credential_renewed'] } };
895
+ value.signature = signProtocolPayload(value, signer);
896
+ return value;
897
+ }
898
+ export function createMemberCredential(binding, signer, options) {
899
+ validateMembershipBindingPayload(binding, true);
900
+ if (!['owner', 'admin', 'member', 'observer'].includes(String(binding.role)))
901
+ throw invalid('membership role is invalid');
902
+ const fixed = { iss: binding.org_id, sub: binding.member_aid, aud: [binding.org_id],
903
+ cap: [{ ns: 'org', action: 'role.' + String(binding.role), resource: binding.org_id, constraints: [] }],
904
+ delegate: false, delegation_depth: 0, proof_chain: [],
905
+ ext: { credential_type: 'member', org_id: binding.org_id, membership_id: binding.membership_id, binding_version: binding.binding_version, membership_terms_hash: binding.terms_hash, bound_to_membership: true } };
906
+ const text = (value) => new TextDecoder().decode(canonicalJson(value));
907
+ if (!isRecord(options) || Object.entries(fixed).some(([key, value]) => key in options && text(options[key]) !== text(value)) || options.parent !== undefined && options.parent !== null)
908
+ throw invalid('member credential options conflict with binding');
909
+ return createCredential({ exp: binding.exp, ...options, ...fixed }, signer);
910
+ }
911
+ export function verifyCredential(credential, context) {
912
+ if (!context || !context.audience || !context.subject)
913
+ throw invalid('credential verification requires a target audience and holder');
914
+ validateCredential(credential, 0, context.proofChain);
915
+ const value = credential;
916
+ if (value.sub !== context.subject)
917
+ throw invalid('credential subject does not match holder');
918
+ const resolveStatus = context.resolveStatus;
919
+ if (!resolveStatus && !context.resolveStatusProof)
920
+ throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'trusted credential status is unavailable');
921
+ const now = context.now ?? Math.trunc(Date.now() / 1000);
922
+ const documents = credentialChainDocuments(credential, context.proofChain);
923
+ const status = (cid) => {
924
+ let proof;
925
+ try {
926
+ proof = context.resolveStatusProof?.(cid);
927
+ }
928
+ catch {
929
+ throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'trusted credential status proof is unavailable');
930
+ }
931
+ const document = documents.find((item) => item.cid === cid);
932
+ if (proof != null || document !== value) {
933
+ if (!isRecord(proof) || proof.ver !== 'aun-credential-status/1' || typeof proof.credentialCid !== 'string' || !proof.credentialCid || 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)
934
+ throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'trusted credential status proof is invalid');
935
+ if (typeof proof.responder !== 'string' || !AID_RE.test(proof.responder) || !proof.responder.startsWith('credential.') || typeof proof.request_id !== 'string' || !proof.request_id || typeof proof.nonce !== 'string' || !proof.nonce || typeof proof.target !== 'string' || !AID_RE.test(proof.target) || !isRecord(proof.signature) || proof.signature.timestamp !== proof.asOf)
936
+ throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'trusted credential status proof is incomplete');
937
+ try {
938
+ signature(proof.signature);
939
+ }
940
+ catch {
941
+ throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'trusted credential status proof signature structure is invalid');
942
+ }
943
+ if (proof.credentialCid !== cid || proof.credential_hash !== digest(document))
944
+ throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'credential differs from its registered identity or hash');
945
+ return typeof proof.status === 'string' ? proof.status : undefined;
946
+ }
947
+ try {
948
+ return resolveStatus?.(cid);
949
+ }
950
+ catch {
951
+ throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'trusted credential status is unavailable');
952
+ }
953
+ };
954
+ verifyCredentialChain(credential, context.resolveCertificate, status, now, context.audience, context.proofChain);
955
+ if (credentialChainDocuments(credential, context.proofChain).at(-1).iss !== (context.rootIssuer ?? context.audience))
956
+ throw invalid('credential root issuer is not trusted for this resource');
957
+ const rate = context.checkRateLimit ? (field, unit, limit) => context.checkRateLimit(String(value.cid), String(value.sub), field, unit, limit) : undefined;
958
+ validateCredentialCapability(credential, { ns: context.ns, action: context.action, resource: context.resource, params: context.params, units: context.units, now, client_ip: context.clientIp, proof_chain: context.proofChain }, rate);
959
+ if (!context.method || !context.target)
960
+ throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'the actual request context is required');
961
+ validateRequestBinding(context.requestBinding, context.method, context.target, context.params ?? {}, String(value.cid), context.nonce ?? '', context.requestId ?? '', now);
962
+ const ext = value.ext;
963
+ if (isRecord(ext) && ext.credential_type === 'member' && (ext.bound_to_membership !== true || value.delegate === true))
964
+ throw invalid('member credential must be bound and cannot delegate');
965
+ if (isRecord(ext) && ext.bound_to_membership === true) {
966
+ const binding = context.membershipBinding;
967
+ if (!isRecord(binding) || binding.member_aid !== value.sub || binding.org_id !== ext.org_id || binding.membership_id !== ext.membership_id || binding.binding_version !== ext.binding_version || binding.terms_hash !== ext.membership_terms_hash)
968
+ throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'current membership binding is required');
969
+ if (ext.credential_type === 'member') {
970
+ const caps = value.cap;
971
+ 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)
972
+ throw invalid('member credential capability does not match binding role');
973
+ }
974
+ verifyMembershipBinding(binding, context.resolveCertificate, now, context.orgSigner, true);
975
+ if (!context.resolveMembershipStatus || context.resolveMembershipStatus(binding, value) !== true)
976
+ throw new ProtocolError('MEMBERSHIP_STATE_INVALID', 'trusted current membership status does not permit this operation');
977
+ }
978
+ const result = { signature_valid: true, chain_valid: true, status_checked: true, capability_checked: true, request_bound: true };
979
+ const params = context.params ?? {};
980
+ const target = context.for ?? params.for;
981
+ const actor = context.actor ?? context.subject;
982
+ if (typeof actor !== 'string' || !AID_RE.test(actor))
983
+ throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'authenticated actor is invalid');
984
+ if (target !== undefined && (typeof target !== 'string' || !target || !context.actor || Object.hasOwn(params, 'for') && params.for !== target))
985
+ throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'authenticated representation context is required');
986
+ if (target !== undefined && target !== actor || actor !== context.subject) {
987
+ if (target === undefined || target === actor)
988
+ throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'credential holder requires representation');
989
+ const packaged = isRecord(params.credentials) ? params.credentials.representation_proof : undefined;
990
+ const proof = context.representationProof ?? packaged;
991
+ if (packaged !== undefined && canonicalText(packaged) !== canonicalText(proof))
992
+ throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'representation proof differs from the request');
993
+ verifyRepresentationProof(proof, credential, { from: actor, for: target, audience: context.target, resource: target, params, binding: context.requestBinding }, context.resolveActorCertificate ?? context.resolveCertificate, now, context.proofChain);
994
+ const subjects = [String(target), ...(context.subject !== actor && context.subject !== target ? [context.subject] : [])];
995
+ let allowed = Boolean(context.resolveOwnership);
996
+ try {
997
+ for (const subject of subjects)
998
+ if (!context.resolveOwnership || context.resolveOwnership(subject, actor) !== true)
999
+ allowed = false;
1000
+ if (context.ns === 'org' && !['active', 'nonmember', 'frozen', 'offboarding', 'ended'].includes(context.resolveActorStatus?.(context.audience, actor) ?? 'unknown'))
1001
+ allowed = false;
1002
+ }
1003
+ catch {
1004
+ allowed = false;
1005
+ }
1006
+ if (!allowed)
1007
+ throw new ProtocolError('ROLE_ACCESS_DENIED', 'trusted current ownership does not permit representation');
1008
+ result.representation_checked = true;
1009
+ }
1010
+ return result;
1011
+ }
1012
+ export function validateCredentialAt(credential, now, audience, proofChain) {
1013
+ validateCredential(credential, 0, proofChain);
1014
+ if (!isInt(now))
1015
+ throw invalid('credential validation time must be an integer');
1016
+ const value = credential;
1017
+ const nbf = value.nbf;
1018
+ const exp = value.exp;
1019
+ if (!isInt(nbf) || !isInt(exp) || now < nbf || now >= exp)
1020
+ throw new ProtocolError('CREDENTIAL_EXPIRED', 'credential is outside its validity window');
1021
+ if (audience !== undefined) {
1022
+ if (!Array.isArray(value.aud) || !value.aud.includes(audience))
1023
+ throw new ProtocolError('CREDENTIAL_AUDIENCE_MISMATCH', 'credential audience does not match target');
1024
+ }
1025
+ }
1026
+ export function verifyCredentialSignature(credential, resolveCertificate, now, audience, proofChain) {
1027
+ try {
1028
+ validateCredential(credential, 0, proofChain);
1029
+ }
1030
+ catch (error) {
1031
+ if (error instanceof ProtocolError && error.message.toLowerCase().includes('signature')) {
1032
+ throw new ProtocolError('CREDENTIAL_SIGNATURE_INVALID', 'credential signature structure is invalid');
1033
+ }
1034
+ throw error;
1035
+ }
1036
+ const value = credential;
1037
+ const signatureValue = value.signature;
1038
+ const issuer = value.iss;
1039
+ try {
1040
+ const payload = { ...value };
1041
+ delete payload.signature;
1042
+ verifyAgentSignature(signatureValue, payload, issuer, resolveCertificate, now);
1043
+ }
1044
+ catch {
1045
+ throw new ProtocolError('CREDENTIAL_SIGNATURE_INVALID', 'credential signature verification failed');
1046
+ }
1047
+ if (now !== undefined) {
1048
+ validateCredentialAt(credential, now, audience, proofChain);
1049
+ }
1050
+ else if (audience !== undefined && !value.aud.includes(audience)) {
1051
+ throw new ProtocolError('CREDENTIAL_AUDIENCE_MISMATCH', 'credential audience does not match target');
1052
+ }
1053
+ }
1054
+ function canonicalText(value) {
1055
+ return new TextDecoder().decode(canonicalJson(value));
1056
+ }
1057
+ function constraintField(context, field) {
1058
+ let value = context;
1059
+ for (const part of field.split('.')) {
1060
+ if (!isRecord(value) || !Object.hasOwn(value, part))
1061
+ return [undefined, false];
1062
+ value = value[part];
1063
+ }
1064
+ return [value, true];
1065
+ }
1066
+ function evaluateConstraints(constraints, context, rate) {
1067
+ const pending = [];
1068
+ for (const item of constraints) {
1069
+ if (item.type === 'rate_limit') {
1070
+ pending.push(item);
1071
+ continue;
1072
+ }
1073
+ const field = String(item.field);
1074
+ let [actual, found] = constraintField(context, field);
1075
+ if (!found)
1076
+ [actual, found] = constraintField(context.params, field);
1077
+ if (!found)
1078
+ return false;
1079
+ const units = isRecord(context.units) ? context.units : {};
1080
+ if (item.type !== 'time_window' && (units[field] ?? '') !== constraintUnit(item))
1081
+ return false;
1082
+ try {
1083
+ if (!constraintSatisfied(item, actual))
1084
+ return false;
1085
+ }
1086
+ catch {
1087
+ return false;
1088
+ }
1089
+ }
1090
+ for (const item of pending)
1091
+ if (!rate || rate(String(item.field), constraintUnit(item), Number(item.value)) !== true)
1092
+ return false;
1093
+ return true;
1094
+ }
1095
+ export function validateCredentialCapability(credential, context, rate) {
1096
+ validateCredential(credential, 0, context.proof_chain);
1097
+ if (!isRecord(context) || !['ns', 'action', 'resource'].every((field) => typeof context[field] === 'string' && context[field]))
1098
+ throw new ProtocolError('CREDENTIAL_CAPABILITY_DENIED', 'an explicit capability requirement is required');
1099
+ const value = credential;
1100
+ const actual = { ...context, sub: value.sub, delegation_depth: value.delegation_depth ?? 0, operation: String(context.action).split('.').at(-1), valid_time: context.now };
1101
+ for (const cap of value.cap) {
1102
+ if (cap.ns === context.ns && (cap.action === context.action || cap.action === '*') && (cap.resource === context.resource || cap.resource === '*') && evaluateConstraints(cap.constraints, actual, rate))
1103
+ return;
1104
+ }
1105
+ throw new ProtocolError('CREDENTIAL_CAPABILITY_DENIED', 'credential does not cover the actual operation or constraints');
1106
+ }
1107
+ function constraintUnit(item) {
1108
+ return String(item.unit || (item.type === 'rate_limit' ? 'second' : item.type === 'time_window' ? 'UTC' : ''));
1109
+ }
1110
+ function constraintSatisfied(item, actual) {
1111
+ const value = item.value;
1112
+ switch (item.type) {
1113
+ case 'max_value':
1114
+ case 'min_value':
1115
+ return typeof actual === 'number' && Number.isFinite(actual) && typeof value === 'number' && (item.type === 'max_value' ? actual <= value : actual >= value);
1116
+ case 'range':
1117
+ return typeof actual === 'number' && Number.isFinite(actual) && Array.isArray(value) && actual >= value[0] && actual <= value[1];
1118
+ case 'allowed_values':
1119
+ case 'denied_values': {
1120
+ if (!Array.isArray(value))
1121
+ return false;
1122
+ const found = value.some((entry) => canonicalText(entry) === canonicalText(actual));
1123
+ return item.type === 'allowed_values' ? found : !found;
1124
+ }
1125
+ case 'path_prefix': {
1126
+ const path = canonicalPath(actual), prefix = canonicalPath(value);
1127
+ return path !== undefined && prefix !== undefined && (path === prefix || path.startsWith(prefix.replace(/\/$/u, '') + '/'));
1128
+ }
1129
+ case 'regex': {
1130
+ if (typeof actual !== 'string' || new TextEncoder().encode(actual).length > 4096 || typeof value !== 'string' || !validConstraintRegex(value))
1131
+ return false;
1132
+ const match = new RegExp(value, 'u').exec(actual);
1133
+ return match !== null && match[0] === actual;
1134
+ }
1135
+ case 'ip_whitelist': {
1136
+ if (typeof actual !== 'string' || /[/%]/u.test(actual) || !Array.isArray(value))
1137
+ return false;
1138
+ const address = constraintIP(actual);
1139
+ return address !== undefined && value.some((entry) => {
1140
+ const prefix = constraintIP(entry);
1141
+ return prefix !== undefined && address.bits === prefix.bits && (address.address >> BigInt(address.bits - prefix.prefix)) === (prefix.address >> BigInt(prefix.bits - prefix.prefix));
1142
+ });
1143
+ }
1144
+ case 'time_window': {
1145
+ if (!isInt(actual) || actual < 0 || !Array.isArray(value))
1146
+ return false;
1147
+ const current = new Date(actual * 1000).toISOString().slice(11, 16);
1148
+ return current >= value[0] && current < value[1];
1149
+ }
1150
+ }
1151
+ return false;
1152
+ }
1153
+ function constraintNarrower(child, parent) {
1154
+ if (!isRecord(child) || !isRecord(parent) || child.type !== parent.type || child.field !== parent.field || constraintUnit(child) !== constraintUnit(parent))
1155
+ return false;
1156
+ const a = child.value, b = parent.value;
1157
+ if (['max_value', 'min_value', 'rate_limit'].includes(child.type))
1158
+ return typeof a === 'number' && Number.isFinite(a) && typeof b === 'number' && Number.isFinite(b) && (child.type === 'min_value' ? a >= b : a <= b);
1159
+ if (child.type === 'allowed_values' || child.type === 'denied_values') {
1160
+ if (!Array.isArray(a) || !Array.isArray(b))
1161
+ return false;
1162
+ const [subset, superset] = child.type === 'denied_values' ? [b, a] : [a, b];
1163
+ return subset.every((entry) => superset.some((candidate) => canonicalText(entry) === canonicalText(candidate)));
1164
+ }
1165
+ if (child.type === 'ip_whitelist') {
1166
+ if (!Array.isArray(a) || !Array.isArray(b))
1167
+ return false;
1168
+ return a.every((entry) => {
1169
+ const subnet = constraintIP(entry);
1170
+ return subnet !== undefined && b.some((candidate) => {
1171
+ const prefix = constraintIP(candidate);
1172
+ return prefix !== undefined && subnet.bits === prefix.bits && subnet.prefix >= prefix.prefix && (subnet.address >> BigInt(subnet.bits - prefix.prefix)) === (prefix.address >> BigInt(prefix.bits - prefix.prefix));
1173
+ });
1174
+ });
1175
+ }
1176
+ if (child.type === 'range' || child.type === 'time_window')
1177
+ return Array.isArray(a) && Array.isArray(b) && a.length === 2 && b.length === 2 && a[0] >= b[0] && a[1] <= b[1];
1178
+ if (child.type === 'path_prefix') {
1179
+ const path = canonicalPath(a), prefix = canonicalPath(b);
1180
+ return path !== undefined && prefix !== undefined && (path === prefix || path.startsWith(prefix.replace(/\/$/u, '') + '/'));
1181
+ }
1182
+ return canonicalText(a) === canonicalText(b);
1183
+ }
1184
+ function capabilitiesNarrower(child, parent, subject, depth) {
1185
+ if (!Array.isArray(child) || !Array.isArray(parent))
1186
+ return false;
1187
+ return child.every((current) => isRecord(current) && Array.isArray(current.constraints) && parent.some((candidate) => {
1188
+ if (!isRecord(candidate) || current.ns !== candidate.ns || current.action !== candidate.action && candidate.action !== '*' || current.resource !== candidate.resource && candidate.resource !== '*' || !Array.isArray(candidate.constraints))
1189
+ return false;
1190
+ return candidate.constraints.every((raw) => {
1191
+ if (!isRecord(raw))
1192
+ return false;
1193
+ const actual = { sub: subject, delegation_depth: depth };
1194
+ if (candidate.action === '*')
1195
+ actual.operation = String(current.action).split('.').at(-1);
1196
+ if (candidate.resource === '*' && current.resource !== '*')
1197
+ actual.resource = current.resource;
1198
+ if (Object.hasOwn(actual, String(raw.field)) && !constraintSatisfied(raw, actual[String(raw.field)]))
1199
+ return false;
1200
+ return current.constraints.some((item) => constraintNarrower(item, raw));
1201
+ });
1202
+ }));
1203
+ }
1204
+ function verifyAgentSignature(signatureValue, payload, expectedAid, resolveCertificate, now, enforceWindow = true) {
1205
+ const certPem = certificatePem(resolveCertificate(String(signatureValue.cert_fingerprint)));
1206
+ const lower = payload.iat ?? payload.registered_at;
1207
+ if (enforceWindow && typeof lower === 'number' && (Number(signatureValue.timestamp) < lower || Number(signatureValue.timestamp) >= Number(payload.exp)))
1208
+ throw new Error('signature timestamp is outside the signed object window');
1209
+ if (!certPem)
1210
+ throw new Error('certificate is missing');
1211
+ const cert = new X509Certificate(certPem);
1212
+ if (certFingerprint(certPem).toLowerCase() !== String(signatureValue.cert_fingerprint).toLowerCase())
1213
+ throw new Error('certificate fingerprint mismatch');
1214
+ validateAgentMdCertificate(certPem, expectedAid, Number(signatureValue.timestamp), { now, requireCurrentValidity: true });
1215
+ if (signatureValue.public_key_fingerprint !== undefined && publicKeyFingerprint(certPem).toLowerCase() !== String(signatureValue.public_key_fingerprint).toLowerCase())
1216
+ throw new Error('public key fingerprint mismatch');
1217
+ const current = Math.trunc(now ?? Date.now() / 1000);
1218
+ if (Number(signatureValue.timestamp) > current + 300)
1219
+ throw new Error('signature timestamp is too far in the future');
1220
+ if (!verifySignatureWithCert(certPem, Buffer.from(String(signatureValue.signature), 'base64'), Buffer.from(canonicalJson(payload))))
1221
+ throw new Error('signature verification failed');
1222
+ }
1223
+ function auditScopeTime(value) {
1224
+ if (isInt(value) && value >= 0)
1225
+ return value;
1226
+ if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u.test(value))
1227
+ return undefined;
1228
+ const parsed = Math.floor(Date.parse(value) / 1000);
1229
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
1230
+ }
1231
+ function auditScopeNarrower(child, parent) {
1232
+ const current = isRecord(child.ext) ? child.ext : {};
1233
+ const previous = isRecord(parent.ext) ? parent.ext : {};
1234
+ const archive = previous.credential_type === 'archive' || current.credential_type === 'archive';
1235
+ if (archive && (previous.credential_type !== 'archive' || current.credential_type !== 'archive' || previous.org_id !== current.org_id))
1236
+ return false;
1237
+ if (previous.credential_type === 'audit' || 'audit_fields' in previous) {
1238
+ const allowed = new Set(['event_id', 'event_seq', 'event_type', 'org_id', 'occurred_at', 'received_at', 'previous_hash', 'event_hash']);
1239
+ if (archive)
1240
+ allowed.clear();
1241
+ for (const ext of [previous, current])
1242
+ if ('audit_fields' in ext && (!Array.isArray(ext.audit_fields) || !ext.audit_fields.length || ext.audit_fields.some((item) => typeof item !== 'string' || !item)))
1243
+ return false;
1244
+ for (const field of (previous.audit_fields ?? []))
1245
+ allowed.add(field);
1246
+ if (!(current.audit_fields ?? []).every((field) => allowed.has(field)))
1247
+ return false;
1248
+ }
1249
+ if ('audit_window' in previous) {
1250
+ const bounds = [];
1251
+ for (const ext of [previous, current]) {
1252
+ if (!isRecord(ext.audit_window))
1253
+ return false;
1254
+ const start = auditScopeTime(ext.audit_window.from);
1255
+ const end = ext.audit_window.to == null ? Infinity : auditScopeTime(ext.audit_window.to);
1256
+ if (start === undefined || end === undefined || end < start)
1257
+ return false;
1258
+ bounds.push([start, end]);
1259
+ }
1260
+ if (bounds[1][0] < bounds[0][0] || bounds[1][1] > bounds[0][1])
1261
+ return false;
1262
+ }
1263
+ return true;
1264
+ }
1265
+ function credentialChainDocuments(credential, externalProofChain) {
1266
+ validateCredential(credential, 0, externalProofChain);
1267
+ const value = credential;
1268
+ const chain = value.parent !== undefined && value.parent !== null ? (value.proof_chain ?? externalProofChain) : [];
1269
+ const documents = [value, ...chain];
1270
+ const seen = new Set();
1271
+ for (const document of documents) {
1272
+ if (!isRecord(document) || typeof document.cid !== 'string' || seen.has(document.cid))
1273
+ throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'proof_chain contains a repeated cid');
1274
+ seen.add(document.cid);
1275
+ }
1276
+ let child = value;
1277
+ for (const parent of chain) {
1278
+ if (!isRecord(parent) || child.parent !== parent.cid)
1279
+ throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'parent cid does not match proof_chain');
1280
+ if (child.iss !== parent.sub || parent.delegate !== true)
1281
+ throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'proof_chain issuer or delegation is invalid');
1282
+ if (child.delegation_depth !== (parent.delegation_depth ?? 0) + 1)
1283
+ throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'delegation depth is invalid');
1284
+ if (child.iat < parent.iat || child.nbf < parent.nbf || child.exp > parent.exp)
1285
+ throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'credential time window is wider than parent');
1286
+ if (!child.aud.every((item) => parent.aud.includes(item)))
1287
+ throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'credential audience is wider than parent');
1288
+ if (!capabilitiesNarrower(child.cap, parent.cap, child.sub, child.delegation_depth))
1289
+ throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'credential capability is wider than parent');
1290
+ if (!auditScopeNarrower(child, parent))
1291
+ throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'credential audit scope is wider than parent');
1292
+ child = parent;
1293
+ }
1294
+ if (chain.length > 0 && child.parent !== undefined && child.parent !== null)
1295
+ throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'proof_chain is incomplete');
1296
+ return documents;
1297
+ }
1298
+ export function verifyCredentialChain(credential, resolveCertificate, resolveStatus, now, audience, proofChain) {
1299
+ validateCredential(credential, 0, proofChain);
1300
+ if (resolveStatus === undefined)
1301
+ throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'credential status is unavailable');
1302
+ const documents = credentialChainDocuments(credential, proofChain);
1303
+ for (const document of documents) {
1304
+ const status = resolveStatus(document.cid);
1305
+ if (status === 'revoked')
1306
+ throw new ProtocolError('CREDENTIAL_REVOKED', 'credential is revoked');
1307
+ if (status === 'expired')
1308
+ throw new ProtocolError('CREDENTIAL_EXPIRED', 'credential is expired');
1309
+ if (status !== 'active')
1310
+ throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'credential status is unavailable');
1311
+ }
1312
+ for (const [index, document] of documents.entries())
1313
+ verifyCredentialSignature(document, resolveCertificate, now, audience, index === 0 ? proofChain : undefined);
1314
+ }
1315
+ function validateMembershipBindingPayload(binding, allowPartial = false) {
1316
+ if (!isRecord(binding))
1317
+ throw invalid('membership_binding must be an object');
1318
+ onlyKnown(binding, BINDING_FIELDS);
1319
+ required(binding, allowPartial ? new Set([...BINDING_FIELDS].filter((key) => key !== 'member_signature' && key !== 'org_signature')) : BINDING_FIELDS);
1320
+ if (binding.ver !== 'aun-membership-binding/1' || typeof binding.membership_id !== 'string' || !binding.membership_id)
1321
+ throw invalid('membership_binding identity is invalid');
1322
+ for (const field of ['member_type', 'category', 'role', 'ownership', 'visibility', 'request_id', 'nonce'])
1323
+ if (typeof binding[field] !== 'string' || binding[field].length === 0)
1324
+ throw invalid(`${field} is invalid`);
1325
+ aid(binding.org_id, 'org_id');
1326
+ aid(binding.member_aid, 'member_aid');
1327
+ if (!isInt(binding.binding_version) || binding.binding_version < 1)
1328
+ throw invalid('binding_version is invalid');
1329
+ if (typeof binding.terms_hash !== 'string' || !DIGEST_RE.test(binding.terms_hash))
1330
+ throw invalid('terms_hash is invalid');
1331
+ if (!isInt(binding.iat) || !isInt(binding.exp) || binding.exp <= binding.iat)
1332
+ throw invalid('membership_binding time window is invalid');
1333
+ if ('member_signature' in binding)
1334
+ signature(binding.member_signature, 'member_signature');
1335
+ if ('org_signature' in binding)
1336
+ signature(binding.org_signature, 'org_signature');
1337
+ }
1338
+ export function validateMembershipBinding(binding) {
1339
+ validateMembershipBindingPayload(binding);
1340
+ }
1341
+ export function validateMembershipBindingAt(binding, now, expectedRole) {
1342
+ validateMembershipBinding(binding);
1343
+ const value = binding;
1344
+ const iat = value.iat;
1345
+ const exp = value.exp;
1346
+ if (!isInt(now) || !isInt(iat) || !isInt(exp) || now < iat || now >= exp)
1347
+ throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'membership binding is outside its validity window');
1348
+ if (expectedRole !== undefined && value.role !== expectedRole)
1349
+ throw invalid('membership role is invalid');
1350
+ }
1351
+ export function verifyMembershipBinding(binding, resolveCertificate, now, expectedOrgSigner, historical = false) {
1352
+ try {
1353
+ validateMembershipBinding(binding);
1354
+ }
1355
+ catch (error) {
1356
+ if (error instanceof ProtocolError && error.message.toLowerCase().includes('signature')) {
1357
+ throw new ProtocolError('CREDENTIAL_SIGNATURE_INVALID', 'membership binding signature structure is invalid');
1358
+ }
1359
+ throw error;
1360
+ }
1361
+ if (now !== undefined && !historical)
1362
+ validateMembershipBindingAt(binding, now);
1363
+ const value = binding;
1364
+ const payload = { ...value };
1365
+ const memberSignature = payload.member_signature;
1366
+ const orgSignature = payload.org_signature;
1367
+ delete payload.member_signature;
1368
+ delete payload.org_signature;
1369
+ for (const [signatureValue, signer] of [[memberSignature, value.member_aid], [orgSignature, expectedOrgSigner ?? value.org_id]]) {
1370
+ try {
1371
+ verifyAgentSignature(signatureValue, payload, String(signer), resolveCertificate, now);
1372
+ }
1373
+ catch {
1374
+ throw new ProtocolError('CREDENTIAL_SIGNATURE_INVALID', 'membership binding signature verification failed');
1375
+ }
1376
+ }
1377
+ }
1378
+ export function validateRegistrationReceipt(receipt, credential, request, expectedServiceAid, now) {
1379
+ if (!isRecord(request))
1380
+ throw new ProtocolError('CREDENTIAL_REGISTRATION_RECEIPT_INVALID', 'request must be an object');
1381
+ validateCredential(credential, 0, request.proof_chain);
1382
+ if (!isRecord(receipt))
1383
+ throw new ProtocolError('CREDENTIAL_REGISTRATION_RECEIPT_INVALID', 'receipt must be an object');
1384
+ onlyKnown(receipt, RECEIPT_FIELDS);
1385
+ required(receipt, new Set([...RECEIPT_FIELDS].filter((field) => field !== 'registration_authorization')));
1386
+ if (receipt.ver !== 'aun-credential-registration-receipt/1' || receipt.status !== 'active')
1387
+ throw new ProtocolError('CREDENTIAL_REGISTRATION_RECEIPT_INVALID', 'receipt status is invalid');
1388
+ if (receipt.credential_hash !== digest(credential))
1389
+ throw new ProtocolError('CREDENTIAL_REGISTRATION_RECEIPT_MISMATCH', 'credential hash mismatch');
1390
+ if (isRecord(credential) && (receipt.credential_cid !== credential.cid || receipt.request_id !== request.request_id || receipt.nonce !== request.nonce))
1391
+ throw new ProtocolError('CREDENTIAL_REGISTRATION_RECEIPT_MISMATCH', 'receipt binding mismatch');
1392
+ if (!isInt(receipt.registered_at) || !isInt(receipt.exp) || receipt.exp <= receipt.registered_at)
1393
+ throw new ProtocolError('CREDENTIAL_REGISTRATION_RECEIPT_INVALID', 'receipt time window is invalid');
1394
+ if (receipt.registrar_aid !== credential.iss)
1395
+ throw new ProtocolError('CREDENTIAL_REGISTRATION_RECEIPT_MISMATCH', 'receipt registrar is not the credential issuer');
1396
+ aid(receipt.registrar_aid, 'registrar_aid');
1397
+ aid(receipt.service_aid, 'service_aid');
1398
+ if (request.registrar_aid !== undefined && receipt.registrar_aid !== request.registrar_aid)
1399
+ throw new ProtocolError('CREDENTIAL_REGISTRATION_RECEIPT_MISMATCH', 'receipt registrar does not match request');
1400
+ if (expectedServiceAid !== undefined && receipt.service_aid !== expectedServiceAid)
1401
+ throw new ProtocolError('CREDENTIAL_REGISTRATION_RECEIPT_INVALID', 'receipt service AID mismatch');
1402
+ if (now !== undefined && (!isInt(now) || now < receipt.registered_at || now >= receipt.exp))
1403
+ throw new ProtocolError('CREDENTIAL_REGISTRATION_RECEIPT_INVALID', 'receipt is outside its validity window');
1404
+ signature(receipt.signature);
1405
+ }
1406
+ export function verifyRegistrationReceipt(receipt, credential, request, resolveCertificate, expectedServiceAid, now) {
1407
+ if (!isRecord(request))
1408
+ throw new ProtocolError('CREDENTIAL_REGISTRATION_RECEIPT_INVALID', 'request must be an object');
1409
+ validateCredential(credential, 0, request.proof_chain);
1410
+ try {
1411
+ validateRegistrationReceipt(receipt, credential, request, expectedServiceAid, now);
1412
+ }
1413
+ catch (error) {
1414
+ if (error instanceof ProtocolError && error.code === 'CREDENTIAL_INVALID') {
1415
+ throw new ProtocolError('CREDENTIAL_REGISTRATION_RECEIPT_INVALID', 'registration receipt structure is invalid');
1416
+ }
1417
+ throw error;
1418
+ }
1419
+ const value = receipt;
1420
+ const signatureValue = value.signature;
1421
+ try {
1422
+ const payload = { ...value };
1423
+ delete payload.signature;
1424
+ verifyAgentSignature(signatureValue, payload, String(value.service_aid), resolveCertificate, now);
1425
+ }
1426
+ catch {
1427
+ throw new ProtocolError('CREDENTIAL_REGISTRATION_RECEIPT_INVALID', 'registration receipt signature verification failed');
1428
+ }
1429
+ }
1430
+ export function digest(value) {
1431
+ return `sha256:${createHash('sha256').update(canonicalJson(value)).digest('hex')}`;
1432
+ }
1433
+ export function orgRequestParams(method, params) {
1434
+ if (!method.startsWith('org.'))
1435
+ return params;
1436
+ const orgId = params.org_id ?? null;
1437
+ if (orgId === null) {
1438
+ if (!['org.lifecycle.list', 'org.health', 'org.status'].includes(method))
1439
+ throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'org_id is required');
1440
+ }
1441
+ else if (typeof orgId !== 'string' || !AID_RE.test(orgId))
1442
+ throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'org_id is invalid');
1443
+ const aad = params.aad === undefined ? {} : params.aad;
1444
+ if (!isRecord(aad))
1445
+ throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'organization AAD is invalid');
1446
+ if ('org_id' in aad && aad.org_id !== orgId)
1447
+ throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'AAD org_id does not match request');
1448
+ return { ...params, aad: { ...aad, org_id: orgId } };
1449
+ }
1450
+ function orgRequestBindingParams(method, params) {
1451
+ const prepared = orgRequestParams(method, params);
1452
+ const result = Object.fromEntries(Object.entries(prepared).filter(([key]) => !['request_binding', 'client_signature'].includes(key) && !key.startsWith('_')));
1453
+ if (method.startsWith('org.') && isRecord(result.credentials))
1454
+ result.credentials = Object.fromEntries(Object.entries(result.credentials).filter(([key]) => key !== 'representation_proof'));
1455
+ return result;
1456
+ }
1457
+ export function buildRequestBinding(method, target, params, credentialCid, nonce, requestId, iat, exp) {
1458
+ if (![method, target, nonce, requestId].every((value) => typeof value === 'string' && value.length > 0) || typeof credentialCid !== 'string' || (!credentialCid !== (method === 'credential.revocationList')))
1459
+ throw invalid('request binding identity is invalid');
1460
+ if (!isInt(iat) || !isInt(exp) || exp <= iat)
1461
+ throw invalid('request binding time window is invalid');
1462
+ return { request_id: requestId, nonce, target, method, params_hash: digest(orgRequestBindingParams(method, params)), iat, exp, credential_cid: credentialCid };
1463
+ }
1464
+ export function validateRequestBinding(binding, method, target, params, credentialCid, nonce, requestId, now) {
1465
+ if (!isRecord(binding) || Object.keys(binding).length !== 8 || Object.keys(binding).some((key) => !['request_id', 'nonce', 'target', 'method', 'params_hash', 'iat', 'exp', 'credential_cid'].includes(key)))
1466
+ throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'request binding is invalid');
1467
+ if (![binding.request_id, binding.nonce, binding.target, binding.method, binding.params_hash].every((value) => typeof value === 'string' && value.length > 0) || typeof binding.credential_cid !== 'string' || (!binding.credential_cid !== (method === 'credential.revocationList')))
1468
+ throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'request binding is invalid');
1469
+ 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)))
1470
+ throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'request binding does not match request');
1471
+ if (!isInt(binding.iat) || !isInt(binding.exp) || binding.exp <= binding.iat || !isInt(now) || now < binding.iat || now >= binding.exp)
1472
+ throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'request binding is outside its validity window');
1473
+ }
1474
+ function representationBindingInvalid(message) {
1475
+ return new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', message);
1476
+ }
1477
+ function validateRepresentationSignature(value) {
1478
+ if (!isRecord(value) || !['cert_fingerprint', 'timestamp', 'signature'].every((field) => field in value) || Object.keys(value).some((key) => !REPRESENTATION_SIGNATURE_FIELDS.has(key)))
1479
+ throw new ProtocolError('CREDENTIAL_SIGNATURE_INVALID', 'representation signature structure is invalid');
1480
+ if (typeof value.cert_fingerprint !== 'string' || !FINGERPRINT_RE.test(value.cert_fingerprint))
1481
+ throw new ProtocolError('CREDENTIAL_SIGNATURE_INVALID', 'representation certificate fingerprint is invalid');
1482
+ if (value.public_key_fingerprint !== undefined && (typeof value.public_key_fingerprint !== 'string' || !FINGERPRINT_RE.test(value.public_key_fingerprint)))
1483
+ throw new ProtocolError('CREDENTIAL_SIGNATURE_INVALID', 'representation public key fingerprint is invalid');
1484
+ if (!isInt(value.timestamp) || value.timestamp < 0)
1485
+ throw new ProtocolError('CREDENTIAL_SIGNATURE_INVALID', 'representation signature timestamp is invalid');
1486
+ if (typeof value.signature !== 'string' || !strictDerBase64(value.signature))
1487
+ throw new ProtocolError('CREDENTIAL_SIGNATURE_INVALID', 'representation signature encoding is invalid');
1488
+ }
1489
+ function validateRepresentationContext(proof, credential, request, now) {
1490
+ if (!isRecord(request))
1491
+ throw representationBindingInvalid('representation request is invalid');
1492
+ if (Object.keys(request).length !== 6 || Object.keys(request).some((key) => !['from', 'for', 'audience', 'resource', 'params', 'binding'].includes(key)))
1493
+ throw representationBindingInvalid('representation request is invalid');
1494
+ for (const field of ['from', 'for', 'audience', 'resource'])
1495
+ if (typeof request[field] !== 'string' || !request[field])
1496
+ throw representationBindingInvalid('representation request is invalid');
1497
+ for (const field of ['from', 'for', 'audience'])
1498
+ if (!AID_RE.test(request[field]))
1499
+ throw representationBindingInvalid(`representation request ${field} is invalid`);
1500
+ if (!isRecord(request.params) || !isRecord(request.binding))
1501
+ throw representationBindingInvalid('representation request is invalid');
1502
+ const binding = request.binding;
1503
+ if (typeof binding.method !== 'string' || !binding.method || typeof binding.target !== 'string' || !binding.target)
1504
+ throw representationBindingInvalid('representation request binding is invalid');
1505
+ validateRequestBinding(binding, binding.method, binding.target, request.params, String(credential.cid), String(binding.nonce ?? ''), String(binding.request_id ?? ''), now);
1506
+ if (binding.target !== request.audience || proof.issuer !== request.from)
1507
+ throw representationBindingInvalid('representation signer or service target does not match request');
1508
+ if (proof.from !== request.from || proof.for !== request.for || proof.audience !== request.audience || proof.resource !== request.resource)
1509
+ throw representationBindingInvalid('representation identity does not match request');
1510
+ if (proof.represented !== credential.sub || proof.credential_cid !== credential.cid)
1511
+ throw representationBindingInvalid('representation subject does not match credential');
1512
+ if (proof.method !== binding.method || proof.params_hash !== binding.params_hash || proof.nonce !== binding.nonce)
1513
+ throw representationBindingInvalid('representation operation does not match request');
1514
+ if (proof.iat !== binding.iat || proof.exp !== binding.exp)
1515
+ throw representationBindingInvalid('representation time window does not match request');
1516
+ }
1517
+ export function verifyRepresentationProof(proof, credential, request, resolveCertificate, now, proofChain) {
1518
+ if (!isRecord(proof) || Object.keys(proof).length !== REPRESENTATION_FIELDS.size || Object.keys(proof).some((key) => !REPRESENTATION_FIELDS.has(key)))
1519
+ throw representationBindingInvalid('representation proof is invalid');
1520
+ for (const field of ['issuer', 'represented', 'from', 'for', 'audience', 'credential_cid', 'method', 'resource', 'params_hash', 'nonce'])
1521
+ if (typeof proof[field] !== 'string' || !proof[field])
1522
+ throw representationBindingInvalid('representation proof is invalid');
1523
+ for (const field of ['issuer', 'represented', 'from', 'for', 'audience'])
1524
+ if (!AID_RE.test(proof[field]))
1525
+ throw representationBindingInvalid('representation proof AID is invalid');
1526
+ if (!DIGEST_RE.test(proof.params_hash))
1527
+ throw representationBindingInvalid('representation params_hash is invalid');
1528
+ if (!isInt(proof.iat) || !isInt(proof.exp) || proof.exp <= proof.iat || !isInt(now) || now < proof.iat || now >= proof.exp)
1529
+ throw representationBindingInvalid('representation time window is invalid');
1530
+ validateRepresentationSignature(proof.signature);
1531
+ if (!isRecord(credential))
1532
+ throw new ProtocolError('CREDENTIAL_INVALID', 'credential must be an object');
1533
+ validateCredential(credential, 0, proofChain);
1534
+ try {
1535
+ const certPem = certificatePem(resolveCertificate(proof.signature.cert_fingerprint));
1536
+ if (!certPem)
1537
+ throw new Error('certificate is missing');
1538
+ const cert = new X509Certificate(certPem);
1539
+ if (certFingerprint(certPem).toLowerCase() !== proof.signature.cert_fingerprint.toLowerCase())
1540
+ throw new Error('certificate fingerprint mismatch');
1541
+ const key = cert.publicKey;
1542
+ if (key.asymmetricKeyType !== 'ec' || !['prime256v1', 'P-256'].includes(key.asymmetricKeyDetails?.namedCurve ?? ''))
1543
+ throw new Error('certificate public key is not P-256');
1544
+ if (proof.signature.public_key_fingerprint !== undefined && publicKeyFingerprint(certPem).toLowerCase() !== String(proof.signature.public_key_fingerprint).toLowerCase())
1545
+ throw new Error('public key fingerprint mismatch');
1546
+ if (certCommonName(certPem).toLowerCase() !== String(proof.issuer).toLowerCase())
1547
+ throw new Error('certificate subject does not match issuer');
1548
+ const timestamp = proof.signature.timestamp;
1549
+ if (timestamp > now + 300 || timestamp < proof.iat || timestamp >= proof.exp)
1550
+ throw new Error('representation signature timestamp is outside its window');
1551
+ if (timestamp < Math.floor(new Date(cert.validFrom).getTime() / 1000) || timestamp > Math.floor(new Date(cert.validTo).getTime() / 1000) || now < Math.floor(new Date(cert.validFrom).getTime() / 1000) || now > Math.floor(new Date(cert.validTo).getTime() / 1000))
1552
+ throw new Error('certificate is not valid at signature or verification time');
1553
+ const payload = { ...proof };
1554
+ delete payload.signature;
1555
+ if (!verifySignatureWithCert(certPem, Buffer.from(proof.signature.signature, 'base64'), Buffer.from(canonicalJson(payload))))
1556
+ throw new Error('representation proof signature verification failed');
1557
+ }
1558
+ catch {
1559
+ throw new ProtocolError('CREDENTIAL_SIGNATURE_INVALID', 'representation proof signature verification failed');
1560
+ }
1561
+ validateRepresentationContext(proof, credential, request, now);
1562
+ }
1563
+ export function validateRequestNonce(nonce, usedNonces) {
1564
+ if (typeof nonce !== 'string' || nonce.length === 0 || usedNonces.includes(nonce))
1565
+ throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'request nonce has already been used');
1566
+ }
1567
+ export function validateIdempotency(existing, incoming) {
1568
+ if (existing.request_id === incoming.request_id && digest(existing) !== digest(incoming))
1569
+ throw new ProtocolError('CREDENTIAL_IDEMPOTENCY_KEY_REUSE', 'request id was reused with a different payload');
1570
+ }
1571
+ export function buildRpcRequest(id, method, params) {
1572
+ return { jsonrpc: '2.0', id, method, params };
1573
+ }
1574
+ export function buildRpcNotification(method, params) {
1575
+ return { jsonrpc: '2.0', method, params };
1576
+ }
1577
+ export function buildRpcResult(id, result) {
1578
+ return { jsonrpc: '2.0', id, result };
1579
+ }
1580
+ export function buildRpcError(id, code, message, errorCode) {
1581
+ return { jsonrpc: '2.0', id, error: { code, message, data: { error_code: errorCode } } };
1582
+ }
1583
+ export function parseJsonStrict(text) {
1584
+ try {
1585
+ let index = scanValue(text, 0);
1586
+ index = skipWhitespace(text, index);
1587
+ if (index !== text.length)
1588
+ throw new Error('trailing JSON data');
1589
+ const value = JSON.parse(text);
1590
+ normalizeNfc(value);
1591
+ return value;
1592
+ }
1593
+ catch (error) {
1594
+ if (error instanceof ProtocolError)
1595
+ throw error;
1596
+ throw invalid(error instanceof Error ? error.message : String(error));
1597
+ }
1598
+ }
1599
+ function skipWhitespace(text, index) {
1600
+ while (index < text.length && /\s/.test(text[index]))
1601
+ index++;
1602
+ return index;
1603
+ }
1604
+ function scanString(text, start) {
1605
+ if (text[start] !== '"')
1606
+ throw new Error('expected string');
1607
+ let escaped = false;
1608
+ for (let index = start + 1; index < text.length; index++) {
1609
+ const char = text[index];
1610
+ if (escaped) {
1611
+ escaped = false;
1612
+ continue;
1613
+ }
1614
+ if (char === '\\') {
1615
+ escaped = true;
1616
+ continue;
1617
+ }
1618
+ if (char === '"')
1619
+ return index + 1;
1620
+ }
1621
+ throw new Error('unterminated string');
1622
+ }
1623
+ function scanValue(text, start) {
1624
+ const index = skipWhitespace(text, start);
1625
+ const char = text[index];
1626
+ if (char === '"')
1627
+ return scanString(text, index);
1628
+ if (char === '{')
1629
+ return scanObject(text, index);
1630
+ if (char === '[')
1631
+ return scanArray(text, index);
1632
+ for (const literal of ['true', 'false', 'null'])
1633
+ if (text.startsWith(literal, index))
1634
+ return index + literal.length;
1635
+ if (char && '-0123456789'.includes(char)) {
1636
+ let end = index + 1;
1637
+ while (end < text.length && !/[\s,\]}]/.test(text[end]))
1638
+ end++;
1639
+ const token = text.slice(index, end);
1640
+ const match = /^-?(0|[1-9]\d*)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(token);
1641
+ if (match && Number.isInteger(Number(token))) {
1642
+ const digits = match[1] + (match[2] ?? '');
1643
+ const scale = (match[2]?.length ?? 0) - Number(match[3] ?? 0);
1644
+ if (scale > 0 && /[1-9]/.test(digits.slice(-Math.min(scale, digits.length))))
1645
+ throw invalid('non-integer JSON token rounds to an integer');
1646
+ }
1647
+ return end;
1648
+ }
1649
+ throw new Error('invalid JSON value');
1650
+ }
1651
+ function scanArray(text, start) {
1652
+ let index = skipWhitespace(text, start + 1);
1653
+ if (text[index] === ']')
1654
+ return index + 1;
1655
+ while (true) {
1656
+ index = scanValue(text, index);
1657
+ index = skipWhitespace(text, index);
1658
+ if (text[index] === ']')
1659
+ return index + 1;
1660
+ if (text[index] !== ',')
1661
+ throw new Error('expected comma');
1662
+ index = skipWhitespace(text, index + 1);
1663
+ }
1664
+ }
1665
+ function scanObject(text, start) {
1666
+ let index = skipWhitespace(text, start + 1);
1667
+ const keys = new Set();
1668
+ if (text[index] === '}')
1669
+ return index + 1;
1670
+ while (true) {
1671
+ const keyStart = index;
1672
+ const keyEnd = scanString(text, keyStart);
1673
+ const key = normalizeNfc(JSON.parse(text.slice(keyStart, keyEnd)));
1674
+ if (keys.has(key))
1675
+ throw invalid('duplicate object key');
1676
+ keys.add(key);
1677
+ index = skipWhitespace(text, keyEnd);
1678
+ if (text[index] !== ':')
1679
+ throw new Error('expected colon');
1680
+ index = scanValue(text, index + 1);
1681
+ index = skipWhitespace(text, index);
1682
+ if (text[index] === '}')
1683
+ return index + 1;
1684
+ if (text[index] !== ',')
1685
+ throw new Error('expected comma');
1686
+ index = skipWhitespace(text, index + 1);
1687
+ }
1688
+ }
1689
+ //# sourceMappingURL=org-protocol.js.map