@humanagencyp/hap-core 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +61 -7
- package/dist/index.d.ts +61 -7
- package/dist/index.js +80 -10
- package/dist/index.mjs +78 -10
- package/package.json +2 -1
- package/src/frame.ts +115 -14
- package/src/gatekeeper.ts +62 -2
- package/src/types.ts +28 -2
package/dist/index.d.mts
CHANGED
|
@@ -610,7 +610,21 @@ interface ExecutionLogQuery {
|
|
|
610
610
|
* Sum a field's values within a time window for a given profile.
|
|
611
611
|
* Use field="_count" to count executions instead of summing a field.
|
|
612
612
|
*/
|
|
613
|
-
sumByWindow(profileId: string, path: string, field: string, window: CumulativeWindow, now?: number
|
|
613
|
+
sumByWindow(profileId: string, path: string, field: string, window: CumulativeWindow, now?: number,
|
|
614
|
+
/**
|
|
615
|
+
* Only count executions whose `execution.action_type` is one of these.
|
|
616
|
+
* Omitted → every execution counts (a bound that governs all action types).
|
|
617
|
+
*
|
|
618
|
+
* Consumption is partitioned by action type (*Cumulative Tracking* rule 4):
|
|
619
|
+
* a bound counts only the action types it governs. Without the filter a
|
|
620
|
+
* profile with several action types feeds every bound the combined total —
|
|
621
|
+
* sales' "orders per day" counted quotes and sends too, and its daily order
|
|
622
|
+
* value summed quote values. Use {@link boundActionTypes} to derive it.
|
|
623
|
+
*
|
|
624
|
+
* Implementations SHOULD count an entry that carries no `action_type`
|
|
625
|
+
* against every filter: an unknown action is not evidence of headroom.
|
|
626
|
+
*/
|
|
627
|
+
actionTypes?: readonly string[]): number;
|
|
614
628
|
}
|
|
615
629
|
/**
|
|
616
630
|
* Agent frame parameters — mixed types (strings and numbers).
|
|
@@ -656,7 +670,7 @@ interface GatekeeperRequest {
|
|
|
656
670
|
* Structured error from Gatekeeper verification.
|
|
657
671
|
*/
|
|
658
672
|
interface GatekeeperError {
|
|
659
|
-
code: 'BOUND_EXCEEDED' | 'CUMULATIVE_LIMIT_EXCEEDED' | 'INVALID_SIGNATURE' | 'TTL_EXPIRED' | 'FRAME_MISMATCH' | 'BOUNDS_MISMATCH' | 'CONTEXT_MISMATCH' | 'DOMAIN_NOT_COVERED' | 'INVALID_PROFILE' | 'MALFORMED_ATTESTATION' | 'INVALID_ACTION_TYPE' | 'ATTESTATION_REVOKED' | 'APPROVAL_REQUIRED' | 'IDEMPOTENCY_MISMATCH' | 'MANDATE_SIGNATURE_REQUIRED' | 'MANDATE_SIGNATURE_INVALID' | 'APPROVAL_SIGNATURE_REQUIRED' | 'APPROVAL_SIGNATURE_INVALID';
|
|
673
|
+
code: 'BOUND_EXCEEDED' | 'CUMULATIVE_LIMIT_EXCEEDED' | 'INVALID_SIGNATURE' | 'TTL_EXPIRED' | 'FRAME_MISMATCH' | 'BOUNDS_MISMATCH' | 'CONTEXT_MISMATCH' | 'DOMAIN_NOT_COVERED' | 'INVALID_PROFILE' | 'MALFORMED_ATTESTATION' | 'INVALID_ACTION_TYPE' | 'ATTESTATION_REVOKED' | 'APPROVAL_REQUIRED' | 'IDEMPOTENCY_MISMATCH' | 'MANDATE_SIGNATURE_REQUIRED' | 'MANDATE_SIGNATURE_INVALID' | 'APPROVAL_SIGNATURE_REQUIRED' | 'APPROVAL_SIGNATURE_INVALID' | 'BOUNDS_INVALID_VALUE' | 'CONTEXT_INVALID_VALUE';
|
|
660
674
|
field?: string;
|
|
661
675
|
message: string;
|
|
662
676
|
bound?: string | number;
|
|
@@ -921,8 +935,11 @@ declare function deriveIdentityLine(subject: Subject | undefined | null, { opera
|
|
|
921
935
|
* Frame Canonicalization for Agent Profiles
|
|
922
936
|
*
|
|
923
937
|
* Agent profiles support mixed-type fields (strings and numbers).
|
|
924
|
-
* Canonical form:
|
|
925
|
-
*
|
|
938
|
+
* Canonical form: `key=value` records joined with LF, keys in the profile's
|
|
939
|
+
* keyOrder. Values are stringified with String(value) — the shortest
|
|
940
|
+
* round-trippable form for numbers — then percent-encoded per protocol.md
|
|
941
|
+
* (`=`, `%`, and every byte outside printable ASCII); a value carrying a raw
|
|
942
|
+
* LF/CR is refused, never normalized. See `canonicalRecords` below.
|
|
926
943
|
*
|
|
927
944
|
* v0.3: frameSchema
|
|
928
945
|
* v0.4: boundsSchema + contextSchema (separate hashes)
|
|
@@ -966,19 +983,39 @@ declare function validateContextParams(params: AgentContextParams, profile: Agen
|
|
|
966
983
|
valid: boolean;
|
|
967
984
|
errors: string[];
|
|
968
985
|
};
|
|
986
|
+
/**
|
|
987
|
+
* Thrown when a value cannot be canonicalized at all — currently only for raw
|
|
988
|
+
* LF/CR inside a value. Carries the protocol error code so callers can map it
|
|
989
|
+
* straight onto the wire without string-matching a message.
|
|
990
|
+
*
|
|
991
|
+
* protocol.md → *Bounds & Scope Canonicalization* → Value encoding:
|
|
992
|
+
* "Values MUST NOT contain raw newline (\n) or carriage-return (\r)
|
|
993
|
+
* characters. Implementations MUST reject input containing them; silent
|
|
994
|
+
* stripping or normalization is a violation because it produces a hash that
|
|
995
|
+
* does not faithfully represent the input."
|
|
996
|
+
*/
|
|
997
|
+
declare class CanonicalValueError extends Error {
|
|
998
|
+
readonly code: 'BOUNDS_INVALID_VALUE' | 'CONTEXT_INVALID_VALUE';
|
|
999
|
+
readonly field: string;
|
|
1000
|
+
constructor(code: 'BOUNDS_INVALID_VALUE' | 'CONTEXT_INVALID_VALUE', field: string, message: string);
|
|
1001
|
+
}
|
|
969
1002
|
/**
|
|
970
1003
|
* Builds the canonical bounds string from parameters.
|
|
971
|
-
*
|
|
1004
|
+
* Keys are ordered per profile's boundsSchema.keyOrder; values are encoded per
|
|
1005
|
+
* protocol.md → *Value encoding* (see `canonicalRecords`).
|
|
972
1006
|
*
|
|
973
1007
|
* @throws Error if any field fails validation
|
|
1008
|
+
* @throws CanonicalValueError (code BOUNDS_INVALID_VALUE) if a value carries a raw LF/CR
|
|
974
1009
|
*/
|
|
975
1010
|
declare function canonicalBounds(params: AgentBoundsParams, profile: AgentProfile): string;
|
|
976
1011
|
/**
|
|
977
1012
|
* Builds the canonical context string from parameters.
|
|
978
|
-
*
|
|
1013
|
+
* Keys are ordered per profile's contextSchema.keyOrder; values are encoded per
|
|
1014
|
+
* protocol.md → *Value encoding* (see `canonicalRecords`).
|
|
979
1015
|
* For empty context (no contextSchema or no fields), returns "".
|
|
980
1016
|
*
|
|
981
1017
|
* @throws Error if any field fails validation
|
|
1018
|
+
* @throws CanonicalValueError (code CONTEXT_INVALID_VALUE) if a value carries a raw LF/CR
|
|
982
1019
|
*/
|
|
983
1020
|
declare function canonicalContext(params: AgentContextParams, profile: AgentProfile): string;
|
|
984
1021
|
/**
|
|
@@ -1103,6 +1140,23 @@ declare function verifyAttestationV4(blob: string, publicKeyHex: string, expecte
|
|
|
1103
1140
|
* @param executionLog - Optional execution log for resolving cumulative fields
|
|
1104
1141
|
*/
|
|
1105
1142
|
declare function verify(request: GatekeeperRequest, publicKeyHex: string, now?: number, executionLog?: ExecutionLogQuery): Promise<GatekeeperResult>;
|
|
1143
|
+
/**
|
|
1144
|
+
* Which action types' executions count toward this cumulative bound's running
|
|
1145
|
+
* total. `undefined` means all of them.
|
|
1146
|
+
*
|
|
1147
|
+
* `boundGovernsAction` decides whether a bound applies to the current call; this
|
|
1148
|
+
* decides what the bound's total is made of. Both are needed: selecting the right
|
|
1149
|
+
* bound and then feeding it the profile's combined total is still wrong — sales'
|
|
1150
|
+
* "orders per day" was checked only on orders, but counted every quote and send.
|
|
1151
|
+
* Consumption is partitioned by action type (*Cumulative Tracking* rule 4), as
|
|
1152
|
+
* the Authority Server already does.
|
|
1153
|
+
*
|
|
1154
|
+
* Same answer, same order of preference as `boundGovernsAction`: the declared
|
|
1155
|
+
* `appliesTo`; else, for a count bound named for an action (`delete_daily_max`),
|
|
1156
|
+
* that action; else every action type. Exported so display surfaces report the
|
|
1157
|
+
* same totals the gate enforces.
|
|
1158
|
+
*/
|
|
1159
|
+
declare function boundActionTypes(fieldName: string, fieldDef: ProfileBoundsField): readonly string[] | undefined;
|
|
1106
1160
|
|
|
1107
1161
|
/**
|
|
1108
1162
|
* Profile Registry — dynamically populated from git-hosted profile sources.
|
|
@@ -1299,4 +1353,4 @@ interface ReceiptPayload {
|
|
|
1299
1353
|
*/
|
|
1300
1354
|
declare function verifyReceiptSignature(receipt: ReceiptPayload, publicKeyHex: string): Promise<void>;
|
|
1301
1355
|
|
|
1302
|
-
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type ApprovalObject, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, 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 };
|
|
1356
|
+
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type ApprovalObject, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, CanonicalValueError, type ContentBinding, ContentBindingError, type ContentBindingErrorCode, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type MandateBinding, MandateError, type MandateProjection, type OwnerMandate, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ReceiptPayload, type ResolvedDomain, type SigningSurface, type Subject, type SubjectValidation, approvalSigningBytes, attestationId, bindingAppliesTo, boundActionTypes, buildMandateProjection, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFieldsContentHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, decodeDidKey, deriveIdentityLine, encodeAttestationBlob, encodeDidKey, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isFieldBinding, isKeyBearingDid, isV4Attestation, listProfiles, mandateSigningBytes, registerProfile, selectBoundFields, signApproval, signMandateProjection, validateBoundsParams, validateContextParams, validateFrameParams, validateSubject, verify, verifyApproval, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash, verifyOwnerMandate, verifyOwnerMandates, verifyReceiptSignature };
|
package/dist/index.d.ts
CHANGED
|
@@ -610,7 +610,21 @@ interface ExecutionLogQuery {
|
|
|
610
610
|
* Sum a field's values within a time window for a given profile.
|
|
611
611
|
* Use field="_count" to count executions instead of summing a field.
|
|
612
612
|
*/
|
|
613
|
-
sumByWindow(profileId: string, path: string, field: string, window: CumulativeWindow, now?: number
|
|
613
|
+
sumByWindow(profileId: string, path: string, field: string, window: CumulativeWindow, now?: number,
|
|
614
|
+
/**
|
|
615
|
+
* Only count executions whose `execution.action_type` is one of these.
|
|
616
|
+
* Omitted → every execution counts (a bound that governs all action types).
|
|
617
|
+
*
|
|
618
|
+
* Consumption is partitioned by action type (*Cumulative Tracking* rule 4):
|
|
619
|
+
* a bound counts only the action types it governs. Without the filter a
|
|
620
|
+
* profile with several action types feeds every bound the combined total —
|
|
621
|
+
* sales' "orders per day" counted quotes and sends too, and its daily order
|
|
622
|
+
* value summed quote values. Use {@link boundActionTypes} to derive it.
|
|
623
|
+
*
|
|
624
|
+
* Implementations SHOULD count an entry that carries no `action_type`
|
|
625
|
+
* against every filter: an unknown action is not evidence of headroom.
|
|
626
|
+
*/
|
|
627
|
+
actionTypes?: readonly string[]): number;
|
|
614
628
|
}
|
|
615
629
|
/**
|
|
616
630
|
* Agent frame parameters — mixed types (strings and numbers).
|
|
@@ -656,7 +670,7 @@ interface GatekeeperRequest {
|
|
|
656
670
|
* Structured error from Gatekeeper verification.
|
|
657
671
|
*/
|
|
658
672
|
interface GatekeeperError {
|
|
659
|
-
code: 'BOUND_EXCEEDED' | 'CUMULATIVE_LIMIT_EXCEEDED' | 'INVALID_SIGNATURE' | 'TTL_EXPIRED' | 'FRAME_MISMATCH' | 'BOUNDS_MISMATCH' | 'CONTEXT_MISMATCH' | 'DOMAIN_NOT_COVERED' | 'INVALID_PROFILE' | 'MALFORMED_ATTESTATION' | 'INVALID_ACTION_TYPE' | 'ATTESTATION_REVOKED' | 'APPROVAL_REQUIRED' | 'IDEMPOTENCY_MISMATCH' | 'MANDATE_SIGNATURE_REQUIRED' | 'MANDATE_SIGNATURE_INVALID' | 'APPROVAL_SIGNATURE_REQUIRED' | 'APPROVAL_SIGNATURE_INVALID';
|
|
673
|
+
code: 'BOUND_EXCEEDED' | 'CUMULATIVE_LIMIT_EXCEEDED' | 'INVALID_SIGNATURE' | 'TTL_EXPIRED' | 'FRAME_MISMATCH' | 'BOUNDS_MISMATCH' | 'CONTEXT_MISMATCH' | 'DOMAIN_NOT_COVERED' | 'INVALID_PROFILE' | 'MALFORMED_ATTESTATION' | 'INVALID_ACTION_TYPE' | 'ATTESTATION_REVOKED' | 'APPROVAL_REQUIRED' | 'IDEMPOTENCY_MISMATCH' | 'MANDATE_SIGNATURE_REQUIRED' | 'MANDATE_SIGNATURE_INVALID' | 'APPROVAL_SIGNATURE_REQUIRED' | 'APPROVAL_SIGNATURE_INVALID' | 'BOUNDS_INVALID_VALUE' | 'CONTEXT_INVALID_VALUE';
|
|
660
674
|
field?: string;
|
|
661
675
|
message: string;
|
|
662
676
|
bound?: string | number;
|
|
@@ -921,8 +935,11 @@ declare function deriveIdentityLine(subject: Subject | undefined | null, { opera
|
|
|
921
935
|
* Frame Canonicalization for Agent Profiles
|
|
922
936
|
*
|
|
923
937
|
* Agent profiles support mixed-type fields (strings and numbers).
|
|
924
|
-
* Canonical form:
|
|
925
|
-
*
|
|
938
|
+
* Canonical form: `key=value` records joined with LF, keys in the profile's
|
|
939
|
+
* keyOrder. Values are stringified with String(value) — the shortest
|
|
940
|
+
* round-trippable form for numbers — then percent-encoded per protocol.md
|
|
941
|
+
* (`=`, `%`, and every byte outside printable ASCII); a value carrying a raw
|
|
942
|
+
* LF/CR is refused, never normalized. See `canonicalRecords` below.
|
|
926
943
|
*
|
|
927
944
|
* v0.3: frameSchema
|
|
928
945
|
* v0.4: boundsSchema + contextSchema (separate hashes)
|
|
@@ -966,19 +983,39 @@ declare function validateContextParams(params: AgentContextParams, profile: Agen
|
|
|
966
983
|
valid: boolean;
|
|
967
984
|
errors: string[];
|
|
968
985
|
};
|
|
986
|
+
/**
|
|
987
|
+
* Thrown when a value cannot be canonicalized at all — currently only for raw
|
|
988
|
+
* LF/CR inside a value. Carries the protocol error code so callers can map it
|
|
989
|
+
* straight onto the wire without string-matching a message.
|
|
990
|
+
*
|
|
991
|
+
* protocol.md → *Bounds & Scope Canonicalization* → Value encoding:
|
|
992
|
+
* "Values MUST NOT contain raw newline (\n) or carriage-return (\r)
|
|
993
|
+
* characters. Implementations MUST reject input containing them; silent
|
|
994
|
+
* stripping or normalization is a violation because it produces a hash that
|
|
995
|
+
* does not faithfully represent the input."
|
|
996
|
+
*/
|
|
997
|
+
declare class CanonicalValueError extends Error {
|
|
998
|
+
readonly code: 'BOUNDS_INVALID_VALUE' | 'CONTEXT_INVALID_VALUE';
|
|
999
|
+
readonly field: string;
|
|
1000
|
+
constructor(code: 'BOUNDS_INVALID_VALUE' | 'CONTEXT_INVALID_VALUE', field: string, message: string);
|
|
1001
|
+
}
|
|
969
1002
|
/**
|
|
970
1003
|
* Builds the canonical bounds string from parameters.
|
|
971
|
-
*
|
|
1004
|
+
* Keys are ordered per profile's boundsSchema.keyOrder; values are encoded per
|
|
1005
|
+
* protocol.md → *Value encoding* (see `canonicalRecords`).
|
|
972
1006
|
*
|
|
973
1007
|
* @throws Error if any field fails validation
|
|
1008
|
+
* @throws CanonicalValueError (code BOUNDS_INVALID_VALUE) if a value carries a raw LF/CR
|
|
974
1009
|
*/
|
|
975
1010
|
declare function canonicalBounds(params: AgentBoundsParams, profile: AgentProfile): string;
|
|
976
1011
|
/**
|
|
977
1012
|
* Builds the canonical context string from parameters.
|
|
978
|
-
*
|
|
1013
|
+
* Keys are ordered per profile's contextSchema.keyOrder; values are encoded per
|
|
1014
|
+
* protocol.md → *Value encoding* (see `canonicalRecords`).
|
|
979
1015
|
* For empty context (no contextSchema or no fields), returns "".
|
|
980
1016
|
*
|
|
981
1017
|
* @throws Error if any field fails validation
|
|
1018
|
+
* @throws CanonicalValueError (code CONTEXT_INVALID_VALUE) if a value carries a raw LF/CR
|
|
982
1019
|
*/
|
|
983
1020
|
declare function canonicalContext(params: AgentContextParams, profile: AgentProfile): string;
|
|
984
1021
|
/**
|
|
@@ -1103,6 +1140,23 @@ declare function verifyAttestationV4(blob: string, publicKeyHex: string, expecte
|
|
|
1103
1140
|
* @param executionLog - Optional execution log for resolving cumulative fields
|
|
1104
1141
|
*/
|
|
1105
1142
|
declare function verify(request: GatekeeperRequest, publicKeyHex: string, now?: number, executionLog?: ExecutionLogQuery): Promise<GatekeeperResult>;
|
|
1143
|
+
/**
|
|
1144
|
+
* Which action types' executions count toward this cumulative bound's running
|
|
1145
|
+
* total. `undefined` means all of them.
|
|
1146
|
+
*
|
|
1147
|
+
* `boundGovernsAction` decides whether a bound applies to the current call; this
|
|
1148
|
+
* decides what the bound's total is made of. Both are needed: selecting the right
|
|
1149
|
+
* bound and then feeding it the profile's combined total is still wrong — sales'
|
|
1150
|
+
* "orders per day" was checked only on orders, but counted every quote and send.
|
|
1151
|
+
* Consumption is partitioned by action type (*Cumulative Tracking* rule 4), as
|
|
1152
|
+
* the Authority Server already does.
|
|
1153
|
+
*
|
|
1154
|
+
* Same answer, same order of preference as `boundGovernsAction`: the declared
|
|
1155
|
+
* `appliesTo`; else, for a count bound named for an action (`delete_daily_max`),
|
|
1156
|
+
* that action; else every action type. Exported so display surfaces report the
|
|
1157
|
+
* same totals the gate enforces.
|
|
1158
|
+
*/
|
|
1159
|
+
declare function boundActionTypes(fieldName: string, fieldDef: ProfileBoundsField): readonly string[] | undefined;
|
|
1106
1160
|
|
|
1107
1161
|
/**
|
|
1108
1162
|
* Profile Registry — dynamically populated from git-hosted profile sources.
|
|
@@ -1299,4 +1353,4 @@ interface ReceiptPayload {
|
|
|
1299
1353
|
*/
|
|
1300
1354
|
declare function verifyReceiptSignature(receipt: ReceiptPayload, publicKeyHex: string): Promise<void>;
|
|
1301
1355
|
|
|
1302
|
-
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type ApprovalObject, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, 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 };
|
|
1356
|
+
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type ApprovalObject, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, CanonicalValueError, type ContentBinding, ContentBindingError, type ContentBindingErrorCode, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type MandateBinding, MandateError, type MandateProjection, type OwnerMandate, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ReceiptPayload, type ResolvedDomain, type SigningSurface, type Subject, type SubjectValidation, approvalSigningBytes, attestationId, bindingAppliesTo, boundActionTypes, buildMandateProjection, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFieldsContentHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, decodeDidKey, deriveIdentityLine, encodeAttestationBlob, encodeDidKey, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isFieldBinding, isKeyBearingDid, isV4Attestation, listProfiles, mandateSigningBytes, registerProfile, selectBoundFields, signApproval, signMandateProjection, validateBoundsParams, validateContextParams, validateFrameParams, validateSubject, verify, verifyApproval, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash, verifyOwnerMandate, verifyOwnerMandates, verifyReceiptSignature };
|
package/dist/index.js
CHANGED
|
@@ -30,11 +30,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
CanonicalValueError: () => CanonicalValueError,
|
|
33
34
|
ContentBindingError: () => ContentBindingError,
|
|
34
35
|
MandateError: () => MandateError,
|
|
35
36
|
approvalSigningBytes: () => approvalSigningBytes,
|
|
36
37
|
attestationId: () => attestationId,
|
|
37
38
|
bindingAppliesTo: () => bindingAppliesTo,
|
|
39
|
+
boundActionTypes: () => boundActionTypes,
|
|
38
40
|
buildMandateProjection: () => buildMandateProjection,
|
|
39
41
|
canonicalBounds: () => canonicalBounds,
|
|
40
42
|
canonicalContext: () => canonicalContext,
|
|
@@ -376,15 +378,51 @@ function validateContextParams(params, profile) {
|
|
|
376
378
|
}
|
|
377
379
|
return { valid: errors.length === 0, errors };
|
|
378
380
|
}
|
|
381
|
+
var CanonicalValueError = class extends Error {
|
|
382
|
+
code;
|
|
383
|
+
field;
|
|
384
|
+
constructor(code, field, message) {
|
|
385
|
+
super(message);
|
|
386
|
+
this.name = "CanonicalValueError";
|
|
387
|
+
this.code = code;
|
|
388
|
+
this.field = field;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
function percentEncodeCanonicalValue(raw) {
|
|
392
|
+
const bytes = new TextEncoder().encode(raw);
|
|
393
|
+
let out = "";
|
|
394
|
+
for (const b of bytes) {
|
|
395
|
+
if (b === 61 || b === 37 || b < 32 || b > 126) {
|
|
396
|
+
out += "%" + b.toString(16).toUpperCase().padStart(2, "0");
|
|
397
|
+
} else {
|
|
398
|
+
out += String.fromCharCode(b);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
function canonicalRecords(params, keyOrder, code) {
|
|
404
|
+
const lines = [];
|
|
405
|
+
for (const key of keyOrder) {
|
|
406
|
+
const value = params[key];
|
|
407
|
+
if (value === void 0 || value === null) continue;
|
|
408
|
+
const raw = String(value);
|
|
409
|
+
if (raw.includes("\n") || raw.includes("\r")) {
|
|
410
|
+
throw new CanonicalValueError(
|
|
411
|
+
code,
|
|
412
|
+
key,
|
|
413
|
+
`Value for "${key}" contains a raw newline or carriage return. Refusing: a hash over stripped or normalized input would not represent what was authorized.`
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
lines.push(`${key}=${percentEncodeCanonicalValue(raw)}`);
|
|
417
|
+
}
|
|
418
|
+
return lines.join("\n");
|
|
419
|
+
}
|
|
379
420
|
function canonicalBounds(params, profile) {
|
|
380
421
|
const validation = validateBoundsParams(params, profile);
|
|
381
422
|
if (!validation.valid) {
|
|
382
423
|
throw new Error(`Invalid bounds parameters: ${validation.errors.join("; ")}`);
|
|
383
424
|
}
|
|
384
|
-
|
|
385
|
-
(key) => `${key}=${String(params[key])}`
|
|
386
|
-
);
|
|
387
|
-
return lines.join("\n");
|
|
425
|
+
return canonicalRecords(params, profile.boundsSchema.keyOrder, "BOUNDS_INVALID_VALUE");
|
|
388
426
|
}
|
|
389
427
|
function canonicalContext(params, profile) {
|
|
390
428
|
if (!profile.contextSchema || Object.keys(profile.contextSchema.fields).length === 0) {
|
|
@@ -394,10 +432,7 @@ function canonicalContext(params, profile) {
|
|
|
394
432
|
if (!validation.valid) {
|
|
395
433
|
throw new Error(`Invalid context parameters: ${validation.errors.join("; ")}`);
|
|
396
434
|
}
|
|
397
|
-
|
|
398
|
-
(key) => `${key}=${String(params[key])}`
|
|
399
|
-
);
|
|
400
|
-
return lines.join("\n");
|
|
435
|
+
return canonicalRecords(params, profile.contextSchema.keyOrder, "CONTEXT_INVALID_VALUE");
|
|
401
436
|
}
|
|
402
437
|
function computeBoundsHash(params, profile) {
|
|
403
438
|
const canonical = canonicalBounds(params, profile);
|
|
@@ -527,7 +562,17 @@ async function verify(request, publicKeyHex, now = Math.floor(Date.now() / 1e3),
|
|
|
527
562
|
return verifyV3(request, profile, publicKeyHex, now, executionLog, errors);
|
|
528
563
|
}
|
|
529
564
|
}
|
|
565
|
+
function noAttestationSupplied(request) {
|
|
566
|
+
return !Array.isArray(request.attestations) || request.attestations.length === 0;
|
|
567
|
+
}
|
|
568
|
+
function noAttestationResult() {
|
|
569
|
+
return {
|
|
570
|
+
approved: false,
|
|
571
|
+
errors: [{ code: "MALFORMED_ATTESTATION", message: "No attestation supplied \u2014 nothing to verify" }]
|
|
572
|
+
};
|
|
573
|
+
}
|
|
530
574
|
async function verifyV3(request, profile, publicKeyHex, now, executionLog, errors) {
|
|
575
|
+
if (noAttestationSupplied(request)) return noAttestationResult();
|
|
531
576
|
let expectedFrameHash;
|
|
532
577
|
try {
|
|
533
578
|
expectedFrameHash = computeFrameHash(request.frame, profile);
|
|
@@ -588,6 +633,7 @@ async function verifyV3(request, profile, publicKeyHex, now, executionLog, error
|
|
|
588
633
|
return { approved: true };
|
|
589
634
|
}
|
|
590
635
|
async function verifyV4(request, profile, publicKeyHex, now, executionLog) {
|
|
636
|
+
if (noAttestationSupplied(request)) return noAttestationResult();
|
|
591
637
|
const errors = [];
|
|
592
638
|
const bounds = request.frame;
|
|
593
639
|
const context = request.context;
|
|
@@ -740,6 +786,14 @@ function boundGovernsAction(fieldName, fieldDef, bt, actionType) {
|
|
|
740
786
|
const matches = prefix === actionType || prefix.startsWith("transaction");
|
|
741
787
|
return !namedForAnAction || matches;
|
|
742
788
|
}
|
|
789
|
+
function boundActionTypes(fieldName, fieldDef) {
|
|
790
|
+
if (fieldDef.appliesTo) return fieldDef.appliesTo;
|
|
791
|
+
const bt = fieldDef.boundType;
|
|
792
|
+
if (!bt || bt.kind !== "cumulative_count") return void 0;
|
|
793
|
+
const prefix = fieldName.replace(/_(?:daily|monthly|weekly)_max$/, "");
|
|
794
|
+
if (prefix === fieldName || prefix.startsWith("transaction")) return void 0;
|
|
795
|
+
return [prefix];
|
|
796
|
+
}
|
|
743
797
|
function checkBoundsV4(request, profile, executionLog, now) {
|
|
744
798
|
const errors = [];
|
|
745
799
|
if (!profile.boundsSchema) return errors;
|
|
@@ -800,7 +854,14 @@ function checkBoundsV4(request, profile, executionLog, now) {
|
|
|
800
854
|
break;
|
|
801
855
|
}
|
|
802
856
|
if (!executionLog) break;
|
|
803
|
-
const runningTotal = executionLog.sumByWindow(
|
|
857
|
+
const runningTotal = executionLog.sumByWindow(
|
|
858
|
+
profileId,
|
|
859
|
+
path,
|
|
860
|
+
bt.of,
|
|
861
|
+
bt.window,
|
|
862
|
+
now,
|
|
863
|
+
boundActionTypes(fieldName, fieldDef)
|
|
864
|
+
);
|
|
804
865
|
const currentRaw = request.execution[bt.of];
|
|
805
866
|
const current = typeof currentRaw === "number" ? currentRaw : currentRaw !== void 0 ? Number(currentRaw) : 0;
|
|
806
867
|
const total = runningTotal + current;
|
|
@@ -827,7 +888,14 @@ function checkBoundsV4(request, profile, executionLog, now) {
|
|
|
827
888
|
break;
|
|
828
889
|
}
|
|
829
890
|
if (!executionLog) break;
|
|
830
|
-
const runningCount = executionLog.sumByWindow(
|
|
891
|
+
const runningCount = executionLog.sumByWindow(
|
|
892
|
+
profileId,
|
|
893
|
+
path,
|
|
894
|
+
"_count",
|
|
895
|
+
bt.window,
|
|
896
|
+
now,
|
|
897
|
+
boundActionTypes(fieldName, fieldDef)
|
|
898
|
+
);
|
|
831
899
|
const total = runningCount + 1;
|
|
832
900
|
if (total > boundValue) {
|
|
833
901
|
errors.push({
|
|
@@ -1156,11 +1224,13 @@ async function verifyReceiptSignature(receipt, publicKeyHex) {
|
|
|
1156
1224
|
}
|
|
1157
1225
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1158
1226
|
0 && (module.exports = {
|
|
1227
|
+
CanonicalValueError,
|
|
1159
1228
|
ContentBindingError,
|
|
1160
1229
|
MandateError,
|
|
1161
1230
|
approvalSigningBytes,
|
|
1162
1231
|
attestationId,
|
|
1163
1232
|
bindingAppliesTo,
|
|
1233
|
+
boundActionTypes,
|
|
1164
1234
|
buildMandateProjection,
|
|
1165
1235
|
canonicalBounds,
|
|
1166
1236
|
canonicalContext,
|
package/dist/index.mjs
CHANGED
|
@@ -287,15 +287,51 @@ function validateContextParams(params, profile) {
|
|
|
287
287
|
}
|
|
288
288
|
return { valid: errors.length === 0, errors };
|
|
289
289
|
}
|
|
290
|
+
var CanonicalValueError = class extends Error {
|
|
291
|
+
code;
|
|
292
|
+
field;
|
|
293
|
+
constructor(code, field, message) {
|
|
294
|
+
super(message);
|
|
295
|
+
this.name = "CanonicalValueError";
|
|
296
|
+
this.code = code;
|
|
297
|
+
this.field = field;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
function percentEncodeCanonicalValue(raw) {
|
|
301
|
+
const bytes = new TextEncoder().encode(raw);
|
|
302
|
+
let out = "";
|
|
303
|
+
for (const b of bytes) {
|
|
304
|
+
if (b === 61 || b === 37 || b < 32 || b > 126) {
|
|
305
|
+
out += "%" + b.toString(16).toUpperCase().padStart(2, "0");
|
|
306
|
+
} else {
|
|
307
|
+
out += String.fromCharCode(b);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
function canonicalRecords(params, keyOrder, code) {
|
|
313
|
+
const lines = [];
|
|
314
|
+
for (const key of keyOrder) {
|
|
315
|
+
const value = params[key];
|
|
316
|
+
if (value === void 0 || value === null) continue;
|
|
317
|
+
const raw = String(value);
|
|
318
|
+
if (raw.includes("\n") || raw.includes("\r")) {
|
|
319
|
+
throw new CanonicalValueError(
|
|
320
|
+
code,
|
|
321
|
+
key,
|
|
322
|
+
`Value for "${key}" contains a raw newline or carriage return. Refusing: a hash over stripped or normalized input would not represent what was authorized.`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
lines.push(`${key}=${percentEncodeCanonicalValue(raw)}`);
|
|
326
|
+
}
|
|
327
|
+
return lines.join("\n");
|
|
328
|
+
}
|
|
290
329
|
function canonicalBounds(params, profile) {
|
|
291
330
|
const validation = validateBoundsParams(params, profile);
|
|
292
331
|
if (!validation.valid) {
|
|
293
332
|
throw new Error(`Invalid bounds parameters: ${validation.errors.join("; ")}`);
|
|
294
333
|
}
|
|
295
|
-
|
|
296
|
-
(key) => `${key}=${String(params[key])}`
|
|
297
|
-
);
|
|
298
|
-
return lines.join("\n");
|
|
334
|
+
return canonicalRecords(params, profile.boundsSchema.keyOrder, "BOUNDS_INVALID_VALUE");
|
|
299
335
|
}
|
|
300
336
|
function canonicalContext(params, profile) {
|
|
301
337
|
if (!profile.contextSchema || Object.keys(profile.contextSchema.fields).length === 0) {
|
|
@@ -305,10 +341,7 @@ function canonicalContext(params, profile) {
|
|
|
305
341
|
if (!validation.valid) {
|
|
306
342
|
throw new Error(`Invalid context parameters: ${validation.errors.join("; ")}`);
|
|
307
343
|
}
|
|
308
|
-
|
|
309
|
-
(key) => `${key}=${String(params[key])}`
|
|
310
|
-
);
|
|
311
|
-
return lines.join("\n");
|
|
344
|
+
return canonicalRecords(params, profile.contextSchema.keyOrder, "CONTEXT_INVALID_VALUE");
|
|
312
345
|
}
|
|
313
346
|
function computeBoundsHash(params, profile) {
|
|
314
347
|
const canonical = canonicalBounds(params, profile);
|
|
@@ -438,7 +471,17 @@ async function verify(request, publicKeyHex, now = Math.floor(Date.now() / 1e3),
|
|
|
438
471
|
return verifyV3(request, profile, publicKeyHex, now, executionLog, errors);
|
|
439
472
|
}
|
|
440
473
|
}
|
|
474
|
+
function noAttestationSupplied(request) {
|
|
475
|
+
return !Array.isArray(request.attestations) || request.attestations.length === 0;
|
|
476
|
+
}
|
|
477
|
+
function noAttestationResult() {
|
|
478
|
+
return {
|
|
479
|
+
approved: false,
|
|
480
|
+
errors: [{ code: "MALFORMED_ATTESTATION", message: "No attestation supplied \u2014 nothing to verify" }]
|
|
481
|
+
};
|
|
482
|
+
}
|
|
441
483
|
async function verifyV3(request, profile, publicKeyHex, now, executionLog, errors) {
|
|
484
|
+
if (noAttestationSupplied(request)) return noAttestationResult();
|
|
442
485
|
let expectedFrameHash;
|
|
443
486
|
try {
|
|
444
487
|
expectedFrameHash = computeFrameHash(request.frame, profile);
|
|
@@ -499,6 +542,7 @@ async function verifyV3(request, profile, publicKeyHex, now, executionLog, error
|
|
|
499
542
|
return { approved: true };
|
|
500
543
|
}
|
|
501
544
|
async function verifyV4(request, profile, publicKeyHex, now, executionLog) {
|
|
545
|
+
if (noAttestationSupplied(request)) return noAttestationResult();
|
|
502
546
|
const errors = [];
|
|
503
547
|
const bounds = request.frame;
|
|
504
548
|
const context = request.context;
|
|
@@ -651,6 +695,14 @@ function boundGovernsAction(fieldName, fieldDef, bt, actionType) {
|
|
|
651
695
|
const matches = prefix === actionType || prefix.startsWith("transaction");
|
|
652
696
|
return !namedForAnAction || matches;
|
|
653
697
|
}
|
|
698
|
+
function boundActionTypes(fieldName, fieldDef) {
|
|
699
|
+
if (fieldDef.appliesTo) return fieldDef.appliesTo;
|
|
700
|
+
const bt = fieldDef.boundType;
|
|
701
|
+
if (!bt || bt.kind !== "cumulative_count") return void 0;
|
|
702
|
+
const prefix = fieldName.replace(/_(?:daily|monthly|weekly)_max$/, "");
|
|
703
|
+
if (prefix === fieldName || prefix.startsWith("transaction")) return void 0;
|
|
704
|
+
return [prefix];
|
|
705
|
+
}
|
|
654
706
|
function checkBoundsV4(request, profile, executionLog, now) {
|
|
655
707
|
const errors = [];
|
|
656
708
|
if (!profile.boundsSchema) return errors;
|
|
@@ -711,7 +763,14 @@ function checkBoundsV4(request, profile, executionLog, now) {
|
|
|
711
763
|
break;
|
|
712
764
|
}
|
|
713
765
|
if (!executionLog) break;
|
|
714
|
-
const runningTotal = executionLog.sumByWindow(
|
|
766
|
+
const runningTotal = executionLog.sumByWindow(
|
|
767
|
+
profileId,
|
|
768
|
+
path,
|
|
769
|
+
bt.of,
|
|
770
|
+
bt.window,
|
|
771
|
+
now,
|
|
772
|
+
boundActionTypes(fieldName, fieldDef)
|
|
773
|
+
);
|
|
715
774
|
const currentRaw = request.execution[bt.of];
|
|
716
775
|
const current = typeof currentRaw === "number" ? currentRaw : currentRaw !== void 0 ? Number(currentRaw) : 0;
|
|
717
776
|
const total = runningTotal + current;
|
|
@@ -738,7 +797,14 @@ function checkBoundsV4(request, profile, executionLog, now) {
|
|
|
738
797
|
break;
|
|
739
798
|
}
|
|
740
799
|
if (!executionLog) break;
|
|
741
|
-
const runningCount = executionLog.sumByWindow(
|
|
800
|
+
const runningCount = executionLog.sumByWindow(
|
|
801
|
+
profileId,
|
|
802
|
+
path,
|
|
803
|
+
"_count",
|
|
804
|
+
bt.window,
|
|
805
|
+
now,
|
|
806
|
+
boundActionTypes(fieldName, fieldDef)
|
|
807
|
+
);
|
|
742
808
|
const total = runningCount + 1;
|
|
743
809
|
if (total > boundValue) {
|
|
744
810
|
errors.push({
|
|
@@ -1066,11 +1132,13 @@ async function verifyReceiptSignature(receipt, publicKeyHex) {
|
|
|
1066
1132
|
}
|
|
1067
1133
|
}
|
|
1068
1134
|
export {
|
|
1135
|
+
CanonicalValueError,
|
|
1069
1136
|
ContentBindingError,
|
|
1070
1137
|
MandateError,
|
|
1071
1138
|
approvalSigningBytes,
|
|
1072
1139
|
attestationId,
|
|
1073
1140
|
bindingAppliesTo,
|
|
1141
|
+
boundActionTypes,
|
|
1074
1142
|
buildMandateProjection,
|
|
1075
1143
|
canonicalBounds,
|
|
1076
1144
|
canonicalContext,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanagencyp/hap-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Core types, cryptographic primitives, and verification logic for the Human Agency Protocol",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
26
26
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
27
28
|
"test": "vitest run",
|
|
28
29
|
"test:watch": "vitest",
|
|
29
30
|
"prepublishOnly": "pnpm build",
|
package/src/frame.ts
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
* Frame Canonicalization for Agent Profiles
|
|
3
3
|
*
|
|
4
4
|
* Agent profiles support mixed-type fields (strings and numbers).
|
|
5
|
-
* Canonical form:
|
|
6
|
-
*
|
|
5
|
+
* Canonical form: `key=value` records joined with LF, keys in the profile's
|
|
6
|
+
* keyOrder. Values are stringified with String(value) — the shortest
|
|
7
|
+
* round-trippable form for numbers — then percent-encoded per protocol.md
|
|
8
|
+
* (`=`, `%`, and every byte outside printable ASCII); a value carrying a raw
|
|
9
|
+
* LF/CR is refused, never normalized. See `canonicalRecords` below.
|
|
7
10
|
*
|
|
8
11
|
* v0.3: frameSchema
|
|
9
12
|
* v0.4: boundsSchema + contextSchema (separate hashes)
|
|
@@ -176,11 +179,115 @@ export function validateContextParams(
|
|
|
176
179
|
return { valid: errors.length === 0, errors };
|
|
177
180
|
}
|
|
178
181
|
|
|
182
|
+
// ─── Value Encoding (normative, v0.5+) ───────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Thrown when a value cannot be canonicalized at all — currently only for raw
|
|
186
|
+
* LF/CR inside a value. Carries the protocol error code so callers can map it
|
|
187
|
+
* straight onto the wire without string-matching a message.
|
|
188
|
+
*
|
|
189
|
+
* protocol.md → *Bounds & Scope Canonicalization* → Value encoding:
|
|
190
|
+
* "Values MUST NOT contain raw newline (\n) or carriage-return (\r)
|
|
191
|
+
* characters. Implementations MUST reject input containing them; silent
|
|
192
|
+
* stripping or normalization is a violation because it produces a hash that
|
|
193
|
+
* does not faithfully represent the input."
|
|
194
|
+
*/
|
|
195
|
+
export class CanonicalValueError extends Error {
|
|
196
|
+
readonly code: 'BOUNDS_INVALID_VALUE' | 'CONTEXT_INVALID_VALUE';
|
|
197
|
+
readonly field: string;
|
|
198
|
+
|
|
199
|
+
constructor(code: 'BOUNDS_INVALID_VALUE' | 'CONTEXT_INVALID_VALUE', field: string, message: string) {
|
|
200
|
+
super(message);
|
|
201
|
+
this.name = 'CanonicalValueError';
|
|
202
|
+
this.code = code;
|
|
203
|
+
this.field = field;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Percent-encode a value per protocol.md → *Value encoding*.
|
|
209
|
+
*
|
|
210
|
+
* Encoded, over the value's UTF-8 bytes, as `%` + two UPPERCASE hex digits:
|
|
211
|
+
* - `=` (0x3D) — otherwise it would be read as the key/value separator
|
|
212
|
+
* - `%` (0x25) — so the encoding is self-inverse
|
|
213
|
+
* - every byte outside printable ASCII 0x20–0x7E
|
|
214
|
+
*
|
|
215
|
+
* LF and CR are deliberately NOT in this list: they are refused upstream, so
|
|
216
|
+
* encoding them is unreachable (v0.7 removed the spec's contradiction here).
|
|
217
|
+
*
|
|
218
|
+
* This runs at canonicalization time only. Stored values keep the human's
|
|
219
|
+
* original bytes.
|
|
220
|
+
*/
|
|
221
|
+
function percentEncodeCanonicalValue(raw: string): string {
|
|
222
|
+
const bytes = new TextEncoder().encode(raw);
|
|
223
|
+
let out = '';
|
|
224
|
+
for (const b of bytes) {
|
|
225
|
+
if (b === 0x3d || b === 0x25 || b < 0x20 || b > 0x7e) {
|
|
226
|
+
out += '%' + b.toString(16).toUpperCase().padStart(2, '0');
|
|
227
|
+
} else {
|
|
228
|
+
out += String.fromCharCode(b);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The one place `key=value` records are built for bounds and context.
|
|
236
|
+
*
|
|
237
|
+
* Rules applied here (all normative, protocol.md → *Bounds & Scope
|
|
238
|
+
* Canonicalization*):
|
|
239
|
+
* - keys in the schema's keyOrder, never alphabetical
|
|
240
|
+
* - a value carrying a raw LF/CR is REFUSED (never stripped or encoded)
|
|
241
|
+
* - `=`, `%`, and any byte outside 0x20–0x7E are percent-encoded (UPPERCASE)
|
|
242
|
+
* - numbers use JS `String()`, which is the shortest round-trippable form
|
|
243
|
+
* (`String(20.0)` === "20")
|
|
244
|
+
* - a key with no value is OMITTED entirely — it emits no record
|
|
245
|
+
*
|
|
246
|
+
* On the omission rule: required keys are guaranteed present by the caller's
|
|
247
|
+
* validation (`validateBoundsParams` / `validateContextParams` reject a missing
|
|
248
|
+
* required field), so "explicit inclusion of all required keys" still holds.
|
|
249
|
+
* What remains are *optional* keys the human never set. Rendering those as the
|
|
250
|
+
* literal string "undefined" — the pre-fix behaviour — hashed a JavaScript
|
|
251
|
+
* artifact that no other language would produce and that collides with a real
|
|
252
|
+
* value of "undefined". Omitting them (rather than emitting `key=`) also keeps
|
|
253
|
+
* "the human set no limit" distinct from "the human set an empty value", and
|
|
254
|
+
* matches this package's JSON canonicalization, which drops undefined-valued
|
|
255
|
+
* properties.
|
|
256
|
+
*/
|
|
257
|
+
function canonicalRecords(
|
|
258
|
+
params: Record<string, string | number | undefined>,
|
|
259
|
+
keyOrder: string[],
|
|
260
|
+
code: 'BOUNDS_INVALID_VALUE' | 'CONTEXT_INVALID_VALUE',
|
|
261
|
+
): string {
|
|
262
|
+
const lines: string[] = [];
|
|
263
|
+
|
|
264
|
+
for (const key of keyOrder) {
|
|
265
|
+
const value = params[key];
|
|
266
|
+
if (value === undefined || value === null) continue;
|
|
267
|
+
|
|
268
|
+
const raw = String(value);
|
|
269
|
+
if (raw.includes('\n') || raw.includes('\r')) {
|
|
270
|
+
throw new CanonicalValueError(
|
|
271
|
+
code,
|
|
272
|
+
key,
|
|
273
|
+
`Value for "${key}" contains a raw newline or carriage return. ` +
|
|
274
|
+
'Refusing: a hash over stripped or normalized input would not represent what was authorized.',
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
lines.push(`${key}=${percentEncodeCanonicalValue(raw)}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return lines.join('\n');
|
|
282
|
+
}
|
|
283
|
+
|
|
179
284
|
/**
|
|
180
285
|
* Builds the canonical bounds string from parameters.
|
|
181
|
-
*
|
|
286
|
+
* Keys are ordered per profile's boundsSchema.keyOrder; values are encoded per
|
|
287
|
+
* protocol.md → *Value encoding* (see `canonicalRecords`).
|
|
182
288
|
*
|
|
183
289
|
* @throws Error if any field fails validation
|
|
290
|
+
* @throws CanonicalValueError (code BOUNDS_INVALID_VALUE) if a value carries a raw LF/CR
|
|
184
291
|
*/
|
|
185
292
|
export function canonicalBounds(params: AgentBoundsParams, profile: AgentProfile): string {
|
|
186
293
|
const validation = validateBoundsParams(params, profile);
|
|
@@ -188,19 +295,17 @@ export function canonicalBounds(params: AgentBoundsParams, profile: AgentProfile
|
|
|
188
295
|
throw new Error(`Invalid bounds parameters: ${validation.errors.join('; ')}`);
|
|
189
296
|
}
|
|
190
297
|
|
|
191
|
-
|
|
192
|
-
(key) => `${key}=${String(params[key])}`
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
return lines.join('\n');
|
|
298
|
+
return canonicalRecords(params, profile.boundsSchema!.keyOrder, 'BOUNDS_INVALID_VALUE');
|
|
196
299
|
}
|
|
197
300
|
|
|
198
301
|
/**
|
|
199
302
|
* Builds the canonical context string from parameters.
|
|
200
|
-
*
|
|
303
|
+
* Keys are ordered per profile's contextSchema.keyOrder; values are encoded per
|
|
304
|
+
* protocol.md → *Value encoding* (see `canonicalRecords`).
|
|
201
305
|
* For empty context (no contextSchema or no fields), returns "".
|
|
202
306
|
*
|
|
203
307
|
* @throws Error if any field fails validation
|
|
308
|
+
* @throws CanonicalValueError (code CONTEXT_INVALID_VALUE) if a value carries a raw LF/CR
|
|
204
309
|
*/
|
|
205
310
|
export function canonicalContext(params: AgentContextParams, profile: AgentProfile): string {
|
|
206
311
|
// No contextSchema or no fields → empty context
|
|
@@ -213,11 +318,7 @@ export function canonicalContext(params: AgentContextParams, profile: AgentProfi
|
|
|
213
318
|
throw new Error(`Invalid context parameters: ${validation.errors.join('; ')}`);
|
|
214
319
|
}
|
|
215
320
|
|
|
216
|
-
|
|
217
|
-
(key) => `${key}=${String(params[key])}`
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
return lines.join('\n');
|
|
321
|
+
return canonicalRecords(params, profile.contextSchema.keyOrder, 'CONTEXT_INVALID_VALUE');
|
|
221
322
|
}
|
|
222
323
|
|
|
223
324
|
/**
|
package/src/gatekeeper.ts
CHANGED
|
@@ -86,6 +86,28 @@ export async function verify(
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Fail closed when there is nothing to verify.
|
|
91
|
+
*
|
|
92
|
+
* The attestation loop below is a `for` over `request.attestations`. With an
|
|
93
|
+
* empty (or missing) array it runs zero times, pushes zero errors, and control
|
|
94
|
+
* falls through to the bounds check — which happily approves a call whose
|
|
95
|
+
* signature, bounds_hash, and TTL were never checked. "No errors" is not the
|
|
96
|
+
* same as "verified"; an absent attestation is the one case where silence must
|
|
97
|
+
* read as refusal.
|
|
98
|
+
*/
|
|
99
|
+
function noAttestationSupplied(request: GatekeeperRequest): boolean {
|
|
100
|
+
return !Array.isArray(request.attestations) || request.attestations.length === 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Built fresh per call so a caller cannot mutate a shared error array. */
|
|
104
|
+
function noAttestationResult(): GatekeeperResult {
|
|
105
|
+
return {
|
|
106
|
+
approved: false,
|
|
107
|
+
errors: [{ code: 'MALFORMED_ATTESTATION', message: 'No attestation supplied — nothing to verify' }],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
89
111
|
// ─── v0.3 Verification ────────────────────────────────────────────────────────
|
|
90
112
|
|
|
91
113
|
async function verifyV3(
|
|
@@ -96,6 +118,8 @@ async function verifyV3(
|
|
|
96
118
|
executionLog: ExecutionLogQuery | undefined,
|
|
97
119
|
errors: GatekeeperError[],
|
|
98
120
|
): Promise<GatekeeperResult> {
|
|
121
|
+
if (noAttestationSupplied(request)) return noAttestationResult();
|
|
122
|
+
|
|
99
123
|
let expectedFrameHash: string;
|
|
100
124
|
try {
|
|
101
125
|
expectedFrameHash = computeFrameHash(request.frame, profile);
|
|
@@ -179,6 +203,8 @@ async function verifyV4(
|
|
|
179
203
|
now: number,
|
|
180
204
|
executionLog: ExecutionLogQuery | undefined,
|
|
181
205
|
): Promise<GatekeeperResult> {
|
|
206
|
+
if (noAttestationSupplied(request)) return noAttestationResult();
|
|
207
|
+
|
|
182
208
|
const errors: GatekeeperError[] = [];
|
|
183
209
|
|
|
184
210
|
// In v0.4 the `frame` param carries bounds; `context` carries context params
|
|
@@ -443,6 +469,36 @@ function boundGovernsAction(
|
|
|
443
469
|
return !namedForAnAction || matches;
|
|
444
470
|
}
|
|
445
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
|
+
|
|
446
502
|
function checkBoundsV4(
|
|
447
503
|
request: GatekeeperRequest,
|
|
448
504
|
profile: AgentProfile,
|
|
@@ -526,7 +582,9 @@ function checkBoundsV4(
|
|
|
526
582
|
}
|
|
527
583
|
if (!executionLog) break;
|
|
528
584
|
|
|
529
|
-
const runningTotal = executionLog.sumByWindow(
|
|
585
|
+
const runningTotal = executionLog.sumByWindow(
|
|
586
|
+
profileId, path, bt.of, bt.window, now, boundActionTypes(fieldName, fieldDef),
|
|
587
|
+
);
|
|
530
588
|
const currentRaw = request.execution[bt.of];
|
|
531
589
|
const current = typeof currentRaw === 'number'
|
|
532
590
|
? currentRaw
|
|
@@ -558,7 +616,9 @@ function checkBoundsV4(
|
|
|
558
616
|
}
|
|
559
617
|
if (!executionLog) break;
|
|
560
618
|
|
|
561
|
-
const runningCount = executionLog.sumByWindow(
|
|
619
|
+
const runningCount = executionLog.sumByWindow(
|
|
620
|
+
profileId, path, '_count', bt.window, now, boundActionTypes(fieldName, fieldDef),
|
|
621
|
+
);
|
|
562
622
|
const total = runningCount + 1;
|
|
563
623
|
|
|
564
624
|
if (total > boundValue) {
|
package/src/types.ts
CHANGED
|
@@ -636,7 +636,27 @@ export interface ExecutionLogQuery {
|
|
|
636
636
|
* Sum a field's values within a time window for a given profile.
|
|
637
637
|
* Use field="_count" to count executions instead of summing a field.
|
|
638
638
|
*/
|
|
639
|
-
sumByWindow(
|
|
639
|
+
sumByWindow(
|
|
640
|
+
profileId: string,
|
|
641
|
+
path: string,
|
|
642
|
+
field: string,
|
|
643
|
+
window: CumulativeWindow,
|
|
644
|
+
now?: number,
|
|
645
|
+
/**
|
|
646
|
+
* Only count executions whose `execution.action_type` is one of these.
|
|
647
|
+
* Omitted → every execution counts (a bound that governs all action types).
|
|
648
|
+
*
|
|
649
|
+
* Consumption is partitioned by action type (*Cumulative Tracking* rule 4):
|
|
650
|
+
* a bound counts only the action types it governs. Without the filter a
|
|
651
|
+
* profile with several action types feeds every bound the combined total —
|
|
652
|
+
* sales' "orders per day" counted quotes and sends too, and its daily order
|
|
653
|
+
* value summed quote values. Use {@link boundActionTypes} to derive it.
|
|
654
|
+
*
|
|
655
|
+
* Implementations SHOULD count an entry that carries no `action_type`
|
|
656
|
+
* against every filter: an unknown action is not evidence of headroom.
|
|
657
|
+
*/
|
|
658
|
+
actionTypes?: readonly string[],
|
|
659
|
+
): number;
|
|
640
660
|
}
|
|
641
661
|
|
|
642
662
|
// ─── Frame Types ─────────────────────────────────────────────────────────────
|
|
@@ -712,7 +732,13 @@ export interface GatekeeperError {
|
|
|
712
732
|
| 'MANDATE_SIGNATURE_REQUIRED'
|
|
713
733
|
| 'MANDATE_SIGNATURE_INVALID'
|
|
714
734
|
| 'APPROVAL_SIGNATURE_REQUIRED'
|
|
715
|
-
| 'APPROVAL_SIGNATURE_INVALID'
|
|
735
|
+
| 'APPROVAL_SIGNATURE_INVALID'
|
|
736
|
+
// Canonicalization refusals (protocol.md → Bounds & Scope Canonicalization
|
|
737
|
+
// → Value encoding). Raised when a value cannot be canonicalized at all —
|
|
738
|
+
// a raw LF/CR, or a profile `constraint.pattern` violation. Distinct from
|
|
739
|
+
// BOUNDS_MISMATCH / CONTEXT_MISMATCH, which mean the hashes disagree.
|
|
740
|
+
| 'BOUNDS_INVALID_VALUE'
|
|
741
|
+
| 'CONTEXT_INVALID_VALUE';
|
|
716
742
|
field?: string;
|
|
717
743
|
message: string;
|
|
718
744
|
bound?: string | number;
|