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