@fonderie/permissions 1.0.0 → 1.0.2
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/brain/outcomes.md +28 -0
- package/brain/signatures.md +72 -0
- package/dist/index.cjs +13 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -9
- package/dist/index.d.ts +4 -9
- package/dist/index.js +1 -6
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +12 -11
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.d.cts +1 -2
- package/dist/middlewares/index.d.ts +1 -2
- package/dist/middlewares/index.js +1 -0
- package/dist/middlewares/index.js.map +1 -1
- package/dist/migrations/index.d.ts +3 -0
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +3 -2
- package/dist/types.d.ts +3 -2
- package/package.json +8 -7
- package/dist/migrations/sql/001_permissions.sql +0 -17
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!-- GENERATED — do not edit. Regenerate with: npm run docs:signatures -->
|
|
2
|
+
|
|
3
|
+
# @fonderie/permissions — outcomes
|
|
4
|
+
|
|
5
|
+
What this package does to a running app: tables its migrations create,
|
|
6
|
+
rows it seeds, routes it registers. Generated from the migration SQL and
|
|
7
|
+
route tables in source — trust this file instead of reading `dist/` or
|
|
8
|
+
downloading tarballs.
|
|
9
|
+
|
|
10
|
+
## Database tables (after all migrations)
|
|
11
|
+
|
|
12
|
+
### `fonderie_role_permissions`
|
|
13
|
+
|
|
14
|
+
```sql
|
|
15
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
|
|
16
|
+
role_id UUID NOT NULL
|
|
17
|
+
workspace_id UUID
|
|
18
|
+
permission_key TEXT NOT NULL
|
|
19
|
+
can_create BOOLEAN NOT NULL DEFAULT false
|
|
20
|
+
can_read BOOLEAN NOT NULL DEFAULT false
|
|
21
|
+
can_update BOOLEAN NOT NULL DEFAULT false
|
|
22
|
+
can_delete BOOLEAN NOT NULL DEFAULT false
|
|
23
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
24
|
+
-- UNIQUE (role_id, permission_key)
|
|
25
|
+
-- INDEX idx_frp_permission_key (permission_key)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Raw SQL ships in `node_modules/@fonderie/permissions/dist/migrations/sql/` — read it there if you must; never download tarballs.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<!-- GENERATED — do not edit. Regenerate with: npm run docs:signatures -->
|
|
2
|
+
|
|
3
|
+
# @fonderie/permissions — signatures
|
|
4
|
+
|
|
5
|
+
## @fonderie/permissions
|
|
6
|
+
|
|
7
|
+
Subpath exports: `@fonderie/permissions/config`, `@fonderie/permissions/types`, `@fonderie/permissions/middleware`, `@fonderie/permissions/migrations`
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
type Operation = 'create' | 'read' | 'update' | 'delete';
|
|
11
|
+
|
|
12
|
+
type PermissionKey = string;
|
|
13
|
+
|
|
14
|
+
interface IRole {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
isSystem: boolean;
|
|
18
|
+
workspaceId: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface IPermission {
|
|
22
|
+
permissionKey: PermissionKey;
|
|
23
|
+
canCreate: boolean;
|
|
24
|
+
canRead: boolean;
|
|
25
|
+
canUpdate: boolean;
|
|
26
|
+
canDelete: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface IMembership {
|
|
30
|
+
userId: string;
|
|
31
|
+
workspaceId: string;
|
|
32
|
+
roleId: string;
|
|
33
|
+
roleName: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface IRoleWithPermissions extends IRole {
|
|
37
|
+
permissions: IPermission[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
new PermissionsModule(store: IStoreAdapter, config?: IPermissionsConfig): PermissionsModule
|
|
41
|
+
.engine: PermissionsEngine
|
|
42
|
+
.name: "@fonderie/permissions"
|
|
43
|
+
.deps: string[]
|
|
44
|
+
.install(app: IFonderieApp): void
|
|
45
|
+
|
|
46
|
+
new PermissionsEngine(store: IStoreAdapter, config?: IPermissionsConfig): PermissionsEngine
|
|
47
|
+
.getMembership(userId: string, workspaceId: string): Promise<IMembership | null>
|
|
48
|
+
.can(userId: string, operation: Operation, permissionKey: string, workspaceId: string): Promise<boolean>
|
|
49
|
+
.assert(userId: string, operation: Operation, permissionKey: string, workspaceId: string): Promise<void>
|
|
50
|
+
.canAll(userId: string, checks: { operation: Operation; permissionKey: string; }[], workspaceId: string): Promise<boolean>
|
|
51
|
+
.canAny(userId: string, checks: { operation: Operation; permissionKey: string; }[], workspaceId: string): Promise<boolean>
|
|
52
|
+
|
|
53
|
+
new PermissionDeniedError(operation: string, permissionKey: string): PermissionDeniedError
|
|
54
|
+
.status: 403
|
|
55
|
+
.name: string
|
|
56
|
+
.message: string
|
|
57
|
+
.stack: string
|
|
58
|
+
.cause: unknown
|
|
59
|
+
|
|
60
|
+
interface IPermissionsConfig {
|
|
61
|
+
wildcards?: boolean;
|
|
62
|
+
superRole?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const OPERATIONS: { readonly CREATE: "create"; readonly READ: "read"; readonly UPDATE: "update"; readonly DELETE: "delete"; }
|
|
66
|
+
|
|
67
|
+
const PERMISSION_COLUMN: { create: string; read: string; update: string; delete: string; }
|
|
68
|
+
|
|
69
|
+
function requireRole(roleName: string | string[], store: IStoreAdapter): Middleware
|
|
70
|
+
|
|
71
|
+
function requirePermission(operation: Operation, permissionKey: string): Middleware
|
|
72
|
+
```
|
package/dist/index.cjs
CHANGED
|
@@ -20,7 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
-
OPERATIONS: () => OPERATIONS,
|
|
23
|
+
OPERATIONS: () => import_core.OPERATIONS,
|
|
24
24
|
PERMISSION_COLUMN: () => PERMISSION_COLUMN,
|
|
25
25
|
PermissionDeniedError: () => PermissionDeniedError,
|
|
26
26
|
PermissionsEngine: () => PermissionsEngine,
|
|
@@ -69,12 +69,7 @@ async function hasRole(userId, workspaceId, roleName, store) {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// src/constants.ts
|
|
72
|
-
var
|
|
73
|
-
CREATE: "create",
|
|
74
|
-
READ: "read",
|
|
75
|
-
UPDATE: "update",
|
|
76
|
-
DELETE: "delete"
|
|
77
|
-
};
|
|
72
|
+
var import_core = require("@fonderie/core");
|
|
78
73
|
var PERMISSION_COLUMN = {
|
|
79
74
|
create: "can_create",
|
|
80
75
|
read: "can_read",
|
|
@@ -176,24 +171,24 @@ var PermissionsModule = class {
|
|
|
176
171
|
};
|
|
177
172
|
|
|
178
173
|
// src/middlewares/require-role.ts
|
|
179
|
-
var
|
|
174
|
+
var import_core2 = require("@fonderie/core");
|
|
180
175
|
function makeHandler(roleName, store) {
|
|
181
176
|
const allowed = Array.isArray(roleName) ? roleName : [roleName];
|
|
182
177
|
return async (ctx, next) => {
|
|
183
178
|
if (!ctx.user) {
|
|
184
|
-
return (0,
|
|
179
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
185
180
|
}
|
|
186
181
|
const engine = ctx.meta[PERMISSIONS_ENGINE_KEY];
|
|
187
182
|
if (!(engine instanceof PermissionsEngine)) {
|
|
188
|
-
return (0,
|
|
183
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.SERVER_ERROR, "SERVER_ERROR", "Permissions module not installed");
|
|
189
184
|
}
|
|
190
185
|
const workspaceId = ctx.workspace?.id ?? ctx.meta["params"]?.["workspaceId"];
|
|
191
186
|
if (!workspaceId) {
|
|
192
|
-
return (0,
|
|
187
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.BAD_REQUEST, "WORKSPACE_REQUIRED", "Workspace context required");
|
|
193
188
|
}
|
|
194
189
|
const membership = await getMembership(ctx.user.id, workspaceId, store);
|
|
195
190
|
if (!membership || !allowed.includes(membership.roleName)) {
|
|
196
|
-
return (0,
|
|
191
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.FORBIDDEN, "FORBIDDEN", "Insufficient role");
|
|
197
192
|
}
|
|
198
193
|
return next();
|
|
199
194
|
};
|
|
@@ -205,24 +200,24 @@ function requireRole(roleName, store, ctx, next) {
|
|
|
205
200
|
}
|
|
206
201
|
|
|
207
202
|
// src/middlewares/require-permission.ts
|
|
208
|
-
var
|
|
203
|
+
var import_core3 = require("@fonderie/core");
|
|
209
204
|
function makeHandler2(operation, permissionKey) {
|
|
210
205
|
return async (ctx, next) => {
|
|
211
206
|
if (!ctx.user) {
|
|
212
|
-
return (0,
|
|
207
|
+
return (0, import_core3.setApiResponse)(import_core3.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
213
208
|
}
|
|
214
209
|
const engine = ctx.meta[PERMISSIONS_ENGINE_KEY];
|
|
215
210
|
if (!(engine instanceof PermissionsEngine)) {
|
|
216
|
-
return (0,
|
|
211
|
+
return (0, import_core3.setApiResponse)(import_core3.HTTP.SERVER_ERROR, "SERVER_ERROR", "Permissions module not installed");
|
|
217
212
|
}
|
|
218
213
|
const workspaceId = resolveWorkspaceId(ctx);
|
|
219
214
|
if (!workspaceId) {
|
|
220
|
-
return (0,
|
|
215
|
+
return (0, import_core3.setApiResponse)(import_core3.HTTP.BAD_REQUEST, "WORKSPACE_REQUIRED", "Workspace context required");
|
|
221
216
|
}
|
|
222
217
|
const allowed = await engine.can(ctx.user.id, operation, permissionKey, workspaceId);
|
|
223
218
|
if (!allowed) {
|
|
224
|
-
return (0,
|
|
225
|
-
|
|
219
|
+
return (0, import_core3.setApiResponse)(
|
|
220
|
+
import_core3.HTTP.FORBIDDEN,
|
|
226
221
|
"FORBIDDEN",
|
|
227
222
|
`Permission denied: ${operation}:${permissionKey}`
|
|
228
223
|
);
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/services/membership.ts","../src/constants.ts","../src/services/permissions.ts","../src/engine.ts","../src/module.ts","../src/middlewares/require-role.ts","../src/middlewares/require-permission.ts"],"sourcesContent":["export type {\n\tOperation,\n\tPermissionKey,\n\tIRole,\n\tIPermission,\n\tIMembership,\n\tIRoleWithPermissions,\n} from './types';\nexport { PermissionsModule } from './module';\nexport { PermissionsEngine, PermissionDeniedError } from './engine';\nexport type { IPermissionsConfig } from './config';\n\nexport { OPERATIONS, PERMISSION_COLUMN } from './constants';\nexport { requireRole } from './middlewares/require-role';\nexport { requirePermission } from './middlewares/require-permission';\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IMembership } from '../types';\n\nexport async function getMembership(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<IMembership | null> {\n\tconst [row] = await store.query<{\n\t\tuser_id: string;\n\t\tworkspace_id: string;\n\t\trole_id: string;\n\t\trole_name: string;\n\t}>(\n\t\t`SELECT ruw.user_id, ruw.workspace_id, ruw.role_id, r.name AS role_name\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON r.id = ruw.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t LIMIT 1`,\n\t\t[userId, workspaceId],\n\t);\n\n\tif (!row) return null;\n\n\treturn {\n\t\tuserId: row.user_id,\n\t\tworkspaceId: row.workspace_id,\n\t\troleId: row.role_id,\n\t\troleName: row.role_name,\n\t};\n}\n\nexport async function hasRole(\n\tuserId: string,\n\tworkspaceId: string,\n\troleName: string,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst [row] = await store.query<{ exists: boolean }>(\n\t\t`SELECT EXISTS (\n\t\t SELECT 1\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON r.id = ruw.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND r.name = $3\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS exists`,\n\t\t[userId, workspaceId, roleName],\n\t);\n\n\treturn row?.exists ?? false;\n}\n","import type { Operation } from './types';\n\nexport const OPERATIONS = {\n\tCREATE: 'create',\n\tREAD: 'read',\n\tUPDATE: 'update',\n\tDELETE: 'delete',\n} as const satisfies Record<string, Operation>;\n\nexport const PERMISSION_COLUMN: Record<Operation, string> = {\n\tcreate: 'can_create',\n\tread: 'can_read',\n\tupdate: 'can_update',\n\tdelete: 'can_delete',\n};\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation } from '../types';\nimport { PERMISSION_COLUMN } from '../constants';\n\nexport async function checkPermission(\n\tuserId: string,\n\tworkspaceId: string,\n\tpermissionKey: string,\n\toperation: Operation,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst col = PERMISSION_COLUMN[operation];\n\n\tconst [row] = await store.query<{ has_permission: boolean }>(\n\t\t`SELECT BOOL_OR(rp.${col}) AS has_permission\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON ruw.role_id = r.id\n\t\t JOIN fonderie_role_permissions rp ON r.id = rp.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND rp.workspace_id = $2\n\t\t AND rp.permission_key = $3\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t AND (r.workspace_id = $2 OR r.is_system = true)\n\t\t GROUP BY ruw.user_id, ruw.workspace_id`,\n\t\t[userId, workspaceId, permissionKey],\n\t);\n\n\treturn row?.has_permission ?? false;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation } from './types';\nimport type { IPermissionsConfig } from './config';\nimport { getMembership, hasRole } from './services/membership';\nimport { checkPermission } from './services/permissions';\n\nexport class PermissionsEngine {\n\tprivate superRole: string;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tconfig: IPermissionsConfig = {},\n\t) {\n\t\tthis.superRole = config.superRole ?? 'owner';\n\t}\n\n\tasync getMembership(userId: string, workspaceId: string) {\n\t\treturn getMembership(userId, workspaceId, this.store);\n\t}\n\n\tasync can(\n\t\tuserId: string,\n\t\toperation: Operation,\n\t\tpermissionKey: string,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\treturn checkPermission(userId, workspaceId, permissionKey, operation, this.store);\n\t}\n\n\tasync assert(\n\t\tuserId: string,\n\t\toperation: Operation,\n\t\tpermissionKey: string,\n\t\tworkspaceId: string,\n\t): Promise<void> {\n\t\tconst allowed = await this.can(userId, operation, permissionKey, workspaceId);\n\t\tif (!allowed) {\n\t\t\tthrow new PermissionDeniedError(operation, permissionKey);\n\t\t}\n\t}\n\n\tasync canAll(\n\t\tuserId: string,\n\t\tchecks: Array<{ operation: Operation; permissionKey: string }>,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\tconst results = await Promise.all(\n\t\t\tchecks.map((c) =>\n\t\t\t\tcheckPermission(userId, workspaceId, c.permissionKey, c.operation, this.store),\n\t\t\t),\n\t\t);\n\t\treturn results.every(Boolean);\n\t}\n\n\tasync canAny(\n\t\tuserId: string,\n\t\tchecks: Array<{ operation: Operation; permissionKey: string }>,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\tconst results = await Promise.all(\n\t\t\tchecks.map((c) =>\n\t\t\t\tcheckPermission(userId, workspaceId, c.permissionKey, c.operation, this.store),\n\t\t\t),\n\t\t);\n\t\treturn results.some(Boolean);\n\t}\n}\n\nexport class PermissionDeniedError extends Error {\n\treadonly status = 403;\n\n\tconstructor(operation: string, permissionKey: string) {\n\t\tsuper(`Permission denied: ${operation}:${permissionKey}`);\n\t\tthis.name = 'PermissionDeniedError';\n\t}\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IPermissionsConfig } from './config';\nimport { PermissionsEngine } from './engine';\n\nexport const PERMISSIONS_ENGINE_KEY = 'fonderie.permissions.engine';\n\nexport class PermissionsModule implements IFonderieModule {\n\treadonly engine: PermissionsEngine;\n\treadonly name = '@fonderie/permissions';\n\treadonly deps = ['@fonderie/auth'];\n\n\tconstructor(store: IStoreAdapter, config: IPermissionsConfig = {}) {\n\t\tthis.engine = new PermissionsEngine(store, config);\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\tconst engine = this.engine;\n\n\t\t// Inject engine into every request context\n\t\t// requirePermission() reads it from here — no manual passing needed\n\t\tapp.use(async (ctx, next) => {\n\t\t\tctx.meta[PERMISSIONS_ENGINE_KEY] = engine;\n\t\t\treturn next();\n\t\t});\n\t}\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { PermissionsEngine } from '../engine';\nimport { PERMISSIONS_ENGINE_KEY } from '../module';\nimport { getMembership } from '../services/membership';\n\nfunction makeHandler(roleName: string | string[], store: IStoreAdapter): Middleware {\n\tconst allowed = Array.isArray(roleName) ? roleName : [roleName];\n\n\treturn async (ctx, next) => {\n\t\tif (!ctx.user) {\n\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t}\n\n\t\tconst engine = ctx.meta[PERMISSIONS_ENGINE_KEY];\n\t\tif (!(engine instanceof PermissionsEngine)) {\n\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Permissions module not installed');\n\t\t}\n\n\t\tconst workspaceId =\n\t\t\tctx.workspace?.id ??\n\t\t\t(ctx.meta['params'] as Record<string, string> | undefined)?.['workspaceId'];\n\n\t\tif (!workspaceId) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'WORKSPACE_REQUIRED', 'Workspace context required');\n\t\t}\n\n\t\tconst membership = await getMembership(ctx.user.id, workspaceId, store);\n\t\tif (!membership || !allowed.includes(membership.roleName)) {\n\t\t\treturn setApiResponse(HTTP.FORBIDDEN, 'FORBIDDEN', 'Insufficient role');\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\nexport function requireRole(roleName: string | string[], store: IStoreAdapter): Middleware;\nexport function requireRole(\n\troleName: string | string[],\n\tstore: IStoreAdapter,\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n): Promise<Response>;\nexport function requireRole(\n\troleName: string | string[],\n\tstore: IStoreAdapter,\n\tctx?: IFonderieContext,\n\tnext?: () => Promise<Response>,\n): Middleware | Promise<Response> {\n\tconst handler = makeHandler(roleName, store);\n\tif (ctx !== undefined && next !== undefined) return handler(ctx, next);\n\treturn handler;\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\n\nimport type { Operation } from '../types';\nimport { PermissionsEngine } from '../engine';\nimport { PERMISSIONS_ENGINE_KEY } from '../module';\n\nfunction makeHandler(operation: Operation, permissionKey: string): Middleware {\n\treturn async (ctx, next) => {\n\t\tif (!ctx.user) {\n\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t}\n\n\t\tconst engine = ctx.meta[PERMISSIONS_ENGINE_KEY];\n\t\tif (!(engine instanceof PermissionsEngine)) {\n\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Permissions module not installed');\n\t\t}\n\n\t\tconst workspaceId = resolveWorkspaceId(ctx);\n\t\tif (!workspaceId) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'WORKSPACE_REQUIRED', 'Workspace context required');\n\t\t}\n\n\t\tconst allowed = await engine.can(ctx.user.id, operation, permissionKey, workspaceId);\n\t\tif (!allowed) {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t'FORBIDDEN',\n\t\t\t\t`Permission denied: ${operation}:${permissionKey}`,\n\t\t\t);\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\nexport function requirePermission(operation: Operation, permissionKey: string): Middleware;\nexport function requirePermission(\n\toperation: Operation,\n\tpermissionKey: string,\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n): Promise<Response>;\nexport function requirePermission(\n\toperation: Operation,\n\tpermissionKey: string,\n\tctx?: IFonderieContext,\n\tnext?: () => Promise<Response>,\n): Middleware | Promise<Response> {\n\tconst handler = makeHandler(operation, permissionKey);\n\tif (ctx !== undefined && next !== undefined) return handler(ctx, next);\n\treturn handler;\n}\n\nfunction resolveWorkspaceId(ctx: {\n\tworkspace?: { id: string } | null;\n\tmeta: Record<string, unknown>;\n}): string | null {\n\tif (ctx.workspace?.id) return ctx.workspace.id;\n\n\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\treturn params?.['workspaceId'] ?? null;\n}\n"],"mappings":"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","names":["import_core","makeHandler"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/services/membership.ts","../src/constants.ts","../src/services/permissions.ts","../src/engine.ts","../src/module.ts","../src/middlewares/require-role.ts","../src/middlewares/require-permission.ts"],"sourcesContent":["export type {\n\tOperation,\n\tPermissionKey,\n\tIRole,\n\tIPermission,\n\tIMembership,\n\tIRoleWithPermissions,\n} from './types';\nexport { PermissionsModule } from './module';\nexport { PermissionsEngine, PermissionDeniedError } from './engine';\nexport type { IPermissionsConfig } from './config';\n\nexport { OPERATIONS, PERMISSION_COLUMN } from './constants';\nexport { requireRole } from './middlewares/require-role';\nexport { requirePermission } from './middlewares/require-permission';\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IMembership } from '../types';\n\nexport async function getMembership(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<IMembership | null> {\n\tconst [row] = await store.query<{\n\t\tuser_id: string;\n\t\tworkspace_id: string;\n\t\trole_id: string;\n\t\trole_name: string;\n\t}>(\n\t\t`SELECT ruw.user_id, ruw.workspace_id, ruw.role_id, r.name AS role_name\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON r.id = ruw.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t LIMIT 1`,\n\t\t[userId, workspaceId],\n\t);\n\n\tif (!row) return null;\n\n\treturn {\n\t\tuserId: row.user_id,\n\t\tworkspaceId: row.workspace_id,\n\t\troleId: row.role_id,\n\t\troleName: row.role_name,\n\t};\n}\n\nexport async function hasRole(\n\tuserId: string,\n\tworkspaceId: string,\n\troleName: string,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst [row] = await store.query<{ exists: boolean }>(\n\t\t`SELECT EXISTS (\n\t\t SELECT 1\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON r.id = ruw.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND r.name = $3\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS exists`,\n\t\t[userId, workspaceId, roleName],\n\t);\n\n\treturn row?.exists ?? false;\n}\n","import type { Operation } from './types';\n\n// Canonical definition moved to @fonderie/core; re-exported here for back-compat.\nexport { OPERATIONS } from '@fonderie/core';\n\nexport const PERMISSION_COLUMN: Record<Operation, string> = {\n\tcreate: 'can_create',\n\tread: 'can_read',\n\tupdate: 'can_update',\n\tdelete: 'can_delete',\n};\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation } from '../types';\nimport { PERMISSION_COLUMN } from '../constants';\n\nexport async function checkPermission(\n\tuserId: string,\n\tworkspaceId: string,\n\tpermissionKey: string,\n\toperation: Operation,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst col = PERMISSION_COLUMN[operation];\n\n\tconst [row] = await store.query<{ has_permission: boolean }>(\n\t\t`SELECT BOOL_OR(rp.${col}) AS has_permission\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON ruw.role_id = r.id\n\t\t JOIN fonderie_role_permissions rp ON r.id = rp.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND rp.workspace_id = $2\n\t\t AND rp.permission_key = $3\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t AND (r.workspace_id = $2 OR r.is_system = true)\n\t\t GROUP BY ruw.user_id, ruw.workspace_id`,\n\t\t[userId, workspaceId, permissionKey],\n\t);\n\n\treturn row?.has_permission ?? false;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation } from './types';\nimport type { IPermissionsConfig } from './config';\nimport { getMembership, hasRole } from './services/membership';\nimport { checkPermission } from './services/permissions';\n\nexport class PermissionsEngine {\n\tprivate superRole: string;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tconfig: IPermissionsConfig = {},\n\t) {\n\t\tthis.superRole = config.superRole ?? 'owner';\n\t}\n\n\tasync getMembership(userId: string, workspaceId: string) {\n\t\treturn getMembership(userId, workspaceId, this.store);\n\t}\n\n\tasync can(\n\t\tuserId: string,\n\t\toperation: Operation,\n\t\tpermissionKey: string,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\treturn checkPermission(userId, workspaceId, permissionKey, operation, this.store);\n\t}\n\n\tasync assert(\n\t\tuserId: string,\n\t\toperation: Operation,\n\t\tpermissionKey: string,\n\t\tworkspaceId: string,\n\t): Promise<void> {\n\t\tconst allowed = await this.can(userId, operation, permissionKey, workspaceId);\n\t\tif (!allowed) {\n\t\t\tthrow new PermissionDeniedError(operation, permissionKey);\n\t\t}\n\t}\n\n\tasync canAll(\n\t\tuserId: string,\n\t\tchecks: Array<{ operation: Operation; permissionKey: string }>,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\tconst results = await Promise.all(\n\t\t\tchecks.map((c) =>\n\t\t\t\tcheckPermission(userId, workspaceId, c.permissionKey, c.operation, this.store),\n\t\t\t),\n\t\t);\n\t\treturn results.every(Boolean);\n\t}\n\n\tasync canAny(\n\t\tuserId: string,\n\t\tchecks: Array<{ operation: Operation; permissionKey: string }>,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\tconst results = await Promise.all(\n\t\t\tchecks.map((c) =>\n\t\t\t\tcheckPermission(userId, workspaceId, c.permissionKey, c.operation, this.store),\n\t\t\t),\n\t\t);\n\t\treturn results.some(Boolean);\n\t}\n}\n\nexport class PermissionDeniedError extends Error {\n\treadonly status = 403;\n\n\tconstructor(operation: string, permissionKey: string) {\n\t\tsuper(`Permission denied: ${operation}:${permissionKey}`);\n\t\tthis.name = 'PermissionDeniedError';\n\t}\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IPermissionsConfig } from './config';\nimport { PermissionsEngine } from './engine';\n\nexport const PERMISSIONS_ENGINE_KEY = 'fonderie.permissions.engine';\n\nexport class PermissionsModule implements IFonderieModule {\n\treadonly engine: PermissionsEngine;\n\treadonly name = '@fonderie/permissions';\n\treadonly deps = ['@fonderie/auth'];\n\n\tconstructor(store: IStoreAdapter, config: IPermissionsConfig = {}) {\n\t\tthis.engine = new PermissionsEngine(store, config);\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\tconst engine = this.engine;\n\n\t\t// Inject engine into every request context\n\t\t// requirePermission() reads it from here — no manual passing needed\n\t\tapp.use(async (ctx, next) => {\n\t\t\tctx.meta[PERMISSIONS_ENGINE_KEY] = engine;\n\t\t\treturn next();\n\t\t});\n\t}\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { PermissionsEngine } from '../engine';\nimport { PERMISSIONS_ENGINE_KEY } from '../module';\nimport { getMembership } from '../services/membership';\n\nfunction makeHandler(roleName: string | string[], store: IStoreAdapter): Middleware {\n\tconst allowed = Array.isArray(roleName) ? roleName : [roleName];\n\n\treturn async (ctx, next) => {\n\t\tif (!ctx.user) {\n\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t}\n\n\t\tconst engine = ctx.meta[PERMISSIONS_ENGINE_KEY];\n\t\tif (!(engine instanceof PermissionsEngine)) {\n\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Permissions module not installed');\n\t\t}\n\n\t\tconst workspaceId =\n\t\t\tctx.workspace?.id ??\n\t\t\t(ctx.meta['params'] as Record<string, string> | undefined)?.['workspaceId'];\n\n\t\tif (!workspaceId) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'WORKSPACE_REQUIRED', 'Workspace context required');\n\t\t}\n\n\t\tconst membership = await getMembership(ctx.user.id, workspaceId, store);\n\t\tif (!membership || !allowed.includes(membership.roleName)) {\n\t\t\treturn setApiResponse(HTTP.FORBIDDEN, 'FORBIDDEN', 'Insufficient role');\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\nexport function requireRole(roleName: string | string[], store: IStoreAdapter): Middleware;\nexport function requireRole(\n\troleName: string | string[],\n\tstore: IStoreAdapter,\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n): Promise<Response>;\nexport function requireRole(\n\troleName: string | string[],\n\tstore: IStoreAdapter,\n\tctx?: IFonderieContext,\n\tnext?: () => Promise<Response>,\n): Middleware | Promise<Response> {\n\tconst handler = makeHandler(roleName, store);\n\tif (ctx !== undefined && next !== undefined) return handler(ctx, next);\n\treturn handler;\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\n\nimport type { Operation } from '../types';\nimport { PermissionsEngine } from '../engine';\nimport { PERMISSIONS_ENGINE_KEY } from '../module';\n\nfunction makeHandler(operation: Operation, permissionKey: string): Middleware {\n\treturn async (ctx, next) => {\n\t\tif (!ctx.user) {\n\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t}\n\n\t\tconst engine = ctx.meta[PERMISSIONS_ENGINE_KEY];\n\t\tif (!(engine instanceof PermissionsEngine)) {\n\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Permissions module not installed');\n\t\t}\n\n\t\tconst workspaceId = resolveWorkspaceId(ctx);\n\t\tif (!workspaceId) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'WORKSPACE_REQUIRED', 'Workspace context required');\n\t\t}\n\n\t\tconst allowed = await engine.can(ctx.user.id, operation, permissionKey, workspaceId);\n\t\tif (!allowed) {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t'FORBIDDEN',\n\t\t\t\t`Permission denied: ${operation}:${permissionKey}`,\n\t\t\t);\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\nexport function requirePermission(operation: Operation, permissionKey: string): Middleware;\nexport function requirePermission(\n\toperation: Operation,\n\tpermissionKey: string,\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n): Promise<Response>;\nexport function requirePermission(\n\toperation: Operation,\n\tpermissionKey: string,\n\tctx?: IFonderieContext,\n\tnext?: () => Promise<Response>,\n): Middleware | Promise<Response> {\n\tconst handler = makeHandler(operation, permissionKey);\n\tif (ctx !== undefined && next !== undefined) return handler(ctx, next);\n\treturn handler;\n}\n\nfunction resolveWorkspaceId(ctx: {\n\tworkspace?: { id: string } | null;\n\tmeta: Record<string, unknown>;\n}): string | null {\n\tif (ctx.workspace?.id) return ctx.workspace.id;\n\n\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\treturn params?.['workspaceId'] ?? null;\n}\n"],"mappings":"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","names":["import_core","import_core","makeHandler"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { IMembership
|
|
1
|
+
import { IMembership } from './types.cjs';
|
|
2
2
|
export { IPermission, IRole, IRoleWithPermissions, PermissionKey } from './types.cjs';
|
|
3
|
-
import { IFonderieModule, IFonderieApp } from '@fonderie/core';
|
|
3
|
+
import { Operation, IFonderieModule, IFonderieApp } from '@fonderie/core';
|
|
4
|
+
export { OPERATIONS, Operation } from '@fonderie/core';
|
|
4
5
|
import { IStoreAdapter } from '@fonderie/store';
|
|
5
6
|
import { IPermissionsConfig } from './config.cjs';
|
|
6
7
|
export { requirePermission, requireRole } from './middlewares/index.cjs';
|
|
@@ -34,12 +35,6 @@ declare class PermissionsModule implements IFonderieModule {
|
|
|
34
35
|
install(app: IFonderieApp): void;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
declare const OPERATIONS: {
|
|
38
|
-
readonly CREATE: "create";
|
|
39
|
-
readonly READ: "read";
|
|
40
|
-
readonly UPDATE: "update";
|
|
41
|
-
readonly DELETE: "delete";
|
|
42
|
-
};
|
|
43
38
|
declare const PERMISSION_COLUMN: Record<Operation, string>;
|
|
44
39
|
|
|
45
|
-
export { IMembership, IPermissionsConfig,
|
|
40
|
+
export { IMembership, IPermissionsConfig, PERMISSION_COLUMN, PermissionDeniedError, PermissionsEngine, PermissionsModule };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { IMembership
|
|
1
|
+
import { IMembership } from './types.js';
|
|
2
2
|
export { IPermission, IRole, IRoleWithPermissions, PermissionKey } from './types.js';
|
|
3
|
-
import { IFonderieModule, IFonderieApp } from '@fonderie/core';
|
|
3
|
+
import { Operation, IFonderieModule, IFonderieApp } from '@fonderie/core';
|
|
4
|
+
export { OPERATIONS, Operation } from '@fonderie/core';
|
|
4
5
|
import { IStoreAdapter } from '@fonderie/store';
|
|
5
6
|
import { IPermissionsConfig } from './config.js';
|
|
6
7
|
export { requirePermission, requireRole } from './middlewares/index.js';
|
|
@@ -34,12 +35,6 @@ declare class PermissionsModule implements IFonderieModule {
|
|
|
34
35
|
install(app: IFonderieApp): void;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
declare const OPERATIONS: {
|
|
38
|
-
readonly CREATE: "create";
|
|
39
|
-
readonly READ: "read";
|
|
40
|
-
readonly UPDATE: "update";
|
|
41
|
-
readonly DELETE: "delete";
|
|
42
|
-
};
|
|
43
38
|
declare const PERMISSION_COLUMN: Record<Operation, string>;
|
|
44
39
|
|
|
45
|
-
export { IMembership, IPermissionsConfig,
|
|
40
|
+
export { IMembership, IPermissionsConfig, PERMISSION_COLUMN, PermissionDeniedError, PermissionsEngine, PermissionsModule };
|
package/dist/index.js
CHANGED
|
@@ -37,12 +37,7 @@ async function hasRole(userId, workspaceId, roleName, store) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// src/constants.ts
|
|
40
|
-
|
|
41
|
-
CREATE: "create",
|
|
42
|
-
READ: "read",
|
|
43
|
-
UPDATE: "update",
|
|
44
|
-
DELETE: "delete"
|
|
45
|
-
};
|
|
40
|
+
import { OPERATIONS } from "@fonderie/core";
|
|
46
41
|
var PERMISSION_COLUMN = {
|
|
47
42
|
create: "can_create",
|
|
48
43
|
read: "can_read",
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/services/membership.ts","../src/constants.ts","../src/services/permissions.ts","../src/engine.ts","../src/module.ts","../src/middlewares/require-role.ts","../src/middlewares/require-permission.ts"],"sourcesContent":["import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IMembership } from '../types';\n\nexport async function getMembership(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<IMembership | null> {\n\tconst [row] = await store.query<{\n\t\tuser_id: string;\n\t\tworkspace_id: string;\n\t\trole_id: string;\n\t\trole_name: string;\n\t}>(\n\t\t`SELECT ruw.user_id, ruw.workspace_id, ruw.role_id, r.name AS role_name\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON r.id = ruw.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t LIMIT 1`,\n\t\t[userId, workspaceId],\n\t);\n\n\tif (!row) return null;\n\n\treturn {\n\t\tuserId: row.user_id,\n\t\tworkspaceId: row.workspace_id,\n\t\troleId: row.role_id,\n\t\troleName: row.role_name,\n\t};\n}\n\nexport async function hasRole(\n\tuserId: string,\n\tworkspaceId: string,\n\troleName: string,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst [row] = await store.query<{ exists: boolean }>(\n\t\t`SELECT EXISTS (\n\t\t SELECT 1\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON r.id = ruw.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND r.name = $3\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS exists`,\n\t\t[userId, workspaceId, roleName],\n\t);\n\n\treturn row?.exists ?? false;\n}\n","import type { Operation } from './types';\n\nexport const OPERATIONS = {\n\tCREATE: 'create',\n\tREAD: 'read',\n\tUPDATE: 'update',\n\tDELETE: 'delete',\n} as const satisfies Record<string, Operation>;\n\nexport const PERMISSION_COLUMN: Record<Operation, string> = {\n\tcreate: 'can_create',\n\tread: 'can_read',\n\tupdate: 'can_update',\n\tdelete: 'can_delete',\n};\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation } from '../types';\nimport { PERMISSION_COLUMN } from '../constants';\n\nexport async function checkPermission(\n\tuserId: string,\n\tworkspaceId: string,\n\tpermissionKey: string,\n\toperation: Operation,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst col = PERMISSION_COLUMN[operation];\n\n\tconst [row] = await store.query<{ has_permission: boolean }>(\n\t\t`SELECT BOOL_OR(rp.${col}) AS has_permission\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON ruw.role_id = r.id\n\t\t JOIN fonderie_role_permissions rp ON r.id = rp.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND rp.workspace_id = $2\n\t\t AND rp.permission_key = $3\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t AND (r.workspace_id = $2 OR r.is_system = true)\n\t\t GROUP BY ruw.user_id, ruw.workspace_id`,\n\t\t[userId, workspaceId, permissionKey],\n\t);\n\n\treturn row?.has_permission ?? false;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation } from './types';\nimport type { IPermissionsConfig } from './config';\nimport { getMembership, hasRole } from './services/membership';\nimport { checkPermission } from './services/permissions';\n\nexport class PermissionsEngine {\n\tprivate superRole: string;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tconfig: IPermissionsConfig = {},\n\t) {\n\t\tthis.superRole = config.superRole ?? 'owner';\n\t}\n\n\tasync getMembership(userId: string, workspaceId: string) {\n\t\treturn getMembership(userId, workspaceId, this.store);\n\t}\n\n\tasync can(\n\t\tuserId: string,\n\t\toperation: Operation,\n\t\tpermissionKey: string,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\treturn checkPermission(userId, workspaceId, permissionKey, operation, this.store);\n\t}\n\n\tasync assert(\n\t\tuserId: string,\n\t\toperation: Operation,\n\t\tpermissionKey: string,\n\t\tworkspaceId: string,\n\t): Promise<void> {\n\t\tconst allowed = await this.can(userId, operation, permissionKey, workspaceId);\n\t\tif (!allowed) {\n\t\t\tthrow new PermissionDeniedError(operation, permissionKey);\n\t\t}\n\t}\n\n\tasync canAll(\n\t\tuserId: string,\n\t\tchecks: Array<{ operation: Operation; permissionKey: string }>,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\tconst results = await Promise.all(\n\t\t\tchecks.map((c) =>\n\t\t\t\tcheckPermission(userId, workspaceId, c.permissionKey, c.operation, this.store),\n\t\t\t),\n\t\t);\n\t\treturn results.every(Boolean);\n\t}\n\n\tasync canAny(\n\t\tuserId: string,\n\t\tchecks: Array<{ operation: Operation; permissionKey: string }>,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\tconst results = await Promise.all(\n\t\t\tchecks.map((c) =>\n\t\t\t\tcheckPermission(userId, workspaceId, c.permissionKey, c.operation, this.store),\n\t\t\t),\n\t\t);\n\t\treturn results.some(Boolean);\n\t}\n}\n\nexport class PermissionDeniedError extends Error {\n\treadonly status = 403;\n\n\tconstructor(operation: string, permissionKey: string) {\n\t\tsuper(`Permission denied: ${operation}:${permissionKey}`);\n\t\tthis.name = 'PermissionDeniedError';\n\t}\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IPermissionsConfig } from './config';\nimport { PermissionsEngine } from './engine';\n\nexport const PERMISSIONS_ENGINE_KEY = 'fonderie.permissions.engine';\n\nexport class PermissionsModule implements IFonderieModule {\n\treadonly engine: PermissionsEngine;\n\treadonly name = '@fonderie/permissions';\n\treadonly deps = ['@fonderie/auth'];\n\n\tconstructor(store: IStoreAdapter, config: IPermissionsConfig = {}) {\n\t\tthis.engine = new PermissionsEngine(store, config);\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\tconst engine = this.engine;\n\n\t\t// Inject engine into every request context\n\t\t// requirePermission() reads it from here — no manual passing needed\n\t\tapp.use(async (ctx, next) => {\n\t\t\tctx.meta[PERMISSIONS_ENGINE_KEY] = engine;\n\t\t\treturn next();\n\t\t});\n\t}\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { PermissionsEngine } from '../engine';\nimport { PERMISSIONS_ENGINE_KEY } from '../module';\nimport { getMembership } from '../services/membership';\n\nfunction makeHandler(roleName: string | string[], store: IStoreAdapter): Middleware {\n\tconst allowed = Array.isArray(roleName) ? roleName : [roleName];\n\n\treturn async (ctx, next) => {\n\t\tif (!ctx.user) {\n\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t}\n\n\t\tconst engine = ctx.meta[PERMISSIONS_ENGINE_KEY];\n\t\tif (!(engine instanceof PermissionsEngine)) {\n\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Permissions module not installed');\n\t\t}\n\n\t\tconst workspaceId =\n\t\t\tctx.workspace?.id ??\n\t\t\t(ctx.meta['params'] as Record<string, string> | undefined)?.['workspaceId'];\n\n\t\tif (!workspaceId) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'WORKSPACE_REQUIRED', 'Workspace context required');\n\t\t}\n\n\t\tconst membership = await getMembership(ctx.user.id, workspaceId, store);\n\t\tif (!membership || !allowed.includes(membership.roleName)) {\n\t\t\treturn setApiResponse(HTTP.FORBIDDEN, 'FORBIDDEN', 'Insufficient role');\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\nexport function requireRole(roleName: string | string[], store: IStoreAdapter): Middleware;\nexport function requireRole(\n\troleName: string | string[],\n\tstore: IStoreAdapter,\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n): Promise<Response>;\nexport function requireRole(\n\troleName: string | string[],\n\tstore: IStoreAdapter,\n\tctx?: IFonderieContext,\n\tnext?: () => Promise<Response>,\n): Middleware | Promise<Response> {\n\tconst handler = makeHandler(roleName, store);\n\tif (ctx !== undefined && next !== undefined) return handler(ctx, next);\n\treturn handler;\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\n\nimport type { Operation } from '../types';\nimport { PermissionsEngine } from '../engine';\nimport { PERMISSIONS_ENGINE_KEY } from '../module';\n\nfunction makeHandler(operation: Operation, permissionKey: string): Middleware {\n\treturn async (ctx, next) => {\n\t\tif (!ctx.user) {\n\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t}\n\n\t\tconst engine = ctx.meta[PERMISSIONS_ENGINE_KEY];\n\t\tif (!(engine instanceof PermissionsEngine)) {\n\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Permissions module not installed');\n\t\t}\n\n\t\tconst workspaceId = resolveWorkspaceId(ctx);\n\t\tif (!workspaceId) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'WORKSPACE_REQUIRED', 'Workspace context required');\n\t\t}\n\n\t\tconst allowed = await engine.can(ctx.user.id, operation, permissionKey, workspaceId);\n\t\tif (!allowed) {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t'FORBIDDEN',\n\t\t\t\t`Permission denied: ${operation}:${permissionKey}`,\n\t\t\t);\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\nexport function requirePermission(operation: Operation, permissionKey: string): Middleware;\nexport function requirePermission(\n\toperation: Operation,\n\tpermissionKey: string,\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n): Promise<Response>;\nexport function requirePermission(\n\toperation: Operation,\n\tpermissionKey: string,\n\tctx?: IFonderieContext,\n\tnext?: () => Promise<Response>,\n): Middleware | Promise<Response> {\n\tconst handler = makeHandler(operation, permissionKey);\n\tif (ctx !== undefined && next !== undefined) return handler(ctx, next);\n\treturn handler;\n}\n\nfunction resolveWorkspaceId(ctx: {\n\tworkspace?: { id: string } | null;\n\tmeta: Record<string, unknown>;\n}): string | null {\n\tif (ctx.workspace?.id) return ctx.workspace.id;\n\n\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\treturn params?.['workspaceId'] ?? null;\n}\n"],"mappings":"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","names":["setApiResponse","HTTP","makeHandler","setApiResponse","HTTP"]}
|
|
1
|
+
{"version":3,"sources":["../src/services/membership.ts","../src/constants.ts","../src/services/permissions.ts","../src/engine.ts","../src/module.ts","../src/middlewares/require-role.ts","../src/middlewares/require-permission.ts"],"sourcesContent":["import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IMembership } from '../types';\n\nexport async function getMembership(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<IMembership | null> {\n\tconst [row] = await store.query<{\n\t\tuser_id: string;\n\t\tworkspace_id: string;\n\t\trole_id: string;\n\t\trole_name: string;\n\t}>(\n\t\t`SELECT ruw.user_id, ruw.workspace_id, ruw.role_id, r.name AS role_name\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON r.id = ruw.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t LIMIT 1`,\n\t\t[userId, workspaceId],\n\t);\n\n\tif (!row) return null;\n\n\treturn {\n\t\tuserId: row.user_id,\n\t\tworkspaceId: row.workspace_id,\n\t\troleId: row.role_id,\n\t\troleName: row.role_name,\n\t};\n}\n\nexport async function hasRole(\n\tuserId: string,\n\tworkspaceId: string,\n\troleName: string,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst [row] = await store.query<{ exists: boolean }>(\n\t\t`SELECT EXISTS (\n\t\t SELECT 1\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON r.id = ruw.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND r.name = $3\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS exists`,\n\t\t[userId, workspaceId, roleName],\n\t);\n\n\treturn row?.exists ?? false;\n}\n","import type { Operation } from './types';\n\n// Canonical definition moved to @fonderie/core; re-exported here for back-compat.\nexport { OPERATIONS } from '@fonderie/core';\n\nexport const PERMISSION_COLUMN: Record<Operation, string> = {\n\tcreate: 'can_create',\n\tread: 'can_read',\n\tupdate: 'can_update',\n\tdelete: 'can_delete',\n};\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation } from '../types';\nimport { PERMISSION_COLUMN } from '../constants';\n\nexport async function checkPermission(\n\tuserId: string,\n\tworkspaceId: string,\n\tpermissionKey: string,\n\toperation: Operation,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst col = PERMISSION_COLUMN[operation];\n\n\tconst [row] = await store.query<{ has_permission: boolean }>(\n\t\t`SELECT BOOL_OR(rp.${col}) AS has_permission\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON ruw.role_id = r.id\n\t\t JOIN fonderie_role_permissions rp ON r.id = rp.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND rp.workspace_id = $2\n\t\t AND rp.permission_key = $3\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t AND (r.workspace_id = $2 OR r.is_system = true)\n\t\t GROUP BY ruw.user_id, ruw.workspace_id`,\n\t\t[userId, workspaceId, permissionKey],\n\t);\n\n\treturn row?.has_permission ?? false;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation } from './types';\nimport type { IPermissionsConfig } from './config';\nimport { getMembership, hasRole } from './services/membership';\nimport { checkPermission } from './services/permissions';\n\nexport class PermissionsEngine {\n\tprivate superRole: string;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tconfig: IPermissionsConfig = {},\n\t) {\n\t\tthis.superRole = config.superRole ?? 'owner';\n\t}\n\n\tasync getMembership(userId: string, workspaceId: string) {\n\t\treturn getMembership(userId, workspaceId, this.store);\n\t}\n\n\tasync can(\n\t\tuserId: string,\n\t\toperation: Operation,\n\t\tpermissionKey: string,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\treturn checkPermission(userId, workspaceId, permissionKey, operation, this.store);\n\t}\n\n\tasync assert(\n\t\tuserId: string,\n\t\toperation: Operation,\n\t\tpermissionKey: string,\n\t\tworkspaceId: string,\n\t): Promise<void> {\n\t\tconst allowed = await this.can(userId, operation, permissionKey, workspaceId);\n\t\tif (!allowed) {\n\t\t\tthrow new PermissionDeniedError(operation, permissionKey);\n\t\t}\n\t}\n\n\tasync canAll(\n\t\tuserId: string,\n\t\tchecks: Array<{ operation: Operation; permissionKey: string }>,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\tconst results = await Promise.all(\n\t\t\tchecks.map((c) =>\n\t\t\t\tcheckPermission(userId, workspaceId, c.permissionKey, c.operation, this.store),\n\t\t\t),\n\t\t);\n\t\treturn results.every(Boolean);\n\t}\n\n\tasync canAny(\n\t\tuserId: string,\n\t\tchecks: Array<{ operation: Operation; permissionKey: string }>,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\tconst results = await Promise.all(\n\t\t\tchecks.map((c) =>\n\t\t\t\tcheckPermission(userId, workspaceId, c.permissionKey, c.operation, this.store),\n\t\t\t),\n\t\t);\n\t\treturn results.some(Boolean);\n\t}\n}\n\nexport class PermissionDeniedError extends Error {\n\treadonly status = 403;\n\n\tconstructor(operation: string, permissionKey: string) {\n\t\tsuper(`Permission denied: ${operation}:${permissionKey}`);\n\t\tthis.name = 'PermissionDeniedError';\n\t}\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IPermissionsConfig } from './config';\nimport { PermissionsEngine } from './engine';\n\nexport const PERMISSIONS_ENGINE_KEY = 'fonderie.permissions.engine';\n\nexport class PermissionsModule implements IFonderieModule {\n\treadonly engine: PermissionsEngine;\n\treadonly name = '@fonderie/permissions';\n\treadonly deps = ['@fonderie/auth'];\n\n\tconstructor(store: IStoreAdapter, config: IPermissionsConfig = {}) {\n\t\tthis.engine = new PermissionsEngine(store, config);\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\tconst engine = this.engine;\n\n\t\t// Inject engine into every request context\n\t\t// requirePermission() reads it from here — no manual passing needed\n\t\tapp.use(async (ctx, next) => {\n\t\t\tctx.meta[PERMISSIONS_ENGINE_KEY] = engine;\n\t\t\treturn next();\n\t\t});\n\t}\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { PermissionsEngine } from '../engine';\nimport { PERMISSIONS_ENGINE_KEY } from '../module';\nimport { getMembership } from '../services/membership';\n\nfunction makeHandler(roleName: string | string[], store: IStoreAdapter): Middleware {\n\tconst allowed = Array.isArray(roleName) ? roleName : [roleName];\n\n\treturn async (ctx, next) => {\n\t\tif (!ctx.user) {\n\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t}\n\n\t\tconst engine = ctx.meta[PERMISSIONS_ENGINE_KEY];\n\t\tif (!(engine instanceof PermissionsEngine)) {\n\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Permissions module not installed');\n\t\t}\n\n\t\tconst workspaceId =\n\t\t\tctx.workspace?.id ??\n\t\t\t(ctx.meta['params'] as Record<string, string> | undefined)?.['workspaceId'];\n\n\t\tif (!workspaceId) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'WORKSPACE_REQUIRED', 'Workspace context required');\n\t\t}\n\n\t\tconst membership = await getMembership(ctx.user.id, workspaceId, store);\n\t\tif (!membership || !allowed.includes(membership.roleName)) {\n\t\t\treturn setApiResponse(HTTP.FORBIDDEN, 'FORBIDDEN', 'Insufficient role');\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\nexport function requireRole(roleName: string | string[], store: IStoreAdapter): Middleware;\nexport function requireRole(\n\troleName: string | string[],\n\tstore: IStoreAdapter,\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n): Promise<Response>;\nexport function requireRole(\n\troleName: string | string[],\n\tstore: IStoreAdapter,\n\tctx?: IFonderieContext,\n\tnext?: () => Promise<Response>,\n): Middleware | Promise<Response> {\n\tconst handler = makeHandler(roleName, store);\n\tif (ctx !== undefined && next !== undefined) return handler(ctx, next);\n\treturn handler;\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\n\nimport type { Operation } from '../types';\nimport { PermissionsEngine } from '../engine';\nimport { PERMISSIONS_ENGINE_KEY } from '../module';\n\nfunction makeHandler(operation: Operation, permissionKey: string): Middleware {\n\treturn async (ctx, next) => {\n\t\tif (!ctx.user) {\n\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t}\n\n\t\tconst engine = ctx.meta[PERMISSIONS_ENGINE_KEY];\n\t\tif (!(engine instanceof PermissionsEngine)) {\n\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Permissions module not installed');\n\t\t}\n\n\t\tconst workspaceId = resolveWorkspaceId(ctx);\n\t\tif (!workspaceId) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'WORKSPACE_REQUIRED', 'Workspace context required');\n\t\t}\n\n\t\tconst allowed = await engine.can(ctx.user.id, operation, permissionKey, workspaceId);\n\t\tif (!allowed) {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t'FORBIDDEN',\n\t\t\t\t`Permission denied: ${operation}:${permissionKey}`,\n\t\t\t);\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\nexport function requirePermission(operation: Operation, permissionKey: string): Middleware;\nexport function requirePermission(\n\toperation: Operation,\n\tpermissionKey: string,\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n): Promise<Response>;\nexport function requirePermission(\n\toperation: Operation,\n\tpermissionKey: string,\n\tctx?: IFonderieContext,\n\tnext?: () => Promise<Response>,\n): Middleware | Promise<Response> {\n\tconst handler = makeHandler(operation, permissionKey);\n\tif (ctx !== undefined && next !== undefined) return handler(ctx, next);\n\treturn handler;\n}\n\nfunction resolveWorkspaceId(ctx: {\n\tworkspace?: { id: string } | null;\n\tmeta: Record<string, unknown>;\n}): string | null {\n\tif (ctx.workspace?.id) return ctx.workspace.id;\n\n\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\treturn params?.['workspaceId'] ?? null;\n}\n"],"mappings":"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","names":["setApiResponse","HTTP","makeHandler","setApiResponse","HTTP"]}
|
|
@@ -26,7 +26,7 @@ __export(middlewares_exports, {
|
|
|
26
26
|
module.exports = __toCommonJS(middlewares_exports);
|
|
27
27
|
|
|
28
28
|
// src/middlewares/require-role.ts
|
|
29
|
-
var
|
|
29
|
+
var import_core2 = require("@fonderie/core");
|
|
30
30
|
|
|
31
31
|
// src/services/membership.ts
|
|
32
32
|
async function getMembership(userId, workspaceId, store) {
|
|
@@ -67,6 +67,7 @@ async function hasRole(userId, workspaceId, roleName, store) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// src/constants.ts
|
|
70
|
+
var import_core = require("@fonderie/core");
|
|
70
71
|
var PERMISSION_COLUMN = {
|
|
71
72
|
create: "can_create",
|
|
72
73
|
read: "can_read",
|
|
@@ -157,19 +158,19 @@ function makeHandler(roleName, store) {
|
|
|
157
158
|
const allowed = Array.isArray(roleName) ? roleName : [roleName];
|
|
158
159
|
return async (ctx, next) => {
|
|
159
160
|
if (!ctx.user) {
|
|
160
|
-
return (0,
|
|
161
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
161
162
|
}
|
|
162
163
|
const engine = ctx.meta[PERMISSIONS_ENGINE_KEY];
|
|
163
164
|
if (!(engine instanceof PermissionsEngine)) {
|
|
164
|
-
return (0,
|
|
165
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.SERVER_ERROR, "SERVER_ERROR", "Permissions module not installed");
|
|
165
166
|
}
|
|
166
167
|
const workspaceId = ctx.workspace?.id ?? ctx.meta["params"]?.["workspaceId"];
|
|
167
168
|
if (!workspaceId) {
|
|
168
|
-
return (0,
|
|
169
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.BAD_REQUEST, "WORKSPACE_REQUIRED", "Workspace context required");
|
|
169
170
|
}
|
|
170
171
|
const membership = await getMembership(ctx.user.id, workspaceId, store);
|
|
171
172
|
if (!membership || !allowed.includes(membership.roleName)) {
|
|
172
|
-
return (0,
|
|
173
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.FORBIDDEN, "FORBIDDEN", "Insufficient role");
|
|
173
174
|
}
|
|
174
175
|
return next();
|
|
175
176
|
};
|
|
@@ -181,24 +182,24 @@ function requireRole(roleName, store, ctx, next) {
|
|
|
181
182
|
}
|
|
182
183
|
|
|
183
184
|
// src/middlewares/require-permission.ts
|
|
184
|
-
var
|
|
185
|
+
var import_core3 = require("@fonderie/core");
|
|
185
186
|
function makeHandler2(operation, permissionKey) {
|
|
186
187
|
return async (ctx, next) => {
|
|
187
188
|
if (!ctx.user) {
|
|
188
|
-
return (0,
|
|
189
|
+
return (0, import_core3.setApiResponse)(import_core3.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
189
190
|
}
|
|
190
191
|
const engine = ctx.meta[PERMISSIONS_ENGINE_KEY];
|
|
191
192
|
if (!(engine instanceof PermissionsEngine)) {
|
|
192
|
-
return (0,
|
|
193
|
+
return (0, import_core3.setApiResponse)(import_core3.HTTP.SERVER_ERROR, "SERVER_ERROR", "Permissions module not installed");
|
|
193
194
|
}
|
|
194
195
|
const workspaceId = resolveWorkspaceId(ctx);
|
|
195
196
|
if (!workspaceId) {
|
|
196
|
-
return (0,
|
|
197
|
+
return (0, import_core3.setApiResponse)(import_core3.HTTP.BAD_REQUEST, "WORKSPACE_REQUIRED", "Workspace context required");
|
|
197
198
|
}
|
|
198
199
|
const allowed = await engine.can(ctx.user.id, operation, permissionKey, workspaceId);
|
|
199
200
|
if (!allowed) {
|
|
200
|
-
return (0,
|
|
201
|
-
|
|
201
|
+
return (0, import_core3.setApiResponse)(
|
|
202
|
+
import_core3.HTTP.FORBIDDEN,
|
|
202
203
|
"FORBIDDEN",
|
|
203
204
|
`Permission denied: ${operation}:${permissionKey}`
|
|
204
205
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/middlewares/index.ts","../../src/middlewares/require-role.ts","../../src/services/membership.ts","../../src/constants.ts","../../src/services/permissions.ts","../../src/engine.ts","../../src/module.ts","../../src/middlewares/require-permission.ts"],"sourcesContent":["export { requireRole } from './require-role';\nexport { requirePermission } from './require-permission';\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { PermissionsEngine } from '../engine';\nimport { PERMISSIONS_ENGINE_KEY } from '../module';\nimport { getMembership } from '../services/membership';\n\nfunction makeHandler(roleName: string | string[], store: IStoreAdapter): Middleware {\n\tconst allowed = Array.isArray(roleName) ? roleName : [roleName];\n\n\treturn async (ctx, next) => {\n\t\tif (!ctx.user) {\n\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t}\n\n\t\tconst engine = ctx.meta[PERMISSIONS_ENGINE_KEY];\n\t\tif (!(engine instanceof PermissionsEngine)) {\n\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Permissions module not installed');\n\t\t}\n\n\t\tconst workspaceId =\n\t\t\tctx.workspace?.id ??\n\t\t\t(ctx.meta['params'] as Record<string, string> | undefined)?.['workspaceId'];\n\n\t\tif (!workspaceId) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'WORKSPACE_REQUIRED', 'Workspace context required');\n\t\t}\n\n\t\tconst membership = await getMembership(ctx.user.id, workspaceId, store);\n\t\tif (!membership || !allowed.includes(membership.roleName)) {\n\t\t\treturn setApiResponse(HTTP.FORBIDDEN, 'FORBIDDEN', 'Insufficient role');\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\nexport function requireRole(roleName: string | string[], store: IStoreAdapter): Middleware;\nexport function requireRole(\n\troleName: string | string[],\n\tstore: IStoreAdapter,\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n): Promise<Response>;\nexport function requireRole(\n\troleName: string | string[],\n\tstore: IStoreAdapter,\n\tctx?: IFonderieContext,\n\tnext?: () => Promise<Response>,\n): Middleware | Promise<Response> {\n\tconst handler = makeHandler(roleName, store);\n\tif (ctx !== undefined && next !== undefined) return handler(ctx, next);\n\treturn handler;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IMembership } from '../types';\n\nexport async function getMembership(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<IMembership | null> {\n\tconst [row] = await store.query<{\n\t\tuser_id: string;\n\t\tworkspace_id: string;\n\t\trole_id: string;\n\t\trole_name: string;\n\t}>(\n\t\t`SELECT ruw.user_id, ruw.workspace_id, ruw.role_id, r.name AS role_name\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON r.id = ruw.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t LIMIT 1`,\n\t\t[userId, workspaceId],\n\t);\n\n\tif (!row) return null;\n\n\treturn {\n\t\tuserId: row.user_id,\n\t\tworkspaceId: row.workspace_id,\n\t\troleId: row.role_id,\n\t\troleName: row.role_name,\n\t};\n}\n\nexport async function hasRole(\n\tuserId: string,\n\tworkspaceId: string,\n\troleName: string,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst [row] = await store.query<{ exists: boolean }>(\n\t\t`SELECT EXISTS (\n\t\t SELECT 1\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON r.id = ruw.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND r.name = $3\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS exists`,\n\t\t[userId, workspaceId, roleName],\n\t);\n\n\treturn row?.exists ?? false;\n}\n","import type { Operation } from './types';\n\nexport const OPERATIONS = {\n\tCREATE: 'create',\n\tREAD: 'read',\n\tUPDATE: 'update',\n\tDELETE: 'delete',\n} as const satisfies Record<string, Operation>;\n\nexport const PERMISSION_COLUMN: Record<Operation, string> = {\n\tcreate: 'can_create',\n\tread: 'can_read',\n\tupdate: 'can_update',\n\tdelete: 'can_delete',\n};\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation } from '../types';\nimport { PERMISSION_COLUMN } from '../constants';\n\nexport async function checkPermission(\n\tuserId: string,\n\tworkspaceId: string,\n\tpermissionKey: string,\n\toperation: Operation,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst col = PERMISSION_COLUMN[operation];\n\n\tconst [row] = await store.query<{ has_permission: boolean }>(\n\t\t`SELECT BOOL_OR(rp.${col}) AS has_permission\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON ruw.role_id = r.id\n\t\t JOIN fonderie_role_permissions rp ON r.id = rp.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND rp.workspace_id = $2\n\t\t AND rp.permission_key = $3\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t AND (r.workspace_id = $2 OR r.is_system = true)\n\t\t GROUP BY ruw.user_id, ruw.workspace_id`,\n\t\t[userId, workspaceId, permissionKey],\n\t);\n\n\treturn row?.has_permission ?? false;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation } from './types';\nimport type { IPermissionsConfig } from './config';\nimport { getMembership, hasRole } from './services/membership';\nimport { checkPermission } from './services/permissions';\n\nexport class PermissionsEngine {\n\tprivate superRole: string;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tconfig: IPermissionsConfig = {},\n\t) {\n\t\tthis.superRole = config.superRole ?? 'owner';\n\t}\n\n\tasync getMembership(userId: string, workspaceId: string) {\n\t\treturn getMembership(userId, workspaceId, this.store);\n\t}\n\n\tasync can(\n\t\tuserId: string,\n\t\toperation: Operation,\n\t\tpermissionKey: string,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\treturn checkPermission(userId, workspaceId, permissionKey, operation, this.store);\n\t}\n\n\tasync assert(\n\t\tuserId: string,\n\t\toperation: Operation,\n\t\tpermissionKey: string,\n\t\tworkspaceId: string,\n\t): Promise<void> {\n\t\tconst allowed = await this.can(userId, operation, permissionKey, workspaceId);\n\t\tif (!allowed) {\n\t\t\tthrow new PermissionDeniedError(operation, permissionKey);\n\t\t}\n\t}\n\n\tasync canAll(\n\t\tuserId: string,\n\t\tchecks: Array<{ operation: Operation; permissionKey: string }>,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\tconst results = await Promise.all(\n\t\t\tchecks.map((c) =>\n\t\t\t\tcheckPermission(userId, workspaceId, c.permissionKey, c.operation, this.store),\n\t\t\t),\n\t\t);\n\t\treturn results.every(Boolean);\n\t}\n\n\tasync canAny(\n\t\tuserId: string,\n\t\tchecks: Array<{ operation: Operation; permissionKey: string }>,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\tconst results = await Promise.all(\n\t\t\tchecks.map((c) =>\n\t\t\t\tcheckPermission(userId, workspaceId, c.permissionKey, c.operation, this.store),\n\t\t\t),\n\t\t);\n\t\treturn results.some(Boolean);\n\t}\n}\n\nexport class PermissionDeniedError extends Error {\n\treadonly status = 403;\n\n\tconstructor(operation: string, permissionKey: string) {\n\t\tsuper(`Permission denied: ${operation}:${permissionKey}`);\n\t\tthis.name = 'PermissionDeniedError';\n\t}\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IPermissionsConfig } from './config';\nimport { PermissionsEngine } from './engine';\n\nexport const PERMISSIONS_ENGINE_KEY = 'fonderie.permissions.engine';\n\nexport class PermissionsModule implements IFonderieModule {\n\treadonly engine: PermissionsEngine;\n\treadonly name = '@fonderie/permissions';\n\treadonly deps = ['@fonderie/auth'];\n\n\tconstructor(store: IStoreAdapter, config: IPermissionsConfig = {}) {\n\t\tthis.engine = new PermissionsEngine(store, config);\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\tconst engine = this.engine;\n\n\t\t// Inject engine into every request context\n\t\t// requirePermission() reads it from here — no manual passing needed\n\t\tapp.use(async (ctx, next) => {\n\t\t\tctx.meta[PERMISSIONS_ENGINE_KEY] = engine;\n\t\t\treturn next();\n\t\t});\n\t}\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\n\nimport type { Operation } from '../types';\nimport { PermissionsEngine } from '../engine';\nimport { PERMISSIONS_ENGINE_KEY } from '../module';\n\nfunction makeHandler(operation: Operation, permissionKey: string): Middleware {\n\treturn async (ctx, next) => {\n\t\tif (!ctx.user) {\n\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t}\n\n\t\tconst engine = ctx.meta[PERMISSIONS_ENGINE_KEY];\n\t\tif (!(engine instanceof PermissionsEngine)) {\n\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Permissions module not installed');\n\t\t}\n\n\t\tconst workspaceId = resolveWorkspaceId(ctx);\n\t\tif (!workspaceId) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'WORKSPACE_REQUIRED', 'Workspace context required');\n\t\t}\n\n\t\tconst allowed = await engine.can(ctx.user.id, operation, permissionKey, workspaceId);\n\t\tif (!allowed) {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t'FORBIDDEN',\n\t\t\t\t`Permission denied: ${operation}:${permissionKey}`,\n\t\t\t);\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\nexport function requirePermission(operation: Operation, permissionKey: string): Middleware;\nexport function requirePermission(\n\toperation: Operation,\n\tpermissionKey: string,\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n): Promise<Response>;\nexport function requirePermission(\n\toperation: Operation,\n\tpermissionKey: string,\n\tctx?: IFonderieContext,\n\tnext?: () => Promise<Response>,\n): Middleware | Promise<Response> {\n\tconst handler = makeHandler(operation, permissionKey);\n\tif (ctx !== undefined && next !== undefined) return handler(ctx, next);\n\treturn handler;\n}\n\nfunction resolveWorkspaceId(ctx: {\n\tworkspace?: { id: string } | null;\n\tmeta: Record<string, unknown>;\n}): string | null {\n\tif (ctx.workspace?.id) return ctx.workspace.id;\n\n\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\treturn params?.['workspaceId'] ?? null;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,kBAAqC;;;ACIrC,eAAsB,cACrB,QACA,aACA,OAC8B;AAC9B,QAAM,CAAC,GAAG,IAAI,MAAM,MAAM;AAAA,IAMzB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA,CAAC,QAAQ,WAAW;AAAA,EACrB;AAEA,MAAI,CAAC,IAAK,QAAO;AAEjB,SAAO;AAAA,IACN,QAAQ,IAAI;AAAA,IACZ,aAAa,IAAI;AAAA,IACjB,QAAQ,IAAI;AAAA,IACZ,UAAU,IAAI;AAAA,EACf;AACD;AAEA,eAAsB,QACrB,QACA,aACA,UACA,OACmB;AACnB,QAAM,CAAC,GAAG,IAAI,MAAM,MAAM;AAAA,IACzB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAUA,CAAC,QAAQ,aAAa,QAAQ;AAAA,EAC/B;AAEA,SAAO,KAAK,UAAU;AACvB;;;AChDO,IAAM,oBAA+C;AAAA,EAC3D,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AACT;;;ACTA,eAAsB,gBACrB,QACA,aACA,eACA,WACA,OACmB;AACnB,QAAM,MAAM,kBAAkB,SAAS;AAEvC,QAAM,CAAC,GAAG,IAAI,MAAM,MAAM;AAAA,IACzB,qBAAqB,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAYxB,CAAC,QAAQ,aAAa,aAAa;AAAA,EACpC;AAEA,SAAO,KAAK,kBAAkB;AAC/B;;;ACxBO,IAAM,oBAAN,MAAwB;AAAA,EAG9B,YACS,OACR,SAA6B,CAAC,GAC7B;AAFO;AAGR,SAAK,YAAY,OAAO,aAAa;AAAA,EACtC;AAAA,EAJS;AAAA,EAHD;AAAA,EASR,MAAM,cAAc,QAAgB,aAAqB;AACxD,WAAO,cAAc,QAAQ,aAAa,KAAK,KAAK;AAAA,EACrD;AAAA,EAEA,MAAM,IACL,QACA,WACA,eACA,aACmB;AACnB,UAAM,WAAW,MAAM,cAAc,QAAQ,aAAa,KAAK,KAAK;AACpE,QAAI,CAAC,SAAU,QAAO;AAEtB,QAAI,MAAM,QAAQ,QAAQ,aAAa,KAAK,WAAW,KAAK,KAAK,EAAG,QAAO;AAE3E,WAAO,gBAAgB,QAAQ,aAAa,eAAe,WAAW,KAAK,KAAK;AAAA,EACjF;AAAA,EAEA,MAAM,OACL,QACA,WACA,eACA,aACgB;AAChB,UAAM,UAAU,MAAM,KAAK,IAAI,QAAQ,WAAW,eAAe,WAAW;AAC5E,QAAI,CAAC,SAAS;AACb,YAAM,IAAI,sBAAsB,WAAW,aAAa;AAAA,IACzD;AAAA,EACD;AAAA,EAEA,MAAM,OACL,QACA,QACA,aACmB;AACnB,UAAM,WAAW,MAAM,cAAc,QAAQ,aAAa,KAAK,KAAK;AACpE,QAAI,CAAC,SAAU,QAAO;AAEtB,QAAI,MAAM,QAAQ,QAAQ,aAAa,KAAK,WAAW,KAAK,KAAK,EAAG,QAAO;AAE3E,UAAM,UAAU,MAAM,QAAQ;AAAA,MAC7B,OAAO;AAAA,QAAI,CAAC,MACX,gBAAgB,QAAQ,aAAa,EAAE,eAAe,EAAE,WAAW,KAAK,KAAK;AAAA,MAC9E;AAAA,IACD;AACA,WAAO,QAAQ,MAAM,OAAO;AAAA,EAC7B;AAAA,EAEA,MAAM,OACL,QACA,QACA,aACmB;AACnB,UAAM,WAAW,MAAM,cAAc,QAAQ,aAAa,KAAK,KAAK;AACpE,QAAI,CAAC,SAAU,QAAO;AAEtB,QAAI,MAAM,QAAQ,QAAQ,aAAa,KAAK,WAAW,KAAK,KAAK,EAAG,QAAO;AAE3E,UAAM,UAAU,MAAM,QAAQ;AAAA,MAC7B,OAAO;AAAA,QAAI,CAAC,MACX,gBAAgB,QAAQ,aAAa,EAAE,eAAe,EAAE,WAAW,KAAK,KAAK;AAAA,MAC9E;AAAA,IACD;AACA,WAAO,QAAQ,KAAK,OAAO;AAAA,EAC5B;AACD;AAEO,IAAM,wBAAN,cAAoC,MAAM;AAAA,EACvC,SAAS;AAAA,EAElB,YAAY,WAAmB,eAAuB;AACrD,UAAM,sBAAsB,SAAS,IAAI,aAAa,EAAE;AACxD,SAAK,OAAO;AAAA,EACb;AACD;;;ACrFO,IAAM,yBAAyB;;;ALGtC,SAAS,YAAY,UAA6B,OAAkC;AACnF,QAAM,UAAU,MAAM,QAAQ,QAAQ,IAAI,WAAW,CAAC,QAAQ;AAE9D,SAAO,OAAO,KAAK,SAAS;AAC3B,QAAI,CAAC,IAAI,MAAM;AACd,iBAAO,4BAAe,iBAAK,cAAc,gBAAgB,cAAc;AAAA,IACxE;AAEA,UAAM,SAAS,IAAI,KAAK,sBAAsB;AAC9C,QAAI,EAAE,kBAAkB,oBAAoB;AAC3C,iBAAO,4BAAe,iBAAK,cAAc,gBAAgB,kCAAkC;AAAA,IAC5F;AAEA,UAAM,cACL,IAAI,WAAW,MACd,IAAI,KAAK,QAAQ,IAA2C,aAAa;AAE3E,QAAI,CAAC,aAAa;AACjB,iBAAO,4BAAe,iBAAK,aAAa,sBAAsB,4BAA4B;AAAA,IAC3F;AAEA,UAAM,aAAa,MAAM,cAAc,IAAI,KAAK,IAAI,aAAa,KAAK;AACtE,QAAI,CAAC,cAAc,CAAC,QAAQ,SAAS,WAAW,QAAQ,GAAG;AAC1D,iBAAO,4BAAe,iBAAK,WAAW,aAAa,mBAAmB;AAAA,IACvE;AAEA,WAAO,KAAK;AAAA,EACb;AACD;AASO,SAAS,YACf,UACA,OACA,KACA,MACiC;AACjC,QAAM,UAAU,YAAY,UAAU,KAAK;AAC3C,MAAI,QAAQ,UAAa,SAAS,OAAW,QAAO,QAAQ,KAAK,IAAI;AACrE,SAAO;AACR;;;AMvDA,IAAAA,eAAqC;AAQrC,SAASC,aAAY,WAAsB,eAAmC;AAC7E,SAAO,OAAO,KAAK,SAAS;AAC3B,QAAI,CAAC,IAAI,MAAM;AACd,iBAAO,6BAAe,kBAAK,cAAc,gBAAgB,cAAc;AAAA,IACxE;AAEA,UAAM,SAAS,IAAI,KAAK,sBAAsB;AAC9C,QAAI,EAAE,kBAAkB,oBAAoB;AAC3C,iBAAO,6BAAe,kBAAK,cAAc,gBAAgB,kCAAkC;AAAA,IAC5F;AAEA,UAAM,cAAc,mBAAmB,GAAG;AAC1C,QAAI,CAAC,aAAa;AACjB,iBAAO,6BAAe,kBAAK,aAAa,sBAAsB,4BAA4B;AAAA,IAC3F;AAEA,UAAM,UAAU,MAAM,OAAO,IAAI,IAAI,KAAK,IAAI,WAAW,eAAe,WAAW;AACnF,QAAI,CAAC,SAAS;AACb,iBAAO;AAAA,QACN,kBAAK;AAAA,QACL;AAAA,QACA,sBAAsB,SAAS,IAAI,aAAa;AAAA,MACjD;AAAA,IACD;AAEA,WAAO,KAAK;AAAA,EACb;AACD;AASO,SAAS,kBACf,WACA,eACA,KACA,MACiC;AACjC,QAAM,UAAUA,aAAY,WAAW,aAAa;AACpD,MAAI,QAAQ,UAAa,SAAS,OAAW,QAAO,QAAQ,KAAK,IAAI;AACrE,SAAO;AACR;AAEA,SAAS,mBAAmB,KAGV;AACjB,MAAI,IAAI,WAAW,GAAI,QAAO,IAAI,UAAU;AAE5C,QAAM,SAAS,IAAI,KAAK,QAAQ;AAChC,SAAO,SAAS,aAAa,KAAK;AACnC;","names":["import_core","makeHandler"]}
|
|
1
|
+
{"version":3,"sources":["../../src/middlewares/index.ts","../../src/middlewares/require-role.ts","../../src/services/membership.ts","../../src/constants.ts","../../src/services/permissions.ts","../../src/engine.ts","../../src/module.ts","../../src/middlewares/require-permission.ts"],"sourcesContent":["export { requireRole } from './require-role';\nexport { requirePermission } from './require-permission';\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { PermissionsEngine } from '../engine';\nimport { PERMISSIONS_ENGINE_KEY } from '../module';\nimport { getMembership } from '../services/membership';\n\nfunction makeHandler(roleName: string | string[], store: IStoreAdapter): Middleware {\n\tconst allowed = Array.isArray(roleName) ? roleName : [roleName];\n\n\treturn async (ctx, next) => {\n\t\tif (!ctx.user) {\n\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t}\n\n\t\tconst engine = ctx.meta[PERMISSIONS_ENGINE_KEY];\n\t\tif (!(engine instanceof PermissionsEngine)) {\n\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Permissions module not installed');\n\t\t}\n\n\t\tconst workspaceId =\n\t\t\tctx.workspace?.id ??\n\t\t\t(ctx.meta['params'] as Record<string, string> | undefined)?.['workspaceId'];\n\n\t\tif (!workspaceId) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'WORKSPACE_REQUIRED', 'Workspace context required');\n\t\t}\n\n\t\tconst membership = await getMembership(ctx.user.id, workspaceId, store);\n\t\tif (!membership || !allowed.includes(membership.roleName)) {\n\t\t\treturn setApiResponse(HTTP.FORBIDDEN, 'FORBIDDEN', 'Insufficient role');\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\nexport function requireRole(roleName: string | string[], store: IStoreAdapter): Middleware;\nexport function requireRole(\n\troleName: string | string[],\n\tstore: IStoreAdapter,\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n): Promise<Response>;\nexport function requireRole(\n\troleName: string | string[],\n\tstore: IStoreAdapter,\n\tctx?: IFonderieContext,\n\tnext?: () => Promise<Response>,\n): Middleware | Promise<Response> {\n\tconst handler = makeHandler(roleName, store);\n\tif (ctx !== undefined && next !== undefined) return handler(ctx, next);\n\treturn handler;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IMembership } from '../types';\n\nexport async function getMembership(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<IMembership | null> {\n\tconst [row] = await store.query<{\n\t\tuser_id: string;\n\t\tworkspace_id: string;\n\t\trole_id: string;\n\t\trole_name: string;\n\t}>(\n\t\t`SELECT ruw.user_id, ruw.workspace_id, ruw.role_id, r.name AS role_name\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON r.id = ruw.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t LIMIT 1`,\n\t\t[userId, workspaceId],\n\t);\n\n\tif (!row) return null;\n\n\treturn {\n\t\tuserId: row.user_id,\n\t\tworkspaceId: row.workspace_id,\n\t\troleId: row.role_id,\n\t\troleName: row.role_name,\n\t};\n}\n\nexport async function hasRole(\n\tuserId: string,\n\tworkspaceId: string,\n\troleName: string,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst [row] = await store.query<{ exists: boolean }>(\n\t\t`SELECT EXISTS (\n\t\t SELECT 1\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON r.id = ruw.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND r.name = $3\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS exists`,\n\t\t[userId, workspaceId, roleName],\n\t);\n\n\treturn row?.exists ?? false;\n}\n","import type { Operation } from './types';\n\n// Canonical definition moved to @fonderie/core; re-exported here for back-compat.\nexport { OPERATIONS } from '@fonderie/core';\n\nexport const PERMISSION_COLUMN: Record<Operation, string> = {\n\tcreate: 'can_create',\n\tread: 'can_read',\n\tupdate: 'can_update',\n\tdelete: 'can_delete',\n};\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation } from '../types';\nimport { PERMISSION_COLUMN } from '../constants';\n\nexport async function checkPermission(\n\tuserId: string,\n\tworkspaceId: string,\n\tpermissionKey: string,\n\toperation: Operation,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst col = PERMISSION_COLUMN[operation];\n\n\tconst [row] = await store.query<{ has_permission: boolean }>(\n\t\t`SELECT BOOL_OR(rp.${col}) AS has_permission\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON ruw.role_id = r.id\n\t\t JOIN fonderie_role_permissions rp ON r.id = rp.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND rp.workspace_id = $2\n\t\t AND rp.permission_key = $3\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t AND (r.workspace_id = $2 OR r.is_system = true)\n\t\t GROUP BY ruw.user_id, ruw.workspace_id`,\n\t\t[userId, workspaceId, permissionKey],\n\t);\n\n\treturn row?.has_permission ?? false;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation } from './types';\nimport type { IPermissionsConfig } from './config';\nimport { getMembership, hasRole } from './services/membership';\nimport { checkPermission } from './services/permissions';\n\nexport class PermissionsEngine {\n\tprivate superRole: string;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tconfig: IPermissionsConfig = {},\n\t) {\n\t\tthis.superRole = config.superRole ?? 'owner';\n\t}\n\n\tasync getMembership(userId: string, workspaceId: string) {\n\t\treturn getMembership(userId, workspaceId, this.store);\n\t}\n\n\tasync can(\n\t\tuserId: string,\n\t\toperation: Operation,\n\t\tpermissionKey: string,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\treturn checkPermission(userId, workspaceId, permissionKey, operation, this.store);\n\t}\n\n\tasync assert(\n\t\tuserId: string,\n\t\toperation: Operation,\n\t\tpermissionKey: string,\n\t\tworkspaceId: string,\n\t): Promise<void> {\n\t\tconst allowed = await this.can(userId, operation, permissionKey, workspaceId);\n\t\tif (!allowed) {\n\t\t\tthrow new PermissionDeniedError(operation, permissionKey);\n\t\t}\n\t}\n\n\tasync canAll(\n\t\tuserId: string,\n\t\tchecks: Array<{ operation: Operation; permissionKey: string }>,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\tconst results = await Promise.all(\n\t\t\tchecks.map((c) =>\n\t\t\t\tcheckPermission(userId, workspaceId, c.permissionKey, c.operation, this.store),\n\t\t\t),\n\t\t);\n\t\treturn results.every(Boolean);\n\t}\n\n\tasync canAny(\n\t\tuserId: string,\n\t\tchecks: Array<{ operation: Operation; permissionKey: string }>,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\tconst results = await Promise.all(\n\t\t\tchecks.map((c) =>\n\t\t\t\tcheckPermission(userId, workspaceId, c.permissionKey, c.operation, this.store),\n\t\t\t),\n\t\t);\n\t\treturn results.some(Boolean);\n\t}\n}\n\nexport class PermissionDeniedError extends Error {\n\treadonly status = 403;\n\n\tconstructor(operation: string, permissionKey: string) {\n\t\tsuper(`Permission denied: ${operation}:${permissionKey}`);\n\t\tthis.name = 'PermissionDeniedError';\n\t}\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IPermissionsConfig } from './config';\nimport { PermissionsEngine } from './engine';\n\nexport const PERMISSIONS_ENGINE_KEY = 'fonderie.permissions.engine';\n\nexport class PermissionsModule implements IFonderieModule {\n\treadonly engine: PermissionsEngine;\n\treadonly name = '@fonderie/permissions';\n\treadonly deps = ['@fonderie/auth'];\n\n\tconstructor(store: IStoreAdapter, config: IPermissionsConfig = {}) {\n\t\tthis.engine = new PermissionsEngine(store, config);\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\tconst engine = this.engine;\n\n\t\t// Inject engine into every request context\n\t\t// requirePermission() reads it from here — no manual passing needed\n\t\tapp.use(async (ctx, next) => {\n\t\t\tctx.meta[PERMISSIONS_ENGINE_KEY] = engine;\n\t\t\treturn next();\n\t\t});\n\t}\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\n\nimport type { Operation } from '../types';\nimport { PermissionsEngine } from '../engine';\nimport { PERMISSIONS_ENGINE_KEY } from '../module';\n\nfunction makeHandler(operation: Operation, permissionKey: string): Middleware {\n\treturn async (ctx, next) => {\n\t\tif (!ctx.user) {\n\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t}\n\n\t\tconst engine = ctx.meta[PERMISSIONS_ENGINE_KEY];\n\t\tif (!(engine instanceof PermissionsEngine)) {\n\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Permissions module not installed');\n\t\t}\n\n\t\tconst workspaceId = resolveWorkspaceId(ctx);\n\t\tif (!workspaceId) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'WORKSPACE_REQUIRED', 'Workspace context required');\n\t\t}\n\n\t\tconst allowed = await engine.can(ctx.user.id, operation, permissionKey, workspaceId);\n\t\tif (!allowed) {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t'FORBIDDEN',\n\t\t\t\t`Permission denied: ${operation}:${permissionKey}`,\n\t\t\t);\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\nexport function requirePermission(operation: Operation, permissionKey: string): Middleware;\nexport function requirePermission(\n\toperation: Operation,\n\tpermissionKey: string,\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n): Promise<Response>;\nexport function requirePermission(\n\toperation: Operation,\n\tpermissionKey: string,\n\tctx?: IFonderieContext,\n\tnext?: () => Promise<Response>,\n): Middleware | Promise<Response> {\n\tconst handler = makeHandler(operation, permissionKey);\n\tif (ctx !== undefined && next !== undefined) return handler(ctx, next);\n\treturn handler;\n}\n\nfunction resolveWorkspaceId(ctx: {\n\tworkspace?: { id: string } | null;\n\tmeta: Record<string, unknown>;\n}): string | null {\n\tif (ctx.workspace?.id) return ctx.workspace.id;\n\n\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\treturn params?.['workspaceId'] ?? null;\n}\n"],"mappings":"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","names":["import_core","import_core","makeHandler"]}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { Middleware, IFonderieContext } from '@fonderie/core';
|
|
1
|
+
import { Middleware, IFonderieContext, Operation } from '@fonderie/core';
|
|
2
2
|
import { IStoreAdapter } from '@fonderie/store';
|
|
3
|
-
import { Operation } from '../types.cjs';
|
|
4
3
|
|
|
5
4
|
declare function requireRole(roleName: string | string[], store: IStoreAdapter): Middleware;
|
|
6
5
|
declare function requireRole(roleName: string | string[], store: IStoreAdapter, ctx: IFonderieContext, next: () => Promise<Response>): Promise<Response>;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { Middleware, IFonderieContext } from '@fonderie/core';
|
|
1
|
+
import { Middleware, IFonderieContext, Operation } from '@fonderie/core';
|
|
2
2
|
import { IStoreAdapter } from '@fonderie/store';
|
|
3
|
-
import { Operation } from '../types.js';
|
|
4
3
|
|
|
5
4
|
declare function requireRole(roleName: string | string[], store: IStoreAdapter): Middleware;
|
|
6
5
|
declare function requireRole(roleName: string | string[], store: IStoreAdapter, ctx: IFonderieContext, next: () => Promise<Response>): Promise<Response>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/middlewares/require-role.ts","../../src/services/membership.ts","../../src/constants.ts","../../src/services/permissions.ts","../../src/engine.ts","../../src/module.ts","../../src/middlewares/require-permission.ts"],"sourcesContent":["import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { PermissionsEngine } from '../engine';\nimport { PERMISSIONS_ENGINE_KEY } from '../module';\nimport { getMembership } from '../services/membership';\n\nfunction makeHandler(roleName: string | string[], store: IStoreAdapter): Middleware {\n\tconst allowed = Array.isArray(roleName) ? roleName : [roleName];\n\n\treturn async (ctx, next) => {\n\t\tif (!ctx.user) {\n\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t}\n\n\t\tconst engine = ctx.meta[PERMISSIONS_ENGINE_KEY];\n\t\tif (!(engine instanceof PermissionsEngine)) {\n\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Permissions module not installed');\n\t\t}\n\n\t\tconst workspaceId =\n\t\t\tctx.workspace?.id ??\n\t\t\t(ctx.meta['params'] as Record<string, string> | undefined)?.['workspaceId'];\n\n\t\tif (!workspaceId) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'WORKSPACE_REQUIRED', 'Workspace context required');\n\t\t}\n\n\t\tconst membership = await getMembership(ctx.user.id, workspaceId, store);\n\t\tif (!membership || !allowed.includes(membership.roleName)) {\n\t\t\treturn setApiResponse(HTTP.FORBIDDEN, 'FORBIDDEN', 'Insufficient role');\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\nexport function requireRole(roleName: string | string[], store: IStoreAdapter): Middleware;\nexport function requireRole(\n\troleName: string | string[],\n\tstore: IStoreAdapter,\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n): Promise<Response>;\nexport function requireRole(\n\troleName: string | string[],\n\tstore: IStoreAdapter,\n\tctx?: IFonderieContext,\n\tnext?: () => Promise<Response>,\n): Middleware | Promise<Response> {\n\tconst handler = makeHandler(roleName, store);\n\tif (ctx !== undefined && next !== undefined) return handler(ctx, next);\n\treturn handler;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IMembership } from '../types';\n\nexport async function getMembership(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<IMembership | null> {\n\tconst [row] = await store.query<{\n\t\tuser_id: string;\n\t\tworkspace_id: string;\n\t\trole_id: string;\n\t\trole_name: string;\n\t}>(\n\t\t`SELECT ruw.user_id, ruw.workspace_id, ruw.role_id, r.name AS role_name\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON r.id = ruw.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t LIMIT 1`,\n\t\t[userId, workspaceId],\n\t);\n\n\tif (!row) return null;\n\n\treturn {\n\t\tuserId: row.user_id,\n\t\tworkspaceId: row.workspace_id,\n\t\troleId: row.role_id,\n\t\troleName: row.role_name,\n\t};\n}\n\nexport async function hasRole(\n\tuserId: string,\n\tworkspaceId: string,\n\troleName: string,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst [row] = await store.query<{ exists: boolean }>(\n\t\t`SELECT EXISTS (\n\t\t SELECT 1\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON r.id = ruw.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND r.name = $3\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS exists`,\n\t\t[userId, workspaceId, roleName],\n\t);\n\n\treturn row?.exists ?? false;\n}\n","import type { Operation } from './types';\n\nexport const OPERATIONS = {\n\tCREATE: 'create',\n\tREAD: 'read',\n\tUPDATE: 'update',\n\tDELETE: 'delete',\n} as const satisfies Record<string, Operation>;\n\nexport const PERMISSION_COLUMN: Record<Operation, string> = {\n\tcreate: 'can_create',\n\tread: 'can_read',\n\tupdate: 'can_update',\n\tdelete: 'can_delete',\n};\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation } from '../types';\nimport { PERMISSION_COLUMN } from '../constants';\n\nexport async function checkPermission(\n\tuserId: string,\n\tworkspaceId: string,\n\tpermissionKey: string,\n\toperation: Operation,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst col = PERMISSION_COLUMN[operation];\n\n\tconst [row] = await store.query<{ has_permission: boolean }>(\n\t\t`SELECT BOOL_OR(rp.${col}) AS has_permission\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON ruw.role_id = r.id\n\t\t JOIN fonderie_role_permissions rp ON r.id = rp.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND rp.workspace_id = $2\n\t\t AND rp.permission_key = $3\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t AND (r.workspace_id = $2 OR r.is_system = true)\n\t\t GROUP BY ruw.user_id, ruw.workspace_id`,\n\t\t[userId, workspaceId, permissionKey],\n\t);\n\n\treturn row?.has_permission ?? false;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation } from './types';\nimport type { IPermissionsConfig } from './config';\nimport { getMembership, hasRole } from './services/membership';\nimport { checkPermission } from './services/permissions';\n\nexport class PermissionsEngine {\n\tprivate superRole: string;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tconfig: IPermissionsConfig = {},\n\t) {\n\t\tthis.superRole = config.superRole ?? 'owner';\n\t}\n\n\tasync getMembership(userId: string, workspaceId: string) {\n\t\treturn getMembership(userId, workspaceId, this.store);\n\t}\n\n\tasync can(\n\t\tuserId: string,\n\t\toperation: Operation,\n\t\tpermissionKey: string,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\treturn checkPermission(userId, workspaceId, permissionKey, operation, this.store);\n\t}\n\n\tasync assert(\n\t\tuserId: string,\n\t\toperation: Operation,\n\t\tpermissionKey: string,\n\t\tworkspaceId: string,\n\t): Promise<void> {\n\t\tconst allowed = await this.can(userId, operation, permissionKey, workspaceId);\n\t\tif (!allowed) {\n\t\t\tthrow new PermissionDeniedError(operation, permissionKey);\n\t\t}\n\t}\n\n\tasync canAll(\n\t\tuserId: string,\n\t\tchecks: Array<{ operation: Operation; permissionKey: string }>,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\tconst results = await Promise.all(\n\t\t\tchecks.map((c) =>\n\t\t\t\tcheckPermission(userId, workspaceId, c.permissionKey, c.operation, this.store),\n\t\t\t),\n\t\t);\n\t\treturn results.every(Boolean);\n\t}\n\n\tasync canAny(\n\t\tuserId: string,\n\t\tchecks: Array<{ operation: Operation; permissionKey: string }>,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\tconst results = await Promise.all(\n\t\t\tchecks.map((c) =>\n\t\t\t\tcheckPermission(userId, workspaceId, c.permissionKey, c.operation, this.store),\n\t\t\t),\n\t\t);\n\t\treturn results.some(Boolean);\n\t}\n}\n\nexport class PermissionDeniedError extends Error {\n\treadonly status = 403;\n\n\tconstructor(operation: string, permissionKey: string) {\n\t\tsuper(`Permission denied: ${operation}:${permissionKey}`);\n\t\tthis.name = 'PermissionDeniedError';\n\t}\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IPermissionsConfig } from './config';\nimport { PermissionsEngine } from './engine';\n\nexport const PERMISSIONS_ENGINE_KEY = 'fonderie.permissions.engine';\n\nexport class PermissionsModule implements IFonderieModule {\n\treadonly engine: PermissionsEngine;\n\treadonly name = '@fonderie/permissions';\n\treadonly deps = ['@fonderie/auth'];\n\n\tconstructor(store: IStoreAdapter, config: IPermissionsConfig = {}) {\n\t\tthis.engine = new PermissionsEngine(store, config);\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\tconst engine = this.engine;\n\n\t\t// Inject engine into every request context\n\t\t// requirePermission() reads it from here — no manual passing needed\n\t\tapp.use(async (ctx, next) => {\n\t\t\tctx.meta[PERMISSIONS_ENGINE_KEY] = engine;\n\t\t\treturn next();\n\t\t});\n\t}\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\n\nimport type { Operation } from '../types';\nimport { PermissionsEngine } from '../engine';\nimport { PERMISSIONS_ENGINE_KEY } from '../module';\n\nfunction makeHandler(operation: Operation, permissionKey: string): Middleware {\n\treturn async (ctx, next) => {\n\t\tif (!ctx.user) {\n\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t}\n\n\t\tconst engine = ctx.meta[PERMISSIONS_ENGINE_KEY];\n\t\tif (!(engine instanceof PermissionsEngine)) {\n\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Permissions module not installed');\n\t\t}\n\n\t\tconst workspaceId = resolveWorkspaceId(ctx);\n\t\tif (!workspaceId) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'WORKSPACE_REQUIRED', 'Workspace context required');\n\t\t}\n\n\t\tconst allowed = await engine.can(ctx.user.id, operation, permissionKey, workspaceId);\n\t\tif (!allowed) {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t'FORBIDDEN',\n\t\t\t\t`Permission denied: ${operation}:${permissionKey}`,\n\t\t\t);\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\nexport function requirePermission(operation: Operation, permissionKey: string): Middleware;\nexport function requirePermission(\n\toperation: Operation,\n\tpermissionKey: string,\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n): Promise<Response>;\nexport function requirePermission(\n\toperation: Operation,\n\tpermissionKey: string,\n\tctx?: IFonderieContext,\n\tnext?: () => Promise<Response>,\n): Middleware | Promise<Response> {\n\tconst handler = makeHandler(operation, permissionKey);\n\tif (ctx !== undefined && next !== undefined) return handler(ctx, next);\n\treturn handler;\n}\n\nfunction resolveWorkspaceId(ctx: {\n\tworkspace?: { id: string } | null;\n\tmeta: Record<string, unknown>;\n}): string | null {\n\tif (ctx.workspace?.id) return ctx.workspace.id;\n\n\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\treturn params?.['workspaceId'] ?? null;\n}\n"],"mappings":"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","names":["setApiResponse","HTTP","makeHandler","setApiResponse","HTTP"]}
|
|
1
|
+
{"version":3,"sources":["../../src/middlewares/require-role.ts","../../src/services/membership.ts","../../src/constants.ts","../../src/services/permissions.ts","../../src/engine.ts","../../src/module.ts","../../src/middlewares/require-permission.ts"],"sourcesContent":["import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { PermissionsEngine } from '../engine';\nimport { PERMISSIONS_ENGINE_KEY } from '../module';\nimport { getMembership } from '../services/membership';\n\nfunction makeHandler(roleName: string | string[], store: IStoreAdapter): Middleware {\n\tconst allowed = Array.isArray(roleName) ? roleName : [roleName];\n\n\treturn async (ctx, next) => {\n\t\tif (!ctx.user) {\n\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t}\n\n\t\tconst engine = ctx.meta[PERMISSIONS_ENGINE_KEY];\n\t\tif (!(engine instanceof PermissionsEngine)) {\n\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Permissions module not installed');\n\t\t}\n\n\t\tconst workspaceId =\n\t\t\tctx.workspace?.id ??\n\t\t\t(ctx.meta['params'] as Record<string, string> | undefined)?.['workspaceId'];\n\n\t\tif (!workspaceId) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'WORKSPACE_REQUIRED', 'Workspace context required');\n\t\t}\n\n\t\tconst membership = await getMembership(ctx.user.id, workspaceId, store);\n\t\tif (!membership || !allowed.includes(membership.roleName)) {\n\t\t\treturn setApiResponse(HTTP.FORBIDDEN, 'FORBIDDEN', 'Insufficient role');\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\nexport function requireRole(roleName: string | string[], store: IStoreAdapter): Middleware;\nexport function requireRole(\n\troleName: string | string[],\n\tstore: IStoreAdapter,\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n): Promise<Response>;\nexport function requireRole(\n\troleName: string | string[],\n\tstore: IStoreAdapter,\n\tctx?: IFonderieContext,\n\tnext?: () => Promise<Response>,\n): Middleware | Promise<Response> {\n\tconst handler = makeHandler(roleName, store);\n\tif (ctx !== undefined && next !== undefined) return handler(ctx, next);\n\treturn handler;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IMembership } from '../types';\n\nexport async function getMembership(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<IMembership | null> {\n\tconst [row] = await store.query<{\n\t\tuser_id: string;\n\t\tworkspace_id: string;\n\t\trole_id: string;\n\t\trole_name: string;\n\t}>(\n\t\t`SELECT ruw.user_id, ruw.workspace_id, ruw.role_id, r.name AS role_name\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON r.id = ruw.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t LIMIT 1`,\n\t\t[userId, workspaceId],\n\t);\n\n\tif (!row) return null;\n\n\treturn {\n\t\tuserId: row.user_id,\n\t\tworkspaceId: row.workspace_id,\n\t\troleId: row.role_id,\n\t\troleName: row.role_name,\n\t};\n}\n\nexport async function hasRole(\n\tuserId: string,\n\tworkspaceId: string,\n\troleName: string,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst [row] = await store.query<{ exists: boolean }>(\n\t\t`SELECT EXISTS (\n\t\t SELECT 1\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON r.id = ruw.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND r.name = $3\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS exists`,\n\t\t[userId, workspaceId, roleName],\n\t);\n\n\treturn row?.exists ?? false;\n}\n","import type { Operation } from './types';\n\n// Canonical definition moved to @fonderie/core; re-exported here for back-compat.\nexport { OPERATIONS } from '@fonderie/core';\n\nexport const PERMISSION_COLUMN: Record<Operation, string> = {\n\tcreate: 'can_create',\n\tread: 'can_read',\n\tupdate: 'can_update',\n\tdelete: 'can_delete',\n};\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation } from '../types';\nimport { PERMISSION_COLUMN } from '../constants';\n\nexport async function checkPermission(\n\tuserId: string,\n\tworkspaceId: string,\n\tpermissionKey: string,\n\toperation: Operation,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst col = PERMISSION_COLUMN[operation];\n\n\tconst [row] = await store.query<{ has_permission: boolean }>(\n\t\t`SELECT BOOL_OR(rp.${col}) AS has_permission\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON ruw.role_id = r.id\n\t\t JOIN fonderie_role_permissions rp ON r.id = rp.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND rp.workspace_id = $2\n\t\t AND rp.permission_key = $3\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t AND (r.workspace_id = $2 OR r.is_system = true)\n\t\t GROUP BY ruw.user_id, ruw.workspace_id`,\n\t\t[userId, workspaceId, permissionKey],\n\t);\n\n\treturn row?.has_permission ?? false;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation } from './types';\nimport type { IPermissionsConfig } from './config';\nimport { getMembership, hasRole } from './services/membership';\nimport { checkPermission } from './services/permissions';\n\nexport class PermissionsEngine {\n\tprivate superRole: string;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tconfig: IPermissionsConfig = {},\n\t) {\n\t\tthis.superRole = config.superRole ?? 'owner';\n\t}\n\n\tasync getMembership(userId: string, workspaceId: string) {\n\t\treturn getMembership(userId, workspaceId, this.store);\n\t}\n\n\tasync can(\n\t\tuserId: string,\n\t\toperation: Operation,\n\t\tpermissionKey: string,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\treturn checkPermission(userId, workspaceId, permissionKey, operation, this.store);\n\t}\n\n\tasync assert(\n\t\tuserId: string,\n\t\toperation: Operation,\n\t\tpermissionKey: string,\n\t\tworkspaceId: string,\n\t): Promise<void> {\n\t\tconst allowed = await this.can(userId, operation, permissionKey, workspaceId);\n\t\tif (!allowed) {\n\t\t\tthrow new PermissionDeniedError(operation, permissionKey);\n\t\t}\n\t}\n\n\tasync canAll(\n\t\tuserId: string,\n\t\tchecks: Array<{ operation: Operation; permissionKey: string }>,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\tconst results = await Promise.all(\n\t\t\tchecks.map((c) =>\n\t\t\t\tcheckPermission(userId, workspaceId, c.permissionKey, c.operation, this.store),\n\t\t\t),\n\t\t);\n\t\treturn results.every(Boolean);\n\t}\n\n\tasync canAny(\n\t\tuserId: string,\n\t\tchecks: Array<{ operation: Operation; permissionKey: string }>,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\tconst results = await Promise.all(\n\t\t\tchecks.map((c) =>\n\t\t\t\tcheckPermission(userId, workspaceId, c.permissionKey, c.operation, this.store),\n\t\t\t),\n\t\t);\n\t\treturn results.some(Boolean);\n\t}\n}\n\nexport class PermissionDeniedError extends Error {\n\treadonly status = 403;\n\n\tconstructor(operation: string, permissionKey: string) {\n\t\tsuper(`Permission denied: ${operation}:${permissionKey}`);\n\t\tthis.name = 'PermissionDeniedError';\n\t}\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IPermissionsConfig } from './config';\nimport { PermissionsEngine } from './engine';\n\nexport const PERMISSIONS_ENGINE_KEY = 'fonderie.permissions.engine';\n\nexport class PermissionsModule implements IFonderieModule {\n\treadonly engine: PermissionsEngine;\n\treadonly name = '@fonderie/permissions';\n\treadonly deps = ['@fonderie/auth'];\n\n\tconstructor(store: IStoreAdapter, config: IPermissionsConfig = {}) {\n\t\tthis.engine = new PermissionsEngine(store, config);\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\tconst engine = this.engine;\n\n\t\t// Inject engine into every request context\n\t\t// requirePermission() reads it from here — no manual passing needed\n\t\tapp.use(async (ctx, next) => {\n\t\t\tctx.meta[PERMISSIONS_ENGINE_KEY] = engine;\n\t\t\treturn next();\n\t\t});\n\t}\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\n\nimport type { Operation } from '../types';\nimport { PermissionsEngine } from '../engine';\nimport { PERMISSIONS_ENGINE_KEY } from '../module';\n\nfunction makeHandler(operation: Operation, permissionKey: string): Middleware {\n\treturn async (ctx, next) => {\n\t\tif (!ctx.user) {\n\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t}\n\n\t\tconst engine = ctx.meta[PERMISSIONS_ENGINE_KEY];\n\t\tif (!(engine instanceof PermissionsEngine)) {\n\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Permissions module not installed');\n\t\t}\n\n\t\tconst workspaceId = resolveWorkspaceId(ctx);\n\t\tif (!workspaceId) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'WORKSPACE_REQUIRED', 'Workspace context required');\n\t\t}\n\n\t\tconst allowed = await engine.can(ctx.user.id, operation, permissionKey, workspaceId);\n\t\tif (!allowed) {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t'FORBIDDEN',\n\t\t\t\t`Permission denied: ${operation}:${permissionKey}`,\n\t\t\t);\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\nexport function requirePermission(operation: Operation, permissionKey: string): Middleware;\nexport function requirePermission(\n\toperation: Operation,\n\tpermissionKey: string,\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n): Promise<Response>;\nexport function requirePermission(\n\toperation: Operation,\n\tpermissionKey: string,\n\tctx?: IFonderieContext,\n\tnext?: () => Promise<Response>,\n): Middleware | Promise<Response> {\n\tconst handler = makeHandler(operation, permissionKey);\n\tif (ctx !== undefined && next !== undefined) return handler(ctx, next);\n\treturn handler;\n}\n\nfunction resolveWorkspaceId(ctx: {\n\tworkspace?: { id: string } | null;\n\tmeta: Record<string, unknown>;\n}): string | null {\n\tif (ctx.workspace?.id) return ctx.workspace.id;\n\n\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\treturn params?.['workspaceId'] ?? null;\n}\n"],"mappings":"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","names":["setApiResponse","HTTP","makeHandler","setApiResponse","HTTP"]}
|
package/dist/types.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["// Canonical definition moved to @fonderie/core; re-exported here for back-compat.\nexport type { Operation } from '@fonderie/core';\nexport type PermissionKey = string;\n\nexport interface IPermission {\n\tpermissionKey: PermissionKey;\n\tcanCreate: boolean;\n\tcanRead: boolean;\n\tcanUpdate: boolean;\n\tcanDelete: boolean;\n}\n\nexport interface IRole {\n\tid: string;\n\tname: string;\n\tisSystem: boolean;\n\tworkspaceId: string | null;\n}\n\nexport interface IRoleWithPermissions extends IRole {\n\tpermissions: IPermission[];\n}\n\nexport interface IMembership {\n\tuserId: string;\n\tworkspaceId: string;\n\troleId: string;\n\troleName: string;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
|
package/dist/types.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
export { Operation } from '@fonderie/core';
|
|
2
|
+
|
|
2
3
|
type PermissionKey = string;
|
|
3
4
|
interface IPermission {
|
|
4
5
|
permissionKey: PermissionKey;
|
|
@@ -23,4 +24,4 @@ interface IMembership {
|
|
|
23
24
|
roleName: string;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
export type { IMembership, IPermission, IRole, IRoleWithPermissions,
|
|
27
|
+
export type { IMembership, IPermission, IRole, IRoleWithPermissions, PermissionKey };
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
export { Operation } from '@fonderie/core';
|
|
2
|
+
|
|
2
3
|
type PermissionKey = string;
|
|
3
4
|
interface IPermission {
|
|
4
5
|
permissionKey: PermissionKey;
|
|
@@ -23,4 +24,4 @@ interface IMembership {
|
|
|
23
24
|
roleName: string;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
export type { IMembership, IPermission, IRole, IRoleWithPermissions,
|
|
27
|
+
export type { IMembership, IPermission, IRole, IRoleWithPermissions, PermissionKey };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fonderie/permissions",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Role-based access control for SaaS — define roles, assign CRUD permissions per resource, and gate any route with requirePermission. Supports multiple roles per user via BOOL_OR aggregation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fonderie-js",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"module": "./dist/index.js",
|
|
48
48
|
"types": "./dist/index.d.ts",
|
|
49
49
|
"scripts": {
|
|
50
|
-
"build": "tsup &&
|
|
50
|
+
"build": "tsup && tsup --config tsup.migrations.ts",
|
|
51
51
|
"dev": "tsup --watch",
|
|
52
52
|
"typecheck": "tsc --noEmit",
|
|
53
53
|
"test": "tsx --test src/__tests__/*.test.ts",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"check": "biome check --write src"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@fonderie/core": "^0.1.
|
|
60
|
-
"@fonderie/store": "^0.1.
|
|
59
|
+
"@fonderie/core": "^0.1.1",
|
|
60
|
+
"@fonderie/store": "^0.1.1"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@fonderie/core": "../core",
|
|
@@ -72,16 +72,17 @@
|
|
|
72
72
|
},
|
|
73
73
|
"files": [
|
|
74
74
|
"dist",
|
|
75
|
+
"brain",
|
|
75
76
|
"LICENSE",
|
|
76
77
|
"README.md"
|
|
77
78
|
],
|
|
78
79
|
"repository": {
|
|
79
80
|
"type": "git",
|
|
80
|
-
"url": "git+https://github.com/
|
|
81
|
+
"url": "git+https://github.com/fonderiejs/sdk.git",
|
|
81
82
|
"directory": "packages/permissions"
|
|
82
83
|
},
|
|
83
|
-
"homepage": "https://github.com/
|
|
84
|
+
"homepage": "https://github.com/fonderiejs/sdk/tree/main/packages/permissions#readme",
|
|
84
85
|
"bugs": {
|
|
85
|
-
"url": "https://github.com/
|
|
86
|
+
"url": "https://github.com/fonderiejs/sdk/issues"
|
|
86
87
|
}
|
|
87
88
|
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
-- fonderie_role_permissions: CRUD bit flags per role per resource key
|
|
2
|
-
CREATE TABLE IF NOT EXISTS fonderie_role_permissions (
|
|
3
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
4
|
-
role_id UUID NOT NULL,
|
|
5
|
-
workspace_id UUID,
|
|
6
|
-
permission_key TEXT NOT NULL,
|
|
7
|
-
can_create BOOLEAN NOT NULL DEFAULT false,
|
|
8
|
-
can_read BOOLEAN NOT NULL DEFAULT false,
|
|
9
|
-
can_update BOOLEAN NOT NULL DEFAULT false,
|
|
10
|
-
can_delete BOOLEAN NOT NULL DEFAULT false,
|
|
11
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
12
|
-
UNIQUE (role_id, permission_key)
|
|
13
|
-
);
|
|
14
|
-
|
|
15
|
-
CREATE INDEX IF NOT EXISTS idx_frp_role_id ON fonderie_role_permissions (role_id);
|
|
16
|
-
CREATE INDEX IF NOT EXISTS idx_frp_workspace_id ON fonderie_role_permissions (workspace_id);
|
|
17
|
-
CREATE INDEX IF NOT EXISTS idx_frp_permission_key ON fonderie_role_permissions (permission_key);
|