@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.js CHANGED
@@ -30,43 +30,70 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ BINDING_RANK: () => BINDING_RANK,
34
+ COMMITMENT_MODES: () => COMMITMENT_MODES,
33
35
  CanonicalValueError: () => CanonicalValueError,
34
36
  ContentBindingError: () => ContentBindingError,
35
37
  HapError: () => HapError,
38
+ ISSUABLE_VERSIONS: () => ISSUABLE_VERSIONS,
36
39
  MANDATE_ERROR_CODES: () => MANDATE_ERROR_CODES,
37
40
  OwnerSignatureError: () => OwnerSignatureError,
41
+ PROJECTION_MAX_VALIDITY_SECONDS: () => PROJECTION_MAX_VALIDITY_SECONDS,
42
+ PROPOSAL_ERROR_CODES: () => PROPOSAL_ERROR_CODES,
38
43
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
44
+ PolicyError: () => PolicyError,
39
45
  SIGNABLE_COMMITMENT_MODES: () => SIGNABLE_COMMITMENT_MODES,
40
46
  SUPPORTED_VERSIONS: () => SUPPORTED_VERSIONS,
41
47
  TICKET_ERROR_CODES: () => TICKET_ERROR_CODES,
48
+ TICKET_FRESHNESS_SECONDS: () => TICKET_FRESHNESS_SECONDS,
42
49
  allowedCommitmentModes: () => allowedCommitmentModes,
43
50
  approvalSigningBytes: () => approvalSigningBytes,
51
+ approvalsSufficient: () => approvalsSufficient,
52
+ assertSupportedVersion: () => assertSupportedVersion,
53
+ assertTicketShape: () => assertTicketShape,
44
54
  bindingAppliesTo: () => bindingAppliesTo,
55
+ bindingRank: () => bindingRank,
45
56
  boundActionTypes: () => boundActionTypes,
57
+ boundGoverns: () => boundGoverns,
58
+ boundReadFields: () => boundReadFields,
46
59
  buildMandateProjection: () => buildMandateProjection,
60
+ buildTicketProjection: () => buildTicketProjection,
47
61
  canonicalBounds: () => canonicalBounds,
48
62
  canonicalScope: () => canonicalScope,
49
63
  canonicalize: () => canonicalize,
50
64
  canonicalizeText: () => canonicalizeText,
65
+ capsExceeded: () => capsExceeded,
66
+ checkBoundsValues: () => checkBoundsValues,
51
67
  checkDiscloseSubset: () => checkDiscloseSubset,
68
+ checkExecutionValues: () => checkExecutionValues,
69
+ checkGroupCeilings: () => checkGroupCeilings,
52
70
  checkMandateExpiry: () => checkMandateExpiry,
53
71
  checkOwnerSignatureRequirement: () => checkOwnerSignatureRequirement,
72
+ checkProposalReaders: () => checkProposalReaders,
54
73
  clearProfiles: () => clearProfiles,
55
74
  computeBoundsHash: () => computeBoundsHash,
56
75
  computeContentHash: () => computeContentHash,
76
+ computeExecutionContextHash: () => computeExecutionContextHash,
57
77
  computeFieldsContentHash: () => computeFieldsContentHash,
58
78
  computeIntentDisclosureHash: () => computeIntentDisclosureHash,
59
79
  computeIntentHash: () => computeIntentHash,
60
80
  computeProfileHash: () => computeProfileHash,
81
+ computeProposalContentHash: () => computeProposalContentHash,
61
82
  computeScopeHash: () => computeScopeHash,
62
83
  contentCanonicalBytes: () => contentCanonicalBytes,
63
84
  decodeDidKey: () => decodeDidKey,
64
85
  decodeMandateBlob: () => decodeMandateBlob,
86
+ decryptProposalContent: () => decryptProposalContent,
65
87
  deriveIdentityLine: () => deriveIdentityLine,
88
+ deriveReaderKeyPair: () => deriveReaderKeyPair,
66
89
  encodeDidKey: () => encodeDidKey,
67
90
  encodeMandateBlob: () => encodeMandateBlob,
91
+ encryptProposalContent: () => encryptProposalContent,
92
+ evaluateVerifierPolicy: () => evaluateVerifierPolicy,
93
+ exceedsCap: () => exceedsCap,
68
94
  filesFields: () => filesFields,
69
95
  fromBase64Url: () => fromBase64Url,
96
+ generateReaderKeyPair: () => generateReaderKeyPair,
70
97
  getAllProfiles: () => getAllProfiles,
71
98
  getProfile: () => getProfile,
72
99
  intentDisclosureCanonicalBytes: () => intentDisclosureCanonicalBytes,
