@vicacha-devs/strapi-plugin-events 0.0.0-stage → 1.0.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 (89) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +121 -2
  3. package/dist/admin/CalendarPage-CwCvPDRx.js +1215 -0
  4. package/dist/admin/CalendarPage-Dz7g19qp.mjs +1215 -0
  5. package/dist/admin/RRuleInput-BQl-siRj.js +11 -0
  6. package/dist/admin/RRuleInput-C8bR37X9.mjs +6 -0
  7. package/dist/admin/SettingsPage-CNG9AEO_.mjs +415 -0
  8. package/dist/admin/SettingsPage-DhjtKDmY.js +415 -0
  9. package/dist/admin/ar-mMKUXxch.js +216 -0
  10. package/dist/admin/ar-tWKEvJLB.mjs +216 -0
  11. package/dist/admin/de-CGk8UZ3W.js +216 -0
  12. package/dist/admin/de-OdbJEFs0.mjs +216 -0
  13. package/dist/admin/en-Dskn9ZPa.mjs +216 -0
  14. package/dist/admin/en-imdSlLhJ.js +216 -0
  15. package/dist/admin/es-DcMsMHrI.mjs +216 -0
  16. package/dist/admin/es-DkqqxUJQ.js +216 -0
  17. package/dist/admin/fr-BTEIPOFw.mjs +216 -0
  18. package/dist/admin/fr-D92B_ZTv.js +216 -0
  19. package/dist/admin/index-BTq_X4uT.js +1005 -0
  20. package/dist/admin/index-BatOiwBu.mjs +1006 -0
  21. package/dist/admin/index.js +6 -0
  22. package/dist/admin/index.mjs +6 -0
  23. package/dist/admin/it-CAxPpL_9.mjs +216 -0
  24. package/dist/admin/it-ClKaSCgv.js +216 -0
  25. package/dist/admin/nl-CS1aUfT7.mjs +216 -0
  26. package/dist/admin/nl-F6FIi7zw.js +216 -0
  27. package/dist/admin/pl-BfmBU9FW.mjs +216 -0
  28. package/dist/admin/pl-p8VLXi02.js +216 -0
  29. package/dist/admin/pt-BUdH0uSw.js +216 -0
  30. package/dist/admin/pt-DdulKcbk.mjs +216 -0
  31. package/dist/admin/src/components/EventSidePanel.d.ts +13 -0
  32. package/dist/admin/src/components/Initializer.d.ts +5 -0
  33. package/dist/admin/src/components/PluginIcon.d.ts +2 -0
  34. package/dist/admin/src/components/RRuleInput.d.ts +3 -0
  35. package/dist/admin/src/components/events/AttendeesManagerPanel.d.ts +15 -0
  36. package/dist/admin/src/components/events/EventsDirectoryPanel.d.ts +11 -0
  37. package/dist/admin/src/components/events/EventsKpiCards.d.ts +16 -0
  38. package/dist/admin/src/components/events/QuickCreateEventPanel.d.ts +21 -0
  39. package/dist/admin/src/components/events/calendarPageHelpers.d.ts +8 -0
  40. package/dist/admin/src/fields/rruleField.d.ts +41 -0
  41. package/dist/admin/src/index.d.ts +15 -0
  42. package/dist/admin/src/pages/CalendarPage.d.ts +3 -0
  43. package/dist/admin/src/pages/SettingsPage.d.ts +3 -0
  44. package/dist/admin/src/permissions.d.ts +15 -0
  45. package/dist/admin/src/translations/cmLabels.d.ts +1 -0
  46. package/dist/admin/src/translations/cmPlaceholders.d.ts +1 -0
  47. package/dist/admin/tr-Bp50qvL8.mjs +216 -0
  48. package/dist/admin/tr-C7G9mPtg.js +216 -0
  49. package/dist/server/index.js +3709 -0
  50. package/dist/server/index.mjs +3705 -0
  51. package/dist/server/src/bootstrap.d.ts +5 -0
  52. package/dist/server/src/config/index.d.ts +7 -0
  53. package/dist/server/src/content-types/event/schema.json.d.ts +279 -0
  54. package/dist/server/src/content-types/index.d.ts +366 -0
  55. package/dist/server/src/content-types/registration/schema.json.d.ts +86 -0
  56. package/dist/server/src/controllers/admin.d.ts +17 -0
  57. package/dist/server/src/controllers/content-api.d.ts +14 -0
  58. package/dist/server/src/controllers/index.d.ts +28 -0
  59. package/dist/server/src/destroy.d.ts +5 -0
  60. package/dist/server/src/index.d.ts +551 -0
  61. package/dist/server/src/middlewares/index.d.ts +2 -0
  62. package/dist/server/src/policies/index.d.ts +2 -0
  63. package/dist/server/src/register.d.ts +5 -0
  64. package/dist/server/src/routes/admin.d.ts +17 -0
  65. package/dist/server/src/routes/content-api.d.ts +12 -0
  66. package/dist/server/src/routes/index.d.ts +30 -0
  67. package/dist/server/src/services/ical.d.ts +18 -0
  68. package/dist/server/src/services/index.d.ts +110 -0
  69. package/dist/server/src/services/occurrence.d.ts +44 -0
  70. package/dist/server/src/services/occurrenceHelpers.d.ts +16 -0
  71. package/dist/server/src/services/occurrencePayloadHelpers.d.ts +30 -0
  72. package/dist/server/src/services/occurrenceRsvpBatch.d.ts +25 -0
  73. package/dist/server/src/services/rsvp.d.ts +51 -0
  74. package/dist/server/src/services/rsvpHelpers.d.ts +52 -0
  75. package/dist/server/src/services/rsvpMutations.d.ts +34 -0
  76. package/dist/server/src/services/rsvpSecurity.d.ts +22 -0
  77. package/dist/server/src/services/settings.d.ts +10 -0
  78. package/dist/server/src/types.d.ts +171 -0
  79. package/dist/shared/calendarSystems.d.ts +6 -0
  80. package/dist/shared/icalGenerator.d.ts +25 -0
  81. package/dist/shared/index.d.ts +6 -0
  82. package/dist/shared/index.js +753 -0
  83. package/dist/shared/index.mjs +727 -0
  84. package/dist/shared/pluginId.d.ts +4 -0
  85. package/dist/shared/rruleEngine.d.ts +20 -0
  86. package/dist/shared/spotlightHelpers.d.ts +17 -0
  87. package/dist/shared/types.d.ts +309 -0
  88. package/dist/shared/urlAndPosterHelpers.d.ts +18 -0
  89. package/package.json +109 -3
