@lenso/authorization 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +407 -2
- package/VALIDATION.md +105 -0
- package/dist/auth.d.ts +22 -0
- package/dist/auth.js +59 -0
- package/dist/conditions.d.ts +8 -0
- package/dist/core.d.ts +2 -0
- package/dist/drizzle/d1.d.ts +4 -0
- package/dist/drizzle/d1.js +12 -0
- package/dist/drizzle/pg.d.ts +5 -0
- package/dist/drizzle/pg.js +32 -0
- package/dist/drizzle/schema-pg.d.ts +61 -0
- package/dist/drizzle/schema-pg.js +6 -0
- package/dist/drizzle/schema-sqlite.d.ts +65 -0
- package/dist/drizzle/schema-sqlite.js +6 -0
- package/dist/drizzle/shared.d.ts +8 -0
- package/dist/drizzle/sqlite.d.ts +11 -0
- package/dist/drizzle/sqlite.js +9 -0
- package/dist/errors.d.ts +6 -0
- package/dist/index-4znahyqj.js +9 -0
- package/dist/index-5k98eatm.js +224 -0
- package/dist/index-6f5nrdh9.js +16 -0
- package/dist/index-pmwmdjss.js +32 -0
- package/dist/index-qjqy8v4k.js +9 -0
- package/dist/index-t6xdqvqs.js +56 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +464 -0
- package/dist/list.d.ts +11 -0
- package/dist/management.d.ts +43 -0
- package/dist/plugin.d.ts +11 -0
- package/dist/plugin.js +12 -0
- package/dist/rbac.d.ts +24 -0
- package/dist/snapshot.d.ts +5 -0
- package/dist/types.d.ts +162 -0
- package/migrations/0001-role-graphs-pg.sql +5 -0
- package/migrations/0001-role-graphs-sqlite.sql +5 -0
- package/package.json +80 -4
- package/src/drizzle/README.md +48 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
export type Awaitable<T> = T | Promise<T>;
|
|
2
|
+
export type Attributes = Readonly<Record<string, unknown>>;
|
|
3
|
+
/** Trusted application facts, not a credential or an authenticated JSON DTO. */
|
|
4
|
+
export interface Principal {
|
|
5
|
+
readonly realmId: string;
|
|
6
|
+
readonly subjectId: string;
|
|
7
|
+
readonly kind: string;
|
|
8
|
+
readonly attributes?: Attributes;
|
|
9
|
+
}
|
|
10
|
+
export interface Scope {
|
|
11
|
+
readonly type: string;
|
|
12
|
+
readonly id: string;
|
|
13
|
+
}
|
|
14
|
+
export interface Resource {
|
|
15
|
+
readonly type: string;
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly scope: Scope;
|
|
18
|
+
readonly attributes?: Attributes;
|
|
19
|
+
}
|
|
20
|
+
export interface Permission<A extends string = string> {
|
|
21
|
+
readonly action: A;
|
|
22
|
+
readonly resourceType: string;
|
|
23
|
+
readonly scope: Scope;
|
|
24
|
+
/** Omission explicitly means every resource of this type in the exact scope. */
|
|
25
|
+
readonly resourceId?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface CredentialLimit<A extends string = string> {
|
|
28
|
+
readonly permissions: readonly Permission<A>[];
|
|
29
|
+
readonly expiresAt?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface Request<A extends string = string, R extends Resource = Resource, C = Attributes> {
|
|
32
|
+
readonly principal: Principal | null;
|
|
33
|
+
readonly action: A;
|
|
34
|
+
readonly resource: R;
|
|
35
|
+
readonly context: C;
|
|
36
|
+
readonly audience?: string;
|
|
37
|
+
readonly credential?: CredentialLimit<A>;
|
|
38
|
+
}
|
|
39
|
+
export type Effect = "allow" | "deny" | "abstain";
|
|
40
|
+
export type ReasonCode = "ALLOWED" | "DEFAULT_DENY" | "EXPLICIT_DENY" | "BOUNDARY_DENIED" | "UNKNOWN_ACTION" | "EVALUATION_FAILED" | "TIMEOUT" | "CANCELLED";
|
|
41
|
+
export interface Decision {
|
|
42
|
+
readonly effect: "allow" | "deny";
|
|
43
|
+
readonly code: ReasonCode;
|
|
44
|
+
}
|
|
45
|
+
export interface Evaluation {
|
|
46
|
+
readonly signal: AbortSignal;
|
|
47
|
+
readonly now: number;
|
|
48
|
+
}
|
|
49
|
+
export type Predicate<A extends string = string, R extends Resource = Resource, C = Attributes> = (request: Request<A, R, C>, evaluation: Evaluation) => Awaitable<boolean>;
|
|
50
|
+
export type Condition<A extends string = string, R extends Resource = Resource, C = Attributes> = {
|
|
51
|
+
readonly kind: "all";
|
|
52
|
+
readonly conditions: readonly Condition<A, R, C>[];
|
|
53
|
+
} | {
|
|
54
|
+
readonly kind: "any";
|
|
55
|
+
readonly conditions: readonly Condition<A, R, C>[];
|
|
56
|
+
} | {
|
|
57
|
+
readonly kind: "predicate";
|
|
58
|
+
readonly test: Predicate<A, R, C>;
|
|
59
|
+
} | {
|
|
60
|
+
readonly kind: "attribute";
|
|
61
|
+
readonly source: "principal" | "resource" | "context";
|
|
62
|
+
readonly key: string;
|
|
63
|
+
readonly operator: "equals" | "in";
|
|
64
|
+
readonly value: string | number | boolean | null | readonly (string | number | boolean | null)[];
|
|
65
|
+
} | {
|
|
66
|
+
readonly kind: "relation";
|
|
67
|
+
readonly relation: string;
|
|
68
|
+
readonly target?: Resource;
|
|
69
|
+
};
|
|
70
|
+
export interface Rule<A extends string = string, R extends Resource = Resource, C = Attributes> {
|
|
71
|
+
/** Local identifier; explanations return ordinal paths, never this value. */
|
|
72
|
+
readonly id: string;
|
|
73
|
+
readonly effect: "allow" | "deny";
|
|
74
|
+
readonly actions: readonly A[];
|
|
75
|
+
readonly resourceType: string;
|
|
76
|
+
readonly scope?: Scope;
|
|
77
|
+
readonly resourceId?: string;
|
|
78
|
+
readonly when: Condition<A, R, C>;
|
|
79
|
+
}
|
|
80
|
+
export interface Policy<A extends string = string, R extends Resource = Resource, C = Attributes> {
|
|
81
|
+
evaluate(request: Request<A, R, C>, evaluation: Evaluation): Awaitable<Effect>;
|
|
82
|
+
}
|
|
83
|
+
export interface RelationResolver {
|
|
84
|
+
check(principal: Principal, relation: string, resource: Resource, evaluation: Evaluation): Awaitable<boolean>;
|
|
85
|
+
}
|
|
86
|
+
export interface ResolvedAttributes<C = Attributes> {
|
|
87
|
+
readonly principal?: Attributes;
|
|
88
|
+
readonly resource?: Attributes;
|
|
89
|
+
readonly context?: C;
|
|
90
|
+
}
|
|
91
|
+
export interface AttributeResolver<A extends string = string, R extends Resource = Resource, C = Attributes> {
|
|
92
|
+
resolve(request: Request<A, R, C>, evaluation: Evaluation): Awaitable<ResolvedAttributes<C>>;
|
|
93
|
+
}
|
|
94
|
+
export interface AuthorizationOptions<A extends string, R extends Resource, C> {
|
|
95
|
+
readonly actions: readonly A[];
|
|
96
|
+
readonly rules?: readonly Rule<A, R, C>[];
|
|
97
|
+
readonly policies?: readonly Policy<A, R, C>[];
|
|
98
|
+
/** These intersect before any allow-producing extension executes. */
|
|
99
|
+
readonly boundaries?: readonly Predicate<A, R, C>[];
|
|
100
|
+
readonly identity?: {
|
|
101
|
+
readonly required?: boolean;
|
|
102
|
+
readonly realms?: readonly string[];
|
|
103
|
+
readonly audiences?: readonly string[];
|
|
104
|
+
readonly credentialRequired?: boolean;
|
|
105
|
+
};
|
|
106
|
+
readonly relations?: RelationResolver;
|
|
107
|
+
readonly attributes?: AttributeResolver<A, R, C>;
|
|
108
|
+
readonly resolveResource?: (resource: R, evaluation: Evaluation) => Awaitable<R>;
|
|
109
|
+
readonly timeoutMs?: number;
|
|
110
|
+
readonly clock?: () => number;
|
|
111
|
+
/** No subject, resource IDs, policy data or exception messages are supplied. */
|
|
112
|
+
readonly observe?: (decision: Decision) => Awaitable<void>;
|
|
113
|
+
readonly explain?: {
|
|
114
|
+
readonly action: A;
|
|
115
|
+
readonly resource: R;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export interface Explanation {
|
|
119
|
+
readonly decision: Decision;
|
|
120
|
+
readonly paths: readonly string[];
|
|
121
|
+
}
|
|
122
|
+
export interface Authorization<A extends string = string, R extends Resource = Resource, C = Attributes> {
|
|
123
|
+
check(request: Request<A, R, C>, options?: {
|
|
124
|
+
signal?: AbortSignal;
|
|
125
|
+
}): Promise<Decision>;
|
|
126
|
+
can(request: Request<A, R, C>, options?: {
|
|
127
|
+
signal?: AbortSignal;
|
|
128
|
+
}): Promise<boolean>;
|
|
129
|
+
enforce(request: Request<A, R, C>, options?: {
|
|
130
|
+
signal?: AbortSignal;
|
|
131
|
+
}): Promise<void>;
|
|
132
|
+
explain(manager: Request<A, R, C>, target: Request<A, R, C>, options?: {
|
|
133
|
+
signal?: AbortSignal;
|
|
134
|
+
}): Promise<Explanation>;
|
|
135
|
+
}
|
|
136
|
+
export interface Role<A extends string = string> {
|
|
137
|
+
readonly id: string;
|
|
138
|
+
readonly scope: Scope;
|
|
139
|
+
readonly permissions: readonly Permission<A>[];
|
|
140
|
+
/** IDs resolve only within this role's exact scope. */
|
|
141
|
+
readonly inherits?: readonly string[];
|
|
142
|
+
}
|
|
143
|
+
export interface Binding {
|
|
144
|
+
readonly id: string;
|
|
145
|
+
readonly principal: Principal;
|
|
146
|
+
readonly roleId: string;
|
|
147
|
+
readonly scope: Scope;
|
|
148
|
+
readonly expiresAt?: number;
|
|
149
|
+
}
|
|
150
|
+
export interface RoleGraph<A extends string = string> {
|
|
151
|
+
readonly roles: readonly Role<A>[];
|
|
152
|
+
readonly bindings: readonly Binding[];
|
|
153
|
+
}
|
|
154
|
+
export interface RoleSnapshot<A extends string = string> {
|
|
155
|
+
readonly revision: string;
|
|
156
|
+
readonly graph: RoleGraph<A>;
|
|
157
|
+
}
|
|
158
|
+
/** Trusted internal repository. Mutations belong behind the authorized management service. */
|
|
159
|
+
export interface RoleStore<A extends string = string> {
|
|
160
|
+
read(evaluation: Evaluation): Awaitable<RoleSnapshot<A>>;
|
|
161
|
+
compareAndSwap(expectedRevision: string, next: RoleSnapshot<A>, evaluation: Evaluation): Awaitable<boolean>;
|
|
162
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,82 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenso/authorization",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/LioRael/lenso.git",
|
|
8
|
+
"directory": "packages/authorization"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"migrations",
|
|
13
|
+
"README.md",
|
|
14
|
+
"VALIDATION.md",
|
|
15
|
+
"src/drizzle/README.md"
|
|
16
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./auth": {
|
|
24
|
+
"types": "./dist/auth.d.ts",
|
|
25
|
+
"default": "./dist/auth.js"
|
|
26
|
+
},
|
|
27
|
+
"./plugin": {
|
|
28
|
+
"types": "./dist/plugin.d.ts",
|
|
29
|
+
"default": "./dist/plugin.js"
|
|
30
|
+
},
|
|
31
|
+
"./drizzle/pg": {
|
|
32
|
+
"types": "./dist/drizzle/pg.d.ts",
|
|
33
|
+
"default": "./dist/drizzle/pg.js"
|
|
34
|
+
},
|
|
35
|
+
"./drizzle/d1": {
|
|
36
|
+
"types": "./dist/drizzle/d1.d.ts",
|
|
37
|
+
"default": "./dist/drizzle/d1.js"
|
|
38
|
+
},
|
|
39
|
+
"./drizzle/sqlite": {
|
|
40
|
+
"types": "./dist/drizzle/sqlite.d.ts",
|
|
41
|
+
"default": "./dist/drizzle/sqlite.js"
|
|
42
|
+
},
|
|
43
|
+
"./drizzle/schema-pg": {
|
|
44
|
+
"types": "./dist/drizzle/schema-pg.d.ts",
|
|
45
|
+
"default": "./dist/drizzle/schema-pg.js"
|
|
46
|
+
},
|
|
47
|
+
"./drizzle/schema-sqlite": {
|
|
48
|
+
"types": "./dist/drizzle/schema-sqlite.d.ts",
|
|
49
|
+
"default": "./dist/drizzle/schema-sqlite.js"
|
|
50
|
+
},
|
|
51
|
+
"./migrations/*": "./migrations/*"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && bun build src/index.ts src/auth.ts src/plugin.ts src/drizzle/pg.ts src/drizzle/d1.ts src/drizzle/sqlite.ts src/drizzle/schema-pg.ts src/drizzle/schema-sqlite.ts --root src --outdir dist --target browser --packages external --splitting",
|
|
55
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
56
|
+
"test": "bun test test"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@cloudflare/workers-types": "5.20261008.1",
|
|
60
|
+
"@lenso/auth": "^0.3.1",
|
|
61
|
+
"@lenso/core": "^0.3.0",
|
|
62
|
+
"@standard-schema/spec": "1.1.0",
|
|
63
|
+
"drizzle-orm": "0.45.3",
|
|
64
|
+
"miniflare": "5.20261006.0-alpha"
|
|
65
|
+
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"@lenso/auth": "^0.3.1",
|
|
68
|
+
"@lenso/core": "^0.3.0",
|
|
69
|
+
"drizzle-orm": "^0.45.3"
|
|
70
|
+
},
|
|
71
|
+
"peerDependenciesMeta": {
|
|
72
|
+
"@lenso/auth": {
|
|
73
|
+
"optional": true
|
|
74
|
+
},
|
|
75
|
+
"@lenso/core": {
|
|
76
|
+
"optional": true
|
|
77
|
+
},
|
|
78
|
+
"drizzle-orm": {
|
|
79
|
+
"optional": true
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Drizzle role stores
|
|
2
|
+
|
|
3
|
+
`postgresRoleStore(db, namespace, actions)`, `d1RoleStore(db, namespace, actions)`
|
|
4
|
+
and `sqliteRoleStore(db, namespace, actions)` borrow the supplied native Drizzle
|
|
5
|
+
database. Reads and writes use the same core role-graph validation, including
|
|
6
|
+
known actions, inheritance and scoped bindings; returned snapshots are immutable.
|
|
7
|
+
|
|
8
|
+
Apply `migrations/0001-role-graphs-pg.sql` or
|
|
9
|
+
`migrations/0001-role-graphs-sqlite.sql` through your explicit migration owner,
|
|
10
|
+
then call `store.initialize(snapshot)` once using trusted bootstrap configuration.
|
|
11
|
+
This is an internal seed API, not an unauthenticated management operation.
|
|
12
|
+
An existing namespace is preserved. Construction and setup run no migration or
|
|
13
|
+
initialization. PostgreSQL JSON is explicitly bound as text and cast to JSONB to
|
|
14
|
+
match the repository's Bun SQL/Drizzle serialization pattern.
|
|
15
|
+
|
|
16
|
+
Each namespace is stored as one graph document. Compare-and-swap is a single
|
|
17
|
+
conditional `UPDATE ... WHERE namespace = ? AND revision = ? RETURNING ...`.
|
|
18
|
+
It does not wrap authorization data and resource facts in a transaction.
|
|
19
|
+
|
|
20
|
+
All writers must use a new, never-reused revision token; management generates a
|
|
21
|
+
UUID for every mutation. Reusing the current revision is rejected. Raw store
|
|
22
|
+
access is trusted infrastructure, not an API for clients to choose revisions.
|
|
23
|
+
One graph document makes a role/binding update indivisible within that row,
|
|
24
|
+
but serializes writes in the namespace and is bounded to the core's graph limits.
|
|
25
|
+
It is not a scalable relationship database.
|
|
26
|
+
|
|
27
|
+
PostgreSQL supports transactions, but this adapter needs only one conditional
|
|
28
|
+
statement. D1 uses the same SQLite statement and no interactive transaction.
|
|
29
|
+
SQLite and local Miniflare exercise `UPDATE RETURNING` with the actual driver.
|
|
30
|
+
Cancellation is checked before and after read/CAS. Cancellation or connection
|
|
31
|
+
loss after commit can leave a successful mutation with an uncertain caller
|
|
32
|
+
outcome: no rollback or automatic retry is promised.
|
|
33
|
+
|
|
34
|
+
The borrowed database controls consistency. PostgreSQL repeatable-read
|
|
35
|
+
transactions/replicas can retain older snapshots. D1's direct binding queries
|
|
36
|
+
use primary routing per the current docs; an injected D1 Sessions API/replica
|
|
37
|
+
client must be configured for the application's revocation requirement.
|
|
38
|
+
`first-unconstrained` can begin stale; use a fresh `first-primary` session where
|
|
39
|
+
latest primary data is required. The adapter does not infer or override routing.
|
|
40
|
+
|
|
41
|
+
The update/returning pattern follows Drizzle's
|
|
42
|
+
[PostgreSQL](https://orm.drizzle.team/docs/update) and
|
|
43
|
+
[SQLite](https://orm.drizzle.team/docs/sqlite/update) update APIs. D1 uses
|
|
44
|
+
SQLite-compatible SQL through Drizzle's D1 driver; its prepared statement API
|
|
45
|
+
documents SQLite binding semantics and result retrieval at
|
|
46
|
+
[Cloudflare D1](https://developers.cloudflare.com/d1/worker-api/prepared-statements/).
|
|
47
|
+
Replication/consistency reference:
|
|
48
|
+
[D1 global read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/).
|