@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,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis-based Replay Protection Cache for SDK
|
|
3
|
+
* Production-ready replay protection for clustered client deployments
|
|
4
|
+
*
|
|
5
|
+
* Use case: When running multiple instances of the same app (e.g., web app with
|
|
6
|
+
* multiple tabs, mobile app with background sync, or server-side SDK usage)
|
|
7
|
+
*/
|
|
8
|
+
import { IReplayCache } from './replay-manager.js';
|
|
9
|
+
/**
|
|
10
|
+
* Redis client interface (compatible with ioredis, node-redis, etc.)
|
|
11
|
+
*/
|
|
12
|
+
export interface RedisClient {
|
|
13
|
+
setEx(key: string, ttl: number, value: string): Promise<void>;
|
|
14
|
+
exists(key: string): Promise<number>;
|
|
15
|
+
keys(pattern: string): Promise<string[]>;
|
|
16
|
+
ping(): Promise<string>;
|
|
17
|
+
quit(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Configuration for Redis replay cache
|
|
21
|
+
*/
|
|
22
|
+
export interface RedisReplayCacheConfig {
|
|
23
|
+
/** Redis client instance (ioredis, node-redis, etc.) */
|
|
24
|
+
client: RedisClient;
|
|
25
|
+
/** Prefix for all keys (default: 'stvor:replay:') */
|
|
26
|
+
keyPrefix?: string;
|
|
27
|
+
/** TTL in seconds (default: 300 = 5 minutes) */
|
|
28
|
+
ttlSeconds?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Redis-based replay cache implementation
|
|
32
|
+
* Works with any Redis client (ioris, node-redis, etc.)
|
|
33
|
+
*/
|
|
34
|
+
export declare class RedisReplayCache implements IReplayCache {
|
|
35
|
+
private client;
|
|
36
|
+
private keyPrefix;
|
|
37
|
+
private ttlSeconds;
|
|
38
|
+
constructor(config: RedisReplayCacheConfig);
|
|
39
|
+
/**
|
|
40
|
+
* Build Redis key for nonce
|
|
41
|
+
*/
|
|
42
|
+
private buildKey;
|
|
43
|
+
/**
|
|
44
|
+
* Add nonce to cache
|
|
45
|
+
*/
|
|
46
|
+
addNonce(userId: string, nonce: string, timestamp: number): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Check if nonce exists in cache
|
|
49
|
+
*/
|
|
50
|
+
hasNonce(userId: string, nonce: string): Promise<boolean>;
|
|
51
|
+
/**
|
|
52
|
+
* Cleanup expired nonces
|
|
53
|
+
* Note: With Redis SETEX, keys auto-expire, so this is a no-op
|
|
54
|
+
*/
|
|
55
|
+
cleanup(userId: string, maxAge: number): Promise<number>;
|
|
56
|
+
/**
|
|
57
|
+
* Get cache statistics
|
|
58
|
+
*/
|
|
59
|
+
getStats(): Promise<{
|
|
60
|
+
size: number;
|
|
61
|
+
}>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Example: Create Redis cache using ioredis
|
|
65
|
+
*
|
|
66
|
+
* ```typescript
|
|
67
|
+
* import Redis from 'ioredis';
|
|
68
|
+
* import { initializeReplayProtection, RedisReplayCache } from '@stvor/sdk';
|
|
69
|
+
*
|
|
70
|
+
* const redis = new Redis(process.env.REDIS_URL!);
|
|
71
|
+
* const cache = new RedisReplayCache({ client: redis });
|
|
72
|
+
* initializeReplayProtection(cache);
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
/**
|
|
76
|
+
* Example: Create Redis cache using node-redis
|
|
77
|
+
*
|
|
78
|
+
* ```typescript
|
|
79
|
+
* import { createClient } from 'redis';
|
|
80
|
+
* import { initializeReplayProtection, RedisReplayCache } from '@stvor/sdk';
|
|
81
|
+
*
|
|
82
|
+
* const redis = createClient({ url: process.env.REDIS_URL });
|
|
83
|
+
* await redis.connect();
|
|
84
|
+
* const cache = new RedisReplayCache({ client: redis });
|
|
85
|
+
* initializeReplayProtection(cache);
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export type { IReplayCache } from './replay-manager.js';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis-based Replay Protection Cache for SDK
|
|
3
|
+
* Production-ready replay protection for clustered client deployments
|
|
4
|
+
*
|
|
5
|
+
* Use case: When running multiple instances of the same app (e.g., web app with
|
|
6
|
+
* multiple tabs, mobile app with background sync, or server-side SDK usage)
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Redis-based replay cache implementation
|
|
10
|
+
* Works with any Redis client (ioris, node-redis, etc.)
|
|
11
|
+
*/
|
|
12
|
+
export class RedisReplayCache {
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.client = config.client;
|
|
15
|
+
this.keyPrefix = config.keyPrefix || 'stvor:replay:';
|
|
16
|
+
this.ttlSeconds = config.ttlSeconds || 300;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Build Redis key for nonce
|
|
20
|
+
*/
|
|
21
|
+
buildKey(userId, nonce) {
|
|
22
|
+
return `${this.keyPrefix}${userId}:${nonce}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Add nonce to cache
|
|
26
|
+
*/
|
|
27
|
+
async addNonce(userId, nonce, timestamp) {
|
|
28
|
+
const key = this.buildKey(userId, nonce);
|
|
29
|
+
await this.client.setEx(key, this.ttlSeconds, String(timestamp));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Check if nonce exists in cache
|
|
33
|
+
*/
|
|
34
|
+
async hasNonce(userId, nonce) {
|
|
35
|
+
const key = this.buildKey(userId, nonce);
|
|
36
|
+
const result = await this.client.exists(key);
|
|
37
|
+
return result === 1;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Cleanup expired nonces
|
|
41
|
+
* Note: With Redis SETEX, keys auto-expire, so this is a no-op
|
|
42
|
+
*/
|
|
43
|
+
async cleanup(userId, maxAge) {
|
|
44
|
+
// Redis handles TTL automatically via SETEX
|
|
45
|
+
// This is kept for interface compatibility but returns 0
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get cache statistics
|
|
50
|
+
*/
|
|
51
|
+
async getStats() {
|
|
52
|
+
try {
|
|
53
|
+
const keys = await this.client.keys(`${this.keyPrefix}*`);
|
|
54
|
+
return { size: keys.length };
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return { size: 0 };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for facade/relay-client.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
|
+
});
|
|
@@ -1,36 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* STVOR DX Facade - Relay Client
|
|
3
3
|
*/
|
|
4
|
-
type
|
|
5
|
-
|
|
4
|
+
import type { SerializedPublicKeys } from './crypto-session';
|
|
5
|
+
interface OutgoingMessage {
|
|
6
|
+
to: string;
|
|
7
|
+
from: string;
|
|
8
|
+
ciphertext: string;
|
|
9
|
+
header: string;
|
|
10
|
+
}
|
|
11
|
+
interface IncomingMessage {
|
|
12
|
+
id?: string;
|
|
13
|
+
from: string;
|
|
14
|
+
ciphertext: string;
|
|
15
|
+
header: string;
|
|
16
|
+
timestamp: string;
|
|
17
|
+
}
|
|
6
18
|
export declare class RelayClient {
|
|
7
19
|
private relayUrl;
|
|
8
20
|
private timeout;
|
|
9
21
|
private appToken;
|
|
10
|
-
private ws?;
|
|
11
22
|
private connected;
|
|
12
|
-
private handshakeComplete;
|
|
13
|
-
private backoff;
|
|
14
|
-
private queue;
|
|
15
|
-
private handlers;
|
|
16
|
-
private reconnecting;
|
|
17
|
-
private connectPromise?;
|
|
18
|
-
private connectResolve?;
|
|
19
|
-
private connectReject?;
|
|
20
|
-
private authFailed;
|
|
21
23
|
constructor(relayUrl: string, appToken: string, timeout?: number);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* Throws StvorError if API key is rejected.
|
|
25
|
-
*/
|
|
26
|
-
init(): Promise<void>;
|
|
24
|
+
getAppToken(): string;
|
|
25
|
+
getBaseUrl(): string;
|
|
27
26
|
private getAuthHeaders;
|
|
28
|
-
|
|
29
|
-
private scheduleReconnect;
|
|
30
|
-
private doSend;
|
|
31
|
-
send(obj: JSONable): void;
|
|
32
|
-
onMessage(h: RelayHandler): void;
|
|
27
|
+
healthCheck(): Promise<void>;
|
|
33
28
|
isConnected(): boolean;
|
|
34
|
-
|
|
29
|
+
register(userId: string, publicKeys: SerializedPublicKeys): Promise<void>;
|
|
30
|
+
getPublicKeys(userId: string): Promise<SerializedPublicKeys | null>;
|
|
31
|
+
send(message: OutgoingMessage): Promise<void>;
|
|
32
|
+
fetchMessages(userId: string): Promise<IncomingMessage[]>;
|
|
33
|
+
disconnect(): void;
|
|
35
34
|
}
|
|
36
35
|
export {};
|
|
@@ -1,154 +1,133 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* STVOR DX Facade - Relay Client
|
|
3
3
|
*/
|
|
4
|
-
import { Errors
|
|
5
|
-
import * as WS from 'ws';
|
|
4
|
+
import { Errors } from './errors.js';
|
|
6
5
|
export class RelayClient {
|
|
7
6
|
constructor(relayUrl, appToken, timeout = 10000) {
|
|
8
7
|
this.connected = false;
|
|
9
|
-
this.
|
|
10
|
-
this.backoff = 1000;
|
|
11
|
-
this.queue = [];
|
|
12
|
-
this.handlers = [];
|
|
13
|
-
this.reconnecting = false;
|
|
14
|
-
this.authFailed = false;
|
|
15
|
-
this.relayUrl = relayUrl.replace(/^http/, 'ws');
|
|
8
|
+
this.relayUrl = relayUrl;
|
|
16
9
|
this.appToken = appToken;
|
|
17
10
|
this.timeout = timeout;
|
|
18
11
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (this.authFailed) {
|
|
25
|
-
throw new StvorError(Errors.INVALID_API_KEY, 'Relay rejected connection: invalid API key');
|
|
26
|
-
}
|
|
27
|
-
if (this.handshakeComplete)
|
|
28
|
-
return;
|
|
29
|
-
await this.connect();
|
|
12
|
+
getAppToken() {
|
|
13
|
+
return this.appToken;
|
|
14
|
+
}
|
|
15
|
+
getBaseUrl() {
|
|
16
|
+
return this.relayUrl;
|
|
30
17
|
}
|
|
31
18
|
getAuthHeaders() {
|
|
32
19
|
return {
|
|
33
|
-
Authorization: `Bearer ${this.appToken}`,
|
|
20
|
+
'Authorization': `Bearer ${this.appToken}`,
|
|
21
|
+
'Content-Type': 'application/json',
|
|
34
22
|
};
|
|
35
23
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
this.connectReject = reject;
|
|
44
|
-
const WSClass = WS.default ?? WS;
|
|
45
|
-
this.ws = new WSClass(this.relayUrl, { headers: this.getAuthHeaders() });
|
|
46
|
-
// Timeout for handshake
|
|
47
|
-
const handshakeTimeout = setTimeout(() => {
|
|
48
|
-
if (!this.handshakeComplete) {
|
|
49
|
-
this.ws?.close();
|
|
50
|
-
reject(new StvorError(Errors.RELAY_UNAVAILABLE, 'Relay handshake timeout'));
|
|
51
|
-
}
|
|
52
|
-
}, this.timeout);
|
|
53
|
-
this.ws.on('open', () => {
|
|
54
|
-
this.connected = true;
|
|
55
|
-
this.backoff = 1000;
|
|
56
|
-
// Don't flush queue yet - wait for handshake
|
|
24
|
+
async healthCheck() {
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(`${this.relayUrl}/health`, {
|
|
29
|
+
method: 'GET',
|
|
30
|
+
signal: controller.signal,
|
|
57
31
|
});
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
this.handshakeComplete = true;
|
|
66
|
-
// Now flush the queue
|
|
67
|
-
while (this.queue.length) {
|
|
68
|
-
const m = this.queue.shift();
|
|
69
|
-
this.doSend(m);
|
|
70
|
-
}
|
|
71
|
-
this.connectResolve?.();
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
// Handshake rejected
|
|
75
|
-
this.authFailed = true;
|
|
76
|
-
this.ws?.close();
|
|
77
|
-
const err = new StvorError(Errors.INVALID_API_KEY, `Relay rejected connection: ${json.reason || 'invalid API key'}`);
|
|
78
|
-
this.connectReject?.(err);
|
|
79
|
-
}
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
// Regular message
|
|
83
|
-
for (const h of this.handlers)
|
|
84
|
-
h(json);
|
|
85
|
-
}
|
|
86
|
-
catch (e) {
|
|
87
|
-
// ignore parse errors
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
this.ws.on('close', (code) => {
|
|
91
|
-
this.connected = false;
|
|
92
|
-
this.handshakeComplete = false;
|
|
93
|
-
this.ws = undefined;
|
|
94
|
-
this.connectPromise = undefined;
|
|
95
|
-
// If auth failed, don't reconnect
|
|
96
|
-
if (this.authFailed) {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
// 401/403 close codes mean auth failure
|
|
100
|
-
if (code === 4001 || code === 4003) {
|
|
101
|
-
this.authFailed = true;
|
|
102
|
-
this.connectReject?.(new StvorError(Errors.INVALID_API_KEY, 'Relay rejected connection: invalid API key'));
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
this.scheduleReconnect();
|
|
106
|
-
});
|
|
107
|
-
this.ws.on('error', (err) => {
|
|
108
|
-
this.connected = false;
|
|
109
|
-
this.handshakeComplete = false;
|
|
110
|
-
this.ws = undefined;
|
|
111
|
-
this.connectPromise = undefined;
|
|
112
|
-
if (this.authFailed) {
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
this.scheduleReconnect();
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
return this.connectPromise;
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
throw Errors.relayUnavailable();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
clearTimeout(timeoutId);
|
|
38
|
+
}
|
|
119
39
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return;
|
|
123
|
-
this.reconnecting = true;
|
|
124
|
-
setTimeout(() => {
|
|
125
|
-
this.reconnecting = false;
|
|
126
|
-
this.connect();
|
|
127
|
-
this.backoff = Math.min(this.backoff * 2, 30000);
|
|
128
|
-
}, this.backoff);
|
|
40
|
+
isConnected() {
|
|
41
|
+
return this.connected;
|
|
129
42
|
}
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
43
|
+
async register(userId, publicKeys) {
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch(`${this.relayUrl}/register`, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: this.getAuthHeaders(),
|
|
50
|
+
body: JSON.stringify({ user_id: userId, publicKeys }),
|
|
51
|
+
signal: controller.signal,
|
|
52
|
+
});
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
const error = await res.json().catch(() => ({}));
|
|
55
|
+
if (error.code === 'AUTH_FAILED') {
|
|
56
|
+
throw Errors.authFailed();
|
|
57
|
+
}
|
|
58
|
+
throw Errors.relayUnavailable();
|
|
59
|
+
}
|
|
60
|
+
this.connected = true;
|
|
134
61
|
}
|
|
135
|
-
|
|
136
|
-
|
|
62
|
+
finally {
|
|
63
|
+
clearTimeout(timeoutId);
|
|
137
64
|
}
|
|
138
65
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
66
|
+
async getPublicKeys(userId) {
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetch(`${this.relayUrl}/public-key/${userId}`, {
|
|
71
|
+
method: 'GET',
|
|
72
|
+
headers: this.getAuthHeaders(),
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
});
|
|
75
|
+
if (res.status === 404) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
throw Errors.relayUnavailable();
|
|
80
|
+
}
|
|
81
|
+
const data = await res.json();
|
|
82
|
+
return data.publicKeys;
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
clearTimeout(timeoutId);
|
|
142
86
|
}
|
|
143
|
-
this.doSend(obj);
|
|
144
87
|
}
|
|
145
|
-
|
|
146
|
-
|
|
88
|
+
async send(message) {
|
|
89
|
+
const controller = new AbortController();
|
|
90
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
91
|
+
try {
|
|
92
|
+
const res = await fetch(`${this.relayUrl}/message`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: this.getAuthHeaders(),
|
|
95
|
+
body: JSON.stringify({
|
|
96
|
+
to: message.to,
|
|
97
|
+
from: message.from,
|
|
98
|
+
ciphertext: message.ciphertext,
|
|
99
|
+
header: message.header,
|
|
100
|
+
}),
|
|
101
|
+
signal: controller.signal,
|
|
102
|
+
});
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
throw Errors.deliveryFailed(message.to);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
clearTimeout(timeoutId);
|
|
109
|
+
}
|
|
147
110
|
}
|
|
148
|
-
|
|
149
|
-
|
|
111
|
+
async fetchMessages(userId) {
|
|
112
|
+
const controller = new AbortController();
|
|
113
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
114
|
+
try {
|
|
115
|
+
const res = await fetch(`${this.relayUrl}/messages/${userId}`, {
|
|
116
|
+
method: 'GET',
|
|
117
|
+
headers: this.getAuthHeaders(),
|
|
118
|
+
signal: controller.signal,
|
|
119
|
+
});
|
|
120
|
+
if (!res.ok) {
|
|
121
|
+
throw Errors.relayUnavailable();
|
|
122
|
+
}
|
|
123
|
+
const data = await res.json();
|
|
124
|
+
return data.messages || [];
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
clearTimeout(timeoutId);
|
|
128
|
+
}
|
|
150
129
|
}
|
|
151
|
-
|
|
152
|
-
|
|
130
|
+
disconnect() {
|
|
131
|
+
this.connected = false;
|
|
153
132
|
}
|
|
154
133
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for facade/replay-manager.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
|
+
});
|
|
@@ -1,39 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* STVOR Replay Protection Manager
|
|
3
|
-
*
|
|
3
|
+
* Implements persistent replay protection with fallback to in-memory
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* VERSION 2.0 - PRODUCTION READY
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* 2. ATTACK WINDOW: 5 minutes after restart/cache clear
|
|
13
|
-
*
|
|
14
|
-
* 3. PRODUCTION REQUIREMENTS:
|
|
15
|
-
* - Redis or distributed cache (Memcached, DynamoDB)
|
|
16
|
-
* - Persistent storage survives restarts
|
|
17
|
-
* - Shared state across instances
|
|
18
|
-
*
|
|
19
|
-
* 4. ACCEPTABLE FOR:
|
|
20
|
-
* ✓ Single-instance development
|
|
21
|
-
* ✓ Proof-of-concept deployments
|
|
22
|
-
* ✓ Low-security use cases
|
|
23
|
-
*
|
|
24
|
-
* 5. NOT ACCEPTABLE FOR:
|
|
25
|
-
* ✗ Multi-instance production
|
|
26
|
-
* ✗ High-security environments
|
|
27
|
-
* ✗ Mobile apps (background kills)
|
|
28
|
-
*
|
|
29
|
-
* STATUS: Transitional implementation - Redis integration planned for v2.2
|
|
7
|
+
* Features:
|
|
8
|
+
* - Persistent storage interface (Redis, PostgreSQL, etc.)
|
|
9
|
+
* - In-memory fallback for development
|
|
10
|
+
* - Proper cleanup of expired entries
|
|
11
|
+
* - Cache statistics
|
|
30
12
|
*/
|
|
13
|
+
export interface IReplayCache {
|
|
14
|
+
addNonce(userId: string, nonce: string, timestamp: number): Promise<void>;
|
|
15
|
+
hasNonce(userId: string, nonce: string): Promise<boolean>;
|
|
16
|
+
cleanup(userId: string, maxAge: number): Promise<number>;
|
|
17
|
+
getStats(): Promise<{
|
|
18
|
+
size: number;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Initialize replay protection with optional persistent storage
|
|
23
|
+
*/
|
|
24
|
+
export declare function initializeReplayProtection(customCache?: IReplayCache): void;
|
|
31
25
|
/**
|
|
32
26
|
* Check if message is a replay attack
|
|
33
|
-
* @param userId - Sender's user ID
|
|
34
|
-
* @param nonce - Message nonce (base64 or hex)
|
|
35
|
-
* @param timestamp - Message timestamp (Unix seconds)
|
|
36
|
-
* @returns true if replay detected
|
|
37
27
|
*/
|
|
38
28
|
export declare function isReplay(userId: string, nonce: string, timestamp: number): Promise<boolean>;
|
|
39
29
|
/**
|
|
@@ -45,14 +35,17 @@ export declare function validateMessage(userId: string, nonce: string, timestamp
|
|
|
45
35
|
* Validate message with Uint8Array nonce
|
|
46
36
|
*/
|
|
47
37
|
export declare function validateMessageWithNonce(userId: string, nonce: Uint8Array, timestamp: number): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Cleanup expired nonces from cache
|
|
40
|
+
* Should be called periodically in production
|
|
41
|
+
*/
|
|
42
|
+
export declare function cleanupExpiredNonces(): Promise<number>;
|
|
48
43
|
/**
|
|
49
44
|
* Get cache statistics (for monitoring)
|
|
50
45
|
*/
|
|
51
|
-
export declare function getCacheStats(): {
|
|
46
|
+
export declare function getCacheStats(): Promise<{
|
|
52
47
|
size: number;
|
|
53
48
|
maxSize: number;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
*/
|
|
58
|
-
export declare function clearNonceCache(): void;
|
|
49
|
+
}>;
|
|
50
|
+
export declare function startAutoCleanup(): void;
|
|
51
|
+
export declare function stopAutoCleanup(): void;
|