@brainai/satp-client 2.0.6 → 2.0.8
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/LICENSE +201 -0
- package/README.md +63 -35
- package/idls/v3/attestations_v3.json +439 -0
- package/idls/v3/escrow_v3.json +972 -0
- package/idls/v3/identity_v3.json +1344 -0
- package/idls/v3/reputation_v3.json +140 -0
- package/idls/v3/reviews_v3.json +300 -0
- package/idls/v3/validation_v3.json +146 -0
- package/package.json +12 -4
- package/src/attestation-request.js +176 -0
- package/src/index.d.ts +221 -0
- package/src/index.js +12 -1
- package/src/runtime-authorization-evidence.js +350 -0
- package/src/runtime-policy-adapter.js +335 -5
- package/examples/adoption-quickstarts.js +0 -173
- package/examples/integration-patterns.js +0 -495
- package/examples/runtime-policy-adapter.js +0 -107
- package/examples/x402-discovery-evidence-lookup.js +0 -66
- package/examples/x402-reputation-evidence-lookup-client.js +0 -182
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const RUNTIME_AUTHORIZATION_EVIDENCE_SCHEMA_VERSION = 'satp.runtimeAuthorizationEvidence.v0';
|
|
4
|
+
const RUNTIME_AUTHORIZATION_EVIDENCE_PROFILE_ID = 'satp.runtimeAuthorizationEvidence';
|
|
5
|
+
const RUNTIME_AUTHORIZATION_EVIDENCE_PROFILE_VERSION = '0';
|
|
6
|
+
|
|
7
|
+
const RUNTIME_AUTHORIZATION_EVIDENCE_REASON_CODES = Object.freeze({
|
|
8
|
+
UNSUPPORTED_PROFILE: 'unsupported_profile',
|
|
9
|
+
INVALID_EVIDENCE: 'invalid_evidence',
|
|
10
|
+
AUTHORIZATION_EXPIRED: 'authorization_expired',
|
|
11
|
+
SCOPE_MISMATCH: 'scope_mismatch',
|
|
12
|
+
VERIFIER_UNAVAILABLE: 'verifier_unavailable',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function firstDefined(...values) {
|
|
16
|
+
return values.find((value) => value !== undefined);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeString(value, field) {
|
|
20
|
+
const candidate = value && typeof value === 'object' && !Array.isArray(value)
|
|
21
|
+
? firstDefined(value.id, value.value)
|
|
22
|
+
: value;
|
|
23
|
+
if (typeof candidate !== 'string' || candidate.trim() === '') {
|
|
24
|
+
throw new Error(`${field} must be a non-empty string`);
|
|
25
|
+
}
|
|
26
|
+
return candidate.trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeProfile(input) {
|
|
30
|
+
const profile = input.profile && typeof input.profile === 'object' && !Array.isArray(input.profile)
|
|
31
|
+
? input.profile
|
|
32
|
+
: {};
|
|
33
|
+
const version = firstDefined(profile.version, input.profileVersion, input.profile_version);
|
|
34
|
+
return {
|
|
35
|
+
id: normalizeString(
|
|
36
|
+
firstDefined(profile.id, input.profileId, input.profile_id),
|
|
37
|
+
'profile.id'
|
|
38
|
+
),
|
|
39
|
+
version: normalizeString(typeof version === 'number' ? String(version) : version, 'profile.version'),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readProfile(input) {
|
|
44
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return null;
|
|
45
|
+
const profile = input.profile && typeof input.profile === 'object' && !Array.isArray(input.profile)
|
|
46
|
+
? input.profile
|
|
47
|
+
: {};
|
|
48
|
+
const id = firstDefined(profile.id, input.profileId, input.profile_id);
|
|
49
|
+
const version = firstDefined(profile.version, input.profileVersion, input.profile_version);
|
|
50
|
+
return {
|
|
51
|
+
id: typeof id === 'string' ? id.trim() : null,
|
|
52
|
+
version: typeof version === 'string' || typeof version === 'number' ? String(version).trim() : null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeDigest(value, field) {
|
|
57
|
+
let algorithm;
|
|
58
|
+
let digest;
|
|
59
|
+
if (typeof value === 'string') {
|
|
60
|
+
const match = value.trim().match(/^([a-z0-9-]+):([a-fA-F0-9]+)$/);
|
|
61
|
+
if (!match) throw new Error(`${field} must use algorithm:hex form or an object`);
|
|
62
|
+
[, algorithm, digest] = match;
|
|
63
|
+
} else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
64
|
+
algorithm = firstDefined(value.algorithm, value.alg);
|
|
65
|
+
digest = firstDefined(value.value, value.digest, value.hex);
|
|
66
|
+
} else {
|
|
67
|
+
throw new Error(`${field} must be a digest object or algorithm:hex string`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
algorithm = normalizeString(algorithm, `${field}.algorithm`).toLowerCase();
|
|
71
|
+
digest = normalizeString(digest, `${field}.value`).toLowerCase();
|
|
72
|
+
if (algorithm !== 'sha256') throw new Error(`${field}.algorithm must be sha256`);
|
|
73
|
+
if (!/^[a-f0-9]{64}$/.test(digest)) throw new Error(`${field}.value must be 64 hexadecimal characters`);
|
|
74
|
+
return { algorithm, value: digest };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeTimestamp(value, field) {
|
|
78
|
+
if (value === undefined || value === null || value === '') {
|
|
79
|
+
throw new Error(`${field} is required`);
|
|
80
|
+
}
|
|
81
|
+
const timestamp = value instanceof Date ? new Date(value.getTime()) : new Date(value);
|
|
82
|
+
if (Number.isNaN(timestamp.getTime())) throw new Error(`${field} must be a valid timestamp`);
|
|
83
|
+
return timestamp.toISOString();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeScope(value) {
|
|
87
|
+
const raw = typeof value === 'string' ? [value] : value;
|
|
88
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
89
|
+
throw new Error('authorizationScope must be a non-empty string array');
|
|
90
|
+
}
|
|
91
|
+
const normalized = raw.map((item) => normalizeString(item, 'authorizationScope item'));
|
|
92
|
+
return Array.from(new Set(normalized)).sort();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizeRuntimeAuthorizationEvidence(input) {
|
|
96
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
97
|
+
throw new Error('runtime authorization evidence must be an object');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const authorization = input.authorization && typeof input.authorization === 'object' && !Array.isArray(input.authorization)
|
|
101
|
+
? input.authorization
|
|
102
|
+
: {};
|
|
103
|
+
const evidence = input.evidence && typeof input.evidence === 'object' && !Array.isArray(input.evidence)
|
|
104
|
+
? input.evidence
|
|
105
|
+
: {};
|
|
106
|
+
const policy = input.policy && typeof input.policy === 'object' && !Array.isArray(input.policy)
|
|
107
|
+
? input.policy
|
|
108
|
+
: {};
|
|
109
|
+
|
|
110
|
+
const normalized = {
|
|
111
|
+
schemaVersion: normalizeString(
|
|
112
|
+
firstDefined(input.schemaVersion, input.schema_version),
|
|
113
|
+
'schemaVersion'
|
|
114
|
+
),
|
|
115
|
+
profile: normalizeProfile(input),
|
|
116
|
+
issuer: normalizeString(firstDefined(input.issuer, input.issuer_id, input.issuerId), 'issuer'),
|
|
117
|
+
verifier: normalizeString(firstDefined(input.verifier, input.verifier_id, input.verifierId), 'verifier'),
|
|
118
|
+
subject: normalizeString(firstDefined(input.subject, input.subject_id, input.subjectId), 'subject'),
|
|
119
|
+
audience: normalizeString(firstDefined(input.audience, input.audience_id, input.audienceId), 'audience'),
|
|
120
|
+
resource: normalizeString(input.resource, 'resource'),
|
|
121
|
+
evidenceDigest: normalizeDigest(
|
|
122
|
+
firstDefined(input.evidenceDigest, input.evidence_digest, evidence.digest),
|
|
123
|
+
'evidenceDigest'
|
|
124
|
+
),
|
|
125
|
+
observedAt: normalizeTimestamp(
|
|
126
|
+
firstDefined(input.observedAt, input.observed_at, evidence.observedAt, evidence.observed_at),
|
|
127
|
+
'observedAt'
|
|
128
|
+
),
|
|
129
|
+
expiresAt: normalizeTimestamp(
|
|
130
|
+
firstDefined(input.expiresAt, input.expires_at, authorization.expiresAt, authorization.expires_at),
|
|
131
|
+
'expiresAt'
|
|
132
|
+
),
|
|
133
|
+
authorizationScope: normalizeScope(
|
|
134
|
+
firstDefined(input.authorizationScope, input.authorization_scope, authorization.scope)
|
|
135
|
+
),
|
|
136
|
+
policyDigest: normalizeDigest(
|
|
137
|
+
firstDefined(input.policyDigest, input.policy_digest, policy.digest),
|
|
138
|
+
'policyDigest'
|
|
139
|
+
),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (normalized.schemaVersion !== RUNTIME_AUTHORIZATION_EVIDENCE_SCHEMA_VERSION) {
|
|
143
|
+
throw new Error(`schemaVersion must be ${RUNTIME_AUTHORIZATION_EVIDENCE_SCHEMA_VERSION}`);
|
|
144
|
+
}
|
|
145
|
+
if (new Date(normalized.expiresAt).getTime() <= new Date(normalized.observedAt).getTime()) {
|
|
146
|
+
throw new Error('expiresAt must be after observedAt');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return normalized;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function normalizeExpectedDigest(value, field) {
|
|
153
|
+
return value === undefined ? null : normalizeDigest(value, field);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function digestMatches(actual, expected) {
|
|
157
|
+
return !expected || (actual.algorithm === expected.algorithm && actual.value === expected.value);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalizeRequiredScopes(options) {
|
|
161
|
+
const value = firstDefined(options.requiredScopes, options.requiredScope);
|
|
162
|
+
if (value === undefined) return [];
|
|
163
|
+
return normalizeScope(value);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function verifierIsAvailable(availableVerifiers, verifier) {
|
|
167
|
+
if (typeof availableVerifiers === 'string') return availableVerifiers === verifier;
|
|
168
|
+
if (Array.isArray(availableVerifiers)) return availableVerifiers.includes(verifier);
|
|
169
|
+
if (availableVerifiers instanceof Set) return availableVerifiers.has(verifier);
|
|
170
|
+
if (availableVerifiers && typeof availableVerifiers === 'object') {
|
|
171
|
+
return Object.prototype.hasOwnProperty.call(availableVerifiers, verifier)
|
|
172
|
+
&& availableVerifiers[verifier] === true;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function failure(reasonCode, message, evidence, checks) {
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
reasonCode,
|
|
181
|
+
reasonCodes: [reasonCode],
|
|
182
|
+
message,
|
|
183
|
+
evidence,
|
|
184
|
+
checks,
|
|
185
|
+
guardrails: guardrails(),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function guardrails() {
|
|
190
|
+
return {
|
|
191
|
+
offlineOnly: true,
|
|
192
|
+
networkRequests: false,
|
|
193
|
+
writesSolanaState: false,
|
|
194
|
+
usesKeypairs: false,
|
|
195
|
+
authorizesPayment: false,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function verifyRuntimeAuthorizationEvidence(input, options = {}) {
|
|
200
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
201
|
+
return failure(
|
|
202
|
+
RUNTIME_AUTHORIZATION_EVIDENCE_REASON_CODES.INVALID_EVIDENCE,
|
|
203
|
+
'verification options must be an object',
|
|
204
|
+
null,
|
|
205
|
+
{ profileSupported: false, structurallyValid: false }
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const checks = {
|
|
210
|
+
profileSupported: false,
|
|
211
|
+
structurallyValid: false,
|
|
212
|
+
authorizationCurrent: false,
|
|
213
|
+
scopeMatches: false,
|
|
214
|
+
verifierAvailable: false,
|
|
215
|
+
};
|
|
216
|
+
const profile = readProfile(input);
|
|
217
|
+
const expectedProfileId = firstDefined(options.expectedProfileId, RUNTIME_AUTHORIZATION_EVIDENCE_PROFILE_ID);
|
|
218
|
+
const expectedProfileVersion = String(firstDefined(
|
|
219
|
+
options.expectedProfileVersion,
|
|
220
|
+
RUNTIME_AUTHORIZATION_EVIDENCE_PROFILE_VERSION
|
|
221
|
+
));
|
|
222
|
+
|
|
223
|
+
if (!profile || profile.id !== expectedProfileId || profile.version !== expectedProfileVersion) {
|
|
224
|
+
return failure(
|
|
225
|
+
RUNTIME_AUTHORIZATION_EVIDENCE_REASON_CODES.UNSUPPORTED_PROFILE,
|
|
226
|
+
`profile must be ${expectedProfileId}@${expectedProfileVersion}`,
|
|
227
|
+
null,
|
|
228
|
+
checks
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
checks.profileSupported = true;
|
|
232
|
+
|
|
233
|
+
let evidence;
|
|
234
|
+
try {
|
|
235
|
+
evidence = normalizeRuntimeAuthorizationEvidence(input);
|
|
236
|
+
const expectedEvidenceDigest = normalizeExpectedDigest(options.expectedEvidenceDigest, 'expectedEvidenceDigest');
|
|
237
|
+
if (!digestMatches(evidence.evidenceDigest, expectedEvidenceDigest)) {
|
|
238
|
+
throw new Error('evidenceDigest does not match expected evidence digest');
|
|
239
|
+
}
|
|
240
|
+
if (options.expectedIssuer !== undefined && evidence.issuer !== normalizeString(options.expectedIssuer, 'expectedIssuer')) {
|
|
241
|
+
throw new Error('issuer does not match expected issuer');
|
|
242
|
+
}
|
|
243
|
+
} catch (error) {
|
|
244
|
+
return failure(
|
|
245
|
+
RUNTIME_AUTHORIZATION_EVIDENCE_REASON_CODES.INVALID_EVIDENCE,
|
|
246
|
+
error.message,
|
|
247
|
+
null,
|
|
248
|
+
checks
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
checks.structurallyValid = true;
|
|
252
|
+
|
|
253
|
+
let now;
|
|
254
|
+
try {
|
|
255
|
+
now = options.now === undefined ? new Date() : new Date(options.now);
|
|
256
|
+
if (Number.isNaN(now.getTime())) throw new Error('now must be a valid timestamp');
|
|
257
|
+
if (new Date(evidence.observedAt).getTime() > now.getTime()) {
|
|
258
|
+
throw new Error('observedAt must not be in the future');
|
|
259
|
+
}
|
|
260
|
+
} catch (error) {
|
|
261
|
+
return failure(
|
|
262
|
+
RUNTIME_AUTHORIZATION_EVIDENCE_REASON_CODES.INVALID_EVIDENCE,
|
|
263
|
+
error.message,
|
|
264
|
+
evidence,
|
|
265
|
+
checks
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (new Date(evidence.expiresAt).getTime() <= now.getTime()) {
|
|
270
|
+
return failure(
|
|
271
|
+
RUNTIME_AUTHORIZATION_EVIDENCE_REASON_CODES.AUTHORIZATION_EXPIRED,
|
|
272
|
+
'runtime authorization evidence is expired',
|
|
273
|
+
evidence,
|
|
274
|
+
checks
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
checks.authorizationCurrent = true;
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const requiredScopes = normalizeRequiredScopes(options);
|
|
281
|
+
const expectedPolicyDigest = normalizeExpectedDigest(options.expectedPolicyDigest, 'expectedPolicyDigest');
|
|
282
|
+
const contextMatches = [
|
|
283
|
+
['expectedSubject', evidence.subject],
|
|
284
|
+
['expectedAudience', evidence.audience],
|
|
285
|
+
['expectedResource', evidence.resource],
|
|
286
|
+
].every(([key, actual]) => options[key] === undefined || normalizeString(options[key], key) === actual);
|
|
287
|
+
const scopesMatch = requiredScopes.every((scope) => evidence.authorizationScope.includes(scope));
|
|
288
|
+
const policyMatches = digestMatches(evidence.policyDigest, expectedPolicyDigest);
|
|
289
|
+
checks.scopeMatches = contextMatches && scopesMatch && policyMatches;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
return failure(
|
|
292
|
+
RUNTIME_AUTHORIZATION_EVIDENCE_REASON_CODES.INVALID_EVIDENCE,
|
|
293
|
+
error.message,
|
|
294
|
+
evidence,
|
|
295
|
+
checks
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!checks.scopeMatches) {
|
|
300
|
+
return failure(
|
|
301
|
+
RUNTIME_AUTHORIZATION_EVIDENCE_REASON_CODES.SCOPE_MISMATCH,
|
|
302
|
+
'subject, audience, resource, authorization scope, or policy digest does not match the requested context',
|
|
303
|
+
evidence,
|
|
304
|
+
checks
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let expectedVerifier;
|
|
309
|
+
try {
|
|
310
|
+
expectedVerifier = options.expectedVerifier === undefined
|
|
311
|
+
? evidence.verifier
|
|
312
|
+
: normalizeString(options.expectedVerifier, 'expectedVerifier');
|
|
313
|
+
} catch (error) {
|
|
314
|
+
return failure(
|
|
315
|
+
RUNTIME_AUTHORIZATION_EVIDENCE_REASON_CODES.INVALID_EVIDENCE,
|
|
316
|
+
error.message,
|
|
317
|
+
evidence,
|
|
318
|
+
checks
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
checks.verifierAvailable = evidence.verifier === expectedVerifier
|
|
322
|
+
&& verifierIsAvailable(options.availableVerifiers, evidence.verifier);
|
|
323
|
+
if (!checks.verifierAvailable) {
|
|
324
|
+
return failure(
|
|
325
|
+
RUNTIME_AUTHORIZATION_EVIDENCE_REASON_CODES.VERIFIER_UNAVAILABLE,
|
|
326
|
+
'the named verifier is not available in the explicit offline verifier set',
|
|
327
|
+
evidence,
|
|
328
|
+
checks
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
ok: true,
|
|
334
|
+
reasonCode: null,
|
|
335
|
+
reasonCodes: [],
|
|
336
|
+
message: 'Runtime authorization evidence verified for the requested offline context.',
|
|
337
|
+
evidence,
|
|
338
|
+
checks,
|
|
339
|
+
guardrails: guardrails(),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
module.exports = {
|
|
344
|
+
RUNTIME_AUTHORIZATION_EVIDENCE_SCHEMA_VERSION,
|
|
345
|
+
RUNTIME_AUTHORIZATION_EVIDENCE_PROFILE_ID,
|
|
346
|
+
RUNTIME_AUTHORIZATION_EVIDENCE_PROFILE_VERSION,
|
|
347
|
+
RUNTIME_AUTHORIZATION_EVIDENCE_REASON_CODES,
|
|
348
|
+
normalizeRuntimeAuthorizationEvidence,
|
|
349
|
+
verifyRuntimeAuthorizationEvidence,
|
|
350
|
+
};
|