@agentunion/fastaun 0.5.26 → 0.5.28

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 (94) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/_packed_docs/CHANGELOG.md +77 -0
  3. package/_packed_docs/INDEX.md +37 -14
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +23 -15
  5. 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 +16 -1
  6. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +37 -9
  7. package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +20 -9
  8. package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +57 -12
  9. package/_packed_docs/sdk/0.5.22-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +2 -2
  10. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +2 -2
  11. package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +187 -73
  12. package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +1 -1
  13. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +13 -9
  14. package/_packed_docs/sdk/INDEX.md +23 -13
  15. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +973 -266
  16. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +950 -242
  17. package/dist/agent-md-changes.d.ts +42 -0
  18. package/dist/agent-md-changes.js +339 -0
  19. package/dist/agent-md-changes.js.map +1 -0
  20. package/dist/agent-md-schema.d.ts +1 -0
  21. package/dist/agent-md-schema.js +31 -14
  22. package/dist/agent-md-schema.js.map +1 -1
  23. package/dist/agent-md.d.ts +11 -0
  24. package/dist/agent-md.js +91 -15
  25. package/dist/agent-md.js.map +1 -1
  26. package/dist/aid-store.d.ts +11 -0
  27. package/dist/aid-store.js +102 -21
  28. package/dist/aid-store.js.map +1 -1
  29. package/dist/aid.js +3 -3
  30. package/dist/aid.js.map +1 -1
  31. package/dist/auth.d.ts +10 -0
  32. package/dist/auth.js +65 -23
  33. package/dist/auth.js.map +1 -1
  34. package/dist/cert-utils.d.ts +1 -1
  35. package/dist/cert-utils.js +4 -4
  36. package/dist/cert-utils.js.map +1 -1
  37. package/dist/client/control-messages.d.ts +31 -0
  38. package/dist/client/control-messages.js +474 -0
  39. package/dist/client/control-messages.js.map +1 -0
  40. package/dist/client/delivery.js +10 -2
  41. package/dist/client/delivery.js.map +1 -1
  42. package/dist/client/lifecycle.d.ts +1 -1
  43. package/dist/client/lifecycle.js +10 -0
  44. package/dist/client/lifecycle.js.map +1 -1
  45. package/dist/client/rpc-pipeline.js +84 -14
  46. package/dist/client/rpc-pipeline.js.map +1 -1
  47. package/dist/client/v2-e2ee.d.ts +3 -0
  48. package/dist/client/v2-e2ee.js +12 -0
  49. package/dist/client/v2-e2ee.js.map +1 -1
  50. package/dist/client.d.ts +38 -0
  51. package/dist/client.js +572 -36
  52. package/dist/client.js.map +1 -1
  53. package/dist/discovery.d.ts +3 -0
  54. package/dist/discovery.js +52 -2
  55. package/dist/discovery.js.map +1 -1
  56. package/dist/events.js +14 -1
  57. package/dist/events.js.map +1 -1
  58. package/dist/facades.d.ts +38 -0
  59. package/dist/facades.js +49 -5
  60. package/dist/facades.js.map +1 -1
  61. package/dist/index.d.ts +4 -0
  62. package/dist/index.js +3 -0
  63. package/dist/index.js.map +1 -1
  64. package/dist/org-protocol.d.ts +1 -0
  65. package/dist/org-protocol.js +39 -36
  66. package/dist/org-protocol.js.map +1 -1
  67. package/dist/ownership-v21.js +7 -7
  68. package/dist/ownership-v21.js.map +1 -1
  69. package/dist/register-flow.js +2 -2
  70. package/dist/register-flow.js.map +1 -1
  71. package/dist/risk-control.d.ts +68 -0
  72. package/dist/risk-control.js +344 -0
  73. package/dist/risk-control.js.map +1 -0
  74. package/dist/semantic-facade.d.ts +176 -0
  75. package/dist/semantic-facade.js +611 -0
  76. package/dist/semantic-facade.js.map +1 -0
  77. package/dist/semantic-protocol.d.ts +810 -0
  78. package/dist/semantic-protocol.js +894 -0
  79. package/dist/semantic-protocol.js.map +1 -0
  80. package/dist/storage/lowlevel.d.ts +1 -0
  81. package/dist/storage/lowlevel.js +2 -2
  82. package/dist/storage/lowlevel.js.map +1 -1
  83. package/dist/tools/cross-sdk-agent.d.ts +11 -1
  84. package/dist/tools/cross-sdk-agent.js +388 -7
  85. package/dist/tools/cross-sdk-agent.js.map +1 -1
  86. package/dist/transport.d.ts +22 -1
  87. package/dist/transport.js +227 -22
  88. package/dist/transport.js.map +1 -1
  89. package/dist/v2/e2ee/decrypt.d.ts +5 -0
  90. package/dist/v2/e2ee/decrypt.js +22 -2
  91. package/dist/v2/e2ee/decrypt.js.map +1 -1
  92. package/dist/version.d.ts +1 -1
  93. package/dist/version.js +1 -1
  94. package/package.json +1 -1
