@agentunion/fastaun 0.5.25 → 0.5.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/_packed_docs/CHANGELOG.md +41 -0
- package/_packed_docs/INDEX.md +22 -14
- package/_packed_docs/KITE_DOCS_GUIDE.md +11 -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 +16 -1
- 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
- package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +20 -9
- package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +57 -12
- package/_packed_docs/cli//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241CLI/345/274/200/345/217/221/344/270/216/344/275/277/347/224/250/346/214/207/345/215/227.md +869 -0
- 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
- package/_packed_docs/sdk/0.5.27-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +408 -0
- package/_packed_docs/sdk/0.5.27-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +410 -0
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +2 -2
- 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
- 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
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +13 -9
- package/_packed_docs/sdk/INDEX.md +21 -13
- package/_packed_docs/sdk/agent.md/344/270/216group.index/347/232/204ETag/350/247/202/345/257/237/344/270/216/345/220/214/346/255/245/346/234/272/345/210/266.md +240 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +973 -266
- 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
- package/dist/agent-md-schema.js +1 -1
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.js +5 -0
- package/dist/agent-md.js.map +1 -1
- package/dist/aid.js +3 -3
- package/dist/aid.js.map +1 -1
- package/dist/auth.js +21 -19
- package/dist/auth.js.map +1 -1
- package/dist/cert-utils.d.ts +1 -1
- package/dist/cert-utils.js +4 -4
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/lifecycle.d.ts +1 -1
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/rpc-pipeline.js +8 -1
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.js +7 -0
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +12 -0
- package/dist/client.js +305 -29
- package/dist/client.js.map +1 -1
- package/dist/facades.d.ts +38 -0
- package/dist/facades.js +49 -5
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/keystore/local-identity-store.js +16 -1
- package/dist/keystore/local-identity-store.js.map +1 -1
- package/dist/keystore/local-token-store.js +16 -1
- package/dist/keystore/local-token-store.js.map +1 -1
- package/dist/org-protocol.d.ts +1 -0
- package/dist/org-protocol.js +39 -36
- package/dist/org-protocol.js.map +1 -1
- package/dist/ownership-v21.js +7 -7
- package/dist/ownership-v21.js.map +1 -1
- package/dist/register-flow.js +2 -2
- package/dist/register-flow.js.map +1 -1
- package/dist/semantic-facade.d.ts +176 -0
- package/dist/semantic-facade.js +611 -0
- package/dist/semantic-facade.js.map +1 -0
- package/dist/semantic-protocol.d.ts +810 -0
- package/dist/semantic-protocol.js +894 -0
- package/dist/semantic-protocol.js.map +1 -0
- package/dist/tools/cross-sdk-agent.d.ts +11 -1
- package/dist/tools/cross-sdk-agent.js +388 -7
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +1 -0
- package/dist/transport.js +28 -2
- package/dist/transport.js.map +1 -1
- package/dist/v2/e2ee/decrypt.d.ts +5 -0
- package/dist/v2/e2ee/decrypt.js +22 -2
- package/dist/v2/e2ee/decrypt.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
- package/src/certs/root.crt +13 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import { buildManagementBinding, bytesDigest, canonicalJson, decodeSignedJson, digest, parseJsonStrict, SemanticProtocolError, signRecord, validateAuthorizationRequestTime, validateLineageEntry, validateManagementParams, validateSignedRecord, validateStatusProof, validateWindow, verifyRecord, createEvidenceRef, resolveEvidence, archiveMessage, verifyMessageBody, } from './semantic-protocol.js';
|
|
2
|
+
export class SemanticRPCFailure extends Error {
|
|
3
|
+
data;
|
|
4
|
+
code = -32000;
|
|
5
|
+
retryable;
|
|
6
|
+
constructor(message, data) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.data = data;
|
|
9
|
+
this.name = 'SemanticRPCFailure';
|
|
10
|
+
this.retryable = data.retryable;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function fail(message, code = 'SEMANTIC_FORMAT_INVALID') {
|
|
14
|
+
throw new SemanticProtocolError(code, message);
|
|
15
|
+
}
|
|
16
|
+
function clone(value) { return structuredClone(value); }
|
|
17
|
+
function canonicalObject(value) { return parseJsonStrict(canonicalJson(value)); }
|
|
18
|
+
function same(left, right, message) {
|
|
19
|
+
if (digest(left) !== digest(right))
|
|
20
|
+
fail(message, 'SEMANTIC_BINDING_INVALID');
|
|
21
|
+
}
|
|
22
|
+
function reference(metadata, cid) {
|
|
23
|
+
return { cid, original_hash: metadata.original_hash, metadata_hash: digest(metadata), metadata_version: metadata.version };
|
|
24
|
+
}
|
|
25
|
+
function recordReference(value) {
|
|
26
|
+
return { cid: value.cid, original_hash: value.original_hash, metadata_hash: value.metadata_hash, metadata_version: value.metadata_version };
|
|
27
|
+
}
|
|
28
|
+
function usageReference(metadata) {
|
|
29
|
+
return metadata.usage ? { epoch: metadata.usage.epoch, hash: digest(metadata.usage) } : undefined;
|
|
30
|
+
}
|
|
31
|
+
function usageMatches(value, expected) {
|
|
32
|
+
if (expected === undefined) {
|
|
33
|
+
if (value.usage !== undefined)
|
|
34
|
+
fail('unexpected usage reference', 'SEMANTIC_BINDING_INVALID');
|
|
35
|
+
}
|
|
36
|
+
else
|
|
37
|
+
same(value.usage, expected, 'usage reference differs from confirmed contract');
|
|
38
|
+
}
|
|
39
|
+
function decoded(value) {
|
|
40
|
+
return value && typeof value === 'object' && Object.hasOwn(value, 'bytes_base64') ? decodeSignedJson(value) : value;
|
|
41
|
+
}
|
|
42
|
+
export class SemanticCredentialFacade {
|
|
43
|
+
client;
|
|
44
|
+
options;
|
|
45
|
+
state = {};
|
|
46
|
+
tail = Promise.resolve();
|
|
47
|
+
proofFirstSeen = new Map();
|
|
48
|
+
allocationProofFirstSeen = new Map();
|
|
49
|
+
statusCache = new Map();
|
|
50
|
+
constructor(client, options = {}) {
|
|
51
|
+
this.client = client;
|
|
52
|
+
this.options = { ...options };
|
|
53
|
+
}
|
|
54
|
+
configure(options) {
|
|
55
|
+
Object.assign(this.options, options);
|
|
56
|
+
this.statusCache.clear();
|
|
57
|
+
}
|
|
58
|
+
clock() { return Math.trunc(Date.now() / 1000); }
|
|
59
|
+
monotonic() { return this.options.monotonicNow?.() ?? performance.now() / 1000; }
|
|
60
|
+
identity() {
|
|
61
|
+
const actor = this.client.aid ?? this.client.currentAid?.aid;
|
|
62
|
+
if (!actor)
|
|
63
|
+
fail('authenticated client identity is required', 'SEMANTIC_IDENTITY_INVALID');
|
|
64
|
+
return this.client._semanticContextKey?.() ?? actor;
|
|
65
|
+
}
|
|
66
|
+
guard(actor) { if (this.identity() !== actor)
|
|
67
|
+
fail('semantic operation identity changed', 'SEMANTIC_IDENTITY_INVALID'); }
|
|
68
|
+
async service(options = {}) {
|
|
69
|
+
const aid = options.serviceAid ?? this.options.serviceAid ?? await this.client._semanticServiceAid?.();
|
|
70
|
+
if (aid)
|
|
71
|
+
return aid;
|
|
72
|
+
const actor = this.client.aid ?? this.client.currentAid.aid;
|
|
73
|
+
return 'credential.' + actor.slice(actor.indexOf('.') + 1);
|
|
74
|
+
}
|
|
75
|
+
context(extra = {}) {
|
|
76
|
+
const base = this.options.verificationContext ?? this.client._semanticVerificationContext?.();
|
|
77
|
+
const context = { ...base, ...extra };
|
|
78
|
+
if (typeof context.resolveCertificate !== 'function' || typeof context.verifyCertificateTrust !== 'function')
|
|
79
|
+
fail('certificate resolver and trust verifier are required', 'SEMANTIC_IDENTITY_INVALID');
|
|
80
|
+
return context;
|
|
81
|
+
}
|
|
82
|
+
async mutate(operation) {
|
|
83
|
+
const actor = this.identity();
|
|
84
|
+
const task = this.tail.then(async () => {
|
|
85
|
+
this.guard(actor);
|
|
86
|
+
const current = clone(await this.client._loadSemanticState?.() ?? this.state);
|
|
87
|
+
const result = await operation(current);
|
|
88
|
+
this.guard(actor);
|
|
89
|
+
await this.client._saveSemanticState?.(current);
|
|
90
|
+
this.guard(actor);
|
|
91
|
+
this.state = current;
|
|
92
|
+
return result;
|
|
93
|
+
});
|
|
94
|
+
this.tail = task.catch(() => { });
|
|
95
|
+
return task;
|
|
96
|
+
}
|
|
97
|
+
async invoke(method, input, options, verify) {
|
|
98
|
+
const actor = this.identity(), service = await this.service(options);
|
|
99
|
+
this.guard(actor);
|
|
100
|
+
if (Object.hasOwn(input, 'request_binding'))
|
|
101
|
+
fail('facade input must omit request_binding');
|
|
102
|
+
const params = clone(input);
|
|
103
|
+
params.request_binding = buildManagementBinding(method, service, params, this.clock());
|
|
104
|
+
validateManagementParams(method, params, { target: service });
|
|
105
|
+
const began = this.monotonic();
|
|
106
|
+
let result;
|
|
107
|
+
try {
|
|
108
|
+
result = await this.client.call(method, params);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
const data = error?.data;
|
|
112
|
+
if (data && typeof data.error_code === 'string' && data.error_code.startsWith('SEMANTIC_'))
|
|
113
|
+
throw new SemanticRPCFailure(error.message ?? data.error_code, data);
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
this.guard(actor);
|
|
117
|
+
await verify(result, params, service, began);
|
|
118
|
+
this.guard(actor);
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
async responseRecord(domain, value, service, options) {
|
|
122
|
+
await verifyRecord(domain, value, this.context({ ...options.verificationContext, expectedAid: service }));
|
|
123
|
+
}
|
|
124
|
+
async registered(records, cid, metadata, service, options) {
|
|
125
|
+
const parsed = metadata.map(item => decoded(item));
|
|
126
|
+
const expected = parsed.flatMap(item => item.aud).sort();
|
|
127
|
+
same(records.map(item => item.audience), expected, 'registered audiences differ from requested metadata');
|
|
128
|
+
for (const record of records) {
|
|
129
|
+
const candidate = parsed.find(item => item.aud.includes(record.audience));
|
|
130
|
+
if (!candidate || !Number.isSafeInteger(record.state_seq) || record.state_seq < 1)
|
|
131
|
+
fail('invalid registered audience');
|
|
132
|
+
same(record.revision, reference(candidate, cid), 'registered revision differs from requested metadata');
|
|
133
|
+
await this.responseRecord('registration-receipt', record.registration_receipt, service, options);
|
|
134
|
+
same(recordReference(record.registration_receipt), record.revision, 'receipt revision differs from registered audience');
|
|
135
|
+
same(record.registration_receipt.audiences, candidate.aud, 'receipt audiences differ from metadata');
|
|
136
|
+
usageMatches(record.registration_receipt, usageReference(candidate));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async revisionHeads(records, withdrawn, service) {
|
|
140
|
+
await this.mutate(state => {
|
|
141
|
+
for (const [items, status] of [[records, 'current'], [withdrawn, 'withdrawn']])
|
|
142
|
+
for (const item of items) {
|
|
143
|
+
const revision = status === 'current' ? item.revision : item.last_revision;
|
|
144
|
+
if (!revision || !Number.isSafeInteger(item.state_seq) || item.state_seq < 1)
|
|
145
|
+
fail('invalid lifecycle head');
|
|
146
|
+
const key = JSON.stringify([service, revision.cid, item.audience]), prior = state.sequences?.[key];
|
|
147
|
+
if (prior && item.state_seq < prior.state_seq)
|
|
148
|
+
fail('revision head is older than observed lifecycle state', 'SEMANTIC_REVISION_CONFLICT');
|
|
149
|
+
(state.sequences ??= {})[key] = { state_seq: item.state_seq, status, revision };
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
async register(input, options = {}) {
|
|
154
|
+
return this.invoke('credential.semantic.register', input, options, async (result, params, service) => {
|
|
155
|
+
if (result.cid !== params.credential_cid || result.original_hash !== digest(decodeSignedJson(params.original)))
|
|
156
|
+
fail('register original binding differs', 'SEMANTIC_BINDING_INVALID');
|
|
157
|
+
await this.registered(result.audiences, result.cid, params.metadata, service, options);
|
|
158
|
+
await this.revisionHeads(result.audiences, [], service);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
async revise(input, options = {}) {
|
|
162
|
+
return this.invoke('credential.semantic.revise', input, options, async (result, params, service) => {
|
|
163
|
+
if (result.kind !== params.kind)
|
|
164
|
+
fail('revision kind differs from request', 'SEMANTIC_BINDING_INVALID');
|
|
165
|
+
if (result.kind === 'metadata') {
|
|
166
|
+
if (result.cid !== params.credential_cid)
|
|
167
|
+
fail('revision CID differs', 'SEMANTIC_BINDING_INVALID');
|
|
168
|
+
await this.registered(result.audiences, result.cid, params.change.metadata, service, options);
|
|
169
|
+
same(result.withdrawn.map((entry) => entry.audience), params.change.withdraw ?? [], 'withdrawn audiences differ');
|
|
170
|
+
await this.revisionHeads(result.audiences, result.withdrawn, service);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
const transition = result.transition;
|
|
174
|
+
await this.responseRecord('usage-transition', transition, service, options);
|
|
175
|
+
if (transition.cid !== params.credential_cid || transition.transition_id !== params.transition_id || params.fence !== undefined && transition.fence !== params.fence)
|
|
176
|
+
fail('transition binding differs from request', 'SEMANTIC_BINDING_INVALID');
|
|
177
|
+
if (params.from_usage)
|
|
178
|
+
same(transition.from_usage, params.from_usage, 'transition source UsageRef differs');
|
|
179
|
+
if (params.to_usage)
|
|
180
|
+
same(transition.to_usage, params.to_usage, 'transition target UsageRef differs');
|
|
181
|
+
}
|
|
182
|
+
this.statusCache.clear();
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
async prepareUsage(input, options = {}) {
|
|
186
|
+
return (await this.revise({ ...input, kind: 'usage_prepare' }, options)).transition;
|
|
187
|
+
}
|
|
188
|
+
async commitUsage(input, options = {}) {
|
|
189
|
+
return (await this.revise({ ...input, kind: 'usage_commit' }, options)).transition;
|
|
190
|
+
}
|
|
191
|
+
async abortUsage(input, options = {}) {
|
|
192
|
+
return (await this.revise({ ...input, kind: 'usage_abort' }, options)).transition;
|
|
193
|
+
}
|
|
194
|
+
async get(input, options = {}) {
|
|
195
|
+
return this.invoke('credential.semantic.get', input, options, async (result, params, service) => {
|
|
196
|
+
const original = decodeSignedJson(result.original);
|
|
197
|
+
if (original.cid !== params.credential_cid)
|
|
198
|
+
fail('get returned another CID', 'SEMANTIC_BINDING_INVALID');
|
|
199
|
+
if ((params.transition_id !== undefined) !== (result.kind === 'transition'))
|
|
200
|
+
fail('get response kind differs from selected branch', 'SEMANTIC_BINDING_INVALID');
|
|
201
|
+
if (result.kind === 'transition') {
|
|
202
|
+
await this.responseRecord('usage-transition', result.transition, service, options);
|
|
203
|
+
if (result.transition.transition_id !== params.transition_id || result.transition.cid !== original.cid || result.transition.original_hash !== digest(original))
|
|
204
|
+
fail('get transition binding differs', 'SEMANTIC_BINDING_INVALID');
|
|
205
|
+
}
|
|
206
|
+
else if (result.kind === 'revision') {
|
|
207
|
+
const metadata = decodeSignedJson(result.metadata);
|
|
208
|
+
same(result.revision, reference(metadata, original.cid), 'get revision differs from metadata');
|
|
209
|
+
if (metadata.original_hash !== digest(original) || !metadata.aud.includes(params.audience) || params.metadata_version && metadata.version !== params.metadata_version || result.head.audience !== params.audience)
|
|
210
|
+
fail('get metadata selection differs', 'SEMANTIC_BINDING_INVALID');
|
|
211
|
+
await this.responseRecord('registration-receipt', result.registration_receipt, service, options);
|
|
212
|
+
same(recordReference(result.registration_receipt), result.revision, 'get receipt differs from metadata');
|
|
213
|
+
}
|
|
214
|
+
else
|
|
215
|
+
fail('unknown semantic get result kind');
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
proofIdentity(proof) {
|
|
219
|
+
const { signature: ignored, ...statement } = proof;
|
|
220
|
+
return digest([this.identity(), statement]);
|
|
221
|
+
}
|
|
222
|
+
async rememberProof(key, received, local) {
|
|
223
|
+
return this.mutate(state => {
|
|
224
|
+
let firstSeen = local.get(key);
|
|
225
|
+
if (firstSeen === undefined) {
|
|
226
|
+
if (state.proofs?.[key])
|
|
227
|
+
fail('status proof requires a fresh local time basis', 'SEMANTIC_STATE_UNAVAILABLE');
|
|
228
|
+
firstSeen = received;
|
|
229
|
+
local.set(key, firstSeen);
|
|
230
|
+
}
|
|
231
|
+
(state.proofs ??= {})[key] = true;
|
|
232
|
+
return firstSeen;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
async observe(proof, service) {
|
|
236
|
+
await this.mutate(state => {
|
|
237
|
+
const key = JSON.stringify([service, proof.cid, proof.audience]), revokedKey = JSON.stringify([service, proof.cid]);
|
|
238
|
+
const sequences = state.sequences ??= {}, revoked = state.revoked ??= {};
|
|
239
|
+
const previous = sequences[key];
|
|
240
|
+
if (revoked[revokedKey] && proof.status !== 'revoked' || previous && proof.state_seq < previous.state_seq)
|
|
241
|
+
fail('proof is older than observed lifecycle state', 'SEMANTIC_REVISION_CONFLICT');
|
|
242
|
+
if (previous && proof.state_seq === previous.state_seq) {
|
|
243
|
+
same(previous.revision, recordReference(proof), 'equal state sequence has another revision');
|
|
244
|
+
if (previous.status === 'withdrawn' || previous.status === 'revoked' && proof.status !== 'revoked'
|
|
245
|
+
|| previous.status === 'expired' && proof.status === 'active')
|
|
246
|
+
fail('proof conflicts with observed terminal lifecycle state', 'SEMANTIC_REVISION_CONFLICT');
|
|
247
|
+
}
|
|
248
|
+
sequences[key] = { state_seq: proof.state_seq, status: proof.status, revision: recordReference(proof) };
|
|
249
|
+
if (proof.status === 'revoked')
|
|
250
|
+
revoked[revokedKey] = true;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
async status(input, options = {}) {
|
|
254
|
+
const service = await this.service(options), actor = this.identity();
|
|
255
|
+
const key = digest([actor, service, input.credential_cid, input.audience, input.expected_revision, input.policy]);
|
|
256
|
+
const cached = this.statusCache.get(key);
|
|
257
|
+
if (input.policy.mode === 'bounded' && cached) {
|
|
258
|
+
if (this.monotonic() - cached.firstSeen < input.policy.max_staleness_seconds) {
|
|
259
|
+
validateStatusProof(cached.proof, input.policy, { now: this.clock(), elapsedSeconds: this.monotonic() - cached.firstSeen });
|
|
260
|
+
await this.observe(cached.proof, service);
|
|
261
|
+
validateStatusProof(cached.proof, input.policy, { now: this.clock(), elapsedSeconds: this.monotonic() - cached.firstSeen });
|
|
262
|
+
return clone(cached.proof);
|
|
263
|
+
}
|
|
264
|
+
this.statusCache.delete(key);
|
|
265
|
+
}
|
|
266
|
+
return this.invoke('credential.semantic.status', input, options, async (proof, params, boundService, began) => {
|
|
267
|
+
const received = this.monotonic();
|
|
268
|
+
await this.responseRecord('credential-status', proof, boundService, options);
|
|
269
|
+
same(recordReference(proof), params.expected_revision, 'status revision differs from query');
|
|
270
|
+
if (proof.audience !== params.audience)
|
|
271
|
+
fail('status audience differs from query', 'SEMANTIC_BINDING_INVALID');
|
|
272
|
+
const proofKey = this.proofIdentity(proof), firstSeen = await this.rememberProof(proofKey, received, this.proofFirstSeen);
|
|
273
|
+
validateStatusProof(proof, params.policy, { now: this.clock(), elapsedSeconds: params.policy.mode === 'online' ? this.monotonic() - began : this.monotonic() - firstSeen, query: params.query });
|
|
274
|
+
await this.observe(proof, boundService);
|
|
275
|
+
validateStatusProof(proof, params.policy, { now: this.clock(), elapsedSeconds: params.policy.mode === 'online' ? Math.max(this.monotonic() - began, this.monotonic() - firstSeen) : this.monotonic() - firstSeen, query: params.query });
|
|
276
|
+
if (params.policy.mode === 'bounded')
|
|
277
|
+
this.statusCache.set(key, { proof: clone(proof), service: boundService, firstSeen });
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
async revoke(input, options = {}) {
|
|
281
|
+
return this.invoke('credential.semantic.revoke', input, options, async (result, params, service) => {
|
|
282
|
+
if (result.cid !== params.credential_cid || !Array.isArray(result.affected) || !result.affected.length)
|
|
283
|
+
fail('revoke result differs from request', 'SEMANTIC_BINDING_INVALID');
|
|
284
|
+
if (result.transition) {
|
|
285
|
+
await this.responseRecord('usage-transition', result.transition, service, options);
|
|
286
|
+
if (result.transition.cid !== result.cid || result.transition.original_hash !== result.original_hash || result.transition.state !== 'aborted'
|
|
287
|
+
|| result.transition.recovery.eligible !== false || result.transition.recovery.reason !== 'credential_revoked')
|
|
288
|
+
fail('revocation transition differs from CID terminal state', 'SEMANTIC_BINDING_INVALID');
|
|
289
|
+
}
|
|
290
|
+
await this.mutate(state => {
|
|
291
|
+
(state.revoked ??= {})[JSON.stringify([service, result.cid])] = true;
|
|
292
|
+
for (const affected of result.affected) {
|
|
293
|
+
const key = JSON.stringify([service, result.cid, affected.audience]), previous = state.sequences?.[key];
|
|
294
|
+
if (!previous || affected.state_seq >= previous.state_seq)
|
|
295
|
+
(state.sequences ??= {})[key] = { state_seq: affected.state_seq, status: 'revoked', revision: affected.revision };
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
this.statusCache.clear();
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
async revocations(input, options = {}) {
|
|
302
|
+
return this.invoke('credential.semantic.revocationList', input, options, async (page, params, service) => {
|
|
303
|
+
await this.responseRecord('semantic-crl', page, service, options);
|
|
304
|
+
same(page.scope, params.scope, 'CRL scope differs from query');
|
|
305
|
+
same(page.query, { params_hash: params.request_binding.params_hash, nonce: params.nonce }, 'CRL management query differs');
|
|
306
|
+
if (page.cursor !== params.cursor || page.after_event_seq !== params.after_event_seq || page.events.length > params.limit)
|
|
307
|
+
fail('CRL page selection differs from query', 'SEMANTIC_BINDING_INVALID');
|
|
308
|
+
await this.mutate(state => {
|
|
309
|
+
const scopeKey = digest([this.identity(), service, params.scope]), scopes = state.scopes ??= {}, previous = scopes[scopeKey];
|
|
310
|
+
const { signature: ignored, ...statement } = page;
|
|
311
|
+
const pageKey = digest([scopeKey, statement]);
|
|
312
|
+
if (state.mergedPages?.[pageKey])
|
|
313
|
+
return;
|
|
314
|
+
if (params.cursor !== null && (!previous || previous.next_cursor !== params.cursor || previous.after_event_seq !== params.after_event_seq || previous.upper_event_seq !== page.upper_event_seq))
|
|
315
|
+
fail('CRL continuation has no matching scope snapshot', 'SEMANTIC_BINDING_INVALID');
|
|
316
|
+
if (params.cursor === null && previous && params.after_event_seq !== previous.after_event_seq && params.after_event_seq !== previous.upper_event_seq)
|
|
317
|
+
fail('CRL incremental sequence differs from scope progress', 'SEMANTIC_BINDING_INVALID');
|
|
318
|
+
for (const event of page.events) {
|
|
319
|
+
(state.revoked ??= {})[JSON.stringify([service, event.cid])] = true;
|
|
320
|
+
for (const affected of event.affected) {
|
|
321
|
+
const key = JSON.stringify([service, event.cid, affected.audience]), prior = state.sequences?.[key];
|
|
322
|
+
if (!prior || affected.state_seq >= prior.state_seq)
|
|
323
|
+
(state.sequences ??= {})[key] = { state_seq: affected.state_seq, status: 'revoked', revision: affected.revision };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
scopes[scopeKey] = { after_event_seq: page.next_cursor === null ? page.upper_event_seq : page.after_event_seq, upper_event_seq: page.upper_event_seq, next_cursor: page.next_cursor };
|
|
327
|
+
(state.mergedPages ??= {})[pageKey] = true;
|
|
328
|
+
});
|
|
329
|
+
this.statusCache.clear();
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
signer(value) {
|
|
333
|
+
const signer = value ?? this.client.currentAid;
|
|
334
|
+
if (!signer)
|
|
335
|
+
fail('local signing identity is required', 'SEMANTIC_IDENTITY_INVALID');
|
|
336
|
+
return signer;
|
|
337
|
+
}
|
|
338
|
+
async signCredential(value, signer, options = {}) { return signRecord('semantic-credential', value, this.signer(signer), options); }
|
|
339
|
+
async signMetadata(value, signer, options = {}) { return signRecord('registration-metadata', value, this.signer(signer), options); }
|
|
340
|
+
async verifyAllocation(value, context) {
|
|
341
|
+
validateLineageEntry(context.parent);
|
|
342
|
+
validateLineageEntry(context.child);
|
|
343
|
+
const parent = this.effective(context.parent), child = this.effective(context.child);
|
|
344
|
+
if (!parent.usage || !child.usage)
|
|
345
|
+
fail('allocation requires parent and child usage contracts', 'SEMANTIC_BINDING_INVALID');
|
|
346
|
+
const allocation = decoded(value);
|
|
347
|
+
const report = await verifyRecord('usage-allocation', allocation, this.context({ ...context, expectedAid: allocation.parent_authority }));
|
|
348
|
+
if (allocation.parent_original_hash !== digest(context.parent.credential) || allocation.child_original_hash !== digest(context.child.credential)
|
|
349
|
+
|| allocation.grant_key !== parent.usage.grant_key || allocation.parent_epoch !== parent.usage.epoch || allocation.child_epoch !== child.usage.epoch)
|
|
350
|
+
fail('allocation original or usage binding differs', 'SEMANTIC_BINDING_INVALID');
|
|
351
|
+
if (!allocation.audiences.includes(context.audience))
|
|
352
|
+
fail('allocation does not cover requested audience', 'SEMANTIC_BINDING_INVALID');
|
|
353
|
+
for (const [effective, authority] of [[parent, allocation.parent_authority], [child, allocation.child_authority]]) {
|
|
354
|
+
for (const audience of allocation.audiences) {
|
|
355
|
+
const ledger = effective.usage.ledger;
|
|
356
|
+
if (!effective.aud.includes(audience) || !effective.usage.audiences.includes(audience)
|
|
357
|
+
|| (ledger.scope === 'global' ? ledger.authority : ledger.authorities[audience]) !== authority)
|
|
358
|
+
fail('allocation audience or ledger authority differs', 'SEMANTIC_BINDING_INVALID');
|
|
359
|
+
}
|
|
360
|
+
if (allocation.nbf < effective.nbf || effective.exp !== null && allocation.exp > effective.exp)
|
|
361
|
+
fail('allocation exceeds confirmed time bounds', 'SEMANTIC_BINDING_INVALID');
|
|
362
|
+
}
|
|
363
|
+
const exceeds = (amount, limit) => {
|
|
364
|
+
const [whole, fraction = ''] = amount.split('.'), [limitWhole, limitFraction = ''] = limit.split('.');
|
|
365
|
+
if (whole.length !== limitWhole.length)
|
|
366
|
+
return whole.length > limitWhole.length;
|
|
367
|
+
if (whole !== limitWhole)
|
|
368
|
+
return whole > limitWhole;
|
|
369
|
+
const precision = Math.max(fraction.length, limitFraction.length);
|
|
370
|
+
return fraction.padEnd(precision, '0') > limitFraction.padEnd(precision, '0');
|
|
371
|
+
};
|
|
372
|
+
for (const budget of allocation.budgets) {
|
|
373
|
+
const parentLimit = parent.usage.limits.find((limit) => limit.id === budget.parent_limit_id);
|
|
374
|
+
const childLimit = child.usage.limits.find((limit) => limit.id === budget.child_limit_id);
|
|
375
|
+
if (!parentLimit || !childLimit || [parentLimit, childLimit].some(limit => limit.metric !== budget.metric || limit.unit !== budget.unit || exceeds(budget.amount, limit.limit)))
|
|
376
|
+
fail('allocation budget exceeds or differs from confirmed limits', 'SEMANTIC_BINDING_INVALID');
|
|
377
|
+
}
|
|
378
|
+
return { ...report, revisions_bound: true, checked: [...report.checked, 'allocation_bindings', 'audience_bindings', 'ledger_authorities', 'allocation_budgets', 'allocation_time_bounds'],
|
|
379
|
+
required: [...report.required.filter(item => item !== 'revision_bindings'), 'package_verification', 'usage_reservation'] };
|
|
380
|
+
}
|
|
381
|
+
async signAllocation(value, signer, options = {}) { return signRecord('usage-allocation', value, this.signer(signer), options); }
|
|
382
|
+
async signUsageRecord(domain, value, signer, options = {}) {
|
|
383
|
+
if (!['usage-allocation', 'allocation-status', 'semantic-crl', 'usage-transition', 'usage-checkpoint', 'usage-acceptance', 'ledger-abort'].includes(domain))
|
|
384
|
+
fail('unsupported usage record domain');
|
|
385
|
+
return signRecord(domain, value, this.signer(signer), options);
|
|
386
|
+
}
|
|
387
|
+
async verifyAllocationStatus(value, context) {
|
|
388
|
+
const began = this.monotonic(), proof = decoded(value), allocation = decoded(context.allocation);
|
|
389
|
+
if (!Number.isFinite(context.elapsedSeconds) || context.elapsedSeconds < 0 || context.elapsedSeconds >= 30)
|
|
390
|
+
fail('allocation status requires monotonic query elapsed time below 30 seconds');
|
|
391
|
+
validateSignedRecord('usage-allocation', allocation);
|
|
392
|
+
if (proof.authority !== allocation.parent_authority || proof.allocation_id !== allocation.allocation_id || proof.allocation_hash !== digest(allocation))
|
|
393
|
+
fail('allocation status differs from original allocation', 'SEMANTIC_BINDING_INVALID');
|
|
394
|
+
same(proof.query, context.query, 'allocation status query differs from request');
|
|
395
|
+
const report = await verifyRecord('allocation-status', proof, this.context({ ...context, expectedAid: allocation.parent_authority }));
|
|
396
|
+
const { signature: ignored, ...statement } = proof;
|
|
397
|
+
const proofKey = digest([this.identity(), statement]), firstSeen = await this.rememberProof(proofKey, began, this.allocationProofFirstSeen);
|
|
398
|
+
if (Math.max(context.elapsedSeconds + this.monotonic() - began, this.monotonic() - firstSeen) >= 30)
|
|
399
|
+
fail('allocation status monotonic lifetime exceeded');
|
|
400
|
+
await this.mutate(state => {
|
|
401
|
+
const key = JSON.stringify([proof.authority, proof.allocation_id]), prior = state.allocations?.[key];
|
|
402
|
+
const order = { active: 0, draining: 1, closed: 2 };
|
|
403
|
+
if (prior && (prior.allocation_hash !== proof.allocation_hash || proof.state_seq < prior.state_seq || order[proof.state] < order[prior.state] || proof.state_seq === prior.state_seq && proof.state !== prior.state))
|
|
404
|
+
fail('allocation status conflicts with observed state', 'SEMANTIC_REVISION_CONFLICT');
|
|
405
|
+
(state.allocations ??= {})[key] = { allocation_hash: proof.allocation_hash, state_seq: proof.state_seq, state: proof.state };
|
|
406
|
+
});
|
|
407
|
+
if (Math.max(context.elapsedSeconds + this.monotonic() - began, this.monotonic() - firstSeen) >= 30)
|
|
408
|
+
fail('allocation status monotonic lifetime exceeded');
|
|
409
|
+
return { ...report, revisions_bound: true, checked: [...report.checked, 'allocation_binding', 'query_binding', 'monotonic_ttl', 'allocation_state'],
|
|
410
|
+
required: [...report.required.filter(item => item !== 'revision_bindings'), 'allocation_verification', 'usage_reservation'] };
|
|
411
|
+
}
|
|
412
|
+
async verifyUsageRecord(domain, value, context = {}) {
|
|
413
|
+
if (!['usage-allocation', 'allocation-status', 'semantic-crl', 'usage-transition', 'usage-checkpoint', 'usage-acceptance', 'ledger-abort'].includes(domain))
|
|
414
|
+
fail('unsupported usage record domain');
|
|
415
|
+
return verifyRecord(domain, decoded(value), this.context(context));
|
|
416
|
+
}
|
|
417
|
+
async verifyOriginal(value, context = {}) { return verifyRecord('semantic-credential', decoded(value), this.context(context)); }
|
|
418
|
+
async verifyMetadata(value, context = {}) { return verifyRecord('registration-metadata', decoded(value), this.context(context)); }
|
|
419
|
+
async verifyReceipt(value, context = {}) { return verifyRecord('registration-receipt', decoded(value), this.context(context)); }
|
|
420
|
+
async verifyStatus(value, context = {}) {
|
|
421
|
+
const began = this.monotonic(), proof = decoded(value);
|
|
422
|
+
const service = context.expectedServiceAid ?? context.expectedAid ?? await this.service();
|
|
423
|
+
const report = await verifyRecord('credential-status', proof, this.context({ ...context, expectedAid: service }));
|
|
424
|
+
const key = this.proofIdentity(proof), firstSeen = await this.rememberProof(key, began, this.proofFirstSeen);
|
|
425
|
+
if (context.policy) {
|
|
426
|
+
if (context.elapsedSeconds !== undefined && (!Number.isFinite(context.elapsedSeconds) || context.elapsedSeconds < 0) || context.policy.mode === 'online' && context.elapsedSeconds === undefined)
|
|
427
|
+
fail('status requires valid monotonic query elapsed time');
|
|
428
|
+
validateStatusProof(proof, context.policy, { now: context.now ?? this.clock(),
|
|
429
|
+
elapsedSeconds: Math.max((context.elapsedSeconds ?? 0) + this.monotonic() - began, this.monotonic() - firstSeen), query: context.query });
|
|
430
|
+
}
|
|
431
|
+
await this.observe(proof, service);
|
|
432
|
+
if (context.policy)
|
|
433
|
+
validateStatusProof(proof, context.policy, { now: context.now ?? this.clock(),
|
|
434
|
+
elapsedSeconds: Math.max((context.elapsedSeconds ?? 0) + this.monotonic() - began, this.monotonic() - firstSeen), query: context.query });
|
|
435
|
+
return report;
|
|
436
|
+
}
|
|
437
|
+
async buildPackage(lineage) {
|
|
438
|
+
if (!Array.isArray(lineage) || !lineage.length || lineage.length > 9)
|
|
439
|
+
fail('lineage must contain root through leaf, maximum delegation depth 8');
|
|
440
|
+
lineage.forEach(validateLineageEntry);
|
|
441
|
+
this.lineageBindings(lineage);
|
|
442
|
+
return clone({ lineage });
|
|
443
|
+
}
|
|
444
|
+
effective(entry) { return entry.credential.ver === 'aun-cap/semantic-1' ? entry.metadata : entry.credential; }
|
|
445
|
+
lineageBindings(lineage) {
|
|
446
|
+
const seen = new Set();
|
|
447
|
+
for (let index = 0; index < lineage.length; index++) {
|
|
448
|
+
const entry = lineage[index], credential = entry.credential, effective = this.effective(entry);
|
|
449
|
+
if (seen.has(credential.cid))
|
|
450
|
+
fail('lineage contains duplicate CID');
|
|
451
|
+
seen.add(credential.cid);
|
|
452
|
+
if ((effective.delegation_depth ?? 0) !== index || index === 0 && effective.parent !== undefined)
|
|
453
|
+
fail('lineage root or depth differs');
|
|
454
|
+
if (index > 0) {
|
|
455
|
+
const parent = this.effective(lineage[index - 1]);
|
|
456
|
+
if (effective.parent !== lineage[index - 1].credential.cid || effective.iss !== parent.sub || parent.delegate !== true || effective.nbf < parent.nbf || parent.exp !== null && (effective.exp === null || effective.exp > parent.exp) || effective.aud.some((aud) => !parent.aud.includes(aud)))
|
|
457
|
+
fail('lineage delegation identity or upper bounds differ');
|
|
458
|
+
}
|
|
459
|
+
for (const chain of [credential.proof_chain, effective === credential ? undefined : effective.proof_chain]) {
|
|
460
|
+
if (chain !== undefined)
|
|
461
|
+
same(chain.map((item) => digest(item)), lineage.slice(0, index).reverse().map(item => digest(item.credential)), 'proof_chain differs from ordered lineage');
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async verifyPackage(value, context) {
|
|
466
|
+
const began = this.monotonic(), grant = await this.buildPackage(value.lineage), now = context.now ?? this.clock();
|
|
467
|
+
const required = new Set(['issuer_authority', 'authorization_policy', 'business_acceptance', 'parent_semantics', 'capability_intersection', 'usage_reservation']);
|
|
468
|
+
for (const entry of grant.lineage) {
|
|
469
|
+
const effective = this.effective(entry);
|
|
470
|
+
if (!effective.aud.includes(context.audience))
|
|
471
|
+
fail('package audience differs from request', 'SEMANTIC_BINDING_INVALID');
|
|
472
|
+
if (entry.credential.ver === 'aun-cap/1') {
|
|
473
|
+
if (!context.legacyVerifier)
|
|
474
|
+
fail('trusted legacy credential verifier is required', 'SEMANTIC_STATE_UNAVAILABLE');
|
|
475
|
+
await context.legacyVerifier(entry, context.audience, now);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
const semantic = entry;
|
|
479
|
+
const signer = context.originalSigner ? await context.originalSigner(entry) : context.expectedAid;
|
|
480
|
+
const originalReport = await this.verifyOriginal(semantic.credential, { ...context, expectedAid: signer, now });
|
|
481
|
+
originalReport.required.forEach(item => required.add(item));
|
|
482
|
+
await this.verifyMetadata(semantic.metadata, { ...context, expectedAid: semantic.metadata.confirmation.authority, now });
|
|
483
|
+
const originalHash = digest(semantic.credential), metadata = semantic.metadata;
|
|
484
|
+
if (metadata.original_hash !== originalHash)
|
|
485
|
+
fail('metadata binds another original', 'SEMANTIC_BINDING_INVALID');
|
|
486
|
+
for (const field of ['iss', 'sub', 'parent', 'delegation_depth'])
|
|
487
|
+
if (semantic.credential[field] !== undefined && semantic.credential[field] !== metadata[field])
|
|
488
|
+
fail(`explicit original ${field} differs from metadata`, 'SEMANTIC_BINDING_INVALID');
|
|
489
|
+
if (semantic.credential.aud && metadata.aud.some(aud => !semantic.credential.aud.includes(aud)) || semantic.credential.nbf !== undefined && metadata.nbf < semantic.credential.nbf || semantic.credential.exp !== undefined && (metadata.exp === null || metadata.exp > semantic.credential.exp) || semantic.credential.delegate === false && metadata.delegate)
|
|
490
|
+
fail('metadata expands explicit original upper bound', 'SEMANTIC_BINDING_INVALID');
|
|
491
|
+
const expected = reference(metadata, semantic.credential.cid);
|
|
492
|
+
same(recordReference(semantic.registration_receipt), expected, 'package receipt binds another revision');
|
|
493
|
+
same(recordReference(semantic.status), expected, 'package status binds another revision');
|
|
494
|
+
same(semantic.registration_receipt.audiences, metadata.aud, 'package receipt audiences differ');
|
|
495
|
+
usageMatches(semantic.registration_receipt, usageReference(metadata));
|
|
496
|
+
usageMatches(semantic.status, usageReference(metadata));
|
|
497
|
+
const service = context.serviceFor ? await context.serviceFor(semantic.credential.cid) : context.expectedServiceAid ?? await this.service();
|
|
498
|
+
await this.verifyReceipt(semantic.registration_receipt, { ...context, expectedAid: service, now });
|
|
499
|
+
await this.verifyStatus(semantic.status, { ...context, expectedAid: service, expectedServiceAid: service, now,
|
|
500
|
+
policy: context.statePolicy ?? { mode: 'online' }, elapsedSeconds: context.elapsedSeconds === undefined ? undefined : context.elapsedSeconds + this.monotonic() - began, query: context.queries?.[semantic.credential.cid] });
|
|
501
|
+
if (semantic.status.audience !== context.audience || semantic.status.status !== 'active')
|
|
502
|
+
fail('package lifecycle state is not active', 'SEMANTIC_REVISION_CONFLICT');
|
|
503
|
+
validateWindow(metadata.nbf, metadata.exp, now);
|
|
504
|
+
}
|
|
505
|
+
for (const entry of grant.lineage)
|
|
506
|
+
if (entry.credential.ver === 'aun-cap/semantic-1') {
|
|
507
|
+
const semantic = entry;
|
|
508
|
+
const firstSeen = this.proofFirstSeen.get(this.proofIdentity(semantic.status)) ?? began;
|
|
509
|
+
validateStatusProof(semantic.status, context.statePolicy ?? { mode: 'online' }, { now: context.now ?? this.clock(),
|
|
510
|
+
elapsedSeconds: Math.max((context.elapsedSeconds ?? 0) + this.monotonic() - began, this.monotonic() - firstSeen),
|
|
511
|
+
query: context.queries?.[semantic.credential.cid] });
|
|
512
|
+
}
|
|
513
|
+
return { original_hash: digest(grant.lineage.at(-1).credential), signatures_verified: true, identities_bound: true, revisions_bound: true,
|
|
514
|
+
checked: ['original_signatures', 'metadata_signatures', 'receipt_signatures', 'status_signatures', 'revision_bindings', 'lineage', 'time_windows', 'lifecycle_state'], required: [...required] };
|
|
515
|
+
}
|
|
516
|
+
async requestHash(request) {
|
|
517
|
+
const { grant, request_binding: ignored, ...operation } = request;
|
|
518
|
+
const lineage = grant.lineage.map(entry => entry.credential.ver === 'aun-cap/semantic-1'
|
|
519
|
+
? { revision: reference(entry.metadata, entry.credential.cid), receipt_hash: digest(entry.registration_receipt) }
|
|
520
|
+
: { revision: { cid: entry.credential.cid, original_hash: digest(entry.credential) }, ...(entry.legacy.registration_receipt ? { receipt_hash: digest(entry.legacy.registration_receipt) } : {}) });
|
|
521
|
+
return digest(['aun-cap-request/1', { ...operation, lineage }]);
|
|
522
|
+
}
|
|
523
|
+
async buildRequest(input, options = {}) {
|
|
524
|
+
const request = canonicalObject(input), now = options.now ?? this.clock();
|
|
525
|
+
if (!Number.isSafeInteger(request.expires_at) || request.expires_at <= now || request.expires_at - now > 300)
|
|
526
|
+
fail('sender request lifetime must be within 300 seconds');
|
|
527
|
+
await this.buildPackage(request.grant.lineage);
|
|
528
|
+
request.params_hash = digest(request.params);
|
|
529
|
+
request.request_binding = { profile: 'aun-cap-request/1', request_hash: await this.requestHash(request) };
|
|
530
|
+
this.requestShape(request);
|
|
531
|
+
await this.rememberRequest(request);
|
|
532
|
+
return request;
|
|
533
|
+
}
|
|
534
|
+
requestShape(request) {
|
|
535
|
+
const fields = ['request_id', 'nonce', 'actor', 'sub', 'target', 'aud', 'ns', 'action', 'resource', 'params', 'params_hash', 'expires_at', 'grant', 'request_binding'];
|
|
536
|
+
if (Object.keys(request).some(key => !fields.includes(key)) || fields.some(key => !Object.hasOwn(request, key)))
|
|
537
|
+
fail('authorization request fields differ from contract');
|
|
538
|
+
for (const key of ['request_id', 'actor', 'sub', 'target', 'aud', 'ns', 'action', 'resource'])
|
|
539
|
+
if (typeof request[key] !== 'string' || !request[key])
|
|
540
|
+
fail('authorization request identifier is empty');
|
|
541
|
+
for (const key of ['actor', 'sub', 'target', 'aud'])
|
|
542
|
+
if (!/^[^\s/]+\.[^\s/]+$/.test(request[key]))
|
|
543
|
+
fail('authorization request identity is not an AID');
|
|
544
|
+
if (!/^[A-Za-z0-9_-]+$/.test(request.nonce) || Buffer.from(request.nonce, 'base64url').length < 16 || Buffer.from(request.nonce, 'base64url').toString('base64url') !== request.nonce)
|
|
545
|
+
fail('authorization request nonce is invalid');
|
|
546
|
+
if (!request.params || typeof request.params !== 'object' || Array.isArray(request.params) || !request.request_binding || Object.keys(request.request_binding).sort().join(',') !== 'profile,request_hash' || request.request_binding.profile !== 'aun-cap-request/1' || !/^sha256:[0-9a-f]{64}$/.test(request.request_binding.request_hash))
|
|
547
|
+
fail('authorization request params or profile is invalid');
|
|
548
|
+
}
|
|
549
|
+
async rememberRequest(request, accepted = false, now = this.clock()) {
|
|
550
|
+
await this.mutate(state => {
|
|
551
|
+
const requests = state.requests ??= {}, nonces = state.nonces ??= {};
|
|
552
|
+
const requestKey = JSON.stringify([request.actor, request.aud, request.request_id]), nonceKey = JSON.stringify([request.actor, request.aud, request.nonce]);
|
|
553
|
+
const existing = requests[requestKey], used = nonces[nonceKey];
|
|
554
|
+
if (existing && existing.request_hash !== request.request_binding.request_hash)
|
|
555
|
+
fail('request_id already binds another request', 'SEMANTIC_IDEMPOTENCY_CONFLICT');
|
|
556
|
+
if (used && (used.request_id !== request.request_id || used.request_hash !== request.request_binding.request_hash))
|
|
557
|
+
fail('nonce already binds another request', 'SEMANTIC_BINDING_INVALID');
|
|
558
|
+
if (accepted && !existing?.accepted)
|
|
559
|
+
validateAuthorizationRequestTime(request, now);
|
|
560
|
+
requests[requestKey] = { request_hash: request.request_binding.request_hash, nonce: request.nonce, expires_at: request.expires_at, accepted: existing?.accepted || accepted };
|
|
561
|
+
nonces[nonceKey] = { request_id: request.request_id, request_hash: request.request_binding.request_hash, expires_at: request.expires_at };
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
async verifyRequest(request, authentication, options = {}) {
|
|
565
|
+
this.requestShape(request);
|
|
566
|
+
await this.buildPackage(request.grant.lineage);
|
|
567
|
+
const now = options.now ?? this.clock();
|
|
568
|
+
if (request.actor !== authentication.actor || request.aud !== authentication.service_aid || request.target !== authentication.service_aid || !authentication.authentication_ref)
|
|
569
|
+
fail('authenticated actor or service differs from request', 'SEMANTIC_IDENTITY_INVALID');
|
|
570
|
+
const leaf = this.effective(request.grant.lineage.at(-1));
|
|
571
|
+
if (request.sub !== leaf.sub || !leaf.aud.includes(request.aud))
|
|
572
|
+
fail('request subject or audience differs from leaf', 'SEMANTIC_BINDING_INVALID');
|
|
573
|
+
if (request.actor !== request.sub && !await options.representedActor?.(request.actor, request.sub))
|
|
574
|
+
fail('actor representation evidence is required', 'SEMANTIC_PERMISSION_DENIED');
|
|
575
|
+
same(request.params_hash, digest(request.params), 'request params digest differs');
|
|
576
|
+
const requestHash = await this.requestHash(request);
|
|
577
|
+
same(request.request_binding.request_hash, requestHash, 'authorization request digest differs');
|
|
578
|
+
await this.rememberRequest(request, true, now);
|
|
579
|
+
return { request_hash: requestHash, checked: ['authenticated_actor', 'authenticated_service', 'subject', 'params_hash', 'request_hash', 'nonce_binding'], required: ['package_verification', 'business_acceptance', 'atomic_execution_idempotency'] };
|
|
580
|
+
}
|
|
581
|
+
async buildDecisionInput(request, bodies, facts, policy) {
|
|
582
|
+
const leaf = request.grant.lineage.at(-1);
|
|
583
|
+
if (!leaf?.metadata || !bodies.length)
|
|
584
|
+
fail('semantic decision requires verified leaf metadata and bodies');
|
|
585
|
+
for (const entry of request.grant.lineage)
|
|
586
|
+
if (entry.credential.ver === 'aun-cap/semantic-1' && !bodies.some(body => body.credential_hash === digest(entry.credential)))
|
|
587
|
+
fail('decision is missing an applicable semantic body');
|
|
588
|
+
for (const body of bodies)
|
|
589
|
+
if (body.body_hash !== bytesDigest(new TextEncoder().encode(body.text)) || !request.grant.lineage.some(entry => digest(entry.credential) === body.credential_hash))
|
|
590
|
+
fail('decision body binding differs', 'SEMANTIC_BINDING_INVALID');
|
|
591
|
+
const { actor, sub, target, aud, ns, action, resource, params } = request;
|
|
592
|
+
const evidence = [...new Set(bodies.flatMap(body => body.evidence_hashes))].sort();
|
|
593
|
+
const lineage = request.grant.lineage.map(entry => {
|
|
594
|
+
const body = bodies.find(item => item.credential_hash === digest(entry.credential));
|
|
595
|
+
return { revision: entry.credential.ver === 'aun-cap/semantic-1' ? reference(entry.metadata, entry.credential.cid)
|
|
596
|
+
: { cid: entry.credential.cid, original_hash: digest(entry.credential) },
|
|
597
|
+
...(body ? { body_hash: body.body_hash, evidence_hashes: [...new Set(body.evidence_hashes)].sort() } : {}) };
|
|
598
|
+
});
|
|
599
|
+
return clone({ credential_hash: digest(leaf.credential), metadata_hash: digest(leaf.metadata), metadata_version: leaf.metadata.version,
|
|
600
|
+
lineage_hash: digest(lineage), request_hash: await this.requestHash(request), evidence_hashes: evidence,
|
|
601
|
+
bodies: bodies.map(body => ({ credential_hash: body.credential_hash, body_hash: body.body_hash, text: body.text })),
|
|
602
|
+
operation: { actor, sub, target, aud, ns, action, resource, params }, facts, facts_hash: digest(facts), policy });
|
|
603
|
+
}
|
|
604
|
+
async createEvidenceRef(bytes, profile, locator) { return createEvidenceRef(bytes, profile, locator); }
|
|
605
|
+
async resolveEvidence(ref, resolvers = {}) { return resolveEvidence(ref, resolvers); }
|
|
606
|
+
async archiveMessage(envelope, received) { return archiveMessage(envelope, received); }
|
|
607
|
+
async verifyMessageBody(value, context = {}) {
|
|
608
|
+
return verifyMessageBody(value, { ...this.client._semanticMessageVerificationContext?.(), ...context, ...this.context(context) });
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
//# sourceMappingURL=semantic-facade.js.map
|