@vicacha-devs/strapi-plugin-inbox 1.0.0 → 1.1.1

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.
@@ -102,15 +102,122 @@ const DEFAULT_INBOX_SETTINGS = {
102
102
  defaultNotifyEmails: [],
103
103
  outboundWebhookUrl: ""
104
104
  };
105
+ const SUPPORTED_INTL_CALENDARS = Object.freeze([
106
+ { id: "gregory", intlCalendar: "gregory", defaultLabel: "Gregorian" },
107
+ { id: "iso8601", intlCalendar: "iso8601", defaultLabel: "ISO 8601" },
108
+ {
109
+ id: "islamic-umalqura",
110
+ intlCalendar: "islamic-umalqura",
111
+ defaultLabel: "Hijri (Umm al-Qura)"
112
+ },
113
+ {
114
+ id: "islamic-civil",
115
+ intlCalendar: "islamic-civil",
116
+ defaultLabel: "Hijri (Civil)"
117
+ },
118
+ {
119
+ id: "islamic-tbla",
120
+ intlCalendar: "islamic-tbla",
121
+ defaultLabel: "Hijri (Tabular)"
122
+ },
123
+ { id: "hebrew", intlCalendar: "hebrew", defaultLabel: "Hebrew" },
124
+ {
125
+ id: "persian",
126
+ intlCalendar: "persian",
127
+ defaultLabel: "Persian (Solar Hijri)"
128
+ },
129
+ { id: "buddhist", intlCalendar: "buddhist", defaultLabel: "Buddhist" },
130
+ { id: "japanese", intlCalendar: "japanese", defaultLabel: "Japanese" },
131
+ { id: "chinese", intlCalendar: "chinese", defaultLabel: "Chinese Lunar" },
132
+ { id: "indian", intlCalendar: "indian", defaultLabel: "Indian National" },
133
+ { id: "coptic", intlCalendar: "coptic", defaultLabel: "Coptic" },
134
+ { id: "ethiopic", intlCalendar: "ethiopic", defaultLabel: "Ethiopic" }
135
+ ]);
136
+ Object.freeze([
137
+ "none",
138
+ ...SUPPORTED_INTL_CALENDARS.map((item) => item.id)
139
+ ]);
140
+ const BASE_BCP47_REGEX = /^[a-zA-Z]{2,3}(?:-[a-zA-Z0-9]{2,8})*$/;
141
+ function sanitizeCalendarBaseLocale(locale, fallbackLocale = "es") {
142
+ if (typeof locale !== "string" || !locale.trim()) {
143
+ return fallbackLocale;
144
+ }
145
+ const trimmed = locale.trim();
146
+ const uIdx = trimmed.toLowerCase().indexOf("-u-");
147
+ let stripped = uIdx >= 0 ? trimmed.slice(0, uIdx) : trimmed;
148
+ while (stripped.endsWith("-")) {
149
+ stripped = stripped.slice(0, -1);
150
+ }
151
+ if (!stripped || stripped.length > 35 || !BASE_BCP47_REGEX.test(stripped)) {
152
+ return fallbackLocale;
153
+ }
154
+ return stripped;
155
+ }
156
+ function formatSecondaryDate(isoDateString, calendar = "islamic-umalqura", locale = "es") {
157
+ if (!isoDateString || calendar === "none") {
158
+ return { calendar, formatted: "" };
159
+ }
160
+ const date = new Date(isoDateString);
161
+ if (Number.isNaN(date.getTime())) {
162
+ return { calendar, formatted: isoDateString };
163
+ }
164
+ try {
165
+ const baseLocale = sanitizeCalendarBaseLocale(locale);
166
+ const formatter = new Intl.DateTimeFormat(`${baseLocale}-u-ca-${calendar}`, {
167
+ year: "numeric",
168
+ month: "short",
169
+ day: "numeric"
170
+ });
171
+ const parts = formatter.formatToParts(date);
172
+ const year = Number(parts.find((p) => p.type === "year")?.value) || void 0;
173
+ const month = Number(parts.find((p) => p.type === "month")?.value) || void 0;
174
+ const day = Number(parts.find((p) => p.type === "day")?.value) || void 0;
175
+ return {
176
+ calendar,
177
+ formatted: formatter.format(date),
178
+ year,
179
+ month,
180
+ day
181
+ };
182
+ } catch {
183
+ return {
184
+ calendar,
185
+ formatted: date.toISOString().slice(0, 10)
186
+ };
187
+ }
188
+ }
105
189
  function isValidEmail(value) {
106
- if (/\s/.test(value))
190
+ if (typeof value !== "string" || value.length < 3 || value.length > 254) {
191
+ return false;
192
+ }
193
+ if (/[\u0000-\u0020\u007f]/.test(value) || /\s/.test(value))
107
194
  return false;
108
195
  const at = value.indexOf("@");
109
- if (at < 1 || at !== value.lastIndexOf("@"))
196
+ if (at < 1 || at > 64 || at !== value.lastIndexOf("@"))
110
197
  return false;
198
+ const local = value.slice(0, at);
199
+ if (local.startsWith(".") || local.endsWith(".") || local.includes("..")) {
200
+ return false;
201
+ }
111
202
  const domain = value.slice(at + 1);
112
- const dot = domain.indexOf(".");
113
- return dot > 0 && dot < domain.length - 1;
203
+ if (domain.startsWith(".") || domain.endsWith(".") || domain.includes("..")) {
204
+ return false;
205
+ }
206
+ const labels = domain.split(".");
207
+ if (labels.length < 2)
208
+ return false;
209
+ return labels.every((label) => label.length > 0 && label.length <= 63 && !label.startsWith("-") && !label.endsWith("-"));
210
+ }
211
+ const CSV_FORMULA_PREFIX_RE = /^[\s\u0085\u2028\u2029]*[=+\-@|]|^[\t\r\n]/;
212
+ function escapeCsvCell(value) {
213
+ if (value === null || value === void 0)
214
+ return "";
215
+ const raw = typeof value === "object" ? JSON.stringify(value) : String(value);
216
+ const sanitized = raw.length > 0 && CSV_FORMULA_PREFIX_RE.test(raw) ? `'${raw}` : raw;
217
+ if (sanitized.includes('"') || sanitized.includes(",") || sanitized.includes("\n") || sanitized.includes("\r")) {
218
+ return `"${sanitized.replace(/"/g, '""')}"`;
219
+ }
220
+ return sanitized;
114
221
  }
