@azlib/cms 0.6.0 → 0.7.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.
package/dist/index.mjs CHANGED
@@ -1,3 +1,7 @@
1
+ import { isEmail, isURL } from "@azlib/validator";
2
+ import { promises } from "node:fs";
3
+ import * as path from "node:path";
4
+ import { and, column, createTable, eq, generateCreateTableDdl, inArray, like, or } from "@azlib/persistence";
1
5
  import ExcelJS from "exceljs";
2
6
  //#region src/content/schema.ts
3
7
  const fields = {
@@ -14,7 +18,10 @@ const fields = {
14
18
  label: options.label ?? "Slug",
15
19
  fromField: options.from ?? "title",
16
20
  required: options.required ?? false,
17
- unique: options.unique ?? true
21
+ unique: options.unique ?? true,
22
+ readRoles: options.readRoles,
23
+ writeRoles: options.writeRoles,
24
+ localized: options.localized
18
25
  };
19
26
  },
20
27
  richText(options) {
@@ -77,6 +84,31 @@ const fields = {
77
84
  type: "repeater",
78
85
  ...options
79
86
  };
87
+ },
88
+ blocks(options) {
89
+ return {
90
+ type: "blocks",
91
+ ...options
92
+ };
93
+ },
94
+ email(options) {
95
+ return {
96
+ type: "email",
97
+ ...options
98
+ };
99
+ },
100
+ url(options) {
101
+ return {
102
+ type: "url",
103
+ ...options
104
+ };
105
+ },
106
+ array(options) {
107
+ return {
108
+ type: "array",
109
+ defaultValue: [],
110
+ ...options
111
+ };
80
112
  }
81
113
  };
