warning_sdk 1.0.0 → 2.0.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/dist/index.js CHANGED
@@ -1,27 +1 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
- };
16
- Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.default = void 0;
18
- __exportStar(require("./core/config"), exports);
19
- __exportStar(require("./core/client"), exports);
20
- __exportStar(require("./core/bucket"), exports);
21
- __exportStar(require("./core/aggregator"), exports);
22
- __exportStar(require("./platforms/express"), exports);
23
- // export * from './platforms/fastify';
24
- // export * from './platforms/nestjs';
25
- // export * from './platforms/next';
26
- var express_1 = require("./platforms/express");
27
- Object.defineProperty(exports, "default", { enumerable: true, get: function () { return express_1.warning; } });
1
+ var x=(n,t)=>()=>(t||n((t={exports:{}}).exports,t),t.exports);var g=x((O,f)=>{var C=require("http"),d=require("https"),m=300,D="2.0.1";function k(n,t){if(!t)return null;if(t.scheme==="bearer"){let e=n.headers?.authorization;return e?e.split(" ")[1]:null}else{let e=t.name;if(!e)return null;let r=String(e).toLowerCase();return n.headers?.[r]??n.query?.[e]??n.body?.[e]??null}}f.exports=function(t={}){let e=t.url,r=t.projectName,a=t.project_id;if(!e)throw new Error("[Warning-SDK] config missing: url");if(!r)throw new Error("[Warning-SDK] config missing: projectName");if(!a)throw new Error("[Warning-SDK] config missing: project_id");let c=new URL(e);if(c.protocol!=="https:")throw new Error("[Warning-SDK] config invalid: url must be https");let S=d,w=t.apiKeys||null,y=new d.Agent({keepAlive:!0,keepAliveMsecs:6e4,maxSockets:50,maxFreeSockets:10,timeout:m});return function(o,i,_){try{let u=i.end,p=200;i.end=function(...j){p=i.statusCode||200;let E=(o.originalUrl||o.url||o.path||"/").split("?")[0].split("#")[0]||"/",l={time_stamp:Math.floor(Date.now()/6e4),method:o.method,path:E,ip:o.ip,project_name:r,project_id:a,status_code:p},h=k(o,w);h&&(l.api_key=h);let s=S.request(c,{method:"POST",headers:{"Content-Type":"application/json","X-SDK-Version":D},timeout:m,agent:y},K=>K.resume());return s.on("timeout",()=>s.destroy()),s.on("error",()=>{}),s.end(JSON.stringify(l)),u.apply(i,j)}}catch{}_()}}});module.exports={express:g()};
package/package.json CHANGED
@@ -1,41 +1,25 @@
1
1
  {
2
2
  "name": "warning_sdk",
3
- "version": "1.0.0",
4
- "description": "",
3
+ "version": "2.0.1",
4
+ "description": "Warning SDK for Express",
5
5
  "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
6
  "files": [
8
7
  "dist"
9
8
  ],
10
9
  "scripts": {
11
- "build": "tsc",
12
- "example:simple-server": "ts-node examples/simple-server.ts",
13
- "test": "echo \"Error: no test specified\" && exit 1"
10
+ "test": "echo \"Error: no test specified\" && exit 1",
11
+ "build": "esbuild index.js --bundle --minify --platform=node --outfile=dist/index.js",
12
+ "prepublishOnly": "npm run build"
14
13
  },
15
- "keywords": [],
16
- "author": "",
17
- "license": "ISC",
18
- "type": "commonjs",
19
14
  "devDependencies": {
20
- "@nestjs/common": "^11.1.9",
21
- "@nestjs/core": "^11.1.9",
22
- "@nestjs/platform-express": "^11.1.9",
23
- "@types/express": "^5.0.6",
24
- "@types/node": "^25.0.3",
25
- "@types/react": "^19.2.7",
26
- "@types/react-dom": "^19.2.3",
27
- "fastify": "^5.6.2",
28
- "fastify-plugin": "^5.1.0",
29
- "next": "^16.1.0",
30
- "react": "^19.2.3",
31
- "react-dom": "^19.2.3",
32
- "reflect-metadata": "^0.2.2",
33
- "rxjs": "^7.8.2",
34
- "ts-node": "^10.9.2",
35
- "typescript": "^5.9.3",
36
- "express": "^5.2.1"
15
+ "esbuild": "^0.19.0"
37
16
  },
38
- "peerDependencies": {
39
- "express": ">=4"
40
- }
17
+ "keywords": [
18
+ "warning",
19
+ "sdk",
20
+ "express",
21
+ "middleware"
22
+ ],
23
+ "author": "",
24
+ "license": "ISC"
41
25
  }
