@humanagencyp/hap-core 0.8.0 → 0.8.1
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/dist/index.d.mts +103 -6
- package/dist/index.d.ts +103 -6
- package/dist/index.js +93 -0
- package/dist/index.mjs +88 -0
- package/package.json +1 -1
- package/src/content-binding.ts +168 -0
- package/src/types.ts +47 -5
package/dist/index.d.mts
CHANGED
|
@@ -267,22 +267,63 @@ interface ProfileBoundsField {
|
|
|
267
267
|
* backward compatibility). The gateway computes the hash; the SP only ever
|
|
268
268
|
* receives the hash, never the content, so HAP's privacy-minimal design holds.
|
|
269
269
|
*
|
|
270
|
-
*
|
|
271
|
-
* canonicalize. It does NOT name the tool field: that is tool-specific
|
|
272
|
-
* resolved at runtime (the same content-field resolver the footer uses
|
|
273
|
-
* `kind:"text"`; the whole record payload for `kind:"jcs"`).
|
|
270
|
+
* At `version:"1"` the profile declares only the *policy* — whether to bind and
|
|
271
|
+
* how to canonicalize. It does NOT name the tool field: that is tool-specific
|
|
272
|
+
* and is resolved at runtime (the same content-field resolver the footer uses
|
|
273
|
+
* for `kind:"text"`; the whole record payload for `kind:"jcs"`).
|
|
274
|
+
*
|
|
275
|
+
* At `version:"2"` the profile additionally declares WHICH fields are bound (see
|
|
276
|
+
* {@link ContentBinding.fields}). Neither v1 mode is the general case: `text`
|
|
277
|
+
* binds one field and leaves everything beside it unbound, while `jcs` over the
|
|
278
|
+
* whole payload is checkable only by a party that already knows the whole
|
|
279
|
+
* payload — an email recipient holds the body, the subject and their own
|
|
280
|
+
* address, but not `bcc`. The general case is a declared subset, chosen so the
|
|
281
|
+
* intended verifier can reproduce it.
|
|
274
282
|
*/
|
|
275
283
|
interface ContentBinding {
|
|
276
284
|
/** Canonicalization version. A verifier MUST pin the version named here. */
|
|
277
285
|
version: string;
|
|
278
286
|
/**
|
|
279
|
-
* - 'jcs' → structured writes: RFC 8785 JCS over the record payload
|
|
287
|
+
* - 'jcs' → structured writes: RFC 8785 JCS over the record payload
|
|
288
|
+
* (v1) or over the object built from {@link fields} (v2).
|
|
280
289
|
* - 'text' → free text: NFC + LF + trailing-whitespace strip (see
|
|
281
290
|
* canonicalizeText), auto-detected content field.
|
|
282
291
|
*/
|
|
283
292
|
kind: 'jcs' | 'text';
|
|
284
293
|
/** text only: hash the content BEFORE any appended Suveren footer. */
|
|
285
294
|
pre_footer?: boolean;
|
|
295
|
+
/**
|
|
296
|
+
* v2 only — the tool-argument keys this binding covers, and the complete
|
|
297
|
+
* statement of what a verifier must reproduce. The Gatekeeper builds an
|
|
298
|
+
* object from exactly these keys and canonicalizes it by `kind`.
|
|
299
|
+
*
|
|
300
|
+
* Adding or removing an entry changes every resulting hash, so it is a
|
|
301
|
+
* BREAKING profile change requiring a version bump, never a silent edit.
|
|
302
|
+
*
|
|
303
|
+
* Choose the subset by one rule: bind everything the approving human is
|
|
304
|
+
* shown, and nothing the intended verifier cannot see.
|
|
305
|
+
*/
|
|
306
|
+
fields?: string[];
|
|
307
|
+
/**
|
|
308
|
+
* v2 only — the subset of {@link fields} whose absence is a fault rather than
|
|
309
|
+
* a fact. An absent OPTIONAL field is omitted from the hashed object (an
|
|
310
|
+
* email legitimately has no `cc`); an absent REQUIRED field means the call is
|
|
311
|
+
* not the call this profile thinks it is, and MUST refuse rather than hash a
|
|
312
|
+
* partial object that reads exactly like a complete one.
|
|
313
|
+
*
|
|
314
|
+
* MUST be a subset of `fields`. Absent → every field is optional, and only a
|
|
315
|
+
* wholly empty selection refuses.
|
|
316
|
+
*/
|
|
317
|
+
required_fields?: string[];
|
|
318
|
+
/**
|
|
319
|
+
* v2 only — the action types this binding covers, using the same vocabulary
|
|
320
|
+
* as {@link ProfileBoundsField.appliesTo}. A profile gates more than its
|
|
321
|
+
* content-bearing calls: `email` also gates deletes, which carry an id and no
|
|
322
|
+
* content, and applying a field binding to those would refuse them.
|
|
323
|
+
*
|
|
324
|
+
* Absent → the binding applies to every gated action under the profile.
|
|
325
|
+
*/
|
|
326
|
+
appliesTo?: string[];
|
|
286
327
|
}
|
|
287
328
|
/**
|
|
288
329
|
* Context field definition within a v0.4 profile.
|
|
@@ -596,6 +637,12 @@ declare function canonicalize(value: unknown): string;
|
|
|
596
637
|
* (Unicode NFC, LF line endings, trailing per-line whitespace stripped,
|
|
597
638
|
* trailing blank lines removed), taken pre-footer when `pre_footer` is set.
|
|
598
639
|
*
|
|
640
|
+
* At `version:"2"` the profile also declares WHICH tool arguments are bound
|
|
641
|
+
* ({@link selectBoundFields}), so a receipt can commit to an email's recipients
|
|
642
|
+
* and not only its prose — while still omitting what the intended verifier
|
|
643
|
+
* cannot see. Every string entering the hashed object is canonicalized by the
|
|
644
|
+
* same `text` rule, so a delivered copy with CRLF endings still reproduces it.
|
|
645
|
+
*
|
|
599
646
|
* Both Node and the browser produce byte-identical output: JCS relies only on
|
|
600
647
|
* environment-independent primitives, and the text rule uses String.normalize +
|
|
601
648
|
* plain string ops. The SHA-256 is computed with Node `crypto` here (the same
|
|
@@ -626,6 +673,56 @@ declare function contentCanonicalBytes(kind: ContentBinding['kind'], content: Re
|
|
|
626
673
|
* @param content the record payload (jcs) or the resolved text field (text)
|
|
627
674
|
*/
|
|
628
675
|
declare function computeContentHash(binding: ContentBinding, content: Record<string, unknown> | string): string;
|
|
676
|
+
/** Why a field binding refused. Every case is fail-closed by design. */
|
|
677
|
+
type ContentBindingErrorCode =
|
|
678
|
+
/** The profile declares version:"2" with no usable `fields` list. */
|
|
679
|
+
'NO_FIELDS_DECLARED'
|
|
680
|
+
/** `required_fields` names something absent from `fields`. */
|
|
681
|
+
| 'REQUIRED_FIELD_NOT_DECLARED'
|
|
682
|
+
/** A required field was absent or empty at call time. */
|
|
683
|
+
| 'MISSING_REQUIRED_FIELD'
|
|
684
|
+
/** No declared field carried a value — the hash would commit to nothing. */
|
|
685
|
+
| 'EMPTY_BINDING';
|
|
686
|
+
/**
|
|
687
|
+
* A field binding could not be computed. ALWAYS a refusal, never a downgrade:
|
|
688
|
+
* the alternative is a receipt that verifies while proving less than it appears
|
|
689
|
+
* to, which is the failure content binding exists to prevent.
|
|
690
|
+
*/
|
|
691
|
+
declare class ContentBindingError extends Error {
|
|
692
|
+
readonly code: ContentBindingErrorCode;
|
|
693
|
+
/** The offending field, when the code names one. */
|
|
694
|
+
readonly field?: string;
|
|
695
|
+
constructor(code: ContentBindingErrorCode, message: string, field?: string);
|
|
696
|
+
}
|
|
697
|
+
/** True when this binding selects a declared subset (v2) rather than v1's implicit scope. */
|
|
698
|
+
declare function isFieldBinding(binding: ContentBinding): boolean;
|
|
699
|
+
/**
|
|
700
|
+
* Whether a field binding covers this action type, per the profile's `appliesTo`.
|
|
701
|
+
*
|
|
702
|
+
* Read STRICTLY — an undeclared action type is NOT covered. This differs from
|
|
703
|
+
* how bounds read the same key (there, an unknown action type enforces the
|
|
704
|
+
* bound, because an extra limit is safe). Here the two directions are not
|
|
705
|
+
* symmetric: applying a field binding to a call that carries no content refuses
|
|
706
|
+
* a legitimate action, so an unknown action type must fall outside rather than
|
|
707
|
+
* inside. Callers are expected to warn on the undeclared case — it is a
|
|
708
|
+
* manifest bug either way.
|
|
709
|
+
*/
|
|
710
|
+
declare function bindingAppliesTo(binding: ContentBinding, actionType: string | undefined): boolean;
|
|
711
|
+
/**
|
|
712
|
+
* Build the object a v2 binding hashes: exactly the declared `fields` that
|
|
713
|
+
* carry a value, canonicalized. Exported so a verifier can construct the same
|
|
714
|
+
* object from what they hold and see it before hashing.
|
|
715
|
+
*
|
|
716
|
+
* Throws {@link ContentBindingError} rather than returning a partial result —
|
|
717
|
+
* see that class for why refusing is the only safe outcome.
|
|
718
|
+
*/
|
|
719
|
+
declare function selectBoundFields(binding: ContentBinding, args: Record<string, unknown>): Record<string, unknown>;
|
|
720
|
+
/**
|
|
721
|
+
* Compute a v2 field-binding hash from raw tool arguments: select the declared
|
|
722
|
+
* subset, then hash it by the declared `kind`. Convenience over
|
|
723
|
+
* {@link selectBoundFields} + {@link computeContentHash} for the common path.
|
|
724
|
+
*/
|
|
725
|
+
declare function computeFieldsContentHash(binding: ContentBinding, args: Record<string, unknown>): string;
|
|
629
726
|
|
|
630
727
|
/**
|
|
631
728
|
* Intent hashing + intent-disclosure binding (HAP v0.5).
|
|
@@ -917,4 +1014,4 @@ declare function listProfiles(): string[];
|
|
|
917
1014
|
declare function getAllProfiles(): AgentProfile[];
|
|
918
1015
|
declare function clearProfiles(): void;
|
|
919
1016
|
|
|
920
|
-
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, type ContentBinding, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ResolvedDomain, type Subject, type SubjectValidation, attestationId, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, deriveIdentityLine, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, validateSubject, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
|
|
1017
|
+
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, type ContentBinding, ContentBindingError, type ContentBindingErrorCode, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ResolvedDomain, type Subject, type SubjectValidation, attestationId, bindingAppliesTo, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFieldsContentHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, deriveIdentityLine, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isFieldBinding, isV4Attestation, listProfiles, registerProfile, selectBoundFields, validateBoundsParams, validateContextParams, validateFrameParams, validateSubject, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
|
package/dist/index.d.ts
CHANGED
|
@@ -267,22 +267,63 @@ interface ProfileBoundsField {
|
|
|
267
267
|
* backward compatibility). The gateway computes the hash; the SP only ever
|
|
268
268
|
* receives the hash, never the content, so HAP's privacy-minimal design holds.
|
|
269
269
|
*
|
|
270
|
-
*
|
|
271
|
-
* canonicalize. It does NOT name the tool field: that is tool-specific
|
|
272
|
-
* resolved at runtime (the same content-field resolver the footer uses
|
|
273
|
-
* `kind:"text"`; the whole record payload for `kind:"jcs"`).
|
|
270
|
+
* At `version:"1"` the profile declares only the *policy* — whether to bind and
|
|
271
|
+
* how to canonicalize. It does NOT name the tool field: that is tool-specific
|
|
272
|
+
* and is resolved at runtime (the same content-field resolver the footer uses
|
|
273
|
+
* for `kind:"text"`; the whole record payload for `kind:"jcs"`).
|
|
274
|
+
*
|
|
275
|
+
* At `version:"2"` the profile additionally declares WHICH fields are bound (see
|
|
276
|
+
* {@link ContentBinding.fields}). Neither v1 mode is the general case: `text`
|
|
277
|
+
* binds one field and leaves everything beside it unbound, while `jcs` over the
|
|
278
|
+
* whole payload is checkable only by a party that already knows the whole
|
|
279
|
+
* payload — an email recipient holds the body, the subject and their own
|
|
280
|
+
* address, but not `bcc`. The general case is a declared subset, chosen so the
|
|
281
|
+
* intended verifier can reproduce it.
|
|
274
282
|
*/
|
|
275
283
|
interface ContentBinding {
|
|
276
284
|
/** Canonicalization version. A verifier MUST pin the version named here. */
|
|
277
285
|
version: string;
|
|
278
286
|
/**
|
|
279
|
-
* - 'jcs' → structured writes: RFC 8785 JCS over the record payload
|
|
287
|
+
* - 'jcs' → structured writes: RFC 8785 JCS over the record payload
|
|
288
|
+
* (v1) or over the object built from {@link fields} (v2).
|
|
280
289
|
* - 'text' → free text: NFC + LF + trailing-whitespace strip (see
|
|
281
290
|
* canonicalizeText), auto-detected content field.
|
|
282
291
|
*/
|
|
283
292
|
kind: 'jcs' | 'text';
|
|
284
293
|
/** text only: hash the content BEFORE any appended Suveren footer. */
|
|
285
294
|
pre_footer?: boolean;
|
|
295
|
+
/**
|
|
296
|
+
* v2 only — the tool-argument keys this binding covers, and the complete
|
|
297
|
+
* statement of what a verifier must reproduce. The Gatekeeper builds an
|
|
298
|
+
* object from exactly these keys and canonicalizes it by `kind`.
|
|
299
|
+
*
|
|
300
|
+
* Adding or removing an entry changes every resulting hash, so it is a
|
|
301
|
+
* BREAKING profile change requiring a version bump, never a silent edit.
|
|
302
|
+
*
|
|
303
|
+
* Choose the subset by one rule: bind everything the approving human is
|
|
304
|
+
* shown, and nothing the intended verifier cannot see.
|
|
305
|
+
*/
|
|
306
|
+
fields?: string[];
|
|
307
|
+
/**
|
|
308
|
+
* v2 only — the subset of {@link fields} whose absence is a fault rather than
|
|
309
|
+
* a fact. An absent OPTIONAL field is omitted from the hashed object (an
|
|
310
|
+
* email legitimately has no `cc`); an absent REQUIRED field means the call is
|
|
311
|
+
* not the call this profile thinks it is, and MUST refuse rather than hash a
|
|
312
|
+
* partial object that reads exactly like a complete one.
|
|
313
|
+
*
|
|
314
|
+
* MUST be a subset of `fields`. Absent → every field is optional, and only a
|
|
315
|
+
* wholly empty selection refuses.
|
|
316
|
+
*/
|
|
317
|
+
required_fields?: string[];
|
|
318
|
+
/**
|
|
319
|
+
* v2 only — the action types this binding covers, using the same vocabulary
|
|
320
|
+
* as {@link ProfileBoundsField.appliesTo}. A profile gates more than its
|
|
321
|
+
* content-bearing calls: `email` also gates deletes, which carry an id and no
|
|
322
|
+
* content, and applying a field binding to those would refuse them.
|
|
323
|
+
*
|
|
324
|
+
* Absent → the binding applies to every gated action under the profile.
|
|
325
|
+
*/
|
|
326
|
+
appliesTo?: string[];
|
|
286
327
|
}
|
|
287
328
|
/**
|
|
288
329
|
* Context field definition within a v0.4 profile.
|
|
@@ -596,6 +637,12 @@ declare function canonicalize(value: unknown): string;
|
|
|
596
637
|
* (Unicode NFC, LF line endings, trailing per-line whitespace stripped,
|
|
597
638
|
* trailing blank lines removed), taken pre-footer when `pre_footer` is set.
|
|
598
639
|
*
|
|
640
|
+
* At `version:"2"` the profile also declares WHICH tool arguments are bound
|
|
641
|
+
* ({@link selectBoundFields}), so a receipt can commit to an email's recipients
|
|
642
|
+
* and not only its prose — while still omitting what the intended verifier
|
|
643
|
+
* cannot see. Every string entering the hashed object is canonicalized by the
|
|
644
|
+
* same `text` rule, so a delivered copy with CRLF endings still reproduces it.
|
|
645
|
+
*
|
|
599
646
|
* Both Node and the browser produce byte-identical output: JCS relies only on
|
|
600
647
|
* environment-independent primitives, and the text rule uses String.normalize +
|
|
601
648
|
* plain string ops. The SHA-256 is computed with Node `crypto` here (the same
|
|
@@ -626,6 +673,56 @@ declare function contentCanonicalBytes(kind: ContentBinding['kind'], content: Re
|
|
|
626
673
|
* @param content the record payload (jcs) or the resolved text field (text)
|
|
627
674
|
*/
|
|
628
675
|
declare function computeContentHash(binding: ContentBinding, content: Record<string, unknown> | string): string;
|
|
676
|
+
/** Why a field binding refused. Every case is fail-closed by design. */
|
|
677
|
+
type ContentBindingErrorCode =
|
|
678
|
+
/** The profile declares version:"2" with no usable `fields` list. */
|
|
679
|
+
'NO_FIELDS_DECLARED'
|
|
680
|
+
/** `required_fields` names something absent from `fields`. */
|
|
681
|
+
| 'REQUIRED_FIELD_NOT_DECLARED'
|
|
682
|
+
/** A required field was absent or empty at call time. */
|
|
683
|
+
| 'MISSING_REQUIRED_FIELD'
|
|
684
|
+
/** No declared field carried a value — the hash would commit to nothing. */
|
|
685
|
+
| 'EMPTY_BINDING';
|
|
686
|
+
/**
|
|
687
|
+
* A field binding could not be computed. ALWAYS a refusal, never a downgrade:
|
|
688
|
+
* the alternative is a receipt that verifies while proving less than it appears
|
|
689
|
+
* to, which is the failure content binding exists to prevent.
|
|
690
|
+
*/
|
|
691
|
+
declare class ContentBindingError extends Error {
|
|
692
|
+
readonly code: ContentBindingErrorCode;
|
|
693
|
+
/** The offending field, when the code names one. */
|
|
694
|
+
readonly field?: string;
|
|
695
|
+
constructor(code: ContentBindingErrorCode, message: string, field?: string);
|
|
696
|
+
}
|
|
697
|
+
/** True when this binding selects a declared subset (v2) rather than v1's implicit scope. */
|
|
698
|
+
declare function isFieldBinding(binding: ContentBinding): boolean;
|
|
699
|
+
/**
|
|
700
|
+
* Whether a field binding covers this action type, per the profile's `appliesTo`.
|
|
701
|
+
*
|
|
702
|
+
* Read STRICTLY — an undeclared action type is NOT covered. This differs from
|
|
703
|
+
* how bounds read the same key (there, an unknown action type enforces the
|
|
704
|
+
* bound, because an extra limit is safe). Here the two directions are not
|
|
705
|
+
* symmetric: applying a field binding to a call that carries no content refuses
|
|
706
|
+
* a legitimate action, so an unknown action type must fall outside rather than
|
|
707
|
+
* inside. Callers are expected to warn on the undeclared case — it is a
|
|
708
|
+
* manifest bug either way.
|
|
709
|
+
*/
|
|
710
|
+
declare function bindingAppliesTo(binding: ContentBinding, actionType: string | undefined): boolean;
|
|
711
|
+
/**
|
|
712
|
+
* Build the object a v2 binding hashes: exactly the declared `fields` that
|
|
713
|
+
* carry a value, canonicalized. Exported so a verifier can construct the same
|
|
714
|
+
* object from what they hold and see it before hashing.
|
|
715
|
+
*
|
|
716
|
+
* Throws {@link ContentBindingError} rather than returning a partial result —
|
|
717
|
+
* see that class for why refusing is the only safe outcome.
|
|
718
|
+
*/
|
|
719
|
+
declare function selectBoundFields(binding: ContentBinding, args: Record<string, unknown>): Record<string, unknown>;
|
|
720
|
+
/**
|
|
721
|
+
* Compute a v2 field-binding hash from raw tool arguments: select the declared
|
|
722
|
+
* subset, then hash it by the declared `kind`. Convenience over
|
|
723
|
+
* {@link selectBoundFields} + {@link computeContentHash} for the common path.
|
|
724
|
+
*/
|
|
725
|
+
declare function computeFieldsContentHash(binding: ContentBinding, args: Record<string, unknown>): string;
|
|
629
726
|
|
|
630
727
|
/**
|
|
631
728
|
* Intent hashing + intent-disclosure binding (HAP v0.5).
|
|
@@ -917,4 +1014,4 @@ declare function listProfiles(): string[];
|
|
|
917
1014
|
declare function getAllProfiles(): AgentProfile[];
|
|
918
1015
|
declare function clearProfiles(): void;
|
|
919
1016
|
|
|
920
|
-
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, type ContentBinding, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ResolvedDomain, type Subject, type SubjectValidation, attestationId, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, deriveIdentityLine, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, validateSubject, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
|
|
1017
|
+
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, type ContentBinding, ContentBindingError, type ContentBindingErrorCode, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ResolvedDomain, type Subject, type SubjectValidation, attestationId, bindingAppliesTo, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFieldsContentHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, deriveIdentityLine, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isFieldBinding, isV4Attestation, listProfiles, registerProfile, selectBoundFields, validateBoundsParams, validateContextParams, validateFrameParams, validateSubject, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
|
package/dist/index.js
CHANGED
|
@@ -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,
|
package/dist/index.mjs
CHANGED
|
@@ -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,
|
package/package.json
CHANGED
package/src/content-binding.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -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
|
/**
|