@humanagencyp/hap-core 0.7.0 → 0.8.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
@@ -113,6 +113,27 @@ interface Attestation {
113
113
  interface FieldConstraint {
114
114
  type: 'number' | 'string';
115
115
  enforceable: Array<'max' | 'enum' | 'subset'>;
116
+ /**
117
+ * Action types for which this dimension MUST be present in the execution
118
+ * context. When the grant constrains this field and the action is one of
119
+ * these, an absent value is a denial rather than a skipped check.
120
+ *
121
+ * Without it, a constraint is only enforced against calls that happen to
122
+ * expose the value. Gmail's `send_message` is the live case: pass `raw` and
123
+ * `to` is never populated, so `allowed_recipients` had nothing to compare and
124
+ * the check passed — a send to an unverified recipient looked authorized.
125
+ * `send_draft` has the same shape: it transmits, and the Gatekeeper cannot
126
+ * see to whom.
127
+ *
128
+ * Keyed on `action_type` because "does this action engage recipients?" is a
129
+ * property of the action, not of the field name — deleting a draft has no
130
+ * recipients and must keep passing, while sending one must not. This keeps
131
+ * the engine profile-agnostic: it compares declared strings and knows nothing
132
+ * about email, calendars or payments.
133
+ *
134
+ * Omitted → previous behaviour: absence skips the check.
135
+ */
136
+ requiredFor?: string[];
116
137
  }
117
138
  /**
118
139
  * Frame field definition within a profile.
package/dist/index.d.ts CHANGED
@@ -113,6 +113,27 @@ interface Attestation {
113
113
  interface FieldConstraint {
114
114
  type: 'number' | 'string';
115
115
  enforceable: Array<'max' | 'enum' | 'subset'>;
116
+ /**
117
+ * Action types for which this dimension MUST be present in the execution
118
+ * context. When the grant constrains this field and the action is one of
119
+ * these, an absent value is a denial rather than a skipped check.
120
+ *
121
+ * Without it, a constraint is only enforced against calls that happen to
122
+ * expose the value. Gmail's `send_message` is the live case: pass `raw` and
123
+ * `to` is never populated, so `allowed_recipients` had nothing to compare and
124
+ * the check passed — a send to an unverified recipient looked authorized.
125
+ * `send_draft` has the same shape: it transmits, and the Gatekeeper cannot
126
+ * see to whom.
127
+ *
128
+ * Keyed on `action_type` because "does this action engage recipients?" is a
129
+ * property of the action, not of the field name — deleting a draft has no
130
+ * recipients and must keep passing, while sending one must not. This keeps
131
+ * the engine profile-agnostic: it compares declared strings and knows nothing
132
+ * about email, calendars or payments.
133
+ *
134
+ * Omitted → previous behaviour: absence skips the check.
135
+ */
136
+ requiredFor?: string[];
116
137
  }
