@m5kdev/backend 0.1.0 → 0.1.2

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 (294) hide show
  1. package/.cursor/rules/backend.mdc +70 -0
  2. package/.turbo/turbo-build.log +5 -0
  3. package/.turbo/turbo-check-types.log +5 -0
  4. package/.turbo/turbo-lint$colon$fix.log +255 -0
  5. package/CHANGELOG.md +19 -0
  6. package/dist/src/lib/posthog.d.ts +3 -0
  7. package/dist/src/lib/posthog.d.ts.map +1 -0
  8. package/dist/src/lib/posthog.js +7 -0
  9. package/dist/src/lib/sentry.d.ts +2 -0
  10. package/dist/src/lib/sentry.d.ts.map +1 -0
  11. package/dist/src/lib/sentry.js +9 -0
  12. package/dist/src/modules/access/access.repository.d.ts +2348 -0
  13. package/dist/src/modules/access/access.repository.d.ts.map +1 -0
  14. package/dist/src/modules/access/access.repository.js +32 -0
  15. package/dist/src/modules/access/access.service.d.ts +22 -0
  16. package/dist/src/modules/access/access.service.d.ts.map +1 -0
  17. package/dist/src/modules/access/access.service.js +51 -0
  18. package/dist/src/modules/access/access.test.d.ts +2 -0
  19. package/dist/src/modules/access/access.test.d.ts.map +1 -0
  20. package/dist/src/modules/access/access.test.js +182 -0
  21. package/dist/src/modules/access/access.utils.d.ts +17 -0
  22. package/dist/src/modules/access/access.utils.d.ts.map +1 -0
  23. package/dist/src/modules/access/access.utils.js +20 -0
  24. package/dist/src/modules/ai/ai.db.d.ts +396 -0
  25. package/dist/src/modules/ai/ai.db.d.ts.map +1 -0
  26. package/dist/src/modules/ai/ai.db.js +39 -0
  27. package/dist/src/modules/ai/ai.prompt.d.ts +28 -0
  28. package/dist/src/modules/ai/ai.prompt.d.ts.map +1 -0
  29. package/dist/src/modules/ai/ai.prompt.js +30 -0
  30. package/dist/src/modules/ai/ai.repository.d.ts +424 -0
  31. package/dist/src/modules/ai/ai.repository.d.ts.map +1 -0
  32. package/dist/src/modules/ai/ai.repository.js +26 -0
  33. package/dist/src/modules/ai/ai.router.d.ts +2 -0
  34. package/dist/src/modules/ai/ai.router.d.ts.map +1 -0
  35. package/dist/src/modules/ai/ai.router.js +132 -0
  36. package/dist/src/modules/ai/ai.service.d.ts +115 -0
  37. package/dist/src/modules/ai/ai.service.d.ts.map +1 -0
  38. package/dist/src/modules/ai/ai.service.js +207 -0
  39. package/dist/src/modules/ai/ai.trpc.d.ts +59 -0
  40. package/dist/src/modules/ai/ai.trpc.d.ts.map +1 -0
  41. package/dist/src/modules/ai/ai.trpc.js +20 -0
  42. package/dist/src/modules/ai/ideogram/ideogram.constants.d.ts +8 -0
  43. package/dist/src/modules/ai/ideogram/ideogram.constants.d.ts.map +1 -0
  44. package/dist/src/modules/ai/ideogram/ideogram.constants.js +167 -0
  45. package/dist/src/modules/ai/ideogram/ideogram.dto.d.ts +230 -0
  46. package/dist/src/modules/ai/ideogram/ideogram.dto.d.ts.map +1 -0
  47. package/dist/src/modules/ai/ideogram/ideogram.dto.js +49 -0
  48. package/dist/src/modules/ai/ideogram/ideogram.prompt.d.ts +3 -0
  49. package/dist/src/modules/ai/ideogram/ideogram.prompt.d.ts.map +1 -0
  50. package/dist/src/modules/ai/ideogram/ideogram.prompt.js +860 -0
  51. package/dist/src/modules/ai/ideogram/ideogram.repository.d.ts +7 -0
  52. package/dist/src/modules/ai/ideogram/ideogram.repository.d.ts.map +1 -0
  53. package/dist/src/modules/ai/ideogram/ideogram.repository.js +46 -0
  54. package/dist/src/modules/ai/ideogram/ideogram.service.d.ts +10 -0
  55. package/dist/src/modules/ai/ideogram/ideogram.service.d.ts.map +1 -0
  56. package/dist/src/modules/ai/ideogram/ideogram.service.js +11 -0
  57. package/dist/src/modules/auth/auth.db.d.ts +2336 -0
  58. package/dist/src/modules/auth/auth.db.d.ts.map +1 -0
  59. package/dist/src/modules/auth/auth.db.js +215 -0
  60. package/dist/src/modules/auth/auth.dto.d.ts +66 -0
  61. package/dist/src/modules/auth/auth.dto.d.ts.map +1 -0
  62. package/dist/src/modules/auth/auth.dto.js +38 -0
  63. package/dist/src/modules/auth/auth.lib.d.ts +4874 -0
  64. package/dist/src/modules/auth/auth.lib.d.ts.map +1 -0
  65. package/dist/src/modules/auth/auth.lib.js +284 -0
  66. package/dist/src/modules/auth/auth.middleware.d.ts +615 -0
  67. package/dist/src/modules/auth/auth.middleware.d.ts.map +1 -0
  68. package/dist/src/modules/auth/auth.middleware.js +52 -0
  69. package/dist/src/modules/auth/auth.repository.d.ts +2417 -0
  70. package/dist/src/modules/auth/auth.repository.d.ts.map +1 -0
  71. package/dist/src/modules/auth/auth.repository.js +541 -0
  72. package/dist/src/modules/auth/auth.service.d.ts +104 -0
  73. package/dist/src/modules/auth/auth.service.d.ts.map +1 -0
  74. package/dist/src/modules/auth/auth.service.js +201 -0
  75. package/dist/src/modules/auth/auth.trpc.d.ts +309 -0
  76. package/dist/src/modules/auth/auth.trpc.d.ts.map +1 -0
  77. package/dist/src/modules/auth/auth.trpc.js +157 -0
  78. package/dist/src/modules/auth/auth.utils.d.ts +2352 -0
  79. package/dist/src/modules/auth/auth.utils.d.ts.map +1 -0
  80. package/dist/src/modules/auth/auth.utils.js +97 -0
  81. package/dist/src/modules/base/base.abstract.d.ts +19 -0
  82. package/dist/src/modules/base/base.abstract.d.ts.map +1 -0
  83. package/dist/src/modules/base/base.abstract.js +53 -0
  84. package/dist/src/modules/base/base.dto.d.ts +70 -0
  85. package/dist/src/modules/base/base.dto.d.ts.map +1 -0
  86. package/dist/src/modules/base/base.dto.js +112 -0
  87. package/dist/src/modules/base/base.grants.d.ts +29 -0
  88. package/dist/src/modules/base/base.grants.d.ts.map +1 -0
  89. package/dist/src/modules/base/base.grants.js +123 -0
  90. package/dist/src/modules/base/base.grants.test.d.ts +2 -0
  91. package/dist/src/modules/base/base.grants.test.d.ts.map +1 -0
  92. package/dist/src/modules/base/base.grants.test.js +668 -0
  93. package/dist/src/modules/base/base.repository.d.ts +97 -0
  94. package/dist/src/modules/base/base.repository.d.ts.map +1 -0
  95. package/dist/src/modules/base/base.repository.js +307 -0
  96. package/dist/src/modules/base/base.service.d.ts +42 -0
  97. package/dist/src/modules/base/base.service.d.ts.map +1 -0
  98. package/dist/src/modules/base/base.service.js +109 -0
  99. package/dist/src/modules/base/base.types.d.ts +2 -0
  100. package/dist/src/modules/base/base.types.d.ts.map +1 -0
  101. package/dist/src/modules/base/base.types.js +2 -0
  102. package/dist/src/modules/billing/billing.db.d.ts +366 -0
  103. package/dist/src/modules/billing/billing.db.d.ts.map +1 -0
  104. package/dist/src/modules/billing/billing.db.js +29 -0
  105. package/dist/src/modules/billing/billing.repository.d.ts +2764 -0
  106. package/dist/src/modules/billing/billing.repository.d.ts.map +1 -0
  107. package/dist/src/modules/billing/billing.repository.js +235 -0
  108. package/dist/src/modules/billing/billing.router.d.ts +5 -0
  109. package/dist/src/modules/billing/billing.router.d.ts.map +1 -0
  110. package/dist/src/modules/billing/billing.router.js +56 -0
  111. package/dist/src/modules/billing/billing.service.d.ts +60 -0
  112. package/dist/src/modules/billing/billing.service.d.ts.map +1 -0
  113. package/dist/src/modules/billing/billing.service.js +147 -0
  114. package/dist/src/modules/billing/billing.trpc.d.ts +75 -0
  115. package/dist/src/modules/billing/billing.trpc.d.ts.map +1 -0
  116. package/dist/src/modules/billing/billing.trpc.js +17 -0
  117. package/dist/src/modules/clay/clay.repository.d.ts +6 -0
  118. package/dist/src/modules/clay/clay.repository.d.ts.map +1 -0
  119. package/dist/src/modules/clay/clay.repository.js +26 -0
  120. package/dist/src/modules/clay/clay.service.d.ts +29 -0
  121. package/dist/src/modules/clay/clay.service.d.ts.map +1 -0
  122. package/dist/src/modules/clay/clay.service.js +24 -0
  123. package/dist/src/modules/connect/connect.db.d.ts +357 -0
  124. package/dist/src/modules/connect/connect.db.d.ts.map +1 -0
  125. package/dist/src/modules/connect/connect.db.js +30 -0
  126. package/dist/src/modules/connect/connect.dto.d.ts +75 -0
  127. package/dist/src/modules/connect/connect.dto.d.ts.map +1 -0
  128. package/dist/src/modules/connect/connect.dto.js +36 -0
  129. package/dist/src/modules/connect/connect.linkedin.d.ts +3 -0
  130. package/dist/src/modules/connect/connect.linkedin.d.ts.map +1 -0
  131. package/dist/src/modules/connect/connect.linkedin.js +53 -0
  132. package/dist/src/modules/connect/connect.oauth.d.ts +28 -0
  133. package/dist/src/modules/connect/connect.oauth.d.ts.map +1 -0
  134. package/dist/src/modules/connect/connect.oauth.js +198 -0
  135. package/dist/src/modules/connect/connect.repository.d.ts +414 -0
  136. package/dist/src/modules/connect/connect.repository.d.ts.map +1 -0
  137. package/dist/src/modules/connect/connect.repository.js +54 -0
  138. package/dist/src/modules/connect/connect.router.d.ts +5 -0
  139. package/dist/src/modules/connect/connect.router.d.ts.map +1 -0
  140. package/dist/src/modules/connect/connect.router.js +54 -0
  141. package/dist/src/modules/connect/connect.service.d.ts +89 -0
  142. package/dist/src/modules/connect/connect.service.d.ts.map +1 -0
  143. package/dist/src/modules/connect/connect.service.js +114 -0
  144. package/dist/src/modules/connect/connect.trpc.d.ts +81 -0
  145. package/dist/src/modules/connect/connect.trpc.d.ts.map +1 -0
  146. package/dist/src/modules/connect/connect.trpc.js +21 -0
  147. package/dist/src/modules/connect/connect.types.d.ts +26 -0
  148. package/dist/src/modules/connect/connect.types.d.ts.map +1 -0
  149. package/dist/src/modules/connect/connect.types.js +2 -0
  150. package/dist/src/modules/crypto/crypto.db.d.ts +152 -0
  151. package/dist/src/modules/crypto/crypto.db.d.ts.map +1 -0
  152. package/dist/src/modules/crypto/crypto.db.js +17 -0
  153. package/dist/src/modules/crypto/crypto.repository.d.ts +160 -0
  154. package/dist/src/modules/crypto/crypto.repository.d.ts.map +1 -0
  155. package/dist/src/modules/crypto/crypto.repository.js +10 -0
  156. package/dist/src/modules/crypto/crypto.service.d.ts +11 -0
  157. package/dist/src/modules/crypto/crypto.service.d.ts.map +1 -0
  158. package/dist/src/modules/crypto/crypto.service.js +52 -0
  159. package/dist/src/modules/email/email.service.d.ts +57 -0
  160. package/dist/src/modules/email/email.service.d.ts.map +1 -0
  161. package/dist/src/modules/email/email.service.js +107 -0
  162. package/dist/src/modules/file/file.repository.d.ts +13 -0
  163. package/dist/src/modules/file/file.repository.d.ts.map +1 -0
  164. package/dist/src/modules/file/file.repository.js +79 -0
  165. package/dist/src/modules/file/file.router.d.ts +4 -0
  166. package/dist/src/modules/file/file.router.d.ts.map +1 -0
  167. package/dist/src/modules/file/file.router.js +99 -0
  168. package/dist/src/modules/file/file.service.d.ts +25 -0
  169. package/dist/src/modules/file/file.service.d.ts.map +1 -0
  170. package/dist/src/modules/file/file.service.js +150 -0
  171. package/dist/src/modules/recurrence/recurrence.db.d.ts +563 -0
  172. package/dist/src/modules/recurrence/recurrence.db.d.ts.map +1 -0
  173. package/dist/src/modules/recurrence/recurrence.db.js +66 -0
  174. package/dist/src/modules/recurrence/recurrence.repository.d.ts +585 -0
  175. package/dist/src/modules/recurrence/recurrence.repository.d.ts.map +1 -0
  176. package/dist/src/modules/recurrence/recurrence.repository.js +39 -0
  177. package/dist/src/modules/recurrence/recurrence.service.d.ts +30 -0
  178. package/dist/src/modules/recurrence/recurrence.service.d.ts.map +1 -0
  179. package/dist/src/modules/recurrence/recurrence.service.js +70 -0
  180. package/dist/src/modules/recurrence/recurrence.trpc.d.ts +243 -0
  181. package/dist/src/modules/recurrence/recurrence.trpc.d.ts.map +1 -0
  182. package/dist/src/modules/recurrence/recurrence.trpc.js +65 -0
  183. package/dist/src/modules/social/social.dto.d.ts +35 -0
  184. package/dist/src/modules/social/social.dto.d.ts.map +1 -0
  185. package/dist/src/modules/social/social.dto.js +18 -0
  186. package/dist/src/modules/social/social.linkedin.d.ts +11 -0
  187. package/dist/src/modules/social/social.linkedin.d.ts.map +1 -0
  188. package/dist/src/modules/social/social.linkedin.js +427 -0
  189. package/dist/src/modules/social/social.linkedin.test.d.ts +2 -0
  190. package/dist/src/modules/social/social.linkedin.test.d.ts.map +1 -0
  191. package/dist/src/modules/social/social.linkedin.test.js +235 -0
  192. package/dist/src/modules/social/social.service.d.ts +29 -0
  193. package/dist/src/modules/social/social.service.d.ts.map +1 -0
  194. package/dist/src/modules/social/social.service.js +76 -0
  195. package/dist/src/modules/social/social.types.d.ts +36 -0
  196. package/dist/src/modules/social/social.types.d.ts.map +1 -0
  197. package/dist/src/modules/social/social.types.js +2 -0
  198. package/dist/src/modules/tag/tag.db.d.ts +347 -0
  199. package/dist/src/modules/tag/tag.db.d.ts.map +1 -0
  200. package/dist/src/modules/tag/tag.db.js +42 -0
  201. package/dist/src/modules/tag/tag.dto.d.ts +1019 -0
  202. package/dist/src/modules/tag/tag.dto.d.ts.map +1 -0
  203. package/dist/src/modules/tag/tag.dto.js +9 -0
  204. package/dist/src/modules/tag/tag.repository.d.ts +384 -0
  205. package/dist/src/modules/tag/tag.repository.d.ts.map +1 -0
  206. package/dist/src/modules/tag/tag.repository.js +154 -0
  207. package/dist/src/modules/tag/tag.service.d.ts +36 -0
  208. package/dist/src/modules/tag/tag.service.d.ts.map +1 -0
  209. package/dist/src/modules/tag/tag.service.js +31 -0
  210. package/dist/src/modules/tag/tag.trpc.d.ts +191 -0
  211. package/dist/src/modules/tag/tag.trpc.d.ts.map +1 -0
  212. package/dist/src/modules/tag/tag.trpc.js +47 -0
  213. package/dist/src/modules/utils/applyPagination.d.ts +7 -0
  214. package/dist/src/modules/utils/applyPagination.d.ts.map +1 -0
  215. package/dist/src/modules/utils/applyPagination.js +16 -0
  216. package/dist/src/modules/utils/applySorting.d.ts +9 -0
  217. package/dist/src/modules/utils/applySorting.d.ts.map +1 -0
  218. package/dist/src/modules/utils/applySorting.js +18 -0
  219. package/dist/src/modules/utils/getConditionsFromFilters.d.ts +5 -0
  220. package/dist/src/modules/utils/getConditionsFromFilters.d.ts.map +1 -0
  221. package/dist/src/modules/utils/getConditionsFromFilters.js +200 -0
  222. package/dist/src/modules/video/video.service.d.ts +8 -0
  223. package/dist/src/modules/video/video.service.d.ts.map +1 -0
  224. package/dist/src/modules/video/video.service.js +84 -0
  225. package/dist/src/modules/webhook/webhook.constants.d.ts +9 -0
  226. package/dist/src/modules/webhook/webhook.constants.d.ts.map +1 -0
  227. package/dist/src/modules/webhook/webhook.constants.js +10 -0
  228. package/dist/src/modules/webhook/webhook.db.d.ts +137 -0
  229. package/dist/src/modules/webhook/webhook.db.d.ts.map +1 -0
  230. package/dist/src/modules/webhook/webhook.db.js +17 -0
  231. package/dist/src/modules/webhook/webhook.dto.d.ts +395 -0
  232. package/dist/src/modules/webhook/webhook.dto.d.ts.map +1 -0
  233. package/dist/src/modules/webhook/webhook.dto.js +7 -0
  234. package/dist/src/modules/webhook/webhook.repository.d.ts +149 -0
  235. package/dist/src/modules/webhook/webhook.repository.d.ts.map +1 -0
  236. package/dist/src/modules/webhook/webhook.repository.js +56 -0
  237. package/dist/src/modules/webhook/webhook.router.d.ts +4 -0
  238. package/dist/src/modules/webhook/webhook.router.d.ts.map +1 -0
  239. package/dist/src/modules/webhook/webhook.router.js +30 -0
  240. package/dist/src/modules/webhook/webhook.service.d.ts +10 -0
  241. package/dist/src/modules/webhook/webhook.service.d.ts.map +1 -0
  242. package/dist/src/modules/webhook/webhook.service.js +68 -0
  243. package/dist/src/modules/workflow/workflow.db.d.ts +297 -0
  244. package/dist/src/modules/workflow/workflow.db.d.ts.map +1 -0
  245. package/dist/src/modules/workflow/workflow.db.js +30 -0
  246. package/dist/src/modules/workflow/workflow.repository.d.ts +344 -0
  247. package/dist/src/modules/workflow/workflow.repository.d.ts.map +1 -0
  248. package/dist/src/modules/workflow/workflow.repository.js +105 -0
  249. package/dist/src/modules/workflow/workflow.service.d.ts +22 -0
  250. package/dist/src/modules/workflow/workflow.service.d.ts.map +1 -0
  251. package/dist/src/modules/workflow/workflow.service.js +37 -0
  252. package/dist/src/modules/workflow/workflow.trpc.d.ts +93 -0
  253. package/dist/src/modules/workflow/workflow.trpc.d.ts.map +1 -0
  254. package/dist/src/modules/workflow/workflow.trpc.js +21 -0
  255. package/dist/src/modules/workflow/workflow.types.d.ts +21 -0
  256. package/dist/src/modules/workflow/workflow.types.d.ts.map +1 -0
  257. package/dist/src/modules/workflow/workflow.types.js +2 -0
  258. package/dist/src/modules/workflow/workflow.utils.d.ts +22 -0
  259. package/dist/src/modules/workflow/workflow.utils.d.ts.map +1 -0
  260. package/dist/src/modules/workflow/workflow.utils.js +173 -0
  261. package/dist/src/test/stubs/utils.d.ts +3 -0
  262. package/dist/src/test/stubs/utils.d.ts.map +1 -0
  263. package/dist/src/test/stubs/utils.js +5 -0
  264. package/dist/src/trpc/context.d.ts +42 -0
  265. package/dist/src/trpc/context.d.ts.map +1 -0
  266. package/dist/src/trpc/context.js +17 -0
  267. package/dist/src/trpc/index.d.ts +4 -0
  268. package/dist/src/trpc/index.d.ts.map +1 -0
  269. package/dist/src/trpc/index.js +6 -0
  270. package/dist/src/trpc/procedures.d.ts +234 -0
  271. package/dist/src/trpc/procedures.d.ts.map +1 -0
  272. package/dist/src/trpc/procedures.js +32 -0
  273. package/dist/src/trpc/utils.d.ts +5 -0
  274. package/dist/src/trpc/utils.d.ts.map +1 -0
  275. package/dist/src/trpc/utils.js +20 -0
  276. package/dist/src/types.d.ts +486 -0
  277. package/dist/src/types.d.ts.map +1 -0
  278. package/dist/src/types.js +13 -0
  279. package/dist/src/utils/errors.d.ts +50 -0
  280. package/dist/src/utils/errors.d.ts.map +1 -0
  281. package/dist/src/utils/errors.js +104 -0
  282. package/dist/src/utils/logger.d.ts +2 -0
  283. package/dist/src/utils/logger.d.ts.map +1 -0
  284. package/dist/src/utils/logger.js +11 -0
  285. package/dist/src/utils/posthog.d.ts +14 -0
  286. package/dist/src/utils/posthog.d.ts.map +1 -0
  287. package/dist/src/utils/posthog.js +31 -0
  288. package/dist/src/utils/types.d.ts +5 -0
  289. package/dist/src/utils/types.d.ts.map +1 -0
  290. package/dist/src/utils/types.js +2 -0
  291. package/dist/tsconfig.tsbuildinfo +1 -0
  292. package/jest.config.ts +19 -0
  293. package/package.json +3 -6
  294. package/tsconfig.json +21 -0
