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.
@@ -15,503 +15,4 @@
15
15
  *
16
16
  * @module base-component
17
17
  * @license MIT
18
- */
19
- var _a;
20
- import { SecurityTier, getTierConfig, isValidTier, } from './security-config.js';
21
- /**
22
- * Base class for all Secure-UI components
23
- *
24
- * All components in the Secure-UI library should extend this class to inherit
25
- * core security functionality and standardized behavior.
26
- *
27
- * Security Architecture:
28
- * - Closed Shadow DOM prevents external tampering
29
- * - All attributes are sanitized on read
30
- * - Security tier is immutable after initial set
31
- * - Default tier is CRITICAL (fail secure)
32
- */
33
- export class SecureBaseComponent extends HTMLElement {
34
- /** Maximum number of entries retained in the in-memory audit log */
35
- static #MAX_AUDIT_LOG_SIZE = 1000;
36
- /**
37
- * Injection detection patterns applied to raw input values on every input event.
38
- * Ordered by descending severity. Only the first match is reported per event.
39
- * Raw values are never included in the resulting threat event.
40
- */
41
- /**
42
- * Human-readable labels for each injection pattern ID.
43
- * Used by components that render threat feedback UI (threat-feedback attribute).
44
- */
45
- static #THREAT_LABELS = {
46
- 'script-tag': 'Script injection blocked',
47
- 'js-protocol': 'JavaScript protocol blocked',
48
- 'event-handler': 'Event handler injection blocked',
49
- 'html-injection': 'HTML element injection blocked',
50
- 'css-expression': 'CSS expression injection blocked',
51
- 'vbscript': 'VBScript injection blocked',
52
- 'data-uri-html': 'Data URI injection blocked',
53
- 'template-syntax': 'Template injection blocked',
54
- };
55
- static #INJECTION_PATTERNS = [
56
- { id: 'script-tag', pattern: /<script[\s>/]/i },
57
- { id: 'js-protocol', pattern: /javascript\s*:/i },
58
- { id: 'event-handler', pattern: /\bon\w+\s*=/i },
59
- { id: 'html-injection', pattern: /<\s*(img|svg|iframe|object|embed|link|meta|base)[^>]*/i },
60
- { id: 'css-expression', pattern: /expression\s*\(/i },
61
- { id: 'vbscript', pattern: /vbscript\s*:/i },
62
- { id: 'data-uri-html', pattern: /data:\s*text\/html/i },
63
- { id: 'template-syntax', pattern: /\{\{[\s\S]*?\}\}/ },
64
- ];
65
- #securityTier = SecurityTier.CRITICAL;
66
- #config;
67
- #shadow;
68
- #auditLog = [];
69
- #rateLimitState = {
70
- attempts: 0,
71
- windowStart: Date.now()
72
- };
73
- #initialized = false;
74
- #telemetryState = {
75
- focusAt: null,
76
- firstKeystrokeAt: null,
77
- blurAt: null,
78
- keyCount: 0,
79
- correctionCount: 0,
80
- pasteDetected: false,
81
- autofillDetected: false,
82
- focusCount: 0,
83
- blurWithoutChange: 0,
84
- lastInputLength: 0,
85
- };
86
- /**
87
- * Constructor
88
- *
89
- * Security Note: Creates a CLOSED shadow DOM to prevent external JavaScript
90
- * from accessing or modifying the component's internal DOM.
91
- */
92
- constructor() {
93
- super();
94
- this.#shadow = this.attachShadow({ mode: 'closed' });
95
- this.#config = getTierConfig(this.#securityTier);
96
- }
97
- /**
98
- * Observed attributes - must be overridden by child classes
99
- */
100
- static get observedAttributes() {
101
- return ['security-tier', 'disabled', 'readonly', 'threat-feedback'];
102
- }
103
- /**
104
- * Called when element is added to DOM
105
- */
106
- connectedCallback() {
107
- if (!this.#initialized) {
108
- this.#initialize();
109
- this.#initialized = true;
110
- }
111
- }
112
- #initialize() {
113
- this.initializeSecurity();
114
- this.#render();
115
- }
116
- /**
117
- * Initialize security tier, config, and audit logging without triggering render.
118
- *
119
- * Components that manage their own rendering (e.g. secure-table) can call this
120
- * from their connectedCallback instead of super.connectedCallback() to get
121
- * security initialization without the base render lifecycle.
122
- * @protected
123
- */
124
- initializeSecurity() {
125
- const tierAttr = this.getAttribute('security-tier');
126
- if (tierAttr && isValidTier(tierAttr)) {
127
- this.#securityTier = tierAttr;
128
- }
129
- this.#config = getTierConfig(this.#securityTier);
130
- this.#audit('component_initialized', {
131
- tier: this.#securityTier,
132
- timestamp: new Date().toISOString()
133
- });
134
- }
135
- /**
136
- * Called when an observed attribute changes
137
- *
138
- * Security Note: security-tier attribute is immutable after initialization
139
- * to prevent privilege escalation.
140
- */
141
- attributeChangedCallback(name, oldValue, newValue) {
142
- if (name === 'security-tier' && this.#initialized) {
143
- console.warn(`Security tier cannot be changed after initialization. ` +
144
- `Attempted change from "${oldValue}" to "${newValue}" blocked.`);
145
- if (oldValue !== null) {
146
- this.setAttribute('security-tier', oldValue);
147
- }
148
- return;
149
- }
150
- if (this.#initialized) {
151
- this.handleAttributeChange(name, oldValue, newValue);
152
- }
153
- }
154
- /**
155
- * Handle attribute changes - to be overridden by child classes
156
- */
157
- handleAttributeChange(_name, _oldValue, _newValue) {
158
- // Child classes should override this method
159
- }
160
- #render() {
161
- this.#shadow.innerHTML = '';
162
- // Base styles via <link> — loads from 'self', fully CSP-safe.
163
- // Using adoptedStyleSheets + replaceSync(inlineString) triggers CSP violations
164
- // when style-src lacks 'unsafe-inline'. A <link> element loading from 'self'
165
- // is always permitted.
166
- const baseLink = document.createElement('link');
167
- baseLink.rel = 'stylesheet';
168
- baseLink.href = new URL('./base.css', import.meta.url).href;
169
- this.#shadow.appendChild(baseLink);
170
- const content = this.render();
171
- if (content) {
172
- this.#shadow.appendChild(content);
173
- }
174
- }
175
- /**
176
- * Returns the URL of the shared base stylesheet.
177
- * Components that manage their own rendering (e.g. secure-table) can use
178
- * this to inject the base <link> themselves.
179
- * @protected
180
- */
181
- getBaseStylesheetUrl() {
182
- return new URL('./base.css', import.meta.url).href;
183
- }
184
- /**
185
- * Inject a component stylesheet into the shadow root via <link>.
186
- * Accepts a URL (use import.meta.url to derive it, e.g.
187
- * new URL('./my-component.css', import.meta.url).href
188
- * ). Loading from 'self' satisfies strict CSP without unsafe-inline.
189
- * @protected
190
- */
191
- addComponentStyles(cssUrl) {
192
- const link = document.createElement('link');
193
- link.rel = 'stylesheet';
194
- link.href = cssUrl;
195
- this.#shadow.appendChild(link);
196
- }
197
- /**
198
- * Sanitize a string value to prevent XSS
199
- */
200
- sanitizeValue(value) {
201
- if (typeof value !== 'string') {
202
- return '';
203
- }
204
- const div = document.createElement('div');
205
- div.textContent = value;
206
- return div.innerHTML;
207
- }
208
- /**
209
- * Validate input against tier-specific rules
210
- */
211
- validateInput(value, options = {}) {
212
- const errors = [];
213
- const config = this.#config;
214
- const isRequired = options.required !== undefined ? options.required : config.validation.required;
215
- if (isRequired && (!value || value.trim().length === 0)) {
216
- errors.push('This field is required');
217
- }
218
- const maxLength = options.maxLength || config.validation.maxLength;
219
- if (value && value.length > maxLength) {
220
- errors.push(`Value exceeds maximum length of ${maxLength}`);
221
- }
222
- const minLength = options.minLength || 0;
223
- if (value && value.length < minLength) {
224
- errors.push(`Value must be at least ${minLength} characters`);
225
- }
226
- const pattern = options.pattern || config.validation.pattern;
227
- if (pattern && value && !pattern.test(value)) {
228
- errors.push('Value does not match required format');
229
- }
230
- if (config.validation.strict && errors.length > 0) {
231
- this.#audit('validation_failed', {
232
- errors,
233
- valueLength: value ? value.length : 0
234
- });
235
- }
236
- return {
237
- valid: errors.length === 0,
238
- errors
239
- };
240
- }
241
- /**
242
- * Check rate limit for this component
243
- */
244
- checkRateLimit() {
245
- if (!this.#config.rateLimit.enabled) {
246
- return { allowed: true, retryAfter: 0 };
247
- }
248
- const now = Date.now();
249
- const windowMs = this.#config.rateLimit.windowMs;
250
- if (now - this.#rateLimitState.windowStart > windowMs) {
251
- this.#rateLimitState.attempts = 0;
252
- this.#rateLimitState.windowStart = now;
253
- }
254
- if (this.#rateLimitState.attempts >= this.#config.rateLimit.maxAttempts) {
255
- const retryAfter = windowMs - (now - this.#rateLimitState.windowStart);
256
- this.#audit('rate_limit_exceeded', {
257
- attempts: this.#rateLimitState.attempts,
258
- retryAfter
259
- });
260
- return { allowed: false, retryAfter };
261
- }
262
- this.#rateLimitState.attempts++;
263
- return { allowed: true, retryAfter: 0 };
264
- }
265
- #audit(event, data = {}) {
266
- const config = this.#config.audit;
267
- const shouldLog = (event.includes('access') && config.logAccess) ||
268
- (event.includes('change') && config.logChanges) ||
269
- (event.includes('submit') && config.logSubmission) ||
270
- event.includes('initialized') ||
271
- event.includes('rate_limit') ||
272
- event.includes('validation') ||
273
- event.includes('threat');
274
- if (!shouldLog) {
275
- return;
276
- }
277
- const logEntry = {
278
- event,
279
- tier: this.#securityTier,
280
- timestamp: new Date().toISOString(),
281
- ...data
282
- };
283
- if (config.includeMetadata) {
284
- logEntry.userAgent = navigator.userAgent;
285
- logEntry.language = navigator.language;
286
- }
287
- // Cap log size to prevent unbounded memory growth (DoS mitigation)
288
- if (this.#auditLog.length >= _a.#MAX_AUDIT_LOG_SIZE) {
289
- this.#auditLog.shift();
290
- }
291
- this.#auditLog.push(logEntry);
292
- this.dispatchEvent(new CustomEvent('secure-audit', {
293
- detail: logEntry,
294
- bubbles: true,
295
- composed: true
296
- }));
297
- }
298
- /**
299
- * Get the shadow root (protected access for child classes)
300
- */
301
- get shadowRoot() {
302
- return this.#shadow;
303
- }
304
- /**
305
- * Get the current security tier
306
- */
307
- get securityTier() {
308
- return this.#securityTier;
309
- }
310
- /**
311
- * Get the tier configuration
312
- */
313
- get config() {
314
- return this.#config;
315
- }
316
- /**
317
- * Get all audit log entries
318
- */
319
- getAuditLog() {
320
- return [...this.#auditLog];
321
- }
322
- /**
323
- * Clear the local audit log
324
- */
325
- clearAuditLog() {
326
- this.#auditLog = [];
327
- }
328
- /**
329
- * Trigger an audit event from child classes
330
- */
331
- audit(event, data) {
332
- this.#audit(event, data);
333
- }
334
- /**
335
- * Check a raw input value for injection patterns and signal if one is found.
336
- *
337
- * Called by input components on every input event, after the raw value is captured.
338
- * Only the first matching pattern is reported to avoid event flooding.
339
- * The raw value is never included in the dispatched event detail.
340
- *
341
- * @param value - Raw field value (unmodified user input)
342
- * @param fieldName - The field's name attribute
343
- */
344
- detectInjection(value, fieldName) {
345
- for (const { id, pattern } of _a.#INJECTION_PATTERNS) {
346
- if (pattern.test(value)) {
347
- this.audit('threat_detected', {
348
- fieldName,
349
- patternId: id,
350
- threatType: 'injection',
351
- });
352
- this.dispatchEvent(new CustomEvent('secure-threat-detected', {
353
- detail: {
354
- fieldName,
355
- threatType: 'injection',
356
- patternId: id,
357
- tier: this.securityTier,
358
- timestamp: Date.now(),
359
- },
360
- bubbles: true,
361
- composed: true,
362
- }));
363
- if (this.hasAttribute('threat-feedback')) {
364
- this.showThreatFeedback(id, this.securityTier);
365
- }
366
- return; // first match only
367
- }
368
- }
369
- // No threat found — clear any lingering feedback
370
- if (this.hasAttribute('threat-feedback')) {
371
- this.clearThreatFeedback();
372
- }
373
- }
374
- /**
375
- * Show an inline threat feedback message inside the component.
376
- * Called by detectInjection() when threat-feedback attribute is present.
377
- * Override in child classes that render a threat UI container.
378
- * @protected
379
- */
380
- showThreatFeedback(_patternId, _tier) {
381
- // No-op — child classes override when they support inline threat UI
382
- }
383
- /**
384
- * Clear any visible threat feedback.
385
- * Called by detectInjection() when input is clean and threat-feedback is set.
386
- * @protected
387
- */
388
- clearThreatFeedback() {
389
- // No-op — child classes override when they support inline threat UI
390
- }
391
- /**
392
- * Returns a human-readable label for the given injection pattern ID.
393
- * @protected
394
- */
395
- getThreatLabel(patternId) {
396
- return _a.#THREAT_LABELS[patternId] ?? `Injection blocked: ${patternId}`;
397
- }
398
- /**
399
- * Force re-render of the component
400
- */
401
- rerender() {
402
- this.#render();
403
- }
404
- // ── Telemetry collection ────────────────────────────────────────────────────
405
- /**
406
- * Call from the field's `focus` event handler to start a telemetry session.
407
- * @protected
408
- */
409
- recordTelemetryFocus() {
410
- const t = this.#telemetryState;
411
- t.focusAt = Date.now();
412
- t.blurAt = null;
413
- t.focusCount++;
414
- // snapshot input length at focus so we can detect blur-without-change
415
- const el = this.shadowRoot.querySelector('input:not([type="hidden"]), textarea, select');
416
- t.lastInputLength = el ? el.value.length : 0;
417
- }
418
- /**
419
- * Call from the field's `input` event handler.
420
- * Pass the native `InputEvent` so inputType can be inspected.
421
- * @protected
422
- */
423
- recordTelemetryInput(event) {
424
- const t = this.#telemetryState;
425
- const now = Date.now();
426
- if (t.firstKeystrokeAt === null) {
427
- t.firstKeystrokeAt = now;
428
- }
429
- const inputEvent = event;
430
- const inputType = inputEvent.inputType ?? '';
431
- if (inputType === 'insertFromPaste' || inputType === 'insertFromPasteAsQuotation') {
432
- t.pasteDetected = true;
433
- }
434
- else if (inputType === 'insertReplacementText') {
435
- // Browser autofill triggers this type
436
- t.autofillDetected = true;
437
- }
438
- else if (inputType.startsWith('delete') ||
439
- inputType === 'historyUndo' ||
440
- inputType === 'historyRedo') {
441
- t.correctionCount++;
442
- }
443
- else {
444
- t.keyCount++;
445
- }
446
- const el = event.target;
447
- if (el)
448
- t.lastInputLength = el.value.length;
449
- }
450
- /**
451
- * Call from the field's `blur` event handler to finalise the telemetry session.
452
- * @protected
453
- */
454
- recordTelemetryBlur() {
455
- const t = this.#telemetryState;
456
- t.blurAt = Date.now();
457
- const el = this.shadowRoot.querySelector('input:not([type="hidden"]), textarea, select');
458
- const currentLength = el ? el.value.length : 0;
459
- if (currentLength === t.lastInputLength && t.keyCount === 0 && !t.pasteDetected) {
460
- t.blurWithoutChange++;
461
- }
462
- }
463
- /**
464
- * Returns computed behavioral signals for the current (or last completed)
465
- * interaction session. Safe to include in server payloads — contains no
466
- * raw field values or PII.
467
- */
468
- getFieldTelemetry() {
469
- const t = this.#telemetryState;
470
- const focusAt = t.focusAt ?? Date.now();
471
- const firstKeystrokeAt = t.firstKeystrokeAt;
472
- const blurAt = t.blurAt ?? Date.now();
473
- const dwell = firstKeystrokeAt !== null ? firstKeystrokeAt - focusAt : 0;
474
- const completionTime = firstKeystrokeAt !== null ? blurAt - firstKeystrokeAt : 0;
475
- const durationSec = completionTime / 1000;
476
- const velocity = durationSec > 0 ? t.keyCount / durationSec : 0;
477
- return {
478
- dwell,
479
- completionTime,
480
- velocity: Math.round(velocity * 100) / 100,
481
- corrections: t.correctionCount,
482
- pasteDetected: t.pasteDetected,
483
- autofillDetected: t.autofillDetected,
484
- focusCount: t.focusCount,
485
- blurWithoutChange: t.blurWithoutChange,
486
- };
487
- }
488
- #resetTelemetryState() {
489
- this.#telemetryState = {
490
- focusAt: null,
491
- firstKeystrokeAt: null,
492
- blurAt: null,
493
- keyCount: 0,
494
- correctionCount: 0,
495
- pasteDetected: false,
496
- autofillDetected: false,
497
- focusCount: 0,
498
- blurWithoutChange: 0,
499
- lastInputLength: 0,
500
- };
501
- }
502
- /**
503
- * Clean up when component is removed from DOM
504
- */
505
- disconnectedCallback() {
506
- this.#rateLimitState = { attempts: 0, windowStart: Date.now() };
507
- this.#resetTelemetryState();
508
- if (this.#config.audit.logAccess) {
509
- this.#audit('component_disconnected', {
510
- timestamp: new Date().toISOString()
511
- });
512
- }
513
- }
514
- }
515
- _a = SecureBaseComponent;
516
- export default SecureBaseComponent;
517
- //# sourceMappingURL=base-component.js.map
18
+ */var c;import{SecurityTier as h,getTierConfig as l,isValidTier as d}from"./security-config.js";class u extends HTMLElement{static#l=1e3;static#u={"script-tag":"Script injection blocked","js-protocol":"JavaScript protocol blocked","event-handler":"Event handler injection blocked","html-injection":"HTML element injection blocked","css-expression":"CSS expression injection blocked",vbscript:"VBScript injection blocked","data-uri-html":"Data URI injection blocked","template-syntax":"Template injection blocked"};static#h=[{id:"script-tag",pattern:/<script[\s>/]/i},{id:"js-protocol",pattern:/javascript\s*:/i},{id:"event-handler",pattern:/\bon\w+\s*=/i},{id:"html-injection",pattern:/<\s*(img|svg|iframe|object|embed|link|meta|base)[^>]*/i},{id:"css-expression",pattern:/expression\s*\(/i},{id:"vbscript",pattern:/vbscript\s*:/i},{id:"data-uri-html",pattern:/data:\s*text\/html/i},{id:"template-syntax",pattern:/\{\{[\s\S]*?\}\}/}];#i=h.CRITICAL;#t;#n;#s=[];#e={attempts:0,windowStart:Date.now()};#o=!1;#r={focusAt:null,firstKeystrokeAt:null,blurAt:null,keyCount:0,correctionCount:0,pasteDetected:!1,autofillDetected:!1,focusCount:0,blurWithoutChange:0,lastInputLength:0};constructor(){super(),this.#n=this.attachShadow({mode:"closed"}),this.#t=l(this.#i)}static get observedAttributes(){return["security-tier","disabled","readonly","threat-feedback"]}connectedCallback(){this.#o||(this.#d(),this.#o=!0)}#d(){this.initializeSecurity(),this.#c()}initializeSecurity(){const t=this.getAttribute("security-tier");t&&d(t)&&(this.#i=t),this.#t=l(this.#i),this.#a("component_initialized",{tier:this.#i,timestamp:new Date().toISOString()})}attributeChangedCallback(t,e,i){if(t==="security-tier"&&this.#o){console.warn(`Security tier cannot be changed after initialization. Attempted change from "${e}" to "${i}" blocked.`),e!==null&&this.setAttribute("security-tier",e);return}this.#o&&this.handleAttributeChange(t,e,i)}handleAttributeChange(t,e,i){}#c(){this.#n.innerHTML="";const t=document.createElement("link");t.rel="stylesheet",t.href=new URL("./base.css",import.meta.url).href,this.#n.appendChild(t);const e=this.render();e&&this.#n.appendChild(e)}getBaseStylesheetUrl(){return new URL("./base.css",import.meta.url).href}addComponentStyles(t){const e=document.createElement("link");e.rel="stylesheet",e.href=t,this.#n.appendChild(e)}sanitizeValue(t){if(typeof t!="string")return"";const e=document.createElement("div");return e.textContent=t,e.innerHTML}validateInput(t,e={}){const i=[],s=this.#t;(e.required!==void 0?e.required:s.validation.required)&&(!t||t.trim().length===0)&&i.push("This field is required");const r=e.maxLength||s.validation.maxLength;t&&t.length>r&&i.push(`Value exceeds maximum length of ${r}`);const a=e.minLength||0;t&&t.length<a&&i.push(`Value must be at least ${a} characters`);const o=e.pattern||s.validation.pattern;return o&&t&&!o.test(t)&&i.push("Value does not match required format"),s.validation.strict&&i.length>0&&this.#a("validation_failed",{errors:i,valueLength:t?t.length:0}),{valid:i.length===0,errors:i}}checkRateLimit(){if(!this.#t.rateLimit.enabled)return{allowed:!0,retryAfter:0};const t=Date.now(),e=this.#t.rateLimit.windowMs;if(t-this.#e.windowStart>e&&(this.#e.attempts=0,this.#e.windowStart=t),this.#e.attempts>=this.#t.rateLimit.maxAttempts){const i=e-(t-this.#e.windowStart);return this.#a("rate_limit_exceeded",{attempts:this.#e.attempts,retryAfter:i}),{allowed:!1,retryAfter:i}}return this.#e.attempts++,{allowed:!0,retryAfter:0}}#a(t,e={}){const i=this.#t.audit;if(!(t.includes("access")&&i.logAccess||t.includes("change")&&i.logChanges||t.includes("submit")&&i.logSubmission||t.includes("initialized")||t.includes("rate_limit")||t.includes("validation")||t.includes("threat")))return;const n={event:t,tier:this.#i,timestamp:new Date().toISOString(),...e};i.includeMetadata&&(n.userAgent=navigator.userAgent,n.language=navigator.language),this.#s.length>=c.#l&&this.#s.shift(),this.#s.push(n),this.dispatchEvent(new CustomEvent("secure-audit",{detail:n,bubbles:!0,composed:!0}))}get shadowRoot(){return this.#n}get securityTier(){return this.#i}get config(){return this.#t}getAuditLog(){return[...this.#s]}clearAuditLog(){this.#s=[]}audit(t,e){this.#a(t,e)}detectInjection(t,e){for(const{id:i,pattern:s}of c.#h)if(s.test(t)){this.audit("threat_detected",{fieldName:e,patternId:i,threatType:"injection"}),this.dispatchEvent(new CustomEvent("secure-threat-detected",{detail:{fieldName:e,threatType:"injection",patternId:i,tier:this.securityTier,timestamp:Date.now()},bubbles:!0,composed:!0})),this.hasAttribute("threat-feedback")&&this.showThreatFeedback(i,this.securityTier);return}this.hasAttribute("threat-feedback")&&this.clearThreatFeedback()}showThreatFeedback(t,e){}clearThreatFeedback(){}getThreatLabel(t){return c.#u[t]??`Injection blocked: ${t}`}rerender(){this.#c()}recordTelemetryFocus(){const t=this.#r;t.focusAt=Date.now(),t.blurAt=null,t.focusCount++;const e=this.shadowRoot.querySelector('input:not([type="hidden"]), textarea, select');t.lastInputLength=e?e.value.length:0}recordTelemetryInput(t){const e=this.#r,i=Date.now();e.firstKeystrokeAt===null&&(e.firstKeystrokeAt=i);const n=t.inputType??"";n==="insertFromPaste"||n==="insertFromPasteAsQuotation"?e.pasteDetected=!0:n==="insertReplacementText"?e.autofillDetected=!0:n.startsWith("delete")||n==="historyUndo"||n==="historyRedo"?e.correctionCount++:e.keyCount++;const r=t.target;r&&(e.lastInputLength=r.value.length)}recordTelemetryBlur(){const t=this.#r;t.blurAt=Date.now();const e=this.shadowRoot.querySelector('input:not([type="hidden"]), textarea, select');(e?e.value.length:0)===t.lastInputLength&&t.keyCount===0&&!t.pasteDetected&&t.blurWithoutChange++}getFieldTelemetry(){const t=this.#r,e=t.focusAt??Date.now(),i=t.firstKeystrokeAt,s=t.blurAt??Date.now(),n=i!==null?i-e:0,r=i!==null?s-i:0,a=r/1e3,o=a>0?t.keyCount/a:0;return{dwell:n,completionTime:r,velocity:Math.round(o*100)/100,corrections:t.correctionCount,pasteDetected:t.pasteDetected,autofillDetected:t.autofillDetected,focusCount:t.focusCount,blurWithoutChange:t.blurWithoutChange}}#p(){this.#r={focusAt:null,firstKeystrokeAt:null,blurAt:null,keyCount:0,correctionCount:0,pasteDetected:!1,autofillDetected:!1,focusCount:0,blurWithoutChange:0,lastInputLength:0}}disconnectedCallback(){this.#e={attempts:0,windowStart:Date.now()},this.#p(),this.#t.audit.logAccess&&this.#a("component_disconnected",{timestamp:new Date().toISOString()})}}c=u;var f=u;export{u as SecureBaseComponent,f as default};