@vicacha-devs/strapi-plugin-inbox 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 (90) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +17 -2
  3. package/dist/admin/InboxHomePage-B5xUPD7Y.mjs +2153 -0
  4. package/dist/admin/InboxHomePage-BCHbPE2t.js +2171 -0
  5. package/dist/admin/LocalizationRoutingTab-CEXbHNe4.mjs +282 -0
  6. package/dist/admin/LocalizationRoutingTab-Dt6U-kXn.js +281 -0
  7. package/dist/admin/SettingsPage-Dif7Cq7I.mjs +819 -0
  8. package/dist/admin/SettingsPage-qB_efKCl.js +837 -0
  9. package/dist/admin/ar-CbjX4o07.mjs +268 -0
  10. package/dist/admin/ar-KedDMvsH.js +268 -0
  11. package/dist/admin/de-BrJIGJEV.mjs +268 -0
  12. package/dist/admin/de-rj5pqQ0S.js +268 -0
  13. package/dist/admin/en-BE8TARw0.mjs +268 -0
  14. package/dist/admin/en-CuTmgXZL.js +268 -0
  15. package/dist/admin/es-Berf1dRb.js +268 -0
  16. package/dist/admin/es-Ce4nMwC7.mjs +268 -0
  17. package/dist/admin/fr-CfdjWCiu.js +268 -0
  18. package/dist/admin/fr-Y3GKi6mQ.mjs +268 -0
  19. package/dist/admin/index-5e9dAMBs.mjs +822 -0
  20. package/dist/admin/index-DLfoIVZP.js +839 -0
  21. package/dist/admin/index.js +5 -0
  22. package/dist/admin/index.mjs +5 -0
  23. package/dist/admin/it-DXXnvX5J.mjs +268 -0
  24. package/dist/admin/it-DkJ3WwLA.js +268 -0
  25. package/dist/admin/pt-D7WKv-MR.js +268 -0
  26. package/dist/admin/pt-QDCJ6lOH.mjs +268 -0
  27. package/dist/admin/ru-DgYTw9aE.js +268 -0
  28. package/dist/admin/ru-DzP0H4fh.mjs +268 -0
  29. package/dist/admin/src/components/Initializer.d.ts +6 -0
  30. package/dist/admin/src/components/PluginIcon.d.ts +3 -0
  31. package/dist/admin/src/components/inbox/AlertBannersTab.d.ts +10 -0
  32. package/dist/admin/src/components/inbox/CaseworkDetailPanel.d.ts +15 -0
  33. package/dist/admin/src/components/inbox/CaseworkPayloadChecklistColumn.d.ts +12 -0
  34. package/dist/admin/src/components/inbox/CaseworkSubmissionsTab.d.ts +12 -0
  35. package/dist/admin/src/components/inbox/FormBuilderTab.d.ts +17 -0
  36. package/dist/admin/src/components/inbox/FormChecklistAndLocalePanels.d.ts +17 -0
  37. package/dist/admin/src/components/inbox/FormRoutingDrawer.d.ts +15 -0
  38. package/dist/admin/src/components/inbox/SubscribersTab.d.ts +10 -0
  39. package/dist/admin/src/components/settings/AntiSpamCaptchaTab.d.ts +9 -0
  40. package/dist/admin/src/components/settings/BridgesNotificationsTab.d.ts +8 -0
  41. package/dist/admin/src/components/settings/CaptchaProviderCard.d.ts +21 -0
  42. package/dist/admin/src/components/settings/LocalizationRoutingTab.d.ts +16 -0
  43. package/dist/admin/src/index.d.ts +21 -0
  44. package/dist/admin/src/pages/InboxHomePage.d.ts +3 -0
  45. package/dist/admin/src/pages/SettingsPage.d.ts +3 -0
  46. package/dist/admin/src/pluginId.d.ts +3 -0
  47. package/dist/admin/src/translations/cmLabels.d.ts +1 -0
  48. package/dist/admin/src/translations/cmPlaceholders.d.ts +1 -0
  49. package/dist/admin/src/utils/getTranslation.d.ts +3 -0
  50. package/dist/admin/tr-5q4NwTgF.mjs +268 -0
  51. package/dist/admin/tr-CT-24fLA.js +268 -0
  52. package/dist/admin/zh-CYhWM5Qr.mjs +268 -0
  53. package/dist/admin/zh-CmCHcNcd.js +268 -0
  54. package/dist/server/index.js +3108 -0
  55. package/dist/server/index.mjs +3108 -0
  56. package/dist/server/src/bootstrap.d.ts +5 -0
  57. package/dist/server/src/config/index.d.ts +5 -0
  58. package/dist/server/src/content-types/alert-banner/index.d.ts +73 -0
  59. package/dist/server/src/content-types/alert-banner/schema.json.d.ts +73 -0
  60. package/dist/server/src/content-types/index.d.ts +253 -0
  61. package/dist/server/src/content-types/submission/index.d.ts +111 -0
  62. package/dist/server/src/content-types/submission/schema.json.d.ts +126 -0
  63. package/dist/server/src/content-types/subscriber/index.d.ts +69 -0
  64. package/dist/server/src/content-types/subscriber/schema.json.d.ts +69 -0
  65. package/dist/server/src/controllers/caseworkHandlers.d.ts +10 -0
  66. package/dist/server/src/controllers/communityHandlers.d.ts +10 -0
  67. package/dist/server/src/controllers/formHandlers.d.ts +14 -0
  68. package/dist/server/src/controllers/inboxController.d.ts +40 -0
  69. package/dist/server/src/controllers/index.d.ts +2 -0
  70. package/dist/server/src/destroy.d.ts +5 -0
  71. package/dist/server/src/index.d.ts +2 -0
  72. package/dist/server/src/register.d.ts +5 -0
  73. package/dist/server/src/routes/admin.d.ts +12 -0
  74. package/dist/server/src/routes/content-api.d.ts +13 -0
  75. package/dist/server/src/routes/index.d.ts +26 -0
  76. package/dist/server/src/services/alertService.d.ts +16 -0
  77. package/dist/server/src/services/docHelper.d.ts +28 -0
  78. package/dist/server/src/services/formBuilderMapper.d.ts +22 -0
  79. package/dist/server/src/services/formBuilderSync.d.ts +13 -0
  80. package/dist/server/src/services/formService.d.ts +40 -0
  81. package/dist/server/src/services/index.d.ts +2 -0
  82. package/dist/server/src/services/settings.d.ts +9 -0
  83. package/dist/server/src/services/submissionEcosystemBridges.d.ts +21 -0
  84. package/dist/server/src/services/submissionFormBuilderBridge.d.ts +23 -0
  85. package/dist/server/src/services/submissionService.d.ts +58 -0
  86. package/dist/server/src/services/subscriberService.d.ts +22 -0
  87. package/dist/shared/index.d.ts +1 -0
  88. package/dist/shared/index.js +917 -0
  89. package/dist/shared/index.mjs +917 -0
  90. package/package.json +125 -3
