Generic SSE (Server-Sent Events) server with JWKS authentication and plugin hooks. Mounts onto any Express application.
- JWKS authentication — strict RS256/ES256 allowlist, audience+issuer pinning, 60s clock skew
- Channel subscriptions — clients subscribe to named channels
- User-targeted events —
publishToUser(userId, event)reaches all of a user's connections - Reconnect support — ring-buffer per channel;
Last-Event-IDtriggers replay - Plugin hooks — override auth, authorization, connect/disconnect logic
- Heartbeat — configurable comment-based keepalive
npm install @progalaxyelabs/stonescriptphp-sseExpress 5 is a peer dependency:
npm install express@^5import express from 'express';
import { createSseServer } from '@progalaxyelabs/stonescriptphp-sse';
const app = express();
const sseServer = createSseServer({
jwks: {
url: process.env.JWKS_URL, // e.g. https://auth.example.com/.well-known/jwks.json
issuer: process.env.JWT_ISSUER, // e.g. https://auth.example.com
audience: process.env.JWT_AUDIENCE, // e.g. my-app
},
heartbeatInterval: 30_000,
});
app.use('/sse', sseServer.router);
app.listen(3000, () => console.log('Listening on :3000'));
// Publish from anywhere in your business logic:
sseServer.publish('low-stock', { type: 'low_stock', data: { item_id: 42, qty: 2 } });
sseServer.publishToUser('user-123', { type: 'alert', data: { message: 'Low stock warning' } });// Browsers send the Authorization header via EventSource only in fetch-based polyfills.
// The recommended approach is a token query parameter:
const token = getAccessToken(); // your auth flow
const es = new EventSource(`/sse?token=${token}&channel=low-stock`);
es.addEventListener('low_stock', (e) => {
console.log('Low stock alert:', JSON.parse(e.data));
});
es.addEventListener('connected', (e) => {
console.log('SSE connected', JSON.parse(e.data));
});Override any hook by passing a hooks object to createSseServer:
const sseServer = createSseServer({
jwks: { /* ... */ },
hooks: {
/**
* Custom authentication — e.g. add extra user fields from your DB.
* Receives the raw token string. Must return a user payload or throw with status=401.
*/
authenticateSubscriber: async (token) => {
const payload = await myJwksVerifier.verify(token); // or use the default
const user = await db.users.findById(payload.sub);
return { ...payload, role: user.role };
},
/**
* Channel authorization — return false to block the subscription.
* Default: allow all channels.
*/
authorizeChannel: async (user, channel) => {
if (channel.startsWith('admin:')) return user.role === 'admin';
if (channel.startsWith('user:')) return channel === `user:${user.sub}`;
return true;
},
onConnect: async (client) => console.log('connect', client.id),
onDisconnect: async (client) => console.log('disconnect', client.id),
},
});Returns { router, publish, publishToUser, stop, clients }.
| Option | Type | Default | Description |
|---|---|---|---|
jwks.url |
string |
JWKS_URL env |
Remote JWKS endpoint |
jwks.issuer |
string |
JWT_ISSUER env |
Expected iss claim |
jwks.audience |
string |
JWT_AUDIENCE env |
Expected aud claim |
heartbeatInterval |
number |
30000 (or SSE_HEARTBEAT_INTERVAL env) |
ms between heartbeat comments |
hooks |
object |
defaults | Plugin hooks (see above) |
Express Router. Mount with app.use('/sse', sseServer.router).
Routes:
GET /— SSE subscribe endpointGET /health— liveness probe (no auth, returns{"status":"healthy","clients":N})
Query parameters for GET /:
channel(repeatable) — channels to subscribe totoken— JWT bearer token (alternative toAuthorization: Bearerheader)
Broadcast an event to all subscribers of channel. Always buffered for reconnect replay.
sseServer.publish('notifications', {
type: 'new_order',
data: { order_id: 99, total: 250 }
});Send an event to all connections belonging to a specific user (sub claim).
sseServer.publishToUser('user-42', {
type: 'alert',
data: { message: 'Your order shipped!' }
});Stop the heartbeat timer. Call during graceful shutdown.
Map<clientId, { id, userId, channels, res, connectedAt }> — live connected clients.
| Variable | Required | Default | Description |
|---|---|---|---|
JWKS_URL |
✓ (or pass in jwks.url) |
— | JWKS endpoint URL |
JWT_ISSUER |
✓ (or pass in jwks.issuer) |
— | JWT issuer |
JWT_AUDIENCE |
✓ (or pass in jwks.audience) |
— | JWT audience |
SSE_HEARTBEAT_INTERVAL |
— | 30000 |
Heartbeat interval (ms) |
PORT |
— | app decides | Port (not used by this lib directly) |
CORS_ORIGINS |
— | app decides | CORS (configure on your Express app) |
- Allowed algorithms: RS256, ES256 only
- Rejected: HS256,
none, any unknown algorithm - Audience pinning: enforced via
audienceconfig - Issuer pinning: enforced via
issuerconfig - Clock skew: max 60 seconds
Any token that fails these checks returns a 401 before JWKS lookup (where applicable).
The server maintains a ring buffer of the last 100 events per channel. When a client reconnects and sends Last-Event-ID: N, all buffered events with id > N are replayed immediately after the connected event.
Browser EventSource handles Last-Event-ID automatically.
MIT