@azlib/cms 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -21,6 +21,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
+ let _azlib_validator = require("@azlib/validator");
25
+ let node_fs = require("node:fs");
26
+ let node_path = require("node:path");
27
+ node_path = __toESM(node_path, 1);
28
+ let _azlib_persistence = require("@azlib/persistence");
24
29
  let exceljs = require("exceljs");
25
30
  exceljs = __toESM(exceljs, 1);
26
31
  //#region src/content/schema.ts
@@ -38,7 +43,10 @@ const fields = {
38
43
  label: options.label ?? "Slug",
39
44
  fromField: options.from ?? "title",
40
45
  required: options.required ?? false,
41
- unique: options.unique ?? true
46
+ unique: options.unique ?? true,
47
+ readRoles: options.readRoles,
48
+ writeRoles: options.writeRoles,
49
+ localized: options.localized
42
50
  };
43
51
  },
44
52
  richText(options) {
@@ -101,6 +109,31 @@ const fields = {
101
109
  type: "repeater",
102
110
  ...options
103
111
  };
112
+ },
113
+ blocks(options) {
114
+ return {
115
+ type: "blocks",
116
+ ...options
117
+ };
118
+ },
119
+ email(options) {
120
+ return {
121
+ type: "email",
122
+ ...options
123
+ };
124
+ },
125
+ url(options) {
126
+ return {
127
+ type: "url",
128
+ ...options
129
+ };
130
+ },
131
+ array(options) {
132
+ return {
133
+ type: "array",
134
+ defaultValue: [],
135
+ ...options
136
+ };
104
137
  }
105
138
  };
