@stvor/sdk 2.4.0 → 3.0.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/dist/facade/app.cjs +29 -0
- package/dist/facade/app.d.ts +83 -76
- package/dist/facade/app.js +330 -195
- package/dist/facade/crypto-session.cjs +29 -0
- package/dist/facade/crypto-session.d.ts +49 -54
- package/dist/facade/crypto-session.js +117 -140
- package/dist/facade/errors.cjs +29 -0
- package/dist/facade/errors.d.ts +29 -12
- package/dist/facade/errors.js +49 -8
- package/dist/facade/index.cjs +29 -0
- package/dist/facade/index.d.ts +27 -8
- package/dist/facade/index.js +23 -3
- package/dist/facade/local-storage-identity-store.cjs +29 -0
- package/dist/facade/local-storage-identity-store.d.ts +50 -0
- package/dist/facade/local-storage-identity-store.js +100 -0
- package/dist/facade/metrics-attestation.cjs +29 -0
- package/dist/facade/metrics-attestation.d.ts +209 -0
- package/dist/facade/metrics-attestation.js +333 -0
- package/dist/facade/metrics-engine.cjs +29 -0
- package/dist/facade/metrics-engine.d.ts +91 -0
- package/dist/facade/metrics-engine.js +170 -0
- package/dist/facade/redis-replay-cache.cjs +29 -0
- package/dist/facade/redis-replay-cache.d.ts +88 -0
- package/dist/facade/redis-replay-cache.js +60 -0
- package/dist/facade/relay-client.cjs +29 -0
- package/dist/facade/relay-client.d.ts +22 -23
- package/dist/facade/relay-client.js +107 -128
- package/dist/facade/replay-manager.cjs +29 -0
- package/dist/facade/replay-manager.d.ts +28 -35
- package/dist/facade/replay-manager.js +102 -69
- package/dist/facade/sodium-singleton.cjs +29 -0
- package/dist/facade/tofu-manager.cjs +29 -0
- package/dist/facade/tofu-manager.d.ts +38 -36
- package/dist/facade/tofu-manager.js +109 -77
- package/dist/facade/types.cjs +29 -0
- package/dist/facade/types.d.ts +2 -0
- package/dist/index.cjs +29 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -0
- package/dist/legacy.cjs +29 -0
- package/dist/legacy.d.ts +31 -1
- package/dist/legacy.js +90 -2
- package/dist/ratchet/core-production.cjs +29 -0
- package/dist/ratchet/core-production.d.ts +95 -0
- package/dist/ratchet/core-production.js +286 -0
- package/dist/ratchet/index.cjs +29 -0
- package/dist/ratchet/index.d.ts +49 -78
- package/dist/ratchet/index.js +313 -288
- package/dist/ratchet/key-recovery.cjs +29 -0
- package/dist/ratchet/replay-protection.cjs +29 -0
- package/dist/ratchet/tofu.cjs +29 -0
- package/dist/src/facade/app.cjs +29 -0
- package/dist/src/facade/app.d.ts +105 -0
- package/dist/src/facade/app.js +245 -0
- package/dist/src/facade/crypto.cjs +29 -0
- package/dist/src/facade/errors.cjs +29 -0
- package/dist/src/facade/errors.d.ts +19 -0
- package/dist/src/facade/errors.js +21 -0
- package/dist/src/facade/index.cjs +29 -0
- package/dist/src/facade/index.d.ts +8 -0
- package/dist/src/facade/index.js +5 -0
- package/dist/src/facade/relay-client.cjs +29 -0
- package/dist/src/facade/relay-client.d.ts +36 -0
- package/dist/src/facade/relay-client.js +154 -0
- package/dist/src/facade/types.cjs +29 -0
- package/dist/src/facade/types.d.ts +50 -0
- package/dist/src/facade/types.js +4 -0
- package/dist/src/index.cjs +29 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/legacy.cjs +29 -0
- package/dist/src/legacy.d.ts +0 -0
- package/dist/src/legacy.js +1 -0
- package/dist/src/mock-relay-server.cjs +29 -0
- package/dist/src/mock-relay-server.d.ts +30 -0
- package/dist/src/mock-relay-server.js +236 -0
- package/package.json +37 -11
- package/dist/ratchet/tests/ratchet.test.d.ts +0 -1
- package/dist/ratchet/tests/ratchet.test.js +0 -160
- /package/dist/{facade → src/facade}/crypto.d.ts +0 -0
- /package/dist/{facade → src/facade}/crypto.js +0 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STVOR v2.4.0 - Metrics Attestation Engine
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ CRITICAL SECURITY MODEL:
|
|
5
|
+
*
|
|
6
|
+
* This module ONLY records real E2EE events and creates attestations.
|
|
7
|
+
* It does NOT verify attestations (that's backend's job).
|
|
8
|
+
*
|
|
9
|
+
* Trust boundary:
|
|
10
|
+
* ┌─────────────────────────────────────────┐
|
|
11
|
+
* │ SDK (Trusted) - Record + Sign │
|
|
12
|
+
* │ ┌─────────────────────────────────────┐ │
|
|
13
|
+
* │ │ MetricsAttestationEngine │ │
|
|
14
|
+
* │ └─────────────────────────────────────┘ │
|
|
15
|
+
* └──────────────┬──────────────────────────┘
|
|
16
|
+
* │ POST /api/metrics/attest
|
|
17
|
+
* ▼
|
|
18
|
+
* ┌─────────────────────────────────────────┐
|
|
19
|
+
* │ BACKEND (Trusted) - Verify + Store │
|
|
20
|
+
* │ ┌─────────────────────────────────────┐ │
|
|
21
|
+
* │ │ MetricsVerificationService │ │
|
|
22
|
+
* │ │ - Check signature │ │
|
|
23
|
+
* │ │ - Check monotonicity │ │
|
|
24
|
+
* │ │ - Check anti-replay │ │
|
|
25
|
+
* │ └─────────────────────────────────────┘ │
|
|
26
|
+
* └──────────────┬──────────────────────────┘
|
|
27
|
+
* │ Verified metrics in DB
|
|
28
|
+
* ▼
|
|
29
|
+
* ┌─────────────────────────────────────────┐
|
|
30
|
+
* │ Dashboard (Untrusted) - Display Only │
|
|
31
|
+
* │ - No crypto verification in browser │
|
|
32
|
+
* │ - No calculations │
|
|
33
|
+
* │ - No fallback numbers │
|
|
34
|
+
* │ - Only: fetch from /api/metrics │
|
|
35
|
+
* └─────────────────────────────────────────┘
|
|
36
|
+
*/
|
|
37
|
+
import { createHmac, randomBytes } from 'crypto';
|
|
38
|
+
/**
|
|
39
|
+
* MetricsAttestationEngine
|
|
40
|
+
*
|
|
41
|
+
* RESPONSIBILITY: Record real events + create attestations
|
|
42
|
+
* NOT RESPONSIBLE: Verify attestations (backend does that)
|
|
43
|
+
*/
|
|
44
|
+
export class MetricsAttestationEngine {
|
|
45
|
+
constructor(appToken) {
|
|
46
|
+
this.sequenceNumber = 0;
|
|
47
|
+
// Initialize raw metrics
|
|
48
|
+
this.metrics = {
|
|
49
|
+
messagesEncrypted: 0,
|
|
50
|
+
messagesDecrypted: 0,
|
|
51
|
+
messagesRejected: 0,
|
|
52
|
+
replayAttempts: 0,
|
|
53
|
+
authFailures: 0,
|
|
54
|
+
};
|
|
55
|
+
// Generate session ID (unique per SDK instance)
|
|
56
|
+
this.sessionId = this.generateSessionId();
|
|
57
|
+
// Derive attestation key from appToken
|
|
58
|
+
// This key is NEVER used for crypto operations in browser
|
|
59
|
+
// It's sent to backend along with metrics for verification
|
|
60
|
+
this.attestationKey = this.deriveAttestationKey(appToken);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Record real event: Successful encryption with AEAD
|
|
64
|
+
* INVARIANT: Only called after cryptoSession.encryptForPeer() succeeds
|
|
65
|
+
*/
|
|
66
|
+
recordMessageEncrypted() {
|
|
67
|
+
this.metrics.messagesEncrypted++;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Record real event: Successful decryption with AAD verification
|
|
71
|
+
* INVARIANT: Only called after cryptoSession.decryptFromPeer() succeeds
|
|
72
|
+
*/
|
|
73
|
+
recordMessageDecrypted() {
|
|
74
|
+
this.metrics.messagesDecrypted++;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Record real event: AAD verification failed (auth failure)
|
|
78
|
+
* INVARIANT: Only called when AEAD auth tag is invalid
|
|
79
|
+
*/
|
|
80
|
+
recordMessageRejected() {
|
|
81
|
+
this.metrics.messagesRejected++;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Record real event: Replay attack detected
|
|
85
|
+
* INVARIANT: Only called when nonce is duplicate
|
|
86
|
+
*/
|
|
87
|
+
recordReplayAttempt() {
|
|
88
|
+
this.metrics.replayAttempts++;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Record real event: Signature verification failed
|
|
92
|
+
* INVARIANT: Only called on crypto auth failure
|
|
93
|
+
*/
|
|
94
|
+
recordAuthFailure() {
|
|
95
|
+
this.metrics.authFailures++;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Create attestation that can be sent to backend
|
|
99
|
+
*
|
|
100
|
+
* Backend MUST verify:
|
|
101
|
+
* - Signature is valid
|
|
102
|
+
* - Metrics are monotonic
|
|
103
|
+
* - Timestamp is within acceptable window
|
|
104
|
+
* - sessionId matches
|
|
105
|
+
* - sequenceNumber hasn't been seen before
|
|
106
|
+
*/
|
|
107
|
+
createAttestation() {
|
|
108
|
+
const attestationId = this.generateAttestationId();
|
|
109
|
+
const timestamp = Date.now();
|
|
110
|
+
const attestation = {
|
|
111
|
+
metrics: { ...this.metrics },
|
|
112
|
+
attestationId,
|
|
113
|
+
timestamp,
|
|
114
|
+
sessionId: this.sessionId,
|
|
115
|
+
sequenceNumber: this.sequenceNumber++,
|
|
116
|
+
proof: '', // Will be filled in next
|
|
117
|
+
};
|
|
118
|
+
// Create proof that backend can verify
|
|
119
|
+
const proof = this.createProof(attestation);
|
|
120
|
+
attestation.proof = proof;
|
|
121
|
+
return attestation;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get current metrics snapshot (immutable)
|
|
125
|
+
* Used for monitoring/debugging, NOT for Dashboard display
|
|
126
|
+
*/
|
|
127
|
+
getMetrics() {
|
|
128
|
+
return Object.freeze({ ...this.metrics });
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Internal: Create proof for attestation
|
|
132
|
+
*
|
|
133
|
+
* Format:
|
|
134
|
+
* proof = HMAC-SHA256(
|
|
135
|
+
* JSON.stringify({metrics, sessionId, sequenceNumber, timestamp}),
|
|
136
|
+
* attestationKey
|
|
137
|
+
* )
|
|
138
|
+
*
|
|
139
|
+
* Backend will recompute this with the appToken sent by SDK.
|
|
140
|
+
* If proof matches, backend knows:
|
|
141
|
+
* - Metrics came from this SDK instance
|
|
142
|
+
* - Metrics haven't been tampered
|
|
143
|
+
* - This is the correct sequence number
|
|
144
|
+
*/
|
|
145
|
+
createProof(attestation) {
|
|
146
|
+
const payload = JSON.stringify({
|
|
147
|
+
metrics: attestation.metrics,
|
|
148
|
+
sessionId: attestation.sessionId,
|
|
149
|
+
sequenceNumber: attestation.sequenceNumber,
|
|
150
|
+
timestamp: attestation.timestamp,
|
|
151
|
+
attestationId: attestation.attestationId,
|
|
152
|
+
});
|
|
153
|
+
const hmac = createHmac('sha256', this.attestationKey);
|
|
154
|
+
hmac.update(payload);
|
|
155
|
+
return hmac.digest('hex');
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Derive attestation key from appToken
|
|
159
|
+
*
|
|
160
|
+
* HKDF-SHA256 with:
|
|
161
|
+
* - IKM: appToken
|
|
162
|
+
* - salt: 32 zero bytes
|
|
163
|
+
* - info: "stvor-metrics-attestation-v1"
|
|
164
|
+
*
|
|
165
|
+
* Result is deterministic: same appToken → same key
|
|
166
|
+
*
|
|
167
|
+
* This key is sent to backend for verification.
|
|
168
|
+
* Backend computes same key from appToken and verifies proof.
|
|
169
|
+
*/
|
|
170
|
+
deriveAttestationKey(appToken) {
|
|
171
|
+
const salt = Buffer.alloc(32, 0);
|
|
172
|
+
const info = Buffer.from('stvor-metrics-attestation-v1');
|
|
173
|
+
// Extract
|
|
174
|
+
const hmacExtract = createHmac('sha256', salt);
|
|
175
|
+
hmacExtract.update(appToken);
|
|
176
|
+
const prk = hmacExtract.digest();
|
|
177
|
+
// Expand
|
|
178
|
+
const hmacExpand = createHmac('sha256', prk);
|
|
179
|
+
hmacExpand.update(info);
|
|
180
|
+
hmacExpand.update(Buffer.from([1]));
|
|
181
|
+
return hmacExpand.digest();
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Generate unique session ID
|
|
185
|
+
* Used to distinguish different SDK instances
|
|
186
|
+
*/
|
|
187
|
+
generateSessionId() {
|
|
188
|
+
return `session_${randomBytes(16).toString('hex')}`;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Generate unique attestation ID
|
|
192
|
+
* Used for anti-replay detection
|
|
193
|
+
*/
|
|
194
|
+
generateAttestationId() {
|
|
195
|
+
return `attest_${randomBytes(16).toString('hex')}`;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Backend Verification Service (Pseudo-code)
|
|
200
|
+
*
|
|
201
|
+
* This runs on BACKEND, not in browser or SDK
|
|
202
|
+
*/
|
|
203
|
+
export class MetricsVerificationService {
|
|
204
|
+
/**
|
|
205
|
+
* Verify attestation received from SDK
|
|
206
|
+
*
|
|
207
|
+
* RETURN: VerificationResult
|
|
208
|
+
*/
|
|
209
|
+
verifyAttestation(attestation, appToken, lastSequenceNumber // From DB for this session
|
|
210
|
+
) {
|
|
211
|
+
// 1. Verify proof signature
|
|
212
|
+
if (!this.verifyProof(attestation, appToken)) {
|
|
213
|
+
return {
|
|
214
|
+
valid: false,
|
|
215
|
+
reason: 'Signature verification failed',
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// 2. Verify monotonicity
|
|
219
|
+
if (attestation.sequenceNumber !== lastSequenceNumber + 1) {
|
|
220
|
+
return {
|
|
221
|
+
valid: false,
|
|
222
|
+
reason: 'Sequence number not monotonic',
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
// 3. Verify timestamp is recent
|
|
226
|
+
const now = Date.now();
|
|
227
|
+
const maxAge = 5 * 60 * 1000; // 5 minutes
|
|
228
|
+
if (now - attestation.timestamp > maxAge) {
|
|
229
|
+
return {
|
|
230
|
+
valid: false,
|
|
231
|
+
reason: 'Attestation timestamp too old',
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
// 4. Check for replay (attestationId must be unique)
|
|
235
|
+
if (this.hasSeenAttestationId(attestation.attestationId)) {
|
|
236
|
+
return {
|
|
237
|
+
valid: false,
|
|
238
|
+
reason: 'Attestation already processed (replay detected)',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
// 5. Verify metrics are monotonic across SDK sessions
|
|
242
|
+
const lastMetrics = this.getLastVerifiedMetrics(attestation.sessionId);
|
|
243
|
+
if (lastMetrics) {
|
|
244
|
+
if (attestation.metrics.messagesEncrypted < lastMetrics.messagesEncrypted) {
|
|
245
|
+
return {
|
|
246
|
+
valid: false,
|
|
247
|
+
reason: 'Metrics rolled back (not monotonic)',
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return { valid: true, reason: 'Attestation verified' };
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Verify proof signature (backend-side)
|
|
255
|
+
*/
|
|
256
|
+
verifyProof(attestation, appToken) {
|
|
257
|
+
// Derive same key from appToken
|
|
258
|
+
const expectedKey = this.deriveAttestationKey(appToken);
|
|
259
|
+
// Recompute proof
|
|
260
|
+
const payload = JSON.stringify({
|
|
261
|
+
metrics: attestation.metrics,
|
|
262
|
+
sessionId: attestation.sessionId,
|
|
263
|
+
sequenceNumber: attestation.sequenceNumber,
|
|
264
|
+
timestamp: attestation.timestamp,
|
|
265
|
+
attestationId: attestation.attestationId,
|
|
266
|
+
});
|
|
267
|
+
const hmac = createHmac('sha256', expectedKey);
|
|
268
|
+
hmac.update(payload);
|
|
269
|
+
const computedProof = hmac.digest('hex');
|
|
270
|
+
// Constant-time comparison
|
|
271
|
+
return this.constantTimeCompare(computedProof, attestation.proof);
|
|
272
|
+
}
|
|
273
|
+
deriveAttestationKey(appToken) {
|
|
274
|
+
const salt = Buffer.alloc(32, 0);
|
|
275
|
+
const info = Buffer.from('stvor-metrics-attestation-v1');
|
|
276
|
+
const hmacExtract = createHmac('sha256', salt);
|
|
277
|
+
hmacExtract.update(appToken);
|
|
278
|
+
const prk = hmacExtract.digest();
|
|
279
|
+
const hmacExpand = createHmac('sha256', prk);
|
|
280
|
+
hmacExpand.update(info);
|
|
281
|
+
hmacExpand.update(Buffer.from([1]));
|
|
282
|
+
return hmacExpand.digest();
|
|
283
|
+
}
|
|
284
|
+
constantTimeCompare(a, b) {
|
|
285
|
+
if (a.length !== b.length)
|
|
286
|
+
return false;
|
|
287
|
+
let result = 0;
|
|
288
|
+
for (let i = 0; i < a.length; i++) {
|
|
289
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
290
|
+
}
|
|
291
|
+
return result === 0;
|
|
292
|
+
}
|
|
293
|
+
hasSeenAttestationId(attestationId) {
|
|
294
|
+
// Check DB for duplicate attestationId
|
|
295
|
+
// Return true if already seen (replay detected)
|
|
296
|
+
return false; // Pseudo-code
|
|
297
|
+
}
|
|
298
|
+
getLastVerifiedMetrics(sessionId) {
|
|
299
|
+
// Query DB for last verified metrics from this session
|
|
300
|
+
return null; // Pseudo-code
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* SECURITY INVARIANTS (MUST BE ENFORCED)
|
|
305
|
+
*
|
|
306
|
+
* I1: Dashboard NEVER generates metric numbers
|
|
307
|
+
* ✓ MetricsAttestationEngine records only on crypto success
|
|
308
|
+
* ✓ Dashboard only fetches from /api/metrics
|
|
309
|
+
*
|
|
310
|
+
* I2: Metrics without backend verification are discarded
|
|
311
|
+
* ✓ Backend verifies proof before storing
|
|
312
|
+
* ✓ Only verified metrics go to DB
|
|
313
|
+
* ✓ Dashboard reads DB, not SDK
|
|
314
|
+
*
|
|
315
|
+
* I3: Metric counters are monotonic
|
|
316
|
+
* ✓ SDK: counters only increment (never set)
|
|
317
|
+
* ✓ Backend: checks sequenceNumber is sequential
|
|
318
|
+
* ✓ Backend: checks metrics don't roll back
|
|
319
|
+
*
|
|
320
|
+
* I4: Metrics replay is impossible
|
|
321
|
+
* ✓ SDK: each attestation has unique attestationId
|
|
322
|
+
* ✓ Backend: stores all seen attestationIds
|
|
323
|
+
* ✓ Backend: rejects duplicate attestationIds
|
|
324
|
+
*
|
|
325
|
+
* I5: Different SDK instances cannot forge each other
|
|
326
|
+
* ✓ Each SDK has unique sessionId
|
|
327
|
+
* ✓ Backend checks sessionId matches appToken
|
|
328
|
+
*
|
|
329
|
+
* I6: appToken compromise ≠ metrics forgery
|
|
330
|
+
* ✓ appToken only derives the signing key
|
|
331
|
+
* ✓ Backend verifies timestamp is recent
|
|
332
|
+
* ✓ Backend checks monotonicity constraints
|
|
333
|
+
*/
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for facade/metrics-engine.js
|
|
4
|
+
// This allows `require('@stvor/sdk')` to work alongside ESM `import`.
|
|
5
|
+
|
|
6
|
+
const mod = require('module');
|
|
7
|
+
const url = require('url');
|
|
8
|
+
|
|
9
|
+
// Use dynamic import to load the ESM module
|
|
10
|
+
let _cached;
|
|
11
|
+
async function _load() {
|
|
12
|
+
if (!_cached) {
|
|
13
|
+
_cached = await import(url.pathToFileURL(__filename.replace(/\.cjs$/, '.js')).href);
|
|
14
|
+
}
|
|
15
|
+
return _cached;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// For simple CJS usage, expose a promise-based loader
|
|
19
|
+
module.exports = new Proxy({ load: _load }, {
|
|
20
|
+
get(target, prop) {
|
|
21
|
+
if (prop === '__esModule') return true;
|
|
22
|
+
if (prop === 'then') return undefined; // prevent treating as thenable
|
|
23
|
+
if (prop === 'load') return _load;
|
|
24
|
+
if (prop === 'default') {
|
|
25
|
+
return _load().then(m => m.default);
|
|
26
|
+
}
|
|
27
|
+
return _load().then(m => m[prop]);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STVOR v2.4.0 - Cryptographically Verified Metrics Engine
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for E2EE metrics.
|
|
5
|
+
* NO UI-side generation. NO localStorage counters. ONLY verified activity.
|
|
6
|
+
*/
|
|
7
|
+
export interface Metrics {
|
|
8
|
+
messagesEncrypted: number;
|
|
9
|
+
messagesDecrypted: number;
|
|
10
|
+
messagesRejected: number;
|
|
11
|
+
replayAttempts: number;
|
|
12
|
+
authFailures: number;
|
|
13
|
+
timestamp: number;
|
|
14
|
+
appToken: string;
|
|
15
|
+
}
|
|
16
|
+
export interface SignedMetrics {
|
|
17
|
+
metrics: Metrics;
|
|
18
|
+
proof: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* MetricsEngine: Runtime counter for real E2EE events only
|
|
22
|
+
*
|
|
23
|
+
* INVARIANT: Can only increment from SDK internals after cryptographic success
|
|
24
|
+
*/
|
|
25
|
+
export declare class MetricsEngine {
|
|
26
|
+
private metrics;
|
|
27
|
+
private appToken;
|
|
28
|
+
private analyticsUrl;
|
|
29
|
+
constructor(appToken: string, analyticsUrl?: string);
|
|
30
|
+
/**
|
|
31
|
+
* Called ONLY after successful encrypt with AEAD
|
|
32
|
+
*/
|
|
33
|
+
recordMessageEncrypted(): void;
|
|
34
|
+
/**
|
|
35
|
+
* Called ONLY after successful decrypt with AAD verification
|
|
36
|
+
* Cannot be called externally
|
|
37
|
+
*/
|
|
38
|
+
recordMessageDecrypted(): void;
|
|
39
|
+
/**
|
|
40
|
+
* Called when message fails AAD check or other auth failures
|
|
41
|
+
*/
|
|
42
|
+
recordMessageRejected(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Called when replay cache detects duplicate nonce
|
|
45
|
+
*/
|
|
46
|
+
recordReplayAttempt(): void;
|
|
47
|
+
/**
|
|
48
|
+
* Called when signature verification fails
|
|
49
|
+
*/
|
|
50
|
+
recordAuthFailure(): void;
|
|
51
|
+
/**
|
|
52
|
+
* Get current metrics snapshot (immutable)
|
|
53
|
+
*/
|
|
54
|
+
getMetrics(): Metrics;
|
|
55
|
+
/**
|
|
56
|
+
* Reset metrics (for testing only, not accessible in production)
|
|
57
|
+
*/
|
|
58
|
+
private updateTimestamp;
|
|
59
|
+
/**
|
|
60
|
+
* Get metrics with cryptographic proof
|
|
61
|
+
* proof = HMAC-SHA256(JSON(metrics), derived_key)
|
|
62
|
+
*
|
|
63
|
+
* Derived key = HKDF(appToken, "stvor-metrics-v3")
|
|
64
|
+
*/
|
|
65
|
+
getSignedMetrics(): SignedMetrics;
|
|
66
|
+
/**
|
|
67
|
+
* Derive metrics signing key from API token
|
|
68
|
+
* Using HKDF pattern for key derivation
|
|
69
|
+
*/
|
|
70
|
+
private deriveMetricsKey;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Verify metrics signature on Dashboard side
|
|
74
|
+
*
|
|
75
|
+
* Takes: payload (JSON string), proof (hex string), apiKey
|
|
76
|
+
* Returns: boolean (valid or not)
|
|
77
|
+
*
|
|
78
|
+
* USAGE:
|
|
79
|
+
* const valid = verifyMetricsSignature(payload, proof, apiKey);
|
|
80
|
+
* if (valid) {
|
|
81
|
+
* const metrics = JSON.parse(payload);
|
|
82
|
+
* display(metrics);
|
|
83
|
+
* } else {
|
|
84
|
+
* display("Unverified");
|
|
85
|
+
* }
|
|
86
|
+
*/
|
|
87
|
+
export declare function verifyMetricsSignature(payload: string, proof: string, apiKey: string): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Export MetricsEngine for use in facade/app.ts
|
|
90
|
+
*/
|
|
91
|
+
export default MetricsEngine;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STVOR v2.4.0 - Cryptographically Verified Metrics Engine
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for E2EE metrics.
|
|
5
|
+
* NO UI-side generation. NO localStorage counters. ONLY verified activity.
|
|
6
|
+
*/
|
|
7
|
+
import { createHmac } from 'crypto';
|
|
8
|
+
/**
|
|
9
|
+
* MetricsEngine: Runtime counter for real E2EE events only
|
|
10
|
+
*
|
|
11
|
+
* INVARIANT: Can only increment from SDK internals after cryptographic success
|
|
12
|
+
*/
|
|
13
|
+
export class MetricsEngine {
|
|
14
|
+
constructor(appToken, analyticsUrl = 'http://localhost:3001') {
|
|
15
|
+
this.appToken = appToken;
|
|
16
|
+
this.analyticsUrl = analyticsUrl;
|
|
17
|
+
this.metrics = {
|
|
18
|
+
messagesEncrypted: 0,
|
|
19
|
+
messagesDecrypted: 0,
|
|
20
|
+
messagesRejected: 0,
|
|
21
|
+
replayAttempts: 0,
|
|
22
|
+
authFailures: 0,
|
|
23
|
+
timestamp: Date.now(),
|
|
24
|
+
appToken: appToken
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Called ONLY after successful encrypt with AEAD
|
|
29
|
+
*/
|
|
30
|
+
recordMessageEncrypted() {
|
|
31
|
+
this.metrics.messagesEncrypted++;
|
|
32
|
+
this.updateTimestamp();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Called ONLY after successful decrypt with AAD verification
|
|
36
|
+
* Cannot be called externally
|
|
37
|
+
*/
|
|
38
|
+
recordMessageDecrypted() {
|
|
39
|
+
this.metrics.messagesDecrypted++;
|
|
40
|
+
this.updateTimestamp();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Called when message fails AAD check or other auth failures
|
|
44
|
+
*/
|
|
45
|
+
recordMessageRejected() {
|
|
46
|
+
this.metrics.messagesRejected++;
|
|
47
|
+
this.updateTimestamp();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Called when replay cache detects duplicate nonce
|
|
51
|
+
*/
|
|
52
|
+
recordReplayAttempt() {
|
|
53
|
+
this.metrics.replayAttempts++;
|
|
54
|
+
this.updateTimestamp();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Called when signature verification fails
|
|
58
|
+
*/
|
|
59
|
+
recordAuthFailure() {
|
|
60
|
+
this.metrics.authFailures++;
|
|
61
|
+
this.updateTimestamp();
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get current metrics snapshot (immutable)
|
|
65
|
+
*/
|
|
66
|
+
getMetrics() {
|
|
67
|
+
return Object.freeze({
|
|
68
|
+
...this.metrics,
|
|
69
|
+
timestamp: Date.now()
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Reset metrics (for testing only, not accessible in production)
|
|
74
|
+
*/
|
|
75
|
+
updateTimestamp() {
|
|
76
|
+
this.metrics.timestamp = Date.now();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Get metrics with cryptographic proof
|
|
80
|
+
* proof = HMAC-SHA256(JSON(metrics), derived_key)
|
|
81
|
+
*
|
|
82
|
+
* Derived key = HKDF(appToken, "stvor-metrics-v3")
|
|
83
|
+
*/
|
|
84
|
+
getSignedMetrics() {
|
|
85
|
+
const metrics = this.getMetrics();
|
|
86
|
+
const payload = JSON.stringify(metrics);
|
|
87
|
+
// Derive key from appToken
|
|
88
|
+
// appToken = "sk_live_" or "stvor_" prefix
|
|
89
|
+
const derivedKey = this.deriveMetricsKey();
|
|
90
|
+
// HMAC-SHA256
|
|
91
|
+
const hmac = createHmac('sha256', derivedKey);
|
|
92
|
+
hmac.update(payload);
|
|
93
|
+
const proof = hmac.digest('hex');
|
|
94
|
+
return {
|
|
95
|
+
metrics,
|
|
96
|
+
proof
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Derive metrics signing key from API token
|
|
101
|
+
* Using HKDF pattern for key derivation
|
|
102
|
+
*/
|
|
103
|
+
deriveMetricsKey() {
|
|
104
|
+
// Use libsodium for HKDF
|
|
105
|
+
// info = "stvor-metrics-v3" (domain separation)
|
|
106
|
+
const salt = Buffer.alloc(32, 0); // empty salt
|
|
107
|
+
const info = Buffer.from('stvor-metrics-v3');
|
|
108
|
+
// Extract phase: PRK = HMAC-Hash(salt, IKM)
|
|
109
|
+
const hmacExtract = createHmac('sha256', salt);
|
|
110
|
+
hmacExtract.update(this.appToken);
|
|
111
|
+
const prk = hmacExtract.digest();
|
|
112
|
+
// Expand phase: OKM = HMAC-Hash(PRK, info)
|
|
113
|
+
const hmacExpand = createHmac('sha256', prk);
|
|
114
|
+
hmacExpand.update(info);
|
|
115
|
+
hmacExpand.update(Buffer.from([1])); // counter
|
|
116
|
+
const okm = hmacExpand.digest();
|
|
117
|
+
return okm; // 32 bytes for SHA256
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Verify metrics signature on Dashboard side
|
|
122
|
+
*
|
|
123
|
+
* Takes: payload (JSON string), proof (hex string), apiKey
|
|
124
|
+
* Returns: boolean (valid or not)
|
|
125
|
+
*
|
|
126
|
+
* USAGE:
|
|
127
|
+
* const valid = verifyMetricsSignature(payload, proof, apiKey);
|
|
128
|
+
* if (valid) {
|
|
129
|
+
* const metrics = JSON.parse(payload);
|
|
130
|
+
* display(metrics);
|
|
131
|
+
* } else {
|
|
132
|
+
* display("Unverified");
|
|
133
|
+
* }
|
|
134
|
+
*/
|
|
135
|
+
export function verifyMetricsSignature(payload, proof, apiKey) {
|
|
136
|
+
try {
|
|
137
|
+
// Parse to validate JSON
|
|
138
|
+
JSON.parse(payload);
|
|
139
|
+
// Derive same key from API token
|
|
140
|
+
const derivedKey = deriveMetricsKeyForVerification(apiKey);
|
|
141
|
+
// Compute HMAC
|
|
142
|
+
const hmac = createHmac('sha256', derivedKey);
|
|
143
|
+
hmac.update(payload);
|
|
144
|
+
const computedProof = hmac.digest('hex');
|
|
145
|
+
// Constant-time comparison to prevent timing attacks
|
|
146
|
+
return computedProof === proof;
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
// Any parsing error = invalid
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Derive same key on verification side (Dashboard)
|
|
155
|
+
*/
|
|
156
|
+
function deriveMetricsKeyForVerification(apiKey) {
|
|
157
|
+
const salt = Buffer.alloc(32, 0);
|
|
158
|
+
const info = Buffer.from('stvor-metrics-v3');
|
|
159
|
+
const hmacExtract = createHmac('sha256', salt);
|
|
160
|
+
hmacExtract.update(apiKey);
|
|
161
|
+
const prk = hmacExtract.digest();
|
|
162
|
+
const hmacExpand = createHmac('sha256', prk);
|
|
163
|
+
hmacExpand.update(info);
|
|
164
|
+
hmacExpand.update(Buffer.from([1]));
|
|
165
|
+
return hmacExpand.digest();
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Export MetricsEngine for use in facade/app.ts
|
|
169
|
+
*/
|
|
170
|
+
export default MetricsEngine;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for facade/redis-replay-cache.js
|
|
4
|
+
// This allows `require('@stvor/sdk')` to work alongside ESM `import`.
|
|
5
|
+
|
|
6
|
+
const mod = require('module');
|
|
7
|
+
const url = require('url');
|
|
8
|
+
|
|
9
|
+
// Use dynamic import to load the ESM module
|
|
10
|
+
let _cached;
|
|
11
|
+
async function _load() {
|
|
12
|
+
if (!_cached) {
|
|
13
|
+
_cached = await import(url.pathToFileURL(__filename.replace(/\.cjs$/, '.js')).href);
|
|
14
|
+
}
|
|
15
|
+
return _cached;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// For simple CJS usage, expose a promise-based loader
|
|
19
|
+
module.exports = new Proxy({ load: _load }, {
|
|
20
|
+
get(target, prop) {
|
|
21
|
+
if (prop === '__esModule') return true;
|
|
22
|
+
if (prop === 'then') return undefined; // prevent treating as thenable
|
|
23
|
+
if (prop === 'load') return _load;
|
|
24
|
+
if (prop === 'default') {
|
|
25
|
+
return _load().then(m => m.default);
|
|
26
|
+
}
|
|
27
|
+
return _load().then(m => m[prop]);
|
|
28
|
+
}
|
|
29
|
+
});
|