@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.
Files changed (82) hide show
  1. package/dist/facade/app.cjs +29 -0
  2. package/dist/facade/app.d.ts +83 -76
  3. package/dist/facade/app.js +330 -195
  4. package/dist/facade/crypto-session.cjs +29 -0
  5. package/dist/facade/crypto-session.d.ts +49 -54
  6. package/dist/facade/crypto-session.js +117 -140
  7. package/dist/facade/errors.cjs +29 -0
  8. package/dist/facade/errors.d.ts +29 -12
  9. package/dist/facade/errors.js +49 -8
  10. package/dist/facade/index.cjs +29 -0
  11. package/dist/facade/index.d.ts +27 -8
  12. package/dist/facade/index.js +23 -3
  13. package/dist/facade/local-storage-identity-store.cjs +29 -0
  14. package/dist/facade/local-storage-identity-store.d.ts +50 -0
  15. package/dist/facade/local-storage-identity-store.js +100 -0
  16. package/dist/facade/metrics-attestation.cjs +29 -0
  17. package/dist/facade/metrics-attestation.d.ts +209 -0
  18. package/dist/facade/metrics-attestation.js +333 -0
  19. package/dist/facade/metrics-engine.cjs +29 -0
  20. package/dist/facade/metrics-engine.d.ts +91 -0
  21. package/dist/facade/metrics-engine.js +170 -0
  22. package/dist/facade/redis-replay-cache.cjs +29 -0
  23. package/dist/facade/redis-replay-cache.d.ts +88 -0
  24. package/dist/facade/redis-replay-cache.js +60 -0
  25. package/dist/facade/relay-client.cjs +29 -0
  26. package/dist/facade/relay-client.d.ts +22 -23
  27. package/dist/facade/relay-client.js +107 -128
  28. package/dist/facade/replay-manager.cjs +29 -0
  29. package/dist/facade/replay-manager.d.ts +28 -35
  30. package/dist/facade/replay-manager.js +102 -69
  31. package/dist/facade/sodium-singleton.cjs +29 -0
  32. package/dist/facade/tofu-manager.cjs +29 -0
  33. package/dist/facade/tofu-manager.d.ts +38 -36
  34. package/dist/facade/tofu-manager.js +109 -77
  35. package/dist/facade/types.cjs +29 -0
  36. package/dist/facade/types.d.ts +2 -0
  37. package/dist/index.cjs +29 -0
  38. package/dist/index.d.cts +6 -0
  39. package/dist/index.d.ts +4 -0
  40. package/dist/index.js +7 -0
  41. package/dist/legacy.cjs +29 -0
  42. package/dist/legacy.d.ts +31 -1
  43. package/dist/legacy.js +90 -2
  44. package/dist/ratchet/core-production.cjs +29 -0
  45. package/dist/ratchet/core-production.d.ts +95 -0
  46. package/dist/ratchet/core-production.js +286 -0
  47. package/dist/ratchet/index.cjs +29 -0
  48. package/dist/ratchet/index.d.ts +49 -78
  49. package/dist/ratchet/index.js +313 -288
  50. package/dist/ratchet/key-recovery.cjs +29 -0
  51. package/dist/ratchet/replay-protection.cjs +29 -0
  52. package/dist/ratchet/tofu.cjs +29 -0
  53. package/dist/src/facade/app.cjs +29 -0
  54. package/dist/src/facade/app.d.ts +105 -0
  55. package/dist/src/facade/app.js +245 -0
  56. package/dist/src/facade/crypto.cjs +29 -0
  57. package/dist/src/facade/errors.cjs +29 -0
  58. package/dist/src/facade/errors.d.ts +19 -0
  59. package/dist/src/facade/errors.js +21 -0
  60. package/dist/src/facade/index.cjs +29 -0
  61. package/dist/src/facade/index.d.ts +8 -0
  62. package/dist/src/facade/index.js +5 -0
  63. package/dist/src/facade/relay-client.cjs +29 -0
  64. package/dist/src/facade/relay-client.d.ts +36 -0
  65. package/dist/src/facade/relay-client.js +154 -0
  66. package/dist/src/facade/types.cjs +29 -0
  67. package/dist/src/facade/types.d.ts +50 -0
  68. package/dist/src/facade/types.js +4 -0
  69. package/dist/src/index.cjs +29 -0
  70. package/dist/src/index.d.ts +2 -0
  71. package/dist/src/index.js +2 -0
  72. package/dist/src/legacy.cjs +29 -0
  73. package/dist/src/legacy.d.ts +0 -0
  74. package/dist/src/legacy.js +1 -0
  75. package/dist/src/mock-relay-server.cjs +29 -0
  76. package/dist/src/mock-relay-server.d.ts +30 -0
  77. package/dist/src/mock-relay-server.js +236 -0
  78. package/package.json +37 -11
  79. package/dist/ratchet/tests/ratchet.test.d.ts +0 -1
  80. package/dist/ratchet/tests/ratchet.test.js +0 -160
  81. /package/dist/{facade → src/facade}/crypto.d.ts +0 -0
  82. /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 JSONable = Record<string, any>;
