@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 +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +17 -0
- package/dist/index.mjs +17 -0
- package/package.json +1 -1
- package/src/gatekeeper.ts +28 -0
- package/src/types.ts +21 -0
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
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
|
/**
|