@@ -0,0 +1,3709 @@
1
+ "use strict";
2
+ Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
+ const shared = require("@vicacha-devs/strapi-plugin-calendar/shared");
4
+ const crypto = require("node:crypto");
5
+ const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
6
+ const crypto__default = /* @__PURE__ */ _interopDefault(crypto);
7
+ const PLUGIN_ID = "events";
8
+ const EVENT_CONTENT_TYPE_UID = `plugin::${PLUGIN_ID}.event`;
9
+ const REGISTRATION_CONTENT_TYPE_UID = `plugin::${PLUGIN_ID}.registration`;
10
+ class PluginValidationError extends Error {
11
+ statusCode = 400;
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = "PluginValidationError";
15
+ }
16
+ }
17
+ class PluginNotFoundError extends Error {
18
+ statusCode = 404;
19
+ constructor(message) {
20
+ super(message);
21
+ this.name = "PluginNotFoundError";
22
+ }
23
+ }
24
+ function getDocumentsClient(strapi, uid) {
25
+ return strapi.documents(
26
+ uid
27
+ );
28
+ }
29
+ function getSettingsService(strapi) {
30
+ return strapi.plugin(PLUGIN_ID).service("settings");
31
+ }
32
+ function getOccurrenceService(strapi) {
33
+ return strapi.plugin(PLUGIN_ID).service("occurrence");
34
+ }
35
+ function getRsvpService(strapi) {
36
+ return strapi.plugin(PLUGIN_ID).service("rsvp");
37
+ }
38
+ function getIcalService(strapi) {
39
+ return strapi.plugin(PLUGIN_ID).service("ical");
40
+ }
41
+ function clampPaginatedSlice(items, params) {
42
+ const maxLimit = params?.maxLimit ?? 500;
43
+ const defaultLimit = params?.defaultLimit ?? maxLimit;
44
+ if (params?.page !== void 0 || params?.pageSize !== void 0) {
45
+ const rawPage = Number(params.page);
46
+ const page = Number.isFinite(rawPage) && rawPage >= 1 ? Math.min(1e3, Math.floor(rawPage)) : 1;
47
+ const rawSize = Number(params.pageSize ?? params.limit);
48
+ const pageSize = Number.isFinite(rawSize) && rawSize >= 1 ? Math.min(maxLimit, Math.floor(rawSize)) : Math.min(50, maxLimit);
49
+ const offset = (page - 1) * pageSize;
50
+ return items.slice(offset, offset + pageSize);
51
+ }
52
+ if (params?.limit !== void 0) {
53
+ const rawLimit = Number(params.limit);
54
+ const safeLimit = Number.isFinite(rawLimit) && rawLimit >= 1 ? Math.min(maxLimit, Math.floor(rawLimit)) : Math.min(defaultLimit, maxLimit);
55
+ return items.slice(0, safeLimit);
56
+ }
57
+ return items.slice(0, defaultLimit);
58
+ }
59
+ function handleControllerError(strapi, ctx, err, fallbackMessage) {
60
+ if (err instanceof PluginNotFoundError) {
61
+ if (typeof ctx.notFound === "function") {
62
+ return ctx.notFound(err.message);
63
+ }
64
+ ctx.status = 404;
65
+ ctx.body = { error: { status: 404, message: err.message } };
66
+ return void 0;
67
+ }
68
+ if (err instanceof PluginValidationError) {
69
+ return ctx.badRequest(err.message);
70
+ }
71
+ const detail = err instanceof Error ? err.message : String(err);
72
+ strapi.log?.error?.(`[events] ${fallbackMessage}: ${detail}`);
73
+ return ctx.badRequest(fallbackMessage);
74
+ }
75
+ const bootstrap = async ({ strapi }) => {
76
+ const actions = [
77
+ {
78
+ section: "plugins",
79
+ displayName: "Read Calendar & Events",
80
+ uid: "read",
81
+ pluginName: PLUGIN_ID
82
+ },
83
+ {
84
+ section: "plugins",
85
+ displayName: "Create & Reschedule Events / Manage RSVPs",
86
+ uid: "update",
87
+ pluginName: PLUGIN_ID
88
+ },
89
+ {
90
+ section: "plugins",
91
+ displayName: "Manage Events Plugin Settings",
92
+ uid: "settings.update",
93
+ pluginName: PLUGIN_ID
94
+ }
95
+ ];
96
+ if (strapi.admin?.services?.permission?.actionProvider?.registerMany) {
97
+ await strapi.admin.services.permission.actionProvider.registerMany(actions);
98
+ }
99
+ const settingsService = getSettingsService(strapi);
100
+ let settings2 = null;
101
+ if (settingsService && typeof settingsService.ensureInitialized === "function") {
102
+ settings2 = await settingsService.ensureInitialized();
103
+ }
104
+ const calendarPlugin = strapi.plugin("calendar");
105
+ const calendarRegistry = calendarPlugin?.service("registry");
106
+ if (calendarRegistry && typeof calendarRegistry.registerSource === "function" && settings2) {
107
+ for (const source of settings2.sources ?? []) {
108
+ try {
109
+ await calendarRegistry.registerSource({
110
+ id: source.id,
111
+ label: source.label,
112
+ uid: source.uid,
113
+ color: source.color,
114
+ enabled: source.enabled,
115
+ fields: {
116
+ title: source.fields.title,
117
+ startDate: source.fields.startDate,
118
+ endDate: source.fields.endDate,
119
+ rrule: source.fields.rrule,
120
+ location: source.fields.location,
121
+ category: source.fields.category,
122
+ status: source.fields.status,
123
+ summary: source.fields.summary,
124
+ slug: source.fields.slug,
125
+ timeSlot: source.fields.timeSlot,
126
+ alternateDateFields: source.fields.alternateDateFields,
127
+ customFields: source.fields.customFields
128
+ },
129
+ timeSlotMap: source.timeSlotMap,
130
+ categoryColors: source.categoryColors,
131
+ defaultAllDay: source.defaultAllDay
132
+ });
133
+ } catch {
134
+ }
135
+ }
136
+ }
137
+ };
138
+ const DEFAULT_PLUGIN_SETTINGS = {
139
+ defaultTimezone: "UTC",
140
+ defaultCapacity: 100,
141
+ waitlistEnabledByDefault: true,
142
+ defaultMaxGuestsPerRegistration: 5,
143
+ sendRsvpEmails: true,
144
+ feedTitle: "Events Calendar",
145
+ sources: [
146
+ {
147
+ id: "builtin-events",
148
+ label: "Events",
149
+ uid: EVENT_CONTENT_TYPE_UID,
150
+ color: "#0D9488",
151
+ enabled: true,
152
+ fields: {
153
+ title: "title",
154
+ startDate: "startDate",
155
+ endDate: "endDate",
156
+ rrule: "recurrence",
157
+ location: "location",
158
+ category: "category",
159
+ status: "eventStatus",
160
+ summary: "summary",
161
+ slug: "slug",
162
+ capacity: "capacity",
163
+ rsvpEnabled: "rsvpEnabled",
164
+ poster: "poster",
165
+ actionLinks: "actionLinks",
166
+ spotlightConfig: "spotlightConfig"
167
+ }
168
+ }
169
+ ],
170
+ secondaryCalendars: [],
171
+ recurrencePresets: [
172
+ { id: "daily", label: "Daily", rrule: "FREQ=DAILY;INTERVAL=1" },
173
+ {
174
+ id: "weekly-mon",
175
+ label: "Weekly on Mondays",
176
+ rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=MO"
177
+ },
178
+ {
179
+ id: "weekly-tue",
180
+ label: "Weekly on Tuesdays",
181
+ rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=TU"
182
+ },
183
+ {
184
+ id: "weekly-wed",
185
+ label: "Weekly on Wednesdays",
186
+ rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=WE"
187
+ },
188
+ {
189
+ id: "weekly-thu",
190
+ label: "Weekly on Thursdays",
191
+ rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=TH"
192
+ },
193
+ {
194
+ id: "weekly-fri",
195
+ label: "Weekly on Fridays",
196
+ rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=FR"
197
+ },
198
+ {
199
+ id: "weekly-sat",
200
+ label: "Weekly on Saturdays",
201
+ rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=SA"
202
+ },
203
+ {
204
+ id: "weekly-sun",
205
+ label: "Weekly on Sundays",
206
+ rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=SU"
207
+ },
208
+ { id: "monthly", label: "Monthly", rrule: "FREQ=MONTHLY;INTERVAL=1" },
209
+ { id: "yearly", label: "Yearly", rrule: "FREQ=YEARLY;INTERVAL=1" }
210
+ ]
211
+ };
212
+ const config = {
213
+ default: DEFAULT_PLUGIN_SETTINGS,
214
+ validator(config2) {
215
+ if (config2.defaultCapacity !== void 0 && config2.defaultCapacity < 1) {
216
+ throw new Error("events plugin config: defaultCapacity must be >= 1");
217
+ }
218
+ }
219
+ };
220
+ const kind$1 = "collectionType";
221
+ const collectionName$1 = "plugin_events_items";
222
+ const info$1 = { "singularName": "event", "pluralName": "events", "displayName": "Event", "description": "Recurring & single events managed by strapi-plugin-events" };
223
+ const options$1 = { "draftAndPublish": true };
224
+ const pluginOptions$1 = { "content-manager": { "visible": true }, "content-type-builder": { "visible": true }, "i18n": { "localized": true } };
225
+ const attributes$1 = { "title": { "type": "string", "required": true, "maxLength": 200, "pluginOptions": { "i18n": { "localized": true } } }, "slug": { "type": "uid", "targetField": "title", "required": true, "pluginOptions": { "i18n": { "localized": true } } }, "summary": { "type": "text", "maxLength": 500, "pluginOptions": { "i18n": { "localized": true } } }, "description": { "type": "richtext", "pluginOptions": { "i18n": { "localized": true } } }, "startDate": { "type": "datetime", "required": true, "pluginOptions": { "i18n": { "localized": false } } }, "endDate": { "type": "datetime", "pluginOptions": { "i18n": { "localized": false } } }, "allDay": { "type": "boolean", "default": false, "pluginOptions": { "i18n": { "localized": false } } }, "timezone": { "type": "string", "default": "UTC", "pluginOptions": { "i18n": { "localized": false } } }, "recurrence": { "type": "customField", "customField": "plugin::events.rrule", "pluginOptions": { "i18n": { "localized": false } } }, "eventStatus": { "type": "enumeration", "enum": ["scheduled", "cancelled", "postponed", "rescheduled"], "default": "scheduled", "required": true, "pluginOptions": { "i18n": { "localized": false } } }, "locationType": { "type": "enumeration", "enum": ["in_person", "online", "hybrid"], "default": "in_person", "pluginOptions": { "i18n": { "localized": false } } }, "location": { "type": "string", "pluginOptions": { "i18n": { "localized": true } } }, "virtualUrl": { "type": "string", "pluginOptions": { "i18n": { "localized": false } } }, "category": { "type": "string", "default": "general", "pluginOptions": { "i18n": { "localized": false } } }, "speaker": { "type": "string", "pluginOptions": { "i18n": { "localized": true } } }, "speakerSlug": { "type": "string", "pluginOptions": { "i18n": { "localized": false } } }, "color": { "type": "string", "default": "#0D9488", "pluginOptions": { "i18n": { "localized": false } } }, "featured": { "type": "boolean", "default": false, "pluginOptions": { "i18n": { "localized": false } } }, "rsvpEnabled": { "type": "boolean", "default": false, "pluginOptions": { "i18n": { "localized": false } } }, "capacity": { "type": "integer", "min": 1, "pluginOptions": { "i18n": { "localized": false } } }, "waitlistEnabled": { "type": "boolean", "default": true, "pluginOptions": { "i18n": { "localized": false } } }, "maxGuestsPerRegistration": { "type": "integer", "min": 1, "max": 50, "default": 5, "pluginOptions": { "i18n": { "localized": false } } }, "registrationDeadline": { "type": "datetime", "pluginOptions": { "i18n": { "localized": false } } }, "alternateDates": { "type": "json", "pluginOptions": { "i18n": { "localized": true } } }, "poster": { "type": "media", "multiple": false, "required": false, "allowedTypes": ["images", "files"], "pluginOptions": { "i18n": { "localized": true } } }, "actionLinks": { "type": "json", "pluginOptions": { "i18n": { "localized": true } } }, "spotlightConfig": { "type": "json", "pluginOptions": { "i18n": { "localized": true } } }, "metadata": { "type": "json", "pluginOptions": { "i18n": { "localized": true } } } };
226
+ const eventSchema = {
227
+ kind: kind$1,
228
+ collectionName: collectionName$1,
229
+ info: info$1,
230
+ options: options$1,
231
+ pluginOptions: pluginOptions$1,
232
+ attributes: attributes$1
233
+ };
234
+ const kind = "collectionType";
235
+ const collectionName = "plugin_events_registrations";
236
+ const info = { "singularName": "registration", "pluralName": "registrations", "displayName": "Event Registration (RSVP)", "description": "Attendee RSVPs, waitlists, and check-ins managed by strapi-plugin-events" };
237
+ const options = { "draftAndPublish": false };
238
+ const pluginOptions = { "content-manager": { "visible": true }, "content-type-builder": { "visible": true } };
239
+ const attributes = { "eventUid": { "type": "string", "required": true, "default": "plugin::events.event" }, "eventDocumentId": { "type": "string", "required": true }, "eventTitle": { "type": "string" }, "occurrenceDate": { "type": "string", "required": true }, "attendeeName": { "type": "string", "required": true, "maxLength": 160 }, "attendeeEmail": { "type": "email", "required": true }, "attendeePhone": { "type": "string", "maxLength": 50 }, "guestsCount": { "type": "integer", "required": true, "min": 1, "max": 50, "default": 1 }, "status": { "type": "enumeration", "enum": ["confirmed", "waitlisted", "cancelled", "checked_in"], "default": "confirmed", "required": true }, "checkInToken": { "type": "string", "private": true }, "checkedInAt": { "type": "datetime" }, "locale": { "type": "string", "default": "es" }, "notes": { "type": "text", "maxLength": 2e3 }, "metadata": { "type": "json" } };
240
+ const registrationSchema = {
241
+ kind,
242
+ collectionName,
243
+ info,
244
+ options,
245
+ pluginOptions,
246
+ attributes
247
+ };
248
+ const contentTypes = {
249
+ event: { schema: eventSchema },
250
+ registration: { schema: registrationSchema }
251
+ };
252
+ function optStr$1(val) {
253
+ return typeof val === "string" && val ? val : void 0;
254
+ }
255
+ function optNum(val) {
256
+ return typeof val === "number" && Number.isFinite(val) ? val : void 0;
257
+ }
258
+ function optQueryInt$1(val) {
259
+ if (typeof val === "number" && Number.isFinite(val)) return val;
260
+ if (typeof val === "string" && val.trim() !== "") {
261
+ const parsed = Number(val);
262
+ return Number.isFinite(parsed) ? parsed : void 0;
263
+ }
264
+ return void 0;
265
+ }
266
+ function buildRescheduleInput(body) {
267
+ return {
268
+ sourceId: optStr$1(body.sourceId),
269
+ uid: optStr$1(body.uid),
270
+ documentId: String(body.documentId ?? ""),
271
+ occurrenceDate: String(body.occurrenceDate ?? ""),
272
+ newStartDate: String(body.newStartDate ?? ""),
273
+ newEndDate: optStr$1(body.newEndDate),
274
+ applyToSeries: Boolean(body.applyToSeries),
275
+ cancelOccurrence: Boolean(body.cancelOccurrence)
276
+ };
277
+ }
278
+ function buildQuickCreateInput(body) {
279
+ return {
280
+ sourceId: optStr$1(body.sourceId),
281
+ title: String(body.title ?? ""),
282
+ startDate: String(body.startDate ?? ""),
283
+ endDate: optStr$1(body.endDate),
284
+ location: optStr$1(body.location),
285
+ category: optStr$1(body.category),
286
+ summary: optStr$1(body.summary),
287
+ rrule: optStr$1(body.rrule),
288
+ rsvpEnabled: Boolean(body.rsvpEnabled),
289
+ capacity: optNum(body.capacity)
290
+ };
291
+ }
292
+ const admin$1 = ({ strapi }) => {
293
+ return {
294
+ async getSettings(ctx) {
295
+ const data = await getSettingsService(strapi).getSettings();
296
+ ctx.body = { data };
297
+ },
298
+ async updateSettings(ctx) {
299
+ try {
300
+ const payload = ctx.request?.body ?? {};
301
+ const data = await getSettingsService(strapi).updateSettings(payload);
302
+ ctx.body = { data };
303
+ } catch (err) {
304
+ return handleControllerError(
305
+ strapi,
306
+ ctx,
307
+ err,
308
+ "Unable to update settings."
309
+ );
310
+ }
311
+ },
312
+ async getCalendar(ctx) {
313
+ const q = ctx.query ?? {};
314
+ const settings2 = await getSettingsService(strapi).getSettings();
315
+ const occurrences = await getOccurrenceService(strapi).getOccurrences({
316
+ from: optStr$1(q.from),
317
+ to: optStr$1(q.to),
318
+ category: optStr$1(q.category),
319
+ sourceId: optStr$1(q.sourceId),
320
+ locale: optStr$1(q.locale),
321
+ includeDrafts: true,
322
+ limit: optQueryInt$1(q.limit),
323
+ page: optQueryInt$1(q.page),
324
+ pageSize: optQueryInt$1(q.pageSize)
325
+ });
326
+ ctx.body = {
327
+ data: {
328
+ occurrences,
329
+ settings: settings2
330
+ }
331
+ };
332
+ },
333
+ async reschedule(ctx) {
334
+ try {
335
+ const body = ctx.request?.body ?? {};
336
+ const updated = await getOccurrenceService(strapi).rescheduleOccurrence(
337
+ buildRescheduleInput(body)
338
+ );
339
+ ctx.body = { data: updated };
340
+ } catch (err) {
341
+ return handleControllerError(
342
+ strapi,
343
+ ctx,
344
+ err,
345
+ "Unable to reschedule event occurrence."
346
+ );
347
+ }
348
+ },
349
+ async quickCreate(ctx) {
350
+ try {
351
+ const body = ctx.request?.body ?? {};
352
+ const created = await getOccurrenceService(strapi).quickCreateEvent(
353
+ buildQuickCreateInput(body)
354
+ );
355
+ ctx.status = 201;
356
+ ctx.body = { data: created };
357
+ } catch (err) {
358
+ return handleControllerError(
359
+ strapi,
360
+ ctx,
361
+ err,
362
+ "Unable to create event."
363
+ );
364
+ }
365
+ },
366
+ async getEventStats(ctx) {
367
+ const { documentId } = ctx.params ?? {};
368
+ const q = ctx.query ?? {};
369
+ const data = await getRsvpService(strapi).getEventStats({
370
+ eventUid: optStr$1(q.eventUid),
371
+ eventDocumentId: String(documentId ?? ""),
372
+ occurrenceDate: optStr$1(q.occurrenceDate) ?? null,
373
+ includeDrafts: true
374
+ });
375
+ ctx.body = { data };
376
+ },
377
+ async listRegistrations(ctx) {
378
+ const q = ctx.query ?? {};
379
+ const data = await getRsvpService(strapi).listRegistrations({
380
+ eventDocumentId: optStr$1(q.eventDocumentId),
381
+ occurrenceDate: optStr$1(q.occurrenceDate),
382
+ status: optStr$1(q.status),
383
+ search: optStr$1(q.search),
384
+ limit: optQueryInt$1(q.limit),
385
+ page: optQueryInt$1(q.page),
386
+ pageSize: optQueryInt$1(q.pageSize)
387
+ });
388
+ ctx.body = { data };
389
+ },
390
+ async updateRegistrationStatus(ctx) {
391
+ try {
392
+ const { documentId } = ctx.params ?? {};
393
+ const { status } = ctx.request?.body ?? {};
394
+ const result = await getRsvpService(strapi).updateRegistrationStatus(
395
+ String(documentId ?? ""),
396
+ status
397
+ );
398
+ ctx.body = { data: result };
399
+ } catch (err) {
400
+ return handleControllerError(
401
+ strapi,
402
+ ctx,
403
+ err,
404
+ "Failed to update registration status."
405
+ );
406
+ }
407
+ },
408
+ async checkIn(ctx) {
409
+ try {
410
+ const { documentId } = ctx.params ?? {};
411
+ const { token } = ctx.request?.body ?? {};
412
+ const data = await getRsvpService(strapi).checkInAttendee({
413
+ documentId: optStr$1(documentId),
414
+ token: optStr$1(token)
415
+ });
416
+ ctx.body = { data };
417
+ } catch (err) {
418
+ return handleControllerError(strapi, ctx, err, "Check-in failed.");
419
+ }
420
+ },
421
+ async exportRegistrationsCsv(ctx) {
422
+ const q = ctx.query ?? {};
423
+ const csv = await getRsvpService(strapi).exportRegistrationsCsv({
424
+ eventDocumentId: optStr$1(q.eventDocumentId),
425
+ occurrenceDate: optStr$1(q.occurrenceDate),
426
+ status: optStr$1(q.status)
427
+ });
428
+ ctx.set("Content-Type", "text/csv; charset=utf-8");
429
+ ctx.set(
430
+ "Content-Disposition",
431
+ 'attachment; filename="event-registrations.csv"'
432
+ );
433
+ ctx.set("X-Content-Type-Options", "nosniff");
434
+ ctx.body = csv;
435
+ }
436
+ };
437
+ };
438
+ function isValidEmail(value) {
439
+ if (/\s/.test(value))
440
+ return false;
441
+ const at = value.indexOf("@");
442
+ if (at < 1 || at !== value.lastIndexOf("@"))
443
+ return false;
444
+ const domain = value.slice(at + 1);
445
+ const dot = domain.indexOf(".");
446
+ return dot > 0 && dot < domain.length - 1;
447
+ }
448
+ function freezeOptions(items) {
449
+ return Object.freeze(items.map((item) => Object.freeze({ ...item })));
450
+ }
451
+ freezeOptions([
452
+ { value: "es", label: "es — Español (Spanish)" },
453
+ { value: "en", label: "en — English" },
454
+ { value: "ar", label: "ar — العربية (Arabic)" },
455
+ { value: "tr", label: "tr — Türkçe (Turkish)" },
456
+ { value: "fr", label: "fr — Français (French)" },
457
+ { value: "de", label: "de — Deutsch (German)" },
458
+ { value: "pt", label: "pt — Português (Portuguese)" },
459
+ { value: "it", label: "it — Italiano (Italian)" },
460
+ { value: "nl", label: "nl — Nederlands (Dutch)" },
461
+ { value: "pl", label: "pl — Polski (Polish)" },
462
+ { value: "ru", label: "ru — Русский (Russian)" },
463
+ { value: "id", label: "id — Bahasa Indonesia" },
464
+ { value: "ur", label: "ur — اردو (Urdu)" },
465
+ { value: "zh", label: "zh — 中文 (Chinese)" }
466
+ ]);
467
+ freezeOptions([
468
+ {
469
+ value: "islamic-umalqura",
470
+ label: "islamic-umalqura (Hijri Umm al-Qura)"
471
+ },
472
+ { value: "islamic-civil", label: "islamic-civil (Tabular Islamic Civil)" },
473
+ { value: "islamic-tbla", label: "islamic-tbla (Tabular Astronomical)" },
474
+ { value: "hebrew", label: "hebrew (Hebrew)" },
475
+ { value: "persian", label: "persian (Solar Hijri)" },
476
+ { value: "none", label: "none (Gregorian Only)" }
477
+ ]);
478
+ const DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set([
479
+ "__proto__",
480
+ "constructor",
481
+ "prototype"
482
+ ]);
483
+ function isSafeObjectKey(key) {
484
+ return typeof key === "string" && key.length > 0 && !DANGEROUS_OBJECT_KEYS.has(key);
485
+ }
486
+ function sanitizePlainRecord(raw, maxDepth = 4) {
487
+ if (!raw || typeof raw !== "object" || Array.isArray(raw) || maxDepth < 0) {
488
+ return null;
489
+ }
490
+ const clean = {};
491
+ for (const [k, v] of Object.entries(raw)) {
492
+ if (!isSafeObjectKey(k)) continue;
493
+ if (v && typeof v === "object" && !Array.isArray(v)) {
494
+ if (maxDepth === 0) continue;
495
+ clean[k] = sanitizePlainRecord(v, maxDepth - 1);
496
+ } else if (Array.isArray(v)) {
497
+ clean[k] = maxDepth === 0 ? [] : v.map(
498
+ (item) => item && typeof item === "object" && !Array.isArray(item) ? sanitizePlainRecord(item, maxDepth - 1) : item
499
+ );
500
+ } else {
501
+ clean[k] = v;
502
+ }
503
+ }
504
+ return clean;
505
+ }
506
+ const ALLOWED_ACTION_VARIANTS = /* @__PURE__ */ new Set([
507
+ "primary",
508
+ "secondary",
509
+ "outline"
510
+ ]);
511
+ const HTTP_PROTOCOLS = /* @__PURE__ */ new Set(["https:", "http:"]);
512
+ const ACTION_PROTOCOLS = /* @__PURE__ */ new Set(["https:", "http:", "mailto:"]);
513
+ function isSafeRelativeUrl(trimmed) {
514
+ return trimmed.startsWith("/") && !trimmed.startsWith("//") && !trimmed.includes("\\");
515
+ }
516
+ function isAllowedAbsoluteUrl(trimmed, allowMailto) {
517
+ try {
518
+ const parsed = new URL(trimmed);
519
+ const proto = parsed.protocol.toLowerCase();
520
+ return allowMailto ? ACTION_PROTOCOLS.has(proto) : HTTP_PROTOCOLS.has(proto);
521
+ } catch {
522
+ return false;
523
+ }
524
+ }
525
+ function sanitizeEventUrl(rawUrl, options2) {
526
+ if (typeof rawUrl !== "string") return null;
527
+ const trimmed = rawUrl.trim();
528
+ if (!trimmed || trimmed.length > 2048 || /[\u0000-\u001F\u007F]/.test(trimmed)) {
529
+ return null;
530
+ }
531
+ const allowMailto = options2?.allowMailto ?? true;
532
+ if (isSafeRelativeUrl(trimmed) || isAllowedAbsoluteUrl(trimmed, allowMailto)) {
533
+ return trimmed;
534
+ }
535
+ return null;
536
+ }
537
+ function parseJsonIfNeeded(raw) {
538
+ if (typeof raw !== "string") return raw;
539
+ const trimmed = raw.trim();
540
+ const isJsonLike = trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]");
541
+ if (!isJsonLike) return raw;
542
+ try {
543
+ return JSON.parse(trimmed);
544
+ } catch {
545
+ return raw;
546
+ }
547
+ }
548
+ function getTrimmedString(val, maxLen) {
549
+ if (typeof val !== "string") return void 0;
550
+ const trimmed = val.trim();
551
+ if (!trimmed) return void 0;
552
+ return maxLen ? trimmed.slice(0, maxLen) : trimmed;
553
+ }
554
+ function getPositiveInteger(val) {
555
+ if (typeof val !== "number" || !Number.isFinite(val) || val <= 0) {
556
+ return void 0;
557
+ }
558
+ return Math.round(val);
559
+ }
560
+ function unwrapPosterRecord(obj) {
561
+ if (obj.data && typeof obj.data === "object" && !Array.isArray(obj.data)) {
562
+ return unwrapPosterRecord(obj.data);
563
+ }
564
+ if (obj.attributes && typeof obj.attributes === "object" && !Array.isArray(obj.attributes)) {
565
+ return unwrapPosterRecord(obj.attributes);
566
+ }
567
+ return obj;
568
+ }
569
+ function resolvePosterAltText(obj, fallbackAlt) {
570
+ return getTrimmedString(obj.alt) || getTrimmedString(obj.alternativeText) || getTrimmedString(obj.caption) || fallbackAlt || void 0;
571
+ }
572
+ function normalizeEventPoster(rawPoster, fallbackAlt) {
573
+ if (!rawPoster) return void 0;
574
+ if (typeof rawPoster === "string") {
575
+ const safeUrl2 = sanitizeEventUrl(rawPoster, { allowMailto: false });
576
+ return safeUrl2 ? { url: safeUrl2, alt: fallbackAlt || void 0 } : void 0;
577
+ }
578
+ if (Array.isArray(rawPoster)) {
579
+ return rawPoster.length > 0 ? normalizeEventPoster(rawPoster[0], fallbackAlt) : void 0;
580
+ }
581
+ if (typeof rawPoster !== "object") return void 0;
582
+ const obj = unwrapPosterRecord(rawPoster);
583
+ const safeUrl = sanitizeEventUrl(obj.url, { allowMailto: false });
584
+ if (!safeUrl) return void 0;
585
+ return {
586
+ url: safeUrl,
587
+ alt: resolvePosterAltText(obj, fallbackAlt),
588
+ mime: getTrimmedString(obj.mime),
589
+ width: getPositiveInteger(obj.width),
590
+ height: getPositiveInteger(obj.height)
591
+ };
592
+ }
593
+ function resolveActionVariant(rawVariant, index2) {
594
+ const candidate = getTrimmedString(rawVariant);
595
+ if (candidate && ALLOWED_ACTION_VARIANTS.has(candidate)) {
596
+ return candidate;
597
+ }
598
+ return index2 === 0 ? "primary" : "outline";
599
+ }
600
+ function normalizeSingleActionLink(item, index2) {
601
+ if (!item || typeof item !== "object") return null;
602
+ const rec = item;
603
+ const label = getTrimmedString(rec.label, 120);
604
+ const safeUrl = sanitizeEventUrl(rec.url);
605
+ if (!label || !safeUrl) return null;
606
+ const external = typeof rec.external === "boolean" ? rec.external : /^https?:\/\//i.test(safeUrl);
607
+ const icon = getTrimmedString(rec.icon, 60);
608
+ return {
609
+ label,
610
+ url: safeUrl,
611
+ variant: resolveActionVariant(rec.variant, index2),
612
+ external,
613
+ ...icon ? { icon } : {}
614
+ };
615
+ }
616
+ function normalizeEventActionLinks(rawLinks) {
617
+ const parsed = parseJsonIfNeeded(rawLinks);
618
+ if (!Array.isArray(parsed) || parsed.length === 0) {
619
+ return void 0;
620
+ }
621
+ const normalized = parsed.map((item, idx) => normalizeSingleActionLink(item, idx)).filter((link) => link !== null);
622
+ return normalized.length > 0 ? normalized : void 0;
623
+ }
624
+ function isValidCalendarDateKey(dateStr) {
625
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
626
+ return false;
627
+ }
628
+ const parsed = /* @__PURE__ */ new Date(`${dateStr}T00:00:00.000Z`);
629
+ if (Number.isNaN(parsed.getTime())) {
630
+ return false;
631
+ }
632
+ return parsed.toISOString().slice(0, 10) === dateStr;
633
+ }
634
+ function toDateKey(dateInput) {
635
+ const d = typeof dateInput === "string" ? new Date(dateInput) : dateInput;
636
+ if (Number.isNaN(d.getTime())) {
637
+ return "";
638
+ }
639
+ return d.toISOString().slice(0, 10);
640
+ }
641
+ function normalizeRRuleString(raw) {
642
+ if (!raw || typeof raw !== "string") {
643
+ return "";
644
+ }
645
+ return raw.replace(/[\u0000-\u001F\u007F]+/g, "").trim().replace(/^RRULE:/i, "").trim();
646
+ }
647
+ function sanitizeOccurrenceOverrides(rawOverrides) {
648
+ const clean = {};
649
+ if (!rawOverrides || typeof rawOverrides !== "object" || Array.isArray(rawOverrides)) {
650
+ return clean;
651
+ }
652
+ for (const [key, ov] of Object.entries(rawOverrides)) {
653
+ if (!isSafeObjectKey(key) || !isValidCalendarDateKey(key)) continue;
654
+ if (!ov || typeof ov !== "object" || Array.isArray(ov)) continue;
655
+ let validStart;
656
+ let startMs = null;
657
+ if (ov.startDate !== void 0 && ov.startDate !== "") {
658
+ if (typeof ov.startDate !== "string") continue;
659
+ const parsedStart = new Date(ov.startDate).getTime();
660
+ if (Number.isNaN(parsedStart)) continue;
661
+ validStart = ov.startDate;
662
+ startMs = parsedStart;
663
+ }
664
+ let validEnd;
665
+ if (ov.endDate !== void 0 && ov.endDate !== "") {
666
+ if (typeof ov.endDate !== "string") continue;
667
+ const parsedEnd = new Date(ov.endDate).getTime();
668
+ if (Number.isNaN(parsedEnd)) continue;
669
+ const minStartMs = startMs ?? (/* @__PURE__ */ new Date(`${key}T00:00:00.000Z`)).getTime();
670
+ if (parsedEnd < minStartMs) continue;
671
+ validEnd = ov.endDate;
672
+ }
673
+ clean[key] = {
674
+ ...ov,
675
+ originalDate: typeof ov.originalDate === "string" && isValidCalendarDateKey(ov.originalDate) ? ov.originalDate : key,
676
+ startDate: validStart,
677
+ endDate: validEnd
678
+ };
679
+ }
680
+ return clean;
681
+ }
682
+ function sanitizeRRuleFieldValue(rec, defaultTimezone = "UTC") {
683
+ const rrule = normalizeRRuleString(rec?.rrule);
684
+ const timezone = typeof rec?.timezone === "string" && rec.timezone.trim() ? rec.timezone.trim() : defaultTimezone;
685
+ const exdates = Array.isArray(rec?.exdates) ? rec.exdates.filter(
686
+ (d) => typeof d === "string" && isValidCalendarDateKey(d)
687
+ ) : [];
688
+ const overrides = sanitizeOccurrenceOverrides(rec?.overrides);
689
+ return {
690
+ ...rec,
691
+ rrule,
692
+ timezone,
693
+ allDay: Boolean(rec?.allDay),
694
+ exdates,
695
+ overrides
696
+ };
697
+ }
698
+ function parseRRuleFieldValue(raw, defaultTimezone = "UTC") {
699
+ const parsed = shared.parseRRuleFieldValue(raw, defaultTimezone);
700
+ return sanitizeRRuleFieldValue(parsed, defaultTimezone);
701
+ }
702
+ function buildRRule(parsed) {
703
+ return shared.buildRRule({
704
+ freq: parsed.freq ?? "NONE",
705
+ interval: parsed.interval ?? 1,
706
+ byDay: parsed.byDay ?? [],
707
+ byMonthDay: parsed.byMonthDay,
708
+ count: parsed.count,
709
+ until: parsed.until
710
+ });
711
+ }
712
+ const FREQ_UNITS = {
713
+ DAILY: { singular: "day", plural: "days" },
714
+ WEEKLY: { singular: "week", plural: "weeks" },
715
+ MONTHLY: { singular: "month", plural: "months" },
716
+ YEARLY: { singular: "year", plural: "years" }
717
+ };
718
+ function formatIntervalUnit(freq, interval) {
719
+ const unit = Object.hasOwn(FREQ_UNITS, freq) ? FREQ_UNITS[freq] : FREQ_UNITS.YEARLY;
720
+ return interval === 1 ? unit.singular : `${interval} ${unit.plural}`;
721
+ }
722
+ function describeRRule(rruleString) {
723
+ const parsed = shared.parseRRule(normalizeRRuleString(rruleString));
724
+ if (parsed.freq === "NONE") {
725
+ return "Does not repeat";
726
+ }
727
+ let summary = `Every ${formatIntervalUnit(parsed.freq, parsed.interval)}`;
728
+ if (parsed.freq === "WEEKLY" && parsed.byDay.length > 0) {
729
+ summary += ` on ${parsed.byDay.join(", ")}`;
730
+ } else if (parsed.freq === "MONTHLY" && parsed.byMonthDay) {
731
+ summary += ` on day ${parsed.byMonthDay}`;
732
+ }
733
+ if (parsed.until) {
734
+ summary += ` until ${parsed.until}`;
735
+ } else if (parsed.count) {
736
+ summary += ` (${parsed.count} occurrences)`;
737
+ }
738
+ return summary;
739
+ }
740
+ function expandRuleSlots(options2) {
741
+ const baseStartMs = new Date(options2.baseStartDate).getTime();
742
+ if (Number.isNaN(baseStartMs)) {
743
+ return [];
744
+ }
745
+ let safeBaseEnd = null;
746
+ if (typeof options2.baseEndDate === "string" && options2.baseEndDate) {
747
+ const baseEndMs = new Date(options2.baseEndDate).getTime();
748
+ if (!Number.isNaN(baseEndMs) && baseEndMs >= baseStartMs) {
749
+ safeBaseEnd = options2.baseEndDate;
750
+ }
751
+ }
752
+ const windowStartIso = typeof options2.windowStart === "string" ? options2.windowStart : options2.windowStart.toISOString();
753
+ const windowEndIso = typeof options2.windowEnd === "string" ? options2.windowEnd : options2.windowEnd.toISOString();
754
+ if (Number.isNaN(new Date(windowStartIso).getTime()) || Number.isNaN(new Date(windowEndIso).getTime())) {
755
+ return [];
756
+ }
757
+ return shared.expandRuleSlots({
758
+ baseStartDate: options2.baseStartDate,
759
+ baseEndDate: safeBaseEnd,
760
+ recurrence: sanitizeRRuleFieldValue(
761
+ options2.recurrence,
762
+ options2.recurrence?.timezone || "UTC"
763
+ ),
764
+ windowStart: windowStartIso,
765
+ windowEnd: windowEndIso,
766
+ maxOccurrences: options2.maxInstances ?? 366
767
+ });
768
+ }
769
+ const ALLOWED_SPOTLIGHT_POSITIONS = /* @__PURE__ */ new Set([
770
+ "bottom-end",
771
+ "bottom-start"
772
+ ]);
773
+ function resolveSpotlightPosition(rawPos) {
774
+ const candidate = getTrimmedString(rawPos);
775
+ return candidate && ALLOWED_SPOTLIGHT_POSITIONS.has(candidate) ? candidate : void 0;
776
+ }
777
+ function resolveFiniteRoundNumber(val) {
778
+ return typeof val === "number" && Number.isFinite(val) ? Math.round(val) : void 0;
779
+ }
780
+ function normalizeEventSpotlightConfig(rawConfig) {
781
+ const parsed = parseJsonIfNeeded(rawConfig);
782
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
783
+ return void 0;
784
+ }
785
+ const rec = parsed;
786
+ const config2 = {
787
+ enabled: Boolean(rec.enabled)
788
+ };
789
+ const badgeText = getTrimmedString(rec.badgeText, 80);
790
+ if (badgeText) config2.badgeText = badgeText;
791
+ const priority = resolveFiniteRoundNumber(rec.priority);
792
+ if (priority !== void 0) config2.priority = priority;
793
+ const position = resolveSpotlightPosition(rec.position);
794
+ if (position) config2.position = position;
795
+ if (typeof rec.showCountdown === "boolean") {
796
+ config2.showCountdown = rec.showCountdown;
797
+ }
798
+ const startAt = getTrimmedString(rec.startAt);
799
+ if (startAt) config2.startAt = startAt;
800
+ const endAt = getTrimmedString(rec.endAt);
801
+ if (endAt) config2.endAt = endAt;
802
+ return config2;
803
+ }
804
+ function toBoundaryTimestamp(rawIsoOrDate, isEndBoundary) {
805
+ const trimmed = rawIsoOrDate.trim();
806
+ const isoString = /^\d{4}-\d{2}-\d{2}$/.test(trimmed) ? `${trimmed}${isEndBoundary ? "T23:59:59.999Z" : "T00:00:00.000Z"}` : trimmed;
807
+ const ts = new Date(isoString).getTime();
808
+ return Number.isNaN(ts) ? null : ts;
809
+ }
810
+ function toReferenceTimestamp(referenceDate) {
811
+ if (referenceDate instanceof Date) {
812
+ const ts2 = referenceDate.getTime();
813
+ return Number.isNaN(ts2) ? null : ts2;
814
+ }
815
+ const str = getTrimmedString(referenceDate);
816
+ const dt = str ? new Date(str) : /* @__PURE__ */ new Date();
817
+ const ts = dt.getTime();
818
+ return Number.isNaN(ts) ? null : ts;
819
+ }
820
+ function isEventSpotlightActiveOnDate(params) {
821
+ const { spotlightConfig, eventStartDate, eventEndDate, referenceDate } = params;
822
+ if (!spotlightConfig?.enabled) return false;
823
+ const refTs = toReferenceTimestamp(referenceDate);
824
+ if (refTs === null) return false;
825
+ const startTs = toBoundaryTimestamp(
826
+ spotlightConfig.startAt || eventStartDate,
827
+ false
828
+ );
829
+ const endTs = toBoundaryTimestamp(
830
+ spotlightConfig.endAt || eventEndDate || eventStartDate,
831
+ true
832
+ );
833
+ const afterStart = startTs === null || refTs >= startTs;
834
+ const beforeEnd = endTs === null || refTs <= endTs;
835
+ return afterStart && beforeEnd;
836
+ }
837
+ function deduplicateAndSortSpotlights(occurrences) {
838
+ const byDocKey = /* @__PURE__ */ new Map();
839
+ for (const occ of occurrences) {
840
+ if (!occ.spotlightConfig?.enabled || occ.status === "cancelled") {
841
+ continue;
842
+ }
843
+ const key = `${occ.sourceId}:${occ.documentId}`;
844
+ if (!byDocKey.has(key)) {
845
+ byDocKey.set(key, occ);
846
+ }
847
+ }
848
+ const unique = Array.from(byDocKey.values());
849
+ unique.sort((a, b) => {
850
+ const prioA = a.spotlightConfig?.priority ?? 0;
851
+ const prioB = b.spotlightConfig?.priority ?? 0;
852
+ if (prioB !== prioA) {
853
+ return prioB - prioA;
854
+ }
855
+ return new Date(a.startDate).getTime() - new Date(b.startDate).getTime();
856
+ });
857
+ return unique;
858
+ }
859
+ const DEFAULT_TIME_SLOTS = {
860
+ morning: { start: "09:00:00", end: "11:00:00" },
861
+ afternoon: { start: "14:00:00", end: "16:00:00" },
862
+ evening: { start: "18:00:00", end: "20:00:00" }
863
+ };
864
+ function buildSafeSlotMap(customSlots) {
865
+ const slotMap = { ...DEFAULT_TIME_SLOTS };
866
+ if (customSlots && typeof customSlots === "object") {
867
+ for (const [k, v] of Object.entries(customSlots)) {
868
+ if (isSafeObjectKey(k) && v && typeof v === "object") {
869
+ slotMap[k] = v;
870
+ }
871
+ }
872
+ }
873
+ return slotMap;
874
+ }
875
+ function resolveDateOnlySlotWindow(rawStart, rawSlot, source) {
876
+ if (!isSafeObjectKey(rawSlot)) return null;
877
+ const slotMap = buildSafeSlotMap(source.timeSlotMap);
878
+ const mapped = Object.hasOwn(slotMap, rawSlot) ? slotMap[rawSlot] : void 0;
879
+ if (mapped) {
880
+ return {
881
+ startDate: `${rawStart}T${mapped.start}.000Z`,
882
+ endDate: `${rawStart}T${mapped.end}.000Z`,
883
+ allDay: false
884
+ };
885
+ }
886
+ if (/^\d{2}:\d{2}(:\d{2})?$/.test(rawSlot)) {
887
+ const normalizedTime = rawSlot.length === 5 ? `${rawSlot}:00` : rawSlot;
888
+ return {
889
+ startDate: `${rawStart}T${normalizedTime}.000Z`,
890
+ endDate: null,
891
+ allDay: false
892
+ };
893
+ }
894
+ return null;
895
+ }
896
+ function resolveDateOnlyEnd(rawStart, rawEnd) {
897
+ if (typeof rawEnd === "string" && rawEnd) {
898
+ return /^\d{4}-\d{2}-\d{2}$/.test(rawEnd) ? `${rawEnd}T23:59:59.000Z` : rawEnd;
899
+ }
900
+ return `${rawStart}T23:59:59.000Z`;
901
+ }
902
+ function readOptionalDocField(doc, fieldName) {
903
+ return fieldName && isSafeObjectKey(fieldName) ? doc[fieldName] : null;
904
+ }
905
+ function resolveNonEmptyEnd(rawEnd) {
906
+ return typeof rawEnd === "string" && rawEnd ? rawEnd : null;
907
+ }
908
+ function resolveStartEndFromDocument(doc, source) {
909
+ const startField = source.fields.startDate;
910
+ const rawStart = isSafeObjectKey(startField) ? doc[startField] : void 0;
911
+ if (typeof rawStart !== "string" || !rawStart) {
912
+ return null;
913
+ }
914
+ const rawEnd = readOptionalDocField(doc, source.fields.endDate);
915
+ const rawSlot = readOptionalDocField(doc, source.fields.timeSlot);
916
+ if (/^\d{4}-\d{2}-\d{2}$/.test(rawStart)) {
917
+ const slotWindow = resolveDateOnlySlotWindow(rawStart, rawSlot, source);
918
+ if (slotWindow) return slotWindow;
919
+ return {
920
+ startDate: `${rawStart}T00:00:00.000Z`,
921
+ endDate: resolveDateOnlyEnd(rawStart, rawEnd),
922
+ allDay: source.defaultAllDay ?? true
923
+ };
924
+ }
925
+ const allDay = typeof doc.allDay === "boolean" ? doc.allDay : Boolean(source.defaultAllDay);
926
+ return {
927
+ startDate: rawStart,
928
+ endDate: resolveNonEmptyEnd(rawEnd),
929
+ allDay
930
+ };
931
+ }
932
+ const EVENT_STATUS_ALIAS_MAP = {
933
+ cancelled: "cancelled",
934
+ canceled: "cancelled",
935
+ postponed: "postponed",
936
+ pending: "postponed",
937
+ rescheduled: "rescheduled"
938
+ };
939
+ function mapEventStatus(rawStatus) {
940
+ if (typeof rawStatus !== "string") return "scheduled";
941
+ const lower = rawStatus.toLowerCase();
942
+ return Object.hasOwn(EVENT_STATUS_ALIAS_MAP, lower) ? EVENT_STATUS_ALIAS_MAP[lower] : "scheduled";
943
+ }
944
+ const API_CONTENT_TYPE_UID_RE = /^api::[a-z0-9-]+\.[a-z0-9-]+$/i;
945
+ const SENSITIVE_FIELD_NAMES = /* @__PURE__ */ new Set([
946
+ "password",
947
+ "resetpasswordtoken",
948
+ "validationtoken",
949
+ "confirmationtoken",
950
+ "checkintoken",
951
+ "checkintokenhash",
952
+ "canceltoken",
953
+ "canceltokenhash"
954
+ ]);
955
+ function isAllowedEventSourceUid(uid) {
956
+ if (typeof uid !== "string") return false;
957
+ const trimmed = uid.trim();
958
+ if (!trimmed) return false;
959
+ return trimmed === EVENT_CONTENT_TYPE_UID || API_CONTENT_TYPE_UID_RE.test(trimmed);
960
+ }
961
+ function extractCustomFields(strapi, sourceUid, doc, customFieldKeys) {
962
+ if (!Array.isArray(customFieldKeys) || customFieldKeys.length === 0) {
963
+ return void 0;
964
+ }
965
+ const schemaAttrs = strapi.contentTypes?.[sourceUid]?.attributes;
966
+ const extracted = {};
967
+ for (const key of customFieldKeys) {
968
+ if (!isSafeObjectKey(key)) continue;
969
+ if (SENSITIVE_FIELD_NAMES.has(key.toLowerCase())) continue;
970
+ const attrDef = schemaAttrs && Object.hasOwn(schemaAttrs, key) ? schemaAttrs[key] : void 0;
971
+ if (attrDef?.private === true) continue;
972
+ if (Object.hasOwn(doc, key) && doc[key] !== void 0) {
973
+ extracted[key] = doc[key];
974
+ }
975
+ }
976
+ return Object.keys(extracted).length > 0 ? extracted : void 0;
977
+ }
978
+ function buildEmptySlotRsvpStats(meta) {
979
+ const capacity = meta.capacity;
980
+ return {
981
+ eventUid: meta.eventUid,
982
+ eventDocumentId: meta.eventDocumentId,
983
+ occurrenceDate: meta.occurrenceDate,
984
+ rsvpEnabled: meta.rsvpEnabled,
985
+ capacity,
986
+ waitlistEnabled: meta.waitlistEnabled,
987
+ confirmedCount: 0,
988
+ confirmedSeats: 0,
989
+ checkedInSeats: 0,
990
+ waitlistedCount: 0,
991
+ waitlistedSeats: 0,
992
+ cancelledCount: 0,
993
+ remainingSeats: capacity !== null ? Math.max(0, capacity) : null,
994
+ isFull: capacity !== null ? capacity <= 0 : false
995
+ };
996
+ }
997
+ async function attachBatchedRsvpStats(strapi, occurrences, pendingByOccurrenceId) {
998
+ if (pendingByOccurrenceId.size === 0 || occurrences.length === 0) {
999
+ return;
1000
+ }
1001
+ const rsvpDocIdSet = /* @__PURE__ */ new Set();
1002
+ const occDateSet = /* @__PURE__ */ new Set();
1003
+ for (const meta of pendingByOccurrenceId.values()) {
1004
+ if (meta.rsvpEnabled && meta.eventDocumentId && meta.occurrenceDate) {
1005
+ rsvpDocIdSet.add(meta.eventDocumentId);
1006
+ occDateSet.add(meta.occurrenceDate);
1007
+ }
1008
+ }
1009
+ if (rsvpDocIdSet.size === 0 || occDateSet.size === 0) {
1010
+ return;
1011
+ }
1012
+ const rsvpDocIds = Array.from(rsvpDocIdSet);
1013
+ const occDates = Array.from(occDateSet);
1014
+ let rows;
1015
+ try {
1016
+ rows = await getDocumentsClient(
1017
+ strapi,
1018
+ REGISTRATION_CONTENT_TYPE_UID
1019
+ ).findMany({
1020
+ filters: {
1021
+ eventDocumentId: { $in: rsvpDocIds },
1022
+ occurrenceDate: { $in: occDates }
1023
+ },
1024
+ limit: 1e4
1025
+ });
1026
+ } catch {
1027
+ rows = [];
1028
+ }
1029
+ const buckets = /* @__PURE__ */ new Map();
1030
+ for (const row of rows) {
1031
+ const docId = typeof row.eventDocumentId === "string" ? row.eventDocumentId : "";
1032
+ const occDate = typeof row.occurrenceDate === "string" ? row.occurrenceDate : "";
1033
+ if (!docId || !occDate || !rsvpDocIdSet.has(docId)) continue;
1034
+ const slotKey = `${docId}::${occDate}`;
1035
+ const existingBucket = buckets.get(slotKey);
1036
+ if (existingBucket) {
1037
+ existingBucket.push(row);
1038
+ } else {
1039
+ buckets.set(slotKey, [row]);
1040
+ }
1041
+ }
1042
+ for (const occ of occurrences) {
1043
+ const meta = pendingByOccurrenceId.get(occ.occurrenceId);
1044
+ if (!meta || !meta.rsvpEnabled) continue;
1045
+ const slotKey = `${meta.eventDocumentId}::${meta.occurrenceDate}`;
1046
+ const slotRecords = buckets.get(slotKey);
1047
+ if (!slotRecords || slotRecords.length === 0) {
1048
+ occ.rsvp = buildEmptySlotRsvpStats(meta);
1049
+ continue;
1050
+ }
1051
+ const counts = aggregateRsvpCounts(slotRecords);
1052
+ const remainingSeats = meta.capacity !== null ? Math.max(0, meta.capacity - counts.confirmedSeats) : null;
1053
+ const isFull = meta.capacity !== null ? counts.confirmedSeats >= meta.capacity : false;
1054
+ occ.rsvp = {
1055
+ eventUid: meta.eventUid,
1056
+ eventDocumentId: meta.eventDocumentId,
1057
+ occurrenceDate: meta.occurrenceDate,
1058
+ rsvpEnabled: meta.rsvpEnabled,
1059
+ capacity: meta.capacity,
1060
+ waitlistEnabled: meta.waitlistEnabled,
1061
+ ...counts,
1062
+ remainingSeats,
1063
+ isFull
1064
+ };
1065
+ }
1066
+ }
1067
+ function maskEmail(email) {
1068
+ const [local, domain] = (email || "").split("@");
1069
+ if (!local || !domain) return "***";
1070
+ const visible = local.slice(0, Math.min(2, local.length));
1071
+ return `${visible}***@${domain}`;
1072
+ }
1073
+ function readNullablePositiveInt(val) {
1074
+ if (typeof val === "number") {
1075
+ return Number.isFinite(val) && val >= 1 ? Math.floor(val) : null;
1076
+ }
1077
+ if (typeof val === "string") {
1078
+ const trimmed = val.trim();
1079
+ if (!/^\d+$/.test(trimmed)) return null;
1080
+ const parsed = Number.parseInt(trimmed, 10);
1081
+ return Number.isSafeInteger(parsed) && parsed >= 1 ? parsed : null;
1082
+ }
1083
+ return null;
1084
+ }
1085
+ function readBooleanWithDefault(val, fallback) {
1086
+ return typeof val === "boolean" ? val : fallback;
1087
+ }
1088
+ function readMaxGuestsWithDefault(val, fallback) {
1089
+ const parsed = readNullablePositiveInt(val);
1090
+ return parsed !== null ? Math.min(50, parsed) : fallback;
1091
+ }
1092
+ function extractRawDeadline(rawDeadline) {
1093
+ if (rawDeadline === null || rawDeadline === void 0 || rawDeadline === "") {
1094
+ return null;
1095
+ }
1096
+ if (rawDeadline instanceof Date) {
1097
+ return Number.isNaN(rawDeadline.getTime()) ? "INVALID_DEADLINE" : rawDeadline.toISOString();
1098
+ }
1099
+ if (typeof rawDeadline === "string") {
1100
+ const trimmed = rawDeadline.trim();
1101
+ return trimmed ? trimmed : null;
1102
+ }
1103
+ return "INVALID_DEADLINE";
1104
+ }
1105
+ function buildResolvedTargetEvent(doc, src, eventDocumentId, settings2) {
1106
+ const startEnd = resolveStartEndFromDocument(doc, src);
1107
+ if (!startEnd) {
1108
+ return null;
1109
+ }
1110
+ const f = src.fields;
1111
+ const title = String(
1112
+ (isSafeObjectKey(f.title) ? doc[f.title] : void 0) ?? doc.title ?? "Event"
1113
+ );
1114
+ const rsvpField = f.rsvpEnabled && isSafeObjectKey(f.rsvpEnabled) ? f.rsvpEnabled : "rsvpEnabled";
1115
+ const capField = f.capacity && isSafeObjectKey(f.capacity) ? f.capacity : "capacity";
1116
+ const statusField = f.status && isSafeObjectKey(f.status) ? f.status : "eventStatus";
1117
+ const rruleField = f.rrule && isSafeObjectKey(f.rrule) ? f.rrule : void 0;
1118
+ const docTz = typeof doc.timezone === "string" && doc.timezone ? doc.timezone : settings2.defaultTimezone;
1119
+ const recurrence = parseRRuleFieldValue(
1120
+ rruleField ? doc[rruleField] : doc.recurrence,
1121
+ docTz
1122
+ );
1123
+ if (startEnd.allDay) {
1124
+ recurrence.allDay = true;
1125
+ }
1126
+ return {
1127
+ uid: src.uid,
1128
+ documentId: eventDocumentId,
1129
+ title,
1130
+ startDate: startEnd.startDate,
1131
+ endDate: startEnd.endDate,
1132
+ allDay: startEnd.allDay,
1133
+ recurrence,
1134
+ eventStatus: mapEventStatus(doc[statusField]),
1135
+ rsvpEnabled: doc[rsvpField] !== void 0 ? Boolean(doc[rsvpField]) : false,
1136
+ capacity: readNullablePositiveInt(doc[capField]),
1137
+ waitlistEnabled: readBooleanWithDefault(
1138
+ doc.waitlistEnabled,
1139
+ settings2.waitlistEnabledByDefault
1140
+ ),
1141
+ maxGuestsPerRegistration: readMaxGuestsWithDefault(
1142
+ doc.maxGuestsPerRegistration,
1143
+ settings2.defaultMaxGuestsPerRegistration
1144
+ ),
1145
+ registrationDeadline: extractRawDeadline(doc.registrationDeadline)
1146
+ };
1147
+ }
1148
+ async function resolveTargetEvent(strapi, eventDocumentId, preferredUid, options2) {
1149
+ const includeDrafts = options2?.includeDrafts ?? false;
1150
+ const settings2 = await getSettingsService(strapi).getSettings();
1151
+ const sources = (settings2.sources ?? []).filter(
1152
+ (s) => isAllowedEventSourceUid(s.uid)
1153
+ );
1154
+ const candidateSources = preferredUid ? [
1155
+ ...sources.filter((s) => s.uid === preferredUid),
1156
+ ...sources.filter((s) => s.uid !== preferredUid)
1157
+ ] : sources;
1158
+ for (const src of candidateSources) {
1159
+ try {
1160
+ const doc = await getDocumentsClient(strapi, src.uid).findOne({
1161
+ documentId: eventDocumentId,
1162
+ ...includeDrafts ? {} : { status: "published" }
1163
+ });
1164
+ if (!doc) continue;
1165
+ if (!includeDrafts && doc.publishedAt === null) {
1166
+ continue;
1167
+ }
1168
+ const resolved = buildResolvedTargetEvent(
1169
+ doc,
1170
+ src,
1171
+ eventDocumentId,
1172
+ settings2
1173
+ );
1174
+ if (resolved) return resolved;
1175
+ } catch {
1176
+ }
1177
+ }
1178
+ return null;
1179
+ }
1180
+ function readOptionalText(val) {
1181
+ return val ? String(val) : void 0;
1182
+ }
1183
+ function readStoredGuestsCount(val) {
1184
+ const parsed = readNullablePositiveInt(val);
1185
+ return parsed ?? 1;
1186
+ }
1187
+ function assertValidGuestsCount(val) {
1188
+ if (val === void 0) {
1189
+ return 1;
1190
+ }
1191
+ if (typeof val !== "number" || !Number.isInteger(val) || val < 1) {
1192
+ throw new PluginValidationError(
1193
+ "Invalid guestsCount: must be an integer >= 1."
1194
+ );
1195
+ }
1196
+ return val;
1197
+ }
1198
+ function formatRegistrationRecord(raw) {
1199
+ return {
1200
+ documentId: String(raw.documentId ?? ""),
1201
+ eventUid: String(raw.eventUid ?? EVENT_CONTENT_TYPE_UID),
1202
+ eventDocumentId: String(raw.eventDocumentId ?? ""),
1203
+ eventTitle: readOptionalText(raw.eventTitle),
1204
+ occurrenceDate: String(raw.occurrenceDate ?? ""),
1205
+ attendeeName: String(raw.attendeeName ?? ""),
1206
+ attendeeEmail: String(raw.attendeeEmail ?? ""),
1207
+ attendeePhone: readOptionalText(raw.attendeePhone),
1208
+ guestsCount: readStoredGuestsCount(raw.guestsCount),
1209
+ status: raw.status ?? "confirmed",
1210
+ checkedInAt: raw.checkedInAt ? String(raw.checkedInAt) : null,
1211
+ locale: String(raw.locale ?? "es"),
1212
+ notes: readOptionalText(raw.notes),
1213
+ createdAt: readOptionalText(raw.createdAt)
1214
+ };
1215
+ }
1216
+ function aggregateRsvpCounts(records) {
1217
+ let confirmedCount = 0;
1218
+ let confirmedSeats = 0;
1219
+ let checkedInSeats = 0;
1220
+ let waitlistedCount = 0;
1221
+ let waitlistedSeats = 0;
1222
+ let cancelledCount = 0;
1223
+ for (const rec of records) {
1224
+ const guests = readStoredGuestsCount(rec.guestsCount);
1225
+ const st = rec.status;
1226
+ if (st === "confirmed" || st === "checked_in") {
1227
+ confirmedCount++;
1228
+ confirmedSeats += guests;
1229
+ if (st === "checked_in") {
1230
+ checkedInSeats += guests;
1231
+ }
1232
+ } else if (st === "waitlisted") {
1233
+ waitlistedCount++;
1234
+ waitlistedSeats += guests;
1235
+ } else if (st === "cancelled") {
1236
+ cancelledCount++;
1237
+ }
1238
+ }
1239
+ return {
1240
+ confirmedCount,
1241
+ confirmedSeats,
1242
+ checkedInSeats,
1243
+ waitlistedCount,
1244
+ waitlistedSeats,
1245
+ cancelledCount
1246
+ };
1247
+ }
1248
+ function assertValidAttendeeIdentity(attendeeName, attendeeEmail) {
1249
+ if (!attendeeName || attendeeName.length > 160) {
1250
+ throw new PluginValidationError(
1251
+ "Invalid attendeeName: must be between 1 and 160 characters."
1252
+ );
1253
+ }
1254
+ if (!attendeeEmail || attendeeEmail.length > 254 || !isValidEmail(attendeeEmail)) {
1255
+ throw new PluginValidationError(
1256
+ "Invalid attendeeEmail: must be a valid email address."
1257
+ );
1258
+ }
1259
+ }
1260
+ function validateRegistrationInputAndEvent(input, targetEvent) {
1261
+ const attendeeName = (input.attendeeName ?? "").trim();
1262
+ const attendeeEmail = (input.attendeeEmail ?? "").trim().toLowerCase();
1263
+ assertValidAttendeeIdentity(attendeeName, attendeeEmail);
1264
+ if (targetEvent.eventStatus === "cancelled" || targetEvent.eventStatus === "postponed") {
1265
+ throw new PluginValidationError(
1266
+ `Registrations are unavailable because this event is ${targetEvent.eventStatus}.`
1267
+ );
1268
+ }
1269
+ if (!targetEvent.rsvpEnabled) {
1270
+ throw new PluginValidationError("Registrations are closed for this event.");
1271
+ }
1272
+ assertRegistrationBeforeDeadline(targetEvent.registrationDeadline);
1273
+ const guestsCount = assertValidGuestsCount(input.guestsCount);
1274
+ if (guestsCount > targetEvent.maxGuestsPerRegistration) {
1275
+ throw new PluginValidationError(
1276
+ `Maximum guests per registration is ${targetEvent.maxGuestsPerRegistration}.`
1277
+ );
1278
+ }
1279
+ return { attendeeName, attendeeEmail, guestsCount };
1280
+ }
1281
+ function assertRegistrationBeforeDeadline(deadline) {
1282
+ if (deadline === null) return;
1283
+ const deadlineMs = new Date(deadline).getTime();
1284
+ if (!Number.isFinite(deadlineMs) || Date.now() > deadlineMs) {
1285
+ throw new PluginValidationError(
1286
+ "The registration deadline for this event has passed."
1287
+ );
1288
+ }
1289
+ }
1290
+ function resolveAssignedRsvpStatus(targetEvent, confirmedSeats, guestsCount) {
1291
+ if (targetEvent.capacity !== null && confirmedSeats + guestsCount > targetEvent.capacity) {
1292
+ if (targetEvent.waitlistEnabled) {
1293
+ return "waitlisted";
1294
+ }
1295
+ throw new PluginValidationError("Event has reached maximum capacity.");
1296
+ }
1297
+ return "confirmed";
1298
+ }
1299
+ async function trySendNotificationEmail(strapi, payload) {
1300
+ try {
1301
+ const settings2 = await getSettingsService(strapi).getSettings();
1302
+ if (!settings2.sendRsvpEmails) return;
1303
+ const emailPlugin = strapi.plugin("email");
1304
+ const emailService = emailPlugin?.service("email");
1305
+ if (!emailService || typeof emailService.send !== "function") return;
1306
+ const statusHeadline = payload.promotedFromWaitlist ? "Your waitlist spot has been confirmed!" : payload.status === "waitlisted" ? "You have been added to the event waitlist" : "Your event registration is confirmed";
1307
+ const safeTitle = payload.eventTitle.replace(/[\r\n]+/g, " ").trim();
1308
+ const safeSubject = `${statusHeadline} — ${safeTitle}`.replace(/[\r\n]+/g, " ").trim();
1309
+ const safeAttendeeName = payload.attendeeName.replace(/[\r\n]+/g, " ").trim();
1310
+ await emailService.send({
1311
+ to: payload.to,
1312
+ subject: safeSubject,
1313
+ text: [
1314
+ `Hello ${safeAttendeeName},`,
1315
+ "",
1316
+ statusHeadline,
1317
+ `Event: ${safeTitle}`,
1318
+ `Date: ${payload.occurrenceDate}`,
1319
+ `Seats: ${payload.guestsCount}`,
1320
+ `Status: ${payload.status.toUpperCase()}`
1321
+ ].join("\n")
1322
+ });
1323
+ } catch {
1324
+ strapi.log?.debug?.(
1325
+ `[events] Could not send RSVP notification email to ${maskEmail(payload.to)}`
1326
+ );
1327
+ }
1328
+ }
1329
+ function buildRegistrationCreateData(args) {
1330
+ const {
1331
+ targetEvent,
1332
+ eventDocumentId,
1333
+ occurrenceDate,
1334
+ attendeeName,
1335
+ attendeeEmail,
1336
+ guestsCount,
1337
+ assignedStatus,
1338
+ tokenHash,
1339
+ input
1340
+ } = args;
1341
+ return {
1342
+ eventUid: targetEvent.uid,
1343
+ eventDocumentId,
1344
+ eventTitle: targetEvent.title,
1345
+ occurrenceDate,
1346
+ attendeeName,
1347
+ attendeeEmail,
1348
+ attendeePhone: input.attendeePhone ? String(input.attendeePhone).trim().slice(0, 50) : "",
1349
+ guestsCount,
1350
+ status: assignedStatus,
1351
+ checkInToken: tokenHash,
1352
+ locale: input.locale ? String(input.locale).slice(0, 10) : "es",
1353
+ notes: input.notes ? String(input.notes).trim().slice(0, 2e3) : "",
1354
+ metadata: sanitizePlainRecord(input.metadata)
1355
+ };
1356
+ }
1357
+ const MAX_PUBLIC_PAGE_SIZE = 100;
1358
+ function optStr(val) {
1359
+ return typeof val === "string" && val ? val : void 0;
1360
+ }
1361
+ function optQueryInt(val) {
1362
+ if (typeof val === "number" && Number.isFinite(val)) return val;
1363
+ if (typeof val === "string" && val.trim() !== "") {
1364
+ const parsed = Number(val);
1365
+ return Number.isFinite(parsed) ? parsed : void 0;
1366
+ }
1367
+ return void 0;
1368
+ }
1369
+ function clampPublicQueryPagination(q) {
1370
+ const rawPage = optQueryInt(q.page);
1371
+ const rawPageSize = optQueryInt(q.pageSize);
1372
+ const rawLimit = optQueryInt(q.limit);
1373
+ if (rawPage !== void 0 || rawPageSize !== void 0) {
1374
+ const page = rawPage !== void 0 ? Math.max(1, Math.min(1e3, Math.floor(rawPage))) : 1;
1375
+ const sizeCandidate = rawPageSize ?? rawLimit ?? 50;
1376
+ const pageSize = Math.max(
1377
+ 1,
1378
+ Math.min(MAX_PUBLIC_PAGE_SIZE, Math.floor(sizeCandidate))
1379
+ );
1380
+ return { page, pageSize };
1381
+ }
1382
+ const limit = rawLimit !== void 0 ? Math.max(1, Math.min(MAX_PUBLIC_PAGE_SIZE, Math.floor(rawLimit))) : MAX_PUBLIC_PAGE_SIZE;
1383
+ return { limit };
1384
+ }
1385
+ function unwrapRequestData(rawBody) {
1386
+ if (rawBody.data && typeof rawBody.data === "object" && !Array.isArray(rawBody.data)) {
1387
+ return rawBody.data;
1388
+ }
1389
+ return rawBody;
1390
+ }
1391
+ function buildRegistrationInput(body) {
1392
+ const guestsCount = assertValidGuestsCount(body.guestsCount);
1393
+ const metadata = body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata) ? body.metadata : void 0;
1394
+ return {
1395
+ eventUid: optStr(body.eventUid),
1396
+ occurrenceDate: optStr(body.occurrenceDate),
1397
+ attendeeName: String(body.attendeeName ?? ""),
1398
+ attendeeEmail: String(body.attendeeEmail ?? ""),
1399
+ attendeePhone: optStr(body.attendeePhone),
1400
+ guestsCount,
1401
+ locale: optStr(body.locale),
1402
+ notes: optStr(body.notes),
1403
+ metadata
1404
+ };
1405
+ }
1406
+ const contentApi$1 = ({ strapi }) => {
1407
+ return {
1408
+ async listOccurrences(ctx) {
1409
+ const q = ctx.query ?? {};
1410
+ const pagination = clampPublicQueryPagination(q);
1411
+ const data = await getOccurrenceService(strapi).getOccurrences({
1412
+ from: optStr(q.from),
1413
+ to: optStr(q.to),
1414
+ category: optStr(q.category),
1415
+ sourceId: optStr(q.sourceId),
1416
+ locale: optStr(q.locale),
1417
+ includeDrafts: false,
1418
+ ...pagination
1419
+ });
1420
+ ctx.body = { data };
1421
+ },
1422
+ async listSpotlights(ctx) {
1423
+ const q = ctx.query ?? {};
1424
+ const pagination = clampPublicQueryPagination(q);
1425
+ const data = await getOccurrenceService(strapi).getActiveSpotlights({
1426
+ date: optStr(q.date),
1427
+ category: optStr(q.category),
1428
+ sourceId: optStr(q.sourceId),
1429
+ locale: optStr(q.locale),
1430
+ includeDrafts: false,
1431
+ ...pagination
1432
+ });
1433
+ ctx.body = { data };
1434
+ },
1435
+ async getEventStats(ctx) {
1436
+ const { documentId } = ctx.params ?? {};
1437
+ const q = ctx.query ?? {};
1438
+ const data = await getRsvpService(strapi).getEventStats({
1439
+ eventUid: optStr(q.eventUid),
1440
+ eventDocumentId: String(documentId ?? ""),
1441
+ occurrenceDate: optStr(q.occurrenceDate) ?? null,
1442
+ includeDrafts: false
1443
+ });
1444
+ ctx.body = { data };
1445
+ },
1446
+ async registerAttendee(ctx) {
1447
+ try {
1448
+ const { documentId } = ctx.params ?? {};
1449
+ const body = unwrapRequestData(
1450
+ ctx.request?.body ?? {}
1451
+ );
1452
+ const data = await getRsvpService(strapi).registerAttendee(
1453
+ String(documentId ?? ""),
1454
+ buildRegistrationInput(body)
1455
+ );
1456
+ ctx.status = 201;
1457
+ ctx.body = { data };
1458
+ } catch (err) {
1459
+ return handleControllerError(
1460
+ strapi,
1461
+ ctx,
1462
+ err,
1463
+ "Registration could not be completed."
1464
+ );
1465
+ }
1466
+ },
1467
+ async cancelRegistration(ctx) {
1468
+ try {
1469
+ const { token } = ctx.params ?? {};
1470
+ const data = await getRsvpService(strapi).cancelByToken(
1471
+ String(token ?? "")
1472
+ );
1473
+ ctx.body = { data };
1474
+ } catch (err) {
1475
+ return handleControllerError(
1476
+ strapi,
1477
+ ctx,
1478
+ err,
1479
+ "Cancellation could not be processed."
1480
+ );
1481
+ }
1482
+ },
1483
+ async downloadFeedIcs(ctx) {
1484
+ const q = ctx.query ?? {};
1485
+ const ics = await getIcalService(strapi).generateFeedIcs({
1486
+ from: optStr(q.from),
1487
+ to: optStr(q.to),
1488
+ category: optStr(q.category),
1489
+ sourceId: optStr(q.sourceId),
1490
+ locale: optStr(q.locale)
1491
+ });
1492
+ ctx.set("Content-Type", "text/calendar; charset=utf-8");
1493
+ ctx.set("Content-Disposition", 'attachment; filename="calendar.ics"');
1494
+ ctx.set("X-Content-Type-Options", "nosniff");
1495
+ ctx.body = ics;
1496
+ },
1497
+ async downloadEventInviteIcs(ctx) {
1498
+ try {
1499
+ const { documentId } = ctx.params ?? {};
1500
+ const q = ctx.query ?? {};
1501
+ const safeDocId = String(documentId ?? "event").replace(/[^a-zA-Z0-9_-]/g, "") || "event";
1502
+ const ics = await getIcalService(strapi).generateEventInviteIcs({
1503
+ documentId: String(documentId ?? ""),
1504
+ occurrenceDate: optStr(q.occurrenceDate),
1505
+ locale: optStr(q.locale)
1506
+ });
1507
+ ctx.set("Content-Type", "text/calendar; charset=utf-8");
1508
+ ctx.set(
1509
+ "Content-Disposition",
1510
+ `attachment; filename="event-${safeDocId}.ics"`
1511
+ );
1512
+ ctx.set("X-Content-Type-Options", "nosniff");
1513
+ ctx.body = ics;
1514
+ } catch (err) {
1515
+ return handleControllerError(
1516
+ strapi,
1517
+ ctx,
1518
+ err,
1519
+ "Invite could not be generated."
1520
+ );
1521
+ }
1522
+ }
1523
+ };
1524
+ };
1525
+ const controllers = {
1526
+ admin: admin$1,
1527
+ "content-api": contentApi$1
1528
+ };
1529
+ const utf8Encoder$1 = new TextEncoder();
1530
+ const DAY_MS$1 = 24 * 60 * 60 * 1e3;
1531
+ function generateRawRsvpToken() {
1532
+ return crypto__default.default.randomBytes(24).toString("hex");
1533
+ }
1534
+ function hashRsvpToken(rawToken) {
1535
+ return crypto__default.default.createHash("sha256").update(rawToken, "utf8").digest("hex");
1536
+ }
1537
+ function verifyTokenHashConstantTime(candidateHash, storedValue) {
1538
+ if (typeof storedValue !== "string" || !storedValue) {
1539
+ return false;
1540
+ }
1541
+ const a = utf8Encoder$1.encode(candidateHash);
1542
+ const b = utf8Encoder$1.encode(storedValue);
1543
+ if (a.byteLength !== b.byteLength) {
1544
+ return false;
1545
+ }
1546
+ return crypto__default.default.timingSafeEqual(a, b);
1547
+ }
1548
+ async function findRegistrationByRawToken(strapi, rawToken) {
1549
+ const cleaned = (rawToken ?? "").trim();
1550
+ if (cleaned.length < 16 || cleaned.length > 256) {
1551
+ return null;
1552
+ }
1553
+ const tokenHash = hashRsvpToken(cleaned);
1554
+ const matches = await getDocumentsClient(
1555
+ strapi,
1556
+ REGISTRATION_CONTENT_TYPE_UID
1557
+ ).findMany({
1558
+ filters: { checkInToken: { $eq: tokenHash } },
1559
+ limit: 10
1560
+ });
1561
+ for (const item of matches) {
1562
+ if (verifyTokenHashConstantTime(tokenHash, item.checkInToken)) {
1563
+ return { record: item, tokenHash };
1564
+ }
1565
+ }
1566
+ return null;
1567
+ }
1568
+ const strapiSlotLocks = /* @__PURE__ */ new WeakMap();
1569
+ function getSlotLocksMap(strapi) {
1570
+ let map = strapiSlotLocks.get(strapi);
1571
+ if (!map) {
1572
+ map = /* @__PURE__ */ new Map();
1573
+ strapiSlotLocks.set(strapi, map);
1574
+ }
1575
+ return map;
1576
+ }
1577
+ function clearOccurrenceLocks(strapi) {
1578
+ const map = strapiSlotLocks.get(strapi);
1579
+ if (map) {
1580
+ map.clear();
1581
+ strapiSlotLocks.delete(strapi);
1582
+ }
1583
+ }
1584
+ async function withOccurrenceLock(strapi, eventDocumentId, occurrenceDate, fn) {
1585
+ const slotLocks = getSlotLocksMap(strapi);
1586
+ const lockKey = `${eventDocumentId}::${occurrenceDate}`;
1587
+ const previous = slotLocks.get(lockKey) ?? Promise.resolve();
1588
+ let release;
1589
+ const gate = new Promise((resolve) => {
1590
+ release = resolve;
1591
+ });
1592
+ const currentTail = previous.then(() => gate);
1593
+ slotLocks.set(lockKey, currentTail);
1594
+ await previous;
1595
+ try {
1596
+ const txFn = strapi.db?.transaction;
1597
+ if (typeof txFn === "function") {
1598
+ const runInTransaction = txFn.bind(strapi.db);
1599
+ return await runInTransaction(() => fn());
1600
+ }
1601
+ return await fn();
1602
+ } finally {
1603
+ release();
1604
+ if (slotLocks.get(lockKey) === currentTail) {
1605
+ slotLocks.delete(lockKey);
1606
+ }
1607
+ }
1608
+ }
1609
+ function parseEndTimestamp(endIso, startIso, occurrenceDate, allDay) {
1610
+ if (allDay) {
1611
+ const ms = (/* @__PURE__ */ new Date(`${occurrenceDate}T23:59:59.999Z`)).getTime();
1612
+ return Number.isFinite(ms) ? ms : null;
1613
+ }
1614
+ if (endIso) {
1615
+ const endMs = new Date(endIso).getTime();
1616
+ if (Number.isFinite(endMs)) return endMs;
1617
+ }
1618
+ const startMs = new Date(startIso).getTime();
1619
+ return Number.isFinite(startMs) ? startMs + 60 * 60 * 1e3 : null;
1620
+ }
1621
+ function assertOccurrenceNotEnded(endMs) {
1622
+ if (endMs !== null && Number.isFinite(endMs) && Date.now() > endMs) {
1623
+ throw new PluginValidationError(
1624
+ "Cannot register for an event occurrence that has already ended."
1625
+ );
1626
+ }
1627
+ }
1628
+ function assertScheduledOccurrence(targetEvent, occurrenceDate) {
1629
+ if (!isValidCalendarDateKey(occurrenceDate)) {
1630
+ throw new PluginValidationError(
1631
+ "Invalid occurrenceDate: must be a valid YYYY-MM-DD calendar date."
1632
+ );
1633
+ }
1634
+ const recurrence = targetEvent.recurrence;
1635
+ if (recurrence.exdates?.includes(occurrenceDate)) {
1636
+ throw new PluginValidationError(
1637
+ `Date ${occurrenceDate} is excluded for this event.`
1638
+ );
1639
+ }
1640
+ const directOverride = isSafeObjectKey(occurrenceDate) && recurrence.overrides && Object.hasOwn(recurrence.overrides, occurrenceDate) ? recurrence.overrides[occurrenceDate] : void 0;
1641
+ if (directOverride?.cancelled) {
1642
+ throw new PluginValidationError(
1643
+ `Occurrence on ${occurrenceDate} has been cancelled.`
1644
+ );
1645
+ }
1646
+ if (!recurrence.rrule) {
1647
+ const baseKey = toDateKey(targetEvent.startDate);
1648
+ if (occurrenceDate !== baseKey) {
1649
+ throw new PluginValidationError(
1650
+ `Date ${occurrenceDate} is not a scheduled occurrence for this event.`
1651
+ );
1652
+ }
1653
+ const effStart = directOverride?.startDate || targetEvent.startDate;
1654
+ const effEnd = directOverride?.endDate || targetEvent.endDate;
1655
+ return parseEndTimestamp(
1656
+ effEnd,
1657
+ effStart,
1658
+ occurrenceDate,
1659
+ targetEvent.allDay
1660
+ );
1661
+ }
1662
+ const anchorMs = (/* @__PURE__ */ new Date(`${occurrenceDate}T00:00:00.000Z`)).getTime();
1663
+ const slots = expandRuleSlots({
1664
+ baseStartDate: targetEvent.startDate,
1665
+ baseEndDate: targetEvent.endDate,
1666
+ recurrence,
1667
+ windowStart: new Date(anchorMs - 31 * DAY_MS$1).toISOString(),
1668
+ windowEnd: new Date(anchorMs + 32 * DAY_MS$1 - 1).toISOString(),
1669
+ maxInstances: 1e4
1670
+ });
1671
+ const matched = slots.find(
1672
+ (s) => s.occurrenceDate === occurrenceDate || toDateKey(s.startDate) === occurrenceDate
1673
+ );
1674
+ if (!matched) {
1675
+ throw new PluginValidationError(
1676
+ `Date ${occurrenceDate} is not a scheduled occurrence for this event.`
1677
+ );
1678
+ }
1679
+ return parseEndTimestamp(
1680
+ matched.endDate,
1681
+ matched.startDate,
1682
+ occurrenceDate,
1683
+ Boolean(recurrence.allDay || targetEvent.allDay)
1684
+ );
1685
+ }
1686
+ function resolveValidatedOccurrenceDate(inputDate, targetEvent) {
1687
+ if (inputDate !== void 0 && inputDate !== "") {
1688
+ const trimmed = String(inputDate).trim();
1689
+ const endMs2 = assertScheduledOccurrence(targetEvent, trimmed);
1690
+ assertOccurrenceNotEnded(endMs2);
1691
+ return trimmed;
1692
+ }
1693
+ if (targetEvent.recurrence.rrule) {
1694
+ const nowMs = Date.now();
1695
+ const isAllDay = Boolean(
1696
+ targetEvent.recurrence.allDay || targetEvent.allDay
1697
+ );
1698
+ const upcomingSlots = expandRuleSlots({
1699
+ baseStartDate: targetEvent.startDate,
1700
+ baseEndDate: targetEvent.endDate,
1701
+ recurrence: targetEvent.recurrence,
1702
+ windowStart: new Date(nowMs - DAY_MS$1).toISOString(),
1703
+ windowEnd: new Date(nowMs + 366 * DAY_MS$1).toISOString(),
1704
+ maxInstances: 1e3
1705
+ });
1706
+ for (const slot of upcomingSlots) {
1707
+ const slotEndMs = parseEndTimestamp(
1708
+ slot.endDate,
1709
+ slot.startDate,
1710
+ slot.occurrenceDate,
1711
+ isAllDay
1712
+ );
1713
+ if (slotEndMs === null || !Number.isFinite(slotEndMs) || slotEndMs > nowMs) {
1714
+ return slot.occurrenceDate;
1715
+ }
1716
+ }
1717
+ throw new PluginValidationError(
1718
+ "Cannot register for an event occurrence that has already ended."
1719
+ );
1720
+ }
1721
+ const fallbackKey = toDateKey(targetEvent.startDate);
1722
+ const endMs = assertScheduledOccurrence(targetEvent, fallbackKey);
1723
+ assertOccurrenceNotEnded(endMs);
1724
+ return fallbackKey;
1725
+ }
1726
+ const destroy = ({ strapi }) => {
1727
+ clearOccurrenceLocks(strapi);
1728
+ };
1729
+ const middlewares = {};
1730
+ const policies = {};
1731
+ function mergeAttributeIntoTarget(target, attrName, attrDef) {
1732
+ if (!target || typeof target !== "object" || !isSafeObjectKey(attrName)) {
1733
+ return;
1734
+ }
1735
+ const prev = Object.hasOwn(target, attrName) ? target[attrName] : void 0;
1736
+ const safeDef = {};
1737
+ for (const [k, v] of Object.entries(attrDef)) {
1738
+ if (isSafeObjectKey(k)) {
1739
+ safeDef[k] = v;
1740
+ }
1741
+ }
1742
+ target[attrName] = {
1743
+ ...prev && typeof prev === "object" ? prev : {},
1744
+ ...safeDef
1745
+ };
1746
+ }
1747
+ function applyAttributeExtensions(contentType, extraAttributes) {
1748
+ if (!contentType || !extraAttributes || typeof extraAttributes !== "object") {
1749
+ return;
1750
+ }
1751
+ for (const [attrName, attrDef] of Object.entries(extraAttributes)) {
1752
+ if (!isSafeObjectKey(attrName) || !attrDef || typeof attrDef !== "object" || Array.isArray(attrDef)) {
1753
+ continue;
1754
+ }
1755
+ mergeAttributeIntoTarget(contentType.attributes, attrName, attrDef);
1756
+ mergeAttributeIntoTarget(
1757
+ contentType.__schema__?.attributes,
1758
+ attrName,
1759
+ attrDef
1760
+ );
1761
+ }
1762
+ }
1763
+ function buildCategoryEnumAttribute(categoryColors) {
1764
+ const colorKeys = Object.keys(categoryColors).filter(isSafeObjectKey);
1765
+ const enumValues = Array.from(/* @__PURE__ */ new Set(["general", ...colorKeys]));
1766
+ return {
1767
+ type: "enumeration",
1768
+ enum: enumValues,
1769
+ default: enumValues.includes("general") ? "general" : enumValues[0],
1770
+ pluginOptions: {
1771
+ i18n: {
1772
+ localized: false
1773
+ }
1774
+ }
1775
+ };
1776
+ }
1777
+ const register = ({ strapi }) => {
1778
+ strapi.customFields.register({
1779
+ name: "rrule",
1780
+ plugin: PLUGIN_ID,
1781
+ type: "json",
1782
+ inputSize: {
1783
+ default: 12,
1784
+ isResizable: true
1785
+ }
1786
+ });
1787
+ const userConfig = strapi.config?.get?.(`plugin::${PLUGIN_ID}`) ?? {};
1788
+ const strapiInternal = strapi;
1789
+ const getCt = (uid) => strapiInternal.contentTypes?.[uid] ?? strapiInternal.contentType?.(uid);
1790
+ const eventSourceWithCategories = userConfig.sources?.find(
1791
+ (s) => s?.uid === EVENT_CONTENT_TYPE_UID && Boolean(s.categoryColors && Object.keys(s.categoryColors).length > 0)
1792
+ );
1793
+ const eventExtensions = {};
1794
+ if (userConfig.schemaExtensions?.event && typeof userConfig.schemaExtensions.event === "object") {
1795
+ for (const [k, v] of Object.entries(userConfig.schemaExtensions.event)) {
1796
+ if (isSafeObjectKey(k) && v && typeof v === "object") {
1797
+ eventExtensions[k] = v;
1798
+ }
1799
+ }
1800
+ }
1801
+ if (eventSourceWithCategories?.categoryColors && !Object.hasOwn(eventExtensions, "category")) {
1802
+ eventExtensions.category = buildCategoryEnumAttribute(
1803
+ eventSourceWithCategories.categoryColors
1804
+ );
1805
+ }
1806
+ if (Object.keys(eventExtensions).length > 0) {
1807
+ applyAttributeExtensions(getCt(EVENT_CONTENT_TYPE_UID), eventExtensions);
1808
+ }
1809
+ if (userConfig.schemaExtensions?.registration) {
1810
+ applyAttributeExtensions(
1811
+ getCt(REGISTRATION_CONTENT_TYPE_UID),
1812
+ userConfig.schemaExtensions.registration
1813
+ );
1814
+ }
1815
+ };
1816
+ const readPolicies = [
1817
+ "admin::isAuthenticatedAdmin",
1818
+ {
1819
+ name: "admin::hasPermissions",
1820
+ config: { actions: [`plugin::${PLUGIN_ID}.read`] }
1821
+ }
1822
+ ];
1823
+ const updatePolicies = [
1824
+ "admin::isAuthenticatedAdmin",
1825
+ {
1826
+ name: "admin::hasPermissions",
1827
+ config: { actions: [`plugin::${PLUGIN_ID}.update`] }
1828
+ }
1829
+ ];
1830
+ const settingsPolicies = [
1831
+ "admin::isAuthenticatedAdmin",
1832
+ {
1833
+ name: "admin::hasPermissions",
1834
+ config: { actions: [`plugin::${PLUGIN_ID}.settings.update`] }
1835
+ }
1836
+ ];
1837
+ const admin = {
1838
+ type: "admin",
1839
+ routes: [
1840
+ {
1841
+ method: "GET",
1842
+ path: "/settings",
1843
+ handler: "admin.getSettings",
1844
+ config: {
1845
+ policies: readPolicies
1846
+ }
1847
+ },
1848
+ {
1849
+ method: "PUT",
1850
+ path: "/settings",
1851
+ handler: "admin.updateSettings",
1852
+ config: {
1853
+ policies: settingsPolicies
1854
+ }
1855
+ },
1856
+ {
1857
+ method: "GET",
1858
+ path: "/calendar",
1859
+ handler: "admin.getCalendar",
1860
+ config: {
1861
+ policies: readPolicies
1862
+ }
1863
+ },
1864
+ {
1865
+ method: "POST",
1866
+ path: "/calendar/reschedule",
1867
+ handler: "admin.reschedule",
1868
+ config: {
1869
+ policies: updatePolicies
1870
+ }
1871
+ },
1872
+ {
1873
+ method: "POST",
1874
+ path: "/calendar/quick-create",
1875
+ handler: "admin.quickCreate",
1876
+ config: {
1877
+ policies: updatePolicies
1878
+ }
1879
+ },
1880
+ {
1881
+ method: "GET",
1882
+ path: "/stats/:documentId",
1883
+ handler: "admin.getEventStats",
1884
+ config: {
1885
+ policies: readPolicies
1886
+ }
1887
+ },
1888
+ {
1889
+ method: "GET",
1890
+ path: "/registrations",
1891
+ handler: "admin.listRegistrations",
1892
+ config: {
1893
+ policies: readPolicies
1894
+ }
1895
+ },
1896
+ {
1897
+ method: "GET",
1898
+ path: "/registrations/export.csv",
1899
+ handler: "admin.exportRegistrationsCsv",
1900
+ config: {
1901
+ policies: readPolicies
1902
+ }
1903
+ },
1904
+ {
1905
+ method: "POST",
1906
+ path: "/registrations/:documentId/status",
1907
+ handler: "admin.updateRegistrationStatus",
1908
+ config: {
1909
+ policies: updatePolicies
1910
+ }
1911
+ },
1912
+ {
1913
+ method: "POST",
1914
+ path: "/registrations/:documentId/check-in",
1915
+ handler: "admin.checkIn",
1916
+ config: {
1917
+ policies: updatePolicies
1918
+ }
1919
+ }
1920
+ ]
1921
+ };
1922
+ const contentApi = {
1923
+ type: "content-api",
1924
+ routes: [
1925
+ {
1926
+ method: "GET",
1927
+ path: "/occurrences",
1928
+ handler: "content-api.listOccurrences",
1929
+ config: {
1930
+ policies: []
1931
+ }
1932
+ },
1933
+ {
1934
+ method: "GET",
1935
+ path: "/spotlights",
1936
+ handler: "content-api.listSpotlights",
1937
+ config: {
1938
+ policies: []
1939
+ }
1940
+ },
1941
+ {
1942
+ method: "GET",
1943
+ path: "/feed.ics",
1944
+ handler: "content-api.downloadFeedIcs",
1945
+ config: {
1946
+ policies: []
1947
+ }
1948
+ },
1949
+ {
1950
+ method: "GET",
1951
+ path: "/:documentId/invite.ics",
1952
+ handler: "content-api.downloadEventInviteIcs",
1953
+ config: {
1954
+ policies: []
1955
+ }
1956
+ },
1957
+ {
1958
+ method: "GET",
1959
+ path: "/:documentId/stats",
1960
+ handler: "content-api.getEventStats",
1961
+ config: {
1962
+ policies: []
1963
+ }
1964
+ },
1965
+ {
1966
+ method: "POST",
1967
+ path: "/:documentId/register",
1968
+ handler: "content-api.registerAttendee",
1969
+ config: {
1970
+ policies: []
1971
+ }
1972
+ },
1973
+ {
1974
+ method: "POST",
1975
+ path: "/registrations/:token/cancel",
1976
+ handler: "content-api.cancelRegistration",
1977
+ config: {
1978
+ policies: []
1979
+ }
1980
+ }
1981
+ ]
1982
+ };
1983
+ const routes = {
1984
+ admin,
1985
+ "content-api": contentApi
1986
+ };
1987
+ const utf8Encoder = new TextEncoder();
1988
+ function toIcsTimestamp(isoString, allDay = false) {
1989
+ const d = new Date(isoString);
1990
+ if (Number.isNaN(d.getTime())) {
1991
+ return "";
1992
+ }
1993
+ const iso = d.toISOString();
1994
+ if (allDay) {
1995
+ return iso.slice(0, 10).replace(/-/g, "");
1996
+ }
1997
+ return iso.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
1998
+ }
1999
+ function resolveExclusiveAllDayEndStamp(startDate, endDate, dtStartStamp) {
2000
+ const rawEndStamp = toIcsTimestamp(endDate, true);
2001
+ if (rawEndStamp && rawEndStamp > dtStartStamp) {
2002
+ return rawEndStamp;
2003
+ }
2004
+ const startObj = new Date(startDate);
2005
+ if (Number.isNaN(startObj.getTime())) {
2006
+ return rawEndStamp;
2007
+ }
2008
+ const nextDay = new Date(
2009
+ Date.UTC(
2010
+ startObj.getUTCFullYear(),
2011
+ startObj.getUTCMonth(),
2012
+ startObj.getUTCDate() + 1
2013
+ )
2014
+ );
2015
+ return nextDay.toISOString().slice(0, 10).replace(/-/g, "");
2016
+ }
2017
+ function escapeIcsText(value) {
2018
+ if (!value) return "";
2019
+ return String(value).replace(/\\/g, "\\\\").replace(/\r\n|\r|\n/g, "\\n").replace(/;/g, "\\;").replace(/,/g, "\\,");
2020
+ }
2021
+ function foldIcsLine(line) {
2022
+ if (utf8Encoder.encode(line).byteLength <= 75) {
2023
+ return line;
2024
+ }
2025
+ const chunks = [];
2026
+ let currentChunk = "";
2027
+ let currentBytes = 0;
2028
+ let maxBytes = 75;
2029
+ for (const codePoint of line) {
2030
+ const byteLen = utf8Encoder.encode(codePoint).byteLength;
2031
+ if (currentBytes + byteLen > maxBytes && currentChunk.length > 0) {
2032
+ chunks.push(chunks.length === 0 ? currentChunk : ` ${currentChunk}`);
2033
+ currentChunk = codePoint;
2034
+ currentBytes = byteLen;
2035
+ maxBytes = 74;
2036
+ } else {
2037
+ currentChunk += codePoint;
2038
+ currentBytes += byteLen;
2039
+ }
2040
+ }
2041
+ if (currentChunk.length > 0) {
2042
+ chunks.push(chunks.length === 0 ? currentChunk : ` ${currentChunk}`);
2043
+ }
2044
+ return chunks.join("\r\n");
2045
+ }
2046
+ function buildCalendarLinks(params) {
2047
+ const {
2048
+ documentId,
2049
+ title,
2050
+ startDate,
2051
+ endDate,
2052
+ allDay = false,
2053
+ summary = "",
2054
+ location = "",
2055
+ occurrenceDate
2056
+ } = params;
2057
+ const icsStart = toIcsTimestamp(startDate, allDay);
2058
+ const icsEnd = allDay ? resolveExclusiveAllDayEndStamp(startDate, endDate, icsStart) : toIcsTimestamp(endDate, false);
2059
+ const googleParams = new URLSearchParams({
2060
+ action: "TEMPLATE",
2061
+ text: title,
2062
+ dates: `${icsStart}/${icsEnd}`,
2063
+ details: summary,
2064
+ location
2065
+ });
2066
+ const outlookParams = new URLSearchParams({
2067
+ path: "/calendar/action/compose",
2068
+ rru: "addevent",
2069
+ subject: title,
2070
+ startdt: startDate,
2071
+ enddt: endDate,
2072
+ body: summary,
2073
+ location,
2074
+ allday: allDay ? "true" : "false"
2075
+ });
2076
+ const yahooParams = new URLSearchParams({
2077
+ v: "60",
2078
+ title,
2079
+ st: icsStart,
2080
+ et: icsEnd,
2081
+ desc: summary,
2082
+ in_loc: location
2083
+ });
2084
+ const querySuffix = occurrenceDate ? `?occurrenceDate=${encodeURIComponent(occurrenceDate)}` : "";
2085
+ return {
2086
+ google: `https://calendar.google.com/calendar/render?${googleParams.toString()}`,
2087
+ outlook: `https://outlook.live.com/calendar/0/deeplink/compose?${outlookParams.toString()}`,
2088
+ yahoo: `https://calendar.yahoo.com/?${yahooParams.toString()}`,
2089
+ icsPath: `/api/events/${encodeURIComponent(documentId)}/invite.ics${querySuffix}`
2090
+ };
2091
+ }
2092
+ const ICS_STATUS_MAP = {
2093
+ cancelled: "CANCELLED",
2094
+ postponed: "TENTATIVE"
2095
+ };
2096
+ function resolveIcsStatus(status) {
2097
+ return Object.hasOwn(ICS_STATUS_MAP, status) ? ICS_STATUS_MAP[status] : "CONFIRMED";
2098
+ }
2099
+ function appendVEventDateLines(lines, occ) {
2100
+ const isAllDay = Boolean(occ.allDay);
2101
+ const prefix = isAllDay ? ";VALUE=DATE" : "";
2102
+ const startStamp = toIcsTimestamp(occ.startDate, isAllDay);
2103
+ const endStamp = isAllDay ? resolveExclusiveAllDayEndStamp(occ.startDate, occ.endDate, startStamp) : toIcsTimestamp(occ.endDate, false);
2104
+ lines.push(`DTSTART${prefix}:${startStamp}`);
2105
+ lines.push(`DTEND${prefix}:${endStamp}`);
2106
+ }
2107
+ function appendVEventOptionalLines(lines, occ) {
2108
+ if (occ.summary) {
2109
+ lines.push(foldIcsLine(`DESCRIPTION:${escapeIcsText(occ.summary)}`));
2110
+ }
2111
+ if (occ.location) {
2112
+ lines.push(foldIcsLine(`LOCATION:${escapeIcsText(occ.location)}`));
2113
+ }
2114
+ if (occ.virtualUrl) {
2115
+ lines.push(foldIcsLine(`URL:${escapeIcsText(occ.virtualUrl)}`));
2116
+ }
2117
+ if (occ.isRecurring && occ.rrule) {
2118
+ const normalized = normalizeRRuleString(occ.rrule).replace(/[\r\n]+/g, "");
2119
+ if (normalized) {
2120
+ lines.push(foldIcsLine(`RRULE:${normalized}`));
2121
+ }
2122
+ }
2123
+ }
2124
+ function generateIcsCalendar(occurrences, feedTitle = "Events Calendar") {
2125
+ const nowStamp = toIcsTimestamp((/* @__PURE__ */ new Date()).toISOString(), false);
2126
+ const lines = [
2127
+ "BEGIN:VCALENDAR",
2128
+ "VERSION:2.0",
2129
+ "PRODID:-//Strapi v5//strapi-plugin-events//EN",
2130
+ "CALSCALE:GREGORIAN",
2131
+ "METHOD:PUBLISH",
2132
+ foldIcsLine(`X-WR-CALNAME:${escapeIcsText(feedTitle)}`)
2133
+ ];
2134
+ for (const occ of occurrences) {
2135
+ const uid = `${occ.documentId}-${occ.occurrenceDate}@strapi-plugin-events`;
2136
+ lines.push("BEGIN:VEVENT");
2137
+ lines.push(foldIcsLine(`UID:${escapeIcsText(uid)}`));
2138
+ lines.push(`DTSTAMP:${nowStamp}`);
2139
+ appendVEventDateLines(lines, occ);
2140
+ lines.push(foldIcsLine(`SUMMARY:${escapeIcsText(occ.title)}`));
2141
+ appendVEventOptionalLines(lines, occ);
2142
+ lines.push(`STATUS:${resolveIcsStatus(occ.status)}`);
2143
+ lines.push("END:VEVENT");
2144
+ }
2145
+ lines.push("END:VCALENDAR");
2146
+ return `${lines.join("\r\n")}\r
2147
+ `;
2148
+ }
2149
+ function sanitizeCsvCell(raw) {
2150
+ if (raw === null || raw === void 0) {
2151
+ return '""';
2152
+ }
2153
+ let text = String(raw);
2154
+ if (/^[\s\u0085\u2028\u2029]*[=+\-@|]|^[\t\r\n]/.test(text)) {
2155
+ text = `'${text}`;
2156
+ }
2157
+ const escaped = text.replace(/"/g, '""');
2158
+ return `"${escaped}"`;
2159
+ }
2160
+ function generateRegistrationsCsv(records) {
2161
+ const headers = [
2162
+ "Document ID",
2163
+ "Event UID",
2164
+ "Event Document ID",
2165
+ "Event Title",
2166
+ "Occurrence Date",
2167
+ "Attendee Name",
2168
+ "Attendee Email",
2169
+ "Attendee Phone",
2170
+ "Guests Count",
2171
+ "Status",
2172
+ "Checked In At",
2173
+ "Locale",
2174
+ "Notes",
2175
+ "Created At"
2176
+ ];
2177
+ const rows = [headers.map(sanitizeCsvCell).join(",")];
2178
+ for (const r of records) {
2179
+ rows.push(
2180
+ [
2181
+ r.documentId,
2182
+ r.eventUid,
2183
+ r.eventDocumentId,
2184
+ r.eventTitle ?? "",
2185
+ r.occurrenceDate,
2186
+ r.attendeeName,
2187
+ r.attendeeEmail,
2188
+ r.attendeePhone ?? "",
2189
+ r.guestsCount,
2190
+ r.status,
2191
+ r.checkedInAt ?? "",
2192
+ r.locale,
2193
+ r.notes ?? "",
2194
+ r.createdAt ?? ""
2195
+ ].map(sanitizeCsvCell).join(",")
2196
+ );
2197
+ }
2198
+ return `${rows.join("\r\n")}\r
2199
+ `;
2200
+ }
2201
+ const DAY_MS = 24 * 60 * 60 * 1e3;
2202
+ const ical = ({ strapi }) => {
2203
+ return {
2204
+ async generateFeedIcs(params) {
2205
+ const settings2 = await getSettingsService(strapi).getSettings();
2206
+ const occurrences = await getOccurrenceService(strapi).getOccurrences({
2207
+ from: params?.from,
2208
+ to: params?.to,
2209
+ category: params?.category,
2210
+ sourceId: params?.sourceId,
2211
+ locale: params?.locale,
2212
+ includeDrafts: false
2213
+ });
2214
+ return generateIcsCalendar(occurrences, settings2.feedTitle);
2215
+ },
2216
+ async generateEventInviteIcs(params) {
2217
+ const docId = (params.documentId ?? "").trim();
2218
+ if (!docId) {
2219
+ throw new PluginValidationError("Event documentId is required.");
2220
+ }
2221
+ const settings2 = await getSettingsService(strapi).getSettings();
2222
+ let from;
2223
+ let to;
2224
+ if (params.occurrenceDate) {
2225
+ if (!isValidCalendarDateKey(params.occurrenceDate)) {
2226
+ throw new PluginValidationError(
2227
+ "Invalid occurrenceDate: must be a valid YYYY-MM-DD calendar date."
2228
+ );
2229
+ }
2230
+ const anchorMs = (/* @__PURE__ */ new Date(
2231
+ `${params.occurrenceDate}T00:00:00.000Z`
2232
+ )).getTime();
2233
+ from = new Date(anchorMs - 2 * DAY_MS).toISOString();
2234
+ to = new Date(anchorMs + 3 * DAY_MS).toISOString();
2235
+ } else {
2236
+ const nowMs = Date.now();
2237
+ from = new Date(nowMs - 30 * DAY_MS).toISOString();
2238
+ to = new Date(nowMs + 335 * DAY_MS).toISOString();
2239
+ }
2240
+ const occurrences = await getOccurrenceService(strapi).getOccurrences({
2241
+ documentId: docId,
2242
+ from,
2243
+ to,
2244
+ locale: params.locale,
2245
+ includeDrafts: false
2246
+ });
2247
+ const filtered = params.occurrenceDate ? occurrences.filter((o) => o.occurrenceDate === params.occurrenceDate) : occurrences.slice(0, 1);
2248
+ return generateIcsCalendar(filtered, settings2.feedTitle);
2249
+ }
2250
+ };
2251
+ };
2252
+ const MAX_PUBLIC_WINDOW_MS = 366 * 24 * 60 * 60 * 1e3;
2253
+ const MAX_ADMIN_WINDOW_MS = 5 * 366 * 24 * 60 * 60 * 1e3;
2254
+ function assertValidIsoDateTime(raw, fieldName) {
2255
+ if (typeof raw !== "string" || !raw.trim()) {
2256
+ throw new PluginValidationError(
2257
+ `Invalid ${fieldName}: must be a non-empty ISO 8601 date or datetime.`
2258
+ );
2259
+ }
2260
+ const trimmed = raw.trim();
2261
+ if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
2262
+ if (!isValidCalendarDateKey(trimmed)) {
2263
+ throw new PluginValidationError(
2264
+ `Invalid ${fieldName}: "${trimmed}" is not a valid calendar date.`
2265
+ );
2266
+ }
2267
+ return (/* @__PURE__ */ new Date(`${trimmed}T00:00:00.000Z`)).getTime();
2268
+ }
2269
+ const ms = new Date(trimmed).getTime();
2270
+ if (!Number.isFinite(ms)) {
2271
+ throw new PluginValidationError(
2272
+ `Invalid ${fieldName}: must be a valid ISO 8601 datetime.`
2273
+ );
2274
+ }
2275
+ return ms;
2276
+ }
2277
+ function isValidIso(val) {
2278
+ if (!val || typeof val !== "string") return false;
2279
+ const trimmed = val.trim();
2280
+ if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
2281
+ return isValidCalendarDateKey(trimmed);
2282
+ }
2283
+ return Number.isFinite(new Date(trimmed).getTime());
2284
+ }
2285
+ function resolveOccurrenceWindow(params) {
2286
+ const now = /* @__PURE__ */ new Date();
2287
+ const defaultFromMs = new Date(
2288
+ now.getFullYear(),
2289
+ now.getMonth() - 1,
2290
+ 1
2291
+ ).getTime();
2292
+ const defaultToMs = new Date(
2293
+ now.getFullYear(),
2294
+ now.getMonth() + 3,
2295
+ 0,
2296
+ 23,
2297
+ 59,
2298
+ 59
2299
+ ).getTime();
2300
+ const startMs = isValidIso(params.from) ? new Date(String(params.from)).getTime() : defaultFromMs;
2301
+ const rawEndMs = isValidIso(params.to) ? new Date(String(params.to)).getTime() : defaultToMs;
2302
+ if (rawEndMs < startMs) {
2303
+ return {
2304
+ windowStart: new Date(startMs).toISOString(),
2305
+ windowEnd: new Date(rawEndMs).toISOString(),
2306
+ isEmptyWindow: true
2307
+ };
2308
+ }
2309
+ const maxSpanMs = params.includeDrafts ? MAX_ADMIN_WINDOW_MS : MAX_PUBLIC_WINDOW_MS;
2310
+ const clampedEndMs = rawEndMs - startMs > maxSpanMs ? startMs + maxSpanMs : rawEndMs;
2311
+ return {
2312
+ windowStart: new Date(startMs).toISOString(),
2313
+ windowEnd: new Date(clampedEndMs).toISOString(),
2314
+ isEmptyWindow: false
2315
+ };
2316
+ }
2317
+ function resolveQuickCreateSource(settings2, sourceId) {
2318
+ const sources = (settings2.sources ?? []).filter(
2319
+ (s) => isAllowedEventSourceUid(s.uid)
2320
+ );
2321
+ const chosen = sources.find((s) => s.id === sourceId) ?? sources.find((s) => s.uid === EVENT_CONTENT_TYPE_UID) ?? sources[0];
2322
+ if (!chosen) {
2323
+ throw new PluginValidationError("No allowed event source is configured.");
2324
+ }
2325
+ return chosen;
2326
+ }
2327
+ function applyQuickCreateRRuleAndRsvp(data, source, settings2, params) {
2328
+ const rruleField = source.fields.rrule;
2329
+ const cleanRRule = normalizeRRuleString(params.rrule);
2330
+ if (rruleField && isSafeObjectKey(rruleField) && cleanRRule) {
2331
+ data[rruleField] = {
2332
+ rrule: cleanRRule,
2333
+ timezone: settings2.defaultTimezone,
2334
+ allDay: false,
2335
+ exdates: [],
2336
+ overrides: {}
2337
+ };
2338
+ }
2339
+ if (source.fields.rsvpEnabled && isSafeObjectKey(source.fields.rsvpEnabled) && typeof params.rsvpEnabled === "boolean") {
2340
+ data[source.fields.rsvpEnabled] = params.rsvpEnabled;
2341
+ }
2342
+ if (source.fields.capacity && isSafeObjectKey(source.fields.capacity) && typeof params.capacity === "number" && Number.isFinite(params.capacity) && params.capacity >= 1) {
2343
+ data[source.fields.capacity] = Math.floor(params.capacity);
2344
+ }
2345
+ }
2346
+ function buildQuickCreatePayload(source, settings2, params) {
2347
+ const title = (params.title ?? "").trim();
2348
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
2349
+ const data = {
2350
+ [source.fields.title]: title,
2351
+ [source.fields.startDate]: params.startDate
2352
+ };
2353
+ const optionalStrings = [
2354
+ [source.fields.endDate, params.endDate],
2355
+ [source.fields.location, params.location],
2356
+ [source.fields.category, params.category],
2357
+ [source.fields.summary, params.summary]
2358
+ ];
2359
+ for (const [fieldKey, val] of optionalStrings) {
2360
+ if (fieldKey && isSafeObjectKey(fieldKey) && val) {
2361
+ data[fieldKey] = val;
2362
+ }
2363
+ }
2364
+ if (source.fields.slug && isSafeObjectKey(source.fields.slug)) {
2365
+ data[source.fields.slug] = `${slug}-${Date.now().toString(36)}`;
2366
+ }
2367
+ applyQuickCreateRRuleAndRsvp(data, source, settings2, params);
2368
+ return data;
2369
+ }
2370
+ function resolveEffectiveQueryLocale(paramLoc, defaultLoc) {
2371
+ if (paramLoc && paramLoc !== "auto") return paramLoc;
2372
+ return defaultLoc && defaultLoc !== "auto" ? defaultLoc : void 0;
2373
+ }
2374
+ function buildSourceQueryOptions(source, params, settings2) {
2375
+ const posterField = source.fields.poster;
2376
+ const queryOpts = {
2377
+ limit: 500,
2378
+ ...posterField ? { populate: [posterField] } : {}
2379
+ };
2380
+ if (params.documentId) {
2381
+ queryOpts.filters = { documentId: { $eq: params.documentId } };
2382
+ }
2383
+ const loc = resolveEffectiveQueryLocale(
2384
+ params.locale,
2385
+ settings2.defaultLocale
2386
+ );
2387
+ if (loc) queryOpts.locale = loc;
2388
+ if (!params.includeDrafts) queryOpts.status = "published";
2389
+ return queryOpts;
2390
+ }
2391
+ function filterOutExplicitDrafts(docs, includeDrafts) {
2392
+ if (includeDrafts) return docs;
2393
+ return docs.filter((doc) => doc.publishedAt !== null);
2394
+ }
2395
+ async function fetchDocumentsForSource(strapi, source, params, settings2) {
2396
+ if (!isAllowedEventSourceUid(source.uid)) {
2397
+ return [];
2398
+ }
2399
+ const client = getDocumentsClient(strapi, source.uid);
2400
+ try {
2401
+ const queryOpts = buildSourceQueryOptions(source, params, settings2);
2402
+ const docs = await client.findMany(queryOpts);
2403
+ if (docs.length === 0 && queryOpts.locale) {
2404
+ const { locale: _ignoredLocale, ...withoutLocaleOpts } = queryOpts;
2405
+ const fallbackLocaleDocs = await client.findMany(withoutLocaleOpts);
2406
+ return filterOutExplicitDrafts(fallbackLocaleDocs, params.includeDrafts);
2407
+ }
2408
+ return filterOutExplicitDrafts(docs, params.includeDrafts);
2409
+ } catch {
2410
+ return fetchDocumentsFallback(client, params);
2411
+ }
2412
+ }
2413
+ async function fetchDocumentsFallback(client, params) {
2414
+ try {
2415
+ const fallbackOpts = { limit: 500 };
2416
+ if (params.documentId) {
2417
+ fallbackOpts.filters = { documentId: { $eq: params.documentId } };
2418
+ }
2419
+ if (!params.includeDrafts) {
2420
+ fallbackOpts.status = "published";
2421
+ }
2422
+ const docs = await client.findMany(fallbackOpts);
2423
+ return filterOutExplicitDrafts(docs, params.includeDrafts);
2424
+ } catch {
2425
+ return [];
2426
+ }
2427
+ }
2428
+ function resolveDocColor(doc, source, categoryVal) {
2429
+ const colors = source.categoryColors;
2430
+ const categoryColor = categoryVal && isSafeObjectKey(categoryVal) && colors && Object.hasOwn(colors, categoryVal) ? colors[categoryVal] : void 0;
2431
+ if (categoryColor) return categoryColor;
2432
+ if (typeof doc.color === "string" && doc.color.startsWith("#")) {
2433
+ return doc.color;
2434
+ }
2435
+ return source.color;
2436
+ }
2437
+ const readOptionalFieldString = (doc, key) => key && isSafeObjectKey(key) ? String(doc[key] ?? "") : "";
2438
+ function extractDocMetadata(strapi, doc, source, settings2, docId, baseDateKey) {
2439
+ const f = source.fields;
2440
+ const categoryVal = readOptionalFieldString(doc, f.category);
2441
+ const baseTitle = String(
2442
+ (isSafeObjectKey(f.title) ? doc[f.title] : void 0) ?? doc.title ?? "Untitled"
2443
+ );
2444
+ const slugVal = readOptionalFieldString(doc, f.slug);
2445
+ const rsvpField = f.rsvpEnabled && isSafeObjectKey(f.rsvpEnabled) ? f.rsvpEnabled : "rsvpEnabled";
2446
+ const capField = f.capacity && isSafeObjectKey(f.capacity) ? f.capacity : "capacity";
2447
+ return {
2448
+ docId,
2449
+ baseTitle,
2450
+ baseLocation: readOptionalFieldString(doc, f.location),
2451
+ summary: readOptionalFieldString(doc, f.summary),
2452
+ slug: slugVal || void 0,
2453
+ categoryVal,
2454
+ statusVal: mapEventStatus(
2455
+ f.status && isSafeObjectKey(f.status) ? doc[f.status] : doc.eventStatus
2456
+ ),
2457
+ color: resolveDocColor(doc, source, categoryVal),
2458
+ rsvpEnabled: doc[rsvpField] !== void 0 ? Boolean(doc[rsvpField]) : false,
2459
+ capacity: readNullablePositiveInt(doc[capField]),
2460
+ waitlistEnabled: typeof doc.waitlistEnabled === "boolean" ? doc.waitlistEnabled : settings2.waitlistEnabledByDefault,
2461
+ baseDateKey,
2462
+ virtualUrl: sanitizeEventUrl(doc.virtualUrl, { allowMailto: false }) ?? void 0,
2463
+ poster: normalizeEventPoster(
2464
+ f.poster && isSafeObjectKey(f.poster) ? doc[f.poster] : doc.poster,
2465
+ baseTitle
2466
+ ),
2467
+ actionLinks: normalizeEventActionLinks(doc[f.actionLinks ?? "actionLinks"]),
2468
+ spotlightConfig: normalizeEventSpotlightConfig(
2469
+ doc[f.spotlightConfig ?? "spotlightConfig"]
2470
+ ),
2471
+ extractedCustomFields: extractCustomFields(
2472
+ strapi,
2473
+ source.uid,
2474
+ doc,
2475
+ f.customFields
2476
+ )
2477
+ };
2478
+ }
2479
+ const matchesCategoryFilter = (cat, filter) => !filter || filter === "all" || cat.toLowerCase() === filter.toLowerCase();
2480
+ function resolveDocumentRecurrence(doc, source, defaultTimezone, allDay) {
2481
+ const rruleField = source.fields.rrule;
2482
+ const rawRecurrence = rruleField && isSafeObjectKey(rruleField) ? doc[rruleField] : null;
2483
+ const docTz = typeof doc.timezone === "string" && doc.timezone ? doc.timezone : defaultTimezone;
2484
+ const recurrence = parseRRuleFieldValue(rawRecurrence, docTz);
2485
+ if (allDay) recurrence.allDay = true;
2486
+ return recurrence;
2487
+ }
2488
+ function expandDocumentOccurrencesSync(args) {
2489
+ const {
2490
+ strapi,
2491
+ doc,
2492
+ source,
2493
+ params,
2494
+ settings: settings2,
2495
+ windowStart,
2496
+ windowEnd,
2497
+ pendingRsvpMap
2498
+ } = args;
2499
+ if (!isAllowedEventSourceUid(source.uid)) return [];
2500
+ const docId = String(doc.documentId ?? doc.id ?? "");
2501
+ if (!docId) return [];
2502
+ const startEnd = resolveStartEndFromDocument(doc, source);
2503
+ if (!startEnd) return [];
2504
+ const meta = extractDocMetadata(
2505
+ strapi,
2506
+ doc,
2507
+ source,
2508
+ settings2,
2509
+ docId,
2510
+ startEnd.startDate.slice(0, 10)
2511
+ );
2512
+ if (params.onlySpotlights && !meta.spotlightConfig?.enabled) {
2513
+ return [];
2514
+ }
2515
+ if (!matchesCategoryFilter(meta.categoryVal, params.category)) return [];
2516
+ const recurrence = resolveDocumentRecurrence(
2517
+ doc,
2518
+ source,
2519
+ settings2.defaultTimezone,
2520
+ startEnd.allDay
2521
+ );
2522
+ const slots = expandRuleSlots({
2523
+ baseStartDate: startEnd.startDate,
2524
+ baseEndDate: startEnd.endDate,
2525
+ recurrence,
2526
+ windowStart,
2527
+ windowEnd
2528
+ });
2529
+ const results = [];
2530
+ for (const slot of slots) {
2531
+ const occ = buildSingleExpandedOccurrence({
2532
+ doc,
2533
+ source,
2534
+ meta,
2535
+ slot,
2536
+ recurrence,
2537
+ settings: settings2,
2538
+ paramLocale: params.locale
2539
+ });
2540
+ if (meta.rsvpEnabled) {
2541
+ pendingRsvpMap.set(occ.occurrenceId, {
2542
+ eventUid: source.uid,
2543
+ eventDocumentId: meta.docId,
2544
+ occurrenceDate: slot.occurrenceDate,
2545
+ capacity: meta.capacity,
2546
+ waitlistEnabled: meta.waitlistEnabled,
2547
+ rsvpEnabled: meta.rsvpEnabled
2548
+ });
2549
+ }
2550
+ results.push(occ);
2551
+ }
2552
+ return results;
2553
+ }
2554
+ const toUndefinedIfEmpty = (val) => val ? val : void 0;
2555
+ const readOptionalDocString = (doc, key) => typeof doc[key] === "string" && doc[key] ? doc[key] : void 0;
2556
+ function buildSingleExpandedOccurrence(args) {
2557
+ const { doc, source, meta, slot, recurrence, settings: settings2, paramLocale } = args;
2558
+ const effectiveTitle = slot.override?.title || meta.baseTitle;
2559
+ const effectiveLocation = slot.override?.location || meta.baseLocation;
2560
+ const calendarLinks = buildCalendarLinks({
2561
+ documentId: meta.docId,
2562
+ title: effectiveTitle,
2563
+ startDate: slot.startDate,
2564
+ endDate: slot.endDate,
2565
+ allDay: Boolean(recurrence.allDay),
2566
+ summary: meta.summary,
2567
+ location: effectiveLocation,
2568
+ occurrenceDate: slot.occurrenceDate
2569
+ });
2570
+ const alternateDates = shared.resolveOccurrenceAlternateDates({
2571
+ occurrenceDate: slot.occurrenceDate,
2572
+ baseDateKey: meta.baseDateKey,
2573
+ isRecurring: slot.isRecurring,
2574
+ doc,
2575
+ source,
2576
+ secondaryCalendars: settings2.secondaryCalendars ?? [],
2577
+ locale: paramLocale && paramLocale !== "auto" ? paramLocale : "en"
2578
+ });
2579
+ return {
2580
+ occurrenceId: `${source.id}:${meta.docId}:${slot.occurrenceDate}`,
2581
+ sourceId: source.id,
2582
+ sourceLabel: source.label,
2583
+ uid: source.uid,
2584
+ documentId: meta.docId,
2585
+ locale: readOptionalDocString(doc, "locale"),
2586
+ occurrenceDate: slot.occurrenceDate,
2587
+ startDate: slot.startDate,
2588
+ endDate: slot.endDate,
2589
+ allDay: Boolean(recurrence.allDay),
2590
+ timezone: recurrence.timezone,
2591
+ title: effectiveTitle,
2592
+ slug: meta.slug,
2593
+ summary: toUndefinedIfEmpty(meta.summary),
2594
+ location: toUndefinedIfEmpty(effectiveLocation),
2595
+ virtualUrl: meta.virtualUrl,
2596
+ category: toUndefinedIfEmpty(meta.categoryVal),
2597
+ status: meta.statusVal,
2598
+ color: meta.color,
2599
+ isRecurring: slot.isRecurring,
2600
+ rrule: toUndefinedIfEmpty(recurrence.rrule),
2601
+ isOverridden: slot.isOverridden,
2602
+ calendarLinks,
2603
+ alternateDates: Object.keys(alternateDates).length > 0 ? alternateDates : void 0,
2604
+ poster: meta.poster,
2605
+ actionLinks: meta.actionLinks,
2606
+ spotlightConfig: meta.spotlightConfig,
2607
+ customFields: meta.extractedCustomFields
2608
+ };
2609
+ }
2610
+ function validateRescheduleDates(params) {
2611
+ if (params.cancelOccurrence && !params.newStartDate) {
2612
+ return;
2613
+ }
2614
+ const startMs = assertValidIsoDateTime(params.newStartDate, "newStartDate");
2615
+ if (params.newEndDate !== void 0 && params.newEndDate !== "") {
2616
+ const endMs = assertValidIsoDateTime(params.newEndDate, "newEndDate");
2617
+ if (endMs <= startMs) {
2618
+ throw new PluginValidationError(
2619
+ "Invalid newEndDate: must be after newStartDate."
2620
+ );
2621
+ }
2622
+ }
2623
+ }
2624
+ function buildReschedulePayload(source, params) {
2625
+ if (params.cancelOccurrence && source.fields.status && isSafeObjectKey(source.fields.status)) {
2626
+ return { [source.fields.status]: "cancelled" };
2627
+ }
2628
+ const updatePayload = {
2629
+ [source.fields.startDate]: params.newStartDate
2630
+ };
2631
+ if (params.newEndDate && source.fields.endDate && isSafeObjectKey(source.fields.endDate)) {
2632
+ updatePayload[source.fields.endDate] = params.newEndDate;
2633
+ }
2634
+ return updatePayload;
2635
+ }
2636
+ function resolveRescheduleSource(sources, sourceId, uid) {
2637
+ const list = (sources ?? []).filter((s) => isAllowedEventSourceUid(s.uid));
2638
+ return list.find((s) => s.id === sourceId || s.uid === uid) ?? list[0];
2639
+ }
2640
+ function buildSingleOccurrenceOverridePatch(rruleField, recurrence, params) {
2641
+ if (!isSafeObjectKey(params.occurrenceDate) || !isValidCalendarDateKey(params.occurrenceDate)) {
2642
+ throw new PluginValidationError(
2643
+ "Invalid occurrenceDate: must be a valid YYYY-MM-DD calendar date."
2644
+ );
2645
+ }
2646
+ const nextOverrides = {};
2647
+ if (recurrence.overrides && typeof recurrence.overrides === "object") {
2648
+ for (const [k, v] of Object.entries(recurrence.overrides)) {
2649
+ if (isSafeObjectKey(k) && v && typeof v === "object") {
2650
+ nextOverrides[k] = v;
2651
+ }
2652
+ }
2653
+ }
2654
+ const prevOverride = Object.hasOwn(nextOverrides, params.occurrenceDate) ? nextOverrides[params.occurrenceDate] : void 0;
2655
+ nextOverrides[params.occurrenceDate] = {
2656
+ ...prevOverride,
2657
+ originalDate: params.occurrenceDate,
2658
+ cancelled: Boolean(params.cancelOccurrence),
2659
+ startDate: params.newStartDate || void 0,
2660
+ endDate: params.newEndDate || void 0
2661
+ };
2662
+ return { [rruleField]: { ...recurrence, overrides: nextOverrides } };
2663
+ }
2664
+ const occurrence = ({ strapi }) => {
2665
+ return {
2666
+ registerCalendarFormatter(calendarId, formatter) {
2667
+ shared.registerCalendarSystemFormatter(calendarId, formatter);
2668
+ },
2669
+ unregisterCalendarFormatter(calendarId) {
2670
+ shared.unregisterCalendarSystemFormatter(calendarId);
2671
+ },
2672
+ formatDateInCalendarSystem(isoOrDateKey, system, locale = "en", options2) {
2673
+ return shared.formatDateInCalendarSystem(isoOrDateKey, system, locale, options2);
2674
+ },
2675
+ async getOccurrences(params) {
2676
+ const settings2 = await getSettingsService(strapi).getSettings();
2677
+ const { windowStart, windowEnd, isEmptyWindow } = resolveOccurrenceWindow(params);
2678
+ if (isEmptyWindow) {
2679
+ return [];
2680
+ }
2681
+ const activeSources = (settings2.sources ?? []).filter(
2682
+ (s) => s.enabled && isAllowedEventSourceUid(s.uid) && (!params.sourceId || s.id === params.sourceId || s.uid === params.sourceId)
2683
+ );
2684
+ const allOccurrences = [];
2685
+ const pendingRsvpMap = /* @__PURE__ */ new Map();
2686
+ for (const source of activeSources) {
2687
+ const docs = await fetchDocumentsForSource(
2688
+ strapi,
2689
+ source,
2690
+ params,
2691
+ settings2
2692
+ );
2693
+ for (const doc of docs) {
2694
+ const docOccurrences = expandDocumentOccurrencesSync({
2695
+ strapi,
2696
+ doc,
2697
+ source,
2698
+ params,
2699
+ settings: settings2,
2700
+ windowStart,
2701
+ windowEnd,
2702
+ pendingRsvpMap
2703
+ });
2704
+ allOccurrences.push(...docOccurrences);
2705
+ }
2706
+ }
2707
+ allOccurrences.sort(
2708
+ (a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
2709
+ );
2710
+ const sliced = clampPaginatedSlice(allOccurrences, {
2711
+ limit: params.limit,
2712
+ page: params.page,
2713
+ pageSize: params.pageSize,
2714
+ defaultLimit: params.includeDrafts ? 2e3 : 500,
2715
+ maxLimit: params.includeDrafts ? 2e3 : 500
2716
+ });
2717
+ await attachBatchedRsvpStats(strapi, sliced, pendingRsvpMap);
2718
+ return sliced;
2719
+ },
2720
+ async getActiveSpotlights(params) {
2721
+ const refDate = params?.date && !Number.isNaN(new Date(params.date).getTime()) ? new Date(params.date) : /* @__PURE__ */ new Date();
2722
+ const fromWindow = new Date(
2723
+ refDate.getTime() - 60 * 24 * 60 * 60 * 1e3
2724
+ ).toISOString();
2725
+ const toWindow = new Date(
2726
+ refDate.getTime() + 180 * 24 * 60 * 60 * 1e3
2727
+ ).toISOString();
2728
+ const occurrences = await this.getOccurrences({
2729
+ from: fromWindow,
2730
+ to: toWindow,
2731
+ locale: params?.locale,
2732
+ sourceId: params?.sourceId,
2733
+ category: params?.category,
2734
+ includeDrafts: params?.includeDrafts ?? false,
2735
+ onlySpotlights: true
2736
+ });
2737
+ const active = occurrences.filter(
2738
+ (occ) => isEventSpotlightActiveOnDate({
2739
+ spotlightConfig: occ.spotlightConfig,
2740
+ eventStartDate: occ.startDate,
2741
+ eventEndDate: occ.endDate,
2742
+ referenceDate: refDate
2743
+ })
2744
+ );
2745
+ const sorted = deduplicateAndSortSpotlights(active);
2746
+ return clampPaginatedSlice(sorted, {
2747
+ limit: params?.limit,
2748
+ page: params?.page,
2749
+ pageSize: params?.pageSize,
2750
+ defaultLimit: 100,
2751
+ maxLimit: 100
2752
+ });
2753
+ },
2754
+ async rescheduleOccurrence(params) {
2755
+ const docId = (params.documentId ?? "").trim();
2756
+ if (!docId) {
2757
+ throw new PluginValidationError("Event documentId is required.");
2758
+ }
2759
+ validateRescheduleDates(params);
2760
+ const settings2 = await getSettingsService(strapi).getSettings();
2761
+ const source = resolveRescheduleSource(
2762
+ settings2.sources,
2763
+ params.sourceId,
2764
+ params.uid
2765
+ );
2766
+ if (!source || !isAllowedEventSourceUid(source.uid)) {
2767
+ throw new PluginValidationError(
2768
+ "Target event source is not configured."
2769
+ );
2770
+ }
2771
+ const client = getDocumentsClient(strapi, source.uid);
2772
+ const existing = await client.findOne({ documentId: docId });
2773
+ if (!existing) {
2774
+ throw new PluginNotFoundError(
2775
+ `Document ${docId} not found in ${source.uid}.`
2776
+ );
2777
+ }
2778
+ const rruleField = source.fields.rrule;
2779
+ const recurrence = rruleField && isSafeObjectKey(rruleField) ? parseRRuleFieldValue(existing[rruleField], settings2.defaultTimezone) : null;
2780
+ if (recurrence?.rrule && !params.applyToSeries && rruleField && isSafeObjectKey(rruleField)) {
2781
+ return client.update({
2782
+ documentId: docId,
2783
+ data: buildSingleOccurrenceOverridePatch(
2784
+ rruleField,
2785
+ recurrence,
2786
+ params
2787
+ )
2788
+ });
2789
+ }
2790
+ return client.update({
2791
+ documentId: docId,
2792
+ data: buildReschedulePayload(source, params)
2793
+ });
2794
+ },
2795
+ async quickCreateEvent(params) {
2796
+ const title = (params.title ?? "").trim();
2797
+ if (!title) {
2798
+ throw new PluginValidationError("Event title is required.");
2799
+ }
2800
+ const startMs = assertValidIsoDateTime(params.startDate, "startDate");
2801
+ if (params.endDate !== void 0 && params.endDate !== "") {
2802
+ const endMs = assertValidIsoDateTime(params.endDate, "endDate");
2803
+ if (endMs <= startMs) {
2804
+ throw new PluginValidationError(
2805
+ "Invalid endDate: must be after startDate."
2806
+ );
2807
+ }
2808
+ }
2809
+ const settings2 = await getSettingsService(strapi).getSettings();
2810
+ const source = resolveQuickCreateSource(settings2, params.sourceId);
2811
+ const data = buildQuickCreatePayload(source, settings2, {
2812
+ ...params,
2813
+ title
2814
+ });
2815
+ return getDocumentsClient(strapi, source.uid).create({
2816
+ data,
2817
+ status: "published"
2818
+ });
2819
+ }
2820
+ };
2821
+ };
2822
+ async function fetchRegistrationRecordsSafe(strapi, eventDocumentId, occurrenceDate) {
2823
+ const filters = {
2824
+ eventDocumentId: { $eq: eventDocumentId },
2825
+ ...occurrenceDate ? { occurrenceDate: { $eq: occurrenceDate } } : {}
2826
+ };
2827
+ try {
2828
+ return await getDocumentsClient(
2829
+ strapi,
2830
+ REGISTRATION_CONTENT_TYPE_UID
2831
+ ).findMany({
2832
+ filters,
2833
+ limit: 5e3
2834
+ });
2835
+ } catch {
2836
+ return [];
2837
+ }
2838
+ }
2839
+ async function promoteFromWaitlistUnlocked(strapi, params) {
2840
+ const targetEvent = params.targetEventOverride !== void 0 ? params.targetEventOverride : await resolveTargetEvent(
2841
+ strapi,
2842
+ params.eventDocumentId,
2843
+ params.eventUid,
2844
+ { includeDrafts: true }
2845
+ );
2846
+ if (!targetEvent || targetEvent.capacity === null || targetEvent.eventStatus === "cancelled") {
2847
+ return { promoted: [], emails: [] };
2848
+ }
2849
+ const slotRecords = await fetchRegistrationRecordsSafe(
2850
+ strapi,
2851
+ params.eventDocumentId,
2852
+ params.occurrenceDate
2853
+ );
2854
+ const counts = aggregateRsvpCounts(slotRecords);
2855
+ let availableSeats = Math.max(
2856
+ 0,
2857
+ targetEvent.capacity - counts.confirmedSeats
2858
+ );
2859
+ if (availableSeats <= 0) {
2860
+ return { promoted: [], emails: [] };
2861
+ }
2862
+ const client = getDocumentsClient(strapi, REGISTRATION_CONTENT_TYPE_UID);
2863
+ const waitlisted = await client.findMany({
2864
+ filters: {
2865
+ eventDocumentId: { $eq: params.eventDocumentId },
2866
+ occurrenceDate: { $eq: params.occurrenceDate },
2867
+ status: { $eq: "waitlisted" }
2868
+ },
2869
+ sort: "createdAt:asc",
2870
+ limit: 200
2871
+ });
2872
+ const promoted = [];
2873
+ const emails = [];
2874
+ for (const candidate of waitlisted) {
2875
+ if (availableSeats <= 0) break;
2876
+ if (candidate.status !== "waitlisted") continue;
2877
+ const needed = readStoredGuestsCount(candidate.guestsCount);
2878
+ if (needed > availableSeats) continue;
2879
+ const updated = await client.update({
2880
+ documentId: String(candidate.documentId),
2881
+ data: { status: "confirmed" }
2882
+ });
2883
+ availableSeats -= needed;
2884
+ promoted.push(formatRegistrationRecord(updated));
2885
+ emails.push({
2886
+ to: String(candidate.attendeeEmail),
2887
+ attendeeName: String(candidate.attendeeName),
2888
+ eventTitle: String(candidate.eventTitle ?? targetEvent.title),
2889
+ occurrenceDate: params.occurrenceDate,
2890
+ guestsCount: needed,
2891
+ status: "confirmed",
2892
+ promotedFromWaitlist: true
2893
+ });
2894
+ }
2895
+ return { promoted, emails };
2896
+ }
2897
+ async function executeCancelByToken(strapi, token) {
2898
+ const cleanedToken = (token ?? "").trim();
2899
+ if (!cleanedToken || cleanedToken.length < 16) {
2900
+ throw new PluginValidationError("Invalid cancellation token.");
2901
+ }
2902
+ const initialMatch = await findRegistrationByRawToken(strapi, cleanedToken);
2903
+ if (!initialMatch) {
2904
+ throw new PluginNotFoundError(
2905
+ "Registration not found for the provided token."
2906
+ );
2907
+ }
2908
+ const regDocId = String(initialMatch.record.documentId);
2909
+ const eventDocId = String(initialMatch.record.eventDocumentId);
2910
+ const occDate = String(initialMatch.record.occurrenceDate);
2911
+ const eventUid = String(
2912
+ initialMatch.record.eventUid ?? EVENT_CONTENT_TYPE_UID
2913
+ );
2914
+ const targetEvent = await resolveTargetEvent(strapi, eventDocId, eventUid, {
2915
+ includeDrafts: true
2916
+ });
2917
+ if (targetEvent) {
2918
+ try {
2919
+ const endMs = assertScheduledOccurrence(targetEvent, occDate);
2920
+ if (endMs !== null && Number.isFinite(endMs) && Date.now() > endMs) {
2921
+ throw new PluginValidationError(
2922
+ "Cannot cancel registration after the event has ended."
2923
+ );
2924
+ }
2925
+ } catch (err) {
2926
+ if (err instanceof PluginValidationError && err.message.includes("ended")) {
2927
+ throw err;
2928
+ }
2929
+ }
2930
+ }
2931
+ const { cancelledRecord, emails, promotedCount } = await withOccurrenceLock(
2932
+ strapi,
2933
+ eventDocId,
2934
+ occDate,
2935
+ async () => {
2936
+ const client = getDocumentsClient(strapi, REGISTRATION_CONTENT_TYPE_UID);
2937
+ const fresh = await client.findOne({ documentId: regDocId });
2938
+ if (!fresh || !verifyTokenHashConstantTime(initialMatch.tokenHash, fresh.checkInToken)) {
2939
+ throw new PluginValidationError(
2940
+ "Invalid or already used cancellation token."
2941
+ );
2942
+ }
2943
+ if (fresh.status === "cancelled") {
2944
+ throw new PluginValidationError(
2945
+ "Registration has already been cancelled."
2946
+ );
2947
+ }
2948
+ if (fresh.status === "checked_in") {
2949
+ throw new PluginValidationError(
2950
+ "Checked-in registrations cannot be cancelled."
2951
+ );
2952
+ }
2953
+ const wasConfirmed = fresh.status === "confirmed";
2954
+ const updated = await client.update({
2955
+ documentId: regDocId,
2956
+ data: { status: "cancelled", checkInToken: null }
2957
+ });
2958
+ const promo = wasConfirmed ? await promoteFromWaitlistUnlocked(strapi, {
2959
+ eventUid,
2960
+ eventDocumentId: eventDocId,
2961
+ occurrenceDate: occDate,
2962
+ targetEventOverride: targetEvent
2963
+ }) : { promoted: [], emails: [] };
2964
+ return {
2965
+ cancelledRecord: formatRegistrationRecord(updated),
2966
+ promotedCount: promo.promoted.length,
2967
+ emails: promo.emails
2968
+ };
2969
+ }
2970
+ );
2971
+ for (const email of emails) {
2972
+ await trySendNotificationEmail(strapi, email);
2973
+ }
2974
+ return {
2975
+ cancelled: cancelledRecord,
2976
+ promotedCount
2977
+ };
2978
+ }
2979
+ async function executeUpdateRegistrationStatus(strapi, registrationDocumentId, newStatus) {
2980
+ const validStatuses = [
2981
+ "confirmed",
2982
+ "waitlisted",
2983
+ "cancelled",
2984
+ "checked_in"
2985
+ ];
2986
+ if (!validStatuses.includes(newStatus)) {
2987
+ throw new PluginValidationError(`Invalid status: ${String(newStatus)}`);
2988
+ }
2989
+ const client = getDocumentsClient(strapi, REGISTRATION_CONTENT_TYPE_UID);
2990
+ const initial = await client.findOne({
2991
+ documentId: registrationDocumentId
2992
+ });
2993
+ if (!initial) {
2994
+ throw new PluginNotFoundError(
2995
+ `Registration ${registrationDocumentId} not found.`
2996
+ );
2997
+ }
2998
+ const eventDocId = String(initial.eventDocumentId);
2999
+ const occDate = String(initial.occurrenceDate);
3000
+ const eventUid = String(initial.eventUid ?? EVENT_CONTENT_TYPE_UID);
3001
+ const { registration, promotedCount, emails } = await withOccurrenceLock(
3002
+ strapi,
3003
+ eventDocId,
3004
+ occDate,
3005
+ async () => {
3006
+ const existing = await client.findOne({
3007
+ documentId: registrationDocumentId
3008
+ });
3009
+ if (!existing) {
3010
+ throw new PluginNotFoundError(
3011
+ `Registration ${registrationDocumentId} not found.`
3012
+ );
3013
+ }
3014
+ const wasOccupyingSeat = existing.status === "confirmed" || existing.status === "checked_in";
3015
+ const nowOccupyingSeat = newStatus === "confirmed" || newStatus === "checked_in";
3016
+ const targetEvent = await resolveTargetEvent(
3017
+ strapi,
3018
+ eventDocId,
3019
+ eventUid,
3020
+ { includeDrafts: true }
3021
+ );
3022
+ if (nowOccupyingSeat && targetEvent?.eventStatus === "cancelled") {
3023
+ throw new PluginValidationError(
3024
+ "Cannot confirm or check in registration for a cancelled event."
3025
+ );
3026
+ }
3027
+ if (!wasOccupyingSeat && nowOccupyingSeat && targetEvent && targetEvent.capacity !== null) {
3028
+ const slotRecords = await fetchRegistrationRecordsSafe(
3029
+ strapi,
3030
+ eventDocId,
3031
+ occDate
3032
+ );
3033
+ const counts = aggregateRsvpCounts(slotRecords);
3034
+ const neededSeats = readStoredGuestsCount(existing.guestsCount);
3035
+ if (counts.confirmedSeats + neededSeats > targetEvent.capacity) {
3036
+ throw new PluginValidationError(
3037
+ "Cannot confirm registration: event has reached maximum capacity."
3038
+ );
3039
+ }
3040
+ }
3041
+ const dataPatch = {
3042
+ status: newStatus,
3043
+ ...newStatus === "checked_in" && !existing.checkedInAt ? { checkedInAt: (/* @__PURE__ */ new Date()).toISOString() } : {},
3044
+ ...newStatus === "cancelled" ? { checkInToken: null } : {}
3045
+ };
3046
+ const updated = await client.update({
3047
+ documentId: registrationDocumentId,
3048
+ data: dataPatch
3049
+ });
3050
+ const promo = wasOccupyingSeat && !nowOccupyingSeat ? await promoteFromWaitlistUnlocked(strapi, {
3051
+ eventUid,
3052
+ eventDocumentId: eventDocId,
3053
+ occurrenceDate: occDate,
3054
+ targetEventOverride: targetEvent
3055
+ }) : { promoted: [], emails: [] };
3056
+ return {
3057
+ registration: formatRegistrationRecord(updated),
3058
+ promotedCount: promo.promoted.length,
3059
+ emails: promo.emails
3060
+ };
3061
+ }
3062
+ );
3063
+ for (const email of emails) {
3064
+ await trySendNotificationEmail(strapi, email);
3065
+ }
3066
+ return { registration, promotedCount };
3067
+ }
3068
+ async function executeCheckInAttendee(strapi, identifier) {
3069
+ let targetDocId = identifier.documentId?.trim();
3070
+ let providedTokenHash = null;
3071
+ if (identifier.token !== void 0) {
3072
+ const cleanedToken = String(identifier.token).trim();
3073
+ if (cleanedToken.length < 16 || cleanedToken.length > 256) {
3074
+ throw new PluginValidationError("Invalid check-in token.");
3075
+ }
3076
+ providedTokenHash = hashRsvpToken(cleanedToken);
3077
+ if (!targetDocId) {
3078
+ const match = await findRegistrationByRawToken(strapi, cleanedToken);
3079
+ if (!match) {
3080
+ throw new PluginValidationError("Invalid check-in token.");
3081
+ }
3082
+ targetDocId = String(match.record.documentId);
3083
+ }
3084
+ }
3085
+ if (!targetDocId) {
3086
+ throw new PluginNotFoundError("Registration not found for check-in.");
3087
+ }
3088
+ const client = getDocumentsClient(strapi, REGISTRATION_CONTENT_TYPE_UID);
3089
+ const initial = await client.findOne({ documentId: targetDocId });
3090
+ if (!initial) {
3091
+ throw new PluginNotFoundError("Registration not found for check-in.");
3092
+ }
3093
+ const eventDocId = String(initial.eventDocumentId);
3094
+ const occDate = String(initial.occurrenceDate);
3095
+ const eventUid = String(initial.eventUid ?? EVENT_CONTENT_TYPE_UID);
3096
+ return withOccurrenceLock(strapi, eventDocId, occDate, async () => {
3097
+ const existing = await client.findOne({ documentId: targetDocId });
3098
+ if (!existing) {
3099
+ throw new PluginNotFoundError("Registration not found for check-in.");
3100
+ }
3101
+ if (providedTokenHash !== null && !verifyTokenHashConstantTime(providedTokenHash, existing.checkInToken)) {
3102
+ throw new PluginValidationError("Invalid check-in token.");
3103
+ }
3104
+ const targetEvent = await resolveTargetEvent(strapi, eventDocId, eventUid, {
3105
+ includeDrafts: true
3106
+ });
3107
+ if (targetEvent?.eventStatus === "cancelled") {
3108
+ throw new PluginValidationError(
3109
+ "Cannot check in attendee for a cancelled event."
3110
+ );
3111
+ }
3112
+ if (existing.status === "checked_in") {
3113
+ return formatRegistrationRecord(existing);
3114
+ }
3115
+ if (existing.status !== "confirmed") {
3116
+ throw new PluginValidationError(
3117
+ `Cannot check in attendee with status "${String(existing.status)}": only confirmed registrations can be checked in.`
3118
+ );
3119
+ }
3120
+ const updated = await client.update({
3121
+ documentId: targetDocId,
3122
+ data: {
3123
+ status: "checked_in",
3124
+ checkedInAt: existing.checkedInAt || (/* @__PURE__ */ new Date()).toISOString()
3125
+ }
3126
+ });
3127
+ return formatRegistrationRecord(updated);
3128
+ });
3129
+ }
3130
+ function buildListRegistrationsQueryFilters(filters) {
3131
+ const queryFilters = {};
3132
+ if (filters?.eventDocumentId) {
3133
+ queryFilters.eventDocumentId = { $eq: filters.eventDocumentId };
3134
+ }
3135
+ if (filters?.occurrenceDate) {
3136
+ queryFilters.occurrenceDate = { $eq: filters.occurrenceDate };
3137
+ }
3138
+ if (filters?.status) {
3139
+ queryFilters.status = { $eq: filters.status };
3140
+ }
3141
+ const q = filters?.search?.trim();
3142
+ if (q) {
3143
+ queryFilters.$or = [
3144
+ { attendeeName: { $containsi: q } },
3145
+ { attendeeEmail: { $containsi: q } },
3146
+ { eventTitle: { $containsi: q } }
3147
+ ];
3148
+ }
3149
+ return queryFilters;
3150
+ }
3151
+ async function resolveStatsEventConfig(strapi, params) {
3152
+ if (params.capacityOverride === void 0) {
3153
+ const resolved = await resolveTargetEvent(
3154
+ strapi,
3155
+ params.eventDocumentId,
3156
+ params.eventUid,
3157
+ { includeDrafts: params.includeDrafts ?? false }
3158
+ );
3159
+ if (resolved) {
3160
+ return {
3161
+ eventUid: resolved.uid,
3162
+ capacity: resolved.capacity,
3163
+ waitlistEnabled: resolved.waitlistEnabled,
3164
+ rsvpEnabled: resolved.rsvpEnabled
3165
+ };
3166
+ }
3167
+ }
3168
+ return {
3169
+ eventUid: params.eventUid ?? EVENT_CONTENT_TYPE_UID,
3170
+ capacity: params.capacityOverride ?? null,
3171
+ waitlistEnabled: params.waitlistOverride ?? true,
3172
+ rsvpEnabled: params.rsvpEnabledOverride ?? false
3173
+ };
3174
+ }
3175
+ const rsvp = ({ strapi }) => {
3176
+ return {
3177
+ async getEventStats(params) {
3178
+ const { eventDocumentId, occurrenceDate = null } = params;
3179
+ const { eventUid, capacity, waitlistEnabled, rsvpEnabled } = await resolveStatsEventConfig(strapi, params);
3180
+ const records = await fetchRegistrationRecordsSafe(
3181
+ strapi,
3182
+ eventDocumentId,
3183
+ occurrenceDate
3184
+ );
3185
+ const counts = aggregateRsvpCounts(records);
3186
+ const remainingSeats = capacity !== null ? Math.max(0, capacity - counts.confirmedSeats) : null;
3187
+ const isFull = capacity !== null ? counts.confirmedSeats >= capacity : false;
3188
+ return {
3189
+ eventUid,
3190
+ eventDocumentId,
3191
+ occurrenceDate,
3192
+ rsvpEnabled,
3193
+ capacity,
3194
+ waitlistEnabled,
3195
+ ...counts,
3196
+ remainingSeats,
3197
+ isFull
3198
+ };
3199
+ },
3200
+ async registerAttendee(eventDocumentId, input) {
3201
+ const docId = (eventDocumentId ?? "").trim();
3202
+ if (!docId) {
3203
+ throw new PluginValidationError("Event documentId is required.");
3204
+ }
3205
+ const targetEvent = await resolveTargetEvent(
3206
+ strapi,
3207
+ docId,
3208
+ input.eventUid,
3209
+ { includeDrafts: false }
3210
+ );
3211
+ if (!targetEvent) {
3212
+ throw new PluginNotFoundError(
3213
+ `Event not found for documentId: ${docId}`
3214
+ );
3215
+ }
3216
+ const { attendeeName, attendeeEmail, guestsCount } = validateRegistrationInputAndEvent(input, targetEvent);
3217
+ const occurrenceDate = resolveValidatedOccurrenceDate(
3218
+ input.occurrenceDate,
3219
+ targetEvent
3220
+ );
3221
+ const rawToken = generateRawRsvpToken();
3222
+ const tokenHash = hashRsvpToken(rawToken);
3223
+ const { formatted, assignedStatus } = await withOccurrenceLock(
3224
+ strapi,
3225
+ docId,
3226
+ occurrenceDate,
3227
+ async () => {
3228
+ const client = getDocumentsClient(
3229
+ strapi,
3230
+ REGISTRATION_CONTENT_TYPE_UID
3231
+ );
3232
+ const existing = await client.findMany({
3233
+ filters: {
3234
+ eventDocumentId: { $eq: docId },
3235
+ occurrenceDate: { $eq: occurrenceDate },
3236
+ attendeeEmail: { $eq: attendeeEmail },
3237
+ status: { $in: ["confirmed", "waitlisted", "checked_in"] }
3238
+ },
3239
+ limit: 1
3240
+ });
3241
+ if (existing.length > 0) {
3242
+ throw new PluginValidationError(
3243
+ "An active registration already exists for this email and date."
3244
+ );
3245
+ }
3246
+ const slotRecords = await fetchRegistrationRecordsSafe(
3247
+ strapi,
3248
+ docId,
3249
+ occurrenceDate
3250
+ );
3251
+ const counts = aggregateRsvpCounts(slotRecords);
3252
+ const status = resolveAssignedRsvpStatus(
3253
+ targetEvent,
3254
+ counts.confirmedSeats,
3255
+ guestsCount
3256
+ );
3257
+ const created = await client.create({
3258
+ data: buildRegistrationCreateData({
3259
+ targetEvent,
3260
+ eventDocumentId: docId,
3261
+ occurrenceDate,
3262
+ attendeeName,
3263
+ attendeeEmail,
3264
+ guestsCount,
3265
+ assignedStatus: status,
3266
+ tokenHash,
3267
+ input
3268
+ })
3269
+ });
3270
+ return {
3271
+ assignedStatus: status,
3272
+ formatted: formatRegistrationRecord({
3273
+ ...created,
3274
+ eventUid: targetEvent.uid,
3275
+ eventDocumentId: docId,
3276
+ eventTitle: targetEvent.title,
3277
+ occurrenceDate,
3278
+ attendeeName,
3279
+ attendeeEmail,
3280
+ guestsCount,
3281
+ status
3282
+ })
3283
+ };
3284
+ }
3285
+ );
3286
+ await trySendNotificationEmail(strapi, {
3287
+ to: attendeeEmail,
3288
+ attendeeName,
3289
+ eventTitle: targetEvent.title,
3290
+ occurrenceDate,
3291
+ guestsCount,
3292
+ status: assignedStatus
3293
+ });
3294
+ return {
3295
+ ...formatted,
3296
+ attendeePhone: formatted.attendeePhone ?? "",
3297
+ notes: formatted.notes ?? "",
3298
+ cancellationToken: rawToken
3299
+ };
3300
+ },
3301
+ async promoteFromWaitlist(params) {
3302
+ const { promoted, emails } = await withOccurrenceLock(
3303
+ strapi,
3304
+ params.eventDocumentId,
3305
+ params.occurrenceDate,
3306
+ () => promoteFromWaitlistUnlocked(strapi, params)
3307
+ );
3308
+ for (const email of emails) {
3309
+ await trySendNotificationEmail(strapi, email);
3310
+ }
3311
+ return promoted;
3312
+ },
3313
+ cancelByToken(token) {
3314
+ return executeCancelByToken(strapi, token);
3315
+ },
3316
+ updateRegistrationStatus(registrationDocumentId, newStatus) {
3317
+ return executeUpdateRegistrationStatus(
3318
+ strapi,
3319
+ registrationDocumentId,
3320
+ newStatus
3321
+ );
3322
+ },
3323
+ checkInAttendee(identifier) {
3324
+ return executeCheckInAttendee(strapi, identifier);
3325
+ },
3326
+ async listRegistrations(filters) {
3327
+ const rows = await getDocumentsClient(
3328
+ strapi,
3329
+ REGISTRATION_CONTENT_TYPE_UID
3330
+ ).findMany({
3331
+ filters: buildListRegistrationsQueryFilters(filters),
3332
+ sort: "createdAt:desc",
3333
+ limit: 1e3
3334
+ });
3335
+ const formatted = rows.map((r) => formatRegistrationRecord(r));
3336
+ return clampPaginatedSlice(formatted, {
3337
+ limit: filters?.limit,
3338
+ page: filters?.page,
3339
+ pageSize: filters?.pageSize,
3340
+ defaultLimit: 1e3,
3341
+ maxLimit: 1e3
3342
+ });
3343
+ },
3344
+ async exportRegistrationsCsv(filters) {
3345
+ const records = await this.listRegistrations(filters);
3346
+ return generateRegistrationsCsv(records);
3347
+ },
3348
+ formatRegistrationRecord
3349
+ };
3350
+ };
3351
+ const STORE_KEY = "settings";
3352
+ function sanitizeStringRecord(raw) {
3353
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
3354
+ return void 0;
3355
+ }
3356
+ const clean = {};
3357
+ for (const [k, v] of Object.entries(raw)) {
3358
+ if (isSafeObjectKey(k) && typeof v === "string") {
3359
+ clean[k] = v;
3360
+ }
3361
+ }
3362
+ return Object.keys(clean).length > 0 ? clean : void 0;
3363
+ }
3364
+ function sanitizeTimeSlotMap(raw) {
3365
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
3366
+ return void 0;
3367
+ }
3368
+ const clean = {};
3369
+ for (const [k, v] of Object.entries(raw)) {
3370
+ if (isSafeObjectKey(k) && v && typeof v === "object" && typeof v.start === "string" && typeof v.end === "string") {
3371
+ clean[k] = { start: v.start, end: v.end };
3372
+ }
3373
+ }
3374
+ return Object.keys(clean).length > 0 ? clean : void 0;
3375
+ }
3376
+ function sanitizeSourceFields(fields) {
3377
+ const safeFields = {
3378
+ title: typeof fields?.title === "string" && isSafeObjectKey(fields.title) ? fields.title : "title",
3379
+ startDate: typeof fields?.startDate === "string" && isSafeObjectKey(fields.startDate) ? fields.startDate : "startDate"
3380
+ };
3381
+ const optionalKeys = [
3382
+ "endDate",
3383
+ "rrule",
3384
+ "location",
3385
+ "category",
3386
+ "status",
3387
+ "summary",
3388
+ "slug",
3389
+ "capacity",
3390
+ "rsvpEnabled",
3391
+ "timeSlot",
3392
+ "poster",
3393
+ "actionLinks",
3394
+ "spotlightConfig"
3395
+ ];
3396
+ for (const key of optionalKeys) {
3397
+ const val = fields?.[key];
3398
+ if (typeof val === "string" && isSafeObjectKey(val)) {
3399
+ safeFields[key] = val;
3400
+ }
3401
+ }
3402
+ const altFields = sanitizeStringRecord(fields?.alternateDateFields);
3403
+ if (altFields) {
3404
+ safeFields.alternateDateFields = altFields;
3405
+ }
3406
+ if (Array.isArray(fields?.customFields)) {
3407
+ safeFields.customFields = fields.customFields.filter(isSafeObjectKey);
3408
+ }
3409
+ return safeFields;
3410
+ }
3411
+ function sanitizeSourceMapping(src) {
3412
+ if (!src || typeof src !== "object") return null;
3413
+ if (!isSafeObjectKey(src.id) || !isAllowedEventSourceUid(src.uid)) {
3414
+ return null;
3415
+ }
3416
+ return {
3417
+ id: src.id.trim(),
3418
+ label: typeof src.label === "string" && src.label ? src.label : src.id,
3419
+ uid: src.uid.trim(),
3420
+ color: typeof src.color === "string" && src.color.startsWith("#") ? src.color : "#0D9488",
3421
+ enabled: typeof src.enabled === "boolean" ? src.enabled : true,
3422
+ fields: sanitizeSourceFields(src.fields),
3423
+ ...sanitizeTimeSlotMap(src.timeSlotMap) ? { timeSlotMap: sanitizeTimeSlotMap(src.timeSlotMap) } : {},
3424
+ ...sanitizeStringRecord(src.categoryColors) ? { categoryColors: sanitizeStringRecord(src.categoryColors) } : {},
3425
+ ...typeof src.defaultAllDay === "boolean" ? { defaultAllDay: src.defaultAllDay } : {}
3426
+ };
3427
+ }
3428
+ function mergeSourcesList(defaults, userSources) {
3429
+ const merged = defaults.map((d) => sanitizeSourceMapping(d)).filter((d) => d !== null);
3430
+ if (!Array.isArray(userSources)) return merged;
3431
+ for (const rawSrc of userSources) {
3432
+ const src = sanitizeSourceMapping(rawSrc);
3433
+ if (!src) continue;
3434
+ const existingIdx = merged.findIndex(
3435
+ (s) => s.id === src.id || s.uid === src.uid
3436
+ );
3437
+ if (existingIdx >= 0) {
3438
+ merged[existingIdx] = {
3439
+ ...merged[existingIdx],
3440
+ ...src,
3441
+ fields: { ...merged[existingIdx].fields, ...src.fields }
3442
+ };
3443
+ } else {
3444
+ merged.push(src);
3445
+ }
3446
+ }
3447
+ return merged;
3448
+ }
3449
+ function sanitizeCalendarConfig(cal) {
3450
+ if (!cal || typeof cal !== "object") return null;
3451
+ if (!isSafeObjectKey(cal.id) || typeof cal.intlCalendar !== "string") {
3452
+ return null;
3453
+ }
3454
+ const intlCal = cal.intlCalendar.trim();
3455
+ if (!intlCal) return null;
3456
+ return {
3457
+ id: cal.id.trim(),
3458
+ label: typeof cal.label === "string" && cal.label ? cal.label : cal.id,
3459
+ intlCalendar: intlCal,
3460
+ ...typeof cal.locale === "string" && cal.locale ? { locale: cal.locale } : {},
3461
+ ...typeof cal.dayOffset === "number" && Number.isFinite(cal.dayOffset) ? { dayOffset: Math.max(-2, Math.min(2, Math.round(cal.dayOffset))) } : {},
3462
+ ...isSafeObjectKey(cal.overrideField) ? { overrideField: cal.overrideField } : {},
3463
+ enabled: Boolean(cal.enabled)
3464
+ };
3465
+ }
3466
+ function mergeCalendarsList(defaults, userCalendars) {
3467
+ const merged = (defaults ?? []).map((c) => sanitizeCalendarConfig(c)).filter((c) => c !== null);
3468
+ if (!Array.isArray(userCalendars)) return merged;
3469
+ for (const rawCal of userCalendars) {
3470
+ const cal = sanitizeCalendarConfig(rawCal);
3471
+ if (!cal) continue;
3472
+ const idx = merged.findIndex((c) => c.id === cal.id);
3473
+ if (idx >= 0) {
3474
+ merged[idx] = { ...merged[idx], ...cal };
3475
+ } else {
3476
+ merged.push(cal);
3477
+ }
3478
+ }
3479
+ return merged;
3480
+ }
3481
+ function sanitizeRecurrencePreset(item) {
3482
+ if (!item || typeof item !== "object") return null;
3483
+ if (!isSafeObjectKey(item.id) || typeof item.rrule !== "string") {
3484
+ return null;
3485
+ }
3486
+ const cleanRule = normalizeRRuleString(item.rrule);
3487
+ if (!cleanRule) return null;
3488
+ return {
3489
+ id: item.id.trim(),
3490
+ label: typeof item.label === "string" && item.label ? item.label : item.id,
3491
+ rrule: cleanRule
3492
+ };
3493
+ }
3494
+ function mergePresetLists(defaults, stored) {
3495
+ const result = (defaults ?? []).map((p) => sanitizeRecurrencePreset(p)).filter((p) => p !== null);
3496
+ if (!Array.isArray(stored)) return result;
3497
+ for (const rawItem of stored) {
3498
+ const item = sanitizeRecurrencePreset(rawItem);
3499
+ if (!item) continue;
3500
+ const idx = result.findIndex(
3501
+ (p) => p.id === item.id || p.rrule === item.rrule
3502
+ );
3503
+ if (idx >= 0) {
3504
+ result[idx] = { ...result[idx], ...item };
3505
+ } else {
3506
+ result.push(item);
3507
+ }
3508
+ }
3509
+ return result;
3510
+ }
3511
+ function resolveNonEmptyString(patchVal, fallback, maxLen) {
3512
+ if (typeof patchVal === "string" && patchVal.trim()) {
3513
+ const trimmed = patchVal.trim();
3514
+ return maxLen ? trimmed.slice(0, maxLen) : trimmed;
3515
+ }
3516
+ return fallback;
3517
+ }
3518
+ function resolvePositiveInt(patchVal, fallback, maxLimit) {
3519
+ if (typeof patchVal === "number" && Number.isFinite(patchVal) && patchVal >= 1) {
3520
+ const floored = Math.floor(patchVal);
3521
+ return maxLimit ? Math.min(maxLimit, floored) : floored;
3522
+ }
3523
+ return fallback;
3524
+ }
3525
+ function resolveBoolean(patchVal, fallback) {
3526
+ return typeof patchVal === "boolean" ? patchVal : fallback;
3527
+ }
3528
+ function validateAndSanitizeSourcesPatch(patchSources, fallback) {
3529
+ if (patchSources === void 0) return fallback;
3530
+ if (!Array.isArray(patchSources)) {
3531
+ throw new PluginValidationError("Invalid sources: must be an array.");
3532
+ }
3533
+ const validated = [];
3534
+ for (const rawItem of patchSources) {
3535
+ const rawObj = rawItem;
3536
+ if (!rawObj || !isAllowedEventSourceUid(rawObj.uid)) {
3537
+ throw new PluginValidationError(
3538
+ `Disallowed event source UID "${String(rawObj?.uid ?? "")}". Only api::* and plugin::events.event are permitted.`
3539
+ );
3540
+ }
3541
+ const sanitized = sanitizeSourceMapping(rawObj);
3542
+ if (!sanitized) {
3543
+ throw new PluginValidationError("Invalid event source configuration.");
3544
+ }
3545
+ validated.push(sanitized);
3546
+ }
3547
+ return validated;
3548
+ }
3549
+ const settings = ({ strapi }) => {
3550
+ const getStore = () => strapi.store({
3551
+ type: "plugin",
3552
+ name: PLUGIN_ID
3553
+ });
3554
+ const getConfigDefaults = () => {
3555
+ const userConfig = strapi.config.get(`plugin::${PLUGIN_ID}`) ?? {};
3556
+ return {
3557
+ ...DEFAULT_PLUGIN_SETTINGS,
3558
+ ...userConfig,
3559
+ sources: mergeSourcesList(
3560
+ DEFAULT_PLUGIN_SETTINGS.sources,
3561
+ userConfig.sources
3562
+ ),
3563
+ secondaryCalendars: mergeCalendarsList(
3564
+ DEFAULT_PLUGIN_SETTINGS.secondaryCalendars,
3565
+ userConfig.secondaryCalendars
3566
+ ),
3567
+ recurrencePresets: mergePresetLists(
3568
+ DEFAULT_PLUGIN_SETTINGS.recurrencePresets,
3569
+ userConfig.recurrencePresets
3570
+ )
3571
+ };
3572
+ };
3573
+ return {
3574
+ async ensureInitialized() {
3575
+ const store = getStore();
3576
+ const existing = await store.get({
3577
+ key: STORE_KEY
3578
+ });
3579
+ const defaults = getConfigDefaults();
3580
+ if (!existing) {
3581
+ await store.set({ key: STORE_KEY, value: defaults });
3582
+ return defaults;
3583
+ }
3584
+ const storedSources = Array.isArray(existing.sources) ? existing.sources.map((s) => sanitizeSourceMapping(s)).filter((s) => s !== null) : [];
3585
+ for (const defSource of defaults.sources) {
3586
+ if (!storedSources.some(
3587
+ (s) => s.id === defSource.id || s.uid === defSource.uid
3588
+ )) {
3589
+ storedSources.push(defSource);
3590
+ }
3591
+ }
3592
+ const storedCalendars = Array.isArray(existing.secondaryCalendars) ? existing.secondaryCalendars.map((c) => sanitizeCalendarConfig(c)).filter((c) => c !== null) : [];
3593
+ for (const defCal of defaults.secondaryCalendars ?? []) {
3594
+ if (!storedCalendars.some((c) => c.id === defCal.id)) {
3595
+ storedCalendars.push(defCal);
3596
+ }
3597
+ }
3598
+ const merged = {
3599
+ ...defaults,
3600
+ ...existing,
3601
+ sources: storedSources,
3602
+ secondaryCalendars: storedCalendars,
3603
+ recurrencePresets: mergePresetLists(
3604
+ defaults.recurrencePresets,
3605
+ existing.recurrencePresets
3606
+ )
3607
+ };
3608
+ await store.set({ key: STORE_KEY, value: merged });
3609
+ return merged;
3610
+ },
3611
+ async getSettings() {
3612
+ const store = getStore();
3613
+ const stored = await store.get({
3614
+ key: STORE_KEY
3615
+ });
3616
+ const defaults = getConfigDefaults();
3617
+ if (!stored) {
3618
+ return defaults;
3619
+ }
3620
+ const cleanSources = Array.isArray(stored.sources) ? stored.sources.map((s) => sanitizeSourceMapping(s)).filter((s) => s !== null) : [];
3621
+ const cleanCalendars = Array.isArray(stored.secondaryCalendars) ? stored.secondaryCalendars.map((c) => sanitizeCalendarConfig(c)).filter((c) => c !== null) : [];
3622
+ return {
3623
+ ...defaults,
3624
+ ...stored,
3625
+ sources: cleanSources.length > 0 ? cleanSources : defaults.sources,
3626
+ secondaryCalendars: cleanCalendars.length > 0 ? cleanCalendars : defaults.secondaryCalendars ?? [],
3627
+ recurrencePresets: mergePresetLists(
3628
+ defaults.recurrencePresets,
3629
+ stored.recurrencePresets
3630
+ )
3631
+ };
3632
+ },
3633
+ async updateSettings(patch) {
3634
+ const current = await this.getSettings();
3635
+ const nextSources = validateAndSanitizeSourcesPatch(
3636
+ patch.sources,
3637
+ current.sources
3638
+ );
3639
+ const nextCalendars = Array.isArray(patch.secondaryCalendars) ? patch.secondaryCalendars.map((c) => sanitizeCalendarConfig(c)).filter((c) => c !== null) : current.secondaryCalendars ?? [];
3640
+ const nextPresets = Array.isArray(patch.recurrencePresets) ? patch.recurrencePresets.map((p) => sanitizeRecurrencePreset(p)).filter((p) => p !== null) : current.recurrencePresets ?? [];
3641
+ const next = {
3642
+ ...current,
3643
+ defaultTimezone: resolveNonEmptyString(
3644
+ patch.defaultTimezone,
3645
+ current.defaultTimezone
3646
+ ),
3647
+ defaultCapacity: resolvePositiveInt(
3648
+ patch.defaultCapacity,
3649
+ current.defaultCapacity
3650
+ ),
3651
+ waitlistEnabledByDefault: resolveBoolean(
3652
+ patch.waitlistEnabledByDefault,
3653
+ current.waitlistEnabledByDefault
3654
+ ),
3655
+ defaultMaxGuestsPerRegistration: resolvePositiveInt(
3656
+ patch.defaultMaxGuestsPerRegistration,
3657
+ current.defaultMaxGuestsPerRegistration,
3658
+ 50
3659
+ ),
3660
+ sendRsvpEmails: resolveBoolean(
3661
+ patch.sendRsvpEmails,
3662
+ current.sendRsvpEmails
3663
+ ),
3664
+ feedTitle: resolveNonEmptyString(
3665
+ patch.feedTitle,
3666
+ current.feedTitle,
3667
+ 140
3668
+ ),
3669
+ sources: nextSources,
3670
+ secondaryCalendars: nextCalendars,
3671
+ recurrencePresets: nextPresets
3672
+ };
3673
+ const store = getStore();
3674
+ await store.set({ key: STORE_KEY, value: next });
3675
+ return next;
3676
+ }
3677
+ };
3678
+ };
3679
+ const services = {
3680
+ settings,
3681
+ occurrence,
3682
+ rsvp,
3683
+ ical
3684
+ };
3685
+ const index = {
3686
+ register,
3687
+ bootstrap,
3688
+ destroy,
3689
+ config,
3690
+ controllers,
3691
+ routes,
3692
+ services,
3693
+ contentTypes,
3694
+ policies,
3695
+ middlewares
3696
+ };
3697
+ Object.defineProperty(exports, "parseRRule", {
3698
+ enumerable: true,
3699
+ get: () => shared.parseRRule
3700
+ });
3701
+ exports.buildCalendarLinks = buildCalendarLinks;
3702
+ exports.buildRRule = buildRRule;
3703
+ exports.default = index;
3704
+ exports.describeRRule = describeRRule;
3705
+ exports.expandRuleSlots = expandRuleSlots;
3706
+ exports.generateIcsCalendar = generateIcsCalendar;
3707
+ exports.generateRegistrationsCsv = generateRegistrationsCsv;
3708
+ exports.parseRRuleFieldValue = parseRRuleFieldValue;
3709
+ exports.sanitizeCsvCell = sanitizeCsvCell;