@soulbatical/tetra-core 0.1.5 → 0.1.8
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.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware/entitlementsMiddleware.d.ts +142 -0
- package/dist/middleware/entitlementsMiddleware.d.ts.map +1 -0
- package/dist/middleware/entitlementsMiddleware.js +246 -0
- package/dist/middleware/entitlementsMiddleware.js.map +1 -0
- package/dist/middleware/permissionsMiddleware.d.ts +181 -0
- package/dist/middleware/permissionsMiddleware.d.ts.map +1 -0
- package/dist/middleware/permissionsMiddleware.js +237 -0
- package/dist/middleware/permissionsMiddleware.js.map +1 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -52,6 +52,10 @@ export { validateQueryParams, sanitizeSearchTerm, validatePagination, validateSo
|
|
|
52
52
|
export { registerFeatureValidators, batchRegisterValidators } from './middleware/autoRegisterValidators.js';
|
|
53
53
|
export { configureSecurity } from './middleware/securityMiddleware.js';
|
|
54
54
|
export type { SecurityConfig } from './middleware/securityMiddleware.js';
|
|
55
|
+
export { configureEntitlements, requireFeature, requireWithinLimit, hasFeature, getLimit, getOrgFeatures, invalidatePlanCache } from './middleware/entitlementsMiddleware.js';
|
|
56
|
+
export type { EntitlementsConfig, PlanFeatures, LimitCheckResult } from './middleware/entitlementsMiddleware.js';
|
|
57
|
+
export { configurePermissions, registerFeaturePermissions, requirePermission, requireAnyPermission, checkPermission, canSeeNav, canSeeTab, canDoAction, getAllPermissionConfigs, getPermissionsForRole } from './middleware/permissionsMiddleware.js';
|
|
58
|
+
export type { FeaturePermissions, RolePermissions, PermissionValue, UIPermissions } from './middleware/permissionsMiddleware.js';
|
|
55
59
|
export { createLogger, rootLogger } from './utils/logger.js';
|
|
56
60
|
export type { Logger } from './utils/logger.js';
|
|
57
61
|
export { validateEnvironment } from './utils/validateEnvironment.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,eAAe,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,eAAe,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,UAAU,EAAE,uBAAuB,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AAClY,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AACtE,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AACjF,YAAY,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AACjG,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC/J,YAAY,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAC;AAG9E,OAAO,EAAE,mBAAmB,EAAE,MAAM,6CAA6C,CAAC;AAClF,OAAO,EAAE,sBAAsB,EAAE,MAAM,gDAAgD,CAAC;AACxF,OAAO,EAAE,4BAA4B,EAAE,MAAM,gDAAgD,CAAC;AAG9F,OAAO,EAAE,kBAAkB,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAC3G,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,2CAA2C,CAAC;AACvG,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7F,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAC;AAC/E,OAAO,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;AAGzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,YAAY,EAAE,iBAAiB,IAAI,UAAU,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAC3G,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AACzE,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AACnH,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AAGjE,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,wBAAwB,EAAE,WAAW,EAAE,0BAA0B,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAC9N,YAAY,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACvH,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAChJ,OAAO,EAAE,yBAAyB,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AAC5G,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AACvE,YAAY,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAGzE,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,kBAAkB,EAAE,UAAU,EAAE,QAAQ,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAC9K,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,wCAAwC,CAAC;AAGjH,OAAO,EAAE,oBAAoB,EAAE,0BAA0B,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,eAAe,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AACtP,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,uCAAuC,CAAC;AAGjI,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC7D,YAAY,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,eAAe,EAAE,eAAe,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AACpO,YAAY,EAAE,aAAa,EAAE,gBAAgB,EAAE,aAAa,EAAE,WAAW,EAAE,yBAAyB,EAAE,MAAM,yCAAyC,CAAC;AACtJ,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,0CAA0C,CAAC;AACpG,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AACrE,OAAO,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AACpH,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAGzF,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAC1H,OAAO,EAAE,oBAAoB,EAAE,6BAA6B,EAAE,eAAe,EAAE,MAAM,8CAA8C,CAAC;AAGpI,YAAY,EAAE,oBAAoB,EAAE,QAAQ,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAOjL,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAGlE,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -46,6 +46,10 @@ export { authenticateToken, authenticateTokenOnly, optionalAuth, requireSuperAdm
|
|
|
46
46
|
export { validateQueryParams, sanitizeSearchTerm, validatePagination, validateSort, validateFields } from './middleware/validateQueryParams.js';
|
|
47
47
|
export { registerFeatureValidators, batchRegisterValidators } from './middleware/autoRegisterValidators.js';
|
|
48
48
|
export { configureSecurity } from './middleware/securityMiddleware.js';
|
|
49
|
+
// Entitlements (plan-based feature gating)
|
|
50
|
+
export { configureEntitlements, requireFeature, requireWithinLimit, hasFeature, getLimit, getOrgFeatures, invalidatePlanCache } from './middleware/entitlementsMiddleware.js';
|
|
51
|
+
// Permissions (config-driven RBAC)
|
|
52
|
+
export { configurePermissions, registerFeaturePermissions, requirePermission, requireAnyPermission, checkPermission, canSeeNav, canSeeTab, canDoAction, getAllPermissionConfigs, getPermissionsForRole } from './middleware/permissionsMiddleware.js';
|
|
49
53
|
// ─── Utils ──────────────────────────────────────────────────
|
|
50
54
|
export { createLogger, rootLogger } from './utils/logger.js';
|
|
51
55
|
export { validateEnvironment } from './utils/validateEnvironment.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAMtE,+DAA+D;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,6CAA6C,CAAC;AAClF,OAAO,EAAE,sBAAsB,EAAE,MAAM,gDAAgD,CAAC;AACxF,OAAO,EAAE,4BAA4B,EAAE,MAAM,gDAAgD,CAAC;AAE9F,+DAA+D;AAC/D,OAAO,EAAE,kBAAkB,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAE3G,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7F,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAC;AAC/E,OAAO,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;AAEzE,+DAA+D;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AAEvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAEzE,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AAEjE,gEAAgE;AAChE,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,wBAAwB,EAAE,WAAW,EAAE,0BAA0B,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAE9N,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAChJ,OAAO,EAAE,yBAAyB,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AAC5G,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAGvE,+DAA+D;AAC/D,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,eAAe,EAAE,eAAe,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAEpO,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,0CAA0C,CAAC;AACpG,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AACrE,OAAO,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AAGpH,+DAA+D;AAC/D,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAC1H,OAAO,EAAE,oBAAoB,EAAE,6BAA6B,EAAE,eAAe,EAAE,MAAM,8CAA8C,CAAC;AAKpI,gEAAgE;AAChE,wEAAwE;AACxE,uFAAuF;AAEvF,8DAA8D;AAC9D,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAElE,+DAA+D;AAC/D,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @tetra/core — Entitlements Middleware (Plan-Based Feature Gating)
|
|
3
|
+
*
|
|
4
|
+
* Controls what an ORGANIZATION can do based on their subscription plan.
|
|
5
|
+
* This is separate from auth/RBAC (what a USER can do based on their role).
|
|
6
|
+
*
|
|
7
|
+
* Evaluation order per request:
|
|
8
|
+
* 1. Auth → Is there a valid JWT?
|
|
9
|
+
* 2. Entitlement → Does this ORG's plan include this feature?
|
|
10
|
+
* 3. RBAC → Does this USER's role allow this action?
|
|
11
|
+
*
|
|
12
|
+
* Projects call configureEntitlements() once at startup with their plan definitions
|
|
13
|
+
* and a getPlanId function that reads the org's current plan from the database.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* configureEntitlements({
|
|
18
|
+
* plans: {
|
|
19
|
+
* free: { max_active_tracks: 3, calendar_sync: false },
|
|
20
|
+
* pro: { max_active_tracks: -1, calendar_sync: true },
|
|
21
|
+
* },
|
|
22
|
+
* getPlanId: async (orgId) => {
|
|
23
|
+
* const { data } = await systemDB('entitlements')
|
|
24
|
+
* .from('organizations').select('plan').eq('id', orgId).single();
|
|
25
|
+
* return data?.plan ?? 'free';
|
|
26
|
+
* },
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* // In routes:
|
|
30
|
+
* router.post('/upload', requireFeature('calendar_sync'), handler);
|
|
31
|
+
* router.post('/', requireWithinLimit('max_active_tracks', countFn), handler);
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
import { Response, NextFunction } from 'express';
|
|
35
|
+
import type { AuthenticatedRequest } from './authMiddleware.js';
|
|
36
|
+
/**
|
|
37
|
+
* Plan features map. Keys are feature names, values are:
|
|
38
|
+
* - boolean: feature is on/off
|
|
39
|
+
* - number: numeric limit (-1 = unlimited, 0 = disabled)
|
|
40
|
+
*/
|
|
41
|
+
export type PlanFeatures = Record<string, boolean | number>;
|
|
42
|
+
/**
|
|
43
|
+
* Configuration for the entitlements system.
|
|
44
|
+
*/
|
|
45
|
+
export interface EntitlementsConfig {
|
|
46
|
+
/**
|
|
47
|
+
* Plan definitions. Key is the plan ID (e.g., 'free', 'pro').
|
|
48
|
+
* Values are feature maps.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* plans: {
|
|
52
|
+
* free: { max_projects: 3, ai_autofix: false },
|
|
53
|
+
* pro: { max_projects: -1, ai_autofix: true },
|
|
54
|
+
* }
|
|
55
|
+
*/
|
|
56
|
+
plans: Record<string, PlanFeatures>;
|
|
57
|
+
/**
|
|
58
|
+
* Async function to get the current plan ID for an organization.
|
|
59
|
+
* Called on each entitlement check (results are cached per request).
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* getPlanId: async (orgId) => {
|
|
63
|
+
* const { data } = await db.from('organizations').select('plan').eq('id', orgId).single();
|
|
64
|
+
* return data?.plan ?? 'free';
|
|
65
|
+
* }
|
|
66
|
+
*/
|
|
67
|
+
getPlanId: (organizationId: string) => Promise<string>;
|
|
68
|
+
/**
|
|
69
|
+
* Default plan ID when org has no plan or plan is unknown.
|
|
70
|
+
* Defaults to 'free'.
|
|
71
|
+
*/
|
|
72
|
+
defaultPlanId?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Cache TTL in milliseconds for plan lookups.
|
|
75
|
+
* Defaults to 60000 (1 minute).
|
|
76
|
+
*/
|
|
77
|
+
cacheTtlMs?: number;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Result of a limit check.
|
|
81
|
+
*/
|
|
82
|
+
export interface LimitCheckResult {
|
|
83
|
+
allowed: boolean;
|
|
84
|
+
current: number;
|
|
85
|
+
limit: number;
|
|
86
|
+
planId: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Configure the entitlements system. Call once at app startup.
|
|
90
|
+
*/
|
|
91
|
+
export declare function configureEntitlements(entitlementsConfig: EntitlementsConfig): void;
|
|
92
|
+
/**
|
|
93
|
+
* Check if a plan has a specific feature enabled.
|
|
94
|
+
*
|
|
95
|
+
* - Boolean features: returns the boolean value
|
|
96
|
+
* - Numeric features: returns true if value !== 0
|
|
97
|
+
* - Unknown features: returns false (fail closed)
|
|
98
|
+
*/
|
|
99
|
+
export declare function hasFeature(planId: string, feature: string): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Get the numeric limit for a feature on a plan.
|
|
102
|
+
* Returns -1 for unlimited, 0 for disabled.
|
|
103
|
+
*/
|
|
104
|
+
export declare function getLimit(planId: string, feature: string): number;
|
|
105
|
+
/**
|
|
106
|
+
* Get all features for an organization (resolved from their plan).
|
|
107
|
+
* Useful for frontend: GET /api/org/entitlements
|
|
108
|
+
*/
|
|
109
|
+
export declare function getOrgFeatures(organizationId: string): Promise<{
|
|
110
|
+
planId: string;
|
|
111
|
+
features: PlanFeatures;
|
|
112
|
+
}>;
|
|
113
|
+
/**
|
|
114
|
+
* Invalidate the plan cache for an organization.
|
|
115
|
+
* Call this after plan changes (e.g., Stripe webhook).
|
|
116
|
+
*/
|
|
117
|
+
export declare function invalidatePlanCache(organizationId?: string): void;
|
|
118
|
+
/**
|
|
119
|
+
* Express middleware: require a feature to be enabled on the org's plan.
|
|
120
|
+
*
|
|
121
|
+
* @param feature - The feature key to check (must match a key in plan features)
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* router.post('/upload', requireFeature('chat_attachments'), handler);
|
|
125
|
+
*/
|
|
126
|
+
export declare function requireFeature(feature: string): (req: AuthenticatedRequest, res: Response, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>;
|
|
127
|
+
/**
|
|
128
|
+
* Express middleware: enforce a numeric limit before creating a resource.
|
|
129
|
+
*
|
|
130
|
+
* @param feature - The feature key for the limit (e.g., 'max_active_tracks')
|
|
131
|
+
* @param countFn - Async function that returns the current usage count for the org
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* const countTracks = async (orgId: string) => {
|
|
135
|
+
* const { count } = await db.from('tracks').select('id', { count: 'exact', head: true })
|
|
136
|
+
* .eq('organization_id', orgId).eq('status', 'active');
|
|
137
|
+
* return count ?? 0;
|
|
138
|
+
* };
|
|
139
|
+
* router.post('/', requireWithinLimit('max_active_tracks', countTracks), handler);
|
|
140
|
+
*/
|
|
141
|
+
export declare function requireWithinLimit(feature: string, countFn: (organizationId: string) => Promise<number>): (req: AuthenticatedRequest, res: Response, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>;
|
|
142
|
+
//# sourceMappingURL=entitlementsMiddleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entitlementsMiddleware.d.ts","sourceRoot":"","sources":["../../src/middleware/entitlementsMiddleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAGjD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAMhE;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC,CAAC;AAE5D;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;;OASG;IACH,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAEpC;;;;;;;;;OASG;IACH,SAAS,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAEvD;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAWD;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,CAOlF;AAgDD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAOnE;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAOhE;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,YAAY,CAAA;CAAE,CAAC,CAGhH;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAMjE;AAID;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,IAC9B,KAAK,oBAAoB,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,wDAiC3E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,IAEtC,KAAK,oBAAoB,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,wDA6D3E"}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @tetra/core — Entitlements Middleware (Plan-Based Feature Gating)
|
|
3
|
+
*
|
|
4
|
+
* Controls what an ORGANIZATION can do based on their subscription plan.
|
|
5
|
+
* This is separate from auth/RBAC (what a USER can do based on their role).
|
|
6
|
+
*
|
|
7
|
+
* Evaluation order per request:
|
|
8
|
+
* 1. Auth → Is there a valid JWT?
|
|
9
|
+
* 2. Entitlement → Does this ORG's plan include this feature?
|
|
10
|
+
* 3. RBAC → Does this USER's role allow this action?
|
|
11
|
+
*
|
|
12
|
+
* Projects call configureEntitlements() once at startup with their plan definitions
|
|
13
|
+
* and a getPlanId function that reads the org's current plan from the database.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* configureEntitlements({
|
|
18
|
+
* plans: {
|
|
19
|
+
* free: { max_active_tracks: 3, calendar_sync: false },
|
|
20
|
+
* pro: { max_active_tracks: -1, calendar_sync: true },
|
|
21
|
+
* },
|
|
22
|
+
* getPlanId: async (orgId) => {
|
|
23
|
+
* const { data } = await systemDB('entitlements')
|
|
24
|
+
* .from('organizations').select('plan').eq('id', orgId).single();
|
|
25
|
+
* return data?.plan ?? 'free';
|
|
26
|
+
* },
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* // In routes:
|
|
30
|
+
* router.post('/upload', requireFeature('calendar_sync'), handler);
|
|
31
|
+
* router.post('/', requireWithinLimit('max_active_tracks', countFn), handler);
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
import { createLogger } from '../utils/logger.js';
|
|
35
|
+
import { RFC7807ErrorResponse } from '../shared/rfc7807ErrorResponse.js';
|
|
36
|
+
const logger = createLogger('middleware:entitlements');
|
|
37
|
+
// ─── State ──────────────────────────────────────────────────
|
|
38
|
+
let config = null;
|
|
39
|
+
// Simple in-memory cache: orgId → { planId, expiresAt }
|
|
40
|
+
const planCache = new Map();
|
|
41
|
+
// ─── Configure ──────────────────────────────────────────────
|
|
42
|
+
/**
|
|
43
|
+
* Configure the entitlements system. Call once at app startup.
|
|
44
|
+
*/
|
|
45
|
+
export function configureEntitlements(entitlementsConfig) {
|
|
46
|
+
config = entitlementsConfig;
|
|
47
|
+
planCache.clear();
|
|
48
|
+
logger.info({ planCount: Object.keys(entitlementsConfig.plans).length }, 'Entitlements configured');
|
|
49
|
+
}
|
|
50
|
+
// ─── Helpers ────────────────────────────────────────────────
|
|
51
|
+
function getDefaultPlanId() {
|
|
52
|
+
return config?.defaultPlanId ?? 'free';
|
|
53
|
+
}
|
|
54
|
+
function getCacheTtl() {
|
|
55
|
+
return config?.cacheTtlMs ?? 60_000;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get the plan ID for an organization, with caching.
|
|
59
|
+
*/
|
|
60
|
+
async function getOrgPlanId(organizationId) {
|
|
61
|
+
if (!config)
|
|
62
|
+
return getDefaultPlanId();
|
|
63
|
+
const cached = planCache.get(organizationId);
|
|
64
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
65
|
+
return cached.planId;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const planId = await config.getPlanId(organizationId);
|
|
69
|
+
const resolvedPlanId = planId && config.plans[planId] ? planId : getDefaultPlanId();
|
|
70
|
+
planCache.set(organizationId, {
|
|
71
|
+
planId: resolvedPlanId,
|
|
72
|
+
expiresAt: Date.now() + getCacheTtl(),
|
|
73
|
+
});
|
|
74
|
+
return resolvedPlanId;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
logger.error({ error, organizationId }, 'Failed to get org plan, falling back to default');
|
|
78
|
+
return getDefaultPlanId();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get the features for a plan ID.
|
|
83
|
+
*/
|
|
84
|
+
function getPlanFeatures(planId) {
|
|
85
|
+
return config?.plans[planId] ?? config?.plans[getDefaultPlanId()] ?? {};
|
|
86
|
+
}
|
|
87
|
+
// ─── Public API ─────────────────────────────────────────────
|
|
88
|
+
/**
|
|
89
|
+
* Check if a plan has a specific feature enabled.
|
|
90
|
+
*
|
|
91
|
+
* - Boolean features: returns the boolean value
|
|
92
|
+
* - Numeric features: returns true if value !== 0
|
|
93
|
+
* - Unknown features: returns false (fail closed)
|
|
94
|
+
*/
|
|
95
|
+
export function hasFeature(planId, feature) {
|
|
96
|
+
const features = getPlanFeatures(planId);
|
|
97
|
+
const value = features[feature];
|
|
98
|
+
if (value === undefined)
|
|
99
|
+
return false;
|
|
100
|
+
if (typeof value === 'boolean')
|
|
101
|
+
return value;
|
|
102
|
+
return value !== 0;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get the numeric limit for a feature on a plan.
|
|
106
|
+
* Returns -1 for unlimited, 0 for disabled.
|
|
107
|
+
*/
|
|
108
|
+
export function getLimit(planId, feature) {
|
|
109
|
+
const features = getPlanFeatures(planId);
|
|
110
|
+
const value = features[feature];
|
|
111
|
+
if (value === undefined)
|
|
112
|
+
return 0;
|
|
113
|
+
if (typeof value === 'boolean')
|
|
114
|
+
return value ? -1 : 0;
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Get all features for an organization (resolved from their plan).
|
|
119
|
+
* Useful for frontend: GET /api/org/entitlements
|
|
120
|
+
*/
|
|
121
|
+
export async function getOrgFeatures(organizationId) {
|
|
122
|
+
const planId = await getOrgPlanId(organizationId);
|
|
123
|
+
return { planId, features: getPlanFeatures(planId) };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Invalidate the plan cache for an organization.
|
|
127
|
+
* Call this after plan changes (e.g., Stripe webhook).
|
|
128
|
+
*/
|
|
129
|
+
export function invalidatePlanCache(organizationId) {
|
|
130
|
+
if (organizationId) {
|
|
131
|
+
planCache.delete(organizationId);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
planCache.clear();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// ─── Middleware ──────────────────────────────────────────────
|
|
138
|
+
/**
|
|
139
|
+
* Express middleware: require a feature to be enabled on the org's plan.
|
|
140
|
+
*
|
|
141
|
+
* @param feature - The feature key to check (must match a key in plan features)
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* router.post('/upload', requireFeature('chat_attachments'), handler);
|
|
145
|
+
*/
|
|
146
|
+
export function requireFeature(feature) {
|
|
147
|
+
return async (req, res, next) => {
|
|
148
|
+
if (!config) {
|
|
149
|
+
logger.warn('requireFeature called but entitlements not configured');
|
|
150
|
+
return next();
|
|
151
|
+
}
|
|
152
|
+
const orgId = req.user?.organizationId || req.user?.active_organization_id;
|
|
153
|
+
if (!orgId) {
|
|
154
|
+
return RFC7807ErrorResponse.unauthorized(res, 'Authentication with organization required');
|
|
155
|
+
}
|
|
156
|
+
// Superadmins bypass entitlement checks
|
|
157
|
+
if (req.user?.is_superadmin || req.user?.isSuperAdmin || req.user?.isSuperadmin) {
|
|
158
|
+
return next();
|
|
159
|
+
}
|
|
160
|
+
const planId = await getOrgPlanId(orgId);
|
|
161
|
+
const allowed = hasFeature(planId, feature);
|
|
162
|
+
if (!allowed) {
|
|
163
|
+
return res.status(403).json({
|
|
164
|
+
type: 'about:blank',
|
|
165
|
+
title: 'Feature not available on your plan',
|
|
166
|
+
detail: `The feature "${feature}" is not included in the "${planId}" plan. Please upgrade.`,
|
|
167
|
+
status: 403,
|
|
168
|
+
instance: req.originalUrl,
|
|
169
|
+
feature,
|
|
170
|
+
currentPlan: planId,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
next();
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Express middleware: enforce a numeric limit before creating a resource.
|
|
178
|
+
*
|
|
179
|
+
* @param feature - The feature key for the limit (e.g., 'max_active_tracks')
|
|
180
|
+
* @param countFn - Async function that returns the current usage count for the org
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* const countTracks = async (orgId: string) => {
|
|
184
|
+
* const { count } = await db.from('tracks').select('id', { count: 'exact', head: true })
|
|
185
|
+
* .eq('organization_id', orgId).eq('status', 'active');
|
|
186
|
+
* return count ?? 0;
|
|
187
|
+
* };
|
|
188
|
+
* router.post('/', requireWithinLimit('max_active_tracks', countTracks), handler);
|
|
189
|
+
*/
|
|
190
|
+
export function requireWithinLimit(feature, countFn) {
|
|
191
|
+
return async (req, res, next) => {
|
|
192
|
+
if (!config) {
|
|
193
|
+
logger.warn('requireWithinLimit called but entitlements not configured');
|
|
194
|
+
return next();
|
|
195
|
+
}
|
|
196
|
+
const orgId = req.user?.organizationId || req.user?.active_organization_id;
|
|
197
|
+
if (!orgId) {
|
|
198
|
+
return RFC7807ErrorResponse.unauthorized(res, 'Authentication with organization required');
|
|
199
|
+
}
|
|
200
|
+
// Superadmins bypass limit checks
|
|
201
|
+
if (req.user?.is_superadmin || req.user?.isSuperAdmin || req.user?.isSuperadmin) {
|
|
202
|
+
return next();
|
|
203
|
+
}
|
|
204
|
+
const planId = await getOrgPlanId(orgId);
|
|
205
|
+
const limit = getLimit(planId, feature);
|
|
206
|
+
// -1 means unlimited
|
|
207
|
+
if (limit === -1) {
|
|
208
|
+
return next();
|
|
209
|
+
}
|
|
210
|
+
// 0 means disabled
|
|
211
|
+
if (limit === 0) {
|
|
212
|
+
return res.status(403).json({
|
|
213
|
+
type: 'about:blank',
|
|
214
|
+
title: 'Feature not available on your plan',
|
|
215
|
+
detail: `The feature "${feature}" is not included in the "${planId}" plan. Please upgrade.`,
|
|
216
|
+
status: 403,
|
|
217
|
+
instance: req.originalUrl,
|
|
218
|
+
feature,
|
|
219
|
+
currentPlan: planId,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const current = await countFn(orgId);
|
|
224
|
+
if (current >= limit) {
|
|
225
|
+
return res.status(403).json({
|
|
226
|
+
type: 'about:blank',
|
|
227
|
+
title: 'Plan limit reached',
|
|
228
|
+
detail: `You have reached the limit of ${limit} for "${feature}" on the "${planId}" plan.`,
|
|
229
|
+
status: 403,
|
|
230
|
+
instance: req.originalUrl,
|
|
231
|
+
feature,
|
|
232
|
+
currentPlan: planId,
|
|
233
|
+
current,
|
|
234
|
+
limit,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
next();
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
logger.error({ error, orgId, feature }, 'Failed to check usage limit');
|
|
241
|
+
// Fail open for count errors — the DB will catch any real violations
|
|
242
|
+
next();
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=entitlementsMiddleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entitlementsMiddleware.js","sourceRoot":"","sources":["../../src/middleware/entitlementsMiddleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @tetra/core — Permissions Middleware (Config-Driven RBAC)
|
|
3
|
+
*
|
|
4
|
+
* Controls what a USER can do based on their role within a feature.
|
|
5
|
+
* This is separate from entitlements (what an ORG can do based on their plan).
|
|
6
|
+
*
|
|
7
|
+
* Projects call configurePermissions() once at startup with their feature
|
|
8
|
+
* permission configs. Each feature defines which roles can create/read/update/delete.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* configurePermissions({
|
|
13
|
+
* notes: {
|
|
14
|
+
* resource: 'notes',
|
|
15
|
+
* roles: {
|
|
16
|
+
* admin: { create: true, read: true, update: true, delete: true },
|
|
17
|
+
* coach: { create: true, read: true, update: 'own', delete: 'own' },
|
|
18
|
+
* client: { create: false, read: true, update: false, delete: false },
|
|
19
|
+
* },
|
|
20
|
+
* ui: {
|
|
21
|
+
* nav: ['admin', 'coach'],
|
|
22
|
+
* tabs: { notities: ['admin', 'coach'] },
|
|
23
|
+
* actions: { create_note: ['admin', 'coach'] },
|
|
24
|
+
* },
|
|
25
|
+
* },
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* // In routes:
|
|
29
|
+
* router.post('/', requirePermission('notes', 'create'), handler);
|
|
30
|
+
* router.delete('/:id', requirePermission('notes', 'delete'), handler);
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
import { Response, NextFunction } from 'express';
|
|
34
|
+
import type { AuthenticatedRequest } from './authMiddleware.js';
|
|
35
|
+
/**
|
|
36
|
+
* Permission value for a role on a specific action.
|
|
37
|
+
* - true: allowed (all records)
|
|
38
|
+
* - false: denied
|
|
39
|
+
* - 'own': allowed on own records only (ownerField must match user id)
|
|
40
|
+
* - 'track_member': allowed on records within tracks the user is a member of
|
|
41
|
+
* - string: custom scope (project-specific, middleware passes through)
|
|
42
|
+
*/
|
|
43
|
+
export type PermissionValue = boolean | 'own' | 'track_member' | string;
|
|
44
|
+
/**
|
|
45
|
+
* CRUD permissions for a role on a feature.
|
|
46
|
+
*/
|
|
47
|
+
export interface RolePermissions {
|
|
48
|
+
create?: PermissionValue;
|
|
49
|
+
read?: PermissionValue;
|
|
50
|
+
update?: PermissionValue;
|
|
51
|
+
delete?: PermissionValue;
|
|
52
|
+
list?: PermissionValue;
|
|
53
|
+
/** Custom actions beyond CRUD */
|
|
54
|
+
[action: string]: PermissionValue | undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* UI visibility config for a feature.
|
|
58
|
+
* Used by frontend usePermissions() hook.
|
|
59
|
+
*/
|
|
60
|
+
export interface UIPermissions {
|
|
61
|
+
/** Which roles see this feature in the nav/sidebar */
|
|
62
|
+
nav?: string[];
|
|
63
|
+
/** Per-tab visibility: { tabId: [roles] } */
|
|
64
|
+
tabs?: Record<string, string[]>;
|
|
65
|
+
/** Per-action visibility: { actionId: [roles] } */
|
|
66
|
+
actions?: Record<string, string[]>;
|
|
67
|
+
/** Per-field visibility: { fieldName: [roles] } */
|
|
68
|
+
fields?: Record<string, string[]>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Permission config for a single feature.
|
|
72
|
+
*/
|
|
73
|
+
export interface FeaturePermissions {
|
|
74
|
+
/** The resource name (e.g., 'notes', 'tracks', 'messages') */
|
|
75
|
+
resource: string;
|
|
76
|
+
/** Permission matrix: role → action → permission value */
|
|
77
|
+
roles: Record<string, RolePermissions>;
|
|
78
|
+
/** UI visibility rules (consumed by frontend) */
|
|
79
|
+
ui?: UIPermissions;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Register permission configs for features. Call once at app startup.
|
|
83
|
+
* Can be called multiple times to add more features.
|
|
84
|
+
*
|
|
85
|
+
* @param configs - Map of resource name → FeaturePermissions
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* configurePermissions({
|
|
89
|
+
* notes: notesPermissions,
|
|
90
|
+
* tracks: tracksPermissions,
|
|
91
|
+
* });
|
|
92
|
+
*/
|
|
93
|
+
export declare function configurePermissions(configs: Record<string, FeaturePermissions>): void;
|
|
94
|
+
/**
|
|
95
|
+
* Register a single feature's permissions.
|
|
96
|
+
*/
|
|
97
|
+
export declare function registerFeaturePermissions(config: FeaturePermissions): void;
|
|
98
|
+
/**
|
|
99
|
+
* Check if a role is allowed to perform an action on a resource.
|
|
100
|
+
*
|
|
101
|
+
* Returns the PermissionValue:
|
|
102
|
+
* - true: allowed
|
|
103
|
+
* - false: denied
|
|
104
|
+
* - 'own': allowed with scope restriction
|
|
105
|
+
* - string: custom scope
|
|
106
|
+
*
|
|
107
|
+
* Unknown roles/resources/actions default to false (fail closed).
|
|
108
|
+
*/
|
|
109
|
+
export declare function checkPermission(resource: string, action: string, role: string): PermissionValue;
|
|
110
|
+
/**
|
|
111
|
+
* Check if a role can see a nav item for a resource.
|
|
112
|
+
*/
|
|
113
|
+
export declare function canSeeNav(resource: string, role: string): boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Check if a role can see a specific tab within a resource.
|
|
116
|
+
*/
|
|
117
|
+
export declare function canSeeTab(resource: string, tabId: string, role: string): boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Check if a role can perform a specific UI action.
|
|
120
|
+
*/
|
|
121
|
+
export declare function canDoAction(resource: string, actionId: string, role: string): boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Get all permission configs. Useful for:
|
|
124
|
+
* - Frontend: GET /api/permissions → returns all UI permissions for the user's role
|
|
125
|
+
* - Documentation: generate permission matrices
|
|
126
|
+
*/
|
|
127
|
+
export declare function getAllPermissionConfigs(): Record<string, FeaturePermissions>;
|
|
128
|
+
/**
|
|
129
|
+
* Get the resolved permissions for a specific role across all features.
|
|
130
|
+
* Returns only UI-relevant data for the frontend.
|
|
131
|
+
*/
|
|
132
|
+
export declare function getPermissionsForRole(role: string): Record<string, {
|
|
133
|
+
nav: boolean;
|
|
134
|
+
tabs: Record<string, boolean>;
|
|
135
|
+
actions: Record<string, boolean>;
|
|
136
|
+
crud: Record<string, PermissionValue>;
|
|
137
|
+
}>;
|
|
138
|
+
/**
|
|
139
|
+
* Express middleware: require a specific permission on a resource.
|
|
140
|
+
*
|
|
141
|
+
* Checks the user's role against the registered permission config.
|
|
142
|
+
* Superadmins bypass all permission checks.
|
|
143
|
+
*
|
|
144
|
+
* When the permission value is 'own' or another scope string,
|
|
145
|
+
* the middleware allows the request but attaches `req.permissionScope`
|
|
146
|
+
* so the controller can apply the appropriate filter.
|
|
147
|
+
*
|
|
148
|
+
* @param resource - The resource name (must match a registered config)
|
|
149
|
+
* @param action - The action to check (create, read, update, delete, or custom)
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* router.post('/', requirePermission('notes', 'create'), handler);
|
|
153
|
+
* router.delete('/:id', requirePermission('notes', 'delete'), handler);
|
|
154
|
+
*
|
|
155
|
+
* // In controller, check scope:
|
|
156
|
+
* if (req.permissionScope === 'own') {
|
|
157
|
+
* query = query.eq('author_id', req.user.id);
|
|
158
|
+
* }
|
|
159
|
+
*/
|
|
160
|
+
export declare function requirePermission(resource: string, action: string): (req: AuthenticatedRequest, res: Response, next: NextFunction) => void | Response<any, Record<string, any>>;
|
|
161
|
+
/**
|
|
162
|
+
* Express middleware: require ANY of the listed permissions.
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* router.get('/feed', requireAnyPermission([
|
|
166
|
+
* { resource: 'notes', action: 'read' },
|
|
167
|
+
* { resource: 'messages', action: 'read' },
|
|
168
|
+
* ]), handler);
|
|
169
|
+
*/
|
|
170
|
+
export declare function requireAnyPermission(permissions: Array<{
|
|
171
|
+
resource: string;
|
|
172
|
+
action: string;
|
|
173
|
+
}>): (req: AuthenticatedRequest, res: Response, next: NextFunction) => void | Response<any, Record<string, any>>;
|
|
174
|
+
declare global {
|
|
175
|
+
namespace Express {
|
|
176
|
+
interface Request {
|
|
177
|
+
permissionScope?: string;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=permissionsMiddleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissionsMiddleware.d.ts","sourceRoot":"","sources":["../../src/middleware/permissionsMiddleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @tetra/core — Permissions Middleware (Config-Driven RBAC)
|
|
3
|
+
*
|
|
4
|
+
* Controls what a USER can do based on their role within a feature.
|
|
5
|
+
* This is separate from entitlements (what an ORG can do based on their plan).
|
|
6
|
+
*
|
|
7
|
+
* Projects call configurePermissions() once at startup with their feature
|
|
8
|
+
* permission configs. Each feature defines which roles can create/read/update/delete.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* configurePermissions({
|
|
13
|
+
* notes: {
|
|
14
|
+
* resource: 'notes',
|
|
15
|
+
* roles: {
|
|
16
|
+
* admin: { create: true, read: true, update: true, delete: true },
|
|
17
|
+
* coach: { create: true, read: true, update: 'own', delete: 'own' },
|
|
18
|
+
* client: { create: false, read: true, update: false, delete: false },
|
|
19
|
+
* },
|
|
20
|
+
* ui: {
|
|
21
|
+
* nav: ['admin', 'coach'],
|
|
22
|
+
* tabs: { notities: ['admin', 'coach'] },
|
|
23
|
+
* actions: { create_note: ['admin', 'coach'] },
|
|
24
|
+
* },
|
|
25
|
+
* },
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* // In routes:
|
|
29
|
+
* router.post('/', requirePermission('notes', 'create'), handler);
|
|
30
|
+
* router.delete('/:id', requirePermission('notes', 'delete'), handler);
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
import { createLogger } from '../utils/logger.js';
|
|
34
|
+
import { RFC7807ErrorResponse } from '../shared/rfc7807ErrorResponse.js';
|
|
35
|
+
const logger = createLogger('middleware:permissions');
|
|
36
|
+
// ─── State ──────────────────────────────────────────────────
|
|
37
|
+
const permissionConfigs = new Map();
|
|
38
|
+
// ─── Configure ──────────────────────────────────────────────
|
|
39
|
+
/**
|
|
40
|
+
* Register permission configs for features. Call once at app startup.
|
|
41
|
+
* Can be called multiple times to add more features.
|
|
42
|
+
*
|
|
43
|
+
* @param configs - Map of resource name → FeaturePermissions
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* configurePermissions({
|
|
47
|
+
* notes: notesPermissions,
|
|
48
|
+
* tracks: tracksPermissions,
|
|
49
|
+
* });
|
|
50
|
+
*/
|
|
51
|
+
export function configurePermissions(configs) {
|
|
52
|
+
for (const [key, config] of Object.entries(configs)) {
|
|
53
|
+
permissionConfigs.set(key, config);
|
|
54
|
+
}
|
|
55
|
+
logger.info({ features: Object.keys(configs) }, `Permissions configured for ${Object.keys(configs).length} feature(s)`);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Register a single feature's permissions.
|
|
59
|
+
*/
|
|
60
|
+
export function registerFeaturePermissions(config) {
|
|
61
|
+
permissionConfigs.set(config.resource, config);
|
|
62
|
+
}
|
|
63
|
+
// ─── Public API ─────────────────────────────────────────────
|
|
64
|
+
/**
|
|
65
|
+
* Check if a role is allowed to perform an action on a resource.
|
|
66
|
+
*
|
|
67
|
+
* Returns the PermissionValue:
|
|
68
|
+
* - true: allowed
|
|
69
|
+
* - false: denied
|
|
70
|
+
* - 'own': allowed with scope restriction
|
|
71
|
+
* - string: custom scope
|
|
72
|
+
*
|
|
73
|
+
* Unknown roles/resources/actions default to false (fail closed).
|
|
74
|
+
*/
|
|
75
|
+
export function checkPermission(resource, action, role) {
|
|
76
|
+
const config = permissionConfigs.get(resource);
|
|
77
|
+
if (!config)
|
|
78
|
+
return false;
|
|
79
|
+
const rolePerms = config.roles[role];
|
|
80
|
+
if (!rolePerms)
|
|
81
|
+
return false;
|
|
82
|
+
const value = rolePerms[action];
|
|
83
|
+
// Fallback: 'list' inherits from 'read' if not explicitly defined
|
|
84
|
+
if (value === undefined && action === 'list') {
|
|
85
|
+
return rolePerms['read'] ?? false;
|
|
86
|
+
}
|
|
87
|
+
return value ?? false;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Check if a role can see a nav item for a resource.
|
|
91
|
+
*/
|
|
92
|
+
export function canSeeNav(resource, role) {
|
|
93
|
+
const config = permissionConfigs.get(resource);
|
|
94
|
+
if (!config?.ui?.nav)
|
|
95
|
+
return true; // No nav config = visible to all
|
|
96
|
+
return config.ui.nav.includes(role);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Check if a role can see a specific tab within a resource.
|
|
100
|
+
*/
|
|
101
|
+
export function canSeeTab(resource, tabId, role) {
|
|
102
|
+
const config = permissionConfigs.get(resource);
|
|
103
|
+
if (!config?.ui?.tabs?.[tabId])
|
|
104
|
+
return true; // No tab config = visible to all
|
|
105
|
+
return config.ui.tabs[tabId].includes(role);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Check if a role can perform a specific UI action.
|
|
109
|
+
*/
|
|
110
|
+
export function canDoAction(resource, actionId, role) {
|
|
111
|
+
const config = permissionConfigs.get(resource);
|
|
112
|
+
if (!config?.ui?.actions?.[actionId])
|
|
113
|
+
return true; // No action config = visible to all
|
|
114
|
+
return config.ui.actions[actionId].includes(role);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get all permission configs. Useful for:
|
|
118
|
+
* - Frontend: GET /api/permissions → returns all UI permissions for the user's role
|
|
119
|
+
* - Documentation: generate permission matrices
|
|
120
|
+
*/
|
|
121
|
+
export function getAllPermissionConfigs() {
|
|
122
|
+
return Object.fromEntries(permissionConfigs);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get the resolved permissions for a specific role across all features.
|
|
126
|
+
* Returns only UI-relevant data for the frontend.
|
|
127
|
+
*/
|
|
128
|
+
export function getPermissionsForRole(role) {
|
|
129
|
+
const result = {};
|
|
130
|
+
for (const [resource, config] of permissionConfigs) {
|
|
131
|
+
const rolePerms = config.roles[role] ?? {};
|
|
132
|
+
result[resource] = {
|
|
133
|
+
nav: config.ui?.nav ? config.ui.nav.includes(role) : true,
|
|
134
|
+
tabs: Object.fromEntries(Object.entries(config.ui?.tabs ?? {}).map(([tabId, roles]) => [tabId, roles.includes(role)])),
|
|
135
|
+
actions: Object.fromEntries(Object.entries(config.ui?.actions ?? {}).map(([actionId, roles]) => [actionId, roles.includes(role)])),
|
|
136
|
+
crud: {
|
|
137
|
+
create: rolePerms.create ?? false,
|
|
138
|
+
read: rolePerms.read ?? false,
|
|
139
|
+
update: rolePerms.update ?? false,
|
|
140
|
+
delete: rolePerms.delete ?? false,
|
|
141
|
+
list: rolePerms.list ?? rolePerms.read ?? false,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
// ─── Middleware ──────────────────────────────────────────────
|
|
148
|
+
/**
|
|
149
|
+
* Express middleware: require a specific permission on a resource.
|
|
150
|
+
*
|
|
151
|
+
* Checks the user's role against the registered permission config.
|
|
152
|
+
* Superadmins bypass all permission checks.
|
|
153
|
+
*
|
|
154
|
+
* When the permission value is 'own' or another scope string,
|
|
155
|
+
* the middleware allows the request but attaches `req.permissionScope`
|
|
156
|
+
* so the controller can apply the appropriate filter.
|
|
157
|
+
*
|
|
158
|
+
* @param resource - The resource name (must match a registered config)
|
|
159
|
+
* @param action - The action to check (create, read, update, delete, or custom)
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* router.post('/', requirePermission('notes', 'create'), handler);
|
|
163
|
+
* router.delete('/:id', requirePermission('notes', 'delete'), handler);
|
|
164
|
+
*
|
|
165
|
+
* // In controller, check scope:
|
|
166
|
+
* if (req.permissionScope === 'own') {
|
|
167
|
+
* query = query.eq('author_id', req.user.id);
|
|
168
|
+
* }
|
|
169
|
+
*/
|
|
170
|
+
export function requirePermission(resource, action) {
|
|
171
|
+
return (req, res, next) => {
|
|
172
|
+
// Superadmins bypass all permission checks
|
|
173
|
+
if (req.user?.is_superadmin || req.user?.isSuperAdmin || req.user?.isSuperadmin) {
|
|
174
|
+
return next();
|
|
175
|
+
}
|
|
176
|
+
const role = req.user?.role;
|
|
177
|
+
if (!role) {
|
|
178
|
+
return RFC7807ErrorResponse.forbidden(res, 'No role assigned. Cannot check permissions.');
|
|
179
|
+
}
|
|
180
|
+
const permission = checkPermission(resource, action, role);
|
|
181
|
+
if (permission === false) {
|
|
182
|
+
return res.status(403).json({
|
|
183
|
+
type: 'about:blank',
|
|
184
|
+
title: 'Permission denied',
|
|
185
|
+
detail: `Role "${role}" is not allowed to "${action}" on "${resource}".`,
|
|
186
|
+
status: 403,
|
|
187
|
+
instance: req.originalUrl,
|
|
188
|
+
resource,
|
|
189
|
+
action,
|
|
190
|
+
role,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// If permission is a scope string (e.g., 'own', 'track_member'),
|
|
194
|
+
// attach it to the request so controllers can filter accordingly.
|
|
195
|
+
if (typeof permission === 'string') {
|
|
196
|
+
req.permissionScope = permission;
|
|
197
|
+
}
|
|
198
|
+
next();
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Express middleware: require ANY of the listed permissions.
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* router.get('/feed', requireAnyPermission([
|
|
206
|
+
* { resource: 'notes', action: 'read' },
|
|
207
|
+
* { resource: 'messages', action: 'read' },
|
|
208
|
+
* ]), handler);
|
|
209
|
+
*/
|
|
210
|
+
export function requireAnyPermission(permissions) {
|
|
211
|
+
return (req, res, next) => {
|
|
212
|
+
if (req.user?.is_superadmin || req.user?.isSuperAdmin || req.user?.isSuperadmin) {
|
|
213
|
+
return next();
|
|
214
|
+
}
|
|
215
|
+
const role = req.user?.role;
|
|
216
|
+
if (!role) {
|
|
217
|
+
return RFC7807ErrorResponse.forbidden(res, 'No role assigned.');
|
|
218
|
+
}
|
|
219
|
+
const hasAny = permissions.some(({ resource, action }) => {
|
|
220
|
+
const perm = checkPermission(resource, action, role);
|
|
221
|
+
return perm !== false;
|
|
222
|
+
});
|
|
223
|
+
if (!hasAny) {
|
|
224
|
+
return res.status(403).json({
|
|
225
|
+
type: 'about:blank',
|
|
226
|
+
title: 'Permission denied',
|
|
227
|
+
detail: `Role "${role}" lacks all required permissions.`,
|
|
228
|
+
status: 403,
|
|
229
|
+
instance: req.originalUrl,
|
|
230
|
+
required: permissions,
|
|
231
|
+
role,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
next();
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=permissionsMiddleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissionsMiddleware.js","sourceRoot":"","sources":["../../src/middleware/permissionsMiddleware.ts"],"names":[],"mappings":"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"}
|