@fonderie/permissions 5.0.31 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/brain/signatures.md +18 -0
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +5 -0
- package/dist/config.d.ts +5 -0
- package/dist/index.cjs +112 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -2
- package/dist/index.d.ts +17 -2
- package/dist/index.js +112 -17
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +111 -16
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.js +111 -16
- package/dist/middlewares/index.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +18 -1
- package/dist/types.d.ts +18 -1
- package/package.json +2 -2
package/brain/signatures.md
CHANGED
|
@@ -37,6 +37,18 @@ interface IRoleWithPermissions extends IRole {
|
|
|
37
37
|
permissions: IPermission[];
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
interface IPermissionCatalogEntry {
|
|
41
|
+
key: string;
|
|
42
|
+
operations?: Operation[];
|
|
43
|
+
label?: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface IEffectivePermissions {
|
|
48
|
+
isSuper: boolean;
|
|
49
|
+
permissions: Record<string, Record<Operation, boolean>>;
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
new PermissionsModule(store: IStoreAdapter, config?: IPermissionsConfig): PermissionsModule
|
|
41
53
|
.engine: PermissionsEngine
|
|
42
54
|
.name: "@fonderie/permissions"
|
|
@@ -45,6 +57,10 @@ new PermissionsModule(store: IStoreAdapter, config?: IPermissionsConfig): Permis
|
|
|
45
57
|
.install(app: IFonderieApp): void
|
|
46
58
|
|
|
47
59
|
new PermissionsEngine(store: IStoreAdapter, config?: IPermissionsConfig): PermissionsEngine
|
|
60
|
+
.catalog: readonly IPermissionCatalogEntry[] | null
|
|
61
|
+
.isKnown(permissionKey: string): boolean
|
|
62
|
+
.operationsOf(permissionKey: string): Operation[]
|
|
63
|
+
.effective(userId: string, workspaceId: string): Promise<IEffectivePermissions | null>
|
|
48
64
|
.getMembership(userId: string, workspaceId: string): Promise<IMembership | null>
|
|
49
65
|
.can(userId: string, operation: Operation, permissionKey: string, workspaceId: string): Promise<boolean>
|
|
50
66
|
.assert(userId: string, operation: Operation, permissionKey: string, workspaceId: string): Promise<void>
|
|
@@ -61,6 +77,8 @@ new PermissionDeniedError(operation: string, permissionKey: string): PermissionD
|
|
|
61
77
|
interface IPermissionsConfig {
|
|
62
78
|
wildcards?: boolean;
|
|
63
79
|
superRole?: string;
|
|
80
|
+
catalog?: IPermissionCatalogEntry[];
|
|
81
|
+
systemGrants?: Record<string, Record<string, Operation[]>>;
|
|
64
82
|
}
|
|
65
83
|
|
|
66
84
|
const OPERATIONS: { readonly CREATE: "create"; readonly READ: "read"; readonly UPDATE: "update"; readonly DELETE: "delete"; }
|
package/dist/config.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"sources":["../src/config.ts"],"sourcesContent":["import type { Operation, IPermissionCatalogEntry } from './types';\n\nexport interface IPermissionsConfig {\n\t// When true, a permission of action='*' or resource='*' matches anything\n\t// Default: true\n\twildcards?: boolean;\n\n\t// Super-admin role name — members with this role bypass all checks\n\t// Default: 'owner'\n\tsuperRole?: string;\n\n\t// Every resource the app checks with requirePermission, declared ONCE. The\n\t// role editor reads it (GET /workspaces/permissions/catalog), saving a role\n\t// refuses keys outside it, and the effective-permissions read lists exactly\n\t// these. Unset: no catalog — any key is accepted, as before.\n\tcatalog?: IPermissionCatalogEntry[];\n\n\t// What each SYSTEM role (shared by every workspace — e.g. the GUEST role\n\t// invitees land on) may do, by resource. Read from config at check time, so\n\t// every workspace — existing ones included — has it immediately, and\n\t// changing it here changes it everywhere: no seeding, no backfill. The\n\t// super role needs no entry. Custom roles keep their stored grants.\n\t// systemGrants: { GUEST: { jobs: ['read', 'update'], customers: ['read'] } }\n\tsystemGrants?: Record<string, Record<string, Operation[]>>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
|
package/dist/config.d.cts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { IPermissionCatalogEntry } from './types.cjs';
|
|
2
|
+
import { Operation } from '@fonderie/core';
|
|
3
|
+
|
|
1
4
|
interface IPermissionsConfig {
|
|
2
5
|
wildcards?: boolean;
|
|
3
6
|
superRole?: string;
|
|
7
|
+
catalog?: IPermissionCatalogEntry[];
|
|
8
|
+
systemGrants?: Record<string, Record<string, Operation[]>>;
|
|
4
9
|
}
|
|
5
10
|
|
|
6
11
|
export type { IPermissionsConfig };
|
package/dist/config.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { IPermissionCatalogEntry } from './types.js';
|
|
2
|
+
import { Operation } from '@fonderie/core';
|
|
3
|
+
|
|
1
4
|
interface IPermissionsConfig {
|
|
2
5
|
wildcards?: boolean;
|
|
3
6
|
superRole?: string;
|
|
7
|
+
catalog?: IPermissionCatalogEntry[];
|
|
8
|
+
systemGrants?: Record<string, Record<string, Operation[]>>;
|
|
4
9
|
}
|
|
5
10
|
|
|
6
11
|
export type { IPermissionsConfig };
|
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,15 @@ __export(index_exports, {
|
|
|
31
31
|
});
|
|
32
32
|
module.exports = __toCommonJS(index_exports);
|
|
33
33
|
|
|
34
|
+
// src/constants.ts
|
|
35
|
+
var import_core = require("@fonderie/core");
|
|
36
|
+
var PERMISSION_COLUMN = {
|
|
37
|
+
create: "can_create",
|
|
38
|
+
read: "can_read",
|
|
39
|
+
update: "can_update",
|
|
40
|
+
delete: "can_delete"
|
|
41
|
+
};
|
|
42
|
+
|
|
34
43
|
// src/services/membership.ts
|
|
35
44
|
async function getMembership(userId, workspaceId, store) {
|
|
36
45
|
const [row] = await store.query(
|
|
@@ -88,15 +97,21 @@ async function hasAnyRole(userId, workspaceId, roleNames, store) {
|
|
|
88
97
|
);
|
|
89
98
|
return row?.exists ?? false;
|
|
90
99
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
async function listSystemRoleNames(userId, workspaceId, store) {
|
|
101
|
+
const rows = await store.query(
|
|
102
|
+
`SELECT DISTINCT r.name
|
|
103
|
+
FROM fonderie_role_user_workspaces ruw
|
|
104
|
+
JOIN fonderie_roles r ON r.id = ruw.role_id
|
|
105
|
+
WHERE ruw.user_id = $1
|
|
106
|
+
AND ruw.workspace_id = $2
|
|
107
|
+
AND r.is_system = true
|
|
108
|
+
AND r.active = true
|
|
109
|
+
AND ruw.removed = false
|
|
110
|
+
AND ruw.suspended = false`,
|
|
111
|
+
[userId, workspaceId]
|
|
112
|
+
);
|
|
113
|
+
return rows.map((r) => r.name);
|
|
114
|
+
}
|
|
100
115
|
|
|
101
116
|
// src/services/permissions.ts
|
|
102
117
|
async function checkPermission(userId, workspaceId, permissionKey, operation, store) {
|
|
@@ -119,15 +134,99 @@ async function checkPermission(userId, workspaceId, permissionKey, operation, st
|
|
|
119
134
|
);
|
|
120
135
|
return row?.has_permission ?? false;
|
|
121
136
|
}
|
|
137
|
+
async function listGrantedPermissions(userId, workspaceId, store) {
|
|
138
|
+
return store.query(
|
|
139
|
+
`SELECT rp.permission_key AS "permissionKey",
|
|
140
|
+
BOOL_OR(rp.can_create) AS "create",
|
|
141
|
+
BOOL_OR(rp.can_read) AS "read",
|
|
142
|
+
BOOL_OR(rp.can_update) AS "update",
|
|
143
|
+
BOOL_OR(rp.can_delete) AS "delete"
|
|
144
|
+
FROM fonderie_role_user_workspaces ruw
|
|
145
|
+
JOIN fonderie_roles r ON ruw.role_id = r.id
|
|
146
|
+
JOIN fonderie_role_permissions rp ON r.id = rp.role_id
|
|
147
|
+
WHERE ruw.user_id = $1
|
|
148
|
+
AND ruw.workspace_id = $2
|
|
149
|
+
AND rp.workspace_id = $2
|
|
150
|
+
AND ruw.removed = false
|
|
151
|
+
AND ruw.suspended = false
|
|
152
|
+
AND r.active = true
|
|
153
|
+
AND (r.workspace_id = $2 OR r.is_system = true)
|
|
154
|
+
GROUP BY rp.permission_key`,
|
|
155
|
+
[userId, workspaceId]
|
|
156
|
+
);
|
|
157
|
+
}
|
|
122
158
|
|
|
123
159
|
// src/engine.ts
|
|
160
|
+
var ALL_OPERATIONS = Object.values(import_core.OPERATIONS);
|
|
124
161
|
var PermissionsEngine = class {
|
|
125
162
|
constructor(store, config = {}) {
|
|
126
163
|
this.store = store;
|
|
127
164
|
this.superRole = config.superRole ?? "owner";
|
|
165
|
+
this.catalog = config.catalog ? Object.freeze(config.catalog.map((e) => ({ ...e }))) : null;
|
|
166
|
+
this.systemGrants = config.systemGrants ?? {};
|
|
167
|
+
if (this.catalog) {
|
|
168
|
+
const known = new Set(this.catalog.map((e) => e.key));
|
|
169
|
+
for (const [role, grants] of Object.entries(this.systemGrants)) {
|
|
170
|
+
for (const key of Object.keys(grants)) {
|
|
171
|
+
if (!known.has(key)) {
|
|
172
|
+
throw new Error(`[permissions] systemGrants.${role}.${key}: '${key}' is not in the catalog`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
128
177
|
}
|
|
129
178
|
store;
|
|
130
179
|
superRole;
|
|
180
|
+
systemGrants;
|
|
181
|
+
/** The app's declared resources, or null when it declared none. */
|
|
182
|
+
catalog;
|
|
183
|
+
/** Is this key in the catalog? Always true when the app declared no catalog. */
|
|
184
|
+
isKnown(permissionKey) {
|
|
185
|
+
return !this.catalog || this.catalog.some((e) => e.key === permissionKey);
|
|
186
|
+
}
|
|
187
|
+
/** The operations a resource supports (all four when not declared). */
|
|
188
|
+
operationsOf(permissionKey) {
|
|
189
|
+
return this.catalog?.find((e) => e.key === permissionKey)?.operations ?? ALL_OPERATIONS;
|
|
190
|
+
}
|
|
191
|
+
async allows(userId, workspaceId, permissionKey, operation) {
|
|
192
|
+
if (Object.keys(this.systemGrants).length > 0) {
|
|
193
|
+
const roles = await listSystemRoleNames(userId, workspaceId, this.store);
|
|
194
|
+
if (roles.some((r) => this.systemGrants[r]?.[permissionKey]?.includes(operation) ?? false)) return true;
|
|
195
|
+
}
|
|
196
|
+
return checkPermission(userId, workspaceId, permissionKey, operation, this.store);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Everything this member may do in this workspace, in one read — what a
|
|
200
|
+
* client needs to show only the actions that will succeed. Null when the
|
|
201
|
+
* user is not a member. Lists the catalog's resources (every operation
|
|
202
|
+
* present, true or false) plus any granted key outside it.
|
|
203
|
+
*/
|
|
204
|
+
async effective(userId, workspaceId) {
|
|
205
|
+
if (!await getMembership(userId, workspaceId, this.store)) return null;
|
|
206
|
+
const permissions = {};
|
|
207
|
+
const entry = (key) => permissions[key] ??= { create: false, read: false, update: false, delete: false };
|
|
208
|
+
for (const e of this.catalog ?? []) entry(e.key);
|
|
209
|
+
if (await hasRole(userId, workspaceId, this.superRole, this.store)) {
|
|
210
|
+
for (const e of this.catalog ?? []) {
|
|
211
|
+
for (const op of e.operations ?? ALL_OPERATIONS) entry(e.key)[op] = true;
|
|
212
|
+
}
|
|
213
|
+
return { isSuper: true, permissions };
|
|
214
|
+
}
|
|
215
|
+
const [systemRoles, stored] = await Promise.all([
|
|
216
|
+
Object.keys(this.systemGrants).length ? listSystemRoleNames(userId, workspaceId, this.store) : Promise.resolve([]),
|
|
217
|
+
listGrantedPermissions(userId, workspaceId, this.store)
|
|
218
|
+
]);
|
|
219
|
+
for (const role of systemRoles) {
|
|
220
|
+
for (const [key, ops] of Object.entries(this.systemGrants[role] ?? {})) {
|
|
221
|
+
for (const op of ops) entry(key)[op] = true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
for (const g of stored) {
|
|
225
|
+
const e = entry(g.permissionKey);
|
|
226
|
+
for (const op of ALL_OPERATIONS) e[op] ||= Boolean(g[op]);
|
|
227
|
+
}
|
|
228
|
+
return { isSuper: false, permissions };
|
|
229
|
+
}
|
|
131
230
|
async getMembership(userId, workspaceId) {
|
|
132
231
|
return getMembership(userId, workspaceId, this.store);
|
|
133
232
|
}
|
|
@@ -135,7 +234,7 @@ var PermissionsEngine = class {
|
|
|
135
234
|
const isMember = await getMembership(userId, workspaceId, this.store);
|
|
136
235
|
if (!isMember) return false;
|
|
137
236
|
if (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;
|
|
138
|
-
return
|
|
237
|
+
return this.allows(userId, workspaceId, permissionKey, operation);
|
|
139
238
|
}
|
|
140
239
|
async assert(userId, operation, permissionKey, workspaceId) {
|
|
141
240
|
const allowed = await this.can(userId, operation, permissionKey, workspaceId);
|
|
@@ -148,9 +247,7 @@ var PermissionsEngine = class {
|
|
|
148
247
|
if (!isMember) return false;
|
|
149
248
|
if (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;
|
|
150
249
|
const results = await Promise.all(
|
|
151
|
-
checks.map(
|
|
152
|
-
(c) => checkPermission(userId, workspaceId, c.permissionKey, c.operation, this.store)
|
|
153
|
-
)
|
|
250
|
+
checks.map((c) => this.allows(userId, workspaceId, c.permissionKey, c.operation))
|
|
154
251
|
);
|
|
155
252
|
return results.every(Boolean);
|
|
156
253
|
}
|
|
@@ -159,9 +256,7 @@ var PermissionsEngine = class {
|
|
|
159
256
|
if (!isMember) return false;
|
|
160
257
|
if (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;
|
|
161
258
|
const results = await Promise.all(
|
|
162
|
-
checks.map(
|
|
163
|
-
(c) => checkPermission(userId, workspaceId, c.permissionKey, c.operation, this.store)
|
|
164
|
-
)
|
|
259
|
+
checks.map((c) => this.allows(userId, workspaceId, c.permissionKey, c.operation))
|
|
165
260
|
);
|
|
166
261
|
return results.some(Boolean);
|
|
167
262
|
}
|
|
@@ -181,7 +276,7 @@ var PermissionsModule = class {
|
|
|
181
276
|
name = "@fonderie/permissions";
|
|
182
277
|
// Baked in at build time by tsup.base, so the operator's Modules page can
|
|
183
278
|
// say what is actually deployed rather than 'not reported'.
|
|
184
|
-
version = "5.0
|
|
279
|
+
version = "5.1.0";
|
|
185
280
|
deps = ["@fonderie/auth"];
|
|
186
281
|
constructor(store, config = {}) {
|
|
187
282
|
this.engine = new PermissionsEngine(store, config);
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/services/membership.ts","../src/constants.ts","../src/services/permissions.ts","../src/engine.ts","../src/module.ts","../src/middlewares/require-role.ts","../src/middlewares/require-permission.ts","../src/services/grants.ts"],"sourcesContent":["export type {\n\tOperation,\n\tPermissionKey,\n\tIRole,\n\tIPermission,\n\tIMembership,\n\tIRoleWithPermissions,\n} from './types';\nexport { PermissionsModule } from './module';\nexport { PermissionsEngine, PermissionDeniedError } from './engine';\nexport type { IPermissionsConfig } from './config';\n\nexport { OPERATIONS, PERMISSION_COLUMN } from './constants';\nexport { requireRole } from './middlewares/require-role';\nexport { requirePermission } from './middlewares/require-permission';\n\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 { 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","import type { Operation } from './types';\n\n// Canonical definition moved to @fonderie/core; re-exported here for back-compat.\nexport { OPERATIONS } from '@fonderie/core';\n\nexport const PERMISSION_COLUMN: Record<Operation, string> = {\n\tcreate: 'can_create',\n\tread: 'can_read',\n\tupdate: 'can_update',\n\tdelete: 'can_delete',\n};\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation } from '../types';\nimport { PERMISSION_COLUMN } from '../constants';\n\nexport async function checkPermission(\n\tuserId: string,\n\tworkspaceId: string,\n\tpermissionKey: string,\n\toperation: Operation,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst col = PERMISSION_COLUMN[operation];\n\n\tconst [row] = await store.query<{ has_permission: boolean }>(\n\t\t`SELECT BOOL_OR(rp.${col}) AS has_permission\n\t\t FROM fonderie_role_user_workspaces ruw\n\t\t JOIN fonderie_roles r ON ruw.role_id = r.id\n\t\t JOIN fonderie_role_permissions rp ON r.id = rp.role_id\n\t\t WHERE ruw.user_id = $1\n\t\t AND ruw.workspace_id = $2\n\t\t AND rp.workspace_id = $2\n\t\t AND rp.permission_key = $3\n\t\t AND ruw.removed = false\n\t\t AND ruw.suspended = false\n\t\t AND r.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","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { Operation } from './types';\nimport type { IPermissionsConfig } from './config';\nimport { getMembership, hasRole } from './services/membership';\nimport { checkPermission } from './services/permissions';\n\nexport class PermissionsEngine {\n\tprivate superRole: string;\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tconfig: IPermissionsConfig = {},\n\t) {\n\t\tthis.superRole = config.superRole ?? 'owner';\n\t}\n\n\tasync getMembership(userId: string, workspaceId: string) {\n\t\treturn getMembership(userId, workspaceId, this.store);\n\t}\n\n\tasync can(\n\t\tuserId: string,\n\t\toperation: Operation,\n\t\tpermissionKey: string,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\treturn checkPermission(userId, workspaceId, permissionKey, operation, this.store);\n\t}\n\n\tasync assert(\n\t\tuserId: string,\n\t\toperation: Operation,\n\t\tpermissionKey: string,\n\t\tworkspaceId: string,\n\t): Promise<void> {\n\t\tconst allowed = await this.can(userId, operation, permissionKey, workspaceId);\n\t\tif (!allowed) {\n\t\t\tthrow new PermissionDeniedError(operation, permissionKey);\n\t\t}\n\t}\n\n\tasync canAll(\n\t\tuserId: string,\n\t\tchecks: Array<{ operation: Operation; permissionKey: string }>,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\tconst results = await Promise.all(\n\t\t\tchecks.map((c) =>\n\t\t\t\tcheckPermission(userId, workspaceId, c.permissionKey, c.operation, this.store),\n\t\t\t),\n\t\t);\n\t\treturn results.every(Boolean);\n\t}\n\n\tasync canAny(\n\t\tuserId: string,\n\t\tchecks: Array<{ operation: Operation; permissionKey: string }>,\n\t\tworkspaceId: string,\n\t): Promise<boolean> {\n\t\tconst isMember = await getMembership(userId, workspaceId, this.store);\n\t\tif (!isMember) return false;\n\n\t\tif (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;\n\n\t\tconst results = await Promise.all(\n\t\t\tchecks.map((c) =>\n\t\t\t\tcheckPermission(userId, workspaceId, c.permissionKey, c.operation, this.store),\n\t\t\t),\n\t\t);\n\t\treturn results.some(Boolean);\n\t}\n}\n\nexport class PermissionDeniedError extends Error {\n\treadonly status = 403;\n\n\tconstructor(operation: string, permissionKey: string) {\n\t\tsuper(`Permission denied: ${operation}:${permissionKey}`);\n\t\tthis.name = 'PermissionDeniedError';\n\t}\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IPermissionsConfig } from './config';\nimport { PermissionsEngine } from './engine';\n\nexport const PERMISSIONS_ENGINE_KEY = 'fonderie.permissions.engine';\n\nexport class PermissionsModule implements IFonderieModule {\n\treadonly engine: PermissionsEngine;\n\treadonly name = '@fonderie/permissions';\n\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 * 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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACGA,kBAA2B;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,sBAAU;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,IAAAA,eAAqC;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,iBAAO,6BAAe,kBAAK,cAAc,gBAAgB,cAAc;AAAA,IACxE;AAEA,UAAM,SAAS,IAAI,KAAK,sBAAsB;AAC9C,QAAI,EAAE,kBAAkB,oBAAoB;AAC3C,iBAAO,6BAAe,kBAAK,cAAc,gBAAgB,kCAAkC;AAAA,IAC5F;AAEA,UAAM,cACL,IAAI,WAAW,MACd,IAAI,KAAK,QAAQ,IAA2C,aAAa;AAE3E,QAAI,CAAC,aAAa;AACjB,iBAAO,6BAAe,kBAAK,aAAa,sBAAsB,4BAA4B;AAAA,IAC3F;AAIA,QAAI,CAAE,MAAM,WAAW,IAAI,KAAK,IAAI,aAAa,SAAS,KAAK,GAAI;AAClE,iBAAO,6BAAe,kBAAK,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,IAAAC,eAAqC;AAQrC,SAASC,aAAY,WAAsB,eAAmC;AAC7E,SAAO,OAAO,KAAK,SAAS;AAC3B,QAAI,CAAC,IAAI,MAAM;AACd,iBAAO,6BAAe,kBAAK,cAAc,gBAAgB,cAAc;AAAA,IACxE;AAEA,UAAM,SAAS,IAAI,KAAK,sBAAsB;AAC9C,QAAI,EAAE,kBAAkB,oBAAoB;AAC3C,iBAAO,6BAAe,kBAAK,cAAc,gBAAgB,kCAAkC;AAAA,IAC5F;AAEA,UAAM,cAAc,mBAAmB,GAAG;AAC1C,QAAI,CAAC,aAAa;AACjB,iBAAO,6BAAe,kBAAK,aAAa,sBAAsB,4BAA4B;AAAA,IAC3F;AAEA,UAAM,UAAU,MAAM,OAAO,IAAI,IAAI,KAAK,IAAI,WAAW,eAAe,WAAW;AACnF,QAAI,CAAC,SAAS;AACb,iBAAO;AAAA,QACN,kBAAK;AAAA,QACL;AAAA,QACA,sBAAsB,SAAS,IAAI,aAAa;AAAA,MACjD;AAAA,IACD;AAEA,WAAO,KAAK;AAAA,EACb;AACD;AASO,SAAS,kBACf,WACA,eACA,KACA,MACiC;AACjC,QAAM,UAAUA,aAAY,WAAW,aAAa;AACpD,MAAI,QAAQ,UAAa,SAAS,OAAW,QAAO,QAAQ,KAAK,IAAI;AACrE,SAAO;AACR;AAEA,SAAS,mBAAmB,KAGV;AACjB,MAAI,IAAI,WAAW,GAAI,QAAO,IAAI,UAAU;AAE5C,QAAM,SAAS,IAAI,KAAK,QAAQ;AAChC,SAAO,SAAS,aAAa,KAAK;AACnC;;;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":["import_core","import_core","makeHandler"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IMembership } from './types.cjs';
|
|
1
|
+
import { IPermissionCatalogEntry, IEffectivePermissions, IMembership } from './types.cjs';
|
|
2
2
|
export { IPermission, IRole, IRoleWithPermissions, PermissionKey } from './types.cjs';
|
|
3
3
|
import { Operation, IFonderieModule, IFonderieApp } from '@fonderie/core';
|
|
4
4
|
export { OPERATIONS, Operation } from '@fonderie/core';
|
|
@@ -9,7 +9,22 @@ export { requirePermission, requireRole } from './middlewares/index.cjs';
|
|
|
9
9
|
declare class PermissionsEngine {
|
|
10
10
|
private store;
|
|
11
11
|
private superRole;
|
|
12
|
+
private systemGrants;
|
|
13
|
+
/** The app's declared resources, or null when it declared none. */
|
|
14
|
+
readonly catalog: readonly IPermissionCatalogEntry[] | null;
|
|
12
15
|
constructor(store: IStoreAdapter, config?: IPermissionsConfig);
|
|
16
|
+
/** Is this key in the catalog? Always true when the app declared no catalog. */
|
|
17
|
+
isKnown(permissionKey: string): boolean;
|
|
18
|
+
/** The operations a resource supports (all four when not declared). */
|
|
19
|
+
operationsOf(permissionKey: string): Operation[];
|
|
20
|
+
private allows;
|
|
21
|
+
/**
|
|
22
|
+
* Everything this member may do in this workspace, in one read — what a
|
|
23
|
+
* client needs to show only the actions that will succeed. Null when the
|
|
24
|
+
* user is not a member. Lists the catalog's resources (every operation
|
|
25
|
+
* present, true or false) plus any granted key outside it.
|
|
26
|
+
*/
|
|
27
|
+
effective(userId: string, workspaceId: string): Promise<IEffectivePermissions | null>;
|
|
13
28
|
getMembership(userId: string, workspaceId: string): Promise<IMembership | null>;
|
|
14
29
|
can(userId: string, operation: Operation, permissionKey: string, workspaceId: string): Promise<boolean>;
|
|
15
30
|
assert(userId: string, operation: Operation, permissionKey: string, workspaceId: string): Promise<void>;
|
|
@@ -47,4 +62,4 @@ interface IGrant {
|
|
|
47
62
|
}
|
|
48
63
|
declare function listGrants(store: IStoreAdapter): Promise<IGrant[]>;
|
|
49
64
|
|
|
50
|
-
export { type IGrant, IMembership, IPermissionsConfig, PERMISSION_COLUMN, PermissionDeniedError, PermissionsEngine, PermissionsModule, listGrants };
|
|
65
|
+
export { IEffectivePermissions, type IGrant, IMembership, IPermissionCatalogEntry, IPermissionsConfig, PERMISSION_COLUMN, PermissionDeniedError, PermissionsEngine, PermissionsModule, listGrants };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IMembership } from './types.js';
|
|
1
|
+
import { IPermissionCatalogEntry, IEffectivePermissions, IMembership } from './types.js';
|
|
2
2
|
export { IPermission, IRole, IRoleWithPermissions, PermissionKey } from './types.js';
|
|
3
3
|
import { Operation, IFonderieModule, IFonderieApp } from '@fonderie/core';
|
|
4
4
|
export { OPERATIONS, Operation } from '@fonderie/core';
|
|
@@ -9,7 +9,22 @@ export { requirePermission, requireRole } from './middlewares/index.js';
|
|
|
9
9
|
declare class PermissionsEngine {
|
|
10
10
|
private store;
|
|
11
11
|
private superRole;
|
|
12
|
+
private systemGrants;
|
|
13
|
+
/** The app's declared resources, or null when it declared none. */
|
|
14
|
+
readonly catalog: readonly IPermissionCatalogEntry[] | null;
|
|
12
15
|
constructor(store: IStoreAdapter, config?: IPermissionsConfig);
|
|
16
|
+
/** Is this key in the catalog? Always true when the app declared no catalog. */
|
|
17
|
+
isKnown(permissionKey: string): boolean;
|
|
18
|
+
/** The operations a resource supports (all four when not declared). */
|
|
19
|
+
operationsOf(permissionKey: string): Operation[];
|
|
20
|
+
private allows;
|
|
21
|
+
/**
|
|
22
|
+
* Everything this member may do in this workspace, in one read — what a
|
|
23
|
+
* client needs to show only the actions that will succeed. Null when the
|
|
24
|
+
* user is not a member. Lists the catalog's resources (every operation
|
|
25
|
+
* present, true or false) plus any granted key outside it.
|
|
26
|
+
*/
|
|
27
|
+
effective(userId: string, workspaceId: string): Promise<IEffectivePermissions | null>;
|
|
13
28
|
getMembership(userId: string, workspaceId: string): Promise<IMembership | null>;
|
|
14
29
|
can(userId: string, operation: Operation, permissionKey: string, workspaceId: string): Promise<boolean>;
|
|
15
30
|
assert(userId: string, operation: Operation, permissionKey: string, workspaceId: string): Promise<void>;
|
|
@@ -47,4 +62,4 @@ interface IGrant {
|
|
|
47
62
|
}
|
|
48
63
|
declare function listGrants(store: IStoreAdapter): Promise<IGrant[]>;
|
|
49
64
|
|
|
50
|
-
export { type IGrant, IMembership, IPermissionsConfig, PERMISSION_COLUMN, PermissionDeniedError, PermissionsEngine, PermissionsModule, listGrants };
|
|
65
|
+
export { IEffectivePermissions, type IGrant, IMembership, IPermissionCatalogEntry, IPermissionsConfig, PERMISSION_COLUMN, PermissionDeniedError, PermissionsEngine, PermissionsModule, listGrants };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
// src/constants.ts
|
|
2
|
+
import { OPERATIONS } from "@fonderie/core";
|
|
3
|
+
var PERMISSION_COLUMN = {
|
|
4
|
+
create: "can_create",
|
|
5
|
+
read: "can_read",
|
|
6
|
+
update: "can_update",
|
|
7
|
+
delete: "can_delete"
|
|
8
|
+
};
|
|
9
|
+
|
|
1
10
|
// src/services/membership.ts
|
|
2
11
|
async function getMembership(userId, workspaceId, store) {
|
|
3
12
|
const [row] = await store.query(
|
|
@@ -55,15 +64,21 @@ async function hasAnyRole(userId, workspaceId, roleNames, store) {
|
|
|
55
64
|
);
|
|
56
65
|
return row?.exists ?? false;
|
|
57
66
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
async function listSystemRoleNames(userId, workspaceId, store) {
|
|
68
|
+
const rows = await store.query(
|
|
69
|
+
`SELECT DISTINCT r.name
|
|
70
|
+
FROM fonderie_role_user_workspaces ruw
|
|
71
|
+
JOIN fonderie_roles r ON r.id = ruw.role_id
|
|
72
|
+
WHERE ruw.user_id = $1
|
|
73
|
+
AND ruw.workspace_id = $2
|
|
74
|
+
AND r.is_system = true
|
|
75
|
+
AND r.active = true
|
|
76
|
+
AND ruw.removed = false
|
|
77
|
+
AND ruw.suspended = false`,
|
|
78
|
+
[userId, workspaceId]
|
|
79
|
+
);
|
|
80
|
+
return rows.map((r) => r.name);
|
|
81
|
+
}
|
|
67
82
|
|
|
68
83
|
// src/services/permissions.ts
|
|
69
84
|
async function checkPermission(userId, workspaceId, permissionKey, operation, store) {
|
|
@@ -86,15 +101,99 @@ async function checkPermission(userId, workspaceId, permissionKey, operation, st
|
|
|
86
101
|
);
|
|
87
102
|
return row?.has_permission ?? false;
|
|
88
103
|
}
|
|
104
|
+
async function listGrantedPermissions(userId, workspaceId, store) {
|
|
105
|
+
return store.query(
|
|
106
|
+
`SELECT rp.permission_key AS "permissionKey",
|
|
107
|
+
BOOL_OR(rp.can_create) AS "create",
|
|
108
|
+
BOOL_OR(rp.can_read) AS "read",
|
|
109
|
+
BOOL_OR(rp.can_update) AS "update",
|
|
110
|
+
BOOL_OR(rp.can_delete) AS "delete"
|
|
111
|
+
FROM fonderie_role_user_workspaces ruw
|
|
112
|
+
JOIN fonderie_roles r ON ruw.role_id = r.id
|
|
113
|
+
JOIN fonderie_role_permissions rp ON r.id = rp.role_id
|
|
114
|
+
WHERE ruw.user_id = $1
|
|
115
|
+
AND ruw.workspace_id = $2
|
|
116
|
+
AND rp.workspace_id = $2
|
|
117
|
+
AND ruw.removed = false
|
|
118
|
+
AND ruw.suspended = false
|
|
119
|
+
AND r.active = true
|
|
120
|
+
AND (r.workspace_id = $2 OR r.is_system = true)
|
|
121
|
+
GROUP BY rp.permission_key`,
|
|
122
|
+
[userId, workspaceId]
|
|
123
|
+
);
|
|
124
|
+
}
|
|
89
125
|
|
|
90
126
|
// src/engine.ts
|
|
127
|
+
var ALL_OPERATIONS = Object.values(OPERATIONS);
|
|
91
128
|
var PermissionsEngine = class {
|
|
92
129
|
constructor(store, config = {}) {
|
|
93
130
|
this.store = store;
|
|
94
131
|
this.superRole = config.superRole ?? "owner";
|
|
132
|
+
this.catalog = config.catalog ? Object.freeze(config.catalog.map((e) => ({ ...e }))) : null;
|
|
133
|
+
this.systemGrants = config.systemGrants ?? {};
|
|
134
|
+
if (this.catalog) {
|
|
135
|
+
const known = new Set(this.catalog.map((e) => e.key));
|
|
136
|
+
for (const [role, grants] of Object.entries(this.systemGrants)) {
|
|
137
|
+
for (const key of Object.keys(grants)) {
|
|
138
|
+
if (!known.has(key)) {
|
|
139
|
+
throw new Error(`[permissions] systemGrants.${role}.${key}: '${key}' is not in the catalog`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
95
144
|
}
|
|
96
145
|
store;
|
|
97
146
|
superRole;
|
|
147
|
+
systemGrants;
|
|
148
|
+
/** The app's declared resources, or null when it declared none. */
|
|
149
|
+
catalog;
|
|
150
|
+
/** Is this key in the catalog? Always true when the app declared no catalog. */
|
|
151
|
+
isKnown(permissionKey) {
|
|
152
|
+
return !this.catalog || this.catalog.some((e) => e.key === permissionKey);
|
|
153
|
+
}
|
|
154
|
+
/** The operations a resource supports (all four when not declared). */
|
|
155
|
+
operationsOf(permissionKey) {
|
|
156
|
+
return this.catalog?.find((e) => e.key === permissionKey)?.operations ?? ALL_OPERATIONS;
|
|
157
|
+
}
|
|
158
|
+
async allows(userId, workspaceId, permissionKey, operation) {
|
|
159
|
+
if (Object.keys(this.systemGrants).length > 0) {
|
|
160
|
+
const roles = await listSystemRoleNames(userId, workspaceId, this.store);
|
|
161
|
+
if (roles.some((r) => this.systemGrants[r]?.[permissionKey]?.includes(operation) ?? false)) return true;
|
|
162
|
+
}
|
|
163
|
+
return checkPermission(userId, workspaceId, permissionKey, operation, this.store);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Everything this member may do in this workspace, in one read — what a
|
|
167
|
+
* client needs to show only the actions that will succeed. Null when the
|
|
168
|
+
* user is not a member. Lists the catalog's resources (every operation
|
|
169
|
+
* present, true or false) plus any granted key outside it.
|
|
170
|
+
*/
|
|
171
|
+
async effective(userId, workspaceId) {
|
|
172
|
+
if (!await getMembership(userId, workspaceId, this.store)) return null;
|
|
173
|
+
const permissions = {};
|
|
174
|
+
const entry = (key) => permissions[key] ??= { create: false, read: false, update: false, delete: false };
|
|
175
|
+
for (const e of this.catalog ?? []) entry(e.key);
|
|
176
|
+
if (await hasRole(userId, workspaceId, this.superRole, this.store)) {
|
|
177
|
+
for (const e of this.catalog ?? []) {
|
|
178
|
+
for (const op of e.operations ?? ALL_OPERATIONS) entry(e.key)[op] = true;
|
|
179
|
+
}
|
|
180
|
+
return { isSuper: true, permissions };
|
|
181
|
+
}
|
|
182
|
+
const [systemRoles, stored] = await Promise.all([
|
|
183
|
+
Object.keys(this.systemGrants).length ? listSystemRoleNames(userId, workspaceId, this.store) : Promise.resolve([]),
|
|
184
|
+
listGrantedPermissions(userId, workspaceId, this.store)
|
|
185
|
+
]);
|
|
186
|
+
for (const role of systemRoles) {
|
|
187
|
+
for (const [key, ops] of Object.entries(this.systemGrants[role] ?? {})) {
|
|
188
|
+
for (const op of ops) entry(key)[op] = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (const g of stored) {
|
|
192
|
+
const e = entry(g.permissionKey);
|
|
193
|
+
for (const op of ALL_OPERATIONS) e[op] ||= Boolean(g[op]);
|
|
194
|
+
}
|
|
195
|
+
return { isSuper: false, permissions };
|
|
196
|
+
}
|
|
98
197
|
async getMembership(userId, workspaceId) {
|
|
99
198
|
return getMembership(userId, workspaceId, this.store);
|
|
100
199
|
}
|
|
@@ -102,7 +201,7 @@ var PermissionsEngine = class {
|
|
|
102
201
|
const isMember = await getMembership(userId, workspaceId, this.store);
|
|
103
202
|
if (!isMember) return false;
|
|
104
203
|
if (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;
|
|
105
|
-
return
|
|
204
|
+
return this.allows(userId, workspaceId, permissionKey, operation);
|
|
106
205
|
}
|
|
107
206
|
async assert(userId, operation, permissionKey, workspaceId) {
|
|
108
207
|
const allowed = await this.can(userId, operation, permissionKey, workspaceId);
|
|
@@ -115,9 +214,7 @@ var PermissionsEngine = class {
|
|
|
115
214
|
if (!isMember) return false;
|
|
116
215
|
if (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;
|
|
117
216
|
const results = await Promise.all(
|
|
118
|
-
checks.map(
|
|
119
|
-
(c) => checkPermission(userId, workspaceId, c.permissionKey, c.operation, this.store)
|
|
120
|
-
)
|
|
217
|
+
checks.map((c) => this.allows(userId, workspaceId, c.permissionKey, c.operation))
|
|
121
218
|
);
|
|
122
219
|
return results.every(Boolean);
|
|
123
220
|
}
|
|
@@ -126,9 +223,7 @@ var PermissionsEngine = class {
|
|
|
126
223
|
if (!isMember) return false;
|
|
127
224
|
if (await hasRole(userId, workspaceId, this.superRole, this.store)) return true;
|
|
128
225
|
const results = await Promise.all(
|
|
129
|
-
checks.map(
|
|
130
|
-
(c) => checkPermission(userId, workspaceId, c.permissionKey, c.operation, this.store)
|
|
131
|
-
)
|
|
226
|
+
checks.map((c) => this.allows(userId, workspaceId, c.permissionKey, c.operation))
|
|
132
227
|
);
|
|
133
228
|
return results.some(Boolean);
|
|
134
229
|
}
|
|
@@ -148,7 +243,7 @@ var PermissionsModule = class {
|
|
|
148
243
|
name = "@fonderie/permissions";
|
|
149
244
|
// Baked in at build time by tsup.base, so the operator's Modules page can
|
|
150
245
|
// say what is actually deployed rather than 'not reported'.
|
|
151
|
-
version = "5.0
|
|
246
|
+
version = "5.1.0";
|
|
152
247
|
deps = ["@fonderie/auth"];
|
|
153
248
|
constructor(store, config = {}) {
|
|
154
249
|
this.engine = new PermissionsEngine(store, config);
|