@heavybit/pendoadmin-shared-lib 1.0.0 → 1.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.
Files changed (61) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +4 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/rate-limit/create-rate-limiter.d.ts +23 -0
  6. package/dist/rate-limit/create-rate-limiter.d.ts.map +1 -0
  7. package/dist/rate-limit/create-rate-limiter.js +60 -0
  8. package/dist/rate-limit/create-rate-limiter.js.map +1 -0
  9. package/dist/rate-limit/index.d.ts +3 -0
  10. package/dist/rate-limit/index.d.ts.map +1 -0
  11. package/dist/rate-limit/index.js +2 -0
  12. package/dist/rate-limit/index.js.map +1 -0
  13. package/dist/scheduling/event-driven-scheduler.d.ts +33 -0
  14. package/dist/scheduling/event-driven-scheduler.d.ts.map +1 -0
  15. package/dist/scheduling/event-driven-scheduler.js +67 -0
  16. package/dist/scheduling/event-driven-scheduler.js.map +1 -0
  17. package/dist/scheduling/index.d.ts +3 -0
  18. package/dist/scheduling/index.d.ts.map +1 -0
  19. package/dist/scheduling/index.js +2 -0
  20. package/dist/scheduling/index.js.map +1 -0
  21. package/dist/socket/create-socket-server.d.ts +30 -0
  22. package/dist/socket/create-socket-server.d.ts.map +1 -0
  23. package/dist/socket/create-socket-server.js +59 -0
  24. package/dist/socket/create-socket-server.js.map +1 -0
  25. package/dist/socket/index.d.ts +5 -0
  26. package/dist/socket/index.d.ts.map +1 -0
  27. package/dist/socket/index.js +3 -0
  28. package/dist/socket/index.js.map +1 -0
  29. package/dist/socket/nestjs-socket.module.d.ts +20 -0
  30. package/dist/socket/nestjs-socket.module.d.ts.map +1 -0
  31. package/dist/socket/nestjs-socket.module.js +64 -0
  32. package/dist/socket/nestjs-socket.module.js.map +1 -0
  33. package/dist/tenant/drizzle-tenant.d.ts +25 -0
  34. package/dist/tenant/drizzle-tenant.d.ts.map +1 -0
  35. package/dist/tenant/drizzle-tenant.js +35 -0
  36. package/dist/tenant/drizzle-tenant.js.map +1 -0
  37. package/dist/tenant/index.d.ts +7 -0
  38. package/dist/tenant/index.d.ts.map +1 -0
  39. package/dist/tenant/index.js +7 -0
  40. package/dist/tenant/index.js.map +1 -0
  41. package/dist/tenant/knex-tenant.d.ts +13 -0
  42. package/dist/tenant/knex-tenant.d.ts.map +1 -0
  43. package/dist/tenant/knex-tenant.js +43 -0
  44. package/dist/tenant/knex-tenant.js.map +1 -0
  45. package/dist/tenant/tenant-access-control.d.ts +93 -0
  46. package/dist/tenant/tenant-access-control.d.ts.map +1 -0
  47. package/dist/tenant/tenant-access-control.js +260 -0
  48. package/dist/tenant/tenant-access-control.js.map +1 -0
  49. package/package.json +33 -4
  50. package/src/index.ts +6 -0
  51. package/src/rate-limit/create-rate-limiter.ts +107 -0
  52. package/src/rate-limit/index.ts +2 -0
  53. package/src/scheduling/event-driven-scheduler.ts +87 -0
  54. package/src/scheduling/index.ts +2 -0
  55. package/src/socket/create-socket-server.ts +112 -0
  56. package/src/socket/index.ts +14 -0
  57. package/src/socket/nestjs-socket.module.ts +77 -0
  58. package/src/tenant/drizzle-tenant.ts +51 -0
  59. package/src/tenant/index.ts +24 -0
  60. package/src/tenant/knex-tenant.ts +59 -0
  61. package/src/tenant/tenant-access-control.ts +370 -0
