@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/README.md +3 -3
- package/dist/index.d.mts +591 -123
- package/dist/index.d.ts +591 -123
- package/dist/index.js +887 -23
- package/dist/index.mjs +845 -22
- package/package.json +2 -1
- package/src/canonicalize.ts +9 -0
- package/src/commitment-modes.ts +21 -9
- package/src/content-binding.ts +26 -1
- package/src/errors.ts +24 -8
- package/src/gatekeeper.ts +52 -8
- package/src/index.ts +5 -1
- package/src/issuance.ts +250 -0
- package/src/mandate.ts +52 -14
- package/src/owner-signature.ts +86 -39
- package/src/profile.ts +45 -0
- package/src/proposal-content.ts +219 -0
- package/src/ticket-projection.ts +167 -0
- package/src/ticket.ts +254 -20
- package/src/types.ts +50 -5
- package/src/verifier-policy.ts +178 -0
- package/src/versions.ts +50 -19
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
|
-
|
|
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.
|
|
196
|
-
var SUPPORTED_VERSIONS = ["0.7"];
|
|
197
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
894
|
-
|
|
895
|
-
|
|
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("
|
|
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 =
|
|
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
|
|
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 ${
|
|
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
|
-
|
|
2251
|
+
verifyProjectionOwner,
|
|
2252
|
+
verifyTicketAgainstRequest,
|
|
2253
|
+
verifyTicketApprovals,
|
|
2254
|
+
verifyTicketProjection,
|
|
2255
|
+
verifyTicketSignature,
|
|
2256
|
+
wireExecutionContext
|
|
1393
2257
|
});
|