@humanagencyp/hap-core 0.10.1 → 0.11.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 +91 -1
- package/dist/index.d.ts +91 -1
- package/dist/index.js +85 -0
- package/dist/index.mjs +80 -0
- package/package.json +1 -1
- package/src/commitment-modes.ts +51 -0
- package/src/frame.ts +63 -1
- package/src/gatekeeper.ts +37 -0
- package/src/index.ts +1 -0
- package/src/types.ts +48 -1
package/dist/index.d.mts
CHANGED
|
@@ -205,10 +205,29 @@ type BoundType =
|
|
|
205
205
|
* Per-transaction cap. The execution context field named in `of` must
|
|
206
206
|
* satisfy `execution[of] <= bound` for the current call. No cumulative
|
|
207
207
|
* tracking. Used by: amount_max, recipient_max, booking_duration_max, etc.
|
|
208
|
+
*
|
|
209
|
+
* `requiredFor` (v0.8+) closes the converse: without it, a call that does
|
|
210
|
+
* not carry `of` at all is simply skipped — a 5,000 cap refuses 6,000 but
|
|
211
|
+
* permits a call that declares no value whatsoever, which is indistinguishable
|
|
212
|
+
* from an unenforced bound. The same hole, and the same fix, as
|
|
213
|
+
* `FieldConstraint.requiredFor` on a scope constraint (below): values drawn
|
|
214
|
+
* from the profile's `boundsSchema.actionTypes` registry. For a listed
|
|
215
|
+
* `action_type`, an execution whose context lacks `of` — or whose value is
|
|
216
|
+
* not a finite number — MUST be refused rather than skipped. For an
|
|
217
|
+
* unlisted (or when `requiredFor` is absent) action type, today's
|
|
218
|
+
* skip-on-absence behaviour is unchanged. Meaningful only on
|
|
219
|
+
* `per_transaction`; the other `BoundType` kinds have no per-call "value
|
|
220
|
+
* exposed or not" question — `cumulative_sum`/`cumulative_count` always read
|
|
221
|
+
* from the execution log regardless of what this call declares, and `enum`
|
|
222
|
+
* is a capability flag, not a runtime value — so the field does not exist
|
|
223
|
+
* on those variants and a profile that puts it there has it silently
|
|
224
|
+
* ignored by every enforcement point (TypeScript also refuses it when the
|
|
225
|
+
* profile is authored against this type rather than raw JSON).
|
|
208
226
|
*/
|
|
209
227
|
{
|
|
210
228
|
kind: 'per_transaction';
|
|
211
229
|
of: string;
|
|
230
|
+
requiredFor?: string[];
|
|
212
231
|
}
|
|
213
232
|
/**
|
|
214
233
|
* Cumulative sum within a time window. The SP maintains a running sum
|
|
@@ -448,6 +467,11 @@ interface ExecutionPath {
|
|
|
448
467
|
*
|
|
449
468
|
* Supports both v0.3 (frameSchema) and v0.4 (boundsSchema + contextSchema).
|
|
450
469
|
*/
|
|
470
|
+
/**
|
|
471
|
+
* A commitment mode a person can choose when signing a mandate. (`review_above_cap`
|
|
472
|
+
* is not chosen by the signer — it follows from team caps.)
|
|
473
|
+
*/
|
|
474
|
+
type SignableCommitmentMode = 'automatic' | 'review';
|
|
451
475
|
interface AgentProfile {
|
|
452
476
|
id: string;
|
|
453
477
|
name?: string;
|
|
@@ -489,6 +513,18 @@ interface AgentProfile {
|
|
|
489
513
|
* a forwarded message has usually lost the footer that carried it.
|
|
490
514
|
*/
|
|
491
515
|
receipt_lookup?: boolean;
|
|
516
|
+
/**
|
|
517
|
+
* The commitment modes a mandate under this profile may be signed with.
|
|
518
|
+
* Absent → every mode (`automatic`, `review`), as before this field existed.
|
|
519
|
+
*
|
|
520
|
+
* Lets a profile be review-only: every action it governs then needs a
|
|
521
|
+
* person's approval, whatever the signer would have picked. The Authority
|
|
522
|
+
* Server refuses to sign a mandate whose `commitment_mode` is not listed;
|
|
523
|
+
* a mandate screen offers only the listed modes. Fail-closed: a declared
|
|
524
|
+
* list with no valid entry allows nothing — a malformed declaration never
|
|
525
|
+
* widens what a profile allows. See {@link allowedCommitmentModes}.
|
|
526
|
+
*/
|
|
527
|
+
commitment_modes?: readonly SignableCommitmentMode[];
|
|
492
528
|
/**
|
|
493
529
|
* v0.3 frame schema (deprecated, kept for backward compat).
|
|
494
530
|
* Used when boundsSchema is not present.
|
|
@@ -503,6 +539,16 @@ interface AgentProfile {
|
|
|
503
539
|
boundsSchema?: {
|
|
504
540
|
keyOrder: string[];
|
|
505
541
|
fields: Record<string, ProfileBoundsField>;
|
|
542
|
+
/**
|
|
543
|
+
* v0.5+ registry of the action types this profile recognizes (e.g.
|
|
544
|
+
* `["send", "delete", "setup"]`). Every `execution.action_type` the
|
|
545
|
+
* Authority Server and gateway accept for this profile MUST be a member
|
|
546
|
+
* when the registry is declared (protocol.md → Bounds Schema, rule 2);
|
|
547
|
+
* it is also what a `per_transaction` bound's `requiredFor` draws from.
|
|
548
|
+
* Absent on profiles published before the registry existed — membership
|
|
549
|
+
* is then uncheckable, not a violation.
|
|
550
|
+
*/
|
|
551
|
+
actionTypes?: readonly string[];
|
|
506
552
|
};
|
|
507
553
|
/**
|
|
508
554
|
* v0.4 context schema — defines the execution context parameters (e.g., currency, action_type).
|
|
@@ -983,6 +1029,25 @@ declare function validateContextParams(params: AgentContextParams, profile: Agen
|
|
|
983
1029
|
valid: boolean;
|
|
984
1030
|
errors: string[];
|
|
985
1031
|
};
|
|
1032
|
+
/**
|
|
1033
|
+
* Authoring-time check for `per_transaction` bound `requiredFor` (v0.8+):
|
|
1034
|
+
* entries must name a real `action_type` from the profile's declared
|
|
1035
|
+
* `boundsSchema.actionTypes` registry, and the field is only meaningful on
|
|
1036
|
+
* `per_transaction` bounds (see `BoundType`'s doc comment for why the other
|
|
1037
|
+
* kinds don't carry it).
|
|
1038
|
+
*
|
|
1039
|
+
* This is NOT wired into `registerProfile` or any enforcement path — nothing
|
|
1040
|
+
* in hap-core validates profile JSON at load time today (the pre-existing
|
|
1041
|
+
* `requiredFor` on a context/scope constraint has the same gap), so adding
|
|
1042
|
+
* an automatic refusal here would be new behaviour beyond this change's
|
|
1043
|
+
* scope. Call it explicitly from an authoring tool, a hap-profiles CI check,
|
|
1044
|
+
* or a test, to catch a `requiredFor` entry that can never match (misspelled
|
|
1045
|
+
* action type) or is attached to a bound kind that silently ignores it.
|
|
1046
|
+
*
|
|
1047
|
+
* Returns an empty array when the profile has no boundsSchema or nothing to
|
|
1048
|
+
* check — never throws.
|
|
1049
|
+
*/
|
|
1050
|
+
declare function validateBoundsRequiredFor(profile: AgentProfile): string[];
|
|
986
1051
|
/**
|
|
987
1052
|
* Thrown when a value cannot be canonicalized at all — currently only for raw
|
|
988
1053
|
* LF/CR inside a value. Carries the protocol error code so callers can map it
|
|
@@ -1033,6 +1098,31 @@ declare function computeBoundsHash(params: AgentBoundsParams, profile: AgentProf
|
|
|
1033
1098
|
*/
|
|
1034
1099
|
declare function computeContextHash(params: AgentContextParams, profile: AgentProfile): string;
|
|
1035
1100
|
|
|
1101
|
+
/**
|
|
1102
|
+
* Which commitment modes a profile allows its mandates to be signed with
|
|
1103
|
+
* (`AgentProfile.commitment_modes`). One source for the Authority Server's
|
|
1104
|
+
* refusal and the mandate screen's choice, so the two cannot disagree.
|
|
1105
|
+
*/
|
|
1106
|
+
|
|
1107
|
+
/** Every mode a signer can choose, in the order a mandate screen shows them. */
|
|
1108
|
+
declare const SIGNABLE_COMMITMENT_MODES: readonly SignableCommitmentMode[];
|
|
1109
|
+
/**
|
|
1110
|
+
* The modes a mandate under this profile may carry, in display order.
|
|
1111
|
+
* No declaration → all of them. A declaration is read fail-closed: entries
|
|
1112
|
+
* that are not a signable mode are dropped, so a malformed list can only
|
|
1113
|
+
* narrow — `[]`, `"review"` (not a list) or `["reveiw"]` allow nothing.
|
|
1114
|
+
*/
|
|
1115
|
+
declare function allowedCommitmentModes(profile: AgentProfile): readonly SignableCommitmentMode[];
|
|
1116
|
+
/** Whether a mandate under this profile may be signed with `mode`. */
|
|
1117
|
+
declare function isCommitmentModeAllowed(profile: AgentProfile, mode: string): boolean;
|
|
1118
|
+
/**
|
|
1119
|
+
* Authoring check for a profile's `commitment_modes` declaration — for
|
|
1120
|
+
* hap-profiles CI, an authoring tool or a test. Like `validateBoundsRequiredFor`
|
|
1121
|
+
* it is not wired into profile loading; enforcement reads the declaration
|
|
1122
|
+
* fail-closed through `allowedCommitmentModes`. Empty array = nothing to fix.
|
|
1123
|
+
*/
|
|
1124
|
+
declare function validateCommitmentModes(profile: AgentProfile): string[];
|
|
1125
|
+
|
|
1036
1126
|
/**
|
|
1037
1127
|
* Attestation Encoding, Decoding, and Verification
|
|
1038
1128
|
*
|
|
@@ -1353,4 +1443,4 @@ interface ReceiptPayload {
|
|
|
1353
1443
|
*/
|
|
1354
1444
|
declare function verifyReceiptSignature(receipt: ReceiptPayload, publicKeyHex: string): Promise<void>;
|
|
1355
1445
|
|
|
1356
|
-
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type ApprovalObject, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, CanonicalValueError, 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 MandateBinding, MandateError, type MandateProjection, type OwnerMandate, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ReceiptPayload, type ResolvedDomain, type SigningSurface, type Subject, type SubjectValidation, approvalSigningBytes, attestationId, bindingAppliesTo, boundActionTypes, buildMandateProjection, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFieldsContentHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, decodeDidKey, deriveIdentityLine, encodeAttestationBlob, encodeDidKey, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isFieldBinding, isKeyBearingDid, isV4Attestation, listProfiles, mandateSigningBytes, registerProfile, selectBoundFields, signApproval, signMandateProjection, validateBoundsParams, validateContextParams, validateFrameParams, validateSubject, verify, verifyApproval, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash, verifyOwnerMandate, verifyOwnerMandates, verifyReceiptSignature };
|
|
1446
|
+
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type ApprovalObject, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, CanonicalValueError, 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 MandateBinding, MandateError, type MandateProjection, type OwnerMandate, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ReceiptPayload, type ResolvedDomain, SIGNABLE_COMMITMENT_MODES, type SignableCommitmentMode, type SigningSurface, type Subject, type SubjectValidation, allowedCommitmentModes, approvalSigningBytes, attestationId, bindingAppliesTo, boundActionTypes, buildMandateProjection, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFieldsContentHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, decodeDidKey, deriveIdentityLine, encodeAttestationBlob, encodeDidKey, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isCommitmentModeAllowed, isFieldBinding, isKeyBearingDid, isV4Attestation, listProfiles, mandateSigningBytes, registerProfile, selectBoundFields, signApproval, signMandateProjection, validateBoundsParams, validateBoundsRequiredFor, validateCommitmentModes, validateContextParams, validateFrameParams, validateSubject, verify, verifyApproval, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash, verifyOwnerMandate, verifyOwnerMandates, verifyReceiptSignature };
|
package/dist/index.d.ts
CHANGED
|
@@ -205,10 +205,29 @@ type BoundType =
|
|
|
205
205
|
* Per-transaction cap. The execution context field named in `of` must
|
|
206
206
|
* satisfy `execution[of] <= bound` for the current call. No cumulative
|
|
207
207
|
* tracking. Used by: amount_max, recipient_max, booking_duration_max, etc.
|
|
208
|
+
*
|
|
209
|
+
* `requiredFor` (v0.8+) closes the converse: without it, a call that does
|
|
210
|
+
* not carry `of` at all is simply skipped — a 5,000 cap refuses 6,000 but
|
|
211
|
+
* permits a call that declares no value whatsoever, which is indistinguishable
|
|
212
|
+
* from an unenforced bound. The same hole, and the same fix, as
|
|
213
|
+
* `FieldConstraint.requiredFor` on a scope constraint (below): values drawn
|
|
214
|
+
* from the profile's `boundsSchema.actionTypes` registry. For a listed
|
|
215
|
+
* `action_type`, an execution whose context lacks `of` — or whose value is
|
|
216
|
+
* not a finite number — MUST be refused rather than skipped. For an
|
|
217
|
+
* unlisted (or when `requiredFor` is absent) action type, today's
|
|
218
|
+
* skip-on-absence behaviour is unchanged. Meaningful only on
|
|
219
|
+
* `per_transaction`; the other `BoundType` kinds have no per-call "value
|
|
220
|
+
* exposed or not" question — `cumulative_sum`/`cumulative_count` always read
|
|
221
|
+
* from the execution log regardless of what this call declares, and `enum`
|
|
222
|
+
* is a capability flag, not a runtime value — so the field does not exist
|
|
223
|
+
* on those variants and a profile that puts it there has it silently
|
|
224
|
+
* ignored by every enforcement point (TypeScript also refuses it when the
|
|
225
|
+
* profile is authored against this type rather than raw JSON).
|
|
208
226
|
*/
|
|
209
227
|
{
|
|
210
228
|
kind: 'per_transaction';
|
|
211
229
|
of: string;
|
|
230
|
+
requiredFor?: string[];
|
|
212
231
|
}
|
|
213
232
|
/**
|
|
214
233
|
* Cumulative sum within a time window. The SP maintains a running sum
|
|
@@ -448,6 +467,11 @@ interface ExecutionPath {
|
|
|
448
467
|
*
|
|
449
468
|
* Supports both v0.3 (frameSchema) and v0.4 (boundsSchema + contextSchema).
|
|
450
469
|
*/
|
|
470
|
+
/**
|
|
471
|
+
* A commitment mode a person can choose when signing a mandate. (`review_above_cap`
|
|
472
|
+
* is not chosen by the signer — it follows from team caps.)
|
|
473
|
+
*/
|
|
474
|
+
type SignableCommitmentMode = 'automatic' | 'review';
|
|
451
475
|
interface AgentProfile {
|
|
452
476
|
id: string;
|
|
453
477
|
name?: string;
|
|
@@ -489,6 +513,18 @@ interface AgentProfile {
|
|
|
489
513
|
* a forwarded message has usually lost the footer that carried it.
|
|
490
514
|
*/
|
|
491
515
|
receipt_lookup?: boolean;
|
|
516
|
+
/**
|
|
517
|
+
* The commitment modes a mandate under this profile may be signed with.
|
|
518
|
+
* Absent → every mode (`automatic`, `review`), as before this field existed.
|
|
519
|
+
*
|
|
520
|
+
* Lets a profile be review-only: every action it governs then needs a
|
|
521
|
+
* person's approval, whatever the signer would have picked. The Authority
|
|
522
|
+
* Server refuses to sign a mandate whose `commitment_mode` is not listed;
|
|
523
|
+
* a mandate screen offers only the listed modes. Fail-closed: a declared
|
|
524
|
+
* list with no valid entry allows nothing — a malformed declaration never
|
|
525
|
+
* widens what a profile allows. See {@link allowedCommitmentModes}.
|
|
526
|
+
*/
|
|
527
|
+
commitment_modes?: readonly SignableCommitmentMode[];
|
|
492
528
|
/**
|
|
493
529
|
* v0.3 frame schema (deprecated, kept for backward compat).
|
|
494
530
|
* Used when boundsSchema is not present.
|
|
@@ -503,6 +539,16 @@ interface AgentProfile {
|
|
|
503
539
|
boundsSchema?: {
|
|
504
540
|
keyOrder: string[];
|
|
505
541
|
fields: Record<string, ProfileBoundsField>;
|
|
542
|
+
/**
|
|
543
|
+
* v0.5+ registry of the action types this profile recognizes (e.g.
|
|
544
|
+
* `["send", "delete", "setup"]`). Every `execution.action_type` the
|
|
545
|
+
* Authority Server and gateway accept for this profile MUST be a member
|
|
546
|
+
* when the registry is declared (protocol.md → Bounds Schema, rule 2);
|
|
547
|
+
* it is also what a `per_transaction` bound's `requiredFor` draws from.
|
|
548
|
+
* Absent on profiles published before the registry existed — membership
|
|
549
|
+
* is then uncheckable, not a violation.
|
|
550
|
+
*/
|
|
551
|
+
actionTypes?: readonly string[];
|
|
506
552
|
};
|
|
507
553
|
/**
|
|
508
554
|
* v0.4 context schema — defines the execution context parameters (e.g., currency, action_type).
|
|
@@ -983,6 +1029,25 @@ declare function validateContextParams(params: AgentContextParams, profile: Agen
|
|
|
983
1029
|
valid: boolean;
|
|
984
1030
|
errors: string[];
|
|
985
1031
|
};
|
|
1032
|
+
/**
|
|
1033
|
+
* Authoring-time check for `per_transaction` bound `requiredFor` (v0.8+):
|
|
1034
|
+
* entries must name a real `action_type` from the profile's declared
|
|
1035
|
+
* `boundsSchema.actionTypes` registry, and the field is only meaningful on
|
|
1036
|
+
* `per_transaction` bounds (see `BoundType`'s doc comment for why the other
|
|
1037
|
+
* kinds don't carry it).
|
|
1038
|
+
*
|
|
1039
|
+
* This is NOT wired into `registerProfile` or any enforcement path — nothing
|
|
1040
|
+
* in hap-core validates profile JSON at load time today (the pre-existing
|
|
1041
|
+
* `requiredFor` on a context/scope constraint has the same gap), so adding
|
|
1042
|
+
* an automatic refusal here would be new behaviour beyond this change's
|
|
1043
|
+
* scope. Call it explicitly from an authoring tool, a hap-profiles CI check,
|
|
1044
|
+
* or a test, to catch a `requiredFor` entry that can never match (misspelled
|
|
1045
|
+
* action type) or is attached to a bound kind that silently ignores it.
|
|
1046
|
+
*
|
|
1047
|
+
* Returns an empty array when the profile has no boundsSchema or nothing to
|
|
1048
|
+
* check — never throws.
|
|
1049
|
+
*/
|
|
1050
|
+
declare function validateBoundsRequiredFor(profile: AgentProfile): string[];
|
|
986
1051
|
/**
|
|
987
1052
|
* Thrown when a value cannot be canonicalized at all — currently only for raw
|
|
988
1053
|
* LF/CR inside a value. Carries the protocol error code so callers can map it
|
|
@@ -1033,6 +1098,31 @@ declare function computeBoundsHash(params: AgentBoundsParams, profile: AgentProf
|
|
|
1033
1098
|
*/
|
|
1034
1099
|
declare function computeContextHash(params: AgentContextParams, profile: AgentProfile): string;
|
|
1035
1100
|
|
|
1101
|
+
/**
|
|
1102
|
+
* Which commitment modes a profile allows its mandates to be signed with
|
|
1103
|
+
* (`AgentProfile.commitment_modes`). One source for the Authority Server's
|
|
1104
|
+
* refusal and the mandate screen's choice, so the two cannot disagree.
|
|
1105
|
+
*/
|
|
1106
|
+
|
|
1107
|
+
/** Every mode a signer can choose, in the order a mandate screen shows them. */
|
|
1108
|
+
declare const SIGNABLE_COMMITMENT_MODES: readonly SignableCommitmentMode[];
|
|
1109
|
+
/**
|
|
1110
|
+
* The modes a mandate under this profile may carry, in display order.
|
|
1111
|
+
* No declaration → all of them. A declaration is read fail-closed: entries
|
|
1112
|
+
* that are not a signable mode are dropped, so a malformed list can only
|
|
1113
|
+
* narrow — `[]`, `"review"` (not a list) or `["reveiw"]` allow nothing.
|
|
1114
|
+
*/
|
|
1115
|
+
declare function allowedCommitmentModes(profile: AgentProfile): readonly SignableCommitmentMode[];
|
|
1116
|
+
/** Whether a mandate under this profile may be signed with `mode`. */
|
|
1117
|
+
declare function isCommitmentModeAllowed(profile: AgentProfile, mode: string): boolean;
|
|
1118
|
+
/**
|
|
1119
|
+
* Authoring check for a profile's `commitment_modes` declaration — for
|
|
1120
|
+
* hap-profiles CI, an authoring tool or a test. Like `validateBoundsRequiredFor`
|
|
1121
|
+
* it is not wired into profile loading; enforcement reads the declaration
|
|
1122
|
+
* fail-closed through `allowedCommitmentModes`. Empty array = nothing to fix.
|
|
1123
|
+
*/
|
|
1124
|
+
declare function validateCommitmentModes(profile: AgentProfile): string[];
|
|
1125
|
+
|
|
1036
1126
|
/**
|
|
1037
1127
|
* Attestation Encoding, Decoding, and Verification
|
|
1038
1128
|
*
|
|
@@ -1353,4 +1443,4 @@ interface ReceiptPayload {
|
|
|
1353
1443
|
*/
|
|
1354
1444
|
declare function verifyReceiptSignature(receipt: ReceiptPayload, publicKeyHex: string): Promise<void>;
|
|
1355
1445
|
|
|
1356
|
-
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type ApprovalObject, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, CanonicalValueError, 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 MandateBinding, MandateError, type MandateProjection, type OwnerMandate, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ReceiptPayload, type ResolvedDomain, type SigningSurface, type Subject, type SubjectValidation, approvalSigningBytes, attestationId, bindingAppliesTo, boundActionTypes, buildMandateProjection, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFieldsContentHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, decodeDidKey, deriveIdentityLine, encodeAttestationBlob, encodeDidKey, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isFieldBinding, isKeyBearingDid, isV4Attestation, listProfiles, mandateSigningBytes, registerProfile, selectBoundFields, signApproval, signMandateProjection, validateBoundsParams, validateContextParams, validateFrameParams, validateSubject, verify, verifyApproval, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash, verifyOwnerMandate, verifyOwnerMandates, verifyReceiptSignature };
|
|
1446
|
+
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type ApprovalObject, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, CanonicalValueError, 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 MandateBinding, MandateError, type MandateProjection, type OwnerMandate, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ReceiptPayload, type ResolvedDomain, SIGNABLE_COMMITMENT_MODES, type SignableCommitmentMode, type SigningSurface, type Subject, type SubjectValidation, allowedCommitmentModes, approvalSigningBytes, attestationId, bindingAppliesTo, boundActionTypes, buildMandateProjection, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFieldsContentHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, decodeDidKey, deriveIdentityLine, encodeAttestationBlob, encodeDidKey, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isCommitmentModeAllowed, isFieldBinding, isKeyBearingDid, isV4Attestation, listProfiles, mandateSigningBytes, registerProfile, selectBoundFields, signApproval, signMandateProjection, validateBoundsParams, validateBoundsRequiredFor, validateCommitmentModes, validateContextParams, validateFrameParams, validateSubject, verify, verifyApproval, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash, verifyOwnerMandate, verifyOwnerMandates, verifyReceiptSignature };
|
package/dist/index.js
CHANGED
|
@@ -33,6 +33,8 @@ __export(index_exports, {
|
|
|
33
33
|
CanonicalValueError: () => CanonicalValueError,
|
|
34
34
|
ContentBindingError: () => ContentBindingError,
|
|
35
35
|
MandateError: () => MandateError,
|
|
36
|
+
SIGNABLE_COMMITMENT_MODES: () => SIGNABLE_COMMITMENT_MODES,
|
|
37
|
+
allowedCommitmentModes: () => allowedCommitmentModes,
|
|
36
38
|
approvalSigningBytes: () => approvalSigningBytes,
|
|
37
39
|
attestationId: () => attestationId,
|
|
38
40
|
bindingAppliesTo: () => bindingAppliesTo,
|
|
@@ -62,6 +64,7 @@ __export(index_exports, {
|
|
|
62
64
|
getAllProfiles: () => getAllProfiles,
|
|
63
65
|
getProfile: () => getProfile,
|
|
64
66
|
intentDisclosureCanonicalBytes: () => intentDisclosureCanonicalBytes,
|
|
67
|
+
isCommitmentModeAllowed: () => isCommitmentModeAllowed,
|
|
65
68
|
isFieldBinding: () => isFieldBinding,
|
|
66
69
|
isKeyBearingDid: () => isKeyBearingDid,
|
|
67
70
|
isV4Attestation: () => isV4Attestation,
|
|
@@ -72,6 +75,8 @@ __export(index_exports, {
|
|
|
72
75
|
signApproval: () => signApproval,
|
|
73
76
|
signMandateProjection: () => signMandateProjection,
|
|
74
77
|
validateBoundsParams: () => validateBoundsParams,
|
|
78
|
+
validateBoundsRequiredFor: () => validateBoundsRequiredFor,
|
|
79
|
+
validateCommitmentModes: () => validateCommitmentModes,
|
|
75
80
|
validateContextParams: () => validateContextParams,
|
|
76
81
|
validateFrameParams: () => validateFrameParams,
|
|
77
82
|
validateSubject: () => validateSubject,
|
|
@@ -378,6 +383,38 @@ function validateContextParams(params, profile) {
|
|
|
378
383
|
}
|
|
379
384
|
return { valid: errors.length === 0, errors };
|
|
380
385
|
}
|
|
386
|
+
function validateBoundsRequiredFor(profile) {
|
|
387
|
+
const errors = [];
|
|
388
|
+
const boundsSchema = profile.boundsSchema;
|
|
389
|
+
if (!boundsSchema) return errors;
|
|
390
|
+
const actionTypes = boundsSchema.actionTypes ? [...boundsSchema.actionTypes] : void 0;
|
|
391
|
+
for (const [fieldName, fieldDef] of Object.entries(boundsSchema.fields)) {
|
|
392
|
+
const bt = fieldDef.boundType;
|
|
393
|
+
if (!bt) continue;
|
|
394
|
+
const requiredFor = bt.requiredFor;
|
|
395
|
+
if (requiredFor === void 0) continue;
|
|
396
|
+
if (!Array.isArray(requiredFor) || !requiredFor.every((v) => typeof v === "string")) {
|
|
397
|
+
errors.push(`Bound "${fieldName}": requiredFor must be a string array.`);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (bt.kind !== "per_transaction") {
|
|
401
|
+
errors.push(
|
|
402
|
+
`Bound "${fieldName}": requiredFor is only meaningful on a per_transaction bound (found on kind "${bt.kind}") \u2014 every enforcement point ignores it there. Remove it.`
|
|
403
|
+
);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (actionTypes) {
|
|
407
|
+
for (const actionType of requiredFor) {
|
|
408
|
+
if (!actionTypes.includes(actionType)) {
|
|
409
|
+
errors.push(
|
|
410
|
+
`Bound "${fieldName}": requiredFor names action type "${actionType}", which is not in this profile's boundsSchema.actionTypes registry [${actionTypes.join(", ")}]. It can never match a real execution and the requirement is dead.`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return errors;
|
|
417
|
+
}
|
|
381
418
|
var CanonicalValueError = class extends Error {
|
|
382
419
|
code;
|
|
383
420
|
field;
|
|
@@ -445,6 +482,33 @@ function computeContextHash(params, profile) {
|
|
|
445
482
|
return `sha256:${hash}`;
|
|
446
483
|
}
|
|
447
484
|
|
|
485
|
+
// src/commitment-modes.ts
|
|
486
|
+
var SIGNABLE_COMMITMENT_MODES = ["review", "automatic"];
|
|
487
|
+
var isSignable = (v) => typeof v === "string" && SIGNABLE_COMMITMENT_MODES.includes(v);
|
|
488
|
+
function allowedCommitmentModes(profile) {
|
|
489
|
+
const declared = profile.commitment_modes;
|
|
490
|
+
if (declared === void 0) return SIGNABLE_COMMITMENT_MODES;
|
|
491
|
+
if (!Array.isArray(declared)) return [];
|
|
492
|
+
return SIGNABLE_COMMITMENT_MODES.filter((m) => declared.includes(m));
|
|
493
|
+
}
|
|
494
|
+
function isCommitmentModeAllowed(profile, mode) {
|
|
495
|
+
return isSignable(mode) && allowedCommitmentModes(profile).includes(mode);
|
|
496
|
+
}
|
|
497
|
+
function validateCommitmentModes(profile) {
|
|
498
|
+
const declared = profile.commitment_modes;
|
|
499
|
+
if (declared === void 0) return [];
|
|
500
|
+
if (!Array.isArray(declared)) return ["commitment_modes must be a list of modes."];
|
|
501
|
+
const errors = [];
|
|
502
|
+
if (declared.length === 0) errors.push("commitment_modes is empty \u2014 no mandate could ever be signed under this profile.");
|
|
503
|
+
const seen = /* @__PURE__ */ new Set();
|
|
504
|
+
for (const m of declared) {
|
|
505
|
+
if (!isSignable(m)) errors.push(`commitment_modes: "${String(m)}" is not a mode a signer can choose (${SIGNABLE_COMMITMENT_MODES.join(", ")}).`);
|
|
506
|
+
if (seen.has(m)) errors.push(`commitment_modes: "${String(m)}" appears twice.`);
|
|
507
|
+
seen.add(m);
|
|
508
|
+
}
|
|
509
|
+
return errors;
|
|
510
|
+
}
|
|
511
|
+
|
|
448
512
|
// src/attestation.ts
|
|
449
513
|
var import_crypto4 = require("crypto");
|
|
450
514
|
var ed = __toESM(require("@noble/ed25519"));
|
|
@@ -820,6 +884,22 @@ function checkBoundsV4(request, profile, executionLog, now) {
|
|
|
820
884
|
switch (bt.kind) {
|
|
821
885
|
case "per_transaction": {
|
|
822
886
|
const actual = request.execution[bt.of];
|
|
887
|
+
const engaged = !!bt.requiredFor?.length && actionType !== void 0 && bt.requiredFor.includes(actionType);
|
|
888
|
+
if (engaged) {
|
|
889
|
+
const missing = actual === void 0 || actual === null;
|
|
890
|
+
const numericActual = missing ? NaN : typeof actual === "number" ? actual : Number(actual);
|
|
891
|
+
const missingOrNonNumeric = missing || Number.isNaN(numericActual);
|
|
892
|
+
if (missingOrNonNumeric) {
|
|
893
|
+
errors.push({
|
|
894
|
+
code: "BOUND_EXCEEDED",
|
|
895
|
+
field: bt.of,
|
|
896
|
+
message: missing ? `Bound "${fieldName}" requires "${bt.of}" for "${actionType}" calls, but this call exposes no ${bt.of} to check against it. Refusing: the call cannot be shown to stay within ${fieldName}.` : `Bound "${fieldName}" requires a numeric "${bt.of}" for "${actionType}" calls, but this call's value (${String(actual)}) is not a number. Refusing: the call cannot be shown to stay within ${fieldName}.`,
|
|
897
|
+
bound: boundValue,
|
|
898
|
+
actual
|
|
899
|
+
});
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
823
903
|
if (actual === void 0) continue;
|
|
824
904
|
if (typeof boundValue !== "number" || typeof actual !== "number") {
|
|
825
905
|
errors.push({
|
|
@@ -1227,6 +1307,8 @@ async function verifyReceiptSignature(receipt, publicKeyHex) {
|
|
|
1227
1307
|
CanonicalValueError,
|
|
1228
1308
|
ContentBindingError,
|
|
1229
1309
|
MandateError,
|
|
1310
|
+
SIGNABLE_COMMITMENT_MODES,
|
|
1311
|
+
allowedCommitmentModes,
|
|
1230
1312
|
approvalSigningBytes,
|
|
1231
1313
|
attestationId,
|
|
1232
1314
|
bindingAppliesTo,
|
|
@@ -1256,6 +1338,7 @@ async function verifyReceiptSignature(receipt, publicKeyHex) {
|
|
|
1256
1338
|
getAllProfiles,
|
|
1257
1339
|
getProfile,
|
|
1258
1340
|
intentDisclosureCanonicalBytes,
|
|
1341
|
+
isCommitmentModeAllowed,
|
|
1259
1342
|
isFieldBinding,
|
|
1260
1343
|
isKeyBearingDid,
|
|
1261
1344
|
isV4Attestation,
|
|
@@ -1266,6 +1349,8 @@ async function verifyReceiptSignature(receipt, publicKeyHex) {
|
|
|
1266
1349
|
signApproval,
|
|
1267
1350
|
signMandateProjection,
|
|
1268
1351
|
validateBoundsParams,
|
|
1352
|
+
validateBoundsRequiredFor,
|
|
1353
|
+
validateCommitmentModes,
|
|
1269
1354
|
validateContextParams,
|
|
1270
1355
|
validateFrameParams,
|
|
1271
1356
|
validateSubject,
|
package/dist/index.mjs
CHANGED
|
@@ -287,6 +287,38 @@ function validateContextParams(params, profile) {
|
|
|
287
287
|
}
|
|
288
288
|
return { valid: errors.length === 0, errors };
|
|
289
289
|
}
|
|
290
|
+
function validateBoundsRequiredFor(profile) {
|
|
291
|
+
const errors = [];
|
|
292
|
+
const boundsSchema = profile.boundsSchema;
|
|
293
|
+
if (!boundsSchema) return errors;
|
|
294
|
+
const actionTypes = boundsSchema.actionTypes ? [...boundsSchema.actionTypes] : void 0;
|
|
295
|
+
for (const [fieldName, fieldDef] of Object.entries(boundsSchema.fields)) {
|
|
296
|
+
const bt = fieldDef.boundType;
|
|
297
|
+
if (!bt) continue;
|
|
298
|
+
const requiredFor = bt.requiredFor;
|
|
299
|
+
if (requiredFor === void 0) continue;
|
|
300
|
+
if (!Array.isArray(requiredFor) || !requiredFor.every((v) => typeof v === "string")) {
|
|
301
|
+
errors.push(`Bound "${fieldName}": requiredFor must be a string array.`);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (bt.kind !== "per_transaction") {
|
|
305
|
+
errors.push(
|
|
306
|
+
`Bound "${fieldName}": requiredFor is only meaningful on a per_transaction bound (found on kind "${bt.kind}") \u2014 every enforcement point ignores it there. Remove it.`
|
|
307
|
+
);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (actionTypes) {
|
|
311
|
+
for (const actionType of requiredFor) {
|
|
312
|
+
if (!actionTypes.includes(actionType)) {
|
|
313
|
+
errors.push(
|
|
314
|
+
`Bound "${fieldName}": requiredFor names action type "${actionType}", which is not in this profile's boundsSchema.actionTypes registry [${actionTypes.join(", ")}]. It can never match a real execution and the requirement is dead.`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return errors;
|
|
321
|
+
}
|
|
290
322
|
var CanonicalValueError = class extends Error {
|
|
291
323
|
code;
|
|
292
324
|
field;
|
|
@@ -354,6 +386,33 @@ function computeContextHash(params, profile) {
|
|
|
354
386
|
return `sha256:${hash}`;
|
|
355
387
|
}
|
|
356
388
|
|
|
389
|
+
// src/commitment-modes.ts
|
|
390
|
+
var SIGNABLE_COMMITMENT_MODES = ["review", "automatic"];
|
|
391
|
+
var isSignable = (v) => typeof v === "string" && SIGNABLE_COMMITMENT_MODES.includes(v);
|
|
392
|
+
function allowedCommitmentModes(profile) {
|
|
393
|
+
const declared = profile.commitment_modes;
|
|
394
|
+
if (declared === void 0) return SIGNABLE_COMMITMENT_MODES;
|
|
395
|
+
if (!Array.isArray(declared)) return [];
|
|
396
|
+
return SIGNABLE_COMMITMENT_MODES.filter((m) => declared.includes(m));
|
|
397
|
+
}
|
|
398
|
+
function isCommitmentModeAllowed(profile, mode) {
|
|
399
|
+
return isSignable(mode) && allowedCommitmentModes(profile).includes(mode);
|
|
400
|
+
}
|
|
401
|
+
function validateCommitmentModes(profile) {
|
|
402
|
+
const declared = profile.commitment_modes;
|
|
403
|
+
if (declared === void 0) return [];
|
|
404
|
+
if (!Array.isArray(declared)) return ["commitment_modes must be a list of modes."];
|
|
405
|
+
const errors = [];
|
|
406
|
+
if (declared.length === 0) errors.push("commitment_modes is empty \u2014 no mandate could ever be signed under this profile.");
|
|
407
|
+
const seen = /* @__PURE__ */ new Set();
|
|
408
|
+
for (const m of declared) {
|
|
409
|
+
if (!isSignable(m)) errors.push(`commitment_modes: "${String(m)}" is not a mode a signer can choose (${SIGNABLE_COMMITMENT_MODES.join(", ")}).`);
|
|
410
|
+
if (seen.has(m)) errors.push(`commitment_modes: "${String(m)}" appears twice.`);
|
|
411
|
+
seen.add(m);
|
|
412
|
+
}
|
|
413
|
+
return errors;
|
|
414
|
+
}
|
|
415
|
+
|
|
357
416
|
// src/attestation.ts
|
|
358
417
|
import { createHash as createHash4 } from "crypto";
|
|
359
418
|
import * as ed from "@noble/ed25519";
|
|
@@ -729,6 +788,22 @@ function checkBoundsV4(request, profile, executionLog, now) {
|
|
|
729
788
|
switch (bt.kind) {
|
|
730
789
|
case "per_transaction": {
|
|
731
790
|
const actual = request.execution[bt.of];
|
|
791
|
+
const engaged = !!bt.requiredFor?.length && actionType !== void 0 && bt.requiredFor.includes(actionType);
|
|
792
|
+
if (engaged) {
|
|
793
|
+
const missing = actual === void 0 || actual === null;
|
|
794
|
+
const numericActual = missing ? NaN : typeof actual === "number" ? actual : Number(actual);
|
|
795
|
+
const missingOrNonNumeric = missing || Number.isNaN(numericActual);
|
|
796
|
+
if (missingOrNonNumeric) {
|
|
797
|
+
errors.push({
|
|
798
|
+
code: "BOUND_EXCEEDED",
|
|
799
|
+
field: bt.of,
|
|
800
|
+
message: missing ? `Bound "${fieldName}" requires "${bt.of}" for "${actionType}" calls, but this call exposes no ${bt.of} to check against it. Refusing: the call cannot be shown to stay within ${fieldName}.` : `Bound "${fieldName}" requires a numeric "${bt.of}" for "${actionType}" calls, but this call's value (${String(actual)}) is not a number. Refusing: the call cannot be shown to stay within ${fieldName}.`,
|
|
801
|
+
bound: boundValue,
|
|
802
|
+
actual
|
|
803
|
+
});
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
732
807
|
if (actual === void 0) continue;
|
|
733
808
|
if (typeof boundValue !== "number" || typeof actual !== "number") {
|
|
734
809
|
errors.push({
|
|
@@ -1135,6 +1210,8 @@ export {
|
|
|
1135
1210
|
CanonicalValueError,
|
|
1136
1211
|
ContentBindingError,
|
|
1137
1212
|
MandateError,
|
|
1213
|
+
SIGNABLE_COMMITMENT_MODES,
|
|
1214
|
+
allowedCommitmentModes,
|
|
1138
1215
|
approvalSigningBytes,
|
|
1139
1216
|
attestationId,
|
|
1140
1217
|
bindingAppliesTo,
|
|
@@ -1164,6 +1241,7 @@ export {
|
|
|
1164
1241
|
getAllProfiles,
|
|
1165
1242
|
getProfile,
|
|
1166
1243
|
intentDisclosureCanonicalBytes,
|
|
1244
|
+
isCommitmentModeAllowed,
|
|
1167
1245
|
isFieldBinding,
|
|
1168
1246
|
isKeyBearingDid,
|
|
1169
1247
|
isV4Attestation,
|
|
@@ -1174,6 +1252,8 @@ export {
|
|
|
1174
1252
|
signApproval,
|
|
1175
1253
|
signMandateProjection,
|
|
1176
1254
|
validateBoundsParams,
|
|
1255
|
+
validateBoundsRequiredFor,
|
|
1256
|
+
validateCommitmentModes,
|
|
1177
1257
|
validateContextParams,
|
|
1178
1258
|
validateFrameParams,
|
|
1179
1259
|
validateSubject,
|
package/package.json
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which commitment modes a profile allows its mandates to be signed with
|
|
3
|
+
* (`AgentProfile.commitment_modes`). One source for the Authority Server's
|
|
4
|
+
* refusal and the mandate screen's choice, so the two cannot disagree.
|
|
5
|
+
*/
|
|
6
|
+
import type { AgentProfile, SignableCommitmentMode } from './types';
|
|
7
|
+
|
|
8
|
+
/** Every mode a signer can choose, in the order a mandate screen shows them. */
|
|
9
|
+
export const SIGNABLE_COMMITMENT_MODES: readonly SignableCommitmentMode[] = ['review', 'automatic'];
|
|
10
|
+
|
|
11
|
+
const isSignable = (v: unknown): v is SignableCommitmentMode =>
|
|
12
|
+
typeof v === 'string' && (SIGNABLE_COMMITMENT_MODES as readonly string[]).includes(v);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The modes a mandate under this profile may carry, in display order.
|
|
16
|
+
* No declaration → all of them. A declaration is read fail-closed: entries
|
|
17
|
+
* that are not a signable mode are dropped, so a malformed list can only
|
|
18
|
+
* narrow — `[]`, `"review"` (not a list) or `["reveiw"]` allow nothing.
|
|
19
|
+
*/
|
|
20
|
+
export function allowedCommitmentModes(profile: AgentProfile): readonly SignableCommitmentMode[] {
|
|
21
|
+
const declared = (profile as { commitment_modes?: unknown }).commitment_modes;
|
|
22
|
+
if (declared === undefined) return SIGNABLE_COMMITMENT_MODES;
|
|
23
|
+
if (!Array.isArray(declared)) return [];
|
|
24
|
+
return SIGNABLE_COMMITMENT_MODES.filter((m) => declared.includes(m));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Whether a mandate under this profile may be signed with `mode`. */
|
|
28
|
+
export function isCommitmentModeAllowed(profile: AgentProfile, mode: string): boolean {
|
|
29
|
+
return isSignable(mode) && allowedCommitmentModes(profile).includes(mode);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Authoring check for a profile's `commitment_modes` declaration — for
|
|
34
|
+
* hap-profiles CI, an authoring tool or a test. Like `validateBoundsRequiredFor`
|
|
35
|
+
* it is not wired into profile loading; enforcement reads the declaration
|
|
36
|
+
* fail-closed through `allowedCommitmentModes`. Empty array = nothing to fix.
|
|
37
|
+
*/
|
|
38
|
+
export function validateCommitmentModes(profile: AgentProfile): string[] {
|
|
39
|
+
const declared = (profile as { commitment_modes?: unknown }).commitment_modes;
|
|
40
|
+
if (declared === undefined) return [];
|
|
41
|
+
if (!Array.isArray(declared)) return ['commitment_modes must be a list of modes.'];
|
|
42
|
+
const errors: string[] = [];
|
|
43
|
+
if (declared.length === 0) errors.push('commitment_modes is empty — no mandate could ever be signed under this profile.');
|
|
44
|
+
const seen = new Set<unknown>();
|
|
45
|
+
for (const m of declared) {
|
|
46
|
+
if (!isSignable(m)) errors.push(`commitment_modes: "${String(m)}" is not a mode a signer can choose (${SIGNABLE_COMMITMENT_MODES.join(', ')}).`);
|
|
47
|
+
if (seen.has(m)) errors.push(`commitment_modes: "${String(m)}" appears twice.`);
|
|
48
|
+
seen.add(m);
|
|
49
|
+
}
|
|
50
|
+
return errors;
|
|
51
|
+
}
|
package/src/frame.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { createHash } from 'crypto';
|
|
16
|
-
import type { AgentFrameParams, AgentBoundsParams, AgentContextParams, AgentProfile } from './types';
|
|
16
|
+
import type { AgentFrameParams, AgentBoundsParams, AgentContextParams, AgentProfile, BoundType } from './types';
|
|
17
17
|
|
|
18
18
|
// ─── v0.3 Frame Functions ─────────────────────────────────────────────────────
|
|
19
19
|
|
|
@@ -179,6 +179,68 @@ export function validateContextParams(
|
|
|
179
179
|
return { valid: errors.length === 0, errors };
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Authoring-time check for `per_transaction` bound `requiredFor` (v0.8+):
|
|
184
|
+
* entries must name a real `action_type` from the profile's declared
|
|
185
|
+
* `boundsSchema.actionTypes` registry, and the field is only meaningful on
|
|
186
|
+
* `per_transaction` bounds (see `BoundType`'s doc comment for why the other
|
|
187
|
+
* kinds don't carry it).
|
|
188
|
+
*
|
|
189
|
+
* This is NOT wired into `registerProfile` or any enforcement path — nothing
|
|
190
|
+
* in hap-core validates profile JSON at load time today (the pre-existing
|
|
191
|
+
* `requiredFor` on a context/scope constraint has the same gap), so adding
|
|
192
|
+
* an automatic refusal here would be new behaviour beyond this change's
|
|
193
|
+
* scope. Call it explicitly from an authoring tool, a hap-profiles CI check,
|
|
194
|
+
* or a test, to catch a `requiredFor` entry that can never match (misspelled
|
|
195
|
+
* action type) or is attached to a bound kind that silently ignores it.
|
|
196
|
+
*
|
|
197
|
+
* Returns an empty array when the profile has no boundsSchema or nothing to
|
|
198
|
+
* check — never throws.
|
|
199
|
+
*/
|
|
200
|
+
export function validateBoundsRequiredFor(profile: AgentProfile): string[] {
|
|
201
|
+
const errors: string[] = [];
|
|
202
|
+
const boundsSchema = profile.boundsSchema;
|
|
203
|
+
if (!boundsSchema) return errors;
|
|
204
|
+
|
|
205
|
+
const actionTypes = boundsSchema.actionTypes ? [...boundsSchema.actionTypes] : undefined;
|
|
206
|
+
|
|
207
|
+
for (const [fieldName, fieldDef] of Object.entries(boundsSchema.fields)) {
|
|
208
|
+
const bt = fieldDef.boundType as (BoundType & { requiredFor?: unknown }) | undefined;
|
|
209
|
+
if (!bt) continue;
|
|
210
|
+
const requiredFor = (bt as { requiredFor?: unknown }).requiredFor;
|
|
211
|
+
if (requiredFor === undefined) continue;
|
|
212
|
+
|
|
213
|
+
if (!Array.isArray(requiredFor) || !requiredFor.every((v) => typeof v === 'string')) {
|
|
214
|
+
errors.push(`Bound "${fieldName}": requiredFor must be a string array.`);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (bt.kind !== 'per_transaction') {
|
|
219
|
+
errors.push(
|
|
220
|
+
`Bound "${fieldName}": requiredFor is only meaningful on a per_transaction bound ` +
|
|
221
|
+
`(found on kind "${bt.kind}") — every enforcement point ignores it there. Remove it.`,
|
|
222
|
+
);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (actionTypes) {
|
|
227
|
+
for (const actionType of requiredFor) {
|
|
228
|
+
if (!actionTypes.includes(actionType)) {
|
|
229
|
+
errors.push(
|
|
230
|
+
`Bound "${fieldName}": requiredFor names action type "${actionType}", which is not ` +
|
|
231
|
+
`in this profile's boundsSchema.actionTypes registry [${actionTypes.join(', ')}]. ` +
|
|
232
|
+
`It can never match a real execution and the requirement is dead.`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// No actionTypes registry declared: membership cannot be checked (same
|
|
238
|
+
// tolerance the AS receipt route applies to pre-registry profiles).
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return errors;
|
|
242
|
+
}
|
|
243
|
+
|
|
182
244
|
// ─── Value Encoding (normative, v0.5+) ───────────────────────────────────────
|
|
183
245
|
|
|
184
246
|
/**
|
package/src/gatekeeper.ts
CHANGED
|
@@ -546,6 +546,43 @@ function checkBoundsV4(
|
|
|
546
546
|
switch (bt.kind) {
|
|
547
547
|
case 'per_transaction': {
|
|
548
548
|
const actual = request.execution[bt.of];
|
|
549
|
+
|
|
550
|
+
// `requiredFor`: for a listed action_type, absence (or a non-numeric
|
|
551
|
+
// value) is a denial, not a skip — see BoundType.per_transaction's
|
|
552
|
+
// doc comment for why. Unlisted action types, or no `requiredFor` at
|
|
553
|
+
// all, keep the pre-existing skip-on-absence behaviour below.
|
|
554
|
+
const engaged = !!bt.requiredFor?.length
|
|
555
|
+
&& actionType !== undefined
|
|
556
|
+
&& bt.requiredFor.includes(actionType);
|
|
557
|
+
|
|
558
|
+
if (engaged) {
|
|
559
|
+
// `null` is treated as "missing", not as the number 0 — JSON has no
|
|
560
|
+
// undefined, so a value lost somewhere upstream (an unmapped
|
|
561
|
+
// connector field serialized through JSON) commonly arrives as
|
|
562
|
+
// null. `Number(null) === 0` would otherwise read as "an amount of
|
|
563
|
+
// zero was declared and is within bound", which is the exact
|
|
564
|
+
// silent-pass this feature exists to close.
|
|
565
|
+
const missing = actual === undefined || actual === null;
|
|
566
|
+
const numericActual = missing ? NaN : (typeof actual === 'number' ? actual : Number(actual));
|
|
567
|
+
const missingOrNonNumeric = missing || Number.isNaN(numericActual);
|
|
568
|
+
if (missingOrNonNumeric) {
|
|
569
|
+
errors.push({
|
|
570
|
+
code: 'BOUND_EXCEEDED',
|
|
571
|
+
field: bt.of,
|
|
572
|
+
message: missing
|
|
573
|
+
? `Bound "${fieldName}" requires "${bt.of}" for "${actionType}" calls, but this ` +
|
|
574
|
+
`call exposes no ${bt.of} to check against it. Refusing: the call cannot be ` +
|
|
575
|
+
`shown to stay within ${fieldName}.`
|
|
576
|
+
: `Bound "${fieldName}" requires a numeric "${bt.of}" for "${actionType}" calls, ` +
|
|
577
|
+
`but this call's value (${String(actual)}) is not a number. Refusing: the call ` +
|
|
578
|
+
`cannot be shown to stay within ${fieldName}.`,
|
|
579
|
+
bound: boundValue,
|
|
580
|
+
actual,
|
|
581
|
+
});
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
549
586
|
if (actual === undefined) continue;
|
|
550
587
|
if (typeof boundValue !== 'number' || typeof actual !== 'number') {
|
|
551
588
|
errors.push({
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from './content-binding';
|
|
|
10
10
|
export * from './intent-disclosure';
|
|
11
11
|
export * from './identity';
|
|
12
12
|
export * from './frame';
|
|
13
|
+
export * from './commitment-modes';
|
|
13
14
|
export * from './attestation';
|
|
14
15
|
export * from './gatekeeper';
|
|
15
16
|
export * from './profiles';
|
package/src/types.ts
CHANGED
|
@@ -218,8 +218,26 @@ export type BoundType =
|
|
|
218
218
|
* Per-transaction cap. The execution context field named in `of` must
|
|
219
219
|
* satisfy `execution[of] <= bound` for the current call. No cumulative
|
|
220
220
|
* tracking. Used by: amount_max, recipient_max, booking_duration_max, etc.
|
|
221
|
+
*
|
|
222
|
+
* `requiredFor` (v0.8+) closes the converse: without it, a call that does
|
|
223
|
+
* not carry `of` at all is simply skipped — a 5,000 cap refuses 6,000 but
|
|
224
|
+
* permits a call that declares no value whatsoever, which is indistinguishable
|
|
225
|
+
* from an unenforced bound. The same hole, and the same fix, as
|
|
226
|
+
* `FieldConstraint.requiredFor` on a scope constraint (below): values drawn
|
|
227
|
+
* from the profile's `boundsSchema.actionTypes` registry. For a listed
|
|
228
|
+
* `action_type`, an execution whose context lacks `of` — or whose value is
|
|
229
|
+
* not a finite number — MUST be refused rather than skipped. For an
|
|
230
|
+
* unlisted (or when `requiredFor` is absent) action type, today's
|
|
231
|
+
* skip-on-absence behaviour is unchanged. Meaningful only on
|
|
232
|
+
* `per_transaction`; the other `BoundType` kinds have no per-call "value
|
|
233
|
+
* exposed or not" question — `cumulative_sum`/`cumulative_count` always read
|
|
234
|
+
* from the execution log regardless of what this call declares, and `enum`
|
|
235
|
+
* is a capability flag, not a runtime value — so the field does not exist
|
|
236
|
+
* on those variants and a profile that puts it there has it silently
|
|
237
|
+
* ignored by every enforcement point (TypeScript also refuses it when the
|
|
238
|
+
* profile is authored against this type rather than raw JSON).
|
|
221
239
|
*/
|
|
222
|
-
| { kind: 'per_transaction'; of: string }
|
|
240
|
+
| { kind: 'per_transaction'; of: string; requiredFor?: string[] }
|
|
223
241
|
/**
|
|
224
242
|
* Cumulative sum within a time window. The SP maintains a running sum
|
|
225
243
|
* of `execution[of]` across all prior executions in the window; the
|
|
@@ -461,6 +479,12 @@ export interface ExecutionPath {
|
|
|
461
479
|
*
|
|
462
480
|
* Supports both v0.3 (frameSchema) and v0.4 (boundsSchema + contextSchema).
|
|
463
481
|
*/
|
|
482
|
+
/**
|
|
483
|
+
* A commitment mode a person can choose when signing a mandate. (`review_above_cap`
|
|
484
|
+
* is not chosen by the signer — it follows from team caps.)
|
|
485
|
+
*/
|
|
486
|
+
export type SignableCommitmentMode = 'automatic' | 'review';
|
|
487
|
+
|
|
464
488
|
export interface AgentProfile {
|
|
465
489
|
id: string;
|
|
466
490
|
name?: string;
|
|
@@ -505,6 +529,19 @@ export interface AgentProfile {
|
|
|
505
529
|
*/
|
|
506
530
|
receipt_lookup?: boolean;
|
|
507
531
|
|
|
532
|
+
/**
|
|
533
|
+
* The commitment modes a mandate under this profile may be signed with.
|
|
534
|
+
* Absent → every mode (`automatic`, `review`), as before this field existed.
|
|
535
|
+
*
|
|
536
|
+
* Lets a profile be review-only: every action it governs then needs a
|
|
537
|
+
* person's approval, whatever the signer would have picked. The Authority
|
|
538
|
+
* Server refuses to sign a mandate whose `commitment_mode` is not listed;
|
|
539
|
+
* a mandate screen offers only the listed modes. Fail-closed: a declared
|
|
540
|
+
* list with no valid entry allows nothing — a malformed declaration never
|
|
541
|
+
* widens what a profile allows. See {@link allowedCommitmentModes}.
|
|
542
|
+
*/
|
|
543
|
+
commitment_modes?: readonly SignableCommitmentMode[];
|
|
544
|
+
|
|
508
545
|
/**
|
|
509
546
|
* v0.3 frame schema (deprecated, kept for backward compat).
|
|
510
547
|
* Used when boundsSchema is not present.
|
|
@@ -520,6 +557,16 @@ export interface AgentProfile {
|
|
|
520
557
|
boundsSchema?: {
|
|
521
558
|
keyOrder: string[];
|
|
522
559
|
fields: Record<string, ProfileBoundsField>;
|
|
560
|
+
/**
|
|
561
|
+
* v0.5+ registry of the action types this profile recognizes (e.g.
|
|
562
|
+
* `["send", "delete", "setup"]`). Every `execution.action_type` the
|
|
563
|
+
* Authority Server and gateway accept for this profile MUST be a member
|
|
564
|
+
* when the registry is declared (protocol.md → Bounds Schema, rule 2);
|
|
565
|
+
* it is also what a `per_transaction` bound's `requiredFor` draws from.
|
|
566
|
+
* Absent on profiles published before the registry existed — membership
|
|
567
|
+
* is then uncheckable, not a violation.
|
|
568
|
+
*/
|
|
569
|
+
actionTypes?: readonly string[];
|
|
523
570
|
};
|
|
524
571
|
|
|
525
572
|
/**
|