@contractspec/lib.contracts 1.50.0 → 1.51.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/app-config/contracts.d.ts +51 -51
- package/dist/app-config/events.d.ts +27 -27
- package/dist/app-config/lifecycle-contracts.d.ts +55 -55
- package/dist/app-config/runtime.d.ts +1 -1
- package/dist/app-config/spec.d.ts +1 -1
- package/dist/capabilities/capabilities.d.ts +40 -4
- package/dist/capabilities/capabilities.js +125 -0
- package/dist/capabilities/context.d.ts +88 -0
- package/dist/capabilities/context.js +87 -0
- package/dist/capabilities/docs/capabilities.docblock.js +191 -2
- package/dist/capabilities/guards.d.ts +110 -0
- package/dist/capabilities/guards.js +146 -0
- package/dist/capabilities/index.d.ts +4 -1
- package/dist/capabilities/index.js +4 -1
- package/dist/capabilities/validation.d.ts +76 -0
- package/dist/capabilities/validation.js +141 -0
- package/dist/client/react/feature-render.d.ts +2 -2
- package/dist/contract-registry/schemas.d.ts +2 -2
- package/dist/data-views/runtime.d.ts +1 -1
- package/dist/events.d.ts +6 -0
- package/dist/examples/schema.d.ts +11 -11
- package/dist/experiments/spec.d.ts +1 -1
- package/dist/features/install.d.ts +4 -4
- package/dist/features/types.d.ts +4 -4
- package/dist/index.d.ts +21 -13
- package/dist/index.js +11 -3
- package/dist/install.d.ts +1 -1
- package/dist/integrations/openbanking/contracts/accounts.d.ts +67 -67
- package/dist/integrations/openbanking/contracts/balances.d.ts +35 -35
- package/dist/integrations/openbanking/contracts/transactions.d.ts +49 -49
- package/dist/integrations/openbanking/models.d.ts +55 -55
- package/dist/integrations/operations.d.ts +1 -1
- package/dist/integrations/spec.d.ts +1 -1
- package/dist/knowledge/operations.d.ts +67 -67
- package/dist/llm/exporters.d.ts +2 -2
- package/dist/markdown.d.ts +1 -1
- package/dist/onboarding-base.d.ts +29 -29
- package/dist/operations/operation.d.ts +6 -0
- package/dist/policy/context.d.ts +237 -0
- package/dist/policy/context.js +227 -0
- package/dist/policy/guards.d.ts +145 -0
- package/dist/policy/guards.js +254 -0
- package/dist/policy/index.d.ts +12 -1
- package/dist/policy/index.js +11 -1
- package/dist/policy/spec.d.ts +1 -1
- package/dist/policy/validation.d.ts +67 -0
- package/dist/policy/validation.js +307 -0
- package/dist/presentations/presentations.d.ts +6 -0
- package/dist/tests/spec.d.ts +1 -1
- package/dist/themes.d.ts +1 -1
- package/dist/translations/index.d.ts +6 -0
- package/dist/translations/index.js +5 -0
- package/dist/translations/registry.d.ts +144 -0
- package/dist/translations/registry.js +223 -0
- package/dist/translations/spec.d.ts +126 -0
- package/dist/translations/spec.js +31 -0
- package/dist/translations/validation.d.ts +85 -0
- package/dist/translations/validation.js +328 -0
- package/dist/workflow/context.d.ts +191 -0
- package/dist/workflow/context.js +227 -0
- package/dist/workflow/index.d.ts +4 -2
- package/dist/workflow/index.js +4 -2
- package/dist/workflow/spec.d.ts +1 -1
- package/dist/workflow/validation.d.ts +64 -2
- package/dist/workflow/validation.js +194 -1
- package/package.json +18 -6
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { PolicyViolationError } from "./context.js";
|
|
2
|
+
|
|
3
|
+
//#region src/policy/guards.ts
|
|
4
|
+
function checkAuthLevel(ctx, required) {
|
|
5
|
+
if (required === "anonymous") return true;
|
|
6
|
+
if (required === "user") return ctx.user.id !== "anonymous";
|
|
7
|
+
if (required === "admin") return ctx.hasRole("admin");
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Check if an operation's policy constraints are satisfied.
|
|
12
|
+
*
|
|
13
|
+
* @param ctx - Policy context to check against
|
|
14
|
+
* @param operation - Operation spec to check
|
|
15
|
+
* @param flags - Available feature flags
|
|
16
|
+
* @returns Guard result indicating if operation is allowed
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const result = checkPolicyForOperation(ctx, myOperation, ['feature-x']);
|
|
21
|
+
* if (!result.allowed) {
|
|
22
|
+
* console.log('Denied:', result.reason);
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
function checkPolicyForOperation(ctx, operation, flags = []) {
|
|
27
|
+
const { policy } = operation;
|
|
28
|
+
const operationName = `${operation.meta.key}.v${operation.meta.version}`;
|
|
29
|
+
const missing = {};
|
|
30
|
+
if (!checkAuthLevel(ctx, policy.auth)) return {
|
|
31
|
+
allowed: false,
|
|
32
|
+
reason: `Operation "${operationName}" requires "${policy.auth}" auth level`,
|
|
33
|
+
missing: { roles: policy.auth === "admin" ? ["admin"] : void 0 }
|
|
34
|
+
};
|
|
35
|
+
if (policy.flags?.length) {
|
|
36
|
+
const availableFlags = new Set(flags);
|
|
37
|
+
const missingFlags = policy.flags.filter((f) => !availableFlags.has(f));
|
|
38
|
+
if (missingFlags.length > 0) {
|
|
39
|
+
missing.flags = missingFlags;
|
|
40
|
+
return {
|
|
41
|
+
allowed: false,
|
|
42
|
+
reason: `Operation "${operationName}" requires feature flags: ${missingFlags.join(", ")}`,
|
|
43
|
+
missing
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { allowed: true };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Assert that an operation's policy constraints are satisfied.
|
|
51
|
+
*
|
|
52
|
+
* @param ctx - Policy context to check against
|
|
53
|
+
* @param operation - Operation spec to check
|
|
54
|
+
* @param flags - Available feature flags
|
|
55
|
+
* @throws {PolicyViolationError} If policy is not satisfied
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* // Throws if policy not satisfied
|
|
60
|
+
* assertPolicyForOperation(ctx, myOperation);
|
|
61
|
+
*
|
|
62
|
+
* // Safe to proceed with operation
|
|
63
|
+
* await handler(input);
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
function assertPolicyForOperation(ctx, operation, flags = []) {
|
|
67
|
+
const result = checkPolicyForOperation(ctx, operation, flags);
|
|
68
|
+
if (!result.allowed) {
|
|
69
|
+
const operationName$1 = `${operation.meta.key}.v${operation.meta.version}`;
|
|
70
|
+
ctx.auditAccess(operationName$1, "denied", result.reason);
|
|
71
|
+
throw new PolicyViolationError("access_denied", {
|
|
72
|
+
operation: operationName$1,
|
|
73
|
+
reason: result.reason
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const operationName = `${operation.meta.key}.v${operation.meta.version}`;
|
|
77
|
+
ctx.auditAccess(operationName, "allowed");
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Filter operations to only those with satisfied policy constraints.
|
|
81
|
+
*
|
|
82
|
+
* @param ctx - Policy context to check against
|
|
83
|
+
* @param operations - Operations to filter
|
|
84
|
+
* @param flags - Available feature flags
|
|
85
|
+
* @returns Operations that have their policies satisfied
|
|
86
|
+
*/
|
|
87
|
+
function filterOperationsByPolicy(ctx, operations, flags = []) {
|
|
88
|
+
return operations.filter((op) => checkPolicyForOperation(ctx, op, flags).allowed);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Check if user has the required role for an operation.
|
|
92
|
+
*
|
|
93
|
+
* @param ctx - Policy context to check against
|
|
94
|
+
* @param requiredRole - Role required for the operation
|
|
95
|
+
* @param operation - Optional operation name for error messages
|
|
96
|
+
* @returns Guard result
|
|
97
|
+
*/
|
|
98
|
+
function checkRole(ctx, requiredRole, operation) {
|
|
99
|
+
if (ctx.hasRole(requiredRole)) return { allowed: true };
|
|
100
|
+
return {
|
|
101
|
+
allowed: false,
|
|
102
|
+
reason: operation ? `Operation "${operation}" requires role "${requiredRole}"` : `Missing required role "${requiredRole}"`,
|
|
103
|
+
missing: { roles: [requiredRole] }
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Assert user has the required role.
|
|
108
|
+
*
|
|
109
|
+
* @param ctx - Policy context to check against
|
|
110
|
+
* @param requiredRole - Role required
|
|
111
|
+
* @param operation - Optional operation name for error messages
|
|
112
|
+
* @throws {PolicyViolationError} If role is missing
|
|
113
|
+
*/
|
|
114
|
+
function assertRole(ctx, requiredRole, operation) {
|
|
115
|
+
if (!checkRole(ctx, requiredRole, operation).allowed) throw new PolicyViolationError("missing_role", {
|
|
116
|
+
operation,
|
|
117
|
+
requiredRole
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Check if user has any of the required roles.
|
|
122
|
+
*
|
|
123
|
+
* @param ctx - Policy context to check against
|
|
124
|
+
* @param requiredRoles - Any of these roles is sufficient
|
|
125
|
+
* @param operation - Optional operation name for error messages
|
|
126
|
+
* @returns Guard result
|
|
127
|
+
*/
|
|
128
|
+
function checkAnyRole(ctx, requiredRoles, operation) {
|
|
129
|
+
if (ctx.hasAnyRole(requiredRoles)) return { allowed: true };
|
|
130
|
+
return {
|
|
131
|
+
allowed: false,
|
|
132
|
+
reason: operation ? `Operation "${operation}" requires one of roles: ${requiredRoles.join(", ")}` : `Missing required role (need one of: ${requiredRoles.join(", ")})`,
|
|
133
|
+
missing: { roles: requiredRoles }
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Check if user has the required permission.
|
|
138
|
+
*
|
|
139
|
+
* @param ctx - Policy context to check against
|
|
140
|
+
* @param requiredPermission - Permission required
|
|
141
|
+
* @param operation - Optional operation name for error messages
|
|
142
|
+
* @returns Guard result
|
|
143
|
+
*/
|
|
144
|
+
function checkPermission(ctx, requiredPermission, operation) {
|
|
145
|
+
if (ctx.hasPermission(requiredPermission)) return { allowed: true };
|
|
146
|
+
return {
|
|
147
|
+
allowed: false,
|
|
148
|
+
reason: operation ? `Operation "${operation}" requires permission "${requiredPermission}"` : `Missing required permission "${requiredPermission}"`,
|
|
149
|
+
missing: { permissions: [requiredPermission] }
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Assert user has the required permission.
|
|
154
|
+
*
|
|
155
|
+
* @param ctx - Policy context to check against
|
|
156
|
+
* @param requiredPermission - Permission required
|
|
157
|
+
* @param operation - Optional operation name for error messages
|
|
158
|
+
* @throws {PolicyViolationError} If permission is missing
|
|
159
|
+
*/
|
|
160
|
+
function assertPermission(ctx, requiredPermission, operation) {
|
|
161
|
+
if (!checkPermission(ctx, requiredPermission, operation).allowed) throw new PolicyViolationError("missing_permission", {
|
|
162
|
+
operation,
|
|
163
|
+
requiredPermission
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Check if user has all of the required permissions.
|
|
168
|
+
*
|
|
169
|
+
* @param ctx - Policy context to check against
|
|
170
|
+
* @param requiredPermissions - All of these permissions are required
|
|
171
|
+
* @param operation - Optional operation name for error messages
|
|
172
|
+
* @returns Guard result
|
|
173
|
+
*/
|
|
174
|
+
function checkAllPermissions(ctx, requiredPermissions, operation) {
|
|
175
|
+
const missing = requiredPermissions.filter((p) => !ctx.hasPermission(p));
|
|
176
|
+
if (missing.length === 0) return { allowed: true };
|
|
177
|
+
return {
|
|
178
|
+
allowed: false,
|
|
179
|
+
reason: operation ? `Operation "${operation}" requires permissions: ${missing.join(", ")}` : `Missing required permissions: ${missing.join(", ")}`,
|
|
180
|
+
missing: { permissions: missing }
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Check multiple policy requirements at once.
|
|
185
|
+
*
|
|
186
|
+
* @param ctx - Policy context to check against
|
|
187
|
+
* @param requirements - Combined requirements to check
|
|
188
|
+
* @param flags - Available feature flags
|
|
189
|
+
* @param operation - Optional operation name for error messages
|
|
190
|
+
* @returns Guard result
|
|
191
|
+
*/
|
|
192
|
+
function checkCombinedPolicy(ctx, requirements, flags = [], operation) {
|
|
193
|
+
const missing = {};
|
|
194
|
+
const reasons = [];
|
|
195
|
+
if (requirements.roles?.length) {
|
|
196
|
+
const missingRoles = requirements.roles.filter((r) => !ctx.hasRole(r));
|
|
197
|
+
if (missingRoles.length > 0) {
|
|
198
|
+
missing.roles = missingRoles;
|
|
199
|
+
reasons.push(`Missing roles: ${missingRoles.join(", ")}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (requirements.anyRole?.length) {
|
|
203
|
+
if (!ctx.hasAnyRole(requirements.anyRole)) {
|
|
204
|
+
missing.roles = [...missing.roles ?? [], ...requirements.anyRole];
|
|
205
|
+
reasons.push(`Need one of roles: ${requirements.anyRole.join(", ")}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (requirements.permissions?.length) {
|
|
209
|
+
const missingPerms = requirements.permissions.filter((p) => !ctx.hasPermission(p));
|
|
210
|
+
if (missingPerms.length > 0) {
|
|
211
|
+
missing.permissions = missingPerms;
|
|
212
|
+
reasons.push(`Missing permissions: ${missingPerms.join(", ")}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (requirements.anyPermission?.length) {
|
|
216
|
+
if (!ctx.hasAnyPermission(requirements.anyPermission)) {
|
|
217
|
+
missing.permissions = [...missing.permissions ?? [], ...requirements.anyPermission];
|
|
218
|
+
reasons.push(`Need one of permissions: ${requirements.anyPermission.join(", ")}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (requirements.flags?.length) {
|
|
222
|
+
const availableFlags = new Set(flags);
|
|
223
|
+
const missingFlags = requirements.flags.filter((f) => !availableFlags.has(f));
|
|
224
|
+
if (missingFlags.length > 0) {
|
|
225
|
+
missing.flags = missingFlags;
|
|
226
|
+
reasons.push(`Missing feature flags: ${missingFlags.join(", ")}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (reasons.length > 0) return {
|
|
230
|
+
allowed: false,
|
|
231
|
+
reason: (operation ? `Operation "${operation}": ` : "") + reasons.join("; "),
|
|
232
|
+
missing
|
|
233
|
+
};
|
|
234
|
+
return { allowed: true };
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Assert multiple policy requirements at once.
|
|
238
|
+
*
|
|
239
|
+
* @param ctx - Policy context to check against
|
|
240
|
+
* @param requirements - Combined requirements to check
|
|
241
|
+
* @param flags - Available feature flags
|
|
242
|
+
* @param operation - Optional operation name for error messages
|
|
243
|
+
* @throws {PolicyViolationError} If any requirement is not met
|
|
244
|
+
*/
|
|
245
|
+
function assertCombinedPolicy(ctx, requirements, flags = [], operation) {
|
|
246
|
+
const result = checkCombinedPolicy(ctx, requirements, flags, operation);
|
|
247
|
+
if (!result.allowed) throw new PolicyViolationError("access_denied", {
|
|
248
|
+
operation,
|
|
249
|
+
reason: result.reason
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
//#endregion
|
|
254
|
+
export { assertCombinedPolicy, assertPermission, assertPolicyForOperation, assertRole, checkAllPermissions, checkAnyRole, checkCombinedPolicy, checkPermission, checkPolicyForOperation, checkRole, filterOperationsByPolicy };
|
package/dist/policy/index.d.ts
CHANGED
|
@@ -2,4 +2,15 @@ import { AttributeMatcher, ConsentDefinition, FieldPolicyRule, PIIPolicy, Policy
|
|
|
2
2
|
import { PolicyRegistry } from "./registry.js";
|
|
3
3
|
import { DecisionContext, PolicyEngine, ResourceContext, SubjectContext, SubjectRelationship } from "./engine.js";
|
|
4
4
|
import { OPAAdapterOptions, OPAClient, OPAEvaluationResult, OPAPolicyAdapter, buildOPAInput } from "./opa-adapter.js";
|
|
5
|
-
|
|
5
|
+
import { AuditEntry, AuditHandler, PolicyContext, PolicyContextOptions, PolicyUser, PolicyViolationDetails, PolicyViolationError, PolicyViolationType, RateLimitResult, RateLimitState, createAnonymousPolicyContext, createBypassPolicyContext, createPolicyContext } from "./context.js";
|
|
6
|
+
import { CombinedPolicyRequirements, PolicyGuardResult, assertCombinedPolicy, assertPermission, assertPolicyForOperation, assertRole, checkAllPermissions, checkAnyRole, checkCombinedPolicy, checkPermission, checkPolicyForOperation, checkRole, filterOperationsByPolicy } from "./guards.js";
|
|
7
|
+
import { PolicyConsistencyDeps, PolicyValidationError, PolicyValidationIssue, PolicyValidationLevel, PolicyValidationResult, assertPolicyConsistency, assertPolicySpecValid, validatePolicyConsistency, validatePolicySpec } from "./validation.js";
|
|
8
|
+
|
|
9
|
+
//#region src/policy/index.d.ts
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Helper to define a Policy.
|
|
13
|
+
*/
|
|
14
|
+
declare const definePolicy: (spec: PolicySpec) => PolicySpec;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { AttributeMatcher, AuditEntry, AuditHandler, CombinedPolicyRequirements, ConsentDefinition, DecisionContext, FieldPolicyRule, OPAAdapterOptions, OPAClient, OPAEvaluationResult, OPAPolicyAdapter, PIIPolicy, PolicyCondition, PolicyConsistencyDeps, PolicyContext, PolicyContextOptions, PolicyEffect, PolicyEngine, PolicyGuardResult, PolicyMeta, PolicyOPAConfig, PolicyRef, PolicyRegistry, PolicyRule, PolicySpec, PolicyUser, PolicyValidationError, PolicyValidationIssue, PolicyValidationLevel, PolicyValidationResult, PolicyViolationDetails, PolicyViolationError, PolicyViolationType, RateLimitDefinition, RateLimitResult, RateLimitState, RelationshipDefinition, RelationshipMatcher, ResourceContext, ResourceMatcher, SubjectContext, SubjectMatcher, SubjectRelationship, assertCombinedPolicy, assertPermission, assertPolicyConsistency, assertPolicyForOperation, assertPolicySpecValid, assertRole, buildOPAInput, checkAllPermissions, checkAnyRole, checkCombinedPolicy, checkPermission, checkPolicyForOperation, checkRole, createAnonymousPolicyContext, createBypassPolicyContext, createPolicyContext, definePolicy, filterOperationsByPolicy, validatePolicyConsistency, validatePolicySpec };
|
package/dist/policy/index.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { PolicyRegistry } from "./registry.js";
|
|
2
2
|
import { PolicyEngine } from "./engine.js";
|
|
3
3
|
import { OPAPolicyAdapter, buildOPAInput } from "./opa-adapter.js";
|
|
4
|
+
import { PolicyViolationError, createAnonymousPolicyContext, createBypassPolicyContext, createPolicyContext } from "./context.js";
|
|
5
|
+
import { assertCombinedPolicy, assertPermission, assertPolicyForOperation, assertRole, checkAllPermissions, checkAnyRole, checkCombinedPolicy, checkPermission, checkPolicyForOperation, checkRole, filterOperationsByPolicy } from "./guards.js";
|
|
6
|
+
import { PolicyValidationError, assertPolicyConsistency, assertPolicySpecValid, validatePolicyConsistency, validatePolicySpec } from "./validation.js";
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
//#region src/policy/index.ts
|
|
9
|
+
/**
|
|
10
|
+
* Helper to define a Policy.
|
|
11
|
+
*/
|
|
12
|
+
const definePolicy = (spec) => spec;
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
export { OPAPolicyAdapter, PolicyEngine, PolicyRegistry, PolicyValidationError, PolicyViolationError, assertCombinedPolicy, assertPermission, assertPolicyConsistency, assertPolicyForOperation, assertPolicySpecValid, assertRole, buildOPAInput, checkAllPermissions, checkAnyRole, checkCombinedPolicy, checkPermission, checkPolicyForOperation, checkRole, createAnonymousPolicyContext, createBypassPolicyContext, createPolicyContext, definePolicy, filterOperationsByPolicy, validatePolicyConsistency, validatePolicySpec };
|
package/dist/policy/spec.d.ts
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { PolicySpec } from "./spec.js";
|
|
2
|
+
import { OperationSpecRegistry } from "../operations/registry.js";
|
|
3
|
+
import { PolicyRegistry } from "./registry.js";
|
|
4
|
+
|
|
5
|
+
//#region src/policy/validation.d.ts
|
|
6
|
+
|
|
7
|
+
type PolicyValidationLevel = 'error' | 'warning' | 'info';
|
|
8
|
+
interface PolicyValidationIssue {
|
|
9
|
+
level: PolicyValidationLevel;
|
|
10
|
+
message: string;
|
|
11
|
+
path?: string;
|
|
12
|
+
context?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
interface PolicyValidationResult {
|
|
15
|
+
valid: boolean;
|
|
16
|
+
issues: PolicyValidationIssue[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Validate a policy spec for internal consistency.
|
|
20
|
+
*
|
|
21
|
+
* @param spec - Policy spec to validate
|
|
22
|
+
* @returns Validation result with any issues found
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const result = validatePolicySpec(myPolicy);
|
|
27
|
+
* if (!result.valid) {
|
|
28
|
+
* console.log('Issues:', result.issues);
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
declare function validatePolicySpec(spec: PolicySpec): PolicyValidationResult;
|
|
33
|
+
interface PolicyConsistencyDeps {
|
|
34
|
+
policies: PolicyRegistry;
|
|
35
|
+
operations?: OperationSpecRegistry;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Validate policy consistency across registries.
|
|
39
|
+
*
|
|
40
|
+
* Checks that:
|
|
41
|
+
* - Operations reference valid policies
|
|
42
|
+
* - Policy actions align with operation kinds
|
|
43
|
+
*
|
|
44
|
+
* @param deps - Registry dependencies
|
|
45
|
+
* @returns Validation result
|
|
46
|
+
*/
|
|
47
|
+
declare function validatePolicyConsistency(deps: PolicyConsistencyDeps): PolicyValidationResult;
|
|
48
|
+
declare class PolicyValidationError extends Error {
|
|
49
|
+
readonly issues: PolicyValidationIssue[];
|
|
50
|
+
constructor(message: string, issues: PolicyValidationIssue[]);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Assert that a policy spec is valid, throwing if not.
|
|
54
|
+
*
|
|
55
|
+
* @param spec - Policy spec to validate
|
|
56
|
+
* @throws {PolicyValidationError} If validation fails
|
|
57
|
+
*/
|
|
58
|
+
declare function assertPolicySpecValid(spec: PolicySpec): void;
|
|
59
|
+
/**
|
|
60
|
+
* Assert policy consistency across registries.
|
|
61
|
+
*
|
|
62
|
+
* @param deps - Registry dependencies
|
|
63
|
+
* @throws {PolicyValidationError} If validation fails
|
|
64
|
+
*/
|
|
65
|
+
declare function assertPolicyConsistency(deps: PolicyConsistencyDeps): void;
|
|
66
|
+
//#endregion
|
|
67
|
+
export { PolicyConsistencyDeps, PolicyValidationError, PolicyValidationIssue, PolicyValidationLevel, PolicyValidationResult, assertPolicyConsistency, assertPolicySpecValid, validatePolicyConsistency, validatePolicySpec };
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
//#region src/policy/validation.ts
|
|
2
|
+
/**
|
|
3
|
+
* Validate a policy spec for internal consistency.
|
|
4
|
+
*
|
|
5
|
+
* @param spec - Policy spec to validate
|
|
6
|
+
* @returns Validation result with any issues found
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const result = validatePolicySpec(myPolicy);
|
|
11
|
+
* if (!result.valid) {
|
|
12
|
+
* console.log('Issues:', result.issues);
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
function validatePolicySpec(spec) {
|
|
17
|
+
const issues = [];
|
|
18
|
+
validateMeta(spec, issues);
|
|
19
|
+
validateRules(spec, issues);
|
|
20
|
+
validateFieldPolicies(spec, issues);
|
|
21
|
+
validateRateLimits(spec, issues);
|
|
22
|
+
validateConsents(spec, issues);
|
|
23
|
+
validateRelationships(spec, issues);
|
|
24
|
+
return {
|
|
25
|
+
valid: issues.filter((i) => i.level === "error").length === 0,
|
|
26
|
+
issues
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function validateMeta(spec, issues) {
|
|
30
|
+
const { meta } = spec;
|
|
31
|
+
if (!meta.key?.trim()) issues.push({
|
|
32
|
+
level: "error",
|
|
33
|
+
message: "Policy must have a non-empty key",
|
|
34
|
+
path: "meta.key"
|
|
35
|
+
});
|
|
36
|
+
if (!meta.version?.trim()) issues.push({
|
|
37
|
+
level: "error",
|
|
38
|
+
message: "Policy must have a non-empty version",
|
|
39
|
+
path: "meta.version"
|
|
40
|
+
});
|
|
41
|
+
if (!meta.owners?.length) issues.push({
|
|
42
|
+
level: "warning",
|
|
43
|
+
message: "Policy should specify owners",
|
|
44
|
+
path: "meta.owners"
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function validateRules(spec, issues) {
|
|
48
|
+
if (!spec.rules?.length) {
|
|
49
|
+
issues.push({
|
|
50
|
+
level: "warning",
|
|
51
|
+
message: "Policy has no rules defined",
|
|
52
|
+
path: "rules"
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const seenRateRefs = /* @__PURE__ */ new Set();
|
|
57
|
+
spec.rules.forEach((rule, index) => {
|
|
58
|
+
const path = `rules[${index}]`;
|
|
59
|
+
if (!rule.actions?.length) issues.push({
|
|
60
|
+
level: "error",
|
|
61
|
+
message: "Rule must specify at least one action",
|
|
62
|
+
path: `${path}.actions`
|
|
63
|
+
});
|
|
64
|
+
if (!["allow", "deny"].includes(rule.effect)) issues.push({
|
|
65
|
+
level: "error",
|
|
66
|
+
message: `Invalid rule effect: ${rule.effect}`,
|
|
67
|
+
path: `${path}.effect`
|
|
68
|
+
});
|
|
69
|
+
if (typeof rule.rateLimit === "string") {
|
|
70
|
+
seenRateRefs.add(rule.rateLimit);
|
|
71
|
+
if (!spec.rateLimits?.some((rl) => rl.id === rule.rateLimit)) issues.push({
|
|
72
|
+
level: "error",
|
|
73
|
+
message: `Rate limit "${rule.rateLimit}" referenced but not defined`,
|
|
74
|
+
path: `${path}.rateLimit`
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (rule.requiresConsent?.length) {
|
|
78
|
+
for (const consentId of rule.requiresConsent) if (!spec.consents?.some((c) => c.id === consentId)) issues.push({
|
|
79
|
+
level: "error",
|
|
80
|
+
message: `Consent "${consentId}" referenced but not defined`,
|
|
81
|
+
path: `${path}.requiresConsent`
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
validateConditions(rule.conditions, `${path}.conditions`, issues);
|
|
85
|
+
});
|
|
86
|
+
if (spec.rateLimits?.length) {
|
|
87
|
+
for (const rl of spec.rateLimits) if (!seenRateRefs.has(rl.id)) {
|
|
88
|
+
if (!spec.rules.some((r) => typeof r.rateLimit === "object" && r.rateLimit.id === rl.id)) issues.push({
|
|
89
|
+
level: "info",
|
|
90
|
+
message: `Rate limit "${rl.id}" is defined but not referenced`,
|
|
91
|
+
path: `rateLimits`
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function validateFieldPolicies(spec, issues) {
|
|
97
|
+
if (!spec.fieldPolicies?.length) return;
|
|
98
|
+
const seenFields = /* @__PURE__ */ new Map();
|
|
99
|
+
spec.fieldPolicies.forEach((rule, index) => {
|
|
100
|
+
const path = `fieldPolicies[${index}]`;
|
|
101
|
+
if (!rule.field?.trim()) issues.push({
|
|
102
|
+
level: "error",
|
|
103
|
+
message: "Field policy must specify a field",
|
|
104
|
+
path: `${path}.field`
|
|
105
|
+
});
|
|
106
|
+
if (!rule.actions?.length) issues.push({
|
|
107
|
+
level: "error",
|
|
108
|
+
message: "Field policy must specify at least one action",
|
|
109
|
+
path: `${path}.actions`
|
|
110
|
+
});
|
|
111
|
+
for (const action of rule.actions) if (!["read", "write"].includes(action)) issues.push({
|
|
112
|
+
level: "error",
|
|
113
|
+
message: `Invalid field action: ${action}`,
|
|
114
|
+
path: `${path}.actions`
|
|
115
|
+
});
|
|
116
|
+
const existing = seenFields.get(rule.field) ?? [];
|
|
117
|
+
existing.push(rule);
|
|
118
|
+
seenFields.set(rule.field, existing);
|
|
119
|
+
validateConditions(rule.conditions, `${path}.conditions`, issues);
|
|
120
|
+
});
|
|
121
|
+
for (const [field, rules] of seenFields.entries()) if (rules.length > 1) {
|
|
122
|
+
if (rules.some((r) => r.effect === "allow") && rules.some((r) => r.effect === "deny")) issues.push({
|
|
123
|
+
level: "warning",
|
|
124
|
+
message: `Field "${field}" has potentially conflicting allow/deny policies`,
|
|
125
|
+
path: "fieldPolicies",
|
|
126
|
+
context: {
|
|
127
|
+
field,
|
|
128
|
+
ruleCount: rules.length
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function validateRateLimits(spec, issues) {
|
|
134
|
+
if (!spec.rateLimits?.length) return;
|
|
135
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
136
|
+
spec.rateLimits.forEach((rl, index) => {
|
|
137
|
+
const path = `rateLimits[${index}]`;
|
|
138
|
+
if (!rl.id?.trim()) issues.push({
|
|
139
|
+
level: "error",
|
|
140
|
+
message: "Rate limit must have an id",
|
|
141
|
+
path: `${path}.id`
|
|
142
|
+
});
|
|
143
|
+
else if (seenIds.has(rl.id)) issues.push({
|
|
144
|
+
level: "error",
|
|
145
|
+
message: `Duplicate rate limit id: ${rl.id}`,
|
|
146
|
+
path: `${path}.id`
|
|
147
|
+
});
|
|
148
|
+
else seenIds.add(rl.id);
|
|
149
|
+
if (typeof rl.rpm !== "number" || rl.rpm <= 0) issues.push({
|
|
150
|
+
level: "error",
|
|
151
|
+
message: "Rate limit rpm must be a positive number",
|
|
152
|
+
path: `${path}.rpm`
|
|
153
|
+
});
|
|
154
|
+
if (rl.windowSeconds !== void 0 && rl.windowSeconds <= 0) issues.push({
|
|
155
|
+
level: "error",
|
|
156
|
+
message: "Rate limit windowSeconds must be positive if specified",
|
|
157
|
+
path: `${path}.windowSeconds`
|
|
158
|
+
});
|
|
159
|
+
if (rl.burst !== void 0 && rl.burst < 0) issues.push({
|
|
160
|
+
level: "error",
|
|
161
|
+
message: "Rate limit burst must be non-negative if specified",
|
|
162
|
+
path: `${path}.burst`
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function validateConsents(spec, issues) {
|
|
167
|
+
if (!spec.consents?.length) return;
|
|
168
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
169
|
+
spec.consents.forEach((consent, index) => {
|
|
170
|
+
const path = `consents[${index}]`;
|
|
171
|
+
if (!consent.id?.trim()) issues.push({
|
|
172
|
+
level: "error",
|
|
173
|
+
message: "Consent must have an id",
|
|
174
|
+
path: `${path}.id`
|
|
175
|
+
});
|
|
176
|
+
else if (seenIds.has(consent.id)) issues.push({
|
|
177
|
+
level: "error",
|
|
178
|
+
message: `Duplicate consent id: ${consent.id}`,
|
|
179
|
+
path: `${path}.id`
|
|
180
|
+
});
|
|
181
|
+
else seenIds.add(consent.id);
|
|
182
|
+
if (!consent.scope?.trim()) issues.push({
|
|
183
|
+
level: "error",
|
|
184
|
+
message: "Consent must specify a scope",
|
|
185
|
+
path: `${path}.scope`
|
|
186
|
+
});
|
|
187
|
+
if (!consent.purpose?.trim()) issues.push({
|
|
188
|
+
level: "error",
|
|
189
|
+
message: "Consent must specify a purpose",
|
|
190
|
+
path: `${path}.purpose`
|
|
191
|
+
});
|
|
192
|
+
if (consent.expiresInDays !== void 0 && consent.expiresInDays <= 0) issues.push({
|
|
193
|
+
level: "error",
|
|
194
|
+
message: "Consent expiresInDays must be positive if specified",
|
|
195
|
+
path: `${path}.expiresInDays`
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function validateRelationships(spec, issues) {
|
|
200
|
+
if (!spec.relationships?.length) return;
|
|
201
|
+
const seenRelations = /* @__PURE__ */ new Set();
|
|
202
|
+
spec.relationships.forEach((rel, index) => {
|
|
203
|
+
const path = `relationships[${index}]`;
|
|
204
|
+
const key = `${rel.subjectType}:${rel.relation}:${rel.objectType}`;
|
|
205
|
+
if (!rel.subjectType?.trim()) issues.push({
|
|
206
|
+
level: "error",
|
|
207
|
+
message: "Relationship must specify subjectType",
|
|
208
|
+
path: `${path}.subjectType`
|
|
209
|
+
});
|
|
210
|
+
if (!rel.relation?.trim()) issues.push({
|
|
211
|
+
level: "error",
|
|
212
|
+
message: "Relationship must specify relation",
|
|
213
|
+
path: `${path}.relation`
|
|
214
|
+
});
|
|
215
|
+
if (!rel.objectType?.trim()) issues.push({
|
|
216
|
+
level: "error",
|
|
217
|
+
message: "Relationship must specify objectType",
|
|
218
|
+
path: `${path}.objectType`
|
|
219
|
+
});
|
|
220
|
+
if (seenRelations.has(key)) issues.push({
|
|
221
|
+
level: "warning",
|
|
222
|
+
message: `Duplicate relationship definition: ${key}`,
|
|
223
|
+
path
|
|
224
|
+
});
|
|
225
|
+
else seenRelations.add(key);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function validateConditions(conditions, path, issues) {
|
|
229
|
+
if (!conditions?.length) return;
|
|
230
|
+
conditions.forEach((cond, index) => {
|
|
231
|
+
if (!cond.expression?.trim()) issues.push({
|
|
232
|
+
level: "error",
|
|
233
|
+
message: "Condition must have a non-empty expression",
|
|
234
|
+
path: `${path}[${index}].expression`
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Validate policy consistency across registries.
|
|
240
|
+
*
|
|
241
|
+
* Checks that:
|
|
242
|
+
* - Operations reference valid policies
|
|
243
|
+
* - Policy actions align with operation kinds
|
|
244
|
+
*
|
|
245
|
+
* @param deps - Registry dependencies
|
|
246
|
+
* @returns Validation result
|
|
247
|
+
*/
|
|
248
|
+
function validatePolicyConsistency(deps) {
|
|
249
|
+
const issues = [];
|
|
250
|
+
for (const policy of deps.policies.list()) {
|
|
251
|
+
const specResult = validatePolicySpec(policy);
|
|
252
|
+
issues.push(...specResult.issues.map((i) => ({
|
|
253
|
+
...i,
|
|
254
|
+
path: `${policy.meta.key}.v${policy.meta.version}${i.path ? `.${i.path}` : ""}`
|
|
255
|
+
})));
|
|
256
|
+
}
|
|
257
|
+
if (deps.operations) for (const operation of deps.operations.list()) {
|
|
258
|
+
const policyRefs = operation.policy.policies ?? [];
|
|
259
|
+
for (const ref of policyRefs) if (!deps.policies.get(ref.key, ref.version)) issues.push({
|
|
260
|
+
level: "error",
|
|
261
|
+
message: `Operation "${operation.meta.key}.v${operation.meta.version}" references unknown policy "${ref.key}.v${ref.version}"`,
|
|
262
|
+
path: `operations.${operation.meta.key}.policy.policies`
|
|
263
|
+
});
|
|
264
|
+
const fieldPolicies = operation.policy.fieldPolicies ?? [];
|
|
265
|
+
for (const fp of fieldPolicies) if (fp.policy) {
|
|
266
|
+
if (!deps.policies.get(fp.policy.key, fp.policy.version)) issues.push({
|
|
267
|
+
level: "error",
|
|
268
|
+
message: `Operation "${operation.meta.key}.v${operation.meta.version}" references unknown field policy "${fp.policy.key}.v${fp.policy.version}"`,
|
|
269
|
+
path: `operations.${operation.meta.key}.policy.fieldPolicies`
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
valid: issues.filter((i) => i.level === "error").length === 0,
|
|
275
|
+
issues
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
var PolicyValidationError = class extends Error {
|
|
279
|
+
constructor(message, issues) {
|
|
280
|
+
super(message);
|
|
281
|
+
this.issues = issues;
|
|
282
|
+
this.name = "PolicyValidationError";
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
/**
|
|
286
|
+
* Assert that a policy spec is valid, throwing if not.
|
|
287
|
+
*
|
|
288
|
+
* @param spec - Policy spec to validate
|
|
289
|
+
* @throws {PolicyValidationError} If validation fails
|
|
290
|
+
*/
|
|
291
|
+
function assertPolicySpecValid(spec) {
|
|
292
|
+
const result = validatePolicySpec(spec);
|
|
293
|
+
if (!result.valid) throw new PolicyValidationError(`Policy ${spec.meta.key}.v${spec.meta.version} is invalid`, result.issues);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Assert policy consistency across registries.
|
|
297
|
+
*
|
|
298
|
+
* @param deps - Registry dependencies
|
|
299
|
+
* @throws {PolicyValidationError} If validation fails
|
|
300
|
+
*/
|
|
301
|
+
function assertPolicyConsistency(deps) {
|
|
302
|
+
const result = validatePolicyConsistency(deps);
|
|
303
|
+
if (!result.valid) throw new PolicyValidationError("Policy consistency check failed", result.issues);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
//#endregion
|
|
307
|
+
export { PolicyValidationError, assertPolicyConsistency, assertPolicySpecValid, validatePolicyConsistency, validatePolicySpec };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CapabilityRef } from "../capabilities/capabilities.js";
|
|
1
2
|
import { OwnerShipMeta } from "../ownership.js";
|
|
2
3
|
import { AnySchemaModel } from "@contractspec/lib.schema";
|
|
3
4
|
import { BlockConfig } from "@blocknote/core";
|
|
@@ -38,6 +39,11 @@ type PresentationSource = PresentationSourceComponentReact | PresentationSourceB
|
|
|
38
39
|
*/
|
|
39
40
|
interface PresentationSpec {
|
|
40
41
|
meta: PresentationSpecMeta;
|
|
42
|
+
/**
|
|
43
|
+
* Optional reference to the capability that provides this presentation.
|
|
44
|
+
* Used for bidirectional linking between capabilities and presentations.
|
|
45
|
+
*/
|
|
46
|
+
capability?: CapabilityRef;
|
|
41
47
|
policy?: {
|
|
42
48
|
flags?: string[];
|
|
43
49
|
pii?: string[];
|