@humanagencyp/hap-core 0.4.0
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/README.md +71 -0
- package/dist/index.d.mts +499 -0
- package/dist/index.d.ts +499 -0
- package/dist/index.js +676 -0
- package/dist/index.mjs +613 -0
- package/package.json +40 -0
- package/src/attestation.ts +170 -0
- package/src/frame.ts +245 -0
- package/src/gatekeeper.ts +577 -0
- package/src/index.ts +11 -0
- package/src/profiles/index.ts +29 -0
- package/src/types.ts +333 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
attestationId: () => attestationId,
|
|
34
|
+
canonicalBounds: () => canonicalBounds,
|
|
35
|
+
canonicalContext: () => canonicalContext,
|
|
36
|
+
canonicalFrame: () => canonicalFrame,
|
|
37
|
+
checkAttestationExpiry: () => checkAttestationExpiry,
|
|
38
|
+
clearProfiles: () => clearProfiles,
|
|
39
|
+
computeBoundsHash: () => computeBoundsHash,
|
|
40
|
+
computeContextHash: () => computeContextHash,
|
|
41
|
+
computeFrameHash: () => computeFrameHash,
|
|
42
|
+
decodeAttestationBlob: () => decodeAttestationBlob,
|
|
43
|
+
encodeAttestationBlob: () => encodeAttestationBlob,
|
|
44
|
+
frameHash: () => frameHash,
|
|
45
|
+
getAllProfiles: () => getAllProfiles,
|
|
46
|
+
getProfile: () => getProfile,
|
|
47
|
+
isV4Attestation: () => isV4Attestation,
|
|
48
|
+
listProfiles: () => listProfiles,
|
|
49
|
+
registerProfile: () => registerProfile,
|
|
50
|
+
validateBoundsParams: () => validateBoundsParams,
|
|
51
|
+
validateContextParams: () => validateContextParams,
|
|
52
|
+
validateFrameParams: () => validateFrameParams,
|
|
53
|
+
verify: () => verify,
|
|
54
|
+
verifyAttestation: () => verifyAttestation,
|
|
55
|
+
verifyAttestationSignature: () => verifyAttestationSignature,
|
|
56
|
+
verifyAttestationV4: () => verifyAttestationV4,
|
|
57
|
+
verifyBoundsHash: () => verifyBoundsHash,
|
|
58
|
+
verifyContextHash: () => verifyContextHash,
|
|
59
|
+
verifyFrameHash: () => verifyFrameHash
|
|
60
|
+
});
|
|
61
|
+
module.exports = __toCommonJS(index_exports);
|
|
62
|
+
|
|
63
|
+
// src/frame.ts
|
|
64
|
+
var import_crypto = require("crypto");
|
|
65
|
+
function validateFrameParams(params, profile) {
|
|
66
|
+
const errors = [];
|
|
67
|
+
if (!profile.frameSchema) {
|
|
68
|
+
return { valid: false, errors: ["Profile does not have a frameSchema"] };
|
|
69
|
+
}
|
|
70
|
+
for (const [fieldName, fieldDef] of Object.entries(profile.frameSchema.fields)) {
|
|
71
|
+
if (fieldDef.required && !(fieldName in params)) {
|
|
72
|
+
errors.push(`Missing required field: ${fieldName}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const [field, value] of Object.entries(params)) {
|
|
76
|
+
const fieldDef = profile.frameSchema.fields[field];
|
|
77
|
+
if (!fieldDef) {
|
|
78
|
+
errors.push(`Unknown field "${field}" not defined in profile ${profile.id}`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (fieldDef.type === "number" && typeof value !== "number") {
|
|
82
|
+
errors.push(`Field "${field}" must be a number, got ${typeof value}`);
|
|
83
|
+
}
|
|
84
|
+
if (fieldDef.type === "string" && typeof value !== "string") {
|
|
85
|
+
errors.push(`Field "${field}" must be a string, got ${typeof value}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { valid: errors.length === 0, errors };
|
|
89
|
+
}
|
|
90
|
+
function canonicalFrame(params, profile) {
|
|
91
|
+
const validation = validateFrameParams(params, profile);
|
|
92
|
+
if (!validation.valid) {
|
|
93
|
+
throw new Error(`Invalid frame parameters: ${validation.errors.join("; ")}`);
|
|
94
|
+
}
|
|
95
|
+
const lines = profile.frameSchema.keyOrder.map(
|
|
96
|
+
(key) => `${key}=${String(params[key])}`
|
|
97
|
+
);
|
|
98
|
+
return lines.join("\n");
|
|
99
|
+
}
|
|
100
|
+
function frameHash(canonicalFrameString) {
|
|
101
|
+
const hash = (0, import_crypto.createHash)("sha256").update(canonicalFrameString, "utf8").digest("hex");
|
|
102
|
+
return `sha256:${hash}`;
|
|
103
|
+
}
|
|
104
|
+
function computeFrameHash(params, profile) {
|
|
105
|
+
return frameHash(canonicalFrame(params, profile));
|
|
106
|
+
}
|
|
107
|
+
function validateBoundsParams(params, profile) {
|
|
108
|
+
const errors = [];
|
|
109
|
+
if (!profile.boundsSchema) {
|
|
110
|
+
return { valid: false, errors: ["Profile does not have a boundsSchema"] };
|
|
111
|
+
}
|
|
112
|
+
for (const [fieldName, fieldDef] of Object.entries(profile.boundsSchema.fields)) {
|
|
113
|
+
if (fieldDef.required && !(fieldName in params)) {
|
|
114
|
+
errors.push(`Missing required field: ${fieldName}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const [field, value] of Object.entries(params)) {
|
|
118
|
+
const fieldDef = profile.boundsSchema.fields[field];
|
|
119
|
+
if (!fieldDef) {
|
|
120
|
+
errors.push(`Unknown field "${field}" not defined in boundsSchema of profile ${profile.id}`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (fieldDef.type === "number" && typeof value !== "number") {
|
|
124
|
+
errors.push(`Field "${field}" must be a number, got ${typeof value}`);
|
|
125
|
+
}
|
|
126
|
+
if (fieldDef.type === "string" && typeof value !== "string") {
|
|
127
|
+
errors.push(`Field "${field}" must be a string, got ${typeof value}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { valid: errors.length === 0, errors };
|
|
131
|
+
}
|
|
132
|
+
function validateContextParams(params, profile) {
|
|
133
|
+
const errors = [];
|
|
134
|
+
if (!profile.contextSchema) {
|
|
135
|
+
if (Object.keys(params).length > 0) {
|
|
136
|
+
errors.push("Profile does not have a contextSchema but context params were provided");
|
|
137
|
+
}
|
|
138
|
+
return { valid: errors.length === 0, errors };
|
|
139
|
+
}
|
|
140
|
+
for (const [fieldName, fieldDef] of Object.entries(profile.contextSchema.fields)) {
|
|
141
|
+
if (fieldDef.required && !(fieldName in params)) {
|
|
142
|
+
errors.push(`Missing required field: ${fieldName}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (const [field, value] of Object.entries(params)) {
|
|
146
|
+
const fieldDef = profile.contextSchema.fields[field];
|
|
147
|
+
if (!fieldDef) {
|
|
148
|
+
errors.push(`Unknown field "${field}" not defined in contextSchema of profile ${profile.id}`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (fieldDef.type === "number" && typeof value !== "number") {
|
|
152
|
+
errors.push(`Field "${field}" must be a number, got ${typeof value}`);
|
|
153
|
+
}
|
|
154
|
+
if (fieldDef.type === "string" && typeof value !== "string") {
|
|
155
|
+
errors.push(`Field "${field}" must be a string, got ${typeof value}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { valid: errors.length === 0, errors };
|
|
159
|
+
}
|
|
160
|
+
function canonicalBounds(params, profile) {
|
|
161
|
+
const validation = validateBoundsParams(params, profile);
|
|
162
|
+
if (!validation.valid) {
|
|
163
|
+
throw new Error(`Invalid bounds parameters: ${validation.errors.join("; ")}`);
|
|
164
|
+
}
|
|
165
|
+
const lines = profile.boundsSchema.keyOrder.map(
|
|
166
|
+
(key) => `${key}=${String(params[key])}`
|
|
167
|
+
);
|
|
168
|
+
return lines.join("\n");
|
|
169
|
+
}
|
|
170
|
+
function canonicalContext(params, profile) {
|
|
171
|
+
if (!profile.contextSchema || Object.keys(profile.contextSchema.fields).length === 0) {
|
|
172
|
+
return "";
|
|
173
|
+
}
|
|
174
|
+
const validation = validateContextParams(params, profile);
|
|
175
|
+
if (!validation.valid) {
|
|
176
|
+
throw new Error(`Invalid context parameters: ${validation.errors.join("; ")}`);
|
|
177
|
+
}
|
|
178
|
+
const lines = profile.contextSchema.keyOrder.map(
|
|
179
|
+
(key) => `${key}=${String(params[key])}`
|
|
180
|
+
);
|
|
181
|
+
return lines.join("\n");
|
|
182
|
+
}
|
|
183
|
+
function computeBoundsHash(params, profile) {
|
|
184
|
+
const canonical = canonicalBounds(params, profile);
|
|
185
|
+
const hash = (0, import_crypto.createHash)("sha256").update(canonical, "utf8").digest("hex");
|
|
186
|
+
return `sha256:${hash}`;
|
|
187
|
+
}
|
|
188
|
+
function computeContextHash(params, profile) {
|
|
189
|
+
const canonical = canonicalContext(params, profile);
|
|
190
|
+
const hash = (0, import_crypto.createHash)("sha256").update(canonical, "utf8").digest("hex");
|
|
191
|
+
return `sha256:${hash}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/attestation.ts
|
|
195
|
+
var import_crypto2 = require("crypto");
|
|
196
|
+
var ed = __toESM(require("@noble/ed25519"));
|
|
197
|
+
function decodeAttestationBlob(blob) {
|
|
198
|
+
try {
|
|
199
|
+
const base64 = blob.replace(/-/g, "+").replace(/_/g, "/");
|
|
200
|
+
const padding = base64.length % 4 === 0 ? "" : "=".repeat(4 - base64.length % 4);
|
|
201
|
+
const json = Buffer.from(base64 + padding, "base64").toString("utf8");
|
|
202
|
+
return JSON.parse(json);
|
|
203
|
+
} catch {
|
|
204
|
+
throw new Error("MALFORMED_ATTESTATION: Failed to decode attestation blob");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function encodeAttestationBlob(attestation) {
|
|
208
|
+
const json = JSON.stringify(attestation);
|
|
209
|
+
const base64 = Buffer.from(json, "utf8").toString("base64");
|
|
210
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
211
|
+
}
|
|
212
|
+
function attestationId(blob) {
|
|
213
|
+
const hash = (0, import_crypto2.createHash)("sha256").update(blob, "utf8").digest("hex");
|
|
214
|
+
return `sha256:${hash}`;
|
|
215
|
+
}
|
|
216
|
+
async function verifyAttestationSignature(attestation, publicKeyHex) {
|
|
217
|
+
try {
|
|
218
|
+
const payloadJson = JSON.stringify(attestation.payload);
|
|
219
|
+
const payloadBytes = new TextEncoder().encode(payloadJson);
|
|
220
|
+
const signatureBytes = Buffer.from(attestation.signature, "base64");
|
|
221
|
+
const publicKeyBytes = Buffer.from(publicKeyHex, "hex");
|
|
222
|
+
const isValid = await ed.verifyAsync(signatureBytes, payloadBytes, publicKeyBytes);
|
|
223
|
+
if (!isValid) {
|
|
224
|
+
throw new Error("INVALID_SIGNATURE: Attestation signature verification failed");
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error instanceof Error && error.message.startsWith("INVALID_SIGNATURE")) throw error;
|
|
228
|
+
throw new Error(`INVALID_SIGNATURE: Signature verification error: ${error}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function checkAttestationExpiry(payload, now = Math.floor(Date.now() / 1e3)) {
|
|
232
|
+
if (payload.expires_at <= now) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`TTL_EXPIRED: Attestation expired at ${payload.expires_at}, current time is ${now}`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function verifyFrameHash(attestation, expectedFrameHash) {
|
|
239
|
+
if (attestation.payload.frame_hash !== expectedFrameHash) {
|
|
240
|
+
throw new Error("FRAME_MISMATCH: Frame hash mismatch");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function isV4Attestation(attestation) {
|
|
244
|
+
return attestation.payload.bounds_hash !== void 0;
|
|
245
|
+
}
|
|
246
|
+
function verifyBoundsHash(attestation, expectedBoundsHash) {
|
|
247
|
+
const attestedHash = attestation.payload.bounds_hash ?? attestation.payload.frame_hash;
|
|
248
|
+
if (attestedHash !== expectedBoundsHash) {
|
|
249
|
+
throw new Error("BOUNDS_MISMATCH: Bounds hash mismatch");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function verifyContextHash(attestation, expectedContextHash) {
|
|
253
|
+
if (attestation.payload.context_hash !== expectedContextHash) {
|
|
254
|
+
throw new Error("CONTEXT_MISMATCH: Context hash mismatch");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async function verifyAttestation(blob, publicKeyHex, expectedFrameHash) {
|
|
258
|
+
const attestation = decodeAttestationBlob(blob);
|
|
259
|
+
await verifyAttestationSignature(attestation, publicKeyHex);
|
|
260
|
+
checkAttestationExpiry(attestation.payload);
|
|
261
|
+
verifyFrameHash(attestation, expectedFrameHash);
|
|
262
|
+
return attestation.payload;
|
|
263
|
+
}
|
|
264
|
+
async function verifyAttestationV4(blob, publicKeyHex, expectedBoundsHash, expectedContextHash) {
|
|
265
|
+
const attestation = decodeAttestationBlob(blob);
|
|
266
|
+
await verifyAttestationSignature(attestation, publicKeyHex);
|
|
267
|
+
checkAttestationExpiry(attestation.payload);
|
|
268
|
+
verifyBoundsHash(attestation, expectedBoundsHash);
|
|
269
|
+
verifyContextHash(attestation, expectedContextHash);
|
|
270
|
+
return attestation.payload;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/profiles/index.ts
|
|
274
|
+
var PROFILES = {};
|
|
275
|
+
function registerProfile(profileId, profile) {
|
|
276
|
+
PROFILES[profileId] = profile;
|
|
277
|
+
}
|
|
278
|
+
function getProfile(profileId) {
|
|
279
|
+
return PROFILES[profileId];
|
|
280
|
+
}
|
|
281
|
+
function listProfiles() {
|
|
282
|
+
return Object.keys(PROFILES);
|
|
283
|
+
}
|
|
284
|
+
function getAllProfiles() {
|
|
285
|
+
return Object.values(PROFILES);
|
|
286
|
+
}
|
|
287
|
+
function clearProfiles() {
|
|
288
|
+
for (const key of Object.keys(PROFILES)) {
|
|
289
|
+
delete PROFILES[key];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/gatekeeper.ts
|
|
294
|
+
async function verify(request, publicKeyHex, now = Math.floor(Date.now() / 1e3), executionLog) {
|
|
295
|
+
const errors = [];
|
|
296
|
+
const profileId = request.frame.profile;
|
|
297
|
+
if (typeof profileId !== "string") {
|
|
298
|
+
return { approved: false, errors: [{ code: "INVALID_PROFILE", message: "Missing profile in frame" }] };
|
|
299
|
+
}
|
|
300
|
+
const profile = getProfile(profileId);
|
|
301
|
+
if (!profile) {
|
|
302
|
+
return { approved: false, errors: [{ code: "INVALID_PROFILE", message: `Unknown profile: ${profileId}` }] };
|
|
303
|
+
}
|
|
304
|
+
const isV4Profile = !!profile.boundsSchema;
|
|
305
|
+
if (isV4Profile) {
|
|
306
|
+
return verifyV4(request, profile, publicKeyHex, now, executionLog);
|
|
307
|
+
} else {
|
|
308
|
+
return verifyV3(request, profile, publicKeyHex, now, executionLog, errors);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function verifyV3(request, profile, publicKeyHex, now, executionLog, errors) {
|
|
312
|
+
const pathId = request.frame.path;
|
|
313
|
+
if (typeof pathId !== "string") {
|
|
314
|
+
return { approved: false, errors: [{ code: "INVALID_PROFILE", message: "Missing path in frame" }] };
|
|
315
|
+
}
|
|
316
|
+
const executionPath = profile.executionPaths[pathId];
|
|
317
|
+
if (!executionPath) {
|
|
318
|
+
return { approved: false, errors: [{ code: "INVALID_PROFILE", message: `Unknown execution path: ${pathId}` }] };
|
|
319
|
+
}
|
|
320
|
+
let expectedFrameHash;
|
|
321
|
+
try {
|
|
322
|
+
expectedFrameHash = computeFrameHash(request.frame, profile);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
return { approved: false, errors: [{ code: "FRAME_MISMATCH", message: `Frame hash computation failed: ${err}` }] };
|
|
325
|
+
}
|
|
326
|
+
const requiredDomains = executionPath.requiredDomains ?? [];
|
|
327
|
+
const coveredDomains = /* @__PURE__ */ new Set();
|
|
328
|
+
for (const blob of request.attestations) {
|
|
329
|
+
let attestation;
|
|
330
|
+
try {
|
|
331
|
+
attestation = decodeAttestationBlob(blob);
|
|
332
|
+
} catch {
|
|
333
|
+
errors.push({ code: "MALFORMED_ATTESTATION", message: "Failed to decode attestation blob" });
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
await verifyAttestationSignature(attestation, publicKeyHex);
|
|
338
|
+
} catch {
|
|
339
|
+
errors.push({ code: "INVALID_SIGNATURE", message: "Attestation signature verification failed" });
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
verifyFrameHash(attestation, expectedFrameHash);
|
|
344
|
+
} catch {
|
|
345
|
+
errors.push({ code: "FRAME_MISMATCH", message: "Attestation frame_hash does not match computed frame_hash" });
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
checkAttestationExpiry(attestation.payload, now);
|
|
350
|
+
} catch {
|
|
351
|
+
const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
|
|
352
|
+
errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
for (const rd of attestation.payload.resolved_domains) {
|
|
356
|
+
coveredDomains.add(rd.domain);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for (const domain of requiredDomains) {
|
|
360
|
+
if (!coveredDomains.has(domain)) {
|
|
361
|
+
errors.push({ code: "DOMAIN_NOT_COVERED", message: `Required domain "${domain}" not covered by any valid attestation` });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (errors.length > 0) {
|
|
365
|
+
return { approved: false, errors };
|
|
366
|
+
}
|
|
367
|
+
if (executionLog && profile.executionContextSchema?.fields) {
|
|
368
|
+
const cumulativeErrors = resolveCumulativeFields(request, profile, executionLog, now);
|
|
369
|
+
if (cumulativeErrors.length > 0) {
|
|
370
|
+
return { approved: false, errors: cumulativeErrors };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const boundsErrors = checkBoundsFromFrameSchema(request, profile);
|
|
374
|
+
if (boundsErrors.length > 0) {
|
|
375
|
+
return { approved: false, errors: boundsErrors };
|
|
376
|
+
}
|
|
377
|
+
return { approved: true };
|
|
378
|
+
}
|
|
379
|
+
async function verifyV4(request, profile, publicKeyHex, now, executionLog) {
|
|
380
|
+
const errors = [];
|
|
381
|
+
const bounds = request.frame;
|
|
382
|
+
const context = request.context ?? {};
|
|
383
|
+
const pathId = bounds.path;
|
|
384
|
+
if (typeof pathId !== "string") {
|
|
385
|
+
return { approved: false, errors: [{ code: "INVALID_PROFILE", message: "Missing path in bounds" }] };
|
|
386
|
+
}
|
|
387
|
+
const executionPath = profile.executionPaths[pathId];
|
|
388
|
+
if (!executionPath) {
|
|
389
|
+
return { approved: false, errors: [{ code: "INVALID_PROFILE", message: `Unknown execution path: ${pathId}` }] };
|
|
390
|
+
}
|
|
391
|
+
let expectedBoundsHash;
|
|
392
|
+
let expectedContextHash;
|
|
393
|
+
try {
|
|
394
|
+
expectedBoundsHash = computeBoundsHash(bounds, profile);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
return { approved: false, errors: [{ code: "BOUNDS_MISMATCH", message: `Bounds hash computation failed: ${err}` }] };
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
expectedContextHash = computeContextHash(context, profile);
|
|
400
|
+
} catch (err) {
|
|
401
|
+
return { approved: false, errors: [{ code: "CONTEXT_MISMATCH", message: `Context hash computation failed: ${err}` }] };
|
|
402
|
+
}
|
|
403
|
+
const requiredDomains = executionPath.requiredDomains ?? [];
|
|
404
|
+
const coveredDomains = /* @__PURE__ */ new Set();
|
|
405
|
+
for (const blob of request.attestations) {
|
|
406
|
+
let attestation;
|
|
407
|
+
try {
|
|
408
|
+
attestation = decodeAttestationBlob(blob);
|
|
409
|
+
} catch {
|
|
410
|
+
errors.push({ code: "MALFORMED_ATTESTATION", message: "Failed to decode attestation blob" });
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
await verifyAttestationSignature(attestation, publicKeyHex);
|
|
415
|
+
} catch {
|
|
416
|
+
errors.push({ code: "INVALID_SIGNATURE", message: "Attestation signature verification failed" });
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
verifyBoundsHash(attestation, expectedBoundsHash);
|
|
421
|
+
} catch {
|
|
422
|
+
errors.push({ code: "BOUNDS_MISMATCH", message: "Attestation bounds_hash does not match computed bounds_hash" });
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (isV4Attestation(attestation)) {
|
|
426
|
+
try {
|
|
427
|
+
verifyContextHash(attestation, expectedContextHash);
|
|
428
|
+
} catch {
|
|
429
|
+
errors.push({ code: "CONTEXT_MISMATCH", message: "Attestation context_hash does not match computed context_hash" });
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
checkAttestationExpiry(attestation.payload, now);
|
|
435
|
+
} catch {
|
|
436
|
+
const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
|
|
437
|
+
errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
for (const rd of attestation.payload.resolved_domains) {
|
|
441
|
+
coveredDomains.add(rd.domain);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
for (const domain of requiredDomains) {
|
|
445
|
+
if (!coveredDomains.has(domain)) {
|
|
446
|
+
errors.push({ code: "DOMAIN_NOT_COVERED", message: `Required domain "${domain}" not covered by any valid attestation` });
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (errors.length > 0) {
|
|
450
|
+
return { approved: false, errors };
|
|
451
|
+
}
|
|
452
|
+
if (executionLog && profile.executionContextSchema?.fields) {
|
|
453
|
+
const cumulativeErrors = resolveCumulativeFields(request, profile, executionLog, now);
|
|
454
|
+
if (cumulativeErrors.length > 0) {
|
|
455
|
+
return { approved: false, errors: cumulativeErrors };
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const boundsErrors = checkBoundsFromBoundsSchema(request, profile);
|
|
459
|
+
if (boundsErrors.length > 0) {
|
|
460
|
+
return { approved: false, errors: boundsErrors };
|
|
461
|
+
}
|
|
462
|
+
if (profile.contextSchema && Object.keys(profile.contextSchema.fields).length > 0) {
|
|
463
|
+
const contextErrors = checkContextConstraints(context, request.execution, profile);
|
|
464
|
+
if (contextErrors.length > 0) {
|
|
465
|
+
return { approved: false, errors: contextErrors };
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return { approved: true };
|
|
469
|
+
}
|
|
470
|
+
function checkBoundsFromFrameSchema(request, profile) {
|
|
471
|
+
const errors = [];
|
|
472
|
+
if (!profile.frameSchema) return errors;
|
|
473
|
+
for (const [fieldName, fieldDef] of Object.entries(profile.frameSchema.fields)) {
|
|
474
|
+
if (!fieldDef.constraint) continue;
|
|
475
|
+
const constraint = fieldDef.constraint;
|
|
476
|
+
for (const enforceType of constraint.enforceable) {
|
|
477
|
+
if (enforceType === "max") {
|
|
478
|
+
const execField = fieldName.replace(/_max$/, "");
|
|
479
|
+
const boundValue = request.frame[fieldName];
|
|
480
|
+
const actualValue = request.execution[execField];
|
|
481
|
+
if (actualValue === void 0) continue;
|
|
482
|
+
if (typeof boundValue !== "number" || typeof actualValue !== "number") {
|
|
483
|
+
errors.push({
|
|
484
|
+
code: "BOUND_EXCEEDED",
|
|
485
|
+
field: execField,
|
|
486
|
+
message: `Bound check requires numeric values for "${execField}"`,
|
|
487
|
+
bound: boundValue,
|
|
488
|
+
actual: actualValue
|
|
489
|
+
});
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (actualValue > boundValue) {
|
|
493
|
+
errors.push({
|
|
494
|
+
code: "BOUND_EXCEEDED",
|
|
495
|
+
field: execField,
|
|
496
|
+
message: `Value ${actualValue} exceeds authorized maximum of ${boundValue}`,
|
|
497
|
+
bound: boundValue,
|
|
498
|
+
actual: actualValue
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (enforceType === "enum") {
|
|
503
|
+
const boundValue = request.frame[fieldName];
|
|
504
|
+
const actualValue = request.execution[fieldName];
|
|
505
|
+
if (actualValue === void 0) continue;
|
|
506
|
+
const allowed = typeof boundValue === "string" ? boundValue.split(",").map((s) => s.trim()) : [String(boundValue)];
|
|
507
|
+
const actualStr = String(actualValue);
|
|
508
|
+
if (!allowed.includes(actualStr)) {
|
|
509
|
+
errors.push({
|
|
510
|
+
code: "BOUND_EXCEEDED",
|
|
511
|
+
field: fieldName,
|
|
512
|
+
message: `Value "${actualStr}" not in authorized values [${allowed.join(", ")}]`,
|
|
513
|
+
bound: boundValue,
|
|
514
|
+
actual: actualValue
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return errors;
|
|
521
|
+
}
|
|
522
|
+
function checkBoundsFromBoundsSchema(request, profile) {
|
|
523
|
+
const errors = [];
|
|
524
|
+
const bounds = request.frame;
|
|
525
|
+
if (!profile.boundsSchema) return errors;
|
|
526
|
+
for (const [fieldName, fieldDef] of Object.entries(profile.boundsSchema.fields)) {
|
|
527
|
+
if (!fieldDef.constraint) continue;
|
|
528
|
+
const constraint = fieldDef.constraint;
|
|
529
|
+
for (const enforceType of constraint.enforceable) {
|
|
530
|
+
if (enforceType === "max") {
|
|
531
|
+
const execField = fieldName.replace(/_max$/, "");
|
|
532
|
+
const boundValue = bounds[fieldName];
|
|
533
|
+
const actualValue = request.execution[execField];
|
|
534
|
+
if (actualValue === void 0) continue;
|
|
535
|
+
if (typeof boundValue !== "number" || typeof actualValue !== "number") {
|
|
536
|
+
errors.push({
|
|
537
|
+
code: "BOUND_EXCEEDED",
|
|
538
|
+
field: execField,
|
|
539
|
+
message: `Bound check requires numeric values for "${execField}"`,
|
|
540
|
+
bound: boundValue,
|
|
541
|
+
actual: actualValue
|
|
542
|
+
});
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (actualValue > boundValue) {
|
|
546
|
+
errors.push({
|
|
547
|
+
code: "BOUND_EXCEEDED",
|
|
548
|
+
field: execField,
|
|
549
|
+
message: `Value ${actualValue} exceeds authorized maximum of ${boundValue}`,
|
|
550
|
+
bound: boundValue,
|
|
551
|
+
actual: actualValue
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return errors;
|
|
558
|
+
}
|
|
559
|
+
function checkContextConstraints(context, execution, profile) {
|
|
560
|
+
const errors = [];
|
|
561
|
+
if (!profile.contextSchema) return errors;
|
|
562
|
+
for (const [fieldName, fieldDef] of Object.entries(profile.contextSchema.fields)) {
|
|
563
|
+
if (!fieldDef.constraint) continue;
|
|
564
|
+
for (const enforceType of fieldDef.constraint.enforceable) {
|
|
565
|
+
if (enforceType === "enum") {
|
|
566
|
+
const boundValue = context[fieldName];
|
|
567
|
+
const actualValue = execution[fieldName];
|
|
568
|
+
if (actualValue === void 0) continue;
|
|
569
|
+
const allowed = typeof boundValue === "string" ? boundValue.split(",").map((s) => s.trim()) : [String(boundValue)];
|
|
570
|
+
const actualStr = String(actualValue);
|
|
571
|
+
if (!allowed.includes(actualStr)) {
|
|
572
|
+
errors.push({
|
|
573
|
+
code: "BOUND_EXCEEDED",
|
|
574
|
+
field: fieldName,
|
|
575
|
+
message: `Value "${actualStr}" not in authorized context values [${allowed.join(", ")}]`,
|
|
576
|
+
bound: boundValue,
|
|
577
|
+
actual: actualValue
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (enforceType === "subset") {
|
|
582
|
+
const boundValue = context[fieldName];
|
|
583
|
+
const actualValue = execution[fieldName];
|
|
584
|
+
if (boundValue === void 0 || boundValue === "") continue;
|
|
585
|
+
if (actualValue === void 0 || actualValue === "") continue;
|
|
586
|
+
const allowed = String(boundValue).split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
587
|
+
const actuals = String(actualValue).split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
588
|
+
const disallowed = actuals.filter((v) => !allowed.includes(v));
|
|
589
|
+
if (disallowed.length > 0) {
|
|
590
|
+
errors.push({
|
|
591
|
+
code: "BOUND_EXCEEDED",
|
|
592
|
+
field: fieldName,
|
|
593
|
+
message: `Values [${disallowed.join(", ")}] not in authorized set [${allowed.join(", ")}]`,
|
|
594
|
+
bound: boundValue,
|
|
595
|
+
actual: actualValue
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return errors;
|
|
602
|
+
}
|
|
603
|
+
function resolveCumulativeFields(request, profile, executionLog, now) {
|
|
604
|
+
const errors = [];
|
|
605
|
+
const profileId = String(request.frame.profile);
|
|
606
|
+
const path = String(request.frame.path);
|
|
607
|
+
const boundsOrFrame = request.frame;
|
|
608
|
+
for (const [fieldName, fieldDef] of Object.entries(profile.executionContextSchema.fields)) {
|
|
609
|
+
if (fieldDef.source !== "cumulative") continue;
|
|
610
|
+
const cumDef = fieldDef;
|
|
611
|
+
const { cumulativeField, window: windowType } = cumDef;
|
|
612
|
+
const runningTotal = executionLog.sumByWindow(profileId, path, cumulativeField, windowType, now);
|
|
613
|
+
let currentContribution;
|
|
614
|
+
if (cumulativeField === "_count") {
|
|
615
|
+
currentContribution = 1;
|
|
616
|
+
} else {
|
|
617
|
+
const val = request.execution[cumulativeField];
|
|
618
|
+
currentContribution = typeof val === "number" ? val : val !== void 0 ? Number(val) : 0;
|
|
619
|
+
}
|
|
620
|
+
const cumulativeValue = runningTotal + currentContribution;
|
|
621
|
+
request.execution[fieldName] = cumulativeValue;
|
|
622
|
+
const boundFieldName = fieldName + "_max";
|
|
623
|
+
const boundValue = boundsOrFrame[boundFieldName];
|
|
624
|
+
if (boundValue === void 0) continue;
|
|
625
|
+
if (typeof boundValue !== "number") {
|
|
626
|
+
errors.push({
|
|
627
|
+
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
628
|
+
field: fieldName,
|
|
629
|
+
message: `Cumulative bound requires numeric value for "${boundFieldName}"`,
|
|
630
|
+
bound: boundValue,
|
|
631
|
+
actual: cumulativeValue
|
|
632
|
+
});
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
if (cumulativeValue > boundValue) {
|
|
636
|
+
errors.push({
|
|
637
|
+
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
638
|
+
field: fieldName,
|
|
639
|
+
message: `Cumulative ${windowType} value ${cumulativeValue} exceeds limit of ${boundValue}`,
|
|
640
|
+
bound: boundValue,
|
|
641
|
+
actual: cumulativeValue
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return errors;
|
|
646
|
+
}
|
|
647
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
648
|
+
0 && (module.exports = {
|
|
649
|
+
attestationId,
|
|
650
|
+
canonicalBounds,
|
|
651
|
+
canonicalContext,
|
|
652
|
+
canonicalFrame,
|
|
653
|
+
checkAttestationExpiry,
|
|
654
|
+
clearProfiles,
|
|
655
|
+
computeBoundsHash,
|
|
656
|
+
computeContextHash,
|
|
657
|
+
computeFrameHash,
|
|
658
|
+
decodeAttestationBlob,
|
|
659
|
+
encodeAttestationBlob,
|
|
660
|
+
frameHash,
|
|
661
|
+
getAllProfiles,
|
|
662
|
+
getProfile,
|
|
663
|
+
isV4Attestation,
|
|
664
|
+
listProfiles,
|
|
665
|
+
registerProfile,
|
|
666
|
+
validateBoundsParams,
|
|
667
|
+
validateContextParams,
|
|
668
|
+
validateFrameParams,
|
|
669
|
+
verify,
|
|
670
|
+
verifyAttestation,
|
|
671
|
+
verifyAttestationSignature,
|
|
672
|
+
verifyAttestationV4,
|
|
673
|
+
verifyBoundsHash,
|
|
674
|
+
verifyContextHash,
|
|
675
|
+
verifyFrameHash
|
|
676
|
+
});
|