@fonderie/permissions 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/config.cjs +19 -0
- package/dist/config.cjs.map +1 -0
- package/dist/config.d.cts +6 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -0
- package/dist/index.cjs +253 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +45 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +220 -0
- package/dist/index.js.map +1 -0
- package/dist/middlewares/index.cjs +224 -0
- package/dist/middlewares/index.cjs.map +1 -0
- package/dist/middlewares/index.d.cts +11 -0
- package/dist/middlewares/index.d.ts +11 -0
- package/dist/middlewares/index.js +196 -0
- package/dist/middlewares/index.js.map +1 -0
- package/dist/migrations/index.js +7 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/sql/001_permissions.sql +17 -0
- package/dist/types.cjs +19 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.cts +26 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +87 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// src/middlewares/require-role.ts
|
|
2
|
+
import { setApiResponse, HTTP } from "@fonderie/core";
|
|
3
|
+
|
|
4
|
+
// src/services/membership.ts
|
|
5
|
+
async function getMembership(userId, workspaceId, store) {
|
|
6
|
+
const [row] = await store.query(
|
|
7
|
+
`SELECT ruw.user_id, ruw.workspace_id, ruw.role_id, r.name AS role_name
|
|
8
|
+
FROM fonderie_role_user_workspaces ruw
|
|
9
|
+
JOIN fonderie_roles r ON r.id = ruw.role_id
|
|
10
|
+
WHERE ruw.user_id = $1
|
|
11
|
+
AND ruw.workspace_id = $2
|
|
12
|
+
AND ruw.removed = false
|
|
13
|
+
AND ruw.suspended = false
|
|
14
|
+
LIMIT 1`,
|
|
15
|
+
[userId, workspaceId]
|
|
16
|
+
);
|
|
17
|
+
if (!row) return null;
|
|
18
|
+
return {
|
|
19
|
+
userId: row.user_id,
|
|
20
|
+
workspaceId: row.workspace_id,
|
|
21
|
+
roleId: row.role_id,
|
|
22
|
+
roleName: row.role_name
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
async function hasRole(userId, workspaceId, roleName, store) {
|
|
26
|
+
const [row] = await store.query(
|
|
27
|
+
`SELECT EXISTS (
|
|
28
|
+
SELECT 1
|
|
29
|
+
FROM fonderie_role_user_workspaces ruw
|
|
30
|
+
JOIN fonderie_roles r ON r.id = ruw.role_id
|
|
31
|
+
WHERE ruw.user_id = $1
|
|
32
|
+
AND ruw.workspace_id = $2
|
|
33
|
+
AND r.name = $3
|
|
34
|
+
AND ruw.removed = false
|
|
35
|
+
AND ruw.suspended = false
|
|
36
|
+
) AS exists`,
|
|
37
|
+
[userId, workspaceId, roleName]
|
|
38
|
+
);
|
|
39
|
+
return row?.exists ?? false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/constants.ts
|
|
43
|
+
var PERMISSION_COLUMN = {
|
|
44
|
+
create: "can_create",
|
|
45
|
+
read: "can_read",
|
|
46
|
+
update: "can_update",
|
|
47
|
+
delete: "can_delete"
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/services/permissions.ts
|
|
51
|
+
async function checkPermission(userId, workspaceId, permissionKey, operation, store) {
|
|
52
|
+
const col = PERMISSION_COLUMN[operation];
|
|
53
|
+
const [row] = await store.query(
|
|
54
|
+
`SELECT BOOL_OR(rp.${col}) AS has_permission
|
|
55
|
+
FROM fonderie_role_user_workspaces ruw
|
|
56
|
+
JOIN fonderie_roles r ON ruw.role_id = r.id
|
|
57
|
+
JOIN fonderie_role_permissions rp ON r.id = rp.role_id
|
|
58
|
+
WHERE ruw.user_id = $1
|
|
59
|
+
AND ruw.workspace_id = $2
|
|
60
|
+
AND rp.workspace_id = $2
|
|
61
|
+
AND rp.permission_key = $3
|
|
62
|
+
AND ruw.removed = false
|
|
63
|
+
AND ruw.suspended = false
|
|
64
|
+
AND (r.workspace_id = $2 OR r.is_system = true)
|
|
65
|
+
GROUP BY ruw.user_id, ruw.workspace_id`,
|
|
66
|
+
[userId, workspaceId, permissionKey]
|
|
67
|
+
);
|
|
68
|
+
return row?.has_permission ?? false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/engine.ts
|
|
72
|
+
var PermissionsEngine = class {
|
|
73
|
+
constructor(store, config = {}) {
|
|
74
|
+
this.store = store;
|
|
75
|
+
this.superRole = config.superRole ?? "owner";
|
|
76
|
+
}
|
|
77
|
+
store;
|
|
78
|
+
superRole;
|
|
79
|
+
async getMembership(userId, workspaceId) {
|
|
80
|
+
return getMembership(userId, workspaceId, this.store);
|
|
81
|
+
}
|
|
82
|
+
async can(userId, operation, permissionKey, workspaceId) {
|
|
83
|
+
const isMember = await getMembership(userId, workspaceId, this.store);
|
|
84
|
+
if (!isMember) return false;
|
|
85
|
+
if (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;
|
|
86
|
+
return checkPermission(userId, workspaceId, permissionKey, operation, this.store);
|
|
87
|
+
}
|
|
88
|
+
async assert(userId, operation, permissionKey, workspaceId) {
|
|
89
|
+
const allowed = await this.can(userId, operation, permissionKey, workspaceId);
|
|
90
|
+
if (!allowed) {
|
|
91
|
+
throw new PermissionDeniedError(operation, permissionKey);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async canAll(userId, checks, workspaceId) {
|
|
95
|
+
const isMember = await getMembership(userId, workspaceId, this.store);
|
|
96
|
+
if (!isMember) return false;
|
|
97
|
+
if (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;
|
|
98
|
+
const results = await Promise.all(
|
|
99
|
+
checks.map(
|
|
100
|
+
(c) => checkPermission(userId, workspaceId, c.permissionKey, c.operation, this.store)
|
|
101
|
+
)
|
|
102
|
+
);
|
|
103
|
+
return results.every(Boolean);
|
|
104
|
+
}
|
|
105
|
+
async canAny(userId, checks, workspaceId) {
|
|
106
|
+
const isMember = await getMembership(userId, workspaceId, this.store);
|
|
107
|
+
if (!isMember) return false;
|
|
108
|
+
if (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;
|
|
109
|
+
const results = await Promise.all(
|
|
110
|
+
checks.map(
|
|
111
|
+
(c) => checkPermission(userId, workspaceId, c.permissionKey, c.operation, this.store)
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
return results.some(Boolean);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
var PermissionDeniedError = class extends Error {
|
|
118
|
+
status = 403;
|
|
119
|
+
constructor(operation, permissionKey) {
|
|
120
|
+
super(`Permission denied: ${operation}:${permissionKey}`);
|
|
121
|
+
this.name = "PermissionDeniedError";
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// src/module.ts
|
|
126
|
+
var PERMISSIONS_ENGINE_KEY = "fonderie.permissions.engine";
|
|
127
|
+
|
|
128
|
+
// src/middlewares/require-role.ts
|
|
129
|
+
function makeHandler(roleName, store) {
|
|
130
|
+
const allowed = Array.isArray(roleName) ? roleName : [roleName];
|
|
131
|
+
return async (ctx, next) => {
|
|
132
|
+
if (!ctx.user) {
|
|
133
|
+
return setApiResponse(HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
134
|
+
}
|
|
135
|
+
const engine = ctx.meta[PERMISSIONS_ENGINE_KEY];
|
|
136
|
+
if (!(engine instanceof PermissionsEngine)) {
|
|
137
|
+
return setApiResponse(HTTP.SERVER_ERROR, "SERVER_ERROR", "Permissions module not installed");
|
|
138
|
+
}
|
|
139
|
+
const workspaceId = ctx.workspace?.id ?? ctx.meta["params"]?.["workspaceId"];
|
|
140
|
+
if (!workspaceId) {
|
|
141
|
+
return setApiResponse(HTTP.BAD_REQUEST, "WORKSPACE_REQUIRED", "Workspace context required");
|
|
142
|
+
}
|
|
143
|
+
const membership = await getMembership(ctx.user.id, workspaceId, store);
|
|
144
|
+
if (!membership || !allowed.includes(membership.roleName)) {
|
|
145
|
+
return setApiResponse(HTTP.FORBIDDEN, "FORBIDDEN", "Insufficient role");
|
|
146
|
+
}
|
|
147
|
+
return next();
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function requireRole(roleName, store, ctx, next) {
|
|
151
|
+
const handler = makeHandler(roleName, store);
|
|
152
|
+
if (ctx !== void 0 && next !== void 0) return handler(ctx, next);
|
|
153
|
+
return handler;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/middlewares/require-permission.ts
|
|
157
|
+
import { setApiResponse as setApiResponse2, HTTP as HTTP2 } from "@fonderie/core";
|
|
158
|
+
function makeHandler2(operation, permissionKey) {
|
|
159
|
+
return async (ctx, next) => {
|
|
160
|
+
if (!ctx.user) {
|
|
161
|
+
return setApiResponse2(HTTP2.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
162
|
+
}
|
|
163
|
+
const engine = ctx.meta[PERMISSIONS_ENGINE_KEY];
|
|
164
|
+
if (!(engine instanceof PermissionsEngine)) {
|
|
165
|
+
return setApiResponse2(HTTP2.SERVER_ERROR, "SERVER_ERROR", "Permissions module not installed");
|
|
166
|
+
}
|
|
167
|
+
const workspaceId = resolveWorkspaceId(ctx);
|
|
168
|
+
if (!workspaceId) {
|
|
169
|
+
return setApiResponse2(HTTP2.BAD_REQUEST, "WORKSPACE_REQUIRED", "Workspace context required");
|
|
170
|
+
}
|
|
171
|
+
const allowed = await engine.can(ctx.user.id, operation, permissionKey, workspaceId);
|
|
172
|
+
if (!allowed) {
|
|
173
|
+
return setApiResponse2(
|
|
174
|
+
HTTP2.FORBIDDEN,
|
|
175
|
+
"FORBIDDEN",
|
|
176
|
+
`Permission denied: ${operation}:${permissionKey}`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return next();
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function requirePermission(operation, permissionKey, ctx, next) {
|
|
183
|
+
const handler = makeHandler2(operation, permissionKey);
|
|
184
|
+
if (ctx !== void 0 && next !== void 0) return handler(ctx, next);
|
|
185
|
+
return handler;
|
|
186
|
+
}
|
|
187
|
+
function resolveWorkspaceId(ctx) {
|
|
188
|
+
if (ctx.workspace?.id) return ctx.workspace.id;
|
|
189
|
+
const params = ctx.meta["params"];
|
|
190
|
+
return params?.["workspaceId"] ?? null;
|
|
191
|
+
}
|
|
192
|
+
export {
|
|
193
|
+
requirePermission,
|
|
194
|
+
requireRole
|
|
195
|
+
};
|
|
196
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/migrations/index.ts"],"sourcesContent":["import { createMigrationsPath } from '@fonderie/store';\n\nexport const getMigrationsPath = (): string => createMigrationsPath(import.meta.url);\n"],"mappings":";AAAA,SAAS,4BAA4B;AAE9B,IAAM,oBAAoB,MAAc,qBAAqB,YAAY,GAAG;","names":[]}
|
|
@@ -0,0 +1,17 @@
|
|
|
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);
|
package/dist/types.cjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __copyProps = (to, from, except, desc) => {
|
|
7
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
8
|
+
for (let key of __getOwnPropNames(from))
|
|
9
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
10
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
11
|
+
}
|
|
12
|
+
return to;
|
|
13
|
+
};
|
|
14
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
15
|
+
|
|
16
|
+
// src/types.ts
|
|
17
|
+
var types_exports = {};
|
|
18
|
+
module.exports = __toCommonJS(types_exports);
|
|
19
|
+
//# sourceMappingURL=types.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["export type Operation = 'create' | 'read' | 'update' | 'delete';\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
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
type Operation = 'create' | 'read' | 'update' | 'delete';
|
|
2
|
+
type PermissionKey = string;
|
|
3
|
+
interface IPermission {
|
|
4
|
+
permissionKey: PermissionKey;
|
|
5
|
+
canCreate: boolean;
|
|
6
|
+
canRead: boolean;
|
|
7
|
+
canUpdate: boolean;
|
|
8
|
+
canDelete: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface IRole {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
isSystem: boolean;
|
|
14
|
+
workspaceId: string | null;
|
|
15
|
+
}
|
|
16
|
+
interface IRoleWithPermissions extends IRole {
|
|
17
|
+
permissions: IPermission[];
|
|
18
|
+
}
|
|
19
|
+
interface IMembership {
|
|
20
|
+
userId: string;
|
|
21
|
+
workspaceId: string;
|
|
22
|
+
roleId: string;
|
|
23
|
+
roleName: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type { IMembership, IPermission, IRole, IRoleWithPermissions, Operation, PermissionKey };
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
type Operation = 'create' | 'read' | 'update' | 'delete';
|
|
2
|
+
type PermissionKey = string;
|
|
3
|
+
interface IPermission {
|
|
4
|
+
permissionKey: PermissionKey;
|
|
5
|
+
canCreate: boolean;
|
|
6
|
+
canRead: boolean;
|
|
7
|
+
canUpdate: boolean;
|
|
8
|
+
canDelete: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface IRole {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
isSystem: boolean;
|
|
14
|
+
workspaceId: string | null;
|
|
15
|
+
}
|
|
16
|
+
interface IRoleWithPermissions extends IRole {
|
|
17
|
+
permissions: IPermission[];
|
|
18
|
+
}
|
|
19
|
+
interface IMembership {
|
|
20
|
+
userId: string;
|
|
21
|
+
workspaceId: string;
|
|
22
|
+
roleId: string;
|
|
23
|
+
roleName: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type { IMembership, IPermission, IRole, IRoleWithPermissions, Operation, PermissionKey };
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fonderie/permissions",
|
|
3
|
+
"version": "1.0.0",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"fonderie-js",
|
|
7
|
+
"rbac",
|
|
8
|
+
"permissions",
|
|
9
|
+
"access-control",
|
|
10
|
+
"roles",
|
|
11
|
+
"authorization",
|
|
12
|
+
"saas",
|
|
13
|
+
"typescript"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"require": "./dist/index.cjs"
|
|
25
|
+
},
|
|
26
|
+
"./config": {
|
|
27
|
+
"types": "./dist/config.d.ts",
|
|
28
|
+
"import": "./dist/config.js",
|
|
29
|
+
"require": "./dist/config.cjs"
|
|
30
|
+
},
|
|
31
|
+
"./types": {
|
|
32
|
+
"types": "./dist/types.d.ts",
|
|
33
|
+
"import": "./dist/types.js",
|
|
34
|
+
"require": "./dist/types.cjs"
|
|
35
|
+
},
|
|
36
|
+
"./middleware": {
|
|
37
|
+
"types": "./dist/middlewares/index.d.ts",
|
|
38
|
+
"import": "./dist/middlewares/index.js",
|
|
39
|
+
"require": "./dist/middlewares/index.cjs"
|
|
40
|
+
},
|
|
41
|
+
"./migrations": {
|
|
42
|
+
"types": "./dist/migrations/index.d.ts",
|
|
43
|
+
"import": "./dist/migrations/index.js"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"main": "./dist/index.cjs",
|
|
47
|
+
"module": "./dist/index.js",
|
|
48
|
+
"types": "./dist/index.d.ts",
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsup && node -e \"require('node:fs').cpSync('src/migrations/sql', 'dist/migrations/sql', {recursive:true})\"",
|
|
51
|
+
"dev": "tsup --watch",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"test": "tsx --test src/__tests__/*.test.ts",
|
|
54
|
+
"lint": "biome lint src",
|
|
55
|
+
"format": "biome format --write src",
|
|
56
|
+
"check": "biome check --write src"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"@fonderie/core": "^0.1.0",
|
|
60
|
+
"@fonderie/store": "^0.1.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@fonderie/core": "../core",
|
|
64
|
+
"@fonderie/store": "../store",
|
|
65
|
+
"@types/node": "^25.6.0",
|
|
66
|
+
"tsup": "^8.5.1",
|
|
67
|
+
"tsx": "^4.21.0",
|
|
68
|
+
"typescript": "^6.0.3"
|
|
69
|
+
},
|
|
70
|
+
"publishConfig": {
|
|
71
|
+
"access": "public"
|
|
72
|
+
},
|
|
73
|
+
"files": [
|
|
74
|
+
"dist",
|
|
75
|
+
"LICENSE",
|
|
76
|
+
"README.md"
|
|
77
|
+
],
|
|
78
|
+
"repository": {
|
|
79
|
+
"type": "git",
|
|
80
|
+
"url": "git+https://github.com/fonderie-js/sdk.git",
|
|
81
|
+
"directory": "packages/permissions"
|
|
82
|
+
},
|
|
83
|
+
"homepage": "https://github.com/fonderie-js/sdk/tree/main/packages/permissions#readme",
|
|
84
|
+
"bugs": {
|
|
85
|
+
"url": "https://github.com/fonderie-js/sdk/issues"
|
|
86
|
+
}
|
|
87
|
+
}
|