@humanagencyp/hap-core 0.11.2 → 0.12.1

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