@@ -0,0 +1,917 @@
1
+ const PLUGIN_ID = "inbox";
2
+ const FORM_BUILDER_CMS_PLUGIN_ID = "strapi-plugin-form-builder-cms";
3
+ const FORM_BUILDER_CMS_FORM_UID = `plugin::${FORM_BUILDER_CMS_PLUGIN_ID}.form`;
4
+ const FORM_BUILDER_CMS_SUBMISSION_UID = `plugin::${FORM_BUILDER_CMS_PLUGIN_ID}.form-submission`;
5
+ const FORM_UID = FORM_BUILDER_CMS_FORM_UID;
6
+ const SUBMISSION_UID = `plugin::${PLUGIN_ID}.submission`;
7
+ const SUBSCRIBER_UID = `plugin::${PLUGIN_ID}.subscriber`;
8
+ const ALERT_BANNER_UID = `plugin::${PLUGIN_ID}.alert-banner`;
9
+ const DECORATIVE_FIELD_TYPES = [
10
+ "heading",
11
+ "paragraph",
12
+ "divider"
13
+ ];
14
+ const FORM_FIELD_TYPES = [
15
+ "text",
16
+ "email",
17
+ "number",
18
+ "phone",
19
+ "url",
20
+ "password",
21
+ "date",
22
+ "time",
23
+ "textarea",
24
+ "select",
25
+ "radio",
26
+ "checkbox",
27
+ "checkbox-group",
28
+ "file",
29
+ "hidden",
30
+ "heading",
31
+ "paragraph",
32
+ "divider"
33
+ ];
34
+ const FORM_CATEGORIES = [
35
+ "contact",
36
+ "service_desk",
37
+ "inquiry",
38
+ "application",
39
+ "volunteer",
40
+ "custom"
41
+ ];
42
+ const SUBMISSION_STATUSES = [
43
+ "new",
44
+ "read",
45
+ "under_review",
46
+ "scheduled",
47
+ "completed",
48
+ "rejected",
49
+ "archived"
50
+ ];
51
+ const SUBMISSION_PRIORITIES = [
52
+ "low",
53
+ "normal",
54
+ "high",
55
+ "urgent"
56
+ ];
57
+ const SUBSCRIBER_STATUSES = [
58
+ "pending",
59
+ "confirmed",
60
+ "unsubscribed"
61
+ ];
62
+ const ALERT_SEVERITIES = [
63
+ "info",
64
+ "warning",
65
+ "urgent",
66
+ "celebration"
67
+ ];
68
+ const CAPTCHA_PROVIDERS = [
69
+ "none",
70
+ "turnstile",
71
+ "recaptcha"
72
+ ];
73
+ const CAPTCHA_ENDPOINTS = {
74
+ turnstile: "https://challenges.cloudflare.com/turnstile/v0/siteverify",
75
+ recaptcha: "https://www.google.com/recaptcha/api/siteverify"
76
+ };
77
+ const DEFAULT_INBOX_SETTINGS = {
78
+ primaryCalendar: "gregory",
79
+ secondaryCalendars: ["islamic-umalqura"],
80
+ defaultLocale: "es",
81
+ supportedLocales: ["es", "en", "ar"],
82
+ defaultRateLimitPerHour: 15,
83
+ defaultHoneypotField: "_website_url",
84
+ defaultCaptchaProvider: "none",
85
+ turnstileSiteKey: "",
86
+ turnstileSecretKey: "",
87
+ recaptchaSiteKey: "",
88
+ recaptchaSecretKey: "",
89
+ enableDoubleOptIn: true,
90
+ availableTopics: ["announcements", "events", "courses", "newsletter"],
91
+ availableDepartments: [
92
+ "general",
93
+ "administration",
94
+ "services",
95
+ "education",
96
+ "social_aid"
97
+ ],
98
+ enableFormBuilderBridge: true,
99
+ autoSyncFormBuilderForms: true,
100
+ enableBookingBridge: true,
101
+ enableCertificateBridge: true,
102
+ defaultNotifyEmails: [],
103
+ outboundWebhookUrl: ""
104
+ };
105
+ function isValidEmail(value) {
106
+ if (/\s/.test(value))
107
+ return false;
108
+ const at = value.indexOf("@");
109
+ if (at < 1 || at !== value.lastIndexOf("@"))
110
+ return false;
111
+ const domain = value.slice(at + 1);
112
+ const dot = domain.indexOf(".");
113
+ return dot > 0 && dot < domain.length - 1;
114
+ }
115
+ function freezeOptions(items) {
116
+ return Object.freeze(items.map((item) => Object.freeze({ ...item })));
117
+ }
118
+ freezeOptions([
119
+ { value: "es", label: "es — Español (Spanish)" },
120
+ { value: "en", label: "en — English" },
121
+ { value: "ar", label: "ar — العربية (Arabic)" },
122
+ { value: "tr", label: "tr — Türkçe (Turkish)" },
123
+ { value: "fr", label: "fr — Français (French)" },
124
+ { value: "de", label: "de — Deutsch (German)" },
125
+ { value: "pt", label: "pt — Português (Portuguese)" },
126
+ { value: "it", label: "it — Italiano (Italian)" },
127
+ { value: "nl", label: "nl — Nederlands (Dutch)" },
128
+ { value: "pl", label: "pl — Polski (Polish)" },
129
+ { value: "ru", label: "ru — Русский (Russian)" },
130
+ { value: "id", label: "id — Bahasa Indonesia" },
131
+ { value: "ur", label: "ur — اردو (Urdu)" },
132
+ { value: "zh", label: "zh — 中文 (Chinese)" }
133
+ ]);
134
+ freezeOptions([
135
+ {
136
+ value: "islamic-umalqura",
137
+ label: "islamic-umalqura (Hijri Umm al-Qura)"
138
+ },
139
+ { value: "islamic-civil", label: "islamic-civil (Tabular Islamic Civil)" },
140
+ { value: "islamic-tbla", label: "islamic-tbla (Tabular Astronomical)" },
141
+ { value: "hebrew", label: "hebrew (Hebrew)" },
142
+ { value: "persian", label: "persian (Solar Hijri)" },
143
+ { value: "none", label: "none (Gregorian Only)" }
144
+ ]);
145
+ const LOCALIZED_REQUIRED_FALLBACK = {
146
+ es: (label) => `${label} es obligatorio`,
147
+ en: (label) => `${label} is required`,
148
+ ar: (label) => `${label} مطلوب`
149
+ };
150
+ function isValueEmpty(value) {
151
+ if (value === null || value === void 0) return true;
152
+ if (typeof value === "string") return value.trim().length === 0;
153
+ if (Array.isArray(value)) return value.length === 0;
154
+ if (typeof value === "boolean") return value === false;
155
+ return false;
156
+ }
157
+ function resolveRuleMessage(rule, locale, fallback) {
158
+ if (!rule) return fallback;
159
+ if (rule.localizedMessages && rule.localizedMessages[locale]) {
160
+ return rule.localizedMessages[locale];
161
+ }
162
+ if (rule.message) return rule.message;
163
+ return fallback;
164
+ }
165
+ function isValidHttpUrl(strVal) {
166
+ try {
167
+ const parsed = new URL(strVal);
168
+ return ["http:", "https:"].includes(parsed.protocol);
169
+ } catch {
170
+ return false;
171
+ }
172
+ }
173
+ function checkFieldOptions(field, rawValue, strVal, fieldLabel) {
174
+ if (!Array.isArray(field.options) || field.options.length === 0) return null;
175
+ const allowed = new Set(field.options.map((o) => o.value));
176
+ if (field.type === "select" || field.type === "radio") {
177
+ return allowed.has(strVal) ? null : `${fieldLabel} contains an invalid selection`;
178
+ }
179
+ if (field.type === "checkbox-group") {
180
+ const values = Array.isArray(rawValue) ? rawValue.map(String) : [strVal];
181
+ return values.some((v) => !allowed.has(v)) ? `${fieldLabel} contains an invalid selection` : null;
182
+ }
183
+ return null;
184
+ }
185
+ function validateFieldTypeAndOptions(field, rawValue, strVal, fieldLabel, locale, rules) {
186
+ const fieldErrors = [];
187
+ if (field.type === "email" && !isValidEmail(strVal)) {
188
+ const emailRule = rules.find((r) => r.type === "email");
189
+ fieldErrors.push(
190
+ resolveRuleMessage(
191
+ emailRule,
192
+ locale,
193
+ `${fieldLabel} must be a valid email address`
194
+ )
195
+ );
196
+ }
197
+ if (field.type === "url" && !isValidHttpUrl(strVal)) {
198
+ const urlRule = rules.find((r) => r.type === "url");
199
+ fieldErrors.push(
200
+ resolveRuleMessage(urlRule, locale, `${fieldLabel} must be a valid URL`)
201
+ );
202
+ }
203
+ const optError = checkFieldOptions(field, rawValue, strVal, fieldLabel);
204
+ if (optError) fieldErrors.push(optError);
205
+ return fieldErrors;
206
+ }
207
+ function checkLengthRule(rule, limit, strVal, fieldLabel, locale) {
208
+ if (rule.type === "minLength" && strVal.length < limit) {
209
+ return resolveRuleMessage(
210
+ rule,
211
+ locale,
212
+ `${fieldLabel} must be at least ${limit} characters`
213
+ );
214
+ }
215
+ if (rule.type === "maxLength" && strVal.length > limit) {
216
+ return resolveRuleMessage(
217
+ rule,
218
+ locale,
219
+ `${fieldLabel} must be at most ${limit} characters`
220
+ );
221
+ }
222
+ return null;
223
+ }
224
+ function checkNumericBoundRule(rule, limit, rawValue, fieldLabel, locale) {
225
+ const num = Number(rawValue);
226
+ if (rule.type === "min" && (Number.isNaN(num) || num < limit)) {
227
+ return resolveRuleMessage(
228
+ rule,
229
+ locale,
230
+ `${fieldLabel} must be at least ${limit}`
231
+ );
232
+ }
233
+ if (rule.type === "max" && (Number.isNaN(num) || num > limit)) {
234
+ return resolveRuleMessage(
235
+ rule,
236
+ locale,
237
+ `${fieldLabel} must be at most ${limit}`
238
+ );
239
+ }
240
+ return null;
241
+ }
242
+ function checkLengthOrNumberRule(rule, rawValue, strVal, fieldLabel, locale) {
243
+ if (typeof rule.value !== "number") return null;
244
+ return checkLengthRule(rule, rule.value, strVal, fieldLabel, locale) ?? checkNumericBoundRule(rule, rule.value, rawValue, fieldLabel, locale);
245
+ }
246
+ function checkPatternOrEmailRule(rule, fieldType, strVal, fieldLabel, locale) {
247
+ if (rule.type === "pattern" && typeof rule.value === "string") {
248
+ try {
249
+ const regex = new RegExp(rule.value);
250
+ if (!regex.test(strVal)) {
251
+ return resolveRuleMessage(
252
+ rule,
253
+ locale,
254
+ `${fieldLabel} format is invalid`
255
+ );
256
+ }
257
+ } catch {
258
+ }
259
+ }
260
+ if (rule.type === "email" && fieldType !== "email" && !isValidEmail(strVal)) {
261
+ return resolveRuleMessage(
262
+ rule,
263
+ locale,
264
+ `${fieldLabel} must be a valid email address`
265
+ );
266
+ }
267
+ return null;
268
+ }
269
+ function evaluateCustomRules(field, rawValue, strVal, fieldLabel, locale, rules) {
270
+ const errors = [];
271
+ for (const rule of rules) {
272
+ if (rule.type === "required") continue;
273
+ const lenOrNumErr = checkLengthOrNumberRule(
274
+ rule,
275
+ rawValue,
276
+ strVal,
277
+ fieldLabel,
278
+ locale
279
+ );
280
+ if (lenOrNumErr) errors.push(lenOrNumErr);
281
+ const patOrEmailErr = checkPatternOrEmailRule(
282
+ rule,
283
+ field.type,
284
+ strVal,
285
+ fieldLabel,
286
+ locale
287
+ );
288
+ if (patOrEmailErr) errors.push(patOrEmailErr);
289
+ }
290
+ return errors;
291
+ }
292
+ function resolveFieldLabel(field, locale) {
293
+ return field.localizedLabels?.[locale] || field.label || field.name;
294
+ }
295
+ function buildRequiredError(rules, locale, fieldLabel) {
296
+ const reqRule = rules.find((r) => r.type === "required");
297
+ const fallbackFn = LOCALIZED_REQUIRED_FALLBACK[locale] || LOCALIZED_REQUIRED_FALLBACK.en;
298
+ return resolveRuleMessage(reqRule, locale, fallbackFn(fieldLabel));
299
+ }
300
+ function validateSingleField(field, data, locale) {
301
+ const rawValue = data[field.name] ?? field.defaultValue;
302
+ const fieldLabel = resolveFieldLabel(field, locale);
303
+ const rules = field.validation || [];
304
+ if (field.required && isValueEmpty(rawValue)) {
305
+ return {
306
+ fieldErrors: [buildRequiredError(rules, locale, fieldLabel)],
307
+ cleanedValue: null
308
+ };
309
+ }
310
+ if (isValueEmpty(rawValue)) {
311
+ return { fieldErrors: [], cleanedValue: rawValue ?? null };
312
+ }
313
+ const strVal = typeof rawValue === "string" ? rawValue.trim() : String(rawValue);
314
+ const fieldErrors = [
315
+ ...validateFieldTypeAndOptions(
316
+ field,
317
+ rawValue,
318
+ strVal,
319
+ fieldLabel,
320
+ locale,
321
+ rules
322
+ ),
323
+ ...evaluateCustomRules(field, rawValue, strVal, fieldLabel, locale, rules)
324
+ ];
325
+ return {
326
+ fieldErrors,
327
+ cleanedValue: typeof rawValue === "string" ? strVal : rawValue
328
+ };
329
+ }
330
+ function validateFormSubmission(fields, data, locale = "en") {
331
+ const errors = {};
332
+ const cleanedData = {};
333
+ for (const field of fields) {
334
+ if (DECORATIVE_FIELD_TYPES.includes(field.type)) continue;
335
+ const { fieldErrors, cleanedValue } = validateSingleField(
336
+ field,
337
+ data,
338
+ locale
339
+ );
340
+ if (fieldErrors.length > 0) {
341
+ errors[field.name] = fieldErrors;
342
+ } else {
343
+ cleanedData[field.name] = cleanedValue;
344
+ }
345
+ }
346
+ return {
347
+ valid: Object.keys(errors).length === 0,
348
+ errors,
349
+ cleanedData
350
+ };
351
+ }
352
+ function isHoneypotTriggered(payload, honeypotField = "_website_url") {
353
+ if (!honeypotField) return false;
354
+ const val = payload[honeypotField];
355
+ return typeof val === "string" && val.trim().length > 0;
356
+ }
357
+ class SlidingWindowRateLimiter {
358
+ hits = /* @__PURE__ */ new Map();
359
+ check(ip, bucketKey, limitPerHour, nowMs = Date.now()) {
360
+ if (limitPerHour <= 0) {
361
+ return { allowed: true, remaining: 999, retryAfterSeconds: 0 };
362
+ }
363
+ const windowMs = 60 * 60 * 1e3;
364
+ const key = `${bucketKey}::${ip || "unknown"}`;
365
+ const cutoff = nowMs - windowMs;
366
+ const previous = (this.hits.get(key) || []).filter((ts) => ts > cutoff);
367
+ if (previous.length >= limitPerHour) {
368
+ const oldest = previous[0] ?? nowMs;
369
+ const retryAfterSeconds = Math.max(
370
+ 1,
371
+ Math.ceil((oldest + windowMs - nowMs) / 1e3)
372
+ );
373
+ this.hits.set(key, previous);
374
+ return {
375
+ allowed: false,
376
+ remaining: 0,
377
+ retryAfterSeconds
378
+ };
379
+ }
380
+ previous.push(nowMs);
381
+ this.hits.set(key, previous);
382
+ return {
383
+ allowed: true,
384
+ remaining: Math.max(0, limitPerHour - previous.length),
385
+ retryAfterSeconds: 0
386
+ };
387
+ }
388
+ reset() {
389
+ this.hits.clear();
390
+ }
391
+ }
392
+ const defaultRateLimiter = new SlidingWindowRateLimiter();
393
+ function resolveFetchRunner(fetchImpl) {
394
+ return fetchImpl ?? globalThis.fetch;
395
+ }
396
+ async function postCaptchaVerify(runner, url, params) {
397
+ try {
398
+ const res = await runner(url, {
399
+ method: "POST",
400
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
401
+ body: params.toString()
402
+ });
403
+ return await res.json();
404
+ } catch {
405
+ return null;
406
+ }
407
+ }
408
+ async function verifyCaptchaToken(provider, secret, token, ip = "", fetchImpl) {
409
+ if (provider === "none") return true;
410
+ const url = CAPTCHA_ENDPOINTS[provider];
411
+ const trimmedSecret = (secret || "").trim();
412
+ const trimmedToken = (token || "").trim();
413
+ const runner = resolveFetchRunner(fetchImpl);
414
+ if (!url || !trimmedSecret || !trimmedToken || !runner) return false;
415
+ const params = new URLSearchParams({
416
+ secret: trimmedSecret,
417
+ response: trimmedToken
418
+ });
419
+ if (ip) params.append("remoteip", ip);
420
+ const json = await postCaptchaVerify(runner, url, params);
421
+ return Boolean(json?.success);
422
+ }
423
+ async function testCaptchaSecretKey(provider, secret, fetchImpl) {
424
+ if (provider === "none") return { ok: true };
425
+ const url = CAPTCHA_ENDPOINTS[provider];
426
+ if (!url) return { ok: false, reason: "Unknown CAPTCHA provider" };
427
+ const trimmedSecret = (secret || "").trim();
428
+ if (!trimmedSecret) return { ok: false, reason: "Secret key is empty" };
429
+ const runner = resolveFetchRunner(fetchImpl);
430
+ if (!runner) return { ok: false, reason: "Fetch implementation unavailable" };
431
+ const params = new URLSearchParams({
432
+ secret: trimmedSecret,
433
+ response: "test-token"
434
+ });
435
+ const json = await postCaptchaVerify(runner, url, params);
436
+ if (!json) {
437
+ return { ok: false, reason: "Could not reach the CAPTCHA provider" };
438
+ }
439
+ const codes = json["error-codes"] || [];
440
+ if (codes.includes("invalid-input-secret") || codes.includes("missing-input-secret")) {
441
+ return { ok: false, reason: "Secret key is invalid" };
442
+ }
443
+ return { ok: true };
444
+ }
445
+ const STARTER_FORM_TEMPLATES = [
446
+ {
447
+ id: "general-contact",
448
+ name: "General Contact & Inquiry",
449
+ description: "Multi-language contact form with subject routing and anti-spam protection.",
450
+ form: {
451
+ title: "General Contact",
452
+ slug: "general-contact",
453
+ description: "Send us a message and our team will respond promptly.",
454
+ category: "contact",
455
+ active: true,
456
+ publishedAt: "2026-01-01T00:00:00.000Z",
457
+ translations: {
458
+ es: {
459
+ title: "Contacto General",
460
+ description: "Envíanos un mensaje y nuestro equipo te responderá pronto.",
461
+ submitButtonLabel: "Enviar mensaje",
462
+ successMessage: "Gracias por contactarnos. Hemos recibido tu mensaje."
463
+ },
464
+ en: {
465
+ title: "General Contact",
466
+ description: "Send us a message and our team will respond promptly.",
467
+ submitButtonLabel: "Send message",
468
+ successMessage: "Thank you for reaching out. We have received your message."
469
+ },
470
+ ar: {
471
+ title: "الاتصال العام",
472
+ description: "أرسل لنا رسالة وسيرد فريقنا في أقرب وقت.",
473
+ submitButtonLabel: "إرسال الرسالة",
474
+ successMessage: "شكراً لتواصلك معنا. لقد استلمنا رسالتك."
475
+ }
476
+ },
477
+ fields: [
478
+ {
479
+ id: "fld_full_name",
480
+ type: "text",
481
+ name: "fullName",
482
+ label: "Full Name",
483
+ required: true,
484
+ width: "half",
485
+ localizedLabels: {
486
+ es: "Nombre completo",
487
+ en: "Full Name",
488
+ ar: "الاسم الكامل"
489
+ },
490
+ validation: [{ type: "minLength", value: 2 }]
491
+ },
492
+ {
493
+ id: "fld_email",
494
+ type: "email",
495
+ name: "email",
496
+ label: "Email Address",
497
+ required: true,
498
+ width: "half",
499
+ localizedLabels: {
500
+ es: "Correo electrónico",
501
+ en: "Email Address",
502
+ ar: "البريد الإلكتروني"
503
+ }
504
+ },
505
+ {
506
+ id: "fld_phone",
507
+ type: "phone",
508
+ name: "phone",
509
+ label: "Phone / WhatsApp",
510
+ required: false,
511
+ width: "half",
512
+ localizedLabels: {
513
+ es: "Teléfono / WhatsApp",
514
+ en: "Phone / WhatsApp",
515
+ ar: "الهاتف / واتساب"
516
+ }
517
+ },
518
+ {
519
+ id: "fld_subject",
520
+ type: "select",
521
+ name: "subject",
522
+ label: "Subject",
523
+ required: true,
524
+ width: "half",
525
+ localizedLabels: {
526
+ es: "Asunto",
527
+ en: "Subject",
528
+ ar: "الموضوع"
529
+ },
530
+ options: [
531
+ {
532
+ value: "general",
533
+ label: "General Inquiry",
534
+ localizedLabels: {
535
+ es: "Consulta general",
536
+ en: "General Inquiry",
537
+ ar: "استفسار عام"
538
+ }
539
+ },
540
+ {
541
+ value: "visit",
542
+ label: "Guided Visit",
543
+ localizedLabels: {
544
+ es: "Visita guiada",
545
+ en: "Guided Visit",
546
+ ar: "زيارة إرشادية"
547
+ }
548
+ },
549
+ {
550
+ value: "support",
551
+ label: "Support & Assistance",
552
+ localizedLabels: {
553
+ es: "Apoyo y asistencia",
554
+ en: "Support & Assistance",
555
+ ar: "الدعم والمساعدة"
556
+ }
557
+ }
558
+ ]
559
+ },
560
+ {
561
+ id: "fld_message",
562
+ type: "textarea",
563
+ name: "message",
564
+ label: "Message",
565
+ required: true,
566
+ width: "full",
567
+ localizedLabels: {
568
+ es: "Mensaje",
569
+ en: "Message",
570
+ ar: "الرسالة"
571
+ },
572
+ validation: [
573
+ { type: "minLength", value: 10 },
574
+ { type: "maxLength", value: 2e3 }
575
+ ]
576
+ }
577
+ ],
578
+ settings: {
579
+ honeypotField: "_website_url",
580
+ rateLimitPerHour: 15,
581
+ captchaProvider: "none",
582
+ defaultPriority: "normal",
583
+ department: "general"
584
+ }
585
+ }
586
+ },
587
+ {
588
+ id: "service-desk-application",
589
+ name: "Service-Desk Casework & Document Checklist",
590
+ description: "Casework intake form with document verification checklist, booking bridge, and certificate bridge.",
591
+ form: {
592
+ title: "Formal Service Request",
593
+ slug: "service-request",
594
+ description: "Submit an official service application and required supporting documents.",
595
+ category: "service_desk",
596
+ active: true,
597
+ publishedAt: "2026-01-01T00:00:00.000Z",
598
+ translations: {
599
+ es: {
600
+ title: "Solicitud Formal de Servicio",
601
+ description: "Envía una solicitud oficial de servicio con los documentos requeridos.",
602
+ submitButtonLabel: "Radicar solicitud",
603
+ successMessage: "Tu solicitud ha sido radicada con éxito."
604
+ },
605
+ en: {
606
+ title: "Formal Service Request",
607
+ description: "Submit an official service application and required supporting documents.",
608
+ submitButtonLabel: "Submit application",
609
+ successMessage: "Your application has been registered."
610
+ },
611
+ ar: {
612
+ title: "طلب خدمة رسمي",
613
+ description: "قدم طلب خدمة رسمي مع المستندات المطلوبة.",
614
+ submitButtonLabel: "تقديم الطلب",
615
+ successMessage: "تم تسجيل طلبك بنجاح."
616
+ }
617
+ },
618
+ fields: [
619
+ {
620
+ id: "fld_applicant_name",
621
+ type: "text",
622
+ name: "applicantName",
623
+ label: "Applicant Full Name",
624
+ required: true,
625
+ width: "half",
626
+ localizedLabels: {
627
+ es: "Nombre completo del solicitante",
628
+ en: "Applicant Full Name",
629
+ ar: "الاسم الكامل لمقدم الطلب"
630
+ }
631
+ },
632
+ {
633
+ id: "fld_applicant_email",
634
+ type: "email",
635
+ name: "applicantEmail",
636
+ label: "Applicant Email",
637
+ required: true,
638
+ width: "half",
639
+ localizedLabels: {
640
+ es: "Correo del solicitante",
641
+ en: "Applicant Email",
642
+ ar: "البريد الإلكتروني لمقدم الطلب"
643
+ }
644
+ },
645
+ {
646
+ id: "fld_applicant_phone",
647
+ type: "phone",
648
+ name: "applicantPhone",
649
+ label: "Contact Phone",
650
+ required: true,
651
+ width: "half",
652
+ localizedLabels: {
653
+ es: "Teléfono de contacto",
654
+ en: "Contact Phone",
655
+ ar: "هاتف الاتصال"
656
+ }
657
+ },
658
+ {
659
+ id: "fld_preferred_date",
660
+ type: "date",
661
+ name: "preferredDate",
662
+ label: "Preferred Appointment Date",
663
+ required: false,
664
+ width: "half",
665
+ localizedLabels: {
666
+ es: "Fecha preferida de cita",
667
+ en: "Preferred Appointment Date",
668
+ ar: "تاريخ الموعد المفضل"
669
+ }
670
+ },
671
+ {
672
+ id: "fld_details",
673
+ type: "textarea",
674
+ name: "details",
675
+ label: "Case Details & Notes",
676
+ required: true,
677
+ width: "full",
678
+ localizedLabels: {
679
+ es: "Detalles del caso y observaciones",
680
+ en: "Case Details & Notes",
681
+ ar: "تفاصيل الطلب والملاحظات"
682
+ }
683
+ }
684
+ ],
685
+ checklistTemplate: [
686
+ {
687
+ id: "chk_id_document",
688
+ label: "Government ID / Passport Copy",
689
+ required: true,
690
+ verified: false,
691
+ localizedLabels: {
692
+ es: "Copia de documento de identidad / Pasaporte",
693
+ en: "Government ID / Passport Copy",
694
+ ar: "نسخة من بطاقة الهوية أو جواز السفر"
695
+ }
696
+ },
697
+ {
698
+ id: "chk_supporting_form",
699
+ label: "Signed Application Form",
700
+ required: true,
701
+ verified: false,
702
+ localizedLabels: {
703
+ es: "Formulario de solicitud firmado",
704
+ en: "Signed Application Form",
705
+ ar: "نموذج الطلب الموقع"
706
+ }
707
+ }
708
+ ],
709
+ settings: {
710
+ honeypotField: "_website_url",
711
+ rateLimitPerHour: 10,
712
+ captchaProvider: "none",
713
+ enableBookingBridge: true,
714
+ enableCertificateBridge: true,
715
+ defaultPriority: "normal",
716
+ department: "services"
717
+ }
718
+ }
719
+ }
720
+ ];
721
+ function localizeFormForLocale(form, locale) {
722
+ const tr = form.translations?.[locale];
723
+ const localizedFields = (form.fields || []).map((field) => ({
724
+ ...field,
725
+ label: field.localizedLabels?.[locale] || field.label,
726
+ placeholder: field.localizedPlaceholders?.[locale] || field.placeholder,
727
+ helpText: field.localizedHelpTexts?.[locale] || field.helpText,
728
+ options: field.options?.map((opt) => ({
729
+ ...opt,
730
+ label: opt.localizedLabels?.[locale] || opt.label
731
+ }))
732
+ }));
733
+ const localizedChecklist = form.checklistTemplate?.map((item) => ({
734
+ ...item,
735
+ label: item.localizedLabels?.[locale] || item.label
736
+ }));
737
+ return {
738
+ ...form,
739
+ title: tr?.title || form.title,
740
+ description: tr?.description || form.description,
741
+ fields: localizedFields,
742
+ checklistTemplate: localizedChecklist,
743
+ settings: {
744
+ ...form.settings,
745
+ submitButtonLabel: tr?.submitButtonLabel || form.settings?.localizedSubmitLabels?.[locale] || form.settings?.submitButtonLabel,
746
+ successMessage: tr?.successMessage || form.settings?.localizedSuccessMessages?.[locale] || form.settings?.successMessage
747
+ }
748
+ };
749
+ }
750
+ const FORMULA_PREFIXES = ["=", "+", "-", "@", " ", "\r"];
751
+ function escapeCsvCell(value) {
752
+ if (value === null || value === void 0) return "";
753
+ const raw = typeof value === "object" ? JSON.stringify(value) : String(value);
754
+ const sanitized = raw.length > 0 && FORMULA_PREFIXES.includes(raw[0]) ? `'${raw}` : raw;
755
+ if (sanitized.includes('"') || sanitized.includes(",") || sanitized.includes("\n") || sanitized.includes("\r")) {
756
+ return `"${sanitized.replace(/"/g, '""')}"`;
757
+ }
758
+ return sanitized;
759
+ }
760
+ function collectDataFieldNames(submissions, fields) {
761
+ const dataFieldNames = [];
762
+ const seen = /* @__PURE__ */ new Set();
763
+ for (const field of fields) {
764
+ if (!DECORATIVE_FIELD_TYPES.includes(field.type) && !seen.has(field.name)) {
765
+ seen.add(field.name);
766
+ dataFieldNames.push(field.name);
767
+ }
768
+ }
769
+ for (const sub of submissions) {
770
+ for (const key of Object.keys(sub.data || {})) {
771
+ if (!seen.has(key)) {
772
+ seen.add(key);
773
+ dataFieldNames.push(key);
774
+ }
775
+ }
776
+ }
777
+ return dataFieldNames;
778
+ }
779
+ function getBaseSubmissionValues(sub) {
780
+ return [
781
+ sub.referenceCode,
782
+ sub.formSlug,
783
+ sub.status,
784
+ sub.priority,
785
+ sub.department ?? "",
786
+ sub.locale,
787
+ sub.applicantName ?? "",
788
+ sub.applicantEmail ?? "",
789
+ sub.applicantPhone ?? "",
790
+ sub.submittedAt,
791
+ sub.secondaryDate?.formatted ?? "",
792
+ sub.linkedBookingId ?? "",
793
+ sub.linkedCertificateNumber ?? ""
794
+ ];
795
+ }
796
+ const BASE_CSV_HEADERS = [
797
+ "referenceCode",
798
+ "formSlug",
799
+ "status",
800
+ "priority",
801
+ "department",
802
+ "locale",
803
+ "applicantName",
804
+ "applicantEmail",
805
+ "applicantPhone",
806
+ "submittedAt",
807
+ "secondaryDate",
808
+ "linkedBookingId",
809
+ "linkedCertificateNumber"
810
+ ];
811
+ function submissionsToCsv(submissions, fields = []) {
812
+ const dataFieldNames = collectDataFieldNames(submissions, fields);
813
+ const headers = [...BASE_CSV_HEADERS, ...dataFieldNames];
814
+ const rows = [headers.map(escapeCsvCell).join(",")];
815
+ for (const sub of submissions) {
816
+ const baseValues = getBaseSubmissionValues(sub);
817
+ const dynamicValues = dataFieldNames.map((name) => sub.data?.[name] ?? "");
818
+ rows.push([...baseValues, ...dynamicValues].map(escapeCsvCell).join(","));
819
+ }
820
+ return rows.join("\n");
821
+ }
822
+ const SUPPORTED_INTL_CALENDARS = [
823
+ { id: "gregory", intlCalendar: "gregory", defaultLabel: "Gregorian" },
824
+ { id: "iso8601", intlCalendar: "iso8601", defaultLabel: "ISO 8601" },
825
+ {
826
+ id: "islamic-umalqura",
827
+ intlCalendar: "islamic-umalqura",
828
+ defaultLabel: "Hijri (Umm al-Qura)"
829
+ },
830
+ {
831
+ id: "islamic-civil",
832
+ intlCalendar: "islamic-civil",
833
+ defaultLabel: "Hijri (Civil)"
834
+ },
835
+ {
836
+ id: "islamic-tbla",
837
+ intlCalendar: "islamic-tbla",
838
+ defaultLabel: "Hijri (Tabular)"
839
+ },
840
+ { id: "hebrew", intlCalendar: "hebrew", defaultLabel: "Hebrew" },
841
+ {
842
+ id: "persian",
843
+ intlCalendar: "persian",
844
+ defaultLabel: "Persian (Solar Hijri)"
845
+ },
846
+ { id: "buddhist", intlCalendar: "buddhist", defaultLabel: "Buddhist" },
847
+ { id: "japanese", intlCalendar: "japanese", defaultLabel: "Japanese" },
848
+ { id: "chinese", intlCalendar: "chinese", defaultLabel: "Chinese Lunar" },
849
+ { id: "indian", intlCalendar: "indian", defaultLabel: "Indian National" },
850
+ { id: "coptic", intlCalendar: "coptic", defaultLabel: "Coptic" },
851
+ { id: "ethiopic", intlCalendar: "ethiopic", defaultLabel: "Ethiopic" }
852
+ ];
853
+ function convertIsoDateToCalendar(isoDateString, calendar = "islamic-umalqura", locale = "en") {
854
+ if (!isoDateString) {
855
+ return { calendar, formatted: "" };
856
+ }
857
+ const date = new Date(isoDateString);
858
+ if (Number.isNaN(date.getTime())) {
859
+ return { calendar, formatted: isoDateString };
860
+ }
861
+ try {
862
+ const formatter = new Intl.DateTimeFormat(`${locale}-u-ca-${calendar}`, {
863
+ year: "numeric",
864
+ month: "short",
865
+ day: "numeric"
866
+ });
867
+ const parts = formatter.formatToParts(date);
868
+ const year = Number(parts.find((p) => p.type === "year")?.value) || void 0;
869
+ const month = Number(parts.find((p) => p.type === "month")?.value) || void 0;
870
+ const day = Number(parts.find((p) => p.type === "day")?.value) || void 0;
871
+ return {
872
+ calendar,
873
+ formatted: formatter.format(date),
874
+ year,
875
+ month,
876
+ day
877
+ };
878
+ } catch {
879
+ return {
880
+ calendar,
881
+ formatted: date.toISOString().slice(0, 10)
882
+ };
883
+ }
884
+ }
885
+ export {
886
+ ALERT_BANNER_UID,
887
+ ALERT_SEVERITIES,
888
+ CAPTCHA_ENDPOINTS,
889
+ CAPTCHA_PROVIDERS,
890
+ DECORATIVE_FIELD_TYPES,
891
+ DEFAULT_INBOX_SETTINGS,
892
+ FORM_BUILDER_CMS_FORM_UID,
893
+ FORM_BUILDER_CMS_PLUGIN_ID,
894
+ FORM_BUILDER_CMS_SUBMISSION_UID,
895
+ FORM_CATEGORIES,
896
+ FORM_FIELD_TYPES,
897
+ FORM_UID,
898
+ PLUGIN_ID,
899
+ STARTER_FORM_TEMPLATES,
900
+ SUBMISSION_PRIORITIES,
901
+ SUBMISSION_STATUSES,
902
+ SUBMISSION_UID,
903
+ SUBSCRIBER_STATUSES,
904
+ SUBSCRIBER_UID,
905
+ SUPPORTED_INTL_CALENDARS,
906
+ SlidingWindowRateLimiter,
907
+ convertIsoDateToCalendar,
908
+ defaultRateLimiter,
909
+ escapeCsvCell,
910
+ isHoneypotTriggered,
911
+ isValueEmpty,
912
+ localizeFormForLocale,
913
+ submissionsToCsv,
914
+ testCaptchaSecretKey,
915
+ validateFormSubmission,
916
+ verifyCaptchaToken
917
+ };