@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,2790 @@
1
+ import {
2
+ getI18nConfig
3
+ } from "./chunk-4ZLMEKFX.js";
4
+ import {
5
+ NP_DEFAULT_SITE_ID,
6
+ getCollectionConfig,
7
+ getCollectionRegistration,
8
+ getCollectionTable
9
+ } from "./chunk-FZ7O6DWI.js";
10
+ import {
11
+ getCurrentSiteId
12
+ } from "./chunk-SBCVAC2Z.js";
13
+ import {
14
+ NpError,
15
+ NpForbiddenError,
16
+ NpNotFoundError,
17
+ NpValidationError
18
+ } from "./chunk-ZCINJSS4.js";
19
+ import {
20
+ reportError
21
+ } from "./chunk-WV272MPW.js";
22
+ import {
23
+ deleteMedia,
24
+ getMediaById,
25
+ getStorageAdapter,
26
+ listMedia,
27
+ uploadMedia
28
+ } from "./chunk-473S4TER.js";
29
+ import {
30
+ enqueueJob
31
+ } from "./chunk-V2UNHGAP.js";
32
+ import {
33
+ getLogger,
34
+ getScopedLogger
35
+ } from "./chunk-JJL74ZPK.js";
36
+ import {
37
+ getDb
38
+ } from "./chunk-XANPEOJC.js";
39
+ import {
40
+ NP_GLOBAL_PLUGIN_SITE_ID,
41
+ npComments,
42
+ npMediaRefs,
43
+ npPluginStorage,
44
+ npPlugins,
45
+ npReactions,
46
+ npReports,
47
+ npRevisions,
48
+ npSettings,
49
+ npSlugHistory
50
+ } from "./chunk-M43PGOQY.js";
51
+
52
+ // src/plugins/context.ts
53
+ import { and, eq as eq2, gt, isNull, like, or } from "drizzle-orm";
54
+
55
+ // src/collections/pipeline.ts
56
+ import { randomUUID } from "crypto";
57
+ import { asc, count, desc, eq, inArray, sql as sql2 } from "drizzle-orm";
58
+
59
+ // src/collections/slug.ts
60
+ function slugify(value) {
61
+ return value.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").normalize("NFC").replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, 96);
62
+ }
63
+ function applySlugField(config, data, originalDoc) {
64
+ if (!config.slugField) return;
65
+ const existingSlug = typeof data.slug === "string" ? data.slug.trim() : "";
66
+ if (existingSlug.length > 0) {
67
+ data.slug = slugify(existingSlug) || existingSlug;
68
+ return;
69
+ }
70
+ if (originalDoc && typeof originalDoc.slug === "string" && originalDoc.slug.length > 0) {
71
+ data.slug = originalDoc.slug;
72
+ return;
73
+ }
74
+ const useField = typeof config.slugField === "object" && config.slugField.useField ? config.slugField.useField : "title";
75
+ const source = data[useField];
76
+ const candidate = typeof source === "string" ? slugify(source) : "";
77
+ if (candidate.length === 0) {
78
+ throw new NpValidationError("Slug generation failed", [
79
+ {
80
+ field: "slug",
81
+ message: `Cannot derive a slug \u2014 provide "slug" or a non-empty "${useField}".`
82
+ }
83
+ ]);
84
+ }
85
+ data.slug = candidate;
86
+ }
87
+
88
+ // src/collections/validation.ts
89
+ import { z } from "zod";
90
+ function buildZodSchema(fields) {
91
+ const shape = {};
92
+ for (const field of fields) {
93
+ if (field.type === "row" || field.type === "collapsible") {
94
+ Object.assign(shape, buildZodSchema(field.fields).shape);
95
+ continue;
96
+ }
97
+ if (field.type === "group") {
98
+ const schema = buildZodSchema(field.fields);
99
+ shape[field.name] = applyOptionality(schema, field.required);
100
+ continue;
101
+ }
102
+ shape[field.name] = applyOptionality(buildFieldSchema(field), field.required);
103
+ }
104
+ return z.object(shape);
105
+ }
106
+ function getCollectionZodSchema(config) {
107
+ const base = buildZodSchema(config.fields).extend({
108
+ // Phase 21.17 — per-doc visibility flag. Optional on writes;
109
+ // the pipeline lets the column default to "public" when the
110
+ // caller doesn't specify. Allowed values are the same
111
+ // codegen enum from `getBaseColumns`.
112
+ visibility: z.enum(["public", "private"]).optional()
113
+ });
114
+ if (config.i18n) {
115
+ return base.extend({
116
+ locale: z.string().min(1).optional(),
117
+ translationGroupId: z.string().uuid().optional()
118
+ });
119
+ }
120
+ return base;
121
+ }
122
+ function buildFieldSchema(field) {
123
+ switch (field.type) {
124
+ case "text": {
125
+ let schema = z.string();
126
+ if (field.minLength !== void 0) schema = schema.min(field.minLength);
127
+ if (field.maxLength !== void 0) schema = schema.max(field.maxLength);
128
+ return schema;
129
+ }
130
+ case "textarea": {
131
+ let schema = z.string();
132
+ if (field.minLength !== void 0) schema = schema.min(field.minLength);
133
+ if (field.maxLength !== void 0) schema = schema.max(field.maxLength);
134
+ return schema;
135
+ }
136
+ case "email":
137
+ return z.string().email();
138
+ case "number": {
139
+ let schema = z.number();
140
+ if (field.integerOnly) schema = schema.int();
141
+ if (field.min !== void 0) schema = schema.min(field.min);
142
+ if (field.max !== void 0) schema = schema.max(field.max);
143
+ return schema;
144
+ }
145
+ case "checkbox":
146
+ return z.boolean();
147
+ case "select":
148
+ return createEnumSchema(field.options.map((option) => option.value));
149
+ case "radio":
150
+ return createEnumSchema(field.options.map((option) => option.value));
151
+ case "relationship":
152
+ return field.hasMany ? z.array(z.string().uuid()) : z.string().uuid();
153
+ case "upload":
154
+ return z.string().uuid();
155
+ case "date":
156
+ return z.coerce.date();
157
+ case "richText":
158
+ case "blocks":
159
+ case "json":
160
+ return z.unknown();
161
+ case "array": {
162
+ let schema = z.array(buildZodSchema(field.fields));
163
+ if (field.minRows !== void 0) schema = schema.min(field.minRows);
164
+ if (field.maxRows !== void 0) schema = schema.max(field.maxRows);
165
+ return schema;
166
+ }
167
+ default:
168
+ return z.unknown();
169
+ }
170
+ }
171
+ function applyOptionality(schema, required) {
172
+ return required ? schema : schema.optional().nullable();
173
+ }
174
+ function createEnumSchema(values) {
175
+ const [first, ...rest] = values;
176
+ if (!first) {
177
+ return z.string();
178
+ }
179
+ return z.enum([first, ...rest]);
180
+ }
181
+
182
+ // src/collections/search.ts
183
+ import { sql } from "drizzle-orm";
184
+ function buildSearchVector(config, data) {
185
+ const parts = [];
186
+ for (const field of config.fields) {
187
+ if (field.type === "text" || field.type === "textarea") {
188
+ const value = data[field.name];
189
+ if (typeof value === "string") parts.push(value);
190
+ }
191
+ if (field.type === "richText") {
192
+ const value = data[field.name];
193
+ if (value) parts.push(extractPlainText(value));
194
+ }
195
+ }
196
+ return parts.join(" ");
197
+ }
198
+ var TITLE_LIKE_NAMES = /* @__PURE__ */ new Set(["title", "name"]);
199
+ function buildSearchVectorParts(config, data) {
200
+ const parts = { a: "", b: "", c: "", d: "" };
201
+ const append = (bucket, value) => {
202
+ parts[bucket] = parts[bucket] ? `${parts[bucket]} ${value}` : value;
203
+ };
204
+ for (const field of config.fields) {
205
+ if (field.type === "text" || field.type === "textarea" || field.type === "email") {
206
+ const value = data[field.name];
207
+ if (typeof value !== "string" || value.length === 0) continue;
208
+ if (TITLE_LIKE_NAMES.has(field.name)) {
209
+ append("a", value);
210
+ } else {
211
+ append("b", value);
212
+ }
213
+ }
214
+ if (field.type === "richText") {
215
+ const value = data[field.name];
216
+ if (!value) continue;
217
+ const text = extractPlainText(value);
218
+ if (text.length > 0) append("c", text);
219
+ }
220
+ }
221
+ return parts;
222
+ }
223
+ function buildWeightedSearchVectorSql(config, data) {
224
+ const parts = buildSearchVectorParts(config, data);
225
+ const chunks = [];
226
+ if (parts.a)
227
+ chunks.push(sql`setweight(to_tsvector('english', ${parts.a}), 'A')`);
228
+ if (parts.b)
229
+ chunks.push(sql`setweight(to_tsvector('english', ${parts.b}), 'B')`);
230
+ if (parts.c)
231
+ chunks.push(sql`setweight(to_tsvector('english', ${parts.c}), 'C')`);
232
+ if (parts.d)
233
+ chunks.push(sql`setweight(to_tsvector('english', ${parts.d}), 'D')`);
234
+ if (chunks.length === 0) {
235
+ return sql`''::tsvector`;
236
+ }
237
+ if (chunks.length === 1) {
238
+ return chunks[0];
239
+ }
240
+ return sql.join(chunks, sql` || `);
241
+ }
242
+ function extractPlainText(content) {
243
+ if (!content || typeof content !== "object") return "";
244
+ const root = content.root;
245
+ if (!root?.children) return "";
246
+ const parts = [];
247
+ walkNodes(root.children, parts);
248
+ return parts.join(" ");
249
+ }
250
+ function walkNodes(nodes, parts) {
251
+ for (const node of nodes) {
252
+ if (!node || typeof node !== "object") continue;
253
+ const n = node;
254
+ if (typeof n.text === "string") {
255
+ parts.push(n.text);
256
+ }
257
+ if (Array.isArray(n.children)) {
258
+ walkNodes(n.children, parts);
259
+ }
260
+ }
261
+ }
262
+
263
+ // src/collections/pipeline.ts
264
+ function actorUserOrNull(actor) {
265
+ return actor.kind === "staff" ? actor.user : null;
266
+ }
267
+ function actorUserId(actor) {
268
+ return actor.kind === "staff" ? actor.user.id : null;
269
+ }
270
+ function actorPrincipal(actor) {
271
+ switch (actor.kind) {
272
+ case "staff":
273
+ return { kind: "staff", user: actor.user };
274
+ case "member":
275
+ return { kind: "member", memberId: actor.memberId };
276
+ default: {
277
+ const _exhaustive = actor;
278
+ void _exhaustive;
279
+ throw new Error("actorPrincipal: unhandled SaveActor kind");
280
+ }
281
+ }
282
+ }
283
+ async function runPostCommit(label, context, fn) {
284
+ try {
285
+ await fn();
286
+ } catch (err) {
287
+ const { getLogger: getLogger2 } = await import("./logger-S7REWDNE.js");
288
+ getLogger2().error(
289
+ `post-commit ${label} failed \u2014 document persisted, follow-up skipped`,
290
+ {
291
+ collection: context.collection,
292
+ documentId: context.documentId,
293
+ operation: context.operation,
294
+ label,
295
+ error: err instanceof Error ? err.message : String(err),
296
+ stack: err instanceof Error ? err.stack : void 0
297
+ }
298
+ );
299
+ }
300
+ }
301
+ async function saveDocument(collection, docId, data, user, options) {
302
+ return saveDocumentImpl(collection, docId, data, { kind: "staff", user }, options);
303
+ }
304
+ async function updateMemberDocument(collection, docId, data, memberId, options) {
305
+ const memberOptions = { ...options ?? {} };
306
+ delete memberOptions.status;
307
+ const config = getCollectionConfig(collection);
308
+ if (!config.community?.memberWrite?.update) {
309
+ throw new NpForbiddenError(collection, "update");
310
+ }
311
+ const table = getCollectionTable(collection);
312
+ const dbForGate = getDb();
313
+ const originalDoc = await getDocumentByIdInternal(dbForGate, table, collection, docId);
314
+ if (!originalDoc) {
315
+ throw new NpNotFoundError(collection, docId);
316
+ }
317
+ const authorId = originalDoc.memberAuthorId ?? null;
318
+ if (authorId !== memberId) {
319
+ throw new NpForbiddenError(collection, "update");
320
+ }
321
+ const { assertNotBanned } = await import("./can-YLUHRJAB.js");
322
+ await assertNotBanned(memberId);
323
+ const moderation = await runMemberDocModeration({
324
+ collection,
325
+ data,
326
+ memberId,
327
+ targetId: docId
328
+ });
329
+ if (moderation.flaggedBy.length > 0) {
330
+ memberOptions.status = "pending";
331
+ }
332
+ const result = await saveDocumentImpl(
333
+ collection,
334
+ docId,
335
+ data,
336
+ { kind: "member", memberId },
337
+ memberOptions
338
+ );
339
+ const { recordAuditEvent } = await import("./audit-54XLVCWD.js");
340
+ await recordAuditEvent({
341
+ actor: { kind: "member", memberId },
342
+ action: moderation.flaggedBy.length > 0 ? "document.flag" : "document.update",
343
+ targetType: collection,
344
+ targetId: docId,
345
+ payload: {
346
+ collectionSlug: collection,
347
+ event: "update",
348
+ ...moderation.flaggedBy.length > 0 ? { sources: moderation.flaggedBy } : {},
349
+ ...moderation.profanityVerdict ? { profanityVerdict: moderation.profanityVerdict } : {},
350
+ ...moderation.spamVerdict ? { spamVerdict: moderation.spamVerdict } : {}
351
+ }
352
+ });
353
+ const resultStatus = result.doc.status;
354
+ if (resultStatus === "published") {
355
+ const { extractMentionHandlesFromDocData, fanOutMentionNotifications } = await import("./mentions-2IHFVSHW.js");
356
+ const previousHandles = new Set(extractMentionHandlesFromDocData(originalDoc));
357
+ await fanOutMentionNotifications({
358
+ actorMemberId: memberId,
359
+ kind: "document.mention",
360
+ data,
361
+ previousHandles,
362
+ payload: {
363
+ collectionSlug: collection,
364
+ documentId: docId
365
+ }
366
+ });
367
+ }
368
+ return result;
369
+ }
370
+ async function createMemberDocument(collection, data, memberId, options) {
371
+ const config = getCollectionConfig(collection);
372
+ if (!config.community?.memberWrite?.create) {
373
+ throw new NpForbiddenError(collection, "create");
374
+ }
375
+ const { assertNotBanned } = await import("./can-YLUHRJAB.js");
376
+ await assertNotBanned(memberId);
377
+ const defaultStatus = config.community?.memberWrite?.defaultStatus === "pending" ? "pending" : "published";
378
+ const moderation = await runMemberDocModeration({
379
+ collection,
380
+ data,
381
+ memberId,
382
+ targetId: ""
383
+ });
384
+ const flaggedBy = moderation.flaggedBy;
385
+ const spamStatus = flaggedBy.length > 0 ? "pending" : defaultStatus;
386
+ const memberOptions = { ...options ?? {}, status: spamStatus };
387
+ const result = await saveDocumentImpl(
388
+ collection,
389
+ null,
390
+ data,
391
+ { kind: "member", memberId },
392
+ memberOptions
393
+ );
394
+ const { applyReputation } = await import("./reputation-JRL2YQHM.js");
395
+ const { recordAuditEvent } = await import("./audit-54XLVCWD.js");
396
+ const documentId = getRecordId(result.doc);
397
+ await recordAuditEvent({
398
+ actor: { kind: "member", memberId },
399
+ action: flaggedBy.length > 0 ? "document.flag" : "document.create",
400
+ targetType: collection,
401
+ targetId: documentId,
402
+ payload: {
403
+ collectionSlug: collection,
404
+ event: "create",
405
+ ...flaggedBy.length > 0 ? { sources: flaggedBy } : {},
406
+ ...moderation.profanityVerdict ? { profanityVerdict: moderation.profanityVerdict } : {},
407
+ ...moderation.spamVerdict ? { spamVerdict: moderation.spamVerdict } : {}
408
+ }
409
+ });
410
+ if (spamStatus === "published") {
411
+ await applyReputation(memberId, {
412
+ kind: "document.created",
413
+ collectionSlug: collection,
414
+ documentId,
415
+ memberId
416
+ });
417
+ }
418
+ if (spamStatus === "published") {
419
+ const { fanOutMentionNotifications } = await import("./mentions-2IHFVSHW.js");
420
+ await fanOutMentionNotifications({
421
+ actorMemberId: memberId,
422
+ kind: "document.mention",
423
+ data,
424
+ payload: {
425
+ collectionSlug: collection,
426
+ documentId
427
+ }
428
+ });
429
+ }
430
+ return result;
431
+ }
432
+ async function runMemberDocModeration(input) {
433
+ const { collection, data, memberId, targetId } = input;
434
+ const config = getCollectionConfig(collection);
435
+ const { getSpamAdapter } = await import("./spam-adapter-XX3G737Z.js");
436
+ const { getProfanityAdapter } = await import("./profanity-adapter-NU2JQSLX.js");
437
+ const { getLogger: getLogger2 } = await import("./logger-S7REWDNE.js");
438
+ const moderationText = buildSearchVector(config, data);
439
+ const ctx = {
440
+ memberId,
441
+ targetType: collection,
442
+ targetId,
443
+ parentId: null
444
+ };
445
+ let profanityVerdict = null;
446
+ try {
447
+ const verdict = await getProfanityAdapter().check(moderationText, ctx);
448
+ if (verdict.kind === "reject") {
449
+ throw new NpValidationError("Invalid input", [
450
+ {
451
+ field: "body",
452
+ message: verdict.reason ?? "Submission contains prohibited language"
453
+ }
454
+ ]);
455
+ }
456
+ if (verdict.kind === "flag") {
457
+ profanityVerdict = {
458
+ reason: verdict.reason ?? null,
459
+ metadata: verdict.metadata ?? null
460
+ };
461
+ }
462
+ } catch (err) {
463
+ if (err instanceof NpValidationError) throw err;
464
+ getLogger2().warn("profanity adapter threw on doc write \u2014 treating as pass", {
465
+ error: err instanceof Error ? err.message : String(err),
466
+ collection,
467
+ memberId
468
+ });
469
+ }
470
+ let spamVerdict = null;
471
+ try {
472
+ const verdict = await getSpamAdapter().check(moderationText, ctx);
473
+ if (verdict.kind === "reject") {
474
+ throw new NpValidationError("Invalid input", [
475
+ {
476
+ field: "body",
477
+ message: verdict.reason ?? "Submission rejected"
478
+ }
479
+ ]);
480
+ }
481
+ if (verdict.kind === "flag") {
482
+ spamVerdict = {
483
+ reason: verdict.reason ?? null,
484
+ metadata: verdict.metadata ?? null
485
+ };
486
+ }
487
+ } catch (err) {
488
+ if (err instanceof NpValidationError) throw err;
489
+ getLogger2().warn("spam adapter threw on doc write \u2014 treating as pass", {
490
+ error: err instanceof Error ? err.message : String(err),
491
+ collection,
492
+ memberId
493
+ });
494
+ }
495
+ const flaggedBy = [];
496
+ if (profanityVerdict) flaggedBy.push("profanity");
497
+ if (spamVerdict) flaggedBy.push("spam");
498
+ return { flaggedBy, profanityVerdict, spamVerdict };
499
+ }
500
+ async function initSaveContext(collection, docId, data, actor, options) {
501
+ const config = getCollectionConfig(collection);
502
+ const registration = getCollectionRegistration(collection);
503
+ const table = getCollectionTable(collection);
504
+ const db = getDb();
505
+ const validatedData = toRecord(getCollectionZodSchema(config).parse(data));
506
+ const operation = docId ? "update" : "create";
507
+ const originalDoc = docId ? await getDocumentByIdInternal(db, table, collection, docId) : null;
508
+ return {
509
+ collection,
510
+ docId,
511
+ validatedData,
512
+ actor,
513
+ options,
514
+ config,
515
+ registration,
516
+ table,
517
+ db,
518
+ operation,
519
+ originalDoc,
520
+ userForHooks: actorUserOrNull(actor),
521
+ principal: actorPrincipal(actor)
522
+ };
523
+ }
524
+ async function validateActorAccess(ctx) {
525
+ if (ctx.actor.kind === "staff") {
526
+ await assertWriteAccess(
527
+ ctx.config,
528
+ ctx.collection,
529
+ ctx.operation,
530
+ ctx.actor.user,
531
+ ctx.validatedData,
532
+ ctx.originalDoc
533
+ );
534
+ return;
535
+ }
536
+ const { assertNotBanned } = await import("./can-YLUHRJAB.js");
537
+ if (ctx.operation === "create") {
538
+ if (!ctx.config.community?.memberWrite?.create) {
539
+ throw new NpForbiddenError(ctx.collection, "create");
540
+ }
541
+ await assertNotBanned(ctx.actor.memberId);
542
+ return;
543
+ }
544
+ if (!ctx.originalDoc) {
545
+ throw new NpNotFoundError(ctx.collection, ctx.docId ?? "unknown");
546
+ }
547
+ if (!ctx.config.community?.memberWrite?.update) {
548
+ throw new NpForbiddenError(ctx.collection, "update");
549
+ }
550
+ const authorId = ctx.originalDoc.memberAuthorId ?? null;
551
+ if (authorId !== ctx.actor.memberId) {
552
+ throw new NpForbiddenError(ctx.collection, "update");
553
+ }
554
+ await assertNotBanned(ctx.actor.memberId);
555
+ }
556
+ async function prepareDocumentForWrite(c) {
557
+ c.hookData = await runHooks(
558
+ c.operation === "create" ? c.config.hooks?.beforeCreate : c.config.hooks?.beforeUpdate,
559
+ {
560
+ data: c.validatedData,
561
+ user: c.userForHooks,
562
+ principal: c.principal,
563
+ collection: c.collection,
564
+ originalDoc: c.originalDoc
565
+ }
566
+ );
567
+ applySlugField(c.config, c.hookData, c.originalDoc);
568
+ let i18nResolved = null;
569
+ if (c.config.i18n) {
570
+ const i18n = getI18nConfig();
571
+ if (!i18n) {
572
+ throw new Error(
573
+ `Collection "${c.collection}" is i18n-enabled but the framework has no i18n config (setI18nConfig was never called).`
574
+ );
575
+ }
576
+ if (c.operation === "create") {
577
+ const requestedLocale = c.hookData.locale;
578
+ const locale = typeof requestedLocale === "string" && requestedLocale.length > 0 ? requestedLocale : i18n.defaultLocale;
579
+ if (!i18n.locales.includes(locale)) {
580
+ throw new NpValidationError("Invalid input", [
581
+ {
582
+ field: "locale",
583
+ message: `Locale "${locale}" is not configured. Allowed: ${i18n.locales.join(", ")}.`
584
+ }
585
+ ]);
586
+ }
587
+ const requestedGroup = c.hookData.translationGroupId;
588
+ const translationGroupId = typeof requestedGroup === "string" && requestedGroup.length > 0 ? requestedGroup : randomUUID();
589
+ i18nResolved = { locale, translationGroupId };
590
+ } else {
591
+ const original = c.originalDoc;
592
+ if (!original?.locale || !original.translationGroupId) {
593
+ throw new Error(
594
+ `i18n collection "${c.collection}" doc ${c.docId} is missing locale/translationGroupId. The row predates i18n opt-in; backfill required.`
595
+ );
596
+ }
597
+ i18nResolved = {
598
+ locale: original.locale,
599
+ translationGroupId: original.translationGroupId
600
+ };
601
+ }
602
+ }
603
+ c.prepared = prepareDocumentData(c.config.fields, c.hookData);
604
+ if (c.options?.status) {
605
+ c.prepared.mainData.status = c.options.status;
606
+ }
607
+ if (i18nResolved) {
608
+ c.prepared.mainData.locale = i18nResolved.locale;
609
+ c.prepared.mainData.translationGroupId = i18nResolved.translationGroupId;
610
+ }
611
+ if (c.operation === "create") {
612
+ const resolved = await getCurrentSiteId();
613
+ c.prepared.mainData.siteId = resolved ?? NP_DEFAULT_SITE_ID;
614
+ } else {
615
+ const original = c.originalDoc;
616
+ c.prepared.mainData.siteId = original?.siteId ?? NP_DEFAULT_SITE_ID;
617
+ }
618
+ if (c.actor.kind === "member") {
619
+ if (c.operation === "create") {
620
+ c.prepared.mainData.memberAuthorId = c.actor.memberId;
621
+ } else {
622
+ delete c.prepared.mainData.memberAuthorId;
623
+ }
624
+ }
625
+ c.now = /* @__PURE__ */ new Date();
626
+ const desiredStatus = c.prepared.mainData.status;
627
+ const publishedAtValue = c.prepared.mainData.publishedAt;
628
+ if (desiredStatus === "published" && publishedAtValue instanceof Date && publishedAtValue > c.now) {
629
+ c.prepared.mainData.status = "scheduled";
630
+ }
631
+ c.searchVector = buildWeightedSearchVectorSql(c.config, c.hookData);
632
+ const nextStatus = c.prepared.mainData.status ?? (c.operation === "update" ? c.originalDoc?.status ?? "published" : "published");
633
+ const previousStatus = c.originalDoc?.status;
634
+ const wasPublished = previousStatus === "published";
635
+ const willBePublished = nextStatus === "published";
636
+ c.publishTransition = !wasPublished && willBePublished;
637
+ c.unpublishTransition = wasPublished && !willBePublished;
638
+ }
639
+ async function persistDocumentTx(ctx) {
640
+ await runHook(
641
+ ctx.operation === "create" ? "content:beforeCreate" : "content:beforeUpdate",
642
+ {
643
+ collection: ctx.collection,
644
+ data: ctx.hookData,
645
+ originalDoc: ctx.originalDoc,
646
+ user: ctx.userForHooks,
647
+ principal: ctx.principal,
648
+ operation: ctx.operation
649
+ }
650
+ );
651
+ if (ctx.publishTransition) {
652
+ await runHook("content:beforePublish", {
653
+ collection: ctx.collection,
654
+ data: ctx.hookData,
655
+ originalDoc: ctx.originalDoc,
656
+ user: ctx.userForHooks,
657
+ principal: ctx.principal
658
+ });
659
+ }
660
+ if (ctx.unpublishTransition) {
661
+ await runHook("content:beforeUnpublish", {
662
+ collection: ctx.collection,
663
+ data: ctx.hookData,
664
+ originalDoc: ctx.originalDoc,
665
+ user: ctx.userForHooks,
666
+ principal: ctx.principal
667
+ });
668
+ }
669
+ return ctx.db.transaction(async (tx) => {
670
+ const persistedDoc = ctx.operation === "update" ? await updateMainDocument(
671
+ tx,
672
+ ctx.table,
673
+ ctx.collection,
674
+ ctx.docId,
675
+ ctx.prepared.mainData,
676
+ ctx.searchVector,
677
+ ctx.config,
678
+ ctx.userForHooks,
679
+ ctx.now
680
+ ) : await createMainDocument(
681
+ tx,
682
+ ctx.table,
683
+ ctx.prepared.mainData,
684
+ ctx.searchVector,
685
+ ctx.config,
686
+ ctx.userForHooks,
687
+ ctx.now
688
+ );
689
+ const persistedDocId = getRecordId(persistedDoc);
690
+ await syncChildTables(tx, ctx.registration.childTables, ctx.prepared.childRows, persistedDocId);
691
+ await syncJoinTables(tx, ctx.registration.joinTables, ctx.prepared.joinRows, persistedDocId);
692
+ await syncMediaRefsForDocument(tx, ctx.collection, persistedDocId, ctx.config.fields, ctx.hookData);
693
+ if (ctx.operation === "update" && ctx.config.slugField && ctx.originalDoc && typeof ctx.originalDoc.slug === "string" && typeof persistedDoc.slug === "string" && ctx.originalDoc.slug.length > 0 && ctx.originalDoc.slug !== persistedDoc.slug) {
694
+ const siteId = persistedDoc.siteId ?? NP_DEFAULT_SITE_ID;
695
+ await tx.insert(npSlugHistory).values({
696
+ siteId,
697
+ collection: ctx.collection,
698
+ documentId: String(persistedDocId),
699
+ oldSlug: ctx.originalDoc.slug,
700
+ newSlug: persistedDoc.slug
701
+ });
702
+ }
703
+ if (ctx.config.versions) {
704
+ const docStatus = persistedDoc.status;
705
+ const revisionStatus = docStatus === "published" ? "published" : "draft";
706
+ const maxRevisions = typeof ctx.config.versions === "object" && ctx.config.versions.max !== void 0 ? ctx.config.versions.max : void 0;
707
+ await insertRevision(
708
+ tx,
709
+ ctx.collection,
710
+ persistedDocId,
711
+ ctx.operation,
712
+ ctx.hookData,
713
+ ctx.originalDoc,
714
+ ctx.userForHooks,
715
+ revisionStatus,
716
+ maxRevisions
717
+ );
718
+ }
719
+ return persistedDoc;
720
+ });
721
+ }
722
+ async function firePostCommitHooks(ctx, savedDoc) {
723
+ const savedDocId = getRecordId(savedDoc);
724
+ const postCommitCtx = {
725
+ collection: ctx.collection,
726
+ documentId: savedDocId,
727
+ operation: ctx.operation
728
+ };
729
+ await runPostCommit(
730
+ "enqueue:content:afterSave",
731
+ postCommitCtx,
732
+ () => enqueueJob("content:afterSave", {
733
+ collection: ctx.collection,
734
+ documentId: savedDocId,
735
+ operation: ctx.operation,
736
+ userId: actorUserId(ctx.actor)
737
+ })
738
+ );
739
+ const pluginHookName = ctx.operation === "create" ? "content:afterCreate" : "content:afterUpdate";
740
+ await runPostCommit(
741
+ `hook:${pluginHookName}`,
742
+ postCommitCtx,
743
+ () => runHook(pluginHookName, {
744
+ collection: ctx.collection,
745
+ doc: savedDoc,
746
+ operation: ctx.operation,
747
+ user: ctx.userForHooks,
748
+ principal: ctx.principal
749
+ })
750
+ );
751
+ if (ctx.publishTransition) {
752
+ await runPostCommit(
753
+ "hook:content:afterPublish",
754
+ postCommitCtx,
755
+ () => runHook("content:afterPublish", {
756
+ collection: ctx.collection,
757
+ doc: savedDoc,
758
+ operation: ctx.operation,
759
+ user: ctx.userForHooks,
760
+ principal: ctx.principal
761
+ })
762
+ );
763
+ }
764
+ }
765
+ async function saveDocumentImpl(collection, docId, data, actor, options) {
766
+ const ctxBase = await initSaveContext(collection, docId, data, actor, options);
767
+ await validateActorAccess(ctxBase);
768
+ const ctx = ctxBase;
769
+ await prepareDocumentForWrite(ctx);
770
+ const savedDoc = await persistDocumentTx(ctx);
771
+ await firePostCommitHooks(ctx, savedDoc);
772
+ return { doc: savedDoc, operation: ctx.operation };
773
+ }
774
+ async function autosaveRevision(collection, documentId, data, user) {
775
+ const config = getCollectionConfig(collection);
776
+ const registration = getCollectionRegistration(collection);
777
+ const table = getCollectionTable(collection);
778
+ const db = getDb();
779
+ const drafts = config.versions?.drafts;
780
+ if (!drafts) {
781
+ throw new NpValidationError("Autosave not available", [
782
+ {
783
+ field: "collection",
784
+ message: `Collection "${collection}" has versions.drafts disabled \u2014 autosave is unavailable.`
785
+ }
786
+ ]);
787
+ }
788
+ const autosaveEnabled = typeof drafts === "object" && drafts.autosave === true;
789
+ if (!autosaveEnabled) {
790
+ throw new NpValidationError("Autosave disabled", [
791
+ {
792
+ field: "collection",
793
+ message: `Autosave is not enabled for "${collection}" \u2014 set versions.drafts.autosave = true.`
794
+ }
795
+ ]);
796
+ }
797
+ const originalDoc = await getDocumentByIdInternal(db, table, collection, documentId);
798
+ if (!originalDoc) {
799
+ throw new NpNotFoundError(collection, documentId);
800
+ }
801
+ await assertWriteAccess(config, collection, "update", user, data, originalDoc);
802
+ const [latestAutosave] = await db.select({
803
+ id: npRevisions.id,
804
+ version: npRevisions.version,
805
+ snapshot: npRevisions.snapshot,
806
+ createdAt: npRevisions.createdAt
807
+ }).from(npRevisions).where(
808
+ sql2`${eq(npRevisions.collection, collection)} and ${eq(npRevisions.documentId, documentId)} and ${eq(npRevisions.status, "autosave")}`
809
+ ).orderBy(desc(npRevisions.version)).limit(1);
810
+ if (latestAutosave && stableJson(latestAutosave.snapshot) === stableJson(data)) {
811
+ return {
812
+ id: latestAutosave.id,
813
+ version: latestAutosave.version,
814
+ status: "autosave",
815
+ createdAt: latestAutosave.createdAt,
816
+ reused: true
817
+ };
818
+ }
819
+ const maxRevisions = typeof config.versions === "object" && config.versions.max !== void 0 ? config.versions.max : void 0;
820
+ const inserted = await db.transaction(async (tx) => {
821
+ const [revisionCount] = await tx.select({ total: count() }).from(npRevisions).where(
822
+ sql2`${eq(npRevisions.collection, collection)} and ${eq(npRevisions.documentId, documentId)}`
823
+ );
824
+ const nextVersion = Number(revisionCount?.total ?? 0) + 1;
825
+ const createdAt = /* @__PURE__ */ new Date();
826
+ await tx.insert(npRevisions).values({
827
+ collection,
828
+ documentId,
829
+ version: nextVersion,
830
+ status: "autosave",
831
+ snapshot: data,
832
+ changedFields: getChangedFields(data, originalDoc, "update"),
833
+ authorId: user.id,
834
+ createdAt
835
+ });
836
+ if (maxRevisions !== void 0 && maxRevisions > 0 && nextVersion > maxRevisions) {
837
+ const overflow = nextVersion - maxRevisions;
838
+ const toDelete = await tx.select({ id: npRevisions.id }).from(npRevisions).where(
839
+ sql2`${eq(npRevisions.collection, collection)} and ${eq(npRevisions.documentId, documentId)}`
840
+ ).orderBy(asc(npRevisions.version)).limit(overflow);
841
+ if (toDelete.length > 0) {
842
+ const ids = toDelete.map((r) => r.id);
843
+ await tx.delete(npRevisions).where(sql2`${npRevisions.id} = any(${ids}::uuid[])`);
844
+ }
845
+ }
846
+ const [row] = await tx.select({ id: npRevisions.id }).from(npRevisions).where(
847
+ sql2`${eq(npRevisions.collection, collection)} and ${eq(npRevisions.documentId, documentId)} and ${eq(npRevisions.version, nextVersion)}`
848
+ ).limit(1);
849
+ return { id: row?.id ?? "", version: nextVersion, createdAt };
850
+ });
851
+ void registration;
852
+ return { ...inserted, status: "autosave", reused: false };
853
+ }
854
+ function stableJson(value) {
855
+ return JSON.stringify(value, (_key, val) => {
856
+ if (val && typeof val === "object" && !Array.isArray(val)) {
857
+ const sorted = {};
858
+ for (const k of Object.keys(val).sort()) {
859
+ sorted[k] = val[k];
860
+ }
861
+ return sorted;
862
+ }
863
+ return val;
864
+ });
865
+ }
866
+ async function deleteDocument(collection, docId, user) {
867
+ return deleteDocumentImpl(collection, docId, { kind: "staff", user });
868
+ }
869
+ async function deleteMemberDocument(collection, docId, memberId) {
870
+ const table = getCollectionTable(collection);
871
+ const db = getDb();
872
+ const original = await getDocumentByIdInternal(db, table, collection, docId);
873
+ const wasPublished = typeof original?.status === "string" && original.status === "published";
874
+ await deleteDocumentImpl(collection, docId, { kind: "member", memberId });
875
+ const { applyReputation } = await import("./reputation-JRL2YQHM.js");
876
+ const { recordAuditEvent } = await import("./audit-54XLVCWD.js");
877
+ await recordAuditEvent({
878
+ actor: { kind: "member", memberId },
879
+ action: "document.delete",
880
+ targetType: collection,
881
+ targetId: docId,
882
+ payload: {
883
+ collectionSlug: collection,
884
+ // Capture the status that was in effect at delete time so a
885
+ // mod re-reading the audit log can tell "they deleted a
886
+ // pending submission" from "they retracted a published
887
+ // post."
888
+ previousStatus: typeof original?.status === "string" ? original.status : null
889
+ }
890
+ });
891
+ if (wasPublished) {
892
+ await applyReputation(memberId, {
893
+ kind: "document.deleted",
894
+ collectionSlug: collection,
895
+ documentId: docId,
896
+ memberId
897
+ });
898
+ }
899
+ }
900
+ async function promoteMemberDocument(collection, docId, staffUserId) {
901
+ const table = getCollectionTable(collection);
902
+ const db = getDb();
903
+ const originalDoc = await getDocumentByIdInternal(db, table, collection, docId);
904
+ if (!originalDoc) {
905
+ throw new NpNotFoundError(collection, docId);
906
+ }
907
+ const status = originalDoc.status;
908
+ if (status !== "pending") {
909
+ throw new NpValidationError("Invalid input", [
910
+ {
911
+ field: "status",
912
+ message: `Cannot promote: document is ${status ?? "unknown"}, expected pending`
913
+ }
914
+ ]);
915
+ }
916
+ const memberAuthorId = originalDoc.memberAuthorId ?? null;
917
+ if (!memberAuthorId) {
918
+ throw new NpValidationError("Invalid input", [
919
+ {
920
+ field: "memberAuthorId",
921
+ message: "Cannot promote: document is not member-authored"
922
+ }
923
+ ]);
924
+ }
925
+ const requestSiteId = await getCurrentSiteId() ?? NP_DEFAULT_SITE_ID;
926
+ const now = /* @__PURE__ */ new Date();
927
+ const updated = await db.update(table).set({ status: "published", updatedAt: now, updatedBy: staffUserId }).where(
928
+ sql2`${eq(getTableColumn(table, "id"), docId)} and ${eq(getTableColumn(table, "status"), "pending")} and ${eq(getTableColumn(table, "siteId"), requestSiteId)}`
929
+ ).returning();
930
+ if (updated.length === 0) {
931
+ throw new NpValidationError("Invalid input", [
932
+ {
933
+ field: "status",
934
+ message: "Cannot promote: row is no longer pending (concurrent change)"
935
+ }
936
+ ]);
937
+ }
938
+ const persistedDoc = toRecord(updated[0]);
939
+ const { applyReputation } = await import("./reputation-JRL2YQHM.js");
940
+ const { recordAuditEvent } = await import("./audit-54XLVCWD.js");
941
+ await recordAuditEvent({
942
+ actor: { kind: "staff", userId: staffUserId },
943
+ action: "document.promote",
944
+ targetType: collection,
945
+ targetId: docId,
946
+ payload: {
947
+ collectionSlug: collection,
948
+ memberAuthorId,
949
+ previousStatus: "pending"
950
+ }
951
+ });
952
+ await applyReputation(memberAuthorId, {
953
+ kind: "document.created",
954
+ collectionSlug: collection,
955
+ documentId: docId,
956
+ memberId: memberAuthorId
957
+ });
958
+ return { doc: persistedDoc, operation: "update" };
959
+ }
960
+ async function deleteDocumentImpl(collection, docId, actor) {
961
+ const config = getCollectionConfig(collection);
962
+ const registration = getCollectionRegistration(collection);
963
+ const table = getCollectionTable(collection);
964
+ const db = getDb();
965
+ const originalDoc = await getDocumentByIdInternal(db, table, collection, docId);
966
+ if (!originalDoc) {
967
+ throw new NpNotFoundError(collection, docId);
968
+ }
969
+ if (actor.kind === "staff") {
970
+ if (config.access?.delete) {
971
+ const allowed = await config.access.delete({ user: actor.user, doc: originalDoc });
972
+ if (!allowed) {
973
+ throw new NpForbiddenError(collection, "delete");
974
+ }
975
+ }
976
+ } else {
977
+ if (!config.community?.memberWrite?.delete) {
978
+ throw new NpForbiddenError(collection, "delete");
979
+ }
980
+ const authorId = originalDoc.memberAuthorId ?? null;
981
+ if (authorId !== actor.memberId) {
982
+ throw new NpForbiddenError(collection, "delete");
983
+ }
984
+ const { assertNotBanned } = await import("./can-YLUHRJAB.js");
985
+ await assertNotBanned(actor.memberId);
986
+ }
987
+ const userForHooks = actorUserOrNull(actor);
988
+ const principal = actorPrincipal(actor);
989
+ await runHooks(config.hooks?.beforeDelete, {
990
+ data: originalDoc,
991
+ user: userForHooks,
992
+ principal,
993
+ collection,
994
+ originalDoc
995
+ });
996
+ await runHook("content:beforeDelete", {
997
+ collection,
998
+ doc: originalDoc,
999
+ user: userForHooks,
1000
+ principal
1001
+ });
1002
+ await db.transaction(async (tx) => {
1003
+ await deleteChildTables(tx, registration.childTables, docId);
1004
+ await deleteJoinTables(tx, registration.joinTables, docId);
1005
+ await tx.delete(npMediaRefs).where(
1006
+ sql2`${eq(getTableColumn(npMediaRefs, "collection"), collection)} and ${eq(getTableColumn(npMediaRefs, "documentId"), docId)}`
1007
+ );
1008
+ const commentIdRows = await tx.select({
1009
+ id: getTableColumn(npComments, "id")
1010
+ }).from(npComments).where(
1011
+ sql2`${eq(getTableColumn(npComments, "targetType"), collection)} and ${eq(getTableColumn(npComments, "targetId"), docId)}`
1012
+ );
1013
+ if (commentIdRows.length > 0) {
1014
+ const commentIds = commentIdRows.map((row) => row.id);
1015
+ await tx.delete(npReactions).where(
1016
+ sql2`${eq(getTableColumn(npReactions, "targetType"), "comment")} and ${inArray(getTableColumn(npReactions, "targetId"), commentIds)}`
1017
+ );
1018
+ await tx.delete(npReports).where(
1019
+ sql2`${eq(getTableColumn(npReports, "targetType"), "comment")} and ${inArray(getTableColumn(npReports, "targetId"), commentIds)}`
1020
+ );
1021
+ }
1022
+ await tx.delete(npComments).where(
1023
+ sql2`${eq(getTableColumn(npComments, "targetType"), collection)} and ${eq(getTableColumn(npComments, "targetId"), docId)}`
1024
+ );
1025
+ await tx.delete(npReactions).where(
1026
+ sql2`${eq(getTableColumn(npReactions, "targetType"), collection)} and ${eq(getTableColumn(npReactions, "targetId"), docId)}`
1027
+ );
1028
+ await tx.delete(npReports).where(
1029
+ sql2`${eq(getTableColumn(npReports, "targetType"), collection)} and ${eq(getTableColumn(npReports, "targetId"), docId)}`
1030
+ );
1031
+ await tx.delete(table).where(eq(getTableColumn(table, "id"), docId));
1032
+ });
1033
+ const postCommitCtx = { collection, documentId: docId, operation: "delete" };
1034
+ await runPostCommit(
1035
+ "enqueue:content:afterDelete",
1036
+ postCommitCtx,
1037
+ () => enqueueJob("content:afterDelete", {
1038
+ collection,
1039
+ documentId: docId,
1040
+ userId: actorUserId(actor)
1041
+ })
1042
+ );
1043
+ await runPostCommit(
1044
+ "hook:content:afterDelete",
1045
+ postCommitCtx,
1046
+ () => runHook("content:afterDelete", {
1047
+ collection,
1048
+ documentId: docId,
1049
+ user: userForHooks,
1050
+ principal
1051
+ })
1052
+ );
1053
+ }
1054
+ async function findDocuments(collection, options, user) {
1055
+ const config = getCollectionConfig(collection);
1056
+ const table = getCollectionTable(collection);
1057
+ const db = getDb();
1058
+ const page = normalizePage(options.page);
1059
+ const limit = normalizeLimit(options.limit);
1060
+ const offset = (page - 1) * limit;
1061
+ await assertReadAccess(config, collection, user ?? null);
1062
+ let effectiveWhere = options.where ?? {};
1063
+ if (config.i18n && options.locale) {
1064
+ effectiveWhere = { ...effectiveWhere, locale: options.locale };
1065
+ }
1066
+ if (effectiveWhere.siteId === void 0) {
1067
+ const resolved = await getCurrentSiteId();
1068
+ effectiveWhere = {
1069
+ ...effectiveWhere,
1070
+ siteId: resolved ?? NP_DEFAULT_SITE_ID
1071
+ };
1072
+ } else if (effectiveWhere.siteId === "*") {
1073
+ const { siteId: _siteId, ...rest } = effectiveWhere;
1074
+ void _siteId;
1075
+ effectiveWhere = rest;
1076
+ }
1077
+ if (effectiveWhere.visibility === void 0 && !user) {
1078
+ effectiveWhere = { ...effectiveWhere, visibility: "public" };
1079
+ } else if (effectiveWhere.visibility === "*") {
1080
+ const { visibility: _vis, ...rest } = effectiveWhere;
1081
+ void _vis;
1082
+ effectiveWhere = rest;
1083
+ }
1084
+ const effectiveOptions = {
1085
+ ...options,
1086
+ where: effectiveWhere
1087
+ };
1088
+ const conditions = buildQueryConditions(table, effectiveOptions);
1089
+ const whereClause = combineConditions(conditions);
1090
+ const docs = await executeFindQuery(db, table, options, whereClause, limit, offset);
1091
+ const totalResult = await (whereClause ? db.select({ total: count() }).from(table).where(whereClause) : db.select({ total: count() }).from(table).limit(1));
1092
+ const totalDocs = Number(totalResult[0]?.total ?? 0);
1093
+ const totalPages = totalDocs === 0 ? 0 : Math.ceil(totalDocs / limit);
1094
+ return {
1095
+ // Runtime rows are `Record<string, unknown>`; the generic `T`
1096
+ // is a structural promise — generated wrapper functions narrow
1097
+ // it to the per-collection document shape. We don't validate
1098
+ // at runtime (Drizzle has already shaped the row to the table
1099
+ // schema, which the generator derived from the same field
1100
+ // configs T was generated from).
1101
+ docs,
1102
+ totalDocs,
1103
+ totalPages,
1104
+ page,
1105
+ limit,
1106
+ hasNextPage: page < totalPages,
1107
+ hasPrevPage: page > 1 && totalDocs > 0
1108
+ };
1109
+ }
1110
+ async function getDocumentById(collection, id, user) {
1111
+ const config = getCollectionConfig(collection);
1112
+ const table = getCollectionTable(collection);
1113
+ const db = getDb();
1114
+ const doc = await getDocumentByIdOptional(db, table, id);
1115
+ if (!doc) {
1116
+ return null;
1117
+ }
1118
+ const requestSiteId = await getCurrentSiteId() ?? NP_DEFAULT_SITE_ID;
1119
+ const docSiteId = typeof doc.siteId === "string" && doc.siteId.length > 0 ? doc.siteId : NP_DEFAULT_SITE_ID;
1120
+ if (docSiteId !== requestSiteId) {
1121
+ throw new NpForbiddenError(collection, "cross-site");
1122
+ }
1123
+ if (config.access?.read) {
1124
+ const allowed = await config.access.read({ user: user ?? null, doc });
1125
+ if (!allowed) {
1126
+ throw new NpForbiddenError(collection, "read");
1127
+ }
1128
+ }
1129
+ return doc;
1130
+ }
1131
+ async function assertWriteAccess(config, collection, operation, user, data, originalDoc) {
1132
+ const access = operation === "create" ? config.access?.create : config.access?.update;
1133
+ if (!access) {
1134
+ return;
1135
+ }
1136
+ const allowed = await access({ user, doc: originalDoc ?? void 0, data });
1137
+ if (!allowed) {
1138
+ throw new NpForbiddenError(collection, operation);
1139
+ }
1140
+ }
1141
+ async function assertReadAccess(config, collection, user) {
1142
+ if (!config.access?.read) {
1143
+ return;
1144
+ }
1145
+ const allowed = await config.access.read({ user });
1146
+ if (!allowed) {
1147
+ throw new NpForbiddenError(collection, "read");
1148
+ }
1149
+ }
1150
+ async function runHooks(hooks, args) {
1151
+ let nextData = args.data;
1152
+ for (const hook of hooks ?? []) {
1153
+ nextData = await hook({
1154
+ ...args,
1155
+ data: nextData
1156
+ });
1157
+ }
1158
+ return nextData;
1159
+ }
1160
+ async function createMainDocument(tx, table, mainData, searchVectorSql, config, user, now) {
1161
+ const values = {
1162
+ id: randomUUID(),
1163
+ status: "published",
1164
+ ...mainData,
1165
+ createdBy: user?.id ?? null,
1166
+ updatedBy: user?.id ?? null,
1167
+ // Phase 10.7 — composed setweight() tsvector so titles
1168
+ // outrank body matches at query time. The 11.x
1169
+ // to_tsvector wrap (so colon-containing content doesn't
1170
+ // crash the cast) is preserved inside each setweight call
1171
+ // by buildWeightedSearchVectorSql.
1172
+ searchVector: searchVectorSql
1173
+ };
1174
+ if (config.timestamps !== false) {
1175
+ values.createdAt = now;
1176
+ values.updatedAt = now;
1177
+ }
1178
+ const [created] = await tx.insert(table).values(values).returning();
1179
+ return toRecord(created);
1180
+ }
1181
+ async function updateMainDocument(tx, table, collection, docId, mainData, searchVectorSql, config, user, now) {
1182
+ if (!docId) {
1183
+ throw new NpNotFoundError(collection, "unknown");
1184
+ }
1185
+ const values = {
1186
+ ...mainData,
1187
+ updatedBy: user?.id ?? null,
1188
+ // Phase 10.7 — see createMainDocument: weighted setweight()
1189
+ // tsvector preserves the 11.x to_tsvector safety AND adds
1190
+ // title boost.
1191
+ searchVector: searchVectorSql
1192
+ };
1193
+ if (config.timestamps !== false) {
1194
+ values.updatedAt = now;
1195
+ }
1196
+ const [updated] = await tx.update(table).set(values).where(eq(getTableColumn(table, "id"), docId)).returning();
1197
+ if (!updated) {
1198
+ throw new NpNotFoundError(collection, docId);
1199
+ }
1200
+ return toRecord(updated);
1201
+ }
1202
+ async function syncChildTables(tx, childTables, childRows, documentId) {
1203
+ for (const [fieldPath, rows] of Object.entries(childRows)) {
1204
+ const table = resolveRelatedTable(childTables, fieldPath);
1205
+ if (!table) {
1206
+ continue;
1207
+ }
1208
+ const pgTable = table;
1209
+ const parentColumnName = findParentColumnName(pgTable, ["parentId"]);
1210
+ await tx.delete(pgTable).where(eq(getTableColumn(pgTable, parentColumnName), documentId));
1211
+ if (rows.length === 0) {
1212
+ continue;
1213
+ }
1214
+ const values = rows.map((row, index) => ({
1215
+ id: randomUUID(),
1216
+ ...row,
1217
+ [parentColumnName]: documentId,
1218
+ order: index
1219
+ }));
1220
+ await tx.insert(pgTable).values(values);
1221
+ }
1222
+ }
1223
+ async function syncJoinTables(tx, joinTables, joinRows, documentId) {
1224
+ for (const [fieldPath, ids] of Object.entries(joinRows)) {
1225
+ const table = resolveRelatedTable(joinTables, fieldPath);
1226
+ if (!table) {
1227
+ continue;
1228
+ }
1229
+ const pgTable = table;
1230
+ const parentColumnName = findParentColumnName(pgTable, ["parentId"]);
1231
+ await tx.delete(pgTable).where(eq(getTableColumn(pgTable, parentColumnName), documentId));
1232
+ if (ids.length === 0) {
1233
+ continue;
1234
+ }
1235
+ const values = ids.map((targetId, index) => ({
1236
+ id: randomUUID(),
1237
+ [parentColumnName]: documentId,
1238
+ targetId,
1239
+ order: index
1240
+ }));
1241
+ await tx.insert(pgTable).values(values);
1242
+ }
1243
+ }
1244
+ async function deleteChildTables(tx, childTables, documentId) {
1245
+ for (const table of Object.values(childTables ?? {})) {
1246
+ const pgTable = table;
1247
+ const parentColumnName = findParentColumnName(pgTable, ["parentId"]);
1248
+ await tx.delete(pgTable).where(eq(getTableColumn(pgTable, parentColumnName), documentId));
1249
+ }
1250
+ }
1251
+ async function deleteJoinTables(tx, joinTables, documentId) {
1252
+ for (const table of Object.values(joinTables ?? {})) {
1253
+ const pgTable = table;
1254
+ const parentColumnName = findParentColumnName(pgTable, ["parentId"]);
1255
+ await tx.delete(pgTable).where(eq(getTableColumn(pgTable, parentColumnName), documentId));
1256
+ }
1257
+ }
1258
+ async function insertRevision(tx, collection, documentId, operation, data, originalDoc, user, status, maxRevisions) {
1259
+ const revisionConditions = sql2`${eq(npRevisions.collection, collection)} and ${eq(npRevisions.documentId, documentId)}`;
1260
+ const [revisionCount] = await tx.select({ total: count() }).from(npRevisions).where(revisionConditions);
1261
+ await tx.insert(npRevisions).values({
1262
+ collection,
1263
+ documentId,
1264
+ version: Number(revisionCount?.total ?? 0) + 1,
1265
+ status,
1266
+ snapshot: data,
1267
+ changedFields: getChangedFields(data, originalDoc, operation),
1268
+ // `authorId` references np_users; member-authored revisions
1269
+ // store null and the audit log carries the actual member id.
1270
+ authorId: user?.id ?? null,
1271
+ createdAt: /* @__PURE__ */ new Date()
1272
+ });
1273
+ if (maxRevisions !== void 0 && maxRevisions > 0) {
1274
+ const currentCount = Number(revisionCount?.total ?? 0) + 1;
1275
+ const overflow = currentCount - maxRevisions;
1276
+ if (overflow > 0) {
1277
+ const toDelete = await tx.select({ id: npRevisions.id }).from(npRevisions).where(revisionConditions).orderBy(asc(npRevisions.version)).limit(overflow);
1278
+ if (toDelete.length > 0) {
1279
+ const ids = toDelete.map((r) => r.id);
1280
+ await tx.delete(npRevisions).where(sql2`${npRevisions.id} = any(${ids}::uuid[])`);
1281
+ }
1282
+ }
1283
+ }
1284
+ }
1285
+ function buildQueryConditions(table, options) {
1286
+ const conditions = [];
1287
+ if (options.where) {
1288
+ for (const [field, value] of Object.entries(options.where)) {
1289
+ if (value === void 0) {
1290
+ continue;
1291
+ }
1292
+ if (Array.isArray(value)) {
1293
+ if (value.length === 0) {
1294
+ conditions.push(sql2`false`);
1295
+ } else {
1296
+ conditions.push(inArray(getTableColumn(table, field), value));
1297
+ }
1298
+ continue;
1299
+ }
1300
+ conditions.push(eq(getTableColumn(table, field), value));
1301
+ }
1302
+ }
1303
+ if (options.search) {
1304
+ conditions.push(
1305
+ sql2`${getTableColumn(table, "searchVector")} @@ plainto_tsquery('english', ${options.search})`
1306
+ );
1307
+ }
1308
+ return conditions;
1309
+ }
1310
+ async function executeFindQuery(db, table, options, whereClause, limit, offset) {
1311
+ if (options.search) {
1312
+ const query = whereClause ? db.select().from(table).where(whereClause).orderBy(
1313
+ sql2`ts_rank(${getTableColumn(table, "searchVector")}, plainto_tsquery('english', ${options.search})) DESC`
1314
+ ).limit(limit).offset(offset) : db.select().from(table).orderBy(
1315
+ sql2`ts_rank(${getTableColumn(table, "searchVector")}, plainto_tsquery('english', ${options.search})) DESC`
1316
+ ).limit(limit).offset(offset);
1317
+ return await query;
1318
+ }
1319
+ const orderClause = getSortOrderClause(table, options.sort);
1320
+ if (whereClause && orderClause) {
1321
+ return await db.select().from(table).where(whereClause).orderBy(orderClause).limit(limit).offset(offset);
1322
+ }
1323
+ if (whereClause) {
1324
+ return await db.select().from(table).where(whereClause).limit(limit).offset(offset);
1325
+ }
1326
+ if (orderClause) {
1327
+ return await db.select().from(table).orderBy(orderClause).limit(limit).offset(offset);
1328
+ }
1329
+ return await db.select().from(table).limit(limit).offset(offset);
1330
+ }
1331
+ function getSortOrderClause(table, sortValue) {
1332
+ const sort = sortValue?.trim();
1333
+ if (!sort) {
1334
+ return void 0;
1335
+ }
1336
+ const isDescending = sort.startsWith("-");
1337
+ const field = isDescending ? sort.slice(1) : sort;
1338
+ const column = getTableColumn(table, field);
1339
+ return isDescending ? desc(column) : asc(column);
1340
+ }
1341
+ async function getDocumentByIdInternal(db, table, collection, id, options) {
1342
+ const doc = await getDocumentByIdOptional(db, table, id);
1343
+ if (!doc) {
1344
+ throw new NpNotFoundError(collection, id);
1345
+ }
1346
+ if (!options?.allowCrossSite) {
1347
+ const requestSiteId = await getCurrentSiteId() ?? NP_DEFAULT_SITE_ID;
1348
+ const docSiteId = typeof doc.siteId === "string" && doc.siteId.length > 0 ? doc.siteId : NP_DEFAULT_SITE_ID;
1349
+ if (docSiteId !== requestSiteId) {
1350
+ throw new NpForbiddenError(collection, "cross-site");
1351
+ }
1352
+ }
1353
+ return doc;
1354
+ }
1355
+ async function getDocumentByIdOptional(db, table, id) {
1356
+ const [doc] = await db.select().from(table).where(eq(getTableColumn(table, "id"), id)).limit(1);
1357
+ return doc ? toRecord(doc) : null;
1358
+ }
1359
+ function prepareDocumentData(fields, data) {
1360
+ const prepared = {
1361
+ mainData: {},
1362
+ childRows: {},
1363
+ joinRows: {}
1364
+ };
1365
+ collectPreparedDocumentData(fields, data, prepared, []);
1366
+ if (typeof data.slug === "string") {
1367
+ prepared.mainData.slug = data.slug;
1368
+ }
1369
+ if (typeof data.locale === "string") {
1370
+ prepared.mainData.locale = data.locale;
1371
+ }
1372
+ if (typeof data.translationGroupId === "string") {
1373
+ prepared.mainData.translationGroupId = data.translationGroupId;
1374
+ }
1375
+ if (typeof data.siteId === "string") {
1376
+ prepared.mainData.siteId = data.siteId;
1377
+ }
1378
+ if (typeof data.visibility === "string") {
1379
+ prepared.mainData.visibility = data.visibility;
1380
+ }
1381
+ return prepared;
1382
+ }
1383
+ function collectPreparedDocumentData(fields, data, prepared, prefix) {
1384
+ for (const field of fields) {
1385
+ if (field.type === "row" || field.type === "collapsible") {
1386
+ collectPreparedDocumentData(field.fields, data, prepared, prefix);
1387
+ continue;
1388
+ }
1389
+ if (field.type === "group") {
1390
+ const groupValue = toOptionalRecord(data[field.name]);
1391
+ if (groupValue) {
1392
+ collectPreparedDocumentData(field.fields, groupValue, prepared, [...prefix, field.name]);
1393
+ }
1394
+ continue;
1395
+ }
1396
+ const fieldPath = [...prefix, field.name];
1397
+ const fieldKey = fieldPath.join(".");
1398
+ const value = data[field.name];
1399
+ if (field.type === "array") {
1400
+ prepared.childRows[fieldKey] = normalizeChildRows(field.fields, value);
1401
+ continue;
1402
+ }
1403
+ if (field.type === "relationship" && field.hasMany) {
1404
+ prepared.joinRows[fieldKey] = normalizeJoinIds(value);
1405
+ continue;
1406
+ }
1407
+ prepared.mainData[getFlattenedFieldName(prefix, field.name)] = value ?? null;
1408
+ }
1409
+ }
1410
+ function normalizeChildRows(fields, value) {
1411
+ if (!Array.isArray(value)) {
1412
+ return [];
1413
+ }
1414
+ return value.map((item) => {
1415
+ const row = toOptionalRecord(item) ?? {};
1416
+ const prepared = {
1417
+ mainData: {},
1418
+ childRows: {},
1419
+ joinRows: {}
1420
+ };
1421
+ collectPreparedDocumentData(fields, row, prepared, []);
1422
+ return prepared.mainData;
1423
+ });
1424
+ }
1425
+ function normalizeJoinIds(value) {
1426
+ if (!Array.isArray(value)) {
1427
+ return [];
1428
+ }
1429
+ return value.filter((item) => typeof item === "string");
1430
+ }
1431
+ async function syncMediaRefsForDocument(tx, collection, documentId, fields, data) {
1432
+ const refs = extractMediaIdsFromFields(fields, data, []);
1433
+ if (refs.length === 0) {
1434
+ await tx.delete(npMediaRefs).where(
1435
+ sql2`${eq(getTableColumn(npMediaRefs, "collection"), collection)} and ${eq(getTableColumn(npMediaRefs, "documentId"), documentId)}`
1436
+ );
1437
+ return;
1438
+ }
1439
+ await tx.delete(npMediaRefs).where(
1440
+ sql2`${eq(getTableColumn(npMediaRefs, "collection"), collection)} and ${eq(getTableColumn(npMediaRefs, "documentId"), documentId)}`
1441
+ );
1442
+ const values = refs.map((ref) => ({
1443
+ id: randomUUID(),
1444
+ mediaId: ref.mediaId,
1445
+ collection,
1446
+ documentId,
1447
+ field: ref.field
1448
+ }));
1449
+ await tx.insert(npMediaRefs).values(values);
1450
+ }
1451
+ function extractMediaIdsFromFields(fields, data, prefix) {
1452
+ const refs = [];
1453
+ for (const field of fields) {
1454
+ if (field.type === "row" || field.type === "collapsible") {
1455
+ refs.push(...extractMediaIdsFromFields(field.fields, data, prefix));
1456
+ continue;
1457
+ }
1458
+ if (field.type === "group") {
1459
+ const groupData = toOptionalRecord(data[field.name]);
1460
+ if (groupData) {
1461
+ refs.push(...extractMediaIdsFromFields(field.fields, groupData, [...prefix, field.name]));
1462
+ }
1463
+ continue;
1464
+ }
1465
+ const fieldPath = [...prefix, field.name].join(".");
1466
+ if (field.type === "upload") {
1467
+ const mediaId = data[field.name];
1468
+ if (typeof mediaId === "string" && mediaId.length > 0) {
1469
+ refs.push({ mediaId, field: fieldPath });
1470
+ }
1471
+ continue;
1472
+ }
1473
+ if (field.type === "richText") {
1474
+ const richTextValue = data[field.name];
1475
+ if (richTextValue && typeof richTextValue === "object") {
1476
+ refs.push(...extractMediaIdsFromLexicalJson(richTextValue, fieldPath));
1477
+ }
1478
+ continue;
1479
+ }
1480
+ if (field.type === "array") {
1481
+ const arrayValue = data[field.name];
1482
+ if (Array.isArray(arrayValue)) {
1483
+ for (const item of arrayValue) {
1484
+ const itemRecord = toOptionalRecord(item);
1485
+ if (itemRecord) {
1486
+ refs.push(
1487
+ ...extractMediaIdsFromFields(field.fields, itemRecord, [...prefix, field.name])
1488
+ );
1489
+ }
1490
+ }
1491
+ }
1492
+ continue;
1493
+ }
1494
+ if (field.type === "blocks") {
1495
+ const blocksValue = data[field.name];
1496
+ if (Array.isArray(blocksValue)) {
1497
+ for (const block of blocksValue) {
1498
+ const blockRecord = toOptionalRecord(block);
1499
+ if (blockRecord) {
1500
+ extractBlockMediaIds(blockRecord, fieldPath, refs);
1501
+ }
1502
+ }
1503
+ }
1504
+ continue;
1505
+ }
1506
+ }
1507
+ return refs;
1508
+ }
1509
+ function extractMediaIdsFromLexicalJson(node, fieldPath) {
1510
+ const refs = [];
1511
+ if (!node || typeof node !== "object") {
1512
+ return refs;
1513
+ }
1514
+ const record = node;
1515
+ if (record.type === "image" || record.type === "upload") {
1516
+ const mediaId = record.mediaId ?? record.value;
1517
+ if (typeof mediaId === "string" && mediaId.length > 0) {
1518
+ refs.push({ mediaId, field: fieldPath });
1519
+ }
1520
+ }
1521
+ const children = record.children ?? toOptionalRecord(record.root)?.children;
1522
+ if (Array.isArray(children)) {
1523
+ for (const child of children) {
1524
+ refs.push(...extractMediaIdsFromLexicalJson(child, fieldPath));
1525
+ }
1526
+ }
1527
+ return refs;
1528
+ }
1529
+ function extractBlockMediaIds(block, fieldPath, refs) {
1530
+ for (const [key, value] of Object.entries(block)) {
1531
+ if (key === "blockType" || key === "id") {
1532
+ continue;
1533
+ }
1534
+ if (typeof value === "string" && isUuid(value)) {
1535
+ refs.push({ mediaId: value, field: `${fieldPath}.${key}` });
1536
+ }
1537
+ }
1538
+ }
1539
+ function isUuid(value) {
1540
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
1541
+ }
1542
+ function getChangedFields(data, originalDoc, operation) {
1543
+ if (operation === "create" || !originalDoc) {
1544
+ return Object.keys(data);
1545
+ }
1546
+ return Object.keys(data).filter((field) => !Object.is(data[field], originalDoc[field]));
1547
+ }
1548
+ function combineConditions(conditions) {
1549
+ if (conditions.length === 0) {
1550
+ return void 0;
1551
+ }
1552
+ return sql2`${sql2.join(conditions, sql2` and `)}`;
1553
+ }
1554
+ function resolveRelatedTable(tables, fieldPath) {
1555
+ return tables?.[fieldPath] ?? tables?.[fieldPath.split(".").at(-1) ?? fieldPath];
1556
+ }
1557
+ function findParentColumnName(table, preferred) {
1558
+ const keys = Object.keys(table);
1559
+ for (const key of preferred) {
1560
+ if (keys.includes(key)) {
1561
+ return key;
1562
+ }
1563
+ }
1564
+ const derived = keys.find(
1565
+ (key) => key !== "id" && key !== "targetId" && key !== "order" && key.endsWith("Id")
1566
+ );
1567
+ if (!derived) {
1568
+ throw new Error("Unable to resolve parent column for related table.");
1569
+ }
1570
+ return derived;
1571
+ }
1572
+ function getTableColumn(table, key) {
1573
+ const column = table[key];
1574
+ if (!column) {
1575
+ throw new Error(`Column '${key}' not found on table.`);
1576
+ }
1577
+ return column;
1578
+ }
1579
+ function getRecordId(record) {
1580
+ const id = record.id;
1581
+ if (typeof id !== "string") {
1582
+ throw new Error("Expected saved document to include a string id.");
1583
+ }
1584
+ return id;
1585
+ }
1586
+ function toRecord(value) {
1587
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1588
+ throw new Error("Expected object record.");
1589
+ }
1590
+ return value;
1591
+ }
1592
+ function toOptionalRecord(value) {
1593
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1594
+ return null;
1595
+ }
1596
+ return value;
1597
+ }
1598
+ function normalizePage(page) {
1599
+ if (!page || page < 1) {
1600
+ return 1;
1601
+ }
1602
+ return Math.floor(page);
1603
+ }
1604
+ function normalizeLimit(limit) {
1605
+ if (!limit || limit < 1) {
1606
+ return 10;
1607
+ }
1608
+ return Math.floor(limit);
1609
+ }
1610
+ function getFlattenedFieldName(prefix, name) {
1611
+ if (prefix.length === 0) {
1612
+ return toCamelCase(name);
1613
+ }
1614
+ return `${prefix.map(toPascalCase).join("")}${toPascalCase(name)}`.replace(
1615
+ /^./u,
1616
+ (char) => char.toLowerCase()
1617
+ );
1618
+ }
1619
+ function toCamelCase(value) {
1620
+ const parts = splitName(value);
1621
+ const [first = "", ...rest] = parts;
1622
+ return `${first}${rest.map(toPascalCase).join("")}`;
1623
+ }
1624
+ function toPascalCase(value) {
1625
+ return splitName(value).map(capitalize).join("");
1626
+ }
1627
+ function splitName(value) {
1628
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^a-zA-Z0-9]+/).map((part) => part.toLowerCase()).filter(Boolean);
1629
+ }
1630
+ function capitalize(value) {
1631
+ return value.charAt(0).toUpperCase() + value.slice(1);
1632
+ }
1633
+
1634
+ // src/plugins/context.ts
1635
+ async function resolveStorageSiteId() {
1636
+ return await getCurrentSiteId() ?? NP_GLOBAL_PLUGIN_SITE_ID;
1637
+ }
1638
+ async function resolveSettingsSiteId() {
1639
+ return await getCurrentSiteId() ?? NP_DEFAULT_SITE_ID;
1640
+ }
1641
+ var pluginPrincipal = (pluginId) => ({
1642
+ id: `plugin:${pluginId}`,
1643
+ email: `${pluginId}@plugins.local`,
1644
+ name: `plugin/${pluginId}`,
1645
+ role: "admin",
1646
+ tokenVersion: 0
1647
+ });
1648
+ var pluginCache = /* @__PURE__ */ new Map();
1649
+ function cacheKey(pluginId, key) {
1650
+ return `${pluginId}:${key}`;
1651
+ }
1652
+ function assertCap(pluginId, capabilities, required) {
1653
+ if (!capabilities.includes(required)) {
1654
+ throw new NpForbiddenError(
1655
+ `plugin:${pluginId}`,
1656
+ `capability "${required}" not declared in manifest`
1657
+ );
1658
+ }
1659
+ }
1660
+ async function loadOptionalNextCache() {
1661
+ try {
1662
+ const moduleId = "next/cache";
1663
+ const mod = await import(moduleId);
1664
+ return mod;
1665
+ } catch {
1666
+ return null;
1667
+ }
1668
+ }
1669
+ function createPluginRuntimeContext(options) {
1670
+ const { pluginId, capabilities, allowedHosts, config, registration, lookupRegistration } = options;
1671
+ const db = () => getDb();
1672
+ const principal = pluginPrincipal(pluginId);
1673
+ const pluginLog = getScopedLogger({ pluginId });
1674
+ return {
1675
+ pluginId,
1676
+ config,
1677
+ capabilities,
1678
+ content: {
1679
+ async find(collection, query) {
1680
+ assertCap(pluginId, capabilities, "content:read");
1681
+ return findDocuments(collection, query ?? {}, principal);
1682
+ },
1683
+ async findOne(collection, id) {
1684
+ assertCap(pluginId, capabilities, "content:read");
1685
+ const doc = await getDocumentById(collection, id, principal);
1686
+ return doc ?? null;
1687
+ },
1688
+ async create(collection, data) {
1689
+ assertCap(pluginId, capabilities, "content:write");
1690
+ const result = await saveDocument(collection, null, data, principal);
1691
+ return result.doc;
1692
+ },
1693
+ async update(collection, id, data) {
1694
+ assertCap(pluginId, capabilities, "content:write");
1695
+ const result = await saveDocument(collection, id, data, principal);
1696
+ return result.doc;
1697
+ },
1698
+ async delete(collection, id) {
1699
+ assertCap(pluginId, capabilities, "content:delete");
1700
+ await deleteDocument(collection, id, principal);
1701
+ },
1702
+ async count(collection) {
1703
+ assertCap(pluginId, capabilities, "content:read");
1704
+ const result = await findDocuments(collection, { limit: 1 }, principal);
1705
+ return result.totalDocs;
1706
+ }
1707
+ },
1708
+ media: {
1709
+ async list(query) {
1710
+ assertCap(pluginId, capabilities, "media:read");
1711
+ return listMedia({
1712
+ page: query?.page,
1713
+ limit: query?.limit,
1714
+ mimeType: query?.mimeType,
1715
+ folderId: query?.folder
1716
+ });
1717
+ },
1718
+ async getById(id) {
1719
+ assertCap(pluginId, capabilities, "media:read");
1720
+ return getMediaById(id);
1721
+ },
1722
+ async getUrl(id) {
1723
+ assertCap(pluginId, capabilities, "media:read");
1724
+ const media = await getMediaById(id);
1725
+ if (!media || typeof media.storageKey !== "string") return "";
1726
+ const adapter = getStorageAdapter();
1727
+ return adapter.getUrl(media.storageKey);
1728
+ },
1729
+ async upload(file, metadata) {
1730
+ assertCap(pluginId, capabilities, "media:write");
1731
+ const buffer = Buffer.from(file instanceof ArrayBuffer ? new Uint8Array(file) : file);
1732
+ return uploadMedia(
1733
+ {
1734
+ buffer,
1735
+ originalFilename: metadata.filename,
1736
+ mimeType: metadata.mimeType
1737
+ },
1738
+ // `uploaded_by` is a nullable FK to `np_users.id`. The
1739
+ // previous `plugin:<id>` synthetic value violated the FK
1740
+ // and threw at insert time, leaving the storage object
1741
+ // orphaned. (#62) Plugin attribution lives in the audit
1742
+ // log + plugin-storage layer; the uploader column is for
1743
+ // staff-user provenance only.
1744
+ null,
1745
+ metadata.folder
1746
+ );
1747
+ },
1748
+ async delete(id) {
1749
+ assertCap(pluginId, capabilities, "media:delete");
1750
+ const result = await deleteMedia(id);
1751
+ if (!result.deleted && result.references && result.references.length > 0) {
1752
+ throw new NpError(
1753
+ `[plugin:${pluginId}] media.delete: ${id} is referenced by ${result.references.length} document(s).`,
1754
+ "CONFLICT",
1755
+ 409
1756
+ );
1757
+ }
1758
+ }
1759
+ },
1760
+ storage: {
1761
+ // Phase 17 — every storage call resolves the current
1762
+ // site at call time and uses it as part of the composite
1763
+ // PK `(plugin_id, site_id, key)`. Background workers and
1764
+ // scripts (no site resolver) fall back to the
1765
+ // `_global_` sentinel so legacy single-site callers keep
1766
+ // their existing keyspace.
1767
+ async get(key) {
1768
+ assertCap(pluginId, capabilities, "storage:kv");
1769
+ const siteId = await resolveStorageSiteId();
1770
+ const now = /* @__PURE__ */ new Date();
1771
+ const rows = await db().select().from(npPluginStorage).where(
1772
+ and(
1773
+ eq2(npPluginStorage.pluginId, pluginId),
1774
+ eq2(npPluginStorage.siteId, siteId),
1775
+ eq2(npPluginStorage.key, key),
1776
+ or(isNull(npPluginStorage.expiresAt), gt(npPluginStorage.expiresAt, now))
1777
+ )
1778
+ ).limit(1);
1779
+ const row = rows[0];
1780
+ return row?.value ?? null;
1781
+ },
1782
+ async set(key, value, opts) {
1783
+ assertCap(pluginId, capabilities, "storage:kv");
1784
+ const siteId = await resolveStorageSiteId();
1785
+ const expiresAt = opts?.ttl && opts.ttl > 0 ? new Date(Date.now() + opts.ttl * 1e3) : null;
1786
+ await db().insert(npPluginStorage).values({
1787
+ pluginId,
1788
+ siteId,
1789
+ key,
1790
+ value,
1791
+ expiresAt,
1792
+ updatedAt: /* @__PURE__ */ new Date()
1793
+ }).onConflictDoUpdate({
1794
+ target: [npPluginStorage.pluginId, npPluginStorage.siteId, npPluginStorage.key],
1795
+ set: { value, expiresAt, updatedAt: /* @__PURE__ */ new Date() }
1796
+ });
1797
+ },
1798
+ async delete(key) {
1799
+ assertCap(pluginId, capabilities, "storage:kv");
1800
+ const siteId = await resolveStorageSiteId();
1801
+ await db().delete(npPluginStorage).where(
1802
+ and(
1803
+ eq2(npPluginStorage.pluginId, pluginId),
1804
+ eq2(npPluginStorage.siteId, siteId),
1805
+ eq2(npPluginStorage.key, key)
1806
+ )
1807
+ );
1808
+ },
1809
+ async list(prefix) {
1810
+ assertCap(pluginId, capabilities, "storage:kv");
1811
+ const siteId = await resolveStorageSiteId();
1812
+ const now = /* @__PURE__ */ new Date();
1813
+ const where = prefix ? and(
1814
+ eq2(npPluginStorage.pluginId, pluginId),
1815
+ eq2(npPluginStorage.siteId, siteId),
1816
+ like(npPluginStorage.key, `${prefix}%`),
1817
+ or(isNull(npPluginStorage.expiresAt), gt(npPluginStorage.expiresAt, now))
1818
+ ) : and(
1819
+ eq2(npPluginStorage.pluginId, pluginId),
1820
+ eq2(npPluginStorage.siteId, siteId),
1821
+ or(isNull(npPluginStorage.expiresAt), gt(npPluginStorage.expiresAt, now))
1822
+ );
1823
+ const rows = await db().select({ key: npPluginStorage.key }).from(npPluginStorage).where(where);
1824
+ return rows.map((row) => row.key);
1825
+ },
1826
+ async has(key) {
1827
+ assertCap(pluginId, capabilities, "storage:kv");
1828
+ const siteId = await resolveStorageSiteId();
1829
+ const now = /* @__PURE__ */ new Date();
1830
+ const rows = await db().select({ key: npPluginStorage.key }).from(npPluginStorage).where(
1831
+ and(
1832
+ eq2(npPluginStorage.pluginId, pluginId),
1833
+ eq2(npPluginStorage.siteId, siteId),
1834
+ eq2(npPluginStorage.key, key),
1835
+ or(isNull(npPluginStorage.expiresAt), gt(npPluginStorage.expiresAt, now))
1836
+ )
1837
+ ).limit(1);
1838
+ return rows.length > 0;
1839
+ }
1840
+ },
1841
+ cache: {
1842
+ // The cache namespace is in-memory today (a process-
1843
+ // scoped Map). The interface is `Promise<...>` so a
1844
+ // future Redis-backed implementation can swap in
1845
+ // without breaking plugin authors; the sync
1846
+ // implementations return resolved promises directly so
1847
+ // the require-await rule stays happy.
1848
+ get(key) {
1849
+ const entry = pluginCache.get(cacheKey(pluginId, key));
1850
+ if (!entry) return Promise.resolve(null);
1851
+ if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
1852
+ pluginCache.delete(cacheKey(pluginId, key));
1853
+ return Promise.resolve(null);
1854
+ }
1855
+ return Promise.resolve(entry.value);
1856
+ },
1857
+ set(key, value, ttl) {
1858
+ pluginCache.set(cacheKey(pluginId, key), {
1859
+ value,
1860
+ expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null
1861
+ });
1862
+ return Promise.resolve();
1863
+ },
1864
+ invalidate(key) {
1865
+ pluginCache.delete(cacheKey(pluginId, key));
1866
+ return Promise.resolve();
1867
+ },
1868
+ invalidateAll() {
1869
+ const prefix = `${pluginId}:`;
1870
+ for (const key of pluginCache.keys()) {
1871
+ if (key.startsWith(prefix)) pluginCache.delete(key);
1872
+ }
1873
+ return Promise.resolve();
1874
+ }
1875
+ },
1876
+ settings: {
1877
+ async getSite() {
1878
+ assertCap(pluginId, capabilities, "settings:read");
1879
+ const siteId = await resolveSettingsSiteId();
1880
+ const rows = await db().select().from(npSettings).where(and(eq2(npSettings.siteId, siteId), eq2(npSettings.key, "site")));
1881
+ const row = rows[0];
1882
+ if (!row || !row.value || typeof row.value !== "object" || Array.isArray(row.value)) {
1883
+ return {};
1884
+ }
1885
+ return row.value;
1886
+ },
1887
+ async getPlugin() {
1888
+ const { getPluginConfig } = await import("./config-2GDU7PCK.js");
1889
+ const value = await getPluginConfig(pluginId);
1890
+ if (value && typeof value === "object" && !Array.isArray(value)) {
1891
+ return value;
1892
+ }
1893
+ return {};
1894
+ },
1895
+ async setPlugin(data) {
1896
+ const { setPluginConfig } = await import("./config-2GDU7PCK.js");
1897
+ const { getPluginRegistration: getPluginRegistration2 } = await import("./host-OBOI4MJK.js");
1898
+ const reg = getPluginRegistration2(pluginId);
1899
+ if (reg?.configSchema) {
1900
+ await setPluginConfig(pluginId, data, null);
1901
+ return;
1902
+ }
1903
+ const siteId = await getCurrentSiteId() ?? NP_DEFAULT_SITE_ID;
1904
+ const now = /* @__PURE__ */ new Date();
1905
+ await db().insert(npSettings).values({
1906
+ siteId,
1907
+ key: `plugin.config:${pluginId}`,
1908
+ value: { __npVersion: 1, __npSettings: data },
1909
+ updatedAt: now,
1910
+ updatedBy: null
1911
+ }).onConflictDoUpdate({
1912
+ target: [npSettings.siteId, npSettings.key],
1913
+ set: {
1914
+ value: { __npVersion: 1, __npSettings: data },
1915
+ updatedAt: now,
1916
+ updatedBy: null
1917
+ }
1918
+ });
1919
+ }
1920
+ },
1921
+ theme: {
1922
+ async getTokens() {
1923
+ assertCap(pluginId, capabilities, "theme:read");
1924
+ const siteId = await resolveSettingsSiteId();
1925
+ const rows = await db().select().from(npSettings).where(and(eq2(npSettings.siteId, siteId), eq2(npSettings.key, "theme")));
1926
+ const row = rows[0];
1927
+ if (!row || !row.value || typeof row.value !== "object" || Array.isArray(row.value)) {
1928
+ return {};
1929
+ }
1930
+ return row.value;
1931
+ },
1932
+ async setTokens(partial) {
1933
+ assertCap(pluginId, capabilities, "theme:write");
1934
+ const siteId = await resolveSettingsSiteId();
1935
+ const rows = await db().select().from(npSettings).where(and(eq2(npSettings.siteId, siteId), eq2(npSettings.key, "theme")));
1936
+ const existing = rows[0] && rows[0].value && typeof rows[0].value === "object" && !Array.isArray(rows[0].value) ? rows[0].value : {};
1937
+ const merged = { ...existing, ...partial };
1938
+ await db().insert(npSettings).values({ siteId, key: "theme", value: merged, updatedAt: /* @__PURE__ */ new Date() }).onConflictDoUpdate({
1939
+ target: [npSettings.siteId, npSettings.key],
1940
+ set: { value: merged, updatedAt: /* @__PURE__ */ new Date() }
1941
+ });
1942
+ }
1943
+ },
1944
+ http: {
1945
+ async fetch(url, opts) {
1946
+ assertCap(pluginId, capabilities, "network:fetch");
1947
+ let target;
1948
+ try {
1949
+ target = new URL(url);
1950
+ } catch {
1951
+ throw new NpError(
1952
+ `[plugin:${pluginId}] http.fetch: invalid URL "${url}"`,
1953
+ "INVALID_URL",
1954
+ 400
1955
+ );
1956
+ }
1957
+ const hostMatches = allowedHosts.some((pattern) => {
1958
+ if (pattern === target.hostname) return true;
1959
+ if (pattern.startsWith("*.") && target.hostname.endsWith(pattern.slice(1))) return true;
1960
+ return false;
1961
+ });
1962
+ if (!hostMatches) {
1963
+ throw new NpForbiddenError(
1964
+ `plugin:${pluginId}`,
1965
+ `http.fetch to "${target.hostname}" blocked; add it to manifest.allowedHosts`
1966
+ );
1967
+ }
1968
+ const timeoutMs = opts?.timeoutMs ?? 1e4;
1969
+ const controller = new AbortController();
1970
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
1971
+ try {
1972
+ let body;
1973
+ if (opts?.body !== void 0 && opts.body !== null) {
1974
+ if (typeof opts.body === "string") {
1975
+ body = opts.body;
1976
+ } else if (opts.body instanceof Uint8Array) {
1977
+ body = opts.body;
1978
+ } else {
1979
+ body = JSON.stringify(opts.body);
1980
+ }
1981
+ }
1982
+ const response = await globalThis.fetch(url, {
1983
+ method: opts?.method ?? (body !== void 0 ? "POST" : "GET"),
1984
+ headers: opts?.headers,
1985
+ body,
1986
+ signal: controller.signal
1987
+ });
1988
+ const headers = {};
1989
+ response.headers.forEach((v, k) => {
1990
+ headers[k] = v;
1991
+ });
1992
+ const contentType = response.headers.get("content-type") ?? "";
1993
+ let parsedBody = void 0;
1994
+ if (contentType.includes("application/json")) {
1995
+ parsedBody = await response.json().catch(() => void 0);
1996
+ } else if (contentType.startsWith("text/")) {
1997
+ parsedBody = await response.text();
1998
+ }
1999
+ return {
2000
+ ok: response.ok,
2001
+ status: response.status,
2002
+ headers,
2003
+ body: parsedBody
2004
+ };
2005
+ } finally {
2006
+ clearTimeout(timeout);
2007
+ }
2008
+ }
2009
+ },
2010
+ log: {
2011
+ debug(message, data) {
2012
+ pluginLog.debug(message, data);
2013
+ },
2014
+ info(message, data) {
2015
+ pluginLog.info(message, data);
2016
+ },
2017
+ warn(message, data) {
2018
+ pluginLog.warn(message, data);
2019
+ },
2020
+ error(message, data) {
2021
+ pluginLog.error(message, data);
2022
+ }
2023
+ },
2024
+ errors: {
2025
+ // Plugin-side error reporting with pluginId auto-tagged. The host
2026
+ // already auto-reports thrown hook handlers (in `dispatchHookHandler`),
2027
+ // so plugins typically only need this when *catching* an error
2028
+ // internally — e.g. a non-fatal upstream failure they want to log to
2029
+ // Sentry but recover from.
2030
+ report(error, context) {
2031
+ const err = error instanceof Error ? error : new Error(String(error));
2032
+ return reportError(err, {
2033
+ tags: { source: "plugin", pluginId, ...context?.tags },
2034
+ extra: context?.extra,
2035
+ user: context?.user
2036
+ });
2037
+ }
2038
+ },
2039
+ next: {
2040
+ async revalidatePath(path) {
2041
+ const mod = await loadOptionalNextCache();
2042
+ mod?.revalidatePath?.(path);
2043
+ },
2044
+ async revalidateTag(tag) {
2045
+ const mod = await loadOptionalNextCache();
2046
+ const fn = mod?.revalidateTag;
2047
+ if (typeof fn !== "function") return;
2048
+ if (fn.length >= 2) {
2049
+ fn(tag, "default");
2050
+ } else {
2051
+ fn(tag);
2052
+ }
2053
+ }
2054
+ },
2055
+ actions: {
2056
+ register(actionName, handler) {
2057
+ registration.actions.set(actionName, handler);
2058
+ },
2059
+ async dispatch(targetPluginId, actionName, data) {
2060
+ const target = lookupRegistration(targetPluginId);
2061
+ const action = target?.actions.get(actionName);
2062
+ if (!action) {
2063
+ return {
2064
+ ok: false,
2065
+ error: `Action "${actionName}" not found on plugin "${targetPluginId}"`
2066
+ };
2067
+ }
2068
+ return action(data);
2069
+ }
2070
+ }
2071
+ };
2072
+ }
2073
+
2074
+ // src/plugins/enabled-gate.ts
2075
+ import { eq as eq3 } from "drizzle-orm";
2076
+ var DEFAULT_TTL_MS = 5e3;
2077
+ var cache = /* @__PURE__ */ new Map();
2078
+ var inflight = /* @__PURE__ */ new Map();
2079
+ var generation = /* @__PURE__ */ new Map();
2080
+ var ttlMs = DEFAULT_TTL_MS;
2081
+ function currentGeneration(pluginId) {
2082
+ return generation.get(pluginId) ?? 0;
2083
+ }
2084
+ async function fetchEnabled(pluginId) {
2085
+ if (fetchOverride) return fetchOverride(pluginId);
2086
+ try {
2087
+ const db = getDb();
2088
+ const rows = await db.select({ enabled: npPlugins.enabled }).from(npPlugins).where(eq3(npPlugins.id, pluginId)).limit(1);
2089
+ const row = rows[0];
2090
+ if (row && typeof row.enabled === "boolean") {
2091
+ return row.enabled;
2092
+ }
2093
+ return true;
2094
+ } catch {
2095
+ return true;
2096
+ }
2097
+ }
2098
+ async function isPluginEnabled(pluginId) {
2099
+ const now = Date.now();
2100
+ const cached = cache.get(pluginId);
2101
+ if (cached && cached.expiresAt > now) {
2102
+ return cached.enabled;
2103
+ }
2104
+ const existing = inflight.get(pluginId);
2105
+ if (existing) return existing;
2106
+ const fetchGeneration = currentGeneration(pluginId);
2107
+ const promise = fetchEnabled(pluginId).then((enabled) => {
2108
+ if (currentGeneration(pluginId) === fetchGeneration) {
2109
+ cache.set(pluginId, { enabled, expiresAt: Date.now() + ttlMs });
2110
+ }
2111
+ return enabled;
2112
+ }).finally(() => {
2113
+ if (inflight.get(pluginId) === promise) {
2114
+ inflight.delete(pluginId);
2115
+ }
2116
+ });
2117
+ inflight.set(pluginId, promise);
2118
+ return promise;
2119
+ }
2120
+ function invalidatePluginEnabled(pluginId) {
2121
+ cache.delete(pluginId);
2122
+ inflight.delete(pluginId);
2123
+ generation.set(pluginId, currentGeneration(pluginId) + 1);
2124
+ }
2125
+ var fetchOverride = null;
2126
+
2127
+ // src/plugins/compat.ts
2128
+ var FRAMEWORK_VERSION_FROM_PACKAGE = "0.1.0";
2129
+ var frameworkVersion = FRAMEWORK_VERSION_FROM_PACKAGE;
2130
+ function getFrameworkVersion() {
2131
+ return frameworkVersion;
2132
+ }
2133
+ function parse(version) {
2134
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-.]+))?(?:\+[0-9A-Za-z-.]+)?$/.exec(version);
2135
+ if (!match) return null;
2136
+ const [, majorStr, minorStr, patchStr, prerelease] = match;
2137
+ return {
2138
+ major: Number.parseInt(majorStr ?? "0", 10),
2139
+ minor: Number.parseInt(minorStr ?? "0", 10),
2140
+ patch: Number.parseInt(patchStr ?? "0", 10),
2141
+ prerelease: prerelease ?? null
2142
+ };
2143
+ }
2144
+ function compareSemver(a, b) {
2145
+ const pa = parse(a);
2146
+ const pb = parse(b);
2147
+ if (!pa || !pb) {
2148
+ return a < b ? -1 : a > b ? 1 : 0;
2149
+ }
2150
+ if (pa.major !== pb.major) return pa.major < pb.major ? -1 : 1;
2151
+ if (pa.minor !== pb.minor) return pa.minor < pb.minor ? -1 : 1;
2152
+ if (pa.patch !== pb.patch) return pa.patch < pb.patch ? -1 : 1;
2153
+ if (pa.prerelease === pb.prerelease) return 0;
2154
+ if (pa.prerelease === null) return 1;
2155
+ if (pb.prerelease === null) return -1;
2156
+ return pa.prerelease < pb.prerelease ? -1 : 1;
2157
+ }
2158
+ function checkNexpressCompat(manifest, framework = getFrameworkVersion()) {
2159
+ const min = manifest.nexpress?.minVersion;
2160
+ if (!min) {
2161
+ return { compatible: true };
2162
+ }
2163
+ if (compareSemver(framework, min) < 0) {
2164
+ return {
2165
+ compatible: false,
2166
+ reason: `requires NexPress >= ${min}, host is ${framework}`
2167
+ };
2168
+ }
2169
+ const max = manifest.nexpress?.maxVersion;
2170
+ if (max && compareSemver(framework, max) > 0) {
2171
+ return {
2172
+ compatible: false,
2173
+ reason: `requires NexPress <= ${max}, host is ${framework}`
2174
+ };
2175
+ }
2176
+ return { compatible: true };
2177
+ }
2178
+ function topoSort(plugins) {
2179
+ const skipped = [];
2180
+ let eligible = [...plugins];
2181
+ while (true) {
2182
+ const eligibleIds2 = new Set(eligible.map((p) => p.id));
2183
+ const stillEligible = [];
2184
+ let dropped = false;
2185
+ for (const plugin of eligible) {
2186
+ const missing = plugin.requires.filter((dep) => !eligibleIds2.has(dep));
2187
+ if (missing.length > 0) {
2188
+ skipped.push({
2189
+ id: plugin.id,
2190
+ reason: `missing required plugin(s): ${missing.join(", ")}`
2191
+ });
2192
+ dropped = true;
2193
+ continue;
2194
+ }
2195
+ stillEligible.push(plugin);
2196
+ }
2197
+ eligible = stillEligible;
2198
+ if (!dropped) break;
2199
+ }
2200
+ const eligibleIds = new Set(eligible.map((p) => p.id));
2201
+ const indegree = /* @__PURE__ */ new Map();
2202
+ const dependents = /* @__PURE__ */ new Map();
2203
+ for (const plugin of eligible) {
2204
+ indegree.set(plugin.id, 0);
2205
+ dependents.set(plugin.id, []);
2206
+ }
2207
+ for (const plugin of eligible) {
2208
+ for (const dep of plugin.requires) {
2209
+ if (!eligibleIds.has(dep)) continue;
2210
+ indegree.set(plugin.id, (indegree.get(plugin.id) ?? 0) + 1);
2211
+ dependents.get(dep).push(plugin.id);
2212
+ }
2213
+ }
2214
+ const queue = eligible.filter((p) => (indegree.get(p.id) ?? 0) === 0);
2215
+ const ordered = [];
2216
+ const byId = new Map(eligible.map((p) => [p.id, p]));
2217
+ while (queue.length > 0) {
2218
+ const next = queue.shift();
2219
+ ordered.push(next);
2220
+ for (const dependent of dependents.get(next.id) ?? []) {
2221
+ const updated = (indegree.get(dependent) ?? 0) - 1;
2222
+ indegree.set(dependent, updated);
2223
+ if (updated === 0) {
2224
+ const plugin = byId.get(dependent);
2225
+ if (plugin) queue.push(plugin);
2226
+ }
2227
+ }
2228
+ }
2229
+ if (ordered.length !== eligible.length) {
2230
+ for (const plugin of eligible) {
2231
+ if (!ordered.includes(plugin)) {
2232
+ skipped.push({
2233
+ id: plugin.id,
2234
+ reason: "dependency cycle \u2014 refusing to load"
2235
+ });
2236
+ }
2237
+ }
2238
+ }
2239
+ return { ordered, skipped };
2240
+ }
2241
+
2242
+ // src/plugins/host.ts
2243
+ function hookCapabilityFor(hookName) {
2244
+ const namespace = hookName.split(":")[0];
2245
+ if (!namespace) return null;
2246
+ return `hooks:${namespace}`;
2247
+ }
2248
+ function assertCapability(pluginId, requirement, declared) {
2249
+ if (declared.includes(requirement)) return;
2250
+ throw new Error(
2251
+ `[plugin:${pluginId}] declares capabilities ${JSON.stringify(declared)} but is registering something that requires "${requirement}". Add "${requirement}" to the plugin manifest's capabilities array.`
2252
+ );
2253
+ }
2254
+ var pluginRegistry = /* @__PURE__ */ new Map();
2255
+ var globalHooks = /* @__PURE__ */ new Map();
2256
+ var globalRoutes = [];
2257
+ var DEFAULT_HOOK_PRIORITY = 100;
2258
+ function normalizeHookValue(value) {
2259
+ if (typeof value === "function") {
2260
+ return { handler: value, priority: DEFAULT_HOOK_PRIORITY };
2261
+ }
2262
+ if (value && typeof value === "object") {
2263
+ const v = value;
2264
+ if (typeof v.handler !== "function") return null;
2265
+ return {
2266
+ handler: v.handler,
2267
+ priority: typeof v.priority === "number" ? v.priority : DEFAULT_HOOK_PRIORITY,
2268
+ timeoutMs: typeof v.timeoutMs === "number" && v.timeoutMs > 0 ? v.timeoutMs : void 0
2269
+ };
2270
+ }
2271
+ return null;
2272
+ }
2273
+ function insertSortedByPriority(list, entry) {
2274
+ list.push(entry);
2275
+ list.sort((a, b) => a.priority - b.priority);
2276
+ }
2277
+ async function loadPluginConfig(pluginId) {
2278
+ const { getPluginConfig } = await import("./config-2GDU7PCK.js");
2279
+ const value = await getPluginConfig(pluginId);
2280
+ if (value && typeof value === "object" && !Array.isArray(value)) {
2281
+ return value;
2282
+ }
2283
+ return {};
2284
+ }
2285
+ async function buildCtxFor(pluginId) {
2286
+ const registration = pluginRegistry.get(pluginId);
2287
+ if (!registration) {
2288
+ throw new Error(`[plugin:${pluginId}] attempted to build ctx before registration.`);
2289
+ }
2290
+ const config = await loadPluginConfig(pluginId);
2291
+ return createPluginRuntimeContext({
2292
+ pluginId,
2293
+ capabilities: registration.capabilities,
2294
+ allowedHosts: registration.allowedHosts,
2295
+ config,
2296
+ registration,
2297
+ lookupRegistration: (id) => pluginRegistry.get(id)
2298
+ });
2299
+ }
2300
+ function isResolvedPlugin(value) {
2301
+ if (!value || typeof value !== "object") return false;
2302
+ const candidate = value;
2303
+ if (!candidate.manifest || typeof candidate.manifest !== "object") return false;
2304
+ const manifest = candidate.manifest;
2305
+ return typeof manifest.id === "string" && Array.isArray(manifest.capabilities);
2306
+ }
2307
+ function registerHookHandler(registration, hookName, handler) {
2308
+ if (!registration.hooks.has(hookName)) {
2309
+ registration.hooks.set(hookName, []);
2310
+ }
2311
+ insertSortedByPriority(registration.hooks.get(hookName), handler);
2312
+ if (!globalHooks.has(hookName)) {
2313
+ globalHooks.set(hookName, []);
2314
+ }
2315
+ insertSortedByPriority(globalHooks.get(hookName), handler);
2316
+ }
2317
+ function createPluginContext(pluginId, registration) {
2318
+ return {
2319
+ addCollection: () => {
2320
+ throw new Error(
2321
+ `[plugin:${pluginId}] Runtime collection registration not supported in v1. Add collections to nexpress.config.ts.`
2322
+ );
2323
+ },
2324
+ addBlock: () => {
2325
+ throw new Error(
2326
+ `[plugin:${pluginId}] Runtime block registration not supported in v1. Add blocks to nexpress.config.ts.`
2327
+ );
2328
+ },
2329
+ addHook: (collection, event, hook) => {
2330
+ const hookName = `content:${event}`;
2331
+ const requirement = hookCapabilityFor(hookName);
2332
+ if (requirement) {
2333
+ assertCapability(pluginId, requirement, registration.capabilities);
2334
+ }
2335
+ registerHookHandler(registration, hookName, {
2336
+ pluginId,
2337
+ priority: DEFAULT_HOOK_PRIORITY,
2338
+ handler: async (data) => {
2339
+ if (typeof data.collection === "string" && data.collection !== collection) {
2340
+ return;
2341
+ }
2342
+ await hook({ data, collection });
2343
+ }
2344
+ });
2345
+ }
2346
+ };
2347
+ }
2348
+ async function loadResolvedPlugin(plugin) {
2349
+ const { manifest } = plugin;
2350
+ const previous = pluginRegistry.get(manifest.id);
2351
+ if (previous) {
2352
+ for (const [hookName, list] of previous.hooks) {
2353
+ const global = globalHooks.get(hookName);
2354
+ if (!global) continue;
2355
+ const filtered = global.filter((h) => !list.includes(h));
2356
+ if (filtered.length === 0) globalHooks.delete(hookName);
2357
+ else globalHooks.set(hookName, filtered);
2358
+ }
2359
+ for (const route of previous.routes) {
2360
+ const idx = globalRoutes.indexOf(route);
2361
+ if (idx !== -1) globalRoutes.splice(idx, 1);
2362
+ }
2363
+ }
2364
+ const registration = {
2365
+ id: manifest.id,
2366
+ name: manifest.name,
2367
+ version: manifest.version,
2368
+ description: manifest.description,
2369
+ capabilities: [...manifest.capabilities],
2370
+ allowedHosts: [...manifest.allowedHosts ?? []],
2371
+ admin: plugin.admin,
2372
+ hooks: /* @__PURE__ */ new Map(),
2373
+ routes: [],
2374
+ actions: /* @__PURE__ */ new Map(),
2375
+ schedules: /* @__PURE__ */ new Map(),
2376
+ configSchema: plugin.configSchema,
2377
+ configVersion: plugin.configVersion,
2378
+ configMigrate: plugin.configMigrate,
2379
+ pageRoutes: normalizePageRoutes(plugin)
2380
+ };
2381
+ pluginRegistry.set(manifest.id, registration);
2382
+ if (registration.configSchema !== void 0 && plugin.admin?.settings?.fields && plugin.admin.settings.fields.length > 0) {
2383
+ getLogger().warn("Plugin declares both configSchema and admin.settings.fields", {
2384
+ pluginId: manifest.id,
2385
+ note: "Auto-form wins; admin.settings.fields is ignored at render time. Remove admin.settings.fields when migrating to configSchema."
2386
+ });
2387
+ }
2388
+ const scheduledRaw = plugin.scheduled;
2389
+ if (Array.isArray(scheduledRaw)) {
2390
+ for (const entry of scheduledRaw) {
2391
+ if (!entry || typeof entry !== "object") continue;
2392
+ const e = entry;
2393
+ if (typeof e.id !== "string" || e.id.length === 0) continue;
2394
+ if (typeof e.cron !== "string" || e.cron.length === 0) continue;
2395
+ if (typeof e.handler !== "function") continue;
2396
+ registration.schedules.set(e.id, {
2397
+ pluginId: manifest.id,
2398
+ taskId: e.id,
2399
+ cron: e.cron,
2400
+ description: typeof e.description === "string" ? e.description : void 0,
2401
+ handler: e.handler
2402
+ });
2403
+ }
2404
+ }
2405
+ for (const [hookName, rawValue] of Object.entries(plugin.hooks ?? {})) {
2406
+ const normalized = normalizeHookValue(rawValue);
2407
+ if (!normalized) continue;
2408
+ const requirement = hookCapabilityFor(hookName);
2409
+ if (requirement) {
2410
+ assertCapability(manifest.id, requirement, registration.capabilities);
2411
+ }
2412
+ const userHandler = normalized.handler;
2413
+ registerHookHandler(registration, hookName, {
2414
+ pluginId: manifest.id,
2415
+ priority: normalized.priority,
2416
+ timeoutMs: normalized.timeoutMs,
2417
+ handler: async (data) => {
2418
+ const collection = typeof data.collection === "string" ? data.collection : void 0;
2419
+ const ctx = await buildCtxFor(manifest.id);
2420
+ return await userHandler({ hook: hookName, data, collection, ctx });
2421
+ }
2422
+ });
2423
+ }
2424
+ for (const route of plugin.routes ?? []) {
2425
+ if (typeof route.handler !== "function") continue;
2426
+ assertCapability(manifest.id, "api:route", registration.capabilities);
2427
+ const userHandler = route.handler;
2428
+ const wrapped = async (req) => {
2429
+ const ctx = await buildCtxFor(manifest.id);
2430
+ return userHandler(req, ctx);
2431
+ };
2432
+ const auth = route.auth === true;
2433
+ const method = route.method.toUpperCase();
2434
+ if (!auth && method !== "GET" && method !== "HEAD" && method !== "OPTIONS") {
2435
+ getLogger().warn("Plugin registered a public mutating route", {
2436
+ pluginId: manifest.id,
2437
+ path: route.path,
2438
+ method,
2439
+ note: "Plugins are responsible for their own auth on `auth: false` routes. The framework rate-limits the plugin catch-all to 30 req/min/IP; verify the handler enforces signature / token checks before mutating state."
2440
+ });
2441
+ }
2442
+ const entry = {
2443
+ pluginId: manifest.id,
2444
+ path: route.path,
2445
+ method,
2446
+ auth,
2447
+ handler: wrapped
2448
+ };
2449
+ registration.routes.push(entry);
2450
+ globalRoutes.push(entry);
2451
+ }
2452
+ const i18nBundles = plugin.i18n;
2453
+ if (i18nBundles && typeof i18nBundles === "object") {
2454
+ const { addStrings } = await import("./strings-VAE47B2C.js");
2455
+ for (const [locale, bundle] of Object.entries(i18nBundles)) {
2456
+ if (bundle && typeof bundle === "object") {
2457
+ addStrings(locale, bundle);
2458
+ }
2459
+ }
2460
+ }
2461
+ const pluginTemplates = plugin.templates;
2462
+ if (pluginTemplates && typeof pluginTemplates === "object") {
2463
+ const { registerPluginTemplates } = await import("./templates-IFVJMCJ6.js");
2464
+ registerPluginTemplates(manifest.id, pluginTemplates);
2465
+ }
2466
+ const setup = plugin.setup;
2467
+ if (typeof setup === "function") {
2468
+ const ctx = await buildCtxFor(manifest.id);
2469
+ await setup(ctx);
2470
+ }
2471
+ }
2472
+ async function loadLegacyPlugin(plugin) {
2473
+ const registration = {
2474
+ id: plugin.id,
2475
+ name: plugin.name,
2476
+ capabilities: ["hooks:content"],
2477
+ allowedHosts: [],
2478
+ hooks: /* @__PURE__ */ new Map(),
2479
+ routes: [],
2480
+ actions: /* @__PURE__ */ new Map(),
2481
+ schedules: /* @__PURE__ */ new Map(),
2482
+ // Legacy `init()` plugins predate the page-routes contract;
2483
+ // they always register zero routes. Kept as a literal `[]` so
2484
+ // the registration shape is consistent across the two paths
2485
+ // and `getPluginPageRoutes()` doesn't need to special-case
2486
+ // legacy entries.
2487
+ pageRoutes: []
2488
+ };
2489
+ pluginRegistry.set(plugin.id, registration);
2490
+ if (plugin.init) {
2491
+ const ctx = createPluginContext(plugin.id, registration);
2492
+ await plugin.init(ctx);
2493
+ }
2494
+ }
2495
+ async function loadPlugins(plugins) {
2496
+ const filtered = [];
2497
+ for (const plugin of plugins) {
2498
+ if (isResolvedPlugin(plugin)) {
2499
+ const compat = checkNexpressCompat(plugin.manifest);
2500
+ if (!compat.compatible) {
2501
+ getLogger().warn("Skipping incompatible plugin", {
2502
+ pluginId: plugin.manifest.id,
2503
+ reason: compat.reason
2504
+ });
2505
+ continue;
2506
+ }
2507
+ }
2508
+ filtered.push(plugin);
2509
+ }
2510
+ const legacy = [];
2511
+ const resolved = [];
2512
+ for (const plugin of filtered) {
2513
+ if (isResolvedPlugin(plugin)) {
2514
+ resolved.push(plugin);
2515
+ } else {
2516
+ legacy.push(plugin);
2517
+ }
2518
+ }
2519
+ const sortInput = resolved.map((plugin) => ({
2520
+ id: plugin.manifest.id,
2521
+ requires: plugin.manifest.requires ?? [],
2522
+ plugin
2523
+ }));
2524
+ const { ordered, skipped } = topoSort(sortInput);
2525
+ for (const entry of skipped) {
2526
+ getLogger().warn("Skipping plugin with unsatisfied dependency", {
2527
+ pluginId: entry.id,
2528
+ reason: entry.reason
2529
+ });
2530
+ }
2531
+ for (const plugin of legacy) {
2532
+ try {
2533
+ await loadLegacyPlugin(plugin);
2534
+ } catch (err) {
2535
+ pluginRegistry.delete(plugin.id);
2536
+ getLogger().error("Plugin failed to load \u2014 skipped", {
2537
+ pluginId: plugin.id,
2538
+ error: err instanceof Error ? err.message : String(err)
2539
+ });
2540
+ }
2541
+ }
2542
+ for (const entry of ordered) {
2543
+ try {
2544
+ await loadResolvedPlugin(entry.plugin);
2545
+ } catch (err) {
2546
+ const pluginId = entry.plugin.manifest.id;
2547
+ pluginRegistry.delete(pluginId);
2548
+ getLogger().error("Plugin failed to load \u2014 skipped", {
2549
+ pluginId,
2550
+ error: err instanceof Error ? err.message : String(err)
2551
+ });
2552
+ }
2553
+ }
2554
+ }
2555
+ async function dispatchHookHandler(hookName, handler, data) {
2556
+ try {
2557
+ const result = handler.handler(data);
2558
+ if (handler.timeoutMs === void 0 || !(result instanceof Promise)) {
2559
+ const value = await result;
2560
+ return { ok: true, value };
2561
+ }
2562
+ let timer;
2563
+ const timeoutPromise = new Promise((_, reject) => {
2564
+ timer = setTimeout(() => {
2565
+ reject(
2566
+ new Error(
2567
+ `Plugin hook handler timed out after ${handler.timeoutMs}ms`
2568
+ )
2569
+ );
2570
+ }, handler.timeoutMs);
2571
+ });
2572
+ try {
2573
+ const value = await Promise.race([result, timeoutPromise]);
2574
+ return { ok: true, value };
2575
+ } finally {
2576
+ if (timer) clearTimeout(timer);
2577
+ }
2578
+ } catch (error) {
2579
+ const err = error instanceof Error ? error : new Error(String(error));
2580
+ getLogger().error("Plugin hook handler threw", {
2581
+ pluginId: handler.pluginId,
2582
+ hook: hookName,
2583
+ timeoutMs: handler.timeoutMs,
2584
+ message: err.message,
2585
+ stack: err.stack
2586
+ });
2587
+ void reportError(err, {
2588
+ tags: { source: "plugin-hook", pluginId: handler.pluginId, hook: hookName }
2589
+ });
2590
+ return { ok: false };
2591
+ }
2592
+ }
2593
+ async function runHook(hookName, data) {
2594
+ const handlers = globalHooks.get(hookName);
2595
+ if (!handlers || handlers.length === 0) return;
2596
+ for (const handler of handlers) {
2597
+ if (!await isPluginEnabled(handler.pluginId)) continue;
2598
+ await dispatchHookHandler(hookName, handler, data);
2599
+ }
2600
+ }
2601
+ async function runHookAndCollect(hookName, data) {
2602
+ const handlers = globalHooks.get(hookName);
2603
+ if (!handlers || handlers.length === 0) return [];
2604
+ const results = [];
2605
+ for (const handler of handlers) {
2606
+ if (!await isPluginEnabled(handler.pluginId)) continue;
2607
+ const outcome = await dispatchHookHandler(hookName, handler, data);
2608
+ if (outcome.ok && outcome.value !== void 0 && outcome.value !== null) {
2609
+ results.push(outcome.value);
2610
+ }
2611
+ }
2612
+ return results;
2613
+ }
2614
+ function getPluginRoutes() {
2615
+ return globalRoutes;
2616
+ }
2617
+ function getPluginPageRoutes() {
2618
+ const out = [];
2619
+ for (const [pluginId, registration] of pluginRegistry) {
2620
+ for (const route of registration.pageRoutes) {
2621
+ out.push({ pluginId, route });
2622
+ }
2623
+ }
2624
+ return out;
2625
+ }
2626
+ function normalizePageRoutes(plugin) {
2627
+ const raw = plugin.pageRoutes;
2628
+ if (!Array.isArray(raw)) return [];
2629
+ const out = [];
2630
+ for (const entry of raw) {
2631
+ if (!entry || typeof entry !== "object") continue;
2632
+ const r = entry;
2633
+ if (typeof r.pattern !== "string" || r.pattern.length === 0) continue;
2634
+ if (typeof r.component !== "function") {
2635
+ if (typeof r.component !== "object" || r.component === null) continue;
2636
+ }
2637
+ out.push({
2638
+ pattern: r.pattern,
2639
+ component: r.component,
2640
+ metadata: r.metadata,
2641
+ surface: r.surface === "member" ? "member" : "site",
2642
+ locale: r.locale === "none" ? "none" : "auto"
2643
+ });
2644
+ }
2645
+ return out;
2646
+ }
2647
+ function getPluginRegistration(pluginId) {
2648
+ return pluginRegistry.get(pluginId);
2649
+ }
2650
+ function getAllPluginIds() {
2651
+ return [...pluginRegistry.keys()];
2652
+ }
2653
+ function getPluginAdminExtension(pluginId) {
2654
+ return pluginRegistry.get(pluginId)?.admin;
2655
+ }
2656
+ function getCollectionTabsForSlug(collectionSlug) {
2657
+ const result = [];
2658
+ for (const registration of pluginRegistry.values()) {
2659
+ const tabs = registration.admin?.collectionTabs;
2660
+ if (!tabs || tabs.length === 0) continue;
2661
+ for (const tab of tabs) {
2662
+ const matches = tab.collections === "*" || Array.isArray(tab.collections) && tab.collections.includes(collectionSlug);
2663
+ if (!matches) continue;
2664
+ result.push({
2665
+ pluginId: registration.id,
2666
+ pluginName: registration.name,
2667
+ id: tab.id,
2668
+ label: tab.label,
2669
+ widgets: tab.widgets,
2670
+ actions: tab.actions,
2671
+ description: tab.description
2672
+ });
2673
+ }
2674
+ }
2675
+ return result;
2676
+ }
2677
+ function getDashboardWidgetsFromPlugins() {
2678
+ const result = [];
2679
+ for (const registration of pluginRegistry.values()) {
2680
+ const widgets = registration.admin?.dashboardWidgets;
2681
+ if (!widgets || widgets.length === 0) continue;
2682
+ for (const widget of widgets) {
2683
+ result.push({
2684
+ pluginId: registration.id,
2685
+ pluginName: registration.name,
2686
+ id: widget.id,
2687
+ label: widget.label,
2688
+ kind: widget.kind,
2689
+ actionId: widget.actionId,
2690
+ description: widget.description,
2691
+ priority: widget.priority
2692
+ });
2693
+ }
2694
+ }
2695
+ return result.map((widget, index) => ({ widget, index })).sort((a, b) => {
2696
+ const ap = a.widget.priority ?? Number.POSITIVE_INFINITY;
2697
+ const bp = b.widget.priority ?? Number.POSITIVE_INFINITY;
2698
+ if (ap !== bp) return ap - bp;
2699
+ return a.index - b.index;
2700
+ }).map(({ widget }) => widget);
2701
+ }
2702
+ async function dispatchPluginAction(pluginId, actionId, data) {
2703
+ const registration = pluginRegistry.get(pluginId);
2704
+ if (!registration) {
2705
+ return { ok: false, error: `Plugin "${pluginId}" is not registered` };
2706
+ }
2707
+ if (!await isPluginEnabled(pluginId)) {
2708
+ return { ok: false, error: `Plugin "${pluginId}" is disabled` };
2709
+ }
2710
+ const handler = registration.actions.get(actionId);
2711
+ if (!handler) {
2712
+ return { ok: false, error: `Action "${actionId}" not found on plugin "${pluginId}"` };
2713
+ }
2714
+ return handler(data);
2715
+ }
2716
+ async function schedulePluginTask(pluginId, taskId) {
2717
+ const { enqueueJob: enqueueJob2 } = await import("./queue-XE5BC75T.js");
2718
+ await enqueueJob2("plugin:scheduledTask", { pluginId, taskId });
2719
+ }
2720
+ function getRegisteredPluginSchedules() {
2721
+ const out = [];
2722
+ for (const reg of pluginRegistry.values()) {
2723
+ for (const schedule of reg.schedules.values()) {
2724
+ out.push(schedule);
2725
+ }
2726
+ }
2727
+ return out;
2728
+ }
2729
+ async function runPluginScheduledTask(pluginId, taskId) {
2730
+ const registration = pluginRegistry.get(pluginId);
2731
+ if (!registration) {
2732
+ throw new Error(`Plugin "${pluginId}" is not registered`);
2733
+ }
2734
+ if (!await isPluginEnabled(pluginId)) {
2735
+ getLogger().debug("Skipping plugin scheduled task \u2014 plugin disabled", {
2736
+ pluginId,
2737
+ taskId
2738
+ });
2739
+ return;
2740
+ }
2741
+ const entry = registration.schedules.get(taskId);
2742
+ if (!entry) {
2743
+ throw new Error(`Plugin "${pluginId}" has no scheduled task with id "${taskId}"`);
2744
+ }
2745
+ const ctx = await buildCtxFor(pluginId);
2746
+ await entry.handler(ctx);
2747
+ }
2748
+ function resetPlugins() {
2749
+ pluginRegistry.clear();
2750
+ globalHooks.clear();
2751
+ globalRoutes.length = 0;
2752
+ }
2753
+
2754
+ export {
2755
+ buildSearchVector,
2756
+ buildSearchVectorParts,
2757
+ buildWeightedSearchVectorSql,
2758
+ buildZodSchema,
2759
+ getCollectionZodSchema,
2760
+ isPluginEnabled,
2761
+ invalidatePluginEnabled,
2762
+ getFrameworkVersion,
2763
+ compareSemver,
2764
+ checkNexpressCompat,
2765
+ loadPlugins,
2766
+ runHook,
2767
+ runHookAndCollect,
2768
+ getPluginRoutes,
2769
+ getPluginPageRoutes,
2770
+ getPluginRegistration,
2771
+ getAllPluginIds,
2772
+ getPluginAdminExtension,
2773
+ getCollectionTabsForSlug,
2774
+ getDashboardWidgetsFromPlugins,
2775
+ dispatchPluginAction,
2776
+ schedulePluginTask,
2777
+ getRegisteredPluginSchedules,
2778
+ runPluginScheduledTask,
2779
+ resetPlugins,
2780
+ saveDocument,
2781
+ updateMemberDocument,
2782
+ createMemberDocument,
2783
+ autosaveRevision,
2784
+ deleteDocument,
2785
+ deleteMemberDocument,
2786
+ promoteMemberDocument,
2787
+ findDocuments,
2788
+ getDocumentById
2789
+ };
2790
+ //# sourceMappingURL=chunk-VGTPQXNQ.js.map