From 24edc8d802c45af565ef6d7cd723682cd717f100 Mon Sep 17 00:00:00 2001 From: Tejas Badadare Date: Tue, 2 Sep 2025 13:51:03 -0700 Subject: [PATCH 1/3] feat(lazer/js/sdk): QSP auth for browser support --- lazer/sdk/js/README.md | 56 ++++++++++++++- lazer/sdk/js/package.json | 2 +- lazer/sdk/js/src/socket/websocket-pool.ts | 87 +++++++++++++++++++++-- 3 files changed, 136 insertions(+), 9 deletions(-) diff --git a/lazer/sdk/js/README.md b/lazer/sdk/js/README.md index 65e587c6c9..d5cae31334 100644 --- a/lazer/sdk/js/README.md +++ b/lazer/sdk/js/README.md @@ -1,4 +1,58 @@ -# pyth-lazer-sdk - Readme +# Pyth Lazer JavaScript SDK + +A JavaScript/TypeScript SDK for connecting to the Pyth Lazer service, supporting both Node and browser environments. + + +## Quick Start +Install with: +```sh +npm install @pythnetwork/pyth-lazer-sdk +``` + +Connect to Lazer and process the messages: +```javascript +import { PythLazerClient } from '@pythnetwork/pyth-lazer-sdk'; + +const client = await PythLazerClient.create({ + urls: ['wss://your-lazer-endpoint/v1/stream'], + token: 'your-access-token', + numConnections: 3 +}); + +// Register an event handler for each price update message. +client.addMessageListener((message) => { + console.log('Received:', message); +}); + +// Subscribe to a feed. You can call subscribe() multiple times. +client.subscribe({ + type: "subscribe", + subscriptionId: 1, + priceFeedIds: [1, 2], + properties: ["price"], + formats: ["solana"], + deliveryFormat: "binary", + channel: "fixed_rate@200ms", + parsed: false, + jsonBinaryEncoding: "base64", +}); +``` + +For a full demo, run the example in `examples/index.ts` with: +``` +pnpm run example +``` +### Build locally + +Build ESM and CJS packages with: + +```sh +pnpm turbo build -F @pythnetwork/pyth-lazer-sdk +``` + +## API Reference + +For detailed API documentation, see the [TypeDoc documentation](docs/typedoc/). ## Contributing & Development diff --git a/lazer/sdk/js/package.json b/lazer/sdk/js/package.json index 6c412d5281..7f209389df 100644 --- a/lazer/sdk/js/package.json +++ b/lazer/sdk/js/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/pyth-lazer-sdk", - "version": "2.0.0", + "version": "2.1.0", "description": "Pyth Lazer SDK", "publishConfig": { "access": "public" diff --git a/lazer/sdk/js/src/socket/websocket-pool.ts b/lazer/sdk/js/src/socket/websocket-pool.ts index 6e57811c0c..c46d8c33ca 100644 --- a/lazer/sdk/js/src/socket/websocket-pool.ts +++ b/lazer/sdk/js/src/socket/websocket-pool.ts @@ -8,6 +8,77 @@ import type { Request, Response } from "../protocol.js"; import type { ResilientWebSocketConfig } from "./resilient-websocket.js"; import { ResilientWebSocket } from "./resilient-websocket.js"; +/** + * Detects if the code is running in a regular DOM or Web Worker context. + * @returns true if running in a DOM or Web Worker context, false if running in Node.js + */ +function isBrowser(): boolean { + try { + // Check for browser's window object (DOM context) + if (typeof globalThis !== "undefined") { + const global = globalThis as any; + if (global.window && global.window.document) { + return true; + } + + // Check for Web Worker context (has importScripts but no window) + if (typeof global.importScripts === "function" && !global.window) { + return true; + } + } + + // Node.js environment + return false; + } catch { + // If any error occurs, assume Node.js environment + return false; + } +} + +/** + * Adds authentication to either the URL as a query parameter or as an Authorization header. + * + * Browsers don't support custom headers for WebSocket connections, so if a browser is detected, + * the token is added as a query parameter instead. Else, the token is added as an Authorization header. + * + * @param url - The WebSocket URL + * @param token - The authentication token + * @param wsOptions - Existing WebSocket options + * @returns Object containing the modified endpoint and wsOptions + */ +function addAuthentication( + url: string, + token: string, + wsOptions: any = {} +): { endpoint: string; wsOptions: any } { + if (isBrowser()) { + // Browser: Add token as query parameter + const urlObj = new URL(url); + urlObj.searchParams.set("ACCESS_TOKEN", token); + + // For browsers, we need to filter out any options that aren't valid for WebSocket constructor + // Browser WebSocket constructor only accepts protocols as second parameter + const browserWsOptions = wsOptions.protocols ? wsOptions.protocols : undefined; + + return { + endpoint: urlObj.toString(), + wsOptions: browserWsOptions, + }; + } else { + // Node.js: Add Authorization header + return { + endpoint: url, + wsOptions: { + ...wsOptions, + headers: { + ...wsOptions.headers, + Authorization: `Bearer ${token}`, + }, + }, + }; + } +} + const DEFAULT_NUM_CONNECTIONS = 4; export type WebSocketPoolConfig = { @@ -63,15 +134,17 @@ export class WebSocketPool { if (!url) { throw new Error(`URLs must not be null or empty`); } - const wsOptions = { - ...config.rwsConfig?.wsOptions, - headers: { - Authorization: `Bearer ${config.token}`, - }, - }; + + // Apply authentication based on environment (browser vs Node.js) + const { endpoint, wsOptions } = addAuthentication( + url, + config.token, + config.rwsConfig?.wsOptions + ); + const rws = new ResilientWebSocket({ ...config.rwsConfig, - endpoint: url, + endpoint, wsOptions, logger, }); From bc8a6a99277fd2e061e6f9b205ef00dce13d7e5f Mon Sep 17 00:00:00 2001 From: Tejas Badadare Date: Fri, 5 Sep 2025 15:42:25 -0700 Subject: [PATCH 2/3] fix: use more specific type for wsOptions --- lazer/sdk/js/src/socket/websocket-pool.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lazer/sdk/js/src/socket/websocket-pool.ts b/lazer/sdk/js/src/socket/websocket-pool.ts index c46d8c33ca..cf1fc8242b 100644 --- a/lazer/sdk/js/src/socket/websocket-pool.ts +++ b/lazer/sdk/js/src/socket/websocket-pool.ts @@ -7,6 +7,7 @@ import { dummyLogger } from "ts-log"; import type { Request, Response } from "../protocol.js"; import type { ResilientWebSocketConfig } from "./resilient-websocket.js"; import { ResilientWebSocket } from "./resilient-websocket.js"; +import type { ClientRequestArgs } from "node:http"; /** * Detects if the code is running in a regular DOM or Web Worker context. @@ -49,7 +50,7 @@ function isBrowser(): boolean { function addAuthentication( url: string, token: string, - wsOptions: any = {} + wsOptions: WebSocket.ClientOptions | ClientRequestArgs | undefined = {} ): { endpoint: string; wsOptions: any } { if (isBrowser()) { // Browser: Add token as query parameter @@ -58,7 +59,7 @@ function addAuthentication( // For browsers, we need to filter out any options that aren't valid for WebSocket constructor // Browser WebSocket constructor only accepts protocols as second parameter - const browserWsOptions = wsOptions.protocols ? wsOptions.protocols : undefined; + const browserWsOptions = wsOptions.protocol ? wsOptions.protocol : undefined; return { endpoint: urlObj.toString(), From dc8b74920f081cafd21902c63cbadfd874d7b8dd Mon Sep 17 00:00:00 2001 From: Tejas Badadare Date: Mon, 8 Sep 2025 11:03:02 -0700 Subject: [PATCH 3/3] Fix lint --- lazer/sdk/js/examples/index.ts | 2 +- lazer/sdk/js/src/socket/websocket-pool.ts | 28 +++++++++++++++-------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/lazer/sdk/js/examples/index.ts b/lazer/sdk/js/examples/index.ts index f6170b542e..e86febb4cb 100644 --- a/lazer/sdk/js/examples/index.ts +++ b/lazer/sdk/js/examples/index.ts @@ -4,7 +4,7 @@ import { PythLazerClient } from "../src/index.js"; // Ignore debug messages -console.debug = () => {}; +console.debug = () => { }; const client = await PythLazerClient.create({ urls: [ diff --git a/lazer/sdk/js/src/socket/websocket-pool.ts b/lazer/sdk/js/src/socket/websocket-pool.ts index cf1fc8242b..23a6b0c701 100644 --- a/lazer/sdk/js/src/socket/websocket-pool.ts +++ b/lazer/sdk/js/src/socket/websocket-pool.ts @@ -1,3 +1,5 @@ +import type { ClientRequestArgs } from "node:http"; + import TTLCache from "@isaacs/ttlcache"; import type { ErrorEvent } from "isomorphic-ws"; import WebSocket from "isomorphic-ws"; @@ -7,7 +9,16 @@ import { dummyLogger } from "ts-log"; import type { Request, Response } from "../protocol.js"; import type { ResilientWebSocketConfig } from "./resilient-websocket.js"; import { ResilientWebSocket } from "./resilient-websocket.js"; -import type { ClientRequestArgs } from "node:http"; + +/** + * Browser global interface for proper typing + */ +type BrowserGlobal = { + window?: { + document?: unknown; + }; + importScripts?: (...urls: string[]) => void; +} /** * Detects if the code is running in a regular DOM or Web Worker context. @@ -17,8 +28,8 @@ function isBrowser(): boolean { try { // Check for browser's window object (DOM context) if (typeof globalThis !== "undefined") { - const global = globalThis as any; - if (global.window && global.window.document) { + const global = globalThis as BrowserGlobal; + if (global.window?.document) { return true; } @@ -51,19 +62,16 @@ function addAuthentication( url: string, token: string, wsOptions: WebSocket.ClientOptions | ClientRequestArgs | undefined = {} -): { endpoint: string; wsOptions: any } { +): { endpoint: string; wsOptions: WebSocket.ClientOptions | ClientRequestArgs | undefined } { if (isBrowser()) { // Browser: Add token as query parameter const urlObj = new URL(url); urlObj.searchParams.set("ACCESS_TOKEN", token); - // For browsers, we need to filter out any options that aren't valid for WebSocket constructor - // Browser WebSocket constructor only accepts protocols as second parameter - const browserWsOptions = wsOptions.protocol ? wsOptions.protocol : undefined; - + // For browsers, filter out wsOptions since headers aren't supported return { endpoint: urlObj.toString(), - wsOptions: browserWsOptions, + wsOptions: undefined, }; } else { // Node.js: Add Authorization header @@ -72,7 +80,7 @@ function addAuthentication( wsOptions: { ...wsOptions, headers: { - ...wsOptions.headers, + ...(wsOptions.headers as Record | undefined), Authorization: `Bearer ${token}`, }, },