secure-ui-components 0.2.3 → 0.2.5
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/components/secure-card/secure-card.js +1 -766
- package/dist/components/secure-datetime/secure-datetime.js +1 -570
- package/dist/components/secure-file-upload/secure-file-upload.js +1 -868
- package/dist/components/secure-form/secure-form.js +1 -797
- package/dist/components/secure-input/secure-input.js +1 -867
- package/dist/components/secure-password-confirm/secure-password-confirm.js +1 -329
- package/dist/components/secure-select/secure-select.js +1 -589
- package/dist/components/secure-submit-button/secure-submit-button.js +1 -378
- package/dist/components/secure-table/secure-table.js +33 -528
- package/dist/components/secure-telemetry-provider/secure-telemetry-provider.js +1 -201
- package/dist/components/secure-textarea/secure-textarea.js +1 -491
- package/dist/core/base-component.js +1 -500
- package/dist/core/security-config.js +1 -242
- package/dist/core/types.js +0 -2
- package/dist/index.js +1 -17
- package/dist/package.json +4 -2
- package/package.json +4 -2
|
@@ -30,800 +30,4 @@
|
|
|
30
30
|
*
|
|
31
31
|
* @module secure-form
|
|
32
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
|
-
* Timestamp when the form was connected to the DOM.
|
|
74
|
-
* Used to compute session duration for telemetry.
|
|
75
|
-
* @private
|
|
76
|
-
*/
|
|
77
|
-
#sessionStart = Date.now();
|
|
78
|
-
/**
|
|
79
|
-
* Unique ID for this form instance
|
|
80
|
-
* @private
|
|
81
|
-
*/
|
|
82
|
-
#instanceId = `secure-form-${Math.random().toString(36).substring(2, 11)}`;
|
|
83
|
-
/**
|
|
84
|
-
* Security tier for this form
|
|
85
|
-
* @private
|
|
86
|
-
*/
|
|
87
|
-
#securityTier = SecurityTier.PUBLIC;
|
|
88
|
-
/**
|
|
89
|
-
* Observed attributes for this component
|
|
90
|
-
*
|
|
91
|
-
* @static
|
|
92
|
-
*/
|
|
93
|
-
static get observedAttributes() {
|
|
94
|
-
return [
|
|
95
|
-
'security-tier',
|
|
96
|
-
'action',
|
|
97
|
-
'method',
|
|
98
|
-
'enctype',
|
|
99
|
-
'csrf-token',
|
|
100
|
-
'csrf-header-name',
|
|
101
|
-
'csrf-field-name',
|
|
102
|
-
'novalidate'
|
|
103
|
-
];
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Constructor
|
|
107
|
-
*/
|
|
108
|
-
constructor() {
|
|
109
|
-
super();
|
|
110
|
-
// No Shadow DOM - we work exclusively in light DOM for form compatibility
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Called when element is connected to DOM
|
|
114
|
-
*
|
|
115
|
-
* Progressive Enhancement Strategy:
|
|
116
|
-
* - Create a native <form> in light DOM (not Shadow DOM)
|
|
117
|
-
* - Move all children into the form
|
|
118
|
-
* - Add CSRF token as hidden field
|
|
119
|
-
* - Attach event listeners for validation and optional enhancement
|
|
120
|
-
*/
|
|
121
|
-
connectedCallback() {
|
|
122
|
-
// Only initialize once
|
|
123
|
-
if (this.#formElement) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
this.#sessionStart = Date.now();
|
|
127
|
-
// Read security tier from attribute before anything else.
|
|
128
|
-
// attributeChangedCallback fires before connectedCallback but early-returns
|
|
129
|
-
// when #formElement is null, so the tier needs to be read here.
|
|
130
|
-
const tierAttr = this.getAttribute('security-tier');
|
|
131
|
-
if (tierAttr) {
|
|
132
|
-
this.#securityTier = tierAttr;
|
|
133
|
-
}
|
|
134
|
-
// Progressive enhancement: check for server-rendered <form> in light DOM
|
|
135
|
-
const existingForm = this.querySelector('form');
|
|
136
|
-
if (existingForm) {
|
|
137
|
-
// Adopt the existing form element
|
|
138
|
-
this.#formElement = existingForm;
|
|
139
|
-
this.#formElement.id = this.#instanceId;
|
|
140
|
-
if (!this.#formElement.classList.contains('secure-form')) {
|
|
141
|
-
this.#formElement.classList.add('secure-form');
|
|
142
|
-
}
|
|
143
|
-
// Apply/override form attributes from the custom element
|
|
144
|
-
this.#applyFormAttributes();
|
|
145
|
-
// Check if CSRF field already exists in the server-rendered form
|
|
146
|
-
const csrfFieldName = this.getAttribute('csrf-field-name') || 'csrf_token';
|
|
147
|
-
const existingCsrf = existingForm.querySelector(`input[name="${csrfFieldName}"]`);
|
|
148
|
-
if (existingCsrf) {
|
|
149
|
-
this.#csrfInput = existingCsrf;
|
|
150
|
-
// Update token value from attribute if it differs
|
|
151
|
-
const csrfToken = this.getAttribute('csrf-token');
|
|
152
|
-
if (csrfToken && existingCsrf.value !== csrfToken) {
|
|
153
|
-
existingCsrf.value = csrfToken;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
this.#createCsrfField();
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
// No server-rendered form: create one (original behavior)
|
|
162
|
-
this.#formElement = document.createElement('form');
|
|
163
|
-
this.#formElement.id = this.#instanceId;
|
|
164
|
-
this.#formElement.className = 'secure-form';
|
|
165
|
-
// Apply form attributes
|
|
166
|
-
this.#applyFormAttributes();
|
|
167
|
-
// Create CSRF token field
|
|
168
|
-
this.#createCsrfField();
|
|
169
|
-
// Move all existing children (inputs, buttons) into the form
|
|
170
|
-
while (this.firstChild) {
|
|
171
|
-
this.#formElement.appendChild(this.firstChild);
|
|
172
|
-
}
|
|
173
|
-
// Append the form to this element
|
|
174
|
-
this.appendChild(this.#formElement);
|
|
175
|
-
}
|
|
176
|
-
// Create status message area
|
|
177
|
-
this.#statusElement = document.createElement('div');
|
|
178
|
-
this.#statusElement.className = 'form-status form-status-hidden';
|
|
179
|
-
this.#statusElement.setAttribute('role', 'status');
|
|
180
|
-
this.#statusElement.setAttribute('aria-live', 'polite');
|
|
181
|
-
this.#formElement.insertBefore(this.#statusElement, this.#formElement.firstChild);
|
|
182
|
-
// Add inline styles (since we're not using Shadow DOM)
|
|
183
|
-
this.#addInlineStyles();
|
|
184
|
-
// Set up event listeners
|
|
185
|
-
this.#attachEventListeners();
|
|
186
|
-
this.audit('form_initialized', {
|
|
187
|
-
formId: this.#instanceId,
|
|
188
|
-
action: this.#formElement.action,
|
|
189
|
-
method: this.#formElement.method
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Add component styles (CSP-compliant via adoptedStyleSheets on document)
|
|
194
|
-
*
|
|
195
|
-
* Uses constructable stylesheets instead of injecting <style> elements,
|
|
196
|
-
* which would be blocked by strict Content Security Policy.
|
|
197
|
-
*
|
|
198
|
-
* @private
|
|
199
|
-
*/
|
|
200
|
-
#addInlineStyles() {
|
|
201
|
-
// Only inject once globally — <link> in document head, loads from 'self' (CSP-safe)
|
|
202
|
-
if (!_a.__stylesAdded) {
|
|
203
|
-
const link = document.createElement('link');
|
|
204
|
-
link.rel = 'stylesheet';
|
|
205
|
-
link.href = new URL('./secure-form.css', import.meta.url).href;
|
|
206
|
-
document.head.appendChild(link);
|
|
207
|
-
_a.__stylesAdded = true;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Apply attributes to the form element
|
|
212
|
-
*
|
|
213
|
-
* @private
|
|
214
|
-
*/
|
|
215
|
-
#applyFormAttributes() {
|
|
216
|
-
const action = this.getAttribute('action');
|
|
217
|
-
if (action) {
|
|
218
|
-
this.#formElement.action = action;
|
|
219
|
-
}
|
|
220
|
-
const method = this.getAttribute('method') || 'POST';
|
|
221
|
-
this.#formElement.method = method.toUpperCase();
|
|
222
|
-
const enctype = this.getAttribute('enctype') || 'application/x-www-form-urlencoded';
|
|
223
|
-
this.#formElement.enctype = enctype;
|
|
224
|
-
// Disable browser validation - we handle it ourselves
|
|
225
|
-
const novalidate = this.hasAttribute('novalidate');
|
|
226
|
-
if (novalidate) {
|
|
227
|
-
this.#formElement.noValidate = true;
|
|
228
|
-
}
|
|
229
|
-
// Disable autocomplete for SENSITIVE and CRITICAL tiers
|
|
230
|
-
if (this.#securityTier === SecurityTier.SENSITIVE || this.#securityTier === SecurityTier.CRITICAL) {
|
|
231
|
-
this.#formElement.autocomplete = 'off';
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Create CSRF token hidden field
|
|
236
|
-
*
|
|
237
|
-
* Security Note: CSRF tokens prevent Cross-Site Request Forgery attacks.
|
|
238
|
-
* The token should be unique per session and validated server-side.
|
|
239
|
-
*
|
|
240
|
-
* @private
|
|
241
|
-
*/
|
|
242
|
-
#createCsrfField() {
|
|
243
|
-
const csrfToken = this.getAttribute('csrf-token');
|
|
244
|
-
if (csrfToken) {
|
|
245
|
-
this.#csrfInput = document.createElement('input');
|
|
246
|
-
this.#csrfInput.type = 'hidden';
|
|
247
|
-
// Use 'csrf_token' for backend compatibility (common convention)
|
|
248
|
-
// Backends can configure via csrf-field-name attribute if needed
|
|
249
|
-
const fieldName = this.getAttribute('csrf-field-name') || 'csrf_token';
|
|
250
|
-
this.#csrfInput.name = fieldName;
|
|
251
|
-
this.#csrfInput.value = csrfToken;
|
|
252
|
-
this.#formElement.appendChild(this.#csrfInput);
|
|
253
|
-
this.audit('csrf_token_injected', {
|
|
254
|
-
formId: this.#instanceId,
|
|
255
|
-
fieldName: fieldName
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
else if (this.securityTier === SecurityTier.SENSITIVE ||
|
|
259
|
-
this.securityTier === SecurityTier.CRITICAL) {
|
|
260
|
-
console.warn('CSRF token not provided for SENSITIVE/CRITICAL tier form');
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Attach event listeners
|
|
265
|
-
*
|
|
266
|
-
* @private
|
|
267
|
-
*/
|
|
268
|
-
#attachEventListeners() {
|
|
269
|
-
// Submit event - validate and enhance submission
|
|
270
|
-
this.#formElement.addEventListener('submit', (e) => {
|
|
271
|
-
void this.#handleSubmit(e);
|
|
272
|
-
});
|
|
273
|
-
// Listen for secure field events
|
|
274
|
-
this.addEventListener('secure-input', (e) => {
|
|
275
|
-
this.#handleFieldChange(e);
|
|
276
|
-
});
|
|
277
|
-
this.addEventListener('secure-textarea', (e) => {
|
|
278
|
-
this.#handleFieldChange(e);
|
|
279
|
-
});
|
|
280
|
-
this.addEventListener('secure-select', (e) => {
|
|
281
|
-
this.#handleFieldChange(e);
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* Handle field change events
|
|
286
|
-
*
|
|
287
|
-
* @private
|
|
288
|
-
*/
|
|
289
|
-
#handleFieldChange(_event) {
|
|
290
|
-
// Clear form-level errors when user makes changes
|
|
291
|
-
this.#clearStatus();
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Handle form submission
|
|
295
|
-
*
|
|
296
|
-
* Progressive Enhancement Strategy:
|
|
297
|
-
* - If 'enhance' attribute is NOT set: Allow native form submission (backend agnostic)
|
|
298
|
-
* - If 'enhance' attribute IS set: Intercept and use Fetch API with JSON
|
|
299
|
-
*
|
|
300
|
-
* Security Note: This is where we perform comprehensive validation,
|
|
301
|
-
* rate limiting, and secure data collection before submission.
|
|
302
|
-
*
|
|
303
|
-
* @private
|
|
304
|
-
*/
|
|
305
|
-
async #handleSubmit(event) {
|
|
306
|
-
// Check if we should enhance the form submission with JavaScript
|
|
307
|
-
const shouldEnhance = this.hasAttribute('enhance');
|
|
308
|
-
// Prevent double submission
|
|
309
|
-
if (this.#isSubmitting) {
|
|
310
|
-
event.preventDefault();
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
// Detect absent CSRF token on sensitive/critical tiers at submission time
|
|
314
|
-
if ((this.#securityTier === SecurityTier.SENSITIVE || this.#securityTier === SecurityTier.CRITICAL) &&
|
|
315
|
-
!this.#csrfInput?.value) {
|
|
316
|
-
this.dispatchEvent(new CustomEvent('secure-threat-detected', {
|
|
317
|
-
detail: {
|
|
318
|
-
fieldName: this.#instanceId,
|
|
319
|
-
threatType: 'csrf-token-absent',
|
|
320
|
-
patternId: 'csrf-token-absent',
|
|
321
|
-
tier: this.#securityTier,
|
|
322
|
-
timestamp: Date.now(),
|
|
323
|
-
},
|
|
324
|
-
bubbles: true,
|
|
325
|
-
composed: true,
|
|
326
|
-
}));
|
|
327
|
-
}
|
|
328
|
-
// Check rate limit
|
|
329
|
-
const rateLimitCheck = this.checkRateLimit();
|
|
330
|
-
if (!rateLimitCheck.allowed) {
|
|
331
|
-
event.preventDefault();
|
|
332
|
-
this.#showStatus(`Too many submission attempts. Please wait ${Math.ceil(rateLimitCheck.retryAfter / 1000)} seconds.`, 'error');
|
|
333
|
-
this.audit('form_rate_limited', {
|
|
334
|
-
formId: this.#instanceId,
|
|
335
|
-
retryAfter: rateLimitCheck.retryAfter
|
|
336
|
-
});
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
// Discover and validate all secure fields
|
|
340
|
-
const validation = this.#validateAllFields();
|
|
341
|
-
if (!validation.valid) {
|
|
342
|
-
event.preventDefault();
|
|
343
|
-
this.#showStatus(validation.errors.join(', '), 'error');
|
|
344
|
-
this.audit('form_validation_failed', {
|
|
345
|
-
formId: this.#instanceId,
|
|
346
|
-
errors: validation.errors
|
|
347
|
-
});
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
// If not enhancing, allow native form submission
|
|
351
|
-
if (!shouldEnhance) {
|
|
352
|
-
// CRITICAL: Sync secure-input values to hidden fields for native submission
|
|
353
|
-
// Secure-input components have their actual <input> in Shadow DOM,
|
|
354
|
-
// so we need to create hidden inputs for native form submission
|
|
355
|
-
this.#syncSecureInputsToForm();
|
|
356
|
-
// Let the browser handle the submission normally
|
|
357
|
-
this.audit('form_submitted_native', {
|
|
358
|
-
formId: this.#instanceId,
|
|
359
|
-
action: this.#formElement.action,
|
|
360
|
-
method: this.#formElement.method
|
|
361
|
-
});
|
|
362
|
-
return; // Allow default behavior
|
|
363
|
-
}
|
|
364
|
-
// Enhanced submission with JavaScript (Fetch API)
|
|
365
|
-
event.preventDefault();
|
|
366
|
-
// Mark as submitting
|
|
367
|
-
this.#isSubmitting = true;
|
|
368
|
-
this.#showStatus('Submitting...', 'info');
|
|
369
|
-
this.#disableForm();
|
|
370
|
-
// Collect form data securely
|
|
371
|
-
const formData = this.#collectFormData();
|
|
372
|
-
// Collect behavioral telemetry from all secure fields
|
|
373
|
-
const telemetry = this.#collectTelemetry();
|
|
374
|
-
// Audit log submission (include risk score for server-side correlation)
|
|
375
|
-
this.audit('form_submitted_enhanced', {
|
|
376
|
-
formId: this.#instanceId,
|
|
377
|
-
action: this.#formElement.action,
|
|
378
|
-
method: this.#formElement.method,
|
|
379
|
-
fieldCount: Object.keys(formData).length,
|
|
380
|
-
riskScore: telemetry.riskScore,
|
|
381
|
-
riskSignals: telemetry.riskSignals
|
|
382
|
-
});
|
|
383
|
-
// Dispatch pre-submit event for custom handling
|
|
384
|
-
const preSubmitEvent = new CustomEvent('secure-form-submit', {
|
|
385
|
-
detail: {
|
|
386
|
-
formData,
|
|
387
|
-
formElement: this.#formElement,
|
|
388
|
-
telemetry,
|
|
389
|
-
preventDefault: () => {
|
|
390
|
-
this.#isSubmitting = false;
|
|
391
|
-
this.#enableForm();
|
|
392
|
-
}
|
|
393
|
-
},
|
|
394
|
-
bubbles: true,
|
|
395
|
-
composed: true,
|
|
396
|
-
cancelable: true
|
|
397
|
-
});
|
|
398
|
-
const shouldContinue = this.dispatchEvent(preSubmitEvent);
|
|
399
|
-
if (!shouldContinue) {
|
|
400
|
-
// Custom handler prevented default submission
|
|
401
|
-
this.#isSubmitting = false;
|
|
402
|
-
this.#enableForm();
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
// Perform secure submission via Fetch
|
|
406
|
-
try {
|
|
407
|
-
await this.#submitForm(formData, telemetry);
|
|
408
|
-
}
|
|
409
|
-
catch (error) {
|
|
410
|
-
this.#showStatus('Submission failed. Please try again.', 'error');
|
|
411
|
-
this.audit('form_submission_error', {
|
|
412
|
-
formId: this.#instanceId,
|
|
413
|
-
error: error.message
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
finally {
|
|
417
|
-
this.#isSubmitting = false;
|
|
418
|
-
this.#enableForm();
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Sync secure-input component values to hidden form inputs
|
|
423
|
-
*
|
|
424
|
-
* CRITICAL for native form submission: Secure-input components have their
|
|
425
|
-
* actual <input> elements in Shadow DOM, which can't participate in native
|
|
426
|
-
* form submission. We create/update hidden inputs in the form for each
|
|
427
|
-
* secure-input to enable backend-agnostic form submission.
|
|
428
|
-
*
|
|
429
|
-
* @private
|
|
430
|
-
*/
|
|
431
|
-
#syncSecureInputsToForm() {
|
|
432
|
-
const secureInputs = this.#formElement.querySelectorAll('secure-input, secure-textarea, secure-select, secure-datetime, secure-file-upload');
|
|
433
|
-
secureInputs.forEach((input) => {
|
|
434
|
-
const name = input.getAttribute('name');
|
|
435
|
-
if (!name)
|
|
436
|
-
return;
|
|
437
|
-
// CRITICAL: Disable the native fallback inputs inside the secure component
|
|
438
|
-
// so they don't participate in native form submission (they are empty because
|
|
439
|
-
// the user typed into the shadow DOM input). Without this, the server receives
|
|
440
|
-
// the empty native input value first, ignoring the synced hidden input.
|
|
441
|
-
const nativeFallbacks = input.querySelectorAll(`input[name="${name}"], textarea[name="${name}"], select[name="${name}"]`);
|
|
442
|
-
nativeFallbacks.forEach((fallback) => {
|
|
443
|
-
fallback.removeAttribute('name');
|
|
444
|
-
});
|
|
445
|
-
// Check if hidden input already exists
|
|
446
|
-
let hiddenInput = this.#formElement.querySelector(`input[type="hidden"][data-secure-input="${name}"]`);
|
|
447
|
-
if (!hiddenInput) {
|
|
448
|
-
// Create hidden input for this secure-input
|
|
449
|
-
hiddenInput = document.createElement('input');
|
|
450
|
-
hiddenInput.type = 'hidden';
|
|
451
|
-
hiddenInput.setAttribute('data-secure-input', name);
|
|
452
|
-
hiddenInput.name = name;
|
|
453
|
-
this.#formElement.appendChild(hiddenInput);
|
|
454
|
-
}
|
|
455
|
-
// Sync the value
|
|
456
|
-
hiddenInput.value = input.value || '';
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Validate all secure fields in the form
|
|
461
|
-
*
|
|
462
|
-
* @private
|
|
463
|
-
*/
|
|
464
|
-
#validateAllFields() {
|
|
465
|
-
const errors = [];
|
|
466
|
-
// Find all secure input components within the form
|
|
467
|
-
const inputs = this.#formElement.querySelectorAll('secure-input, secure-textarea, secure-select, secure-datetime, secure-file-upload');
|
|
468
|
-
inputs.forEach((input) => {
|
|
469
|
-
if (typeof input.valid === 'boolean' && !input.valid) {
|
|
470
|
-
const label = input.getAttribute('label') || input.getAttribute('name') || 'Field';
|
|
471
|
-
errors.push(`${label} is invalid`);
|
|
472
|
-
}
|
|
473
|
-
});
|
|
474
|
-
return {
|
|
475
|
-
valid: errors.length === 0,
|
|
476
|
-
errors
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
/**
|
|
480
|
-
* Collect form data from secure fields
|
|
481
|
-
*
|
|
482
|
-
* Security Note: We collect data from secure components which have already
|
|
483
|
-
* sanitized their values. We also include the CSRF token.
|
|
484
|
-
*
|
|
485
|
-
* @private
|
|
486
|
-
*/
|
|
487
|
-
#collectFormData() {
|
|
488
|
-
const formData = Object.create(null);
|
|
489
|
-
// Collect from secure components within the form
|
|
490
|
-
const secureInputs = this.#formElement.querySelectorAll('secure-input, secure-textarea, secure-select, secure-datetime, secure-file-upload');
|
|
491
|
-
secureInputs.forEach((input) => {
|
|
492
|
-
const typedInput = input;
|
|
493
|
-
if (typedInput.name) {
|
|
494
|
-
formData[typedInput.name] = typedInput.value;
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
// Collect from standard form inputs (for non-secure fields)
|
|
498
|
-
const standardInputs = this.#formElement.querySelectorAll('input:not([type="hidden"]), textarea:not(.textarea-field), select:not(.select-field)');
|
|
499
|
-
standardInputs.forEach((input) => {
|
|
500
|
-
const typedInput = input;
|
|
501
|
-
if (typedInput.name) {
|
|
502
|
-
formData[typedInput.name] = this.sanitizeValue(typedInput.value);
|
|
503
|
-
}
|
|
504
|
-
});
|
|
505
|
-
// Include CSRF token
|
|
506
|
-
if (this.#csrfInput) {
|
|
507
|
-
formData[this.#csrfInput.name] = this.#csrfInput.value;
|
|
508
|
-
}
|
|
509
|
-
return formData;
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* Submit form data securely
|
|
513
|
-
*
|
|
514
|
-
* Security Note: We use fetch API with secure headers and proper CSRF handling.
|
|
515
|
-
* In production, ensure the server validates the CSRF token.
|
|
516
|
-
*
|
|
517
|
-
* @private
|
|
518
|
-
*/
|
|
519
|
-
async #submitForm(formData, telemetry) {
|
|
520
|
-
const action = this.#formElement.action;
|
|
521
|
-
const method = this.#formElement.method;
|
|
522
|
-
// Prepare headers
|
|
523
|
-
const headers = {
|
|
524
|
-
'Content-Type': 'application/json'
|
|
525
|
-
};
|
|
526
|
-
// Add CSRF token to header if specified
|
|
527
|
-
const csrfHeaderName = this.getAttribute('csrf-header-name');
|
|
528
|
-
if (csrfHeaderName && this.#csrfInput) {
|
|
529
|
-
headers[csrfHeaderName] = this.#csrfInput.value;
|
|
530
|
-
}
|
|
531
|
-
// Bundle telemetry alongside form data in a single request.
|
|
532
|
-
// The server receives both the user's input and behavioral context in one
|
|
533
|
-
// atomic payload, enabling server-side risk evaluation without a second round-trip.
|
|
534
|
-
// Using a prefixed key (_telemetry) avoids collisions with form field names.
|
|
535
|
-
const payload = { ...formData, _telemetry: telemetry };
|
|
536
|
-
// Perform fetch
|
|
537
|
-
const response = await fetch(action, {
|
|
538
|
-
method: method,
|
|
539
|
-
headers: headers,
|
|
540
|
-
body: JSON.stringify(payload),
|
|
541
|
-
credentials: 'same-origin', // Include cookies for CSRF validation
|
|
542
|
-
mode: 'cors',
|
|
543
|
-
cache: 'no-cache',
|
|
544
|
-
redirect: 'follow'
|
|
545
|
-
});
|
|
546
|
-
if (!response.ok) {
|
|
547
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
548
|
-
}
|
|
549
|
-
// Success
|
|
550
|
-
this.#showStatus('Form submitted successfully!', 'success');
|
|
551
|
-
// Dispatch success event
|
|
552
|
-
this.dispatchEvent(new CustomEvent('secure-form-success', {
|
|
553
|
-
detail: {
|
|
554
|
-
formData,
|
|
555
|
-
response,
|
|
556
|
-
telemetry
|
|
557
|
-
},
|
|
558
|
-
bubbles: true,
|
|
559
|
-
composed: true
|
|
560
|
-
}));
|
|
561
|
-
return response;
|
|
562
|
-
}
|
|
563
|
-
/**
|
|
564
|
-
* Disable form during submission
|
|
565
|
-
*
|
|
566
|
-
* @private
|
|
567
|
-
*/
|
|
568
|
-
#disableForm() {
|
|
569
|
-
// Disable all form controls
|
|
570
|
-
const controls = this.#formElement.querySelectorAll('input, textarea, select, button');
|
|
571
|
-
controls.forEach((control) => {
|
|
572
|
-
control.disabled = true;
|
|
573
|
-
});
|
|
574
|
-
// Also disable secure components
|
|
575
|
-
const secureFields = this.querySelectorAll('secure-input, secure-textarea, secure-select, secure-datetime, secure-file-upload');
|
|
576
|
-
secureFields.forEach((field) => {
|
|
577
|
-
field.setAttribute('disabled', '');
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
/**
|
|
581
|
-
* Enable form after submission
|
|
582
|
-
*
|
|
583
|
-
* @private
|
|
584
|
-
*/
|
|
585
|
-
#enableForm() {
|
|
586
|
-
// Enable all form controls
|
|
587
|
-
const controls = this.#formElement.querySelectorAll('input, textarea, select, button');
|
|
588
|
-
controls.forEach((control) => {
|
|
589
|
-
control.disabled = false;
|
|
590
|
-
});
|
|
591
|
-
// Also enable secure components
|
|
592
|
-
const secureFields = this.querySelectorAll('secure-input, secure-textarea, secure-select, secure-datetime, secure-file-upload');
|
|
593
|
-
secureFields.forEach((field) => {
|
|
594
|
-
field.removeAttribute('disabled');
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
/**
|
|
598
|
-
* Show status message
|
|
599
|
-
*
|
|
600
|
-
* @private
|
|
601
|
-
*/
|
|
602
|
-
#showStatus(message, type = 'info') {
|
|
603
|
-
this.#statusElement.textContent = message;
|
|
604
|
-
this.#statusElement.className = `form-status form-status-${type}`;
|
|
605
|
-
}
|
|
606
|
-
/**
|
|
607
|
-
* Clear status message
|
|
608
|
-
*
|
|
609
|
-
* @private
|
|
610
|
-
*/
|
|
611
|
-
#clearStatus() {
|
|
612
|
-
this.#statusElement.textContent = '';
|
|
613
|
-
this.#statusElement.className = 'form-status form-status-hidden';
|
|
614
|
-
}
|
|
615
|
-
// ── Telemetry ──────────────────────────────────────────────────────────────
|
|
616
|
-
/**
|
|
617
|
-
* Collect behavioral telemetry from all secure child fields and compute
|
|
618
|
-
* a session-level risk score.
|
|
619
|
-
*
|
|
620
|
-
* @private
|
|
621
|
-
*/
|
|
622
|
-
#collectTelemetry() {
|
|
623
|
-
const selector = 'secure-input, secure-textarea, secure-select, secure-datetime, secure-card';
|
|
624
|
-
const secureFields = this.querySelectorAll(selector);
|
|
625
|
-
const fields = [];
|
|
626
|
-
secureFields.forEach((field) => {
|
|
627
|
-
const typedField = field;
|
|
628
|
-
if (typeof typedField.getFieldTelemetry !== 'function')
|
|
629
|
-
return;
|
|
630
|
-
const fieldName = field.getAttribute('name') ?? field.tagName.toLowerCase();
|
|
631
|
-
const snapshot = {
|
|
632
|
-
...typedField.getFieldTelemetry(),
|
|
633
|
-
fieldName,
|
|
634
|
-
fieldType: field.tagName.toLowerCase(),
|
|
635
|
-
};
|
|
636
|
-
fields.push(snapshot);
|
|
637
|
-
});
|
|
638
|
-
const sessionDuration = Date.now() - this.#sessionStart;
|
|
639
|
-
const { riskScore, riskSignals } = this.#computeRiskScore(fields, sessionDuration);
|
|
640
|
-
return {
|
|
641
|
-
sessionDuration,
|
|
642
|
-
fieldCount: fields.length,
|
|
643
|
-
fields,
|
|
644
|
-
riskScore,
|
|
645
|
-
riskSignals,
|
|
646
|
-
submittedAt: new Date().toISOString(),
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
/**
|
|
650
|
-
* Compute a composite risk score 0–100 and list of contributing signals.
|
|
651
|
-
*
|
|
652
|
-
* Signal weights (additive, capped at 100):
|
|
653
|
-
* - Session completed in under 3 s: +30 (inhuman speed)
|
|
654
|
-
* - Session completed in under 8 s: +10 (very fast)
|
|
655
|
-
* - All fields pasted (no keystrokes anywhere): +25 (credential stuffing / scripted fill)
|
|
656
|
-
* - Any field has typing velocity > 15 ks/s: +15 (bot-like keyboard simulation)
|
|
657
|
-
* - Any field never focused (focusCount = 0): +15 (field was filled without user interaction)
|
|
658
|
-
* - Multiple fields probed without entry: +10 (focusCount > 1 but blurWithoutChange > 1)
|
|
659
|
-
* - High correction count on any field (> 5): +5 (deliberate obfuscation / hesitation)
|
|
660
|
-
* - Autofill on all non-empty fields: -10 (genuine browser autofill is low-risk)
|
|
661
|
-
*
|
|
662
|
-
* @private
|
|
663
|
-
*/
|
|
664
|
-
#computeRiskScore(fields, sessionDuration) {
|
|
665
|
-
const signals = [];
|
|
666
|
-
let score = 0;
|
|
667
|
-
// Session speed
|
|
668
|
-
if (sessionDuration < 3000) {
|
|
669
|
-
score += 30;
|
|
670
|
-
signals.push('session_too_fast');
|
|
671
|
-
}
|
|
672
|
-
else if (sessionDuration < 8000) {
|
|
673
|
-
score += 10;
|
|
674
|
-
signals.push('session_fast');
|
|
675
|
-
}
|
|
676
|
-
if (fields.length === 0) {
|
|
677
|
-
return { riskScore: Math.min(score, 100), riskSignals: signals };
|
|
678
|
-
}
|
|
679
|
-
// All fields pasted with zero keystrokes — scripted fill
|
|
680
|
-
const allPasted = fields.every(f => f.pasteDetected && f.velocity === 0);
|
|
681
|
-
if (allPasted) {
|
|
682
|
-
score += 25;
|
|
683
|
-
signals.push('all_fields_pasted');
|
|
684
|
-
}
|
|
685
|
-
// Any field with superhuman typing speed
|
|
686
|
-
const hasHighVelocity = fields.some(f => f.velocity > 15);
|
|
687
|
-
if (hasHighVelocity) {
|
|
688
|
-
score += 15;
|
|
689
|
-
signals.push('high_velocity_typing');
|
|
690
|
-
}
|
|
691
|
-
// Any field never touched (focusCount === 0) — programmatic fill
|
|
692
|
-
const hasUnfocusedField = fields.some(f => f.focusCount === 0);
|
|
693
|
-
if (hasUnfocusedField) {
|
|
694
|
-
score += 15;
|
|
695
|
-
signals.push('field_filled_without_focus');
|
|
696
|
-
}
|
|
697
|
-
// Form probing: multiple focus/blur cycles with no value entry
|
|
698
|
-
const hasProbing = fields.some(f => f.focusCount > 1 && f.blurWithoutChange > 1);
|
|
699
|
-
if (hasProbing) {
|
|
700
|
-
score += 10;
|
|
701
|
-
signals.push('form_probing');
|
|
702
|
-
}
|
|
703
|
-
// Excessive corrections on any field
|
|
704
|
-
const hasHighCorrections = fields.some(f => f.corrections > 5);
|
|
705
|
-
if (hasHighCorrections) {
|
|
706
|
-
score += 5;
|
|
707
|
-
signals.push('high_correction_count');
|
|
708
|
-
}
|
|
709
|
-
// Genuine autofill on all non-empty fields is a trust signal — reduce score
|
|
710
|
-
const autofillFields = fields.filter(f => f.autofillDetected);
|
|
711
|
-
if (autofillFields.length > 0 && autofillFields.length === fields.length) {
|
|
712
|
-
score -= 10;
|
|
713
|
-
signals.push('autofill_detected');
|
|
714
|
-
}
|
|
715
|
-
return { riskScore: Math.max(0, Math.min(score, 100)), riskSignals: signals };
|
|
716
|
-
}
|
|
717
|
-
/**
|
|
718
|
-
* Get form data
|
|
719
|
-
*
|
|
720
|
-
* @public
|
|
721
|
-
*/
|
|
722
|
-
getData() {
|
|
723
|
-
return this.#collectFormData();
|
|
724
|
-
}
|
|
725
|
-
/**
|
|
726
|
-
* Reset the form
|
|
727
|
-
*
|
|
728
|
-
* @public
|
|
729
|
-
*/
|
|
730
|
-
reset() {
|
|
731
|
-
if (this.#formElement) {
|
|
732
|
-
this.#formElement.reset();
|
|
733
|
-
this.#clearStatus();
|
|
734
|
-
this.audit('form_reset', {
|
|
735
|
-
formId: this.#instanceId
|
|
736
|
-
});
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
/**
|
|
740
|
-
* Programmatically submit the form
|
|
741
|
-
*
|
|
742
|
-
* @public
|
|
743
|
-
*/
|
|
744
|
-
submit() {
|
|
745
|
-
if (this.#formElement) {
|
|
746
|
-
// Trigger submit event which will run our validation
|
|
747
|
-
this.#formElement.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
/**
|
|
751
|
-
* Check if form is valid
|
|
752
|
-
*
|
|
753
|
-
* @public
|
|
754
|
-
*/
|
|
755
|
-
get valid() {
|
|
756
|
-
const validation = this.#validateAllFields();
|
|
757
|
-
return validation.valid;
|
|
758
|
-
}
|
|
759
|
-
/**
|
|
760
|
-
* Cleanup on disconnect
|
|
761
|
-
*/
|
|
762
|
-
disconnectedCallback() {
|
|
763
|
-
// Clear any sensitive form data
|
|
764
|
-
if (this.#formElement) {
|
|
765
|
-
this.#formElement.reset();
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
/**
|
|
769
|
-
* Handle attribute changes
|
|
770
|
-
*/
|
|
771
|
-
attributeChangedCallback(name, _oldValue, newValue) {
|
|
772
|
-
if (!this.#formElement)
|
|
773
|
-
return;
|
|
774
|
-
switch (name) {
|
|
775
|
-
case 'security-tier':
|
|
776
|
-
this.#securityTier = (newValue || SecurityTier.PUBLIC);
|
|
777
|
-
break;
|
|
778
|
-
case 'action':
|
|
779
|
-
this.#formElement.action = newValue;
|
|
780
|
-
break;
|
|
781
|
-
case 'method':
|
|
782
|
-
this.#formElement.method = newValue;
|
|
783
|
-
break;
|
|
784
|
-
case 'csrf-token':
|
|
785
|
-
if (this.#csrfInput) {
|
|
786
|
-
this.#csrfInput.value = newValue;
|
|
787
|
-
}
|
|
788
|
-
break;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
/**
|
|
792
|
-
* Get security tier
|
|
793
|
-
*/
|
|
794
|
-
get securityTier() {
|
|
795
|
-
return this.#securityTier;
|
|
796
|
-
}
|
|
797
|
-
/**
|
|
798
|
-
* Sanitize a value to prevent XSS
|
|
799
|
-
*
|
|
800
|
-
* Uses the same div.textContent round-trip as SecureBaseComponent to correctly
|
|
801
|
-
* handle all injection vectors (attribute injection, entity encoding, etc).
|
|
802
|
-
*/
|
|
803
|
-
sanitizeValue(value) {
|
|
804
|
-
if (typeof value !== 'string')
|
|
805
|
-
return '';
|
|
806
|
-
const div = document.createElement('div');
|
|
807
|
-
div.textContent = value;
|
|
808
|
-
return div.innerHTML;
|
|
809
|
-
}
|
|
810
|
-
/**
|
|
811
|
-
* Audit log helper
|
|
812
|
-
*/
|
|
813
|
-
audit(action, data) {
|
|
814
|
-
if (console.debug) {
|
|
815
|
-
console.debug(`[secure-form] ${action}`, data);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
/**
|
|
819
|
-
* Check rate limit (stub - implement proper rate limiting in production)
|
|
820
|
-
*/
|
|
821
|
-
checkRateLimit() {
|
|
822
|
-
return { allowed: true, retryAfter: 0 };
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
_a = SecureForm;
|
|
826
|
-
// Define the custom element
|
|
827
|
-
customElements.define('secure-form', SecureForm);
|
|
828
|
-
export default SecureForm;
|
|
829
|
-
//# sourceMappingURL=secure-form.js.map
|
|
33
|
+
*/var d;import{SecurityTier as u}from"../../core/security-config.js";class m extends HTMLElement{static __stylesAdded=!1;#t=null;#e=null;#i=null;#n=!1;#u=Date.now();#s=`secure-form-${Math.random().toString(36).substring(2,11)}`;#r=u.PUBLIC;static get observedAttributes(){return["security-tier","action","method","enctype","csrf-token","csrf-header-name","csrf-field-name","novalidate"]}constructor(){super()}connectedCallback(){if(this.#t)return;this.#u=Date.now();const t=this.getAttribute("security-tier");t&&(this.#r=t);const s=this.querySelector("form");if(s){this.#t=s,this.#t.id=this.#s,this.#t.classList.contains("secure-form")||this.#t.classList.add("secure-form"),this.#l();const e=this.getAttribute("csrf-field-name")||"csrf_token",i=s.querySelector(`input[name="${e}"]`);if(i){this.#e=i;const r=this.getAttribute("csrf-token");r&&i.value!==r&&(i.value=r)}else this.#h()}else{for(this.#t=document.createElement("form"),this.#t.id=this.#s,this.#t.className="secure-form",this.#l(),this.#h();this.firstChild;)this.#t.appendChild(this.firstChild);this.appendChild(this.#t)}this.#i=document.createElement("div"),this.#i.className="form-status form-status-hidden",this.#i.setAttribute("role","status"),this.#i.setAttribute("aria-live","polite"),this.#t.insertBefore(this.#i,this.#t.firstChild),this.#p(),this.#b(),this.audit("form_initialized",{formId:this.#s,action:this.#t.action,method:this.#t.method})}#p(){if(!d.__stylesAdded){const t=document.createElement("link");t.rel="stylesheet",t.href=new URL("./secure-form.css",import.meta.url).href,document.head.appendChild(t),d.__stylesAdded=!0}}#l(){const t=this.getAttribute("action");t&&(this.#t.action=t);const s=this.getAttribute("method")||"POST";this.#t.method=s.toUpperCase();const e=this.getAttribute("enctype")||"application/x-www-form-urlencoded";this.#t.enctype=e,this.hasAttribute("novalidate")&&(this.#t.noValidate=!0),(this.#r===u.SENSITIVE||this.#r===u.CRITICAL)&&(this.#t.autocomplete="off")}#h(){const t=this.getAttribute("csrf-token");if(t){this.#e=document.createElement("input"),this.#e.type="hidden";const s=this.getAttribute("csrf-field-name")||"csrf_token";this.#e.name=s,this.#e.value=t,this.#t.appendChild(this.#e),this.audit("csrf_token_injected",{formId:this.#s,fieldName:s})}else(this.securityTier===u.SENSITIVE||this.securityTier===u.CRITICAL)&&console.warn("CSRF token not provided for SENSITIVE/CRITICAL tier form")}#b(){this.#t.addEventListener("submit",t=>{this.#y(t)}),this.addEventListener("secure-input",t=>{this.#o(t)}),this.addEventListener("secure-textarea",t=>{this.#o(t)}),this.addEventListener("secure-select",t=>{this.#o(t)})}#o(t){this.#f()}async#y(t){const s=this.hasAttribute("enhance");if(this.#n){t.preventDefault();return}(this.#r===u.SENSITIVE||this.#r===u.CRITICAL)&&!this.#e?.value&&this.dispatchEvent(new CustomEvent("secure-threat-detected",{detail:{fieldName:this.#s,threatType:"csrf-token-absent",patternId:"csrf-token-absent",tier:this.#r,timestamp:Date.now()},bubbles:!0,composed:!0}));const e=this.checkRateLimit();if(!e.allowed){t.preventDefault(),this.#a(`Too many submission attempts. Please wait ${Math.ceil(e.retryAfter/1e3)} seconds.`,"error"),this.audit("form_rate_limited",{formId:this.#s,retryAfter:e.retryAfter});return}const i=this.#d();if(!i.valid){t.preventDefault(),this.#a(i.errors.join(", "),"error"),this.audit("form_validation_failed",{formId:this.#s,errors:i.errors});return}if(!s){this.#g(),this.audit("form_submitted_native",{formId:this.#s,action:this.#t.action,method:this.#t.method});return}t.preventDefault(),this.#n=!0,this.#a("Submitting...","info"),this.#S();const r=this.#m(),a=this.#A();this.audit("form_submitted_enhanced",{formId:this.#s,action:this.#t.action,method:this.#t.method,fieldCount:Object.keys(r).length,riskScore:a.riskScore,riskSignals:a.riskSignals});const c=new CustomEvent("secure-form-submit",{detail:{formData:r,formElement:this.#t,telemetry:a,preventDefault:()=>{this.#n=!1,this.#c()}},bubbles:!0,composed:!0,cancelable:!0});if(!this.dispatchEvent(c)){this.#n=!1,this.#c();return}try{await this.#v(r,a)}catch(l){this.#a("Submission failed. Please try again.","error"),this.audit("form_submission_error",{formId:this.#s,error:l.message})}finally{this.#n=!1,this.#c()}}#g(){this.#t.querySelectorAll("secure-input, secure-textarea, secure-select, secure-datetime, secure-file-upload").forEach(s=>{const e=s.getAttribute("name");if(!e)return;s.querySelectorAll(`input[name="${e}"], textarea[name="${e}"], select[name="${e}"]`).forEach(a=>{a.removeAttribute("name")});let r=this.#t.querySelector(`input[type="hidden"][data-secure-input="${e}"]`);r||(r=document.createElement("input"),r.type="hidden",r.setAttribute("data-secure-input",e),r.name=e,this.#t.appendChild(r)),r.value=s.value||""})}#d(){const t=[];return this.#t.querySelectorAll("secure-input, secure-textarea, secure-select, secure-datetime, secure-file-upload").forEach(e=>{if(typeof e.valid=="boolean"&&!e.valid){const i=e.getAttribute("label")||e.getAttribute("name")||"Field";t.push(`${i} is invalid`)}}),{valid:t.length===0,errors:t}}#m(){const t=Object.create(null);return this.#t.querySelectorAll("secure-input, secure-textarea, secure-select, secure-datetime, secure-file-upload").forEach(i=>{const r=i;r.name&&(t[r.name]=r.value)}),this.#t.querySelectorAll('input:not([type="hidden"]), textarea:not(.textarea-field), select:not(.select-field)').forEach(i=>{const r=i;r.name&&(t[r.name]=this.sanitizeValue(r.value))}),this.#e&&(t[this.#e.name]=this.#e.value),t}async#v(t,s){const e=this.#t.action,i=this.#t.method,r={"Content-Type":"application/json"},a=this.getAttribute("csrf-header-name");a&&this.#e&&(r[a]=this.#e.value);const c={...t,_telemetry:s},o=await fetch(e,{method:i,headers:r,body:JSON.stringify(c),credentials:"same-origin",mode:"cors",cache:"no-cache",redirect:"follow"});if(!o.ok)throw new Error(`HTTP ${o.status}: ${o.statusText}`);return this.#a("Form submitted successfully!","success"),this.dispatchEvent(new CustomEvent("secure-form-success",{detail:{formData:t,response:o,telemetry:s},bubbles:!0,composed:!0})),o}#S(){this.#t.querySelectorAll("input, textarea, select, button").forEach(e=>{e.disabled=!0}),this.querySelectorAll("secure-input, secure-textarea, secure-select, secure-datetime, secure-file-upload").forEach(e=>{e.setAttribute("disabled","")})}#c(){this.#t.querySelectorAll("input, textarea, select, button").forEach(e=>{e.disabled=!1}),this.querySelectorAll("secure-input, secure-textarea, secure-select, secure-datetime, secure-file-upload").forEach(e=>{e.removeAttribute("disabled")})}#a(t,s="info"){this.#i.textContent=t,this.#i.className=`form-status form-status-${s}`}#f(){this.#i.textContent="",this.#i.className="form-status form-status-hidden"}#A(){const s=this.querySelectorAll("secure-input, secure-textarea, secure-select, secure-datetime, secure-card"),e=[];s.forEach(c=>{const o=c;if(typeof o.getFieldTelemetry!="function")return;const l=c.getAttribute("name")??c.tagName.toLowerCase(),h={...o.getFieldTelemetry(),fieldName:l,fieldType:c.tagName.toLowerCase()};e.push(h)});const i=Date.now()-this.#u,{riskScore:r,riskSignals:a}=this.#C(e,i);return{sessionDuration:i,fieldCount:e.length,fields:e,riskScore:r,riskSignals:a,submittedAt:new Date().toISOString()}}#C(t,s){const e=[];let i=0;if(s<3e3?(i+=30,e.push("session_too_fast")):s<8e3&&(i+=10,e.push("session_fast")),t.length===0)return{riskScore:Math.min(i,100),riskSignals:e};t.every(n=>n.pasteDetected&&n.velocity===0)&&(i+=25,e.push("all_fields_pasted")),t.some(n=>n.velocity>15)&&(i+=15,e.push("high_velocity_typing")),t.some(n=>n.focusCount===0)&&(i+=15,e.push("field_filled_without_focus")),t.some(n=>n.focusCount>1&&n.blurWithoutChange>1)&&(i+=10,e.push("form_probing")),t.some(n=>n.corrections>5)&&(i+=5,e.push("high_correction_count"));const h=t.filter(n=>n.autofillDetected);return h.length>0&&h.length===t.length&&(i-=10,e.push("autofill_detected")),{riskScore:Math.max(0,Math.min(i,100)),riskSignals:e}}getData(){return this.#m()}reset(){this.#t&&(this.#t.reset(),this.#f(),this.audit("form_reset",{formId:this.#s}))}submit(){this.#t&&this.#t.dispatchEvent(new Event("submit",{bubbles:!0,cancelable:!0}))}get valid(){return this.#d().valid}disconnectedCallback(){this.#t&&this.#t.reset()}attributeChangedCallback(t,s,e){if(this.#t)switch(t){case"security-tier":this.#r=e||u.PUBLIC;break;case"action":this.#t.action=e;break;case"method":this.#t.method=e;break;case"csrf-token":this.#e&&(this.#e.value=e);break}}get securityTier(){return this.#r}sanitizeValue(t){if(typeof t!="string")return"";const s=document.createElement("div");return s.textContent=t,s.innerHTML}audit(t,s){console.debug&&console.debug(`[secure-form] ${t}`,s)}checkRateLimit(){return{allowed:!0,retryAfter:0}}}d=m,customElements.define("secure-form",m);var b=m;export{m as SecureForm,b as default};
|