@simple-auth-kit/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (820) hide show
  1. package/README.md +88 -0
  2. package/bin/simple-auth-kit.mjs +18 -0
  3. package/lib/copy.ts +190 -0
  4. package/lib/rename-native.ts +173 -0
  5. package/package.json +31 -0
  6. package/registry/README.md +312 -0
  7. package/registry/admin-apps/README.md +71 -0
  8. package/registry/admin-apps/nextjs/shared/components.json +25 -0
  9. package/registry/admin-apps/nextjs/shared/next.config.ts +9 -0
  10. package/registry/admin-apps/nextjs/shared/postcss.config.mjs +7 -0
  11. package/registry/admin-apps/nextjs/shared/src/app/globals.css +159 -0
  12. package/registry/admin-apps/nextjs/shared/src/app/signup/page.tsx +72 -0
  13. package/registry/admin-apps/nextjs/shared/src/components/alert-modal.tsx +32 -0
  14. package/registry/admin-apps/nextjs/shared/src/components/breadcrumb.tsx +48 -0
  15. package/registry/admin-apps/nextjs/shared/src/components/form-error-alert.tsx +24 -0
  16. package/registry/admin-apps/nextjs/shared/src/components/loader/skeleton.tsx +39 -0
  17. package/registry/admin-apps/nextjs/shared/src/components/loader/table-skeleton-loader.tsx +57 -0
  18. package/registry/admin-apps/nextjs/shared/src/components/permission-group-select.tsx +101 -0
  19. package/registry/admin-apps/nextjs/shared/src/components/permission-group-view.tsx +53 -0
  20. package/registry/admin-apps/nextjs/shared/src/components/role-multi-select.tsx +71 -0
  21. package/registry/admin-apps/nextjs/shared/src/components/theme-toggle.tsx +26 -0
  22. package/registry/admin-apps/nextjs/shared/src/components/ui/alert.tsx +78 -0
  23. package/registry/admin-apps/nextjs/shared/src/components/ui/avatar.tsx +53 -0
  24. package/registry/admin-apps/nextjs/shared/src/components/ui/badge.tsx +51 -0
  25. package/registry/admin-apps/nextjs/shared/src/components/ui/breadcrumb.tsx +109 -0
  26. package/registry/admin-apps/nextjs/shared/src/components/ui/button.tsx +67 -0
  27. package/registry/admin-apps/nextjs/shared/src/components/ui/card.tsx +103 -0
  28. package/registry/admin-apps/nextjs/shared/src/components/ui/checkbox.tsx +32 -0
  29. package/registry/admin-apps/nextjs/shared/src/components/ui/collapsible.tsx +33 -0
  30. package/registry/admin-apps/nextjs/shared/src/components/ui/data-table.tsx +107 -0
  31. package/registry/admin-apps/nextjs/shared/src/components/ui/dialog.tsx +143 -0
  32. package/registry/admin-apps/nextjs/shared/src/components/ui/dropdown-menu.tsx +234 -0
  33. package/registry/admin-apps/nextjs/shared/src/components/ui/heading.tsx +16 -0
  34. package/registry/admin-apps/nextjs/shared/src/components/ui/input.tsx +19 -0
  35. package/registry/admin-apps/nextjs/shared/src/components/ui/label.tsx +24 -0
  36. package/registry/admin-apps/nextjs/shared/src/components/ui/modal.tsx +29 -0
  37. package/registry/admin-apps/nextjs/shared/src/components/ui/select.tsx +192 -0
  38. package/registry/admin-apps/nextjs/shared/src/components/ui/separator.tsx +28 -0
  39. package/registry/admin-apps/nextjs/shared/src/components/ui/sheet.tsx +135 -0
  40. package/registry/admin-apps/nextjs/shared/src/components/ui/sidebar.tsx +567 -0
  41. package/registry/admin-apps/nextjs/shared/src/components/ui/skeleton.tsx +13 -0
  42. package/registry/admin-apps/nextjs/shared/src/components/ui/sonner.tsx +34 -0
  43. package/registry/admin-apps/nextjs/shared/src/components/ui/table.tsx +116 -0
  44. package/registry/admin-apps/nextjs/shared/src/components/ui/tooltip.tsx +61 -0
  45. package/registry/admin-apps/nextjs/shared/src/hooks/use-live-audit-feed.ts +51 -0
  46. package/registry/admin-apps/nextjs/shared/src/hooks/use-mobile.ts +17 -0
  47. package/registry/admin-apps/nextjs/shared/src/lib/error.ts +23 -0
  48. package/registry/admin-apps/nextjs/shared/src/lib/query-client.tsx +10 -0
  49. package/registry/admin-apps/nextjs/shared/src/lib/token-storage.ts +36 -0
  50. package/registry/admin-apps/nextjs/shared/src/lib/utils.ts +6 -0
  51. package/registry/admin-apps/nextjs/shared/tsconfig.json +41 -0
  52. package/registry/admin-apps/nextjs/variants/base/.env.example +9 -0
  53. package/registry/admin-apps/nextjs/variants/base/Dockerfile +32 -0
  54. package/registry/admin-apps/nextjs/variants/base/package.json +58 -0
  55. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/account/change-password/page.tsx +163 -0
  56. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/account/page.tsx +569 -0
  57. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/audit-log/page.tsx +180 -0
  58. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/countries/[id]/edit/page.tsx +357 -0
  59. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/countries/[id]/page.tsx +191 -0
  60. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/countries/country-schema.ts +18 -0
  61. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/countries/new/page.tsx +305 -0
  62. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/countries/page.tsx +367 -0
  63. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/customers/[id]/edit/page.tsx +386 -0
  64. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/customers/[id]/page.tsx +213 -0
  65. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/customers/customer-schema.ts +35 -0
  66. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/customers/new/page.tsx +334 -0
  67. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/customers/page.tsx +400 -0
  68. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/dashboard/page.tsx +263 -0
  69. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/languages/[id]/edit/page.tsx +275 -0
  70. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/languages/[id]/page.tsx +183 -0
  71. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/languages/language-schema.ts +20 -0
  72. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/languages/new/page.tsx +226 -0
  73. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/languages/page.tsx +342 -0
  74. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/layout.tsx +50 -0
  75. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/permissions/[id]/edit/page.tsx +309 -0
  76. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/permissions/[id]/page.tsx +119 -0
  77. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/permissions/new/page.tsx +275 -0
  78. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/permissions/page.tsx +311 -0
  79. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/permissions/permission-schema.ts +44 -0
  80. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/roles/[id]/edit/page.tsx +276 -0
  81. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/roles/[id]/page.tsx +179 -0
  82. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/roles/new/page.tsx +241 -0
  83. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/roles/page.tsx +325 -0
  84. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/roles/role-schema.ts +20 -0
  85. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/users/[id]/edit/page.tsx +411 -0
  86. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/users/[id]/page.tsx +253 -0
  87. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/users/new/page.tsx +444 -0
  88. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/users/page.tsx +410 -0
  89. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/users/user-schema.ts +37 -0
  90. package/registry/admin-apps/nextjs/variants/base/src/app/api/auth/[...nextauth]/route.ts +3 -0
  91. package/registry/admin-apps/nextjs/variants/base/src/app/icon.svg +5 -0
  92. package/registry/admin-apps/nextjs/variants/base/src/app/layout.tsx +31 -0
  93. package/registry/admin-apps/nextjs/variants/base/src/app/login/page.tsx +162 -0
  94. package/registry/admin-apps/nextjs/variants/base/src/app/page.tsx +22 -0
  95. package/registry/admin-apps/nextjs/variants/base/src/auth.ts +105 -0
  96. package/registry/admin-apps/nextjs/variants/base/src/components/app-sidebar.tsx +182 -0
  97. package/registry/admin-apps/nextjs/variants/base/src/components/nav-user.tsx +99 -0
  98. package/registry/admin-apps/nextjs/variants/base/src/components/permission-required.tsx +32 -0
  99. package/registry/admin-apps/nextjs/variants/base/src/components/photo-upload.tsx +203 -0
  100. package/registry/admin-apps/nextjs/variants/base/src/components/ui/accordion.tsx +66 -0
  101. package/registry/admin-apps/nextjs/variants/base/src/components/ui/alert-dialog.tsx +157 -0
  102. package/registry/admin-apps/nextjs/variants/base/src/components/ui/button-group.tsx +83 -0
  103. package/registry/admin-apps/nextjs/variants/base/src/components/ui/calendar.tsx +218 -0
  104. package/registry/admin-apps/nextjs/variants/base/src/components/ui/carousel.tsx +241 -0
  105. package/registry/admin-apps/nextjs/variants/base/src/components/ui/combobox.tsx +243 -0
  106. package/registry/admin-apps/nextjs/variants/base/src/components/ui/command.tsx +184 -0
  107. package/registry/admin-apps/nextjs/variants/base/src/components/ui/context-menu.tsx +252 -0
  108. package/registry/admin-apps/nextjs/variants/base/src/components/ui/date-picker.tsx +323 -0
  109. package/registry/admin-apps/nextjs/variants/base/src/components/ui/datetime-picker.tsx +896 -0
  110. package/registry/admin-apps/nextjs/variants/base/src/components/ui/drawer.tsx +135 -0
  111. package/registry/admin-apps/nextjs/variants/base/src/components/ui/empty.tsx +104 -0
  112. package/registry/admin-apps/nextjs/variants/base/src/components/ui/field.tsx +248 -0
  113. package/registry/admin-apps/nextjs/variants/base/src/components/ui/form.tsx +168 -0
  114. package/registry/admin-apps/nextjs/variants/base/src/components/ui/hover-card.tsx +44 -0
  115. package/registry/admin-apps/nextjs/variants/base/src/components/ui/input-group.tsx +170 -0
  116. package/registry/admin-apps/nextjs/variants/base/src/components/ui/input-otp.tsx +77 -0
  117. package/registry/admin-apps/nextjs/variants/base/src/components/ui/item.tsx +193 -0
  118. package/registry/admin-apps/nextjs/variants/base/src/components/ui/kbd.tsx +28 -0
  119. package/registry/admin-apps/nextjs/variants/base/src/components/ui/menubar.tsx +276 -0
  120. package/registry/admin-apps/nextjs/variants/base/src/components/ui/multi-select.tsx +381 -0
  121. package/registry/admin-apps/nextjs/variants/base/src/components/ui/multi-selector.tsx +641 -0
  122. package/registry/admin-apps/nextjs/variants/base/src/components/ui/native-select.tsx +48 -0
  123. package/registry/admin-apps/nextjs/variants/base/src/components/ui/navigation-menu.tsx +168 -0
  124. package/registry/admin-apps/nextjs/variants/base/src/components/ui/pagination.tsx +127 -0
  125. package/registry/admin-apps/nextjs/variants/base/src/components/ui/popover.tsx +48 -0
  126. package/registry/admin-apps/nextjs/variants/base/src/components/ui/progress.tsx +31 -0
  127. package/registry/admin-apps/nextjs/variants/base/src/components/ui/radio-group.tsx +45 -0
  128. package/registry/admin-apps/nextjs/variants/base/src/components/ui/resizable.tsx +55 -0
  129. package/registry/admin-apps/nextjs/variants/base/src/components/ui/scroll-area.tsx +58 -0
  130. package/registry/admin-apps/nextjs/variants/base/src/components/ui/spinner.tsx +27 -0
  131. package/registry/admin-apps/nextjs/variants/base/src/components/ui/switch.tsx +31 -0
  132. package/registry/admin-apps/nextjs/variants/base/src/components/ui/tabs.tsx +66 -0
  133. package/registry/admin-apps/nextjs/variants/base/src/components/ui/textarea.tsx +18 -0
  134. package/registry/admin-apps/nextjs/variants/base/src/components/ui/toggle-group.tsx +83 -0
  135. package/registry/admin-apps/nextjs/variants/base/src/components/ui/toggle.tsx +47 -0
  136. package/registry/admin-apps/nextjs/variants/base/src/components/user-menu.tsx +61 -0
  137. package/registry/admin-apps/nextjs/variants/base/src/lib/ability.ts +90 -0
  138. package/registry/admin-apps/nextjs/variants/base/src/lib/auth-client.ts +10 -0
  139. package/registry/admin-apps/nextjs/variants/base/src/lib/env.ts +10 -0
  140. package/registry/admin-apps/nextjs/variants/base/src/lib/server-token-storage.ts +33 -0
  141. package/registry/admin-apps/nextjs/variants/base/src/lib/stores/auth-store.ts +117 -0
  142. package/registry/admin-apps/nextjs/variants/base/src/lib/stores/store-context.tsx +50 -0
  143. package/registry/admin-apps/nextjs/variants/base/src/proxy.ts +81 -0
  144. package/registry/admin-apps/nextjs/variants/base/src/routes.ts +7 -0
  145. package/registry/admin-apps/nextjs/variants/workspaces/.env.example +7 -0
  146. package/registry/admin-apps/nextjs/variants/workspaces/Dockerfile +32 -0
  147. package/registry/admin-apps/nextjs/variants/workspaces/package.json +48 -0
  148. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/account/page.tsx +360 -0
  149. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/audit-log/page.tsx +185 -0
  150. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/countries/[id]/edit/page.tsx +296 -0
  151. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/countries/[id]/page.tsx +194 -0
  152. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/countries/country-schema.ts +27 -0
  153. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/countries/new/page.tsx +242 -0
  154. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/countries/page.tsx +373 -0
  155. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/customers/[id]/edit/page.tsx +306 -0
  156. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/customers/[id]/page.tsx +219 -0
  157. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/customers/customer-schema.ts +36 -0
  158. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/customers/new/page.tsx +251 -0
  159. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/customers/page.tsx +406 -0
  160. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/dashboard/page.tsx +146 -0
  161. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/languages/[id]/edit/page.tsx +240 -0
  162. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/languages/[id]/page.tsx +186 -0
  163. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/languages/language-schema.ts +26 -0
  164. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/languages/new/page.tsx +189 -0
  165. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/languages/page.tsx +348 -0
  166. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/layout.tsx +96 -0
  167. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/members/page.tsx +263 -0
  168. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/permissions/[id]/edit/page.tsx +264 -0
  169. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/permissions/[id]/page.tsx +126 -0
  170. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/permissions/new/page.tsx +238 -0
  171. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/permissions/page.tsx +294 -0
  172. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/permissions/permission-schema.ts +46 -0
  173. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/roles/[id]/edit/page.tsx +248 -0
  174. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/roles/[id]/page.tsx +186 -0
  175. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/roles/new/page.tsx +205 -0
  176. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/roles/page.tsx +485 -0
  177. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/roles/role-schema.ts +23 -0
  178. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/users/[id]/page.tsx +521 -0
  179. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/users/new/page.tsx +342 -0
  180. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/users/page.tsx +414 -0
  181. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/users/user-schema.ts +44 -0
  182. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/workspaces/new/page.tsx +66 -0
  183. package/registry/admin-apps/nextjs/variants/workspaces/src/app/layout.tsx +31 -0
  184. package/registry/admin-apps/nextjs/variants/workspaces/src/app/login/page.tsx +110 -0
  185. package/registry/admin-apps/nextjs/variants/workspaces/src/app/page.tsx +33 -0
  186. package/registry/admin-apps/nextjs/variants/workspaces/src/components/app-sidebar.tsx +180 -0
  187. package/registry/admin-apps/nextjs/variants/workspaces/src/components/change-password-dialog.tsx +119 -0
  188. package/registry/admin-apps/nextjs/variants/workspaces/src/components/nav-user.tsx +105 -0
  189. package/registry/admin-apps/nextjs/variants/workspaces/src/components/permission-required.tsx +32 -0
  190. package/registry/admin-apps/nextjs/variants/workspaces/src/components/photo-upload.tsx +226 -0
  191. package/registry/admin-apps/nextjs/variants/workspaces/src/components/ui/calendar.tsx +218 -0
  192. package/registry/admin-apps/nextjs/variants/workspaces/src/components/ui/datetime-picker.tsx +896 -0
  193. package/registry/admin-apps/nextjs/variants/workspaces/src/components/ui/popover.tsx +48 -0
  194. package/registry/admin-apps/nextjs/variants/workspaces/src/components/user-menu.tsx +67 -0
  195. package/registry/admin-apps/nextjs/variants/workspaces/src/components/workspace-switcher.tsx +57 -0
  196. package/registry/admin-apps/nextjs/variants/workspaces/src/lib/ability.ts +101 -0
  197. package/registry/admin-apps/nextjs/variants/workspaces/src/lib/active-workspace.ts +38 -0
  198. package/registry/admin-apps/nextjs/variants/workspaces/src/lib/auth-client.ts +17 -0
  199. package/registry/admin-apps/nextjs/variants/workspaces/src/lib/env.ts +4 -0
  200. package/registry/admin-apps/nextjs/variants/workspaces/src/lib/stores/auth-store.ts +139 -0
  201. package/registry/admin-apps/nextjs/variants/workspaces/src/lib/stores/store-context.tsx +66 -0
  202. package/registry/admin-apps/nextjs/variants/workspaces/src/lib/stores/workspace-store.ts +89 -0
  203. package/registry/admin-apps/react/shared/components.json +25 -0
  204. package/registry/admin-apps/react/shared/postcss.config.js +5 -0
  205. package/registry/admin-apps/react/shared/src/components/alert-modal.tsx +30 -0
  206. package/registry/admin-apps/react/shared/src/components/loader/skeleton.tsx +39 -0
  207. package/registry/admin-apps/react/shared/src/components/loader/table-skeleton-loader.tsx +57 -0
  208. package/registry/admin-apps/react/shared/src/components/nav-user.tsx +103 -0
  209. package/registry/admin-apps/react/shared/src/components/role-multi-select.tsx +69 -0
  210. package/registry/admin-apps/react/shared/src/components/theme-toggle.tsx +24 -0
  211. package/registry/admin-apps/react/shared/src/components/ui/accordion.tsx +66 -0
  212. package/registry/admin-apps/react/shared/src/components/ui/alert-dialog.tsx +157 -0
  213. package/registry/admin-apps/react/shared/src/components/ui/alert.tsx +78 -0
  214. package/registry/admin-apps/react/shared/src/components/ui/avatar.tsx +53 -0
  215. package/registry/admin-apps/react/shared/src/components/ui/badge.tsx +51 -0
  216. package/registry/admin-apps/react/shared/src/components/ui/breadcrumb.tsx +109 -0
  217. package/registry/admin-apps/react/shared/src/components/ui/button-group.tsx +83 -0
  218. package/registry/admin-apps/react/shared/src/components/ui/button.tsx +67 -0
  219. package/registry/admin-apps/react/shared/src/components/ui/calendar.tsx +218 -0
  220. package/registry/admin-apps/react/shared/src/components/ui/card.tsx +103 -0
  221. package/registry/admin-apps/react/shared/src/components/ui/carousel.tsx +241 -0
  222. package/registry/admin-apps/react/shared/src/components/ui/checkbox.tsx +32 -0
  223. package/registry/admin-apps/react/shared/src/components/ui/collapsible.tsx +33 -0
  224. package/registry/admin-apps/react/shared/src/components/ui/context-menu.tsx +252 -0
  225. package/registry/admin-apps/react/shared/src/components/ui/data-table.tsx +107 -0
  226. package/registry/admin-apps/react/shared/src/components/ui/datetime-picker.tsx +896 -0
  227. package/registry/admin-apps/react/shared/src/components/ui/dialog.tsx +143 -0
  228. package/registry/admin-apps/react/shared/src/components/ui/drawer.tsx +135 -0
  229. package/registry/admin-apps/react/shared/src/components/ui/dropdown-menu.tsx +234 -0
  230. package/registry/admin-apps/react/shared/src/components/ui/empty.tsx +104 -0
  231. package/registry/admin-apps/react/shared/src/components/ui/field.tsx +248 -0
  232. package/registry/admin-apps/react/shared/src/components/ui/form.tsx +168 -0
  233. package/registry/admin-apps/react/shared/src/components/ui/heading.tsx +16 -0
  234. package/registry/admin-apps/react/shared/src/components/ui/hover-card.tsx +44 -0
  235. package/registry/admin-apps/react/shared/src/components/ui/input-group.tsx +170 -0
  236. package/registry/admin-apps/react/shared/src/components/ui/input-otp.tsx +77 -0
  237. package/registry/admin-apps/react/shared/src/components/ui/input.tsx +19 -0
  238. package/registry/admin-apps/react/shared/src/components/ui/item.tsx +193 -0
  239. package/registry/admin-apps/react/shared/src/components/ui/kbd.tsx +28 -0
  240. package/registry/admin-apps/react/shared/src/components/ui/label.tsx +24 -0
  241. package/registry/admin-apps/react/shared/src/components/ui/menubar.tsx +276 -0
  242. package/registry/admin-apps/react/shared/src/components/ui/modal.tsx +29 -0
  243. package/registry/admin-apps/react/shared/src/components/ui/multi-select.tsx +381 -0
  244. package/registry/admin-apps/react/shared/src/components/ui/multi-selector.tsx +641 -0
  245. package/registry/admin-apps/react/shared/src/components/ui/native-select.tsx +48 -0
  246. package/registry/admin-apps/react/shared/src/components/ui/navigation-menu.tsx +168 -0
  247. package/registry/admin-apps/react/shared/src/components/ui/pagination.tsx +127 -0
  248. package/registry/admin-apps/react/shared/src/components/ui/popover.tsx +48 -0
  249. package/registry/admin-apps/react/shared/src/components/ui/progress.tsx +31 -0
  250. package/registry/admin-apps/react/shared/src/components/ui/radio-group.tsx +45 -0
  251. package/registry/admin-apps/react/shared/src/components/ui/resizable.tsx +55 -0
  252. package/registry/admin-apps/react/shared/src/components/ui/scroll-area.tsx +58 -0
  253. package/registry/admin-apps/react/shared/src/components/ui/select.tsx +192 -0
  254. package/registry/admin-apps/react/shared/src/components/ui/separator.tsx +28 -0
  255. package/registry/admin-apps/react/shared/src/components/ui/sheet.tsx +135 -0
  256. package/registry/admin-apps/react/shared/src/components/ui/sidebar.tsx +567 -0
  257. package/registry/admin-apps/react/shared/src/components/ui/skeleton.tsx +13 -0
  258. package/registry/admin-apps/react/shared/src/components/ui/spinner.tsx +27 -0
  259. package/registry/admin-apps/react/shared/src/components/ui/switch.tsx +31 -0
  260. package/registry/admin-apps/react/shared/src/components/ui/table.tsx +116 -0
  261. package/registry/admin-apps/react/shared/src/components/ui/tabs.tsx +66 -0
  262. package/registry/admin-apps/react/shared/src/components/ui/textarea.tsx +18 -0
  263. package/registry/admin-apps/react/shared/src/components/ui/toggle-group.tsx +83 -0
  264. package/registry/admin-apps/react/shared/src/components/ui/toggle.tsx +47 -0
  265. package/registry/admin-apps/react/shared/src/components/ui/tooltip.tsx +61 -0
  266. package/registry/admin-apps/react/shared/src/hooks/use-mobile.ts +17 -0
  267. package/registry/admin-apps/react/shared/src/lib/cn.ts +6 -0
  268. package/registry/admin-apps/react/shared/src/lib/error.ts +23 -0
  269. package/registry/admin-apps/react/shared/src/lib/query-client.tsx +8 -0
  270. package/registry/admin-apps/react/shared/src/lib/utils.ts +1 -0
  271. package/registry/admin-apps/react/shared/src/vite-env.d.ts +9 -0
  272. package/registry/admin-apps/react/shared/tsconfig.json +24 -0
  273. package/registry/admin-apps/react/variants/base/.env.example +3 -0
  274. package/registry/admin-apps/react/variants/base/Dockerfile +32 -0
  275. package/registry/admin-apps/react/variants/base/index.html +12 -0
  276. package/registry/admin-apps/react/variants/base/package.json +61 -0
  277. package/registry/admin-apps/react/variants/base/src/App.tsx +103 -0
  278. package/registry/admin-apps/react/variants/base/src/components/app-sidebar.tsx +181 -0
  279. package/registry/admin-apps/react/variants/base/src/components/breadcrumb.tsx +47 -0
  280. package/registry/admin-apps/react/variants/base/src/components/change-password-dialog.tsx +102 -0
  281. package/registry/admin-apps/react/variants/base/src/components/form-error-alert.tsx +23 -0
  282. package/registry/admin-apps/react/variants/base/src/components/layout/AppShell.tsx +36 -0
  283. package/registry/admin-apps/react/variants/base/src/components/layout/RequireAuth.tsx +66 -0
  284. package/registry/admin-apps/react/variants/base/src/components/permission-group-select.tsx +99 -0
  285. package/registry/admin-apps/react/variants/base/src/components/permission-group-view.tsx +53 -0
  286. package/registry/admin-apps/react/variants/base/src/components/photo-upload.tsx +221 -0
  287. package/registry/admin-apps/react/variants/base/src/components/ui/combobox.tsx +243 -0
  288. package/registry/admin-apps/react/variants/base/src/components/ui/command.tsx +177 -0
  289. package/registry/admin-apps/react/variants/base/src/components/ui/date-picker.tsx +322 -0
  290. package/registry/admin-apps/react/variants/base/src/components/ui/sonner.tsx +34 -0
  291. package/registry/admin-apps/react/variants/base/src/components/user-menu.tsx +65 -0
  292. package/registry/admin-apps/react/variants/base/src/hooks/use-live-audit-feed.ts +50 -0
  293. package/registry/admin-apps/react/variants/base/src/index.css +161 -0
  294. package/registry/admin-apps/react/variants/base/src/lib/ability.ts +72 -0
  295. package/registry/admin-apps/react/variants/base/src/lib/auth-client.ts +9 -0
  296. package/registry/admin-apps/react/variants/base/src/lib/theme.tsx +64 -0
  297. package/registry/admin-apps/react/variants/base/src/lib/token-storage.ts +34 -0
  298. package/registry/admin-apps/react/variants/base/src/main.tsx +24 -0
  299. package/registry/admin-apps/react/variants/base/src/pages/AccountPage.tsx +368 -0
  300. package/registry/admin-apps/react/variants/base/src/pages/AddCountryPage.tsx +227 -0
  301. package/registry/admin-apps/react/variants/base/src/pages/AddCustomerPage.tsx +220 -0
  302. package/registry/admin-apps/react/variants/base/src/pages/AddLanguagePage.tsx +167 -0
  303. package/registry/admin-apps/react/variants/base/src/pages/AddRolePage.tsx +187 -0
  304. package/registry/admin-apps/react/variants/base/src/pages/AddUserPage.tsx +328 -0
  305. package/registry/admin-apps/react/variants/base/src/pages/AuditLogPage.tsx +180 -0
  306. package/registry/admin-apps/react/variants/base/src/pages/CountriesPage.tsx +371 -0
  307. package/registry/admin-apps/react/variants/base/src/pages/CountryDetailPage.tsx +209 -0
  308. package/registry/admin-apps/react/variants/base/src/pages/CustomerDetailPage.tsx +231 -0
  309. package/registry/admin-apps/react/variants/base/src/pages/CustomersPage.tsx +404 -0
  310. package/registry/admin-apps/react/variants/base/src/pages/DashboardPage.tsx +190 -0
  311. package/registry/admin-apps/react/variants/base/src/pages/EditCountryPage.tsx +279 -0
  312. package/registry/admin-apps/react/variants/base/src/pages/EditCustomerPage.tsx +272 -0
  313. package/registry/admin-apps/react/variants/base/src/pages/EditLanguagePage.tsx +216 -0
  314. package/registry/admin-apps/react/variants/base/src/pages/EditRolePage.tsx +232 -0
  315. package/registry/admin-apps/react/variants/base/src/pages/EditUserPage.tsx +308 -0
  316. package/registry/admin-apps/react/variants/base/src/pages/LanguageDetailPage.tsx +201 -0
  317. package/registry/admin-apps/react/variants/base/src/pages/LanguagesPage.tsx +346 -0
  318. package/registry/admin-apps/react/variants/base/src/pages/LoginPage.tsx +149 -0
  319. package/registry/admin-apps/react/variants/base/src/pages/PermissionDetailPage.tsx +135 -0
  320. package/registry/admin-apps/react/variants/base/src/pages/PermissionsPage.tsx +331 -0
  321. package/registry/admin-apps/react/variants/base/src/pages/RoleDetailPage.tsx +167 -0
  322. package/registry/admin-apps/react/variants/base/src/pages/RolesPage.tsx +613 -0
  323. package/registry/admin-apps/react/variants/base/src/pages/UserDetailPage.tsx +268 -0
  324. package/registry/admin-apps/react/variants/base/src/pages/UsersPage.tsx +423 -0
  325. package/registry/admin-apps/react/variants/base/src/pages/country-schema.ts +29 -0
  326. package/registry/admin-apps/react/variants/base/src/pages/customer-schema.ts +37 -0
  327. package/registry/admin-apps/react/variants/base/src/pages/language-schema.ts +28 -0
  328. package/registry/admin-apps/react/variants/base/src/pages/user-schema.ts +36 -0
  329. package/registry/admin-apps/react/variants/base/src/stores/auth-store.ts +144 -0
  330. package/registry/admin-apps/react/variants/base/src/stores/store-context.tsx +15 -0
  331. package/registry/admin-apps/react/variants/base/vite.config.ts +15 -0
  332. package/registry/admin-apps/react/variants/workspaces/.env.example +5 -0
  333. package/registry/admin-apps/react/variants/workspaces/Dockerfile +32 -0
  334. package/registry/admin-apps/react/variants/workspaces/index.html +12 -0
  335. package/registry/admin-apps/react/variants/workspaces/package.json +61 -0
  336. package/registry/admin-apps/react/variants/workspaces/src/App.tsx +118 -0
  337. package/registry/admin-apps/react/variants/workspaces/src/components/app-sidebar.tsx +186 -0
  338. package/registry/admin-apps/react/variants/workspaces/src/components/breadcrumb.tsx +39 -0
  339. package/registry/admin-apps/react/variants/workspaces/src/components/change-password-dialog.tsx +111 -0
  340. package/registry/admin-apps/react/variants/workspaces/src/components/form-error-alert.tsx +23 -0
  341. package/registry/admin-apps/react/variants/workspaces/src/components/layout/AppShell.tsx +37 -0
  342. package/registry/admin-apps/react/variants/workspaces/src/components/layout/CreateWorkspaceDialog.tsx +66 -0
  343. package/registry/admin-apps/react/variants/workspaces/src/components/layout/RequireAuth.tsx +107 -0
  344. package/registry/admin-apps/react/variants/workspaces/src/components/layout/WorkspaceSwitcher.tsx +52 -0
  345. package/registry/admin-apps/react/variants/workspaces/src/components/permission-group-select.tsx +99 -0
  346. package/registry/admin-apps/react/variants/workspaces/src/components/permission-group-view.tsx +53 -0
  347. package/registry/admin-apps/react/variants/workspaces/src/components/photo-upload.tsx +223 -0
  348. package/registry/admin-apps/react/variants/workspaces/src/components/ui/combobox.tsx +243 -0
  349. package/registry/admin-apps/react/variants/workspaces/src/components/ui/command.tsx +184 -0
  350. package/registry/admin-apps/react/variants/workspaces/src/components/ui/date-picker.tsx +323 -0
  351. package/registry/admin-apps/react/variants/workspaces/src/components/ui/sonner.tsx +34 -0
  352. package/registry/admin-apps/react/variants/workspaces/src/components/user-menu.tsx +66 -0
  353. package/registry/admin-apps/react/variants/workspaces/src/hooks/use-live-audit-feed.ts +50 -0
  354. package/registry/admin-apps/react/variants/workspaces/src/index.css +162 -0
  355. package/registry/admin-apps/react/variants/workspaces/src/lib/ability.ts +78 -0
  356. package/registry/admin-apps/react/variants/workspaces/src/lib/auth-client.ts +27 -0
  357. package/registry/admin-apps/react/variants/workspaces/src/lib/permission-groups.ts +22 -0
  358. package/registry/admin-apps/react/variants/workspaces/src/lib/theme.tsx +64 -0
  359. package/registry/admin-apps/react/variants/workspaces/src/lib/token-storage.ts +37 -0
  360. package/registry/admin-apps/react/variants/workspaces/src/lib/user-fields.ts +13 -0
  361. package/registry/admin-apps/react/variants/workspaces/src/main.tsx +24 -0
  362. package/registry/admin-apps/react/variants/workspaces/src/pages/AccountPage.tsx +351 -0
  363. package/registry/admin-apps/react/variants/workspaces/src/pages/AddCountryPage.tsx +230 -0
  364. package/registry/admin-apps/react/variants/workspaces/src/pages/AddCustomerPage.tsx +224 -0
  365. package/registry/admin-apps/react/variants/workspaces/src/pages/AddLanguagePage.tsx +170 -0
  366. package/registry/admin-apps/react/variants/workspaces/src/pages/AddUserPage.tsx +297 -0
  367. package/registry/admin-apps/react/variants/workspaces/src/pages/AuditLogPage.tsx +195 -0
  368. package/registry/admin-apps/react/variants/workspaces/src/pages/CountriesPage.tsx +378 -0
  369. package/registry/admin-apps/react/variants/workspaces/src/pages/CountryDetailPage.tsx +215 -0
  370. package/registry/admin-apps/react/variants/workspaces/src/pages/CustomerDetailPage.tsx +237 -0
  371. package/registry/admin-apps/react/variants/workspaces/src/pages/CustomersPage.tsx +411 -0
  372. package/registry/admin-apps/react/variants/workspaces/src/pages/DashboardPage.tsx +191 -0
  373. package/registry/admin-apps/react/variants/workspaces/src/pages/EditCountryPage.tsx +285 -0
  374. package/registry/admin-apps/react/variants/workspaces/src/pages/EditCustomerPage.tsx +279 -0
  375. package/registry/admin-apps/react/variants/workspaces/src/pages/EditLanguagePage.tsx +222 -0
  376. package/registry/admin-apps/react/variants/workspaces/src/pages/LanguageDetailPage.tsx +207 -0
  377. package/registry/admin-apps/react/variants/workspaces/src/pages/LanguagesPage.tsx +353 -0
  378. package/registry/admin-apps/react/variants/workspaces/src/pages/LoginPage.tsx +149 -0
  379. package/registry/admin-apps/react/variants/workspaces/src/pages/MembersPage.tsx +349 -0
  380. package/registry/admin-apps/react/variants/workspaces/src/pages/PermissionDetailPage.tsx +123 -0
  381. package/registry/admin-apps/react/variants/workspaces/src/pages/PermissionsPage.tsx +317 -0
  382. package/registry/admin-apps/react/variants/workspaces/src/pages/RoleDetailPage.tsx +207 -0
  383. package/registry/admin-apps/react/variants/workspaces/src/pages/RolesPage.tsx +617 -0
  384. package/registry/admin-apps/react/variants/workspaces/src/pages/UserDetailPage.tsx +405 -0
  385. package/registry/admin-apps/react/variants/workspaces/src/pages/UsersPage.tsx +425 -0
  386. package/registry/admin-apps/react/variants/workspaces/src/pages/country-schema.ts +29 -0
  387. package/registry/admin-apps/react/variants/workspaces/src/pages/customer-schema.ts +34 -0
  388. package/registry/admin-apps/react/variants/workspaces/src/pages/language-schema.ts +27 -0
  389. package/registry/admin-apps/react/variants/workspaces/src/stores/auth-store.ts +169 -0
  390. package/registry/admin-apps/react/variants/workspaces/src/stores/store-context.tsx +43 -0
  391. package/registry/admin-apps/react/variants/workspaces/src/stores/workspace-store.ts +120 -0
  392. package/registry/admin-apps/react/variants/workspaces/vite.config.ts +15 -0
  393. package/registry/combos/express-drizzle/package.json +32 -0
  394. package/registry/combos/express-drizzle/scripts/materialize.mjs +104 -0
  395. package/registry/combos/express-drizzle/scripts/migrate.mjs +41 -0
  396. package/registry/combos/express-drizzle/scripts/prove-cycle.mjs +29 -0
  397. package/registry/combos/express-drizzle/scripts/seed.mjs +25 -0
  398. package/registry/combos/express-drizzle/shared/drizzle.config.ts +67 -0
  399. package/registry/combos/express-drizzle/shared/src/ability.ts +44 -0
  400. package/registry/combos/express-drizzle/shared/src/auth-core-error.middleware.ts +31 -0
  401. package/registry/combos/express-drizzle/shared/src/auth.config.ts +63 -0
  402. package/registry/combos/express-drizzle/shared/src/auth.middleware.ts +38 -0
  403. package/registry/combos/express-drizzle/shared/src/auth.router.ts +266 -0
  404. package/registry/combos/express-drizzle/shared/src/db.ts +12 -0
  405. package/registry/combos/express-drizzle/shared/src/http-error.ts +18 -0
  406. package/registry/combos/express-drizzle/shared/src/id.helper.ts +22 -0
  407. package/registry/combos/express-drizzle/shared/src/key-provider.ts +52 -0
  408. package/registry/combos/express-drizzle/shared/src/oauth.repository.ts +47 -0
  409. package/registry/combos/express-drizzle/shared/src/openapi-fragment.ts +49 -0
  410. package/registry/combos/express-drizzle/shared/src/openapi-spec.ts +493 -0
  411. package/registry/combos/express-drizzle/shared/src/pagination.ts +30 -0
  412. package/registry/combos/express-drizzle/shared/src/password-reset.repository.ts +35 -0
  413. package/registry/combos/express-drizzle/shared/src/permission-cache.ts +191 -0
  414. package/registry/combos/express-drizzle/shared/src/rate-limit.store.ts +24 -0
  415. package/registry/combos/express-drizzle/shared/src/request-context.ts +35 -0
  416. package/registry/combos/express-drizzle/shared/src/response-envelope.middleware.ts +46 -0
  417. package/registry/combos/express-drizzle/shared/src/route-tiers.ts +148 -0
  418. package/registry/combos/express-drizzle/shared/src/session.repository.ts +130 -0
  419. package/registry/combos/express-drizzle/shared/src/two-factor.repository.ts +38 -0
  420. package/registry/combos/express-drizzle/test/bootstrap.ts +40 -0
  421. package/registry/combos/express-drizzle/test/harness.ts +187 -0
  422. package/registry/combos/express-drizzle/test/prove-cycle.ts +789 -0
  423. package/registry/combos/express-drizzle/tsconfig.json +20 -0
  424. package/registry/combos/express-drizzle/variants/base/.env.example +25 -0
  425. package/registry/combos/express-drizzle/variants/base/drizzle/0000_init.sql +219 -0
  426. package/registry/combos/express-drizzle/variants/base/drizzle/0001_add_is_active_field.sql +1 -0
  427. package/registry/combos/express-drizzle/variants/base/drizzle/meta/0000_snapshot.json +1861 -0
  428. package/registry/combos/express-drizzle/variants/base/drizzle/meta/0001_snapshot.json +1868 -0
  429. package/registry/combos/express-drizzle/variants/base/drizzle/meta/_journal.json +20 -0
  430. package/registry/combos/express-drizzle/variants/base/src/admin.router.ts +307 -0
  431. package/registry/combos/express-drizzle/variants/base/src/audit-log.repository.ts +93 -0
  432. package/registry/combos/express-drizzle/variants/base/src/auth.service.ts +528 -0
  433. package/registry/combos/express-drizzle/variants/base/src/authz.middleware.ts +58 -0
  434. package/registry/combos/express-drizzle/variants/base/src/create-auth-app.ts +78 -0
  435. package/registry/combos/express-drizzle/variants/base/src/openapi-admin.ts +565 -0
  436. package/registry/combos/express-drizzle/variants/base/src/rbac.defaults.ts +218 -0
  437. package/registry/combos/express-drizzle/variants/base/src/rbac.repository.ts +606 -0
  438. package/registry/combos/express-drizzle/variants/base/src/schema.ts +425 -0
  439. package/registry/combos/express-drizzle/variants/base/src/seed.ts +138 -0
  440. package/registry/combos/express-drizzle/variants/base/test/variant-hooks.ts +179 -0
  441. package/registry/combos/express-drizzle/variants/workspaces/.env.example +30 -0
  442. package/registry/combos/express-drizzle/variants/workspaces/drizzle/0000_init.sql +251 -0
  443. package/registry/combos/express-drizzle/variants/workspaces/drizzle/0001_add_is_active_field.sql +1 -0
  444. package/registry/combos/express-drizzle/variants/workspaces/drizzle/meta/0000_snapshot.json +2155 -0
  445. package/registry/combos/express-drizzle/variants/workspaces/drizzle/meta/0001_snapshot.json +2162 -0
  446. package/registry/combos/express-drizzle/variants/workspaces/drizzle/meta/_journal.json +20 -0
  447. package/registry/combos/express-drizzle/variants/workspaces/src/admin.router.ts +323 -0
  448. package/registry/combos/express-drizzle/variants/workspaces/src/audit-log.repository.ts +105 -0
  449. package/registry/combos/express-drizzle/variants/workspaces/src/auth.service.ts +545 -0
  450. package/registry/combos/express-drizzle/variants/workspaces/src/authz.middleware.ts +94 -0
  451. package/registry/combos/express-drizzle/variants/workspaces/src/create-auth-app.ts +87 -0
  452. package/registry/combos/express-drizzle/variants/workspaces/src/openapi-admin.ts +596 -0
  453. package/registry/combos/express-drizzle/variants/workspaces/src/openapi-workspace.ts +183 -0
  454. package/registry/combos/express-drizzle/variants/workspaces/src/rbac.defaults.ts +248 -0
  455. package/registry/combos/express-drizzle/variants/workspaces/src/rbac.repository.ts +698 -0
  456. package/registry/combos/express-drizzle/variants/workspaces/src/schema.ts +487 -0
  457. package/registry/combos/express-drizzle/variants/workspaces/src/seed.ts +173 -0
  458. package/registry/combos/express-drizzle/variants/workspaces/src/workspace.repository.ts +254 -0
  459. package/registry/combos/express-drizzle/variants/workspaces/src/workspace.router.ts +123 -0
  460. package/registry/combos/express-drizzle/variants/workspaces/test/variant-hooks.ts +370 -0
  461. package/registry/combos/express-prisma/package.json +32 -0
  462. package/registry/combos/express-prisma/scripts/materialize.mjs +84 -0
  463. package/registry/combos/express-prisma/scripts/migrate.mjs +32 -0
  464. package/registry/combos/express-prisma/scripts/prove-cycle.mjs +30 -0
  465. package/registry/combos/express-prisma/scripts/seed.mjs +24 -0
  466. package/registry/combos/express-prisma/shared/prisma.config.ts +38 -0
  467. package/registry/combos/express-prisma/shared/src/ability.ts +44 -0
  468. package/registry/combos/express-prisma/shared/src/auth-core-error.middleware.ts +31 -0
  469. package/registry/combos/express-prisma/shared/src/auth.config.ts +63 -0
  470. package/registry/combos/express-prisma/shared/src/auth.middleware.ts +44 -0
  471. package/registry/combos/express-prisma/shared/src/auth.router.ts +269 -0
  472. package/registry/combos/express-prisma/shared/src/http-error.ts +18 -0
  473. package/registry/combos/express-prisma/shared/src/id.helper.ts +22 -0
  474. package/registry/combos/express-prisma/shared/src/key-provider.ts +52 -0
  475. package/registry/combos/express-prisma/shared/src/oauth.repository.ts +33 -0
  476. package/registry/combos/express-prisma/shared/src/openapi-fragment.ts +49 -0
  477. package/registry/combos/express-prisma/shared/src/openapi-spec.ts +487 -0
  478. package/registry/combos/express-prisma/shared/src/pagination.ts +30 -0
  479. package/registry/combos/express-prisma/shared/src/password-reset.repository.ts +29 -0
  480. package/registry/combos/express-prisma/shared/src/permission-cache.ts +191 -0
  481. package/registry/combos/express-prisma/shared/src/rate-limit.store.ts +21 -0
  482. package/registry/combos/express-prisma/shared/src/request-context.ts +35 -0
  483. package/registry/combos/express-prisma/shared/src/response-envelope.middleware.ts +46 -0
  484. package/registry/combos/express-prisma/shared/src/route-tiers.ts +148 -0
  485. package/registry/combos/express-prisma/shared/src/session.repository.ts +140 -0
  486. package/registry/combos/express-prisma/shared/src/two-factor.repository.ts +30 -0
  487. package/registry/combos/express-prisma/test/bootstrap.ts +45 -0
  488. package/registry/combos/express-prisma/test/harness.ts +187 -0
  489. package/registry/combos/express-prisma/test/prove-cycle.ts +793 -0
  490. package/registry/combos/express-prisma/tsconfig.json +21 -0
  491. package/registry/combos/express-prisma/variants/base/.env.example +25 -0
  492. package/registry/combos/express-prisma/variants/base/prisma/migrations/20260810165456_init/migration.sql +370 -0
  493. package/registry/combos/express-prisma/variants/base/prisma/migrations/20260811151048_add_is_active_field/migration.sql +2 -0
  494. package/registry/combos/express-prisma/variants/base/prisma/migrations/migration_lock.toml +3 -0
  495. package/registry/combos/express-prisma/variants/base/prisma/schema/simple-auth-kit.prisma +332 -0
  496. package/registry/combos/express-prisma/variants/base/src/admin.router.ts +314 -0
  497. package/registry/combos/express-prisma/variants/base/src/audit-log.repository.ts +91 -0
  498. package/registry/combos/express-prisma/variants/base/src/auth.service.ts +521 -0
  499. package/registry/combos/express-prisma/variants/base/src/authz.middleware.ts +58 -0
  500. package/registry/combos/express-prisma/variants/base/src/create-auth-app.ts +93 -0
  501. package/registry/combos/express-prisma/variants/base/src/openapi-admin.ts +569 -0
  502. package/registry/combos/express-prisma/variants/base/src/rbac.defaults.ts +195 -0
  503. package/registry/combos/express-prisma/variants/base/src/rbac.repository.ts +484 -0
  504. package/registry/combos/express-prisma/variants/base/src/seed.ts +115 -0
  505. package/registry/combos/express-prisma/variants/base/test/variant-hooks.ts +159 -0
  506. package/registry/combos/express-prisma/variants/workspaces/.env.example +30 -0
  507. package/registry/combos/express-prisma/variants/workspaces/prisma/migrations/20260810165502_init/migration.sql +443 -0
  508. package/registry/combos/express-prisma/variants/workspaces/prisma/migrations/20260811151051_add_is_active_field/migration.sql +2 -0
  509. package/registry/combos/express-prisma/variants/workspaces/prisma/migrations/migration_lock.toml +3 -0
  510. package/registry/combos/express-prisma/variants/workspaces/prisma/schema/simple-auth-kit.prisma +394 -0
  511. package/registry/combos/express-prisma/variants/workspaces/src/admin.router.ts +330 -0
  512. package/registry/combos/express-prisma/variants/workspaces/src/audit-log.repository.ts +102 -0
  513. package/registry/combos/express-prisma/variants/workspaces/src/auth.service.ts +542 -0
  514. package/registry/combos/express-prisma/variants/workspaces/src/authz.middleware.ts +94 -0
  515. package/registry/combos/express-prisma/variants/workspaces/src/create-auth-app.ts +99 -0
  516. package/registry/combos/express-prisma/variants/workspaces/src/openapi-admin.ts +601 -0
  517. package/registry/combos/express-prisma/variants/workspaces/src/openapi-workspace.ts +183 -0
  518. package/registry/combos/express-prisma/variants/workspaces/src/rbac.defaults.ts +222 -0
  519. package/registry/combos/express-prisma/variants/workspaces/src/rbac.repository.ts +527 -0
  520. package/registry/combos/express-prisma/variants/workspaces/src/seed.ts +147 -0
  521. package/registry/combos/express-prisma/variants/workspaces/src/workspace.repository.ts +188 -0
  522. package/registry/combos/express-prisma/variants/workspaces/src/workspace.router.ts +123 -0
  523. package/registry/combos/express-prisma/variants/workspaces/test/variant-hooks.ts +358 -0
  524. package/registry/combos/nestjs-drizzle/package.json +36 -0
  525. package/registry/combos/nestjs-drizzle/scripts/materialize.mjs +104 -0
  526. package/registry/combos/nestjs-drizzle/scripts/migrate.mjs +34 -0
  527. package/registry/combos/nestjs-drizzle/scripts/prove-cycle.mjs +29 -0
  528. package/registry/combos/nestjs-drizzle/scripts/seed.mjs +24 -0
  529. package/registry/combos/nestjs-drizzle/shared/drizzle.config.ts +67 -0
  530. package/registry/combos/nestjs-drizzle/shared/src/ability.guard.ts +46 -0
  531. package/registry/combos/nestjs-drizzle/shared/src/ability.ts +44 -0
  532. package/registry/combos/nestjs-drizzle/shared/src/auth-core-error.filter.ts +32 -0
  533. package/registry/combos/nestjs-drizzle/shared/src/auth.config.ts +65 -0
  534. package/registry/combos/nestjs-drizzle/shared/src/auth.controller.ts +299 -0
  535. package/registry/combos/nestjs-drizzle/shared/src/auth.guard.ts +34 -0
  536. package/registry/combos/nestjs-drizzle/shared/src/db.ts +12 -0
  537. package/registry/combos/nestjs-drizzle/shared/src/dto/auth.dto.ts +429 -0
  538. package/registry/combos/nestjs-drizzle/shared/src/id.helper.ts +22 -0
  539. package/registry/combos/nestjs-drizzle/shared/src/key-provider.ts +51 -0
  540. package/registry/combos/nestjs-drizzle/shared/src/oauth.repository.ts +36 -0
  541. package/registry/combos/nestjs-drizzle/shared/src/pagination.ts +30 -0
  542. package/registry/combos/nestjs-drizzle/shared/src/password-reset.repository.ts +29 -0
  543. package/registry/combos/nestjs-drizzle/shared/src/permission-cache.ts +196 -0
  544. package/registry/combos/nestjs-drizzle/shared/src/rate-limit.store.ts +26 -0
  545. package/registry/combos/nestjs-drizzle/shared/src/request-context.ts +25 -0
  546. package/registry/combos/nestjs-drizzle/shared/src/response.interceptor.ts +48 -0
  547. package/registry/combos/nestjs-drizzle/shared/src/route-tiers.ts +144 -0
  548. package/registry/combos/nestjs-drizzle/shared/src/session.repository.ts +132 -0
  549. package/registry/combos/nestjs-drizzle/shared/src/two-factor.repository.ts +35 -0
  550. package/registry/combos/nestjs-drizzle/test/bootstrap.ts +49 -0
  551. package/registry/combos/nestjs-drizzle/test/harness.ts +187 -0
  552. package/registry/combos/nestjs-drizzle/test/prove-cycle.ts +795 -0
  553. package/registry/combos/nestjs-drizzle/tsconfig.json +20 -0
  554. package/registry/combos/nestjs-drizzle/variants/base/.env.example +25 -0
  555. package/registry/combos/nestjs-drizzle/variants/base/drizzle/0000_init.sql +219 -0
  556. package/registry/combos/nestjs-drizzle/variants/base/drizzle/0001_add_is_active_field.sql +1 -0
  557. package/registry/combos/nestjs-drizzle/variants/base/drizzle/0002_colossal_true_believers.sql +3 -0
  558. package/registry/combos/nestjs-drizzle/variants/base/drizzle/meta/0000_snapshot.json +1861 -0
  559. package/registry/combos/nestjs-drizzle/variants/base/drizzle/meta/0001_snapshot.json +1868 -0
  560. package/registry/combos/nestjs-drizzle/variants/base/drizzle/meta/0002_snapshot.json +1887 -0
  561. package/registry/combos/nestjs-drizzle/variants/base/drizzle/meta/_journal.json +27 -0
  562. package/registry/combos/nestjs-drizzle/variants/base/src/admin.controller.ts +361 -0
  563. package/registry/combos/nestjs-drizzle/variants/base/src/audit-log.repository.ts +90 -0
  564. package/registry/combos/nestjs-drizzle/variants/base/src/auth.module.ts +83 -0
  565. package/registry/combos/nestjs-drizzle/variants/base/src/auth.service.ts +544 -0
  566. package/registry/combos/nestjs-drizzle/variants/base/src/authz.guard.ts +53 -0
  567. package/registry/combos/nestjs-drizzle/variants/base/src/dto/admin.dto.ts +130 -0
  568. package/registry/combos/nestjs-drizzle/variants/base/src/rbac.defaults.ts +218 -0
  569. package/registry/combos/nestjs-drizzle/variants/base/src/rbac.repository.ts +633 -0
  570. package/registry/combos/nestjs-drizzle/variants/base/src/schema.ts +427 -0
  571. package/registry/combos/nestjs-drizzle/variants/base/src/seed.ts +138 -0
  572. package/registry/combos/nestjs-drizzle/variants/base/test/variant-hooks.ts +179 -0
  573. package/registry/combos/nestjs-drizzle/variants/workspaces/.env.example +30 -0
  574. package/registry/combos/nestjs-drizzle/variants/workspaces/drizzle/0000_init.sql +251 -0
  575. package/registry/combos/nestjs-drizzle/variants/workspaces/drizzle/0001_add_is_active_field.sql +1 -0
  576. package/registry/combos/nestjs-drizzle/variants/workspaces/drizzle/0002_calm_tyger_tiger.sql +3 -0
  577. package/registry/combos/nestjs-drizzle/variants/workspaces/drizzle/meta/0000_snapshot.json +2155 -0
  578. package/registry/combos/nestjs-drizzle/variants/workspaces/drizzle/meta/0001_snapshot.json +2162 -0
  579. package/registry/combos/nestjs-drizzle/variants/workspaces/drizzle/meta/0002_snapshot.json +2181 -0
  580. package/registry/combos/nestjs-drizzle/variants/workspaces/drizzle/meta/_journal.json +27 -0
  581. package/registry/combos/nestjs-drizzle/variants/workspaces/src/admin.controller.ts +383 -0
  582. package/registry/combos/nestjs-drizzle/variants/workspaces/src/audit-log.repository.ts +102 -0
  583. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.module.ts +87 -0
  584. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.service.ts +553 -0
  585. package/registry/combos/nestjs-drizzle/variants/workspaces/src/authz.guard.ts +87 -0
  586. package/registry/combos/nestjs-drizzle/variants/workspaces/src/dto/admin.dto.ts +137 -0
  587. package/registry/combos/nestjs-drizzle/variants/workspaces/src/dto/workspace.dto.ts +60 -0
  588. package/registry/combos/nestjs-drizzle/variants/workspaces/src/rbac.defaults.ts +248 -0
  589. package/registry/combos/nestjs-drizzle/variants/workspaces/src/rbac.repository.ts +719 -0
  590. package/registry/combos/nestjs-drizzle/variants/workspaces/src/schema.ts +489 -0
  591. package/registry/combos/nestjs-drizzle/variants/workspaces/src/seed.ts +174 -0
  592. package/registry/combos/nestjs-drizzle/variants/workspaces/src/workspace.controller.ts +123 -0
  593. package/registry/combos/nestjs-drizzle/variants/workspaces/src/workspace.repository.ts +255 -0
  594. package/registry/combos/nestjs-drizzle/variants/workspaces/test/variant-hooks.ts +370 -0
  595. package/registry/combos/nestjs-prisma/package.json +41 -0
  596. package/registry/combos/nestjs-prisma/scripts/materialize.mjs +84 -0
  597. package/registry/combos/nestjs-prisma/scripts/migrate.mjs +32 -0
  598. package/registry/combos/nestjs-prisma/scripts/prove-cycle.mjs +30 -0
  599. package/registry/combos/nestjs-prisma/scripts/seed.mjs +24 -0
  600. package/registry/combos/nestjs-prisma/shared/prisma.config.ts +38 -0
  601. package/registry/combos/nestjs-prisma/shared/src/ability.guard.ts +29 -0
  602. package/registry/combos/nestjs-prisma/shared/src/ability.ts +23 -0
  603. package/registry/combos/nestjs-prisma/shared/src/auth-core-error.filter.ts +32 -0
  604. package/registry/combos/nestjs-prisma/shared/src/auth.config.ts +71 -0
  605. package/registry/combos/nestjs-prisma/shared/src/auth.controller.ts +303 -0
  606. package/registry/combos/nestjs-prisma/shared/src/auth.guard.ts +31 -0
  607. package/registry/combos/nestjs-prisma/shared/src/dto/auth.dto.ts +438 -0
  608. package/registry/combos/nestjs-prisma/shared/src/id.helper.ts +22 -0
  609. package/registry/combos/nestjs-prisma/shared/src/key-provider.ts +42 -0
  610. package/registry/combos/nestjs-prisma/shared/src/oauth.repository.ts +32 -0
  611. package/registry/combos/nestjs-prisma/shared/src/pagination.ts +30 -0
  612. package/registry/combos/nestjs-prisma/shared/src/password-reset.repository.ts +29 -0
  613. package/registry/combos/nestjs-prisma/shared/src/permission-cache.ts +140 -0
  614. package/registry/combos/nestjs-prisma/shared/src/rate-limit.store.ts +26 -0
  615. package/registry/combos/nestjs-prisma/shared/src/request-context.ts +19 -0
  616. package/registry/combos/nestjs-prisma/shared/src/response.interceptor.ts +48 -0
  617. package/registry/combos/nestjs-prisma/shared/src/route-tiers.ts +102 -0
  618. package/registry/combos/nestjs-prisma/shared/src/session.repository.ts +129 -0
  619. package/registry/combos/nestjs-prisma/shared/src/two-factor.repository.ts +29 -0
  620. package/registry/combos/nestjs-prisma/test/bootstrap.ts +54 -0
  621. package/registry/combos/nestjs-prisma/test/harness.ts +186 -0
  622. package/registry/combos/nestjs-prisma/test/prove-cycle.ts +795 -0
  623. package/registry/combos/nestjs-prisma/tsconfig.json +21 -0
  624. package/registry/combos/nestjs-prisma/variants/base/.env.example +31 -0
  625. package/registry/combos/nestjs-prisma/variants/base/prisma/migrations/20260810084937_init/migration.sql +269 -0
  626. package/registry/combos/nestjs-prisma/variants/base/prisma/migrations/20260810122546_add_profile_audit_soft_delete/migration.sql +96 -0
  627. package/registry/combos/nestjs-prisma/variants/base/prisma/migrations/20260810123620_add_display_name/migration.sql +3 -0
  628. package/registry/combos/nestjs-prisma/variants/base/prisma/migrations/20260810131421_drop_signing_keys/migration.sql +3 -0
  629. package/registry/combos/nestjs-prisma/variants/base/prisma/migrations/20260810161515_bigint_ids/migration.sql +305 -0
  630. package/registry/combos/nestjs-prisma/variants/base/prisma/migrations/20260810162120_drop_name_unique/migration.sql +6 -0
  631. package/registry/combos/nestjs-prisma/variants/base/prisma/migrations/20260811145755_add_is_active_field/migration.sql +2 -0
  632. package/registry/combos/nestjs-prisma/variants/base/prisma/migrations/20260811183529_add_countries_languages_customers/migration.sql +146 -0
  633. package/registry/combos/nestjs-prisma/variants/base/prisma/migrations/20260812121128_add_user_profile_details/migration.sql +4 -0
  634. package/registry/combos/nestjs-prisma/variants/base/prisma/migrations/migration_lock.toml +3 -0
  635. package/registry/combos/nestjs-prisma/variants/base/prisma/schema/simple-auth-kit.prisma +437 -0
  636. package/registry/combos/nestjs-prisma/variants/base/src/admin.controller.ts +684 -0
  637. package/registry/combos/nestjs-prisma/variants/base/src/audit-log.gateway.ts +51 -0
  638. package/registry/combos/nestjs-prisma/variants/base/src/audit-log.repository.ts +111 -0
  639. package/registry/combos/nestjs-prisma/variants/base/src/auth.module.ts +87 -0
  640. package/registry/combos/nestjs-prisma/variants/base/src/auth.service.ts +618 -0
  641. package/registry/combos/nestjs-prisma/variants/base/src/authz.guard.ts +34 -0
  642. package/registry/combos/nestjs-prisma/variants/base/src/country.repository.ts +149 -0
  643. package/registry/combos/nestjs-prisma/variants/base/src/customer.repository.ts +169 -0
  644. package/registry/combos/nestjs-prisma/variants/base/src/docs.ts +60 -0
  645. package/registry/combos/nestjs-prisma/variants/base/src/dto/admin.dto.ts +130 -0
  646. package/registry/combos/nestjs-prisma/variants/base/src/dto/country.dto.ts +120 -0
  647. package/registry/combos/nestjs-prisma/variants/base/src/dto/customer.dto.ts +147 -0
  648. package/registry/combos/nestjs-prisma/variants/base/src/dto/language.dto.ts +93 -0
  649. package/registry/combos/nestjs-prisma/variants/base/src/language.repository.ts +135 -0
  650. package/registry/combos/nestjs-prisma/variants/base/src/rbac.defaults.ts +193 -0
  651. package/registry/combos/nestjs-prisma/variants/base/src/rbac.repository.ts +514 -0
  652. package/registry/combos/nestjs-prisma/variants/base/src/seed.ts +89 -0
  653. package/registry/combos/nestjs-prisma/variants/base/test/variant-hooks.ts +135 -0
  654. package/registry/combos/nestjs-prisma/variants/workspaces/.env.example +30 -0
  655. package/registry/combos/nestjs-prisma/variants/workspaces/prisma/migrations/20260810084938_init/migration.sql +308 -0
  656. package/registry/combos/nestjs-prisma/variants/workspaces/prisma/migrations/20260810123450_add_profile_audit_soft_delete/migration.sql +96 -0
  657. package/registry/combos/nestjs-prisma/variants/workspaces/prisma/migrations/20260810123632_add_display_name/migration.sql +3 -0
  658. package/registry/combos/nestjs-prisma/variants/workspaces/prisma/migrations/20260810124243_workspace_audit_soft_delete/migration.sql +25 -0
  659. package/registry/combos/nestjs-prisma/variants/workspaces/prisma/migrations/20260810131432_drop_signing_keys/migration.sql +3 -0
  660. package/registry/combos/nestjs-prisma/variants/workspaces/prisma/migrations/20260810161551_bigint_ids/migration.sql +393 -0
  661. package/registry/combos/nestjs-prisma/variants/workspaces/prisma/migrations/20260810162131_drop_name_unique/migration.sql +3 -0
  662. package/registry/combos/nestjs-prisma/variants/workspaces/prisma/migrations/20260811145805_add_is_active_field/migration.sql +2 -0
  663. package/registry/combos/nestjs-prisma/variants/workspaces/prisma/migrations/20260813120000_add_countries_languages_customers/migration.sql +158 -0
  664. package/registry/combos/nestjs-prisma/variants/workspaces/prisma/migrations/migration_lock.toml +3 -0
  665. package/registry/combos/nestjs-prisma/variants/workspaces/prisma/schema/simple-auth-kit.prisma +521 -0
  666. package/registry/combos/nestjs-prisma/variants/workspaces/src/admin.controller.ts +733 -0
  667. package/registry/combos/nestjs-prisma/variants/workspaces/src/audit-log.repository.ts +98 -0
  668. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.module.ts +83 -0
  669. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.service.ts +509 -0
  670. package/registry/combos/nestjs-prisma/variants/workspaces/src/authz.guard.ts +69 -0
  671. package/registry/combos/nestjs-prisma/variants/workspaces/src/country.repository.ts +158 -0
  672. package/registry/combos/nestjs-prisma/variants/workspaces/src/customer.repository.ts +179 -0
  673. package/registry/combos/nestjs-prisma/variants/workspaces/src/dto/admin.dto.ts +128 -0
  674. package/registry/combos/nestjs-prisma/variants/workspaces/src/dto/country.dto.ts +120 -0
  675. package/registry/combos/nestjs-prisma/variants/workspaces/src/dto/customer.dto.ts +147 -0
  676. package/registry/combos/nestjs-prisma/variants/workspaces/src/dto/language.dto.ts +93 -0
  677. package/registry/combos/nestjs-prisma/variants/workspaces/src/dto/workspace.dto.ts +60 -0
  678. package/registry/combos/nestjs-prisma/variants/workspaces/src/language.repository.ts +143 -0
  679. package/registry/combos/nestjs-prisma/variants/workspaces/src/rbac.defaults.ts +211 -0
  680. package/registry/combos/nestjs-prisma/variants/workspaces/src/rbac.repository.ts +505 -0
  681. package/registry/combos/nestjs-prisma/variants/workspaces/src/seed.ts +122 -0
  682. package/registry/combos/nestjs-prisma/variants/workspaces/src/workspace.controller.ts +111 -0
  683. package/registry/combos/nestjs-prisma/variants/workspaces/src/workspace.repository.ts +177 -0
  684. package/registry/combos/nestjs-prisma/variants/workspaces/test/variant-hooks.ts +333 -0
  685. package/registry/core/crypto.ts +43 -0
  686. package/registry/core/oauth.ts +159 -0
  687. package/registry/core/package.json +20 -0
  688. package/registry/core/password-reset.ts +42 -0
  689. package/registry/core/rate-limit.ts +33 -0
  690. package/registry/core/rbac.ts +4 -0
  691. package/registry/core/session-policy.ts +96 -0
  692. package/registry/core/test/crypto.test.ts +25 -0
  693. package/registry/core/test/oauth.test.ts +213 -0
  694. package/registry/core/test/password-reset.test.ts +77 -0
  695. package/registry/core/test/rate-limit.test.ts +41 -0
  696. package/registry/core/test/rbac.test.ts +20 -0
  697. package/registry/core/test/session-policy.test.ts +209 -0
  698. package/registry/core/test/test-keys.ts +6 -0
  699. package/registry/core/test/token-service.test.ts +81 -0
  700. package/registry/core/test/two-factor.test.ts +72 -0
  701. package/registry/core/token-service.ts +143 -0
  702. package/registry/core/tsconfig.json +5 -0
  703. package/registry/core/two-factor.ts +108 -0
  704. package/registry/core/types.ts +127 -0
  705. package/registry/mobile-apps/README.md +65 -0
  706. package/registry/mobile-apps/bare-rn/shared/.bundle/config +2 -0
  707. package/registry/mobile-apps/bare-rn/shared/.eslintrc.js +4 -0
  708. package/registry/mobile-apps/bare-rn/shared/.prettierrc.js +5 -0
  709. package/registry/mobile-apps/bare-rn/shared/.watchmanconfig +1 -0
  710. package/registry/mobile-apps/bare-rn/shared/App.tsx +28 -0
  711. package/registry/mobile-apps/bare-rn/shared/Gemfile +17 -0
  712. package/registry/mobile-apps/bare-rn/shared/README.md +97 -0
  713. package/registry/mobile-apps/bare-rn/shared/__mocks__/reactNativeConfigMock.js +3 -0
  714. package/registry/mobile-apps/bare-rn/shared/__mocks__/styleMock.js +1 -0
  715. package/registry/mobile-apps/bare-rn/shared/__tests__/App.test.tsx +13 -0
  716. package/registry/mobile-apps/bare-rn/shared/android/app/build.gradle +121 -0
  717. package/registry/mobile-apps/bare-rn/shared/android/app/debug.keystore +0 -0
  718. package/registry/mobile-apps/bare-rn/shared/android/app/proguard-rules.pro +10 -0
  719. package/registry/mobile-apps/bare-rn/shared/android/app/src/main/AndroidManifest.xml +27 -0
  720. package/registry/mobile-apps/bare-rn/shared/android/app/src/main/java/com/mobilebarern/MainActivity.kt +22 -0
  721. package/registry/mobile-apps/bare-rn/shared/android/app/src/main/java/com/mobilebarern/MainApplication.kt +27 -0
  722. package/registry/mobile-apps/bare-rn/shared/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
  723. package/registry/mobile-apps/bare-rn/shared/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  724. package/registry/mobile-apps/bare-rn/shared/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  725. package/registry/mobile-apps/bare-rn/shared/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  726. package/registry/mobile-apps/bare-rn/shared/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  727. package/registry/mobile-apps/bare-rn/shared/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  728. package/registry/mobile-apps/bare-rn/shared/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  729. package/registry/mobile-apps/bare-rn/shared/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  730. package/registry/mobile-apps/bare-rn/shared/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  731. package/registry/mobile-apps/bare-rn/shared/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  732. package/registry/mobile-apps/bare-rn/shared/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  733. package/registry/mobile-apps/bare-rn/shared/android/app/src/main/res/values/strings.xml +3 -0
  734. package/registry/mobile-apps/bare-rn/shared/android/app/src/main/res/values/styles.xml +9 -0
  735. package/registry/mobile-apps/bare-rn/shared/android/build.gradle +21 -0
  736. package/registry/mobile-apps/bare-rn/shared/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  737. package/registry/mobile-apps/bare-rn/shared/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  738. package/registry/mobile-apps/bare-rn/shared/android/gradle.properties +44 -0
  739. package/registry/mobile-apps/bare-rn/shared/android/gradlew +248 -0
  740. package/registry/mobile-apps/bare-rn/shared/android/gradlew.bat +98 -0
  741. package/registry/mobile-apps/bare-rn/shared/android/settings.gradle +6 -0
  742. package/registry/mobile-apps/bare-rn/shared/app.json +4 -0
  743. package/registry/mobile-apps/bare-rn/shared/babel.config.js +6 -0
  744. package/registry/mobile-apps/bare-rn/shared/global.css +2 -0
  745. package/registry/mobile-apps/bare-rn/shared/index.js +10 -0
  746. package/registry/mobile-apps/bare-rn/shared/ios/.xcode.env +11 -0
  747. package/registry/mobile-apps/bare-rn/shared/ios/MobileBareRn/AppDelegate.swift +48 -0
  748. package/registry/mobile-apps/bare-rn/shared/ios/MobileBareRn/Images.xcassets/AppIcon.appiconset/Contents.json +53 -0
  749. package/registry/mobile-apps/bare-rn/shared/ios/MobileBareRn/Images.xcassets/Contents.json +6 -0
  750. package/registry/mobile-apps/bare-rn/shared/ios/MobileBareRn/Info.plist +59 -0
  751. package/registry/mobile-apps/bare-rn/shared/ios/MobileBareRn/LaunchScreen.storyboard +47 -0
  752. package/registry/mobile-apps/bare-rn/shared/ios/MobileBareRn/PrivacyInfo.xcprivacy +37 -0
  753. package/registry/mobile-apps/bare-rn/shared/ios/MobileBareRn.xcodeproj/project.pbxproj +475 -0
  754. package/registry/mobile-apps/bare-rn/shared/ios/MobileBareRn.xcodeproj/xcshareddata/xcschemes/MobileBareRn.xcscheme +88 -0
  755. package/registry/mobile-apps/bare-rn/shared/ios/Podfile +34 -0
  756. package/registry/mobile-apps/bare-rn/shared/jest.config.js +17 -0
  757. package/registry/mobile-apps/bare-rn/shared/metro.config.js +55 -0
  758. package/registry/mobile-apps/bare-rn/shared/nativewind-env.d.ts +1 -0
  759. package/registry/mobile-apps/bare-rn/shared/src/api/tokenStorage.ts +29 -0
  760. package/registry/mobile-apps/bare-rn/shared/src/components/FormInput.tsx +21 -0
  761. package/registry/mobile-apps/bare-rn/shared/src/components/PrimaryButton.tsx +47 -0
  762. package/registry/mobile-apps/bare-rn/shared/src/screens/LoginScreen.tsx +58 -0
  763. package/registry/mobile-apps/bare-rn/shared/src/screens/SessionsScreen.tsx +77 -0
  764. package/registry/mobile-apps/bare-rn/shared/src/screens/SignupScreen.tsx +54 -0
  765. package/registry/mobile-apps/bare-rn/shared/src/screens/TwoFactorScreen.tsx +59 -0
  766. package/registry/mobile-apps/bare-rn/shared/tailwind.config.js +9 -0
  767. package/registry/mobile-apps/bare-rn/shared/tsconfig.json +17 -0
  768. package/registry/mobile-apps/bare-rn/variants/base/package.json +58 -0
  769. package/registry/mobile-apps/bare-rn/variants/base/src/api/authClient.ts +14 -0
  770. package/registry/mobile-apps/bare-rn/variants/base/src/navigation/RootNavigator.tsx +58 -0
  771. package/registry/mobile-apps/bare-rn/variants/base/src/navigation/types.ts +15 -0
  772. package/registry/mobile-apps/bare-rn/variants/base/src/screens/HomeScreen.tsx +64 -0
  773. package/registry/mobile-apps/bare-rn/variants/base/src/store/authStore.ts +143 -0
  774. package/registry/mobile-apps/bare-rn/variants/workspaces/package.json +58 -0
  775. package/registry/mobile-apps/bare-rn/variants/workspaces/src/api/authClient.ts +22 -0
  776. package/registry/mobile-apps/bare-rn/variants/workspaces/src/navigation/RootNavigator.tsx +72 -0
  777. package/registry/mobile-apps/bare-rn/variants/workspaces/src/navigation/types.ts +18 -0
  778. package/registry/mobile-apps/bare-rn/variants/workspaces/src/screens/HomeScreen.tsx +85 -0
  779. package/registry/mobile-apps/bare-rn/variants/workspaces/src/screens/WorkspaceSelectScreen.tsx +165 -0
  780. package/registry/mobile-apps/bare-rn/variants/workspaces/src/store/authStore.ts +177 -0
  781. package/registry/mobile-apps/bare-rn/variants/workspaces/src/store/workspaceStore.ts +90 -0
  782. package/registry/mobile-apps/bare-rn/variants/workspaces/src/workspace/activeWorkspace.ts +55 -0
  783. package/registry/mobile-apps/expo/shared/App.tsx +15 -0
  784. package/registry/mobile-apps/expo/shared/assets/android-icon-background.png +0 -0
  785. package/registry/mobile-apps/expo/shared/assets/android-icon-foreground.png +0 -0
  786. package/registry/mobile-apps/expo/shared/assets/android-icon-monochrome.png +0 -0
  787. package/registry/mobile-apps/expo/shared/assets/favicon.png +0 -0
  788. package/registry/mobile-apps/expo/shared/assets/icon.png +0 -0
  789. package/registry/mobile-apps/expo/shared/assets/splash-icon.png +0 -0
  790. package/registry/mobile-apps/expo/shared/babel.config.js +6 -0
  791. package/registry/mobile-apps/expo/shared/global.css +2 -0
  792. package/registry/mobile-apps/expo/shared/index.ts +8 -0
  793. package/registry/mobile-apps/expo/shared/metro.config.js +6 -0
  794. package/registry/mobile-apps/expo/shared/nativewind-env.d.ts +3 -0
  795. package/registry/mobile-apps/expo/shared/postcss.config.mjs +5 -0
  796. package/registry/mobile-apps/expo/shared/src/screens/LoginScreen.tsx +81 -0
  797. package/registry/mobile-apps/expo/shared/src/screens/SessionsScreen.tsx +77 -0
  798. package/registry/mobile-apps/expo/shared/src/screens/SignupScreen.tsx +77 -0
  799. package/registry/mobile-apps/expo/shared/src/screens/TwoFactorScreen.tsx +59 -0
  800. package/registry/mobile-apps/expo/shared/src/storage/asyncStorageTokenStorage.ts +32 -0
  801. package/registry/mobile-apps/expo/shared/tsconfig.json +6 -0
  802. package/registry/mobile-apps/expo/variants/base/app.json +25 -0
  803. package/registry/mobile-apps/expo/variants/base/package.json +35 -0
  804. package/registry/mobile-apps/expo/variants/base/src/api/authClient.ts +17 -0
  805. package/registry/mobile-apps/expo/variants/base/src/navigation/RootNavigator.tsx +58 -0
  806. package/registry/mobile-apps/expo/variants/base/src/navigation/types.ts +10 -0
  807. package/registry/mobile-apps/expo/variants/base/src/screens/HomeScreen.tsx +75 -0
  808. package/registry/mobile-apps/expo/variants/base/src/store/authStore.ts +146 -0
  809. package/registry/mobile-apps/expo/variants/workspaces/app.json +25 -0
  810. package/registry/mobile-apps/expo/variants/workspaces/package.json +35 -0
  811. package/registry/mobile-apps/expo/variants/workspaces/src/api/authClient.ts +21 -0
  812. package/registry/mobile-apps/expo/variants/workspaces/src/navigation/RootNavigator.tsx +82 -0
  813. package/registry/mobile-apps/expo/variants/workspaces/src/navigation/types.ts +20 -0
  814. package/registry/mobile-apps/expo/variants/workspaces/src/screens/HomeScreen.tsx +94 -0
  815. package/registry/mobile-apps/expo/variants/workspaces/src/screens/WorkspaceSelectScreen.tsx +175 -0
  816. package/registry/mobile-apps/expo/variants/workspaces/src/store/authStore.ts +179 -0
  817. package/registry/mobile-apps/expo/variants/workspaces/src/store/workspaceStore.ts +90 -0
  818. package/registry/mobile-apps/expo/variants/workspaces/src/workspace/activeWorkspace.ts +55 -0
  819. package/registry.json +263 -0
  820. package/simple-auth-kit.ts +715 -0
