includio-cms 0.36.9 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/API.md +178 -3
  2. package/CHANGELOG.md +54 -0
  3. package/DOCS.md +28 -3
  4. package/ROADMAP.md +18 -0
  5. package/dist/admin/client/booking/booking-dashboard.svelte +40 -0
  6. package/dist/admin/client/booking/booking-dashboard.svelte.d.ts +3 -0
  7. package/dist/admin/client/booking/customer-panel.svelte +102 -0
  8. package/dist/admin/client/booking/customer-panel.svelte.d.ts +11 -0
  9. package/dist/admin/client/booking/customer-view.d.ts +52 -0
  10. package/dist/admin/client/booking/customer-view.js +134 -0
  11. package/dist/admin/client/booking/participant-data-panel.svelte +330 -0
  12. package/dist/admin/client/booking/participant-data-panel.svelte.d.ts +22 -0
  13. package/dist/admin/client/booking/reservation-detail-page.svelte +637 -0
  14. package/dist/admin/client/booking/reservation-detail-page.svelte.d.ts +3 -0
  15. package/dist/admin/client/booking/reservations-page.svelte +221 -0
  16. package/dist/admin/client/booking/reservations-page.svelte.d.ts +3 -0
  17. package/dist/admin/client/collection/collection-entries.svelte +1 -1
  18. package/dist/admin/client/collection/entry-link.svelte +6 -4
  19. package/dist/admin/client/collection/entry-link.svelte.d.ts +3 -1
  20. package/dist/admin/client/collection/status-badge.svelte +39 -3
  21. package/dist/admin/client/collection/status-badge.svelte.d.ts +5 -3
  22. package/dist/admin/client/entry/entry.svelte +26 -9
  23. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +73 -10
  24. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte.d.ts +6 -5
  25. package/dist/admin/client/entry/preview-views-context.svelte.d.ts +22 -0
  26. package/dist/admin/client/entry/preview-views-context.svelte.js +58 -0
  27. package/dist/admin/client/entry/preview-views.d.ts +27 -0
  28. package/dist/admin/client/entry/preview-views.js +33 -0
  29. package/dist/admin/client/index.d.ts +3 -0
  30. package/dist/admin/client/index.js +3 -0
  31. package/dist/admin/client/modules/dashboard-registry.d.ts +7 -0
  32. package/dist/admin/client/modules/dashboard-registry.js +11 -0
  33. package/dist/admin/client/modules/module-dashboard-page.svelte +26 -0
  34. package/dist/admin/client/modules/module-dashboard-page.svelte.d.ts +6 -0
  35. package/dist/admin/client/modules/stat-card.svelte +22 -0
  36. package/dist/admin/client/modules/stat-card.svelte.d.ts +10 -0
  37. package/dist/admin/client/shop/shop-dashboard.svelte +29 -0
  38. package/dist/admin/client/shop/shop-dashboard.svelte.d.ts +3 -0
  39. package/dist/admin/components/fields/relation-field.svelte +11 -2
  40. package/dist/admin/components/icons/icon-map.js +8 -0
  41. package/dist/admin/components/layout/app-sidebar.svelte +45 -14
  42. package/dist/admin/components/layout/context-switcher.svelte +90 -0
  43. package/dist/admin/components/layout/context-switcher.svelte.d.ts +3 -0
  44. package/dist/admin/components/layout/lang.d.ts +4 -0
  45. package/dist/admin/components/layout/lang.js +8 -0
  46. package/dist/admin/components/layout/layout-renderer.svelte +2 -1
  47. package/dist/admin/components/layout/layout-tabs.svelte +14 -0
  48. package/dist/admin/components/layout/layout-tabs.svelte.d.ts +2 -0
  49. package/dist/admin/components/layout/nav-collections.svelte +16 -9
  50. package/dist/admin/components/layout/nav-collections.svelte.d.ts +5 -1
  51. package/dist/admin/components/layout/nav-global.svelte +45 -0
  52. package/dist/admin/components/layout/nav-global.svelte.d.ts +3 -0
  53. package/dist/admin/components/layout/nav-main.svelte +8 -32
  54. package/dist/admin/components/layout/nav-main.svelte.d.ts +6 -1
  55. package/dist/admin/components/layout/nav-module.svelte +57 -0
  56. package/dist/admin/components/layout/nav-module.svelte.d.ts +7 -0
  57. package/dist/admin/components/layout/nav-section.svelte +1 -1
  58. package/dist/admin/components/layout/nav-section.svelte.d.ts +1 -1
  59. package/dist/admin/components/layout/nav-singletons.svelte +14 -7
  60. package/dist/admin/components/layout/nav-singletons.svelte.d.ts +5 -1
  61. package/dist/admin/remote/booking.remote.d.ts +244 -0
  62. package/dist/admin/remote/booking.remote.js +283 -0
  63. package/dist/admin/remote/index.d.ts +2 -0
  64. package/dist/admin/remote/index.js +2 -0
  65. package/dist/admin/remote/modules.remote.d.ts +10 -0
  66. package/dist/admin/remote/modules.remote.js +14 -0
  67. package/dist/admin/state/active-context-resolve.d.ts +37 -0
  68. package/dist/admin/state/active-context-resolve.js +66 -0
  69. package/dist/admin/state/active-context.svelte.d.ts +8 -0
  70. package/dist/admin/state/active-context.svelte.js +27 -0
  71. package/dist/booking/adjustments.d.ts +33 -0
  72. package/dist/booking/adjustments.js +26 -0
  73. package/dist/booking/capacity.d.ts +12 -0
  74. package/dist/booking/capacity.js +10 -0
  75. package/dist/booking/client/index.d.ts +39 -0
  76. package/dist/booking/client/index.js +44 -0
  77. package/dist/booking/client/use-booking.svelte.d.ts +25 -0
  78. package/dist/booking/client/use-booking.svelte.js +56 -0
  79. package/dist/booking/config.d.ts +156 -0
  80. package/dist/booking/config.js +36 -0
  81. package/dist/booking/draft.d.ts +77 -0
  82. package/dist/booking/draft.js +67 -0
  83. package/dist/booking/expiry-logic.d.ts +30 -0
  84. package/dist/booking/expiry-logic.js +33 -0
  85. package/dist/booking/index.d.ts +18 -0
  86. package/dist/booking/index.js +52 -0
  87. package/dist/booking/ledger.d.ts +46 -0
  88. package/dist/booking/ledger.js +60 -0
  89. package/dist/booking/number.d.ts +2 -0
  90. package/dist/booking/number.js +11 -0
  91. package/dist/booking/participant-fields/access.d.ts +10 -0
  92. package/dist/booking/participant-fields/access.js +14 -0
  93. package/dist/booking/participant-fields/age.d.ts +12 -0
  94. package/dist/booking/participant-fields/age.js +28 -0
  95. package/dist/booking/participant-fields/completeness.d.ts +8 -0
  96. package/dist/booking/participant-fields/completeness.js +13 -0
  97. package/dist/booking/participant-fields/group.d.ts +6 -0
  98. package/dist/booking/participant-fields/group.js +15 -0
  99. package/dist/booking/participant-fields/index.d.ts +6 -0
  100. package/dist/booking/participant-fields/index.js +6 -0
  101. package/dist/booking/participant-fields/types.d.ts +50 -0
  102. package/dist/booking/participant-fields/types.js +5 -0
  103. package/dist/booking/participant-fields/validate.d.ts +7 -0
  104. package/dist/booking/participant-fields/validate.js +44 -0
  105. package/dist/booking/participant-pricing.d.ts +93 -0
  106. package/dist/booking/participant-pricing.js +91 -0
  107. package/dist/booking/participants-view.d.ts +32 -0
  108. package/dist/booking/participants-view.js +22 -0
  109. package/dist/booking/payment-context.d.ts +19 -0
  110. package/dist/booking/payment-context.js +29 -0
  111. package/dist/booking/pricing-inputs.d.ts +28 -0
  112. package/dist/booking/pricing-inputs.js +35 -0
  113. package/dist/booking/pricing.d.ts +86 -0
  114. package/dist/booking/pricing.js +125 -0
  115. package/dist/booking/recompute.d.ts +22 -0
  116. package/dist/booking/recompute.js +31 -0
  117. package/dist/booking/reminders-schedule.d.ts +11 -0
  118. package/dist/booking/reminders-schedule.js +28 -0
  119. package/dist/booking/server/adjustments.d.ts +27 -0
  120. package/dist/booking/server/adjustments.js +136 -0
  121. package/dist/booking/server/bookings.d.ts +45 -0
  122. package/dist/booking/server/bookings.js +288 -0
  123. package/dist/booking/server/catalog.d.ts +9 -0
  124. package/dist/booking/server/catalog.js +20 -0
  125. package/dist/booking/server/db.d.ts +7 -0
  126. package/dist/booking/server/db.js +18 -0
  127. package/dist/booking/server/email.d.ts +23 -0
  128. package/dist/booking/server/email.js +59 -0
  129. package/dist/booking/server/emailTemplateRegistry.d.ts +29 -0
  130. package/dist/booking/server/emailTemplateRegistry.js +125 -0
  131. package/dist/booking/server/http/create-handler.d.ts +5 -0
  132. package/dist/booking/server/http/create-handler.js +68 -0
  133. package/dist/booking/server/http/index.d.ts +4 -0
  134. package/dist/booking/server/http/index.js +4 -0
  135. package/dist/booking/server/http/maintenance-handler.d.ts +9 -0
  136. package/dist/booking/server/http/maintenance-handler.js +36 -0
  137. package/dist/booking/server/http/portal-handler.d.ts +13 -0
  138. package/dist/booking/server/http/portal-handler.js +99 -0
  139. package/dist/booking/server/http/webhook-handler.d.ts +9 -0
  140. package/dist/booking/server/http/webhook-handler.js +39 -0
  141. package/dist/booking/server/index.d.ts +12 -0
  142. package/dist/booking/server/index.js +14 -0
  143. package/dist/booking/server/maintenance.d.ts +28 -0
  144. package/dist/booking/server/maintenance.js +134 -0
  145. package/dist/booking/server/participants.d.ts +26 -0
  146. package/dist/booking/server/participants.js +92 -0
  147. package/dist/booking/server/payments.d.ts +34 -0
  148. package/dist/booking/server/payments.js +207 -0
  149. package/dist/booking/server/portal-session.d.ts +16 -0
  150. package/dist/booking/server/portal-session.js +42 -0
  151. package/dist/booking/server/portal.d.ts +207 -0
  152. package/dist/booking/server/portal.js +234 -0
  153. package/dist/booking/server/refunds.d.ts +7 -0
  154. package/dist/booking/server/refunds.js +58 -0
  155. package/dist/booking/server/reminders.d.ts +9 -0
  156. package/dist/booking/server/reminders.js +24 -0
  157. package/dist/booking/server/validation.d.ts +17 -0
  158. package/dist/booking/server/validation.js +55 -0
  159. package/dist/booking/state-machine.d.ts +41 -0
  160. package/dist/booking/state-machine.js +73 -0
  161. package/dist/booking/templates/_partials/footer.en.html +4 -0
  162. package/dist/booking/templates/_partials/footer.pl.html +4 -0
  163. package/dist/booking/templates/_partials/header.en.html +4 -0
  164. package/dist/booking/templates/_partials/header.pl.html +4 -0
  165. package/dist/booking/templates/balance-due.en.html +10 -0
  166. package/dist/booking/templates/balance-due.pl.html +10 -0
  167. package/dist/booking/templates/balance-overdue-cancelled.en.html +5 -0
  168. package/dist/booking/templates/balance-overdue-cancelled.pl.html +5 -0
  169. package/dist/booking/templates/booking-received.en.html +9 -0
  170. package/dist/booking/templates/booking-received.pl.html +9 -0
  171. package/dist/booking/templates/deposit-paid.en.html +6 -0
  172. package/dist/booking/templates/deposit-paid.pl.html +6 -0
  173. package/dist/booking/templates/payment-link.en.html +5 -0
  174. package/dist/booking/templates/payment-link.pl.html +5 -0
  175. package/dist/booking/templates/payment-received.en.html +10 -0
  176. package/dist/booking/templates/payment-received.pl.html +10 -0
  177. package/dist/booking/totals.d.ts +38 -0
  178. package/dist/booking/totals.js +61 -0
  179. package/dist/booking/webhook-logic.d.ts +28 -0
  180. package/dist/booking/webhook-logic.js +50 -0
  181. package/dist/cli/create-user.d.ts +7 -1
  182. package/dist/cli/create-user.js +23 -15
  183. package/dist/cli/index.js +55 -7
  184. package/dist/cli/scaffold/admin.d.ts +6 -0
  185. package/dist/cli/scaffold/admin.js +5 -3
  186. package/dist/cli/scaffold/booking.d.ts +12 -0
  187. package/dist/cli/scaffold/booking.js +103 -0
  188. package/dist/core/cms.d.ts +24 -9
  189. package/dist/core/cms.js +72 -34
  190. package/dist/core/config/validatePreviews.d.ts +12 -0
  191. package/dist/core/config/validatePreviews.js +90 -0
  192. package/dist/core/fields/slugPath.d.ts +9 -1
  193. package/dist/core/fields/slugPath.js +16 -2
  194. package/dist/core/server/fields/resolveRelationFields.d.ts +11 -2
  195. package/dist/core/server/fields/resolveRelationFields.js +12 -3
  196. package/dist/core/server/generator/generator.js +7 -2
  197. package/dist/db-postgres/schema/booking/bookingAdjustments.d.ts +186 -0
  198. package/dist/db-postgres/schema/booking/bookingAdjustments.js +22 -0
  199. package/dist/db-postgres/schema/booking/bookingItemPersons.d.ts +117 -0
  200. package/dist/db-postgres/schema/booking/bookingItemPersons.js +22 -0
  201. package/dist/db-postgres/schema/booking/bookingItems.d.ts +181 -0
  202. package/dist/db-postgres/schema/booking/bookingItems.js +22 -0
  203. package/dist/db-postgres/schema/booking/bookingPayments.d.ts +204 -0
  204. package/dist/db-postgres/schema/booking/bookingPayments.js +25 -0
  205. package/dist/db-postgres/schema/booking/bookingPersons.d.ts +99 -0
  206. package/dist/db-postgres/schema/booking/bookingPersons.js +17 -0
  207. package/dist/db-postgres/schema/booking/bookingReminders.d.ts +131 -0
  208. package/dist/db-postgres/schema/booking/bookingReminders.js +18 -0
  209. package/dist/db-postgres/schema/booking/bookings.d.ts +307 -0
  210. package/dist/db-postgres/schema/booking/bookings.js +40 -0
  211. package/dist/db-postgres/schema/booking/index.d.ts +7 -0
  212. package/dist/db-postgres/schema/booking/index.js +7 -0
  213. package/dist/db-postgres/schema/index.d.ts +1 -0
  214. package/dist/db-postgres/schema/index.js +1 -0
  215. package/dist/db-postgres/schema-booking.d.ts +1 -0
  216. package/dist/db-postgres/schema-booking.js +1 -0
  217. package/dist/server/security/rate-limit-handler.d.ts +19 -0
  218. package/dist/server/security/rate-limit-handler.js +34 -0
  219. package/dist/shop/adapters/payu/client.js +3 -1
  220. package/dist/shop/adapters/payu/index.js +37 -17
  221. package/dist/shop/adapters/payu/payload.d.ts +6 -0
  222. package/dist/shop/adapters/payu/payload.js +1 -1
  223. package/dist/shop/adapters/stripe/index.js +36 -21
  224. package/dist/shop/index.d.ts +11 -2
  225. package/dist/shop/index.js +43 -1
  226. package/dist/shop/rate-limit.d.ts +1 -9
  227. package/dist/shop/rate-limit.js +3 -28
  228. package/dist/shop/types.d.ts +19 -0
  229. package/dist/sveltekit/config.d.ts +4 -0
  230. package/dist/sveltekit/config.js +6 -0
  231. package/dist/types/cms.d.ts +15 -1
  232. package/dist/types/cms.schema.d.ts +96 -2
  233. package/dist/types/cms.schema.js +84 -1
  234. package/dist/types/config.d.ts +33 -0
  235. package/dist/types/layout.d.ts +6 -0
  236. package/dist/types/modules.d.ts +133 -0
  237. package/dist/types/modules.js +27 -0
  238. package/dist/updates/0.37.0/index.d.ts +2 -0
  239. package/dist/updates/0.37.0/index.js +52 -0
  240. package/dist/updates/index.js +3 -1
  241. package/package.json +19 -1
  242. package/dist/admin/components/layout/nav-shop.svelte +0 -33
  243. package/dist/admin/components/layout/nav-shop.svelte.d.ts +0 -3
