nextly 0.0.1 → 0.0.2-alpha.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 (268) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +122 -0
  3. package/dist/_dts-chunks/collections-handler.d-DjgO74Wt.d.ts +20540 -0
  4. package/dist/_dts-chunks/config.d-DNwsDnjs.d.ts +2589 -0
  5. package/dist/_dts-chunks/define-component.d-BUgTHmt3.d.ts +1149 -0
  6. package/dist/_dts-chunks/image-processor.d-OO1PmMrv.d.ts +335 -0
  7. package/dist/_dts-chunks/index.d-axCAzZ7m.d.ts +17842 -0
  8. package/dist/_dts-chunks/media.d-DjDOZo4B.d.ts +117 -0
  9. package/dist/_dts-chunks/on-error.d-CHIKWNxd.d.ts +38 -0
  10. package/dist/_dts-chunks/storage.d-BUhQ2we_.d.ts +404 -0
  11. package/dist/actions/index.d.ts +239 -0
  12. package/dist/actions/index.mjs +281 -0
  13. package/dist/api/auth-state.d.ts +5 -0
  14. package/dist/api/auth-state.mjs +131 -0
  15. package/dist/api/collections-schema-detail.d.ts +56 -0
  16. package/dist/api/collections-schema-detail.mjs +244 -0
  17. package/dist/api/collections-schema-export.d.ts +56 -0
  18. package/dist/api/collections-schema-export.mjs +129 -0
  19. package/dist/api/collections-schema.d.ts +59 -0
  20. package/dist/api/collections-schema.mjs +207 -0
  21. package/dist/api/components-detail.d.ts +50 -0
  22. package/dist/api/components-detail.mjs +132 -0
  23. package/dist/api/components.d.ts +69 -0
  24. package/dist/api/components.mjs +144 -0
  25. package/dist/api/email-providers-default.d.ts +40 -0
  26. package/dist/api/email-providers-default.mjs +75 -0
  27. package/dist/api/email-providers-detail.d.ts +81 -0
  28. package/dist/api/email-providers-detail.mjs +109 -0
  29. package/dist/api/email-providers-test.d.ts +43 -0
  30. package/dist/api/email-providers-test.mjs +114 -0
  31. package/dist/api/email-providers.d.ts +69 -0
  32. package/dist/api/email-providers.mjs +110 -0
  33. package/dist/api/email-send-template.d.ts +41 -0
  34. package/dist/api/email-send-template.mjs +58 -0
  35. package/dist/api/email-send.d.ts +42 -0
  36. package/dist/api/email-send.mjs +58 -0
  37. package/dist/api/email-templates-detail.d.ts +74 -0
  38. package/dist/api/email-templates-detail.mjs +112 -0
  39. package/dist/api/email-templates-layout.d.ts +55 -0
  40. package/dist/api/email-templates-layout.mjs +92 -0
  41. package/dist/api/email-templates-preview.d.ts +48 -0
  42. package/dist/api/email-templates-preview.mjs +93 -0
  43. package/dist/api/email-templates.d.ts +61 -0
  44. package/dist/api/email-templates.mjs +118 -0
  45. package/dist/api/health.d.ts +68 -0
  46. package/dist/api/health.mjs +67 -0
  47. package/dist/api/index.d.ts +54 -0
  48. package/dist/api/index.mjs +16 -0
  49. package/dist/api/media-bulk.d.ts +74 -0
  50. package/dist/api/media-bulk.mjs +196 -0
  51. package/dist/api/media-folders.d.ts +112 -0
  52. package/dist/api/media-folders.mjs +187 -0
  53. package/dist/api/media-handlers.d.ts +102 -0
  54. package/dist/api/media-handlers.mjs +437 -0
  55. package/dist/api/media.d.ts +117 -0
  56. package/dist/api/media.mjs +242 -0
  57. package/dist/api/singles-detail.d.ts +87 -0
  58. package/dist/api/singles-detail.mjs +170 -0
  59. package/dist/api/singles-schema-detail.d.ts +54 -0
  60. package/dist/api/singles-schema-detail.mjs +182 -0
  61. package/dist/api/singles.d.ts +34 -0
  62. package/dist/api/singles.mjs +94 -0
  63. package/dist/api/storage-upload-url.d.ts +48 -0
  64. package/dist/api/storage-upload-url.mjs +202 -0
  65. package/dist/api/uploads.d.ts +109 -0
  66. package/dist/api/uploads.mjs +359 -0
  67. package/dist/auth/index.d.ts +425 -0
  68. package/dist/auth/index.mjs +199 -0
  69. package/dist/boot-apply-PQSYLDIN.mjs +7 -0
  70. package/dist/chunk-2OALJTK6.mjs +489 -0
  71. package/dist/chunk-2Q2SX2CS.mjs +365 -0
  72. package/dist/chunk-2TFX4ND3.mjs +13 -0
  73. package/dist/chunk-2TWPDSYD.mjs +87 -0
  74. package/dist/chunk-2W3DVD7S.mjs +647 -0
  75. package/dist/chunk-2ZFKXPQM.mjs +88 -0
  76. package/dist/chunk-3FA7FKAV.mjs +832 -0
  77. package/dist/chunk-3NZ2KMBL.mjs +58 -0
  78. package/dist/chunk-4MJLT6PZ.mjs +0 -0
  79. package/dist/chunk-56WO4WX7.mjs +0 -0
  80. package/dist/chunk-5APFUGAD.mjs +89 -0
  81. package/dist/chunk-5HMZ644B.mjs +108 -0
  82. package/dist/chunk-67GXH6PR.mjs +32 -0
  83. package/dist/chunk-6JNEPWRW.mjs +14368 -0
  84. package/dist/chunk-6NFHQIJD.mjs +45 -0
  85. package/dist/chunk-7P6ASYW6.mjs +9 -0
  86. package/dist/chunk-A3WPLSDT.mjs +1364 -0
  87. package/dist/chunk-AGJ6F2T3.mjs +144 -0
  88. package/dist/chunk-AK6Z23OX.mjs +1464 -0
  89. package/dist/chunk-APKKRD2G.mjs +102 -0
  90. package/dist/chunk-B2GV2BWH.mjs +73 -0
  91. package/dist/chunk-D5HQBNUB.mjs +74 -0
  92. package/dist/chunk-DNNG377Z.mjs +204 -0
  93. package/dist/chunk-DP3G27G5.mjs +135 -0
  94. package/dist/chunk-DV6WVX2Q.mjs +0 -0
  95. package/dist/chunk-DXGGXIUZ.mjs +57 -0
  96. package/dist/chunk-EGXBZCGC.mjs +943 -0
  97. package/dist/chunk-ERCNLX3V.mjs +176 -0
  98. package/dist/chunk-FQULBZ53.mjs +850 -0
  99. package/dist/chunk-G2AA4QLC.mjs +262 -0
  100. package/dist/chunk-GDBJ5JCU.mjs +488 -0
  101. package/dist/chunk-GJNSJU4S.mjs +19 -0
  102. package/dist/chunk-GZ6DCQKC.mjs +69 -0
  103. package/dist/chunk-H26B4FYG.mjs +167 -0
  104. package/dist/chunk-I4JMR3UR.mjs +21 -0
  105. package/dist/chunk-INV7QKLG.mjs +508 -0
  106. package/dist/chunk-IUDOC7N7.mjs +46 -0
  107. package/dist/chunk-IZWPRDC3.mjs +206 -0
  108. package/dist/chunk-KIMNCZGV.mjs +15 -0
  109. package/dist/chunk-L6HW2DA7.mjs +15 -0
  110. package/dist/chunk-LAZXX4HR.mjs +100 -0
  111. package/dist/chunk-LDKCUMHK.mjs +95 -0
  112. package/dist/chunk-LRXMECUA.mjs +0 -0
  113. package/dist/chunk-M52VMPGA.mjs +119 -0
  114. package/dist/chunk-MGUWEEI6.mjs +160 -0
  115. package/dist/chunk-NRUWQ5Z7.mjs +419 -0
  116. package/dist/chunk-NSEFNNU4.mjs +25360 -0
  117. package/dist/chunk-NTHVDFGO.mjs +138 -0
  118. package/dist/chunk-O3QHXMOX.mjs +3166 -0
  119. package/dist/chunk-P7NH2OSC.mjs +2605 -0
  120. package/dist/chunk-PKMABBB5.mjs +184 -0
  121. package/dist/chunk-PWS6XGJK.mjs +76 -0
  122. package/dist/chunk-R6JJQHFC.mjs +20 -0
  123. package/dist/chunk-RJLLGGPG.mjs +0 -0
  124. package/dist/chunk-SBACDPNX.mjs +689 -0
  125. package/dist/chunk-TO5AFLVQ.mjs +124 -0
  126. package/dist/chunk-TS7GHTG2.mjs +5436 -0
  127. package/dist/chunk-UJ2IMJ4W.mjs +133 -0
  128. package/dist/chunk-UOP63Q54.mjs +102 -0
  129. package/dist/chunk-UUOFWCM6.mjs +78 -0
  130. package/dist/chunk-V4EQTOA4.mjs +893 -0
  131. package/dist/chunk-VJ66NCL4.mjs +193 -0
  132. package/dist/chunk-VQJQHVEV.mjs +29 -0
  133. package/dist/chunk-VTJADRO3.mjs +141 -0
  134. package/dist/chunk-VWF3JO32.mjs +0 -0
  135. package/dist/chunk-W4MGXIRR.mjs +27 -0
  136. package/dist/chunk-W5KKPZT5.mjs +1204 -0
  137. package/dist/chunk-WD34YQ6T.mjs +381 -0
  138. package/dist/chunk-WZBYMYVW.mjs +14 -0
  139. package/dist/chunk-X23WKS3Z.mjs +50 -0
  140. package/dist/chunk-X7TXCYYN.mjs +6496 -0
  141. package/dist/chunk-XGI4EMS3.mjs +140 -0
  142. package/dist/chunk-XZKLBMN6.mjs +1153 -0
  143. package/dist/chunk-YB7INWPY.mjs +0 -0
  144. package/dist/chunk-YV4Y7SDL.mjs +83 -0
  145. package/dist/chunk-YZNBLFIW.mjs +1688 -0
  146. package/dist/chunk-YZZCTONM.mjs +263 -0
  147. package/dist/chunk-ZE6A3FYH.mjs +289 -0
  148. package/dist/cli/nextly.mjs +68 -0
  149. package/dist/cli/utils/index.d.ts +449 -0
  150. package/dist/cli/utils/index.mjs +49 -0
  151. package/dist/component-schema-service-5577KVW6.mjs +11 -0
  152. package/dist/config-loader-23YEMC3Z.mjs +23 -0
  153. package/dist/config.d.ts +44 -0
  154. package/dist/config.mjs +109 -0
  155. package/dist/container-ORGFGYSZ.mjs +9 -0
  156. package/dist/database/index.d.ts +12 -0
  157. package/dist/database/index.mjs +40 -0
  158. package/dist/database/seeders/index.d.ts +93 -0
  159. package/dist/database/seeders/index.mjs +47 -0
  160. package/dist/db-sync-demote-LJGKLB3S.mjs +117 -0
  161. package/dist/db-sync-promote-B26VSYQF.mjs +113 -0
  162. package/dist/dev-reload-broadcaster-B73IQ53V.mjs +25 -0
  163. package/dist/dist-M2NOU37V.mjs +19 -0
  164. package/dist/drizzle-kit-lazy-D2M2PXR2.mjs +13 -0
  165. package/dist/dynamic-collection-schema-service-IEXTPIZ7.mjs +8 -0
  166. package/dist/errors/index.d.ts +159 -0
  167. package/dist/errors/index.mjs +10 -0
  168. package/dist/factory-IWMBKUJM.mjs +15 -0
  169. package/dist/first-run-QIVKWJIF.mjs +63 -0
  170. package/dist/fresh-push-NR67DC3R.mjs +8 -0
  171. package/dist/index.d.ts +4175 -0
  172. package/dist/index.mjs +1336 -0
  173. package/dist/local-plugin-PTET4NAT.mjs +7 -0
  174. package/dist/logger-NU46DXNY.mjs +15 -0
  175. package/dist/logger-YE4TC7ZN.mjs +9 -0
  176. package/dist/migration-journal-EP532Y4L.mjs +139 -0
  177. package/dist/migrations/mysql/0000_eager_sentry.sql +174 -0
  178. package/dist/migrations/mysql/0001_soft_giant_girl.sql +27 -0
  179. package/dist/migrations/mysql/0002_media_table.sql +24 -0
  180. package/dist/migrations/mysql/0003_dynamic_singles.sql +37 -0
  181. package/dist/migrations/mysql/0004_dynamic_components.sql +35 -0
  182. package/dist/migrations/mysql/0005_user_management_tables.sql +92 -0
  183. package/dist/migrations/mysql/0006_api_keys.sql +36 -0
  184. package/dist/migrations/mysql/0007_general_settings.sql +20 -0
  185. package/dist/migrations/mysql/0008_site_settings_logo_url.sql +9 -0
  186. package/dist/migrations/mysql/0009_activity_log.sql +30 -0
  187. package/dist/migrations/mysql/0010_site_settings_sidebar.sql +13 -0
  188. package/dist/migrations/mysql/0011_missing_tables_and_columns.sql +54 -0
  189. package/dist/migrations/mysql/0012_image_sizes_and_focal_point.sql +30 -0
  190. package/dist/migrations/mysql/0012_media_folders.sql +43 -0
  191. package/dist/migrations/mysql/0013_user_brute_force_protection.sql +31 -0
  192. package/dist/migrations/mysql/0014_email_template_attachments.sql +12 -0
  193. package/dist/migrations/mysql/0015_media_uploaded_by_nullable.sql +15 -0
  194. package/dist/migrations/mysql/20260429_000000_000_initial_journal.sql +22 -0
  195. package/dist/migrations/mysql/20260501_000000_journal_batch.sql +17 -0
  196. package/dist/migrations/mysql/20260501_000001_audit_log.sql +24 -0
  197. package/dist/migrations/mysql/20260504_000000_nextly_meta.sql +21 -0
  198. package/dist/migrations/mysql/meta/0000_snapshot.json +1005 -0
  199. package/dist/migrations/mysql/meta/0001_snapshot.json +1099 -0
  200. package/dist/migrations/mysql/meta/_journal.json +41 -0
  201. package/dist/migrations/postgresql/0000_misty_king_bedlam.sql +169 -0
  202. package/dist/migrations/postgresql/0001_perpetual_captain_marvel.sql +8 -0
  203. package/dist/migrations/postgresql/0002_sad_spectrum.sql +16 -0
  204. package/dist/migrations/postgresql/0003_hesitant_ultron.sql +17 -0
  205. package/dist/migrations/postgresql/0004_media_table.sql +24 -0
  206. package/dist/migrations/postgresql/0005_media_folders.sql +36 -0
  207. package/dist/migrations/postgresql/0006_dynamic_collections_update.sql +50 -0
  208. package/dist/migrations/postgresql/0007_dynamic_singles.sql +38 -0
  209. package/dist/migrations/postgresql/0008_dynamic_components.sql +37 -0
  210. package/dist/migrations/postgresql/0009_user_management_tables.sql +95 -0
  211. package/dist/migrations/postgresql/0010_api_keys.sql +34 -0
  212. package/dist/migrations/postgresql/0011_general_settings.sql +20 -0
  213. package/dist/migrations/postgresql/0012_site_settings_logo_url.sql +9 -0
  214. package/dist/migrations/postgresql/0013_activity_log.sql +29 -0
  215. package/dist/migrations/postgresql/0014_image_sizes_and_focal_point.sql +33 -0
  216. package/dist/migrations/postgresql/0014_site_settings_sidebar.sql +13 -0
  217. package/dist/migrations/postgresql/0015_user_brute_force_protection.sql +29 -0
  218. package/dist/migrations/postgresql/0016_email_template_attachments.sql +12 -0
  219. package/dist/migrations/postgresql/0017_media_uploaded_by_nullable.sql +15 -0
  220. package/dist/migrations/postgresql/20260429_000000_000_initial_journal.sql +24 -0
  221. package/dist/migrations/postgresql/20260501_000000_journal_batch.sql +17 -0
  222. package/dist/migrations/postgresql/20260501_000001_audit_log.sql +24 -0
  223. package/dist/migrations/postgresql/20260504_000000_nextly_meta.sql +22 -0
  224. package/dist/migrations/postgresql/meta/0000_snapshot.json +1286 -0
  225. package/dist/migrations/postgresql/meta/0001_snapshot.json +1407 -0
  226. package/dist/migrations/postgresql/meta/0002_snapshot.json +1552 -0
  227. package/dist/migrations/postgresql/meta/0003_snapshot.json +1695 -0
  228. package/dist/migrations/postgresql/meta/0010_snapshot.json +2345 -0
  229. package/dist/migrations/postgresql/meta/_journal.json +90 -0
  230. package/dist/migrations/sqlite/0000_api_keys.sql +34 -0
  231. package/dist/migrations/sqlite/0001_general_settings.sql +20 -0
  232. package/dist/migrations/sqlite/0002_site_settings_logo_url.sql +9 -0
  233. package/dist/migrations/sqlite/0003_activity_log.sql +29 -0
  234. package/dist/migrations/sqlite/0004_image_sizes_and_focal_point.sql +29 -0
  235. package/dist/migrations/sqlite/0004_site_settings_sidebar.sql +11 -0
  236. package/dist/migrations/sqlite/0005_user_brute_force_protection.sql +29 -0
  237. package/dist/migrations/sqlite/0006_email_template_attachments.sql +12 -0
  238. package/dist/migrations/sqlite/0007_media_uploaded_by_nullable.sql +111 -0
  239. package/dist/migrations/sqlite/20260429_000000_000_initial_journal.sql +24 -0
  240. package/dist/migrations/sqlite/20260501_000000_journal_batch.sql +19 -0
  241. package/dist/migrations/sqlite/20260501_000001_audit_log.sql +24 -0
  242. package/dist/migrations/sqlite/20260504_000000_nextly_meta.sql +21 -0
  243. package/dist/migrations/sqlite/20260505_000000_user_management_tables.sql +77 -0
  244. package/dist/next.d.ts +57 -0
  245. package/dist/next.mjs +55 -0
  246. package/dist/observability/index.d.ts +87 -0
  247. package/dist/observability/index.mjs +57 -0
  248. package/dist/permissions-3DZZQZMI.mjs +39 -0
  249. package/dist/pipeline-YOML7SWF.mjs +29 -0
  250. package/dist/preview-ZZTR3QGS.mjs +9 -0
  251. package/dist/program-PW6UB2ZC.mjs +5934 -0
  252. package/dist/reconcile-single-tables-7ENVXJGB.mjs +7 -0
  253. package/dist/register-SF6E6FVU.mjs +49 -0
  254. package/dist/reload-config-HWQ4G5MM.mjs +23 -0
  255. package/dist/resolve-single-table-name-JSOMUB3R.mjs +7 -0
  256. package/dist/routeHandler-UNMMJIBM.mjs +77 -0
  257. package/dist/runtime-schema-generator-NRA6A6Z6.mjs +8 -0
  258. package/dist/runtime.d.ts +120 -0
  259. package/dist/runtime.mjs +73 -0
  260. package/dist/schema-hash-FMMG6VPJ.mjs +13 -0
  261. package/dist/schema-registry-EQ36FZDP.mjs +7 -0
  262. package/dist/scripts/load-env.mjs +42 -0
  263. package/dist/storage/index.d.ts +566 -0
  264. package/dist/storage/index.mjs +45 -0
  265. package/dist/super-admin-G5ZK5F4T.mjs +39 -0
  266. package/dist/system-table-service-WGSRVEGT.mjs +17 -0
  267. package/dist/users-7KELGRYJ.mjs +38 -0
  268. package/package.json +308 -9
