@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
package/src/middleware/next.js
CHANGED
|
@@ -1,175 +1,174 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @
|
|
3
|
-
*
|
|
4
|
-
* Works with Next.js App Router (13+) and Edge Runtime.
|
|
5
|
-
*
|
|
6
|
-
* ── Route Handler wrapper ──────────────────────────────────────────────────
|
|
7
|
-
*
|
|
8
|
-
* // app/api/checkout/route.js
|
|
9
|
-
* import { withPulse } from '@
|
|
10
|
-
*
|
|
11
|
-
* export const POST = withPulse({ threshold: 0.6 })(
|
|
12
|
-
* async (req) => {
|
|
13
|
-
* const { score, provider } = req.pulse;
|
|
14
|
-
* return Response.json({ ok: true, score });
|
|
15
|
-
* }
|
|
16
|
-
* );
|
|
17
|
-
*
|
|
18
|
-
* ── Challenge endpoint (copy-paste ready) ─────────────────────────────────
|
|
19
|
-
*
|
|
20
|
-
* // app/api/pulse/challenge/route.js
|
|
21
|
-
* import { pulseChallenge } from '@
|
|
22
|
-
* export const GET = pulseChallenge();
|
|
23
|
-
*
|
|
24
|
-
* ── Edge-compatible nonce store ────────────────────────────────────────────
|
|
25
|
-
*
|
|
26
|
-
* // Uses in-memory by default. For multi-instance deployments, provide
|
|
27
|
-
* // a KV store (Vercel KV, Cloudflare KV, Redis via fetch):
|
|
28
|
-
*
|
|
29
|
-
* import { kv } from '@vercel/kv';
|
|
30
|
-
* export const POST = withPulse({
|
|
31
|
-
* threshold: 0.6,
|
|
32
|
-
* store: {
|
|
33
|
-
* set: (k, ttl) => kv.set(k, '1', { ex: ttl }),
|
|
34
|
-
* consume: (k) => kv.del(k).then(n => n === 1),
|
|
35
|
-
* },
|
|
36
|
-
* })(handler);
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
import { validateProof, generateNonce } from '../proof/validator.js';
|
|
40
|
-
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
// Shared in-memory nonce store (single instance / dev only)
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
* @param {
|
|
67
|
-
* @param {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
* @param {
|
|
87
|
-
* @param {number} [opts.
|
|
88
|
-
* @param {
|
|
89
|
-
* @param {boolean} [opts.
|
|
90
|
-
* @param {
|
|
91
|
-
* @
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
'
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
{
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @svrnsec/pulse — Next.js App Router Middleware
|
|
3
|
+
*
|
|
4
|
+
* Works with Next.js App Router (13+) and Edge Runtime.
|
|
5
|
+
*
|
|
6
|
+
* ── Route Handler wrapper ──────────────────────────────────────────────────
|
|
7
|
+
*
|
|
8
|
+
* // app/api/checkout/route.js
|
|
9
|
+
* import { withPulse } from '@svrnsec/pulse/middleware/next';
|
|
10
|
+
*
|
|
11
|
+
* export const POST = withPulse({ threshold: 0.6 })(
|
|
12
|
+
* async (req) => {
|
|
13
|
+
* const { score, provider } = req.pulse;
|
|
14
|
+
* return Response.json({ ok: true, score });
|
|
15
|
+
* }
|
|
16
|
+
* );
|
|
17
|
+
*
|
|
18
|
+
* ── Challenge endpoint (copy-paste ready) ─────────────────────────────────
|
|
19
|
+
*
|
|
20
|
+
* // app/api/pulse/challenge/route.js
|
|
21
|
+
* import { pulseChallenge } from '@svrnsec/pulse/middleware/next';
|
|
22
|
+
* export const GET = pulseChallenge();
|
|
23
|
+
*
|
|
24
|
+
* ── Edge-compatible nonce store ────────────────────────────────────────────
|
|
25
|
+
*
|
|
26
|
+
* // Uses in-memory by default. For multi-instance deployments, provide
|
|
27
|
+
* // a KV store (Vercel KV, Cloudflare KV, Redis via fetch):
|
|
28
|
+
*
|
|
29
|
+
* import { kv } from '@vercel/kv';
|
|
30
|
+
* export const POST = withPulse({
|
|
31
|
+
* threshold: 0.6,
|
|
32
|
+
* store: {
|
|
33
|
+
* set: (k, ttl) => kv.set(k, '1', { ex: ttl }),
|
|
34
|
+
* consume: (k) => kv.del(k).then(n => n === 1),
|
|
35
|
+
* },
|
|
36
|
+
* })(handler);
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { validateProof, generateNonce } from '../proof/validator.js';
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Shared in-memory nonce store (single instance / dev only)
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
function memoryStore(ttlSec) {
|
|
46
|
+
const _memStore = new Map();
|
|
47
|
+
return {
|
|
48
|
+
set(key) {
|
|
49
|
+
_memStore.set(key, Date.now() + ttlSec * 1000);
|
|
50
|
+
},
|
|
51
|
+
consume(key) {
|
|
52
|
+
const exp = _memStore.get(key);
|
|
53
|
+
if (!exp || Date.now() > exp) return false;
|
|
54
|
+
_memStore.delete(key);
|
|
55
|
+
return true;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// pulseChallenge — GET /api/pulse/challenge
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {object} [opts]
|
|
66
|
+
* @param {number} [opts.ttl=300] - nonce TTL in seconds
|
|
67
|
+
* @param {object} [opts.store] - custom nonce store
|
|
68
|
+
*/
|
|
69
|
+
export function pulseChallenge(opts = {}) {
|
|
70
|
+
const { ttl = 300, store } = opts;
|
|
71
|
+
const _store = store ?? memoryStore(ttl);
|
|
72
|
+
|
|
73
|
+
return async function GET() {
|
|
74
|
+
const nonce = generateNonce();
|
|
75
|
+
await _store.set(`pulse:${nonce}`);
|
|
76
|
+
return Response.json({ nonce, expiresIn: ttl });
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// withPulse — wraps a Next.js route handler
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {object} opts
|
|
86
|
+
* @param {number} [opts.threshold=0.55]
|
|
87
|
+
* @param {number} [opts.ttl=300] - nonce TTL
|
|
88
|
+
* @param {boolean} [opts.requireBio=false]
|
|
89
|
+
* @param {boolean} [opts.blockSoftwareRenderer=true]
|
|
90
|
+
* @param {object} [opts.store] - custom nonce store
|
|
91
|
+
* @returns {(handler: Function) => Function} - HOC
|
|
92
|
+
*/
|
|
93
|
+
export function withPulse(opts = {}) {
|
|
94
|
+
const {
|
|
95
|
+
threshold = 0.55,
|
|
96
|
+
ttl = 300,
|
|
97
|
+
requireBio = false,
|
|
98
|
+
blockSoftwareRenderer = true,
|
|
99
|
+
store,
|
|
100
|
+
} = opts;
|
|
101
|
+
|
|
102
|
+
const _store = store ?? memoryStore(ttl);
|
|
103
|
+
|
|
104
|
+
return function wrap(handler) {
|
|
105
|
+
return async function wrappedHandler(req, ...args) {
|
|
106
|
+
// ── Read proof from headers (preferred) or body ──────────────────────
|
|
107
|
+
const proofHeader = req.headers.get('x-pulse-proof');
|
|
108
|
+
const hashHeader = req.headers.get('x-pulse-hash');
|
|
109
|
+
|
|
110
|
+
let payload, hash;
|
|
111
|
+
|
|
112
|
+
if (proofHeader) {
|
|
113
|
+
try { payload = JSON.parse(proofHeader); }
|
|
114
|
+
catch { return _err(400, 'MALFORMED_PROOF', 'Could not parse x-pulse-proof header'); }
|
|
115
|
+
hash = hashHeader;
|
|
116
|
+
} else {
|
|
117
|
+
// Attempt to read from JSON body (non-streaming)
|
|
118
|
+
try {
|
|
119
|
+
const body = await req.clone().json();
|
|
120
|
+
payload = body.pulsePayload;
|
|
121
|
+
hash = body.pulseHash;
|
|
122
|
+
} catch {}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!payload || !hash) {
|
|
126
|
+
return _err(401, 'MISSING_PROOF',
|
|
127
|
+
'Provide pulse proof via x-pulse-proof + x-pulse-hash headers, ' +
|
|
128
|
+
'or pulsePayload + pulseHash in the request body.'
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Validate ───────────────────────────────────────────────────────────
|
|
133
|
+
let result;
|
|
134
|
+
try {
|
|
135
|
+
result = await validateProof(payload, hash, {
|
|
136
|
+
minJitterScore: threshold,
|
|
137
|
+
requireBio,
|
|
138
|
+
blockSoftwareRenderer,
|
|
139
|
+
checkNonce: async (n) => _store.consume(`pulse:${n}`),
|
|
140
|
+
});
|
|
141
|
+
} catch (err) {
|
|
142
|
+
console.error('[pulse] validateProof error:', err);
|
|
143
|
+
return _err(500, 'VALIDATION_ERROR', 'Internal error during proof validation');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!result.valid) {
|
|
147
|
+
return Response.json(
|
|
148
|
+
{ error: 'PULSE_REJECTED', reasons: result.reasons, riskFlags: result.riskFlags },
|
|
149
|
+
{ status: 403 }
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── Attach pulse result and call the real handler ──────────────────────
|
|
154
|
+
// Next.js Request is immutable so we inject via a lightweight proxy
|
|
155
|
+
const enriched = new Proxy(req, {
|
|
156
|
+
get(target, prop) {
|
|
157
|
+
if (prop === 'pulse') return result;
|
|
158
|
+
const val = target[prop];
|
|
159
|
+
return typeof val === 'function' ? val.bind(target) : val;
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return handler(enriched, ...args);
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Internal helpers
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
function _err(status, code, message) {
|
|
173
|
+
return Response.json({ error: code, message }, { status });
|
|
174
|
+
}
|