@nexpress/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -0
  3. package/dist/audit-54XLVCWD.js +14 -0
  4. package/dist/audit-54XLVCWD.js.map +1 -0
  5. package/dist/auth.d.ts +640 -0
  6. package/dist/auth.js +94 -0
  7. package/dist/auth.js.map +1 -0
  8. package/dist/can-YLUHRJAB.js +19 -0
  9. package/dist/can-YLUHRJAB.js.map +1 -0
  10. package/dist/chunk-2G264RCD.js +68 -0
  11. package/dist/chunk-2G264RCD.js.map +1 -0
  12. package/dist/chunk-2YDGE7YX.js +92 -0
  13. package/dist/chunk-2YDGE7YX.js.map +1 -0
  14. package/dist/chunk-473S4TER.js +538 -0
  15. package/dist/chunk-473S4TER.js.map +1 -0
  16. package/dist/chunk-4ZLMEKFX.js +18 -0
  17. package/dist/chunk-4ZLMEKFX.js.map +1 -0
  18. package/dist/chunk-55FU6WED.js +179 -0
  19. package/dist/chunk-55FU6WED.js.map +1 -0
  20. package/dist/chunk-6YI5K2TI.js +1959 -0
  21. package/dist/chunk-6YI5K2TI.js.map +1 -0
  22. package/dist/chunk-BHK3AD3Q.js +41 -0
  23. package/dist/chunk-BHK3AD3Q.js.map +1 -0
  24. package/dist/chunk-CRUQBZUF.js +39 -0
  25. package/dist/chunk-CRUQBZUF.js.map +1 -0
  26. package/dist/chunk-CTSQ7BRI.js +175 -0
  27. package/dist/chunk-CTSQ7BRI.js.map +1 -0
  28. package/dist/chunk-DK2JBJH7.js +81 -0
  29. package/dist/chunk-DK2JBJH7.js.map +1 -0
  30. package/dist/chunk-DP2PREDU.js +597 -0
  31. package/dist/chunk-DP2PREDU.js.map +1 -0
  32. package/dist/chunk-EQ2Z3KMD.js +24 -0
  33. package/dist/chunk-EQ2Z3KMD.js.map +1 -0
  34. package/dist/chunk-FZ7O6DWI.js +305 -0
  35. package/dist/chunk-FZ7O6DWI.js.map +1 -0
  36. package/dist/chunk-ISLYFQWL.js +1270 -0
  37. package/dist/chunk-ISLYFQWL.js.map +1 -0
  38. package/dist/chunk-JJL74ZPK.js +68 -0
  39. package/dist/chunk-JJL74ZPK.js.map +1 -0
  40. package/dist/chunk-JKXAPSU4.js +24 -0
  41. package/dist/chunk-JKXAPSU4.js.map +1 -0
  42. package/dist/chunk-KU5M27ZC.js +24 -0
  43. package/dist/chunk-KU5M27ZC.js.map +1 -0
  44. package/dist/chunk-LSHHRDVR.js +34 -0
  45. package/dist/chunk-LSHHRDVR.js.map +1 -0
  46. package/dist/chunk-M43PGOQY.js +715 -0
  47. package/dist/chunk-M43PGOQY.js.map +1 -0
  48. package/dist/chunk-MEJAHXIO.js +150 -0
  49. package/dist/chunk-MEJAHXIO.js.map +1 -0
  50. package/dist/chunk-NUCGHWCF.js +101 -0
  51. package/dist/chunk-NUCGHWCF.js.map +1 -0
  52. package/dist/chunk-OK5HOCQI.js +845 -0
  53. package/dist/chunk-OK5HOCQI.js.map +1 -0
  54. package/dist/chunk-OROPGO65.js +13 -0
  55. package/dist/chunk-OROPGO65.js.map +1 -0
  56. package/dist/chunk-PPAS4SZR.js +176 -0
  57. package/dist/chunk-PPAS4SZR.js.map +1 -0
  58. package/dist/chunk-PPBWRKO2.js +171 -0
  59. package/dist/chunk-PPBWRKO2.js.map +1 -0
  60. package/dist/chunk-PZ5AY32C.js +10 -0
  61. package/dist/chunk-PZ5AY32C.js.map +1 -0
  62. package/dist/chunk-QO7LAQZH.js +321 -0
  63. package/dist/chunk-QO7LAQZH.js.map +1 -0
  64. package/dist/chunk-QVJ2HCAX.js +225 -0
  65. package/dist/chunk-QVJ2HCAX.js.map +1 -0
  66. package/dist/chunk-RIPHIRPP.js +68 -0
  67. package/dist/chunk-RIPHIRPP.js.map +1 -0
  68. package/dist/chunk-S27S42QY.js +134 -0
  69. package/dist/chunk-S27S42QY.js.map +1 -0
  70. package/dist/chunk-SBCVAC2Z.js +40 -0
  71. package/dist/chunk-SBCVAC2Z.js.map +1 -0
  72. package/dist/chunk-TFJ4MKPH.js +694 -0
  73. package/dist/chunk-TFJ4MKPH.js.map +1 -0
  74. package/dist/chunk-THX3SHYA.js +75 -0
  75. package/dist/chunk-THX3SHYA.js.map +1 -0
  76. package/dist/chunk-UGQSQO5B.js +222 -0
  77. package/dist/chunk-UGQSQO5B.js.map +1 -0
  78. package/dist/chunk-V2UNHGAP.js +26 -0
  79. package/dist/chunk-V2UNHGAP.js.map +1 -0
  80. package/dist/chunk-VGTPQXNQ.js +2790 -0
  81. package/dist/chunk-VGTPQXNQ.js.map +1 -0
  82. package/dist/chunk-VNIHXQ7W.js +194 -0
  83. package/dist/chunk-VNIHXQ7W.js.map +1 -0
  84. package/dist/chunk-WV272MPW.js +31 -0
  85. package/dist/chunk-WV272MPW.js.map +1 -0
  86. package/dist/chunk-X5KKBOUS.js +26 -0
  87. package/dist/chunk-X5KKBOUS.js.map +1 -0
  88. package/dist/chunk-XANPEOJC.js +17 -0
  89. package/dist/chunk-XANPEOJC.js.map +1 -0
  90. package/dist/chunk-XPVQIHAQ.js +83 -0
  91. package/dist/chunk-XPVQIHAQ.js.map +1 -0
  92. package/dist/chunk-ZCINJSS4.js +75 -0
  93. package/dist/chunk-ZCINJSS4.js.map +1 -0
  94. package/dist/community.d.ts +1425 -0
  95. package/dist/community.js +206 -0
  96. package/dist/community.js.map +1 -0
  97. package/dist/config-2GDU7PCK.js +32 -0
  98. package/dist/config-2GDU7PCK.js.map +1 -0
  99. package/dist/context-MNZ4QXPC.js +16 -0
  100. package/dist/context-MNZ4QXPC.js.map +1 -0
  101. package/dist/db-schema.d.ts +4 -0
  102. package/dist/db-schema.js +102 -0
  103. package/dist/db-schema.js.map +1 -0
  104. package/dist/db.d.ts +7 -0
  105. package/dist/db.js +117 -0
  106. package/dist/db.js.map +1 -0
  107. package/dist/digest-SY42GQSU.js +17 -0
  108. package/dist/digest-SY42GQSU.js.map +1 -0
  109. package/dist/errors-5OS3S2J3.js +22 -0
  110. package/dist/errors-5OS3S2J3.js.map +1 -0
  111. package/dist/host-OBOI4MJK.js +51 -0
  112. package/dist/host-OBOI4MJK.js.map +1 -0
  113. package/dist/i18n.d.ts +301 -0
  114. package/dist/i18n.js +68 -0
  115. package/dist/i18n.js.map +1 -0
  116. package/dist/index-B6-_vr_m.d.ts +590 -0
  117. package/dist/index-CY55LC0u.d.ts +4722 -0
  118. package/dist/index-CeiTvwbp.d.ts +168 -0
  119. package/dist/index-XwP1ET8b.d.ts +61 -0
  120. package/dist/index.d.ts +2037 -0
  121. package/dist/index.js +2205 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/job-log-VZXWQUDK.js +24 -0
  124. package/dist/job-log-VZXWQUDK.js.map +1 -0
  125. package/dist/jobs.d.ts +4 -0
  126. package/dist/jobs.js +76 -0
  127. package/dist/jobs.js.map +1 -0
  128. package/dist/logger-DqGaOU_j.d.ts +29 -0
  129. package/dist/logger-S7REWDNE.js +16 -0
  130. package/dist/logger-S7REWDNE.js.map +1 -0
  131. package/dist/media.d.ts +5 -0
  132. package/dist/media.js +41 -0
  133. package/dist/media.js.map +1 -0
  134. package/dist/mentions-2IHFVSHW.js +23 -0
  135. package/dist/mentions-2IHFVSHW.js.map +1 -0
  136. package/dist/mutes-EWAE5FZR.js +21 -0
  137. package/dist/mutes-EWAE5FZR.js.map +1 -0
  138. package/dist/notification-prefs-VPJDU7I6.js +21 -0
  139. package/dist/notification-prefs-VPJDU7I6.js.map +1 -0
  140. package/dist/observability.d.ts +156 -0
  141. package/dist/observability.js +32 -0
  142. package/dist/observability.js.map +1 -0
  143. package/dist/profanity-adapter-NU2JQSLX.js +12 -0
  144. package/dist/profanity-adapter-NU2JQSLX.js.map +1 -0
  145. package/dist/queue-XE5BC75T.js +14 -0
  146. package/dist/queue-XE5BC75T.js.map +1 -0
  147. package/dist/rate-limit.d.ts +99 -0
  148. package/dist/rate-limit.js +14 -0
  149. package/dist/rate-limit.js.map +1 -0
  150. package/dist/registry-XIXDEPVI.js +31 -0
  151. package/dist/registry-XIXDEPVI.js.map +1 -0
  152. package/dist/reputation-JRL2YQHM.js +11 -0
  153. package/dist/reputation-JRL2YQHM.js.map +1 -0
  154. package/dist/routes.d.ts +43 -0
  155. package/dist/routes.js +12 -0
  156. package/dist/routes.js.map +1 -0
  157. package/dist/scheduled-CIQM57HT.js +20 -0
  158. package/dist/scheduled-CIQM57HT.js.map +1 -0
  159. package/dist/seo.d.ts +410 -0
  160. package/dist/seo.js +44 -0
  161. package/dist/seo.js.map +1 -0
  162. package/dist/settings-FOBIESPB.js +17 -0
  163. package/dist/settings-FOBIESPB.js.map +1 -0
  164. package/dist/spam-adapter-XX3G737Z.js +12 -0
  165. package/dist/spam-adapter-XX3G737Z.js.map +1 -0
  166. package/dist/strings-VAE47B2C.js +29 -0
  167. package/dist/strings-VAE47B2C.js.map +1 -0
  168. package/dist/templates-IFVJMCJ6.js +12 -0
  169. package/dist/templates-IFVJMCJ6.js.map +1 -0
  170. package/dist/types-TlsbXS0T.d.ts +871 -0
  171. package/package.json +129 -0