@@ -0,0 +1,668 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const neverthrow_1 = require("neverthrow");
4
+ const base_grants_1 = require("#modules/base/base.grants");
5
+ const errors_1 = require("#utils/errors");
6
+ // ============================================
7
+ // Mock Factories
8
+ // ============================================
9
+ function createMockUser(overrides = {}) {
10
+ return {
11
+ id: "user-123",
12
+ role: "member",
13
+ email: "test@example.com",
14
+ emailVerified: true,
15
+ name: "Test User",
16
+ createdAt: new Date(),
17
+ updatedAt: new Date(),
18
+ image: null,
19
+ onboarding: null,
20
+ preferences: null,
21
+ flags: null,
22
+ stripeCustomerId: null,
23
+ paymentCustomerId: null,
24
+ paymentPlanTier: null,
25
+ paymentPlanExpiresAt: null,
26
+ ...overrides,
27
+ };
28
+ }
29
+ function createMockSession(overrides = {}) {
30
+ return {
31
+ id: "session-123",
32
+ userId: "user-123",
33
+ token: "token-123",
34
+ expiresAt: new Date(Date.now() + 86400000),
35
+ createdAt: new Date(),
36
+ updatedAt: new Date(),
37
+ ipAddress: null,
38
+ userAgent: null,
39
+ activeOrganizationId: null,
40
+ activeTeamId: null,
41
+ activeOrganizationRole: null,
42
+ activeTeamRole: null,
43
+ ...overrides,
44
+ };
45
+ }
46
+ function createMockContext(userOverrides = {}, sessionOverrides = {}) {
47
+ return {
48
+ user: createMockUser(userOverrides),
49
+ session: createMockSession(sessionOverrides),
50
+ };
51
+ }
52
+ function createMockEntity(overrides = {}) {
53
+ return {
54
+ userId: "user-123",
55
+ teamId: undefined,
56
+ organizationId: undefined,
57
+ ...overrides,
58
+ };
59
+ }
60
+ // ============================================
61
+ // flattenNestedGrants
62
+ // ============================================
63
+ describe("flattenNestedGrants", () => {
64
+ it("converts permission object to grants array", () => {
65
+ const permission = {
66
+ posts: {
67
+ user: {
68
+ member: { read: "own", create: "own" },
69
+ },
70
+ team: {
71
+ admin: { write: "all" },
72
+ },
73
+ },
74
+ };
75
+ const result = (0, base_grants_1.flattenNestedGrants)(permission);
76
+ expect(result).toHaveLength(3);
77
+ expect(result).toContainEqual({
78
+ resource: "posts",
79
+ level: "user",
80
+ role: "member",
81
+ action: "read",
82
+ access: "own",
83
+ });
84
+ expect(result).toContainEqual({
85
+ resource: "posts",
86
+ level: "user",
87
+ role: "member",
88
+ action: "create",
89
+ access: "own",
90
+ });
91
+ expect(result).toContainEqual({
92
+ resource: "posts",
93
+ level: "team",
94
+ role: "admin",
95
+ action: "write",
96
+ access: "all",
97
+ });
98
+ });
99
+ it("handles multiple resources", () => {
100
+ const permission = {
101
+ posts: {
102
+ user: { member: { read: "own" } },
103
+ },
104
+ comments: {
105
+ team: { admin: { delete: "all" } },
106
+ },
107
+ };
108
+ const result = (0, base_grants_1.flattenNestedGrants)(permission);
109
+ expect(result).toHaveLength(2);
110
+ expect(result).toContainEqual({
111
+ resource: "posts",
112
+ level: "user",
113
+ role: "member",
114
+ action: "read",
115
+ access: "own",
116
+ });
117
+ expect(result).toContainEqual({
118
+ resource: "comments",
119
+ level: "team",
120
+ role: "admin",
121
+ action: "delete",
122
+ access: "all",
123
+ });
124
+ });
125
+ it("handles permission with only some levels defined", () => {
126
+ const permission = {
127
+ posts: {
128
+ organization: {
129
+ owner: { delete: "all" },
130
+ },
131
+ },
132
+ };
133
+ const result = (0, base_grants_1.flattenNestedGrants)(permission);
134
+ expect(result).toHaveLength(1);
135
+ expect(result[0]).toEqual({
136
+ resource: "posts",
137
+ level: "organization",
138
+ role: "owner",
139
+ action: "delete",
140
+ access: "all",
141
+ });
142
+ });
143
+ it("returns empty array for empty permission", () => {
144
+ const result = (0, base_grants_1.flattenNestedGrants)({});
145
+ expect(result).toEqual([]);
146
+ });
147
+ it("handles multiple roles per level", () => {
148
+ const permission = {
149
+ posts: {
150
+ user: {
151
+ member: { read: "own" },
152
+ admin: { read: "all", write: "all" },
153
+ },
154
+ },
155
+ };
156
+ const result = (0, base_grants_1.flattenNestedGrants)(permission);
157
+ expect(result).toHaveLength(3);
158
+ expect(result).toContainEqual({
159
+ resource: "posts",
160
+ level: "user",
161
+ role: "member",
162
+ action: "read",
163
+ access: "own",
164
+ });
165
+ expect(result).toContainEqual({
166
+ resource: "posts",
167
+ level: "user",
168
+ role: "admin",
169
+ action: "read",
170
+ access: "all",
171
+ });
172
+ expect(result).toContainEqual({
173
+ resource: "posts",
174
+ level: "user",
175
+ role: "admin",
176
+ action: "write",
177
+ access: "all",
178
+ });
179
+ });
180
+ });
181
+ // ============================================
182
+ // checkPermissionSync
183
+ // ============================================
184
+ describe("checkPermissionSync", () => {
185
+ describe("edge cases", () => {
186
+ it("returns false for empty grants array", () => {
187
+ const ctx = createMockContext();
188
+ const result = (0, base_grants_1.checkPermissionSync)(ctx, []);
189
+ expect(result).toBe(false);
190
+ });
191
+ it("returns false for undefined grants", () => {
192
+ const ctx = createMockContext();
193
+ const result = (0, base_grants_1.checkPermissionSync)(ctx, undefined);
194
+ expect(result).toBe(false);
195
+ });
196
+ });
197
+ describe("user-level permissions", () => {
198
+ it("grants access with 'all' access regardless of entity", () => {
199
+ const ctx = createMockContext({ role: "member" });
200
+ const grants = [{ level: "user", role: "member", access: "all" }];
201
+ // No entity provided
202
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
203
+ // Entity with different userId - still allowed because "all" access
204
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants, { userId: "other-user" })).toBe(true);
205
+ });
206
+ it("grants access with 'own' access when userId matches", () => {
207
+ const ctx = createMockContext({ id: "user-123", role: "member" });
208
+ const grants = [{ level: "user", role: "member", access: "own" }];
209
+ const entity = createMockEntity({ userId: "user-123" });
210
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(true);
211
+ });
212
+ it("denies access with 'own' access when userId does not match", () => {
213
+ const ctx = createMockContext({ id: "user-123", role: "member" });
214
+ const grants = [{ level: "user", role: "member", access: "own" }];
215
+ const entity = createMockEntity({ userId: "other-user" });
216
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(false);
217
+ });
218
+ it("denies access with 'own' access when no entity provided", () => {
219
+ const ctx = createMockContext({ id: "user-123", role: "member" });
220
+ const grants = [{ level: "user", role: "member", access: "own" }];
221
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(false);
222
+ });
223
+ it("denies access when user role does not match grant role", () => {
224
+ const ctx = createMockContext({ role: "viewer" });
225
+ const grants = [{ level: "user", role: "admin", access: "all" }];
226
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(false);
227
+ });
228
+ });
229
+ describe("team-level permissions", () => {
230
+ it("grants access with 'all' access when team role matches", () => {
231
+ const ctx = createMockContext({}, { activeTeamId: "team-1", activeTeamRole: "admin" });
232
+ const grants = [{ level: "team", role: "admin", access: "all" }];
233
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
234
+ });
235
+ it("grants access with 'own' access when teamId matches", () => {
236
+ const ctx = createMockContext({}, { activeTeamId: "team-1", activeTeamRole: "member" });
237
+ const grants = [{ level: "team", role: "member", access: "own" }];
238
+ const entity = createMockEntity({ teamId: "team-1" });
239
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(true);
240
+ });
241
+ it("denies access with 'own' access when teamId does not match", () => {
242
+ const ctx = createMockContext({}, { activeTeamId: "team-1", activeTeamRole: "member" });
243
+ const grants = [{ level: "team", role: "member", access: "own" }];
244
+ const entity = createMockEntity({ teamId: "team-2" });
245
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(false);
246
+ });
247
+ it("denies access when team role does not match", () => {
248
+ const ctx = createMockContext({}, { activeTeamId: "team-1", activeTeamRole: "viewer" });
249
+ const grants = [{ level: "team", role: "admin", access: "all" }];
250
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(false);
251
+ });
252
+ it("denies access when no active team", () => {
253
+ const ctx = createMockContext({}, { activeTeamId: null, activeTeamRole: null });
254
+ const grants = [{ level: "team", role: "admin", access: "all" }];
255
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(false);
256
+ });
257
+ });
258
+ describe("organization-level permissions", () => {
259
+ it("grants access with 'all' access when organization role matches", () => {
260
+ const ctx = createMockContext({}, { activeOrganizationId: "org-1", activeOrganizationRole: "owner" });
261
+ const grants = [
262
+ { level: "organization", role: "owner", access: "all" },
263
+ ];
264
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
265
+ });
266
+ it("grants access with 'own' access when organizationId matches", () => {
267
+ const ctx = createMockContext({}, { activeOrganizationId: "org-1", activeOrganizationRole: "member" });
268
+ const grants = [
269
+ { level: "organization", role: "member", access: "own" },
270
+ ];
271
+ const entity = createMockEntity({ organizationId: "org-1" });
272
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(true);
273
+ });
274
+ it("denies access with 'own' access when organizationId does not match", () => {
275
+ const ctx = createMockContext({}, { activeOrganizationId: "org-1", activeOrganizationRole: "member" });
276
+ const grants = [
277
+ { level: "organization", role: "member", access: "own" },
278
+ ];
279
+ const entity = createMockEntity({ organizationId: "org-2" });
280
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(false);
281
+ });
282
+ });
283
+ describe("multiple grants", () => {
284
+ it("checks 'all' access before 'own' access (optimization)", () => {
285
+ const ctx = createMockContext({ id: "user-123", role: "admin" });
286
+ const grants = [
287
+ { level: "user", role: "member", access: "own" }, // Would need entity check
288
+ { level: "user", role: "admin", access: "all" }, // Should match first in pass 1
289
+ ];
290
+ // No entity provided, but should still pass because "all" is checked first
291
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
292
+ });
293
+ it("falls back to 'own' access if no 'all' access matches", () => {
294
+ const ctx = createMockContext({ id: "user-123", role: "member" });
295
+ const grants = [
296
+ { level: "user", role: "admin", access: "all" }, // Role doesn't match
297
+ { level: "user", role: "member", access: "own" }, // Should match in pass 2
298
+ ];
299
+ const entity = createMockEntity({ userId: "user-123" });
300
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(true);
301
+ });
302
+ it("grants access if any level matches with 'all'", () => {
303
+ const ctx = createMockContext({ role: "viewer" }, { activeTeamId: "team-1", activeTeamRole: "admin" });
304
+ const grants = [
305
+ { level: "user", role: "member", access: "all" }, // User role doesn't match
306
+ { level: "team", role: "admin", access: "all" }, // Team role matches
307
+ ];
308
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
309
+ });
310
+ });
311
+ describe("multiple entities", () => {
312
+ it("requires all entities to match for 'own' access", () => {
313
+ const ctx = createMockContext({ id: "user-123", role: "member" });
314
+ const grants = [{ level: "user", role: "member", access: "own" }];
315
+ const matchingEntities = [{ userId: "user-123" }, { userId: "user-123" }];
316
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants, matchingEntities)).toBe(true);
317
+ const mixedEntities = [{ userId: "user-123" }, { userId: "other-user" }];
318
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants, mixedEntities)).toBe(false);
319
+ });
320
+ it("denies access if any entity does not match", () => {
321
+ const ctx = createMockContext({}, { activeTeamId: "team-1", activeTeamRole: "member" });
322
+ const grants = [{ level: "team", role: "member", access: "own" }];
323
+ const entities = [{ teamId: "team-1" }, { teamId: "team-1" }, { teamId: "team-2" }];
324
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entities)).toBe(false);
325
+ });
326
+ });
327
+ describe("level priority (user -> team -> organization)", () => {
328
+ it("checks user level before team level", () => {
329
+ const ctx = createMockContext({ id: "user-123", role: "member" }, { activeTeamId: "team-1", activeTeamRole: "member" });
330
+ // Both levels have matching grants, but user should be checked first
331
+ const grants = [
332
+ { level: "team", role: "member", access: "own" },
333
+ { level: "user", role: "member", access: "all" },
334
+ ];
335
+ // Should return true from user-level "all" without needing entity
336
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
337
+ });
338
+ it("checks team level before organization level", () => {
339
+ const ctx = createMockContext({ role: "viewer" }, {
340
+ activeTeamId: "team-1",
341
+ activeTeamRole: "admin",
342
+ activeOrganizationId: "org-1",
343
+ activeOrganizationRole: "admin",
344
+ });
345
+ const grants = [
346
+ { level: "organization", role: "admin", access: "all" },
347
+ { level: "team", role: "admin", access: "all" },
348
+ ];
349
+ // Both would match, but team is checked before org in the priority
350
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
351
+ });
352
+ });
353
+ describe("multi-level grants with different roles per level", () => {
354
+ it("user with different roles at each level - matches user level", () => {
355
+ const ctx = createMockContext({ id: "user-123", role: "viewer" }, {
356
+ activeTeamId: "team-1",
357
+ activeTeamRole: "member",
358
+ activeOrganizationId: "org-1",
359
+ activeOrganizationRole: "admin",
360
+ });
361
+ // Grant requires "viewer" at user level
362
+ const grants = [{ level: "user", role: "viewer", access: "all" }];
363
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
364
+ });
365
+ it("user with different roles at each level - matches team level only", () => {
366
+ const ctx = createMockContext({ id: "user-123", role: "viewer" }, {
367
+ activeTeamId: "team-1",
368
+ activeTeamRole: "manager",
369
+ activeOrganizationId: "org-1",
370
+ activeOrganizationRole: "member",
371
+ });
372
+ // Grant requires "admin" at user level (no match) or "manager" at team level (match)
373
+ const grants = [
374
+ { level: "user", role: "admin", access: "all" },
375
+ { level: "team", role: "manager", access: "all" },
376
+ ];
377
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
378
+ });
379
+ it("user with different roles at each level - matches organization level only", () => {
380
+ const ctx = createMockContext({ id: "user-123", role: "viewer" }, {
381
+ activeTeamId: "team-1",
382
+ activeTeamRole: "member",
383
+ activeOrganizationId: "org-1",
384
+ activeOrganizationRole: "owner",
385
+ });
386
+ // Grant requires roles that only match at organization level
387
+ const grants = [
388
+ { level: "user", role: "admin", access: "all" },
389
+ { level: "team", role: "admin", access: "all" },
390
+ { level: "organization", role: "owner", access: "all" },
391
+ ];
392
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
393
+ });
394
+ it("user with different roles at each level - no level matches", () => {
395
+ const ctx = createMockContext({ id: "user-123", role: "viewer" }, {
396
+ activeTeamId: "team-1",
397
+ activeTeamRole: "member",
398
+ activeOrganizationId: "org-1",
399
+ activeOrganizationRole: "member",
400
+ });
401
+ // Grant requires roles that don't match any level
402
+ const grants = [
403
+ { level: "user", role: "admin", access: "all" },
404
+ { level: "team", role: "admin", access: "all" },
405
+ { level: "organization", role: "owner", access: "all" },
406
+ ];
407
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(false);
408
+ });
409
+ it("user with mixed 'all' and 'own' grants across levels - 'all' wins", () => {
410
+ const ctx = createMockContext({ id: "user-123", role: "member" }, {
411
+ activeTeamId: "team-1",
412
+ activeTeamRole: "admin",
413
+ activeOrganizationId: "org-1",
414
+ activeOrganizationRole: "member",
415
+ });
416
+ // User level has "own" (would need entity), team level has "all" (no entity needed)
417
+ const grants = [
418
+ { level: "user", role: "member", access: "own" },
419
+ { level: "team", role: "admin", access: "all" },
420
+ { level: "organization", role: "owner", access: "own" },
421
+ ];
422
+ // Should pass because team-level "all" is checked in pass 1
423
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
424
+ });
425
+ it("user with 'own' grants at multiple levels - first matching level wins", () => {
426
+ const ctx = createMockContext({ id: "user-123", role: "member" }, {
427
+ activeTeamId: "team-1",
428
+ activeTeamRole: "member",
429
+ activeOrganizationId: "org-1",
430
+ activeOrganizationRole: "member",
431
+ });
432
+ // All levels have "own" access, entity matches user level
433
+ const grants = [
434
+ { level: "user", role: "member", access: "own" },
435
+ { level: "team", role: "member", access: "own" },
436
+ { level: "organization", role: "member", access: "own" },
437
+ ];
438
+ const entity = createMockEntity({
439
+ userId: "user-123",
440
+ teamId: "team-2",
441
+ organizationId: "org-2",
442
+ });
443
+ // Should pass because user-level "own" matches (checked first in pass 2)
444
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(true);
445
+ });
446
+ it("user with 'own' grants - team level matches when user level does not", () => {
447
+ const ctx = createMockContext({ id: "user-123", role: "member" }, {
448
+ activeTeamId: "team-1",
449
+ activeTeamRole: "member",
450
+ activeOrganizationId: "org-1",
451
+ activeOrganizationRole: "member",
452
+ });
453
+ const grants = [
454
+ { level: "user", role: "member", access: "own" },
455
+ { level: "team", role: "member", access: "own" },
456
+ { level: "organization", role: "member", access: "own" },
457
+ ];
458
+ // Entity belongs to a different user but same team
459
+ const entity = createMockEntity({
460
+ userId: "other-user",
461
+ teamId: "team-1",
462
+ organizationId: "org-2",
463
+ });
464
+ // User-level fails (userId mismatch), team-level passes (teamId matches)
465
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(true);
466
+ });
467
+ it("user with 'own' grants - organization level matches when user and team do not", () => {
468
+ const ctx = createMockContext({ id: "user-123", role: "member" }, {
469
+ activeTeamId: "team-1",
470
+ activeTeamRole: "member",
471
+ activeOrganizationId: "org-1",
472
+ activeOrganizationRole: "member",
473
+ });
474
+ const grants = [
475
+ { level: "user", role: "member", access: "own" },
476
+ { level: "team", role: "member", access: "own" },
477
+ { level: "organization", role: "member", access: "own" },
478
+ ];
479
+ // Entity belongs to a different user and team, but same organization
480
+ const entity = createMockEntity({
481
+ userId: "other-user",
482
+ teamId: "team-2",
483
+ organizationId: "org-1",
484
+ });
485
+ // User-level fails, team-level fails, organization-level passes
486
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(true);
487
+ });
488
+ it("complex scenario: admin user bypasses ownership checks", () => {
489
+ const ctx = createMockContext({ id: "admin-user", role: "admin" }, {
490
+ activeTeamId: "team-1",
491
+ activeTeamRole: "member",
492
+ activeOrganizationId: "org-1",
493
+ activeOrganizationRole: "member",
494
+ });
495
+ // Grant allows admins to access all, or regular members to access own
496
+ const grants = [
497
+ { level: "user", role: "admin", access: "all" },
498
+ { level: "user", role: "member", access: "own" },
499
+ ];
500
+ // Entity belongs to someone else, but admin has "all" access
501
+ const entity = createMockEntity({ userId: "other-user" });
502
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(true);
503
+ });
504
+ it("complex scenario: regular user limited to own resources", () => {
505
+ const ctx = createMockContext({ id: "user-123", role: "member" }, {
506
+ activeTeamId: "team-1",
507
+ activeTeamRole: "member",
508
+ activeOrganizationId: "org-1",
509
+ activeOrganizationRole: "member",
510
+ });
511
+ // Grant allows admins to access all, or regular members to access own
512
+ const grants = [
513
+ { level: "user", role: "admin", access: "all" },
514
+ { level: "user", role: "member", access: "own" },
515
+ ];
516
+ // Entity belongs to someone else - member can't access
517
+ const otherEntity = createMockEntity({ userId: "other-user" });
518
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants, otherEntity)).toBe(false);
519
+ // Entity belongs to the user - member can access
520
+ const ownEntity = createMockEntity({ userId: "user-123" });
521
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants, ownEntity)).toBe(true);
522
+ });
523
+ it("team admin can access all team resources regardless of user ownership", () => {
524
+ const ctx = createMockContext({ id: "user-123", role: "member" }, {
525
+ activeTeamId: "team-1",
526
+ activeTeamRole: "admin",
527
+ activeOrganizationId: "org-1",
528
+ activeOrganizationRole: "member",
529
+ });
530
+ // Grant: user-level own OR team-level all for admins
531
+ const grants = [
532
+ { level: "user", role: "member", access: "own" },
533
+ { level: "team", role: "admin", access: "all" },
534
+ ];
535
+ // Entity belongs to another user in the same team
536
+ const entity = createMockEntity({ userId: "other-user", teamId: "team-1" });
537
+ // Team admin has "all" access, so ownership doesn't matter
538
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(true);
539
+ });
540
+ it("organization owner can access all organization resources", () => {
541
+ const ctx = createMockContext({ id: "user-123", role: "member" }, {
542
+ activeTeamId: "team-1",
543
+ activeTeamRole: "member",
544
+ activeOrganizationId: "org-1",
545
+ activeOrganizationRole: "owner",
546
+ });
547
+ // Grant: owner at org level has all access
548
+ const grants = [
549
+ { level: "user", role: "member", access: "own" },
550
+ { level: "team", role: "admin", access: "own" },
551
+ { level: "organization", role: "owner", access: "all" },
552
+ ];
553
+ // Entity belongs to another user and team, but in the same org
554
+ const entity = createMockEntity({
555
+ userId: "other-user",
556
+ teamId: "team-2",
557
+ organizationId: "org-1",
558
+ });
559
+ // Org owner has "all" access
560
+ expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(true);
561
+ });
562
+ });
563
+ });
564
+ // ============================================
565
+ // checkPermissionAsync
566
+ // ============================================
567
+ describe("checkPermissionAsync", () => {
568
+ describe("edge cases", () => {
569
+ it("returns ok(false) for empty grants array", async () => {
570
+ const ctx = createMockContext();
571
+ const getEntities = jest.fn();
572
+ const result = await (0, base_grants_1.checkPermissionAsync)(ctx, [], getEntities);
573
+ expect(result.isOk()).toBe(true);
574
+ if (result.isOk())
575
+ expect(result.value).toBe(false);
576
+ expect(getEntities).not.toHaveBeenCalled();
577
+ });
578
+ });
579
+ describe("'all' access optimization", () => {
580
+ it("returns ok(true) without calling getEntities when 'all' access matches", async () => {
581
+ const ctx = createMockContext({ role: "admin" });
582
+ const grants = [{ level: "user", role: "admin", access: "all" }];
583
+ const getEntities = jest.fn();
584
+ const result = await (0, base_grants_1.checkPermissionAsync)(ctx, grants, getEntities);
585
+ expect(result.isOk()).toBe(true);
586
+ if (result.isOk())
587
+ expect(result.value).toBe(true);
588
+ expect(getEntities).not.toHaveBeenCalled();
589
+ });
590
+ });
591
+ describe("'own' access with entity fetch", () => {
592
+ it("calls getEntities and grants access when ownership matches", async () => {
593
+ const ctx = createMockContext({ id: "user-123", role: "member" });
594
+ const grants = [{ level: "user", role: "member", access: "own" }];
595
+ const entity = createMockEntity({ userId: "user-123" });
596
+ const getEntities = jest.fn().mockResolvedValue((0, neverthrow_1.ok)(entity));
597
+ const result = await (0, base_grants_1.checkPermissionAsync)(ctx, grants, getEntities);
598
+ expect(result.isOk()).toBe(true);
599
+ if (result.isOk())
600
+ expect(result.value).toBe(true);
601
+ expect(getEntities).toHaveBeenCalledTimes(1);
602
+ });
603
+ it("calls getEntities and denies access when ownership does not match", async () => {
604
+ const ctx = createMockContext({ id: "user-123", role: "member" });
605
+ const grants = [{ level: "user", role: "member", access: "own" }];
606
+ const entity = createMockEntity({ userId: "other-user" });
607
+ const getEntities = jest.fn().mockResolvedValue((0, neverthrow_1.ok)(entity));
608
+ const result = await (0, base_grants_1.checkPermissionAsync)(ctx, grants, getEntities);
609
+ expect(result.isOk()).toBe(true);
610
+ if (result.isOk())
611
+ expect(result.value).toBe(false);
612
+ expect(getEntities).toHaveBeenCalledTimes(1);
613
+ });
614
+ it("handles undefined entities from getEntities", async () => {
615
+ const ctx = createMockContext({ id: "user-123", role: "member" });
616
+ const grants = [{ level: "user", role: "member", access: "own" }];
617
+ const getEntities = jest.fn().mockResolvedValue((0, neverthrow_1.ok)(undefined));
618
+ const result = await (0, base_grants_1.checkPermissionAsync)(ctx, grants, getEntities);
619
+ expect(result.isOk()).toBe(true);
620
+ if (result.isOk())
621
+ expect(result.value).toBe(false);
622
+ });
623
+ });
624
+ describe("error propagation", () => {
625
+ it("propagates errors from getEntities", async () => {
626
+ const ctx = createMockContext({ id: "user-123", role: "member" });
627
+ const grants = [{ level: "user", role: "member", access: "own" }];
628
+ const mockError = new errors_1.ServerError({
629
+ layer: "service",
630
+ layerName: "BasePermissionService",
631
+ code: "NOT_FOUND",
632
+ message: "Entity not found",
633
+ cause: null,
634
+ });
635
+ const getEntities = jest.fn().mockResolvedValue((0, neverthrow_1.err)(mockError));
636
+ const result = await (0, base_grants_1.checkPermissionAsync)(ctx, grants, getEntities);
637
+ expect(result.isErr()).toBe(true);
638
+ if (result.isErr()) {
639
+ expect(result.error.code).toBe("NOT_FOUND");
640
+ expect(result.error.message).toBe("Entity not found");
641
+ }
642
+ });
643
+ });
644
+ describe("team and organization levels", () => {
645
+ it("checks team-level 'own' access correctly", async () => {
646
+ const ctx = createMockContext({}, { activeTeamId: "team-1", activeTeamRole: "member" });
647
+ const grants = [{ level: "team", role: "member", access: "own" }];
648
+ const entity = createMockEntity({ teamId: "team-1" });
649
+ const getEntities = jest.fn().mockResolvedValue((0, neverthrow_1.ok)(entity));
650
+ const result = await (0, base_grants_1.checkPermissionAsync)(ctx, grants, getEntities);
651
+ expect(result.isOk()).toBe(true);
652
+ if (result.isOk())
653
+ expect(result.value).toBe(true);
654
+ });
655
+ it("checks organization-level 'all' access without fetching entities", async () => {
656
+ const ctx = createMockContext({}, { activeOrganizationId: "org-1", activeOrganizationRole: "owner" });
657
+ const grants = [
658
+ { level: "organization", role: "owner", access: "all" },
659
+ ];
660
+ const getEntities = jest.fn();
661
+ const result = await (0, base_grants_1.checkPermissionAsync)(ctx, grants, getEntities);
662
+ expect(result.isOk()).toBe(true);
663
+ if (result.isOk())
664
+ expect(result.value).toBe(true);
665
+ expect(getEntities).not.toHaveBeenCalled();
666
+ });
667
+ });
668
+ });