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.
@@ -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};