convex-cms 0.0.1

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 (379) hide show
  1. package/dist/cli/commands/admin.d.ts +16 -0
  2. package/dist/cli/commands/admin.d.ts.map +1 -0
  3. package/dist/cli/commands/admin.js +88 -0
  4. package/dist/cli/commands/admin.js.map +1 -0
  5. package/dist/cli/index.d.ts +3 -0
  6. package/dist/cli/index.d.ts.map +1 -0
  7. package/dist/cli/index.js +18 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/cli/utils/detectConvexUrl.d.ts +13 -0
  10. package/dist/cli/utils/detectConvexUrl.d.ts.map +1 -0
  11. package/dist/cli/utils/detectConvexUrl.js +48 -0
  12. package/dist/cli/utils/detectConvexUrl.js.map +1 -0
  13. package/dist/cli/utils/openBrowser.d.ts +7 -0
  14. package/dist/cli/utils/openBrowser.d.ts.map +1 -0
  15. package/dist/cli/utils/openBrowser.js +17 -0
  16. package/dist/cli/utils/openBrowser.js.map +1 -0
  17. package/dist/client/admin-config.d.ts +126 -0
  18. package/dist/client/admin-config.d.ts.map +1 -0
  19. package/dist/client/admin-config.js +117 -0
  20. package/dist/client/admin-config.js.map +1 -0
  21. package/dist/client/adminApi.d.ts +2273 -0
  22. package/dist/client/adminApi.d.ts.map +1 -0
  23. package/dist/client/adminApi.js +716 -0
  24. package/dist/client/adminApi.js.map +1 -0
  25. package/dist/client/agentTools.d.ts +933 -0
  26. package/dist/client/agentTools.d.ts.map +1 -0
  27. package/dist/client/agentTools.js +1004 -0
  28. package/dist/client/agentTools.js.map +1 -0
  29. package/dist/client/argTypes.d.ts +212 -0
  30. package/dist/client/argTypes.d.ts.map +1 -0
  31. package/dist/client/argTypes.js +5 -0
  32. package/dist/client/argTypes.js.map +1 -0
  33. package/dist/client/field-types.d.ts +55 -0
  34. package/dist/client/field-types.d.ts.map +1 -0
  35. package/dist/client/field-types.js +152 -0
  36. package/dist/client/field-types.js.map +1 -0
  37. package/dist/client/index.d.ts +189 -0
  38. package/dist/client/index.d.ts.map +1 -0
  39. package/dist/client/index.js +668 -0
  40. package/dist/client/index.js.map +1 -0
  41. package/dist/client/queryBuilder.d.ts +765 -0
  42. package/dist/client/queryBuilder.d.ts.map +1 -0
  43. package/dist/client/queryBuilder.js +970 -0
  44. package/dist/client/queryBuilder.js.map +1 -0
  45. package/dist/client/schema/codegen.d.ts +128 -0
  46. package/dist/client/schema/codegen.d.ts.map +1 -0
  47. package/dist/client/schema/codegen.js +318 -0
  48. package/dist/client/schema/codegen.js.map +1 -0
  49. package/dist/client/schema/defineContentType.d.ts +221 -0
  50. package/dist/client/schema/defineContentType.d.ts.map +1 -0
  51. package/dist/client/schema/defineContentType.js +380 -0
  52. package/dist/client/schema/defineContentType.js.map +1 -0
  53. package/dist/client/schema/index.d.ts +85 -0
  54. package/dist/client/schema/index.d.ts.map +1 -0
  55. package/dist/client/schema/index.js +92 -0
  56. package/dist/client/schema/index.js.map +1 -0
  57. package/dist/client/schema/schemaDrift.d.ts +199 -0
  58. package/dist/client/schema/schemaDrift.d.ts.map +1 -0
  59. package/dist/client/schema/schemaDrift.js +340 -0
  60. package/dist/client/schema/schemaDrift.js.map +1 -0
  61. package/dist/client/schema/typedClient.d.ts +401 -0
  62. package/dist/client/schema/typedClient.d.ts.map +1 -0
  63. package/dist/client/schema/typedClient.js +269 -0
  64. package/dist/client/schema/typedClient.js.map +1 -0
  65. package/dist/client/schema/types.d.ts +477 -0
  66. package/dist/client/schema/types.d.ts.map +1 -0
  67. package/dist/client/schema/types.js +39 -0
  68. package/dist/client/schema/types.js.map +1 -0
  69. package/dist/client/types.d.ts +449 -0
  70. package/dist/client/types.d.ts.map +1 -0
  71. package/dist/client/types.js +149 -0
  72. package/dist/client/types.js.map +1 -0
  73. package/dist/client/workflows.d.ts +51 -0
  74. package/dist/client/workflows.d.ts.map +1 -0
  75. package/dist/client/workflows.js +103 -0
  76. package/dist/client/workflows.js.map +1 -0
  77. package/dist/client/wrapper.d.ts +2198 -0
  78. package/dist/client/wrapper.d.ts.map +1 -0
  79. package/dist/client/wrapper.js +2651 -0
  80. package/dist/client/wrapper.js.map +1 -0
  81. package/dist/component/_generated/api.d.ts +124 -0
  82. package/dist/component/_generated/api.d.ts.map +1 -0
  83. package/dist/component/_generated/api.js +31 -0
  84. package/dist/component/_generated/api.js.map +1 -0
  85. package/dist/component/_generated/component.d.ts +4321 -0
  86. package/dist/component/_generated/component.d.ts.map +1 -0
  87. package/dist/component/_generated/component.js +11 -0
  88. package/dist/component/_generated/component.js.map +1 -0
  89. package/dist/component/_generated/dataModel.d.ts +46 -0
  90. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  91. package/dist/component/_generated/dataModel.js +11 -0
  92. package/dist/component/_generated/dataModel.js.map +1 -0
  93. package/dist/component/_generated/server.d.ts +121 -0
  94. package/dist/component/_generated/server.d.ts.map +1 -0
  95. package/dist/component/_generated/server.js +78 -0
  96. package/dist/component/_generated/server.js.map +1 -0
  97. package/dist/component/auditLog.d.ts +410 -0
  98. package/dist/component/auditLog.d.ts.map +1 -0
  99. package/dist/component/auditLog.js +607 -0
  100. package/dist/component/auditLog.js.map +1 -0
  101. package/dist/component/authorization.d.ts +323 -0
  102. package/dist/component/authorization.d.ts.map +1 -0
  103. package/dist/component/authorization.js +464 -0
  104. package/dist/component/authorization.js.map +1 -0
  105. package/dist/component/authorizationHooks.d.ts +184 -0
  106. package/dist/component/authorizationHooks.d.ts.map +1 -0
  107. package/dist/component/authorizationHooks.js +521 -0
  108. package/dist/component/authorizationHooks.js.map +1 -0
  109. package/dist/component/bulkOperations.d.ts +200 -0
  110. package/dist/component/bulkOperations.d.ts.map +1 -0
  111. package/dist/component/bulkOperations.js +568 -0
  112. package/dist/component/bulkOperations.js.map +1 -0
  113. package/dist/component/contentEntries.d.ts +719 -0
  114. package/dist/component/contentEntries.d.ts.map +1 -0
  115. package/dist/component/contentEntries.js +1617 -0
  116. package/dist/component/contentEntries.js.map +1 -0
  117. package/dist/component/contentEntryMutations.d.ts +505 -0
  118. package/dist/component/contentEntryMutations.d.ts.map +1 -0
  119. package/dist/component/contentEntryMutations.js +1009 -0
  120. package/dist/component/contentEntryMutations.js.map +1 -0
  121. package/dist/component/contentEntryValidation.d.ts +115 -0
  122. package/dist/component/contentEntryValidation.d.ts.map +1 -0
  123. package/dist/component/contentEntryValidation.js +546 -0
  124. package/dist/component/contentEntryValidation.js.map +1 -0
  125. package/dist/component/contentLock.d.ts +328 -0
  126. package/dist/component/contentLock.d.ts.map +1 -0
  127. package/dist/component/contentLock.js +471 -0
  128. package/dist/component/contentLock.js.map +1 -0
  129. package/dist/component/contentTypeMigration.d.ts +411 -0
  130. package/dist/component/contentTypeMigration.d.ts.map +1 -0
  131. package/dist/component/contentTypeMigration.js +805 -0
  132. package/dist/component/contentTypeMigration.js.map +1 -0
  133. package/dist/component/contentTypeMutations.d.ts +975 -0
  134. package/dist/component/contentTypeMutations.d.ts.map +1 -0
  135. package/dist/component/contentTypeMutations.js +768 -0
  136. package/dist/component/contentTypeMutations.js.map +1 -0
  137. package/dist/component/contentTypes.d.ts +538 -0
  138. package/dist/component/contentTypes.d.ts.map +1 -0
  139. package/dist/component/contentTypes.js +304 -0
  140. package/dist/component/contentTypes.js.map +1 -0
  141. package/dist/component/convex.config.d.ts +42 -0
  142. package/dist/component/convex.config.d.ts.map +1 -0
  143. package/dist/component/convex.config.js +43 -0
  144. package/dist/component/convex.config.js.map +1 -0
  145. package/dist/component/documentTypes.d.ts +186 -0
  146. package/dist/component/documentTypes.d.ts.map +1 -0
  147. package/dist/component/documentTypes.js +23 -0
  148. package/dist/component/documentTypes.js.map +1 -0
  149. package/dist/component/eventEmitter.d.ts +281 -0
  150. package/dist/component/eventEmitter.d.ts.map +1 -0
  151. package/dist/component/eventEmitter.js +300 -0
  152. package/dist/component/eventEmitter.js.map +1 -0
  153. package/dist/component/exportImport.d.ts +1120 -0
  154. package/dist/component/exportImport.d.ts.map +1 -0
  155. package/dist/component/exportImport.js +931 -0
  156. package/dist/component/exportImport.js.map +1 -0
  157. package/dist/component/index.d.ts +28 -0
  158. package/dist/component/index.d.ts.map +1 -0
  159. package/dist/component/index.js +142 -0
  160. package/dist/component/index.js.map +1 -0
  161. package/dist/component/lib/deepReferenceResolver.d.ts +252 -0
  162. package/dist/component/lib/deepReferenceResolver.d.ts.map +1 -0
  163. package/dist/component/lib/deepReferenceResolver.js +601 -0
  164. package/dist/component/lib/deepReferenceResolver.js.map +1 -0
  165. package/dist/component/lib/errors.d.ts +306 -0
  166. package/dist/component/lib/errors.d.ts.map +1 -0
  167. package/dist/component/lib/errors.js +407 -0
  168. package/dist/component/lib/errors.js.map +1 -0
  169. package/dist/component/lib/index.d.ts +10 -0
  170. package/dist/component/lib/index.d.ts.map +1 -0
  171. package/dist/component/lib/index.js +33 -0
  172. package/dist/component/lib/index.js.map +1 -0
  173. package/dist/component/lib/mediaReferenceResolver.d.ts +217 -0
  174. package/dist/component/lib/mediaReferenceResolver.d.ts.map +1 -0
  175. package/dist/component/lib/mediaReferenceResolver.js +326 -0
  176. package/dist/component/lib/mediaReferenceResolver.js.map +1 -0
  177. package/dist/component/lib/metadataExtractor.d.ts +245 -0
  178. package/dist/component/lib/metadataExtractor.d.ts.map +1 -0
  179. package/dist/component/lib/metadataExtractor.js +548 -0
  180. package/dist/component/lib/metadataExtractor.js.map +1 -0
  181. package/dist/component/lib/mutationAuth.d.ts +95 -0
  182. package/dist/component/lib/mutationAuth.d.ts.map +1 -0
  183. package/dist/component/lib/mutationAuth.js +146 -0
  184. package/dist/component/lib/mutationAuth.js.map +1 -0
  185. package/dist/component/lib/queries.d.ts +17 -0
  186. package/dist/component/lib/queries.d.ts.map +1 -0
  187. package/dist/component/lib/queries.js +49 -0
  188. package/dist/component/lib/queries.js.map +1 -0
  189. package/dist/component/lib/ragContentChunker.d.ts +423 -0
  190. package/dist/component/lib/ragContentChunker.d.ts.map +1 -0
  191. package/dist/component/lib/ragContentChunker.js +897 -0
  192. package/dist/component/lib/ragContentChunker.js.map +1 -0
  193. package/dist/component/lib/referenceResolver.d.ts +175 -0
  194. package/dist/component/lib/referenceResolver.d.ts.map +1 -0
  195. package/dist/component/lib/referenceResolver.js +293 -0
  196. package/dist/component/lib/referenceResolver.js.map +1 -0
  197. package/dist/component/lib/slugGenerator.d.ts +71 -0
  198. package/dist/component/lib/slugGenerator.d.ts.map +1 -0
  199. package/dist/component/lib/slugGenerator.js +207 -0
  200. package/dist/component/lib/slugGenerator.js.map +1 -0
  201. package/dist/component/lib/slugUniqueness.d.ts +131 -0
  202. package/dist/component/lib/slugUniqueness.d.ts.map +1 -0
  203. package/dist/component/lib/slugUniqueness.js +229 -0
  204. package/dist/component/lib/slugUniqueness.js.map +1 -0
  205. package/dist/component/lib/softDelete.d.ts +18 -0
  206. package/dist/component/lib/softDelete.d.ts.map +1 -0
  207. package/dist/component/lib/softDelete.js +29 -0
  208. package/dist/component/lib/softDelete.js.map +1 -0
  209. package/dist/component/localeFallbackChain.d.ts +410 -0
  210. package/dist/component/localeFallbackChain.d.ts.map +1 -0
  211. package/dist/component/localeFallbackChain.js +467 -0
  212. package/dist/component/localeFallbackChain.js.map +1 -0
  213. package/dist/component/localeFields.d.ts +508 -0
  214. package/dist/component/localeFields.d.ts.map +1 -0
  215. package/dist/component/localeFields.js +592 -0
  216. package/dist/component/localeFields.js.map +1 -0
  217. package/dist/component/mediaAssetMutations.d.ts +235 -0
  218. package/dist/component/mediaAssetMutations.d.ts.map +1 -0
  219. package/dist/component/mediaAssetMutations.js +558 -0
  220. package/dist/component/mediaAssetMutations.js.map +1 -0
  221. package/dist/component/mediaAssets.d.ts +168 -0
  222. package/dist/component/mediaAssets.d.ts.map +1 -0
  223. package/dist/component/mediaAssets.js +618 -0
  224. package/dist/component/mediaAssets.js.map +1 -0
  225. package/dist/component/mediaFolderMutations.d.ts +642 -0
  226. package/dist/component/mediaFolderMutations.d.ts.map +1 -0
  227. package/dist/component/mediaFolderMutations.js +849 -0
  228. package/dist/component/mediaFolderMutations.js.map +1 -0
  229. package/dist/component/mediaUploadMutations.d.ts +136 -0
  230. package/dist/component/mediaUploadMutations.d.ts.map +1 -0
  231. package/dist/component/mediaUploadMutations.js +205 -0
  232. package/dist/component/mediaUploadMutations.js.map +1 -0
  233. package/dist/component/mediaVariantMutations.d.ts +468 -0
  234. package/dist/component/mediaVariantMutations.d.ts.map +1 -0
  235. package/dist/component/mediaVariantMutations.js +737 -0
  236. package/dist/component/mediaVariantMutations.js.map +1 -0
  237. package/dist/component/mediaVariants.d.ts +525 -0
  238. package/dist/component/mediaVariants.d.ts.map +1 -0
  239. package/dist/component/mediaVariants.js +661 -0
  240. package/dist/component/mediaVariants.js.map +1 -0
  241. package/dist/component/ragContentIndexer.d.ts +595 -0
  242. package/dist/component/ragContentIndexer.d.ts.map +1 -0
  243. package/dist/component/ragContentIndexer.js +794 -0
  244. package/dist/component/ragContentIndexer.js.map +1 -0
  245. package/dist/component/rateLimitHooks.d.ts +266 -0
  246. package/dist/component/rateLimitHooks.d.ts.map +1 -0
  247. package/dist/component/rateLimitHooks.js +412 -0
  248. package/dist/component/rateLimitHooks.js.map +1 -0
  249. package/dist/component/roles.d.ts +649 -0
  250. package/dist/component/roles.d.ts.map +1 -0
  251. package/dist/component/roles.js +884 -0
  252. package/dist/component/roles.js.map +1 -0
  253. package/dist/component/scheduledPublish.d.ts +182 -0
  254. package/dist/component/scheduledPublish.d.ts.map +1 -0
  255. package/dist/component/scheduledPublish.js +304 -0
  256. package/dist/component/scheduledPublish.js.map +1 -0
  257. package/dist/component/schema.d.ts +4114 -0
  258. package/dist/component/schema.d.ts.map +1 -0
  259. package/dist/component/schema.js +469 -0
  260. package/dist/component/schema.js.map +1 -0
  261. package/dist/component/taxonomies.d.ts +476 -0
  262. package/dist/component/taxonomies.d.ts.map +1 -0
  263. package/dist/component/taxonomies.js +785 -0
  264. package/dist/component/taxonomies.js.map +1 -0
  265. package/dist/component/taxonomyMutations.d.ts +206 -0
  266. package/dist/component/taxonomyMutations.d.ts.map +1 -0
  267. package/dist/component/taxonomyMutations.js +1001 -0
  268. package/dist/component/taxonomyMutations.js.map +1 -0
  269. package/dist/component/trash.d.ts +265 -0
  270. package/dist/component/trash.d.ts.map +1 -0
  271. package/dist/component/trash.js +621 -0
  272. package/dist/component/trash.js.map +1 -0
  273. package/dist/component/types.d.ts +4 -0
  274. package/dist/component/types.d.ts.map +1 -0
  275. package/dist/component/types.js +2 -0
  276. package/dist/component/types.js.map +1 -0
  277. package/dist/component/userContext.d.ts +508 -0
  278. package/dist/component/userContext.d.ts.map +1 -0
  279. package/dist/component/userContext.js +615 -0
  280. package/dist/component/userContext.js.map +1 -0
  281. package/dist/component/validation.d.ts +387 -0
  282. package/dist/component/validation.d.ts.map +1 -0
  283. package/dist/component/validation.js +1052 -0
  284. package/dist/component/validation.js.map +1 -0
  285. package/dist/component/validators.d.ts +4645 -0
  286. package/dist/component/validators.d.ts.map +1 -0
  287. package/dist/component/validators.js +641 -0
  288. package/dist/component/validators.js.map +1 -0
  289. package/dist/component/versionMutations.d.ts +216 -0
  290. package/dist/component/versionMutations.d.ts.map +1 -0
  291. package/dist/component/versionMutations.js +321 -0
  292. package/dist/component/versionMutations.js.map +1 -0
  293. package/dist/component/webhookTrigger.d.ts +770 -0
  294. package/dist/component/webhookTrigger.d.ts.map +1 -0
  295. package/dist/component/webhookTrigger.js +1413 -0
  296. package/dist/component/webhookTrigger.js.map +1 -0
  297. package/dist/react/index.d.ts +316 -0
  298. package/dist/react/index.d.ts.map +1 -0
  299. package/dist/react/index.js +558 -0
  300. package/dist/react/index.js.map +1 -0
  301. package/dist/test.d.ts +2230 -0
  302. package/dist/test.d.ts.map +1 -0
  303. package/dist/test.js +1107 -0
  304. package/dist/test.js.map +1 -0
  305. package/package.json +95 -0
  306. package/src/cli/commands/admin.ts +104 -0
  307. package/src/cli/index.ts +21 -0
  308. package/src/cli/utils/detectConvexUrl.ts +54 -0
  309. package/src/cli/utils/openBrowser.ts +16 -0
  310. package/src/client/admin-config.ts +138 -0
  311. package/src/client/adminApi.ts +942 -0
  312. package/src/client/agentTools.ts +1311 -0
  313. package/src/client/argTypes.ts +316 -0
  314. package/src/client/field-types.ts +187 -0
  315. package/src/client/index.ts +1301 -0
  316. package/src/client/queryBuilder.ts +1100 -0
  317. package/src/client/schema/codegen.ts +500 -0
  318. package/src/client/schema/defineContentType.ts +501 -0
  319. package/src/client/schema/index.ts +169 -0
  320. package/src/client/schema/schemaDrift.ts +574 -0
  321. package/src/client/schema/typedClient.ts +688 -0
  322. package/src/client/schema/types.ts +666 -0
  323. package/src/client/types.ts +723 -0
  324. package/src/client/workflows.ts +141 -0
  325. package/src/client/wrapper.ts +4304 -0
  326. package/src/component/_generated/api.ts +140 -0
  327. package/src/component/_generated/component.ts +5029 -0
  328. package/src/component/_generated/dataModel.ts +60 -0
  329. package/src/component/_generated/server.ts +156 -0
  330. package/src/component/authorization.ts +647 -0
  331. package/src/component/authorizationHooks.ts +668 -0
  332. package/src/component/bulkOperations.ts +687 -0
  333. package/src/component/contentEntries.ts +1976 -0
  334. package/src/component/contentEntryMutations.ts +1223 -0
  335. package/src/component/contentEntryValidation.ts +707 -0
  336. package/src/component/contentLock.ts +550 -0
  337. package/src/component/contentTypeMigration.ts +1064 -0
  338. package/src/component/contentTypeMutations.ts +969 -0
  339. package/src/component/contentTypes.ts +346 -0
  340. package/src/component/convex.config.ts +44 -0
  341. package/src/component/documentTypes.ts +240 -0
  342. package/src/component/eventEmitter.ts +485 -0
  343. package/src/component/exportImport.ts +1169 -0
  344. package/src/component/index.ts +491 -0
  345. package/src/component/lib/deepReferenceResolver.ts +999 -0
  346. package/src/component/lib/errors.ts +816 -0
  347. package/src/component/lib/index.ts +145 -0
  348. package/src/component/lib/mediaReferenceResolver.ts +495 -0
  349. package/src/component/lib/metadataExtractor.ts +792 -0
  350. package/src/component/lib/mutationAuth.ts +199 -0
  351. package/src/component/lib/queries.ts +79 -0
  352. package/src/component/lib/ragContentChunker.ts +1371 -0
  353. package/src/component/lib/referenceResolver.ts +430 -0
  354. package/src/component/lib/slugGenerator.ts +262 -0
  355. package/src/component/lib/slugUniqueness.ts +333 -0
  356. package/src/component/lib/softDelete.ts +44 -0
  357. package/src/component/localeFallbackChain.ts +673 -0
  358. package/src/component/localeFields.ts +896 -0
  359. package/src/component/mediaAssetMutations.ts +725 -0
  360. package/src/component/mediaAssets.ts +932 -0
  361. package/src/component/mediaFolderMutations.ts +1046 -0
  362. package/src/component/mediaUploadMutations.ts +224 -0
  363. package/src/component/mediaVariantMutations.ts +900 -0
  364. package/src/component/mediaVariants.ts +793 -0
  365. package/src/component/ragContentIndexer.ts +1067 -0
  366. package/src/component/rateLimitHooks.ts +572 -0
  367. package/src/component/roles.ts +1360 -0
  368. package/src/component/scheduledPublish.ts +358 -0
  369. package/src/component/schema.ts +617 -0
  370. package/src/component/taxonomies.ts +949 -0
  371. package/src/component/taxonomyMutations.ts +1210 -0
  372. package/src/component/trash.ts +724 -0
  373. package/src/component/userContext.ts +898 -0
  374. package/src/component/validation.ts +1388 -0
  375. package/src/component/validators.ts +949 -0
  376. package/src/component/versionMutations.ts +392 -0
  377. package/src/component/webhookTrigger.ts +1922 -0
  378. package/src/react/index.ts +898 -0
  379. package/src/test.ts +1580 -0
