nextly 0.0.1 → 0.0.2-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +122 -0
- package/dist/_dts-chunks/collections-handler.d-DjgO74Wt.d.ts +20540 -0
- package/dist/_dts-chunks/config.d-DNwsDnjs.d.ts +2589 -0
- package/dist/_dts-chunks/define-component.d-BUgTHmt3.d.ts +1149 -0
- package/dist/_dts-chunks/image-processor.d-OO1PmMrv.d.ts +335 -0
- package/dist/_dts-chunks/index.d-axCAzZ7m.d.ts +17842 -0
- package/dist/_dts-chunks/media.d-DjDOZo4B.d.ts +117 -0
- package/dist/_dts-chunks/on-error.d-CHIKWNxd.d.ts +38 -0
- package/dist/_dts-chunks/storage.d-BUhQ2we_.d.ts +404 -0
- package/dist/actions/index.d.ts +239 -0
- package/dist/actions/index.mjs +281 -0
- package/dist/api/auth-state.d.ts +5 -0
- package/dist/api/auth-state.mjs +131 -0
- package/dist/api/collections-schema-detail.d.ts +56 -0
- package/dist/api/collections-schema-detail.mjs +244 -0
- package/dist/api/collections-schema-export.d.ts +56 -0
- package/dist/api/collections-schema-export.mjs +129 -0
- package/dist/api/collections-schema.d.ts +59 -0
- package/dist/api/collections-schema.mjs +207 -0
- package/dist/api/components-detail.d.ts +50 -0
- package/dist/api/components-detail.mjs +132 -0
- package/dist/api/components.d.ts +69 -0
- package/dist/api/components.mjs +144 -0
- package/dist/api/email-providers-default.d.ts +40 -0
- package/dist/api/email-providers-default.mjs +75 -0
- package/dist/api/email-providers-detail.d.ts +81 -0
- package/dist/api/email-providers-detail.mjs +109 -0
- package/dist/api/email-providers-test.d.ts +43 -0
- package/dist/api/email-providers-test.mjs +114 -0
- package/dist/api/email-providers.d.ts +69 -0
- package/dist/api/email-providers.mjs +110 -0
- package/dist/api/email-send-template.d.ts +41 -0
- package/dist/api/email-send-template.mjs +58 -0
- package/dist/api/email-send.d.ts +42 -0
- package/dist/api/email-send.mjs +58 -0
- package/dist/api/email-templates-detail.d.ts +74 -0
- package/dist/api/email-templates-detail.mjs +112 -0
- package/dist/api/email-templates-layout.d.ts +55 -0
- package/dist/api/email-templates-layout.mjs +92 -0
- package/dist/api/email-templates-preview.d.ts +48 -0
- package/dist/api/email-templates-preview.mjs +93 -0
- package/dist/api/email-templates.d.ts +61 -0
- package/dist/api/email-templates.mjs +118 -0
- package/dist/api/health.d.ts +68 -0
- package/dist/api/health.mjs +67 -0
- package/dist/api/index.d.ts +54 -0
- package/dist/api/index.mjs +16 -0
- package/dist/api/media-bulk.d.ts +74 -0
- package/dist/api/media-bulk.mjs +196 -0
- package/dist/api/media-folders.d.ts +112 -0
- package/dist/api/media-folders.mjs +187 -0
- package/dist/api/media-handlers.d.ts +102 -0
- package/dist/api/media-handlers.mjs +437 -0
- package/dist/api/media.d.ts +117 -0
- package/dist/api/media.mjs +242 -0
- package/dist/api/singles-detail.d.ts +87 -0
- package/dist/api/singles-detail.mjs +170 -0
- package/dist/api/singles-schema-detail.d.ts +54 -0
- package/dist/api/singles-schema-detail.mjs +182 -0
- package/dist/api/singles.d.ts +34 -0
- package/dist/api/singles.mjs +94 -0
- package/dist/api/storage-upload-url.d.ts +48 -0
- package/dist/api/storage-upload-url.mjs +202 -0
- package/dist/api/uploads.d.ts +109 -0
- package/dist/api/uploads.mjs +359 -0
- package/dist/auth/index.d.ts +425 -0
- package/dist/auth/index.mjs +199 -0
- package/dist/boot-apply-PQSYLDIN.mjs +7 -0
- package/dist/chunk-2OALJTK6.mjs +489 -0
- package/dist/chunk-2Q2SX2CS.mjs +365 -0
- package/dist/chunk-2TFX4ND3.mjs +13 -0
- package/dist/chunk-2TWPDSYD.mjs +87 -0
- package/dist/chunk-2W3DVD7S.mjs +647 -0
- package/dist/chunk-2ZFKXPQM.mjs +88 -0
- package/dist/chunk-3FA7FKAV.mjs +832 -0
- package/dist/chunk-3NZ2KMBL.mjs +58 -0
- package/dist/chunk-4MJLT6PZ.mjs +0 -0
- package/dist/chunk-56WO4WX7.mjs +0 -0
- package/dist/chunk-5APFUGAD.mjs +89 -0
- package/dist/chunk-5HMZ644B.mjs +108 -0
- package/dist/chunk-67GXH6PR.mjs +32 -0
- package/dist/chunk-6JNEPWRW.mjs +14368 -0
- package/dist/chunk-6NFHQIJD.mjs +45 -0
- package/dist/chunk-7P6ASYW6.mjs +9 -0
- package/dist/chunk-A3WPLSDT.mjs +1364 -0
- package/dist/chunk-AGJ6F2T3.mjs +144 -0
- package/dist/chunk-AK6Z23OX.mjs +1464 -0
- package/dist/chunk-APKKRD2G.mjs +102 -0
- package/dist/chunk-B2GV2BWH.mjs +73 -0
- package/dist/chunk-D5HQBNUB.mjs +74 -0
- package/dist/chunk-DNNG377Z.mjs +204 -0
- package/dist/chunk-DP3G27G5.mjs +135 -0
- package/dist/chunk-DV6WVX2Q.mjs +0 -0
- package/dist/chunk-DXGGXIUZ.mjs +57 -0
- package/dist/chunk-EGXBZCGC.mjs +943 -0
- package/dist/chunk-ERCNLX3V.mjs +176 -0
- package/dist/chunk-FQULBZ53.mjs +850 -0
- package/dist/chunk-G2AA4QLC.mjs +262 -0
- package/dist/chunk-GDBJ5JCU.mjs +488 -0
- package/dist/chunk-GJNSJU4S.mjs +19 -0
- package/dist/chunk-GZ6DCQKC.mjs +69 -0
- package/dist/chunk-H26B4FYG.mjs +167 -0
- package/dist/chunk-I4JMR3UR.mjs +21 -0
- package/dist/chunk-INV7QKLG.mjs +508 -0
- package/dist/chunk-IUDOC7N7.mjs +46 -0
- package/dist/chunk-IZWPRDC3.mjs +206 -0
- package/dist/chunk-KIMNCZGV.mjs +15 -0
- package/dist/chunk-L6HW2DA7.mjs +15 -0
- package/dist/chunk-LAZXX4HR.mjs +100 -0
- package/dist/chunk-LDKCUMHK.mjs +95 -0
- package/dist/chunk-LRXMECUA.mjs +0 -0
- package/dist/chunk-M52VMPGA.mjs +119 -0
- package/dist/chunk-MGUWEEI6.mjs +160 -0
- package/dist/chunk-NRUWQ5Z7.mjs +419 -0
- package/dist/chunk-NSEFNNU4.mjs +25360 -0
- package/dist/chunk-NTHVDFGO.mjs +138 -0
- package/dist/chunk-O3QHXMOX.mjs +3166 -0
- package/dist/chunk-P7NH2OSC.mjs +2605 -0
- package/dist/chunk-PKMABBB5.mjs +184 -0
- package/dist/chunk-PWS6XGJK.mjs +76 -0
- package/dist/chunk-R6JJQHFC.mjs +20 -0
- package/dist/chunk-RJLLGGPG.mjs +0 -0
- package/dist/chunk-SBACDPNX.mjs +689 -0
- package/dist/chunk-TO5AFLVQ.mjs +124 -0
- package/dist/chunk-TS7GHTG2.mjs +5436 -0
- package/dist/chunk-UJ2IMJ4W.mjs +133 -0
- package/dist/chunk-UOP63Q54.mjs +102 -0
- package/dist/chunk-UUOFWCM6.mjs +78 -0
- package/dist/chunk-V4EQTOA4.mjs +893 -0
- package/dist/chunk-VJ66NCL4.mjs +193 -0
- package/dist/chunk-VQJQHVEV.mjs +29 -0
- package/dist/chunk-VTJADRO3.mjs +141 -0
- package/dist/chunk-VWF3JO32.mjs +0 -0
- package/dist/chunk-W4MGXIRR.mjs +27 -0
- package/dist/chunk-W5KKPZT5.mjs +1204 -0
- package/dist/chunk-WD34YQ6T.mjs +381 -0
- package/dist/chunk-WZBYMYVW.mjs +14 -0
- package/dist/chunk-X23WKS3Z.mjs +50 -0
- package/dist/chunk-X7TXCYYN.mjs +6496 -0
- package/dist/chunk-XGI4EMS3.mjs +140 -0
- package/dist/chunk-XZKLBMN6.mjs +1153 -0
- package/dist/chunk-YB7INWPY.mjs +0 -0
- package/dist/chunk-YV4Y7SDL.mjs +83 -0
- package/dist/chunk-YZNBLFIW.mjs +1688 -0
- package/dist/chunk-YZZCTONM.mjs +263 -0
- package/dist/chunk-ZE6A3FYH.mjs +289 -0
- package/dist/cli/nextly.mjs +68 -0
- package/dist/cli/utils/index.d.ts +449 -0
- package/dist/cli/utils/index.mjs +49 -0
- package/dist/component-schema-service-5577KVW6.mjs +11 -0
- package/dist/config-loader-23YEMC3Z.mjs +23 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.mjs +109 -0
- package/dist/container-ORGFGYSZ.mjs +9 -0
- package/dist/database/index.d.ts +12 -0
- package/dist/database/index.mjs +40 -0
- package/dist/database/seeders/index.d.ts +93 -0
- package/dist/database/seeders/index.mjs +47 -0
- package/dist/db-sync-demote-LJGKLB3S.mjs +117 -0
- package/dist/db-sync-promote-B26VSYQF.mjs +113 -0
- package/dist/dev-reload-broadcaster-B73IQ53V.mjs +25 -0
- package/dist/dist-M2NOU37V.mjs +19 -0
- package/dist/drizzle-kit-lazy-D2M2PXR2.mjs +13 -0
- package/dist/dynamic-collection-schema-service-IEXTPIZ7.mjs +8 -0
- package/dist/errors/index.d.ts +159 -0
- package/dist/errors/index.mjs +10 -0
- package/dist/factory-IWMBKUJM.mjs +15 -0
- package/dist/first-run-QIVKWJIF.mjs +63 -0
- package/dist/fresh-push-NR67DC3R.mjs +8 -0
- package/dist/index.d.ts +4175 -0
- package/dist/index.mjs +1336 -0
- package/dist/local-plugin-PTET4NAT.mjs +7 -0
- package/dist/logger-NU46DXNY.mjs +15 -0
- package/dist/logger-YE4TC7ZN.mjs +9 -0
- package/dist/migration-journal-EP532Y4L.mjs +139 -0
- package/dist/migrations/mysql/0000_eager_sentry.sql +174 -0
- package/dist/migrations/mysql/0001_soft_giant_girl.sql +27 -0
- package/dist/migrations/mysql/0002_media_table.sql +24 -0
- package/dist/migrations/mysql/0003_dynamic_singles.sql +37 -0
- package/dist/migrations/mysql/0004_dynamic_components.sql +35 -0
- package/dist/migrations/mysql/0005_user_management_tables.sql +92 -0
- package/dist/migrations/mysql/0006_api_keys.sql +36 -0
- package/dist/migrations/mysql/0007_general_settings.sql +20 -0
- package/dist/migrations/mysql/0008_site_settings_logo_url.sql +9 -0
- package/dist/migrations/mysql/0009_activity_log.sql +30 -0
- package/dist/migrations/mysql/0010_site_settings_sidebar.sql +13 -0
- package/dist/migrations/mysql/0011_missing_tables_and_columns.sql +54 -0
- package/dist/migrations/mysql/0012_image_sizes_and_focal_point.sql +30 -0
- package/dist/migrations/mysql/0012_media_folders.sql +43 -0
- package/dist/migrations/mysql/0013_user_brute_force_protection.sql +31 -0
- package/dist/migrations/mysql/0014_email_template_attachments.sql +12 -0
- package/dist/migrations/mysql/0015_media_uploaded_by_nullable.sql +15 -0
- package/dist/migrations/mysql/20260429_000000_000_initial_journal.sql +22 -0
- package/dist/migrations/mysql/20260501_000000_journal_batch.sql +17 -0
- package/dist/migrations/mysql/20260501_000001_audit_log.sql +24 -0
- package/dist/migrations/mysql/20260504_000000_nextly_meta.sql +21 -0
- package/dist/migrations/mysql/meta/0000_snapshot.json +1005 -0
- package/dist/migrations/mysql/meta/0001_snapshot.json +1099 -0
- package/dist/migrations/mysql/meta/_journal.json +41 -0
- package/dist/migrations/postgresql/0000_misty_king_bedlam.sql +169 -0
- package/dist/migrations/postgresql/0001_perpetual_captain_marvel.sql +8 -0
- package/dist/migrations/postgresql/0002_sad_spectrum.sql +16 -0
- package/dist/migrations/postgresql/0003_hesitant_ultron.sql +17 -0
- package/dist/migrations/postgresql/0004_media_table.sql +24 -0
- package/dist/migrations/postgresql/0005_media_folders.sql +36 -0
- package/dist/migrations/postgresql/0006_dynamic_collections_update.sql +50 -0
- package/dist/migrations/postgresql/0007_dynamic_singles.sql +38 -0
- package/dist/migrations/postgresql/0008_dynamic_components.sql +37 -0
- package/dist/migrations/postgresql/0009_user_management_tables.sql +95 -0
- package/dist/migrations/postgresql/0010_api_keys.sql +34 -0
- package/dist/migrations/postgresql/0011_general_settings.sql +20 -0
- package/dist/migrations/postgresql/0012_site_settings_logo_url.sql +9 -0
- package/dist/migrations/postgresql/0013_activity_log.sql +29 -0
- package/dist/migrations/postgresql/0014_image_sizes_and_focal_point.sql +33 -0
- package/dist/migrations/postgresql/0014_site_settings_sidebar.sql +13 -0
- package/dist/migrations/postgresql/0015_user_brute_force_protection.sql +29 -0
- package/dist/migrations/postgresql/0016_email_template_attachments.sql +12 -0
- package/dist/migrations/postgresql/0017_media_uploaded_by_nullable.sql +15 -0
- package/dist/migrations/postgresql/20260429_000000_000_initial_journal.sql +24 -0
- package/dist/migrations/postgresql/20260501_000000_journal_batch.sql +17 -0
- package/dist/migrations/postgresql/20260501_000001_audit_log.sql +24 -0
- package/dist/migrations/postgresql/20260504_000000_nextly_meta.sql +22 -0
- package/dist/migrations/postgresql/meta/0000_snapshot.json +1286 -0
- package/dist/migrations/postgresql/meta/0001_snapshot.json +1407 -0
- package/dist/migrations/postgresql/meta/0002_snapshot.json +1552 -0
- package/dist/migrations/postgresql/meta/0003_snapshot.json +1695 -0
- package/dist/migrations/postgresql/meta/0010_snapshot.json +2345 -0
- package/dist/migrations/postgresql/meta/_journal.json +90 -0
- package/dist/migrations/sqlite/0000_api_keys.sql +34 -0
- package/dist/migrations/sqlite/0001_general_settings.sql +20 -0
- package/dist/migrations/sqlite/0002_site_settings_logo_url.sql +9 -0
- package/dist/migrations/sqlite/0003_activity_log.sql +29 -0
- package/dist/migrations/sqlite/0004_image_sizes_and_focal_point.sql +29 -0
- package/dist/migrations/sqlite/0004_site_settings_sidebar.sql +11 -0
- package/dist/migrations/sqlite/0005_user_brute_force_protection.sql +29 -0
- package/dist/migrations/sqlite/0006_email_template_attachments.sql +12 -0
- package/dist/migrations/sqlite/0007_media_uploaded_by_nullable.sql +111 -0
- package/dist/migrations/sqlite/20260429_000000_000_initial_journal.sql +24 -0
- package/dist/migrations/sqlite/20260501_000000_journal_batch.sql +19 -0
- package/dist/migrations/sqlite/20260501_000001_audit_log.sql +24 -0
- package/dist/migrations/sqlite/20260504_000000_nextly_meta.sql +21 -0
- package/dist/migrations/sqlite/20260505_000000_user_management_tables.sql +77 -0
- package/dist/next.d.ts +57 -0
- package/dist/next.mjs +55 -0
- package/dist/observability/index.d.ts +87 -0
- package/dist/observability/index.mjs +57 -0
- package/dist/permissions-3DZZQZMI.mjs +39 -0
- package/dist/pipeline-YOML7SWF.mjs +29 -0
- package/dist/preview-ZZTR3QGS.mjs +9 -0
- package/dist/program-PW6UB2ZC.mjs +5934 -0
- package/dist/reconcile-single-tables-7ENVXJGB.mjs +7 -0
- package/dist/register-SF6E6FVU.mjs +49 -0
- package/dist/reload-config-HWQ4G5MM.mjs +23 -0
- package/dist/resolve-single-table-name-JSOMUB3R.mjs +7 -0
- package/dist/routeHandler-UNMMJIBM.mjs +77 -0
- package/dist/runtime-schema-generator-NRA6A6Z6.mjs +8 -0
- package/dist/runtime.d.ts +120 -0
- package/dist/runtime.mjs +73 -0
- package/dist/schema-hash-FMMG6VPJ.mjs +13 -0
- package/dist/schema-registry-EQ36FZDP.mjs +7 -0
- package/dist/scripts/load-env.mjs +42 -0
- package/dist/storage/index.d.ts +566 -0
- package/dist/storage/index.mjs +45 -0
- package/dist/super-admin-G5ZK5F4T.mjs +39 -0
- package/dist/system-table-service-WGSRVEGT.mjs +17 -0
- package/dist/users-7KELGRYJ.mjs +38 -0
- 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 };
|