@@ -0,0 +1,43 @@
1
+ // pendoadmin-shared-lib/src/tenant/knex-tenant.ts
2
+ //
3
+ // Proxy-based Knex wrapper that automatically applies
4
+ // WHERE company_id = ? to every query on the returned builder.
5
+ /**
6
+ * Returns a Proxy around `knex` that intercepts table calls and
7
+ * injects `.where(tenantColumn, companyId)`.
8
+ */
9
+ export function scopedKnex(knex, companyId, options = {}) {
10
+ const { tenantColumn = "company_id", excludeTables = [] } = options;
11
+ const excluded = new Set(excludeTables.map((t) => t.toLowerCase()));
12
+ return new Proxy(knex, {
13
+ apply(_target, _thisArg, args) {
14
+ const tableName = args[0];
15
+ const qb = knex(tableName);
16
+ if (typeof tableName === "string" && !excluded.has(tableName.toLowerCase())) {
17
+ return qb.where(`${tableName}.${tenantColumn}`, companyId);
18
+ }
19
+ return qb;
20
+ },
21
+ get(target, prop, receiver) {
22
+ if (prop === "raw" || prop === "schema" || prop === "destroy" || prop === "transaction") {
23
+ return Reflect.get(target, prop, receiver);
24
+ }
25
+ const value = Reflect.get(target, prop, receiver);
26
+ if (typeof value === "function" && typeof prop === "string") {
27
+ return (...args) => {
28
+ const result = value.apply(target, args);
29
+ if (result &&
30
+ typeof result === "object" &&
31
+ "where" in result &&
32
+ typeof args[0] === "string" &&
33
+ !excluded.has(args[0].toLowerCase())) {
34
+ return result.where(`${args[0]}.${tenantColumn}`, companyId);
35
+ }
36
+ return result;
37
+ };
38
+ }
39
+ return value;
40
+ },
41
+ });
42
+ }
43
+ //# sourceMappingURL=knex-tenant.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"knex-tenant.js","sourceRoot":"","sources":["../../src/tenant/knex-tenant.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,EAAE;AACF,sDAAsD;AACtD,iEAAiE;AAWjE;;;GAGG;AACH,MAAM,UAAU,UAAU,CACxB,IAAU,EACV,SAA0B,EAC1B,UAA6B,EAAE;IAE/B,MAAM,EAAE,YAAY,GAAG,YAAY,EAAE,aAAa,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC;IACpE,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAEpE,OAAO,IAAI,KAAK,CAAC,IAAI,EAAE;QACrB,KAAK,CAAC,OAAO,EAAE,QAAQ,EAAE,IAA4B;YACnD,MAAM,SAAS,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;YAC1B,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC;YAC3B,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;gBAC5E,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,SAAS,IAAI,YAAY,EAAE,EAAE,SAAS,CAAC,CAAC;YAC7D,CAAC;YACD,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ;YACxB,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,aAAa,EAAE,CAAC;gBACxF,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;YAC7C,CAAC;YACD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;YAClD,IAAI,OAAO,KAAK,KAAK,UAAU,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC5D,OAAO,CAAC,GAAG,IAAe,EAAE,EAAE;oBAC5B,MAAM,MAAM,GAAI,KAAkB,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;oBACvD,IACE,MAAM;wBACN,OAAO,MAAM,KAAK,QAAQ;wBAC1B,OAAO,IAAI,MAAM;wBACjB,OAAO,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ;wBAC3B,CAAC,QAAQ,CAAC,GAAG,CAAE,IAAI,CAAC,CAAC,CAAY,CAAC,WAAW,EAAE,CAAC,EAChD,CAAC;wBACD,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,YAAY,EAAE,EAAE,SAAS,CAAC,CAAC;oBAC/D,CAAC;oBACD,OAAO,MAAM,CAAC;gBAChB,CAAC,CAAC;YACJ,CAAC;YACD,OAAO,KAAK,CAAC;QACf,CAAC;KACF,CAAoB,CAAC;AACxB,CAAC"}
@@ -0,0 +1,93 @@
1
+ import type { Request, Response, NextFunction } from "express";
2
+ import type { Knex } from "knex";
3
+ /**
4
+ * Every discrete feature / service area that can be gated.
5
+ * Extend this union as the platform grows.
6
+ */
7
+ export type Feature = "sms" | "email" | "whatsapp" | "ussd" | "mpesa" | "analytics" | "ai" | "notifications" | "api-gateway" | (string & {});
8
+ /**
9
+ * Named subscription plans. Each plan grants access to a set of features.
10
+ * Plans are ordered by tier — higher tiers include all features of lower tiers.
11
+ */
12
+ export interface SubscriptionPlan {
13
+ name: string;
14
+ /** Numeric tier level — higher = more features */
15
+ tier: number;
16
+ /** Features included in this plan */
17
+ features: Feature[];
18
+ }
19
+ /** Default plans — consumers can override with their own */
20
+ export declare const DEFAULT_PLANS: SubscriptionPlan[];
21
+ export interface TenantAccessControlConfig {
22
+ /**
23
+ * Function that looks up a company's active features.
24
+ * Should return the list of feature names the company is entitled to.
25
+ * This is called once per request and cached on the request object.
26
+ */
27
+ getCompanyFeatures: (companyId: string) => Promise<Feature[]>;
28
+ /**
29
+ * Optional: check whether the tenant (company) itself is active.
30
+ * If it returns false the request is rejected with 403.
31
+ * Default: always active (not checked).
32
+ */
33
+ isCompanyActive?: (companyId: string) => Promise<boolean>;
34
+ /** Cache TTL in seconds for feature lookups. 0 = no cache. Default: 60. */
35
+ cacheTtlSeconds?: number;
36
+ /** Custom logger (default: console) */
37
+ logger?: Pick<Console, "info" | "warn" | "error">;
38
+ }
39
+ /** Clear the entire feature cache (useful for tests or after plan changes) */
40
+ export declare function clearFeatureCache(): void;
41
+ /** Invalidate cache for a specific company (call after plan upgrade/downgrade) */
42
+ export declare function invalidateCompanyFeatureCache(companyId: string): void;
43
+ /**
44
+ * Creates an Express middleware that checks whether the requesting tenant
45
+ * is active and has access to the specified feature(s).
46
+ *
47
+ * Usage:
48
+ * ```ts
49
+ * const ctrl = createTenantAccessControl({ getCompanyFeatures, isCompanyActive });
50
+ *
51
+ * // Gate an entire router
52
+ * router.use(ctrl.requireFeature("whatsapp"));
53
+ *
54
+ * // Gate a specific route with multiple required features
55
+ * router.post("/campaigns", ctrl.requireAllFeatures(["sms", "analytics"]), handler);
56
+ *
57
+ * // Gate where ANY one feature is sufficient
58
+ * router.get("/inbox", ctrl.requireAnyFeature(["sms", "whatsapp", "email"]), handler);
59
+ * ```
60
+ */
61
+ export declare function createTenantAccessControl(config: TenantAccessControlConfig): {
62
+ requireFeature: (feature: Feature) => (req: Request, res: Response, next: NextFunction) => Promise<Response<any, Record<string, any>> | undefined>;
63
+ requireAllFeatures: (features: Feature[]) => (req: Request, res: Response, next: NextFunction) => Promise<Response<any, Record<string, any>> | undefined>;
64
+ requireAnyFeature: (features: Feature[]) => (req: Request, res: Response, next: NextFunction) => Promise<Response<any, Record<string, any>> | undefined>;
65
+ requireActiveTenant: () => (req: Request, res: Response, next: NextFunction) => Promise<Response<any, Record<string, any>> | undefined>;
66
+ resolveFeatures: (companyId: string) => Promise<Feature[]>;
67
+ clearFeatureCache: typeof clearFeatureCache;
68
+ invalidateCompanyFeatureCache: typeof invalidateCompanyFeatureCache;
69
+ };
70
+ /**
71
+ * Default implementation of `getCompanyFeatures` using Knex
72
+ * against the `company_service_configs` table.
73
+ *
74
+ * Returns the `service_name` of every row where `is_active = true`
75
+ * for the given company.
76
+ */
77
+ export declare function knexFeatureResolver(knex: Knex): (companyId: string) => Promise<Feature[]>;
78
+ /**
79
+ * Default implementation of `isCompanyActive` using Knex
80
+ * against the `companies` table.
81
+ */
82
+ export declare function knexCompanyActiveChecker(knex: Knex): (companyId: string) => Promise<boolean>;
83
+ /**
84
+ * Given a plan name, return the set of features it grants.
85
+ * Useful for provisioning a new company.
86
+ */
87
+ export declare function getFeaturesForPlan(planName: string, plans?: SubscriptionPlan[]): Feature[];
88
+ /**
89
+ * Provision a company's service configs based on a plan.
90
+ * Inserts rows into `company_service_configs` for each feature in the plan.
91
+ */
92
+ export declare function provisionCompanyPlan(knex: Knex, companyId: string, planName: string, plans?: SubscriptionPlan[]): Promise<void>;
93
+ //# sourceMappingURL=tenant-access-control.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-access-control.d.ts","sourceRoot":"","sources":["../../src/tenant/tenant-access-control.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAIjC;;;GAGG;AACH,MAAM,MAAM,OAAO,GACf,KAAK,GACL,OAAO,GACP,UAAU,GACV,MAAM,GACN,OAAO,GACP,WAAW,GACX,IAAI,GACJ,eAAe,GACf,aAAa,GACb,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,4DAA4D;AAC5D,eAAO,MAAM,aAAa,EAAE,gBAAgB,EAqB3C,CAAC;AAIF,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,kBAAkB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IAE9D;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAE1D,2EAA2E;IAC3E,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,uCAAuC;IACvC,MAAM,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;CACnD;AAwBD,8EAA8E;AAC9E,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED,kFAAkF;AAClF,wBAAgB,6BAA6B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAErE;AAID;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,yBAAyB;8BAqCxC,OAAO,MACxB,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY;mCAwBzB,OAAO,EAAE,MAC/B,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY;kCAyB1B,OAAO,EAAE,MAC9B,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY;gCAuB/C,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY;iCAtGrB,MAAM,KAAG,OAAO,CAAC,OAAO,EAAE,CAAC;;;EAyHtE;AAID;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,IAC9B,WAAW,MAAM,KAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAMrD;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,IAAI,IACnC,WAAW,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC,CAOnD;AAID;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,GAAE,gBAAgB,EAAkB,GACxC,OAAO,EAAE,CAGX;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,IAAI,EACV,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,KAAK,GAAE,gBAAgB,EAAkB,GACxC,OAAO,CAAC,IAAI,CAAC,CA+Bf"}
@@ -0,0 +1,260 @@
1
+ // pendoadmin-shared-lib/src/tenant/tenant-access-control.ts
2
+ //
3
+ // Feature-gating middleware and helpers.
4
+ // Restricts tenant access to services/features based on their subscription plan
5
+ // and per-service activation status (ICompanyServiceConfig).
6
+ /** Default plans — consumers can override with their own */
7
+ export const DEFAULT_PLANS = [
8
+ {
9
+ name: "starter",
10
+ tier: 1,
11
+ features: ["sms", "notifications"],
12
+ },
13
+ {
14
+ name: "professional",
15
+ tier: 2,
16
+ features: ["sms", "email", "notifications", "analytics"],
17
+ },
18
+ {
19
+ name: "business",
20
+ tier: 3,
21
+ features: ["sms", "email", "whatsapp", "mpesa", "notifications", "analytics"],
22
+ },
23
+ {
24
+ name: "enterprise",
25
+ tier: 4,
26
+ features: ["sms", "email", "whatsapp", "ussd", "mpesa", "analytics", "ai", "notifications", "api-gateway"],
27
+ },
28
+ ];
29
+ const featureCache = new Map();
30
+ function getCachedFeatures(companyId, ttl) {
31
+ if (ttl <= 0)
32
+ return null;
33
+ const entry = featureCache.get(companyId);
34
+ if (entry && Date.now() < entry.expiresAt)
35
+ return entry.features;
36
+ featureCache.delete(companyId);
37
+ return null;
38
+ }
39
+ function setCachedFeatures(companyId, features, ttl) {
40
+ if (ttl <= 0)
41
+ return;
42
+ featureCache.set(companyId, { features, expiresAt: Date.now() + ttl * 1000 });
43
+ }
44
+ /** Clear the entire feature cache (useful for tests or after plan changes) */
45
+ export function clearFeatureCache() {
46
+ featureCache.clear();
47
+ }
48
+ /** Invalidate cache for a specific company (call after plan upgrade/downgrade) */
49
+ export function invalidateCompanyFeatureCache(companyId) {
50
+ featureCache.delete(companyId);
51
+ }
52
+ // ─── Middleware Factory ─────────────────────────────────
53
+ /**
54
+ * Creates an Express middleware that checks whether the requesting tenant
55
+ * is active and has access to the specified feature(s).
56
+ *
57
+ * Usage:
58
+ * ```ts
59
+ * const ctrl = createTenantAccessControl({ getCompanyFeatures, isCompanyActive });
60
+ *
61
+ * // Gate an entire router
62
+ * router.use(ctrl.requireFeature("whatsapp"));
63
+ *
64
+ * // Gate a specific route with multiple required features
65
+ * router.post("/campaigns", ctrl.requireAllFeatures(["sms", "analytics"]), handler);
66
+ *
67
+ * // Gate where ANY one feature is sufficient
68
+ * router.get("/inbox", ctrl.requireAnyFeature(["sms", "whatsapp", "email"]), handler);
69
+ * ```
70
+ */
71
+ export function createTenantAccessControl(config) {
72
+ const { getCompanyFeatures, isCompanyActive, cacheTtlSeconds = 60, logger = console, } = config;
73
+ // ─── Resolve features (with cache) ─────────────────
74
+ async function resolveFeatures(companyId) {
75
+ const cached = getCachedFeatures(companyId, cacheTtlSeconds);
76
+ if (cached)
77
+ return cached;
78
+ const features = await getCompanyFeatures(companyId);
79
+ setCachedFeatures(companyId, features, cacheTtlSeconds);
80
+ return features;
81
+ }
82
+ // ─── Tenant active check ───────────────────────────
83
+ async function checkTenantActive(companyId, res) {
84
+ if (!isCompanyActive)
85
+ return true;
86
+ const active = await isCompanyActive(companyId);
87
+ if (!active) {
88
+ logger.warn(`[TenantAccess] Company ${companyId} is disabled — blocking request`);
89
+ res.status(403).json({
90
+ success: false,
91
+ message: "Your account has been disabled. Please contact support.",
92
+ });
93
+ return false;
94
+ }
95
+ return true;
96
+ }
97
+ // ─── Middleware: require a single feature ──────────
98
+ function requireFeature(feature) {
99
+ return async (req, res, next) => {
100
+ const companyId = extractCompanyId(req);
101
+ if (!companyId) {
102
+ return res.status(401).json({ success: false, message: "Company context required." });
103
+ }
104
+ if (!(await checkTenantActive(companyId, res)))
105
+ return;
106
+ const features = await resolveFeatures(companyId);
107
+ if (!features.includes(feature)) {
108
+ logger.info(`[TenantAccess] Company ${companyId} denied access to "${feature}" — not in plan`);
109
+ return res.status(403).json({
110
+ success: false,
111
+ message: `Your subscription does not include access to the "${feature}" service. Please upgrade your plan.`,
112
+ });
113
+ }
114
+ next();
115
+ };
116
+ }
117
+ // ─── Middleware: require ALL listed features ───────
118
+ function requireAllFeatures(features) {
119
+ return async (req, res, next) => {
120
+ const companyId = extractCompanyId(req);
121
+ if (!companyId) {
122
+ return res.status(401).json({ success: false, message: "Company context required." });
123
+ }
124
+ if (!(await checkTenantActive(companyId, res)))
125
+ return;
126
+ const allowed = await resolveFeatures(companyId);
127
+ const missing = features.filter((f) => !allowed.includes(f));
128
+ if (missing.length > 0) {
129
+ logger.info(`[TenantAccess] Company ${companyId} missing features: ${missing.join(", ")}`);
130
+ return res.status(403).json({
131
+ success: false,
132
+ message: `Your subscription is missing access to: ${missing.join(", ")}. Please upgrade your plan.`,
133
+ });
134
+ }
135
+ next();
136
+ };
137
+ }
138
+ // ─── Middleware: require ANY one of listed features ─
139
+ function requireAnyFeature(features) {
140
+ return async (req, res, next) => {
141
+ const companyId = extractCompanyId(req);
142
+ if (!companyId) {
143
+ return res.status(401).json({ success: false, message: "Company context required." });
144
+ }
145
+ if (!(await checkTenantActive(companyId, res)))
146
+ return;
147
+ const allowed = await resolveFeatures(companyId);
148
+ const hasAny = features.some((f) => allowed.includes(f));
149
+ if (!hasAny) {
150
+ return res.status(403).json({
151
+ success: false,
152
+ message: `Your subscription does not include any of: ${features.join(", ")}. Please upgrade your plan.`,
153
+ });
154
+ }
155
+ next();
156
+ };
157
+ }
158
+ // ─── Middleware: just check tenant is active ───────
159
+ function requireActiveTenant() {
160
+ return async (req, res, next) => {
161
+ const companyId = extractCompanyId(req);
162
+ if (!companyId) {
163
+ return res.status(401).json({ success: false, message: "Company context required." });
164
+ }
165
+ if (!(await checkTenantActive(companyId, res)))
166
+ return;
167
+ next();
168
+ };
169
+ }
170
+ return {
171
+ requireFeature,
172
+ requireAllFeatures,
173
+ requireAnyFeature,
174
+ requireActiveTenant,
175
+ resolveFeatures,
176
+ clearFeatureCache,
177
+ invalidateCompanyFeatureCache,
178
+ };
179
+ }
180
+ // ─── Knex-backed Feature Resolver ───────────────────────
181
+ /**
182
+ * Default implementation of `getCompanyFeatures` using Knex
183
+ * against the `company_service_configs` table.
184
+ *
185
+ * Returns the `service_name` of every row where `is_active = true`
186
+ * for the given company.
187
+ */
188
+ export function knexFeatureResolver(knex) {
189
+ return async (companyId) => {
190
+ const rows = await knex("company_service_configs")
191
+ .select("service_name")
192
+ .where({ company_id: companyId, is_active: true });
193
+ return rows.map((r) => r.service_name);
194
+ };
195
+ }
196
+ /**
197
+ * Default implementation of `isCompanyActive` using Knex
198
+ * against the `companies` table.
199
+ */
200
+ export function knexCompanyActiveChecker(knex) {
201
+ return async (companyId) => {
202
+ const row = await knex("companies")
203
+ .select("is_active")
204
+ .where({ id: companyId })
205
+ .first();
206
+ return row?.is_active === true || row?.is_active === 1;
207
+ };
208
+ }
209
+ // ─── Plan Helpers ───────────────────────────────────────
210
+ /**
211
+ * Given a plan name, return the set of features it grants.
212
+ * Useful for provisioning a new company.
213
+ */
214
+ export function getFeaturesForPlan(planName, plans = DEFAULT_PLANS) {
215
+ const plan = plans.find((p) => p.name === planName);
216
+ return plan ? [...plan.features] : [];
217
+ }
218
+ /**
219
+ * Provision a company's service configs based on a plan.
220
+ * Inserts rows into `company_service_configs` for each feature in the plan.
221
+ */
222
+ export async function provisionCompanyPlan(knex, companyId, planName, plans = DEFAULT_PLANS) {
223
+ const features = getFeaturesForPlan(planName, plans);
224
+ if (features.length === 0)
225
+ return;
226
+ // Deactivate all existing configs
227
+ await knex("company_service_configs")
228
+ .where({ company_id: companyId })
229
+ .update({ is_active: false });
230
+ // Upsert new configs
231
+ for (const feature of features) {
232
+ const existing = await knex("company_service_configs")
233
+ .where({ company_id: companyId, service_name: feature })
234
+ .first();
235
+ if (existing) {
236
+ await knex("company_service_configs")
237
+ .where({ id: existing.id })
238
+ .update({ is_active: true, updated_at: knex.fn.now() });
239
+ }
240
+ else {
241
+ await knex("company_service_configs").insert({
242
+ id: knex.raw("UUID()"),
243
+ company_id: companyId,
244
+ service_name: feature,
245
+ config: JSON.stringify({}),
246
+ is_active: true,
247
+ created_at: knex.fn.now(),
248
+ updated_at: knex.fn.now(),
249
+ });
250
+ }
251
+ }
252
+ }
253
+ // ─── Helpers ────────────────────────────────────────────
254
+ function extractCompanyId(req) {
255
+ // Gateway sets x-company-id header; fallback to req.user.company_id / companyId
256
+ return (req.headers["x-company-id"] ||
257
+ req.user?.company_id ||
258
+ req.user?.companyId);
259
+ }
260
+ //# sourceMappingURL=tenant-access-control.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-access-control.js","sourceRoot":"","sources":["../../src/tenant/tenant-access-control.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heavybit/pendoadmin-shared-lib",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Shared library for PendoAdmin microservices",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -40,7 +40,11 @@
40
40
  "./validation": "./dist/validation/index.js",
