From 3d83f073068672d0c60ad6c43646efaf448979f6 Mon Sep 17 00:00:00 2001 From: "Tiago Siebler @ Siebly.io" Date: Mon, 27 Jul 2026 17:44:28 +0100 Subject: [PATCH 1/4] feat: experimental modern release bundling with browser-adapted event emitter --- .eslintrc.cjs | 59 - .github/workflows/e2etest.yml | 23 + .github/workflows/npmpublish.yml | 53 +- .gitignore | 3 +- .npmrc | 1 + README.md | 104 +- docs/endpointFunctionList.md | 2 +- docs/images/logoDarkMode2.svg | 78 +- eslint.config.cjs | 10 +- jest.config.ts => jest.config.cjs | 20 +- package-lock.json | 3886 +++++++++-------------- package.json | 68 +- postBuild.sh | 19 - scripts/browser-bundle-validation.mjs | 183 ++ scripts/browser-smoke.mjs | 42 + scripts/clean.mjs | 6 + scripts/consumer-smoke.mjs | 68 + scripts/consumer-types-smoke.mjs | 71 + scripts/jest-transformer.cjs | 15 + scripts/packed-consumer-smoke.mjs | 213 ++ scripts/postbuild.mjs | 58 + scripts/verify-extension-rule.mjs | 169 + src/WebsocketClient.ts | 38 +- src/index.ts | 2 + src/lib/BaseRestClient.ts | 32 +- src/lib/BaseWSClient.ts | 139 +- src/lib/event-emitter.browser.ts | 26 + src/lib/event-emitter.ts | 9 + src/lib/https-agent.browser.ts | 13 + src/lib/https-agent.ts | 26 + src/lib/requestUtils.ts | 4 +- src/lib/webCryptoAPI.ts | 2 +- src/lib/websocket/WsStore.ts | 12 +- src/lib/websocket/WsStore.types.ts | 6 +- src/lib/websocket/type-guards.ts | 2 +- src/lib/websocket/websocket-util.ts | 52 +- src/types/websockets/ws-events.ts | 10 +- src/types/websockets/ws-general.ts | 8 +- src/types/websockets/ws-portable.ts | 98 + test/consumer/browser-rest.ts | 7 + test/consumer/browser-websocket.ts | 21 + test/tsconfig.test.json | 13 +- test/types/portable-options-exact.ts | 11 + test/types/portable-websocket-types.ts | 30 + test/types/tsconfig.exact.json | 14 + test/websockets/runtimeAdapters.test.ts | 430 +++ test/websockets/websocketUtil.test.ts | 55 + tsconfig.cjs.json | 2 + tsconfig.esm.json | 2 + tsconfig.extensions.json | 10 + tsconfig.json | 9 +- tsconfig.linting.json | 14 +- webpack/webpack.config.cjs | 48 - 53 files changed, 3547 insertions(+), 2749 deletions(-) delete mode 100644 .eslintrc.cjs create mode 100644 .npmrc rename jest.config.ts => jest.config.cjs (94%) delete mode 100755 postBuild.sh create mode 100644 scripts/browser-bundle-validation.mjs create mode 100644 scripts/browser-smoke.mjs create mode 100644 scripts/clean.mjs create mode 100644 scripts/consumer-smoke.mjs create mode 100644 scripts/consumer-types-smoke.mjs create mode 100644 scripts/jest-transformer.cjs create mode 100644 scripts/packed-consumer-smoke.mjs create mode 100644 scripts/postbuild.mjs create mode 100644 scripts/verify-extension-rule.mjs create mode 100644 src/lib/event-emitter.browser.ts create mode 100644 src/lib/event-emitter.ts create mode 100644 src/lib/https-agent.browser.ts create mode 100644 src/lib/https-agent.ts create mode 100644 src/types/websockets/ws-portable.ts create mode 100644 test/consumer/browser-rest.ts create mode 100644 test/consumer/browser-websocket.ts create mode 100644 test/types/portable-options-exact.ts create mode 100644 test/types/portable-websocket-types.ts create mode 100644 test/types/tsconfig.exact.json create mode 100644 test/websockets/runtimeAdapters.test.ts create mode 100644 tsconfig.extensions.json delete mode 100644 webpack/webpack.config.cjs diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index bcc0229..0000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,59 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - parserOptions: { - project: 'tsconfig.linting.json', - tsconfigRootDir: __dirname, - sourceType: 'module', - }, - plugins: [ - '@typescript-eslint/eslint-plugin', - 'simple-import-sort', - 'require-extensions', - ], - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - 'plugin:require-extensions/recommended', - ], - root: true, - env: { - node: true, - jest: true, - }, - ignorePatterns: ['.eslintrc.js', 'webpack.config.js', 'examples/apidoc'], - rules: { - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/ban-types': 'off', - 'no-param-reassign': ['error'], - 'simple-import-sort/imports': 'error', - 'simple-import-sort/exports': 'error', - 'array-bracket-spacing': ['error', 'never'], - 'linebreak-style': ['error', 'unix'], - 'lines-between-class-members': ['warn', 'always'], - '@typescript-eslint/no-empty-object-type': [ - 'error', - { allowObjectTypes: 'always' }, - ], - '@typescript-eslint/no-unused-vars': 'off', - semi: ['error', 'always'], - 'new-cap': 'off', - 'no-console': 'off', - 'no-debugger': 'off', - 'no-mixed-spaces-and-tabs': 2, - 'no-use-before-define': [2, 'nofunc'], - 'no-unreachable': ['warn'], - // 'no-unused-vars': ['warn'], - 'no-extra-parens': ['off'], - 'no-mixed-operators': ['off'], - quotes: [2, 'single', 'avoid-escape'], - 'block-scoped-var': 2, - 'brace-style': [2, '1tbs', { allowSingleLine: true }], - 'computed-property-spacing': [2, 'never'], - 'keyword-spacing': 2, - 'space-unary-ops': 2, - }, -}; diff --git a/.github/workflows/e2etest.yml b/.github/workflows/e2etest.yml index d27ad6f..7a63964 100644 --- a/.github/workflows/e2etest.yml +++ b/.github/workflows/e2etest.yml @@ -27,12 +27,35 @@ jobs: registry-url: 'https://registry.npmjs.org/' cache: 'npm' + - name: 'Use pinned npm' + run: npm install --global --ignore-scripts npm@11.12.0 + - name: Install run: npm ci --ignore-scripts + - name: Lint + run: npm run lint + + - name: Type check + run: npm run test:types + + - name: Test focused units + run: npm run test:unit + - name: Build run: npm run build + - name: Test browser packaging + run: node scripts/browser-smoke.mjs + + - name: Test package consumers + run: | + node scripts/consumer-smoke.mjs + node scripts/consumer-types-smoke.mjs + + - name: Test packed package + run: node scripts/packed-consumer-smoke.mjs + - name: Test Public REST API Calls run: npm run test -- public.test.ts diff --git a/.github/workflows/npmpublish.yml b/.github/workflows/npmpublish.yml index e2aaa41..e4f6a2b 100644 --- a/.github/workflows/npmpublish.yml +++ b/.github/workflows/npmpublish.yml @@ -31,18 +31,53 @@ jobs: registry-url: https://registry.npmjs.org/ cache: 'npm' - - name: Assert latest npm - run: npm i -g npm@latest + - name: Use pinned npm + run: npm install --global --ignore-scripts npm@11.12.0 - - name: Guard - block registry overrides and shady files + - name: Verify exact repository npm policy run: | - # fail if any .npmrc exists in repo - if git ls-files -z | xargs -0 -I{} bash -lc '[[ "{}" == *.npmrc ]]' | grep -q .; then - echo "Repo contains an .npmrc. Refusing to publish."; exit 1; - fi - # fail if publishConfig.registry set + node <<'NODE' + const { readFileSync, readdirSync } = require('node:fs'); + const { join, relative, sep } = require('node:path'); + + const npmrcFiles = []; + + function findNpmrcFiles(directory) { + for (const entry of readdirSync(directory, { withFileTypes: true })) { + if (entry.name === '.git' || entry.name === 'node_modules') { + continue; + } + + const entryPath = join(directory, entry.name); + if (entry.name === '.npmrc') { + npmrcFiles.push(relative('.', entryPath).split(sep).join('/')); + } else if (entry.isDirectory()) { + findNpmrcFiles(entryPath); + } + } + } + + findNpmrcFiles('.'); + npmrcFiles.sort(); + + if (npmrcFiles.length !== 1 || npmrcFiles[0] !== '.npmrc') { + console.error( + `Expected only the root .npmrc; found: ${npmrcFiles.join(', ') || 'none'}`, + ); + process.exit(1); + } + + if (readFileSync('.npmrc', 'utf8') !== 'min-release-age=30\n') { + console.error('Root .npmrc must contain exactly: min-release-age=30'); + process.exit(1); + } + NODE + + - name: Guard publish target and release workflow + run: | + # Reject repository-level registry redirection. node -e "const p=require('./package.json'); if(p.publishConfig?.registry){console.error('publishConfig.registry present - refuse to publish'); process.exit(1)}" - # optional: block workflow/script changes in the release commit + # Block release-time workflow or script changes. SHA=$(git rev-list -n 1 "$RELEASE_TAG") PARENT=$(git rev-list -n 1 "$SHA^") git diff --name-only "$PARENT" "$SHA" | grep -E '^\\.github/(workflows|scripts)/' \ diff --git a/.gitignore b/.gitignore index 77e793d..f8a9bc0 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,5 @@ repomix.sh doc .cursor/ futuresOrder2.ts -.vscode/ \ No newline at end of file +.vscode/ +WSWIP.md diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..8f0a88c --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +min-release-age=30 diff --git a/README.md b/README.md index d11f780..e9b174d 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # Node.js & JavaScript SDK for HTX REST APIs & WebSockets [![Build & Test](https://github.com/sieblyio/htx-api/actions/workflows/e2etest.yml/badge.svg?branch=main)](https://github.com/sieblyio/htx-api/actions/workflows/e2etest.yml) -[![npm version](https://img.shields.io/npm/v/@siebly/htx-api)][1] -[![npm size](https://img.shields.io/bundlephobia/min/@siebly/htx-api/latest)][1] -[![npm downloads](https://img.shields.io/npm/dt/@siebly/htx-api)][1] +[![npm version](https://img.shields.io/npm/v/htx-api)][1] +[![npm size](https://img.shields.io/bundlephobia/min/htx-api/latest)][1] +[![npm downloads](https://img.shields.io/npm/dt/htx-api)][1] [![last commit](https://img.shields.io/github/last-commit/sieblyio/htx-api)][1] [![Telegram](https://img.shields.io/badge/chat-on%20telegram-blue.svg)](https://t.me/nodetraders) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/sieblyio/htx-api)

- + SDK Logo @@ -17,7 +17,7 @@

-[1]: https://www.npmjs.com/package/@siebly/htx-api +[1]: https://www.npmjs.com/package/htx-api Complete & robust JavaScript & Node.js SDK for the HTX REST APIs and WebSockets: @@ -34,7 +34,7 @@ Complete & robust JavaScript & Node.js SDK for the HTX REST APIs and WebSockets: - Smart WebSocket persistence with automatic reconnection handling. - Emit `reconnected` event when dropped connection is restored. - Support for both public and private WebSocket streams. -- Browser-friendly HMAC signature mechanism. +- Portable HMAC signing support for trusted server-side runtimes. - Automatically supports both ESM and CJS projects. - Heavy automated end-to-end testing with real API calls. - Proxy support via axios integration. @@ -58,14 +58,19 @@ Complete & robust JavaScript & Node.js SDK for the HTX REST APIs and WebSockets: - [WebSocket API (WebsocketAPIClient)](#websocket-api-websocketapiclient) - [Customise Logging](#customise-logging) - [Browser/Frontend Usage](#browserfrontend-usage) + - [React and Vite](#react-and-vite) - [Webpack](#webpack) + - [Browser Requirements](#browser-requirements) + - [Security and CORS](#security-and-cors) - [LLMs & AI](#use-with-llms--ai) - [Used By](#used-by) - [Contributions & Thanks](#contributions--thanks) ## Installation -`npm install --save @siebly/htx-api` +`npm install --save htx-api` + +Node.js usage requires Node 22.13.0 or newer. Browser applications should follow the public-data and credential-safety guidance below. ## Examples @@ -144,9 +149,9 @@ Both clients default to HTX's AWS CDN domains for better connectivity. You can o To use HTX's Spot APIs, import (or require) the `SpotClient`: ```javascript -import { SpotClient } from '@siebly/htx-api'; +import { SpotClient } from 'htx-api'; // or if you prefer require: -// const { SpotClient } = require('@siebly/htx-api'); +// const { SpotClient } = require('htx-api'); // For public endpoints, API credentials are optional const publicClient = new SpotClient(); @@ -242,9 +247,9 @@ See [SpotClient](./src/SpotClient.ts) for further information. Use the `FuturesClient` for futures and swap trading operations: ```javascript -import { FuturesClient } from '@siebly/htx-api'; +import { FuturesClient } from 'htx-api'; // or if you prefer require: -// const { FuturesClient } = require('@siebly/htx-api'); +// const { FuturesClient } = require('htx-api'); // For public endpoints, API credentials are optional const publicClient = new FuturesClient(); @@ -329,9 +334,9 @@ Each connection is tracked using a `WsKey` (see [WS_KEY_MAP](./src/lib/websocket For public market data, API credentials are not required: ```javascript -import { WebsocketClient, WS_KEY_MAP } from '@siebly/htx-api'; +import { WebsocketClient, WS_KEY_MAP } from 'htx-api'; // or if you prefer require: -// const { WebsocketClient, WS_KEY_MAP } = require('@siebly/htx-api'); +// const { WebsocketClient, WS_KEY_MAP } = require('htx-api'); // Create WebSocket client for public streams const wsClient = new WebsocketClient(); @@ -393,7 +398,7 @@ wsClient.subscribe( For private account data streams, API credentials are required: ```javascript -import { WebsocketClient, WS_KEY_MAP } from '@siebly/htx-api'; +import { WebsocketClient, WS_KEY_MAP } from 'htx-api'; // Create WebSocket client with API credentials for private streams const wsClient = new WebsocketClient({ @@ -447,7 +452,7 @@ Trade connections connect and authenticate lazily on the first request. Optional Trade keys: `spotTrade`, `linearSwapTrade`, `coinDeliveryTrade`, `coinSwapTrade`. ```javascript -import { WebsocketAPIClient, WS_KEY_MAP } from '@siebly/htx-api'; +import { WebsocketAPIClient, WS_KEY_MAP } from 'htx-api'; const client = new WebsocketAPIClient({ apiKey: 'your-api-key', @@ -487,7 +492,7 @@ See [WebsocketAPIClient](./src/WebsocketAPIClient.ts) for all typed methods. Exa Pass a custom logger which supports the log methods `trace`, `info` and `error`, or override methods from the default logger as desired. ```javascript -import { WebsocketClient, DefaultLogger } from '@siebly/htx-api'; +import { WebsocketClient, DefaultLogger } from 'htx-api'; // E.g. customise logging for only the trace level: const customLogger = { @@ -516,15 +521,72 @@ In rare situations, you may want to see the raw HTTP requests being built as wel ## Browser/Frontend Usage +The package's ESM entry can be imported directly by modern frontend bundlers. Do not build or copy a separate SDK bundle into your application. + +Browser applications should use the SDK only for public market data. Keep API keys, API secrets, authenticated REST calls, private WebSocket subscriptions, and WebSocket API trading on a trusted backend. + +### React and Vite + +Install and import the package normally; no SDK-specific Vite plugin or Node.js polyfill is required: + +```bash +npm install htx-api +``` + +```tsx +import { useEffect, useState } from 'react'; +import { SpotClient, WebsocketClient, WS_KEY_MAP } from 'htx-api'; + +export function BtcTicker() { + const [ticker, setTicker] = useState(); + + useEffect(() => { + const restClient = new SpotClient(); + const wsClient = new WebsocketClient(); + + restClient.getTicker({ symbol: 'btcusdt' }).then(setTicker); + + const onMessage = (message: unknown) => setTicker(message); + wsClient.on('message', onMessage); + wsClient.subscribe('market.btcusdt.ticker', WS_KEY_MAP.spotPublic); + + return () => { + wsClient.off('message', onMessage); + wsClient.closeAll(); + }; + }, []); + + return
{JSON.stringify(ticker, null, 2)}
; +} +``` + +The cleanup is important during navigation, hot reloads, and React Strict Mode development checks so that an old socket is not left reconnecting in the background. + ### Webpack -Build a bundle using webpack: +Webpack 5 can consume the same package entry directly: + +```javascript +import { SpotClient, WebsocketClient } from 'htx-api'; +``` + +Use a normal `target: 'web'` application build. The SDK does not require a checked-in UMD bundle or `resolve.fallback` shims for Node.js core modules. + +Direct `