toride 0.1.0 → 0.2.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.
@@ -54,6 +54,7 @@ var FieldAccessDefSchema = v.object({
54
54
  var ResourceBlockSchema = v.object({
55
55
  roles: v.array(v.string()),
56
56
  permissions: v.array(v.string()),
57
+ attributes: v.optional(v.record(v.string(), AttributeTypeSchema)),
57
58
  relations: v.optional(v.record(v.string(), v.string())),
58
59
  grants: v.optional(v.record(v.string(), v.array(v.string()))),
59
60
  derived_roles: v.optional(v.array(DerivedRoleEntrySchema)),
@@ -2031,8 +2032,11 @@ var Toride = class {
2031
2032
  * Default-deny: returns false if resource type is unknown or no grants match.
2032
2033
  */
2033
2034
  async can(actor, action, resource, options) {
2034
- const result = await this.evaluateInternal(actor, action, resource, options);
2035
- this.fireDecisionEvent(actor, action, resource, result);
2035
+ const a = actor;
2036
+ const r = resource;
2037
+ const act = action;
2038
+ const result = await this.evaluateInternal(a, act, r, options);
2039
+ this.fireDecisionEvent(a, act, r, result);
2036
2040
  return result.allowed;
2037
2041
  }
2038
2042
  /**
@@ -2048,8 +2052,11 @@ var Toride = class {
2048
2052
  * granted permissions, matched rules, and human-readable final decision.
2049
2053
  */
2050
2054
  async explain(actor, action, resource, options) {
2051
- const result = await this.evaluateInternal(actor, action, resource, options);
2052
- this.fireDecisionEvent(actor, action, resource, result);
2055
+ const a = actor;
2056
+ const r = resource;
2057
+ const act = action;
2058
+ const result = await this.evaluateInternal(a, act, r, options);
2059
+ this.fireDecisionEvent(a, act, r, result);
2053
2060
  return result;
2054
2061
  }
2055
2062
  /**
@@ -2057,7 +2064,9 @@ var Toride = class {
2057
2064
  * Uses a shared cache across all per-action evaluations.
2058
2065
  */
2059
2066
  async permittedActions(actor, resource, options) {
2060
- const resourceBlock = this.policy.resources[resource.type];
2067
+ const a = actor;
2068
+ const r = resource;
2069
+ const resourceBlock = this.policy.resources[r.type];
2061
2070
  if (!resourceBlock) {
2062
2071
  return [];
2063
2072
  }
@@ -2065,9 +2074,9 @@ var Toride = class {
2065
2074
  const permitted = [];
2066
2075
  for (const action of resourceBlock.permissions) {
2067
2076
  const result = await this.evaluateInternal(
2068
- actor,
2077
+ a,
2069
2078
  action,
2070
- resource,
2079
+ r,
2071
2080
  options,
2072
2081
  sharedCache
2073
2082
  );
@@ -2084,7 +2093,12 @@ var Toride = class {
2084
2093
  * Suitable for serializing to the client via TorideClient.
2085
2094
  */
2086
2095
  async snapshot(actor, resources, options) {
2087
- return snapshot(this, actor, resources, options);
2096
+ return snapshot(
2097
+ this,
2098
+ actor,
2099
+ resources,
2100
+ options
2101
+ );
2088
2102
  }
2089
2103
  /**
2090
2104
  * T095: Check if an actor can perform a field-level operation on a specific field.
@@ -2092,33 +2106,52 @@ var Toride = class {
2092
2106
  * Unlisted fields are unrestricted: any actor with the resource-level permission can access them.
2093
2107
  */
2094
2108
  async canField(actor, operation, resource, field, options) {
2095
- const resourceBlock = this.policy.resources[resource.type];
2109
+ const r = resource;
2110
+ const resourceBlock = this.policy.resources[r.type];
2096
2111
  if (!resourceBlock) {
2097
2112
  return false;
2098
2113
  }
2099
- return canField(this, actor, operation, resource, field, resourceBlock.field_access, options);
2114
+ return canField(
2115
+ this,
2116
+ actor,
2117
+ operation,
2118
+ r,
2119
+ field,
2120
+ resourceBlock.field_access,
2121
+ options
2122
+ );
2100
2123
  }
2101
2124
  /**
2102
2125
  * T095: Return the list of declared field_access field names the actor can access
2103
2126
  * for the given operation. Only returns explicitly declared fields.
2104
2127
  */
2105
2128
  async permittedFields(actor, operation, resource, options) {
2106
- const resourceBlock = this.policy.resources[resource.type];
2129
+ const r = resource;
2130
+ const resourceBlock = this.policy.resources[r.type];
2107
2131
  if (!resourceBlock) {
2108
2132
  return [];
2109
2133
  }
2110
- return permittedFields(this, actor, operation, resource, resourceBlock.field_access, options);
2134
+ return permittedFields(
2135
+ this,
2136
+ actor,
2137
+ operation,
2138
+ r,
2139
+ resourceBlock.field_access,
2140
+ options
2141
+ );
2111
2142
  }
2112
2143
  /**
2113
2144
  * T070: Return flat deduplicated list of all resolved roles (direct + derived).
2114
2145
  */
2115
2146
  async resolvedRoles(actor, resource, options) {
2116
- const resourceBlock = this.policy.resources[resource.type];
2147
+ const a = actor;
2148
+ const r = resource;
2149
+ const resourceBlock = this.policy.resources[r.type];
2117
2150
  if (!resourceBlock) {
2118
2151
  return [];
2119
2152
  }
2120
2153
  const action = resourceBlock.permissions[0] ?? "__resolvedRoles__";
2121
- const result = await this.evaluateInternal(actor, action, resource, options);
2154
+ const result = await this.evaluateInternal(a, action, r, options);
2122
2155
  const directRoles = result.resolvedRoles.direct;
2123
2156
  const derivedRoleNames = result.resolvedRoles.derived.map((d) => d.role);
2124
2157
  return [.../* @__PURE__ */ new Set([...directRoles, ...derivedRoleNames])];
@@ -2131,17 +2164,20 @@ var Toride = class {
2131
2164
  if (checks.length === 0) {
2132
2165
  return [];
2133
2166
  }
2167
+ const a = actor;
2134
2168
  const sharedCache = new AttributeCache(this.resolvers);
2135
2169
  const results = [];
2136
2170
  for (const check of checks) {
2171
+ const r = check.resource;
2172
+ const act = check.action;
2137
2173
  const result = await this.evaluateInternal(
2138
- actor,
2139
- check.action,
2140
- check.resource,
2174
+ a,
2175
+ act,
2176
+ r,
2141
2177
  options,
2142
2178
  sharedCache
2143
2179
  );
2144
- this.fireDecisionEvent(actor, check.action, check.resource, result);
2180
+ this.fireDecisionEvent(a, act, r, result);
2145
2181
  results.push(result.allowed);
2146
2182
  }
2147
2183
  return results;
@@ -2151,11 +2187,14 @@ var Toride = class {
2151
2187
  * Returns ConstraintResult with unrestricted/forbidden sentinels or constraint AST.
2152
2188
  */
2153
2189
  async buildConstraints(actor, action, resourceType, options) {
2190
+ const a = actor;
2191
+ const act = action;
2192
+ const rt = resourceType;
2154
2193
  const cache = new AttributeCache(this.resolvers);
2155
2194
  const constraintResult = await buildConstraints(
2156
- actor,
2157
- action,
2158
- resourceType,
2195
+ a,
2196
+ act,
2197
+ rt,
2159
2198
  cache,
2160
2199
  this.policy,
2161
2200
  {
@@ -2164,7 +2203,7 @@ var Toride = class {
2164
2203
  customEvaluators: this.options.customEvaluators
2165
2204
  }
2166
2205
  );
2167
- this.fireQueryEvent(actor, action, resourceType, constraintResult);
2206
+ this.fireQueryEvent(a, act, rt, constraintResult);
2168
2207
  return constraintResult;
2169
2208
  }
2170
2209
  /**
@@ -0,0 +1,37 @@
1
+ // src/client.ts
2
+ var CLIENT_VERSION = "0.0.1";
3
+ var TorideClient = class {
4
+ permissions;
5
+ constructor(snapshot) {
6
+ this.permissions = /* @__PURE__ */ new Map();
7
+ for (const [key, actions] of Object.entries(snapshot)) {
8
+ this.permissions.set(key, new Set(actions));
9
+ }
10
+ }
11
+ /**
12
+ * Synchronous permission check.
13
+ * Returns true if the action is permitted for the resource, false otherwise.
14
+ * Unknown resources return false (default-deny).
15
+ */
16
+ can(action, resource) {
17
+ const key = `${resource.type}:${resource.id}`;
18
+ const actions = this.permissions.get(key);
19
+ if (!actions) return false;
20
+ return actions.has(action);
21
+ }
22
+ /**
23
+ * Return the list of permitted actions for a resource.
24
+ * Returns empty array for unknown resources.
25
+ */
26
+ permittedActions(resource) {
27
+ const key = `${resource.type}:${resource.id}`;
28
+ const actions = this.permissions.get(key);
29
+ if (!actions) return [];
30
+ return [...actions];
31
+ }
32
+ };
33
+
34
+ export {
35
+ CLIENT_VERSION,
36
+ TorideClient
37
+ };
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  runTestCases,
8
8
  validatePolicyResult,
9
9
  validatePolicyStrict
10
- } from "./chunk-QE4FJZEG.js";
10
+ } from "./chunk-24PMDTLE.js";
11
11
 
12
12
  // src/cli.ts
13
13
  import { readFileSync, readdirSync, statSync } from "fs";
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Shape constraint for Toride's type parameter.
3
+ * Each property is a union or mapped type that codegen fills with literals.
4
+ */
5
+ interface TorideSchema {
6
+ /** Union of all resource type names (e.g., "Document" | "Organization") */
7
+ resources: string;
8
+ /** Global union of all action/permission names across all resources */
9
+ actions: string;
10
+ /** Union of all actor type names (e.g., "User" | "ServiceAccount") */
11
+ actorTypes: string;
12
+ /** Per-resource permission unions: { Document: "read" | "write"; ... } */
13
+ permissionMap: {
14
+ [R in string]: string;
15
+ };
16
+ /** Per-resource role unions: { Document: "admin" | "editor"; ... } */
17
+ roleMap: {
18
+ [R in string]: string;
19
+ };
20
+ /** Per-resource attribute shapes: { Document: { status: string; ownerId: string }; ... } */
21
+ resourceAttributeMap: {
22
+ [R in string]: Record<string, unknown>;
23
+ };
24
+ /** Per-actor attribute shapes: { User: { email: string; is_admin: boolean }; ... } */
25
+ actorAttributeMap: {
26
+ [A in string]: Record<string, unknown>;
27
+ };
28
+ /** Per-resource relation maps: { Document: { org: "Organization" }; ... } */
29
+ relationMap: {
30
+ [R in string]: Record<string, string>;
31
+ };
32
+ }
33
+ /**
34
+ * Default schema where everything is string / Record<string, unknown>.
35
+ * Used when Toride is instantiated without a type parameter.
36
+ * Provides full backward compatibility with the current untyped API.
37
+ */
38
+ interface DefaultSchema extends TorideSchema {
39
+ resources: string;
40
+ actions: string;
41
+ actorTypes: string;
42
+ permissionMap: Record<string, string>;
43
+ roleMap: Record<string, string>;
44
+ resourceAttributeMap: Record<string, Record<string, unknown>>;
45
+ actorAttributeMap: Record<string, Record<string, unknown>>;
46
+ relationMap: Record<string, Record<string, string>>;
47
+ }
48
+ /**
49
+ * Represents an entity performing actions.
50
+ * Generic discriminated union over actor types in S.
51
+ * When S = DefaultSchema, collapses to the original untyped shape.
52
+ */
53
+ type ActorRef<S extends TorideSchema = DefaultSchema> = {
54
+ [A in S["actorTypes"]]: {
55
+ readonly type: A;
56
+ readonly id: string;
57
+ readonly attributes: S["actorAttributeMap"][A];
58
+ };
59
+ }[S["actorTypes"]];
60
+ /**
61
+ * Represents a protected entity being accessed.
62
+ * Generic over schema S and resource type R.
63
+ * When S = DefaultSchema, collapses to the original untyped shape.
64
+ */
65
+ type ResourceRef<S extends TorideSchema = DefaultSchema, R extends S["resources"] = S["resources"]> = {
66
+ readonly type: R;
67
+ readonly id: string;
68
+ /** Pre-fetched attributes. Inline values take precedence over resolver results. */
69
+ readonly attributes?: S["resourceAttributeMap"][R];
70
+ };
71
+ /** Optional per-check configuration. */
72
+ interface CheckOptions {
73
+ readonly env?: Record<string, unknown>;
74
+ }
75
+ /** A single item in a canBatch() call. Action narrowed to global actions union. */
76
+ interface BatchCheckItem<S extends TorideSchema = DefaultSchema> {
77
+ readonly action: S["actions"];
78
+ readonly resource: ResourceRef<S>;
79
+ }
80
+ /**
81
+ * Per-type resolver function.
82
+ * Called when the engine needs attributes not available inline.
83
+ * Called at most once per unique resource per evaluation (cached).
84
+ *
85
+ * Registering a resolver is **optional** per resource type. When no resolver is
86
+ * registered, inline {@link ResourceRef.attributes} are used as the sole data
87
+ * source — this is referred to as "default resolver" behavior (analogous to
88
+ * GraphQL's default field resolver, which returns `parent[fieldName]`).
89
+ *
90
+ * A resolver is only needed when attributes must be fetched from an external
91
+ * source (e.g., a database). When both inline attributes and a resolver are
92
+ * present, inline attributes take precedence field-by-field over resolver
93
+ * results.
94
+ */
95
+ type ResourceResolver<S extends TorideSchema = DefaultSchema, R extends S["resources"] = S["resources"]> = (ref: ResourceRef<S, R>) => Promise<Record<string, unknown>>;
96
+ /**
97
+ * Map of resource type names to their resolver functions.
98
+ *
99
+ * Not all types need resolvers. Types without a registered resolver use
100
+ * **default resolver** behavior (also called "trivial resolution"): the engine
101
+ * reads attribute values directly from the inline {@link ResourceRef.attributes}
102
+ * passed at the call site. Fields not present inline resolve to `undefined`,
103
+ * causing conditions that reference them to fail (default-deny).
104
+ *
105
+ * This mirrors GraphQL's default field resolver pattern, where an unresolved
106
+ * field simply returns `parent[fieldName]` — here, inline attributes play the
107
+ * role of the `parent` object.
108
+ */
109
+ type Resolvers<S extends TorideSchema = DefaultSchema> = {
110
+ [R in S["resources"]]?: ResourceResolver<S, R>;
111
+ };
112
+ /** Custom evaluator function signature. */
113
+ type EvaluatorFn = (actor: ActorRef, resource: ResourceRef, env: Record<string, unknown>) => Promise<boolean>;
114
+ /** Engine construction options. */
115
+ interface TorideOptions<S extends TorideSchema = DefaultSchema> {
116
+ readonly policy: Policy;
117
+ /**
118
+ * Per-type resolver map.
119
+ *
120
+ * Optional — the engine works without any resolvers when all required data is
121
+ * provided inline via {@link ResourceRef.attributes}. This "default resolver"
122
+ * mode is the simplest way to use toride and requires no async data fetching.
123
+ *
124
+ * When both inline attributes and a resolver are present for the same resource
125
+ * type, **inline attributes take precedence** over resolver results on a
126
+ * field-by-field basis.
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * // Inline-only mode — no resolvers needed
131
+ * const toride = new Toride({ policy });
132
+ *
133
+ * const allowed = await toride.can(actor, "read", {
134
+ * type: "Document",
135
+ * id: "doc-1",
136
+ * attributes: { status: "published", ownerId: "user-42" },
137
+ * });
138
+ * ```
139
+ */
140
+ readonly resolvers?: Resolvers<S>;
141
+ readonly maxConditionDepth?: number;
142
+ readonly maxDerivedRoleDepth?: number;
143
+ readonly customEvaluators?: Record<string, EvaluatorFn>;
144
+ readonly onDecision?: (event: DecisionEvent) => void;
145
+ readonly onQuery?: (event: QueryEvent) => void;
146
+ }
147
+ /** Attribute type for actor declarations. */
148
+ type AttributeType = "string" | "number" | "boolean";
149
+ /** Actor type declaration with attribute schema. */
150
+ interface ActorDeclaration {
151
+ readonly attributes: Record<string, AttributeType>;
152
+ }
153
+ /** Global role definition derived from actor attributes. */
154
+ interface GlobalRole {
155
+ readonly actor_type: string;
156
+ readonly when: ConditionExpression;
157
+ }
158
+ /**
159
+ * Derived role entry. Exactly one derivation pattern per entry.
160
+ * Patterns:
161
+ * 1. from_global_role
162
+ * 2. from_role + on_relation
163
+ * 3. from_relation
164
+ * 4. actor_type + when (conditional)
165
+ * 5. when only
166
+ */
167
+ interface DerivedRoleEntry {
168
+ readonly role: string;
169
+ readonly from_global_role?: string;
170
+ readonly from_role?: string;
171
+ readonly on_relation?: string;
172
+ readonly from_relation?: string;
173
+ readonly actor_type?: string;
174
+ readonly when?: ConditionExpression;
175
+ }
176
+ /** Conditional rule (permit or forbid). */
177
+ interface Rule {
178
+ readonly effect: "permit" | "forbid";
179
+ readonly roles?: string[];
180
+ readonly permissions: string[];
181
+ readonly when: ConditionExpression;
182
+ }
183
+ /** Field-level access control definition. */
184
+ interface FieldAccessDef {
185
+ readonly read?: string[];
186
+ readonly update?: string[];
187
+ }
188
+ /** Resource block definition. */
189
+ interface ResourceBlock {
190
+ readonly roles: string[];
191
+ readonly permissions: string[];
192
+ /** Optional typed attribute declarations for this resource type. */
193
+ readonly attributes?: Record<string, AttributeType>;
194
+ /** Relations map field names to target resource type names (simplified). */
195
+ readonly relations?: Record<string, string>;
196
+ readonly grants?: Record<string, string[]>;
197
+ readonly derived_roles?: DerivedRoleEntry[];
198
+ readonly rules?: Rule[];
199
+ readonly field_access?: Record<string, FieldAccessDef>;
200
+ }
201
+ /** Operator-based condition value. */
202
+ type ConditionOperator = {
203
+ readonly eq: unknown;
204
+ } | {
205
+ readonly neq: unknown;
206
+ } | {
207
+ readonly gt: unknown;
208
+ } | {
209
+ readonly gte: unknown;
210
+ } | {
211
+ readonly lt: unknown;
212
+ } | {
213
+ readonly lte: unknown;
214
+ } | {
215
+ readonly in: unknown[] | string;
216
+ } | {
217
+ readonly includes: unknown;
218
+ } | {
219
+ readonly exists: boolean;
220
+ } | {
221
+ readonly startsWith: string;
222
+ } | {
223
+ readonly endsWith: string;
224
+ } | {
225
+ readonly contains: string;
226
+ } | {
227
+ readonly custom: string;
228
+ };
229
+ /**
230
+ * Condition value: either a primitive (equality shorthand),
231
+ * a cross-reference string ($actor.x, $resource.x, $env.x),
232
+ * or an operator object.
233
+ */
234
+ type ConditionValue = string | number | boolean | ConditionOperator;
235
+ /** Simple conditions: all key-value pairs ANDed together. */
236
+ type SimpleConditions = Record<string, ConditionValue>;
237
+ /**
238
+ * Recursive condition expression.
239
+ * Either simple conditions (Record<string, ConditionValue>),
240
+ * or a logical combinator ({ any: ... } or { all: ... }).
241
+ */
242
+ type ConditionExpression = SimpleConditions | {
243
+ readonly any: ConditionExpression[];
244
+ } | {
245
+ readonly all: ConditionExpression[];
246
+ };
247
+ /** Test case for declarative YAML tests. */
248
+ interface TestCase {
249
+ readonly name: string;
250
+ readonly actor: ActorRef;
251
+ /** Mock resolver data: keyed by "Type:id", values are attribute objects. */
252
+ readonly resolvers?: Record<string, Record<string, unknown>>;
253
+ readonly action: string;
254
+ readonly resource: ResourceRef;
255
+ readonly expected: "allow" | "deny";
256
+ }
257
+ /** Top-level policy object. */
258
+ interface Policy {
259
+ readonly version: "1";
260
+ readonly actors: Record<string, ActorDeclaration>;
261
+ readonly global_roles?: Record<string, GlobalRole>;
262
+ readonly resources: Record<string, ResourceBlock>;
263
+ readonly tests?: TestCase[];
264
+ }
265
+ /** Trace for a derived role showing derivation path. */
266
+ interface DerivedRoleTrace {
267
+ readonly role: string;
268
+ readonly via: string;
269
+ }
270
+ /** Resolved roles detail with direct and derived breakdown. */
271
+ interface ResolvedRolesDetail {
272
+ readonly direct: string[];
273
+ readonly derived: DerivedRoleTrace[];
274
+ }
275
+ /** A matched rule with evaluation context. */
276
+ interface MatchedRule {
277
+ readonly effect: "permit" | "forbid";
278
+ readonly matched: boolean;
279
+ readonly rule: Rule;
280
+ readonly resolvedValues: Record<string, unknown>;
281
+ }
282
+ /** Full decision trace from explain(). */
283
+ interface ExplainResult<S extends TorideSchema = DefaultSchema, R extends S["resources"] = S["resources"]> {
284
+ readonly allowed: boolean;
285
+ readonly resolvedRoles: ResolvedRolesDetail;
286
+ readonly grantedPermissions: S["permissionMap"][R][];
287
+ readonly matchedRules: MatchedRule[];
288
+ readonly finalDecision: string;
289
+ }
290
+ /** Audit event for authorization checks. */
291
+ interface DecisionEvent {
292
+ readonly actor: ActorRef;
293
+ readonly action: string;
294
+ readonly resource: ResourceRef;
295
+ readonly allowed: boolean;
296
+ readonly resolvedRoles: string[];
297
+ readonly matchedRules: {
298
+ effect: string;
299
+ matched: boolean;
300
+ }[];
301
+ readonly timestamp: Date;
302
+ }
303
+ /** Audit event for constraint queries. */
304
+ interface QueryEvent {
305
+ readonly actor: ActorRef;
306
+ readonly action: string;
307
+ readonly resourceType: string;
308
+ readonly resultType: "unrestricted" | "forbidden" | "constrained";
309
+ readonly timestamp: Date;
310
+ }
311
+ /** Thrown when policy validation fails. */
312
+ declare class ValidationError extends Error {
313
+ readonly path: string;
314
+ constructor(message: string, path: string);
315
+ }
316
+ /** Thrown when a cycle is detected in relation traversal. */
317
+ declare class CycleError extends Error {
318
+ readonly path: string[];
319
+ constructor(message: string, path: string[]);
320
+ }
321
+ /** Thrown when depth limit is exceeded. */
322
+ declare class DepthLimitError extends Error {
323
+ readonly limit: number;
324
+ readonly limitType: "condition" | "derivation";
325
+ constructor(message: string, limit: number, limitType: "condition" | "derivation");
326
+ }
327
+
328
+ /**
329
+ * A serializable map of permissions keyed by "Type:id".
330
+ * Values are arrays of permitted action strings.
331
+ * Suitable for JSON transport to client-side TorideClient.
332
+ */
333
+ type PermissionSnapshot = Record<string, string[]>;
334
+
335
+ declare const CLIENT_VERSION = "0.0.1";
336
+
337
+ /** Minimal resource reference for client-side lookups. Generic over S for type narrowing. */
338
+ interface ClientResourceRef<S extends TorideSchema = DefaultSchema> {
339
+ readonly type: S["resources"];
340
+ readonly id: string;
341
+ }
342
+ /**
343
+ * Client-side permission checker that provides instant synchronous checks
344
+ * against a PermissionSnapshot received from the server.
345
+ *
346
+ * Generic over TorideSchema so that action names and resource types
347
+ * are validated at compile time when a concrete schema is provided.
348
+ *
349
+ * Default-deny: unknown resources or actions return false.
350
+ * The snapshot is defensively copied to prevent external mutation.
351
+ */
352
+ declare class TorideClient<S extends TorideSchema = DefaultSchema> {
353
+ private readonly permissions;
354
+ constructor(snapshot: PermissionSnapshot);
355
+ /**
356
+ * Synchronous permission check.
357
+ * Returns true if the action is permitted for the resource, false otherwise.
358
+ * Unknown resources return false (default-deny).
359
+ */
360
+ can(action: S["actions"], resource: ClientResourceRef<S>): boolean;
361
+ /**
362
+ * Return the list of permitted actions for a resource.
363
+ * Returns empty array for unknown resources.
364
+ */
365
+ permittedActions(resource: ClientResourceRef<S>): S["actions"][];
366
+ }
367
+
368
+ export { type ActorRef as A, type BatchCheckItem as B, type CheckOptions as C, type DefaultSchema as D, type ExplainResult as E, type FieldAccessDef as F, type GlobalRole as G, type MatchedRule as M, type Policy as P, type QueryEvent as Q, type ResourceRef as R, type SimpleConditions as S, type TorideSchema as T, ValidationError as V, type TorideOptions as a, type PermissionSnapshot as b, type TestCase as c, type Resolvers as d, type ActorDeclaration as e, type AttributeType as f, type ClientResourceRef as g, type ConditionExpression as h, type ConditionOperator as i, type ConditionValue as j, CycleError as k, type DecisionEvent as l, DepthLimitError as m, type DerivedRoleEntry as n, type DerivedRoleTrace as o, type EvaluatorFn as p, type ResolvedRolesDetail as q, type ResourceBlock as r, type ResourceResolver as s, type Rule as t, TorideClient as u, CLIENT_VERSION as v };
package/dist/client.d.ts CHANGED
@@ -1,33 +1 @@
1
- import { P as PermissionSnapshot } from './snapshot-CcCYeU0Q.js';
2
-
3
- declare const CLIENT_VERSION = "0.0.1";
4
-
5
- /** Minimal resource reference for client-side lookups. */
6
- interface ClientResourceRef {
7
- readonly type: string;
8
- readonly id: string;
9
- }
10
- /**
11
- * Client-side permission checker that provides instant synchronous checks
12
- * against a PermissionSnapshot received from the server.
13
- *
14
- * Default-deny: unknown resources or actions return false.
15
- * The snapshot is defensively copied to prevent external mutation.
16
- */
17
- declare class TorideClient {
18
- private readonly permissions;
19
- constructor(snapshot: PermissionSnapshot);
20
- /**
21
- * Synchronous permission check.
22
- * Returns true if the action is permitted for the resource, false otherwise.
23
- * Unknown resources return false (default-deny).
24
- */
25
- can(action: string, resource: ClientResourceRef): boolean;
26
- /**
27
- * Return the list of permitted actions for a resource.
28
- * Returns empty array for unknown resources.
29
- */
30
- permittedActions(resource: ClientResourceRef): string[];
31
- }
32
-
33
- export { CLIENT_VERSION, type ClientResourceRef, PermissionSnapshot, TorideClient };
1
+ export { v as CLIENT_VERSION, g as ClientResourceRef, b as PermissionSnapshot, u as TorideClient } from './client-RqwW0K_-.js';
package/dist/client.js CHANGED
@@ -1,35 +1,7 @@
1
- // src/client.ts
2
- var CLIENT_VERSION = "0.0.1";
3
- var TorideClient = class {
4
- permissions;
5
- constructor(snapshot) {
6
- this.permissions = /* @__PURE__ */ new Map();
7
- for (const [key, actions] of Object.entries(snapshot)) {
8
- this.permissions.set(key, new Set(actions));
9
- }
10
- }
11
- /**
12
- * Synchronous permission check.
13
- * Returns true if the action is permitted for the resource, false otherwise.
14
- * Unknown resources return false (default-deny).
15
- */
16
- can(action, resource) {
17
- const key = `${resource.type}:${resource.id}`;
18
- const actions = this.permissions.get(key);
19
- if (!actions) return false;
20
- return actions.has(action);
21
- }
22
- /**
23
- * Return the list of permitted actions for a resource.
24
- * Returns empty array for unknown resources.
25
- */
26
- permittedActions(resource) {
27
- const key = `${resource.type}:${resource.id}`;
28
- const actions = this.permissions.get(key);
29
- if (!actions) return [];
30
- return [...actions];
31
- }
32
- };
1
+ import {
2
+ CLIENT_VERSION,
3
+ TorideClient
4
+ } from "./chunk-475CNU63.js";
33
5
  export {
34
6
  CLIENT_VERSION,
35
7
  TorideClient
package/dist/index.d.ts CHANGED
@@ -1,230 +1,5 @@
1
- import { P as PermissionSnapshot } from './snapshot-CcCYeU0Q.js';
2
-
3
- /** Represents an entity performing actions. */
4
- interface ActorRef {
5
- readonly type: string;
6
- readonly id: string;
7
- readonly attributes: Record<string, unknown>;
8
- }
9
- /** Represents a protected entity being accessed. */
10
- interface ResourceRef {
11
- readonly type: string;
12
- readonly id: string;
13
- /** Pre-fetched attributes. Inline values take precedence over resolver results. */
14
- readonly attributes?: Record<string, unknown>;
15
- }
16
- /** Optional per-check configuration. */
17
- interface CheckOptions {
18
- readonly env?: Record<string, unknown>;
19
- }
20
- /** A single item in a canBatch() call. */
21
- interface BatchCheckItem {
22
- readonly action: string;
23
- readonly resource: ResourceRef;
24
- }
25
- /**
26
- * Per-type resolver function.
27
- * Called when the engine needs attributes not available inline.
28
- * Called at most once per unique resource per evaluation (cached).
29
- */
30
- type ResourceResolver = (ref: ResourceRef) => Promise<Record<string, unknown>>;
31
- /**
32
- * Map of resource type names to their resolver functions.
33
- * Not all types need resolvers — types without resolvers use trivial resolution
34
- * (fields are undefined unless provided inline).
35
- */
36
- type Resolvers = Record<string, ResourceResolver>;
37
- /** Custom evaluator function signature. */
38
- type EvaluatorFn = (actor: ActorRef, resource: ResourceRef, env: Record<string, unknown>) => Promise<boolean>;
39
- /** Engine construction options. */
40
- interface TorideOptions {
41
- readonly policy: Policy;
42
- /** Per-type resolver map. Optional — engine works without resolvers if all data is inline. */
43
- readonly resolvers?: Resolvers;
44
- readonly maxConditionDepth?: number;
45
- readonly maxDerivedRoleDepth?: number;
46
- readonly customEvaluators?: Record<string, EvaluatorFn>;
47
- readonly onDecision?: (event: DecisionEvent) => void;
48
- readonly onQuery?: (event: QueryEvent) => void;
49
- }
50
- /** Attribute type for actor declarations. */
51
- type AttributeType = "string" | "number" | "boolean";
52
- /** Actor type declaration with attribute schema. */
53
- interface ActorDeclaration {
54
- readonly attributes: Record<string, AttributeType>;
55
- }
56
- /** Global role definition derived from actor attributes. */
57
- interface GlobalRole {
58
- readonly actor_type: string;
59
- readonly when: ConditionExpression;
60
- }
61
- /**
62
- * Derived role entry. Exactly one derivation pattern per entry.
63
- * Patterns:
64
- * 1. from_global_role
65
- * 2. from_role + on_relation
66
- * 3. from_relation
67
- * 4. actor_type + when (conditional)
68
- * 5. when only
69
- */
70
- interface DerivedRoleEntry {
71
- readonly role: string;
72
- readonly from_global_role?: string;
73
- readonly from_role?: string;
74
- readonly on_relation?: string;
75
- readonly from_relation?: string;
76
- readonly actor_type?: string;
77
- readonly when?: ConditionExpression;
78
- }
79
- /** Conditional rule (permit or forbid). */
80
- interface Rule {
81
- readonly effect: "permit" | "forbid";
82
- readonly roles?: string[];
83
- readonly permissions: string[];
84
- readonly when: ConditionExpression;
85
- }
86
- /** Field-level access control definition. */
87
- interface FieldAccessDef {
88
- readonly read?: string[];
89
- readonly update?: string[];
90
- }
91
- /** Resource block definition. */
92
- interface ResourceBlock {
93
- readonly roles: string[];
94
- readonly permissions: string[];
95
- /** Relations map field names to target resource type names (simplified). */
96
- readonly relations?: Record<string, string>;
97
- readonly grants?: Record<string, string[]>;
98
- readonly derived_roles?: DerivedRoleEntry[];
99
- readonly rules?: Rule[];
100
- readonly field_access?: Record<string, FieldAccessDef>;
101
- }
102
- /** Operator-based condition value. */
103
- type ConditionOperator = {
104
- readonly eq: unknown;
105
- } | {
106
- readonly neq: unknown;
107
- } | {
108
- readonly gt: unknown;
109
- } | {
110
- readonly gte: unknown;
111
- } | {
112
- readonly lt: unknown;
113
- } | {
114
- readonly lte: unknown;
115
- } | {
116
- readonly in: unknown[] | string;
117
- } | {
118
- readonly includes: unknown;
119
- } | {
120
- readonly exists: boolean;
121
- } | {
122
- readonly startsWith: string;
123
- } | {
124
- readonly endsWith: string;
125
- } | {
126
- readonly contains: string;
127
- } | {
128
- readonly custom: string;
129
- };
130
- /**
131
- * Condition value: either a primitive (equality shorthand),
132
- * a cross-reference string ($actor.x, $resource.x, $env.x),
133
- * or an operator object.
134
- */
135
- type ConditionValue = string | number | boolean | ConditionOperator;
136
- /** Simple conditions: all key-value pairs ANDed together. */
137
- type SimpleConditions = Record<string, ConditionValue>;
138
- /**
139
- * Recursive condition expression.
140
- * Either simple conditions (Record<string, ConditionValue>),
141
- * or a logical combinator ({ any: ... } or { all: ... }).
142
- */
143
- type ConditionExpression = SimpleConditions | {
144
- readonly any: ConditionExpression[];
145
- } | {
146
- readonly all: ConditionExpression[];
147
- };
148
- /** Test case for declarative YAML tests. */
149
- interface TestCase {
150
- readonly name: string;
151
- readonly actor: ActorRef;
152
- /** Mock resolver data: keyed by "Type:id", values are attribute objects. */
153
- readonly resolvers?: Record<string, Record<string, unknown>>;
154
- readonly action: string;
155
- readonly resource: ResourceRef;
156
- readonly expected: "allow" | "deny";
157
- }
158
- /** Top-level policy object. */
159
- interface Policy {
160
- readonly version: "1";
161
- readonly actors: Record<string, ActorDeclaration>;
162
- readonly global_roles?: Record<string, GlobalRole>;
163
- readonly resources: Record<string, ResourceBlock>;
164
- readonly tests?: TestCase[];
165
- }
166
- /** Trace for a derived role showing derivation path. */
167
- interface DerivedRoleTrace {
168
- readonly role: string;
169
- readonly via: string;
170
- }
171
- /** Resolved roles detail with direct and derived breakdown. */
172
- interface ResolvedRolesDetail {
173
- readonly direct: string[];
174
- readonly derived: DerivedRoleTrace[];
175
- }
176
- /** A matched rule with evaluation context. */
177
- interface MatchedRule {
178
- readonly effect: "permit" | "forbid";
179
- readonly matched: boolean;
180
- readonly rule: Rule;
181
- readonly resolvedValues: Record<string, unknown>;
182
- }
183
- /** Full decision trace from explain(). */
184
- interface ExplainResult {
185
- readonly allowed: boolean;
186
- readonly resolvedRoles: ResolvedRolesDetail;
187
- readonly grantedPermissions: string[];
188
- readonly matchedRules: MatchedRule[];
189
- readonly finalDecision: string;
190
- }
191
- /** Audit event for authorization checks. */
192
- interface DecisionEvent {
193
- readonly actor: ActorRef;
194
- readonly action: string;
195
- readonly resource: ResourceRef;
196
- readonly allowed: boolean;
197
- readonly resolvedRoles: string[];
198
- readonly matchedRules: {
199
- effect: string;
200
- matched: boolean;
201
- }[];
202
- readonly timestamp: Date;
203
- }
204
- /** Audit event for constraint queries. */
205
- interface QueryEvent {
206
- readonly actor: ActorRef;
207
- readonly action: string;
208
- readonly resourceType: string;
209
- readonly resultType: "unrestricted" | "forbidden" | "constrained";
210
- readonly timestamp: Date;
211
- }
212
- /** Thrown when policy validation fails. */
213
- declare class ValidationError extends Error {
214
- readonly path: string;
215
- constructor(message: string, path: string);
216
- }
217
- /** Thrown when a cycle is detected in relation traversal. */
218
- declare class CycleError extends Error {
219
- readonly path: string[];
220
- constructor(message: string, path: string[]);
221
- }
222
- /** Thrown when depth limit is exceeded. */
223
- declare class DepthLimitError extends Error {
224
- readonly limit: number;
225
- readonly limitType: "condition" | "derivation";
226
- constructor(message: string, limit: number, limitType: "condition" | "derivation");
227
- }
1
+ import { P as Policy, T as TorideSchema, D as DefaultSchema, a as TorideOptions, A as ActorRef, R as ResourceRef, C as CheckOptions, E as ExplainResult, b as PermissionSnapshot, B as BatchCheckItem, c as TestCase, d as Resolvers } from './client-RqwW0K_-.js';
2
+ export { e as ActorDeclaration, f as AttributeType, g as ClientResourceRef, h as ConditionExpression, i as ConditionOperator, j as ConditionValue, k as CycleError, l as DecisionEvent, m as DepthLimitError, n as DerivedRoleEntry, o as DerivedRoleTrace, p as EvaluatorFn, F as FieldAccessDef, G as GlobalRole, M as MatchedRule, Q as QueryEvent, q as ResolvedRolesDetail, r as ResourceBlock, s as ResourceResolver, t as Rule, S as SimpleConditions, u as TorideClient, V as ValidationError } from './client-RqwW0K_-.js';
228
3
 
229
4
  /**
230
5
  * Parse and validate a YAML string into a typed Policy object.
@@ -398,16 +173,16 @@ interface ConstraintAdapter<TQuery> {
398
173
  * Creates a per-check cache, resolves direct roles, checks grants,
399
174
  * and returns boolean with default-deny semantics.
400
175
  */
401
- declare class Toride {
176
+ declare class Toride<S extends TorideSchema = DefaultSchema> {
402
177
  private policy;
403
178
  private readonly resolvers;
404
179
  private readonly options;
405
- constructor(options: TorideOptions);
180
+ constructor(options: TorideOptions<S>);
406
181
  /**
407
182
  * Check if an actor can perform an action on a resource.
408
183
  * Default-deny: returns false if resource type is unknown or no grants match.
409
184
  */
410
- can(actor: ActorRef, action: string, resource: ResourceRef, options?: CheckOptions): Promise<boolean>;
185
+ can<R extends S["resources"]>(actor: ActorRef<S>, action: S["permissionMap"][R], resource: ResourceRef<S, R>, options?: CheckOptions): Promise<boolean>;
411
186
  /**
412
187
  * T097: Atomic policy swap. In-flight checks capture the resource block
413
188
  * at the start of evaluateInternal, so they complete with the old policy.
@@ -418,44 +193,44 @@ declare class Toride {
418
193
  * T068: Return full ExplainResult with role derivation traces,
419
194
  * granted permissions, matched rules, and human-readable final decision.
420
195
  */
421
- explain(actor: ActorRef, action: string, resource: ResourceRef, options?: CheckOptions): Promise<ExplainResult>;
196
+ explain<R extends S["resources"]>(actor: ActorRef<S>, action: S["permissionMap"][R], resource: ResourceRef<S, R>, options?: CheckOptions): Promise<ExplainResult<S, R>>;
422
197
  /**
423
198
  * T069: Check all declared permissions for a resource and return permitted ones.
424
199
  * Uses a shared cache across all per-action evaluations.
425
200
  */
426
- permittedActions(actor: ActorRef, resource: ResourceRef, options?: CheckOptions): Promise<string[]>;
201
+ permittedActions<R extends S["resources"]>(actor: ActorRef<S>, resource: ResourceRef<S, R>, options?: CheckOptions): Promise<S["permissionMap"][R][]>;
427
202
  /**
428
203
  * T083: Generate a PermissionSnapshot for a list of resources.
429
204
  * Calls permittedActions() for each resource and returns a map
430
205
  * keyed by "Type:id" with arrays of permitted action strings.
431
206
  * Suitable for serializing to the client via TorideClient.
432
207
  */
433
- snapshot(actor: ActorRef, resources: ResourceRef[], options?: CheckOptions): Promise<PermissionSnapshot>;
208
+ snapshot(actor: ActorRef<S>, resources: ResourceRef<S>[], options?: CheckOptions): Promise<PermissionSnapshot>;
434
209
  /**
435
210
  * T095: Check if an actor can perform a field-level operation on a specific field.
436
211
  * Restricted fields require the actor to have a role listed in field_access.
437
212
  * Unlisted fields are unrestricted: any actor with the resource-level permission can access them.
438
213
  */
439
- canField(actor: ActorRef, operation: "read" | "update", resource: ResourceRef, field: string, options?: CheckOptions): Promise<boolean>;
214
+ canField<R extends S["resources"]>(actor: ActorRef<S>, operation: "read" | "update", resource: ResourceRef<S, R>, field: string, options?: CheckOptions): Promise<boolean>;
440
215
  /**
441
216
  * T095: Return the list of declared field_access field names the actor can access
442
217
  * for the given operation. Only returns explicitly declared fields.
443
218
  */
444
- permittedFields(actor: ActorRef, operation: "read" | "update", resource: ResourceRef, options?: CheckOptions): Promise<string[]>;
219
+ permittedFields<R extends S["resources"]>(actor: ActorRef<S>, operation: "read" | "update", resource: ResourceRef<S, R>, options?: CheckOptions): Promise<string[]>;
445
220
  /**
446
221
  * T070: Return flat deduplicated list of all resolved roles (direct + derived).
447
222
  */
448
- resolvedRoles(actor: ActorRef, resource: ResourceRef, options?: CheckOptions): Promise<string[]>;
223
+ resolvedRoles<R extends S["resources"]>(actor: ActorRef<S>, resource: ResourceRef<S, R>, options?: CheckOptions): Promise<string[]>;
449
224
  /**
450
225
  * T071: Evaluate multiple checks for the same actor with a shared resolver cache.
451
226
  * Returns boolean[] in the same order as the input checks.
452
227
  */
453
- canBatch(actor: ActorRef, checks: BatchCheckItem[], options?: CheckOptions): Promise<boolean[]>;
228
+ canBatch(actor: ActorRef<S>, checks: BatchCheckItem<S>[], options?: CheckOptions): Promise<boolean[]>;
454
229
  /**
455
230
  * T064: Build constraint AST for partial evaluation / data filtering.
456
231
  * Returns ConstraintResult with unrestricted/forbidden sentinels or constraint AST.
457
232
  */
458
- buildConstraints(actor: ActorRef, action: string, resourceType: string, options?: CheckOptions): Promise<ConstraintResult>;
233
+ buildConstraints<R extends S["resources"]>(actor: ActorRef<S>, action: S["permissionMap"][R], resourceType: R, options?: CheckOptions): Promise<ConstraintResult>;
459
234
  /**
460
235
  * T064: Translate constraint AST using an adapter.
461
236
  * Dispatches each constraint node to the adapter's methods.
@@ -480,7 +255,7 @@ declare class Toride {
480
255
  /**
481
256
  * Typed factory function for creating a Toride instance.
482
257
  */
483
- declare function createToride(options: TorideOptions): Toride;
258
+ declare function createToride<S extends TorideSchema = DefaultSchema>(options: TorideOptions<S>): Toride<S>;
484
259
 
485
260
  /**
486
261
  * Constructs a Resolvers map from test case mock data.
@@ -531,4 +306,4 @@ declare function runTestCases(policy: Policy, tests: TestCase[]): Promise<TestRe
531
306
 
532
307
  declare const VERSION = "0.0.1";
533
308
 
534
- export { type ActorDeclaration, type ActorRef, type AttributeType, type BatchCheckItem, type CheckOptions, type ConditionExpression, type ConditionOperator, type ConditionValue, type Constraint, type ConstraintAdapter, type ConstraintResult, CycleError, type DecisionEvent, DepthLimitError, type DerivedRoleEntry, type DerivedRoleTrace, type EvaluatorFn, type ExplainResult, type FieldAccessDef, type GlobalRole, type LeafConstraint, type MatchedRule, PermissionSnapshot, type Policy, type QueryEvent, type ResolvedRolesDetail, type Resolvers, type ResourceBlock, type ResourceRef, type ResourceResolver, type Rule, type SimpleConditions, type StrictValidationResult, type TestCase, type TestFileResult, type TestResult, Toride, type TorideOptions, VERSION, type ValidationDiagnostic, ValidationError, type ValidationResult, createMockResolver, createToride, loadJson, loadYaml, mergePolicies, parseInlineTests, parseTestFile, runTestCases, validatePolicy, validatePolicyResult, validatePolicyStrict };
309
+ export { ActorRef, BatchCheckItem, CheckOptions, type Constraint, type ConstraintAdapter, type ConstraintResult, DefaultSchema, ExplainResult, type LeafConstraint, PermissionSnapshot, Policy, Resolvers, ResourceRef, type StrictValidationResult, TestCase, type TestFileResult, type TestResult, Toride, TorideOptions, TorideSchema, VERSION, type ValidationDiagnostic, type ValidationResult, createMockResolver, createToride, loadJson, loadYaml, mergePolicies, parseInlineTests, parseTestFile, runTestCases, validatePolicy, validatePolicyResult, validatePolicyStrict };
package/dist/index.js CHANGED
@@ -12,7 +12,10 @@ import {
12
12
  validatePolicy,
13
13
  validatePolicyResult,
14
14
  validatePolicyStrict
15
- } from "./chunk-QE4FJZEG.js";
15
+ } from "./chunk-24PMDTLE.js";
16
+ import {
17
+ TorideClient
18
+ } from "./chunk-475CNU63.js";
16
19
 
17
20
  // src/policy/parser.ts
18
21
  import * as YAML from "yaml";
@@ -226,6 +229,7 @@ export {
226
229
  CycleError,
227
230
  DepthLimitError,
228
231
  Toride,
232
+ TorideClient,
229
233
  VERSION,
230
234
  ValidationError,
231
235
  createMockResolver,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toride",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Relation-aware authorization engine for TypeScript",
5
5
  "type": "module",
6
6
  "exports": {
@@ -27,8 +27,8 @@
27
27
  ]
28
28
  },
29
29
  "dependencies": {
30
- "yaml": "^2.3.0",
31
- "valibot": "^1.2.0"
30
+ "valibot": "^1.2.0",
31
+ "yaml": "^2.3.0"
32
32
  },
33
33
  "keywords": [
34
34
  "authorization",
@@ -42,9 +42,13 @@
42
42
  "type": "git",
43
43
  "url": "https://github.com/toride-auth/toride"
44
44
  },
45
+ "devDependencies": {
46
+ "tsd": "^0.33.0"
47
+ },
45
48
  "scripts": {
46
49
  "build": "tsup",
47
50
  "test": "vitest run",
51
+ "typetest": "tsd --files 'src/__typetests__/*.test-d.ts'",
48
52
  "lint": "tsc --noEmit",
49
53
  "bench": "vitest bench"
50
54
  }
@@ -1,8 +0,0 @@
1
- /**
2
- * A serializable map of permissions keyed by "Type:id".
3
- * Values are arrays of permitted action strings.
4
- * Suitable for JSON transport to client-side TorideClient.
5
- */
6
- type PermissionSnapshot = Record<string, string[]>;
7
-
8
- export type { PermissionSnapshot as P };