@discover-cloud/shared 1.2.9 → 1.4.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 (183) hide show
  1. package/dist/authorization/index.d.ts +2 -2
  2. package/dist/authorization/index.js +1 -1
  3. package/dist/authorization/permission-cache.service.d.ts +104 -16
  4. package/dist/authorization/permission-cache.service.js +177 -148
  5. package/dist/authorization/permission-checker.d.ts +30 -0
  6. package/dist/authorization/permission-checker.js +40 -0
  7. package/dist/context/index.d.ts +1 -1
  8. package/dist/context/index.js +1 -1
  9. package/dist/context/request-context.storage.d.ts +21 -0
  10. package/dist/context/request-context.storage.js +37 -0
  11. package/dist/contracts/auth-service/account.dto.d.ts +30 -0
  12. package/dist/contracts/auth-service/account.dto.js +7 -0
  13. package/dist/contracts/auth-service/auth.dto.d.ts +21 -0
  14. package/dist/contracts/auth-service/auth.dto.js +10 -0
  15. package/dist/contracts/index.d.ts +1 -0
  16. package/dist/{security → contracts}/index.js +1 -1
  17. package/dist/contracts/visibility.d.ts +63 -0
  18. package/dist/contracts/visibility.js +10 -0
  19. package/dist/dtos/audit-service.types.d.ts +42 -0
  20. package/dist/dtos/audit-service.types.js +31 -0
  21. package/dist/dtos/auth-service.dto.d.ts +87 -26
  22. package/dist/dtos/auth-service.dto.js +4 -0
  23. package/dist/dtos/cloud-service.dto.d.ts +57 -27
  24. package/dist/dtos/cloud-service.dto.js +5 -0
  25. package/dist/dtos/index.d.ts +6 -5
  26. package/dist/dtos/index.js +1 -0
  27. package/dist/dtos/insights-service.dto.d.ts +184 -32
  28. package/dist/dtos/insights-service.dto.js +8 -0
  29. package/dist/dtos/response.dto.d.ts +12 -83
  30. package/dist/dtos/response.dto.js +2 -21
  31. package/dist/dtos/user-service.dto.d.ts +163 -13
  32. package/dist/enums/domain.enums.d.ts +272 -75
  33. package/dist/enums/domain.enums.js +329 -102
  34. package/dist/enums/index.d.ts +2 -2
  35. package/dist/enums/permissions.enums.d.ts +142 -80
  36. package/dist/enums/permissions.enums.js +174 -133
  37. package/dist/errors/app-error.d.ts +13 -18
  38. package/dist/errors/app-error.js +15 -21
  39. package/dist/errors/http-errors.d.ts +106 -18
  40. package/dist/errors/http-errors.js +136 -51
  41. package/dist/errors/index.d.ts +2 -2
  42. package/dist/http/index.d.ts +2 -1
  43. package/dist/http/index.js +1 -0
  44. package/dist/http/request-context.d.ts +8 -0
  45. package/dist/http/request-context.js +37 -0
  46. package/dist/http/service-client.d.ts +93 -40
  47. package/dist/http/service-client.js +104 -55
  48. package/dist/index.d.ts +11 -9
  49. package/dist/index.js +2 -0
  50. package/dist/jwt/index.d.ts +3 -2
  51. package/dist/jwt/index.js +2 -1
  52. package/dist/jwt/machine-jwt-verifier.d.ts +21 -0
  53. package/dist/jwt/machine-jwt-verifier.js +83 -0
  54. package/dist/jwt/machine-token-client.d.ts +36 -5
  55. package/dist/jwt/machine-token-client.js +61 -27
  56. package/dist/jwt/user-jwt-verifier.d.ts +31 -0
  57. package/dist/jwt/user-jwt-verifier.js +102 -0
  58. package/dist/messaging/audit.publisher.d.ts +14 -0
  59. package/dist/messaging/audit.publisher.js +49 -0
  60. package/dist/messaging/index.d.ts +2 -0
  61. package/dist/{dto → messaging}/index.js +2 -3
  62. package/dist/messaging/rabbitmq.client.d.ts +15 -0
  63. package/dist/messaging/rabbitmq.client.js +133 -0
  64. package/dist/middleware/error-handler.middleware.d.ts +8 -28
  65. package/dist/middleware/error-handler.middleware.js +63 -54
  66. package/dist/middleware/index.d.ts +10 -8
  67. package/dist/middleware/index.js +7 -5
  68. package/dist/middleware/request-context.middleware.d.ts +6 -0
  69. package/dist/middleware/request-context.middleware.js +48 -0
  70. package/dist/middleware/require-auth.middleware.d.ts +7 -76
  71. package/dist/middleware/require-auth.middleware.js +29 -117
  72. package/dist/middleware/require-machine.middleware.d.ts +13 -0
  73. package/dist/middleware/require-machine.middleware.js +128 -0
  74. package/dist/middleware/require-org-permission-from-body.middleware.d.ts +10 -0
  75. package/dist/middleware/require-org-permission-from-body.middleware.js +23 -0
  76. package/dist/middleware/require-org-permission.middleware.d.ts +19 -0
  77. package/dist/middleware/require-org-permission.middleware.js +115 -0
  78. package/dist/middleware/require-platform-permission.middleware.d.ts +19 -0
  79. package/dist/middleware/require-platform-permission.middleware.js +91 -0
  80. package/dist/middleware/require-user.middleware.d.ts +14 -0
  81. package/dist/middleware/require-user.middleware.js +30 -0
  82. package/dist/middleware/require-workspace-permission.middleware.d.ts +20 -0
  83. package/dist/middleware/require-workspace-permission.middleware.js +114 -0
  84. package/dist/middleware/validate.middleware.d.ts +31 -31
  85. package/dist/middleware/validate.middleware.js +31 -33
  86. package/dist/types/audit.types.d.ts +39 -0
  87. package/dist/types/audit.types.js +30 -0
  88. package/dist/types/express.types.d.ts +92 -124
  89. package/dist/types/express.types.js +30 -49
  90. package/dist/types/index.d.ts +1 -1
  91. package/dist/utils/date.util.d.ts +6 -0
  92. package/dist/utils/date.util.js +11 -0
  93. package/dist/utils/env.util.d.ts +6 -0
  94. package/dist/utils/env.util.js +19 -0
  95. package/dist/utils/index.d.ts +5 -4
  96. package/dist/utils/index.js +5 -4
  97. package/dist/utils/logger.util.d.ts +31 -0
  98. package/dist/utils/logger.util.js +91 -0
  99. package/dist/utils/pagination.util.d.ts +6 -0
  100. package/dist/utils/pagination.util.js +20 -0
  101. package/dist/utils/request-context.als.d.ts +10 -0
  102. package/dist/utils/request-context.als.js +5 -0
  103. package/dist/utils/response.util.d.ts +27 -0
  104. package/dist/utils/response.util.js +56 -0
  105. package/dist/utils/slug.util.d.ts +9 -0
  106. package/dist/utils/slug.util.js +32 -0
  107. package/dist/utils/url-safety.util.d.ts +6 -0
  108. package/dist/utils/url-safety.util.js +75 -0
  109. package/package.json +3 -1
  110. package/dist/authorization/permissions.d.ts +0 -78
  111. package/dist/authorization/permissions.js +0 -174
  112. package/dist/context/access-context.d.ts +0 -10
  113. package/dist/context/access-context.js +0 -2
  114. package/dist/dto/auth-service.dtos.d.ts +0 -44
  115. package/dist/dto/auth-service.dtos.js +0 -2
  116. package/dist/dto/index.d.ts +0 -3
  117. package/dist/dto/response.dtos.d.ts +0 -55
  118. package/dist/dto/response.dtos.js +0 -6
  119. package/dist/dto/user-service.dtos.d.ts +0 -50
  120. package/dist/dto/user-service.dtos.js +0 -2
  121. package/dist/enums/auth-service.enums.d.ts +0 -12
  122. package/dist/enums/auth-service.enums.js +0 -17
  123. package/dist/enums/permissions.types.d.ts +0 -12
  124. package/dist/enums/permissions.types.js +0 -17
  125. package/dist/enums/user-service.enums.d.ts +0 -32
  126. package/dist/enums/user-service.enums.js +0 -41
  127. package/dist/internal/index.d.ts +0 -4
  128. package/dist/internal/index.js +0 -20
  129. package/dist/internal/internal-jwt.service.d.ts +0 -13
  130. package/dist/internal/internal-jwt.service.js +0 -88
  131. package/dist/internal/internal-jwt.types.d.ts +0 -7
  132. package/dist/internal/internal-jwt.types.js +0 -2
  133. package/dist/internal/internal-key-manager.d.ts +0 -16
  134. package/dist/internal/internal-key-manager.js +0 -67
  135. package/dist/internal/registry.d.ts +0 -8
  136. package/dist/internal/registry.js +0 -34
  137. package/dist/internal/service-client.d.ts +0 -9
  138. package/dist/internal/service-client.js +0 -94
  139. package/dist/jwt/internal-jwt-verifier.d.ts +0 -41
  140. package/dist/jwt/internal-jwt-verifier.js +0 -185
  141. package/dist/jwt/jwt-verifier.d.ts +0 -9
  142. package/dist/jwt/jwt-verifier.js +0 -36
  143. package/dist/jwt/service-client.d.ts +0 -7
  144. package/dist/jwt/service-client.js +0 -87
  145. package/dist/middleware/authorize.d.ts +0 -3
  146. package/dist/middleware/authorize.js +0 -24
  147. package/dist/middleware/authorize.middleware.d.ts +0 -54
  148. package/dist/middleware/authorize.middleware.js +0 -104
  149. package/dist/middleware/error-handler.d.ts +0 -4
  150. package/dist/middleware/error-handler.js +0 -23
  151. package/dist/middleware/request-id.d.ts +0 -2
  152. package/dist/middleware/request-id.js +0 -9
  153. package/dist/middleware/request-id.middleware.d.ts +0 -22
  154. package/dist/middleware/request-id.middleware.js +0 -34
  155. package/dist/middleware/require-auth.d.ts +0 -10
  156. package/dist/middleware/require-auth.js +0 -34
  157. package/dist/middleware/require-human.middleware.d.ts +0 -2
  158. package/dist/middleware/require-human.middleware.js +0 -18
  159. package/dist/middleware/require-internal.middleware.d.ts +0 -18
  160. package/dist/middleware/require-internal.middleware.js +0 -183
  161. package/dist/middleware/validate.d.ts +0 -5
  162. package/dist/middleware/validate.js +0 -18
  163. package/dist/middleware/validated-merge.middleware.d.ts +0 -20
  164. package/dist/middleware/validated-merge.middleware.js +0 -33
  165. package/dist/middleware/verify-internal-jwt.d.ts +0 -7
  166. package/dist/middleware/verify-internal-jwt.js +0 -25
  167. package/dist/security/guard.d.ts +0 -10
  168. package/dist/security/guard.js +0 -40
  169. package/dist/security/index.d.ts +0 -1
  170. package/dist/types/express.d.ts +0 -22
  171. package/dist/types/express.js +0 -3
  172. package/dist/utils/date.utils.d.ts +0 -25
  173. package/dist/utils/date.utils.js +0 -30
  174. package/dist/utils/env.d.ts +0 -46
  175. package/dist/utils/env.js +0 -61
  176. package/dist/utils/env.utils.d.ts +0 -46
  177. package/dist/utils/env.utils.js +0 -61
  178. package/dist/utils/logger.utils.d.ts +0 -66
  179. package/dist/utils/logger.utils.js +0 -97
  180. package/dist/utils/response.d.ts +0 -4
  181. package/dist/utils/response.js +0 -35
  182. package/dist/utils/response.utils.d.ts +0 -54
  183. package/dist/utils/response.utils.js +0 -85