@@ -19,31 +19,51 @@ export function payuAdapter(opts) {
19
19
  id,
20
20
  label: opts.label ?? DEFAULT_LABEL,
21
21
  async createPayment(order, ctx) {
22
- const shop = requireShopConfig();
23
- const continueTemplate = opts.continueUrl ?? shop.orderViewUrl;
24
- if (!/^https?:\/\//i.test(continueTemplate)) {
25
- throw new Error('payuAdapter: continueUrl (or shop.orderViewUrl) must be an absolute URL.');
22
+ let continueUrl;
23
+ const buyer = {};
24
+ if (ctx?.continueUrl) {
25
+ // Non-shop caller (e.g. booking) supplied everything skip shop hydration.
26
+ if (!/^https?:\/\//i.test(ctx.continueUrl)) {
27
+ throw new Error('payuAdapter: ctx.continueUrl must be an absolute URL.');
28
+ }
29
+ continueUrl = ctx.continueUrl;
30
+ buyer.firstName = ctx.buyer?.firstName;
31
+ buyer.lastName = ctx.buyer?.lastName;
32
+ buyer.phone = ctx.buyer?.phone;
33
+ buyer.language = ctx.language ?? undefined;
34
+ }
35
+ else {
36
+ // Shop path: hydrate buyer + continueUrl from the stored shop order.
37
+ const shop = requireShopConfig();
38
+ const continueTemplate = opts.continueUrl ?? shop.orderViewUrl;
39
+ if (!/^https?:\/\//i.test(continueTemplate)) {
40
+ throw new Error('payuAdapter: continueUrl (or shop.orderViewUrl) must be an absolute URL.');
41
+ }
42
+ const full = await getOrderById(order.id);
43
+ const names = splitFullName(full?.customerName);
44
+ continueUrl = buildOrderViewUrl(continueTemplate, {
45
+ orderNumber: order.number,
46
+ orderId: order.id,
47
+ accessToken: full?.accessToken ?? '',
48
+ language: ctx?.language ?? full?.language ?? null
49
+ });
50
+ buyer.firstName = names.firstName;
51
+ buyer.lastName = names.lastName;
52
+ buyer.phone = full?.customerPhone ?? undefined;
53
+ buyer.language = ctx?.language ?? full?.language ?? undefined;
26
54
  }
27
- // Hydrate buyer details from stored order (customerName/phone not on OrderRef)
28
- const full = await getOrderById(order.id);
29
- const names = splitFullName(full?.customerName);
30
- const continueUrl = buildOrderViewUrl(continueTemplate, {
31
- orderNumber: order.number,
32
- orderId: order.id,
33
- accessToken: full?.accessToken ?? '',
34
- language: ctx?.language ?? full?.language ?? null
35
- });
36
55
  const payload = buildPayuOrderPayload({
37
56
  order,
38
57
  posId: opts.posId,
39
58
  notifyUrl: opts.notifyUrl,
40
59
  continueUrl,
41
60
  customerIp: ctx?.customerIp ?? '127.0.0.1',
61
+ externalId: ctx?.externalId,
42
62
  buyer: {
43
- firstName: names.firstName,
44
- lastName: names.lastName,
45
- phone: full?.customerPhone ?? undefined,
46
- language: ctx?.language ?? full?.language ?? undefined
63
+ firstName: buyer.firstName,
64
+ lastName: buyer.lastName,
65
+ phone: buyer.phone,
66
+ language: buyer.language
47
67
  }
48
68
  });
49
69
  const { redirectUri, orderId } = await client.createOrder(payload);
@@ -32,6 +32,12 @@ export interface BuildPayuPayloadInput {
32
32
  buyer?: Omit<PayuBuyer, 'email'> & {
33
33
  email?: string;
34
34
  };
35
+ /**
36
+ * Unique `extOrderId` for this attempt. PayU requires it unique per POS, so a
37
+ * single order/booking paying twice (deposit + balance) must vary it. Falls
38
+ * back to `order.number` when omitted.
39
+ */
40
+ externalId?: string;
35
41
  }
36
42
  /**
37
43
  * Split a single "Imię Nazwisko" string into first/last name heuristically.
@@ -35,7 +35,7 @@ export function buildPayuOrderPayload(input) {
35
35
  description: input.description ?? `Order ${order.number}`,
36
36
  currencyCode: order.currency,
37
37
  totalAmount: String(order.totalGross),
38
- extOrderId: order.number,
38
+ extOrderId: input.externalId ?? order.number,
39
39
  buyer,
40
40
  products: [
41
41
  {
@@ -63,29 +63,44 @@ export function stripeAdapter(opts) {
63
63
  id,
64
64
  label: opts.label ?? DEFAULT_LABEL,
65
65
  async createPayment(order, ctx) {
66
- const shop = requireShopConfig();
67
- const successTemplate = opts.successUrl ?? shop.orderViewUrl;
68
- const cancelTemplate = opts.cancelUrl ?? successTemplate;
69
- if (!/^https?:\/\//i.test(successTemplate)) {
70
- throw new Error('stripeAdapter: successUrl (or shop.orderViewUrl) must be an absolute URL.');
66
+ let successUrl;
67
+ let cancelUrl;
68
+ let language;
69
+ if (ctx?.continueUrl) {
70
+ // Non-shop caller (e.g. booking) supplied a fully-resolved return URL
71
+ // use it verbatim and skip any shop-order hydration (mirrors payuAdapter).
72
+ if (!/^https?:\/\//i.test(ctx.continueUrl)) {
73
+ throw new Error('stripeAdapter: ctx.continueUrl must be an absolute URL.');
74
+ }
75
+ successUrl = ctx.continueUrl;
76
+ cancelUrl = ctx.continueUrl;
77
+ language = ctx.language ?? null;
71
78
  }
72
- if (!/^https?:\/\//i.test(cancelTemplate)) {
73
- throw new Error('stripeAdapter: cancelUrl must be an absolute URL.');
79
+ else {
80
+ const shop = requireShopConfig();
81
+ const successTemplate = opts.successUrl ?? shop.orderViewUrl;
82
+ const cancelTemplate = opts.cancelUrl ?? successTemplate;
83
+ if (!/^https?:\/\//i.test(successTemplate)) {
84
+ throw new Error('stripeAdapter: successUrl (or shop.orderViewUrl) must be an absolute URL.');
85
+ }
86
+ if (!/^https?:\/\//i.test(cancelTemplate)) {
87
+ throw new Error('stripeAdapter: cancelUrl must be an absolute URL.');
88
+ }
89
+ const full = await getOrderById(order.id);
90
+ language = ctx?.language ?? full?.language ?? null;
91
+ successUrl = buildOrderViewUrl(successTemplate, {
92
+ orderNumber: order.number,
93
+ orderId: order.id,
94
+ accessToken: full?.accessToken ?? '',
95
+ language
96
+ });
97
+ cancelUrl = buildOrderViewUrl(cancelTemplate, {
98
+ orderNumber: order.number,
99
+ orderId: order.id,
100
+ accessToken: full?.accessToken ?? '',
101
+ language
102
+ });
74
103
  }
75
- const full = await getOrderById(order.id);
76
- const language = ctx?.language ?? full?.language ?? null;
77
- const successUrl = buildOrderViewUrl(successTemplate, {
78
- orderNumber: order.number,
79
- orderId: order.id,
80
- accessToken: full?.accessToken ?? '',
81
- language
82
- });
83
- const cancelUrl = buildOrderViewUrl(cancelTemplate, {
84
- orderNumber: order.number,
85
- orderId: order.id,
86
- accessToken: full?.accessToken ?? '',
87
- language
88
- });
89
104
  const productNameTpl = pickI18n(opts.productName ?? DEFAULT_PRODUCT_NAME, language);
90
105
  const productName = productNameTpl.replace('{number}', order.number);
91
106
  const payload = buildStripeCheckoutPayload({
@@ -1,5 +1,14 @@
1
- import type { ShopConfig, ResolvedShopConfig } from './types.js';
2
- export declare function defineShop(config: ShopConfig): ResolvedShopConfig;
1
+ import type { ShopConfig } from './types.js';
2
+ import type { CMSModule } from '../types/modules.js';
3
+ /**
4
+ * Defines the shop module. Returns a {@link CMSModule} for
5
+ * `defineConfig({ modules: [defineShop({...})] })`. The resolved shop config
6
+ * is carried on `module.config` and reachable via `getCMS().getModuleConfig('shop')`
7
+ * (or the back-compat `getCMS().shopConfig`).
8
+ *
9
+ * @public
10
+ */
11
+ export declare function defineShop(config: ShopConfig): CMSModule;
3
12
  export { InvalidVariantAttributesError } from './variant-attributes.js';
4
13
  export { isVariantExpired, filterUpcoming, VariantExpiredError } from './expiry.js';
5
14
  export { manualAdapter } from './adapters/manual/index.js';
@@ -1,4 +1,5 @@
1
- export function defineShop(config) {
1
+ /** Resolves a raw {@link ShopConfig} into a fully-defaulted {@link ResolvedShopConfig}. */
2
+ function resolveShopConfig(config) {
2
3
  return {
3
4
  ...config,
4
5
  features: {
@@ -22,6 +23,47 @@ export function defineShop(config) {
22
23
  cartName: config.cartName ?? null
23
24
  };
24
25
  }
26
+ /** Server-only init for the shop module — applies GIN indexes + smoke-tests email templates. */
27
+ async function shopModuleInit({ drizzle, config, defaultLanguage }) {
28
+ const shop = config;
29
+ if (drizzle) {
30
+ const { applyVariantAttributeIndexes } = await import('./server/init.js');
31
+ await applyVariantAttributeIndexes(shop, drizzle);
32
+ }
33
+ // Smoke-test built-in shop email templates — fast lookup-only sweep. Catches
34
+ // corrupt installs (missing dist files) at boot instead of at first send.
35
+ const { REQUIRED_TEMPLATE_NAMES } = await import('./server/email.js');
36
+ const { validateBuiltinTemplates } = await import('./server/emailTemplateRegistry.js');
37
+ validateBuiltinTemplates([...REQUIRED_TEMPLATE_NAMES], defaultLanguage);
38
+ }
39
+ /**
40
+ * Defines the shop module. Returns a {@link CMSModule} for
41
+ * `defineConfig({ modules: [defineShop({...})] })`. The resolved shop config
42
+ * is carried on `module.config` and reachable via `getCMS().getModuleConfig('shop')`
43
+ * (or the back-compat `getCMS().shopConfig`).
44
+ *
45
+ * @public
46
+ */
47
+ export function defineShop(config) {
48
+ const resolved = resolveShopConfig(config);
49
+ return {
50
+ id: 'shop',
51
+ label: { pl: 'Sklep', en: 'Shop' },
52
+ icon: 'building-store',
53
+ context: {
54
+ contextLabel: { pl: 'Sklep', en: 'Shop' },
55
+ landingUrl: '/admin/shop',
56
+ sections: [
57
+ { key: 'products', title: { pl: 'Produkty', en: 'Products' }, url: '/admin/shop/products', icon: 'package' },
58
+ { key: 'orders', title: { pl: 'Zamówienia', en: 'Orders' }, url: '/admin/shop/orders', icon: 'shopping-cart' },
59
+ { key: 'shipping', title: { pl: 'Metody wysyłki', en: 'Shipping methods' }, url: '/admin/shop/shipping-methods', icon: 'truck' },
60
+ { key: 'coupons', title: { pl: 'Kody rabatowe', en: 'Coupon codes' }, url: '/admin/shop/coupons', icon: 'ticket' }
61
+ ]
62
+ },
63
+ config: resolved,
64
+ serverHooks: { init: shopModuleInit }
65
+ };
66
+ }
25
67
  export { InvalidVariantAttributesError } from './variant-attributes.js';
26
68
  export { isVariantExpired, filterUpcoming, VariantExpiredError } from './expiry.js';
27
69
  export { manualAdapter } from './adapters/manual/index.js';
@@ -1,9 +1 @@
1
- import type { RateLimitRule } from './types.js';
2
- export interface RateLimitResult {
3
- allowed: boolean;
4
- remaining: number;
5
- resetAt: number;
6
- }
7
- export declare function checkRateLimit(key: string, rule: RateLimitRule): RateLimitResult;
8
- export declare function resetRateLimit(key?: string): void;
9
- export declare function clientKey(req: Request, prefix: string): string;
1
+ export { checkRateLimit, resetRateLimit, clientKey, type RateLimitResult } from '../server/security/rate-limit-handler.js';
@@ -1,28 +1,3 @@
1
- const buckets = new Map();
2
- export function checkRateLimit(key, rule) {
3
- const now = Date.now();
4
- const windowMs = rule.windowSec * 1000;
5
- const bucket = buckets.get(key);
6
- if (!bucket || bucket.resetAt <= now) {
7
- const fresh = { count: 1, resetAt: now + windowMs };
8
- buckets.set(key, fresh);
9
- return { allowed: true, remaining: rule.limit - 1, resetAt: fresh.resetAt };
10
- }
11
- if (bucket.count >= rule.limit) {
12
- return { allowed: false, remaining: 0, resetAt: bucket.resetAt };
13
- }
14
- bucket.count += 1;
15
- return { allowed: true, remaining: rule.limit - bucket.count, resetAt: bucket.resetAt };
16
- }
17
- export function resetRateLimit(key) {
18
- if (key)
19
- buckets.delete(key);
20
- else
21
- buckets.clear();
22
- }
23
- export function clientKey(req, prefix) {
24
- const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
25
- req.headers.get('x-real-ip') ||
26
- 'unknown';
27
- return `${prefix}:${ip}`;
28
- }
1
+ // Lifted to core (`$lib/server/security/rate-limit-handler`) so shop + booking
2
+ // share one generic per-handler limiter without a booking→shop dependency.
3
+ export { checkRateLimit, resetRateLimit, clientKey } from '../server/security/rate-limit-handler.js';
@@ -70,6 +70,25 @@ export interface ConsentConfig {
70
70
  export interface PaymentCreateContext {
71
71
  customerIp?: string;
72
72
  language?: string | null;
73
+ /**
74
+ * Fully-resolved absolute return URL. When set, adapters MUST use it verbatim
75
+ * and skip any shop-order hydration — lets non-shop callers (e.g. booking)
76
+ * reuse the adapter without a shop module.
77
+ */
78
+ continueUrl?: string;
79
+ /** Caller-supplied buyer identity (for non-shop callers). */
80
+ buyer?: {
81
+ firstName?: string;
82
+ lastName?: string;
83
+ phone?: string;
84
+ };
85
+ /**
86
+ * Unique external id for THIS payment attempt. Adapters whose provider requires
87
+ * a unique-per-attempt order id (e.g. PayU's `extOrderId`, unique per POS) use
88
+ * it instead of `order.number`. Required when one order/booking spawns multiple
89
+ * provider orders (deposit + balance), otherwise the provider rejects the reuse.
90
+ */
91
+ externalId?: string;
73
92
  }
74
93
  /**
75
94
  * Result of a non-invasive integration connectivity check. Returned by an
@@ -19,6 +19,10 @@ type SingleInput = Omit<SingleConfig, 'fields'> & {
19
19
  * (extends `CmsError`) listing every issue with a path and a fix hint, e.g.
20
20
  * `languages[0].code: must be a 2-letter ISO code (e.g. 'en')`
21
21
  *
22
+ * Also checks preview views: unique names, at most one `default`, `previewUrl`
23
+ * and `previews` are mutually exclusive, and every `tab.preview` points at a
24
+ * declared view.
25
+ *
22
26
  * @param config - The CMSConfig (collections, singles, forms, adapters, languages, ...).
23
27
  * @returns The same config, parsed and frozen against typos.
24
28
  * @throws {ConfigValidationError} when validation fails — message aggregates all issues.
@@ -1,5 +1,6 @@
1
1
  import { cmsConfigSchema } from '../types/cms.schema.js';
2
2
  import { formatConfigError } from '../core/errors.js';
3
+ import { validatePreviews } from '../core/config/validatePreviews.js';
3
4
  /**
4
5
  * Defines the root CMS configuration. Pass to `includioCMS()` in `hooks.server.ts`.
5
6
  *
@@ -7,6 +8,10 @@ import { formatConfigError } from '../core/errors.js';
7
8
  * (extends `CmsError`) listing every issue with a path and a fix hint, e.g.
8
9
  * `languages[0].code: must be a 2-letter ISO code (e.g. 'en')`
9
10
  *
11
+ * Also checks preview views: unique names, at most one `default`, `previewUrl`
12
+ * and `previews` are mutually exclusive, and every `tab.preview` points at a
13
+ * declared view.
14
+ *
10
15
  * @param config - The CMSConfig (collections, singles, forms, adapters, languages, ...).
11
16
  * @returns The same config, parsed and frozen against typos.
12
17
  * @throws {ConfigValidationError} when validation fails — message aggregates all issues.
@@ -26,6 +31,7 @@ export function defineConfig(config) {
26
31
  if (!result.success) {
27
32
  throw formatConfigError(result.error);
28
33
  }
34
+ validatePreviews(config);
29
35
  return config;
30
36
  }
31
37
  /**
@@ -9,6 +9,7 @@ import type { AIAdapter } from './adapters/ai.js';
9
9
  import type { EmailAdapter } from './adapters/email.js';
10
10
  import type { ResolvedShopConfig } from '../shop/types.js';
11
11
  import type { ResolvedCmpConfig } from '../cmp/types.js';
12
+ import type { CMSModule } from './modules.js';
12
13
  export interface VideoTranscodeConfig {
13
14
  /** Enable/disable auto-transcoding on upload (default: true) */
14
15
  transcode?: boolean;
@@ -157,7 +158,12 @@ export interface CMSConfig {
157
158
  apiKeys?: ApiKeyConfig[];
158
159
  typography?: TypographyConfig;
159
160
  sidebarHelp?: boolean;
160
- shop?: ResolvedShopConfig;
161
+ /**
162
+ * Optional modules ("overlays") layered on the CMS core — shop, booking,
163
+ * etc. Each is a {@link CMSModule} (typically from `defineShop()` /
164
+ * `defineBooking()`). Renders a context in the admin switcher.
165
+ */
166
+ modules?: CMSModule[];
161
167
  cmp?: ResolvedCmpConfig;
162
168
  admin?: AdminConfig;
163
169
  }
@@ -180,4 +186,12 @@ export interface ICMS {
180
186
  shopConfig: ResolvedShopConfig | null;
181
187
  cmpConfig: ResolvedCmpConfig | null;
182
188
  adminConfig: AdminConfig | null;
189
+ /** Registered modules ("overlays"), keyed by `module.id`. */
190
+ modules: Map<string, CMSModule>;
191
+ /** Returns a registered module by id, or `undefined`. */
192
+ getModule(id: string): CMSModule | undefined;
193
+ /** Returns a registered module's `config`, narrowed to `T`, or `undefined`. */
194
+ getModuleConfig<T = unknown>(id: string): T | undefined;
195
+ /** Whether a module with the given id is registered. */
196
+ hasModule(id: string): boolean;
183
197
  }
@@ -34,6 +34,12 @@ export declare const cmsConfigSchema: z.ZodObject<{
34
34
  slug: z.ZodString;
35
35
  sidebarIcon: z.ZodOptional<z.ZodString>;
36
36
  previewUrl: z.ZodOptional<z.ZodString>;
37
+ previews: z.ZodOptional<z.ZodArray<z.ZodObject<{
38
+ name: z.ZodString;
39
+ label: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
40
+ url: z.ZodString;
41
+ default: z.ZodOptional<z.ZodBoolean>;
42
+ }, z.core.$loose>>>;
37
43
  fields: z.ZodArray<z.ZodType<FieldDef, unknown, z.core.$ZodTypeInternals<FieldDef, unknown>>>;
38
44
  layout: z.ZodOptional<z.ZodUnknown>;
39
45
  slugField: z.ZodOptional<z.ZodString>;
@@ -44,6 +50,12 @@ export declare const cmsConfigSchema: z.ZodObject<{
44
50
  slug: z.ZodString;
45
51
  sidebarIcon: z.ZodOptional<z.ZodString>;
46
52
  previewUrl: z.ZodOptional<z.ZodString>;
53
+ previews: z.ZodOptional<z.ZodArray<z.ZodObject<{
54
+ name: z.ZodString;
55
+ label: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
56
+ url: z.ZodString;
57
+ default: z.ZodOptional<z.ZodBoolean>;
58
+ }, z.core.$loose>>>;
47
59
  fields: z.ZodArray<z.ZodType<FieldDef, unknown, z.core.$ZodTypeInternals<FieldDef, unknown>>>;
48
60
  layout: z.ZodOptional<z.ZodUnknown>;
49
61
  slugField: z.ZodOptional<z.ZodString>;
@@ -145,7 +157,26 @@ export declare const cmsConfigSchema: z.ZodObject<{
145
157
  fixOrphans: z.ZodOptional<z.ZodBoolean>;
146
158
  }, z.core.$loose>>;
147
159
  sidebarHelp: z.ZodOptional<z.ZodBoolean>;
148
- shop: z.ZodOptional<z.ZodUnknown>;
160
+ modules: z.ZodOptional<z.ZodArray<z.ZodObject<{
161
+ id: z.ZodString;
162
+ label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
163
+ icon: z.ZodString;
164
+ context: z.ZodObject<{
165
+ contextLabel: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
166
+ landingUrl: z.ZodString;
167
+ sections: z.ZodArray<z.ZodObject<{
168
+ key: z.ZodString;
169
+ title: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
170
+ url: z.ZodString;
171
+ icon: z.ZodString;
172
+ countSource: z.ZodOptional<z.ZodString>;
173
+ }, z.core.$loose>>;
174
+ surfacedCollections: z.ZodOptional<z.ZodArray<z.ZodString>>;
175
+ surfacedSingles: z.ZodOptional<z.ZodArray<z.ZodString>>;
176
+ }, z.core.$loose>;
177
+ config: z.ZodOptional<z.ZodUnknown>;
178
+ serverHooks: z.ZodOptional<z.ZodUnknown>;
179
+ }, z.core.$loose>>>;
149
180
  cmp: z.ZodOptional<z.ZodUnknown>;
150
181
  }, z.core.$loose>;
151
182
  export type CmsConfigSchema = z.infer<typeof cmsConfigSchema>;
@@ -228,6 +259,12 @@ export declare const _internal: {
228
259
  slug: z.ZodString;
229
260
  sidebarIcon: z.ZodOptional<z.ZodString>;
230
261
  previewUrl: z.ZodOptional<z.ZodString>;
262
+ previews: z.ZodOptional<z.ZodArray<z.ZodObject<{
263
+ name: z.ZodString;
264
+ label: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
265
+ url: z.ZodString;
266
+ default: z.ZodOptional<z.ZodBoolean>;
267
+ }, z.core.$loose>>>;
231
268
  fields: z.ZodArray<z.ZodType<FieldDef, unknown, z.core.$ZodTypeInternals<FieldDef, unknown>>>;
232
269
  layout: z.ZodOptional<z.ZodUnknown>;
233
270
  slugField: z.ZodOptional<z.ZodString>;
@@ -238,6 +275,12 @@ export declare const _internal: {
238
275
  slug: z.ZodString;
239
276
  sidebarIcon: z.ZodOptional<z.ZodString>;
240
277
  previewUrl: z.ZodOptional<z.ZodString>;
278
+ previews: z.ZodOptional<z.ZodArray<z.ZodObject<{
279
+ name: z.ZodString;
280
+ label: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
281
+ url: z.ZodString;
282
+ default: z.ZodOptional<z.ZodBoolean>;
283
+ }, z.core.$loose>>>;
241
284
  fields: z.ZodArray<z.ZodType<FieldDef, unknown, z.core.$ZodTypeInternals<FieldDef, unknown>>>;
242
285
  layout: z.ZodOptional<z.ZodUnknown>;
243
286
  slugField: z.ZodOptional<z.ZodString>;
@@ -314,6 +357,26 @@ export declare const _internal: {
314
357
  label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
315
358
  notificationEmailAddresses: z.ZodOptional<z.ZodArray<z.ZodString>>;
316
359
  }, z.core.$loose>;
360
+ moduleSchema: z.ZodObject<{
361
+ id: z.ZodString;
362
+ label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
363
+ icon: z.ZodString;
364
+ context: z.ZodObject<{
365
+ contextLabel: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
366
+ landingUrl: z.ZodString;
367
+ sections: z.ZodArray<z.ZodObject<{
368
+ key: z.ZodString;
369
+ title: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
370
+ url: z.ZodString;
371
+ icon: z.ZodString;
372
+ countSource: z.ZodOptional<z.ZodString>;
373
+ }, z.core.$loose>>;
374
+ surfacedCollections: z.ZodOptional<z.ZodArray<z.ZodString>>;
375
+ surfacedSingles: z.ZodOptional<z.ZodArray<z.ZodString>>;
376
+ }, z.core.$loose>;
377
+ config: z.ZodOptional<z.ZodUnknown>;
378
+ serverHooks: z.ZodOptional<z.ZodUnknown>;
379
+ }, z.core.$loose>;
317
380
  cmsConfigBaseSchema: z.ZodObject<{
318
381
  languages: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
319
382
  code: z.ZodString;
@@ -331,6 +394,12 @@ export declare const _internal: {
331
394
  slug: z.ZodString;
332
395
  sidebarIcon: z.ZodOptional<z.ZodString>;
333
396
  previewUrl: z.ZodOptional<z.ZodString>;
397
+ previews: z.ZodOptional<z.ZodArray<z.ZodObject<{
398
+ name: z.ZodString;
399
+ label: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
400
+ url: z.ZodString;
401
+ default: z.ZodOptional<z.ZodBoolean>;
402
+ }, z.core.$loose>>>;
334
403
  fields: z.ZodArray<z.ZodType<FieldDef, unknown, z.core.$ZodTypeInternals<FieldDef, unknown>>>;
335
404
  layout: z.ZodOptional<z.ZodUnknown>;
336
405
  slugField: z.ZodOptional<z.ZodString>;
@@ -341,6 +410,12 @@ export declare const _internal: {
341
410
  slug: z.ZodString;
342
411
  sidebarIcon: z.ZodOptional<z.ZodString>;
343
412
  previewUrl: z.ZodOptional<z.ZodString>;
413
+ previews: z.ZodOptional<z.ZodArray<z.ZodObject<{
414
+ name: z.ZodString;
415
+ label: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
416
+ url: z.ZodString;
417
+ default: z.ZodOptional<z.ZodBoolean>;
418
+ }, z.core.$loose>>>;
344
419
  fields: z.ZodArray<z.ZodType<FieldDef, unknown, z.core.$ZodTypeInternals<FieldDef, unknown>>>;
345
420
  layout: z.ZodOptional<z.ZodUnknown>;
346
421
  slugField: z.ZodOptional<z.ZodString>;
@@ -442,7 +517,26 @@ export declare const _internal: {
442
517
  fixOrphans: z.ZodOptional<z.ZodBoolean>;
443
518
  }, z.core.$loose>>;
444
519
  sidebarHelp: z.ZodOptional<z.ZodBoolean>;
445
- shop: z.ZodOptional<z.ZodUnknown>;
520
+ modules: z.ZodOptional<z.ZodArray<z.ZodObject<{
521
+ id: z.ZodString;
522
+ label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
523
+ icon: z.ZodString;
524
+ context: z.ZodObject<{
525
+ contextLabel: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
526
+ landingUrl: z.ZodString;
527
+ sections: z.ZodArray<z.ZodObject<{
528
+ key: z.ZodString;
529
+ title: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
530
+ url: z.ZodString;
531
+ icon: z.ZodString;
532
+ countSource: z.ZodOptional<z.ZodString>;
533
+ }, z.core.$loose>>;
534
+ surfacedCollections: z.ZodOptional<z.ZodArray<z.ZodString>>;
535
+ surfacedSingles: z.ZodOptional<z.ZodArray<z.ZodString>>;
536
+ }, z.core.$loose>;
537
+ config: z.ZodOptional<z.ZodUnknown>;
538
+ serverHooks: z.ZodOptional<z.ZodUnknown>;
539
+ }, z.core.$loose>>>;
446
540
  cmp: z.ZodOptional<z.ZodUnknown>;
447
541
  }, z.core.$loose>;
448
542
  FIELD_TYPES: readonly ["text", "content", "number", "boolean", "date", "datetime", "file", "media", "select", "radio", "checkboxes", "relation", "object", "array", "blocks", "slug", "seo", "shop", "url", "icon", "custom"];
@@ -344,10 +344,19 @@ const formFieldSchema = z.discriminatedUnion('type', [
344
344
  /* -------------------------------------------------------------------------- */
345
345
  /* Collections / Singles / Forms */
346
346
  /* -------------------------------------------------------------------------- */
347
+ const previewViewSchema = z
348
+ .object({
349
+ name: z.string().min(1, { message: 'preview view must have a name' }),
350
+ label: localizedSchema.optional(),
351
+ url: z.string().min(1, { message: 'preview view must have a url' }),
352
+ default: z.boolean().optional()
353
+ })
354
+ .passthrough();
347
355
  const configBaseShape = {
348
356
  slug: slugSchema,
349
357
  sidebarIcon: z.string().optional(),
350
358
  previewUrl: z.string().optional(),
359
+ previews: z.array(previewViewSchema).optional(),
351
360
  fields: z.array(fieldSchema).min(1, {
352
361
  message: 'must declare at least one field'
353
362
  }),
@@ -436,6 +445,39 @@ const apiKeySchema = z
436
445
  })
437
446
  .passthrough();
438
447
  /* -------------------------------------------------------------------------- */
448
+ /* Modules */
449
+ /* -------------------------------------------------------------------------- */
450
+ const moduleNavSectionSchema = z
451
+ .object({
452
+ key: z.string().min(1, { message: 'module nav section.key is required' }),
453
+ title: localizedSchema,
454
+ url: z.string().min(1, { message: 'module nav section.url is required' }),
455
+ icon: z.string().min(1, { message: 'module nav section.icon is required' }),
456
+ countSource: z.string().optional()
457
+ })
458
+ .passthrough();
459
+ const moduleContextSchema = z
460
+ .object({
461
+ contextLabel: localizedSchema.optional(),
462
+ landingUrl: z.string().min(1, { message: 'module context.landingUrl is required' }),
463
+ sections: z.array(moduleNavSectionSchema),
464
+ surfacedCollections: z.array(z.string()).optional(),
465
+ surfacedSingles: z.array(z.string()).optional()
466
+ })
467
+ .passthrough();
468
+ const moduleSchema = z
469
+ .object({
470
+ id: slugSchema,
471
+ label: localizedSchema,
472
+ icon: z.string().min(1, { message: 'module.icon is required' }),
473
+ context: moduleContextSchema,
474
+ // `config` holds adapter functions / resolved module config — opaque.
475
+ config: z.unknown().optional(),
476
+ // `serverHooks` holds functions — never validated, never serialized.
477
+ serverHooks: z.unknown().optional()
478
+ })
479
+ .passthrough();
480
+ /* -------------------------------------------------------------------------- */
439
481
  /* CMSConfig */
440
482
  /* -------------------------------------------------------------------------- */
441
483
  const cmsConfigBaseSchema = z
@@ -465,7 +507,7 @@ const cmsConfigBaseSchema = z
465
507
  .passthrough()
466
508
  .optional(),
467
509
  sidebarHelp: z.boolean().optional(),
468
- shop: z.unknown().optional(),
510
+ modules: z.array(moduleSchema).optional(),
469
511
  cmp: z.unknown().optional()
470
512
  })
471
513
  .passthrough();
@@ -625,6 +667,46 @@ export const cmsConfigSchema = cmsConfigBaseSchema.superRefine((data, ctx) => {
625
667
  }
626
668
  }
627
669
  });
670
+ // Module id uniqueness + surfaced collection/single references
671
+ const moduleIds = new Set();
672
+ (data.modules ?? []).forEach((m, i) => {
673
+ const mod = m;
674
+ const id = mod.id;
675
+ if (typeof id === 'string') {
676
+ if (moduleIds.has(id)) {
677
+ ctx.addIssue({
678
+ code: z.ZodIssueCode.custom,
679
+ path: ['modules', i, 'id'],
680
+ message: `duplicate module id '${id}'`
681
+ });
682
+ }
683
+ moduleIds.add(id);
684
+ }
685
+ const surfacedCollections = mod.context?.surfacedCollections;
686
+ if (Array.isArray(surfacedCollections)) {
687
+ surfacedCollections.forEach((slug, j) => {
688
+ if (!collectionSlugs.has(slug)) {
689
+ ctx.addIssue({
690
+ code: z.ZodIssueCode.custom,
691
+ path: ['modules', i, 'context', 'surfacedCollections', j],
692
+ message: `surfaced collection '${slug}' does not reference a declared collection`
693
+ });
694
+ }
695
+ });
696
+ }
697
+ const surfacedSingles = mod.context?.surfacedSingles;
698
+ if (Array.isArray(surfacedSingles)) {
699
+ surfacedSingles.forEach((slug, j) => {
700
+ if (!singleSlugs.has(slug)) {
701
+ ctx.addIssue({
702
+ code: z.ZodIssueCode.custom,
703
+ path: ['modules', i, 'context', 'surfacedSingles', j],
704
+ message: `surfaced single '${slug}' does not reference a declared single`
705
+ });
706
+ }
707
+ });
708
+ }
709
+ });
628
710
  });
629
711
  /** Exposed for tests only. Not part of the public API. */
630
712
  export const _internal = {
@@ -633,6 +715,7 @@ export const _internal = {
633
715
  collectionSchema,
634
716
  singleSchema,
635
717
  formSchema,
718
+ moduleSchema,
636
719
  cmsConfigBaseSchema,
637
720
  FIELD_TYPES,
638
721
  FORM_FIELD_TYPES,