106
139
  /**
@@ -133,19 +166,121 @@ function validateAndNormalizeData(fieldsList, inputData) {
133
166
  for (const field of fieldsList) {
134
167
  let val = normalized[field.name];
135
168
  if (val === void 0 && field.defaultValue !== void 0) {
136
- val = field.defaultValue;
169
+ val = typeof field.defaultValue === "object" && field.defaultValue !== null ? JSON.parse(JSON.stringify(field.defaultValue)) : field.defaultValue;
137
170
  normalized[field.name] = val;
138
171
  }
139
- if (field.required && (val === void 0 || val === null || val === "")) {
172
+ const isBlank = val === void 0 || val === null || val === "" || Array.isArray(val) && val.length === 0;
173
+ if (field.required && isBlank) {
140
174
  errors[field.name] = `${field.label || field.name} is required.`;
141
175
  continue;
142
176
  }
143
- if (val !== void 0 && val !== null) {
144
- if (field.type === "number" && typeof val !== "number") {
145
- const parsed = Number(val);
146
- if (Number.isNaN(parsed)) errors[field.name] = `${field.label || field.name} must be a valid number.`;
147
- else normalized[field.name] = parsed;
148
- } else if (field.type === "boolean" && typeof val !== "boolean") normalized[field.name] = Boolean(val);
177
+ if (val !== void 0 && val !== null && val !== "") switch (field.type) {
178
+ case "number": {
179
+ let num = val;
180
+ if (typeof num !== "number") {
181
+ const parsed = Number(num);
182
+ if (Number.isNaN(parsed)) {
183
+ errors[field.name] = `${field.label || field.name} must be a valid number.`;
184
+ break;
185
+ }
186
+ num = parsed;
187
+ normalized[field.name] = num;
188
+ }
189
+ if (field.min !== void 0 && num < field.min) errors[field.name] = `${field.label || field.name} must be at least ${field.min}.`;
190
+ if (field.max !== void 0 && num > field.max) errors[field.name] = `${field.label || field.name} must be at most ${field.max}.`;
191
+ break;
192
+ }
193
+ case "boolean":
194
+ if (typeof val !== "boolean") normalized[field.name] = Boolean(val);
195
+ break;
196
+ case "text":
197
+ case "slug":
198
+ case "richText": {
199
+ const strVal = String(val);
200
+ if (field.min !== void 0 && strVal.length < field.min) errors[field.name] = `${field.label || field.name} must have at least ${field.min} characters.`;
201
+ if (field.max !== void 0 && strVal.length > field.max) errors[field.name] = `${field.label || field.name} must have at most ${field.max} characters.`;
202
+ if (field.pattern) try {
203
+ if (!new RegExp(field.pattern).test(strVal)) errors[field.name] = `${field.label || field.name} format is invalid.`;
204
+ } catch {}
205
+ break;
206
+ }
207
+ case "email": {
208
+ const emailStr = String(val).trim();
209
+ if (!(0, _azlib_validator.isEmail)(emailStr)) errors[field.name] = `${field.label || field.name} must be a valid email address.`;
210
+ else normalized[field.name] = emailStr.toLowerCase();
211
+ break;
212
+ }
213
+ case "url": {
214
+ const urlStr = String(val).trim();
215
+ if (!(0, _azlib_validator.isURL)(urlStr)) errors[field.name] = `${field.label || field.name} must be a valid URL.`;
216
+ else normalized[field.name] = urlStr;
217
+ break;
218
+ }
219
+ case "select":
220
+ if (field.options && field.options.length > 0) {
221
+ const validValues = field.options.map((opt) => typeof opt === "object" && opt !== null ? opt.value : opt);
222
+ if (!validValues.includes(val)) errors[field.name] = `${field.label || field.name} must be one of: ${validValues.join(", ")}.`;
223
+ }
224
+ break;
225
+ case "repeater":
226
+ if (!Array.isArray(val)) {
227
+ errors[field.name] = `${field.label || field.name} must be an array.`;
228
+ break;
229
+ }
230
+ if (field.min !== void 0 && val.length < field.min) errors[field.name] = `${field.label || field.name} must have at least ${field.min} items.`;
231
+ if (field.max !== void 0 && val.length > field.max) errors[field.name] = `${field.label || field.name} must have at most ${field.max} items.`;
232
+ if (field.fields && field.fields.length > 0) {
233
+ const normalizedItems = [];
234
+ for (let i = 0; i < val.length; i++) {
235
+ const item = val[i];
236
+ if (typeof item !== "object" || item === null) {
237
+ errors[`${field.name}[${i}]`] = `Item must be an object.`;
238
+ continue;
239
+ }
240
+ const { data: itemData, errors: itemErrors } = validateAndNormalizeData(field.fields, item);
241
+ for (const [errKey, errMsg] of Object.entries(itemErrors)) errors[`${field.name}[${i}].${errKey}`] = errMsg;
242
+ normalizedItems.push(itemData);
243
+ }
244
+ normalized[field.name] = normalizedItems;
245
+ }
246
+ break;
247
+ case "blocks": {
248
+ if (!Array.isArray(val)) {
249
+ errors[field.name] = `${field.label || field.name} must be an array of blocks.`;
250
+ break;
251
+ }
252
+ if (field.min !== void 0 && val.length < field.min) errors[field.name] = `${field.label || field.name} must have at least ${field.min} blocks.`;
253
+ if (field.max !== void 0 && val.length > field.max) errors[field.name] = `${field.label || field.name} must have at most ${field.max} blocks.`;
254
+ const normalizedBlocks = [];
255
+ for (let i = 0; i < val.length; i++) {
256
+ const block = val[i];
257
+ if (typeof block !== "object" || block === null) {
258
+ errors[`${field.name}[${i}]`] = `Block must be an object.`;
259
+ continue;
260
+ }
261
+ const blockType = block.blockType;
262
+ if (typeof blockType !== "string" || !blockType) {
263
+ errors[`${field.name}[${i}].blockType`] = `Block missing 'blockType' identifier.`;
264
+ continue;
265
+ }
266
+ const blockDef = field.blocks?.find((b) => b.slug === blockType);
267
+ if (!blockDef) {
268
+ errors[`${field.name}[${i}].blockType`] = `Unknown block type '${blockType}'.`;
269
+ continue;
270
+ }
271
+ const { data: blockData, errors: blockErrors } = validateAndNormalizeData(blockDef.fields, block);
272
+ for (const [errKey, errMsg] of Object.entries(blockErrors)) errors[`${field.name}[${i}].${errKey}`] = errMsg;
273
+ normalizedBlocks.push({
274
+ ...blockData,
275
+ blockType
276
+ });
277
+ }
278
+ normalized[field.name] = normalizedBlocks;
279
+ break;
280
+ }
281
+ case "array":
282
+ if (!Array.isArray(val)) errors[field.name] = `${field.label || field.name} must be an array.`;
283
+ break;
149
284
  }
150
285
  if (field.validate && val !== void 0) {
151
286
  const res = field.validate(val, normalized);
@@ -259,7 +394,11 @@ function normalizeConfig(config) {
259
394
  route: config.admin?.route ?? "/admin",
260
395
  enableRegistration: config.admin?.enableRegistration ?? false,
261
396
  ...config.admin
262
- }
397
+ },
398
+ auth: config.auth,
399
+ webhooks: config.webhooks ?? [],
400
+ media: config.media,
401
+ i18n: config.i18n
263
402
  };
264
403
  }
265
404
  //#endregion
@@ -802,17 +941,21 @@ var MediaManager = class {
802
941
  storage;
803
942
  hooks;
804
943
  publicBaseUrl;
944
+ driver;
805
945
  allowedMimePrefixes = [
806
946
  "image/",
807
947
  "video/",
808
948
  "audio/",
809
949
  "application/pdf",
810
- "text/"
950
+ "text/",
951
+ "application/msword",
952
+ "application/vnd."
811
953
  ];
812
- constructor(storage, hooks, publicBaseUrl = "/uploads") {
954
+ constructor(storage, hooks, publicBaseUrl = "/uploads", driver) {
813
955
  this.storage = storage;
814
956
  this.hooks = hooks;
815
957
  this.publicBaseUrl = publicBaseUrl;
958
+ this.driver = driver;
816
959
  }
817
960
  /**
818
961
  * Upload / register a new media item.
@@ -822,13 +965,26 @@ var MediaManager = class {
822
965
  const extMatch = input.filename.match(/\.([a-zA-Z0-9]+)$/);
823
966
  const ext = extMatch ? `.${extMatch[1].toLowerCase()}` : "";
824
967
  const cleanFilename = `${slugify(input.filename.replace(/\.[^/.]+$/, ""))}${ext}`;
825
- const url = input.url ?? `${this.publicBaseUrl}/${cleanFilename}`;
968
+ let url = input.url ?? `${this.publicBaseUrl}/${cleanFilename}`;
969
+ let path = input.path;
970
+ let sizeBytes = input.sizeBytes ?? (input.buffer ? input.buffer.byteLength : 0);
971
+ if (input.buffer && this.driver) {
972
+ const written = await this.driver.write({
973
+ filename: cleanFilename,
974
+ buffer: input.buffer,
975
+ mimeType: input.mimeType
976
+ });
977
+ url = written.url;
978
+ path = written.path;
979
+ sizeBytes = written.sizeBytes;
980
+ }
826
981
  const media = await this.storage.createMedia({
827
982
  filename: cleanFilename,
828
983
  originalName: input.filename,
829
984
  mimeType: input.mimeType,
830
- sizeBytes: input.sizeBytes,
985
+ sizeBytes,
831
986
  url,
987
+ path,
832
988
  width: input.width,
833
989
  height: input.height,
834
990
  altText: input.altText,
@@ -866,7 +1022,10 @@ var MediaManager = class {
866
1022
  const media = await this.storage.getMedia(id);
867
1023
  if (!media) return false;
868
1024
  const deleted = await this.storage.deleteMedia(id);
869
- if (deleted && this.hooks) await this.hooks.doAction("cms.media_deleted", media);
1025
+ if (deleted) {
1026
+ if (media.path && this.driver) await this.driver.delete(media.path);
1027
+ if (this.hooks) await this.hooks.doAction("cms.media_deleted", media);
1028
+ }
870
1029
  return deleted;
871
1030
  }
872
1031
  };
@@ -959,10 +1118,481 @@ var RBACManager = class {
959
1118
  }
960
1119
  };
961
1120
  //#endregion
1121
+ //#region src/auth/jwt.ts
1122
+ /**
1123
+ * @azlib/cms - Universal Web Crypto JWT & Password Hashing
1124
+ */
1125
+ function base64UrlEncode(buffer) {
1126
+ let binary = "";
1127
+ for (let i = 0; i < buffer.byteLength; i++) binary += String.fromCharCode(buffer[i]);
1128
+ return btoa(binary).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
1129
+ }
1130
+ function base64UrlDecode(str) {
1131
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
1132
+ while (base64.length % 4) base64 += "=";
1133
+ const binary = atob(base64);
1134
+ const bytes = new Uint8Array(binary.length);
1135
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
1136
+ return bytes;
1137
+ }
1138
+ async function signJwt(payload, secret, expiresInSeconds = 86400) {
1139
+ const header = {
1140
+ alg: "HS256",
1141
+ typ: "JWT"
1142
+ };
1143
+ const now = Math.floor(Date.now() / 1e3);
1144
+ const fullPayload = {
1145
+ ...payload,
1146
+ iat: now,
1147
+ exp: now + expiresInSeconds
1148
+ };
1149
+ const enc = new TextEncoder();
1150
+ const dataToSign = `${base64UrlEncode(enc.encode(JSON.stringify(header)))}.${base64UrlEncode(enc.encode(JSON.stringify(fullPayload)))}`;
1151
+ const key = await crypto.subtle.importKey("raw", enc.encode(secret), {
1152
+ name: "HMAC",
1153
+ hash: "SHA-256"
1154
+ }, false, ["sign"]);
1155
+ const signature = await crypto.subtle.sign("HMAC", key, enc.encode(dataToSign));
1156
+ return `${dataToSign}.${base64UrlEncode(new Uint8Array(signature))}`;
1157
+ }
1158
+ async function verifyJwt(token, secret) {
1159
+ if (!token || typeof token !== "string") return null;
1160
+ const parts = token.split(".");
1161
+ if (parts.length !== 3) return null;
1162
+ const [headerEncoded, payloadEncoded, signatureEncoded] = parts;
1163
+ const dataToVerify = `${headerEncoded}.${payloadEncoded}`;
1164
+ try {
1165
+ const enc = new TextEncoder();
1166
+ const key = await crypto.subtle.importKey("raw", enc.encode(secret), {
1167
+ name: "HMAC",
1168
+ hash: "SHA-256"
1169
+ }, false, ["verify"]);
1170
+ const signature = base64UrlDecode(signatureEncoded);
1171
+ if (!await crypto.subtle.verify("HMAC", key, signature, enc.encode(dataToVerify))) return null;
1172
+ const payloadJson = new TextDecoder().decode(base64UrlDecode(payloadEncoded));
1173
+ const payload = JSON.parse(payloadJson);
1174
+ if (payload.exp && Math.floor(Date.now() / 1e3) > payload.exp) return null;
1175
+ return payload;
1176
+ } catch {
1177
+ return null;
1178
+ }
1179
+ }
1180
+ async function hashPassword(password) {
1181
+ const enc = new TextEncoder();
1182
+ const saltBytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
1183
+ const saltHex = Array.from(saltBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1184
+ const keyMaterial = await crypto.subtle.importKey("raw", enc.encode(password), { name: "PBKDF2" }, false, ["deriveBits"]);
1185
+ const derived = await crypto.subtle.deriveBits({
1186
+ name: "PBKDF2",
1187
+ salt: saltBytes,
1188
+ iterations: 1e5,
1189
+ hash: "SHA-256"
1190
+ }, keyMaterial, 256);
1191
+ return `pbkdf2$100000$${saltHex}$${Array.from(new Uint8Array(derived)).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
1192
+ }
1193
+ async function verifyPassword(password, storedHash) {
1194
+ const parts = storedHash.split("$");
1195
+ if (parts.length !== 4 || parts[0] !== "pbkdf2") return false;
1196
+ const iterations = Number(parts[1]);
1197
+ const saltHex = parts[2];
1198
+ const expectedHash = parts[3];
1199
+ const saltBytes = new Uint8Array(saltHex.match(/.{1,2}/g)?.map((byte) => parseInt(byte, 16)) || []);
1200
+ const enc = new TextEncoder();
1201
+ const keyMaterial = await crypto.subtle.importKey("raw", enc.encode(password), { name: "PBKDF2" }, false, ["deriveBits"]);
1202
+ const derived = await crypto.subtle.deriveBits({
1203
+ name: "PBKDF2",
1204
+ salt: saltBytes,
1205
+ iterations,
1206
+ hash: "SHA-256"
1207
+ }, keyMaterial, 256);
1208
+ return Array.from(new Uint8Array(derived)).map((b) => b.toString(16).padStart(2, "0")).join("") === expectedHash;
1209
+ }
1210
+ //#endregion
1211
+ //#region src/auth/auth-service.ts
1212
+ var AuthService = class {
1213
+ config;
1214
+ rbac;
1215
+ users = /* @__PURE__ */ new Map();
1216
+ defaultSecret;
1217
+ constructor(config = {}, rbac) {
1218
+ this.config = config;
1219
+ this.rbac = rbac;
1220
+ this.defaultSecret = config.jwtSecret || "azlib_cms_secret_key_change_in_production";
1221
+ }
1222
+ get enabled() {
1223
+ return Boolean(this.config.enabled);
1224
+ }
1225
+ /**
1226
+ * Register a new user account.
1227
+ */
1228
+ async register(input) {
1229
+ if (Array.from(this.users.values()).find((u) => u.username.toLowerCase() === input.username.toLowerCase())) throw new Error(`Username '${input.username}' is already taken.`);
1230
+ if (Array.from(this.users.values()).find((u) => u.email.toLowerCase() === input.email.toLowerCase())) throw new Error(`Email '${input.email}' is already registered.`);
1231
+ const id = `usr_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`;
1232
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1233
+ const passwordHash = await hashPassword(input.password);
1234
+ const role = input.role || "subscriber";
1235
+ const record = {
1236
+ id,
1237
+ username: input.username,
1238
+ email: input.email.toLowerCase(),
1239
+ displayName: input.displayName || input.username,
1240
+ role,
1241
+ passwordHash,
1242
+ active: true,
1243
+ createdAt: now,
1244
+ updatedAt: now
1245
+ };
1246
+ this.users.set(id, record);
1247
+ return this.sanitizeUser(record);
1248
+ }
1249
+ /**
1250
+ * Authenticate with email/username and password.
1251
+ */
1252
+ async login(identifier, password) {
1253
+ const normalized = identifier.toLowerCase().trim();
1254
+ const record = Array.from(this.users.values()).find((u) => u.username.toLowerCase() === normalized || u.email.toLowerCase() === normalized);
1255
+ if (!record || record.active === false) throw new Error("Invalid username or password.");
1256
+ if (!await verifyPassword(password, record.passwordHash)) throw new Error("Invalid username or password.");
1257
+ const user = this.sanitizeUser(record);
1258
+ return {
1259
+ user,
1260
+ token: await signJwt({
1261
+ sub: user.id,
1262
+ role: user.role,
1263
+ username: user.username,
1264
+ email: user.email
1265
+ }, this.defaultSecret, typeof this.config.tokenExpiresIn === "number" ? this.config.tokenExpiresIn : 86400 * 7)
1266
+ };
1267
+ }
1268
+ /**
1269
+ * Extract and authenticate user from Request (Bearer JWT, ApiKey, or X-API-Key).
1270
+ */
1271
+ async authenticateRequest(request) {
1272
+ const authHeader = request.headers.get("authorization") || request.headers.get("Authorization");
1273
+ const apiKeyHeader = request.headers.get("x-api-key") || request.headers.get("X-API-Key");
1274
+ if (apiKeyHeader && this.config.apiKeys) {
1275
+ const match = this.config.apiKeys.find((k) => k.key === apiKeyHeader);
1276
+ if (match) return {
1277
+ id: `api_key_${match.key.substring(0, 8)}`,
1278
+ username: match.name || "API Key User",
1279
+ email: `${match.role}@system.local`,
1280
+ displayName: match.name || "API Key",
1281
+ role: match.role,
1282
+ capabilities: match.capabilities,
1283
+ active: true
1284
+ };
1285
+ }
1286
+ if (authHeader) {
1287
+ const [type, token] = authHeader.trim().split(/\s+/);
1288
+ if (type?.toLowerCase() === "apikey" && this.config.apiKeys) {
1289
+ const match = this.config.apiKeys.find((k) => k.key === token);
1290
+ if (match) return {
1291
+ id: `api_key_${match.key.substring(0, 8)}`,
1292
+ username: match.name || "API Key User",
1293
+ email: `${match.role}@system.local`,
1294
+ displayName: match.name || "API Key",
1295
+ role: match.role,
1296
+ capabilities: match.capabilities,
1297
+ active: true
1298
+ };
1299
+ }
1300
+ if (type?.toLowerCase() === "bearer" && token) {
1301
+ const payload = await verifyJwt(token, this.defaultSecret);
1302
+ if (payload) {
1303
+ const registeredUser = this.users.get(payload.sub);
1304
+ if (registeredUser) return this.sanitizeUser(registeredUser);
1305
+ return {
1306
+ id: payload.sub,
1307
+ username: payload.username,
1308
+ email: payload.email,
1309
+ displayName: payload.username,
1310
+ role: payload.role,
1311
+ active: true
1312
+ };
1313
+ }
1314
+ }
1315
+ }
1316
+ const cookieHeader = request.headers.get("cookie");
1317
+ if (cookieHeader) {
1318
+ const cookies = Object.fromEntries(cookieHeader.split(";").map((c) => {
1319
+ const [k, v] = c.trim().split("=");
1320
+ return [k, decodeURIComponent(v || "")];
1321
+ }));
1322
+ const token = cookies.access_token || cookies.at;
1323
+ if (token) {
1324
+ const payload = await verifyJwt(token, this.defaultSecret);
1325
+ if (payload) {
1326
+ const registeredUser = this.users.get(payload.sub);
1327
+ if (registeredUser) return this.sanitizeUser(registeredUser);
1328
+ return {
1329
+ id: payload.sub,
1330
+ username: payload.username,
1331
+ email: payload.email,
1332
+ displayName: payload.username,
1333
+ role: payload.role,
1334
+ active: true
1335
+ };
1336
+ }
1337
+ }
1338
+ }
1339
+ return null;
1340
+ }
1341
+ /**
1342
+ * Find a user by ID.
1343
+ */
1344
+ getUserById(id) {
1345
+ const u = this.users.get(id);
1346
+ return u ? this.sanitizeUser(u) : null;
1347
+ }
1348
+ sanitizeUser(record) {
1349
+ return {
1350
+ id: record.id,
1351
+ username: record.username,
1352
+ email: record.email,
1353
+ displayName: record.displayName,
1354
+ role: record.role,
1355
+ capabilities: record.capabilities,
1356
+ active: record.active
1357
+ };
1358
+ }
1359
+ };
1360
+ //#endregion
1361
+ //#region src/core/webhooks.ts
1362
+ function bufferToHex(buffer) {
1363
+ return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
1364
+ }
1365
+ async function computeHmacSignature(payload, secret) {
1366
+ const enc = new TextEncoder();
1367
+ const key = await crypto.subtle.importKey("raw", enc.encode(secret), {
1368
+ name: "HMAC",
1369
+ hash: "SHA-256"
1370
+ }, false, ["sign"]);
1371
+ return `sha256=${bufferToHex(await crypto.subtle.sign("HMAC", key, enc.encode(payload)))}`;
1372
+ }
1373
+ var WebhookManager = class {
1374
+ hooks;
1375
+ webhooks = /* @__PURE__ */ new Map();
1376
+ fetchFn;
1377
+ constructor(initialWebhooks = [], hooks, fetchFn) {
1378
+ this.hooks = hooks;
1379
+ this.fetchFn = fetchFn ?? globalThis.fetch?.bind(globalThis);
1380
+ for (const wh of initialWebhooks) this.registerWebhook(wh);
1381
+ }
1382
+ /**
1383
+ * Register a new webhook endpoint.
1384
+ */
1385
+ registerWebhook(webhook) {
1386
+ const id = webhook.id || `wh_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 7)}`;
1387
+ this.webhooks.set(id, {
1388
+ ...webhook,
1389
+ id,
1390
+ enabled: webhook.enabled ?? true
1391
+ });
1392
+ return id;
1393
+ }
1394
+ /**
1395
+ * Get all registered webhooks.
1396
+ */
1397
+ getWebhooks() {
1398
+ return Array.from(this.webhooks.values());
1399
+ }
1400
+ /**
1401
+ * Delete a webhook by ID.
1402
+ */
1403
+ deleteWebhook(id) {
1404
+ return this.webhooks.delete(id);
1405
+ }
1406
+ /**
1407
+ * Dispatch an event to all matching webhooks asynchronously.
1408
+ */
1409
+ async dispatch(event, data) {
1410
+ const matching = Array.from(this.webhooks.values()).filter((wh) => wh.enabled !== false && (wh.events.includes(event) || wh.events.includes("*")));
1411
+ if (matching.length === 0) return [];
1412
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1413
+ const payloadString = JSON.stringify({
1414
+ event,
1415
+ timestamp: now,
1416
+ data
1417
+ });
1418
+ const deliveryPromises = matching.map(async (webhook) => {
1419
+ try {
1420
+ const headers = {
1421
+ "Content-Type": "application/json",
1422
+ "User-Agent": "azlib-cms-webhooks/1.0",
1423
+ "X-CMS-Event": event,
1424
+ "X-CMS-Delivery-Time": now,
1425
+ ...webhook.headers || {}
1426
+ };
1427
+ if (webhook.secret) headers["X-CMS-Signature"] = await computeHmacSignature(payloadString, webhook.secret);
1428
+ const res = await this.fetchFn(webhook.url, {
1429
+ method: "POST",
1430
+ headers,
1431
+ body: payloadString
1432
+ });
1433
+ if (this.hooks) await this.hooks.doAction("cms.webhook_delivered", {
1434
+ webhook,
1435
+ event,
1436
+ status: res.status,
1437
+ ok: res.ok
1438
+ });
1439
+ return {
1440
+ url: webhook.url,
1441
+ success: res.ok,
1442
+ status: res.status
1443
+ };
1444
+ } catch (error) {
1445
+ if (this.hooks) await this.hooks.doAction("cms.webhook_failed", {
1446
+ webhook,
1447
+ event,
1448
+ error: error.message
1449
+ });
1450
+ return {
1451
+ url: webhook.url,
1452
+ success: false
1453
+ };
1454
+ }
1455
+ });
1456
+ return Promise.all(deliveryPromises);
1457
+ }
1458
+ };
1459
+ //#endregion
1460
+ //#region src/core/preview.ts
1461
+ /**
1462
+ * @azlib/cms - Draft Preview Mode Engine
1463
+ */
1464
+ var PreviewManager = class {
1465
+ secret;
1466
+ constructor(secret = "azlib_preview_secret_token_default") {
1467
+ this.secret = secret;
1468
+ }
1469
+ /**
1470
+ * Mint a signed draft preview token.
1471
+ */
1472
+ async createPreviewToken(contentId, collection, expiresInSeconds = 3600) {
1473
+ return signJwt({
1474
+ contentId,
1475
+ collection,
1476
+ scope: "preview"
1477
+ }, this.secret, expiresInSeconds);
1478
+ }
1479
+ /**
1480
+ * Verify and unpack a draft preview token.
1481
+ */
1482
+ async verifyPreviewToken(token) {
1483
+ const payload = await verifyJwt(token, this.secret);
1484
+ if (!payload || payload.scope !== "preview") return null;
1485
+ return {
1486
+ contentId: payload.contentId,
1487
+ collection: payload.collection
1488
+ };
1489
+ }
1490
+ };
1491
+ //#endregion
1492
+ //#region src/content/i18n.ts
1493
+ /**
1494
+ * Resolve localized fields on a content record for a target locale.
1495
+ */
1496
+ function resolveLocalizedData(data, fieldsList, targetLocale, fallbackLocale = "en") {
1497
+ const resolved = { ...data };
1498
+ for (const field of fieldsList) {
1499
+ if (!field.localized) continue;
1500
+ const val = data[field.name];
1501
+ if (val && typeof val === "object" && !Array.isArray(val)) {
1502
+ const locMap = val;
1503
+ if (targetLocale in locMap && locMap[targetLocale] !== void 0) resolved[field.name] = locMap[targetLocale];
1504
+ else if (fallbackLocale && fallbackLocale in locMap && locMap[fallbackLocale] !== void 0) resolved[field.name] = locMap[fallbackLocale];
1505
+ else {
1506
+ const firstAvailable = Object.values(locMap)[0];
1507
+ resolved[field.name] = firstAvailable;
1508
+ }
1509
+ }
1510
+ }
1511
+ return resolved;
1512
+ }
1513
+ /**
1514
+ * Merge an incoming update for a specific locale into existing localized dictionaries.
1515
+ */
1516
+ function mergeLocalizedInput(incomingData, existingData = {}, fieldsList, locale) {
1517
+ const merged = {
1518
+ ...existingData,
1519
+ ...incomingData
1520
+ };
1521
+ for (const field of fieldsList) {
1522
+ if (!field.localized) continue;
1523
+ const incomingVal = incomingData[field.name];
1524
+ if (incomingVal === void 0) continue;
1525
+ if (incomingVal && typeof incomingVal === "object" && !Array.isArray(incomingVal) && Object.keys(incomingVal).some((k) => k.length === 2 || k.includes("-"))) {
1526
+ const existingObj = typeof existingData[field.name] === "object" && existingData[field.name] !== null ? existingData[field.name] : {};
1527
+ merged[field.name] = {
1528
+ ...existingObj,
1529
+ ...incomingVal
1530
+ };
1531
+ continue;
1532
+ }
1533
+ const existingMap = typeof existingData[field.name] === "object" && existingData[field.name] !== null ? { ...existingData[field.name] } : {};
1534
+ existingMap[locale] = incomingVal;
1535
+ merged[field.name] = existingMap;
1536
+ }
1537
+ return merged;
1538
+ }
1539
+ //#endregion
962
1540
  //#region src/storage/memory-adapter.ts
963
- function generateId(prefix = "") {
1541
+ function generateId$1(prefix = "") {
964
1542
  return `${prefix}${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 9)}`;
965
1543
  }
1544
+ function matchesCondition(actualValue, expected) {
1545
+ if (expected === null || expected === void 0 || typeof expected !== "object" || Array.isArray(expected)) return actualValue === expected;
1546
+ const exp = expected;
1547
+ if (!Object.keys(exp).some((k) => k.startsWith("$"))) return JSON.stringify(actualValue) === JSON.stringify(expected);
1548
+ for (const [op, target] of Object.entries(exp)) switch (op) {
1549
+ case "$eq":
1550
+ if (actualValue !== target) return false;
1551
+ break;
1552
+ case "$ne":
1553
+ if (actualValue === target) return false;
1554
+ break;
1555
+ case "$gt":
1556
+ if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
1557
+ if (actualValue <= target) return false;
1558
+ break;
1559
+ case "$gte":
1560
+ if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
1561
+ if (actualValue < target) return false;
1562
+ break;
1563
+ case "$lt":
1564
+ if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
1565
+ if (actualValue >= target) return false;
1566
+ break;
1567
+ case "$lte":
1568
+ if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
1569
+ if (actualValue > target) return false;
1570
+ break;
1571
+ case "$in":
1572
+ if (!Array.isArray(target) || !target.includes(actualValue)) return false;
1573
+ break;
1574
+ case "$nin":
1575
+ if (Array.isArray(target) && target.includes(actualValue)) return false;
1576
+ break;
1577
+ case "$contains":
1578
+ if (typeof actualValue !== "string" || !actualValue.toLowerCase().includes(String(target).toLowerCase())) return false;
1579
+ break;
1580
+ case "$startsWith":
1581
+ if (typeof actualValue !== "string" || !actualValue.startsWith(String(target))) return false;
1582
+ break;
1583
+ case "$endsWith":
1584
+ if (typeof actualValue !== "string" || !actualValue.endsWith(String(target))) return false;
1585
+ break;
1586
+ case "$between":
1587
+ if (!Array.isArray(target) || target.length !== 2) return false;
1588
+ if (actualValue < target[0] || actualValue > target[1]) return false;
1589
+ break;
1590
+ case "$exists":
1591
+ if (actualValue !== void 0 !== Boolean(target)) return false;
1592
+ break;
1593
+ }
1594
+ return true;
1595
+ }
966
1596
  var MemoryStorageAdapter = class {
967
1597
  content = /* @__PURE__ */ new Map();
968
1598
  revisions = /* @__PURE__ */ new Map();
@@ -973,7 +1603,7 @@ var MemoryStorageAdapter = class {
973
1603
  async init() {}
974
1604
  async close() {}
975
1605
  async createContent(item) {
976
- const id = generateId("cnt_");
1606
+ const id = generateId$1("cnt_");
977
1607
  const now = (/* @__PURE__ */ new Date()).toISOString();
978
1608
  const fullItem = {
979
1609
  ...item,
@@ -1016,7 +1646,7 @@ var MemoryStorageAdapter = class {
1016
1646
  }
1017
1647
  if (options.where) {
1018
1648
  let match = true;
1019
- for (const [key, val] of Object.entries(options.where)) if (item.data[key] !== val && item[key] !== val) {
1649
+ for (const [key, expected] of Object.entries(options.where)) if (!matchesCondition(item.data[key] !== void 0 ? item.data[key] : item[key], expected)) {
1020
1650
  match = false;
1021
1651
  break;
1022
1652
  }
@@ -1037,14 +1667,49 @@ var MemoryStorageAdapter = class {
1037
1667
  const offset = options.offset ?? 0;
1038
1668
  const limit = options.limit ?? 20;
1039
1669
  const paged = list.slice(offset, offset + limit);
1670
+ let items = JSON.parse(JSON.stringify(paged));
1671
+ if (options.populate && options.populate.length > 0) items = items.map((it) => this.populateItem(it, options.populate));
1672
+ if (options.select && options.select.length > 0) items = items.map((it) => this.projectItem(it, options.select));
1040
1673
  return {
1041
- items: JSON.parse(JSON.stringify(paged)),
1674
+ items,
1042
1675
  total,
1043
1676
  limit,
1044
1677
  offset,
1045
1678
  hasMore: offset + limit < total
1046
1679
  };
1047
1680
  }
1681
+ populateItem(item, populateFields) {
1682
+ const populated = { ...item.populated || {} };
1683
+ for (const field of populateFields) {
1684
+ const rawVal = item.data[field];
1685
+ if (!rawVal) continue;
1686
+ if (Array.isArray(rawVal)) populated[field] = rawVal.map((id) => {
1687
+ if (typeof id === "string") return this.content.get(id) || this.media.get(id) || id;
1688
+ return id;
1689
+ });
1690
+ else if (typeof rawVal === "string") {
1691
+ const target = this.content.get(rawVal) || this.media.get(rawVal) || null;
1692
+ if (target) populated[field] = target;
1693
+ }
1694
+ }
1695
+ return {
1696
+ ...item,
1697
+ populated
1698
+ };
1699
+ }
1700
+ projectItem(item, selectFields) {
1701
+ const projected = {
1702
+ id: item.id,
1703
+ collection: item.collection
1704
+ };
1705
+ for (const key of selectFields) if (key in item) projected[key] = item[key];
1706
+ else if (key in item.data) {
1707
+ if (!projected.data) projected.data = {};
1708
+ projected.data[key] = item.data[key];
1709
+ }
1710
+ if (item.populated) projected.populated = item.populated;
1711
+ return projected;
1712
+ }
1048
1713
  async updateContent(collection, id, updates) {
1049
1714
  const existing = this.content.get(id);
1050
1715
  if (!existing || existing.collection !== collection) return null;
@@ -1076,7 +1741,7 @@ var MemoryStorageAdapter = class {
1076
1741
  })).total;
1077
1742
  }
1078
1743
  async createRevision(contentId, collection, version, snapshot, authorId, note) {
1079
- const id = generateId("rev_");
1744
+ const id = generateId$1("rev_");
1080
1745
  const record = {
1081
1746
  id,
1082
1747
  contentId,
@@ -1109,7 +1774,7 @@ var MemoryStorageAdapter = class {
1109
1774
  return count;
1110
1775
  }
1111
1776
  async createTerm(term) {
1112
- const id = generateId("trm_");
1777
+ const id = generateId$1("trm_");
1113
1778
  const now = (/* @__PURE__ */ new Date()).toISOString();
1114
1779
  const item = {
1115
1780
  ...term,
@@ -1187,7 +1852,7 @@ var MemoryStorageAdapter = class {
1187
1852
  for (const [id, term] of this.terms.entries()) term.count = counts.get(id) || 0;
1188
1853
  }
1189
1854
  async createMedia(item) {
1190
- const id = generateId("med_");
1855
+ const id = generateId$1("med_");
1191
1856
  const now = (/* @__PURE__ */ new Date()).toISOString();
1192
1857
  const mediaItem = {
1193
1858
  ...item,
@@ -1268,6 +1933,9 @@ var CMSEngine = class {
1268
1933
  taxonomies;
1269
1934
  media;
1270
1935
  rbac;
1936
+ auth;
1937
+ webhooks;
1938
+ preview;
1271
1939
  revisions;
1272
1940
  lifecycle;
1273
1941
  collections = /* @__PURE__ */ new Map();
@@ -1280,8 +1948,11 @@ var CMSEngine = class {
1280
1948
  this.storage = storage ?? new MemoryStorageAdapter();
1281
1949
  this.options = new OptionsManager(this.storage, this.hooks);
1282
1950
  this.taxonomies = new TaxonomyManager(this.storage, this.hooks);
1283
- this.media = new MediaManager(this.storage, this.hooks);
1284
1951
  this.rbac = new RBACManager();
1952
+ this.auth = new AuthService(this.config.auth, this.rbac);
1953
+ this.webhooks = new WebhookManager(this.config.webhooks || [], this.hooks);
1954
+ this.preview = new PreviewManager(this.config.auth?.jwtSecret || "azlib_preview_secret_token_default");
1955
+ this.media = new MediaManager(this.storage, this.hooks, this.config.media?.publicBaseUrl || "/uploads", this.config.media?.storageDriver);
1285
1956
  this.revisions = new RevisionManager(this.storage, this.hooks);
1286
1957
  this.lifecycle = new ContentLifecycle(this.storage, this.hooks);
1287
1958
  for (const coll of this.config.collections) this.collections.set(coll.slug, coll);
@@ -1422,11 +2093,12 @@ var CMSEngine = class {
1422
2093
  const filteredInput = await self.hooks.applyFilters("cms.before_create_input", input, { collection: slug });
1423
2094
  const inputData = { ...filteredInput.data || {} };
1424
2095
  if (filteredInput.title !== void 0 && inputData.title === void 0) inputData.title = filteredInput.title;
2096
+ if (filteredInput.name !== void 0 && inputData.name === void 0) inputData.name = filteredInput.name;
1425
2097
  if (filteredInput.slug !== void 0 && inputData.slug === void 0) inputData.slug = filteredInput.slug;
1426
2098
  if (filteredInput.status !== void 0 && inputData.status === void 0) inputData.status = filteredInput.status;
1427
2099
  const { data: normalizedData, errors } = validateAndNormalizeData(collConfig.fields, inputData);
1428
2100
  if (Object.keys(errors).length > 0) throw new Error(`[CMSEngine] Validation failed for collection '${slug}': ${JSON.stringify(errors)}`);
1429
- const title = filteredInput.title ?? normalizedData.title ?? "";
2101
+ const title = filteredInput.title ?? filteredInput.name ?? normalizedData.title ?? normalizedData.name ?? "";
1430
2102
  const finalSlug = await resolveUniqueSlug(filteredInput.slug ? slugify(filteredInput.slug) : slugify(title) || "item", async (s) => {
1431
2103
  return await self.storage.getContentBySlug(slug, s) !== null;
1432
2104
  });
@@ -1451,17 +2123,48 @@ var CMSEngine = class {
1451
2123
  if (collConfig.revisions) await self.revisions.createRevision(item, authorId, "Initial creation");
1452
2124
  await self.hooks.doAction("cms.content_created", item);
1453
2125
  await self.hooks.doAction(`cms.${slug}_created`, item);
2126
+ self.webhooks.dispatch("content.created", item);
2127
+ if (item.status === "published") self.webhooks.dispatch("content.published", item);
1454
2128
  return self.hooks.applyFilters("cms.after_create_item", item, { collection: slug });
1455
2129
  },
1456
- async findById(id) {
1457
- return self.storage.getContent(slug, id);
2130
+ async findById(id, options) {
2131
+ if (options?.populate || options?.select) {
2132
+ const it = (await self.storage.findContent(slug, {
2133
+ where: { id },
2134
+ limit: 1,
2135
+ populate: options.populate,
2136
+ select: options.select
2137
+ })).items[0] || null;
2138
+ if (it && options.locale) it.data = resolveLocalizedData(it.data, collConfig.fields, options.locale, self.config.i18n?.defaultLocale);
2139
+ return it;
2140
+ }
2141
+ const item = await self.storage.getContent(slug, id);
2142
+ if (item && options?.locale) item.data = resolveLocalizedData(item.data, collConfig.fields, options.locale, self.config.i18n?.defaultLocale);
2143
+ return item;
1458
2144
  },
1459
- async findBySlug(contentSlug) {
1460
- return self.storage.getContentBySlug(slug, contentSlug);
2145
+ async findBySlug(contentSlug, options) {
2146
+ if (options?.populate || options?.select) {
2147
+ const it = (await self.storage.findContent(slug, {
2148
+ where: { slug: contentSlug },
2149
+ limit: 1,
2150
+ populate: options.populate,
2151
+ select: options.select
2152
+ })).items[0] || null;
2153
+ if (it && options.locale) it.data = resolveLocalizedData(it.data, collConfig.fields, options.locale, self.config.i18n?.defaultLocale);
2154
+ return it;
2155
+ }
2156
+ const item = await self.storage.getContentBySlug(slug, contentSlug);
2157
+ if (item && options?.locale) item.data = resolveLocalizedData(item.data, collConfig.fields, options.locale, self.config.i18n?.defaultLocale);
2158
+ return item;
1461
2159
  },
1462
2160
  async find(options = {}) {
1463
2161
  const filteredOptions = await self.hooks.applyFilters("cms.find_options", options, { collection: slug });
1464
- return self.storage.findContent(slug, filteredOptions);
2162
+ const res = await self.storage.findContent(slug, filteredOptions);
2163
+ if (options.locale) res.items = res.items.map((it) => ({
2164
+ ...it,
2165
+ data: resolveLocalizedData(it.data, collConfig.fields, options.locale, self.config.i18n?.defaultLocale)
2166
+ }));
2167
+ return res;
1465
2168
  },
1466
2169
  async update(id, input, authorId = null, revisionNote) {
1467
2170
  const existing = await self.storage.getContent(slug, id);
@@ -1511,6 +2214,8 @@ var CMSEngine = class {
1511
2214
  if (collConfig.revisions) await self.revisions.createRevision(updated, authorId, revisionNote ?? `Updated version ${updated.version}`);
1512
2215
  await self.hooks.doAction("cms.content_updated", updated);
1513
2216
  await self.hooks.doAction(`cms.${slug}_updated`, updated);
2217
+ self.webhooks.dispatch("content.updated", updated);
2218
+ if (updated.status === "published" && existing.status !== "published") self.webhooks.dispatch("content.published", updated);
1514
2219
  return self.hooks.applyFilters("cms.after_update_item", updated, { collection: slug });
1515
2220
  }
1516
2221
  return updated;
@@ -1522,6 +2227,7 @@ var CMSEngine = class {
1522
2227
  if (deleted) {
1523
2228
  await self.hooks.doAction("cms.content_deleted", item);
1524
2229
  await self.hooks.doAction(`cms.${slug}_deleted`, item);
2230
+ self.webhooks.dispatch("content.deleted", item);
1525
2231
  }
1526
2232
  return deleted;
1527
2233
  },
@@ -1576,6 +2282,700 @@ function definePlugin(factory) {
1576
2282
  return factory;
1577
2283
  }
1578
2284
  //#endregion
2285
+ //#region src/media/drivers/disk-driver.ts
2286
+ /**
2287
+ * @azlib/cms - Local Disk Media Storage Driver
2288
+ */
2289
+ var DiskMediaStorageDriver = class {
2290
+ uploadDir;
2291
+ publicBaseUrl;
2292
+ constructor(options) {
2293
+ this.uploadDir = options.uploadDir;
2294
+ this.publicBaseUrl = options.publicBaseUrl?.replace(/\/+$/, "") || "/uploads";
2295
+ }
2296
+ async write(input) {
2297
+ await node_fs.promises.mkdir(this.uploadDir, { recursive: true });
2298
+ const safeName = `${Date.now()}_${input.filename.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
2299
+ const filePath = node_path.join(this.uploadDir, safeName);
2300
+ await node_fs.promises.writeFile(filePath, input.buffer);
2301
+ return {
2302
+ url: `${this.publicBaseUrl}/${safeName}`,
2303
+ path: filePath,
2304
+ sizeBytes: input.buffer.byteLength
2305
+ };
2306
+ }
2307
+ async read(pathOrUrl) {
2308
+ try {
2309
+ const filename = node_path.basename(pathOrUrl);
2310
+ const filePath = node_path.join(this.uploadDir, filename);
2311
+ const data = await node_fs.promises.readFile(filePath);
2312
+ return new Uint8Array(data);
2313
+ } catch {
2314
+ return null;
2315
+ }
2316
+ }
2317
+ async delete(pathOrUrl) {
2318
+ try {
2319
+ const filename = node_path.basename(pathOrUrl);
2320
+ const filePath = node_path.join(this.uploadDir, filename);
2321
+ await node_fs.promises.unlink(filePath);
2322
+ return true;
2323
+ } catch {
2324
+ return false;
2325
+ }
2326
+ }
2327
+ getUrl(filePathOrName) {
2328
+ const filename = node_path.basename(filePathOrName);
2329
+ return `${this.publicBaseUrl}/${filename}`;
2330
+ }
2331
+ };
2332
+ //#endregion
2333
+ //#region src/storage/persistence-adapter.ts
2334
+ /**
2335
+ * @azlib/cms - Persistence Storage Adapter (@azlib/persistence)
2336
+ */
2337
+ function generateId(prefix = "") {
2338
+ return `${prefix}${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 9)}`;
2339
+ }
2340
+ const cmsContentTable = (0, _azlib_persistence.createTable)("cms_content", {
2341
+ id: _azlib_persistence.column.varchar("id", { length: 64 }).notNull().primaryKey(),
2342
+ collection: _azlib_persistence.column.varchar("collection", { length: 64 }).notNull(),
2343
+ slug: _azlib_persistence.column.varchar("slug", { length: 255 }).notNull(),
2344
+ status: _azlib_persistence.column.varchar("status", { length: 32 }).notNull(),
2345
+ title: _azlib_persistence.column.text("title"),
2346
+ parentId: _azlib_persistence.column.varchar("parent_id", { length: 64 }),
2347
+ authorId: _azlib_persistence.column.varchar("author_id", { length: 64 }),
2348
+ publishedAt: _azlib_persistence.column.varchar("published_at", { length: 32 }),
2349
+ scheduledAt: _azlib_persistence.column.varchar("scheduled_at", { length: 32 }),
2350
+ createdAt: _azlib_persistence.column.varchar("created_at", { length: 32 }).notNull(),
2351
+ updatedAt: _azlib_persistence.column.varchar("updated_at", { length: 32 }).notNull(),
2352
+ version: _azlib_persistence.column.integer("version").notNull().default(1),
2353
+ locale: _azlib_persistence.column.varchar("locale", { length: 16 }),
2354
+ data: _azlib_persistence.column.text("data").notNull(),
2355
+ terms: _azlib_persistence.column.text("terms")
2356
+ });
2357
+ const cmsRevisionsTable = (0, _azlib_persistence.createTable)("cms_revisions", {
2358
+ id: _azlib_persistence.column.varchar("id", { length: 64 }).notNull().primaryKey(),
2359
+ contentId: _azlib_persistence.column.varchar("content_id", { length: 64 }).notNull(),
2360
+ collection: _azlib_persistence.column.varchar("collection", { length: 64 }).notNull(),
2361
+ version: _azlib_persistence.column.integer("version").notNull(),
2362
+ snapshot: _azlib_persistence.column.text("snapshot").notNull(),
2363
+ authorId: _azlib_persistence.column.varchar("author_id", { length: 64 }),
2364
+ note: _azlib_persistence.column.text("note"),
2365
+ createdAt: _azlib_persistence.column.varchar("created_at", { length: 32 }).notNull()
2366
+ });
2367
+ const cmsTermsTable = (0, _azlib_persistence.createTable)("cms_terms", {
2368
+ id: _azlib_persistence.column.varchar("id", { length: 64 }).notNull().primaryKey(),
2369
+ taxonomy: _azlib_persistence.column.varchar("taxonomy", { length: 64 }).notNull(),
2370
+ name: _azlib_persistence.column.varchar("name", { length: 255 }).notNull(),
2371
+ slug: _azlib_persistence.column.varchar("slug", { length: 255 }).notNull(),
2372
+ description: _azlib_persistence.column.text("description"),
2373
+ parentId: _azlib_persistence.column.varchar("parent_id", { length: 64 }),
2374
+ count: _azlib_persistence.column.integer("count").notNull().default(0),
2375
+ meta: _azlib_persistence.column.text("meta"),
2376
+ createdAt: _azlib_persistence.column.varchar("created_at", { length: 32 }).notNull(),
2377
+ updatedAt: _azlib_persistence.column.varchar("updated_at", { length: 32 }).notNull()
2378
+ });
2379
+ const cmsContentTermsTable = (0, _azlib_persistence.createTable)("cms_content_terms", {
2380
+ contentId: _azlib_persistence.column.varchar("content_id", { length: 64 }).notNull(),
2381
+ termId: _azlib_persistence.column.varchar("term_id", { length: 64 }).notNull()
2382
+ });
2383
+ const cmsMediaTable = (0, _azlib_persistence.createTable)("cms_media", {
2384
+ id: _azlib_persistence.column.varchar("id", { length: 64 }).notNull().primaryKey(),
2385
+ filename: _azlib_persistence.column.varchar("filename", { length: 255 }).notNull(),
2386
+ originalName: _azlib_persistence.column.varchar("original_name", { length: 255 }).notNull(),
2387
+ mimeType: _azlib_persistence.column.varchar("mime_type", { length: 128 }).notNull(),
2388
+ sizeBytes: _azlib_persistence.column.integer("size_bytes").notNull(),
2389
+ url: _azlib_persistence.column.text("url").notNull(),
2390
+ path: _azlib_persistence.column.text("path"),
2391
+ width: _azlib_persistence.column.integer("width"),
2392
+ height: _azlib_persistence.column.integer("height"),
2393
+ altText: _azlib_persistence.column.text("alt_text"),
2394
+ caption: _azlib_persistence.column.text("caption"),
2395
+ authorId: _azlib_persistence.column.varchar("author_id", { length: 64 }),
2396
+ variants: _azlib_persistence.column.text("variants"),
2397
+ createdAt: _azlib_persistence.column.varchar("created_at", { length: 32 }).notNull(),
2398
+ updatedAt: _azlib_persistence.column.varchar("updated_at", { length: 32 }).notNull()
2399
+ });
2400
+ const cmsOptionsTable = (0, _azlib_persistence.createTable)("cms_options", {
2401
+ key: _azlib_persistence.column.varchar("key", { length: 128 }).notNull().primaryKey(),
2402
+ value: _azlib_persistence.column.text("value").notNull(),
2403
+ autoload: _azlib_persistence.column.boolean("autoload").notNull().default(false),
2404
+ namespace: _azlib_persistence.column.varchar("namespace", { length: 64 }),
2405
+ updatedAt: _azlib_persistence.column.varchar("updated_at", { length: 32 }).notNull()
2406
+ });
2407
+ var PersistenceStorageAdapter = class {
2408
+ client;
2409
+ config;
2410
+ autoMigrate;
2411
+ constructor(options) {
2412
+ this.client = options.client;
2413
+ this.config = options.config;
2414
+ this.autoMigrate = options.autoMigrate ?? true;
2415
+ }
2416
+ async init() {
2417
+ if (!this.autoMigrate) return;
2418
+ const tables = [
2419
+ cmsContentTable,
2420
+ cmsRevisionsTable,
2421
+ cmsTermsTable,
2422
+ cmsContentTermsTable,
2423
+ cmsMediaTable,
2424
+ cmsOptionsTable
2425
+ ];
2426
+ for (const table of tables) {
2427
+ const ddl = (0, _azlib_persistence.generateCreateTableDdl)(table, this.config.dialect);
2428
+ try {
2429
+ await this.client.raw(ddl);
2430
+ } catch (err) {}
2431
+ }
2432
+ }
2433
+ async close() {}
2434
+ async createContent(item) {
2435
+ const id = generateId("cnt_");
2436
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2437
+ const version = 1;
2438
+ const record = {
2439
+ id,
2440
+ collection: item.collection,
2441
+ slug: item.slug,
2442
+ status: item.status,
2443
+ title: item.title ?? null,
2444
+ parentId: item.parentId ?? null,
2445
+ authorId: item.authorId ?? null,
2446
+ publishedAt: item.publishedAt ?? null,
2447
+ scheduledAt: item.scheduledAt ?? null,
2448
+ createdAt: now,
2449
+ updatedAt: now,
2450
+ version,
2451
+ locale: item.locale ?? null,
2452
+ data: JSON.stringify(item.data || {}),
2453
+ terms: item.terms ? JSON.stringify(item.terms) : null
2454
+ };
2455
+ await this.client.insert(cmsContentTable).values(record).execute();
2456
+ return {
2457
+ id,
2458
+ collection: item.collection,
2459
+ slug: item.slug,
2460
+ status: item.status,
2461
+ title: item.title,
2462
+ parentId: item.parentId ?? null,
2463
+ authorId: item.authorId ?? null,
2464
+ publishedAt: item.publishedAt ?? null,
2465
+ scheduledAt: item.scheduledAt ?? null,
2466
+ createdAt: now,
2467
+ updatedAt: now,
2468
+ version,
2469
+ locale: item.locale,
2470
+ data: item.data,
2471
+ terms: item.terms
2472
+ };
2473
+ }
2474
+ async getContent(collection, id) {
2475
+ const rows = await this.client.select().from(cmsContentTable).where((0, _azlib_persistence.and)((0, _azlib_persistence.eq)(cmsContentTable.id, id), (0, _azlib_persistence.eq)(cmsContentTable.collection, collection))).execute();
2476
+ if (!rows || rows.length === 0) return null;
2477
+ return this.deserializeContent(rows[0]);
2478
+ }
2479
+ async getContentBySlug(collection, slug) {
2480
+ const rows = await this.client.select().from(cmsContentTable).where((0, _azlib_persistence.and)((0, _azlib_persistence.eq)(cmsContentTable.slug, slug), (0, _azlib_persistence.eq)(cmsContentTable.collection, collection))).execute();
2481
+ if (!rows || rows.length === 0) return null;
2482
+ return this.deserializeContent(rows[0]);
2483
+ }
2484
+ async findContent(collection, options = {}) {
2485
+ const conditions = [(0, _azlib_persistence.eq)(cmsContentTable.collection, collection)];
2486
+ if (options.status) if (Array.isArray(options.status)) conditions.push((0, _azlib_persistence.inArray)(cmsContentTable.status, options.status));
2487
+ else conditions.push((0, _azlib_persistence.eq)(cmsContentTable.status, options.status));
2488
+ if (options.authorId) conditions.push((0, _azlib_persistence.eq)(cmsContentTable.authorId, options.authorId));
2489
+ if (options.parentId !== void 0) conditions.push((0, _azlib_persistence.eq)(cmsContentTable.parentId, options.parentId));
2490
+ if (options.locale) conditions.push((0, _azlib_persistence.eq)(cmsContentTable.locale, options.locale));
2491
+ if (options.search) {
2492
+ const q = `%${options.search}%`;
2493
+ conditions.push((0, _azlib_persistence.or)((0, _azlib_persistence.like)(cmsContentTable.title, q), (0, _azlib_persistence.like)(cmsContentTable.slug, q), (0, _azlib_persistence.like)(cmsContentTable.data, q)));
2494
+ }
2495
+ if (options.termIds && options.termIds.length > 0) {
2496
+ const termRows = await this.client.select().from(cmsContentTermsTable).where((0, _azlib_persistence.inArray)(cmsContentTermsTable.termId, options.termIds)).execute();
2497
+ const matchedContentIds = Array.from(new Set(termRows.map((r) => r.contentId || r.content_id)));
2498
+ if (matchedContentIds.length === 0) return {
2499
+ items: [],
2500
+ total: 0,
2501
+ limit: options.limit ?? 20,
2502
+ offset: options.offset ?? 0,
2503
+ hasMore: false
2504
+ };
2505
+ conditions.push((0, _azlib_persistence.inArray)(cmsContentTable.id, matchedContentIds));
2506
+ }
2507
+ const whereClause = conditions.length === 1 ? conditions[0] : (0, _azlib_persistence.and)(...conditions);
2508
+ let list = (await this.client.select().from(cmsContentTable).where(whereClause).execute()).map((r) => this.deserializeContent(r));
2509
+ if (options.where) list = list.filter((item) => {
2510
+ for (const [key, expected] of Object.entries(options.where)) {
2511
+ const actual = item.data[key] !== void 0 ? item.data[key] : item[key];
2512
+ if (!this.matchesCondition(actual, expected)) return false;
2513
+ }
2514
+ return true;
2515
+ });
2516
+ const total = list.length;
2517
+ const orderBy = options.orderBy || "createdAt";
2518
+ const dir = options.orderDirection === "asc" ? 1 : -1;
2519
+ list.sort((a, b) => {
2520
+ const valA = a[orderBy] ?? a.data[orderBy] ?? "";
2521
+ const valB = b[orderBy] ?? b.data[orderBy] ?? "";
2522
+ if (valA < valB) return -1 * dir;
2523
+ if (valA > valB) return 1 * dir;
2524
+ return 0;
2525
+ });
2526
+ const offset = options.offset ?? 0;
2527
+ const limit = options.limit ?? 20;
2528
+ let items = list.slice(offset, offset + limit);
2529
+ if (options.populate && options.populate.length > 0) items = await Promise.all(items.map((it) => this.populateItem(it, options.populate)));
2530
+ return {
2531
+ items,
2532
+ total,
2533
+ limit,
2534
+ offset,
2535
+ hasMore: offset + limit < total
2536
+ };
2537
+ }
2538
+ async updateContent(collection, id, updates) {
2539
+ const existing = await this.getContent(collection, id);
2540
+ if (!existing) return null;
2541
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2542
+ const nextVersion = existing.version + 1;
2543
+ const mergedData = {
2544
+ ...existing.data,
2545
+ ...updates.data || {}
2546
+ };
2547
+ const updateRecord = {
2548
+ updatedAt: now,
2549
+ version: nextVersion,
2550
+ data: JSON.stringify(mergedData)
2551
+ };
2552
+ if (updates.title !== void 0) updateRecord.title = updates.title;
2553
+ if (updates.slug !== void 0) updateRecord.slug = updates.slug;
2554
+ if (updates.status !== void 0) updateRecord.status = updates.status;
2555
+ if (updates.parentId !== void 0) updateRecord.parentId = updates.parentId;
2556
+ if (updates.authorId !== void 0) updateRecord.authorId = updates.authorId;
2557
+ if (updates.publishedAt !== void 0) updateRecord.publishedAt = updates.publishedAt;
2558
+ if (updates.scheduledAt !== void 0) updateRecord.scheduledAt = updates.scheduledAt;
2559
+ if (updates.locale !== void 0) updateRecord.locale = updates.locale;
2560
+ if (updates.terms !== void 0) updateRecord.terms = JSON.stringify(updates.terms);
2561
+ await this.client.update(cmsContentTable).set(updateRecord).where((0, _azlib_persistence.and)((0, _azlib_persistence.eq)(cmsContentTable.id, id), (0, _azlib_persistence.eq)(cmsContentTable.collection, collection))).execute();
2562
+ return this.getContent(collection, id);
2563
+ }
2564
+ async deleteContent(collection, id) {
2565
+ if (!await this.getContent(collection, id)) return false;
2566
+ await this.client.delete(cmsContentTable).where((0, _azlib_persistence.and)((0, _azlib_persistence.eq)(cmsContentTable.id, id), (0, _azlib_persistence.eq)(cmsContentTable.collection, collection))).execute();
2567
+ await this.client.delete(cmsContentTermsTable).where((0, _azlib_persistence.eq)(cmsContentTermsTable.contentId, id)).execute();
2568
+ await this.deleteRevisionsByContentId(id);
2569
+ return true;
2570
+ }
2571
+ async countContent(collection, options = {}) {
2572
+ return (await this.findContent(collection, {
2573
+ ...options,
2574
+ limit: 1e6
2575
+ })).total;
2576
+ }
2577
+ async createRevision(contentId, collection, version, snapshot, authorId, note) {
2578
+ const id = generateId("rev_");
2579
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2580
+ const record = {
2581
+ id,
2582
+ contentId,
2583
+ collection,
2584
+ version,
2585
+ snapshot: JSON.stringify(snapshot),
2586
+ authorId: authorId ?? null,
2587
+ note: note ?? null,
2588
+ createdAt: now
2589
+ };
2590
+ await this.client.insert(cmsRevisionsTable).values(record).execute();
2591
+ return {
2592
+ id,
2593
+ contentId,
2594
+ collection,
2595
+ version,
2596
+ snapshot,
2597
+ authorId,
2598
+ note,
2599
+ createdAt: now
2600
+ };
2601
+ }
2602
+ async getRevisions(contentId) {
2603
+ return (await this.client.select().from(cmsRevisionsTable).where((0, _azlib_persistence.eq)(cmsRevisionsTable.contentId, contentId)).execute()).map((r) => ({
2604
+ id: r.id,
2605
+ contentId: r.contentId || r.content_id,
2606
+ collection: r.collection,
2607
+ version: Number(r.version),
2608
+ snapshot: typeof r.snapshot === "string" ? JSON.parse(r.snapshot) : r.snapshot,
2609
+ authorId: r.authorId || r.author_id || null,
2610
+ note: r.note || void 0,
2611
+ createdAt: r.createdAt || r.created_at
2612
+ }));
2613
+ }
2614
+ async getRevision(revisionId) {
2615
+ const rows = await this.client.select().from(cmsRevisionsTable).where((0, _azlib_persistence.eq)(cmsRevisionsTable.id, revisionId)).execute();
2616
+ if (!rows || rows.length === 0) return null;
2617
+ const r = rows[0];
2618
+ return {
2619
+ id: r.id,
2620
+ contentId: r.contentId || r.content_id,
2621
+ collection: r.collection,
2622
+ version: Number(r.version),
2623
+ snapshot: typeof r.snapshot === "string" ? JSON.parse(r.snapshot) : r.snapshot,
2624
+ authorId: r.authorId || r.author_id || null,
2625
+ note: r.note || void 0,
2626
+ createdAt: r.createdAt || r.created_at
2627
+ };
2628
+ }
2629
+ async deleteRevisionsByContentId(contentId) {
2630
+ const rows = await this.getRevisions(contentId);
2631
+ await this.client.delete(cmsRevisionsTable).where((0, _azlib_persistence.eq)(cmsRevisionsTable.contentId, contentId)).execute();
2632
+ return rows.length;
2633
+ }
2634
+ async createTerm(term) {
2635
+ const id = generateId("trm_");
2636
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2637
+ const record = {
2638
+ id,
2639
+ taxonomy: term.taxonomy,
2640
+ name: term.name,
2641
+ slug: term.slug,
2642
+ description: term.description ?? null,
2643
+ parentId: term.parentId ?? null,
2644
+ count: 0,
2645
+ meta: term.meta ? JSON.stringify(term.meta) : null,
2646
+ createdAt: now,
2647
+ updatedAt: now
2648
+ };
2649
+ await this.client.insert(cmsTermsTable).values(record).execute();
2650
+ return {
2651
+ id,
2652
+ taxonomy: term.taxonomy,
2653
+ name: term.name,
2654
+ slug: term.slug,
2655
+ description: term.description,
2656
+ parentId: term.parentId ?? null,
2657
+ count: 0,
2658
+ meta: term.meta,
2659
+ createdAt: now,
2660
+ updatedAt: now
2661
+ };
2662
+ }
2663
+ async getTermById(id) {
2664
+ const rows = await this.client.select().from(cmsTermsTable).where((0, _azlib_persistence.eq)(cmsTermsTable.id, id)).execute();
2665
+ if (!rows || rows.length === 0) return null;
2666
+ return this.deserializeTerm(rows[0]);
2667
+ }
2668
+ async getTermBySlug(taxonomy, slug) {
2669
+ const rows = await this.client.select().from(cmsTermsTable).where((0, _azlib_persistence.and)((0, _azlib_persistence.eq)(cmsTermsTable.taxonomy, taxonomy), (0, _azlib_persistence.eq)(cmsTermsTable.slug, slug))).execute();
2670
+ if (!rows || rows.length === 0) return null;
2671
+ return this.deserializeTerm(rows[0]);
2672
+ }
2673
+ async getTerms(taxonomy, options) {
2674
+ const conditions = [(0, _azlib_persistence.eq)(cmsTermsTable.taxonomy, taxonomy)];
2675
+ if (options?.parentId !== void 0) conditions.push((0, _azlib_persistence.eq)(cmsTermsTable.parentId, options.parentId));
2676
+ return (await this.client.select().from(cmsTermsTable).where(conditions.length === 1 ? conditions[0] : (0, _azlib_persistence.and)(...conditions)).execute()).map((r) => this.deserializeTerm(r));
2677
+ }
2678
+ async updateTerm(id, updates) {
2679
+ if (!await this.getTermById(id)) return null;
2680
+ const updateRecord = { updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
2681
+ if (updates.name !== void 0) updateRecord.name = updates.name;
2682
+ if (updates.slug !== void 0) updateRecord.slug = updates.slug;
2683
+ if (updates.description !== void 0) updateRecord.description = updates.description;
2684
+ if (updates.parentId !== void 0) updateRecord.parentId = updates.parentId;
2685
+ if (updates.count !== void 0) updateRecord.count = updates.count;
2686
+ if (updates.meta !== void 0) updateRecord.meta = JSON.stringify(updates.meta);
2687
+ await this.client.update(cmsTermsTable).set(updateRecord).where((0, _azlib_persistence.eq)(cmsTermsTable.id, id)).execute();
2688
+ return this.getTermById(id);
2689
+ }
2690
+ async deleteTerm(id) {
2691
+ if (!await this.getTermById(id)) return false;
2692
+ await this.client.delete(cmsTermsTable).where((0, _azlib_persistence.eq)(cmsTermsTable.id, id)).execute();
2693
+ await this.client.delete(cmsContentTermsTable).where((0, _azlib_persistence.eq)(cmsContentTermsTable.termId, id)).execute();
2694
+ return true;
2695
+ }
2696
+ async assignTermsToContent(contentId, termIds) {
2697
+ for (const termId of termIds) try {
2698
+ await this.client.insert(cmsContentTermsTable).values({
2699
+ contentId,
2700
+ termId
2701
+ }).execute();
2702
+ const term = await this.getTermById(termId);
2703
+ if (term) await this.updateTerm(termId, { count: term.count + 1 });
2704
+ } catch {}
2705
+ }
2706
+ async getContentTerms(contentId, taxonomy) {
2707
+ const termIds = (await this.client.select().from(cmsContentTermsTable).where((0, _azlib_persistence.eq)(cmsContentTermsTable.contentId, contentId)).execute()).map((r) => r.termId || r.term_id);
2708
+ if (termIds.length === 0) return [];
2709
+ const terms = [];
2710
+ for (const tId of termIds) {
2711
+ const t = await this.getTermById(tId);
2712
+ if (t && (!taxonomy || t.taxonomy === taxonomy)) terms.push(t);
2713
+ }
2714
+ return terms;
2715
+ }
2716
+ async removeTermsFromContent(contentId, termIds) {
2717
+ for (const termId of termIds) {
2718
+ await this.client.delete(cmsContentTermsTable).where((0, _azlib_persistence.and)((0, _azlib_persistence.eq)(cmsContentTermsTable.contentId, contentId), (0, _azlib_persistence.eq)(cmsContentTermsTable.termId, termId))).execute();
2719
+ const term = await this.getTermById(termId);
2720
+ if (term && term.count > 0) await this.updateTerm(termId, { count: term.count - 1 });
2721
+ }
2722
+ }
2723
+ async createMedia(item) {
2724
+ const id = generateId("med_");
2725
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2726
+ const record = {
2727
+ id,
2728
+ filename: item.filename,
2729
+ originalName: item.originalName,
2730
+ mimeType: item.mimeType,
2731
+ sizeBytes: item.sizeBytes,
2732
+ url: item.url,
2733
+ path: item.path ?? null,
2734
+ width: item.width ?? null,
2735
+ height: item.height ?? null,
2736
+ altText: item.altText ?? null,
2737
+ caption: item.caption ?? null,
2738
+ authorId: item.authorId ?? null,
2739
+ variants: item.variants ? JSON.stringify(item.variants) : null,
2740
+ createdAt: now,
2741
+ updatedAt: now
2742
+ };
2743
+ await this.client.insert(cmsMediaTable).values(record).execute();
2744
+ return {
2745
+ id,
2746
+ filename: item.filename,
2747
+ originalName: item.originalName,
2748
+ mimeType: item.mimeType,
2749
+ sizeBytes: item.sizeBytes,
2750
+ url: item.url,
2751
+ path: item.path,
2752
+ width: item.width,
2753
+ height: item.height,
2754
+ altText: item.altText,
2755
+ caption: item.caption,
2756
+ authorId: item.authorId,
2757
+ variants: item.variants,
2758
+ createdAt: now,
2759
+ updatedAt: now
2760
+ };
2761
+ }
2762
+ async getMedia(id) {
2763
+ const rows = await this.client.select().from(cmsMediaTable).where((0, _azlib_persistence.eq)(cmsMediaTable.id, id)).execute();
2764
+ if (!rows || rows.length === 0) return null;
2765
+ return this.deserializeMedia(rows[0]);
2766
+ }
2767
+ async findMedia(options) {
2768
+ const conditions = [];
2769
+ if (options?.mimeType) conditions.push((0, _azlib_persistence.like)(cmsMediaTable.mimeType, `${options.mimeType}%`));
2770
+ if (options?.authorId) conditions.push((0, _azlib_persistence.eq)(cmsMediaTable.authorId, options.authorId));
2771
+ if (options?.search) conditions.push((0, _azlib_persistence.or)((0, _azlib_persistence.like)(cmsMediaTable.filename, `%${options.search}%`), (0, _azlib_persistence.like)(cmsMediaTable.originalName, `%${options.search}%`)));
2772
+ const items = (await this.client.select().from(cmsMediaTable).where(conditions.length > 0 ? conditions.length === 1 ? conditions[0] : (0, _azlib_persistence.and)(...conditions) : void 0).execute()).map((r) => this.deserializeMedia(r));
2773
+ const total = items.length;
2774
+ const offset = options?.offset ?? 0;
2775
+ const limit = options?.limit ?? 20;
2776
+ return {
2777
+ items: items.slice(offset, offset + limit),
2778
+ total,
2779
+ limit,
2780
+ offset,
2781
+ hasMore: offset + limit < total
2782
+ };
2783
+ }
2784
+ async updateMedia(id, updates) {
2785
+ if (!await this.getMedia(id)) return null;
2786
+ const updateRecord = { updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
2787
+ if (updates.filename !== void 0) updateRecord.filename = updates.filename;
2788
+ if (updates.altText !== void 0) updateRecord.altText = updates.altText;
2789
+ if (updates.caption !== void 0) updateRecord.caption = updates.caption;
2790
+ if (updates.width !== void 0) updateRecord.width = updates.width;
2791
+ if (updates.height !== void 0) updateRecord.height = updates.height;
2792
+ if (updates.variants !== void 0) updateRecord.variants = JSON.stringify(updates.variants);
2793
+ await this.client.update(cmsMediaTable).set(updateRecord).where((0, _azlib_persistence.eq)(cmsMediaTable.id, id)).execute();
2794
+ return this.getMedia(id);
2795
+ }
2796
+ async deleteMedia(id) {
2797
+ if (!await this.getMedia(id)) return false;
2798
+ await this.client.delete(cmsMediaTable).where((0, _azlib_persistence.eq)(cmsMediaTable.id, id)).execute();
2799
+ return true;
2800
+ }
2801
+ async getOption(key) {
2802
+ const rows = await this.client.select().from(cmsOptionsTable).where((0, _azlib_persistence.eq)(cmsOptionsTable.key, key)).execute();
2803
+ if (!rows || rows.length === 0) return null;
2804
+ const r = rows[0];
2805
+ return {
2806
+ key: r.key,
2807
+ value: typeof r.value === "string" ? JSON.parse(r.value) : r.value,
2808
+ autoload: Boolean(r.autoload),
2809
+ namespace: r.namespace || void 0,
2810
+ updatedAt: r.updatedAt || r.updated_at
2811
+ };
2812
+ }
2813
+ async setOption(item) {
2814
+ const existing = await this.getOption(item.key);
2815
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2816
+ if (existing) await this.client.update(cmsOptionsTable).set({
2817
+ value: JSON.stringify(item.value),
2818
+ autoload: item.autoload,
2819
+ namespace: item.namespace ?? null,
2820
+ updatedAt: now
2821
+ }).where((0, _azlib_persistence.eq)(cmsOptionsTable.key, item.key)).execute();
2822
+ else await this.client.insert(cmsOptionsTable).values({
2823
+ key: item.key,
2824
+ value: JSON.stringify(item.value),
2825
+ autoload: item.autoload,
2826
+ namespace: item.namespace ?? null,
2827
+ updatedAt: now
2828
+ }).execute();
2829
+ }
2830
+ async deleteOption(key) {
2831
+ if (!await this.getOption(key)) return false;
2832
+ await this.client.delete(cmsOptionsTable).where((0, _azlib_persistence.eq)(cmsOptionsTable.key, key)).execute();
2833
+ return true;
2834
+ }
2835
+ async getOptions(namespace) {
2836
+ const builder = this.client.select().from(cmsOptionsTable);
2837
+ return (await (namespace ? builder.where((0, _azlib_persistence.eq)(cmsOptionsTable.namespace, namespace)).execute() : builder.execute())).map((r) => ({
2838
+ key: r.key,
2839
+ value: typeof r.value === "string" ? JSON.parse(r.value) : r.value,
2840
+ autoload: Boolean(r.autoload),
2841
+ namespace: r.namespace || void 0,
2842
+ updatedAt: r.updatedAt || r.updated_at
2843
+ }));
2844
+ }
2845
+ deserializeContent(row) {
2846
+ return {
2847
+ id: row.id,
2848
+ collection: row.collection,
2849
+ slug: row.slug,
2850
+ status: row.status,
2851
+ title: row.title || void 0,
2852
+ parentId: row.parentId || row.parent_id || null,
2853
+ authorId: row.authorId || row.author_id || null,
2854
+ publishedAt: row.publishedAt || row.published_at || null,
2855
+ scheduledAt: row.scheduledAt || row.scheduled_at || null,
2856
+ createdAt: row.createdAt || row.created_at,
2857
+ updatedAt: row.updatedAt || row.updated_at,
2858
+ version: Number(row.version),
2859
+ locale: row.locale || void 0,
2860
+ data: typeof row.data === "string" ? JSON.parse(row.data) : row.data || {},
2861
+ terms: row.terms ? typeof row.terms === "string" ? JSON.parse(row.terms) : row.terms : void 0
2862
+ };
2863
+ }
2864
+ deserializeTerm(row) {
2865
+ return {
2866
+ id: row.id,
2867
+ taxonomy: row.taxonomy,
2868
+ name: row.name,
2869
+ slug: row.slug,
2870
+ description: row.description || void 0,
2871
+ parentId: row.parentId || row.parent_id || null,
2872
+ count: Number(row.count),
2873
+ meta: row.meta ? typeof row.meta === "string" ? JSON.parse(row.meta) : row.meta : void 0,
2874
+ createdAt: row.createdAt || row.created_at,
2875
+ updatedAt: row.updatedAt || row.updated_at
2876
+ };
2877
+ }
2878
+ deserializeMedia(row) {
2879
+ return {
2880
+ id: row.id,
2881
+ filename: row.filename,
2882
+ originalName: row.originalName || row.original_name,
2883
+ mimeType: row.mimeType || row.mime_type,
2884
+ sizeBytes: Number(row.sizeBytes || row.size_bytes),
2885
+ url: row.url,
2886
+ path: row.path || void 0,
2887
+ width: row.width ? Number(row.width) : void 0,
2888
+ height: row.height ? Number(row.height) : void 0,
2889
+ altText: row.altText || row.alt_text || void 0,
2890
+ caption: row.caption || void 0,
2891
+ authorId: row.authorId || row.author_id || null,
2892
+ variants: row.variants ? typeof row.variants === "string" ? JSON.parse(row.variants) : row.variants : void 0,
2893
+ createdAt: row.createdAt || row.created_at,
2894
+ updatedAt: row.updatedAt || row.updated_at
2895
+ };
2896
+ }
2897
+ async populateItem(item, populateFields) {
2898
+ const populated = { ...item.populated || {} };
2899
+ for (const field of populateFields) {
2900
+ const rawVal = item.data[field];
2901
+ if (!rawVal) continue;
2902
+ if (Array.isArray(rawVal)) populated[field] = await Promise.all(rawVal.map(async (id) => {
2903
+ if (typeof id === "string") {
2904
+ const rows = await this.client.select().from(cmsContentTable).where((0, _azlib_persistence.eq)(cmsContentTable.id, id)).execute();
2905
+ if (rows.length > 0) return this.deserializeContent(rows[0]);
2906
+ const m = await this.getMedia(id);
2907
+ if (m) return m;
2908
+ }
2909
+ return id;
2910
+ }));
2911
+ else if (typeof rawVal === "string") {
2912
+ const rows = await this.client.select().from(cmsContentTable).where((0, _azlib_persistence.eq)(cmsContentTable.id, rawVal)).execute();
2913
+ if (rows.length > 0) populated[field] = this.deserializeContent(rows[0]);
2914
+ else {
2915
+ const m = await this.getMedia(rawVal);
2916
+ if (m) populated[field] = m;
2917
+ }
2918
+ }
2919
+ }
2920
+ return {
2921
+ ...item,
2922
+ populated
2923
+ };
2924
+ }
2925
+ matchesCondition(actualValue, expected) {
2926
+ if (expected === null || expected === void 0 || typeof expected !== "object" || Array.isArray(expected)) return actualValue === expected;
2927
+ const exp = expected;
2928
+ if (!Object.keys(exp).some((k) => k.startsWith("$"))) return JSON.stringify(actualValue) === JSON.stringify(expected);
2929
+ for (const [op, target] of Object.entries(exp)) switch (op) {
2930
+ case "$eq":
2931
+ if (actualValue !== target) return false;
2932
+ break;
2933
+ case "$ne":
2934
+ if (actualValue === target) return false;
2935
+ break;
2936
+ case "$gt":
2937
+ if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
2938
+ if (actualValue <= target) return false;
2939
+ break;
2940
+ case "$gte":
2941
+ if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
2942
+ if (actualValue < target) return false;
2943
+ break;
2944
+ case "$lt":
2945
+ if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
2946
+ if (actualValue >= target) return false;
2947
+ break;
2948
+ case "$lte":
2949
+ if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
2950
+ if (actualValue > target) return false;
2951
+ break;
2952
+ case "$in":
2953
+ if (!Array.isArray(target) || !target.includes(actualValue)) return false;
2954
+ break;
2955
+ case "$nin":
2956
+ if (Array.isArray(target) && target.includes(actualValue)) return false;
2957
+ break;
2958
+ case "$contains":
2959
+ if (typeof actualValue !== "string" || !actualValue.toLowerCase().includes(String(target).toLowerCase())) return false;
2960
+ break;
2961
+ case "$startsWith":
2962
+ if (typeof actualValue !== "string" || !actualValue.startsWith(String(target))) return false;
2963
+ break;
2964
+ case "$endsWith":
2965
+ if (typeof actualValue !== "string" || !actualValue.endsWith(String(target))) return false;
2966
+ break;
2967
+ case "$between":
2968
+ if (!Array.isArray(target) || target.length !== 2) return false;
2969
+ if (actualValue < target[0] || actualValue > target[1]) return false;
2970
+ break;
2971
+ case "$exists":
2972
+ if (actualValue !== void 0 !== Boolean(target)) return false;
2973
+ break;
2974
+ }
2975
+ return true;
2976
+ }
2977
+ };
2978
+ //#endregion
1579
2979
  //#region src/api/router.ts
1580
2980
  function compilePath(path) {
1581
2981
  const keys = [];
@@ -1602,7 +3002,7 @@ function jsonResponse(data, status = 200, headers = {}) {
1602
3002
  "Content-Type": "application/json",
1603
3003
  "Access-Control-Allow-Origin": "*",
1604
3004
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
1605
- "Access-Control-Allow-Headers": "Content-Type, Authorization",
3005
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key",
1606
3006
  ...headers
1607
3007
  }
1608
3008
  });
@@ -1639,10 +3039,11 @@ var CMSRouter = class {
1639
3039
  headers: {
1640
3040
  "Access-Control-Allow-Origin": "*",
1641
3041
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
1642
- "Access-Control-Allow-Headers": "Content-Type, Authorization"
3042
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key"
1643
3043
  }
1644
3044
  });
