seqpulse 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Nassir
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # seqpulse (Node.js SDK)
2
+
3
+ SeqPulse SDK for:
4
+
5
+ - HTTP metrics instrumentation
6
+ - metrics endpoint exposure
7
+ - optional HMAC v2 validation on inbound SeqPulse calls
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install seqpulse
13
+ # or
14
+ pnpm add seqpulse
15
+ ```
16
+
17
+ ## Express usage
18
+
19
+ ```js
20
+ const express = require("express");
21
+ const seqpulse = require("seqpulse");
22
+
23
+ const app = express();
24
+
25
+ seqpulse.init({
26
+ apiKey: process.env.SEQPULSE_API_KEY,
27
+ endpoint: "/seqpulse-metrics",
28
+ hmacEnabled: process.env.SEQPULSE_HMAC_ENABLED === "true",
29
+ hmacSecret: process.env.SEQPULSE_HMAC_SECRET,
30
+ });
31
+
32
+ app.use(seqpulse.metrics());
33
+
34
+ app.get("/", (_req, res) => res.send("ok"));
35
+
36
+ app.listen(3000, () => {
37
+ console.log("Running on :3000");
38
+ });
39
+ ```
40
+
41
+ ## Returned payload
42
+
43
+ `GET /seqpulse-metrics` returns:
44
+
45
+ ```json
46
+ {
47
+ "metrics": {
48
+ "requests_per_sec": 0.25,
49
+ "latency_p95": 31.221,
50
+ "error_rate": 0,
51
+ "cpu_usage": 0.42,
52
+ "memory_usage": 0.18
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## Notes
58
+
59
+ - This SDK does not manage CI/CD trigger/finish workflow.
60
+ - `apiKey` is required for config consistency, but not used directly in endpoint responses.
61
+
62
+ ## Local smoke test
63
+
64
+ ```bash
65
+ npm run smoke
66
+ ```
package/index.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ export type SeqPulseInitConfig = {
2
+ apiKey: string
3
+ endpoint?: string
4
+ windowSeconds?: number
5
+ hmacEnabled?: boolean
6
+ hmacSecret?: string
7
+ maxSkewPastSeconds?: number
8
+ maxSkewFutureSeconds?: number
9
+ }
10
+
11
+ export type SeqPulseMetrics = {
12
+ requests_per_sec: number
13
+ latency_p95: number
14
+ error_rate: number
15
+ cpu_usage: number
16
+ memory_usage: number
17
+ }
18
+
19
+ export type SeqPulseInstance = {
20
+ init(config: SeqPulseInitConfig): void
21
+ metrics(): (req: any, res: any, next: () => void) => void
22
+ getMetricsSnapshot(): SeqPulseMetrics
23
+ }
24
+
25
+ declare const seqpulse: SeqPulseInstance
26
+ export = seqpulse
package/index.js ADDED
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+
3
+ const crypto = require("node:crypto");
4
+ const os = require("node:os");
5
+
6
+ const DEFAULT_WINDOW_SECONDS = 60;
7
+ const DEFAULT_ENDPOINT = "/seqpulse-metrics";
8
+ const DEFAULT_MAX_SKEW_PAST_SECONDS = 300;
9
+ const DEFAULT_MAX_SKEW_FUTURE_SECONDS = 30;
10
+
11
+ const state = {
12
+ initialized: false,
13
+ config: {
14
+ apiKey: "",
15
+ endpoint: DEFAULT_ENDPOINT,
16
+ windowSeconds: DEFAULT_WINDOW_SECONDS,
17
+ hmacEnabled: false,
18
+ hmacSecret: "",
19
+ maxSkewPastSeconds: DEFAULT_MAX_SKEW_PAST_SECONDS,
20
+ maxSkewFutureSeconds: DEFAULT_MAX_SKEW_FUTURE_SECONDS,
21
+ },
22
+ samples: [],
23
+ nonceSeenAt: new Map(),
24
+ };
25
+
26
+ function canonicalizePath(path) {
27
+ if (!path) return "/";
28
+ let normalized = String(path);
29
+ if (!normalized.startsWith("/")) normalized = `/${normalized}`;
30
+ if (normalized !== "/" && normalized.endsWith("/")) normalized = normalized.slice(0, -1);
31
+ return normalized;
32
+ }
33
+
34
+ function nowMs() {
35
+ return Date.now();
36
+ }
37
+
38
+ function percentile95(values) {
39
+ if (!values.length) return 0;
40
+ const sorted = values.slice().sort((a, b) => a - b);
41
+ const idx = Math.floor(0.95 * (sorted.length - 1));
42
+ return sorted[idx];
43
+ }
44
+
45
+ function round(value, digits) {
46
+ const factor = 10 ** digits;
47
+ return Math.round(value * factor) / factor;
48
+ }
49
+
50
+ function cleanupOldSamples() {
51
+ const cutoff = nowMs() - state.config.windowSeconds * 1000;
52
+ state.samples = state.samples.filter((item) => item.atMs >= cutoff);
53
+ }
54
+
55
+ function cleanupOldNonces() {
56
+ const ttlMs = (state.config.maxSkewPastSeconds + state.config.maxSkewFutureSeconds) * 1000;
57
+ const cutoff = nowMs() - ttlMs;
58
+ for (const [nonce, seenAt] of state.nonceSeenAt.entries()) {
59
+ if (seenAt < cutoff) state.nonceSeenAt.delete(nonce);
60
+ }
61
+ }
62
+
63
+ function buildSignature(secret, timestamp, method, path, nonce) {
64
+ const payload = `${timestamp}|${String(method || "GET").toUpperCase()}|${canonicalizePath(path)}|${nonce}`;
65
+ const digest = crypto.createHmac("sha256", secret).update(payload).digest("hex");
66
+ return `sha256=${digest}`;
67
+ }
68
+
69
+ function rejectHmac(res, message) {
70
+ res.status(401).json({ error: message || "Unauthorized" });
71
+ }
72
+
73
+ function validateTimestamp(ts) {
74
+ if (!ts) throw new Error("Missing timestamp");
75
+ const sentMs = Date.parse(ts);
76
+ if (Number.isNaN(sentMs)) throw new Error("Invalid timestamp");
77
+
78
+ const deltaSeconds = (nowMs() - sentMs) / 1000;
79
+ if (deltaSeconds > state.config.maxSkewPastSeconds) throw new Error("Timestamp too old");
80
+ if (deltaSeconds < -state.config.maxSkewFutureSeconds) throw new Error("Timestamp too far in the future");
81
+ }
82
+
83
+ function validateHmacRequest(req, res) {
84
+ if (!state.config.hmacEnabled) return true;
85
+
86
+ const timestamp = req.get("X-SeqPulse-Timestamp") || "";
87
+ const signature = req.get("X-SeqPulse-Signature") || "";
88
+ const nonce = req.get("X-SeqPulse-Nonce") || "";
89
+ const version = req.get("X-SeqPulse-Signature-Version") || "";
90
+ const method = (req.get("X-SeqPulse-Method") || req.method || "GET").toUpperCase();
91
+ const canonicalPath = canonicalizePath(req.get("X-SeqPulse-Canonical-Path") || req.path || req.url || "/");
92
+
93
+ if (!timestamp || !signature || !nonce || version !== "v2") {
94
+ rejectHmac(res, "Missing or invalid HMAC headers");
95
+ return false;
96
+ }
97
+
98
+ try {
99
+ validateTimestamp(timestamp);
100
+ } catch (error) {
101
+ rejectHmac(res, error.message);
102
+ return false;
103
+ }
104
+
105
+ cleanupOldNonces();
106
+ if (state.nonceSeenAt.has(nonce)) {
107
+ rejectHmac(res, "Nonce reuse");
108
+ return false;
109
+ }
110
+
111
+ const expected = buildSignature(state.config.hmacSecret, timestamp, method, canonicalPath, nonce);
112
+ const expectedBuf = Buffer.from(expected);
113
+ const providedBuf = Buffer.from(signature);
114
+ if (expectedBuf.length !== providedBuf.length || !crypto.timingSafeEqual(expectedBuf, providedBuf)) {
115
+ rejectHmac(res, "Invalid signature");
116
+ return false;
117
+ }
118
+
119
+ state.nonceSeenAt.set(nonce, nowMs());
120
+ return true;
121
+ }
122
+
123
+ function getMetricsSnapshot() {
124
+ cleanupOldSamples();
125
+ const sampleCount = state.samples.length;
126
+ const windowSeconds = state.config.windowSeconds;
127
+ const totalErrors = state.samples.reduce((acc, item) => acc + (item.isError ? 1 : 0), 0);
128
+ const latencies = state.samples.map((item) => item.latencyMs);
129
+
130
+ const cpuUsageRaw = os.cpus().length > 0 ? os.loadavg()[0] / os.cpus().length : 0;
131
+ const memoryUsageRaw = os.totalmem() > 0 ? process.memoryUsage().rss / os.totalmem() : 0;
132
+
133
+ return {
134
+ requests_per_sec: round(sampleCount / windowSeconds, 3),
135
+ latency_p95: round(percentile95(latencies), 3),
136
+ error_rate: sampleCount > 0 ? round(totalErrors / sampleCount, 6) : 0,
137
+ cpu_usage: round(Math.min(Math.max(cpuUsageRaw, 0), 1), 6),
138
+ memory_usage: round(Math.min(Math.max(memoryUsageRaw, 0), 1), 6),
139
+ };
140
+ }
141
+
142
+ function init(config) {
143
+ if (!config || typeof config !== "object") {
144
+ throw new Error("seqpulse.init(config) requires a config object");
145
+ }
146
+ if (!config.apiKey || typeof config.apiKey !== "string") {
147
+ throw new Error("seqpulse.init requires apiKey");
148
+ }
149
+
150
+ const endpoint = canonicalizePath(config.endpoint || DEFAULT_ENDPOINT);
151
+ const windowSeconds = Number(config.windowSeconds || DEFAULT_WINDOW_SECONDS);
152
+ const hmacEnabled = Boolean(config.hmacEnabled);
153
+ const hmacSecret = String(config.hmacSecret || "");
154
+
155
+ if (!Number.isFinite(windowSeconds) || windowSeconds <= 0) {
156
+ throw new Error("windowSeconds must be a positive number");
157
+ }
158
+ if (hmacEnabled && !hmacSecret) {
159
+ throw new Error("hmacSecret is required when hmacEnabled=true");
160
+ }
161
+
162
+ state.config = {
163
+ apiKey: config.apiKey,
164
+ endpoint,
165
+ windowSeconds,
166
+ hmacEnabled,
167
+ hmacSecret,
168
+ maxSkewPastSeconds: Number(config.maxSkewPastSeconds || DEFAULT_MAX_SKEW_PAST_SECONDS),
169
+ maxSkewFutureSeconds: Number(config.maxSkewFutureSeconds || DEFAULT_MAX_SKEW_FUTURE_SECONDS),
170
+ };
171
+ state.samples = [];
172
+ state.nonceSeenAt = new Map();
173
+ state.initialized = true;
174
+ }
175
+
176
+ function metrics() {
177
+ return function seqpulseMiddleware(req, res, next) {
178
+ if (!state.initialized) {
179
+ throw new Error("seqpulse.init() must be called before seqpulse.metrics()");
180
+ }
181
+
182
+ const path = canonicalizePath(req.path || req.url || "/");
183
+ if (path === state.config.endpoint && (req.method || "GET").toUpperCase() === "GET") {
184
+ if (!validateHmacRequest(req, res)) return;
185
+ res.json({ metrics: getMetricsSnapshot() });
186
+ return;
187
+ }
188
+
189
+ const startedAt = process.hrtime.bigint();
190
+ res.on("finish", () => {
191
+ const latencyMs = Number(process.hrtime.bigint() - startedAt) / 1_000_000;
192
+ state.samples.push({
193
+ atMs: nowMs(),
194
+ latencyMs,
195
+ isError: res.statusCode >= 500,
196
+ });
197
+ cleanupOldSamples();
198
+ });
199
+ next();
200
+ };
201
+ }
202
+
203
+ module.exports = {
204
+ init,
205
+ metrics,
206
+ getMetricsSnapshot,
207
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "seqpulse",
3
+ "version": "0.1.0",
4
+ "description": "SeqPulse SDK for metrics endpoint instrumentation and HMAC validation",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "require": "./index.js",
10
+ "types": "./index.d.ts"
11
+ }
12
+ },
13
+ "files": [
14
+ "index.js",
15
+ "index.d.ts",
16
+ "README.md",
17
+ "LICENSE",
18
+ "scripts/smoke.js"
19
+ ],
20
+ "scripts": {
21
+ "smoke": "node scripts/smoke.js"
22
+ },
23
+ "keywords": [
24
+ "seqpulse",
25
+ "metrics",
26
+ "hmac",
27
+ "express"
28
+ ],
29
+ "author": "Nassir",
30
+ "license": "MIT",
31
+ "engines": {
32
+ "node": ">=18"
33
+ }
34
+ }
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+
3
+ const crypto = require("node:crypto");
4
+ const EventEmitter = require("node:events");
5
+ const sdk = require("../index");
6
+
7
+ class MockRes extends EventEmitter {
8
+ constructor() {
9
+ super();
10
+ this.statusCode = 200;
11
+ this.body = null;
12
+ }
13
+ status(code) {
14
+ this.statusCode = code;
15
+ return this;
16
+ }
17
+ json(payload) {
18
+ this.body = payload;
19
+ return this;
20
+ }
21
+ }
22
+
23
+ function mockReq({ path, method = "GET", headers = {} }) {
24
+ return {
25
+ path,
26
+ url: path,
27
+ method,
28
+ get(name) {
29
+ return headers[name] || headers[name.toLowerCase()] || "";
30
+ },
31
+ };
32
+ }
33
+
34
+ function runMiddleware(mw, req, res) {
35
+ return new Promise((resolve, reject) => {
36
+ try {
37
+ mw(req, res, () => resolve("next"));
38
+ } catch (err) {
39
+ reject(err);
40
+ }
41
+ });
42
+ }
43
+
44
+ async function testNoHmac() {
45
+ sdk.init({ apiKey: "sp_local", endpoint: "/seqpulse-metrics", hmacEnabled: false });
46
+ const mw = sdk.metrics();
47
+
48
+ const req1 = mockReq({ path: "/health" });
49
+ const res1 = new MockRes();
50
+ await runMiddleware(mw, req1, res1);
51
+ res1.statusCode = 200;
52
+ res1.emit("finish");
53
+
54
+ const req2 = mockReq({ path: "/seqpulse-metrics" });
55
+ const res2 = new MockRes();
56
+ mw(req2, res2, () => {});
57
+
58
+ if (res2.statusCode !== 200 || !res2.body || !res2.body.metrics) {
59
+ throw new Error("No-HMAC metrics endpoint failed");
60
+ }
61
+ }
62
+
63
+ async function testHmac() {
64
+ const secret = "local_hmac_secret";
65
+ sdk.init({
66
+ apiKey: "sp_local",
67
+ endpoint: "/seqpulse-metrics",
68
+ hmacEnabled: true,
69
+ hmacSecret: secret,
70
+ });
71
+ const mw = sdk.metrics();
72
+
73
+ const ts = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
74
+ const nonce = "nonce-smoke-1";
75
+ const path = "/seqpulse-metrics";
76
+ const payload = `${ts}|GET|${path}|${nonce}`;
77
+ const sig = `sha256=${crypto.createHmac("sha256", secret).update(payload).digest("hex")}`;
78
+ const headers = {
79
+ "X-SeqPulse-Timestamp": ts,
80
+ "X-SeqPulse-Nonce": nonce,
81
+ "X-SeqPulse-Signature": sig,
82
+ "X-SeqPulse-Signature-Version": "v2",
83
+ "X-SeqPulse-Canonical-Path": path,
84
+ "X-SeqPulse-Method": "GET",
85
+ };
86
+
87
+ const req = mockReq({ path, headers });
88
+ const res = new MockRes();
89
+ mw(req, res, () => {});
90
+ if (res.statusCode !== 200 || !res.body || !res.body.metrics) {
91
+ throw new Error("HMAC valid request failed");
92
+ }
93
+
94
+ const replayReq = mockReq({ path, headers });
95
+ const replayRes = new MockRes();
96
+ mw(replayReq, replayRes, () => {});
97
+ if (replayRes.statusCode !== 401) {
98
+ throw new Error("HMAC replay protection failed");
99
+ }
100
+ }
101
+
102
+ async function main() {
103
+ await testNoHmac();
104
+ await testHmac();
105
+ console.log("seqpulse node smoke: OK");
106
+ }
107
+
108
+ main().catch((err) => {
109
+ console.error(err);
110
+ process.exit(1);
111
+ });