@svrnsec/pulse 0.6.0 → 0.8.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 +21 -21
- package/README.md +883 -622
- package/SECURITY.md +86 -86
- package/bin/svrnsec-pulse.js +7 -7
- package/dist/{pulse.cjs.js → pulse.cjs} +6379 -6420
- package/dist/pulse.cjs.map +1 -0
- package/dist/pulse.esm.js +6380 -6421
- package/dist/pulse.esm.js.map +1 -1
- package/index.d.ts +895 -846
- package/package.json +185 -165
- package/pkg/pulse_core.js +174 -173
- package/src/analysis/audio.js +213 -213
- package/src/analysis/authenticityAudit.js +408 -390
- package/src/analysis/coherence.js +502 -502
- package/src/analysis/coordinatedBehavior.js +825 -0
- package/src/analysis/heuristic.js +428 -428
- package/src/analysis/jitter.js +446 -446
- package/src/analysis/llm.js +473 -472
- package/src/analysis/populationEntropy.js +404 -403
- package/src/analysis/provider.js +248 -248
- package/src/analysis/refraction.js +392 -0
- package/src/analysis/trustScore.js +356 -356
- package/src/cli/args.js +36 -36
- package/src/cli/commands/scan.js +192 -192
- package/src/cli/runner.js +157 -157
- package/src/collector/adaptive.js +200 -200
- package/src/collector/bio.js +297 -287
- package/src/collector/canvas.js +247 -239
- package/src/collector/dram.js +203 -203
- package/src/collector/enf.js +311 -311
- package/src/collector/entropy.js +195 -195
- package/src/collector/gpu.js +248 -245
- package/src/collector/idleAttestation.js +480 -480
- package/src/collector/sabTimer.js +189 -191
- package/src/fingerprint.js +475 -475
- package/src/index.js +342 -342
- package/src/integrations/react-native.js +462 -459
- package/src/integrations/react.js +184 -185
- package/src/middleware/express.js +155 -155
- package/src/middleware/next.js +174 -175
- package/src/proof/challenge.js +249 -249
- package/src/proof/engagementToken.js +426 -394
- package/src/proof/fingerprint.js +268 -268
- package/src/proof/validator.js +83 -143
- package/src/registry/serializer.js +349 -349
- package/src/terminal.js +263 -263
- package/src/update-notifier.js +259 -264
- package/dist/pulse.cjs.js.map +0 -1
|
@@ -1,155 +1,155 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @
|
|
3
|
-
*
|
|
4
|
-
* Drop-in middleware for Express / Fastify / Hono.
|
|
5
|
-
* Handles the full challenge → verify flow in two lines of code.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
*
|
|
9
|
-
* import { createPulseMiddleware } from '@
|
|
10
|
-
*
|
|
11
|
-
* const pulse = createPulseMiddleware({ threshold: 0.6 });
|
|
12
|
-
*
|
|
13
|
-
* app.get('/api/pulse/challenge', pulse.challenge);
|
|
14
|
-
* app.post('/checkout', pulse.verify, checkoutHandler);
|
|
15
|
-
*
|
|
16
|
-
* With Redis (recommended for production):
|
|
17
|
-
*
|
|
18
|
-
* import Redis from 'ioredis';
|
|
19
|
-
* const redis = new Redis(process.env.REDIS_URL);
|
|
20
|
-
*
|
|
21
|
-
* const pulse = createPulseMiddleware({
|
|
22
|
-
* threshold: 0.6,
|
|
23
|
-
* store: {
|
|
24
|
-
* set: (k, ttl) => redis.set(k, '1', 'EX', ttl),
|
|
25
|
-
* consume: (k) => redis.del(k).then(n => n === 1),
|
|
26
|
-
* },
|
|
27
|
-
* });
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import { validateProof, generateNonce } from '../proof/validator.js';
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// In-memory nonce store (single-process / development only)
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
function createMemoryStore(ttlMs = 300_000) {
|
|
37
|
-
const store = new Map();
|
|
38
|
-
return {
|
|
39
|
-
set(key) {
|
|
40
|
-
store.set(key, Date.now() + ttlMs);
|
|
41
|
-
// Lazy cleanup — don't leak memory in long-running processes
|
|
42
|
-
if (store.size > 10_000) {
|
|
43
|
-
const now = Date.now();
|
|
44
|
-
for (const [k, exp] of store) {
|
|
45
|
-
if (exp < now) store.delete(k);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
consume(key) {
|
|
50
|
-
const exp = store.get(key);
|
|
51
|
-
if (!exp || Date.now() > exp) return false;
|
|
52
|
-
store.delete(key);
|
|
53
|
-
return true;
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
// createPulseMiddleware
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* @param {object} opts
|
|
64
|
-
* @param {number} [opts.threshold=0.55] - minimum jitter score (0–1)
|
|
65
|
-
* @param {number} [opts.nonceTTL=300] - nonce lifetime in seconds
|
|
66
|
-
* @param {boolean} [opts.requireBio=false] - reject if no mouse/keyboard activity
|
|
67
|
-
* @param {boolean} [opts.blockSoftwareRenderer=true]
|
|
68
|
-
* @param {object} [opts.store] - custom nonce store (see above)
|
|
69
|
-
* @param {string} [opts.proofHeader='x-pulse-proof'] - request header name
|
|
70
|
-
* @param {string} [opts.hashHeader='x-pulse-hash']
|
|
71
|
-
* @param {Function} [opts.onReject] - custom rejection handler
|
|
72
|
-
* @param {Function} [opts.onError] - custom error handler
|
|
73
|
-
* @returns {{ challenge: Function, verify: Function }}
|
|
74
|
-
*/
|
|
75
|
-
export function createPulseMiddleware(opts = {}) {
|
|
76
|
-
const {
|
|
77
|
-
threshold = 0.55,
|
|
78
|
-
nonceTTL = 300,
|
|
79
|
-
requireBio = false,
|
|
80
|
-
blockSoftwareRenderer = true,
|
|
81
|
-
proofHeader = 'x-pulse-proof',
|
|
82
|
-
hashHeader = 'x-pulse-hash',
|
|
83
|
-
onReject,
|
|
84
|
-
onError,
|
|
85
|
-
} = opts;
|
|
86
|
-
|
|
87
|
-
// Allow external store (Redis, etc.) or default to in-memory
|
|
88
|
-
const store = opts.store ?? createMemoryStore(nonceTTL * 1000);
|
|
89
|
-
|
|
90
|
-
// ── challenge — GET /api/pulse/challenge ──────────────────────────────────
|
|
91
|
-
async function challenge(req, res) {
|
|
92
|
-
try {
|
|
93
|
-
const nonce = generateNonce();
|
|
94
|
-
await store.set(`pulse:${nonce}`);
|
|
95
|
-
res.json({ nonce, expiresIn: nonceTTL });
|
|
96
|
-
} catch (err) {
|
|
97
|
-
if (onError) return onError(err, req, res);
|
|
98
|
-
res.status(500).json({ error: 'Failed to generate challenge' });
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ── verify — middleware for protected routes ───────────────────────────────
|
|
103
|
-
async function verify(req, res, next) {
|
|
104
|
-
try {
|
|
105
|
-
// Support both header and body delivery
|
|
106
|
-
let payload, hash;
|
|
107
|
-
if (req.headers[proofHeader]) {
|
|
108
|
-
try { payload = JSON.parse(req.headers[proofHeader]); }
|
|
109
|
-
catch { return _reject(res, 400, 'MALFORMED_PROOF_HEADER', 'Could not parse x-pulse-proof header as JSON', onReject, req); }
|
|
110
|
-
hash = req.headers[hashHeader];
|
|
111
|
-
} else if (req.body?.pulsePayload) {
|
|
112
|
-
payload = req.body.pulsePayload;
|
|
113
|
-
hash = req.body.pulseHash;
|
|
114
|
-
} else {
|
|
115
|
-
return _reject(res, 401, 'MISSING_PROOF', 'No pulse proof found in headers or body', onReject, req);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (!hash) {
|
|
119
|
-
return _reject(res, 401, 'MISSING_HASH', 'No pulse hash provided', onReject, req);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const result = await validateProof(payload, hash, {
|
|
123
|
-
minJitterScore: threshold,
|
|
124
|
-
requireBio,
|
|
125
|
-
blockSoftwareRenderer,
|
|
126
|
-
checkNonce: async (n) => store.consume(`pulse:${n}`),
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
if (!result.valid) {
|
|
130
|
-
return _reject(res, 403, 'PROOF_INVALID', result.reasons.join('; '), onReject, req, result);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Attach result to request for downstream handlers
|
|
134
|
-
req.pulse = result;
|
|
135
|
-
next();
|
|
136
|
-
|
|
137
|
-
} catch (err) {
|
|
138
|
-
if (onError) return onError(err, req, res, next);
|
|
139
|
-
res.status(500).json({ error: 'Pulse verification error' });
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return { challenge, verify };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ---------------------------------------------------------------------------
|
|
147
|
-
// Internal helpers
|
|
148
|
-
// ---------------------------------------------------------------------------
|
|
149
|
-
|
|
150
|
-
function _reject(res, status, code, message, customHandler, req, result = {}) {
|
|
151
|
-
if (customHandler) {
|
|
152
|
-
return customHandler(req, res, { code, message, ...result });
|
|
153
|
-
}
|
|
154
|
-
res.status(status).json({ error: code, message,
|
|
155
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @svrnsec/pulse — Express Middleware
|
|
3
|
+
*
|
|
4
|
+
* Drop-in middleware for Express / Fastify / Hono.
|
|
5
|
+
* Handles the full challenge → verify flow in two lines of code.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
*
|
|
9
|
+
* import { createPulseMiddleware } from '@svrnsec/pulse/middleware/express';
|
|
10
|
+
*
|
|
11
|
+
* const pulse = createPulseMiddleware({ threshold: 0.6 });
|
|
12
|
+
*
|
|
13
|
+
* app.get('/api/pulse/challenge', pulse.challenge);
|
|
14
|
+
* app.post('/checkout', pulse.verify, checkoutHandler);
|
|
15
|
+
*
|
|
16
|
+
* With Redis (recommended for production):
|
|
17
|
+
*
|
|
18
|
+
* import Redis from 'ioredis';
|
|
19
|
+
* const redis = new Redis(process.env.REDIS_URL);
|
|
20
|
+
*
|
|
21
|
+
* const pulse = createPulseMiddleware({
|
|
22
|
+
* threshold: 0.6,
|
|
23
|
+
* store: {
|
|
24
|
+
* set: (k, ttl) => redis.set(k, '1', 'EX', ttl),
|
|
25
|
+
* consume: (k) => redis.del(k).then(n => n === 1),
|
|
26
|
+
* },
|
|
27
|
+
* });
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { validateProof, generateNonce } from '../proof/validator.js';
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// In-memory nonce store (single-process / development only)
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
function createMemoryStore(ttlMs = 300_000) {
|
|
37
|
+
const store = new Map();
|
|
38
|
+
return {
|
|
39
|
+
set(key) {
|
|
40
|
+
store.set(key, Date.now() + ttlMs);
|
|
41
|
+
// Lazy cleanup — don't leak memory in long-running processes
|
|
42
|
+
if (store.size > 10_000) {
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
for (const [k, exp] of store) {
|
|
45
|
+
if (exp < now) store.delete(k);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
consume(key) {
|
|
50
|
+
const exp = store.get(key);
|
|
51
|
+
if (!exp || Date.now() > exp) return false;
|
|
52
|
+
store.delete(key);
|
|
53
|
+
return true;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// createPulseMiddleware
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {object} opts
|
|
64
|
+
* @param {number} [opts.threshold=0.55] - minimum jitter score (0–1)
|
|
65
|
+
* @param {number} [opts.nonceTTL=300] - nonce lifetime in seconds
|
|
66
|
+
* @param {boolean} [opts.requireBio=false] - reject if no mouse/keyboard activity
|
|
67
|
+
* @param {boolean} [opts.blockSoftwareRenderer=true]
|
|
68
|
+
* @param {object} [opts.store] - custom nonce store (see above)
|
|
69
|
+
* @param {string} [opts.proofHeader='x-pulse-proof'] - request header name
|
|
70
|
+
* @param {string} [opts.hashHeader='x-pulse-hash']
|
|
71
|
+
* @param {Function} [opts.onReject] - custom rejection handler
|
|
72
|
+
* @param {Function} [opts.onError] - custom error handler
|
|
73
|
+
* @returns {{ challenge: Function, verify: Function }}
|
|
74
|
+
*/
|
|
75
|
+
export function createPulseMiddleware(opts = {}) {
|
|
76
|
+
const {
|
|
77
|
+
threshold = 0.55,
|
|
78
|
+
nonceTTL = 300,
|
|
79
|
+
requireBio = false,
|
|
80
|
+
blockSoftwareRenderer = true,
|
|
81
|
+
proofHeader = 'x-pulse-proof',
|
|
82
|
+
hashHeader = 'x-pulse-hash',
|
|
83
|
+
onReject,
|
|
84
|
+
onError,
|
|
85
|
+
} = opts;
|
|
86
|
+
|
|
87
|
+
// Allow external store (Redis, etc.) or default to in-memory
|
|
88
|
+
const store = opts.store ?? createMemoryStore(nonceTTL * 1000);
|
|
89
|
+
|
|
90
|
+
// ── challenge — GET /api/pulse/challenge ──────────────────────────────────
|
|
91
|
+
async function challenge(req, res) {
|
|
92
|
+
try {
|
|
93
|
+
const nonce = generateNonce();
|
|
94
|
+
await store.set(`pulse:${nonce}`);
|
|
95
|
+
res.json({ nonce, expiresIn: nonceTTL });
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (onError) return onError(err, req, res);
|
|
98
|
+
res.status(500).json({ error: 'Failed to generate challenge' });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── verify — middleware for protected routes ───────────────────────────────
|
|
103
|
+
async function verify(req, res, next) {
|
|
104
|
+
try {
|
|
105
|
+
// Support both header and body delivery
|
|
106
|
+
let payload, hash;
|
|
107
|
+
if (req.headers[proofHeader]) {
|
|
108
|
+
try { payload = JSON.parse(req.headers[proofHeader]); }
|
|
109
|
+
catch { return _reject(res, 400, 'MALFORMED_PROOF_HEADER', 'Could not parse x-pulse-proof header as JSON', onReject, req); }
|
|
110
|
+
hash = req.headers[hashHeader];
|
|
111
|
+
} else if (req.body?.pulsePayload) {
|
|
112
|
+
payload = req.body.pulsePayload;
|
|
113
|
+
hash = req.body.pulseHash;
|
|
114
|
+
} else {
|
|
115
|
+
return _reject(res, 401, 'MISSING_PROOF', 'No pulse proof found in headers or body', onReject, req);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!hash) {
|
|
119
|
+
return _reject(res, 401, 'MISSING_HASH', 'No pulse hash provided', onReject, req);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const result = await validateProof(payload, hash, {
|
|
123
|
+
minJitterScore: threshold,
|
|
124
|
+
requireBio,
|
|
125
|
+
blockSoftwareRenderer,
|
|
126
|
+
checkNonce: async (n) => store.consume(`pulse:${n}`),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (!result.valid) {
|
|
130
|
+
return _reject(res, 403, 'PROOF_INVALID', result.reasons.join('; '), onReject, req, result);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Attach result to request for downstream handlers
|
|
134
|
+
req.pulse = result;
|
|
135
|
+
next();
|
|
136
|
+
|
|
137
|
+
} catch (err) {
|
|
138
|
+
if (onError) return onError(err, req, res, next);
|
|
139
|
+
res.status(500).json({ error: 'Pulse verification error' });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { challenge, verify };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Internal helpers
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
function _reject(res, status, code, message, customHandler, req, result = {}) {
|
|
151
|
+
if (customHandler) {
|
|
152
|
+
return customHandler(req, res, { code, message, ...result });
|
|
153
|
+
}
|
|
154
|
+
res.status(status).json({ error: code, message, valid: result?.valid, reasons: result?.reasons, riskFlags: result?.riskFlags });
|
|
155
|
+
}
|