@vicacha-devs/strapi-plugin-inbox 1.1.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.
@@ -5,7 +5,12 @@ declare const _default: {
5
5
  path: string;
6
6
  handler: string;
7
7
  config: {
8
- policies: never[];
8
+ policies: (string | {
9
+ name: string;
10
+ config: {
11
+ actions: string[];
12
+ };
13
+ })[];
9
14
  };
10
15
  }[];
11
16
  };
@@ -18,7 +18,12 @@ declare const _default: {
18
18
  path: string;
19
19
  handler: string;
20
20
  config: {
21
- policies: never[];
21
+ policies: (string | {
22
+ name: string;
23
+ config: {
24
+ actions: string[];
25
+ };
26
+ })[];
22
27
  };
23
28
  }[];
24
29
  };
@@ -1 +1,2 @@
1
- export * from '../../shared/index.js';
1
+ declare const shared: Record<string, unknown>;
2
+ export default shared;
@@ -104,15 +104,122 @@ const DEFAULT_INBOX_SETTINGS = {
104
104
  defaultNotifyEmails: [],
105
105
  outboundWebhookUrl: ""
106
106
  };
107
+ const SUPPORTED_INTL_CALENDARS = Object.freeze([
108
+ { id: "gregory", intlCalendar: "gregory", defaultLabel: "Gregorian" },
109
+ { id: "iso8601", intlCalendar: "iso8601", defaultLabel: "ISO 8601" },
110
+ {
111
+ id: "islamic-umalqura",
112
+ intlCalendar: "islamic-umalqura",
113
+ defaultLabel: "Hijri (Umm al-Qura)"
114
+ },
115
+ {
116
+ id: "islamic-civil",
117
+ intlCalendar: "islamic-civil",
118
+ defaultLabel: "Hijri (Civil)"
119
+ },
120
+ {
121
+ id: "islamic-tbla",
122
+ intlCalendar: "islamic-tbla",
123
+ defaultLabel: "Hijri (Tabular)"
124
+ },
125
+ { id: "hebrew", intlCalendar: "hebrew", defaultLabel: "Hebrew" },
126
+ {
127
+ id: "persian",
128
+ intlCalendar: "persian",
129
+ defaultLabel: "Persian (Solar Hijri)"
130
+ },
131
+ { id: "buddhist", intlCalendar: "buddhist", defaultLabel: "Buddhist" },
132
+ { id: "japanese", intlCalendar: "japanese", defaultLabel: "Japanese" },
133
+ { id: "chinese", intlCalendar: "chinese", defaultLabel: "Chinese Lunar" },
134
+ { id: "indian", intlCalendar: "indian", defaultLabel: "Indian National" },
135
+ { id: "coptic", intlCalendar: "coptic", defaultLabel: "Coptic" },
136
+ { id: "ethiopic", intlCalendar: "ethiopic", defaultLabel: "Ethiopic" }
137
+ ]);
138
+ Object.freeze([
139
+ "none",
140
+ ...SUPPORTED_INTL_CALENDARS.map((item) => item.id)
141
+ ]);
142
+ const BASE_BCP47_REGEX = /^[a-zA-Z]{2,3}(?:-[a-zA-Z0-9]{2,8})*$/;
143
+ function sanitizeCalendarBaseLocale(locale, fallbackLocale = "es") {
144
+ if (typeof locale !== "string" || !locale.trim()) {
145
+ return fallbackLocale;
146
+ }
147
+ const trimmed = locale.trim();
148
+ const uIdx = trimmed.toLowerCase().indexOf("-u-");
149
+ let stripped = uIdx >= 0 ? trimmed.slice(0, uIdx) : trimmed;
150
+ while (stripped.endsWith("-")) {
151
+ stripped = stripped.slice(0, -1);
152
+ }
153
+ if (!stripped || stripped.length > 35 || !BASE_BCP47_REGEX.test(stripped)) {
154
+ return fallbackLocale;
155
+ }
156
+ return stripped;
157
+ }
158
+ function formatSecondaryDate(isoDateString, calendar = "islamic-umalqura", locale = "es") {
159
+ if (!isoDateString || calendar === "none") {
160
+ return { calendar, formatted: "" };
161
+ }
162
+ const date = new Date(isoDateString);
163
+ if (Number.isNaN(date.getTime())) {
164
+ return { calendar, formatted: isoDateString };
165
+ }
166
+ try {
167
+ const baseLocale = sanitizeCalendarBaseLocale(locale);
168
+ const formatter = new Intl.DateTimeFormat(`${baseLocale}-u-ca-${calendar}`, {
169
+ year: "numeric",
170
+ month: "short",
171
+ day: "numeric"
172
+ });
173
+ const parts = formatter.formatToParts(date);
174
+ const year = Number(parts.find((p) => p.type === "year")?.value) || void 0;
175
+ const month = Number(parts.find((p) => p.type === "month")?.value) || void 0;
176
+ const day = Number(parts.find((p) => p.type === "day")?.value) || void 0;
177
+ return {
178
+ calendar,
179
+ formatted: formatter.format(date),
180
+ year,
181
+ month,
182
+ day
183
+ };
184
+ } catch {
185
+ return {
186
+ calendar,
187
+ formatted: date.toISOString().slice(0, 10)
188
+ };
189
+ }
190
+ }
107
191
  function isValidEmail(value) {
108
- if (/\s/.test(value))
192
+ if (typeof value !== "string" || value.length < 3 || value.length > 254) {
193
+ return false;
194
+ }
195
+ if (/[\u0000-\u0020\u007f]/.test(value) || /\s/.test(value))
109
196
  return false;
110
197
  const at = value.indexOf("@");
111
- if (at < 1 || at !== value.lastIndexOf("@"))
198
+ if (at < 1 || at > 64 || at !== value.lastIndexOf("@"))
112
199
  return false;
200
+ const local = value.slice(0, at);
201
+ if (local.startsWith(".") || local.endsWith(".") || local.includes("..")) {
202
+ return false;
203
+ }
113
204
  const domain = value.slice(at + 1);
114
- const dot = domain.indexOf(".");
115
- return dot > 0 && dot < domain.length - 1;
205
+ if (domain.startsWith(".") || domain.endsWith(".") || domain.includes("..")) {
206
+ return false;
207
+ }
208
+ const labels = domain.split(".");
209
+ if (labels.length < 2)
210
+ return false;
211
+ return labels.every((label) => label.length > 0 && label.length <= 63 && !label.startsWith("-") && !label.endsWith("-"));
212
+ }
213
+ const CSV_FORMULA_PREFIX_RE = /^[\s\u0085\u2028\u2029]*[=+\-@|]|^[\t\r\n]/;
214
+ function escapeCsvCell(value) {
215
+ if (value === null || value === void 0)
216
+ return "";
217
+ const raw = typeof value === "object" ? JSON.stringify(value) : String(value);
218
+ const sanitized = raw.length > 0 && CSV_FORMULA_PREFIX_RE.test(raw) ? `'${raw}` : raw;
219
+ if (sanitized.includes('"') || sanitized.includes(",") || sanitized.includes("\n") || sanitized.includes("\r")) {
220
+ return `"${sanitized.replace(/"/g, '""')}"`;
221
+ }
222
+ return sanitized;
116
223
  }
