securenow 8.0.3 → 8.2.0

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/events.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ export interface TrackProps {
2
+ /** End-user / account identifier (durable identity for correlation). */
3
+ userId?: string | number;
4
+ /** Session identifier (finer-grained than userId). */
5
+ sessionId?: string | number;
6
+ /** End-user client IP as seen by your app (enriched to geo/ASN server-side). */
7
+ ip?: string;
8
+ /** End-user agent string. */
9
+ userAgent?: string;
10
+ /** Severity hint. Defaults to "info". */
11
+ severity?: "trace" | "debug" | "info" | "warn" | "error" | "fatal";
12
+ /** Event time in epoch ms. Defaults to now. */
13
+ ts?: number;
14
+ /** Arbitrary structured attributes (string/number/boolean values). */
15
+ attributes?: Record<string, string | number | boolean>;
16
+ }
17
+
18
+ /**
19
+ * Emit a custom security event to SecureNow. Fire-and-forget: returns
20
+ * immediately, batches in the background, and never throws into your app.
21
+ *
22
+ * @example
23
+ * import { track } from "securenow/events";
24
+ * track("auth.login.success", { userId, sessionId, ip, attributes: { method: "magic_link" } });
25
+ */
26
+ export function track(type: string, props?: TrackProps): void;
27
+
28
+ /** Force a flush of any buffered events. Best-effort; never throws. */
29
+ export function flush(): Promise<unknown>;
package/events.js ADDED
@@ -0,0 +1,160 @@
1
+ 'use strict';
2
+
3
+ // SecureNow custom events — a tiny, batched, fire-and-forget emitter.
4
+ //
5
+ // const { track } = require('securenow/events');
6
+ // track('auth.login.success', { userId, sessionId, ip, attributes: { method: 'magic_link' } });
7
+ //
8
+ // Design goals:
9
+ // - Never throw into the caller. A SecureNow/network outage must not affect
10
+ // the app's request path. Events are advisory.
11
+ // - Non-blocking: events are buffered and flushed asynchronously in batches.
12
+ // - Bounded memory: the buffer is capped; oldest events drop on overflow.
13
+ // - Reuses the SDK's resolved runtime config (app key + auth headers +
14
+ // endpoint), so there is nothing extra to configure.
15
+
16
+ const appConfig = require('./app-config');
17
+
18
+ const MAX_BUFFER = 1000; // hard cap on queued events
19
+ const MAX_BATCH = 100; // events sent per flush
20
+ const FLUSH_INTERVAL_MS = 2000;
21
+ const POST_TIMEOUT_MS = 5000;
22
+
23
+ let buffer = [];
24
+ let timer = null;
25
+ let target = null;
26
+ let targetResolved = false;
27
+
28
+ function resolveTarget() {
29
+ if (targetResolved) return target;
30
+ targetResolved = true;
31
+ try {
32
+ const endpoints = appConfig.resolveEndpoints();
33
+ const base = String((endpoints && endpoints.endpointBase) || '').replace(/\/$/, '');
34
+ if (!base) {
35
+ target = null;
36
+ return null;
37
+ }
38
+ const headers = Object.assign({ 'Content-Type': 'application/json' }, (endpoints && endpoints.headers) || {});
39
+ // Without an app key + auth we can't attribute events; stay silent.
40
+ const hasAppKey = headers['x-securenow-app-key'] || headers['X-SecureNow-App-Key'];
41
+ if (!hasAppKey) {
42
+ target = null;
43
+ return null;
44
+ }
45
+ target = { url: `${base}/v1/events`, headers };
46
+ } catch {
47
+ target = null;
48
+ }
49
+ return target;
50
+ }
51
+
52
+ function post(t, payload) {
53
+ return new Promise((resolve) => {
54
+ let parsed;
55
+ let lib;
56
+ try {
57
+ parsed = new (require('url').URL)(t.url);
58
+ lib = parsed.protocol === 'https:' ? require('https') : require('http');
59
+ } catch {
60
+ return resolve(false);
61
+ }
62
+ let body;
63
+ try {
64
+ body = Buffer.from(JSON.stringify(payload));
65
+ } catch {
66
+ return resolve(false);
67
+ }
68
+ const req = lib.request(
69
+ {
70
+ method: 'POST',
71
+ hostname: parsed.hostname,
72
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
73
+ path: parsed.pathname + parsed.search,
74
+ headers: Object.assign({ 'Content-Length': body.length }, t.headers),
75
+ timeout: POST_TIMEOUT_MS,
76
+ },
77
+ (res) => {
78
+ res.on('data', () => {});
79
+ res.on('end', () => resolve(res.statusCode >= 200 && res.statusCode < 300));
80
+ }
81
+ );
82
+ req.on('error', () => resolve(false));
83
+ req.on('timeout', () => {
84
+ try { req.destroy(); } catch {}
85
+ resolve(false);
86
+ });
87
+ try {
88
+ req.write(body);
89
+ req.end();
90
+ } catch {
91
+ resolve(false);
92
+ }
93
+ });
94
+ }
95
+
96
+ function scheduleFlush() {
97
+ if (timer) return;
98
+ timer = setTimeout(flush, FLUSH_INTERVAL_MS);
99
+ if (timer && typeof timer.unref === 'function') timer.unref();
100
+ }
101
+
102
+ function flush() {
103
+ if (timer) {
104
+ clearTimeout(timer);
105
+ timer = null;
106
+ }
107
+ if (!buffer.length) return Promise.resolve();
108
+ const t = resolveTarget();
109
+ if (!t) {
110
+ buffer = []; // not configured to emit — drop silently
111
+ return Promise.resolve();
112
+ }
113
+ const batch = buffer.splice(0, MAX_BATCH);
114
+ const p = post(t, { events: batch }).catch(() => false);
115
+ if (buffer.length) scheduleFlush();
116
+ return p;
117
+ }
118
+
119
+ function normalizeEvent(type, props) {
120
+ if (!type || typeof type !== 'string') return null;
121
+ const ev = { type, ts: (props && Number(props.ts)) || Date.now() };
122
+ if (props) {
123
+ if (props.userId != null) ev.user_id = String(props.userId);
124
+ if (props.sessionId != null) ev.session_id = String(props.sessionId);
125
+ if (props.ip != null) ev.ip = String(props.ip);
126
+ if (props.userAgent != null) ev.user_agent = String(props.userAgent);
127
+ if (props.severity) ev.severity = String(props.severity);
128
+ if (props.attributes && typeof props.attributes === 'object' && !Array.isArray(props.attributes)) {
129
+ ev.attributes = props.attributes;
130
+ }
131
+ }
132
+ return ev;
133
+ }
134
+
135
+ /**
136
+ * Emit a custom event. Fire-and-forget — returns immediately, never throws.
137
+ * @param {string} type Event type, e.g. "auth.login.success".
138
+ * @param {object} [props] { userId, sessionId, ip, userAgent, severity, ts, attributes }
139
+ */
140
+ function track(type, props = {}) {
141
+ try {
142
+ const ev = normalizeEvent(type, props);
143
+ if (!ev) return;
144
+ buffer.push(ev);
145
+ if (buffer.length > MAX_BUFFER) buffer.splice(0, buffer.length - MAX_BUFFER); // drop oldest
146
+ if (buffer.length >= MAX_BATCH) flush();
147
+ else scheduleFlush();
148
+ } catch {
149
+ /* never throw into the app */
150
+ }
151
+ }
152
+
153
+ // Best-effort flush so buffered events aren't lost on a clean shutdown.
154
+ try {
155
+ process.once('beforeExit', () => {
156
+ try { flush(); } catch {}
157
+ });
158
+ } catch {}
159
+
160
+ module.exports = { track, flush };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "8.0.3",
3
+ "version": "8.2.0",
4
4
  "description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
5
5
  "type": "commonjs",
6
6
  "main": "register.js",
@@ -50,6 +50,10 @@
50
50
  "types": "./tracing.d.ts",
51
51
  "default": "./tracing.js"
52
52
  },
53
+ "./events": {
54
+ "types": "./events.d.ts",
55
+ "default": "./events.js"
56
+ },
53
57
  "./console-instrumentation": {
54
58
  "default": "./console-instrumentation.js"
55
59
  },
@@ -112,6 +116,8 @@
112
116
  "register.d.ts",
113
117
  "tracing.js",
114
118
  "tracing.d.ts",
119
+ "events.js",
120
+ "events.d.ts",
115
121
  "console-instrumentation.js",
116
122
  "nextjs.js",
117
123
  "nextjs.d.ts",