117
138
  /**
118
139
  * Frame field definition within a profile.
package/dist/index.js CHANGED
@@ -757,6 +757,23 @@ function checkContextConstraints(context, execution, profile) {
757
757
  if (!profile.contextSchema) return errors;
758
758
  for (const [fieldName, fieldDef] of Object.entries(profile.contextSchema.fields)) {
759
759
  if (!fieldDef.constraint) continue;
760
+ const requiredFor = fieldDef.constraint.requiredFor;
761
+ if (requiredFor?.length) {
762
+ const boundValue = context[fieldName];
763
+ const actualValue = execution[fieldName];
764
+ const actionType = execution["action_type"];
765
+ const constrained = boundValue !== void 0 && boundValue !== "";
766
+ const engaged = actionType !== void 0 && requiredFor.includes(String(actionType));
767
+ if (constrained && engaged && (actualValue === void 0 || actualValue === "")) {
768
+ errors.push({
769
+ code: "BOUND_EXCEEDED",
770
+ field: fieldName,
771
+ message: `Authorization limits "${fieldName}" to [${boundValue}], but this "${String(actionType)}" call exposes no ${fieldName} to check against it. Refusing: the call cannot be shown to stay in scope.`,
772
+ bound: boundValue
773
+ });
774
+ continue;
775
+ }
776
+ }
760
777
  for (const enforceType of fieldDef.constraint.enforceable) {
761
778
  if (enforceType === "enum") {
762
779
  const boundValue = context[fieldName];
package/dist/index.mjs CHANGED
@@ -686,6 +686,23 @@ function checkContextConstraints(context, execution, profile) {
686
686
  if (!profile.contextSchema) return errors;
687
687
  for (const [fieldName, fieldDef] of Object.entries(profile.contextSchema.fields)) {
688
688
  if (!fieldDef.constraint) continue;
689
+ const requiredFor = fieldDef.constraint.requiredFor;
690
+ if (requiredFor?.length) {
691
+ const boundValue = context[fieldName];
692
+ const actualValue = execution[fieldName];
693
+ const actionType = execution["action_type"];
694
+ const constrained = boundValue !== void 0 && boundValue !== "";
695
+ const engaged = actionType !== void 0 && requiredFor.includes(String(actionType));
696
+ if (constrained && engaged && (actualValue === void 0 || actualValue === "")) {
697
+ errors.push({
698
+ code: "BOUND_EXCEEDED",
699
+ field: fieldName,
700
+ message: `Authorization limits "${fieldName}" to [${boundValue}], but this "${String(actionType)}" call exposes no ${fieldName} to check against it. Refusing: the call cannot be shown to stay in scope.`,
701
+ bound: boundValue
702
+ });
703
+ continue;
704
+ }
705
+ }
689
706
  for (const enforceType of fieldDef.constraint.enforceable) {
690
707
  if (enforceType === "enum") {
691
708
  const boundValue = context[fieldName];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanagencyp/hap-core",
3
- "version": "0.7.0",
3
+ "version": "0.8.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/gatekeeper.ts CHANGED
@@ -612,6 +612,34 @@ function checkContextConstraints(
612
612
  for (const [fieldName, fieldDef] of Object.entries(profile.contextSchema.fields)) {
613
613
  if (!fieldDef.constraint) continue;
614
614
 
615
+ // A constrained dimension that the call does not expose cannot be checked.
616
+ // Skipping is only safe when the action does not engage that dimension —
617
+ // deleting a draft has no recipients. When it DOES engage it, silence must
618
+ // not read as compliance: an absent value means the Gatekeeper cannot show
619
+ // the call is in scope, and an unverifiable send is exactly what the scope
620
+ // exists to prevent. The profile declares which actions engage the field.
621
+ const requiredFor = fieldDef.constraint.requiredFor;
622
+ if (requiredFor?.length) {
623
+ const boundValue = context[fieldName];
624
+ const actualValue = execution[fieldName];
625
+ const actionType = execution['action_type'];
626
+ const constrained = boundValue !== undefined && boundValue !== '';
627
+ const engaged = actionType !== undefined && requiredFor.includes(String(actionType));
628
+
629
+ if (constrained && engaged && (actualValue === undefined || actualValue === '')) {
630
+ errors.push({
631
+ code: 'BOUND_EXCEEDED',
632
+ field: fieldName,
633
+ message:
634
+ `Authorization limits "${fieldName}" to [${boundValue}], but this ` +
635
+ `"${String(actionType)}" call exposes no ${fieldName} to check against it. ` +
636
+ `Refusing: the call cannot be shown to stay in scope.`,
637
+ bound: boundValue,
638
+ });
639
+ continue; // nothing further to compare for this field
640
+ }
641
+ }
642
+
615
643
  for (const enforceType of fieldDef.constraint.enforceable) {
616
644
  if (enforceType === 'enum') {
617
645
  // The context field value is the allowed value; execution must match
package/src/types.ts CHANGED
@@ -121,6 +121,27 @@ export interface Attestation {
121
121
  export interface FieldConstraint {
122
122
  type: 'number' | 'string';
123
123
  enforceable: Array<'max' | 'enum' | 'subset'>;
124
+ /**
125
+ * Action types for which this dimension MUST be present in the execution
126
+ * context. When the grant constrains this field and the action is one of
127
+ * these, an absent value is a denial rather than a skipped check.
128
+ *
129
+ * Without it, a constraint is only enforced against calls that happen to
130
+ * expose the value. Gmail's `send_message` is the live case: pass `raw` and
131
+ * `to` is never populated, so `allowed_recipients` had nothing to compare and
132
+ * the check passed — a send to an unverified recipient looked authorized.
133
+ * `send_draft` has the same shape: it transmits, and the Gatekeeper cannot
134
+ * see to whom.
135
+ *
136
+ * Keyed on `action_type` because "does this action engage recipients?" is a
137
+ * property of the action, not of the field name — deleting a draft has no
138
+ * recipients and must keep passing, while sending one must not. This keeps
139
+ * the engine profile-agnostic: it compares declared strings and knows nothing
140
+ * about email, calendars or payments.
141
+ *
142
+ * Omitted → previous behaviour: absence skips the check.
143
+ */
144
+ requiredFor?: string[];
124
145
  }
125
146
 
126
147
  /**