secure-ui-components 0.1.0-beta.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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +310 -0
  3. package/dist/components/secure-datetime/secure-datetime.css +263 -0
  4. package/dist/components/secure-datetime/secure-datetime.d.ts +124 -0
  5. package/dist/components/secure-datetime/secure-datetime.d.ts.map +1 -0
  6. package/dist/components/secure-datetime/secure-datetime.js +610 -0
  7. package/dist/components/secure-datetime/secure-datetime.js.map +1 -0
  8. package/dist/components/secure-file-upload/secure-file-upload.css +334 -0
  9. package/dist/components/secure-file-upload/secure-file-upload.d.ts +150 -0
  10. package/dist/components/secure-file-upload/secure-file-upload.d.ts.map +1 -0
  11. package/dist/components/secure-file-upload/secure-file-upload.js +911 -0
  12. package/dist/components/secure-file-upload/secure-file-upload.js.map +1 -0
  13. package/dist/components/secure-form/secure-form.css +62 -0
  14. package/dist/components/secure-form/secure-form.d.ts +128 -0
  15. package/dist/components/secure-form/secure-form.d.ts.map +1 -0
  16. package/dist/components/secure-form/secure-form.js +697 -0
  17. package/dist/components/secure-form/secure-form.js.map +1 -0
  18. package/dist/components/secure-input/secure-input.css +168 -0
  19. package/dist/components/secure-input/secure-input.d.ts +114 -0
  20. package/dist/components/secure-input/secure-input.d.ts.map +1 -0
  21. package/dist/components/secure-input/secure-input.js +785 -0
  22. package/dist/components/secure-input/secure-input.js.map +1 -0
  23. package/dist/components/secure-select/secure-select.css +195 -0
  24. package/dist/components/secure-select/secure-select.d.ts +149 -0
  25. package/dist/components/secure-select/secure-select.d.ts.map +1 -0
  26. package/dist/components/secure-select/secure-select.js +634 -0
  27. package/dist/components/secure-select/secure-select.js.map +1 -0
  28. package/dist/components/secure-submit-button/secure-submit-button.css +135 -0
  29. package/dist/components/secure-submit-button/secure-submit-button.d.ts +61 -0
  30. package/dist/components/secure-submit-button/secure-submit-button.d.ts.map +1 -0
  31. package/dist/components/secure-submit-button/secure-submit-button.js +399 -0
  32. package/dist/components/secure-submit-button/secure-submit-button.js.map +1 -0
  33. package/dist/components/secure-table/secure-table.css +341 -0
  34. package/dist/components/secure-table/secure-table.d.ts +64 -0
  35. package/dist/components/secure-table/secure-table.d.ts.map +1 -0
  36. package/dist/components/secure-table/secure-table.js +567 -0
  37. package/dist/components/secure-table/secure-table.js.map +1 -0
  38. package/dist/components/secure-textarea/secure-textarea.css +153 -0
  39. package/dist/components/secure-textarea/secure-textarea.d.ts +111 -0
  40. package/dist/components/secure-textarea/secure-textarea.d.ts.map +1 -0
  41. package/dist/components/secure-textarea/secure-textarea.js +477 -0
  42. package/dist/components/secure-textarea/secure-textarea.js.map +1 -0
  43. package/dist/core/base-component.d.ts +134 -0
  44. package/dist/core/base-component.d.ts.map +1 -0
  45. package/dist/core/base-component.js +303 -0
  46. package/dist/core/base-component.js.map +1 -0
  47. package/dist/core/base.css +37 -0
  48. package/dist/core/security-config.d.ts +89 -0
  49. package/dist/core/security-config.d.ts.map +1 -0
  50. package/dist/core/security-config.js +273 -0
  51. package/dist/core/security-config.js.map +1 -0
  52. package/dist/core/types.d.ts +212 -0
  53. package/dist/core/types.d.ts.map +1 -0
  54. package/dist/core/types.js +7 -0
  55. package/dist/core/types.js.map +1 -0
  56. package/dist/index.d.ts +18 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +19 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/package.json +89 -0
  61. package/dist/styles/tokens.css +257 -0
  62. package/package.json +118 -0
