@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 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): 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): 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(profileId, path, bt.of, bt.window, now);
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(profileId, path, "_count", bt.window, now);
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(profileId, path, bt.of, bt.window, now);
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(profileId, path, "_count", bt.window, now);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanagencyp/hap-core",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Core types, cryptographic primitives, and verification logic for the Human Agency Protocol",
5
5
  "license": "MIT",
6
6
  "repository": {
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(profileId, path, bt.of, bt.window, now);
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(profileId, path, '_count', bt.window, now);
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(profileId: string, path: string, field: string, window: CumulativeWindow, now?: number): number;
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 ─────────────────────────────────────────────────────────────