@pluv/platform-pluv 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/dist/index.mjs ADDED
@@ -0,0 +1,403 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+ var __async = (__this, __arguments, generator) => {
8
+ return new Promise((resolve, reject) => {
9
+ var fulfilled = (value) => {
10
+ try {
11
+ step(generator.next(value));
12
+ } catch (e) {
13
+ reject(e);
14
+ }
15
+ };
16
+ var rejected = (value) => {
17
+ try {
18
+ step(generator.throw(value));
19
+ } catch (e) {
20
+ reject(e);
21
+ }
22
+ };
23
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
24
+ step((generator = generator.apply(__this, __arguments)).next());
25
+ });
26
+ };
27
+
28
+ // src/PluvPlatform.ts
29
+ import { AbstractPlatform } from "@pluv/io";
30
+ import stringify from "fast-json-stable-stringify";
31
+ import { Hono } from "hono";
32
+
33
+ // src/constants.ts
34
+ var SIGNATURE_ALGORITHM = "sha256";
35
+ var SIGNATURE_HEADER = "x-pluv-signature-256";
36
+
37
+ // src/schemas.ts
38
+ import { z } from "zod";
39
+ var ZodEventKind = z.union([
40
+ z.literal("initial-storage"),
41
+ z.literal("room-deleted"),
42
+ z.literal("user-connected"),
43
+ z.literal("user-disconnected")
44
+ ]);
45
+ var ZodEventInitialStorage = z.object({
46
+ event: z.literal("initial-storage"),
47
+ data: z.object({
48
+ room: z.string()
49
+ })
50
+ });
51
+ var ZodEventRoomDeleted = z.object({
52
+ event: z.literal("room-deleted"),
53
+ data: z.object({
54
+ room: z.string(),
55
+ storage: z.string().nullable()
56
+ })
57
+ });
58
+ var ZodEventUserConnected = z.object({
59
+ event: z.literal("user-connected"),
60
+ data: z.object({
61
+ room: z.string(),
62
+ storage: z.string().nullable(),
63
+ user: z.object({
64
+ id: z.string()
65
+ }).passthrough()
66
+ })
67
+ });
68
+ var ZodEventUserDisconnected = z.object({
69
+ event: z.literal("user-disconnected"),
70
+ data: z.object({
71
+ room: z.string(),
72
+ storage: z.string().nullable(),
73
+ user: z.object({
74
+ id: z.string()
75
+ }).passthrough()
76
+ })
77
+ });
78
+ var ZodEvent = z.discriminatedUnion("event", [
79
+ ZodEventInitialStorage,
80
+ ZodEventRoomDeleted,
81
+ ZodEventUserConnected,
82
+ ZodEventUserDisconnected
83
+ ]);
84
+ var ZodInitialStorageResponse = z.object({
85
+ event: z.literal("initial-storage"),
86
+ room: z.string(),
87
+ storage: z.string().nullable()
88
+ });
89
+ var ZodRoomDeletedResponse = z.object({
90
+ event: z.literal("room-deleted"),
91
+ room: z.string()
92
+ });
93
+ var ZodUserConnectedResponse = z.object({
94
+ event: z.literal("user-connected"),
95
+ room: z.string()
96
+ });
97
+ var ZodUserDisconnectedResponse = z.object({
98
+ event: z.literal("user-disconnected"),
99
+ room: z.string()
100
+ });
101
+ var ZodEventResponse = z.discriminatedUnion("event", [
102
+ ZodInitialStorageResponse,
103
+ ZodRoomDeletedResponse,
104
+ ZodUserConnectedResponse,
105
+ ZodUserDisconnectedResponse
106
+ ]);
107
+
108
+ // src/shared/createErrorResponse.ts
109
+ var createErrorResponse = (c, error, status) => {
110
+ return c.json({ ok: false, error: { message: error.message } }, status);
111
+ };
112
+
113
+ // src/shared/getCrypto.ts
114
+ var getCrypto = () => {
115
+ if (typeof crypto !== "undefined") {
116
+ return crypto;
117
+ }
118
+ if (typeof __require === "function") {
119
+ return __require("crypto").webcrypto;
120
+ }
121
+ throw new Error("Missing crypto module");
122
+ };
123
+
124
+ // src/shared/createHmac.ts
125
+ var createHmac = (params) => __async(null, null, function* () {
126
+ const { payload, secret } = params;
127
+ if (!payload || !secret) throw new Error("Secret and payload are required to sign payload");
128
+ const encoder = new TextEncoder();
129
+ const keyBytes = encoder.encode(secret);
130
+ const crypto2 = getCrypto();
131
+ const algorithm = { name: "HMAC", hash: { name: "SHA-256" } };
132
+ const extractable = false;
133
+ const key = yield crypto2.subtle.importKey("raw", keyBytes, algorithm, extractable, [
134
+ "sign",
135
+ "verify"
136
+ ]);
137
+ const payloadBytes = encoder.encode(payload);
138
+ const signature = yield crypto2.subtle.sign("HMAC", key, payloadBytes);
139
+ const hmac = Array.from(new Uint8Array(signature)).map((b) => ("0" + b.toString(16)).slice(-2)).join("");
140
+ return { algorithm: "sha256", hmac };
141
+ });
142
+
143
+ // src/shared/createSuccessResponse.ts
144
+ var createSuccessResponse = (c, data, status = 200) => {
145
+ return c.json(
146
+ {
147
+ ok: true,
148
+ data: ZodEventResponse.parse(data)
149
+ },
150
+ status
151
+ );
152
+ };
153
+
154
+ // src/shared/HttpError.ts
155
+ var HttpError = class extends Error {
156
+ constructor(message, status) {
157
+ super(message);
158
+ this.status = status;
159
+ }
160
+ };
161
+
162
+ // src/shared/timingSafeEqual.ts
163
+ var timingSafeEqual = (a, b) => {
164
+ if (a.length !== b.length) return false;
165
+ let result = 0;
166
+ for (let i = 0; i < a.length; i++) {
167
+ result |= a[i] ^ b[i];
168
+ }
169
+ return result === 0;
170
+ };
171
+
172
+ // src/shared/verifyWebhook.ts
173
+ var verifyWebhook = (params) => __async(null, null, function* () {
174
+ const { payload, secret, signature } = params;
175
+ if (!secret || !payload || !signature) {
176
+ throw new Error("Secret, payload and signature are required to verify payload");
177
+ }
178
+ const { hmac } = yield createHmac({ payload, secret });
179
+ if (hmac.length !== signature.length) return false;
180
+ const encoder = new TextEncoder();
181
+ const verificationBytes = encoder.encode(hmac);
182
+ const signatureBytes = encoder.encode(signature);
183
+ if (verificationBytes.length !== signatureBytes.length) return false;
184
+ return timingSafeEqual(verificationBytes, signatureBytes);
185
+ });
186
+
187
+ // src/PluvPlatform.ts
188
+ var PluvPlatform = class extends AbstractPlatform {
189
+ constructor(params) {
190
+ var _a, _b;
191
+ super();
192
+ this.id = Math.random().toString();
193
+ this._config = {
194
+ authorize: {
195
+ secret: false
196
+ },
197
+ handleMode: "fetch",
198
+ registrationMode: "attached",
199
+ requireAuth: true,
200
+ listeners: {
201
+ onRoomDeleted: true,
202
+ onRoomMessage: false,
203
+ onStorageUpdated: false,
204
+ onUserConnected: true,
205
+ onUserDisconnected: true
206
+ },
207
+ router: false
208
+ };
209
+ this._name = "platformPluv";
210
+ this._createToken = (params) => __async(this, null, function* () {
211
+ var _a, _b;
212
+ const parsed = params.authorize.user.parse(params.user);
213
+ const [endpoints, publicKey, secretKey] = yield Promise.all([
214
+ typeof this._endpoints === "object" ? this._endpoints : this._endpoints(),
215
+ typeof this._publicKey === "string" ? this._publicKey : this._publicKey(),
216
+ typeof this._secretKey === "string" ? this._secretKey : this._secretKey()
217
+ ]);
218
+ this._logDebug({ endpoints, publicKey, secretKey });
219
+ const res = yield fetch(endpoints.createToken, {
220
+ headers: { "content-type": "application/json" },
221
+ method: "post",
222
+ body: JSON.stringify({
223
+ maxAge: (_a = params.maxAge) != null ? _a : null,
224
+ publicKey,
225
+ room: params.room,
226
+ secretKey,
227
+ user: parsed
228
+ })
229
+ }).catch((error) => {
230
+ this._logDebug(error);
231
+ return null;
232
+ });
233
+ this._logDebug({ response: { status: (_b = res == null ? void 0 : res.status) != null ? _b : null } });
234
+ if (!res || !res.ok || res.status !== 200) {
235
+ throw new Error("Authorization failed");
236
+ }
237
+ const token = yield res.text().catch(() => null);
238
+ this._logDebug({ token });
239
+ if (typeof token !== "string") throw new Error("Authorization failed");
240
+ return token;
241
+ });
242
+ this._webhooksRouter = new Hono().basePath("/").post("/", (c) => __async(this, null, function* () {
243
+ var _a, _b, _c, _d, _e, _f, _g;
244
+ const [algorithm, signature] = (_b = (_a = c.req.header(SIGNATURE_HEADER)) == null ? void 0 : _a.split("=")) != null ? _b : [];
245
+ try {
246
+ if (!this._webhookSecret) throw new HttpError("Unauthorized", 401);
247
+ if (algorithm !== SIGNATURE_ALGORITHM) throw new HttpError("Unauthorized", 401);
248
+ if (!signature) throw new HttpError("Unauthorized", 401);
249
+ const [payload, webhookSecret] = yield Promise.all([
250
+ c.req.json(),
251
+ typeof this._webhookSecret === "string" ? this._webhookSecret : yield this._webhookSecret()
252
+ ]);
253
+ const verified = yield verifyWebhook({
254
+ payload: stringify(payload),
255
+ signature,
256
+ secret: webhookSecret
257
+ });
258
+ if (!verified) throw new HttpError("Unauthorized", 401);
259
+ const parsed = ZodEvent.safeParse(payload);
260
+ if (!parsed.success) throw new HttpError("Invalid request", 400);
261
+ const { event, data } = parsed.data;
262
+ const context = this._context;
263
+ switch (event) {
264
+ case "initial-storage": {
265
+ const room = data.room;
266
+ const storage = typeof room === "string" ? (_d = yield (_c = this._getInitialStorage) == null ? void 0 : _c.call(this, { context, room })) != null ? _d : null : null;
267
+ return createSuccessResponse(c, { event, room, storage });
268
+ }
269
+ case "room-deleted": {
270
+ const room = data.room;
271
+ const encodedState = data.storage;
272
+ yield Promise.resolve(
273
+ (_e = this._listeners) == null ? void 0 : _e.onRoomDeleted({ context, encodedState, room })
274
+ );
275
+ return createSuccessResponse(c, { event, room });
276
+ }
277
+ case "user-connected": {
278
+ const room = data.room;
279
+ const encodedState = data.storage;
280
+ const user = data.user;
281
+ yield Promise.resolve(
282
+ (_f = this._listeners) == null ? void 0 : _f.onUserConnected({
283
+ context,
284
+ encodedState,
285
+ platform: this,
286
+ room,
287
+ user
288
+ })
289
+ );
290
+ return createSuccessResponse(c, { event, room });
291
+ }
292
+ case "user-disconnected": {
293
+ const room = data.room;
294
+ const encodedState = data.storage;
295
+ const user = data.user;
296
+ yield Promise.resolve(
297
+ (_g = this._listeners) == null ? void 0 : _g.onUserDisconnected({
298
+ context,
299
+ encodedState,
300
+ platform: this,
301
+ room,
302
+ user
303
+ })
304
+ );
305
+ return createSuccessResponse(c, { event, room });
306
+ }
307
+ default: {
308
+ throw new HttpError("Unknown event", 400);
309
+ }
310
+ }
311
+ } catch (error) {
312
+ const message = error instanceof Error ? error.message : "Unexpected error";
313
+ const status = error instanceof HttpError ? error.status : 500;
314
+ return createErrorResponse(c, { message }, status);
315
+ }
316
+ }));
317
+ const { _defs, basePath, context, publicKey, secretKey, webhookSecret } = params;
318
+ this._basePath = basePath;
319
+ this._context = typeof context === "function" ? context(this._roomContext) : context;
320
+ this._debug = (_a = _defs == null ? void 0 : _defs.debug) != null ? _a : false;
321
+ this._endpoints = (_b = _defs == null ? void 0 : _defs.endpoints) != null ? _b : {
322
+ createToken: "https://rooms.pluv.io/api/room/token"
323
+ };
324
+ this._publicKey = publicKey;
325
+ this._secretKey = secretKey;
326
+ this._webhookSecret = webhookSecret;
327
+ this._app = new Hono().basePath(this._basePath).route("/", this._webhooksRouter);
328
+ this._fetch = this._app.fetch;
329
+ }
330
+ acceptWebSocket(webSocket) {
331
+ throw new Error("Not implemented");
332
+ }
333
+ convertWebSocket(webSocket, config) {
334
+ throw new Error("Not implemented");
335
+ }
336
+ getLastPing(webSocket) {
337
+ throw new Error("Not implemented");
338
+ }
339
+ getSerializedState(webSocket) {
340
+ throw new Error("Not implemented");
341
+ }
342
+ getSessionId(webSocket) {
343
+ throw new Error("Not implemented");
344
+ }
345
+ getWebSockets() {
346
+ throw new Error("Not implemented");
347
+ }
348
+ initialize(config) {
349
+ throw new Error("Not implemented");
350
+ }
351
+ parseData(data) {
352
+ throw new Error("Not implemented");
353
+ }
354
+ randomUUID() {
355
+ throw new Error("Not implemented");
356
+ }
357
+ setSerializedState(webSocket, state) {
358
+ throw new Error("Not implemented");
359
+ }
360
+ validateConfig(config) {
361
+ if (!config.authorize)
362
+ throw new Error("Config `authorize` must be provided to `platformPluv`");
363
+ if (!!config.onRoomMessage)
364
+ throw new Error("Config `onRoomMessage` is not supported on `platformPluv`");
365
+ if (!!config.onStorageUpdated)
366
+ throw new Error("Config `onStorageUpdated` is not supported on `platformPluv`");
367
+ this._getInitialStorage = config.getInitialStorage;
368
+ this._listeners = {
369
+ onRoomDeleted: (event) => {
370
+ var _a;
371
+ return (_a = config.onRoomDeleted) == null ? void 0 : _a.call(config, event);
372
+ },
373
+ onUserConnected: (event) => {
374
+ var _a;
375
+ return (_a = config.onUserConnected) == null ? void 0 : _a.call(config, event);
376
+ },
377
+ onUserDisconnected: (event) => {
378
+ var _a;
379
+ return (_a = config.onUserDisconnected) == null ? void 0 : _a.call(config, event);
380
+ }
381
+ };
382
+ }
383
+ _logDebug(...args) {
384
+ if (!this._debug) return;
385
+ console.log("[PLATFORM PLUV]", ...args);
386
+ }
387
+ };
388
+
389
+ // src/platformPluv.ts
390
+ var platformPluv = (config) => {
391
+ const { authorize, context, crdt, debug } = config;
392
+ return {
393
+ authorize,
394
+ context,
395
+ crdt,
396
+ debug,
397
+ platform: () => new PluvPlatform(config)
398
+ };
399
+ };
400
+ export {
401
+ PluvPlatform,
402
+ platformPluv
403
+ };
@@ -0,0 +1,4 @@
1
+ import { config } from "eslint-config-pluv/base";
2
+
3
+ /** @type {import("eslint").Linter.Config} */
4
+ export default config;
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@pluv/platform-pluv",
3
+ "version": "0.0.0-experimental-20250527040415",
4
+ "description": "@pluv/io adapter for pluv.io",
5
+ "author": "leedavidcs",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/pluv-io/pluv",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/pluv-io/pluv.git",
11
+ "directory": "packages/platform-pluv"
12
+ },
13
+ "main": "./dist/index.js",
14
+ "module": "./dist/index.mjs",
15
+ "types": "./dist/index.d.ts",
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "dependencies": {
20
+ "@types/node": "^22.15.21",
21
+ "fast-json-stable-stringify": "^2.1.0",
22
+ "hono": "^4.7.10",
23
+ "zod": "^3.25.17",
24
+ "@pluv/crdt": "^0.0.0-experimental-20250527040415",
25
+ "@pluv/io": "^0.0.0-experimental-20250527040415",
26
+ "@pluv/types": "^0.0.0-experimental-20250527040415"
27
+ },
28
+ "devDependencies": {
29
+ "eslint": "^9.27.0",
30
+ "tsup": "^8.5.0",
31
+ "typescript": "^5.8.3",
32
+ "@pluv/tsconfig": "^0.0.0-experimental-20250527040415",
33
+ "eslint-config-pluv": "^0.0.0-experimental-20250527040415"
34
+ },
35
+ "scripts": {
36
+ "build": "tsup src/index.ts --format esm,cjs --dts",
37
+ "dev": "tsup src/index.ts --format esm,cjs --watch --dts",
38
+ "lint": "eslint \"src/**/*.ts*\" --fix --max-warnings 0",
39
+ "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
40
+ }
41
+ }