41
41
  "./logging": "./dist/logging/index.js",
42
42
  "./config": "./dist/config/index.js",
43
- "./types": "./dist/types/index.js"
43
+ "./types": "./dist/types/index.js",
44
+ "./rate-limit": "./dist/rate-limit/index.js",
45
+ "./socket": "./dist/socket/index.js",
46
+ "./tenant": "./dist/tenant/index.js",
47
+ "./scheduling": "./dist/scheduling/index.js"
44
48
  },
45
49
  "typesVersions": {
46
50
  "*": {
@@ -52,7 +56,11 @@
52
56
  "validation": ["dist/validation/index.d.ts"],
53
57
  "logging": ["dist/logging/index.d.ts"],
54
58
  "config": ["dist/config/index.d.ts"],
55
- "types": ["dist/types/index.d.ts"]
59
+ "types": ["dist/types/index.d.ts"],
60
+ "rate-limit": ["dist/rate-limit/index.d.ts"],
61
+ "socket": ["dist/socket/index.d.ts"],
62
+ "tenant": ["dist/tenant/index.d.ts"],
63
+ "scheduling": ["dist/scheduling/index.d.ts"]
56
64
  }
57
65
  },
58
66
  "dependencies": {
@@ -64,6 +72,7 @@
64
72
  "mysql2": "^3.11.0",
65
73
  "uuid": "^10.0.0",
66
74
  "winston": "^3.14.0",
75
+ "rate-limiter-flexible": "^5.0.0",
67
76
  "zod": "^3.23.0"
68
77
  },