@@ -0,0 +1,894 @@
1
+ import { canonicalJson as messageCanonical } from './v2/crypto/canonical.js';
2
+ import { decryptMessageWithEvidence } from './v2/e2ee/decrypt.js';
3
+ import { createHash, verify as verifySignature, X509Certificate } from 'node:crypto';
4
+ import { canonicalJson as legacyCanonicalJson, parseJsonStrict as legacyParseJsonStrict, validateCapability } from './org-protocol.js';
5
+ import { certCommonName, publicKeyFingerprint } from './cert-utils.js';
6
+ export const CLOCK_SKEW_SECONDS = 300;
7
+ export const SIGNATURE_PROFILE = 'aun-cap-sign/1';
8
+ export const SEMANTIC_VERSION = 'aun-cap/semantic-1';
9
+ export const SIGNATURE_DOMAINS = [
10
+ 'semantic-credential', 'registration-metadata', 'registration-receipt', 'credential-status',
11
+ 'usage-allocation', 'allocation-status', 'semantic-crl', 'usage-transition',
12
+ 'usage-checkpoint', 'usage-acceptance', 'ledger-abort',
13
+ ];
14
+ export class SemanticProtocolError extends Error {
15
+ code;
16
+ field;
17
+ retryable;
18
+ constructor(code, message, field) {
19
+ super(`${code}: ${message}`);
20
+ this.code = code;
21
+ this.field = field;
22
+ this.name = 'SemanticProtocolError';
23
+ this.retryable = code === 'SEMANTIC_STATE_UNAVAILABLE';
24
+ }
25
+ }
26
+ function invalid(message) { throw new SemanticProtocolError('SEMANTIC_FORMAT_INVALID', message); }
27
+ function object(value) {
28
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
29
+ invalid('expected JSON object');
30
+ }
31
+ function shape(value, fields) {
32
+ object(value);
33
+ const allowed = new Set(Object.keys(fields).map(key => key.replace(/\?$/, '')));
34
+ for (const key of Object.keys(value))
35
+ if (!allowed.has(key))
36
+ invalid(`unknown field: ${key}`);
37
+ for (const [declared, check] of Object.entries(fields)) {
38
+ const key = declared.replace(/\?$/, '');
39
+ if (!Object.hasOwn(value, key)) {
40
+ if (declared.endsWith('?'))
41
+ continue;
42
+ invalid(`missing field: ${key}`);
43
+ }
44
+ check(value[key]);
45
+ }
46
+ }
47
+ const string = value => { if (typeof value !== 'string' || value.length === 0)
48
+ invalid('expected nonempty string'); };
49
+ const text = value => { string(value); if (!value.trim())
50
+ invalid('expected nonempty text'); };
51
+ const integer = value => { if (!Number.isSafeInteger(value) || value < 0)
52
+ invalid('expected nonnegative safe integer'); };
53
+ const positive = value => { integer(value); if (value === 0)
54
+ invalid('expected positive safe integer'); };
55
+ const aid = value => { string(value); if (!/^[^\s/]+\.[^\s/]+$/.test(value))
56
+ invalid('invalid AID'); };
57
+ const hash = value => { if (typeof value !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(value))
58
+ invalid('invalid SHA-256 digest'); };
59
+ const boolean = value => { if (typeof value !== 'boolean')
60
+ invalid('expected boolean'); };
61
+ const decimal = value => { if (typeof value !== 'string' || !/^(0|[1-9]\d*)(\.\d*[1-9])?$/.test(value))
62
+ invalid('expected canonical unsigned decimal'); };
63
+ const jsonObject = value => { object(value); canonicalJson(value); };
64
+ const literal = (...values) => value => { if (!values.includes(value))
65
+ invalid('invalid literal value'); };
66
+ const nullable = (check) => value => { if (value !== null)
67
+ check(value); };
68
+ const array = (check, minimum = 0) => value => {
69
+ if (!Array.isArray(value) || value.length < minimum)
70
+ invalid('invalid array');
71
+ value.forEach(check);
72
+ };
73
+ function compareCodePoints(a, b) {
74
+ const aa = Array.from(a), bb = Array.from(b);
75
+ for (let i = 0; i < Math.min(aa.length, bb.length); i++) {
76
+ const diff = aa[i].codePointAt(0) - bb[i].codePointAt(0);
77
+ if (diff)
78
+ return diff;
79
+ }
80
+ return aa.length - bb.length;
81
+ }
82
+ function sorted(values, key = value => value) {
83
+ for (let i = 1; i < values.length; i++)
84
+ if (compareCodePoints(key(values[i - 1]), key(values[i])) >= 0)
85
+ invalid('collection must be sorted and unique');
86
+ }
87
+ const set = (check, minimum = 0) => value => { array(check, minimum)(value); sorted(value); };
88
+ const policy = value => shape(value, { authority: aid, id: string, version: string, hash });
89
+ const usageRef = value => shape(value, { epoch: positive, hash });
90
+ const revisionFields = { cid: string, original_hash: hash, metadata_hash: hash, metadata_version: string };
91
+ const revision = value => shape(value, revisionFields);
92
+ const audienceRevision = value => shape(value, { audience: aid, revision });
93
+ const withdrawn = value => shape(value, { audience: aid, state_seq: positive, last_revision: revision });
94
+ const revokedAudience = value => shape(value, { audience: aid, revision, state_seq: positive });
95
+ const audienceSet = (check, minimum = 0) => value => { array(check, minimum)(value); sorted(value, item => item.audience); };
96
+ const nonce = value => {
97
+ string(value);
98
+ if (!/^[A-Za-z0-9_-]+$/.test(value))
99
+ invalid('nonce must be unpadded base64url');
100
+ const bytes = Buffer.from(value, 'base64url');
101
+ if (bytes.length < 16 || bytes.toString('base64url') !== value)
102
+ invalid('nonce must contain at least 128 bits');
103
+ };
104
+ export function canonicalJson(value) {
105
+ try {
106
+ return legacyCanonicalJson(value);
107
+ }
108
+ catch (error) {
109
+ return invalid(error instanceof Error ? error.message : String(error));
110
+ }
111
+ }
112
+ export function parseJsonStrict(value) {
113
+ try {
114
+ const source = typeof value === 'string' ? value : new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(value);
115
+ return legacyParseJsonStrict(source);
116
+ }
117
+ catch (error) {
118
+ return invalid(error instanceof Error ? error.message : String(error));
119
+ }
120
+ }
121
+ export function digest(value) {
122
+ return `sha256:${createHash('sha256').update(canonicalJson(value)).digest('hex')}`;
123
+ }
124
+ export function bytesDigest(value) {
125
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
126
+ }
127
+ export function decodeBase64(value) {
128
+ if (typeof value !== 'string' || value.length % 4 !== 0 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value))
129
+ invalid('invalid padded Base64');
130
+ const bytes = Buffer.from(value, 'base64');
131
+ if (bytes.toString('base64') !== value)
132
+ invalid('noncanonical Base64');
133
+ return bytes;
134
+ }
135
+ export function decodeSignedJson(value) {
136
+ shape(value, { bytes_base64: item => { decodeBase64(item); } });
137
+ return parseJsonStrict(decodeBase64(value.bytes_base64));
138
+ }
139
+ export function encodeSignedJson(value) {
140
+ return { bytes_base64: Buffer.from(canonicalJson(value)).toString('base64') };
141
+ }
142
+ function signature(value) {
143
+ shape(value, { profile: literal(SIGNATURE_PROFILE), cert_fingerprint: hash, 'public_key_fingerprint?': hash, timestamp: integer, signature: string });
144
+ const bytes = decodeBase64(value.signature);
145
+ if (bytes.length < 8 || bytes.length > 72 || bytes[0] !== 0x30 || bytes[1] !== bytes.length - 2)
146
+ invalid('signature must be strict P-256 DER');
147
+ let offset = 2;
148
+ const order = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
149
+ for (let index = 0; index < 2; index++) {
150
+ if (bytes[offset++] !== 2)
151
+ invalid('signature must contain two DER integers');
152
+ const length = bytes[offset++];
153
+ if (!length || length > 33 || offset + length > bytes.length)
154
+ invalid('invalid DER integer length');
155
+ const part = bytes.slice(offset, offset + length);
156
+ if (part[0] & 128 || part[0] === 0 && (length === 1 || part[1] < 128))
157
+ invalid('nonminimal DER integer');
158
+ const number = BigInt('0x' + Buffer.from(part).toString('hex'));
159
+ if (number <= 0n || number >= order)
160
+ invalid('signature scalar outside P-256 range');
161
+ offset += length;
162
+ }
163
+ if (offset !== bytes.length)
164
+ invalid('trailing DER signature bytes');
165
+ }
166
+ function signatureOf(domain, value) {
167
+ return domain === 'registration-metadata' ? value.confirmation.signature : value.signature;
168
+ }
169
+ function checkDomain(domain) {
170
+ if (!SIGNATURE_DOMAINS.includes(domain))
171
+ invalid('unsupported signature domain');
172
+ }
173
+ export function signingPayload(domain, value) {
174
+ checkDomain(domain);
175
+ canonicalJson(value);
176
+ const unsigned = parseJsonStrict(canonicalJson(value));
177
+ object(unsigned);
178
+ if (domain === 'registration-metadata')
179
+ object(unsigned.confirmation);
180
+ const block = signatureOf(domain, unsigned);
181
+ object(block);
182
+ delete block.signature;
183
+ return canonicalJson([SIGNATURE_PROFILE, domain, unsigned]);
184
+ }
185
+ export function validateWindow(nbf, exp, now) {
186
+ integer(nbf);
187
+ integer(now);
188
+ if (exp !== null) {
189
+ integer(exp);
190
+ if (exp <= nbf)
191
+ invalid('invalid original validity interval');
192
+ }
193
+ if (nbf - now > CLOCK_SKEW_SECONDS || exp !== null && now - exp > CLOCK_SKEW_SECONDS)
194
+ invalid('outside validity window');
195
+ }
196
+ function proofTime(value, maximum, now) {
197
+ if (value.valid_until <= value.as_of || value.valid_until - value.as_of > maximum)
198
+ invalid('invalid proof declaration interval');
199
+ if (value.signature.timestamp !== value.as_of)
200
+ invalid('proof signature timestamp must equal as_of');
201
+ if (now !== undefined)
202
+ validateWindow(value.as_of, value.valid_until, now);
203
+ }
204
+ function eventTime(value) {
205
+ if (value.signature.timestamp !== value.as_of)
206
+ invalid('signature timestamp must equal as_of');
207
+ }
208
+ export function validateAuthorizationRequestTime(value, now) {
209
+ integer(value.expires_at);
210
+ integer(now);
211
+ if (value.expires_at - now > 600 || now - value.expires_at > 300)
212
+ invalid('outside authorization request validity window');
213
+ }
214
+ export function validateEvidenceRef(value) {
215
+ shape(value, { profile: string, hash, location: location => {
216
+ object(location);
217
+ if (location.kind === 'inline')
218
+ shape(location, { kind: literal('inline'), data_base64: item => { decodeBase64(item); } });
219
+ else
220
+ shape(location, { kind: literal('aun_ref'), ref: string });
221
+ } });
222
+ }
223
+ function evidenceProfile(profile) {
224
+ literal('aun-e2ee-v2-envelope-raw/1', 'aun-cap-usage-allocation/1', 'aun-evidence-bytes/1')(profile);
225
+ }
226
+ function evidenceBytes(value) {
227
+ return ArrayBuffer.isView(value) && Object.prototype.toString.call(value) === '[object Uint8Array]';
228
+ }
229
+ function evidenceLocator(value) {
230
+ if (!value.startsWith('aun-ref:1:'))
231
+ invalid('unsupported AUN evidence locator');
232
+ const encoded = value.slice('aun-ref:1:'.length);
233
+ if (!/^[A-Za-z0-9_-]+$/.test(encoded))
234
+ invalid('invalid evidence locator Base64url');
235
+ const bytes = decodeBase64(encoded.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - encoded.length % 4) % 4));
236
+ if (Buffer.from(bytes).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '') !== encoded)
237
+ invalid('noncanonical evidence locator Base64url');
238
+ const locator = parseJsonStrict(bytes);
239
+ shape(locator, { resolver: literal('aun-evidence-archive/1'), service_aid: aid, object_id: string, object_version: string });
240
+ const canonical = canonicalJson(locator);
241
+ if (canonical.length !== bytes.length || canonical.some((byte, index) => byte !== bytes[index]))
242
+ invalid('noncanonical evidence locator JSON');
243
+ return locator;
244
+ }
245
+ export async function createEvidenceRef(bytes, profile, locator) {
246
+ if (!evidenceBytes(bytes))
247
+ invalid('evidence requires original bytes');
248
+ evidenceProfile(profile);
249
+ const ref = { profile, hash: bytesDigest(bytes), location: locator
250
+ ? { kind: 'aun_ref', ref: 'aun-ref:1:' + Buffer.from(canonicalJson(locator)).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '') }
251
+ : { kind: 'inline', data_base64: Buffer.from(bytes).toString('base64') } };
252
+ validateEvidenceRef(ref);
253
+ if (ref.location.kind === 'aun_ref')
254
+ evidenceLocator(ref.location.ref);
255
+ return ref;
256
+ }
257
+ export async function resolveEvidence(ref, resolvers = {}) {
258
+ validateEvidenceRef(ref);
259
+ evidenceProfile(ref.profile);
260
+ let bytes;
261
+ if (ref.location.kind === 'inline')
262
+ bytes = decodeBase64(ref.location.data_base64);
263
+ else {
264
+ const locator = evidenceLocator(ref.location.ref);
265
+ const resolver = Object.hasOwn(resolvers, locator.service_aid) ? resolvers[locator.service_aid] : undefined;
266
+ if (typeof resolver !== 'function')
267
+ throw new SemanticProtocolError('SEMANTIC_STATE_UNAVAILABLE', 'trusted evidence archive resolver is unavailable');
268
+ bytes = await resolver(locator);
269
+ }
270
+ if (!evidenceBytes(bytes) || bytesDigest(bytes) !== ref.hash)
271
+ throw new SemanticProtocolError('SEMANTIC_BINDING_INVALID', 'resolved evidence bytes differ from signed hash');
272
+ return new Uint8Array(bytes);
273
+ }
274
+ export async function archiveMessage(envelope, received) {
275
+ const bytes = evidenceBytes(envelope) ? new Uint8Array(envelope) : new TextEncoder().encode(typeof envelope === 'string' ? envelope : JSON.stringify(envelope));
276
+ const parsed = parseJsonStrict(bytes);
277
+ object(parsed);
278
+ if (parsed.version !== 'v2' || !['e2ee.p2p_encrypted', 'e2ee.group_encrypted'].includes(parsed.type))
279
+ invalid('archive requires a V2 encrypted envelope');
280
+ shape(received, { received_by: aid, received_at: integer, source: string });
281
+ return { envelope: await createEvidenceRef(bytes, 'aun-e2ee-v2-envelope-raw/1'), envelope_bytes: bytes, ...received };
282
+ }
283
+ async function verifiedMessage(ref, context) {
284
+ messageBody(ref);
285
+ if (ref.envelope.profile !== 'aun-e2ee-v2-envelope-raw/1')
286
+ invalid('unsupported message evidence profile');
287
+ const envelope = parseJsonStrict(await resolveEvidence(ref.envelope, context.resolvers));
288
+ object(envelope);
289
+ object(envelope.aad);
290
+ const aad = envelope.aad;
291
+ if (envelope.version !== 'v2' || envelope.suite !== 'P256_HKDF_SHA256_AES_256_GCM' || aad.suite !== envelope.suite)
292
+ invalid('invalid V2 message suite');
293
+ integer(aad.timestamp);
294
+ string(aad.from_device);
295
+ string(aad.wrap_protocol);
296
+ if (aad.from !== ref.sender || aad.message_id !== ref.message_id)
297
+ throw new SemanticProtocolError('SEMANTIC_BINDING_INVALID', 'protected message identity differs from signed reference');
298
+ if (ref.conversation.kind === 'p2p') {
299
+ if (envelope.type !== 'e2ee.p2p_encrypted' || aad.to !== ref.conversation.to || context.selfAid !== aad.to)
300
+ throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'P2P reference or original recipient differs');
301
+ }
302
+ else if (envelope.type !== 'e2ee.group_encrypted' || aad.group_id !== ref.conversation.group_id || aad.epoch !== ref.conversation.epoch
303
+ || digest(aad.state_commitment) !== digest(ref.conversation.state_commitment))
304
+ throw new SemanticProtocolError('SEMANTIC_BINDING_INVALID', 'group epoch or state commitment differs');
305
+ if (!Array.isArray(envelope.recipients) && (!envelope.recipient || envelope.merkle_proof !== undefined && !Array.isArray(envelope.merkle_proof)))
306
+ throw new SemanticProtocolError('SEMANTIC_STATE_UNAVAILABLE', 'original recipient collection or Merkle proof is required');
307
+ if (typeof envelope.recipients_digest !== 'string' || !/^[0-9a-f]{64}$/.test(envelope.recipients_digest))
308
+ invalid('invalid original recipient digest');
309
+ const ciphertext = decodeBase64(envelope.ciphertext), tag = decodeBase64(envelope.tag), nonce = decodeBase64(envelope.nonce), signature = decodeBase64(envelope.sender_signature);
310
+ decodeBase64(envelope.sender_session_pk);
311
+ string(envelope.sender_cert_fingerprint);
312
+ if (tag.length !== 16 || nonce.length !== 12 || signature.length !== 64)
313
+ invalid('invalid V2 encryption or RAW signature lengths');
314
+ const aadBytes = messageCanonical(aad), root = new Uint8Array(envelope.recipients_digest.match(/../g).map((value) => parseInt(value, 16)));
315
+ const core = new Uint8Array(ciphertext.length + tag.length + aadBytes.length + root.length);
316
+ let offset = 0;
317
+ for (const part of [ciphertext, tag, aadBytes, root]) {
318
+ core.set(part, offset);
319
+ offset += part.length;
320
+ }
321
+ if (bytesDigest(core) !== ref.signed_core_hash)
322
+ throw new SemanticProtocolError('SEMANTIC_BINDING_INVALID', 'message signed core differs from reference');
323
+ if (typeof context.resolveSenderKey !== 'function' || typeof context.verifySenderIdentity !== 'function')
324
+ throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'trusted historical sender identity adapter is required');
325
+ const signedAt = Math.trunc(aad.timestamp / 1000), fingerprint = envelope.sender_cert_fingerprint;
326
+ const senderKey = await context.resolveSenderKey(ref.sender, aad.from_device, fingerprint, signedAt);
327
+ await context.verifySenderIdentity(ref.sender, aad.from_device, fingerprint, signedAt, senderKey);
328
+ const keys = context.resolveDecryptionKeys ? await context.resolveDecryptionKeys(envelope) : { ikPriv: context.selfIkPrivate, spkPriv: context.selfSpkPrivate };
329
+ if (!evidenceBytes(keys.ikPriv))
330
+ throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'original receiver decryption keys are required');
331
+ const decrypted = decryptMessageWithEvidence(envelope, context.selfAid, context.selfDeviceId, keys.ikPriv, keys.spkPriv, senderKey);
332
+ if (!decrypted)
333
+ throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'receiver has no valid original recipient row');
334
+ const payload = parseJsonStrict(decrypted.payloadBytes);
335
+ object(payload);
336
+ if (!['text', 'quote'].includes(payload.type))
337
+ invalid('semantic message requires original text or quote payload');
338
+ text(payload.text);
339
+ return { text: payload.text, payload_hash: bytesDigest(decrypted.payloadBytes) };
340
+ }
341
+ async function verifyEvidenceProfile(ref, context) {
342
+ const bytes = await resolveEvidence(ref, context.resolvers);
343
+ if (ref.profile === 'aun-e2ee-v2-envelope-raw/1') {
344
+ const envelope = parseJsonStrict(bytes);
345
+ object(envelope);
346
+ object(envelope.aad);
347
+ const aad = envelope.aad;
348
+ const ciphertext = decodeBase64(envelope.ciphertext), tag = decodeBase64(envelope.tag), aadBytes = messageCanonical(aad);
349
+ if (typeof envelope.recipients_digest !== 'string' || !/^[0-9a-f]{64}$/.test(envelope.recipients_digest))
350
+ invalid('invalid original recipient digest');
351
+ const root = new Uint8Array(envelope.recipients_digest.match(/../g).map((value) => parseInt(value, 16)));
352
+ const core = new Uint8Array(ciphertext.length + tag.length + aadBytes.length + root.length);
353
+ let offset = 0;
354
+ for (const part of [ciphertext, tag, aadBytes, root]) {
355
+ core.set(part, offset);
356
+ offset += part.length;
357
+ }
358
+ await verifiedMessage({ sender: aad.from, message_id: aad.message_id, signed_core_hash: bytesDigest(core),
359
+ conversation: envelope.type === 'e2ee.p2p_encrypted' ? { kind: 'p2p', to: aad.to }
360
+ : { kind: 'group', group_id: aad.group_id, epoch: aad.epoch, state_commitment: aad.state_commitment }, envelope: ref }, context);
361
+ }
362
+ else if (ref.profile === 'aun-cap-usage-allocation/1') {
363
+ const allocation = parseJsonStrict(bytes), canonical = canonicalJson(allocation);
364
+ if (canonical.length !== bytes.length || canonical.some((byte, index) => byte !== bytes[index]))
365
+ invalid('allocation evidence must retain canonical signed bytes');
366
+ await verifyRecord('usage-allocation', allocation, { ...context, expectedAid: undefined });
367
+ }
368
+ }
369
+ export async function verifyMessageBody(value, context) {
370
+ object(value);
371
+ const credential = (Object.hasOwn(value, 'bytes_base64') ? decodeSignedJson(value) : value);
372
+ if (typeof context.resolveCertificate !== 'function' || typeof context.verifyCertificateTrust !== 'function')
373
+ throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'explicit certificate resolver and trust verifier are required');
374
+ await verifyRecord('semantic-credential', credential, context);
375
+ const evidence = new Set();
376
+ for (const ref of credential.authorization.evidence ?? []) {
377
+ await verifyEvidenceProfile(ref, context);
378
+ evidence.add(ref.hash);
379
+ }
380
+ if (credential.authorization.kind === 'text')
381
+ return { credential_hash: digest(credential), body_hash: bytesDigest(new TextEncoder().encode(credential.authorization.text)), text: credential.authorization.text, evidence_hashes: [...evidence].sort() };
382
+ const ref = credential.authorization.message, verified = await verifiedMessage(ref, context);
383
+ evidence.add(ref.envelope.hash);
384
+ for (const item of ref.context ?? []) {
385
+ if (item.profile !== 'aun-e2ee-v2-envelope-raw/1')
386
+ invalid('necessary context requires message evidence profile');
387
+ await verifyEvidenceProfile(item, context);
388
+ evidence.add(item.hash);
389
+ }
390
+ for (const item of ref.attachments ?? []) {
391
+ if (item.profile !== 'aun-evidence-bytes/1')
392
+ invalid('attachment requires original bytes profile');
393
+ await resolveEvidence(item, context.resolvers);
394
+ evidence.add(item.hash);
395
+ }
396
+ return { credential_hash: digest(credential), body_hash: bytesDigest(new TextEncoder().encode(verified.text)), payload_hash: verified.payload_hash, text: verified.text, evidence_hashes: [...evidence].sort() };
397
+ }
398
+ function messageBody(value) {
399
+ shape(value, {
400
+ message_id: string, sender: aid, signed_core_hash: hash,
401
+ conversation: conversation => {
402
+ object(conversation);
403
+ if (conversation.kind === 'p2p')
404
+ shape(conversation, { kind: literal('p2p'), to: aid });
405
+ else
406
+ shape(conversation, { kind: literal('group'), group_id: string, epoch: integer, state_commitment: jsonObject });
407
+ }, envelope: validateEvidenceRef, 'context?': array(validateEvidenceRef), 'attachments?': array(validateEvidenceRef),
408
+ });
409
+ }
410
+ function capability(value) {
411
+ shape(value, { ns: string, action: string, resource: string, constraints: array(item => shape(item, {
412
+ type: string, field: string, value: item => { canonicalJson(item); }, 'operator?': string, 'unit?': string,
413
+ })) });
414
+ try {
415
+ validateCapability(value);
416
+ }
417
+ catch (error) {
418
+ invalid(error instanceof Error ? error.message : String(error));
419
+ }
420
+ }
421
+ export function validateSemanticCredential(value, now) {
422
+ canonicalJson(value);
423
+ shape(value, {
424
+ ver: literal(SEMANTIC_VERSION), cid: string, cap: array(capability), authorization: basis => {
425
+ object(basis);
426
+ if (basis.kind === 'text')
427
+ shape(basis, { kind: literal('text'), text, 'evidence?': array(validateEvidenceRef) });
428
+ else
429
+ shape(basis, { kind: literal('message_ref'), message: messageBody, 'evidence?': array(validateEvidenceRef) });
430
+ }, signature, 'iss?': aid, 'sub?': aid, 'aud?': set(aid, 1), 'iat?': integer, 'nbf?': integer,
431
+ 'exp?': integer, 'key_version?': string, 'delegate?': boolean, 'parent?': string,
432
+ 'delegation_depth?': item => { integer(item); if (item > 8)
433
+ invalid('delegation depth exceeds 8'); },
434
+ 'proof_chain?': array(jsonObject), 'security_level?': literal('low', 'medium', 'high'),
435
+ 'revocation_endpoint?': item => { canonicalJson(item); }, 'credential_service?': jsonObject, 'ext?': jsonObject,
436
+ });
437
+ const item = value;
438
+ if (item.nbf !== undefined && item.exp !== undefined && item.nbf >= item.exp)
439
+ invalid('invalid credential interval');
440
+ if (item.iat !== undefined && item.exp !== undefined && item.iat >= item.exp)
441
+ invalid('invalid credential issuance interval');
442
+ if (now !== undefined) {
443
+ if (item.iat !== undefined && item.iat - now > 300)
444
+ invalid('credential iat is in the future');
445
+ validateWindow(item.nbf ?? 0, item.exp ?? null, now);
446
+ }
447
+ }
448
+ export function validateUsageContract(value) {
449
+ shape(value, {
450
+ grant_key: hash, epoch: positive, audiences: set(aid, 1), ledger: ledger => {
451
+ object(ledger);
452
+ if (ledger.scope === 'global')
453
+ shape(ledger, { scope: literal('global'), authority: aid });
454
+ else
455
+ shape(ledger, { scope: literal('per_aud'), authorities: authorities => { object(authorities); for (const [key, value] of Object.entries(authorities)) {
456
+ aid(key);
457
+ aid(value);
458
+ } } });
459
+ }, limits: array(limit => shape(limit, { id: string, metric: string, unit: string, limit: decimal }), 1), 'allocations?': array(validateEvidenceRef),
460
+ });
461
+ const item = value;
462
+ if (new Set(item.limits.map(limit => limit.id)).size !== item.limits.length)
463
+ invalid('duplicate usage limit id');
464
+ if (item.ledger.scope === 'per_aud' && digest(Object.keys(item.ledger.authorities).sort(compareCodePoints)) !== digest(item.audiences))
465
+ invalid('ledger authorities must cover exactly audiences');
466
+ }
467
+ export function validateRegistrationMetadata(value, now) {
468
+ canonicalJson(value);
469
+ shape(value, {
470
+ original_hash: hash, version: string, confirmed_at: integer, iss: aid, sub: aid, aud: set(aid, 1),
471
+ nbf: integer, exp: nullable(integer), use_limit: nullable(integer), 'usage?': validateUsageContract,
472
+ delegate: boolean, 'parent?': string, 'delegation_depth?': item => { integer(item); if (item > 8)
473
+ invalid('delegation depth exceeds 8'); },
474
+ 'proof_chain?': array(jsonObject), policy,
475
+ confirmation: confirmation => shape(confirmation, { authority: aid, authorization: policy, audiences: set(aid, 1), signature }),
476
+ });
477
+ const item = value;
478
+ if (item.exp !== null && item.nbf >= item.exp)
479
+ invalid('invalid metadata interval');
480
+ if (item.confirmation.signature.timestamp !== item.confirmed_at)
481
+ invalid('metadata signature timestamp must equal confirmed_at');
482
+ if (item.aud.some(audience => !item.confirmation.audiences.includes(audience)))
483
+ invalid('confirmation does not cover metadata audiences');
484
+ if (item.use_limit !== null && !item.usage)
485
+ invalid('finite usage requires usage contract');
486
+ if (item.usage) {
487
+ if (item.aud.some(audience => !item.usage.audiences.includes(audience)))
488
+ invalid('usage does not cover metadata audiences');
489
+ if (item.use_limit !== null && !item.usage.limits.some(limit => limit.metric === 'count' && limit.limit === String(item.use_limit)))
490
+ invalid('count limit differs from use_limit');
491
+ }
492
+ if (now !== undefined) {
493
+ validateWindow(item.nbf, item.exp, now);
494
+ if (item.confirmed_at - now > 300)
495
+ invalid('confirmation is in the future');
496
+ }
497
+ }
498
+ const transitionFields = {
499
+ service_aid: aid, cid: string, original_hash: hash, transition_id: string, fence: positive,
500
+ candidate_hash: hash, from_usage: usageRef, to_usage: usageRef,
501
+ };
502
+ function transitionEpoch(value) {
503
+ if (value.to_usage.epoch - value.from_usage.epoch !== 1)
504
+ invalid('transition epoch must increment by one');
505
+ }
506
+ const account = value => shape(value, { grant_key: hash, original_hash: hash, limit_id: string, scope_key: string, metric: string, unit: string, used: decimal });
507
+ function accounts(value) {
508
+ array(account, 1)(value);
509
+ const keys = ['grant_key', 'original_hash', 'limit_id', 'scope_key'];
510
+ for (let i = 1; i < value.length; i++) {
511
+ let comparison = 0;
512
+ for (const key of keys) {
513
+ comparison = compareCodePoints(value[i - 1][key], value[i][key]);
514
+ if (comparison)
515
+ break;
516
+ }
517
+ if (comparison >= 0)
518
+ invalid('accounts must be sorted by complete account key and unique');
519
+ }
520
+ }
521
+ function validateTransition(value) {
522
+ object(value);
523
+ const common = { ...transitionFields, expected: audienceSet(audienceRevision, 1), expected_absent: set(aid), expected_withdrawn: audienceSet(withdrawn), withdraw: set(aid), candidate: audienceSet(audienceRevision, 1), prepared_at: integer, as_of: integer, signature };
524
+ if (value.state === 'prepared')
525
+ shape(value, { ...common, state: literal('prepared') });
526
+ else if (value.state === 'committed') {
527
+ shape(value, { ...common, state: literal('committed'), terminal_at: integer, published: audienceSet(audienceRevision, 1), checkpoint_hashes: set(hash, 1), acceptance_hashes: set(hash, 1) });
528
+ if (digest(value.published) !== digest(value.candidate))
529
+ invalid('published revisions must equal candidates');
530
+ }
531
+ else {
532
+ shape(value, { ...common, state: literal('aborted'), terminal_at: integer, recovery: recovery => {
533
+ shape(recovery, { eligible: boolean, reason: literal('cancelled', 'credential_revoked') });
534
+ if (recovery.eligible !== (recovery.reason === 'cancelled'))
535
+ invalid('inconsistent recovery eligibility');
536
+ } });
537
+ }
538
+ transitionEpoch(value);
539
+ eventTime(value);
540
+ if (value.prepared_at > value.as_of || value.state !== 'prepared' && value.terminal_at !== value.as_of)
541
+ invalid('inconsistent transition event time');
542
+ for (const item of [...value.expected, ...value.candidate, ...value.expected_withdrawn.map((entry) => ({ revision: entry.last_revision }))]) {
543
+ if (item.revision.cid !== value.cid || item.revision.original_hash !== value.original_hash)
544
+ invalid('transition revision differs from original');
545
+ }
546
+ const previous = new Set(value.expected.map((entry) => entry.audience));
547
+ const absent = value.expected_absent, restored = value.expected_withdrawn.map((entry) => entry.audience);
548
+ if (new Set([...previous, ...absent, ...restored]).size !== previous.size + absent.length + restored.length)
549
+ invalid('transition audience preconditions overlap');
550
+ if (value.withdraw.some((audience) => !previous.has(audience)))
551
+ invalid('withdraw must refer to current audience');
552
+ const expectedCandidate = [...previous, ...absent, ...restored].filter(audience => !value.withdraw.includes(audience)).sort(compareCodePoints);
553
+ if (digest(value.candidate.map((item) => item.audience)) !== digest(expectedCandidate))
554
+ invalid('candidate audiences do not match transition change');
555
+ }
556
+ function validateCrl(value, now) {
557
+ shape(value, {
558
+ service_aid: aid, scope: scope => shape(scope, { audiences: set(aid, 1) }),
559
+ query: query => shape(query, { params_hash: hash, nonce }), after_event_seq: integer, upper_event_seq: integer,
560
+ cursor: nullable(string), events: array(event => shape(event, {
561
+ event_seq: positive, cid: string, original_hash: hash, affected: audienceSet(revokedAudience, 1), revoked_at: integer,
562
+ actor: aid, authorization: policy, authority_evidence_hashes: set(hash), reason_code: string, 'reason?': string,
563
+ })), next_cursor: nullable(string), as_of: integer, valid_until: integer, signature,
564
+ });
565
+ proofTime(value, 300, now);
566
+ if (value.upper_event_seq < value.after_event_seq)
567
+ invalid('invalid CRL snapshot range');
568
+ let last = value.after_event_seq;
569
+ for (const event of value.events) {
570
+ if (event.event_seq <= last || event.event_seq > value.upper_event_seq)
571
+ invalid('event outside CRL snapshot sequence');
572
+ last = event.event_seq;
573
+ for (const affected of event.affected)
574
+ if (!value.scope.audiences.includes(affected.audience) || affected.revision.cid !== event.cid || affected.revision.original_hash !== event.original_hash)
575
+ invalid('CRL affected binding differs from event or scope');
576
+ }
577
+ }
578
+ export function validateSignedRecord(domain, value, now) {
579
+ checkDomain(domain);
580
+ canonicalJson(value);
581
+ object(value);
582
+ if (now !== undefined) {
583
+ integer(now);
584
+ const block = domain === 'registration-metadata' ? value.confirmation?.signature : value.signature;
585
+ if (typeof block?.timestamp === 'number' && block.timestamp - now > CLOCK_SKEW_SECONDS)
586
+ invalid('signature timestamp is in the future');
587
+ }
588
+ switch (domain) {
589
+ case 'semantic-credential':
590
+ validateSemanticCredential(value, now);
591
+ return;
592
+ case 'registration-metadata':
593
+ validateRegistrationMetadata(value, now);
594
+ return;
595
+ case 'registration-receipt':
596
+ shape(value, { ...revisionFields, audiences: set(aid, 1), service_aid: aid, registered_at: integer, 'usage?': usageRef, signature });
597
+ if (value.signature.timestamp !== value.registered_at)
598
+ invalid('receipt signature timestamp must equal registered_at');
599
+ if (now !== undefined && value.registered_at - now > 300)
600
+ invalid('receipt is in the future');
601
+ return;
602
+ case 'credential-status':
603
+ shape(value, { ...revisionFields, audience: aid, status: literal('active', 'revoked', 'expired'), state_seq: positive, 'usage?': usageRef, 'query?': query => shape(query, { request_hash: hash, nonce }), as_of: integer, valid_until: integer, signature });
604
+ proofTime(value, value.query ? 30 : 300, now);
605
+ return;
606
+ case 'usage-allocation':
607
+ shape(value, { allocation_id: string, grant_key: hash, parent_original_hash: hash, child_original_hash: hash, parent_epoch: positive, child_epoch: positive, parent_authority: aid, child_authority: aid, audiences: set(aid, 1), budgets: array(budget => shape(budget, { parent_limit_id: string, child_limit_id: string, metric: string, unit: string, amount: decimal }), 1), nbf: integer, exp: integer, signature });
608
+ if (value.exp <= value.nbf)
609
+ invalid('invalid allocation interval');
610
+ if (new Set(value.budgets.map((item) => `${item.parent_limit_id}\0${item.child_limit_id}`)).size !== value.budgets.length)
611
+ invalid('duplicate allocation budget');
612
+ if (now !== undefined)
613
+ validateWindow(value.nbf, value.exp, now);
614
+ return;
615
+ case 'allocation-status':
616
+ shape(value, { allocation_id: string, allocation_hash: hash, authority: aid, state: literal('active', 'draining', 'closed'), state_seq: positive, query: query => shape(query, { request_hash: hash, nonce }), as_of: integer, valid_until: integer, signature });
617
+ proofTime(value, 30, now);
618
+ return;
619
+ case 'semantic-crl':
620
+ validateCrl(value, now);
621
+ return;
622
+ case 'usage-transition':
623
+ validateTransition(value);
624
+ return;
625
+ case 'usage-checkpoint':
626
+ shape(value, { ...transitionFields, authority: aid, audiences: set(aid, 1), accounts, in_flight: literal(0), unknown: literal(0), reserved: literal('0'), closed_allocations: set(hash), as_of: integer, signature });
627
+ transitionEpoch(value);
628
+ eventTime(value);
629
+ return;
630
+ case 'usage-acceptance':
631
+ shape(value, { ...transitionFields, authority: aid, audiences: set(aid, 1), checkpoint_hashes: set(hash, 1), accounts, usage: validateUsageContract, as_of: integer, signature });
632
+ transitionEpoch(value);
633
+ eventTime(value);
634
+ if (value.usage.epoch !== value.to_usage.epoch || digest(value.usage) !== value.to_usage.hash)
635
+ invalid('accepted usage does not match target UsageRef');
636
+ if (value.audiences.some((audience) => !value.usage.audiences.includes(audience)))
637
+ invalid('acceptance audience outside new usage contract');
638
+ for (const audience of value.audiences)
639
+ if ((value.usage.ledger.scope === 'global' ? value.usage.ledger.authority : value.usage.ledger.authorities[audience]) !== value.authority)
640
+ invalid('acceptance authority differs from new ledger authority');
641
+ if (value.usage.audiences.some((audience) => (value.usage.ledger.scope === 'global' ? value.usage.ledger.authority : value.usage.ledger.authorities[audience]) === value.authority && !value.audiences.includes(audience)))
642
+ invalid('acceptance must cover all audiences assigned to authority');
643
+ return;
644
+ case 'ledger-abort':
645
+ shape(value, { ...transitionFields, authority: aid, audiences: set(aid, 1), aborted_receipt_hash: hash, usage: usageRef, result: literal('resumed', 'frozen'), as_of: integer, signature });
646
+ transitionEpoch(value);
647
+ eventTime(value);
648
+ if (digest(value.usage) !== digest(value.from_usage))
649
+ invalid('abort ack must bind original UsageRef');
650
+ }
651
+ }
652
+ export function validateUsageRecord(domain, value, now) {
653
+ if (!['usage-allocation', 'allocation-status', 'usage-transition', 'usage-checkpoint', 'usage-acceptance', 'ledger-abort'].includes(domain))
654
+ invalid('expected usage signature domain');
655
+ validateSignedRecord(domain, value, now);
656
+ }
657
+ export function validateStatusProof(value, policy, context) {
658
+ validateSignedRecord('credential-status', value, context.now);
659
+ const proof = value;
660
+ object(policy);
661
+ if (!Number.isFinite(context.elapsedSeconds) || context.elapsedSeconds < 0)
662
+ invalid('invalid monotonic elapsed time');
663
+ let maximum;
664
+ if (policy.mode === 'online') {
665
+ shape(policy, { mode: literal('online') });
666
+ maximum = 30;
667
+ if (!context.query || !proof.query || digest(context.query) !== digest(proof.query))
668
+ throw new SemanticProtocolError('SEMANTIC_BINDING_INVALID', 'status query does not match request');
669
+ }
670
+ else {
671
+ shape(policy, { mode: literal('bounded'), max_staleness_seconds: positive });
672
+ maximum = policy.max_staleness_seconds;
673
+ if (maximum > 300 || proof.query !== undefined)
674
+ invalid('invalid bounded status policy or query');
675
+ }
676
+ if (proof.valid_until - proof.as_of > maximum || context.elapsedSeconds >= maximum)
677
+ invalid('status declaration or monotonic lifetime exceeded');
678
+ }
679
+ export const MANAGEMENT_CONTEXT_FIELDS = ['_auth', '_caller_id'];
680
+ export function projectManagementParams(params) {
681
+ object(params);
682
+ canonicalJson(params);
683
+ return Object.fromEntries(Object.entries(params).filter(([key]) => key !== 'request_binding' && !MANAGEMENT_CONTEXT_FIELDS.includes(key)));
684
+ }
685
+ export function validateManagementBinding(value, now, context) {
686
+ shape(value, {
687
+ request_id: string, nonce, target: aid,
688
+ method: literal('credential.semantic.register', 'credential.semantic.revise', 'credential.semantic.get', 'credential.semantic.status', 'credential.semantic.revoke', 'credential.semantic.revocationList'),
689
+ params_hash: hash, iat: integer, exp: integer, credential_cid: item => { if (typeof item !== 'string')
690
+ invalid('invalid credential CID'); },
691
+ });
692
+ const item = value;
693
+ if ((item.credential_cid === '') !== (item.method === 'credential.semantic.revocationList'))
694
+ invalid('management CID does not match method');
695
+ if (item.exp <= item.iat || item.exp - item.iat > 300)
696
+ invalid('invalid management declaration interval');
697
+ if (now !== undefined) {
698
+ integer(now);
699
+ if (item.iat - now > 300 || now - item.exp > 300)
700
+ throw new SemanticProtocolError('SEMANTIC_BINDING_INVALID', 'management request is outside its validity window');
701
+ }
702
+ if (context) {
703
+ const params = context.params;
704
+ if (item.method !== context.method || item.target !== context.target || item.credential_cid !== params.credential_cid || item.request_id !== params.request_id || item.nonce !== params.nonce || item.params_hash !== digest(projectManagementParams(params)))
705
+ throw new SemanticProtocolError('SEMANTIC_BINDING_INVALID', 'management request binding does not match actual request');
706
+ }
707
+ }
708
+ export function buildManagementBinding(method, target, params, now = Math.trunc(Date.now() / 1000)) {
709
+ const binding = { request_id: params.request_id, nonce: params.nonce, credential_cid: params.credential_cid, target, method, params_hash: digest(projectManagementParams(params)), iat: now, exp: now + 300 };
710
+ validateManagementBinding(binding, now, { method, target, params });
711
+ return binding;
712
+ }
713
+ export function validateMetadataChange(value) {
714
+ shape(value, { expected: audienceSet(audienceRevision), metadata: array(item => { validateRegistrationMetadata(decodeSignedJson(item)); }),
715
+ 'expected_absent?': set(aid), 'withdraw?': set(aid), 'expected_withdrawn?': audienceSet(withdrawn) });
716
+ const change = value;
717
+ const parsed = change.metadata.map(item => decodeSignedJson(item));
718
+ sorted(parsed, item => item.version);
719
+ const replaced = parsed.flatMap(item => item.aud);
720
+ if (new Set(replaced).size !== replaced.length || replaced.some(audience => change.withdraw?.includes(audience)))
721
+ invalid('metadata audience changes overlap');
722
+ if (change.metadata.length === 0 && !change.withdraw?.length)
723
+ invalid('metadata change must publish or withdraw audiences');
724
+ const existing = change.expected.map(item => item.audience), absent = change.expected_absent ?? [], restored = (change.expected_withdrawn ?? []).map(item => item.audience);
725
+ if (new Set([...existing, ...absent, ...restored]).size !== existing.length + absent.length + restored.length)
726
+ invalid('metadata change preconditions overlap');
727
+ const affected = [...replaced, ...change.withdraw ?? []].sort(compareCodePoints);
728
+ if (digest(affected) !== digest([...existing, ...absent, ...restored].sort(compareCodePoints)))
729
+ invalid('metadata change preconditions do not cover changes');
730
+ if (change.withdraw?.some(audience => !existing.includes(audience)))
731
+ invalid('withdraw must refer to an expected current audience');
732
+ }
733
+ export function validateLineageEntry(value) {
734
+ object(value);
735
+ object(value.credential);
736
+ if (value.credential.ver === SEMANTIC_VERSION)
737
+ shape(value, {
738
+ credential: validateSemanticCredential, metadata: validateRegistrationMetadata,
739
+ registration_receipt: item => validateSignedRecord('registration-receipt', item),
740
+ status: item => validateSignedRecord('credential-status', item),
741
+ });
742
+ else
743
+ shape(value, { credential: item => { jsonObject(item); if (item.ver !== 'aun-cap/1')
744
+ invalid('unsupported lineage credential version'); },
745
+ legacy: item => shape(item, { 'registration_receipt?': jsonObject, status_proof: jsonObject }) });
746
+ }
747
+ export function validateManagementParams(method, value, context = {}) {
748
+ canonicalJson(value);
749
+ object(value);
750
+ const base = {
751
+ request_id: string, nonce, credential_cid: (item) => { if (typeof item !== 'string')
752
+ invalid('invalid CID'); },
753
+ request_binding: (binding) => validateManagementBinding(binding, context.now),
754
+ authority: (authority) => shape(authority, { policy, evidence: array(validateEvidenceRef) }),
755
+ };
756
+ switch (method) {
757
+ case 'credential.semantic.register':
758
+ shape(value, { ...base, original: item => { validateSemanticCredential(decodeSignedJson(item)); }, metadata: array(item => { validateRegistrationMetadata(decodeSignedJson(item)); }, 1), lineage_evidence: array(validateLineageEntry) });
759
+ if (decodeSignedJson(value.original).cid !== value.credential_cid)
760
+ invalid('original CID differs from management CID');
761
+ sorted(value.metadata.map((item) => decodeSignedJson(item)), item => item.version);
762
+ break;
763
+ case 'credential.semantic.revise':
764
+ if (value.kind === 'metadata')
765
+ shape(value, { ...base, kind: literal('metadata'), change: validateMetadataChange });
766
+ else if (value.kind === 'usage_prepare') {
767
+ shape(value, { ...base, kind: literal('usage_prepare'), transition_id: string, from_usage: usageRef, to_usage: usageRef,
768
+ change: validateMetadataChange, previous_abort_acks: array(item => validateSignedRecord('ledger-abort', decodeSignedJson(item))) });
769
+ transitionEpoch(value);
770
+ }
771
+ else if (value.kind === 'usage_commit') {
772
+ shape(value, { ...base, kind: literal('usage_commit'), transition_id: string, fence: positive,
773
+ checkpoints: array(item => validateSignedRecord('usage-checkpoint', decodeSignedJson(item)), 1),
774
+ allocation_closures: array(item => {
775
+ const closure = decodeSignedJson(item);
776
+ validateSignedRecord('allocation-status', closure);
777
+ if (closure.state !== 'closed')
778
+ invalid('allocation closure must be closed');
779
+ }), acceptances: array(item => validateSignedRecord('usage-acceptance', decodeSignedJson(item)), 1) });
780
+ sorted(value.allocation_closures.map((item) => digest(decodeSignedJson(item))));
781
+ }
782
+ else
783
+ shape(value, { ...base, kind: literal('usage_abort'), transition_id: string, fence: positive, from_usage: usageRef });
784
+ break;
785
+ case 'credential.semantic.get':
786
+ shape(value, { ...base, audience: aid, 'metadata_version?': string, 'transition_id?': string });
787
+ if (value.metadata_version !== undefined && value.transition_id !== undefined)
788
+ invalid('get selectors are mutually exclusive');
789
+ break;
790
+ case 'credential.semantic.status':
791
+ if (value.policy?.mode === 'online')
792
+ shape(value, { ...base, audience: aid, expected_revision: revision, policy: item => shape(item, { mode: literal('online') }), query: item => shape(item, { request_hash: hash, nonce }) });
793
+ else
794
+ shape(value, { ...base, audience: aid, expected_revision: revision, policy: item => shape(item, { mode: literal('bounded'), max_staleness_seconds: item => { positive(item); if (item > 300)
795
+ invalid('bounded staleness exceeds 300'); } }) });
796
+ if (value.expected_revision.cid !== value.credential_cid)
797
+ invalid('status revision CID differs from management CID');
798
+ break;
799
+ case 'credential.semantic.revoke':
800
+ shape(value, { ...base, reason_code: string, 'reason?': string });
801
+ break;
802
+ case 'credential.semantic.revocationList':
803
+ shape(value, { ...base, scope: item => shape(item, { audiences: set(aid, 1) }), after_event_seq: integer, cursor: nullable(string), limit: item => { positive(item); if (item > 1000)
804
+ invalid('CRL page limit exceeds 1000'); } });
805
+ break;
806
+ default: invalid('unsupported semantic management method');
807
+ }
808
+ validateManagementBinding(value.request_binding, context.now, { method, target: context.target ?? value.request_binding.target, params: value });
809
+ }
810
+ function certificate(material) {
811
+ if (typeof material === 'string' && !material.startsWith('-----BEGIN CERTIFICATE-----'))
812
+ return new X509Certificate(decodeBase64(material));
813
+ return new X509Certificate(material);
814
+ }
815
+ function expectedSigner(domain, value, expected) {
816
+ const bound = domain === 'registration-metadata' ? value.confirmation.authority
817
+ : domain === 'usage-allocation' ? value.parent_authority
818
+ : ['registration-receipt', 'semantic-crl', 'usage-transition'].includes(domain) ? value.service_aid
819
+ : ['allocation-status', 'usage-checkpoint', 'usage-acceptance', 'ledger-abort'].includes(domain) ? value.authority : undefined;
820
+ if (bound && expected && bound !== expected)
821
+ throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'expected signer differs from signed authority');
822
+ const signer = bound ?? expected;
823
+ if (!signer)
824
+ throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'trusted signer identity is required');
825
+ aid(signer);
826
+ return signer;
827
+ }
828
+ export async function verifyRecord(domain, value, context) {
829
+ const now = context.now ?? Math.trunc(Date.now() / 1000);
830
+ integer(now);
831
+ validateSignedRecord(domain, value, now);
832
+ const item = value, block = signatureOf(domain, item);
833
+ const expected = expectedSigner(domain, item, context.expectedAid);
834
+ const resolved = await context.resolveCertificate(block.cert_fingerprint, expected, block.timestamp);
835
+ if (resolved === undefined || resolved === null)
836
+ throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'signing certificate is unavailable');
837
+ try {
838
+ const wrapper = typeof resolved === 'object' && !(resolved instanceof Uint8Array) ? resolved : { certificatePem: resolved };
839
+ const cert = certificate(wrapper.certificatePem);
840
+ if (bytesDigest(cert.raw) !== block.cert_fingerprint || block.public_key_fingerprint && publicKeyFingerprint(cert.toString()) !== block.public_key_fingerprint)
841
+ throw new Error('certificate fingerprint differs from signature');
842
+ if (certCommonName(cert.toString()) !== expected || cert.publicKey.asymmetricKeyType !== 'ec' || cert.publicKey.asymmetricKeyDetails?.namedCurve !== 'prime256v1')
843
+ throw new Error('certificate identity or curve mismatch');
844
+ const start = Date.parse(cert.validFrom) / 1000, end = Date.parse(cert.validTo) / 1000;
845
+ if (block.timestamp < start || block.timestamp > end)
846
+ throw new Error('signature timestamp is outside original certificate validity');
847
+ if (context.requireCurrentValidity ?? true)
848
+ validateWindow(start, end, now);
849
+ if (typeof context.verifyCertificateTrust !== 'function')
850
+ throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'explicit certificate trust verifier is required');
851
+ await context.verifyCertificateTrust(wrapper.certificatePem, { expectedAID: expected, signedAt: block.timestamp, now });
852
+ if (!verifySignature('sha256', signingPayload(domain, item), cert.publicKey, decodeBase64(block.signature)))
853
+ throw new Error('signature verification failed');
854
+ }
855
+ catch (error) {
856
+ if (error instanceof SemanticProtocolError)
857
+ throw error;
858
+ throw new SemanticProtocolError('SEMANTIC_SIGNATURE_INVALID', error instanceof Error ? error.message : String(error));
859
+ }
860
+ const required = ['authorization_policy', 'historical_revocation', 'revision_bindings', 'business_acceptance'];
861
+ if (domain === 'semantic-credential') {
862
+ required.push('issuer_authority');
863
+ if (item.iss && item.iss !== expected)
864
+ required.push('representation');
865
+ }
866
+ return { original_hash: digest(item), signatures_verified: true, identities_bound: true, revisions_bound: false,
867
+ checked: ['format', 'signature_domain', 'signature', 'certificate_fingerprint', 'signer_identity', 'certificate_time', 'trust_chain'],
868
+ required };
869
+ }
870
+ export async function signRecord(domain, value, signer, options = {}) {
871
+ checkDomain(domain);
872
+ const item = parseJsonStrict(canonicalJson(value));
873
+ object(item);
874
+ const timestamp = options.timestamp ?? (domain === 'registration-metadata' ? item.confirmed_at : domain === 'registration-receipt' ? item.registered_at : item.as_of ?? Math.trunc(Date.now() / 1000));
875
+ integer(timestamp);
876
+ const owner = domain === 'registration-metadata' ? item.confirmation : item;
877
+ object(owner);
878
+ if (Object.hasOwn(owner, 'signature'))
879
+ invalid('signing requires an unsigned object');
880
+ owner.signature = { profile: SIGNATURE_PROFILE, cert_fingerprint: signer.certFingerprint, timestamp };
881
+ expectedSigner(domain, item, signer.aid);
882
+ const result = await signer.sign(Buffer.from(signingPayload(domain, item)));
883
+ if (!result.ok)
884
+ throw new SemanticProtocolError('SEMANTIC_SIGNATURE_INVALID', 'signing failed');
885
+ owner.signature.signature = result.data.signature;
886
+ validateSignedRecord(domain, item);
887
+ const cert = certificate(signer.certPem);
888
+ if (bytesDigest(cert.raw) !== signer.certFingerprint || certCommonName(cert.toString()) !== signer.aid || cert.publicKey.asymmetricKeyDetails?.namedCurve !== 'prime256v1' || timestamp < Date.parse(cert.validFrom) / 1000 || timestamp > Date.parse(cert.validTo) / 1000)
889
+ throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'signing certificate does not match identity or original validity');
890
+ if (!verifySignature('sha256', signingPayload(domain, item), cert.publicKey, decodeBase64(owner.signature.signature)))
891
+ throw new SemanticProtocolError('SEMANTIC_SIGNATURE_INVALID', 'generated signature does not verify');
892
+ return encodeSignedJson(item);
893
+ }
894
+ //# sourceMappingURL=semantic-protocol.js.map