@stratal/framework 0.0.27 → 0.1.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/CHANGELOG.md +596 -0
- package/README.md +166 -24
- package/dist/access-control/index.d.mts +43 -16
- package/dist/access-control/index.d.mts.map +1 -1
- package/dist/access-control/index.mjs +5 -5
- package/dist/access-control/index.mjs.map +1 -1
- package/dist/{access.service-BmDhE-re.mjs → access.service-BjmnWBEo.mjs} +35 -17
- package/dist/access.service-BjmnWBEo.mjs.map +1 -0
- package/dist/auth/index.d.mts +105 -103
- package/dist/auth/index.d.mts.map +1 -1
- package/dist/auth/index.mjs +123 -24
- package/dist/auth/index.mjs.map +1 -1
- package/dist/{auth-context-CGVbiSX3.d.mts → auth-context-C1om3Zsr.d.mts} +1 -2
- package/dist/auth-context-C1om3Zsr.d.mts.map +1 -0
- package/dist/{auth-context-C8NBfiMa.mjs → auth-context-cNSS1rmh.mjs} +2 -2
- package/dist/{auth-context-C8NBfiMa.mjs.map → auth-context-cNSS1rmh.mjs.map} +1 -1
- package/dist/auth.service-Onf3JkyL.d.mts +44 -0
- package/dist/auth.service-Onf3JkyL.d.mts.map +1 -0
- package/dist/context/index.d.mts +4 -5
- package/dist/context/index.d.mts.map +1 -1
- package/dist/context/index.mjs +1 -1
- package/dist/database/index.d.mts +3 -3
- package/dist/database/index.mjs +413 -34
- package/dist/database/index.mjs.map +1 -1
- package/dist/{decorate-B7nr7eBl.mjs → decorate-RQD1h28J.mjs} +1 -1
- package/dist/{decorateParam-DwV9LSPl.mjs → decorateParam-xwTkq9gO.mjs} +2 -2
- package/dist/{decorateParam-DwV9LSPl.mjs.map → decorateParam-xwTkq9gO.mjs.map} +1 -1
- package/dist/factory/index.d.mts +3 -5
- package/dist/factory/index.d.mts.map +1 -1
- package/dist/factory/index.mjs.map +1 -1
- package/dist/guards/index.d.mts +3 -4
- package/dist/guards/index.d.mts.map +1 -1
- package/dist/guards/index.mjs +4 -4
- package/dist/guards/index.mjs.map +1 -1
- package/dist/index-e_u1SRyd.d.mts +921 -0
- package/dist/index-e_u1SRyd.d.mts.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/{types-CWZ9q74G.d.mts → types-B35g-lXi.d.mts} +18 -4
- package/dist/types-B35g-lXi.d.mts.map +1 -0
- package/package.json +31 -27
- package/dist/access.service-BmDhE-re.mjs.map +0 -1
- package/dist/auth-context-CGVbiSX3.d.mts.map +0 -1
- package/dist/index-Dt0YUA7r.d.mts +0 -446
- package/dist/index-Dt0YUA7r.d.mts.map +0 -1
- package/dist/types-CWZ9q74G.d.mts.map +0 -1
- package/dist/types-DabF8LGz.d.mts +0 -11
- package/dist/types-DabF8LGz.d.mts.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @stratal/framework
|
|
2
2
|
|
|
3
|
-
Authentication, database ORM,
|
|
3
|
+
Authentication, database ORM, access control, guards, and test factories for [Stratal](https://stratal.dev) applications.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@stratal/framework)
|
|
6
6
|
[](https://github.com/strataljs/stratal/actions/workflows/ci.yml)
|
|
@@ -13,13 +13,13 @@ Authentication, database ORM, RBAC, guards, and test factories for [Stratal](htt
|
|
|
13
13
|
[](https://github.com/strataljs/stratal/pulls)
|
|
14
14
|
[](https://github.com/strataljs/stratal)
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## What's inside
|
|
17
17
|
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **AuthGuard** — Authentication and
|
|
22
|
-
- **AuthContext** — Request-scoped user context for
|
|
18
|
+
- **DatabaseModule** — ZenStack ORM with multiple named connections, plus `db:*` and `migrate:*` Quarry commands
|
|
19
|
+
- **AuthModule** — Better Auth integration with session verification middleware and a `ctx.user()` shorthand
|
|
20
|
+
- **Access control** — Resources, roles and permissions built on Better Auth's `access` plugin
|
|
21
|
+
- **AuthGuard** — Authentication and permission enforcement for routes and controllers
|
|
22
|
+
- **AuthContext** — Request-scoped user context for the authenticated user
|
|
23
23
|
- **Factory** — Test data factories with sequenced attributes powered by Faker.js
|
|
24
24
|
|
|
25
25
|
## Installation
|
|
@@ -32,16 +32,25 @@ yarn add @stratal/framework
|
|
|
32
32
|
|
|
33
33
|
### Peer dependencies
|
|
34
34
|
|
|
35
|
-
| Package |
|
|
35
|
+
| Package | Notes |
|
|
36
36
|
|---|---|
|
|
37
|
-
| `stratal` |
|
|
38
|
-
| `@zenstackhq/orm` |
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
41
|
-
|
|
|
42
|
-
| `@
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
| `stratal` | The core framework |
|
|
38
|
+
| `@zenstackhq/orm` | ORM runtime |
|
|
39
|
+
| `@zenstackhq/schema` | Generated schema types |
|
|
40
|
+
| `pg` | PostgreSQL driver |
|
|
41
|
+
| `better-auth` | Authentication |
|
|
42
|
+
| `@better-auth/core` | Authentication |
|
|
43
|
+
|
|
44
|
+
### Sub-path exports
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { AuthModule } from '@stratal/framework/auth'
|
|
48
|
+
import { DatabaseModule } from '@stratal/framework/database'
|
|
49
|
+
import { createAccessControl } from '@stratal/framework/access-control'
|
|
50
|
+
import { AuthGuard } from '@stratal/framework/guards'
|
|
51
|
+
import type { AuthContext, AuthUser } from '@stratal/framework/context'
|
|
52
|
+
import { Factory } from '@stratal/framework/factory'
|
|
53
|
+
```
|
|
45
54
|
|
|
46
55
|
### AI Agent Skills
|
|
47
56
|
|
|
@@ -53,27 +62,48 @@ npx skills add strataljs/stratal
|
|
|
53
62
|
|
|
54
63
|
| Skill | Description |
|
|
55
64
|
|---|---|
|
|
56
|
-
| `stratal` | Build Cloudflare Workers apps with the Stratal framework — modules, DI, controllers, routing, OpenAPI, queues, cron, events, seeders, CLI, auth, database,
|
|
65
|
+
| `stratal` | Build Cloudflare Workers apps with the Stratal framework — modules, DI, controllers, routing, OpenAPI, queues, cron, events, seeders, CLI, auth, database, access control, testing, and more |
|
|
57
66
|
|
|
58
67
|
## Quick Start
|
|
59
68
|
|
|
60
69
|
```typescript
|
|
61
70
|
import { Stratal } from 'stratal'
|
|
71
|
+
import { DI_TOKENS } from 'stratal/di'
|
|
62
72
|
import { Module } from 'stratal/module'
|
|
63
73
|
import { AuthModule } from '@stratal/framework/auth'
|
|
64
|
-
import { DatabaseModule } from '@stratal/framework/database'
|
|
65
|
-
import {
|
|
74
|
+
import { createPoolFactory, DatabaseModule } from '@stratal/framework/database'
|
|
75
|
+
import { PostgresDialect } from 'kysely'
|
|
76
|
+
import { Pool } from 'pg'
|
|
77
|
+
import { permissions } from './permissions'
|
|
78
|
+
import { schema } from './zenstack/schema'
|
|
66
79
|
|
|
67
80
|
@Module({
|
|
68
81
|
imports: [
|
|
69
|
-
DatabaseModule.
|
|
70
|
-
|
|
82
|
+
DatabaseModule.forRootAsync({
|
|
83
|
+
inject: [DI_TOKENS.CloudflareEnv],
|
|
84
|
+
useFactory: (env) => ({
|
|
85
|
+
default: 'main',
|
|
86
|
+
connections: [
|
|
87
|
+
{
|
|
88
|
+
name: 'main',
|
|
89
|
+
schema,
|
|
90
|
+
dialect: () => new PostgresDialect({
|
|
91
|
+
pool: createPoolFactory(env, () => new Pool({
|
|
92
|
+
connectionString: env.HYPERDRIVE.connectionString,
|
|
93
|
+
})),
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
}),
|
|
71
98
|
}),
|
|
72
99
|
AuthModule.forRootAsync({
|
|
73
|
-
inject: [],
|
|
74
|
-
useFactory: () => ({
|
|
100
|
+
inject: [DI_TOKENS.Database],
|
|
101
|
+
useFactory: (db) => ({
|
|
102
|
+
database: db,
|
|
103
|
+
emailAndPassword: { enabled: true },
|
|
104
|
+
accessControl: permissions,
|
|
105
|
+
}),
|
|
75
106
|
}),
|
|
76
|
-
RbacModule.forRoot({ model: MODEL }),
|
|
77
107
|
],
|
|
78
108
|
})
|
|
79
109
|
class AppModule {}
|
|
@@ -81,10 +111,122 @@ class AppModule {}
|
|
|
81
111
|
export default new Stratal({ module: AppModule })
|
|
82
112
|
```
|
|
83
113
|
|
|
114
|
+
A connection takes a generated ZenStack `schema` and a `dialect` factory. `createPoolFactory()` picks the connection topology from the environment — a fresh pool per resolution behind Hyperdrive in production, one shared pool under `@stratal/testing` — so your app config never branches on whether it is running under test.
|
|
115
|
+
|
|
116
|
+
The factory is only called when the module initializes, so a large generated schema can sit behind an `import()` and stay out of the isolate's startup budget.
|
|
117
|
+
|
|
118
|
+
## Access control
|
|
119
|
+
|
|
120
|
+
Define resources and roles once with `createAccessControl()`, then hand the result to Better Auth:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
// permissions.ts
|
|
124
|
+
import { createAccessControl } from '@stratal/framework/access-control'
|
|
125
|
+
|
|
126
|
+
export const permissions = createAccessControl({
|
|
127
|
+
resources: {
|
|
128
|
+
posts: ['create', 'read', 'update', 'delete'],
|
|
129
|
+
admin: ['access'],
|
|
130
|
+
} as const,
|
|
131
|
+
roles: {
|
|
132
|
+
user: { posts: ['create', 'read'] },
|
|
133
|
+
editor: { posts: ['create', 'read', 'update'] },
|
|
134
|
+
admin: { posts: ['create', 'read', 'update', 'delete'], admin: ['access'] },
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Compose roles with `extendRole()` — duplicate resource keys are merged, not overwritten:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { extendRole } from '@stratal/framework/access-control'
|
|
143
|
+
|
|
144
|
+
const { ac, roles } = permissions
|
|
145
|
+
const superAdmin = extendRole(ac, roles.admin, { users: ['ban', 'delete'] })
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Guards
|
|
149
|
+
|
|
150
|
+
`AuthGuard()` enforces authentication; pass `permissions` to enforce authorization too. Prefer guards over checking the auth context by hand — they throw the right 401 / 403 automatically.
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import { AuthGuard } from '@stratal/framework/guards'
|
|
154
|
+
import { UseGuards } from 'stratal/guards'
|
|
155
|
+
import { Controller } from 'stratal/router'
|
|
156
|
+
|
|
157
|
+
@Controller('/profile', { version: '1' })
|
|
158
|
+
@UseGuards(AuthGuard())
|
|
159
|
+
export class ProfileController {}
|
|
160
|
+
|
|
161
|
+
@Controller('/admin', { version: '1' })
|
|
162
|
+
@UseGuards(AuthGuard({ permissions: 'admin:access' }))
|
|
163
|
+
export class AdminController {}
|
|
164
|
+
|
|
165
|
+
// Several permissions — all must be satisfied by one of the user's roles
|
|
166
|
+
@Controller('/posts', { version: '1' })
|
|
167
|
+
@UseGuards(AuthGuard({ permissions: ['posts:update', 'posts:delete'] }))
|
|
168
|
+
export class PostsController {}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Permissions are written `resource:action`; `resource` alone means "any action on that resource". A list is combined with AND — every permission must be granted — while a user holding several roles passes if any one role satisfies the whole list.
|
|
172
|
+
|
|
173
|
+
Guards can also be applied per method, and combined with Stratal's [versioning](https://stratal.dev) so the version never has to be written into the path.
|
|
174
|
+
|
|
175
|
+
## Reading the authenticated user
|
|
176
|
+
|
|
177
|
+
Enforcement belongs on the guard, not in the handler. Once `AuthGuard()` is applied, the route only runs for an authenticated user, so reading the user is just a read:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
@Controller('/profile', { version: '1' })
|
|
181
|
+
@UseGuards(AuthGuard())
|
|
182
|
+
export class ProfileController {
|
|
183
|
+
async show(ctx: RouterContext) {
|
|
184
|
+
return ctx.json(await this.service.forUser(ctx.user().id))
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`ctx.user()` is a `RouterContext` shorthand that `AuthModule` adds. Inject `AuthContext` where you don't have the request context to hand:
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import type { AuthContext } from '@stratal/framework/context'
|
|
193
|
+
import { DI_TOKENS, inject, Transient } from 'stratal/di'
|
|
194
|
+
|
|
195
|
+
@Transient()
|
|
196
|
+
export class ProfileService {
|
|
197
|
+
constructor(@inject(DI_TOKENS.AuthContext) private auth: AuthContext) {}
|
|
198
|
+
|
|
199
|
+
profile() {
|
|
200
|
+
return this.auth.requireUser()
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`requireUser()` throws `UserNotAuthenticatedError` when signed out — a backstop for code reachable outside a guarded route, not a substitute for the guard. `getUser()` returns `AuthUser | undefined` instead of throwing, and `getRoles()` splits `user.role` on commas.
|
|
206
|
+
|
|
207
|
+
Augment `AuthUser` to type your own fields:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
declare module '@stratal/framework/context' {
|
|
211
|
+
interface AuthUser {
|
|
212
|
+
firstName: string
|
|
213
|
+
role: string
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
84
218
|
## Documentation
|
|
85
219
|
|
|
86
220
|
Full guides and examples are available at **[stratal.dev](https://stratal.dev)**.
|
|
87
221
|
|
|
222
|
+
## Support the project
|
|
223
|
+
|
|
224
|
+
If Stratal is useful to you, **[star the repository](https://github.com/strataljs/stratal)** — it is the simplest way to help others find it.
|
|
225
|
+
|
|
226
|
+
## Maintainer
|
|
227
|
+
|
|
228
|
+
Built and maintained by **Temitayo Fadojutimi** — [@adesege_](https://x.com/adesege_).
|
|
229
|
+
|
|
88
230
|
## License
|
|
89
231
|
|
|
90
232
|
MIT
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { n as RolePermissions, t as
|
|
2
|
-
import {
|
|
3
|
-
import { t as AuthContext } from "../auth-context-CGVbiSX3.mjs";
|
|
1
|
+
import { n as AccessControlOptions, r as RolePermissions, t as AuthService } from "../auth.service-Onf3JkyL.mjs";
|
|
2
|
+
import { t as AuthContext } from "../auth-context-C1om3Zsr.mjs";
|
|
4
3
|
import { HttpException } from "stratal/errors";
|
|
4
|
+
import { Middleware, Next, RouterContext } from "stratal/router";
|
|
5
5
|
import { AccessControl, Role, Statements } from "better-auth/plugins/access";
|
|
6
6
|
import { BetterAuthPlugin } from "better-auth";
|
|
7
|
-
|
|
8
7
|
//#region src/access-control/tokens.d.ts
|
|
9
|
-
declare const AC_TOKENS: {
|
|
10
|
-
/** Request-scoped access service */
|
|
8
|
+
export declare const AC_TOKENS: {
|
|
9
|
+
/** Request-scoped access service */
|
|
10
|
+
readonly AccessService: symbol;
|
|
11
|
+
/** Access control module options (ac, roles) */
|
|
11
12
|
readonly Options: symbol;
|
|
12
13
|
};
|
|
13
14
|
//#endregion
|
|
@@ -24,7 +25,7 @@ declare const AC_TOKENS: {
|
|
|
24
25
|
* resources: {
|
|
25
26
|
* posts: ['create', 'read', 'update', 'delete'],
|
|
26
27
|
* admin: ['access'],
|
|
27
|
-
* }
|
|
28
|
+
* },
|
|
28
29
|
* roles: {
|
|
29
30
|
* admin: { posts: ['create', 'read', 'update', 'delete'], admin: ['access'] },
|
|
30
31
|
* user: { posts: ['create', 'read'] },
|
|
@@ -38,7 +39,7 @@ declare const AC_TOKENS: {
|
|
|
38
39
|
* plugins: [admin({ ...permissions })]
|
|
39
40
|
* ```
|
|
40
41
|
*/
|
|
41
|
-
declare function createAccessControl<TResources extends Statements, TRoles extends Record<string, RolePermissions<TResources>>>(config: {
|
|
42
|
+
export declare function createAccessControl<const TResources extends Statements, const TRoles extends Record<string, RolePermissions<TResources>>>(config: {
|
|
42
43
|
resources: TResources;
|
|
43
44
|
roles: TRoles;
|
|
44
45
|
}): AccessControlOptions<TResources, TRoles>;
|
|
@@ -55,7 +56,7 @@ declare function createAccessControl<TResources extends Statements, TRoles exten
|
|
|
55
56
|
* // → { posts: readonly ('create' | 'read' | 'update')[]; admin: readonly ['access'] }
|
|
56
57
|
* ```
|
|
57
58
|
*/
|
|
58
|
-
type MergeStatements<A extends Statements, B extends Partial<Record<string, readonly string[]>>> = { [K in keyof A | keyof B]: K extends keyof A ? K extends keyof B ? readonly (A[K][number] | NonNullable<B[K]>[number])[] : A[K] : K extends keyof B ? NonNullable<B[K]> : never } & {};
|
|
59
|
+
type MergeStatements<A extends Statements, B extends Partial<Record<string, readonly string[]>>> = { [K in keyof A | keyof B]: K extends keyof A ? K extends keyof B ? readonly (A[K][number] | NonNullable<B[K]>[number])[] : A[K] : K extends keyof B ? NonNullable<B[K]> : never; } & {};
|
|
59
60
|
/**
|
|
60
61
|
* Extend an existing role with additional permissions.
|
|
61
62
|
*
|
|
@@ -73,7 +74,7 @@ type MergeStatements<A extends Statements, B extends Partial<Record<string, read
|
|
|
73
74
|
* // if userRole had posts: ['create', 'read'], editorRole has posts: ['create', 'read', 'update']
|
|
74
75
|
* ```
|
|
75
76
|
*/
|
|
76
|
-
declare function extendRole<TParent extends Statements, TExtra extends Partial<Record<string, readonly string[]>>>(ac: AccessControl<TParent>, parent: Role<TParent>, extra: TExtra): Role<MergeStatements<TParent, TExtra>>;
|
|
77
|
+
export declare function extendRole<TParent extends Statements, TExtra extends Partial<Record<string, readonly string[]>>>(ac: AccessControl<TParent>, parent: Role<TParent>, extra: TExtra): Role<MergeStatements<TParent, TExtra>>;
|
|
77
78
|
//#endregion
|
|
78
79
|
//#region src/access-control/plugin.d.ts
|
|
79
80
|
/**
|
|
@@ -85,7 +86,7 @@ declare function extendRole<TParent extends Statements, TExtra extends Partial<R
|
|
|
85
86
|
* Auto-added to Better Auth options when `accessControl` is provided to
|
|
86
87
|
* `AuthModule.forRootAsync()`. Users never call this directly.
|
|
87
88
|
*/
|
|
88
|
-
declare function createStratalAcPlugin(_options: AccessControlOptions): BetterAuthPlugin;
|
|
89
|
+
export declare function createStratalAcPlugin(_options: AccessControlOptions): BetterAuthPlugin;
|
|
89
90
|
//#endregion
|
|
90
91
|
//#region src/access-control/services/access.service.d.ts
|
|
91
92
|
/**
|
|
@@ -115,11 +116,11 @@ declare function createStratalAcPlugin(_options: AccessControlOptions): BetterAu
|
|
|
115
116
|
* await accessService.setUserRole(userId, ['editor', 'reviewer'])
|
|
116
117
|
* ```
|
|
117
118
|
*/
|
|
118
|
-
declare class AccessService {
|
|
119
|
+
export declare class AccessService {
|
|
119
120
|
private readonly authContext;
|
|
120
|
-
private readonly
|
|
121
|
+
private readonly authService;
|
|
121
122
|
private readonly options;
|
|
122
|
-
constructor(authContext: AuthContext,
|
|
123
|
+
constructor(authContext: AuthContext, authService: AuthService, options: AccessControlOptions);
|
|
123
124
|
/**
|
|
124
125
|
* Get all roles for a user.
|
|
125
126
|
*
|
|
@@ -163,12 +164,38 @@ declare class AccessService {
|
|
|
163
164
|
private mergePermissions;
|
|
164
165
|
}
|
|
165
166
|
//#endregion
|
|
167
|
+
//#region src/access-control/access-share.middleware.d.ts
|
|
168
|
+
/**
|
|
169
|
+
* Shares the current user's roles and merged permissions as the `access` prop
|
|
170
|
+
* on every Inertia page rendered during the request, so the client can gate
|
|
171
|
+
* rendering with `<Can>` / `<HasRole>` from `@stratal/inertia/react/access`.
|
|
172
|
+
*
|
|
173
|
+
* Both values come from `AuthContext` (populated by
|
|
174
|
+
* `SessionVerificationMiddleware`) — no database hit and no I/O, which is why
|
|
175
|
+
* this is registered globally by `AuthModule` rather than being opt-in the way
|
|
176
|
+
* `FeatureFlagShareMiddleware` is.
|
|
177
|
+
*
|
|
178
|
+
* Inert unless access control is configured and Inertia is installed. Not
|
|
179
|
+
* restricted to `GET`: an Inertia render can legally happen on any method —
|
|
180
|
+
* e.g. `return ctx.inertia('Posts/Create', {...})` from a `POST` handler
|
|
181
|
+
* re-rendering after a validation failure — and every `<Can>` / `useCan` on
|
|
182
|
+
* that page needs `access` just as much as it would on a full `GET` visit.
|
|
183
|
+
*
|
|
184
|
+
* Wrapped in `ctx.always()` so partial reloads never drop the prop; the client
|
|
185
|
+
* treats an absent `access` prop as a wiring error, not as "denied".
|
|
186
|
+
*/
|
|
187
|
+
export declare class AccessShareMiddleware implements Middleware {
|
|
188
|
+
private readonly access?;
|
|
189
|
+
constructor(access?: AccessService | undefined);
|
|
190
|
+
handle(ctx: RouterContext, next: Next): Promise<void>;
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
166
193
|
//#region src/access-control/errors/insufficient-permissions.error.d.ts
|
|
167
|
-
declare class InsufficientPermissionsError extends HttpException {
|
|
194
|
+
export declare class InsufficientPermissionsError extends HttpException {
|
|
168
195
|
readonly requiredPermissions: string;
|
|
169
196
|
readonly userId?: string;
|
|
170
197
|
constructor(requiredPermissions: string | string[], userId?: string);
|
|
171
198
|
}
|
|
172
199
|
//#endregion
|
|
173
|
-
export {
|
|
200
|
+
export type { AccessControlOptions };
|
|
174
201
|
//# sourceMappingURL=index.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/access-control/tokens.ts","../../src/access-control/create-access-control.ts","../../src/access-control/extend-role.ts","../../src/access-control/plugin.ts","../../src/access-control/services/access.service.ts","../../src/access-control/errors/insufficient-permissions.error.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/access-control/tokens.ts","../../src/access-control/create-access-control.ts","../../src/access-control/extend-role.ts","../../src/access-control/plugin.ts","../../src/access-control/services/access.service.ts","../../src/access-control/access-share.middleware.ts","../../src/access-control/errors/insufficient-permissions.error.ts"],"mappings":";;;;;;;qBAAa;;WAEX;;WAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBC0Bc,0BACR,mBAAmB,kBACnB,eAAe,eAAe,gBAAgB,cACpD;EACA,WAAW;EACX,OAAO;IACL,qBAAqB,YAAY;;;;;;;;;;;;;;KCvBhC,gBACH,UAAU,YACV,UAAU,QAAQ,yCAEjB,WAAW,UAAU,IAAI,gBAAgB,IACxC,gBAAgB,cACN,EAAE,aAAa,YAAY,EAAE,iBACvC,EAAE,KACF,gBAAgB,IAChB,YAAY,EAAE;;;;;;;;;;;;;;;;;;wBAqBF,WACd,gBAAgB,YAChB,eAAe,QAAQ,oCAEvB,IAAI,cAAc,UAClB,QAAQ,KAAK,UACb,OAAO,SACN,KAAK,gBAAgB,SAAS;;;;;;;;;;;;wBCtCjB,sBAAsB,UAAU,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBC4B1D;mBAGQ;mBAEA;mBAEA;EANnB,YAEmB,aAAa,aAEb,aAAa,aAEb,SAAS;;;;;;;EAStB,aAAa,iBAAiB;;;;;;EAe9B,YAAY,gBAAgB,0BAA0B;;;;;;EActD,cAAc,gBAAgB,aAAa,2BAA2B;;;;;EAStE,sBAAsB,iBAAiB,QAAQ;;;;;EASrD;;;;;EAQA,yBAAyB,aAAa;;;;;EAUtC,6BAA6B;UAKrB;UAqBA;;;;;;;;;;;;;;;;;;;;;;;qBC7GG,iCAAiC;mBAGzB;EAFnB,YAEmB,SAAS;EAGtB,OAAO,KAAK,eAAe,MAAM,OAAO;;;;qBC1CnC,qCAAqC;WAChC;WACA;EAEhB,YAAY,wCAAwC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
import {
|
|
1
|
+
import { n as AC_TOKENS } from "../decorateParam-xwTkq9gO.mjs";
|
|
2
|
+
import { a as AccessShareMiddleware, i as createStratalAcPlugin, t as AccessService } from "../access.service-BjmnWBEo.mjs";
|
|
3
3
|
import { t as InsufficientPermissionsError } from "../insufficient-permissions.error-DeEyZRgy.mjs";
|
|
4
4
|
import { createAccessControl as createAccessControl$1 } from "better-auth/plugins/access";
|
|
5
5
|
//#region src/access-control/create-access-control.ts
|
|
@@ -15,7 +15,7 @@ import { createAccessControl as createAccessControl$1 } from "better-auth/plugin
|
|
|
15
15
|
* resources: {
|
|
16
16
|
* posts: ['create', 'read', 'update', 'delete'],
|
|
17
17
|
* admin: ['access'],
|
|
18
|
-
* }
|
|
18
|
+
* },
|
|
19
19
|
* roles: {
|
|
20
20
|
* admin: { posts: ['create', 'read', 'update', 'delete'], admin: ['access'] },
|
|
21
21
|
* user: { posts: ['create', 'read'] },
|
|
@@ -60,12 +60,12 @@ function extendRole(ac, parent, extra) {
|
|
|
60
60
|
for (const [key, actions] of Object.entries(parent.statements)) merged[key] = [...actions];
|
|
61
61
|
for (const [key, actions] of Object.entries(extra)) {
|
|
62
62
|
if (!actions) continue;
|
|
63
|
-
if (key in merged) merged[key] = [
|
|
63
|
+
if (key in merged) merged[key] = [.../* @__PURE__ */ new Set([...merged[key], ...actions])];
|
|
64
64
|
else merged[key] = [...actions];
|
|
65
65
|
}
|
|
66
66
|
return ac.newRole(merged);
|
|
67
67
|
}
|
|
68
68
|
//#endregion
|
|
69
|
-
export { AC_TOKENS, AccessService, InsufficientPermissionsError, createAccessControl, createStratalAcPlugin, extendRole };
|
|
69
|
+
export { AC_TOKENS, AccessService, AccessShareMiddleware, InsufficientPermissionsError, createAccessControl, createStratalAcPlugin, extendRole };
|
|
70
70
|
|
|
71
71
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["baCreateAC"],"sources":["../../src/access-control/create-access-control.ts","../../src/access-control/extend-role.ts"],"sourcesContent":["import type { Role, Statements } from 'better-auth/plugins/access'\nimport { createAccessControl as baCreateAC } from 'better-auth/plugins/access'\nimport type { AccessControlOptions, RolePermissions } from './types'\n\n/**\n * Define access control resources and roles in one place.\n *\n * Returns `{ ac, roles }` — spread this directly into `accessControl`,\n * `admin({ ...permissions })`, or `organization({ ...permissions })`.\n *\n * @example\n * ```typescript\n * export const permissions = createAccessControl({\n * resources: {\n * posts: ['create', 'read', 'update', 'delete'],\n * admin: ['access'],\n * }
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["baCreateAC"],"sources":["../../src/access-control/create-access-control.ts","../../src/access-control/extend-role.ts"],"sourcesContent":["import type { Role, Statements } from 'better-auth/plugins/access'\nimport { createAccessControl as baCreateAC } from 'better-auth/plugins/access'\nimport type { AccessControlOptions, RolePermissions } from './types'\n\n/**\n * Define access control resources and roles in one place.\n *\n * Returns `{ ac, roles }` — spread this directly into `accessControl`,\n * `admin({ ...permissions })`, or `organization({ ...permissions })`.\n *\n * @example\n * ```typescript\n * export const permissions = createAccessControl({\n * resources: {\n * posts: ['create', 'read', 'update', 'delete'],\n * admin: ['access'],\n * },\n * roles: {\n * admin: { posts: ['create', 'read', 'update', 'delete'], admin: ['access'] },\n * user: { posts: ['create', 'read'] },\n * },\n * })\n *\n * // In AuthModule:\n * accessControl: permissions\n *\n * // With Better Auth admin plugin (same object):\n * plugins: [admin({ ...permissions })]\n * ```\n */\nexport function createAccessControl<\n const TResources extends Statements,\n const TRoles extends Record<string, RolePermissions<TResources>>,\n>(config: {\n resources: TResources\n roles: TRoles\n}): AccessControlOptions<TResources, TRoles> {\n const ac = baCreateAC(config.resources)\n const roles = Object.fromEntries(\n Object.entries(config.roles).map(([name, perms]) => [name, ac.newRole(perms as unknown as Statements)])\n ) as { [K in keyof TRoles]: Role<TResources> }\n return { ac, roles }\n}\n","import type { AccessControl, Role, Statements } from 'better-auth/plugins/access'\n\n/**\n * Merges two Statements types, unioning the action arrays for overlapping resource keys.\n *\n * @example\n * ```typescript\n * type A = { posts: readonly ['create', 'read'] }\n * type B = { posts: readonly ['update']; admin: readonly ['access'] }\n * type M = MergeStatements<A, B>\n * // → { posts: readonly ('create' | 'read' | 'update')[]; admin: readonly ['access'] }\n * ```\n */\ntype MergeStatements<\n A extends Statements,\n B extends Partial<Record<string, readonly string[]>>\n> = {\n [K in keyof A | keyof B]: K extends keyof A\n ? K extends keyof B\n ? readonly (A[K][number] | NonNullable<B[K]>[number])[]\n : A[K]\n : K extends keyof B\n ? NonNullable<B[K]>\n : never\n} & {}\n\n/**\n * Extend an existing role with additional permissions.\n *\n * Duplicate resource keys are merged (actions are unioned), not overwritten.\n * Better Auth has no built-in role inheritance — use this to compose roles.\n *\n * @example\n * ```typescript\n * const adminRole = ac.newRole({ posts: ['create', 'read', 'update', 'delete'] })\n * const superAdminRole = extendRole(ac, adminRole, { users: ['ban', 'delete'] })\n * // superAdminRole has both posts and users permissions\n *\n * // Duplicate keys are merged, not overwritten:\n * const editorRole = extendRole(ac, userRole, { posts: ['update'] })\n * // if userRole had posts: ['create', 'read'], editorRole has posts: ['create', 'read', 'update']\n * ```\n */\nexport function extendRole<\n TParent extends Statements,\n TExtra extends Partial<Record<string, readonly string[]>>\n>(\n ac: AccessControl<TParent>,\n parent: Role<TParent>,\n extra: TExtra\n): Role<MergeStatements<TParent, TExtra>> {\n const merged: Record<string, string[]> = {}\n\n for (const [key, actions] of Object.entries(parent.statements)) {\n merged[key] = [...(actions as string[])]\n }\n\n for (const [key, actions] of Object.entries(extra)) {\n if (!actions) continue\n if (key in merged) {\n merged[key] = [...new Set([...merged[key], ...actions])]\n } else {\n merged[key] = [...actions]\n }\n }\n\n return ac.newRole(merged) as Role<MergeStatements<TParent, TExtra>>\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,oBAGd,QAG2C;CAC3C,MAAM,KAAKA,sBAAW,OAAO,SAAS;CAItC,OAAO;EAAE;EAAI,OAHC,OAAO,YACnB,OAAO,QAAQ,OAAO,KAAK,CAAC,CAAC,KAAK,CAAC,MAAM,WAAW,CAAC,MAAM,GAAG,QAAQ,KAA8B,CAAC,CAAC,CAEvF;CAAE;AACrB;;;;;;;;;;;;;;;;;;;;ACCA,SAAgB,WAId,IACA,QACA,OACwC;CACxC,MAAM,SAAmC,CAAC;CAE1C,KAAK,MAAM,CAAC,KAAK,YAAY,OAAO,QAAQ,OAAO,UAAU,GAC3D,OAAO,OAAO,CAAC,GAAI,OAAoB;CAGzC,KAAK,MAAM,CAAC,KAAK,YAAY,OAAO,QAAQ,KAAK,GAAG;EAClD,IAAI,CAAC,SAAS;EACd,IAAI,OAAO,QACT,OAAO,OAAO,CAAC,mBAAG,IAAI,IAAI,CAAC,GAAG,OAAO,MAAM,GAAG,OAAO,CAAC,CAAC;OAEvD,OAAO,OAAO,CAAC,GAAG,OAAO;CAE7B;CAEA,OAAO,GAAG,QAAQ,MAAM;AAC1B"}
|
|
@@ -1,6 +1,24 @@
|
|
|
1
|
-
import { n as AC_TOKENS, t as __decorateParam } from "./decorateParam-
|
|
2
|
-
import { t as __decorate } from "./decorate-
|
|
3
|
-
import { DI_TOKENS, Request, inject } from "stratal/di";
|
|
1
|
+
import { n as AC_TOKENS, t as __decorateParam } from "./decorateParam-xwTkq9gO.mjs";
|
|
2
|
+
import { t as __decorate } from "./decorate-RQD1h28J.mjs";
|
|
3
|
+
import { DI_TOKENS, Request, Transient, inject } from "stratal/di";
|
|
4
|
+
//#region src/access-control/access-share.middleware.ts
|
|
5
|
+
let AccessShareMiddleware = class AccessShareMiddleware {
|
|
6
|
+
access;
|
|
7
|
+
constructor(access) {
|
|
8
|
+
this.access = access;
|
|
9
|
+
}
|
|
10
|
+
async handle(ctx, next) {
|
|
11
|
+
const access = this.access;
|
|
12
|
+
const inertia = ctx;
|
|
13
|
+
if (access && typeof inertia.share === "function" && typeof inertia.always === "function") inertia.share("access", inertia.always(() => ({
|
|
14
|
+
roles: access.getCurrentUserRoles(),
|
|
15
|
+
permissions: access.getCurrentUserPermissions()
|
|
16
|
+
})));
|
|
17
|
+
await next();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
AccessShareMiddleware = __decorate([Transient(), __decorateParam(0, inject(AC_TOKENS.AccessService, { isOptional: true }))], AccessShareMiddleware);
|
|
21
|
+
//#endregion
|
|
4
22
|
//#region src/access-control/plugin.ts
|
|
5
23
|
/**
|
|
6
24
|
* Creates the Stratal access control Better Auth plugin.
|
|
@@ -23,6 +41,12 @@ function createStratalAcPlugin(_options) {
|
|
|
23
41
|
};
|
|
24
42
|
}
|
|
25
43
|
//#endregion
|
|
44
|
+
//#region src/auth/auth.tokens.ts
|
|
45
|
+
/** Token for AuthService - core authentication service */
|
|
46
|
+
const AUTH_SERVICE = Symbol.for("stratal:auth:service");
|
|
47
|
+
/** Token for Better Auth options configuration */
|
|
48
|
+
const AUTH_OPTIONS = Symbol.for("stratal:auth:options");
|
|
49
|
+
//#endregion
|
|
26
50
|
//#region src/access-control/services/access.service.ts
|
|
27
51
|
function parseRoles(role) {
|
|
28
52
|
if (!role) return [];
|
|
@@ -30,11 +54,11 @@ function parseRoles(role) {
|
|
|
30
54
|
}
|
|
31
55
|
let AccessService = class AccessService {
|
|
32
56
|
authContext;
|
|
33
|
-
|
|
57
|
+
authService;
|
|
34
58
|
options;
|
|
35
|
-
constructor(authContext,
|
|
59
|
+
constructor(authContext, authService, options) {
|
|
36
60
|
this.authContext = authContext;
|
|
37
|
-
this.
|
|
61
|
+
this.authService = authService;
|
|
38
62
|
this.options = options;
|
|
39
63
|
}
|
|
40
64
|
/**
|
|
@@ -48,10 +72,7 @@ let AccessService = class AccessService {
|
|
|
48
72
|
const roles = this.authContext.getRoles();
|
|
49
73
|
if (roles.length > 0) return roles;
|
|
50
74
|
}
|
|
51
|
-
return parseRoles((await this.
|
|
52
|
-
where: { id: userId },
|
|
53
|
-
select: { role: true }
|
|
54
|
-
}))?.role);
|
|
75
|
+
return parseRoles((await (await this.authService.auth.$context).internalAdapter.findUserById(userId))?.role);
|
|
55
76
|
}
|
|
56
77
|
/**
|
|
57
78
|
* Assign one or more roles to a user.
|
|
@@ -60,10 +81,7 @@ let AccessService = class AccessService {
|
|
|
60
81
|
*/
|
|
61
82
|
async setUserRole(userId, role) {
|
|
62
83
|
const roleStr = Array.isArray(role) ? role.join(",") : role;
|
|
63
|
-
await this.
|
|
64
|
-
where: { id: userId },
|
|
65
|
-
data: { role: roleStr }
|
|
66
|
-
});
|
|
84
|
+
await (await this.authService.auth.$context).internalAdapter.updateUser(userId, { role: roleStr });
|
|
67
85
|
}
|
|
68
86
|
/**
|
|
69
87
|
* Check if a user has the required permissions.
|
|
@@ -133,10 +151,10 @@ let AccessService = class AccessService {
|
|
|
133
151
|
AccessService = __decorate([
|
|
134
152
|
Request(AC_TOKENS.AccessService),
|
|
135
153
|
__decorateParam(0, inject(DI_TOKENS.AuthContext)),
|
|
136
|
-
__decorateParam(1, inject(
|
|
154
|
+
__decorateParam(1, inject(AUTH_SERVICE)),
|
|
137
155
|
__decorateParam(2, inject(AC_TOKENS.Options))
|
|
138
156
|
], AccessService);
|
|
139
157
|
//#endregion
|
|
140
|
-
export { createStratalAcPlugin as n, AccessService as t };
|
|
158
|
+
export { AccessShareMiddleware as a, createStratalAcPlugin as i, AUTH_OPTIONS as n, AUTH_SERVICE as r, AccessService as t };
|
|
141
159
|
|
|
142
|
-
//# sourceMappingURL=access.service-
|
|
160
|
+
//# sourceMappingURL=access.service-BjmnWBEo.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access.service-BjmnWBEo.mjs","names":[],"sources":["../src/access-control/access-share.middleware.ts","../src/access-control/plugin.ts","../src/auth/auth.tokens.ts","../src/access-control/services/access.service.ts"],"sourcesContent":["import { inject, Transient } from 'stratal/di'\nimport type { Middleware, Next, RouterContext } from 'stratal/router'\nimport type { AccessService } from './services/access.service'\nimport { AC_TOKENS } from './tokens'\n\n/**\n * The Inertia methods this middleware needs, declared structurally.\n *\n * `@stratal/framework` must not depend on `@stratal/inertia` (the dependency\n * runs the other way at the type level, via the generated registry), so the\n * two optional context methods are described here and probed at runtime\n * instead of pulled in through a `/// <reference types=\"...\" />`.\n */\ninterface MaybeInertiaContext {\n share?: (key: string, value: unknown) => void\n always?: <T>(callback: () => T) => unknown\n}\n\n/**\n * Shares the current user's roles and merged permissions as the `access` prop\n * on every Inertia page rendered during the request, so the client can gate\n * rendering with `<Can>` / `<HasRole>` from `@stratal/inertia/react/access`.\n *\n * Both values come from `AuthContext` (populated by\n * `SessionVerificationMiddleware`) — no database hit and no I/O, which is why\n * this is registered globally by `AuthModule` rather than being opt-in the way\n * `FeatureFlagShareMiddleware` is.\n *\n * Inert unless access control is configured and Inertia is installed. Not\n * restricted to `GET`: an Inertia render can legally happen on any method —\n * e.g. `return ctx.inertia('Posts/Create', {...})` from a `POST` handler\n * re-rendering after a validation failure — and every `<Can>` / `useCan` on\n * that page needs `access` just as much as it would on a full `GET` visit.\n *\n * Wrapped in `ctx.always()` so partial reloads never drop the prop; the client\n * treats an absent `access` prop as a wiring error, not as \"denied\".\n */\n@Transient()\nexport class AccessShareMiddleware implements Middleware {\n constructor(\n @inject(AC_TOKENS.AccessService, { isOptional: true })\n private readonly access?: AccessService,\n ) { }\n\n async handle(ctx: RouterContext, next: Next): Promise<void> {\n const access = this.access\n const inertia = ctx as RouterContext & MaybeInertiaContext\n\n if (\n access\n && typeof inertia.share === 'function'\n && typeof inertia.always === 'function'\n ) {\n inertia.share('access', inertia.always(() => ({\n roles: access.getCurrentUserRoles(),\n permissions: access.getCurrentUserPermissions(),\n })))\n }\n\n await next()\n }\n}\n","import type { BetterAuthPlugin } from 'better-auth'\nimport type { AccessControlOptions } from './types'\n\n/**\n * Creates the Stratal access control Better Auth plugin.\n *\n * Ensures the `user.role` schema field exists.\n * No endpoints are added — all permission logic lives in AccessService.\n *\n * Auto-added to Better Auth options when `accessControl` is provided to\n * `AuthModule.forRootAsync()`. Users never call this directly.\n */\nexport function createStratalAcPlugin(_options: AccessControlOptions): BetterAuthPlugin {\n return {\n id: 'stratal-ac',\n schema: {\n user: {\n fields: {\n role: {\n type: 'string',\n required: false,\n input: false,\n defaultValue: 'user',\n },\n },\n },\n },\n }\n}\n","/** Token for AuthService - core authentication service */\nexport const AUTH_SERVICE = Symbol.for('stratal:auth:service')\n\n/** Token for Better Auth options configuration */\nexport const AUTH_OPTIONS = Symbol.for('stratal:auth:options')\n","import { DI_TOKENS, inject, Request } from 'stratal/di'\nimport { AUTH_SERVICE } from '../../auth/auth.tokens'\nimport type { AuthService } from '../../auth/services/auth.service'\nimport type { AuthContext } from '../../context/auth-context'\nimport { AC_TOKENS } from '../tokens'\nimport type { AccessControlOptions } from '../types'\n\nfunction parseRoles(role: string | null | undefined): string[] {\n if (!role) return []\n return role.split(',').map(r => r.trim()).filter(Boolean)\n}\n\n/**\n * AccessService\n *\n * Request-scoped service for role and permission management.\n *\n * Roles for the current user are read from AuthContext (populated by\n * SessionVerificationMiddleware — no DB hit). Other users are resolved\n * from the database.\n *\n * Permission checks use Better Auth's `role.authorize()` locally with\n * OR logic — access is granted if any of the user's roles allows it.\n *\n * @example\n * ```typescript\n * // Check current user\n * await accessService.currentUserHasPermission({ posts: ['update'] })\n *\n * // Check arbitrary user (e.g. from an admin action)\n * await accessService.hasPermission(userId, { admin: ['access'] })\n *\n * // Assign a role\n * await accessService.setUserRole(userId, 'admin')\n *\n * // Assign multiple roles\n * await accessService.setUserRole(userId, ['editor', 'reviewer'])\n * ```\n */\n@Request(AC_TOKENS.AccessService)\nexport class AccessService {\n constructor(\n @inject(DI_TOKENS.AuthContext)\n private readonly authContext: AuthContext,\n @inject(AUTH_SERVICE)\n private readonly authService: AuthService,\n @inject(AC_TOKENS.Options)\n private readonly options: AccessControlOptions\n ) { }\n\n /**\n * Get all roles for a user.\n *\n * Uses AuthContext for the current user (no DB hit).\n * Falls back to DB for other users.\n */\n async getUserRoles(userId: string): Promise<string[]> {\n if (userId === this.authContext.getUserId()) {\n const roles = this.authContext.getRoles()\n if (roles.length > 0) return roles\n }\n const ctx = await this.authService.auth.$context\n const user = await ctx.internalAdapter.findUserById(userId)\n return parseRoles((user as { role?: string | null } | null)?.role)\n }\n\n /**\n * Assign one or more roles to a user.\n *\n * Multiple roles are stored as a comma-separated string in `user.role`.\n */\n async setUserRole(userId: string, role: string | string[]): Promise<void> {\n const roleStr = Array.isArray(role) ? role.join(',') : role\n const ctx = await this.authService.auth.$context\n await ctx.internalAdapter.updateUser(\n userId,\n { role: roleStr } as Parameters<typeof ctx.internalAdapter.updateUser>[1]\n )\n }\n\n /**\n * Check if a user has the required permissions.\n *\n * Returns true if any of the user's roles grants all of the requested permissions.\n */\n async hasPermission(userId: string, permissions: Record<string, string[]>): Promise<boolean> {\n const roles = await this.getUserRoles(userId)\n return this.checkPermissions(roles, permissions)\n }\n\n /**\n * Get the merged permission set for a user across all their roles.\n * Useful for sending to the frontend.\n */\n async getPermissionsForUser(userId: string): Promise<Record<string, string[]>> {\n const roles = await this.getUserRoles(userId)\n return this.mergePermissions(roles)\n }\n\n /**\n * Get all roles for the currently authenticated user.\n * Reads from AuthContext — no DB hit.\n */\n getCurrentUserRoles(): string[] {\n return this.authContext.getRoles()\n }\n\n /**\n * Check if the currently authenticated user has the required permissions.\n * Reads roles from AuthContext — no DB hit.\n */\n currentUserHasPermission(permissions: Record<string, string[]>): boolean {\n const roles = this.authContext.getRoles()\n if (roles.length === 0) return false\n return this.checkPermissions(roles, permissions)\n }\n\n /**\n * Get merged permissions for the currently authenticated user.\n * Reads roles from AuthContext — no DB hit.\n */\n getCurrentUserPermissions(): Record<string, string[]> {\n const roles = this.authContext.getRoles()\n return this.mergePermissions(roles)\n }\n\n private checkPermissions(roles: string[], permissions: Record<string, string[]>): boolean {\n return roles.some(roleName => {\n const roleObj = this.options.roles[roleName]\n if (!roleObj) return false\n\n const specific: Record<string, string[]> = {}\n\n for (const [resource, actions] of Object.entries(permissions)) {\n if (actions.includes('*')) {\n // Wildcard: role must have at least one action defined for this resource\n const roleActions = (roleObj.statements as Record<string, readonly string[]>)[resource]\n if (!roleActions?.length) return false\n } else {\n specific[resource] = actions\n }\n }\n\n return Object.keys(specific).length === 0 || roleObj.authorize(specific).success\n })\n }\n\n private mergePermissions(roles: string[]): Record<string, string[]> {\n const result: Record<string, string[]> = {}\n for (const roleName of roles) {\n const roleObj = this.options.roles[roleName]\n if (!roleObj) continue\n for (const [resource, actions] of Object.entries(roleObj.statements)) {\n result[resource] ??= []\n for (const action of actions as string[]) {\n if (!result[resource].includes(action)) {\n result[resource].push(action)\n }\n }\n }\n }\n return result\n }\n}\n"],"mappings":"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"}
|