117
224
  function freezeOptions(items) {
118
225
  return Object.freeze(items.map((item) => Object.freeze({ ...item })));
@@ -144,6 +251,54 @@ freezeOptions([
144
251
  { value: "persian", label: "persian (Solar Hijri)" },
145
252
  { value: "none", label: "none (Gregorian Only)" }
146
253
  ]);
254
+ class SlidingWindowRateLimiter {
255
+ hits = /* @__PURE__ */ new Map();
256
+ maxTrackedKeys;
257
+ defaultWindowMs;
258
+ constructor(options) {
259
+ this.maxTrackedKeys = options?.maxTrackedKeys ?? 1e4;
260
+ this.defaultWindowMs = options?.defaultWindowMs ?? 60 * 60 * 1e3;
261
+ }
262
+ check(ip, bucketKey, limitPerWindow, nowMs = Date.now(), windowMs = this.defaultWindowMs) {
263
+ if (limitPerWindow <= 0) {
264
+ return { allowed: true, remaining: 999, retryAfterSeconds: 0 };
265
+ }
266
+ if (this.hits.size > this.maxTrackedKeys) {
267
+ const oldestKey = this.hits.keys().next().value;
268
+ if (oldestKey !== void 0) {
269
+ this.hits.delete(oldestKey);
270
+ }
271
+ }
272
+ const key = `${bucketKey}::${ip || "unknown"}`;
273
+ const cutoff = nowMs - windowMs;
274
+ const previous = (this.hits.get(key) || []).filter((ts) => ts > cutoff);
275
+ if (previous.length >= limitPerWindow) {
276
+ const oldest = previous[0] ?? nowMs;
277
+ const retryAfterSeconds = Math.max(1, Math.ceil((oldest + windowMs - nowMs) / 1e3));
278
+ this.hits.set(key, previous);
279
+ return {
280
+ allowed: false,
281
+ remaining: 0,
282
+ retryAfterSeconds
283
+ };
284
+ }
285
+ previous.push(nowMs);
286
+ this.hits.set(key, previous);
287
+ return {
288
+ allowed: true,
289
+ remaining: Math.max(0, limitPerWindow - previous.length),
290
+ retryAfterSeconds: 0
291
+ };
292
+ }
293
+ reset() {
294
+ this.hits.clear();
295
+ }
296
+ }
297
+ const FORBIDDEN_OBJECT_KEYS = /* @__PURE__ */ new Set([
298
+ "__proto__",
299
+ "constructor",
300
+ "prototype"
301
+ ]);
147
302
  const LOCALIZED_REQUIRED_FALLBACK = {
148
303
  es: (label) => `${label} es obligatorio`,
149
304
  en: (label) => `${label} is required`,
@@ -241,12 +396,23 @@ function checkNumericBoundRule(rule, limit, rawValue, fieldLabel, locale) {
241
396
  }
242
397
  return null;
243
398
  }
399
+ function hasNestedQuantifier(pattern) {
400
+ return /[+*?}]\s*\)\s*[+*?{]/.test(pattern.replaceAll(/\\./g, ""));
401
+ }
244
402
  function checkLengthOrNumberRule(rule, rawValue, strVal, fieldLabel, locale) {
245
- if (typeof rule.value !== "number") return null;
246
- return checkLengthRule(rule, rule.value, strVal, fieldLabel, locale) ?? checkNumericBoundRule(rule, rule.value, rawValue, fieldLabel, locale);
403
+ const numRuleVal = typeof rule.value === "number" ? rule.value : typeof rule.value === "string" && rule.value.trim() !== "" ? Number(rule.value) : Number.NaN;
404
+ if (!Number.isFinite(numRuleVal)) return null;
405
+ return checkLengthRule(rule, numRuleVal, strVal, fieldLabel, locale) ?? checkNumericBoundRule(rule, numRuleVal, rawValue, fieldLabel, locale);
247
406
  }