@@ -0,0 +1,1360 @@
1
+ /**
2
+ * RBAC Default Roles Configuration
3
+ *
4
+ * This module defines the default role configurations for the CMS:
5
+ * - admin: Full access to all CMS features
6
+ * - editor: Can manage all content and media, but not settings
7
+ * - author: Can create and manage own content
8
+ * - viewer: Read-only access to published content
9
+ *
10
+ * Roles are exported as constants for easy customization. Developers can
11
+ * extend or override these defaults using the custom roles feature.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import { DEFAULT_ROLES, hasPermission, type RoleName } from '@convex-cms/core';
16
+ *
17
+ * // Check if a role has a specific permission
18
+ * if (hasPermission('editor', { resource: 'contentEntries', action: 'update' })) {
19
+ * // Allow the action
20
+ * }
21
+ *
22
+ * // Get all permissions for a role
23
+ * const adminPerms = getRolePermissions('admin');
24
+ * ```
25
+ */
26
+
27
+ import { v } from "convex/values";
28
+
29
+ // =============================================================================
30
+ // Role Name Constants
31
+ // =============================================================================
32
+
33
+ /**
34
+ * All built-in role names in the CMS.
35
+ * Custom roles can be added by developers, but these are always available.
36
+ */
37
+ export const roleNames = ["admin", "editor", "author", "viewer"] as const;
38
+
39
+ /**
40
+ * Type representing a built-in role name.
41
+ * Use `string` for custom roles, or extend this type.
42
+ */
43
+ export type RoleName = typeof roleNames[number];
44
+
45
+ /**
46
+ * Convex validator for role names.
47
+ * Use this in function arguments to validate role input.
48
+ */
49
+ export const roleNameValidator = v.union(
50
+ v.literal("admin"),
51
+ v.literal("editor"),
52
+ v.literal("author"),
53
+ v.literal("viewer"),
54
+ );
55
+
56
+ // =============================================================================
57
+ // Resource and Action Constants
58
+ // =============================================================================
59
+
60
+ /**
61
+ * All resources that can be protected by RBAC.
62
+ */
63
+ export const resources = [
64
+ "contentTypes",
65
+ "contentEntries",
66
+ "mediaItems",
67
+ "settings",
68
+ ] as const;
69
+
70
+ export type Resource = typeof resources[number];
71
+
72
+ /**
73
+ * Convex validator for resources.
74
+ */
75
+ export const resourceValidator = v.union(
76
+ v.literal("contentTypes"),
77
+ v.literal("contentEntries"),
78
+ v.literal("mediaItems"),
79
+ v.literal("settings"),
80
+ );
81
+
82
+ /**
83
+ * All actions that can be performed on resources.
84
+ */
85
+ export const actions = [
86
+ "create",
87
+ "read",
88
+ "update",
89
+ "delete",
90
+ "publish",
91
+ "unpublish",
92
+ "restore",
93
+ "manage", // Special action for full control (e.g., settings)
94
+ "move", // Move items between folders (media)
95
+ ] as const;
96
+
97
+ export type Action = typeof actions[number];
98
+
99
+ /**
100
+ * Convex validator for actions.
101
+ */
102
+ export const actionValidator = v.union(
103
+ v.literal("create"),
104
+ v.literal("read"),
105
+ v.literal("update"),
106
+ v.literal("delete"),
107
+ v.literal("publish"),
108
+ v.literal("unpublish"),
109
+ v.literal("restore"),
110
+ v.literal("manage"),
111
+ v.literal("move"),
112
+ );
113
+
114
+ // =============================================================================
115
+ // Permission Types
116
+ // =============================================================================
117
+
118
+ /**
119
+ * Ownership scope for permissions.
120
+ * - "all": Can perform action on any item
121
+ * - "own": Can only perform action on items they created
122
+ */
123
+ export type OwnershipScope = "all" | "own";
124
+
125
+ /**
126
+ * A single permission grant.
127
+ * Defines what action can be performed on which resource, with optional ownership scope.
128
+ */
129
+ export interface Permission {
130
+ /** The resource this permission applies to */
131
+ resource: Resource;
132
+ /** The action being granted */
133
+ action: Action;
134
+ /**
135
+ * Ownership scope (defaults to "all" if not specified).
136
+ * Only relevant for resources that have ownership (contentEntries, mediaAssets).
137
+ */
138
+ scope?: OwnershipScope;
139
+ }
140
+
141
+ /**
142
+ * Convex validator for a permission object.
143
+ */
144
+ export const permissionValidator = v.object({
145
+ resource: resourceValidator,
146
+ action: actionValidator,
147
+ scope: v.optional(v.union(v.literal("all"), v.literal("own"))),
148
+ });
149
+
150
+ /**
151
+ * Complete role definition including metadata and permissions.
152
+ */
153
+ export interface RoleDefinition {
154
+ /** Unique role identifier */
155
+ name: RoleName | string;
156
+ /** Human-readable display name */
157
+ displayName: string;
158
+ /** Description of the role's purpose */
159
+ description: string;
160
+ /** List of permissions granted to this role */
161
+ permissions: Permission[];
162
+ /** Whether this is a system role that cannot be deleted */
163
+ isSystem: boolean;
164
+ }
165
+
166
+ // =============================================================================
167
+ // Permission Factory Helpers
168
+ // =============================================================================
169
+
170
+ /**
171
+ * Helper to create a full CRUD permission set for a resource.
172
+ */
173
+ function fullCrud(
174
+ resource: Resource,
175
+ scope: OwnershipScope = "all",
176
+ ): Permission[] {
177
+ return [
178
+ { resource, action: "create", scope },
179
+ { resource, action: "read", scope },
180
+ { resource, action: "update", scope },
181
+ { resource, action: "delete", scope },
182
+ ];
183
+ }
184
+
185
+ /**
186
+ * Helper to create read-only permission for a resource.
187
+ */
188
+ function readOnly(
189
+ resource: Resource,
190
+ scope: OwnershipScope = "all",
191
+ ): Permission[] {
192
+ return [{ resource, action: "read", scope }];
193
+ }
194
+
195
+ /**
196
+ * Helper to create publish permissions for content.
197
+ */
198
+ function publishPermissions(scope: OwnershipScope = "all"): Permission[] {
199
+ return [
200
+ { resource: "contentEntries", action: "publish", scope },
201
+ { resource: "contentEntries", action: "unpublish", scope },
202
+ ];
203
+ }
204
+
205
+ // =============================================================================
206
+ // Default Role Definitions
207
+ // =============================================================================
208
+
209
+ /**
210
+ * Admin role - Full access to all CMS features.
211
+ *
212
+ * Admins can:
213
+ * - Create, read, update, and delete all content types
214
+ * - Manage all content entries regardless of author
215
+ * - Publish and unpublish any content
216
+ * - Manage all media assets and folders
217
+ * - Access and modify CMS settings
218
+ */
219
+ export const ADMIN_ROLE: RoleDefinition = {
220
+ name: "admin",
221
+ displayName: "Administrator",
222
+ description:
223
+ "Full access to all CMS features including settings and content type management",
224
+ isSystem: true,
225
+ permissions: [
226
+ // Content types - full management
227
+ ...fullCrud("contentTypes"),
228
+
229
+ // Content entries - full CRUD + publish
230
+ ...fullCrud("contentEntries"),
231
+ ...publishPermissions(),
232
+ { resource: "contentEntries", action: "restore" },
233
+
234
+ // Media - full management
235
+ ...fullCrud("mediaItems"),
236
+
237
+ // Settings - full access
238
+ { resource: "settings", action: "manage" },
239
+ ...readOnly("settings"),
240
+ ],
241
+ };
242
+
243
+ /**
244
+ * Editor role - Can manage all content and media, but not settings or content types.
245
+ *
246
+ * Editors can:
247
+ * - Read content type definitions
248
+ * - Create, read, update, and delete all content entries
249
+ * - Publish and unpublish any content
250
+ * - Manage all media assets and folders
251
+ * - Cannot modify CMS settings or content type schemas
252
+ */
253
+ export const EDITOR_ROLE: RoleDefinition = {
254
+ name: "editor",
255
+ displayName: "Editor",
256
+ description:
257
+ "Can manage all content and media, but cannot modify settings or content types",
258
+ isSystem: true,
259
+ permissions: [
260
+ // Content types - read only
261
+ ...readOnly("contentTypes"),
262
+
263
+ // Content entries - full CRUD + publish
264
+ ...fullCrud("contentEntries"),
265
+ ...publishPermissions(),
266
+ { resource: "contentEntries", action: "restore" },
267
+
268
+ // Media - full management
269
+ ...fullCrud("mediaItems"),
270
+ ],
271
+ };
272
+
273
+ /**
274
+ * Author role - Can create and manage own content.
275
+ *
276
+ * Authors can:
277
+ * - Read content type definitions
278
+ * - Create content entries
279
+ * - Read, update, and delete their own content entries
280
+ * - Publish and unpublish their own content (subject to workflow settings)
281
+ * - Upload and manage their own media assets
282
+ * - Read all media (for embedding in content)
283
+ * - Cannot manage other users' content or CMS settings
284
+ */
285
+ export const AUTHOR_ROLE: RoleDefinition = {
286
+ name: "author",
287
+ displayName: "Author",
288
+ description: "Can create and manage own content and media",
289
+ isSystem: true,
290
+ permissions: [
291
+ // Content types - read only
292
+ ...readOnly("contentTypes"),
293
+
294
+ // Content entries - own content only
295
+ { resource: "contentEntries", action: "create" },
296
+ { resource: "contentEntries", action: "read", scope: "own" },
297
+ { resource: "contentEntries", action: "update", scope: "own" },
298
+ { resource: "contentEntries", action: "delete", scope: "own" },
299
+ // Authors can publish/unpublish their own content
300
+ { resource: "contentEntries", action: "publish", scope: "own" },
301
+ { resource: "contentEntries", action: "unpublish", scope: "own" },
302
+
303
+ // Media - can create and manage own, read all (for embedding)
304
+ { resource: "mediaItems", action: "create" },
305
+ { resource: "mediaItems", action: "read", scope: "all" }, // Can read all for embedding
306
+ { resource: "mediaItems", action: "update", scope: "own" },
307
+ { resource: "mediaItems", action: "delete", scope: "own" },
308
+ ],
309
+ };
310
+
311
+ /**
312
+ * Viewer role - Read-only access to published content.
313
+ *
314
+ * Viewers can:
315
+ * - Read content type definitions
316
+ * - Read published content entries only
317
+ * - View media assets
318
+ * - Cannot create, update, delete, or publish any content
319
+ */
320
+ export const VIEWER_ROLE: RoleDefinition = {
321
+ name: "viewer",
322
+ displayName: "Viewer",
323
+ description: "Read-only access to published content and media",
324
+ isSystem: true,
325
+ permissions: [
326
+ // Content types - read only
327
+ ...readOnly("contentTypes"),
328
+
329
+ // Content entries - read published only (scope: "all" means all published)
330
+ ...readOnly("contentEntries"),
331
+
332
+ // Media - read only
333
+ ...readOnly("mediaItems"),
334
+ ],
335
+ };
336
+
337
+ // =============================================================================
338
+ // Default Roles Collection
339
+ // =============================================================================
340
+
341
+ /**
342
+ * All default roles indexed by role name.
343
+ * Use this to look up role definitions or iterate over all roles.
344
+ *
345
+ * @example
346
+ * ```typescript
347
+ * // Get the admin role definition
348
+ * const adminDef = DEFAULT_ROLES.admin;
349
+ *
350
+ * // Iterate over all roles
351
+ * for (const [name, role] of Object.entries(DEFAULT_ROLES)) {
352
+ * console.log(`${name}: ${role.description}`);
353
+ * }
354
+ * ```
355
+ */
356
+ export const DEFAULT_ROLES: Record<RoleName, RoleDefinition> = {
357
+ admin: ADMIN_ROLE,
358
+ editor: EDITOR_ROLE,
359
+ author: AUTHOR_ROLE,
360
+ viewer: VIEWER_ROLE,
361
+ };
362
+
363
+ /**
364
+ * Array of all default role definitions.
365
+ * Useful for UI rendering or iterating over roles.
366
+ */
367
+ export const DEFAULT_ROLES_LIST: RoleDefinition[] = Object.values(
368
+ DEFAULT_ROLES,
369
+ );
370
+
371
+ // =============================================================================
372
+ // Permission Check Utilities
373
+ // =============================================================================
374
+
375
+ /**
376
+ * Check if a permission matches a requested permission.
377
+ * Handles scope matching (own scope only matches if requested scope is also own).
378
+ *
379
+ * @param granted - The permission that was granted to the role
380
+ * @param requested - The permission being requested
381
+ * @returns True if the granted permission satisfies the requested permission
382
+ */
383
+ export function permissionMatches(
384
+ granted: Permission,
385
+ requested: { resource: Resource; action: Action; scope?: OwnershipScope },
386
+ ): boolean {
387
+ // Resource and action must match
388
+ if (
389
+ granted.resource !== requested.resource ||
390
+ granted.action !== requested.action
391
+ ) {
392
+ return false;
393
+ }
394
+
395
+ // Scope matching:
396
+ // - If granted scope is "all" (or undefined), it covers both "all" and "own" requests
397
+ // - If granted scope is "own", it only covers "own" requests
398
+ const grantedScope = granted.scope ?? "all";
399
+ const requestedScope = requested.scope ?? "all";
400
+
401
+ if (grantedScope === "all") {
402
+ return true; // "all" scope covers everything
403
+ }
404
+
405
+ // "own" scope only matches "own" requests
406
+ return requestedScope === "own";
407
+ }
408
+
409
+ /**
410
+ * Check if a role has a specific permission.
411
+ *
412
+ * @param roleName - The name of the role to check
413
+ * @param permission - The permission to check for (resource + action + optional scope)
414
+ * @param customRoles - Optional custom roles to check in addition to defaults
415
+ * @returns True if the role has the permission
416
+ *
417
+ * @example
418
+ * ```typescript
419
+ * // Check if editor can update content entries
420
+ * hasPermission('editor', { resource: 'contentEntries', action: 'update' }); // true
421
+ *
422
+ * // Check if author can publish their own content
423
+ * hasPermission('author', { resource: 'contentEntries', action: 'publish', scope: 'own' }); // true
424
+ *
425
+ * // Check if viewer can update content
426
+ * hasPermission('viewer', { resource: 'contentEntries', action: 'update' }); // false
427
+ * ```
428
+ */
429
+ export function hasPermission(
430
+ roleName: RoleName | string,
431
+ permission: { resource: Resource; action: Action; scope?: OwnershipScope },
432
+ customRoles?: Record<string, RoleDefinition>,
433
+ ): boolean {
434
+ // Look up role in default roles first, then custom roles
435
+ const role = DEFAULT_ROLES[roleName as RoleName] ?? customRoles?.[roleName];
436
+
437
+ if (!role) {
438
+ return false; // Unknown role has no permissions
439
+ }
440
+
441
+ // Check if any granted permission matches the requested permission
442
+ return role.permissions.some((p) => permissionMatches(p, permission));
443
+ }
444
+
445
+ /**
446
+ * Get all permissions for a role.
447
+ *
448
+ * @param roleName - The name of the role
449
+ * @param customRoles - Optional custom roles to check in addition to defaults
450
+ * @returns Array of permissions, or empty array if role not found
451
+ *
452
+ * @example
453
+ * ```typescript
454
+ * const editorPerms = getRolePermissions('editor');
455
+ * console.log(editorPerms.length); // Number of permissions
456
+ * ```
457
+ */
458
+ export function getRolePermissions(
459
+ roleName: RoleName | string,
460
+ customRoles?: Record<string, RoleDefinition>,
461
+ ): Permission[] {
462
+ const role = DEFAULT_ROLES[roleName as RoleName] ?? customRoles?.[roleName];
463
+
464
+ return role?.permissions ?? [];
465
+ }
466
+
467
+ /**
468
+ * Get the role definition for a role name.
469
+ *
470
+ * @param roleName - The name of the role
471
+ * @param customRoles - Optional custom roles to check in addition to defaults
472
+ * @returns The role definition, or undefined if not found
473
+ */
474
+ export function getRole(
475
+ roleName: RoleName | string,
476
+ customRoles?: Record<string, RoleDefinition>,
477
+ ): RoleDefinition | undefined {
478
+ return DEFAULT_ROLES[roleName as RoleName] ?? customRoles?.[roleName];
479
+ }
480
+
481
+ /**
482
+ * Check if a role name is a valid built-in role.
483
+ *
484
+ * @param name - The role name to check
485
+ * @returns True if it's a valid built-in role name
486
+ */
487
+ export function isBuiltInRole(name: string): name is RoleName {
488
+ return roleNames.includes(name as RoleName);
489
+ }
490
+
491
+ /**
492
+ * Get all permissions for a specific resource across a role.
493
+ *
494
+ * @param roleName - The name of the role
495
+ * @param resource - The resource to filter by
496
+ * @param customRoles - Optional custom roles to check in addition to defaults
497
+ * @returns Array of permissions for the specified resource
498
+ *
499
+ * @example
500
+ * ```typescript
501
+ * // Get all content entry permissions for editor
502
+ * const contentPerms = getResourcePermissions('editor', 'contentEntries');
503
+ * ```
504
+ */
505
+ export function getResourcePermissions(
506
+ roleName: RoleName | string,
507
+ resource: Resource,
508
+ customRoles?: Record<string, RoleDefinition>,
509
+ ): Permission[] {
510
+ return getRolePermissions(roleName, customRoles).filter(
511
+ (p) => p.resource === resource,
512
+ );
513
+ }
514
+
515
+ /**
516
+ * Check if a role can perform any action on a resource.
517
+ *
518
+ * @param roleName - The name of the role
519
+ * @param resource - The resource to check
520
+ * @param customRoles - Optional custom roles to check in addition to defaults
521
+ * @returns True if the role h permission on the resource
522
+ */
523
+ export function canAccessResource(
524
+ roleName: RoleName | string,
525
+ resource: Resource,
526
+ customRoles?: Record<string, RoleDefinition>,
527
+ ): boolean {
528
+ return getResourcePermissions(roleName, resource, customRoles).length > 0;
529
+ }
530
+
531
+ // =============================================================================
532
+ // Custom Role Types and Interfaces
533
+ // =============================================================================
534
+
535
+ /**
536
+ * Extended permission with optional content-type-specific restrictions.
537
+ * Allows for fine-grained control over which content types a permission applies to.
538
+ *
539
+ * @example
540
+ * ```typescript
541
+ * // Permission that only applies to blog_post and news content types
542
+ * const permission: ContentTypePermission = {
543
+ * resource: "contentEntries",
544
+ * action: "create",
545
+ * contentTypes: ["blog_post", "news"],
546
+ * };
547
+ *
548
+ * // Permission that applies to all content types except legal
549
+ * const restrictedPerm: ContentTypePermission = {
550
+ * resource: "contentEntries",
551
+ * action: "publish",
552
+ * excludeContentTypes: ["legal_document"],
553
+ * };
554
+ * ```
555
+ */
556
+ export interface ContentTypePermission extends Permission {
557
+ /**
558
+ * Whitelist of content type names this permission applies to.
559
+ * If specified, permission only grants access to these content types.
560
+ * Cannot be used with excludeContentTypes.
561
+ */
562
+ contentTypes?: string[];
563
+
564
+ /**
565
+ * Blacklist of content type names this permission does NOT apply to.
566
+ * If specified, permission grants access to all content types except these.
567
+ * Cannot be used with contentTypes.
568
+ */
569
+ excludeContentTypes?: string[];
570
+ }
571
+
572
+ /**
573
+ * Configuration for creating a custom role.
574
+ *
575
+ * @example
576
+ * ```typescript
577
+ * // Create a new role from scratch
578
+ * const blogAuthor: CustomRoleConfig = {
579
+ * name: "blog-author",
580
+ * displayName: "Blog Author",
581
+ * description: "Can create and manage blog posts only",
582
+ * permissions: [
583
+ * { resource: "contentTypes", action: "read" },
584
+ * { resource: "contentEntries", action: "create", contentTypes: ["blog_post"] },
585
+ * { resource: "contentEntries", action: "read", scope: "own", contentTypes: ["blog_post"] },
586
+ * { resource: "contentEntries", action: "update", scope: "own", contentTypes: ["blog_post"] },
587
+ * ],
588
+ * };
589
+ * ```
590
+ */
591
+ export interface CustomRoleConfig {
592
+ /** Unique identifier for the custom role */
593
+ name: string;
594
+ /** Human-readable display name */
595
+ displayName: string;
596
+ /** Description of the role's purpose */
597
+ description: string;
598
+ /** Permissions granted to this role */
599
+ permissions: ContentTypePermission[];
600
+ /** Whether this role should be treated as a system role (cannot be deleted) */
601
+ isSystem?: boolean;
602
+ }
603
+
604
+ /**
605
+ * Configuration for extending an existing role.
606
+ *
607
+ * @example
608
+ * ```typescript
609
+ * // Extend the author role with additional permissions
610
+ * const seniorAuthor: ExtendRoleConfig = {
611
+ * name: "senior-author",
612
+ * displayName: "Senior Author",
613
+ * description: "Author with additional publishing rights",
614
+ * extends: "author",
615
+ * addPermissions: [
616
+ * { resource: "contentEntries", action: "publish" },
617
+ * ],
618
+ * };
619
+ *
620
+ * // Extend editor but restrict to certain content types
621
+ * const blogEditor: ExtendRoleConfig = {
622
+ * name: "blog-editor",
623
+ * displayName: "Blog Editor",
624
+ * description: "Editor for blog content only",
625
+ * extends: "editor",
626
+ * addPermissions: [],
627
+ * removePermissions: [
628
+ * { resource: "contentEntries", action: "create" },
629
+ * ],
630
+ * restrictToContentTypes: ["blog_post", "blog_category"],
631
+ * };
632
+ * ```
633
+ */
634
+ export interface ExtendRoleConfig {
635
+ /** Unique identifier for the extended role */
636
+ name: string;
637
+ /** Human-readable display name */
638
+ displayName: string;
639
+ /** Description of the role's purpose */
640
+ description: string;
641
+ /** Name of the role to extend (can be built-in or custom) */
642
+ extends: RoleName | string;
643
+ /** Additional permissions to add to the extended role */
644
+ addPermissions?: ContentTypePermission[];
645
+ /**
646
+ * Permissions to remove from the extended role.
647
+ * Matching is done by resource + action (scope is ignored for removal).
648
+ */
649
+ removePermissions?: Array<{ resource: Resource; action: Action }>;
650
+ /**
651
+ * Restrict all contentEntries permissions to these content types.
652
+ * If specified, all contentEntries permissions are limited to only these types.
653
+ */
654
+ restrictToContentTypes?: string[];
655
+ /** Whether this role should be treated as a system role */
656
+ isSystem?: boolean;
657
+ }
658
+
659
+ /**
660
+ * Extended role definition that supports content-type-specific permissions.
661
+ * This is the runtime representation of a role that may have per-content-type restrictions.
662
+ */
663
+ export interface ExtendedRoleDefinition {
664
+ /** Unique role identifier */
665
+ name: string;
666
+ /** Human-readable display name */
667
+ displayName: string;
668
+ /** Description of the role's purpose */
669
+ description: string;
670
+ /** List of permissions granted to this role (may include content-type restrictions) */
671
+ permissions: ContentTypePermission[];
672
+ /** Whether this is a system role that cannot be deleted */
673
+ isSystem: boolean;
674
+ /** If this role was extended from another, the source role name */
675
+ extendsRole?: string;
676
+ }
677
+
678
+ // =============================================================================
679
+ // Custom Role Factory Functions
680
+ // =============================================================================
681
+
682
+ /**
683
+ * Creates a new custom role from configuration.
684
+ *
685
+ * @param config - The custom role configuration
686
+ * @returns A role definition ready to use with the RBAC system
687
+ *
688
+ * @example
689
+ * ```typescript
690
+ * const blogAuthor = createCustomRole({
691
+ * name: "blog-author",
692
+ * displayName: "Blog Author",
693
+ * description: "Can create and manage blog posts",
694
+ * permissions: [
695
+ * { resource: "contentTypes", action: "read" },
696
+ * { resource: "contentEntries", action: "create", contentTypes: ["blog_post"] },
697
+ * { resource: "contentEntries", action: "read", scope: "own", contentTypes: ["blog_post"] },
698
+ * { resource: "contentEntries", action: "update", scope: "own", contentTypes: ["blog_post"] },
699
+ * { resource: "contentEntries", action: "delete", scope: "own", contentTypes: ["blog_post"] },
700
+ * { resource: "mediaItems", action: "create" },
701
+ * { resource: "mediaItems", action: "read" },
702
+ * ],
703
+ * });
704
+ * ```
705
+ */
706
+ export function createCustomRole(
707
+ config: CustomRoleConfig,
708
+ ): ExtendedRoleDefinition {
709
+ // Validate the configuration
710
+ if (!config.name || config.name.trim() === "") {
711
+ throw new Error("Custom role name is required");
712
+ }
713
+
714
+ if (isBuiltInRole(config.name)) {
715
+ throw new Error(
716
+ `Cannot create custom role with built-in role name '${config.name}'. ` +
717
+ "Use extendRole() to extend a built-in role, or choose a different name.",
718
+ );
719
+ }
720
+
721
+ return {
722
+ name: config.name,
723
+ displayName: config.displayName,
724
+ description: config.description,
725
+ permissions: config.permissions,
726
+ isSystem: config.isSystem ?? false,
727
+ };
728
+ }
729
+
730
+ /**
731
+ * Extends an existing role with additional or removed permissions.
732
+ *
733
+ * This function creates a new role based on an existing one, allowing you to:
734
+ * - Add new permissions
735
+ * - Remove existing permissions
736
+ * - Restrict all contentEntries permissions to specific content types
737
+ *
738
+ * @param config - The extend role configuration
739
+ * @param customRoles - Optional existing custom roles to look up the base role from
740
+ * @returns A new role definition with the modified permissions
741
+ *
742
+ * @example
743
+ * ```typescript
744
+ * // Create a senior author who can publish their own content
745
+ * const seniorAuthor = extendRole({
746
+ * name: "senior-author",
747
+ * displayName: "Senior Author",
748
+ * description: "Author with publishing rights",
749
+ * extends: "author",
750
+ * addPermissions: [
751
+ * { resource: "contentEntries", action: "publish", scope: "own" },
752
+ * { resource: "contentEntries", action: "unpublish", scope: "own" },
753
+ * ],
754
+ * });
755
+ *
756
+ * // Create a blog-only editor
757
+ * const blogEditor = extendRole({
758
+ * name: "blog-editor",
759
+ * displayName: "Blog Editor",
760
+ * description: "Can only edit blog content",
761
+ * extends: "editor",
762
+ * restrictToContentTypes: ["blog_post", "blog_category"],
763
+ * });
764
+ * ```
765
+ */
766
+ export function extendRole(
767
+ config: ExtendRoleConfig,
768
+ customRoles?: Record<string, RoleDefinition | ExtendedRoleDefinition>,
769
+ ): ExtendedRoleDefinition {
770
+ // Validate the configuration
771
+ if (!config.name || config.name.trim() === "") {
772
+ throw new Error("Extended role name is required");
773
+ }
774
+
775
+ if (config.name === config.extends) {
776
+ throw new Error(
777
+ "Extended role name must be different from the base role name",
778
+ );
779
+ }
780
+
781
+ // Get the base role
782
+ const baseRole = getRole(config.extends, customRoles);
783
+ if (!baseRole) {
784
+ throw new Error(
785
+ `Cannot extend unknown role '${config.extends}'. ` +
786
+ "Ensure the role exists as a built-in role or is defined in customRoles.",
787
+ );
788
+ }
789
+
790
+ // Start with base permissions
791
+ let permissions: ContentTypePermission[] = [...baseRole.permissions];
792
+
793
+ // Remove specified permissions
794
+ if (config.removePermissions && config.removePermissions.length > 0) {
795
+ permissions = permissions.filter((p) => {
796
+ return !config.removePermissions!.some(
797
+ (r) => r.resource === p.resource && r.action === p.action,
798
+ );
799
+ });
800
+ }
801
+
802
+ // Add new permissions
803
+ if (config.addPermissions && config.addPermissions.length > 0) {
804
+ permissions = [...permissions, ...config.addPermissions];
805
+ }
806
+
807
+ // Apply content type restrictions to all contentEntries permissions
808
+ if (
809
+ config.restrictToContentTypes &&
810
+ config.restrictToContentTypes.length > 0
811
+ ) {
812
+ permissions = permissions.map((p) => {
813
+ if (p.resource === "contentEntries") {
814
+ return {
815
+ ...p,
816
+ contentTypes: config.restrictToContentTypes,
817
+ };
818
+ }
819
+ return p;
820
+ });
821
+ }
822
+
823
+ return {
824
+ name: config.name,
825
+ displayName: config.displayName,
826
+ description: config.description,
827
+ permissions,
828
+ isSystem: config.isSystem ?? false,
829
+ extendsRole: config.extends,
830
+ };
831
+ }
832
+
833
+ /**
834
+ * Merges custom roles with the default roles.
835
+ *
836
+ * Creates a combined role registry that includes both default and custom roles.
837
+ * Custom roles do NOT override default roles - they exist alongside them.
838
+ *
839
+ * @param customRoles - Array of custom role definitions
840
+ * @returns A record of all roles (default + custom)
841
+ *
842
+ * @example
843
+ * ```typescript
844
+ * const blogAuthor = createCustomRole({...});
845
+ * const seniorAuthor = extendRole({...});
846
+ *
847
+ * const allRoles = mergeRolesWithDefaults([blogAuthor, seniorAuthor]);
848
+ * // allRoles contains: admin, editor, author, viewer, blog-author, senior-author
849
+ * ```
850
+ */
851
+ export function mergeRolesWithDefaults(
852
+ customRoles: Array<RoleDefinition | ExtendedRoleDefinition>,
853
+ ): Record<string, RoleDefinition | ExtendedRoleDefinition> {
854
+ const result: Record<string, RoleDefinition | ExtendedRoleDefinition> = {
855
+ ...DEFAULT_ROLES,
856
+ };
857
+
858
+ for (const role of customRoles) {
859
+ if (isBuiltInRole(role.name)) {
860
+ console.warn(
861
+ `Warning: Custom role '${role.name}' has the same name as a built-in role. ` +
862
+ "The built-in role will take precedence.",
863
+ );
864
+ continue;
865
+ }
866
+ result[role.name] = role;
867
+ }
868
+
869
+ return result;
870
+ }
871
+
872
+ /**
873
+ * Creates a custom roles record from an array of role definitions.
874
+ * Use this to pass custom roles to permission checking functions.
875
+ *
876
+ * @param roles - Array of custom role definitions
877
+ * @returns A record indexed by role name
878
+ *
879
+ * @example
880
+ * ```typescript
881
+ * const customRoles = buildCustomRolesRecord([blogAuthor, seniorAuthor]);
882
+ * hasPermission("blog-author", { resource: "contentEntries", action: "create" }, customRoles);
883
+ * ```
884
+ */
885
+ export function buildCustomRolesRecord(
886
+ roles: Array<RoleDefinition | ExtendedRoleDefinition>,
887
+ ): Record<string, RoleDefinition | ExtendedRoleDefinition> {
888
+ const result: Record<string, RoleDefinition | ExtendedRoleDefinition> = {};
889
+ for (const role of roles) {
890
+ result[role.name] = role;
891
+ }
892
+ return result;
893
+ }
894
+
895
+ // =============================================================================
896
+ // Content-Type-Aware Permission Checking
897
+ // =============================================================================
898
+
899
+ /**
900
+ * Options for checking permissions with content-type awareness.
901
+ */
902
+ export interface ContentTypePermissionCheckOptions {
903
+ /**
904
+ * Custom roles to include when checking permissions.
905
+ */
906
+ customRoles?: Record<string, RoleDefinition | ExtendedRoleDefinition>;
907
+
908
+ /**
909
+ * The content type name to check permissions for.
910
+ * Required when the permission may have content-type restrictions.
911
+ */
912
+ contentTypeName?: string;
913
+ }
914
+
915
+ /**
916
+ * Checks if a permission applies to a specific content type.
917
+ *
918
+ * @param permission - The permission to check
919
+ * @param contentTypeName - The content type name to check against
920
+ * @returns True if the permission applies to this content type
921
+ */
922
+ function permissionAppliesToContentType(
923
+ permission: ContentTypePermission,
924
+ contentTypeName?: string,
925
+ ): boolean {
926
+ // If no content type specified, the permission applies
927
+ if (!contentTypeName) {
928
+ return true;
929
+ }
930
+
931
+ // If permission has a whitelist, check if content type is in it
932
+ if (permission.contentTypes && permission.contentTypes.length > 0) {
933
+ return permission.contentTypes.includes(contentTypeName);
934
+ }
935
+
936
+ // If permission has a blacklist, check if content type is NOT in it
937
+ if (
938
+ permission.excludeContentTypes &&
939
+ permission.excludeContentTypes.length > 0
940
+ ) {
941
+ return !permission.excludeContentTypes.includes(contentTypeName);
942
+ }
943
+
944
+ // No restrictions, permission applies
945
+ return true;
946
+ }
947
+
948
+ /**
949
+ * Extended permission check that includes content-type-specific restrictions.
950
+ *
951
+ * Use this function when you need to check if a role can perform an action
952
+ * on a specific content type.
953
+ *
954
+ * @param roleName - The name of the role to check
955
+ * @param permission - The permission to check (resource + action + optional scope)
956
+ * @param options - Additional options including custom roles and content type
957
+ * @returns True if the role has the permission for the specified content type
958
+ *
959
+ * @example
960
+ * ```typescript
961
+ * // Check if blog-author can create blog posts
962
+ * hasContentTypePermission("blog-author", {
963
+ * resource: "contentEntries",
964
+ * action: "create",
965
+ * }, {
966
+ * customRoles: allRoles,
967
+ * contentTypeName: "blog_post",
968
+ * }); // true
969
+ *
970
+ * // Check if blog-author can create legal documents
971
+ * hasContentTypePermission("blog-author", {
972
+ * resource: "contentEntries",
973
+ * action: "create",
974
+ * }, {
975
+ * customRoles: allRoles,
976
+ * contentTypeName: "legal_document",
977
+ * }); // false (restricted to blog_post only)
978
+ * ```
979
+ */
980
+ export function hasContentTypePermission(
981
+ roleName: RoleName | string,
982
+ permission: { resource: Resource; action: Action; scope?: OwnershipScope },
983
+ options?: ContentTypePermissionCheckOptions,
984
+ ): boolean {
985
+ // Get the role definition
986
+ const role = getRole(roleName, options?.customRoles);
987
+
988
+ if (!role) {
989
+ return false;
990
+ }
991
+
992
+ // Check if any granted permission matches
993
+ return role.permissions.some((p) => {
994
+ // Check basic permission match (resource, action, scope)
995
+ if (!permissionMatches(p, permission)) {
996
+ return false;
997
+ }
998
+
999
+ // Check content type restrictions
1000
+ const extendedPerm = p as ContentTypePermission;
1001
+ return permissionAppliesToContentType(
1002
+ extendedPerm,
1003
+ options?.contentTypeName,
1004
+ );
1005
+ });
1006
+ }
1007
+
1008
+ /**
1009
+ * Gets all content types that a role can perform an action on.
1010
+ *
1011
+ * @param roleName - The name of the role
1012
+ * @param action - The action to check
1013
+ * @param options - Additional options
1014
+ * @returns Array of content type names, or ["*"] if unrestricted, or [] if no permission
1015
+ *
1016
+ * @example
1017
+ * ```typescript
1018
+ * // Get content types the blog-author can create
1019
+ * getPermittedContentTypes("blog-author", "create", { customRoles });
1020
+ * // Returns: ["blog_post"]
1021
+ *
1022
+ * // Get content types the editor can update
1023
+ * getPermittedContentTypes("editor", "update", { customRoles });
1024
+ * // Returns: ["*"] (unrestricted)
1025
+ * ```
1026
+ */
1027
+ export function getPermittedContentTypes(
1028
+ roleName: RoleName | string,
1029
+ action: Action,
1030
+ options?: {
1031
+ customRoles?: Record<string, RoleDefinition | ExtendedRoleDefinition>;
1032
+ },
1033
+ ): string[] {
1034
+ const role = getRole(roleName, options?.customRoles);
1035
+
1036
+ if (!role) {
1037
+ return [];
1038
+ }
1039
+
1040
+ // Find all contentEntries permissions for this action
1041
+ const contentPerms = role.permissions.filter(
1042
+ (p) => p.resource === "contentEntries" && p.action === action,
1043
+ ) as ContentTypePermission[];
1044
+
1045
+ if (contentPerms.length === 0) {
1046
+ return [];
1047
+ }
1048
+
1049
+ // Check if any permission is unrestricted
1050
+ const hasUnrestricted = contentPerms.some(
1051
+ (p) =>
1052
+ (!p.contentTypes || p.contentTypes.length === 0) &&
1053
+ (!p.excludeContentTypes || p.excludeContentTypes.length === 0),
1054
+ );
1055
+
1056
+ if (hasUnrestricted) {
1057
+ return ["*"]; // Unrestricted access
1058
+ }
1059
+
1060
+ // Collect all permitted content types
1061
+ const permitted = new Set<string>();
1062
+ for (const perm of contentPerms) {
1063
+ if (perm.contentTypes) {
1064
+ perm.contentTypes.forEach((ct) => permitted.add(ct));
1065
+ }
1066
+ }
1067
+
1068
+ return Array.from(permitted);
1069
+ }
1070
+
1071
+ /**
1072
+ * Gets all content types that a role is excluded from for an action.
1073
+ *
1074
+ * @param roleName - The name of the role
1075
+ * @param action - The action to check
1076
+ * @param options - Additional options
1077
+ * @returns Array of excluded content type names, or [] if none
1078
+ */
1079
+ export function getExcludedContentTypes(
1080
+ roleName: RoleName | string,
1081
+ action: Action,
1082
+ options?: {
1083
+ customRoles?: Record<string, RoleDefinition | ExtendedRoleDefinition>;
1084
+ },
1085
+ ): string[] {
1086
+ const role = getRole(roleName, options?.customRoles);
1087
+
1088
+ if (!role) {
1089
+ return [];
1090
+ }
1091
+
1092
+ // Find all contentEntries permissions for this action
1093
+ const contentPerms = role.permissions.filter(
1094
+ (p) => p.resource === "contentEntries" && p.action === action,
1095
+ ) as ContentTypePermission[];
1096
+
1097
+ // Collect all excluded content types
1098
+ const excluded = new Set<string>();
1099
+ for (const perm of contentPerms) {
1100
+ if (perm.excludeContentTypes) {
1101
+ perm.excludeContentTypes.forEach((ct) => excluded.add(ct));
1102
+ }
1103
+ }
1104
+
1105
+ return Array.from(excluded);
1106
+ }
1107
+
1108
+ // =============================================================================
1109
+ // Permission Factory Helpers for Custom Roles
1110
+ // =============================================================================
1111
+
1112
+ /**
1113
+ * Helper to create a full CRUD permission set for a resource with optional content type restriction.
1114
+ *
1115
+ * @param resource - The resource to grant permissions on
1116
+ * @param options - Optional scope and content type restrictions
1117
+ * @returns Array of permissions
1118
+ *
1119
+ * @example
1120
+ * ```typescript
1121
+ * // Full CRUD on contentEntries for blog_post only
1122
+ * fullCrudForContentType("contentEntries", {
1123
+ * contentTypes: ["blog_post"],
1124
+ * scope: "own",
1125
+ * });
1126
+ * ```
1127
+ */
1128
+ export function fullCrudForContentType(
1129
+ resource: Resource,
1130
+ options?: {
1131
+ scope?: OwnershipScope;
1132
+ contentTypes?: string[];
1133
+ excludeContentTypes?: string[];
1134
+ },
1135
+ ): ContentTypePermission[] {
1136
+ const scope = options?.scope ?? "all";
1137
+ const base = {
1138
+ scope,
1139
+ contentTypes: options?.contentTypes,
1140
+ excludeContentTypes: options?.excludeContentTypes,
1141
+ };
1142
+
1143
+ return [
1144
+ { resource, action: "create", ...base },
1145
+ { resource, action: "read", ...base },
1146
+ { resource, action: "update", ...base },
1147
+ { resource, action: "delete", ...base },
1148
+ ];
1149
+ }
1150
+
1151
+ /**
1152
+ * Helper to create publish permissions with optional content type restriction.
1153
+ *
1154
+ * @param options - Optional scope and content type restrictions
1155
+ * @returns Array of publish/unpublish permissions
1156
+ */
1157
+ export function publishPermissionsForContentType(options?: {
1158
+ scope?: OwnershipScope;
1159
+ contentTypes?: string[];
1160
+ excludeContentTypes?: string[];
1161
+ }): ContentTypePermission[] {
1162
+ const scope = options?.scope ?? "all";
1163
+ const base = {
1164
+ scope,
1165
+ contentTypes: options?.contentTypes,
1166
+ excludeContentTypes: options?.excludeContentTypes,
1167
+ };
1168
+
1169
+ return [
1170
+ { resource: "contentEntries", action: "publish", ...base },
1171
+ { resource: "contentEntries", action: "unpublish", ...base },
1172
+ ];
1173
+ }
1174
+
1175
+ /**
1176
+ * Helper to create read-only permission with optional content type restriction.
1177
+ *
1178
+ * @param resource - The resource to grant read permission on
1179
+ * @param options - Optional scope and content type restrictions
1180
+ * @returns Array with single read permission
1181
+ */
1182
+ export function readOnlyForContentType(
1183
+ resource: Resource,
1184
+ options?: {
1185
+ scope?: OwnershipScope;
1186
+ contentTypes?: string[];
1187
+ excludeContentTypes?: string[];
1188
+ },
1189
+ ): ContentTypePermission[] {
1190
+ return [
1191
+ {
1192
+ resource,
1193
+ action: "read",
1194
+ scope: options?.scope ?? "all",
1195
+ contentTypes: options?.contentTypes,
1196
+ excludeContentTypes: options?.excludeContentTypes,
1197
+ },
1198
+ ];
1199
+ }
1200
+
1201
+ // =============================================================================
1202
+ // Role Validation Utilities
1203
+ // =============================================================================
1204
+
1205
+ /**
1206
+ * Validates a custom role configuration.
1207
+ *
1208
+ * @param config - The custom role configuration to validate
1209
+ * @returns An object with isValid boolean and optional error messages
1210
+ */
1211
+ export function validateCustomRoleConfig(
1212
+ config: CustomRoleConfig,
1213
+ ): {
1214
+ isValid: boolean;
1215
+ errors: string[];
1216
+ } {
1217
+ const errors: string[] = [];
1218
+
1219
+ // Check required fields
1220
+ if (!config.name || config.name.trim() === "") {
1221
+ errors.push("Role name is required");
1222
+ }
1223
+
1224
+ if (!config.displayName || config.displayName.trim() === "") {
1225
+ errors.push("Display name is required");
1226
+ }
1227
+
1228
+ if (!config.description || config.description.trim() === "") {
1229
+ errors.push("Description is required");
1230
+ }
1231
+
1232
+ // Check for built-in name conflict
1233
+ if (config.name && isBuiltInRole(config.name)) {
1234
+ errors.push(`Role name '${config.name}' conflicts with a built-in role`);
1235
+ }
1236
+
1237
+ // Validate permissions
1238
+ if (!config.permissions || !Array.isArray(config.permissions)) {
1239
+ errors.push("Permissions must be an array");
1240
+ } else {
1241
+ for (let i = 0; i < config.permissions.length; i++) {
1242
+ const perm = config.permissions[i];
1243
+
1244
+ if (!resources.includes(perm.resource)) {
1245
+ errors.push(`Permission ${i}: Invalid resource '${perm.resource}'`);
1246
+ }
1247
+
1248
+ if (!actions.includes(perm.action)) {
1249
+ errors.push(`Permission ${i}: Invalid action '${perm.action}'`);
1250
+ }
1251
+
1252
+ if (perm.scope && perm.scope !== "all" && perm.scope !== "own") {
1253
+ errors.push(`Permission ${i}: Invalid scope '${perm.scope}'`);
1254
+ }
1255
+
1256
+ // Check for conflicting content type restrictions
1257
+ if (perm.contentTypes && perm.excludeContentTypes) {
1258
+ if (
1259
+ perm.contentTypes.length > 0 &&
1260
+ perm.excludeContentTypes.length > 0
1261
+ ) {
1262
+ errors.push(
1263
+ `Permission ${i}: Cannot specify both contentTypes and excludeContentTypes`,
1264
+ );
1265
+ }
1266
+ }
1267
+ }
1268
+ }
1269
+
1270
+ return {
1271
+ isValid: errors.length === 0,
1272
+ errors,
1273
+ };
1274
+ }
1275
+
1276
+ /**
1277
+ * Validates an extend role configuration.
1278
+ *
1279
+ * @param config - The extend role configuration to validate
1280
+ * @param customRoles - Optional custom roles to check the base role in
1281
+ * @returns An object with isValid boolean and optional error messages
1282
+ */
1283
+ export function validateExtendRoleConfig(
1284
+ config: ExtendRoleConfig,
1285
+ customRoles?: Record<string, RoleDefinition | ExtendedRoleDefinition>,
1286
+ ): {
1287
+ isValid: boolean;
1288
+ errors: string[];
1289
+ } {
1290
+ const errors: string[] = [];
1291
+
1292
+ // Check required fields
1293
+ if (!config.name || config.name.trim() === "") {
1294
+ errors.push("Role name is required");
1295
+ }
1296
+
1297
+ if (!config.displayName || config.displayName.trim() === "") {
1298
+ errors.push("Display name is required");
1299
+ }
1300
+
1301
+ if (!config.description || config.description.trim() === "") {
1302
+ errors.push("Description is required");
1303
+ }
1304
+
1305
+ if (!config.extends || config.extends.trim() === "") {
1306
+ errors.push("Base role name (extends) is required");
1307
+ }
1308
+
1309
+ // Check for self-reference
1310
+ if (config.name === config.extends) {
1311
+ errors.push("Cannot extend a role with itself");
1312
+ }
1313
+
1314
+ // Check if base role exists
1315
+ if (config.extends) {
1316
+ const baseRole = getRole(config.extends, customRoles);
1317
+ if (!baseRole) {
1318
+ errors.push(`Base role '${config.extends}' does not exist`);
1319
+ }
1320
+ }
1321
+
1322
+ // Validate addPermissions if provided
1323
+ if (config.addPermissions) {
1324
+ for (let i = 0; i < config.addPermissions.length; i++) {
1325
+ const perm = config.addPermissions[i];
1326
+
1327
+ if (!resources.includes(perm.resource)) {
1328
+ errors.push(
1329
+ `addPermissions[${i}]: Invalid resource '${perm.resource}'`,
1330
+ );
1331
+ }
1332
+
1333
+ if (!actions.includes(perm.action)) {
1334
+ errors.push(`addPermissions[${i}]: Invalid action '${perm.action}'`);
1335
+ }
1336
+ }
1337
+ }
1338
+
1339
+ // Validate removePermissions if provided
1340
+ if (config.removePermissions) {
1341
+ for (let i = 0; i < config.removePermissions.length; i++) {
1342
+ const perm = config.removePermissions[i];
1343
+
1344
+ if (!resources.includes(perm.resource)) {
1345
+ errors.push(
1346
+ `removePermissions[${i}]: Invalid resource '${perm.resource}'`,
1347
+ );
1348
+ }
1349
+
1350
+ if (!actions.includes(perm.action)) {
1351
+ errors.push(`removePermissions[${i}]: Invalid action '${perm.action}'`);
1352
+ }
1353
+ }
1354
+ }
1355
+
1356
+ return {
1357
+ isValid: errors.length === 0,
1358
+ errors,
1359
+ };
1360
+ }