@@ -1,2 +1,2 @@
1
- export * from "./permissions";
2
- export * from "./permission-cache.service";
1
+ export * from './permission-cache.service';
2
+ export * from './permission-checker';
@@ -14,5 +14,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- __exportStar(require("./permissions"), exports);
18
17
  __exportStar(require("./permission-cache.service"), exports);
18
+ __exportStar(require("./permission-checker"), exports);
@@ -1,25 +1,113 @@
1
- import { GlobalPermission, AccountRole } from "../enums";
1
+ /**
2
+ * @file permission-cache.service.ts
3
+ * @description Redis-backed permission cache for RBAC authorization.
4
+ * Stores resolved permission sets for platform, organization, and workspace scopes.
5
+ * Does not calculate permissions — RBAC resolution is database-driven.
6
+ */
7
+ /**
8
+ * Redis-backed permission cache.
9
+ *
10
+ * Source of truth:
11
+ *
12
+ * User / Account
13
+ * ↓
14
+ * RoleAssignment
15
+ * ↓
16
+ * Role
17
+ * ↓
18
+ * Permission
19
+ *
20
+ * This service only caches the resolved permission keys.
21
+ */
2
22
  export declare class PermissionCacheService {
3
23
  private readonly client;
24
+ /**
25
+ * Initializes Redis client and connection listeners.
26
+ */
4
27
  constructor();
28
+ /**
29
+ * Resolves platform-level permissions.
30
+ *
31
+ * @param {string} accountId - Account ID.
32
+ * @param {Function} resolver - Database permission resolver.
33
+ * @returns {Promise<string[]>} Permission keys.
34
+ */
35
+ resolvePlatformPermissions(accountId: string, resolver: () => Promise<string[]>): Promise<string[]>;
36
+ /**
37
+ * Resolves organization-scoped permissions.
38
+ *
39
+ * @param {string} userId - User ID.
40
+ * @param {string} organizationId - Organization ID.
41
+ * @param {Function} resolver - Database permission resolver.
42
+ * @returns {Promise<string[]>} Permission keys.
43
+ */
44
+ resolveOrganizationPermissions(userId: string, organizationId: string, resolver: () => Promise<string[]>): Promise<string[]>;
45
+ /**
46
+ * Resolves workspace-scoped permissions.
47
+ *
48
+ * @param {string} userId - User ID.
49
+ * @param {string} workspaceId - Workspace ID.
50
+ * @param {Function} resolver - Database permission resolver.
51
+ * @returns {Promise<string[]>} Permission keys.
52
+ */
53
+ resolveWorkspacePermissions(userId: string, workspaceId: string, resolver: () => Promise<string[]>): Promise<string[]>;
54
+ /**
55
+ * Invalidates platform permission cache.
56
+ *
57
+ * @param {string} accountId - Account ID.
58
+ */
59
+ invalidatePlatform(accountId: string): Promise<void>;
60
+ /**
61
+ * Invalidates organization permission cache.
62
+ *
63
+ * @param {string} userId - User ID.
64
+ * @param {string} organizationId - Organization ID.
65
+ */
66
+ invalidateOrganization(userId: string, organizationId: string): Promise<void>;
67
+ /**
68
+ * Invalidates workspace permission cache.
69
+ *
70
+ * @param {string} userId - User ID.
71
+ * @param {string} workspaceId - Workspace ID.
72
+ */
73
+ invalidateWorkspace(userId: string, workspaceId: string): Promise<void>;
74
+ /**
75
+ * Gracefully closes Redis connection.
76
+ */
5
77
  disconnect(): Promise<void>;
6
- resolve(accountId: string, accountRole: AccountRole): Promise<GlobalPermission[]>;
7
- invalidate(accountId: string): Promise<void>;
8
- invalidateMany(accountIds: string[]): Promise<void>;
9
78
  /**
10
- * isReady
11
- * Authoritative liveness check using the client's own state machine.
79
+ * Resolves permissions using cache-aside pattern.
80
+ *
81
+ * Redis hit:
82
+ * cache → return
83
+ *
84
+ * Redis miss:
85
+ * database → cache → return
86
+ *
87
+ * @param {string} key - Cache key.
88
+ * @param {Function} resolver - Database resolver.
89
+ */
90
+ private resolve;
91
+ /**
92
+ * Reads permission list from Redis.
93
+ *
94
+ * @param {string} key - Cache key.
95
+ */
96
+ private get;
97
+ /**
98
+ * Stores permission list in Redis.
12
99
  *
13
- * Replaces a manually tracked `connected` boolean, which could drift
14
- * after reconnect cycles — the node-redis client reconnects automatically
15
- * after a transient failure, but a boolean set only on the first "ready"
16
- * event would not reflect subsequent disconnects and reconnects.
100
+ * @param {string} key - Cache key.
101
+ * @param {string[]} permissions - Permission keys.
102
+ */
103
+ private set;
104
+ /**
105
+ * Deletes permission cache.
17
106
  *
18
- * `client.isReady` is kept current by the client's internal state machine
19
- * and is the correct primitive for this guard.
107
+ * @param {string} key - Cache key.
20
108
  */
21
- private isReady;
22
- private key;
23
- private safeGet;
24
- private safeSet;
109
+ private delete;
110
+ private platformKey;
111
+ private organizationKey;
112
+ private workspaceKey;
25
113
  }
