@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.
Files changed (48) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +883 -622
  3. package/SECURITY.md +86 -86
  4. package/bin/svrnsec-pulse.js +7 -7
  5. package/dist/{pulse.cjs.js → pulse.cjs} +6379 -6420
  6. package/dist/pulse.cjs.map +1 -0
  7. package/dist/pulse.esm.js +6380 -6421
  8. package/dist/pulse.esm.js.map +1 -1
  9. package/index.d.ts +895 -846
  10. package/package.json +185 -165
  11. package/pkg/pulse_core.js +174 -173
  12. package/src/analysis/audio.js +213 -213
  13. package/src/analysis/authenticityAudit.js +408 -390
  14. package/src/analysis/coherence.js +502 -502
  15. package/src/analysis/coordinatedBehavior.js +825 -0
  16. package/src/analysis/heuristic.js +428 -428
  17. package/src/analysis/jitter.js +446 -446
  18. package/src/analysis/llm.js +473 -472
  19. package/src/analysis/populationEntropy.js +404 -403
  20. package/src/analysis/provider.js +248 -248
  21. package/src/analysis/refraction.js +392 -0
  22. package/src/analysis/trustScore.js +356 -356
  23. package/src/cli/args.js +36 -36
  24. package/src/cli/commands/scan.js +192 -192
  25. package/src/cli/runner.js +157 -157
  26. package/src/collector/adaptive.js +200 -200
  27. package/src/collector/bio.js +297 -287
  28. package/src/collector/canvas.js +247 -239
  29. package/src/collector/dram.js +203 -203
  30. package/src/collector/enf.js +311 -311
  31. package/src/collector/entropy.js +195 -195
  32. package/src/collector/gpu.js +248 -245
  33. package/src/collector/idleAttestation.js +480 -480
  34. package/src/collector/sabTimer.js +189 -191
  35. package/src/fingerprint.js +475 -475
  36. package/src/index.js +342 -342
  37. package/src/integrations/react-native.js +462 -459
  38. package/src/integrations/react.js +184 -185
  39. package/src/middleware/express.js +155 -155
  40. package/src/middleware/next.js +174 -175
  41. package/src/proof/challenge.js +249 -249
  42. package/src/proof/engagementToken.js +426 -394
  43. package/src/proof/fingerprint.js +268 -268
  44. package/src/proof/validator.js +83 -143
  45. package/src/registry/serializer.js +349 -349
  46. package/src/terminal.js +263 -263
  47. package/src/update-notifier.js +259 -264
  48. package/dist/pulse.cjs.js.map +0 -1
@@ -1,155 +1,155 @@
1
- /**
2
- * @sovereign/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 '@sovereign/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, ...result });
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
+ }