@pluv/platform-pluv 0.32.5 → 0.32.7

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,18 +1,18 @@
1
1
 
2
- > @pluv/platform-pluv@0.32.5 build /home/runner/work/pluv/pluv/packages/platform-pluv
2
+ > @pluv/platform-pluv@0.32.7 build /home/runner/work/pluv/pluv/packages/platform-pluv
3
3
  > tsup src/index.ts --format esm,cjs --dts
4
4
 
5
5
  CLI Building entry: src/index.ts
6
6
  CLI Using tsconfig: tsconfig.json
7
- CLI tsup v8.3.0
7
+ CLI tsup v8.3.5
8
8
  CLI Target: es6
9
9
  ESM Build start
10
10
  CJS Build start
11
- ESM dist/index.mjs 4.82 KB
12
- ESM ⚡️ Build success in 114ms
13
- CJS dist/index.js 5.91 KB
14
- CJS ⚡️ Build success in 114ms
11
+ ESM dist/index.mjs 8.84 KB
12
+ ESM ⚡️ Build success in 78ms
13
+ CJS dist/index.js 9.74 KB
14
+ CJS ⚡️ Build success in 80ms
15
15
  DTS Build start
16
- DTS ⚡️ Build success in 6259ms
17
- DTS dist/index.d.mts 2.89 KB
18
- DTS dist/index.d.ts 2.89 KB
16
+ DTS ⚡️ Build success in 6515ms
17
+ DTS dist/index.d.mts 6.99 KB
18
+ DTS dist/index.d.ts 6.99 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,71 @@
1
1
  # @pluv/platform-pluv
2
2
 
3
+ ## 0.32.7
4
+
5
+ ### Patch Changes
6
+
7
+ - a9a1f7b: Export event schemas.
8
+ - @pluv/crdt@0.32.7
9
+ - @pluv/io@0.32.7
10
+ - @pluv/types@0.32.7
11
+
12
+ ## 0.32.6
13
+
14
+ ### Patch Changes
15
+
16
+ - 25292d4: Fix event payload types to only contain serializable fields.
17
+ - c0956e7: Add `onUserConnected` and `onUserDisconnected` events on `PluvServer`.
18
+
19
+ ```ts
20
+ import { createIO } from "@pluv/io";
21
+
22
+ const io = createIO({
23
+ /* ... */
24
+ });
25
+
26
+ const ioServer = io.server({
27
+ // ...
28
+ onUserConnected: (event) => {
29
+ // ...
30
+ },
31
+ onUserDisconnected: (event) => {
32
+ // ...
33
+ },
34
+ });
35
+ ```
36
+
37
+ - fc83a44: Enable `onUserConnected` and `onUserDisconnected` event listeners.
38
+ - be1488f: Validate webhook signatures via webhook secrets.
39
+
40
+ ```ts
41
+ import { createIO } from "@pluv/platform-pluv";
42
+
43
+ const io = createIO({
44
+ // ...
45
+ // If you provide a webhookSecret
46
+ webhookSecret: "whsec_...",
47
+
48
+ // The following properties will be made available to configure
49
+ getInitialStorage: (event) => {
50
+ /* ... */
51
+ },
52
+ onRoomDeleted: (event) => {
53
+ /* ... */
54
+ },
55
+ onUserConnected: (event) => {
56
+ /* ... */
57
+ },
58
+ onUserDisconnected: (event) => {
59
+ /* ... */
60
+ },
61
+ });
62
+ ```
63
+
64
+ - Updated dependencies [c0956e7]
65
+ - @pluv/io@0.32.6
66
+ - @pluv/crdt@0.32.6
67
+ - @pluv/types@0.32.6
68
+
3
69
  ## 0.32.5
4
70
 
5
71
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
- import { AbstractPlatform, WebSocketRegistrationMode, AbstractWebSocket, ConvertWebSocketConfig, WebSocketSerializedState, AbstractPlatformConfig, PluvIOListeners, GetInitialStorageFn, JWTEncodeParams, BaseUser as BaseUser$1 } from '@pluv/io';
2
- import * as hono_types from 'hono/types';
1
+ import { AbstractPlatform, WebSocketRegistrationMode, AbstractWebSocket, ConvertWebSocketConfig, WebSocketSerializedState, AbstractPlatformConfig, JWTEncodeParams, GetInitialStorageFn, BaseUser as BaseUser$1 } from '@pluv/io';
3
2
  import * as hono from 'hono';
4
3
  import { BaseUser, InputZodLike, IOLike } from '@pluv/types';
4
+ import { z } from 'zod';
5
5
 
