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,2589 @@
1
+ import { bc as BaseService, bd as Logger, be as BaseRegistryService, bf as DynamicCollectionRecord, bg as MigrationStatus, bh as PermissionSeedService, bi as BaseListOptions, bj as CollectionSource, bk as BaseListResult, bl as DynamicCollectionInsert, bm as FieldDefinition, bn as CollectionMetadataService, bo as CollectionEntryService, bp as RequestContext, bq as PaginatedResult, br as QueryOptions, P as FieldConfig, bs as HookRegistry, o as CollectionConfig, aQ as SingleConfig, t as ComponentConfig, bt as UserConfig, bu as EmailConfig, bv as UserService, bw as MediaService, bx as SingleRegistryService, by as SingleEntryService, bz as ComponentRegistryService, bA as ComponentDataService, bB as CollectionRelationshipService, bC as UserExtSchemaService, bD as EmailProviderService, bE as EmailTemplateService, bF as EmailService, bG as UserFieldDefinitionService, bH as RBACAccessControlService, bI as ApiKeyService, bJ as AuthService, bK as DatabaseInstance, bL as HookType, $ as HookHandler } from './collections-handler.d-DjgO74Wt.d.ts';
2
+ import { DrizzleAdapter } from '@nextlyhq/adapter-drizzle';
3
+ import { z } from 'zod';
4
+ import { S as StoragePlugin, g as ImageProcessor } from './image-processor.d-OO1PmMrv.d.ts';
5
+ import { TransactionContext } from '@nextlyhq/adapter-drizzle/types';
6
+ import { M as MediaStorage } from './storage.d-BUhQ2we_.d.ts';
7
+
8
+ /**
9
+ * CORS Middleware
10
+ *
11
+ * Origin-based Cross-Origin Resource Sharing enforcement for all API responses.
12
+ * Handles preflight (OPTIONS) requests and applies CORS headers to normal responses.
13
+ *
14
+ * Three origin modes:
15
+ * - `origin: []` (default) — same-origin only, no CORS headers set
16
+ * - `origin: ['*']` — wide-open access (development only), logs warning in production
17
+ * - `origin: ['https://example.com', ...]` — allowlist with dynamic origin reflection
18
+ *
19
+ * @module middleware/cors
20
+ * @since 1.0.0
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const cors = createCorsMiddleware({
25
+ * origin: ['https://example.com', 'https://app.example.com'],
26
+ * credentials: true,
27
+ * });
28
+ *
29
+ * // In request pipeline:
30
+ * const preflightResponse = cors.handlePreflight(request);
31
+ * if (preflightResponse) return preflightResponse;
32
+ *
33
+ * const response = await handler(request);
34
+ * return cors.applyHeaders(request, response);
35
+ * ```
36
+ */
37
+ /**
38
+ * Configuration for CORS middleware.
39
+ *
40
+ * All fields are optional with secure defaults (same-origin only).
41
+ */
42
+ interface CorsConfig {
43
+ /**
44
+ * Allowed origins.
45
+ * - `[]` (default): same-origin only — no CORS headers are set.
46
+ * - `['*']`: wide-open access. Logs a warning in production.
47
+ * - `['https://example.com', ...]`: allowlist with dynamic origin reflection.
48
+ *
49
+ * @default []
50
+ */
51
+ origin?: string[];
52
+ /**
53
+ * Allowed HTTP methods for preflight responses.
54
+ *
55
+ * @default ["GET", "POST", "PATCH", "DELETE", "OPTIONS"]
56
+ */
57
+ methods?: string[];
58
+ /**
59
+ * Headers the client is allowed to send.
60
+ *
61
+ * @default ["Content-Type", "Authorization"]
62
+ */
63
+ allowedHeaders?: string[];
64
+ /**
65
+ * Response headers exposed to client-side JavaScript.
66
+ *
67
+ * @default ["X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset"]
68
+ */
69
+ exposedHeaders?: string[];
70
+ /**
71
+ * Whether to include credentials (cookies, Authorization header).
72
+ * Ignored when origin is `['*']` (CORS spec prohibits credentials with wildcard).
73
+ *
74
+ * @default true
75
+ */
76
+ credentials?: boolean;
77
+ /**
78
+ * Preflight cache duration in seconds.
79
+ *
80
+ * @default 86400 (24 hours)
81
+ */
82
+ maxAge?: number;
83
+ }
84
+
85
+ /**
86
+ * Rate Limiting Middleware
87
+ *
88
+ * Provides configurable rate limiting for API endpoints to protect
89
+ * against abuse and ensure fair resource usage.
90
+ *
91
+ * Features:
92
+ * - Pluggable store interface (in-memory default, Redis-compatible)
93
+ * - Separate read/write limits
94
+ * - Per-collection overrides
95
+ * - Skip function for admin users
96
+ * - Standard rate limit headers
97
+ *
98
+ * @module middleware/rate-limit
99
+ * @since 1.0.0
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * // Enable rate limiting in nextly.config.ts
104
+ * export default defineConfig({
105
+ * rateLimit: {
106
+ * enabled: true,
107
+ * readLimit: 100, // 100 GET requests per minute
108
+ * writeLimit: 30, // 30 POST/PATCH/DELETE per minute
109
+ * },
110
+ * });
111
+ * ```
112
+ */
113
+ /**
114
+ * Result from a rate limit check.
115
+ */
116
+ interface RateLimitResult {
117
+ /** Whether the request is allowed */
118
+ allowed: boolean;
119
+ /** Maximum requests allowed in the window */
120
+ limit: number;
121
+ /** Remaining requests in current window */
122
+ remaining: number;
123
+ /** Unix timestamp (ms) when the window resets */
124
+ resetTime: number;
125
+ }
126
+ /**
127
+ * Pluggable store interface for rate limit state.
128
+ *
129
+ * Implement this interface to use Redis, Memcached, or other
130
+ * distributed stores for rate limiting in production.
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * import Redis from 'ioredis';
135
+ *
136
+ * class RedisRateLimitStore implements RateLimitStore {
137
+ * private redis: Redis;
138
+ *
139
+ * constructor(redis: Redis) {
140
+ * this.redis = redis;
141
+ * }
142
+ *
143
+ * async increment(key: string, windowMs: number): Promise<RateLimitRecord> {
144
+ * const now = Date.now();
145
+ * const resetTime = now + windowMs;
146
+ * const count = await this.redis.incr(key);
147
+ * if (count === 1) {
148
+ * await this.redis.pexpire(key, windowMs);
149
+ * }
150
+ * return { count, resetTime };
151
+ * }
152
+ *
153
+ * async reset(key: string): Promise<void> {
154
+ * await this.redis.del(key);
155
+ * }
156
+ * }
157
+ * ```
158
+ */
159
+ interface RateLimitStore {
160
+ /**
161
+ * Increment the request count for a key.
162
+ *
163
+ * @param key - Unique identifier (e.g., IP address or user ID)
164
+ * @param windowMs - Time window in milliseconds
165
+ * @returns Record with current count and reset time
166
+ */
167
+ increment(key: string, windowMs: number): Promise<RateLimitRecord>;
168
+ /**
169
+ * Reset the request count for a key.
170
+ *
171
+ * @param key - Unique identifier to reset
172
+ */
173
+ reset(key: string): Promise<void>;
174
+ }
175
+ /**
176
+ * Record returned by store increment operation.
177
+ */
178
+ interface RateLimitRecord {
179
+ /** Current request count in the window */
180
+ count: number;
181
+ /** Unix timestamp (ms) when the window resets */
182
+ resetTime: number;
183
+ }
184
+ /**
185
+ * Configuration for rate limiting.
186
+ */
187
+ interface RateLimitConfig {
188
+ /**
189
+ * Enable rate limiting.
190
+ * @default true
191
+ */
192
+ enabled: boolean;
193
+ /**
194
+ * Maximum requests per window for read operations (GET).
195
+ * @default 100
196
+ */
197
+ readLimit?: number;
198
+ /**
199
+ * Maximum requests per window for write operations (POST, PATCH, PUT, DELETE).
200
+ * @default 30
201
+ */
202
+ writeLimit?: number;
203
+ /**
204
+ * Time window in milliseconds.
205
+ * @default 60000 (1 minute)
206
+ */
207
+ windowMs?: number;
208
+ /**
209
+ * Custom store for rate limit state.
210
+ * Defaults to in-memory store if not provided.
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * import { RedisRateLimitStore } from '@nextly/ratelimit-redis';
215
+ *
216
+ * rateLimit: {
217
+ * enabled: true,
218
+ * store: new RedisRateLimitStore(redisClient),
219
+ * }
220
+ * ```
221
+ */
222
+ store?: RateLimitStore;
223
+ /**
224
+ * Function to generate a unique key for rate limiting.
225
+ * Defaults to the trusted client IP address (see `trustProxy` /
226
+ * `trustedProxyIps`). Requests with no resolvable IP fall back to a
227
+ * shared `unknown` bucket so anonymous traffic is still rate-limited.
228
+ *
229
+ * @param request - The incoming request
230
+ * @returns A unique identifier string
231
+ */
232
+ keyGenerator?: (request: Request) => string;
233
+ /**
234
+ * When true, the default keyGenerator parses
235
+ * `X-Forwarded-For` (filtered through `trustedProxyIps`). When false
236
+ * (default), proxy headers are ignored — direct-internet deployments
237
+ * fall back to a single `unknown` bucket. Wired from
238
+ * `nextly.config.ts → security.trustProxy`.
239
+ *
240
+ * @default false
241
+ */
242
+ trustProxy?: boolean;
243
+ /**
244
+ * CIDR list of proxy IPs (from TRUSTED_PROXY_IPS).
245
+ * Used by the default keyGenerator to walk the X-Forwarded-For chain
246
+ * rightmost-first, returning the first non-proxy hop.
247
+ */
248
+ trustedProxyIps?: readonly string[];
249
+ /**
250
+ * Function to skip rate limiting for certain requests.
251
+ * Returns true to skip rate limiting.
252
+ *
253
+ * **Default**: skips the admin internal API (`/admin/api/*`). The
254
+ * rate limiter is meant to protect the public REST surface from
255
+ * anonymous abuse; admin routes are session-authed and already gated
256
+ * by `requireAdminAuth`/`requireCollectionAccess`, so applying the
257
+ * same per-IP cap to admin causes false positives during normal
258
+ * navigation (each admin page fires several parallel queries —
259
+ * `/me`, `/dashboard/stats`, `/schema/journal`, per-collection
260
+ * queries — and a handful of nav events trips the public default).
261
+ *
262
+ * Pass an explicit `skip` to override. If you want to rate-limit
263
+ * admin too (e.g. for insider-abuse defense), wrap the default:
264
+ *
265
+ * @param request - The incoming request
266
+ * @returns True to skip rate limiting
267
+ *
268
+ * @example
269
+ * ```typescript
270
+ * // Override: rate-limit everything, including admin
271
+ * skip: () => false
272
+ *
273
+ * // Override: skip admin AND internal service calls
274
+ * skip: (req) => {
275
+ * const url = new URL(req.url);
276
+ * if (url.pathname.startsWith("/admin/api/")) return true;
277
+ * return req.headers.get("x-internal-key") === process.env.INTERNAL_KEY;
278
+ * }
279
+ * ```
280
+ */
281
+ skip?: (request: Request) => boolean | Promise<boolean>;
282
+ /**
283
+ * Per-collection rate limit overrides.
284
+ *
285
+ * @example
286
+ * ```typescript
287
+ * collections: {
288
+ * 'media': { readLimit: 50, writeLimit: 10 }, // Stricter for media
289
+ * 'logs': { readLimit: 200 }, // More lenient for logs
290
+ * }
291
+ * ```
292
+ */
293
+ collections?: Record<string, {
294
+ readLimit?: number;
295
+ writeLimit?: number;
296
+ }>;
297
+ /**
298
+ * Custom handler for rate limit exceeded responses.
299
+ * If not provided, returns a standard 429 response.
300
+ *
301
+ * @param request - The rate-limited request
302
+ * @param result - The rate limit check result
303
+ * @returns A custom Response
304
+ */
305
+ handler?: (request: Request, result: RateLimitResult) => Response;
306
+ }
307
+ /**
308
+ * In-memory rate limit store.
309
+ *
310
+ * Suitable for development and single-instance deployments.
311
+ * For production with multiple instances, use a Redis-backed store.
312
+ *
313
+ * @internal
314
+ */
315
+ declare class InMemoryRateLimitStore implements RateLimitStore {
316
+ private hits;
317
+ private cleanupInterval;
318
+ constructor();
319
+ increment(key: string, windowMs: number): Promise<RateLimitRecord>;
320
+ reset(key: string): Promise<void>;
321
+ /**
322
+ * Clean up expired records to prevent memory leaks.
323
+ */
324
+ private cleanup;
325
+ /**
326
+ * Destroy the store and stop cleanup interval.
327
+ * Call this when shutting down the application.
328
+ */
329
+ destroy(): void;
330
+ /**
331
+ * Get the current size of the store (for testing/monitoring).
332
+ */
333
+ get size(): number;
334
+ }
335
+ /**
336
+ * Create a rate limiter middleware function.
337
+ *
338
+ * @param config - Rate limiting configuration
339
+ * @returns Middleware function that checks rate limits
340
+ *
341
+ * @example
342
+ * ```typescript
343
+ * const rateLimiter = createRateLimiter({
344
+ * enabled: true,
345
+ * readLimit: 100,
346
+ * writeLimit: 30,
347
+ * });
348
+ *
349
+ * // In route handler
350
+ * const rateLimitResponse = await rateLimiter(request);
351
+ * if (rateLimitResponse) {
352
+ * return rateLimitResponse; // 429 Too Many Requests
353
+ * }
354
+ * // Continue with request handling
355
+ * ```
356
+ */
357
+ declare function createRateLimiter(config: RateLimitConfig): (_request: Request) => Promise<Response | null>;
358
+ /**
359
+ * Create rate limit headers for successful requests.
360
+ *
361
+ * Call this after checking rate limits to add headers to the response.
362
+ *
363
+ * @param result - The rate limit check result
364
+ * @returns Headers object to merge with response
365
+ *
366
+ * @example
367
+ * ```typescript
368
+ * const response = new Response(JSON.stringify(data), {
369
+ * headers: {
370
+ * 'Content-Type': 'application/json',
371
+ * ...createRateLimitHeaders(rateLimitResult),
372
+ * },
373
+ * });
374
+ * ```
375
+ */
376
+ declare function createRateLimitHeaders(result: RateLimitResult): Record<string, string>;
377
+
378
+ /**
379
+ * Security Headers Middleware
380
+ *
381
+ * Response transformer that attaches security headers to every API response.
382
+ * Headers are pre-computed at initialization time for zero per-request overhead.
383
+ *
384
+ * All headers are individually configurable or disableable via
385
+ * `defineConfig({ security: { headers: { ... } } })`.
386
+ *
387
+ * @module middleware/security-headers
388
+ * @since 1.0.0
389
+ *
390
+ * @example
391
+ * ```typescript
392
+ * // Use with all defaults
393
+ * const applyHeaders = createSecurityHeadersMiddleware();
394
+ * const securedResponse = applyHeaders(response);
395
+ *
396
+ * // Customize specific headers
397
+ * const applyHeaders = createSecurityHeadersMiddleware({
398
+ * contentSecurityPolicy: "default-src 'self'",
399
+ * strictTransportSecurity: false, // Disable HSTS
400
+ * });
401
+ * ```
402
+ */
403
+ /**
404
+ * Configuration for security response headers.
405
+ *
406
+ * Each header can be set to a custom string value or `false` to disable it.
407
+ * Omitted headers use their secure defaults.
408
+ *
409
+ * @example
410
+ * ```typescript
411
+ * const config: SecurityHeadersConfig = {
412
+ * contentSecurityPolicy: "default-src 'self'",
413
+ * strictTransportSecurity: false, // Disable HSTS
414
+ * };
415
+ * ```
416
+ */
417
+ interface SecurityHeadersConfig {
418
+ /**
419
+ * Content-Security-Policy header value.
420
+ * Set to `false` to disable.
421
+ *
422
+ * The previous default `default-src 'none'; frame-ancestors 'none'`
423
+ * was a hard "block everything" — fine on a pure
424
+ * JSON response (CSP doesn't enforce on JSON) but instantly broke any
425
+ * HTML response, including the admin SPA. The new default is
426
+ * restrictive but lets a self-hosted Nextly admin UI run end-to-end:
427
+ *
428
+ * default-src 'self';
429
+ * script-src 'self';
430
+ * style-src 'self' 'unsafe-inline';
431
+ * img-src 'self' data: blob:;
432
+ * font-src 'self' data:;
433
+ * connect-src 'self';
434
+ * frame-ancestors 'none';
435
+ * base-uri 'self';
436
+ * form-action 'self';
437
+ * object-src 'none'
438
+ *
439
+ * To extend (e.g. for a CDN, analytics, or third-party fonts), pass
440
+ * an explicit string here — your value replaces the default entirely.
441
+ * To disable CSP entirely, set to `false`.
442
+ *
443
+ * @default see above
444
+ */
445
+ contentSecurityPolicy?: string | false;
446
+ /**
447
+ * X-Content-Type-Options header value.
448
+ * Set to `false` to disable.
449
+ *
450
+ * @default "nosniff"
451
+ */
452
+ xContentTypeOptions?: string | false;
453
+ /**
454
+ * X-Frame-Options header value.
455
+ * Set to `false` to disable.
456
+ *
457
+ * @default "DENY"
458
+ */
459
+ xFrameOptions?: string | false;
460
+ /**
461
+ * Strict-Transport-Security header value.
462
+ * Only applied when `NODE_ENV === 'production'` unless explicitly set.
463
+ * Set to `false` to disable entirely.
464
+ *
465
+ * @default "max-age=31536000; includeSubDomains"
466
+ */
467
+ strictTransportSecurity?: string | false;
468
+ /**
469
+ * Referrer-Policy header value.
470
+ * Set to `false` to disable.
471
+ *
472
+ * @default "strict-origin-when-cross-origin"
473
+ */
474
+ referrerPolicy?: string | false;
475
+ /**
476
+ * Permissions-Policy header value.
477
+ * Set to `false` to disable.
478
+ *
479
+ * @default "camera=(), microphone=(), geolocation=()"
480
+ */
481
+ permissionsPolicy?: string | false;
482
+ }
483
+
484
+ /**
485
+ * Admin Placement Constants
486
+ *
487
+ * Typed constants for valid admin sidebar placement sections.
488
+ * Plugin developers use these to declare where their plugin
489
+ * renders in the admin sidebar with full TypeScript autocomplete.
490
+ *
491
+ * @module plugins/admin-placement
492
+ * @since 1.0.0
493
+ *
494
+ * @example
495
+ * ```typescript
496
+ * import { definePlugin, AdminPlacement } from "nextly";
497
+ *
498
+ * export const analyticsPlugin = definePlugin({
499
+ * name: "Analytics Dashboard",
500
+ * admin: {
501
+ * placement: AdminPlacement.USERS,
502
+ * order: 60,
503
+ * description: "User analytics and insights",
504
+ * },
505
+ * });
506
+ * ```
507
+ */
508
+ /**
509
+ * Valid sidebar placement sections for plugins.
510
+ *
511
+ * Use these constants when specifying `admin.placement` in a plugin definition.
512
+ * Each value maps to a built-in sidebar section in the admin UI.
513
+ *
514
+ * @example
515
+ * ```typescript
516
+ * // Place plugin items alongside collections
517
+ * admin: { placement: AdminPlacement.COLLECTIONS }
518
+ *
519
+ * // Place plugin items in the Users inner sidebar
520
+ * admin: { placement: AdminPlacement.USERS }
521
+ * ```
522
+ */
523
+ declare const AdminPlacement: {
524
+ /** Plugin items appear in the Collections sidebar section */
525
+ readonly COLLECTIONS: "collections";
526
+ /** Plugin items appear in the Singles sidebar section */
527
+ readonly SINGLES: "singles";
528
+ /** Plugin items appear in the Users inner sidebar (alongside Users, User Fields, Roles) */
529
+ readonly USERS: "users";
530
+ /** Plugin items appear in the Settings inner sidebar (alongside General, API Keys, etc.) */
531
+ readonly SETTINGS: "settings";
532
+ /** Plugin items appear in the dedicated Plugins sidebar section (default) */
533
+ readonly PLUGINS: "plugins";
534
+ /** Plugin gets its own top-level icon in the sidebar (requires appearance.icon) */
535
+ readonly STANDALONE: "standalone";
536
+ };
537
+ /**
538
+ * Type representing valid admin sidebar placement values.
539
+ *
540
+ * Derived from the `AdminPlacement` constants object.
541
+ * Accepts: `"collections"` | `"singles"` | `"users"` | `"settings"` | `"plugins"` | `"standalone"`
542
+ */
543
+ type AdminPlacement = (typeof AdminPlacement)[keyof typeof AdminPlacement];
544
+
545
+ /**
546
+ * MetaService — small KV API over the `nextly_meta` table.
547
+ *
548
+ * Used for runtime flags that don't belong in collection schemas
549
+ * (e.g., `seed.completedAt`, `seed.skippedAt`). All values are JSON
550
+ * round-tripped: callers pass / receive JS values; the service
551
+ * handles serialisation. Pg/MySQL native JSON columns store the
552
+ * serialised string verbatim (no double-decoding on read since the
553
+ * service is the only writer).
554
+ *
555
+ * Cross-dialect: looks up the right Drizzle table via `this.dialect`.
556
+ */
557
+ declare class MetaService extends BaseService {
558
+ constructor(adapter: DrizzleAdapter, logger: Logger);
559
+ private get table();
560
+ private get drizzle();
561
+ get<T = unknown>(key: string): Promise<T | null>;
562
+ set(key: string, value: unknown): Promise<void>;
563
+ delete(key: string): Promise<void>;
564
+ getAll(): Promise<Record<string, unknown>>;
565
+ }
566
+
567
+ /**
568
+ * Collection Registry Service
569
+ *
570
+ * Manages the `dynamic_collections` metadata table for both code-first
571
+ * and UI-created collections. Provides schema hash-based change detection
572
+ * for code-first collection syncing.
573
+ *
574
+ * Extends BaseRegistryService for shared CRUD, migration tracking, and utility patterns.
575
+ *
576
+ * @module services/collections/collection-registry-service
577
+ * @since 1.0.0
578
+ */
579
+
580
+ /** Options for updating a collection. */
581
+ interface UpdateCollectionOptions {
582
+ /** Source making the update. Used to enforce locking rules. */
583
+ source?: CollectionSource;
584
+ }
585
+ /** Input for registering a code-first collection during sync. */
586
+ interface CodeFirstCollectionConfig {
587
+ slug: string;
588
+ labels: {
589
+ singular: string;
590
+ plural: string;
591
+ };
592
+ fields: DynamicCollectionInsert["fields"];
593
+ description?: string;
594
+ tableName?: string;
595
+ timestamps?: boolean;
596
+ /** Whether the collection has the Draft/Published status feature enabled. */
597
+ status?: boolean;
598
+ admin?: DynamicCollectionInsert["admin"];
599
+ configPath?: string;
600
+ }
601
+ /** Result of syncing code-first collections. */
602
+ interface SyncResult {
603
+ created: string[];
604
+ updated: string[];
605
+ unchanged: string[];
606
+ errors: Array<{
607
+ slug: string;
608
+ error: string;
609
+ }>;
610
+ }
611
+ /** Options for listing collections. */
612
+ interface ListCollectionsOptions$1 extends BaseListOptions {
613
+ source?: CollectionSource;
614
+ migrationStatus?: MigrationStatus;
615
+ }
616
+ /**
617
+ * Result of listing collections with pagination info.
618
+ *
619
+ * Declared as a `type` alias rather than an empty `interface` because the latter
620
+ * triggers @typescript-eslint/no-empty-object-type. We intentionally keep this
621
+ * named export so callers can import a domain-specific name even though it has
622
+ * no extra members today.
623
+ */
624
+ type ListCollectionsResult = BaseListResult<DynamicCollectionRecord>;
625
+ declare class CollectionRegistryService extends BaseRegistryService<DynamicCollectionRecord, MigrationStatus> {
626
+ protected readonly registryTableName = "dynamic_collections";
627
+ protected readonly resourceType = "Collection";
628
+ protected readonly tableNamePrefix = "dc_";
629
+ private permissionSeedService?;
630
+ constructor(adapter: DrizzleAdapter, logger: Logger);
631
+ protected getSearchColumns(): string[];
632
+ /** Set the PermissionSeedService for auto-seeding permissions on collection sync. */
633
+ setPermissionSeedService(service: PermissionSeedService): void;
634
+ getCollectionBySlug(slug: string): Promise<DynamicCollectionRecord | null>;
635
+ getCollection(slug: string): Promise<DynamicCollectionRecord>;
636
+ getAllCollections(options?: ListCollectionsOptions$1): Promise<DynamicCollectionRecord[]>;
637
+ listCollections(options?: ListCollectionsOptions$1): Promise<ListCollectionsResult>;
638
+ isLocked(slug: string): Promise<boolean>;
639
+ updateMigrationStatus(slug: string, status: MigrationStatus, migrationId?: string): Promise<void>;
640
+ updateMigrationStatusWithVerification(slug: string, tableName: string): Promise<{
641
+ verified: boolean;
642
+ status: MigrationStatus;
643
+ }>;
644
+ getPendingMigrations(): Promise<DynamicCollectionRecord[]>;
645
+ registerCollection(data: DynamicCollectionInsert): Promise<DynamicCollectionRecord>;
646
+ updateCollection(slug: string, data: Partial<DynamicCollectionInsert>, options?: UpdateCollectionOptions): Promise<DynamicCollectionRecord>;
647
+ deleteCollection(slug: string): Promise<void>;
648
+ syncCodeFirstCollections(configs: CodeFirstCollectionConfig[]): Promise<SyncResult>;
649
+ registerCollectionInTransaction(tx: TransactionContext, data: DynamicCollectionInsert): Promise<DynamicCollectionRecord>;
650
+ private seedPermissionsForCollection;
651
+ private labelsChanged;
652
+ protected deserializeRecord(record: DynamicCollectionRecord | Record<string, unknown>): DynamicCollectionRecord;
653
+ }
654
+
655
+ /**
656
+ * CollectionService - Unified service for collection operations
657
+ *
658
+ * This service provides a clean API for both collection metadata (CRUD on collections)
659
+ * and entry operations (CRUD on documents within collections). It follows the new
660
+ * service layer architecture with:
661
+ *
662
+ * - Exception-based error handling using NextlyError
663
+ * - RequestContext for user/locale context
664
+ * - PaginatedResult for list operations
665
+ * - Transaction-aware methods (*InTransaction) using adapter transactions
666
+ * - Database adapter abstraction for multi-DB support (PostgreSQL, MySQL, SQLite)
667
+ *
668
+ * Internally delegates to CollectionMetadataService and CollectionEntryService
669
+ * for the actual implementation, converting their return format to the new pattern.
670
+ *
671
+ * @example
672
+ * ```typescript
673
+ * import { CollectionService, NextlyError } from 'nextly';
674
+ *
675
+ * const service = new CollectionService(adapter, logger, metadataService, entryService);
676
+ *
677
+ * // Create a collection
678
+ * const collection = await service.createCollection({
679
+ * name: 'posts',
680
+ * label: 'Blog Posts',
681
+ * fields: [...]
682
+ * }, context);
683
+ *
684
+ * // Create an entry
685
+ * const entry = await service.createEntry('posts', { title: 'Hello' }, context);
686
+ *
687
+ * // Error handling
688
+ * try {
689
+ * const entry = await service.findEntryById('posts', 'nonexistent', context);
690
+ * } catch (error) {
691
+ * if (NextlyError.is(error)) {
692
+ * console.log(error.code); // 'NOT_FOUND'
693
+ * console.log(error.statusCode); // 404
694
+ * }
695
+ * }
696
+ *
697
+ * // Transaction support
698
+ * await service.withTransaction(async (tx) => {
699
+ * const entry = await service.createEntryInTransaction(tx, 'posts', data, context);
700
+ * await service.updateEntryInTransaction(tx, 'posts', entry.id, moreData, context);
701
+ * });
702
+ * ```
703
+ */
704
+
705
+ /**
706
+ * Collection metadata returned from operations
707
+ */
708
+ interface Collection {
709
+ id: string;
710
+ name: string;
711
+ label: string;
712
+ tableName: string;
713
+ description?: string;
714
+ icon?: string;
715
+ schemaDefinition: {
716
+ fields: FieldDefinition[];
717
+ };
718
+ createdBy?: string;
719
+ createdAt: Date;
720
+ updatedAt: Date;
721
+ }
722
+ /**
723
+ * Input for creating a collection
724
+ */
725
+ interface CreateCollectionInput {
726
+ name: string;
727
+ label: string;
728
+ description?: string;
729
+ icon?: string;
730
+ fields: FieldDefinition[];
731
+ }
732
+ /**
733
+ * Input for updating a collection
734
+ */
735
+ interface UpdateCollectionInput {
736
+ label?: string;
737
+ description?: string;
738
+ icon?: string;
739
+ fields?: FieldDefinition[];
740
+ }
741
+ /**
742
+ * Options for listing collections
743
+ */
744
+ interface ListCollectionsOptions {
745
+ page?: number;
746
+ limit?: number;
747
+ search?: string;
748
+ sortBy?: "slug" | "createdAt" | "updatedAt";
749
+ sortOrder?: "asc" | "desc";
750
+ includeSchema?: boolean;
751
+ }
752
+ /**
753
+ * Entry (document) within a collection
754
+ */
755
+ interface CollectionEntry {
756
+ id: string;
757
+ [key: string]: unknown;
758
+ createdAt: Date;
759
+ updatedAt: Date;
760
+ }
761
+ /**
762
+ * CollectionService - Unified service for collection and entry operations
763
+ *
764
+ * Provides both collection metadata CRUD (create/update/delete collections)
765
+ * and entry CRUD (documents within collections) with:
766
+ *
767
+ * - Exception-based error handling (throws NextlyError)
768
+ * - Type-safe RequestContext
769
+ * - PaginatedResult for list operations
770
+ * - Database adapter abstraction for multi-DB support
771
+ * - Transaction support via adapter transactions
772
+ *
773
+ * @extends BaseService - Provides adapter access, transaction helpers, and WHERE clause builders
774
+ */
775
+ declare class CollectionService extends BaseService {
776
+ private readonly metadataService;
777
+ private readonly entryService;
778
+ constructor(adapter: DrizzleAdapter, logger: Logger, metadataService: CollectionMetadataService, entryService: CollectionEntryService);
779
+ /**
780
+ * Register dynamic collection schemas for runtime use.
781
+ *
782
+ * This should be called during app initialization to register
783
+ * the generated Drizzle schema files for dynamic collections.
784
+ *
785
+ * @param schemas - Object mapping schema names to Drizzle table definitions
786
+ *
787
+ * @example
788
+ * ```typescript
789
+ * import * as dynamicSchemas from "@/db/schemas/dynamic";
790
+ *
791
+ * const service = getCollectionsService();
792
+ * service.registerDynamicSchemas(dynamicSchemas);
793
+ * ```
794
+ */
795
+ registerDynamicSchemas(schemas: Record<string, unknown>): void;
796
+ /**
797
+ * Create a new collection
798
+ *
799
+ * @param input - Collection creation data
800
+ * @param context - Request context with user info
801
+ * @returns Created collection
802
+ * @throws NextlyError if creation fails
803
+ *
804
+ * @example
805
+ * ```typescript
806
+ * const collection = await service.createCollection({
807
+ * name: 'posts',
808
+ * label: 'Blog Posts',
809
+ * fields: [
810
+ * { name: 'title', type: 'text', required: true },
811
+ * { name: 'content', type: 'richText' },
812
+ * ]
813
+ * }, context);
814
+ * ```
815
+ */
816
+ createCollection(input: CreateCollectionInput, context: RequestContext): Promise<Collection>;
817
+ /**
818
+ * List collections with pagination
819
+ *
820
+ * @param options - Pagination and filter options
821
+ * @param context - Request context
822
+ * @returns Paginated list of collections
823
+ * @throws NextlyError if listing fails
824
+ */
825
+ listCollections(options: ListCollectionsOptions | undefined, _context: RequestContext): Promise<PaginatedResult<Collection>>;
826
+ /**
827
+ * Get a single collection by name
828
+ *
829
+ * @param collectionName - Name of the collection
830
+ * @param context - Request context
831
+ * @returns Collection metadata
832
+ * @throws NextlyError with NOT_FOUND if collection doesn't exist
833
+ */
834
+ getCollection(collectionName: string, _context: RequestContext): Promise<Collection>;
835
+ /**
836
+ * Update a collection's metadata and/or schema
837
+ *
838
+ * @param collectionName - Name of the collection to update
839
+ * @param input - Update data
840
+ * @param context - Request context
841
+ * @returns Updated collection
842
+ * @throws NextlyError if update fails
843
+ */
844
+ updateCollection(collectionName: string, input: UpdateCollectionInput, _context: RequestContext): Promise<Collection>;
845
+ /**
846
+ * Delete a collection
847
+ *
848
+ * @param collectionName - Name of the collection to delete
849
+ * @param context - Request context
850
+ * @throws NextlyError if deletion fails
851
+ */
852
+ deleteCollection(collectionName: string, _context: RequestContext): Promise<void>;
853
+ /**
854
+ * Create a new entry in a collection
855
+ *
856
+ * @param collectionName - Name of the collection
857
+ * @param data - Entry data
858
+ * @param context - Request context with user info
859
+ * @returns Created entry
860
+ * @throws NextlyError if creation fails
861
+ *
862
+ * @example
863
+ * ```typescript
864
+ * const post = await service.createEntry('posts', {
865
+ * title: 'Hello World',
866
+ * content: 'My first post',
867
+ * }, context);
868
+ * ```
869
+ */
870
+ createEntry(collectionName: string, data: Record<string, unknown>, context: RequestContext): Promise<CollectionEntry>;
871
+ /**
872
+ * List entries in a collection
873
+ *
874
+ * @param collectionName - Name of the collection
875
+ * @param options - Query options (pagination, sort, where)
876
+ * @param context - Request context
877
+ * @returns Paginated list of entries
878
+ * @throws NextlyError if listing fails
879
+ */
880
+ listEntries(collectionName: string, options: QueryOptions | undefined, context: RequestContext): Promise<PaginatedResult<CollectionEntry>>;
881
+ /**
882
+ * Find an entry by ID
883
+ *
884
+ * @param collectionName - Name of the collection
885
+ * @param entryId - ID of the entry
886
+ * @param context - Request context
887
+ * @returns Entry data
888
+ * @throws NextlyError with NOT_FOUND if entry doesn't exist
889
+ */
890
+ findEntryById(collectionName: string, entryId: string, context: RequestContext): Promise<CollectionEntry>;
891
+ /**
892
+ * Update an entry
893
+ *
894
+ * @param collectionName - Name of the collection
895
+ * @param entryId - ID of the entry to update
896
+ * @param data - Update data
897
+ * @param context - Request context
898
+ * @returns Updated entry
899
+ * @throws NextlyError if update fails
900
+ */
901
+ updateEntry(collectionName: string, entryId: string, data: Record<string, unknown>, context: RequestContext): Promise<CollectionEntry>;
902
+ /**
903
+ * Delete an entry
904
+ *
905
+ * @param collectionName - Name of the collection
906
+ * @param entryId - ID of the entry to delete
907
+ * @param context - Request context
908
+ * @throws NextlyError if deletion fails
909
+ */
910
+ deleteEntry(collectionName: string, entryId: string, context: RequestContext): Promise<void>;
911
+ /**
912
+ * Create an entry within an existing transaction
913
+ *
914
+ * Use this when you need to coordinate multiple operations atomically.
915
+ *
916
+ * @param tx - Transaction context from adapter
917
+ * @param collectionName - Name of the collection
918
+ * @param data - Entry data
919
+ * @param context - Request context
920
+ * @returns Created entry
921
+ * @throws Error if underlying service doesn't support transaction context
922
+ *
923
+ * @example
924
+ * ```typescript
925
+ * await service.withTransaction(async (tx) => {
926
+ * const entry = await service.createEntryInTransaction(tx, 'posts', data, context);
927
+ * await service.updateEntryInTransaction(tx, 'posts', entry.id, moreData, context);
928
+ * });
929
+ * ```
930
+ */
931
+ createEntryInTransaction(tx: TransactionContext, collectionName: string, data: Record<string, unknown>, context: RequestContext): Promise<CollectionEntry>;
932
+ /**
933
+ * Update an entry within an existing transaction
934
+ *
935
+ * @param tx - Transaction context from adapter
936
+ * @param collectionName - Name of the collection
937
+ * @param entryId - ID of the entry to update
938
+ * @param data - Update data
939
+ * @param context - Request context
940
+ * @returns Updated entry
941
+ * @throws Error if underlying service doesn't support transaction context
942
+ */
943
+ updateEntryInTransaction(tx: TransactionContext, collectionName: string, entryId: string, data: Record<string, unknown>, context: RequestContext): Promise<CollectionEntry>;
944
+ /**
945
+ * Delete an entry within an existing transaction
946
+ *
947
+ * @param tx - Transaction context from adapter
948
+ * @param collectionName - Name of the collection
949
+ * @param entryId - ID of the entry to delete
950
+ * @param context - Request context
951
+ * @throws Error if underlying service doesn't support transaction context
952
+ */
953
+ deleteEntryInTransaction(tx: TransactionContext, collectionName: string, entryId: string, context: RequestContext): Promise<void>;
954
+ /**
955
+ * Translate a legacy CollectionServiceResult / MetadataServiceResult failure
956
+ * into a thrown NextlyError. Only used for non-404/403 cases — those have
957
+ * dedicated factory calls inline at each call site so identifiers can move
958
+ * cleanly to logContext.
959
+ *
960
+ * Per §13.8, the public message is generic for the matched factory; the
961
+ * inner legacy message moves to logContext for operators only and never
962
+ * reaches the wire.
963
+ */
964
+ private mapLegacyErrorToNextlyError;
965
+ }
966
+
967
+ /**
968
+ * ComponentSchemaService generates database schemas for component data tables (`comp_{slug}`).
969
+ * Supports PostgreSQL, MySQL, and SQLite dialects.
970
+ */
971
+
972
+ type SupportedDialect = "postgresql" | "mysql" | "sqlite";
973
+ declare class ComponentSchemaService {
974
+ private readonly dialect;
975
+ private readonly q;
976
+ constructor(dialect?: SupportedDialect);
977
+ /**
978
+ * Generate SQL migration for creating a new component data table.
979
+ */
980
+ generateMigrationSQL(tableName: string, fields: FieldConfig[]): string;
981
+ /**
982
+ * Generate ALTER TABLE migration for updating a component data table.
983
+ */
984
+ generateAlterTableMigration(tableName: string, oldFields: FieldConfig[], newFields: FieldConfig[]): string;
985
+ /**
986
+ * Generate DROP TABLE migration for a component data table.
987
+ */
988
+ generateDropTableMigration(tableName: string): {
989
+ migrationSQL: string;
990
+ migrationFileName: string;
991
+ };
992
+ /**
993
+ * Generate a Drizzle table object at runtime for querying component data.
994
+ */
995
+ generateRuntimeSchema(tableName: string, fields: FieldConfig[]): unknown;
996
+ private generatePostgresSchema;
997
+ private generateMySQLSchema;
998
+ private generateSQLiteSchema;
999
+ /**
1000
+ * Generate TypeScript/Drizzle schema code for a component data table.
1001
+ */
1002
+ generateSchemaCode(tableName: string, componentSlug: string, fields: FieldConfig[]): string;
1003
+ private generateColumnSQL;
1004
+ private getColumnType;
1005
+ private mapFieldToPostgresColumn;
1006
+ private mapFieldToMySQLColumn;
1007
+ private mapFieldToSQLiteColumn;
1008
+ private mapFieldToDrizzleCode;
1009
+ private mapFieldToPostgresCode;
1010
+ private mapFieldToMySQLCode;
1011
+ private mapFieldToSQLiteCode;
1012
+ private getDialectConfig;
1013
+ private collectRequiredImports;
1014
+ private generateBaseColumnsCode;
1015
+ private generateTimestampColumnsCode;
1016
+ private fieldHasForeignKey;
1017
+ private isFieldModified;
1018
+ private buildFieldMap;
1019
+ private getDefaultValueForType;
1020
+ private formatDefaultValue;
1021
+ private toSnakeCase;
1022
+ private toPascalCase;
1023
+ }
1024
+
1025
+ /**
1026
+ * Activity Log Service
1027
+ *
1028
+ * Records and queries user activity (create/update/delete) across all
1029
+ * collections. Designed for the dashboard activity feed — not a full
1030
+ * audit log. Writes are fire-and-forget; failures never propagate to
1031
+ * the caller.
1032
+ *
1033
+ * @module services/dashboard/activity-log-service
1034
+ * @since 1.0.0
1035
+ */
1036
+
1037
+ /** The three mutation actions tracked in the activity log. */
1038
+ type ActivityLogAction = "create" | "update" | "delete";
1039
+ /** A single activity log record as returned by queries. */
1040
+ interface ActivityLogEntry {
1041
+ id: string;
1042
+ userId: string;
1043
+ userName: string;
1044
+ userEmail: string;
1045
+ action: ActivityLogAction;
1046
+ collection: string;
1047
+ entryId: string | null;
1048
+ entryTitle: string | null;
1049
+ metadata: Record<string, unknown> | null;
1050
+ createdAt: string;
1051
+ }
1052
+ /** Input for recording a new activity. */
1053
+ interface LogActivityInput {
1054
+ userId: string;
1055
+ userName: string;
1056
+ userEmail: string;
1057
+ action: ActivityLogAction;
1058
+ collection: string;
1059
+ entryId?: string;
1060
+ entryTitle?: string;
1061
+ metadata?: Record<string, unknown>;
1062
+ }
1063
+ /** Paginated activity log response. */
1064
+ interface ActivityLogResult {
1065
+ activities: ActivityLogEntry[];
1066
+ total: number;
1067
+ hasMore: boolean;
1068
+ }
1069
+ /** Options for querying the activity log. */
1070
+ interface ActivityLogQueryOptions {
1071
+ limit?: number;
1072
+ offset?: number;
1073
+ collection?: string;
1074
+ userId?: string;
1075
+ }
1076
+ declare class ActivityLogService extends BaseService {
1077
+ constructor(adapter: DrizzleAdapter, logger: Logger);
1078
+ /**
1079
+ * Record an activity log entry.
1080
+ *
1081
+ * Errors are caught and logged but never propagated — activity logging
1082
+ * must never break a content operation.
1083
+ */
1084
+ logActivity(input: LogActivityInput): Promise<void>;
1085
+ /**
1086
+ * Query recent activity log entries with optional filters.
1087
+ *
1088
+ * Uses the `limit + 1` pattern to determine `hasMore` without a
1089
+ * separate COUNT query. The `total` field uses a separate count query
1090
+ * only when needed.
1091
+ */
1092
+ getRecentActivity(options?: ActivityLogQueryOptions): Promise<ActivityLogResult>;
1093
+ /**
1094
+ * Delete activity log records older than the specified number of days.
1095
+ *
1096
+ * @param olderThanDays - Delete records older than this many days (default: 90)
1097
+ * @returns Number of deleted records
1098
+ */
1099
+ cleanupOldActivities(olderThanDays?: number): Promise<number>;
1100
+ private countActivities;
1101
+ private mapRow;
1102
+ }
1103
+
1104
+ /**
1105
+ * Dashboard Service
1106
+ *
1107
+ * Aggregates content-centric statistics, recent entries across collections,
1108
+ * and project-wide metrics for the admin dashboard. Uses the database adapter
1109
+ * directly for simple read-only aggregate queries — no hooks, access control,
1110
+ * or relationship expansion needed for dashboard stats.
1111
+ *
1112
+ * @module services/dashboard/dashboard-service
1113
+ * @since 1.0.0
1114
+ */
1115
+
1116
+ /** Content statistics for the hero stats row. */
1117
+ interface ContentStats {
1118
+ totalEntries: number;
1119
+ totalMedia: number;
1120
+ contentTypes: number;
1121
+ recentChanges24h: number;
1122
+ }
1123
+ /** Draft vs Published breakdown. */
1124
+ interface ContentStatus {
1125
+ published: number;
1126
+ draft: number;
1127
+ }
1128
+ /** Per-collection entry count for collection quick-links. */
1129
+ interface CollectionCount {
1130
+ slug: string;
1131
+ label: string;
1132
+ group: string | null;
1133
+ count: number;
1134
+ }
1135
+ /** Full dashboard stats response. */
1136
+ interface DashboardStatsResponse {
1137
+ content: ContentStats;
1138
+ status: ContentStatus;
1139
+ collectionCounts: CollectionCount[];
1140
+ users: number;
1141
+ roles: number;
1142
+ permissions: number;
1143
+ components: number;
1144
+ singles: number;
1145
+ apiKeys: number;
1146
+ }
1147
+ /** A recently edited entry across any collection. */
1148
+ interface RecentEntry {
1149
+ id: string;
1150
+ title: string;
1151
+ collectionSlug: string;
1152
+ collectionLabel: string;
1153
+ status: "published" | "draft" | "none";
1154
+ updatedAt: string;
1155
+ }
1156
+ /** Response for the recent entries endpoint. */
1157
+ interface RecentEntriesResponse {
1158
+ entries: RecentEntry[];
1159
+ }
1160
+ /** Single stat item for the project statistics grid. */
1161
+ interface ProjectStat {
1162
+ key: string;
1163
+ label: string;
1164
+ value: number;
1165
+ }
1166
+ /** Response for the project stats endpoint. */
1167
+ interface ProjectStatsResponse {
1168
+ stats: ProjectStat[];
1169
+ }
1170
+ declare class DashboardService extends BaseService {
1171
+ constructor(adapter: DrizzleAdapter, logger: Logger);
1172
+ /**
1173
+ * Get aggregated dashboard statistics.
1174
+ *
1175
+ * Runs all count queries in parallel for fast response. Uses the database
1176
+ * adapter directly for simple COUNT(*) queries.
1177
+ */
1178
+ getStats(options?: {
1179
+ readableResources?: Set<string>;
1180
+ }): Promise<DashboardStatsResponse>;
1181
+ /**
1182
+ * Get recently modified entries across all collections.
1183
+ *
1184
+ * Queries each registered collection for entries sorted by `updated_at DESC`,
1185
+ * merges results, and returns the top N entries. Capped at 20 collections
1186
+ * to prevent excessive DB queries on large installations.
1187
+ *
1188
+ * @param limit - Maximum number of entries to return (default: 5, max: 20)
1189
+ */
1190
+ getRecentEntries(limit?: number, readableResources?: Set<string>): Promise<RecentEntriesResponse>;
1191
+ /**
1192
+ * Get project-wide statistics for the stats grid.
1193
+ *
1194
+ * Returns an array of stat items for display in the 2×4 grid widget.
1195
+ * Reuses the same data sources as `getStats()`.
1196
+ */
1197
+ getProjectStats(options?: {
1198
+ readableResources?: Set<string>;
1199
+ }): Promise<ProjectStatsResponse>;
1200
+ private getRegisteredCollections;
1201
+ private getRegisteredSingles;
1202
+ /**
1203
+ * Format a Date for raw-SQL bind parameters per dialect.
1204
+ *
1205
+ * Phase A follow-up (2026-05-01) — `BaseService.formatDateForDb()`
1206
+ * returns the Date unchanged; that works for Drizzle's typed query
1207
+ * builder (which converts based on column mode) but breaks raw
1208
+ * `adapter.executeQuery(sql, [date])` paths on SQLite, where
1209
+ * better-sqlite3 throws "can only bind numbers, strings, bigints,
1210
+ * buffers, and null" on Date objects.
1211
+ *
1212
+ * Per-dialect format:
1213
+ * - SQLite: epoch SECONDS (matches Drizzle's `integer mode:"timestamp"`
1214
+ * storage, which is what every timestamp column in the schema uses).
1215
+ * - MySQL: 'YYYY-MM-DD HH:MM:SS' (DATETIME/TIMESTAMP format).
1216
+ * - PostgreSQL: ISO 8601 string (driver converts to timestamp natively).
1217
+ *
1218
+ * Helper kept local to this service since it's the only raw-query
1219
+ * consumer; promote to BaseService if more services need it.
1220
+ */
1221
+ private dateForRawBind;
1222
+ private countTable;
1223
+ private countActiveApiKeys;
1224
+ private countRecentChanges24h;
1225
+ private countRegistryItems;
1226
+ private getCollectionCounts;
1227
+ /**
1228
+ * Get draft vs published content breakdown across all collections.
1229
+ *
1230
+ * Collections without a `_status` or `status` field count all entries
1231
+ * as published.
1232
+ */
1233
+ private getContentStatusBreakdown;
1234
+ private countByStatus;
1235
+ private getRecentFromCollection;
1236
+ }
1237
+
1238
+ /**
1239
+ * Shared type definitions for the General Settings schema.
1240
+ *
1241
+ * @module schemas/general-settings/types
1242
+ * @since 1.0.0
1243
+ */
1244
+ /**
1245
+ * Full record type for the `site_settings` singleton row.
1246
+ * The `id` is always `'default'`.
1247
+ */
1248
+ interface GeneralSettingsRecord {
1249
+ /** Always 'default' — enforces singleton pattern. */
1250
+ id: string;
1251
+ /** Display name for the application (used in admin UI title, email templates). */
1252
+ applicationName: string | null;
1253
+ /** Primary URL where the site is hosted (used for email links). */
1254
+ siteUrl: string | null;
1255
+ /** Primary email address for administrative notifications / default sender. */
1256
+ adminEmail: string | null;
1257
+ /** IANA timezone identifier, e.g. 'America/New_York'. */
1258
+ timezone: string | null;
1259
+ /** Date display format string, e.g. 'MM/DD/YYYY'. */
1260
+ dateFormat: string | null;
1261
+ /** Time display format: '12h' or '24h'. */
1262
+ timeFormat: string | null;
1263
+ /** URL of the logo image shown in the admin sidebar and auth pages. */
1264
+ logoUrl: string | null;
1265
+ /** JSON array of custom sidebar groups for admin navigation. */
1266
+ customSidebarGroups: string | null;
1267
+ /** JSON object mapping plugin slugs to their sidebar placement group overrides. */
1268
+ pluginPlacements: string | null;
1269
+ /** When the settings were last updated. */
1270
+ updatedAt: Date;
1271
+ }
1272
+ /**
1273
+ * Fields that can be updated via the settings form.
1274
+ * Excludes immutable `id` and auto-managed `updatedAt`.
1275
+ */
1276
+ type GeneralSettingsUpdate = Omit<GeneralSettingsRecord, "id" | "updatedAt">;
1277
+
1278
+ /**
1279
+ * General Settings Service
1280
+ *
1281
+ * Manages the `site_settings` singleton row — a single record
1282
+ * (id = 'default') that stores application-level configuration:
1283
+ * application name, site URL, admin email, timezone, and display formats.
1284
+ *
1285
+ * @module services/general-settings/general-settings-service
1286
+ * @since 1.0.0
1287
+ */
1288
+
1289
+ interface CustomSidebarGroup {
1290
+ slug: string;
1291
+ name: string;
1292
+ icon?: string;
1293
+ }
1294
+ declare class GeneralSettingsService extends BaseService {
1295
+ private siteSettings;
1296
+ constructor(adapter: DrizzleAdapter, logger: Logger);
1297
+ private toRecord;
1298
+ /**
1299
+ * Retrieve the current general settings.
1300
+ * Returns an all-null record if the singleton row has not been saved yet.
1301
+ */
1302
+ getSettings(): Promise<GeneralSettingsRecord>;
1303
+ /**
1304
+ * Get the configured IANA timezone identifier.
1305
+ * Reads from the singleton row each call so updates are reflected
1306
+ * consistently across long-lived runtime instances.
1307
+ */
1308
+ getTimezone(): Promise<string | null>;
1309
+ /**
1310
+ * Upsert the general settings singleton row.
1311
+ * Only the provided fields are updated; omitted fields are left unchanged.
1312
+ * If the row doesn't exist yet, it is created with the provided values.
1313
+ */
1314
+ updateSettings(data: Partial<GeneralSettingsUpdate>): Promise<GeneralSettingsRecord>;
1315
+ /**
1316
+ * Parse the stored JSON string into an array of custom sidebar groups.
1317
+ * Returns an empty array if no groups are stored or JSON is invalid.
1318
+ */
1319
+ getCustomSidebarGroups(settings: GeneralSettingsRecord): CustomSidebarGroup[];
1320
+ /**
1321
+ * Replace all custom sidebar groups with the provided array.
1322
+ * Persists as a JSON string in the `custom_sidebar_groups` column.
1323
+ */
1324
+ updateCustomSidebarGroups(groups: CustomSidebarGroup[]): Promise<CustomSidebarGroup[]>;
1325
+ }
1326
+
1327
+ /**
1328
+ * Service Registration for DI Container
1329
+ *
1330
+ * Provides the async entrypoint `registerServices()` that bootstraps the
1331
+ * database adapter, media storage, and every Nextly domain service. The
1332
+ * individual domain registrations live in `./registrations/` — this file
1333
+ * is the orchestrator that stitches them together.
1334
+ *
1335
+ * **IMPORTANT:** `registerServices()` is async and must be awaited.
1336
+ * The database adapter is created and connected during registration for
1337
+ * fail-fast error handling and predictable initialization.
1338
+ *
1339
+ * @example
1340
+ * ```typescript
1341
+ * import { registerServices, getService } from 'nextly';
1342
+ *
1343
+ * await registerServices({
1344
+ * imageProcessor: getImageProcessor(),
1345
+ * logger: customLogger, // optional
1346
+ * });
1347
+ *
1348
+ * const userService = getService('userService');
1349
+ * const user = await userService.findById(userId, context);
1350
+ * ```
1351
+ */
1352
+
1353
+ /**
1354
+ * Configuration for service registration.
1355
+ *
1356
+ * **Database Configuration:** if `adapter` is provided, it is used
1357
+ * directly. Otherwise, one is created from environment variables using
1358
+ * `DB_DIALECT` and `DATABASE_URL`.
1359
+ */
1360
+ interface NextlyServiceConfig {
1361
+ /**
1362
+ * Database adapter for multi-database support.
1363
+ * If not provided, created automatically from environment variables.
1364
+ */
1365
+ adapter?: DrizzleAdapter;
1366
+ /** Storage plugins for cloud storage providers (S3, Vercel Blob, etc.). */
1367
+ storagePlugins?: StoragePlugin[];
1368
+ /** Image processor for media operations. */
1369
+ imageProcessor: ImageProcessor;
1370
+ /** Optional logger instance. Defaults to `consoleLogger`. */
1371
+ logger?: Logger;
1372
+ /** Optional hook registry. When absent, hooks are disabled. */
1373
+ hookRegistry?: HookRegistry;
1374
+ /** Optional password hasher for user authentication. */
1375
+ passwordHasher?: {
1376
+ hash(password: string): Promise<string>;
1377
+ verify(password: string, hash: string): Promise<boolean>;
1378
+ };
1379
+ /** Optional base path for collection file operations. */
1380
+ basePath?: string;
1381
+ /** Optional directory for dynamic collection schemas. */
1382
+ schemasDir?: string;
1383
+ /** Optional directory for dynamic collection migrations. */
1384
+ migrationsDir?: string;
1385
+ /** Plugins to initialize with Nextly. */
1386
+ plugins?: PluginDefinition[];
1387
+ /** Collection configurations. */
1388
+ collections?: CollectionConfig[];
1389
+ /** Single (global document) configurations. */
1390
+ singles?: SingleConfig[];
1391
+ /** Component (reusable field group) configurations. */
1392
+ components?: ComponentConfig[];
1393
+ /** User model extension configuration. */
1394
+ users?: UserConfig;
1395
+ /** Email system configuration. */
1396
+ email?: EmailConfig;
1397
+ /** API key authentication configuration with defaults applied. */
1398
+ apiKeys?: SanitizedApiKeysConfig;
1399
+ /** Security configuration (headers, CORS, uploads, sanitization). */
1400
+ security?: SecurityConfig;
1401
+ /**
1402
+ * Admin panel configuration (branding, plugin overrides, devAutoLogin).
1403
+ * Carried through from `nextly.config.ts` so handlers that read from the
1404
+ * DI's "config" service can see admin-level toggles. Without this the
1405
+ * admin object gets dropped during buildServiceConfig.
1406
+ */
1407
+ admin?: AdminConfig;
1408
+ /**
1409
+ * Authentication configuration (revealRegistrationConflict and friends).
1410
+ * Same rationale as admin: carried through so handlers can read it.
1411
+ */
1412
+ auth?: AuthConfig;
1413
+ }
1414
+ /**
1415
+ * Type-safe service map returned by `getService()`.
1416
+ */
1417
+ interface ServiceMap {
1418
+ adapter: DrizzleAdapter;
1419
+ logger: Logger;
1420
+ config: NextlyServiceConfig;
1421
+ mediaStorage: MediaStorage;
1422
+ collectionService: CollectionService;
1423
+ collectionRegistryService: CollectionRegistryService;
1424
+ userService: UserService;
1425
+ mediaService: MediaService;
1426
+ singleRegistryService: SingleRegistryService;
1427
+ singleEntryService: SingleEntryService;
1428
+ componentRegistryService: ComponentRegistryService;
1429
+ componentSchemaService: ComponentSchemaService;
1430
+ componentDataService: ComponentDataService;
1431
+ relationshipService: CollectionRelationshipService;
1432
+ userExtSchemaService: UserExtSchemaService;
1433
+ emailProviderService: EmailProviderService;
1434
+ emailTemplateService: EmailTemplateService;
1435
+ emailService: EmailService;
1436
+ userFieldDefinitionService: UserFieldDefinitionService;
1437
+ permissionSeedService: PermissionSeedService;
1438
+ rbacAccessControlService: RBACAccessControlService;
1439
+ apiKeyService: ApiKeyService;
1440
+ authService: AuthService;
1441
+ generalSettingsService: GeneralSettingsService;
1442
+ activityLogService: ActivityLogService;
1443
+ dashboardService: DashboardService;
1444
+ metaService: MetaService;
1445
+ }
1446
+ /**
1447
+ * Register all Nextly services in the DI container.
1448
+ *
1449
+ * This function should be called once during application initialization.
1450
+ * Services are registered as singletons and lazily initialized on first access.
1451
+ *
1452
+ * @param config - Service configuration with required dependencies
1453
+ * @throws Error if called multiple times (use `clearServices()` first)
1454
+ * @throws Error if database environment configuration is invalid
1455
+ * @throws Error if database connection fails
1456
+ */
1457
+ declare function registerServices(config: NextlyServiceConfig): Promise<void>;
1458
+ /**
1459
+ * Get a service from the container with type safety.
1460
+ * Services must be registered first via `registerServices()`.
1461
+ */
1462
+ declare function getService<T extends keyof ServiceMap>(name: T): ServiceMap[T];
1463
+ /**
1464
+ * Check if services have been registered.
1465
+ */
1466
+ declare function isServicesRegistered(): boolean;
1467
+ /**
1468
+ * Shutdown all services and cleanup resources. Should be called when
1469
+ * shutting down the application to ensure proper cleanup of database
1470
+ * connections and other resources.
1471
+ */
1472
+ declare function shutdownServices(): Promise<void>;
1473
+ /**
1474
+ * Clear all registered services. Primarily for testing or re-initialization
1475
+ * with different configuration. For production shutdown, prefer
1476
+ * `shutdownServices()` so resources are properly released.
1477
+ */
1478
+ declare function clearServices(): void;
1479
+
1480
+ /**
1481
+ * Plugin Context System
1482
+ *
1483
+ * Provides a type-safe context for plugins to access Nextly services.
1484
+ * Plugins receive this context during initialization, enabling them
1485
+ * to interact with core services and register hooks.
1486
+ *
1487
+ * @module plugins/plugin-context
1488
+ * @since 1.0.0
1489
+ */
1490
+
1491
+ /**
1492
+ * Simplified hook registry interface for plugins.
1493
+ *
1494
+ * Provides only the methods plugins should use (register/unregister hooks).
1495
+ * Internal methods like `execute()` and `clear()` are not exposed.
1496
+ *
1497
+ * @example
1498
+ * ```typescript
1499
+ * export const myPlugin = definePlugin({
1500
+ * name: 'my-plugin',
1501
+ *
1502
+ * async init(nextly) {
1503
+ * // Register a beforeCreate hook
1504
+ * nextly.hooks.on('beforeCreate', 'posts', async (context) => {
1505
+ * context.data.slug = slugify(context.data.title);
1506
+ * return context.data;
1507
+ * });
1508
+ *
1509
+ * // Register a global hook (all collections)
1510
+ * nextly.hooks.on('afterCreate', '*', async (context) => {
1511
+ * nextly.infra.logger.info(`Created ${context.collection}:${context.data?.id}`);
1512
+ * });
1513
+ * }
1514
+ * });
1515
+ * ```
1516
+ */
1517
+ interface PluginHookRegistry {
1518
+ /**
1519
+ * Register a hook for a specific collection and hook type.
1520
+ *
1521
+ * @param hookType - Type of hook (beforeCreate, afterCreate, etc.)
1522
+ * @param collection - Collection name or '*' for global hooks
1523
+ * @param handler - Hook function to execute
1524
+ *
1525
+ * @example
1526
+ * ```typescript
1527
+ * // Collection-specific hook
1528
+ * nextly.hooks.on('beforeCreate', 'users', async (context) => {
1529
+ * context.data.password = await bcrypt.hash(context.data.password, 10);
1530
+ * return context.data;
1531
+ * });
1532
+ *
1533
+ * // Global hook (runs for all collections)
1534
+ * nextly.hooks.on('afterDelete', '*', async (context) => {
1535
+ * console.log(`Deleted from ${context.collection}`);
1536
+ * });
1537
+ * ```
1538
+ */
1539
+ on<T = unknown>(hookType: HookType, collection: string, handler: HookHandler<T>): void;
1540
+ /**
1541
+ * Unregister a previously registered hook.
1542
+ *
1543
+ * @param hookType - Type of hook
1544
+ * @param collection - Collection name or '*'
1545
+ * @param handler - The exact handler function to remove
1546
+ *
1547
+ * @example
1548
+ * ```typescript
1549
+ * const myHook = async (context) => { ... };
1550
+ *
1551
+ * // Register
1552
+ * nextly.hooks.on('beforeCreate', 'posts', myHook);
1553
+ *
1554
+ * // Later, unregister
1555
+ * nextly.hooks.off('beforeCreate', 'posts', myHook);
1556
+ * ```
1557
+ */
1558
+ off<T = unknown>(hookType: HookType, collection: string, handler: HookHandler<T>): void;
1559
+ }
1560
+ /**
1561
+ * PluginContext - Type-safe context for plugin service access.
1562
+ *
1563
+ * Plugins receive this context during initialization, providing
1564
+ * access to all Nextly services and infrastructure.
1565
+ *
1566
+ * The context is organized into logical groups:
1567
+ * - `services`: Core business logic services (collections, users, media)
1568
+ * - `infra`: Infrastructure components (database, logger)
1569
+ * - `config`: Read-only configuration
1570
+ * - `hooks`: Hook registration for lifecycle events
1571
+ *
1572
+ * @example
1573
+ * ```typescript
1574
+ * import { definePlugin } from 'nextly';
1575
+ *
1576
+ * export const myPlugin = definePlugin({
1577
+ * name: 'my-plugin',
1578
+ * version: '1.0.0',
1579
+ *
1580
+ * async init(nextly) {
1581
+ * // Access services with full TypeScript autocomplete
1582
+ * const { collections, users, media } = nextly.services;
1583
+ *
1584
+ * // Register hooks
1585
+ * nextly.hooks.on('beforeCreate', 'posts', async (context) => {
1586
+ * // Validate that author exists
1587
+ * const author = await users.findById(context.data.authorId, {});
1588
+ * if (!author) {
1589
+ * throw new Error('Author not found');
1590
+ * }
1591
+ * return context.data;
1592
+ * });
1593
+ *
1594
+ * // Use infrastructure
1595
+ * nextly.infra.logger.info('MyPlugin initialized');
1596
+ * }
1597
+ * });
1598
+ * ```
1599
+ */
1600
+ interface PluginContext {
1601
+ /**
1602
+ * Core services with full TypeScript autocomplete.
1603
+ *
1604
+ * Provides access to the unified service layer for:
1605
+ * - Collections: CRUD operations on dynamic collections
1606
+ * - Users: User management and authentication
1607
+ * - Media: File upload and management
1608
+ */
1609
+ services: {
1610
+ /** Collection service for CRUD operations on dynamic collections */
1611
+ collections: CollectionService;
1612
+ /** User service for user management */
1613
+ users: UserService;
1614
+ /** Media service for file operations */
1615
+ media: MediaService;
1616
+ /** Email service for sending emails via templates and providers */
1617
+ email: EmailService;
1618
+ };
1619
+ /**
1620
+ * Infrastructure access.
1621
+ *
1622
+ * Provides access to low-level infrastructure:
1623
+ * - Database: Direct Drizzle database access (use with caution)
1624
+ * - Logger: Logging interface for plugin diagnostics
1625
+ */
1626
+ infra: {
1627
+ /** Drizzle database instance for direct queries */
1628
+ db: DatabaseInstance;
1629
+ /** Logger for plugin diagnostics */
1630
+ logger: Logger;
1631
+ };
1632
+ /**
1633
+ * Read-only configuration.
1634
+ *
1635
+ * Contains the Nextly service configuration.
1636
+ * Configuration is frozen to prevent accidental modification.
1637
+ */
1638
+ config: Readonly<NextlyServiceConfig>;
1639
+ /**
1640
+ * Hook registration for lifecycle events.
1641
+ *
1642
+ * Allows plugins to register hooks that run before/after
1643
+ * database operations on collections.
1644
+ */
1645
+ hooks: PluginHookRegistry;
1646
+ }
1647
+ /**
1648
+ * Sidebar appearance customization for plugins.
1649
+ *
1650
+ * Allows plugin authors to customize how their plugin appears
1651
+ * in the admin sidebar. All fields are optional — unset fields
1652
+ * use sensible defaults (Package icon, plugin name as label).
1653
+ *
1654
+ * @example
1655
+ * ```typescript
1656
+ * admin: {
1657
+ * appearance: {
1658
+ * icon: "BarChart", // Lucide icon name
1659
+ * label: "Analytics", // Custom sidebar label
1660
+ * badge: "Beta", // Badge text
1661
+ * badgeVariant: "secondary",
1662
+ * },
1663
+ * }
1664
+ * ```
1665
+ */
1666
+ interface PluginAdminAppearance {
1667
+ /** Lucide icon name for the plugin's sidebar entry */
1668
+ icon?: string;
1669
+ /** Custom label override (defaults to plugin name) */
1670
+ label?: string;
1671
+ /** Badge text shown next to the plugin name (e.g., "Beta", "New") */
1672
+ badge?: string;
1673
+ /** Badge variant for styling */
1674
+ badgeVariant?: "default" | "secondary" | "destructive" | "outline";
1675
+ }
1676
+ /**
1677
+ * Plugin admin configuration for sidebar placement and appearance.
1678
+ *
1679
+ * Allows plugins to declare their sidebar placement, sort order,
1680
+ * appearance customization, and description for the plugin settings page.
1681
+ *
1682
+ * @example
1683
+ * ```typescript
1684
+ * import { definePlugin, AdminPlacement } from 'nextly';
1685
+ *
1686
+ * export const analyticsPlugin = definePlugin({
1687
+ * name: 'Analytics Dashboard',
1688
+ * admin: {
1689
+ * placement: AdminPlacement.USERS,
1690
+ * order: 60,
1691
+ * description: 'User analytics and insights',
1692
+ * appearance: {
1693
+ * icon: 'BarChart',
1694
+ * label: 'Analytics',
1695
+ * badge: 'Beta',
1696
+ * badgeVariant: 'secondary',
1697
+ * },
1698
+ * },
1699
+ * });
1700
+ * ```
1701
+ */
1702
+ interface PluginAdminConfig {
1703
+ /**
1704
+ * Immutable sidebar placement for this plugin's items.
1705
+ *
1706
+ * Use `AdminPlacement` constants for TypeScript autocomplete:
1707
+ * - `AdminPlacement.COLLECTIONS` (Collections section)
1708
+ * - `AdminPlacement.SINGLES` (Singles section)
1709
+ * - `AdminPlacement.USERS` (Users inner sidebar)
1710
+ * - `AdminPlacement.SETTINGS` (Settings inner sidebar)
1711
+ * - `AdminPlacement.PLUGINS` (Plugins section, default)
1712
+ *
1713
+ * If not set, falls back to `"plugins"`.
1714
+ */
1715
+ placement?: AdminPlacement;
1716
+ /** Sort order when placed in a group (lower = higher position, default: 100) */
1717
+ order?: number;
1718
+ /**
1719
+ * Position anchor for standalone plugins.
1720
+ * Specifies which built-in sidebar section this plugin's icon appears after.
1721
+ *
1722
+ * Valid values: `"dashboard"` | `"collections"` | `"singles"` | `"media"` | `"plugins"` | `"users"`
1723
+ *
1724
+ * Only applies when `placement` is `AdminPlacement.STANDALONE`.
1725
+ * If multiple standalone plugins share the same `after`, they are sorted by `order`.
1726
+ * Defaults to `"plugins"` (after the Plugins icon).
1727
+ *
1728
+ * @example
1729
+ * ```ts
1730
+ * admin: {
1731
+ * placement: AdminPlacement.STANDALONE,
1732
+ * after: "collections", // icon appears right after Collections
1733
+ * order: 10,
1734
+ * }
1735
+ * ```
1736
+ */
1737
+ after?: "dashboard" | "collections" | "singles" | "media" | "plugins" | "users" | "settings";
1738
+ /** Plugin description shown on the plugin settings page */
1739
+ description?: string;
1740
+ /** Sidebar appearance customization (icon, label, badge) */
1741
+ appearance?: PluginAdminAppearance;
1742
+ }
1743
+ /**
1744
+ * Plugin definition interface.
1745
+ *
1746
+ * Defines the structure of a Nextly plugin. Plugins can:
1747
+ * - Initialize with access to PluginContext
1748
+ * - Transform configuration before services are registered
1749
+ *
1750
+ * @example
1751
+ * ```typescript
1752
+ * import { definePlugin } from 'nextly';
1753
+ *
1754
+ * export const auditLogPlugin = definePlugin({
1755
+ * name: 'audit-log',
1756
+ * version: '1.0.0',
1757
+ *
1758
+ * async init(nextly) {
1759
+ * // Log all create/update/delete operations
1760
+ * const logOperation = async (context) => {
1761
+ * nextly.infra.logger.info('Audit', {
1762
+ * collection: context.collection,
1763
+ * operation: context.operation,
1764
+ * user: context.user?.id,
1765
+ * timestamp: new Date().toISOString(),
1766
+ * });
1767
+ * };
1768
+ *
1769
+ * nextly.hooks.on('afterCreate', '*', logOperation);
1770
+ * nextly.hooks.on('afterUpdate', '*', logOperation);
1771
+ * nextly.hooks.on('afterDelete', '*', logOperation);
1772
+ * }
1773
+ * });
1774
+ * ```
1775
+ */
1776
+ interface PluginDefinition {
1777
+ /**
1778
+ * Unique plugin name.
1779
+ * Used for identification and error messages.
1780
+ */
1781
+ name: string;
1782
+ /**
1783
+ * Plugin version (semver format recommended).
1784
+ * Helps with debugging and compatibility checks.
1785
+ */
1786
+ version?: string;
1787
+ /**
1788
+ * Collections provided by this plugin.
1789
+ *
1790
+ * These collections are automatically merged with user collections
1791
+ * in defineConfig(). Users don't need to manually spread plugin collections.
1792
+ *
1793
+ * @example
1794
+ * ```typescript
1795
+ * // Plugin definition
1796
+ * const myPlugin: PluginDefinition = {
1797
+ * name: 'my-plugin',
1798
+ * collections: [FormsCollection, SubmissionsCollection],
1799
+ * };
1800
+ *
1801
+ * // User config - collections are auto-merged
1802
+ * export default defineConfig({
1803
+ * plugins: [myPlugin],
1804
+ * collections: [Posts, Users], // Plugin collections added automatically
1805
+ * });
1806
+ * ```
1807
+ */
1808
+ collections?: CollectionConfig[];
1809
+ /**
1810
+ * Admin configuration for sidebar placement and plugin metadata.
1811
+ *
1812
+ * Controls where the plugin's items appear in the sidebar
1813
+ * and provides metadata for the plugin settings page.
1814
+ */
1815
+ admin?: PluginAdminConfig;
1816
+ /**
1817
+ * Plugin initialization function.
1818
+ *
1819
+ * Called after all services are registered.
1820
+ * Receives PluginContext for service access and hook registration.
1821
+ *
1822
+ * @param context - PluginContext with services, infra, config, hooks
1823
+ */
1824
+ init?: (context: PluginContext) => Promise<void> | void;
1825
+ /**
1826
+ * Configuration transformer (advanced).
1827
+ *
1828
+ * Allows plugins to modify the config before service initialization.
1829
+ * Use with caution - this runs before services are available.
1830
+ *
1831
+ * @param config - Current configuration
1832
+ * @returns Modified configuration
1833
+ */
1834
+ config?: (config: NextlyServiceConfig) => NextlyServiceConfig;
1835
+ }
1836
+ /**
1837
+ * Define a plugin with type safety.
1838
+ *
1839
+ * This is a helper function that provides TypeScript autocomplete
1840
+ * when defining plugins. It simply returns the definition as-is.
1841
+ *
1842
+ * @param definition - Plugin definition
1843
+ * @returns The same definition (for type inference)
1844
+ *
1845
+ * @example
1846
+ * ```typescript
1847
+ * import { definePlugin } from 'nextly';
1848
+ *
1849
+ * export const myPlugin = definePlugin({
1850
+ * name: 'my-plugin',
1851
+ * version: '1.0.0',
1852
+ *
1853
+ * async init(nextly) {
1854
+ * // Full TypeScript autocomplete available
1855
+ * nextly.services.collections.listCollections();
1856
+ * }
1857
+ * });
1858
+ * ```
1859
+ */
1860
+ declare function definePlugin(definition: PluginDefinition): PluginDefinition;
1861
+ /**
1862
+ * Create a PluginContext from the DI container.
1863
+ *
1864
+ * This factory function creates a PluginContext by retrieving
1865
+ * services from the container. It should be called after
1866
+ * `registerServices()` has been invoked.
1867
+ *
1868
+ * The config is frozen to prevent accidental modification.
1869
+ *
1870
+ * @param getServiceFn - Function to get services from container
1871
+ * @param hookRegistry - Hook registry for plugin hook registration
1872
+ * @returns Fully initialized PluginContext
1873
+ *
1874
+ * @example
1875
+ * ```typescript
1876
+ * import { getService, getHookRegistry } from 'nextly';
1877
+ *
1878
+ * // Create context for plugin initialization
1879
+ * const context = createPluginContext(getService, getHookRegistry());
1880
+ *
1881
+ * // Initialize plugins
1882
+ * for (const plugin of plugins) {
1883
+ * await plugin.init?.(context);
1884
+ * }
1885
+ * ```
1886
+ */
1887
+ declare function createPluginContext(getServiceFn: <T extends "collectionService" | "userService" | "mediaService" | "emailService" | "db" | "logger" | "config">(name: T) => T extends "collectionService" ? CollectionService : T extends "userService" ? UserService : T extends "mediaService" ? MediaService : T extends "emailService" ? EmailService : T extends "db" ? DatabaseInstance : T extends "logger" ? Logger : T extends "config" ? NextlyServiceConfig : never, hookRegistry: {
1888
+ register: (hookType: HookType, collection: string, handler: HookHandler) => void;
1889
+ unregister: (hookType: HookType, collection: string, handler: HookHandler) => void;
1890
+ }): PluginContext;
1891
+
1892
+ /**
1893
+ * Security Configuration Zod Schema
1894
+ *
1895
+ * Validates the `security` block in `defineConfig()`. Covers four sub-sections:
1896
+ * - `headers` — Security response headers (CSP, HSTS, X-Frame-Options, etc.)
1897
+ * - `cors` — Cross-Origin Resource Sharing configuration
1898
+ * - `uploads` — File upload MIME type restrictions
1899
+ * - `sanitization` — Input sanitization toggles
1900
+ *
1901
+ * All fields are optional with secure defaults applied at config resolution time.
1902
+ *
1903
+ * @module schemas/security-config
1904
+ * @since 1.0.0
1905
+ */
1906
+
1907
+ /**
1908
+ * Validates the `security.headers` block.
1909
+ *
1910
+ * Each header accepts a custom string value or `false` to disable it.
1911
+ * Omitted headers use their secure defaults (see `security-headers.ts`).
1912
+ */
1913
+ declare const SecurityHeadersConfigSchema: z.ZodObject<{
1914
+ contentSecurityPolicy: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<false>]>>;
1915
+ xContentTypeOptions: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<false>]>>;
1916
+ xFrameOptions: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<false>]>>;
1917
+ strictTransportSecurity: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<false>]>>;
1918
+ referrerPolicy: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<false>]>>;
1919
+ permissionsPolicy: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<false>]>>;
1920
+ }, z.core.$strip>;
1921
+ /**
1922
+ * Validates the `security.cors` block.
1923
+ *
1924
+ * Controls Cross-Origin Resource Sharing behaviour for all API responses.
1925
+ * Default: same-origin only (empty `origin` array).
1926
+ */
1927
+ declare const CorsConfigSchema: z.ZodObject<{
1928
+ origin: z.ZodOptional<z.ZodArray<z.ZodString>>;
1929
+ methods: z.ZodOptional<z.ZodArray<z.ZodString>>;
1930
+ allowedHeaders: z.ZodOptional<z.ZodArray<z.ZodString>>;
1931
+ exposedHeaders: z.ZodOptional<z.ZodArray<z.ZodString>>;
1932
+ credentials: z.ZodOptional<z.ZodBoolean>;
1933
+ maxAge: z.ZodOptional<z.ZodNumber>;
1934
+ }, z.core.$strip>;
1935
+ /**
1936
+ * Validates the `security.uploads` block.
1937
+ *
1938
+ * Controls MIME type restrictions and SVG serving behaviour for file uploads.
1939
+ */
1940
+ declare const UploadSecurityConfigSchema: z.ZodObject<{
1941
+ additionalMimeTypes: z.ZodOptional<z.ZodArray<z.ZodString>>;
1942
+ allowedMimeTypes: z.ZodOptional<z.ZodArray<z.ZodString>>;
1943
+ svgCsp: z.ZodOptional<z.ZodBoolean>;
1944
+ }, z.core.$strip>;
1945
+ /**
1946
+ * Validates the `security.sanitization` block.
1947
+ *
1948
+ * Controls which sanitization features are active. All default to `true`.
1949
+ */
1950
+ declare const SanitizationConfigSchema: z.ZodObject<{
1951
+ enabled: z.ZodOptional<z.ZodBoolean>;
1952
+ stripHtmlFromText: z.ZodOptional<z.ZodBoolean>;
1953
+ validateCssValues: z.ZodOptional<z.ZodBoolean>;
1954
+ validateUrlProtocols: z.ZodOptional<z.ZodBoolean>;
1955
+ }, z.core.$strip>;
1956
+ /**
1957
+ * Request body / multipart size caps. Each field accepts a byte count
1958
+ * or a human-readable suffix (`"1mb"`, `"500kb"`). String shorthand
1959
+ * is parsed at runtime; the schema stays permissive.
1960
+ */
1961
+ declare const SecurityLimitsConfigSchema: z.ZodObject<{
1962
+ json: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
1963
+ multipart: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
1964
+ fileSize: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
1965
+ fileCount: z.ZodOptional<z.ZodNumber>;
1966
+ fieldCount: z.ZodOptional<z.ZodNumber>;
1967
+ fieldSize: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
1968
+ }, z.core.$strip>;
1969
+ /**
1970
+ * Validates the full `security` namespace in `defineConfig()`.
1971
+ *
1972
+ * @example
1973
+ * ```typescript
1974
+ * import { SecurityConfigSchema } from '@nextly/schemas/security-config';
1975
+ *
1976
+ * const parsed = SecurityConfigSchema.parse({
1977
+ * headers: { contentSecurityPolicy: "default-src 'self'" },
1978
+ * cors: { origin: ['https://example.com'] },
1979
+ * sanitization: { enabled: true },
1980
+ * trustProxy: true,
1981
+ * limits: { multipart: "100mb" },
1982
+ * });
1983
+ * ```
1984
+ */
1985
+ /**
1986
+ * Per-IP rate limit on auth write endpoints (`/auth/login`,
1987
+ * `/auth/register`, `/auth/forgot-password`, `/auth/reset-password`).
1988
+ * Layered on top of the per-user lockout so an attacker can't cycle
1989
+ * usernames at full speed from one IP.
1990
+ *
1991
+ * The limiter shares one bucket across the four endpoints per IP so an
1992
+ * attacker can't reset their budget by switching paths. Set
1993
+ * `requestsPerHour` to `0` to disable (test/dev only).
1994
+ */
1995
+ declare const AuthRateLimitConfigSchema: z.ZodObject<{
1996
+ requestsPerHour: z.ZodOptional<z.ZodNumber>;
1997
+ windowMs: z.ZodOptional<z.ZodNumber>;
1998
+ }, z.core.$strip>;
1999
+ declare const SecurityConfigSchema: z.ZodObject<{
2000
+ headers: z.ZodOptional<z.ZodObject<{
2001
+ contentSecurityPolicy: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<false>]>>;
2002
+ xContentTypeOptions: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<false>]>>;
2003
+ xFrameOptions: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<false>]>>;
2004
+ strictTransportSecurity: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<false>]>>;
2005
+ referrerPolicy: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<false>]>>;
2006
+ permissionsPolicy: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<false>]>>;
2007
+ }, z.core.$strip>>;
2008
+ cors: z.ZodOptional<z.ZodObject<{
2009
+ origin: z.ZodOptional<z.ZodArray<z.ZodString>>;
2010
+ methods: z.ZodOptional<z.ZodArray<z.ZodString>>;
2011
+ allowedHeaders: z.ZodOptional<z.ZodArray<z.ZodString>>;
2012
+ exposedHeaders: z.ZodOptional<z.ZodArray<z.ZodString>>;
2013
+ credentials: z.ZodOptional<z.ZodBoolean>;
2014
+ maxAge: z.ZodOptional<z.ZodNumber>;
2015
+ }, z.core.$strip>>;
2016
+ uploads: z.ZodOptional<z.ZodObject<{
2017
+ additionalMimeTypes: z.ZodOptional<z.ZodArray<z.ZodString>>;
2018
+ allowedMimeTypes: z.ZodOptional<z.ZodArray<z.ZodString>>;
2019
+ svgCsp: z.ZodOptional<z.ZodBoolean>;
2020
+ }, z.core.$strip>>;
2021
+ sanitization: z.ZodOptional<z.ZodObject<{
2022
+ enabled: z.ZodOptional<z.ZodBoolean>;
2023
+ stripHtmlFromText: z.ZodOptional<z.ZodBoolean>;
2024
+ validateCssValues: z.ZodOptional<z.ZodBoolean>;
2025
+ validateUrlProtocols: z.ZodOptional<z.ZodBoolean>;
2026
+ }, z.core.$strip>>;
2027
+ limits: z.ZodOptional<z.ZodObject<{
2028
+ json: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
2029
+ multipart: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
2030
+ fileSize: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
2031
+ fileCount: z.ZodOptional<z.ZodNumber>;
2032
+ fieldCount: z.ZodOptional<z.ZodNumber>;
2033
+ fieldSize: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
2034
+ }, z.core.$strip>>;
2035
+ authRateLimit: z.ZodOptional<z.ZodObject<{
2036
+ requestsPerHour: z.ZodOptional<z.ZodNumber>;
2037
+ windowMs: z.ZodOptional<z.ZodNumber>;
2038
+ }, z.core.$strip>>;
2039
+ trustProxy: z.ZodOptional<z.ZodBoolean>;
2040
+ }, z.core.$strip>;
2041
+ type SecurityConfigInput = z.infer<typeof SecurityConfigSchema>;
2042
+ type SecurityHeadersConfigInput = z.infer<typeof SecurityHeadersConfigSchema>;
2043
+ type CorsConfigInput = z.infer<typeof CorsConfigSchema>;
2044
+ type UploadSecurityConfigInput = z.infer<typeof UploadSecurityConfigSchema>;
2045
+ type SanitizationConfigInput = z.infer<typeof SanitizationConfigSchema>;
2046
+ type SecurityLimitsConfigInput = z.infer<typeof SecurityLimitsConfigSchema>;
2047
+ type AuthRateLimitConfigInput = z.infer<typeof AuthRateLimitConfigSchema>;
2048
+
2049
+ /**
2050
+ * Nextly Config Types
2051
+ *
2052
+ * Canonical home for the public Nextly configuration interfaces and the
2053
+ * pure sanitization helper that fills in defaults. User-facing modules
2054
+ * like `src/collections/config/define-config.ts` re-export these types
2055
+ * and delegate the "fill defaults" step to `sanitizeConfig()`.
2056
+ *
2057
+ * @module shared/types/config
2058
+ * @since 1.0.0
2059
+ */
2060
+
2061
+ /**
2062
+ * TypeScript code generation configuration.
2063
+ *
2064
+ * Controls how TypeScript types are generated for collections.
2065
+ */
2066
+ interface TypeScriptConfig {
2067
+ /**
2068
+ * Path to the generated TypeScript file.
2069
+ * Can be absolute or relative to the project root.
2070
+ *
2071
+ * @default './src/types/generated/payload-types.ts'
2072
+ */
2073
+ outputFile?: string;
2074
+ /**
2075
+ * Whether to add module augmentation declarations.
2076
+ * When `true`, generates `declare module` blocks for type inference.
2077
+ *
2078
+ * @default true
2079
+ */
2080
+ declare?: boolean;
2081
+ }
2082
+ /**
2083
+ * Database schema and migration configuration.
2084
+ *
2085
+ * Controls where Drizzle schemas and migration files are generated.
2086
+ */
2087
+ interface DatabaseConfig {
2088
+ /**
2089
+ * Directory for generated Drizzle schema files.
2090
+ * Each collection generates a separate schema file.
2091
+ *
2092
+ * @default './src/db/schemas/collections'
2093
+ */
2094
+ schemasDir?: string;
2095
+ /**
2096
+ * Directory for generated migration files.
2097
+ * Migrations are created via CLI commands.
2098
+ *
2099
+ * @default './src/db/migrations'
2100
+ */
2101
+ migrationsDir?: string;
2102
+ }
2103
+ /**
2104
+ * Rate limiting configuration for API protection.
2105
+ *
2106
+ * Protects against abuse by limiting the number of requests
2107
+ * per time window. Enabled by default (100 read / 30 write per minute).
2108
+ * Opt out with `rateLimit: { enabled: false }`.
2109
+ */
2110
+ interface RateLimitingConfig {
2111
+ /**
2112
+ * Enable rate limiting.
2113
+ * @default true
2114
+ */
2115
+ enabled: boolean;
2116
+ /**
2117
+ * Maximum requests per window for read operations (GET).
2118
+ * @default 100
2119
+ */
2120
+ readLimit?: number;
2121
+ /**
2122
+ * Maximum requests per window for write operations (POST, PATCH, PUT, DELETE).
2123
+ * @default 30
2124
+ */
2125
+ writeLimit?: number;
2126
+ /**
2127
+ * Time window in milliseconds.
2128
+ * @default 60000 (1 minute)
2129
+ */
2130
+ windowMs?: number;
2131
+ /**
2132
+ * Custom store for rate limit state.
2133
+ * Defaults to in-memory store if not provided.
2134
+ *
2135
+ * For production with multiple instances, use a Redis-backed store.
2136
+ */
2137
+ store?: RateLimitStore;
2138
+ /**
2139
+ * Function to generate a unique key for rate limiting.
2140
+ * Defaults to using the client IP address.
2141
+ */
2142
+ keyGenerator?: (request: Request) => string;
2143
+ /**
2144
+ * Function to skip rate limiting for certain requests.
2145
+ * Returns true to skip rate limiting.
2146
+ */
2147
+ skip?: (request: Request) => boolean | Promise<boolean>;
2148
+ /**
2149
+ * Per-collection rate limit overrides.
2150
+ */
2151
+ collections?: Record<string, {
2152
+ readLimit?: number;
2153
+ writeLimit?: number;
2154
+ }>;
2155
+ }
2156
+ /**
2157
+ * Sanitized rate limiting configuration with defaults applied.
2158
+ */
2159
+ interface SanitizedRateLimitingConfig {
2160
+ /** Rate limiting is enabled */
2161
+ enabled: true;
2162
+ /** Maximum requests per window for read operations (GET) */
2163
+ readLimit: number;
2164
+ /** Maximum requests per window for write operations (POST, PATCH, PUT, DELETE) */
2165
+ writeLimit: number;
2166
+ /** Time window in milliseconds */
2167
+ windowMs: number;
2168
+ /** Custom store for rate limit state (optional) */
2169
+ store?: RateLimitStore;
2170
+ /** Function to generate a unique key for rate limiting (optional) */
2171
+ keyGenerator?: (request: Request) => string;
2172
+ /** Function to skip rate limiting for certain requests (optional) */
2173
+ skip?: (request: Request) => boolean | Promise<boolean>;
2174
+ /** Per-collection rate limit overrides (optional) */
2175
+ collections?: Record<string, {
2176
+ readLimit?: number;
2177
+ writeLimit?: number;
2178
+ }>;
2179
+ }
2180
+ /**
2181
+ * API key configuration.
2182
+ *
2183
+ * Controls per-key rate limiting for API key authentication.
2184
+ * All fields are optional — omitting the block entirely uses built-in defaults.
2185
+ */
2186
+ interface ApiKeysConfig {
2187
+ /**
2188
+ * Per-key rate limiting settings.
2189
+ * Omit to use defaults (1 000 req/hour, 1-hour window).
2190
+ */
2191
+ rateLimit?: {
2192
+ /**
2193
+ * Maximum requests an API key may make per sliding window.
2194
+ * Must be a positive integer.
2195
+ * @default 1000
2196
+ */
2197
+ requestsPerHour?: number;
2198
+ /**
2199
+ * Sliding window duration in milliseconds.
2200
+ * @default 3_600_000 (1 hour)
2201
+ */
2202
+ windowMs?: number;
2203
+ };
2204
+ }
2205
+ /**
2206
+ * Sanitized API key configuration with all defaults applied.
2207
+ */
2208
+ interface SanitizedApiKeysConfig {
2209
+ rateLimit: {
2210
+ /** Maximum requests per sliding window. */
2211
+ requestsPerHour: number;
2212
+ /** Sliding window duration in milliseconds. */
2213
+ windowMs: number;
2214
+ };
2215
+ }
2216
+ /**
2217
+ * Security configuration for Nextly.
2218
+ *
2219
+ * Controls security headers, CORS, file upload restrictions, and
2220
+ * input sanitization. All sub-sections are optional — secure defaults
2221
+ * are applied by the respective middleware factories at runtime.
2222
+ */
2223
+ interface SecurityConfig {
2224
+ /**
2225
+ * Security response headers configuration.
2226
+ *
2227
+ * Controls CSP, X-Content-Type-Options, X-Frame-Options, HSTS,
2228
+ * Referrer-Policy, and Permissions-Policy headers on API responses.
2229
+ * Each header can be set to a custom string or `false` to disable.
2230
+ * Omitted headers use secure defaults.
2231
+ */
2232
+ headers?: SecurityHeadersConfig;
2233
+ /**
2234
+ * Cross-Origin Resource Sharing (CORS) configuration.
2235
+ *
2236
+ * Default: same-origin only (no CORS headers). Use `origin: ['*']`
2237
+ * for development or provide an explicit allowlist for production.
2238
+ */
2239
+ cors?: CorsConfig;
2240
+ /**
2241
+ * File upload security configuration.
2242
+ *
2243
+ * Controls MIME type allowlist and SVG serving behaviour.
2244
+ * Default: common safe MIME types allowed, HTML/JS blocked,
2245
+ * SVG served with restrictive CSP.
2246
+ */
2247
+ uploads?: UploadSecurityConfigInput;
2248
+ /**
2249
+ * Input sanitization configuration.
2250
+ *
2251
+ * Controls HTML tag stripping for plain-text fields, CSS value
2252
+ * validation in rich text, and URL protocol validation.
2253
+ * All features enabled by default.
2254
+ */
2255
+ sanitization?: SanitizationConfigInput;
2256
+ /**
2257
+ * Request body / multipart size caps. Each numeric field accepts
2258
+ * either a byte count or a human-readable suffix (`"1mb"`,
2259
+ * `"500kb"`). Defaults: json 1mb / multipart 50mb / fileSize 10mb /
2260
+ * fileCount 10 / fieldCount 50 / fieldSize 100kb.
2261
+ */
2262
+ limits?: {
2263
+ json?: string | number;
2264
+ multipart?: string | number;
2265
+ fileSize?: string | number;
2266
+ fileCount?: number;
2267
+ fieldCount?: number;
2268
+ fieldSize?: string | number;
2269
+ };
2270
+ /**
2271
+ * Per-IP rate limit on `/auth/login`, `/auth/register`,
2272
+ * `/auth/forgot-password`, `/auth/reset-password`. One shared
2273
+ * bucket per IP across all four endpoints so credential-
2274
+ * stuffing from a single source can't cycle paths to refill its
2275
+ * budget. Layered on top of the per-user lockout, not in place of.
2276
+ *
2277
+ * Set `requestsPerHour: 0` to disable the per-IP envelope (test /
2278
+ * dev only — leaves the deployment exposed to credential-stuffing).
2279
+ *
2280
+ * @default `{ requestsPerHour: 30, windowMs: 3_600_000 }`
2281
+ */
2282
+ authRateLimit?: {
2283
+ requestsPerHour?: number;
2284
+ windowMs?: number;
2285
+ };
2286
+ /**
2287
+ * Trust reverse-proxy headers when resolving the client IP.
2288
+ *
2289
+ * When `true`, `X-Forwarded-For` (filtered through the
2290
+ * `TRUSTED_PROXY_IPS` env-var CIDR list) is used to determine the
2291
+ * client IP for rate limiting, refresh-token binding, and audit
2292
+ * logging. When `false` (default), proxy headers are ignored —
2293
+ * direct-internet deployments fall back to a single `unknown`
2294
+ * bucket so an attacker cannot rotate `X-Forwarded-For` to bypass
2295
+ * per-IP throttles.
2296
+ *
2297
+ * Audit: closes C4 (XFF blindly trusted across rate-limit / auth flows).
2298
+ *
2299
+ * @default false
2300
+ */
2301
+ trustProxy?: boolean;
2302
+ }
2303
+ /**
2304
+ * Resolved (HSL-triplet) color overrides for the admin UI.
2305
+ * These are derived from AdminBrandingColors after server-side hex conversion.
2306
+ */
2307
+ interface AdminBrandingColors {
2308
+ /** Hex color for the primary brand color, e.g. "#6366f1". Replaces blue-500. */
2309
+ primary?: string;
2310
+ /** Hex color for the accent brand color, e.g. "#f59e0b". Replaces cyan-500. */
2311
+ accent?: string;
2312
+ }
2313
+ /**
2314
+ * Branding configuration for the Nextly admin UI.
2315
+ */
2316
+ interface AdminBrandingConfig {
2317
+ /**
2318
+ * URL of a logo image to display in the sidebar.
2319
+ * Can be an absolute URL or a path served from your Next.js public folder.
2320
+ * When set, the logo image is shown instead of the text logo.
2321
+ *
2322
+ * @example "/logo.svg" or "https://cdn.example.com/logo.png"
2323
+ */
2324
+ logoUrl?: string;
2325
+ /**
2326
+ * URL of the light-mode logo image.
2327
+ * Used when `logoUrl` is not set.
2328
+ */
2329
+ logoUrlLight?: string;
2330
+ /**
2331
+ * URL of the dark-mode logo image.
2332
+ * Used when `logoUrl` is not set.
2333
+ */
2334
+ logoUrlDark?: string;
2335
+ /**
2336
+ * Text label shown in the sidebar header.
2337
+ * Replaces the default "Nextly" label.
2338
+ * Also used as the `alt` attribute when `logoUrl` is set.
2339
+ *
2340
+ * @default "Nextly"
2341
+ */
2342
+ logoText?: string;
2343
+ /**
2344
+ * URL of a custom favicon to inject into the admin page.
2345
+ */
2346
+ favicon?: string;
2347
+ /**
2348
+ * Custom brand colors for the admin UI.
2349
+ * Accept 6-digit hex values only (e.g. "#6366f1").
2350
+ * Foreground colors are calculated automatically to ensure WCAG AA contrast.
2351
+ */
2352
+ colors?: AdminBrandingColors;
2353
+ /**
2354
+ * Toggle visibility of builder-related navigation (Collections/Singles/Components builders).
2355
+ *
2356
+ * This is evaluated at runtime via the `/api/admin-meta` response.
2357
+ *
2358
+ * Default behavior follows `NODE_ENV`:
2359
+ * - `production` => hidden
2360
+ * - `development` / `test` => visible
2361
+ *
2362
+ * Precedence:
2363
+ * 1) `admin.branding.showBuilder` (this field)
2364
+ * 2) `NODE_ENV` default mapping
2365
+ *
2366
+ * @default `process.env.NODE_ENV !== "production"`
2367
+ */
2368
+ showBuilder?: boolean;
2369
+ }
2370
+ /**
2371
+ * Per-plugin overrides for sidebar placement and appearance.
2372
+ *
2373
+ * The host developer can override any subset of a plugin's admin config
2374
+ * without modifying the plugin's source code. Uses shallow merge —
2375
+ * only specified fields override the plugin author's defaults.
2376
+ */
2377
+ interface PluginOverride {
2378
+ /** Override the plugin's sidebar placement */
2379
+ placement?: AdminPlacement;
2380
+ /** Override the plugin's sort order */
2381
+ order?: number;
2382
+ /** Override the position anchor for standalone plugins (which built-in section to appear after) */
2383
+ after?: "dashboard" | "collections" | "singles" | "media" | "plugins" | "users" | "settings";
2384
+ /** Override or extend the plugin's sidebar appearance (shallow-merged) */
2385
+ appearance?: Partial<PluginAdminAppearance>;
2386
+ }
2387
+ /**
2388
+ * Top-level admin UI configuration for the Nextly admin panel.
2389
+ */
2390
+ interface AdminConfig {
2391
+ /** Branding customizations: logo, colors, favicon. */
2392
+ branding?: AdminBrandingConfig;
2393
+ /**
2394
+ * Per-plugin overrides for sidebar placement and appearance.
2395
+ *
2396
+ * Keys are plugin slugs (derived from plugin name, e.g., "form-builder").
2397
+ * Values are partial overrides - only specified fields are changed.
2398
+ */
2399
+ pluginOverrides?: Record<string, PluginOverride>;
2400
+ /**
2401
+ * Development-only auto-login.
2402
+ *
2403
+ * When set in dev (NODE_ENV !== "production"), the admin auth gate
2404
+ * issues a real session cookie for the named user on the first
2405
+ * /admin visit if no session is present. Same JWT-signing codepath
2406
+ * the real login flow uses; the only difference is the trigger.
2407
+ *
2408
+ * Hard-blocked when NODE_ENV === "production": Nextly's runtime
2409
+ * ignores this field with a console warning so a misconfigured prod
2410
+ * deploy can't silently auto-login users.
2411
+ *
2412
+ * Useful for the contributor playground and for local development
2413
+ * of your own Nextly project to skip the manual login step.
2414
+ *
2415
+ * @example
2416
+ * admin: {
2417
+ * devAutoLogin: { email: "dev@nextly.local", password: "dev" },
2418
+ * }
2419
+ *
2420
+ * DO NOT enable in production deployments.
2421
+ */
2422
+ devAutoLogin?: false | {
2423
+ /** Email address of the user to auto-login as. The user must exist (auto-login does not create users). */
2424
+ email: string;
2425
+ /**
2426
+ * Optional. Used only as a label in dev logs. Auto-login does
2427
+ * not verify the password - it bypasses the password-check
2428
+ * codepath entirely since the contributor configured this.
2429
+ */
2430
+ password?: string;
2431
+ };
2432
+ }
2433
+ /**
2434
+ * Authentication configuration for Nextly.
2435
+ *
2436
+ * PR 5 (unified-error-system): introduces the `revealRegistrationConflict`
2437
+ * opt-in flag. Default behaviour is silent-success on duplicate-email
2438
+ * registration to prevent account enumeration via the registration form
2439
+ * (spec §13.2). Some products (e.g. internal admin tools where every user
2440
+ * is known) prefer an explicit "email already in use" message — flip this
2441
+ * to `true` to opt into the legacy reveal-on-conflict behaviour.
2442
+ */
2443
+ interface AuthConfig {
2444
+ /**
2445
+ * Whether `/auth/register` should respond with an explicit
2446
+ * `DUPLICATE` / "An account already exists for this email." error when
2447
+ * the submitted email is already registered.
2448
+ *
2449
+ * Default: `false`. The registration endpoint instead returns the same
2450
+ * "If this email is available, we've sent a confirmation link." success
2451
+ * shape it would on a fresh signup, regardless of whether the email
2452
+ * existed. The duplicate is logged for operators.
2453
+ *
2454
+ * Set to `true` only if your threat model genuinely doesn't care about
2455
+ * email enumeration (e.g. a closed admin tool with controlled signup).
2456
+ */
2457
+ revealRegistrationConflict?: boolean;
2458
+ }
2459
+ /**
2460
+ * Sanitized auth configuration with all defaults applied.
2461
+ */
2462
+ interface SanitizedAuthConfig {
2463
+ /** Whether to reveal duplicate-email registrations on the wire. Defaults to false. */
2464
+ revealRegistrationConflict: boolean;
2465
+ }
2466
+ /**
2467
+ * Complete Nextly configuration interface.
2468
+ *
2469
+ * This is the main configuration object for a Nextly application,
2470
+ * typically exported from `nextly.config.ts` at the project root.
2471
+ */
2472
+ interface NextlyConfig {
2473
+ /** Array of collection configurations. */
2474
+ collections?: CollectionConfig[];
2475
+ /** Array of Single configurations. */
2476
+ singles?: SingleConfig[];
2477
+ /** Array of Component configurations. */
2478
+ components?: ComponentConfig[];
2479
+ /** User model extension configuration. */
2480
+ users?: UserConfig;
2481
+ /** Email provider and template configuration. */
2482
+ email?: EmailConfig;
2483
+ /** TypeScript type generation configuration. */
2484
+ typescript?: TypeScriptConfig;
2485
+ /** Database schema and migration configuration. */
2486
+ db?: DatabaseConfig;
2487
+ /**
2488
+ * Rate limiting configuration for API protection.
2489
+ *
2490
+ * Enabled by default (100 read / 30 write per minute). Opt out with `enabled: false`.
2491
+ */
2492
+ rateLimit?: RateLimitingConfig;
2493
+ /**
2494
+ * API key authentication configuration.
2495
+ *
2496
+ * Controls per-key rate limiting applied when requests authenticate via
2497
+ * `Authorization: Bearer nx_live_...`. Session-based requests are unaffected.
2498
+ */
2499
+ apiKeys?: ApiKeysConfig;
2500
+ /**
2501
+ * Authentication configuration.
2502
+ *
2503
+ * Currently exposes the `revealRegistrationConflict` opt-in flag (PR 5,
2504
+ * spec §13.2). Future auth-related options (token TTLs, lockout policy,
2505
+ * etc.) will land here so the wire surface has a single canonical home.
2506
+ */
2507
+ auth?: AuthConfig;
2508
+ /** Storage plugins for cloud storage providers. */
2509
+ storage?: StoragePlugin[];
2510
+ /** Plugins to extend Nextly functionality. */
2511
+ plugins?: PluginDefinition[];
2512
+ /** Security configuration for headers, CORS, uploads, and sanitization. */
2513
+ security?: SecurityConfig;
2514
+ /** Admin UI customization. */
2515
+ admin?: AdminConfig;
2516
+ }
2517
+ /**
2518
+ * Normalized Nextly configuration with all defaults applied.
2519
+ *
2520
+ * This type represents the config after `sanitizeConfig()` has processed it,
2521
+ * with all array-valued and default-bearing fields filled in.
2522
+ *
2523
+ * Returned by `defineConfig()` and consumed by `getNextly()`, the DI
2524
+ * registration pipeline, and downstream services.
2525
+ */
2526
+ interface SanitizedNextlyConfig {
2527
+ /** Array of collection configurations (empty array if none provided). */
2528
+ collections: CollectionConfig[];
2529
+ /** Array of Single configurations (empty array if none provided). */
2530
+ singles: SingleConfig[];
2531
+ /** Array of Component configurations (empty array if none provided). */
2532
+ components: ComponentConfig[];
2533
+ /** User model extension configuration. Undefined if no user config provided. */
2534
+ users?: UserConfig;
2535
+ /** Email provider and template configuration. Undefined if no email config provided. */
2536
+ email?: EmailConfig;
2537
+ /** TypeScript configuration with defaults applied. */
2538
+ typescript: Required<TypeScriptConfig>;
2539
+ /** Database configuration with defaults applied. */
2540
+ db: Required<DatabaseConfig>;
2541
+ /**
2542
+ * Rate limiting configuration.
2543
+ * Built automatically unless `rateLimit: { enabled: false }` is set.
2544
+ */
2545
+ rateLimit?: SanitizedRateLimitingConfig;
2546
+ /**
2547
+ * API key configuration with defaults applied.
2548
+ * Undefined if omitted from defineConfig() (built-in defaults used).
2549
+ */
2550
+ apiKeys?: SanitizedApiKeysConfig;
2551
+ /**
2552
+ * Auth configuration with defaults applied. Always present after
2553
+ * sanitization; the `revealRegistrationConflict` flag falls back to
2554
+ * `false` (silent-success on duplicate email).
2555
+ */
2556
+ auth: SanitizedAuthConfig;
2557
+ /** Storage plugins for cloud storage providers (empty array if none configured). */
2558
+ storage: StoragePlugin[];
2559
+ /** Plugins to extend Nextly functionality (empty array if none configured). */
2560
+ plugins: PluginDefinition[];
2561
+ /** Security configuration for headers, CORS, uploads, and sanitization. */
2562
+ security?: SecurityConfig;
2563
+ /** Admin UI customization config. */
2564
+ admin?: AdminConfig;
2565
+ }
2566
+ /**
2567
+ * Fill defaults on a raw `NextlyConfig` and return a `SanitizedNextlyConfig`.
2568
+ *
2569
+ * This is a pure transformation — it does **not** validate slug uniqueness,
2570
+ * component nesting depth, or user-field constraints. Callers that need
2571
+ * validation (like `defineConfig()`) should validate first and then call
2572
+ * this helper.
2573
+ *
2574
+ * After this step, downstream code can rely on `collections`, `singles`,
2575
+ * `components`, `storage`, `plugins`, `typescript`, and `db` being present
2576
+ * and nil-check-free.
2577
+ *
2578
+ * Validates that `apiKeys.rateLimit.requestsPerHour` and `apiKeys.rateLimit.windowMs`
2579
+ * are positive, because accepting those values without a bound would silently
2580
+ * disable rate limiting in production.
2581
+ *
2582
+ * @param config - Raw Nextly configuration
2583
+ * @returns Sanitized configuration with defaults applied
2584
+ * @throws Error if `apiKeys.rateLimit` values are invalid
2585
+ */
2586
+ declare function sanitizeConfig(config: NextlyConfig): SanitizedNextlyConfig;
2587
+
2588
+ export { isServicesRegistered as $, SecurityConfigSchema as F, InMemoryRateLimitStore as I, SecurityHeadersConfigSchema as J, SecurityLimitsConfigSchema as M, UploadSecurityConfigSchema as Q, clearServices as V, createPluginContext as W, createRateLimitHeaders as X, createRateLimiter as Y, definePlugin as Z, getService as _, registerServices as a0, shutdownServices as a1, AdminPlacement as h, AuthRateLimitConfigSchema as j, CollectionService as l, CorsConfigSchema as o, sanitizeConfig as s, SanitizationConfigSchema as z };
2589
+ export type { AdminBrandingColors as A, SecurityConfig as B, Collection as C, DatabaseConfig as D, SecurityConfigInput as E, SecurityHeadersConfig as G, SecurityHeadersConfigInput as H, SecurityLimitsConfigInput as K, ListCollectionsOptions as L, NextlyConfig as N, UploadSecurityConfigInput as O, PluginAdminAppearance as P, RateLimitRecord as R, SanitizedNextlyConfig as S, TypeScriptConfig as T, UpdateCollectionInput as U, RateLimitStore as a, RateLimitingConfig as b, SanitizedRateLimitingConfig as c, NextlyServiceConfig as d, ServiceMap as e, AdminBrandingConfig as f, AdminConfig as g, AuthRateLimitConfigInput as i, CollectionEntry as k, CorsConfig as m, CorsConfigInput as n, CreateCollectionInput as p, PluginAdminConfig as q, PluginContext as r, PluginDefinition as t, PluginHookRegistry as u, PluginOverride as v, RateLimitConfig as w, RateLimitResult as x, SanitizationConfigInput as y };