@@ -0,0 +1,789 @@
1
+ // Black-box proof of the full auth cycle, run against a materialized variant of this combo by
2
+ // `npm run prove-cycle`. Everything below is variant-agnostic and speaks HTTP only; the three
3
+ // places where variants genuinely differ come from ./variant-hooks.js, which each variant
4
+ // supplies. Do not branch on the variant name in this file — add a hook instead.
5
+ //
6
+ // The two exceptions to "HTTP only" are deliberate and both are about *startup*: the tier section
7
+ // below registers a route the way a developer would, and the front/back equivalence section
8
+ // imports the same `defineAbilitiesFor` a client would use. Neither reads the database.
9
+ import "dotenv/config";
10
+ import { randomUUID } from "node:crypto";
11
+ import { generateTotpCode } from "@/lib/auth/core/two-factor.js";
12
+ import { ABILITY_SUBJECT, defineAbilitiesFor } from "../src/ability.js";
13
+ import { ability, createTieredRouter } from "../src/route-tiers.js";
14
+ import { bootstrap, capturedResetTokens } from "./bootstrap.js";
15
+ import { adminRouteProbes, AuthTokens, BASE_PORT, CallOptions, decodeJwtPayload, ProofContext, RouteProbe } from "./harness.js";
16
+ import { hooks } from "./variant-hooks.js";
17
+
18
+ const BASE = `http://localhost:${BASE_PORT}`;
19
+
20
+ let failures = 0;
21
+ function assert(condition: boolean, message: string) {
22
+ if (condition) {
23
+ console.log(` ok ${message}`);
24
+ } else {
25
+ failures += 1;
26
+ console.error(` FAIL ${message}`);
27
+ }
28
+ }
29
+
30
+ async function call(method: string, path: string, opts: CallOptions = {}): Promise<{ status: number; body: any }> {
31
+ const res = await fetch(`${BASE}${path}`, {
32
+ method,
33
+ headers: {
34
+ "content-type": "application/json",
35
+ ...(opts.token ? { authorization: `Bearer ${opts.token}` } : {}),
36
+ ...(opts.workspaceId ? { "x-workspace-id": opts.workspaceId } : {}),
37
+ ...(opts.headers ?? {}),
38
+ },
39
+ body: opts.body ? JSON.stringify(opts.body) : undefined,
40
+ });
41
+ const envelope = await res.json().catch(() => undefined);
42
+ const body = envelope && typeof envelope === "object" && "data" in envelope ? envelope.data : envelope;
43
+ return { status: res.status, body: body as any };
44
+ }
45
+
46
+ const RUN_ID = randomUUID().slice(0, 8);
47
+ const uniqueEmail = (prefix: string) => `${prefix}-${randomUUID()}@example.com`;
48
+ /** Role slugs are unique within their scope, so each run needs its own — the proof runs against a database it does not reset. */
49
+ const ROLE_SLUG = `billing-manager-${RUN_ID}`;
50
+
51
+ async function signup(email: string, password: string): Promise<AuthTokens> {
52
+ const res = await call("POST", "/auth/signup", { body: { email, password } });
53
+ if (res.status !== 200 && res.status !== 201) throw new Error(`signup failed (${res.status}): ${JSON.stringify(res.body)}`);
54
+ return res.body as AuthTokens;
55
+ }
56
+
57
+ const ctx: ProofContext = { call, assert, signup, uniqueEmail };
58
+
59
+ /**
60
+ * Registers a route on a tiered router exactly as a developer would, but naming a permission the
61
+ * catalog does not define.
62
+ *
63
+ * This is the Express shape of the reference combo's "a route that declares no tier fails at
64
+ * startup". There, a handler with no marker is caught by walking the route table at boot. Here the
65
+ * tier is a required argument of `route()`, so a route with *no* tier is not something a running
66
+ * program can express — it is a compile error, which is the stronger guarantee and cannot be
67
+ * asserted at runtime. What remains runtime-checkable is the other half of the same property: a
68
+ * route gated on a slug nothing can ever grant. `ability()` refuses it at registration, i.e. at
69
+ * startup, so even an untyped (plain-JS) caller fails loudly at boot rather than serving a route
70
+ * no one can reach.
71
+ */
72
+ function registerRouteGatedOnAnUndefinedSlug(): unknown {
73
+ const passthrough = (_req: unknown, _res: unknown, next: () => void) => next();
74
+ const router = createTieredRouter({ authentication: passthrough as never, authorization: passthrough as never });
75
+ try {
76
+ // Cast because the whole point is that TypeScript rejects this; the check being proved is the
77
+ // runtime one, which catches a caller who is not type-checked at all.
78
+ router.route("get", "/__tier-proof", ability("no:such-permission" as never), ((_req: unknown, res: { json: (b: unknown) => void }) =>
79
+ res.json({ ok: true })) as never);
80
+ return undefined;
81
+ } catch (err) {
82
+ return err;
83
+ }
84
+ }
85
+
86
+ async function main() {
87
+ console.log("0. fail-closed at startup: a route gated on a permission nothing can grant stops the app from booting");
88
+ const bootError = registerRouteGatedOnAnUndefinedSlug();
89
+ const bootMessage = bootError instanceof Error ? bootError.message : String(bootError);
90
+ assert(bootError !== undefined, "registering a route gated on a slug outside PERMISSION_CATALOG throws at registration, not at request time");
91
+ assert(
92
+ bootMessage.includes("no:such-permission") && bootMessage.includes("PERMISSION_CATALOG"),
93
+ `…and the error names the offending permission (got: ${bootMessage.split("\n")[0]})`,
94
+ );
95
+
96
+ const app = await bootstrap(BASE_PORT);
97
+ assert(true, "…and a router whose routes all name catalog slugs builds fine — the check is about the slug, not the boot path");
98
+ console.log(`variant: ${hooks.variant}\n`);
99
+
100
+ try {
101
+ console.log("1. signup (session A)");
102
+ const emailA = uniqueEmail("a");
103
+ const signupA = await call("POST", "/auth/signup", { body: { email: emailA, password: "correct-horse-battery" } });
104
+ assert(signupA.status === 201 || signupA.status === 200, `signup succeeds with only email + password in the body (got ${signupA.status})`);
105
+ const tokensA = signupA.body as AuthTokens;
106
+ assert(!!tokensA.accessToken && !!tokensA.refreshToken, "signup returns access + refresh tokens");
107
+
108
+ // Asserted as an exact set rather than a list of absences, and kept exact on purpose: this is
109
+ // the assertion that stops authorization data creeping back into the token later. There is no
110
+ // `roles` claim and no `permissions` claim — not empty ones, none. A token that carries
111
+ // permissions is a snapshot that keeps working after the grant behind it is gone, for as long
112
+ // as the token lives; everything here is resolved server-side on the request that uses it.
113
+ const claimsA = decodeJwtPayload(tokensA.accessToken);
114
+ assert(
115
+ Object.keys(claimsA).sort().join(",") === "exp,iat,jti,sessionId,sub",
116
+ `the access token carries exactly the expected claims — identity and session only (got ${Object.keys(claimsA).sort().join(",")})`,
117
+ );
118
+ assert(claimsA.sub === (await call("GET", "/auth/me", { token: tokensA.accessToken })).body.sub, "the token's subject is the user");
119
+
120
+ const meSignup = await call("GET", "/auth/me", { token: tokensA.accessToken });
121
+ assert(
122
+ hooks.variant === "workspaces" ? meSignup.body.roles.length === 0 : meSignup.body.roles.length > 0,
123
+ `a new signup's roles come from the database, not the token (got ${JSON.stringify(meSignup.body.roles)})`,
124
+ );
125
+
126
+ console.log("2. login (session B, a second device for the same user)");
127
+ const loginB = await call("POST", "/auth/login", { body: { identifier: emailA, password: "correct-horse-battery" } });
128
+ const tokensB = loginB.body as AuthTokens;
129
+ assert(loginB.status === 201 || loginB.status === 200, `login succeeds (got ${loginB.status})`);
130
+ assert(tokensB.sessionId !== tokensA.sessionId, "login creates a distinct session from signup's");
131
+
132
+ console.log("2b. login with wrong password is rejected");
133
+ const badLogin = await call("POST", "/auth/login", { body: { identifier: emailA, password: "wrong" } });
134
+ assert(badLogin.status === 401, `wrong password rejected (got ${badLogin.status})`);
135
+
136
+ console.log("2c. a second signup with the same email is rejected — email is globally unique");
137
+ const duplicate = await call("POST", "/auth/signup", { body: { email: emailA, password: "another-password-1" } });
138
+ assert(duplicate.status === 409, `duplicate email rejected (got ${duplicate.status})`);
139
+
140
+ console.log("3. protected route: GET /auth/me with session A's access token");
141
+ const meA = await call("GET", "/auth/me", { token: tokensA.accessToken });
142
+ assert(meA.status === 200, `me succeeds with a valid token (got ${meA.status})`);
143
+ assert(meA.body?.sessionId === tokensA.sessionId, "me reflects session A's identity");
144
+
145
+ console.log("3b. protected route rejects a missing token");
146
+ const meNoAuth = await call("GET", "/auth/me");
147
+ assert(meNoAuth.status === 401, `me rejects no token (got ${meNoAuth.status})`);
148
+
149
+ console.log("3c. the three route tiers, from the outside");
150
+ // Tier 1: no token at all. Each of these is marked @Public() in the source; the assertion is
151
+ // that the marker and the behaviour agree. "Reached" is asserted as "not refused for want of
152
+ // a bearer token" — several of them answer 401 for a bad *credential*, which is the handler
153
+ // running, not the guard refusing.
154
+ const MISSING_TOKEN = "missing bearer token";
155
+ const publicProbes: Array<[string, string, unknown]> = [
156
+ ["POST", "/auth/login", { identifier: emailA, password: "wrong-on-purpose" }],
157
+ ["POST", "/auth/refresh", { refreshToken: "not-a-real-token" }],
158
+ ["POST", "/auth/password/forgot", { email: uniqueEmail("nobody") }],
159
+ ["POST", "/auth/login/2fa", { challengeToken: "nope", code: "000000" }],
160
+ ["POST", "/auth/password/reset", { token: "nope", newPassword: "irrelevant-1" }],
161
+ ["GET", "/auth/oauth/google/start", undefined],
162
+ ];
163
+ for (const [method, path, body] of publicProbes) {
164
+ const res = await call(method, path, { body });
165
+ assert(res.body?.message !== MISSING_TOKEN, `${method} ${path} is reachable with no token (got ${res.status} ${res.body?.message ?? ""})`);
166
+ }
167
+
168
+ // Tier 2: any authenticated caller, no permission. `tokensA` holds no administrative
169
+ // permission whatsoever, and these must still answer.
170
+ for (const path of ["/auth/me", "/auth/sessions"]) {
171
+ const authed = await call("GET", path, { token: tokensA.accessToken });
172
+ assert(authed.status === 200, `GET ${path} is open to any authenticated user (got ${authed.status})`);
173
+ const anon = await call("GET", path);
174
+ assert(anon.status === 401, `…and refuses an anonymous caller (got ${anon.status})`);
175
+ }
176
+
177
+ console.log("4. refresh session A's token");
178
+ const refreshed = await call("POST", "/auth/refresh", { body: { refreshToken: tokensA.refreshToken } });
179
+ assert(refreshed.status === 200 || refreshed.status === 201, `refresh succeeds (got ${refreshed.status})`);
180
+ const rotatedA = refreshed.body as { accessToken: string; refreshToken: string };
181
+ assert(rotatedA.refreshToken !== tokensA.refreshToken, "refresh issues a new refresh token (rotation)");
182
+
183
+ console.log("4b. reuse of the now-dead original refresh token is detected and kills the whole session family");
184
+ const reuse = await call("POST", "/auth/refresh", { body: { refreshToken: tokensA.refreshToken } });
185
+ assert(reuse.status === 401, `stale/reused refresh token rejected (got ${reuse.status})`);
186
+
187
+ const reuseFalloutB = await call("POST", "/auth/refresh", { body: { refreshToken: tokensB.refreshToken } });
188
+ assert(reuseFalloutB.status === 401, `reuse detection also killed session B's refresh (got ${reuseFalloutB.status})`);
189
+
190
+ // Self-contained on purpose: the reuse detection just above kills every session belonging to
191
+ // user A, so anything asserting on a *live* session has to bring its own.
192
+ console.log("4c. a session's lifetime: an absolute expiry that refreshing does not extend");
193
+ type LiveSession = { id: string; createdAt: string; expiresAt: string; provider?: string };
194
+ const lifetimeEmail = uniqueEmail("lifetime");
195
+ const lifetimeFirst = await signup(lifetimeEmail, "lifetime-pw-123");
196
+ const lifetimeSessionId = decodeJwtPayload(lifetimeFirst.accessToken).sessionId as string;
197
+
198
+ const listSessions = async (token: string) => (await call("GET", "/auth/sessions", { token })).body as LiveSession[];
199
+ const live = (await listSessions(lifetimeFirst.accessToken)).find((s) => s.id === lifetimeSessionId);
200
+ assert(!!live, "the caller's own session is listed");
201
+ assert(
202
+ !!live && new Date(live.expiresAt).getTime() > new Date(live.createdAt).getTime(),
203
+ `a session expires strictly after it was created (got ${live?.createdAt} → ${live?.expiresAt})`,
204
+ );
205
+ assert(!live?.provider, "a password login records no OAuth provider on the session");
206
+
207
+ // The point of `expires_at` being written once and never rewritten: rotation issues a fresh
208
+ // refresh token every time, so without an absolute cap a continuously active client would hold
209
+ // one session open forever.
210
+ const lifetimeRotated = (await call("POST", "/auth/refresh", { body: { refreshToken: lifetimeFirst.refreshToken } })).body as AuthTokens;
211
+ const afterRefresh = (await listSessions(lifetimeRotated.accessToken)).find((s) => s.id === lifetimeSessionId);
212
+ assert(afterRefresh?.expiresAt === live?.expiresAt, `refreshing does not push the session's expiry out (got ${afterRefresh?.expiresAt})`);
213
+
214
+ // A second device for the same user, so the list can be read after the first one is revoked.
215
+ const lifetimeSecond = (await call("POST", "/auth/login", { body: { identifier: lifetimeEmail, password: "lifetime-pw-123" } })).body as AuthTokens;
216
+ assert((await listSessions(lifetimeSecond.accessToken)).some((s) => s.id === lifetimeSessionId), "a second device sees the first device's session");
217
+
218
+ await call("POST", "/auth/logout", { token: lifetimeRotated.accessToken });
219
+ // Revocation is never a delete: the row survives with `is_revoked` set, and what changes is
220
+ // that it stops counting as active.
221
+ assert(
222
+ !(await listSessions(lifetimeSecond.accessToken)).some((s) => s.id === lifetimeSessionId),
223
+ "a revoked session stops being listed as active",
224
+ );
225
+
226
+ console.log("5. logout-one-device: revoke session A specifically using its (rotated) access token");
227
+ const logoutA = await call("POST", "/auth/logout", { token: rotatedA.accessToken });
228
+ assert(logoutA.status === 200 || logoutA.status === 201, `logout succeeds (got ${logoutA.status})`);
229
+
230
+ const meAAfterLogout = await call("GET", "/auth/me", { token: rotatedA.accessToken });
231
+ assert(meAAfterLogout.status === 401, `session A's access token is denylisted after logout (got ${meAAfterLogout.status})`);
232
+
233
+ console.log("6. block/unblock (admin-scoped)");
234
+ const adminEmail = uniqueEmail("admin");
235
+ const adminSignup = await signup(adminEmail, "admin-pw-123");
236
+ const adminUserId = (await call("GET", "/auth/me", { token: adminSignup.accessToken })).body.sub as string;
237
+ const admin = await hooks.makeAdmin(ctx, { userId: adminUserId, email: adminEmail, password: "admin-pw-123", tokens: adminSignup });
238
+
239
+ const victimEmail = uniqueEmail("victim");
240
+ const victimTokens = await signup(victimEmail, "victim-pw-123");
241
+ const victimId = (await call("GET", "/auth/me", { token: victimTokens.accessToken })).body.sub as string;
242
+ await hooks.admitUser(ctx, admin, { userId: victimId, email: victimEmail });
243
+
244
+ console.log("6a. a non-admin cannot block anyone");
245
+ const forbiddenBlock = await call("POST", `/auth/admin/users/${victimId}/block`, { token: victimTokens.accessToken, workspaceId: admin.workspaceId });
246
+ assert(forbiddenBlock.status === 403, `non-admin block attempt rejected (got ${forbiddenBlock.status})`);
247
+
248
+ console.log("6b. an admin cannot block themselves");
249
+ const selfBlock = await call("POST", `/auth/admin/users/${adminUserId}/block`, { token: await admin.freshToken(), workspaceId: admin.workspaceId });
250
+ assert(selfBlock.status === 403, `admin self-block rejected (got ${selfBlock.status})`);
251
+
252
+ console.log("6b-ii. an admin cannot revoke their own role");
253
+ // Self-revoking `admin` strips the permission that authorised the call. In a workspace
254
+ // there is no way back in afterwards, so the route refuses it the same way self-block does.
255
+ const selfRevoke = await call("POST", `/auth/admin/users/${adminUserId}/roles/admin/revoke`, { token: await admin.freshToken(), workspaceId: admin.workspaceId });
256
+ assert(selfRevoke.status === 403, `admin self-role-revoke rejected (got ${selfRevoke.status})`);
257
+ const stillAdmin = await call("GET", "/auth/admin/users", { token: await admin.freshToken(), workspaceId: admin.workspaceId });
258
+ assert(stillAdmin.status === 200, `admin still authorised after the refused self-revoke (got ${stillAdmin.status})`);
259
+
260
+ console.log("6c. admin blocks the victim");
261
+ const block = await call("POST", `/auth/admin/users/${victimId}/block`, { token: await admin.freshToken(), workspaceId: admin.workspaceId });
262
+ assert(block.status === 200 || block.status === 201, `block succeeds (got ${block.status})`);
263
+
264
+ console.log("7. get-rejected-after-block: victim cannot log in or refresh anymore");
265
+ const victimLoginAfterBlock = await call("POST", "/auth/login", { body: { identifier: victimEmail, password: "victim-pw-123" } });
266
+ assert(victimLoginAfterBlock.status === 401, `blocked user cannot log in (got ${victimLoginAfterBlock.status})`);
267
+
268
+ const victimRefreshAfterBlock = await call("POST", "/auth/refresh", { body: { refreshToken: victimTokens.refreshToken } });
269
+ assert(victimRefreshAfterBlock.status === 401, `blocked user's refresh token rejected (got ${victimRefreshAfterBlock.status})`);
270
+
271
+ console.log("7b. unblock restores access");
272
+ const unblock = await call("POST", `/auth/admin/users/${victimId}/unblock`, { token: await admin.freshToken(), workspaceId: admin.workspaceId });
273
+ assert(unblock.status === 200 || unblock.status === 201, `unblock succeeds (got ${unblock.status})`);
274
+ const victimLoginAfterUnblock = await call("POST", "/auth/login", { body: { identifier: victimEmail, password: "victim-pw-123" } });
275
+ assert(
276
+ victimLoginAfterUnblock.status === 200 || victimLoginAfterUnblock.status === 201,
277
+ `unblocked user can log in again (got ${victimLoginAfterUnblock.status})`,
278
+ );
279
+
280
+ console.log("7c. deactivate is a distinct toggle from block — both independently deny login, neither implies the other");
281
+ const deactivate = await call("POST", `/auth/admin/users/${victimId}/deactivate`, { token: await admin.freshToken(), workspaceId: admin.workspaceId });
282
+ assert(deactivate.status === 200 || deactivate.status === 201, `deactivate succeeds (got ${deactivate.status})`);
283
+
284
+ const victimLoginAfterDeactivate = await call("POST", "/auth/login", { body: { identifier: victimEmail, password: "victim-pw-123" } });
285
+ assert(victimLoginAfterDeactivate.status === 401, `deactivated (but not blocked) user cannot log in either (got ${victimLoginAfterDeactivate.status})`);
286
+
287
+ const usersAfterDeactivate = await call("GET", "/auth/admin/users", { token: await admin.freshToken(), workspaceId: admin.workspaceId });
288
+ const victimAfterDeactivate = (usersAfterDeactivate.body as { items: Array<{ id?: string; userId?: string; blocked: boolean; isActive: boolean }> }).items.find(
289
+ (u) => (u.id ?? u.userId) === victimId,
290
+ );
291
+ assert(victimAfterDeactivate?.isActive === false, "…and the user list reflects isActive=false");
292
+ assert(victimAfterDeactivate?.blocked === false, "…while blocked stays false — deactivating never sets it, they are independent flags");
293
+
294
+ const reactivate = await call("POST", `/auth/admin/users/${victimId}/activate`, { token: await admin.freshToken(), workspaceId: admin.workspaceId });
295
+ assert(reactivate.status === 200 || reactivate.status === 201, `activate succeeds (got ${reactivate.status})`);
296
+ const victimLoginAfterReactivate = await call("POST", "/auth/login", { body: { identifier: victimEmail, password: "victim-pw-123" } });
297
+ assert(
298
+ victimLoginAfterReactivate.status === 200 || victimLoginAfterReactivate.status === 201,
299
+ `reactivated user can log in again (got ${victimLoginAfterReactivate.status})`,
300
+ );
301
+
302
+ console.log("8. two-factor: enroll -> confirm -> login now requires 2FA -> complete challenge -> logout");
303
+ const tfaEmail = uniqueEmail("tfa");
304
+ const tfaTokens = await signup(tfaEmail, "tfa-pw-12345");
305
+
306
+ const enroll = await call("POST", "/auth/2fa/enroll", { token: tfaTokens.accessToken });
307
+ assert(enroll.status === 200 || enroll.status === 201, `2fa enroll succeeds (got ${enroll.status})`);
308
+ const { secret } = enroll.body as { secret: string; provisioningUri: string };
309
+
310
+ const confirm = await call("POST", "/auth/2fa/confirm", { token: tfaTokens.accessToken, body: { code: generateTotpCode(secret) } });
311
+ assert(confirm.status === 200 || confirm.status === 201, `2fa confirm succeeds (got ${confirm.status})`);
312
+ const { backupCodes } = confirm.body as { backupCodes: string[] };
313
+ assert(backupCodes.length === 10, `2fa confirm returns backup codes (got ${backupCodes.length})`);
314
+
315
+ const tfaLogin = await call("POST", "/auth/login", { body: { identifier: tfaEmail, password: "tfa-pw-12345" } });
316
+ const tfaChallenge = tfaLogin.body as { twoFactorRequired: true; challengeToken: string };
317
+ assert(tfaChallenge.twoFactorRequired === true, `login now returns a 2fa challenge instead of tokens (got status ${tfaLogin.status})`);
318
+
319
+ const tfaComplete = await call("POST", "/auth/login/2fa", { body: { challengeToken: tfaChallenge.challengeToken, code: generateTotpCode(secret) } });
320
+ assert(tfaComplete.status === 200 || tfaComplete.status === 201, `2fa challenge completes login (got ${tfaComplete.status})`);
321
+ const tfaSessionTokens = tfaComplete.body as { accessToken: string };
322
+
323
+ const tfaBadChallenge = await call("POST", "/auth/login/2fa", { body: { challengeToken: tfaChallenge.challengeToken, code: "000000" } });
324
+ assert(tfaBadChallenge.status === 401, `wrong 2fa code rejected (got ${tfaBadChallenge.status})`);
325
+
326
+ const tfaLogout = await call("POST", "/auth/logout", { token: tfaSessionTokens.accessToken });
327
+ assert(tfaLogout.status === 200 || tfaLogout.status === 201, `logout after 2fa login succeeds (got ${tfaLogout.status})`);
328
+
329
+ console.log("9. forgot/reset password -> old password stops working -> old sessions revoked");
330
+ const resetEmail = uniqueEmail("reset");
331
+ const resetTokens = await signup(resetEmail, "original-password-1");
332
+
333
+ const forgot = await call("POST", "/auth/password/forgot", { body: { email: resetEmail } });
334
+ assert(forgot.status === 200 || forgot.status === 201, `forgot-password always reports success (got ${forgot.status})`);
335
+ const rawResetToken = capturedResetTokens.get(resetEmail);
336
+ assert(!!rawResetToken, "the reset token reached the sendPasswordResetEmail hook");
337
+
338
+ const badReset = await call("POST", "/auth/password/reset", { body: { token: "not-a-real-token", newPassword: "irrelevant-1" } });
339
+ assert(badReset.status === 401, `reset with an invalid token is rejected (got ${badReset.status})`);
340
+
341
+ const reset = await call("POST", "/auth/password/reset", { body: { token: rawResetToken, newPassword: "new-password-2" } });
342
+ assert(reset.status === 200 || reset.status === 201, `reset with a valid token succeeds (got ${reset.status})`);
343
+
344
+ // Session revocation bumps sessionVersion/revokedAt, which invalidates future *refreshes*
345
+ // immediately; it does not retroactively invalidate an already-issued, still-unexpired
346
+ // access token (same "denylist is for instant per-token kill, sessionVersion is for
347
+ // refresh-time revocation" split that /auth/logout relies on elsewhere in this file).
348
+ const refreshAfterReset = await call("POST", "/auth/refresh", { body: { refreshToken: resetTokens.refreshToken } });
349
+ assert(refreshAfterReset.status === 401, `pre-reset session's refresh token is invalid after password reset (got ${refreshAfterReset.status})`);
350
+
351
+ const loginWithOldPassword = await call("POST", "/auth/login", { body: { identifier: resetEmail, password: "original-password-1" } });
352
+ assert(loginWithOldPassword.status === 401, `old password no longer works (got ${loginWithOldPassword.status})`);
353
+
354
+ const loginWithNewPassword = await call("POST", "/auth/login", { body: { identifier: resetEmail, password: "new-password-2" } });
355
+ assert(loginWithNewPassword.status === 200 || loginWithNewPassword.status === 201, `new password works (got ${loginWithNewPassword.status})`);
356
+
357
+ console.log("10. RBAC: create role, attach permission, assign to user, permission takes effect");
358
+ const rbacEmail = uniqueEmail("rbac");
359
+ const rbacTokens = await signup(rbacEmail, "rbac-pw-12345");
360
+ const rbacUserId = (await call("GET", "/auth/me", { token: rbacTokens.accessToken })).body.sub as string;
361
+ await hooks.admitUser(ctx, admin, { userId: rbacUserId, email: rbacEmail });
362
+
363
+ const createRole = await call("POST", "/auth/admin/roles", {
364
+ token: await admin.freshToken(),
365
+ workspaceId: admin.workspaceId,
366
+ body: { slug: ROLE_SLUG, displayName: "Billing manager" },
367
+ });
368
+ assert(createRole.status === 200 || createRole.status === 201, `admin can create a role (got ${createRole.status})`);
369
+ const role = createRole.body as { id: string; slug: string; displayName: string };
370
+ assert(role.slug === ROLE_SLUG && role.displayName === "Billing manager", "a role carries a stable slug and a separate human label");
371
+
372
+ const attachPermission = await call("POST", `/auth/admin/roles/${role.id}/permissions`, {
373
+ token: await admin.freshToken(),
374
+ workspaceId: admin.workspaceId,
375
+ body: { permission: "billing:manage" },
376
+ });
377
+ assert(attachPermission.status === 200 || attachPermission.status === 201, `admin can attach a permission to a role (got ${attachPermission.status})`);
378
+
379
+ const assignRole = await call("POST", `/auth/admin/users/${rbacUserId}/roles`, {
380
+ token: await admin.freshToken(),
381
+ workspaceId: admin.workspaceId,
382
+ body: { role: ROLE_SLUG },
383
+ });
384
+ assert(assignRole.status === 200 || assignRole.status === 201, `admin can assign a role to a user (got ${assignRole.status})`);
385
+
386
+ const rbacDirect = await call("POST", `/auth/admin/users/${rbacUserId}/permissions`, {
387
+ token: await admin.freshToken(),
388
+ workspaceId: admin.workspaceId,
389
+ body: { permission: "reports:export" },
390
+ });
391
+ assert(rbacDirect.status === 200 || rbacDirect.status === 201, `admin can grant a permission directly (got ${rbacDirect.status})`);
392
+
393
+ // No re-login. Nothing about authorization lives in the token in either variant, so both
394
+ // grants are in effect on the caller's very next request, on the token they already hold.
395
+ const rbacMe = await call("GET", "/auth/me", { token: rbacTokens.accessToken, workspaceId: admin.workspaceId });
396
+ assert((rbacMe.body?.permissions ?? []).includes("billing:manage"), "the assigned role's permission is in effect on the user's existing token");
397
+ assert((rbacMe.body?.permissions ?? []).includes("reports:export"), "a direct grant is unioned with the role-derived permissions");
398
+ assert((rbacMe.body?.roles ?? []).includes(ROLE_SLUG), "…and /auth/me reports the role by slug");
399
+
400
+ console.log("11. admin audit log: the role_assigned event from step 10 is readable back");
401
+ const auditList = await call("GET", `/auth/admin/audit-log?userId=${rbacUserId}&action=role_assigned`, {
402
+ token: await admin.freshToken(),
403
+ workspaceId: admin.workspaceId,
404
+ });
405
+ assert(auditList.status === 200, `admin can list the audit log (got ${auditList.status})`);
406
+ const auditEntries = (auditList.body as { items: Array<{ action: string; name: string; userId: string | null }> }).items;
407
+ assert(
408
+ auditEntries.some((e) => e.action === "role_assigned" && e.userId === rbacUserId),
409
+ "the role_assigned event for this user shows up in the audit log",
410
+ );
411
+ assert(auditEntries.every((e) => e.name === "Role assigned"), "audit entries carry a human-readable name alongside the action");
412
+
413
+ const auditForbidden = await call("GET", "/auth/admin/audit-log", { token: rbacTokens.accessToken, workspaceId: admin.workspaceId });
414
+ assert(auditForbidden.status === 403, `non-admin cannot list the audit log (got ${auditForbidden.status})`);
415
+
416
+ console.log("11b. user/role CRUD: fetch, edit, and soft-delete both — on disposable rows, so the shared probe target/role are untouched");
417
+ const crudUserEmail = uniqueEmail("crud-user");
418
+ const crudUserTokens = await signup(crudUserEmail, "crud-user-pw-12345");
419
+ const crudUserId = (await call("GET", "/auth/me", { token: crudUserTokens.accessToken })).body.sub as string;
420
+ await hooks.admitUser(ctx, admin, { userId: crudUserId, email: crudUserEmail });
421
+
422
+ const getUser = await call("GET", `/auth/admin/users/${crudUserId}`, { token: await admin.freshToken(), workspaceId: admin.workspaceId });
423
+ assert(getUser.status === 200, `admin can fetch a single user (got ${getUser.status})`);
424
+ assert((getUser.body as { email: string }).email === crudUserEmail, "…and it's the right one");
425
+
426
+ const updateUser = await call("PATCH", `/auth/admin/users/${crudUserId}`, {
427
+ token: await admin.freshToken(),
428
+ workspaceId: admin.workspaceId,
429
+ body: { displayName: "CRUD Test User" },
430
+ });
431
+ assert(updateUser.status === 200, `admin can edit a user's profile (got ${updateUser.status})`);
432
+ assert((updateUser.body as { displayName: string | null }).displayName === "CRUD Test User", "…and the edit is reflected in the response");
433
+
434
+ const crudRoleSlug = `crud-role-${RUN_ID}`;
435
+ const createCrudRole = await call("POST", "/auth/admin/roles", { token: await admin.freshToken(), workspaceId: admin.workspaceId, body: { slug: crudRoleSlug } });
436
+ assert(createCrudRole.status === 201, `admin can create a disposable role for the delete test (got ${createCrudRole.status})`);
437
+ const crudRoleId = (createCrudRole.body as { id: string }).id;
438
+
439
+ const updateRole = await call("PATCH", `/auth/admin/roles/${crudRoleId}`, {
440
+ token: await admin.freshToken(),
441
+ workspaceId: admin.workspaceId,
442
+ body: { displayName: "CRUD Test Role" },
443
+ });
444
+ assert(updateRole.status === 200, `admin can edit a role (got ${updateRole.status})`);
445
+ assert((updateRole.body as { displayName: string }).displayName === "CRUD Test Role", "…and the edit is reflected in the response");
446
+
447
+ const assignCrudRole = await call("POST", `/auth/admin/users/${crudUserId}/roles`, {
448
+ token: await admin.freshToken(),
449
+ workspaceId: admin.workspaceId,
450
+ body: { role: crudRoleSlug },
451
+ });
452
+ assert(assignCrudRole.status === 201, `the disposable role can be assigned before it's deleted (got ${assignCrudRole.status})`);
453
+
454
+ const deleteRole = await call("DELETE", `/auth/admin/roles/${crudRoleId}`, {
455
+ token: await admin.freshToken(),
456
+ workspaceId: admin.workspaceId,
457
+ body: { reason: "prove-cycle cleanup" },
458
+ });
459
+ assert(deleteRole.status === 200, `admin can delete a role (got ${deleteRole.status})`);
460
+
461
+ const rolesAfterDelete = await call("GET", "/auth/admin/roles", { token: await admin.freshToken(), workspaceId: admin.workspaceId });
462
+ assert(
463
+ !(rolesAfterDelete.body as { roles: Array<{ id: string }> }).roles.some((r) => r.id === crudRoleId),
464
+ "a deleted role no longer appears in the role list",
465
+ );
466
+
467
+ const meAfterRoleDelete = await call("GET", "/auth/me", { token: crudUserTokens.accessToken, workspaceId: admin.workspaceId });
468
+ assert(!(meAfterRoleDelete.body?.roles ?? []).includes(crudRoleSlug), "…and a member who held it no longer resolves it, with no re-login");
469
+
470
+ const deleteUser = await call("DELETE", `/auth/admin/users/${crudUserId}`, { token: await admin.freshToken(), workspaceId: admin.workspaceId });
471
+ assert(deleteUser.status === 200, `admin can delete a user (got ${deleteUser.status})`);
472
+
473
+ const usersAfterDelete = await call("GET", "/auth/admin/users", { token: await admin.freshToken(), workspaceId: admin.workspaceId });
474
+ assert(
475
+ !(usersAfterDelete.body as { items: Array<{ id: string }> }).items.some((u) => u.id === crudUserId),
476
+ "a deleted user no longer appears in the user list",
477
+ );
478
+
479
+ const getDeletedUser = await call("GET", `/auth/admin/users/${crudUserId}`, { token: await admin.freshToken(), workspaceId: admin.workspaceId });
480
+ assert(getDeletedUser.status === 404, `…and fetching them directly by id now 404s (got ${getDeletedUser.status})`);
481
+
482
+ const loginAsDeletedUser = await call("POST", "/auth/login", { body: { identifier: crudUserEmail, password: "crud-user-pw-12345" } });
483
+ assert(loginAsDeletedUser.status === 401, `…and they can no longer log in (got ${loginAsDeletedUser.status})`);
484
+
485
+ const selfDeleteAttempt = await call("DELETE", `/auth/admin/users/${(await call("GET", "/auth/me", { token: await admin.freshToken() })).body.sub}`, {
486
+ token: await admin.freshToken(),
487
+ workspaceId: admin.workspaceId,
488
+ });
489
+ assert(selfDeleteAttempt.status === 403, `an admin cannot delete their own account (got ${selfDeleteAttempt.status})`);
490
+
491
+ console.log("11c. login accepts a username or a phone number in place of the email, same password");
492
+ const identEmail = uniqueEmail("ident");
493
+ const identTokens = await signup(identEmail, "ident-user-pw-12345");
494
+ const identUserId = (await call("GET", "/auth/me", { token: identTokens.accessToken })).body.sub as string;
495
+ await hooks.admitUser(ctx, admin, { userId: identUserId, email: identEmail });
496
+
497
+ const identUsername = `ident-user-${RUN_ID}`;
498
+ const identPhone = `+1-555-${RUN_ID}`;
499
+ const setIdentFields = await call("PATCH", `/auth/admin/users/${identUserId}`, {
500
+ token: await admin.freshToken(),
501
+ workspaceId: admin.workspaceId,
502
+ body: { username: identUsername, phone: identPhone },
503
+ });
504
+ assert(setIdentFields.status === 200, `admin can set a user's username and phone (got ${setIdentFields.status})`);
505
+
506
+ const loginByUsername = await call("POST", "/auth/login", { body: { identifier: identUsername, password: "ident-user-pw-12345" } });
507
+ assert(loginByUsername.status === 201 || loginByUsername.status === 200, `login by username succeeds (got ${loginByUsername.status})`);
508
+ assert((loginByUsername.body as AuthTokens).accessToken !== undefined, "…and returns a real access token");
509
+
510
+ const loginByPhone = await call("POST", "/auth/login", { body: { identifier: identPhone, password: "ident-user-pw-12345" } });
511
+ assert(loginByPhone.status === 201 || loginByPhone.status === 200, `login by phone succeeds (got ${loginByPhone.status})`);
512
+
513
+ const loginByEmailStill = await call("POST", "/auth/login", { body: { identifier: identEmail, password: "ident-user-pw-12345" } });
514
+ assert(loginByEmailStill.status === 201 || loginByEmailStill.status === 200, `login by email still works once username/phone are also set (got ${loginByEmailStill.status})`);
515
+
516
+ const loginByUsernameWrongPassword = await call("POST", "/auth/login", { body: { identifier: identUsername, password: "wrong" } });
517
+ assert(loginByUsernameWrongPassword.status === 401, `login by username with the wrong password is still rejected (got ${loginByUsernameWrongPassword.status})`);
518
+
519
+ const loginByUnknownIdentifier = await call("POST", "/auth/login", { body: { identifier: `nobody-${RUN_ID}`, password: "whatever" } });
520
+ assert(loginByUnknownIdentifier.status === 401, `an identifier matching no email/username/phone is rejected the same way (got ${loginByUnknownIdentifier.status})`);
521
+
522
+ console.log("11d. change password: wrong current password rejected, right one rotates it and revokes every other session but this one");
523
+ const otherDeviceLogin = await call("POST", "/auth/login", { body: { identifier: identEmail, password: "ident-user-pw-12345" } });
524
+ const otherDeviceTokens = otherDeviceLogin.body as AuthTokens;
525
+ assert(otherDeviceTokens.sessionId !== identTokens.sessionId, "a second session for the identifier-test user exists, distinct from the signup session");
526
+
527
+ const wrongCurrentPassword = await call("POST", "/auth/password/change", {
528
+ token: identTokens.accessToken,
529
+ body: { currentPassword: "not-the-real-password", newPassword: "ident-new-pw-67890" },
530
+ });
531
+ assert(wrongCurrentPassword.status === 401, `changing the password with the wrong current password is rejected (got ${wrongCurrentPassword.status})`);
532
+
533
+ const changePassword = await call("POST", "/auth/password/change", {
534
+ token: identTokens.accessToken,
535
+ body: { currentPassword: "ident-user-pw-12345", newPassword: "ident-new-pw-67890" },
536
+ });
537
+ assert(changePassword.status === 201 || changePassword.status === 200, `change-password succeeds with the right current password (got ${changePassword.status})`);
538
+
539
+ const oldPasswordNowFails = await call("POST", "/auth/login", { body: { identifier: identEmail, password: "ident-user-pw-12345" } });
540
+ assert(oldPasswordNowFails.status === 401, `the old password no longer works (got ${oldPasswordNowFails.status})`);
541
+
542
+ const newPasswordWorks = await call("POST", "/auth/login", { body: { identifier: identEmail, password: "ident-new-pw-67890" } });
543
+ assert(newPasswordWorks.status === 201 || newPasswordWorks.status === 200, `the new password works (got ${newPasswordWorks.status})`);
544
+
545
+ const callingSessionSurvives = await call("GET", "/auth/me", { token: identTokens.accessToken });
546
+ assert(callingSessionSurvives.status === 200, `the session that called change-password is left alone, not logged out by its own request (got ${callingSessionSurvives.status})`);
547
+
548
+ // Revocation lands on the session row (checked at refresh time), not an instant denylist of
549
+ // the still-live access token — same split `logout()` vs. `logoutOthers()` relies on elsewhere.
550
+ const otherSessionRefreshRevoked = await call("POST", "/auth/refresh", { body: { refreshToken: otherDeviceTokens.refreshToken } });
551
+ assert(
552
+ otherSessionRefreshRevoked.status === 401,
553
+ `…while the other, unrelated session for the same user can no longer refresh (got ${otherSessionRefreshRevoked.status})`,
554
+ );
555
+
556
+ console.log("11d-2. self-service profile update: no admin permission required, own row only");
557
+ const selfProfileUpdate = await call("PATCH", "/auth/me", {
558
+ token: identTokens.accessToken,
559
+ body: { displayName: "Self Updated", photo: "data:image/png;base64,AAAA" },
560
+ });
561
+ assert(selfProfileUpdate.status === 200, `a user can update their own profile with no admin permission (got ${selfProfileUpdate.status})`);
562
+ const selfProfile = selfProfileUpdate.body as { id: string; displayName: string | null; photo: string | null };
563
+ assert(selfProfile.displayName === "Self Updated", "…and the response reflects the new value");
564
+ assert(selfProfile.photo === "data:image/png;base64,AAAA", "…including the photo field");
565
+
566
+ const selfProfileRejectsNoToken = await call("PATCH", "/auth/me", { body: { displayName: "No Auth" } });
567
+ assert(selfProfileRejectsNoToken.status === 401, `updating a profile without a token is rejected (got ${selfProfileRejectsNoToken.status})`);
568
+
569
+ const meAfterProfileUpdate = await call("GET", "/auth/me", { token: identTokens.accessToken });
570
+ assert(
571
+ (meAfterProfileUpdate.body as { displayName: string | null }).displayName === "Self Updated",
572
+ `…and GET /auth/me reflects the same update, no separate refetch path (got ${JSON.stringify(meAfterProfileUpdate.body)})`,
573
+ );
574
+
575
+ console.log("11e. admin creates a user directly — no signup, usable immediately");
576
+ const createdEmail = uniqueEmail("admin-created");
577
+ const createUser = await call("POST", "/auth/admin/users", {
578
+ token: await admin.freshToken(),
579
+ workspaceId: admin.workspaceId,
580
+ body: { email: createdEmail, password: "admin-created-pw-12345", displayName: "Admin Created" },
581
+ });
582
+ assert(createUser.status === 201 || createUser.status === 200, `admin can create a user directly (got ${createUser.status})`);
583
+ const createdUser = createUser.body as { id: string; email: string; displayName: string | null; roles: string[] };
584
+ assert(createdUser.email === createdEmail, "…and the response is the new user, not something stale");
585
+ assert(createdUser.displayName === "Admin Created", "…with the profile fields given at creation time");
586
+
587
+ const createdUserCanLogin = await call("POST", "/auth/login", { body: { identifier: createdEmail, password: "admin-created-pw-12345" } });
588
+ assert(
589
+ createdUserCanLogin.status === 201 || createdUserCanLogin.status === 200,
590
+ `the account is usable immediately, no separate activation step (got ${createdUserCanLogin.status})`,
591
+ );
592
+
593
+ const duplicateCreate = await call("POST", "/auth/admin/users", {
594
+ token: await admin.freshToken(),
595
+ workspaceId: admin.workspaceId,
596
+ body: { email: createdEmail, password: "another-pw-12345" },
597
+ });
598
+ assert(duplicateCreate.status === 409, `creating a second account with the same email is rejected (got ${duplicateCreate.status})`);
599
+
600
+ const createWithRole = await call("POST", "/auth/admin/users", {
601
+ token: await admin.freshToken(),
602
+ workspaceId: admin.workspaceId,
603
+ body: { email: uniqueEmail("admin-created-role"), password: "admin-created-pw-67890", roles: [ROLE_SLUG] },
604
+ });
605
+ assert(createWithRole.status === 201 || createWithRole.status === 200, `admin can name specific roles at creation time (got ${createWithRole.status})`);
606
+ assert(
607
+ (createWithRole.body as { roles: string[] }).roles.includes(ROLE_SLUG),
608
+ `…and the named role — not the default — is what the new account actually holds (got ${JSON.stringify((createWithRole.body as { roles: string[] }).roles)})`,
609
+ );
610
+
611
+ console.log("12. permission enforcement: each admin route is opened by its own permission and by nothing else");
612
+ // The point of this section is that permissions — not roles — are the boundary. Every probe
613
+ // user below holds a real role that carries nothing, so "has a role" can never be mistaken
614
+ // for "has authority", and the only thing that ever changes between them is which single
615
+ // permission was granted directly.
616
+ const NO_PERMS_ROLE = `probe-no-perms-${RUN_ID}`;
617
+ const ASSIGNABLE_ROLE = `probe-assignable-${RUN_ID}`;
618
+
619
+ const noPermsRole = await call("POST", "/auth/admin/roles", { token: await admin.freshToken(), workspaceId: admin.workspaceId, body: { slug: NO_PERMS_ROLE } });
620
+ const assignableRole = await call("POST", "/auth/admin/roles", {
621
+ token: await admin.freshToken(),
622
+ workspaceId: admin.workspaceId,
623
+ body: { slug: ASSIGNABLE_ROLE },
624
+ });
625
+ assert(noPermsRole.status === 201 && assignableRole.status === 201, "the probe roles are created (neither carries any permission)");
626
+
627
+ const probeTargetEmail = uniqueEmail("probe-target");
628
+ const probeTargetTokens = await signup(probeTargetEmail, "probe-target-pw-1");
629
+ const probeTargetId = (await call("GET", "/auth/me", { token: probeTargetTokens.accessToken })).body.sub as string;
630
+ await hooks.admitUser(ctx, admin, { userId: probeTargetId, email: probeTargetEmail });
631
+
632
+ const probesFor = (seq: number) =>
633
+ adminRouteProbes({
634
+ targetUserId: probeTargetId,
635
+ roleId: (assignableRole.body as { id: string }).id,
636
+ roleSlug: ASSIGNABLE_ROLE,
637
+ newRoleSlug: `${ASSIGNABLE_ROLE}-new-${seq}`,
638
+ newPermissionSlug: `probe:defined-${RUN_ID}-${seq}`,
639
+ });
640
+
641
+ /** Runs every admin route as one caller and reports what each answered. Leaves the target user as it found it. */
642
+ async function runProbes(token: string, seq: number) {
643
+ const results: Array<{ probe: RouteProbe; status: number }> = [];
644
+ for (const probe of probesFor(seq)) {
645
+ const res = await call(probe.method, probe.path, { token, workspaceId: admin.workspaceId, body: probe.body });
646
+ results.push({ probe, status: res.status });
647
+ }
648
+ return results;
649
+ }
650
+
651
+ const succeeded = (status: number) => status === 200 || status === 201;
652
+ const refused = (rs: Array<{ probe: { label: string }; status: number }>) =>
653
+ rs
654
+ .filter((r) => r.status !== 403)
655
+ .map((r) => `${r.probe.label} -> ${r.status}`)
656
+ .join(", ");
657
+
658
+ /** A user with the empty role, plus exactly the permissions named — granted directly, never through a role. */
659
+ async function probeUser(label: string, grants: string[]) {
660
+ const email = uniqueEmail(label);
661
+ const password = `${label}-pw-12345`;
662
+ const tokens = await signup(email, password);
663
+ const userId = (await call("GET", "/auth/me", { token: tokens.accessToken })).body.sub as string;
664
+ await hooks.admitUser(ctx, admin, { userId, email });
665
+ await call("POST", `/auth/admin/users/${userId}/roles`, { token: await admin.freshToken(), workspaceId: admin.workspaceId, body: { role: NO_PERMS_ROLE } });
666
+ for (const permission of grants) {
667
+ await call("POST", `/auth/admin/users/${userId}/permissions`, { token: await admin.freshToken(), workspaceId: admin.workspaceId, body: { permission } });
668
+ }
669
+ // No re-login: authorization is read from the database on every request in both variants,
670
+ // so the token this user already holds sees the grants immediately.
671
+ return { email, password, userId, token: tokens.accessToken };
672
+ }
673
+
674
+ console.log("12a. holding a role that carries no permissions opens nothing");
675
+ const bare = await probeUser("probe-bare", []);
676
+ const bareMe = await call("GET", "/auth/me", { token: bare.token, workspaceId: admin.workspaceId });
677
+ assert(bareMe.body?.roles?.includes(NO_PERMS_ROLE), "the probe user really does hold a role — it just carries no permissions");
678
+ const bareResults = await runProbes(bare.token, 0);
679
+ assert(bareResults.every((r) => r.status === 403), `all ${bareResults.length} admin routes refuse them (offenders: ${refused(bareResults) || "none"})`);
680
+
681
+ console.log("12b. one directly-granted permission opens exactly the routes it names, and no others");
682
+ const permissions = [...new Set(probesFor(0).map((p) => p.permission))];
683
+ let probeSeq = 1;
684
+ /** Kept for 12d: one caller holding exactly one permission is the sharpest case for the equivalence check. */
685
+ let singleGrantUser: { token: string; permission: string } | undefined;
686
+ for (const permission of permissions) {
687
+ const holder = await probeUser(`probe-${permission.replace(/[^a-z]/g, "")}`, [permission]);
688
+ if (permission === "users:read") singleGrantUser = { token: holder.token, permission };
689
+ const results = await runProbes(holder.token, probeSeq++);
690
+ for (const r of results.filter((r) => r.probe.permission === permission)) {
691
+ assert(succeeded(r.status), `"${permission}" opens ${r.probe.label} (got ${r.status})`);
692
+ }
693
+ const others = results.filter((r) => r.probe.permission !== permission);
694
+ assert(others.every((r) => r.status === 403), `…and "${permission}" opens nothing else (offenders: ${refused(others) || "none"})`);
695
+ }
696
+
697
+ console.log("12c. revoking a permission takes it away again — on the same token, with no re-login");
698
+ const revokee = await probeUser("probe-revokee", ["users:read"]);
699
+ const beforeRevoke = await call("GET", "/auth/admin/users", { token: revokee.token, workspaceId: admin.workspaceId });
700
+ assert(beforeRevoke.status === 200, `the grant is in effect to begin with (got ${beforeRevoke.status})`);
701
+
702
+ const revoked = await call("POST", `/auth/admin/users/${revokee.userId}/permissions/${encodeURIComponent("users:read")}/revoke`, {
703
+ token: await admin.freshToken(),
704
+ workspaceId: admin.workspaceId,
705
+ });
706
+ assert(succeeded(revoked.status), `admin can revoke a direct grant (got ${revoked.status})`);
707
+
708
+ const afterRevokeCall = await call("GET", "/auth/admin/users", { token: revokee.token, workspaceId: admin.workspaceId });
709
+ assert(afterRevokeCall.status === 403, `and the route closes again on the very next request (got ${afterRevokeCall.status})`);
710
+
711
+ console.log("12d. front/back equivalence: an ability rebuilt from /auth/me answers exactly as the server enforces");
712
+ // This is the property the whole flat-slug model exists for. The client is given permission
713
+ // slugs and builds its ability with the *same* `defineAbilitiesFor` the server's guard used.
714
+ // Comparing predicted allow/deny against the real HTTP status for every gated route is what
715
+ // makes "the console cannot offer a button the API refuses" a fact rather than a hope.
716
+ async function proveEquivalence(label: string, token: string) {
717
+ const me = await call("GET", "/auth/me", { token, workspaceId: admin.workspaceId });
718
+ const ability = defineAbilitiesFor((me.body?.permissions ?? []) as string[]);
719
+ const results = await runProbes(token, probeSeq++);
720
+
721
+ const disagreements = results
722
+ .map((r) => ({ label: r.probe.label, predicted: ability.can(r.probe.permission, ABILITY_SUBJECT), served: r.status !== 403 }))
723
+ .filter((r) => r.predicted !== r.served);
724
+ assert(
725
+ disagreements.length === 0,
726
+ `${label}: all ${results.length} gated routes answer exactly as the client-side ability predicts` +
727
+ (disagreements.length ? ` (disagreements: ${disagreements.map((d) => `${d.label} predicted=${d.predicted}`).join(", ")})` : ""),
728
+ );
729
+ return new Set(results.filter((r) => r.status !== 403).map((r) => r.probe.label));
730
+ }
731
+
732
+ const adminAllows = await proveEquivalence("admin", await admin.freshToken());
733
+ const bareAllows = await proveEquivalence("a member holding a role with no permissions", bare.token);
734
+ const grantAllows = await proveEquivalence(`a user holding only "${singleGrantUser!.permission}"`, singleGrantUser!.token);
735
+ assert(
736
+ adminAllows.size > grantAllows.size && grantAllows.size > bareAllows.size && bareAllows.size === 0,
737
+ `…and the three callers genuinely differ, so the comparison is not vacuous (admin=${adminAllows.size}, single-grant=${grantAllows.size}, bare=${bareAllows.size})`,
738
+ );
739
+
740
+ console.log("12e. a permission edited in the database changes enforcement, with no code change and no redeploy");
741
+ // `isActive: false` is the deny: the row stops appearing in any ability that would have
742
+ // carried it, without unpicking a single grant. Written here through the admin API — each
743
+ // variant's hooks prove the same thing again with a raw database write, to rule out the API
744
+ // doing anything in memory.
745
+ const deactivated = await call("POST", "/auth/admin/permissions", {
746
+ token: await admin.freshToken(),
747
+ workspaceId: admin.workspaceId,
748
+ body: { slug: "users:read", isActive: false },
749
+ });
750
+ assert(succeeded(deactivated.status), `an admin can deactivate a permission (got ${deactivated.status})`);
751
+
752
+ const listAfterDeactivate = await call("GET", "/auth/admin/permissions", { token: await admin.freshToken(), workspaceId: admin.workspaceId });
753
+ assert(
754
+ (listAfterDeactivate.body?.permissions ?? []).some((p: { slug: string; isActive: boolean }) => p.slug === "users:read" && p.isActive === false),
755
+ "the catalog reports it as inactive",
756
+ );
757
+
758
+ const deniedByEdit = await call("GET", "/auth/admin/users", { token: singleGrantUser!.token, workspaceId: admin.workspaceId });
759
+ assert(deniedByEdit.status === 403, `the route it opened is now refused for a user whose grant is untouched (got ${deniedByEdit.status})`);
760
+
761
+ const adminDeniedToo = await call("GET", "/auth/admin/users", { token: await admin.freshToken(), workspaceId: admin.workspaceId });
762
+ assert(adminDeniedToo.status === 403, `…including for the administrator, whose role still carries it (got ${adminDeniedToo.status})`);
763
+
764
+ const reactivated = await call("POST", "/auth/admin/permissions", {
765
+ token: await admin.freshToken(),
766
+ workspaceId: admin.workspaceId,
767
+ body: { slug: "users:read", isActive: true },
768
+ });
769
+ assert(succeeded(reactivated.status), `an admin can reactivate it (got ${reactivated.status})`);
770
+ const restored = await call("GET", "/auth/admin/users", { token: singleGrantUser!.token, workspaceId: admin.workspaceId });
771
+ assert(restored.status === 200, `and every grant that pointed at it works again immediately (got ${restored.status})`);
772
+
773
+ console.log(`13. ${hooks.variant}-specific properties`);
774
+ await hooks.proveVariantProperties(ctx, admin);
775
+ } finally {
776
+ await app.close();
777
+ }
778
+
779
+ if (failures > 0) {
780
+ console.error(`\n${failures} assertion(s) failed`);
781
+ process.exit(1);
782
+ }
783
+ console.log("\nAll assertions passed.");
784
+ }
785
+
786
+ main().catch((err) => {
787
+ console.error(err);
788
+ process.exit(1);
789
+ });