@pluv/platform-pluv 0.33.0 → 0.34.0

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.
@@ -1,14 +1,125 @@
1
- import {
2
- AbstractPlatform,
1
+ import type {
3
2
  AbstractPlatformConfig,
4
3
  AbstractWebSocket,
5
4
  ConvertWebSocketConfig,
6
- WebSocketRegistrationMode,
5
+ GetInitialStorageFn,
6
+ JWTEncodeParams,
7
7
  WebSocketSerializedState,
8
8
  } from "@pluv/io";
9
+ import { AbstractPlatform, authorize } from "@pluv/io";
10
+ import { Hono } from "hono";
11
+ import { SIGNATURE_HEADER } from "./constants";
12
+ import { ZodEvent } from "./schemas";
13
+ import { verifyWebhook } from "./shared";
14
+ import type { PluvIOEndpoints, PluvIOListeners } from "./types";
9
15
 
10
- export class PluvPlatform extends AbstractPlatform {
11
- _registrationMode: WebSocketRegistrationMode = "attached";
16
+ export interface PluvPlatformConfig {
17
+ /**
18
+ * @ignore
19
+ * @readonly
20
+ * @deprecated Internal use only. Changes to this will never be marked as breaking.
21
+ */
22
+ _defs?: {
23
+ endpoints?: PluvIOEndpoints;
24
+ };
25
+ basePath: string;
26
+ publicKey: string;
27
+ secretKey: string;
28
+ webhookSecret?: string;
29
+ }
30
+
31
+ export class PluvPlatform extends AbstractPlatform<
32
+ any,
33
+ {},
34
+ {},
35
+ {
36
+ authorize: {
37
+ required: false;
38
+ secret: false;
39
+ };
40
+ handleMode: "fetch";
41
+ registrationMode: "attached";
42
+ requireAuth: true;
43
+ listeners: {
44
+ onRoomDeleted: true;
45
+ onRoomMessage: false;
46
+ onStorageUpdated: false;
47
+ onUserConnected: true;
48
+ onUserDisconnected: true;
49
+ };
50
+ }
51
+ > {
52
+ public readonly _config = {
53
+ authorize: {
54
+ required: false as const,
55
+ secret: false as const,
56
+ },
57
+ handleMode: "fetch" as const,
58
+ registrationMode: "attached" as const,
59
+ requireAuth: true as const,
60
+ listeners: {
61
+ onRoomDeleted: true as const,
62
+ onRoomMessage: false as const,
63
+ onStorageUpdated: false as const,
64
+ onUserConnected: true as const,
65
+ onUserDisconnected: true as const,
66
+ },
67
+ };
68
+ public readonly _name = "platformPluv";
69
+
70
+ private _authorize: any;
71
+ private readonly _basePath: string;
72
+ private readonly _endpoints: PluvIOEndpoints;
73
+ private _getInitialStorage?: GetInitialStorageFn<{}>;
74
+ private _listeners?: PluvIOListeners;
75
+ private readonly _publicKey: string;
76
+ private readonly _secretKey: string;
77
+ private readonly _webhookSecret?: string;
78
+
79
+ public _createToken = async (params: JWTEncodeParams<any, any>): Promise<string> => {
80
+ const parsed = this._authorize.user.parse(params.user);
81
+
82
+ const res = await fetch(this._endpoints.createToken, {
83
+ headers: { "content-type": "application/json" },
84
+ method: "post",
85
+ body: JSON.stringify({
86
+ maxAge: params.maxAge ?? null,
87
+ publicKey: this._publicKey,
88
+ room: params.room,
89
+ secretKey: this._secretKey,
90
+ user: parsed,
91
+ }),
92
+ }).catch(() => null);
93
+
94
+ if (!res || !res.ok || res.status !== 200) {
95
+ throw new Error("Authorization failed");
96
+ }
97
+
98
+ const token = await res.text().catch(() => null);
99
+
100
+ if (typeof token !== "string") throw new Error("Authorization failed");
101
+
102
+ return token;
103
+ };
104
+
105
+ constructor(params: PluvPlatformConfig) {
106
+ super();
107
+
108
+ const { _defs, basePath, publicKey, secretKey, webhookSecret } = params;
109
+
110
+ this._basePath = basePath;
111
+ this._endpoints = {
112
+ createToken: "https://pluv.io/api/room/token",
113
+ ..._defs?.endpoints,
114
+ };
115
+ this._publicKey = publicKey;
116
+ this._secretKey = secretKey;
117
+ this._webhookSecret = webhookSecret;
118
+
119
+ this._fetch = new Hono().basePath(this._basePath).route("/", this._webhooks).fetch as (
120
+ req: any,
121
+ ) => Promise<any>;
122
+ }
12
123
 
13
124
  public acceptWebSocket(webSocket: AbstractWebSocket): Promise<void> {
14
125
  throw new Error("Not implemented");
@@ -49,4 +160,85 @@ export class PluvPlatform extends AbstractPlatform {
49
160
  public setSerializedState(webSocket: AbstractWebSocket, state: WebSocketSerializedState): void {
50
161
  throw new Error("Not implemented");
51
162
  }
163
+
164
+ public validateConfig(config: any): void {
165
+ if (!config.authorize) throw new Error("Config `authorize` must be provided to `platformPluv`");
166
+ if (!!config.onRoomMessage) throw new Error("Config `onRoomMessage` is not supported on `platformPluv`");
167
+ if (!!config.onStorageUpdated) throw new Error("Config `onStorageUpdated` is not supported on `platformPluv`");
168
+ if (!!config.authorize.required) {
169
+ throw new Error("Config `authorize.required` is not allowed to be false on `platformPluv`");
170
+ }
171
+
172
+ /**
173
+ * !HACK
174
+ * @description Assign these on the validation step, because we know this will happen on the
175
+ * server constructor internally.
176
+ * @date December 16, 2024
177
+ */
178
+ this._authorize = config.authorize;
179
+ this._getInitialStorage = config.getInitialStorage;
180
+ this._listeners = {
181
+ onRoomDeleted: config.onRoomDeleted,
182
+ onUserConnected: config.onUserConnected,
183
+ onUserDisconnected: config.onUserDisconnected,
184
+ };
185
+ }
186
+
187
+ private _webhooks = new Hono().basePath("/").post("/", async (c) => {
188
+ const signature = c.req.header(SIGNATURE_HEADER);
189
+
190
+ if (!this._webhookSecret || !signature) return c.json({ error: "Unauthorized" }, 401);
191
+
192
+ const payload = await c.req.json();
193
+
194
+ const verified = await verifyWebhook({
195
+ payload,
196
+ signature,
197
+ secret: this._webhookSecret,
198
+ });
199
+
200
+ if (!verified) return c.json({ error: "Unauthorized" }, 401);
201
+
202
+ const parsed = ZodEvent.safeParse(payload);
203
+
204
+ if (!parsed.success) return c.json({ data: { ok: true } }, 200);
205
+
206
+ const { event, data } = parsed.data;
207
+
208
+ switch (event) {
209
+ case "initial-storage": {
210
+ const room = data.room;
211
+ const storage =
212
+ typeof room === "string"
213
+ ? ((await this._getInitialStorage?.({ context: {}, room })) ?? null)
214
+ : null;
215
+
216
+ return c.json({ data: { storage } }, 200);
217
+ }
218
+ case "room-deleted": {
219
+ const room = data.room;
220
+ const encodedState = data.storage;
221
+
222
+ await Promise.resolve(this._listeners?.onRoomDeleted({ encodedState, room }));
223
+
224
+ return c.json({ data: { room } }, 200);
225
+ }
226
+ case "user-connected": {
227
+ const room = data.room;
228
+ const encodedState = data.storage;
229
+ const user = data.user as any;
230
+
231
+ await Promise.resolve(this._listeners?.onUserConnected({ encodedState, room, user }));
232
+ }
233
+ case "user-disconnected": {
234
+ const room = data.room;
235
+ const encodedState = data.storage;
236
+ const user = data.user as any;
237
+
238
+ await Promise.resolve(this._listeners?.onUserDisconnected({ encodedState, room, user }));
239
+ }
240
+ default:
241
+ return c.json({ data: { ok: true } }, 200);
242
+ }
243
+ });
52
244
  }
package/src/index.ts CHANGED
@@ -1,4 +1,8 @@
1
- export { createIO } from "./createIO";
2
- export type { PluvIO, PluvIOConfig } from "./PluvIO";
3
- export type { PluvPlatform } from "./PluvPlatform";
4
- export { ZodEvent } from "./schemas";
1
+ export { platformPluv } from "./platformPluv";
2
+ export { PluvPlatform } from "./PluvPlatform";
3
+ export type {
4
+ PluvIOEndpoints,
5
+ RoomDeletedMessageEventData,
6
+ UserConnectedEventData,
7
+ UserDisconnectedEventData,
8
+ } from "./types";
@@ -0,0 +1,6 @@
1
+ import type { PluvPlatformConfig } from "./PluvPlatform";
2
+ import { PluvPlatform } from "./PluvPlatform";
3
+
4
+ export const platformPluv = (config: PluvPlatformConfig) => {
5
+ return new PluvPlatform(config);
6
+ };
package/src/types.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { GetInitialStorageFn } from "@pluv/io";
2
+
3
+ export interface PluvIOEndpoints {
4
+ createToken: string;
5
+ }
6
+
7
+ export type RoomDeletedMessageEventData = {
8
+ encodedState: string | null;
9
+ room: string;
10
+ };
11
+
12
+ export type UserConnectedEventData = {
13
+ encodedState: string | null;
14
+ room: string;
15
+ user: any;
16
+ };
17
+
18
+ export type UserDisconnectedEventData = {
19
+ encodedState: string | null;
20
+ room: string;
21
+ user: any;
22
+ };
23
+
24
+ export type PluvIOListeners = {
25
+ getInitialStorage?: GetInitialStorageFn<{}>;
26
+ onRoomDeleted: (event: RoomDeletedMessageEventData) => void;
27
+ onUserConnected: (event: UserConnectedEventData) => void;
28
+ onUserDisconnected: (event: UserDisconnectedEventData) => void;
29
+ };
package/src/PluvIO.ts DELETED
@@ -1,221 +0,0 @@
1
- import type { GetInitialStorageFn, JWTEncodeParams } from "@pluv/io";
2
- import type { BaseUser, IOLike, InputZodLike } from "@pluv/types";
3
- import { Hono } from "hono";
4
- import { handle } from "hono/vercel";
5
- import { SIGNATURE_HEADER } from "./constants";
6
- import type { PluvPlatform } from "./PluvPlatform";
7
- import { ZodEvent } from "./schemas";
8
- import { verifyWebhook } from "./shared";
9
-
10
- interface PluvAuthorize<TUser extends BaseUser> {
11
- required: true;
12
- secret: string;
13
- user: InputZodLike<TUser>;
14
- }
15
-
16
- interface PluvIOEndpoints {
17
- createToken: string;
18
- }
19
-
20
- type RoomDeletedMessageEventData = {
21
- encodedState: string | null;
22
- room: string;
23
- };
24
-
25
- type UserConnectedEventData<TUser extends BaseUser> = {
26
- encodedState: string | null;
27
- room: string;
28
- user: TUser;
29
- };
30
-
31
- type UserDisconnectedEventData<TUser extends BaseUser> = {
32
- encodedState: string | null;
33
- room: string;
34
- user: TUser;
35
- };
36
-
37
- type PluvIOListeners<TUser extends BaseUser> = {
38
- getInitialStorage?: GetInitialStorageFn<{}>;
39
- onRoomDeleted: (event: RoomDeletedMessageEventData) => void;
40
- onUserConnected: (event: UserConnectedEventData<TUser>) => void;
41
- onUserDisconnected: (event: UserDisconnectedEventData<TUser>) => void;
42
- };
43
-
44
- type WebhooksConfig<TUser extends BaseUser> =
45
- | ({ webhookSecret?: undefined } & { [P in keyof PluvIOListeners<TUser>]?: undefined })
46
- | ({ webhookSecret: string } & Partial<PluvIOListeners<TUser>>);
47
-
48
- export type PluvIOConfig<TUser extends BaseUser> = WebhooksConfig<TUser> & {
49
- /**
50
- * @ignore
51
- * @readonly
52
- * @deprecated Internal use only. Changes to this will never be marked as breaking.
53
- */
54
- _defs?: {
55
- endpoints?: PluvIOEndpoints;
56
- };
57
- authorize: {
58
- user: InputZodLike<TUser>;
59
- };
60
- basePath: string;
61
- publicKey: string;
62
- secretKey: string;
63
- };
64
-
65
- export class PluvIO<TUser extends BaseUser> implements IOLike<PluvAuthorize<TUser>, {}> {
66
- private readonly _authorize: PluvAuthorize<TUser>;
67
- private readonly _basePath: string;
68
- private readonly _endpoints: PluvIOEndpoints;
69
- private readonly _getInitialStorage?: GetInitialStorageFn<{}>;
70
- private readonly _listeners: PluvIOListeners<TUser>;
71
- private readonly _publicKey: string;
72
- private readonly _secretKey: string;
73
- private readonly _webhookSecret?: string;
74
-
75
- /**
76
- * @ignore
77
- * @readonly
78
- * @deprecated Internal use only. Changes to this will never be marked as breaking.
79
- */
80
- public get _defs() {
81
- return {
82
- authorize: this._authorize,
83
- context: {},
84
- events: {},
85
- get platform() {
86
- throw new Error("Invalid platform reference");
87
- },
88
- };
89
- }
90
-
91
- public get fetch() {
92
- const app = new Hono().basePath(this._basePath).route("/", this._webhooks);
93
-
94
- return app.fetch;
95
- }
96
-
97
- public get handler() {
98
- const app = new Hono().basePath(this._basePath).route("/", this._webhooks);
99
-
100
- return handle(app);
101
- }
102
-
103
- private _webhooks = new Hono().basePath("/").post("/", async (c) => {
104
- const signature = c.req.header(SIGNATURE_HEADER);
105
-
106
- if (!this._webhookSecret || !signature) return c.json({ error: "Unauthorized" }, 401);
107
-
108
- const payload = await c.req.json();
109
-
110
- const verified = await verifyWebhook({
111
- payload,
112
- signature,
113
- secret: this._webhookSecret,
114
- });
115
-
116
- if (!verified) return c.json({ error: "Unauthorized" }, 401);
117
-
118
- const parsed = ZodEvent.safeParse(payload);
119
-
120
- if (!parsed.success) return c.json({ data: { ok: true } }, 200);
121
-
122
- const { event, data } = parsed.data;
123
-
124
- switch (event) {
125
- case "initial-storage": {
126
- const room = data.room;
127
- const storage =
128
- typeof room === "string"
129
- ? ((await this._getInitialStorage?.({ context: {}, room })) ?? null)
130
- : null;
131
-
132
- return c.json({ data: { storage } }, 200);
133
- }
134
- case "room-deleted": {
135
- const room = data.room;
136
- const encodedState = data.storage;
137
-
138
- await Promise.resolve(this._listeners.onRoomDeleted({ encodedState, room }));
139
-
140
- return c.json({ data: { room } }, 200);
141
- }
142
- case "user-connected": {
143
- const room = data.room;
144
- const encodedState = data.storage;
145
- const user = data.user as TUser;
146
-
147
- await Promise.resolve(this._listeners.onUserConnected({ encodedState, room, user }));
148
- }
149
- case "user-disconnected": {
150
- const room = data.room;
151
- const encodedState = data.storage;
152
- const user = data.user as TUser;
153
-
154
- await Promise.resolve(this._listeners.onUserDisconnected({ encodedState, room, user }));
155
- }
156
- default:
157
- return c.json({ data: { ok: true } }, 200);
158
- }
159
- });
160
-
161
- constructor(options: PluvIOConfig<TUser>) {
162
- const {
163
- _defs,
164
- authorize,
165
- basePath,
166
- getInitialStorage,
167
- onRoomDeleted,
168
- onUserConnected,
169
- onUserDisconnected,
170
- publicKey,
171
- secretKey,
172
- webhookSecret,
173
- } = options;
174
-
175
- this._authorize = {
176
- required: true,
177
- secret: "",
178
- user: authorize.user,
179
- };
180
- this._basePath = basePath;
181
- this._endpoints = {
182
- createToken: "https://pluv.io/api/room/token",
183
- ..._defs?.endpoints,
184
- };
185
- this._getInitialStorage = getInitialStorage;
186
- this._listeners = {
187
- onRoomDeleted: (event) => onRoomDeleted?.(event),
188
- onUserConnected: (event) => onUserConnected?.(event),
189
- onUserDisconnected: (event) => onUserDisconnected?.(event),
190
- };
191
- this._publicKey = publicKey;
192
- this._secretKey = secretKey;
193
- this._webhookSecret = webhookSecret;
194
- }
195
-
196
- public async createToken(params: JWTEncodeParams<TUser, PluvPlatform>): Promise<string> {
197
- const parsed = this._authorize.user.parse(params.user);
198
-
199
- const res = await fetch(this._endpoints.createToken, {
200
- headers: { "content-type": "application/json" },
201
- method: "post",
202
- body: JSON.stringify({
203
- maxAge: params.maxAge ?? null,
204
- publicKey: this._publicKey,
205
- room: params.room,
206
- secretKey: this._secretKey,
207
- user: parsed,
208
- }),
209
- }).catch(() => null);
210
-
211
- if (!res || !res.ok || res.status !== 200) {
212
- throw new Error("Authorization failed");
213
- }
214
-
215
- const token = await res.text().catch(() => null);
216
-
217
- if (typeof token !== "string") throw new Error("Authorization failed");
218
-
219
- return token;
220
- }
221
- }
package/src/createIO.ts DELETED
@@ -1,7 +0,0 @@
1
- import type { BaseUser } from "@pluv/io";
2
- import type { PluvIOConfig } from "./PluvIO";
3
- import { PluvIO } from "./PluvIO";
4
-
5
- export const createIO = <TUser extends BaseUser>(config: PluvIOConfig<TUser>): PluvIO<TUser> => {
6
- return new PluvIO(config);
7
- };