@socwarden/browser 1.0.0-alpha.1

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/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # @socwarden/browser
2
+
3
+ Lightweight browser SDK for SOCWarden client-side context collection. Under 3KB minified.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @socwarden/browser
9
+ ```
10
+
11
+ Or include via CDN (UMD):
12
+
13
+ ```html
14
+ <script src="https://cdn.jsdelivr.net/npm/@socwarden/browser/dist/socwarden.min.js"></script>
15
+ ```
16
+
17
+ ## Modes
18
+
19
+ ### Relay Mode (recommended)
20
+
21
+ Collects client context and attaches it as a `X-SOCWarden-Context` header on your app's own requests. Your server-side SDK (e.g. `@socwarden/laravel`) merges this into events automatically.
22
+
23
+ ```js
24
+ import { SOCWardenBrowser } from '@socwarden/browser';
25
+
26
+ const sw = new SOCWardenBrowser({ mode: 'relay' });
27
+ sw.installRelay();
28
+ // All subsequent fetch() and XMLHttpRequest calls include the context header.
29
+ ```
30
+
31
+ ### Direct Mode
32
+
33
+ Sends events directly to the SOCWarden ingestor. Useful for SPAs without a backend.
34
+
35
+ ```js
36
+ import { SOCWardenBrowser } from '@socwarden/browser';
37
+
38
+ const sw = new SOCWardenBrowser({
39
+ mode: 'direct',
40
+ apiKey: 'sw_live_xxxxxxxxxxxxxxxxxxxx',
41
+ endpoint: 'https://ingest.socwarden.io',
42
+ });
43
+
44
+ await sw.track('auth.login.success', { user_id: '42' });
45
+ ```
46
+
47
+ ## Collected Context
48
+
49
+ | Field | Source |
50
+ |-------|--------|
51
+ | `timezone` | `Intl.DateTimeFormat().resolvedOptions().timeZone` |
52
+ | `language` | `navigator.language` |
53
+ | `platform` | `navigator.platform` |
54
+ | `screen` | `screen.width` x `screen.height` |
55
+ | `viewport` | `window.innerWidth` x `window.innerHeight` |
56
+ | `color_depth` | `screen.colorDepth` |
57
+ | `cookie_enabled` | `navigator.cookieEnabled` |
58
+ | `do_not_track` | `navigator.doNotTrack === '1'` |
59
+ | `connection_type` | `navigator.connection?.effectiveType` |
60
+ | `downlink` | `navigator.connection?.downlink` |
61
+
62
+ ## Custom Header Name
63
+
64
+ ```js
65
+ const sw = new SOCWardenBrowser({
66
+ mode: 'relay',
67
+ headerName: 'X-My-Custom-Header',
68
+ });
69
+ ```
70
+
71
+ ## License
72
+
73
+ MIT
@@ -0,0 +1,76 @@
1
+ /**
2
+ * SOCWarden Browser SDK
3
+ *
4
+ * Lightweight client-side context collection (<3KB minified).
5
+ * Two modes:
6
+ * - relay: Collects context and injects it as X-SOCWarden-Context header
7
+ * on the app's own fetch/XHR requests. Server-side SDK merges it.
8
+ * - direct: Sends events straight to the SOCWarden ingestor (for SPAs).
9
+ */
10
+ interface SOCWardenBrowserConfig {
11
+ /** Operating mode. */
12
+ mode: 'relay' | 'direct';
13
+ /** API key — required for direct mode. */
14
+ apiKey?: string;
15
+ /** Ingestor endpoint URL — required for direct mode. */
16
+ endpoint?: string;
17
+ /** Header name used in relay mode. Default: X-SOCWarden-Context */
18
+ headerName?: string;
19
+ /**
20
+ * D5 FIX (GDPR): Controls how much device data is collected.
21
+ *
22
+ * - 'none' — collect nothing (SDK is effectively disabled for context).
23
+ * - 'basic' — collect only page URL, referrer, browser language, viewport size.
24
+ * No fingerprinting data. **Default.**
25
+ * - 'full' — collect all data including WebGL GPU renderer, hardware
26
+ * concurrency, device memory, and screen resolution.
27
+ * Only use with explicit user consent.
28
+ */
29
+ consentLevel?: 'none' | 'basic' | 'full';
30
+ }
31
+ interface ClientContext {
32
+ timezone: string;
33
+ language: string;
34
+ languages: string[];
35
+ touch: boolean;
36
+ platform: string;
37
+ screen: string;
38
+ viewport: string;
39
+ color_depth: number;
40
+ cookie_enabled: boolean;
41
+ do_not_track: boolean;
42
+ connection_type: string | undefined;
43
+ downlink: number | undefined;
44
+ gpu_renderer: string | undefined;
45
+ device_memory: number | undefined;
46
+ cpu_cores: number | undefined;
47
+ page_url: string;
48
+ page_referrer: string;
49
+ page_title: string;
50
+ }
51
+ declare class SOCWardenBrowser {
52
+ private readonly config;
53
+ private readonly headerName;
54
+ private ctx;
55
+ private encoded;
56
+ private _backedOff;
57
+ private _backoffTimer;
58
+ constructor(config: SOCWardenBrowserConfig);
59
+ /** Return the current client context snapshot (respects configured consentLevel). */
60
+ collectContext(): ClientContext;
61
+ /**
62
+ * Relay mode — monkey-patch `fetch` and `XMLHttpRequest` so that every
63
+ * outgoing request automatically carries the context header.
64
+ */
65
+ installRelay(): void;
66
+ /**
67
+ * Direct mode — POST an event to the SOCWarden ingestor.
68
+ * Never throws — errors are silently logged via console.warn.
69
+ *
70
+ * @param event Event type, e.g. "auth.login.success"
71
+ * @param metadata Arbitrary key-value metadata to attach
72
+ */
73
+ track(event: string, metadata?: Record<string, any>): Promise<void>;
74
+ }
75
+
76
+ export { type ClientContext, SOCWardenBrowser, type SOCWardenBrowserConfig, SOCWardenBrowser as default };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * SOCWarden Browser SDK
3
+ *
4
+ * Lightweight client-side context collection (<3KB minified).
5
+ * Two modes:
6
+ * - relay: Collects context and injects it as X-SOCWarden-Context header
7
+ * on the app's own fetch/XHR requests. Server-side SDK merges it.
8
+ * - direct: Sends events straight to the SOCWarden ingestor (for SPAs).
9
+ */
10
+ interface SOCWardenBrowserConfig {
11
+ /** Operating mode. */
12
+ mode: 'relay' | 'direct';
13
+ /** API key — required for direct mode. */
14
+ apiKey?: string;
15
+ /** Ingestor endpoint URL — required for direct mode. */
16
+ endpoint?: string;
17
+ /** Header name used in relay mode. Default: X-SOCWarden-Context */
18
+ headerName?: string;
19
+ /**
20
+ * D5 FIX (GDPR): Controls how much device data is collected.
21
+ *
22
+ * - 'none' — collect nothing (SDK is effectively disabled for context).
23
+ * - 'basic' — collect only page URL, referrer, browser language, viewport size.
24
+ * No fingerprinting data. **Default.**
25
+ * - 'full' — collect all data including WebGL GPU renderer, hardware
26
+ * concurrency, device memory, and screen resolution.
27
+ * Only use with explicit user consent.
28
+ */
29
+ consentLevel?: 'none' | 'basic' | 'full';
30
+ }
31
+ interface ClientContext {
32
+ timezone: string;
33
+ language: string;
34
+ languages: string[];
35
+ touch: boolean;
36
+ platform: string;
37
+ screen: string;
38
+ viewport: string;
39
+ color_depth: number;
40
+ cookie_enabled: boolean;
41
+ do_not_track: boolean;
42
+ connection_type: string | undefined;
43
+ downlink: number | undefined;
44
+ gpu_renderer: string | undefined;
45
+ device_memory: number | undefined;
46
+ cpu_cores: number | undefined;
47
+ page_url: string;
48
+ page_referrer: string;
49
+ page_title: string;
50
+ }
51
+ declare class SOCWardenBrowser {
52
+ private readonly config;
53
+ private readonly headerName;
54
+ private ctx;
55
+ private encoded;
56
+ private _backedOff;
57
+ private _backoffTimer;
58
+ constructor(config: SOCWardenBrowserConfig);
59
+ /** Return the current client context snapshot (respects configured consentLevel). */
60
+ collectContext(): ClientContext;
61
+ /**
62
+ * Relay mode — monkey-patch `fetch` and `XMLHttpRequest` so that every
63
+ * outgoing request automatically carries the context header.
64
+ */
65
+ installRelay(): void;
66
+ /**
67
+ * Direct mode — POST an event to the SOCWarden ingestor.
68
+ * Never throws — errors are silently logged via console.warn.
69
+ *
70
+ * @param event Event type, e.g. "auth.login.success"
71
+ * @param metadata Arbitrary key-value metadata to attach
72
+ */
73
+ track(event: string, metadata?: Record<string, any>): Promise<void>;
74
+ }
75
+
76
+ export { type ClientContext, SOCWardenBrowser, type SOCWardenBrowserConfig, SOCWardenBrowser as default };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";var u=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var h=Object.getOwnPropertyNames;var m=Object.prototype.hasOwnProperty;var _=(t,e)=>{for(var n in e)u(t,n,{get:e[n],enumerable:!0})},y=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of h(e))!m.call(t,o)&&o!==n&&u(t,o,{get:()=>e[o],enumerable:!(r=g(e,o))||r.enumerable});return t};var w=t=>y(u({},"__esModule",{value:!0}),t);var x={};_(x,{SOCWardenBrowser:()=>d,default:()=>k});module.exports=w(x);function v(){try{let t=document.createElement("canvas"),e=t.getContext("webgl")||t.getContext("experimental-webgl");if(e&&e instanceof WebGLRenderingContext){let n=e.getExtension("WEBGL_debug_renderer_info");if(n)return e.getParameter(n.UNMASKED_RENDERER_WEBGL)}}catch{}}var b=/^[a-z][a-z0-9]{0,29}(\.[a-z][a-z0-9_]{0,29}){1,3}$/;function C(t){try{let e=new URL(t),n=["token","key","password","secret","code","api_key","apikey","access_token","refresh_token"];for(let r of n)e.searchParams.has(r)&&e.searchParams.set(r,"[REDACTED]");return e.toString()}catch{return t}}function p(t="basic"){if(t==="none")return{timezone:"",language:"",languages:[],touch:!1,platform:"",screen:"",viewport:"",color_depth:0,cookie_enabled:!1,do_not_track:!1,connection_type:void 0,downlink:void 0,gpu_renderer:void 0,device_memory:void 0,cpu_cores:void 0,page_url:"",page_referrer:"",page_title:""};let e={timezone:Intl.DateTimeFormat().resolvedOptions().timeZone,language:navigator.language,languages:Array.from(navigator.languages||[navigator.language]),touch:!1,platform:"",screen:"",viewport:`${window.innerWidth}x${window.innerHeight}`,color_depth:0,cookie_enabled:!1,do_not_track:navigator.doNotTrack==="1",connection_type:void 0,downlink:void 0,gpu_renderer:void 0,device_memory:void 0,cpu_cores:void 0,page_url:C(location.href),page_referrer:document.referrer,page_title:document.title};return t!=="full"?e:{...e,touch:navigator.maxTouchPoints>0,platform:navigator.platform,screen:`${screen.width}x${screen.height}`,color_depth:screen.colorDepth,cookie_enabled:navigator.cookieEnabled,connection_type:navigator.connection?.effectiveType,downlink:navigator.connection?.downlink,gpu_renderer:v(),device_memory:navigator.deviceMemory,cpu_cores:navigator.hardwareConcurrency}}function f(t){return btoa(JSON.stringify(t))}var d=class{constructor(e){this._backedOff=!1;this._backoffTimer=null;if(e.mode==="direct"){if(!e.apiKey)throw new Error("SOCWarden: apiKey is required in direct mode");if(!e.endpoint)throw new Error("SOCWarden: endpoint is required in direct mode");if(e.endpoint&&!e.endpoint.startsWith("https://")){if(!(e.endpoint.startsWith("http://localhost")||e.endpoint.startsWith("http://127.0.0.1")))throw new Error("SOCWarden: endpoint must use HTTPS. API keys must not be transmitted in cleartext.");console.warn("[SOCWarden] WARNING: Endpoint is using HTTP. API keys will be transmitted in cleartext.")}}this.config=e,this.headerName=e.headerName??"X-SOCWarden-Context",this.ctx=p(e.consentLevel??"basic"),this.encoded=f(this.ctx)}collectContext(){return this.ctx=p(this.config.consentLevel??"basic"),this.encoded=f(this.ctx),this.ctx}installRelay(){let e=this.headerName,n=()=>this.encoded,r=window.fetch;window.fetch=function(c,a){a=a??{};let l=new Headers(a.headers);return l.has(e)||l.set(e,n()),a.headers=l,r.call(window,c,a)};let o=XMLHttpRequest.prototype.open,i=XMLHttpRequest.prototype.send;XMLHttpRequest.prototype.open=function(...s){return this.__socwardenPatched=!1,o.apply(this,s)},XMLHttpRequest.prototype.send=function(...s){if(!this.__socwardenPatched){try{this.setRequestHeader(e,n())}catch{}this.__socwardenPatched=!0}return i.apply(this,s)}}async track(e,n){if(this.config.mode!=="direct"){console.warn("SOCWarden: track() is only available in direct mode");return}if(!b.test(e)){console.warn(`[SOCWarden] Invalid event type format, dropping event: "${e}". Event types must match ^[a-z][a-z0-9]{0,29}(\\.[a-z][a-z0-9_]{0,29}){1,3}$`);return}if(this._backedOff){console.warn("SOCWarden: rate-limited, skipping event");return}try{let r=this.config.endpoint.replace(/\/+$/,""),o={event:e,source:"browser",metadata:n??{},context:this.ctx,timestamp:new Date().toISOString()},i=await fetch(`${r}/v1/events`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify(o),keepalive:!0});if(i.status===429){this._backedOff=!0;let s=i.headers?.get?.("Retry-After"),c=60;if(s){let a=parseInt(s,10);!isNaN(a)&&a>0&&(c=Math.min(a,300))}this._backoffTimer=setTimeout(()=>{this._backedOff=!1,this._backoffTimer=null},c*1e3),console.warn(`SOCWarden: rate-limited, backing off for ${c}s`);return}if(!i.ok&&i.status!==202){console.warn(`SOCWarden: ingestor responded with ${i.status}`);return}}catch(r){console.warn("SOCWarden: failed to send event",r);return}}},k=d;0&&(module.exports={SOCWardenBrowser});
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ function f(){try{let t=document.createElement("canvas"),e=t.getContext("webgl")||t.getContext("experimental-webgl");if(e&&e instanceof WebGLRenderingContext){let n=e.getExtension("WEBGL_debug_renderer_info");if(n)return e.getParameter(n.UNMASKED_RENDERER_WEBGL)}}catch{}}var g=/^[a-z][a-z0-9]{0,29}(\.[a-z][a-z0-9_]{0,29}){1,3}$/;function h(t){try{let e=new URL(t),n=["token","key","password","secret","code","api_key","apikey","access_token","refresh_token"];for(let o of n)e.searchParams.has(o)&&e.searchParams.set(o,"[REDACTED]");return e.toString()}catch{return t}}function u(t="basic"){if(t==="none")return{timezone:"",language:"",languages:[],touch:!1,platform:"",screen:"",viewport:"",color_depth:0,cookie_enabled:!1,do_not_track:!1,connection_type:void 0,downlink:void 0,gpu_renderer:void 0,device_memory:void 0,cpu_cores:void 0,page_url:"",page_referrer:"",page_title:""};let e={timezone:Intl.DateTimeFormat().resolvedOptions().timeZone,language:navigator.language,languages:Array.from(navigator.languages||[navigator.language]),touch:!1,platform:"",screen:"",viewport:`${window.innerWidth}x${window.innerHeight}`,color_depth:0,cookie_enabled:!1,do_not_track:navigator.doNotTrack==="1",connection_type:void 0,downlink:void 0,gpu_renderer:void 0,device_memory:void 0,cpu_cores:void 0,page_url:h(location.href),page_referrer:document.referrer,page_title:document.title};return t!=="full"?e:{...e,touch:navigator.maxTouchPoints>0,platform:navigator.platform,screen:`${screen.width}x${screen.height}`,color_depth:screen.colorDepth,cookie_enabled:navigator.cookieEnabled,connection_type:navigator.connection?.effectiveType,downlink:navigator.connection?.downlink,gpu_renderer:f(),device_memory:navigator.deviceMemory,cpu_cores:navigator.hardwareConcurrency}}function p(t){return btoa(JSON.stringify(t))}var l=class{constructor(e){this._backedOff=!1;this._backoffTimer=null;if(e.mode==="direct"){if(!e.apiKey)throw new Error("SOCWarden: apiKey is required in direct mode");if(!e.endpoint)throw new Error("SOCWarden: endpoint is required in direct mode");if(e.endpoint&&!e.endpoint.startsWith("https://")){if(!(e.endpoint.startsWith("http://localhost")||e.endpoint.startsWith("http://127.0.0.1")))throw new Error("SOCWarden: endpoint must use HTTPS. API keys must not be transmitted in cleartext.");console.warn("[SOCWarden] WARNING: Endpoint is using HTTP. API keys will be transmitted in cleartext.")}}this.config=e,this.headerName=e.headerName??"X-SOCWarden-Context",this.ctx=u(e.consentLevel??"basic"),this.encoded=p(this.ctx)}collectContext(){return this.ctx=u(this.config.consentLevel??"basic"),this.encoded=p(this.ctx),this.ctx}installRelay(){let e=this.headerName,n=()=>this.encoded,o=window.fetch;window.fetch=function(s,r){r=r??{};let d=new Headers(r.headers);return d.has(e)||d.set(e,n()),r.headers=d,o.call(window,s,r)};let c=XMLHttpRequest.prototype.open,a=XMLHttpRequest.prototype.send;XMLHttpRequest.prototype.open=function(...i){return this.__socwardenPatched=!1,c.apply(this,i)},XMLHttpRequest.prototype.send=function(...i){if(!this.__socwardenPatched){try{this.setRequestHeader(e,n())}catch{}this.__socwardenPatched=!0}return a.apply(this,i)}}async track(e,n){if(this.config.mode!=="direct"){console.warn("SOCWarden: track() is only available in direct mode");return}if(!g.test(e)){console.warn(`[SOCWarden] Invalid event type format, dropping event: "${e}". Event types must match ^[a-z][a-z0-9]{0,29}(\\.[a-z][a-z0-9_]{0,29}){1,3}$`);return}if(this._backedOff){console.warn("SOCWarden: rate-limited, skipping event");return}try{let o=this.config.endpoint.replace(/\/+$/,""),c={event:e,source:"browser",metadata:n??{},context:this.ctx,timestamp:new Date().toISOString()},a=await fetch(`${o}/v1/events`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify(c),keepalive:!0});if(a.status===429){this._backedOff=!0;let i=a.headers?.get?.("Retry-After"),s=60;if(i){let r=parseInt(i,10);!isNaN(r)&&r>0&&(s=Math.min(r,300))}this._backoffTimer=setTimeout(()=>{this._backedOff=!1,this._backoffTimer=null},s*1e3),console.warn(`SOCWarden: rate-limited, backing off for ${s}s`);return}if(!a.ok&&a.status!==202){console.warn(`SOCWarden: ingestor responded with ${a.status}`);return}}catch(o){console.warn("SOCWarden: failed to send event",o);return}}},m=l;export{l as SOCWardenBrowser,m as default};
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@socwarden/browser",
3
+ "version": "1.0.0-alpha.1",
4
+ "description": "Lightweight browser SDK for SOCWarden client-side context collection",
5
+ "main": "dist/socwarden.min.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist/"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsup src/index.ts --format cjs,esm --minify --dts --global-name SOCWarden",
13
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
14
+ "test": "npx tsx --test src/__tests__/*.test.ts"
15
+ },
16
+ "keywords": [
17
+ "socwarden",
18
+ "security",
19
+ "observability",
20
+ "browser",
21
+ "sdk"
22
+ ],
23
+ "author": "SOCWarden",
24
+ "license": "MIT",
25
+ "devDependencies": {
26
+ "tsup": "^8.0.0",
27
+ "tsx": "^4.21.0",
28
+ "typescript": "^5.4.0"
29
+ }
30
+ }