@humanagencyp/hap-core 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +81 -2
- package/dist/index.d.ts +81 -2
- package/dist/index.js +76 -2
- package/dist/index.mjs +74 -2
- package/package.json +1 -1
- package/src/frame.ts +63 -1
- package/src/gatekeeper.ts +73 -2
- package/src/types.ts +50 -2
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
|
|
@@ -503,6 +522,16 @@ interface AgentProfile {
|
|
|
503
522
|
boundsSchema?: {
|
|
504
523
|
keyOrder: string[];
|
|
505
524
|
fields: Record<string, ProfileBoundsField>;
|
|
525
|
+
/**
|
|
526
|
+
* v0.5+ registry of the action types this profile recognizes (e.g.
|
|
527
|
+
* `["send", "delete", "setup"]`). Every `execution.action_type` the
|
|
528
|
+
* Authority Server and gateway accept for this profile MUST be a member
|
|
529
|
+
* when the registry is declared (protocol.md → Bounds Schema, rule 2);
|
|
530
|
+
* it is also what a `per_transaction` bound's `requiredFor` draws from.
|
|
531
|
+
* Absent on profiles published before the registry existed — membership
|
|
532
|
+
* is then uncheckable, not a violation.
|
|
533
|
+
*/
|
|
534
|
+
actionTypes?: readonly string[];
|
|
506
535
|
};
|
|
507
536
|
/**
|
|
508
537
|
* v0.4 context schema — defines the execution context parameters (e.g., currency, action_type).
|
|
@@ -610,7 +639,21 @@ interface ExecutionLogQuery {
|
|
|
610
639
|
* Sum a field's values within a time window for a given profile.
|
|
611
640
|
* Use field="_count" to count executions instead of summing a field.
|
|
612
641
|
*/
|
|
613
|
-
sumByWindow(profileId: string, path: string, field: string, window: CumulativeWindow, now?: number
|
|
642
|
+
sumByWindow(profileId: string, path: string, field: string, window: CumulativeWindow, now?: number,
|
|
643
|
+
/**
|
|
644
|
+
* Only count executions whose `execution.action_type` is one of these.
|
|
645
|
+
* Omitted → every execution counts (a bound that governs all action types).
|
|
646
|
+
*
|
|
647
|
+
* Consumption is partitioned by action type (*Cumulative Tracking* rule 4):
|
|
648
|
+
* a bound counts only the action types it governs. Without the filter a
|
|
649
|
+
* profile with several action types feeds every bound the combined total —
|
|
650
|
+
* sales' "orders per day" counted quotes and sends too, and its daily order
|
|
651
|
+
* value summed quote values. Use {@link boundActionTypes} to derive it.
|
|
652
|
+
*
|
|
653
|
+
* Implementations SHOULD count an entry that carries no `action_type`
|
|
654
|
+
* against every filter: an unknown action is not evidence of headroom.
|
|
655
|
+
*/
|
|
656
|
+
actionTypes?: readonly string[]): number;
|
|
614
657
|
}
|
|
615
658
|
/**
|
|
616
659
|
* Agent frame parameters — mixed types (strings and numbers).
|
|
@@ -969,6 +1012,25 @@ declare function validateContextParams(params: AgentContextParams, profile: Agen
|
|
|
969
1012
|
valid: boolean;
|
|
970
1013
|
errors: string[];
|
|
971
1014
|
};
|
|
1015
|
+
/**
|
|
1016
|
+
* Authoring-time check for `per_transaction` bound `requiredFor` (v0.8+):
|
|
1017
|
+
* entries must name a real `action_type` from the profile's declared
|
|
1018
|
+
* `boundsSchema.actionTypes` registry, and the field is only meaningful on
|
|
1019
|
+
* `per_transaction` bounds (see `BoundType`'s doc comment for why the other
|
|
1020
|
+
* kinds don't carry it).
|
|
1021
|
+
*
|
|
1022
|
+
* This is NOT wired into `registerProfile` or any enforcement path — nothing
|
|
1023
|
+
* in hap-core validates profile JSON at load time today (the pre-existing
|
|
1024
|
+
* `requiredFor` on a context/scope constraint has the same gap), so adding
|
|
1025
|
+
* an automatic refusal here would be new behaviour beyond this change's
|
|
1026
|
+
* scope. Call it explicitly from an authoring tool, a hap-profiles CI check,
|
|
1027
|
+
* or a test, to catch a `requiredFor` entry that can never match (misspelled
|
|
1028
|
+
* action type) or is attached to a bound kind that silently ignores it.
|
|
1029
|
+
*
|
|
1030
|
+
* Returns an empty array when the profile has no boundsSchema or nothing to
|
|
1031
|
+
* check — never throws.
|
|
1032
|
+
*/
|
|
1033
|
+
declare function validateBoundsRequiredFor(profile: AgentProfile): string[];
|
|
972
1034
|
/**
|
|
973
1035
|
* Thrown when a value cannot be canonicalized at all — currently only for raw
|
|
974
1036
|
* LF/CR inside a value. Carries the protocol error code so callers can map it
|
|
@@ -1126,6 +1188,23 @@ declare function verifyAttestationV4(blob: string, publicKeyHex: string, expecte
|
|
|
1126
1188
|
* @param executionLog - Optional execution log for resolving cumulative fields
|
|
1127
1189
|
*/
|
|
1128
1190
|
declare function verify(request: GatekeeperRequest, publicKeyHex: string, now?: number, executionLog?: ExecutionLogQuery): Promise<GatekeeperResult>;
|
|
1191
|
+
/**
|
|
1192
|
+
* Which action types' executions count toward this cumulative bound's running
|
|
1193
|
+
* total. `undefined` means all of them.
|
|
1194
|
+
*
|
|
1195
|
+
* `boundGovernsAction` decides whether a bound applies to the current call; this
|
|
1196
|
+
* decides what the bound's total is made of. Both are needed: selecting the right
|
|
1197
|
+
* bound and then feeding it the profile's combined total is still wrong — sales'
|
|
1198
|
+
* "orders per day" was checked only on orders, but counted every quote and send.
|
|
1199
|
+
* Consumption is partitioned by action type (*Cumulative Tracking* rule 4), as
|
|
1200
|
+
* the Authority Server already does.
|
|
1201
|
+
*
|
|
1202
|
+
* Same answer, same order of preference as `boundGovernsAction`: the declared
|
|
1203
|
+
* `appliesTo`; else, for a count bound named for an action (`delete_daily_max`),
|
|
1204
|
+
* that action; else every action type. Exported so display surfaces report the
|
|
1205
|
+
* same totals the gate enforces.
|
|
1206
|
+
*/
|
|
1207
|
+
declare function boundActionTypes(fieldName: string, fieldDef: ProfileBoundsField): readonly string[] | undefined;
|
|
1129
1208
|
|
|
1130
1209
|
/**
|
|
1131
1210
|
* Profile Registry — dynamically populated from git-hosted profile sources.
|
|
@@ -1322,4 +1401,4 @@ interface ReceiptPayload {
|
|
|
1322
1401
|
*/
|
|
1323
1402
|
declare function verifyReceiptSignature(receipt: ReceiptPayload, publicKeyHex: string): Promise<void>;
|
|
1324
1403
|
|
|
1325
|
-
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, 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 };
|
|
1404
|
+
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, validateBoundsRequiredFor, 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
|
|
@@ -503,6 +522,16 @@ interface AgentProfile {
|
|
|
503
522
|
boundsSchema?: {
|
|
504
523
|
keyOrder: string[];
|
|
505
524
|
fields: Record<string, ProfileBoundsField>;
|
|
525
|
+
/**
|
|
526
|
+
* v0.5+ registry of the action types this profile recognizes (e.g.
|
|
527
|
+
* `["send", "delete", "setup"]`). Every `execution.action_type` the
|
|
528
|
+
* Authority Server and gateway accept for this profile MUST be a member
|
|
529
|
+
* when the registry is declared (protocol.md → Bounds Schema, rule 2);
|
|
530
|
+
* it is also what a `per_transaction` bound's `requiredFor` draws from.
|
|
531
|
+
* Absent on profiles published before the registry existed — membership
|
|
532
|
+
* is then uncheckable, not a violation.
|
|
533
|
+
*/
|
|
534
|
+
actionTypes?: readonly string[];
|
|
506
535
|
};
|
|
507
536
|
/**
|
|
508
537
|
* v0.4 context schema — defines the execution context parameters (e.g., currency, action_type).
|
|
@@ -610,7 +639,21 @@ interface ExecutionLogQuery {
|
|
|
610
639
|
* Sum a field's values within a time window for a given profile.
|
|
611
640
|
* Use field="_count" to count executions instead of summing a field.
|
|
612
641
|
*/
|
|
613
|
-
sumByWindow(profileId: string, path: string, field: string, window: CumulativeWindow, now?: number
|
|
642
|
+
sumByWindow(profileId: string, path: string, field: string, window: CumulativeWindow, now?: number,
|
|
643
|
+
/**
|
|
644
|
+
* Only count executions whose `execution.action_type` is one of these.
|
|
645
|
+
* Omitted → every execution counts (a bound that governs all action types).
|
|
646
|
+
*
|
|
647
|
+
* Consumption is partitioned by action type (*Cumulative Tracking* rule 4):
|
|
648
|
+
* a bound counts only the action types it governs. Without the filter a
|
|
649
|
+
* profile with several action types feeds every bound the combined total —
|
|
650
|
+
* sales' "orders per day" counted quotes and sends too, and its daily order
|
|
651
|
+
* value summed quote values. Use {@link boundActionTypes} to derive it.
|
|
652
|
+
*
|
|
653
|
+
* Implementations SHOULD count an entry that carries no `action_type`
|
|
654
|
+
* against every filter: an unknown action is not evidence of headroom.
|
|
655
|
+
*/
|
|
656
|
+
actionTypes?: readonly string[]): number;
|
|
614
657
|
}
|
|
615
658
|
/**
|
|
616
659
|
* Agent frame parameters — mixed types (strings and numbers).
|
|
@@ -969,6 +1012,25 @@ declare function validateContextParams(params: AgentContextParams, profile: Agen
|
|
|
969
1012
|
valid: boolean;
|
|
970
1013
|
errors: string[];
|
|
971
1014
|
};
|
|
1015
|
+
/**
|
|
1016
|
+
* Authoring-time check for `per_transaction` bound `requiredFor` (v0.8+):
|
|
1017
|
+
* entries must name a real `action_type` from the profile's declared
|
|
1018
|
+
* `boundsSchema.actionTypes` registry, and the field is only meaningful on
|
|
1019
|
+
* `per_transaction` bounds (see `BoundType`'s doc comment for why the other
|
|
1020
|
+
* kinds don't carry it).
|
|
1021
|
+
*
|
|
1022
|
+
* This is NOT wired into `registerProfile` or any enforcement path — nothing
|
|
1023
|
+
* in hap-core validates profile JSON at load time today (the pre-existing
|
|
1024
|
+
* `requiredFor` on a context/scope constraint has the same gap), so adding
|
|
1025
|
+
* an automatic refusal here would be new behaviour beyond this change's
|
|
1026
|
+
* scope. Call it explicitly from an authoring tool, a hap-profiles CI check,
|
|
1027
|
+
* or a test, to catch a `requiredFor` entry that can never match (misspelled
|
|
1028
|
+
* action type) or is attached to a bound kind that silently ignores it.
|
|
1029
|
+
*
|
|
1030
|
+
* Returns an empty array when the profile has no boundsSchema or nothing to
|
|
1031
|
+
* check — never throws.
|
|
1032
|
+
*/
|
|
1033
|
+
declare function validateBoundsRequiredFor(profile: AgentProfile): string[];
|
|
972
1034
|
/**
|
|
973
1035
|
* Thrown when a value cannot be canonicalized at all — currently only for raw
|
|
974
1036
|
* LF/CR inside a value. Carries the protocol error code so callers can map it
|
|
@@ -1126,6 +1188,23 @@ declare function verifyAttestationV4(blob: string, publicKeyHex: string, expecte
|
|
|
1126
1188
|
* @param executionLog - Optional execution log for resolving cumulative fields
|
|
1127
1189
|
*/
|
|
1128
1190
|
declare function verify(request: GatekeeperRequest, publicKeyHex: string, now?: number, executionLog?: ExecutionLogQuery): Promise<GatekeeperResult>;
|
|
1191
|
+
/**
|
|
1192
|
+
* Which action types' executions count toward this cumulative bound's running
|
|
1193
|
+
* total. `undefined` means all of them.
|
|
1194
|
+
*
|
|
1195
|
+
* `boundGovernsAction` decides whether a bound applies to the current call; this
|
|
1196
|
+
* decides what the bound's total is made of. Both are needed: selecting the right
|
|
1197
|
+
* bound and then feeding it the profile's combined total is still wrong — sales'
|
|
1198
|
+
* "orders per day" was checked only on orders, but counted every quote and send.
|
|
1199
|
+
* Consumption is partitioned by action type (*Cumulative Tracking* rule 4), as
|
|
1200
|
+
* the Authority Server already does.
|
|
1201
|
+
*
|
|
1202
|
+
* Same answer, same order of preference as `boundGovernsAction`: the declared
|
|
1203
|
+
* `appliesTo`; else, for a count bound named for an action (`delete_daily_max`),
|
|
1204
|
+
* that action; else every action type. Exported so display surfaces report the
|
|
1205
|
+
* same totals the gate enforces.
|
|
1206
|
+
*/
|
|
1207
|
+
declare function boundActionTypes(fieldName: string, fieldDef: ProfileBoundsField): readonly string[] | undefined;
|
|
1129
1208
|
|
|
1130
1209
|
/**
|
|
1131
1210
|
* Profile Registry — dynamically populated from git-hosted profile sources.
|
|
@@ -1322,4 +1401,4 @@ interface ReceiptPayload {
|
|
|
1322
1401
|
*/
|
|
1323
1402
|
declare function verifyReceiptSignature(receipt: ReceiptPayload, publicKeyHex: string): Promise<void>;
|
|
1324
1403
|
|
|
1325
|
-
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, 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 };
|
|
1404
|
+
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, validateBoundsRequiredFor, validateContextParams, validateFrameParams, validateSubject, verify, verifyApproval, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash, verifyOwnerMandate, verifyOwnerMandates, verifyReceiptSignature };
|
package/dist/index.js
CHANGED
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
approvalSigningBytes: () => approvalSigningBytes,
|
|
37
37
|
attestationId: () => attestationId,
|
|
38
38
|
bindingAppliesTo: () => bindingAppliesTo,
|
|
39
|
+
boundActionTypes: () => boundActionTypes,
|
|
39
40
|
buildMandateProjection: () => buildMandateProjection,
|
|
40
41
|
canonicalBounds: () => canonicalBounds,
|
|
41
42
|
canonicalContext: () => canonicalContext,
|
|
@@ -71,6 +72,7 @@ __export(index_exports, {
|
|
|
71
72
|
signApproval: () => signApproval,
|
|
72
73
|
signMandateProjection: () => signMandateProjection,
|
|
73
74
|
validateBoundsParams: () => validateBoundsParams,
|
|
75
|
+
validateBoundsRequiredFor: () => validateBoundsRequiredFor,
|
|
74
76
|
validateContextParams: () => validateContextParams,
|
|
75
77
|
validateFrameParams: () => validateFrameParams,
|
|
76
78
|
validateSubject: () => validateSubject,
|
|
@@ -377,6 +379,38 @@ function validateContextParams(params, profile) {
|
|
|
377
379
|
}
|
|
378
380
|
return { valid: errors.length === 0, errors };
|
|
379
381
|
}
|
|
382
|
+
function validateBoundsRequiredFor(profile) {
|
|
383
|
+
const errors = [];
|
|
384
|
+
const boundsSchema = profile.boundsSchema;
|
|
385
|
+
if (!boundsSchema) return errors;
|
|
386
|
+
const actionTypes = boundsSchema.actionTypes ? [...boundsSchema.actionTypes] : void 0;
|
|
387
|
+
for (const [fieldName, fieldDef] of Object.entries(boundsSchema.fields)) {
|
|
388
|
+
const bt = fieldDef.boundType;
|
|
389
|
+
if (!bt) continue;
|
|
390
|
+
const requiredFor = bt.requiredFor;
|
|
391
|
+
if (requiredFor === void 0) continue;
|
|
392
|
+
if (!Array.isArray(requiredFor) || !requiredFor.every((v) => typeof v === "string")) {
|
|
393
|
+
errors.push(`Bound "${fieldName}": requiredFor must be a string array.`);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (bt.kind !== "per_transaction") {
|
|
397
|
+
errors.push(
|
|
398
|
+
`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.`
|
|
399
|
+
);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (actionTypes) {
|
|
403
|
+
for (const actionType of requiredFor) {
|
|
404
|
+
if (!actionTypes.includes(actionType)) {
|
|
405
|
+
errors.push(
|
|
406
|
+
`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.`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return errors;
|
|
413
|
+
}
|
|
380
414
|
var CanonicalValueError = class extends Error {
|
|
381
415
|
code;
|
|
382
416
|
field;
|
|
@@ -785,6 +819,14 @@ function boundGovernsAction(fieldName, fieldDef, bt, actionType) {
|
|
|
785
819
|
const matches = prefix === actionType || prefix.startsWith("transaction");
|
|
786
820
|
return !namedForAnAction || matches;
|
|
787
821
|
}
|
|
822
|
+
function boundActionTypes(fieldName, fieldDef) {
|
|
823
|
+
if (fieldDef.appliesTo) return fieldDef.appliesTo;
|
|
824
|
+
const bt = fieldDef.boundType;
|
|
825
|
+
if (!bt || bt.kind !== "cumulative_count") return void 0;
|
|
826
|
+
const prefix = fieldName.replace(/_(?:daily|monthly|weekly)_max$/, "");
|
|
827
|
+
if (prefix === fieldName || prefix.startsWith("transaction")) return void 0;
|
|
828
|
+
return [prefix];
|
|
829
|
+
}
|
|
788
830
|
function checkBoundsV4(request, profile, executionLog, now) {
|
|
789
831
|
const errors = [];
|
|
790
832
|
if (!profile.boundsSchema) return errors;
|
|
@@ -811,6 +853,22 @@ function checkBoundsV4(request, profile, executionLog, now) {
|
|
|
811
853
|
switch (bt.kind) {
|
|
812
854
|
case "per_transaction": {
|
|
813
855
|
const actual = request.execution[bt.of];
|
|
856
|
+
const engaged = !!bt.requiredFor?.length && actionType !== void 0 && bt.requiredFor.includes(actionType);
|
|
857
|
+
if (engaged) {
|
|
858
|
+
const missing = actual === void 0 || actual === null;
|
|
859
|
+
const numericActual = missing ? NaN : typeof actual === "number" ? actual : Number(actual);
|
|
860
|
+
const missingOrNonNumeric = missing || Number.isNaN(numericActual);
|
|
861
|
+
if (missingOrNonNumeric) {
|
|
862
|
+
errors.push({
|
|
863
|
+
code: "BOUND_EXCEEDED",
|
|
864
|
+
field: bt.of,
|
|
865
|
+
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}.`,
|
|
866
|
+
bound: boundValue,
|
|
867
|
+
actual
|
|
868
|
+
});
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
814
872
|
if (actual === void 0) continue;
|
|
815
873
|
if (typeof boundValue !== "number" || typeof actual !== "number") {
|
|
816
874
|
errors.push({
|
|
@@ -845,7 +903,14 @@ function checkBoundsV4(request, profile, executionLog, now) {
|
|
|
845
903
|
break;
|
|
846
904
|
}
|
|
847
905
|
if (!executionLog) break;
|
|
848
|
-
const runningTotal = executionLog.sumByWindow(
|
|
906
|
+
const runningTotal = executionLog.sumByWindow(
|
|
907
|
+
profileId,
|
|
908
|
+
path,
|
|
909
|
+
bt.of,
|
|
910
|
+
bt.window,
|
|
911
|
+
now,
|
|
912
|
+
boundActionTypes(fieldName, fieldDef)
|
|
913
|
+
);
|
|
849
914
|
const currentRaw = request.execution[bt.of];
|
|
850
915
|
const current = typeof currentRaw === "number" ? currentRaw : currentRaw !== void 0 ? Number(currentRaw) : 0;
|
|
851
916
|
const total = runningTotal + current;
|
|
@@ -872,7 +937,14 @@ function checkBoundsV4(request, profile, executionLog, now) {
|
|
|
872
937
|
break;
|
|
873
938
|
}
|
|
874
939
|
if (!executionLog) break;
|
|
875
|
-
const runningCount = executionLog.sumByWindow(
|
|
940
|
+
const runningCount = executionLog.sumByWindow(
|
|
941
|
+
profileId,
|
|
942
|
+
path,
|
|
943
|
+
"_count",
|
|
944
|
+
bt.window,
|
|
945
|
+
now,
|
|
946
|
+
boundActionTypes(fieldName, fieldDef)
|
|
947
|
+
);
|
|
876
948
|
const total = runningCount + 1;
|
|
877
949
|
if (total > boundValue) {
|
|
878
950
|
errors.push({
|
|
@@ -1207,6 +1279,7 @@ async function verifyReceiptSignature(receipt, publicKeyHex) {
|
|
|
1207
1279
|
approvalSigningBytes,
|
|
1208
1280
|
attestationId,
|
|
1209
1281
|
bindingAppliesTo,
|
|
1282
|
+
boundActionTypes,
|
|
1210
1283
|
buildMandateProjection,
|
|
1211
1284
|
canonicalBounds,
|
|
1212
1285
|
canonicalContext,
|
|
@@ -1242,6 +1315,7 @@ async function verifyReceiptSignature(receipt, publicKeyHex) {
|
|
|
1242
1315
|
signApproval,
|
|
1243
1316
|
signMandateProjection,
|
|
1244
1317
|
validateBoundsParams,
|
|
1318
|
+
validateBoundsRequiredFor,
|
|
1245
1319
|
validateContextParams,
|
|
1246
1320
|
validateFrameParams,
|
|
1247
1321
|
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;
|
|
@@ -695,6 +727,14 @@ function boundGovernsAction(fieldName, fieldDef, bt, actionType) {
|
|
|
695
727
|
const matches = prefix === actionType || prefix.startsWith("transaction");
|
|
696
728
|
return !namedForAnAction || matches;
|
|
697
729
|
}
|
|
730
|
+
function boundActionTypes(fieldName, fieldDef) {
|
|
731
|
+
if (fieldDef.appliesTo) return fieldDef.appliesTo;
|
|
732
|
+
const bt = fieldDef.boundType;
|
|
733
|
+
if (!bt || bt.kind !== "cumulative_count") return void 0;
|
|
734
|
+
const prefix = fieldName.replace(/_(?:daily|monthly|weekly)_max$/, "");
|
|
735
|
+
if (prefix === fieldName || prefix.startsWith("transaction")) return void 0;
|
|
736
|
+
return [prefix];
|
|
737
|
+
}
|
|
698
738
|
function checkBoundsV4(request, profile, executionLog, now) {
|
|
699
739
|
const errors = [];
|
|
700
740
|
if (!profile.boundsSchema) return errors;
|
|
@@ -721,6 +761,22 @@ function checkBoundsV4(request, profile, executionLog, now) {
|
|
|
721
761
|
switch (bt.kind) {
|
|
722
762
|
case "per_transaction": {
|
|
723
763
|
const actual = request.execution[bt.of];
|
|
764
|
+
const engaged = !!bt.requiredFor?.length && actionType !== void 0 && bt.requiredFor.includes(actionType);
|
|
765
|
+
if (engaged) {
|
|
766
|
+
const missing = actual === void 0 || actual === null;
|
|
767
|
+
const numericActual = missing ? NaN : typeof actual === "number" ? actual : Number(actual);
|
|
768
|
+
const missingOrNonNumeric = missing || Number.isNaN(numericActual);
|
|
769
|
+
if (missingOrNonNumeric) {
|
|
770
|
+
errors.push({
|
|
771
|
+
code: "BOUND_EXCEEDED",
|
|
772
|
+
field: bt.of,
|
|
773
|
+
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}.`,
|
|
774
|
+
bound: boundValue,
|
|
775
|
+
actual
|
|
776
|
+
});
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
724
780
|
if (actual === void 0) continue;
|
|
725
781
|
if (typeof boundValue !== "number" || typeof actual !== "number") {
|
|
726
782
|
errors.push({
|
|
@@ -755,7 +811,14 @@ function checkBoundsV4(request, profile, executionLog, now) {
|
|
|
755
811
|
break;
|
|
756
812
|
}
|
|
757
813
|
if (!executionLog) break;
|
|
758
|
-
const runningTotal = executionLog.sumByWindow(
|
|
814
|
+
const runningTotal = executionLog.sumByWindow(
|
|
815
|
+
profileId,
|
|
816
|
+
path,
|
|
817
|
+
bt.of,
|
|
818
|
+
bt.window,
|
|
819
|
+
now,
|
|
820
|
+
boundActionTypes(fieldName, fieldDef)
|
|
821
|
+
);
|
|
759
822
|
const currentRaw = request.execution[bt.of];
|
|
760
823
|
const current = typeof currentRaw === "number" ? currentRaw : currentRaw !== void 0 ? Number(currentRaw) : 0;
|
|
761
824
|
const total = runningTotal + current;
|
|
@@ -782,7 +845,14 @@ function checkBoundsV4(request, profile, executionLog, now) {
|
|
|
782
845
|
break;
|
|
783
846
|
}
|
|
784
847
|
if (!executionLog) break;
|
|
785
|
-
const runningCount = executionLog.sumByWindow(
|
|
848
|
+
const runningCount = executionLog.sumByWindow(
|
|
849
|
+
profileId,
|
|
850
|
+
path,
|
|
851
|
+
"_count",
|
|
852
|
+
bt.window,
|
|
853
|
+
now,
|
|
854
|
+
boundActionTypes(fieldName, fieldDef)
|
|
855
|
+
);
|
|
786
856
|
const total = runningCount + 1;
|
|
787
857
|
if (total > boundValue) {
|
|
788
858
|
errors.push({
|
|
@@ -1116,6 +1186,7 @@ export {
|
|
|
1116
1186
|
approvalSigningBytes,
|
|
1117
1187
|
attestationId,
|
|
1118
1188
|
bindingAppliesTo,
|
|
1189
|
+
boundActionTypes,
|
|
1119
1190
|
buildMandateProjection,
|
|
1120
1191
|
canonicalBounds,
|
|
1121
1192
|
canonicalContext,
|
|
@@ -1151,6 +1222,7 @@ export {
|
|
|
1151
1222
|
signApproval,
|
|
1152
1223
|
signMandateProjection,
|
|
1153
1224
|
validateBoundsParams,
|
|
1225
|
+
validateBoundsRequiredFor,
|
|
1154
1226
|
validateContextParams,
|
|
1155
1227
|
validateFrameParams,
|
|
1156
1228
|
validateSubject,
|
package/package.json
CHANGED
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
|
@@ -469,6 +469,36 @@ function boundGovernsAction(
|
|
|
469
469
|
return !namedForAnAction || matches;
|
|
470
470
|
}
|
|
471
471
|
|
|
472
|
+
/**
|
|
473
|
+
* Which action types' executions count toward this cumulative bound's running
|
|
474
|
+
* total. `undefined` means all of them.
|
|
475
|
+
*
|
|
476
|
+
* `boundGovernsAction` decides whether a bound applies to the current call; this
|
|
477
|
+
* decides what the bound's total is made of. Both are needed: selecting the right
|
|
478
|
+
* bound and then feeding it the profile's combined total is still wrong — sales'
|
|
479
|
+
* "orders per day" was checked only on orders, but counted every quote and send.
|
|
480
|
+
* Consumption is partitioned by action type (*Cumulative Tracking* rule 4), as
|
|
481
|
+
* the Authority Server already does.
|
|
482
|
+
*
|
|
483
|
+
* Same answer, same order of preference as `boundGovernsAction`: the declared
|
|
484
|
+
* `appliesTo`; else, for a count bound named for an action (`delete_daily_max`),
|
|
485
|
+
* that action; else every action type. Exported so display surfaces report the
|
|
486
|
+
* same totals the gate enforces.
|
|
487
|
+
*/
|
|
488
|
+
export function boundActionTypes(
|
|
489
|
+
fieldName: string,
|
|
490
|
+
fieldDef: ProfileBoundsField,
|
|
491
|
+
): readonly string[] | undefined {
|
|
492
|
+
if (fieldDef.appliesTo) return fieldDef.appliesTo;
|
|
493
|
+
|
|
494
|
+
const bt = fieldDef.boundType;
|
|
495
|
+
if (!bt || bt.kind !== 'cumulative_count') return undefined;
|
|
496
|
+
|
|
497
|
+
const prefix = fieldName.replace(/_(?:daily|monthly|weekly)_max$/, '');
|
|
498
|
+
if (prefix === fieldName || prefix.startsWith('transaction')) return undefined;
|
|
499
|
+
return [prefix];
|
|
500
|
+
}
|
|
501
|
+
|
|
472
502
|
function checkBoundsV4(
|
|
473
503
|
request: GatekeeperRequest,
|
|
474
504
|
profile: AgentProfile,
|
|
@@ -516,6 +546,43 @@ function checkBoundsV4(
|
|
|
516
546
|
switch (bt.kind) {
|
|
517
547
|
case 'per_transaction': {
|
|
518
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
|
+
|
|
519
586
|
if (actual === undefined) continue;
|
|
520
587
|
if (typeof boundValue !== 'number' || typeof actual !== 'number') {
|
|
521
588
|
errors.push({
|
|
@@ -552,7 +619,9 @@ function checkBoundsV4(
|
|
|
552
619
|
}
|
|
553
620
|
if (!executionLog) break;
|
|
554
621
|
|
|
555
|
-
const runningTotal = executionLog.sumByWindow(
|
|
622
|
+
const runningTotal = executionLog.sumByWindow(
|
|
623
|
+
profileId, path, bt.of, bt.window, now, boundActionTypes(fieldName, fieldDef),
|
|
624
|
+
);
|
|
556
625
|
const currentRaw = request.execution[bt.of];
|
|
557
626
|
const current = typeof currentRaw === 'number'
|
|
558
627
|
? currentRaw
|
|
@@ -584,7 +653,9 @@ function checkBoundsV4(
|
|
|
584
653
|
}
|
|
585
654
|
if (!executionLog) break;
|
|
586
655
|
|
|
587
|
-
const runningCount = executionLog.sumByWindow(
|
|
656
|
+
const runningCount = executionLog.sumByWindow(
|
|
657
|
+
profileId, path, '_count', bt.window, now, boundActionTypes(fieldName, fieldDef),
|
|
658
|
+
);
|
|
588
659
|
const total = runningCount + 1;
|
|
589
660
|
|
|
590
661
|
if (total > boundValue) {
|
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
|
|
@@ -520,6 +538,16 @@ export interface AgentProfile {
|
|
|
520
538
|
boundsSchema?: {
|
|
521
539
|
keyOrder: string[];
|
|
522
540
|
fields: Record<string, ProfileBoundsField>;
|
|
541
|
+
/**
|
|
542
|
+
* v0.5+ registry of the action types this profile recognizes (e.g.
|
|
543
|
+
* `["send", "delete", "setup"]`). Every `execution.action_type` the
|
|
544
|
+
* Authority Server and gateway accept for this profile MUST be a member
|
|
545
|
+
* when the registry is declared (protocol.md → Bounds Schema, rule 2);
|
|
546
|
+
* it is also what a `per_transaction` bound's `requiredFor` draws from.
|
|
547
|
+
* Absent on profiles published before the registry existed — membership
|
|
548
|
+
* is then uncheckable, not a violation.
|
|
549
|
+
*/
|
|
550
|
+
actionTypes?: readonly string[];
|
|
523
551
|
};
|
|
524
552
|
|
|
525
553
|
/**
|
|
@@ -636,7 +664,27 @@ export interface ExecutionLogQuery {
|
|
|
636
664
|
* Sum a field's values within a time window for a given profile.
|
|
637
665
|
* Use field="_count" to count executions instead of summing a field.
|
|
638
666
|
*/
|
|
639
|
-
sumByWindow(
|
|
667
|
+
sumByWindow(
|
|
668
|
+
profileId: string,
|
|
669
|
+
path: string,
|
|
670
|
+
field: string,
|
|
671
|
+
window: CumulativeWindow,
|
|
672
|
+
now?: number,
|
|
673
|
+
/**
|
|
674
|
+
* Only count executions whose `execution.action_type` is one of these.
|
|
675
|
+
* Omitted → every execution counts (a bound that governs all action types).
|
|
676
|
+
*
|
|
677
|
+
* Consumption is partitioned by action type (*Cumulative Tracking* rule 4):
|
|
678
|
+
* a bound counts only the action types it governs. Without the filter a
|
|
679
|
+
* profile with several action types feeds every bound the combined total —
|
|
680
|
+
* sales' "orders per day" counted quotes and sends too, and its daily order
|
|
681
|
+
* value summed quote values. Use {@link boundActionTypes} to derive it.
|
|
682
|
+
*
|
|
683
|
+
* Implementations SHOULD count an entry that carries no `action_type`
|
|
684
|
+
* against every filter: an unknown action is not evidence of headroom.
|
|
685
|
+
*/
|
|
686
|
+
actionTypes?: readonly string[],
|
|
687
|
+
): number;
|
|
640
688
|
}
|
|
641
689
|
|
|
642
690
|
// ─── Frame Types ─────────────────────────────────────────────────────────────
|