easy-forms-core 1.1.3 → 1.1.6

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.
package/dist/index.js CHANGED
@@ -265,6 +265,13 @@ function getBaseStyles(colors) {
265
265
  .easy-form-submit:active {
266
266
  transform: scale(0.98);
267
267
  }
268
+ .easy-form-submit-wrapper {
269
+ margin-top: 1rem;
270
+ margin-bottom: 0.5rem;
271
+ }
272
+ .easy-form-submit-wrapper .easy-form-submit {
273
+ min-width: 100px;
274
+ }
268
275
  input:not([type="checkbox"]):not([type="radio"]), textarea, select {
269
276
  width: 100%;
270
277
  padding: 0.5rem;
@@ -710,6 +717,28 @@ function getBaseStyles(colors) {
710
717
  transform: rotate(360deg);
711
718
  }
712
719
  }
720
+ /* Lock Overlay (bloqueo por intentos) */
721
+ .easy-form-lock-overlay {
722
+ position: absolute;
723
+ top: 0;
724
+ left: 0;
725
+ right: 0;
726
+ bottom: 0;
727
+ background: rgba(255, 255, 255, 0.9);
728
+ display: flex;
729
+ align-items: center;
730
+ justify-content: center;
731
+ z-index: 1001;
732
+ backdrop-filter: blur(4px);
733
+ border-radius: inherit;
734
+ text-align: center;
735
+ padding: 1.5rem;
736
+ }
737
+ .easy-form-lock-message {
738
+ font-size: 1rem;
739
+ color: var(--easy-form-text);
740
+ max-width: 280px;
741
+ }
713
742
  /* Disabled State */
714
743
  .easy-form-disabled,
715
744
  .easy-form-disabled * {
@@ -1635,6 +1664,191 @@ function getPredefinedMask(type) {
1635
1664
  return PREDEFINED_MASKS[type];
1636
1665
  }
1637
1666
 
1667
+ // src/utils/injection-validation.ts
1668
+ var INJECTION_VALIDATION_MESSAGE = "El valor contiene caracteres o patrones no permitidos";
1669
+ var INJECTION_PATTERNS = [
1670
+ // SQL Injection
1671
+ /\b(union|select|insert|update|delete|drop|exec|execute|declare)\s+(all\s+)?(select|from|into|table)/i,
1672
+ /\b(or|and)\s+['"]?\d+['"]?\s*=\s*['"]?\d+/i,
1673
+ /;\s*(drop|delete|truncate|alter)\s+/i,
1674
+ /--\s*$/,
1675
+ // SQL comment
1676
+ /\/\*[\s\S]*\*\//,
1677
+ // Block comment
1678
+ /'\s*or\s+'1'\s*=\s*'1/i,
1679
+ /"\s*or\s+"1"\s*=\s*"1/i,
1680
+ /\bexec\s*\(/i,
1681
+ /\bxp_\w+/i,
1682
+ // SQL Server extended procedures
1683
+ // XSS / Scripting
1684
+ /<script\b[\s\S]*?>[\s\S]*?<\/script>/i,
1685
+ /<script\b/i,
1686
+ /javascript\s*:/i,
1687
+ /vbscript\s*:/i,
1688
+ /on\w+\s*=\s*["'][^"']*["']/i,
1689
+ // onclick=, onerror=, onload=, etc.
1690
+ /on\w+\s*=\s*[^\s>]+/i,
1691
+ /<iframe\b/i,
1692
+ /<object\b/i,
1693
+ /<embed\b/i,
1694
+ /\beval\s*\(/i,
1695
+ /document\.(cookie|write|location)/i,
1696
+ /window\.(location|open|eval)/i,
1697
+ // Command injection (shell)
1698
+ /[;&|]\s*(ls|cat|rm|wget|curl|nc|bash|sh|python|perl)\s/i,
1699
+ /\$\s*\([^)]+\)/,
1700
+ // $(...)
1701
+ /`[^`]+`/,
1702
+ // Backtick command substitution
1703
+ /\|\s*\w+/,
1704
+ // Pipe to command (with word after)
1705
+ // NoSQL / Template injection
1706
+ /\$\s*where\b/i,
1707
+ /\$\s*gt\b|\$\s*ne\b|\$\s*regex\b/i,
1708
+ /\{\{[^}]*\}\}/,
1709
+ // Template literals
1710
+ /\$\{[^}]*\}/
1711
+ // JS template literals
1712
+ ];
1713
+ function containsInjection(value) {
1714
+ if (typeof value !== "string" || value.length === 0) {
1715
+ return false;
1716
+ }
1717
+ const normalized = value.trim();
1718
+ if (normalized.length === 0) return false;
1719
+ for (const pattern of INJECTION_PATTERNS) {
1720
+ if (pattern.test(value)) {
1721
+ return true;
1722
+ }
1723
+ }
1724
+ return false;
1725
+ }
1726
+ function isSafeFromInjection(value) {
1727
+ if (value === null || value === void 0) {
1728
+ return true;
1729
+ }
1730
+ if (typeof value === "string") {
1731
+ return !containsInjection(value);
1732
+ }
1733
+ if (Array.isArray(value)) {
1734
+ return value.every((item) => isSafeFromInjection(item));
1735
+ }
1736
+ if (typeof value === "object") {
1737
+ return Object.values(value).every(
1738
+ (v) => isSafeFromInjection(v)
1739
+ );
1740
+ }
1741
+ return true;
1742
+ }
1743
+
1744
+ // src/utils/attempts-lock.ts
1745
+ var AttemptsLock = class {
1746
+ constructor(options) {
1747
+ this.attempts = 0;
1748
+ this.lockedUntil = null;
1749
+ this.unlockCheckInterval = null;
1750
+ this.maxAttempts = Math.max(1, options.maxAttempts);
1751
+ this.blockDurationMs = (options.blockDurationMinutes ?? 5) * 60 * 1e3;
1752
+ this.storageKey = options.storageKey;
1753
+ this.onLocked = options.onLocked;
1754
+ this.onUnlocked = options.onUnlocked;
1755
+ if (this.storageKey && typeof sessionStorage !== "undefined") {
1756
+ this.loadFromStorage();
1757
+ }
1758
+ }
1759
+ loadFromStorage() {
1760
+ if (!this.storageKey || typeof sessionStorage === "undefined") return;
1761
+ try {
1762
+ const stored = sessionStorage.getItem(this.storageKey);
1763
+ if (stored) {
1764
+ const data = JSON.parse(stored);
1765
+ this.attempts = data.attempts ?? 0;
1766
+ this.lockedUntil = data.lockedUntil ?? null;
1767
+ this.checkExpiration();
1768
+ }
1769
+ } catch {
1770
+ }
1771
+ }
1772
+ saveToStorage() {
1773
+ if (!this.storageKey || typeof sessionStorage === "undefined") return;
1774
+ try {
1775
+ const data = {
1776
+ attempts: this.attempts,
1777
+ lockedUntil: this.lockedUntil ?? void 0
1778
+ };
1779
+ sessionStorage.setItem(this.storageKey, JSON.stringify(data));
1780
+ } catch {
1781
+ }
1782
+ }
1783
+ checkExpiration() {
1784
+ if (this.lockedUntil === null) return false;
1785
+ if (Date.now() >= this.lockedUntil) {
1786
+ this.reset();
1787
+ this.onUnlocked?.();
1788
+ return true;
1789
+ }
1790
+ return false;
1791
+ }
1792
+ startUnlockCheck() {
1793
+ if (this.unlockCheckInterval) return;
1794
+ this.unlockCheckInterval = setInterval(() => {
1795
+ if (this.checkExpiration()) {
1796
+ this.stopUnlockCheck();
1797
+ }
1798
+ }, 1e3);
1799
+ }
1800
+ stopUnlockCheck() {
1801
+ if (this.unlockCheckInterval) {
1802
+ clearInterval(this.unlockCheckInterval);
1803
+ this.unlockCheckInterval = null;
1804
+ }
1805
+ }
1806
+ /**
1807
+ * Incrementa el contador de intentos. Si alcanza maxAttempts, bloquea.
1808
+ */
1809
+ incrementAttempts() {
1810
+ this.attempts++;
1811
+ if (this.attempts >= this.maxAttempts) {
1812
+ this.lockedUntil = Date.now() + this.blockDurationMs;
1813
+ this.saveToStorage();
1814
+ this.onLocked?.(this.blockDurationMs);
1815
+ this.startUnlockCheck();
1816
+ } else {
1817
+ this.saveToStorage();
1818
+ }
1819
+ }
1820
+ /**
1821
+ * Retorna true si el lock está activo (aún dentro del período de bloqueo)
1822
+ */
1823
+ isLocked() {
1824
+ if (this.checkExpiration()) return false;
1825
+ return this.lockedUntil !== null && Date.now() < this.lockedUntil;
1826
+ }
1827
+ /**
1828
+ * Retorna los milisegundos restantes del bloqueo, o 0 si no está bloqueado
1829
+ */
1830
+ getRemainingBlockTimeMs() {
1831
+ if (this.checkExpiration()) return 0;
1832
+ if (this.lockedUntil === null) return 0;
1833
+ return Math.max(0, this.lockedUntil - Date.now());
1834
+ }
1835
+ /**
1836
+ * Resetea el contador de intentos y el bloqueo
1837
+ */
1838
+ reset() {
1839
+ this.attempts = 0;
1840
+ this.lockedUntil = null;
1841
+ this.stopUnlockCheck();
1842
+ this.saveToStorage();
1843
+ }
1844
+ /**
1845
+ * Retorna el número actual de intentos
1846
+ */
1847
+ getAttempts() {
1848
+ return this.attempts;
1849
+ }
1850
+ };
1851
+
1638
1852
  // src/utils/index.ts
1639
1853
  function attributeValue(value) {
1640
1854
  if (value === null || value === void 0) {
@@ -1728,6 +1942,8 @@ var ValidationEngine = class {
1728
1942
  return this.validatePattern(value, validation.value);
1729
1943
  case "custom":
1730
1944
  return await this.validateCustom(value, validation);
1945
+ case "noInjection":
1946
+ return this.validateNoInjection(value, validation);
1731
1947
  default:
1732
1948
  return { isValid: true };
1733
1949
  }
@@ -1838,6 +2054,19 @@ var ValidationEngine = class {
1838
2054
  message: isValid ? void 0 : "El formato no es v\xE1lido"
1839
2055
  };
1840
2056
  }
2057
+ /**
2058
+ * Valida que el valor no contenga patrones de inyección (SQL, XSS, etc.)
2059
+ */
2060
+ validateNoInjection(value, validation) {
2061
+ if (value === null || value === void 0 || value === "") {
2062
+ return { isValid: true };
2063
+ }
2064
+ const isValid = isSafeFromInjection(value);
2065
+ return {
2066
+ isValid,
2067
+ message: isValid ? void 0 : validation.message || INJECTION_VALIDATION_MESSAGE
2068
+ };
2069
+ }
1841
2070
  /**
1842
2071
  * Valida con función personalizada
1843
2072
  */
@@ -1876,6 +2105,8 @@ var ValidationEngine = class {
1876
2105
  return "El formato no es v\xE1lido";
1877
2106
  case "custom":
1878
2107
  return "Validaci\xF3n fallida";
2108
+ case "noInjection":
2109
+ return INJECTION_VALIDATION_MESSAGE;
1879
2110
  default:
1880
2111
  return "Campo inv\xE1lido";
1881
2112
  }
@@ -2354,6 +2585,11 @@ var StateManager = class {
2354
2585
  */
2355
2586
  getActiveValidations(field) {
2356
2587
  let validations = [...field.validations || []];
2588
+ const textFieldTypes = ["text", "email", "password", "textarea"];
2589
+ const skipInjection = field.skipInjectionValidation ?? field.props?.skipInjectionValidation;
2590
+ if (textFieldTypes.includes(field.type) && !skipInjection && !validations.some((v) => v.type === "noInjection")) {
2591
+ validations = [{ type: "noInjection" }, ...validations];
2592
+ }
2357
2593
  const isRequired = this.getFieldRequired(field.name);
2358
2594
  const hasRequiredValidation = validations.some((v) => v.type === "required");
2359
2595
  if (isRequired && !hasRequiredValidation) {
@@ -4534,12 +4770,27 @@ var EasyForm = class extends BrowserHTMLElement {
4534
4770
  super();
4535
4771
  this.customComponents = {};
4536
4772
  this.isRendering = false;
4773
+ this.attemptsLock = null;
4774
+ this.lockCountdownInterval = null;
4537
4775
  this.dependencyRenderTimeout = null;
4538
4776
  this.stateManager = new StateManager();
4539
4777
  this.shadow = this.attachShadow({ mode: "open" });
4540
4778
  }
4541
4779
  static get observedAttributes() {
4542
- return ["schema", "template", "template-extend", "theme", "colors", "initialData", "loading", "disabled"];
4780
+ return [
4781
+ "schema",
4782
+ "template",
4783
+ "template-extend",
4784
+ "theme",
4785
+ "colors",
4786
+ "initialData",
4787
+ "loading",
4788
+ "disabled",
4789
+ "max-attempts",
4790
+ "block-duration-minutes",
4791
+ "attempts-storage-key",
4792
+ "submit-button"
4793
+ ];
4543
4794
  }
4544
4795
  /**
4545
4796
  * Obtiene el schema
@@ -4613,10 +4864,91 @@ var EasyForm = class extends BrowserHTMLElement {
4613
4864
  this.removeAttribute("template-extend");
4614
4865
  }
4615
4866
  }
4867
+ /**
4868
+ * Máximo de intentos antes de bloquear (para AttemptsLock)
4869
+ */
4870
+ get maxAttempts() {
4871
+ const attr = this.getAttribute("max-attempts");
4872
+ if (!attr) return null;
4873
+ const n = parseInt(attr, 10);
4874
+ return isNaN(n) ? null : n;
4875
+ }
4876
+ set maxAttempts(value) {
4877
+ if (value != null && value >= 1) {
4878
+ this.setAttribute("max-attempts", String(value));
4879
+ } else {
4880
+ this.removeAttribute("max-attempts");
4881
+ }
4882
+ }
4883
+ /**
4884
+ * Duración del bloqueo en minutos (default: 5)
4885
+ */
4886
+ get blockDurationMinutes() {
4887
+ const attr = this.getAttribute("block-duration-minutes");
4888
+ if (!attr) return null;
4889
+ const n = parseInt(attr, 10);
4890
+ return isNaN(n) ? null : n;
4891
+ }
4892
+ set blockDurationMinutes(value) {
4893
+ if (value != null && value >= 1) {
4894
+ this.setAttribute("block-duration-minutes", String(value));
4895
+ } else {
4896
+ this.removeAttribute("block-duration-minutes");
4897
+ }
4898
+ }
4899
+ /**
4900
+ * Clave para persistir intentos en sessionStorage
4901
+ */
4902
+ get attemptsStorageKey() {
4903
+ return this.getAttribute("attempts-storage-key");
4904
+ }
4905
+ set attemptsStorageKey(value) {
4906
+ if (value) {
4907
+ this.setAttribute("attempts-storage-key", value);
4908
+ } else {
4909
+ this.removeAttribute("attempts-storage-key");
4910
+ }
4911
+ }
4912
+ /**
4913
+ * Configuración del botón de submit (desde atributo o schema)
4914
+ */
4915
+ get submitButton() {
4916
+ const attr = this.getAttribute("submit-button");
4917
+ if (attr) {
4918
+ try {
4919
+ return parseAttributeValue(attr);
4920
+ } catch {
4921
+ return null;
4922
+ }
4923
+ }
4924
+ return null;
4925
+ }
4926
+ set submitButton(value) {
4927
+ if (value && typeof value === "object") {
4928
+ this.setAttribute("submit-button", attributeValue(value));
4929
+ } else {
4930
+ this.removeAttribute("submit-button");
4931
+ }
4932
+ }
4933
+ /**
4934
+ * Obtiene la configuración efectiva del botón submit (atributo > schema > defaults)
4935
+ */
4936
+ getSubmitButtonConfig(schema) {
4937
+ const fromAttr = this.submitButton;
4938
+ const fromSchema = schema?.submitButton;
4939
+ const merged = { ...fromSchema, ...fromAttr };
4940
+ return {
4941
+ visible: merged.visible ?? true,
4942
+ text: merged.text ?? "Enviar",
4943
+ width: merged.width ?? "auto",
4944
+ align: merged.align ?? "left"
4945
+ };
4946
+ }
4616
4947
  /**
4617
4948
  * Se llama cuando el componente se conecta al DOM
4618
4949
  */
4619
4950
  connectedCallback() {
4951
+ this.setupAttemptsLock();
4620
4952
  this.setupStyles();
4621
4953
  this.render();
4622
4954
  }
@@ -4663,6 +4995,36 @@ var EasyForm = class extends BrowserHTMLElement {
4663
4995
  if (name === "disabled" && newValue !== oldValue) {
4664
4996
  this.render();
4665
4997
  }
4998
+ if ((name === "max-attempts" || name === "block-duration-minutes" || name === "attempts-storage-key") && newValue !== oldValue) {
4999
+ this.setupAttemptsLock();
5000
+ this.updateLockOverlay();
5001
+ }
5002
+ if (name === "submit-button" && newValue !== oldValue) {
5003
+ this.render();
5004
+ }
5005
+ }
5006
+ /**
5007
+ * Configura el AttemptsLock según los atributos actuales
5008
+ */
5009
+ setupAttemptsLock() {
5010
+ const maxAttempts = this.maxAttempts;
5011
+ if (maxAttempts == null || maxAttempts < 1) {
5012
+ this.attemptsLock = null;
5013
+ return;
5014
+ }
5015
+ this.attemptsLock = new AttemptsLock({
5016
+ maxAttempts,
5017
+ blockDurationMinutes: this.blockDurationMinutes ?? 5,
5018
+ storageKey: this.attemptsStorageKey ?? void 0,
5019
+ onLocked: () => {
5020
+ this.updateLockOverlay();
5021
+ },
5022
+ onUnlocked: () => {
5023
+ this.stopLockCountdown();
5024
+ this.updateLockOverlay();
5025
+ this.render();
5026
+ }
5027
+ });
4666
5028
  }
4667
5029
  /**
4668
5030
  * Maneja el cambio de schema
@@ -4753,17 +5115,25 @@ var EasyForm = class extends BrowserHTMLElement {
4753
5115
  newFormElement.classList.add("easy-form-disabled");
4754
5116
  }
4755
5117
  if (finalWizardState) {
4756
- this.renderWizard(newFormElement);
5118
+ this.renderWizard(newFormElement, schema);
4757
5119
  } else {
4758
5120
  this.renderFields(newFormElement, schema.fields || []);
4759
- const submitButton = document.createElement("button");
4760
- submitButton.type = "submit";
4761
- submitButton.textContent = "Enviar";
4762
- submitButton.className = "easy-form-submit";
4763
- if (this.disabled || this.loading) {
4764
- submitButton.disabled = true;
5121
+ const submitConfig = this.getSubmitButtonConfig(schema);
5122
+ if (submitConfig.visible) {
5123
+ const submitWrapper = document.createElement("div");
5124
+ submitWrapper.className = "easy-form-submit-wrapper";
5125
+ submitWrapper.style.textAlign = submitConfig.align;
5126
+ const submitButton = document.createElement("button");
5127
+ submitButton.type = "submit";
5128
+ submitButton.textContent = submitConfig.text;
5129
+ submitButton.className = "easy-form-submit";
5130
+ submitButton.style.width = submitConfig.width;
5131
+ if (this.disabled || this.loading) {
5132
+ submitButton.disabled = true;
5133
+ }
5134
+ submitWrapper.appendChild(submitButton);
5135
+ newFormElement.appendChild(submitWrapper);
4765
5136
  }
4766
- newFormElement.appendChild(submitButton);
4767
5137
  }
4768
5138
  const oldForm = this.shadow.querySelector("form");
4769
5139
  if (oldForm && oldForm.parentNode === this.shadow && oldForm !== newFormElement) {
@@ -4777,10 +5147,50 @@ var EasyForm = class extends BrowserHTMLElement {
4777
5147
  if (this.loading) {
4778
5148
  this.updateLoadingOverlay(newFormElement);
4779
5149
  }
5150
+ this.updateLockOverlay(newFormElement);
4780
5151
  } finally {
4781
5152
  this.isRendering = false;
4782
5153
  }
4783
5154
  }
5155
+ /**
5156
+ * Actualiza el overlay de bloqueo por intentos
5157
+ */
5158
+ updateLockOverlay(formElement) {
5159
+ const existingOverlay = this.shadow.querySelector(".easy-form-lock-overlay");
5160
+ if (existingOverlay) {
5161
+ existingOverlay.remove();
5162
+ }
5163
+ this.stopLockCountdown();
5164
+ if (!this.attemptsLock?.isLocked()) return;
5165
+ const form = formElement || this.shadow.querySelector("form");
5166
+ if (!form) return;
5167
+ const overlay = document.createElement("div");
5168
+ overlay.className = "easy-form-lock-overlay";
5169
+ const message = document.createElement("div");
5170
+ message.className = "easy-form-lock-message";
5171
+ message.setAttribute("role", "alert");
5172
+ const updateCountdown = () => {
5173
+ const remainingMs = this.attemptsLock.getRemainingBlockTimeMs();
5174
+ if (remainingMs <= 0) {
5175
+ this.stopLockCountdown();
5176
+ return;
5177
+ }
5178
+ const minutes = Math.floor(remainingMs / 6e4);
5179
+ const seconds = Math.floor(remainingMs % 6e4 / 1e3);
5180
+ const timeStr = minutes > 0 ? `${minutes} min ${seconds} s` : `${seconds} segundos`;
5181
+ message.textContent = `Demasiados intentos. Intenta de nuevo en ${timeStr}.`;
5182
+ };
5183
+ updateCountdown();
5184
+ overlay.appendChild(message);
5185
+ form.appendChild(overlay);
5186
+ this.lockCountdownInterval = setInterval(updateCountdown, 1e3);
5187
+ }
5188
+ stopLockCountdown() {
5189
+ if (this.lockCountdownInterval) {
5190
+ clearInterval(this.lockCountdownInterval);
5191
+ this.lockCountdownInterval = null;
5192
+ }
5193
+ }
4784
5194
  /**
4785
5195
  * Actualiza el overlay de loading sobre el formulario
4786
5196
  */
@@ -5085,14 +5495,13 @@ var EasyForm = class extends BrowserHTMLElement {
5085
5495
  /**
5086
5496
  * Renderiza wizard
5087
5497
  */
5088
- renderWizard(container) {
5498
+ renderWizard(container, schema) {
5089
5499
  const wizardState = this.stateManager.getWizardState();
5090
5500
  if (!wizardState) return;
5091
5501
  const wizardContainer = document.createElement("div");
5092
5502
  wizardContainer.className = "easy-form-wizard";
5093
5503
  const stepsIndicator = document.createElement("div");
5094
5504
  stepsIndicator.className = "easy-form-wizard-steps";
5095
- const schema = this.schema;
5096
5505
  if (schema?.steps) {
5097
5506
  for (let i = 0; i < schema.steps.length; i++) {
5098
5507
  const stepEl = document.createElement("div");
@@ -5177,11 +5586,13 @@ var EasyForm = class extends BrowserHTMLElement {
5177
5586
  }
5178
5587
  navContainer.appendChild(nextButton);
5179
5588
  }
5180
- if (wizardState.currentStep === wizardState.totalSteps - 1) {
5589
+ const submitConfig = this.getSubmitButtonConfig(schema);
5590
+ if (wizardState.currentStep === wizardState.totalSteps - 1 && submitConfig.visible) {
5181
5591
  const submitButton = document.createElement("button");
5182
5592
  submitButton.type = "button";
5183
- submitButton.textContent = "Enviar";
5593
+ submitButton.textContent = submitConfig.text;
5184
5594
  submitButton.className = "easy-form-wizard-next";
5595
+ submitButton.style.width = submitConfig.width;
5185
5596
  if (this.disabled || this.loading) {
5186
5597
  submitButton.disabled = true;
5187
5598
  } else {
@@ -5337,6 +5748,9 @@ var EasyForm = class extends BrowserHTMLElement {
5337
5748
  */
5338
5749
  async handleSubmit(event) {
5339
5750
  event.preventDefault();
5751
+ if (this.attemptsLock?.isLocked()) {
5752
+ return;
5753
+ }
5340
5754
  const errors = await this.stateManager.validateForm();
5341
5755
  const state = this.stateManager.getState();
5342
5756
  if (Object.keys(errors).length > 0) {
@@ -5402,6 +5816,48 @@ var EasyForm = class extends BrowserHTMLElement {
5402
5816
  this.stateManager.reset();
5403
5817
  this.render();
5404
5818
  }
5819
+ /**
5820
+ * Incrementa el contador de intentos (para bloqueo por intentos fallidos).
5821
+ * El consumidor debe llamar esto cuando la API/login falle.
5822
+ */
5823
+ incrementAttempts() {
5824
+ this.attemptsLock?.incrementAttempts();
5825
+ if (this.attemptsLock?.isLocked()) {
5826
+ this.updateLockOverlay();
5827
+ }
5828
+ }
5829
+ /**
5830
+ * Resetea el contador de intentos y desbloquea el formulario.
5831
+ */
5832
+ resetAttempts() {
5833
+ this.attemptsLock?.reset();
5834
+ this.stopLockCountdown();
5835
+ this.updateLockOverlay();
5836
+ this.render();
5837
+ }
5838
+ /**
5839
+ * Retorna true si el formulario está bloqueado por intentos.
5840
+ */
5841
+ isLocked() {
5842
+ return this.attemptsLock?.isLocked() ?? false;
5843
+ }
5844
+ /**
5845
+ * Retorna los milisegundos restantes del bloqueo, o 0 si no está bloqueado.
5846
+ */
5847
+ getRemainingBlockTimeMs() {
5848
+ return this.attemptsLock?.getRemainingBlockTimeMs() ?? 0;
5849
+ }
5850
+ /**
5851
+ * Dispara el submit del formulario programáticamente.
5852
+ * Útil cuando el botón submit está oculto (visible: false).
5853
+ */
5854
+ requestSubmit() {
5855
+ const form = this.shadow.querySelector("form");
5856
+ if (form && typeof form.requestSubmit === "function") {
5857
+ ;
5858
+ form.requestSubmit();
5859
+ }
5860
+ }
5405
5861
  /**
5406
5862
  * Limpia todos los valores del formulario
5407
5863
  */
@@ -5609,14 +6065,17 @@ if (typeof window !== "undefined" && typeof customElements !== "undefined" && !c
5609
6065
  customElements.define("easy-form", EasyForm);
5610
6066
  }
5611
6067
  export {
6068
+ AttemptsLock,
5612
6069
  ConditionEngine,
5613
6070
  EasyForm,
6071
+ INJECTION_VALIDATION_MESSAGE,
5614
6072
  MaskEngine,
5615
6073
  PREDEFINED_MASKS,
5616
6074
  SchemaParser,
5617
6075
  StateManager,
5618
6076
  ValidationEngine,
5619
6077
  attributeValue,
6078
+ containsInjection,
5620
6079
  createInput,
5621
6080
  extendTemplate,
5622
6081
  generateId,
@@ -5627,6 +6086,7 @@ export {
5627
6086
  getPredefinedMask,
5628
6087
  getTemplate,
5629
6088
  getThemeStyles,
6089
+ isSafeFromInjection,
5630
6090
  isValidEmail,
5631
6091
  parseAttributeValue,
5632
6092
  registerComponent,