115
222
  function freezeOptions(items) {
116
223
  return Object.freeze(items.map((item) => Object.freeze({ ...item })));
@@ -142,6 +249,54 @@ freezeOptions([
142
249
  { value: "persian", label: "persian (Solar Hijri)" },
143
250
  { value: "none", label: "none (Gregorian Only)" }
144
251
  ]);
252
+ class SlidingWindowRateLimiter {
253
+ hits = /* @__PURE__ */ new Map();
254
+ maxTrackedKeys;
255
+ defaultWindowMs;
256
+ constructor(options) {
257
+ this.maxTrackedKeys = options?.maxTrackedKeys ?? 1e4;
258
+ this.defaultWindowMs = options?.defaultWindowMs ?? 60 * 60 * 1e3;
259
+ }
260
+ check(ip, bucketKey, limitPerWindow, nowMs = Date.now(), windowMs = this.defaultWindowMs) {
261
+ if (limitPerWindow <= 0) {
262
+ return { allowed: true, remaining: 999, retryAfterSeconds: 0 };
263
+ }
264
+ if (this.hits.size > this.maxTrackedKeys) {
265
+ const oldestKey = this.hits.keys().next().value;
266
+ if (oldestKey !== void 0) {
267
+ this.hits.delete(oldestKey);
268
+ }
269
+ }
270
+ const key = `${bucketKey}::${ip || "unknown"}`;
271
+ const cutoff = nowMs - windowMs;
272
+ const previous = (this.hits.get(key) || []).filter((ts) => ts > cutoff);
273
+ if (previous.length >= limitPerWindow) {
274
+ const oldest = previous[0] ?? nowMs;
275
+ const retryAfterSeconds = Math.max(1, Math.ceil((oldest + windowMs - nowMs) / 1e3));
276
+ this.hits.set(key, previous);
277
+ return {
278
+ allowed: false,
279
+ remaining: 0,
280
+ retryAfterSeconds
281
+ };
282
+ }
283
+ previous.push(nowMs);
284
+ this.hits.set(key, previous);
285
+ return {
286
+ allowed: true,
287
+ remaining: Math.max(0, limitPerWindow - previous.length),
288
+ retryAfterSeconds: 0
289
+ };
290
+ }
291
+ reset() {
292
+ this.hits.clear();
293
+ }
294
+ }
295
+ const FORBIDDEN_OBJECT_KEYS = /* @__PURE__ */ new Set([
296
+ "__proto__",
297
+ "constructor",
298
+ "prototype"
299
+ ]);
145
300
  const LOCALIZED_REQUIRED_FALLBACK = {
146
301
  es: (label) => `${label} es obligatorio`,
147
302
  en: (label) => `${label} is required`,
@@ -239,12 +394,23 @@ function checkNumericBoundRule(rule, limit, rawValue, fieldLabel, locale) {
239
394
  }
240
395
  return null;
241
396
  }
397
+ function hasNestedQuantifier(pattern) {
398
+ return /[+*?}]\s*\)\s*[+*?{]/.test(pattern.replaceAll(/\\./g, ""));
399
+ }
242
400
  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);
401
+ const numRuleVal = typeof rule.value === "number" ? rule.value : typeof rule.value === "string" && rule.value.trim() !== "" ? Number(rule.value) : Number.NaN;
402
+ if (!Number.isFinite(numRuleVal)) return null;
403
+ return checkLengthRule(rule, numRuleVal, strVal, fieldLabel, locale) ?? checkNumericBoundRule(rule, numRuleVal, rawValue, fieldLabel, locale);
245
404
  }