@@ -74,18 +101,25 @@ __export(index_exports, {
74
101
  isCommitmentModeAllowed: () => isCommitmentModeAllowed,
75
102
  isFieldBinding: () => isFieldBinding,
76
103
  isKeyBearingDid: () => isKeyBearingDid,
104
+ isSupportedVersion: () => isSupportedVersion,
77
105
  listProfiles: () => listProfiles,
78
106
  mandateBlobId: () => mandateBlobId,
107
+ missingSumFields: () => missingSumFields,
79
108
  negotiateVersion: () => negotiateVersion,
80
109
  projectionSigningBytes: () => projectionSigningBytes,
110
+ proposalContentJcs: () => proposalContentJcs,
81
111
  publicTicketView: () => publicTicketView,
112
+ reconstructApproval: () => reconstructApproval,
82
113
  registerProfile: () => registerProfile,
83
114
  selectBoundFields: () => selectBoundFields,
84
115
  signApproval: () => signApproval,
85
116
  signMandate: () => signMandate,
86
117
  signMandateProjection: () => signMandateProjection,
87
118
  signTicket: () => signTicket,
119
+ signTicketProjection: () => signTicketProjection,
88
120
  toBase64Url: () => toBase64Url,
121
+ validateAboveCapConfig: () => validateAboveCapConfig,
122
+ validateAboveCapFields: () => validateAboveCapFields,
89
123
  validateBoundsParams: () => validateBoundsParams,
90
124
  validateMandateOwners: () => validateMandateOwners,
91
125
  validateProfile: () => validateProfile,
@@ -97,11 +131,17 @@ __export(index_exports, {
97
131
  verifyMandateSignature: () => verifyMandateSignature,
98
132
  verifyOwnerSignature: () => verifyOwnerSignature,
99
133
  verifyOwnerSignatures: () => verifyOwnerSignatures,
100
- verifyTicketSignature: () => verifyTicketSignature
134
+ verifyProjectionOwner: () => verifyProjectionOwner,
135
+ verifyTicketAgainstRequest: () => verifyTicketAgainstRequest,
136
+ verifyTicketApprovals: () => verifyTicketApprovals,
137
+ verifyTicketProjection: () => verifyTicketProjection,
138
+ verifyTicketSignature: () => verifyTicketSignature,
139
+ wireExecutionContext: () => wireExecutionContext
101
140
  });
102
141
  module.exports = __toCommonJS(index_exports);
103
142
 
104
143
  // src/canonicalize.ts
144
+ var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
105
145
  function canonicalize(value) {
106
146
  if (value === void 0) {
107
147
  throw new Error("canonicalize: undefined is not serializable");
@@ -109,6 +149,9 @@ function canonicalize(value) {
109
149
  if (typeof value === "number" && !Number.isFinite(value)) {
110
150
  throw new Error(`canonicalize: ${value} is not a valid JSON number`);
111
151
  }
152
+ if (typeof value === "string" && LONE_SURROGATE.test(value)) {
153
+ throw new Error("canonicalize: string contains a lone surrogate and cannot be signed");
154
+ }
112
155
  if (value === null || typeof value !== "object") {
113
156
  return JSON.stringify(value);
114
157
  }
@@ -158,6 +201,10 @@ var MANDATE_ERROR_CODES = [
158
201
  "BOUNDS_INVALID_VALUE",
159
202
  "SCOPE_INVALID_VALUE",
160
203
  "ABOVE_CAP_CONFIG_INVALID",
204
+ "COMMITMENT_MODE_NOT_ALLOWED",
205
+ // v0.8
206
+ "GROUP_CAP_EXCEEDED",
207
+ // v0.8
161
208
  "OWNER_SIGNATURE_REQUIRED",
162
209
  "OWNER_SIGNATURE_INVALID"
163
210
  ];
@@ -171,6 +218,10 @@ var TICKET_ERROR_CODES = [
171
218
  "PROFILE_NOT_FOUND",
172
219
  "INVALID_ACTION_TYPE",
173
220
  "APPROVAL_REQUIRED",
221
+ "APPROVAL_IMPOSSIBLE",
222
+ // v0.8
223
+ "PROPOSAL_CONTENT_MISMATCH",
224
+ // v0.8
174
225
  "PROPOSAL_REQUIRED",
175
226
  "PROPOSAL_NOT_FOUND",
176
227
  "PROPOSAL_NOT_APPROVED",
@@ -179,11 +230,25 @@ var TICKET_ERROR_CODES = [
179
230
  "PROPOSAL_MISMATCH",
180
231
  "PROPOSAL_MANDATE_MISMATCH",
181
232
  "PROPOSAL_ALREADY_EXECUTED",
233
+ "PROPOSAL_WITHDRAWN",
234
+ // v0.8
182
235
  "MALFORMED_TICKET_REQUEST",
236
+ "CONTENT_BINDING_INCOMPLETE",
237
+ // v0.8, Gatekeeper · local
183
238
  "IDEMPOTENCY_KEY_REQUIRED",
184
239
  "IDEMPOTENCY_MISMATCH",
185
240
  "APPROVAL_SIGNATURE_REQUIRED",
186
- "APPROVAL_SIGNATURE_INVALID"
241
+ "APPROVAL_SIGNATURE_INVALID",
242
+ "TICKET_REQUEST_MISMATCH",
243
+ // v0.8, Gatekeeper · local
244
+ "POLICY_UNMET"
245
+ // v0.8, Verifier
246
+ ];
247
+ var PROPOSAL_ERROR_CODES = [
248
+ "PROPOSAL_NOT_ENCRYPTED",
249
+ "DECIDER_INVALID",
250
+ "PROPOSAL_ALREADY_DECIDED",
251
+ "PROPOSAL_APPROVERS_INVALID"
187
252
  ];
188
253
  var HapError = class extends Error {
189
254
  constructor(code, message) {
@@ -194,9 +259,13 @@ var HapError = class extends Error {
194
259
  };
195
260
 
196
261
  // src/versions.ts
197
- var PROTOCOL_VERSION = "0.7";
198
- var SUPPORTED_VERSIONS = ["0.7"];
199
- function negotiateVersion(requested, supported = SUPPORTED_VERSIONS) {
262
+ var PROTOCOL_VERSION = "0.8";
263
+ var SUPPORTED_VERSIONS = ["0.8", "0.7"];
264
+ var ISSUABLE_VERSIONS = [PROTOCOL_VERSION];
265
+ function isSupportedVersion(v) {
266
+ return typeof v === "string" && SUPPORTED_VERSIONS.includes(v);
267
+ }
268
+ function negotiateVersion(requested, supported = ISSUABLE_VERSIONS) {
200
269
  const requestedVersions = requested ?? ["0.6"];
201
270
  const common = supported.filter((v) => requestedVersions.includes(v)).sort().reverse();
202
271
  if (common.length === 0) {
@@ -207,6 +276,14 @@ function negotiateVersion(requested, supported = SUPPORTED_VERSIONS) {
207
276
  }
208
277
  return common[0];
209
278
  }
279
+ function assertSupportedVersion(version, what) {
280
+ if (!isSupportedVersion(version)) {
281
+ throw new HapError(
282
+ "VERSION_UNSUPPORTED",
283
+ `${what} version ${JSON.stringify(version)} is not verifiable by this implementation (verifies [${SUPPORTED_VERSIONS.join(", ")}])`
284
+ );
285
+ }
286
+ }
210
287
 
211
288
  // src/content-binding.ts
212
289
  var import_crypto = require("crypto");
@@ -513,6 +590,7 @@ function computeScopeHash(params, profile) {
513
590
 
514
591
  // src/commitment-modes.ts
515
592
  var SIGNABLE_COMMITMENT_MODES = ["review", "automatic"];
593
+ var COMMITMENT_MODES = ["automatic", "review", "review_above_cap"];
516
594
  var isSignable = (v) => typeof v === "string" && SIGNABLE_COMMITMENT_MODES.includes(v);
517
595
  function allowedCommitmentModes(profile) {
518
596
  const declared = profile.commitment_modes;
@@ -521,7 +599,9 @@ function allowedCommitmentModes(profile) {
521
599
  return SIGNABLE_COMMITMENT_MODES.filter((m) => declared.includes(m));
522
600
  }
523
601
  function isCommitmentModeAllowed(profile, mode) {
524
- return isSignable(mode) && allowedCommitmentModes(profile).includes(mode);
602
+ const allowed = allowedCommitmentModes(profile);
603
+ if (mode === "review_above_cap") return allowed.includes("automatic");
604
+ return isSignable(mode) && allowed.includes(mode);
525
605
  }
526
606
 
527
607
  // src/profile.ts
@@ -636,6 +716,7 @@ function validateProfile(profile) {
636
716
  }
637
717
  const seen = /* @__PURE__ */ new Set();
638
718
  for (const m of declaredModes) {
719
+ if (m === "review_above_cap") continue;
639
720
  if (!SIGNABLE_COMMITMENT_MODES.includes(m)) {
640
721
  errors.push({
641
722
  code: "PROFILE_INVALID",
@@ -657,6 +738,46 @@ function validateProfile(profile) {
657
738
  message: "commitment_modes declares no mode this profile can actually use."
658
739
  });
659
740
  }
741
+ if (boundsSchema) {
742
+ const readFields = /* @__PURE__ */ new Set();
743
+ for (const [fieldName, fieldDef] of Object.entries(boundsSchema.fields)) {
744
+ const bt = fieldDef.boundType;
745
+ if (bt && (bt.kind === "per_transaction" || bt.kind === "cumulative_sum")) readFields.add(bt.of);
746
+ const max = fieldDef.maximum;
747
+ if (max !== void 0 && (typeof max !== "number" || !Number.isFinite(max))) {
748
+ errors.push({
749
+ code: "PROFILE_INVALID",
750
+ field: fieldName,
751
+ message: `Bounds field "${fieldName}": maximum MUST be a number (Bounds Schema rule 9).`
752
+ });
753
+ }
754
+ const enforcedBy = fieldDef.enforcedBy;
755
+ if (enforcedBy !== void 0 && enforcedBy !== "gatekeeper") {
756
+ errors.push({
757
+ code: "PROFILE_INVALID",
758
+ field: fieldName,
759
+ message: `Bounds field "${fieldName}": enforcedBy MUST be "gatekeeper" when present (Bounds Schema rule 11).`
760
+ });
761
+ }
762
+ }
763
+ for (const f of profile.disclose_fields ?? []) {
764
+ if (!readFields.has(f)) {
765
+ errors.push({
766
+ code: "PROFILE_INVALID",
767
+ field: "disclose_fields",
768
+ message: `disclose_fields names "${f}", which no bound reads \u2014 only what a ticket carries can be disclosed.`
769
+ });
770
+ }
771
+ }
772
+ }
773
+ const binding = profile.content_binding;
774
+ if (binding && String(binding.version) === "2" && binding.kind === "text") {
775
+ errors.push({
776
+ code: "PROFILE_INVALID",
777
+ field: "content_binding",
778
+ message: 'A version-2 content_binding is defined for kind "jcs" only; kind "text" is invalid.'
779
+ });
780
+ }
660
781
  return errors;
661
782
  }
662
783
  function checkDiscloseSubset(mandateDiscloseFields, profileDiscloseFields) {
@@ -823,7 +944,7 @@ function validateMandateOwners(payload) {
823
944
  if (!Array.isArray(owners) || owners.length !== 1) {
824
945
  throw new HapError(
825
946
  "MALFORMED_MANDATE",
826
- `mandate_owners MUST contain exactly one entry in v0.7, got ${Array.isArray(owners) ? owners.length : typeof owners}`
947
+ `mandate_owners MUST contain exactly one entry (Mandate rule 7), got ${Array.isArray(owners) ? owners.length : typeof owners}`
827
948
  );
828
949
  }
829
950
  const entry = owners[0];
@@ -842,17 +963,45 @@ function validateMandateOwners(payload) {
842
963
  throw new HapError("OWNER_SIGNATURE_INVALID", `co-signing DID ${entry.did} is not key-bearing`);
843
964
  }
844
965
  }
966
+ function validateAboveCapFields(payload) {
967
+ const hasCaps = payload.above_cap_caps !== void 0;
968
+ const hasApprovers = payload.above_cap_approvers !== void 0;
969
+ const hasCount = payload.above_cap_approvals !== void 0;
970
+ if (hasCaps !== hasApprovers) {
971
+ throw new HapError("MALFORMED_MANDATE", "above_cap_caps and above_cap_approvers are present together or absent together");
972
+ }
973
+ if (payload.commitment_mode === "review_above_cap" && !hasApprovers) {
974
+ throw new HapError("MALFORMED_MANDATE", "a review_above_cap mandate MUST carry above_cap_caps and above_cap_approvers");
975
+ }
976
+ if (payload.version === "0.7") {
977
+ if (hasCount) {
978
+ throw new HapError("MALFORMED_MANDATE", "a v0.7 mandate carries no above_cap_approvals");
979
+ }
980
+ if (hasApprovers && payload.commitment_mode !== "review_above_cap") {
981
+ throw new HapError("MALFORMED_MANDATE", "a v0.7 mandate carries above-cap fields only under review_above_cap");
982
+ }
983
+ return;
984
+ }
985
+ if (hasApprovers !== hasCount) {
986
+ throw new HapError("MALFORMED_MANDATE", "above_cap_approvals is present exactly when above_cap_approvers is (Mandate rule 10)");
987
+ }
988
+ if (hasCount) {
989
+ const n = payload.above_cap_approvals;
990
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1) {
991
+ throw new HapError("MALFORMED_MANDATE", `above_cap_approvals MUST be an integer >= 1, got ${JSON.stringify(n)}`);
992
+ }
993
+ }
994
+ if (hasApprovers && payload.commitment_mode === "automatic") {
995
+ throw new HapError("MALFORMED_MANDATE", "an automatic mandate carries no above-cap fields");
996
+ }
997
+ }
845
998
  async function verifyMandate(blob, opts) {
846
999
  const mandate = decodeMandateBlob(blob);
847
- if (mandate.payload.version !== PROTOCOL_VERSION) {
848
- throw new HapError(
849
- "VERSION_UNSUPPORTED",
850
- `mandate version ${JSON.stringify(mandate.payload.version)} is not verifiable by this implementation (verifies ${PROTOCOL_VERSION} only \u2014 re-issue the mandate)`
851
- );
852
- }
1000
+ assertSupportedVersion(mandate.payload.version, "mandate");
853
1001
  await verifyMandateSignature(mandate, opts);
854
1002
  checkMandateExpiry(mandate.payload, opts?.now);
855
1003
  validateMandateOwners(mandate.payload);
1004
+ validateAboveCapFields(mandate.payload);
856
1005
  return mandate.payload;
857
1006
  }
858
1007
 
@@ -889,9 +1038,10 @@ function buildMandateProjection(payload, entry) {
889
1038
  "mandate projection requires profile_hash, bounds_hash, scope_hash and commitment_mode"
890
1039
  );
891
1040
  }
1041
+ const version = payload.version === "0.7" ? "0.7" : "0.8";
892
1042
  const projection = {
893
1043
  typ: "HAP-mandate-projection",
894
- version: "0.7",
1044
+ version,
895
1045
  profile_id,
896
1046
  profile_hash,
897
1047
  owner_did: entry.did,
@@ -906,13 +1056,32 @@ function buildMandateProjection(payload, entry) {
906
1056
  if (payload.intent_disclosure_hash !== void 0) {
907
1057
  projection.intent_disclosure_hash = payload.intent_disclosure_hash;
908
1058
  }
909
- if (payload.commitment_mode === "review_above_cap") {
910
- projection.above_cap_caps = payload.above_cap_caps;
911
- projection.above_cap_approvers = payload.above_cap_approvers;
1059
+ if (version === "0.7") {
1060
+ if (payload.commitment_mode === "review_above_cap") {
1061
+ projection.above_cap_caps = payload.above_cap_caps;
1062
+ projection.above_cap_approvers = payload.above_cap_approvers;
1063
+ }
1064
+ } else {
1065
+ if (payload.above_cap_approvers !== void 0) {
1066
+ projection.above_cap_caps = payload.above_cap_caps;
1067
+ projection.above_cap_approvers = payload.above_cap_approvers;
1068
+ projection.above_cap_approvals = payload.above_cap_approvals;
1069
+ }
912
1070
  }
913
1071
  if (payload.disclose_fields !== void 0) {
914
1072
  projection.disclose_fields = payload.disclose_fields;
915
1073
  }
1074
+ if (version === "0.8") {
1075
+ if (entry.binding === void 0 || entry.signing_surface === void 0) {
1076
+ throw new OwnerSignatureError(
1077
+ "OWNER_SIGNATURE_INVALID",
1078
+ "a v0.8 projection requires the entry's binding and signing_surface"
1079
+ );
1080
+ }
1081
+ projection.binding = entry.binding;
1082
+ projection.signing_surface = entry.signing_surface;
1083
+ projection.subjects_disclosed = (payload.subjects ?? []).some((s) => s.did === entry.did);
1084
+ }
916
1085
  return projection;
917
1086
  }
918
1087
  function projectionSigningBytes(projection) {
@@ -958,11 +1127,14 @@ async function verifyOwnerSignatures(mandate) {
958
1127
  return entries;
959
1128
  }
960
1129
  async function verifyApproval(approval, signature, signerDid) {
1130
+ if (approval.version === "0.8" && approval.approver_did !== signerDid) {
1131
+ throw new OwnerSignatureError("APPROVAL_SIGNATURE_INVALID", `approval names approver ${approval.approver_did}, verified under ${signerDid}`);
1132
+ }
961
1133
  let publicKey;
962
1134
  try {
963
1135
  publicKey = decodeDidKey(signerDid);
964
1136
  } catch (err) {
965
- throw new OwnerSignatureError("OWNER_SIGNATURE_INVALID", err.message);
1137
+ throw new OwnerSignatureError("APPROVAL_SIGNATURE_INVALID", err.message);
966
1138
  }
967
1139
  const ok = await ed2.verifyAsync(decodeOrThrow(signature, "APPROVAL_SIGNATURE_INVALID"), approvalSigningBytes(approval), publicKey).catch(() => false);
968
1140
  if (!ok) {
@@ -970,6 +1142,9 @@ async function verifyApproval(approval, signature, signerDid) {
970
1142
  }
971
1143
  }
972
1144
  var BINDING_RANK = { raw: 0, webauthn: 1, eudi: 2 };
1145
+ function bindingRank(binding) {
1146
+ return BINDING_RANK[binding] ?? 0;
1147
+ }
973
1148
  function checkOwnerSignatureRequirement(payload, floor) {
974
1149
  if (!floor?.required) return;
975
1150
  const entry = payload.mandate_owners[0];
@@ -981,7 +1156,7 @@ function checkOwnerSignatureRequirement(payload, floor) {
981
1156
  );
982
1157
  }
983
1158
  const minRank = floor.minBinding ? BINDING_RANK[floor.minBinding] : 0;
984
- const haveRank = BINDING_RANK[entry.binding] ?? 0;
1159
+ const haveRank = bindingRank(entry.binding);
985
1160
  if (haveRank < minRank) {
986
1161
  throw new OwnerSignatureError(
987
1162
  "OWNER_SIGNATURE_REQUIRED",
@@ -992,11 +1167,27 @@ function checkOwnerSignatureRequirement(payload, floor) {
992
1167
 
993
1168
  // src/ticket.ts
994
1169
  var ed3 = __toESM(require("@noble/ed25519"));
1170
+ var TICKET_FRESHNESS_SECONDS = 300;
995
1171
  async function signTicket(payload, privateKey) {
996
1172
  const signature = toBase64Url(await ed3.signAsync(new TextEncoder().encode(canonicalize(payload)), privateKey));
997
1173
  return { ...payload, signature };
998
1174
  }
1175
+ function assertTicketShape(ticket) {
1176
+ if ("typ" in ticket) {
1177
+ throw new HapError("INVALID_SIGNATURE", "a ticket carries no typ \u2014 an object with one is a projection, not a ticket");
1178
+ }
1179
+ if ("mandate_id" in ticket || "issued_at" in ticket) {
1180
+ throw new HapError("INVALID_SIGNATURE", "a ticket carries no mandate_id or issued_at \u2014 this object is a mandate payload");
1181
+ }
1182
+ for (const required of ["id", "mandateId", "actionType", "timestamp", "issuer"]) {
1183
+ if (ticket[required] === void 0) {
1184
+ throw new HapError("INVALID_SIGNATURE", `ticket is missing required field ${required}`);
1185
+ }
1186
+ }
1187
+ }
999
1188
  async function verifyTicketSignature(ticket, opts) {
1189
+ assertTicketShape(ticket);
1190
+ assertSupportedVersion(ticket.version, "ticket");
1000
1191
  const { signature, ...unsigned } = ticket;
1001
1192
  if (!signature) {
1002
1193
  throw new HapError("INVALID_SIGNATURE", "ticket carries no signature");
@@ -1022,6 +1213,120 @@ async function verifyTicketSignature(ticket, opts) {
1022
1213
  throw new HapError("INVALID_SIGNATURE", "ticket signature verification failed");
1023
1214
  }
1024
1215
  }
1216
+ var ECHOED_FIELDS = [
1217
+ "profileId",
1218
+ "actionType",
1219
+ "boundsHash",
1220
+ "executionContext",
1221
+ "contentHash",
1222
+ "contentBinding",
1223
+ "proposalId",
1224
+ "proposalContentHash"
1225
+ ];
1226
+ function verifyTicketAgainstRequest(ticket, request, opts = {}) {
1227
+ for (const field of ECHOED_FIELDS) {
1228
+ const sent = request[field];
1229
+ const got = ticket[field];
1230
+ if (sent === void 0 && got === void 0) continue;
1231
+ if (sent === void 0 || got === void 0 || canonicalize(sent) !== canonicalize(got)) {
1232
+ throw new HapError("TICKET_REQUEST_MISMATCH", `ticket ${field} differs from the request's`);
1233
+ }
1234
+ }
1235
+ if (request.idempotencyKey !== void 0 && ticket.idempotencyKey !== request.idempotencyKey) {
1236
+ throw new HapError(
1237
+ "TICKET_REQUEST_MISMATCH",
1238
+ ticket.idempotencyKey === void 0 ? "ticket carries no idempotencyKey although the request sent one" : "ticket idempotencyKey differs from the one the request sent"
1239
+ );
1240
+ }
1241
+ const expectedMandateId = opts.mandate?.payload.mandate_id ?? request.mandateId;
1242
+ if (expectedMandateId !== void 0 && ticket.mandateId !== expectedMandateId) {
1243
+ throw new HapError("TICKET_REQUEST_MISMATCH", `ticket mandateId ${ticket.mandateId} is not the mandate this request was made under`);
1244
+ }
1245
+ if (opts.mandate && ticket.version !== opts.mandate.payload.version) {
1246
+ throw new HapError("TICKET_REQUEST_MISMATCH", `ticket version ${ticket.version} differs from its mandate's ${opts.mandate.payload.version}`);
1247
+ }
1248
+ if (!opts.replay) {
1249
+ const now = opts.now ?? Math.floor(Date.now() / 1e3);
1250
+ if (Math.abs(now - ticket.timestamp) > TICKET_FRESHNESS_SECONDS) {
1251
+ throw new HapError("TICKET_REQUEST_MISMATCH", `ticket timestamp ${ticket.timestamp} is outside the freshness window at ${now}`);
1252
+ }
1253
+ }
1254
+ }
1255
+ function reconstructApproval(ticket, entry, approverDid) {
1256
+ if (!ticket.proposalId) {
1257
+ throw new HapError("APPROVAL_SIGNATURE_INVALID", "ticket carries an approval signature but no proposalId");
1258
+ }
1259
+ if (ticket.version === "0.7") {
1260
+ if (!ticket.contentHash) {
1261
+ throw new HapError("APPROVAL_SIGNATURE_INVALID", "a v0.7 approval without a content binding is not reconstructible from the ticket alone");
1262
+ }
1263
+ return {
1264
+ typ: "HAP-approval",
1265
+ version: "0.7",
1266
+ proposal_id: ticket.proposalId,
1267
+ mandate_id: ticket.mandateId,
1268
+ decision: "commit",
1269
+ content_hash: ticket.contentHash,
1270
+ decided_at: entry.decided_at,
1271
+ nonce: entry.nonce
1272
+ };
1273
+ }
1274
+ if (!ticket.proposalContentHash) {
1275
+ throw new HapError("APPROVAL_SIGNATURE_INVALID", "a v0.8 review-path ticket carries proposalContentHash");
1276
+ }
1277
+ return {
1278
+ typ: "HAP-approval",
1279
+ version: "0.8",
1280
+ approver_did: approverDid,
1281
+ proposal_id: ticket.proposalId,
1282
+ mandate_id: ticket.mandateId,
1283
+ action: ticket.action,
1284
+ action_type: ticket.actionType,
1285
+ decision: "commit",
1286
+ content_hash: ticket.contentHash ?? ticket.proposalContentHash,
1287
+ proposal_content_hash: ticket.proposalContentHash,
1288
+ decided_at: entry.decided_at,
1289
+ nonce: entry.nonce
1290
+ };
1291
+ }
1292
+ async function verifyTicketApprovals(ticket, mandate) {
1293
+ if (ticket.mandateId !== mandate.payload.mandate_id) {
1294
+ throw new HapError("APPROVAL_SIGNATURE_INVALID", "the mandate supplied is not the one the ticket names");
1295
+ }
1296
+ const ownerDid = mandate.payload.mandate_owners[0]?.did;
1297
+ const result = { approvers: [] };
1298
+ try {
1299
+ if (ticket.approvalSignature) {
1300
+ const approval = reconstructApproval(ticket, ticket.approvalSignature, ownerDid);
1301
+ await verifyApproval(approval, ticket.approvalSignature.signature, ownerDid);
1302
+ result.owner = ownerDid;
1303
+ }
1304
+ const signed = new Set(mandate.payload.above_cap_approvers ?? []);
1305
+ for (const entry of ticket.approvalSignatures ?? []) {
1306
+ if (!signed.has(entry.approver_did)) {
1307
+ throw new HapError("APPROVAL_SIGNATURE_INVALID", `approver ${entry.approver_did} is not in the mandate's signed above_cap_approvers`);
1308
+ }
1309
+ if (entry.approver_did === ownerDid) {
1310
+ throw new HapError("APPROVAL_SIGNATURE_INVALID", "the proposer never counts as an above-cap approver");
1311
+ }
1312
+ const approval = reconstructApproval(ticket, entry, entry.approver_did);
1313
+ await verifyApproval(approval, entry.signature, entry.approver_did);
1314
+ if (!result.approvers.includes(entry.approver_did)) result.approvers.push(entry.approver_did);
1315
+ }
1316
+ } catch (err) {
1317
+ if (err instanceof OwnerSignatureError) throw new HapError(err.code, err.message);
1318
+ throw err;
1319
+ }
1320
+ return result;
1321
+ }
1322
+ function approvalsSufficient(mandate, verified, aboveCap) {
1323
+ const p = mandate.payload;
1324
+ const ownerNeeded = p.commitment_mode === "review";
1325
+ if (ownerNeeded && !verified.owner) return false;
1326
+ if (!aboveCap) return true;
1327
+ const required = p.version === "0.7" ? (p.above_cap_approvers ?? []).length : p.above_cap_approvals ?? 1;
1328
+ return verified.approvers.length >= required;
1329
+ }
1025
1330
  function publicTicketView(ticket, governingDiscloseFields = []) {
1026
1331
  const { userId: _userId, cumulativeState: _cumulativeState, limits: _limits, executionContext, ...rest } = ticket;
1027
1332
  const allowed = new Set(governingDiscloseFields);
@@ -1032,6 +1337,475 @@ function publicTicketView(ticket, governingDiscloseFields = []) {
1032
1337
  return { ...rest, executionContext: disclosedExecutionContext };
1033
1338
  }
1034
1339
 
1340
+ // src/ticket-projection.ts
1341
+ var ed4 = __toESM(require("@noble/ed25519"));
1342
+ var PROJECTION_MAX_VALIDITY_SECONDS = 86400;
1343
+ function buildTicketProjection(ticket, mandate, opts) {
1344
+ if (ticket.mandateId !== mandate.payload.mandate_id) {
1345
+ throw new HapError("MALFORMED_TICKET_REQUEST", "the mandate supplied is not the one the ticket names");
1346
+ }
1347
+ if (opts.validUntil > ticket.timestamp + PROJECTION_MAX_VALIDITY_SECONDS) {
1348
+ throw new HapError("MALFORMED_TICKET_REQUEST", `valid_until MUST NOT end later than ${PROJECTION_MAX_VALIDITY_SECONDS}s after the ticket's timestamp`);
1349
+ }
1350
+ const projection = {
1351
+ typ: "HAP-ticket-projection",
1352
+ version: ticket.version,
1353
+ id: ticket.id,
1354
+ mandateId: ticket.mandateId,
1355
+ issuer: ticket.issuer,
1356
+ timestamp: ticket.timestamp,
1357
+ valid_until: opts.validUntil,
1358
+ profileId: ticket.profileId,
1359
+ actionType: ticket.actionType,
1360
+ boundsHash: ticket.boundsHash
1361
+ };
1362
+ if (ticket.contentHash !== void 0) projection.contentHash = ticket.contentHash;
1363
+ if (ticket.contentBinding !== void 0) projection.contentBinding = ticket.contentBinding;
1364
+ if (ticket.subjects !== void 0) projection.subjects = ticket.subjects;
1365
+ const disclosed = {};
1366
+ for (const field of opts.discloseFields ?? []) {
1367
+ if (ticket.executionContext[field] !== void 0) disclosed[field] = ticket.executionContext[field];
1368
+ }
1369
+ if (Object.keys(disclosed).length > 0) projection.executionContext = disclosed;
1370
+ const owner = mandate.payload.mandate_owners[0];
1371
+ if (owner?.signature !== void 0) projection.owner = owner;
1372
+ return projection;
1373
+ }
1374
+ async function signTicketProjection(projection, privateKey) {
1375
+ const signature = toBase64Url(await ed4.signAsync(new TextEncoder().encode(canonicalize(projection)), privateKey));
1376
+ return { ...projection, signature };
1377
+ }
1378
+ async function verifyTicketProjection(projection, opts = {}) {
1379
+ if (projection.typ !== "HAP-ticket-projection") {
1380
+ throw new HapError("INVALID_SIGNATURE", 'a projection verifier requires typ "HAP-ticket-projection"');
1381
+ }
1382
+ for (const k of ["userId", "cumulativeState", "limits", "action"]) {
1383
+ if (k in projection) throw new HapError("INVALID_SIGNATURE", `a projection MUST NOT carry ${k}`);
1384
+ }
1385
+ if (opts.trustedIssuers && !opts.trustedIssuers.includes(projection.issuer)) {
1386
+ throw new HapError("INVALID_SIGNATURE", `issuer ${JSON.stringify(projection.issuer)} is not a trusted Authority Server`);
1387
+ }
1388
+ const { signature, ...unsigned } = projection;
1389
+ if (!signature) throw new HapError("INVALID_SIGNATURE", "projection carries no signature");
1390
+ let publicKey;
1391
+ try {
1392
+ publicKey = decodeDidKey(projection.issuer);
1393
+ } catch (err) {
1394
+ throw new HapError("INVALID_SIGNATURE", `cannot resolve a key from issuer ${JSON.stringify(projection.issuer)}: ${err.message}`);
1395
+ }
1396
+ let sigBytes;
1397
+ try {
1398
+ sigBytes = fromBase64Url(signature);
1399
+ } catch (err) {
1400
+ throw new HapError("INVALID_SIGNATURE", err.message);
1401
+ }
1402
+ const ok = await ed4.verifyAsync(sigBytes, new TextEncoder().encode(canonicalize(unsigned)), publicKey).catch(() => false);
1403
+ if (!ok) throw new HapError("INVALID_SIGNATURE", "projection signature verification failed");
1404
+ if (opts.asAuthorization) {
1405
+ const now = opts.now ?? Math.floor(Date.now() / 1e3);
1406
+ if (now > projection.valid_until) {
1407
+ throw new HapError("POLICY_UNMET", `projection presented after valid_until ${projection.valid_until}`);
1408
+ }
1409
+ }
1410
+ }
1411
+ async function verifyProjectionOwner(projection, mandate) {
1412
+ if (!projection.owner) {
1413
+ throw new HapError("OWNER_SIGNATURE_INVALID", "projection carries no owner entry");
1414
+ }
1415
+ if (projection.mandateId !== mandate.payload.mandate_id) {
1416
+ throw new HapError("OWNER_SIGNATURE_INVALID", "the mandate supplied is not the one the projection names");
1417
+ }
1418
+ const own = mandate.payload.mandate_owners[0];
1419
+ if (!own || canonicalize(own) !== canonicalize(projection.owner)) {
1420
+ throw new HapError("OWNER_SIGNATURE_INVALID", "projection owner entry differs from the mandate's mandate_owners entry");
1421
+ }
1422
+ try {
1423
+ await verifyOwnerSignature(mandate, projection.owner);
1424
+ } catch (err) {
1425
+ if (err instanceof OwnerSignatureError) throw new HapError(err.code, err.message);
1426
+ throw err;
1427
+ }
1428
+ }
1429
+
1430
+ // src/verifier-policy.ts
1431
+ var PolicyError = class extends HapError {
1432
+ constructor(line, code, message) {
1433
+ super(code, `${line}: ${message}`);
1434
+ this.line = line;
1435
+ this.name = "PolicyError";
1436
+ }
1437
+ };
1438
+ async function evaluateVerifierPolicy(policy, input) {
1439
+ if (policy.typ !== "HAP-verifier-policy") {
1440
+ throw new HapError("POLICY_UNMET", 'policy document must carry typ "HAP-verifier-policy"');
1441
+ }
1442
+ const artifact = input.ticket ?? input.projection;
1443
+ if (!artifact) throw new HapError("POLICY_UNMET", "no ticket or projection to evaluate");
1444
+ const now = input.now ?? Math.floor(Date.now() / 1e3);
1445
+ try {
1446
+ if (input.ticket) {
1447
+ await verifyTicketSignature(input.ticket, { trustedIssuers: policy.trusted_issuers });
1448
+ } else if (input.projection) {
1449
+ await verifyTicketProjection(input.projection, { trustedIssuers: policy.trusted_issuers, now, asAuthorization: input.asAuthorization });
1450
+ }
1451
+ if (input.mandate) {
1452
+ await verifyMandateSignature(input.mandate, { trustedIssuers: policy.trusted_issuers });
1453
+ }
1454
+ } catch (err) {
1455
+ const code = err instanceof HapError ? err.code : "INVALID_SIGNATURE";
1456
+ throw new PolicyError(code === "POLICY_UNMET" ? "freshness_seconds" : "trusted_issuers", code, err.message);
1457
+ }
1458
+ let verifiedOwnerDids;
1459
+ const needOwners = policy.min_binding !== void 0 || policy.required_owners !== void 0;
1460
+ if (needOwners) {
1461
+ verifiedOwnerDids = /* @__PURE__ */ new Map();
1462
+ if (input.mandate) {
1463
+ try {
1464
+ const entries = await verifyOwnerSignatures(input.mandate);
1465
+ for (const e of entries) verifiedOwnerDids.set(e.did, e.binding ?? "raw");
1466
+ if (input.projection?.owner) await verifyProjectionOwner(input.projection, input.mandate);
1467
+ } catch (err) {
1468
+ throw new PolicyError("min_binding", "OWNER_SIGNATURE_REQUIRED", `no verifiable owner signature: ${err.message}`);
1469
+ }
1470
+ }
1471
+ }
1472
+ if (policy.min_binding !== void 0) {
1473
+ const minRank = BINDING_RANK[policy.min_binding];
1474
+ if (minRank === void 0) {
1475
+ throw new PolicyError("min_binding", "OWNER_SIGNATURE_REQUIRED", `unknown binding "${policy.min_binding}" \u2014 satisfies no minimum`);
1476
+ }
1477
+ const best = Math.max(-1, ...[...verifiedOwnerDids.values()].map(bindingRank));
1478
+ if (best < minRank) {
1479
+ throw new PolicyError("min_binding", "OWNER_SIGNATURE_REQUIRED", `no verified owner signature at binding >= "${policy.min_binding}"`);
1480
+ }
1481
+ }
1482
+ if (policy.required_owners !== void 0) {
1483
+ for (const did of policy.required_owners) {
1484
+ if (!verifiedOwnerDids.has(did)) {
1485
+ throw new PolicyError("required_owners", "OWNER_SIGNATURE_REQUIRED", `no verified owner signature by ${did}`);
1486
+ }
1487
+ }
1488
+ }
1489
+ if (policy.profile_hashes !== void 0) {
1490
+ const mandate = input.mandate;
1491
+ if (!mandate) throw new PolicyError("profile_hashes", "PROFILE_HASH_MISMATCH", "the mandate is needed to check profile_hashes");
1492
+ const expected = policy.profile_hashes[mandate.payload.profile_id];
1493
+ if (expected === void 0) {
1494
+ throw new PolicyError("profile_hashes", "PROFILE_HASH_MISMATCH", `profile ${mandate.payload.profile_id} is not listed`);
1495
+ }
1496
+ if (expected !== mandate.payload.profile_hash) {
1497
+ throw new PolicyError("profile_hashes", "PROFILE_HASH_MISMATCH", `profile_hash differs from the listed one for ${mandate.payload.profile_id}`);
1498
+ }
1499
+ if (mandate.payload.mandate_id !== artifact.mandateId) {
1500
+ throw new PolicyError("profile_hashes", "PROFILE_HASH_MISMATCH", "the mandate supplied is not the one the artifact names");
1501
+ }
1502
+ }
1503
+ if (policy.action_types !== void 0 && !policy.action_types.includes(artifact.actionType)) {
1504
+ throw new PolicyError("action_types", "POLICY_UNMET", `actionType ${artifact.actionType} is not in the listed set`);
1505
+ }
1506
+ if (policy.freshness_seconds !== void 0) {
1507
+ if (now - artifact.timestamp > policy.freshness_seconds) {
1508
+ throw new PolicyError("freshness_seconds", "POLICY_UNMET", `ticket is ${now - artifact.timestamp}s old, policy allows ${policy.freshness_seconds}s`);
1509
+ }
1510
+ if (input.projection && input.asAuthorization && now > input.projection.valid_until) {
1511
+ throw new PolicyError("freshness_seconds", "POLICY_UNMET", "projection presented after valid_until");
1512
+ }
1513
+ }
1514
+ if (policy.required_fields !== void 0) {
1515
+ const ctx = input.ticket?.executionContext ?? input.projection?.executionContext ?? {};
1516
+ for (const f of policy.required_fields) {
1517
+ if (ctx[f] === void 0) {
1518
+ throw new PolicyError("required_fields", "POLICY_UNMET", `execution-context field ${f} is not ${input.ticket ? "present" : "disclosed"}`);
1519
+ }
1520
+ }
1521
+ }
1522
+ if (policy.require_approval_signature) {
1523
+ const ticket = input.ticket;
1524
+ const mandate = input.mandate;
1525
+ if (!ticket || !mandate) {
1526
+ throw new PolicyError("require_approval_signature", "APPROVAL_SIGNATURE_REQUIRED", "the complete ticket and its mandate are needed to verify approvals");
1527
+ }
1528
+ if (ticket.proposalId !== void 0) {
1529
+ let verified;
1530
+ try {
1531
+ verified = await verifyTicketApprovals(ticket, mandate);
1532
+ } catch (err) {
1533
+ throw new PolicyError("require_approval_signature", "APPROVAL_SIGNATURE_REQUIRED", err.message);
1534
+ }
1535
+ const aboveCap = input.aboveCap ?? (ticket.approvalSignatures !== void 0 && ticket.approvalSignatures.length > 0);
1536
+ if (!approvalsSufficient(mandate, verified, aboveCap)) {
1537
+ throw new PolicyError("require_approval_signature", "APPROVAL_SIGNATURE_REQUIRED", "the ticket lacks a verifiable signature for each decision it rests on");
1538
+ }
1539
+ }
1540
+ }
1541
+ }
1542
+
1543
+ // src/proposal-content.ts
1544
+ var import_crypto6 = require("crypto");
1545
+ var import_core = require("@hpke/core");
1546
+ function suite() {
1547
+ return new import_core.CipherSuite({ kem: new import_core.DhkemX25519HkdfSha256(), kdf: new import_core.HkdfSha256(), aead: new import_core.Aes256Gcm() });
1548
+ }
1549
+ var webCrypto = () => {
1550
+ const c = globalThis.crypto;
1551
+ if (!c?.subtle) throw new Error("WebCrypto is not available in this runtime");
1552
+ return c;
1553
+ };
1554
+ function proposalContentJcs(args) {
1555
+ if (args === null || typeof args !== "object" || Array.isArray(args)) {
1556
+ throw new HapError("PROPOSAL_NOT_ENCRYPTED", "proposal content is one object \u2014 the action's argument object");
1557
+ }
1558
+ return canonicalize(args);
1559
+ }
1560
+ function computeProposalContentHash(args) {
1561
+ return `sha256:${(0, import_crypto6.createHash)("sha256").update(proposalContentJcs(args), "utf8").digest("hex")}`;
1562
+ }
1563
+ async function generateReaderKeyPair() {
1564
+ const kp = await suite().kem.generateKeyPair();
1565
+ return exportKeyPair(kp);
1566
+ }
1567
+ async function deriveReaderKeyPair(ikm) {
1568
+ const kp = await suite().kem.deriveKeyPair(ikm);
1569
+ return exportKeyPair(kp);
1570
+ }
1571
+ async function exportKeyPair(kp) {
1572
+ const s = suite();
1573
+ return {
1574
+ privateKey: new Uint8Array(await s.kem.serializePrivateKey(kp.privateKey)),
1575
+ publicKey: new Uint8Array(await s.kem.serializePublicKey(kp.publicKey))
1576
+ };
1577
+ }
1578
+ async function encryptProposalContent(args, readers, opts = {}) {
1579
+ if (readers.length === 0) {
1580
+ throw new HapError("PROPOSAL_NOT_ENCRYPTED", "a proposal has at least one reader (the Mandate Owner)");
1581
+ }
1582
+ const jcs = proposalContentJcs(args);
1583
+ const proposalContentHash = computeProposalContentHash(args);
1584
+ const crypto = webCrypto();
1585
+ const s = suite();
1586
+ const cek = opts.deterministic?.contentKey ?? crypto.getRandomValues(new Uint8Array(32));
1587
+ const iv = opts.deterministic?.iv ?? crypto.getRandomValues(new Uint8Array(12));
1588
+ if (cek.length !== 32 || iv.length !== 12) {
1589
+ throw new HapError("PROPOSAL_NOT_ENCRYPTED", "content key is 32 bytes and IV is 12 bytes");
1590
+ }
1591
+ const aesKey = await crypto.subtle.importKey("raw", cek, { name: "AES-GCM", length: 256 }, false, ["encrypt"]);
1592
+ const sealed = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, aesKey, new TextEncoder().encode(jcs)));
1593
+ const ciphertext = new Uint8Array(iv.length + sealed.length);
1594
+ ciphertext.set(iv, 0);
1595
+ ciphertext.set(sealed, iv.length);
1596
+ const encrypted_keys = {};
1597
+ for (const { did, publicKey } of readers) {
1598
+ const recipientPublicKey = await s.kem.deserializePublicKey(publicKey);
1599
+ const ekmIkm = opts.deterministic?.ephemeralIkm[did];
1600
+ const sender = await s.createSenderContext({
1601
+ recipientPublicKey,
1602
+ ...ekmIkm ? { ekm: await s.kem.deriveKeyPair(ekmIkm) } : {}
1603
+ });
1604
+ const ct = new Uint8Array(await sender.seal(cek));
1605
+ encrypted_keys[did] = { ct: toBase64Url(ct), enc: toBase64Url(new Uint8Array(sender.enc)) };
1606
+ }
1607
+ return {
1608
+ content: {
1609
+ ciphertext: toBase64Url(ciphertext),
1610
+ encrypted_keys,
1611
+ readers: readers.map((r) => r.did),
1612
+ unreadable: [...opts.unreadable ?? []]
1613
+ },
1614
+ proposalContentHash
1615
+ };
1616
+ }
1617
+ async function decryptProposalContent(content, readerDid, readerPrivateKey) {
1618
+ const wrap = content.encrypted_keys[readerDid];
1619
+ if (!wrap) throw new HapError("PROPOSAL_NOT_ENCRYPTED", `no key wrapped for reader ${readerDid}`);
1620
+ const s = suite();
1621
+ const crypto = webCrypto();
1622
+ let cek;
1623
+ try {
1624
+ const recipient = await s.createRecipientContext({
1625
+ recipientKey: await s.kem.deserializePrivateKey(readerPrivateKey),
1626
+ enc: fromBase64Url(wrap.enc)
1627
+ });
1628
+ cek = new Uint8Array(await recipient.open(fromBase64Url(wrap.ct)));
1629
+ } catch (err) {
1630
+ throw new HapError("PROPOSAL_NOT_ENCRYPTED", `proposal content is unreadable for ${readerDid}: ${err.message}`);
1631
+ }
1632
+ const bytes = fromBase64Url(content.ciphertext);
1633
+ if (bytes.length < 12 + 16) throw new HapError("PROPOSAL_NOT_ENCRYPTED", "ciphertext is shorter than IV + tag");
1634
+ let plaintext;
1635
+ try {
1636
+ const aesKey = await crypto.subtle.importKey("raw", cek, { name: "AES-GCM", length: 256 }, false, ["decrypt"]);
1637
+ plaintext = new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: bytes.subarray(0, 12) }, aesKey, bytes.subarray(12)));
1638
+ } catch (err) {
1639
+ throw new HapError("PROPOSAL_NOT_ENCRYPTED", `proposal content is unreadable for ${readerDid}: ${err.message}`);
1640
+ }
1641
+ const contentJcs = new TextDecoder().decode(plaintext);
1642
+ let args;
1643
+ try {
1644
+ args = JSON.parse(contentJcs);
1645
+ } catch {
1646
+ throw new HapError("PROPOSAL_NOT_ENCRYPTED", "decrypted content is not JSON");
1647
+ }
1648
+ return { arguments: args, contentJcs, proposalContentHash: computeProposalContentHash(args) };
1649
+ }
1650
+ function checkProposalReaders(content, expected) {
1651
+ const want = /* @__PURE__ */ new Set([expected.ownerDid, ...expected.deciders]);
1652
+ const have = /* @__PURE__ */ new Set([...content.readers, ...content.unreadable]);
1653
+ if (want.size !== have.size || [...want].some((d) => !have.has(d))) {
1654
+ throw new HapError("PROPOSAL_APPROVERS_INVALID", "readers \u222A unreadable differs from the owner plus the signed deciders");
1655
+ }
1656
+ const keyed = new Set(Object.keys(content.encrypted_keys));
1657
+ const readers = new Set(content.readers);
1658
+ if (keyed.size !== readers.size || [...readers].some((d) => !keyed.has(d))) {
1659
+ throw new HapError("PROPOSAL_NOT_ENCRYPTED", "readers differs from the key set of encrypted_keys");
1660
+ }
1661
+ }
1662
+
1663
+ // src/issuance.ts
1664
+ var import_crypto7 = require("crypto");
1665
+ function computeExecutionContextHash(profile) {
1666
+ const schema = profile.executionContextSchema ?? {};
1667
+ return `sha256:${(0, import_crypto7.createHash)("sha256").update(canonicalize(schema), "utf8").digest("hex")}`;
1668
+ }
1669
+ function boundReadFields(profile) {
1670
+ const fields = /* @__PURE__ */ new Set();
1671
+ for (const def of Object.values(profile.boundsSchema?.fields ?? {})) {
1672
+ const bt = def.boundType;
1673
+ if (bt && (bt.kind === "per_transaction" || bt.kind === "cumulative_sum")) fields.add(bt.of);
1674
+ }
1675
+ return fields;
1676
+ }
1677
+ function wireExecutionContext(execution, profile) {
1678
+ const allowed = boundReadFields(profile);
1679
+ const out = {};
1680
+ for (const [k, v] of Object.entries(execution)) {
1681
+ if (!allowed.has(k) || k.startsWith("_") || v === void 0 || v === null) continue;
1682
+ out[k] = v;
1683
+ }
1684
+ return out;
1685
+ }
1686
+ function checkExecutionValues(execution, profile) {
1687
+ for (const k of Object.keys(execution)) {
1688
+ if (k.startsWith("_")) {
1689
+ throw new HapError("MALFORMED_TICKET_REQUEST", `executionContext key "${k}" is reserved and MUST NOT travel`);
1690
+ }
1691
+ }
1692
+ for (const field of boundReadFields(profile)) {
1693
+ const v = execution[field];
1694
+ if (v === void 0 || v === null) continue;
1695
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0) {
1696
+ throw new HapError(
1697
+ "MALFORMED_TICKET_REQUEST",
1698
+ `executionContext.${field} MUST be a finite, non-negative number, got ${JSON.stringify(v)}`
1699
+ );
1700
+ }
1701
+ }
1702
+ }
1703
+ function boundGoverns(def, actionType) {
1704
+ return !def.appliesTo || def.appliesTo.includes(actionType);
1705
+ }
1706
+ function missingSumFields(execution, profile, actionType) {
1707
+ const missing = [];
1708
+ for (const [name, def] of Object.entries(profile.boundsSchema?.fields ?? {})) {
1709
+ const bt = def.boundType;
1710
+ if (!bt || bt.kind !== "cumulative_sum" || !boundGoverns(def, actionType)) continue;
1711
+ const v = execution[bt.of];
1712
+ if (v === void 0 || v === null) missing.push({ bound: name, field: bt.of });
1713
+ }
1714
+ return missing;
1715
+ }
1716
+ function checkBoundsValues(bounds, profile) {
1717
+ for (const [name, def] of Object.entries(profile.boundsSchema?.fields ?? {})) {
1718
+ if (name === "profile") continue;
1719
+ const v = bounds[name];
1720
+ if (v === void 0 || v === null) continue;
1721
+ if (def.type === "number" || def.maximum !== void 0) {
1722
+ if (typeof v !== "number" || !Number.isFinite(v)) {
1723
+ throw new HapError("BOUNDS_INVALID_VALUE", `bound "${name}" MUST be a number, got ${JSON.stringify(v)}`);
1724
+ }
1725
+ if (def.maximum !== void 0 && v > def.maximum) {
1726
+ throw new HapError("BOUNDS_INVALID_VALUE", `bound "${name}" = ${v} exceeds the profile's maximum ${def.maximum}`);
1727
+ }
1728
+ }
1729
+ const bt = def.boundType;
1730
+ if (bt?.kind === "enum" && !bt.values.includes(String(v))) {
1731
+ throw new HapError("BOUNDS_INVALID_VALUE", `bound "${name}" = ${JSON.stringify(v)} is not in [${bt.values.join(", ")}]`);
1732
+ }
1733
+ }
1734
+ }
1735
+ function exceedsCap(value, cap, def) {
1736
+ const bt = def.boundType;
1737
+ if (bt?.kind === "enum") {
1738
+ const order = bt.values;
1739
+ const vi = order.indexOf(String(value));
1740
+ const ci = order.indexOf(String(cap));
1741
+ if (vi < 0 || ci < 0) return true;
1742
+ return vi < ci;
1743
+ }
1744
+ return typeof value !== "number" || typeof cap !== "number" || value > cap;
1745
+ }
1746
+ function checkGroupCeilings(bounds, profile, caps) {
1747
+ const fields = profile.boundsSchema?.fields ?? {};
1748
+ const hard = { ...caps.hard ?? {} };
1749
+ for (const [name, cap] of Object.entries(caps.approval ?? {})) {
1750
+ const def = fields[name];
1751
+ const bt = def?.boundType;
1752
+ if (!def || !bt) continue;
1753
+ if (bt.kind !== "per_transaction" || def.enforcedBy === "gatekeeper") hard[name] = cap;
1754
+ }
1755
+ for (const [name, def] of Object.entries(fields)) {
1756
+ if (name === "profile") continue;
1757
+ const cap = hard[name];
1758
+ if (cap === void 0) continue;
1759
+ const v = bounds[name];
1760
+ if (v === void 0 || v === null) {
1761
+ if (!def.required) {
1762
+ throw new HapError("GROUP_CAP_EXCEEDED", `bound "${name}" is omitted under a group ceiling of ${JSON.stringify(cap)} \u2014 an omitted bound is unlimited`);
1763
+ }
1764
+ continue;
1765
+ }
1766
+ if (exceedsCap(v, cap, def)) {
1767
+ throw new HapError("GROUP_CAP_EXCEEDED", `bound "${name}" = ${JSON.stringify(v)} exceeds the group's hard cap ${JSON.stringify(cap)}`);
1768
+ }
1769
+ }
1770
+ }
1771
+ function validateAboveCapConfig(config, profile, opts) {
1772
+ const fields = profile.boundsSchema?.fields ?? {};
1773
+ for (const name of Object.keys(config.caps ?? {})) {
1774
+ if (name === "profile") throw new HapError("ABOVE_CAP_CONFIG_INVALID", "above_cap_caps MUST NOT name profile");
1775
+ const def = fields[name];
1776
+ if (!def || def.boundType?.kind !== "per_transaction" || def.type !== "number") {
1777
+ throw new HapError("ABOVE_CAP_CONFIG_INVALID", `above_cap_caps names "${name}", which is not a numeric per_transaction bound of ${profile.id}`);
1778
+ }
1779
+ }
1780
+ for (const did of config.approvers ?? []) {
1781
+ if (!isKeyBearingDid(did)) {
1782
+ throw new HapError("ABOVE_CAP_CONFIG_INVALID", `above-cap approver ${did} is not a key-bearing DID`);
1783
+ }
1784
+ }
1785
+ if (config.approvers !== void 0 && !opts.inGroup) {
1786
+ const others = config.approvers.filter((d) => d !== opts.ownerDid).length;
1787
+ const n = config.approvals;
1788
+ if (n === void 0 || !Number.isInteger(n) || n < 1 || n > others) {
1789
+ throw new HapError(
1790
+ "ABOVE_CAP_CONFIG_INVALID",
1791
+ `above_cap_approvals MUST be an integer between 1 and the ${others} approver(s) other than the owner, got ${JSON.stringify(n)}`
1792
+ );
1793
+ }
1794
+ }
1795
+ }
1796
+ function capsExceeded(execution, mandate, profile) {
1797
+ const out = [];
1798
+ for (const [name, cap] of Object.entries(mandate.above_cap_caps ?? {})) {
1799
+ const bt = profile.boundsSchema?.fields[name]?.boundType;
1800
+ if (!bt || bt.kind !== "per_transaction") continue;
1801
+ const v = execution[bt.of];
1802
+ if (v === void 0 || v === null || typeof v !== "number" || v > cap) {
1803
+ out.push({ bound: name, field: bt.of, cap, value: v });
1804
+ }
1805
+ }
1806
+ return out;
1807
+ }
1808
+
1035
1809
  // src/profiles/index.ts
1036
1810
  var PROFILES = {};
1037
1811
  function registerProfile(profileId, profile) {
@@ -1090,8 +1864,9 @@ async function verify(request, opts = {}) {
1090
1864
  }
1091
1865
  const expectedProfileHash = computeProfileHash(profile);
1092
1866
  const errors = [];
1867
+ const verifiedPayloads = [];
1093
1868
  for (const blob of request.mandates) {
1094
- const mandateErrors = await verifyOneMandate(blob, {
1869
+ const { errors: mandateErrors, payload } = await verifyOneMandate(blob, {
1095
1870
  expectedBoundsHash,
1096
1871
  expectedScopeHash,
1097
1872
  expectedProfileHash,
@@ -1099,6 +1874,7 @@ async function verify(request, opts = {}) {
1099
1874
  now
1100
1875
  });
1101
1876
  errors.push(...mandateErrors);
1877
+ if (payload) verifiedPayloads.push(payload);
1102
1878
  }
1103
1879
  if (errors.length > 0) {
1104
1880
  return { approved: false, errors };
@@ -1117,6 +1893,31 @@ async function verify(request, opts = {}) {
1117
1893
  };
1118
1894
  }
1119
1895
  }
1896
+ try {
1897
+ checkExecutionValues(request.execution, profile);
1898
+ } catch (err) {
1899
+ return { approved: false, errors: [{ code: codeFromThrown(err), message: err.message }] };
1900
+ }
1901
+ if (actionType !== void 0) {
1902
+ const capped = /* @__PURE__ */ new Set();
1903
+ for (const p of verifiedPayloads) {
1904
+ for (const name of Object.keys(p.above_cap_caps ?? {})) {
1905
+ const bt = profile.boundsSchema?.fields[name]?.boundType;
1906
+ if (bt && bt.kind === "per_transaction") capped.add(bt.of);
1907
+ }
1908
+ }
1909
+ const missing = missingSumFields(request.execution, profile, actionType).filter((m) => !capped.has(m.field));
1910
+ if (missing.length > 0) {
1911
+ return {
1912
+ approved: false,
1913
+ errors: missing.map(({ bound, field }) => ({
1914
+ code: "BOUND_EXCEEDED",
1915
+ field,
1916
+ 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).`
1917
+ }))
1918
+ };
1919
+ }
1920
+ }
1120
1921
  const boundsErrors = checkBounds(request, profile, actionType);
1121
1922
  if (boundsErrors.length > 0) {
1122
1923
  return { approved: false, errors: boundsErrors };
@@ -1150,16 +1951,21 @@ function codeFromThrown(err) {
1150
1951
  return "MALFORMED_MANDATE";
1151
1952
  }
1152
1953
  async function verifyOneMandate(blob, opts) {
1954
+ const errors = await verifyOneMandateErrors(blob, opts);
1955
+ if (errors.length > 0) return { errors };
1956
+ return { errors, payload: decodeMandateBlob(blob).payload };
1957
+ }
1958
+ async function verifyOneMandateErrors(blob, opts) {
1153
1959
  let mandate;
1154
1960
  try {
1155
1961
  mandate = decodeMandateBlob(blob);
1156
1962
  } catch (err) {
1157
1963
  return [{ code: "MALFORMED_MANDATE", message: err.message }];
1158
1964
  }
1159
- if (mandate.payload.version !== PROTOCOL_VERSION) {
1965
+ if (!isSupportedVersion(mandate.payload.version)) {
1160
1966
  return [{
1161
1967
  code: "VERSION_UNSUPPORTED",
1162
- message: `mandate version ${JSON.stringify(mandate.payload.version)} is not verifiable by this Gatekeeper (verifies ${PROTOCOL_VERSION} only)`
1968
+ message: `mandate version ${JSON.stringify(mandate.payload.version)} is not verifiable by this Gatekeeper (verifies [${SUPPORTED_VERSIONS.join(", ")}])`
1163
1969
  }];
1164
1970
  }
1165
1971
  try {
@@ -1174,6 +1980,7 @@ async function verifyOneMandate(blob, opts) {
1174
1980
  }
1175
1981
  try {
1176
1982
  validateMandateOwners(mandate.payload);
1983
+ validateAboveCapFields(mandate.payload);
1177
1984
  } catch (err) {
1178
1985
  return [{ code: codeFromThrown(err), message: err.message }];
1179
1986
  }
@@ -1340,43 +2147,70 @@ function checkScopeConstraints(scope, execution, profile) {
1340
2147
  }
1341
2148
  // Annotate the CommonJS export names for ESM import in node:
1342
2149
  0 && (module.exports = {
2150
+ BINDING_RANK,
2151
+ COMMITMENT_MODES,
1343
2152
  CanonicalValueError,
1344
2153
  ContentBindingError,
1345
2154
  HapError,
2155
+ ISSUABLE_VERSIONS,
1346
2156
  MANDATE_ERROR_CODES,
1347
2157
  OwnerSignatureError,
2158
+ PROJECTION_MAX_VALIDITY_SECONDS,
2159
+ PROPOSAL_ERROR_CODES,
1348
2160
  PROTOCOL_VERSION,
2161
+ PolicyError,
1349
2162
  SIGNABLE_COMMITMENT_MODES,
1350
2163
  SUPPORTED_VERSIONS,
1351
2164
  TICKET_ERROR_CODES,
2165
+ TICKET_FRESHNESS_SECONDS,
1352
2166
  allowedCommitmentModes,
1353
2167
  approvalSigningBytes,
2168
+ approvalsSufficient,
2169
+ assertSupportedVersion,
2170
+ assertTicketShape,
1354
2171
  bindingAppliesTo,
2172
+ bindingRank,
1355
2173
  boundActionTypes,
2174
+ boundGoverns,
2175
+ boundReadFields,
1356
2176
  buildMandateProjection,
2177
+ buildTicketProjection,
1357
2178
  canonicalBounds,
1358
2179
  canonicalScope,
1359
2180
  canonicalize,
1360
2181
  canonicalizeText,
2182
+ capsExceeded,
2183
+ checkBoundsValues,
1361
2184
  checkDiscloseSubset,
2185
+ checkExecutionValues,
2186
+ checkGroupCeilings,
1362
2187
  checkMandateExpiry,
1363
2188
  checkOwnerSignatureRequirement,
2189
+ checkProposalReaders,
1364
2190
  clearProfiles,
1365
2191
  computeBoundsHash,
1366
2192
  computeContentHash,
2193
+ computeExecutionContextHash,
1367
2194
  computeFieldsContentHash,
1368
2195
  computeIntentDisclosureHash,
1369
2196
  computeIntentHash,
1370
2197
  computeProfileHash,
2198
+ computeProposalContentHash,
1371
2199
  computeScopeHash,
1372
2200
  contentCanonicalBytes,
1373
2201
  decodeDidKey,
1374
2202
  decodeMandateBlob,
2203
+ decryptProposalContent,
1375
2204
  deriveIdentityLine,
2205
+ deriveReaderKeyPair,
1376
2206
  encodeDidKey,
1377
2207
  encodeMandateBlob,
2208
+ encryptProposalContent,
2209
+ evaluateVerifierPolicy,
2210
+ exceedsCap,
1378
2211
  filesFields,
1379
2212
  fromBase64Url,
2213
+ generateReaderKeyPair,
1380
2214
  getAllProfiles,
1381
2215
  getProfile,
1382
2216
  intentDisclosureCanonicalBytes,
@@ -1384,18 +2218,25 @@ function checkScopeConstraints(scope, execution, profile) {
1384
2218
  isCommitmentModeAllowed,
1385
2219
  isFieldBinding,
1386
2220
  isKeyBearingDid,
2221
+ isSupportedVersion,
1387
2222
  listProfiles,
1388
2223
  mandateBlobId,
2224
+ missingSumFields,
1389
2225
  negotiateVersion,
1390
2226
  projectionSigningBytes,
2227
+ proposalContentJcs,
1391
2228
  publicTicketView,
2229
+ reconstructApproval,
1392
2230
  registerProfile,
1393
2231
  selectBoundFields,
1394
2232
  signApproval,
1395
2233
  signMandate,
1396
2234
  signMandateProjection,
1397
2235
  signTicket,
2236
+ signTicketProjection,
1398
2237
  toBase64Url,
2238
+ validateAboveCapConfig,
2239
+ validateAboveCapFields,
1399
2240
  validateBoundsParams,
1400
2241
  validateMandateOwners,
1401
2242
  validateProfile,
@@ -1407,5 +2248,10 @@ function checkScopeConstraints(scope, execution, profile) {
1407
2248
  verifyMandateSignature,
1408
2249
  verifyOwnerSignature,
1409
2250
  verifyOwnerSignatures,
1410
- verifyTicketSignature
2251
+ verifyProjectionOwner,
2252
+ verifyTicketAgainstRequest,
2253
+ verifyTicketApprovals,
2254
+ verifyTicketProjection,
2255
+ verifyTicketSignature,
2256
+ wireExecutionContext
1411
2257
  });