Skip to content

Add secure-ws library#22

Open
sudo-rgorai wants to merge 61 commits intomainfrom
websocket-provider
Open

Add secure-ws library#22
sudo-rgorai wants to merge 61 commits intomainfrom
websocket-provider

Conversation

@sudo-rgorai
Copy link
Copy Markdown

@sudo-rgorai sudo-rgorai commented Sep 12, 2025

Add new secure-ws library

Overview

Add secure-ws library to manage websocket connections for Node.js and TypeScript projects.

Motivation

  • To provide a reusable WebSocket provider for projects requiring real-time communication.
  • To enable easy integration with existing Express middleware and TypeScript-based codebases.

Resolution

Linked to getfundwave/discussions/issues/445
Closes getfundwave/discussions/issues/477

Features

  • Core WebSocket Provider:
    Implements a modular WebSocket provider in core/web-socket-provider.ts with support for custom protocols and extensibility.

  • Protocol Codec:
    Encapsulates WebSocket protocol encoding/decoding logic in core/ws-protocol-codec.ts to send headers and body in the websocket upgrade handshake

  • Middleware Support:
    Includes utilities for adapting Express-style middleware to WebSocket handlers in utils/middleware-adapter.ts and utils/inject-http-request.ts.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 19 changed files in this pull request and generated 13 comments.

Files not reviewed (1)
  • secure-ws/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +8 to +34