246
405
  function checkPatternOrEmailRule(rule, fieldType, strVal, fieldLabel, locale) {
247
406
  if (rule.type === "pattern" && typeof rule.value === "string") {
407
+ if (strVal.length > 4096 || rule.value.length > 512 || hasNestedQuantifier(rule.value)) {
408
+ return resolveRuleMessage(
409
+ rule,
410
+ locale,
411
+ `${fieldLabel} format is invalid`
412
+ );
413
+ }
248
414
  try {
249
415
  const regex = new RegExp(rule.value);
250
416
  if (!regex.test(strVal)) {
@@ -264,6 +430,13 @@ function checkPatternOrEmailRule(rule, fieldType, strVal, fieldLabel, locale) {
264
430
  `${fieldLabel} must be a valid email address`
265
431
  );
266
432
  }
433
+ if (rule.type === "url" && fieldType !== "url" && !isValidHttpUrl(strVal)) {
434
+ return resolveRuleMessage(
435
+ rule,
436
+ locale,
437
+ `${fieldLabel} must be a valid URL`
438
+ );
439
+ }
267
440
  return null;
268
441
  }
269
442
  function evaluateCustomRules(field, rawValue, strVal, fieldLabel, locale, rules) {
@@ -298,7 +471,7 @@ function buildRequiredError(rules, locale, fieldLabel) {
298
471
  return resolveRuleMessage(reqRule, locale, fallbackFn(fieldLabel));
299
472
  }
300
473
  function validateSingleField(field, data, locale) {
301
- const rawValue = data[field.name] ?? field.defaultValue;
474
+ const rawValue = Object.hasOwn(data, field.name) ? data[field.name] : field.defaultValue;
302
475
  const fieldLabel = resolveFieldLabel(field, locale);
303
476
  const rules = field.validation || [];
304
477
  if (field.required && isValueEmpty(rawValue)) {
@@ -331,7 +504,9 @@ function validateFormSubmission(fields, data, locale = "en") {
331
504
  const errors = {};
332
505
  const cleanedData = {};
333
506
  for (const field of fields) {
334
- if (DECORATIVE_FIELD_TYPES.includes(field.type)) continue;
507
+ if (DECORATIVE_FIELD_TYPES.includes(field.type) || FORBIDDEN_OBJECT_KEYS.has(field.name)) {
508
+ continue;
509
+ }
335
510
  const { fieldErrors, cleanedValue } = validateSingleField(
336
511
  field,
337
512
  data,
@@ -350,45 +525,10 @@ function validateFormSubmission(fields, data, locale = "en") {
350
525
  };
351
526
  }
352
527
  function isHoneypotTriggered(payload, honeypotField = "_website_url") {
353
- if (!honeypotField) return false;
528
+ if (!honeypotField || !Object.hasOwn(payload, honeypotField)) return false;
354
529
  const val = payload[honeypotField];
355
530
  return typeof val === "string" && val.trim().length > 0;
356
531
  }
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
532
  const defaultRateLimiter = new SlidingWindowRateLimiter();
393
533
  function resolveFetchRunner(fetchImpl) {
394
534
  return fetchImpl ?? globalThis.fetch;
@@ -747,16 +887,6 @@ function localizeFormForLocale(form, locale) {
747
887
  }
748
888
  };
749
889
  }
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
890
  function collectDataFieldNames(submissions, fields) {
761
891
  const dataFieldNames = [];
762
892
  const seen = /* @__PURE__ */ new Set();
@@ -819,68 +949,8 @@ function submissionsToCsv(submissions, fields = []) {
819
949
  }
820
950
  return rows.join("\n");
821
951
  }
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
952
  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
- }
953
+ return formatSecondaryDate(isoDateString, calendar, locale);
884
954
  }
885
955
  export {
886
956
  ALERT_BANNER_UID,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vicacha-devs/strapi-plugin-inbox",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Generic Strapi v5 Visual Form Builder, Service-Desk Casework Inbox, Double Opt-In Subscriber Lists & Site-Wide Alert Banners (forked & extended from devCluna/strapi-plugin-form-builder-cms)",
5
5
  "keywords": [
6
6
  "alert-banners",
@@ -83,9 +83,9 @@
83
83
  "peerDependencies": {
84
84
  "@strapi/sdk-plugin": "^5.0.0 || ^6.0.0",
85
85
  "@strapi/strapi": "^5.0.0",
86
- "@vicacha-devs/strapi-plugin-bookings": "^1.0.0",
87
- "@vicacha-devs/strapi-plugin-calendar": "^1.0.0",
88
- "@vicacha-devs/strapi-plugin-certificates": "^1.0.0",
86
+ "@vicacha-devs/strapi-plugin-bookings": "^1.1.1",
87
+ "@vicacha-devs/strapi-plugin-calendar": "^1.1.3",
88
+ "@vicacha-devs/strapi-plugin-certificates": "^1.1.2",
89
89
  "react": "^18.0.0",
90
90
  "react-dom": "^18.0.0",
91
91
  "react-router-dom": "^6.0.0",