@@ -1,200 +1,229 @@
1
1
  "use strict";
2
+ /**
3
+ * @file permission-cache.service.ts
4
+ * @description Redis-backed permission cache for RBAC authorization.
5
+ * Stores resolved permission sets for platform, organization, and workspace scopes.
6
+ * Does not calculate permissions — RBAC resolution is database-driven.
7
+ */
2
8
  Object.defineProperty(exports, "__esModule", { value: true });
3
9
  exports.PermissionCacheService = void 0;
4
10
  const redis_1 = require("redis");
5
- const permissions_1 = require("./permissions");
11
+ const env_util_1 = require("../utils/env.util");
12
+ const CACHE_TTL_SECONDS = 24 * 60 * 60; // 24 hours
13
+ const PLATFORM_PREFIX = 'perms:platform';
14
+ const ORG_PREFIX = 'perms:org';
15
+ const WORKSPACE_PREFIX = 'perms:workspace';
6
16
  /**
7
- * PERMISSION CACHE SERVICE (@discover-cloud/shared)
8
- * ─────────────────────────────────────────────────────
9
- * Redis-backed permission cache. Sits between JWT verification and route
10
- * handlers — loads permissions for a verified accountId + role, caches
11
- * them, and exposes invalidation for role changes and account lifecycle events.
12
- *
13
- * Cache key: perms:{accountId}
14
- * Cache value: JSON array of GlobalPermission
15
- * TTL: 24 hours (auto-evicted; role changes trigger explicit invalidation)
16
- *
17
- * Cold cache strategy:
18
- * Derive from globalRolePermissions (role → perms map).
19
- * No DB call — the role is already verified in the JWT, and all permissions
20
- * are statically derived from it.
17
+ * Redis-backed permission cache.
21
18
  *
22
- * Invalidation triggers:
23
- * - Role change → invalidate(accountId)
24
- * - Suspension → invalidate(accountId)
25
- * - Account deletion → invalidate(accountId)
19
+ * Source of truth:
26
20
  *
27
- * Connection lifecycle:
28
- * - isReady() is the authoritative liveness check — it reflects the actual
29
- * client state (ready / connecting / reconnecting / closing) rather than a
30
- * manually tracked boolean that can drift after automatic reconnects.
31
- * - Call disconnect() in your SIGTERM handler to drain in-flight commands
32
- * and release the TCP socket before the process exits.
21
+ * User / Account
22
+ *
23
+ * RoleAssignment
24
+ *
25
+ * Role
26
+ *
27
+ * Permission
33
28
  *
34
- * Fault tolerance:
35
- * - All read/write paths guard via isReady() and fall back gracefully.
36
- * - On a cold-cache miss during Redis unavailability, permissions are derived
37
- * from the static role map (degraded but functional — no request is blocked).
38
- * - safeGet validates the parsed cache value shape before returning it;
39
- * malformed entries are treated as cache misses rather than runtime errors.
40
- *
41
- * NOTE: Uses console.* for logging — shared cannot depend on a service-specific
42
- * logger (pino, winston, etc.). Each service's own logger will capture these
43
- * via stdout in production.
29
+ * This service only caches the resolved permission keys.
44
30
  */
