@fonderie/permissions 5.1.4 → 5.1.6
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/dist/index.cjs +71 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +71 -5
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +70 -4
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.js +70 -4
- package/dist/middlewares/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -134,6 +134,65 @@ async function checkPermission(userId, workspaceId, permissionKey, operation, st
|
|
|
134
134
|
);
|
|
135
135
|
return row?.has_permission ?? false;
|
|
136
136
|
}
|
|
137
|
+
async function readAccess(userId, workspaceId, permissionKey, operation, superRole, store) {
|
|
138
|
+
const col = PERMISSION_COLUMN[operation];
|
|
139
|
+
const [row] = await store.query(
|
|
140
|
+
`SELECT
|
|
141
|
+
EXISTS (
|
|
142
|
+
SELECT 1
|
|
143
|
+
FROM fonderie_role_user_workspaces ruw
|
|
144
|
+
JOIN fonderie_roles r ON r.id = ruw.role_id
|
|
145
|
+
WHERE ruw.user_id = $1
|
|
146
|
+
AND ruw.workspace_id = $2
|
|
147
|
+
AND ruw.removed = false
|
|
148
|
+
AND ruw.suspended = false
|
|
149
|
+
) AS member,
|
|
150
|
+
EXISTS (
|
|
151
|
+
SELECT 1
|
|
152
|
+
FROM fonderie_role_user_workspaces ruw
|
|
153
|
+
JOIN fonderie_roles r ON r.id = ruw.role_id
|
|
154
|
+
WHERE ruw.user_id = $1
|
|
155
|
+
AND ruw.workspace_id = $2
|
|
156
|
+
AND r.name = $4
|
|
157
|
+
AND r.is_system = true
|
|
158
|
+
AND r.active = true
|
|
159
|
+
AND ruw.removed = false
|
|
160
|
+
AND ruw.suspended = false
|
|
161
|
+
) AS is_super,
|
|
162
|
+
ARRAY (
|
|
163
|
+
SELECT DISTINCT r.name
|
|
164
|
+
FROM fonderie_role_user_workspaces ruw
|
|
165
|
+
JOIN fonderie_roles r ON r.id = ruw.role_id
|
|
166
|
+
WHERE ruw.user_id = $1
|
|
167
|
+
AND ruw.workspace_id = $2
|
|
168
|
+
AND r.is_system = true
|
|
169
|
+
AND r.active = true
|
|
170
|
+
AND ruw.removed = false
|
|
171
|
+
AND ruw.suspended = false
|
|
172
|
+
) AS system_roles,
|
|
173
|
+
(
|
|
174
|
+
SELECT BOOL_OR(rp.${col})
|
|
175
|
+
FROM fonderie_role_user_workspaces ruw
|
|
176
|
+
JOIN fonderie_roles r ON ruw.role_id = r.id
|
|
177
|
+
JOIN fonderie_role_permissions rp ON r.id = rp.role_id
|
|
178
|
+
WHERE ruw.user_id = $1
|
|
179
|
+
AND ruw.workspace_id = $2
|
|
180
|
+
AND rp.workspace_id = $2
|
|
181
|
+
AND rp.permission_key = $3
|
|
182
|
+
AND ruw.removed = false
|
|
183
|
+
AND ruw.suspended = false
|
|
184
|
+
AND r.active = true
|
|
185
|
+
AND (r.workspace_id = $2 OR r.is_system = true)
|
|
186
|
+
) AS granted`,
|
|
187
|
+
[userId, workspaceId, permissionKey, superRole]
|
|
188
|
+
);
|
|
189
|
+
return {
|
|
190
|
+
member: row?.member === true,
|
|
191
|
+
isSuper: row?.is_super === true,
|
|
192
|
+
systemRoles: row?.system_roles ?? [],
|
|
193
|
+
granted: row?.granted === true
|
|
194
|
+
};
|
|
195
|
+
}
|
|
137
196
|
async function listGrantedPermissions(userId, workspaceId, store) {
|
|
138
197
|
return store.query(
|
|
139
198
|
`SELECT rp.permission_key AS "permissionKey",
|
|
@@ -231,10 +290,17 @@ var PermissionsEngine = class {
|
|
|
231
290
|
return getMembership(userId, workspaceId, this.store);
|
|
232
291
|
}
|
|
233
292
|
async can(userId, operation, permissionKey, workspaceId) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
293
|
+
if (!Object.hasOwn(PERMISSION_COLUMN, operation)) {
|
|
294
|
+
const isMember = await getMembership(userId, workspaceId, this.store);
|
|
295
|
+
if (!isMember) return false;
|
|
296
|
+
if (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;
|
|
297
|
+
return this.allows(userId, workspaceId, permissionKey, operation);
|
|
298
|
+
}
|
|
299
|
+
const a = await readAccess(userId, workspaceId, permissionKey, operation, this.superRole, this.store);
|
|
300
|
+
if (!a.member) return false;
|
|
301
|
+
if (a.isSuper) return true;
|
|
302
|
+
if (a.systemRoles.some((r) => this.systemGrants[r]?.[permissionKey]?.includes(operation) ?? false)) return true;
|
|
303
|
+
return a.granted;
|
|
238
304
|
}
|
|
239
305
|
async assert(userId, operation, permissionKey, workspaceId) {
|
|
240
306
|
const allowed = await this.can(userId, operation, permissionKey, workspaceId);
|
|
@@ -276,7 +342,7 @@ var PermissionsModule = class {
|
|
|
276
342
|
name = "@fonderie/permissions";
|
|
277
343
|
// Baked in at build time by tsup.base, so the operator's Modules page can
|
|
278
344
|
// say what is actually deployed rather than 'not reported'.
|
|
279
|
-
version = "5.1.
|
|
345
|
+
version = "5.1.6";
|
|
280
346
|
deps = ["@fonderie/auth"];
|
|
281
347
|
constructor(store, config = {}) {
|
|
282
348
|
this.engine = new PermissionsEngine(store, config);
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/constants.ts","../src/services/membership.ts","../src/services/permissions.ts","../src/engine.ts","../src/module.ts","../src/middlewares/require-role.ts","../src/middlewares/require-permission.ts","../src/services/grants.ts"],"sourcesContent":["export type {\n\tOperation,\n\tPermissionKey,\n\tIRole,\n\tIPermission,\n\tIMembership,\n\tIRoleWithPermissions,\n\tIPermissionCatalogEntry,\n\tIEffectivePermissions,\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\n// Access-grant export for the SOC 2 access-review register.\nexport { listGrants } from './services/grants';\nexport type { IGrant } from './services/grants';\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 { 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\n/**\n * Does the user hold the SUPER-ROLE in this workspace? The engine uses this for\n * its total-access bypass, so the match is deliberately restricted to a\n * **system** role: `r.is_system = true`. Without that guard, a member could\n * create a workspace-local role whose name equals the configured super-role\n * (e.g. 'ADMIN') and self-assign it to gain full access — a privilege\n * escalation. System roles are seeded by the app, not member-creatable. Also\n * requires `active = true` so a deactivated role grants nothing.\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 r.is_system = true\n\t\t AND r.active = true\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\n/**\n * Does the user hold ANY of the named roles in this workspace (active, not\n * removed/suspended)? Used by requireRole — an EXISTS over ALL of the user's\n * role rows, so a member holding several roles isn't denied because an\n * arbitrary single row (LIMIT 1) happened to be the wrong one. Not restricted\n * to system roles: requireRole checks arbitrary named roles.\n */\nexport async function hasAnyRole(\n\tuserId: string,\n\tworkspaceId: string,\n\troleNames: string[],\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tif (roleNames.length === 0) return false;\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 = ANY($3)\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS exists`,\n\t\t[userId, workspaceId, roleNames],\n\t);\n\n\treturn row?.exists ?? false;\n}\n\n/**\n * The SYSTEM roles (shared across workspaces) the user holds here — the ones\n * whose rights come from config (systemGrants). System only, for the same\n * reason as hasRole: a member-created local role named 'GUEST' must not pick\n * up the system GUEST's rights.\n */\nexport async function listSystemRoleNames(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<string[]> {\n\tconst rows = await store.query<{ name: string }>(\n\t\t`SELECT DISTINCT r.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 r.is_system = true\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false`,\n\t\t[userId, workspaceId],\n\t);\n\treturn rows.map((r) => r.name);\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.active = true\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\n/**\n * Every grant the member's STORED roles give in this workspace, folded across\n * roles (a member with two roles gets the union) — one query for the\n * effective-permissions read instead of one check per key.\n */\nexport async function listGrantedPermissions(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<Array<{ permissionKey: string; create: boolean; read: boolean; update: boolean; delete: boolean }>> {\n\treturn store.query(\n\t\t`SELECT rp.permission_key AS \"permissionKey\",\n\t\t BOOL_OR(rp.can_create) AS \"create\",\n\t\t BOOL_OR(rp.can_read) AS \"read\",\n\t\t BOOL_OR(rp.can_update) AS \"update\",\n\t\t BOOL_OR(rp.can_delete) AS \"delete\"\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 ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t AND r.active = true\n\t\t AND (r.workspace_id = $2 OR r.is_system = true)\n\t\t GROUP BY rp.permission_key`,\n\t\t[userId, workspaceId],\n\t);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation, IPermissionCatalogEntry, IEffectivePermissions } from './types';\nimport type { IPermissionsConfig } from './config';\nimport { OPERATIONS } from './constants';\nimport { getMembership, hasRole, listSystemRoleNames } from './services/membership';\nimport { checkPermission, listGrantedPermissions } from './services/permissions';\n\nconst ALL_OPERATIONS = Object.values(OPERATIONS) as Operation[];\n\nexport class PermissionsEngine {\n\tprivate superRole: string;\n\tprivate systemGrants: Record<string, Record<string, Operation[]>>;\n\t/** The app's declared resources, or null when it declared none. */\n\treadonly catalog: readonly IPermissionCatalogEntry[] | null;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tconfig: IPermissionsConfig = {},\n\t) {\n\t\tthis.superRole = config.superRole ?? 'owner';\n\t\tthis.catalog = config.catalog ? Object.freeze(config.catalog.map((e) => ({ ...e }))) : null;\n\t\tthis.systemGrants = config.systemGrants ?? {};\n\t\t// A misspelled resource in systemGrants would grant nothing, silently.\n\t\t// Refuse to start instead — a config mistake, caught at boot.\n\t\tif (this.catalog) {\n\t\t\tconst known = new Set(this.catalog.map((e) => e.key));\n\t\t\tfor (const [role, grants] of Object.entries(this.systemGrants)) {\n\t\t\t\tfor (const key of Object.keys(grants)) {\n\t\t\t\t\tif (!known.has(key)) {\n\t\t\t\t\t\tthrow new Error(`[permissions] systemGrants.${role}.${key}: '${key}' is not in the catalog`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Is this key in the catalog? Always true when the app declared no catalog. */\n\tisKnown(permissionKey: string): boolean {\n\t\treturn !this.catalog || this.catalog.some((e) => e.key === permissionKey);\n\t}\n\n\t/** The operations a resource supports (all four when not declared). */\n\toperationsOf(permissionKey: string): Operation[] {\n\t\treturn this.catalog?.find((e) => e.key === permissionKey)?.operations ?? ALL_OPERATIONS;\n\t}\n\n\tprivate async allows(\n\t\tuserId: string,\n\t\tworkspaceId: string,\n\t\tpermissionKey: string,\n\t\toperation: Operation,\n\t): Promise<boolean> {\n\t\tif (Object.keys(this.systemGrants).length > 0) {\n\t\t\tconst roles = await listSystemRoleNames(userId, workspaceId, this.store);\n\t\t\tif (roles.some((r) => this.systemGrants[r]?.[permissionKey]?.includes(operation) ?? false)) return true;\n\t\t}\n\t\treturn checkPermission(userId, workspaceId, permissionKey, operation, this.store);\n\t}\n\n\t/**\n\t * Everything this member may do in this workspace, in one read — what a\n\t * client needs to show only the actions that will succeed. Null when the\n\t * user is not a member. Lists the catalog's resources (every operation\n\t * present, true or false) plus any granted key outside it.\n\t */\n\tasync effective(userId: string, workspaceId: string): Promise<IEffectivePermissions | null> {\n\t\tif (!(await getMembership(userId, workspaceId, this.store))) return null;\n\n\t\tconst permissions: Record<string, Record<Operation, boolean>> = {};\n\t\tconst entry = (key: string) =>\n\t\t\t(permissions[key] ??= { create: false, read: false, update: false, delete: false });\n\t\tfor (const e of this.catalog ?? []) entry(e.key);\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) {\n\t\t\tfor (const e of this.catalog ?? []) {\n\t\t\t\tfor (const op of e.operations ?? ALL_OPERATIONS) entry(e.key)[op] = true;\n\t\t\t}\n\t\t\treturn { isSuper: true, permissions };\n\t\t}\n\n\t\tconst [systemRoles, stored] = await Promise.all([\n\t\t\tObject.keys(this.systemGrants).length ? listSystemRoleNames(userId, workspaceId, this.store) : Promise.resolve([]),\n\t\t\tlistGrantedPermissions(userId, workspaceId, this.store),\n\t\t]);\n\t\tfor (const role of systemRoles) {\n\t\t\tfor (const [key, ops] of Object.entries(this.systemGrants[role] ?? {})) {\n\t\t\t\tfor (const op of ops) entry(key)[op] = true;\n\t\t\t}\n\t\t}\n\t\tfor (const g of stored) {\n\t\t\tconst e = entry(g.permissionKey);\n\t\t\tfor (const op of ALL_OPERATIONS) e[op] ||= Boolean(g[op]);\n\t\t}\n\t\treturn { isSuper: false, permissions };\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 this.allows(userId, workspaceId, permissionKey, operation);\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) => this.allows(userId, workspaceId, c.permissionKey, c.operation)),\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) => this.allows(userId, workspaceId, c.permissionKey, c.operation)),\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\t// Baked in at build time by tsup.base, so the operator's Modules page can\n\t// say what is actually deployed rather than 'not reported'.\n\treadonly version = process.env['FONDERIE_PKG_VERSION'] ?? '0.0.0-dev';\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 { hasAnyRole } 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\t// EXISTS across ALL of the user's roles — a member holding several roles\n\t\t// must not be denied because a single arbitrary row was the wrong one.\n\t\tif (!(await hasAnyRole(ctx.user.id, workspaceId, allowed, store))) {\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","import type { IStoreAdapter } from '@fonderie/store';\n\n// Access-grant export (SOC 2 CC6.2/6.3) — feeds the access-review register with\n// the current who-has-what: every active user→workspace→role grant. Run this on\n// a schedule (or during a review) and diff against the previous snapshot.\n\nexport interface IGrant {\n\tuserId: string;\n\tworkspaceId: string;\n\troleId: string;\n\troleName: string;\n\tsuspended: boolean;\n}\n\nexport async function listGrants(store: IStoreAdapter): Promise<IGrant[]> {\n\tconst rows = 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\tsuspended: boolean;\n\t}>(\n\t\t`SELECT ruw.user_id, ruw.workspace_id, ruw.role_id, r.name AS role_name, ruw.suspended\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.removed = false\n\t\t ORDER BY ruw.workspace_id, ruw.user_id`,\n\t);\n\treturn rows.map((r) => ({\n\t\tuserId: r.user_id,\n\t\tworkspaceId: r.workspace_id,\n\t\troleId: r.role_id,\n\t\troleName: r.role_name,\n\t\tsuspended: r.suspended,\n\t}));\n}\n"],"mappings":"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","names":["import_core","import_core","makeHandler"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/constants.ts","../src/services/membership.ts","../src/services/permissions.ts","../src/engine.ts","../src/module.ts","../src/middlewares/require-role.ts","../src/middlewares/require-permission.ts","../src/services/grants.ts"],"sourcesContent":["export type {\n\tOperation,\n\tPermissionKey,\n\tIRole,\n\tIPermission,\n\tIMembership,\n\tIRoleWithPermissions,\n\tIPermissionCatalogEntry,\n\tIEffectivePermissions,\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\n// Access-grant export for the SOC 2 access-review register.\nexport { listGrants } from './services/grants';\nexport type { IGrant } from './services/grants';\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 { 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\n/**\n * Does the user hold the SUPER-ROLE in this workspace? The engine uses this for\n * its total-access bypass, so the match is deliberately restricted to a\n * **system** role: `r.is_system = true`. Without that guard, a member could\n * create a workspace-local role whose name equals the configured super-role\n * (e.g. 'ADMIN') and self-assign it to gain full access — a privilege\n * escalation. System roles are seeded by the app, not member-creatable. Also\n * requires `active = true` so a deactivated role grants nothing.\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 r.is_system = true\n\t\t AND r.active = true\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\n/**\n * Does the user hold ANY of the named roles in this workspace (active, not\n * removed/suspended)? Used by requireRole — an EXISTS over ALL of the user's\n * role rows, so a member holding several roles isn't denied because an\n * arbitrary single row (LIMIT 1) happened to be the wrong one. Not restricted\n * to system roles: requireRole checks arbitrary named roles.\n */\nexport async function hasAnyRole(\n\tuserId: string,\n\tworkspaceId: string,\n\troleNames: string[],\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tif (roleNames.length === 0) return false;\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 = ANY($3)\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS exists`,\n\t\t[userId, workspaceId, roleNames],\n\t);\n\n\treturn row?.exists ?? false;\n}\n\n/**\n * The SYSTEM roles (shared across workspaces) the user holds here — the ones\n * whose rights come from config (systemGrants). System only, for the same\n * reason as hasRole: a member-created local role named 'GUEST' must not pick\n * up the system GUEST's rights.\n */\nexport async function listSystemRoleNames(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<string[]> {\n\tconst rows = await store.query<{ name: string }>(\n\t\t`SELECT DISTINCT r.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 r.is_system = true\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false`,\n\t\t[userId, workspaceId],\n\t);\n\treturn rows.map((r) => r.name);\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.active = true\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\n/**\n * Everything one permission check needs, in ONE round-trip — the same four\n * predicates as getMembership, hasRole (super-role), listSystemRoleNames and\n * checkPermission, each written exactly as there, read side by side:\n *\n * member — an active row joined to an existing role (getMembership)\n * isSuper — holds the super-role as an ACTIVE SYSTEM role (hasRole)\n * systemRoles — the active SYSTEM roles held (listSystemRoleNames)\n * granted — a stored grant for `operation` on `permissionKey` (checkPermission)\n *\n * The caller applies the same order of decisions the engine always has.\n */\nexport async function readAccess(\n\tuserId: string,\n\tworkspaceId: string,\n\tpermissionKey: string,\n\toperation: Operation,\n\tsuperRole: string,\n\tstore: IStoreAdapter,\n): Promise<{ member: boolean; isSuper: boolean; systemRoles: string[]; granted: boolean }> {\n\tconst col = PERMISSION_COLUMN[operation];\n\tconst [row] = await store.query<{ member: boolean; is_super: boolean; system_roles: string[] | null; granted: boolean | null }>(\n\t\t`SELECT\n\t\t 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 ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS member,\n\t\t 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 = $4\n\t\t AND r.is_system = true\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS is_super,\n\t\t ARRAY (\n\t\t SELECT DISTINCT r.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 r.is_system = true\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS system_roles,\n\t\t (\n\t\t SELECT BOOL_OR(rp.${col})\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.active = true\n\t\t AND (r.workspace_id = $2 OR r.is_system = true)\n\t\t ) AS granted`,\n\t\t[userId, workspaceId, permissionKey, superRole],\n\t);\n\treturn {\n\t\tmember: row?.member === true,\n\t\tisSuper: row?.is_super === true,\n\t\tsystemRoles: row?.system_roles ?? [],\n\t\tgranted: row?.granted === true,\n\t};\n}\n\n/**\n * Every grant the member's STORED roles give in this workspace, folded across\n * roles (a member with two roles gets the union) — one query for the\n * effective-permissions read instead of one check per key.\n */\nexport async function listGrantedPermissions(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<Array<{ permissionKey: string; create: boolean; read: boolean; update: boolean; delete: boolean }>> {\n\treturn store.query(\n\t\t`SELECT rp.permission_key AS \"permissionKey\",\n\t\t BOOL_OR(rp.can_create) AS \"create\",\n\t\t BOOL_OR(rp.can_read) AS \"read\",\n\t\t BOOL_OR(rp.can_update) AS \"update\",\n\t\t BOOL_OR(rp.can_delete) AS \"delete\"\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 ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t AND r.active = true\n\t\t AND (r.workspace_id = $2 OR r.is_system = true)\n\t\t GROUP BY rp.permission_key`,\n\t\t[userId, workspaceId],\n\t);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation, IPermissionCatalogEntry, IEffectivePermissions } from './types';\nimport type { IPermissionsConfig } from './config';\nimport { OPERATIONS, PERMISSION_COLUMN } from './constants';\nimport { getMembership, hasRole, listSystemRoleNames } from './services/membership';\nimport { checkPermission, listGrantedPermissions, readAccess } from './services/permissions';\n\nconst ALL_OPERATIONS = Object.values(OPERATIONS) as Operation[];\n\nexport class PermissionsEngine {\n\tprivate superRole: string;\n\tprivate systemGrants: Record<string, Record<string, Operation[]>>;\n\t/** The app's declared resources, or null when it declared none. */\n\treadonly catalog: readonly IPermissionCatalogEntry[] | null;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tconfig: IPermissionsConfig = {},\n\t) {\n\t\tthis.superRole = config.superRole ?? 'owner';\n\t\tthis.catalog = config.catalog ? Object.freeze(config.catalog.map((e) => ({ ...e }))) : null;\n\t\tthis.systemGrants = config.systemGrants ?? {};\n\t\t// A misspelled resource in systemGrants would grant nothing, silently.\n\t\t// Refuse to start instead — a config mistake, caught at boot.\n\t\tif (this.catalog) {\n\t\t\tconst known = new Set(this.catalog.map((e) => e.key));\n\t\t\tfor (const [role, grants] of Object.entries(this.systemGrants)) {\n\t\t\t\tfor (const key of Object.keys(grants)) {\n\t\t\t\t\tif (!known.has(key)) {\n\t\t\t\t\t\tthrow new Error(`[permissions] systemGrants.${role}.${key}: '${key}' is not in the catalog`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Is this key in the catalog? Always true when the app declared no catalog. */\n\tisKnown(permissionKey: string): boolean {\n\t\treturn !this.catalog || this.catalog.some((e) => e.key === permissionKey);\n\t}\n\n\t/** The operations a resource supports (all four when not declared). */\n\toperationsOf(permissionKey: string): Operation[] {\n\t\treturn this.catalog?.find((e) => e.key === permissionKey)?.operations ?? ALL_OPERATIONS;\n\t}\n\n\tprivate async allows(\n\t\tuserId: string,\n\t\tworkspaceId: string,\n\t\tpermissionKey: string,\n\t\toperation: Operation,\n\t): Promise<boolean> {\n\t\tif (Object.keys(this.systemGrants).length > 0) {\n\t\t\tconst roles = await listSystemRoleNames(userId, workspaceId, this.store);\n\t\t\tif (roles.some((r) => this.systemGrants[r]?.[permissionKey]?.includes(operation) ?? false)) return true;\n\t\t}\n\t\treturn checkPermission(userId, workspaceId, permissionKey, operation, this.store);\n\t}\n\n\t/**\n\t * Everything this member may do in this workspace, in one read — what a\n\t * client needs to show only the actions that will succeed. Null when the\n\t * user is not a member. Lists the catalog's resources (every operation\n\t * present, true or false) plus any granted key outside it.\n\t */\n\tasync effective(userId: string, workspaceId: string): Promise<IEffectivePermissions | null> {\n\t\tif (!(await getMembership(userId, workspaceId, this.store))) return null;\n\n\t\tconst permissions: Record<string, Record<Operation, boolean>> = {};\n\t\tconst entry = (key: string) =>\n\t\t\t(permissions[key] ??= { create: false, read: false, update: false, delete: false });\n\t\tfor (const e of this.catalog ?? []) entry(e.key);\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) {\n\t\t\tfor (const e of this.catalog ?? []) {\n\t\t\t\tfor (const op of e.operations ?? ALL_OPERATIONS) entry(e.key)[op] = true;\n\t\t\t}\n\t\t\treturn { isSuper: true, permissions };\n\t\t}\n\n\t\tconst [systemRoles, stored] = await Promise.all([\n\t\t\tObject.keys(this.systemGrants).length ? listSystemRoleNames(userId, workspaceId, this.store) : Promise.resolve([]),\n\t\t\tlistGrantedPermissions(userId, workspaceId, this.store),\n\t\t]);\n\t\tfor (const role of systemRoles) {\n\t\t\tfor (const [key, ops] of Object.entries(this.systemGrants[role] ?? {})) {\n\t\t\t\tfor (const op of ops) entry(key)[op] = true;\n\t\t\t}\n\t\t}\n\t\tfor (const g of stored) {\n\t\t\tconst e = entry(g.permissionKey);\n\t\t\tfor (const op of ALL_OPERATIONS) e[op] ||= Boolean(g[op]);\n\t\t}\n\t\treturn { isSuper: false, permissions };\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\t// An operation outside the four has no grant column; keep the old,\n\t\t// step-by-step path for it rather than change what it answers.\n\t\tif (!Object.hasOwn(PERMISSION_COLUMN, operation)) {\n\t\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\t\tif (!isMember) return false;\n\t\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\t\t\treturn this.allows(userId, workspaceId, permissionKey, operation);\n\t\t}\n\n\t\t// Member → super-role → config system grants → stored grants: the same\n\t\t// decisions in the same order, read in one round-trip.\n\t\tconst a = await readAccess(userId, workspaceId, permissionKey, operation, this.superRole, this.store);\n\t\tif (!a.member) return false;\n\t\tif (a.isSuper) return true;\n\t\tif (a.systemRoles.some((r) => this.systemGrants[r]?.[permissionKey]?.includes(operation) ?? false)) return true;\n\t\treturn a.granted;\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) => this.allows(userId, workspaceId, c.permissionKey, c.operation)),\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) => this.allows(userId, workspaceId, c.permissionKey, c.operation)),\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\t// Baked in at build time by tsup.base, so the operator's Modules page can\n\t// say what is actually deployed rather than 'not reported'.\n\treadonly version = process.env['FONDERIE_PKG_VERSION'] ?? '0.0.0-dev';\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 { hasAnyRole } 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\t// EXISTS across ALL of the user's roles — a member holding several roles\n\t\t// must not be denied because a single arbitrary row was the wrong one.\n\t\tif (!(await hasAnyRole(ctx.user.id, workspaceId, allowed, store))) {\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","import type { IStoreAdapter } from '@fonderie/store';\n\n// Access-grant export (SOC 2 CC6.2/6.3) — feeds the access-review register with\n// the current who-has-what: every active user→workspace→role grant. Run this on\n// a schedule (or during a review) and diff against the previous snapshot.\n\nexport interface IGrant {\n\tuserId: string;\n\tworkspaceId: string;\n\troleId: string;\n\troleName: string;\n\tsuspended: boolean;\n}\n\nexport async function listGrants(store: IStoreAdapter): Promise<IGrant[]> {\n\tconst rows = 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\tsuspended: boolean;\n\t}>(\n\t\t`SELECT ruw.user_id, ruw.workspace_id, ruw.role_id, r.name AS role_name, ruw.suspended\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.removed = false\n\t\t ORDER BY ruw.workspace_id, ruw.user_id`,\n\t);\n\treturn rows.map((r) => ({\n\t\tuserId: r.user_id,\n\t\tworkspaceId: r.workspace_id,\n\t\troleId: r.role_id,\n\t\troleName: r.role_name,\n\t\tsuspended: r.suspended,\n\t}));\n}\n"],"mappings":"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","names":["import_core","import_core","makeHandler"]}
|
package/dist/index.js
CHANGED
|
@@ -101,6 +101,65 @@ async function checkPermission(userId, workspaceId, permissionKey, operation, st
|
|
|
101
101
|
);
|
|
102
102
|
return row?.has_permission ?? false;
|
|
103
103
|
}
|
|
104
|
+
async function readAccess(userId, workspaceId, permissionKey, operation, superRole, store) {
|
|
105
|
+
const col = PERMISSION_COLUMN[operation];
|
|
106
|
+
const [row] = await store.query(
|
|
107
|
+
`SELECT
|
|
108
|
+
EXISTS (
|
|
109
|
+
SELECT 1
|
|
110
|
+
FROM fonderie_role_user_workspaces ruw
|
|
111
|
+
JOIN fonderie_roles r ON r.id = ruw.role_id
|
|
112
|
+
WHERE ruw.user_id = $1
|
|
113
|
+
AND ruw.workspace_id = $2
|
|
114
|
+
AND ruw.removed = false
|
|
115
|
+
AND ruw.suspended = false
|
|
116
|
+
) AS member,
|
|
117
|
+
EXISTS (
|
|
118
|
+
SELECT 1
|
|
119
|
+
FROM fonderie_role_user_workspaces ruw
|
|
120
|
+
JOIN fonderie_roles r ON r.id = ruw.role_id
|
|
121
|
+
WHERE ruw.user_id = $1
|
|
122
|
+
AND ruw.workspace_id = $2
|
|
123
|
+
AND r.name = $4
|
|
124
|
+
AND r.is_system = true
|
|
125
|
+
AND r.active = true
|
|
126
|
+
AND ruw.removed = false
|
|
127
|
+
AND ruw.suspended = false
|
|
128
|
+
) AS is_super,
|
|
129
|
+
ARRAY (
|
|
130
|
+
SELECT DISTINCT r.name
|
|
131
|
+
FROM fonderie_role_user_workspaces ruw
|
|
132
|
+
JOIN fonderie_roles r ON r.id = ruw.role_id
|
|
133
|
+
WHERE ruw.user_id = $1
|
|
134
|
+
AND ruw.workspace_id = $2
|
|
135
|
+
AND r.is_system = true
|
|
136
|
+
AND r.active = true
|
|
137
|
+
AND ruw.removed = false
|
|
138
|
+
AND ruw.suspended = false
|
|
139
|
+
) AS system_roles,
|
|
140
|
+
(
|
|
141
|
+
SELECT BOOL_OR(rp.${col})
|
|
142
|
+
FROM fonderie_role_user_workspaces ruw
|
|
143
|
+
JOIN fonderie_roles r ON ruw.role_id = r.id
|
|
144
|
+
JOIN fonderie_role_permissions rp ON r.id = rp.role_id
|
|
145
|
+
WHERE ruw.user_id = $1
|
|
146
|
+
AND ruw.workspace_id = $2
|
|
147
|
+
AND rp.workspace_id = $2
|
|
148
|
+
AND rp.permission_key = $3
|
|
149
|
+
AND ruw.removed = false
|
|
150
|
+
AND ruw.suspended = false
|
|
151
|
+
AND r.active = true
|
|
152
|
+
AND (r.workspace_id = $2 OR r.is_system = true)
|
|
153
|
+
) AS granted`,
|
|
154
|
+
[userId, workspaceId, permissionKey, superRole]
|
|
155
|
+
);
|
|
156
|
+
return {
|
|
157
|
+
member: row?.member === true,
|
|
158
|
+
isSuper: row?.is_super === true,
|
|
159
|
+
systemRoles: row?.system_roles ?? [],
|
|
160
|
+
granted: row?.granted === true
|
|
161
|
+
};
|
|
162
|
+
}
|
|
104
163
|
async function listGrantedPermissions(userId, workspaceId, store) {
|
|
105
164
|
return store.query(
|
|
106
165
|
`SELECT rp.permission_key AS "permissionKey",
|
|
@@ -198,10 +257,17 @@ var PermissionsEngine = class {
|
|
|
198
257
|
return getMembership(userId, workspaceId, this.store);
|
|
199
258
|
}
|
|
200
259
|
async can(userId, operation, permissionKey, workspaceId) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
260
|
+
if (!Object.hasOwn(PERMISSION_COLUMN, operation)) {
|
|
261
|
+
const isMember = await getMembership(userId, workspaceId, this.store);
|
|
262
|
+
if (!isMember) return false;
|
|
263
|
+
if (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;
|
|
264
|
+
return this.allows(userId, workspaceId, permissionKey, operation);
|
|
265
|
+
}
|
|
266
|
+
const a = await readAccess(userId, workspaceId, permissionKey, operation, this.superRole, this.store);
|
|
267
|
+
if (!a.member) return false;
|
|
268
|
+
if (a.isSuper) return true;
|
|
269
|
+
if (a.systemRoles.some((r) => this.systemGrants[r]?.[permissionKey]?.includes(operation) ?? false)) return true;
|
|
270
|
+
return a.granted;
|
|
205
271
|
}
|
|
206
272
|
async assert(userId, operation, permissionKey, workspaceId) {
|
|
207
273
|
const allowed = await this.can(userId, operation, permissionKey, workspaceId);
|
|
@@ -243,7 +309,7 @@ var PermissionsModule = class {
|
|
|
243
309
|
name = "@fonderie/permissions";
|
|
244
310
|
// Baked in at build time by tsup.base, so the operator's Modules page can
|
|
245
311
|
// say what is actually deployed rather than 'not reported'.
|
|
246
|
-
version = "5.1.
|
|
312
|
+
version = "5.1.6";
|
|
247
313
|
deps = ["@fonderie/auth"];
|
|
248
314
|
constructor(store, config = {}) {
|
|
249
315
|
this.engine = new PermissionsEngine(store, config);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/constants.ts","../src/services/membership.ts","../src/services/permissions.ts","../src/engine.ts","../src/module.ts","../src/middlewares/require-role.ts","../src/middlewares/require-permission.ts","../src/services/grants.ts"],"sourcesContent":["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 { 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\n/**\n * Does the user hold the SUPER-ROLE in this workspace? The engine uses this for\n * its total-access bypass, so the match is deliberately restricted to a\n * **system** role: `r.is_system = true`. Without that guard, a member could\n * create a workspace-local role whose name equals the configured super-role\n * (e.g. 'ADMIN') and self-assign it to gain full access — a privilege\n * escalation. System roles are seeded by the app, not member-creatable. Also\n * requires `active = true` so a deactivated role grants nothing.\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 r.is_system = true\n\t\t AND r.active = true\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\n/**\n * Does the user hold ANY of the named roles in this workspace (active, not\n * removed/suspended)? Used by requireRole — an EXISTS over ALL of the user's\n * role rows, so a member holding several roles isn't denied because an\n * arbitrary single row (LIMIT 1) happened to be the wrong one. Not restricted\n * to system roles: requireRole checks arbitrary named roles.\n */\nexport async function hasAnyRole(\n\tuserId: string,\n\tworkspaceId: string,\n\troleNames: string[],\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tif (roleNames.length === 0) return false;\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 = ANY($3)\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS exists`,\n\t\t[userId, workspaceId, roleNames],\n\t);\n\n\treturn row?.exists ?? false;\n}\n\n/**\n * The SYSTEM roles (shared across workspaces) the user holds here — the ones\n * whose rights come from config (systemGrants). System only, for the same\n * reason as hasRole: a member-created local role named 'GUEST' must not pick\n * up the system GUEST's rights.\n */\nexport async function listSystemRoleNames(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<string[]> {\n\tconst rows = await store.query<{ name: string }>(\n\t\t`SELECT DISTINCT r.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 r.is_system = true\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false`,\n\t\t[userId, workspaceId],\n\t);\n\treturn rows.map((r) => r.name);\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.active = true\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\n/**\n * Every grant the member's STORED roles give in this workspace, folded across\n * roles (a member with two roles gets the union) — one query for the\n * effective-permissions read instead of one check per key.\n */\nexport async function listGrantedPermissions(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<Array<{ permissionKey: string; create: boolean; read: boolean; update: boolean; delete: boolean }>> {\n\treturn store.query(\n\t\t`SELECT rp.permission_key AS \"permissionKey\",\n\t\t BOOL_OR(rp.can_create) AS \"create\",\n\t\t BOOL_OR(rp.can_read) AS \"read\",\n\t\t BOOL_OR(rp.can_update) AS \"update\",\n\t\t BOOL_OR(rp.can_delete) AS \"delete\"\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 ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t AND r.active = true\n\t\t AND (r.workspace_id = $2 OR r.is_system = true)\n\t\t GROUP BY rp.permission_key`,\n\t\t[userId, workspaceId],\n\t);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation, IPermissionCatalogEntry, IEffectivePermissions } from './types';\nimport type { IPermissionsConfig } from './config';\nimport { OPERATIONS } from './constants';\nimport { getMembership, hasRole, listSystemRoleNames } from './services/membership';\nimport { checkPermission, listGrantedPermissions } from './services/permissions';\n\nconst ALL_OPERATIONS = Object.values(OPERATIONS) as Operation[];\n\nexport class PermissionsEngine {\n\tprivate superRole: string;\n\tprivate systemGrants: Record<string, Record<string, Operation[]>>;\n\t/** The app's declared resources, or null when it declared none. */\n\treadonly catalog: readonly IPermissionCatalogEntry[] | null;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tconfig: IPermissionsConfig = {},\n\t) {\n\t\tthis.superRole = config.superRole ?? 'owner';\n\t\tthis.catalog = config.catalog ? Object.freeze(config.catalog.map((e) => ({ ...e }))) : null;\n\t\tthis.systemGrants = config.systemGrants ?? {};\n\t\t// A misspelled resource in systemGrants would grant nothing, silently.\n\t\t// Refuse to start instead — a config mistake, caught at boot.\n\t\tif (this.catalog) {\n\t\t\tconst known = new Set(this.catalog.map((e) => e.key));\n\t\t\tfor (const [role, grants] of Object.entries(this.systemGrants)) {\n\t\t\t\tfor (const key of Object.keys(grants)) {\n\t\t\t\t\tif (!known.has(key)) {\n\t\t\t\t\t\tthrow new Error(`[permissions] systemGrants.${role}.${key}: '${key}' is not in the catalog`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Is this key in the catalog? Always true when the app declared no catalog. */\n\tisKnown(permissionKey: string): boolean {\n\t\treturn !this.catalog || this.catalog.some((e) => e.key === permissionKey);\n\t}\n\n\t/** The operations a resource supports (all four when not declared). */\n\toperationsOf(permissionKey: string): Operation[] {\n\t\treturn this.catalog?.find((e) => e.key === permissionKey)?.operations ?? ALL_OPERATIONS;\n\t}\n\n\tprivate async allows(\n\t\tuserId: string,\n\t\tworkspaceId: string,\n\t\tpermissionKey: string,\n\t\toperation: Operation,\n\t): Promise<boolean> {\n\t\tif (Object.keys(this.systemGrants).length > 0) {\n\t\t\tconst roles = await listSystemRoleNames(userId, workspaceId, this.store);\n\t\t\tif (roles.some((r) => this.systemGrants[r]?.[permissionKey]?.includes(operation) ?? false)) return true;\n\t\t}\n\t\treturn checkPermission(userId, workspaceId, permissionKey, operation, this.store);\n\t}\n\n\t/**\n\t * Everything this member may do in this workspace, in one read — what a\n\t * client needs to show only the actions that will succeed. Null when the\n\t * user is not a member. Lists the catalog's resources (every operation\n\t * present, true or false) plus any granted key outside it.\n\t */\n\tasync effective(userId: string, workspaceId: string): Promise<IEffectivePermissions | null> {\n\t\tif (!(await getMembership(userId, workspaceId, this.store))) return null;\n\n\t\tconst permissions: Record<string, Record<Operation, boolean>> = {};\n\t\tconst entry = (key: string) =>\n\t\t\t(permissions[key] ??= { create: false, read: false, update: false, delete: false });\n\t\tfor (const e of this.catalog ?? []) entry(e.key);\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) {\n\t\t\tfor (const e of this.catalog ?? []) {\n\t\t\t\tfor (const op of e.operations ?? ALL_OPERATIONS) entry(e.key)[op] = true;\n\t\t\t}\n\t\t\treturn { isSuper: true, permissions };\n\t\t}\n\n\t\tconst [systemRoles, stored] = await Promise.all([\n\t\t\tObject.keys(this.systemGrants).length ? listSystemRoleNames(userId, workspaceId, this.store) : Promise.resolve([]),\n\t\t\tlistGrantedPermissions(userId, workspaceId, this.store),\n\t\t]);\n\t\tfor (const role of systemRoles) {\n\t\t\tfor (const [key, ops] of Object.entries(this.systemGrants[role] ?? {})) {\n\t\t\t\tfor (const op of ops) entry(key)[op] = true;\n\t\t\t}\n\t\t}\n\t\tfor (const g of stored) {\n\t\t\tconst e = entry(g.permissionKey);\n\t\t\tfor (const op of ALL_OPERATIONS) e[op] ||= Boolean(g[op]);\n\t\t}\n\t\treturn { isSuper: false, permissions };\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 this.allows(userId, workspaceId, permissionKey, operation);\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) => this.allows(userId, workspaceId, c.permissionKey, c.operation)),\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) => this.allows(userId, workspaceId, c.permissionKey, c.operation)),\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\t// Baked in at build time by tsup.base, so the operator's Modules page can\n\t// say what is actually deployed rather than 'not reported'.\n\treadonly version = process.env['FONDERIE_PKG_VERSION'] ?? '0.0.0-dev';\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 { hasAnyRole } 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\t// EXISTS across ALL of the user's roles — a member holding several roles\n\t\t// must not be denied because a single arbitrary row was the wrong one.\n\t\tif (!(await hasAnyRole(ctx.user.id, workspaceId, allowed, store))) {\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","import type { IStoreAdapter } from '@fonderie/store';\n\n// Access-grant export (SOC 2 CC6.2/6.3) — feeds the access-review register with\n// the current who-has-what: every active user→workspace→role grant. Run this on\n// a schedule (or during a review) and diff against the previous snapshot.\n\nexport interface IGrant {\n\tuserId: string;\n\tworkspaceId: string;\n\troleId: string;\n\troleName: string;\n\tsuspended: boolean;\n}\n\nexport async function listGrants(store: IStoreAdapter): Promise<IGrant[]> {\n\tconst rows = 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\tsuspended: boolean;\n\t}>(\n\t\t`SELECT ruw.user_id, ruw.workspace_id, ruw.role_id, r.name AS role_name, ruw.suspended\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.removed = false\n\t\t ORDER BY ruw.workspace_id, ruw.user_id`,\n\t);\n\treturn rows.map((r) => ({\n\t\tuserId: r.user_id,\n\t\tworkspaceId: r.workspace_id,\n\t\troleId: r.role_id,\n\t\troleName: r.role_name,\n\t\tsuspended: r.suspended,\n\t}));\n}\n"],"mappings":";AAGA,SAAS,kBAAkB;AAEpB,IAAM,oBAA+C;AAAA,EAC3D,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AACT;;;ACNA,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;AAWA,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;AAAA;AAAA,IAYA,CAAC,QAAQ,aAAa,QAAQ;AAAA,EAC/B;AAEA,SAAO,KAAK,UAAU;AACvB;AASA,eAAsB,WACrB,QACA,aACA,WACA,OACmB;AACnB,MAAI,UAAU,WAAW,EAAG,QAAO;AACnC,QAAM,CAAC,GAAG,IAAI,MAAM,MAAM;AAAA,IACzB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAWA,CAAC,QAAQ,aAAa,SAAS;AAAA,EAChC;AAEA,SAAO,KAAK,UAAU;AACvB;AAQA,eAAsB,oBACrB,QACA,aACA,OACoB;AACpB,QAAM,OAAO,MAAM,MAAM;AAAA,IACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASA,CAAC,QAAQ,WAAW;AAAA,EACrB;AACA,SAAO,KAAK,IAAI,CAAC,MAAM,EAAE,IAAI;AAC9B;;;ACzHA,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;AAAA,IAaxB,CAAC,QAAQ,aAAa,aAAa;AAAA,EACpC;AAEA,SAAO,KAAK,kBAAkB;AAC/B;AAOA,eAAsB,uBACrB,QACA,aACA,OAC8G;AAC9G,SAAO,MAAM;AAAA,IACZ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAgBA,CAAC,QAAQ,WAAW;AAAA,EACrB;AACD;;;ACvDA,IAAM,iBAAiB,OAAO,OAAO,UAAU;AAExC,IAAM,oBAAN,MAAwB;AAAA,EAM9B,YACS,OACR,SAA6B,CAAC,GAC7B;AAFO;AAGR,SAAK,YAAY,OAAO,aAAa;AACrC,SAAK,UAAU,OAAO,UAAU,OAAO,OAAO,OAAO,QAAQ,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,CAAC,IAAI;AACvF,SAAK,eAAe,OAAO,gBAAgB,CAAC;AAG5C,QAAI,KAAK,SAAS;AACjB,YAAM,QAAQ,IAAI,IAAI,KAAK,QAAQ,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC;AACpD,iBAAW,CAAC,MAAM,MAAM,KAAK,OAAO,QAAQ,KAAK,YAAY,GAAG;AAC/D,mBAAW,OAAO,OAAO,KAAK,MAAM,GAAG;AACtC,cAAI,CAAC,MAAM,IAAI,GAAG,GAAG;AACpB,kBAAM,IAAI,MAAM,8BAA8B,IAAI,IAAI,GAAG,MAAM,GAAG,yBAAyB;AAAA,UAC5F;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,EACD;AAAA,EAlBS;AAAA,EAND;AAAA,EACA;AAAA;AAAA,EAEC;AAAA;AAAA,EAwBT,QAAQ,eAAgC;AACvC,WAAO,CAAC,KAAK,WAAW,KAAK,QAAQ,KAAK,CAAC,MAAM,EAAE,QAAQ,aAAa;AAAA,EACzE;AAAA;AAAA,EAGA,aAAa,eAAoC;AAChD,WAAO,KAAK,SAAS,KAAK,CAAC,MAAM,EAAE,QAAQ,aAAa,GAAG,cAAc;AAAA,EAC1E;AAAA,EAEA,MAAc,OACb,QACA,aACA,eACA,WACmB;AACnB,QAAI,OAAO,KAAK,KAAK,YAAY,EAAE,SAAS,GAAG;AAC9C,YAAM,QAAQ,MAAM,oBAAoB,QAAQ,aAAa,KAAK,KAAK;AACvE,UAAI,MAAM,KAAK,CAAC,MAAM,KAAK,aAAa,CAAC,IAAI,aAAa,GAAG,SAAS,SAAS,KAAK,KAAK,EAAG,QAAO;AAAA,IACpG;AACA,WAAO,gBAAgB,QAAQ,aAAa,eAAe,WAAW,KAAK,KAAK;AAAA,EACjF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,UAAU,QAAgB,aAA4D;AAC3F,QAAI,CAAE,MAAM,cAAc,QAAQ,aAAa,KAAK,KAAK,EAAI,QAAO;AAEpE,UAAM,cAA0D,CAAC;AACjE,UAAM,QAAQ,CAAC,QACb,YAAY,GAAG,MAAM,EAAE,QAAQ,OAAO,MAAM,OAAO,QAAQ,OAAO,QAAQ,MAAM;AAClF,eAAW,KAAK,KAAK,WAAW,CAAC,EAAG,OAAM,EAAE,GAAG;AAE/C,QAAI,MAAM,QAAQ,QAAQ,aAAa,KAAK,WAAW,KAAK,KAAK,GAAG;AACnE,iBAAW,KAAK,KAAK,WAAW,CAAC,GAAG;AACnC,mBAAW,MAAM,EAAE,cAAc,eAAgB,OAAM,EAAE,GAAG,EAAE,EAAE,IAAI;AAAA,MACrE;AACA,aAAO,EAAE,SAAS,MAAM,YAAY;AAAA,IACrC;AAEA,UAAM,CAAC,aAAa,MAAM,IAAI,MAAM,QAAQ,IAAI;AAAA,MAC/C,OAAO,KAAK,KAAK,YAAY,EAAE,SAAS,oBAAoB,QAAQ,aAAa,KAAK,KAAK,IAAI,QAAQ,QAAQ,CAAC,CAAC;AAAA,MACjH,uBAAuB,QAAQ,aAAa,KAAK,KAAK;AAAA,IACvD,CAAC;AACD,eAAW,QAAQ,aAAa;AAC/B,iBAAW,CAAC,KAAK,GAAG,KAAK,OAAO,QAAQ,KAAK,aAAa,IAAI,KAAK,CAAC,CAAC,GAAG;AACvE,mBAAW,MAAM,IAAK,OAAM,GAAG,EAAE,EAAE,IAAI;AAAA,MACxC;AAAA,IACD;AACA,eAAW,KAAK,QAAQ;AACvB,YAAM,IAAI,MAAM,EAAE,aAAa;AAC/B,iBAAW,MAAM,eAAgB,GAAE,EAAE,MAAM,QAAQ,EAAE,EAAE,CAAC;AAAA,IACzD;AACA,WAAO,EAAE,SAAS,OAAO,YAAY;AAAA,EACtC;AAAA,EAEA,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,KAAK,OAAO,QAAQ,aAAa,eAAe,SAAS;AAAA,EACjE;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,IAAI,CAAC,MAAM,KAAK,OAAO,QAAQ,aAAa,EAAE,eAAe,EAAE,SAAS,CAAC;AAAA,IACjF;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,IAAI,CAAC,MAAM,KAAK,OAAO,QAAQ,aAAa,EAAE,eAAe,EAAE,SAAS,CAAC;AAAA,IACjF;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;;;ACjKO,IAAM,yBAAyB;AAE/B,IAAM,oBAAN,MAAmD;AAAA,EAChD;AAAA,EACA,OAAO;AAAA;AAAA;AAAA,EAGP,UAAU;AAAA,EACV,OAAO,CAAC,gBAAgB;AAAA,EAEjC,YAAY,OAAsB,SAA6B,CAAC,GAAG;AAClE,SAAK,SAAS,IAAI,kBAAkB,OAAO,MAAM;AAAA,EAClD;AAAA,EAEA,QAAQ,KAAyB;AAChC,UAAM,SAAS,KAAK;AAIpB,QAAI,IAAI,OAAO,KAAK,SAAS;AAC5B,UAAI,KAAK,sBAAsB,IAAI;AACnC,aAAO,KAAK;AAAA,IACb,CAAC;AAAA,EACF;AACD;;;AC9BA,SAAS,gBAAgB,YAAY;AASrC,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,aAAO,eAAe,KAAK,cAAc,gBAAgB,cAAc;AAAA,IACxE;AAEA,UAAM,SAAS,IAAI,KAAK,sBAAsB;AAC9C,QAAI,EAAE,kBAAkB,oBAAoB;AAC3C,aAAO,eAAe,KAAK,cAAc,gBAAgB,kCAAkC;AAAA,IAC5F;AAEA,UAAM,cACL,IAAI,WAAW,MACd,IAAI,KAAK,QAAQ,IAA2C,aAAa;AAE3E,QAAI,CAAC,aAAa;AACjB,aAAO,eAAe,KAAK,aAAa,sBAAsB,4BAA4B;AAAA,IAC3F;AAIA,QAAI,CAAE,MAAM,WAAW,IAAI,KAAK,IAAI,aAAa,SAAS,KAAK,GAAI;AAClE,aAAO,eAAe,KAAK,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;;;ACxDA,SAAS,kBAAAA,iBAAgB,QAAAC,aAAY;AAQrC,SAASC,aAAY,WAAsB,eAAmC;AAC7E,SAAO,OAAO,KAAK,SAAS;AAC3B,QAAI,CAAC,IAAI,MAAM;AACd,aAAOC,gBAAeC,MAAK,cAAc,gBAAgB,cAAc;AAAA,IACxE;AAEA,UAAM,SAAS,IAAI,KAAK,sBAAsB;AAC9C,QAAI,EAAE,kBAAkB,oBAAoB;AAC3C,aAAOD,gBAAeC,MAAK,cAAc,gBAAgB,kCAAkC;AAAA,IAC5F;AAEA,UAAM,cAAc,mBAAmB,GAAG;AAC1C,QAAI,CAAC,aAAa;AACjB,aAAOD,gBAAeC,MAAK,aAAa,sBAAsB,4BAA4B;AAAA,IAC3F;AAEA,UAAM,UAAU,MAAM,OAAO,IAAI,IAAI,KAAK,IAAI,WAAW,eAAe,WAAW;AACnF,QAAI,CAAC,SAAS;AACb,aAAOD;AAAA,QACNC,MAAK;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,UAAUF,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;;;ACjDA,eAAsB,WAAW,OAAyC;AACzE,QAAM,OAAO,MAAM,MAAM;AAAA,IAOxB;AAAA;AAAA;AAAA;AAAA;AAAA,EAKD;AACA,SAAO,KAAK,IAAI,CAAC,OAAO;AAAA,IACvB,QAAQ,EAAE;AAAA,IACV,aAAa,EAAE;AAAA,IACf,QAAQ,EAAE;AAAA,IACV,UAAU,EAAE;AAAA,IACZ,WAAW,EAAE;AAAA,EACd,EAAE;AACH;","names":["setApiResponse","HTTP","makeHandler","setApiResponse","HTTP"]}
|
|
1
|
+
{"version":3,"sources":["../src/constants.ts","../src/services/membership.ts","../src/services/permissions.ts","../src/engine.ts","../src/module.ts","../src/middlewares/require-role.ts","../src/middlewares/require-permission.ts","../src/services/grants.ts"],"sourcesContent":["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 { 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\n/**\n * Does the user hold the SUPER-ROLE in this workspace? The engine uses this for\n * its total-access bypass, so the match is deliberately restricted to a\n * **system** role: `r.is_system = true`. Without that guard, a member could\n * create a workspace-local role whose name equals the configured super-role\n * (e.g. 'ADMIN') and self-assign it to gain full access — a privilege\n * escalation. System roles are seeded by the app, not member-creatable. Also\n * requires `active = true` so a deactivated role grants nothing.\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 r.is_system = true\n\t\t AND r.active = true\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\n/**\n * Does the user hold ANY of the named roles in this workspace (active, not\n * removed/suspended)? Used by requireRole — an EXISTS over ALL of the user's\n * role rows, so a member holding several roles isn't denied because an\n * arbitrary single row (LIMIT 1) happened to be the wrong one. Not restricted\n * to system roles: requireRole checks arbitrary named roles.\n */\nexport async function hasAnyRole(\n\tuserId: string,\n\tworkspaceId: string,\n\troleNames: string[],\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tif (roleNames.length === 0) return false;\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 = ANY($3)\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS exists`,\n\t\t[userId, workspaceId, roleNames],\n\t);\n\n\treturn row?.exists ?? false;\n}\n\n/**\n * The SYSTEM roles (shared across workspaces) the user holds here — the ones\n * whose rights come from config (systemGrants). System only, for the same\n * reason as hasRole: a member-created local role named 'GUEST' must not pick\n * up the system GUEST's rights.\n */\nexport async function listSystemRoleNames(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<string[]> {\n\tconst rows = await store.query<{ name: string }>(\n\t\t`SELECT DISTINCT r.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 r.is_system = true\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false`,\n\t\t[userId, workspaceId],\n\t);\n\treturn rows.map((r) => r.name);\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.active = true\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\n/**\n * Everything one permission check needs, in ONE round-trip — the same four\n * predicates as getMembership, hasRole (super-role), listSystemRoleNames and\n * checkPermission, each written exactly as there, read side by side:\n *\n * member — an active row joined to an existing role (getMembership)\n * isSuper — holds the super-role as an ACTIVE SYSTEM role (hasRole)\n * systemRoles — the active SYSTEM roles held (listSystemRoleNames)\n * granted — a stored grant for `operation` on `permissionKey` (checkPermission)\n *\n * The caller applies the same order of decisions the engine always has.\n */\nexport async function readAccess(\n\tuserId: string,\n\tworkspaceId: string,\n\tpermissionKey: string,\n\toperation: Operation,\n\tsuperRole: string,\n\tstore: IStoreAdapter,\n): Promise<{ member: boolean; isSuper: boolean; systemRoles: string[]; granted: boolean }> {\n\tconst col = PERMISSION_COLUMN[operation];\n\tconst [row] = await store.query<{ member: boolean; is_super: boolean; system_roles: string[] | null; granted: boolean | null }>(\n\t\t`SELECT\n\t\t 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 ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS member,\n\t\t 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 = $4\n\t\t AND r.is_system = true\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS is_super,\n\t\t ARRAY (\n\t\t SELECT DISTINCT r.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 r.is_system = true\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS system_roles,\n\t\t (\n\t\t SELECT BOOL_OR(rp.${col})\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.active = true\n\t\t AND (r.workspace_id = $2 OR r.is_system = true)\n\t\t ) AS granted`,\n\t\t[userId, workspaceId, permissionKey, superRole],\n\t);\n\treturn {\n\t\tmember: row?.member === true,\n\t\tisSuper: row?.is_super === true,\n\t\tsystemRoles: row?.system_roles ?? [],\n\t\tgranted: row?.granted === true,\n\t};\n}\n\n/**\n * Every grant the member's STORED roles give in this workspace, folded across\n * roles (a member with two roles gets the union) — one query for the\n * effective-permissions read instead of one check per key.\n */\nexport async function listGrantedPermissions(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<Array<{ permissionKey: string; create: boolean; read: boolean; update: boolean; delete: boolean }>> {\n\treturn store.query(\n\t\t`SELECT rp.permission_key AS \"permissionKey\",\n\t\t BOOL_OR(rp.can_create) AS \"create\",\n\t\t BOOL_OR(rp.can_read) AS \"read\",\n\t\t BOOL_OR(rp.can_update) AS \"update\",\n\t\t BOOL_OR(rp.can_delete) AS \"delete\"\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 ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t AND r.active = true\n\t\t AND (r.workspace_id = $2 OR r.is_system = true)\n\t\t GROUP BY rp.permission_key`,\n\t\t[userId, workspaceId],\n\t);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation, IPermissionCatalogEntry, IEffectivePermissions } from './types';\nimport type { IPermissionsConfig } from './config';\nimport { OPERATIONS, PERMISSION_COLUMN } from './constants';\nimport { getMembership, hasRole, listSystemRoleNames } from './services/membership';\nimport { checkPermission, listGrantedPermissions, readAccess } from './services/permissions';\n\nconst ALL_OPERATIONS = Object.values(OPERATIONS) as Operation[];\n\nexport class PermissionsEngine {\n\tprivate superRole: string;\n\tprivate systemGrants: Record<string, Record<string, Operation[]>>;\n\t/** The app's declared resources, or null when it declared none. */\n\treadonly catalog: readonly IPermissionCatalogEntry[] | null;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tconfig: IPermissionsConfig = {},\n\t) {\n\t\tthis.superRole = config.superRole ?? 'owner';\n\t\tthis.catalog = config.catalog ? Object.freeze(config.catalog.map((e) => ({ ...e }))) : null;\n\t\tthis.systemGrants = config.systemGrants ?? {};\n\t\t// A misspelled resource in systemGrants would grant nothing, silently.\n\t\t// Refuse to start instead — a config mistake, caught at boot.\n\t\tif (this.catalog) {\n\t\t\tconst known = new Set(this.catalog.map((e) => e.key));\n\t\t\tfor (const [role, grants] of Object.entries(this.systemGrants)) {\n\t\t\t\tfor (const key of Object.keys(grants)) {\n\t\t\t\t\tif (!known.has(key)) {\n\t\t\t\t\t\tthrow new Error(`[permissions] systemGrants.${role}.${key}: '${key}' is not in the catalog`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Is this key in the catalog? Always true when the app declared no catalog. */\n\tisKnown(permissionKey: string): boolean {\n\t\treturn !this.catalog || this.catalog.some((e) => e.key === permissionKey);\n\t}\n\n\t/** The operations a resource supports (all four when not declared). */\n\toperationsOf(permissionKey: string): Operation[] {\n\t\treturn this.catalog?.find((e) => e.key === permissionKey)?.operations ?? ALL_OPERATIONS;\n\t}\n\n\tprivate async allows(\n\t\tuserId: string,\n\t\tworkspaceId: string,\n\t\tpermissionKey: string,\n\t\toperation: Operation,\n\t): Promise<boolean> {\n\t\tif (Object.keys(this.systemGrants).length > 0) {\n\t\t\tconst roles = await listSystemRoleNames(userId, workspaceId, this.store);\n\t\t\tif (roles.some((r) => this.systemGrants[r]?.[permissionKey]?.includes(operation) ?? false)) return true;\n\t\t}\n\t\treturn checkPermission(userId, workspaceId, permissionKey, operation, this.store);\n\t}\n\n\t/**\n\t * Everything this member may do in this workspace, in one read — what a\n\t * client needs to show only the actions that will succeed. Null when the\n\t * user is not a member. Lists the catalog's resources (every operation\n\t * present, true or false) plus any granted key outside it.\n\t */\n\tasync effective(userId: string, workspaceId: string): Promise<IEffectivePermissions | null> {\n\t\tif (!(await getMembership(userId, workspaceId, this.store))) return null;\n\n\t\tconst permissions: Record<string, Record<Operation, boolean>> = {};\n\t\tconst entry = (key: string) =>\n\t\t\t(permissions[key] ??= { create: false, read: false, update: false, delete: false });\n\t\tfor (const e of this.catalog ?? []) entry(e.key);\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) {\n\t\t\tfor (const e of this.catalog ?? []) {\n\t\t\t\tfor (const op of e.operations ?? ALL_OPERATIONS) entry(e.key)[op] = true;\n\t\t\t}\n\t\t\treturn { isSuper: true, permissions };\n\t\t}\n\n\t\tconst [systemRoles, stored] = await Promise.all([\n\t\t\tObject.keys(this.systemGrants).length ? listSystemRoleNames(userId, workspaceId, this.store) : Promise.resolve([]),\n\t\t\tlistGrantedPermissions(userId, workspaceId, this.store),\n\t\t]);\n\t\tfor (const role of systemRoles) {\n\t\t\tfor (const [key, ops] of Object.entries(this.systemGrants[role] ?? {})) {\n\t\t\t\tfor (const op of ops) entry(key)[op] = true;\n\t\t\t}\n\t\t}\n\t\tfor (const g of stored) {\n\t\t\tconst e = entry(g.permissionKey);\n\t\t\tfor (const op of ALL_OPERATIONS) e[op] ||= Boolean(g[op]);\n\t\t}\n\t\treturn { isSuper: false, permissions };\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\t// An operation outside the four has no grant column; keep the old,\n\t\t// step-by-step path for it rather than change what it answers.\n\t\tif (!Object.hasOwn(PERMISSION_COLUMN, operation)) {\n\t\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\t\tif (!isMember) return false;\n\t\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\t\t\treturn this.allows(userId, workspaceId, permissionKey, operation);\n\t\t}\n\n\t\t// Member → super-role → config system grants → stored grants: the same\n\t\t// decisions in the same order, read in one round-trip.\n\t\tconst a = await readAccess(userId, workspaceId, permissionKey, operation, this.superRole, this.store);\n\t\tif (!a.member) return false;\n\t\tif (a.isSuper) return true;\n\t\tif (a.systemRoles.some((r) => this.systemGrants[r]?.[permissionKey]?.includes(operation) ?? false)) return true;\n\t\treturn a.granted;\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) => this.allows(userId, workspaceId, c.permissionKey, c.operation)),\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) => this.allows(userId, workspaceId, c.permissionKey, c.operation)),\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\t// Baked in at build time by tsup.base, so the operator's Modules page can\n\t// say what is actually deployed rather than 'not reported'.\n\treadonly version = process.env['FONDERIE_PKG_VERSION'] ?? '0.0.0-dev';\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 { hasAnyRole } 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\t// EXISTS across ALL of the user's roles — a member holding several roles\n\t\t// must not be denied because a single arbitrary row was the wrong one.\n\t\tif (!(await hasAnyRole(ctx.user.id, workspaceId, allowed, store))) {\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","import type { IStoreAdapter } from '@fonderie/store';\n\n// Access-grant export (SOC 2 CC6.2/6.3) — feeds the access-review register with\n// the current who-has-what: every active user→workspace→role grant. Run this on\n// a schedule (or during a review) and diff against the previous snapshot.\n\nexport interface IGrant {\n\tuserId: string;\n\tworkspaceId: string;\n\troleId: string;\n\troleName: string;\n\tsuspended: boolean;\n}\n\nexport async function listGrants(store: IStoreAdapter): Promise<IGrant[]> {\n\tconst rows = 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\tsuspended: boolean;\n\t}>(\n\t\t`SELECT ruw.user_id, ruw.workspace_id, ruw.role_id, r.name AS role_name, ruw.suspended\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.removed = false\n\t\t ORDER BY ruw.workspace_id, ruw.user_id`,\n\t);\n\treturn rows.map((r) => ({\n\t\tuserId: r.user_id,\n\t\tworkspaceId: r.workspace_id,\n\t\troleId: r.role_id,\n\t\troleName: r.role_name,\n\t\tsuspended: r.suspended,\n\t}));\n}\n"],"mappings":"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","names":["setApiResponse","HTTP","makeHandler","setApiResponse","HTTP"]}
|
|
@@ -131,6 +131,65 @@ async function checkPermission(userId, workspaceId, permissionKey, operation, st
|
|
|
131
131
|
);
|
|
132
132
|
return row?.has_permission ?? false;
|
|
133
133
|
}
|
|
134
|
+
async function readAccess(userId, workspaceId, permissionKey, operation, superRole, store) {
|
|
135
|
+
const col = PERMISSION_COLUMN[operation];
|
|
136
|
+
const [row] = await store.query(
|
|
137
|
+
`SELECT
|
|
138
|
+
EXISTS (
|
|
139
|
+
SELECT 1
|
|
140
|
+
FROM fonderie_role_user_workspaces ruw
|
|
141
|
+
JOIN fonderie_roles r ON r.id = ruw.role_id
|
|
142
|
+
WHERE ruw.user_id = $1
|
|
143
|
+
AND ruw.workspace_id = $2
|
|
144
|
+
AND ruw.removed = false
|
|
145
|
+
AND ruw.suspended = false
|
|
146
|
+
) AS member,
|
|
147
|
+
EXISTS (
|
|
148
|
+
SELECT 1
|
|
149
|
+
FROM fonderie_role_user_workspaces ruw
|
|
150
|
+
JOIN fonderie_roles r ON r.id = ruw.role_id
|
|
151
|
+
WHERE ruw.user_id = $1
|
|
152
|
+
AND ruw.workspace_id = $2
|
|
153
|
+
AND r.name = $4
|
|
154
|
+
AND r.is_system = true
|
|
155
|
+
AND r.active = true
|
|
156
|
+
AND ruw.removed = false
|
|
157
|
+
AND ruw.suspended = false
|
|
158
|
+
) AS is_super,
|
|
159
|
+
ARRAY (
|
|
160
|
+
SELECT DISTINCT r.name
|
|
161
|
+
FROM fonderie_role_user_workspaces ruw
|
|
162
|
+
JOIN fonderie_roles r ON r.id = ruw.role_id
|
|
163
|
+
WHERE ruw.user_id = $1
|
|
164
|
+
AND ruw.workspace_id = $2
|
|
165
|
+
AND r.is_system = true
|
|
166
|
+
AND r.active = true
|
|
167
|
+
AND ruw.removed = false
|
|
168
|
+
AND ruw.suspended = false
|
|
169
|
+
) AS system_roles,
|
|
170
|
+
(
|
|
171
|
+
SELECT BOOL_OR(rp.${col})
|
|
172
|
+
FROM fonderie_role_user_workspaces ruw
|
|
173
|
+
JOIN fonderie_roles r ON ruw.role_id = r.id
|
|
174
|
+
JOIN fonderie_role_permissions rp ON r.id = rp.role_id
|
|
175
|
+
WHERE ruw.user_id = $1
|
|
176
|
+
AND ruw.workspace_id = $2
|
|
177
|
+
AND rp.workspace_id = $2
|
|
178
|
+
AND rp.permission_key = $3
|
|
179
|
+
AND ruw.removed = false
|
|
180
|
+
AND ruw.suspended = false
|
|
181
|
+
AND r.active = true
|
|
182
|
+
AND (r.workspace_id = $2 OR r.is_system = true)
|
|
183
|
+
) AS granted`,
|
|
184
|
+
[userId, workspaceId, permissionKey, superRole]
|
|
185
|
+
);
|
|
186
|
+
return {
|
|
187
|
+
member: row?.member === true,
|
|
188
|
+
isSuper: row?.is_super === true,
|
|
189
|
+
systemRoles: row?.system_roles ?? [],
|
|
190
|
+
granted: row?.granted === true
|
|
191
|
+
};
|
|
192
|
+
}
|
|
134
193
|
async function listGrantedPermissions(userId, workspaceId, store) {
|
|
135
194
|
return store.query(
|
|
136
195
|
`SELECT rp.permission_key AS "permissionKey",
|
|
@@ -228,10 +287,17 @@ var PermissionsEngine = class {
|
|
|
228
287
|
return getMembership(userId, workspaceId, this.store);
|
|
229
288
|
}
|
|
230
289
|
async can(userId, operation, permissionKey, workspaceId) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
290
|
+
if (!Object.hasOwn(PERMISSION_COLUMN, operation)) {
|
|
291
|
+
const isMember = await getMembership(userId, workspaceId, this.store);
|
|
292
|
+
if (!isMember) return false;
|
|
293
|
+
if (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;
|
|
294
|
+
return this.allows(userId, workspaceId, permissionKey, operation);
|
|
295
|
+
}
|
|
296
|
+
const a = await readAccess(userId, workspaceId, permissionKey, operation, this.superRole, this.store);
|
|
297
|
+
if (!a.member) return false;
|
|
298
|
+
if (a.isSuper) return true;
|
|
299
|
+
if (a.systemRoles.some((r) => this.systemGrants[r]?.[permissionKey]?.includes(operation) ?? false)) return true;
|
|
300
|
+
return a.granted;
|
|
235
301
|
}
|
|
236
302
|
async assert(userId, operation, permissionKey, workspaceId) {
|
|
237
303
|
const allowed = await this.can(userId, operation, permissionKey, workspaceId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/middlewares/index.ts","../../src/middlewares/require-role.ts","../../src/constants.ts","../../src/services/membership.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 { hasAnyRole } 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\t// EXISTS across ALL of the user's roles — a member holding several roles\n\t\t// must not be denied because a single arbitrary row was the wrong one.\n\t\tif (!(await hasAnyRole(ctx.user.id, workspaceId, allowed, store))) {\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 { 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 { 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\n/**\n * Does the user hold the SUPER-ROLE in this workspace? The engine uses this for\n * its total-access bypass, so the match is deliberately restricted to a\n * **system** role: `r.is_system = true`. Without that guard, a member could\n * create a workspace-local role whose name equals the configured super-role\n * (e.g. 'ADMIN') and self-assign it to gain full access — a privilege\n * escalation. System roles are seeded by the app, not member-creatable. Also\n * requires `active = true` so a deactivated role grants nothing.\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 r.is_system = true\n\t\t AND r.active = true\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\n/**\n * Does the user hold ANY of the named roles in this workspace (active, not\n * removed/suspended)? Used by requireRole — an EXISTS over ALL of the user's\n * role rows, so a member holding several roles isn't denied because an\n * arbitrary single row (LIMIT 1) happened to be the wrong one. Not restricted\n * to system roles: requireRole checks arbitrary named roles.\n */\nexport async function hasAnyRole(\n\tuserId: string,\n\tworkspaceId: string,\n\troleNames: string[],\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tif (roleNames.length === 0) return false;\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 = ANY($3)\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS exists`,\n\t\t[userId, workspaceId, roleNames],\n\t);\n\n\treturn row?.exists ?? false;\n}\n\n/**\n * The SYSTEM roles (shared across workspaces) the user holds here — the ones\n * whose rights come from config (systemGrants). System only, for the same\n * reason as hasRole: a member-created local role named 'GUEST' must not pick\n * up the system GUEST's rights.\n */\nexport async function listSystemRoleNames(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<string[]> {\n\tconst rows = await store.query<{ name: string }>(\n\t\t`SELECT DISTINCT r.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 r.is_system = true\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false`,\n\t\t[userId, workspaceId],\n\t);\n\treturn rows.map((r) => r.name);\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.active = true\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\n/**\n * Every grant the member's STORED roles give in this workspace, folded across\n * roles (a member with two roles gets the union) — one query for the\n * effective-permissions read instead of one check per key.\n */\nexport async function listGrantedPermissions(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<Array<{ permissionKey: string; create: boolean; read: boolean; update: boolean; delete: boolean }>> {\n\treturn store.query(\n\t\t`SELECT rp.permission_key AS \"permissionKey\",\n\t\t BOOL_OR(rp.can_create) AS \"create\",\n\t\t BOOL_OR(rp.can_read) AS \"read\",\n\t\t BOOL_OR(rp.can_update) AS \"update\",\n\t\t BOOL_OR(rp.can_delete) AS \"delete\"\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 ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t AND r.active = true\n\t\t AND (r.workspace_id = $2 OR r.is_system = true)\n\t\t GROUP BY rp.permission_key`,\n\t\t[userId, workspaceId],\n\t);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation, IPermissionCatalogEntry, IEffectivePermissions } from './types';\nimport type { IPermissionsConfig } from './config';\nimport { OPERATIONS } from './constants';\nimport { getMembership, hasRole, listSystemRoleNames } from './services/membership';\nimport { checkPermission, listGrantedPermissions } from './services/permissions';\n\nconst ALL_OPERATIONS = Object.values(OPERATIONS) as Operation[];\n\nexport class PermissionsEngine {\n\tprivate superRole: string;\n\tprivate systemGrants: Record<string, Record<string, Operation[]>>;\n\t/** The app's declared resources, or null when it declared none. */\n\treadonly catalog: readonly IPermissionCatalogEntry[] | null;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tconfig: IPermissionsConfig = {},\n\t) {\n\t\tthis.superRole = config.superRole ?? 'owner';\n\t\tthis.catalog = config.catalog ? Object.freeze(config.catalog.map((e) => ({ ...e }))) : null;\n\t\tthis.systemGrants = config.systemGrants ?? {};\n\t\t// A misspelled resource in systemGrants would grant nothing, silently.\n\t\t// Refuse to start instead — a config mistake, caught at boot.\n\t\tif (this.catalog) {\n\t\t\tconst known = new Set(this.catalog.map((e) => e.key));\n\t\t\tfor (const [role, grants] of Object.entries(this.systemGrants)) {\n\t\t\t\tfor (const key of Object.keys(grants)) {\n\t\t\t\t\tif (!known.has(key)) {\n\t\t\t\t\t\tthrow new Error(`[permissions] systemGrants.${role}.${key}: '${key}' is not in the catalog`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Is this key in the catalog? Always true when the app declared no catalog. */\n\tisKnown(permissionKey: string): boolean {\n\t\treturn !this.catalog || this.catalog.some((e) => e.key === permissionKey);\n\t}\n\n\t/** The operations a resource supports (all four when not declared). */\n\toperationsOf(permissionKey: string): Operation[] {\n\t\treturn this.catalog?.find((e) => e.key === permissionKey)?.operations ?? ALL_OPERATIONS;\n\t}\n\n\tprivate async allows(\n\t\tuserId: string,\n\t\tworkspaceId: string,\n\t\tpermissionKey: string,\n\t\toperation: Operation,\n\t): Promise<boolean> {\n\t\tif (Object.keys(this.systemGrants).length > 0) {\n\t\t\tconst roles = await listSystemRoleNames(userId, workspaceId, this.store);\n\t\t\tif (roles.some((r) => this.systemGrants[r]?.[permissionKey]?.includes(operation) ?? false)) return true;\n\t\t}\n\t\treturn checkPermission(userId, workspaceId, permissionKey, operation, this.store);\n\t}\n\n\t/**\n\t * Everything this member may do in this workspace, in one read — what a\n\t * client needs to show only the actions that will succeed. Null when the\n\t * user is not a member. Lists the catalog's resources (every operation\n\t * present, true or false) plus any granted key outside it.\n\t */\n\tasync effective(userId: string, workspaceId: string): Promise<IEffectivePermissions | null> {\n\t\tif (!(await getMembership(userId, workspaceId, this.store))) return null;\n\n\t\tconst permissions: Record<string, Record<Operation, boolean>> = {};\n\t\tconst entry = (key: string) =>\n\t\t\t(permissions[key] ??= { create: false, read: false, update: false, delete: false });\n\t\tfor (const e of this.catalog ?? []) entry(e.key);\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) {\n\t\t\tfor (const e of this.catalog ?? []) {\n\t\t\t\tfor (const op of e.operations ?? ALL_OPERATIONS) entry(e.key)[op] = true;\n\t\t\t}\n\t\t\treturn { isSuper: true, permissions };\n\t\t}\n\n\t\tconst [systemRoles, stored] = await Promise.all([\n\t\t\tObject.keys(this.systemGrants).length ? listSystemRoleNames(userId, workspaceId, this.store) : Promise.resolve([]),\n\t\t\tlistGrantedPermissions(userId, workspaceId, this.store),\n\t\t]);\n\t\tfor (const role of systemRoles) {\n\t\t\tfor (const [key, ops] of Object.entries(this.systemGrants[role] ?? {})) {\n\t\t\t\tfor (const op of ops) entry(key)[op] = true;\n\t\t\t}\n\t\t}\n\t\tfor (const g of stored) {\n\t\t\tconst e = entry(g.permissionKey);\n\t\t\tfor (const op of ALL_OPERATIONS) e[op] ||= Boolean(g[op]);\n\t\t}\n\t\treturn { isSuper: false, permissions };\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 this.allows(userId, workspaceId, permissionKey, operation);\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) => this.allows(userId, workspaceId, c.permissionKey, c.operation)),\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) => this.allows(userId, workspaceId, c.permissionKey, c.operation)),\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\t// Baked in at build time by tsup.base, so the operator's Modules page can\n\t// say what is actually deployed rather than 'not reported'.\n\treadonly version = process.env['FONDERIE_PKG_VERSION'] ?? '0.0.0-dev';\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
|
+
{"version":3,"sources":["../../src/middlewares/index.ts","../../src/middlewares/require-role.ts","../../src/constants.ts","../../src/services/membership.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 { hasAnyRole } 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\t// EXISTS across ALL of the user's roles — a member holding several roles\n\t\t// must not be denied because a single arbitrary row was the wrong one.\n\t\tif (!(await hasAnyRole(ctx.user.id, workspaceId, allowed, store))) {\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 { 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 { 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\n/**\n * Does the user hold the SUPER-ROLE in this workspace? The engine uses this for\n * its total-access bypass, so the match is deliberately restricted to a\n * **system** role: `r.is_system = true`. Without that guard, a member could\n * create a workspace-local role whose name equals the configured super-role\n * (e.g. 'ADMIN') and self-assign it to gain full access — a privilege\n * escalation. System roles are seeded by the app, not member-creatable. Also\n * requires `active = true` so a deactivated role grants nothing.\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 r.is_system = true\n\t\t AND r.active = true\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\n/**\n * Does the user hold ANY of the named roles in this workspace (active, not\n * removed/suspended)? Used by requireRole — an EXISTS over ALL of the user's\n * role rows, so a member holding several roles isn't denied because an\n * arbitrary single row (LIMIT 1) happened to be the wrong one. Not restricted\n * to system roles: requireRole checks arbitrary named roles.\n */\nexport async function hasAnyRole(\n\tuserId: string,\n\tworkspaceId: string,\n\troleNames: string[],\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tif (roleNames.length === 0) return false;\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 = ANY($3)\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS exists`,\n\t\t[userId, workspaceId, roleNames],\n\t);\n\n\treturn row?.exists ?? false;\n}\n\n/**\n * The SYSTEM roles (shared across workspaces) the user holds here — the ones\n * whose rights come from config (systemGrants). System only, for the same\n * reason as hasRole: a member-created local role named 'GUEST' must not pick\n * up the system GUEST's rights.\n */\nexport async function listSystemRoleNames(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<string[]> {\n\tconst rows = await store.query<{ name: string }>(\n\t\t`SELECT DISTINCT r.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 r.is_system = true\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false`,\n\t\t[userId, workspaceId],\n\t);\n\treturn rows.map((r) => r.name);\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.active = true\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\n/**\n * Everything one permission check needs, in ONE round-trip — the same four\n * predicates as getMembership, hasRole (super-role), listSystemRoleNames and\n * checkPermission, each written exactly as there, read side by side:\n *\n * member — an active row joined to an existing role (getMembership)\n * isSuper — holds the super-role as an ACTIVE SYSTEM role (hasRole)\n * systemRoles — the active SYSTEM roles held (listSystemRoleNames)\n * granted — a stored grant for `operation` on `permissionKey` (checkPermission)\n *\n * The caller applies the same order of decisions the engine always has.\n */\nexport async function readAccess(\n\tuserId: string,\n\tworkspaceId: string,\n\tpermissionKey: string,\n\toperation: Operation,\n\tsuperRole: string,\n\tstore: IStoreAdapter,\n): Promise<{ member: boolean; isSuper: boolean; systemRoles: string[]; granted: boolean }> {\n\tconst col = PERMISSION_COLUMN[operation];\n\tconst [row] = await store.query<{ member: boolean; is_super: boolean; system_roles: string[] | null; granted: boolean | null }>(\n\t\t`SELECT\n\t\t 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 ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS member,\n\t\t 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 = $4\n\t\t AND r.is_system = true\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS is_super,\n\t\t ARRAY (\n\t\t SELECT DISTINCT r.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 r.is_system = true\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS system_roles,\n\t\t (\n\t\t SELECT BOOL_OR(rp.${col})\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.active = true\n\t\t AND (r.workspace_id = $2 OR r.is_system = true)\n\t\t ) AS granted`,\n\t\t[userId, workspaceId, permissionKey, superRole],\n\t);\n\treturn {\n\t\tmember: row?.member === true,\n\t\tisSuper: row?.is_super === true,\n\t\tsystemRoles: row?.system_roles ?? [],\n\t\tgranted: row?.granted === true,\n\t};\n}\n\n/**\n * Every grant the member's STORED roles give in this workspace, folded across\n * roles (a member with two roles gets the union) — one query for the\n * effective-permissions read instead of one check per key.\n */\nexport async function listGrantedPermissions(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<Array<{ permissionKey: string; create: boolean; read: boolean; update: boolean; delete: boolean }>> {\n\treturn store.query(\n\t\t`SELECT rp.permission_key AS \"permissionKey\",\n\t\t BOOL_OR(rp.can_create) AS \"create\",\n\t\t BOOL_OR(rp.can_read) AS \"read\",\n\t\t BOOL_OR(rp.can_update) AS \"update\",\n\t\t BOOL_OR(rp.can_delete) AS \"delete\"\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 ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t AND r.active = true\n\t\t AND (r.workspace_id = $2 OR r.is_system = true)\n\t\t GROUP BY rp.permission_key`,\n\t\t[userId, workspaceId],\n\t);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation, IPermissionCatalogEntry, IEffectivePermissions } from './types';\nimport type { IPermissionsConfig } from './config';\nimport { OPERATIONS, PERMISSION_COLUMN } from './constants';\nimport { getMembership, hasRole, listSystemRoleNames } from './services/membership';\nimport { checkPermission, listGrantedPermissions, readAccess } from './services/permissions';\n\nconst ALL_OPERATIONS = Object.values(OPERATIONS) as Operation[];\n\nexport class PermissionsEngine {\n\tprivate superRole: string;\n\tprivate systemGrants: Record<string, Record<string, Operation[]>>;\n\t/** The app's declared resources, or null when it declared none. */\n\treadonly catalog: readonly IPermissionCatalogEntry[] | null;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tconfig: IPermissionsConfig = {},\n\t) {\n\t\tthis.superRole = config.superRole ?? 'owner';\n\t\tthis.catalog = config.catalog ? Object.freeze(config.catalog.map((e) => ({ ...e }))) : null;\n\t\tthis.systemGrants = config.systemGrants ?? {};\n\t\t// A misspelled resource in systemGrants would grant nothing, silently.\n\t\t// Refuse to start instead — a config mistake, caught at boot.\n\t\tif (this.catalog) {\n\t\t\tconst known = new Set(this.catalog.map((e) => e.key));\n\t\t\tfor (const [role, grants] of Object.entries(this.systemGrants)) {\n\t\t\t\tfor (const key of Object.keys(grants)) {\n\t\t\t\t\tif (!known.has(key)) {\n\t\t\t\t\t\tthrow new Error(`[permissions] systemGrants.${role}.${key}: '${key}' is not in the catalog`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Is this key in the catalog? Always true when the app declared no catalog. */\n\tisKnown(permissionKey: string): boolean {\n\t\treturn !this.catalog || this.catalog.some((e) => e.key === permissionKey);\n\t}\n\n\t/** The operations a resource supports (all four when not declared). */\n\toperationsOf(permissionKey: string): Operation[] {\n\t\treturn this.catalog?.find((e) => e.key === permissionKey)?.operations ?? ALL_OPERATIONS;\n\t}\n\n\tprivate async allows(\n\t\tuserId: string,\n\t\tworkspaceId: string,\n\t\tpermissionKey: string,\n\t\toperation: Operation,\n\t): Promise<boolean> {\n\t\tif (Object.keys(this.systemGrants).length > 0) {\n\t\t\tconst roles = await listSystemRoleNames(userId, workspaceId, this.store);\n\t\t\tif (roles.some((r) => this.systemGrants[r]?.[permissionKey]?.includes(operation) ?? false)) return true;\n\t\t}\n\t\treturn checkPermission(userId, workspaceId, permissionKey, operation, this.store);\n\t}\n\n\t/**\n\t * Everything this member may do in this workspace, in one read — what a\n\t * client needs to show only the actions that will succeed. Null when the\n\t * user is not a member. Lists the catalog's resources (every operation\n\t * present, true or false) plus any granted key outside it.\n\t */\n\tasync effective(userId: string, workspaceId: string): Promise<IEffectivePermissions | null> {\n\t\tif (!(await getMembership(userId, workspaceId, this.store))) return null;\n\n\t\tconst permissions: Record<string, Record<Operation, boolean>> = {};\n\t\tconst entry = (key: string) =>\n\t\t\t(permissions[key] ??= { create: false, read: false, update: false, delete: false });\n\t\tfor (const e of this.catalog ?? []) entry(e.key);\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) {\n\t\t\tfor (const e of this.catalog ?? []) {\n\t\t\t\tfor (const op of e.operations ?? ALL_OPERATIONS) entry(e.key)[op] = true;\n\t\t\t}\n\t\t\treturn { isSuper: true, permissions };\n\t\t}\n\n\t\tconst [systemRoles, stored] = await Promise.all([\n\t\t\tObject.keys(this.systemGrants).length ? listSystemRoleNames(userId, workspaceId, this.store) : Promise.resolve([]),\n\t\t\tlistGrantedPermissions(userId, workspaceId, this.store),\n\t\t]);\n\t\tfor (const role of systemRoles) {\n\t\t\tfor (const [key, ops] of Object.entries(this.systemGrants[role] ?? {})) {\n\t\t\t\tfor (const op of ops) entry(key)[op] = true;\n\t\t\t}\n\t\t}\n\t\tfor (const g of stored) {\n\t\t\tconst e = entry(g.permissionKey);\n\t\t\tfor (const op of ALL_OPERATIONS) e[op] ||= Boolean(g[op]);\n\t\t}\n\t\treturn { isSuper: false, permissions };\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\t// An operation outside the four has no grant column; keep the old,\n\t\t// step-by-step path for it rather than change what it answers.\n\t\tif (!Object.hasOwn(PERMISSION_COLUMN, operation)) {\n\t\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\t\tif (!isMember) return false;\n\t\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\t\t\treturn this.allows(userId, workspaceId, permissionKey, operation);\n\t\t}\n\n\t\t// Member → super-role → config system grants → stored grants: the same\n\t\t// decisions in the same order, read in one round-trip.\n\t\tconst a = await readAccess(userId, workspaceId, permissionKey, operation, this.superRole, this.store);\n\t\tif (!a.member) return false;\n\t\tif (a.isSuper) return true;\n\t\tif (a.systemRoles.some((r) => this.systemGrants[r]?.[permissionKey]?.includes(operation) ?? false)) return true;\n\t\treturn a.granted;\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) => this.allows(userId, workspaceId, c.permissionKey, c.operation)),\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) => this.allows(userId, workspaceId, c.permissionKey, c.operation)),\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\t// Baked in at build time by tsup.base, so the operator's Modules page can\n\t// say what is actually deployed rather than 'not reported'.\n\treadonly version = process.env['FONDERIE_PKG_VERSION'] ?? '0.0.0-dev';\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"]}
|
|
@@ -104,6 +104,65 @@ async function checkPermission(userId, workspaceId, permissionKey, operation, st
|
|
|
104
104
|
);
|
|
105
105
|
return row?.has_permission ?? false;
|
|
106
106
|
}
|
|
107
|
+
async function readAccess(userId, workspaceId, permissionKey, operation, superRole, store) {
|
|
108
|
+
const col = PERMISSION_COLUMN[operation];
|
|
109
|
+
const [row] = await store.query(
|
|
110
|
+
`SELECT
|
|
111
|
+
EXISTS (
|
|
112
|
+
SELECT 1
|
|
113
|
+
FROM fonderie_role_user_workspaces ruw
|
|
114
|
+
JOIN fonderie_roles r ON r.id = ruw.role_id
|
|
115
|
+
WHERE ruw.user_id = $1
|
|
116
|
+
AND ruw.workspace_id = $2
|
|
117
|
+
AND ruw.removed = false
|
|
118
|
+
AND ruw.suspended = false
|
|
119
|
+
) AS member,
|
|
120
|
+
EXISTS (
|
|
121
|
+
SELECT 1
|
|
122
|
+
FROM fonderie_role_user_workspaces ruw
|
|
123
|
+
JOIN fonderie_roles r ON r.id = ruw.role_id
|
|
124
|
+
WHERE ruw.user_id = $1
|
|
125
|
+
AND ruw.workspace_id = $2
|
|
126
|
+
AND r.name = $4
|
|
127
|
+
AND r.is_system = true
|
|
128
|
+
AND r.active = true
|
|
129
|
+
AND ruw.removed = false
|
|
130
|
+
AND ruw.suspended = false
|
|
131
|
+
) AS is_super,
|
|
132
|
+
ARRAY (
|
|
133
|
+
SELECT DISTINCT r.name
|
|
134
|
+
FROM fonderie_role_user_workspaces ruw
|
|
135
|
+
JOIN fonderie_roles r ON r.id = ruw.role_id
|
|
136
|
+
WHERE ruw.user_id = $1
|
|
137
|
+
AND ruw.workspace_id = $2
|
|
138
|
+
AND r.is_system = true
|
|
139
|
+
AND r.active = true
|
|
140
|
+
AND ruw.removed = false
|
|
141
|
+
AND ruw.suspended = false
|
|
142
|
+
) AS system_roles,
|
|
143
|
+
(
|
|
144
|
+
SELECT BOOL_OR(rp.${col})
|
|
145
|
+
FROM fonderie_role_user_workspaces ruw
|
|
146
|
+
JOIN fonderie_roles r ON ruw.role_id = r.id
|
|
147
|
+
JOIN fonderie_role_permissions rp ON r.id = rp.role_id
|
|
148
|
+
WHERE ruw.user_id = $1
|
|
149
|
+
AND ruw.workspace_id = $2
|
|
150
|
+
AND rp.workspace_id = $2
|
|
151
|
+
AND rp.permission_key = $3
|
|
152
|
+
AND ruw.removed = false
|
|
153
|
+
AND ruw.suspended = false
|
|
154
|
+
AND r.active = true
|
|
155
|
+
AND (r.workspace_id = $2 OR r.is_system = true)
|
|
156
|
+
) AS granted`,
|
|
157
|
+
[userId, workspaceId, permissionKey, superRole]
|
|
158
|
+
);
|
|
159
|
+
return {
|
|
160
|
+
member: row?.member === true,
|
|
161
|
+
isSuper: row?.is_super === true,
|
|
162
|
+
systemRoles: row?.system_roles ?? [],
|
|
163
|
+
granted: row?.granted === true
|
|
164
|
+
};
|
|
165
|
+
}
|
|
107
166
|
async function listGrantedPermissions(userId, workspaceId, store) {
|
|
108
167
|
return store.query(
|
|
109
168
|
`SELECT rp.permission_key AS "permissionKey",
|
|
@@ -201,10 +260,17 @@ var PermissionsEngine = class {
|
|
|
201
260
|
return getMembership(userId, workspaceId, this.store);
|
|
202
261
|
}
|
|
203
262
|
async can(userId, operation, permissionKey, workspaceId) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
263
|
+
if (!Object.hasOwn(PERMISSION_COLUMN, operation)) {
|
|
264
|
+
const isMember = await getMembership(userId, workspaceId, this.store);
|
|
265
|
+
if (!isMember) return false;
|
|
266
|
+
if (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;
|
|
267
|
+
return this.allows(userId, workspaceId, permissionKey, operation);
|
|
268
|
+
}
|
|
269
|
+
const a = await readAccess(userId, workspaceId, permissionKey, operation, this.superRole, this.store);
|
|
270
|
+
if (!a.member) return false;
|
|
271
|
+
if (a.isSuper) return true;
|
|
272
|
+
if (a.systemRoles.some((r) => this.systemGrants[r]?.[permissionKey]?.includes(operation) ?? false)) return true;
|
|
273
|
+
return a.granted;
|
|
208
274
|
}
|
|
209
275
|
async assert(userId, operation, permissionKey, workspaceId) {
|
|
210
276
|
const allowed = await this.can(userId, operation, permissionKey, workspaceId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/middlewares/require-role.ts","../../src/constants.ts","../../src/services/membership.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 { hasAnyRole } 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\t// EXISTS across ALL of the user's roles — a member holding several roles\n\t\t// must not be denied because a single arbitrary row was the wrong one.\n\t\tif (!(await hasAnyRole(ctx.user.id, workspaceId, allowed, store))) {\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 { 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 { 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\n/**\n * Does the user hold the SUPER-ROLE in this workspace? The engine uses this for\n * its total-access bypass, so the match is deliberately restricted to a\n * **system** role: `r.is_system = true`. Without that guard, a member could\n * create a workspace-local role whose name equals the configured super-role\n * (e.g. 'ADMIN') and self-assign it to gain full access — a privilege\n * escalation. System roles are seeded by the app, not member-creatable. Also\n * requires `active = true` so a deactivated role grants nothing.\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 r.is_system = true\n\t\t AND r.active = true\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\n/**\n * Does the user hold ANY of the named roles in this workspace (active, not\n * removed/suspended)? Used by requireRole — an EXISTS over ALL of the user's\n * role rows, so a member holding several roles isn't denied because an\n * arbitrary single row (LIMIT 1) happened to be the wrong one. Not restricted\n * to system roles: requireRole checks arbitrary named roles.\n */\nexport async function hasAnyRole(\n\tuserId: string,\n\tworkspaceId: string,\n\troleNames: string[],\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tif (roleNames.length === 0) return false;\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 = ANY($3)\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS exists`,\n\t\t[userId, workspaceId, roleNames],\n\t);\n\n\treturn row?.exists ?? false;\n}\n\n/**\n * The SYSTEM roles (shared across workspaces) the user holds here — the ones\n * whose rights come from config (systemGrants). System only, for the same\n * reason as hasRole: a member-created local role named 'GUEST' must not pick\n * up the system GUEST's rights.\n */\nexport async function listSystemRoleNames(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<string[]> {\n\tconst rows = await store.query<{ name: string }>(\n\t\t`SELECT DISTINCT r.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 r.is_system = true\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false`,\n\t\t[userId, workspaceId],\n\t);\n\treturn rows.map((r) => r.name);\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.active = true\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\n/**\n * Every grant the member's STORED roles give in this workspace, folded across\n * roles (a member with two roles gets the union) — one query for the\n * effective-permissions read instead of one check per key.\n */\nexport async function listGrantedPermissions(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<Array<{ permissionKey: string; create: boolean; read: boolean; update: boolean; delete: boolean }>> {\n\treturn store.query(\n\t\t`SELECT rp.permission_key AS \"permissionKey\",\n\t\t BOOL_OR(rp.can_create) AS \"create\",\n\t\t BOOL_OR(rp.can_read) AS \"read\",\n\t\t BOOL_OR(rp.can_update) AS \"update\",\n\t\t BOOL_OR(rp.can_delete) AS \"delete\"\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 ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t AND r.active = true\n\t\t AND (r.workspace_id = $2 OR r.is_system = true)\n\t\t GROUP BY rp.permission_key`,\n\t\t[userId, workspaceId],\n\t);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation, IPermissionCatalogEntry, IEffectivePermissions } from './types';\nimport type { IPermissionsConfig } from './config';\nimport { OPERATIONS } from './constants';\nimport { getMembership, hasRole, listSystemRoleNames } from './services/membership';\nimport { checkPermission, listGrantedPermissions } from './services/permissions';\n\nconst ALL_OPERATIONS = Object.values(OPERATIONS) as Operation[];\n\nexport class PermissionsEngine {\n\tprivate superRole: string;\n\tprivate systemGrants: Record<string, Record<string, Operation[]>>;\n\t/** The app's declared resources, or null when it declared none. */\n\treadonly catalog: readonly IPermissionCatalogEntry[] | null;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tconfig: IPermissionsConfig = {},\n\t) {\n\t\tthis.superRole = config.superRole ?? 'owner';\n\t\tthis.catalog = config.catalog ? Object.freeze(config.catalog.map((e) => ({ ...e }))) : null;\n\t\tthis.systemGrants = config.systemGrants ?? {};\n\t\t// A misspelled resource in systemGrants would grant nothing, silently.\n\t\t// Refuse to start instead — a config mistake, caught at boot.\n\t\tif (this.catalog) {\n\t\t\tconst known = new Set(this.catalog.map((e) => e.key));\n\t\t\tfor (const [role, grants] of Object.entries(this.systemGrants)) {\n\t\t\t\tfor (const key of Object.keys(grants)) {\n\t\t\t\t\tif (!known.has(key)) {\n\t\t\t\t\t\tthrow new Error(`[permissions] systemGrants.${role}.${key}: '${key}' is not in the catalog`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Is this key in the catalog? Always true when the app declared no catalog. */\n\tisKnown(permissionKey: string): boolean {\n\t\treturn !this.catalog || this.catalog.some((e) => e.key === permissionKey);\n\t}\n\n\t/** The operations a resource supports (all four when not declared). */\n\toperationsOf(permissionKey: string): Operation[] {\n\t\treturn this.catalog?.find((e) => e.key === permissionKey)?.operations ?? ALL_OPERATIONS;\n\t}\n\n\tprivate async allows(\n\t\tuserId: string,\n\t\tworkspaceId: string,\n\t\tpermissionKey: string,\n\t\toperation: Operation,\n\t): Promise<boolean> {\n\t\tif (Object.keys(this.systemGrants).length > 0) {\n\t\t\tconst roles = await listSystemRoleNames(userId, workspaceId, this.store);\n\t\t\tif (roles.some((r) => this.systemGrants[r]?.[permissionKey]?.includes(operation) ?? false)) return true;\n\t\t}\n\t\treturn checkPermission(userId, workspaceId, permissionKey, operation, this.store);\n\t}\n\n\t/**\n\t * Everything this member may do in this workspace, in one read — what a\n\t * client needs to show only the actions that will succeed. Null when the\n\t * user is not a member. Lists the catalog's resources (every operation\n\t * present, true or false) plus any granted key outside it.\n\t */\n\tasync effective(userId: string, workspaceId: string): Promise<IEffectivePermissions | null> {\n\t\tif (!(await getMembership(userId, workspaceId, this.store))) return null;\n\n\t\tconst permissions: Record<string, Record<Operation, boolean>> = {};\n\t\tconst entry = (key: string) =>\n\t\t\t(permissions[key] ??= { create: false, read: false, update: false, delete: false });\n\t\tfor (const e of this.catalog ?? []) entry(e.key);\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) {\n\t\t\tfor (const e of this.catalog ?? []) {\n\t\t\t\tfor (const op of e.operations ?? ALL_OPERATIONS) entry(e.key)[op] = true;\n\t\t\t}\n\t\t\treturn { isSuper: true, permissions };\n\t\t}\n\n\t\tconst [systemRoles, stored] = await Promise.all([\n\t\t\tObject.keys(this.systemGrants).length ? listSystemRoleNames(userId, workspaceId, this.store) : Promise.resolve([]),\n\t\t\tlistGrantedPermissions(userId, workspaceId, this.store),\n\t\t]);\n\t\tfor (const role of systemRoles) {\n\t\t\tfor (const [key, ops] of Object.entries(this.systemGrants[role] ?? {})) {\n\t\t\t\tfor (const op of ops) entry(key)[op] = true;\n\t\t\t}\n\t\t}\n\t\tfor (const g of stored) {\n\t\t\tconst e = entry(g.permissionKey);\n\t\t\tfor (const op of ALL_OPERATIONS) e[op] ||= Boolean(g[op]);\n\t\t}\n\t\treturn { isSuper: false, permissions };\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 this.allows(userId, workspaceId, permissionKey, operation);\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) => this.allows(userId, workspaceId, c.permissionKey, c.operation)),\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) => this.allows(userId, workspaceId, c.permissionKey, c.operation)),\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\t// Baked in at build time by tsup.base, so the operator's Modules page can\n\t// say what is actually deployed rather than 'not reported'.\n\treadonly version = process.env['FONDERIE_PKG_VERSION'] ?? '0.0.0-dev';\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/constants.ts","../../src/services/membership.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 { hasAnyRole } 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\t// EXISTS across ALL of the user's roles — a member holding several roles\n\t\t// must not be denied because a single arbitrary row was the wrong one.\n\t\tif (!(await hasAnyRole(ctx.user.id, workspaceId, allowed, store))) {\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 { 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 { 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\n/**\n * Does the user hold the SUPER-ROLE in this workspace? The engine uses this for\n * its total-access bypass, so the match is deliberately restricted to a\n * **system** role: `r.is_system = true`. Without that guard, a member could\n * create a workspace-local role whose name equals the configured super-role\n * (e.g. 'ADMIN') and self-assign it to gain full access — a privilege\n * escalation. System roles are seeded by the app, not member-creatable. Also\n * requires `active = true` so a deactivated role grants nothing.\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 r.is_system = true\n\t\t AND r.active = true\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\n/**\n * Does the user hold ANY of the named roles in this workspace (active, not\n * removed/suspended)? Used by requireRole — an EXISTS over ALL of the user's\n * role rows, so a member holding several roles isn't denied because an\n * arbitrary single row (LIMIT 1) happened to be the wrong one. Not restricted\n * to system roles: requireRole checks arbitrary named roles.\n */\nexport async function hasAnyRole(\n\tuserId: string,\n\tworkspaceId: string,\n\troleNames: string[],\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tif (roleNames.length === 0) return false;\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 = ANY($3)\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS exists`,\n\t\t[userId, workspaceId, roleNames],\n\t);\n\n\treturn row?.exists ?? false;\n}\n\n/**\n * The SYSTEM roles (shared across workspaces) the user holds here — the ones\n * whose rights come from config (systemGrants). System only, for the same\n * reason as hasRole: a member-created local role named 'GUEST' must not pick\n * up the system GUEST's rights.\n */\nexport async function listSystemRoleNames(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<string[]> {\n\tconst rows = await store.query<{ name: string }>(\n\t\t`SELECT DISTINCT r.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 r.is_system = true\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false`,\n\t\t[userId, workspaceId],\n\t);\n\treturn rows.map((r) => r.name);\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.active = true\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\n/**\n * Everything one permission check needs, in ONE round-trip — the same four\n * predicates as getMembership, hasRole (super-role), listSystemRoleNames and\n * checkPermission, each written exactly as there, read side by side:\n *\n * member — an active row joined to an existing role (getMembership)\n * isSuper — holds the super-role as an ACTIVE SYSTEM role (hasRole)\n * systemRoles — the active SYSTEM roles held (listSystemRoleNames)\n * granted — a stored grant for `operation` on `permissionKey` (checkPermission)\n *\n * The caller applies the same order of decisions the engine always has.\n */\nexport async function readAccess(\n\tuserId: string,\n\tworkspaceId: string,\n\tpermissionKey: string,\n\toperation: Operation,\n\tsuperRole: string,\n\tstore: IStoreAdapter,\n): Promise<{ member: boolean; isSuper: boolean; systemRoles: string[]; granted: boolean }> {\n\tconst col = PERMISSION_COLUMN[operation];\n\tconst [row] = await store.query<{ member: boolean; is_super: boolean; system_roles: string[] | null; granted: boolean | null }>(\n\t\t`SELECT\n\t\t 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 ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS member,\n\t\t 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 = $4\n\t\t AND r.is_system = true\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS is_super,\n\t\t ARRAY (\n\t\t SELECT DISTINCT r.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 r.is_system = true\n\t\t AND r.active = true\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t ) AS system_roles,\n\t\t (\n\t\t SELECT BOOL_OR(rp.${col})\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.active = true\n\t\t AND (r.workspace_id = $2 OR r.is_system = true)\n\t\t ) AS granted`,\n\t\t[userId, workspaceId, permissionKey, superRole],\n\t);\n\treturn {\n\t\tmember: row?.member === true,\n\t\tisSuper: row?.is_super === true,\n\t\tsystemRoles: row?.system_roles ?? [],\n\t\tgranted: row?.granted === true,\n\t};\n}\n\n/**\n * Every grant the member's STORED roles give in this workspace, folded across\n * roles (a member with two roles gets the union) — one query for the\n * effective-permissions read instead of one check per key.\n */\nexport async function listGrantedPermissions(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<Array<{ permissionKey: string; create: boolean; read: boolean; update: boolean; delete: boolean }>> {\n\treturn store.query(\n\t\t`SELECT rp.permission_key AS \"permissionKey\",\n\t\t BOOL_OR(rp.can_create) AS \"create\",\n\t\t BOOL_OR(rp.can_read) AS \"read\",\n\t\t BOOL_OR(rp.can_update) AS \"update\",\n\t\t BOOL_OR(rp.can_delete) AS \"delete\"\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 ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t AND r.active = true\n\t\t AND (r.workspace_id = $2 OR r.is_system = true)\n\t\t GROUP BY rp.permission_key`,\n\t\t[userId, workspaceId],\n\t);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation, IPermissionCatalogEntry, IEffectivePermissions } from './types';\nimport type { IPermissionsConfig } from './config';\nimport { OPERATIONS, PERMISSION_COLUMN } from './constants';\nimport { getMembership, hasRole, listSystemRoleNames } from './services/membership';\nimport { checkPermission, listGrantedPermissions, readAccess } from './services/permissions';\n\nconst ALL_OPERATIONS = Object.values(OPERATIONS) as Operation[];\n\nexport class PermissionsEngine {\n\tprivate superRole: string;\n\tprivate systemGrants: Record<string, Record<string, Operation[]>>;\n\t/** The app's declared resources, or null when it declared none. */\n\treadonly catalog: readonly IPermissionCatalogEntry[] | null;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tconfig: IPermissionsConfig = {},\n\t) {\n\t\tthis.superRole = config.superRole ?? 'owner';\n\t\tthis.catalog = config.catalog ? Object.freeze(config.catalog.map((e) => ({ ...e }))) : null;\n\t\tthis.systemGrants = config.systemGrants ?? {};\n\t\t// A misspelled resource in systemGrants would grant nothing, silently.\n\t\t// Refuse to start instead — a config mistake, caught at boot.\n\t\tif (this.catalog) {\n\t\t\tconst known = new Set(this.catalog.map((e) => e.key));\n\t\t\tfor (const [role, grants] of Object.entries(this.systemGrants)) {\n\t\t\t\tfor (const key of Object.keys(grants)) {\n\t\t\t\t\tif (!known.has(key)) {\n\t\t\t\t\t\tthrow new Error(`[permissions] systemGrants.${role}.${key}: '${key}' is not in the catalog`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Is this key in the catalog? Always true when the app declared no catalog. */\n\tisKnown(permissionKey: string): boolean {\n\t\treturn !this.catalog || this.catalog.some((e) => e.key === permissionKey);\n\t}\n\n\t/** The operations a resource supports (all four when not declared). */\n\toperationsOf(permissionKey: string): Operation[] {\n\t\treturn this.catalog?.find((e) => e.key === permissionKey)?.operations ?? ALL_OPERATIONS;\n\t}\n\n\tprivate async allows(\n\t\tuserId: string,\n\t\tworkspaceId: string,\n\t\tpermissionKey: string,\n\t\toperation: Operation,\n\t): Promise<boolean> {\n\t\tif (Object.keys(this.systemGrants).length > 0) {\n\t\t\tconst roles = await listSystemRoleNames(userId, workspaceId, this.store);\n\t\t\tif (roles.some((r) => this.systemGrants[r]?.[permissionKey]?.includes(operation) ?? false)) return true;\n\t\t}\n\t\treturn checkPermission(userId, workspaceId, permissionKey, operation, this.store);\n\t}\n\n\t/**\n\t * Everything this member may do in this workspace, in one read — what a\n\t * client needs to show only the actions that will succeed. Null when the\n\t * user is not a member. Lists the catalog's resources (every operation\n\t * present, true or false) plus any granted key outside it.\n\t */\n\tasync effective(userId: string, workspaceId: string): Promise<IEffectivePermissions | null> {\n\t\tif (!(await getMembership(userId, workspaceId, this.store))) return null;\n\n\t\tconst permissions: Record<string, Record<Operation, boolean>> = {};\n\t\tconst entry = (key: string) =>\n\t\t\t(permissions[key] ??= { create: false, read: false, update: false, delete: false });\n\t\tfor (const e of this.catalog ?? []) entry(e.key);\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) {\n\t\t\tfor (const e of this.catalog ?? []) {\n\t\t\t\tfor (const op of e.operations ?? ALL_OPERATIONS) entry(e.key)[op] = true;\n\t\t\t}\n\t\t\treturn { isSuper: true, permissions };\n\t\t}\n\n\t\tconst [systemRoles, stored] = await Promise.all([\n\t\t\tObject.keys(this.systemGrants).length ? listSystemRoleNames(userId, workspaceId, this.store) : Promise.resolve([]),\n\t\t\tlistGrantedPermissions(userId, workspaceId, this.store),\n\t\t]);\n\t\tfor (const role of systemRoles) {\n\t\t\tfor (const [key, ops] of Object.entries(this.systemGrants[role] ?? {})) {\n\t\t\t\tfor (const op of ops) entry(key)[op] = true;\n\t\t\t}\n\t\t}\n\t\tfor (const g of stored) {\n\t\t\tconst e = entry(g.permissionKey);\n\t\t\tfor (const op of ALL_OPERATIONS) e[op] ||= Boolean(g[op]);\n\t\t}\n\t\treturn { isSuper: false, permissions };\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\t// An operation outside the four has no grant column; keep the old,\n\t\t// step-by-step path for it rather than change what it answers.\n\t\tif (!Object.hasOwn(PERMISSION_COLUMN, operation)) {\n\t\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\t\tif (!isMember) return false;\n\t\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\t\t\treturn this.allows(userId, workspaceId, permissionKey, operation);\n\t\t}\n\n\t\t// Member → super-role → config system grants → stored grants: the same\n\t\t// decisions in the same order, read in one round-trip.\n\t\tconst a = await readAccess(userId, workspaceId, permissionKey, operation, this.superRole, this.store);\n\t\tif (!a.member) return false;\n\t\tif (a.isSuper) return true;\n\t\tif (a.systemRoles.some((r) => this.systemGrants[r]?.[permissionKey]?.includes(operation) ?? false)) return true;\n\t\treturn a.granted;\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) => this.allows(userId, workspaceId, c.permissionKey, c.operation)),\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) => this.allows(userId, workspaceId, c.permissionKey, c.operation)),\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\t// Baked in at build time by tsup.base, so the operator's Modules page can\n\t// say what is actually deployed rather than 'not reported'.\n\treadonly version = process.env['FONDERIE_PKG_VERSION'] ?? '0.0.0-dev';\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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fonderie/permissions",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.6",
|
|
4
4
|
"fonderie": { "stability": "stable" },
|
|
5
5
|
"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.",
|
|
6
6
|
"keywords": [
|
|
@@ -58,13 +58,13 @@
|
|
|
58
58
|
"check": "biome check --write src"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@fonderie/core": "^0.
|
|
61
|
+
"@fonderie/core": "^0.33.0",
|
|
62
62
|
"@fonderie/store": "^0.7.0"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@fonderie/core": "../core",
|
|
66
66
|
"@fonderie/store": "../store",
|
|
67
|
-
"@types/node": "^26.6.
|
|
67
|
+
"@types/node": "^26.6.4",
|
|
68
68
|
"tsup": "^8.5.1",
|
|
69
69
|
"tsx": "^4.23.15",
|
|
70
70
|
"typescript": "^6.0.3"
|