@stvor/sdk 2.4.1 → 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 (81) hide show
  1. package/dist/facade/app.d.ts +83 -76
  2. package/dist/facade/app.js +330 -195
  3. package/dist/facade/crypto-session.cjs +29 -0
  4. package/dist/facade/crypto-session.d.ts +71 -0
  5. package/dist/facade/crypto-session.js +152 -0
  6. package/dist/facade/errors.d.ts +29 -12
  7. package/dist/facade/errors.js +49 -8
  8. package/dist/facade/index.d.ts +27 -8
  9. package/dist/facade/index.js +23 -3
  10. package/dist/facade/local-storage-identity-store.cjs +29 -0
  11. package/dist/facade/local-storage-identity-store.d.ts +50 -0
  12. package/dist/facade/local-storage-identity-store.js +100 -0
  13. package/dist/facade/metrics-attestation.cjs +29 -0
  14. package/dist/facade/metrics-attestation.d.ts +209 -0
  15. package/dist/facade/metrics-attestation.js +333 -0
  16. package/dist/facade/metrics-engine.cjs +29 -0
  17. package/dist/facade/metrics-engine.d.ts +91 -0
  18. package/dist/facade/metrics-engine.js +170 -0
  19. package/dist/facade/redis-replay-cache.cjs +29 -0
  20. package/dist/facade/redis-replay-cache.d.ts +88 -0
  21. package/dist/facade/redis-replay-cache.js +60 -0
  22. package/dist/facade/relay-client.d.ts +22 -23
  23. package/dist/facade/relay-client.js +107 -128
  24. package/dist/facade/replay-manager.cjs +29 -0
  25. package/dist/facade/replay-manager.d.ts +51 -0
  26. package/dist/facade/replay-manager.js +150 -0
  27. package/dist/facade/sodium-singleton.cjs +29 -0
  28. package/dist/facade/sodium-singleton.d.ts +20 -0
  29. package/dist/facade/sodium-singleton.js +44 -0
  30. package/dist/facade/tofu-manager.cjs +29 -0
  31. package/dist/facade/tofu-manager.d.ts +82 -0
  32. package/dist/facade/tofu-manager.js +166 -0
  33. package/dist/facade/types.d.ts +2 -0
  34. package/dist/index.d.cts +4 -0
  35. package/dist/index.d.ts +4 -0
  36. package/dist/index.js +7 -0
  37. package/dist/legacy.d.ts +31 -1
  38. package/dist/legacy.js +90 -2
  39. package/dist/ratchet/core-production.cjs +29 -0
  40. package/dist/ratchet/core-production.d.ts +95 -0
  41. package/dist/ratchet/core-production.js +286 -0
  42. package/dist/{facade/crypto.cjs → ratchet/index.cjs} +1 -1
  43. package/dist/ratchet/index.d.ts +59 -0
  44. package/dist/ratchet/index.js +343 -0
  45. package/dist/ratchet/key-recovery.cjs +29 -0
  46. package/dist/ratchet/key-recovery.d.ts +45 -0
  47. package/dist/ratchet/key-recovery.js +148 -0
  48. package/dist/ratchet/replay-protection.cjs +29 -0
  49. package/dist/ratchet/replay-protection.d.ts +21 -0
  50. package/dist/ratchet/replay-protection.js +50 -0
  51. package/dist/{mock-relay-server.cjs → ratchet/tofu.cjs} +1 -1
  52. package/dist/ratchet/tofu.d.ts +27 -0
  53. package/dist/ratchet/tofu.js +62 -0
  54. package/dist/src/facade/app.cjs +29 -0
  55. package/dist/src/facade/app.d.ts +105 -0
  56. package/dist/src/facade/app.js +245 -0
  57. package/dist/src/facade/crypto.cjs +29 -0
  58. package/dist/src/facade/errors.cjs +29 -0
  59. package/dist/src/facade/errors.d.ts +19 -0
  60. package/dist/src/facade/errors.js +21 -0
  61. package/dist/src/facade/index.cjs +29 -0
  62. package/dist/src/facade/index.d.ts +8 -0
  63. package/dist/src/facade/index.js +5 -0
  64. package/dist/src/facade/relay-client.cjs +29 -0
  65. package/dist/src/facade/relay-client.d.ts +36 -0
  66. package/dist/src/facade/relay-client.js +154 -0
  67. package/dist/src/facade/types.cjs +29 -0
  68. package/dist/src/facade/types.d.ts +50 -0
  69. package/dist/src/facade/types.js +4 -0
  70. package/dist/src/index.cjs +29 -0
  71. package/dist/src/index.d.ts +2 -0
  72. package/dist/src/index.js +2 -0
  73. package/dist/src/legacy.cjs +29 -0
  74. package/dist/src/legacy.d.ts +0 -0
  75. package/dist/src/legacy.js +1 -0
  76. package/dist/src/mock-relay-server.cjs +29 -0
  77. package/package.json +16 -5
  78. /package/dist/{facade → src/facade}/crypto.d.ts +0 -0
  79. /package/dist/{facade → src/facade}/crypto.js +0 -0
  80. /package/dist/{mock-relay-server.d.ts → src/mock-relay-server.d.ts} +0 -0
  81. /package/dist/{mock-relay-server.js → src/mock-relay-server.js} +0 -0
@@ -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
+ });
@@ -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
+ }
@@ -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
  }