secure-ui-components 0.2.3 → 0.2.4
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
|
@@ -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};
|