@@ -1,28 +0,0 @@
1
- import { MetricFields } from './bucket';
2
- import { WarningSDKConfig } from './config';
3
- import { Transport } from './transport';
4
- export declare class Aggregator {
5
- private config;
6
- private transport;
7
- private currentBucket;
8
- private flushTimer;
9
- private pendingBuckets;
10
- private sendLoop;
11
- private degradedMinute;
12
- private capFlushMinute;
13
- private pausedUntilMs;
14
- private readonly MAX_PENDING_BUCKETS;
15
- private readonly DEGRADE_THRESHOLD;
16
- constructor(config: WarningSDKConfig, transport: Transport);
17
- isPaused(): boolean;
18
- private pauseForMs;
19
- private createNewBucket;
20
- private rotateBucket;
21
- private enqueueBucket;
22
- private drainQueue;
23
- record(fields: MetricFields): void;
24
- private startFlushInterval;
25
- flush(): Promise<void>;
26
- private flushBucket;
27
- shutdown(): Promise<void>;
28
- }
@@ -1,163 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Aggregator = void 0;
4
- const bucket_1 = require("./bucket");
5
- class Aggregator {
6
- constructor(config, transport) {
7
- this.flushTimer = null;
8
- this.pendingBuckets = [];
9
- this.sendLoop = null;
10
- this.degradedMinute = null;
11
- this.capFlushMinute = null;
12
- this.pausedUntilMs = 0;
13
- this.MAX_PENDING_BUCKETS = 5;
14
- this.DEGRADE_THRESHOLD = Math.floor(bucket_1.Bucket.MAX_TOTAL_KEYS * 0.8);
15
- this.config = config;
16
- this.transport = transport;
17
- this.currentBucket = this.createNewBucket();
18
- this.startFlushInterval();
19
- }
20
- isPaused() {
21
- return Date.now() < this.pausedUntilMs;
22
- }
23
- pauseForMs(durationMs) {
24
- const d = Number.isFinite(durationMs) ? Math.max(0, durationMs) : 0;
25
- const next = Date.now() + (d || 300000);
26
- if (next > this.pausedUntilMs)
27
- this.pausedUntilMs = next;
28
- // Drop queued data to avoid memory growth and unnecessary work.
29
- this.pendingBuckets = [];
30
- this.currentBucket = this.createNewBucket();
31
- this.degradedMinute = null;
32
- this.capFlushMinute = null;
33
- }
34
- createNewBucket(alignedTime) {
35
- const time = alignedTime ?? Date.now();
36
- const bucketTime = Math.floor(time / 60000) * 60000;
37
- return new bucket_1.Bucket(bucketTime);
38
- }
39
- rotateBucket(nextAlignedTime) {
40
- const bucketToSend = this.currentBucket;
41
- this.currentBucket = this.createNewBucket(nextAlignedTime);
42
- if (bucketToSend.timestamp !== nextAlignedTime) {
43
- this.degradedMinute = null;
44
- this.capFlushMinute = null;
45
- }
46
- return bucketToSend;
47
- }
48
- enqueueBucket(bucket) {
49
- if (bucket.isEmpty())
50
- return;
51
- if (this.pendingBuckets.length >= this.MAX_PENDING_BUCKETS) {
52
- this.pendingBuckets.shift();
53
- }
54
- this.pendingBuckets.push(bucket);
55
- }
56
- async drainQueue() {
57
- if (this.sendLoop)
58
- return this.sendLoop;
59
- this.sendLoop = (async () => {
60
- while (this.pendingBuckets.length > 0) {
61
- const bucket = this.pendingBuckets.shift();
62
- if (!bucket)
63
- continue;
64
- try {
65
- await this.flushBucket(bucket);
66
- }
67
- catch {
68
- // Silently fail, data is dropped to prevent memory leaks/blocking
69
- }
70
- }
71
- })().finally(() => {
72
- this.sendLoop = null;
73
- if (this.pendingBuckets.length > 0) {
74
- void this.drainQueue();
75
- }
76
- });
77
- return this.sendLoop;
78
- }
79
- record(fields) {
80
- if (this.isPaused())
81
- return;
82
- const eventTime = fields.timestamp ?? Date.now();
83
- const alignedTime = Math.floor(eventTime / 60000) * 60000;
84
- if (this.currentBucket.timestamp !== alignedTime) {
85
- const bucketToSend = this.rotateBucket(alignedTime);
86
- this.enqueueBucket(bucketToSend);
87
- void this.drainQueue();
88
- }
89
- const degrade = this.degradedMinute === alignedTime;
90
- if (degrade) {
91
- this.currentBucket.incrementDegraded(fields);
92
- }
93
- else {
94
- this.currentBucket.increment(fields);
95
- }
96
- if (this.degradedMinute === null && this.currentBucket.size() >= this.DEGRADE_THRESHOLD) {
97
- this.degradedMinute = alignedTime;
98
- }
99
- if (this.currentBucket.size() >= bucket_1.Bucket.MAX_TOTAL_KEYS) {
100
- if (this.capFlushMinute !== alignedTime) {
101
- this.capFlushMinute = alignedTime;
102
- this.degradedMinute = alignedTime;
103
- const bucketToSend = this.rotateBucket(alignedTime);
104
- this.enqueueBucket(bucketToSend);
105
- void this.drainQueue();
106
- }
107
- }
108
- }
109
- startFlushInterval() {
110
- if (this.flushTimer)
111
- clearInterval(this.flushTimer);
112
- this.flushTimer = setInterval(() => {
113
- void this.flush().catch(() => { });
114
- }, 10000);
115
- if (this.flushTimer.unref) {
116
- this.flushTimer.unref();
117
- }
118
- }
119
- async flush() {
120
- if (this.currentBucket.isEmpty()) {
121
- const nowAligned = Math.floor(Date.now() / 60000) * 60000;
122
- if (this.currentBucket.timestamp !== nowAligned) {
123
- this.currentBucket = new bucket_1.Bucket(nowAligned);
124
- this.degradedMinute = null;
125
- this.capFlushMinute = null;
126
- }
127
- await this.drainQueue();
128
- return;
129
- }
130
- const nowAligned = Math.floor(Date.now() / 60000) * 60000;
131
- const bucketToSend = this.rotateBucket(nowAligned);
132
- this.enqueueBucket(bucketToSend);
133
- await this.drainQueue();
134
- }
135
- async flushBucket(bucketToSend) {
136
- if (bucketToSend.isEmpty())
137
- return;
138
- if (this.isPaused())
139
- return;
140
- const metricsWithTime = bucketToSend.toJSONWithMinute(bucketToSend.timestamp);
141
- const maxRows = Math.min(2000, metricsWithTime.length);
142
- for (let i = 0; i < metricsWithTime.length; i += maxRows) {
143
- const payload = {
144
- project_id: this.config.projectId,
145
- project_token: this.config.projectToken,
146
- sent_at: Date.now(),
147
- metrics: metricsWithTime.slice(i, i + maxRows)
148
- };
149
- const res = await this.transport.send(payload);
150
- if (res?.paused) {
151
- this.pauseForMs(res.retryAfterMs || 300000);
152
- return;
153
- }
154
- }
155
- }
156
- async shutdown() {
157
- if (this.flushTimer) {
158
- clearInterval(this.flushTimer);
159
- }
160
- await this.flush();
161
- }
162
- }
163
- exports.Aggregator = Aggregator;
@@ -1,41 +0,0 @@
1
- export interface MetricFields {
2
- serviceName?: string;
3
- method: string;
4
- route: string;
5
- status: number;
6
- apiKeyId?: string;
7
- ipHash?: string;
8
- uaHash?: string;
9
- latencyMs?: number;
10
- aborted?: boolean;
11
- timestamp?: number;
12
- }
13
- export declare class Bucket {
14
- timestamp: number;
15
- counts: Map<string, {
16
- count: number;
17
- sumLatencyMs?: number;
18
- minLatencyMs?: number;
19
- maxLatencyMs?: number;
20
- latencyHistogram?: Record<string, number>;
21
- }>;
22
- private ipTracking;
23
- private overflowProtection;
24
- private static MAX_UNIQUE_IPS;
25
- static readonly MAX_TOTAL_KEYS = 2000;
26
- private static readonly SEP;
27
- private static readonly ESC;
28
- constructor(timestamp: number);
29
- private static escapePart;
30
- private static unescapePart;
31
- private static splitKey;
32
- private static latencyBucket;
33
- static generateKeyParts(serviceName: string, method: string, route: string, status: number, apiKeyId?: string, ipHash?: string, uaHash?: string): string;
34
- private incrementInternal;
35
- increment(fields: MetricFields): void;
36
- incrementDegraded(fields: MetricFields): void;
37
- isEmpty(): boolean;
38
- size(): number;
39
- toJSON(): any[];
40
- toJSONWithMinute(minute: number): any[];
41
- }
@@ -1,237 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Bucket = void 0;
4
- class Bucket {
5
- constructor(timestamp) {
6
- this.ipTracking = new Map();
7
- this.overflowProtection = new Set();
8
- this.timestamp = timestamp;
9
- this.counts = new Map();
10
- }
11
- static escapePart(value) {
12
- let out = '';
13
- for (let i = 0; i < value.length; i += 1) {
14
- const ch = value[i];
15
- if (ch === Bucket.ESC || ch === Bucket.SEP) {
16
- out += Bucket.ESC;
17
- }
18
- out += ch;
19
- }
20
- return out;
21
- }
22
- static unescapePart(value) {
23
- let out = '';
24
- let escaped = false;
25
- for (let i = 0; i < value.length; i += 1) {
26
- const ch = value[i];
27
- if (escaped) {
28
- out += ch;
29
- escaped = false;
30
- continue;
31
- }
32
- if (ch === Bucket.ESC) {
33
- escaped = true;
34
- continue;
35
- }
36
- out += ch;
37
- }
38
- if (escaped)
39
- return null;
40
- return out;
41
- }
42
- static splitKey(key) {
43
- const parts = [];
44
- let current = '';
45
- let escaped = false;
46
- for (let i = 0; i < key.length; i += 1) {
47
- const ch = key[i];
48
- if (escaped) {
49
- current += Bucket.ESC;
50
- current += ch;
51
- escaped = false;
52
- continue;
53
- }
54
- if (ch === Bucket.ESC) {
55
- escaped = true;
56
- continue;
57
- }
58
- if (ch === Bucket.SEP) {
59
- parts.push(current);
60
- current = '';
61
- continue;
62
- }
63
- current += ch;
64
- }
65
- if (escaped)
66
- return null;
67
- parts.push(current);
68
- return parts;
69
- }
70
- static latencyBucket(ms) {
71
- // Buckets must match backend parser:
72
- // - "a-b" or "a+" (see warning_backend/analytics.py:_parse_bucket_range)
73
- if (ms < 0)
74
- ms = 0;
75
- if (ms <= 10)
76
- return "0-10";
77
- if (ms <= 25)
78
- return "10-25";
79
- if (ms <= 50)
80
- return "25-50";
81
- if (ms <= 100)
82
- return "50-100";
83
- if (ms <= 200)
84
- return "100-200";
85
- if (ms <= 350)
86
- return "200-350";
87
- if (ms <= 500)
88
- return "350-500";
89
- if (ms <= 750)
90
- return "500-750";
91
- if (ms <= 1000)
92
- return "750-1000";
93
- if (ms <= 1500)
94
- return "1000-1500";
95
- if (ms <= 2000)
96
- return "1500-2000";
97
- if (ms <= 3000)
98
- return "2000-3000";
99
- if (ms <= 5000)
100
- return "3000-5000";
101
- return "5000+";
102
- }
103
- static generateKeyParts(serviceName, method, route, status, apiKeyId, ipHash, uaHash) {
104
- const svc = serviceName || '';
105
- const ak = apiKeyId || '';
106
- const ip = ipHash || '';
107
- const ua = uaHash || '';
108
- return [
109
- Bucket.escapePart(svc),
110
- Bucket.escapePart(method),
111
- Bucket.escapePart(route),
112
- Bucket.escapePart(String(status)),
113
- Bucket.escapePart(ak),
114
- Bucket.escapePart(ip),
115
- Bucket.escapePart(ua)
116
- ].join(Bucket.SEP);
117
- }
118
- incrementInternal(fields, degraded) {
119
- let ipHash = fields.ipHash || '';
120
- if (!degraded && fields.ipHash) {
121
- const guardKey = `${fields.serviceName || ''}|${fields.method}|${fields.route}|${fields.apiKeyId || 'none'}`;
122
- if (this.overflowProtection.has(guardKey)) {
123
- ipHash = 'MANY';
124
- }
125
- else {
126
- let s = this.ipTracking.get(guardKey);
127
- if (!s) {
128
- s = new Set();
129
- this.ipTracking.set(guardKey, s);
130
- }
131
- s.add(ipHash);
132
- if (s.size > Bucket.MAX_UNIQUE_IPS) {
133
- this.overflowProtection.add(guardKey);
134
- this.ipTracking.delete(guardKey);
135
- ipHash = 'MANY';
136
- }
137
- }
138
- }
139
- const k = Bucket.generateKeyParts(fields.serviceName || '', fields.method, fields.route, fields.status, fields.apiKeyId, degraded ? undefined : ipHash, degraded ? undefined : fields.uaHash);
140
- let current = this.counts.get(k);
141
- if (!current) {
142
- if (this.counts.size >= Bucket.MAX_TOTAL_KEYS) {
143
- return;
144
- }
145
- current = { count: 0 };
146
- this.counts.set(k, current);
147
- }
148
- current.count += 1;
149
- if (Number.isFinite(fields.latencyMs)) {
150
- const latency = fields.latencyMs;
151
- current.sumLatencyMs = (current.sumLatencyMs || 0) + latency;
152
- current.maxLatencyMs = current.maxLatencyMs === undefined ? latency : Math.max(current.maxLatencyMs, latency);
153
- current.minLatencyMs = current.minLatencyMs === undefined ? latency : Math.min(current.minLatencyMs, latency);
154
- const bucket = Bucket.latencyBucket(latency);
155
- const hist = current.latencyHistogram || (current.latencyHistogram = {});
156
- hist[bucket] = (hist[bucket] || 0) + 1;
157
- }
158
- }
159
- increment(fields) {
160
- this.incrementInternal(fields, false);
161
- }
162
- incrementDegraded(fields) {
163
- this.incrementInternal(fields, true);
164
- }
165
- isEmpty() {
166
- return this.counts.size === 0;
167
- }
168
- size() {
169
- return this.counts.size;
170
- }
171
- toJSON() {
172
- const metrics = [];
173
- for (const [k, v] of this.counts.entries()) {
174
- const parts = Bucket.splitKey(k);
175
- if (!parts || parts.length !== 7)
176
- continue;
177
- const unescaped = parts.map(part => Bucket.unescapePart(part));
178
- if (unescaped.some(part => part === null))
179
- continue;
180
- const [serviceName, method, route, statusRaw, apiKeyId, ipHash, uaHash] = unescaped;
181
- const statusValue = parseInt(statusRaw, 10);
182
- if (!Number.isFinite(statusValue))
183
- continue;
184
- metrics.push({
185
- service_name: serviceName || undefined,
186
- method,
187
- route,
188
- status: statusValue,
189
- api_key_id: apiKeyId || undefined,
190
- ip_hash: ipHash || undefined,
191
- ua_hash: uaHash || undefined,
192
- count: v.count,
193
- sum_latency_ms: v.sumLatencyMs,
194
- min_latency_ms: v.minLatencyMs,
195
- max_latency_ms: v.maxLatencyMs,
196
- latency_histogram: v.latencyHistogram && Object.keys(v.latencyHistogram).length ? v.latencyHistogram : undefined
197
- });
198
- }
199
- return metrics;
200
- }
201
- toJSONWithMinute(minute) {
202
- const metrics = [];
203
- for (const [k, v] of this.counts.entries()) {
204
- const parts = Bucket.splitKey(k);
205
- if (!parts || parts.length !== 7)
206
- continue;
207
- const unescaped = parts.map(part => Bucket.unescapePart(part));
208
- if (unescaped.some(part => part === null))
209
- continue;
210
- const [serviceName, method, route, statusRaw, apiKeyId, ipHash, uaHash] = unescaped;
211
- const statusValue = parseInt(statusRaw, 10);
212
- if (!Number.isFinite(statusValue))
213
- continue;
214
- metrics.push({
215
- service_name: serviceName || undefined,
216
- method,
217
- route,
218
- status: statusValue,
219
- api_key_id: apiKeyId || undefined,
220
- ip_hash: ipHash || undefined,
221
- ua_hash: uaHash || undefined,
222
- count: v.count,
223
- sum_latency_ms: v.sumLatencyMs,
224
- min_latency_ms: v.minLatencyMs,
225
- max_latency_ms: v.maxLatencyMs,
226
- latency_histogram: v.latencyHistogram && Object.keys(v.latencyHistogram).length ? v.latencyHistogram : undefined,
227
- minute
228
- });
229
- }
230
- return metrics;
231
- }
232
- }
233
- exports.Bucket = Bucket;
234
- Bucket.MAX_UNIQUE_IPS = 50;
235
- Bucket.MAX_TOTAL_KEYS = 2000;
236
- Bucket.SEP = '\u001f';
237
- Bucket.ESC = '\\';
@@ -1,22 +0,0 @@
1
- import { WarningSDKConfig } from './config';
2
- export declare class WarningClient {
3
- readonly config: WarningSDKConfig;
4
- private aggregator;
5
- private initialized;
6
- constructor(config: Partial<WarningSDKConfig>);
7
- isPaused(): boolean;
8
- recordRequest(req: {
9
- serviceName?: string;
10
- method: string;
11
- route: string;
12
- status: number;
13
- ip?: string;
14
- userAgent?: string;
15
- apiKeyId?: string;
16
- authToken?: string;
17
- aborted?: boolean;
18
- latencyMs?: number;
19
- timestamp?: number;
20
- }): void;
21
- flush(): Promise<void>;
22
- }
@@ -1,51 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WarningClient = void 0;
4
- const config_1 = require("./config");
5
- const aggregator_1 = require("./aggregator");
6
- const transport_1 = require("./transport");
7
- const privacy_1 = require("./privacy");
8
- class WarningClient {
9
- constructor(config) {
10
- this.initialized = false;
11
- this.config = (0, config_1.validateConfig)(config);
12
- const transport = new transport_1.Transport(this.config);
13
- this.aggregator = new aggregator_1.Aggregator(this.config, transport);
14
- this.initialized = true;
15
- }
16
- isPaused() {
17
- return this.aggregator.isPaused();
18
- }
19
- recordRequest(req) {
20
- if (!this.initialized)
21
- return;
22
- if (this.aggregator.isPaused())
23
- return;
24
- const serviceName = req.serviceName || this.config.serviceName;
25
- let finalIpHash = req.ip ? privacy_1.Privacy.passThroughIp(req.ip) : undefined;
26
- let finalUaHash = req.userAgent ? privacy_1.Privacy.passThroughUa(req.userAgent) : undefined;
27
- let apiKeyId = req.apiKeyId;
28
- if (!apiKeyId && req.authToken) {
29
- apiKeyId = privacy_1.Privacy.passThroughApiKey(req.authToken);
30
- }
31
- let latencyMs = req.latencyMs;
32
- if (!Number.isFinite(latencyMs) || latencyMs < 0 || latencyMs >= 120000) {
33
- latencyMs = undefined;
34
- }
35
- this.aggregator.record({
36
- serviceName: serviceName,
37
- method: req.method.toUpperCase(),
38
- route: req.route,
39
- status: req.status,
40
- apiKeyId: apiKeyId,
41
- ipHash: finalIpHash,
42
- uaHash: finalUaHash,
43
- latencyMs: latencyMs,
44
- timestamp: req.timestamp || Date.now()
45
- });
46
- }
47
- flush() {
48
- return this.aggregator.flush();
49
- }
50
- }
51
- exports.WarningClient = WarningClient;
@@ -1,18 +0,0 @@
1
- export interface WarningSDKConfig {
2
- projectId: string;
3
- projectToken: string;
4
- ingestKey: string;
5
- serviceName?: string;
6
- collectorUrl?: string;
7
- /**
8
- * Which inbound header contains the business's end-user API token.
9
- * Used to derive `api_key_id` (hashed identifier) for key-sharing detection.
10
- *
11
- * Defaults to "authorization".
12
- */
13
- authHeaderName?: string;
14
- debug?: boolean;
15
- logger?: (level: 'debug' | 'info' | 'warn' | 'error', message: string) => void;
16
- }
17
- export declare const DEFAULT_CONFIG: Partial<WarningSDKConfig>;
18
- export declare function validateConfig(config: Partial<WarningSDKConfig>): WarningSDKConfig;
@@ -1,46 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_CONFIG = void 0;
4
- exports.validateConfig = validateConfig;
5
- exports.DEFAULT_CONFIG = {};
6
- function validateConfig(config) {
7
- const sanitized = { ...config };
8
- const merged = { ...exports.DEFAULT_CONFIG, ...sanitized };
9
- const log = (level, message) => {
10
- if (merged.logger) {
11
- merged.logger(level, message);
12
- return;
13
- }
14
- if (level === 'error') {
15
- console.error(message);
16
- return;
17
- }
18
- if (merged.debug) {
19
- const fn = level === 'debug'
20
- ? console.debug
21
- : level === 'info'
22
- ? console.info
23
- : level === 'warn'
24
- ? console.warn
25
- : console.error;
26
- fn(message);
27
- }
28
- };
29
- if (!merged.projectId) {
30
- log('error', "WarningSDK: Missing 'projectId'.");
31
- throw new Error("WarningSDK: Missing required config 'projectId'.");
32
- }
33
- if (!merged.projectToken) {
34
- log('error', "WarningSDK: Missing 'projectToken'.");
35
- throw new Error("WarningSDK: Missing required config 'projectToken'.");
36
- }
37
- if (!merged.ingestKey) {
38
- log('error', "WarningSDK: Missing 'ingestKey'.");
39
- throw new Error("WarningSDK: Missing required config 'ingestKey'.");
40
- }
41
- if (!merged.collectorUrl) {
42
- log('error', "WarningSDK: Missing 'collectorUrl' (Collector URL).");
43
- throw new Error("WarningSDK: Missing required config 'collectorUrl'.");
44
- }
45
- return merged;
46
- }
@@ -1,8 +0,0 @@
1
- export declare class DeterministicCrypto {
2
- private encKey;
3
- private macKey;
4
- private nonceKey;
5
- constructor(cryptoKey: string);
6
- encryptDeterministic(value: string, aad: string): string;
7
- decryptDeterministic(token: string, aad: string): string | null;
8
- }
@@ -1,153 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.DeterministicCrypto = void 0;
37
- const crypto = __importStar(require("crypto"));
38
- const VERSION_PREFIX = 'w1.';
39
- const VERSION_BYTE = 1;
40
- const SALT = Buffer.from('warning-crypto-v1', 'utf8');
41
- const INFO = Buffer.from('deterministic-v1', 'utf8');
42
- function toBase64Url(buf) {
43
- return buf
44
- .toString('base64')
45
- .replace(/\+/g, '-')
46
- .replace(/\//g, '_')
47
- .replace(/=+$/g, '');
48
- }
49
- function fromBase64Url(s) {
50
- const normalized = s.replace(/-/g, '+').replace(/_/g, '/');
51
- const padLen = (4 - (normalized.length % 4)) % 4;
52
- const padded = normalized + '='.repeat(padLen);
53
- return Buffer.from(padded, 'base64');
54
- }
55
- function packParts(...parts) {
56
- const out = [];
57
- for (const part of parts) {
58
- const len = Buffer.alloc(4);
59
- len.writeUInt32BE(part.length, 0);
60
- out.push(len, part);
61
- }
62
- return Buffer.concat(out);
63
- }
64
- function hmacSha256(key, data) {
65
- return crypto.createHmac('sha256', key).update(data).digest();
66
- }
67
- function deriveKeys(masterKey) {
68
- // HKDF-SHA256(masterKey, salt=SALT, info=INFO) -> 96 bytes
69
- const okm = Buffer.from(crypto.hkdfSync('sha256', masterKey, SALT, INFO, 96));
70
- return {
71
- encKey: okm.subarray(0, 32),
72
- macKey: okm.subarray(32, 64),
73
- nonceKey: okm.subarray(64, 96),
74
- };
75
- }
76
- function parseKeyString(key) {
77
- const trimmed = (key || '').trim();
78
- if (!trimmed)
79
- throw new Error("WarningSDK: Missing required config 'cryptoKey'.");
80
- if (trimmed.startsWith('hex:')) {
81
- const hex = trimmed.slice(4).trim();
82
- const buf = Buffer.from(hex, 'hex');
83
- if (buf.length < 32)
84
- throw new Error("WarningSDK: 'cryptoKey' (hex) must be at least 32 bytes.");
85
- return buf;
86
- }
87
- // Accept base64 / base64url, with or without padding.
88
- const buf = fromBase64Url(trimmed);
89
- if (buf.length < 32)
90
- throw new Error("WarningSDK: 'cryptoKey' (base64) must be at least 32 bytes.");
91
- return buf;
92
- }
93
- function xorWithKeystream(encKey, nonce, data) {
94
- const out = Buffer.alloc(data.length);
95
- let offset = 0;
96
- let counter = 0;
97
- while (offset < data.length) {
98
- const ctr = Buffer.alloc(4);
99
- ctr.writeUInt32BE(counter >>> 0, 0);
100
- const block = hmacSha256(encKey, Buffer.concat([nonce, ctr]));
101
- const take = Math.min(block.length, data.length - offset);
102
- for (let i = 0; i < take; i += 1) {
103
- out[offset + i] = data[offset + i] ^ block[i];
104
- }
105
- offset += take;
106
- counter += 1;
107
- }
108
- return out;
109
- }
110
- class DeterministicCrypto {
111
- constructor(cryptoKey) {
112
- const master = parseKeyString(cryptoKey);
113
- const keys = deriveKeys(master);
114
- this.encKey = keys.encKey;
115
- this.macKey = keys.macKey;
116
- this.nonceKey = keys.nonceKey;
117
- }
118
- encryptDeterministic(value, aad) {
119
- if (!value)
120
- return '';
121
- const aadBuf = Buffer.from(aad || '', 'utf8');
122
- const pt = Buffer.from(value, 'utf8');
123
- const nonce = hmacSha256(this.nonceKey, packParts(aadBuf, pt)).subarray(0, 16);
124
- const ct = xorWithKeystream(this.encKey, nonce, pt);
125
- const tag = hmacSha256(this.macKey, packParts(aadBuf, nonce, ct)).subarray(0, 16);
126
- const payload = Buffer.concat([Buffer.from([VERSION_BYTE]), nonce, ct, tag]);
127
- return VERSION_PREFIX + toBase64Url(payload);
128
- }
129
- decryptDeterministic(token, aad) {
130
- if (!token || !token.startsWith(VERSION_PREFIX))
131
- return null;
132
- const aadBuf = Buffer.from(aad || '', 'utf8');
133
- const raw = fromBase64Url(token.slice(VERSION_PREFIX.length));
134
- if (raw.length < 1 + 16 + 16)
135
- return null;
136
- if (raw[0] !== VERSION_BYTE)
137
- return null;
138
- const nonce = raw.subarray(1, 1 + 16);
139
- const tag = raw.subarray(raw.length - 16);
140
- const ct = raw.subarray(1 + 16, raw.length - 16);
141
- const expected = hmacSha256(this.macKey, packParts(aadBuf, nonce, ct)).subarray(0, 16);
142
- if (expected.length !== tag.length || !crypto.timingSafeEqual(expected, tag))
143
- return null;
144
- const pt = xorWithKeystream(this.encKey, nonce, ct);
145
- try {
146
- return pt.toString('utf8');
147
- }
148
- catch {
149
- return null;
150
- }
151
- }
152
- }
153
- exports.DeterministicCrypto = DeterministicCrypto;
@@ -1,5 +0,0 @@
1
- export declare class Privacy {
2
- static passThroughIp(ip: string): string | undefined;
3
- static passThroughApiKey(rawKey: string): string | undefined;
4
- static passThroughUa(ua: string): string | undefined;
5
- }
@@ -1,18 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Privacy = void 0;
4
- class Privacy {
5
- static passThroughIp(ip) {
6
- const v = (ip || '').trim();
7
- return v.length ? v : undefined;
8
- }
9
- static passThroughApiKey(rawKey) {
10
- const v = (rawKey || '').trim();
11
- return v.length ? v : undefined;
12
- }
13
- static passThroughUa(ua) {
14
- const v = (ua || '').trim();
15
- return v.length ? v : undefined;
16
- }
17
- }
18
- exports.Privacy = Privacy;
@@ -1,12 +0,0 @@
1
- import { WarningSDKConfig } from './config';
2
- export type TransportSendResult = {
3
- paused?: boolean;
4
- reason?: string;
5
- retryAfterMs?: number;
6
- statusCode?: number;
7
- };
8
- export declare class Transport {
9
- private config;
10
- constructor(config: WarningSDKConfig);
11
- send(payload: any): Promise<TransportSendResult>;
12
- }
@@ -1,107 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.Transport = void 0;
37
- const http = __importStar(require("http"));
38
- const https = __importStar(require("https"));
39
- class Transport {
40
- constructor(config) {
41
- this.config = config;
42
- }
43
- async send(payload) {
44
- if (!this.config.projectToken)
45
- return {};
46
- const data = JSON.stringify(payload);
47
- let url;
48
- try {
49
- url = new URL(this.config.collectorUrl || '');
50
- }
51
- catch {
52
- return {};
53
- }
54
- const isHttps = url.protocol === 'https:';
55
- const options = {
56
- hostname: url.hostname,
57
- port: url.port || (isHttps ? 443 : 80),
58
- path: `${url.pathname}${url.search}`,
59
- method: 'POST',
60
- headers: {
61
- 'Content-Type': 'application/json',
62
- 'Content-Length': Buffer.byteLength(data),
63
- 'x-ingest-key': this.config.ingestKey,
64
- 'User-Agent': 'warning-sdk-node/1.0.0'
65
- },
66
- timeout: 2000
67
- };
68
- return new Promise((resolve) => {
69
- const requester = isHttps ? https : http;
70
- const req = requester.request(options, (res) => {
71
- const statusCode = res.statusCode || 0;
72
- let body = '';
73
- res.setEncoding('utf8');
74
- res.on('data', (chunk) => {
75
- if (body.length <= 8192)
76
- body += chunk;
77
- });
78
- res.on('end', () => {
79
- const out = { statusCode };
80
- // Default behavior: treat as "sent" even if backend rejected (so app never breaks).
81
- // But if backend says credits are 0, we pause sending on the SDK side.
82
- try {
83
- const parsed = JSON.parse(body || '{}');
84
- const paused = Boolean(parsed?.paused) || statusCode === 402;
85
- if (paused) {
86
- out.paused = true;
87
- out.reason = String(parsed?.reason || 'paused');
88
- const retry = parsed?.retry_after_ms;
89
- if (Number.isFinite(retry) && retry > 0)
90
- out.retryAfterMs = Number(retry);
91
- }
92
- }
93
- catch {
94
- // ignore
95
- }
96
- resolve(out);
97
- });
98
- });
99
- req.on('error', () => {
100
- resolve({});
101
- });
102
- req.write(data);
103
- req.end();
104
- });
105
- }
106
- }
107
- exports.Transport = Transport;
package/dist/index.d.ts DELETED
@@ -1,6 +0,0 @@
1
- export * from './core/config';
2
- export * from './core/client';
3
- export * from './core/bucket';
4
- export * from './core/aggregator';
5
- export * from './platforms/express';
6
- export { warning as default } from './platforms/express';
@@ -1,5 +0,0 @@
1
- import { Request, Response, NextFunction } from 'express';
2
- import { WarningClient } from '../core/client';
3
- import { WarningSDKConfig } from '../core/config';
4
- export declare function warningMiddleware(config: Partial<WarningSDKConfig> | WarningClient): (req: Request, res: Response, next: NextFunction) => void;
5
- export declare function warning(config: Partial<WarningSDKConfig> | WarningClient): (req: Request, res: Response, next: NextFunction) => void;
@@ -1,102 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.warningMiddleware = warningMiddleware;
4
- exports.warning = warning;
5
- const client_1 = require("../core/client");
6
- function warningMiddleware(config) {
7
- const client = config instanceof client_1.WarningClient ? config : new client_1.WarningClient(config);
8
- return (req, res, next) => {
9
- if (client.isPaused()) {
10
- return next();
11
- }
12
- const startTime = process.hrtime.bigint();
13
- let recorded = false;
14
- const record = () => {
15
- if (recorded)
16
- return;
17
- recorded = true;
18
- try {
19
- const method = req.method;
20
- const statusCode = res.statusCode;
21
- const aborted = req.aborted || !res.writableEnded;
22
- let routePath = 'UNKNOWN';
23
- if (req.route && req.route.path) {
24
- routePath = req.baseUrl ? (req.baseUrl + req.route.path) : req.route.path;
25
- }
26
- else {
27
- const fullPath = (req.baseUrl || '') + (req.path || '');
28
- const segments = fullPath.split('/').map((segment) => {
29
- if (!segment)
30
- return segment; // empty
31
- if (/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(segment)) {
32
- return ':uuid';
33
- }
34
- if (/^[0-9a-fA-F]{24,}$/.test(segment)) {
35
- return ':hex_id';
36
- }
37
- if (/^\d+$/.test(segment)) {
38
- return ':id';
39
- }
40
- if (segment.length >= 26) {
41
- if (/[0-9]/.test(segment)) {
42
- return ':id_str';
43
- }
44
- }
45
- if (segment.length >= 20 && /[-_]/.test(segment)) {
46
- return ':id_str';
47
- }
48
- return segment;
49
- });
50
- routePath = segments.join('/');
51
- }
52
- if (!routePath || routePath === 'UNKNOWN') {
53
- routePath = 'RAW_PATH_UNAVAILABLE';
54
- }
55
- // If behind a proxy/CDN, enable: app.set('trust proxy', true)
56
- let ip = req.ip;
57
- if (ip) {
58
- ip = ip.trim();
59
- }
60
- if (!ip || ip.length === 0)
61
- ip = undefined;
62
- const authHeaderName = (client.config.authHeaderName || 'authorization').toLowerCase();
63
- const authHeader = req.headers[authHeaderName];
64
- let authToken;
65
- if (authHeader) {
66
- let raw = Array.isArray(authHeader) ? authHeader[0] : authHeader;
67
- if (raw.toLowerCase().startsWith('bearer ')) {
68
- raw = raw.substring(7).trim();
69
- }
70
- if (raw.toLowerCase().startsWith('token ')) {
71
- raw = raw.substring(6).trim();
72
- }
73
- if (raw.length > 0) {
74
- authToken = raw;
75
- }
76
- }
77
- const userAgent = req.headers['user-agent'];
78
- const endTime = process.hrtime.bigint();
79
- const latencyMs = Number((endTime - startTime) / BigInt(1000000));
80
- client.recordRequest({
81
- serviceName: client.config.serviceName,
82
- method: method,
83
- route: routePath,
84
- status: statusCode,
85
- ip: ip,
86
- userAgent: userAgent,
87
- authToken: authToken,
88
- aborted: aborted,
89
- latencyMs: latencyMs,
90
- timestamp: Date.now()
91
- });
92
- }
93
- catch (e) { }
94
- };
95
- res.once('finish', record);
96
- res.once('close', record);
97
- next();
98
- };
99
- }
100
- function warning(config) {
101
- return warningMiddleware(config);
102
- }