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