@lenso/authorization 0.0.0-stage → 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.
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ CREATE TABLE IF NOT EXISTS authorization_role_graphs (
2
+ namespace text PRIMARY KEY,
3
+ revision text NOT NULL,
4
+ graph jsonb NOT NULL
5
+ );
@@ -0,0 +1,5 @@
1
+ CREATE TABLE IF NOT EXISTS authorization_role_graphs (
2
+ namespace TEXT PRIMARY KEY,
3
+ revision TEXT NOT NULL,
4
+ graph TEXT NOT NULL
5
+ );
package/package.json CHANGED
@@ -1,6 +1,82 @@
1
1
  {
2
2
  "name": "@lenso/authorization",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
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.0",
61
+ "@lenso/core": "^0.2.1",
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.0",
68
+ "@lenso/core": "^0.2.1",
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/).