@stvor/sdk 2.3.0 → 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.
- package/dist/facade/app.d.ts +3 -0
- package/dist/facade/app.js +5 -0
- package/dist/facade/errors.d.ts +1 -0
- package/dist/facade/errors.js +1 -0
- package/dist/facade/relay-client.d.ts +12 -0
- package/dist/facade/relay-client.js +107 -35
- package/package.json +1 -1
package/dist/facade/app.d.ts
CHANGED
|
@@ -69,6 +69,9 @@ export declare class StvorFacadeClient {
|
|
|
69
69
|
* Register a handler that fires when a new user becomes available.
|
|
70
70
|
* This is triggered when we receive a user's public key announcement.
|
|
71
71
|
*
|
|
72
|
+
* **Edge-triggered**: Fires only ONCE per user, on first key discovery.
|
|
73
|
+
* Will NOT fire again if user reconnects with same identity.
|
|
74
|
+
*
|
|
72
75
|
* @example
|
|
73
76
|
* ```typescript
|
|
74
77
|
* client.onUserAvailable((userId) => {
|
package/dist/facade/app.js
CHANGED
|
@@ -175,6 +175,9 @@ export class StvorFacadeClient {
|
|
|
175
175
|
* Register a handler that fires when a new user becomes available.
|
|
176
176
|
* This is triggered when we receive a user's public key announcement.
|
|
177
177
|
*
|
|
178
|
+
* **Edge-triggered**: Fires only ONCE per user, on first key discovery.
|
|
179
|
+
* Will NOT fire again if user reconnects with same identity.
|
|
180
|
+
*
|
|
178
181
|
* @example
|
|
179
182
|
* ```typescript
|
|
180
183
|
* client.onUserAvailable((userId) => {
|
|
@@ -201,6 +204,8 @@ export class StvorApp {
|
|
|
201
204
|
if (existing)
|
|
202
205
|
return existing;
|
|
203
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();
|
|
204
209
|
const client = new StvorFacadeClient(userId, relay, this.config.timeout ?? 10000);
|
|
205
210
|
await client.internalInitialize();
|
|
206
211
|
this.clients.set(userId, client);
|
package/dist/facade/errors.d.ts
CHANGED
|
@@ -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";
|
package/dist/facade/errors.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
this.
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
}
|