Skip to content

Commit 6cc0560

Browse files
authored
fix: browser WebSocket authentication via query param (#38)
* fix: use query param auth for WebSocket in browser environments Browsers' native WebSocket doesn't support custom headers, so the Authorization header was being interpreted as a subprotocol string. Detect browser vs Node.js and pass apiKey as a URL query parameter in browsers while keeping header auth for Node.js.
1 parent bf92469 commit 6cc0560

16 files changed

Lines changed: 758 additions & 27 deletions

File tree

.changeset/config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"relayer-ws",
1515
"bundler-ws",
1616
"relayer-get-balance",
17-
"bundler-get-balance"
17+
"bundler-get-balance",
18+
"browser-ws"
1819
],
1920
"linked": [],
2021
"updateInternalDependencies": "patch"

.changeset/tricky-carrots-sniff.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@gelatocloud/gasless": patch
3+
---
4+
5+
fix: ws auth for browsers

examples/account/ws/src/global.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ const main = async () => {
6060
console.log(`[reverted] Transaction ${data.id} reverted on block ${data.receipt.blockNumber}`)
6161
);
6262

63-
const taskId = await relayer.sendTransaction({
63+
const id = await relayer.sendTransaction({
6464
calls: [
6565
{
6666
data: '0xd09de08a',
@@ -69,7 +69,7 @@ const main = async () => {
6969
]
7070
});
7171

72-
console.log(`Sent transaction ${taskId}`);
72+
console.log(`Sent transaction ${id}`);
7373

7474
// Graceful shutdown
7575
process.on('SIGINT', async () => {

examples/browser-ws/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
VITE_GELATO_API_KEY="Get your api key at https://app.gelato.cloud/"

examples/browser-ws/index.html

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Gasless WebSocket Test</title>
7+
<style>
8+
* {
9+
box-sizing: border-box;
10+
padding: 0;
11+
margin: 0;
12+
}
13+
body {
14+
max-width: 720px;
15+
padding: 24px;
16+
margin: 0 auto;
17+
font-family: system-ui, -apple-system, sans-serif;
18+
color: #e0e0e0;
19+
background: #0a0a0a;
20+
}
21+
h1 {
22+
margin-bottom: 20px;
23+
font-size: 1.4rem;
24+
color: #fff;
25+
}
26+
.controls {
27+
display: flex;
28+
flex-wrap: wrap;
29+
gap: 8px;
30+
margin-bottom: 16px;
31+
}
32+
input {
33+
flex: 1;
34+
min-width: 200px;
35+
padding: 8px 12px;
36+
font-size: 0.9rem;
37+
color: #e0e0e0;
38+
background: #1a1a1a;
39+
border: 1px solid #333;
40+
border-radius: 6px;
41+
}
42+
input::placeholder {
43+
color: #666;
44+
}
45+
button {
46+
padding: 8px 16px;
47+
font-size: 0.9rem;
48+
font-weight: 500;
49+
cursor: pointer;
50+
border: none;
51+
border-radius: 6px;
52+
transition: opacity 0.15s;
53+
}
54+
button:hover:not(:disabled) {
55+
opacity: 0.85;
56+
}
57+
button:disabled {
58+
cursor: not-allowed;
59+
opacity: 0.4;
60+
}
61+
#connectBtn {
62+
color: #fff;
63+
background: #2563eb;
64+
}
65+
#sendBtn {
66+
color: #fff;
67+
background: #16a34a;
68+
}
69+
#disconnectBtn {
70+
color: #fff;
71+
background: #dc2626;
72+
}
73+
.status {
74+
margin-bottom: 12px;
75+
font-size: 0.85rem;
76+
color: #888;
77+
}
78+
.status .dot {
79+
display: inline-block;
80+
width: 8px;
81+
height: 8px;
82+
margin-right: 6px;
83+
background: #666;
84+
border-radius: 50%;
85+
}
86+
.status .dot.connected {
87+
background: #22c55e;
88+
}
89+
#log {
90+
height: 400px;
91+
padding: 12px;
92+
overflow-y: auto;
93+
font-family: "SF Mono", "Fira Code", monospace;
94+
font-size: 0.8rem;
95+
line-height: 1.6;
96+
background: #111;
97+
border: 1px solid #222;
98+
border-radius: 8px;
99+
}
100+
.log-entry {
101+
padding: 2px 0;
102+
border-bottom: 1px solid #1a1a1a;
103+
}
104+
.log-entry .time {
105+
margin-right: 8px;
106+
color: #555;
107+
}
108+
.log-entry.submitted {
109+
color: #60a5fa;
110+
}
111+
.log-entry.success {
112+
color: #4ade80;
113+
}
114+
.log-entry.rejected {
115+
color: #f87171;
116+
}
117+
.log-entry.reverted {
118+
color: #fb923c;
119+
}
120+
.log-entry.info {
121+
color: #888;
122+
}
123+
.log-entry.error {
124+
color: #f87171;
125+
}
126+
</style>
127+
</head>
128+
<body>
129+
<h1>Gasless WebSocket Test</h1>
130+
<div class="controls">
131+
<input id="apiKey" type="text" placeholder="Enter Gelato API Key" />
132+
<button id="connectBtn">Connect</button>
133+
<button id="sendBtn" disabled>Send Tx</button>
134+
<button id="disconnectBtn" disabled>Disconnect</button>
135+
</div>
136+
<div class="status">
137+
<span class="dot" id="statusDot"></span>
138+
<span id="statusText">Disconnected</span>
139+
</div>
140+
<div id="log"></div>
141+
<script type="module" src="/src/main.ts"></script>
142+
</body>
143+
</html>

examples/browser-ws/package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "browser-ws",
3+
"private": true,
4+
"version": "0.0.1",
5+
"scripts": {
6+
"dev": "vite"
7+
},
8+
"dependencies": {
9+
"@gelatocloud/gasless": "workspace:*",
10+
"viem": "catalog:"
11+
},
12+
"devDependencies": {
13+
"typescript": "catalog:",
14+
"vite": "^6.3.5"
15+
}
16+
}

examples/browser-ws/src/main.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import type { GelatoEvmRelayerClient } from '@gelatocloud/gasless';
2+
import { createGelatoEvmRelayerClient } from '@gelatocloud/gasless';
3+
import { baseSepolia } from 'viem/chains';
4+
5+
const chain = baseSepolia;
6+
const TARGET_CONTRACT = '0xE27C1359cf02B49acC6474311Bd79d1f10b1f8De';
7+
const INCREMENT_CALLDATA = '0xd09de08a';
8+
9+
// Pre-fill API key from env (Vite exposes VITE_-prefixed vars)
10+
const envApiKey = import.meta.env['VITE_GELATO_API_KEY'] as string | undefined;
11+
12+
// DOM elements
13+
const apiKeyInput = document.getElementById('apiKey') as HTMLInputElement;
14+
const connectBtn = document.getElementById('connectBtn') as HTMLButtonElement;
15+
const sendBtn = document.getElementById('sendBtn') as HTMLButtonElement;
16+
const disconnectBtn = document.getElementById('disconnectBtn') as HTMLButtonElement;
17+
const statusDot = document.getElementById('statusDot') as HTMLSpanElement;
18+
const statusText = document.getElementById('statusText') as HTMLSpanElement;
19+
const logEl = document.getElementById('log') as HTMLDivElement;
20+
21+
if (envApiKey) apiKeyInput.value = envApiKey;
22+
23+
let relayer: GelatoEvmRelayerClient | null = null;
24+
let subscriptionId: string | null = null;
25+
26+
function log(
27+
message: string,
28+
type: 'info' | 'submitted' | 'success' | 'rejected' | 'reverted' | 'error' = 'info'
29+
) {
30+
const entry = document.createElement('div');
31+
entry.className = `log-entry ${type}`;
32+
const time = new Date().toLocaleTimeString();
33+
entry.innerHTML = `<span class="time">${time}</span>${message}`;
34+
logEl.appendChild(entry);
35+
logEl.scrollTop = logEl.scrollHeight;
36+
}
37+
38+
function setConnected(connected: boolean) {
39+
connectBtn.disabled = connected;
40+
apiKeyInput.disabled = connected;
41+
sendBtn.disabled = !connected;
42+
disconnectBtn.disabled = !connected;
43+
statusDot.className = connected ? 'dot connected' : 'dot';
44+
statusText.textContent = connected ? 'Connected' : 'Disconnected';
45+
}
46+
47+
connectBtn.addEventListener('click', async () => {
48+
const apiKey = apiKeyInput.value.trim();
49+
if (!apiKey) {
50+
log('Please enter an API key', 'error');
51+
return;
52+
}
53+
54+
try {
55+
log('Creating relayer client...');
56+
57+
relayer = createGelatoEvmRelayerClient({
58+
apiKey,
59+
testnet: chain.testnet
60+
});
61+
62+
log('Subscribing to transaction updates...');
63+
const subscription = await relayer.ws.subscribe();
64+
subscriptionId = subscription.subscriptionId;
65+
66+
subscription.on('submitted', (data) =>
67+
log(`[submitted] Transaction ${data.id} submitted with hash: ${data.hash}`, 'submitted')
68+
);
69+
subscription.on('success', (data) =>
70+
log(
71+
`[success] Transaction ${data.id} included in block ${data.receipt.blockNumber}`,
72+
'success'
73+
)
74+
);
75+
subscription.on('rejected', (data) =>
76+
log(`[rejected] Transaction ${data.id} was rejected: ${data.message}`, 'rejected')
77+
);
78+
subscription.on('reverted', (data) =>
79+
log(
80+
`[reverted] Transaction ${data.id} reverted on block ${data.receipt.blockNumber}`,
81+
'reverted'
82+
)
83+
);
84+
85+
setConnected(true);
86+
log('Connected! Listening for transaction updates...');
87+
} catch (err) {
88+
log(`Connection failed: ${err instanceof Error ? err.message : err}`, 'error');
89+
}
90+
});
91+
92+
sendBtn.addEventListener('click', async () => {
93+
if (!relayer) return;
94+
95+
try {
96+
sendBtn.disabled = true;
97+
log(`Sending increment() to ${TARGET_CONTRACT} on ${chain.name}...`);
98+
99+
const id = await relayer.sendTransaction({
100+
chainId: chain.id,
101+
data: INCREMENT_CALLDATA,
102+
to: TARGET_CONTRACT
103+
});
104+
105+
log(`Transaction sent: ${id}`);
106+
sendBtn.disabled = false;
107+
} catch (err) {
108+
log(`Send failed: ${err instanceof Error ? err.message : err}`, 'error');
109+
sendBtn.disabled = false;
110+
}
111+
});
112+
113+
disconnectBtn.addEventListener('click', async () => {
114+
if (!relayer) return;
115+
116+
try {
117+
if (subscriptionId) {
118+
await relayer.ws.unsubscribe(subscriptionId);
119+
subscriptionId = null;
120+
}
121+
relayer.ws.disconnect();
122+
relayer = null;
123+
setConnected(false);
124+
log('Disconnected.');
125+
} catch (err) {
126+
log(`Disconnect error: ${err instanceof Error ? err.message : err}`, 'error');
127+
}
128+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/// <reference types="vite/client" />

examples/browser-ws/tsconfig.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"compilerOptions": {
4+
"lib": ["DOM", "ESNext"]
5+
}
6+
}

examples/browser-ws/vite.config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { resolve } from 'node:path';
2+
import { defineConfig } from 'vite';
3+
4+
export default defineConfig({
5+
resolve: {
6+
alias: {
7+
'@gelatocloud/gasless': resolve(__dirname, '../../src/index.ts')
8+
}
9+
}
10+
});

0 commit comments

Comments
 (0)