@azlib/cms 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2008,6 +2008,12 @@ var CMSEngine = class {
2008
2008
  return this.plugins.has(name);
2009
2009
  }
2010
2010
  /**
2011
+ * Check if a collection is registered.
2012
+ */
2013
+ hasCollection(slug) {
2014
+ return this.collections.has(slug);
2015
+ }
2016
+ /**
2011
2017
  * Register a collection dynamically.
2012
2018
  */
2013
2019
  registerCollection(coll) {
@@ -2215,7 +2221,11 @@ var CMSEngine = class {
2215
2221
  await self.hooks.doAction("cms.content_updated", updated);
2216
2222
  await self.hooks.doAction(`cms.${slug}_updated`, updated);
2217
2223
  self.webhooks.dispatch("content.updated", updated);
2218
- if (updated.status === "published" && existing.status !== "published") self.webhooks.dispatch("content.published", updated);
2224
+ if (updated.status === "published" && existing.status !== "published") {
2225
+ await self.hooks.doAction("cms.content_published", updated);
2226
+ await self.hooks.doAction(`cms.${slug}_published`, updated);
2227
+ self.webhooks.dispatch("content.published", updated);
2228
+ }
2219
2229
  return self.hooks.applyFilters("cms.after_update_item", updated, { collection: slug });
2220
2230
  }
2221
2231
  return updated;
@@ -4389,7 +4399,7 @@ var EcommerceService = class {
4389
4399
  };
4390
4400
  //#endregion
4391
4401
  //#region src/plugins/ecommerce/routes.ts
4392
- function json$2(data, status = 200) {
4402
+ function json$7(data, status = 200) {
4393
4403
  return new Response(JSON.stringify(data), {
4394
4404
  status,
4395
4405
  headers: {
@@ -4399,10 +4409,10 @@ function json$2(data, status = 200) {
4399
4409
  });
4400
4410
  }
4401
4411
  function badRequest$2(message) {
4402
- return json$2({ error: message }, 400);
4412
+ return json$7({ error: message }, 400);
4403
4413
  }
4404
4414
  function notFound$2(message) {
4405
- return json$2({ error: message }, 404);
4415
+ return json$7({ error: message }, 404);
4406
4416
  }
4407
4417
  function registerEcommerceRoutes(ctx, service, options = {}) {
4408
4418
  const prefix = (options.apiPrefix ?? "/api/ecommerce").replace(/\/+$/, "");
@@ -4425,7 +4435,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
4425
4435
  const offsetStr = url.searchParams.get("offset");
4426
4436
  const limit = limitStr ? parseInt(limitStr, 10) : 20;
4427
4437
  const offset = offsetStr ? parseInt(offsetStr, 10) : 0;
4428
- return json$2(await service.listProducts({
4438
+ return json$7(await service.listProducts({
4429
4439
  categorySlug,
4430
4440
  categoryId,
4431
4441
  tagSlug,
@@ -4454,14 +4464,14 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
4454
4464
  if (!product) product = await service.getProductBySlug(id);
4455
4465
  }
4456
4466
  if (!product) return notFound$2(`Product '${id}' not found`);
4457
- return json$2(product);
4467
+ return json$7(product);
4458
4468
  });
4459
4469
  ctx.registerRoute("POST", `${prefix}/products`, async (req) => {
4460
4470
  try {
4461
4471
  const body = await req.json();
4462
4472
  if (!body.title) return badRequest$2("Product 'title' is required.");
4463
4473
  if (body.price === void 0 || body.price < 0) return badRequest$2("Valid product 'price' is required.");
4464
- return json$2(await service.createProduct(body), 201);
4474
+ return json$7(await service.createProduct(body), 201);
4465
4475
  } catch (err) {
4466
4476
  return badRequest$2(err instanceof Error ? err.message : String(err));
4467
4477
  }
@@ -4471,14 +4481,14 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
4471
4481
  const body = await req.json();
4472
4482
  const updated = await service.updateProduct(params.id, body);
4473
4483
  if (!updated) return notFound$2(`Product '${params.id}' not found.`);
4474
- return json$2(updated);
4484
+ return json$7(updated);
4475
4485
  } catch (err) {
4476
4486
  return badRequest$2(err instanceof Error ? err.message : String(err));
4477
4487
  }
4478
4488
  });
4479
4489
  ctx.registerRoute("DELETE", `${prefix}/products/:id`, async (_req, { params }) => {
4480
4490
  if (!await service.deleteProduct(params.id)) return notFound$2(`Product '${params.id}' not found.`);
4481
- return json$2({
4491
+ return json$7({
4482
4492
  success: true,
4483
4493
  id: params.id
4484
4494
  });
@@ -4487,7 +4497,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
4487
4497
  try {
4488
4498
  const body = await req.json();
4489
4499
  if (!body.filename || !body.mimeType) return badRequest$2("'filename' and 'mimeType' are required.");
4490
- return json$2(await service.uploadProductImage(params.id, {
4500
+ return json$7(await service.uploadProductImage(params.id, {
4491
4501
  filename: body.filename,
4492
4502
  mimeType: body.mimeType,
4493
4503
  sizeBytes: body.sizeBytes ?? 0,
@@ -4504,9 +4514,9 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
4504
4514
  });
4505
4515
  ctx.registerRoute("GET", `${prefix}/categories`, async (_req, { url }) => {
4506
4516
  try {
4507
- if (url.searchParams.get("tree") === "true") return json$2(await service.getCategoryTree());
4517
+ if (url.searchParams.get("tree") === "true") return json$7(await service.getCategoryTree());
4508
4518
  const parentId = url.searchParams.get("parentId");
4509
- return json$2(await service.getCategories({ parentId: parentId === "null" ? null : parentId ?? void 0 }));
4519
+ return json$7(await service.getCategories({ parentId: parentId === "null" ? null : parentId ?? void 0 }));
4510
4520
  } catch (err) {
4511
4521
  return badRequest$2(err instanceof Error ? err.message : String(err));
4512
4522
  }
@@ -4515,7 +4525,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
4515
4525
  try {
4516
4526
  const body = await req.json();
4517
4527
  if (!body.name) return badRequest$2("Category 'name' is required.");
4518
- return json$2(await service.createCategory(body), 201);
4528
+ return json$7(await service.createCategory(body), 201);
4519
4529
  } catch (err) {
4520
4530
  return badRequest$2(err instanceof Error ? err.message : String(err));
4521
4531
  }
@@ -4526,7 +4536,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
4526
4536
  const body = await req.json();
4527
4537
  if (!body.title || !body.code) return badRequest$2("'title' and 'code' are required.");
4528
4538
  if (body.value === void 0 || body.value < 0) return badRequest$2("Valid discount 'value' is required.");
4529
- return json$2(await service.createDiscount(body), 201);
4539
+ return json$7(await service.createDiscount(body), 201);
4530
4540
  } catch (err) {
4531
4541
  return badRequest$2(err instanceof Error ? err.message : String(err));
4532
4542
  }
@@ -4537,7 +4547,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
4537
4547
  if (!body.code) return badRequest$2("Discount 'code' is required.");
4538
4548
  const subtotal = Number(body.subtotal ?? 0);
4539
4549
  const productIds = Array.isArray(body.productIds) ? body.productIds : [];
4540
- return json$2(await service.validateDiscount(body.code, subtotal, productIds));
4550
+ return json$7(await service.validateDiscount(body.code, subtotal, productIds));
4541
4551
  } catch (err) {
4542
4552
  return badRequest$2(err instanceof Error ? err.message : String(err));
4543
4553
  }
@@ -4547,7 +4557,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
4547
4557
  try {
4548
4558
  const body = await req.json();
4549
4559
  if (!body.items || !Array.isArray(body.items) || body.items.length === 0) return badRequest$2("'items' array is required and must not be empty.");
4550
- return json$2(await service.calculateCart(body));
4560
+ return json$7(await service.calculateCart(body));
4551
4561
  } catch (err) {
4552
4562
  return badRequest$2(err instanceof Error ? err.message : String(err));
4553
4563
  }
@@ -4558,7 +4568,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
4558
4568
  const body = await req.json();
4559
4569
  if (!body.customerEmail) return badRequest$2("'customerEmail' is required.");
4560
4570
  if (!body.items || !Array.isArray(body.items) || body.items.length === 0) return badRequest$2("'items' array is required.");
4561
- return json$2(await service.createOrder(body), 201);
4571
+ return json$7(await service.createOrder(body), 201);
4562
4572
  } catch (err) {
4563
4573
  return badRequest$2(err instanceof Error ? err.message : String(err));
4564
4574
  }
@@ -4573,7 +4583,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
4573
4583
  if (!order) order = await service.getOrderByNumber(id);
4574
4584
  }
4575
4585
  if (!order) return notFound$2(`Order '${id}' not found.`);
4576
- return json$2(order);
4586
+ return json$7(order);
4577
4587
  });
4578
4588
  ctx.registerRoute("PATCH", `${prefix}/orders/:id/status`, async (req, { params }) => {
4579
4589
  try {
@@ -4581,7 +4591,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
4581
4591
  if (!body.status) return badRequest$2("New 'status' is required.");
4582
4592
  const updated = await service.updateOrderStatus(params.id, body.status, body.note);
4583
4593
  if (!updated) return notFound$2(`Order '${params.id}' not found.`);
4584
- return json$2(updated);
4594
+ return json$7(updated);
4585
4595
  } catch (err) {
4586
4596
  return badRequest$2(err instanceof Error ? err.message : String(err));
4587
4597
  }
@@ -5817,7 +5827,7 @@ var HRMSService = class {
5817
5827
  };
5818
5828
  //#endregion
5819
5829
  //#region src/plugins/hrms/routes.ts
5820
- function json$1(data, status = 200) {
5830
+ function json$6(data, status = 200) {
5821
5831
  return new Response(JSON.stringify(data), {
5822
5832
  status,
5823
5833
  headers: {
@@ -5827,10 +5837,10 @@ function json$1(data, status = 200) {
5827
5837
  });
5828
5838
  }
5829
5839
  function badRequest$1(message) {
5830
- return json$1({ error: message }, 400);
5840
+ return json$6({ error: message }, 400);
5831
5841
  }
5832
5842
  function notFound$1(message) {
5833
- return json$1({ error: message }, 404);
5843
+ return json$6({ error: message }, 404);
5834
5844
  }
5835
5845
  function registerHRMSRoutes(ctx, service, options = {}) {
5836
5846
  const prefix = (options.apiPrefix ?? "/api/hrms").replace(/\/+$/, "");
@@ -5841,7 +5851,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
5841
5851
  const limitStr = url.searchParams.get("limit");
5842
5852
  const page = pageStr ? parseInt(pageStr, 10) : void 0;
5843
5853
  const limit = limitStr ? parseInt(limitStr, 10) : void 0;
5844
- return json$1(await service.listEmployers({
5854
+ return json$6(await service.listEmployers({
5845
5855
  status,
5846
5856
  page,
5847
5857
  limit
@@ -5854,7 +5864,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
5854
5864
  try {
5855
5865
  const body = await req.json();
5856
5866
  if (!body.companyName) return badRequest$1("companyName is required.");
5857
- return json$1(await service.createEmployer(body), 201);
5867
+ return json$6(await service.createEmployer(body), 201);
5858
5868
  } catch (err) {
5859
5869
  return badRequest$1(err instanceof Error ? err.message : String(err));
5860
5870
  }
@@ -5863,7 +5873,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
5863
5873
  try {
5864
5874
  const employer = await service.getEmployer(params.id);
5865
5875
  if (!employer) return notFound$1(`Employer '${params.id}' not found.`);
5866
- return json$1(employer);
5876
+ return json$6(employer);
5867
5877
  } catch (err) {
5868
5878
  return badRequest$1(err instanceof Error ? err.message : String(err));
5869
5879
  }
@@ -5873,7 +5883,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
5873
5883
  const body = await req.json();
5874
5884
  const updated = await service.updateEmployer(params.id, body);
5875
5885
  if (!updated) return notFound$1(`Employer '${params.id}' not found.`);
5876
- return json$1(updated);
5886
+ return json$6(updated);
5877
5887
  } catch (err) {
5878
5888
  return badRequest$1(err instanceof Error ? err.message : String(err));
5879
5889
  }
@@ -5889,7 +5899,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
5889
5899
  const limitStr = url.searchParams.get("limit");
5890
5900
  const page = pageStr ? parseInt(pageStr, 10) : void 0;
5891
5901
  const limit = limitStr ? parseInt(limitStr, 10) : void 0;
5892
- return json$1(await service.listEmployees({
5902
+ return json$6(await service.listEmployees({
5893
5903
  employerId,
5894
5904
  department,
5895
5905
  employmentType,
@@ -5909,7 +5919,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
5909
5919
  if (!body.employeeNumber) return badRequest$1("employeeNumber is required.");
5910
5920
  if (!body.firstName || !body.lastName) return badRequest$1("firstName and lastName are required.");
5911
5921
  if (!body.email) return badRequest$1("email is required.");
5912
- return json$1(await service.createEmployee(body), 201);
5922
+ return json$6(await service.createEmployee(body), 201);
5913
5923
  } catch (err) {
5914
5924
  return badRequest$1(err instanceof Error ? err.message : String(err));
5915
5925
  }
@@ -5918,7 +5928,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
5918
5928
  try {
5919
5929
  const employee = await service.getEmployee(params.id);
5920
5930
  if (!employee) return notFound$1(`Employee '${params.id}' not found.`);
5921
- return json$1(employee);
5931
+ return json$6(employee);
5922
5932
  } catch (err) {
5923
5933
  return badRequest$1(err instanceof Error ? err.message : String(err));
5924
5934
  }
@@ -5928,7 +5938,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
5928
5938
  const body = await req.json();
5929
5939
  const updated = await service.updateEmployee(params.id, body);
5930
5940
  if (!updated) return notFound$1(`Employee '${params.id}' not found.`);
5931
- return json$1(updated);
5941
+ return json$6(updated);
5932
5942
  } catch (err) {
5933
5943
  return badRequest$1(err instanceof Error ? err.message : String(err));
5934
5944
  }
@@ -5936,7 +5946,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
5936
5946
  ctx.registerRoute("DELETE", `${prefix}/employees/:id`, async (_req, { params }) => {
5937
5947
  try {
5938
5948
  if (!await service.deleteEmployee(params.id)) return notFound$1(`Employee '${params.id}' not found.`);
5939
- return json$1({ success: true });
5949
+ return json$6({ success: true });
5940
5950
  } catch (err) {
5941
5951
  return badRequest$1(err instanceof Error ? err.message : String(err));
5942
5952
  }
@@ -5945,7 +5955,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
5945
5955
  try {
5946
5956
  const yearStr = url.searchParams.get("year");
5947
5957
  const year = yearStr ? parseInt(yearStr, 10) : void 0;
5948
- return json$1(await service.calculateLeaveBalance(params.id, year));
5958
+ return json$6(await service.calculateLeaveBalance(params.id, year));
5949
5959
  } catch (err) {
5950
5960
  return badRequest$1(err instanceof Error ? err.message : String(err));
5951
5961
  }
@@ -5953,7 +5963,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
5953
5963
  ctx.registerRoute("GET", `${prefix}/employees/:id/direct-reports`, async (_req, { params }) => {
5954
5964
  try {
5955
5965
  const reports = await service.getDirectReports(params.id);
5956
- return json$1({
5966
+ return json$6({
5957
5967
  items: reports,
5958
5968
  total: reports.length
5959
5969
  });
@@ -5965,7 +5975,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
5965
5975
  try {
5966
5976
  const body = await req.json();
5967
5977
  if (!body.employeeId) return badRequest$1("employeeId is required.");
5968
- return json$1(await service.checkIn(body), 201);
5978
+ return json$6(await service.checkIn(body), 201);
5969
5979
  } catch (err) {
5970
5980
  return badRequest$1(err instanceof Error ? err.message : String(err));
5971
5981
  }
@@ -5974,7 +5984,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
5974
5984
  try {
5975
5985
  const body = await req.json();
5976
5986
  if (!body.employeeId) return badRequest$1("employeeId is required.");
5977
- return json$1(await service.checkOut(body));
5987
+ return json$6(await service.checkOut(body));
5978
5988
  } catch (err) {
5979
5989
  return badRequest$1(err instanceof Error ? err.message : String(err));
5980
5990
  }
@@ -5991,7 +6001,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
5991
6001
  const limitStr = url.searchParams.get("limit");
5992
6002
  const page = pageStr ? parseInt(pageStr, 10) : void 0;
5993
6003
  const limit = limitStr ? parseInt(limitStr, 10) : void 0;
5994
- return json$1(await service.listAttendance({
6004
+ return json$6(await service.listAttendance({
5995
6005
  employerId,
5996
6006
  employeeId,
5997
6007
  date,
@@ -6009,7 +6019,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
6009
6019
  try {
6010
6020
  const body = await req.json();
6011
6021
  if (!body.employerId || !body.employeeId || !body.date || !body.checkInAt) return badRequest$1("employerId, employeeId, date, and checkInAt are required.");
6012
- return json$1(await service.recordAttendanceManual(body), 201);
6022
+ return json$6(await service.recordAttendanceManual(body), 201);
6013
6023
  } catch (err) {
6014
6024
  return badRequest$1(err instanceof Error ? err.message : String(err));
6015
6025
  }
@@ -6018,7 +6028,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
6018
6028
  try {
6019
6029
  const employerId = url.searchParams.get("employerId") ?? void 0;
6020
6030
  const types = await service.listLeaveTypes(employerId);
6021
- return json$1({
6031
+ return json$6({
6022
6032
  items: types,
6023
6033
  total: types.length
6024
6034
  });
@@ -6030,7 +6040,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
6030
6040
  try {
6031
6041
  const body = await req.json();
6032
6042
  if (!body.name || !body.code || body.daysAllowedPerYear === void 0) return badRequest$1("name, code, and daysAllowedPerYear are required.");
6033
- return json$1(await service.createLeaveType(body), 201);
6043
+ return json$6(await service.createLeaveType(body), 201);
6034
6044
  } catch (err) {
6035
6045
  return badRequest$1(err instanceof Error ? err.message : String(err));
6036
6046
  }
@@ -6039,7 +6049,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
6039
6049
  try {
6040
6050
  const leaveType = await service.getLeaveType(params.id);
6041
6051
  if (!leaveType) return notFound$1(`Leave type '${params.id}' not found.`);
6042
- return json$1(leaveType);
6052
+ return json$6(leaveType);
6043
6053
  } catch (err) {
6044
6054
  return badRequest$1(err instanceof Error ? err.message : String(err));
6045
6055
  }
@@ -6056,7 +6066,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
6056
6066
  const limitStr = url.searchParams.get("limit");
6057
6067
  const page = pageStr ? parseInt(pageStr, 10) : void 0;
6058
6068
  const limit = limitStr ? parseInt(limitStr, 10) : void 0;
6059
- return json$1(await service.listLeaveRequests({
6069
+ return json$6(await service.listLeaveRequests({
6060
6070
  employerId,
6061
6071
  employeeId,
6062
6072
  leaveTypeId,
@@ -6073,7 +6083,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
6073
6083
  try {
6074
6084
  const body = await req.json();
6075
6085
  if (!body.employeeId || !body.leaveTypeId || !body.startDate || !body.endDate) return badRequest$1("employeeId, leaveTypeId, startDate, and endDate are required.");
6076
- return json$1(await service.requestLeave(body), 201);
6086
+ return json$6(await service.requestLeave(body), 201);
6077
6087
  } catch (err) {
6078
6088
  return badRequest$1(err instanceof Error ? err.message : String(err));
6079
6089
  }
@@ -6081,7 +6091,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
6081
6091
  ctx.registerRoute("POST", `${prefix}/leave-requests/:id/approve`, async (req, { params }) => {
6082
6092
  try {
6083
6093
  const approverId = (await req.json().catch(() => ({}))).approverId ?? "admin";
6084
- return json$1(await service.approveLeave({
6094
+ return json$6(await service.approveLeave({
6085
6095
  requestId: params.id,
6086
6096
  approverId
6087
6097
  }));
@@ -6093,7 +6103,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
6093
6103
  try {
6094
6104
  const body = await req.json().catch(() => ({}));
6095
6105
  const approverId = body.approverId ?? "admin";
6096
- return json$1(await service.rejectLeave({
6106
+ return json$6(await service.rejectLeave({
6097
6107
  requestId: params.id,
6098
6108
  approverId,
6099
6109
  reason: body.reason
@@ -6104,7 +6114,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
6104
6114
  });
6105
6115
  ctx.registerRoute("POST", `${prefix}/leave-requests/:id/cancel`, async (_req, { params }) => {
6106
6116
  try {
6107
- return json$1(await service.cancelLeave(params.id));
6117
+ return json$6(await service.cancelLeave(params.id));
6108
6118
  } catch (err) {
6109
6119
  return badRequest$1(err instanceof Error ? err.message : String(err));
6110
6120
  }
@@ -7839,7 +7849,7 @@ var TransferService = class {
7839
7849
  };
7840
7850
  //#endregion
7841
7851
  //#region src/plugins/transfer/routes.ts
7842
- function json(data, status = 200, headers = {}) {
7852
+ function json$5(data, status = 200, headers = {}) {
7843
7853
  return new Response(JSON.stringify(data), {
7844
7854
  status,
7845
7855
  headers: {
@@ -7850,16 +7860,16 @@ function json(data, status = 200, headers = {}) {
7850
7860
  });
7851
7861
  }
7852
7862
  function badRequest(message) {
7853
- return json({ error: message }, 400);
7863
+ return json$5({ error: message }, 400);
7854
7864
  }
7855
7865
  function notFound(message) {
7856
- return json({ error: message }, 404);
7866
+ return json$5({ error: message }, 404);
7857
7867
  }
7858
7868
  function registerTransferRoutes(ctx, service, options = {}) {
7859
7869
  const prefix = (options.apiPrefix ?? "/api/transfer").replace(/\/+$/, "");
7860
7870
  ctx.registerRoute("GET", `${prefix}/collections`, async () => {
7861
7871
  try {
7862
- return json({ collections: service.listCollections() });
7872
+ return json$5({ collections: service.listCollections() });
7863
7873
  } catch (err) {
7864
7874
  return badRequest(err instanceof Error ? err.message : String(err));
7865
7875
  }
@@ -7867,7 +7877,7 @@ function registerTransferRoutes(ctx, service, options = {}) {
7867
7877
  ctx.registerRoute("GET", `${prefix}/presets`, async (_req, { url }) => {
7868
7878
  try {
7869
7879
  const collectionSlug = url.searchParams.get("collection") || void 0;
7870
- return json({ presets: service.getPresets(collectionSlug) });
7880
+ return json$5({ presets: service.getPresets(collectionSlug) });
7871
7881
  } catch (err) {
7872
7882
  return badRequest(err instanceof Error ? err.message : String(err));
7873
7883
  }
@@ -7877,7 +7887,7 @@ function registerTransferRoutes(ctx, service, options = {}) {
7877
7887
  const body = await req.json();
7878
7888
  if (!body.collectionSlug || !Array.isArray(body.fields)) return badRequest("collectionSlug and fields array are required.");
7879
7889
  service.registerPreset(body);
7880
- return json({
7890
+ return json$5({
7881
7891
  success: true,
7882
7892
  preset: body
7883
7893
  }, 201);
@@ -7887,7 +7897,7 @@ function registerTransferRoutes(ctx, service, options = {}) {
7887
7897
  });
7888
7898
  ctx.registerRoute("GET", `${prefix}/:collection/schema`, async (_req, { params }) => {
7889
7899
  try {
7890
- return json({
7900
+ return json$5({
7891
7901
  schema: service.getCollectionSchema(params.collection),
7892
7902
  template: service.getMappingTemplate(params.collection)
7893
7903
  });
@@ -7898,7 +7908,7 @@ function registerTransferRoutes(ctx, service, options = {}) {
7898
7908
  ctx.registerRoute("POST", `${prefix}/inspect`, async (req) => {
7899
7909
  try {
7900
7910
  const payload = await extractPayload(req);
7901
- return json(await service.inspectSource(payload.data, {
7911
+ return json$5(await service.inspectSource(payload.data, {
7902
7912
  collectionSlug: payload.collectionSlug,
7903
7913
  format: payload.format,
7904
7914
  fileName: payload.fileName,
@@ -7911,7 +7921,7 @@ function registerTransferRoutes(ctx, service, options = {}) {
7911
7921
  ctx.registerRoute("POST", `${prefix}/:collection/preview`, async (req, { params }) => {
7912
7922
  try {
7913
7923
  const payload = await extractPayload(req);
7914
- return json(await service.validateImport(params.collection, payload.data, payload.mapping, {
7924
+ return json$5(await service.validateImport(params.collection, payload.data, payload.mapping, {
7915
7925
  format: payload.format,
7916
7926
  fileName: payload.fileName,
7917
7927
  sheetName: payload.sheetName,
@@ -7934,7 +7944,7 @@ function registerTransferRoutes(ctx, service, options = {}) {
7934
7944
  authorId: payload.authorId,
7935
7945
  revisionNote: payload.revisionNote
7936
7946
  });
7937
- return json(result, result.success ? 200 : 207);
7947
+ return json$5(result, result.success ? 200 : 207);
7938
7948
  } catch (err) {
7939
7949
  return badRequest(err instanceof Error ? err.message : String(err));
7940
7950
  }
@@ -8162,6 +8172,2140 @@ function getTransferService(engine, options) {
8162
8172
  return service;
8163
8173
  }
8164
8174
  //#endregion
8175
+ //#region src/plugins/seo/service.ts
8176
+ var SeoService = class {
8177
+ engine;
8178
+ options;
8179
+ siteUrl;
8180
+ constructor(engine, options = {}) {
8181
+ this.engine = engine;
8182
+ this.options = options;
8183
+ this.siteUrl = (options.siteUrl || "").replace(/\/+$/, "");
8184
+ }
8185
+ /**
8186
+ * Generate an XML sitemap for all published content items in configured collections.
8187
+ */
8188
+ async generateSitemap() {
8189
+ const sitemapItems = [];
8190
+ if (this.siteUrl) sitemapItems.push({
8191
+ loc: `${this.siteUrl}/`,
8192
+ changefreq: "daily",
8193
+ priority: 1
8194
+ });
8195
+ const collectionSlugs = this.options.sitemapCollections ?? this.engine.config.collections.map((c) => c.slug);
8196
+ for (const slug of collectionSlugs) {
8197
+ if (slug.startsWith("cms_") || slug.startsWith("audit_")) continue;
8198
+ try {
8199
+ const result = await this.engine.collection(slug).find({
8200
+ status: "published",
8201
+ limit: 1e3
8202
+ });
8203
+ for (const item of result.items) {
8204
+ if (item.data?.noIndex === true) continue;
8205
+ const path = slug === "pages" ? `/${item.slug}` : `/${slug}/${item.slug}`;
8206
+ const loc = this.siteUrl ? `${this.siteUrl}${path}` : path;
8207
+ const lastmod = item.updatedAt || item.publishedAt || item.createdAt;
8208
+ sitemapItems.push({
8209
+ loc,
8210
+ lastmod: lastmod ? new Date(lastmod).toISOString() : void 0,
8211
+ changefreq: "weekly",
8212
+ priority: .8
8213
+ });
8214
+ }
8215
+ } catch {}
8216
+ }
8217
+ const xmlLines = ["<?xml version=\"1.0\" encoding=\"UTF-8\"?>", "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">"];
8218
+ for (const entry of sitemapItems) {
8219
+ xmlLines.push(" <url>");
8220
+ xmlLines.push(` <loc>${this.escapeXml(entry.loc)}</loc>`);
8221
+ if (entry.lastmod) xmlLines.push(` <lastmod>${entry.lastmod}</lastmod>`);
8222
+ if (entry.changefreq) xmlLines.push(` <changefreq>${entry.changefreq}</changefreq>`);
8223
+ if (entry.priority !== void 0) xmlLines.push(` <priority>${entry.priority.toFixed(1)}</priority>`);
8224
+ xmlLines.push(" </url>");
8225
+ }
8226
+ xmlLines.push("</urlset>");
8227
+ return xmlLines.join("\n");
8228
+ }
8229
+ /**
8230
+ * Generate robots.txt content adhering to search crawler specifications.
8231
+ */
8232
+ generateRobotsTxt() {
8233
+ const config = this.options.robotsTxt || {};
8234
+ const lines = [];
8235
+ if (config.policies && config.policies.length > 0) for (const policy of config.policies) {
8236
+ lines.push(`User-agent: ${policy.userAgent}`);
8237
+ if (policy.allow) for (const a of policy.allow) lines.push(`Allow: ${a}`);
8238
+ if (policy.disallow) for (const d of policy.disallow) lines.push(`Disallow: ${d}`);
8239
+ lines.push("");
8240
+ }
8241
+ else {
8242
+ lines.push("User-agent: *");
8243
+ lines.push("Allow: /");
8244
+ lines.push("Disallow: /api/");
8245
+ lines.push("Disallow: /admin/");
8246
+ lines.push("");
8247
+ }
8248
+ const sitemap = config.sitemapUrl || (this.siteUrl ? `${this.siteUrl}/sitemap.xml` : "/sitemap.xml");
8249
+ lines.push(`Sitemap: ${sitemap}`);
8250
+ if (config.host || this.siteUrl) lines.push(`Host: ${config.host || this.siteUrl}`);
8251
+ return lines.join("\n").trim() + "\n";
8252
+ }
8253
+ /**
8254
+ * Resolve consolidated SEO metadata for a document, using smart fallbacks.
8255
+ */
8256
+ async resolveMetadata(collectionSlug, documentOrId) {
8257
+ let item = null;
8258
+ if (typeof documentOrId === "string") item = await this.engine.collection(collectionSlug).findById(documentOrId);
8259
+ else item = documentOrId;
8260
+ if (!item) return {
8261
+ title: this.formatTitle(this.options.defaultTitle || "Not Found"),
8262
+ description: this.options.defaultDescription,
8263
+ robots: "noindex, nofollow"
8264
+ };
8265
+ const data = item.data || {};
8266
+ const rawTitle = typeof data.seoTitle === "string" && data.seoTitle.trim() ? data.seoTitle : item.title || this.options.defaultTitle || "Untitled";
8267
+ const title = this.formatTitle(rawTitle);
8268
+ let description = typeof data.seoDescription === "string" && data.seoDescription.trim() ? data.seoDescription : typeof data.excerpt === "string" && data.excerpt.trim() ? data.excerpt : this.options.defaultDescription;
8269
+ if (!description && typeof data.content === "string") description = this.stripHtml(data.content).slice(0, 160);
8270
+ const path = collectionSlug === "pages" ? `/${item.slug}` : `/${collectionSlug}/${item.slug}`;
8271
+ const canonicalUrl = typeof data.canonicalUrl === "string" && data.canonicalUrl.trim() ? data.canonicalUrl : this.siteUrl ? `${this.siteUrl}${path}` : path;
8272
+ const ogImage = typeof data.ogImage === "string" && data.ogImage.trim() ? data.ogImage : typeof data.featuredImage === "string" && data.featuredImage.trim() ? data.featuredImage : this.options.defaultOgImage;
8273
+ const robots = data.noIndex === true || item.status !== "published" ? "noindex, nofollow" : "index, follow";
8274
+ let keywords;
8275
+ if (typeof data.seoKeywords === "string" && data.seoKeywords.trim()) keywords = data.seoKeywords.split(",").map((k) => k.trim()).filter(Boolean);
8276
+ const jsonLd = data.structuredData && typeof data.structuredData === "object" ? data.structuredData : this.generateJsonLd(collectionSlug, item);
8277
+ return {
8278
+ title,
8279
+ description,
8280
+ canonicalUrl,
8281
+ ogTitle: title,
8282
+ ogDescription: description,
8283
+ ogImage,
8284
+ ogType: collectionSlug === "posts" ? "article" : "website",
8285
+ twitterCard: this.options.defaultTwitterCard || "summary_large_image",
8286
+ robots,
8287
+ keywords,
8288
+ jsonLd
8289
+ };
8290
+ }
8291
+ /**
8292
+ * Generate schema.org JSON-LD structured data for a content item.
8293
+ */
8294
+ generateJsonLd(collectionSlug, item) {
8295
+ const schemaType = collectionSlug === "posts" || collectionSlug === "articles" ? "Article" : "WebPage";
8296
+ const path = collectionSlug === "pages" ? `/${item.slug}` : `/${collectionSlug}/${item.slug}`;
8297
+ const url = this.siteUrl ? `${this.siteUrl}${path}` : path;
8298
+ const schema = {
8299
+ "@context": "https://schema.org",
8300
+ "@type": schemaType,
8301
+ headline: item.title,
8302
+ url,
8303
+ datePublished: item.publishedAt || item.createdAt,
8304
+ dateModified: item.updatedAt || item.publishedAt || item.createdAt
8305
+ };
8306
+ if (item.data?.seoDescription) schema.description = item.data.seoDescription;
8307
+ else if (item.data?.excerpt) schema.description = item.data.excerpt;
8308
+ if (item.data?.ogImage || item.data?.featuredImage) schema.image = item.data.ogImage || item.data.featuredImage;
8309
+ if (item.authorId) schema.author = {
8310
+ "@type": "Person",
8311
+ name: item.authorId
8312
+ };
8313
+ return schema;
8314
+ }
8315
+ /**
8316
+ * Analyze text content and title for SEO optimization, readability, and keyword density.
8317
+ */
8318
+ analyzeContent(input) {
8319
+ const title = (input.title || "").trim();
8320
+ const rawContent = (input.content || "").trim();
8321
+ const cleanContent = this.stripHtml(rawContent);
8322
+ const keyword = (input.keyword || "").trim().toLowerCase();
8323
+ const metaDesc = (input.metaDescription || "").trim();
8324
+ const wordCount = (cleanContent.match(/\b[\w'-]+\b/g) || []).length;
8325
+ const readingTimeMinutes = Math.max(1, Math.ceil(wordCount / 200));
8326
+ let keywordDensity = 0;
8327
+ let keywordCount = 0;
8328
+ if (keyword && wordCount > 0) {
8329
+ const lowerText = cleanContent.toLowerCase();
8330
+ let pos = 0;
8331
+ while ((pos = lowerText.indexOf(keyword, pos)) !== -1) {
8332
+ keywordCount++;
8333
+ pos += keyword.length;
8334
+ }
8335
+ keywordDensity = Number((keywordCount / wordCount * 100).toFixed(2));
8336
+ }
8337
+ const checks = [];
8338
+ const recommendations = [];
8339
+ let score = 100;
8340
+ if (title.length >= 30 && title.length <= 60) checks.push({
8341
+ name: "title_length",
8342
+ passed: true,
8343
+ message: `Title length (${title.length} chars) is optimal.`,
8344
+ scoreImpact: 0
8345
+ });
8346
+ else if (title.length === 0) {
8347
+ checks.push({
8348
+ name: "title_length",
8349
+ passed: false,
8350
+ message: "Title is missing.",
8351
+ scoreImpact: -20
8352
+ });
8353
+ score -= 20;
8354
+ recommendations.push("Provide a page title between 30 and 60 characters.");
8355
+ } else {
8356
+ checks.push({
8357
+ name: "title_length",
8358
+ passed: false,
8359
+ message: `Title is ${title.length} characters long. Recommended is 30–60 characters.`,
8360
+ scoreImpact: -10
8361
+ });
8362
+ score -= 10;
8363
+ recommendations.push(title.length < 30 ? "Expand title to at least 30 characters." : "Shorten title to under 60 characters to avoid snippet truncation.");
8364
+ }
8365
+ if (metaDesc.length >= 50 && metaDesc.length <= 160) checks.push({
8366
+ name: "meta_description_length",
8367
+ passed: true,
8368
+ message: `Meta description length (${metaDesc.length} chars) is optimal.`,
8369
+ scoreImpact: 0
8370
+ });
8371
+ else if (metaDesc.length === 0) {
8372
+ checks.push({
8373
+ name: "meta_description_length",
8374
+ passed: false,
8375
+ message: "Meta description is missing.",
8376
+ scoreImpact: -15
8377
+ });
8378
+ score -= 15;
8379
+ recommendations.push("Add a meta description between 50 and 160 characters.");
8380
+ } else {
8381
+ checks.push({
8382
+ name: "meta_description_length",
8383
+ passed: false,
8384
+ message: `Meta description length (${metaDesc.length} chars) is outside optimal range (50-160).`,
8385
+ scoreImpact: -10
8386
+ });
8387
+ score -= 10;
8388
+ recommendations.push("Adjust meta description length to 50–160 characters.");
8389
+ }
8390
+ if (wordCount >= 300) checks.push({
8391
+ name: "word_count",
8392
+ passed: true,
8393
+ message: `Content length (${wordCount} words) is sufficient for indexing.`,
8394
+ scoreImpact: 0
8395
+ });
8396
+ else {
8397
+ const impact = wordCount === 0 ? -25 : -15;
8398
+ checks.push({
8399
+ name: "word_count",
8400
+ passed: false,
8401
+ message: `Content has ${wordCount} words. At least 300 words recommended.`,
8402
+ scoreImpact: impact
8403
+ });
8404
+ score += impact;
8405
+ recommendations.push("Increase content length to at least 300 words.");
8406
+ }
8407
+ if (keyword) {
8408
+ const titleHasKeyword = title.toLowerCase().includes(keyword);
8409
+ checks.push({
8410
+ name: "keyword_in_title",
8411
+ passed: titleHasKeyword,
8412
+ message: titleHasKeyword ? "Focus keyword appears in the title." : `Focus keyword "${keyword}" is missing from the title.`,
8413
+ scoreImpact: titleHasKeyword ? 0 : -10
8414
+ });
8415
+ if (!titleHasKeyword) {
8416
+ score -= 10;
8417
+ recommendations.push(`Include target keyword "${keyword}" in the title.`);
8418
+ }
8419
+ const descHasKeyword = metaDesc.toLowerCase().includes(keyword);
8420
+ checks.push({
8421
+ name: "keyword_in_description",
8422
+ passed: descHasKeyword,
8423
+ message: descHasKeyword ? "Focus keyword appears in the meta description." : `Focus keyword "${keyword}" is missing from the meta description.`,
8424
+ scoreImpact: descHasKeyword ? 0 : -10
8425
+ });
8426
+ if (!descHasKeyword) {
8427
+ score -= 10;
8428
+ recommendations.push(`Include target keyword "${keyword}" in the meta description.`);
8429
+ }
8430
+ const densityOptimal = keywordDensity >= .5 && keywordDensity <= 2.5;
8431
+ checks.push({
8432
+ name: "keyword_density",
8433
+ passed: densityOptimal,
8434
+ message: `Keyword density is ${keywordDensity}%. Recommended: 0.5% - 2.5%.`,
8435
+ scoreImpact: densityOptimal ? 0 : -10
8436
+ });
8437
+ if (!densityOptimal) {
8438
+ score -= 10;
8439
+ recommendations.push(keywordDensity < .5 ? `Use the keyword "${keyword}" more frequently in content.` : `Reduce repetition of "${keyword}" to prevent keyword stuffing penalties.`);
8440
+ }
8441
+ }
8442
+ return {
8443
+ wordCount,
8444
+ readingTimeMinutes,
8445
+ keywordDensity,
8446
+ score: Math.max(0, Math.min(100, score)),
8447
+ checks,
8448
+ recommendations
8449
+ };
8450
+ }
8451
+ formatTitle(rawTitle) {
8452
+ if (typeof this.options.titleTemplate === "function") return this.options.titleTemplate(rawTitle);
8453
+ if (typeof this.options.titleTemplate === "string") return this.options.titleTemplate.replace("%s", rawTitle);
8454
+ return rawTitle;
8455
+ }
8456
+ stripHtml(html) {
8457
+ return html.replace(/<[^>]*>?/gm, " ").replace(/\s+/g, " ").trim();
8458
+ }
8459
+ escapeXml(unsafe) {
8460
+ return unsafe.replace(/[<>&'"]/g, (c) => {
8461
+ switch (c) {
8462
+ case "<": return "&lt;";
8463
+ case ">": return "&gt;";
8464
+ case "&": return "&amp;";
8465
+ case "'": return "&apos;";
8466
+ case "\"": return "&quot;";
8467
+ default: return c;
8468
+ }
8469
+ });
8470
+ }
8471
+ };
8472
+ /**
8473
+ * Retrieve the active SeoService instance associated with a CMSEngine.
8474
+ */
8475
+ function getSeoService(engine, options) {
8476
+ if (engine.__seoService) return engine.__seoService;
8477
+ const service = new SeoService(engine, options);
8478
+ engine.__seoService = service;
8479
+ return service;
8480
+ }
8481
+ //#endregion
8482
+ //#region src/plugins/seo/routes.ts
8483
+ function json$4(data, status = 200) {
8484
+ return new Response(JSON.stringify(data), {
8485
+ status,
8486
+ headers: {
8487
+ "Content-Type": "application/json",
8488
+ "Access-Control-Allow-Origin": "*"
8489
+ }
8490
+ });
8491
+ }
8492
+ function xml(content, status = 200) {
8493
+ return new Response(content, {
8494
+ status,
8495
+ headers: {
8496
+ "Content-Type": "application/xml; charset=utf-8",
8497
+ "Access-Control-Allow-Origin": "*"
8498
+ }
8499
+ });
8500
+ }
8501
+ function text(content, status = 200) {
8502
+ return new Response(content, {
8503
+ status,
8504
+ headers: {
8505
+ "Content-Type": "text/plain; charset=utf-8",
8506
+ "Access-Control-Allow-Origin": "*"
8507
+ }
8508
+ });
8509
+ }
8510
+ function registerSeoRoutes(ctx, service, options = {}) {
8511
+ const prefix = (options.apiPrefix ?? "/api/seo").replace(/\/+$/, "");
8512
+ ctx.registerRoute("GET", "/sitemap.xml", async () => {
8513
+ try {
8514
+ return xml(await service.generateSitemap());
8515
+ } catch (err) {
8516
+ return json$4({ error: err.message || "Failed to generate sitemap" }, 500);
8517
+ }
8518
+ });
8519
+ ctx.registerRoute("GET", "/robots.txt", () => {
8520
+ try {
8521
+ return text(service.generateRobotsTxt());
8522
+ } catch (err) {
8523
+ return text("User-agent: *\nDisallow: /api/\n", 500);
8524
+ }
8525
+ });
8526
+ ctx.registerRoute("GET", `${prefix}/metadata/:collection/:id`, async (_req, { params }) => {
8527
+ try {
8528
+ const { collection, id } = params;
8529
+ return json$4(await service.resolveMetadata(collection, id));
8530
+ } catch (err) {
8531
+ return json$4({ error: err.message || "Failed to resolve metadata" }, 400);
8532
+ }
8533
+ });
8534
+ ctx.registerRoute("POST", `${prefix}/analyze`, async (req) => {
8535
+ try {
8536
+ const body = await req.json();
8537
+ return json$4(service.analyzeContent(body));
8538
+ } catch (err) {
8539
+ return json$4({ error: err.message || "Invalid SEO analysis request" }, 400);
8540
+ }
8541
+ });
8542
+ }
8543
+ //#endregion
8544
+ //#region src/plugins/seo/schemas.ts
8545
+ /**
8546
+ * Generate standard SEO field definitions to attach to or extend content collections.
8547
+ */
8548
+ function createSeoFieldDefinitions() {
8549
+ return [
8550
+ fields.text({
8551
+ name: "seoTitle",
8552
+ label: "SEO Meta Title",
8553
+ description: "Custom title tag override for search engines and social cards",
8554
+ max: 70
8555
+ }),
8556
+ fields.text({
8557
+ name: "seoDescription",
8558
+ label: "SEO Meta Description",
8559
+ description: "Custom meta description for search engine snippets",
8560
+ max: 160
8561
+ }),
8562
+ fields.text({
8563
+ name: "canonicalUrl",
8564
+ label: "Canonical URL",
8565
+ description: "Authoritative canonical link URL for this content"
8566
+ }),
8567
+ fields.image({
8568
+ name: "ogImage",
8569
+ label: "OpenGraph / Social Image",
8570
+ description: "Featured image displayed when shared on Facebook, Twitter, LinkedIn"
8571
+ }),
8572
+ fields.boolean({
8573
+ name: "noIndex",
8574
+ label: "No Index",
8575
+ description: "Instruct search engines not to index this page (robots noindex)",
8576
+ defaultValue: false
8577
+ }),
8578
+ fields.text({
8579
+ name: "seoKeywords",
8580
+ label: "Keywords",
8581
+ description: "Comma-separated target keywords"
8582
+ }),
8583
+ fields.json({
8584
+ name: "structuredData",
8585
+ label: "Structured Data (JSON-LD)",
8586
+ description: "Custom schema.org structured data JSON override"
8587
+ })
8588
+ ];
8589
+ }
8590
+ //#endregion
8591
+ //#region src/plugins/seo/client.ts
8592
+ var SeoClient = class {
8593
+ client;
8594
+ options;
8595
+ service;
8596
+ prefix;
8597
+ constructor(client, options = {}) {
8598
+ this.client = client;
8599
+ this.options = options;
8600
+ this.prefix = (options.apiPrefix ?? "/api/seo").replace(/\/+$/, "");
8601
+ const engine = client.getEngine();
8602
+ if (engine) this.service = getSeoService(engine, options);
8603
+ }
8604
+ /**
8605
+ * Fetch XML sitemap string.
8606
+ */
8607
+ async getSitemapXml() {
8608
+ if (this.service) return this.service.generateSitemap();
8609
+ const res = await this.client.fetchFn?.(`${this.client.baseUrl || ""}/sitemap.xml`);
8610
+ if (!res) throw new Error("Fetch not available in client");
8611
+ return res.text();
8612
+ }
8613
+ /**
8614
+ * Fetch robots.txt plain text string.
8615
+ */
8616
+ async getRobotsTxt() {
8617
+ if (this.service) return this.service.generateRobotsTxt();
8618
+ const res = await this.client.fetchFn?.(`${this.client.baseUrl || ""}/robots.txt`);
8619
+ if (!res) throw new Error("Fetch not available in client");
8620
+ return res.text();
8621
+ }
8622
+ /**
8623
+ * Resolve consolidated SEO metadata for a document.
8624
+ */
8625
+ async getMetadata(collection, id) {
8626
+ if (this.service) return this.service.resolveMetadata(collection, id);
8627
+ return this.client.request(`${this.prefix}/metadata/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`);
8628
+ }
8629
+ /**
8630
+ * Run SEO and readability analysis on draft content.
8631
+ */
8632
+ async analyze(input) {
8633
+ if (this.service) return this.service.analyzeContent(input);
8634
+ return this.client.request(`${this.prefix}/analyze`, {
8635
+ method: "POST",
8636
+ body: JSON.stringify(input)
8637
+ });
8638
+ }
8639
+ };
8640
+ /**
8641
+ * Get or create an SeoClient adapter for a CMSClient.
8642
+ */
8643
+ function getSeoClient(client, options) {
8644
+ return new SeoClient(client, options);
8645
+ }
8646
+ //#endregion
8647
+ //#region src/plugins/seo/index.ts
8648
+ /**
8649
+ * @azlib/cms - Built-in SEO & Metadata Plugin
8650
+ */
8651
+ /**
8652
+ * Built-in SEO & Metadata plugin factory for @azlib/cms.
8653
+ * Equips the CMS engine with dynamic XML sitemaps, robots.txt generation,
8654
+ * OpenGraph/Twitter social cards, JSON-LD structured data, SEO readability scoring,
8655
+ * and declarative schema field extensions.
8656
+ */
8657
+ const seoPlugin = definePlugin((options) => {
8658
+ const opts = options || {};
8659
+ const extendCollections = {};
8660
+ if (opts.autoExtendCollections) {
8661
+ const targets = Array.isArray(opts.autoExtendCollections) ? opts.autoExtendCollections : ["posts", "pages"];
8662
+ const seoFields = createSeoFieldDefinitions();
8663
+ for (const slug of targets) extendCollections[slug] = seoFields;
8664
+ }
8665
+ return {
8666
+ name: "seo",
8667
+ version: "1.0.0",
8668
+ description: "Universal SEO and metadata plugin supporting XML sitemaps, robots.txt, OpenGraph, JSON-LD, and content auditing",
8669
+ extendCollections: Object.keys(extendCollections).length > 0 ? extendCollections : void 0,
8670
+ setup(ctx) {
8671
+ const service = new SeoService(ctx.engine, opts);
8672
+ ctx.engine.__seoService = service;
8673
+ if (opts.enableRoutes !== false) registerSeoRoutes(ctx, service, opts);
8674
+ }
8675
+ };
8676
+ });
8677
+ //#endregion
8678
+ //#region src/plugins/search/adapters/memory-search-adapter.ts
8679
+ var MemorySearchAdapter = class {
8680
+ docs = /* @__PURE__ */ new Map();
8681
+ invertedIndex = /* @__PURE__ */ new Map();
8682
+ async index(documents) {
8683
+ for (const doc of documents) {
8684
+ const docKey = `${doc.collection}:${doc.id}`;
8685
+ if (this.docs.has(docKey)) await this.remove(doc.collection, doc.id);
8686
+ this.docs.set(docKey, doc);
8687
+ const fieldsToIndex = {
8688
+ title: doc.title || "",
8689
+ slug: doc.slug || "",
8690
+ excerpt: doc.excerpt || "",
8691
+ content: doc.content || ""
8692
+ };
8693
+ if (doc.data) {
8694
+ for (const [k, v] of Object.entries(doc.data)) if (typeof v === "string" && !fieldsToIndex[k]) fieldsToIndex[k] = v;
8695
+ }
8696
+ for (const [field, text] of Object.entries(fieldsToIndex)) {
8697
+ const tokens = this.tokenize(text);
8698
+ for (const token of tokens) {
8699
+ let postingMap = this.invertedIndex.get(token);
8700
+ if (!postingMap) {
8701
+ postingMap = /* @__PURE__ */ new Map();
8702
+ this.invertedIndex.set(token, postingMap);
8703
+ }
8704
+ let entry = postingMap.get(docKey);
8705
+ if (!entry) {
8706
+ entry = {
8707
+ docKey,
8708
+ fieldHits: {}
8709
+ };
8710
+ postingMap.set(docKey, entry);
8711
+ }
8712
+ entry.fieldHits[field] = (entry.fieldHits[field] || 0) + 1;
8713
+ }
8714
+ }
8715
+ }
8716
+ }
8717
+ async remove(collection, id) {
8718
+ const docKey = `${collection}:${id}`;
8719
+ if (!this.docs.has(docKey)) return;
8720
+ this.docs.delete(docKey);
8721
+ for (const [token, postingMap] of this.invertedIndex.entries()) {
8722
+ postingMap.delete(docKey);
8723
+ if (postingMap.size === 0) this.invertedIndex.delete(token);
8724
+ }
8725
+ }
8726
+ async clear() {
8727
+ this.docs.clear();
8728
+ this.invertedIndex.clear();
8729
+ }
8730
+ async search(query) {
8731
+ const startTime = performance.now();
8732
+ const queryTokens = this.tokenize(query.q);
8733
+ if (queryTokens.length === 0) return {
8734
+ items: [],
8735
+ total: 0,
8736
+ query: query.q,
8737
+ tookMs: 0
8738
+ };
8739
+ const docScores = /* @__PURE__ */ new Map();
8740
+ const collectionFilter = query.collections ? new Set(query.collections) : null;
8741
+ const fieldWeights = {
8742
+ title: 10,
8743
+ slug: 6,
8744
+ excerpt: 3,
8745
+ content: 1
8746
+ };
8747
+ for (const qToken of queryTokens) for (const [indexToken, postingMap] of this.invertedIndex.entries()) {
8748
+ const isExact = indexToken === qToken;
8749
+ const isPrefix = !isExact && indexToken.startsWith(qToken);
8750
+ if (!isExact && !isPrefix) continue;
8751
+ const tokenMultiplier = isExact ? 1 : .5;
8752
+ for (const [docKey, entry] of postingMap.entries()) {
8753
+ const doc = this.docs.get(docKey);
8754
+ if (!doc) continue;
8755
+ if (collectionFilter && !collectionFilter.has(doc.collection)) continue;
8756
+ if (query.status && doc.status && doc.status !== query.status) continue;
8757
+ let score = 0;
8758
+ for (const [field, hitCount] of Object.entries(entry.fieldHits)) {
8759
+ const weight = fieldWeights[field] || 1;
8760
+ score += hitCount * weight * tokenMultiplier;
8761
+ }
8762
+ if (doc.title.toLowerCase().includes(query.q.toLowerCase())) score += 15;
8763
+ const current = docScores.get(docKey) || 0;
8764
+ docScores.set(docKey, current + score * (doc.boost || 1));
8765
+ }
8766
+ }
8767
+ const sortedEntries = Array.from(docScores.entries()).sort((a, b) => b[1] - a[1]);
8768
+ const offset = query.offset || 0;
8769
+ const limit = query.limit || 20;
8770
+ const items = sortedEntries.slice(offset, offset + limit).map(([docKey, score]) => {
8771
+ const doc = this.docs.get(docKey);
8772
+ return {
8773
+ id: doc.id,
8774
+ collection: doc.collection,
8775
+ title: doc.title,
8776
+ slug: doc.slug,
8777
+ snippet: this.generateSnippet(doc, query.q),
8778
+ score: Number(score.toFixed(2)),
8779
+ terms: doc.terms
8780
+ };
8781
+ });
8782
+ const tookMs = Number((performance.now() - startTime).toFixed(2));
8783
+ return {
8784
+ items,
8785
+ total: sortedEntries.length,
8786
+ query: query.q,
8787
+ tookMs
8788
+ };
8789
+ }
8790
+ generateSnippet(doc, rawQuery) {
8791
+ const text = doc.excerpt || doc.content;
8792
+ if (!text) return void 0;
8793
+ const queryWords = this.tokenize(rawQuery);
8794
+ const lower = text.toLowerCase();
8795
+ let bestPos = -1;
8796
+ for (const w of queryWords) {
8797
+ const pos = lower.indexOf(w);
8798
+ if (pos !== -1 && (bestPos === -1 || pos < bestPos)) bestPos = pos;
8799
+ }
8800
+ if (bestPos === -1) return text.slice(0, 160) + (text.length > 160 ? "..." : "");
8801
+ const start = Math.max(0, bestPos - 60);
8802
+ const end = Math.min(text.length, bestPos + 100);
8803
+ let snippet = text.slice(start, end).trim();
8804
+ if (start > 0) snippet = "..." + snippet;
8805
+ if (end < text.length) snippet = snippet + "...";
8806
+ return snippet;
8807
+ }
8808
+ tokenize(text) {
8809
+ if (!text) return [];
8810
+ return text.toLowerCase().replace(/<[^>]*>?/gm, " ").replace(/[^\p{L}\p{N}\s-]/gu, " ").split(/\s+/).map((t) => t.trim()).filter((t) => t.length >= 2);
8811
+ }
8812
+ };
8813
+ //#endregion
8814
+ //#region src/plugins/search/service.ts
8815
+ var SearchService = class {
8816
+ engine;
8817
+ options;
8818
+ adapter;
8819
+ constructor(engine, options = {}) {
8820
+ this.engine = engine;
8821
+ this.options = options;
8822
+ this.adapter = options.adapter || new MemorySearchAdapter();
8823
+ if (options.enableAutoIndex !== false) this.attachHooks();
8824
+ }
8825
+ getAdapter() {
8826
+ return this.adapter;
8827
+ }
8828
+ /**
8829
+ * Search across indexed CMS collections.
8830
+ */
8831
+ async search(query) {
8832
+ return this.adapter.search(query);
8833
+ }
8834
+ /**
8835
+ * Index or update a single content item.
8836
+ */
8837
+ async indexItem(item) {
8838
+ if (!this.shouldIndex(item)) {
8839
+ await this.removeItem(item.collection, item.id);
8840
+ return;
8841
+ }
8842
+ const doc = this.toSearchDocument(item);
8843
+ await this.adapter.index([doc]);
8844
+ }
8845
+ /**
8846
+ * Remove a single content item from the search index.
8847
+ */
8848
+ async removeItem(collection, id) {
8849
+ await this.adapter.remove(collection, id);
8850
+ }
8851
+ /**
8852
+ * Reindex all published items across configured CMS collections.
8853
+ */
8854
+ async reindexAll() {
8855
+ await this.adapter.clear();
8856
+ const documents = [];
8857
+ const collectionSlugs = this.getTargetCollections();
8858
+ for (const slug of collectionSlugs) try {
8859
+ const result = await this.engine.collection(slug).find({
8860
+ status: "published",
8861
+ limit: 5e3
8862
+ });
8863
+ for (const item of result.items) if (this.shouldIndex(item)) documents.push(this.toSearchDocument(item));
8864
+ } catch {}
8865
+ if (documents.length > 0) await this.adapter.index(documents);
8866
+ return { indexedCount: documents.length };
8867
+ }
8868
+ shouldIndex(item) {
8869
+ if (item.collection.startsWith("cms_") || item.collection.startsWith("audit_")) return false;
8870
+ if (item.status !== "published") return false;
8871
+ if (item.data?.noIndex === true) return false;
8872
+ if (this.options.collections) if (Array.isArray(this.options.collections)) {
8873
+ if (!this.options.collections.includes(item.collection)) return false;
8874
+ } else {
8875
+ const rule = this.options.collections[item.collection];
8876
+ if (!rule) return false;
8877
+ if (rule.filter && !rule.filter(item)) return false;
8878
+ }
8879
+ return true;
8880
+ }
8881
+ toSearchDocument(item) {
8882
+ const data = item.data || {};
8883
+ const content = typeof data.content === "string" ? data.content : typeof data.description === "string" ? data.description : void 0;
8884
+ const excerpt = typeof data.excerpt === "string" ? data.excerpt : typeof data.seoDescription === "string" ? data.seoDescription : void 0;
8885
+ return {
8886
+ id: item.id,
8887
+ collection: item.collection,
8888
+ title: item.title || item.slug,
8889
+ slug: item.slug,
8890
+ content,
8891
+ excerpt,
8892
+ status: item.status,
8893
+ terms: item.terms,
8894
+ data
8895
+ };
8896
+ }
8897
+ getTargetCollections() {
8898
+ if (this.options.collections) return Array.isArray(this.options.collections) ? this.options.collections : Object.keys(this.options.collections);
8899
+ return this.engine.config.collections.map((c) => c.slug).filter((s) => !s.startsWith("cms_") && !s.startsWith("audit_"));
8900
+ }
8901
+ attachHooks() {
8902
+ this.engine.hooks.addAction("cms.content_created", async (...args) => {
8903
+ const item = args[0];
8904
+ if (item) await this.indexItem(item);
8905
+ });
8906
+ this.engine.hooks.addAction("cms.content_updated", async (...args) => {
8907
+ const item = args[0];
8908
+ if (item) await this.indexItem(item);
8909
+ });
8910
+ this.engine.hooks.addAction("cms.content_published", async (...args) => {
8911
+ const item = args[0];
8912
+ if (item) await this.indexItem(item);
8913
+ });
8914
+ this.engine.hooks.addAction("cms.content_deleted", async (...args) => {
8915
+ const item = args[0];
8916
+ if (item) await this.removeItem(item.collection, item.id);
8917
+ });
8918
+ }
8919
+ };
8920
+ /**
8921
+ * Retrieve the active SearchService instance associated with a CMSEngine.
8922
+ */
8923
+ function getSearchService(engine, options) {
8924
+ if (engine.__searchService) return engine.__searchService;
8925
+ const service = new SearchService(engine, options);
8926
+ engine.__searchService = service;
8927
+ return service;
8928
+ }
8929
+ //#endregion
8930
+ //#region src/plugins/search/routes.ts
8931
+ function json$3(data, status = 200) {
8932
+ return new Response(JSON.stringify(data), {
8933
+ status,
8934
+ headers: {
8935
+ "Content-Type": "application/json",
8936
+ "Access-Control-Allow-Origin": "*"
8937
+ }
8938
+ });
8939
+ }
8940
+ function registerSearchRoutes(ctx, service, options = {}) {
8941
+ const prefix = (options.apiPrefix ?? "/api/search").replace(/\/+$/, "");
8942
+ ctx.registerRoute("GET", prefix, async (_req, { url }) => {
8943
+ try {
8944
+ const q = url.searchParams.get("q") || "";
8945
+ const collectionsParam = url.searchParams.get("collections");
8946
+ const searchQuery = {
8947
+ q,
8948
+ collections: collectionsParam ? collectionsParam.split(",").map((c) => c.trim()).filter(Boolean) : void 0,
8949
+ limit: url.searchParams.get("limit") ? parseInt(url.searchParams.get("limit"), 10) : 20,
8950
+ offset: url.searchParams.get("offset") ? parseInt(url.searchParams.get("offset"), 10) : 0
8951
+ };
8952
+ return json$3(await service.search(searchQuery));
8953
+ } catch (err) {
8954
+ return json$3({ error: err.message || "Failed to perform search" }, 500);
8955
+ }
8956
+ });
8957
+ ctx.registerRoute("POST", `${prefix}/reindex`, async () => {
8958
+ try {
8959
+ return json$3({
8960
+ success: true,
8961
+ ...await service.reindexAll()
8962
+ });
8963
+ } catch (err) {
8964
+ return json$3({ error: err.message || "Failed to reindex search" }, 500);
8965
+ }
8966
+ });
8967
+ }
8968
+ //#endregion
8969
+ //#region src/plugins/search/client.ts
8970
+ var SearchClient = class {
8971
+ client;
8972
+ options;
8973
+ service;
8974
+ prefix;
8975
+ constructor(client, options = {}) {
8976
+ this.client = client;
8977
+ this.options = options;
8978
+ this.prefix = (options.apiPrefix ?? "/api/search").replace(/\/+$/, "");
8979
+ const engine = client.getEngine();
8980
+ if (engine) this.service = getSearchService(engine, options);
8981
+ }
8982
+ /**
8983
+ * Search across indexed CMS collections.
8984
+ */
8985
+ async search(query) {
8986
+ if (this.service) return this.service.search(query);
8987
+ const params = new URLSearchParams();
8988
+ params.set("q", query.q);
8989
+ if (query.collections && query.collections.length > 0) params.set("collections", query.collections.join(","));
8990
+ if (query.limit !== void 0) params.set("limit", String(query.limit));
8991
+ if (query.offset !== void 0) params.set("offset", String(query.offset));
8992
+ if (query.status) params.set("status", query.status);
8993
+ const q = params.toString() ? `?${params.toString()}` : "";
8994
+ return this.client.request(`${this.prefix}${q}`);
8995
+ }
8996
+ /**
8997
+ * Reindex all content in the search index.
8998
+ */
8999
+ async reindex() {
9000
+ if (this.service) return {
9001
+ success: true,
9002
+ ...await this.service.reindexAll()
9003
+ };
9004
+ return this.client.request(`${this.prefix}/reindex`, { method: "POST" });
9005
+ }
9006
+ };
9007
+ /**
9008
+ * Get or create a SearchClient adapter for a CMSClient.
9009
+ */
9010
+ function getSearchClient(client, options) {
9011
+ return new SearchClient(client, options);
9012
+ }
9013
+ //#endregion
9014
+ //#region src/plugins/search/index.ts
9015
+ /**
9016
+ * @azlib/cms - Built-in Search Indexing & Discovery Plugin
9017
+ */
9018
+ /**
9019
+ * Built-in Search Indexing & Discovery plugin factory for @azlib/cms.
9020
+ * Equips the CMS engine with fast inverted-index full-text search, field weighting,
9021
+ * fuzzy/prefix matching, snippet highlights, and automatic indexing via lifecycle hooks.
9022
+ */
9023
+ const searchPlugin = definePlugin((options) => {
9024
+ const opts = options || {};
9025
+ return {
9026
+ name: "search",
9027
+ version: "1.0.0",
9028
+ description: "Universal full-text search indexing and discovery plugin with inverted index, field weighting, and auto-indexing",
9029
+ setup(ctx) {
9030
+ const service = new SearchService(ctx.engine, opts);
9031
+ ctx.engine.__searchService = service;
9032
+ if (opts.enableRoutes !== false) registerSearchRoutes(ctx, service, opts);
9033
+ }
9034
+ };
9035
+ });
9036
+ //#endregion
9037
+ //#region src/plugins/audit-log/service.ts
9038
+ var AuditLogService = class {
9039
+ engine;
9040
+ options;
9041
+ inMemoryLogs = [];
9042
+ maxEntries;
9043
+ constructor(engine, options = {}) {
9044
+ this.engine = engine;
9045
+ this.options = options;
9046
+ this.maxEntries = options.maxEntries ?? 1e4;
9047
+ this.attachHooks();
9048
+ }
9049
+ /**
9050
+ * Record an audit log entry.
9051
+ */
9052
+ async record(entry) {
9053
+ const id = `audit_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
9054
+ const fullEntry = {
9055
+ ...entry,
9056
+ id,
9057
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
9058
+ };
9059
+ this.inMemoryLogs.unshift(fullEntry);
9060
+ if (this.inMemoryLogs.length > this.maxEntries) this.inMemoryLogs.pop();
9061
+ try {
9062
+ if (this.engine.hasCollection("cms_audit_logs")) await this.engine.collection("cms_audit_logs").create({
9063
+ title: `[${fullEntry.action.toUpperCase()}] ${fullEntry.entityType}:${fullEntry.entityId}`,
9064
+ data: {
9065
+ action: fullEntry.action,
9066
+ entityType: fullEntry.entityType,
9067
+ collectionSlug: fullEntry.collectionSlug,
9068
+ entityId: fullEntry.entityId,
9069
+ actorId: fullEntry.actorId,
9070
+ actorName: fullEntry.actorName,
9071
+ ip: fullEntry.ip,
9072
+ userAgent: fullEntry.userAgent,
9073
+ diff: fullEntry.diff,
9074
+ metadata: fullEntry.metadata
9075
+ }
9076
+ });
9077
+ } catch {}
9078
+ await this.engine.hooks.doAction("audit.logged", fullEntry);
9079
+ return fullEntry;
9080
+ }
9081
+ /**
9082
+ * Query audit logs with multi-attribute filtering and pagination.
9083
+ */
9084
+ async query(query = {}) {
9085
+ let filtered = [...this.inMemoryLogs];
9086
+ if (query.action) filtered = filtered.filter((e) => e.action === query.action);
9087
+ if (query.entityType) filtered = filtered.filter((e) => e.entityType === query.entityType);
9088
+ if (query.collectionSlug) filtered = filtered.filter((e) => e.collectionSlug === query.collectionSlug);
9089
+ if (query.entityId) filtered = filtered.filter((e) => e.entityId === query.entityId);
9090
+ if (query.actorId) filtered = filtered.filter((e) => e.actorId === query.actorId);
9091
+ if (query.startDate) {
9092
+ const start = new Date(query.startDate).getTime();
9093
+ filtered = filtered.filter((e) => new Date(e.timestamp).getTime() >= start);
9094
+ }
9095
+ if (query.endDate) {
9096
+ const end = new Date(query.endDate).getTime();
9097
+ filtered = filtered.filter((e) => new Date(e.timestamp).getTime() <= end);
9098
+ }
9099
+ const total = filtered.length;
9100
+ const limit = query.limit ?? 50;
9101
+ const offset = query.offset ?? 0;
9102
+ const items = filtered.slice(offset, offset + limit);
9103
+ return {
9104
+ items,
9105
+ total,
9106
+ limit,
9107
+ offset,
9108
+ hasMore: offset + items.length < total
9109
+ };
9110
+ }
9111
+ /**
9112
+ * Get an audit log entry by its ID.
9113
+ */
9114
+ async getById(id) {
9115
+ const memoryFound = this.inMemoryLogs.find((e) => e.id === id);
9116
+ if (memoryFound) return memoryFound;
9117
+ try {
9118
+ if (this.engine.hasCollection("cms_audit_logs")) {
9119
+ const item = await this.engine.collection("cms_audit_logs").findById(id);
9120
+ if (item) return {
9121
+ id: item.id,
9122
+ action: item.data?.action || "unknown",
9123
+ entityType: item.data?.entityType || "content",
9124
+ collectionSlug: item.data?.collectionSlug,
9125
+ entityId: item.data?.entityId || item.id,
9126
+ actorId: item.data?.actorId,
9127
+ actorName: item.data?.actorName,
9128
+ ip: item.data?.ip,
9129
+ userAgent: item.data?.userAgent,
9130
+ diff: item.data?.diff,
9131
+ timestamp: item.createdAt,
9132
+ metadata: item.data?.metadata
9133
+ };
9134
+ }
9135
+ } catch {}
9136
+ return null;
9137
+ }
9138
+ /**
9139
+ * Calculate a field-level diff between two states.
9140
+ */
9141
+ calculateDiff(before, after) {
9142
+ const b = before || {};
9143
+ const a = after || {};
9144
+ const allKeys = Array.from(/* @__PURE__ */ new Set([...Object.keys(b), ...Object.keys(a)]));
9145
+ const changedFields = [];
9146
+ const beforeDiff = {};
9147
+ const afterDiff = {};
9148
+ for (const key of allKeys) {
9149
+ const valBefore = b[key];
9150
+ const valAfter = a[key];
9151
+ if (JSON.stringify(valBefore) !== JSON.stringify(valAfter)) {
9152
+ changedFields.push(key);
9153
+ if (valBefore !== void 0) beforeDiff[key] = valBefore;
9154
+ if (valAfter !== void 0) afterDiff[key] = valAfter;
9155
+ }
9156
+ }
9157
+ return {
9158
+ before: Object.keys(beforeDiff).length > 0 ? beforeDiff : void 0,
9159
+ after: Object.keys(afterDiff).length > 0 ? afterDiff : void 0,
9160
+ changedFields
9161
+ };
9162
+ }
9163
+ shouldAuditCollection(collectionSlug) {
9164
+ if (collectionSlug.startsWith("cms_") || collectionSlug.startsWith("audit_")) return false;
9165
+ if (this.options.collections && this.options.collections.length > 0) return this.options.collections.includes(collectionSlug);
9166
+ return true;
9167
+ }
9168
+ attachHooks() {
9169
+ this.engine.hooks.addAction("cms.content_created", async (...args) => {
9170
+ const item = args[0];
9171
+ if (!item || !this.shouldAuditCollection(item.collection)) return;
9172
+ await this.record({
9173
+ action: "create",
9174
+ entityType: "content",
9175
+ collectionSlug: item.collection,
9176
+ entityId: item.id,
9177
+ actorId: item.authorId,
9178
+ diff: {
9179
+ after: {
9180
+ title: item.title,
9181
+ slug: item.slug,
9182
+ status: item.status,
9183
+ ...item.data
9184
+ },
9185
+ changedFields: [
9186
+ "title",
9187
+ "slug",
9188
+ "status",
9189
+ ...Object.keys(item.data || {})
9190
+ ]
9191
+ },
9192
+ metadata: { status: item.status }
9193
+ });
9194
+ });
9195
+ this.engine.hooks.addAction("cms.content_updated", async (...args) => {
9196
+ const item = args[0];
9197
+ if (!item || !this.shouldAuditCollection(item.collection)) return;
9198
+ const action = item.status === "published" ? "publish" : "update";
9199
+ await this.record({
9200
+ action,
9201
+ entityType: "content",
9202
+ collectionSlug: item.collection,
9203
+ entityId: item.id,
9204
+ actorId: item.authorId,
9205
+ diff: {
9206
+ after: {
9207
+ title: item.title,
9208
+ slug: item.slug,
9209
+ status: item.status,
9210
+ ...item.data
9211
+ },
9212
+ changedFields: [
9213
+ "title",
9214
+ "slug",
9215
+ "status",
9216
+ ...Object.keys(item.data || {})
9217
+ ]
9218
+ },
9219
+ metadata: { status: item.status }
9220
+ });
9221
+ });
9222
+ this.engine.hooks.addAction("cms.content_deleted", async (...args) => {
9223
+ const item = args[0];
9224
+ if (!item || !this.shouldAuditCollection(item.collection)) return;
9225
+ await this.record({
9226
+ action: "delete",
9227
+ entityType: "content",
9228
+ collectionSlug: item.collection,
9229
+ entityId: item.id,
9230
+ actorId: item.authorId,
9231
+ metadata: {
9232
+ title: item.title,
9233
+ slug: item.slug
9234
+ }
9235
+ });
9236
+ });
9237
+ if (this.options.recordMedia !== false) {
9238
+ this.engine.hooks.addAction("cms.media_uploaded", async (...args) => {
9239
+ const media = args[0];
9240
+ if (!media) return;
9241
+ await this.record({
9242
+ action: "upload",
9243
+ entityType: "media",
9244
+ entityId: media.id,
9245
+ actorId: media.authorId,
9246
+ metadata: {
9247
+ filename: media.filename,
9248
+ mimeType: media.mimeType,
9249
+ sizeBytes: media.sizeBytes
9250
+ }
9251
+ });
9252
+ });
9253
+ this.engine.hooks.addAction("cms.media_deleted", async (...args) => {
9254
+ const media = args[0];
9255
+ if (!media) return;
9256
+ await this.record({
9257
+ action: "delete",
9258
+ entityType: "media",
9259
+ entityId: media.id,
9260
+ actorId: media.authorId,
9261
+ metadata: { filename: media.filename }
9262
+ });
9263
+ });
9264
+ }
9265
+ if (this.options.recordTaxonomies !== false) {
9266
+ this.engine.hooks.addAction("cms.term_created", async (...args) => {
9267
+ const term = args[0];
9268
+ if (!term) return;
9269
+ await this.record({
9270
+ action: "create",
9271
+ entityType: "taxonomy",
9272
+ collectionSlug: term.taxonomy,
9273
+ entityId: term.id,
9274
+ metadata: {
9275
+ name: term.name,
9276
+ slug: term.slug
9277
+ }
9278
+ });
9279
+ });
9280
+ this.engine.hooks.addAction("cms.term_deleted", async (...args) => {
9281
+ const term = args[0];
9282
+ if (!term) return;
9283
+ await this.record({
9284
+ action: "delete",
9285
+ entityType: "taxonomy",
9286
+ collectionSlug: term.taxonomy,
9287
+ entityId: term.id,
9288
+ metadata: {
9289
+ name: term.name,
9290
+ slug: term.slug
9291
+ }
9292
+ });
9293
+ });
9294
+ }
9295
+ if (this.options.recordOptions !== false) this.engine.hooks.addAction("cms.option_updated", async (...args) => {
9296
+ const key = args[0];
9297
+ const value = args[1];
9298
+ if (!key) return;
9299
+ await this.record({
9300
+ action: "option_change",
9301
+ entityType: "option",
9302
+ entityId: key,
9303
+ diff: {
9304
+ after: { [key]: value },
9305
+ changedFields: [key]
9306
+ }
9307
+ });
9308
+ });
9309
+ }
9310
+ };
9311
+ /**
9312
+ * Retrieve the active AuditLogService instance associated with a CMSEngine.
9313
+ */
9314
+ function getAuditLogService(engine, options) {
9315
+ if (engine.__auditLogService) return engine.__auditLogService;
9316
+ const service = new AuditLogService(engine, options);
9317
+ engine.__auditLogService = service;
9318
+ return service;
9319
+ }
9320
+ //#endregion
9321
+ //#region src/plugins/audit-log/routes.ts
9322
+ function json$2(data, status = 200) {
9323
+ return new Response(JSON.stringify(data), {
9324
+ status,
9325
+ headers: {
9326
+ "Content-Type": "application/json",
9327
+ "Access-Control-Allow-Origin": "*"
9328
+ }
9329
+ });
9330
+ }
9331
+ function registerAuditLogRoutes(ctx, service, options = {}) {
9332
+ const prefix = (options.apiPrefix ?? "/api/audit-logs").replace(/\/+$/, "");
9333
+ ctx.registerRoute("GET", prefix, async (_req, { url }) => {
9334
+ try {
9335
+ const query = {
9336
+ action: url.searchParams.get("action") || void 0,
9337
+ entityType: url.searchParams.get("entityType") || void 0,
9338
+ collectionSlug: url.searchParams.get("collectionSlug") || void 0,
9339
+ entityId: url.searchParams.get("entityId") || void 0,
9340
+ actorId: url.searchParams.get("actorId") || void 0,
9341
+ startDate: url.searchParams.get("startDate") || void 0,
9342
+ endDate: url.searchParams.get("endDate") || void 0,
9343
+ limit: url.searchParams.get("limit") ? parseInt(url.searchParams.get("limit"), 10) : void 0,
9344
+ offset: url.searchParams.get("offset") ? parseInt(url.searchParams.get("offset"), 10) : void 0
9345
+ };
9346
+ return json$2(await service.query(query));
9347
+ } catch (err) {
9348
+ return json$2({ error: err.message || "Failed to query audit logs" }, 500);
9349
+ }
9350
+ });
9351
+ ctx.registerRoute("GET", `${prefix}/:id`, async (_req, { params }) => {
9352
+ try {
9353
+ const entry = await service.getById(params.id);
9354
+ if (!entry) return json$2({ error: "Audit log entry not found" }, 404);
9355
+ return json$2(entry);
9356
+ } catch (err) {
9357
+ return json$2({ error: err.message || "Failed to retrieve audit log" }, 500);
9358
+ }
9359
+ });
9360
+ }
9361
+ //#endregion
9362
+ //#region src/plugins/audit-log/schemas.ts
9363
+ /**
9364
+ * @azlib/cms - Built-in Audit Log & Activity Trail Schemas
9365
+ */
9366
+ function createAuditLogCollection(_options = {}) {
9367
+ return collection({
9368
+ label: "Audit Logs",
9369
+ slug: "cms_audit_logs",
9370
+ timestamps: true,
9371
+ revisions: false,
9372
+ fields: [
9373
+ fields.text({
9374
+ name: "action",
9375
+ label: "Action",
9376
+ required: true
9377
+ }),
9378
+ fields.text({
9379
+ name: "entityType",
9380
+ label: "Entity Type",
9381
+ required: true
9382
+ }),
9383
+ fields.text({
9384
+ name: "collectionSlug",
9385
+ label: "Collection Slug"
9386
+ }),
9387
+ fields.text({
9388
+ name: "entityId",
9389
+ label: "Entity ID",
9390
+ required: true
9391
+ }),
9392
+ fields.text({
9393
+ name: "actorId",
9394
+ label: "Actor / User ID"
9395
+ }),
9396
+ fields.text({
9397
+ name: "actorName",
9398
+ label: "Actor Name"
9399
+ }),
9400
+ fields.text({
9401
+ name: "ip",
9402
+ label: "IP Address"
9403
+ }),
9404
+ fields.text({
9405
+ name: "userAgent",
9406
+ label: "User Agent"
9407
+ }),
9408
+ fields.json({
9409
+ name: "diff",
9410
+ label: "Change Diff"
9411
+ }),
9412
+ fields.json({
9413
+ name: "metadata",
9414
+ label: "Event Metadata"
9415
+ })
9416
+ ]
9417
+ });
9418
+ }
9419
+ //#endregion
9420
+ //#region src/plugins/audit-log/client.ts
9421
+ var AuditLogClient = class {
9422
+ client;
9423
+ options;
9424
+ service;
9425
+ prefix;
9426
+ constructor(client, options = {}) {
9427
+ this.client = client;
9428
+ this.options = options;
9429
+ this.prefix = (options.apiPrefix ?? "/api/audit-logs").replace(/\/+$/, "");
9430
+ const engine = client.getEngine();
9431
+ if (engine) this.service = getAuditLogService(engine, options);
9432
+ }
9433
+ /**
9434
+ * Query recorded audit logs with multi-field filters.
9435
+ */
9436
+ async query(query = {}) {
9437
+ if (this.service) return this.service.query(query);
9438
+ const params = new URLSearchParams();
9439
+ if (query.action) params.set("action", query.action);
9440
+ if (query.entityType) params.set("entityType", query.entityType);
9441
+ if (query.collectionSlug) params.set("collectionSlug", query.collectionSlug);
9442
+ if (query.entityId) params.set("entityId", query.entityId);
9443
+ if (query.actorId) params.set("actorId", query.actorId);
9444
+ if (query.startDate) params.set("startDate", query.startDate);
9445
+ if (query.endDate) params.set("endDate", query.endDate);
9446
+ if (query.limit !== void 0) params.set("limit", String(query.limit));
9447
+ if (query.offset !== void 0) params.set("offset", String(query.offset));
9448
+ const q = params.toString() ? `?${params.toString()}` : "";
9449
+ return this.client.request(`${this.prefix}${q}`);
9450
+ }
9451
+ /**
9452
+ * Fetch a single audit log entry by ID.
9453
+ */
9454
+ async get(id) {
9455
+ if (this.service) return this.service.getById(id);
9456
+ return this.client.request(`${this.prefix}/${encodeURIComponent(id)}`);
9457
+ }
9458
+ };
9459
+ /**
9460
+ * Get or create an AuditLogClient adapter for a CMSClient.
9461
+ */
9462
+ function getAuditLogClient(client, options) {
9463
+ return new AuditLogClient(client, options);
9464
+ }
9465
+ //#endregion
9466
+ //#region src/plugins/audit-log/index.ts
9467
+ /**
9468
+ * @azlib/cms - Built-in Audit Log & Activity Trail Plugin
9469
+ */
9470
+ /**
9471
+ * Built-in Audit Log & Activity Trail plugin factory for @azlib/cms.
9472
+ * Equips the CMS engine with immutable enterprise change logs, mutation auditing
9473
+ * across content, media, taxonomies, and options, field-level diffs, and query APIs.
9474
+ */
9475
+ const auditLogPlugin = definePlugin((options) => {
9476
+ const opts = options || {};
9477
+ return {
9478
+ name: "audit-log",
9479
+ version: "1.0.0",
9480
+ description: "Universal enterprise audit logging and activity trail plugin with deep diff tracking and compliance query APIs",
9481
+ collections: [createAuditLogCollection(opts)],
9482
+ setup(ctx) {
9483
+ const service = new AuditLogService(ctx.engine, opts);
9484
+ ctx.engine.__auditLogService = service;
9485
+ if (opts.enableRoutes !== false) registerAuditLogRoutes(ctx, service, opts);
9486
+ }
9487
+ };
9488
+ });
9489
+ //#endregion
9490
+ //#region src/plugins/forms/service.ts
9491
+ var FormsService = class {
9492
+ engine;
9493
+ options;
9494
+ inMemoryForms = /* @__PURE__ */ new Map();
9495
+ inMemorySubmissions = [];
9496
+ honeypotField;
9497
+ constructor(engine, options = {}) {
9498
+ this.engine = engine;
9499
+ this.options = options;
9500
+ this.honeypotField = options.honeypotFieldName || "_az_hp";
9501
+ }
9502
+ /**
9503
+ * Register or update a form schema programmatically or dynamically.
9504
+ */
9505
+ async registerForm(form) {
9506
+ this.inMemoryForms.set(form.slug, form);
9507
+ try {
9508
+ if (this.engine.hasCollection("cms_forms")) {
9509
+ const existing = await this.engine.collection("cms_forms").findBySlug(form.slug);
9510
+ if (existing) await this.engine.collection("cms_forms").update(existing.id, {
9511
+ title: form.title,
9512
+ data: {
9513
+ description: form.description,
9514
+ fields: form.fields,
9515
+ successMessage: form.successMessage,
9516
+ redirectUrl: form.redirectUrl,
9517
+ notifyEmails: form.notifyEmails,
9518
+ isActive: form.isActive ?? true
9519
+ }
9520
+ });
9521
+ else await this.engine.collection("cms_forms").create({
9522
+ title: form.title,
9523
+ slug: form.slug,
9524
+ data: {
9525
+ description: form.description,
9526
+ fields: form.fields,
9527
+ successMessage: form.successMessage,
9528
+ redirectUrl: form.redirectUrl,
9529
+ notifyEmails: form.notifyEmails,
9530
+ isActive: form.isActive ?? true
9531
+ }
9532
+ });
9533
+ }
9534
+ } catch {}
9535
+ return form;
9536
+ }
9537
+ /**
9538
+ * Retrieve a form definition by its slug.
9539
+ */
9540
+ async getForm(slug) {
9541
+ if (this.inMemoryForms.has(slug)) return this.inMemoryForms.get(slug);
9542
+ try {
9543
+ if (this.engine.hasCollection("cms_forms")) {
9544
+ const item = await this.engine.collection("cms_forms").findBySlug(slug);
9545
+ if (item) {
9546
+ const form = {
9547
+ id: item.id,
9548
+ slug: item.slug,
9549
+ title: item.title || item.slug,
9550
+ description: item.data?.description,
9551
+ fields: item.data?.fields || [],
9552
+ successMessage: item.data?.successMessage,
9553
+ redirectUrl: item.data?.redirectUrl,
9554
+ notifyEmails: item.data?.notifyEmails,
9555
+ isActive: item.data?.isActive !== false
9556
+ };
9557
+ this.inMemoryForms.set(slug, form);
9558
+ return form;
9559
+ }
9560
+ }
9561
+ } catch {}
9562
+ return null;
9563
+ }
9564
+ /**
9565
+ * List all registered forms.
9566
+ */
9567
+ async listForms() {
9568
+ try {
9569
+ if (this.engine.hasCollection("cms_forms")) {
9570
+ const result = await this.engine.collection("cms_forms").find({ limit: 100 });
9571
+ for (const item of result.items) if (!this.inMemoryForms.has(item.slug)) this.inMemoryForms.set(item.slug, {
9572
+ id: item.id,
9573
+ slug: item.slug,
9574
+ title: item.title || item.slug,
9575
+ description: item.data?.description,
9576
+ fields: item.data?.fields || [],
9577
+ successMessage: item.data?.successMessage,
9578
+ redirectUrl: item.data?.redirectUrl,
9579
+ notifyEmails: item.data?.notifyEmails,
9580
+ isActive: item.data?.isActive !== false
9581
+ });
9582
+ }
9583
+ } catch {}
9584
+ return Array.from(this.inMemoryForms.values());
9585
+ }
9586
+ /**
9587
+ * Validate a submission payload against the form's field definitions.
9588
+ */
9589
+ validateSubmission(form, input) {
9590
+ const rawData = input.data || {};
9591
+ const errors = {};
9592
+ const sanitizedData = {};
9593
+ const hpValue = input.honeypotValue ?? rawData[this.honeypotField];
9594
+ if (this.options.enableAntiSpam !== false && hpValue && String(hpValue).trim() !== "") {
9595
+ errors["_spam"] = "Bot submission detected.";
9596
+ return {
9597
+ valid: false,
9598
+ errors,
9599
+ sanitizedData
9600
+ };
9601
+ }
9602
+ for (const rule of form.fields) {
9603
+ const val = rawData[rule.name];
9604
+ if (rule.required) {
9605
+ if (val === void 0 || val === null || typeof val === "string" && val.trim() === "") {
9606
+ errors[rule.name] = `${rule.label || rule.name} is required.`;
9607
+ continue;
9608
+ }
9609
+ }
9610
+ if (val === void 0 || val === null || val === "") {
9611
+ if (rule.defaultValue !== void 0) sanitizedData[rule.name] = rule.defaultValue;
9612
+ continue;
9613
+ }
9614
+ if (rule.type === "email") {
9615
+ if (typeof val !== "string" || !(0, _azlib_validator.isEmail)(val.trim())) {
9616
+ errors[rule.name] = `${rule.label || rule.name} must be a valid email address.`;
9617
+ continue;
9618
+ }
9619
+ sanitizedData[rule.name] = val.trim().toLowerCase();
9620
+ } else if (rule.type === "url") {
9621
+ if (typeof val !== "string" || !(0, _azlib_validator.isURL)(val.trim())) {
9622
+ errors[rule.name] = `${rule.label || rule.name} must be a valid URL.`;
9623
+ continue;
9624
+ }
9625
+ sanitizedData[rule.name] = val.trim();
9626
+ } else if (rule.type === "number") {
9627
+ const num = Number(val);
9628
+ if (isNaN(num)) {
9629
+ errors[rule.name] = `${rule.label || rule.name} must be a valid number.`;
9630
+ continue;
9631
+ }
9632
+ if (rule.min !== void 0 && num < rule.min) {
9633
+ errors[rule.name] = `${rule.label || rule.name} must be at least ${rule.min}.`;
9634
+ continue;
9635
+ }
9636
+ if (rule.max !== void 0 && num > rule.max) {
9637
+ errors[rule.name] = `${rule.label || rule.name} must be at most ${rule.max}.`;
9638
+ continue;
9639
+ }
9640
+ sanitizedData[rule.name] = num;
9641
+ } else if (rule.type === "checkbox") sanitizedData[rule.name] = Boolean(val);
9642
+ else {
9643
+ const strVal = String(val).trim();
9644
+ if (rule.minLength && strVal.length < rule.minLength) {
9645
+ errors[rule.name] = `${rule.label || rule.name} must be at least ${rule.minLength} characters.`;
9646
+ continue;
9647
+ }
9648
+ if (rule.maxLength && strVal.length > rule.maxLength) {
9649
+ errors[rule.name] = `${rule.label || rule.name} must be at most ${rule.maxLength} characters.`;
9650
+ continue;
9651
+ }
9652
+ if (rule.pattern) {
9653
+ if (!new RegExp(rule.pattern).test(strVal)) {
9654
+ errors[rule.name] = `${rule.label || rule.name} format is invalid.`;
9655
+ continue;
9656
+ }
9657
+ }
9658
+ sanitizedData[rule.name] = strVal;
9659
+ }
9660
+ }
9661
+ return {
9662
+ valid: Object.keys(errors).length === 0,
9663
+ errors,
9664
+ sanitizedData
9665
+ };
9666
+ }
9667
+ /**
9668
+ * Submit data to a form by its slug.
9669
+ */
9670
+ async submit(formSlug, input, clientMeta) {
9671
+ const form = await this.getForm(formSlug);
9672
+ if (!form) throw new Error(`[FormsService] Form '${formSlug}' not found.`);
9673
+ if (form.isActive === false) throw new Error(`[FormsService] Form '${formSlug}' is currently disabled.`);
9674
+ const validation = this.validateSubmission(form, input);
9675
+ if (!validation.valid) {
9676
+ const errorMsg = Object.values(validation.errors).join("; ");
9677
+ const err = /* @__PURE__ */ new Error(`Form validation failed: ${errorMsg}`);
9678
+ err.validationErrors = validation.errors;
9679
+ throw err;
9680
+ }
9681
+ const id = `sub_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
9682
+ const submission = {
9683
+ id,
9684
+ formSlug,
9685
+ data: validation.sanitizedData,
9686
+ ip: clientMeta?.ip,
9687
+ userAgent: clientMeta?.userAgent,
9688
+ submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
9689
+ status: "unread"
9690
+ };
9691
+ this.inMemorySubmissions.unshift(submission);
9692
+ try {
9693
+ if (this.engine.hasCollection("cms_form_submissions")) await this.engine.collection("cms_form_submissions").create({
9694
+ title: `Submission: ${formSlug} (${id})`,
9695
+ data: {
9696
+ formSlug,
9697
+ data: submission.data,
9698
+ ip: submission.ip,
9699
+ userAgent: submission.userAgent,
9700
+ status: submission.status
9701
+ }
9702
+ });
9703
+ } catch {}
9704
+ await this.engine.hooks.doAction("forms.submitted", {
9705
+ form,
9706
+ submission
9707
+ });
9708
+ return submission;
9709
+ }
9710
+ /**
9711
+ * List submissions for a specific form.
9712
+ */
9713
+ async listSubmissions(formSlug, options) {
9714
+ let list = this.inMemorySubmissions.filter((s) => s.formSlug === formSlug);
9715
+ if (options?.status) list = list.filter((s) => s.status === options.status);
9716
+ const total = list.length;
9717
+ const limit = options?.limit ?? 50;
9718
+ const offset = options?.offset ?? 0;
9719
+ const items = list.slice(offset, offset + limit);
9720
+ return {
9721
+ items,
9722
+ total,
9723
+ limit,
9724
+ offset,
9725
+ hasMore: offset + items.length < total
9726
+ };
9727
+ }
9728
+ /**
9729
+ * Update submission status (e.g. read, spam, archived).
9730
+ */
9731
+ async updateSubmissionStatus(id, status) {
9732
+ const found = this.inMemorySubmissions.find((s) => s.id === id);
9733
+ if (found) {
9734
+ found.status = status;
9735
+ return found;
9736
+ }
9737
+ return null;
9738
+ }
9739
+ };
9740
+ /**
9741
+ * Retrieve the active FormsService instance associated with a CMSEngine.
9742
+ */
9743
+ function getFormsService(engine, options) {
9744
+ if (engine.__formsService) return engine.__formsService;
9745
+ const service = new FormsService(engine, options);
9746
+ engine.__formsService = service;
9747
+ return service;
9748
+ }
9749
+ //#endregion
9750
+ //#region src/plugins/forms/routes.ts
9751
+ function json$1(data, status = 200) {
9752
+ return new Response(JSON.stringify(data), {
9753
+ status,
9754
+ headers: {
9755
+ "Content-Type": "application/json",
9756
+ "Access-Control-Allow-Origin": "*"
9757
+ }
9758
+ });
9759
+ }
9760
+ function registerFormsRoutes(ctx, service, options = {}) {
9761
+ const prefix = (options.apiPrefix ?? "/api/forms").replace(/\/+$/, "");
9762
+ ctx.registerRoute("GET", prefix, async () => {
9763
+ try {
9764
+ return json$1((await service.listForms()).map((f) => ({
9765
+ slug: f.slug,
9766
+ title: f.title,
9767
+ description: f.description,
9768
+ fields: f.fields,
9769
+ isActive: f.isActive
9770
+ })));
9771
+ } catch (err) {
9772
+ return json$1({ error: err.message || "Failed to list forms" }, 500);
9773
+ }
9774
+ });
9775
+ ctx.registerRoute("GET", `${prefix}/:slug`, async (_req, { params }) => {
9776
+ try {
9777
+ const form = await service.getForm(params.slug);
9778
+ if (!form) return json$1({ error: "Form not found" }, 404);
9779
+ return json$1({
9780
+ slug: form.slug,
9781
+ title: form.title,
9782
+ description: form.description,
9783
+ fields: form.fields,
9784
+ successMessage: form.successMessage,
9785
+ redirectUrl: form.redirectUrl,
9786
+ isActive: form.isActive
9787
+ });
9788
+ } catch (err) {
9789
+ return json$1({ error: err.message || "Failed to get form" }, 500);
9790
+ }
9791
+ });
9792
+ ctx.registerRoute("POST", `${prefix}/:slug/submit`, async (req, { params }) => {
9793
+ try {
9794
+ const body = await req.json();
9795
+ const ip = req.headers.get("x-forwarded-for") || void 0;
9796
+ const userAgent = req.headers.get("user-agent") || void 0;
9797
+ const input = body && typeof body.data === "object" ? body : {
9798
+ data: body,
9799
+ honeypotValue: body?._az_hp
9800
+ };
9801
+ const submission = await service.submit(params.slug, input, {
9802
+ ip,
9803
+ userAgent
9804
+ });
9805
+ const form = await service.getForm(params.slug);
9806
+ return json$1({
9807
+ success: true,
9808
+ message: form?.successMessage || "Form submitted successfully.",
9809
+ redirectUrl: form?.redirectUrl,
9810
+ submissionId: submission.id
9811
+ }, 201);
9812
+ } catch (err) {
9813
+ return json$1({
9814
+ error: err.message || "Form submission failed",
9815
+ validationErrors: err.validationErrors
9816
+ }, 400);
9817
+ }
9818
+ });
9819
+ ctx.registerRoute("GET", `${prefix}/:slug/submissions`, async (_req, { params, url }) => {
9820
+ try {
9821
+ const status = url.searchParams.get("status") || void 0;
9822
+ const limit = url.searchParams.get("limit") ? parseInt(url.searchParams.get("limit"), 10) : void 0;
9823
+ const offset = url.searchParams.get("offset") ? parseInt(url.searchParams.get("offset"), 10) : void 0;
9824
+ return json$1(await service.listSubmissions(params.slug, {
9825
+ status,
9826
+ limit,
9827
+ offset
9828
+ }));
9829
+ } catch (err) {
9830
+ return json$1({ error: err.message || "Failed to list submissions" }, 500);
9831
+ }
9832
+ });
9833
+ ctx.registerRoute("PUT", `${prefix}/submissions/:id/status`, async (req, { params }) => {
9834
+ try {
9835
+ const body = await req.json();
9836
+ const updated = await service.updateSubmissionStatus(params.id, body.status);
9837
+ if (!updated) return json$1({ error: "Submission not found" }, 404);
9838
+ return json$1(updated);
9839
+ } catch (err) {
9840
+ return json$1({ error: err.message || "Failed to update submission status" }, 400);
9841
+ }
9842
+ });
9843
+ }
9844
+ //#endregion
9845
+ //#region src/plugins/forms/schemas.ts
9846
+ /**
9847
+ * @azlib/cms - Built-in Forms & Submissions Declarative Content Schemas
9848
+ */
9849
+ /**
9850
+ * Forms definition collection schema.
9851
+ */
9852
+ function createFormsCollection(_options = {}) {
9853
+ return collection({
9854
+ label: "Forms",
9855
+ slug: "cms_forms",
9856
+ timestamps: true,
9857
+ revisions: false,
9858
+ fields: [
9859
+ fields.text({
9860
+ name: "title",
9861
+ label: "Form Title",
9862
+ required: true
9863
+ }),
9864
+ fields.slug({
9865
+ name: "slug",
9866
+ label: "Form Slug",
9867
+ from: "title",
9868
+ required: true,
9869
+ unique: true
9870
+ }),
9871
+ fields.text({
9872
+ name: "description",
9873
+ label: "Description"
9874
+ }),
9875
+ fields.json({
9876
+ name: "fields",
9877
+ label: "Form Field Rules",
9878
+ required: true
9879
+ }),
9880
+ fields.text({
9881
+ name: "successMessage",
9882
+ label: "Success Message",
9883
+ defaultValue: "Thank you! Your submission has been received."
9884
+ }),
9885
+ fields.url({
9886
+ name: "redirectUrl",
9887
+ label: "Redirect URL on Success"
9888
+ }),
9889
+ fields.array({
9890
+ name: "notifyEmails",
9891
+ label: "Notification Emails"
9892
+ }),
9893
+ fields.boolean({
9894
+ name: "isActive",
9895
+ label: "Active",
9896
+ defaultValue: true
9897
+ })
9898
+ ]
9899
+ });
9900
+ }
9901
+ /**
9902
+ * Form submissions collection schema.
9903
+ */
9904
+ function createSubmissionsCollection(_options = {}) {
9905
+ return collection({
9906
+ label: "Form Submissions",
9907
+ slug: "cms_form_submissions",
9908
+ timestamps: true,
9909
+ revisions: false,
9910
+ fields: [
9911
+ fields.text({
9912
+ name: "formSlug",
9913
+ label: "Form Slug",
9914
+ required: true
9915
+ }),
9916
+ fields.json({
9917
+ name: "data",
9918
+ label: "Submitted Fields",
9919
+ required: true
9920
+ }),
9921
+ fields.text({
9922
+ name: "ip",
9923
+ label: "IP Address"
9924
+ }),
9925
+ fields.text({
9926
+ name: "userAgent",
9927
+ label: "User Agent"
9928
+ }),
9929
+ fields.select({
9930
+ name: "status",
9931
+ label: "Submission Status",
9932
+ options: [
9933
+ "unread",
9934
+ "read",
9935
+ "spam",
9936
+ "archived"
9937
+ ],
9938
+ defaultValue: "unread"
9939
+ })
9940
+ ]
9941
+ });
9942
+ }
9943
+ //#endregion
9944
+ //#region src/plugins/forms/client.ts
9945
+ var FormsClient = class {
9946
+ client;
9947
+ options;
9948
+ service;
9949
+ prefix;
9950
+ constructor(client, options = {}) {
9951
+ this.client = client;
9952
+ this.options = options;
9953
+ this.prefix = (options.apiPrefix ?? "/api/forms").replace(/\/+$/, "");
9954
+ const engine = client.getEngine();
9955
+ if (engine) this.service = getFormsService(engine, options);
9956
+ }
9957
+ /**
9958
+ * List public forms.
9959
+ */
9960
+ async listForms() {
9961
+ if (this.service) return this.service.listForms();
9962
+ return this.client.request(this.prefix);
9963
+ }
9964
+ /**
9965
+ * Get public form schema definition by slug.
9966
+ */
9967
+ async getForm(slug) {
9968
+ if (this.service) return this.service.getForm(slug);
9969
+ return this.client.request(`${this.prefix}/${encodeURIComponent(slug)}`);
9970
+ }
9971
+ /**
9972
+ * Submit data to a form.
9973
+ */
9974
+ async submit(slug, data, honeypotValue) {
9975
+ if (this.service) {
9976
+ const submission = await this.service.submit(slug, {
9977
+ data,
9978
+ honeypotValue
9979
+ });
9980
+ const form = await this.service.getForm(slug);
9981
+ return {
9982
+ success: true,
9983
+ message: form?.successMessage || "Form submitted successfully.",
9984
+ submissionId: submission.id,
9985
+ redirectUrl: form?.redirectUrl
9986
+ };
9987
+ }
9988
+ return this.client.request(`${this.prefix}/${encodeURIComponent(slug)}/submit`, {
9989
+ method: "POST",
9990
+ body: JSON.stringify({
9991
+ data,
9992
+ honeypotValue
9993
+ })
9994
+ });
9995
+ }
9996
+ /**
9997
+ * List submissions for a form.
9998
+ */
9999
+ async listSubmissions(slug, options) {
10000
+ if (this.service) return this.service.listSubmissions(slug, options);
10001
+ const params = new URLSearchParams();
10002
+ if (options?.status) params.set("status", options.status);
10003
+ if (options?.limit !== void 0) params.set("limit", String(options.limit));
10004
+ if (options?.offset !== void 0) params.set("offset", String(options.offset));
10005
+ const q = params.toString() ? `?${params.toString()}` : "";
10006
+ return this.client.request(`${this.prefix}/${encodeURIComponent(slug)}/submissions${q}`);
10007
+ }
10008
+ };
10009
+ /**
10010
+ * Get or create a FormsClient adapter for a CMSClient.
10011
+ */
10012
+ function getFormsClient(client, options) {
10013
+ return new FormsClient(client, options);
10014
+ }
10015
+ //#endregion
10016
+ //#region src/plugins/forms/index.ts
10017
+ /**
10018
+ * @azlib/cms - Built-in Forms & Submissions Plugin
10019
+ */
10020
+ /**
10021
+ * Built-in Forms & Submissions plugin factory for @azlib/cms.
10022
+ * Equips the CMS engine with dynamic form creation, schema validation,
10023
+ * anti-spam honeypots, submission storage, and custom Web Standard REST routes.
10024
+ */
10025
+ const formsPlugin = definePlugin((options) => {
10026
+ const opts = options || {};
10027
+ return {
10028
+ name: "forms",
10029
+ version: "1.0.0",
10030
+ description: "Universal dynamic forms and submissions plugin with schema validation and honeypot anti-spam",
10031
+ collections: [createFormsCollection(opts), createSubmissionsCollection(opts)],
10032
+ setup(ctx) {
10033
+ const service = new FormsService(ctx.engine, opts);
10034
+ ctx.engine.__formsService = service;
10035
+ if (opts.enableRoutes !== false) registerFormsRoutes(ctx, service, opts);
10036
+ }
10037
+ };
10038
+ });
10039
+ //#endregion
10040
+ //#region src/plugins/revalidation/service.ts
10041
+ var RevalidationService = class {
10042
+ engine;
10043
+ options;
10044
+ endpoints = [];
10045
+ history = [];
10046
+ tagPrefix;
10047
+ fetchFn;
10048
+ constructor(engine, options = {}) {
10049
+ this.engine = engine;
10050
+ this.options = options;
10051
+ this.endpoints = options.endpoints ? [...options.endpoints] : [];
10052
+ this.tagPrefix = options.tagPrefix || "cms";
10053
+ this.fetchFn = globalThis.fetch?.bind(globalThis);
10054
+ if (options.enableAutoRevalidate !== false) this.attachHooks();
10055
+ }
10056
+ addEndpoint(endpoint) {
10057
+ this.endpoints.push(endpoint);
10058
+ }
10059
+ getEndpoints() {
10060
+ return this.endpoints;
10061
+ }
10062
+ getHistory() {
10063
+ return this.history;
10064
+ }
10065
+ /**
10066
+ * Derive cache tags for a content item.
10067
+ */
10068
+ deriveTags(collection, id, slug) {
10069
+ const tags = [`${this.tagPrefix}:${collection}`];
10070
+ if (id) tags.push(`${this.tagPrefix}:${collection}:${id}`);
10071
+ if (slug) tags.push(`${this.tagPrefix}:${collection}:${slug}`);
10072
+ return tags;
10073
+ }
10074
+ /**
10075
+ * Derive frontend URL paths affected by this content change.
10076
+ */
10077
+ derivePaths(item) {
10078
+ if (this.options.collectionPathMap?.[item.collection]) {
10079
+ const mapped = this.options.collectionPathMap[item.collection](item);
10080
+ return Array.isArray(mapped) ? mapped : [mapped];
10081
+ }
10082
+ if (item.collection === "pages") return item.slug === "home" || item.slug === "index" ? ["/"] : [`/${item.slug}`];
10083
+ return [`/${item.collection}/${item.slug}`, `/${item.collection}`];
10084
+ }
10085
+ /**
10086
+ * Dispatch revalidation to all configured endpoints.
10087
+ */
10088
+ async revalidate(input) {
10089
+ const paths = input.paths || [];
10090
+ const tags = input.tags || (input.collectionSlug ? this.deriveTags(input.collectionSlug, input.itemId) : []);
10091
+ const payload = {
10092
+ event: input.event || "manual",
10093
+ collectionSlug: input.collectionSlug,
10094
+ itemId: input.itemId,
10095
+ paths,
10096
+ tags,
10097
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
10098
+ };
10099
+ const results = [];
10100
+ for (const ep of this.endpoints) {
10101
+ const startTime = performance.now();
10102
+ try {
10103
+ const bodyStr = JSON.stringify(payload);
10104
+ const headers = {
10105
+ "Content-Type": "application/json",
10106
+ "X-Azlib-Timestamp": payload.timestamp,
10107
+ ...ep.headers || {}
10108
+ };
10109
+ if (ep.secret) headers["X-Azlib-Signature"] = await computeHmacSignature(bodyStr, ep.secret);
10110
+ let status = 200;
10111
+ let success = true;
10112
+ let errorMsg;
10113
+ if (this.fetchFn) try {
10114
+ const method = ep.method || "POST";
10115
+ let targetUrl = ep.url;
10116
+ if (method === "GET") {
10117
+ const url = new URL(ep.url);
10118
+ if (paths.length > 0) url.searchParams.set("paths", paths.join(","));
10119
+ if (tags.length > 0) url.searchParams.set("tags", tags.join(","));
10120
+ if (ep.secret) url.searchParams.set("secret", ep.secret);
10121
+ targetUrl = url.toString();
10122
+ }
10123
+ const response = await this.fetchFn(targetUrl, {
10124
+ method,
10125
+ headers,
10126
+ body: method === "POST" ? bodyStr : void 0
10127
+ });
10128
+ status = response.status;
10129
+ success = response.ok;
10130
+ if (!response.ok) errorMsg = `Endpoint returned status ${response.status}`;
10131
+ } catch (err) {
10132
+ status = "error";
10133
+ success = false;
10134
+ errorMsg = err.message || "Fetch failed";
10135
+ }
10136
+ const durationMs = Number((performance.now() - startTime).toFixed(2));
10137
+ const res = {
10138
+ endpoint: ep.url,
10139
+ status,
10140
+ success,
10141
+ durationMs,
10142
+ error: errorMsg
10143
+ };
10144
+ results.push(res);
10145
+ this.history.unshift(res);
10146
+ if (this.history.length > 500) this.history.pop();
10147
+ } catch (err) {
10148
+ const durationMs = Number((performance.now() - startTime).toFixed(2));
10149
+ const res = {
10150
+ endpoint: ep.url,
10151
+ status: "error",
10152
+ success: false,
10153
+ durationMs,
10154
+ error: err.message
10155
+ };
10156
+ results.push(res);
10157
+ this.history.unshift(res);
10158
+ }
10159
+ }
10160
+ await this.engine.hooks.doAction("revalidation.dispatched", {
10161
+ payload,
10162
+ results
10163
+ });
10164
+ return results;
10165
+ }
10166
+ attachHooks() {
10167
+ const handleContentEvent = async (event, item) => {
10168
+ if (item.collection.startsWith("cms_") || item.collection.startsWith("audit_")) return;
10169
+ const paths = this.derivePaths(item);
10170
+ const tags = this.deriveTags(item.collection, item.id, item.slug);
10171
+ await this.revalidate({
10172
+ event,
10173
+ collectionSlug: item.collection,
10174
+ itemId: item.id,
10175
+ paths,
10176
+ tags
10177
+ });
10178
+ };
10179
+ this.engine.hooks.addAction("cms.content_created", async (...args) => {
10180
+ const item = args[0];
10181
+ if (item) await handleContentEvent("create", item);
10182
+ });
10183
+ this.engine.hooks.addAction("cms.content_updated", async (...args) => {
10184
+ const item = args[0];
10185
+ if (item) await handleContentEvent("update", item);
10186
+ });
10187
+ this.engine.hooks.addAction("cms.content_published", async (...args) => {
10188
+ const item = args[0];
10189
+ if (item) await handleContentEvent("publish", item);
10190
+ });
10191
+ this.engine.hooks.addAction("cms.content_deleted", async (...args) => {
10192
+ const item = args[0];
10193
+ if (item) await handleContentEvent("delete", item);
10194
+ });
10195
+ }
10196
+ };
10197
+ /**
10198
+ * Retrieve the active RevalidationService instance associated with a CMSEngine.
10199
+ */
10200
+ function getRevalidationService(engine, options) {
10201
+ if (engine.__revalidationService) return engine.__revalidationService;
10202
+ const service = new RevalidationService(engine, options);
10203
+ engine.__revalidationService = service;
10204
+ return service;
10205
+ }
10206
+ //#endregion
10207
+ //#region src/plugins/revalidation/routes.ts
10208
+ function json(data, status = 200) {
10209
+ return new Response(JSON.stringify(data), {
10210
+ status,
10211
+ headers: {
10212
+ "Content-Type": "application/json",
10213
+ "Access-Control-Allow-Origin": "*"
10214
+ }
10215
+ });
10216
+ }
10217
+ function registerRevalidationRoutes(ctx, service, options = {}) {
10218
+ const prefix = (options.apiPrefix ?? "/api/revalidate").replace(/\/+$/, "");
10219
+ ctx.registerRoute("POST", prefix, async (req) => {
10220
+ try {
10221
+ const body = await req.json();
10222
+ const results = await service.revalidate(body);
10223
+ return json({
10224
+ success: results.every((r) => r.success),
10225
+ results
10226
+ });
10227
+ } catch (err) {
10228
+ return json({ error: err.message || "Failed to trigger revalidation" }, 500);
10229
+ }
10230
+ });
10231
+ ctx.registerRoute("GET", `${prefix}/history`, async () => {
10232
+ try {
10233
+ return json(service.getHistory());
10234
+ } catch (err) {
10235
+ return json({ error: err.message || "Failed to fetch revalidation history" }, 500);
10236
+ }
10237
+ });
10238
+ }
10239
+ //#endregion
10240
+ //#region src/plugins/revalidation/client.ts
10241
+ var RevalidationClient = class {
10242
+ client;
10243
+ options;
10244
+ service;
10245
+ prefix;
10246
+ constructor(client, options = {}) {
10247
+ this.client = client;
10248
+ this.options = options;
10249
+ this.prefix = (options.apiPrefix ?? "/api/revalidate").replace(/\/+$/, "");
10250
+ const engine = client.getEngine();
10251
+ if (engine) this.service = getRevalidationService(engine, options);
10252
+ }
10253
+ /**
10254
+ * Manually trigger cache invalidation and revalidation for paths or tags.
10255
+ */
10256
+ async revalidate(input) {
10257
+ if (this.service) {
10258
+ const results = await this.service.revalidate(input);
10259
+ return {
10260
+ success: results.every((r) => r.success),
10261
+ results
10262
+ };
10263
+ }
10264
+ return this.client.request(this.prefix, {
10265
+ method: "POST",
10266
+ body: JSON.stringify(input)
10267
+ });
10268
+ }
10269
+ /**
10270
+ * Fetch recent revalidation webhook delivery logs.
10271
+ */
10272
+ async getHistory() {
10273
+ if (this.service) return [...this.service.getHistory()];
10274
+ return this.client.request(`${this.prefix}/history`);
10275
+ }
10276
+ };
10277
+ /**
10278
+ * Get or create a RevalidationClient adapter for a CMSClient.
10279
+ */
10280
+ function getRevalidationClient(client, options) {
10281
+ return new RevalidationClient(client, options);
10282
+ }
10283
+ //#endregion
10284
+ //#region src/plugins/revalidation/index.ts
10285
+ /**
10286
+ * @azlib/cms - Built-in Frontend Cache & Revalidation Plugin
10287
+ */
10288
+ /**
10289
+ * Built-in Frontend Cache & Revalidation plugin factory for @azlib/cms.
10290
+ * Automatically invalidates edge caches and triggers on-demand revalidation
10291
+ * (Next.js, Vercel, Cloudflare, Astro) with HMAC-signed webhooks and manual APIs.
10292
+ */
10293
+ const revalidationPlugin = definePlugin((options) => {
10294
+ const opts = options || {};
10295
+ return {
10296
+ name: "revalidation",
10297
+ version: "1.0.0",
10298
+ description: "Universal frontend cache invalidation and ISR revalidation plugin with HMAC signatures",
10299
+ setup(ctx) {
10300
+ const service = new RevalidationService(ctx.engine, opts);
10301
+ ctx.engine.__revalidationService = service;
10302
+ if (opts.enableRoutes !== false) registerRevalidationRoutes(ctx, service, opts);
10303
+ }
10304
+ };
10305
+ });
10306
+ //#endregion
10307
+ exports.AuditLogClient = AuditLogClient;
10308
+ exports.AuditLogService = AuditLogService;
8165
10309
  exports.AuthService = AuthService;
8166
10310
  exports.CMSClient = CMSClient;
8167
10311
  exports.CMSEngine = CMSEngine;
@@ -8172,27 +10316,38 @@ exports.DEFAULT_ROLE_CAPABILITIES = DEFAULT_ROLE_CAPABILITIES;
8172
10316
  exports.DiskMediaStorageDriver = DiskMediaStorageDriver;
8173
10317
  exports.EcommerceClient = EcommerceClient;
8174
10318
  exports.EcommerceService = EcommerceService;
10319
+ exports.FormsClient = FormsClient;
10320
+ exports.FormsService = FormsService;
8175
10321
  exports.HRMSClient = HRMSClient;
8176
10322
  exports.HRMSService = HRMSService;
8177
10323
  exports.HRMS_EMPLOYEE_TRANSFER_PRESET = HRMS_EMPLOYEE_TRANSFER_PRESET;
8178
10324
  exports.HRMS_EMPLOYER_TRANSFER_PRESET = HRMS_EMPLOYER_TRANSFER_PRESET;
8179
10325
  exports.HooksManager = HooksManager;
8180
10326
  exports.MediaManager = MediaManager;
10327
+ exports.MemorySearchAdapter = MemorySearchAdapter;
8181
10328
  exports.MemoryStorageAdapter = MemoryStorageAdapter;
8182
10329
  exports.OptionsManager = OptionsManager;
8183
10330
  exports.PersistenceStorageAdapter = PersistenceStorageAdapter;
8184
10331
  exports.PreviewManager = PreviewManager;
8185
10332
  exports.RBACManager = RBACManager;
10333
+ exports.RevalidationClient = RevalidationClient;
10334
+ exports.RevalidationService = RevalidationService;
8186
10335
  exports.RevisionManager = RevisionManager;
10336
+ exports.SearchClient = SearchClient;
10337
+ exports.SearchService = SearchService;
10338
+ exports.SeoClient = SeoClient;
10339
+ exports.SeoService = SeoService;
8187
10340
  exports.TaxonomyManager = TaxonomyManager;
8188
10341
  exports.TransferClient = TransferClient;
8189
10342
  exports.TransferService = TransferService;
8190
10343
  exports.VALID_STATUS_TRANSITIONS = VALID_STATUS_TRANSITIONS;
8191
10344
  exports.WebhookManager = WebhookManager;
8192
10345
  exports.applyFieldTransform = applyFieldTransform;
10346
+ exports.auditLogPlugin = auditLogPlugin;
8193
10347
  exports.collection = collection;
8194
10348
  exports.computeHmacSignature = computeHmacSignature;
8195
10349
  exports.createAttendanceCollection = createAttendanceCollection;
10350
+ exports.createAuditLogCollection = createAuditLogCollection;
8196
10351
  exports.createCMSEngine = createCMSEngine;
8197
10352
  exports.createCMSRouter = createCMSRouter;
8198
10353
  exports.createCmsClient = createCmsClient;
@@ -8200,22 +10355,36 @@ exports.createDiscountCollection = createDiscountCollection;
8200
10355
  exports.createEcommerceTaxonomies = createEcommerceTaxonomies;
8201
10356
  exports.createEmployeeCollection = createEmployeeCollection;
8202
10357
  exports.createEmployerCollection = createEmployerCollection;
10358
+ exports.createFormsCollection = createFormsCollection;
8203
10359
  exports.createHRMSTaxonomies = createHRMSTaxonomies;
8204
10360
  exports.createLeaveRequestCollection = createLeaveRequestCollection;
8205
10361
  exports.createLeaveTypeCollection = createLeaveTypeCollection;
8206
10362
  exports.createOrderCollection = createOrderCollection;
8207
10363
  exports.createProductCollection = createProductCollection;
10364
+ exports.createSeoFieldDefinitions = createSeoFieldDefinitions;
10365
+ exports.createSubmissionsCollection = createSubmissionsCollection;
8208
10366
  exports.defaultHooks = defaultHooks;
8209
10367
  exports.defineConfig = defineConfig;
8210
10368
  exports.definePlugin = definePlugin;
8211
10369
  exports.detectFormat = detectFormat;
8212
10370
  exports.ecommercePlugin = ecommercePlugin;
8213
10371
  exports.fields = fields;
10372
+ exports.formsPlugin = formsPlugin;
8214
10373
  exports.generateSuggestedMapping = generateSuggestedMapping;
10374
+ exports.getAuditLogClient = getAuditLogClient;
10375
+ exports.getAuditLogService = getAuditLogService;
8215
10376
  exports.getEcommerceClient = getEcommerceClient;
8216
10377
  exports.getEcommerceService = getEcommerceService;
10378
+ exports.getFormsClient = getFormsClient;
10379
+ exports.getFormsService = getFormsService;
8217
10380
  exports.getHRMSClient = getHRMSClient;
8218
10381
  exports.getHRMSService = getHRMSService;
10382
+ exports.getRevalidationClient = getRevalidationClient;
10383
+ exports.getRevalidationService = getRevalidationService;
10384
+ exports.getSearchClient = getSearchClient;
10385
+ exports.getSearchService = getSearchService;
10386
+ exports.getSeoClient = getSeoClient;
10387
+ exports.getSeoService = getSeoService;
8219
10388
  exports.getTransferClient = getTransferClient;
8220
10389
  exports.getTransferService = getTransferService;
8221
10390
  exports.hashPassword = hashPassword;
@@ -8231,6 +10400,9 @@ exports.parseJsonSource = parseJsonSource;
8231
10400
  exports.parseSource = parseSource;
8232
10401
  exports.resolveLocalizedData = resolveLocalizedData;
8233
10402
  exports.resolveUniqueSlug = resolveUniqueSlug;
10403
+ exports.revalidationPlugin = revalidationPlugin;
10404
+ exports.searchPlugin = searchPlugin;
10405
+ exports.seoPlugin = seoPlugin;
8234
10406
  exports.serializeCsv = serializeCsv;
8235
10407
  exports.serializeExcel = serializeExcel;
8236
10408
  exports.serializeJson = serializeJson;