@veloxts/events 0.6.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GUIDE.md +199 -0
- package/LICENSE +21 -0
- package/README.md +32 -0
- package/dist/auth.d.ts +51 -0
- package/dist/auth.js +71 -0
- package/dist/drivers/sse.d.ts +42 -0
- package/dist/drivers/sse.js +284 -0
- package/dist/drivers/ws.d.ts +41 -0
- package/dist/drivers/ws.js +401 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +49 -0
- package/dist/manager.d.ts +54 -0
- package/dist/manager.js +104 -0
- package/dist/plugin.d.ts +105 -0
- package/dist/plugin.js +242 -0
- package/dist/schemas.d.ts +178 -0
- package/dist/schemas.js +101 -0
- package/dist/types.d.ts +265 -0
- package/dist/types.js +7 -0
- package/package.json +83 -0
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events Plugin
|
|
3
|
+
*
|
|
4
|
+
* Fastify plugin for real-time event broadcasting.
|
|
5
|
+
* Extends BaseContext with events manager access.
|
|
6
|
+
*/
|
|
7
|
+
import type { FastifyInstance } from 'fastify';
|
|
8
|
+
import '@veloxts/core';
|
|
9
|
+
import type { EventsManager, EventsPluginOptions } from './types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Extend BaseContext with events manager.
|
|
12
|
+
*
|
|
13
|
+
* After registering the events plugin, `ctx.events` becomes available
|
|
14
|
+
* in all procedures:
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const createOrder = procedure
|
|
19
|
+
* .input(CreateOrderSchema)
|
|
20
|
+
* .mutation(async ({ input, ctx }) => {
|
|
21
|
+
* const order = await ctx.db.order.create({ data: input });
|
|
22
|
+
*
|
|
23
|
+
* // Broadcast order creation
|
|
24
|
+
* await ctx.events.broadcast(`orders.${order.userId}`, 'order.created', order);
|
|
25
|
+
*
|
|
26
|
+
* return order;
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
declare module '@veloxts/core' {
|
|
31
|
+
interface BaseContext {
|
|
32
|
+
/** Events manager for real-time broadcasting */
|
|
33
|
+
events: EventsManager;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Events plugin for Fastify.
|
|
38
|
+
*
|
|
39
|
+
* Registers an events manager and sets up WebSocket/SSE endpoints.
|
|
40
|
+
*
|
|
41
|
+
* @param options - Events plugin options (driver configuration)
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* import { eventsPlugin } from '@veloxts/events';
|
|
46
|
+
*
|
|
47
|
+
* // WebSocket driver (recommended)
|
|
48
|
+
* app.register(eventsPlugin, {
|
|
49
|
+
* driver: 'ws',
|
|
50
|
+
* path: '/ws',
|
|
51
|
+
* redis: process.env.REDIS_URL, // For horizontal scaling
|
|
52
|
+
* authorizer: async (channel, request) => {
|
|
53
|
+
* if (channel.type === 'public') return { authorized: true };
|
|
54
|
+
* const user = await getUserFromRequest(request);
|
|
55
|
+
* if (!user) return { authorized: false, error: 'Not authenticated' };
|
|
56
|
+
* return { authorized: true, member: { id: user.id, info: { name: user.name } } };
|
|
57
|
+
* },
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* // SSE driver (fallback for environments without WebSocket)
|
|
61
|
+
* app.register(eventsPlugin, {
|
|
62
|
+
* driver: 'sse',
|
|
63
|
+
* path: '/events',
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export declare function eventsPlugin(options?: EventsPluginOptions): (fastify: FastifyInstance) => Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Get the events manager from a Fastify instance.
|
|
70
|
+
*
|
|
71
|
+
* @param fastify - Fastify instance
|
|
72
|
+
* @returns Events manager
|
|
73
|
+
* @throws If events plugin is not registered
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const events = getEventsFromInstance(fastify);
|
|
78
|
+
* await events.broadcast('notifications', 'alert', { message: 'Hello!' });
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export declare function getEventsFromInstance(fastify: FastifyInstance): EventsManager;
|
|
82
|
+
/**
|
|
83
|
+
* Get or create a standalone events manager.
|
|
84
|
+
*
|
|
85
|
+
* This is useful when you need events access outside of a Fastify request
|
|
86
|
+
* context, such as in background jobs or CLI commands.
|
|
87
|
+
*
|
|
88
|
+
* @param options - Events options (only used on first call)
|
|
89
|
+
* @returns Events manager instance
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* import { getEvents } from '@veloxts/events';
|
|
94
|
+
*
|
|
95
|
+
* // In a background job
|
|
96
|
+
* const events = await getEvents({ driver: 'ws' });
|
|
97
|
+
* await events.broadcast('jobs', 'completed', { jobId: '123' });
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export declare function getEvents(options?: EventsPluginOptions): Promise<EventsManager>;
|
|
101
|
+
/**
|
|
102
|
+
* Reset the standalone events instance.
|
|
103
|
+
* Primarily used for testing.
|
|
104
|
+
*/
|
|
105
|
+
export declare function _resetStandaloneEvents(): Promise<void>;
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events Plugin
|
|
3
|
+
*
|
|
4
|
+
* Fastify plugin for real-time event broadcasting.
|
|
5
|
+
* Extends BaseContext with events manager access.
|
|
6
|
+
*/
|
|
7
|
+
import fp from 'fastify-plugin';
|
|
8
|
+
// Side-effect import for declaration merging
|
|
9
|
+
import '@veloxts/core';
|
|
10
|
+
import { createChannelAuthSigner } from './auth.js';
|
|
11
|
+
import { createSseDriver } from './drivers/sse.js';
|
|
12
|
+
import { createWsDriver } from './drivers/ws.js';
|
|
13
|
+
import { createManagerFromDriver } from './manager.js';
|
|
14
|
+
import { formatValidationErrors, SseSubscribeBodySchema, SseUnsubscribeBodySchema, validateBody, WsAuthBodySchema, } from './schemas.js';
|
|
15
|
+
/**
|
|
16
|
+
* Symbol for accessing events manager from Fastify instance.
|
|
17
|
+
*/
|
|
18
|
+
const EVENTS_KEY = Symbol.for('velox.events');
|
|
19
|
+
/**
|
|
20
|
+
* Default channel authorizer that allows public channels and denies private/presence.
|
|
21
|
+
*/
|
|
22
|
+
const defaultAuthorizer = (channel) => {
|
|
23
|
+
// Public channels (no prefix) are allowed
|
|
24
|
+
if (!channel.name.startsWith('private-') && !channel.name.startsWith('presence-')) {
|
|
25
|
+
return { authorized: true };
|
|
26
|
+
}
|
|
27
|
+
// Private/presence channels require explicit authorization
|
|
28
|
+
return { authorized: false, error: 'Authentication required' };
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Events plugin for Fastify.
|
|
32
|
+
*
|
|
33
|
+
* Registers an events manager and sets up WebSocket/SSE endpoints.
|
|
34
|
+
*
|
|
35
|
+
* @param options - Events plugin options (driver configuration)
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* import { eventsPlugin } from '@veloxts/events';
|
|
40
|
+
*
|
|
41
|
+
* // WebSocket driver (recommended)
|
|
42
|
+
* app.register(eventsPlugin, {
|
|
43
|
+
* driver: 'ws',
|
|
44
|
+
* path: '/ws',
|
|
45
|
+
* redis: process.env.REDIS_URL, // For horizontal scaling
|
|
46
|
+
* authorizer: async (channel, request) => {
|
|
47
|
+
* if (channel.type === 'public') return { authorized: true };
|
|
48
|
+
* const user = await getUserFromRequest(request);
|
|
49
|
+
* if (!user) return { authorized: false, error: 'Not authenticated' };
|
|
50
|
+
* return { authorized: true, member: { id: user.id, info: { name: user.name } } };
|
|
51
|
+
* },
|
|
52
|
+
* });
|
|
53
|
+
*
|
|
54
|
+
* // SSE driver (fallback for environments without WebSocket)
|
|
55
|
+
* app.register(eventsPlugin, {
|
|
56
|
+
* driver: 'sse',
|
|
57
|
+
* path: '/events',
|
|
58
|
+
* });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function eventsPlugin(options = {}) {
|
|
62
|
+
return fp(async (fastify) => {
|
|
63
|
+
const authorizer = options.authorizer ?? defaultAuthorizer;
|
|
64
|
+
let driver;
|
|
65
|
+
if (options.driver === 'sse') {
|
|
66
|
+
const sseOptions = options;
|
|
67
|
+
const sse = createSseDriver(sseOptions);
|
|
68
|
+
driver = sse;
|
|
69
|
+
// Register SSE endpoint
|
|
70
|
+
const path = sseOptions.path ?? '/events';
|
|
71
|
+
fastify.get(path, async (request, reply) => {
|
|
72
|
+
await sse.handler(request, reply);
|
|
73
|
+
});
|
|
74
|
+
// Register subscription endpoint with Zod validation
|
|
75
|
+
fastify.post(`${path}/subscribe`, async (request, reply) => {
|
|
76
|
+
const validation = validateBody(request.body, SseSubscribeBodySchema);
|
|
77
|
+
if (!validation.success) {
|
|
78
|
+
return reply.status(400).send(formatValidationErrors(validation.errors));
|
|
79
|
+
}
|
|
80
|
+
const { connectionId, channel, member } = validation.data;
|
|
81
|
+
// Authorize the channel
|
|
82
|
+
const channelInfo = parseChannel(channel);
|
|
83
|
+
const authResult = await authorizer(channelInfo, request);
|
|
84
|
+
if (!authResult.authorized) {
|
|
85
|
+
return reply.status(403).send({ error: authResult.error ?? 'Unauthorized' });
|
|
86
|
+
}
|
|
87
|
+
sse.subscribe(connectionId, channel, authResult.member ?? member);
|
|
88
|
+
return { success: true };
|
|
89
|
+
});
|
|
90
|
+
// Register unsubscribe endpoint with Zod validation
|
|
91
|
+
fastify.post(`${path}/unsubscribe`, async (request, reply) => {
|
|
92
|
+
const validation = validateBody(request.body, SseUnsubscribeBodySchema);
|
|
93
|
+
if (!validation.success) {
|
|
94
|
+
return reply.status(400).send(formatValidationErrors(validation.errors));
|
|
95
|
+
}
|
|
96
|
+
const { connectionId, channel } = validation.data;
|
|
97
|
+
sse.unsubscribe(connectionId, channel);
|
|
98
|
+
return { success: true };
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// WebSocket driver (default)
|
|
103
|
+
const wsOptions = (options.driver === 'ws' ? options : { ...options, driver: 'ws' });
|
|
104
|
+
const ws = await createWsDriver(wsOptions, { server: fastify.server });
|
|
105
|
+
driver = ws;
|
|
106
|
+
// Initialize auth signer if authSecret is provided
|
|
107
|
+
let authSigner = null;
|
|
108
|
+
if (options.authSecret) {
|
|
109
|
+
authSigner = createChannelAuthSigner(options.authSecret);
|
|
110
|
+
}
|
|
111
|
+
// Handle WebSocket upgrade
|
|
112
|
+
const path = wsOptions.path ?? '/ws';
|
|
113
|
+
fastify.server.on('upgrade', (request, socket, head) => {
|
|
114
|
+
const url = new URL(request.url ?? '', `http://${request.headers.host}`);
|
|
115
|
+
if (url.pathname === path) {
|
|
116
|
+
ws.handleUpgrade(request, socket, head);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
// Register auth endpoint for private/presence channels with Zod validation
|
|
120
|
+
fastify.post(`${path}/auth`, async (request, reply) => {
|
|
121
|
+
const validation = validateBody(request.body, WsAuthBodySchema);
|
|
122
|
+
if (!validation.success) {
|
|
123
|
+
return reply.status(400).send(formatValidationErrors(validation.errors));
|
|
124
|
+
}
|
|
125
|
+
const { socketId, channel } = validation.data;
|
|
126
|
+
const channelInfo = parseChannel(channel);
|
|
127
|
+
const authResult = await authorizer(channelInfo, request);
|
|
128
|
+
if (!authResult.authorized) {
|
|
129
|
+
return reply.status(403).send({ error: authResult.error ?? 'Unauthorized' });
|
|
130
|
+
}
|
|
131
|
+
// Require authSecret for private/presence channels
|
|
132
|
+
if (!authSigner && channelInfo.type !== 'public') {
|
|
133
|
+
return reply.status(500).send({
|
|
134
|
+
error: 'Server configuration error: authSecret required for private channels',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
// For public channels, no signature needed
|
|
138
|
+
if (channelInfo.type === 'public') {
|
|
139
|
+
return { auth: null };
|
|
140
|
+
}
|
|
141
|
+
// Generate HMAC-SHA256 signature for secure auth
|
|
142
|
+
const channelData = authResult.member ? JSON.stringify(authResult.member) : undefined;
|
|
143
|
+
const signature = authSigner?.sign(socketId, channel, channelData) ?? '';
|
|
144
|
+
return {
|
|
145
|
+
auth: `${socketId}:${signature}`,
|
|
146
|
+
channel_data: channelData,
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
// Create events manager
|
|
151
|
+
const events = createManagerFromDriver(driver);
|
|
152
|
+
// Store on fastify instance
|
|
153
|
+
fastify[EVENTS_KEY] = events;
|
|
154
|
+
// Decorate request with events accessor
|
|
155
|
+
fastify.decorateRequest('events', {
|
|
156
|
+
getter() {
|
|
157
|
+
return events;
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
// Register cleanup hook
|
|
161
|
+
fastify.addHook('onClose', async () => {
|
|
162
|
+
await events.close();
|
|
163
|
+
});
|
|
164
|
+
}, {
|
|
165
|
+
name: '@veloxts/events',
|
|
166
|
+
fastify: '5.x',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Parse channel name to determine type.
|
|
171
|
+
*/
|
|
172
|
+
function parseChannel(name) {
|
|
173
|
+
if (name.startsWith('presence-')) {
|
|
174
|
+
return { name, type: 'presence' };
|
|
175
|
+
}
|
|
176
|
+
if (name.startsWith('private-')) {
|
|
177
|
+
return { name, type: 'private' };
|
|
178
|
+
}
|
|
179
|
+
return { name, type: 'public' };
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Get the events manager from a Fastify instance.
|
|
183
|
+
*
|
|
184
|
+
* @param fastify - Fastify instance
|
|
185
|
+
* @returns Events manager
|
|
186
|
+
* @throws If events plugin is not registered
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```typescript
|
|
190
|
+
* const events = getEventsFromInstance(fastify);
|
|
191
|
+
* await events.broadcast('notifications', 'alert', { message: 'Hello!' });
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
export function getEventsFromInstance(fastify) {
|
|
195
|
+
const events = fastify[EVENTS_KEY];
|
|
196
|
+
if (!events) {
|
|
197
|
+
throw new Error('Events plugin not registered. Register it with: app.register(eventsPlugin, { ... })');
|
|
198
|
+
}
|
|
199
|
+
return events;
|
|
200
|
+
}
|
|
201
|
+
// =============================================================================
|
|
202
|
+
// Standalone Usage (outside Fastify context)
|
|
203
|
+
// =============================================================================
|
|
204
|
+
/**
|
|
205
|
+
* Singleton events instance for standalone usage.
|
|
206
|
+
*/
|
|
207
|
+
let standaloneEvents = null;
|
|
208
|
+
/**
|
|
209
|
+
* Get or create a standalone events manager.
|
|
210
|
+
*
|
|
211
|
+
* This is useful when you need events access outside of a Fastify request
|
|
212
|
+
* context, such as in background jobs or CLI commands.
|
|
213
|
+
*
|
|
214
|
+
* @param options - Events options (only used on first call)
|
|
215
|
+
* @returns Events manager instance
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```typescript
|
|
219
|
+
* import { getEvents } from '@veloxts/events';
|
|
220
|
+
*
|
|
221
|
+
* // In a background job
|
|
222
|
+
* const events = await getEvents({ driver: 'ws' });
|
|
223
|
+
* await events.broadcast('jobs', 'completed', { jobId: '123' });
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
export async function getEvents(options) {
|
|
227
|
+
if (!standaloneEvents) {
|
|
228
|
+
const { createEventsManager } = await import('./manager.js');
|
|
229
|
+
standaloneEvents = await createEventsManager(options ?? {});
|
|
230
|
+
}
|
|
231
|
+
return standaloneEvents;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Reset the standalone events instance.
|
|
235
|
+
* Primarily used for testing.
|
|
236
|
+
*/
|
|
237
|
+
export async function _resetStandaloneEvents() {
|
|
238
|
+
if (standaloneEvents) {
|
|
239
|
+
await standaloneEvents.close();
|
|
240
|
+
standaloneEvents = null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation Schemas
|
|
3
|
+
*
|
|
4
|
+
* Zod schemas for validating request bodies and WebSocket messages.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
/**
|
|
8
|
+
* Presence member schema for channel subscriptions.
|
|
9
|
+
*/
|
|
10
|
+
export declare const PresenceMemberSchema: z.ZodObject<{
|
|
11
|
+
id: z.ZodString;
|
|
12
|
+
info: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
id: string;
|
|
15
|
+
info?: Record<string, unknown> | undefined;
|
|
16
|
+
}, {
|
|
17
|
+
id: string;
|
|
18
|
+
info?: Record<string, unknown> | undefined;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* Schema for SSE subscribe endpoint.
|
|
22
|
+
* POST /events/subscribe
|
|
23
|
+
*/
|
|
24
|
+
export declare const SseSubscribeBodySchema: z.ZodObject<{
|
|
25
|
+
connectionId: z.ZodString;
|
|
26
|
+
channel: z.ZodString;
|
|
27
|
+
member: z.ZodOptional<z.ZodObject<{
|
|
28
|
+
id: z.ZodString;
|
|
29
|
+
info: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
30
|
+
}, "strip", z.ZodTypeAny, {
|
|
31
|
+
id: string;
|
|
32
|
+
info?: Record<string, unknown> | undefined;
|
|
33
|
+
}, {
|
|
34
|
+
id: string;
|
|
35
|
+
info?: Record<string, unknown> | undefined;
|
|
36
|
+
}>>;
|
|
37
|
+
}, "strip", z.ZodTypeAny, {
|
|
38
|
+
connectionId: string;
|
|
39
|
+
channel: string;
|
|
40
|
+
member?: {
|
|
41
|
+
id: string;
|
|
42
|
+
info?: Record<string, unknown> | undefined;
|
|
43
|
+
} | undefined;
|
|
44
|
+
}, {
|
|
45
|
+
connectionId: string;
|
|
46
|
+
channel: string;
|
|
47
|
+
member?: {
|
|
48
|
+
id: string;
|
|
49
|
+
info?: Record<string, unknown> | undefined;
|
|
50
|
+
} | undefined;
|
|
51
|
+
}>;
|
|
52
|
+
export type SseSubscribeBody = z.infer<typeof SseSubscribeBodySchema>;
|
|
53
|
+
/**
|
|
54
|
+
* Schema for SSE unsubscribe endpoint.
|
|
55
|
+
* POST /events/unsubscribe
|
|
56
|
+
*/
|
|
57
|
+
export declare const SseUnsubscribeBodySchema: z.ZodObject<{
|
|
58
|
+
connectionId: z.ZodString;
|
|
59
|
+
channel: z.ZodString;
|
|
60
|
+
}, "strip", z.ZodTypeAny, {
|
|
61
|
+
connectionId: string;
|
|
62
|
+
channel: string;
|
|
63
|
+
}, {
|
|
64
|
+
connectionId: string;
|
|
65
|
+
channel: string;
|
|
66
|
+
}>;
|
|
67
|
+
export type SseUnsubscribeBody = z.infer<typeof SseUnsubscribeBodySchema>;
|
|
68
|
+
/**
|
|
69
|
+
* Schema for WebSocket auth endpoint.
|
|
70
|
+
* POST /ws/auth
|
|
71
|
+
*/
|
|
72
|
+
export declare const WsAuthBodySchema: z.ZodObject<{
|
|
73
|
+
socketId: z.ZodString;
|
|
74
|
+
channel: z.ZodString;
|
|
75
|
+
}, "strip", z.ZodTypeAny, {
|
|
76
|
+
channel: string;
|
|
77
|
+
socketId: string;
|
|
78
|
+
}, {
|
|
79
|
+
channel: string;
|
|
80
|
+
socketId: string;
|
|
81
|
+
}>;
|
|
82
|
+
export type WsAuthBody = z.infer<typeof WsAuthBodySchema>;
|
|
83
|
+
/**
|
|
84
|
+
* Schema for WebSocket client messages.
|
|
85
|
+
* Uses discriminated union for type-safe message handling.
|
|
86
|
+
*/
|
|
87
|
+
export declare const ClientMessageSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
88
|
+
type: z.ZodLiteral<"subscribe">;
|
|
89
|
+
channel: z.ZodString;
|
|
90
|
+
auth: z.ZodOptional<z.ZodString>;
|
|
91
|
+
channelData: z.ZodOptional<z.ZodString>;
|
|
92
|
+
data: z.ZodOptional<z.ZodObject<{
|
|
93
|
+
id: z.ZodString;
|
|
94
|
+
info: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
95
|
+
}, "strip", z.ZodTypeAny, {
|
|
96
|
+
id: string;
|
|
97
|
+
info?: Record<string, unknown> | undefined;
|
|
98
|
+
}, {
|
|
99
|
+
id: string;
|
|
100
|
+
info?: Record<string, unknown> | undefined;
|
|
101
|
+
}>>;
|
|
102
|
+
}, "strip", z.ZodTypeAny, {
|
|
103
|
+
channel: string;
|
|
104
|
+
type: "subscribe";
|
|
105
|
+
auth?: string | undefined;
|
|
106
|
+
channelData?: string | undefined;
|
|
107
|
+
data?: {
|
|
108
|
+
id: string;
|
|
109
|
+
info?: Record<string, unknown> | undefined;
|
|
110
|
+
} | undefined;
|
|
111
|
+
}, {
|
|
112
|
+
channel: string;
|
|
113
|
+
type: "subscribe";
|
|
114
|
+
auth?: string | undefined;
|
|
115
|
+
channelData?: string | undefined;
|
|
116
|
+
data?: {
|
|
117
|
+
id: string;
|
|
118
|
+
info?: Record<string, unknown> | undefined;
|
|
119
|
+
} | undefined;
|
|
120
|
+
}>, z.ZodObject<{
|
|
121
|
+
type: z.ZodLiteral<"unsubscribe">;
|
|
122
|
+
channel: z.ZodString;
|
|
123
|
+
}, "strip", z.ZodTypeAny, {
|
|
124
|
+
channel: string;
|
|
125
|
+
type: "unsubscribe";
|
|
126
|
+
}, {
|
|
127
|
+
channel: string;
|
|
128
|
+
type: "unsubscribe";
|
|
129
|
+
}>, z.ZodObject<{
|
|
130
|
+
type: z.ZodLiteral<"ping">;
|
|
131
|
+
}, "strip", z.ZodTypeAny, {
|
|
132
|
+
type: "ping";
|
|
133
|
+
}, {
|
|
134
|
+
type: "ping";
|
|
135
|
+
}>, z.ZodObject<{
|
|
136
|
+
type: z.ZodLiteral<"message">;
|
|
137
|
+
channel: z.ZodString;
|
|
138
|
+
event: z.ZodString;
|
|
139
|
+
data: z.ZodOptional<z.ZodUnknown>;
|
|
140
|
+
}, "strip", z.ZodTypeAny, {
|
|
141
|
+
channel: string;
|
|
142
|
+
type: "message";
|
|
143
|
+
event: string;
|
|
144
|
+
data?: unknown;
|
|
145
|
+
}, {
|
|
146
|
+
channel: string;
|
|
147
|
+
type: "message";
|
|
148
|
+
event: string;
|
|
149
|
+
data?: unknown;
|
|
150
|
+
}>]>;
|
|
151
|
+
export type ValidatedClientMessage = z.infer<typeof ClientMessageSchema>;
|
|
152
|
+
/**
|
|
153
|
+
* Validation result type - discriminated union for type-safe error handling.
|
|
154
|
+
*/
|
|
155
|
+
export type ValidationResult<T> = {
|
|
156
|
+
success: true;
|
|
157
|
+
data: T;
|
|
158
|
+
} | {
|
|
159
|
+
success: false;
|
|
160
|
+
errors: z.ZodIssue[];
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Validate request body against a Zod schema.
|
|
164
|
+
* Returns a discriminated union for type-safe error handling.
|
|
165
|
+
*
|
|
166
|
+
* @template TSchema - The Zod schema type
|
|
167
|
+
*/
|
|
168
|
+
export declare function validateBody<TSchema extends z.ZodTypeAny>(body: unknown, schema: TSchema): ValidationResult<z.infer<TSchema>>;
|
|
169
|
+
/**
|
|
170
|
+
* Format Zod validation errors for API response.
|
|
171
|
+
*/
|
|
172
|
+
export declare function formatValidationErrors(errors: z.ZodIssue[]): {
|
|
173
|
+
error: string;
|
|
174
|
+
details: Array<{
|
|
175
|
+
path: string;
|
|
176
|
+
message: string;
|
|
177
|
+
}>;
|
|
178
|
+
};
|
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation Schemas
|
|
3
|
+
*
|
|
4
|
+
* Zod schemas for validating request bodies and WebSocket messages.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// Presence Member Schema
|
|
9
|
+
// =============================================================================
|
|
10
|
+
/**
|
|
11
|
+
* Presence member schema for channel subscriptions.
|
|
12
|
+
*/
|
|
13
|
+
export const PresenceMemberSchema = z.object({
|
|
14
|
+
id: z.string().min(1, 'Member ID is required'),
|
|
15
|
+
info: z.record(z.unknown()).optional(),
|
|
16
|
+
});
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// SSE Endpoint Schemas
|
|
19
|
+
// =============================================================================
|
|
20
|
+
/**
|
|
21
|
+
* Schema for SSE subscribe endpoint.
|
|
22
|
+
* POST /events/subscribe
|
|
23
|
+
*/
|
|
24
|
+
export const SseSubscribeBodySchema = z.object({
|
|
25
|
+
connectionId: z.string().min(1, 'Connection ID is required'),
|
|
26
|
+
channel: z.string().min(1, 'Channel name is required'),
|
|
27
|
+
member: PresenceMemberSchema.optional(),
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* Schema for SSE unsubscribe endpoint.
|
|
31
|
+
* POST /events/unsubscribe
|
|
32
|
+
*/
|
|
33
|
+
export const SseUnsubscribeBodySchema = z.object({
|
|
34
|
+
connectionId: z.string().min(1, 'Connection ID is required'),
|
|
35
|
+
channel: z.string().min(1, 'Channel name is required'),
|
|
36
|
+
});
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// WebSocket Endpoint Schemas
|
|
39
|
+
// =============================================================================
|
|
40
|
+
/**
|
|
41
|
+
* Schema for WebSocket auth endpoint.
|
|
42
|
+
* POST /ws/auth
|
|
43
|
+
*/
|
|
44
|
+
export const WsAuthBodySchema = z.object({
|
|
45
|
+
socketId: z.string().min(1, 'Socket ID is required'),
|
|
46
|
+
channel: z.string().min(1, 'Channel name is required'),
|
|
47
|
+
});
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// WebSocket Client Message Schemas
|
|
50
|
+
// =============================================================================
|
|
51
|
+
/**
|
|
52
|
+
* Schema for WebSocket client messages.
|
|
53
|
+
* Uses discriminated union for type-safe message handling.
|
|
54
|
+
*/
|
|
55
|
+
export const ClientMessageSchema = z.discriminatedUnion('type', [
|
|
56
|
+
z.object({
|
|
57
|
+
type: z.literal('subscribe'),
|
|
58
|
+
channel: z.string().min(1),
|
|
59
|
+
auth: z.string().optional(),
|
|
60
|
+
channelData: z.string().optional(),
|
|
61
|
+
data: PresenceMemberSchema.optional(),
|
|
62
|
+
}),
|
|
63
|
+
z.object({
|
|
64
|
+
type: z.literal('unsubscribe'),
|
|
65
|
+
channel: z.string().min(1),
|
|
66
|
+
}),
|
|
67
|
+
z.object({
|
|
68
|
+
type: z.literal('ping'),
|
|
69
|
+
}),
|
|
70
|
+
z.object({
|
|
71
|
+
type: z.literal('message'),
|
|
72
|
+
channel: z.string().min(1),
|
|
73
|
+
event: z.string().min(1),
|
|
74
|
+
data: z.unknown().optional(),
|
|
75
|
+
}),
|
|
76
|
+
]);
|
|
77
|
+
/**
|
|
78
|
+
* Validate request body against a Zod schema.
|
|
79
|
+
* Returns a discriminated union for type-safe error handling.
|
|
80
|
+
*
|
|
81
|
+
* @template TSchema - The Zod schema type
|
|
82
|
+
*/
|
|
83
|
+
export function validateBody(body, schema) {
|
|
84
|
+
const result = schema.safeParse(body);
|
|
85
|
+
if (result.success) {
|
|
86
|
+
return { success: true, data: result.data };
|
|
87
|
+
}
|
|
88
|
+
return { success: false, errors: result.error.issues };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Format Zod validation errors for API response.
|
|
92
|
+
*/
|
|
93
|
+
export function formatValidationErrors(errors) {
|
|
94
|
+
return {
|
|
95
|
+
error: 'Validation failed',
|
|
96
|
+
details: errors.map((issue) => ({
|
|
97
|
+
path: issue.path.join('.'),
|
|
98
|
+
message: issue.message,
|
|
99
|
+
})),
|
|
100
|
+
};
|
|
101
|
+
}
|