@humanagencyp/hap-core 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -30,60 +30,96 @@ 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,
94
+ filesFields: () => filesFields,
68
95
  fromBase64Url: () => fromBase64Url,
96
+ generateReaderKeyPair: () => generateReaderKeyPair,
69
97
  getAllProfiles: () => getAllProfiles,
70
98
  getProfile: () => getProfile,
71
99
  intentDisclosureCanonicalBytes: () => intentDisclosureCanonicalBytes,
100
+ invalidFieldKindsReason: () => invalidFieldKindsReason,
72
101
  isCommitmentModeAllowed: () => isCommitmentModeAllowed,
73
102
  isFieldBinding: () => isFieldBinding,
74
103
  isKeyBearingDid: () => isKeyBearingDid,
104
+ isSupportedVersion: () => isSupportedVersion,
75
105
  listProfiles: () => listProfiles,
76
106
  mandateBlobId: () => mandateBlobId,
107
+ missingSumFields: () => missingSumFields,
77
108
  negotiateVersion: () => negotiateVersion,
78
109
  projectionSigningBytes: () => projectionSigningBytes,
110
+ proposalContentJcs: () => proposalContentJcs,
79
111
  publicTicketView: () => publicTicketView,
112
+ reconstructApproval: () => reconstructApproval,
80
113
  registerProfile: () => registerProfile,
81
114
  selectBoundFields: () => selectBoundFields,
82
115
  signApproval: () => signApproval,
83
116
  signMandate: () => signMandate,
84
117
  signMandateProjection: () => signMandateProjection,
85
118
  signTicket: () => signTicket,
119
+ signTicketProjection: () => signTicketProjection,
86
120
  toBase64Url: () => toBase64Url,
121
+ validateAboveCapConfig: () => validateAboveCapConfig,
122
+ validateAboveCapFields: () => validateAboveCapFields,
87
123
  validateBoundsParams: () => validateBoundsParams,
88
124
  validateMandateOwners: () => validateMandateOwners,
89
125
  validateProfile: () => validateProfile,
@@ -95,11 +131,17 @@ __export(index_exports, {
95
131
  verifyMandateSignature: () => verifyMandateSignature,
96
132
  verifyOwnerSignature: () => verifyOwnerSignature,
97
133
  verifyOwnerSignatures: () => verifyOwnerSignatures,
98
- verifyTicketSignature: () => verifyTicketSignature
134
+ verifyProjectionOwner: () => verifyProjectionOwner,
135
+ verifyTicketAgainstRequest: () => verifyTicketAgainstRequest,
136
+ verifyTicketApprovals: () => verifyTicketApprovals,
137
+ verifyTicketProjection: () => verifyTicketProjection,
138
+ verifyTicketSignature: () => verifyTicketSignature,
139
+ wireExecutionContext: () => wireExecutionContext
99
140
  });
100
141
  module.exports = __toCommonJS(index_exports);
101
142
 
102
143
  // src/canonicalize.ts
