@blazium/ton-connect-mobile 1.2.5 → 1.2.6
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/core/bridge.d.ts +61 -0
- package/dist/core/bridge.js +237 -0
- package/dist/core/crypto.d.ts +8 -19
- package/dist/core/crypto.js +15 -141
- package/dist/core/index.d.ts +5 -3
- package/dist/core/index.js +20 -17
- package/dist/core/protocol.d.ts +35 -34
- package/dist/core/protocol.js +109 -288
- package/dist/core/session.d.ts +65 -0
- package/dist/core/session.js +235 -0
- package/dist/core/wallets.d.ts +6 -6
- package/dist/core/wallets.js +17 -18
- package/dist/index.d.ts +33 -72
- package/dist/index.js +322 -769
- package/dist/react/TonConnectUIProvider.d.ts +4 -52
- package/dist/react/TonConnectUIProvider.js +18 -122
- package/dist/react/index.d.ts +1 -2
- package/dist/react/index.js +0 -1
- package/dist/types/index.d.ts +84 -139
- package/dist/types/index.js +1 -1
- package/package.json +2 -3
- package/src/core/bridge.ts +307 -0
- package/src/core/crypto.ts +62 -238
- package/src/core/index.ts +17 -7
- package/src/core/protocol.ts +217 -443
- package/src/core/session.ts +247 -0
- package/src/core/wallets.ts +90 -93
- package/src/index.ts +811 -1338
- package/src/react/TonConnectUIProvider.tsx +272 -441
- package/src/react/index.ts +23 -27
- package/src/types/index.ts +217 -272
package/dist/types/index.d.ts
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Core types for TON Connect Mobile SDK
|
|
3
|
-
*
|
|
3
|
+
* Types for TON Connect v2 protocol
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* Wallet information returned after successful connection
|
|
7
7
|
*/
|
|
8
8
|
export interface WalletInfo {
|
|
9
|
-
/** Wallet name (e.g., "Tonkeeper"
|
|
9
|
+
/** Wallet name (e.g., "Tonkeeper") */
|
|
10
10
|
name: string;
|
|
11
11
|
/** Wallet app name */
|
|
12
12
|
appName: string;
|
|
13
13
|
/** Wallet app version */
|
|
14
14
|
version: string;
|
|
15
|
-
/** Platform
|
|
15
|
+
/** Platform */
|
|
16
16
|
platform: 'ios' | 'android' | 'unknown';
|
|
17
|
-
/** TON address
|
|
17
|
+
/** TON address (raw format: workchain:hash) */
|
|
18
18
|
address: string;
|
|
19
19
|
/** Public key in hex format */
|
|
20
20
|
publicKey: string;
|
|
21
|
+
/** Network identifier ("-239" = mainnet, "-3" = testnet) */
|
|
22
|
+
network?: string;
|
|
23
|
+
/** Wallet state init (base64) */
|
|
24
|
+
walletStateInit?: string;
|
|
21
25
|
/** Wallet icon URL */
|
|
22
26
|
icon?: string;
|
|
23
27
|
}
|
|
@@ -25,18 +29,16 @@ export interface WalletInfo {
|
|
|
25
29
|
* Connection status
|
|
26
30
|
*/
|
|
27
31
|
export interface ConnectionStatus {
|
|
28
|
-
/** Whether a wallet is currently connected */
|
|
29
32
|
connected: boolean;
|
|
30
|
-
/** Wallet information if connected, null otherwise */
|
|
31
33
|
wallet: WalletInfo | null;
|
|
32
34
|
}
|
|
33
35
|
/**
|
|
34
36
|
* Transaction message to send
|
|
35
37
|
*/
|
|
36
38
|
export interface TransactionMessage {
|
|
37
|
-
/** Recipient address
|
|
39
|
+
/** Recipient address */
|
|
38
40
|
address: string;
|
|
39
|
-
/** Amount in nanotons
|
|
41
|
+
/** Amount in nanotons */
|
|
40
42
|
amount: string;
|
|
41
43
|
/** Optional message payload (base64 encoded) */
|
|
42
44
|
payload?: string;
|
|
@@ -51,128 +53,21 @@ export interface SendTransactionRequest {
|
|
|
51
53
|
validUntil: number;
|
|
52
54
|
/** Array of messages to send */
|
|
53
55
|
messages: TransactionMessage[];
|
|
54
|
-
/** Optional network
|
|
56
|
+
/** Optional network */
|
|
55
57
|
network?: 'mainnet' | 'testnet';
|
|
56
58
|
/** Optional from address */
|
|
57
59
|
from?: string;
|
|
58
60
|
}
|
|
59
61
|
/**
|
|
60
|
-
*
|
|
61
|
-
*/
|
|
62
|
-
export interface TransactionResponse {
|
|
63
|
-
/** Base64 encoded BOC of the transaction */
|
|
64
|
-
boc: string;
|
|
65
|
-
/** Transaction signature */
|
|
66
|
-
signature: string;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Connection request payload (sent to wallet)
|
|
70
|
-
*/
|
|
71
|
-
export interface ConnectionRequestPayload {
|
|
72
|
-
/** Manifest URL for the app */
|
|
73
|
-
manifestUrl: string;
|
|
74
|
-
/** Items requested from wallet */
|
|
75
|
-
items: Array<{
|
|
76
|
-
name: 'ton_addr';
|
|
77
|
-
}>;
|
|
78
|
-
/** Return strategy - how wallet should return to the app */
|
|
79
|
-
returnStrategy?: 'back' | 'post_redirect' | 'none';
|
|
80
|
-
/** Return scheme for mobile apps (required by many wallets for proper callback handling) */
|
|
81
|
-
returnScheme?: string;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Connection response payload (received from wallet)
|
|
85
|
-
*/
|
|
86
|
-
export interface ConnectionResponsePayload {
|
|
87
|
-
/** Session ID */
|
|
88
|
-
session: string;
|
|
89
|
-
/** Wallet information */
|
|
90
|
-
name: string;
|
|
91
|
-
/** Wallet app name */
|
|
92
|
-
appName: string;
|
|
93
|
-
/** Wallet version */
|
|
94
|
-
version: string;
|
|
95
|
-
/** Platform */
|
|
96
|
-
platform: 'ios' | 'android' | 'unknown';
|
|
97
|
-
/** TON address */
|
|
98
|
-
address: string;
|
|
99
|
-
/** Public key in hex */
|
|
100
|
-
publicKey: string;
|
|
101
|
-
/** Wallet icon URL */
|
|
102
|
-
icon?: string;
|
|
103
|
-
/** Proof (signature) for verification */
|
|
104
|
-
proof?: {
|
|
105
|
-
timestamp: number;
|
|
106
|
-
domain: {
|
|
107
|
-
lengthBytes: number;
|
|
108
|
-
value: string;
|
|
109
|
-
};
|
|
110
|
-
signature: string;
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Transaction request payload (sent to wallet)
|
|
115
|
-
*/
|
|
116
|
-
export interface TransactionRequestPayload {
|
|
117
|
-
/** Manifest URL */
|
|
118
|
-
manifestUrl: string;
|
|
119
|
-
/** Transaction request */
|
|
120
|
-
request: {
|
|
121
|
-
/** Unix timestamp (ms) when request expires */
|
|
122
|
-
validUntil: number;
|
|
123
|
-
/** Array of messages */
|
|
124
|
-
messages: Array<{
|
|
125
|
-
address: string;
|
|
126
|
-
amount: string;
|
|
127
|
-
payload?: string;
|
|
128
|
-
stateInit?: string;
|
|
129
|
-
}>;
|
|
130
|
-
/** Optional network */
|
|
131
|
-
network?: 'mainnet' | 'testnet';
|
|
132
|
-
/** Optional from address */
|
|
133
|
-
from?: string;
|
|
134
|
-
};
|
|
135
|
-
/** Return URL scheme (for mobile apps) */
|
|
136
|
-
returnScheme?: string;
|
|
137
|
-
/** Return strategy */
|
|
138
|
-
returnStrategy?: 'back' | 'post_redirect' | 'none';
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Transaction response payload (received from wallet)
|
|
142
|
-
*/
|
|
143
|
-
export interface TransactionResponsePayload {
|
|
144
|
-
/** Base64 encoded BOC */
|
|
145
|
-
boc: string;
|
|
146
|
-
/** Transaction signature */
|
|
147
|
-
signature: string;
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Error response from wallet
|
|
151
|
-
*/
|
|
152
|
-
export interface ErrorResponse {
|
|
153
|
-
/** Error code */
|
|
154
|
-
error: {
|
|
155
|
-
code: number;
|
|
156
|
-
message: string;
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Platform adapter interface for deep linking and storage
|
|
62
|
+
* Platform adapter interface
|
|
161
63
|
*/
|
|
162
64
|
export interface PlatformAdapter {
|
|
163
|
-
/** Open a deep link URL */
|
|
164
65
|
openURL(url: string, skipCanOpenURLCheck?: boolean): Promise<boolean>;
|
|
165
|
-
/** Get initial URL when app was opened via deep link */
|
|
166
66
|
getInitialURL(): Promise<string | null>;
|
|
167
|
-
/** Add listener for URL changes */
|
|
168
67
|
addURLListener(callback: (url: string) => void): () => void;
|
|
169
|
-
/** Store data */
|
|
170
68
|
setItem(key: string, value: string): Promise<void>;
|
|
171
|
-
/** Retrieve data */
|
|
172
69
|
getItem(key: string): Promise<string | null>;
|
|
173
|
-
/** Remove data */
|
|
174
70
|
removeItem(key: string): Promise<void>;
|
|
175
|
-
/** Generate random bytes */
|
|
176
71
|
randomBytes(length: number): Promise<Uint8Array>;
|
|
177
72
|
}
|
|
178
73
|
/**
|
|
@@ -185,7 +80,7 @@ export type Network = 'mainnet' | 'testnet';
|
|
|
185
80
|
export interface TonConnectMobileConfig {
|
|
186
81
|
/** Manifest URL (required) */
|
|
187
82
|
manifestUrl: string;
|
|
188
|
-
/** Deep link scheme for callbacks (required) */
|
|
83
|
+
/** Deep link scheme for callbacks (required for return strategy) */
|
|
189
84
|
scheme: string;
|
|
190
85
|
/** Optional storage key prefix */
|
|
191
86
|
storageKeyPrefix?: string;
|
|
@@ -193,29 +88,21 @@ export interface TonConnectMobileConfig {
|
|
|
193
88
|
connectionTimeout?: number;
|
|
194
89
|
/** Optional transaction timeout in ms (default: 300000 = 5 minutes) */
|
|
195
90
|
transactionTimeout?: number;
|
|
196
|
-
/** Skip canOpenURL check
|
|
197
|
-
* Set to false if you want to check if URL can be opened before attempting to open it.
|
|
198
|
-
* Note: On Android, canOpenURL may return false for tonconnect:// even if wallet is installed.
|
|
199
|
-
*/
|
|
91
|
+
/** Skip canOpenURL check (default: true) */
|
|
200
92
|
skipCanOpenURLCheck?: boolean;
|
|
201
|
-
/** Preferred wallet name
|
|
202
|
-
* If not specified, will use default wallet (Tonkeeper)
|
|
203
|
-
* Available: 'Tonkeeper', 'MyTonWallet', 'Wallet in Telegram', 'Tonhub'
|
|
204
|
-
*/
|
|
93
|
+
/** Preferred wallet name */
|
|
205
94
|
preferredWallet?: string;
|
|
206
|
-
/** Network (
|
|
95
|
+
/** Network (default: 'mainnet') */
|
|
207
96
|
network?: Network;
|
|
208
|
-
/** TON API endpoint
|
|
209
|
-
* Default: 'https://toncenter.com/api/v2' for mainnet, 'https://testnet.toncenter.com/api/v2' for testnet
|
|
210
|
-
*/
|
|
97
|
+
/** Custom TON API endpoint */
|
|
211
98
|
tonApiEndpoint?: string;
|
|
212
99
|
}
|
|
213
100
|
/**
|
|
214
|
-
*
|
|
101
|
+
* Status change callback
|
|
215
102
|
*/
|
|
216
103
|
export type StatusChangeCallback = (status: ConnectionStatus) => void;
|
|
217
104
|
/**
|
|
218
|
-
* Event types
|
|
105
|
+
* Event types
|
|
219
106
|
*/
|
|
220
107
|
export type TonConnectEventType = 'connect' | 'disconnect' | 'transaction' | 'error' | 'statusChange';
|
|
221
108
|
/**
|
|
@@ -230,23 +117,81 @@ export type TransactionStatus = 'pending' | 'confirmed' | 'failed' | 'unknown';
|
|
|
230
117
|
* Transaction status response
|
|
231
118
|
*/
|
|
232
119
|
export interface TransactionStatusResponse {
|
|
233
|
-
/** Transaction status */
|
|
234
120
|
status: TransactionStatus;
|
|
235
|
-
/** Transaction hash (if available) */
|
|
236
121
|
hash?: string;
|
|
237
|
-
/** Block number (if confirmed) */
|
|
238
122
|
blockNumber?: number;
|
|
239
|
-
/** Error message (if failed) */
|
|
240
123
|
error?: string;
|
|
241
124
|
}
|
|
242
125
|
/**
|
|
243
126
|
* Balance response
|
|
244
127
|
*/
|
|
245
128
|
export interface BalanceResponse {
|
|
246
|
-
/** Balance in nanotons */
|
|
247
129
|
balance: string;
|
|
248
|
-
/** Balance in TON (formatted) */
|
|
249
130
|
balanceTon: string;
|
|
250
|
-
/** Network */
|
|
251
131
|
network: Network;
|
|
252
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Connect event from wallet (received via bridge after decryption)
|
|
135
|
+
*/
|
|
136
|
+
export interface ConnectEvent {
|
|
137
|
+
event: 'connect';
|
|
138
|
+
id: number;
|
|
139
|
+
payload: {
|
|
140
|
+
items: Array<{
|
|
141
|
+
name: string;
|
|
142
|
+
address: string;
|
|
143
|
+
network: string;
|
|
144
|
+
publicKey: string;
|
|
145
|
+
walletStateInit?: string;
|
|
146
|
+
[key: string]: any;
|
|
147
|
+
}>;
|
|
148
|
+
device: {
|
|
149
|
+
platform: string;
|
|
150
|
+
appName: string;
|
|
151
|
+
appVersion: string;
|
|
152
|
+
maxProtocolVersion: number;
|
|
153
|
+
features: any[];
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Connect error event from wallet
|
|
159
|
+
*/
|
|
160
|
+
export interface ConnectErrorEvent {
|
|
161
|
+
event: 'connect_error';
|
|
162
|
+
id: number;
|
|
163
|
+
payload: {
|
|
164
|
+
code: number;
|
|
165
|
+
message: string;
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* JSON-RPC response (success)
|
|
170
|
+
*/
|
|
171
|
+
export interface RpcResponse {
|
|
172
|
+
result: string;
|
|
173
|
+
id: number;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* JSON-RPC response (error)
|
|
177
|
+
*/
|
|
178
|
+
export interface RpcErrorResponse {
|
|
179
|
+
error: {
|
|
180
|
+
code: number;
|
|
181
|
+
message: string;
|
|
182
|
+
};
|
|
183
|
+
id: number;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Persisted session data
|
|
187
|
+
*/
|
|
188
|
+
export interface PersistedSession {
|
|
189
|
+
/** Hex-encoded session secret key */
|
|
190
|
+
sessionSecretKey: string;
|
|
191
|
+
/** Hex-encoded wallet public key (from bridge "from" field) */
|
|
192
|
+
walletPublicKey: string;
|
|
193
|
+
/** Wallet bridge URL */
|
|
194
|
+
bridgeUrl: string;
|
|
195
|
+
/** Wallet info */
|
|
196
|
+
wallet: WalletInfo;
|
|
197
|
+
}
|
package/dist/types/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blazium/ton-connect-mobile",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
4
4
|
"description": "Production-ready TON Connect Mobile SDK for React Native and Expo. Implements the real TonConnect protocol for mobile applications using deep links and callbacks.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -66,5 +66,4 @@
|
|
|
66
66
|
"dist",
|
|
67
67
|
"src"
|
|
68
68
|
]
|
|
69
|
-
}
|
|
70
|
-
|
|
69
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TON Connect v2 Bridge Gateway
|
|
3
|
+
* Handles SSE connection for receiving wallet responses and POST for sending messages
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { bytesToBase64, base64ToBytes } from './session';
|
|
7
|
+
|
|
8
|
+
// Type declarations
|
|
9
|
+
declare const XMLHttpRequest: {
|
|
10
|
+
new (): XMLHttpRequestInstance;
|
|
11
|
+
} | undefined;
|
|
12
|
+
|
|
13
|
+
interface XMLHttpRequestInstance {
|
|
14
|
+
readyState: number;
|
|
15
|
+
responseText: string;
|
|
16
|
+
status: number;
|
|
17
|
+
open(method: string, url: string, async?: boolean): void;
|
|
18
|
+
setRequestHeader(name: string, value: string): void;
|
|
19
|
+
send(data?: string | null): void;
|
|
20
|
+
abort(): void;
|
|
21
|
+
onreadystatechange: (() => void) | null;
|
|
22
|
+
onerror: (() => void) | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parsed SSE event
|
|
27
|
+
*/
|
|
28
|
+
interface SSEEvent {
|
|
29
|
+
id?: string;
|
|
30
|
+
data?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Bridge message received from wallet
|
|
35
|
+
*/
|
|
36
|
+
export interface BridgeIncomingMessage {
|
|
37
|
+
from: string; // hex-encoded sender public key
|
|
38
|
+
message: string; // base64-encoded encrypted message
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Bridge Gateway for TON Connect v2 HTTP Bridge
|
|
43
|
+
*/
|
|
44
|
+
export class BridgeGateway {
|
|
45
|
+
private xhr: XMLHttpRequestInstance | null = null;
|
|
46
|
+
private lastEventId: string = '';
|
|
47
|
+
private active: boolean = false;
|
|
48
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
49
|
+
private bridgeUrl: string = '';
|
|
50
|
+
private clientId: string = '';
|
|
51
|
+
private onMessageCallback: ((msg: BridgeIncomingMessage) => void) | null = null;
|
|
52
|
+
private onErrorCallback: ((error: Error) => void) | null = null;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Connect to the bridge SSE endpoint
|
|
56
|
+
* Listens for incoming messages from the wallet
|
|
57
|
+
*/
|
|
58
|
+
connect(
|
|
59
|
+
bridgeUrl: string,
|
|
60
|
+
clientId: string,
|
|
61
|
+
onMessage: (msg: BridgeIncomingMessage) => void,
|
|
62
|
+
onError?: (error: Error) => void
|
|
63
|
+
): void {
|
|
64
|
+
this.bridgeUrl = bridgeUrl;
|
|
65
|
+
this.clientId = clientId;
|
|
66
|
+
this.onMessageCallback = onMessage;
|
|
67
|
+
this.onErrorCallback = onError || null;
|
|
68
|
+
this.active = true;
|
|
69
|
+
this.openSSE();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Open SSE connection using XMLHttpRequest (works in React Native)
|
|
74
|
+
*/
|
|
75
|
+
private openSSE(): void {
|
|
76
|
+
if (!this.active) return;
|
|
77
|
+
|
|
78
|
+
// Build URL
|
|
79
|
+
let url = `${this.bridgeUrl}/events?client_id=${this.clientId}`;
|
|
80
|
+
if (this.lastEventId) {
|
|
81
|
+
url += `&last_event_id=${this.lastEventId}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log('[Bridge] Opening SSE connection:', url);
|
|
85
|
+
|
|
86
|
+
// Use XMLHttpRequest for SSE (available in React Native)
|
|
87
|
+
if (typeof XMLHttpRequest === 'undefined') {
|
|
88
|
+
// Fallback: use fetch-based polling
|
|
89
|
+
this.pollWithFetch(url);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const xhr = new XMLHttpRequest();
|
|
94
|
+
this.xhr = xhr;
|
|
95
|
+
|
|
96
|
+
let processedLength = 0;
|
|
97
|
+
let buffer = '';
|
|
98
|
+
|
|
99
|
+
xhr.onreadystatechange = () => {
|
|
100
|
+
// LOADING (3) or DONE (4) — process incoming data
|
|
101
|
+
if (xhr.readyState >= 3) {
|
|
102
|
+
try {
|
|
103
|
+
const newData = xhr.responseText.substring(processedLength);
|
|
104
|
+
processedLength = xhr.responseText.length;
|
|
105
|
+
|
|
106
|
+
if (newData) {
|
|
107
|
+
buffer += newData;
|
|
108
|
+
const parsed = this.parseSSE(buffer);
|
|
109
|
+
buffer = parsed.remaining;
|
|
110
|
+
|
|
111
|
+
for (const event of parsed.events) {
|
|
112
|
+
if (event.id) {
|
|
113
|
+
this.lastEventId = event.id;
|
|
114
|
+
}
|
|
115
|
+
if (event.data) {
|
|
116
|
+
this.handleEventData(event.data);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (e) {
|
|
121
|
+
// Ignore parse errors, continue listening
|
|
122
|
+
console.warn('[Bridge] SSE parse error:', e);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Connection closed (readyState 4) — reconnect if still active
|
|
127
|
+
if (xhr.readyState === 4 && this.active) {
|
|
128
|
+
console.log('[Bridge] SSE connection closed, reconnecting...');
|
|
129
|
+
this.scheduleReconnect();
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
xhr.onerror = () => {
|
|
134
|
+
console.error('[Bridge] SSE connection error');
|
|
135
|
+
if (this.active) {
|
|
136
|
+
this.scheduleReconnect();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
xhr.open('GET', url, true);
|
|
141
|
+
xhr.setRequestHeader('Accept', 'text/event-stream');
|
|
142
|
+
xhr.send();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Fallback: poll bridge with fetch (for environments without XMLHttpRequest streaming)
|
|
147
|
+
*/
|
|
148
|
+
private async pollWithFetch(url: string): Promise<void> {
|
|
149
|
+
while (this.active) {
|
|
150
|
+
try {
|
|
151
|
+
let pollUrl = `${this.bridgeUrl}/events?client_id=${this.clientId}`;
|
|
152
|
+
if (this.lastEventId) {
|
|
153
|
+
pollUrl += `&last_event_id=${this.lastEventId}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const controller = new AbortController();
|
|
157
|
+
const timeoutId = setTimeout(() => controller.abort(), 25000);
|
|
158
|
+
|
|
159
|
+
const response = await fetch(pollUrl, {
|
|
160
|
+
headers: { 'Accept': 'text/event-stream' },
|
|
161
|
+
signal: controller.signal,
|
|
162
|
+
});
|
|
163
|
+
clearTimeout(timeoutId);
|
|
164
|
+
|
|
165
|
+
const text = await response.text();
|
|
166
|
+
const parsed = this.parseSSE(text);
|
|
167
|
+
|
|
168
|
+
for (const event of parsed.events) {
|
|
169
|
+
if (event.id) {
|
|
170
|
+
this.lastEventId = event.id;
|
|
171
|
+
}
|
|
172
|
+
if (event.data) {
|
|
173
|
+
this.handleEventData(event.data);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch (error: any) {
|
|
177
|
+
if (error?.name === 'AbortError') {
|
|
178
|
+
// Timeout — reconnect
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
console.error('[Bridge] Fetch poll error:', error);
|
|
182
|
+
// Wait before retrying
|
|
183
|
+
await new Promise<void>((resolve) => setTimeout(() => resolve(), 2000));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Handle a single SSE event data
|
|
190
|
+
*/
|
|
191
|
+
private handleEventData(data: string): void {
|
|
192
|
+
try {
|
|
193
|
+
const parsed = JSON.parse(data) as BridgeIncomingMessage;
|
|
194
|
+
if (parsed.from && parsed.message) {
|
|
195
|
+
console.log('[Bridge] Received message from:', parsed.from.substring(0, 16) + '...');
|
|
196
|
+
if (this.onMessageCallback) {
|
|
197
|
+
this.onMessageCallback(parsed);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} catch (e) {
|
|
201
|
+
// Might be a heartbeat or other non-JSON data — ignore
|
|
202
|
+
console.log('[Bridge] Non-message event:', data.substring(0, 50));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Schedule reconnection after a delay
|
|
208
|
+
*/
|
|
209
|
+
private scheduleReconnect(): void {
|
|
210
|
+
if (this.reconnectTimer) {
|
|
211
|
+
clearTimeout(this.reconnectTimer);
|
|
212
|
+
}
|
|
213
|
+
this.reconnectTimer = setTimeout(() => {
|
|
214
|
+
if (this.active) {
|
|
215
|
+
this.openSSE();
|
|
216
|
+
}
|
|
217
|
+
}, 1500);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Send an encrypted message via the bridge
|
|
222
|
+
*/
|
|
223
|
+
async send(
|
|
224
|
+
bridgeUrl: string,
|
|
225
|
+
fromClientId: string,
|
|
226
|
+
toClientId: string,
|
|
227
|
+
encryptedMessage: Uint8Array,
|
|
228
|
+
ttl: number = 300
|
|
229
|
+
): Promise<void> {
|
|
230
|
+
const base64Message = bytesToBase64(encryptedMessage);
|
|
231
|
+
const url = `${bridgeUrl}/message?client_id=${fromClientId}&to=${toClientId}&ttl=${ttl}`;
|
|
232
|
+
|
|
233
|
+
console.log('[Bridge] Sending message to:', toClientId.substring(0, 16) + '...');
|
|
234
|
+
|
|
235
|
+
const response = await fetch(url, {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
body: base64Message,
|
|
238
|
+
headers: {
|
|
239
|
+
'Content-Type': 'text/plain',
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (!response.ok) {
|
|
244
|
+
throw new Error(`Bridge send failed: ${response.status} ${response.statusText}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
console.log('[Bridge] Message sent successfully');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Close the bridge connection
|
|
252
|
+
*/
|
|
253
|
+
close(): void {
|
|
254
|
+
console.log('[Bridge] Closing connection');
|
|
255
|
+
this.active = false;
|
|
256
|
+
|
|
257
|
+
if (this.xhr) {
|
|
258
|
+
this.xhr.abort();
|
|
259
|
+
this.xhr = null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (this.reconnectTimer) {
|
|
263
|
+
clearTimeout(this.reconnectTimer);
|
|
264
|
+
this.reconnectTimer = null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
this.onMessageCallback = null;
|
|
268
|
+
this.onErrorCallback = null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Check if bridge is currently connected/active
|
|
273
|
+
*/
|
|
274
|
+
get isConnected(): boolean {
|
|
275
|
+
return this.active;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Parse SSE text into events
|
|
280
|
+
*/
|
|
281
|
+
private parseSSE(text: string): { events: SSEEvent[]; remaining: string } {
|
|
282
|
+
const events: SSEEvent[] = [];
|
|
283
|
+
const parts = text.split('\n\n');
|
|
284
|
+
const remaining = parts.pop() || '';
|
|
285
|
+
|
|
286
|
+
for (const part of parts) {
|
|
287
|
+
if (!part.trim()) continue;
|
|
288
|
+
|
|
289
|
+
const event: SSEEvent = {};
|
|
290
|
+
const lines = part.split('\n');
|
|
291
|
+
|
|
292
|
+
for (const line of lines) {
|
|
293
|
+
if (line.startsWith('id:')) {
|
|
294
|
+
event.id = line.substring(3).trim();
|
|
295
|
+
} else if (line.startsWith('data:')) {
|
|
296
|
+
event.data = (event.data || '') + line.substring(5).trim();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (event.data || event.id) {
|
|
301
|
+
events.push(event);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { events, remaining };
|
|
306
|
+
}
|
|
307
|
+
}
|