@@ -0,0 +1,697 @@
1
+ /**
2
+ * @fileoverview Secure Form Component
3
+ *
4
+ * A security-first form component that implements CSRF protection, automatic
5
+ * field collection, validation, and comprehensive audit logging.
6
+ *
7
+ * Progressive Enhancement Strategy:
8
+ * 1. Without JavaScript: Falls back to native HTML5 form submission
9
+ * 2. With JavaScript: Enhances with CSRF tokens, validation, secure submission
10
+ *
11
+ * Usage:
12
+ * <secure-form
13
+ * security-tier="sensitive"
14
+ * action="/api/submit"
15
+ * method="POST"
16
+ * csrf-token="your-csrf-token"
17
+ * >
18
+ * <secure-input name="email" label="Email" required></secure-input>
19
+ * <button type="submit">Submit</button>
20
+ * </secure-form>
21
+ *
22
+ * Security Features:
23
+ * - CSRF token injection and validation
24
+ * - Automatic secure field collection
25
+ * - XSS prevention via sanitization
26
+ * - Rate limiting on submission
27
+ * - Comprehensive audit logging
28
+ * - Secure headers for form submission
29
+ * - Double-submit cookie pattern support
30
+ *
31
+ * @module secure-form
32
+ * @license MIT
33
+ */
34
+ var _a;
35
+ import { SecurityTier } from '../../core/security-config.js';
36
+ /**
37
+ * Secure Form Web Component
38
+ *
39
+ * Provides a security-hardened form with CSRF protection and validation.
40
+ * The component works as a standard HTML form without JavaScript and
41
+ * enhances with security features when JavaScript is available.
42
+ *
43
+ * IMPORTANT: This component does NOT use Shadow DOM. It creates a native
44
+ * <form> element in light DOM to ensure proper form submission and
45
+ * accessibility. It extends HTMLElement directly, not SecureBaseComponent.
46
+ *
47
+ * @extends HTMLElement
48
+ */
49
+ export class SecureForm extends HTMLElement {
50
+ /** @private Whether component styles have been added to the document */
51
+ static __stylesAdded = false;
52
+ /**
53
+ * Form element reference
54
+ * @private
55
+ */
56
+ #formElement = null;
57
+ /**
58
+ * CSRF token hidden input reference
59
+ * @private
60
+ */
61
+ #csrfInput = null;
62
+ /**
63
+ * Form status message element
64
+ * @private
65
+ */
66
+ #statusElement = null;
67
+ /**
68
+ * Whether form is currently submitting
69
+ * @private
70
+ */
71
+ #isSubmitting = false;
72
+ /**
73
+ * Unique ID for this form instance
74
+ * @private
75
+ */
76
+ #instanceId = `secure-form-${Math.random().toString(36).substr(2, 9)}`;
77
+ /**
78
+ * Security tier for this form
79
+ * @private
80
+ */
81
+ #securityTier = SecurityTier.PUBLIC;
82
+ /**
83
+ * Observed attributes for this component
84
+ *
85
+ * @static
86
+ */
87
+ static get observedAttributes() {
88
+ return [
89
+ 'security-tier',
90
+ 'action',
91
+ 'method',
92
+ 'enctype',
93
+ 'csrf-token',
94
+ 'csrf-header-name',
95
+ 'csrf-field-name',
96
+ 'novalidate'
97
+ ];
98
+ }
99
+ /**
100
+ * Constructor
101
+ */
102
+ constructor() {
103
+ super();
104
+ // No Shadow DOM - we work exclusively in light DOM for form compatibility
105
+ }
106
+ /**
107
+ * Called when element is connected to DOM
108
+ *
109
+ * Progressive Enhancement Strategy:
110
+ * - Create a native <form> in light DOM (not Shadow DOM)
111
+ * - Move all children into the form
112
+ * - Add CSRF token as hidden field
113
+ * - Attach event listeners for validation and optional enhancement
114
+ */
115
+ connectedCallback() {
116
+ // Only initialize once
117
+ if (this.#formElement) {
118
+ return;
119
+ }
120
+ // Read security tier from attribute before anything else.
121
+ // attributeChangedCallback fires before connectedCallback but early-returns
122
+ // when #formElement is null, so the tier needs to be read here.
123
+ const tierAttr = this.getAttribute('security-tier');
124
+ if (tierAttr) {
125
+ this.#securityTier = tierAttr;
126
+ }
127
+ // Progressive enhancement: check for server-rendered <form> in light DOM
128
+ const existingForm = this.querySelector('form');
129
+ if (existingForm) {
130
+ // Adopt the existing form element
131
+ this.#formElement = existingForm;
132
+ this.#formElement.id = this.#instanceId;
133
+ if (!this.#formElement.classList.contains('secure-form')) {
134
+ this.#formElement.classList.add('secure-form');
135
+ }
136
+ // Apply/override form attributes from the custom element
137
+ this.#applyFormAttributes();
138
+ // Check if CSRF field already exists in the server-rendered form
139
+ const csrfFieldName = this.getAttribute('csrf-field-name') || 'csrf_token';
140
+ const existingCsrf = existingForm.querySelector(`input[name="${csrfFieldName}"]`);
141
+ if (existingCsrf) {
142
+ this.#csrfInput = existingCsrf;
143
+ // Update token value from attribute if it differs
144
+ const csrfToken = this.getAttribute('csrf-token');
145
+ if (csrfToken && existingCsrf.value !== csrfToken) {
146
+ existingCsrf.value = csrfToken;
147
+ }
148
+ }
149
+ else {
150
+ this.#createCsrfField();
151
+ }
152
+ }
153
+ else {
154
+ // No server-rendered form: create one (original behavior)
155
+ this.#formElement = document.createElement('form');
156
+ this.#formElement.id = this.#instanceId;
157
+ this.#formElement.className = 'secure-form';
158
+ // Apply form attributes
159
+ this.#applyFormAttributes();
160
+ // Create CSRF token field
161
+ this.#createCsrfField();
162
+ // Move all existing children (inputs, buttons) into the form
163
+ while (this.firstChild) {
164
+ this.#formElement.appendChild(this.firstChild);
165
+ }
166
+ // Append the form to this element
167
+ this.appendChild(this.#formElement);
168
+ }
169
+ // Create status message area
170
+ this.#statusElement = document.createElement('div');
171
+ this.#statusElement.className = 'form-status form-status-hidden';
172
+ this.#statusElement.setAttribute('role', 'status');
173
+ this.#statusElement.setAttribute('aria-live', 'polite');
174
+ this.#formElement.insertBefore(this.#statusElement, this.#formElement.firstChild);
175
+ // Add inline styles (since we're not using Shadow DOM)
176
+ this.#addInlineStyles();
177
+ // Set up event listeners
178
+ this.#attachEventListeners();
179
+ this.audit('form_initialized', {
180
+ formId: this.#instanceId,
181
+ action: this.#formElement.action,
182
+ method: this.#formElement.method
183
+ });
184
+ }
185
+ /**
186
+ * Add component styles (CSP-compliant via adoptedStyleSheets on document)
187
+ *
188
+ * Uses constructable stylesheets instead of injecting <style> elements,
189
+ * which would be blocked by strict Content Security Policy.
190
+ *
191
+ * @private
192
+ */
193
+ #addInlineStyles() {
194
+ // Only inject once globally — <link> in document head, loads from 'self' (CSP-safe)
195
+ if (!_a.__stylesAdded) {
196
+ const link = document.createElement('link');
197
+ link.rel = 'stylesheet';
198
+ link.href = new URL('./secure-form.css', import.meta.url).href;
199
+ document.head.appendChild(link);
200
+ _a.__stylesAdded = true;
201
+ }
202
+ }
203
+ /**
204
+ * Apply attributes to the form element
205
+ *
206
+ * @private
207
+ */
208
+ #applyFormAttributes() {
209
+ const action = this.getAttribute('action');
210
+ if (action) {
211
+ this.#formElement.action = action;
212
+ }
213
+ const method = this.getAttribute('method') || 'POST';
214
+ this.#formElement.method = method.toUpperCase();
215
+ const enctype = this.getAttribute('enctype') || 'application/x-www-form-urlencoded';
216
+ this.#formElement.enctype = enctype;
217
+ // Disable browser validation - we handle it ourselves
218
+ const novalidate = this.hasAttribute('novalidate');
219
+ if (novalidate) {
220
+ this.#formElement.noValidate = true;
221
+ }
222
+ // Disable autocomplete for SENSITIVE and CRITICAL tiers
223
+ if (this.#securityTier === SecurityTier.SENSITIVE || this.#securityTier === SecurityTier.CRITICAL) {
224
+ this.#formElement.autocomplete = 'off';
225
+ }
226
+ }
227
+ /**
228
+ * Create CSRF token hidden field
229
+ *
230
+ * Security Note: CSRF tokens prevent Cross-Site Request Forgery attacks.
231
+ * The token should be unique per session and validated server-side.
232
+ *
233
+ * @private
234
+ */
235
+ #createCsrfField() {
236
+ const csrfToken = this.getAttribute('csrf-token');
237
+ if (csrfToken) {
238
+ this.#csrfInput = document.createElement('input');
239
+ this.#csrfInput.type = 'hidden';
240
+ // Use 'csrf_token' for backend compatibility (common convention)
241
+ // Backends can configure via csrf-field-name attribute if needed
242
+ const fieldName = this.getAttribute('csrf-field-name') || 'csrf_token';
243
+ this.#csrfInput.name = fieldName;
244
+ this.#csrfInput.value = csrfToken;
245
+ this.#formElement.appendChild(this.#csrfInput);
246
+ this.audit('csrf_token_injected', {
247
+ formId: this.#instanceId,
248
+ fieldName: fieldName
249
+ });
250
+ }
251
+ else if (this.securityTier === SecurityTier.SENSITIVE ||
252
+ this.securityTier === SecurityTier.CRITICAL) {
253
+ console.warn('CSRF token not provided for SENSITIVE/CRITICAL tier form');
254
+ }
255
+ }
256
+ /**
257
+ * Attach event listeners
258
+ *
259
+ * @private
260
+ */
261
+ #attachEventListeners() {
262
+ // Submit event - validate and enhance submission
263
+ this.#formElement.addEventListener('submit', (e) => {
264
+ void this.#handleSubmit(e);
265
+ });
266
+ // Listen for secure field events
267
+ this.addEventListener('secure-input', (e) => {
268
+ this.#handleFieldChange(e);
269
+ });
270
+ this.addEventListener('secure-textarea', (e) => {
271
+ this.#handleFieldChange(e);
272
+ });
273
+ this.addEventListener('secure-select', (e) => {
274
+ this.#handleFieldChange(e);
275
+ });
276
+ }
277
+ /**
278
+ * Handle field change events
279
+ *
280
+ * @private
281
+ */
282
+ #handleFieldChange(_event) {
283
+ // Clear form-level errors when user makes changes
284
+ this.#clearStatus();
285
+ }
286
+ /**
287
+ * Handle form submission
288
+ *
289
+ * Progressive Enhancement Strategy:
290
+ * - If 'enhance' attribute is NOT set: Allow native form submission (backend agnostic)
291
+ * - If 'enhance' attribute IS set: Intercept and use Fetch API with JSON
292
+ *
293
+ * Security Note: This is where we perform comprehensive validation,
294
+ * rate limiting, and secure data collection before submission.
295
+ *
296
+ * @private
297
+ */
298
+ async #handleSubmit(event) {
299
+ // Check if we should enhance the form submission with JavaScript
300
+ const shouldEnhance = this.hasAttribute('enhance');
301
+ // Prevent double submission
302
+ if (this.#isSubmitting) {
303
+ event.preventDefault();
304
+ return;
305
+ }
306
+ // Check rate limit
307
+ const rateLimitCheck = this.checkRateLimit();
308
+ if (!rateLimitCheck.allowed) {
309
+ event.preventDefault();
310
+ this.#showStatus(`Too many submission attempts. Please wait ${Math.ceil(rateLimitCheck.retryAfter / 1000)} seconds.`, 'error');
311
+ this.audit('form_rate_limited', {
312
+ formId: this.#instanceId,
313
+ retryAfter: rateLimitCheck.retryAfter
314
+ });
315
+ return;
316
+ }
317
+ // Discover and validate all secure fields
318
+ const validation = this.#validateAllFields();
319
+ if (!validation.valid) {
320
+ event.preventDefault();
321
+ console.log('[secure-form] Validation failed:', validation.errors);
322
+ this.#showStatus(validation.errors.join(', '), 'error');
323
+ this.audit('form_validation_failed', {
324
+ formId: this.#instanceId,
325
+ errors: validation.errors
326
+ });
327
+ return;
328
+ }
329
+ console.log('[secure-form] Validation passed, shouldEnhance:', shouldEnhance);
330
+ // If not enhancing, allow native form submission
331
+ if (!shouldEnhance) {
332
+ // CRITICAL: Sync secure-input values to hidden fields for native submission
333
+ // Secure-input components have their actual <input> in Shadow DOM,
334
+ // so we need to create hidden inputs for native form submission
335
+ this.#syncSecureInputsToForm();
336
+ // Let the browser handle the submission normally
337
+ console.log('[secure-form] Allowing native submission to:', this.#formElement.action);
338
+ this.audit('form_submitted_native', {
339
+ formId: this.#instanceId,
340
+ action: this.#formElement.action,
341
+ method: this.#formElement.method
342
+ });
343
+ return; // Allow default behavior
344
+ }
345
+ // Enhanced submission with JavaScript (Fetch API)
346
+ event.preventDefault();
347
+ // Mark as submitting
348
+ this.#isSubmitting = true;
349
+ this.#showStatus('Submitting...', 'info');
350
+ this.#disableForm();
351
+ // Collect form data securely
352
+ const formData = this.#collectFormData();
353
+ // Audit log submission
354
+ this.audit('form_submitted_enhanced', {
355
+ formId: this.#instanceId,
356
+ action: this.#formElement.action,
357
+ method: this.#formElement.method,
358
+ fieldCount: Object.keys(formData).length
359
+ });
360
+ // Dispatch pre-submit event for custom handling
361
+ const preSubmitEvent = new CustomEvent('secure-form-submit', {
362
+ detail: {
363
+ formData,
364
+ formElement: this.#formElement,
365
+ preventDefault: () => {
366
+ this.#isSubmitting = false;
367
+ this.#enableForm();
368
+ }
369
+ },
370
+ bubbles: true,
371
+ composed: true,
372
+ cancelable: true
373
+ });
374
+ const shouldContinue = this.dispatchEvent(preSubmitEvent);
375
+ if (!shouldContinue) {
376
+ // Custom handler prevented default submission
377
+ this.#isSubmitting = false;
378
+ this.#enableForm();
379
+ return;
380
+ }
381
+ // Perform secure submission via Fetch
382
+ try {
383
+ await this.#submitForm(formData);
384
+ }
385
+ catch (error) {
386
+ this.#showStatus('Submission failed. Please try again.', 'error');
387
+ this.audit('form_submission_error', {
388
+ formId: this.#instanceId,
389
+ error: error.message
390
+ });
391
+ }
392
+ finally {
393
+ this.#isSubmitting = false;
394
+ this.#enableForm();
395
+ }
396
+ }
397
+ /**
398
+ * Sync secure-input component values to hidden form inputs
399
+ *
400
+ * CRITICAL for native form submission: Secure-input components have their
401
+ * actual <input> elements in Shadow DOM, which can't participate in native
402
+ * form submission. We create/update hidden inputs in the form for each
403
+ * secure-input to enable backend-agnostic form submission.
404
+ *
405
+ * @private
406
+ */
407
+ #syncSecureInputsToForm() {
408
+ const secureInputs = this.#formElement.querySelectorAll('secure-input, secure-textarea, secure-select');
409
+ secureInputs.forEach((input) => {
410
+ const name = input.getAttribute('name');
411
+ if (!name)
412
+ return;
413
+ // CRITICAL: Disable the native fallback inputs inside the secure component
414
+ // so they don't participate in native form submission (they are empty because
415
+ // the user typed into the shadow DOM input). Without this, the server receives
416
+ // the empty native input value first, ignoring the synced hidden input.
417
+ const nativeFallbacks = input.querySelectorAll(`input[name="${name}"], textarea[name="${name}"], select[name="${name}"]`);
418
+ nativeFallbacks.forEach((fallback) => {
419
+ fallback.removeAttribute('name');
420
+ });
421
+ // Check if hidden input already exists
422
+ let hiddenInput = this.#formElement.querySelector(`input[type="hidden"][data-secure-input="${name}"]`);
423
+ if (!hiddenInput) {
424
+ // Create hidden input for this secure-input
425
+ hiddenInput = document.createElement('input');
426
+ hiddenInput.type = 'hidden';
427
+ hiddenInput.setAttribute('data-secure-input', name);
428
+ hiddenInput.name = name;
429
+ this.#formElement.appendChild(hiddenInput);
430
+ }
431
+ // Sync the value
432
+ hiddenInput.value = input.value || '';
433
+ });
434
+ }
435
+ /**
436
+ * Validate all secure fields in the form
437
+ *
438
+ * @private
439
+ */
440
+ #validateAllFields() {
441
+ const errors = [];
442
+ // Find all secure input components within the form
443
+ const inputs = this.#formElement.querySelectorAll('secure-input, secure-textarea, secure-select');
444
+ inputs.forEach((input) => {
445
+ if (typeof input.valid === 'boolean' && !input.valid) {
446
+ const label = input.getAttribute('label') || input.getAttribute('name') || 'Field';
447
+ errors.push(`${label} is invalid`);
448
+ }
449
+ });
450
+ return {
451
+ valid: errors.length === 0,
452
+ errors
453
+ };
454
+ }
455
+ /**
456
+ * Collect form data from secure fields
457
+ *
458
+ * Security Note: We collect data from secure components which have already
459
+ * sanitized their values. We also include the CSRF token.
460
+ *
461
+ * @private
462
+ */
463
+ #collectFormData() {
464
+ const formData = {};
465
+ // Collect from secure components within the form
466
+ const secureInputs = this.#formElement.querySelectorAll('secure-input, secure-textarea, secure-select');
467
+ secureInputs.forEach((input) => {
468
+ const typedInput = input;
469
+ if (typedInput.name) {
470
+ formData[typedInput.name] = typedInput.value;
471
+ }
472
+ });
473
+ // Collect from standard form inputs (for non-secure fields)
474
+ const standardInputs = this.#formElement.querySelectorAll('input:not([type="hidden"]), textarea:not(.textarea-field), select:not(.select-field)');
475
+ standardInputs.forEach((input) => {
476
+ const typedInput = input;
477
+ if (typedInput.name) {
478
+ formData[typedInput.name] = this.sanitizeValue(typedInput.value);
479
+ }
480
+ });
481
+ // Include CSRF token
482
+ if (this.#csrfInput) {
483
+ formData[this.#csrfInput.name] = this.#csrfInput.value;
484
+ }
485
+ return formData;
486
+ }
487
+ /**
488
+ * Submit form data securely
489
+ *
490
+ * Security Note: We use fetch API with secure headers and proper CSRF handling.
491
+ * In production, ensure the server validates the CSRF token.
492
+ *
493
+ * @private
494
+ */
495
+ async #submitForm(formData) {
496
+ const action = this.#formElement.action;
497
+ const method = this.#formElement.method;
498
+ // Prepare headers
499
+ const headers = {
500
+ 'Content-Type': 'application/json'
501
+ };
502
+ // Add CSRF token to header if specified
503
+ const csrfHeaderName = this.getAttribute('csrf-header-name');
504
+ if (csrfHeaderName && this.#csrfInput) {
505
+ headers[csrfHeaderName] = this.#csrfInput.value;
506
+ }
507
+ // Perform fetch
508
+ const response = await fetch(action, {
509
+ method: method,
510
+ headers: headers,
511
+ body: JSON.stringify(formData),
512
+ credentials: 'same-origin', // Include cookies for CSRF validation
513
+ mode: 'cors',
514
+ cache: 'no-cache',
515
+ redirect: 'follow'
516
+ });
517
+ if (!response.ok) {
518
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
519
+ }
520
+ // Success
521
+ this.#showStatus('Form submitted successfully!', 'success');
522
+ // Dispatch success event
523
+ this.dispatchEvent(new CustomEvent('secure-form-success', {
524
+ detail: {
525
+ formData,
526
+ response
527
+ },
528
+ bubbles: true,
529
+ composed: true
530
+ }));
531
+ return response;
532
+ }
533
+ /**
534
+ * Disable form during submission
535
+ *
536
+ * @private
537
+ */
538
+ #disableForm() {
539
+ // Disable all form controls
540
+ const controls = this.#formElement.querySelectorAll('input, textarea, select, button');
541
+ controls.forEach((control) => {
542
+ control.disabled = true;
543
+ });
544
+ // Also disable secure components
545
+ const secureFields = this.querySelectorAll('secure-input, secure-textarea, secure-select');
546
+ secureFields.forEach((field) => {
547
+ field.setAttribute('disabled', '');
548
+ });
549
+ }
550
+ /**
551
+ * Enable form after submission
552
+ *
553
+ * @private
554
+ */
555
+ #enableForm() {
556
+ // Enable all form controls
557
+ const controls = this.#formElement.querySelectorAll('input, textarea, select, button');
558
+ controls.forEach((control) => {
559
+ control.disabled = false;
560
+ });
561
+ // Also enable secure components
562
+ const secureFields = this.querySelectorAll('secure-input, secure-textarea, secure-select');
563
+ secureFields.forEach((field) => {
564
+ field.removeAttribute('disabled');
565
+ });
566
+ }
567
+ /**
568
+ * Show status message
569
+ *
570
+ * @private
571
+ */
572
+ #showStatus(message, type = 'info') {
573
+ this.#statusElement.textContent = message;
574
+ this.#statusElement.className = `form-status form-status-${type}`;
575
+ }
576
+ /**
577
+ * Clear status message
578
+ *
579
+ * @private
580
+ */
581
+ #clearStatus() {
582
+ this.#statusElement.textContent = '';
583
+ this.#statusElement.className = 'form-status form-status-hidden';
584
+ }
585
+ /**
586
+ * Get form data
587
+ *
588
+ * @public
589
+ */
590
+ getData() {
591
+ return this.#collectFormData();
592
+ }
593
+ /**
594
+ * Reset the form
595
+ *
596
+ * @public
597
+ */
598
+ reset() {
599
+ if (this.#formElement) {
600
+ this.#formElement.reset();
601
+ this.#clearStatus();
602
+ this.audit('form_reset', {
603
+ formId: this.#instanceId
604
+ });
605
+ }
606
+ }
607
+ /**
608
+ * Programmatically submit the form
609
+ *
610
+ * @public
611
+ */
612
+ submit() {
613
+ if (this.#formElement) {
614
+ // Trigger submit event which will run our validation
615
+ this.#formElement.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
616
+ }
617
+ }
618
+ /**
619
+ * Check if form is valid
620
+ *
621
+ * @public
622
+ */
623
+ get valid() {
624
+ const validation = this.#validateAllFields();
625
+ return validation.valid;
626
+ }
627
+ /**
628
+ * Cleanup on disconnect
629
+ */
630
+ disconnectedCallback() {
631
+ // Clear any sensitive form data
632
+ if (this.#formElement) {
633
+ this.#formElement.reset();
634
+ }
635
+ }
636
+ /**
637
+ * Handle attribute changes
638
+ */
639
+ attributeChangedCallback(name, _oldValue, newValue) {
640
+ if (!this.#formElement)
641
+ return;
642
+ switch (name) {
643
+ case 'security-tier':
644
+ this.#securityTier = (newValue || SecurityTier.PUBLIC);
645
+ break;
646
+ case 'action':
647
+ this.#formElement.action = newValue;
648
+ break;
649
+ case 'method':
650
+ this.#formElement.method = newValue;
651
+ break;
652
+ case 'csrf-token':
653
+ if (this.#csrfInput) {
654
+ this.#csrfInput.value = newValue;
655
+ }
656
+ break;
657
+ }
658
+ }
659
+ /**
660
+ * Get security tier
661
+ */
662
+ get securityTier() {
663
+ return this.#securityTier;
664
+ }
665
+ /**
666
+ * Sanitize a value to prevent XSS
667
+ *
668
+ * Uses the same div.textContent round-trip as SecureBaseComponent to correctly
669
+ * handle all injection vectors (attribute injection, entity encoding, etc).
670
+ */
671
+ sanitizeValue(value) {
672
+ if (typeof value !== 'string')
673
+ return '';
674
+ const div = document.createElement('div');
675
+ div.textContent = value;
676
+ return div.innerHTML;
677
+ }
678
+ /**
679
+ * Audit log helper
680
+ */
681
+ audit(action, data) {
682
+ if (console.debug) {
683
+ console.debug(`[secure-form] ${action}`, data);
684
+ }
685
+ }
686
+ /**
687
+ * Check rate limit (stub - implement proper rate limiting in production)
688
+ */
689
+ checkRateLimit() {
690
+ return { allowed: true, retryAfter: 0 };
691
+ }
692
+ }
693
+ _a = SecureForm;
694
+ // Define the custom element
695
+ customElements.define('secure-form', SecureForm);
696
+ export default SecureForm;
697
+ //# sourceMappingURL=secure-form.js.map