82
114
  /**
@@ -109,19 +141,121 @@ function validateAndNormalizeData(fieldsList, inputData) {
109
141
  for (const field of fieldsList) {
110
142
  let val = normalized[field.name];
111
143
  if (val === void 0 && field.defaultValue !== void 0) {
112
- val = field.defaultValue;
144
+ val = typeof field.defaultValue === "object" && field.defaultValue !== null ? JSON.parse(JSON.stringify(field.defaultValue)) : field.defaultValue;
113
145
  normalized[field.name] = val;
114
146
  }
115
- if (field.required && (val === void 0 || val === null || val === "")) {
147
+ const isBlank = val === void 0 || val === null || val === "" || Array.isArray(val) && val.length === 0;
148
+ if (field.required && isBlank) {
116
149
  errors[field.name] = `${field.label || field.name} is required.`;
117
150
  continue;
118
151
  }
119
- if (val !== void 0 && val !== null) {
120
- if (field.type === "number" && typeof val !== "number") {
121
- const parsed = Number(val);
122
- if (Number.isNaN(parsed)) errors[field.name] = `${field.label || field.name} must be a valid number.`;
123
- else normalized[field.name] = parsed;
124
- } else if (field.type === "boolean" && typeof val !== "boolean") normalized[field.name] = Boolean(val);
152
+ if (val !== void 0 && val !== null && val !== "") switch (field.type) {
153
+ case "number": {
154
+ let num = val;
155
+ if (typeof num !== "number") {
156
+ const parsed = Number(num);
157
+ if (Number.isNaN(parsed)) {
158
+ errors[field.name] = `${field.label || field.name} must be a valid number.`;
159
+ break;
160
+ }
161
+ num = parsed;
162
+ normalized[field.name] = num;
163
+ }
164
+ if (field.min !== void 0 && num < field.min) errors[field.name] = `${field.label || field.name} must be at least ${field.min}.`;
165
+ if (field.max !== void 0 && num > field.max) errors[field.name] = `${field.label || field.name} must be at most ${field.max}.`;
166
+ break;
167
+ }
168
+ case "boolean":
169
+ if (typeof val !== "boolean") normalized[field.name] = Boolean(val);
170
+ break;
171
+ case "text":
172
+ case "slug":
173
+ case "richText": {
174
+ const strVal = String(val);
175
+ if (field.min !== void 0 && strVal.length < field.min) errors[field.name] = `${field.label || field.name} must have at least ${field.min} characters.`;
176
+ if (field.max !== void 0 && strVal.length > field.max) errors[field.name] = `${field.label || field.name} must have at most ${field.max} characters.`;
177
+ if (field.pattern) try {
178
+ if (!new RegExp(field.pattern).test(strVal)) errors[field.name] = `${field.label || field.name} format is invalid.`;
179
+ } catch {}
180
+ break;
181
+ }
182
+ case "email": {
183
+ const emailStr = String(val).trim();
184
+ if (!isEmail(emailStr)) errors[field.name] = `${field.label || field.name} must be a valid email address.`;
185
+ else normalized[field.name] = emailStr.toLowerCase();
186
+ break;
187
+ }
188
+ case "url": {
189
+ const urlStr = String(val).trim();
190
+ if (!isURL(urlStr)) errors[field.name] = `${field.label || field.name} must be a valid URL.`;
191
+ else normalized[field.name] = urlStr;
192
+ break;
193
+ }
194
+ case "select":
195
+ if (field.options && field.options.length > 0) {
196
+ const validValues = field.options.map((opt) => typeof opt === "object" && opt !== null ? opt.value : opt);
197
+ if (!validValues.includes(val)) errors[field.name] = `${field.label || field.name} must be one of: ${validValues.join(", ")}.`;
198
+ }
199
+ break;
200
+ case "repeater":
201
+ if (!Array.isArray(val)) {
202
+ errors[field.name] = `${field.label || field.name} must be an array.`;
203
+ break;
204
+ }
205
+ if (field.min !== void 0 && val.length < field.min) errors[field.name] = `${field.label || field.name} must have at least ${field.min} items.`;
206
+ if (field.max !== void 0 && val.length > field.max) errors[field.name] = `${field.label || field.name} must have at most ${field.max} items.`;
207
+ if (field.fields && field.fields.length > 0) {
208
+ const normalizedItems = [];
209
+ for (let i = 0; i < val.length; i++) {
210
+ const item = val[i];
211
+ if (typeof item !== "object" || item === null) {
212
+ errors[`${field.name}[${i}]`] = `Item must be an object.`;
213
+ continue;
214
+ }
215
+ const { data: itemData, errors: itemErrors } = validateAndNormalizeData(field.fields, item);
216
+ for (const [errKey, errMsg] of Object.entries(itemErrors)) errors[`${field.name}[${i}].${errKey}`] = errMsg;
217
+ normalizedItems.push(itemData);
218
+ }
219
+ normalized[field.name] = normalizedItems;
220
+ }
221
+ break;
222
+ case "blocks": {
223
+ if (!Array.isArray(val)) {
224
+ errors[field.name] = `${field.label || field.name} must be an array of blocks.`;
225
+ break;
226
+ }
227
+ if (field.min !== void 0 && val.length < field.min) errors[field.name] = `${field.label || field.name} must have at least ${field.min} blocks.`;
228
+ if (field.max !== void 0 && val.length > field.max) errors[field.name] = `${field.label || field.name} must have at most ${field.max} blocks.`;
229
+ const normalizedBlocks = [];
230
+ for (let i = 0; i < val.length; i++) {
231
+ const block = val[i];
232
+ if (typeof block !== "object" || block === null) {
233
+ errors[`${field.name}[${i}]`] = `Block must be an object.`;
234
+ continue;
235
+ }
236
+ const blockType = block.blockType;
237
+ if (typeof blockType !== "string" || !blockType) {
238
+ errors[`${field.name}[${i}].blockType`] = `Block missing 'blockType' identifier.`;
239
+ continue;
240
+ }
241
+ const blockDef = field.blocks?.find((b) => b.slug === blockType);
242
+ if (!blockDef) {
243
+ errors[`${field.name}[${i}].blockType`] = `Unknown block type '${blockType}'.`;
244
+ continue;
245
+ }
246
+ const { data: blockData, errors: blockErrors } = validateAndNormalizeData(blockDef.fields, block);
247
+ for (const [errKey, errMsg] of Object.entries(blockErrors)) errors[`${field.name}[${i}].${errKey}`] = errMsg;
248
+ normalizedBlocks.push({
249
+ ...blockData,
250
+ blockType
251
+ });
252
+ }
253
+ normalized[field.name] = normalizedBlocks;
254
+ break;
255
+ }
256
+ case "array":
257
+ if (!Array.isArray(val)) errors[field.name] = `${field.label || field.name} must be an array.`;
258
+ break;
125
259
  }
126
260
  if (field.validate && val !== void 0) {
127
261
  const res = field.validate(val, normalized);
@@ -235,7 +369,11 @@ function normalizeConfig(config) {
235
369
  route: config.admin?.route ?? "/admin",
236
370
  enableRegistration: config.admin?.enableRegistration ?? false,
237
371
  ...config.admin
238
- }
372
+ },
373
+ auth: config.auth,
374
+ webhooks: config.webhooks ?? [],
375
+ media: config.media,
376
+ i18n: config.i18n
239
377
  };
240
378
  }
241
379
  //#endregion
@@ -778,17 +916,21 @@ var MediaManager = class {
778
916
  storage;
779
917
  hooks;
780
918
  publicBaseUrl;
919
+ driver;
781
920
  allowedMimePrefixes = [
782
921
  "image/",
783
922
  "video/",
784
923
  "audio/",
785
924
  "application/pdf",
786
- "text/"
925
+ "text/",
926
+ "application/msword",
927
+ "application/vnd."
787
928
  ];
788
- constructor(storage, hooks, publicBaseUrl = "/uploads") {
929
+ constructor(storage, hooks, publicBaseUrl = "/uploads", driver) {
789
930
  this.storage = storage;
790
931
  this.hooks = hooks;
791
932
  this.publicBaseUrl = publicBaseUrl;
933
+ this.driver = driver;
792
934
  }
793
935
  /**
794
936
  * Upload / register a new media item.
@@ -798,13 +940,26 @@ var MediaManager = class {
798
940
  const extMatch = input.filename.match(/\.([a-zA-Z0-9]+)$/);
799
941
  const ext = extMatch ? `.${extMatch[1].toLowerCase()}` : "";
800
942
  const cleanFilename = `${slugify(input.filename.replace(/\.[^/.]+$/, ""))}${ext}`;
801
- const url = input.url ?? `${this.publicBaseUrl}/${cleanFilename}`;
943
+ let url = input.url ?? `${this.publicBaseUrl}/${cleanFilename}`;
944
+ let path = input.path;
945
+ let sizeBytes = input.sizeBytes ?? (input.buffer ? input.buffer.byteLength : 0);
946
+ if (input.buffer && this.driver) {
947
+ const written = await this.driver.write({
948
+ filename: cleanFilename,
949
+ buffer: input.buffer,
950
+ mimeType: input.mimeType
951
+ });
952
+ url = written.url;
953
+ path = written.path;
954
+ sizeBytes = written.sizeBytes;
955
+ }
802
956
  const media = await this.storage.createMedia({
803
957
  filename: cleanFilename,
804
958
  originalName: input.filename,
805
959
  mimeType: input.mimeType,
806
- sizeBytes: input.sizeBytes,
960
+ sizeBytes,
807
961
  url,
962
+ path,
808
963
  width: input.width,
809
964
  height: input.height,
810
965
  altText: input.altText,
@@ -842,7 +997,10 @@ var MediaManager = class {
842
997
  const media = await this.storage.getMedia(id);
843
998
  if (!media) return false;
844
999
  const deleted = await this.storage.deleteMedia(id);
845
- if (deleted && this.hooks) await this.hooks.doAction("cms.media_deleted", media);
1000
+ if (deleted) {
1001
+ if (media.path && this.driver) await this.driver.delete(media.path);
1002
+ if (this.hooks) await this.hooks.doAction("cms.media_deleted", media);
1003
+ }
846
1004
  return deleted;
847
1005
  }
848
1006
  };
@@ -935,10 +1093,481 @@ var RBACManager = class {
935
1093
  }
936
1094
  };
937
1095
  //#endregion
1096
+ //#region src/auth/jwt.ts
1097
+ /**
1098
+ * @azlib/cms - Universal Web Crypto JWT & Password Hashing
1099
+ */
1100
+ function base64UrlEncode(buffer) {
1101
+ let binary = "";
1102
+ for (let i = 0; i < buffer.byteLength; i++) binary += String.fromCharCode(buffer[i]);
1103
+ return btoa(binary).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
1104
+ }
1105
+ function base64UrlDecode(str) {
1106
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
1107
+ while (base64.length % 4) base64 += "=";
1108
+ const binary = atob(base64);
1109
+ const bytes = new Uint8Array(binary.length);
1110
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
1111
+ return bytes;
1112
+ }
1113
+ async function signJwt(payload, secret, expiresInSeconds = 86400) {
1114
+ const header = {
1115
+ alg: "HS256",
1116
+ typ: "JWT"
1117
+ };
1118
+ const now = Math.floor(Date.now() / 1e3);
1119
+ const fullPayload = {
1120
+ ...payload,
1121
+ iat: now,
1122
+ exp: now + expiresInSeconds
1123
+ };
1124
+ const enc = new TextEncoder();
1125
+ const dataToSign = `${base64UrlEncode(enc.encode(JSON.stringify(header)))}.${base64UrlEncode(enc.encode(JSON.stringify(fullPayload)))}`;
1126
+ const key = await crypto.subtle.importKey("raw", enc.encode(secret), {
1127
+ name: "HMAC",
1128
+ hash: "SHA-256"
1129
+ }, false, ["sign"]);
1130
+ const signature = await crypto.subtle.sign("HMAC", key, enc.encode(dataToSign));
1131
+ return `${dataToSign}.${base64UrlEncode(new Uint8Array(signature))}`;
1132
+ }
1133
+ async function verifyJwt(token, secret) {
1134
+ if (!token || typeof token !== "string") return null;
1135
+ const parts = token.split(".");
1136
+ if (parts.length !== 3) return null;
1137
+ const [headerEncoded, payloadEncoded, signatureEncoded] = parts;
1138
+ const dataToVerify = `${headerEncoded}.${payloadEncoded}`;
1139
+ try {
1140
+ const enc = new TextEncoder();
1141
+ const key = await crypto.subtle.importKey("raw", enc.encode(secret), {
1142
+ name: "HMAC",
1143
+ hash: "SHA-256"
1144
+ }, false, ["verify"]);
1145
+ const signature = base64UrlDecode(signatureEncoded);
1146
+ if (!await crypto.subtle.verify("HMAC", key, signature, enc.encode(dataToVerify))) return null;
1147
+ const payloadJson = new TextDecoder().decode(base64UrlDecode(payloadEncoded));
1148
+ const payload = JSON.parse(payloadJson);
1149
+ if (payload.exp && Math.floor(Date.now() / 1e3) > payload.exp) return null;
1150
+ return payload;
1151
+ } catch {
1152
+ return null;
1153
+ }
1154
+ }
1155
+ async function hashPassword(password) {
1156
+ const enc = new TextEncoder();
1157
+ const saltBytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
1158
+ const saltHex = Array.from(saltBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1159
+ const keyMaterial = await crypto.subtle.importKey("raw", enc.encode(password), { name: "PBKDF2" }, false, ["deriveBits"]);
1160
+ const derived = await crypto.subtle.deriveBits({
1161
+ name: "PBKDF2",
1162
+ salt: saltBytes,
1163
+ iterations: 1e5,
1164
+ hash: "SHA-256"
1165
+ }, keyMaterial, 256);
1166
+ return `pbkdf2$100000$${saltHex}$${Array.from(new Uint8Array(derived)).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
1167
+ }
1168
+ async function verifyPassword(password, storedHash) {
1169
+ const parts = storedHash.split("$");
1170
+ if (parts.length !== 4 || parts[0] !== "pbkdf2") return false;
1171
+ const iterations = Number(parts[1]);
1172
+ const saltHex = parts[2];
1173
+ const expectedHash = parts[3];
1174
+ const saltBytes = new Uint8Array(saltHex.match(/.{1,2}/g)?.map((byte) => parseInt(byte, 16)) || []);
1175
+ const enc = new TextEncoder();
1176
+ const keyMaterial = await crypto.subtle.importKey("raw", enc.encode(password), { name: "PBKDF2" }, false, ["deriveBits"]);
1177
+ const derived = await crypto.subtle.deriveBits({
1178
+ name: "PBKDF2",
1179
+ salt: saltBytes,
1180
+ iterations,
1181
+ hash: "SHA-256"
1182
+ }, keyMaterial, 256);
1183
+ return Array.from(new Uint8Array(derived)).map((b) => b.toString(16).padStart(2, "0")).join("") === expectedHash;
1184
+ }
1185
+ //#endregion
1186
+ //#region src/auth/auth-service.ts
1187
+ var AuthService = class {
1188
+ config;
1189
+ rbac;
1190
+ users = /* @__PURE__ */ new Map();
1191
+ defaultSecret;
1192
+ constructor(config = {}, rbac) {
1193
+ this.config = config;
1194
+ this.rbac = rbac;
1195
+ this.defaultSecret = config.jwtSecret || "azlib_cms_secret_key_change_in_production";
1196
+ }
1197
+ get enabled() {
1198
+ return Boolean(this.config.enabled);
1199
+ }
1200
+ /**
1201
+ * Register a new user account.
1202
+ */
1203
+ async register(input) {
1204
+ if (Array.from(this.users.values()).find((u) => u.username.toLowerCase() === input.username.toLowerCase())) throw new Error(`Username '${input.username}' is already taken.`);
1205
+ if (Array.from(this.users.values()).find((u) => u.email.toLowerCase() === input.email.toLowerCase())) throw new Error(`Email '${input.email}' is already registered.`);
1206
+ const id = `usr_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`;
1207
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1208
+ const passwordHash = await hashPassword(input.password);
1209
+ const role = input.role || "subscriber";
1210
+ const record = {
1211
+ id,
1212
+ username: input.username,
1213
+ email: input.email.toLowerCase(),
1214
+ displayName: input.displayName || input.username,
1215
+ role,
1216
+ passwordHash,
1217
+ active: true,
1218
+ createdAt: now,
1219
+ updatedAt: now
1220
+ };
1221
+ this.users.set(id, record);
1222
+ return this.sanitizeUser(record);
1223
+ }
1224
+ /**
1225
+ * Authenticate with email/username and password.
1226
+ */
1227
+ async login(identifier, password) {
1228
+ const normalized = identifier.toLowerCase().trim();
1229
+ const record = Array.from(this.users.values()).find((u) => u.username.toLowerCase() === normalized || u.email.toLowerCase() === normalized);
1230
+ if (!record || record.active === false) throw new Error("Invalid username or password.");
1231
+ if (!await verifyPassword(password, record.passwordHash)) throw new Error("Invalid username or password.");
1232
+ const user = this.sanitizeUser(record);
1233
+ return {
1234
+ user,
1235
+ token: await signJwt({
1236
+ sub: user.id,
1237
+ role: user.role,
1238
+ username: user.username,
1239
+ email: user.email
1240
+ }, this.defaultSecret, typeof this.config.tokenExpiresIn === "number" ? this.config.tokenExpiresIn : 86400 * 7)
1241
+ };
1242
+ }
1243
+ /**
1244
+ * Extract and authenticate user from Request (Bearer JWT, ApiKey, or X-API-Key).
1245
+ */
1246
+ async authenticateRequest(request) {
1247
+ const authHeader = request.headers.get("authorization") || request.headers.get("Authorization");
1248
+ const apiKeyHeader = request.headers.get("x-api-key") || request.headers.get("X-API-Key");
1249
+ if (apiKeyHeader && this.config.apiKeys) {
1250
+ const match = this.config.apiKeys.find((k) => k.key === apiKeyHeader);
1251
+ if (match) return {
1252
+ id: `api_key_${match.key.substring(0, 8)}`,
1253
+ username: match.name || "API Key User",
1254
+ email: `${match.role}@system.local`,
1255
+ displayName: match.name || "API Key",
1256
+ role: match.role,
1257
+ capabilities: match.capabilities,
1258
+ active: true
1259
+ };
1260
+ }
1261
+ if (authHeader) {
1262
+ const [type, token] = authHeader.trim().split(/\s+/);
1263
+ if (type?.toLowerCase() === "apikey" && this.config.apiKeys) {
1264
+ const match = this.config.apiKeys.find((k) => k.key === token);
1265
+ if (match) return {
1266
+ id: `api_key_${match.key.substring(0, 8)}`,
1267
+ username: match.name || "API Key User",
1268
+ email: `${match.role}@system.local`,
1269
+ displayName: match.name || "API Key",
1270
+ role: match.role,
1271
+ capabilities: match.capabilities,
1272
+ active: true
1273
+ };
1274
+ }
1275
+ if (type?.toLowerCase() === "bearer" && token) {
1276
+ const payload = await verifyJwt(token, this.defaultSecret);
1277
+ if (payload) {
1278
+ const registeredUser = this.users.get(payload.sub);
1279
+ if (registeredUser) return this.sanitizeUser(registeredUser);
1280
+ return {
1281
+ id: payload.sub,
1282
+ username: payload.username,
1283
+ email: payload.email,
1284
+ displayName: payload.username,
1285
+ role: payload.role,
1286
+ active: true
1287
+ };
1288
+ }
1289
+ }
1290
+ }
1291
+ const cookieHeader = request.headers.get("cookie");
1292
+ if (cookieHeader) {
1293
+ const cookies = Object.fromEntries(cookieHeader.split(";").map((c) => {
1294
+ const [k, v] = c.trim().split("=");
1295
+ return [k, decodeURIComponent(v || "")];
1296
+ }));
1297
+ const token = cookies.access_token || cookies.at;
1298
+ if (token) {
1299
+ const payload = await verifyJwt(token, this.defaultSecret);
1300
+ if (payload) {
1301
+ const registeredUser = this.users.get(payload.sub);
1302
+ if (registeredUser) return this.sanitizeUser(registeredUser);
1303
+ return {
1304
+ id: payload.sub,
1305
+ username: payload.username,
1306
+ email: payload.email,
1307
+ displayName: payload.username,
1308
+ role: payload.role,
1309
+ active: true
1310
+ };
1311
+ }
1312
+ }
1313
+ }
1314
+ return null;
1315
+ }
1316
+ /**
1317
+ * Find a user by ID.
1318
+ */
1319
+ getUserById(id) {
1320
+ const u = this.users.get(id);
1321
+ return u ? this.sanitizeUser(u) : null;
1322
+ }
1323
+ sanitizeUser(record) {
1324
+ return {
1325
+ id: record.id,
1326
+ username: record.username,
1327
+ email: record.email,
1328
+ displayName: record.displayName,
1329
+ role: record.role,
1330
+ capabilities: record.capabilities,
1331
+ active: record.active
1332
+ };
1333
+ }
1334
+ };
1335
+ //#endregion
1336
+ //#region src/core/webhooks.ts
1337
+ function bufferToHex(buffer) {
1338
+ return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
1339
+ }
1340
+ async function computeHmacSignature(payload, secret) {
1341
+ const enc = new TextEncoder();
1342
+ const key = await crypto.subtle.importKey("raw", enc.encode(secret), {
1343
+ name: "HMAC",
1344
+ hash: "SHA-256"
1345
+ }, false, ["sign"]);
1346
+ return `sha256=${bufferToHex(await crypto.subtle.sign("HMAC", key, enc.encode(payload)))}`;
1347
+ }
1348
+ var WebhookManager = class {
1349
+ hooks;
1350
+ webhooks = /* @__PURE__ */ new Map();
1351
+ fetchFn;
1352
+ constructor(initialWebhooks = [], hooks, fetchFn) {
1353
+ this.hooks = hooks;
1354
+ this.fetchFn = fetchFn ?? globalThis.fetch?.bind(globalThis);
1355
+ for (const wh of initialWebhooks) this.registerWebhook(wh);
1356
+ }
1357
+ /**
1358
+ * Register a new webhook endpoint.
1359
+ */
1360
+ registerWebhook(webhook) {
1361
+ const id = webhook.id || `wh_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 7)}`;
1362
+ this.webhooks.set(id, {
1363
+ ...webhook,
1364
+ id,
1365
+ enabled: webhook.enabled ?? true
1366
+ });
1367
+ return id;
1368
+ }
1369
+ /**
1370
+ * Get all registered webhooks.
1371
+ */
1372
+ getWebhooks() {
1373
+ return Array.from(this.webhooks.values());
1374
+ }
1375
+ /**
1376
+ * Delete a webhook by ID.
1377
+ */
1378
+ deleteWebhook(id) {
1379
+ return this.webhooks.delete(id);
1380
+ }
1381
+ /**
1382
+ * Dispatch an event to all matching webhooks asynchronously.
1383
+ */
1384
+ async dispatch(event, data) {
1385
+ const matching = Array.from(this.webhooks.values()).filter((wh) => wh.enabled !== false && (wh.events.includes(event) || wh.events.includes("*")));
1386
+ if (matching.length === 0) return [];
1387
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1388
+ const payloadString = JSON.stringify({
1389
+ event,
1390
+ timestamp: now,
1391
+ data
1392
+ });
1393
+ const deliveryPromises = matching.map(async (webhook) => {
1394
+ try {
1395
+ const headers = {
1396
+ "Content-Type": "application/json",
1397
+ "User-Agent": "azlib-cms-webhooks/1.0",
1398
+ "X-CMS-Event": event,
1399
+ "X-CMS-Delivery-Time": now,
1400
+ ...webhook.headers || {}
1401
+ };
1402
+ if (webhook.secret) headers["X-CMS-Signature"] = await computeHmacSignature(payloadString, webhook.secret);
1403
+ const res = await this.fetchFn(webhook.url, {
1404
+ method: "POST",
1405
+ headers,
1406
+ body: payloadString
1407
+ });
1408
+ if (this.hooks) await this.hooks.doAction("cms.webhook_delivered", {
1409
+ webhook,
1410
+ event,
1411
+ status: res.status,
1412
+ ok: res.ok
1413
+ });
1414
+ return {
1415
+ url: webhook.url,
1416
+ success: res.ok,
1417
+ status: res.status
1418
+ };
1419
+ } catch (error) {
1420
+ if (this.hooks) await this.hooks.doAction("cms.webhook_failed", {
1421
+ webhook,
1422
+ event,
1423
+ error: error.message
1424
+ });
1425
+ return {
1426
+ url: webhook.url,
1427
+ success: false
1428
+ };
1429
+ }
1430
+ });
1431
+ return Promise.all(deliveryPromises);
1432
+ }
1433
+ };
1434
+ //#endregion
1435
+ //#region src/core/preview.ts
1436
+ /**
1437
+ * @azlib/cms - Draft Preview Mode Engine
1438
+ */
1439
+ var PreviewManager = class {
1440
+ secret;
1441
+ constructor(secret = "azlib_preview_secret_token_default") {
1442
+ this.secret = secret;
1443
+ }
1444
+ /**
1445
+ * Mint a signed draft preview token.
1446
+ */
1447
+ async createPreviewToken(contentId, collection, expiresInSeconds = 3600) {
1448
+ return signJwt({
1449
+ contentId,
1450
+ collection,
1451
+ scope: "preview"
1452
+ }, this.secret, expiresInSeconds);
1453
+ }
1454
+ /**
1455
+ * Verify and unpack a draft preview token.
1456
+ */
1457
+ async verifyPreviewToken(token) {
1458
+ const payload = await verifyJwt(token, this.secret);
1459
+ if (!payload || payload.scope !== "preview") return null;
1460
+ return {
1461
+ contentId: payload.contentId,
1462
+ collection: payload.collection
1463
+ };
1464
+ }
1465
+ };
1466
+ //#endregion
1467
+ //#region src/content/i18n.ts
1468
+ /**
1469
+ * Resolve localized fields on a content record for a target locale.
1470
+ */
1471
+ function resolveLocalizedData(data, fieldsList, targetLocale, fallbackLocale = "en") {
1472
+ const resolved = { ...data };
1473
+ for (const field of fieldsList) {
1474
+ if (!field.localized) continue;
1475
+ const val = data[field.name];
1476
+ if (val && typeof val === "object" && !Array.isArray(val)) {
1477
+ const locMap = val;
1478
+ if (targetLocale in locMap && locMap[targetLocale] !== void 0) resolved[field.name] = locMap[targetLocale];
1479
+ else if (fallbackLocale && fallbackLocale in locMap && locMap[fallbackLocale] !== void 0) resolved[field.name] = locMap[fallbackLocale];
1480
+ else {
1481
+ const firstAvailable = Object.values(locMap)[0];
1482
+ resolved[field.name] = firstAvailable;
1483
+ }
1484
+ }
1485
+ }
1486
+ return resolved;
1487
+ }
1488
+ /**
1489
+ * Merge an incoming update for a specific locale into existing localized dictionaries.
1490
+ */
1491
+ function mergeLocalizedInput(incomingData, existingData = {}, fieldsList, locale) {
1492
+ const merged = {
1493
+ ...existingData,
1494
+ ...incomingData
1495
+ };
1496
+ for (const field of fieldsList) {
1497
+ if (!field.localized) continue;
1498
+ const incomingVal = incomingData[field.name];
1499
+ if (incomingVal === void 0) continue;
1500
+ if (incomingVal && typeof incomingVal === "object" && !Array.isArray(incomingVal) && Object.keys(incomingVal).some((k) => k.length === 2 || k.includes("-"))) {
1501
+ const existingObj = typeof existingData[field.name] === "object" && existingData[field.name] !== null ? existingData[field.name] : {};
1502
+ merged[field.name] = {
1503
+ ...existingObj,
1504
+ ...incomingVal
1505
+ };
1506
+ continue;
1507
+ }
1508
+ const existingMap = typeof existingData[field.name] === "object" && existingData[field.name] !== null ? { ...existingData[field.name] } : {};
1509
+ existingMap[locale] = incomingVal;
1510
+ merged[field.name] = existingMap;
1511
+ }
1512
+ return merged;
1513
+ }
1514
+ //#endregion
938
1515
  //#region src/storage/memory-adapter.ts
939
- function generateId(prefix = "") {
1516
+ function generateId$1(prefix = "") {
940
1517
  return `${prefix}${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 9)}`;
941
1518
  }
1519
+ function matchesCondition(actualValue, expected) {
1520
+ if (expected === null || expected === void 0 || typeof expected !== "object" || Array.isArray(expected)) return actualValue === expected;
1521
+ const exp = expected;
1522
+ if (!Object.keys(exp).some((k) => k.startsWith("$"))) return JSON.stringify(actualValue) === JSON.stringify(expected);
1523
+ for (const [op, target] of Object.entries(exp)) switch (op) {
1524
+ case "$eq":
1525
+ if (actualValue !== target) return false;
1526
+ break;
1527
+ case "$ne":
1528
+ if (actualValue === target) return false;
1529
+ break;
1530
+ case "$gt":
1531
+ if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
1532
+ if (actualValue <= target) return false;
1533
+ break;
1534
+ case "$gte":
1535
+ if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
1536
+ if (actualValue < target) return false;
1537
+ break;
1538
+ case "$lt":
1539
+ if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
1540
+ if (actualValue >= target) return false;
1541
+ break;
1542
+ case "$lte":
1543
+ if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
1544
+ if (actualValue > target) return false;
1545
+ break;
1546
+ case "$in":
1547
+ if (!Array.isArray(target) || !target.includes(actualValue)) return false;
1548
+ break;
1549
+ case "$nin":
1550
+ if (Array.isArray(target) && target.includes(actualValue)) return false;
1551
+ break;
1552
+ case "$contains":
1553
+ if (typeof actualValue !== "string" || !actualValue.toLowerCase().includes(String(target).toLowerCase())) return false;
1554
+ break;
1555
+ case "$startsWith":
1556
+ if (typeof actualValue !== "string" || !actualValue.startsWith(String(target))) return false;
1557
+ break;
1558
+ case "$endsWith":
1559
+ if (typeof actualValue !== "string" || !actualValue.endsWith(String(target))) return false;
1560
+ break;
1561
+ case "$between":
1562
+ if (!Array.isArray(target) || target.length !== 2) return false;
1563
+ if (actualValue < target[0] || actualValue > target[1]) return false;
1564
+ break;
1565
+ case "$exists":
1566
+ if (actualValue !== void 0 !== Boolean(target)) return false;
1567
+ break;
1568
+ }
1569
+ return true;
1570
+ }
942
1571
  var MemoryStorageAdapter = class {
943
1572
  content = /* @__PURE__ */ new Map();
944
1573
  revisions = /* @__PURE__ */ new Map();
@@ -949,7 +1578,7 @@ var MemoryStorageAdapter = class {
949
1578
  async init() {}
950
1579
  async close() {}
951
1580
  async createContent(item) {
952
- const id = generateId("cnt_");
1581
+ const id = generateId$1("cnt_");
953
1582
  const now = (/* @__PURE__ */ new Date()).toISOString();
954
1583
  const fullItem = {
955
1584
  ...item,
@@ -992,7 +1621,7 @@ var MemoryStorageAdapter = class {
992
1621
  }
993
1622
  if (options.where) {
994
1623
  let match = true;
995
- for (const [key, val] of Object.entries(options.where)) if (item.data[key] !== val && item[key] !== val) {
1624
+ for (const [key, expected] of Object.entries(options.where)) if (!matchesCondition(item.data[key] !== void 0 ? item.data[key] : item[key], expected)) {
996
1625
  match = false;
997
1626
  break;
998
1627
  }
@@ -1013,14 +1642,49 @@ var MemoryStorageAdapter = class {
1013
1642
  const offset = options.offset ?? 0;
1014
1643
  const limit = options.limit ?? 20;
1015
1644
  const paged = list.slice(offset, offset + limit);
1645
+ let items = JSON.parse(JSON.stringify(paged));
1646
+ if (options.populate && options.populate.length > 0) items = items.map((it) => this.populateItem(it, options.populate));
1647
+ if (options.select && options.select.length > 0) items = items.map((it) => this.projectItem(it, options.select));
1016
1648
  return {
1017
- items: JSON.parse(JSON.stringify(paged)),
1649
+ items,
1018
1650
  total,
1019
1651
  limit,
1020
1652
  offset,
1021
1653
  hasMore: offset + limit < total
1022
1654
  };
1023
1655
  }
1656
+ populateItem(item, populateFields) {
1657
+ const populated = { ...item.populated || {} };
1658
+ for (const field of populateFields) {
1659
+ const rawVal = item.data[field];
1660
+ if (!rawVal) continue;
1661
+ if (Array.isArray(rawVal)) populated[field] = rawVal.map((id) => {
1662
+ if (typeof id === "string") return this.content.get(id) || this.media.get(id) || id;
1663
+ return id;
1664
+ });
1665
+ else if (typeof rawVal === "string") {
1666
+ const target = this.content.get(rawVal) || this.media.get(rawVal) || null;
1667
+ if (target) populated[field] = target;
1668
+ }
1669
+ }
1670
+ return {
1671
+ ...item,
1672
+ populated
1673
+ };
1674
+ }
1675
+ projectItem(item, selectFields) {
1676
+ const projected = {
1677
+ id: item.id,
1678
+ collection: item.collection
1679
+ };
1680
+ for (const key of selectFields) if (key in item) projected[key] = item[key];
1681
+ else if (key in item.data) {
1682
+ if (!projected.data) projected.data = {};
1683
+ projected.data[key] = item.data[key];
1684
+ }
1685
+ if (item.populated) projected.populated = item.populated;
1686
+ return projected;
1687
+ }
1024
1688
  async updateContent(collection, id, updates) {
1025
1689
  const existing = this.content.get(id);
1026
1690
  if (!existing || existing.collection !== collection) return null;
@@ -1052,7 +1716,7 @@ var MemoryStorageAdapter = class {
1052
1716
  })).total;
1053
1717
  }
1054
1718
  async createRevision(contentId, collection, version, snapshot, authorId, note) {
1055
- const id = generateId("rev_");
1719
+ const id = generateId$1("rev_");
1056
1720
  const record = {
1057
1721
  id,
1058
1722
  contentId,
@@ -1085,7 +1749,7 @@ var MemoryStorageAdapter = class {
1085
1749
  return count;
1086
1750
  }
1087
1751
  async createTerm(term) {
1088
- const id = generateId("trm_");
1752
+ const id = generateId$1("trm_");
1089
1753
  const now = (/* @__PURE__ */ new Date()).toISOString();
1090
1754
  const item = {
1091
1755
  ...term,
@@ -1163,7 +1827,7 @@ var MemoryStorageAdapter = class {
1163
1827
  for (const [id, term] of this.terms.entries()) term.count = counts.get(id) || 0;
1164
1828
  }
1165
1829
  async createMedia(item) {
1166
- const id = generateId("med_");
1830
+ const id = generateId$1("med_");
1167
1831
  const now = (/* @__PURE__ */ new Date()).toISOString();
1168
1832
  const mediaItem = {
1169
1833
  ...item,
@@ -1244,6 +1908,9 @@ var CMSEngine = class {
1244
1908
  taxonomies;
1245
1909
  media;
1246
1910
  rbac;
1911
+ auth;
1912
+ webhooks;
1913
+ preview;
1247
1914
  revisions;
1248
1915
  lifecycle;
1249
1916
  collections = /* @__PURE__ */ new Map();
@@ -1256,8 +1923,11 @@ var CMSEngine = class {
1256
1923
  this.storage = storage ?? new MemoryStorageAdapter();
1257
1924
  this.options = new OptionsManager(this.storage, this.hooks);
1258
1925
  this.taxonomies = new TaxonomyManager(this.storage, this.hooks);
1259
- this.media = new MediaManager(this.storage, this.hooks);
1260
1926
  this.rbac = new RBACManager();
1927
+ this.auth = new AuthService(this.config.auth, this.rbac);
1928
+ this.webhooks = new WebhookManager(this.config.webhooks || [], this.hooks);
1929
+ this.preview = new PreviewManager(this.config.auth?.jwtSecret || "azlib_preview_secret_token_default");
1930
+ this.media = new MediaManager(this.storage, this.hooks, this.config.media?.publicBaseUrl || "/uploads", this.config.media?.storageDriver);
1261
1931
  this.revisions = new RevisionManager(this.storage, this.hooks);
1262
1932
  this.lifecycle = new ContentLifecycle(this.storage, this.hooks);
1263
1933
  for (const coll of this.config.collections) this.collections.set(coll.slug, coll);
@@ -1398,11 +2068,12 @@ var CMSEngine = class {
1398
2068
  const filteredInput = await self.hooks.applyFilters("cms.before_create_input", input, { collection: slug });
1399
2069
  const inputData = { ...filteredInput.data || {} };
1400
2070
  if (filteredInput.title !== void 0 && inputData.title === void 0) inputData.title = filteredInput.title;
2071
+ if (filteredInput.name !== void 0 && inputData.name === void 0) inputData.name = filteredInput.name;
1401
2072
  if (filteredInput.slug !== void 0 && inputData.slug === void 0) inputData.slug = filteredInput.slug;
1402
2073
  if (filteredInput.status !== void 0 && inputData.status === void 0) inputData.status = filteredInput.status;
1403
2074
  const { data: normalizedData, errors } = validateAndNormalizeData(collConfig.fields, inputData);
1404
2075
  if (Object.keys(errors).length > 0) throw new Error(`[CMSEngine] Validation failed for collection '${slug}': ${JSON.stringify(errors)}`);
1405
- const title = filteredInput.title ?? normalizedData.title ?? "";
2076
+ const title = filteredInput.title ?? filteredInput.name ?? normalizedData.title ?? normalizedData.name ?? "";
1406
2077
  const finalSlug = await resolveUniqueSlug(filteredInput.slug ? slugify(filteredInput.slug) : slugify(title) || "item", async (s) => {
1407
2078
  return await self.storage.getContentBySlug(slug, s) !== null;
1408
2079
  });
@@ -1427,17 +2098,48 @@ var CMSEngine = class {
1427
2098
  if (collConfig.revisions) await self.revisions.createRevision(item, authorId, "Initial creation");
1428
2099
  await self.hooks.doAction("cms.content_created", item);
1429
2100
  await self.hooks.doAction(`cms.${slug}_created`, item);
2101
+ self.webhooks.dispatch("content.created", item);
2102
+ if (item.status === "published") self.webhooks.dispatch("content.published", item);
1430
2103
  return self.hooks.applyFilters("cms.after_create_item", item, { collection: slug });
1431
2104
  },
1432
- async findById(id) {
1433
- return self.storage.getContent(slug, id);
2105
+ async findById(id, options) {
2106
+ if (options?.populate || options?.select) {
2107
+ const it = (await self.storage.findContent(slug, {
2108
+ where: { id },
2109
+ limit: 1,
2110
+ populate: options.populate,
2111
+ select: options.select
2112
+ })).items[0] || null;
2113
+ if (it && options.locale) it.data = resolveLocalizedData(it.data, collConfig.fields, options.locale, self.config.i18n?.defaultLocale);
2114
+ return it;
2115
+ }
2116
+ const item = await self.storage.getContent(slug, id);
2117
+ if (item && options?.locale) item.data = resolveLocalizedData(item.data, collConfig.fields, options.locale, self.config.i18n?.defaultLocale);
2118
+ return item;
1434
2119
  },
1435
- async findBySlug(contentSlug) {
1436
- return self.storage.getContentBySlug(slug, contentSlug);
2120
+ async findBySlug(contentSlug, options) {
2121
+ if (options?.populate || options?.select) {
2122
+ const it = (await self.storage.findContent(slug, {
2123
+ where: { slug: contentSlug },
2124
+ limit: 1,
2125
+ populate: options.populate,
2126
+ select: options.select
2127
+ })).items[0] || null;
2128
+ if (it && options.locale) it.data = resolveLocalizedData(it.data, collConfig.fields, options.locale, self.config.i18n?.defaultLocale);
2129
+ return it;
2130
+ }
2131
+ const item = await self.storage.getContentBySlug(slug, contentSlug);
2132
+ if (item && options?.locale) item.data = resolveLocalizedData(item.data, collConfig.fields, options.locale, self.config.i18n?.defaultLocale);
2133
+ return item;
1437
2134
  },
1438
2135
  async find(options = {}) {
1439
2136
  const filteredOptions = await self.hooks.applyFilters("cms.find_options", options, { collection: slug });
1440
- return self.storage.findContent(slug, filteredOptions);
2137
+ const res = await self.storage.findContent(slug, filteredOptions);
2138
+ if (options.locale) res.items = res.items.map((it) => ({
2139
+ ...it,
2140
+ data: resolveLocalizedData(it.data, collConfig.fields, options.locale, self.config.i18n?.defaultLocale)
2141
+ }));
2142
+ return res;
1441
2143
  },
1442
2144
  async update(id, input, authorId = null, revisionNote) {
1443
2145
  const existing = await self.storage.getContent(slug, id);
@@ -1487,6 +2189,8 @@ var CMSEngine = class {
1487
2189
  if (collConfig.revisions) await self.revisions.createRevision(updated, authorId, revisionNote ?? `Updated version ${updated.version}`);
1488
2190
  await self.hooks.doAction("cms.content_updated", updated);
1489
2191
  await self.hooks.doAction(`cms.${slug}_updated`, updated);
2192
+ self.webhooks.dispatch("content.updated", updated);
2193
+ if (updated.status === "published" && existing.status !== "published") self.webhooks.dispatch("content.published", updated);
1490
2194
  return self.hooks.applyFilters("cms.after_update_item", updated, { collection: slug });
1491
2195
  }
1492
2196
  return updated;
@@ -1498,6 +2202,7 @@ var CMSEngine = class {
1498
2202
  if (deleted) {
1499
2203
  await self.hooks.doAction("cms.content_deleted", item);
1500
2204
  await self.hooks.doAction(`cms.${slug}_deleted`, item);
2205
+ self.webhooks.dispatch("content.deleted", item);
1501
2206
  }
1502
2207
  return deleted;
1503
2208
  },
@@ -1552,6 +2257,700 @@ function definePlugin(factory) {
1552
2257
  return factory;
1553
2258
  }
1554
2259
  //#endregion
2260
+ //#region src/media/drivers/disk-driver.ts
2261
+ /**
2262
+ * @azlib/cms - Local Disk Media Storage Driver
2263
+ */
2264
+ var DiskMediaStorageDriver = class {
2265
+ uploadDir;
2266
+ publicBaseUrl;
2267
+ constructor(options) {
2268
+ this.uploadDir = options.uploadDir;
2269
+ this.publicBaseUrl = options.publicBaseUrl?.replace(/\/+$/, "") || "/uploads";
2270
+ }
2271
+ async write(input) {
2272
+ await promises.mkdir(this.uploadDir, { recursive: true });
2273
+ const safeName = `${Date.now()}_${input.filename.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
2274
+ const filePath = path.join(this.uploadDir, safeName);
2275
+ await promises.writeFile(filePath, input.buffer);
2276
+ return {
2277
+ url: `${this.publicBaseUrl}/${safeName}`,
2278
+ path: filePath,
2279
+ sizeBytes: input.buffer.byteLength
2280
+ };
2281
+ }
2282
+ async read(pathOrUrl) {
2283
+ try {
2284
+ const filename = path.basename(pathOrUrl);
2285
+ const filePath = path.join(this.uploadDir, filename);
2286
+ const data = await promises.readFile(filePath);
2287
+ return new Uint8Array(data);
2288
+ } catch {
2289
+ return null;
2290
+ }
2291
+ }
2292
+ async delete(pathOrUrl) {
2293
+ try {
2294
+ const filename = path.basename(pathOrUrl);
2295
+ const filePath = path.join(this.uploadDir, filename);
2296
+ await promises.unlink(filePath);
2297
+ return true;
2298
+ } catch {
2299
+ return false;
2300
+ }
2301
+ }
2302
+ getUrl(filePathOrName) {
2303
+ const filename = path.basename(filePathOrName);
2304
+ return `${this.publicBaseUrl}/${filename}`;
2305
+ }
2306
+ };
2307
+ //#endregion
2308
+ //#region src/storage/persistence-adapter.ts
2309
+ /**
2310
+ * @azlib/cms - Persistence Storage Adapter (@azlib/persistence)
2311
+ */
2312
+ function generateId(prefix = "") {
2313
+ return `${prefix}${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 9)}`;
2314
+ }
2315
+ const cmsContentTable = createTable("cms_content", {
2316
+ id: column.varchar("id", { length: 64 }).notNull().primaryKey(),
2317
+ collection: column.varchar("collection", { length: 64 }).notNull(),
2318
+ slug: column.varchar("slug", { length: 255 }).notNull(),
2319
+ status: column.varchar("status", { length: 32 }).notNull(),
2320
+ title: column.text("title"),
2321
+ parentId: column.varchar("parent_id", { length: 64 }),
2322
+ authorId: column.varchar("author_id", { length: 64 }),
2323
+ publishedAt: column.varchar("published_at", { length: 32 }),
2324
+ scheduledAt: column.varchar("scheduled_at", { length: 32 }),
2325
+ createdAt: column.varchar("created_at", { length: 32 }).notNull(),
2326
+ updatedAt: column.varchar("updated_at", { length: 32 }).notNull(),
2327
+ version: column.integer("version").notNull().default(1),
2328
+ locale: column.varchar("locale", { length: 16 }),
2329
+ data: column.text("data").notNull(),
2330
+ terms: column.text("terms")
2331
+ });
2332
+ const cmsRevisionsTable = createTable("cms_revisions", {
2333
+ id: column.varchar("id", { length: 64 }).notNull().primaryKey(),
2334
+ contentId: column.varchar("content_id", { length: 64 }).notNull(),
2335
+ collection: column.varchar("collection", { length: 64 }).notNull(),
2336
+ version: column.integer("version").notNull(),
2337
+ snapshot: column.text("snapshot").notNull(),
2338
+ authorId: column.varchar("author_id", { length: 64 }),
2339
+ note: column.text("note"),
2340
+ createdAt: column.varchar("created_at", { length: 32 }).notNull()
2341
+ });
2342
+ const cmsTermsTable = createTable("cms_terms", {
2343
+ id: column.varchar("id", { length: 64 }).notNull().primaryKey(),
2344
+ taxonomy: column.varchar("taxonomy", { length: 64 }).notNull(),
2345
+ name: column.varchar("name", { length: 255 }).notNull(),
2346
+ slug: column.varchar("slug", { length: 255 }).notNull(),
2347
+ description: column.text("description"),
2348
+ parentId: column.varchar("parent_id", { length: 64 }),
2349
+ count: column.integer("count").notNull().default(0),
2350
+ meta: column.text("meta"),
2351
+ createdAt: column.varchar("created_at", { length: 32 }).notNull(),
2352
+ updatedAt: column.varchar("updated_at", { length: 32 }).notNull()
2353
+ });
2354
+ const cmsContentTermsTable = createTable("cms_content_terms", {
2355
+ contentId: column.varchar("content_id", { length: 64 }).notNull(),
2356
+ termId: column.varchar("term_id", { length: 64 }).notNull()
2357
+ });
2358
+ const cmsMediaTable = createTable("cms_media", {
2359
+ id: column.varchar("id", { length: 64 }).notNull().primaryKey(),
2360
+ filename: column.varchar("filename", { length: 255 }).notNull(),
2361
+ originalName: column.varchar("original_name", { length: 255 }).notNull(),
2362
+ mimeType: column.varchar("mime_type", { length: 128 }).notNull(),
2363
+ sizeBytes: column.integer("size_bytes").notNull(),
2364
+ url: column.text("url").notNull(),
2365
+ path: column.text("path"),
2366
+ width: column.integer("width"),
2367
+ height: column.integer("height"),
2368
+ altText: column.text("alt_text"),
2369
+ caption: column.text("caption"),
2370
+ authorId: column.varchar("author_id", { length: 64 }),
2371
+ variants: column.text("variants"),
2372
+ createdAt: column.varchar("created_at", { length: 32 }).notNull(),
2373
+ updatedAt: column.varchar("updated_at", { length: 32 }).notNull()
2374
+ });
2375
+ const cmsOptionsTable = createTable("cms_options", {
2376
+ key: column.varchar("key", { length: 128 }).notNull().primaryKey(),
2377
+ value: column.text("value").notNull(),
2378
+ autoload: column.boolean("autoload").notNull().default(false),
2379
+ namespace: column.varchar("namespace", { length: 64 }),
2380
+ updatedAt: column.varchar("updated_at", { length: 32 }).notNull()
2381
+ });
2382
+ var PersistenceStorageAdapter = class {
2383
+ client;
2384
+ config;
2385
+ autoMigrate;
2386
+ constructor(options) {
2387
+ this.client = options.client;
2388
+ this.config = options.config;
2389
+ this.autoMigrate = options.autoMigrate ?? true;
2390
+ }
2391
+ async init() {
2392
+ if (!this.autoMigrate) return;
2393
+ const tables = [
2394
+ cmsContentTable,
2395
+ cmsRevisionsTable,
2396
+ cmsTermsTable,
2397
+ cmsContentTermsTable,
2398
+ cmsMediaTable,
2399
+ cmsOptionsTable
2400
+ ];
2401
+ for (const table of tables) {
2402
+ const ddl = generateCreateTableDdl(table, this.config.dialect);
2403
+ try {
2404
+ await this.client.raw(ddl);
2405
+ } catch (err) {}
2406
+ }
2407
+ }
2408
+ async close() {}
2409
+ async createContent(item) {
2410
+ const id = generateId("cnt_");
2411
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2412
+ const version = 1;
2413
+ const record = {
2414
+ id,
2415
+ collection: item.collection,
2416
+ slug: item.slug,
2417
+ status: item.status,
2418
+ title: item.title ?? null,
2419
+ parentId: item.parentId ?? null,
2420
+ authorId: item.authorId ?? null,
2421
+ publishedAt: item.publishedAt ?? null,
2422
+ scheduledAt: item.scheduledAt ?? null,
2423
+ createdAt: now,
2424
+ updatedAt: now,
2425
+ version,
2426
+ locale: item.locale ?? null,
2427
+ data: JSON.stringify(item.data || {}),
2428
+ terms: item.terms ? JSON.stringify(item.terms) : null
2429
+ };
2430
+ await this.client.insert(cmsContentTable).values(record).execute();
2431
+ return {
2432
+ id,
2433
+ collection: item.collection,
2434
+ slug: item.slug,
2435
+ status: item.status,
2436
+ title: item.title,
2437
+ parentId: item.parentId ?? null,
2438
+ authorId: item.authorId ?? null,
2439
+ publishedAt: item.publishedAt ?? null,
2440
+ scheduledAt: item.scheduledAt ?? null,
2441
+ createdAt: now,
2442
+ updatedAt: now,
2443
+ version,
2444
+ locale: item.locale,
2445
+ data: item.data,
2446
+ terms: item.terms
2447
+ };
2448
+ }
2449
+ async getContent(collection, id) {
2450
+ const rows = await this.client.select().from(cmsContentTable).where(and(eq(cmsContentTable.id, id), eq(cmsContentTable.collection, collection))).execute();
2451
+ if (!rows || rows.length === 0) return null;
2452
+ return this.deserializeContent(rows[0]);
2453
+ }
2454
+ async getContentBySlug(collection, slug) {
2455
+ const rows = await this.client.select().from(cmsContentTable).where(and(eq(cmsContentTable.slug, slug), eq(cmsContentTable.collection, collection))).execute();
2456
+ if (!rows || rows.length === 0) return null;
2457
+ return this.deserializeContent(rows[0]);
2458
+ }
2459
+ async findContent(collection, options = {}) {
2460
+ const conditions = [eq(cmsContentTable.collection, collection)];
2461
+ if (options.status) if (Array.isArray(options.status)) conditions.push(inArray(cmsContentTable.status, options.status));
2462
+ else conditions.push(eq(cmsContentTable.status, options.status));
2463
+ if (options.authorId) conditions.push(eq(cmsContentTable.authorId, options.authorId));
2464
+ if (options.parentId !== void 0) conditions.push(eq(cmsContentTable.parentId, options.parentId));
2465
+ if (options.locale) conditions.push(eq(cmsContentTable.locale, options.locale));
2466
+ if (options.search) {
2467
+ const q = `%${options.search}%`;
2468
+ conditions.push(or(like(cmsContentTable.title, q), like(cmsContentTable.slug, q), like(cmsContentTable.data, q)));
2469
+ }
2470
+ if (options.termIds && options.termIds.length > 0) {
2471
+ const termRows = await this.client.select().from(cmsContentTermsTable).where(inArray(cmsContentTermsTable.termId, options.termIds)).execute();
2472
+ const matchedContentIds = Array.from(new Set(termRows.map((r) => r.contentId || r.content_id)));
2473
+ if (matchedContentIds.length === 0) return {
2474
+ items: [],
2475
+ total: 0,
2476
+ limit: options.limit ?? 20,
2477
+ offset: options.offset ?? 0,
2478
+ hasMore: false
2479
+ };
2480
+ conditions.push(inArray(cmsContentTable.id, matchedContentIds));
2481
+ }
2482
+ const whereClause = conditions.length === 1 ? conditions[0] : and(...conditions);
2483
+ let list = (await this.client.select().from(cmsContentTable).where(whereClause).execute()).map((r) => this.deserializeContent(r));
2484
+ if (options.where) list = list.filter((item) => {
2485
+ for (const [key, expected] of Object.entries(options.where)) {
2486
+ const actual = item.data[key] !== void 0 ? item.data[key] : item[key];
2487
+ if (!this.matchesCondition(actual, expected)) return false;
2488
+ }
2489
+ return true;
2490
+ });
2491
+ const total = list.length;
2492
+ const orderBy = options.orderBy || "createdAt";
2493
+ const dir = options.orderDirection === "asc" ? 1 : -1;
2494
+ list.sort((a, b) => {
2495
+ const valA = a[orderBy] ?? a.data[orderBy] ?? "";
2496
+ const valB = b[orderBy] ?? b.data[orderBy] ?? "";
2497
+ if (valA < valB) return -1 * dir;
2498
+ if (valA > valB) return 1 * dir;
2499
+ return 0;
2500
+ });
2501
+ const offset = options.offset ?? 0;
2502
+ const limit = options.limit ?? 20;
2503
+ let items = list.slice(offset, offset + limit);
2504
+ if (options.populate && options.populate.length > 0) items = await Promise.all(items.map((it) => this.populateItem(it, options.populate)));
2505
+ return {
2506
+ items,
2507
+ total,
2508
+ limit,
2509
+ offset,
2510
+ hasMore: offset + limit < total
2511
+ };
2512
+ }
2513
+ async updateContent(collection, id, updates) {
2514
+ const existing = await this.getContent(collection, id);
2515
+ if (!existing) return null;
2516
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2517
+ const nextVersion = existing.version + 1;
2518
+ const mergedData = {
2519
+ ...existing.data,
2520
+ ...updates.data || {}
2521
+ };
2522
+ const updateRecord = {
2523
+ updatedAt: now,
2524
+ version: nextVersion,
2525
+ data: JSON.stringify(mergedData)
2526
+ };
2527
+ if (updates.title !== void 0) updateRecord.title = updates.title;
2528
+ if (updates.slug !== void 0) updateRecord.slug = updates.slug;
2529
+ if (updates.status !== void 0) updateRecord.status = updates.status;
2530
+ if (updates.parentId !== void 0) updateRecord.parentId = updates.parentId;
2531
+ if (updates.authorId !== void 0) updateRecord.authorId = updates.authorId;
2532
+ if (updates.publishedAt !== void 0) updateRecord.publishedAt = updates.publishedAt;
2533
+ if (updates.scheduledAt !== void 0) updateRecord.scheduledAt = updates.scheduledAt;
2534
+ if (updates.locale !== void 0) updateRecord.locale = updates.locale;
2535
+ if (updates.terms !== void 0) updateRecord.terms = JSON.stringify(updates.terms);
2536
+ await this.client.update(cmsContentTable).set(updateRecord).where(and(eq(cmsContentTable.id, id), eq(cmsContentTable.collection, collection))).execute();
2537
+ return this.getContent(collection, id);
2538
+ }
2539
+ async deleteContent(collection, id) {
2540
+ if (!await this.getContent(collection, id)) return false;
2541
+ await this.client.delete(cmsContentTable).where(and(eq(cmsContentTable.id, id), eq(cmsContentTable.collection, collection))).execute();
2542
+ await this.client.delete(cmsContentTermsTable).where(eq(cmsContentTermsTable.contentId, id)).execute();
2543
+ await this.deleteRevisionsByContentId(id);
2544
+ return true;
2545
+ }
2546
+ async countContent(collection, options = {}) {
2547
+ return (await this.findContent(collection, {
2548
+ ...options,
2549
+ limit: 1e6
2550
+ })).total;
2551
+ }
2552
+ async createRevision(contentId, collection, version, snapshot, authorId, note) {
2553
+ const id = generateId("rev_");
2554
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2555
+ const record = {
2556
+ id,
2557
+ contentId,
2558
+ collection,
2559
+ version,
2560
+ snapshot: JSON.stringify(snapshot),
2561
+ authorId: authorId ?? null,
2562
+ note: note ?? null,
2563
+ createdAt: now
2564
+ };
2565
+ await this.client.insert(cmsRevisionsTable).values(record).execute();
2566
+ return {
2567
+ id,
2568
+ contentId,
2569
+ collection,
2570
+ version,
2571
+ snapshot,
2572
+ authorId,
2573
+ note,
2574
+ createdAt: now
2575
+ };
2576
+ }
2577
+ async getRevisions(contentId) {
2578
+ return (await this.client.select().from(cmsRevisionsTable).where(eq(cmsRevisionsTable.contentId, contentId)).execute()).map((r) => ({
2579
+ id: r.id,
2580
+ contentId: r.contentId || r.content_id,
2581
+ collection: r.collection,
2582
+ version: Number(r.version),
2583
+ snapshot: typeof r.snapshot === "string" ? JSON.parse(r.snapshot) : r.snapshot,
2584
+ authorId: r.authorId || r.author_id || null,
2585
+ note: r.note || void 0,
2586
+ createdAt: r.createdAt || r.created_at
2587
+ }));
2588
+ }
2589
+ async getRevision(revisionId) {
2590
+ const rows = await this.client.select().from(cmsRevisionsTable).where(eq(cmsRevisionsTable.id, revisionId)).execute();
2591
+ if (!rows || rows.length === 0) return null;
2592
+ const r = rows[0];
2593
+ return {
2594
+ id: r.id,
2595
+ contentId: r.contentId || r.content_id,
2596
+ collection: r.collection,
2597
+ version: Number(r.version),
2598
+ snapshot: typeof r.snapshot === "string" ? JSON.parse(r.snapshot) : r.snapshot,
2599
+ authorId: r.authorId || r.author_id || null,
2600
+ note: r.note || void 0,
2601
+ createdAt: r.createdAt || r.created_at
2602
+ };
2603
+ }
2604
+ async deleteRevisionsByContentId(contentId) {
2605
+ const rows = await this.getRevisions(contentId);
2606
+ await this.client.delete(cmsRevisionsTable).where(eq(cmsRevisionsTable.contentId, contentId)).execute();
2607
+ return rows.length;
2608
+ }
2609
+ async createTerm(term) {
2610
+ const id = generateId("trm_");
2611
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2612
+ const record = {
2613
+ id,
2614
+ taxonomy: term.taxonomy,
2615
+ name: term.name,
2616
+ slug: term.slug,
2617
+ description: term.description ?? null,
2618
+ parentId: term.parentId ?? null,
2619
+ count: 0,
2620
+ meta: term.meta ? JSON.stringify(term.meta) : null,
2621
+ createdAt: now,
2622
+ updatedAt: now
2623
+ };
2624
+ await this.client.insert(cmsTermsTable).values(record).execute();
2625
+ return {
2626
+ id,
2627
+ taxonomy: term.taxonomy,
2628
+ name: term.name,
2629
+ slug: term.slug,
2630
+ description: term.description,
2631
+ parentId: term.parentId ?? null,
2632
+ count: 0,
2633
+ meta: term.meta,
2634
+ createdAt: now,
2635
+ updatedAt: now
2636
+ };
2637
+ }
2638
+ async getTermById(id) {
2639
+ const rows = await this.client.select().from(cmsTermsTable).where(eq(cmsTermsTable.id, id)).execute();
2640
+ if (!rows || rows.length === 0) return null;
2641
+ return this.deserializeTerm(rows[0]);
2642
+ }
2643
+ async getTermBySlug(taxonomy, slug) {
2644
+ const rows = await this.client.select().from(cmsTermsTable).where(and(eq(cmsTermsTable.taxonomy, taxonomy), eq(cmsTermsTable.slug, slug))).execute();
2645
+ if (!rows || rows.length === 0) return null;
2646
+ return this.deserializeTerm(rows[0]);
2647
+ }
2648
+ async getTerms(taxonomy, options) {
2649
+ const conditions = [eq(cmsTermsTable.taxonomy, taxonomy)];
2650
+ if (options?.parentId !== void 0) conditions.push(eq(cmsTermsTable.parentId, options.parentId));
2651
+ return (await this.client.select().from(cmsTermsTable).where(conditions.length === 1 ? conditions[0] : and(...conditions)).execute()).map((r) => this.deserializeTerm(r));
2652
+ }
2653
+ async updateTerm(id, updates) {
2654
+ if (!await this.getTermById(id)) return null;
2655
+ const updateRecord = { updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
2656
+ if (updates.name !== void 0) updateRecord.name = updates.name;
2657
+ if (updates.slug !== void 0) updateRecord.slug = updates.slug;
2658
+ if (updates.description !== void 0) updateRecord.description = updates.description;
2659
+ if (updates.parentId !== void 0) updateRecord.parentId = updates.parentId;
2660
+ if (updates.count !== void 0) updateRecord.count = updates.count;
2661
+ if (updates.meta !== void 0) updateRecord.meta = JSON.stringify(updates.meta);
2662
+ await this.client.update(cmsTermsTable).set(updateRecord).where(eq(cmsTermsTable.id, id)).execute();
2663
+ return this.getTermById(id);
2664
+ }
2665
+ async deleteTerm(id) {
2666
+ if (!await this.getTermById(id)) return false;
2667
+ await this.client.delete(cmsTermsTable).where(eq(cmsTermsTable.id, id)).execute();
2668
+ await this.client.delete(cmsContentTermsTable).where(eq(cmsContentTermsTable.termId, id)).execute();
2669
+ return true;
2670
+ }
2671
+ async assignTermsToContent(contentId, termIds) {
2672
+ for (const termId of termIds) try {
2673
+ await this.client.insert(cmsContentTermsTable).values({
2674
+ contentId,
2675
+ termId
2676
+ }).execute();
2677
+ const term = await this.getTermById(termId);
2678
+ if (term) await this.updateTerm(termId, { count: term.count + 1 });
2679
+ } catch {}
2680
+ }
2681
+ async getContentTerms(contentId, taxonomy) {
2682
+ const termIds = (await this.client.select().from(cmsContentTermsTable).where(eq(cmsContentTermsTable.contentId, contentId)).execute()).map((r) => r.termId || r.term_id);
2683
+ if (termIds.length === 0) return [];
2684
+ const terms = [];
2685
+ for (const tId of termIds) {
2686
+ const t = await this.getTermById(tId);
2687
+ if (t && (!taxonomy || t.taxonomy === taxonomy)) terms.push(t);
2688
+ }
2689
+ return terms;
2690
+ }
2691
+ async removeTermsFromContent(contentId, termIds) {
2692
+ for (const termId of termIds) {
2693
+ await this.client.delete(cmsContentTermsTable).where(and(eq(cmsContentTermsTable.contentId, contentId), eq(cmsContentTermsTable.termId, termId))).execute();
2694
+ const term = await this.getTermById(termId);
2695
+ if (term && term.count > 0) await this.updateTerm(termId, { count: term.count - 1 });
2696
+ }
2697
+ }
2698
+ async createMedia(item) {
2699
+ const id = generateId("med_");
2700
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2701
+ const record = {
2702
+ id,
2703
+ filename: item.filename,
2704
+ originalName: item.originalName,
2705
+ mimeType: item.mimeType,
2706
+ sizeBytes: item.sizeBytes,
2707
+ url: item.url,
2708
+ path: item.path ?? null,
2709
+ width: item.width ?? null,
2710
+ height: item.height ?? null,
2711
+ altText: item.altText ?? null,
2712
+ caption: item.caption ?? null,
2713
+ authorId: item.authorId ?? null,
2714
+ variants: item.variants ? JSON.stringify(item.variants) : null,
2715
+ createdAt: now,
2716
+ updatedAt: now
2717
+ };
2718
+ await this.client.insert(cmsMediaTable).values(record).execute();
2719
+ return {
2720
+ id,
2721
+ filename: item.filename,
2722
+ originalName: item.originalName,
2723
+ mimeType: item.mimeType,
2724
+ sizeBytes: item.sizeBytes,
2725
+ url: item.url,
2726
+ path: item.path,
2727
+ width: item.width,
2728
+ height: item.height,
2729
+ altText: item.altText,
2730
+ caption: item.caption,
2731
+ authorId: item.authorId,
2732
+ variants: item.variants,
2733
+ createdAt: now,
2734
+ updatedAt: now
2735
+ };
2736
+ }
2737
+ async getMedia(id) {
2738
+ const rows = await this.client.select().from(cmsMediaTable).where(eq(cmsMediaTable.id, id)).execute();
2739
+ if (!rows || rows.length === 0) return null;
2740
+ return this.deserializeMedia(rows[0]);
2741
+ }
2742
+ async findMedia(options) {
2743
+ const conditions = [];
2744
+ if (options?.mimeType) conditions.push(like(cmsMediaTable.mimeType, `${options.mimeType}%`));
2745
+ if (options?.authorId) conditions.push(eq(cmsMediaTable.authorId, options.authorId));
2746
+ if (options?.search) conditions.push(or(like(cmsMediaTable.filename, `%${options.search}%`), like(cmsMediaTable.originalName, `%${options.search}%`)));
2747
+ const items = (await this.client.select().from(cmsMediaTable).where(conditions.length > 0 ? conditions.length === 1 ? conditions[0] : and(...conditions) : void 0).execute()).map((r) => this.deserializeMedia(r));
2748
+ const total = items.length;
2749
+ const offset = options?.offset ?? 0;
2750
+ const limit = options?.limit ?? 20;
2751
+ return {
2752
+ items: items.slice(offset, offset + limit),
2753
+ total,
2754
+ limit,
2755
+ offset,
2756
+ hasMore: offset + limit < total
2757
+ };
2758
+ }
2759
+ async updateMedia(id, updates) {
2760
+ if (!await this.getMedia(id)) return null;
2761
+ const updateRecord = { updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
2762
+ if (updates.filename !== void 0) updateRecord.filename = updates.filename;
2763
+ if (updates.altText !== void 0) updateRecord.altText = updates.altText;
2764
+ if (updates.caption !== void 0) updateRecord.caption = updates.caption;
2765
+ if (updates.width !== void 0) updateRecord.width = updates.width;
2766
+ if (updates.height !== void 0) updateRecord.height = updates.height;
2767
+ if (updates.variants !== void 0) updateRecord.variants = JSON.stringify(updates.variants);
2768
+ await this.client.update(cmsMediaTable).set(updateRecord).where(eq(cmsMediaTable.id, id)).execute();
2769
+ return this.getMedia(id);
2770
+ }
2771
+ async deleteMedia(id) {
2772
+ if (!await this.getMedia(id)) return false;
2773
+ await this.client.delete(cmsMediaTable).where(eq(cmsMediaTable.id, id)).execute();
2774
+ return true;
2775
+ }
2776
+ async getOption(key) {
2777
+ const rows = await this.client.select().from(cmsOptionsTable).where(eq(cmsOptionsTable.key, key)).execute();
2778
+ if (!rows || rows.length === 0) return null;
2779
+ const r = rows[0];
2780
+ return {
2781
+ key: r.key,
2782
+ value: typeof r.value === "string" ? JSON.parse(r.value) : r.value,
2783
+ autoload: Boolean(r.autoload),
2784
+ namespace: r.namespace || void 0,
2785
+ updatedAt: r.updatedAt || r.updated_at
2786
+ };
2787
+ }
2788
+ async setOption(item) {
2789
+ const existing = await this.getOption(item.key);
2790
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2791
+ if (existing) await this.client.update(cmsOptionsTable).set({
2792
+ value: JSON.stringify(item.value),
2793
+ autoload: item.autoload,
2794
+ namespace: item.namespace ?? null,
2795
+ updatedAt: now
2796
+ }).where(eq(cmsOptionsTable.key, item.key)).execute();
2797
+ else await this.client.insert(cmsOptionsTable).values({
2798
+ key: item.key,
2799
+ value: JSON.stringify(item.value),
2800
+ autoload: item.autoload,
2801
+ namespace: item.namespace ?? null,
2802
+ updatedAt: now
2803
+ }).execute();
2804
+ }
2805
+ async deleteOption(key) {
2806
+ if (!await this.getOption(key)) return false;
2807
+ await this.client.delete(cmsOptionsTable).where(eq(cmsOptionsTable.key, key)).execute();
2808
+ return true;
2809
+ }
2810
+ async getOptions(namespace) {
2811
+ const builder = this.client.select().from(cmsOptionsTable);
2812
+ return (await (namespace ? builder.where(eq(cmsOptionsTable.namespace, namespace)).execute() : builder.execute())).map((r) => ({
2813
+ key: r.key,
2814
+ value: typeof r.value === "string" ? JSON.parse(r.value) : r.value,
2815
+ autoload: Boolean(r.autoload),
2816
+ namespace: r.namespace || void 0,
2817
+ updatedAt: r.updatedAt || r.updated_at
2818
+ }));
2819
+ }
2820
+ deserializeContent(row) {
2821
+ return {
2822
+ id: row.id,
2823
+ collection: row.collection,
2824
+ slug: row.slug,
2825
+ status: row.status,
2826
+ title: row.title || void 0,
2827
+ parentId: row.parentId || row.parent_id || null,
2828
+ authorId: row.authorId || row.author_id || null,
2829
+ publishedAt: row.publishedAt || row.published_at || null,
2830
+ scheduledAt: row.scheduledAt || row.scheduled_at || null,
2831
+ createdAt: row.createdAt || row.created_at,
2832
+ updatedAt: row.updatedAt || row.updated_at,
2833
+ version: Number(row.version),
2834
+ locale: row.locale || void 0,
2835
+ data: typeof row.data === "string" ? JSON.parse(row.data) : row.data || {},
2836
+ terms: row.terms ? typeof row.terms === "string" ? JSON.parse(row.terms) : row.terms : void 0
2837
+ };
2838
+ }
2839
+ deserializeTerm(row) {
2840
+ return {
2841
+ id: row.id,
2842
+ taxonomy: row.taxonomy,
2843
+ name: row.name,
2844
+ slug: row.slug,
2845
+ description: row.description || void 0,
2846
+ parentId: row.parentId || row.parent_id || null,
2847
+ count: Number(row.count),
2848
+ meta: row.meta ? typeof row.meta === "string" ? JSON.parse(row.meta) : row.meta : void 0,
2849
+ createdAt: row.createdAt || row.created_at,
2850
+ updatedAt: row.updatedAt || row.updated_at
2851
+ };
2852
+ }
2853
+ deserializeMedia(row) {
2854
+ return {
2855
+ id: row.id,
2856
+ filename: row.filename,
2857
+ originalName: row.originalName || row.original_name,
2858
+ mimeType: row.mimeType || row.mime_type,
2859
+ sizeBytes: Number(row.sizeBytes || row.size_bytes),
2860
+ url: row.url,
2861
+ path: row.path || void 0,
2862
+ width: row.width ? Number(row.width) : void 0,
2863
+ height: row.height ? Number(row.height) : void 0,
2864
+ altText: row.altText || row.alt_text || void 0,
2865
+ caption: row.caption || void 0,
2866
+ authorId: row.authorId || row.author_id || null,
2867
+ variants: row.variants ? typeof row.variants === "string" ? JSON.parse(row.variants) : row.variants : void 0,
2868
+ createdAt: row.createdAt || row.created_at,
2869
+ updatedAt: row.updatedAt || row.updated_at
2870
+ };
2871
+ }
2872
+ async populateItem(item, populateFields) {
2873
+ const populated = { ...item.populated || {} };
2874
+ for (const field of populateFields) {
2875
+ const rawVal = item.data[field];
2876
+ if (!rawVal) continue;
2877
+ if (Array.isArray(rawVal)) populated[field] = await Promise.all(rawVal.map(async (id) => {
2878
+ if (typeof id === "string") {
2879
+ const rows = await this.client.select().from(cmsContentTable).where(eq(cmsContentTable.id, id)).execute();
2880
+ if (rows.length > 0) return this.deserializeContent(rows[0]);
2881
+ const m = await this.getMedia(id);
2882
+ if (m) return m;
2883
+ }
2884
+ return id;
2885
+ }));
2886
+ else if (typeof rawVal === "string") {
2887
+ const rows = await this.client.select().from(cmsContentTable).where(eq(cmsContentTable.id, rawVal)).execute();
2888
+ if (rows.length > 0) populated[field] = this.deserializeContent(rows[0]);
2889
+ else {
2890
+ const m = await this.getMedia(rawVal);
2891
+ if (m) populated[field] = m;
2892
+ }
2893
+ }
2894
+ }
2895
+ return {
2896
+ ...item,
2897
+ populated
2898
+ };
2899
+ }
2900
+ matchesCondition(actualValue, expected) {
2901
+ if (expected === null || expected === void 0 || typeof expected !== "object" || Array.isArray(expected)) return actualValue === expected;
2902
+ const exp = expected;
2903
+ if (!Object.keys(exp).some((k) => k.startsWith("$"))) return JSON.stringify(actualValue) === JSON.stringify(expected);
2904
+ for (const [op, target] of Object.entries(exp)) switch (op) {
2905
+ case "$eq":
2906
+ if (actualValue !== target) return false;
2907
+ break;
2908
+ case "$ne":
2909
+ if (actualValue === target) return false;
2910
+ break;
2911
+ case "$gt":
2912
+ if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
2913
+ if (actualValue <= target) return false;
2914
+ break;
2915
+ case "$gte":
2916
+ if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
2917
+ if (actualValue < target) return false;
2918
+ break;
2919
+ case "$lt":
2920
+ if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
2921
+ if (actualValue >= target) return false;
2922
+ break;
2923
+ case "$lte":
2924
+ if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
2925
+ if (actualValue > target) return false;
2926
+ break;
2927
+ case "$in":
2928
+ if (!Array.isArray(target) || !target.includes(actualValue)) return false;
2929
+ break;
2930
+ case "$nin":
2931
+ if (Array.isArray(target) && target.includes(actualValue)) return false;
2932
+ break;
2933
+ case "$contains":
2934
+ if (typeof actualValue !== "string" || !actualValue.toLowerCase().includes(String(target).toLowerCase())) return false;
2935
+ break;
2936
+ case "$startsWith":
2937
+ if (typeof actualValue !== "string" || !actualValue.startsWith(String(target))) return false;
2938
+ break;
2939
+ case "$endsWith":
2940
+ if (typeof actualValue !== "string" || !actualValue.endsWith(String(target))) return false;
2941
+ break;
2942
+ case "$between":
2943
+ if (!Array.isArray(target) || target.length !== 2) return false;
2944
+ if (actualValue < target[0] || actualValue > target[1]) return false;
2945
+ break;
2946
+ case "$exists":
2947
+ if (actualValue !== void 0 !== Boolean(target)) return false;
2948
+ break;
2949
+ }
2950
+ return true;
2951
+ }
2952
+ };
2953
+ //#endregion
1555
2954
  //#region src/api/router.ts
1556
2955
  function compilePath(path) {
1557
2956
  const keys = [];
@@ -1578,7 +2977,7 @@ function jsonResponse(data, status = 200, headers = {}) {
1578
2977
  "Content-Type": "application/json",
1579
2978
  "Access-Control-Allow-Origin": "*",
1580
2979
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
1581
- "Access-Control-Allow-Headers": "Content-Type, Authorization",
2980
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key",
1582
2981
  ...headers
1583
2982
  }
1584
2983
  });
@@ -1615,10 +3014,11 @@ var CMSRouter = class {
1615
3014
  headers: {
1616
3015
  "Access-Control-Allow-Origin": "*",
1617
3016
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
1618
- "Access-Control-Allow-Headers": "Content-Type, Authorization"
3017
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key"
1619
3018
  }
1620
3019
  });