248
407
  function checkPatternOrEmailRule(rule, fieldType, strVal, fieldLabel, locale) {
249
408
  if (rule.type === "pattern" && typeof rule.value === "string") {
409
+ if (strVal.length > 4096 || rule.value.length > 512 || hasNestedQuantifier(rule.value)) {
410
+ return resolveRuleMessage(
411
+ rule,
412
+ locale,
413
+ `${fieldLabel} format is invalid`
414
+ );
415
+ }
250
416
  try {
251
417
  const regex = new RegExp(rule.value);
252
418
  if (!regex.test(strVal)) {
@@ -266,6 +432,13 @@ function checkPatternOrEmailRule(rule, fieldType, strVal, fieldLabel, locale) {
266
432
  `${fieldLabel} must be a valid email address`
267
433
  );
268
434
  }
435
+ if (rule.type === "url" && fieldType !== "url" && !isValidHttpUrl(strVal)) {
436
+ return resolveRuleMessage(
437
+ rule,
438
+ locale,
439
+ `${fieldLabel} must be a valid URL`
440
+ );
441
+ }
269
442
  return null;
270
443
  }
271
444
  function evaluateCustomRules(field, rawValue, strVal, fieldLabel, locale, rules) {
@@ -300,7 +473,7 @@ function buildRequiredError(rules, locale, fieldLabel) {
300
473
  return resolveRuleMessage(reqRule, locale, fallbackFn(fieldLabel));
301
474
  }
302
475
  function validateSingleField(field, data, locale) {
303
- const rawValue = data[field.name] ?? field.defaultValue;
476
+ const rawValue = Object.hasOwn(data, field.name) ? data[field.name] : field.defaultValue;
304
477
  const fieldLabel = resolveFieldLabel(field, locale);
305
478
  const rules = field.validation || [];
306
479
  if (field.required && isValueEmpty(rawValue)) {
@@ -333,7 +506,9 @@ function validateFormSubmission(fields, data, locale = "en") {
333
506
  const errors = {};
334
507
  const cleanedData = {};
335
508
  for (const field of fields) {
336
- if (DECORATIVE_FIELD_TYPES.includes(field.type)) continue;
509
+ if (DECORATIVE_FIELD_TYPES.includes(field.type) || FORBIDDEN_OBJECT_KEYS.has(field.name)) {
510
+ continue;
511
+ }
337
512
  const { fieldErrors, cleanedValue } = validateSingleField(
338
513
  field,
339
514
  data,
@@ -352,45 +527,10 @@ function validateFormSubmission(fields, data, locale = "en") {
352
527
  };
353
528
  }
354
529
  function isHoneypotTriggered(payload, honeypotField = "_website_url") {
355
- if (!honeypotField) return false;
530
+ if (!honeypotField || !Object.hasOwn(payload, honeypotField)) return false;
356
531
  const val = payload[honeypotField];
357
532
  return typeof val === "string" && val.trim().length > 0;
358
533
  }
359
- class SlidingWindowRateLimiter {
360
- hits = /* @__PURE__ */ new Map();
361
- check(ip, bucketKey, limitPerHour, nowMs = Date.now()) {
362
- if (limitPerHour <= 0) {
363
- return { allowed: true, remaining: 999, retryAfterSeconds: 0 };
364
- }
365
- const windowMs = 60 * 60 * 1e3;
366
- const key = `${bucketKey}::${ip || "unknown"}`;
367
- const cutoff = nowMs - windowMs;
368
- const previous = (this.hits.get(key) || []).filter((ts) => ts > cutoff);
369
- if (previous.length >= limitPerHour) {
370
- const oldest = previous[0] ?? nowMs;
371
- const retryAfterSeconds = Math.max(
372
- 1,
373
- Math.ceil((oldest + windowMs - nowMs) / 1e3)
374
- );
375
- this.hits.set(key, previous);
376
- return {
377
- allowed: false,
378
- remaining: 0,
379
- retryAfterSeconds
380
- };
381
- }
382
- previous.push(nowMs);
383
- this.hits.set(key, previous);
384
- return {
385
- allowed: true,
386
- remaining: Math.max(0, limitPerHour - previous.length),
387
- retryAfterSeconds: 0
388
- };
389
- }
390
- reset() {
391
- this.hits.clear();
392
- }
393
- }
394
534
  const defaultRateLimiter = new SlidingWindowRateLimiter();
395
535
  function resolveFetchRunner(fetchImpl) {
396
536
  return fetchImpl ?? globalThis.fetch;
@@ -749,16 +889,6 @@ function localizeFormForLocale(form, locale) {
749
889
  }
750
890
  };
751
891
  }
752
- const FORMULA_PREFIXES = ["=", "+", "-", "@", " ", "\r"];
753
- function escapeCsvCell(value) {
754
- if (value === null || value === void 0) return "";
755
- const raw = typeof value === "object" ? JSON.stringify(value) : String(value);
756
- const sanitized = raw.length > 0 && FORMULA_PREFIXES.includes(raw[0]) ? `'${raw}` : raw;
757
- if (sanitized.includes('"') || sanitized.includes(",") || sanitized.includes("\n") || sanitized.includes("\r")) {
758
- return `"${sanitized.replace(/"/g, '""')}"`;
759
- }
760
- return sanitized;
761
- }
762
892
  function collectDataFieldNames(submissions, fields) {
763
893
  const dataFieldNames = [];
764
894
  const seen = /* @__PURE__ */ new Set();
@@ -821,68 +951,8 @@ function submissionsToCsv(submissions, fields = []) {
821
951
  }
822
952
  return rows.join("\n");
823
953
  }
824
- const SUPPORTED_INTL_CALENDARS = [
825
- { id: "gregory", intlCalendar: "gregory", defaultLabel: "Gregorian" },
826
- { id: "iso8601", intlCalendar: "iso8601", defaultLabel: "ISO 8601" },
827
- {
828
- id: "islamic-umalqura",
829
- intlCalendar: "islamic-umalqura",
830
- defaultLabel: "Hijri (Umm al-Qura)"
831
- },
832
- {
833
- id: "islamic-civil",
834
- intlCalendar: "islamic-civil",
835
- defaultLabel: "Hijri (Civil)"
836
- },
837
- {
838
- id: "islamic-tbla",
839
- intlCalendar: "islamic-tbla",
840
- defaultLabel: "Hijri (Tabular)"
841
- },
842
- { id: "hebrew", intlCalendar: "hebrew", defaultLabel: "Hebrew" },
843
- {
844
- id: "persian",
845
- intlCalendar: "persian",
846
- defaultLabel: "Persian (Solar Hijri)"
847
- },
848
- { id: "buddhist", intlCalendar: "buddhist", defaultLabel: "Buddhist" },
849
- { id: "japanese", intlCalendar: "japanese", defaultLabel: "Japanese" },
850
- { id: "chinese", intlCalendar: "chinese", defaultLabel: "Chinese Lunar" },
851
- { id: "indian", intlCalendar: "indian", defaultLabel: "Indian National" },
852
- { id: "coptic", intlCalendar: "coptic", defaultLabel: "Coptic" },
853
- { id: "ethiopic", intlCalendar: "ethiopic", defaultLabel: "Ethiopic" }
854
- ];
855
954
  function convertIsoDateToCalendar(isoDateString, calendar = "islamic-umalqura", locale = "en") {
856
- if (!isoDateString) {
857
- return { calendar, formatted: "" };
858
- }
859
- const date = new Date(isoDateString);
860
- if (Number.isNaN(date.getTime())) {
861
- return { calendar, formatted: isoDateString };
862
- }
863
- try {
864
- const formatter = new Intl.DateTimeFormat(`${locale}-u-ca-${calendar}`, {
865
- year: "numeric",
866
- month: "short",
867
- day: "numeric"
868
- });
869
- const parts = formatter.formatToParts(date);
870
- const year = Number(parts.find((p) => p.type === "year")?.value) || void 0;
871
- const month = Number(parts.find((p) => p.type === "month")?.value) || void 0;
872
- const day = Number(parts.find((p) => p.type === "day")?.value) || void 0;
873
- return {
874
- calendar,
875
- formatted: formatter.format(date),
876
- year,
877
- month,
878
- day
879
- };
880
- } catch {
881
- return {
882
- calendar,
883
- formatted: date.toISOString().slice(0, 10)
884
- };
885
- }
955
+ return formatSecondaryDate(isoDateString, calendar, locale);
886
956
  }
887
957
  exports.ALERT_BANNER_UID = ALERT_BANNER_UID;
888
958
  exports.ALERT_SEVERITIES = ALERT_SEVERITIES;