secure-ui-components 0.2.2 → 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.
Files changed (28) hide show
  1. package/dist/components/secure-card/secure-card.js +1 -766
  2. package/dist/components/secure-datetime/secure-datetime.js +1 -570
  3. package/dist/components/secure-file-upload/secure-file-upload.js +1 -868
  4. package/dist/components/secure-form/secure-form.js +1 -797
  5. package/dist/components/secure-input/secure-input.css +67 -1
  6. package/dist/components/secure-input/secure-input.d.ts +14 -0
  7. package/dist/components/secure-input/secure-input.d.ts.map +1 -1
  8. package/dist/components/secure-input/secure-input.js +1 -805
  9. package/dist/components/secure-input/secure-input.js.map +1 -1
  10. package/dist/components/secure-password-confirm/secure-password-confirm.js +1 -329
  11. package/dist/components/secure-select/secure-select.js +1 -589
  12. package/dist/components/secure-submit-button/secure-submit-button.js +1 -378
  13. package/dist/components/secure-table/secure-table.js +33 -528
  14. package/dist/components/secure-telemetry-provider/secure-telemetry-provider.js +1 -201
  15. package/dist/components/secure-textarea/secure-textarea.css +66 -1
  16. package/dist/components/secure-textarea/secure-textarea.d.ts +11 -0
  17. package/dist/components/secure-textarea/secure-textarea.d.ts.map +1 -1
  18. package/dist/components/secure-textarea/secure-textarea.js +1 -436
  19. package/dist/components/secure-textarea/secure-textarea.js.map +1 -1
  20. package/dist/core/base-component.d.ts +18 -0
  21. package/dist/core/base-component.d.ts.map +1 -1
  22. package/dist/core/base-component.js +1 -455
  23. package/dist/core/base-component.js.map +1 -1
  24. package/dist/core/security-config.js +1 -242
  25. package/dist/core/types.js +0 -2
  26. package/dist/index.js +1 -17
  27. package/dist/package.json +4 -2
  28. package/package.json +4 -2
@@ -35,204 +35,4 @@
35
35
  *
36
36
  * @module secure-telemetry-provider
37
37
  * @license MIT
