@humanagencyp/hap-core 0.12.1 → 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";
@@ -410,6 +448,7 @@ function computeScopeHash(params, profile) {
410
448
 
411
449
  // src/commitment-modes.ts
412
450
  var SIGNABLE_COMMITMENT_MODES = ["review", "automatic"];
451
+ var COMMITMENT_MODES = ["automatic", "review", "review_above_cap"];
413
452
  var isSignable = (v) => typeof v === "string" && SIGNABLE_COMMITMENT_MODES.includes(v);
414
453
  function allowedCommitmentModes(profile) {
415
454
  const declared = profile.commitment_modes;
@@ -418,7 +457,9 @@ function allowedCommitmentModes(profile) {
418
457
  return SIGNABLE_COMMITMENT_MODES.filter((m) => declared.includes(m));
419
458
  }
420
459
  function isCommitmentModeAllowed(profile, mode) {
421
- 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);
422
463
  }
423
464
 
424
465
  // src/profile.ts
@@ -533,6 +574,7 @@ function validateProfile(profile) {
533
574
  }
534
575
  const seen = /* @__PURE__ */ new Set();
535
576
  for (const m of declaredModes) {
577
+ if (m === "review_above_cap") continue;
536
578
  if (!SIGNABLE_COMMITMENT_MODES.includes(m)) {
537
579
  errors.push({
538
580
  code: "PROFILE_INVALID",
@@ -554,6 +596,46 @@ function validateProfile(profile) {
554
596
  message: "commitment_modes declares no mode this profile can actually use."
555
597
  });
556
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
+ }
557
639
  return errors;
558
640
  }
559
641
  function checkDiscloseSubset(mandateDiscloseFields, profileDiscloseFields) {
@@ -720,7 +802,7 @@ function validateMandateOwners(payload) {
720
802
  if (!Array.isArray(owners) || owners.length !== 1) {
721
803
  throw new HapError(
722
804
  "MALFORMED_MANDATE",
723
- `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}`
724
806
  );
725
807
  }
726
808
  const entry = owners[0];
@@ -739,17 +821,45 @@ function validateMandateOwners(payload) {
739
821
  throw new HapError("OWNER_SIGNATURE_INVALID", `co-signing DID ${entry.did} is not key-bearing`);
740
822
  }
741
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
+ }
742
856
  async function verifyMandate(blob, opts) {
743
857
  const mandate = decodeMandateBlob(blob);
744
- if (mandate.payload.version !== PROTOCOL_VERSION) {
745
- throw new HapError(
746
- "VERSION_UNSUPPORTED",
747
- `mandate version ${JSON.stringify(mandate.payload.version)} is not verifiable by this implementation (verifies ${PROTOCOL_VERSION} only \u2014 re-issue the mandate)`
748
- );
749
- }
858
+ assertSupportedVersion(mandate.payload.version, "mandate");
750
859
  await verifyMandateSignature(mandate, opts);
751
860
  checkMandateExpiry(mandate.payload, opts?.now);
752
861
  validateMandateOwners(mandate.payload);
862
+ validateAboveCapFields(mandate.payload);
753
863
  return mandate.payload;
754
864
  }
755
865
 
@@ -786,9 +896,10 @@ function buildMandateProjection(payload, entry) {
786
896
  "mandate projection requires profile_hash, bounds_hash, scope_hash and commitment_mode"
787
897
  );
788
898
  }
899
+ const version = payload.version === "0.7" ? "0.7" : "0.8";
789
900
  const projection = {
790
901
  typ: "HAP-mandate-projection",
791
- version: "0.7",
902
+ version,
792
903
  profile_id,
793
904
  profile_hash,
794
905
  owner_did: entry.did,
@@ -803,13 +914,32 @@ function buildMandateProjection(payload, entry) {
803
914
  if (payload.intent_disclosure_hash !== void 0) {
804
915
  projection.intent_disclosure_hash = payload.intent_disclosure_hash;
805
916
  }
806
- if (payload.commitment_mode === "review_above_cap") {
807
- projection.above_cap_caps = payload.above_cap_caps;
808
- projection.above_cap_approvers = payload.above_cap_approvers;
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
+ }
809
928
  }
810
929
  if (payload.disclose_fields !== void 0) {
811
930
  projection.disclose_fields = payload.disclose_fields;
812
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
+ }
813
943
  return projection;
814
944
  }
815
945
  function projectionSigningBytes(projection) {
@@ -855,11 +985,14 @@ async function verifyOwnerSignatures(mandate) {
855
985
  return entries;
856
986
  }
857
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
+ }
858
991
  let publicKey;
859
992
  try {
860
993
  publicKey = decodeDidKey(signerDid);
861
994
  } catch (err) {
862
- throw new OwnerSignatureError("OWNER_SIGNATURE_INVALID", err.message);
995
+ throw new OwnerSignatureError("APPROVAL_SIGNATURE_INVALID", err.message);
863
996
  }
864
997
  const ok = await ed2.verifyAsync(decodeOrThrow(signature, "APPROVAL_SIGNATURE_INVALID"), approvalSigningBytes(approval), publicKey).catch(() => false);
865
998
  if (!ok) {
@@ -867,6 +1000,9 @@ async function verifyApproval(approval, signature, signerDid) {
867
1000
  }
868
1001
  }
869
1002
  var BINDING_RANK = { raw: 0, webauthn: 1, eudi: 2 };
1003
+ function bindingRank(binding) {
1004
+ return BINDING_RANK[binding] ?? 0;
1005
+ }
870
1006
  function checkOwnerSignatureRequirement(payload, floor) {
871
1007
  if (!floor?.required) return;
872
1008
  const entry = payload.mandate_owners[0];
@@ -878,7 +1014,7 @@ function checkOwnerSignatureRequirement(payload, floor) {
878
1014
  );
879
1015
  }
880
1016
  const minRank = floor.minBinding ? BINDING_RANK[floor.minBinding] : 0;
881
- const haveRank = BINDING_RANK[entry.binding] ?? 0;
1017
+ const haveRank = bindingRank(entry.binding);
882
1018
  if (haveRank < minRank) {
883
1019
  throw new OwnerSignatureError(
884
1020
  "OWNER_SIGNATURE_REQUIRED",
@@ -889,11 +1025,27 @@ function checkOwnerSignatureRequirement(payload, floor) {
889
1025
 
890
1026
  // src/ticket.ts
891
1027
  import * as ed3 from "@noble/ed25519";
1028
+ var TICKET_FRESHNESS_SECONDS = 300;
892
1029
  async function signTicket(payload, privateKey) {
893
1030
  const signature = toBase64Url(await ed3.signAsync(new TextEncoder().encode(canonicalize(payload)), privateKey));
894
1031
  return { ...payload, signature };
895
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
+ }
896
1046
  async function verifyTicketSignature(ticket, opts) {
1047
+ assertTicketShape(ticket);
1048
+ assertSupportedVersion(ticket.version, "ticket");
897
1049
  const { signature, ...unsigned } = ticket;
898
1050
  if (!signature) {
899
1051
  throw new HapError("INVALID_SIGNATURE", "ticket carries no signature");
@@ -919,6 +1071,120 @@ async function verifyTicketSignature(ticket, opts) {
919
1071
  throw new HapError("INVALID_SIGNATURE", "ticket signature verification failed");
920
1072
  }
921
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
+ }
922
1188
  function publicTicketView(ticket, governingDiscloseFields = []) {
923
1189
  const { userId: _userId, cumulativeState: _cumulativeState, limits: _limits, executionContext, ...rest } = ticket;
924
1190
  const allowed = new Set(governingDiscloseFields);
@@ -929,6 +1195,475 @@ function publicTicketView(ticket, governingDiscloseFields = []) {
929
1195
  return { ...rest, executionContext: disclosedExecutionContext };
930
1196
  }
931
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
+
932
1667
  // src/profiles/index.ts
933
1668
  var PROFILES = {};
934
1669
  function registerProfile(profileId, profile) {
@@ -987,8 +1722,9 @@ async function verify(request, opts = {}) {
987
1722
  }
988
1723
  const expectedProfileHash = computeProfileHash(profile);
989
1724
  const errors = [];
1725
+ const verifiedPayloads = [];
990
1726
  for (const blob of request.mandates) {
991
- const mandateErrors = await verifyOneMandate(blob, {
1727
+ const { errors: mandateErrors, payload } = await verifyOneMandate(blob, {
992
1728
  expectedBoundsHash,
993
1729
  expectedScopeHash,
994
1730
  expectedProfileHash,
@@ -996,6 +1732,7 @@ async function verify(request, opts = {}) {
996
1732
  now
997
1733
  });
998
1734
  errors.push(...mandateErrors);
1735
+ if (payload) verifiedPayloads.push(payload);
999
1736
  }
1000
1737
  if (errors.length > 0) {
1001
1738
  return { approved: false, errors };
@@ -1014,6 +1751,31 @@ async function verify(request, opts = {}) {
1014
1751
  };
1015
1752
  }
1016
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
+ }
1017
1779
  const boundsErrors = checkBounds(request, profile, actionType);
1018
1780
  if (boundsErrors.length > 0) {
1019
1781
  return { approved: false, errors: boundsErrors };
@@ -1047,16 +1809,21 @@ function codeFromThrown(err) {
1047
1809
  return "MALFORMED_MANDATE";
1048
1810
  }
1049
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) {
1050
1817
  let mandate;
1051
1818
  try {
1052
1819
  mandate = decodeMandateBlob(blob);
1053
1820
  } catch (err) {
1054
1821
  return [{ code: "MALFORMED_MANDATE", message: err.message }];
1055
1822
  }
1056
- if (mandate.payload.version !== PROTOCOL_VERSION) {
1823
+ if (!isSupportedVersion(mandate.payload.version)) {
1057
1824
  return [{
1058
1825
  code: "VERSION_UNSUPPORTED",
1059
- 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(", ")}])`
1060
1827
  }];
1061
1828
  }
1062
1829
  try {
@@ -1071,6 +1838,7 @@ async function verifyOneMandate(blob, opts) {
1071
1838
  }
1072
1839
  try {
1073
1840
  validateMandateOwners(mandate.payload);
1841
+ validateAboveCapFields(mandate.payload);
1074
1842
  } catch (err) {
1075
1843
  return [{ code: codeFromThrown(err), message: err.message }];
1076
1844
  }
@@ -1236,43 +2004,70 @@ function checkScopeConstraints(scope, execution, profile) {
1236
2004
  return errors;
1237
2005
  }
1238
2006
  export {
2007
+ BINDING_RANK,
2008
+ COMMITMENT_MODES,
1239
2009
  CanonicalValueError,
1240
2010
  ContentBindingError,
1241
2011
  HapError,
2012
+ ISSUABLE_VERSIONS,
1242
2013
  MANDATE_ERROR_CODES,
1243
2014
  OwnerSignatureError,
2015
+ PROJECTION_MAX_VALIDITY_SECONDS,
2016
+ PROPOSAL_ERROR_CODES,
1244
2017
  PROTOCOL_VERSION,
2018
+ PolicyError,
1245
2019
  SIGNABLE_COMMITMENT_MODES,
1246
2020
  SUPPORTED_VERSIONS,
1247
2021
  TICKET_ERROR_CODES,
2022
+ TICKET_FRESHNESS_SECONDS,
1248
2023
  allowedCommitmentModes,
1249
2024
  approvalSigningBytes,
2025
+ approvalsSufficient,
2026
+ assertSupportedVersion,
2027
+ assertTicketShape,
1250
2028
  bindingAppliesTo,
2029
+ bindingRank,
1251
2030
  boundActionTypes,
2031
+ boundGoverns,
2032
+ boundReadFields,
1252
2033
  buildMandateProjection,
2034
+ buildTicketProjection,
1253
2035
  canonicalBounds,
1254
2036
  canonicalScope,
1255
2037
  canonicalize,
1256
2038
  canonicalizeText,
2039
+ capsExceeded,
2040
+ checkBoundsValues,
1257
2041
  checkDiscloseSubset,
2042
+ checkExecutionValues,
2043
+ checkGroupCeilings,
1258
2044
  checkMandateExpiry,
1259
2045
  checkOwnerSignatureRequirement,
2046
+ checkProposalReaders,
1260
2047
  clearProfiles,
1261
2048
  computeBoundsHash,
1262
2049
  computeContentHash,
2050
+ computeExecutionContextHash,
1263
2051
  computeFieldsContentHash,
1264
2052
  computeIntentDisclosureHash,
1265
2053
  computeIntentHash,
1266
2054
  computeProfileHash,
2055
+ computeProposalContentHash,
1267
2056
  computeScopeHash,
1268
2057
  contentCanonicalBytes,
1269
2058
  decodeDidKey,
1270
2059
  decodeMandateBlob,
2060
+ decryptProposalContent,
1271
2061
  deriveIdentityLine,
2062
+ deriveReaderKeyPair,
1272
2063
  encodeDidKey,
1273
2064
  encodeMandateBlob,
2065
+ encryptProposalContent,
2066
+ evaluateVerifierPolicy,
2067
+ exceedsCap,
1274
2068
  filesFields,
1275
2069
  fromBase64Url,
2070
+ generateReaderKeyPair,
1276
2071
  getAllProfiles,
1277
2072
  getProfile,
1278
2073
  intentDisclosureCanonicalBytes,
@@ -1280,18 +2075,25 @@ export {
1280
2075
  isCommitmentModeAllowed,
1281
2076
  isFieldBinding,
1282
2077
  isKeyBearingDid,
2078
+ isSupportedVersion,
1283
2079
  listProfiles,
1284
2080
  mandateBlobId,
2081
+ missingSumFields,
1285
2082
  negotiateVersion,
1286
2083
  projectionSigningBytes,
2084
+ proposalContentJcs,
1287
2085
  publicTicketView,
2086
+ reconstructApproval,
1288
2087
  registerProfile,
1289
2088
  selectBoundFields,
1290
2089
  signApproval,
1291
2090
  signMandate,
1292
2091
  signMandateProjection,
1293
2092
  signTicket,
2093
+ signTicketProjection,
1294
2094
  toBase64Url,
2095
+ validateAboveCapConfig,
2096
+ validateAboveCapFields,
1295
2097
  validateBoundsParams,
1296
2098
  validateMandateOwners,
1297
2099
  validateProfile,
@@ -1303,5 +2105,10 @@ export {
1303
2105
  verifyMandateSignature,
1304
2106
  verifyOwnerSignature,
1305
2107
  verifyOwnerSignatures,
1306
- verifyTicketSignature
2108
+ verifyProjectionOwner,
2109
+ verifyTicketAgainstRequest,
2110
+ verifyTicketApprovals,
2111
+ verifyTicketProjection,
2112
+ verifyTicketSignature,
2113
+ wireExecutionContext
1307
2114
  };