@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.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/canonicalize.ts
|
|
2
|
+
var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
2
3
|
function canonicalize(value) {
|
|
3
4
|
if (value === void 0) {
|
|
4
5
|
throw new Error("canonicalize: undefined is not serializable");
|
|
@@ -6,6 +7,9 @@ function canonicalize(value) {
|
|
|
6
7
|
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
7
8
|
throw new Error(`canonicalize: ${value} is not a valid JSON number`);
|
|
8
9
|
}
|
|
10
|
+
if (typeof value === "string" && LONE_SURROGATE.test(value)) {
|
|
11
|
+
throw new Error("canonicalize: string contains a lone surrogate and cannot be signed");
|
|
12
|
+
}
|
|
9
13
|
if (value === null || typeof value !== "object") {
|
|
10
14
|
return JSON.stringify(value);
|
|
11
15
|
}
|
|
@@ -55,6 +59,10 @@ var MANDATE_ERROR_CODES = [
|
|
|
55
59
|
"BOUNDS_INVALID_VALUE",
|
|
56
60
|
"SCOPE_INVALID_VALUE",
|
|
57
61
|
"ABOVE_CAP_CONFIG_INVALID",
|
|
62
|
+
"COMMITMENT_MODE_NOT_ALLOWED",
|
|
63
|
+
// v0.8
|
|
64
|
+
"GROUP_CAP_EXCEEDED",
|
|
65
|
+
// v0.8
|
|
58
66
|
"OWNER_SIGNATURE_REQUIRED",
|
|
59
67
|
"OWNER_SIGNATURE_INVALID"
|
|
60
68
|
];
|
|
@@ -68,6 +76,10 @@ var TICKET_ERROR_CODES = [
|
|
|
68
76
|
"PROFILE_NOT_FOUND",
|
|
69
77
|
"INVALID_ACTION_TYPE",
|
|
70
78
|
"APPROVAL_REQUIRED",
|
|
79
|
+
"APPROVAL_IMPOSSIBLE",
|
|
80
|
+
// v0.8
|
|
81
|
+
"PROPOSAL_CONTENT_MISMATCH",
|
|
82
|
+
// v0.8
|
|
71
83
|
"PROPOSAL_REQUIRED",
|
|
72
84
|
"PROPOSAL_NOT_FOUND",
|
|
73
85
|
"PROPOSAL_NOT_APPROVED",
|
|
@@ -76,11 +88,25 @@ var TICKET_ERROR_CODES = [
|
|
|
76
88
|
"PROPOSAL_MISMATCH",
|
|
77
89
|
"PROPOSAL_MANDATE_MISMATCH",
|
|
78
90
|
"PROPOSAL_ALREADY_EXECUTED",
|
|
91
|
+
"PROPOSAL_WITHDRAWN",
|
|
92
|
+
// v0.8
|
|
79
93
|
"MALFORMED_TICKET_REQUEST",
|
|
94
|
+
"CONTENT_BINDING_INCOMPLETE",
|
|
95
|
+
// v0.8, Gatekeeper · local
|
|
80
96
|
"IDEMPOTENCY_KEY_REQUIRED",
|
|
81
97
|
"IDEMPOTENCY_MISMATCH",
|
|
82
98
|
"APPROVAL_SIGNATURE_REQUIRED",
|
|
83
|
-
"APPROVAL_SIGNATURE_INVALID"
|
|
99
|
+
"APPROVAL_SIGNATURE_INVALID",
|
|
100
|
+
"TICKET_REQUEST_MISMATCH",
|
|
101
|
+
// v0.8, Gatekeeper · local
|
|
102
|
+
"POLICY_UNMET"
|
|
103
|
+
// v0.8, Verifier
|
|
104
|
+
];
|
|
105
|
+
var PROPOSAL_ERROR_CODES = [
|
|
106
|
+
"PROPOSAL_NOT_ENCRYPTED",
|
|
107
|
+
"DECIDER_INVALID",
|
|
108
|
+
"PROPOSAL_ALREADY_DECIDED",
|
|
109
|
+
"PROPOSAL_APPROVERS_INVALID"
|
|
84
110
|
];
|
|
85
111
|
var HapError = class extends Error {
|
|
86
112
|
constructor(code, message) {
|
|
@@ -91,9 +117,13 @@ var HapError = class extends Error {
|
|
|
91
117
|
};
|
|
92
118
|
|
|
93
119
|
// src/versions.ts
|
|
94
|
-
var PROTOCOL_VERSION = "0.
|
|
95
|
-
var SUPPORTED_VERSIONS = ["0.7"];
|
|
96
|
-
|
|
120
|
+
var PROTOCOL_VERSION = "0.8";
|
|
121
|
+
var SUPPORTED_VERSIONS = ["0.8", "0.7"];
|
|
122
|
+
var ISSUABLE_VERSIONS = [PROTOCOL_VERSION];
|
|
123
|
+
function isSupportedVersion(v) {
|
|
124
|
+
return typeof v === "string" && SUPPORTED_VERSIONS.includes(v);
|
|
125
|
+
}
|
|
126
|
+
function negotiateVersion(requested, supported = ISSUABLE_VERSIONS) {
|
|
97
127
|
const requestedVersions = requested ?? ["0.6"];
|
|
98
128
|
const common = supported.filter((v) => requestedVersions.includes(v)).sort().reverse();
|
|
99
129
|
if (common.length === 0) {
|
|
@@ -104,6 +134,14 @@ function negotiateVersion(requested, supported = SUPPORTED_VERSIONS) {
|
|
|
104
134
|
}
|
|
105
135
|
return common[0];
|
|
106
136
|
}
|
|
137
|
+
function assertSupportedVersion(version, what) {
|
|
138
|
+
if (!isSupportedVersion(version)) {
|
|
139
|
+
throw new HapError(
|
|
140
|
+
"VERSION_UNSUPPORTED",
|
|
141
|
+
`${what} version ${JSON.stringify(version)} is not verifiable by this implementation (verifies [${SUPPORTED_VERSIONS.join(", ")}])`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
107
145
|
|
|
108
146
|
// src/content-binding.ts
|
|
109
147
|
import { createHash } from "crypto";
|
|
@@ -214,6 +252,20 @@ function selectBoundFields(binding, args) {
|
|
|
214
252
|
function computeFieldsContentHash(binding, args) {
|
|
215
253
|
return computeContentHash(binding, selectBoundFields(binding, args));
|
|
216
254
|
}
|
|
255
|
+
var FIELD_KINDS = /* @__PURE__ */ new Set(["text", "list", "files"]);
|
|
256
|
+
function invalidFieldKindsReason(binding) {
|
|
257
|
+
const kinds = binding.field_kinds;
|
|
258
|
+
if (kinds === void 0) return null;
|
|
259
|
+
if (!kinds || typeof kinds !== "object" || Array.isArray(kinds)) return "field_kinds must be an object";
|
|
260
|
+
for (const [field, kind] of Object.entries(kinds)) {
|
|
261
|
+
if (!(binding.fields ?? []).includes(field)) return `field_kinds names "${field}", which is not in fields`;
|
|
262
|
+
if (!FIELD_KINDS.has(kind)) return `field_kinds.${field} must be one of text, list, files`;
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
function filesFields(binding) {
|
|
267
|
+
return Object.entries(binding.field_kinds ?? {}).filter(([, k]) => k === "files").map(([f]) => f);
|
|
268
|
+
}
|
|
217
269
|
|
|
218
270
|
// src/intent-disclosure.ts
|
|
219
271
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -396,6 +448,7 @@ function computeScopeHash(params, profile) {
|
|
|
396
448
|
|
|
397
449
|
// src/commitment-modes.ts
|
|
398
450
|
var SIGNABLE_COMMITMENT_MODES = ["review", "automatic"];
|
|
451
|
+
var COMMITMENT_MODES = ["automatic", "review", "review_above_cap"];
|
|
399
452
|
var isSignable = (v) => typeof v === "string" && SIGNABLE_COMMITMENT_MODES.includes(v);
|
|
400
453
|
function allowedCommitmentModes(profile) {
|
|
401
454
|
const declared = profile.commitment_modes;
|
|
@@ -404,7 +457,9 @@ function allowedCommitmentModes(profile) {
|
|
|
404
457
|
return SIGNABLE_COMMITMENT_MODES.filter((m) => declared.includes(m));
|
|
405
458
|
}
|
|
406
459
|
function isCommitmentModeAllowed(profile, mode) {
|
|
407
|
-
|
|
460
|
+
const allowed = allowedCommitmentModes(profile);
|
|
461
|
+
if (mode === "review_above_cap") return allowed.includes("automatic");
|
|
462
|
+
return isSignable(mode) && allowed.includes(mode);
|
|
408
463
|
}
|
|
409
464
|
|
|
410
465
|
// src/profile.ts
|
|
@@ -519,6 +574,7 @@ function validateProfile(profile) {
|
|
|
519
574
|
}
|
|
520
575
|
const seen = /* @__PURE__ */ new Set();
|
|
521
576
|
for (const m of declaredModes) {
|
|
577
|
+
if (m === "review_above_cap") continue;
|
|
522
578
|
if (!SIGNABLE_COMMITMENT_MODES.includes(m)) {
|
|
523
579
|
errors.push({
|
|
524
580
|
code: "PROFILE_INVALID",
|
|
@@ -540,6 +596,46 @@ function validateProfile(profile) {
|
|
|
540
596
|
message: "commitment_modes declares no mode this profile can actually use."
|
|
541
597
|
});
|
|
542
598
|
}
|
|
599
|
+
if (boundsSchema) {
|
|
600
|
+
const readFields = /* @__PURE__ */ new Set();
|
|
601
|
+
for (const [fieldName, fieldDef] of Object.entries(boundsSchema.fields)) {
|
|
602
|
+
const bt = fieldDef.boundType;
|
|
603
|
+
if (bt && (bt.kind === "per_transaction" || bt.kind === "cumulative_sum")) readFields.add(bt.of);
|
|
604
|
+
const max = fieldDef.maximum;
|
|
605
|
+
if (max !== void 0 && (typeof max !== "number" || !Number.isFinite(max))) {
|
|
606
|
+
errors.push({
|
|
607
|
+
code: "PROFILE_INVALID",
|
|
608
|
+
field: fieldName,
|
|
609
|
+
message: `Bounds field "${fieldName}": maximum MUST be a number (Bounds Schema rule 9).`
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
const enforcedBy = fieldDef.enforcedBy;
|
|
613
|
+
if (enforcedBy !== void 0 && enforcedBy !== "gatekeeper") {
|
|
614
|
+
errors.push({
|
|
615
|
+
code: "PROFILE_INVALID",
|
|
616
|
+
field: fieldName,
|
|
617
|
+
message: `Bounds field "${fieldName}": enforcedBy MUST be "gatekeeper" when present (Bounds Schema rule 11).`
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
for (const f of profile.disclose_fields ?? []) {
|
|
622
|
+
if (!readFields.has(f)) {
|
|
623
|
+
errors.push({
|
|
624
|
+
code: "PROFILE_INVALID",
|
|
625
|
+
field: "disclose_fields",
|
|
626
|
+
message: `disclose_fields names "${f}", which no bound reads \u2014 only what a ticket carries can be disclosed.`
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const binding = profile.content_binding;
|
|
632
|
+
if (binding && String(binding.version) === "2" && binding.kind === "text") {
|
|
633
|
+
errors.push({
|
|
634
|
+
code: "PROFILE_INVALID",
|
|
635
|
+
field: "content_binding",
|
|
636
|
+
message: 'A version-2 content_binding is defined for kind "jcs" only; kind "text" is invalid.'
|
|
637
|
+
});
|
|
638
|
+
}
|
|
543
639
|
return errors;
|
|
544
640
|
}
|
|
545
641
|
function checkDiscloseSubset(mandateDiscloseFields, profileDiscloseFields) {
|
|
@@ -706,7 +802,7 @@ function validateMandateOwners(payload) {
|
|
|
706
802
|
if (!Array.isArray(owners) || owners.length !== 1) {
|
|
707
803
|
throw new HapError(
|
|
708
804
|
"MALFORMED_MANDATE",
|
|
709
|
-
`mandate_owners MUST contain exactly one entry
|
|
805
|
+
`mandate_owners MUST contain exactly one entry (Mandate rule 7), got ${Array.isArray(owners) ? owners.length : typeof owners}`
|
|
710
806
|
);
|
|
711
807
|
}
|
|
712
808
|
const entry = owners[0];
|
|
@@ -725,17 +821,45 @@ function validateMandateOwners(payload) {
|
|
|
725
821
|
throw new HapError("OWNER_SIGNATURE_INVALID", `co-signing DID ${entry.did} is not key-bearing`);
|
|
726
822
|
}
|
|
727
823
|
}
|
|
824
|
+
function validateAboveCapFields(payload) {
|
|
825
|
+
const hasCaps = payload.above_cap_caps !== void 0;
|
|
826
|
+
const hasApprovers = payload.above_cap_approvers !== void 0;
|
|
827
|
+
const hasCount = payload.above_cap_approvals !== void 0;
|
|
828
|
+
if (hasCaps !== hasApprovers) {
|
|
829
|
+
throw new HapError("MALFORMED_MANDATE", "above_cap_caps and above_cap_approvers are present together or absent together");
|
|
830
|
+
}
|
|
831
|
+
if (payload.commitment_mode === "review_above_cap" && !hasApprovers) {
|
|
832
|
+
throw new HapError("MALFORMED_MANDATE", "a review_above_cap mandate MUST carry above_cap_caps and above_cap_approvers");
|
|
833
|
+
}
|
|
834
|
+
if (payload.version === "0.7") {
|
|
835
|
+
if (hasCount) {
|
|
836
|
+
throw new HapError("MALFORMED_MANDATE", "a v0.7 mandate carries no above_cap_approvals");
|
|
837
|
+
}
|
|
838
|
+
if (hasApprovers && payload.commitment_mode !== "review_above_cap") {
|
|
839
|
+
throw new HapError("MALFORMED_MANDATE", "a v0.7 mandate carries above-cap fields only under review_above_cap");
|
|
840
|
+
}
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (hasApprovers !== hasCount) {
|
|
844
|
+
throw new HapError("MALFORMED_MANDATE", "above_cap_approvals is present exactly when above_cap_approvers is (Mandate rule 10)");
|
|
845
|
+
}
|
|
846
|
+
if (hasCount) {
|
|
847
|
+
const n = payload.above_cap_approvals;
|
|
848
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 1) {
|
|
849
|
+
throw new HapError("MALFORMED_MANDATE", `above_cap_approvals MUST be an integer >= 1, got ${JSON.stringify(n)}`);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
if (hasApprovers && payload.commitment_mode === "automatic") {
|
|
853
|
+
throw new HapError("MALFORMED_MANDATE", "an automatic mandate carries no above-cap fields");
|
|
854
|
+
}
|
|
855
|
+
}
|
|
728
856
|
async function verifyMandate(blob, opts) {
|
|
729
857
|
const mandate = decodeMandateBlob(blob);
|
|
730
|
-
|
|
731
|
-
throw new HapError(
|
|
732
|
-
"VERSION_UNSUPPORTED",
|
|
733
|
-
`mandate version ${JSON.stringify(mandate.payload.version)} is not verifiable by this implementation (verifies ${PROTOCOL_VERSION} only \u2014 re-issue the mandate)`
|
|
734
|
-
);
|
|
735
|
-
}
|
|
858
|
+
assertSupportedVersion(mandate.payload.version, "mandate");
|
|
736
859
|
await verifyMandateSignature(mandate, opts);
|
|
737
860
|
checkMandateExpiry(mandate.payload, opts?.now);
|
|
738
861
|
validateMandateOwners(mandate.payload);
|
|
862
|
+
validateAboveCapFields(mandate.payload);
|
|
739
863
|
return mandate.payload;
|
|
740
864
|
}
|
|
741
865
|
|
|
@@ -772,9 +896,10 @@ function buildMandateProjection(payload, entry) {
|
|
|
772
896
|
"mandate projection requires profile_hash, bounds_hash, scope_hash and commitment_mode"
|
|
773
897
|
);
|
|
774
898
|
}
|
|
899
|
+
const version = payload.version === "0.7" ? "0.7" : "0.8";
|
|
775
900
|
const projection = {
|
|
776
901
|
typ: "HAP-mandate-projection",
|
|
777
|
-
version
|
|
902
|
+
version,
|
|
778
903
|
profile_id,
|
|
779
904
|
profile_hash,
|
|
780
905
|
owner_did: entry.did,
|
|
@@ -789,13 +914,32 @@ function buildMandateProjection(payload, entry) {
|
|
|
789
914
|
if (payload.intent_disclosure_hash !== void 0) {
|
|
790
915
|
projection.intent_disclosure_hash = payload.intent_disclosure_hash;
|
|
791
916
|
}
|
|
792
|
-
if (
|
|
793
|
-
|
|
794
|
-
|
|
917
|
+
if (version === "0.7") {
|
|
918
|
+
if (payload.commitment_mode === "review_above_cap") {
|
|
919
|
+
projection.above_cap_caps = payload.above_cap_caps;
|
|
920
|
+
projection.above_cap_approvers = payload.above_cap_approvers;
|
|
921
|
+
}
|
|
922
|
+
} else {
|
|
923
|
+
if (payload.above_cap_approvers !== void 0) {
|
|
924
|
+
projection.above_cap_caps = payload.above_cap_caps;
|
|
925
|
+
projection.above_cap_approvers = payload.above_cap_approvers;
|
|
926
|
+
projection.above_cap_approvals = payload.above_cap_approvals;
|
|
927
|
+
}
|
|
795
928
|
}
|
|
796
929
|
if (payload.disclose_fields !== void 0) {
|
|
797
930
|
projection.disclose_fields = payload.disclose_fields;
|
|
798
931
|
}
|
|
932
|
+
if (version === "0.8") {
|
|
933
|
+
if (entry.binding === void 0 || entry.signing_surface === void 0) {
|
|
934
|
+
throw new OwnerSignatureError(
|
|
935
|
+
"OWNER_SIGNATURE_INVALID",
|
|
936
|
+
"a v0.8 projection requires the entry's binding and signing_surface"
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
projection.binding = entry.binding;
|
|
940
|
+
projection.signing_surface = entry.signing_surface;
|
|
941
|
+
projection.subjects_disclosed = (payload.subjects ?? []).some((s) => s.did === entry.did);
|
|
942
|
+
}
|
|
799
943
|
return projection;
|
|
800
944
|
}
|
|
801
945
|
function projectionSigningBytes(projection) {
|
|
@@ -841,11 +985,14 @@ async function verifyOwnerSignatures(mandate) {
|
|
|
841
985
|
return entries;
|
|
842
986
|
}
|
|
843
987
|
async function verifyApproval(approval, signature, signerDid) {
|
|
988
|
+
if (approval.version === "0.8" && approval.approver_did !== signerDid) {
|
|
989
|
+
throw new OwnerSignatureError("APPROVAL_SIGNATURE_INVALID", `approval names approver ${approval.approver_did}, verified under ${signerDid}`);
|
|
990
|
+
}
|
|
844
991
|
let publicKey;
|
|
845
992
|
try {
|
|
846
993
|
publicKey = decodeDidKey(signerDid);
|
|
847
994
|
} catch (err) {
|
|
848
|
-
throw new OwnerSignatureError("
|
|
995
|
+
throw new OwnerSignatureError("APPROVAL_SIGNATURE_INVALID", err.message);
|
|
849
996
|
}
|
|
850
997
|
const ok = await ed2.verifyAsync(decodeOrThrow(signature, "APPROVAL_SIGNATURE_INVALID"), approvalSigningBytes(approval), publicKey).catch(() => false);
|
|
851
998
|
if (!ok) {
|
|
@@ -853,6 +1000,9 @@ async function verifyApproval(approval, signature, signerDid) {
|
|
|
853
1000
|
}
|
|
854
1001
|
}
|
|
855
1002
|
var BINDING_RANK = { raw: 0, webauthn: 1, eudi: 2 };
|
|
1003
|
+
function bindingRank(binding) {
|
|
1004
|
+
return BINDING_RANK[binding] ?? 0;
|
|
1005
|
+
}
|
|
856
1006
|
function checkOwnerSignatureRequirement(payload, floor) {
|
|
857
1007
|
if (!floor?.required) return;
|
|
858
1008
|
const entry = payload.mandate_owners[0];
|
|
@@ -864,7 +1014,7 @@ function checkOwnerSignatureRequirement(payload, floor) {
|
|
|
864
1014
|
);
|
|
865
1015
|
}
|
|
866
1016
|
const minRank = floor.minBinding ? BINDING_RANK[floor.minBinding] : 0;
|
|
867
|
-
const haveRank =
|
|
1017
|
+
const haveRank = bindingRank(entry.binding);
|
|
868
1018
|
if (haveRank < minRank) {
|
|
869
1019
|
throw new OwnerSignatureError(
|
|
870
1020
|
"OWNER_SIGNATURE_REQUIRED",
|
|
@@ -875,11 +1025,27 @@ function checkOwnerSignatureRequirement(payload, floor) {
|
|
|
875
1025
|
|
|
876
1026
|
// src/ticket.ts
|
|
877
1027
|
import * as ed3 from "@noble/ed25519";
|
|
1028
|
+
var TICKET_FRESHNESS_SECONDS = 300;
|
|
878
1029
|
async function signTicket(payload, privateKey) {
|
|
879
1030
|
const signature = toBase64Url(await ed3.signAsync(new TextEncoder().encode(canonicalize(payload)), privateKey));
|
|
880
1031
|
return { ...payload, signature };
|
|
881
1032
|
}
|
|
1033
|
+
function assertTicketShape(ticket) {
|
|
1034
|
+
if ("typ" in ticket) {
|
|
1035
|
+
throw new HapError("INVALID_SIGNATURE", "a ticket carries no typ \u2014 an object with one is a projection, not a ticket");
|
|
1036
|
+
}
|
|
1037
|
+
if ("mandate_id" in ticket || "issued_at" in ticket) {
|
|
1038
|
+
throw new HapError("INVALID_SIGNATURE", "a ticket carries no mandate_id or issued_at \u2014 this object is a mandate payload");
|
|
1039
|
+
}
|
|
1040
|
+
for (const required of ["id", "mandateId", "actionType", "timestamp", "issuer"]) {
|
|
1041
|
+
if (ticket[required] === void 0) {
|
|
1042
|
+
throw new HapError("INVALID_SIGNATURE", `ticket is missing required field ${required}`);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
882
1046
|
async function verifyTicketSignature(ticket, opts) {
|
|
1047
|
+
assertTicketShape(ticket);
|
|
1048
|
+
assertSupportedVersion(ticket.version, "ticket");
|
|
883
1049
|
const { signature, ...unsigned } = ticket;
|
|
884
1050
|
if (!signature) {
|
|
885
1051
|
throw new HapError("INVALID_SIGNATURE", "ticket carries no signature");
|
|
@@ -905,6 +1071,120 @@ async function verifyTicketSignature(ticket, opts) {
|
|
|
905
1071
|
throw new HapError("INVALID_SIGNATURE", "ticket signature verification failed");
|
|
906
1072
|
}
|
|
907
1073
|
}
|
|
1074
|
+
var ECHOED_FIELDS = [
|
|
1075
|
+
"profileId",
|
|
1076
|
+
"actionType",
|
|
1077
|
+
"boundsHash",
|
|
1078
|
+
"executionContext",
|
|
1079
|
+
"contentHash",
|
|
1080
|
+
"contentBinding",
|
|
1081
|
+
"proposalId",
|
|
1082
|
+
"proposalContentHash"
|
|
1083
|
+
];
|
|
1084
|
+
function verifyTicketAgainstRequest(ticket, request, opts = {}) {
|
|
1085
|
+
for (const field of ECHOED_FIELDS) {
|
|
1086
|
+
const sent = request[field];
|
|
1087
|
+
const got = ticket[field];
|
|
1088
|
+
if (sent === void 0 && got === void 0) continue;
|
|
1089
|
+
if (sent === void 0 || got === void 0 || canonicalize(sent) !== canonicalize(got)) {
|
|
1090
|
+
throw new HapError("TICKET_REQUEST_MISMATCH", `ticket ${field} differs from the request's`);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (request.idempotencyKey !== void 0 && ticket.idempotencyKey !== request.idempotencyKey) {
|
|
1094
|
+
throw new HapError(
|
|
1095
|
+
"TICKET_REQUEST_MISMATCH",
|
|
1096
|
+
ticket.idempotencyKey === void 0 ? "ticket carries no idempotencyKey although the request sent one" : "ticket idempotencyKey differs from the one the request sent"
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
const expectedMandateId = opts.mandate?.payload.mandate_id ?? request.mandateId;
|
|
1100
|
+
if (expectedMandateId !== void 0 && ticket.mandateId !== expectedMandateId) {
|
|
1101
|
+
throw new HapError("TICKET_REQUEST_MISMATCH", `ticket mandateId ${ticket.mandateId} is not the mandate this request was made under`);
|
|
1102
|
+
}
|
|
1103
|
+
if (opts.mandate && ticket.version !== opts.mandate.payload.version) {
|
|
1104
|
+
throw new HapError("TICKET_REQUEST_MISMATCH", `ticket version ${ticket.version} differs from its mandate's ${opts.mandate.payload.version}`);
|
|
1105
|
+
}
|
|
1106
|
+
if (!opts.replay) {
|
|
1107
|
+
const now = opts.now ?? Math.floor(Date.now() / 1e3);
|
|
1108
|
+
if (Math.abs(now - ticket.timestamp) > TICKET_FRESHNESS_SECONDS) {
|
|
1109
|
+
throw new HapError("TICKET_REQUEST_MISMATCH", `ticket timestamp ${ticket.timestamp} is outside the freshness window at ${now}`);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
function reconstructApproval(ticket, entry, approverDid) {
|
|
1114
|
+
if (!ticket.proposalId) {
|
|
1115
|
+
throw new HapError("APPROVAL_SIGNATURE_INVALID", "ticket carries an approval signature but no proposalId");
|
|
1116
|
+
}
|
|
1117
|
+
if (ticket.version === "0.7") {
|
|
1118
|
+
if (!ticket.contentHash) {
|
|
1119
|
+
throw new HapError("APPROVAL_SIGNATURE_INVALID", "a v0.7 approval without a content binding is not reconstructible from the ticket alone");
|
|
1120
|
+
}
|
|
1121
|
+
return {
|
|
1122
|
+
typ: "HAP-approval",
|
|
1123
|
+
version: "0.7",
|
|
1124
|
+
proposal_id: ticket.proposalId,
|
|
1125
|
+
mandate_id: ticket.mandateId,
|
|
1126
|
+
decision: "commit",
|
|
1127
|
+
content_hash: ticket.contentHash,
|
|
1128
|
+
decided_at: entry.decided_at,
|
|
1129
|
+
nonce: entry.nonce
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
if (!ticket.proposalContentHash) {
|
|
1133
|
+
throw new HapError("APPROVAL_SIGNATURE_INVALID", "a v0.8 review-path ticket carries proposalContentHash");
|
|
1134
|
+
}
|
|
1135
|
+
return {
|
|
1136
|
+
typ: "HAP-approval",
|
|
1137
|
+
version: "0.8",
|
|
1138
|
+
approver_did: approverDid,
|
|
1139
|
+
proposal_id: ticket.proposalId,
|
|
1140
|
+
mandate_id: ticket.mandateId,
|
|
1141
|
+
action: ticket.action,
|
|
1142
|
+
action_type: ticket.actionType,
|
|
1143
|
+
decision: "commit",
|
|
1144
|
+
content_hash: ticket.contentHash ?? ticket.proposalContentHash,
|
|
1145
|
+
proposal_content_hash: ticket.proposalContentHash,
|
|
1146
|
+
decided_at: entry.decided_at,
|
|
1147
|
+
nonce: entry.nonce
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
async function verifyTicketApprovals(ticket, mandate) {
|
|
1151
|
+
if (ticket.mandateId !== mandate.payload.mandate_id) {
|
|
1152
|
+
throw new HapError("APPROVAL_SIGNATURE_INVALID", "the mandate supplied is not the one the ticket names");
|
|
1153
|
+
}
|
|
1154
|
+
const ownerDid = mandate.payload.mandate_owners[0]?.did;
|
|
1155
|
+
const result = { approvers: [] };
|
|
1156
|
+
try {
|
|
1157
|
+
if (ticket.approvalSignature) {
|
|
1158
|
+
const approval = reconstructApproval(ticket, ticket.approvalSignature, ownerDid);
|
|
1159
|
+
await verifyApproval(approval, ticket.approvalSignature.signature, ownerDid);
|
|
1160
|
+
result.owner = ownerDid;
|
|
1161
|
+
}
|
|
1162
|
+
const signed = new Set(mandate.payload.above_cap_approvers ?? []);
|
|
1163
|
+
for (const entry of ticket.approvalSignatures ?? []) {
|
|
1164
|
+
if (!signed.has(entry.approver_did)) {
|
|
1165
|
+
throw new HapError("APPROVAL_SIGNATURE_INVALID", `approver ${entry.approver_did} is not in the mandate's signed above_cap_approvers`);
|
|
1166
|
+
}
|
|
1167
|
+
if (entry.approver_did === ownerDid) {
|
|
1168
|
+
throw new HapError("APPROVAL_SIGNATURE_INVALID", "the proposer never counts as an above-cap approver");
|
|
1169
|
+
}
|
|
1170
|
+
const approval = reconstructApproval(ticket, entry, entry.approver_did);
|
|
1171
|
+
await verifyApproval(approval, entry.signature, entry.approver_did);
|
|
1172
|
+
if (!result.approvers.includes(entry.approver_did)) result.approvers.push(entry.approver_did);
|
|
1173
|
+
}
|
|
1174
|
+
} catch (err) {
|
|
1175
|
+
if (err instanceof OwnerSignatureError) throw new HapError(err.code, err.message);
|
|
1176
|
+
throw err;
|
|
1177
|
+
}
|
|
1178
|
+
return result;
|
|
1179
|
+
}
|
|
1180
|
+
function approvalsSufficient(mandate, verified, aboveCap) {
|
|
1181
|
+
const p = mandate.payload;
|
|
1182
|
+
const ownerNeeded = p.commitment_mode === "review";
|
|
1183
|
+
if (ownerNeeded && !verified.owner) return false;
|
|
1184
|
+
if (!aboveCap) return true;
|
|
1185
|
+
const required = p.version === "0.7" ? (p.above_cap_approvers ?? []).length : p.above_cap_approvals ?? 1;
|
|
1186
|
+
return verified.approvers.length >= required;
|
|
1187
|
+
}
|
|
908
1188
|
function publicTicketView(ticket, governingDiscloseFields = []) {
|
|
909
1189
|
const { userId: _userId, cumulativeState: _cumulativeState, limits: _limits, executionContext, ...rest } = ticket;
|
|
910
1190
|
const allowed = new Set(governingDiscloseFields);
|
|
@@ -915,6 +1195,475 @@ function publicTicketView(ticket, governingDiscloseFields = []) {
|
|
|
915
1195
|
return { ...rest, executionContext: disclosedExecutionContext };
|
|
916
1196
|
}
|
|
917
1197
|
|
|
1198
|
+
// src/ticket-projection.ts
|
|
1199
|
+
import * as ed4 from "@noble/ed25519";
|
|
1200
|
+
var PROJECTION_MAX_VALIDITY_SECONDS = 86400;
|
|
1201
|
+
function buildTicketProjection(ticket, mandate, opts) {
|
|
1202
|
+
if (ticket.mandateId !== mandate.payload.mandate_id) {
|
|
1203
|
+
throw new HapError("MALFORMED_TICKET_REQUEST", "the mandate supplied is not the one the ticket names");
|
|
1204
|
+
}
|
|
1205
|
+
if (opts.validUntil > ticket.timestamp + PROJECTION_MAX_VALIDITY_SECONDS) {
|
|
1206
|
+
throw new HapError("MALFORMED_TICKET_REQUEST", `valid_until MUST NOT end later than ${PROJECTION_MAX_VALIDITY_SECONDS}s after the ticket's timestamp`);
|
|
1207
|
+
}
|
|
1208
|
+
const projection = {
|
|
1209
|
+
typ: "HAP-ticket-projection",
|
|
1210
|
+
version: ticket.version,
|
|
1211
|
+
id: ticket.id,
|
|
1212
|
+
mandateId: ticket.mandateId,
|
|
1213
|
+
issuer: ticket.issuer,
|
|
1214
|
+
timestamp: ticket.timestamp,
|
|
1215
|
+
valid_until: opts.validUntil,
|
|
1216
|
+
profileId: ticket.profileId,
|
|
1217
|
+
actionType: ticket.actionType,
|
|
1218
|
+
boundsHash: ticket.boundsHash
|
|
1219
|
+
};
|
|
1220
|
+
if (ticket.contentHash !== void 0) projection.contentHash = ticket.contentHash;
|
|
1221
|
+
if (ticket.contentBinding !== void 0) projection.contentBinding = ticket.contentBinding;
|
|
1222
|
+
if (ticket.subjects !== void 0) projection.subjects = ticket.subjects;
|
|
1223
|
+
const disclosed = {};
|
|
1224
|
+
for (const field of opts.discloseFields ?? []) {
|
|
1225
|
+
if (ticket.executionContext[field] !== void 0) disclosed[field] = ticket.executionContext[field];
|
|
1226
|
+
}
|
|
1227
|
+
if (Object.keys(disclosed).length > 0) projection.executionContext = disclosed;
|
|
1228
|
+
const owner = mandate.payload.mandate_owners[0];
|
|
1229
|
+
if (owner?.signature !== void 0) projection.owner = owner;
|
|
1230
|
+
return projection;
|
|
1231
|
+
}
|
|
1232
|
+
async function signTicketProjection(projection, privateKey) {
|
|
1233
|
+
const signature = toBase64Url(await ed4.signAsync(new TextEncoder().encode(canonicalize(projection)), privateKey));
|
|
1234
|
+
return { ...projection, signature };
|
|
1235
|
+
}
|
|
1236
|
+
async function verifyTicketProjection(projection, opts = {}) {
|
|
1237
|
+
if (projection.typ !== "HAP-ticket-projection") {
|
|
1238
|
+
throw new HapError("INVALID_SIGNATURE", 'a projection verifier requires typ "HAP-ticket-projection"');
|
|
1239
|
+
}
|
|
1240
|
+
for (const k of ["userId", "cumulativeState", "limits", "action"]) {
|
|
1241
|
+
if (k in projection) throw new HapError("INVALID_SIGNATURE", `a projection MUST NOT carry ${k}`);
|
|
1242
|
+
}
|
|
1243
|
+
if (opts.trustedIssuers && !opts.trustedIssuers.includes(projection.issuer)) {
|
|
1244
|
+
throw new HapError("INVALID_SIGNATURE", `issuer ${JSON.stringify(projection.issuer)} is not a trusted Authority Server`);
|
|
1245
|
+
}
|
|
1246
|
+
const { signature, ...unsigned } = projection;
|
|
1247
|
+
if (!signature) throw new HapError("INVALID_SIGNATURE", "projection carries no signature");
|
|
1248
|
+
let publicKey;
|
|
1249
|
+
try {
|
|
1250
|
+
publicKey = decodeDidKey(projection.issuer);
|
|
1251
|
+
} catch (err) {
|
|
1252
|
+
throw new HapError("INVALID_SIGNATURE", `cannot resolve a key from issuer ${JSON.stringify(projection.issuer)}: ${err.message}`);
|
|
1253
|
+
}
|
|
1254
|
+
let sigBytes;
|
|
1255
|
+
try {
|
|
1256
|
+
sigBytes = fromBase64Url(signature);
|
|
1257
|
+
} catch (err) {
|
|
1258
|
+
throw new HapError("INVALID_SIGNATURE", err.message);
|
|
1259
|
+
}
|
|
1260
|
+
const ok = await ed4.verifyAsync(sigBytes, new TextEncoder().encode(canonicalize(unsigned)), publicKey).catch(() => false);
|
|
1261
|
+
if (!ok) throw new HapError("INVALID_SIGNATURE", "projection signature verification failed");
|
|
1262
|
+
if (opts.asAuthorization) {
|
|
1263
|
+
const now = opts.now ?? Math.floor(Date.now() / 1e3);
|
|
1264
|
+
if (now > projection.valid_until) {
|
|
1265
|
+
throw new HapError("POLICY_UNMET", `projection presented after valid_until ${projection.valid_until}`);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
async function verifyProjectionOwner(projection, mandate) {
|
|
1270
|
+
if (!projection.owner) {
|
|
1271
|
+
throw new HapError("OWNER_SIGNATURE_INVALID", "projection carries no owner entry");
|
|
1272
|
+
}
|
|
1273
|
+
if (projection.mandateId !== mandate.payload.mandate_id) {
|
|
1274
|
+
throw new HapError("OWNER_SIGNATURE_INVALID", "the mandate supplied is not the one the projection names");
|
|
1275
|
+
}
|
|
1276
|
+
const own = mandate.payload.mandate_owners[0];
|
|
1277
|
+
if (!own || canonicalize(own) !== canonicalize(projection.owner)) {
|
|
1278
|
+
throw new HapError("OWNER_SIGNATURE_INVALID", "projection owner entry differs from the mandate's mandate_owners entry");
|
|
1279
|
+
}
|
|
1280
|
+
try {
|
|
1281
|
+
await verifyOwnerSignature(mandate, projection.owner);
|
|
1282
|
+
} catch (err) {
|
|
1283
|
+
if (err instanceof OwnerSignatureError) throw new HapError(err.code, err.message);
|
|
1284
|
+
throw err;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// src/verifier-policy.ts
|
|
1289
|
+
var PolicyError = class extends HapError {
|
|
1290
|
+
constructor(line, code, message) {
|
|
1291
|
+
super(code, `${line}: ${message}`);
|
|
1292
|
+
this.line = line;
|
|
1293
|
+
this.name = "PolicyError";
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
async function evaluateVerifierPolicy(policy, input) {
|
|
1297
|
+
if (policy.typ !== "HAP-verifier-policy") {
|
|
1298
|
+
throw new HapError("POLICY_UNMET", 'policy document must carry typ "HAP-verifier-policy"');
|
|
1299
|
+
}
|
|
1300
|
+
const artifact = input.ticket ?? input.projection;
|
|
1301
|
+
if (!artifact) throw new HapError("POLICY_UNMET", "no ticket or projection to evaluate");
|
|
1302
|
+
const now = input.now ?? Math.floor(Date.now() / 1e3);
|
|
1303
|
+
try {
|
|
1304
|
+
if (input.ticket) {
|
|
1305
|
+
await verifyTicketSignature(input.ticket, { trustedIssuers: policy.trusted_issuers });
|
|
1306
|
+
} else if (input.projection) {
|
|
1307
|
+
await verifyTicketProjection(input.projection, { trustedIssuers: policy.trusted_issuers, now, asAuthorization: input.asAuthorization });
|
|
1308
|
+
}
|
|
1309
|
+
if (input.mandate) {
|
|
1310
|
+
await verifyMandateSignature(input.mandate, { trustedIssuers: policy.trusted_issuers });
|
|
1311
|
+
}
|
|
1312
|
+
} catch (err) {
|
|
1313
|
+
const code = err instanceof HapError ? err.code : "INVALID_SIGNATURE";
|
|
1314
|
+
throw new PolicyError(code === "POLICY_UNMET" ? "freshness_seconds" : "trusted_issuers", code, err.message);
|
|
1315
|
+
}
|
|
1316
|
+
let verifiedOwnerDids;
|
|
1317
|
+
const needOwners = policy.min_binding !== void 0 || policy.required_owners !== void 0;
|
|
1318
|
+
if (needOwners) {
|
|
1319
|
+
verifiedOwnerDids = /* @__PURE__ */ new Map();
|
|
1320
|
+
if (input.mandate) {
|
|
1321
|
+
try {
|
|
1322
|
+
const entries = await verifyOwnerSignatures(input.mandate);
|
|
1323
|
+
for (const e of entries) verifiedOwnerDids.set(e.did, e.binding ?? "raw");
|
|
1324
|
+
if (input.projection?.owner) await verifyProjectionOwner(input.projection, input.mandate);
|
|
1325
|
+
} catch (err) {
|
|
1326
|
+
throw new PolicyError("min_binding", "OWNER_SIGNATURE_REQUIRED", `no verifiable owner signature: ${err.message}`);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
if (policy.min_binding !== void 0) {
|
|
1331
|
+
const minRank = BINDING_RANK[policy.min_binding];
|
|
1332
|
+
if (minRank === void 0) {
|
|
1333
|
+
throw new PolicyError("min_binding", "OWNER_SIGNATURE_REQUIRED", `unknown binding "${policy.min_binding}" \u2014 satisfies no minimum`);
|
|
1334
|
+
}
|
|
1335
|
+
const best = Math.max(-1, ...[...verifiedOwnerDids.values()].map(bindingRank));
|
|
1336
|
+
if (best < minRank) {
|
|
1337
|
+
throw new PolicyError("min_binding", "OWNER_SIGNATURE_REQUIRED", `no verified owner signature at binding >= "${policy.min_binding}"`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
if (policy.required_owners !== void 0) {
|
|
1341
|
+
for (const did of policy.required_owners) {
|
|
1342
|
+
if (!verifiedOwnerDids.has(did)) {
|
|
1343
|
+
throw new PolicyError("required_owners", "OWNER_SIGNATURE_REQUIRED", `no verified owner signature by ${did}`);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
if (policy.profile_hashes !== void 0) {
|
|
1348
|
+
const mandate = input.mandate;
|
|
1349
|
+
if (!mandate) throw new PolicyError("profile_hashes", "PROFILE_HASH_MISMATCH", "the mandate is needed to check profile_hashes");
|
|
1350
|
+
const expected = policy.profile_hashes[mandate.payload.profile_id];
|
|
1351
|
+
if (expected === void 0) {
|
|
1352
|
+
throw new PolicyError("profile_hashes", "PROFILE_HASH_MISMATCH", `profile ${mandate.payload.profile_id} is not listed`);
|
|
1353
|
+
}
|
|
1354
|
+
if (expected !== mandate.payload.profile_hash) {
|
|
1355
|
+
throw new PolicyError("profile_hashes", "PROFILE_HASH_MISMATCH", `profile_hash differs from the listed one for ${mandate.payload.profile_id}`);
|
|
1356
|
+
}
|
|
1357
|
+
if (mandate.payload.mandate_id !== artifact.mandateId) {
|
|
1358
|
+
throw new PolicyError("profile_hashes", "PROFILE_HASH_MISMATCH", "the mandate supplied is not the one the artifact names");
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
if (policy.action_types !== void 0 && !policy.action_types.includes(artifact.actionType)) {
|
|
1362
|
+
throw new PolicyError("action_types", "POLICY_UNMET", `actionType ${artifact.actionType} is not in the listed set`);
|
|
1363
|
+
}
|
|
1364
|
+
if (policy.freshness_seconds !== void 0) {
|
|
1365
|
+
if (now - artifact.timestamp > policy.freshness_seconds) {
|
|
1366
|
+
throw new PolicyError("freshness_seconds", "POLICY_UNMET", `ticket is ${now - artifact.timestamp}s old, policy allows ${policy.freshness_seconds}s`);
|
|
1367
|
+
}
|
|
1368
|
+
if (input.projection && input.asAuthorization && now > input.projection.valid_until) {
|
|
1369
|
+
throw new PolicyError("freshness_seconds", "POLICY_UNMET", "projection presented after valid_until");
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
if (policy.required_fields !== void 0) {
|
|
1373
|
+
const ctx = input.ticket?.executionContext ?? input.projection?.executionContext ?? {};
|
|
1374
|
+
for (const f of policy.required_fields) {
|
|
1375
|
+
if (ctx[f] === void 0) {
|
|
1376
|
+
throw new PolicyError("required_fields", "POLICY_UNMET", `execution-context field ${f} is not ${input.ticket ? "present" : "disclosed"}`);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
if (policy.require_approval_signature) {
|
|
1381
|
+
const ticket = input.ticket;
|
|
1382
|
+
const mandate = input.mandate;
|
|
1383
|
+
if (!ticket || !mandate) {
|
|
1384
|
+
throw new PolicyError("require_approval_signature", "APPROVAL_SIGNATURE_REQUIRED", "the complete ticket and its mandate are needed to verify approvals");
|
|
1385
|
+
}
|
|
1386
|
+
if (ticket.proposalId !== void 0) {
|
|
1387
|
+
let verified;
|
|
1388
|
+
try {
|
|
1389
|
+
verified = await verifyTicketApprovals(ticket, mandate);
|
|
1390
|
+
} catch (err) {
|
|
1391
|
+
throw new PolicyError("require_approval_signature", "APPROVAL_SIGNATURE_REQUIRED", err.message);
|
|
1392
|
+
}
|
|
1393
|
+
const aboveCap = input.aboveCap ?? (ticket.approvalSignatures !== void 0 && ticket.approvalSignatures.length > 0);
|
|
1394
|
+
if (!approvalsSufficient(mandate, verified, aboveCap)) {
|
|
1395
|
+
throw new PolicyError("require_approval_signature", "APPROVAL_SIGNATURE_REQUIRED", "the ticket lacks a verifiable signature for each decision it rests on");
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// src/proposal-content.ts
|
|
1402
|
+
import { createHash as createHash6 } from "crypto";
|
|
1403
|
+
import { CipherSuite, DhkemX25519HkdfSha256, HkdfSha256, Aes256Gcm } from "@hpke/core";
|
|
1404
|
+
function suite() {
|
|
1405
|
+
return new CipherSuite({ kem: new DhkemX25519HkdfSha256(), kdf: new HkdfSha256(), aead: new Aes256Gcm() });
|
|
1406
|
+
}
|
|
1407
|
+
var webCrypto = () => {
|
|
1408
|
+
const c = globalThis.crypto;
|
|
1409
|
+
if (!c?.subtle) throw new Error("WebCrypto is not available in this runtime");
|
|
1410
|
+
return c;
|
|
1411
|
+
};
|
|
1412
|
+
function proposalContentJcs(args) {
|
|
1413
|
+
if (args === null || typeof args !== "object" || Array.isArray(args)) {
|
|
1414
|
+
throw new HapError("PROPOSAL_NOT_ENCRYPTED", "proposal content is one object \u2014 the action's argument object");
|
|
1415
|
+
}
|
|
1416
|
+
return canonicalize(args);
|
|
1417
|
+
}
|
|
1418
|
+
function computeProposalContentHash(args) {
|
|
1419
|
+
return `sha256:${createHash6("sha256").update(proposalContentJcs(args), "utf8").digest("hex")}`;
|
|
1420
|
+
}
|
|
1421
|
+
async function generateReaderKeyPair() {
|
|
1422
|
+
const kp = await suite().kem.generateKeyPair();
|
|
1423
|
+
return exportKeyPair(kp);
|
|
1424
|
+
}
|
|
1425
|
+
async function deriveReaderKeyPair(ikm) {
|
|
1426
|
+
const kp = await suite().kem.deriveKeyPair(ikm);
|
|
1427
|
+
return exportKeyPair(kp);
|
|
1428
|
+
}
|
|
1429
|
+
async function exportKeyPair(kp) {
|
|
1430
|
+
const s = suite();
|
|
1431
|
+
return {
|
|
1432
|
+
privateKey: new Uint8Array(await s.kem.serializePrivateKey(kp.privateKey)),
|
|
1433
|
+
publicKey: new Uint8Array(await s.kem.serializePublicKey(kp.publicKey))
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
async function encryptProposalContent(args, readers, opts = {}) {
|
|
1437
|
+
if (readers.length === 0) {
|
|
1438
|
+
throw new HapError("PROPOSAL_NOT_ENCRYPTED", "a proposal has at least one reader (the Mandate Owner)");
|
|
1439
|
+
}
|
|
1440
|
+
const jcs = proposalContentJcs(args);
|
|
1441
|
+
const proposalContentHash = computeProposalContentHash(args);
|
|
1442
|
+
const crypto = webCrypto();
|
|
1443
|
+
const s = suite();
|
|
1444
|
+
const cek = opts.deterministic?.contentKey ?? crypto.getRandomValues(new Uint8Array(32));
|
|
1445
|
+
const iv = opts.deterministic?.iv ?? crypto.getRandomValues(new Uint8Array(12));
|
|
1446
|
+
if (cek.length !== 32 || iv.length !== 12) {
|
|
1447
|
+
throw new HapError("PROPOSAL_NOT_ENCRYPTED", "content key is 32 bytes and IV is 12 bytes");
|
|
1448
|
+
}
|
|
1449
|
+
const aesKey = await crypto.subtle.importKey("raw", cek, { name: "AES-GCM", length: 256 }, false, ["encrypt"]);
|
|
1450
|
+
const sealed = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, aesKey, new TextEncoder().encode(jcs)));
|
|
1451
|
+
const ciphertext = new Uint8Array(iv.length + sealed.length);
|
|
1452
|
+
ciphertext.set(iv, 0);
|
|
1453
|
+
ciphertext.set(sealed, iv.length);
|
|
1454
|
+
const encrypted_keys = {};
|
|
1455
|
+
for (const { did, publicKey } of readers) {
|
|
1456
|
+
const recipientPublicKey = await s.kem.deserializePublicKey(publicKey);
|
|
1457
|
+
const ekmIkm = opts.deterministic?.ephemeralIkm[did];
|
|
1458
|
+
const sender = await s.createSenderContext({
|
|
1459
|
+
recipientPublicKey,
|
|
1460
|
+
...ekmIkm ? { ekm: await s.kem.deriveKeyPair(ekmIkm) } : {}
|
|
1461
|
+
});
|
|
1462
|
+
const ct = new Uint8Array(await sender.seal(cek));
|
|
1463
|
+
encrypted_keys[did] = { ct: toBase64Url(ct), enc: toBase64Url(new Uint8Array(sender.enc)) };
|
|
1464
|
+
}
|
|
1465
|
+
return {
|
|
1466
|
+
content: {
|
|
1467
|
+
ciphertext: toBase64Url(ciphertext),
|
|
1468
|
+
encrypted_keys,
|
|
1469
|
+
readers: readers.map((r) => r.did),
|
|
1470
|
+
unreadable: [...opts.unreadable ?? []]
|
|
1471
|
+
},
|
|
1472
|
+
proposalContentHash
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
async function decryptProposalContent(content, readerDid, readerPrivateKey) {
|
|
1476
|
+
const wrap = content.encrypted_keys[readerDid];
|
|
1477
|
+
if (!wrap) throw new HapError("PROPOSAL_NOT_ENCRYPTED", `no key wrapped for reader ${readerDid}`);
|
|
1478
|
+
const s = suite();
|
|
1479
|
+
const crypto = webCrypto();
|
|
1480
|
+
let cek;
|
|
1481
|
+
try {
|
|
1482
|
+
const recipient = await s.createRecipientContext({
|
|
1483
|
+
recipientKey: await s.kem.deserializePrivateKey(readerPrivateKey),
|
|
1484
|
+
enc: fromBase64Url(wrap.enc)
|
|
1485
|
+
});
|
|
1486
|
+
cek = new Uint8Array(await recipient.open(fromBase64Url(wrap.ct)));
|
|
1487
|
+
} catch (err) {
|
|
1488
|
+
throw new HapError("PROPOSAL_NOT_ENCRYPTED", `proposal content is unreadable for ${readerDid}: ${err.message}`);
|
|
1489
|
+
}
|
|
1490
|
+
const bytes = fromBase64Url(content.ciphertext);
|
|
1491
|
+
if (bytes.length < 12 + 16) throw new HapError("PROPOSAL_NOT_ENCRYPTED", "ciphertext is shorter than IV + tag");
|
|
1492
|
+
let plaintext;
|
|
1493
|
+
try {
|
|
1494
|
+
const aesKey = await crypto.subtle.importKey("raw", cek, { name: "AES-GCM", length: 256 }, false, ["decrypt"]);
|
|
1495
|
+
plaintext = new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: bytes.subarray(0, 12) }, aesKey, bytes.subarray(12)));
|
|
1496
|
+
} catch (err) {
|
|
1497
|
+
throw new HapError("PROPOSAL_NOT_ENCRYPTED", `proposal content is unreadable for ${readerDid}: ${err.message}`);
|
|
1498
|
+
}
|
|
1499
|
+
const contentJcs = new TextDecoder().decode(plaintext);
|
|
1500
|
+
let args;
|
|
1501
|
+
try {
|
|
1502
|
+
args = JSON.parse(contentJcs);
|
|
1503
|
+
} catch {
|
|
1504
|
+
throw new HapError("PROPOSAL_NOT_ENCRYPTED", "decrypted content is not JSON");
|
|
1505
|
+
}
|
|
1506
|
+
return { arguments: args, contentJcs, proposalContentHash: computeProposalContentHash(args) };
|
|
1507
|
+
}
|
|
1508
|
+
function checkProposalReaders(content, expected) {
|
|
1509
|
+
const want = /* @__PURE__ */ new Set([expected.ownerDid, ...expected.deciders]);
|
|
1510
|
+
const have = /* @__PURE__ */ new Set([...content.readers, ...content.unreadable]);
|
|
1511
|
+
if (want.size !== have.size || [...want].some((d) => !have.has(d))) {
|
|
1512
|
+
throw new HapError("PROPOSAL_APPROVERS_INVALID", "readers \u222A unreadable differs from the owner plus the signed deciders");
|
|
1513
|
+
}
|
|
1514
|
+
const keyed = new Set(Object.keys(content.encrypted_keys));
|
|
1515
|
+
const readers = new Set(content.readers);
|
|
1516
|
+
if (keyed.size !== readers.size || [...readers].some((d) => !keyed.has(d))) {
|
|
1517
|
+
throw new HapError("PROPOSAL_NOT_ENCRYPTED", "readers differs from the key set of encrypted_keys");
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// src/issuance.ts
|
|
1522
|
+
import { createHash as createHash7 } from "crypto";
|
|
1523
|
+
function computeExecutionContextHash(profile) {
|
|
1524
|
+
const schema = profile.executionContextSchema ?? {};
|
|
1525
|
+
return `sha256:${createHash7("sha256").update(canonicalize(schema), "utf8").digest("hex")}`;
|
|
1526
|
+
}
|
|
1527
|
+
function boundReadFields(profile) {
|
|
1528
|
+
const fields = /* @__PURE__ */ new Set();
|
|
1529
|
+
for (const def of Object.values(profile.boundsSchema?.fields ?? {})) {
|
|
1530
|
+
const bt = def.boundType;
|
|
1531
|
+
if (bt && (bt.kind === "per_transaction" || bt.kind === "cumulative_sum")) fields.add(bt.of);
|
|
1532
|
+
}
|
|
1533
|
+
return fields;
|
|
1534
|
+
}
|
|
1535
|
+
function wireExecutionContext(execution, profile) {
|
|
1536
|
+
const allowed = boundReadFields(profile);
|
|
1537
|
+
const out = {};
|
|
1538
|
+
for (const [k, v] of Object.entries(execution)) {
|
|
1539
|
+
if (!allowed.has(k) || k.startsWith("_") || v === void 0 || v === null) continue;
|
|
1540
|
+
out[k] = v;
|
|
1541
|
+
}
|
|
1542
|
+
return out;
|
|
1543
|
+
}
|
|
1544
|
+
function checkExecutionValues(execution, profile) {
|
|
1545
|
+
for (const k of Object.keys(execution)) {
|
|
1546
|
+
if (k.startsWith("_")) {
|
|
1547
|
+
throw new HapError("MALFORMED_TICKET_REQUEST", `executionContext key "${k}" is reserved and MUST NOT travel`);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
for (const field of boundReadFields(profile)) {
|
|
1551
|
+
const v = execution[field];
|
|
1552
|
+
if (v === void 0 || v === null) continue;
|
|
1553
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 0) {
|
|
1554
|
+
throw new HapError(
|
|
1555
|
+
"MALFORMED_TICKET_REQUEST",
|
|
1556
|
+
`executionContext.${field} MUST be a finite, non-negative number, got ${JSON.stringify(v)}`
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
function boundGoverns(def, actionType) {
|
|
1562
|
+
return !def.appliesTo || def.appliesTo.includes(actionType);
|
|
1563
|
+
}
|
|
1564
|
+
function missingSumFields(execution, profile, actionType) {
|
|
1565
|
+
const missing = [];
|
|
1566
|
+
for (const [name, def] of Object.entries(profile.boundsSchema?.fields ?? {})) {
|
|
1567
|
+
const bt = def.boundType;
|
|
1568
|
+
if (!bt || bt.kind !== "cumulative_sum" || !boundGoverns(def, actionType)) continue;
|
|
1569
|
+
const v = execution[bt.of];
|
|
1570
|
+
if (v === void 0 || v === null) missing.push({ bound: name, field: bt.of });
|
|
1571
|
+
}
|
|
1572
|
+
return missing;
|
|
1573
|
+
}
|
|
1574
|
+
function checkBoundsValues(bounds, profile) {
|
|
1575
|
+
for (const [name, def] of Object.entries(profile.boundsSchema?.fields ?? {})) {
|
|
1576
|
+
if (name === "profile") continue;
|
|
1577
|
+
const v = bounds[name];
|
|
1578
|
+
if (v === void 0 || v === null) continue;
|
|
1579
|
+
if (def.type === "number" || def.maximum !== void 0) {
|
|
1580
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
1581
|
+
throw new HapError("BOUNDS_INVALID_VALUE", `bound "${name}" MUST be a number, got ${JSON.stringify(v)}`);
|
|
1582
|
+
}
|
|
1583
|
+
if (def.maximum !== void 0 && v > def.maximum) {
|
|
1584
|
+
throw new HapError("BOUNDS_INVALID_VALUE", `bound "${name}" = ${v} exceeds the profile's maximum ${def.maximum}`);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
const bt = def.boundType;
|
|
1588
|
+
if (bt?.kind === "enum" && !bt.values.includes(String(v))) {
|
|
1589
|
+
throw new HapError("BOUNDS_INVALID_VALUE", `bound "${name}" = ${JSON.stringify(v)} is not in [${bt.values.join(", ")}]`);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
function exceedsCap(value, cap, def) {
|
|
1594
|
+
const bt = def.boundType;
|
|
1595
|
+
if (bt?.kind === "enum") {
|
|
1596
|
+
const order = bt.values;
|
|
1597
|
+
const vi = order.indexOf(String(value));
|
|
1598
|
+
const ci = order.indexOf(String(cap));
|
|
1599
|
+
if (vi < 0 || ci < 0) return true;
|
|
1600
|
+
return vi < ci;
|
|
1601
|
+
}
|
|
1602
|
+
return typeof value !== "number" || typeof cap !== "number" || value > cap;
|
|
1603
|
+
}
|
|
1604
|
+
function checkGroupCeilings(bounds, profile, caps) {
|
|
1605
|
+
const fields = profile.boundsSchema?.fields ?? {};
|
|
1606
|
+
const hard = { ...caps.hard ?? {} };
|
|
1607
|
+
for (const [name, cap] of Object.entries(caps.approval ?? {})) {
|
|
1608
|
+
const def = fields[name];
|
|
1609
|
+
const bt = def?.boundType;
|
|
1610
|
+
if (!def || !bt) continue;
|
|
1611
|
+
if (bt.kind !== "per_transaction" || def.enforcedBy === "gatekeeper") hard[name] = cap;
|
|
1612
|
+
}
|
|
1613
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
1614
|
+
if (name === "profile") continue;
|
|
1615
|
+
const cap = hard[name];
|
|
1616
|
+
if (cap === void 0) continue;
|
|
1617
|
+
const v = bounds[name];
|
|
1618
|
+
if (v === void 0 || v === null) {
|
|
1619
|
+
if (!def.required) {
|
|
1620
|
+
throw new HapError("GROUP_CAP_EXCEEDED", `bound "${name}" is omitted under a group ceiling of ${JSON.stringify(cap)} \u2014 an omitted bound is unlimited`);
|
|
1621
|
+
}
|
|
1622
|
+
continue;
|
|
1623
|
+
}
|
|
1624
|
+
if (exceedsCap(v, cap, def)) {
|
|
1625
|
+
throw new HapError("GROUP_CAP_EXCEEDED", `bound "${name}" = ${JSON.stringify(v)} exceeds the group's hard cap ${JSON.stringify(cap)}`);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
function validateAboveCapConfig(config, profile, opts) {
|
|
1630
|
+
const fields = profile.boundsSchema?.fields ?? {};
|
|
1631
|
+
for (const name of Object.keys(config.caps ?? {})) {
|
|
1632
|
+
if (name === "profile") throw new HapError("ABOVE_CAP_CONFIG_INVALID", "above_cap_caps MUST NOT name profile");
|
|
1633
|
+
const def = fields[name];
|
|
1634
|
+
if (!def || def.boundType?.kind !== "per_transaction" || def.type !== "number") {
|
|
1635
|
+
throw new HapError("ABOVE_CAP_CONFIG_INVALID", `above_cap_caps names "${name}", which is not a numeric per_transaction bound of ${profile.id}`);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
for (const did of config.approvers ?? []) {
|
|
1639
|
+
if (!isKeyBearingDid(did)) {
|
|
1640
|
+
throw new HapError("ABOVE_CAP_CONFIG_INVALID", `above-cap approver ${did} is not a key-bearing DID`);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
if (config.approvers !== void 0 && !opts.inGroup) {
|
|
1644
|
+
const others = config.approvers.filter((d) => d !== opts.ownerDid).length;
|
|
1645
|
+
const n = config.approvals;
|
|
1646
|
+
if (n === void 0 || !Number.isInteger(n) || n < 1 || n > others) {
|
|
1647
|
+
throw new HapError(
|
|
1648
|
+
"ABOVE_CAP_CONFIG_INVALID",
|
|
1649
|
+
`above_cap_approvals MUST be an integer between 1 and the ${others} approver(s) other than the owner, got ${JSON.stringify(n)}`
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
function capsExceeded(execution, mandate, profile) {
|
|
1655
|
+
const out = [];
|
|
1656
|
+
for (const [name, cap] of Object.entries(mandate.above_cap_caps ?? {})) {
|
|
1657
|
+
const bt = profile.boundsSchema?.fields[name]?.boundType;
|
|
1658
|
+
if (!bt || bt.kind !== "per_transaction") continue;
|
|
1659
|
+
const v = execution[bt.of];
|
|
1660
|
+
if (v === void 0 || v === null || typeof v !== "number" || v > cap) {
|
|
1661
|
+
out.push({ bound: name, field: bt.of, cap, value: v });
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
return out;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
918
1667
|
// src/profiles/index.ts
|
|
919
1668
|
var PROFILES = {};
|
|
920
1669
|
function registerProfile(profileId, profile) {
|
|
@@ -973,8 +1722,9 @@ async function verify(request, opts = {}) {
|
|
|
973
1722
|
}
|
|
974
1723
|
const expectedProfileHash = computeProfileHash(profile);
|
|
975
1724
|
const errors = [];
|
|
1725
|
+
const verifiedPayloads = [];
|
|
976
1726
|
for (const blob of request.mandates) {
|
|
977
|
-
const mandateErrors = await verifyOneMandate(blob, {
|
|
1727
|
+
const { errors: mandateErrors, payload } = await verifyOneMandate(blob, {
|
|
978
1728
|
expectedBoundsHash,
|
|
979
1729
|
expectedScopeHash,
|
|
980
1730
|
expectedProfileHash,
|
|
@@ -982,6 +1732,7 @@ async function verify(request, opts = {}) {
|
|
|
982
1732
|
now
|
|
983
1733
|
});
|
|
984
1734
|
errors.push(...mandateErrors);
|
|
1735
|
+
if (payload) verifiedPayloads.push(payload);
|
|
985
1736
|
}
|
|
986
1737
|
if (errors.length > 0) {
|
|
987
1738
|
return { approved: false, errors };
|
|
@@ -1000,6 +1751,31 @@ async function verify(request, opts = {}) {
|
|
|
1000
1751
|
};
|
|
1001
1752
|
}
|
|
1002
1753
|
}
|
|
1754
|
+
try {
|
|
1755
|
+
checkExecutionValues(request.execution, profile);
|
|
1756
|
+
} catch (err) {
|
|
1757
|
+
return { approved: false, errors: [{ code: codeFromThrown(err), message: err.message }] };
|
|
1758
|
+
}
|
|
1759
|
+
if (actionType !== void 0) {
|
|
1760
|
+
const capped = /* @__PURE__ */ new Set();
|
|
1761
|
+
for (const p of verifiedPayloads) {
|
|
1762
|
+
for (const name of Object.keys(p.above_cap_caps ?? {})) {
|
|
1763
|
+
const bt = profile.boundsSchema?.fields[name]?.boundType;
|
|
1764
|
+
if (bt && bt.kind === "per_transaction") capped.add(bt.of);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
const missing = missingSumFields(request.execution, profile, actionType).filter((m) => !capped.has(m.field));
|
|
1768
|
+
if (missing.length > 0) {
|
|
1769
|
+
return {
|
|
1770
|
+
approved: false,
|
|
1771
|
+
errors: missing.map(({ bound, field }) => ({
|
|
1772
|
+
code: "BOUND_EXCEEDED",
|
|
1773
|
+
field,
|
|
1774
|
+
message: `Bound "${bound}" governs "${actionType}" and reads "${field}", but this call exposes no ${field}. Refusing: an action without an amount would consume nothing of the limit (v0.8: a missing amount fails safe).`
|
|
1775
|
+
}))
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1003
1779
|
const boundsErrors = checkBounds(request, profile, actionType);
|
|
1004
1780
|
if (boundsErrors.length > 0) {
|
|
1005
1781
|
return { approved: false, errors: boundsErrors };
|
|
@@ -1033,16 +1809,21 @@ function codeFromThrown(err) {
|
|
|
1033
1809
|
return "MALFORMED_MANDATE";
|
|
1034
1810
|
}
|
|
1035
1811
|
async function verifyOneMandate(blob, opts) {
|
|
1812
|
+
const errors = await verifyOneMandateErrors(blob, opts);
|
|
1813
|
+
if (errors.length > 0) return { errors };
|
|
1814
|
+
return { errors, payload: decodeMandateBlob(blob).payload };
|
|
1815
|
+
}
|
|
1816
|
+
async function verifyOneMandateErrors(blob, opts) {
|
|
1036
1817
|
let mandate;
|
|
1037
1818
|
try {
|
|
1038
1819
|
mandate = decodeMandateBlob(blob);
|
|
1039
1820
|
} catch (err) {
|
|
1040
1821
|
return [{ code: "MALFORMED_MANDATE", message: err.message }];
|
|
1041
1822
|
}
|
|
1042
|
-
if (mandate.payload.version
|
|
1823
|
+
if (!isSupportedVersion(mandate.payload.version)) {
|
|
1043
1824
|
return [{
|
|
1044
1825
|
code: "VERSION_UNSUPPORTED",
|
|
1045
|
-
message: `mandate version ${JSON.stringify(mandate.payload.version)} is not verifiable by this Gatekeeper (verifies ${
|
|
1826
|
+
message: `mandate version ${JSON.stringify(mandate.payload.version)} is not verifiable by this Gatekeeper (verifies [${SUPPORTED_VERSIONS.join(", ")}])`
|
|
1046
1827
|
}];
|
|
1047
1828
|
}
|
|
1048
1829
|
try {
|
|
@@ -1057,6 +1838,7 @@ async function verifyOneMandate(blob, opts) {
|
|
|
1057
1838
|
}
|
|
1058
1839
|
try {
|
|
1059
1840
|
validateMandateOwners(mandate.payload);
|
|
1841
|
+
validateAboveCapFields(mandate.payload);
|
|
1060
1842
|
} catch (err) {
|
|
1061
1843
|
return [{ code: codeFromThrown(err), message: err.message }];
|
|
1062
1844
|
}
|
|
@@ -1222,60 +2004,96 @@ function checkScopeConstraints(scope, execution, profile) {
|
|
|
1222
2004
|
return errors;
|
|
1223
2005
|
}
|
|
1224
2006
|
export {
|
|
2007
|
+
BINDING_RANK,
|
|
2008
|
+
COMMITMENT_MODES,
|
|
1225
2009
|
CanonicalValueError,
|
|
1226
2010
|
ContentBindingError,
|
|
1227
2011
|
HapError,
|
|
2012
|
+
ISSUABLE_VERSIONS,
|
|
1228
2013
|
MANDATE_ERROR_CODES,
|
|
1229
2014
|
OwnerSignatureError,
|
|
2015
|
+
PROJECTION_MAX_VALIDITY_SECONDS,
|
|
2016
|
+
PROPOSAL_ERROR_CODES,
|
|
1230
2017
|
PROTOCOL_VERSION,
|
|
2018
|
+
PolicyError,
|
|
1231
2019
|
SIGNABLE_COMMITMENT_MODES,
|
|
1232
2020
|
SUPPORTED_VERSIONS,
|
|
1233
2021
|
TICKET_ERROR_CODES,
|
|
2022
|
+
TICKET_FRESHNESS_SECONDS,
|
|
1234
2023
|
allowedCommitmentModes,
|
|
1235
2024
|
approvalSigningBytes,
|
|
2025
|
+
approvalsSufficient,
|
|
2026
|
+
assertSupportedVersion,
|
|
2027
|
+
assertTicketShape,
|
|
1236
2028
|
bindingAppliesTo,
|
|
2029
|
+
bindingRank,
|
|
1237
2030
|
boundActionTypes,
|
|
2031
|
+
boundGoverns,
|
|
2032
|
+
boundReadFields,
|
|
1238
2033
|
buildMandateProjection,
|
|
2034
|
+
buildTicketProjection,
|
|
1239
2035
|
canonicalBounds,
|
|
1240
2036
|
canonicalScope,
|
|
1241
2037
|
canonicalize,
|
|
1242
2038
|
canonicalizeText,
|
|
2039
|
+
capsExceeded,
|
|
2040
|
+
checkBoundsValues,
|
|
1243
2041
|
checkDiscloseSubset,
|
|
2042
|
+
checkExecutionValues,
|
|
2043
|
+
checkGroupCeilings,
|
|
1244
2044
|
checkMandateExpiry,
|
|
1245
2045
|
checkOwnerSignatureRequirement,
|
|
2046
|
+
checkProposalReaders,
|
|
1246
2047
|
clearProfiles,
|
|
1247
2048
|
computeBoundsHash,
|
|
1248
2049
|
computeContentHash,
|
|
2050
|
+
computeExecutionContextHash,
|
|
1249
2051
|
computeFieldsContentHash,
|
|
1250
2052
|
computeIntentDisclosureHash,
|
|
1251
2053
|
computeIntentHash,
|
|
1252
2054
|
computeProfileHash,
|
|
2055
|
+
computeProposalContentHash,
|
|
1253
2056
|
computeScopeHash,
|
|
1254
2057
|
contentCanonicalBytes,
|
|
1255
2058
|
decodeDidKey,
|
|
1256
2059
|
decodeMandateBlob,
|
|
2060
|
+
decryptProposalContent,
|
|
1257
2061
|
deriveIdentityLine,
|
|
2062
|
+
deriveReaderKeyPair,
|
|
1258
2063
|
encodeDidKey,
|
|
1259
2064
|
encodeMandateBlob,
|
|
2065
|
+
encryptProposalContent,
|
|
2066
|
+
evaluateVerifierPolicy,
|
|
2067
|
+
exceedsCap,
|
|
2068
|
+
filesFields,
|
|
1260
2069
|
fromBase64Url,
|
|
2070
|
+
generateReaderKeyPair,
|
|
1261
2071
|
getAllProfiles,
|
|
1262
2072
|
getProfile,
|
|
1263
2073
|
intentDisclosureCanonicalBytes,
|
|
2074
|
+
invalidFieldKindsReason,
|
|
1264
2075
|
isCommitmentModeAllowed,
|
|
1265
2076
|
isFieldBinding,
|
|
1266
2077
|
isKeyBearingDid,
|
|
2078
|
+
isSupportedVersion,
|
|
1267
2079
|
listProfiles,
|
|
1268
2080
|
mandateBlobId,
|
|
2081
|
+
missingSumFields,
|
|
1269
2082
|
negotiateVersion,
|
|
1270
2083
|
projectionSigningBytes,
|
|
2084
|
+
proposalContentJcs,
|
|
1271
2085
|
publicTicketView,
|
|
2086
|
+
reconstructApproval,
|
|
1272
2087
|
registerProfile,
|
|
1273
2088
|
selectBoundFields,
|
|
1274
2089
|
signApproval,
|
|
1275
2090
|
signMandate,
|
|
1276
2091
|
signMandateProjection,
|
|
1277
2092
|
signTicket,
|
|
2093
|
+
signTicketProjection,
|
|
1278
2094
|
toBase64Url,
|
|
2095
|
+
validateAboveCapConfig,
|
|
2096
|
+
validateAboveCapFields,
|
|
1279
2097
|
validateBoundsParams,
|
|
1280
2098
|
validateMandateOwners,
|
|
1281
2099
|
validateProfile,
|
|
@@ -1287,5 +2105,10 @@ export {
|
|
|
1287
2105
|
verifyMandateSignature,
|
|
1288
2106
|
verifyOwnerSignature,
|
|
1289
2107
|
verifyOwnerSignatures,
|
|
1290
|
-
|
|
2108
|
+
verifyProjectionOwner,
|
|
2109
|
+
verifyTicketAgainstRequest,
|
|
2110
|
+
verifyTicketApprovals,
|
|
2111
|
+
verifyTicketProjection,
|
|
2112
|
+
verifyTicketSignature,
|
|
2113
|
+
wireExecutionContext
|
|
1291
2114
|
};
|