1645
3045
  try {
3046
+ const user = await this.engine.auth.authenticateRequest(request);
1646
3047
  if (pathname === "/api/cron/scheduled" && method === "POST") {
1647
3048
  const published = await this.engine.processScheduledContent();
1648
3049
  return jsonResponse({
@@ -1651,10 +3052,11 @@ var CMSRouter = class {
1651
3052
  items: published
1652
3053
  });
1653
3054
  }
1654
- if (pathname.startsWith("/api/options")) return this.handleOptions(pathname, method, url, request);
1655
- if (pathname.startsWith("/api/taxonomies")) return this.handleTaxonomies(pathname, method, url, request);
1656
- if (pathname.startsWith("/api/media")) return this.handleMedia(pathname, method, url, request);
1657
- if (pathname.startsWith("/api/content")) return this.handleContent(pathname, method, url, request);
3055
+ if (pathname.startsWith("/api/auth")) return this.handleAuth(pathname, method, request, user);
3056
+ if (pathname.startsWith("/api/options")) return this.handleOptions(pathname, method, url, request, user);
3057
+ if (pathname.startsWith("/api/taxonomies")) return this.handleTaxonomies(pathname, method, url, request, user);
3058
+ if (pathname.startsWith("/api/media")) return this.handleMedia(pathname, method, url, request, user);
3059
+ if (pathname.startsWith("/api/content")) return this.handleContent(pathname, method, url, request, user);
1658
3060
  const customResponse = await this.handleCustomRoute(pathname, method, url, request);
1659
3061
  if (customResponse) return customResponse;
1660
3062
  return jsonResponse({
@@ -1688,55 +3090,126 @@ var CMSRouter = class {
1688
3090
  }
1689
3091
  return null;
1690
3092
  }
1691
- async handleContent(pathname, method, url, request) {
3093
+ async handleAuth(pathname, method, request, user) {
3094
+ if (pathname === "/api/auth/register" && method === "POST") {
3095
+ const body = await request.json();
3096
+ try {
3097
+ return jsonResponse(await this.engine.auth.register(body), 201);
3098
+ } catch (err) {
3099
+ return jsonResponse({ error: err.message }, 400);
3100
+ }
3101
+ }
3102
+ if (pathname === "/api/auth/login" && method === "POST") {
3103
+ const body = await request.json();
3104
+ try {
3105
+ return jsonResponse(await this.engine.auth.login(body.identifier || body.username || body.email, body.password), 200);
3106
+ } catch (err) {
3107
+ return jsonResponse({ error: err.message }, 401);
3108
+ }
3109
+ }
3110
+ if (pathname === "/api/auth/me" && method === "GET") {
3111
+ if (!user) return jsonResponse({ error: "Unauthorized: Authentication required" }, 401);
3112
+ return jsonResponse(user, 200);
3113
+ }
3114
+ return jsonResponse({ error: "Endpoint not found" }, 404);
3115
+ }
3116
+ async handleContent(pathname, method, url, request, user) {
1692
3117
  const parts = pathname.replace(/^\/api\/content\/?/, "").split("/").filter(Boolean);
1693
- const collectionSlug = parts[0];
1694
- if (!collectionSlug) return jsonResponse({ collections: this.engine.getCollections().map((c) => ({
3118
+ if (parts.length === 0 && method === "GET") return jsonResponse({ collections: this.engine.getCollections().map((c) => ({
1695
3119
  slug: c.slug,
1696
3120
  label: c.label,
1697
3121
  hierarchical: c.hierarchical
1698
3122
  })) }, 200);
3123
+ const collectionSlug = parts[0];
3124
+ const collConfig = this.engine.getCollectionConfig(collectionSlug);
3125
+ if (!collConfig) return jsonResponse({ error: `Collection '${collectionSlug}' not found` }, 404);
1699
3126
  const coll = this.engine.collection(collectionSlug);
1700
3127
  if (parts.length === 1 && method === "GET") {
1701
3128
  const status = url.searchParams.get("status") || void 0;
1702
3129
  const search = url.searchParams.get("search") || void 0;
3130
+ const locale = url.searchParams.get("locale") || void 0;
1703
3131
  const limit = url.searchParams.has("limit") ? Number(url.searchParams.get("limit")) : void 0;
1704
3132
  const offset = url.searchParams.has("offset") ? Number(url.searchParams.get("offset")) : void 0;
1705
3133
  const termIds = url.searchParams.has("termIds") ? url.searchParams.get("termIds").split(",") : void 0;
1706
- return jsonResponse(await coll.find({
3134
+ const populate = url.searchParams.has("populate") ? url.searchParams.get("populate").split(",") : void 0;
3135
+ const select = url.searchParams.has("select") ? url.searchParams.get("select").split(",") : void 0;
3136
+ let where = void 0;
3137
+ if (url.searchParams.has("where")) try {
3138
+ where = JSON.parse(url.searchParams.get("where"));
3139
+ } catch {}
3140
+ const result = await coll.find({
1707
3141
  status,
1708
3142
  search,
1709
3143
  limit,
1710
3144
  offset,
1711
- termIds
1712
- }));
3145
+ termIds,
3146
+ populate,
3147
+ select,
3148
+ locale,
3149
+ where
3150
+ });
3151
+ result.items = result.items.map((it) => this.filterRestrictedFields(it, collConfig, user));
3152
+ return jsonResponse(result);
1713
3153
  }
1714
3154
  if (parts.length === 1 && method === "POST") {
3155
+ 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);
1715
3156
  const body = await request.json();
1716
- return jsonResponse(await coll.create(body), 201);
3157
+ const created = await coll.create(body, user?.id);
3158
+ return jsonResponse(this.filterRestrictedFields(created, collConfig, user), 201);
1717
3159
  }
1718
3160
  const idOrSlug = parts[1];
3161
+ if (parts.length === 3 && parts[2] === "preview" && method === "GET") {
3162
+ const token = url.searchParams.get("token");
3163
+ if (!token) return jsonResponse({ error: "Missing preview token" }, 401);
3164
+ const verified = await this.engine.preview.verifyPreviewToken(token);
3165
+ if (!verified || verified.contentId !== idOrSlug || verified.collection !== collectionSlug) return jsonResponse({ error: "Invalid or expired preview token" }, 403);
3166
+ const item = await coll.findById(idOrSlug);
3167
+ if (!item) return jsonResponse({ error: "Content item not found" }, 404);
3168
+ return jsonResponse(this.filterRestrictedFields(item, collConfig, user));
3169
+ }
1719
3170
  if (parts.length === 3 && parts[2] === "revisions" && method === "GET") return jsonResponse(await coll.getRevisions(idOrSlug));
1720
3171
  if (parts.length === 4 && parts[2] === "restore" && method === "POST") {
3172
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "edit_posts")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
1721
3173
  const revisionId = parts[3];
1722
- const restored = await coll.restoreRevision(idOrSlug, revisionId);
3174
+ const restored = await coll.restoreRevision(idOrSlug, revisionId, user?.id);
1723
3175
  if (!restored) return jsonResponse({ error: "Revision or content item not found" }, 404);
1724
- return jsonResponse(restored);
3176
+ return jsonResponse(this.filterRestrictedFields(restored, collConfig, user));
1725
3177
  }
1726
3178
  if (parts.length === 2 && method === "GET") {
1727
3179
  const bySlug = url.searchParams.get("by") === "slug";
1728
- let item = bySlug ? await coll.findBySlug(idOrSlug) : await coll.findById(idOrSlug);
1729
- if (!item && !bySlug) item = await coll.findBySlug(idOrSlug);
3180
+ const populate = url.searchParams.has("populate") ? url.searchParams.get("populate").split(",") : void 0;
3181
+ const select = url.searchParams.has("select") ? url.searchParams.get("select").split(",") : void 0;
3182
+ const locale = url.searchParams.get("locale") || void 0;
3183
+ let item = bySlug ? await coll.findBySlug(idOrSlug, {
3184
+ populate,
3185
+ select,
3186
+ locale
3187
+ }) : await coll.findById(idOrSlug, {
3188
+ populate,
3189
+ select,
3190
+ locale
3191
+ });
3192
+ if (!item && !bySlug) item = await coll.findBySlug(idOrSlug, {
3193
+ populate,
3194
+ select,
3195
+ locale
3196
+ });
1730
3197
  if (!item) return jsonResponse({ error: "Content item not found" }, 404);
1731
- return jsonResponse(item);
3198
+ return jsonResponse(this.filterRestrictedFields(item, collConfig, user));
1732
3199
  }
1733
3200
  if (parts.length === 2 && method === "PUT") {
3201
+ const existing = await coll.findById(idOrSlug);
3202
+ if (!existing) return jsonResponse({ error: "Content item not found" }, 404);
3203
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "edit_posts", { contentItem: existing })) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
1734
3204
  const body = await request.json();
1735
- const updated = await coll.update(idOrSlug, body);
3205
+ const updated = await coll.update(idOrSlug, body, user?.id);
1736
3206
  if (!updated) return jsonResponse({ error: "Content item not found" }, 404);
1737
- return jsonResponse(updated);
3207
+ return jsonResponse(this.filterRestrictedFields(updated, collConfig, user));
1738
3208
  }
1739
3209
  if (parts.length === 2 && method === "DELETE") {
3210
+ const existing = await coll.findById(idOrSlug);
3211
+ if (!existing) return jsonResponse({ error: "Content item not found" }, 404);
3212
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "delete_posts", { contentItem: existing })) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
1740
3213
  if (!await coll.delete(idOrSlug)) return jsonResponse({ error: "Content item not found" }, 404);
1741
3214
  return jsonResponse({
1742
3215
  success: true,
@@ -1745,7 +3218,7 @@ var CMSRouter = class {
1745
3218
  }
1746
3219
  return jsonResponse({ error: "Method not allowed" }, 405);
1747
3220
  }
1748
- async handleTaxonomies(pathname, method, url, request) {
3221
+ async handleTaxonomies(pathname, method, url, request, user) {
1749
3222
  const parts = pathname.replace(/^\/api\/taxonomies\/?/, "").split("/").filter(Boolean);
1750
3223
  if (parts.length === 0 && method === "GET") return jsonResponse(this.engine.taxonomies.getTaxonomies());
1751
3224
  const taxonomySlug = parts[0];
@@ -1754,6 +3227,7 @@ var CMSRouter = class {
1754
3227
  return jsonResponse(await this.engine.taxonomies.getTerms(taxonomySlug));
1755
3228
  }
1756
3229
  if (parts.length === 2 && parts[1] === "terms" && method === "POST") {
3230
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "manage_taxonomies")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
1757
3231
  const body = await request.json();
1758
3232
  return jsonResponse(await this.engine.taxonomies.createTerm(taxonomySlug, body), 201);
1759
3233
  }
@@ -1763,12 +3237,14 @@ var CMSRouter = class {
1763
3237
  return jsonResponse(term);
1764
3238
  }
1765
3239
  if (parts.length === 3 && parts[1] === "terms" && method === "PUT") {
3240
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "manage_taxonomies")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
1766
3241
  const body = await request.json();
1767
3242
  const updated = await this.engine.taxonomies.updateTerm(parts[2], body);
1768
3243
  if (!updated) return jsonResponse({ error: "Term not found" }, 404);
1769
3244
  return jsonResponse(updated);
1770
3245
  }
1771
3246
  if (parts.length === 3 && parts[1] === "terms" && method === "DELETE") {
3247
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "manage_taxonomies")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
1772
3248
  if (!await this.engine.taxonomies.deleteTerm(parts[2])) return jsonResponse({ error: "Term not found" }, 404);
1773
3249
  return jsonResponse({
1774
3250
  success: true,
@@ -1777,7 +3253,7 @@ var CMSRouter = class {
1777
3253
  }
1778
3254
  return jsonResponse({ error: "Endpoint not found" }, 404);
1779
3255
  }
1780
- async handleMedia(pathname, method, url, request) {
3256
+ async handleMedia(pathname, method, url, request, user) {
1781
3257
  const parts = pathname.replace(/^\/api\/media\/?/, "").split("/").filter(Boolean);
1782
3258
  if (parts.length === 0 && method === "GET") {
1783
3259
  const search = url.searchParams.get("search") || void 0;
@@ -1792,8 +3268,25 @@ var CMSRouter = class {
1792
3268
  }));
1793
3269
  }
1794
3270
  if (parts.length === 0 && method === "POST") {
3271
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "upload_files")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
3272
+ if ((request.headers.get("content-type") || request.headers.get("Content-Type") || "").includes("multipart/form-data")) {
3273
+ const formData = await request.formData();
3274
+ const file = formData.get("file");
3275
+ if (!file || typeof file.arrayBuffer !== "function") return jsonResponse({ error: "No file uploaded in 'file' field" }, 400);
3276
+ const buffer = new Uint8Array(await file.arrayBuffer());
3277
+ const altText = formData.get("altText") || void 0;
3278
+ const caption = formData.get("caption") || void 0;
3279
+ return jsonResponse(await this.engine.media.upload({
3280
+ filename: file.name,
3281
+ mimeType: file.type || "application/octet-stream",
3282
+ buffer,
3283
+ sizeBytes: file.size,
3284
+ altText,
3285
+ caption
3286
+ }, user?.id), 201);
3287
+ }
1795
3288
  const body = await request.json();
1796
- return jsonResponse(await this.engine.media.upload(body), 201);
3289
+ return jsonResponse(await this.engine.media.upload(body, user?.id), 201);
1797
3290
  }
1798
3291
  const id = parts[0];
1799
3292
  if (parts.length === 1 && method === "GET") {
@@ -1802,12 +3295,14 @@ var CMSRouter = class {
1802
3295
  return jsonResponse(media);
1803
3296
  }
1804
3297
  if (parts.length === 1 && method === "PUT") {
3298
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "upload_files")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
1805
3299
  const body = await request.json();
1806
3300
  const updated = await this.engine.media.updateMetadata(id, body);
1807
3301
  if (!updated) return jsonResponse({ error: "Media not found" }, 404);
1808
3302
  return jsonResponse(updated);
1809
3303
  }
1810
3304
  if (parts.length === 1 && method === "DELETE") {
3305
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "delete_files")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
1811
3306
  if (!await this.engine.media.delete(id)) return jsonResponse({ error: "Media not found" }, 404);
1812
3307
  return jsonResponse({
1813
3308
  success: true,
@@ -1816,7 +3311,7 @@ var CMSRouter = class {
1816
3311
  }
1817
3312
  return jsonResponse({ error: "Method not allowed" }, 405);
1818
3313
  }
1819
- async handleOptions(pathname, method, url, request) {
3314
+ async handleOptions(pathname, method, url, request, user) {
1820
3315
  const key = pathname.replace(/^\/api\/options\/?/, "");
1821
3316
  if (!key && method === "GET") {
1822
3317
  const namespace = url.searchParams.get("namespace") || void 0;
@@ -1831,6 +3326,7 @@ var CMSRouter = class {
1831
3326
  });
1832
3327
  }
1833
3328
  if (key && method === "PUT") {
3329
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "manage_options")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
1834
3330
  const body = await request.json();
1835
3331
  return jsonResponse(await this.engine.options.set(key, body.value, {
1836
3332
  autoload: body.autoload,
@@ -1838,6 +3334,7 @@ var CMSRouter = class {
1838
3334
  }));
1839
3335
  }
1840
3336
  if (key && method === "DELETE") {
3337
+ if (this.engine.auth.enabled && !this.engine.rbac.can(user, "manage_options")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
1841
3338
  if (!await this.engine.options.delete(key)) return jsonResponse({ error: "Option not found" }, 404);
1842
3339
  return jsonResponse({
1843
3340
  success: true,
@@ -1846,6 +3343,17 @@ var CMSRouter = class {
1846
3343
  }
1847
3344
  return jsonResponse({ error: "Method not allowed" }, 405);
1848
3345
  }
3346
+ filterRestrictedFields(item, collConfig, user) {
3347
+ if (!collConfig.fields || !item.data) return item;
3348
+ const sanitizedData = { ...item.data };
3349
+ for (const field of collConfig.fields) if (field.readRoles && field.readRoles.length > 0) {
3350
+ if (!(user?.role === "admin" || user?.role && field.readRoles.includes(user.role))) delete sanitizedData[field.name];
3351
+ }
3352
+ return {
3353
+ ...item,
3354
+ data: sanitizedData
3355
+ };
3356
+ }
1849
3357
  };
1850
3358
  function createCMSRouter(engine) {
1851
3359
  return new CMSRouter(engine);
@@ -6654,12 +8162,14 @@ function getTransferService(engine, options) {
6654
8162
  return service;
6655
8163
  }
6656
8164
  //#endregion
8165
+ exports.AuthService = AuthService;
6657
8166
  exports.CMSClient = CMSClient;
6658
8167
  exports.CMSEngine = CMSEngine;
6659
8168
  exports.CMSRouter = CMSRouter;
6660
8169
  exports.ContentLifecycle = ContentLifecycle;
6661
8170
  exports.DEFAULT_COLLECTIONS = DEFAULT_COLLECTIONS;
6662
8171
  exports.DEFAULT_ROLE_CAPABILITIES = DEFAULT_ROLE_CAPABILITIES;
8172
+ exports.DiskMediaStorageDriver = DiskMediaStorageDriver;
6663
8173
  exports.EcommerceClient = EcommerceClient;
6664
8174
  exports.EcommerceService = EcommerceService;
6665
8175
  exports.HRMSClient = HRMSClient;
@@ -6670,14 +8180,18 @@ exports.HooksManager = HooksManager;
6670
8180
  exports.MediaManager = MediaManager;
6671
8181
  exports.MemoryStorageAdapter = MemoryStorageAdapter;
6672
8182
  exports.OptionsManager = OptionsManager;
8183
+ exports.PersistenceStorageAdapter = PersistenceStorageAdapter;
8184
+ exports.PreviewManager = PreviewManager;
6673
8185
  exports.RBACManager = RBACManager;
6674
8186
  exports.RevisionManager = RevisionManager;
6675
8187
  exports.TaxonomyManager = TaxonomyManager;
6676
8188
  exports.TransferClient = TransferClient;
6677
8189
  exports.TransferService = TransferService;
6678
8190
  exports.VALID_STATUS_TRANSITIONS = VALID_STATUS_TRANSITIONS;
8191
+ exports.WebhookManager = WebhookManager;
6679
8192
  exports.applyFieldTransform = applyFieldTransform;
6680
8193
  exports.collection = collection;
8194
+ exports.computeHmacSignature = computeHmacSignature;
6681
8195
  exports.createAttendanceCollection = createAttendanceCollection;
6682
8196
  exports.createCMSEngine = createCMSEngine;
6683
8197
  exports.createCMSRouter = createCMSRouter;
@@ -6704,22 +8218,28 @@ exports.getHRMSClient = getHRMSClient;
6704
8218
  exports.getHRMSService = getHRMSService;
6705
8219
  exports.getTransferClient = getTransferClient;
6706
8220
  exports.getTransferService = getTransferService;
8221
+ exports.hashPassword = hashPassword;
6707
8222
  exports.hrmsPlugin = hrmsPlugin;
6708
8223
  exports.inspectSource = inspectSource;
6709
8224
  exports.mapCmsItemForExport = mapCmsItemForExport;
6710
8225
  exports.mapSourceRecord = mapSourceRecord;
8226
+ exports.mergeLocalizedInput = mergeLocalizedInput;
6711
8227
  exports.normalizeConfig = normalizeConfig;
6712
8228
  exports.parseCsvSource = parseCsvSource;
6713
8229
  exports.parseExcelSource = parseExcelSource;
6714
8230
  exports.parseJsonSource = parseJsonSource;
6715
8231
  exports.parseSource = parseSource;
8232
+ exports.resolveLocalizedData = resolveLocalizedData;
6716
8233
  exports.resolveUniqueSlug = resolveUniqueSlug;
6717
8234
  exports.serializeCsv = serializeCsv;
6718
8235
  exports.serializeExcel = serializeExcel;
6719
8236
  exports.serializeJson = serializeJson;
6720
8237
  exports.serializeSource = serializeSource;
8238
+ exports.signJwt = signJwt;
6721
8239
  exports.slugify = slugify;
6722
8240
  exports.summarizeCollection = summarizeCollection;
6723
8241
  exports.transferPlugin = transferPlugin;
6724
8242
  exports.validateAndNormalizeData = validateAndNormalizeData;
6725
8243
  exports.validateTransferBatch = validateTransferBatch;
8244
+ exports.verifyJwt = verifyJwt;
8245
+ exports.verifyPassword = verifyPassword;