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