@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 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.
@@ -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
+ }