@suveren/gateway 0.19.5 → 0.20.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/content/integrations/crm.json +1 -1
- package/content/integrations/deploy-github.json +1 -1
- package/content/integrations/erp.json +1 -1
- package/content/integrations/mail.json +1 -1
- package/content/integrations/records.json +1 -1
- package/dist/control-plane/index.mjs +21 -13
- package/dist/mcp-server/{chunk-ZRRZFGRL.mjs → chunk-APIQIEUA.mjs} +39 -27
- package/dist/mcp-server/http.mjs +308 -97
- package/dist/mcp-server/report-verify-cli.mjs +1 -1
- package/dist/ui/assets/index-1gCMlbLl.js +116 -0
- package/dist/ui/index.html +1 -1
- package/node_modules/@hap/core/README.md +92 -29
- package/node_modules/@hap/core/dist/index.d.mts +614 -593
- package/node_modules/@hap/core/dist/index.d.ts +614 -593
- package/node_modules/@hap/core/dist/index.js +774 -749
- package/node_modules/@hap/core/dist/index.mjs +744 -724
- package/node_modules/@hap/core/package.json +1 -1
- package/node_modules/@hap/core/src/base64url.ts +39 -0
- package/node_modules/@hap/core/src/commitment-modes.ts +0 -21
- package/node_modules/@hap/core/src/errors.ts +76 -0
- package/node_modules/@hap/core/src/frame.ts +47 -178
- package/node_modules/@hap/core/src/gatekeeper.ts +219 -600
- package/node_modules/@hap/core/src/identity.ts +9 -12
- package/node_modules/@hap/core/src/index.ts +8 -4
- package/node_modules/@hap/core/src/mandate.ts +177 -166
- package/node_modules/@hap/core/src/owner-signature.ts +282 -0
- package/node_modules/@hap/core/src/profile.ts +234 -0
- package/node_modules/@hap/core/src/ticket.ts +148 -0
- package/node_modules/@hap/core/src/types.ts +191 -344
- package/node_modules/@hap/core/src/versions.ts +47 -0
- package/package.json +2 -2
- package/profiles/calendar/0.6.profile.json +191 -0
- package/profiles/charge/0.6.profile.json +211 -0
- package/profiles/customers/0.9.profile.json +182 -0
- package/profiles/delegation/0.3.profile.json +118 -0
- package/profiles/deploy/0.11.profile.json +177 -0
- package/profiles/email/0.8.profile.json +226 -0
- package/profiles/index.json +12 -1
- package/profiles/publish/0.6.profile.json +187 -0
- package/profiles/purchase/0.7.profile.json +235 -0
- package/profiles/records/0.6.profile.json +118 -0
- package/profiles/reporting/0.3.profile.json +98 -0
- package/profiles/sales/0.4.profile.json +291 -0
- package/dist/ui/assets/index-Du68zJhH.js +0 -116
- package/node_modules/@hap/core/src/attestation.ts +0 -173
- package/node_modules/@hap/core/src/receipt.ts +0 -74
|
@@ -32,65 +32,70 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
CanonicalValueError: () => CanonicalValueError,
|
|
34
34
|
ContentBindingError: () => ContentBindingError,
|
|
35
|
-
|
|
35
|
+
HapError: () => HapError,
|
|
36
|
+
MANDATE_ERROR_CODES: () => MANDATE_ERROR_CODES,
|
|
37
|
+
OwnerSignatureError: () => OwnerSignatureError,
|
|
38
|
+
PROTOCOL_VERSION: () => PROTOCOL_VERSION,
|
|
36
39
|
SIGNABLE_COMMITMENT_MODES: () => SIGNABLE_COMMITMENT_MODES,
|
|
40
|
+
SUPPORTED_VERSIONS: () => SUPPORTED_VERSIONS,
|
|
41
|
+
TICKET_ERROR_CODES: () => TICKET_ERROR_CODES,
|
|
37
42
|
allowedCommitmentModes: () => allowedCommitmentModes,
|
|
38
43
|
approvalSigningBytes: () => approvalSigningBytes,
|
|
39
|
-
attestationId: () => attestationId,
|
|
40
44
|
bindingAppliesTo: () => bindingAppliesTo,
|
|
41
45
|
boundActionTypes: () => boundActionTypes,
|
|
42
46
|
buildMandateProjection: () => buildMandateProjection,
|
|
43
47
|
canonicalBounds: () => canonicalBounds,
|
|
44
|
-
|
|
45
|
-
canonicalFrame: () => canonicalFrame,
|
|
48
|
+
canonicalScope: () => canonicalScope,
|
|
46
49
|
canonicalize: () => canonicalize,
|
|
47
50
|
canonicalizeText: () => canonicalizeText,
|
|
48
|
-
|
|
51
|
+
checkDiscloseSubset: () => checkDiscloseSubset,
|
|
52
|
+
checkMandateExpiry: () => checkMandateExpiry,
|
|
53
|
+
checkOwnerSignatureRequirement: () => checkOwnerSignatureRequirement,
|
|
49
54
|
clearProfiles: () => clearProfiles,
|
|
50
55
|
computeBoundsHash: () => computeBoundsHash,
|
|
51
56
|
computeContentHash: () => computeContentHash,
|
|
52
|
-
computeContextHash: () => computeContextHash,
|
|
53
57
|
computeFieldsContentHash: () => computeFieldsContentHash,
|
|
54
|
-
computeFrameHash: () => computeFrameHash,
|
|
55
58
|
computeIntentDisclosureHash: () => computeIntentDisclosureHash,
|
|
56
59
|
computeIntentHash: () => computeIntentHash,
|
|
60
|
+
computeProfileHash: () => computeProfileHash,
|
|
61
|
+
computeScopeHash: () => computeScopeHash,
|
|
57
62
|
contentCanonicalBytes: () => contentCanonicalBytes,
|
|
58
|
-
decodeAttestationBlob: () => decodeAttestationBlob,
|
|
59
63
|
decodeDidKey: () => decodeDidKey,
|
|
64
|
+
decodeMandateBlob: () => decodeMandateBlob,
|
|
60
65
|
deriveIdentityLine: () => deriveIdentityLine,
|
|
61
|
-
encodeAttestationBlob: () => encodeAttestationBlob,
|
|
62
66
|
encodeDidKey: () => encodeDidKey,
|
|
63
|
-
|
|
67
|
+
encodeMandateBlob: () => encodeMandateBlob,
|
|
68
|
+
fromBase64Url: () => fromBase64Url,
|
|
64
69
|
getAllProfiles: () => getAllProfiles,
|
|
65
70
|
getProfile: () => getProfile,
|
|
66
71
|
intentDisclosureCanonicalBytes: () => intentDisclosureCanonicalBytes,
|
|
67
72
|
isCommitmentModeAllowed: () => isCommitmentModeAllowed,
|
|
68
73
|
isFieldBinding: () => isFieldBinding,
|
|
69
74
|
isKeyBearingDid: () => isKeyBearingDid,
|
|
70
|
-
isV4Attestation: () => isV4Attestation,
|
|
71
75
|
listProfiles: () => listProfiles,
|
|
72
|
-
|
|
76
|
+
mandateBlobId: () => mandateBlobId,
|
|
77
|
+
negotiateVersion: () => negotiateVersion,
|
|
78
|
+
projectionSigningBytes: () => projectionSigningBytes,
|
|
79
|
+
publicTicketView: () => publicTicketView,
|
|
73
80
|
registerProfile: () => registerProfile,
|
|
74
81
|
selectBoundFields: () => selectBoundFields,
|
|
75
82
|
signApproval: () => signApproval,
|
|
83
|
+
signMandate: () => signMandate,
|
|
76
84
|
signMandateProjection: () => signMandateProjection,
|
|
85
|
+
signTicket: () => signTicket,
|
|
86
|
+
toBase64Url: () => toBase64Url,
|
|
77
87
|
validateBoundsParams: () => validateBoundsParams,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
validateFrameParams: () => validateFrameParams,
|
|
88
|
+
validateMandateOwners: () => validateMandateOwners,
|
|
89
|
+
validateProfile: () => validateProfile,
|
|
90
|
+
validateScopeParams: () => validateScopeParams,
|
|
82
91
|
validateSubject: () => validateSubject,
|
|
83
92
|
verify: () => verify,
|
|
84
93
|
verifyApproval: () => verifyApproval,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
verifyFrameHash: () => verifyFrameHash,
|
|
91
|
-
verifyOwnerMandate: () => verifyOwnerMandate,
|
|
92
|
-
verifyOwnerMandates: () => verifyOwnerMandates,
|
|
93
|
-
verifyReceiptSignature: () => verifyReceiptSignature
|
|
94
|
+
verifyMandate: () => verifyMandate,
|
|
95
|
+
verifyMandateSignature: () => verifyMandateSignature,
|
|
96
|
+
verifyOwnerSignature: () => verifyOwnerSignature,
|
|
97
|
+
verifyOwnerSignatures: () => verifyOwnerSignatures,
|
|
98
|
+
verifyTicketSignature: () => verifyTicketSignature
|
|
94
99
|
});
|
|
95
100
|
module.exports = __toCommonJS(index_exports);
|
|
96
101
|
|
|
@@ -119,6 +124,88 @@ function canonicalize(value) {
|
|
|
119
124
|
return "{" + parts.join(",") + "}";
|
|
120
125
|
}
|
|
121
126
|
|
|
127
|
+
// src/base64url.ts
|
|
128
|
+
var STRICT_B64URL = /^[A-Za-z0-9_-]*$/;
|
|
129
|
+
function toBase64Url(bytes) {
|
|
130
|
+
return Buffer.from(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
131
|
+
}
|
|
132
|
+
function fromBase64Url(s) {
|
|
133
|
+
if (!STRICT_B64URL.test(s)) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`${JSON.stringify(s)} is not strict base64url \u2014 standard base64 ("+"/"/"/padding) is retired (protocol.md \u2192 Mandate Payload, rule 3).`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const padding = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
|
|
139
|
+
return new Uint8Array(Buffer.from(s + padding, "base64"));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/errors.ts
|
|
143
|
+
var MANDATE_ERROR_CODES = [
|
|
144
|
+
"BOUNDS_HASH_MISMATCH",
|
|
145
|
+
"SCOPE_HASH_MISMATCH",
|
|
146
|
+
"INVALID_SIGNATURE",
|
|
147
|
+
"OWNER_NOT_COVERED",
|
|
148
|
+
"TTL_EXPIRED",
|
|
149
|
+
"PROFILE_NOT_FOUND",
|
|
150
|
+
"PROFILE_INVALID",
|
|
151
|
+
"PROFILE_HASH_MISMATCH",
|
|
152
|
+
"COVERAGE_INSUFFICIENT",
|
|
153
|
+
"MALFORMED_MANDATE",
|
|
154
|
+
"VERSION_UNSUPPORTED",
|
|
155
|
+
"OWNER_NOT_APPROVER",
|
|
156
|
+
"BOUNDS_INVALID_VALUE",
|
|
157
|
+
"SCOPE_INVALID_VALUE",
|
|
158
|
+
"ABOVE_CAP_CONFIG_INVALID",
|
|
159
|
+
"OWNER_SIGNATURE_REQUIRED",
|
|
160
|
+
"OWNER_SIGNATURE_INVALID"
|
|
161
|
+
];
|
|
162
|
+
var TICKET_ERROR_CODES = [
|
|
163
|
+
"MANDATE_NOT_FOUND",
|
|
164
|
+
"MANDATE_EXPIRED",
|
|
165
|
+
"MANDATE_REVOKED",
|
|
166
|
+
"BOUND_EXCEEDED",
|
|
167
|
+
"CUMULATIVE_LIMIT_EXCEEDED",
|
|
168
|
+
"COVERAGE_INSUFFICIENT",
|
|
169
|
+
"PROFILE_NOT_FOUND",
|
|
170
|
+
"INVALID_ACTION_TYPE",
|
|
171
|
+
"APPROVAL_REQUIRED",
|
|
172
|
+
"PROPOSAL_REQUIRED",
|
|
173
|
+
"PROPOSAL_NOT_FOUND",
|
|
174
|
+
"PROPOSAL_NOT_APPROVED",
|
|
175
|
+
"PROPOSAL_EXPIRED",
|
|
176
|
+
"PROPOSAL_REJECTED",
|
|
177
|
+
"PROPOSAL_MISMATCH",
|
|
178
|
+
"PROPOSAL_MANDATE_MISMATCH",
|
|
179
|
+
"PROPOSAL_ALREADY_EXECUTED",
|
|
180
|
+
"MALFORMED_TICKET_REQUEST",
|
|
181
|
+
"IDEMPOTENCY_KEY_REQUIRED",
|
|
182
|
+
"IDEMPOTENCY_MISMATCH",
|
|
183
|
+
"APPROVAL_SIGNATURE_REQUIRED",
|
|
184
|
+
"APPROVAL_SIGNATURE_INVALID"
|
|
185
|
+
];
|
|
186
|
+
var HapError = class extends Error {
|
|
187
|
+
constructor(code, message) {
|
|
188
|
+
super(`${code}: ${message}`);
|
|
189
|
+
this.code = code;
|
|
190
|
+
this.name = "HapError";
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// src/versions.ts
|
|
195
|
+
var PROTOCOL_VERSION = "0.7";
|
|
196
|
+
var SUPPORTED_VERSIONS = ["0.7"];
|
|
197
|
+
function negotiateVersion(requested, supported = SUPPORTED_VERSIONS) {
|
|
198
|
+
const requestedVersions = requested ?? ["0.6"];
|
|
199
|
+
const common = supported.filter((v) => requestedVersions.includes(v)).sort().reverse();
|
|
200
|
+
if (common.length === 0) {
|
|
201
|
+
throw new HapError(
|
|
202
|
+
"VERSION_UNSUPPORTED",
|
|
203
|
+
`no protocol version in [${requestedVersions.join(", ")}] is supported by this implementation (supports [${supported.join(", ")}])`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return common[0];
|
|
207
|
+
}
|
|
208
|
+
|
|
122
209
|
// src/content-binding.ts
|
|
123
210
|
var import_crypto = require("crypto");
|
|
124
211
|
function canonicalizeText(input) {
|
|
@@ -262,9 +349,9 @@ function validateSubject(subject, opts) {
|
|
|
262
349
|
} else if (method === "eudi") {
|
|
263
350
|
if (trust_root !== "external") errors.push('eudi requires trust_root "external"');
|
|
264
351
|
if (!verifier) errors.push("eudi requires a verifier");
|
|
265
|
-
if (opts?.
|
|
266
|
-
const entry = opts.
|
|
267
|
-
if (!entry) errors.push('eudi requires
|
|
352
|
+
if (opts?.mandateOwners !== void 0) {
|
|
353
|
+
const entry = opts.mandateOwners.find((m) => m.did === did && m.binding === "eudi");
|
|
354
|
+
if (!entry) errors.push('eudi requires a mandate_owners entry with binding "eudi" for this DID');
|
|
268
355
|
}
|
|
269
356
|
} else {
|
|
270
357
|
errors.push(`high assurance requires method "as_vouched" or "eudi", got "${method}"`);
|
|
@@ -288,48 +375,6 @@ function deriveIdentityLine(subject, { operatorName }) {
|
|
|
288
375
|
|
|
289
376
|
// src/frame.ts
|
|
290
377
|
var import_crypto3 = require("crypto");
|
|
291
|
-
function validateFrameParams(params, profile) {
|
|
292
|
-
const errors = [];
|
|
293
|
-
if (!profile.frameSchema) {
|
|
294
|
-
return { valid: false, errors: ["Profile does not have a frameSchema"] };
|
|
295
|
-
}
|
|
296
|
-
for (const [fieldName, fieldDef] of Object.entries(profile.frameSchema.fields)) {
|
|
297
|
-
if (fieldDef.required && !(fieldName in params)) {
|
|
298
|
-
errors.push(`Missing required field: ${fieldName}`);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
for (const [field, value] of Object.entries(params)) {
|
|
302
|
-
const fieldDef = profile.frameSchema.fields[field];
|
|
303
|
-
if (!fieldDef) {
|
|
304
|
-
errors.push(`Unknown field "${field}" not defined in profile ${profile.id}`);
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
if (fieldDef.type === "number" && typeof value !== "number") {
|
|
308
|
-
errors.push(`Field "${field}" must be a number, got ${typeof value}`);
|
|
309
|
-
}
|
|
310
|
-
if (fieldDef.type === "string" && typeof value !== "string") {
|
|
311
|
-
errors.push(`Field "${field}" must be a string, got ${typeof value}`);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
return { valid: errors.length === 0, errors };
|
|
315
|
-
}
|
|
316
|
-
function canonicalFrame(params, profile) {
|
|
317
|
-
const validation = validateFrameParams(params, profile);
|
|
318
|
-
if (!validation.valid) {
|
|
319
|
-
throw new Error(`Invalid frame parameters: ${validation.errors.join("; ")}`);
|
|
320
|
-
}
|
|
321
|
-
const lines = profile.frameSchema.keyOrder.map(
|
|
322
|
-
(key) => `${key}=${String(params[key])}`
|
|
323
|
-
);
|
|
324
|
-
return lines.join("\n");
|
|
325
|
-
}
|
|
326
|
-
function frameHash(canonicalFrameString) {
|
|
327
|
-
const hash = (0, import_crypto3.createHash)("sha256").update(canonicalFrameString, "utf8").digest("hex");
|
|
328
|
-
return `sha256:${hash}`;
|
|
329
|
-
}
|
|
330
|
-
function computeFrameHash(params, profile) {
|
|
331
|
-
return frameHash(canonicalFrame(params, profile));
|
|
332
|
-
}
|
|
333
378
|
function validateBoundsParams(params, profile) {
|
|
334
379
|
const errors = [];
|
|
335
380
|
if (!profile.boundsSchema) {
|
|
@@ -355,23 +400,23 @@ function validateBoundsParams(params, profile) {
|
|
|
355
400
|
}
|
|
356
401
|
return { valid: errors.length === 0, errors };
|
|
357
402
|
}
|
|
358
|
-
function
|
|
403
|
+
function validateScopeParams(params, profile) {
|
|
359
404
|
const errors = [];
|
|
360
|
-
if (!profile.
|
|
405
|
+
if (!profile.scopeSchema) {
|
|
361
406
|
if (Object.keys(params).length > 0) {
|
|
362
|
-
errors.push("Profile does not have a
|
|
407
|
+
errors.push("Profile does not have a scopeSchema but scope params were provided");
|
|
363
408
|
}
|
|
364
409
|
return { valid: errors.length === 0, errors };
|
|
365
410
|
}
|
|
366
|
-
for (const [fieldName, fieldDef] of Object.entries(profile.
|
|
411
|
+
for (const [fieldName, fieldDef] of Object.entries(profile.scopeSchema.fields)) {
|
|
367
412
|
if (fieldDef.required && !(fieldName in params)) {
|
|
368
413
|
errors.push(`Missing required field: ${fieldName}`);
|
|
369
414
|
}
|
|
370
415
|
}
|
|
371
416
|
for (const [field, value] of Object.entries(params)) {
|
|
372
|
-
const fieldDef = profile.
|
|
417
|
+
const fieldDef = profile.scopeSchema.fields[field];
|
|
373
418
|
if (!fieldDef) {
|
|
374
|
-
errors.push(`Unknown field "${field}" not defined in
|
|
419
|
+
errors.push(`Unknown field "${field}" not defined in scopeSchema of profile ${profile.id}`);
|
|
375
420
|
continue;
|
|
376
421
|
}
|
|
377
422
|
if (fieldDef.type === "number" && typeof value !== "number") {
|
|
@@ -383,38 +428,6 @@ function validateContextParams(params, profile) {
|
|
|
383
428
|
}
|
|
384
429
|
return { valid: errors.length === 0, errors };
|
|
385
430
|
}
|
|
386
|
-
function validateBoundsRequiredFor(profile) {
|
|
387
|
-
const errors = [];
|
|
388
|
-
const boundsSchema = profile.boundsSchema;
|
|
389
|
-
if (!boundsSchema) return errors;
|
|
390
|
-
const actionTypes = boundsSchema.actionTypes ? [...boundsSchema.actionTypes] : void 0;
|
|
391
|
-
for (const [fieldName, fieldDef] of Object.entries(boundsSchema.fields)) {
|
|
392
|
-
const bt = fieldDef.boundType;
|
|
393
|
-
if (!bt) continue;
|
|
394
|
-
const requiredFor = bt.requiredFor;
|
|
395
|
-
if (requiredFor === void 0) continue;
|
|
396
|
-
if (!Array.isArray(requiredFor) || !requiredFor.every((v) => typeof v === "string")) {
|
|
397
|
-
errors.push(`Bound "${fieldName}": requiredFor must be a string array.`);
|
|
398
|
-
continue;
|
|
399
|
-
}
|
|
400
|
-
if (bt.kind !== "per_transaction") {
|
|
401
|
-
errors.push(
|
|
402
|
-
`Bound "${fieldName}": requiredFor is only meaningful on a per_transaction bound (found on kind "${bt.kind}") \u2014 every enforcement point ignores it there. Remove it.`
|
|
403
|
-
);
|
|
404
|
-
continue;
|
|
405
|
-
}
|
|
406
|
-
if (actionTypes) {
|
|
407
|
-
for (const actionType of requiredFor) {
|
|
408
|
-
if (!actionTypes.includes(actionType)) {
|
|
409
|
-
errors.push(
|
|
410
|
-
`Bound "${fieldName}": requiredFor names action type "${actionType}", which is not in this profile's boundsSchema.actionTypes registry [${actionTypes.join(", ")}]. It can never match a real execution and the requirement is dead.`
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
return errors;
|
|
417
|
-
}
|
|
418
431
|
var CanonicalValueError = class extends Error {
|
|
419
432
|
code;
|
|
420
433
|
field;
|
|
@@ -461,23 +474,23 @@ function canonicalBounds(params, profile) {
|
|
|
461
474
|
}
|
|
462
475
|
return canonicalRecords(params, profile.boundsSchema.keyOrder, "BOUNDS_INVALID_VALUE");
|
|
463
476
|
}
|
|
464
|
-
function
|
|
465
|
-
if (!profile.
|
|
477
|
+
function canonicalScope(params, profile) {
|
|
478
|
+
if (!profile.scopeSchema || Object.keys(profile.scopeSchema.fields).length === 0) {
|
|
466
479
|
return "";
|
|
467
480
|
}
|
|
468
|
-
const validation =
|
|
481
|
+
const validation = validateScopeParams(params, profile);
|
|
469
482
|
if (!validation.valid) {
|
|
470
|
-
throw new Error(`Invalid
|
|
483
|
+
throw new Error(`Invalid scope parameters: ${validation.errors.join("; ")}`);
|
|
471
484
|
}
|
|
472
|
-
return canonicalRecords(params, profile.
|
|
485
|
+
return canonicalRecords(params, profile.scopeSchema.keyOrder, "SCOPE_INVALID_VALUE");
|
|
473
486
|
}
|
|
474
487
|
function computeBoundsHash(params, profile) {
|
|
475
488
|
const canonical = canonicalBounds(params, profile);
|
|
476
489
|
const hash = (0, import_crypto3.createHash)("sha256").update(canonical, "utf8").digest("hex");
|
|
477
490
|
return `sha256:${hash}`;
|
|
478
491
|
}
|
|
479
|
-
function
|
|
480
|
-
const canonical =
|
|
492
|
+
function computeScopeHash(params, profile) {
|
|
493
|
+
const canonical = canonicalScope(params, profile);
|
|
481
494
|
const hash = (0, import_crypto3.createHash)("sha256").update(canonical, "utf8").digest("hex");
|
|
482
495
|
return `sha256:${hash}`;
|
|
483
496
|
}
|
|
@@ -494,361 +507,672 @@ function allowedCommitmentModes(profile) {
|
|
|
494
507
|
function isCommitmentModeAllowed(profile, mode) {
|
|
495
508
|
return isSignable(mode) && allowedCommitmentModes(profile).includes(mode);
|
|
496
509
|
}
|
|
497
|
-
function validateCommitmentModes(profile) {
|
|
498
|
-
const declared = profile.commitment_modes;
|
|
499
|
-
if (declared === void 0) return [];
|
|
500
|
-
if (!Array.isArray(declared)) return ["commitment_modes must be a list of modes."];
|
|
501
|
-
const errors = [];
|
|
502
|
-
if (declared.length === 0) errors.push("commitment_modes is empty \u2014 no mandate could ever be signed under this profile.");
|
|
503
|
-
const seen = /* @__PURE__ */ new Set();
|
|
504
|
-
for (const m of declared) {
|
|
505
|
-
if (!isSignable(m)) errors.push(`commitment_modes: "${String(m)}" is not a mode a signer can choose (${SIGNABLE_COMMITMENT_MODES.join(", ")}).`);
|
|
506
|
-
if (seen.has(m)) errors.push(`commitment_modes: "${String(m)}" appears twice.`);
|
|
507
|
-
seen.add(m);
|
|
508
|
-
}
|
|
509
|
-
return errors;
|
|
510
|
-
}
|
|
511
510
|
|
|
512
|
-
// src/
|
|
511
|
+
// src/profile.ts
|
|
513
512
|
var import_crypto4 = require("crypto");
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
try {
|
|
517
|
-
const base64 = blob.replace(/-/g, "+").replace(/_/g, "/");
|
|
518
|
-
const padding = base64.length % 4 === 0 ? "" : "=".repeat(4 - base64.length % 4);
|
|
519
|
-
const json = Buffer.from(base64 + padding, "base64").toString("utf8");
|
|
520
|
-
return JSON.parse(json);
|
|
521
|
-
} catch {
|
|
522
|
-
throw new Error("MALFORMED_ATTESTATION: Failed to decode attestation blob");
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
function encodeAttestationBlob(attestation) {
|
|
526
|
-
const json = JSON.stringify(attestation);
|
|
527
|
-
const base64 = Buffer.from(json, "utf8").toString("base64");
|
|
528
|
-
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
529
|
-
}
|
|
530
|
-
function attestationId(blob) {
|
|
531
|
-
const hash = (0, import_crypto4.createHash)("sha256").update(blob, "utf8").digest("hex");
|
|
513
|
+
function computeProfileHash(parsed) {
|
|
514
|
+
const hash = (0, import_crypto4.createHash)("sha256").update(canonicalize(parsed), "utf8").digest("hex");
|
|
532
515
|
return `sha256:${hash}`;
|
|
533
516
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
517
|
+
var RETIRED_PROFILE_KEYS = {
|
|
518
|
+
contextSchema: "scopeSchema",
|
|
519
|
+
ownerMandate: "ownerSignature",
|
|
520
|
+
receipt_lookup: "ticket_lookup",
|
|
521
|
+
frameSchema: "(removed \u2014 v0.3 execution frames are retired)"
|
|
522
|
+
};
|
|
523
|
+
function validateProfile(profile) {
|
|
524
|
+
const errors = [];
|
|
525
|
+
const p = profile;
|
|
526
|
+
for (const [retired, current] of Object.entries(RETIRED_PROFILE_KEYS)) {
|
|
527
|
+
if (retired in p) {
|
|
528
|
+
errors.push({
|
|
529
|
+
code: "PROFILE_INVALID",
|
|
530
|
+
field: retired,
|
|
531
|
+
message: `"${retired}" is retired in v0.7 \u2014 use "${current}" instead.`
|
|
532
|
+
});
|
|
543
533
|
}
|
|
544
|
-
} catch (error) {
|
|
545
|
-
if (error instanceof Error && error.message.startsWith("INVALID_SIGNATURE")) throw error;
|
|
546
|
-
throw new Error(`INVALID_SIGNATURE: Signature verification error: ${error}`);
|
|
547
534
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
);
|
|
535
|
+
if (Array.isArray(p.requiredGates) && p.requiredGates.includes("decision_owner")) {
|
|
536
|
+
errors.push({
|
|
537
|
+
code: "PROFILE_INVALID",
|
|
538
|
+
field: "requiredGates",
|
|
539
|
+
message: '"decision_owner" is retired in v0.7 \u2014 the required gate is "mandate_owner".'
|
|
540
|
+
});
|
|
554
541
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
542
|
+
const boundsSchema = profile.boundsSchema;
|
|
543
|
+
if (boundsSchema) {
|
|
544
|
+
const actionTypes = boundsSchema.actionTypes ? [...boundsSchema.actionTypes] : void 0;
|
|
545
|
+
for (const [fieldName, fieldDef] of Object.entries(boundsSchema.fields)) {
|
|
546
|
+
if (fieldName === "profile") continue;
|
|
547
|
+
const routing = ["path", "paths"].filter((k) => k in fieldDef);
|
|
548
|
+
if (routing.length > 0) {
|
|
549
|
+
errors.push({
|
|
550
|
+
code: "PROFILE_INVALID",
|
|
551
|
+
field: fieldName,
|
|
552
|
+
message: `Bounds field "${fieldName}" declares ${routing.map((k) => `"${k}"`).join(" and ")} \u2014 action-routing arrays are forbidden (Bounds Schema rule 6); use actionType and appliesTo.`
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
if (!fieldDef.boundType) {
|
|
556
|
+
errors.push({
|
|
557
|
+
code: "PROFILE_INVALID",
|
|
558
|
+
field: fieldName,
|
|
559
|
+
message: `Bounds field "${fieldName}" has no boundType \u2014 enforcement semantics undefined (Bounds Schema rule 2).`
|
|
560
|
+
});
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
const bt = fieldDef.boundType;
|
|
564
|
+
if (fieldDef.appliesTo && (bt.kind === "per_transaction" || bt.kind === "enum")) {
|
|
565
|
+
errors.push({
|
|
566
|
+
code: "PROFILE_INVALID",
|
|
567
|
+
field: fieldName,
|
|
568
|
+
message: `Bounds field "${fieldName}": appliesTo MUST NOT be declared on a ${bt.kind} bound (Bounds Schema rule 7).`
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
if (bt.kind === "cumulative_count" && !fieldDef.appliesTo?.length) {
|
|
572
|
+
errors.push({
|
|
573
|
+
code: "PROFILE_INVALID",
|
|
574
|
+
field: fieldName,
|
|
575
|
+
message: `Bounds field "${fieldName}": a cumulative_count bound on a v0.7+ profile MUST declare appliesTo (Bounds Schema rule 7) \u2014 absence would silently throttle every action type.`
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
if (fieldDef.appliesTo && actionTypes) {
|
|
579
|
+
for (const at of fieldDef.appliesTo) {
|
|
580
|
+
if (!actionTypes.includes(at)) {
|
|
581
|
+
errors.push({
|
|
582
|
+
code: "PROFILE_INVALID",
|
|
583
|
+
field: fieldName,
|
|
584
|
+
message: `Bounds field "${fieldName}": appliesTo names "${at}", which is not in boundsSchema.actionTypes [${actionTypes.join(", ")}].`
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (bt.kind !== "per_transaction" && bt.requiredFor !== void 0) {
|
|
590
|
+
errors.push({
|
|
591
|
+
code: "PROFILE_INVALID",
|
|
592
|
+
field: fieldName,
|
|
593
|
+
message: `Bounds field "${fieldName}": requiredFor is only meaningful on a per_transaction bound (found on kind "${bt.kind}") \u2014 every enforcement point ignores it there. Remove it.`
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
if (bt.kind === "per_transaction" && Array.isArray(bt.requiredFor) && actionTypes) {
|
|
597
|
+
for (const at of bt.requiredFor) {
|
|
598
|
+
if (!actionTypes.includes(at)) {
|
|
599
|
+
errors.push({
|
|
600
|
+
code: "PROFILE_INVALID",
|
|
601
|
+
field: fieldName,
|
|
602
|
+
message: `Bounds field "${fieldName}": requiredFor names "${at}", which is not in boundsSchema.actionTypes [${actionTypes.join(", ")}].`
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
559
608
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
609
|
+
const declaredModes = p.commitment_modes;
|
|
610
|
+
if (declaredModes !== void 0) {
|
|
611
|
+
if (!Array.isArray(declaredModes)) {
|
|
612
|
+
errors.push({ code: "PROFILE_INVALID", field: "commitment_modes", message: "commitment_modes must be a list of modes." });
|
|
613
|
+
} else {
|
|
614
|
+
if (declaredModes.length === 0) {
|
|
615
|
+
errors.push({
|
|
616
|
+
code: "PROFILE_INVALID",
|
|
617
|
+
field: "commitment_modes",
|
|
618
|
+
message: "commitment_modes is empty \u2014 no mandate could ever be signed under this profile."
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
const seen = /* @__PURE__ */ new Set();
|
|
622
|
+
for (const m of declaredModes) {
|
|
623
|
+
if (!SIGNABLE_COMMITMENT_MODES.includes(m)) {
|
|
624
|
+
errors.push({
|
|
625
|
+
code: "PROFILE_INVALID",
|
|
626
|
+
field: "commitment_modes",
|
|
627
|
+
message: `commitment_modes: "${String(m)}" is not a mode a signer can choose (${SIGNABLE_COMMITMENT_MODES.join(", ")}).`
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
if (seen.has(m)) {
|
|
631
|
+
errors.push({ code: "PROFILE_INVALID", field: "commitment_modes", message: `commitment_modes: "${String(m)}" appears twice.` });
|
|
632
|
+
}
|
|
633
|
+
seen.add(m);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
568
636
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
637
|
+
if (Array.isArray(declaredModes) && declaredModes.length > 0 && allowedCommitmentModes(profile).length === 0) {
|
|
638
|
+
errors.push({
|
|
639
|
+
code: "PROFILE_INVALID",
|
|
640
|
+
field: "commitment_modes",
|
|
641
|
+
message: "commitment_modes declares no mode this profile can actually use."
|
|
642
|
+
});
|
|
573
643
|
}
|
|
644
|
+
return errors;
|
|
574
645
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
checkAttestationExpiry(attestation.payload);
|
|
586
|
-
verifyBoundsHash(attestation, expectedBoundsHash);
|
|
587
|
-
verifyContextHash(attestation, expectedContextHash);
|
|
588
|
-
return attestation.payload;
|
|
646
|
+
function checkDiscloseSubset(mandateDiscloseFields, profileDiscloseFields) {
|
|
647
|
+
if (!mandateDiscloseFields || mandateDiscloseFields.length === 0) return;
|
|
648
|
+
const allowed = new Set(profileDiscloseFields ?? []);
|
|
649
|
+
const widened = mandateDiscloseFields.filter((f) => !allowed.has(f));
|
|
650
|
+
if (widened.length > 0) {
|
|
651
|
+
throw new HapError(
|
|
652
|
+
"MALFORMED_MANDATE",
|
|
653
|
+
`disclose_fields [${widened.join(", ")}] is not a subset of the profile's disclose_fields [${[...allowed].join(", ")}] \u2014 a mandate may narrow disclosure, never widen it.`
|
|
654
|
+
);
|
|
655
|
+
}
|
|
589
656
|
}
|
|
590
657
|
|
|
591
|
-
// src/
|
|
592
|
-
var
|
|
593
|
-
|
|
594
|
-
|
|
658
|
+
// src/mandate.ts
|
|
659
|
+
var import_crypto5 = require("crypto");
|
|
660
|
+
var ed = __toESM(require("@noble/ed25519"));
|
|
661
|
+
|
|
662
|
+
// src/did-key.ts
|
|
663
|
+
var B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
664
|
+
var B58_MAP = {};
|
|
665
|
+
for (let i = 0; i < B58_ALPHABET.length; i++) B58_MAP[B58_ALPHABET[i]] = i;
|
|
666
|
+
var ED25519_MULTICODEC = Uint8Array.from([237, 1]);
|
|
667
|
+
var ED25519_KEY_BYTES = 32;
|
|
668
|
+
function base58btcEncode(bytes) {
|
|
669
|
+
let zeros = 0;
|
|
670
|
+
while (zeros < bytes.length && bytes[zeros] === 0) zeros++;
|
|
671
|
+
const digits = [];
|
|
672
|
+
for (let i = zeros; i < bytes.length; i++) {
|
|
673
|
+
let carry = bytes[i];
|
|
674
|
+
for (let j = 0; j < digits.length; j++) {
|
|
675
|
+
carry += digits[j] * 256;
|
|
676
|
+
digits[j] = carry % 58;
|
|
677
|
+
carry = carry / 58 | 0;
|
|
678
|
+
}
|
|
679
|
+
while (carry > 0) {
|
|
680
|
+
digits.push(carry % 58);
|
|
681
|
+
carry = carry / 58 | 0;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
let out = "1".repeat(zeros);
|
|
685
|
+
for (let i = digits.length - 1; i >= 0; i--) out += B58_ALPHABET[digits[i]];
|
|
686
|
+
return out;
|
|
595
687
|
}
|
|
596
|
-
function
|
|
597
|
-
|
|
688
|
+
function base58btcDecode(s) {
|
|
689
|
+
let zeros = 0;
|
|
690
|
+
while (zeros < s.length && s[zeros] === "1") zeros++;
|
|
691
|
+
const bytes = [];
|
|
692
|
+
for (let i = zeros; i < s.length; i++) {
|
|
693
|
+
const val = B58_MAP[s[i]];
|
|
694
|
+
if (val === void 0) throw new Error(`invalid base58 character "${s[i]}"`);
|
|
695
|
+
let carry = val;
|
|
696
|
+
for (let j = 0; j < bytes.length; j++) {
|
|
697
|
+
carry += bytes[j] * 58;
|
|
698
|
+
bytes[j] = carry & 255;
|
|
699
|
+
carry >>= 8;
|
|
700
|
+
}
|
|
701
|
+
while (carry > 0) {
|
|
702
|
+
bytes.push(carry & 255);
|
|
703
|
+
carry >>= 8;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const out = new Uint8Array(zeros + bytes.length);
|
|
707
|
+
for (let i = 0; i < bytes.length; i++) out[zeros + i] = bytes[bytes.length - 1 - i];
|
|
708
|
+
return out;
|
|
598
709
|
}
|
|
599
|
-
function
|
|
600
|
-
|
|
710
|
+
function encodeDidKey(publicKey) {
|
|
711
|
+
if (publicKey.length !== ED25519_KEY_BYTES) {
|
|
712
|
+
throw new Error(`INVALID_KEY: expected ${ED25519_KEY_BYTES} bytes, got ${publicKey.length}`);
|
|
713
|
+
}
|
|
714
|
+
const prefixed = new Uint8Array(ED25519_MULTICODEC.length + publicKey.length);
|
|
715
|
+
prefixed.set(ED25519_MULTICODEC, 0);
|
|
716
|
+
prefixed.set(publicKey, ED25519_MULTICODEC.length);
|
|
717
|
+
return `did:key:z${base58btcEncode(prefixed)}`;
|
|
601
718
|
}
|
|
602
|
-
function
|
|
603
|
-
|
|
719
|
+
function decodeDidKey(did) {
|
|
720
|
+
if (!did.startsWith("did:key:z")) {
|
|
721
|
+
throw new Error(`NOT_KEY_BEARING: ${JSON.stringify(did)} is not a base58btc did:key`);
|
|
722
|
+
}
|
|
723
|
+
let decoded;
|
|
724
|
+
try {
|
|
725
|
+
decoded = base58btcDecode(did.slice("did:key:z".length));
|
|
726
|
+
} catch (err) {
|
|
727
|
+
throw new Error(`NOT_KEY_BEARING: ${err.message}`);
|
|
728
|
+
}
|
|
729
|
+
if (decoded.length !== ED25519_MULTICODEC.length + ED25519_KEY_BYTES || decoded[0] !== ED25519_MULTICODEC[0] || decoded[1] !== ED25519_MULTICODEC[1]) {
|
|
730
|
+
throw new Error("NOT_KEY_BEARING: not an ed25519-pub multicodec payload");
|
|
731
|
+
}
|
|
732
|
+
return decoded.slice(ED25519_MULTICODEC.length);
|
|
604
733
|
}
|
|
605
|
-
function
|
|
606
|
-
|
|
607
|
-
|
|
734
|
+
function isKeyBearingDid(did) {
|
|
735
|
+
try {
|
|
736
|
+
decodeDidKey(did);
|
|
737
|
+
return true;
|
|
738
|
+
} catch {
|
|
739
|
+
return false;
|
|
608
740
|
}
|
|
609
741
|
}
|
|
610
742
|
|
|
611
|
-
// src/
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
if (!profile) {
|
|
620
|
-
return { approved: false, errors: [{ code: "INVALID_PROFILE", message: `Unknown profile: ${profileId}` }] };
|
|
743
|
+
// src/mandate.ts
|
|
744
|
+
var CLOCK_SKEW_SECONDS = 300;
|
|
745
|
+
function decodeMandateBlob(blob) {
|
|
746
|
+
let json;
|
|
747
|
+
try {
|
|
748
|
+
json = new TextDecoder().decode(fromBase64Url(blob));
|
|
749
|
+
} catch (err) {
|
|
750
|
+
throw new HapError("MALFORMED_MANDATE", `failed to decode mandate blob: ${err.message}`);
|
|
621
751
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
return verifyV3(request, profile, publicKeyHex, now, executionLog, errors);
|
|
752
|
+
try {
|
|
753
|
+
return JSON.parse(json);
|
|
754
|
+
} catch (err) {
|
|
755
|
+
throw new HapError("MALFORMED_MANDATE", `mandate blob did not decode to JSON: ${err}`);
|
|
627
756
|
}
|
|
628
757
|
}
|
|
629
|
-
function
|
|
630
|
-
return
|
|
758
|
+
function encodeMandateBlob(mandate) {
|
|
759
|
+
return toBase64Url(new TextEncoder().encode(JSON.stringify(mandate)));
|
|
631
760
|
}
|
|
632
|
-
function
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
761
|
+
function mandateBlobId(blob) {
|
|
762
|
+
const hash = (0, import_crypto5.createHash)("sha256").update(blob, "utf8").digest("hex");
|
|
763
|
+
return `sha256:${hash}`;
|
|
764
|
+
}
|
|
765
|
+
async function signMandate(payload, privateKey, opts) {
|
|
766
|
+
const header = { typ: "HAP-mandate", alg: "EdDSA", ...opts?.kid ? { kid: opts.kid } : {} };
|
|
767
|
+
const signature = toBase64Url(await ed.signAsync(new TextEncoder().encode(canonicalize(payload)), privateKey));
|
|
768
|
+
return { header, payload, signature };
|
|
637
769
|
}
|
|
638
|
-
async function
|
|
639
|
-
|
|
640
|
-
|
|
770
|
+
async function verifyMandateSignature(mandate, opts) {
|
|
771
|
+
const { issuer } = mandate.payload;
|
|
772
|
+
if (opts?.trustedIssuers && !opts.trustedIssuers.includes(issuer)) {
|
|
773
|
+
throw new HapError("INVALID_SIGNATURE", `issuer ${JSON.stringify(issuer)} is not a trusted Authority Server`);
|
|
774
|
+
}
|
|
775
|
+
let publicKey;
|
|
641
776
|
try {
|
|
642
|
-
|
|
777
|
+
publicKey = decodeDidKey(issuer);
|
|
643
778
|
} catch (err) {
|
|
644
|
-
|
|
779
|
+
throw new HapError("INVALID_SIGNATURE", `cannot resolve a key from issuer ${JSON.stringify(issuer)}: ${err.message}`);
|
|
645
780
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
try {
|
|
651
|
-
attestation = decodeAttestationBlob(blob);
|
|
652
|
-
} catch {
|
|
653
|
-
errors.push({ code: "MALFORMED_ATTESTATION", message: "Failed to decode attestation blob" });
|
|
654
|
-
continue;
|
|
655
|
-
}
|
|
656
|
-
try {
|
|
657
|
-
await verifyAttestationSignature(attestation, publicKeyHex);
|
|
658
|
-
} catch {
|
|
659
|
-
errors.push({ code: "INVALID_SIGNATURE", message: "Attestation signature verification failed" });
|
|
660
|
-
continue;
|
|
661
|
-
}
|
|
662
|
-
try {
|
|
663
|
-
verifyFrameHash(attestation, expectedFrameHash);
|
|
664
|
-
} catch {
|
|
665
|
-
errors.push({ code: "FRAME_MISMATCH", message: "Attestation frame_hash does not match computed frame_hash" });
|
|
666
|
-
continue;
|
|
667
|
-
}
|
|
668
|
-
try {
|
|
669
|
-
checkAttestationExpiry(attestation.payload, now);
|
|
670
|
-
} catch {
|
|
671
|
-
const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
|
|
672
|
-
errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
|
|
673
|
-
continue;
|
|
674
|
-
}
|
|
675
|
-
for (const rd of attestation.payload.resolved_domains ?? []) {
|
|
676
|
-
coveredDomains.add(rd.domain);
|
|
781
|
+
if (mandate.header.kid !== void 0) {
|
|
782
|
+
const fingerprint = issuer.startsWith("did:key:") ? issuer.slice("did:key:".length) : void 0;
|
|
783
|
+
if (mandate.header.kid !== fingerprint) {
|
|
784
|
+
throw new HapError("INVALID_SIGNATURE", "header.kid disagrees with issuer's key (Mandate rule 8) \u2014 the field a verifier cannot forge wins");
|
|
677
785
|
}
|
|
678
786
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
787
|
+
const payloadBytes = new TextEncoder().encode(canonicalize(mandate.payload));
|
|
788
|
+
let sigBytes;
|
|
789
|
+
try {
|
|
790
|
+
sigBytes = fromBase64Url(mandate.signature);
|
|
791
|
+
} catch (err) {
|
|
792
|
+
throw new HapError("INVALID_SIGNATURE", `${err.message}`);
|
|
683
793
|
}
|
|
684
|
-
|
|
685
|
-
|
|
794
|
+
const isValid = await ed.verifyAsync(sigBytes, payloadBytes, publicKey).catch(() => false);
|
|
795
|
+
if (!isValid) {
|
|
796
|
+
throw new HapError("INVALID_SIGNATURE", "mandate signature verification failed");
|
|
686
797
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
}
|
|
798
|
+
}
|
|
799
|
+
function checkMandateExpiry(payload, now = Math.floor(Date.now() / 1e3)) {
|
|
800
|
+
if (now > payload.expires_at + CLOCK_SKEW_SECONDS) {
|
|
801
|
+
throw new HapError("TTL_EXPIRED", `mandate expired at ${payload.expires_at}, current time is ${now}`);
|
|
692
802
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
803
|
+
}
|
|
804
|
+
var OWNER_SIGNATURE_FIELDS = ["alg", "signature", "signed_at", "nonce", "binding", "signing_surface"];
|
|
805
|
+
function validateMandateOwners(payload) {
|
|
806
|
+
const owners = payload.mandate_owners;
|
|
807
|
+
if (!Array.isArray(owners) || owners.length !== 1) {
|
|
808
|
+
throw new HapError(
|
|
809
|
+
"MALFORMED_MANDATE",
|
|
810
|
+
`mandate_owners MUST contain exactly one entry in v0.7, got ${Array.isArray(owners) ? owners.length : typeof owners}`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
const entry = owners[0];
|
|
814
|
+
if (!entry.did) {
|
|
815
|
+
throw new HapError("MALFORMED_MANDATE", "mandate_owners[0].did is required");
|
|
816
|
+
}
|
|
817
|
+
const present = OWNER_SIGNATURE_FIELDS.filter((f) => entry[f] !== void 0);
|
|
818
|
+
if (present.length > 0 && present.length < OWNER_SIGNATURE_FIELDS.length) {
|
|
819
|
+
const missing = OWNER_SIGNATURE_FIELDS.filter((f) => entry[f] === void 0);
|
|
820
|
+
throw new HapError(
|
|
821
|
+
"MALFORMED_MANDATE",
|
|
822
|
+
`mandate_owners[0] carries [${present.join(", ")}] but not [${missing.join(", ")}] \u2014 signature fields are present together or absent together (Mandate rule 7)`
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
if (present.length === OWNER_SIGNATURE_FIELDS.length && !isKeyBearingDid(entry.did)) {
|
|
826
|
+
throw new HapError("OWNER_SIGNATURE_INVALID", `co-signing DID ${entry.did} is not key-bearing`);
|
|
696
827
|
}
|
|
697
|
-
return { approved: true };
|
|
698
828
|
}
|
|
699
|
-
async function
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
829
|
+
async function verifyMandate(blob, opts) {
|
|
830
|
+
const mandate = decodeMandateBlob(blob);
|
|
831
|
+
if (mandate.payload.version !== PROTOCOL_VERSION) {
|
|
832
|
+
throw new HapError(
|
|
833
|
+
"VERSION_UNSUPPORTED",
|
|
834
|
+
`mandate version ${JSON.stringify(mandate.payload.version)} is not verifiable by this implementation (verifies ${PROTOCOL_VERSION} only \u2014 re-issue the mandate)`
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
await verifyMandateSignature(mandate, opts);
|
|
838
|
+
checkMandateExpiry(mandate.payload, opts?.now);
|
|
839
|
+
validateMandateOwners(mandate.payload);
|
|
840
|
+
return mandate.payload;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// src/owner-signature.ts
|
|
844
|
+
var ed2 = __toESM(require("@noble/ed25519"));
|
|
845
|
+
var OwnerSignatureError = class extends Error {
|
|
846
|
+
constructor(code, message) {
|
|
847
|
+
super(`${code}: ${message}`);
|
|
848
|
+
this.code = code;
|
|
849
|
+
this.name = "OwnerSignatureError";
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
function decodeOrThrow(signature, code) {
|
|
853
|
+
try {
|
|
854
|
+
return fromBase64Url(signature);
|
|
855
|
+
} catch (err) {
|
|
856
|
+
throw new OwnerSignatureError(code, err.message);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function buildMandateProjection(payload, entry) {
|
|
860
|
+
const {
|
|
861
|
+
profile_id,
|
|
862
|
+
profile_hash,
|
|
863
|
+
bounds_hash,
|
|
864
|
+
scope_hash,
|
|
865
|
+
execution_context_hash,
|
|
866
|
+
gate_content_hashes,
|
|
867
|
+
commitment_mode,
|
|
868
|
+
expires_at
|
|
869
|
+
} = payload;
|
|
870
|
+
if (!profile_hash || !bounds_hash || !scope_hash || !commitment_mode) {
|
|
871
|
+
throw new OwnerSignatureError(
|
|
872
|
+
"MALFORMED_MANDATE",
|
|
873
|
+
"mandate projection requires profile_hash, bounds_hash, scope_hash and commitment_mode"
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
const projection = {
|
|
877
|
+
typ: "HAP-mandate-projection",
|
|
878
|
+
version: "0.7",
|
|
879
|
+
profile_id,
|
|
880
|
+
profile_hash,
|
|
881
|
+
owner_did: entry.did,
|
|
882
|
+
bounds_hash,
|
|
883
|
+
scope_hash,
|
|
884
|
+
execution_context_hash,
|
|
885
|
+
gate_content_hashes,
|
|
886
|
+
commitment_mode,
|
|
887
|
+
expires_at,
|
|
888
|
+
nonce: entry.nonce
|
|
889
|
+
};
|
|
890
|
+
if (payload.intent_disclosure_hash !== void 0) {
|
|
891
|
+
projection.intent_disclosure_hash = payload.intent_disclosure_hash;
|
|
892
|
+
}
|
|
893
|
+
if (payload.commitment_mode === "review_above_cap") {
|
|
894
|
+
projection.above_cap_caps = payload.above_cap_caps;
|
|
895
|
+
projection.above_cap_approvers = payload.above_cap_approvers;
|
|
896
|
+
}
|
|
897
|
+
if (payload.disclose_fields !== void 0) {
|
|
898
|
+
projection.disclose_fields = payload.disclose_fields;
|
|
899
|
+
}
|
|
900
|
+
return projection;
|
|
901
|
+
}
|
|
902
|
+
function projectionSigningBytes(projection) {
|
|
903
|
+
return new TextEncoder().encode(canonicalize(projection));
|
|
904
|
+
}
|
|
905
|
+
function approvalSigningBytes(approval) {
|
|
906
|
+
return new TextEncoder().encode(canonicalize(approval));
|
|
907
|
+
}
|
|
908
|
+
async function signMandateProjection(projection, privateKey) {
|
|
909
|
+
return toBase64Url(await ed2.signAsync(projectionSigningBytes(projection), privateKey));
|
|
910
|
+
}
|
|
911
|
+
async function signApproval(approval, privateKey) {
|
|
912
|
+
return toBase64Url(await ed2.signAsync(approvalSigningBytes(approval), privateKey));
|
|
913
|
+
}
|
|
914
|
+
async function verifyOwnerSignature(mandate, entry) {
|
|
915
|
+
const owners = mandate.payload.mandate_owners ?? [];
|
|
916
|
+
if (!owners.some((o) => o.did === entry.did)) {
|
|
917
|
+
throw new OwnerSignatureError("OWNER_SIGNATURE_INVALID", `signing DID ${entry.did} is not this mandate's mandate_owners`);
|
|
918
|
+
}
|
|
919
|
+
if (entry.alg !== void 0 && entry.alg !== "EdDSA") {
|
|
920
|
+
throw new OwnerSignatureError("OWNER_SIGNATURE_INVALID", `alg ${entry.alg} disagrees with the DID's key type (DID is authoritative)`);
|
|
921
|
+
}
|
|
922
|
+
if (!entry.signature || !entry.nonce) {
|
|
923
|
+
throw new OwnerSignatureError("OWNER_SIGNATURE_INVALID", `mandate_owners entry for ${entry.did} carries no signature to verify`);
|
|
924
|
+
}
|
|
925
|
+
let publicKey;
|
|
926
|
+
try {
|
|
927
|
+
publicKey = decodeDidKey(entry.did);
|
|
928
|
+
} catch (err) {
|
|
929
|
+
throw new OwnerSignatureError("OWNER_SIGNATURE_INVALID", err.message);
|
|
930
|
+
}
|
|
931
|
+
const projection = buildMandateProjection(mandate.payload, entry);
|
|
932
|
+
const ok = await ed2.verifyAsync(decodeOrThrow(entry.signature, "OWNER_SIGNATURE_INVALID"), projectionSigningBytes(projection), publicKey).catch(() => false);
|
|
933
|
+
if (!ok) {
|
|
934
|
+
throw new OwnerSignatureError("OWNER_SIGNATURE_INVALID", `owner signature by ${entry.did} does not verify`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
async function verifyOwnerSignatures(mandate) {
|
|
938
|
+
const entries = (mandate.payload.mandate_owners ?? []).filter((e) => e.signature !== void 0);
|
|
939
|
+
for (const entry of entries) {
|
|
940
|
+
await verifyOwnerSignature(mandate, entry);
|
|
941
|
+
}
|
|
942
|
+
return entries;
|
|
943
|
+
}
|
|
944
|
+
async function verifyApproval(approval, signature, signerDid) {
|
|
945
|
+
let publicKey;
|
|
946
|
+
try {
|
|
947
|
+
publicKey = decodeDidKey(signerDid);
|
|
948
|
+
} catch (err) {
|
|
949
|
+
throw new OwnerSignatureError("OWNER_SIGNATURE_INVALID", err.message);
|
|
950
|
+
}
|
|
951
|
+
const ok = await ed2.verifyAsync(decodeOrThrow(signature, "APPROVAL_SIGNATURE_INVALID"), approvalSigningBytes(approval), publicKey).catch(() => false);
|
|
952
|
+
if (!ok) {
|
|
953
|
+
throw new OwnerSignatureError("APPROVAL_SIGNATURE_INVALID", `approval signature by ${signerDid} does not verify`);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
var BINDING_RANK = { raw: 0, webauthn: 1, eudi: 2 };
|
|
957
|
+
function checkOwnerSignatureRequirement(payload, floor) {
|
|
958
|
+
if (!floor?.required) return;
|
|
959
|
+
const entry = payload.mandate_owners[0];
|
|
960
|
+
const signed = entry?.signature !== void 0 && entry?.binding !== void 0;
|
|
961
|
+
if (!signed) {
|
|
962
|
+
throw new OwnerSignatureError(
|
|
963
|
+
"OWNER_SIGNATURE_REQUIRED",
|
|
964
|
+
"this profile requires an owner signature and mandate_owners[0] carries none"
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
const minRank = floor.minBinding ? BINDING_RANK[floor.minBinding] : 0;
|
|
968
|
+
const haveRank = BINDING_RANK[entry.binding] ?? 0;
|
|
969
|
+
if (haveRank < minRank) {
|
|
970
|
+
throw new OwnerSignatureError(
|
|
971
|
+
"OWNER_SIGNATURE_REQUIRED",
|
|
972
|
+
`this profile requires binding >= "${floor.minBinding}", mandate_owners[0] carries "${entry.binding}"`
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// src/ticket.ts
|
|
978
|
+
var ed3 = __toESM(require("@noble/ed25519"));
|
|
979
|
+
async function signTicket(payload, privateKey) {
|
|
980
|
+
const signature = toBase64Url(await ed3.signAsync(new TextEncoder().encode(canonicalize(payload)), privateKey));
|
|
981
|
+
return { ...payload, signature };
|
|
982
|
+
}
|
|
983
|
+
async function verifyTicketSignature(ticket, opts) {
|
|
984
|
+
const { signature, ...unsigned } = ticket;
|
|
985
|
+
if (!signature) {
|
|
986
|
+
throw new HapError("INVALID_SIGNATURE", "ticket carries no signature");
|
|
987
|
+
}
|
|
988
|
+
if (opts?.trustedIssuers && !opts.trustedIssuers.includes(ticket.issuer)) {
|
|
989
|
+
throw new HapError("INVALID_SIGNATURE", `issuer ${JSON.stringify(ticket.issuer)} is not a trusted Authority Server`);
|
|
990
|
+
}
|
|
991
|
+
let publicKey;
|
|
992
|
+
try {
|
|
993
|
+
publicKey = decodeDidKey(ticket.issuer);
|
|
994
|
+
} catch (err) {
|
|
995
|
+
throw new HapError("INVALID_SIGNATURE", `cannot resolve a key from issuer ${JSON.stringify(ticket.issuer)}: ${err.message}`);
|
|
996
|
+
}
|
|
997
|
+
const bytes = new TextEncoder().encode(canonicalize(unsigned));
|
|
998
|
+
let sigBytes;
|
|
999
|
+
try {
|
|
1000
|
+
sigBytes = fromBase64Url(signature);
|
|
1001
|
+
} catch (err) {
|
|
1002
|
+
throw new HapError("INVALID_SIGNATURE", `${err.message}`);
|
|
1003
|
+
}
|
|
1004
|
+
const ok = await ed3.verifyAsync(sigBytes, bytes, publicKey).catch(() => false);
|
|
1005
|
+
if (!ok) {
|
|
1006
|
+
throw new HapError("INVALID_SIGNATURE", "ticket signature verification failed");
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
function publicTicketView(ticket, governingDiscloseFields = []) {
|
|
1010
|
+
const { userId: _userId, cumulativeState: _cumulativeState, limits: _limits, executionContext, ...rest } = ticket;
|
|
1011
|
+
const allowed = new Set(governingDiscloseFields);
|
|
1012
|
+
const disclosedExecutionContext = {};
|
|
1013
|
+
for (const [key, value] of Object.entries(executionContext)) {
|
|
1014
|
+
if (allowed.has(key)) disclosedExecutionContext[key] = value;
|
|
1015
|
+
}
|
|
1016
|
+
return { ...rest, executionContext: disclosedExecutionContext };
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// src/profiles/index.ts
|
|
1020
|
+
var PROFILES = {};
|
|
1021
|
+
function registerProfile(profileId, profile) {
|
|
1022
|
+
PROFILES[profileId] = profile;
|
|
1023
|
+
}
|
|
1024
|
+
function getProfile(profileId) {
|
|
1025
|
+
return PROFILES[profileId];
|
|
1026
|
+
}
|
|
1027
|
+
function listProfiles() {
|
|
1028
|
+
return Object.keys(PROFILES);
|
|
1029
|
+
}
|
|
1030
|
+
function getAllProfiles() {
|
|
1031
|
+
return Object.values(PROFILES);
|
|
1032
|
+
}
|
|
1033
|
+
function clearProfiles() {
|
|
1034
|
+
for (const key of Object.keys(PROFILES)) {
|
|
1035
|
+
delete PROFILES[key];
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// src/gatekeeper.ts
|
|
1040
|
+
async function verify(request, opts = {}) {
|
|
1041
|
+
const now = opts.now ?? Math.floor(Date.now() / 1e3);
|
|
1042
|
+
const profileId = request.bounds.profile;
|
|
1043
|
+
if (typeof profileId !== "string") {
|
|
1044
|
+
return { approved: false, errors: [{ code: "PROFILE_NOT_FOUND", message: "Missing profile in bounds" }] };
|
|
1045
|
+
}
|
|
1046
|
+
const profile = getProfile(profileId);
|
|
1047
|
+
if (!profile) {
|
|
1048
|
+
return { approved: false, errors: [{ code: "PROFILE_NOT_FOUND", message: `Unknown profile: ${profileId}` }] };
|
|
1049
|
+
}
|
|
1050
|
+
if (!Array.isArray(request.mandates) || request.mandates.length === 0) {
|
|
1051
|
+
return { approved: false, errors: [{ code: "MALFORMED_MANDATE", message: "No mandate supplied \u2014 nothing to verify" }] };
|
|
1052
|
+
}
|
|
1053
|
+
const bounds = request.bounds;
|
|
1054
|
+
const scope = request.scope;
|
|
704
1055
|
let expectedBoundsHash;
|
|
705
|
-
let expectedContextHash;
|
|
706
1056
|
try {
|
|
707
1057
|
expectedBoundsHash = computeBoundsHash(bounds, profile);
|
|
708
1058
|
} catch (err) {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
if (context && Object.keys(context).length > 0) {
|
|
712
|
-
try {
|
|
713
|
-
expectedContextHash = computeContextHash(context, profile);
|
|
714
|
-
} catch (err) {
|
|
715
|
-
return { approved: false, errors: [{ code: "CONTEXT_MISMATCH", message: `Context hash computation failed: ${err}` }] };
|
|
1059
|
+
if (err instanceof CanonicalValueError) {
|
|
1060
|
+
return { approved: false, errors: [{ code: err.code, field: err.field, message: err.message }] };
|
|
716
1061
|
}
|
|
1062
|
+
return { approved: false, errors: [{ code: "BOUNDS_HASH_MISMATCH", message: `Bounds hash computation failed: ${err}` }] };
|
|
717
1063
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
for (const blob of request.attestations) {
|
|
721
|
-
let attestation;
|
|
1064
|
+
let expectedScopeHash;
|
|
1065
|
+
if (scope && Object.keys(scope).length > 0) {
|
|
722
1066
|
try {
|
|
723
|
-
|
|
724
|
-
} catch {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}
|
|
728
|
-
try {
|
|
729
|
-
await verifyAttestationSignature(attestation, publicKeyHex);
|
|
730
|
-
} catch {
|
|
731
|
-
errors.push({ code: "INVALID_SIGNATURE", message: "Attestation signature verification failed" });
|
|
732
|
-
continue;
|
|
733
|
-
}
|
|
734
|
-
try {
|
|
735
|
-
verifyBoundsHash(attestation, expectedBoundsHash);
|
|
736
|
-
} catch {
|
|
737
|
-
errors.push({ code: "BOUNDS_MISMATCH", message: "Attestation bounds_hash does not match computed bounds_hash" });
|
|
738
|
-
continue;
|
|
739
|
-
}
|
|
740
|
-
if (isV4Attestation(attestation) && expectedContextHash) {
|
|
741
|
-
try {
|
|
742
|
-
verifyContextHash(attestation, expectedContextHash);
|
|
743
|
-
} catch {
|
|
744
|
-
errors.push({ code: "CONTEXT_MISMATCH", message: "Attestation context_hash does not match computed context_hash" });
|
|
745
|
-
continue;
|
|
1067
|
+
expectedScopeHash = computeScopeHash(scope, profile);
|
|
1068
|
+
} catch (err) {
|
|
1069
|
+
if (err instanceof CanonicalValueError) {
|
|
1070
|
+
return { approved: false, errors: [{ code: err.code, field: err.field, message: err.message }] };
|
|
746
1071
|
}
|
|
747
|
-
|
|
748
|
-
try {
|
|
749
|
-
checkAttestationExpiry(attestation.payload, now);
|
|
750
|
-
} catch {
|
|
751
|
-
const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
|
|
752
|
-
errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
|
|
753
|
-
continue;
|
|
754
|
-
}
|
|
755
|
-
for (const rd of attestation.payload.resolved_domains ?? []) {
|
|
756
|
-
coveredDomains.add(rd.domain);
|
|
1072
|
+
return { approved: false, errors: [{ code: "SCOPE_HASH_MISMATCH", message: `Scope hash computation failed: ${err}` }] };
|
|
757
1073
|
}
|
|
758
1074
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1075
|
+
const expectedProfileHash = computeProfileHash(profile);
|
|
1076
|
+
const errors = [];
|
|
1077
|
+
for (const blob of request.mandates) {
|
|
1078
|
+
const mandateErrors = await verifyOneMandate(blob, {
|
|
1079
|
+
expectedBoundsHash,
|
|
1080
|
+
expectedScopeHash,
|
|
1081
|
+
expectedProfileHash,
|
|
1082
|
+
trustedIssuers: opts.trustedIssuers,
|
|
1083
|
+
now
|
|
1084
|
+
});
|
|
1085
|
+
errors.push(...mandateErrors);
|
|
763
1086
|
}
|
|
764
1087
|
if (errors.length > 0) {
|
|
765
1088
|
return { approved: false, errors };
|
|
766
1089
|
}
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
1090
|
+
const actionType = typeof request.execution.action_type === "string" ? request.execution.action_type : void 0;
|
|
1091
|
+
const registry = profile.boundsSchema?.actionTypes;
|
|
1092
|
+
if (registry && registry.length > 0) {
|
|
1093
|
+
if (actionType === void 0 || !registry.includes(actionType)) {
|
|
1094
|
+
return {
|
|
1095
|
+
approved: false,
|
|
1096
|
+
errors: [{
|
|
1097
|
+
code: "INVALID_ACTION_TYPE",
|
|
1098
|
+
field: "action_type",
|
|
1099
|
+
message: `action_type ${JSON.stringify(actionType)} is not in profile ${profile.id}'s actionTypes registry [${registry.join(", ")}]`
|
|
1100
|
+
}]
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
const boundsErrors = checkBounds(request, profile, actionType);
|
|
1105
|
+
if (boundsErrors.length > 0) {
|
|
1106
|
+
return { approved: false, errors: boundsErrors };
|
|
770
1107
|
}
|
|
771
|
-
if (profile.
|
|
772
|
-
const enforceableFields = Object.entries(profile.
|
|
773
|
-
if (enforceableFields.length > 0 && !
|
|
1108
|
+
if (profile.scopeSchema && Object.keys(profile.scopeSchema.fields).length > 0) {
|
|
1109
|
+
const enforceableFields = Object.entries(profile.scopeSchema.fields).filter(([, def]) => def.constraint?.enforceable?.length);
|
|
1110
|
+
if (enforceableFields.length > 0 && !scope) {
|
|
774
1111
|
return {
|
|
775
1112
|
approved: false,
|
|
776
1113
|
errors: [{
|
|
777
1114
|
code: "BOUND_EXCEEDED",
|
|
778
|
-
message: `Declared
|
|
1115
|
+
message: `Declared scope required for constraint enforcement but not provided. Fields: ${enforceableFields.map(([n]) => n).join(", ")}`
|
|
779
1116
|
}]
|
|
780
1117
|
};
|
|
781
1118
|
}
|
|
782
|
-
if (
|
|
783
|
-
const
|
|
784
|
-
if (
|
|
785
|
-
return { approved: false, errors:
|
|
1119
|
+
if (scope) {
|
|
1120
|
+
const scopeErrors = checkScopeConstraints(scope, request.execution, profile);
|
|
1121
|
+
if (scopeErrors.length > 0) {
|
|
1122
|
+
return { approved: false, errors: scopeErrors };
|
|
786
1123
|
}
|
|
787
1124
|
}
|
|
788
1125
|
}
|
|
789
1126
|
return { approved: true };
|
|
790
1127
|
}
|
|
791
|
-
function
|
|
792
|
-
|
|
793
|
-
if (
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const constraint = fieldDef.constraint;
|
|
797
|
-
for (const enforceType of constraint.enforceable) {
|
|
798
|
-
if (enforceType === "max") {
|
|
799
|
-
const execField = fieldName.replace(/_max$/, "");
|
|
800
|
-
const boundValue = request.frame[fieldName];
|
|
801
|
-
const actualValue = request.execution[execField];
|
|
802
|
-
if (actualValue === void 0) continue;
|
|
803
|
-
if (typeof boundValue !== "number" || typeof actualValue !== "number") {
|
|
804
|
-
errors.push({
|
|
805
|
-
code: "BOUND_EXCEEDED",
|
|
806
|
-
field: execField,
|
|
807
|
-
message: `Bound check requires numeric values for "${execField}"`,
|
|
808
|
-
bound: boundValue,
|
|
809
|
-
actual: actualValue
|
|
810
|
-
});
|
|
811
|
-
continue;
|
|
812
|
-
}
|
|
813
|
-
if (actualValue > boundValue) {
|
|
814
|
-
errors.push({
|
|
815
|
-
code: "BOUND_EXCEEDED",
|
|
816
|
-
field: execField,
|
|
817
|
-
message: `Value ${actualValue} exceeds authorized maximum of ${boundValue}`,
|
|
818
|
-
bound: boundValue,
|
|
819
|
-
actual: actualValue
|
|
820
|
-
});
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
if (enforceType === "enum") {
|
|
824
|
-
const boundValue = request.frame[fieldName];
|
|
825
|
-
const actualValue = request.execution[fieldName];
|
|
826
|
-
if (actualValue === void 0) continue;
|
|
827
|
-
const allowed = typeof boundValue === "string" ? boundValue.split(",").map((s) => s.trim()) : [String(boundValue)];
|
|
828
|
-
const actualStr = String(actualValue);
|
|
829
|
-
if (!allowed.includes(actualStr)) {
|
|
830
|
-
errors.push({
|
|
831
|
-
code: "BOUND_EXCEEDED",
|
|
832
|
-
field: fieldName,
|
|
833
|
-
message: `Value "${actualStr}" not in authorized values [${allowed.join(", ")}]`,
|
|
834
|
-
bound: boundValue,
|
|
835
|
-
actual: actualValue
|
|
836
|
-
});
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
}
|
|
1128
|
+
function codeFromThrown(err) {
|
|
1129
|
+
if (err instanceof HapError) return err.code;
|
|
1130
|
+
if (err instanceof Error) {
|
|
1131
|
+
const [prefix] = err.message.split(":");
|
|
1132
|
+
return prefix;
|
|
840
1133
|
}
|
|
841
|
-
return
|
|
1134
|
+
return "MALFORMED_MANDATE";
|
|
842
1135
|
}
|
|
843
|
-
function
|
|
844
|
-
|
|
845
|
-
|
|
1136
|
+
async function verifyOneMandate(blob, opts) {
|
|
1137
|
+
let mandate;
|
|
1138
|
+
try {
|
|
1139
|
+
mandate = decodeMandateBlob(blob);
|
|
1140
|
+
} catch (err) {
|
|
1141
|
+
return [{ code: "MALFORMED_MANDATE", message: err.message }];
|
|
846
1142
|
}
|
|
847
|
-
if (
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1143
|
+
if (mandate.payload.version !== PROTOCOL_VERSION) {
|
|
1144
|
+
return [{
|
|
1145
|
+
code: "VERSION_UNSUPPORTED",
|
|
1146
|
+
message: `mandate version ${JSON.stringify(mandate.payload.version)} is not verifiable by this Gatekeeper (verifies ${PROTOCOL_VERSION} only)`
|
|
1147
|
+
}];
|
|
1148
|
+
}
|
|
1149
|
+
try {
|
|
1150
|
+
await verifyMandateSignature(mandate, { trustedIssuers: opts.trustedIssuers });
|
|
1151
|
+
} catch (err) {
|
|
1152
|
+
return [{ code: codeFromThrown(err), message: err.message }];
|
|
1153
|
+
}
|
|
1154
|
+
try {
|
|
1155
|
+
checkMandateExpiry(mandate.payload, opts.now);
|
|
1156
|
+
} catch (err) {
|
|
1157
|
+
return [{ code: codeFromThrown(err), message: err.message }];
|
|
1158
|
+
}
|
|
1159
|
+
try {
|
|
1160
|
+
validateMandateOwners(mandate.payload);
|
|
1161
|
+
} catch (err) {
|
|
1162
|
+
return [{ code: codeFromThrown(err), message: err.message }];
|
|
1163
|
+
}
|
|
1164
|
+
const errors = [];
|
|
1165
|
+
const payload = mandate.payload;
|
|
1166
|
+
if (payload.bounds_hash !== opts.expectedBoundsHash) {
|
|
1167
|
+
errors.push({ code: "BOUNDS_HASH_MISMATCH", message: "Mandate bounds_hash does not match the recomputed bounds_hash" });
|
|
1168
|
+
}
|
|
1169
|
+
if (opts.expectedScopeHash !== void 0 && payload.scope_hash !== opts.expectedScopeHash) {
|
|
1170
|
+
errors.push({ code: "SCOPE_HASH_MISMATCH", message: "Mandate scope_hash does not match the recomputed scope_hash" });
|
|
1171
|
+
}
|
|
1172
|
+
if (payload.profile_hash !== opts.expectedProfileHash) {
|
|
1173
|
+
errors.push({ code: "PROFILE_HASH_MISMATCH", message: "Mandate profile_hash does not match the Gatekeeper's provisioned profile bytes" });
|
|
1174
|
+
}
|
|
1175
|
+
return errors;
|
|
852
1176
|
}
|
|
853
1177
|
function boundActionTypes(fieldName, fieldDef) {
|
|
854
1178
|
if (fieldDef.appliesTo) return fieldDef.appliesTo;
|
|
@@ -858,29 +1182,25 @@ function boundActionTypes(fieldName, fieldDef) {
|
|
|
858
1182
|
if (prefix === fieldName || prefix.startsWith("transaction")) return void 0;
|
|
859
1183
|
return [prefix];
|
|
860
1184
|
}
|
|
861
|
-
function
|
|
1185
|
+
function checkBounds(request, profile, actionType) {
|
|
862
1186
|
const errors = [];
|
|
863
1187
|
if (!profile.boundsSchema) return errors;
|
|
864
|
-
const bounds = request.
|
|
865
|
-
const profileId = String(bounds.profile ?? profile.id);
|
|
866
|
-
const actionType = typeof request.execution.action_type === "string" ? request.execution.action_type : void 0;
|
|
867
|
-
const path = request.path ?? (bounds.path ? String(bounds.path) : "");
|
|
1188
|
+
const bounds = request.bounds;
|
|
868
1189
|
for (const [fieldName, fieldDef] of Object.entries(profile.boundsSchema.fields)) {
|
|
869
|
-
if (fieldName === "profile"
|
|
1190
|
+
if (fieldName === "profile") continue;
|
|
870
1191
|
const boundValue = bounds[fieldName];
|
|
871
1192
|
if (boundValue === void 0) continue;
|
|
872
1193
|
const bt = fieldDef.boundType;
|
|
873
1194
|
if (!bt) {
|
|
874
1195
|
errors.push({
|
|
875
|
-
code: "
|
|
1196
|
+
code: "PROFILE_INVALID",
|
|
876
1197
|
field: fieldName,
|
|
877
|
-
message: `Profile ${profile.id} bound "${fieldName}" has no boundType \u2014 enforcement semantics undefined
|
|
1198
|
+
message: `Profile ${profile.id} bound "${fieldName}" has no boundType \u2014 enforcement semantics undefined.`,
|
|
878
1199
|
bound: boundValue,
|
|
879
1200
|
actual: boundValue
|
|
880
1201
|
});
|
|
881
1202
|
continue;
|
|
882
1203
|
}
|
|
883
|
-
if (!boundGovernsAction(fieldName, fieldDef, bt, actionType)) continue;
|
|
884
1204
|
switch (bt.kind) {
|
|
885
1205
|
case "per_transaction": {
|
|
886
1206
|
const actual = request.execution[bt.of];
|
|
@@ -922,72 +1242,6 @@ function checkBoundsV4(request, profile, executionLog, now) {
|
|
|
922
1242
|
}
|
|
923
1243
|
break;
|
|
924
1244
|
}
|
|
925
|
-
case "cumulative_sum": {
|
|
926
|
-
if (typeof boundValue !== "number") {
|
|
927
|
-
errors.push({
|
|
928
|
-
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
929
|
-
field: bt.of,
|
|
930
|
-
message: `Cumulative bound "${fieldName}" must be numeric`,
|
|
931
|
-
bound: boundValue,
|
|
932
|
-
actual: 0
|
|
933
|
-
});
|
|
934
|
-
break;
|
|
935
|
-
}
|
|
936
|
-
if (!executionLog) break;
|
|
937
|
-
const runningTotal = executionLog.sumByWindow(
|
|
938
|
-
profileId,
|
|
939
|
-
path,
|
|
940
|
-
bt.of,
|
|
941
|
-
bt.window,
|
|
942
|
-
now,
|
|
943
|
-
boundActionTypes(fieldName, fieldDef)
|
|
944
|
-
);
|
|
945
|
-
const currentRaw = request.execution[bt.of];
|
|
946
|
-
const current = typeof currentRaw === "number" ? currentRaw : currentRaw !== void 0 ? Number(currentRaw) : 0;
|
|
947
|
-
const total = runningTotal + current;
|
|
948
|
-
if (total > boundValue) {
|
|
949
|
-
errors.push({
|
|
950
|
-
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
951
|
-
field: bt.of,
|
|
952
|
-
message: `Cumulative ${bt.window} sum of ${bt.of} (${total}) exceeds limit of ${boundValue} for ${fieldName}`,
|
|
953
|
-
bound: boundValue,
|
|
954
|
-
actual: total
|
|
955
|
-
});
|
|
956
|
-
}
|
|
957
|
-
break;
|
|
958
|
-
}
|
|
959
|
-
case "cumulative_count": {
|
|
960
|
-
if (typeof boundValue !== "number") {
|
|
961
|
-
errors.push({
|
|
962
|
-
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
963
|
-
field: fieldName,
|
|
964
|
-
message: `Cumulative count bound "${fieldName}" must be numeric`,
|
|
965
|
-
bound: boundValue,
|
|
966
|
-
actual: 0
|
|
967
|
-
});
|
|
968
|
-
break;
|
|
969
|
-
}
|
|
970
|
-
if (!executionLog) break;
|
|
971
|
-
const runningCount = executionLog.sumByWindow(
|
|
972
|
-
profileId,
|
|
973
|
-
path,
|
|
974
|
-
"_count",
|
|
975
|
-
bt.window,
|
|
976
|
-
now,
|
|
977
|
-
boundActionTypes(fieldName, fieldDef)
|
|
978
|
-
);
|
|
979
|
-
const total = runningCount + 1;
|
|
980
|
-
if (total > boundValue) {
|
|
981
|
-
errors.push({
|
|
982
|
-
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
983
|
-
field: fieldName,
|
|
984
|
-
message: `Cumulative ${bt.window} count (${total}) exceeds limit of ${boundValue} for ${fieldName}`,
|
|
985
|
-
bound: boundValue,
|
|
986
|
-
actual: total
|
|
987
|
-
});
|
|
988
|
-
}
|
|
989
|
-
break;
|
|
990
|
-
}
|
|
991
1245
|
case "enum": {
|
|
992
1246
|
if (typeof boundValue === "string" && !bt.values.includes(boundValue)) {
|
|
993
1247
|
errors.push({
|
|
@@ -1000,18 +1254,21 @@ function checkBoundsV4(request, profile, executionLog, now) {
|
|
|
1000
1254
|
}
|
|
1001
1255
|
break;
|
|
1002
1256
|
}
|
|
1257
|
+
case "cumulative_sum":
|
|
1258
|
+
case "cumulative_count":
|
|
1259
|
+
break;
|
|
1003
1260
|
}
|
|
1004
1261
|
}
|
|
1005
1262
|
return errors;
|
|
1006
1263
|
}
|
|
1007
|
-
function
|
|
1264
|
+
function checkScopeConstraints(scope, execution, profile) {
|
|
1008
1265
|
const errors = [];
|
|
1009
|
-
if (!profile.
|
|
1010
|
-
for (const [fieldName, fieldDef] of Object.entries(profile.
|
|
1266
|
+
if (!profile.scopeSchema) return errors;
|
|
1267
|
+
for (const [fieldName, fieldDef] of Object.entries(profile.scopeSchema.fields)) {
|
|
1011
1268
|
if (!fieldDef.constraint) continue;
|
|
1012
1269
|
const requiredFor = fieldDef.constraint.requiredFor;
|
|
1013
1270
|
if (requiredFor?.length) {
|
|
1014
|
-
const boundValue =
|
|
1271
|
+
const boundValue = scope[fieldName];
|
|
1015
1272
|
const actualValue = execution[fieldName];
|
|
1016
1273
|
const actionType = execution["action_type"];
|
|
1017
1274
|
const constrained = boundValue !== void 0 && boundValue !== "";
|
|
@@ -1028,7 +1285,7 @@ function checkContextConstraints(context, execution, profile) {
|
|
|
1028
1285
|
}
|
|
1029
1286
|
for (const enforceType of fieldDef.constraint.enforceable) {
|
|
1030
1287
|
if (enforceType === "enum") {
|
|
1031
|
-
const boundValue =
|
|
1288
|
+
const boundValue = scope[fieldName];
|
|
1032
1289
|
const actualValue = execution[fieldName];
|
|
1033
1290
|
if (actualValue === void 0) continue;
|
|
1034
1291
|
const allowed = typeof boundValue === "string" ? boundValue.split(",").map((s) => s.trim()) : [String(boundValue)];
|
|
@@ -1037,14 +1294,14 @@ function checkContextConstraints(context, execution, profile) {
|
|
|
1037
1294
|
errors.push({
|
|
1038
1295
|
code: "BOUND_EXCEEDED",
|
|
1039
1296
|
field: fieldName,
|
|
1040
|
-
message: `Value "${actualStr}" not in authorized
|
|
1297
|
+
message: `Value "${actualStr}" not in authorized scope values [${allowed.join(", ")}]`,
|
|
1041
1298
|
bound: boundValue,
|
|
1042
1299
|
actual: actualValue
|
|
1043
1300
|
});
|
|
1044
1301
|
}
|
|
1045
1302
|
}
|
|
1046
1303
|
if (enforceType === "subset") {
|
|
1047
|
-
const boundValue =
|
|
1304
|
+
const boundValue = scope[fieldName];
|
|
1048
1305
|
const actualValue = execution[fieldName];
|
|
1049
1306
|
if (boundValue === void 0 || boundValue === "") continue;
|
|
1050
1307
|
if (actualValue === void 0 || actualValue === "") continue;
|
|
@@ -1065,304 +1322,72 @@ function checkContextConstraints(context, execution, profile) {
|
|
|
1065
1322
|
}
|
|
1066
1323
|
return errors;
|
|
1067
1324
|
}
|
|
1068
|
-
function resolveCumulativeFields(request, profile, executionLog, now) {
|
|
1069
|
-
const errors = [];
|
|
1070
|
-
const profileId = String(request.frame.profile);
|
|
1071
|
-
const boundsOrFrame = request.frame;
|
|
1072
|
-
for (const [fieldName, fieldDef] of Object.entries(profile.executionContextSchema.fields)) {
|
|
1073
|
-
if (fieldDef.source !== "cumulative") continue;
|
|
1074
|
-
const cumDef = fieldDef;
|
|
1075
|
-
const { cumulativeField, window: windowType } = cumDef;
|
|
1076
|
-
const path = request.frame.path ? String(request.frame.path) : "";
|
|
1077
|
-
const runningTotal = executionLog.sumByWindow(profileId, path, cumulativeField, windowType, now);
|
|
1078
|
-
let currentContribution;
|
|
1079
|
-
if (cumulativeField === "_count") {
|
|
1080
|
-
currentContribution = 1;
|
|
1081
|
-
} else {
|
|
1082
|
-
const val = request.execution[cumulativeField];
|
|
1083
|
-
currentContribution = typeof val === "number" ? val : val !== void 0 ? Number(val) : 0;
|
|
1084
|
-
}
|
|
1085
|
-
const cumulativeValue = runningTotal + currentContribution;
|
|
1086
|
-
request.execution[fieldName] = cumulativeValue;
|
|
1087
|
-
const boundFieldName = fieldName + "_max";
|
|
1088
|
-
const boundValue = boundsOrFrame[boundFieldName];
|
|
1089
|
-
if (boundValue === void 0) continue;
|
|
1090
|
-
if (typeof boundValue !== "number") {
|
|
1091
|
-
errors.push({
|
|
1092
|
-
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
1093
|
-
field: fieldName,
|
|
1094
|
-
message: `Cumulative bound requires numeric value for "${boundFieldName}"`,
|
|
1095
|
-
bound: boundValue,
|
|
1096
|
-
actual: cumulativeValue
|
|
1097
|
-
});
|
|
1098
|
-
continue;
|
|
1099
|
-
}
|
|
1100
|
-
if (cumulativeValue > boundValue) {
|
|
1101
|
-
errors.push({
|
|
1102
|
-
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
1103
|
-
field: fieldName,
|
|
1104
|
-
message: `Cumulative ${windowType} value ${cumulativeValue} exceeds limit of ${boundValue}`,
|
|
1105
|
-
bound: boundValue,
|
|
1106
|
-
actual: cumulativeValue
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
return errors;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
// src/did-key.ts
|
|
1114
|
-
var B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
1115
|
-
var B58_MAP = {};
|
|
1116
|
-
for (let i = 0; i < B58_ALPHABET.length; i++) B58_MAP[B58_ALPHABET[i]] = i;
|
|
1117
|
-
var ED25519_MULTICODEC = Uint8Array.from([237, 1]);
|
|
1118
|
-
var ED25519_KEY_BYTES = 32;
|
|
1119
|
-
function base58btcEncode(bytes) {
|
|
1120
|
-
let zeros = 0;
|
|
1121
|
-
while (zeros < bytes.length && bytes[zeros] === 0) zeros++;
|
|
1122
|
-
const digits = [];
|
|
1123
|
-
for (let i = zeros; i < bytes.length; i++) {
|
|
1124
|
-
let carry = bytes[i];
|
|
1125
|
-
for (let j = 0; j < digits.length; j++) {
|
|
1126
|
-
carry += digits[j] * 256;
|
|
1127
|
-
digits[j] = carry % 58;
|
|
1128
|
-
carry = carry / 58 | 0;
|
|
1129
|
-
}
|
|
1130
|
-
while (carry > 0) {
|
|
1131
|
-
digits.push(carry % 58);
|
|
1132
|
-
carry = carry / 58 | 0;
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
let out = "1".repeat(zeros);
|
|
1136
|
-
for (let i = digits.length - 1; i >= 0; i--) out += B58_ALPHABET[digits[i]];
|
|
1137
|
-
return out;
|
|
1138
|
-
}
|
|
1139
|
-
function base58btcDecode(s) {
|
|
1140
|
-
let zeros = 0;
|
|
1141
|
-
while (zeros < s.length && s[zeros] === "1") zeros++;
|
|
1142
|
-
const bytes = [];
|
|
1143
|
-
for (let i = zeros; i < s.length; i++) {
|
|
1144
|
-
const val = B58_MAP[s[i]];
|
|
1145
|
-
if (val === void 0) throw new Error(`invalid base58 character "${s[i]}"`);
|
|
1146
|
-
let carry = val;
|
|
1147
|
-
for (let j = 0; j < bytes.length; j++) {
|
|
1148
|
-
carry += bytes[j] * 58;
|
|
1149
|
-
bytes[j] = carry & 255;
|
|
1150
|
-
carry >>= 8;
|
|
1151
|
-
}
|
|
1152
|
-
while (carry > 0) {
|
|
1153
|
-
bytes.push(carry & 255);
|
|
1154
|
-
carry >>= 8;
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
const out = new Uint8Array(zeros + bytes.length);
|
|
1158
|
-
for (let i = 0; i < bytes.length; i++) out[zeros + i] = bytes[bytes.length - 1 - i];
|
|
1159
|
-
return out;
|
|
1160
|
-
}
|
|
1161
|
-
function encodeDidKey(publicKey) {
|
|
1162
|
-
if (publicKey.length !== ED25519_KEY_BYTES) {
|
|
1163
|
-
throw new Error(`INVALID_KEY: expected ${ED25519_KEY_BYTES} bytes, got ${publicKey.length}`);
|
|
1164
|
-
}
|
|
1165
|
-
const prefixed = new Uint8Array(ED25519_MULTICODEC.length + publicKey.length);
|
|
1166
|
-
prefixed.set(ED25519_MULTICODEC, 0);
|
|
1167
|
-
prefixed.set(publicKey, ED25519_MULTICODEC.length);
|
|
1168
|
-
return `did:key:z${base58btcEncode(prefixed)}`;
|
|
1169
|
-
}
|
|
1170
|
-
function decodeDidKey(did) {
|
|
1171
|
-
if (!did.startsWith("did:key:z")) {
|
|
1172
|
-
throw new Error(`NOT_KEY_BEARING: ${JSON.stringify(did)} is not a base58btc did:key`);
|
|
1173
|
-
}
|
|
1174
|
-
let decoded;
|
|
1175
|
-
try {
|
|
1176
|
-
decoded = base58btcDecode(did.slice("did:key:z".length));
|
|
1177
|
-
} catch (err) {
|
|
1178
|
-
throw new Error(`NOT_KEY_BEARING: ${err.message}`);
|
|
1179
|
-
}
|
|
1180
|
-
if (decoded.length !== ED25519_MULTICODEC.length + ED25519_KEY_BYTES || decoded[0] !== ED25519_MULTICODEC[0] || decoded[1] !== ED25519_MULTICODEC[1]) {
|
|
1181
|
-
throw new Error("NOT_KEY_BEARING: not an ed25519-pub multicodec payload");
|
|
1182
|
-
}
|
|
1183
|
-
return decoded.slice(ED25519_MULTICODEC.length);
|
|
1184
|
-
}
|
|
1185
|
-
function isKeyBearingDid(did) {
|
|
1186
|
-
try {
|
|
1187
|
-
decodeDidKey(did);
|
|
1188
|
-
return true;
|
|
1189
|
-
} catch {
|
|
1190
|
-
return false;
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
// src/mandate.ts
|
|
1195
|
-
var ed2 = __toESM(require("@noble/ed25519"));
|
|
1196
|
-
var MandateError = class extends Error {
|
|
1197
|
-
constructor(code, message) {
|
|
1198
|
-
super(`${code}: ${message}`);
|
|
1199
|
-
this.code = code;
|
|
1200
|
-
this.name = "MandateError";
|
|
1201
|
-
}
|
|
1202
|
-
};
|
|
1203
|
-
function buildMandateProjection(payload, entry) {
|
|
1204
|
-
const { profile_id, bounds_hash, context_hash, execution_context_hash, gate_content_hashes, commitment_mode, expires_at } = payload;
|
|
1205
|
-
if (!bounds_hash || !context_hash || !commitment_mode) {
|
|
1206
|
-
throw new MandateError("MALFORMED_ATTESTATION", "mandate projection requires bounds_hash, context_hash and commitment_mode");
|
|
1207
|
-
}
|
|
1208
|
-
const projection = {
|
|
1209
|
-
typ: "HAP-mandate",
|
|
1210
|
-
version: "0.6",
|
|
1211
|
-
profile_id,
|
|
1212
|
-
owner_did: entry.did,
|
|
1213
|
-
bounds_hash,
|
|
1214
|
-
context_hash,
|
|
1215
|
-
execution_context_hash,
|
|
1216
|
-
gate_content_hashes,
|
|
1217
|
-
commitment_mode,
|
|
1218
|
-
expires_at,
|
|
1219
|
-
nonce: entry.nonce
|
|
1220
|
-
};
|
|
1221
|
-
if (payload.intent_disclosure_hash !== void 0) {
|
|
1222
|
-
projection.intent_disclosure_hash = payload.intent_disclosure_hash;
|
|
1223
|
-
}
|
|
1224
|
-
return projection;
|
|
1225
|
-
}
|
|
1226
|
-
function mandateSigningBytes(projection) {
|
|
1227
|
-
return new TextEncoder().encode(canonicalize(projection));
|
|
1228
|
-
}
|
|
1229
|
-
function approvalSigningBytes(approval) {
|
|
1230
|
-
return new TextEncoder().encode(canonicalize(approval));
|
|
1231
|
-
}
|
|
1232
|
-
function base64urlToBytes(s) {
|
|
1233
|
-
const base64 = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
1234
|
-
const padding = base64.length % 4 === 0 ? "" : "=".repeat(4 - base64.length % 4);
|
|
1235
|
-
return new Uint8Array(Buffer.from(base64 + padding, "base64"));
|
|
1236
|
-
}
|
|
1237
|
-
function bytesToBase64url(bytes) {
|
|
1238
|
-
return Buffer.from(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1239
|
-
}
|
|
1240
|
-
async function signMandateProjection(projection, privateKey) {
|
|
1241
|
-
const sig = await ed2.signAsync(mandateSigningBytes(projection), privateKey);
|
|
1242
|
-
return bytesToBase64url(sig);
|
|
1243
|
-
}
|
|
1244
|
-
async function signApproval(approval, privateKey) {
|
|
1245
|
-
const sig = await ed2.signAsync(approvalSigningBytes(approval), privateKey);
|
|
1246
|
-
return bytesToBase64url(sig);
|
|
1247
|
-
}
|
|
1248
|
-
async function verifyOwnerMandate(attestation, entry) {
|
|
1249
|
-
const owners = attestation.payload.resolved_owners ?? [];
|
|
1250
|
-
if (!owners.includes(entry.did)) {
|
|
1251
|
-
throw new MandateError("OWNER_NOT_RESOLVED", `signing DID ${entry.did} is not in resolved_owners`);
|
|
1252
|
-
}
|
|
1253
|
-
if (entry.alg !== "EdDSA") {
|
|
1254
|
-
throw new MandateError("MANDATE_SIGNATURE_INVALID", `alg ${entry.alg} disagrees with the DID's key type (DID is authoritative)`);
|
|
1255
|
-
}
|
|
1256
|
-
let publicKey;
|
|
1257
|
-
try {
|
|
1258
|
-
publicKey = decodeDidKey(entry.did);
|
|
1259
|
-
} catch (err) {
|
|
1260
|
-
throw new MandateError("NOT_KEY_BEARING", err.message);
|
|
1261
|
-
}
|
|
1262
|
-
const projection = buildMandateProjection(attestation.payload, entry);
|
|
1263
|
-
const ok = await ed2.verifyAsync(base64urlToBytes(entry.signature), mandateSigningBytes(projection), publicKey);
|
|
1264
|
-
if (!ok) {
|
|
1265
|
-
throw new MandateError("MANDATE_SIGNATURE_INVALID", `owner mandate signature by ${entry.did} does not verify`);
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
async function verifyOwnerMandates(attestation) {
|
|
1269
|
-
const entries = attestation.payload.owner_mandates ?? [];
|
|
1270
|
-
for (const entry of entries) {
|
|
1271
|
-
await verifyOwnerMandate(attestation, entry);
|
|
1272
|
-
}
|
|
1273
|
-
return entries;
|
|
1274
|
-
}
|
|
1275
|
-
async function verifyApproval(approval, signature, signerDid) {
|
|
1276
|
-
let publicKey;
|
|
1277
|
-
try {
|
|
1278
|
-
publicKey = decodeDidKey(signerDid);
|
|
1279
|
-
} catch (err) {
|
|
1280
|
-
throw new MandateError("NOT_KEY_BEARING", err.message);
|
|
1281
|
-
}
|
|
1282
|
-
const ok = await ed2.verifyAsync(base64urlToBytes(signature), approvalSigningBytes(approval), publicKey);
|
|
1283
|
-
if (!ok) {
|
|
1284
|
-
throw new MandateError("APPROVAL_SIGNATURE_INVALID", `approval signature by ${signerDid} does not verify`);
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
// src/receipt.ts
|
|
1289
|
-
var ed3 = __toESM(require("@noble/ed25519"));
|
|
1290
|
-
async function verifyReceiptSignature(receipt, publicKeyHex) {
|
|
1291
|
-
const { signature, ...unsigned } = receipt;
|
|
1292
|
-
if (!signature) {
|
|
1293
|
-
throw new Error("INVALID_SIGNATURE: receipt carries no signature");
|
|
1294
|
-
}
|
|
1295
|
-
const bytes = new TextEncoder().encode(canonicalize(unsigned));
|
|
1296
|
-
const base64 = signature.replace(/-/g, "+").replace(/_/g, "/");
|
|
1297
|
-
const padding = base64.length % 4 === 0 ? "" : "=".repeat(4 - base64.length % 4);
|
|
1298
|
-
const sigBytes = new Uint8Array(Buffer.from(base64 + padding, "base64"));
|
|
1299
|
-
const publicKeyBytes = new Uint8Array(Buffer.from(publicKeyHex, "hex"));
|
|
1300
|
-
const ok = await ed3.verifyAsync(sigBytes, bytes, publicKeyBytes).catch(() => false);
|
|
1301
|
-
if (!ok) {
|
|
1302
|
-
throw new Error("INVALID_SIGNATURE: receipt signature verification failed");
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
1325
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1306
1326
|
0 && (module.exports = {
|
|
1307
1327
|
CanonicalValueError,
|
|
1308
1328
|
ContentBindingError,
|
|
1309
|
-
|
|
1329
|
+
HapError,
|
|
1330
|
+
MANDATE_ERROR_CODES,
|
|
1331
|
+
OwnerSignatureError,
|
|
1332
|
+
PROTOCOL_VERSION,
|
|
1310
1333
|
SIGNABLE_COMMITMENT_MODES,
|
|
1334
|
+
SUPPORTED_VERSIONS,
|
|
1335
|
+
TICKET_ERROR_CODES,
|
|
1311
1336
|
allowedCommitmentModes,
|
|
1312
1337
|
approvalSigningBytes,
|
|
1313
|
-
attestationId,
|
|
1314
1338
|
bindingAppliesTo,
|
|
1315
1339
|
boundActionTypes,
|
|
1316
1340
|
buildMandateProjection,
|
|
1317
1341
|
canonicalBounds,
|
|
1318
|
-
|
|
1319
|
-
canonicalFrame,
|
|
1342
|
+
canonicalScope,
|
|
1320
1343
|
canonicalize,
|
|
1321
1344
|
canonicalizeText,
|
|
1322
|
-
|
|
1345
|
+
checkDiscloseSubset,
|
|
1346
|
+
checkMandateExpiry,
|
|
1347
|
+
checkOwnerSignatureRequirement,
|
|
1323
1348
|
clearProfiles,
|
|
1324
1349
|
computeBoundsHash,
|
|
1325
1350
|
computeContentHash,
|
|
1326
|
-
computeContextHash,
|
|
1327
1351
|
computeFieldsContentHash,
|
|
1328
|
-
computeFrameHash,
|
|
1329
1352
|
computeIntentDisclosureHash,
|
|
1330
1353
|
computeIntentHash,
|
|
1354
|
+
computeProfileHash,
|
|
1355
|
+
computeScopeHash,
|
|
1331
1356
|
contentCanonicalBytes,
|
|
1332
|
-
decodeAttestationBlob,
|
|
1333
1357
|
decodeDidKey,
|
|
1358
|
+
decodeMandateBlob,
|
|
1334
1359
|
deriveIdentityLine,
|
|
1335
|
-
encodeAttestationBlob,
|
|
1336
1360
|
encodeDidKey,
|
|
1337
|
-
|
|
1361
|
+
encodeMandateBlob,
|
|
1362
|
+
fromBase64Url,
|
|
1338
1363
|
getAllProfiles,
|
|
1339
1364
|
getProfile,
|
|
1340
1365
|
intentDisclosureCanonicalBytes,
|
|
1341
1366
|
isCommitmentModeAllowed,
|
|
1342
1367
|
isFieldBinding,
|
|
1343
1368
|
isKeyBearingDid,
|
|
1344
|
-
isV4Attestation,
|
|
1345
1369
|
listProfiles,
|
|
1346
|
-
|
|
1370
|
+
mandateBlobId,
|
|
1371
|
+
negotiateVersion,
|
|
1372
|
+
projectionSigningBytes,
|
|
1373
|
+
publicTicketView,
|
|
1347
1374
|
registerProfile,
|
|
1348
1375
|
selectBoundFields,
|
|
1349
1376
|
signApproval,
|
|
1377
|
+
signMandate,
|
|
1350
1378
|
signMandateProjection,
|
|
1379
|
+
signTicket,
|
|
1380
|
+
toBase64Url,
|
|
1351
1381
|
validateBoundsParams,
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
validateFrameParams,
|
|
1382
|
+
validateMandateOwners,
|
|
1383
|
+
validateProfile,
|
|
1384
|
+
validateScopeParams,
|
|
1356
1385
|
validateSubject,
|
|
1357
1386
|
verify,
|
|
1358
1387
|
verifyApproval,
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
verifyFrameHash,
|
|
1365
|
-
verifyOwnerMandate,
|
|
1366
|
-
verifyOwnerMandates,
|
|
1367
|
-
verifyReceiptSignature
|
|
1388
|
+
verifyMandate,
|
|
1389
|
+
verifyMandateSignature,
|
|
1390
|
+
verifyOwnerSignature,
|
|
1391
|
+
verifyOwnerSignatures,
|
|
1392
|
+
verifyTicketSignature
|
|
1368
1393
|
});
|