middleware: (req: Request, res: MockResponseType, next: NextFunction) => void,
request: IncomingMessage,
socket: WebSocket
): Promise<{ success: boolean; error?: string }> {
return new Promise((resolve) => {
const mockRes = new MockResponse(socket);
let nextCalled = false;

const next = (error?: any) => {
if (nextCalled) return;
nextCalled = true;

if (error) {
resolve({ success: false, error: error.message || 'Middleware error' });
} else {
resolve({ success: true });
}
};

try {
middleware(request as Request, mockRes as MockResponseType, next);
} catch (error) {
console.error("Middleware exception:", error);
if (!nextCalled) {
nextCalled = true;
resolve({ success: false, error: error.message || 'Middleware exception' });
}
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.

runExpressMiddleware only resolves when next() is called or an exception is thrown. If an Express middleware ends the response (e.g., calls res.send()/res.sendStatus()) without calling next, this Promise never resolves and handleConnection will await indefinitely. Also, async/Promise-returning middleware is not awaited/rejected here, so rejections won’t be caught/resolved. Consider wiring MockResponse.send/sendStatus to resolve the Promise (or emit a completion event) and handle Promise-returning middleware by awaiting/attaching a .catch() to it.

Suggested change
middleware: (req: Request, res: MockResponseType, next: NextFunction) => void,
request: IncomingMessage,
socket: WebSocket
): Promise<{ success: boolean; error?: string }> {
return new Promise((resolve) => {
const mockRes = new MockResponse(socket);
let nextCalled = false;
const next = (error?: any) => {
if (nextCalled) return;
nextCalled = true;
if (error) {
resolve({ success: false, error: error.message || 'Middleware error' });
} else {
resolve({ success: true });
}
};
try {
middleware(request as Request, mockRes as MockResponseType, next);
} catch (error) {
console.error("Middleware exception:", error);
if (!nextCalled) {
nextCalled = true;
resolve({ success: false, error: error.message || 'Middleware exception' });
}
middleware: (req: Request, res: MockResponseType, next: NextFunction) => void | Promise<unknown>,
request: IncomingMessage,
socket: WebSocket
): Promise<{ success: boolean; error?: string }> {
return new Promise((resolve) => {
const mockRes = new MockResponse(socket);
let completed = false;
const resolveOnce = (result: { success: boolean; error?: string }) => {
if (completed) return;
completed = true;
resolve(result);
};
const next = (error?: any) => {
if (error) {
resolveOnce({ success: false, error: error.message || 'Middleware error' });
} else {
resolveOnce({ success: true });
}
};
const mockResWithHandlers = mockRes as MockResponseType & {
send?: (...args: any[]) => any;
sendStatus?: (...args: any[]) => any;
};
if (typeof mockResWithHandlers.send === 'function') {
const originalSend = mockResWithHandlers.send.bind(mockResWithHandlers);
mockResWithHandlers.send = (...args: any[]) => {
const result = originalSend(...args);
resolveOnce({ success: true });
return result;
};
}
if (typeof mockResWithHandlers.sendStatus === 'function') {
const originalSendStatus = mockResWithHandlers.sendStatus.bind(mockResWithHandlers);
mockResWithHandlers.sendStatus = (...args: any[]) => {
const result = originalSendStatus(...args);
resolveOnce({ success: true });
return result;
};
}
try {
const middlewareResult = middleware(request as Request, mockRes as MockResponseType, next);
if (
middlewareResult &&
typeof (middlewareResult as Promise<unknown>).then === 'function'
) {
(middlewareResult as Promise<unknown>).catch((error: any) => {
console.error("Middleware exception:", error);
resolveOnce({ success: false, error: error?.message || 'Middleware exception' });
});
}
} catch (error: any) {
console.error("Middleware exception:", error);
resolveOnce({ success: false, error: error?.message || 'Middleware exception' });

Copilot uses AI. Check for mistakes.
}

// Attach body as a custom property
if (httpRequest.body) {
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.

if (httpRequest.body) skips valid falsy bodies (e.g., empty string, 0, false, null). If the intent is “provided vs not provided”, use an undefined check (e.g., httpRequest.body !== undefined) so the body is injected consistently.

Suggested change
if (httpRequest.body) {
if (httpRequest.body !== undefined) {

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +67
let injectedRequest = {} as IncomingMessage;

if (typeof ws.protocol === 'string' && ws.protocol.trim() !== '') {
const httpRequest = WSProtocolCodec.decode(ws.protocol);
injectedRequest = injectHttpRequest(request, httpRequest);
}
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 +91 to +95
return async (message) => {
const request = decode(message) as IncomingMessage;

for (const controller of onMessage) {
await controller(request, socket);
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.
@@ -0,0 +1,6 @@
// types/mock-response.ts
export interface MockResponse {
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.

MockResponse includes a locals property, but the exported MockResponse interface doesn’t declare it. This prevents middleware from using res.locals in a type-safe way and makes the mock/contract inconsistent. Consider adding locals (and any other supported Express-like fields) to the interface, or removing it from the implementation if it isn’t intended to be supported.

Suggested change
export interface MockResponse {
export interface MockResponse {
locals: Record<string, any>;

Copilot uses AI. Check for mistakes.
Comment thread .talismanrc
Comment on lines +2 to +21
- 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
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 thread .github/workflows/npm-publish-secure-ws.yml
Comment on lines +22 to +40
- 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@v3
with:
node-version: 18

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

- name: Install dependencies
run: npm ci

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.
Comment thread .github/workflows/npm-publish-secure-ws.yml
@@ -0,0 +1,3 @@
import { WebSocket } from 'ws';

export type WSController = (message: any, socket: WebSocket) => any; No newline at end of file
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.

WSController uses any for both the message parameter and return type, which leaks any into consumers and defeats type safety across the public API surface. Consider using unknown for inputs and a more specific return type (often void | Promise<void>) so consumers can model their message shapes explicitly.

Suggested change
export type WSController = (message: any, socket: WebSocket) => any;
export type WSController = (message: unknown, socket: WebSocket) => void | Promise<void>;

Copilot uses AI. Check for mistakes.
… add locals property to MockResponse interface
Copilot AI review requested due to automatic review settings April 9, 2026 13:11
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 19 changed files in this pull request and generated 6 comments.

Files not reviewed (1)
  • secure-ws/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

import { WebSocket } from 'ws';
import { MockResponse } from '../mocks/express-response';
import { MockResponse as MockResponseType } from '../types/mock-response';
import { NextFunction, Request, Response } from 'express';
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.

Response is imported from express but never used in this module. Removing unused imports avoids lint/build failures in projects that enforce noUnusedLocals/noUnusedParameters.

Suggested change
import { NextFunction, Request, Response } from 'express';
import { NextFunction, Request } from 'express';

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +16
// Merge headers
if (httpRequest.headers) {
for (const [key, value] of Object.entries(httpRequest.headers)) {
request.headers[key.toLowerCase()] = value;
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.

injectHttpRequest unconditionally overwrites existing request headers with values provided via ws.protocol. This lets a client spoof/override headers that may already be set by upstream infrastructure (e.g. x-forwarded-for, cookie, etc.). Consider whitelisting which headers can be injected, namespacing them, or only setting a header if it is not already present.

Suggested change
// Merge headers
if (httpRequest.headers) {
for (const [key, value] of Object.entries(httpRequest.headers)) {
request.headers[key.toLowerCase()] = value;
// Merge headers without overwriting headers already present on the request
if (httpRequest.headers) {
for (const [key, value] of Object.entries(httpRequest.headers)) {
const normalizedKey = key.toLowerCase();
if (request.headers[normalizedKey] === undefined) {
request.headers[normalizedKey] = value;
}

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +52
public handleUpgrade = (request: IncomingMessage, socket: Duplex, head: Buffer) => {
const { pathname } = new URL(request.url!, 'wss://base.url');

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);
});
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.
Comment on lines +80 to +81
ws.send(JSON.stringify({ error: res.error }));
ws.close(1000, res.error);
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.
Comment on lines +72 to +77
git commit -a -m "CI: bumps secure-ws to $VERSION" -m "[skip ci]"

- name: Publish package to npm
run: |
npm publish --tag $BRANCH_TAG --access public

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 npm publish step writes only registry=... to ~/.npmrc but does not configure the auth token line (//registry.npmjs.org/:_authToken=...). Unlike the other publish workflows in this repo, this is likely to cause npm publish to fail with an auth error. Consider matching the pattern used in .github/workflows/npm-publish.yml / npm-publish-ssm-env-util.yml by writing the _authToken entry (or using actions/setup-node with registry-url so NODE_AUTH_TOKEN is picked up).

Copilot uses AI. Check for mistakes.
Comment thread .github/workflows/npm-publish-secure-ws.yml
Copilot AI review requested due to automatic review settings April 9, 2026 13:31
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 19 changed files in this pull request and generated 6 comments.

Files not reviewed (1)
  • secure-ws/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +7 to +10
export function runExpressMiddleware(
middleware: (req: Request, res: MockResponseType, next: NextFunction) => void,
request: IncomingMessage,
socket: WebSocket
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.

runExpressMiddleware accepts middleware as (req: Request, res: MockResponseType, next) => void, but routes are typed to accept ExpressMiddleware (req, res: Response, next) => any (see secure-ws/types/express-middleware.ts). This makes the public API type-inconsistent for consumers using strict type-checking. Prefer typing the adapter to accept ExpressMiddleware and cast/wrap the mock response internally to satisfy Response (or extend the mock to implement the needed subset).

Copilot uses AI. Check for mistakes.
Comment thread .talismanrc
Comment on lines +20 to +21
- filename: websocket-provider/package-lock.json
checksum: 605a2d92b91f08ee577d3b1e7ed38d27eef43573a044608c22466f05ab6eaff7
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.
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.
Comment thread secure-ws/package.json
"build": "tsc"
},
"author": "The Fundwave Authors",
"license": "ISC",
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 declared package license (ISC) doesn’t match the README’s stated license (MIT). This should be consistent to avoid legal/packaging confusion; update either package.json or README.md so they agree.

Suggested change
"license": "ISC",
"license": "MIT",

Copilot uses AI. Check for mistakes.
Comment thread secure-ws/README.md
Comment on lines +1 to +3
# 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.
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 +15 to +25
export class WebSocketProvider {
public server: WebSocketServer;
public clientSockets: Map<string, ClientSocket>;
private routes: Record<string, AddRouteParams>;

constructor() {
this.server = new WebSocketServer({ noServer: true });
this.server.on('connection', this.handleConnection);
this.routes = {};
this.clientSockets = new Map();
}
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.
Copilot AI review requested due to automatic review settings April 16, 2026 12:42
@sudo-rgorai sudo-rgorai review requested due to automatic review settings April 16, 2026 12:42
Copilot AI review requested due to automatic review settings April 16, 2026 12:57
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 19 changed files in this pull request and generated 11 comments.

Files not reviewed (1)
  • secure-ws/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +65 to +70
if (allowedOrigin === '*') return true;
if (allowedOrigin instanceof RegExp) {
return allowedOrigin.test(origin || '');
}
const regex = new RegExp(`^${allowedOrigin.replace(/\*/g, '.*')}$`);
return regex.test(origin || '');
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.
Comment on lines +61 to +63

origins.push(...FUNDWAVE_DOMAIN_PATTERNS);

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.
Comment on lines +27 to +35
try {
middleware(request as Request, mockRes as MockResponseType, next);
} catch (error) {
console.error("Middleware exception:", error);
if (!nextCalled) {
nextCalled = true;
resolve({ success: false, error: error.message || 'Middleware exception' });
}
}
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.

Async Express middleware (declared async / returning a Promise) isn't handled: any rejection after an await won't be caught by the surrounding try/catch, and the returned Promise is ignored. Capture the return value from middleware(...) and, if it's a Promise, attach .catch(next) (and/or await it) so failures reliably reject the connection.

Copilot uses AI. Check for mistakes.
Comment thread secure-ws/README.md
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.
Comment on lines +58 to +61
default: "${{ env.BRANCH == 'main' && 'patch' || 'prerelease' }}"
PACKAGEJSON_DIR: "secure-ws"
preid: "${{ env.BRANCH }}"
skip-tag: "true"
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.
Comment thread secure-ws/README.md
Comment on lines +3 to +4
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.

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.
Comment thread secure-ws/package.json
Comment on lines +10 to +13
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc"
},
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.

This new library introduces significant behavior (origin validation, protocol decoding, middleware adaptation) but the package has no real test runner (npm test always exits 1) and there are no automated tests. Add unit/integration tests (similar to jwks-slim's Jest setup) to cover origin allowlisting, middleware success/failure, and protocol encode/decode roundtrips.

Copilot uses AI. Check for mistakes.
if (!res.success) {
console.error("Middleware rejected connection:", res.error);
ws.send(JSON.stringify({ error: res.error }));
ws.close(1000, res.error);
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.
- 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.
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 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants