@pluv/platform-node 0.0.0-experimental-20250527040415
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/.turbo/turbo-build.log +19 -0
- package/.turbo/turbo-lint.log +5 -0
- package/CHANGELOG.md +1623 -0
- package/LICENSE +21 -0
- package/README.md +18 -0
- package/dist/index.d.mts +134 -0
- package/dist/index.d.ts +134 -0
- package/dist/index.js +370 -0
- package/dist/index.mjs +334 -0
- package/eslint.config.mjs +4 -0
- package/package.json +43 -0
- package/src/NodePlatform.ts +144 -0
- package/src/NodeWebSocket.ts +144 -0
- package/src/createPluvHandler.ts +202 -0
- package/src/index.ts +12 -0
- package/src/infer.ts +9 -0
- package/src/platformNode.ts +50 -0
- package/src/utils/identity.ts +1 -0
- package/src/utils/index.ts +1 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { InferInitContextType, PluvServer } from "@pluv/io";
|
|
2
|
+
import type {
|
|
3
|
+
BaseUser,
|
|
4
|
+
InferIOAuthorize,
|
|
5
|
+
InferIOAuthorizeUser,
|
|
6
|
+
Maybe,
|
|
7
|
+
MaybePromise,
|
|
8
|
+
} from "@pluv/types";
|
|
9
|
+
import type { Server as HttpServer, IncomingMessage, ServerResponse } from "node:http";
|
|
10
|
+
import Url from "node:url";
|
|
11
|
+
import { match } from "path-to-regexp";
|
|
12
|
+
import type { WebSocket } from "ws";
|
|
13
|
+
import { WebSocketServer } from "ws";
|
|
14
|
+
import { NodePlatform } from "./NodePlatform";
|
|
15
|
+
|
|
16
|
+
export type AuthorizeFunctionContext<TPluvServer extends PluvServer<any, any, any, any>> = {
|
|
17
|
+
room: string;
|
|
18
|
+
} & InferInitContextType<
|
|
19
|
+
TPluvServer extends PluvServer<infer IPlatform, any, any, any> ? IPlatform : never
|
|
20
|
+
>;
|
|
21
|
+
|
|
22
|
+
export type AuthorizeFunction<TPluvServer extends PluvServer<any, any, any, any>> = (
|
|
23
|
+
ctx: AuthorizeFunctionContext<TPluvServer>,
|
|
24
|
+
) => MaybePromise<Maybe<InferIOAuthorizeUser<InferIOAuthorize<TPluvServer>>>>;
|
|
25
|
+
|
|
26
|
+
export type CreatePluvHandlerConfig<TPluvServer extends PluvServer<any, any, any, any>> = {
|
|
27
|
+
endpoint?: string;
|
|
28
|
+
io: TPluvServer;
|
|
29
|
+
server: HttpServer;
|
|
30
|
+
} & (InferIOAuthorizeUser<InferIOAuthorize<TPluvServer>> extends BaseUser
|
|
31
|
+
? { authorize: AuthorizeFunction<TPluvServer> }
|
|
32
|
+
: { authorize?: undefined });
|
|
33
|
+
|
|
34
|
+
const handle = (ws: WebSocket) => ({
|
|
35
|
+
invalidEndpoint: () => {
|
|
36
|
+
ws.close(1003, "Invalid WebSocket endpoint");
|
|
37
|
+
|
|
38
|
+
return { matched: false };
|
|
39
|
+
},
|
|
40
|
+
invalidRoom: () => {
|
|
41
|
+
ws.close(1003, "Invalid room name");
|
|
42
|
+
|
|
43
|
+
return { matched: true };
|
|
44
|
+
},
|
|
45
|
+
invalidToken: () => {
|
|
46
|
+
ws.close(1003, "Invalid authentication token");
|
|
47
|
+
|
|
48
|
+
return { matched: true };
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export type RequestHandler = (req: IncomingMessage, res: ServerResponse, next?: () => void) => void;
|
|
53
|
+
|
|
54
|
+
export interface WebSocketHandlerResult {
|
|
55
|
+
matched: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type WebSocketHandler = (
|
|
59
|
+
ws: WebSocket,
|
|
60
|
+
req: IncomingMessage,
|
|
61
|
+
) => Promise<WebSocketHandlerResult>;
|
|
62
|
+
|
|
63
|
+
export interface CreatePluvHandlerResult {
|
|
64
|
+
createWsServer: () => WebSocketServer;
|
|
65
|
+
handler: RequestHandler;
|
|
66
|
+
wsHandler: WebSocketHandler;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @deprecated Instructions will be provided on https://pluv.io on how to host this yourself instead.
|
|
71
|
+
* @date April 27, 2025
|
|
72
|
+
*/
|
|
73
|
+
export const createPluvHandler = <TPluvServer extends PluvServer<NodePlatform<any>, any, any, any>>(
|
|
74
|
+
config: CreatePluvHandlerConfig<TPluvServer>,
|
|
75
|
+
): CreatePluvHandlerResult => {
|
|
76
|
+
const { authorize, endpoint = "/api/pluv", io, server } = config;
|
|
77
|
+
|
|
78
|
+
if (io._defs.platform._config.registrationMode === "detached") {
|
|
79
|
+
throw new Error("Cannot use createPluvHandler in detached mode for Node.js");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const wsServer = new WebSocketServer({ server });
|
|
83
|
+
const rooms = new Map<string, ReturnType<typeof io.createRoom>>();
|
|
84
|
+
|
|
85
|
+
const getRoom = (roomId: string): ReturnType<typeof io.createRoom> => {
|
|
86
|
+
Array.from(rooms.values()).forEach((room) => {
|
|
87
|
+
if (!room.getSize()) rooms.delete(room.id);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const existing = rooms.get(roomId);
|
|
91
|
+
|
|
92
|
+
if (existing) return existing;
|
|
93
|
+
|
|
94
|
+
const newRoom = io.createRoom(roomId, {
|
|
95
|
+
onDestroy: (event) => {
|
|
96
|
+
rooms.delete(event.room);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
rooms.set(roomId, newRoom);
|
|
101
|
+
|
|
102
|
+
return newRoom;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const wsHandler = async (
|
|
106
|
+
ws: WebSocket,
|
|
107
|
+
req: IncomingMessage,
|
|
108
|
+
): Promise<WebSocketHandlerResult> => {
|
|
109
|
+
const url = req.url;
|
|
110
|
+
const onError = handle(ws);
|
|
111
|
+
|
|
112
|
+
if (!url) {
|
|
113
|
+
onError.invalidEndpoint();
|
|
114
|
+
|
|
115
|
+
return { matched: false };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const parsed = Url.parse(url, true);
|
|
119
|
+
const { pathname } = parsed;
|
|
120
|
+
|
|
121
|
+
if (!pathname) return onError.invalidEndpoint();
|
|
122
|
+
|
|
123
|
+
const matcher = match<{ roomId: string }>(`${endpoint}/room/:roomId`);
|
|
124
|
+
const matched = matcher(pathname);
|
|
125
|
+
|
|
126
|
+
if (!matched) return onError.invalidEndpoint();
|
|
127
|
+
|
|
128
|
+
const { roomId } = matched.params;
|
|
129
|
+
|
|
130
|
+
if (!roomId) return onError.invalidRoom();
|
|
131
|
+
|
|
132
|
+
const token = parsed.query?.token || undefined;
|
|
133
|
+
|
|
134
|
+
if (Array.isArray(token)) return onError.invalidToken();
|
|
135
|
+
|
|
136
|
+
const room = getRoom(roomId);
|
|
137
|
+
|
|
138
|
+
await room.register(ws, { req, token });
|
|
139
|
+
|
|
140
|
+
return { matched: true };
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const createWsServer = () => {
|
|
144
|
+
return wsServer.on("connection", async (ws, req) => {
|
|
145
|
+
await wsHandler(ws, req);
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const handler = async (
|
|
150
|
+
req: IncomingMessage,
|
|
151
|
+
res: ServerResponse,
|
|
152
|
+
next?: () => void,
|
|
153
|
+
): Promise<ServerResponse | null> => {
|
|
154
|
+
if (!authorize) return next?.() ?? null;
|
|
155
|
+
if (req.method !== "GET") return next?.() ?? null;
|
|
156
|
+
|
|
157
|
+
const url = req.url;
|
|
158
|
+
|
|
159
|
+
if (!url) return next?.() ?? null;
|
|
160
|
+
|
|
161
|
+
const parsed = Url.parse(url, true);
|
|
162
|
+
const { pathname } = parsed;
|
|
163
|
+
|
|
164
|
+
if (!pathname) return next?.() ?? null;
|
|
165
|
+
|
|
166
|
+
const matcher = match<{}>(`${endpoint}/authorize`);
|
|
167
|
+
const matched = matcher(pathname);
|
|
168
|
+
|
|
169
|
+
if (!matched) return next?.() ?? null;
|
|
170
|
+
|
|
171
|
+
const room = parsed.query?.room || undefined;
|
|
172
|
+
|
|
173
|
+
if (typeof room !== "string" || !room) {
|
|
174
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
175
|
+
return res.end("Room does not exist");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const user = await authorize({ req, room } as AuthorizeFunctionContext<TPluvServer>);
|
|
180
|
+
|
|
181
|
+
if (!user) throw new Error();
|
|
182
|
+
|
|
183
|
+
const token = await io.createToken({
|
|
184
|
+
req,
|
|
185
|
+
room,
|
|
186
|
+
user: user as any,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
190
|
+
return res.end(token);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
193
|
+
return res.end(err instanceof Error ? err.message : "Unauthorized");
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
createWsServer,
|
|
199
|
+
handler,
|
|
200
|
+
wsHandler,
|
|
201
|
+
};
|
|
202
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type { WebSocket } from "ws";
|
|
2
|
+
export { createPluvHandler } from "./createPluvHandler";
|
|
3
|
+
export type {
|
|
4
|
+
AuthorizeFunction,
|
|
5
|
+
AuthorizeFunctionContext,
|
|
6
|
+
CreatePluvHandlerConfig,
|
|
7
|
+
} from "./createPluvHandler";
|
|
8
|
+
export { infer } from "./infer";
|
|
9
|
+
export type { InferCallback } from "./infer";
|
|
10
|
+
export type { NodePlatform, NodePlatformConfig } from "./NodePlatform";
|
|
11
|
+
export { platformNode } from "./platformNode";
|
|
12
|
+
export type { PlatformNodeCreateIOParams } from "./platformNode";
|
package/src/infer.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Json } from "@pluv/types";
|
|
2
|
+
import type { identity } from "./utils";
|
|
3
|
+
|
|
4
|
+
export type InferCallback<TMeta extends Record<string, Json> = {}> = (i: typeof identity) => {
|
|
5
|
+
meta?: (io: TMeta) => TMeta;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const infer = <TMeta extends Record<string, Json> = {}>(callback: InferCallback<TMeta>) =>
|
|
9
|
+
callback;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreateIOParams,
|
|
3
|
+
InferInitContextType,
|
|
4
|
+
PluvContext,
|
|
5
|
+
PluvIOAuthorize,
|
|
6
|
+
PluvIOLimits,
|
|
7
|
+
} from "@pluv/io";
|
|
8
|
+
import type { BaseUser, Id, IOAuthorize, Json } from "@pluv/types";
|
|
9
|
+
import type { NodePlatformConfig } from "./NodePlatform";
|
|
10
|
+
import { NodePlatform } from "./NodePlatform";
|
|
11
|
+
import type { InferCallback } from "./infer";
|
|
12
|
+
|
|
13
|
+
export type PlatformNodeCreateIOParams<
|
|
14
|
+
TMeta extends Record<string, Json> = {},
|
|
15
|
+
TContext extends Record<string, any> = {},
|
|
16
|
+
TUser extends BaseUser | null = null,
|
|
17
|
+
> = Id<
|
|
18
|
+
NodePlatformConfig<TMeta> &
|
|
19
|
+
Omit<
|
|
20
|
+
CreateIOParams<NodePlatform<IOAuthorize<TUser, TContext>, TMeta>, TContext, TUser>,
|
|
21
|
+
"authorize" | "context" | "platform"
|
|
22
|
+
> & {
|
|
23
|
+
authorize?: PluvIOAuthorize<
|
|
24
|
+
NodePlatform<IOAuthorize<TUser, TContext>, TMeta>,
|
|
25
|
+
TUser,
|
|
26
|
+
InferInitContextType<NodePlatform<IOAuthorize<TUser, TContext>, TMeta>>
|
|
27
|
+
>;
|
|
28
|
+
context?: PluvContext<NodePlatform<IOAuthorize<TUser, any>, TMeta>, TContext>;
|
|
29
|
+
types?: InferCallback<TMeta>;
|
|
30
|
+
}
|
|
31
|
+
>;
|
|
32
|
+
|
|
33
|
+
export const platformNode = <
|
|
34
|
+
TMeta extends Record<string, Json> = {},
|
|
35
|
+
TContext extends Record<string, any> = {},
|
|
36
|
+
TUser extends BaseUser | null = null,
|
|
37
|
+
>(
|
|
38
|
+
config: PlatformNodeCreateIOParams<TMeta, TContext, TUser> = {},
|
|
39
|
+
): CreateIOParams<NodePlatform<IOAuthorize<TUser, TContext>, TMeta>, TContext, TUser> => {
|
|
40
|
+
const { authorize, context, crdt, debug, limits } = config;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
authorize,
|
|
44
|
+
context,
|
|
45
|
+
crdt,
|
|
46
|
+
debug,
|
|
47
|
+
limits,
|
|
48
|
+
platform: () => new NodePlatform<IOAuthorize<TUser, TContext>, TMeta>(config),
|
|
49
|
+
};
|
|
50
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const identity = <T extends unknown>(x: T): T => x;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { identity } from "./identity";
|