144
+ var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
103
145
  function canonicalize(value) {
104
146
  if (value === void 0) {
105
147
  throw new Error("canonicalize: undefined is not serializable");
@@ -107,6 +149,9 @@ function canonicalize(value) {
107
149
  if (typeof value === "number" && !Number.isFinite(value)) {
108
150
  throw new Error(`canonicalize: ${value} is not a valid JSON number`);
109
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
+ }
110
155
  if (value === null || typeof value !== "object") {
111
156
  return JSON.stringify(value);
112
157
  }
@@ -156,6 +201,10 @@ var MANDATE_ERROR_CODES = [
156
201
  "BOUNDS_INVALID_VALUE",
157
202
  "SCOPE_INVALID_VALUE",
158
203
  "ABOVE_CAP_CONFIG_INVALID",
204
+ "COMMITMENT_MODE_NOT_ALLOWED",
205
+ // v0.8
206
+ "GROUP_CAP_EXCEEDED",
207
+ // v0.8
159
208
  "OWNER_SIGNATURE_REQUIRED",
160
209
  "OWNER_SIGNATURE_INVALID"
161
210
  ];
@@ -169,6 +218,10 @@ var TICKET_ERROR_CODES = [
169
218
  "PROFILE_NOT_FOUND",
170
219
  "INVALID_ACTION_TYPE",
171
220
  "APPROVAL_REQUIRED",
221
+ "APPROVAL_IMPOSSIBLE",
222
+ // v0.8
223
+ "PROPOSAL_CONTENT_MISMATCH",
224
+ // v0.8
172
225
  "PROPOSAL_REQUIRED",
173
226
  "PROPOSAL_NOT_FOUND",
174
227
  "PROPOSAL_NOT_APPROVED",
@@ -177,11 +230,25 @@ var TICKET_ERROR_CODES = [
177
230
  "PROPOSAL_MISMATCH",
178
231
  "PROPOSAL_MANDATE_MISMATCH",
179
232
  "PROPOSAL_ALREADY_EXECUTED",
233
+ "PROPOSAL_WITHDRAWN",
234
+ // v0.8
180
235
  "MALFORMED_TICKET_REQUEST",
236
+ "CONTENT_BINDING_INCOMPLETE",
237
+ // v0.8, Gatekeeper · local
181
238
  "IDEMPOTENCY_KEY_REQUIRED",
182
239
  "IDEMPOTENCY_MISMATCH",
183
240
  "APPROVAL_SIGNATURE_REQUIRED",
184
- "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"
185
252
  ];
186
253
  var HapError = class extends Error {
187
254
  constructor(code, message) {
@@ -192,9 +259,13 @@ var HapError = class extends Error {
192
259
  };
193
260
 
194
261
  // src/versions.ts
195
- var PROTOCOL_VERSION = "0.7";
196
- var SUPPORTED_VERSIONS = ["0.7"];
197
- 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) {
198
269
  const requestedVersions = requested ?? ["0.6"];
199
270
  const common = supported.filter((v) => requestedVersions.includes(v)).sort().reverse();
200
271
  if (common.length === 0) {
@@ -205,6 +276,14 @@ function negotiateVersion(requested, supported = SUPPORTED_VERSIONS) {
205
276
  }
206
277
  return common[0];
207
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
+ }
208
287
 
209
288
  // src/content-binding.ts
210
289
  var import_crypto = require("crypto");
@@ -315,6 +394,20 @@ function selectBoundFields(binding, args) {
315
394
  function computeFieldsContentHash(binding, args) {
316
395
  return computeContentHash(binding, selectBoundFields(binding, args));
317
396
  }
397
+ var FIELD_KINDS = /* @__PURE__ */ new Set(["text", "list", "files"]);
398
+ function invalidFieldKindsReason(binding) {
399
+ const kinds = binding.field_kinds;
400
+ if (kinds === void 0) return null;
401
+ if (!kinds || typeof kinds !== "object" || Array.isArray(kinds)) return "field_kinds must be an object";
402
+ for (const [field, kind] of Object.entries(kinds)) {
403
+ if (!(binding.fields ?? []).includes(field)) return `field_kinds names "${field}", which is not in fields`;
404
+ if (!FIELD_KINDS.has(kind)) return `field_kinds.${field} must be one of text, list, files`;
405
+ }
406
+ return null;
407
+ }
408
+ function filesFields(binding) {
409
+ return Object.entries(binding.field_kinds ?? {}).filter(([, k]) => k === "files").map(([f]) => f);
410
+ }
318
411
 
319
412
  // src/intent-disclosure.ts
320
413
  var import_crypto2 = require("crypto");
@@ -497,6 +590,7 @@ function computeScopeHash(params, profile) {
497
590
 
498
591
  // src/commitment-modes.ts
499
592
  var SIGNABLE_COMMITMENT_MODES = ["review", "automatic"];
593
+ var COMMITMENT_MODES = ["automatic", "review", "review_above_cap"];
500
594
  var isSignable = (v) => typeof v === "string" && SIGNABLE_COMMITMENT_MODES.includes(v);
501
595
  function allowedCommitmentModes(profile) {
502
596
  const declared = profile.commitment_modes;
@@ -505,7 +599,9 @@ function allowedCommitmentModes(profile) {
505
599
  return SIGNABLE_COMMITMENT_MODES.filter((m) => declared.includes(m));
506
600
  }
507
601
  function isCommitmentModeAllowed(profile, mode) {
508
- 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);
509
605
  }
510
606
 
511
607
  // src/profile.ts
@@ -620,6 +716,7 @@ function validateProfile(profile) {
620
716
  }
621
717
  const seen = /* @__PURE__ */ new Set();
622
718
  for (const m of declaredModes) {
719
+ if (m === "review_above_cap") continue;
623
720
  if (!SIGNABLE_COMMITMENT_MODES.includes(m)) {
624
721
  errors.push({
625
722
  code: "PROFILE_INVALID",
@@ -641,6 +738,46 @@ function validateProfile(profile) {
641
738
  message: "commitment_modes declares no mode this profile can actually use."
642
739
  });
643
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
+ }
644
781
  return errors;
645
782
  }
646
783
  function checkDiscloseSubset(mandateDiscloseFields, profileDiscloseFields) {
@@ -807,7 +944,7 @@ function validateMandateOwners(payload) {
807
944
  if (!Array.isArray(owners) || owners.length !== 1) {
808
945
  throw new HapError(
809
946
  "MALFORMED_MANDATE",
810
- `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}`
811
948
  );
812
949
  }
813
950
  const entry = owners[0];
@@ -826,17 +963,45 @@ function validateMandateOwners(payload) {
826
963
  throw new HapError("OWNER_SIGNATURE_INVALID", `co-signing DID ${entry.did} is not key-bearing`);
827
964
  }
828
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
+ }
829
998
  async function verifyMandate(blob, opts) {
830
999
  const mandate = decodeMandateBlob(blob);
831
- if (mandate.payload.version !== PROTOCOL_VERSION) {
832
- throw new HapError(
833
- "VERSION_UNSUPPORTED",
834
- `mandate version ${JSON.stringify(mandate.payload.version)} is not verifiable by this implementation (verifies ${PROTOCOL_VERSION} only \u2014 re-issue the mandate)`
835
- );
836
- }
1000
+ assertSupportedVersion(mandate.payload.version, "mandate");
837
1001
  await verifyMandateSignature(mandate, opts);
838
1002
  checkMandateExpiry(mandate.payload, opts?.now);
839
1003
  validateMandateOwners(mandate.payload);
1004
+ validateAboveCapFields(mandate.payload);
840
1005
  return mandate.payload;
841
1006
  }
842
1007
 
@@ -873,9 +1038,10 @@ function buildMandateProjection(payload, entry) {
873
1038
  "mandate projection requires profile_hash, bounds_hash, scope_hash and commitment_mode"
874
1039
  );
875
1040
  }
1041
+ const version = payload.version === "0.7" ? "0.7" : "0.8";
876
1042
  const projection = {
877
1043
  typ: "HAP-mandate-projection",
878
- version: "0.7",
1044
+ version,
879
1045
  profile_id,
880
1046
  profile_hash,
881
1047
  owner_did: entry.did,
@@ -890,13 +1056,32 @@ function buildMandateProjection(payload, entry) {
890
1056
  if (payload.intent_disclosure_hash !== void 0) {
891
1057
  projection.intent_disclosure_hash = payload.intent_disclosure_hash;
892
1058
  }
893
- if (payload.commitment_mode === "review_above_cap") {
894
- projection.above_cap_caps = payload.above_cap_caps;
895
- 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
+ }
896
1070
  }
897
1071
  if (payload.disclose_fields !== void 0) {
898
1072
  projection.disclose_fields = payload.disclose_fields;
899
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
+ }
900
1085
  return projection;
901
1086
  }
902
1087
  function projectionSigningBytes(projection) {
@@ -942,11 +1127,14 @@ async function verifyOwnerSignatures(mandate) {
942
1127
  return entries;
943
1128
  }
944
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
+ }
945
1133
  let publicKey;
946
1134
  try {
947
1135
  publicKey = decodeDidKey(signerDid);
948
1136
  } catch (err) {
949
- throw new OwnerSignatureError("OWNER_SIGNATURE_INVALID", err.message);
1137
+ throw new OwnerSignatureError("APPROVAL_SIGNATURE_INVALID", err.message);
950
1138
  }
951
1139
  const ok = await ed2.verifyAsync(decodeOrThrow(signature, "APPROVAL_SIGNATURE_INVALID"), approvalSigningBytes(approval), publicKey).catch(() => false);
952
1140
  if (!ok) {
@@ -954,6 +1142,9 @@ async function verifyApproval(approval, signature, signerDid) {
954
1142
  }
955
1143
  }
956
1144
  var BINDING_RANK = { raw: 0, webauthn: 1, eudi: 2 };
1145
+ function bindingRank(binding) {
1146
+ return BINDING_RANK[binding] ?? 0;
1147
+ }
957
1148
  function checkOwnerSignatureRequirement(payload, floor) {
958
1149
  if (!floor?.required) return;
959
1150
  const entry = payload.mandate_owners[0];
@@ -965,7 +1156,7 @@ function checkOwnerSignatureRequirement(payload, floor) {
965
1156
  );
966
1157
  }
967
1158
  const minRank = floor.minBinding ? BINDING_RANK[floor.minBinding] : 0;
968
- const haveRank = BINDING_RANK[entry.binding] ?? 0;
1159
+ const haveRank = bindingRank(entry.binding);
969
1160
  if (haveRank < minRank) {
970
1161
  throw new OwnerSignatureError(
971
1162
  "OWNER_SIGNATURE_REQUIRED",
@@ -976,11 +1167,27 @@ function checkOwnerSignatureRequirement(payload, floor) {
976
1167
 
977
1168
  // src/ticket.ts
978
1169
  var ed3 = __toESM(require("@noble/ed25519"));
1170
+ var TICKET_FRESHNESS_SECONDS = 300;
979
1171
  async function signTicket(payload, privateKey) {
980
1172
  const signature = toBase64Url(await ed3.signAsync(new TextEncoder().encode(canonicalize(payload)), privateKey));
981
1173
  return { ...payload, signature };
982
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
+ }
983
1188
  async function verifyTicketSignature(ticket, opts) {
1189
+ assertTicketShape(ticket);
1190
+ assertSupportedVersion(ticket.version, "ticket");
984
1191
  const { signature, ...unsigned } = ticket;
985
1192
  if (!signature) {
986
1193
  throw new HapError("INVALID_SIGNATURE", "ticket carries no signature");
@@ -1006,6 +1213,120 @@ async function verifyTicketSignature(ticket, opts) {
1006
1213
  throw new HapError("INVALID_SIGNATURE", "ticket signature verification failed");
1007
1214
  }
1008
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
+ }
1009
1330
  function publicTicketView(ticket, governingDiscloseFields = []) {
1010
1331
  const { userId: _userId, cumulativeState: _cumulativeState, limits: _limits, executionContext, ...rest } = ticket;
1011
1332
  const allowed = new Set(governingDiscloseFields);
@@ -1016,6 +1337,475 @@ function publicTicketView(ticket, governingDiscloseFields = []) {
1016
1337
  return { ...rest, executionContext: disclosedExecutionContext };
1017
1338
  }
1018
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
+
1019
1809
  // src/profiles/index.ts
1020
1810
  var PROFILES = {};
1021
1811
  function registerProfile(profileId, profile) {
@@ -1074,8 +1864,9 @@ async function verify(request, opts = {}) {
1074
1864
  }
1075
1865
  const expectedProfileHash = computeProfileHash(profile);
1076
1866
  const errors = [];
1867
+ const verifiedPayloads = [];
1077
1868
  for (const blob of request.mandates) {
1078
- const mandateErrors = await verifyOneMandate(blob, {
1869
+ const { errors: mandateErrors, payload } = await verifyOneMandate(blob, {
1079
1870
  expectedBoundsHash,
1080
1871
  expectedScopeHash,
1081
1872
  expectedProfileHash,
@@ -1083,6 +1874,7 @@ async function verify(request, opts = {}) {
1083
1874
  now
1084
1875
  });
1085
1876
  errors.push(...mandateErrors);
1877
+ if (payload) verifiedPayloads.push(payload);
1086
1878
  }
1087
1879
  if (errors.length > 0) {
1088
1880
  return { approved: false, errors };
@@ -1101,6 +1893,31 @@ async function verify(request, opts = {}) {
1101
1893
  };
1102
1894
  }
1103
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
+ }
1104
1921
  const boundsErrors = checkBounds(request, profile, actionType);
1105
1922
  if (boundsErrors.length > 0) {
1106
1923
  return { approved: false, errors: boundsErrors };
@@ -1134,16 +1951,21 @@ function codeFromThrown(err) {
1134
1951
  return "MALFORMED_MANDATE";
1135
1952
  }
1136
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) {
1137
1959
  let mandate;
1138
1960
  try {
1139
1961
  mandate = decodeMandateBlob(blob);
1140
1962
  } catch (err) {
1141
1963
  return [{ code: "MALFORMED_MANDATE", message: err.message }];
1142
1964
  }
1143
- if (mandate.payload.version !== PROTOCOL_VERSION) {
1965
+ if (!isSupportedVersion(mandate.payload.version)) {
1144
1966
  return [{
1145
1967
  code: "VERSION_UNSUPPORTED",
1146
- 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(", ")}])`
1147
1969
  }];
1148
1970
  }
1149
1971
  try {
@@ -1158,6 +1980,7 @@ async function verifyOneMandate(blob, opts) {
1158
1980
  }
1159
1981
  try {
1160
1982
  validateMandateOwners(mandate.payload);
1983
+ validateAboveCapFields(mandate.payload);
1161
1984
  } catch (err) {
1162
1985
  return [{ code: codeFromThrown(err), message: err.message }];
1163
1986
  }
@@ -1324,60 +2147,96 @@ function checkScopeConstraints(scope, execution, profile) {
1324
2147
  }
1325
2148
  // Annotate the CommonJS export names for ESM import in node:
1326
2149
  0 && (module.exports = {
2150
+ BINDING_RANK,
2151
+ COMMITMENT_MODES,
1327
2152
  CanonicalValueError,
1328
2153
  ContentBindingError,
1329
2154
  HapError,
2155
+ ISSUABLE_VERSIONS,
1330
2156
  MANDATE_ERROR_CODES,
1331
2157
  OwnerSignatureError,
2158
+ PROJECTION_MAX_VALIDITY_SECONDS,
2159
+ PROPOSAL_ERROR_CODES,
1332
2160
  PROTOCOL_VERSION,
2161
+ PolicyError,
1333
2162
  SIGNABLE_COMMITMENT_MODES,
1334
2163
  SUPPORTED_VERSIONS,
1335
2164
  TICKET_ERROR_CODES,
2165
+ TICKET_FRESHNESS_SECONDS,
1336
2166
  allowedCommitmentModes,
1337
2167
  approvalSigningBytes,
2168
+ approvalsSufficient,
2169
+ assertSupportedVersion,
2170
+ assertTicketShape,
1338
2171
  bindingAppliesTo,
2172
+ bindingRank,
1339
2173
  boundActionTypes,
2174
+ boundGoverns,
2175
+ boundReadFields,
1340
2176
  buildMandateProjection,
2177
+ buildTicketProjection,
1341
2178
  canonicalBounds,
1342
2179
  canonicalScope,
1343
2180
  canonicalize,
1344
2181
  canonicalizeText,
2182
+ capsExceeded,
2183
+ checkBoundsValues,
1345
2184
  checkDiscloseSubset,
2185
+ checkExecutionValues,
2186
+ checkGroupCeilings,
1346
2187
  checkMandateExpiry,
1347
2188
  checkOwnerSignatureRequirement,
2189
+ checkProposalReaders,
1348
2190
  clearProfiles,
1349
2191
  computeBoundsHash,
1350
2192
  computeContentHash,
2193
+ computeExecutionContextHash,
1351
2194
  computeFieldsContentHash,
1352
2195
  computeIntentDisclosureHash,
1353
2196
  computeIntentHash,
1354
2197
  computeProfileHash,
2198
+ computeProposalContentHash,
1355
2199
  computeScopeHash,
1356
2200
  contentCanonicalBytes,
1357
2201
  decodeDidKey,
1358
2202
  decodeMandateBlob,
2203
+ decryptProposalContent,
1359
2204
  deriveIdentityLine,
2205
+ deriveReaderKeyPair,
1360
2206
  encodeDidKey,
1361
2207
  encodeMandateBlob,
2208
+ encryptProposalContent,
2209
+ evaluateVerifierPolicy,
2210
+ exceedsCap,
2211
+ filesFields,
1362
2212
  fromBase64Url,
2213
+ generateReaderKeyPair,
1363
2214
  getAllProfiles,
1364
2215
  getProfile,
1365
2216
  intentDisclosureCanonicalBytes,
2217
+ invalidFieldKindsReason,
1366
2218
  isCommitmentModeAllowed,
1367
2219
  isFieldBinding,
1368
2220
  isKeyBearingDid,
2221
+ isSupportedVersion,
1369
2222
  listProfiles,
1370
2223
  mandateBlobId,
2224
+ missingSumFields,
1371
2225
  negotiateVersion,
1372
2226
  projectionSigningBytes,
2227
+ proposalContentJcs,
1373
2228
  publicTicketView,
2229
+ reconstructApproval,
1374
2230
  registerProfile,
1375
2231
  selectBoundFields,
1376
2232
  signApproval,
1377
2233
  signMandate,
1378
2234
  signMandateProjection,
1379
2235
  signTicket,
2236
+ signTicketProjection,
1380
2237
  toBase64Url,
2238
+ validateAboveCapConfig,
2239
+ validateAboveCapFields,
1381
2240
  validateBoundsParams,
1382
2241
  validateMandateOwners,
1383
2242
  validateProfile,
@@ -1389,5 +2248,10 @@ function checkScopeConstraints(scope, execution, profile) {
1389
2248
  verifyMandateSignature,
1390
2249
  verifyOwnerSignature,
1391
2250
  verifyOwnerSignatures,
1392
- verifyTicketSignature
2251
+ verifyProjectionOwner,
2252
+ verifyTicketAgainstRequest,
2253
+ verifyTicketApprovals,
2254
+ verifyTicketProjection,
2255
+ verifyTicketSignature,
2256
+ wireExecutionContext
1393
2257
  });