Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
7213cd9
feat: Implement WebSocket provider with middleware support and protoc…
Sep 12, 2025
fe359a5
temp: update workflow to release and publish websocket-provider inste…
Sep 15, 2025
3d31022
Revert "temp: update workflow to release and publish websocket-provid…
Sep 15, 2025
64a4671
feat: Add GitHub Actions workflow for releasing and publishing websoc…
Sep 15, 2025
62657b4
fix: Update workflow trigger to push on websocket-provider branch
Sep 15, 2025
6603205
fix: Rename job from 'test' to 'setup' and update dependencies for wo…
Sep 15, 2025
42c7e43
CI: bumps websocket-provider to 1.0.1-websocket-provider.0
Sep 15, 2025
1493375
fix: Publish websocket-provider package from feature branches
Sep 15, 2025
1c36935
fix: Correct npm command in publish step of workflow
Sep 15, 2025
95ea7e8
fix: pass correct flags to npm publish command in publish-websocket w…
Sep 15, 2025
36477e8
rename to secure-ws
Sep 16, 2025
6ff441e
refactor: remove duplicate websocket-provider and related files
Sep 16, 2025
421f059
refactor: websocket-provider -> secure-ws in GitHub Actions workflow
Sep 16, 2025
0958ebb
CI: bumps secure-ws to 0.0.2-websocket-provider.0
Sep 16, 2025
4a5939f
fix: add build step before publishing secure-ws package
Sep 16, 2025
024f82a
CI: bumps secure-ws to 0.0.2-websocket-provider.1
Sep 16, 2025
f0f16c7
fix: add missing files entry in package.json
Sep 16, 2025
ee8cf47
CI: bumps secure-ws to 0.0.2-websocket-provider.2
Sep 16, 2025
f30b8c3
Temp: log dir structure after build
Sep 16, 2025
7bf514d
Revert "Temp: log dir structure after build"
Sep 16, 2025
c32a879
fix: remove build job and add it as a step in publish_and_release
Sep 16, 2025
644d138
CI: bumps secure-ws to 0.0.2-websocket-provider.3
Sep 16, 2025
568f2a9
fix: add missing types and declaration options in package.json and ts…
Sep 16, 2025
37d83d9
CI: bumps secure-ws to 0.0.2-websocket-provider.4
Sep 16, 2025
5856992
fix: update workflow trigger to use paths instead of branches for sec…
Sep 17, 2025
a5b60c5
add: README.md for secure-ws
Sep 17, 2025
d26c68f
CI: bumps secure-ws to 0.0.2-websocket-provider.5
Sep 17, 2025
c8d6dbb
fix: correct import path for WebSocketProvider in README.md
Sep 17, 2025
ec03b63
CI: bumps secure-ws to 0.0.2-websocket-provider.6
Sep 17, 2025
e84a201
fix: replace btoa and atob with Buffer for base64 encoding and decoding
Sep 17, 2025
5f60b33
fix: implement MockResponse interface and update middleware type defi…
Sep 17, 2025
b7e47d4
fix: use IncomingMessageWithBody interface in injectHttpRequest
Sep 17, 2025
cf0c8fa
fix: ensure injectedRequest is only created when ws.protocol is valid
Sep 17, 2025
f36634f
CI: bumps secure-ws to 0.0.2-websocket-provider.7
Sep 17, 2025
c306e15
fix: remove trailing comma in tsconfig.json [skip ci]
Sep 17, 2025
b083585
fix: update sendStatus method to send JSON response format
Sep 17, 2025
48ee8a7
CI: bumps secure-ws to 0.0.2-websocket-provider.8
Sep 17, 2025
ecb0ec7
fix: include response type in sendStatus method JSON response
Sep 17, 2025
9016a12
CI: bumps secure-ws to 0.0.2-websocket-provider.9
Sep 17, 2025
348cf65
fix: update send method to send JSON response format
Sep 17, 2025
05eeb34
CI: bumps secure-ws to 0.0.2-websocket-provider.10
Sep 17, 2025
3537477
fix: replace socket.write with console log for missing routes
Sep 18, 2025
6d02b62
CI: bumps secure-ws to 0.0.2-websocket-provider.11
Sep 18, 2025
e13aebc
add: unique ID to socket in handleUpgrade method
Sep 24, 2025
398e7d6
CI: bumps secure-ws to 0.0.2-websocket-provider.12
Sep 24, 2025
2ce2dd2
fix: add socket id to ws object instead of raw socket duplex object
Sep 24, 2025
5d003c0
CI: bumps secure-ws to 0.0.2-websocket-provider.13
Sep 24, 2025
fcb9da3
fix: manage client socket lifecycle by removing from clientSockets ma…
Sep 25, 2025
f03b152
CI: bumps secure-ws to 0.0.2-websocket-provider.14
Sep 25, 2025
5b2782b
fix: initialize locals property in MockResponse class
Apr 9, 2026
c3c8494
fix: update tsconfig.json to include strict type checking options and…
Apr 9, 2026
01e45df
fix: update npm publish workflow to include permissions and remove au…
Apr 9, 2026
daec267
fix: include workflow file in push paths for npm publish
Apr 9, 2026
e1a5169
fix: update node version to 22 and checkout action to v6 in npm publi…
Apr 9, 2026
24fb513
fix: update node version to 24 in npm publish workflow
Apr 9, 2026
3460514
fix: add repository field to package.json
Apr 9, 2026
f1adbc0
CI: bumps secure-ws to 0.0.2-websocket-provider.15
Apr 9, 2026
b1b8c8d
feat: origin validation in WebSocketProvider for cors support
Apr 16, 2026
cc88a57
CI: bumps secure-ws to 0.0.2-websocket-provider.16
Apr 16, 2026
f0288c7
feat: add FUNDWAVE_DOMAIN_PATTERNS for origin validation in WebSocket…
Apr 16, 2026
9f75f61
CI: bumps secure-ws to 0.0.2-websocket-provider.17
Apr 16, 2026
22da40a
add socket: WebSocket to MockResponse interface.
giriparus May 12, 2026
9e7b856
CI: bumps secure-ws to 0.0.2-websocket-provider.18
May 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions .github/workflows/npm-publish-secure-ws.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Release and Publish secure-ws