@@ -0,0 +1,871 @@
1
+ /**
2
+ * Canonical principal type — "who is the actor on this operation".
3
+ *
4
+ * Both staff routes (which carry an authenticated `NpAuthUser`) and
5
+ * member routes (which carry only a `memberId`) share this single
6
+ * union. Used by the collection pipeline, plugin hooks (surfaced as
7
+ * `NpHookPrincipal` for historical reasons), and the community
8
+ * `principalCan()` resolver.
9
+ *
10
+ * Add a new variant only after auditing every `switch (principal.kind)`
11
+ * site — the exhaustive switches deliberately fail to compile when the
12
+ * union grows.
13
+ */
14
+ type NpPrincipal = {
15
+ kind: "staff";
16
+ user: NpAuthUser;
17
+ } | {
18
+ kind: "member";
19
+ memberId: string;
20
+ };
21
+
22
+ type NpUserRole = "admin" | "editor" | "moderator" | "author" | "viewer";
23
+ interface NpAuthUser {
24
+ id: string;
25
+ email: string;
26
+ name: string;
27
+ role: NpUserRole;
28
+ tokenVersion: number;
29
+ }
30
+ type NpAccessFunction = (args: {
31
+ user: NpAuthUser | null;
32
+ doc?: Record<string, unknown>;
33
+ data?: Record<string, unknown>;
34
+ }) => boolean | Promise<boolean>;
35
+ type NpFieldCondition = (data: Record<string, unknown>, siblingData: Record<string, unknown>) => boolean;
36
+ type NpFieldValidator = (value: unknown, args: {
37
+ data: Record<string, unknown>;
38
+ siblingData: Record<string, unknown>;
39
+ }) => string | true | Promise<string | true>;
40
+ type NpRichTextContent = Record<string, unknown>;
41
+ interface NpEditorConfig {
42
+ features?: string[];
43
+ }
44
+ interface NpFieldBase {
45
+ name: string;
46
+ label?: string;
47
+ required?: boolean;
48
+ defaultValue?: unknown;
49
+ hidden?: boolean;
50
+ admin?: {
51
+ description?: string;
52
+ placeholder?: string;
53
+ readOnly?: boolean;
54
+ condition?: NpFieldCondition;
55
+ width?: string;
56
+ /**
57
+ * Optional override for the admin field renderer. The default
58
+ * renderer dispatches on `type` (text → input, textarea →
59
+ * textarea, etc.); `kind` overrides that with a specialized
60
+ * widget.
61
+ * - `templatePicker` (Phase 11.3) replaces the input with a
62
+ * dropdown sourced from the active theme's
63
+ * `templates.{collection}` registry.
64
+ * - `title` renders a large borderless headline input that
65
+ * sits above the rest of the form (intended for the
66
+ * primary title of a document). The edit view skips the
67
+ * Card wrapper around it so the title flows naturally
68
+ * into the editor canvas underneath.
69
+ */
70
+ kind?: "templatePicker" | "title";
71
+ /**
72
+ * Where the field should land in the edit view's two-column
73
+ * layout. Mark publishing-related metadata (SEO, template
74
+ * choice, scheduling inputs) as `"sidebar"` so they group
75
+ * with Status / Slug in the sticky right column rather than
76
+ * competing with the primary editing surface.
77
+ *
78
+ * When unset, the legacy heuristic decides: `type: "date"`
79
+ * fields, fields with an explicit `admin.width`, and the
80
+ * well-known names `status` / `publishedAt` / `slug` all
81
+ * land in the sidebar; everything else goes to main. An
82
+ * explicit `"main"` overrides that heuristic — useful for
83
+ * surfacing a date input in the primary column.
84
+ */
85
+ position?: "main" | "sidebar";
86
+ };
87
+ validate?: NpFieldValidator;
88
+ }
89
+ interface NpTextField extends NpFieldBase {
90
+ type: "text";
91
+ minLength?: number;
92
+ maxLength?: number;
93
+ unique?: boolean;
94
+ }
95
+ interface NpTextareaField extends NpFieldBase {
96
+ type: "textarea";
97
+ minLength?: number;
98
+ maxLength?: number;
99
+ rows?: number;
100
+ }
101
+ interface NpNumberField extends NpFieldBase {
102
+ type: "number";
103
+ min?: number;
104
+ max?: number;
105
+ step?: number;
106
+ integerOnly?: boolean;
107
+ }
108
+ interface NpRichTextField extends NpFieldBase {
109
+ type: "richText";
110
+ editor?: NpEditorConfig;
111
+ }
112
+ interface NpBlocksField extends NpFieldBase {
113
+ type: "blocks";
114
+ allowedBlocks?: string[];
115
+ minRows?: number;
116
+ maxRows?: number;
117
+ }
118
+ interface NpCheckboxField extends NpFieldBase {
119
+ type: "checkbox";
120
+ defaultValue?: boolean;
121
+ }
122
+ interface NpDateField extends NpFieldBase {
123
+ type: "date";
124
+ pickerOptions?: {
125
+ format?: string;
126
+ includeTime?: boolean;
127
+ };
128
+ }
129
+ interface NpUploadField extends NpFieldBase {
130
+ type: "upload";
131
+ relationTo: string;
132
+ }
133
+ interface NpRelationshipField extends NpFieldBase {
134
+ type: "relationship";
135
+ relationTo: string | string[];
136
+ hasMany?: boolean;
137
+ filterOptions?: Record<string, unknown>;
138
+ }
139
+ interface NpSelectField extends NpFieldBase {
140
+ type: "select";
141
+ options: Array<{
142
+ label: string;
143
+ value: string;
144
+ }>;
145
+ hasMany?: boolean;
146
+ }
147
+ interface NpRadioField extends NpFieldBase {
148
+ type: "radio";
149
+ options: Array<{
150
+ label: string;
151
+ value: string;
152
+ }>;
153
+ }
154
+ interface NpEmailField extends NpFieldBase {
155
+ type: "email";
156
+ }
157
+ interface NpJsonField extends NpFieldBase {
158
+ type: "json";
159
+ }
160
+ interface NpArrayField extends NpFieldBase {
161
+ type: "array";
162
+ fields: NpFieldConfig[];
163
+ minRows?: number;
164
+ maxRows?: number;
165
+ }
166
+ interface NpGroupField extends NpFieldBase {
167
+ type: "group";
168
+ fields: NpFieldConfig[];
169
+ }
170
+ interface NpRowField {
171
+ type: "row";
172
+ fields: NpFieldConfig[];
173
+ }
174
+ interface NpCollapsibleField {
175
+ type: "collapsible";
176
+ label: string;
177
+ fields: NpFieldConfig[];
178
+ }
179
+ type NpFieldConfig = NpTextField | NpTextareaField | NpNumberField | NpRichTextField | NpBlocksField | NpCheckboxField | NpDateField | NpUploadField | NpRelationshipField | NpSelectField | NpRadioField | NpEmailField | NpJsonField | NpArrayField | NpGroupField | NpRowField | NpCollapsibleField;
180
+
181
+ type NpHookPrincipal = NpPrincipal;
182
+ type NpCollectionHook = (args: {
183
+ data: Record<string, unknown>;
184
+ /**
185
+ * Resolved staff session, or `null` when the actor is a member.
186
+ * Pre-9.7o this was always non-null because member writes
187
+ * skipped collection hooks entirely. Hooks that key off staff
188
+ * identity should now switch on `principal.kind` instead.
189
+ */
190
+ user: NpAuthUser | null;
191
+ /** Polymorphic actor — see `NpHookPrincipal`. */
192
+ principal: NpHookPrincipal;
193
+ collection: string;
194
+ originalDoc?: Record<string, unknown> | null;
195
+ }) => Record<string, unknown> | Promise<Record<string, unknown>>;
196
+ interface NpUploadConfig {
197
+ maxFileSize?: number;
198
+ allowedMimeTypes?: string[];
199
+ imageSizes?: NpImageSize[];
200
+ }
201
+ interface NpImageSize {
202
+ name: string;
203
+ width: number;
204
+ height?: number;
205
+ crop?: "center" | "top" | "bottom" | "left" | "right";
206
+ }
207
+ interface NpCollectionConfig {
208
+ slug: string;
209
+ labels: {
210
+ singular: string;
211
+ plural: string;
212
+ };
213
+ slugField?: boolean | {
214
+ useField?: string;
215
+ unique?: boolean;
216
+ };
217
+ /**
218
+ * Phase 12.1 — opt this collection into i18n. When set, the
219
+ * codegen adds a `locale` text column and a
220
+ * `translation_group_id` uuid column to the generated table.
221
+ * The slug uniqueness index becomes `(locale, slug)` so the
222
+ * same slug can appear in two locales. Fetching helpers
223
+ * (`findDocuments`, `getDoc`) accept a `locale` option;
224
+ * writes require a `locale` field (the pipeline rejects
225
+ * missing-locale writes with NpValidationError).
226
+ *
227
+ * Requires the top-level `i18n` config to also be set.
228
+ * Without it, `i18n: true` here errors at config validation
229
+ * time — the framework needs to know the locale enum to
230
+ * validate writes.
231
+ */
232
+ i18n?: boolean;
233
+ fields: NpFieldConfig[];
234
+ access?: {
235
+ create?: NpAccessFunction;
236
+ read?: NpAccessFunction;
237
+ update?: NpAccessFunction;
238
+ delete?: NpAccessFunction;
239
+ };
240
+ hooks?: {
241
+ beforeCreate?: NpCollectionHook[];
242
+ afterCreate?: NpCollectionHook[];
243
+ beforeUpdate?: NpCollectionHook[];
244
+ afterUpdate?: NpCollectionHook[];
245
+ beforeDelete?: NpCollectionHook[];
246
+ afterDelete?: NpCollectionHook[];
247
+ beforeRead?: NpCollectionHook[];
248
+ afterRead?: NpCollectionHook[];
249
+ };
250
+ versions?: {
251
+ drafts?: boolean | {
252
+ autosave?: boolean;
253
+ autosaveInterval?: number;
254
+ };
255
+ max?: number;
256
+ };
257
+ /**
258
+ * Community features opt-in per collection. Comments are off by
259
+ * default; flip `comments: true` to let members post comments
260
+ * underneath this collection's documents. Reactions ride on the
261
+ * comment surface — sites enable reactions by enabling comments;
262
+ * a per-collection reactions toggle isn't needed today.
263
+ *
264
+ * `memberWrite.create` (9.7a) lets logged-in members create
265
+ * documents in this collection without needing a staff role.
266
+ * `memberWrite.update` / `memberWrite.delete` (9.7b) extend the
267
+ * member-write surface with owner-only edit / delete (the row's
268
+ * `member_author_id` must match the caller). The staff
269
+ * `access.create` / `access.delete` functions are bypassed on
270
+ * the member path — gating is `assertNotBanned(memberId)` plus
271
+ * the opt-in flag plus the ownership check, not the staff
272
+ * access tree. Member-authored docs default to
273
+ * `_status = "published"` and members CANNOT change status via
274
+ * update; those transitions remain admin-side affordances
275
+ * (a configurable default-status / moderation gate lands in a
276
+ * follow-up).
277
+ */
278
+ community?: {
279
+ comments?: boolean;
280
+ memberWrite?: {
281
+ create?: boolean;
282
+ update?: boolean;
283
+ delete?: boolean;
284
+ /**
285
+ * Status that member-authored creates land in by default.
286
+ * Defaults to `"published"` (a member's thread is live as
287
+ * soon as it's submitted). Set to `"pending"` to require a
288
+ * mod to promote the row before it shows up on the public
289
+ * site — a flag-on-write moderation gate without writing a
290
+ * spam adapter. The spam adapter, if installed, can also
291
+ * downgrade an individual row to `pending` regardless of
292
+ * this default (`flag` verdict).
293
+ */
294
+ defaultStatus?: "published" | "pending";
295
+ };
296
+ };
297
+ /**
298
+ * SEO configuration. Phase 10 introduced this surface for the
299
+ * sitemap / RSS / OG metadata pipeline. The contract is
300
+ * opt-in: a collection appears in `/sitemap.xml` iff it
301
+ * declares `seo.urlPath`, which maps a document row to its
302
+ * public URL path (e.g. `(doc) => "/blog/" + doc.slug`).
303
+ * Collections without `seo.urlPath` are assumed to be admin-
304
+ * internal or rendered through a custom route the framework
305
+ * can't introspect.
306
+ */
307
+ seo?: {
308
+ /**
309
+ * Maps a document row to the public URL path the row is
310
+ * served at, or `null` to skip the row (e.g. a draft / a
311
+ * row whose URL is computed dynamically and shouldn't be
312
+ * indexed). Returned paths must start with `/`. The host
313
+ * comes from `SITE_URL` at sitemap-build time.
314
+ */
315
+ urlPath?: (doc: Record<string, unknown>) => string | null;
316
+ /**
317
+ * Hint for sitemap consumers about how often this
318
+ * collection's content changes. Optional — Google now
319
+ * largely ignores it but other crawlers still honor it.
320
+ */
321
+ changefreq?: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
322
+ /**
323
+ * Sitemap priority hint, 0.0–1.0. Optional, same caveat as
324
+ * changefreq.
325
+ */
326
+ priority?: number;
327
+ };
328
+ timestamps?: boolean;
329
+ admin?: {
330
+ listColumns?: string[];
331
+ defaultSort?: string;
332
+ group?: string;
333
+ hidden?: boolean;
334
+ description?: string;
335
+ components?: {
336
+ listView?: string;
337
+ editView?: string;
338
+ createView?: string;
339
+ };
340
+ /**
341
+ * Opts the collection's edit view into the "In navigation"
342
+ * side panel. Documents in this collection are addressable
343
+ * from the nav editor's `type: "page"` picker via the
344
+ * membership endpoint, so the operator can add/remove the
345
+ * doc from any nav location without leaving the page.
346
+ *
347
+ * Defaults to `false`. The reference `pages` collection in
348
+ * `apps/web` flips it on; sites with a `static-pages` or
349
+ * `landing-pages` collection that should also surface in nav
350
+ * can opt in here too.
351
+ */
352
+ navMembership?: boolean;
353
+ /**
354
+ * Lucide icon name for the admin sidebar entry. Defaults to
355
+ * `FileText` when unset or unrecognized. Examples:
356
+ * `"Newspaper"` for posts, `"FileStack"` for pages,
357
+ * `"FolderTree"` for categories, `"Tag"` for tags.
358
+ *
359
+ * Resolved client-side by `admin-shell.tsx` against a small
360
+ * lucide-react registry; unknown names fall back to the
361
+ * default so a typo can't break the sidebar render.
362
+ */
363
+ icon?: string;
364
+ };
365
+ upload?: NpUploadConfig;
366
+ }
367
+ interface NpBlockConfig {
368
+ slug: string;
369
+ labels: {
370
+ singular: string;
371
+ plural: string;
372
+ };
373
+ fields: NpFieldConfig[];
374
+ imageUrl?: string;
375
+ }
376
+ type NpBlockInstance = {
377
+ blockType: string;
378
+ [key: string]: unknown;
379
+ };
380
+ interface NpPluginConfig {
381
+ id: string;
382
+ name: string;
383
+ init?: (ctx: NpPluginContext) => void | Promise<void>;
384
+ }
385
+ /**
386
+ * Structural shape accepted by `loadPlugins()` for SDK-built plugins.
387
+ * Declared here rather than imported from `@nexpress/plugin-sdk` to avoid a
388
+ * dependency cycle (plugin-sdk already depends on core).
389
+ */
390
+ interface NpResolvedPluginLike {
391
+ manifest: {
392
+ id: string;
393
+ name: string;
394
+ version?: string;
395
+ description?: string;
396
+ capabilities: readonly string[];
397
+ };
398
+ hooks?: Record<string, unknown>;
399
+ routes?: ReadonlyArray<{
400
+ path: string;
401
+ method: string;
402
+ handler: unknown;
403
+ description?: string;
404
+ auth?: boolean;
405
+ }>;
406
+ /**
407
+ * Phase 12.5 — optional UI string bundles per locale. Keys
408
+ * are plugin-namespaced strings the plugin's own templates /
409
+ * routes / admin pages call `t()` against. The host merges
410
+ * every plugin's bundle into the global registry at boot;
411
+ * later plugins overwrite earlier ones on key collision so
412
+ * sites can layer overrides via plugin order.
413
+ */
414
+ i18n?: Record<string, Record<string, string>>;
415
+ /**
416
+ * Phase 14.5 — page templates the plugin contributes to the
417
+ * shared template registry. Same shape as a theme's
418
+ * `impl.templates`: keyed by collection slug, then by
419
+ * template id, with `{ label, description?, component }`
420
+ * values. The plugin host merges these at boot;
421
+ * `getThemeTemplateSummaries` returns plugin templates +
422
+ * theme templates as a union, with theme entries winning
423
+ * id collisions (the active theme is the site's design
424
+ * authority).
425
+ *
426
+ * templates: {
427
+ * pages: {
428
+ * docs: { label: "Documentation", component: DocsTemplate },
429
+ * },
430
+ * }
431
+ */
432
+ templates?: Record<string, Record<string, unknown>>;
433
+ /**
434
+ * Plugin page routes (#623). React-free shape — the framework
435
+ * narrows `component` to `ComponentType<NpRouteRenderProps>`
436
+ * at the dispatcher site. See
437
+ * `docs/design/plugin-routes.md` for the contract +
438
+ * precedence rules.
439
+ */
440
+ pageRoutes?: ReadonlyArray<{
441
+ pattern: string;
442
+ component: unknown;
443
+ metadata?: unknown;
444
+ surface?: "site" | "member";
445
+ locale?: "auto" | "none";
446
+ }>;
447
+ }
448
+ interface NpPluginContext {
449
+ addCollection: (config: NpCollectionConfig) => void;
450
+ addBlock: (config: NpBlockConfig) => void;
451
+ addHook: (collection: string, event: string, hook: NpCollectionHook) => void;
452
+ }
453
+ interface NpNavItem {
454
+ id: string;
455
+ label: string;
456
+ type: "link" | "collection" | "page";
457
+ url?: string;
458
+ collection?: string;
459
+ /**
460
+ * Set when `type === "page"` to record which collection the
461
+ * referenced doc lives in. Defaults to `"pages"` when absent so
462
+ * existing nav rows keep resolving against the reference page
463
+ * collection unchanged. The URL resolver walks the doc through
464
+ * the collection's `seo.urlPath` to produce the public path.
465
+ *
466
+ * The editor doesn't expose this as an editable field — the
467
+ * panel that adds the item knows its source collection and
468
+ * stamps it at write time.
469
+ */
470
+ collectionSlug?: string;
471
+ pageId?: string;
472
+ children?: NpNavItem[];
473
+ }
474
+ /**
475
+ * Phase 11.1 — theme manifest. Pure metadata, kept React-free
476
+ * so it can live in `@nexpress/core` (which is server-only and
477
+ * intentionally has no React peer). The full theme — shell,
478
+ * slots, templates with React component types — lives in
479
+ * `@nexpress/theme` via `defineTheme()`. The registry stores
480
+ * `NpRegisteredTheme` instances; `impl` is opaque to core but
481
+ * typed for consumers downstream.
482
+ */
483
+ interface NpThemeManifest {
484
+ id: string;
485
+ name: string;
486
+ version: string;
487
+ description?: string;
488
+ author?: {
489
+ name: string;
490
+ url?: string;
491
+ };
492
+ /** Optional minimum NexPress version this theme requires. */
493
+ nexpress?: {
494
+ minVersion?: string;
495
+ };
496
+ /**
497
+ * Phase F.1 (theme v0.2) — declared data-shape requirements.
498
+ *
499
+ * Themes whose components assume specific collection fields
500
+ * (e.g. magazine theme reads `posts.featured`) declare them
501
+ * here. Two consumers read this:
502
+ *
503
+ * 1. Admin theme switcher (this phase): compares against the
504
+ * site's registered collections at activation time and
505
+ * surfaces mismatches to the operator BEFORE they click
506
+ * "activate" — so they don't end up with a theme that
507
+ * silently renders fallbacks for missing fields.
508
+ * 2. `pnpm nexpress theme:install` (Phase F.8, deferred):
509
+ * reads this to AST-patch the operator's
510
+ * `src/collections/*.ts` files and run codegen + migrate.
511
+ *
512
+ * F.1 ships only the type + admin warning surface. The CLI
513
+ * patcher is its own phase.
514
+ */
515
+ requires?: {
516
+ collections?: Record<string, NpThemeCollectionRequirement>;
517
+ };
518
+ /**
519
+ * Phase F.3 (theme v0.2) — operator-tunable theme options.
520
+ *
521
+ * A Zod schema describing settings the admin should expose as
522
+ * a form. The framework generates the form fields from the
523
+ * schema (no per-theme admin UI code), persists submissions in
524
+ * `np_settings` keyed by `theme.settings:<themeId>`, and
525
+ * exposes the parsed value to theme components via
526
+ * `getThemeSettings()`.
527
+ *
528
+ * Supported field types in v0.2:
529
+ * - z.string() / z.string().url() / z.string().regex(...)
530
+ * - z.number().int().min().max()
531
+ * - z.boolean()
532
+ * - z.enum([...])
533
+ * - z.array(z.object({...}))
534
+ * - z.object({...})
535
+ *
536
+ * Use `.default(value)` for initial form values and
537
+ * `.describe("Help text")` for the field label/description
538
+ * the admin auto-form picks up.
539
+ *
540
+ * Typed as `unknown` here so `@nexpress/core` doesn't have to
541
+ * re-export Zod type unions through every public surface;
542
+ * theme authors writing `defineTheme({ manifest: { ... } })`
543
+ * still get the proper Zod typing because they construct the
544
+ * schema with `z.object(...)` themselves. The framework
545
+ * narrows back to `ZodTypeAny` at the call site that runs
546
+ * introspection / validation.
547
+ */
548
+ settingsSchema?: unknown;
549
+ /**
550
+ * v0.3 (D) — settings schema version, used by the migration
551
+ * pipeline to detect when stored settings need upgrading.
552
+ *
553
+ * Theme authors bump this whenever `settingsSchema` changes
554
+ * shape in a non-additive way (renaming a field, removing one,
555
+ * tightening a default). Adding a NEW optional field is
556
+ * compatible without bumping — Zod fills the missing key with
557
+ * the field's default on parse.
558
+ *
559
+ * The framework treats absent / undefined as `1` (the v0.2
560
+ * baseline). Themes that never bump stay forever at v1, no
561
+ * migration ever runs.
562
+ */
563
+ settingsVersion?: number;
564
+ /**
565
+ * v0.3 (D) — migration function that brings a value persisted
566
+ * under an older `settingsVersion` up to the current shape.
567
+ *
568
+ * Called on read when stored version < `settingsVersion`. The
569
+ * function receives the OLD value (whatever shape v(N-1) had)
570
+ * and the version it came from (so multi-step migrations can
571
+ * branch). Returns a value that matches the CURRENT
572
+ * `settingsSchema`. The framework re-parses the result and
573
+ * falls back to schema defaults if the migration's output
574
+ * still doesn't validate (defensive — a buggy migrate fn
575
+ * shouldn't blow up the public site).
576
+ *
577
+ * The framework persists the migrated value back on the
578
+ * operator's NEXT save through the admin form. Read paths
579
+ * don't auto-write; the migration is recomputed on each read
580
+ * until the operator triggers a save. That keeps read paths
581
+ * pure (matches every other cached read in the framework).
582
+ *
583
+ * Example for a `accent` → `accentColor` rename at v2:
584
+ *
585
+ * ```ts
586
+ * defineTheme({
587
+ * manifest: {
588
+ * settingsSchema: z.object({
589
+ * accentColor: z.string().regex(...).optional(),
590
+ * ...
591
+ * }),
592
+ * settingsVersion: 2,
593
+ * settingsMigrate: (old, from) => {
594
+ * if (from === 1) {
595
+ * const o = old as { accent?: string };
596
+ * return { ...o, accentColor: o.accent };
597
+ * }
598
+ * return old;
599
+ * },
600
+ * }
601
+ * })
602
+ * ```
603
+ */
604
+ settingsMigrate?: (old: unknown, fromVersion: number) => unknown;
605
+ }
606
+ /**
607
+ * One collection's worth of theme requirements. The collection
608
+ * may exist (admin checks fields) or not (admin flags as missing
609
+ * — the CLI in F.8 will create it if `createIfAbsent` is set).
610
+ */
611
+ interface NpThemeCollectionRequirement {
612
+ fields?: Record<string, NpThemeFieldRequirement>;
613
+ /** True → CLI in F.8 creates this collection if absent.
614
+ * Admin still warns at activation; the operator must run the
615
+ * CLI to actually create it. */
616
+ createIfAbsent?: boolean;
617
+ }
618
+ /**
619
+ * One field's requirement. The `type` matches an `NpFieldConfig`
620
+ * variant's `type` string exactly so the activation check can
621
+ * compare without translation.
622
+ */
623
+ interface NpThemeFieldRequirement {
624
+ type: "text" | "textarea" | "richText" | "number" | "checkbox" | "date" | "select" | "upload" | "relationship" | "blocks";
625
+ /** For `relationship` — the collection slug it points to. */
626
+ relationTo?: string | string[];
627
+ /** For `relationship` / `select` — accepts list values. */
628
+ hasMany?: boolean;
629
+ required?: boolean;
630
+ /**
631
+ * Default `true`. Set `false` for "nice to have, theme degrades
632
+ * gracefully without it" — admin warning shows but at lower
633
+ * severity, and a future F.8 may treat it as opt-in patch.
634
+ */
635
+ hard?: boolean;
636
+ }
637
+ interface NpRegisteredTheme {
638
+ manifest: NpThemeManifest;
639
+ /**
640
+ * The theme's runtime implementation — shell component,
641
+ * slot components, page templates, default tokens.
642
+ * `@nexpress/theme` types this; core treats it as opaque so
643
+ * the React peer dependency stays out of this package.
644
+ */
645
+ impl: unknown;
646
+ }
647
+ interface NpI18nConfig {
648
+ /**
649
+ * Locales this site supports. Order matters only insofar as
650
+ * the first locale becomes the default when `defaultLocale`
651
+ * isn't explicitly set. Locale strings are passed through to
652
+ * BCP-47 consumers (HTML `lang` attribute, hreflang) so
653
+ * conventional codes are recommended (`en`, `en-US`, `ko`,
654
+ * `pt-BR`).
655
+ */
656
+ locales: string[];
657
+ /**
658
+ * Locale used when the caller doesn't specify one — drives
659
+ * default writes and fallback reads. Must appear in `locales`.
660
+ */
661
+ defaultLocale: string;
662
+ }
663
+ interface NpConfig {
664
+ site: {
665
+ name: string;
666
+ url: string;
667
+ };
668
+ db: {
669
+ connectionString: string;
670
+ pool?: {
671
+ max?: number;
672
+ };
673
+ };
674
+ storage?: {
675
+ adapter: "local" | "s3";
676
+ local?: {
677
+ directory: string;
678
+ baseUrl: string;
679
+ };
680
+ s3?: {
681
+ bucket: string;
682
+ region: string;
683
+ endpoint?: string;
684
+ };
685
+ };
686
+ collections: NpCollectionConfig[];
687
+ blocks?: NpBlockConfig[];
688
+ editor?: NpEditorConfig;
689
+ /**
690
+ * Phase 11.1 — multi-theme registry. Sites declare every
691
+ * theme they want available; admins switch between them
692
+ * via the settings UI without rebuilding. The first theme
693
+ * in the array is the default-active until an admin sets
694
+ * a different one (`np_settings.activeTheme`).
695
+ */
696
+ themes?: NpRegisteredTheme[];
697
+ /**
698
+ * Phase 12.1 — i18n config. Sites that want multi-language
699
+ * content declare every locale they intend to support here.
700
+ * Per-collection opt-in via `defineCollection({ i18n: true })`
701
+ * is required: only collections that declare `i18n` get the
702
+ * `locale` / `translation_group_id` columns codegen'd onto
703
+ * their generated table. Sites with no i18n config (or that
704
+ * opt no collections in) keep the existing single-locale
705
+ * shape — i18n is purely additive.
706
+ *
707
+ * i18n: { locales: ["en", "ko", "ja"], defaultLocale: "en" }
708
+ *
709
+ * `defaultLocale` is what new docs land in when the caller
710
+ * doesn't pass an explicit locale, and what the framework
711
+ * falls back to when a translation is missing for a requested
712
+ * locale (the public site renders a 404 only when the doc
713
+ * doesn't exist in any locale).
714
+ */
715
+ i18n?: NpI18nConfig;
716
+ images?: {
717
+ sizes?: NpImageSize[];
718
+ format?: "webp" | "avif" | "jpeg" | "png";
719
+ quality?: number;
720
+ };
721
+ auth?: {
722
+ secret: string;
723
+ tokenExpiration?: number;
724
+ refreshTokenExpiration?: number;
725
+ maxLoginAttempts?: number;
726
+ lockoutDuration?: number;
727
+ };
728
+ plugins?: Array<NpPluginConfig | NpResolvedPluginLike>;
729
+ typescript?: {
730
+ outputFile?: string;
731
+ };
732
+ /**
733
+ * Phase 23.5 — operational thresholds and policies for the job
734
+ * queue. Currently only carries the stuck-job thresholds the
735
+ * admin Jobs widget compares against; future entries land
736
+ * additively.
737
+ */
738
+ jobs?: {
739
+ /**
740
+ * Per-state count thresholds for the admin stuck-job widget.
741
+ * When the live + archive UNION count for a state exceeds the
742
+ * configured value the widget shows a warning indicator. Unset
743
+ * values fall back to sensible defaults applied by the widget
744
+ * itself (currently `failed: 10`, `expired: 50`).
745
+ */
746
+ stuckThreshold?: {
747
+ failed?: number;
748
+ expired?: number;
749
+ };
750
+ };
751
+ }
752
+ type NpJobType = "content:afterSave" | "content:afterDelete" | "content:publishScheduled" | "media:processImage" | "media:cleanup" | "plugin:scheduledTask" | "system:revisionPrune" | "system:sessionCleanup" | "system:jobLogPrune" | "auth:sendPasswordReset" | "members:sendVerifyEmail" | "members:sendPasswordReset" | "notifications:sendDigest";
753
+ /**
754
+ * System-level filters that aren't part of any collection's
755
+ * document shape but still belong on the `where` clause: tenant
756
+ * scoping, visibility gating, locale narrowing. Kept separate
757
+ * from the document type so `Partial<T>` can stay tight while
758
+ * advanced callers (admin queries, bulk exports) can pass these
759
+ * escape-hatch tokens.
760
+ */
761
+ interface NpFindWhereSystemTokens {
762
+ /**
763
+ * Multi-site scoping. Defaults to the resolved current site.
764
+ * Pass `"*"` to query across every site (admin / migration
765
+ * use only — leaks cross-site rows).
766
+ */
767
+ siteId?: string;
768
+ /**
769
+ * Visibility gate. Anonymous traffic is auto-restricted to
770
+ * `"public"`. Pass `"*"` to bypass (the pipeline drops the
771
+ * filter when a user is also passed).
772
+ */
773
+ visibility?: "public" | "private" | "*";
774
+ /**
775
+ * `where: { locale: "ko" }` is equivalent to the top-level
776
+ * `locale` option. Listed here so a typed where clause can
777
+ * still pass it without the document type having to declare
778
+ * a `locale` field (only i18n-enabled collections do).
779
+ */
780
+ locale?: string;
781
+ }
782
+ /**
783
+ * Strip `null` and unwrap arrays so a hasMany field like
784
+ * `categories: string[] | null` reads as a `string` for the
785
+ * single-target filter case.
786
+ */
787
+ type NpFindWhereUnwrap<V> = V extends (infer U)[] | null ? U : V extends (infer U)[] ? U : V extends infer U | null ? U : V;
788
+ /**
789
+ * The accepted value shape for a single where field. Either the
790
+ * unwrapped scalar (single match) or an array (IN match). Array
791
+ * with zero elements short-circuits the query to no rows; the
792
+ * pipeline guards against the SQL syntax error this would
793
+ * otherwise produce.
794
+ */
795
+ type NpFindWhereValue<V> = NpFindWhereUnwrap<V> | NpFindWhereUnwrap<V>[];
796
+ /**
797
+ * Per-row filter. With the default `T = Record<string, unknown>`,
798
+ * any keys are allowed (back-compat). With a typed `T` (the
799
+ * generated wrapper functions pass their `${Pascal}Document`
800
+ * here), only document fields plus the system tokens above are
801
+ * accepted — typos against field names become compile errors.
802
+ *
803
+ * Each field accepts a single value (matched with `=`) or an
804
+ * array (matched with `IN (...)`). For hasMany relationships
805
+ * (where the document's field type is `string[] | null`), the
806
+ * single-value form is the common case — "posts in this one
807
+ * category" — and the array form picks up the `OR` semantics
808
+ * across multiple targets — "posts in any of these categories".
809
+ */
810
+ type NpFindWhere<T extends object = Record<string, unknown>> = {
811
+ [K in keyof T]?: NpFindWhereValue<T[K]>;
812
+ } & {
813
+ [K in keyof NpFindWhereSystemTokens]?: NpFindWhereSystemTokens[K];
814
+ };
815
+ interface NpFindOptions<T extends object = Record<string, unknown>> {
816
+ page?: number;
817
+ limit?: number;
818
+ sort?: string;
819
+ search?: string;
820
+ where?: NpFindWhere<T>;
821
+ /**
822
+ * Phase 12.1 — restrict the result set to one locale on
823
+ * i18n-enabled collections. Equivalent to passing
824
+ * `where: { locale }`, but kept top-level for ergonomics
825
+ * (callers don't have to know it's a column). Ignored on
826
+ * non-i18n collections (no `locale` column to match).
827
+ */
828
+ locale?: string;
829
+ }
830
+ interface NpFindResult<T = Record<string, unknown>> {
831
+ docs: T[];
832
+ totalDocs: number;
833
+ totalPages: number;
834
+ page: number;
835
+ limit: number;
836
+ hasNextPage: boolean;
837
+ hasPrevPage: boolean;
838
+ }
839
+ /**
840
+ * Document lifecycle status. `pending` (Phase 9.7c) is a moderation
841
+ * holding pen for member-authored docs that haven't cleared review
842
+ * — flagged by the spam adapter or sent there because the
843
+ * collection set `community.memberWrite.defaultStatus = "pending"`.
844
+ * Public listings filter to `published`, so pending rows are
845
+ * invisible to anonymous and non-staff members until a mod
846
+ * promotes them.
847
+ */
848
+ type NpDocumentStatus = "draft" | "scheduled" | "published" | "archived" | "pending";
849
+ interface NpSaveOptions {
850
+ status?: NpDocumentStatus;
851
+ }
852
+ interface NpSaveResult {
853
+ doc: Record<string, unknown>;
854
+ operation: "create" | "update";
855
+ }
856
+ /**
857
+ * Numeric ranking of staff roles, retained for the few non-capability
858
+ * call sites that still need to compare role rank — chiefly
859
+ * `hasRoleOnSite()` in `sites/memberships.ts`, which evaluates a
860
+ * per-site membership row's role against the user's. `moderator`
861
+ * shares author-rank because the two are parallel tracks
862
+ * (community-mod vs. content-author authority); the rank is meaningful
863
+ * only on the content-authoring axis.
864
+ *
865
+ * For staff-user authorization, use `can(user, capability)` from
866
+ * `auth/capabilities.ts` (#273) — this hierarchy is no longer the
867
+ * primary check.
868
+ */
869
+ declare const ROLE_HIERARCHY: Record<NpUserRole, number>;
870
+
871
+ export { type NpFieldCondition as A, type NpFieldValidator as B, type NpFindWhere as C, type NpFindWhereSystemTokens as D, type NpGroupField as E, type NpPrincipal as F, type NpI18nConfig as G, type NpImageSize as H, type NpJobType as I, type NpJsonField as J, type NpNumberField as K, type NpPluginContext as L, type NpRadioField as M, type NpRichTextContent as N, type NpRelationshipField as O, type NpResolvedPluginLike as P, type NpRichTextField as Q, type NpRowField as R, type NpSelectField as S, type NpTextField as T, type NpTextareaField as U, type NpThemeCollectionRequirement as V, type NpUploadConfig as W, type NpUploadField as X, ROLE_HIERARCHY as Y, type NpNavItem as a, type NpBlockInstance as b, type NpConfig as c, type NpCollectionConfig as d, type NpAuthUser as e, type NpSaveOptions as f, type NpSaveResult as g, type NpFindOptions as h, type NpFindResult as i, type NpFieldConfig as j, type NpRegisteredTheme as k, type NpThemeFieldRequirement as l, type NpThemeManifest as m, type NpUserRole as n, type NpPluginConfig as o, type NpAccessFunction as p, type NpArrayField as q, type NpBlockConfig as r, type NpBlocksField as s, type NpCheckboxField as t, type NpCollapsibleField as u, type NpCollectionHook as v, type NpDateField as w, type NpDocumentStatus as x, type NpEditorConfig as y, type NpEmailField as z };