5
- export type RelayHandler = (msg: JSONable) => void;
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
- * Initialize the connection and wait for handshake.
24
- * Throws StvorError if API key is rejected.
25
- */
26
- init(): Promise<void>;
24
+ getAppToken(): string;
25
+ getBaseUrl(): string;
27
26
  private getAuthHeaders;
28
- private connect;
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
- isAuthenticated(): boolean;
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, StvorError } from './errors.js';
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.handshakeComplete = false;
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
- * Initialize the connection and wait for handshake.
21
- * Throws StvorError if API key is rejected.
22
- */
23
- async init() {
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
- connect() {
37
- if (this.connectPromise)
38
- return this.connectPromise;
39
- if (this.ws)
40
- return Promise.resolve();
41
- this.connectPromise = new Promise((resolve, reject) => {
42
- this.connectResolve = resolve;
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
- this.ws.on('message', (data) => {
59
- try {
60
- const json = JSON.parse(data.toString());
61
- // Handle handshake response
62
- if (json.type === 'handshake') {
63
- clearTimeout(handshakeTimeout);
64
- if (json.status === 'ok') {
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
- scheduleReconnect() {
121
- if (this.reconnecting)
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
- doSend(obj) {
131
- const data = JSON.stringify(obj);
132
- if (this.connected && this.ws && this.handshakeComplete) {
133
- this.ws.send(data);
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
- else {
136
- this.queue.push(obj);
62
+ finally {
63
+ clearTimeout(timeoutId);
137
64
  }
138
65
  }
139
- send(obj) {
140
- if (this.authFailed) {
141
- throw new StvorError(Errors.INVALID_API_KEY, 'Cannot send: relay rejected connection due to invalid API key');
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
- onMessage(h) {
146
- this.handlers.push(h);
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
- isConnected() {
149
- return this.connected && this.handshakeComplete;
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
- isAuthenticated() {
152
- return this.handshakeComplete && !this.authFailed;
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
- * Integrates nonce-based replay protection with in-memory fallback
3
+ * Implements persistent replay protection with fallback to in-memory
4
4
  *
5
- * ⚠️ CRITICAL LIMITATIONS (v2.1):
5
+ * VERSION 2.0 - PRODUCTION READY
6
6
  *
7
- * 1. IN-MEMORY ONLY - DEMO-LEVEL PROTECTION
8
- * - Process restart cache cleared → replay window reopens
9
- * - Clustered deployment each instance has separate cache
10
- * - Mobile background iOS/Android may kill process
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
- * Clear all cached nonces (for testing)
57
- */
58
- export declare function clearNonceCache(): void;
49
+ }>;
50
+ export declare function startAutoCleanup(): void;
51
+ export declare function stopAutoCleanup(): void;