@humanagencyp/hap-core 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/canonicalize.ts
2
+ var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
2
3
  function canonicalize(value) {
3
4
  if (value === void 0) {
4
5
  throw new Error("canonicalize: undefined is not serializable");
@@ -6,6 +7,9 @@ function canonicalize(value) {
6
7
  if (typeof value === "number" && !Number.isFinite(value)) {
7
8
  throw new Error(`canonicalize: ${value} is not a valid JSON number`);
8
9
  }
10
+ if (typeof value === "string" && LONE_SURROGATE.test(value)) {
11
+ throw new Error("canonicalize: string contains a lone surrogate and cannot be signed");
12
+ }
9
13
  if (value === null || typeof value !== "object") {
10
14
  return JSON.stringify(value);
11
15
  }
@@ -55,6 +59,10 @@ var MANDATE_ERROR_CODES = [
55
59
  "BOUNDS_INVALID_VALUE",
56
60
  "SCOPE_INVALID_VALUE",
57
61
  "ABOVE_CAP_CONFIG_INVALID",
62
+ "COMMITMENT_MODE_NOT_ALLOWED",
63
+ // v0.8
64
+ "GROUP_CAP_EXCEEDED",
65
+ // v0.8
58
66
  "OWNER_SIGNATURE_REQUIRED",
59
67
  "OWNER_SIGNATURE_INVALID"
60
68
  ];
@@ -68,6 +76,10 @@ var TICKET_ERROR_CODES = [
68
76
  "PROFILE_NOT_FOUND",
69
77
  "INVALID_ACTION_TYPE",
70
78
  "APPROVAL_REQUIRED",
79
+ "APPROVAL_IMPOSSIBLE",
80
+ // v0.8
81
+ "PROPOSAL_CONTENT_MISMATCH",
82
+ // v0.8
71
83
  "PROPOSAL_REQUIRED",
72
84
  "PROPOSAL_NOT_FOUND",
73
85
  "PROPOSAL_NOT_APPROVED",
@@ -76,11 +88,25 @@ var TICKET_ERROR_CODES = [
76
88
  "PROPOSAL_MISMATCH",
77
89
  "PROPOSAL_MANDATE_MISMATCH",
78
90
  "PROPOSAL_ALREADY_EXECUTED",
91
+ "PROPOSAL_WITHDRAWN",
92
+ // v0.8
79
93
  "MALFORMED_TICKET_REQUEST",
94
+ "CONTENT_BINDING_INCOMPLETE",
95
+ // v0.8, Gatekeeper · local
80
96
  "IDEMPOTENCY_KEY_REQUIRED",
81
97
  "IDEMPOTENCY_MISMATCH",
82
98
  "APPROVAL_SIGNATURE_REQUIRED",
83
- "APPROVAL_SIGNATURE_INVALID"
99
+ "APPROVAL_SIGNATURE_INVALID",
100
+ "TICKET_REQUEST_MISMATCH",
101
+ // v0.8, Gatekeeper · local
102
+ "POLICY_UNMET"
103
+ // v0.8, Verifier
104
+ ];
105
+ var PROPOSAL_ERROR_CODES = [
106
+ "PROPOSAL_NOT_ENCRYPTED",
107
+ "DECIDER_INVALID",
108
+ "PROPOSAL_ALREADY_DECIDED",
109
+ "PROPOSAL_APPROVERS_INVALID"
84
110
  ];
85
111
  var HapError = class extends Error {
86
112
  constructor(code, message) {
@@ -91,9 +117,13 @@ var HapError = class extends Error {
91
117
  };
92
118
 
93
119
  // src/versions.ts
94
- var PROTOCOL_VERSION = "0.7";
95
- var SUPPORTED_VERSIONS = ["0.7"];
96
- function negotiateVersion(requested, supported = SUPPORTED_VERSIONS) {
120
+ var PROTOCOL_VERSION = "0.8";
121
+ var SUPPORTED_VERSIONS = ["0.8", "0.7"];
122
+ var ISSUABLE_VERSIONS = [PROTOCOL_VERSION];
123
+ function isSupportedVersion(v) {
124
+ return typeof v === "string" && SUPPORTED_VERSIONS.includes(v);
125
+ }
126
+ function negotiateVersion(requested, supported = ISSUABLE_VERSIONS) {
97
127
  const requestedVersions = requested ?? ["0.6"];
98
128
  const common = supported.filter((v) => requestedVersions.includes(v)).sort().reverse();
99
129
  if (common.length === 0) {
@@ -104,6 +134,14 @@ function negotiateVersion(requested, supported = SUPPORTED_VERSIONS) {
104
134
  }
105
135
  return common[0];
106
136
  }
137
+ function assertSupportedVersion(version, what) {
138
+ if (!isSupportedVersion(version)) {
139
+ throw new HapError(
140
+ "VERSION_UNSUPPORTED",
141
+ `${what} version ${JSON.stringify(version)} is not verifiable by this implementation (verifies [${SUPPORTED_VERSIONS.join(", ")}])`
142
+ );
143
+ }
144
+ }
107
145
 
108
146
  // src/content-binding.ts
109
147
  import { createHash } from "crypto";
@@ -214,6 +252,20 @@ function selectBoundFields(binding, args) {
214
252
  function computeFieldsContentHash(binding, args) {
215
253
  return computeContentHash(binding, selectBoundFields(binding, args));
216
254
  }
255
+ var FIELD_KINDS = /* @__PURE__ */ new Set(["text", "list", "files"]);
256
+ function invalidFieldKindsReason(binding) {
257
+ const kinds = binding.field_kinds;
258
+ if (kinds === void 0) return null;
259
+ if (!kinds || typeof kinds !== "object" || Array.isArray(kinds)) return "field_kinds must be an object";
260
+ for (const [field, kind] of Object.entries(kinds)) {
261
+ if (!(binding.fields ?? []).includes(field)) return `field_kinds names "${field}", which is not in fields`;
262
+ if (!FIELD_KINDS.has(kind)) return `field_kinds.${field} must be one of text, list, files`;
263
+ }
264
+ return null;
265
+ }
266
+ function filesFields(binding) {
267
+ return Object.entries(binding.field_kinds ?? {}).filter(([, k]) => k === "files").map(([f]) => f);
268
+ }
217
269
 
218
270
  // src/intent-disclosure.ts
219
271
  import { createHash as createHash2 } from "crypto";
@@ -396,6 +448,7 @@ function computeScopeHash(params, profile) {
396
448
 
397
449
  // src/commitment-modes.ts
398
450
  var SIGNABLE_COMMITMENT_MODES = ["review", "automatic"];
451
+ var COMMITMENT_MODES = ["automatic", "review", "review_above_cap"];
399
452
  var isSignable = (v) => typeof v === "string" && SIGNABLE_COMMITMENT_MODES.includes(v);
400
453
  function allowedCommitmentModes(profile) {
401
454
  const declared = profile.commitment_modes;
@@ -404,7 +457,9 @@ function allowedCommitmentModes(profile) {
404
457
  return SIGNABLE_COMMITMENT_MODES.filter((m) => declared.includes(m));
405
458
  }
406
459
  function isCommitmentModeAllowed(profile, mode) {
407
- return isSignable(mode) && allowedCommitmentModes(profile).includes(mode);
460
+ const allowed = allowedCommitmentModes(profile);
461
+ if (mode === "review_above_cap") return allowed.includes("automatic");
462
+ return isSignable(mode) && allowed.includes(mode);
408
463
  }
409
464
 
410
465
  // src/profile.ts
@@ -519,6 +574,7 @@ function validateProfile(profile) {
519
574
  }
520
575
  const seen = /* @__PURE__ */ new Set();
521
576
  for (const m of declaredModes) {
577
+ if (m === "review_above_cap") continue;
522
578
  if (!SIGNABLE_COMMITMENT_MODES.includes(m)) {
523
579
  errors.push({
524
580
  code: "PROFILE_INVALID",
@@ -540,6 +596,46 @@ function validateProfile(profile) {
540
596
  message: "commitment_modes declares no mode this profile can actually use."
541
597
  });
542
598
  }
599
+ if (boundsSchema) {
600
+ const readFields = /* @__PURE__ */ new Set();
601
+ for (const [fieldName, fieldDef] of Object.entries(boundsSchema.fields)) {
602
+ const bt = fieldDef.boundType;
603
+ if (bt && (bt.kind === "per_transaction" || bt.kind === "cumulative_sum")) readFields.add(bt.of);
604
+ const max = fieldDef.maximum;
605
+ if (max !== void 0 && (typeof max !== "number" || !Number.isFinite(max))) {
606
+ errors.push({
607
+ code: "PROFILE_INVALID",
608
+ field: fieldName,
609
+ message: `Bounds field "${fieldName}": maximum MUST be a number (Bounds Schema rule 9).`
610
+ });
611
+ }
612
+ const enforcedBy = fieldDef.enforcedBy;
613
+ if (enforcedBy !== void 0 && enforcedBy !== "gatekeeper") {
614
+ errors.push({
615
+ code: "PROFILE_INVALID",
616
+ field: fieldName,
617
+ message: `Bounds field "${fieldName}": enforcedBy MUST be "gatekeeper" when present (Bounds Schema rule 11).`
618
+ });
619
+ }
620
+ }
621
+ for (const f of profile.disclose_fields ?? []) {
622
+ if (!readFields.has(f)) {
623
+ errors.push({
624
+ code: "PROFILE_INVALID",
625
+ field: "disclose_fields",
626
+ message: `disclose_fields names "${f}", which no bound reads \u2014 only what a ticket carries can be disclosed.`
627
+ });
628
+ }
629
+ }
630
+ }
631
+ const binding = profile.content_binding;
632
+ if (binding && String(binding.version) === "2" && binding.kind === "text") {
633
+ errors.push({
634
+ code: "PROFILE_INVALID",
635
+ field: "content_binding",
636
+ message: 'A version-2 content_binding is defined for kind "jcs" only; kind "text" is invalid.'
637
+ });
638
+ }
543
639
  return errors;
544
640
  }
545
641
  function checkDiscloseSubset(mandateDiscloseFields, profileDiscloseFields) {
@@ -706,7 +802,7 @@ function validateMandateOwners(payload) {
706
802
  if (!Array.isArray(owners) || owners.length !== 1) {
707
803
  throw new HapError(
708
804
  "MALFORMED_MANDATE",
709
- `mandate_owners MUST contain exactly one entry in v0.7, got ${Array.isArray(owners) ? owners.length : typeof owners}`
805
+ `mandate_owners MUST contain exactly one entry (Mandate rule 7), got ${Array.isArray(owners) ? owners.length : typeof owners}`
710
806
  );
711
807
  }
712
808
  const entry = owners[0];
@@ -725,17 +821,45 @@ function validateMandateOwners(payload) {
725
821
  throw new HapError("OWNER_SIGNATURE_INVALID", `co-signing DID ${entry.did} is not key-bearing`);
726
822
  }
727
823
  }
824
+ function validateAboveCapFields(payload) {
825
+ const hasCaps = payload.above_cap_caps !== void 0;
826
+ const hasApprovers = payload.above_cap_approvers !== void 0;
827
+ const hasCount = payload.above_cap_approvals !== void 0;
828
+ if (hasCaps !== hasApprovers) {
829
+ throw new HapError("MALFORMED_MANDATE", "above_cap_caps and above_cap_approvers are present together or absent together");
830
+ }
831
+ if (payload.commitment_mode === "review_above_cap" && !hasApprovers) {
832
+ throw new HapError("MALFORMED_MANDATE", "a review_above_cap mandate MUST carry above_cap_caps and above_cap_approvers");
833
+ }
834
+ if (payload.version === "0.7") {
835
+ if (hasCount) {
836
+ throw new HapError("MALFORMED_MANDATE", "a v0.7 mandate carries no above_cap_approvals");
837
+ }
838
+ if (hasApprovers && payload.commitment_mode !== "review_above_cap") {
839
+ throw new HapError("MALFORMED_MANDATE", "a v0.7 mandate carries above-cap fields only under review_above_cap");
840
+ }
841
+ return;
842
+ }
843
+ if (hasApprovers !== hasCount) {
844
+ throw new HapError("MALFORMED_MANDATE", "above_cap_approvals is present exactly when above_cap_approvers is (Mandate rule 10)");
845
+ }
846
+ if (hasCount) {
847
+ const n = payload.above_cap_approvals;
848
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1) {
849
+ throw new HapError("MALFORMED_MANDATE", `above_cap_approvals MUST be an integer >= 1, got ${JSON.stringify(n)}`);
850
+ }
851
+ }
852
+ if (hasApprovers && payload.commitment_mode === "automatic") {
853
+ throw new HapError("MALFORMED_MANDATE", "an automatic mandate carries no above-cap fields");
854
+ }
855
+ }
728
856
  async function verifyMandate(blob, opts) {
729
857
  const mandate = decodeMandateBlob(blob);
730
- if (mandate.payload.version !== PROTOCOL_VERSION) {
731
- throw new HapError(
732
- "VERSION_UNSUPPORTED",
733
- `mandate version ${JSON.stringify(mandate.payload.version)} is not verifiable by this implementation (verifies ${PROTOCOL_VERSION} only \u2014 re-issue the mandate)`
734
- );
735
- }
858
+ assertSupportedVersion(mandate.payload.version, "mandate");
736
859
  await verifyMandateSignature(mandate, opts);
737
860
  checkMandateExpiry(mandate.payload, opts?.now);
738
861
  validateMandateOwners(mandate.payload);
862
+ validateAboveCapFields(mandate.payload);
739
863
  return mandate.payload;
740
864
  }
741
865
 
@@ -772,9 +896,10 @@ function buildMandateProjection(payload, entry) {
772
896
  "mandate projection requires profile_hash, bounds_hash, scope_hash and commitment_mode"
773
897
  );
774
898
  }
899
+ const version = payload.version === "0.7" ? "0.7" : "0.8";
775
900
  const projection = {
776
901
  typ: "HAP-mandate-projection",
777
- version: "0.7",
902
+ version,
778
903
  profile_id,
779
904
  profile_hash,
780
905
  owner_did: entry.did,
@@ -789,13 +914,32 @@ function buildMandateProjection(payload, entry) {
789
914
  if (payload.intent_disclosure_hash !== void 0) {
790
915
  projection.intent_disclosure_hash = payload.intent_disclosure_hash;
791
916
  }
792
- if (payload.commitment_mode === "review_above_cap") {
793
- projection.above_cap_caps = payload.above_cap_caps;
794
- projection.above_cap_approvers = payload.above_cap_approvers;
917
+ if (version === "0.7") {
918
+ if (payload.commitment_mode === "review_above_cap") {
919
+ projection.above_cap_caps = payload.above_cap_caps;
920
+ projection.above_cap_approvers = payload.above_cap_approvers;
921
+ }
922
+ } else {
923
+ if (payload.above_cap_approvers !== void 0) {
924
+ projection.above_cap_caps = payload.above_cap_caps;
925
+ projection.above_cap_approvers = payload.above_cap_approvers;
926
+ projection.above_cap_approvals = payload.above_cap_approvals;
927
+ }
795
928
  }
796
929
  if (payload.disclose_fields !== void 0) {
797
930
  projection.disclose_fields = payload.disclose_fields;
798
931
  }
932
+ if (version === "0.8") {
933
+ if (entry.binding === void 0 || entry.signing_surface === void 0) {
934
+ throw new OwnerSignatureError(
935
+ "OWNER_SIGNATURE_INVALID",
936
+ "a v0.8 projection requires the entry's binding and signing_surface"
937
+ );
938
+ }
939
+ projection.binding = entry.binding;
940
+ projection.signing_surface = entry.signing_surface;
941
+ projection.subjects_disclosed = (payload.subjects ?? []).some((s) => s.did === entry.did);
942
+ }
799
943
  return projection;
800
944
  }
801
945
  function projectionSigningBytes(projection) {
@@ -841,11 +985,14 @@ async function verifyOwnerSignatures(mandate) {
841
985
  return entries;
842
986
  }
843
987
  async function verifyApproval(approval, signature, signerDid) {
988
+ if (approval.version === "0.8" && approval.approver_did !== signerDid) {
989
+ throw new OwnerSignatureError("APPROVAL_SIGNATURE_INVALID", `approval names approver ${approval.approver_did}, verified under ${signerDid}`);
990
+ }
844
991
  let publicKey;
845
992
  try {
846
993
  publicKey = decodeDidKey(signerDid);
847
994
  } catch (err) {
848
- throw new OwnerSignatureError("OWNER_SIGNATURE_INVALID", err.message);
995
+ throw new OwnerSignatureError("APPROVAL_SIGNATURE_INVALID", err.message);
849
996
  }
850
997
  const ok = await ed2.verifyAsync(decodeOrThrow(signature, "APPROVAL_SIGNATURE_INVALID"), approvalSigningBytes(approval), publicKey).catch(() => false);
851
998
  if (!ok) {
@@ -853,6 +1000,9 @@ async function verifyApproval(approval, signature, signerDid) {
853
1000
  }
854
1001
  }
855
1002
  var BINDING_RANK = { raw: 0, webauthn: 1, eudi: 2 };
1003
+ function bindingRank(binding) {
1004
+ return BINDING_RANK[binding] ?? 0;
1005
+ }
856
1006
  function checkOwnerSignatureRequirement(payload, floor) {
857
1007
  if (!floor?.required) return;
858
1008
  const entry = payload.mandate_owners[0];
@@ -864,7 +1014,7 @@ function checkOwnerSignatureRequirement(payload, floor) {
864
1014
  );
865
1015
  }
866
1016
  const minRank = floor.minBinding ? BINDING_RANK[floor.minBinding] : 0;
867
- const haveRank = BINDING_RANK[entry.binding] ?? 0;
1017
+ const haveRank = bindingRank(entry.binding);
868
1018
  if (haveRank < minRank) {
869
1019
  throw new OwnerSignatureError(
870
1020
  "OWNER_SIGNATURE_REQUIRED",
@@ -875,11 +1025,27 @@ function checkOwnerSignatureRequirement(payload, floor) {
875
1025
 
876
1026
  // src/ticket.ts
877
1027
  import * as ed3 from "@noble/ed25519";
1028
+ var TICKET_FRESHNESS_SECONDS = 300;
878
1029
  async function signTicket(payload, privateKey) {
879
1030
  const signature = toBase64Url(await ed3.signAsync(new TextEncoder().encode(canonicalize(payload)), privateKey));
880
1031
  return { ...payload, signature };
881
1032
  }
1033
+ function assertTicketShape(ticket) {
1034
+ if ("typ" in ticket) {
1035
+ throw new HapError("INVALID_SIGNATURE", "a ticket carries no typ \u2014 an object with one is a projection, not a ticket");
1036
+ }
1037
+ if ("mandate_id" in ticket || "issued_at" in ticket) {
1038
+ throw new HapError("INVALID_SIGNATURE", "a ticket carries no mandate_id or issued_at \u2014 this object is a mandate payload");
1039
+ }
1040
+ for (const required of ["id", "mandateId", "actionType", "timestamp", "issuer"]) {
1041
+ if (ticket[required] === void 0) {
1042
+ throw new HapError("INVALID_SIGNATURE", `ticket is missing required field ${required}`);
1043
+ }
1044
+ }
1045
+ }
882
1046
  async function verifyTicketSignature(ticket, opts) {
1047
+ assertTicketShape(ticket);
1048
+ assertSupportedVersion(ticket.version, "ticket");
883
1049
  const { signature, ...unsigned } = ticket;
884
1050
  if (!signature) {
885
1051
  throw new HapError("INVALID_SIGNATURE", "ticket carries no signature");
@@ -905,6 +1071,120 @@ async function verifyTicketSignature(ticket, opts) {
905
1071
  throw new HapError("INVALID_SIGNATURE", "ticket signature verification failed");
906
1072
  }
907
1073
  }
1074
+ var ECHOED_FIELDS = [
1075
+ "profileId",
1076
+ "actionType",
1077
+ "boundsHash",
1078
+ "executionContext",
1079
+ "contentHash",
1080
+ "contentBinding",
1081
+ "proposalId",
1082
+ "proposalContentHash"
1083
+ ];
1084
+ function verifyTicketAgainstRequest(ticket, request, opts = {}) {
1085
+ for (const field of ECHOED_FIELDS) {
1086
+ const sent = request[field];
1087
+ const got = ticket[field];
1088
+ if (sent === void 0 && got === void 0) continue;
1089
+ if (sent === void 0 || got === void 0 || canonicalize(sent) !== canonicalize(got)) {
1090
+ throw new HapError("TICKET_REQUEST_MISMATCH", `ticket ${field} differs from the request's`);
1091
+ }
1092
+ }
1093
+ if (request.idempotencyKey !== void 0 && ticket.idempotencyKey !== request.idempotencyKey) {
1094
+ throw new HapError(
1095
+ "TICKET_REQUEST_MISMATCH",
1096
+ ticket.idempotencyKey === void 0 ? "ticket carries no idempotencyKey although the request sent one" : "ticket idempotencyKey differs from the one the request sent"
1097
+ );
1098
+ }
1099
+ const expectedMandateId = opts.mandate?.payload.mandate_id ?? request.mandateId;
1100
+ if (expectedMandateId !== void 0 && ticket.mandateId !== expectedMandateId) {
1101
+ throw new HapError("TICKET_REQUEST_MISMATCH", `ticket mandateId ${ticket.mandateId} is not the mandate this request was made under`);
1102
+ }
1103
+ if (opts.mandate && ticket.version !== opts.mandate.payload.version) {
1104
+ throw new HapError("TICKET_REQUEST_MISMATCH", `ticket version ${ticket.version} differs from its mandate's ${opts.mandate.payload.version}`);
1105
+ }
1106
+ if (!opts.replay) {
1107
+ const now = opts.now ?? Math.floor(Date.now() / 1e3);
1108
+ if (Math.abs(now - ticket.timestamp) > TICKET_FRESHNESS_SECONDS) {
1109
+ throw new HapError("TICKET_REQUEST_MISMATCH", `ticket timestamp ${ticket.timestamp} is outside the freshness window at ${now}`);
1110
+ }
1111
+ }
1112
+ }
1113
+ function reconstructApproval(ticket, entry, approverDid) {
1114
+ if (!ticket.proposalId) {
1115
+ throw new HapError("APPROVAL_SIGNATURE_INVALID", "ticket carries an approval signature but no proposalId");
1116
+ }
1117
+ if (ticket.version === "0.7") {
1118
+ if (!ticket.contentHash) {
1119
+ throw new HapError("APPROVAL_SIGNATURE_INVALID", "a v0.7 approval without a content binding is not reconstructible from the ticket alone");
1120
+ }
1121
+ return {
1122
+ typ: "HAP-approval",
1123
+ version: "0.7",
1124
+ proposal_id: ticket.proposalId,
1125
+ mandate_id: ticket.mandateId,
1126
+ decision: "commit",
1127
+ content_hash: ticket.contentHash,
1128
+ decided_at: entry.decided_at,
1129
+ nonce: entry.nonce
1130
+ };
1131
+ }
1132
+ if (!ticket.proposalContentHash) {
1133
+ throw new HapError("APPROVAL_SIGNATURE_INVALID", "a v0.8 review-path ticket carries proposalContentHash");
1134
+ }
1135
+ return {
1136
+ typ: "HAP-approval",
1137
+ version: "0.8",
1138
+ approver_did: approverDid,
1139
+ proposal_id: ticket.proposalId,
1140
+ mandate_id: ticket.mandateId,
1141
+ action: ticket.action,
1142
+ action_type: ticket.actionType,
1143
+ decision: "commit",
1144
+ content_hash: ticket.contentHash ?? ticket.proposalContentHash,
1145
+ proposal_content_hash: ticket.proposalContentHash,
1146
+ decided_at: entry.decided_at,
1147
+ nonce: entry.nonce
1148
+ };
1149
+ }
1150
+ async function verifyTicketApprovals(ticket, mandate) {
1151
+ if (ticket.mandateId !== mandate.payload.mandate_id) {
1152
+ throw new HapError("APPROVAL_SIGNATURE_INVALID", "the mandate supplied is not the one the ticket names");
1153
+ }
1154
+ const ownerDid = mandate.payload.mandate_owners[0]?.did;
1155
+ const result = { approvers: [] };
1156
+ try {
1157
+ if (ticket.approvalSignature) {
1158
+ const approval = reconstructApproval(ticket, ticket.approvalSignature, ownerDid);
1159
+ await verifyApproval(approval, ticket.approvalSignature.signature, ownerDid);
1160
+ result.owner = ownerDid;
1161
+ }
1162
+ const signed = new Set(mandate.payload.above_cap_approvers ?? []);
1163
+ for (const entry of ticket.approvalSignatures ?? []) {
1164
+ if (!signed.has(entry.approver_did)) {
1165
+ throw new HapError("APPROVAL_SIGNATURE_INVALID", `approver ${entry.approver_did} is not in the mandate's signed above_cap_approvers`);
1166
+ }
1167
+ if (entry.approver_did === ownerDid) {
1168
+ throw new HapError("APPROVAL_SIGNATURE_INVALID", "the proposer never counts as an above-cap approver");
1169
+ }
1170
+ const approval = reconstructApproval(ticket, entry, entry.approver_did);
1171
+ await verifyApproval(approval, entry.signature, entry.approver_did);
1172
+ if (!result.approvers.includes(entry.approver_did)) result.approvers.push(entry.approver_did);
1173
+ }
1174
+ } catch (err) {
1175
+ if (err instanceof OwnerSignatureError) throw new HapError(err.code, err.message);
1176
+ throw err;
1177
+ }
1178
+ return result;
1179
+ }
1180
+ function approvalsSufficient(mandate, verified, aboveCap) {
1181
+ const p = mandate.payload;
1182
+ const ownerNeeded = p.commitment_mode === "review";
1183
+ if (ownerNeeded && !verified.owner) return false;
1184
+ if (!aboveCap) return true;
1185
+ const required = p.version === "0.7" ? (p.above_cap_approvers ?? []).length : p.above_cap_approvals ?? 1;
1186
+ return verified.approvers.length >= required;
1187
+ }
908
1188
  function publicTicketView(ticket, governingDiscloseFields = []) {
909
1189
  const { userId: _userId, cumulativeState: _cumulativeState, limits: _limits, executionContext, ...rest } = ticket;
910
1190
  const allowed = new Set(governingDiscloseFields);
@@ -915,6 +1195,475 @@ function publicTicketView(ticket, governingDiscloseFields = []) {
915
1195
  return { ...rest, executionContext: disclosedExecutionContext };
916
1196
  }
917
1197
 
1198
+ // src/ticket-projection.ts
1199
+ import * as ed4 from "@noble/ed25519";
1200
+ var PROJECTION_MAX_VALIDITY_SECONDS = 86400;
1201
+ function buildTicketProjection(ticket, mandate, opts) {
1202
+ if (ticket.mandateId !== mandate.payload.mandate_id) {
1203
+ throw new HapError("MALFORMED_TICKET_REQUEST", "the mandate supplied is not the one the ticket names");
1204
+ }
1205
+ if (opts.validUntil > ticket.timestamp + PROJECTION_MAX_VALIDITY_SECONDS) {
1206
+ throw new HapError("MALFORMED_TICKET_REQUEST", `valid_until MUST NOT end later than ${PROJECTION_MAX_VALIDITY_SECONDS}s after the ticket's timestamp`);
1207
+ }
1208
+ const projection = {
1209
+ typ: "HAP-ticket-projection",
1210
+ version: ticket.version,
1211
+ id: ticket.id,
1212
+ mandateId: ticket.mandateId,
1213
+ issuer: ticket.issuer,
1214
+ timestamp: ticket.timestamp,
1215
+ valid_until: opts.validUntil,
1216
+ profileId: ticket.profileId,
1217
+ actionType: ticket.actionType,
1218
+ boundsHash: ticket.boundsHash
1219
+ };
1220
+ if (ticket.contentHash !== void 0) projection.contentHash = ticket.contentHash;
1221
+ if (ticket.contentBinding !== void 0) projection.contentBinding = ticket.contentBinding;
1222
+ if (ticket.subjects !== void 0) projection.subjects = ticket.subjects;
1223
+ const disclosed = {};
1224
+ for (const field of opts.discloseFields ?? []) {
1225
+ if (ticket.executionContext[field] !== void 0) disclosed[field] = ticket.executionContext[field];
1226
+ }
1227
+ if (Object.keys(disclosed).length > 0) projection.executionContext = disclosed;
1228
+ const owner = mandate.payload.mandate_owners[0];
1229
+ if (owner?.signature !== void 0) projection.owner = owner;
1230
+ return projection;
1231
+ }
1232
+ async function signTicketProjection(projection, privateKey) {
1233
+ const signature = toBase64Url(await ed4.signAsync(new TextEncoder().encode(canonicalize(projection)), privateKey));
1234
+ return { ...projection, signature };
1235
+ }
1236
+ async function verifyTicketProjection(projection, opts = {}) {
1237
+ if (projection.typ !== "HAP-ticket-projection") {
1238
+ throw new HapError("INVALID_SIGNATURE", 'a projection verifier requires typ "HAP-ticket-projection"');
1239
+ }
1240
+ for (const k of ["userId", "cumulativeState", "limits", "action"]) {
1241
+ if (k in projection) throw new HapError("INVALID_SIGNATURE", `a projection MUST NOT carry ${k}`);
1242
+ }
1243
+ if (opts.trustedIssuers && !opts.trustedIssuers.includes(projection.issuer)) {
1244
+ throw new HapError("INVALID_SIGNATURE", `issuer ${JSON.stringify(projection.issuer)} is not a trusted Authority Server`);
1245
+ }
1246
+ const { signature, ...unsigned } = projection;
1247
+ if (!signature) throw new HapError("INVALID_SIGNATURE", "projection carries no signature");
1248
+ let publicKey;
1249
+ try {
1250
+ publicKey = decodeDidKey(projection.issuer);
1251
+ } catch (err) {
1252
+ throw new HapError("INVALID_SIGNATURE", `cannot resolve a key from issuer ${JSON.stringify(projection.issuer)}: ${err.message}`);
1253
+ }
1254
+ let sigBytes;
1255
+ try {
1256
+ sigBytes = fromBase64Url(signature);
1257
+ } catch (err) {
1258
+ throw new HapError("INVALID_SIGNATURE", err.message);
1259
+ }
1260
+ const ok = await ed4.verifyAsync(sigBytes, new TextEncoder().encode(canonicalize(unsigned)), publicKey).catch(() => false);
1261
+ if (!ok) throw new HapError("INVALID_SIGNATURE", "projection signature verification failed");
1262
+ if (opts.asAuthorization) {
1263
+ const now = opts.now ?? Math.floor(Date.now() / 1e3);
1264
+ if (now > projection.valid_until) {
1265
+ throw new HapError("POLICY_UNMET", `projection presented after valid_until ${projection.valid_until}`);
1266
+ }
1267
+ }
1268
+ }
1269
+ async function verifyProjectionOwner(projection, mandate) {
1270
+ if (!projection.owner) {
1271
+ throw new HapError("OWNER_SIGNATURE_INVALID", "projection carries no owner entry");
1272
+ }
1273
+ if (projection.mandateId !== mandate.payload.mandate_id) {
1274
+ throw new HapError("OWNER_SIGNATURE_INVALID", "the mandate supplied is not the one the projection names");
1275
+ }
1276
+ const own = mandate.payload.mandate_owners[0];
1277
+ if (!own || canonicalize(own) !== canonicalize(projection.owner)) {
1278
+ throw new HapError("OWNER_SIGNATURE_INVALID", "projection owner entry differs from the mandate's mandate_owners entry");
1279
+ }
1280
+ try {
1281
+ await verifyOwnerSignature(mandate, projection.owner);
1282
+ } catch (err) {
1283
+ if (err instanceof OwnerSignatureError) throw new HapError(err.code, err.message);
1284
+ throw err;
1285
+ }
1286
+ }
1287
+
1288
+ // src/verifier-policy.ts
1289
+ var PolicyError = class extends HapError {
1290
+ constructor(line, code, message) {
1291
+ super(code, `${line}: ${message}`);
1292
+ this.line = line;
1293
+ this.name = "PolicyError";
1294
+ }
1295
+ };
1296
+ async function evaluateVerifierPolicy(policy, input) {
1297
+ if (policy.typ !== "HAP-verifier-policy") {
1298
+ throw new HapError("POLICY_UNMET", 'policy document must carry typ "HAP-verifier-policy"');
1299
+ }
1300
+ const artifact = input.ticket ?? input.projection;
1301
+ if (!artifact) throw new HapError("POLICY_UNMET", "no ticket or projection to evaluate");
1302
+ const now = input.now ?? Math.floor(Date.now() / 1e3);
1303
+ try {
1304
+ if (input.ticket) {
1305
+ await verifyTicketSignature(input.ticket, { trustedIssuers: policy.trusted_issuers });
1306
+ } else if (input.projection) {
1307
+ await verifyTicketProjection(input.projection, { trustedIssuers: policy.trusted_issuers, now, asAuthorization: input.asAuthorization });
1308
+ }
1309
+ if (input.mandate) {
1310
+ await verifyMandateSignature(input.mandate, { trustedIssuers: policy.trusted_issuers });
1311
+ }
1312
+ } catch (err) {
1313
+ const code = err instanceof HapError ? err.code : "INVALID_SIGNATURE";
1314
+ throw new PolicyError(code === "POLICY_UNMET" ? "freshness_seconds" : "trusted_issuers", code, err.message);
1315
+ }
1316
+ let verifiedOwnerDids;
1317
+ const needOwners = policy.min_binding !== void 0 || policy.required_owners !== void 0;
1318
+ if (needOwners) {
1319
+ verifiedOwnerDids = /* @__PURE__ */ new Map();
1320
+ if (input.mandate) {
1321
+ try {
1322
+ const entries = await verifyOwnerSignatures(input.mandate);
1323
+ for (const e of entries) verifiedOwnerDids.set(e.did, e.binding ?? "raw");
1324
+ if (input.projection?.owner) await verifyProjectionOwner(input.projection, input.mandate);
1325
+ } catch (err) {
1326
+ throw new PolicyError("min_binding", "OWNER_SIGNATURE_REQUIRED", `no verifiable owner signature: ${err.message}`);
1327
+ }
1328
+ }
1329
+ }
1330
+ if (policy.min_binding !== void 0) {
1331
+ const minRank = BINDING_RANK[policy.min_binding];
1332
+ if (minRank === void 0) {
1333
+ throw new PolicyError("min_binding", "OWNER_SIGNATURE_REQUIRED", `unknown binding "${policy.min_binding}" \u2014 satisfies no minimum`);
1334
+ }
1335
+ const best = Math.max(-1, ...[...verifiedOwnerDids.values()].map(bindingRank));
1336
+ if (best < minRank) {
1337
+ throw new PolicyError("min_binding", "OWNER_SIGNATURE_REQUIRED", `no verified owner signature at binding >= "${policy.min_binding}"`);
1338
+ }
1339
+ }
1340
+ if (policy.required_owners !== void 0) {
1341
+ for (const did of policy.required_owners) {
1342
+ if (!verifiedOwnerDids.has(did)) {
1343
+ throw new PolicyError("required_owners", "OWNER_SIGNATURE_REQUIRED", `no verified owner signature by ${did}`);
1344
+ }
1345
+ }
1346
+ }
1347
+ if (policy.profile_hashes !== void 0) {
1348
+ const mandate = input.mandate;
1349
+ if (!mandate) throw new PolicyError("profile_hashes", "PROFILE_HASH_MISMATCH", "the mandate is needed to check profile_hashes");
1350
+ const expected = policy.profile_hashes[mandate.payload.profile_id];
1351
+ if (expected === void 0) {
1352
+ throw new PolicyError("profile_hashes", "PROFILE_HASH_MISMATCH", `profile ${mandate.payload.profile_id} is not listed`);
1353
+ }
1354
+ if (expected !== mandate.payload.profile_hash) {
1355
+ throw new PolicyError("profile_hashes", "PROFILE_HASH_MISMATCH", `profile_hash differs from the listed one for ${mandate.payload.profile_id}`);
1356
+ }
1357
+ if (mandate.payload.mandate_id !== artifact.mandateId) {
1358
+ throw new PolicyError("profile_hashes", "PROFILE_HASH_MISMATCH", "the mandate supplied is not the one the artifact names");
1359
+ }
1360
+ }
1361
+ if (policy.action_types !== void 0 && !policy.action_types.includes(artifact.actionType)) {
1362
+ throw new PolicyError("action_types", "POLICY_UNMET", `actionType ${artifact.actionType} is not in the listed set`);
1363
+ }
1364
+ if (policy.freshness_seconds !== void 0) {
1365
+ if (now - artifact.timestamp > policy.freshness_seconds) {
1366
+ throw new PolicyError("freshness_seconds", "POLICY_UNMET", `ticket is ${now - artifact.timestamp}s old, policy allows ${policy.freshness_seconds}s`);
1367
+ }
1368
+ if (input.projection && input.asAuthorization && now > input.projection.valid_until) {
1369
+ throw new PolicyError("freshness_seconds", "POLICY_UNMET", "projection presented after valid_until");
1370
+ }
1371
+ }
1372
+ if (policy.required_fields !== void 0) {
1373
+ const ctx = input.ticket?.executionContext ?? input.projection?.executionContext ?? {};
1374
+ for (const f of policy.required_fields) {
1375
+ if (ctx[f] === void 0) {
1376
+ throw new PolicyError("required_fields", "POLICY_UNMET", `execution-context field ${f} is not ${input.ticket ? "present" : "disclosed"}`);
1377
+ }
1378
+ }
1379
+ }
1380
+ if (policy.require_approval_signature) {
1381
+ const ticket = input.ticket;
1382
+ const mandate = input.mandate;
1383
+ if (!ticket || !mandate) {
1384
+ throw new PolicyError("require_approval_signature", "APPROVAL_SIGNATURE_REQUIRED", "the complete ticket and its mandate are needed to verify approvals");
1385
+ }
1386
+ if (ticket.proposalId !== void 0) {
1387
+ let verified;
1388
+ try {
1389
+ verified = await verifyTicketApprovals(ticket, mandate);
1390
+ } catch (err) {
1391
+ throw new PolicyError("require_approval_signature", "APPROVAL_SIGNATURE_REQUIRED", err.message);
1392
+ }
1393
+ const aboveCap = input.aboveCap ?? (ticket.approvalSignatures !== void 0 && ticket.approvalSignatures.length > 0);
1394
+ if (!approvalsSufficient(mandate, verified, aboveCap)) {
1395
+ throw new PolicyError("require_approval_signature", "APPROVAL_SIGNATURE_REQUIRED", "the ticket lacks a verifiable signature for each decision it rests on");
1396
+ }
1397
+ }
1398
+ }
1399
+ }
1400
+
1401
+ // src/proposal-content.ts
1402
+ import { createHash as createHash6 } from "crypto";
1403
+ import { CipherSuite, DhkemX25519HkdfSha256, HkdfSha256, Aes256Gcm } from "@hpke/core";
1404
+ function suite() {
1405
+ return new CipherSuite({ kem: new DhkemX25519HkdfSha256(), kdf: new HkdfSha256(), aead: new Aes256Gcm() });
1406
+ }
1407
+ var webCrypto = () => {
1408
+ const c = globalThis.crypto;
1409
+ if (!c?.subtle) throw new Error("WebCrypto is not available in this runtime");
1410
+ return c;
1411
+ };
1412
+ function proposalContentJcs(args) {
1413
+ if (args === null || typeof args !== "object" || Array.isArray(args)) {
1414
+ throw new HapError("PROPOSAL_NOT_ENCRYPTED", "proposal content is one object \u2014 the action's argument object");
1415
+ }
1416
+ return canonicalize(args);
1417
+ }
1418
+ function computeProposalContentHash(args) {
1419
+ return `sha256:${createHash6("sha256").update(proposalContentJcs(args), "utf8").digest("hex")}`;
1420
+ }
1421
+ async function generateReaderKeyPair() {
1422
+ const kp = await suite().kem.generateKeyPair();
1423
+ return exportKeyPair(kp);
1424
+ }
1425
+ async function deriveReaderKeyPair(ikm) {
1426
+ const kp = await suite().kem.deriveKeyPair(ikm);
1427
+ return exportKeyPair(kp);
1428
+ }
1429
+ async function exportKeyPair(kp) {
1430
+ const s = suite();
1431
+ return {
1432
+ privateKey: new Uint8Array(await s.kem.serializePrivateKey(kp.privateKey)),
1433
+ publicKey: new Uint8Array(await s.kem.serializePublicKey(kp.publicKey))
1434
+ };
1435
+ }
1436
+ async function encryptProposalContent(args, readers, opts = {}) {
1437
+ if (readers.length === 0) {
1438
+ throw new HapError("PROPOSAL_NOT_ENCRYPTED", "a proposal has at least one reader (the Mandate Owner)");
1439
+ }
1440
+ const jcs = proposalContentJcs(args);
1441
+ const proposalContentHash = computeProposalContentHash(args);
1442
+ const crypto = webCrypto();
1443
+ const s = suite();
1444
+ const cek = opts.deterministic?.contentKey ?? crypto.getRandomValues(new Uint8Array(32));
1445
+ const iv = opts.deterministic?.iv ?? crypto.getRandomValues(new Uint8Array(12));
1446
+ if (cek.length !== 32 || iv.length !== 12) {
1447
+ throw new HapError("PROPOSAL_NOT_ENCRYPTED", "content key is 32 bytes and IV is 12 bytes");
1448
+ }
1449
+ const aesKey = await crypto.subtle.importKey("raw", cek, { name: "AES-GCM", length: 256 }, false, ["encrypt"]);
1450
+ const sealed = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, aesKey, new TextEncoder().encode(jcs)));
1451
+ const ciphertext = new Uint8Array(iv.length + sealed.length);
1452
+ ciphertext.set(iv, 0);
1453
+ ciphertext.set(sealed, iv.length);
1454
+ const encrypted_keys = {};
1455
+ for (const { did, publicKey } of readers) {
1456
+ const recipientPublicKey = await s.kem.deserializePublicKey(publicKey);
1457
+ const ekmIkm = opts.deterministic?.ephemeralIkm[did];
1458
+ const sender = await s.createSenderContext({
1459
+ recipientPublicKey,
1460
+ ...ekmIkm ? { ekm: await s.kem.deriveKeyPair(ekmIkm) } : {}
1461
+ });
1462
+ const ct = new Uint8Array(await sender.seal(cek));
1463
+ encrypted_keys[did] = { ct: toBase64Url(ct), enc: toBase64Url(new Uint8Array(sender.enc)) };
1464
+ }
1465
+ return {
1466
+ content: {
1467
+ ciphertext: toBase64Url(ciphertext),
1468
+ encrypted_keys,
1469
+ readers: readers.map((r) => r.did),
1470
+ unreadable: [...opts.unreadable ?? []]
1471
+ },
1472
+ proposalContentHash
1473
+ };
1474
+ }
1475
+ async function decryptProposalContent(content, readerDid, readerPrivateKey) {
1476
+ const wrap = content.encrypted_keys[readerDid];
1477
+ if (!wrap) throw new HapError("PROPOSAL_NOT_ENCRYPTED", `no key wrapped for reader ${readerDid}`);
1478
+ const s = suite();
1479
+ const crypto = webCrypto();
1480
+ let cek;
1481
+ try {
1482
+ const recipient = await s.createRecipientContext({
1483
+ recipientKey: await s.kem.deserializePrivateKey(readerPrivateKey),
1484
+ enc: fromBase64Url(wrap.enc)
1485
+ });
1486
+ cek = new Uint8Array(await recipient.open(fromBase64Url(wrap.ct)));
1487
+ } catch (err) {
1488
+ throw new HapError("PROPOSAL_NOT_ENCRYPTED", `proposal content is unreadable for ${readerDid}: ${err.message}`);
1489
+ }
1490
+ const bytes = fromBase64Url(content.ciphertext);
1491
+ if (bytes.length < 12 + 16) throw new HapError("PROPOSAL_NOT_ENCRYPTED", "ciphertext is shorter than IV + tag");
1492
+ let plaintext;
1493
+ try {
1494
+ const aesKey = await crypto.subtle.importKey("raw", cek, { name: "AES-GCM", length: 256 }, false, ["decrypt"]);
1495
+ plaintext = new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: bytes.subarray(0, 12) }, aesKey, bytes.subarray(12)));
1496
+ } catch (err) {
1497
+ throw new HapError("PROPOSAL_NOT_ENCRYPTED", `proposal content is unreadable for ${readerDid}: ${err.message}`);
1498
+ }
1499
+ const contentJcs = new TextDecoder().decode(plaintext);
1500
+ let args;
1501
+ try {
1502
+ args = JSON.parse(contentJcs);
1503
+ } catch {
1504
+ throw new HapError("PROPOSAL_NOT_ENCRYPTED", "decrypted content is not JSON");
1505
+ }
1506
+ return { arguments: args, contentJcs, proposalContentHash: computeProposalContentHash(args) };
1507
+ }
1508
+ function checkProposalReaders(content, expected) {
1509
+ const want = /* @__PURE__ */ new Set([expected.ownerDid, ...expected.deciders]);
1510
+ const have = /* @__PURE__ */ new Set([...content.readers, ...content.unreadable]);
1511
+ if (want.size !== have.size || [...want].some((d) => !have.has(d))) {
1512
+ throw new HapError("PROPOSAL_APPROVERS_INVALID", "readers \u222A unreadable differs from the owner plus the signed deciders");
1513
+ }
1514
+ const keyed = new Set(Object.keys(content.encrypted_keys));
1515
+ const readers = new Set(content.readers);
1516
+ if (keyed.size !== readers.size || [...readers].some((d) => !keyed.has(d))) {
1517
+ throw new HapError("PROPOSAL_NOT_ENCRYPTED", "readers differs from the key set of encrypted_keys");
1518
+ }
1519
+ }
1520
+
1521
+ // src/issuance.ts
1522
+ import { createHash as createHash7 } from "crypto";
1523
+ function computeExecutionContextHash(profile) {
1524
+ const schema = profile.executionContextSchema ?? {};
1525
+ return `sha256:${createHash7("sha256").update(canonicalize(schema), "utf8").digest("hex")}`;
1526
+ }
1527
+ function boundReadFields(profile) {
1528
+ const fields = /* @__PURE__ */ new Set();
1529
+ for (const def of Object.values(profile.boundsSchema?.fields ?? {})) {
1530
+ const bt = def.boundType;
1531
+ if (bt && (bt.kind === "per_transaction" || bt.kind === "cumulative_sum")) fields.add(bt.of);
1532
+ }
1533
+ return fields;
1534
+ }
1535
+ function wireExecutionContext(execution, profile) {
1536
+ const allowed = boundReadFields(profile);
1537
+ const out = {};
1538
+ for (const [k, v] of Object.entries(execution)) {
1539
+ if (!allowed.has(k) || k.startsWith("_") || v === void 0 || v === null) continue;
1540
+ out[k] = v;
1541
+ }
1542
+ return out;
1543
+ }
1544
+ function checkExecutionValues(execution, profile) {
1545
+ for (const k of Object.keys(execution)) {
1546
+ if (k.startsWith("_")) {
1547
+ throw new HapError("MALFORMED_TICKET_REQUEST", `executionContext key "${k}" is reserved and MUST NOT travel`);
1548
+ }
1549
+ }
1550
+ for (const field of boundReadFields(profile)) {
1551
+ const v = execution[field];
1552
+ if (v === void 0 || v === null) continue;
1553
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0) {
1554
+ throw new HapError(
1555
+ "MALFORMED_TICKET_REQUEST",
1556
+ `executionContext.${field} MUST be a finite, non-negative number, got ${JSON.stringify(v)}`
1557
+ );
1558
+ }
1559
+ }
1560
+ }
1561
+ function boundGoverns(def, actionType) {
1562
+ return !def.appliesTo || def.appliesTo.includes(actionType);
1563
+ }
1564
+ function missingSumFields(execution, profile, actionType) {
1565
+ const missing = [];
1566
+ for (const [name, def] of Object.entries(profile.boundsSchema?.fields ?? {})) {
1567
+ const bt = def.boundType;
1568
+ if (!bt || bt.kind !== "cumulative_sum" || !boundGoverns(def, actionType)) continue;
1569
+ const v = execution[bt.of];
1570
+ if (v === void 0 || v === null) missing.push({ bound: name, field: bt.of });
1571
+ }
1572
+ return missing;
1573
+ }
1574
+ function checkBoundsValues(bounds, profile) {
1575
+ for (const [name, def] of Object.entries(profile.boundsSchema?.fields ?? {})) {
1576
+ if (name === "profile") continue;
1577
+ const v = bounds[name];
1578
+ if (v === void 0 || v === null) continue;
1579
+ if (def.type === "number" || def.maximum !== void 0) {
1580
+ if (typeof v !== "number" || !Number.isFinite(v)) {
1581
+ throw new HapError("BOUNDS_INVALID_VALUE", `bound "${name}" MUST be a number, got ${JSON.stringify(v)}`);
1582
+ }
1583
+ if (def.maximum !== void 0 && v > def.maximum) {
1584
+ throw new HapError("BOUNDS_INVALID_VALUE", `bound "${name}" = ${v} exceeds the profile's maximum ${def.maximum}`);
1585
+ }
1586
+ }
1587
+ const bt = def.boundType;
1588
+ if (bt?.kind === "enum" && !bt.values.includes(String(v))) {
1589
+ throw new HapError("BOUNDS_INVALID_VALUE", `bound "${name}" = ${JSON.stringify(v)} is not in [${bt.values.join(", ")}]`);
1590
+ }
1591
+ }
1592
+ }
1593
+ function exceedsCap(value, cap, def) {
1594
+ const bt = def.boundType;
1595
+ if (bt?.kind === "enum") {
1596
+ const order = bt.values;
1597
+ const vi = order.indexOf(String(value));
1598
+ const ci = order.indexOf(String(cap));
1599
+ if (vi < 0 || ci < 0) return true;
1600
+ return vi < ci;
1601
+ }
1602
+ return typeof value !== "number" || typeof cap !== "number" || value > cap;
1603
+ }
1604
+ function checkGroupCeilings(bounds, profile, caps) {
1605
+ const fields = profile.boundsSchema?.fields ?? {};
1606
+ const hard = { ...caps.hard ?? {} };
1607
+ for (const [name, cap] of Object.entries(caps.approval ?? {})) {
1608
+ const def = fields[name];
1609
+ const bt = def?.boundType;
1610
+ if (!def || !bt) continue;
1611
+ if (bt.kind !== "per_transaction" || def.enforcedBy === "gatekeeper") hard[name] = cap;
1612
+ }
1613
+ for (const [name, def] of Object.entries(fields)) {
1614
+ if (name === "profile") continue;
1615
+ const cap = hard[name];
1616
+ if (cap === void 0) continue;
1617
+ const v = bounds[name];
1618
+ if (v === void 0 || v === null) {
1619
+ if (!def.required) {
1620
+ throw new HapError("GROUP_CAP_EXCEEDED", `bound "${name}" is omitted under a group ceiling of ${JSON.stringify(cap)} \u2014 an omitted bound is unlimited`);
1621
+ }
1622
+ continue;
1623
+ }
1624
+ if (exceedsCap(v, cap, def)) {
1625
+ throw new HapError("GROUP_CAP_EXCEEDED", `bound "${name}" = ${JSON.stringify(v)} exceeds the group's hard cap ${JSON.stringify(cap)}`);
1626
+ }
1627
+ }
1628
+ }
1629
+ function validateAboveCapConfig(config, profile, opts) {
1630
+ const fields = profile.boundsSchema?.fields ?? {};
1631
+ for (const name of Object.keys(config.caps ?? {})) {
1632
+ if (name === "profile") throw new HapError("ABOVE_CAP_CONFIG_INVALID", "above_cap_caps MUST NOT name profile");
1633
+ const def = fields[name];
1634
+ if (!def || def.boundType?.kind !== "per_transaction" || def.type !== "number") {
1635
+ throw new HapError("ABOVE_CAP_CONFIG_INVALID", `above_cap_caps names "${name}", which is not a numeric per_transaction bound of ${profile.id}`);
1636
+ }
1637
+ }
1638
+ for (const did of config.approvers ?? []) {
1639
+ if (!isKeyBearingDid(did)) {
1640
+ throw new HapError("ABOVE_CAP_CONFIG_INVALID", `above-cap approver ${did} is not a key-bearing DID`);
1641
+ }
1642
+ }
1643
+ if (config.approvers !== void 0 && !opts.inGroup) {
1644
+ const others = config.approvers.filter((d) => d !== opts.ownerDid).length;
1645
+ const n = config.approvals;
1646
+ if (n === void 0 || !Number.isInteger(n) || n < 1 || n > others) {
1647
+ throw new HapError(
1648
+ "ABOVE_CAP_CONFIG_INVALID",
1649
+ `above_cap_approvals MUST be an integer between 1 and the ${others} approver(s) other than the owner, got ${JSON.stringify(n)}`
1650
+ );
1651
+ }
1652
+ }
1653
+ }
1654
+ function capsExceeded(execution, mandate, profile) {
1655
+ const out = [];
1656
+ for (const [name, cap] of Object.entries(mandate.above_cap_caps ?? {})) {
1657
+ const bt = profile.boundsSchema?.fields[name]?.boundType;
1658
+ if (!bt || bt.kind !== "per_transaction") continue;
1659
+ const v = execution[bt.of];
1660
+ if (v === void 0 || v === null || typeof v !== "number" || v > cap) {
1661
+ out.push({ bound: name, field: bt.of, cap, value: v });
1662
+ }
1663
+ }
1664
+ return out;
1665
+ }
1666
+
918
1667
  // src/profiles/index.ts
919
1668
  var PROFILES = {};
920
1669
  function registerProfile(profileId, profile) {
@@ -973,8 +1722,9 @@ async function verify(request, opts = {}) {
973
1722
  }
974
1723
  const expectedProfileHash = computeProfileHash(profile);
975
1724
  const errors = [];
1725
+ const verifiedPayloads = [];
976
1726
  for (const blob of request.mandates) {
977
- const mandateErrors = await verifyOneMandate(blob, {
1727
+ const { errors: mandateErrors, payload } = await verifyOneMandate(blob, {
978
1728
  expectedBoundsHash,
979
1729
  expectedScopeHash,
980
1730
  expectedProfileHash,
@@ -982,6 +1732,7 @@ async function verify(request, opts = {}) {
982
1732
  now
983
1733
  });
984
1734
  errors.push(...mandateErrors);
1735
+ if (payload) verifiedPayloads.push(payload);
985
1736
  }
986
1737
  if (errors.length > 0) {
987
1738
  return { approved: false, errors };
@@ -1000,6 +1751,31 @@ async function verify(request, opts = {}) {
1000
1751
  };
1001
1752
  }
1002
1753
  }
1754
+ try {
1755
+ checkExecutionValues(request.execution, profile);
1756
+ } catch (err) {
1757
+ return { approved: false, errors: [{ code: codeFromThrown(err), message: err.message }] };
1758
+ }
1759
+ if (actionType !== void 0) {
1760
+ const capped = /* @__PURE__ */ new Set();
1761
+ for (const p of verifiedPayloads) {
1762
+ for (const name of Object.keys(p.above_cap_caps ?? {})) {
1763
+ const bt = profile.boundsSchema?.fields[name]?.boundType;
1764
+ if (bt && bt.kind === "per_transaction") capped.add(bt.of);
1765
+ }
1766
+ }
1767
+ const missing = missingSumFields(request.execution, profile, actionType).filter((m) => !capped.has(m.field));
1768
+ if (missing.length > 0) {
1769
+ return {
1770
+ approved: false,
1771
+ errors: missing.map(({ bound, field }) => ({
1772
+ code: "BOUND_EXCEEDED",
1773
+ field,
1774
+ message: `Bound "${bound}" governs "${actionType}" and reads "${field}", but this call exposes no ${field}. Refusing: an action without an amount would consume nothing of the limit (v0.8: a missing amount fails safe).`
1775
+ }))
1776
+ };
1777
+ }
1778
+ }
1003
1779
  const boundsErrors = checkBounds(request, profile, actionType);
1004
1780
  if (boundsErrors.length > 0) {
1005
1781
  return { approved: false, errors: boundsErrors };
@@ -1033,16 +1809,21 @@ function codeFromThrown(err) {
1033
1809
  return "MALFORMED_MANDATE";
1034
1810
  }
1035
1811
  async function verifyOneMandate(blob, opts) {
1812
+ const errors = await verifyOneMandateErrors(blob, opts);
1813
+ if (errors.length > 0) return { errors };
1814
+ return { errors, payload: decodeMandateBlob(blob).payload };
1815
+ }
1816
+ async function verifyOneMandateErrors(blob, opts) {
1036
1817
  let mandate;
1037
1818
  try {
1038
1819
  mandate = decodeMandateBlob(blob);
1039
1820
  } catch (err) {
1040
1821
  return [{ code: "MALFORMED_MANDATE", message: err.message }];
1041
1822
  }
1042
- if (mandate.payload.version !== PROTOCOL_VERSION) {
1823
+ if (!isSupportedVersion(mandate.payload.version)) {
1043
1824
  return [{
1044
1825
  code: "VERSION_UNSUPPORTED",
1045
- message: `mandate version ${JSON.stringify(mandate.payload.version)} is not verifiable by this Gatekeeper (verifies ${PROTOCOL_VERSION} only)`
1826
+ message: `mandate version ${JSON.stringify(mandate.payload.version)} is not verifiable by this Gatekeeper (verifies [${SUPPORTED_VERSIONS.join(", ")}])`
1046
1827
  }];
1047
1828
  }
1048
1829
  try {
@@ -1057,6 +1838,7 @@ async function verifyOneMandate(blob, opts) {
1057
1838
  }
1058
1839
  try {
1059
1840
  validateMandateOwners(mandate.payload);
1841
+ validateAboveCapFields(mandate.payload);
1060
1842
  } catch (err) {
1061
1843
  return [{ code: codeFromThrown(err), message: err.message }];
1062
1844
  }
@@ -1222,60 +2004,96 @@ function checkScopeConstraints(scope, execution, profile) {
1222
2004
  return errors;
1223
2005
  }
1224
2006
  export {
2007
+ BINDING_RANK,
2008
+ COMMITMENT_MODES,
1225
2009
  CanonicalValueError,
1226
2010
  ContentBindingError,
1227
2011
  HapError,
2012
+ ISSUABLE_VERSIONS,
1228
2013
  MANDATE_ERROR_CODES,
1229
2014
  OwnerSignatureError,
2015
+ PROJECTION_MAX_VALIDITY_SECONDS,
2016
+ PROPOSAL_ERROR_CODES,
1230
2017
  PROTOCOL_VERSION,
2018
+ PolicyError,
1231
2019
  SIGNABLE_COMMITMENT_MODES,
1232
2020
  SUPPORTED_VERSIONS,
1233
2021
  TICKET_ERROR_CODES,
2022
+ TICKET_FRESHNESS_SECONDS,
1234
2023
  allowedCommitmentModes,
1235
2024
  approvalSigningBytes,
2025
+ approvalsSufficient,
2026
+ assertSupportedVersion,
2027
+ assertTicketShape,
1236
2028
  bindingAppliesTo,
2029
+ bindingRank,
1237
2030
  boundActionTypes,
2031
+ boundGoverns,
2032
+ boundReadFields,
1238
2033
  buildMandateProjection,
2034
+ buildTicketProjection,
1239
2035
  canonicalBounds,
1240
2036
  canonicalScope,
1241
2037
  canonicalize,
1242
2038
  canonicalizeText,
2039
+ capsExceeded,
2040
+ checkBoundsValues,
1243
2041
  checkDiscloseSubset,
2042
+ checkExecutionValues,
2043
+ checkGroupCeilings,
1244
2044
  checkMandateExpiry,
1245
2045
  checkOwnerSignatureRequirement,
2046
+ checkProposalReaders,
1246
2047
  clearProfiles,
1247
2048
  computeBoundsHash,
1248
2049
  computeContentHash,
2050
+ computeExecutionContextHash,
1249
2051
  computeFieldsContentHash,
1250
2052
  computeIntentDisclosureHash,
1251
2053
  computeIntentHash,
1252
2054
  computeProfileHash,
2055
+ computeProposalContentHash,
1253
2056
  computeScopeHash,
1254
2057
  contentCanonicalBytes,
1255
2058
  decodeDidKey,
1256
2059
  decodeMandateBlob,
2060
+ decryptProposalContent,
1257
2061
  deriveIdentityLine,
2062
+ deriveReaderKeyPair,
1258
2063
  encodeDidKey,
1259
2064
  encodeMandateBlob,
2065
+ encryptProposalContent,
2066
+ evaluateVerifierPolicy,
2067
+ exceedsCap,
2068
+ filesFields,
1260
2069
  fromBase64Url,
2070
+ generateReaderKeyPair,
1261
2071
  getAllProfiles,
1262
2072
  getProfile,
1263
2073
  intentDisclosureCanonicalBytes,
2074
+ invalidFieldKindsReason,
1264
2075
  isCommitmentModeAllowed,
1265
2076
  isFieldBinding,
1266
2077
  isKeyBearingDid,
2078
+ isSupportedVersion,
1267
2079
  listProfiles,
1268
2080
  mandateBlobId,
2081
+ missingSumFields,
1269
2082
  negotiateVersion,
1270
2083
  projectionSigningBytes,
2084
+ proposalContentJcs,
1271
2085
  publicTicketView,
2086
+ reconstructApproval,
1272
2087
  registerProfile,
1273
2088
  selectBoundFields,
1274
2089
  signApproval,
1275
2090
  signMandate,
1276
2091
  signMandateProjection,
1277
2092
  signTicket,
2093
+ signTicketProjection,
1278
2094
  toBase64Url,
2095
+ validateAboveCapConfig,
2096
+ validateAboveCapFields,
1279
2097
  validateBoundsParams,
1280
2098
  validateMandateOwners,
1281
2099
  validateProfile,
@@ -1287,5 +2105,10 @@ export {
1287
2105
  verifyMandateSignature,
1288
2106
  verifyOwnerSignature,
1289
2107
  verifyOwnerSignatures,
1290
- verifyTicketSignature
2108
+ verifyProjectionOwner,
2109
+ verifyTicketAgainstRequest,
2110
+ verifyTicketApprovals,
2111
+ verifyTicketProjection,
2112
+ verifyTicketSignature,
2113
+ wireExecutionContext
1291
2114
  };