on:
push:
paths:
- "secure-ws/**"
Comment thread
rickygarg marked this conversation as resolved.
- .github/workflows/npm-publish-secure-ws.yml

permissions:
contents: write
id-token: write

env:
Comment thread
rickygarg marked this conversation as resolved.
BRANCH_TAG: "${{ github.ref_name == 'main' && 'latest' || github.ref_name }}"
BRANCH: ${{ github.ref_name }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

jobs:
publish_and_release:
name: Publish secure-ws package
environment: "${{ github.ref_name == 'main' && 'Prod' || 'Dev' }}"
runs-on: ubuntu-latest
Comment thread
rickygarg marked this conversation as resolved.
defaults:
run:
working-directory: "secure-ws"
steps:
- name: Generate token
if: ${{ github.ref_name == 'main'}}
id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ vars.FUNDABOT_APP_ID }}
private_key: ${{ secrets.FUNDABOT_PRIVATE_KEY }}

- uses: actions/setup-node@v6
with:
node-version: 24
registry-url: https://registry.npmjs.org/

- uses: actions/checkout@v6
with:
token: ${{ github.ref_name == 'main' && steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}

- name: Install dependencies
run: npm ci

Comment on lines +27 to +46
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On main, you generate an app token for checkout, but the later Push changes step uses ${{ env.GITHUB_TOKEN }} which is set to the default secrets.GITHUB_TOKEN, not the generated app token. If main is protected, this push is likely to fail. Consider passing the same conditional token used in checkout (or set env.GITHUB_TOKEN from steps.generate_token.outputs.token on main).

Copilot uses AI. Check for mistakes.
- name: Build
run: npm run build

- name: Version bump
id: version
uses: phips28/gh-action-bump-version@v9.1.0
with:
major-wording: ${{ env.BRANCH == 'main' && '[bump major]' || '[bump major --force]' }}
minor-wording: ${{ env.BRANCH == 'main' && '[bump minor]' || '[bump minor --force]' }}
patch-wording: ${{ null }}
rc-wording: ${{ null }}
default: "${{ env.BRANCH == 'main' && 'patch' || 'prerelease' }}"
PACKAGEJSON_DIR: "secure-ws"
preid: "${{ env.BRANCH }}"
skip-tag: "true"
Comment on lines +58 to +61
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-main branch names often contain / (e.g., feature/foo), but preid: "${{ env.BRANCH }}" and npm publish --tag $BRANCH_TAG will then use an invalid semver prerelease identifier / npm dist-tag. Sanitize github.ref_name for semver/tag safety (e.g., replace / with -).

Copilot uses AI. Check for mistakes.
skip-push: "true"
skip-commit: "true"
bump-policy: "ignore"

- name: Commit changes
env:
VERSION: ${{ steps.version.outputs.newTag }}
run: |
git config user.email "fundabot@fundwave.com"
git config user.name "fundabot"
git commit -a -m "CI: bumps secure-ws to $VERSION" -m "[skip ci]"

- name: Publish package to npm
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow publishes on every push to any branch (no if: github.ref_name == 'main' guard on the publish step), and it does not set NODE_AUTH_TOKEN (unlike existing npm publish workflows in this repo). This is likely to either fail publishes due to missing auth or unintentionally publish prerelease builds from feature branches. Align with .github/workflows/npm-publish.yml by restricting publish to main (or explicitly documenting/restricting which branches are allowed) and configuring npm auth via NODE_AUTH_TOKEN/secrets.

Suggested change
- name: Publish package to npm
- name: Publish package to npm
if: ${{ github.ref_name == 'main'}}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

npm publish requires npm authentication, but this workflow doesn't set NODE_AUTH_TOKEN / .npmrc (unlike the existing npm-publish.yml workflow). Add the npm token setup (or OIDC-based auth if that’s what you intend) so publishing doesn’t fail at runtime.

Suggested change
- name: Publish package to npm
- name: Publish package to npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Copilot uses AI. Check for mistakes.
run: |
npm publish --tag $BRANCH_TAG --access public

Comment thread
rickygarg marked this conversation as resolved.
- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ env.GITHUB_TOKEN }}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On main you generate a GitHub App token for checkout, but the push step always uses ${{ env.GITHUB_TOKEN }} (set to secrets.GITHUB_TOKEN). If main is protected, this can prevent pushing the version-bump commit. Use steps.generate_token.outputs.token for the push step on main (or set env.GITHUB_TOKEN conditionally).

