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,898 @@
1
+ /**
2
+ * User Context Handler Module
3
+ *
4
+ * Functions to receive and validate user context passed from parent apps.
5
+ * Extracts user ID and roles for permission checks in the CMS authorization system.
6
+ *
7
+ * This module bridges the gap between external authentication systems and the
8
+ * CMS's internal RBAC (Role-Based Access Control) system. The CMS is auth-agnostic,
9
+ * meaning it doesn't own user authentication - instead, it relies on the parent
10
+ * application to provide user identification.
11
+ *
12
+ * Key concepts:
13
+ * - UserContext: The validated user information passed to CMS operations
14
+ * - User ID: A string identifier from your auth system (Clerk, Auth0, custom, etc.)
15
+ * - Role: The CMS role assigned to the user (admin, editor, author, viewer, or custom)
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * import { createUserContext, validateUserContext, extractUserId } from './userContext';
20
+ *
21
+ * // In your Convex function:
22
+ * export const createPost = mutation({
23
+ * handler: async (ctx, args) => {
24
+ * // Get user from your auth system
25
+ * const identity = await ctx.auth.getUserIdentity();
26
+ *
27
+ * // Create and validate user context
28
+ * const userContext = await createUserContext({
29
+ * userId: identity?.subject,
30
+ * getUserRole: config.getUserRole,
31
+ * });
32
+ *
33
+ * // Use in CMS operations
34
+ * const entry = await cms.contentEntries.create(ctx, {
35
+ * ...args,
36
+ * createdBy: userContext.userId,
37
+ * });
38
+ * },
39
+ * });
40
+ * ```
41
+ */
42
+
43
+ import type {
44
+ GetUserRoleHook,
45
+ GetUserRoleContext,
46
+ GetUserRoleResult,
47
+ AuthorizationHookContext,
48
+ CmsOperation,
49
+ CmsHookContext,
50
+ } from "../client/types.js";
51
+
52
+ import { getRole, type RoleDefinition } from "./roles.js";
53
+
54
+ // =============================================================================
55
+ // User Context Types
56
+ // =============================================================================
57
+
58
+ /**
59
+ * Raw user context input from the parent application.
60
+ * This is the initial user information before validation.
61
+ */
62
+ export interface UserContextInput {
63
+ /**
64
+ * The user ID from your authentication system.
65
+ * Can be undefined for anonymous/unauthenticated requests.
66
+ *
67
+ * Examples:
68
+ * - Clerk: `user.id` or `identity.subject`
69
+ * - Auth0: `user.sub`
70
+ * - Custom: Your database user ID
71
+ */
72
+ userId?: string | null;
73
+
74
+ /**
75
+ * Optional pre-resolved role name.
76
+ * If provided, skips the getUserRole hook lookup.
77
+ * Useful when you've already determined the role elsewhere.
78
+ */
79
+ role?: string | null;
80
+
81
+ /**
82
+ * Optional email for logging/debugging purposes.
83
+ * Not used for authorization decisions.
84
+ */
85
+ email?: string;
86
+
87
+ /**
88
+ * Optional display name for audit trails.
89
+ * Not used for authorization decisions.
90
+ */
91
+ displayName?: string;
92
+
93
+ /**
94
+ * Additional metadata from your auth system.
95
+ * Can be used in custom authorization hooks.
96
+ */
97
+ metadata?: Record<string, unknown>;
98
+ }
99
+
100
+ /**
101
+ * Validated user context ready for CMS operations.
102
+ * This is the result after validation and role resolution.
103
+ */
104
+ export interface UserContext {
105
+ /**
106
+ * The validated user ID.
107
+ * Will be undefined for anonymous users.
108
+ */
109
+ userId: string | undefined;
110
+
111
+ /**
112
+ * The resolved CMS role name.
113
+ * Will be null if the user has no CMS role assigned.
114
+ */
115
+ role: string | null;
116
+
117
+ /**
118
+ * Whether the user is authenticated (has a valid userId).
119
+ */
120
+ isAuthenticated: boolean;
121
+
122
+ /**
123
+ * Whether the user has a valid CMS role assigned.
124
+ */
125
+ hasRole: boolean;
126
+
127
+ /**
128
+ * The full role definition if the role exists.
129
+ * Useful for checking specific permissions.
130
+ */
131
+ roleDefinition?: RoleDefinition;
132
+
133
+ /**
134
+ * Optional email (passed through from input).
135
+ */
136
+ email?: string;
137
+
138
+ /**
139
+ * Optional display name (passed through from input).
140
+ */
141
+ displayName?: string;
142
+
143
+ /**
144
+ * Optional metadata (passed through from input).
145
+ */
146
+ metadata?: Record<string, unknown>;
147
+ }
148
+
149
+ /**
150
+ * Options for creating a user context.
151
+ */
152
+ export interface CreateUserContextOptions {
153
+ /**
154
+ * The CMS hook context (provides db and auth access for hooks).
155
+ * Required when using getUserRole hook.
156
+ */
157
+ ctx?: CmsHookContext;
158
+
159
+ /**
160
+ * The raw user context input.
161
+ */
162
+ input: UserContextInput;
163
+
164
+ /**
165
+ * The getUserRole hook from CMS configuration.
166
+ * Required if input.role is not provided.
167
+ */
168
+ getUserRole?: GetUserRoleHook;
169
+
170
+ /**
171
+ * Custom role definitions to check against.
172
+ * Used in addition to built-in roles.
173
+ */
174
+ customRoles?: Record<string, RoleDefinition>;
175
+
176
+ /**
177
+ * Whether to allow anonymous (unauthenticated) users.
178
+ * @default true
179
+ */
180
+ allowAnonymous?: boolean;
181
+
182
+ /**
183
+ * Whether to require a valid CMS role.
184
+ * If true, throws an error when the user has no role.
185
+ * @default false
186
+ */
187
+ requireRole?: boolean;
188
+ }
189
+
190
+ /**
191
+ * Validation error details for user context.
192
+ */
193
+ export interface UserContextValidationError {
194
+ /**
195
+ * Error code for programmatic handling.
196
+ */
197
+ code:
198
+ | "INVALID_USER_ID"
199
+ | "ANONYMOUS_NOT_ALLOWED"
200
+ | "ROLE_REQUIRED"
201
+ | "UNKNOWN_ROLE"
202
+ | "HOOK_ERROR";
203
+
204
+ /**
205
+ * Human-readable error message.
206
+ */
207
+ message: string;
208
+
209
+ /**
210
+ * Additional details about the error.
211
+ */
212
+ details?: Record<string, unknown>;
213
+ }
214
+
215
+ /**
216
+ * Result of user context validation.
217
+ */
218
+ export interface UserContextValidationResult {
219
+ /**
220
+ * Whether the validation passed.
221
+ */
222
+ valid: boolean;
223
+
224
+ /**
225
+ * The validated user context (only present if valid).
226
+ */
227
+ context?: UserContext;
228
+
229
+ /**
230
+ * Validation errors (only present if invalid).
231
+ */
232
+ errors?: UserContextValidationError[];
233
+ }
234
+
235
+ // =============================================================================
236
+ // User Context Error Class
237
+ // =============================================================================
238
+
239
+ /**
240
+ * Error thrown when user context validation fails.
241
+ *
242
+ * @example
243
+ * ```typescript
244
+ * try {
245
+ * const context = await createUserContext({ input, getUserRole });
246
+ * } catch (error) {
247
+ * if (error instanceof UserContextError) {
248
+ * console.log(error.code); // 'ANONYMOUS_NOT_ALLOWED'
249
+ * console.log(error.message); // 'Anonymous users are not allowed'
250
+ * }
251
+ * }
252
+ * ```
253
+ */
254
+ export class UserContextError extends Error {
255
+ readonly code: UserContextValidationError["code"];
256
+ readonly details?: Record<string, unknown>;
257
+
258
+ constructor(error: UserContextValidationError) {
259
+ super(error.message);
260
+ this.name = "UserContextError";
261
+ this.code = error.code;
262
+ this.details = error.details;
263
+
264
+ if (Error.captureStackTrace) {
265
+ Error.captureStackTrace(this, UserContextError);
266
+ }
267
+ }
268
+
269
+ toJSON(): Record<string, unknown> {
270
+ return {
271
+ name: this.name,
272
+ code: this.code,
273
+ message: this.message,
274
+ details: this.details,
275
+ };
276
+ }
277
+ }
278
+
279
+ // =============================================================================
280
+ // Validation Functions
281
+ // =============================================================================
282
+
283
+ /**
284
+ * Validate a user ID string.
285
+ *
286
+ * @param userId - The user ID to validate
287
+ * @returns True if the user ID is valid
288
+ *
289
+ * @example
290
+ * ```typescript
291
+ * isValidUserId("user_123"); // true
292
+ * isValidUserId(""); // false
293
+ * isValidUserId(null); // false
294
+ * ```
295
+ */
296
+ export function isValidUserId(userId: unknown): userId is string {
297
+ return typeof userId === "string" && userId.trim().length > 0;
298
+ }
299
+
300
+ /**
301
+ * Validate that a role name exists in the role definitions.
302
+ *
303
+ * @param roleName - The role name to validate
304
+ * @param customRoles - Optional custom role definitions
305
+ * @returns True if the role exists
306
+ *
307
+ * @example
308
+ * ```typescript
309
+ * isValidRole("admin"); // true (built-in)
310
+ * isValidRole("blog-author", { "blog-author": {...} }); // true (custom)
311
+ * isValidRole("unknown"); // false
312
+ * ```
313
+ */
314
+ export function isValidRole(
315
+ roleName: string | null | undefined,
316
+ customRoles?: Record<string, RoleDefinition>
317
+ ): boolean {
318
+ if (roleName === null || roleName === undefined) {
319
+ return false;
320
+ }
321
+ // getRole returns undefined if role not found
322
+ return getRole(roleName, customRoles) !== undefined;
323
+ }
324
+
325
+ /**
326
+ * Validate user context input without creating the full context.
327
+ * Use this for quick validation before expensive operations.
328
+ *
329
+ * @param input - The user context input to validate
330
+ * @param options - Validation options
331
+ * @returns Validation result with errors if invalid
332
+ *
333
+ * @example
334
+ * ```typescript
335
+ * const result = validateUserContextInput(
336
+ * { userId: "user_123" },
337
+ * { allowAnonymous: false }
338
+ * );
339
+ *
340
+ * if (!result.valid) {
341
+ * console.log(result.errors); // Validation errors
342
+ * }
343
+ * ```
344
+ */
345
+ export function validateUserContextInput(
346
+ input: UserContextInput,
347
+ options: {
348
+ allowAnonymous?: boolean;
349
+ requireRole?: boolean;
350
+ customRoles?: Record<string, RoleDefinition>;
351
+ } = {}
352
+ ): { valid: boolean; errors?: UserContextValidationError[] } {
353
+ const { allowAnonymous = true, requireRole = false, customRoles } = options;
354
+ const errors: UserContextValidationError[] = [];
355
+
356
+ // Validate user ID format (if provided)
357
+ if (input.userId !== undefined && input.userId !== null) {
358
+ if (typeof input.userId !== "string") {
359
+ errors.push({
360
+ code: "INVALID_USER_ID",
361
+ message: "User ID must be a string",
362
+ details: { receivedType: typeof input.userId },
363
+ });
364
+ } else if (input.userId.trim().length === 0) {
365
+ errors.push({
366
+ code: "INVALID_USER_ID",
367
+ message: "User ID cannot be empty",
368
+ });
369
+ }
370
+ }
371
+
372
+ // Check anonymous access
373
+ const isAuthenticated = isValidUserId(input.userId);
374
+ if (!allowAnonymous && !isAuthenticated) {
375
+ errors.push({
376
+ code: "ANONYMOUS_NOT_ALLOWED",
377
+ message: "Authentication is required for this operation",
378
+ });
379
+ }
380
+
381
+ // Validate role if provided
382
+ if (input.role !== undefined && input.role !== null) {
383
+ if (!isValidRole(input.role, customRoles)) {
384
+ errors.push({
385
+ code: "UNKNOWN_ROLE",
386
+ message: `Unknown role: ${input.role}`,
387
+ details: { role: input.role },
388
+ });
389
+ }
390
+ } else if (requireRole) {
391
+ // Role is required but not provided
392
+ errors.push({
393
+ code: "ROLE_REQUIRED",
394
+ message: "A CMS role is required for this operation",
395
+ });
396
+ }
397
+
398
+ return {
399
+ valid: errors.length === 0,
400
+ errors: errors.length > 0 ? errors : undefined,
401
+ };
402
+ }
403
+
404
+ // =============================================================================
405
+ // User Context Creation Functions
406
+ // =============================================================================
407
+
408
+ /**
409
+ * Resolve the user's CMS role using the getUserRole hook.
410
+ *
411
+ * @param ctx - The CMS hook context (provides db and auth access)
412
+ * @param userId - The user ID to look up
413
+ * @param getUserRole - The getUserRole hook from configuration
414
+ * @returns The resolved role name or null
415
+ *
416
+ * @example
417
+ * ```typescript
418
+ * const role = await resolveUserRole(ctx, "user_123", config.getUserRole);
419
+ * console.log(role); // "editor" or null
420
+ * ```
421
+ */
422
+ export async function resolveUserRole(
423
+ ctx: CmsHookContext,
424
+ userId: string,
425
+ getUserRole?: GetUserRoleHook
426
+ ): Promise<GetUserRoleResult> {
427
+ if (!getUserRole) {
428
+ return null;
429
+ }
430
+
431
+ try {
432
+ const context: GetUserRoleContext = { userId };
433
+ return await getUserRole(ctx, context);
434
+ } catch (error) {
435
+ // Re-throw with context for better error handling
436
+ const message = error instanceof Error ? error.message : "Unknown error";
437
+ throw new UserContextError({
438
+ code: "HOOK_ERROR",
439
+ message: `getUserRole hook failed: ${message}`,
440
+ details: { userId, originalError: message },
441
+ });
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Create a validated user context from input.
447
+ *
448
+ * This is the main function for converting raw user input into a validated
449
+ * UserContext that can be used throughout CMS operations. It handles:
450
+ * - User ID validation
451
+ * - Role resolution via the getUserRole hook
452
+ * - Role validation against built-in and custom roles
453
+ * - Access control checks (anonymous, role requirements)
454
+ *
455
+ * @param options - Options including input and hooks
456
+ * @returns Validated UserContext
457
+ * @throws UserContextError if validation fails
458
+ *
459
+ * @example
460
+ * ```typescript
461
+ * // Basic usage with getUserRole hook
462
+ * const context = await createUserContext({
463
+ * input: { userId: identity?.subject },
464
+ * getUserRole: config.getUserRole,
465
+ * });
466
+ *
467
+ * // With pre-resolved role (skips hook)
468
+ * const context = await createUserContext({
469
+ * input: { userId: "user_123", role: "editor" },
470
+ * });
471
+ *
472
+ * // Require authentication
473
+ * const context = await createUserContext({
474
+ * input: { userId },
475
+ * allowAnonymous: false,
476
+ * });
477
+ * ```
478
+ */
479
+ export async function createUserContext(
480
+ options: CreateUserContextOptions
481
+ ): Promise<UserContext> {
482
+ const {
483
+ ctx,
484
+ input,
485
+ getUserRole,
486
+ customRoles,
487
+ allowAnonymous = true,
488
+ requireRole = false,
489
+ } = options;
490
+
491
+ // Validate input
492
+ const validation = validateUserContextInput(input, {
493
+ allowAnonymous,
494
+ requireRole: false, // We'll check this after resolving the role
495
+ customRoles,
496
+ });
497
+
498
+ if (!validation.valid && validation.errors) {
499
+ throw new UserContextError(validation.errors[0]);
500
+ }
501
+
502
+ // Normalize userId
503
+ const userId = isValidUserId(input.userId) ? input.userId : undefined;
504
+ const isAuthenticated = userId !== undefined;
505
+
506
+ // Resolve role
507
+ let role: string | null = null;
508
+
509
+ if (input.role !== undefined && input.role !== null) {
510
+ // Use pre-provided role
511
+ role = input.role;
512
+ } else if (userId && getUserRole && ctx) {
513
+ // Resolve role via hook
514
+ role = await resolveUserRole(ctx, userId, getUserRole);
515
+ }
516
+
517
+ // Validate resolved role
518
+ const roleDefinition = role ? getRole(role, customRoles) ?? undefined : undefined;
519
+ const hasRole = roleDefinition !== undefined;
520
+
521
+ // Check role requirement
522
+ if (requireRole && !hasRole) {
523
+ throw new UserContextError({
524
+ code: "ROLE_REQUIRED",
525
+ message: isAuthenticated
526
+ ? `User ${userId} has no CMS role assigned`
527
+ : "Authentication and a CMS role are required",
528
+ details: { userId, resolvedRole: role },
529
+ });
530
+ }
531
+
532
+ // Warn about unknown roles (but don't fail if requireRole is false)
533
+ if (role !== null && !hasRole) {
534
+ // Role was provided but doesn't exist - this is logged but not an error
535
+ // unless requireRole is true (handled above)
536
+ console.warn(`Unknown CMS role "${role}" for user ${userId ?? "anonymous"}`);
537
+ }
538
+
539
+ return {
540
+ userId,
541
+ role,
542
+ isAuthenticated,
543
+ hasRole,
544
+ roleDefinition,
545
+ email: input.email,
546
+ displayName: input.displayName,
547
+ metadata: input.metadata,
548
+ };
549
+ }
550
+
551
+ /**
552
+ * Create a user context synchronously when the role is already known.
553
+ * Use this when you've already resolved the role or want to skip hook execution.
554
+ *
555
+ * @param input - User context input with role pre-resolved
556
+ * @param customRoles - Optional custom role definitions
557
+ * @returns Validated UserContext
558
+ *
559
+ * @example
560
+ * ```typescript
561
+ * // When role is already known
562
+ * const context = createUserContextSync({
563
+ * userId: "user_123",
564
+ * role: "editor",
565
+ * });
566
+ * ```
567
+ */
568
+ export function createUserContextSync(
569
+ input: UserContextInput,
570
+ customRoles?: Record<string, RoleDefinition>
571
+ ): UserContext {
572
+ const userId = isValidUserId(input.userId) ? input.userId : undefined;
573
+ const role = input.role ?? null;
574
+ const roleDefinition = role ? getRole(role, customRoles) ?? undefined : undefined;
575
+
576
+ return {
577
+ userId,
578
+ role,
579
+ isAuthenticated: userId !== undefined,
580
+ hasRole: roleDefinition !== undefined,
581
+ roleDefinition,
582
+ email: input.email,
583
+ displayName: input.displayName,
584
+ metadata: input.metadata,
585
+ };
586
+ }
587
+
588
+ // =============================================================================
589
+ // User ID Extraction Functions
590
+ // =============================================================================
591
+
592
+ /**
593
+ * Extract user ID from various input formats.
594
+ * Handles common patterns from different auth systems.
595
+ *
596
+ * @param input - The input to extract userId from
597
+ * @returns The extracted userId or undefined
598
+ *
599
+ * @example
600
+ * ```typescript
601
+ * // From Convex identity
602
+ * extractUserId(identity); // uses identity.subject
603
+ *
604
+ * // From string
605
+ * extractUserId("user_123"); // "user_123"
606
+ *
607
+ * // From object with common fields
608
+ * extractUserId({ sub: "user_123" }); // "user_123"
609
+ * extractUserId({ userId: "user_123" }); // "user_123"
610
+ * extractUserId({ id: "user_123" }); // "user_123"
611
+ * ```
612
+ */
613
+ export function extractUserId(
614
+ input: string | { subject?: string; sub?: string; userId?: string; id?: string; _id?: string } | null | undefined
615
+ ): string | undefined {
616
+ if (input === null || input === undefined) {
617
+ return undefined;
618
+ }
619
+
620
+ // String input
621
+ if (typeof input === "string") {
622
+ return isValidUserId(input) ? input : undefined;
623
+ }
624
+
625
+ // Object input - try common fields in order of priority
626
+ const candidates = [
627
+ input.subject, // Convex identity
628
+ input.sub, // JWT standard
629
+ input.userId, // Common custom field
630
+ input.id, // Generic ID field
631
+ input._id, // MongoDB-style
632
+ ];
633
+
634
+ for (const candidate of candidates) {
635
+ if (isValidUserId(candidate)) {
636
+ return candidate;
637
+ }
638
+ }
639
+
640
+ return undefined;
641
+ }
642
+
643
+ /**
644
+ * Extract user ID from a Convex auth context.
645
+ * This is a convenience wrapper for extractUserId that works with ctx.auth.
646
+ *
647
+ * @param authContext - The auth context from ctx.auth
648
+ * @returns The user ID or undefined if not authenticated
649
+ *
650
+ * @example
651
+ * ```typescript
652
+ * export const myMutation = mutation({
653
+ * handler: async (ctx, args) => {
654
+ * const userId = await extractUserIdFromAuth(ctx.auth);
655
+ * if (!userId) {
656
+ * throw new Error("Authentication required");
657
+ * }
658
+ * // ... use userId
659
+ * },
660
+ * });
661
+ * ```
662
+ */
663
+ export async function extractUserIdFromAuth(
664
+ authContext: { getUserIdentity: () => Promise<{ subject?: string } | null> }
665
+ ): Promise<string | undefined> {
666
+ const identity = await authContext.getUserIdentity();
667
+ return extractUserId(identity);
668
+ }
669
+
670
+ // =============================================================================
671
+ // Authorization Context Builders
672
+ // =============================================================================
673
+
674
+ /**
675
+ * Build an AuthorizationHookContext from a UserContext and operation details.
676
+ * This creates the context object used by authorization hooks.
677
+ *
678
+ * @param userContext - The validated user context
679
+ * @param operation - The CMS operation being performed
680
+ * @param resourceInfo - Additional resource information
681
+ * @returns AuthorizationHookContext for hook execution
682
+ *
683
+ * @example
684
+ * ```typescript
685
+ * const hookContext = buildAuthorizationContext(
686
+ * userContext,
687
+ * "contentEntries.update",
688
+ * {
689
+ * resourceId: entry._id,
690
+ * resourceOwnerId: entry.createdBy,
691
+ * contentTypeId: entry.contentTypeId,
692
+ * contentTypeName: contentType.name,
693
+ * }
694
+ * );
695
+ * ```
696
+ */
697
+ export function buildAuthorizationContext(
698
+ userContext: UserContext,
699
+ operation: CmsOperation,
700
+ resourceInfo?: {
701
+ resourceId?: string;
702
+ resourceOwnerId?: string;
703
+ contentTypeId?: string;
704
+ contentTypeName?: string;
705
+ operationData?: Record<string, unknown>;
706
+ }
707
+ ): Omit<AuthorizationHookContext, "ctx"> {
708
+ return {
709
+ operation,
710
+ userId: userContext.userId,
711
+ role: userContext.role,
712
+ resourceId: resourceInfo?.resourceId,
713
+ resourceOwnerId: resourceInfo?.resourceOwnerId,
714
+ contentTypeId: resourceInfo?.contentTypeId,
715
+ contentTypeName: resourceInfo?.contentTypeName,
716
+ operationData: {
717
+ ...resourceInfo?.operationData,
718
+ // Include user context metadata for custom hooks
719
+ _userMetadata: userContext.metadata,
720
+ },
721
+ };
722
+ }
723
+
724
+ /**
725
+ * Create a minimal user context for anonymous operations.
726
+ * Use this for public read operations that don't require authentication.
727
+ *
728
+ * @returns A UserContext representing an anonymous user
729
+ *
730
+ * @example
731
+ * ```typescript
732
+ * // For public content queries
733
+ * const context = createAnonymousContext();
734
+ * // context.isAuthenticated === false
735
+ * // context.hasRole === false
736
+ * ```
737
+ */
738
+ export function createAnonymousContext(): UserContext {
739
+ return {
740
+ userId: undefined,
741
+ role: null,
742
+ isAuthenticated: false,
743
+ hasRole: false,
744
+ };
745
+ }
746
+
747
+ /**
748
+ * Create a system context for internal operations.
749
+ * This bypasses normal user authentication for system-level operations.
750
+ * Use with caution - only for trusted internal operations.
751
+ *
752
+ * @param systemId - Optional identifier for the system operation
753
+ * @returns A UserContext representing a system operation
754
+ *
755
+ * @example
756
+ * ```typescript
757
+ * // For scheduled jobs or internal migrations
758
+ * const context = createSystemContext("scheduled-publisher");
759
+ * // context.userId === "_system:scheduled-publisher"
760
+ * // context.role === "admin" (full access)
761
+ * ```
762
+ */
763
+ export function createSystemContext(systemId?: string): UserContext {
764
+ const userId = systemId ? `_system:${systemId}` : "_system";
765
+ return {
766
+ userId,
767
+ role: "admin",
768
+ isAuthenticated: true,
769
+ hasRole: true,
770
+ metadata: {
771
+ isSystemContext: true,
772
+ systemId,
773
+ },
774
+ };
775
+ }
776
+
777
+ // =============================================================================
778
+ // Utility Functions
779
+ // =============================================================================
780
+
781
+ /**
782
+ * Check if a user context represents an authenticated user.
783
+ *
784
+ * @param context - The user context to check
785
+ * @returns True if the user is authenticated
786
+ */
787
+ export function isAuthenticated(context: UserContext): boolean {
788
+ return context.isAuthenticated && context.userId !== undefined;
789
+ }
790
+
791
+ /**
792
+ * Check if a user context has a specific role.
793
+ *
794
+ * @param context - The user context to check
795
+ * @param roleName - The role name to check for
796
+ * @returns True if the user has the specified role
797
+ */
798
+ export function hasUserRole(context: UserContext, roleName: string): boolean {
799
+ return context.role === roleName && context.hasRole;
800
+ }
801
+
802
+ /**
803
+ * Check if a user context represents a system operation.
804
+ *
805
+ * @param context - The user context to check
806
+ * @returns True if this is a system context
807
+ */
808
+ export function isSystemContext(context: UserContext): boolean {
809
+ return context.metadata?.isSystemContext === true;
810
+ }
811
+
812
+ /**
813
+ * Get the display identifier for a user context.
814
+ * Useful for logging and audit trails.
815
+ *
816
+ * @param context - The user context
817
+ * @returns A human-readable identifier for the user
818
+ */
819
+ export function getUserDisplayId(context: UserContext): string {
820
+ if (context.displayName) {
821
+ return context.displayName;
822
+ }
823
+ if (context.email) {
824
+ return context.email;
825
+ }
826
+ if (context.userId) {
827
+ if (context.userId.startsWith("_system")) {
828
+ return `System (${context.userId.replace("_system:", "")})`;
829
+ }
830
+ return context.userId;
831
+ }
832
+ return "Anonymous";
833
+ }
834
+
835
+ /**
836
+ * Validate that a user context meets minimum requirements for an operation.
837
+ * Returns validation result with specific error messages.
838
+ *
839
+ * @param context - The user context to validate
840
+ * @param requirements - The requirements to check
841
+ * @returns Validation result
842
+ *
843
+ * @example
844
+ * ```typescript
845
+ * const result = validateUserContext(context, {
846
+ * requireAuthentication: true,
847
+ * requireRole: true,
848
+ * allowedRoles: ["admin", "editor"],
849
+ * });
850
+ *
851
+ * if (!result.valid) {
852
+ * throw new Error(result.errors[0].message);
853
+ * }
854
+ * ```
855
+ */
856
+ export function validateUserContext(
857
+ context: UserContext,
858
+ requirements: {
859
+ requireAuthentication?: boolean;
860
+ requireRole?: boolean;
861
+ allowedRoles?: string[];
862
+ }
863
+ ): UserContextValidationResult {
864
+ const errors: UserContextValidationError[] = [];
865
+
866
+ if (requirements.requireAuthentication && !context.isAuthenticated) {
867
+ errors.push({
868
+ code: "ANONYMOUS_NOT_ALLOWED",
869
+ message: "Authentication is required for this operation",
870
+ });
871
+ }
872
+
873
+ if (requirements.requireRole && !context.hasRole) {
874
+ errors.push({
875
+ code: "ROLE_REQUIRED",
876
+ message: "A valid CMS role is required for this operation",
877
+ });
878
+ }
879
+
880
+ if (requirements.allowedRoles && context.role) {
881
+ if (!requirements.allowedRoles.includes(context.role)) {
882
+ errors.push({
883
+ code: "UNKNOWN_ROLE",
884
+ message: `Role "${context.role}" is not allowed for this operation`,
885
+ details: {
886
+ allowedRoles: requirements.allowedRoles,
887
+ actualRole: context.role,
888
+ },
889
+ });
890
+ }
891
+ }
892
+
893
+ if (errors.length > 0) {
894
+ return { valid: false, errors };
895
+ }
896
+
897
+ return { valid: true, context };
898
+ }