@suveren/gateway 0.6.2 → 0.6.3

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.
Files changed (35) hide show
  1. package/content/integrations/calendar.json +6 -3
  2. package/content/integrations/crm.json +6 -3
  3. package/content/integrations/deploy-github.json +2 -1
  4. package/content/integrations/gmail.json +16 -6
  5. package/content/integrations/linkedin.json +75 -16
  6. package/content/integrations/records.json +76 -14
  7. package/dist/mcp-server/http.mjs +169 -27
  8. package/dist/ui/assets/index-DIcuh0I4.css +1 -0
  9. package/dist/ui/assets/index-Dl5isaOV.js +105 -0
  10. package/dist/ui/index.html +2 -2
  11. package/dist/ui/mockups/audit-and-header.html +192 -0
  12. package/node_modules/@hap/core/dist/index.d.mts +117 -6
  13. package/node_modules/@hap/core/dist/index.d.ts +117 -6
  14. package/node_modules/@hap/core/dist/index.js +93 -0
  15. package/node_modules/@hap/core/dist/index.mjs +88 -0
  16. package/node_modules/@hap/core/package.json +1 -1
  17. package/node_modules/@hap/core/src/content-binding.ts +168 -0
  18. package/node_modules/@hap/core/src/types.ts +62 -5
  19. package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +8 -0
  20. package/node_modules/jose/dist/webapi/jwe/general/encrypt.js +1 -1
  21. package/node_modules/jose/dist/webapi/jwks/local.js +3 -3
  22. package/node_modules/jose/dist/webapi/jwks/remote.js +1 -1
  23. package/node_modules/jose/dist/webapi/key/generate_key_pair.js +3 -3
  24. package/node_modules/jose/dist/webapi/key/generate_secret.js +2 -2
  25. package/node_modules/jose/dist/webapi/lib/asn1.js +3 -3
  26. package/node_modules/jose/dist/webapi/lib/jwe_algorithms.js +6 -15
  27. package/node_modules/jose/dist/webapi/lib/jws_algorithms.js +2 -5
  28. package/node_modules/jose/dist/webapi/lib/key.js +1 -1
  29. package/node_modules/jose/dist/webapi/lib/key_algorithm.js +8 -8
  30. package/node_modules/jose/package.json +1 -1
  31. package/package.json +2 -2
  32. package/profiles/email/0.5.profile.json +200 -0
  33. package/profiles/index.json +1 -0
  34. package/dist/ui/assets/index-DoZvxHLN.js +0 -105
  35. 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,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanagencyp/hap-core",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Core types, cryptographic primitives, and verification logic for the Human Agency Protocol",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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
- * The profile declares only the *policy* — whether to bind and how to
275
- * canonicalize. It does NOT name the tool field: that is tool-specific and is
276
- * resolved at runtime (the same content-field resolver the footer uses for
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,21 @@ 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
+
373
430
  /**
374
431
  * v0.3 frame schema (deprecated, kept for backward compat).
375
432
  * 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('"dir" and "ECDH-ES" alg may only be used with a single recipient');
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 { maybeJWSAlgorithm } from '../lib/jws_algorithms.js';
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' ? maybeJWSAlgorithm(alg) : undefined;
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.7';
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
- throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
13
+ unsupportedAlg(algArgument);
14
14
  }
15
15
  let algorithm;
16
16
  if (entry.resolve) {
@@ -1,4 +1,4 @@
1
- import { JOSENotSupported } from '../util/errors.js';
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
- throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
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
- throw new JOSENotSupported('Invalid or unsupported "alg" (Algorithm) value');
117
+ unsupportedAlg(algArgument);
118
118
  }
119
119
  const isPublic = keyFormat === 'spki';
120
120
  let algorithm;