@typed-policy/core 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.
- package/LICENSE +21 -0
- package/dist/index.d.ts +281 -0
- package/dist/index.js +27 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Togle Labs <m.ihsan.vp@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
type Primitive = string | number | boolean | null | undefined;
|
|
2
|
+
type Path<T> = T extends Primitive ? never : {
|
|
3
|
+
[K in keyof T & string]: T[K] extends Primitive ? K : K | `${K}.${Path<T[K]>}`;
|
|
4
|
+
}[keyof T & string];
|
|
5
|
+
|
|
6
|
+
type PathValue<T, P extends Path<T>> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? PathValue<T[K], Extract<Rest, Path<T[K]>>> : never : P extends keyof T ? T[P] : never;
|
|
7
|
+
type PolicyContext = {
|
|
8
|
+
user: {
|
|
9
|
+
id: string;
|
|
10
|
+
role: "admin" | "user";
|
|
11
|
+
};
|
|
12
|
+
post: {
|
|
13
|
+
id: string;
|
|
14
|
+
ownerId: string;
|
|
15
|
+
published: boolean;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Expression AST type
|
|
21
|
+
*
|
|
22
|
+
* T = Subject context type (paths reference this)
|
|
23
|
+
* A = Actor context type (available to functions)
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* type MyExpr = Expr<{ post: { published: boolean } }, { user: { role: string } }>;
|
|
27
|
+
*/
|
|
28
|
+
type Expr<T, A = unknown> = {
|
|
29
|
+
kind: "eq";
|
|
30
|
+
left: Path<T>;
|
|
31
|
+
right: Path<T> | PathValue<T, Path<T>>;
|
|
32
|
+
} | {
|
|
33
|
+
kind: "and";
|
|
34
|
+
rules: Expr<T, A>[];
|
|
35
|
+
} | {
|
|
36
|
+
kind: "or";
|
|
37
|
+
rules: Expr<T, A>[];
|
|
38
|
+
} | {
|
|
39
|
+
kind: "literal";
|
|
40
|
+
value: boolean;
|
|
41
|
+
} | {
|
|
42
|
+
kind: "function";
|
|
43
|
+
fn: (ctx: {
|
|
44
|
+
actor: A;
|
|
45
|
+
}) => boolean | Expr<T, A>;
|
|
46
|
+
description?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Context types for Typed Policy v0.2
|
|
51
|
+
*
|
|
52
|
+
* Separates actor (user/requester) from subject (resource)
|
|
53
|
+
* for better authorization modeling.
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* Actor context - represents the user or entity making the request
|
|
57
|
+
* Developers define their own actor shape based on their application
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* type MyActor = {
|
|
61
|
+
* user: {
|
|
62
|
+
* id: string;
|
|
63
|
+
* role: "admin" | "user" | "moderator";
|
|
64
|
+
* organizationId: string;
|
|
65
|
+
* };
|
|
66
|
+
* };
|
|
67
|
+
*/
|
|
68
|
+
type ActorContext = {
|
|
69
|
+
user: {
|
|
70
|
+
id: string;
|
|
71
|
+
role: "admin" | "user";
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Subject context - represents the resource being accessed
|
|
76
|
+
* Developers define their own subject shape based on their data model
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* type MySubject = {
|
|
80
|
+
* post: {
|
|
81
|
+
* id: string;
|
|
82
|
+
* ownerId: string;
|
|
83
|
+
* published: boolean;
|
|
84
|
+
* organizationId: string;
|
|
85
|
+
* };
|
|
86
|
+
* };
|
|
87
|
+
*/
|
|
88
|
+
type SubjectContext = {
|
|
89
|
+
post: {
|
|
90
|
+
id: string;
|
|
91
|
+
ownerId: string;
|
|
92
|
+
published: boolean;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Full context combining actor and subject
|
|
97
|
+
* Used internally but developers typically use separate Actor/Subject types
|
|
98
|
+
*/
|
|
99
|
+
type FullContext<A, S> = A & S;
|
|
100
|
+
/**
|
|
101
|
+
* Evaluation context passed to policy functions
|
|
102
|
+
* Functions ONLY receive actor - subject is accessed through DSL (eq, and, or)
|
|
103
|
+
* This ensures consistency between frontend and backend evaluation
|
|
104
|
+
*/
|
|
105
|
+
type EvalContext<A> = {
|
|
106
|
+
/** The actor (user) making the request */
|
|
107
|
+
actor: A;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Resource mapping for nested resource structure in evaluate
|
|
111
|
+
* Maps resource types to their property types
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* type PostMapping = ResourceMapping<{
|
|
115
|
+
* post: { id: string; ownerId: string; published: boolean };
|
|
116
|
+
* }>;
|
|
117
|
+
* // Results in: { post: { id: string; ownerId: string; published: boolean } }
|
|
118
|
+
*/
|
|
119
|
+
type ResourceMapping<T> = {
|
|
120
|
+
[K in keyof T]: {
|
|
121
|
+
[P in keyof T[K]]: T[K][P];
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Equality comparison operator
|
|
127
|
+
* Compares a subject path to a value or another path
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* eq("post.published", true)
|
|
131
|
+
* eq("post.ownerId", "user.id")
|
|
132
|
+
*/
|
|
133
|
+
declare function eq<T, L extends Path<T>, A = unknown>(left: L, right: Path<T> | PathValue<T, L>): Expr<T, A>;
|
|
134
|
+
/**
|
|
135
|
+
* Logical AND operator
|
|
136
|
+
* All rules must be true
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* and(eq("post.published", true), eq("user.role", "admin"))
|
|
140
|
+
*/
|
|
141
|
+
declare function and<T, A = unknown>(...rules: Expr<T, A>[]): Expr<T, A>;
|
|
142
|
+
/**
|
|
143
|
+
* Logical OR operator
|
|
144
|
+
* At least one rule must be true
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* or(eq("user.role", "admin"), eq("post.published", true))
|
|
148
|
+
*/
|
|
149
|
+
declare function or<T, A = unknown>(...rules: Expr<T, A>[]): Expr<T, A>;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Policy configuration with separate actor and subject types
|
|
153
|
+
*
|
|
154
|
+
* A = Actor context type (the user/requester)
|
|
155
|
+
* S = Subject context type (the resource being accessed)
|
|
156
|
+
*
|
|
157
|
+
* Functions ONLY receive actor - subject is accessed through DSL:
|
|
158
|
+
* @example
|
|
159
|
+
* const policy = policy<MyActor, MySubject>({
|
|
160
|
+
* subject: "Post",
|
|
161
|
+
* actions: {
|
|
162
|
+
* // Function: only actor available
|
|
163
|
+
* read: ({ actor }) => {
|
|
164
|
+
* if (actor.user.role === "admin") return true;
|
|
165
|
+
* return eq("post.published", true); // Subject via DSL
|
|
166
|
+
* },
|
|
167
|
+
* // Boolean literal
|
|
168
|
+
* create: true,
|
|
169
|
+
* // Declarative DSL
|
|
170
|
+
* delete: eq("post.ownerId", "user.id")
|
|
171
|
+
* }
|
|
172
|
+
* });
|
|
173
|
+
*/
|
|
174
|
+
interface PolicyConfig<A, S> {
|
|
175
|
+
subject: string;
|
|
176
|
+
actions: {
|
|
177
|
+
[K: string]: Expr<S, A> | boolean | ((ctx: EvalContext<A>) => boolean | Expr<S, A>);
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Define a policy with type-safe actor and subject contexts
|
|
182
|
+
*
|
|
183
|
+
* Actions can be:
|
|
184
|
+
* - Functions: ({ actor }) => boolean | Expr (actor only!)
|
|
185
|
+
* - Declarative: eq("post.published", true) (subject via DSL)
|
|
186
|
+
* - Literals: true, false
|
|
187
|
+
*/
|
|
188
|
+
declare function policy<A, S>(config: PolicyConfig<A, S>): PolicyConfig<A, S>;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Converts a union type to an intersection type
|
|
192
|
+
* Uses distributive conditional types trick
|
|
193
|
+
*/
|
|
194
|
+
type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
|
|
195
|
+
/**
|
|
196
|
+
* Extracts all subject paths used in an expression
|
|
197
|
+
* This traverses the expression AST and collects all paths that reference the subject (T)
|
|
198
|
+
*/
|
|
199
|
+
type ExprPaths<T, A, E extends Expr<T, A>> = E extends {
|
|
200
|
+
kind: "eq";
|
|
201
|
+
left: infer L extends Path<T>;
|
|
202
|
+
right: infer R;
|
|
203
|
+
} ? R extends Path<T> ? L | R : L : E extends {
|
|
204
|
+
kind: "and" | "or";
|
|
205
|
+
rules: infer Rules extends readonly Expr<T, A>[];
|
|
206
|
+
} ? Rules extends readonly [infer First, ...infer Rest] ? First extends Expr<T, A> ? Rest extends readonly Expr<T, A>[] ? ExprPaths<T, A, First> | ExprPathsMany<T, A, Rest> : ExprPaths<T, A, First> : never : never : never;
|
|
207
|
+
/**
|
|
208
|
+
* Helper type to extract paths from multiple expressions
|
|
209
|
+
*/
|
|
210
|
+
type ExprPathsMany<T, A, Rules extends readonly Expr<T, A>[]> = Rules extends readonly [
|
|
211
|
+
infer First,
|
|
212
|
+
...infer Rest
|
|
213
|
+
] ? First extends Expr<T, A> ? Rest extends readonly Expr<T, A>[] ? ExprPaths<T, A, First> | ExprPathsMany<T, A, Rest> : ExprPaths<T, A, First> : never : never;
|
|
214
|
+
/**
|
|
215
|
+
* DeepPick - picks nested properties from an object type using dot-notation paths
|
|
216
|
+
* Similar to a deep version of TypeScript's built-in Pick
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* type Obj = { post: { published: boolean; title: string } };
|
|
220
|
+
* type Picked = DeepPick<Obj, "post.published">; // { post: { published: boolean } }
|
|
221
|
+
*/
|
|
222
|
+
type DeepPick<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? {
|
|
223
|
+
[Key in K]: DeepPick<T[K], Rest>;
|
|
224
|
+
} : never : P extends keyof T ? {
|
|
225
|
+
[Key in P]: T[Key];
|
|
226
|
+
} : never;
|
|
227
|
+
/**
|
|
228
|
+
* Builds minimal context containing only the paths referenced in an expression
|
|
229
|
+
* This allows creating a subset of the full context with only what's needed
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* type Full = { post: { published: boolean; title: string } };
|
|
233
|
+
* type Minimal = MinimalContext<Full, "post.published">;
|
|
234
|
+
* // Result: { post: { published: boolean } }
|
|
235
|
+
*/
|
|
236
|
+
type MinimalContext<T, P extends Path<T>> = UnionToIntersection<DeepPick<T, P>>;
|
|
237
|
+
/**
|
|
238
|
+
* Infers the minimal subject context type required for an expression
|
|
239
|
+
* Only includes the paths that are actually referenced in the expression
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* type MySubject = { post: { published: boolean; title: string } };
|
|
243
|
+
* type MyActor = { user: { id: string } };
|
|
244
|
+
* type MyExpr = Expr<MySubject, MyActor>; // eq("post.published", true)
|
|
245
|
+
* type Subject = InferSubjectContext<MySubject, MyActor, MyExpr>;
|
|
246
|
+
* // Result: { post: { published: boolean } }
|
|
247
|
+
*/
|
|
248
|
+
type InferSubjectContext<T, A, E extends Expr<T, A>> = ExprPaths<T, A, E> extends Path<T> ? UnionToIntersection<DeepPick<T, ExprPaths<T, A, E>>> : T;
|
|
249
|
+
/**
|
|
250
|
+
* Infers the actor context type from an expression
|
|
251
|
+
* The actor context is passed to function expressions and contains user/requester data
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* type MyActor = { user: { id: string; role: string } };
|
|
255
|
+
* type MySubject = { post: { published: boolean } };
|
|
256
|
+
* type MyExpr = Expr<MySubject, MyActor>;
|
|
257
|
+
* type Actor = InferActorContext<MySubject, MyActor, MyExpr>;
|
|
258
|
+
* // Result: { user: { id: string; role: string } }
|
|
259
|
+
*/
|
|
260
|
+
type InferActorContext<T, A, _E extends Expr<T, A>> = A;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Error message utilities for Typed Policy
|
|
264
|
+
*
|
|
265
|
+
* Provides consistent error messages for common failure scenarios.
|
|
266
|
+
*/
|
|
267
|
+
/**
|
|
268
|
+
* Creates a standardized error message for missing context paths
|
|
269
|
+
*
|
|
270
|
+
* @param missingPath - The path that was not found in the context
|
|
271
|
+
* @returns A formatted error message string
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* ```ts
|
|
275
|
+
* throw new Error(createContextError("user.role"));
|
|
276
|
+
* // throws: Missing required context path: "user.role"
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
declare function createContextError(missingPath: string): string;
|
|
280
|
+
|
|
281
|
+
export { type ActorContext, type DeepPick, type EvalContext, type Expr, type ExprPaths, type FullContext, type InferActorContext, type InferSubjectContext, type MinimalContext, type Path, type PathValue, type PolicyConfig, type PolicyContext, type Primitive, type ResourceMapping, type SubjectContext, type UnionToIntersection, and, createContextError, eq, or, policy };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/operators.ts
|
|
2
|
+
function eq(left, right) {
|
|
3
|
+
return { kind: "eq", left, right };
|
|
4
|
+
}
|
|
5
|
+
function and(...rules) {
|
|
6
|
+
return { kind: "and", rules };
|
|
7
|
+
}
|
|
8
|
+
function or(...rules) {
|
|
9
|
+
return { kind: "or", rules };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/policy.ts
|
|
13
|
+
function policy(config) {
|
|
14
|
+
return config;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/errors.ts
|
|
18
|
+
function createContextError(missingPath) {
|
|
19
|
+
return `Missing required context path: "${missingPath}"`;
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
and,
|
|
23
|
+
createContextError,
|
|
24
|
+
eq,
|
|
25
|
+
or,
|
|
26
|
+
policy
|
|
27
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@typed-policy/core",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Core types and DSL for typed policies",
|
|
5
|
+
"author": "Ihsan VP <m.ihsan.vp@gmail.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/toglelabs/typed-policy.git",
|
|
10
|
+
"directory": "packages/core"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/toglelabs/typed-policy/tree/main/packages/core#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/toglelabs/typed-policy/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"typescript",
|
|
18
|
+
"policy",
|
|
19
|
+
"authorization",
|
|
20
|
+
"type-safe",
|
|
21
|
+
"dsl",
|
|
22
|
+
"ast"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"import": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@biomejs/biome": "^1.9.4",
|
|
36
|
+
"typescript": "^5.7.0",
|
|
37
|
+
"tsup": "^8.3.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {},
|
|
40
|
+
"dependencies": {},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"clean": "rm -rf dist",
|
|
45
|
+
"lint": "biome check src",
|
|
46
|
+
"format": "biome format --write src"
|
|
47
|
+
}
|
|
48
|
+
}
|