6
6
  declare class PluvPlatform extends AbstractPlatform {
7
7
  _registrationMode: WebSocketRegistrationMode;
@@ -25,7 +25,34 @@ interface PluvAuthorize<TUser extends BaseUser> {
25
25
  interface PluvIOEndpoints {
26
26
  createToken: string;
27
27
  }
28
- type PluvIOConfig<TUser extends BaseUser> = Partial<Pick<PluvIOListeners<PluvPlatform, PluvAuthorize<TUser>, {}, {}>, "onRoomDeleted">> & {
28
+ type RoomDeletedMessageEventData = {
29
+ encodedState: string | null;
30
+ room: string;
31
+ };
32
+ type UserConnectedEventData<TUser extends BaseUser> = {
33
+ encodedState: string | null;
34
+ room: string;
35
+ user: TUser;
36
+ };
37
+ type UserDisconnectedEventData<TUser extends BaseUser> = {
38
+ encodedState: string | null;
39
+ room: string;
40
+ user: TUser;
41
+ };
42
+ type PluvIOListeners<TUser extends BaseUser> = {
43
+ getInitialStorage?: GetInitialStorageFn;
44
+ onRoomDeleted: (event: RoomDeletedMessageEventData) => void;
45
+ onUserConnected: (event: UserConnectedEventData<TUser>) => void;
46
+ onUserDisconnected: (event: UserDisconnectedEventData<TUser>) => void;
47
+ };
48
+ type WebhooksConfig<TUser extends BaseUser> = ({
49
+ webhookSecret?: undefined;
50
+ } & {
51
+ [P in keyof PluvIOListeners<TUser>]?: undefined;
52
+ }) | ({
53
+ webhookSecret: string;
54
+ } & Partial<PluvIOListeners<TUser>>);
55
+ type PluvIOConfig<TUser extends BaseUser> = WebhooksConfig<TUser> & {
29
56
  /**
30
57
  * @ignore
31
58
  * @readonly
@@ -38,7 +65,6 @@ type PluvIOConfig<TUser extends BaseUser> = Partial<Pick<PluvIOListeners<PluvPla
38
65
  user: InputZodLike<TUser>;
39
66
  };
40
67
  basePath: string;
41
- getInitialStorage?: GetInitialStorageFn<PluvPlatform>;
42
68
  publicKey: string;
43
69
  secretKey: string;
44
70
  };
@@ -50,6 +76,7 @@ declare class PluvIO<TUser extends BaseUser> implements IOLike<PluvAuthorize<TUs
50
76
  private readonly _listeners;
51
77
  private readonly _publicKey;
52
78
  private readonly _secretKey;
79
+ private readonly _webhookSecret?;
53
80
  /**
54
81
  * @ignore
55
82
  * @readonly
@@ -62,7 +89,7 @@ declare class PluvIO<TUser extends BaseUser> implements IOLike<PluvAuthorize<TUs
62
89
  readonly platform: void;
63
90
  };
64
91
  get fetch(): (request: Request, Env?: unknown, executionCtx?: hono.ExecutionContext) => Response | Promise<Response>;
65
- get handler(): (req: Request, requestContext: hono_types.FetchEventLike) => Response | Promise<Response>;
92
+ get handler(): (req: Request) => Response | Promise<Response>;
66
93
  private _webhooks;
67
94
  constructor(options: PluvIOConfig<TUser>);
68
95
  createToken(params: JWTEncodeParams<TUser, PluvPlatform>): Promise<string>;
@@ -70,4 +97,151 @@ declare class PluvIO<TUser extends BaseUser> implements IOLike<PluvAuthorize<TUs
70
97
 
71
98
  declare const createIO: <TUser extends BaseUser$1>(config: PluvIOConfig<TUser>) => PluvIO<TUser>;
72
99
 
73
- export { PluvIO, type PluvIOConfig, PluvPlatform, createIO };
100
+ declare const ZodEvent: z.ZodDiscriminatedUnion<"event", [z.ZodObject<{
101
+ event: z.ZodLiteral<"initial-storage">;
102
+ data: z.ZodObject<{
103
+ room: z.ZodNullable<z.ZodString>;
104
+ }, "strip", z.ZodTypeAny, {
105
+ room: string | null;
106
+ }, {
107
+ room: string | null;
108
+ }>;
109
+ }, "strip", z.ZodTypeAny, {
110
+ event: "initial-storage";
111
+ data: {
112
+ room: string | null;
113
+ };
114
+ }, {
115
+ event: "initial-storage";
116
+ data: {
117
+ room: string | null;
118
+ };
119
+ }>, z.ZodObject<{
120
+ event: z.ZodLiteral<"room-deleted">;
121
+ data: z.ZodObject<{
122
+ room: z.ZodString;
123
+ storage: z.ZodNullable<z.ZodString>;
124
+ }, "strip", z.ZodTypeAny, {
125
+ room: string;
126
+ storage: string | null;
127
+ }, {
128
+ room: string;
129
+ storage: string | null;
130
+ }>;
131
+ }, "strip", z.ZodTypeAny, {
132
+ event: "room-deleted";
133
+ data: {
134
+ room: string;
135
+ storage: string | null;
136
+ };
137
+ }, {
138
+ event: "room-deleted";
139
+ data: {
140
+ room: string;
141
+ storage: string | null;
142
+ };
143
+ }>, z.ZodObject<{
144
+ event: z.ZodLiteral<"user-connected">;
145
+ data: z.ZodObject<{
146
+ room: z.ZodString;
147
+ storage: z.ZodNullable<z.ZodString>;
148
+ user: z.ZodObject<{
149
+ id: z.ZodString;
150
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
151
+ id: z.ZodString;
152
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
153
+ id: z.ZodString;
154
+ }, z.ZodTypeAny, "passthrough">>;
155
+ }, "strip", z.ZodTypeAny, {
156
+ room: string;
157
+ storage: string | null;
158
+ user: {
159
+ id: string;
160
+ } & {
161
+ [k: string]: unknown;
162
+ };
163
+ }, {
164
+ room: string;
165
+ storage: string | null;
166
+ user: {
167
+ id: string;
168
+ } & {
169
+ [k: string]: unknown;
170
+ };
171
+ }>;
172
+ }, "strip", z.ZodTypeAny, {
173
+ event: "user-connected";
174
+ data: {
175
+ room: string;
176
+ storage: string | null;
177
+ user: {
178
+ id: string;
179
+ } & {
180
+ [k: string]: unknown;
181
+ };
182
+ };
183
+ }, {
184
+ event: "user-connected";
185
+ data: {
186
+ room: string;
187
+ storage: string | null;
188
+ user: {
189
+ id: string;
190
+ } & {
191
+ [k: string]: unknown;
192
+ };
193
+ };
194
+ }>, z.ZodObject<{
195
+ event: z.ZodLiteral<"user-disconnected">;
196
+ data: z.ZodObject<{
197
+ room: z.ZodString;
198
+ storage: z.ZodNullable<z.ZodString>;
199
+ user: z.ZodObject<{
200
+ id: z.ZodString;
201
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
202
+ id: z.ZodString;
203
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
204
+ id: z.ZodString;
205
+ }, z.ZodTypeAny, "passthrough">>;
206
+ }, "strip", z.ZodTypeAny, {
207
+ room: string;
208
+ storage: string | null;
209
+ user: {
210
+ id: string;
211
+ } & {
212
+ [k: string]: unknown;
213
+ };
214
+ }, {
215
+ room: string;
216
+ storage: string | null;
217
+ user: {
218
+ id: string;
219
+ } & {
220
+ [k: string]: unknown;
221
+ };
222
+ }>;
223
+ }, "strip", z.ZodTypeAny, {
224
+ event: "user-disconnected";
225
+ data: {
226
+ room: string;
227
+ storage: string | null;
228
+ user: {
229
+ id: string;
230
+ } & {
231
+ [k: string]: unknown;
232
+ };
233
+ };
234
+ }, {
235
+ event: "user-disconnected";
236
+ data: {
237
+ room: string;
238
+ storage: string | null;
239
+ user: {
240
+ id: string;
241
+ } & {
242
+ [k: string]: unknown;
243
+ };
244
+ };
245
+ }>]>;
246
+
247
+ export { PluvIO, type PluvIOConfig, PluvPlatform, ZodEvent, createIO };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { AbstractPlatform, WebSocketRegistrationMode, AbstractWebSocket, ConvertWebSocketConfig, WebSocketSerializedState, AbstractPlatformConfig, PluvIOListeners, GetInitialStorageFn, JWTEncodeParams, BaseUser as BaseUser$1 } from '@pluv/io';
2
- import * as hono_types from 'hono/types';
1
+ import { AbstractPlatform, WebSocketRegistrationMode, AbstractWebSocket, ConvertWebSocketConfig, WebSocketSerializedState, AbstractPlatformConfig, JWTEncodeParams, GetInitialStorageFn, BaseUser as BaseUser$1 } from '@pluv/io';
3
2
  import * as hono from 'hono';
4
3
  import { BaseUser, InputZodLike, IOLike } from '@pluv/types';
4
+ import { z } from 'zod';
5
5
 
6
6
  declare class PluvPlatform extends AbstractPlatform {
7
7
  _registrationMode: WebSocketRegistrationMode;
@@ -25,7 +25,34 @@ interface PluvAuthorize<TUser extends BaseUser> {
25
25
  interface PluvIOEndpoints {
26
26
  createToken: string;
27
27
  }
28
- type PluvIOConfig<TUser extends BaseUser> = Partial<Pick<PluvIOListeners<PluvPlatform, PluvAuthorize<TUser>, {}, {}>, "onRoomDeleted">> & {
28
+ type RoomDeletedMessageEventData = {
29
+ encodedState: string | null;
30
+ room: string;
31
+ };
32
+ type UserConnectedEventData<TUser extends BaseUser> = {
33
+ encodedState: string | null;
34
+ room: string;
35
+ user: TUser;
36
+ };
37
+ type UserDisconnectedEventData<TUser extends BaseUser> = {
38
+ encodedState: string | null;
39
+ room: string;
40
+ user: TUser;
41
+ };
42
+ type PluvIOListeners<TUser extends BaseUser> = {
43
+ getInitialStorage?: GetInitialStorageFn;
44
+ onRoomDeleted: (event: RoomDeletedMessageEventData) => void;
45
+ onUserConnected: (event: UserConnectedEventData<TUser>) => void;
46
+ onUserDisconnected: (event: UserDisconnectedEventData<TUser>) => void;
47
+ };
48
+ type WebhooksConfig<TUser extends BaseUser> = ({
49
+ webhookSecret?: undefined;
50
+ } & {
51
+ [P in keyof PluvIOListeners<TUser>]?: undefined;
52
+ }) | ({
53
+ webhookSecret: string;
54
+ } & Partial<PluvIOListeners<TUser>>);
55
+ type PluvIOConfig<TUser extends BaseUser> = WebhooksConfig<TUser> & {
29
56
  /**
30
57
  * @ignore
31
58
  * @readonly
@@ -38,7 +65,6 @@ type PluvIOConfig<TUser extends BaseUser> = Partial<Pick<PluvIOListeners<PluvPla
38
65
  user: InputZodLike<TUser>;
39
66
  };
40
67
  basePath: string;
41
- getInitialStorage?: GetInitialStorageFn<PluvPlatform>;
42
68
  publicKey: string;
43
69
  secretKey: string;
44
70
  };
@@ -50,6 +76,7 @@ declare class PluvIO<TUser extends BaseUser> implements IOLike<PluvAuthorize<TUs
50
76
  private readonly _listeners;
51
77
  private readonly _publicKey;
52
78
  private readonly _secretKey;
79
+ private readonly _webhookSecret?;
53
80
  /**
54
81
  * @ignore
55
82
  * @readonly
@@ -62,7 +89,7 @@ declare class PluvIO<TUser extends BaseUser> implements IOLike<PluvAuthorize<TUs
62
89
  readonly platform: void;
63
90
  };
64
91
  get fetch(): (request: Request, Env?: unknown, executionCtx?: hono.ExecutionContext) => Response | Promise<Response>;
65
- get handler(): (req: Request, requestContext: hono_types.FetchEventLike) => Response | Promise<Response>;
92
+ get handler(): (req: Request) => Response | Promise<Response>;
66
93
  private _webhooks;
67
94
  constructor(options: PluvIOConfig<TUser>);
68
95
  createToken(params: JWTEncodeParams<TUser, PluvPlatform>): Promise<string>;
@@ -70,4 +97,151 @@ declare class PluvIO<TUser extends BaseUser> implements IOLike<PluvAuthorize<TUs
70
97
 
71
98
  declare const createIO: <TUser extends BaseUser$1>(config: PluvIOConfig<TUser>) => PluvIO<TUser>;
72
99
 
73
- export { PluvIO, type PluvIOConfig, PluvPlatform, createIO };
100
+ declare const ZodEvent: z.ZodDiscriminatedUnion<"event", [z.ZodObject<{
101
+ event: z.ZodLiteral<"initial-storage">;
102
+ data: z.ZodObject<{
103
+ room: z.ZodNullable<z.ZodString>;
104
+ }, "strip", z.ZodTypeAny, {
105
+ room: string | null;
106
+ }, {
107
+ room: string | null;
108
+ }>;
109
+ }, "strip", z.ZodTypeAny, {
110
+ event: "initial-storage";
111
+ data: {
112
+ room: string | null;
113
+ };
114
+ }, {
115
+ event: "initial-storage";
116
+ data: {
117
+ room: string | null;
118
+ };
119
+ }>, z.ZodObject<{
120
+ event: z.ZodLiteral<"room-deleted">;
121
+ data: z.ZodObject<{
122
+ room: z.ZodString;
123
+ storage: z.ZodNullable<z.ZodString>;
124
+ }, "strip", z.ZodTypeAny, {
125
+ room: string;
126
+ storage: string | null;
127
+ }, {
128
+ room: string;
129
+ storage: string | null;
130
+ }>;
131
+ }, "strip", z.ZodTypeAny, {
132
+ event: "room-deleted";
133
+ data: {
134
+ room: string;
135
+ storage: string | null;
136
+ };
137
+ }, {
138
+ event: "room-deleted";
139
+ data: {
140
+ room: string;
141
+ storage: string | null;
142
+ };
143
+ }>, z.ZodObject<{
144
+ event: z.ZodLiteral<"user-connected">;
145
+ data: z.ZodObject<{
146
+ room: z.ZodString;
147
+ storage: z.ZodNullable<z.ZodString>;
148
+ user: z.ZodObject<{
149
+ id: z.ZodString;
150
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
151
+ id: z.ZodString;
152
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
153
+ id: z.ZodString;
154
+ }, z.ZodTypeAny, "passthrough">>;
155
+ }, "strip", z.ZodTypeAny, {
156
+ room: string;
157
+ storage: string | null;
158
+ user: {
159
+ id: string;
160
+ } & {
161
+ [k: string]: unknown;
162
+ };
163
+ }, {
164
+ room: string;
165
+ storage: string | null;
166
+ user: {
167
+ id: string;
168
+ } & {
169
+ [k: string]: unknown;
170
+ };
171
+ }>;
172
+ }, "strip", z.ZodTypeAny, {
173
+ event: "user-connected";
174
+ data: {
175
+ room: string;
176
+ storage: string | null;
177
+ user: {
178
+ id: string;
179
+ } & {
180
+ [k: string]: unknown;
181
+ };
182
+ };
183
+ }, {
184
+ event: "user-connected";
185
+ data: {
186
+ room: string;
187
+ storage: string | null;
188
+ user: {
189
+ id: string;
190
+ } & {
191
+ [k: string]: unknown;
192
+ };
193
+ };
194
+ }>, z.ZodObject<{
195
+ event: z.ZodLiteral<"user-disconnected">;
196
+ data: z.ZodObject<{
197
+ room: z.ZodString;
198
+ storage: z.ZodNullable<z.ZodString>;
199
+ user: z.ZodObject<{
200
+ id: z.ZodString;
201
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
202
+ id: z.ZodString;
203
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
204
+ id: z.ZodString;
205
+ }, z.ZodTypeAny, "passthrough">>;
206
+ }, "strip", z.ZodTypeAny, {
207
+ room: string;
208
+ storage: string | null;
209
+ user: {
210
+ id: string;
211
+ } & {
212
+ [k: string]: unknown;
213
+ };
214
+ }, {
215
+ room: string;
216
+ storage: string | null;
217
+ user: {
218
+ id: string;
219
+ } & {
220
+ [k: string]: unknown;
221
+ };
222
+ }>;
223
+ }, "strip", z.ZodTypeAny, {
224
+ event: "user-disconnected";
225
+ data: {
226
+ room: string;
227
+ storage: string | null;
228
+ user: {
229
+ id: string;
230
+ } & {
231
+ [k: string]: unknown;
232
+ };
233
+ };
234
+ }, {
235
+ event: "user-disconnected";
236
+ data: {
237
+ room: string;
238
+ storage: string | null;
239
+ user: {
240
+ id: string;
241
+ } & {
242
+ [k: string]: unknown;
243
+ };
244
+ };
245
+ }>]>;
246
+
247
+ export { PluvIO, type PluvIOConfig, PluvPlatform, ZodEvent, createIO };
package/dist/index.js CHANGED
@@ -54,6 +54,7 @@ var __async = (__this, __arguments, generator) => {
54
54
  // src/index.ts
55
55
  var src_exports = {};
56
56
  __export(src_exports, {
57
+ ZodEvent: () => ZodEvent,
57
58
  createIO: () => createIO
58
59
  });
59
60
  module.exports = __toCommonJS(src_exports);
@@ -62,6 +63,9 @@ module.exports = __toCommonJS(src_exports);
62
63
  var import_hono = require("hono");
63
64
  var import_vercel = require("hono/vercel");
64
65
 
66
+ // src/constants.ts
67
+ var SIGNATURE_HEADER = "x-pluv-signature-256";
68
+
65
69
  // src/schemas.ts
66
70
  var import_zod = require("zod");
67
71
  var ZodEventInitialStorage = import_zod.z.object({
@@ -77,15 +81,100 @@ var ZodEventRoomDeleted = import_zod.z.object({
77
81
  storage: import_zod.z.string().nullable()
78
82
  })
79
83
  });
80
- var ZodEvent = import_zod.z.discriminatedUnion("event", [ZodEventInitialStorage, ZodEventRoomDeleted]);
84
+ var ZodEventUserConnected = import_zod.z.object({
85
+ event: import_zod.z.literal("user-connected"),
86
+ data: import_zod.z.object({
87
+ room: import_zod.z.string(),
88
+ storage: import_zod.z.string().nullable(),
89
+ user: import_zod.z.object({
90
+ id: import_zod.z.string()
91
+ }).passthrough()
92
+ })
93
+ });
94
+ var ZodEventUserDisconnected = import_zod.z.object({
95
+ event: import_zod.z.literal("user-disconnected"),
96
+ data: import_zod.z.object({
97
+ room: import_zod.z.string(),
98
+ storage: import_zod.z.string().nullable(),
99
+ user: import_zod.z.object({
100
+ id: import_zod.z.string()
101
+ }).passthrough()
102
+ })
103
+ });
104
+ var ZodEvent = import_zod.z.discriminatedUnion("event", [
105
+ ZodEventInitialStorage,
106
+ ZodEventRoomDeleted,
107
+ ZodEventUserConnected,
108
+ ZodEventUserDisconnected
109
+ ]);
110
+
111
+ // src/shared/getCrypto.ts
112
+ var getCrypto = () => {
113
+ if (typeof crypto !== "undefined") {
114
+ return crypto;
115
+ }
116
+ if (typeof require === "function") {
117
+ return require("crypto").webcrypto;
118
+ }
119
+ throw new Error("Missing crypto module");
120
+ };
121
+
122
+ // src/shared/createHmac.ts
123
+ var createHmac = (params) => __async(void 0, null, function* () {
124
+ const { payload, secret } = params;
125
+ if (!payload || !secret) throw new Error("Secret and payload are required to sign payload");
126
+ const encoder = new TextEncoder();
127
+ const keyBytes = encoder.encode(secret);
128
+ const crypto2 = getCrypto();
129
+ const algorithm = { name: "HMAC", hash: { name: "SHA-256" } };
130
+ const extractable = false;
131
+ const key = yield crypto2.subtle.importKey("raw", keyBytes, algorithm, extractable, ["sign", "verify"]);
132
+ const payloadBytes = encoder.encode(payload);
133
+ const signature = yield crypto2.subtle.sign("HMAC", key, payloadBytes);
134
+ const hmac = Array.from(new Uint8Array(signature)).map((b) => ("0" + b.toString(16)).slice(-2)).join("");
135
+ return { algorithm: "sha256", hmac };
136
+ });
137
+
138
+ // src/shared/timingSafeEqual.ts
139
+ var timingSafeEqual = (a, b) => {
140
+ if (a.length !== b.length) return false;
141
+ let result = 0;
142
+ for (let i = 0; i < a.length; i++) {
143
+ result |= a[i] ^ b[i];
144
+ }
145
+ return result === 0;
146
+ };
147
+
148
+ // src/shared/verifyWebhook.ts
149
+ var verifyWebhook = (params) => __async(void 0, null, function* () {
150
+ const { payload, secret, signature } = params;
151
+ if (!secret || !payload || !signature) {
152
+ throw new Error("Secret, payload and signature are required to verify payload");
153
+ }
154
+ const { hmac } = yield createHmac({ payload, secret });
155
+ if (hmac.length !== signature.length) return false;
156
+ const encoder = new TextEncoder();
157
+ const verificationBytes = encoder.encode(hmac);
158
+ const signatureBytes = encoder.encode(signature);
159
+ if (verificationBytes.length !== signatureBytes.length) return false;
160
+ return timingSafeEqual(verificationBytes, signatureBytes);
161
+ });
81
162
 
82
163
  // src/PluvIO.ts
83
164
  var PluvIO = class {
84
165
  constructor(options) {
85
166
  this._webhooks = new import_hono.Hono().basePath("/").post("/event", (c) => __async(this, null, function* () {
86
167
  var _a, _b;
87
- const body = yield c.req.json();
88
- const parsed = ZodEvent.safeParse(body);
168
+ const signature = c.req.header(SIGNATURE_HEADER);
169
+ if (!this._webhookSecret || !signature) return c.json({ error: "Unauthorized" }, 401);
170
+ const payload = yield c.req.json();
171
+ const verified = yield verifyWebhook({
172
+ payload,
173
+ signature,
174
+ secret: this._webhookSecret
175
+ });
176
+ if (!verified) return c.json({ error: "Unauthorized" }, 401);
177
+ const parsed = ZodEvent.safeParse(payload);
89
178
  if (!parsed.success) return c.json({ data: { ok: true } }, 200);
90
179
  const { event, data } = parsed.data;
91
180
  switch (event) {
@@ -97,14 +186,37 @@ var PluvIO = class {
97
186
  case "room-deleted": {
98
187
  const room = data.room;
99
188
  const encodedState = data.storage;
100
- yield Promise.resolve(this._listeners.onRoomDeleted({ context: {}, encodedState, room }));
189
+ yield Promise.resolve(this._listeners.onRoomDeleted({ encodedState, room }));
101
190
  return c.json({ data: { room } }, 200);
102
191
  }
192
+ case "user-connected": {
193
+ const room = data.room;
194
+ const encodedState = data.storage;
195
+ const user = data.user;
196
+ yield Promise.resolve(this._listeners.onUserConnected({ encodedState, room, user }));
197
+ }
198
+ case "user-disconnected": {
199
+ const room = data.room;
200
+ const encodedState = data.storage;
201
+ const user = data.user;
202
+ yield Promise.resolve(this._listeners.onUserDisconnected({ encodedState, room, user }));
203
+ }
103
204
  default:
104
205
  return c.json({ data: { ok: true } }, 200);
105
206
  }
106
207
  }));
107
- const { _defs, authorize, basePath, onRoomDeleted, getInitialStorage, publicKey, secretKey } = options;
208
+ const {
209
+ _defs,
210
+ authorize,
211
+ basePath,
212
+ getInitialStorage,
213
+ onRoomDeleted,
214
+ onUserConnected,
215
+ onUserDisconnected,
216
+ publicKey,
217
+ secretKey,
218
+ webhookSecret
219
+ } = options;
108
220
  this._authorize = {
109
221
  required: true,
110
222
  secret: "",
@@ -115,9 +227,14 @@ var PluvIO = class {
115
227
  createToken: "https://pluv.io/api/room/token"
116
228
  }, _defs == null ? void 0 : _defs.endpoints);
117
229
  this._getInitialStorage = getInitialStorage;
118
- this._listeners = { onRoomDeleted: (event) => onRoomDeleted == null ? void 0 : onRoomDeleted(event) };
230
+ this._listeners = {
231
+ onRoomDeleted: (event) => onRoomDeleted == null ? void 0 : onRoomDeleted(event),
232
+ onUserConnected: (event) => onUserConnected == null ? void 0 : onUserConnected(event),
233
+ onUserDisconnected: (event) => onUserDisconnected == null ? void 0 : onUserDisconnected(event)
234
+ };
119
235
  this._publicKey = publicKey;
120
236
  this._secretKey = secretKey;
237
+ this._webhookSecret = webhookSecret;
121
238
  }
122
239
  /**
123
240
  * @ignore
@@ -161,9 +278,7 @@ var PluvIO = class {
161
278
  throw new Error("Authorization failed");
162
279
  }
163
280
  const token = yield res.text().catch(() => null);
164
- if (typeof token !== "string") {
165
- throw new Error("Authorization failed");
166
- }
281
+ if (typeof token !== "string") throw new Error("Authorization failed");
167
282
  return token;
168
283
  });
169
284
  }
@@ -175,5 +290,6 @@ var createIO = (config) => {
175
290
  };
176
291
  // Annotate the CommonJS export names for ESM import in node:
177
292
  0 && (module.exports = {
293
+ ZodEvent,
178
294
  createIO
179
295
  });
package/dist/index.mjs CHANGED
@@ -14,6 +14,12 @@ var __spreadValues = (a, b) => {
14
14
  }
15
15
  return a;
16
16
  };
17
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
18
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
19
+ }) : x)(function(x) {
20
+ if (typeof require !== "undefined") return require.apply(this, arguments);
21
+ throw Error('Dynamic require of "' + x + '" is not supported');
22
+ });
17
23
  var __async = (__this, __arguments, generator) => {
18
24
  return new Promise((resolve, reject) => {
19
25
  var fulfilled = (value) => {
@@ -39,6 +45,9 @@ var __async = (__this, __arguments, generator) => {
39
45
  import { Hono } from "hono";
40
46
  import { handle } from "hono/vercel";
41
47
 
48
+ // src/constants.ts
49
+ var SIGNATURE_HEADER = "x-pluv-signature-256";
50
+
42
51
  // src/schemas.ts
43
52
  import { z } from "zod";
44
53
  var ZodEventInitialStorage = z.object({
@@ -54,15 +63,100 @@ var ZodEventRoomDeleted = z.object({
54
63
  storage: z.string().nullable()
55
64
  })
56
65
  });
57
- var ZodEvent = z.discriminatedUnion("event", [ZodEventInitialStorage, ZodEventRoomDeleted]);
66
+ var ZodEventUserConnected = z.object({
67
+ event: z.literal("user-connected"),
68
+ data: z.object({
69
+ room: z.string(),
70
+ storage: z.string().nullable(),
71
+ user: z.object({
72
+ id: z.string()
73
+ }).passthrough()
74
+ })
75
+ });
76
+ var ZodEventUserDisconnected = z.object({
77
+ event: z.literal("user-disconnected"),
78
+ data: z.object({
79
+ room: z.string(),
80
+ storage: z.string().nullable(),
81
+ user: z.object({
82
+ id: z.string()
83
+ }).passthrough()
84
+ })
85
+ });
86
+ var ZodEvent = z.discriminatedUnion("event", [
87
+ ZodEventInitialStorage,
88
+ ZodEventRoomDeleted,
89
+ ZodEventUserConnected,
90
+ ZodEventUserDisconnected
91
+ ]);
92
+
93
+ // src/shared/getCrypto.ts
94
+ var getCrypto = () => {
95
+ if (typeof crypto !== "undefined") {
96
+ return crypto;
97
+ }
98
+ if (typeof __require === "function") {
99
+ return __require("node:crypto").webcrypto;
100
+ }
101
+ throw new Error("Missing crypto module");
102
+ };
103
+
104
+ // src/shared/createHmac.ts
105
+ var createHmac = (params) => __async(void 0, null, function* () {
106
+ const { payload, secret } = params;
107
+ if (!payload || !secret) throw new Error("Secret and payload are required to sign payload");
108
+ const encoder = new TextEncoder();
109
+ const keyBytes = encoder.encode(secret);
110
+ const crypto2 = getCrypto();
111
+ const algorithm = { name: "HMAC", hash: { name: "SHA-256" } };
112
+ const extractable = false;
113
+ const key = yield crypto2.subtle.importKey("raw", keyBytes, algorithm, extractable, ["sign", "verify"]);
114
+ const payloadBytes = encoder.encode(payload);
115
+ const signature = yield crypto2.subtle.sign("HMAC", key, payloadBytes);
116
+ const hmac = Array.from(new Uint8Array(signature)).map((b) => ("0" + b.toString(16)).slice(-2)).join("");
117
+ return { algorithm: "sha256", hmac };
118
+ });
119
+
120
+ // src/shared/timingSafeEqual.ts
121
+ var timingSafeEqual = (a, b) => {
122
+ if (a.length !== b.length) return false;
123
+ let result = 0;
124
+ for (let i = 0; i < a.length; i++) {
125
+ result |= a[i] ^ b[i];
126
+ }
127
+ return result === 0;
128
+ };
129
+
130
+ // src/shared/verifyWebhook.ts
131
+ var verifyWebhook = (params) => __async(void 0, null, function* () {
132
+ const { payload, secret, signature } = params;
133
+ if (!secret || !payload || !signature) {
134
+ throw new Error("Secret, payload and signature are required to verify payload");
135
+ }
136
+ const { hmac } = yield createHmac({ payload, secret });
137
+ if (hmac.length !== signature.length) return false;
138
+ const encoder = new TextEncoder();
139
+ const verificationBytes = encoder.encode(hmac);
140
+ const signatureBytes = encoder.encode(signature);
141
+ if (verificationBytes.length !== signatureBytes.length) return false;
142
+ return timingSafeEqual(verificationBytes, signatureBytes);
143
+ });
58
144
 
59
145
  // src/PluvIO.ts
60
146
  var PluvIO = class {
61
147
  constructor(options) {
62
148
  this._webhooks = new Hono().basePath("/").post("/event", (c) => __async(this, null, function* () {
63
149
  var _a, _b;
64
- const body = yield c.req.json();
65
- const parsed = ZodEvent.safeParse(body);
150
+ const signature = c.req.header(SIGNATURE_HEADER);
151
+ if (!this._webhookSecret || !signature) return c.json({ error: "Unauthorized" }, 401);
152
+ const payload = yield c.req.json();
153
+ const verified = yield verifyWebhook({
154
+ payload,
155
+ signature,
156
+ secret: this._webhookSecret
157
+ });
158
+ if (!verified) return c.json({ error: "Unauthorized" }, 401);
159
+ const parsed = ZodEvent.safeParse(payload);
66
160
  if (!parsed.success) return c.json({ data: { ok: true } }, 200);
67
161
  const { event, data } = parsed.data;
68
162
  switch (event) {
@@ -74,14 +168,37 @@ var PluvIO = class {
74
168
  case "room-deleted": {
75
169
  const room = data.room;
76
170
  const encodedState = data.storage;
77
- yield Promise.resolve(this._listeners.onRoomDeleted({ context: {}, encodedState, room }));
171
+ yield Promise.resolve(this._listeners.onRoomDeleted({ encodedState, room }));
78
172
  return c.json({ data: { room } }, 200);
79
173
  }
174
+ case "user-connected": {
175
+ const room = data.room;
176
+ const encodedState = data.storage;
177
+ const user = data.user;
178
+ yield Promise.resolve(this._listeners.onUserConnected({ encodedState, room, user }));
179
+ }
180
+ case "user-disconnected": {
181
+ const room = data.room;
182
+ const encodedState = data.storage;
183
+ const user = data.user;
184
+ yield Promise.resolve(this._listeners.onUserDisconnected({ encodedState, room, user }));
185
+ }
80
186
  default:
81
187
  return c.json({ data: { ok: true } }, 200);
82
188
  }
83
189
  }));
84
- const { _defs, authorize, basePath, onRoomDeleted, getInitialStorage, publicKey, secretKey } = options;
190
+ const {
191
+ _defs,
192
+ authorize,
193
+ basePath,
194
+ getInitialStorage,
195
+ onRoomDeleted,
196
+ onUserConnected,
197
+ onUserDisconnected,
198
+ publicKey,
199
+ secretKey,
200
+ webhookSecret
201
+ } = options;
85
202
  this._authorize = {
86
203
  required: true,
87
204
  secret: "",
@@ -92,9 +209,14 @@ var PluvIO = class {
92
209
  createToken: "https://pluv.io/api/room/token"
93
210
  }, _defs == null ? void 0 : _defs.endpoints);
94
211
  this._getInitialStorage = getInitialStorage;
95
- this._listeners = { onRoomDeleted: (event) => onRoomDeleted == null ? void 0 : onRoomDeleted(event) };
212
+ this._listeners = {
213
+ onRoomDeleted: (event) => onRoomDeleted == null ? void 0 : onRoomDeleted(event),
214
+ onUserConnected: (event) => onUserConnected == null ? void 0 : onUserConnected(event),
215
+ onUserDisconnected: (event) => onUserDisconnected == null ? void 0 : onUserDisconnected(event)
216
+ };
96
217
  this._publicKey = publicKey;
97
218
  this._secretKey = secretKey;
219
+ this._webhookSecret = webhookSecret;
98
220
  }
99
221
  /**
100
222
  * @ignore
@@ -138,9 +260,7 @@ var PluvIO = class {
138
260
  throw new Error("Authorization failed");
139
261
  }
140
262
  const token = yield res.text().catch(() => null);
141
- if (typeof token !== "string") {
142
- throw new Error("Authorization failed");
143
- }
263
+ if (typeof token !== "string") throw new Error("Authorization failed");
144
264
  return token;
145
265
  });
146
266
  }
@@ -151,5 +271,6 @@ var createIO = (config) => {
151
271
  return new PluvIO(config);
152
272
  };
153
273
  export {
274
+ ZodEvent,
154
275
  createIO
155
276
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pluv/platform-pluv",
3
- "version": "0.32.5",
3
+ "version": "0.32.7",
4
4
  "description": "@pluv/io adapter for pluv.io",
5
5
  "author": "leedavidcs",
6
6
  "license": "MIT",
@@ -17,19 +17,19 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
- "@types/node": "^22.7.8",
21
- "hono": "^4.6.6",
20
+ "@types/node": "^22.8.1",
21
+ "hono": "^4.6.7",
22
22
  "zod": "^3.23.8",
23
- "@pluv/crdt": "^0.32.5",
24
- "@pluv/io": "^0.32.5",
25
- "@pluv/types": "^0.32.5"
23
+ "@pluv/crdt": "^0.32.7",
24
+ "@pluv/io": "^0.32.7",
25
+ "@pluv/types": "^0.32.7"
26
26
  },
27
27
  "devDependencies": {
28
28
  "eslint": "^8.57.0",
29
- "tsup": "^8.3.0",
29
+ "tsup": "^8.3.5",
30
30
  "typescript": "^5.6.3",
31
- "@pluv/tsconfig": "^0.32.5",
32
- "eslint-config-pluv": "^0.32.5"
31
+ "eslint-config-pluv": "^0.32.7",
32
+ "@pluv/tsconfig": "^0.32.7"
33
33
  },
34
34
  "scripts": {
35
35
  "build": "tsup src/index.ts --format esm,cjs --dts",
package/src/PluvIO.ts CHANGED
@@ -1,9 +1,11 @@
1
- import type { GetInitialStorageFn, JWTEncodeParams, PluvIOListeners } from "@pluv/io";
1
+ import type { GetInitialStorageFn, JWTEncodeParams } from "@pluv/io";
2
2
  import type { BaseUser, IOLike, InputZodLike } from "@pluv/types";
3
3
  import { Hono } from "hono";
4
4
  import { handle } from "hono/vercel";
5
+ import { SIGNATURE_HEADER } from "./constants";
5
6
  import type { PluvPlatform } from "./PluvPlatform";
6
7
  import { ZodEvent } from "./schemas";
8
+ import { verifyWebhook } from "./shared";
7
9
 
8
10
  interface PluvAuthorize<TUser extends BaseUser> {
9
11
  required: true;
@@ -15,9 +17,35 @@ interface PluvIOEndpoints {
15
17
  createToken: string;
16
18
  }
17
19
 
18
- export type PluvIOConfig<TUser extends BaseUser> = Partial<
19
- Pick<PluvIOListeners<PluvPlatform, PluvAuthorize<TUser>, {}, {}>, "onRoomDeleted">
20
- > & {
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> & {
21
49
  /**
22
50
  * @ignore
23
51
  * @readonly
@@ -30,7 +58,6 @@ export type PluvIOConfig<TUser extends BaseUser> = Partial<
30
58
  user: InputZodLike<TUser>;
31
59
  };
32
60
  basePath: string;
33
- getInitialStorage?: GetInitialStorageFn<PluvPlatform>;
34
61
  publicKey: string;
35
62
  secretKey: string;
36
63
  };
@@ -39,10 +66,11 @@ export class PluvIO<TUser extends BaseUser> implements IOLike<PluvAuthorize<TUse
39
66
  private readonly _authorize: PluvAuthorize<TUser>;
40
67
  private readonly _basePath: string;
41
68
  private readonly _endpoints: PluvIOEndpoints;
42
- private readonly _getInitialStorage?: GetInitialStorageFn<PluvPlatform>;
43
- private readonly _listeners: Pick<PluvIOListeners<PluvPlatform, PluvAuthorize<TUser>, {}, {}>, "onRoomDeleted">;
69
+ private readonly _getInitialStorage?: GetInitialStorageFn;
70
+ private readonly _listeners: PluvIOListeners<TUser>;
44
71
  private readonly _publicKey: string;
45
72
  private readonly _secretKey: string;
73
+ private readonly _webhookSecret?: string;
46
74
 
47
75
  /**
48
76
  * @ignore
@@ -73,8 +101,21 @@ export class PluvIO<TUser extends BaseUser> implements IOLike<PluvAuthorize<TUse
73
101
  }
74
102
 
75
103
  private _webhooks = new Hono().basePath("/").post("/event", async (c) => {
76
- const body = await c.req.json();
77
- const parsed = ZodEvent.safeParse(body);
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);
78
119
 
79
120
  if (!parsed.success) return c.json({ data: { ok: true } }, 200);
80
121
 
@@ -91,17 +132,42 @@ export class PluvIO<TUser extends BaseUser> implements IOLike<PluvAuthorize<TUse
91
132
  const room = data.room;
92
133
  const encodedState = data.storage;
93
134
 
94
- await Promise.resolve(this._listeners.onRoomDeleted({ context: {}, encodedState, room }));
135
+ await Promise.resolve(this._listeners.onRoomDeleted({ encodedState, room }));
95
136
 
96
137
  return c.json({ data: { room } }, 200);
97
138
  }
139
+ case "user-connected": {
140
+ const room = data.room;
141
+ const encodedState = data.storage;
142
+ const user = data.user as TUser;
143
+
144
+ await Promise.resolve(this._listeners.onUserConnected({ encodedState, room, user }));
145
+ }
146
+ case "user-disconnected": {
147
+ const room = data.room;
148
+ const encodedState = data.storage;
149
+ const user = data.user as TUser;
150
+
151
+ await Promise.resolve(this._listeners.onUserDisconnected({ encodedState, room, user }));
152
+ }
98
153
  default:
99
154
  return c.json({ data: { ok: true } }, 200);
100
155
  }
101
156
  });
102
157
 
103
158
  constructor(options: PluvIOConfig<TUser>) {
104
- const { _defs, authorize, basePath, onRoomDeleted, getInitialStorage, publicKey, secretKey } = options;
159
+ const {
160
+ _defs,
161
+ authorize,
162
+ basePath,
163
+ getInitialStorage,
164
+ onRoomDeleted,
165
+ onUserConnected,
166
+ onUserDisconnected,
167
+ publicKey,
168
+ secretKey,
169
+ webhookSecret,
170
+ } = options;
105
171
 
106
172
  this._authorize = {
107
173
  required: true,
@@ -114,9 +180,14 @@ export class PluvIO<TUser extends BaseUser> implements IOLike<PluvAuthorize<TUse
114
180
  ..._defs?.endpoints,
115
181
  };
116
182
  this._getInitialStorage = getInitialStorage;
117
- this._listeners = { onRoomDeleted: (event) => onRoomDeleted?.(event) };
183
+ this._listeners = {
184
+ onRoomDeleted: (event) => onRoomDeleted?.(event),
185
+ onUserConnected: (event) => onUserConnected?.(event),
186
+ onUserDisconnected: (event) => onUserDisconnected?.(event),
187
+ };
118
188
  this._publicKey = publicKey;
119
189
  this._secretKey = secretKey;
190
+ this._webhookSecret = webhookSecret;
120
191
  }
121
192
 
122
193
  public async createToken(params: JWTEncodeParams<TUser, PluvPlatform>): Promise<string> {
@@ -140,9 +211,7 @@ export class PluvIO<TUser extends BaseUser> implements IOLike<PluvAuthorize<TUse
140
211
 
141
212
  const token = await res.text().catch(() => null);
142
213
 
143
- if (typeof token !== "string") {
144
- throw new Error("Authorization failed");
145
- }
214
+ if (typeof token !== "string") throw new Error("Authorization failed");
146
215
 
147
216
  return token;
148
217
  }
@@ -0,0 +1 @@
1
+ export const SIGNATURE_HEADER = "x-pluv-signature-256";
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { createIO } from "./createIO";
2
2
  export type { PluvIO, PluvIOConfig } from "./PluvIO";
3
3
  export type { PluvPlatform } from "./PluvPlatform";
4
+ export { ZodEvent } from "./schemas";
package/src/schemas.ts CHANGED
@@ -15,4 +15,35 @@ const ZodEventRoomDeleted = z.object({
15
15
  }),
16
16
  });
17
17
 
18
- export const ZodEvent = z.discriminatedUnion("event", [ZodEventInitialStorage, ZodEventRoomDeleted]);
18
+ const ZodEventUserConnected = z.object({
19
+ event: z.literal("user-connected"),
20
+ data: z.object({
21
+ room: z.string(),
22
+ storage: z.string().nullable(),
23
+ user: z
24
+ .object({
25
+ id: z.string(),
26
+ })
27
+ .passthrough(),
28
+ }),
29
+ });
30
+
31
+ const ZodEventUserDisconnected = z.object({
32
+ event: z.literal("user-disconnected"),
33
+ data: z.object({
34
+ room: z.string(),
35
+ storage: z.string().nullable(),
36
+ user: z
37
+ .object({
38
+ id: z.string(),
39
+ })
40
+ .passthrough(),
41
+ }),
42
+ });
43
+
44
+ export const ZodEvent = z.discriminatedUnion("event", [
45
+ ZodEventInitialStorage,
46
+ ZodEventRoomDeleted,
47
+ ZodEventUserConnected,
48
+ ZodEventUserDisconnected,
49
+ ]);
@@ -0,0 +1,38 @@
1
+ import type { webcrypto } from "crypto";
2
+ import { getCrypto } from "./getCrypto";
3
+
4
+ export interface CreateHmacParams {
5
+ payload: string;
6
+ secret: string;
7
+ }
8
+
9
+ export type CreateHmacResult = {
10
+ algorithm: "sha256";
11
+ hmac: string;
12
+ };
13
+
14
+ export const createHmac = async (params: CreateHmacParams): Promise<CreateHmacResult> => {
15
+ const { payload, secret } = params;
16
+
17
+ if (!payload || !secret) throw new Error("Secret and payload are required to sign payload");
18
+
19
+ const encoder = new TextEncoder();
20
+ const keyBytes = encoder.encode(secret);
21
+
22
+ const crypto = getCrypto();
23
+
24
+ const algorithm: webcrypto.HmacImportParams = { name: "HMAC", hash: { name: "SHA-256" } };
25
+ const extractable = false;
26
+
27
+ const key = await crypto.subtle.importKey("raw", keyBytes, algorithm, extractable, ["sign", "verify"]);
28
+ const payloadBytes = encoder.encode(payload);
29
+
30
+ const signature = await crypto.subtle.sign("HMAC", key, payloadBytes);
31
+
32
+ // Convert the signature to a hex string
33
+ const hmac = Array.from(new Uint8Array(signature))
34
+ .map((b) => ("0" + b.toString(16)).slice(-2))
35
+ .join("");
36
+
37
+ return { algorithm: "sha256", hmac };
38
+ };
@@ -0,0 +1,16 @@
1
+ import type { webcrypto } from "crypto";
2
+
3
+ export const getCrypto = (): webcrypto.Crypto => {
4
+ if (typeof crypto !== "undefined") {
5
+ // In a browser or Web Worker (including Cloudflare Workers)
6
+ return crypto;
7
+ }
8
+
9
+ if (typeof require === "function") {
10
+ // In Node.js
11
+ // Node 15+ supports `crypto.webcrypto`
12
+ return require("node:crypto").webcrypto as webcrypto.Crypto;
13
+ }
14
+
15
+ throw new Error("Missing crypto module");
16
+ };
@@ -0,0 +1,8 @@
1
+ export { createHmac } from "./createHmac";
2
+ export type { CreateHmacParams } from "./createHmac";
3
+ export { getCrypto } from "./getCrypto";
4
+ export { signWebhook } from "./signWebhook";
5
+ export type { SignWebhookParams } from "./signWebhook";
6
+ export { timingSafeEqual } from "./timingSafeEqual";
7
+ export { verifyWebhook } from "./verifyWebhook";
8
+ export type { VerifyWebhookParams } from "./verifyWebhook";
@@ -0,0 +1,10 @@
1
+ import type { CreateHmacParams } from "./createHmac";
2
+ import { createHmac } from "./createHmac";
3
+
4
+ export type SignWebhookParams = CreateHmacParams;
5
+
6
+ export const signWebhook = async (params: SignWebhookParams): Promise<string> => {
7
+ const { algorithm, hmac } = await createHmac(params);
8
+
9
+ return `${algorithm}=${hmac}`;
10
+ };
@@ -0,0 +1,10 @@
1
+ export const timingSafeEqual = (a: Uint8Array, b: Uint8Array): boolean => {
2
+ if (a.length !== b.length) return false; // Lengths are different
3
+
4
+ let result = 0;
5
+ for (let i = 0; i < a.length; i++) {
6
+ result |= a[i] ^ b[i]; // XOR each byte
7
+ }
8
+
9
+ return result === 0; // If all bytes are equal, result will be 0
10
+ };
@@ -0,0 +1,28 @@
1
+ import { createHmac } from "./createHmac";
2
+ import { timingSafeEqual } from "./timingSafeEqual";
3
+
4
+ export interface VerifyWebhookParams {
5
+ payload: string;
6
+ secret: string;
7
+ signature: string;
8
+ }
9
+
10
+ export const verifyWebhook = async (params: VerifyWebhookParams): Promise<boolean> => {
11
+ const { payload, secret, signature } = params;
12
+
13
+ if (!secret || !payload || !signature) {
14
+ throw new Error("Secret, payload and signature are required to verify payload");
15
+ }
16
+
17
+ const { hmac } = await createHmac({ payload, secret });
18
+
19
+ if (hmac.length !== signature.length) return false;
20
+
21
+ const encoder = new TextEncoder();
22
+ const verificationBytes = encoder.encode(hmac);
23
+ const signatureBytes = encoder.encode(signature);
24
+
25
+ if (verificationBytes.length !== signatureBytes.length) return false;
26
+
27
+ return timingSafeEqual(verificationBytes, signatureBytes);
28
+ };