@@ -0,0 +1,1204 @@
1
+ import {
2
+ BaseService
3
+ } from "./chunk-2W3DVD7S.mjs";
4
+ import {
5
+ getDialectTables
6
+ } from "./chunk-TS7GHTG2.mjs";
7
+ import {
8
+ getAuthLogger
9
+ } from "./chunk-LAZXX4HR.mjs";
10
+ import {
11
+ container
12
+ } from "./chunk-D5HQBNUB.mjs";
13
+
14
+ // src/schemas/validation.ts
15
+ import { z } from "zod";
16
+ var PaginationSchema = z.object({
17
+ page: z.number().int().min(1).default(1),
18
+ limit: z.number().int().min(1).max(100).default(10),
19
+ offset: z.number().int().min(0).optional()
20
+ });
21
+ var PaginatedResponseSchema = (dataSchema) => z.object({
22
+ data: z.array(dataSchema),
23
+ pagination: z.object({
24
+ page: z.number().int().min(1),
25
+ limit: z.number().int().min(1),
26
+ total: z.number().int().min(0),
27
+ totalPages: z.number().int().min(0),
28
+ hasNext: z.boolean(),
29
+ hasPrev: z.boolean()
30
+ })
31
+ });
32
+ var SortOrderSchema = z.enum(["asc", "desc"]).default("asc");
33
+ var SortSchema = z.object({
34
+ field: z.string().min(1, "Sort field is required"),
35
+ order: SortOrderSchema
36
+ });
37
+ var DateRangeSchema = z.object({
38
+ from: z.date().optional(),
39
+ to: z.date().optional()
40
+ }).refine((data) => !data.from || !data.to || data.from <= data.to, {
41
+ message: "From date must be before or equal to to date",
42
+ path: ["from"]
43
+ });
44
+ var SearchSchema = z.object({
45
+ query: z.string().min(1, "Search query is required").max(255, "Search query too long"),
46
+ fields: z.array(z.string()).optional()
47
+ });
48
+ var SuccessResponseSchema = z.object({
49
+ success: z.literal(true),
50
+ message: z.string().optional(),
51
+ data: z.any().optional()
52
+ });
53
+ var ErrorResponseSchema = z.object({
54
+ success: z.literal(false),
55
+ error: z.string(),
56
+ code: z.string().optional(),
57
+ details: z.any().optional()
58
+ });
59
+ var ValidationErrorSchema = z.object({
60
+ success: z.literal(false),
61
+ error: z.literal("Validation failed"),
62
+ details: z.array(
63
+ z.object({
64
+ field: z.string(),
65
+ message: z.string(),
66
+ code: z.string().optional()
67
+ })
68
+ )
69
+ });
70
+ var BulkOperationSchema = z.object({
71
+ ids: z.array(z.string()).min(1, "At least one ID is required"),
72
+ operation: z.enum(["delete", "update", "activate", "deactivate"])
73
+ });
74
+ var BulkOperationResponseSchema = z.object({
75
+ success: z.boolean(),
76
+ processed: z.number().int().min(0),
77
+ failed: z.number().int().min(0),
78
+ errors: z.array(
79
+ z.object({
80
+ id: z.string(),
81
+ error: z.string()
82
+ })
83
+ ).optional()
84
+ });
85
+ var FileUploadSchema = z.object({
86
+ filename: z.string().min(1, "Filename is required"),
87
+ mimetype: z.string().min(1, "MIME type is required"),
88
+ size: z.number().int().min(1, "File size must be positive"),
89
+ buffer: z.instanceof(Buffer).optional()
90
+ });
91
+ var ImageUploadSchema = FileUploadSchema.extend({
92
+ mimetype: z.string().regex(/^image\//, "File must be an image"),
93
+ size: z.number().int().max(5 * 1024 * 1024, "Image size must be less than 5MB")
94
+ // 5MB limit
95
+ });
96
+ var EmailSchema = z.string().email("Invalid email format").transform((v) => v.trim().toLowerCase());
97
+ var PasswordSchema = z.string().min(8, "Password must be at least 8 characters").max(128, "Password must be less than 128 characters").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/\d/, "Password must contain at least one number").regex(
98
+ /[^A-Za-z0-9]/,
99
+ "Password must contain at least one special character"
100
+ );
101
+ var UrlSchema = z.string().url("Invalid URL format");
102
+ var PhoneSchema = z.string().regex(/^\+?[\d\s\-\\(\\)]+$/, "Invalid phone number format").min(10, "Phone number too short").max(20, "Phone number too long");
103
+
104
+ // src/auth/password/index.ts
105
+ import bcrypt from "bcryptjs";
106
+ var defaultSaltRounds = 12;
107
+ async function hashPassword(plain, saltRounds = defaultSaltRounds) {
108
+ if (!plain) {
109
+ throw new Error("hashPassword: plain must be non-empty");
110
+ }
111
+ const salt = await bcrypt.genSalt(saltRounds);
112
+ return bcrypt.hash(plain, salt);
113
+ }
114
+ async function verifyPassword(plain, hash) {
115
+ if (!plain || !hash) return false;
116
+ try {
117
+ return await bcrypt.compare(plain, hash);
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+ function validatePasswordStrength(password) {
123
+ const result = PasswordSchema.safeParse(password);
124
+ if (result.success) {
125
+ return { ok: true };
126
+ } else {
127
+ return {
128
+ ok: false,
129
+ errors: result.error.issues.map(
130
+ (issue) => issue.message
131
+ )
132
+ };
133
+ }
134
+ }
135
+
136
+ // src/services/lib/permissions.ts
137
+ import { and as and3, eq as eq3, inArray as inArray3, ne } from "drizzle-orm";
138
+
139
+ // src/domains/auth/services/permission-cache-service.ts
140
+ import { and as and2, eq as eq2, gt, lt, sql } from "drizzle-orm";
141
+
142
+ // src/domains/auth/services/permission-checker-service.ts
143
+ import { inArray as inArray2 } from "drizzle-orm";
144
+
145
+ // src/domains/auth/services/role-inheritance-service.ts
146
+ import { and, eq, inArray } from "drizzle-orm";
147
+ var MAX_ROLE_HIERARCHY_DEPTH = 2e3;
148
+ var RoleInheritanceService = class extends BaseService {
149
+ constructor(adapter, logger) {
150
+ super(adapter, logger);
151
+ }
152
+ /**
153
+ * Add a parent-child role inheritance relationship.
154
+ *
155
+ * The child role will inherit all permissions from the parent role.
156
+ * Multi-level inheritance is supported.
157
+ *
158
+ * @param childRoleId - Child role ID (inherits from parent)
159
+ * @param parentRoleId - Parent role ID (provides permissions)
160
+ * @throws Error if self-inheritance or cycle would be created
161
+ */
162
+ async addRoleInheritance(childRoleId, parentRoleId) {
163
+ if (childRoleId === parentRoleId) throw new Error("INHERIT_SELF_FORBIDDEN");
164
+ if (await this.willCreateCycle(childRoleId, parentRoleId))
165
+ throw new Error("INHERIT_CYCLE_FORBIDDEN");
166
+ const duplicate = await this.db.query.roleInherits.findFirst({
167
+ where: and(
168
+ eq(this.tables.roleInherits.parentRoleId, parentRoleId),
169
+ eq(this.tables.roleInherits.childRoleId, childRoleId)
170
+ ),
171
+ columns: {
172
+ id: true
173
+ }
174
+ });
175
+ if (!duplicate) {
176
+ const id = `${parentRoleId}::${childRoleId}`;
177
+ try {
178
+ const inheritanceData = {
179
+ id,
180
+ childRoleId,
181
+ parentRoleId
182
+ };
183
+ const insert = this.db.insert(this.tables.roleInherits).values(inheritanceData);
184
+ if (typeof insert.onConflictDoNothing === "function") {
185
+ await insert.onConflictDoNothing();
186
+ } else {
187
+ await insert;
188
+ }
189
+ } catch (e) {
190
+ const error = e;
191
+ const code = String(error?.code || "");
192
+ const msg = String(error?.message || "").toLowerCase();
193
+ if (!(code === "ER_DUP_ENTRY" || msg.includes("duplicate"))) throw e;
194
+ }
195
+ }
196
+ void invalidatePermissionCache({ roleId: childRoleId });
197
+ }
198
+ /**
199
+ * Remove a parent-child role inheritance relationship.
200
+ *
201
+ * @param childRoleId - Child role ID
202
+ * @param parentRoleId - Parent role ID
203
+ */
204
+ async removeRoleInheritance(childRoleId, parentRoleId) {
205
+ await this.db.delete(this.tables.roleInherits).where(
206
+ and(
207
+ eq(this.tables.roleInherits.childRoleId, childRoleId),
208
+ eq(this.tables.roleInherits.parentRoleId, parentRoleId)
209
+ )
210
+ );
211
+ void invalidatePermissionCache({ roleId: childRoleId });
212
+ }
213
+ /**
214
+ * List all ancestor roles (parents, grandparents, etc.) for a given role.
215
+ *
216
+ * Uses breadth-first traversal to find all roles in the hierarchy above this role.
217
+ * Note: The starting roleId is NOT included in the results - only its ancestors.
218
+ *
219
+ * Safety limit: Stops after visiting MAX_ROLE_HIERARCHY_DEPTH roles to prevent infinite loops.
220
+ *
221
+ * @param roleId - Role ID to find ancestors for
222
+ * @returns Array of ancestor role IDs (excluding the starting roleId)
223
+ */
224
+ async listAncestorRoles(roleId) {
225
+ const visited = /* @__PURE__ */ new Set();
226
+ const queue = [roleId];
227
+ while (queue.length) {
228
+ const batch = queue.splice(0, 50);
229
+ const inheritances = await this.db.query.roleInherits.findMany({
230
+ where: inArray(this.tables.roleInherits.childRoleId, batch),
231
+ columns: {
232
+ parentRoleId: true
233
+ }
234
+ });
235
+ for (const r of inheritances) {
236
+ const parent = String(r.parentRoleId);
237
+ if (!visited.has(parent)) {
238
+ visited.add(parent);
239
+ queue.push(parent);
240
+ }
241
+ }
242
+ if (visited.size > MAX_ROLE_HIERARCHY_DEPTH) break;
243
+ }
244
+ return Array.from(visited);
245
+ }
246
+ /**
247
+ * List all descendant roles (children, grandchildren, etc.) for a given role.
248
+ *
249
+ * Uses breadth-first traversal to find all roles in the hierarchy below this role.
250
+ * Note: The starting roleId is NOT included in the results - only its descendants.
251
+ *
252
+ * Safety limit: Stops after visiting MAX_ROLE_HIERARCHY_DEPTH roles to prevent infinite loops.
253
+ *
254
+ * @param roleId - Role ID to find descendants for
255
+ * @returns Array of descendant role IDs (excluding the starting roleId)
256
+ */
257
+ async listDescendantRoles(roleId) {
258
+ const visited = /* @__PURE__ */ new Set();
259
+ const queue = [roleId];
260
+ while (queue.length) {
261
+ const batch = queue.splice(0, 50);
262
+ const inheritances = await this.db.query.roleInherits.findMany({
263
+ where: inArray(this.tables.roleInherits.parentRoleId, batch),
264
+ columns: {
265
+ childRoleId: true
266
+ }
267
+ });
268
+ for (const r of inheritances) {
269
+ const child = String(r.childRoleId);
270
+ if (!visited.has(child)) {
271
+ visited.add(child);
272
+ queue.push(child);
273
+ }
274
+ }
275
+ if (visited.size > MAX_ROLE_HIERARCHY_DEPTH) break;
276
+ }
277
+ return Array.from(visited);
278
+ }
279
+ async willCreateCycle(childRoleId, parentRoleId) {
280
+ const ancestorsOfParent = await this.listAncestorRoles(parentRoleId);
281
+ return ancestorsOfParent.includes(childRoleId);
282
+ }
283
+ };
284
+
285
+ // src/domains/auth/services/permission-checker-service.ts
286
+ var PermissionCheckerService = class extends BaseService {
287
+ roleInheritanceService;
288
+ constructor(adapter, logger) {
289
+ super(adapter, logger);
290
+ this.roleInheritanceService = new RoleInheritanceService(adapter, logger);
291
+ }
292
+ /**
293
+ * Get all permissions for a given role (direct + inherited).
294
+ *
295
+ * @param roleId - Role ID to get permissions for
296
+ * @returns Array of permission IDs (deduplicated)
297
+ */
298
+ async getAllPermissionsForRole(roleId) {
299
+ const childRoleIds = await this.roleInheritanceService.listDescendantRoles(roleId);
300
+ const allRoleIds = [roleId, ...childRoleIds];
301
+ const allPermissions = await this.db.select({
302
+ permissionId: this.tables.rolePermissions.permissionId
303
+ }).from(this.tables.rolePermissions).where(inArray2(this.tables.rolePermissions.roleId, allRoleIds));
304
+ const permissionSet = /* @__PURE__ */ new Set();
305
+ for (const perm of allPermissions) {
306
+ permissionSet.add(String(perm.permissionId));
307
+ }
308
+ return Array.from(permissionSet);
309
+ }
310
+ };
311
+
312
+ // src/domains/auth/services/permission-cache-service.ts
313
+ var errorLogRateLimiter = /* @__PURE__ */ new Map();
314
+ var ERROR_LOG_INTERVAL_MS = 6e4;
315
+ function shouldLogError(errorKey) {
316
+ const now = Date.now();
317
+ const lastLogged = errorLogRateLimiter.get(errorKey);
318
+ if (!lastLogged || now - lastLogged > ERROR_LOG_INTERVAL_MS) {
319
+ errorLogRateLimiter.set(errorKey, now);
320
+ return true;
321
+ }
322
+ return false;
323
+ }
324
+ var PermissionCacheService = class extends BaseService {
325
+ permissionChecker;
326
+ cacheTtlMs;
327
+ constructor(adapter, logger, options) {
328
+ super(adapter, logger);
329
+ this.permissionChecker = new PermissionCheckerService(adapter, logger);
330
+ const ttlSeconds = options?.cacheTtlSeconds ?? 86400;
331
+ this.cacheTtlMs = ttlSeconds * 1e3;
332
+ }
333
+ /**
334
+ * Serialize a `roleIds` array for the user_permission_cache.role_ids
335
+ * column.
336
+ *
337
+ * The column type differs by dialect:
338
+ * - PostgreSQL: jsonb (Drizzle stringifies the JS array)
339
+ * - MySQL: json (Drizzle stringifies the JS array)
340
+ * - SQLite: text (no automatic serialization; a raw array
341
+ * coerces via `String([a, b])` to "a,b" which
342
+ * is not valid JSON, breaking the
343
+ * `json_each(role_ids)` invalidation query)
344
+ *
345
+ * Match the column type per dialect so PG/MySQL keep the structured
346
+ * value and SQLite gets a JSON-parseable string.
347
+ */
348
+ serializeRoleIdsForDb(roleIds) {
349
+ return this.dialect === "sqlite" ? JSON.stringify(roleIds) : roleIds;
350
+ }
351
+ /**
352
+ * Pre-compute and store all permissions for a user (cache warming).
353
+ *
354
+ * This method is called on:
355
+ * - User login
356
+ * - Role assignment changes
357
+ * - Permission changes affecting user's roles
358
+ *
359
+ * Performance: O(n) where n = number of possible permission checks (~20-50 typically)
360
+ *
361
+ * @param userId - User ID to warm cache for
362
+ * @returns Promise that resolves when cache is warmed
363
+ */
364
+ async warmCacheForUser(userId) {
365
+ if (!userId) {
366
+ getAuthLogger()?.log?.("warn", {
367
+ category: "auth",
368
+ op: "cache",
369
+ message: "warmCacheForUser called with empty userId"
370
+ });
371
+ return;
372
+ }
373
+ try {
374
+ const { permissions, userPermissionCache } = this.tables;
375
+ const allPermissions = await this.db.select({
376
+ id: permissions.id,
377
+ action: permissions.action,
378
+ resource: permissions.resource
379
+ }).from(permissions);
380
+ if (allPermissions.length === 0) {
381
+ return;
382
+ }
383
+ const roleIds = await this.permissionChecker.getAllPermissionsForRole(userId);
384
+ const expiresAt = new Date(Date.now() + this.cacheTtlMs);
385
+ const entries = [];
386
+ const serializedRoleIds = this.serializeRoleIdsForDb(Array.from(roleIds));
387
+ for (const perm of allPermissions) {
388
+ const hasPermission2 = roleIds.includes(perm.id);
389
+ const cacheKey = `${userId}|${perm.action}|${perm.resource}`;
390
+ entries.push({
391
+ id: cacheKey,
392
+ userId,
393
+ action: perm.action,
394
+ resource: perm.resource,
395
+ hasPermission: hasPermission2,
396
+ roleIds: serializedRoleIds,
397
+ expiresAt,
398
+ createdAt: /* @__PURE__ */ new Date()
399
+ });
400
+ }
401
+ if (entries.length > 0) {
402
+ await this.db.insert(userPermissionCache).values(entries).onConflictDoUpdate({
403
+ target: userPermissionCache.id,
404
+ set: {
405
+ hasPermission: sql`EXCLUDED.has_permission`,
406
+ roleIds: sql`EXCLUDED.role_ids`,
407
+ expiresAt: sql`EXCLUDED.expires_at`,
408
+ createdAt: sql`EXCLUDED.created_at`
409
+ }
410
+ });
411
+ }
412
+ if (process.env.DEBUG_CACHE === "1") {
413
+ console.log("[cache][dbg] warmCacheForUser", {
414
+ userId,
415
+ entriesCount: entries.length,
416
+ ttlMs: this.cacheTtlMs
417
+ });
418
+ }
419
+ } catch (error) {
420
+ getAuthLogger()?.log?.("error", {
421
+ category: "auth",
422
+ op: "cache",
423
+ message: "warmCacheForUser failed",
424
+ userId,
425
+ error: String(error)
426
+ });
427
+ throw error;
428
+ }
429
+ }
430
+ /**
431
+ * Get cached permission result from database.
432
+ *
433
+ * Returns:
434
+ * - `true` if permission is cached and granted
435
+ * - `false` if permission is cached and denied
436
+ * - `null` if cache miss (not cached or expired)
437
+ *
438
+ * Performance: <5ms (indexed lookup on composite key)
439
+ *
440
+ * @param userId - User ID
441
+ * @param action - Permission action (create, read, update, delete)
442
+ * @param resource - Permission resource (users, roles, permissions, etc.)
443
+ * @returns Promise resolving to boolean if cached, null if miss
444
+ */
445
+ async getCachedPermission(userId, action, resource) {
446
+ if (!userId || !action || !resource) {
447
+ return null;
448
+ }
449
+ try {
450
+ const { userPermissionCache } = this.tables;
451
+ const cacheKey = `${userId}|${action}|${resource}`;
452
+ const now = /* @__PURE__ */ new Date();
453
+ const result = await this.db.select({
454
+ hasPermission: userPermissionCache.hasPermission,
455
+ expiresAt: userPermissionCache.expiresAt
456
+ }).from(userPermissionCache).where(
457
+ and2(
458
+ eq2(userPermissionCache.id, cacheKey),
459
+ // gt() lets Drizzle convert `now` (Date) to the column's typed
460
+ // representation (epoch seconds for SQLite mode:"timestamp",
461
+ // native timestamp for PG/MySQL). Raw `sql\`${col} > ${now}\``
462
+ // bypassed that conversion and SQLite drivers refused to bind
463
+ // a Date object — `TypeError: SQLite3 can only bind numbers,
464
+ // strings, bigints, buffers, and null` on every authed request.
465
+ // Mirrors the same-file `cleanupExpired` pattern at line 481.
466
+ gt(userPermissionCache.expiresAt, now)
467
+ )
468
+ ).limit(1);
469
+ if (result.length === 0) {
470
+ return null;
471
+ }
472
+ const cached = result[0];
473
+ if (process.env.DEBUG_CACHE === "1") {
474
+ console.log("[cache][dbg] getCachedPermission HIT", {
475
+ userId,
476
+ action,
477
+ resource,
478
+ hasPermission: cached.hasPermission,
479
+ expiresAt: cached.expiresAt
480
+ });
481
+ }
482
+ return cached.hasPermission;
483
+ } catch (error) {
484
+ getAuthLogger()?.log?.("error", {
485
+ category: "auth",
486
+ op: "cache",
487
+ message: "getCachedPermission failed",
488
+ userId,
489
+ action,
490
+ resource,
491
+ error: String(error)
492
+ });
493
+ return null;
494
+ }
495
+ }
496
+ /**
497
+ * Store permission result in database cache.
498
+ *
499
+ * Uses UPSERT (INSERT ON CONFLICT) to handle concurrent writes.
500
+ *
501
+ * Performance: <3ms (single indexed insert with ON CONFLICT)
502
+ *
503
+ * @param userId - User ID
504
+ * @param action - Permission action
505
+ * @param resource - Permission resource
506
+ * @param hasPermission - Whether user has the permission
507
+ * @param roleIds - Role IDs involved (for invalidation)
508
+ * @returns Promise that resolves when cache is updated
509
+ */
510
+ async setCachedPermission(userId, action, resource, hasPermission2, roleIds) {
511
+ if (!userId || !action || !resource) {
512
+ return;
513
+ }
514
+ try {
515
+ const { userPermissionCache } = this.tables;
516
+ const cacheKey = `${userId}|${action}|${resource}`;
517
+ const expiresAt = new Date(Date.now() + this.cacheTtlMs);
518
+ await this.db.insert(userPermissionCache).values({
519
+ id: cacheKey,
520
+ userId,
521
+ action,
522
+ resource,
523
+ hasPermission: hasPermission2,
524
+ roleIds: this.serializeRoleIdsForDb(roleIds),
525
+ expiresAt,
526
+ createdAt: /* @__PURE__ */ new Date()
527
+ }).onConflictDoUpdate({
528
+ target: userPermissionCache.id,
529
+ set: {
530
+ hasPermission: sql`EXCLUDED.has_permission`,
531
+ roleIds: sql`EXCLUDED.role_ids`,
532
+ expiresAt: sql`EXCLUDED.expires_at`,
533
+ createdAt: sql`EXCLUDED.created_at`
534
+ }
535
+ });
536
+ if (process.env.DEBUG_CACHE === "1") {
537
+ console.log("[cache][dbg] setCachedPermission", {
538
+ userId,
539
+ action,
540
+ resource,
541
+ hasPermission: hasPermission2,
542
+ roleIds,
543
+ expiresAt
544
+ });
545
+ }
546
+ } catch (error) {
547
+ const errorKey = `setCachedPermission:${userId}`;
548
+ if (shouldLogError(errorKey)) {
549
+ getAuthLogger()?.log?.("error", {
550
+ category: "auth",
551
+ op: "cache",
552
+ message: "setCachedPermission failed (rate-limited, showing 1/min max)",
553
+ userId,
554
+ action,
555
+ resource,
556
+ error: String(error)
557
+ });
558
+ }
559
+ }
560
+ }
561
+ /**
562
+ * Invalidate all cached permissions for a user.
563
+ *
564
+ * Called on:
565
+ * - Role assignment/removal
566
+ * - User deactivation
567
+ * - Manual cache clear
568
+ *
569
+ * Uses write-through invalidation (tombstone pattern) to prevent race conditions:
570
+ * 1. Mark entries as expired (expiresAt = now) - creates tombstone
571
+ * 2. Any concurrent reads will see expired entries and recompute
572
+ * 3. Concurrent writes will overwrite tombstones with fresh data
573
+ *
574
+ * Performance: <10ms (indexed update by userId)
575
+ *
576
+ * @param userId - User ID to invalidate
577
+ * @returns Promise resolving to number of entries invalidated
578
+ */
579
+ async invalidateByUser(userId) {
580
+ if (!userId) {
581
+ return 0;
582
+ }
583
+ try {
584
+ const { userPermissionCache } = this.tables;
585
+ const result = await this.db.update(userPermissionCache).set({ expiresAt: /* @__PURE__ */ new Date() }).where(eq2(userPermissionCache.userId, userId));
586
+ const invalidatedCount = result.rowCount ?? 0;
587
+ if (process.env.DEBUG_CACHE === "1") {
588
+ console.log("[cache][dbg] invalidateByUser", {
589
+ userId,
590
+ invalidatedCount,
591
+ method: "tombstone"
592
+ });
593
+ }
594
+ return invalidatedCount;
595
+ } catch (error) {
596
+ getAuthLogger()?.log?.("error", {
597
+ category: "auth",
598
+ op: "cache",
599
+ message: "invalidateByUser failed",
600
+ userId,
601
+ error: String(error)
602
+ });
603
+ return 0;
604
+ }
605
+ }
606
+ /**
607
+ * Invalidate cached permissions for all users with a specific role.
608
+ *
609
+ * Called on:
610
+ * - Role permission changes
611
+ * - Role deletion
612
+ * - Permission changes
613
+ *
614
+ * Uses write-through invalidation (tombstone pattern) to prevent race conditions:
615
+ * 1. Mark entries as expired (expiresAt = now) - creates tombstone
616
+ * 2. Any concurrent reads will see expired entries and recompute
617
+ * 3. Concurrent writes will overwrite tombstones with fresh data
618
+ *
619
+ * Uses JSONB contains operator to find affected users.
620
+ *
621
+ * Performance: O(n) where n = users with role (~10-500ms depending on data)
622
+ *
623
+ * @param roleId - Role ID to invalidate
624
+ * @returns Promise resolving to number of entries invalidated
625
+ */
626
+ async invalidateByRole(roleId) {
627
+ if (!roleId) {
628
+ return 0;
629
+ }
630
+ try {
631
+ const { userPermissionCache } = this.tables;
632
+ const containsRoleClause = this.dialect === "postgresql" ? sql`${userPermissionCache.roleIds} @> ${JSON.stringify([roleId])}` : this.dialect === "mysql" ? sql`JSON_CONTAINS(${userPermissionCache.roleIds}, ${JSON.stringify(roleId)})` : sql`EXISTS (SELECT 1 FROM json_each(${userPermissionCache.roleIds}) WHERE value = ${roleId})`;
633
+ const result = await this.db.update(userPermissionCache).set({ expiresAt: /* @__PURE__ */ new Date() }).where(containsRoleClause);
634
+ const invalidatedCount = result.rowCount ?? 0;
635
+ if (process.env.DEBUG_CACHE === "1") {
636
+ console.log("[cache][dbg] invalidateByRole", {
637
+ roleId,
638
+ invalidatedCount,
639
+ method: "tombstone"
640
+ });
641
+ }
642
+ return invalidatedCount;
643
+ } catch (error) {
644
+ getAuthLogger()?.log?.("error", {
645
+ category: "auth",
646
+ op: "cache",
647
+ message: "invalidateByRole failed",
648
+ roleId,
649
+ error: String(error)
650
+ });
651
+ return 0;
652
+ }
653
+ }
654
+ /**
655
+ * Remove expired cache entries (background maintenance).
656
+ *
657
+ * Should run periodically via cron job (recommended: daily).
658
+ *
659
+ * Performance: <100ms for 10k entries, uses indexed scan on expiresAt
660
+ *
661
+ * @returns Promise resolving to number of entries deleted
662
+ */
663
+ async cleanupExpired() {
664
+ try {
665
+ const { userPermissionCache } = this.tables;
666
+ const now = /* @__PURE__ */ new Date();
667
+ const result = await this.db.delete(userPermissionCache).where(lt(userPermissionCache.expiresAt, now));
668
+ const deletedCount = result.rowCount ?? 0;
669
+ if (process.env.DEBUG_CACHE === "1" || deletedCount > 0) {
670
+ console.log("[cache][info] cleanupExpired", {
671
+ deletedCount,
672
+ timestamp: now.toISOString()
673
+ });
674
+ }
675
+ return deletedCount;
676
+ } catch (error) {
677
+ getAuthLogger()?.log?.("error", {
678
+ category: "auth",
679
+ op: "cache",
680
+ message: "cleanupExpired failed",
681
+ error: String(error)
682
+ });
683
+ return 0;
684
+ }
685
+ }
686
+ };
687
+
688
+ // src/services/lib/permissions.ts
689
+ if (typeof window !== "undefined") {
690
+ throw new Error(
691
+ "[nextly] Direct API permissions module loaded in a browser context. Direct API is server-only \u2014 import only from Server Components, Route Handlers, or Server Actions, never from client components."
692
+ );
693
+ }
694
+ function getDb() {
695
+ const adapter = container.get("adapter");
696
+ return adapter.getDrizzle();
697
+ }
698
+ function getAdapter() {
699
+ return container.get("adapter");
700
+ }
701
+ function getLogger() {
702
+ return container.has("logger") ? container.get("logger") : console;
703
+ }
704
+ var CACHE_ENABLED = process.env.PERMISSION_CACHE_ENABLED !== "false" && process.env.PERMISSION_CACHE_ENABLED !== "0";
705
+ var CACHE_TTL_SECONDS = parseInt(
706
+ process.env.PERMISSION_CACHE_TTL_SECONDS || "86400",
707
+ 10
708
+ );
709
+ var _dialectTables = null;
710
+ function getTablesLazy() {
711
+ if (!_dialectTables) {
712
+ _dialectTables = getDialectTables();
713
+ }
714
+ return _dialectTables;
715
+ }
716
+ var PermissionChecker = class {
717
+ memo = /* @__PURE__ */ new Map();
718
+ t = getTablesLazy();
719
+ cacheService = null;
720
+ constructor() {
721
+ this.t = getTablesLazy();
722
+ if (CACHE_ENABLED) {
723
+ try {
724
+ this.cacheService = new PermissionCacheService(
725
+ getAdapter(),
726
+ getLogger(),
727
+ {
728
+ cacheTtlSeconds: CACHE_TTL_SECONDS
729
+ }
730
+ );
731
+ } catch (error) {
732
+ getAuthLogger()?.log?.("warn", {
733
+ category: "auth",
734
+ op: "cache",
735
+ message: "Failed to initialize PermissionCacheService",
736
+ error: String(error)
737
+ });
738
+ }
739
+ }
740
+ }
741
+ async hasPermission(userId, action, resource) {
742
+ if (!userId || !action || !resource) {
743
+ getAuthLogger()?.log?.("debug", {
744
+ category: "auth",
745
+ op: "error",
746
+ userId,
747
+ action,
748
+ resource
749
+ });
750
+ return false;
751
+ }
752
+ const key = `${userId}|${action}|${resource}`;
753
+ const cached = this.memo.get(key);
754
+ if (typeof cached === "boolean") return cached;
755
+ const hit = cache.get(key);
756
+ if (hit) {
757
+ if (hit.expiresAt > Date.now()) {
758
+ this.memo.set(key, hit.value);
759
+ cache.delete(key);
760
+ cache.set(key, hit);
761
+ return hit.value;
762
+ }
763
+ cache.delete(key);
764
+ const rids = keyToRoleIds.get(key);
765
+ keyToRoleIds.delete(key);
766
+ if (rids) for (const rid of rids) roleIdToKeys.get(rid)?.delete(key);
767
+ userIdToKeys.get(userId)?.delete(key);
768
+ }
769
+ if (this.cacheService) {
770
+ try {
771
+ const dbCached = await this.cacheService.getCachedPermission(
772
+ userId,
773
+ action,
774
+ resource
775
+ );
776
+ if (dbCached !== null) {
777
+ this.memo.set(key, dbCached);
778
+ setCacheEntry(key, dbCached, userId, []);
779
+ return dbCached;
780
+ }
781
+ } catch (error) {
782
+ getAuthLogger()?.log?.("warn", {
783
+ category: "auth",
784
+ op: "cache",
785
+ message: "DB cache lookup failed, falling back to fresh computation",
786
+ userId,
787
+ action,
788
+ resource,
789
+ error: String(error)
790
+ });
791
+ }
792
+ }
793
+ try {
794
+ const roleIds = await this.getAllRoleIdsForUser(userId);
795
+ if (roleIds.size === 0) {
796
+ this.memo.set(key, false);
797
+ if (this.cacheService) {
798
+ void this.cacheService.setCachedPermission(
799
+ userId,
800
+ action,
801
+ resource,
802
+ false,
803
+ []
804
+ );
805
+ }
806
+ return false;
807
+ }
808
+ const allowed = await this.roleSetHasPermission(
809
+ Array.from(roleIds),
810
+ action,
811
+ resource
812
+ );
813
+ this.memo.set(key, allowed);
814
+ setCacheEntry(key, allowed, userId, Array.from(roleIds));
815
+ if (this.cacheService) {
816
+ void this.cacheService.setCachedPermission(
817
+ userId,
818
+ action,
819
+ resource,
820
+ allowed,
821
+ Array.from(roleIds)
822
+ );
823
+ }
824
+ return allowed;
825
+ } catch {
826
+ getAuthLogger()?.log?.("error", {
827
+ category: "auth",
828
+ op: "error",
829
+ userId,
830
+ action,
831
+ resource
832
+ });
833
+ return false;
834
+ }
835
+ }
836
+ async hasAnyPermission(userId, checks) {
837
+ if (!userId || !Array.isArray(checks) || checks.length === 0) return false;
838
+ for (const c of checks) {
839
+ if (await this.hasPermission(userId, c.action, c.resource)) return true;
840
+ }
841
+ return false;
842
+ }
843
+ async hasAllPermissions(userId, checks) {
844
+ if (!userId || !Array.isArray(checks) || checks.length === 0) return false;
845
+ for (const c of checks) {
846
+ if (!await this.hasPermission(userId, c.action, c.resource))
847
+ return false;
848
+ }
849
+ return true;
850
+ }
851
+ async getAllRoleIdsForUser(userId) {
852
+ const direct = await this.getDirectRoleIds(userId);
853
+ if (direct.size === 0) return direct;
854
+ const all = new Set(direct);
855
+ const queue = Array.from(direct);
856
+ const visited = new Set(queue);
857
+ const { roleInherits } = this.t;
858
+ while (queue.length > 0) {
859
+ const batch = queue.splice(0, 50);
860
+ const parentRows = await getDb().select({ parentRoleId: roleInherits.parentRoleId }).from(roleInherits).where(inArray3(roleInherits.childRoleId, batch));
861
+ for (const r of parentRows) {
862
+ const parentRoleId = String(r.parentRoleId);
863
+ if (!visited.has(parentRoleId)) {
864
+ visited.add(parentRoleId);
865
+ all.add(parentRoleId);
866
+ queue.push(parentRoleId);
867
+ }
868
+ }
869
+ const childRows = await getDb().select({ childRoleId: roleInherits.childRoleId }).from(roleInherits).where(inArray3(roleInherits.parentRoleId, batch));
870
+ for (const r of childRows) {
871
+ const childRoleId = String(r.childRoleId);
872
+ if (!visited.has(childRoleId)) {
873
+ visited.add(childRoleId);
874
+ all.add(childRoleId);
875
+ queue.push(childRoleId);
876
+ }
877
+ }
878
+ if (visited.size > 2e3) {
879
+ getAuthLogger()?.log?.("warn", {
880
+ category: "auth",
881
+ op: "error",
882
+ userId
883
+ });
884
+ break;
885
+ }
886
+ }
887
+ return all;
888
+ }
889
+ async getDirectRoleIds(userId) {
890
+ const { userRoles } = this.t;
891
+ const rows = await getDb().select({ roleId: userRoles.roleId }).from(userRoles).where(eq3(userRoles.userId, userId));
892
+ return new Set(
893
+ rows.map((r) => String(r.roleId))
894
+ );
895
+ }
896
+ async roleSetHasPermission(roleIds, action, resource) {
897
+ if (roleIds.length === 0) return false;
898
+ const { roles, rolePermissions, permissions } = this.t;
899
+ try {
900
+ const superAdmin = await getDb().select({ id: roles.id }).from(roles).where(and3(inArray3(roles.id, roleIds), eq3(roles.slug, "super-admin"))).limit(1);
901
+ if (superAdmin.length > 0) return true;
902
+ const perm = await getDb().select({ id: permissions.id }).from(permissions).where(
903
+ and3(
904
+ eq3(permissions.action, action),
905
+ eq3(permissions.resource, resource)
906
+ )
907
+ ).limit(1);
908
+ const permId = perm?.[0]?.id ?? null;
909
+ if (!permId) return false;
910
+ const rows = await getDb().select({ id: rolePermissions.id }).from(rolePermissions).where(
911
+ and3(
912
+ inArray3(rolePermissions.roleId, roleIds),
913
+ eq3(rolePermissions.permissionId, permId)
914
+ )
915
+ ).limit(1);
916
+ return rows.length > 0;
917
+ } catch {
918
+ return false;
919
+ }
920
+ }
921
+ };
922
+ var cacheTtlMs = 6e4;
923
+ var cacheMaxEntries = parseInt(process.env.PERMISSION_CACHE_MEMORY_SIZE ?? "10000", 10) || 1e4;
924
+ var cache = /* @__PURE__ */ new Map();
925
+ var keyToRoleIds = /* @__PURE__ */ new Map();
926
+ var roleIdToKeys = /* @__PURE__ */ new Map();
927
+ var userIdToKeys = /* @__PURE__ */ new Map();
928
+ function setCacheEntry(key, value, userId, roleIds) {
929
+ if (cache.size >= cacheMaxEntries) {
930
+ const oldest = cache.keys().next().value;
931
+ if (oldest) {
932
+ cache.delete(oldest);
933
+ const rids = keyToRoleIds.get(oldest);
934
+ keyToRoleIds.delete(oldest);
935
+ if (rids) for (const rid of rids) roleIdToKeys.get(rid)?.delete(oldest);
936
+ const u = oldest.split("|", 1)[0];
937
+ userIdToKeys.get(u)?.delete(oldest);
938
+ if (userIdToKeys.get(u)?.size === 0) userIdToKeys.delete(u);
939
+ }
940
+ }
941
+ cache.set(key, { value, expiresAt: Date.now() + cacheTtlMs });
942
+ const roleSet = new Set(roleIds);
943
+ keyToRoleIds.set(key, roleSet);
944
+ for (const rid of roleSet) {
945
+ if (!roleIdToKeys.has(rid)) roleIdToKeys.set(rid, /* @__PURE__ */ new Set());
946
+ roleIdToKeys.get(rid).add(key);
947
+ }
948
+ if (!userIdToKeys.has(userId)) userIdToKeys.set(userId, /* @__PURE__ */ new Set());
949
+ userIdToKeys.get(userId).add(key);
950
+ }
951
+ async function hasPermission(userId, action, resource) {
952
+ try {
953
+ const checker = new PermissionChecker();
954
+ return await checker.hasPermission(userId, action, resource);
955
+ } catch {
956
+ getAuthLogger()?.log?.("error", {
957
+ category: "auth",
958
+ op: "error",
959
+ userId,
960
+ action,
961
+ resource
962
+ });
963
+ return false;
964
+ }
965
+ }
966
+ async function hasAnyPermission(userId, checks) {
967
+ try {
968
+ const checker = new PermissionChecker();
969
+ return await checker.hasAnyPermission(userId, checks);
970
+ } catch {
971
+ getAuthLogger()?.log?.("error", { category: "auth", op: "error", userId });
972
+ return false;
973
+ }
974
+ }
975
+ async function listEffectivePermissions(userId) {
976
+ if (!userId) {
977
+ getAuthLogger()?.log?.("debug", {
978
+ category: "auth",
979
+ op: "permissions",
980
+ error: "missing userId"
981
+ });
982
+ return [];
983
+ }
984
+ try {
985
+ const checker = new PermissionChecker();
986
+ const roleIds = await checker.getAllRoleIdsForUser(userId);
987
+ if (roleIds.size === 0) {
988
+ return [];
989
+ }
990
+ const t = getTablesLazy();
991
+ const { rolePermissions, permissions } = t;
992
+ const rows = await getDb().select({
993
+ action: permissions.action,
994
+ resource: permissions.resource
995
+ }).from(rolePermissions).innerJoin(permissions, eq3(rolePermissions.permissionId, permissions.id)).where(inArray3(rolePermissions.roleId, Array.from(roleIds)));
996
+ const permissionStrings = /* @__PURE__ */ new Set();
997
+ for (const row of rows) {
998
+ permissionStrings.add(`${row.resource}:${row.action}`);
999
+ }
1000
+ const result = Array.from(permissionStrings).sort();
1001
+ if (process.env.DEBUG_RBAC === "1") {
1002
+ console.log("[permissions][dbg] listEffectivePermissions", {
1003
+ userId,
1004
+ roleCount: roleIds.size,
1005
+ permissionCount: result.length,
1006
+ permissions: result
1007
+ });
1008
+ }
1009
+ return result;
1010
+ } catch (error) {
1011
+ getAuthLogger()?.log?.("error", {
1012
+ category: "auth",
1013
+ op: "permissions",
1014
+ userId,
1015
+ error: String(error)
1016
+ });
1017
+ return [];
1018
+ }
1019
+ }
1020
+ async function invalidatePermissionCache(_hint = {}) {
1021
+ const { userId, roleId } = _hint || {};
1022
+ if (userId) {
1023
+ const keys = userIdToKeys.get(userId);
1024
+ if (keys) {
1025
+ for (const k of keys) {
1026
+ cache.delete(k);
1027
+ const rids = keyToRoleIds.get(k);
1028
+ keyToRoleIds.delete(k);
1029
+ if (rids) for (const rid of rids) roleIdToKeys.get(rid)?.delete(k);
1030
+ }
1031
+ userIdToKeys.delete(userId);
1032
+ }
1033
+ }
1034
+ if (roleId) {
1035
+ const keys = roleIdToKeys.get(roleId);
1036
+ if (keys) {
1037
+ for (const k of keys) {
1038
+ cache.delete(k);
1039
+ const rids = keyToRoleIds.get(k);
1040
+ keyToRoleIds.delete(k);
1041
+ if (rids) for (const rid of rids) roleIdToKeys.get(rid)?.delete(k);
1042
+ const uid = k.split("|", 1)[0];
1043
+ userIdToKeys.get(uid)?.delete(k);
1044
+ if (userIdToKeys.get(uid)?.size === 0) userIdToKeys.delete(uid);
1045
+ }
1046
+ roleIdToKeys.delete(roleId);
1047
+ }
1048
+ }
1049
+ if (CACHE_ENABLED) {
1050
+ try {
1051
+ const cacheService = new PermissionCacheService(
1052
+ getAdapter(),
1053
+ getLogger(),
1054
+ {
1055
+ cacheTtlSeconds: CACHE_TTL_SECONDS
1056
+ }
1057
+ );
1058
+ if (userId) {
1059
+ await cacheService.invalidateByUser(userId);
1060
+ }
1061
+ if (roleId) {
1062
+ await cacheService.invalidateByRole(roleId);
1063
+ }
1064
+ } catch (error) {
1065
+ getAuthLogger()?.log?.("error", {
1066
+ category: "auth",
1067
+ op: "cache",
1068
+ message: "DB cache invalidation failed",
1069
+ userId,
1070
+ roleId,
1071
+ error: String(error)
1072
+ });
1073
+ }
1074
+ }
1075
+ }
1076
+ var superAdminCache = /* @__PURE__ */ new Map();
1077
+ var SUPER_ADMIN_CACHE_TTL_MS = 6e4;
1078
+ async function isSuperAdmin(userId) {
1079
+ if (!userId) return false;
1080
+ const cached = superAdminCache.get(userId);
1081
+ if (cached && cached.expiresAt > Date.now()) {
1082
+ return cached.value;
1083
+ }
1084
+ try {
1085
+ const checker = new PermissionChecker();
1086
+ const roleIds = await checker.getAllRoleIdsForUser(userId);
1087
+ if (roleIds.size === 0) {
1088
+ superAdminCache.set(userId, {
1089
+ value: false,
1090
+ expiresAt: Date.now() + SUPER_ADMIN_CACHE_TTL_MS
1091
+ });
1092
+ return false;
1093
+ }
1094
+ const t = getTablesLazy();
1095
+ const { roles } = t;
1096
+ const superAdmin = await getDb().select({ id: roles.id }).from(roles).where(
1097
+ and3(
1098
+ inArray3(roles.id, Array.from(roleIds)),
1099
+ eq3(roles.slug, "super-admin")
1100
+ )
1101
+ ).limit(1);
1102
+ const result = superAdmin.length > 0;
1103
+ superAdminCache.set(userId, {
1104
+ value: result,
1105
+ expiresAt: Date.now() + SUPER_ADMIN_CACHE_TTL_MS
1106
+ });
1107
+ if (superAdminCache.size > 1e3) {
1108
+ const oldest = superAdminCache.keys().next().value;
1109
+ if (oldest) superAdminCache.delete(oldest);
1110
+ }
1111
+ return result;
1112
+ } catch (error) {
1113
+ getAuthLogger()?.log?.("error", {
1114
+ category: "auth",
1115
+ op: "permissions",
1116
+ userId,
1117
+ error: String(error)
1118
+ });
1119
+ return false;
1120
+ }
1121
+ }
1122
+ async function hasSuperAdminExcluding(excludeUserId) {
1123
+ if (!excludeUserId) return false;
1124
+ try {
1125
+ const t = getTablesLazy();
1126
+ const { roles, userRoles } = t;
1127
+ const superAdminRole = await getDb().select({ id: roles.id }).from(roles).where(eq3(roles.slug, "super-admin")).limit(1);
1128
+ if (superAdminRole.length === 0) return false;
1129
+ const superAdminRoleId = superAdminRole[0].id;
1130
+ const otherSuperAdmins = await getDb().select({ userId: userRoles.userId }).from(userRoles).where(
1131
+ and3(
1132
+ eq3(userRoles.roleId, superAdminRoleId),
1133
+ ne(userRoles.userId, excludeUserId)
1134
+ )
1135
+ ).limit(1);
1136
+ return otherSuperAdmins.length > 0;
1137
+ } catch {
1138
+ return true;
1139
+ }
1140
+ }
1141
+ async function containsSuperAdminRole(roleIds) {
1142
+ if (!roleIds || roleIds.length === 0) return false;
1143
+ try {
1144
+ const t = getTablesLazy();
1145
+ const { roles } = t;
1146
+ const rows = await getDb().select({ id: roles.id }).from(roles).where(and3(inArray3(roles.id, roleIds), eq3(roles.slug, "super-admin"))).limit(1);
1147
+ return rows.length > 0;
1148
+ } catch {
1149
+ return false;
1150
+ }
1151
+ }
1152
+ async function listRoleSlugsForUser(userId) {
1153
+ if (!userId) return [];
1154
+ try {
1155
+ const checker = new PermissionChecker();
1156
+ const roleIds = await checker.getAllRoleIdsForUser(userId);
1157
+ if (roleIds.size === 0) return [];
1158
+ const t = getTablesLazy();
1159
+ const { roles } = t;
1160
+ const rows = await getDb().select({ slug: roles.slug }).from(roles).where(inArray3(roles.id, Array.from(roleIds)));
1161
+ return rows.map((r) => r.slug);
1162
+ } catch (error) {
1163
+ getAuthLogger()?.log?.("error", {
1164
+ category: "auth",
1165
+ op: "permissions",
1166
+ userId,
1167
+ error: String(error)
1168
+ });
1169
+ return [];
1170
+ }
1171
+ }
1172
+
1173
+ export {
1174
+ PaginationSchema,
1175
+ PaginatedResponseSchema,
1176
+ SortOrderSchema,
1177
+ SortSchema,
1178
+ DateRangeSchema,
1179
+ SearchSchema,
1180
+ SuccessResponseSchema,
1181
+ ErrorResponseSchema,
1182
+ ValidationErrorSchema,
1183
+ BulkOperationSchema,
1184
+ BulkOperationResponseSchema,
1185
+ FileUploadSchema,
1186
+ ImageUploadSchema,
1187
+ EmailSchema,
1188
+ PasswordSchema,
1189
+ UrlSchema,
1190
+ PhoneSchema,
1191
+ hashPassword,
1192
+ verifyPassword,
1193
+ validatePasswordStrength,
1194
+ hasPermission,
1195
+ hasAnyPermission,
1196
+ listEffectivePermissions,
1197
+ invalidatePermissionCache,
1198
+ isSuperAdmin,
1199
+ hasSuperAdminExcluding,
1200
+ containsSuperAdminRole,
1201
+ listRoleSlugsForUser,
1202
+ RoleInheritanceService,
1203
+ PermissionCheckerService
1204
+ };