@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.
@@ -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,7 @@
1
+ // src/migrations/index.ts
2
+ import { createMigrationsPath } from "@fonderie/store";
3
+ var getMigrationsPath = () => createMigrationsPath(import.meta.url);
4
+ export {
5
+ getMigrationsPath
6
+ };
7
+ //# sourceMappingURL=index.js.map
@@ -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":[]}
@@ -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 };
@@ -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
+ }