@suveren/gateway 0.6.2 → 0.6.4
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/content/integrations/calendar.json +6 -3
- package/content/integrations/crm.json +6 -3
- package/content/integrations/deploy-github.json +5 -1
- package/content/integrations/gmail.json +16 -6
- package/content/integrations/linkedin.json +75 -16
- package/content/integrations/records.json +76 -14
- package/dist/mcp-server/http.mjs +215 -28
- package/dist/ui/assets/index-DIcuh0I4.css +1 -0
- package/dist/ui/assets/index-Dl5isaOV.js +105 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/mockups/audit-and-header.html +192 -0
- package/node_modules/@hap/core/dist/index.d.mts +139 -6
- package/node_modules/@hap/core/dist/index.d.ts +139 -6
- package/node_modules/@hap/core/dist/index.js +93 -0
- package/node_modules/@hap/core/dist/index.mjs +88 -0
- package/node_modules/@hap/core/package.json +1 -1
- package/node_modules/@hap/core/src/content-binding.ts +168 -0
- package/node_modules/@hap/core/src/types.ts +85 -5
- package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +8 -0
- package/node_modules/jose/dist/webapi/jwe/general/encrypt.js +1 -1
- package/node_modules/jose/dist/webapi/jwks/local.js +3 -3
- package/node_modules/jose/dist/webapi/jwks/remote.js +1 -1
- package/node_modules/jose/dist/webapi/key/generate_key_pair.js +3 -3
- package/node_modules/jose/dist/webapi/key/generate_secret.js +2 -2
- package/node_modules/jose/dist/webapi/lib/asn1.js +3 -3
- package/node_modules/jose/dist/webapi/lib/jwe_algorithms.js +6 -15
- package/node_modules/jose/dist/webapi/lib/jws_algorithms.js +2 -5
- package/node_modules/jose/dist/webapi/lib/key.js +1 -1
- package/node_modules/jose/dist/webapi/lib/key_algorithm.js +8 -8
- package/node_modules/jose/package.json +1 -1
- package/package.json +2 -2
- package/profiles/deploy/0.7.profile.json +2 -1
- package/profiles/email/0.5.profile.json +200 -0
- package/profiles/index.json +1 -0
- package/dist/ui/assets/index-DoZvxHLN.js +0 -105
- package/dist/ui/assets/index-JHaCddDE.css +0 -1
|
@@ -30,7 +30,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
ContentBindingError: () => ContentBindingError,
|
|
33
34
|
attestationId: () => attestationId,
|
|
35
|
+
bindingAppliesTo: () => bindingAppliesTo,
|
|
34
36
|
canonicalBounds: () => canonicalBounds,
|
|
35
37
|
canonicalContext: () => canonicalContext,
|
|
36
38
|
canonicalFrame: () => canonicalFrame,
|
|
@@ -41,6 +43,7 @@ __export(index_exports, {
|
|
|
41
43
|
computeBoundsHash: () => computeBoundsHash,
|
|
42
44
|
computeContentHash: () => computeContentHash,
|
|
43
45
|
computeContextHash: () => computeContextHash,
|
|
46
|
+
computeFieldsContentHash: () => computeFieldsContentHash,
|
|
44
47
|
computeFrameHash: () => computeFrameHash,
|
|
45
48
|
computeIntentDisclosureHash: () => computeIntentDisclosureHash,
|
|
46
49
|
computeIntentHash: () => computeIntentHash,
|
|
@@ -52,9 +55,11 @@ __export(index_exports, {
|
|
|
52
55
|
getAllProfiles: () => getAllProfiles,
|
|
53
56
|
getProfile: () => getProfile,
|
|
54
57
|
intentDisclosureCanonicalBytes: () => intentDisclosureCanonicalBytes,
|
|
58
|
+
isFieldBinding: () => isFieldBinding,
|
|
55
59
|
isV4Attestation: () => isV4Attestation,
|
|
56
60
|
listProfiles: () => listProfiles,
|
|
57
61
|
registerProfile: () => registerProfile,
|
|
62
|
+
selectBoundFields: () => selectBoundFields,
|
|
58
63
|
validateBoundsParams: () => validateBoundsParams,
|
|
59
64
|
validateContextParams: () => validateContextParams,
|
|
60
65
|
validateFrameParams: () => validateFrameParams,
|
|
@@ -120,6 +125,89 @@ function contentCanonicalBytes(kind, content) {
|
|
|
120
125
|
function computeContentHash(binding, content) {
|
|
121
126
|
return `sha256:${sha256Hex(contentCanonicalBytes(binding.kind, content))}`;
|
|
122
127
|
}
|
|
128
|
+
var ContentBindingError = class extends Error {
|
|
129
|
+
code;
|
|
130
|
+
/** The offending field, when the code names one. */
|
|
131
|
+
field;
|
|
132
|
+
constructor(code, message, field) {
|
|
133
|
+
super(message);
|
|
134
|
+
this.name = "ContentBindingError";
|
|
135
|
+
this.code = code;
|
|
136
|
+
this.field = field;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
function isFieldBinding(binding) {
|
|
140
|
+
return binding.version === "2" && Array.isArray(binding.fields) && binding.fields.length > 0;
|
|
141
|
+
}
|
|
142
|
+
function bindingAppliesTo(binding, actionType) {
|
|
143
|
+
if (!binding.appliesTo) return true;
|
|
144
|
+
return actionType !== void 0 && binding.appliesTo.includes(actionType);
|
|
145
|
+
}
|
|
146
|
+
function canonicalizeValue(value) {
|
|
147
|
+
if (value === null || value === void 0) return void 0;
|
|
148
|
+
if (typeof value === "string") {
|
|
149
|
+
const text = canonicalizeText(value);
|
|
150
|
+
return text === "" ? void 0 : text;
|
|
151
|
+
}
|
|
152
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
153
|
+
if (Array.isArray(value)) {
|
|
154
|
+
const items = value.map(canonicalizeValue).filter((v) => v !== void 0);
|
|
155
|
+
return items.length > 0 ? items : void 0;
|
|
156
|
+
}
|
|
157
|
+
if (typeof value === "object") {
|
|
158
|
+
const out = {};
|
|
159
|
+
for (const [key, v] of Object.entries(value)) {
|
|
160
|
+
const c = canonicalizeValue(v);
|
|
161
|
+
if (c !== void 0) out[key] = c;
|
|
162
|
+
}
|
|
163
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
164
|
+
}
|
|
165
|
+
return void 0;
|
|
166
|
+
}
|
|
167
|
+
function selectBoundFields(binding, args) {
|
|
168
|
+
const fields = binding.fields;
|
|
169
|
+
if (!fields || fields.length === 0) {
|
|
170
|
+
throw new ContentBindingError(
|
|
171
|
+
"NO_FIELDS_DECLARED",
|
|
172
|
+
'content_binding version "2" requires a non-empty `fields` list.'
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
const required = new Set(binding.required_fields ?? []);
|
|
176
|
+
for (const field of required) {
|
|
177
|
+
if (!fields.includes(field)) {
|
|
178
|
+
throw new ContentBindingError(
|
|
179
|
+
"REQUIRED_FIELD_NOT_DECLARED",
|
|
180
|
+
`content_binding requires "${field}" but does not bind it \u2014 required_fields must be a subset of fields.`,
|
|
181
|
+
field
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const bound = {};
|
|
186
|
+
for (const field of fields) {
|
|
187
|
+
const value = canonicalizeValue(args[field]);
|
|
188
|
+
if (value === void 0) {
|
|
189
|
+
if (required.has(field)) {
|
|
190
|
+
throw new ContentBindingError(
|
|
191
|
+
"MISSING_REQUIRED_FIELD",
|
|
192
|
+
`content_binding requires "${field}", which is absent or empty in this call. Refusing rather than hashing a partial object.`,
|
|
193
|
+
field
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
bound[field] = value;
|
|
199
|
+
}
|
|
200
|
+
if (Object.keys(bound).length === 0) {
|
|
201
|
+
throw new ContentBindingError(
|
|
202
|
+
"EMPTY_BINDING",
|
|
203
|
+
`No declared field (${fields.join(", ")}) carried a value \u2014 the hash would commit to nothing.`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return bound;
|
|
207
|
+
}
|
|
208
|
+
function computeFieldsContentHash(binding, args) {
|
|
209
|
+
return computeContentHash(binding, selectBoundFields(binding, args));
|
|
210
|
+
}
|
|
123
211
|
|
|
124
212
|
// src/intent-disclosure.ts
|
|
125
213
|
var import_crypto2 = require("crypto");
|
|
@@ -859,7 +947,9 @@ function resolveCumulativeFields(request, profile, executionLog, now) {
|
|
|
859
947
|
}
|
|
860
948
|
// Annotate the CommonJS export names for ESM import in node:
|
|
861
949
|
0 && (module.exports = {
|
|
950
|
+
ContentBindingError,
|
|
862
951
|
attestationId,
|
|
952
|
+
bindingAppliesTo,
|
|
863
953
|
canonicalBounds,
|
|
864
954
|
canonicalContext,
|
|
865
955
|
canonicalFrame,
|
|
@@ -870,6 +960,7 @@ function resolveCumulativeFields(request, profile, executionLog, now) {
|
|
|
870
960
|
computeBoundsHash,
|
|
871
961
|
computeContentHash,
|
|
872
962
|
computeContextHash,
|
|
963
|
+
computeFieldsContentHash,
|
|
873
964
|
computeFrameHash,
|
|
874
965
|
computeIntentDisclosureHash,
|
|
875
966
|
computeIntentHash,
|
|
@@ -881,9 +972,11 @@ function resolveCumulativeFields(request, profile, executionLog, now) {
|
|
|
881
972
|
getAllProfiles,
|
|
882
973
|
getProfile,
|
|
883
974
|
intentDisclosureCanonicalBytes,
|
|
975
|
+
isFieldBinding,
|
|
884
976
|
isV4Attestation,
|
|
885
977
|
listProfiles,
|
|
886
978
|
registerProfile,
|
|
979
|
+
selectBoundFields,
|
|
887
980
|
validateBoundsParams,
|
|
888
981
|
validateContextParams,
|
|
889
982
|
validateFrameParams,
|
|
@@ -49,6 +49,89 @@ function contentCanonicalBytes(kind, content) {
|
|
|
49
49
|
function computeContentHash(binding, content) {
|
|
50
50
|
return `sha256:${sha256Hex(contentCanonicalBytes(binding.kind, content))}`;
|
|
51
51
|
}
|
|
52
|
+
var ContentBindingError = class extends Error {
|
|
53
|
+
code;
|
|
54
|
+
/** The offending field, when the code names one. */
|
|
55
|
+
field;
|
|
56
|
+
constructor(code, message, field) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = "ContentBindingError";
|
|
59
|
+
this.code = code;
|
|
60
|
+
this.field = field;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
function isFieldBinding(binding) {
|
|
64
|
+
return binding.version === "2" && Array.isArray(binding.fields) && binding.fields.length > 0;
|
|
65
|
+
}
|
|
66
|
+
function bindingAppliesTo(binding, actionType) {
|
|
67
|
+
if (!binding.appliesTo) return true;
|
|
68
|
+
return actionType !== void 0 && binding.appliesTo.includes(actionType);
|
|
69
|
+
}
|
|
70
|
+
function canonicalizeValue(value) {
|
|
71
|
+
if (value === null || value === void 0) return void 0;
|
|
72
|
+
if (typeof value === "string") {
|
|
73
|
+
const text = canonicalizeText(value);
|
|
74
|
+
return text === "" ? void 0 : text;
|
|
75
|
+
}
|
|
76
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
77
|
+
if (Array.isArray(value)) {
|
|
78
|
+
const items = value.map(canonicalizeValue).filter((v) => v !== void 0);
|
|
79
|
+
return items.length > 0 ? items : void 0;
|
|
80
|
+
}
|
|
81
|
+
if (typeof value === "object") {
|
|
82
|
+
const out = {};
|
|
83
|
+
for (const [key, v] of Object.entries(value)) {
|
|
84
|
+
const c = canonicalizeValue(v);
|
|
85
|
+
if (c !== void 0) out[key] = c;
|
|
86
|
+
}
|
|
87
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
88
|
+
}
|
|
89
|
+
return void 0;
|
|
90
|
+
}
|
|
91
|
+
function selectBoundFields(binding, args) {
|
|
92
|
+
const fields = binding.fields;
|
|
93
|
+
if (!fields || fields.length === 0) {
|
|
94
|
+
throw new ContentBindingError(
|
|
95
|
+
"NO_FIELDS_DECLARED",
|
|
96
|
+
'content_binding version "2" requires a non-empty `fields` list.'
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const required = new Set(binding.required_fields ?? []);
|
|
100
|
+
for (const field of required) {
|
|
101
|
+
if (!fields.includes(field)) {
|
|
102
|
+
throw new ContentBindingError(
|
|
103
|
+
"REQUIRED_FIELD_NOT_DECLARED",
|
|
104
|
+
`content_binding requires "${field}" but does not bind it \u2014 required_fields must be a subset of fields.`,
|
|
105
|
+
field
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const bound = {};
|
|
110
|
+
for (const field of fields) {
|
|
111
|
+
const value = canonicalizeValue(args[field]);
|
|
112
|
+
if (value === void 0) {
|
|
113
|
+
if (required.has(field)) {
|
|
114
|
+
throw new ContentBindingError(
|
|
115
|
+
"MISSING_REQUIRED_FIELD",
|
|
116
|
+
`content_binding requires "${field}", which is absent or empty in this call. Refusing rather than hashing a partial object.`,
|
|
117
|
+
field
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
bound[field] = value;
|
|
123
|
+
}
|
|
124
|
+
if (Object.keys(bound).length === 0) {
|
|
125
|
+
throw new ContentBindingError(
|
|
126
|
+
"EMPTY_BINDING",
|
|
127
|
+
`No declared field (${fields.join(", ")}) carried a value \u2014 the hash would commit to nothing.`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return bound;
|
|
131
|
+
}
|
|
132
|
+
function computeFieldsContentHash(binding, args) {
|
|
133
|
+
return computeContentHash(binding, selectBoundFields(binding, args));
|
|
134
|
+
}
|
|
52
135
|
|
|
53
136
|
// src/intent-disclosure.ts
|
|
54
137
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -787,7 +870,9 @@ function resolveCumulativeFields(request, profile, executionLog, now) {
|
|
|
787
870
|
return errors;
|
|
788
871
|
}
|
|
789
872
|
export {
|
|
873
|
+
ContentBindingError,
|
|
790
874
|
attestationId,
|
|
875
|
+
bindingAppliesTo,
|
|
791
876
|
canonicalBounds,
|
|
792
877
|
canonicalContext,
|
|
793
878
|
canonicalFrame,
|
|
@@ -798,6 +883,7 @@ export {
|
|
|
798
883
|
computeBoundsHash,
|
|
799
884
|
computeContentHash,
|
|
800
885
|
computeContextHash,
|
|
886
|
+
computeFieldsContentHash,
|
|
801
887
|
computeFrameHash,
|
|
802
888
|
computeIntentDisclosureHash,
|
|
803
889
|
computeIntentHash,
|
|
@@ -809,9 +895,11 @@ export {
|
|
|
809
895
|
getAllProfiles,
|
|
810
896
|
getProfile,
|
|
811
897
|
intentDisclosureCanonicalBytes,
|
|
898
|
+
isFieldBinding,
|
|
812
899
|
isV4Attestation,
|
|
813
900
|
listProfiles,
|
|
814
901
|
registerProfile,
|
|
902
|
+
selectBoundFields,
|
|
815
903
|
validateBoundsParams,
|
|
816
904
|
validateContextParams,
|
|
817
905
|
validateFrameParams,
|
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
* (Unicode NFC, LF line endings, trailing per-line whitespace stripped,
|
|
17
17
|
* trailing blank lines removed), taken pre-footer when `pre_footer` is set.
|
|
18
18
|
*
|
|
19
|
+
* At `version:"2"` the profile also declares WHICH tool arguments are bound
|
|
20
|
+
* ({@link selectBoundFields}), so a receipt can commit to an email's recipients
|
|
21
|
+
* and not only its prose — while still omitting what the intended verifier
|
|
22
|
+
* cannot see. Every string entering the hashed object is canonicalized by the
|
|
23
|
+
* same `text` rule, so a delivered copy with CRLF endings still reproduces it.
|
|
24
|
+
*
|
|
19
25
|
* Both Node and the browser produce byte-identical output: JCS relies only on
|
|
20
26
|
* environment-independent primitives, and the text rule uses String.normalize +
|
|
21
27
|
* plain string ops. The SHA-256 is computed with Node `crypto` here (the same
|
|
@@ -81,3 +87,165 @@ export function computeContentHash(
|
|
|
81
87
|
): string {
|
|
82
88
|
return `sha256:${sha256Hex(contentCanonicalBytes(binding.kind, content))}`;
|
|
83
89
|
}
|
|
90
|
+
|
|
91
|
+
// ─── v2: binding over a declared field subset ────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/** Why a field binding refused. Every case is fail-closed by design. */
|
|
94
|
+
export type ContentBindingErrorCode =
|
|
95
|
+
/** The profile declares version:"2" with no usable `fields` list. */
|
|
96
|
+
| 'NO_FIELDS_DECLARED'
|
|
97
|
+
/** `required_fields` names something absent from `fields`. */
|
|
98
|
+
| 'REQUIRED_FIELD_NOT_DECLARED'
|
|
99
|
+
/** A required field was absent or empty at call time. */
|
|
100
|
+
| 'MISSING_REQUIRED_FIELD'
|
|
101
|
+
/** No declared field carried a value — the hash would commit to nothing. */
|
|
102
|
+
| 'EMPTY_BINDING';
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A field binding could not be computed. ALWAYS a refusal, never a downgrade:
|
|
106
|
+
* the alternative is a receipt that verifies while proving less than it appears
|
|
107
|
+
* to, which is the failure content binding exists to prevent.
|
|
108
|
+
*/
|
|
109
|
+
export class ContentBindingError extends Error {
|
|
110
|
+
readonly code: ContentBindingErrorCode;
|
|
111
|
+
/** The offending field, when the code names one. */
|
|
112
|
+
readonly field?: string;
|
|
113
|
+
|
|
114
|
+
constructor(code: ContentBindingErrorCode, message: string, field?: string) {
|
|
115
|
+
super(message);
|
|
116
|
+
this.name = 'ContentBindingError';
|
|
117
|
+
this.code = code;
|
|
118
|
+
this.field = field;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** True when this binding selects a declared subset (v2) rather than v1's implicit scope. */
|
|
123
|
+
export function isFieldBinding(binding: ContentBinding): boolean {
|
|
124
|
+
return binding.version === '2' && Array.isArray(binding.fields) && binding.fields.length > 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Whether a field binding covers this action type, per the profile's `appliesTo`.
|
|
129
|
+
*
|
|
130
|
+
* Read STRICTLY — an undeclared action type is NOT covered. This differs from
|
|
131
|
+
* how bounds read the same key (there, an unknown action type enforces the
|
|
132
|
+
* bound, because an extra limit is safe). Here the two directions are not
|
|
133
|
+
* symmetric: applying a field binding to a call that carries no content refuses
|
|
134
|
+
* a legitimate action, so an unknown action type must fall outside rather than
|
|
135
|
+
* inside. Callers are expected to warn on the undeclared case — it is a
|
|
136
|
+
* manifest bug either way.
|
|
137
|
+
*/
|
|
138
|
+
export function bindingAppliesTo(
|
|
139
|
+
binding: ContentBinding,
|
|
140
|
+
actionType: string | undefined,
|
|
141
|
+
): boolean {
|
|
142
|
+
if (!binding.appliesTo) return true;
|
|
143
|
+
return actionType !== undefined && binding.appliesTo.includes(actionType);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Canonicalize one value on its way into the bound object, or `undefined` when
|
|
148
|
+
* it carries nothing (absent / null / empty after canonicalization).
|
|
149
|
+
*
|
|
150
|
+
* Strings are run through {@link canonicalizeText} — the SAME rule `kind:"text"`
|
|
151
|
+
* uses, applied per string rather than to one field. This is not decoration:
|
|
152
|
+
* the verifier of an email holds the DELIVERED copy, whose body has CRLF line
|
|
153
|
+
* endings and transport-added trailing whitespace. JCS embeds strings verbatim,
|
|
154
|
+
* so without this the recipient could never reproduce the hash.
|
|
155
|
+
*
|
|
156
|
+
* Deliberately NOT normalized: array order (the To: header preserves what was
|
|
157
|
+
* sent, and reordering recipients is a change worth catching) and address case
|
|
158
|
+
* (the local part is case-sensitive per RFC 5321, so lowercasing would be a
|
|
159
|
+
* semantic claim this layer has no business making).
|
|
160
|
+
*/
|
|
161
|
+
function canonicalizeValue(value: unknown): unknown {
|
|
162
|
+
if (value === null || value === undefined) return undefined;
|
|
163
|
+
if (typeof value === 'string') {
|
|
164
|
+
const text = canonicalizeText(value);
|
|
165
|
+
return text === '' ? undefined : text;
|
|
166
|
+
}
|
|
167
|
+
if (typeof value === 'number' || typeof value === 'boolean') return value;
|
|
168
|
+
if (Array.isArray(value)) {
|
|
169
|
+
const items = value.map(canonicalizeValue).filter((v) => v !== undefined);
|
|
170
|
+
return items.length > 0 ? items : undefined;
|
|
171
|
+
}
|
|
172
|
+
if (typeof value === 'object') {
|
|
173
|
+
const out: Record<string, unknown> = {};
|
|
174
|
+
for (const [key, v] of Object.entries(value as Record<string, unknown>)) {
|
|
175
|
+
const c = canonicalizeValue(v);
|
|
176
|
+
if (c !== undefined) out[key] = c;
|
|
177
|
+
}
|
|
178
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
179
|
+
}
|
|
180
|
+
return undefined; // functions/symbols — not JSON, nothing to bind
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Build the object a v2 binding hashes: exactly the declared `fields` that
|
|
185
|
+
* carry a value, canonicalized. Exported so a verifier can construct the same
|
|
186
|
+
* object from what they hold and see it before hashing.
|
|
187
|
+
*
|
|
188
|
+
* Throws {@link ContentBindingError} rather than returning a partial result —
|
|
189
|
+
* see that class for why refusing is the only safe outcome.
|
|
190
|
+
*/
|
|
191
|
+
export function selectBoundFields(
|
|
192
|
+
binding: ContentBinding,
|
|
193
|
+
args: Record<string, unknown>,
|
|
194
|
+
): Record<string, unknown> {
|
|
195
|
+
const fields = binding.fields;
|
|
196
|
+
if (!fields || fields.length === 0) {
|
|
197
|
+
throw new ContentBindingError(
|
|
198
|
+
'NO_FIELDS_DECLARED',
|
|
199
|
+
'content_binding version "2" requires a non-empty `fields` list.',
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const required = new Set(binding.required_fields ?? []);
|
|
204
|
+
for (const field of required) {
|
|
205
|
+
if (!fields.includes(field)) {
|
|
206
|
+
throw new ContentBindingError(
|
|
207
|
+
'REQUIRED_FIELD_NOT_DECLARED',
|
|
208
|
+
`content_binding requires "${field}" but does not bind it — required_fields must be a subset of fields.`,
|
|
209
|
+
field,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const bound: Record<string, unknown> = {};
|
|
215
|
+
for (const field of fields) {
|
|
216
|
+
const value = canonicalizeValue(args[field]);
|
|
217
|
+
if (value === undefined) {
|
|
218
|
+
if (required.has(field)) {
|
|
219
|
+
throw new ContentBindingError(
|
|
220
|
+
'MISSING_REQUIRED_FIELD',
|
|
221
|
+
`content_binding requires "${field}", which is absent or empty in this call. ` +
|
|
222
|
+
`Refusing rather than hashing a partial object.`,
|
|
223
|
+
field,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
continue; // legitimately absent (an email with no cc)
|
|
227
|
+
}
|
|
228
|
+
bound[field] = value;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (Object.keys(bound).length === 0) {
|
|
232
|
+
throw new ContentBindingError(
|
|
233
|
+
'EMPTY_BINDING',
|
|
234
|
+
`No declared field (${fields.join(', ')}) carried a value — the hash would commit to nothing.`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return bound;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Compute a v2 field-binding hash from raw tool arguments: select the declared
|
|
243
|
+
* subset, then hash it by the declared `kind`. Convenience over
|
|
244
|
+
* {@link selectBoundFields} + {@link computeContentHash} for the common path.
|
|
245
|
+
*/
|
|
246
|
+
export function computeFieldsContentHash(
|
|
247
|
+
binding: ContentBinding,
|
|
248
|
+
args: Record<string, unknown>,
|
|
249
|
+
): string {
|
|
250
|
+
return computeContentHash(binding, selectBoundFields(binding, args));
|
|
251
|
+
}
|
|
@@ -271,22 +271,64 @@ export interface ProfileBoundsField {
|
|
|
271
271
|
* backward compatibility). The gateway computes the hash; the SP only ever
|
|
272
272
|
* receives the hash, never the content, so HAP's privacy-minimal design holds.
|
|
273
273
|
*
|
|
274
|
-
*
|
|
275
|
-
* canonicalize. It does NOT name the tool field: that is tool-specific
|
|
276
|
-
* resolved at runtime (the same content-field resolver the footer uses
|
|
277
|
-
* `kind:"text"`; the whole record payload for `kind:"jcs"`).
|
|
274
|
+
* At `version:"1"` the profile declares only the *policy* — whether to bind and
|
|
275
|
+
* how to canonicalize. It does NOT name the tool field: that is tool-specific
|
|
276
|
+
* and is resolved at runtime (the same content-field resolver the footer uses
|
|
277
|
+
* for `kind:"text"`; the whole record payload for `kind:"jcs"`).
|
|
278
|
+
*
|
|
279
|
+
* At `version:"2"` the profile additionally declares WHICH fields are bound (see
|
|
280
|
+
* {@link ContentBinding.fields}). Neither v1 mode is the general case: `text`
|
|
281
|
+
* binds one field and leaves everything beside it unbound, while `jcs` over the
|
|
282
|
+
* whole payload is checkable only by a party that already knows the whole
|
|
283
|
+
* payload — an email recipient holds the body, the subject and their own
|
|
284
|
+
* address, but not `bcc`. The general case is a declared subset, chosen so the
|
|
285
|
+
* intended verifier can reproduce it.
|
|
278
286
|
*/
|
|
279
287
|
export interface ContentBinding {
|
|
280
288
|
/** Canonicalization version. A verifier MUST pin the version named here. */
|
|
281
289
|
version: string;
|
|
282
290
|
/**
|
|
283
|
-
* - 'jcs' → structured writes: RFC 8785 JCS over the record payload
|
|
291
|
+
* - 'jcs' → structured writes: RFC 8785 JCS over the record payload
|
|
292
|
+
* (v1) or over the object built from {@link fields} (v2).
|
|
284
293
|
* - 'text' → free text: NFC + LF + trailing-whitespace strip (see
|
|
285
294
|
* canonicalizeText), auto-detected content field.
|
|
286
295
|
*/
|
|
287
296
|
kind: 'jcs' | 'text';
|
|
288
297
|
/** text only: hash the content BEFORE any appended Suveren footer. */
|
|
289
298
|
pre_footer?: boolean;
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* v2 only — the tool-argument keys this binding covers, and the complete
|
|
302
|
+
* statement of what a verifier must reproduce. The Gatekeeper builds an
|
|
303
|
+
* object from exactly these keys and canonicalizes it by `kind`.
|
|
304
|
+
*
|
|
305
|
+
* Adding or removing an entry changes every resulting hash, so it is a
|
|
306
|
+
* BREAKING profile change requiring a version bump, never a silent edit.
|
|
307
|
+
*
|
|
308
|
+
* Choose the subset by one rule: bind everything the approving human is
|
|
309
|
+
* shown, and nothing the intended verifier cannot see.
|
|
310
|
+
*/
|
|
311
|
+
fields?: string[];
|
|
312
|
+
/**
|
|
313
|
+
* v2 only — the subset of {@link fields} whose absence is a fault rather than
|
|
314
|
+
* a fact. An absent OPTIONAL field is omitted from the hashed object (an
|
|
315
|
+
* email legitimately has no `cc`); an absent REQUIRED field means the call is
|
|
316
|
+
* not the call this profile thinks it is, and MUST refuse rather than hash a
|
|
317
|
+
* partial object that reads exactly like a complete one.
|
|
318
|
+
*
|
|
319
|
+
* MUST be a subset of `fields`. Absent → every field is optional, and only a
|
|
320
|
+
* wholly empty selection refuses.
|
|
321
|
+
*/
|
|
322
|
+
required_fields?: string[];
|
|
323
|
+
/**
|
|
324
|
+
* v2 only — the action types this binding covers, using the same vocabulary
|
|
325
|
+
* as {@link ProfileBoundsField.appliesTo}. A profile gates more than its
|
|
326
|
+
* content-bearing calls: `email` also gates deletes, which carry an id and no
|
|
327
|
+
* content, and applying a field binding to those would refuse them.
|
|
328
|
+
*
|
|
329
|
+
* Absent → the binding applies to every gated action under the profile.
|
|
330
|
+
*/
|
|
331
|
+
appliesTo?: string[];
|
|
290
332
|
}
|
|
291
333
|
|
|
292
334
|
/**
|
|
@@ -370,6 +412,44 @@ export interface AgentProfile {
|
|
|
370
412
|
version: string;
|
|
371
413
|
description: string;
|
|
372
414
|
|
|
415
|
+
/**
|
|
416
|
+
* One line on what this version changed and why it matters to the person
|
|
417
|
+
* granting authority — written for them, not for a changelog.
|
|
418
|
+
*
|
|
419
|
+
* A grant pins the profile version it was signed against, so authorities
|
|
420
|
+
* issued before a newer version keep their old terms indefinitely and
|
|
421
|
+
* nothing prompts an upgrade. A version number alone does not motivate one:
|
|
422
|
+
* "email@0.4 → 0.5" says nothing, while "binds recipients, not only the
|
|
423
|
+
* message body" says what the older grant is not protecting.
|
|
424
|
+
*
|
|
425
|
+
* Belongs on the profile because the profile is what changed; a UI cannot
|
|
426
|
+
* know why 0.5 exists. Absent → surfaces show the version alone.
|
|
427
|
+
*/
|
|
428
|
+
whatsNew?: string;
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Whether receipts under this profile may be looked up BY THEIR CONTENT — a
|
|
432
|
+
* verifier holding the content supplies its hash and learns which receipts
|
|
433
|
+
* bind it, without needing a receipt id.
|
|
434
|
+
*
|
|
435
|
+
* OFF unless declared, and that default is the point. The lookup is a
|
|
436
|
+
* confirmation oracle: given a guess at the content it says whether that
|
|
437
|
+
* content was authorized. Where the bound content has low entropy this is
|
|
438
|
+
* disclosure, not verification — guessing a message body is hopeless,
|
|
439
|
+
* guessing `production` takes a second. It is the same enumeration hazard
|
|
440
|
+
* recorded for per-field commitments, arriving from the other direction.
|
|
441
|
+
*
|
|
442
|
+
* Enable only when the bound content is unguessable enough that producing it
|
|
443
|
+
* is equivalent to already having it: prose, an artifact URL, a whole record
|
|
444
|
+
* payload. Never for a binding over a short value drawn from a small set.
|
|
445
|
+
*
|
|
446
|
+
* Why it must exist at all: most consequential actions cannot carry their
|
|
447
|
+
* receipt id. A released build was built before the receipt existed, a
|
|
448
|
+
* content-addressed artifact would change identity if the id were added, and
|
|
449
|
+
* a forwarded message has usually lost the footer that carried it.
|
|
450
|
+
*/
|
|
451
|
+
receipt_lookup?: boolean;
|
|
452
|
+
|
|
373
453
|
/**
|
|
374
454
|
* v0.3 frame schema (deprecated, kept for backward compat).
|
|
375
455
|
* Used when boundsSchema is not present.
|
|
@@ -21,6 +21,14 @@ export async function generalDecrypt(jwe, key, options) {
|
|
|
21
21
|
catch {
|
|
22
22
|
throw new JWEDecryptionFailed();
|
|
23
23
|
}
|
|
24
|
+
if (jwe.recipients.length > 1) {
|
|
25
|
+
for (const { header } of jwe.recipients) {
|
|
26
|
+
const alg = token[0]?.alg ?? header?.alg ?? jwe.unprotected?.alg;
|
|
27
|
+
if (alg === 'dir' || alg === 'ECDH-ES') {
|
|
28
|
+
throw new JWEInvalid(`"${alg}" alg may only have a single recipient`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
24
32
|
for (const recipient of jwe.recipients) {
|
|
25
33
|
try {
|
|
26
34
|
const flattened = {
|
|
@@ -125,7 +125,7 @@ export class GeneralEncrypt {
|
|
|
125
125
|
inputs.push(input);
|
|
126
126
|
checked.push(headers);
|
|
127
127
|
if (headers[1] === 'dir' || headers[1] === 'ECDH-ES') {
|
|
128
|
-
throw new JWEInvalid(
|
|
128
|
+
throw new JWEInvalid(`"${headers[1]}" alg may only have a single recipient`);
|
|
129
129
|
}
|
|
130
130
|
if (!enc) {
|
|
131
131
|
enc = headers[2];
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jwkToKey } from '../lib/jwk_to_key.js';
|
|
2
|
-
import {
|
|
2
|
+
import { JWS } from '../lib/jws_algorithms.js';
|
|
3
3
|
import { JWKSInvalid, JOSENotSupported, JWKSNoMatchingKey, JWKSMultipleMatchingKeys, } from '../util/errors.js';
|
|
4
4
|
import { isObject } from '../lib/type_checks.js';
|
|
5
5
|
function signatureAlgorithm(alg) {
|
|
6
|
-
const entry = typeof alg === 'string' ?
|
|
6
|
+
const entry = typeof alg === 'string' ? JWS[alg] : undefined;
|
|
7
7
|
if (!entry || entry.secret) {
|
|
8
8
|
throw new JOSENotSupported('Unsupported "alg" value for a JSON Web Key Set');
|
|
9
9
|
}
|
|
@@ -58,7 +58,7 @@ class LocalJWKSetImpl {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
async function importWithAlgCache(cache, jwk, entry) {
|
|
61
|
-
const cached = cache.get(jwk) || cache.set(jwk, {}).get(jwk);
|
|
61
|
+
const cached = cache.get(jwk) || cache.set(jwk, { __proto__: null }).get(jwk);
|
|
62
62
|
if (cached[entry.alg] === undefined) {
|
|
63
63
|
const key = await jwkToKey(entry, { ...jwk, alg: entry.alg, ext: true });
|
|
64
64
|
if (key.type !== 'public') {
|
|
@@ -9,7 +9,7 @@ function isCloudflareWorkers() {
|
|
|
9
9
|
let USER_AGENT;
|
|
10
10
|
if (typeof navigator === 'undefined' || !navigator.userAgent?.startsWith?.('Mozilla/5.0 ')) {
|
|
11
11
|
const NAME = 'jose';
|
|
12
|
-
const VERSION = 'v6.2.
|
|
12
|
+
const VERSION = 'v6.2.8';
|
|
13
13
|
USER_AGENT = `${NAME}/${VERSION}`;
|
|
14
14
|
}
|
|
15
15
|
export const customFetch = Symbol();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { JOSENotSupported } from '../util/errors.js';
|
|
2
|
-
import { keyAlgorithm } from '../lib/key_algorithm.js';
|
|
2
|
+
import { keyAlgorithm, unsupportedAlg, algArgument } from '../lib/key_algorithm.js';
|
|
3
3
|
function getModulusLengthOption(options) {
|
|
4
4
|
const modulusLength = options?.modulusLength ?? 2048;
|
|
5
5
|
if (typeof modulusLength !== 'number' || modulusLength < 2048) {
|
|
@@ -8,9 +8,9 @@ function getModulusLengthOption(options) {
|
|
|
8
8
|
return modulusLength;
|
|
9
9
|
}
|
|
10
10
|
export async function generateKeyPair(alg, options) {
|
|
11
|
-
const entry = keyAlgorithm(alg);
|
|
11
|
+
const entry = keyAlgorithm(alg, algArgument);
|
|
12
12
|
if (entry.secret) {
|
|
13
|
-
|
|
13
|
+
unsupportedAlg(algArgument);
|
|
14
14
|
}
|
|
15
15
|
let algorithm;
|
|
16
16
|
if (entry.resolve) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { unsupportedAlg, algArgument } from '../lib/key_algorithm.js';
|
|
2
2
|
export async function generateSecret(alg, options) {
|
|
3
3
|
let length;
|
|
4
4
|
let algorithm;
|
|
@@ -34,7 +34,7 @@ export async function generateSecret(alg, options) {
|
|
|
34
34
|
keyUsages = ['encrypt', 'decrypt'];
|
|
35
35
|
break;
|
|
36
36
|
default:
|
|
37
|
-
|
|
37
|
+
unsupportedAlg(algArgument);
|
|
38
38
|
}
|
|
39
39
|
return crypto.subtle.generateKey(algorithm, options?.extractable ?? false, keyUsages);
|
|
40
40
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { invalidKeyInput } from './invalid_key_input.js';
|
|
2
2
|
import { encodeBase64, decodeBase64 } from '../lib/base64.js';
|
|
3
3
|
import { JOSENotSupported } from '../util/errors.js';
|
|
4
|
-
import { keyAlgorithm } from './key_algorithm.js';
|
|
4
|
+
import { keyAlgorithm, unsupportedAlg, algArgument } from './key_algorithm.js';
|
|
5
5
|
import { isCryptoKey, isKeyObject } from './is_key_like.js';
|
|
6
6
|
const formatPEM = (b64, descriptor) => {
|
|
7
7
|
const newlined = (b64.match(/.{1,64}/g) || []).join('\n');
|
|
@@ -112,9 +112,9 @@ const parseECAlgorithmIdentifier = (state) => {
|
|
|
112
112
|
throw new Error('Unsupported named curve');
|
|
113
113
|
};
|
|
114
114
|
const genericImport = async (keyFormat, keyData, alg, options) => {
|
|
115
|
-
const entry = keyAlgorithm(alg);
|
|
115
|
+
const entry = keyAlgorithm(alg, algArgument);
|
|
116
116
|
if (entry.secret) {
|
|
117
|
-
|
|
117
|
+
unsupportedAlg(algArgument);
|
|
118
118
|
}
|
|
119
119
|
const isPublic = keyFormat === 'spki';
|
|
120
120
|
let algorithm;
|