Suggested change
github_token: ${{ env.GITHUB_TOKEN }}
github_token: ${{ github.ref_name == 'main' && steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}

Copilot uses AI. Check for mistakes.
branch: ${{ github.ref }}

- name: Release
if: ${{ github.ref_name == 'main'}}
env:
VERSION: ${{ steps.version.outputs.newTag }}
run: |
if [ "${BRANCH}" != "main" ]; then PRERELEASE="-p"; fi
echo "Releasing version ${VERSION} on branch ${BRANCH}"
gh release create ${VERSION} --target ${BRANCH} --generate-notes ${PRERELEASE}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
**/dist/**
**/node_modules/**
40 changes: 22 additions & 18 deletions .talismanrc
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
threshold: medium
fileignoreconfig:
- filename: aws-credentials-utils/store-credentials.sh
checksum: 784aec6e80314be796af73887ba630fbaf0e116cc4d11bd5cba787a20f9c4bbb
- filename: jwks-slim/README.md
checksum: a1954df51e49fc6a09d7fe04bac198fe60a4fb39b2569180475a749a72df472d
- filename: jwks-slim/package-lock.json
checksum: d014d548f9f2997cbe1d7c4f7ec312f8c634e88788bc453ccc606ea70e081a77
- filename: jwks-slim/tests/cache.test.js
checksum: 716f25000789d7ca18adfe07f968bae6eb2fbfa1c3d05e9e92897b9597ac5f17
- filename: jwks-slim/tests/mock.js
checksum: 0d2fd2ec4847acda384c398e04e8aab9a615a8599b0a06d685e7fb100e71537c
- filename: jwks-slim/tests/keys.js
checksum: 207c0d07dea4c069883822b2b268d3b34c8b18bdfd645b8aa3b2be20f280c270
- filename: jwks-slim/tests/index.test.js
checksum: 992a5c1f6c254fd344ae52955aa0c1c22a59e0472fe836b0450ff499255ef6f8
- filename: aws-ssm/package-lock.json
checksum: 4cf91061e42ed9b1aafb17bdf68c9b4b24a7f86b191133ab4095fa635f8f01a0
version: ""
- filename: aws-credentials-utils/store-credentials.sh
checksum: 784aec6e80314be796af73887ba630fbaf0e116cc4d11bd5cba787a20f9c4bbb
- filename: aws-ssm/package-lock.json
checksum: 4cf91061e42ed9b1aafb17bdf68c9b4b24a7f86b191133ab4095fa635f8f01a0
- filename: jwks-slim/README.md
checksum: a1954df51e49fc6a09d7fe04bac198fe60a4fb39b2569180475a749a72df472d
- filename: jwks-slim/package-lock.json
checksum: d014d548f9f2997cbe1d7c4f7ec312f8c634e88788bc453ccc606ea70e081a77
- filename: jwks-slim/tests/cache.test.js
checksum: 716f25000789d7ca18adfe07f968bae6eb2fbfa1c3d05e9e92897b9597ac5f17
- filename: jwks-slim/tests/index.test.js
checksum: 992a5c1f6c254fd344ae52955aa0c1c22a59e0472fe836b0450ff499255ef6f8
- filename: jwks-slim/tests/keys.js
checksum: 207c0d07dea4c069883822b2b268d3b34c8b18bdfd645b8aa3b2be20f280c270
- filename: jwks-slim/tests/mock.js
checksum: 0d2fd2ec4847acda384c398e04e8aab9a615a8599b0a06d685e7fb100e71537c
- filename: secure-ws/mocks/express-response.ts
checksum: 7774e72616894119c419f7ad86a3da579036b0391fd208766bcdcb96401b8d2c
- filename: websocket-provider/package-lock.json
checksum: 605a2d92b91f08ee577d3b1e7ed38d27eef43573a044608c22466f05ab6eaff7
Comment on lines +2 to +21
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This YAML structure no longer nests the ignore list under fileignoreconfig (the - filename entries are at the top level). That likely breaks talisman’s config parsing. Indent the list items under fileignoreconfig: (e.g., fileignoreconfig:\n - filename: ...).

Suggested change
- filename: aws-credentials-utils/store-credentials.sh
checksum: 784aec6e80314be796af73887ba630fbaf0e116cc4d11bd5cba787a20f9c4bbb
- filename: aws-ssm/package-lock.json
checksum: 4cf91061e42ed9b1aafb17bdf68c9b4b24a7f86b191133ab4095fa635f8f01a0
- filename: jwks-slim/README.md
checksum: a1954df51e49fc6a09d7fe04bac198fe60a4fb39b2569180475a749a72df472d
- filename: jwks-slim/package-lock.json
checksum: d014d548f9f2997cbe1d7c4f7ec312f8c634e88788bc453ccc606ea70e081a77
- filename: jwks-slim/tests/cache.test.js
checksum: 716f25000789d7ca18adfe07f968bae6eb2fbfa1c3d05e9e92897b9597ac5f17
- filename: jwks-slim/tests/index.test.js
checksum: 992a5c1f6c254fd344ae52955aa0c1c22a59e0472fe836b0450ff499255ef6f8
- filename: jwks-slim/tests/keys.js
checksum: 207c0d07dea4c069883822b2b268d3b34c8b18bdfd645b8aa3b2be20f280c270
- filename: jwks-slim/tests/mock.js
checksum: 0d2fd2ec4847acda384c398e04e8aab9a615a8599b0a06d685e7fb100e71537c
- filename: secure-ws/mocks/express-response.ts
checksum: 7774e72616894119c419f7ad86a3da579036b0391fd208766bcdcb96401b8d2c
- filename: websocket-provider/package-lock.json
checksum: 605a2d92b91f08ee577d3b1e7ed38d27eef43573a044608c22466f05ab6eaff7
- filename: aws-credentials-utils/store-credentials.sh
checksum: 784aec6e80314be796af73887ba630fbaf0e116cc4d11bd5cba787a20f9c4bbb
- filename: aws-ssm/package-lock.json
checksum: 4cf91061e42ed9b1aafb17bdf68c9b4b24a7f86b191133ab4095fa635f8f01a0
- filename: jwks-slim/README.md
checksum: a1954df51e49fc6a09d7fe04bac198fe60a4fb39b2569180475a749a72df472d
- filename: jwks-slim/package-lock.json
checksum: d014d548f9f2997cbe1d7c4f7ec312f8c634e88788bc453ccc606ea70e081a77
- filename: jwks-slim/tests/cache.test.js
checksum: 716f25000789d7ca18adfe07f968bae6eb2fbfa1c3d05e9e92897b9597ac5f17
- filename: jwks-slim/tests/index.test.js
checksum: 992a5c1f6c254fd344ae52955aa0c1c22a59e0472fe836b0450ff499255ef6f8
- filename: jwks-slim/tests/keys.js
checksum: 207c0d07dea4c069883822b2b268d3b34c8b18bdfd645b8aa3b2be20f280c270
- filename: jwks-slim/tests/mock.js
checksum: 0d2fd2ec4847acda384c398e04e8aab9a615a8599b0a06d685e7fb100e71537c
- filename: secure-ws/mocks/express-response.ts
checksum: 7774e72616894119c419f7ad86a3da579036b0391fd208766bcdcb96401b8d2c
- filename: websocket-provider/package-lock.json
checksum: 605a2d92b91f08ee577d3b1e7ed38d27eef43573a044608c22466f05ab6eaff7

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +21
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is newly added, but it is already being ignored by Talisman (fileignoreconfig). Adding ignores reduces secret-detection coverage going forward; it’s safer to remove this ignore entry and fix the underlying finding (or add a narrower ignore with justification) so new changes to this file continue to be scanned.

Suggested change
- filename: websocket-provider/package-lock.json
checksum: 605a2d92b91f08ee577d3b1e7ed38d27eef43573a044608c22466f05ab6eaff7

Copilot uses AI. Check for mistakes.
threshold: medium
version: "1.0"
20 changes: 20 additions & 0 deletions secure-ws/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Exclude TypeScript source files and configs
src/
*.ts
*.tsx
tsconfig.json
# Exclude node_modules
node_modules/
# Exclude test files and folders
test/
*.spec.*
*.test.*
# Exclude build scripts and configs
*.log
*.env
# Exclude editor and OS files
.DS_Store
.vscode/
.idea/
# Exclude other common files
coverage/
46 changes: 46 additions & 0 deletions secure-ws/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# secure-ws

secure-ws is a TypeScript library for secure WebSocket servers that lets you run Express-style middleware during the upgrade handshake, ensuring unauthenticated connections are never left open.
Comment on lines +1 to +3
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This repository doesn’t appear to have a .github/instructions/ directory with Copilot instructions for TypeScript/Node files (only custom-instructions/ exists). If this repo relies on GitHub Copilot instruction files, add the appropriate instruction files under .github/instructions for this new package to match the expected project layout.

Copilot uses AI. Check for mistakes.

Comment on lines +3 to +4
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README/PR description says middleware runs "during the WebSocket upgrade handshake", but the current implementation runs middleware in the connection handler (after handleUpgrade completes). If handshake-time rejection is required, consider moving auth/validation to handleUpgrade (e.g., via verifyClient / manual HTTP response) or update the docs to reflect the actual behavior (connection is briefly established then closed).

Copilot uses AI. Check for mistakes.
## Why use secure-ws

- Run middleware during the WebSocket upgrade handshake
- Reuse Express Middlewares for authentication, validation and more
- Abstract away connection, upgrade, and messaging with Express-style routes, middleware, and controllers.

## Installation

```
npm install --save secure-ws
```

## Usage

```typescript
import express from "express";
import { WebSocketProvider } from 'secure-ws';

const wsApp = new WebSocketProvider();
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README usage constructs new WebSocketProvider() without arguments, but the constructor currently requires { allowedOrigins: string }. Either update the README example to pass allowedOrigins, or make the constructor parameter optional with a secure default (ideally explicit allowlist required).

Suggested change
const wsApp = new WebSocketProvider();
const wsApp = new WebSocketProvider({
allowedOrigins: "https://your-app.example.com"
});

Copilot uses AI. Check for mistakes.
const app = express();

// app.get...
// app.post...

wsApp.addRoute(
"/extract/metrics",
{
onConnect: [authMiddleware1, authMiddleware2],
onMessage: [controller]
}
);

const httpServer = app.listen(PORT, () => {
console.info(`Service running at port: ${PORT}`);
});

httpServer.on('upgrade', wsApp.handleUpgrade);
```

## License

MIT
138 changes: 138 additions & 0 deletions secure-ws/core/web-socket-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { WebSocketServer } from 'ws';
import { IncomingMessage } from 'http';
import { decode } from "@msgpack/msgpack";
import { randomUUID } from "crypto";

import { WSProtocolCodec } from './ws-protocol-codec';
import { runExpressMiddleware } from '../utils/middleware-adapter';
import { injectHttpRequest } from '../utils/inject-http-request';

import { WSController } from '../types/ws-controller';
import { AddRouteParams } from '../types/add-route-params';
import { ClientSocket } from '../types/client-socket';
import { Duplex } from 'stream';

const FUNDWAVE_DOMAIN_PATTERNS = [
/^(https:\/\/([a-z0-9-]+[.])*(jcurve|fundwave|dealflow|investorportal))[.]app/,
/^(https:\/\/[a-z0-9-]+[.](get)*fundwave)[.]com/
];

export class WebSocketProvider {
public server: WebSocketServer;
public clientSockets: Map<string, ClientSocket>;
private routes: Record<string, AddRouteParams>;
private allowedOrigins: string;

constructor({ allowedOrigins }: { allowedOrigins: string }) {
this.allowedOrigins = allowedOrigins;
this.server = new WebSocketServer({ noServer: true });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if we set noServer to false? Then we are able to set our own upgrade logic in the specified http server?

https://www.reddit.com/r/node/comments/sfgmum/can_someone_kindly_explain_what_noserver_mode/

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If noServer is set to false (which it is by default), it spins up an http server on the specified port. In this case we won't have any control over the upgrade logic.

const wss = new WebSocketServer({ port: 8080 });

In our case, we already have an http server provided by express and we want to just use the socket connection after upgrade happens. Hence noServer.

The alternative is to pass the server to the constructor.

import { createServer } from 'https';
import { WebSocketServer } from 'ws';

const server = createServer({
  cert: readFileSync('/path/to/cert.pem'),
  key: readFileSync('/path/to/key.pem')
});
const wss = new WebSocketServer({ server });

But even in that case, this.server.handleUpgrade will be called automatically on all upgrades. We don't want this as we only want to upgrade if the enpoint matches a registered route [Ref lines 38-41 core/web-socket-provider.ts]

From docs:

One of port, server or noServer must be provided or an error is thrown.

Refs:

  1. https://www.npmjs.com/package/ws
  2. https://github.com/websockets/ws/blob/0a9621f9ff35e6f80c9c8471d0b202af4e357705/doc/ws.md#new-websocketserveroptions-callback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the idea is to run the WS server in parallel with regular REST API? I'd imagine having a separate server for WS only might help plan scaling up and rate limiting better.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that depends on the number of ws endpoints one wants across services. If we want to have one endpoint for most of the services that would mean a common ws server would need access to all resources for those microservices. This ws server would become a monolith soon. For just a few endpoints, having a common ws makes sense for scalability but, in my opinion, the drawbacks would overcome the benefits as the number of endpoints increase.

The other alternative would be to have ws microservices in parallel with the existing microservices. But that would nearly double the number of microservices we have right now, leading to additional overhead of managing these services.

Another approach that was discussed previously was to have a common ws-service that only manages a single socket connection from the client and relays the request to the respective services. However it would still need ws endpoints on the services that it connects to. We can add rate limiting rules to this service and then later plug this between the client and server with the current architecture.

this.server.on('connection', this.handleConnection);
this.routes = {};
this.clientSockets = new Map();
}
Comment on lines +20 to +32
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new WebSocket provider introduces several non-trivial behaviors (upgrade routing, protocol decoding, middleware execution, message decoding) but there are no automated tests added for them. Since the repo already has test coverage for other published packages (e.g., jwks-slim/tests/*), consider adding tests for at least: middleware rejection/acceptance paths, protocol injection, and message decode error handling.

Copilot uses AI. Check for mistakes.

public addRoute = (
path: string,
{
onConnect,
onMessage,
} : AddRouteParams
) => {
this.routes[path] = {
onConnect,
onMessage
}
}
public handleUpgrade = (request: IncomingMessage, socket: Duplex, head: Buffer) => {
const { pathname } = new URL(request.url!, 'wss://base.url');
const origin = request.headers.origin;

if (this.allowedOrigins) {

const origins = this.allowedOrigins
.split(",")
.filter(origin => origin.trim())
.map(origin => {
if (/^\/.*\/$/.test(origin)) {
return new RegExp(origin.replace(/^\/(.*)\/$/, "$1"));
}
return origin;
});

origins.push(...FUNDWAVE_DOMAIN_PATTERNS);

Comment on lines +61 to +63
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FUNDWAVE_DOMAIN_PATTERNS are always appended to the allowlist, so a consumer cannot configure origins to only their own domains. Consider making these patterns opt-in (config flag) or removing them from the generic library to avoid unintentionally broad origin access.

Copilot uses AI. Check for mistakes.
const isOriginAllowed = origins.some(allowedOrigin => {
if (allowedOrigin === '*') return true;
if (allowedOrigin instanceof RegExp) {
return allowedOrigin.test(origin || '');
}
const regex = new RegExp(`^${allowedOrigin.replace(/\*/g, '.*')}$`);
return regex.test(origin || '');
Comment on lines +65 to +70
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Origin allowlist matching builds a RegExp directly from allowedOrigin without escaping regex metacharacters (e.g., .), so values like https://example.com will match unintended origins. Escape the string before replacing *, or avoid regex and use exact string comparison when no wildcard is present.

Suggested change
if (allowedOrigin === '*') return true;
if (allowedOrigin instanceof RegExp) {
return allowedOrigin.test(origin || '');
}
const regex = new RegExp(`^${allowedOrigin.replace(/\*/g, '.*')}$`);
return regex.test(origin || '');
const requestOrigin = origin || '';
if (allowedOrigin === '*') return true;
if (allowedOrigin instanceof RegExp) {
return allowedOrigin.test(requestOrigin);
}
if (!allowedOrigin.includes('*')) {
return allowedOrigin === requestOrigin;
}
const escapedAllowedOrigin = allowedOrigin
.replace(/[|\\{}()[\]^$+?.]/g, '\\$&')
.replace(/\*/g, '.*');
const regex = new RegExp(`^${escapedAllowedOrigin}$`);
return regex.test(requestOrigin);

Copilot uses AI. Check for mistakes.
});

if (!isOriginAllowed) {
console.error('Origin not allowed:', origin);
socket.end();
return;
}
}


if (this.routes[pathname]) {
this.server.handleUpgrade(request, socket, head, (ws: ClientSocket) => {

ws.id = `${Date.now()}-${randomUUID()}`;
this.clientSockets.set(ws.id, ws);
ws.on('close', () => {
this.clientSockets.delete(ws.id);
});

this.server.emit('connection', ws, request);
});
Comment on lines +46 to +91
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README/PR description claims middleware runs "during the upgrade handshake", but the current flow upgrades the connection in handleUpgrade() and only runs onConnect middleware later in the 'connection' handler. If the intent is to reject unauthenticated clients before the WebSocket is established, the middleware (or an equivalent auth step) needs to run before calling server.handleUpgrade() (or use verifyClient/custom handleUpgrade gating) and respond with an HTTP error instead of upgrading first.

Copilot uses AI. Check for mistakes.
} else {
console.log('No route for path:', pathname);
socket.end();
}
};

private handleConnection = async (ws: ClientSocket, request: IncomingMessage) => {
const { pathname } = new URL(request.url!, 'wss://base.url');

let injectedRequest = {} as IncomingMessage;

if (typeof ws.protocol === 'string' && ws.protocol.trim() !== '') {
const httpRequest = WSProtocolCodec.decode(ws.protocol);
injectedRequest = injectHttpRequest(request, httpRequest);
}
Comment on lines +101 to +106
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

injectedRequest is initialized as an empty object and only set when ws.protocol is non-empty. When no protocol is provided, runExpressMiddleware receives an empty request object instead of the actual IncomingMessage, which will break typical auth/validation middleware expecting headers/url/etc. Initialize injectedRequest to request and then mutate it when protocol data is present.

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +106
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

injectedRequest is initialized as an empty object, so when ws.protocol is empty the onConnect middleware receives a request without headers/url/etc. Initialize injectedRequest to the original request by default, and only mutate it when protocol data is provided.

Copilot uses AI. Check for mistakes.

if(!this.routes[pathname]) {
ws.close(1000, 'Unknown path');
return;
}

const { onConnect, onMessage } = this.routes[pathname];

for (const middleware of onConnect) {
const res = await runExpressMiddleware(middleware, injectedRequest, ws);
if (!res.success) {
console.error("Middleware rejected connection:", res.error);
ws.send(JSON.stringify({ error: res.error }));
ws.close(1000, res.error);
Comment on lines +119 to +120
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On middleware rejection, the code calls ws.send(...) and immediately ws.close(...). In ws, closing right after sending can result in the message being dropped if it hasn't been flushed yet. Consider closing in the send callback (or waiting for the socket to drain) to ensure the client reliably receives the error payload.

Suggested change
ws.send(JSON.stringify({ error: res.error }));
ws.close(1000, res.error);
ws.send(JSON.stringify({ error: res.error }), () => {
ws.close(1000, res.error);
});

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When middleware rejects a connection, the socket is closed with code 1000 (normal closure). Use a more appropriate close code like 1008 (policy violation) so clients can reliably distinguish authentication/authorization failures from normal disconnects.

Suggested change
ws.close(1000, res.error);
ws.close(1008, res.error);

Copilot uses AI. Check for mistakes.
return;
}
}

ws.on('message', this.handleMessage(ws, onMessage));
ws.send(JSON.stringify({ type: "connection:ack" }));
};

private handleMessage = (socket: ClientSocket, onMessage: WSController[]) => {
return async (message) => {
const request = decode(message) as IncomingMessage;

for (const controller of onMessage) {
await controller(request, socket);
Comment on lines +130 to +134
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decode(message) is cast to IncomingMessage, but a msgpack-decoded WebSocket message is not an HTTP IncomingMessage. This misleading type can hide real issues and encourages controllers to treat the payload like an HTTP request. Consider defining a dedicated message payload type (or unknown) and passing that through to controllers.

Suggested change
return async (message) => {
const request = decode(message) as IncomingMessage;
for (const controller of onMessage) {
await controller(request, socket);
type ControllerMessage = Parameters<WSController>[0];
return async (message) => {
const payload: unknown = decode(message);
for (const controller of onMessage) {
await controller(payload as ControllerMessage, socket);

Copilot uses AI. Check for mistakes.
}
};
}
}
16 changes: 16 additions & 0 deletions secure-ws/core/ws-protocol-codec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export class WSProtocolCodec {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather Base64UrlSafeCodec if it isn't specific to WS

Copy link
Copy Markdown
Author

@sudo-rgorai sudo-rgorai Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is specific to the websocket as the name of the websocket protocol (Sec-WebSocket-Protocol) needs to be url safe

static encode(data: unknown): string {
const json = JSON.stringify(data);
return Buffer.from(json).toString('base64')
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}

static decode<T = unknown>(encoded: string): T {
let str = encoded.replace(/-/g, "+").replace(/_/g, "/");
while (str.length % 4) str += "=";
const json = Buffer.from(str, 'base64').toString();
return JSON.parse(json) as T;
}
}
4 changes: 4 additions & 0 deletions secure-ws/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './core/web-socket-provider';
export * from './core/ws-protocol-codec';
export type { MockResponse } from './types/mock-response';
export type { ExpressMiddleware } from './types/express-middleware';
Loading