38
- */
39
- // ── Component ─────────────────────────────────────────────────────────────────
40
- export class SecureTelemetryProvider extends HTMLElement {
41
- static get observedAttributes() {
42
- return ['signing-key'];
43
- }
44
- #state = {
45
- mouseMovementDetected: false,
46
- keyboardActivityDetected: false,
47
- injectedScriptCount: 0,
48
- domMutationDetected: false,
49
- pointerType: 'none',
50
- firstKeystrokeAt: -1,
51
- threatSignals: [],
52
- };
53
- /** performance.now() recorded at connectedCallback — baseline for timing signals */
54
- #connectedAt = 0;
55
- #mutationObserver = null;
56
- #knownScripts = new Set();
57
- /**
58
- * Cached CryptoKey derived from the signing-key attribute.
59
- * Imported once on first use; invalidated when the attribute changes.
60
- * Avoids re-importing the key on every form submission.
61
- */
62
- #cryptoKey = null;
63
- /** The raw key string that #cryptoKey was derived from. Used to detect stale cache. */
64
- #cryptoKeySource = '';
65
- // Bound listener references so we can cleanly remove them
66
- #onMouseMove = () => { this.#state.mouseMovementDetected = true; };
67
- #onKeydown = () => {
68
- this.#state.keyboardActivityDetected = true;
69
- if (this.#state.firstKeystrokeAt < 0) {
70
- this.#state.firstKeystrokeAt = performance.now();
71
- }
72
- };
73
- #onPointerDown = (e) => {
74
- this.#state.pointerType = e.pointerType;
75
- };
76
- #onFormSubmit = (e) => { void this.#handleFormSubmit(e); };
77
- #onThreatDetected = (e) => {
78
- this.#state.threatSignals.push(e.detail);
79
- };
80
- connectedCallback() {
81
- this.#connectedAt = performance.now();
82
- // Snapshot existing scripts so we can detect later injections
83
- document.querySelectorAll('script').forEach(s => this.#knownScripts.add(s));
84
- this.#startMutationObserver();
85
- this.#attachListeners();
86
- }
87
- disconnectedCallback() {
88
- this.#mutationObserver?.disconnect();
89
- this.#mutationObserver = null;
90
- this.#removeListeners();
91
- this.#cryptoKey = null;
92
- this.#cryptoKeySource = '';
93
- }
94
- attributeChangedCallback(name, _oldValue, newValue) {
95
- if (name === 'signing-key' && newValue !== this.#cryptoKeySource) {
96
- // Invalidate the cached key so it is re-imported on the next sign() call
97
- this.#cryptoKey = null;
98
- this.#cryptoKeySource = '';
99
- }
100
- }
101
- // ── Mutation observer: detect script injection ──────────────────────────────
102
- #startMutationObserver() {
103
- this.#mutationObserver = new MutationObserver((mutations) => {
104
- for (const mutation of mutations) {
105
- if (mutation.type !== 'childList')
106
- continue;
107
- for (const node of Array.from(mutation.addedNodes)) {
108
- if (node.tagName === 'SCRIPT' && !this.#knownScripts.has(node)) {
109
- this.#state.injectedScriptCount++;
110
- this.#state.domMutationDetected = true;
111
- this.#knownScripts.add(node);
112
- }
113
- }
114
- }
115
- });
116
- this.#mutationObserver.observe(document.documentElement, {
117
- childList: true,
118
- subtree: true,
119
- });
120
- }
121
- // ── DOM event listeners ─────────────────────────────────────────────────────
122
- #attachListeners() {
123
- document.addEventListener('mousemove', this.#onMouseMove, { passive: true });
124
- document.addEventListener('keydown', this.#onKeydown, { passive: true });
125
- document.addEventListener('pointerdown', this.#onPointerDown, { passive: true });
126
- this.addEventListener('secure-form-submit', this.#onFormSubmit);
127
- this.addEventListener('secure-threat-detected', this.#onThreatDetected);
128
- }
129
- #removeListeners() {
130
- document.removeEventListener('mousemove', this.#onMouseMove);
131
- document.removeEventListener('keydown', this.#onKeydown);
132
- document.removeEventListener('pointerdown', this.#onPointerDown);
133
- this.removeEventListener('secure-form-submit', this.#onFormSubmit);
134
- this.removeEventListener('secure-threat-detected', this.#onThreatDetected);
135
- }
136
- // ── Signal collection ───────────────────────────────────────────────────────
137
- /**
138
- * Collect a point-in-time snapshot of all environmental signals.
139
- */
140
- collectSignals() {
141
- const nav = navigator;
142
- const webdriverDetected = nav['webdriver'] === true ||
143
- Object.prototype.hasOwnProperty.call(nav, 'webdriver');
144
- // Headless Chrome leaves traces in userAgent and missing APIs
145
- const ua = navigator.userAgent;
146
- const headlessDetected = ua.includes('HeadlessChrome') ||
147
- ua.includes('Headless') ||
148
- (typeof window['chrome'] === 'undefined' &&
149
- ua.includes('Chrome'));
150
- const suspiciousScreenSize = screen.width === 0 ||
151
- screen.height === 0 ||
152
- (screen.width < 100 && screen.height < 100);
153
- const now = performance.now();
154
- const pageLoadToFirstKeystroke = this.#state.firstKeystrokeAt >= 0
155
- ? Math.round(this.#state.firstKeystrokeAt - this.#connectedAt)
156
- : -1;
157
- const loadToSubmit = Math.round(now - this.#connectedAt);
158
- return {
159
- webdriverDetected,
160
- headlessDetected,
161
- domMutationDetected: this.#state.domMutationDetected,
162
- injectedScriptCount: this.#state.injectedScriptCount,
163
- suspiciousScreenSize,
164
- pointerType: this.#state.pointerType,
165
- mouseMovementDetected: this.#state.mouseMovementDetected,
166
- keyboardActivityDetected: this.#state.keyboardActivityDetected,
167
- pageLoadToFirstKeystroke,
168
- loadToSubmit,
169
- threatSignals: this.#state.threatSignals.length > 0
170
- ? [...this.#state.threatSignals]
171
- : undefined,
172
- };
173
- }
174
- // ── Signing ─────────────────────────────────────────────────────────────────
175
- /**
176
- * Generate a signed envelope using HMAC-SHA-256 via SubtleCrypto.
177
- * Falls back to an unsigned envelope if SubtleCrypto is unavailable
178
- * (e.g., non-secure context in tests) — the server should treat
179
- * unsigned envelopes with reduced trust.
180
- */
181
- async sign(signals) {
182
- const nonce = this.#generateNonce();
183
- const issuedAt = new Date().toISOString();
184
- const signingKey = this.getAttribute('signing-key') ?? '';
185
- const payload = `${nonce}.${issuedAt}.${JSON.stringify(signals)}`;
186
- const signature = await this.#hmacSha256(signingKey, payload);
187
- return { nonce, issuedAt, environment: signals, signature };
188
- }
189
- #generateNonce() {
190
- const bytes = new Uint8Array(16);
191
- crypto.getRandomValues(bytes);
192
- return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
193
- }
194
- async #hmacSha256(key, data) {
195
- if (!crypto.subtle) {
196
- // Non-secure context (HTTP in dev) — return empty signature
197
- return '';
198
- }
199
- const enc = new TextEncoder();
200
- // Import and cache the CryptoKey. Re-import only when the key string changes.
201
- if (this.#cryptoKey === null || this.#cryptoKeySource !== key) {
202
- this.#cryptoKey = await crypto.subtle.importKey('raw', enc.encode(key), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
203
- this.#cryptoKeySource = key;
204
- }
205
- const signatureBuffer = await crypto.subtle.sign('HMAC', this.#cryptoKey, enc.encode(data));
206
- return Array.from(new Uint8Array(signatureBuffer), b => b.toString(16).padStart(2, '0')).join('');
207
- }
208
- // ── Form submit interception ─────────────────────────────────────────────────
209
- async #handleFormSubmit(event) {
210
- const detail = event.detail;
211
- if (!detail?.telemetry)
212
- return;
213
- try {
214
- const signals = this.collectSignals();
215
- const envelope = await this.sign(signals);
216
- // Attach the signed envelope directly onto the telemetry object.
217
- // Because both this handler and downstream listeners receive the same
218
- // detail object reference, waiting listeners that check after an async
219
- // tick will see the enriched value.
220
- detail.telemetry._env = envelope;
221
- }
222
- catch {
223
- // Signing failure (e.g. non-secure context) must not block form submission.
224
- // The server should treat a missing _env as an unsigned, lower-trust submission.
225
- }
226
- }
227
- // ── Public API ────────────────────────────────────────────────────────────────
228
- /**
229
- * Get the current environmental signals without triggering a form submit.
230
- * Useful for pre-flight checks or progressive disclosure flows.
231
- */
232
- getEnvironmentalSignals() {
233
- return this.collectSignals();
234
- }
235
- }
236
- customElements.define('secure-telemetry-provider', SecureTelemetryProvider);
237
- export default SecureTelemetryProvider;
238
- //# sourceMappingURL=secure-telemetry-provider.js.map
38
+ */class o extends HTMLElement{static get observedAttributes(){return["signing-key"]}#e={mouseMovementDetected:!1,keyboardActivityDetected:!1,injectedScriptCount:0,domMutationDetected:!1,pointerType:"none",firstKeystrokeAt:-1,threatSignals:[]};#i=0;#s=null;#r=new Set;#t=null;#n="";#o=()=>{this.#e.mouseMovementDetected=!0};#c=()=>{this.#e.keyboardActivityDetected=!0,this.#e.firstKeystrokeAt<0&&(this.#e.firstKeystrokeAt=performance.now())};#a=e=>{this.#e.pointerType=e.pointerType};#d=e=>{this.#p(e)};#h=e=>{this.#e.threatSignals.push(e.detail)};connectedCallback(){this.#i=performance.now(),document.querySelectorAll("script").forEach(e=>this.#r.add(e)),this.#u(),this.#l()}disconnectedCallback(){this.#s?.disconnect(),this.#s=null,this.#m(),this.#t=null,this.#n=""}attributeChangedCallback(e,n,t){e==="signing-key"&&t!==this.#n&&(this.#t=null,this.#n="")}#u(){this.#s=new MutationObserver(e=>{for(const n of e)if(n.type==="childList")for(const t of Array.from(n.addedNodes))t.tagName==="SCRIPT"&&!this.#r.has(t)&&(this.#e.injectedScriptCount++,this.#e.domMutationDetected=!0,this.#r.add(t))}),this.#s.observe(document.documentElement,{childList:!0,subtree:!0})}#l(){document.addEventListener("mousemove",this.#o,{passive:!0}),document.addEventListener("keydown",this.#c,{passive:!0}),document.addEventListener("pointerdown",this.#a,{passive:!0}),this.addEventListener("secure-form-submit",this.#d),this.addEventListener("secure-threat-detected",this.#h)}#m(){document.removeEventListener("mousemove",this.#o),document.removeEventListener("keydown",this.#c),document.removeEventListener("pointerdown",this.#a),this.removeEventListener("secure-form-submit",this.#d),this.removeEventListener("secure-threat-detected",this.#h)}collectSignals(){const e=navigator,n=e.webdriver===!0||Object.prototype.hasOwnProperty.call(e,"webdriver"),t=navigator.userAgent,s=t.includes("HeadlessChrome")||t.includes("Headless")||typeof window.chrome>"u"&&t.includes("Chrome"),i=screen.width===0||screen.height===0||screen.width<100&&screen.height<100,r=performance.now(),c=this.#e.firstKeystrokeAt>=0?Math.round(this.#e.firstKeystrokeAt-this.#i):-1,a=Math.round(r-this.#i);return{webdriverDetected:n,headlessDetected:s,domMutationDetected:this.#e.domMutationDetected,injectedScriptCount:this.#e.injectedScriptCount,suspiciousScreenSize:i,pointerType:this.#e.pointerType,mouseMovementDetected:this.#e.mouseMovementDetected,keyboardActivityDetected:this.#e.keyboardActivityDetected,pageLoadToFirstKeystroke:c,loadToSubmit:a,threatSignals:this.#e.threatSignals.length>0?[...this.#e.threatSignals]:void 0}}async sign(e){const n=this.#y(),t=new Date().toISOString(),s=this.getAttribute("signing-key")??"",i=`${n}.${t}.${JSON.stringify(e)}`,r=await this.#v(s,i);return{nonce:n,issuedAt:t,environment:e,signature:r}}#y(){const e=new Uint8Array(16);return crypto.getRandomValues(e),Array.from(e,n=>n.toString(16).padStart(2,"0")).join("")}async#v(e,n){if(!crypto.subtle)return"";const t=new TextEncoder;(this.#t===null||this.#n!==e)&&(this.#t=await crypto.subtle.importKey("raw",t.encode(e),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),this.#n=e);const s=await crypto.subtle.sign("HMAC",this.#t,t.encode(n));return Array.from(new Uint8Array(s),i=>i.toString(16).padStart(2,"0")).join("")}async#p(e){const n=e.detail;if(n?.telemetry)try{const t=this.collectSignals(),s=await this.sign(t);n.telemetry._env=s}catch{}}getEnvironmentalSignals(){return this.collectSignals()}}customElements.define("secure-telemetry-provider",o);var h=o;export{o as SecureTelemetryProvider,h as default};
@@ -134,11 +134,76 @@ label {
134
134
 
135
135
  @media (prefers-reduced-motion: reduce) {
136
136
  .textarea-field,
137
- .error-container {
137
+ .error-container,
138
+ .threat-container {
138
139
  transition: none !important;
139
140
  }
140
141
  }
141
142
 
143
+ /* Threat Feedback Container */
144
+ .threat-container {
145
+ margin-top: var(--secure-ui-form-error-margin-top);
146
+ font-size: var(--secure-ui-error-font-size);
147
+ color: var(--secure-ui-color-warning);
148
+ line-height: var(--secure-ui-line-height-normal);
149
+ overflow: hidden;
150
+ max-height: 40px;
151
+ opacity: 1;
152
+ transform: translateY(0);
153
+ transition: opacity 0.2s ease-out, transform 0.2s ease-out, max-height 0.2s ease-out, margin-top 0.2s ease-out;
154
+ display: flex;
155
+ align-items: center;
156
+ gap: var(--secure-ui-space-1, 4px);
157
+ flex-wrap: wrap;
158
+ }
159
+
160
+ .threat-container.hidden {
161
+ max-height: 0;
162
+ opacity: 0;
163
+ transform: translateY(-4px);
164
+ margin-top: 0;
165
+ }
166
+
167
+ .threat-message {
168
+ flex: 1;
169
+ min-width: 0;
170
+ }
171
+
172
+ .threat-badge {
173
+ display: inline-block;
174
+ padding: var(--secure-ui-badge-padding);
175
+ font-size: var(--secure-ui-badge-font-size);
176
+ font-weight: var(--secure-ui-font-weight-semibold);
177
+ border-radius: var(--secure-ui-badge-border-radius);
178
+ background-color: color-mix(in srgb, var(--secure-ui-color-warning) 12%, transparent);
179
+ color: var(--secure-ui-color-warning);
180
+ border: var(--secure-ui-border-width-thin) solid currentColor;
181
+ font-family: var(--secure-ui-font-family-mono, monospace);
182
+ white-space: nowrap;
183
+ }
184
+
185
+ .threat-tier {
186
+ display: inline-block;
187
+ padding: var(--secure-ui-badge-padding);
188
+ font-size: var(--secure-ui-badge-font-size);
189
+ font-weight: var(--secure-ui-font-weight-semibold);
190
+ border-radius: var(--secure-ui-badge-border-radius);
191
+ background-color: var(--secure-ui-color-bg-tertiary);
192
+ color: var(--secure-ui-color-text-secondary);
193
+ border: var(--secure-ui-border-width-thin) solid var(--secure-ui-color-border);
194
+ text-transform: uppercase;
195
+ white-space: nowrap;
196
+ }
197
+
198
+ /* Threat state on the textarea field */
199
+ .textarea-field.threat {
200
+ border-color: var(--secure-ui-color-warning) !important;
201
+ }
202
+
203
+ .textarea-field.threat:focus {
204
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--secure-ui-color-warning) 25%, transparent);
205
+ }
206
+
142
207
  /* Security Tier Styles */
143
208
  :host([security-tier="authenticated"]) .textarea-field {
144
209
  border-color: var(--secure-ui-tier-authenticated);
@@ -29,6 +29,7 @@
29
29
  * @license MIT
30
30
  */
31
31
  import { SecureBaseComponent } from '../../core/base-component.js';
32
+ import type { SecurityTierValue } from '../../core/types.js';
32
33
  /**
33
34
  * Secure Textarea Web Component
34
35
  *
@@ -105,6 +106,16 @@ export declare class SecureTextarea extends SecureBaseComponent {
105
106
  /**
106
107
  * Cleanup on disconnect
107
108
  */
109
+ /**
110
+ * Show inline threat feedback inside the component's shadow DOM.
111
+ * @protected
112
+ */
113
+ protected showThreatFeedback(patternId: string, tier: SecurityTierValue): void;
114
+ /**
115
+ * Clear the threat feedback container.
116
+ * @protected
117
+ */
118
+ protected clearThreatFeedback(): void;
108
119
  disconnectedCallback(): void;
109
120
  }
110
121
  export default SecureTextarea;
@@ -1 +1 @@
1
- {"version":3,"file":"secure-textarea.d.ts","sourceRoot":"","sources":["../../../src/components/secure-textarea/secure-textarea.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAEnE;;;;;;;;GAQG;AACH,qBAAa,cAAe,SAAQ,mBAAmB;;IA+BrD;;;;OAIG;IACH,MAAM,KAAK,kBAAkB,IAAI,MAAM,EAAE,CAcxC;IAED;;OAEG;;IAKH;;;;;;;;OAQG;IACH,SAAS,CAAC,MAAM,IAAI,gBAAgB,GAAG,WAAW,GAAG,IAAI;IAwTzD;;;;OAIG;IACH,SAAS,CAAC,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAmBtG;;;;OAIG;IACH,IAAI,KAAK,IAAI,MAAM,CAElB;IAED;;;;OAIG;IACH,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,EAKtB;IAED;;;;OAIG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;;OAIG;IACH,IAAI,KAAK,IAAI,OAAO,CAWnB;IAED;;;;OAIG;IACH,KAAK,IAAI,IAAI;IAMb;;;;OAIG;IACH,IAAI,IAAI,IAAI;IAMZ;;OAEG;IACH,oBAAoB,IAAI,IAAI;CAQ7B;AAKD,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"secure-textarea.d.ts","sourceRoot":"","sources":["../../../src/components/secure-textarea/secure-textarea.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7D;;;;;;;;GAQG;AACH,qBAAa,cAAe,SAAQ,mBAAmB;;IAsCrD;;;;OAIG;IACH,MAAM,KAAK,kBAAkB,IAAI,MAAM,EAAE,CAcxC;IAED;;OAEG;;IAKH;;;;;;;;OAQG;IACH,SAAS,CAAC,MAAM,IAAI,gBAAgB,GAAG,WAAW,GAAG,IAAI;IAiUzD;;;;OAIG;IACH,SAAS,CAAC,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAmBtG;;;;OAIG;IACH,IAAI,KAAK,IAAI,MAAM,CAElB;IAED;;;;OAIG;IACH,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,EAKtB;IAED;;;;OAIG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;;OAIG;IACH,IAAI,KAAK,IAAI,OAAO,CAWnB;IAED;;;;OAIG;IACH,KAAK,IAAI,IAAI;IAMb;;;;OAIG;IACH,IAAI,IAAI,IAAI;IAMZ;;OAEG;IACH;;;OAGG;cACgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,IAAI;IA2BvF;;;OAGG;cACgB,mBAAmB,IAAI,IAAI;IAY9C,oBAAoB,IAAI,IAAI;CAQ7B;AAKD,eAAe,cAAc,CAAC"}