diff --git a/.changeset/config.json b/.changeset/config.json
index e8cc3ed..12e1987 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -14,7 +14,8 @@
"relayer-ws",
"bundler-ws",
"relayer-get-balance",
- "bundler-get-balance"
+ "bundler-get-balance",
+ "browser-ws"
],
"linked": [],
"updateInternalDependencies": "patch"
diff --git a/.changeset/tricky-carrots-sniff.md b/.changeset/tricky-carrots-sniff.md
new file mode 100644
index 0000000..78d6de7
--- /dev/null
+++ b/.changeset/tricky-carrots-sniff.md
@@ -0,0 +1,5 @@
+---
+"@gelatocloud/gasless": patch
+---
+
+fix: ws auth for browsers
diff --git a/examples/account/ws/src/global.ts b/examples/account/ws/src/global.ts
index 856cfad..1f3b198 100644
--- a/examples/account/ws/src/global.ts
+++ b/examples/account/ws/src/global.ts
@@ -60,7 +60,7 @@ const main = async () => {
console.log(`[reverted] Transaction ${data.id} reverted on block ${data.receipt.blockNumber}`)
);
- const taskId = await relayer.sendTransaction({
+ const id = await relayer.sendTransaction({
calls: [
{
data: '0xd09de08a',
@@ -69,7 +69,7 @@ const main = async () => {
]
});
- console.log(`Sent transaction ${taskId}`);
+ console.log(`Sent transaction ${id}`);
// Graceful shutdown
process.on('SIGINT', async () => {
diff --git a/examples/browser-ws/.env.example b/examples/browser-ws/.env.example
new file mode 100644
index 0000000..2b09530
--- /dev/null
+++ b/examples/browser-ws/.env.example
@@ -0,0 +1 @@
+VITE_GELATO_API_KEY="Get your api key at https://app.gelato.cloud/"
diff --git a/examples/browser-ws/index.html b/examples/browser-ws/index.html
new file mode 100644
index 0000000..f662690
--- /dev/null
+++ b/examples/browser-ws/index.html
@@ -0,0 +1,143 @@
+
+
+
+
+
+ Gasless WebSocket Test
+
+
+
+ Gasless WebSocket Test
+
+
+
+
+
+
+
+
+ Disconnected
+
+
+
+
+
diff --git a/examples/browser-ws/package.json b/examples/browser-ws/package.json
new file mode 100644
index 0000000..18367e2
--- /dev/null
+++ b/examples/browser-ws/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "browser-ws",
+ "private": true,
+ "version": "0.0.1",
+ "scripts": {
+ "dev": "vite"
+ },
+ "dependencies": {
+ "@gelatocloud/gasless": "workspace:*",
+ "viem": "catalog:"
+ },
+ "devDependencies": {
+ "typescript": "catalog:",
+ "vite": "^6.3.5"
+ }
+}
diff --git a/examples/browser-ws/src/main.ts b/examples/browser-ws/src/main.ts
new file mode 100644
index 0000000..1c11428
--- /dev/null
+++ b/examples/browser-ws/src/main.ts
@@ -0,0 +1,128 @@
+import type { GelatoEvmRelayerClient } from '@gelatocloud/gasless';
+import { createGelatoEvmRelayerClient } from '@gelatocloud/gasless';
+import { baseSepolia } from 'viem/chains';
+
+const chain = baseSepolia;
+const TARGET_CONTRACT = '0xE27C1359cf02B49acC6474311Bd79d1f10b1f8De';
+const INCREMENT_CALLDATA = '0xd09de08a';
+
+// Pre-fill API key from env (Vite exposes VITE_-prefixed vars)
+const envApiKey = import.meta.env['VITE_GELATO_API_KEY'] as string | undefined;
+
+// DOM elements
+const apiKeyInput = document.getElementById('apiKey') as HTMLInputElement;
+const connectBtn = document.getElementById('connectBtn') as HTMLButtonElement;
+const sendBtn = document.getElementById('sendBtn') as HTMLButtonElement;
+const disconnectBtn = document.getElementById('disconnectBtn') as HTMLButtonElement;
+const statusDot = document.getElementById('statusDot') as HTMLSpanElement;
+const statusText = document.getElementById('statusText') as HTMLSpanElement;
+const logEl = document.getElementById('log') as HTMLDivElement;
+
+if (envApiKey) apiKeyInput.value = envApiKey;
+
+let relayer: GelatoEvmRelayerClient | null = null;
+let subscriptionId: string | null = null;
+
+function log(
+ message: string,
+ type: 'info' | 'submitted' | 'success' | 'rejected' | 'reverted' | 'error' = 'info'
+) {
+ const entry = document.createElement('div');
+ entry.className = `log-entry ${type}`;
+ const time = new Date().toLocaleTimeString();
+ entry.innerHTML = `${time}${message}`;
+ logEl.appendChild(entry);
+ logEl.scrollTop = logEl.scrollHeight;
+}
+
+function setConnected(connected: boolean) {
+ connectBtn.disabled = connected;
+ apiKeyInput.disabled = connected;
+ sendBtn.disabled = !connected;
+ disconnectBtn.disabled = !connected;
+ statusDot.className = connected ? 'dot connected' : 'dot';
+ statusText.textContent = connected ? 'Connected' : 'Disconnected';
+}
+
+connectBtn.addEventListener('click', async () => {
+ const apiKey = apiKeyInput.value.trim();
+ if (!apiKey) {
+ log('Please enter an API key', 'error');
+ return;
+ }
+
+ try {
+ log('Creating relayer client...');
+
+ relayer = createGelatoEvmRelayerClient({
+ apiKey,
+ testnet: chain.testnet
+ });
+
+ log('Subscribing to transaction updates...');
+ const subscription = await relayer.ws.subscribe();
+ subscriptionId = subscription.subscriptionId;
+
+ subscription.on('submitted', (data) =>
+ log(`[submitted] Transaction ${data.id} submitted with hash: ${data.hash}`, 'submitted')
+ );
+ subscription.on('success', (data) =>
+ log(
+ `[success] Transaction ${data.id} included in block ${data.receipt.blockNumber}`,
+ 'success'
+ )
+ );
+ subscription.on('rejected', (data) =>
+ log(`[rejected] Transaction ${data.id} was rejected: ${data.message}`, 'rejected')
+ );
+ subscription.on('reverted', (data) =>
+ log(
+ `[reverted] Transaction ${data.id} reverted on block ${data.receipt.blockNumber}`,
+ 'reverted'
+ )
+ );
+
+ setConnected(true);
+ log('Connected! Listening for transaction updates...');
+ } catch (err) {
+ log(`Connection failed: ${err instanceof Error ? err.message : err}`, 'error');
+ }
+});
+
+sendBtn.addEventListener('click', async () => {
+ if (!relayer) return;
+
+ try {
+ sendBtn.disabled = true;
+ log(`Sending increment() to ${TARGET_CONTRACT} on ${chain.name}...`);
+
+ const id = await relayer.sendTransaction({
+ chainId: chain.id,
+ data: INCREMENT_CALLDATA,
+ to: TARGET_CONTRACT
+ });
+
+ log(`Transaction sent: ${id}`);
+ sendBtn.disabled = false;
+ } catch (err) {
+ log(`Send failed: ${err instanceof Error ? err.message : err}`, 'error');
+ sendBtn.disabled = false;
+ }
+});
+
+disconnectBtn.addEventListener('click', async () => {
+ if (!relayer) return;
+
+ try {
+ if (subscriptionId) {
+ await relayer.ws.unsubscribe(subscriptionId);
+ subscriptionId = null;
+ }
+ relayer.ws.disconnect();
+ relayer = null;
+ setConnected(false);
+ log('Disconnected.');
+ } catch (err) {
+ log(`Disconnect error: ${err instanceof Error ? err.message : err}`, 'error');
+ }
+});
diff --git a/examples/browser-ws/src/vite-env.d.ts b/examples/browser-ws/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/examples/browser-ws/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/browser-ws/tsconfig.json b/examples/browser-ws/tsconfig.json
new file mode 100644
index 0000000..5c6637f
--- /dev/null
+++ b/examples/browser-ws/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "lib": ["DOM", "ESNext"]
+ }
+}
diff --git a/examples/browser-ws/vite.config.ts b/examples/browser-ws/vite.config.ts
new file mode 100644
index 0000000..f7aecab
--- /dev/null
+++ b/examples/browser-ws/vite.config.ts
@@ -0,0 +1,10 @@
+import { resolve } from 'node:path';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ resolve: {
+ alias: {
+ '@gelatocloud/gasless': resolve(__dirname, '../../src/index.ts')
+ }
+ }
+});
diff --git a/examples/relayer/sponsored/README.md b/examples/relayer/sponsored/README.md
index e62b806..812cd98 100644
--- a/examples/relayer/sponsored/README.md
+++ b/examples/relayer/sponsored/README.md
@@ -89,6 +89,6 @@ Polls until the transaction reaches a final state (Success, Rejected, or Reverte
| Concept | Description |
|---------|-------------|
| `sponsored()` | Gelato pays gas fees |
-| `sendTransaction` | Async - returns immediately with task ID |
+| `sendTransaction` | Async - returns immediately with ID |
| `waitForStatus` | Blocks until transaction is finalized |
| `StatusCode.Success` | Transaction successfully included on-chain |
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 36e6838..638f844 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -83,6 +83,22 @@ importers:
specifier: 'catalog:'
version: 5.9.3
+ examples/browser-ws:
+ dependencies:
+ '@gelatocloud/gasless':
+ specifier: workspace:*
+ version: link:../../src
+ viem:
+ specifier: 'catalog:'
+ version: 2.39.3(typescript@5.9.3)(zod@4.1.13)
+ devDependencies:
+ typescript:
+ specifier: 'catalog:'
+ version: 5.9.3
+ vite:
+ specifier: ^6.3.5
+ version: 6.4.1(@types/node@25.2.3)(tsx@4.21.0)
+
examples/bundler/gelato/sponsored:
dependencies:
'@gelatocloud/gasless':
@@ -403,156 +419,312 @@ packages:
'@changesets/write@0.4.0':
resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==}
+ '@esbuild/aix-ppc64@0.25.12':
+ resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
'@esbuild/aix-ppc64@0.27.1':
resolution: {integrity: sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
+ '@esbuild/android-arm64@0.25.12':
+ resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
'@esbuild/android-arm64@0.27.1':
resolution: {integrity: sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
+ '@esbuild/android-arm@0.25.12':
+ resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
'@esbuild/android-arm@0.27.1':
resolution: {integrity: sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
+ '@esbuild/android-x64@0.25.12':
+ resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
'@esbuild/android-x64@0.27.1':
resolution: {integrity: sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
+ '@esbuild/darwin-arm64@0.25.12':
+ resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
'@esbuild/darwin-arm64@0.27.1':
resolution: {integrity: sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
+ '@esbuild/darwin-x64@0.25.12':
+ resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
'@esbuild/darwin-x64@0.27.1':
resolution: {integrity: sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
+ '@esbuild/freebsd-arm64@0.25.12':
+ resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
'@esbuild/freebsd-arm64@0.27.1':
resolution: {integrity: sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
+ '@esbuild/freebsd-x64@0.25.12':
+ resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
'@esbuild/freebsd-x64@0.27.1':
resolution: {integrity: sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
+ '@esbuild/linux-arm64@0.25.12':
+ resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
'@esbuild/linux-arm64@0.27.1':
resolution: {integrity: sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
+ '@esbuild/linux-arm@0.25.12':
+ resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
'@esbuild/linux-arm@0.27.1':
resolution: {integrity: sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
+ '@esbuild/linux-ia32@0.25.12':
+ resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
'@esbuild/linux-ia32@0.27.1':
resolution: {integrity: sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
+ '@esbuild/linux-loong64@0.25.12':
+ resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
'@esbuild/linux-loong64@0.27.1':
resolution: {integrity: sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
+ '@esbuild/linux-mips64el@0.25.12':
+ resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
'@esbuild/linux-mips64el@0.27.1':
resolution: {integrity: sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
+ '@esbuild/linux-ppc64@0.25.12':
+ resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
'@esbuild/linux-ppc64@0.27.1':
resolution: {integrity: sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
+ '@esbuild/linux-riscv64@0.25.12':
+ resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
'@esbuild/linux-riscv64@0.27.1':
resolution: {integrity: sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
+ '@esbuild/linux-s390x@0.25.12':
+ resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
'@esbuild/linux-s390x@0.27.1':
resolution: {integrity: sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
+ '@esbuild/linux-x64@0.25.12':
+ resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
'@esbuild/linux-x64@0.27.1':
resolution: {integrity: sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
+ '@esbuild/netbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
'@esbuild/netbsd-arm64@0.27.1':
resolution: {integrity: sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
+ '@esbuild/netbsd-x64@0.25.12':
+ resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
'@esbuild/netbsd-x64@0.27.1':
resolution: {integrity: sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
+ '@esbuild/openbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
'@esbuild/openbsd-arm64@0.27.1':
resolution: {integrity: sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
+ '@esbuild/openbsd-x64@0.25.12':
+ resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
'@esbuild/openbsd-x64@0.27.1':
resolution: {integrity: sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
+ '@esbuild/openharmony-arm64@0.25.12':
+ resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
'@esbuild/openharmony-arm64@0.27.1':
resolution: {integrity: sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
+ '@esbuild/sunos-x64@0.25.12':
+ resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
'@esbuild/sunos-x64@0.27.1':
resolution: {integrity: sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
+ '@esbuild/win32-arm64@0.25.12':
+ resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
'@esbuild/win32-arm64@0.27.1':
resolution: {integrity: sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
+ '@esbuild/win32-ia32@0.25.12':
+ resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
'@esbuild/win32-ia32@0.27.1':
resolution: {integrity: sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
+ '@esbuild/win32-x64@0.25.12':
+ resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
'@esbuild/win32-x64@0.27.1':
resolution: {integrity: sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==}
engines: {node: '>=18'}
@@ -894,6 +1066,11 @@ packages:
es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
+ esbuild@0.25.12:
+ resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
esbuild@0.27.1:
resolution: {integrity: sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==}
engines: {node: '>=18'}
@@ -1351,19 +1528,19 @@ packages:
typescript:
optional: true
- vite@7.3.1:
- resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
- engines: {node: ^20.19.0 || >=22.12.0}
+ vite@6.4.1:
+ resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
- '@types/node': ^20.19.0 || >=22.12.0
+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
jiti: '>=1.21.0'
- less: ^4.0.0
+ less: '*'
lightningcss: ^1.21.0
- sass: ^1.70.0
- sass-embedded: ^1.70.0
- stylus: '>=0.54.8'
- sugarss: ^5.0.0
+ sass: '*'
+ sass-embedded: '*'
+ stylus: '*'
+ sugarss: '*'
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
@@ -1662,81 +1839,159 @@ snapshots:
human-id: 4.1.3
prettier: 2.8.8
+ '@esbuild/aix-ppc64@0.25.12':
+ optional: true
+
'@esbuild/aix-ppc64@0.27.1':
optional: true
+ '@esbuild/android-arm64@0.25.12':
+ optional: true
+
'@esbuild/android-arm64@0.27.1':
optional: true
+ '@esbuild/android-arm@0.25.12':
+ optional: true
+
'@esbuild/android-arm@0.27.1':
optional: true
+ '@esbuild/android-x64@0.25.12':
+ optional: true
+
'@esbuild/android-x64@0.27.1':
optional: true
+ '@esbuild/darwin-arm64@0.25.12':
+ optional: true
+
'@esbuild/darwin-arm64@0.27.1':
optional: true
+ '@esbuild/darwin-x64@0.25.12':
+ optional: true
+
'@esbuild/darwin-x64@0.27.1':
optional: true
+ '@esbuild/freebsd-arm64@0.25.12':
+ optional: true
+
'@esbuild/freebsd-arm64@0.27.1':
optional: true
+ '@esbuild/freebsd-x64@0.25.12':
+ optional: true
+
'@esbuild/freebsd-x64@0.27.1':
optional: true
+ '@esbuild/linux-arm64@0.25.12':
+ optional: true
+
'@esbuild/linux-arm64@0.27.1':
optional: true
+ '@esbuild/linux-arm@0.25.12':
+ optional: true
+
'@esbuild/linux-arm@0.27.1':
optional: true
+ '@esbuild/linux-ia32@0.25.12':
+ optional: true
+
'@esbuild/linux-ia32@0.27.1':
optional: true
+ '@esbuild/linux-loong64@0.25.12':
+ optional: true
+
'@esbuild/linux-loong64@0.27.1':
optional: true
+ '@esbuild/linux-mips64el@0.25.12':
+ optional: true
+
'@esbuild/linux-mips64el@0.27.1':
optional: true
+ '@esbuild/linux-ppc64@0.25.12':
+ optional: true
+
'@esbuild/linux-ppc64@0.27.1':
optional: true
+ '@esbuild/linux-riscv64@0.25.12':
+ optional: true
+
'@esbuild/linux-riscv64@0.27.1':
optional: true
+ '@esbuild/linux-s390x@0.25.12':
+ optional: true
+
'@esbuild/linux-s390x@0.27.1':
optional: true
+ '@esbuild/linux-x64@0.25.12':
+ optional: true
+
'@esbuild/linux-x64@0.27.1':
optional: true
+ '@esbuild/netbsd-arm64@0.25.12':
+ optional: true
+
'@esbuild/netbsd-arm64@0.27.1':
optional: true
+ '@esbuild/netbsd-x64@0.25.12':
+ optional: true
+
'@esbuild/netbsd-x64@0.27.1':
optional: true
+ '@esbuild/openbsd-arm64@0.25.12':
+ optional: true
+
'@esbuild/openbsd-arm64@0.27.1':
optional: true
+ '@esbuild/openbsd-x64@0.25.12':
+ optional: true
+
'@esbuild/openbsd-x64@0.27.1':
optional: true
+ '@esbuild/openharmony-arm64@0.25.12':
+ optional: true
+
'@esbuild/openharmony-arm64@0.27.1':
optional: true
+ '@esbuild/sunos-x64@0.25.12':
+ optional: true
+
'@esbuild/sunos-x64@0.27.1':
optional: true
+ '@esbuild/win32-arm64@0.25.12':
+ optional: true
+
'@esbuild/win32-arm64@0.27.1':
optional: true
+ '@esbuild/win32-ia32@0.25.12':
+ optional: true
+
'@esbuild/win32-ia32@0.27.1':
optional: true
+ '@esbuild/win32-x64@0.25.12':
+ optional: true
+
'@esbuild/win32-x64@0.27.1':
optional: true
@@ -1924,13 +2179,13 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.0.3
- '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.2.3)(tsx@4.21.0))':
+ '@vitest/mocker@4.0.18(vite@6.4.1(@types/node@25.2.3)(tsx@4.21.0))':
dependencies:
'@vitest/spy': 4.0.18
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
- vite: 7.3.1(@types/node@25.2.3)(tsx@4.21.0)
+ vite: 6.4.1(@types/node@25.2.3)(tsx@4.21.0)
'@vitest/pretty-format@4.0.18':
dependencies:
@@ -2021,6 +2276,35 @@ snapshots:
es-module-lexer@1.7.0: {}
+ esbuild@0.25.12:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.25.12
+ '@esbuild/android-arm': 0.25.12
+ '@esbuild/android-arm64': 0.25.12
+ '@esbuild/android-x64': 0.25.12
+ '@esbuild/darwin-arm64': 0.25.12
+ '@esbuild/darwin-x64': 0.25.12
+ '@esbuild/freebsd-arm64': 0.25.12
+ '@esbuild/freebsd-x64': 0.25.12
+ '@esbuild/linux-arm': 0.25.12
+ '@esbuild/linux-arm64': 0.25.12
+ '@esbuild/linux-ia32': 0.25.12
+ '@esbuild/linux-loong64': 0.25.12
+ '@esbuild/linux-mips64el': 0.25.12
+ '@esbuild/linux-ppc64': 0.25.12
+ '@esbuild/linux-riscv64': 0.25.12
+ '@esbuild/linux-s390x': 0.25.12
+ '@esbuild/linux-x64': 0.25.12
+ '@esbuild/netbsd-arm64': 0.25.12
+ '@esbuild/netbsd-x64': 0.25.12
+ '@esbuild/openbsd-arm64': 0.25.12
+ '@esbuild/openbsd-x64': 0.25.12
+ '@esbuild/openharmony-arm64': 0.25.12
+ '@esbuild/sunos-x64': 0.25.12
+ '@esbuild/win32-arm64': 0.25.12
+ '@esbuild/win32-ia32': 0.25.12
+ '@esbuild/win32-x64': 0.25.12
+
esbuild@0.27.1:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.1
@@ -2464,9 +2748,9 @@ snapshots:
- utf-8-validate
- zod
- vite@7.3.1(@types/node@25.2.3)(tsx@4.21.0):
+ vite@6.4.1(@types/node@25.2.3)(tsx@4.21.0):
dependencies:
- esbuild: 0.27.1
+ esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
@@ -2480,7 +2764,7 @@ snapshots:
vitest@4.0.18(@types/node@25.2.3)(tsx@4.21.0):
dependencies:
'@vitest/expect': 4.0.18
- '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.2.3)(tsx@4.21.0))
+ '@vitest/mocker': 4.0.18(vite@6.4.1(@types/node@25.2.3)(tsx@4.21.0))
'@vitest/pretty-format': 4.0.18
'@vitest/runner': 4.0.18
'@vitest/snapshot': 4.0.18
@@ -2497,7 +2781,7 @@ snapshots:
tinyexec: 1.0.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
- vite: 7.3.1(@types/node@25.2.3)(tsx@4.21.0)
+ vite: 6.4.1(@types/node@25.2.3)(tsx@4.21.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 25.2.3
diff --git a/src/version.ts b/src/version.ts
index fe9f16c..30ae1a5 100644
--- a/src/version.ts
+++ b/src/version.ts
@@ -1 +1 @@
-export const version = '0.0.8';
+export const version = '0.0.9';
diff --git a/src/ws/env.ts b/src/ws/env.ts
new file mode 100644
index 0000000..f4b060b
--- /dev/null
+++ b/src/ws/env.ts
@@ -0,0 +1 @@
+export const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
diff --git a/src/ws/manager.test.ts b/src/ws/manager.test.ts
index c8e7bd1..241d1f8 100644
--- a/src/ws/manager.test.ts
+++ b/src/ws/manager.test.ts
@@ -5,6 +5,7 @@ import { createWebSocketManager } from './manager.js';
// Mock state
const mockSend = vi.fn();
const mockClose = vi.fn();
+const mockConstructorArgs: unknown[][] = [];
let mockOnOpen: (() => void) | undefined;
let mockOnMessage: ((event: { data: string }) => void) | undefined;
@@ -15,7 +16,8 @@ vi.mock('isomorphic-ws', () => {
send = mockSend;
close = mockClose;
- constructor() {
+ constructor(...args: unknown[]) {
+ mockConstructorArgs.push(args);
setTimeout(() => mockOnOpen?.(), 0);
}
@@ -37,6 +39,10 @@ vi.mock('isomorphic-ws', () => {
return { default: MockWebSocket };
});
+vi.mock('./env.js', () => ({
+ isBrowser: false
+}));
+
/** Helper to connect a manager and advance timers */
async function connectManager(manager: ReturnType) {
const p = manager.connect();
@@ -53,6 +59,7 @@ describe('createWebSocketManager', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.clearAllMocks();
+ mockConstructorArgs.length = 0;
mockOnOpen = undefined;
mockOnMessage = undefined;
});
@@ -84,6 +91,128 @@ describe('createWebSocketManager', () => {
});
});
+ describe('connect authentication', () => {
+ it('passes Authorization header in Node.js', async () => {
+ const manager = createWebSocketManager(config);
+ await connectManager(manager);
+
+ const [url, opts] = mockConstructorArgs[0] as [
+ string,
+ { headers: { Authorization: string } }
+ ];
+ expect(url).toBe('wss://api.gelato.cloud/ws');
+ expect(opts).toEqual({ headers: { Authorization: 'Bearer test-key' } });
+ });
+
+ it('does not include apiKey in URL in Node.js', async () => {
+ const manager = createWebSocketManager(config);
+ await connectManager(manager);
+
+ const [url] = mockConstructorArgs[0] as [string];
+ expect(url).not.toContain('apiKey');
+ });
+
+ it('passes apiKey as query parameter in browser', async () => {
+ const env = await import('./env.js');
+ vi.mocked(env).isBrowser = true;
+
+ const manager = createWebSocketManager(config);
+ await connectManager(manager);
+
+ const [url] = mockConstructorArgs[0] as [string];
+ const parsed = new URL(url);
+ expect(parsed.searchParams.get('apiKey')).toBe('test-key');
+ expect(mockConstructorArgs[0]).toHaveLength(1);
+
+ vi.mocked(env).isBrowser = false;
+ });
+
+ it('URL-encodes special characters in apiKey for browser', async () => {
+ const env = await import('./env.js');
+ vi.mocked(env).isBrowser = true;
+
+ const specialKey = 'key+with/special=chars&more';
+ const manager = createWebSocketManager({ ...config, apiKey: specialKey });
+ await connectManager(manager);
+
+ const [url] = mockConstructorArgs[0] as [string];
+ const parsed = new URL(url);
+ expect(parsed.searchParams.get('apiKey')).toBe(specialKey);
+ expect(url).not.toContain('+with/');
+
+ vi.mocked(env).isBrowser = false;
+ });
+
+ it('converts https to wss in browser', async () => {
+ const env = await import('./env.js');
+ vi.mocked(env).isBrowser = true;
+
+ const manager = createWebSocketManager(config);
+ await connectManager(manager);
+
+ const [url] = mockConstructorArgs[0] as [string];
+ expect(url).toMatch(/^wss:\/\//);
+
+ vi.mocked(env).isBrowser = false;
+ });
+
+ it('converts http to ws in browser', async () => {
+ const env = await import('./env.js');
+ vi.mocked(env).isBrowser = true;
+
+ const manager = createWebSocketManager({ ...config, baseUrl: 'http://localhost:3000' });
+ await connectManager(manager);
+
+ const [url] = mockConstructorArgs[0] as [string];
+ expect(url).toMatch(/^ws:\/\/localhost:3000\/ws/);
+
+ vi.mocked(env).isBrowser = false;
+ });
+
+ it('does not pass Authorization header in browser', async () => {
+ const env = await import('./env.js');
+ vi.mocked(env).isBrowser = true;
+
+ const manager = createWebSocketManager(config);
+ await connectManager(manager);
+
+ expect(mockConstructorArgs[0]).toHaveLength(1);
+
+ vi.mocked(env).isBrowser = false;
+ });
+
+ it('preserves base URL path in browser', async () => {
+ const env = await import('./env.js');
+ vi.mocked(env).isBrowser = true;
+
+ const manager = createWebSocketManager({ ...config, baseUrl: 'https://api.gelato.cloud' });
+ await connectManager(manager);
+
+ const [url] = mockConstructorArgs[0] as [string];
+ const parsed = new URL(url);
+ expect(parsed.pathname).toBe('/ws');
+ expect(parsed.hostname).toBe('api.gelato.cloud');
+
+ vi.mocked(env).isBrowser = false;
+ });
+
+ it('converts https to wss', async () => {
+ const manager = createWebSocketManager(config);
+ await connectManager(manager);
+
+ const [url] = mockConstructorArgs[0] as [string];
+ expect(url).toMatch(/^wss:\/\//);
+ });
+
+ it('converts http to ws', async () => {
+ const manager = createWebSocketManager({ ...config, baseUrl: 'http://localhost:3000' });
+ await connectManager(manager);
+
+ const [url] = mockConstructorArgs[0] as [string];
+ expect(url).toMatch(/^ws:\/\/localhost:3000\/ws/);
+ });
+ });
+
describe('disconnect', () => {
it('closes connection and clears state', async () => {
const manager = createWebSocketManager(config);
diff --git a/src/ws/manager.ts b/src/ws/manager.ts
index 8faec36..4347c1f 100644
--- a/src/ws/manager.ts
+++ b/src/ws/manager.ts
@@ -1,5 +1,6 @@
import WebSocketImpl from 'isomorphic-ws';
import { statusSchema } from '../types/schema.js';
+import { isBrowser } from './env.js';
import { WebSocketConnectionError, WebSocketSubscriptionError } from './errors.js';
import { createSubscription } from './subscription.js';
import type {
@@ -197,12 +198,17 @@ export function createWebSocketManager(
connectionPromise = new Promise((resolve, reject) => {
const wsUrl = `${fullConfig.baseUrl.replace('https://', 'wss://').replace('http://', 'ws://')}/ws`;
- // Create WebSocket with authorization header
- ws = new WebSocketImpl(wsUrl, {
- headers: {
- Authorization: `Bearer ${fullConfig.apiKey}`
- }
- });
+ if (isBrowser) {
+ const url = new URL(wsUrl);
+ url.searchParams.set('apiKey', fullConfig.apiKey);
+ ws = new WebSocketImpl(url.toString());
+ } else {
+ ws = new WebSocketImpl(wsUrl, {
+ headers: {
+ Authorization: `Bearer ${fullConfig.apiKey}`
+ }
+ });
+ }
ws.onopen = () => {
reconnectAttempts = 0;