45
- const CACHE_TTL_SECONDS = 24 * 60 * 60; // 24 hours
46
- const KEY_PREFIX = "perms";
47
31
  class PermissionCacheService {
32
+ /**
33
+ * Initializes Redis client and connection listeners.
34
+ */
48
35
  constructor() {
49
36
  this.client = (0, redis_1.createClient)({
50
- url: process.env["REDIS_URL"] ?? "redis://localhost:6379",
37
+ url: (0, env_util_1.getEnvOptional)('REDIS_URL') ?? 'redis://localhost:6379',
51
38
  });
52
- this.client.on("error", (err) => {
53
- console.error("[PermissionCache] Redis client error", err);
39
+ this.client.on('error', (err) => {
40
+ console.error('[PermissionCache] Redis error', err);
54
41
  });
55
- this.client.on("ready", () => {
56
- console.info("[PermissionCache] Redis connected and ready");
42
+ this.client.on('ready', () => {
43
+ console.info('[PermissionCache] Redis ready');
57
44
  });
58
- this.client.on("reconnecting", () => {
59
- console.warn("[PermissionCache] Redis reconnecting...");
45
+ this.client.on('reconnecting', () => {
46
+ console.warn('[PermissionCache] Redis reconnecting...');
60
47
  });
61
- // Connect asynchronously — all read/write methods guard via isReady()
62
- // and fall back gracefully, so startup is non-blocking.
63
48
  this.client.connect().catch((err) => {
64
- console.error("[PermissionCache] Redis initial connection failed", err);
49
+ console.error('[PermissionCache] Redis connection failed', err);
65
50
  });
66
51
  }
67
- /* ----------------------------------------------------------------
68
- disconnect
69
- Gracefully closes the Redis connection. Call during service shutdown
70
- (SIGTERM handler) to drain in-flight commands and release the TCP
71
- socket before the process exits.
72
- ---------------------------------------------------------------- */
73
- async disconnect() {
74
- try {
75
- await this.client.quit();
76
- console.info("[PermissionCache] Redis connection closed");
77
- }
78
- catch (err) {
79
- console.error("[PermissionCache] Error during disconnect", err);
80
- }
52
+ /* ---------------------------------------------------------------------------
53
+ Permission Resolution
54
+ --------------------------------------------------------------------------- */
55
+ /**
56
+ * Resolves platform-level permissions.
57
+ *
58
+ * @param {string} accountId - Account ID.
59
+ * @param {Function} resolver - Database permission resolver.
60
+ * @returns {Promise<string[]>} Permission keys.
61
+ */
62
+ async resolvePlatformPermissions(accountId, resolver) {
63
+ return this.resolve(this.platformKey(accountId), resolver);
81
64
  }
82
- /* ----------------------------------------------------------------
83
- resolve
84
- Returns permissions for an account, loading from cache or deriving
85
- from the role map on a miss.
86
-
87
- Never throws — on any Redis failure it falls back to deriving
88
- from the role map directly (degraded but functional).
89
- ---------------------------------------------------------------- */
90
- async resolve(accountId, accountRole) {
91
- const cached = await this.safeGet(accountId);
92
- if (cached !== null)
93
- return cached;
94
- // Cache miss — derive from role map (no DB call needed; the role is
95
- // already verified in the JWT payload by the time we get here).
96
- const perms = [...(permissions_1.globalRolePermissions[accountRole] ?? [])];
97
- // Write back fire-and-forget — safeSet swallows errors internally,
98
- // so we don't await or attach a catch here.
99
- void this.safeSet(accountId, perms);
100
- return perms;
65
+ /* --------------------------------------------------------------------------
66
+ resolveOrganizationPermissions
67
+ -------------------------------------------------------------------------- */
68
+ /**
69
+ * Resolves organization-scoped permissions.
70
+ *
71
+ * @param {string} userId - User ID.
72
+ * @param {string} organizationId - Organization ID.
73
+ * @param {Function} resolver - Database permission resolver.
74
+ * @returns {Promise<string[]>} Permission keys.
75
+ */
76
+ async resolveOrganizationPermissions(userId, organizationId, resolver) {
77
+ return this.resolve(this.organizationKey(userId, organizationId), resolver);
101
78
  }
102
- /* ----------------------------------------------------------------
103
- invalidate
104
- Removes a specific account's permission cache entry.
105
- Call on: role change, suspension, account deletion.
106
- ---------------------------------------------------------------- */
107
- async invalidate(accountId) {
108
- if (!this.isReady()) {
109
- // Skip deletion the stale entry will expire via TTL at worst.
110
- console.warn(`[PermissionCache] Skipping invalidation for ${accountId} Redis not ready`);
111
- return;
112
- }
113
- try {
114
- await this.client.del(this.key(accountId));
115
- console.info(`[PermissionCache] Invalidated: ${accountId}`);
116
- }
117
- catch (err) {
118
- // Log but don't throw — stale entry expires via TTL at worst.
119
- console.error(`[PermissionCache] Failed to invalidate: ${accountId}`, err);
120
- }
79
+ /* --------------------------------------------------------------------------
80
+ resolveWorkspacePermissions
81
+ -------------------------------------------------------------------------- */
82
+ /**
83
+ * Resolves workspace-scoped permissions.
84
+ *
85
+ * @param {string} userId - User ID.
86
+ * @param {string} workspaceId - Workspace ID.
87
+ * @param {Function} resolver - Database permission resolver.
88
+ * @returns {Promise<string[]>} Permission keys.
89
+ */
90
+ async resolveWorkspacePermissions(userId, workspaceId, resolver) {
91
+ return this.resolve(this.workspaceKey(userId, workspaceId), resolver);
121
92
  }
122
- /* ----------------------------------------------------------------
123
- invalidateMany
124
- Batch invalidation — use when a role definition changes globally.
125
- Sends a single DEL with multiple keys (O(n) in Redis, one round-trip).
126
- ---------------------------------------------------------------- */
127
- async invalidateMany(accountIds) {
128
- if (accountIds.length === 0)
129
- return;
130
- if (!this.isReady()) {
131
- console.warn(`[PermissionCache] Skipping batch invalidation (${accountIds.length} entries) — Redis not ready`);
132
- return;
133
- }
134
- try {
135
- const keys = accountIds.map((id) => this.key(id));
136
- await this.client.del(keys);
137
- console.info(`[PermissionCache] Batch invalidated ${accountIds.length} entries`);
138
- }
139
- catch (err) {
140
- console.error("[PermissionCache] Batch invalidation failed", err);
141
- }
93
+ /* ---------------------------------------------------------------------------
94
+ Cache Invalidation
95
+ --------------------------------------------------------------------------- */
96
+ /**
97
+ * Invalidates platform permission cache.
98
+ *
99
+ * @param {string} accountId - Account ID.
100
+ */
101
+ async invalidatePlatform(accountId) {
102
+ await this.delete(this.platformKey(accountId));
142
103
  }
143
- /* ----------------------------------------------------------------
144
- Private helpers
145
- ---------------------------------------------------------------- */
104
+ /* --------------------------------------------------------------------------
105
+ invalidateOrganization
106
+ -------------------------------------------------------------------------- */
146
107
  /**
147
- * isReady
148
- * Authoritative liveness check using the client's own state machine.
108
+ * Invalidates organization permission cache.
149
109
  *
150
- * Replaces a manually tracked `connected` boolean, which could drift
151
- * after reconnect cycles — the node-redis client reconnects automatically
152
- * after a transient failure, but a boolean set only on the first "ready"
153
- * event would not reflect subsequent disconnects and reconnects.
110
+ * @param {string} userId - User ID.
111
+ * @param {string} organizationId - Organization ID.
112
+ */
113
+ async invalidateOrganization(userId, organizationId) {
114
+ await this.delete(this.organizationKey(userId, organizationId));
115
+ }
116
+ /* --------------------------------------------------------------------------
117
+ invalidateWorkspace
118
+ -------------------------------------------------------------------------- */
119
+ /**
120
+ * Invalidates workspace permission cache.
154
121
  *
155
- * `client.isReady` is kept current by the client's internal state machine
156
- * and is the correct primitive for this guard.
122
+ * @param {string} userId - User ID.
123
+ * @param {string} workspaceId - Workspace ID.
124
+ */
125
+ async invalidateWorkspace(userId, workspaceId) {
126
+ await this.delete(this.workspaceKey(userId, workspaceId));
127
+ }
128
+ /* --------------------------------------------------------------------------
129
+ disconnect
130
+ -------------------------------------------------------------------------- */
131
+ /**
132
+ * Gracefully closes Redis connection.
157
133
  */
158
- isReady() {
159
- return this.client.isReady;
134
+ async disconnect() {
135
+ await this.client.quit();
160
136
  }
161
- key(accountId) {
162
- return `${KEY_PREFIX}:${accountId}`;
137
+ /* ---------------------------------------------------------------------------
138
+ Internal Cache Helpers
139
+ --------------------------------------------------------------------------- */
140
+ /**
141
+ * Resolves permissions using cache-aside pattern.
142
+ *
143
+ * Redis hit:
144
+ * cache → return
145
+ *
146
+ * Redis miss:
147
+ * database → cache → return
148
+ *
149
+ * @param {string} key - Cache key.
150
+ * @param {Function} resolver - Database resolver.
151
+ */
152
+ async resolve(key, resolver) {
153
+ const cached = await this.get(key);
154
+ if (cached !== null) {
155
+ return cached;
156
+ }
157
+ const permissions = await resolver();
158
+ await this.set(key, permissions);
159
+ return permissions;
163
160
  }
164
- async safeGet(accountId) {
165
- if (!this.isReady())
161
+ /**
162
+ * Reads permission list from Redis.
163
+ *
164
+ * @param {string} key - Cache key.
165
+ */
166
+ async get(key) {
167
+ if (!this.client.isReady) {
166
168
  return null;
169
+ }
167
170
  try {
168
- const raw = await this.client.get(this.key(accountId));
169
- if (!raw)
171
+ const raw = await this.client.get(key);
172
+ if (!raw) {
170
173
  return null;
174
+ }
171
175
  const parsed = JSON.parse(raw);
172
- // Validate shape before trusting the cached value. A malformed entry
173
- // (e.g. from a prior schema change or a manual key mutation) should
174
- // produce a cache miss, not a silent type error downstream.
175
176
  if (!Array.isArray(parsed)) {
176
- console.warn(`[PermissionCache] Unexpected cache shape for ${accountId}, treating as miss`);
177
177
  return null;
178
178
  }
179
179
  return parsed;
180
180
  }
181
181
  catch (err) {
182
- // JSON.parse failure or Redis error — bypass the cache gracefully.
183
- console.warn(`[PermissionCache] Cache get failed for ${accountId}, bypassing`, err);
182
+ console.warn(`[PermissionCache] Cache read failed: ${key}`, err);
184
183
  return null;
185
184
  }
186
185
  }
187
- async safeSet(accountId, perms) {
188
- if (!this.isReady())
186
+ /**
187
+ * Stores permission list in Redis.
188
+ *
189
+ * @param {string} key - Cache key.
190
+ * @param {string[]} permissions - Permission keys.
191
+ */
192
+ async set(key, permissions) {
193
+ if (!this.client.isReady) {
189
194
  return;
195
+ }
190
196
  try {
191
- await this.client.set(this.key(accountId), JSON.stringify(perms), {
197
+ await this.client.set(key, JSON.stringify(permissions), {
192
198
  EX: CACHE_TTL_SECONDS,
193
199
  });
194
200
  }
195
201
  catch (err) {
196
- console.warn(`[PermissionCache] Cache set failed for ${accountId}`, err);
202
+ console.warn(`[PermissionCache] Cache write failed: ${key}`, err);
197
203
  }
198
204
  }
205
+ /**
206
+ * Deletes permission cache.
207
+ *
208
+ * @param {string} key - Cache key.
209
+ */
210
+ async delete(key) {
211
+ if (!this.client.isReady) {
212
+ return;
213
+ }
214
+ await this.client.del(key);
215
+ }
216
+ /* ---------------------------------------------------------------------------
217
+ Cache Key Builders
218
+ --------------------------------------------------------------------------- */
219
+ platformKey(accountId) {
220
+ return `${PLATFORM_PREFIX}:${accountId}`;
221
+ }
222
+ organizationKey(userId, organizationId) {
223
+ return `${ORG_PREFIX}:${userId}:${organizationId}`;
224
+ }
225
+ workspaceKey(userId, workspaceId) {
226
+ return `${WORKSPACE_PREFIX}:${userId}:${workspaceId}`;
227
+ }
199
228
  }
200
229
  exports.PermissionCacheService = PermissionCacheService;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @file permission-checker.ts
3
+ * @description Centralized utility for validating resolved permission string arrays against required permissions.
4
+ */
5
+ export declare class PermissionChecker {
6
+ /**
7
+ * Checks whether the resolved list has the specific permission.
8
+ *
9
+ * @param {readonly string[]} permissions - Active resolved permissions.
10
+ * @param {string} permission - The required permission key.
11
+ * @returns {boolean} True if the permission exists in the array.
12
+ */
13
+ static has(permissions: readonly string[], permission: string): boolean;
14
+ /**
15
+ * Checks whether the resolved list contains at least one of the required permissions.
16
+ *
17
+ * @param {readonly string[]} permissions - Active resolved permissions.
18
+ * @param {readonly string[]} required - Allowed keys.
19
+ * @returns {boolean} True if at least one matches.
20
+ */
21
+ static hasAny(permissions: readonly string[], required: readonly string[]): boolean;
22
+ /**
23
+ * Checks whether the resolved list contains all of the required permissions.
24
+ *
25
+ * @param {readonly string[]} permissions - Active resolved permissions.
26
+ * @param {readonly string[]} required - Required keys.
27
+ * @returns {boolean} True if all match.
28
+ */
29
+ static hasAll(permissions: readonly string[], required: readonly string[]): boolean;
30
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ /**
3
+ * @file permission-checker.ts
4
+ * @description Centralized utility for validating resolved permission string arrays against required permissions.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.PermissionChecker = void 0;
8
+ class PermissionChecker {
9
+ /**
10
+ * Checks whether the resolved list has the specific permission.
11
+ *
12
+ * @param {readonly string[]} permissions - Active resolved permissions.
13
+ * @param {string} permission - The required permission key.
14
+ * @returns {boolean} True if the permission exists in the array.
15
+ */
16
+ static has(permissions, permission) {
17
+ return permissions.includes(permission);
18
+ }
19
+ /**
20
+ * Checks whether the resolved list contains at least one of the required permissions.
21
+ *
22
+ * @param {readonly string[]} permissions - Active resolved permissions.
23
+ * @param {readonly string[]} required - Allowed keys.
24
+ * @returns {boolean} True if at least one matches.
25
+ */
26
+ static hasAny(permissions, required) {
27
+ return required.some((permission) => permissions.includes(permission));
28
+ }
29
+ /**
30
+ * Checks whether the resolved list contains all of the required permissions.
31
+ *
32
+ * @param {readonly string[]} permissions - Active resolved permissions.
33
+ * @param {readonly string[]} required - Required keys.
34
+ * @returns {boolean} True if all match.
35
+ */
36
+ static hasAll(permissions, required) {
37
+ return required.every((permission) => permissions.includes(permission));
38
+ }
39
+ }
40
+ exports.PermissionChecker = PermissionChecker;
@@ -1 +1 @@
1
- export * from "./access-context.js";
1
+ export * from './request-context.storage';
@@ -14,4 +14,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- __exportStar(require("./access-context.js"), exports);
17
+ __exportStar(require("./request-context.storage"), exports);
@@ -0,0 +1,21 @@
1
+ import { AsyncLocalStorage } from 'async_hooks';
2
+ import { RequestMetadataContext, UserContext, MachineContext } from '../types';
3
+ export type AuditRequestContext = RequestMetadataContext & {
4
+ actor?: UserContext | MachineContext;
5
+ };
6
+ export declare const requestContextStorage: AsyncLocalStorage<AuditRequestContext>;
7
+ export declare class RequestContextProvider {
8
+ /**
9
+ * Retrieves the current context if it exists.
10
+ */
11
+ static get(): AuditRequestContext | undefined;
12
+ /**
13
+ * Retrieves the current context or throws an error.
14
+ * Use this in deeper repositories where context is strictly required.
15
+ */
16
+ static getOrThrow(): AuditRequestContext;
17
+ /**
18
+ * Attaches the authenticated actor to the current request context.
19
+ */
20
+ static setActor(actor: UserContext | MachineContext): void;
21
+ }