69
78
  "devDependencies": {
@@ -76,15 +85,23 @@
76
85
  "@types/jsonwebtoken": "^9.0.7",
77
86
  "@types/node": "^20.14.0",
78
87
  "@types/uuid": "^10.0.0",
88
+ "@nestjs/websockets": "^11.0.0",
89
+ "drizzle-orm": "^0.30.0",
79
90
  "express": "^5.2.1",
91
+ "ioredis": "^5.4.0",
80
92
  "reflect-metadata": "^0.2.2",
81
93
  "rxjs": "^7.8.2",
94
+ "socket.io": "^4.7.0",
82
95
  "typescript": "^5.5.0"
83
96
  },
84
97
  "peerDependencies": {
85
98
  "@nestjs/common": ">=10.0.0",
86
99
  "@nestjs/core": ">=10.0.0",
87
- "express": ">=4.0.0"
100
+ "@nestjs/websockets": ">=10.0.0",
101
+ "drizzle-orm": ">=0.30.0",
102
+ "express": ">=4.0.0",
103
+ "ioredis": ">=5.0.0",
104
+ "socket.io": ">=4.0.0"
88
105
  },
89
106
  "peerDependenciesMeta": {
90
107
  "@nestjs/common": {
@@ -93,8 +110,20 @@
93
110
  "@nestjs/core": {
94
111
  "optional": true
95
112
  },
113
+ "@nestjs/websockets": {
114
+ "optional": true
115
+ },
116
+ "drizzle-orm": {
117
+ "optional": true
118
+ },
96
119
  "express": {
97
120
  "optional": true
121
+ },
122
+ "ioredis": {
123
+ "optional": true
124
+ },
125
+ "socket.io": {
126
+ "optional": true
98
127
  }
99
128
  }
100
129
  }