1621
3020
  try {
3021
+ const user = await this.engine.auth.authenticateRequest(request);
1622
3022
  if (pathname === "/api/cron/scheduled" && method === "POST") {
1623
3023
  const published = await this.engine.processScheduledContent();
1624
3024
  return jsonResponse({
@@ -1627,10 +3027,11 @@ var CMSRouter = class {
1627
3027
  items: published
1628
3028
  });
1629
3029
  }
1630
- if (pathname.startsWith("/api/options")) return this.handleOptions(pathname, method, url, request);
1631
- if (pathname.startsWith("/api/taxonomies")) return this.handleTaxonomies(pathname, method, url, request);
1632
- if (pathname.startsWith("/api/media")) return this.handleMedia(pathname, method, url, request);
1633
- if (pathname.startsWith("/api/content")) return this.handleContent(pathname, method, url, request);
3030
+ if (pathname.startsWith("/api/auth")) return this.handleAuth(pathname, method, request, user);
3031
+ if (pathname.startsWith("/api/options")) return this.handleOptions(pathname, method, url, request, user);
3032
+ if (pathname.startsWith("/api/taxonomies")) return this.handleTaxonomies(pathname, method, url, request, user);
3033
+ if (pathname.startsWith("/api/media")) return this.handleMedia(pathname, method, url, request, user);
3034
+ if (pathname.startsWith("/api/content")) return this.handleContent(pathname, method, url, request, user);
1634
3035
  const customResponse = await this.handleCustomRoute(pathname, method, url, request);
1635
3036
  if (customResponse) return customResponse;
1636
3037
  return jsonResponse({
@@ -1664,55 +3065,126 @@ var CMSRouter = class {
1664
3065
  }
1665
3066
  return null;
1666
3067
  }
1667
- async handleContent(pathname, method, url, request) {
3068
+ async handleAuth(pathname, method, request, user) {
3069
+ if (pathname === "/api/auth/register" && method === "POST") {
3070
+ const body = await request.json();
3071
+ try {
3072
+ return jsonResponse(await this.engine.auth.register(body), 201);
3073
+ } catch (err) {
3074
+ return jsonResponse({ error: err.message }, 400);
3075
+ }
3076
+ }
3077
+ if (pathname === "/api/auth/login" && method === "POST") {
3078
+ const body = await request.json();
3079
+ try {
3080
+ return jsonResponse(await this.engine.auth.login(body.identifier || body.username || body.email, body.password), 200);
3081
+ } catch (err) {
3082
+ return jsonResponse({ error: err.message }, 401);
3083
+ }
3084
+ }
3085
+ if (pathname === "/api/auth/me" && method === "GET") {
3086
+ if (!user) return jsonResponse({ error: "Unauthorized: Authentication required" }, 401);
3087
+ return jsonResponse(user, 200);
3088
+ }
3089
+ return jsonResponse({ error: "Endpoint not found" }, 404);
3090
+ }
3091
+ async handleContent(pathname, method, url, request, user) {
1668
3092
  const parts = pathname.replace(/^\/api\/content\/?/, "").split("/").filter(Boolean);
1669
- const collectionSlug = parts[0];
1670
- if (!collectionSlug) return jsonResponse({ collections: this.engine.getCollections().map((c) => ({
3093
+ if (parts.length === 0 && method === "GET") return jsonResponse({ collections: this.engine.getCollections().map((c) => ({
1671
3094
  slug: c.slug,
1672
3095
  label: c.label,
1673
3096
  hierarchical: c.hierarchical
1674
3097
  })) }, 200);
3098
+ const collectionSlug = parts[0];
3099
+ const collConfig = this.engine.getCollectionConfig(collectionSlug);
3100
+ if (!collConfig) return jsonResponse({ error: `Collection '${collectionSlug}' not found` }, 404);
1675
3101
  const coll = this.engine.collection(collectionSlug);
1676
3102
  if (parts.length === 1 && method === "GET") {
1677
3103
  const status = url.searchParams.get("status") || void 0;
1678
3104
  const search = url.searchParams.get("search") || void 0;
3105
+ const locale = url.searchParams.get("locale") || void 0;
1679
3106
  const limit = url.searchParams.has("limit") ? Number(url.searchParams.get("limit")) : void 0;
1680
3107
  const offset = url.searchParams.has("offset") ? Number(url.searchParams.get("offset")) : void 0;
1681
3108
  const termIds = url.searchParams.has("termIds") ? url.searchParams.get("termIds").split(",") : void 0;
1682
- return jsonResponse(await coll.find({
3109
+ const populate = url.searchParams.has("populate") ? url.searchParams.get("populate").split(",") : void 0;
3110
+ const select = url.searchParams.has("select") ? url.searchParams.get("select").split(",") : void 0;
3111
+ let where = void 0;
3112
+ if (url.searchParams.has("where")) try {
3113
+ where = JSON.parse(url.searchParams.get("where"));
3114
+ } catch {}
3115
+ const result = await coll.find({
1683
3116
  status,
1684
3117
  search,
1685
3118
  limit,
1686
3119
  offset,
1687
- termIds
1688
- }));
3120
+ termIds,
3121
+ populate,
3122
+ select,
3123
+ locale,
3124
+ where
3125
+ });
3126
+ result.items = result.items.map((it) => this.filterRestrictedFields(it, collConfig, user));
3127
+ return jsonResponse(result);
1689
3128
  }
1690
3129
  if (parts.length === 1 && method === "POST") {
3130
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "edit_posts")) return jsonResponse({ error: "Forbidden: You do not have permission to create content." }, user ? 403 : 401);
1691
3131
  const body = await request.json();
1692
- return jsonResponse(await coll.create(body), 201);
3132
+ const created = await coll.create(body, user?.id);
3133
+ return jsonResponse(this.filterRestrictedFields(created, collConfig, user), 201);
1693
3134
  }
1694
3135
  const idOrSlug = parts[1];
3136
+ if (parts.length === 3 && parts[2] === "preview" && method === "GET") {
3137
+ const token = url.searchParams.get("token");
3138
+ if (!token) return jsonResponse({ error: "Missing preview token" }, 401);
3139
+ const verified = await this.engine.preview.verifyPreviewToken(token);
3140
+ if (!verified || verified.contentId !== idOrSlug || verified.collection !== collectionSlug) return jsonResponse({ error: "Invalid or expired preview token" }, 403);
3141
+ const item = await coll.findById(idOrSlug);
3142
+ if (!item) return jsonResponse({ error: "Content item not found" }, 404);
3143
+ return jsonResponse(this.filterRestrictedFields(item, collConfig, user));
3144
+ }
1695
3145
  if (parts.length === 3 && parts[2] === "revisions" && method === "GET") return jsonResponse(await coll.getRevisions(idOrSlug));
1696
3146
  if (parts.length === 4 && parts[2] === "restore" && method === "POST") {
3147
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "edit_posts")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
1697
3148
  const revisionId = parts[3];
1698
- const restored = await coll.restoreRevision(idOrSlug, revisionId);
3149
+ const restored = await coll.restoreRevision(idOrSlug, revisionId, user?.id);
1699
3150
  if (!restored) return jsonResponse({ error: "Revision or content item not found" }, 404);
1700
- return jsonResponse(restored);
3151
+ return jsonResponse(this.filterRestrictedFields(restored, collConfig, user));
1701
3152
  }
1702
3153
  if (parts.length === 2 && method === "GET") {
1703
3154
  const bySlug = url.searchParams.get("by") === "slug";
1704
- let item = bySlug ? await coll.findBySlug(idOrSlug) : await coll.findById(idOrSlug);
1705
- if (!item && !bySlug) item = await coll.findBySlug(idOrSlug);
3155
+ const populate = url.searchParams.has("populate") ? url.searchParams.get("populate").split(",") : void 0;
3156
+ const select = url.searchParams.has("select") ? url.searchParams.get("select").split(",") : void 0;
3157
+ const locale = url.searchParams.get("locale") || void 0;
3158
+ let item = bySlug ? await coll.findBySlug(idOrSlug, {
3159
+ populate,
3160
+ select,
3161
+ locale
3162
+ }) : await coll.findById(idOrSlug, {
3163
+ populate,
3164
+ select,
3165
+ locale
3166
+ });
3167
+ if (!item && !bySlug) item = await coll.findBySlug(idOrSlug, {
3168
+ populate,
3169
+ select,
3170
+ locale
3171
+ });
1706
3172
  if (!item) return jsonResponse({ error: "Content item not found" }, 404);
1707
- return jsonResponse(item);
3173
+ return jsonResponse(this.filterRestrictedFields(item, collConfig, user));
1708
3174
  }
1709
3175
  if (parts.length === 2 && method === "PUT") {
3176
+ const existing = await coll.findById(idOrSlug);
3177
+ if (!existing) return jsonResponse({ error: "Content item not found" }, 404);
3178
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "edit_posts", { contentItem: existing })) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
1710
3179
  const body = await request.json();
1711
- const updated = await coll.update(idOrSlug, body);
3180
+ const updated = await coll.update(idOrSlug, body, user?.id);
1712
3181
  if (!updated) return jsonResponse({ error: "Content item not found" }, 404);
1713
- return jsonResponse(updated);
3182
+ return jsonResponse(this.filterRestrictedFields(updated, collConfig, user));
1714
3183
  }
1715
3184
  if (parts.length === 2 && method === "DELETE") {
3185
+ const existing = await coll.findById(idOrSlug);
3186
+ if (!existing) return jsonResponse({ error: "Content item not found" }, 404);
3187
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "delete_posts", { contentItem: existing })) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
1716
3188
  if (!await coll.delete(idOrSlug)) return jsonResponse({ error: "Content item not found" }, 404);
1717
3189
  return jsonResponse({
1718
3190
  success: true,
@@ -1721,7 +3193,7 @@ var CMSRouter = class {
1721
3193
  }
1722
3194
  return jsonResponse({ error: "Method not allowed" }, 405);
1723
3195
  }
1724
- async handleTaxonomies(pathname, method, url, request) {
3196
+ async handleTaxonomies(pathname, method, url, request, user) {
1725
3197
  const parts = pathname.replace(/^\/api\/taxonomies\/?/, "").split("/").filter(Boolean);
1726
3198
  if (parts.length === 0 && method === "GET") return jsonResponse(this.engine.taxonomies.getTaxonomies());
1727
3199
  const taxonomySlug = parts[0];
@@ -1730,6 +3202,7 @@ var CMSRouter = class {
1730
3202
  return jsonResponse(await this.engine.taxonomies.getTerms(taxonomySlug));
1731
3203
  }
1732
3204
  if (parts.length === 2 && parts[1] === "terms" && method === "POST") {
3205
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "manage_taxonomies")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
1733
3206
  const body = await request.json();
1734
3207
  return jsonResponse(await this.engine.taxonomies.createTerm(taxonomySlug, body), 201);
1735
3208
  }
@@ -1739,12 +3212,14 @@ var CMSRouter = class {
1739
3212
  return jsonResponse(term);
1740
3213
  }
1741
3214
  if (parts.length === 3 && parts[1] === "terms" && method === "PUT") {
3215
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "manage_taxonomies")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
1742
3216
  const body = await request.json();
1743
3217
  const updated = await this.engine.taxonomies.updateTerm(parts[2], body);
1744
3218
  if (!updated) return jsonResponse({ error: "Term not found" }, 404);
1745
3219
  return jsonResponse(updated);
1746
3220
  }
1747
3221
  if (parts.length === 3 && parts[1] === "terms" && method === "DELETE") {
3222
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "manage_taxonomies")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
1748
3223
  if (!await this.engine.taxonomies.deleteTerm(parts[2])) return jsonResponse({ error: "Term not found" }, 404);
1749
3224
  return jsonResponse({
1750
3225
  success: true,
@@ -1753,7 +3228,7 @@ var CMSRouter = class {
1753
3228
  }
1754
3229
  return jsonResponse({ error: "Endpoint not found" }, 404);
1755
3230
  }
1756
- async handleMedia(pathname, method, url, request) {
3231
+ async handleMedia(pathname, method, url, request, user) {
1757
3232
  const parts = pathname.replace(/^\/api\/media\/?/, "").split("/").filter(Boolean);
1758
3233
  if (parts.length === 0 && method === "GET") {
1759
3234
  const search = url.searchParams.get("search") || void 0;
@@ -1768,8 +3243,25 @@ var CMSRouter = class {
1768
3243
  }));
1769
3244
  }
1770
3245
  if (parts.length === 0 && method === "POST") {
3246
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "upload_files")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
3247
+ if ((request.headers.get("content-type") || request.headers.get("Content-Type") || "").includes("multipart/form-data")) {
3248
+ const formData = await request.formData();
3249
+ const file = formData.get("file");
3250
+ if (!file || typeof file.arrayBuffer !== "function") return jsonResponse({ error: "No file uploaded in 'file' field" }, 400);
3251
+ const buffer = new Uint8Array(await file.arrayBuffer());
3252
+ const altText = formData.get("altText") || void 0;
3253
+ const caption = formData.get("caption") || void 0;
3254
+ return jsonResponse(await this.engine.media.upload({
3255
+ filename: file.name,
3256
+ mimeType: file.type || "application/octet-stream",
3257
+ buffer,
3258
+ sizeBytes: file.size,
3259
+ altText,
3260
+ caption
3261
+ }, user?.id), 201);
3262
+ }
1771
3263
  const body = await request.json();
1772
- return jsonResponse(await this.engine.media.upload(body), 201);
3264
+ return jsonResponse(await this.engine.media.upload(body, user?.id), 201);
1773
3265
  }
1774
3266
  const id = parts[0];
1775
3267
  if (parts.length === 1 && method === "GET") {
@@ -1778,12 +3270,14 @@ var CMSRouter = class {
1778
3270
  return jsonResponse(media);
1779
3271
  }
1780
3272
  if (parts.length === 1 && method === "PUT") {
3273
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "upload_files")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
1781
3274
  const body = await request.json();
1782
3275
  const updated = await this.engine.media.updateMetadata(id, body);
1783
3276
  if (!updated) return jsonResponse({ error: "Media not found" }, 404);
1784
3277
  return jsonResponse(updated);
1785
3278
  }
1786
3279
  if (parts.length === 1 && method === "DELETE") {
3280
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "delete_files")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
1787
3281
  if (!await this.engine.media.delete(id)) return jsonResponse({ error: "Media not found" }, 404);
1788
3282
  return jsonResponse({
1789
3283
  success: true,
@@ -1792,7 +3286,7 @@ var CMSRouter = class {
1792
3286
  }
1793
3287
  return jsonResponse({ error: "Method not allowed" }, 405);
1794
3288
  }
1795
- async handleOptions(pathname, method, url, request) {
3289
+ async handleOptions(pathname, method, url, request, user) {
1796
3290
  const key = pathname.replace(/^\/api\/options\/?/, "");
1797
3291
  if (!key && method === "GET") {
1798
3292
  const namespace = url.searchParams.get("namespace") || void 0;
@@ -1807,6 +3301,7 @@ var CMSRouter = class {
1807
3301
  });
1808
3302
  }
1809
3303
  if (key && method === "PUT") {
3304
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "manage_options")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
1810
3305
  const body = await request.json();
1811
3306
  return jsonResponse(await this.engine.options.set(key, body.value, {
1812
3307
  autoload: body.autoload,
@@ -1814,6 +3309,7 @@ var CMSRouter = class {
1814
3309
  }));
1815
3310
  }
1816
3311
  if (key && method === "DELETE") {
3312
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "manage_options")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
1817
3313
  if (!await this.engine.options.delete(key)) return jsonResponse({ error: "Option not found" }, 404);
1818
3314
  return jsonResponse({
1819
3315
  success: true,
@@ -1822,6 +3318,17 @@ var CMSRouter = class {
1822
3318
  }
1823
3319
  return jsonResponse({ error: "Method not allowed" }, 405);
1824
3320
  }
3321
+ filterRestrictedFields(item, collConfig, user) {
3322
+ if (!collConfig.fields || !item.data) return item;
3323
+ const sanitizedData = { ...item.data };
3324
+ for (const field of collConfig.fields) if (field.readRoles && field.readRoles.length > 0) {
3325
+ if (!(user?.role === "admin" || user?.role && field.readRoles.includes(user.role))) delete sanitizedData[field.name];
3326
+ }
3327
+ return {
3328
+ ...item,
3329
+ data: sanitizedData
3330
+ };
3331
+ }
1825
3332
  };
1826
3333
  function createCMSRouter(engine) {
1827
3334
  return new CMSRouter(engine);
@@ -6630,6 +8137,6 @@ function getTransferService(engine, options) {
6630
8137
  return service;
6631
8138
  }
6632
8139
  //#endregion
6633
- export { CMSClient, CMSEngine, CMSRouter, ContentLifecycle, DEFAULT_COLLECTIONS, DEFAULT_ROLE_CAPABILITIES, EcommerceClient, EcommerceService, HRMSClient, HRMSService, HRMS_EMPLOYEE_TRANSFER_PRESET, HRMS_EMPLOYER_TRANSFER_PRESET, HooksManager, MediaManager, MemoryStorageAdapter, OptionsManager, RBACManager, RevisionManager, TaxonomyManager, TransferClient, TransferService, VALID_STATUS_TRANSITIONS, applyFieldTransform, collection, createAttendanceCollection, createCMSEngine, createCMSRouter, createCmsClient, createDiscountCollection, createEcommerceTaxonomies, createEmployeeCollection, createEmployerCollection, createHRMSTaxonomies, createLeaveRequestCollection, createLeaveTypeCollection, createOrderCollection, createProductCollection, defaultHooks, defineConfig, definePlugin, detectFormat, ecommercePlugin, fields, generateSuggestedMapping, getEcommerceClient, getEcommerceService, getHRMSClient, getHRMSService, getTransferClient, getTransferService, hrmsPlugin, inspectSource, mapCmsItemForExport, mapSourceRecord, normalizeConfig, parseCsvSource, parseExcelSource, parseJsonSource, parseSource, resolveUniqueSlug, serializeCsv, serializeExcel, serializeJson, serializeSource, slugify, summarizeCollection, transferPlugin, validateAndNormalizeData, validateTransferBatch };
8140
+ export { AuthService, CMSClient, CMSEngine, CMSRouter, ContentLifecycle, DEFAULT_COLLECTIONS, DEFAULT_ROLE_CAPABILITIES, DiskMediaStorageDriver, EcommerceClient, EcommerceService, HRMSClient, HRMSService, HRMS_EMPLOYEE_TRANSFER_PRESET, HRMS_EMPLOYER_TRANSFER_PRESET, HooksManager, MediaManager, MemoryStorageAdapter, OptionsManager, PersistenceStorageAdapter, PreviewManager, RBACManager, RevisionManager, TaxonomyManager, TransferClient, TransferService, VALID_STATUS_TRANSITIONS, WebhookManager, applyFieldTransform, collection, computeHmacSignature, createAttendanceCollection, createCMSEngine, createCMSRouter, createCmsClient, createDiscountCollection, createEcommerceTaxonomies, createEmployeeCollection, createEmployerCollection, createHRMSTaxonomies, createLeaveRequestCollection, createLeaveTypeCollection, createOrderCollection, createProductCollection, defaultHooks, defineConfig, definePlugin, detectFormat, ecommercePlugin, fields, generateSuggestedMapping, getEcommerceClient, getEcommerceService, getHRMSClient, getHRMSService, getTransferClient, getTransferService, hashPassword, hrmsPlugin, inspectSource, mapCmsItemForExport, mapSourceRecord, mergeLocalizedInput, normalizeConfig, parseCsvSource, parseExcelSource, parseJsonSource, parseSource, resolveLocalizedData, resolveUniqueSlug, serializeCsv, serializeExcel, serializeJson, serializeSource, signJwt, slugify, summarizeCollection, transferPlugin, validateAndNormalizeData, validateTransferBatch, verifyJwt, verifyPassword };
6634
8141
 
6635
8142
  //# sourceMappingURL=index.mjs.map