@stvor/sdk 2.3.1 → 2.3.2

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.
@@ -204,6 +204,8 @@ export class StvorApp {
204
204
  if (existing)
205
205
  return existing;
206
206
  const relay = new RelayClient(this.config.relayUrl ?? 'wss://stvor.xyz/relay', this.config.appToken, this.config.timeout ?? 10000);
207
+ // Wait for relay handshake - throws if API key is invalid
208
+ await relay.init();
207
209
  const client = new StvorFacadeClient(userId, relay, this.config.timeout ?? 10000);
208
210
  await client.internalInitialize();
209
211
  this.clients.set(userId, client);
@@ -1,5 +1,6 @@
1
1
  export declare const Errors: {
2
2
  readonly INVALID_APP_TOKEN: "INVALID_APP_TOKEN";
3
+ readonly INVALID_API_KEY: "INVALID_API_KEY";
3
4
  readonly RELAY_UNAVAILABLE: "RELAY_UNAVAILABLE";
4
5
  readonly DELIVERY_FAILED: "DELIVERY_FAILED";
5
6
  readonly RECIPIENT_NOT_FOUND: "RECIPIENT_NOT_FOUND";
@@ -1,5 +1,6 @@
1
1
  export const Errors = {
2
2
  INVALID_APP_TOKEN: 'INVALID_APP_TOKEN',
3
+ INVALID_API_KEY: 'INVALID_API_KEY',
3
4
  RELAY_UNAVAILABLE: 'RELAY_UNAVAILABLE',
4
5
  DELIVERY_FAILED: 'DELIVERY_FAILED',
5
6
  RECIPIENT_NOT_FOUND: 'RECIPIENT_NOT_FOUND',
@@ -9,16 +9,28 @@ export declare class RelayClient {
9
9
  private appToken;
10
10
  private ws?;
11
11
  private connected;
12
+ private handshakeComplete;
12
13
  private backoff;
13
14
  private queue;
14
15
  private handlers;
15
16
  private reconnecting;
17
+ private connectPromise?;
18
+ private connectResolve?;
19
+ private connectReject?;
20
+ private authFailed;
16
21
  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>;
17
27
  private getAuthHeaders;
18
28
  private connect;
19
29
  private scheduleReconnect;
30
+ private doSend;
20
31
  send(obj: JSONable): void;
21
32
  onMessage(h: RelayHandler): void;
22
33
  isConnected(): boolean;
34
+ isAuthenticated(): boolean;
23
35
  }
24
36
  export {};
@@ -1,18 +1,32 @@
1
1
  /**
2
2
  * STVOR DX Facade - Relay Client
3
3
  */
4
+ import { Errors, StvorError } from './errors.js';
4
5
  import * as WS from 'ws';
5
6
  export class RelayClient {
6
7
  constructor(relayUrl, appToken, timeout = 10000) {
7
8
  this.connected = false;
9
+ this.handshakeComplete = false;
8
10
  this.backoff = 1000;
9
11
  this.queue = [];
10
12
  this.handlers = [];
11
13
  this.reconnecting = false;
14
+ this.authFailed = false;
12
15
  this.relayUrl = relayUrl.replace(/^http/, 'ws');
13
16
  this.appToken = appToken;
14
17
  this.timeout = timeout;
15
- this.connect();
18
+ }
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();
16
30
  }
17
31
  getAuthHeaders() {
18
32
  return {
@@ -20,39 +34,88 @@ export class RelayClient {
20
34
  };
21
35
  }
22
36
  connect() {
37
+ if (this.connectPromise)
38
+ return this.connectPromise;
23
39
  if (this.ws)
24
- return;
25
- const WSClass = WS.default ?? WS;
26
- this.ws = new WSClass(this.relayUrl, { headers: this.getAuthHeaders() });
27
- this.ws.on('open', () => {
28
- this.connected = true;
29
- this.backoff = 1000;
30
- // flush queue
31
- while (this.queue.length) {
32
- const m = this.queue.shift();
33
- this.send(m);
34
- }
35
- });
36
- this.ws.on('message', (data) => {
37
- try {
38
- const json = JSON.parse(data.toString());
39
- for (const h of this.handlers)
40
- h(json);
41
- }
42
- catch (e) {
43
- // ignore
44
- }
45
- });
46
- this.ws.on('close', () => {
47
- this.connected = false;
48
- this.ws = undefined;
49
- this.scheduleReconnect();
50
- });
51
- this.ws.on('error', () => {
52
- this.connected = false;
53
- this.ws = undefined;
54
- this.scheduleReconnect();
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
57
+ });
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
+ });
55
117
  });
118
+ return this.connectPromise;
56
119
  }
57
120
  scheduleReconnect() {
58
121
  if (this.reconnecting)
@@ -64,19 +127,28 @@ export class RelayClient {
64
127
  this.backoff = Math.min(this.backoff * 2, 30000);
65
128
  }, this.backoff);
66
129
  }
67
- send(obj) {
130
+ doSend(obj) {
68
131
  const data = JSON.stringify(obj);
69
- if (this.connected && this.ws) {
132
+ if (this.connected && this.ws && this.handshakeComplete) {
70
133
  this.ws.send(data);
71
134
  }
72
135
  else {
73
136
  this.queue.push(obj);
74
137
  }
75
138
  }
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');
142
+ }
143
+ this.doSend(obj);
144
+ }
76
145
  onMessage(h) {
77
146
  this.handlers.push(h);
78
147
  }
79
148
  isConnected() {
80
- return this.connected;
149
+ return this.connected && this.handshakeComplete;
150
+ }
151
+ isAuthenticated() {
152
+ return this.handshakeComplete && !this.authFailed;
81
153
  }
82
154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stvor/sdk",
3
- "version": "2.3.1",
3
+ "version": "2.3.2",
4
4
  "description": "Stvor DX Facade - Simple E2EE SDK for client-side encryption",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",