package/src/index.ts CHANGED
@@ -21,3 +21,9 @@ export * from './database/index.js';
21
21
 
22
22
  // Express middleware (for Express-based services)
23
23
  export * from './express/index.js';
24
+
25
+ // Rate limiting
26
+ export * from './rate-limit/index.js';
27
+
28
+ // Scheduling
29
+ export * from './scheduling/index.js';
@@ -0,0 +1,107 @@
1
+ // pendoadmin-shared-lib/src/rate-limit/create-rate-limiter.ts
2
+ //
3
+ // Factory that creates a dual-layer (Redis + memory fallback) rate limiter.
4
+ // Available for any Express-based service.
5
+
6
+ import { Request, Response, NextFunction } from "express";
7
+ import { RateLimiterRedis, RateLimiterMemory, RateLimiterAbstract } from "rate-limiter-flexible";
8
+ import type Redis from "ioredis";
9
+
10
+ // ─── Types ──────────────────────────────────────────────
11
+
12
+ export interface RateLimitConfig {
13
+ /** Max requests per window (default: 100) */
14
+ maxRequests?: number;
15
+ /** Window duration in milliseconds (default: 60 000) */
16
+ windowMs?: number;
17
+ /** Redis key prefix — use unique per service to avoid collisions (default: "rl:service") */
18
+ keyPrefix?: string;
19
+ /** Optional Redis client — falls back to in-memory if omitted */
20
+ redisClient?: Redis | null;
21
+ /** Extract the rate-limit key from the request. Default: X-User-Id header → IP → "unknown" */
22
+ keyExtractor?: (req: Request) => string;
23
+ }
24
+
25
+ export interface RateLimitResult {
26
+ /** The rate-limiter instance (for advanced use) */
27
+ limiter: RateLimiterAbstract;
28
+ /** Express middleware */
29
+ middleware: (req: Request, res: Response, next: NextFunction) => Promise<void>;
30
+ }
31
+
32
+ // ─── Factory ────────────────────────────────────────────
33
+
34
+ export function createRateLimiter(cfg: RateLimitConfig = {}): RateLimitResult {
35
+ const {
36
+ maxRequests = 100,
37
+ windowMs = 60_000,
38
+ keyPrefix = "rl:service",
39
+ redisClient = null,
40
+ keyExtractor = defaultKeyExtractor,
41
+ } = cfg;
42
+
43
+ const durationSecs = Math.max(1, Math.round(windowMs / 1000));
44
+
45
+ const memoryLimiter = new RateLimiterMemory({
46
+ keyPrefix,
47
+ points: maxRequests,
48
+ duration: durationSecs,
49
+ });
50
+
51
+ let limiter: RateLimiterAbstract;
52
+
53
+ if (redisClient) {
54
+ limiter = new RateLimiterRedis({
55
+ storeClient: redisClient,
56
+ keyPrefix,
57
+ points: maxRequests,
58
+ duration: durationSecs,
59
+ blockDuration: 0,
60
+ insuranceLimiter: memoryLimiter,
61
+ });
62
+ } else {
63
+ limiter = memoryLimiter;
64
+ }
65
+
66
+ // ─── Middleware ──────────────────────────────────────
67
+
68
+ async function middleware(
69
+ req: Request,
70
+ res: Response,
71
+ next: NextFunction,
72
+ ): Promise<void> {
73
+ try {
74
+ const key = keyExtractor(req);
75
+ const result = await limiter.consume(key);
76
+
77
+ res.setHeader("X-RateLimit-Limit", maxRequests);
78
+ res.setHeader("X-RateLimit-Remaining", result.remainingPoints);
79
+ res.setHeader(
80
+ "X-RateLimit-Reset",
81
+ new Date(Date.now() + result.msBeforeNext).toISOString(),
82
+ );
83
+ next();
84
+ } catch (rejRes: any) {
85
+ // Non-rate-limit errors should not block traffic
86
+ if (typeof rejRes?.msBeforeNext !== "number") {
87
+ console.error(`[RateLimit:${keyPrefix}] Unexpected error:`, rejRes?.message || rejRes);
88
+ return next();
89
+ }
90
+ res.setHeader("X-RateLimit-Limit", maxRequests);
91
+ res.setHeader("X-RateLimit-Remaining", 0);
92
+ res.setHeader("Retry-After", Math.ceil(rejRes.msBeforeNext / 1000));
93
+ res.status(429).json({
94
+ success: false,
95
+ message: "Too many requests. Please try again later.",
96
+ });
97
+ }
98
+ }
99
+
100
+ return { limiter, middleware };
101
+ }
102
+
103
+ // ─── Defaults ───────────────────────────────────────────
104
+
105
+ function defaultKeyExtractor(req: Request): string {
106
+ return (req.headers["x-user-id"] as string) || req.ip || "unknown";
107
+ }
@@ -0,0 +1,2 @@
1
+ export { createRateLimiter } from "./create-rate-limiter.js";
2
+ export type { RateLimitConfig, RateLimitResult } from "./create-rate-limiter.js";