@pluv/platform-pluv 0.32.5 → 0.32.6
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 +9 -9
- package/CHANGELOG.md +57 -0
- package/dist/index.d.mts +31 -5
- package/dist/index.d.ts +31 -5
- package/dist/index.js +123 -9
- package/dist/index.mjs +129 -9
- package/package.json +9 -9
- package/src/PluvIO.ts +84 -15
- package/src/constants.ts +1 -0
- package/src/schemas.ts +32 -1
- package/src/shared/createHmac.ts +38 -0
- package/src/shared/getCrypto.ts +16 -0
- package/src/shared/index.ts +8 -0
- package/src/shared/signWebhook.ts +10 -0
- package/src/shared/timingSafeEqual.ts +10 -0
- package/src/shared/verifyWebhook.ts +28 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
|
|
2
|
-
> @pluv/platform-pluv@0.32.
|
|
2
|
+
> @pluv/platform-pluv@0.32.6 build /home/runner/work/pluv/pluv/packages/platform-pluv
|
|
3
3
|
> tsup src/index.ts --format esm,cjs --dts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
6
6
|
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
|
-
[34mCLI[39m tsup v8.3.
|
|
7
|
+
[34mCLI[39m tsup v8.3.5
|
|
8
8
|
[34mCLI[39m Target: es6
|
|
9
9
|
[34mESM[39m Build start
|
|
10
10
|
[34mCJS[39m Build start
|
|
11
|
-
[32mESM[39m [1mdist/index.mjs [22m[
|
|
12
|
-
[32mESM[39m ⚡️ Build success in
|
|
13
|
-
[32mCJS[39m [1mdist/index.js [22m[
|
|
14
|
-
[32mCJS[39m ⚡️ Build success in
|
|
11
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m8.83 KB[39m
|
|
12
|
+
[32mESM[39m ⚡️ Build success in 80ms
|
|
13
|
+
[32mCJS[39m [1mdist/index.js [22m[32m9.70 KB[39m
|
|
14
|
+
[32mCJS[39m ⚡️ Build success in 81ms
|
|
15
15
|
[34mDTS[39m Build start
|
|
16
|
-
[32mDTS[39m ⚡️ Build success in
|
|
17
|
-
[32mDTS[39m [1mdist/index.d.mts [22m[
|
|
18
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
16
|
+
[32mDTS[39m ⚡️ Build success in 6208ms
|
|
17
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m3.54 KB[39m
|
|
18
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m3.54 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# @pluv/platform-pluv
|
|
2
2
|
|
|
3
|
+
## 0.32.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 25292d4: Fix event payload types to only contain serializable fields.
|
|
8
|
+
- c0956e7: Add `onUserConnected` and `onUserDisconnected` events on `PluvServer`.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { createIO } from "@pluv/io";
|
|
12
|
+
|
|
13
|
+
const io = createIO({
|
|
14
|
+
/* ... */
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const ioServer = io.server({
|
|
18
|
+
// ...
|
|
19
|
+
onUserConnected: (event) => {
|
|
20
|
+
// ...
|
|
21
|
+
},
|
|
22
|
+
onUserDisconnected: (event) => {
|
|
23
|
+
// ...
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- fc83a44: Enable `onUserConnected` and `onUserDisconnected` event listeners.
|
|
29
|
+
- be1488f: Validate webhook signatures via webhook secrets.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { createIO } from "@pluv/platform-pluv";
|
|
33
|
+
|
|
34
|
+
const io = createIO({
|
|
35
|
+
// ...
|
|
36
|
+
// If you provide a webhookSecret
|
|
37
|
+
webhookSecret: "whsec_...",
|
|
38
|
+
|
|
39
|
+
// The following properties will be made available to configure
|
|
40
|
+
getInitialStorage: (event) => {
|
|
41
|
+
/* ... */
|
|
42
|
+
},
|
|
43
|
+
onRoomDeleted: (event) => {
|
|
44
|
+
/* ... */
|
|
45
|
+
},
|
|
46
|
+
onUserConnected: (event) => {
|
|
47
|
+
/* ... */
|
|
48
|
+
},
|
|
49
|
+
onUserDisconnected: (event) => {
|
|
50
|
+
/* ... */
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- Updated dependencies [c0956e7]
|
|
56
|
+
- @pluv/io@0.32.6
|
|
57
|
+
- @pluv/crdt@0.32.6
|
|
58
|
+
- @pluv/types@0.32.6
|
|
59
|
+
|
|
3
60
|
## 0.32.5
|
|
4
61
|
|
|
5
62
|
### Patch Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { AbstractPlatform, WebSocketRegistrationMode, AbstractWebSocket, ConvertWebSocketConfig, WebSocketSerializedState, AbstractPlatformConfig,
|
|
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';
|
|
5
4
|
|
|
@@ -25,7 +24,34 @@ interface PluvAuthorize<TUser extends BaseUser> {
|
|
|
25
24
|
interface PluvIOEndpoints {
|
|
26
25
|
createToken: string;
|
|
27
26
|
}
|
|
28
|
-
type
|
|
27
|
+
type RoomDeletedMessageEventData = {
|
|
28
|
+
encodedState: string | null;
|
|
29
|
+
room: string;
|
|
30
|
+
};
|
|
31
|
+
type UserConnectedEventData<TUser extends BaseUser> = {
|
|
32
|
+
encodedState: string | null;
|
|
33
|
+
room: string;
|
|
34
|
+
user: TUser;
|
|
35
|
+
};
|
|
36
|
+
type UserDisconnectedEventData<TUser extends BaseUser> = {
|
|
37
|
+
encodedState: string | null;
|
|
38
|
+
room: string;
|
|
39
|
+
user: TUser;
|
|
40
|
+
};
|
|
41
|
+
type PluvIOListeners<TUser extends BaseUser> = {
|
|
42
|
+
getInitialStorage?: GetInitialStorageFn;
|
|
43
|
+
onRoomDeleted: (event: RoomDeletedMessageEventData) => void;
|
|
44
|
+
onUserConnected: (event: UserConnectedEventData<TUser>) => void;
|
|
45
|
+
onUserDisconnected: (event: UserDisconnectedEventData<TUser>) => void;
|
|
46
|
+
};
|
|
47
|
+
type WebhooksConfig<TUser extends BaseUser> = ({
|
|
48
|
+
webhookSecret?: undefined;
|
|
49
|
+
} & {
|
|
50
|
+
[P in keyof PluvIOListeners<TUser>]?: undefined;
|
|
51
|
+
}) | ({
|
|
52
|
+
webhookSecret: string;
|
|
53
|
+
} & Partial<PluvIOListeners<TUser>>);
|
|
54
|
+
type PluvIOConfig<TUser extends BaseUser> = WebhooksConfig<TUser> & {
|
|
29
55
|
/**
|
|
30
56
|
* @ignore
|
|
31
57
|
* @readonly
|
|
@@ -38,7 +64,6 @@ type PluvIOConfig<TUser extends BaseUser> = Partial<Pick<PluvIOListeners<PluvPla
|
|
|
38
64
|
user: InputZodLike<TUser>;
|
|
39
65
|
};
|
|
40
66
|
basePath: string;
|
|
41
|
-
getInitialStorage?: GetInitialStorageFn<PluvPlatform>;
|
|
42
67
|
publicKey: string;
|
|
43
68
|
secretKey: string;
|
|
44
69
|
};
|
|
@@ -50,6 +75,7 @@ declare class PluvIO<TUser extends BaseUser> implements IOLike<PluvAuthorize<TUs
|
|
|
50
75
|
private readonly _listeners;
|
|
51
76
|
private readonly _publicKey;
|
|
52
77
|
private readonly _secretKey;
|
|
78
|
+
private readonly _webhookSecret?;
|
|
53
79
|
/**
|
|
54
80
|
* @ignore
|
|
55
81
|
* @readonly
|
|
@@ -62,7 +88,7 @@ declare class PluvIO<TUser extends BaseUser> implements IOLike<PluvAuthorize<TUs
|
|
|
62
88
|
readonly platform: void;
|
|
63
89
|
};
|
|
64
90
|
get fetch(): (request: Request, Env?: unknown, executionCtx?: hono.ExecutionContext) => Response | Promise<Response>;
|
|
65
|
-
get handler(): (req: Request
|
|
91
|
+
get handler(): (req: Request) => Response | Promise<Response>;
|
|
66
92
|
private _webhooks;
|
|
67
93
|
constructor(options: PluvIOConfig<TUser>);
|
|
68
94
|
createToken(params: JWTEncodeParams<TUser, PluvPlatform>): Promise<string>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { AbstractPlatform, WebSocketRegistrationMode, AbstractWebSocket, ConvertWebSocketConfig, WebSocketSerializedState, AbstractPlatformConfig,
|
|
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';
|
|
5
4
|
|
|
@@ -25,7 +24,34 @@ interface PluvAuthorize<TUser extends BaseUser> {
|
|
|
25
24
|
interface PluvIOEndpoints {
|
|
26
25
|
createToken: string;
|
|
27
26
|
}
|
|
28
|
-
type
|
|
27
|
+
type RoomDeletedMessageEventData = {
|
|
28
|
+
encodedState: string | null;
|
|
29
|
+
room: string;
|
|
30
|
+
};
|
|
31
|
+
type UserConnectedEventData<TUser extends BaseUser> = {
|
|
32
|
+
encodedState: string | null;
|
|
33
|
+
room: string;
|
|
34
|
+
user: TUser;
|
|
35
|
+
};
|
|
36
|
+
type UserDisconnectedEventData<TUser extends BaseUser> = {
|
|
37
|
+
encodedState: string | null;
|
|
38
|
+
room: string;
|
|
39
|
+
user: TUser;
|
|
40
|
+
};
|
|
41
|
+
type PluvIOListeners<TUser extends BaseUser> = {
|
|
42
|
+
getInitialStorage?: GetInitialStorageFn;
|
|
43
|
+
onRoomDeleted: (event: RoomDeletedMessageEventData) => void;
|
|
44
|
+
onUserConnected: (event: UserConnectedEventData<TUser>) => void;
|
|
45
|
+
onUserDisconnected: (event: UserDisconnectedEventData<TUser>) => void;
|
|
46
|
+
};
|
|
47
|
+
type WebhooksConfig<TUser extends BaseUser> = ({
|
|
48
|
+
webhookSecret?: undefined;
|
|
49
|
+
} & {
|
|
50
|
+
[P in keyof PluvIOListeners<TUser>]?: undefined;
|
|
51
|
+
}) | ({
|
|
52
|
+
webhookSecret: string;
|
|
53
|
+
} & Partial<PluvIOListeners<TUser>>);
|
|
54
|
+
type PluvIOConfig<TUser extends BaseUser> = WebhooksConfig<TUser> & {
|
|
29
55
|
/**
|
|
30
56
|
* @ignore
|
|
31
57
|
* @readonly
|
|
@@ -38,7 +64,6 @@ type PluvIOConfig<TUser extends BaseUser> = Partial<Pick<PluvIOListeners<PluvPla
|
|
|
38
64
|
user: InputZodLike<TUser>;
|
|
39
65
|
};
|
|
40
66
|
basePath: string;
|
|
41
|
-
getInitialStorage?: GetInitialStorageFn<PluvPlatform>;
|
|
42
67
|
publicKey: string;
|
|
43
68
|
secretKey: string;
|
|
44
69
|
};
|
|
@@ -50,6 +75,7 @@ declare class PluvIO<TUser extends BaseUser> implements IOLike<PluvAuthorize<TUs
|
|
|
50
75
|
private readonly _listeners;
|
|
51
76
|
private readonly _publicKey;
|
|
52
77
|
private readonly _secretKey;
|
|
78
|
+
private readonly _webhookSecret?;
|
|
53
79
|
/**
|
|
54
80
|
* @ignore
|
|
55
81
|
* @readonly
|
|
@@ -62,7 +88,7 @@ declare class PluvIO<TUser extends BaseUser> implements IOLike<PluvAuthorize<TUs
|
|
|
62
88
|
readonly platform: void;
|
|
63
89
|
};
|
|
64
90
|
get fetch(): (request: Request, Env?: unknown, executionCtx?: hono.ExecutionContext) => Response | Promise<Response>;
|
|
65
|
-
get handler(): (req: Request
|
|
91
|
+
get handler(): (req: Request) => Response | Promise<Response>;
|
|
66
92
|
private _webhooks;
|
|
67
93
|
constructor(options: PluvIOConfig<TUser>);
|
|
68
94
|
createToken(params: JWTEncodeParams<TUser, PluvPlatform>): Promise<string>;
|
package/dist/index.js
CHANGED
|
@@ -62,6 +62,9 @@ module.exports = __toCommonJS(src_exports);
|
|
|
62
62
|
var import_hono = require("hono");
|
|
63
63
|
var import_vercel = require("hono/vercel");
|
|
64
64
|
|
|
65
|
+
// src/constants.ts
|
|
66
|
+
var SIGNATURE_HEADER = "x-pluv-signature-256";
|
|
67
|
+
|
|
65
68
|
// src/schemas.ts
|
|
66
69
|
var import_zod = require("zod");
|
|
67
70
|
var ZodEventInitialStorage = import_zod.z.object({
|
|
@@ -77,15 +80,100 @@ var ZodEventRoomDeleted = import_zod.z.object({
|
|
|
77
80
|
storage: import_zod.z.string().nullable()
|
|
78
81
|
})
|
|
79
82
|
});
|
|
80
|
-
var
|
|
83
|
+
var ZodEventUserConnected = import_zod.z.object({
|
|
84
|
+
event: import_zod.z.literal("user-connected"),
|
|
85
|
+
data: import_zod.z.object({
|
|
86
|
+
room: import_zod.z.string(),
|
|
87
|
+
storage: import_zod.z.string().nullable(),
|
|
88
|
+
user: import_zod.z.object({
|
|
89
|
+
id: import_zod.z.string()
|
|
90
|
+
}).passthrough()
|
|
91
|
+
})
|
|
92
|
+
});
|
|
93
|
+
var ZodEventUserDisconnected = import_zod.z.object({
|
|
94
|
+
event: import_zod.z.literal("user-disconnected"),
|
|
95
|
+
data: import_zod.z.object({
|
|
96
|
+
room: import_zod.z.string(),
|
|
97
|
+
storage: import_zod.z.string().nullable(),
|
|
98
|
+
user: import_zod.z.object({
|
|
99
|
+
id: import_zod.z.string()
|
|
100
|
+
}).passthrough()
|
|
101
|
+
})
|
|
102
|
+
});
|
|
103
|
+
var ZodEvent = import_zod.z.discriminatedUnion("event", [
|
|
104
|
+
ZodEventInitialStorage,
|
|
105
|
+
ZodEventRoomDeleted,
|
|
106
|
+
ZodEventUserConnected,
|
|
107
|
+
ZodEventUserDisconnected
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
// src/shared/getCrypto.ts
|
|
111
|
+
var getCrypto = () => {
|
|
112
|
+
if (typeof crypto !== "undefined") {
|
|
113
|
+
return crypto;
|
|
114
|
+
}
|
|
115
|
+
if (typeof require === "function") {
|
|
116
|
+
return require("crypto").webcrypto;
|
|
117
|
+
}
|
|
118
|
+
throw new Error("Missing crypto module");
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// src/shared/createHmac.ts
|
|
122
|
+
var createHmac = (params) => __async(void 0, null, function* () {
|
|
123
|
+
const { payload, secret } = params;
|
|
124
|
+
if (!payload || !secret) throw new Error("Secret and payload are required to sign payload");
|
|
125
|
+
const encoder = new TextEncoder();
|
|
126
|
+
const keyBytes = encoder.encode(secret);
|
|
127
|
+
const crypto2 = getCrypto();
|
|
128
|
+
const algorithm = { name: "HMAC", hash: { name: "SHA-256" } };
|
|
129
|
+
const extractable = false;
|
|
130
|
+
const key = yield crypto2.subtle.importKey("raw", keyBytes, algorithm, extractable, ["sign", "verify"]);
|
|
131
|
+
const payloadBytes = encoder.encode(payload);
|
|
132
|
+
const signature = yield crypto2.subtle.sign("HMAC", key, payloadBytes);
|
|
133
|
+
const hmac = Array.from(new Uint8Array(signature)).map((b) => ("0" + b.toString(16)).slice(-2)).join("");
|
|
134
|
+
return { algorithm: "sha256", hmac };
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// src/shared/timingSafeEqual.ts
|
|
138
|
+
var timingSafeEqual = (a, b) => {
|
|
139
|
+
if (a.length !== b.length) return false;
|
|
140
|
+
let result = 0;
|
|
141
|
+
for (let i = 0; i < a.length; i++) {
|
|
142
|
+
result |= a[i] ^ b[i];
|
|
143
|
+
}
|
|
144
|
+
return result === 0;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// src/shared/verifyWebhook.ts
|
|
148
|
+
var verifyWebhook = (params) => __async(void 0, null, function* () {
|
|
149
|
+
const { payload, secret, signature } = params;
|
|
150
|
+
if (!secret || !payload || !signature) {
|
|
151
|
+
throw new Error("Secret, payload and signature are required to verify payload");
|
|
152
|
+
}
|
|
153
|
+
const { hmac } = yield createHmac({ payload, secret });
|
|
154
|
+
if (hmac.length !== signature.length) return false;
|
|
155
|
+
const encoder = new TextEncoder();
|
|
156
|
+
const verificationBytes = encoder.encode(hmac);
|
|
157
|
+
const signatureBytes = encoder.encode(signature);
|
|
158
|
+
if (verificationBytes.length !== signatureBytes.length) return false;
|
|
159
|
+
return timingSafeEqual(verificationBytes, signatureBytes);
|
|
160
|
+
});
|
|
81
161
|
|
|
82
162
|
// src/PluvIO.ts
|
|
83
163
|
var PluvIO = class {
|
|
84
164
|
constructor(options) {
|
|
85
165
|
this._webhooks = new import_hono.Hono().basePath("/").post("/event", (c) => __async(this, null, function* () {
|
|
86
166
|
var _a, _b;
|
|
87
|
-
const
|
|
88
|
-
|
|
167
|
+
const signature = c.req.header(SIGNATURE_HEADER);
|
|
168
|
+
if (!this._webhookSecret || !signature) return c.json({ error: "Unauthorized" }, 401);
|
|
169
|
+
const payload = yield c.req.json();
|
|
170
|
+
const verified = yield verifyWebhook({
|
|
171
|
+
payload,
|
|
172
|
+
signature,
|
|
173
|
+
secret: this._webhookSecret
|
|
174
|
+
});
|
|
175
|
+
if (!verified) return c.json({ error: "Unauthorized" }, 401);
|
|
176
|
+
const parsed = ZodEvent.safeParse(payload);
|
|
89
177
|
if (!parsed.success) return c.json({ data: { ok: true } }, 200);
|
|
90
178
|
const { event, data } = parsed.data;
|
|
91
179
|
switch (event) {
|
|
@@ -97,14 +185,37 @@ var PluvIO = class {
|
|
|
97
185
|
case "room-deleted": {
|
|
98
186
|
const room = data.room;
|
|
99
187
|
const encodedState = data.storage;
|
|
100
|
-
yield Promise.resolve(this._listeners.onRoomDeleted({
|
|
188
|
+
yield Promise.resolve(this._listeners.onRoomDeleted({ encodedState, room }));
|
|
101
189
|
return c.json({ data: { room } }, 200);
|
|
102
190
|
}
|
|
191
|
+
case "user-connected": {
|
|
192
|
+
const room = data.room;
|
|
193
|
+
const encodedState = data.storage;
|
|
194
|
+
const user = data.user;
|
|
195
|
+
yield Promise.resolve(this._listeners.onUserConnected({ encodedState, room, user }));
|
|
196
|
+
}
|
|
197
|
+
case "user-disconnected": {
|
|
198
|
+
const room = data.room;
|
|
199
|
+
const encodedState = data.storage;
|
|
200
|
+
const user = data.user;
|
|
201
|
+
yield Promise.resolve(this._listeners.onUserDisconnected({ encodedState, room, user }));
|
|
202
|
+
}
|
|
103
203
|
default:
|
|
104
204
|
return c.json({ data: { ok: true } }, 200);
|
|
105
205
|
}
|
|
106
206
|
}));
|
|
107
|
-
const {
|
|
207
|
+
const {
|
|
208
|
+
_defs,
|
|
209
|
+
authorize,
|
|
210
|
+
basePath,
|
|
211
|
+
getInitialStorage,
|
|
212
|
+
onRoomDeleted,
|
|
213
|
+
onUserConnected,
|
|
214
|
+
onUserDisconnected,
|
|
215
|
+
publicKey,
|
|
216
|
+
secretKey,
|
|
217
|
+
webhookSecret
|
|
218
|
+
} = options;
|
|
108
219
|
this._authorize = {
|
|
109
220
|
required: true,
|
|
110
221
|
secret: "",
|
|
@@ -115,9 +226,14 @@ var PluvIO = class {
|
|
|
115
226
|
createToken: "https://pluv.io/api/room/token"
|
|
116
227
|
}, _defs == null ? void 0 : _defs.endpoints);
|
|
117
228
|
this._getInitialStorage = getInitialStorage;
|
|
118
|
-
this._listeners = {
|
|
229
|
+
this._listeners = {
|
|
230
|
+
onRoomDeleted: (event) => onRoomDeleted == null ? void 0 : onRoomDeleted(event),
|
|
231
|
+
onUserConnected: (event) => onUserConnected == null ? void 0 : onUserConnected(event),
|
|
232
|
+
onUserDisconnected: (event) => onUserDisconnected == null ? void 0 : onUserDisconnected(event)
|
|
233
|
+
};
|
|
119
234
|
this._publicKey = publicKey;
|
|
120
235
|
this._secretKey = secretKey;
|
|
236
|
+
this._webhookSecret = webhookSecret;
|
|
121
237
|
}
|
|
122
238
|
/**
|
|
123
239
|
* @ignore
|
|
@@ -161,9 +277,7 @@ var PluvIO = class {
|
|
|
161
277
|
throw new Error("Authorization failed");
|
|
162
278
|
}
|
|
163
279
|
const token = yield res.text().catch(() => null);
|
|
164
|
-
if (typeof token !== "string")
|
|
165
|
-
throw new Error("Authorization failed");
|
|
166
|
-
}
|
|
280
|
+
if (typeof token !== "string") throw new Error("Authorization failed");
|
|
167
281
|
return token;
|
|
168
282
|
});
|
|
169
283
|
}
|
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
|
|
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
|
|
65
|
-
|
|
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({
|
|
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 {
|
|
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 = {
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pluv/platform-pluv",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.6",
|
|
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.
|
|
21
|
-
"hono": "^4.6.
|
|
20
|
+
"@types/node": "^22.8.1",
|
|
21
|
+
"hono": "^4.6.7",
|
|
22
22
|
"zod": "^3.23.8",
|
|
23
|
-
"@pluv/crdt": "^0.32.
|
|
24
|
-
"@pluv/io": "^0.32.
|
|
25
|
-
"@pluv/types": "^0.32.
|
|
23
|
+
"@pluv/crdt": "^0.32.6",
|
|
24
|
+
"@pluv/io": "^0.32.6",
|
|
25
|
+
"@pluv/types": "^0.32.6"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"eslint": "^8.57.0",
|
|
29
|
-
"tsup": "^8.3.
|
|
29
|
+
"tsup": "^8.3.5",
|
|
30
30
|
"typescript": "^5.6.3",
|
|
31
|
-
"@pluv/tsconfig": "^0.32.
|
|
32
|
-
"eslint-config-pluv": "^0.32.
|
|
31
|
+
"@pluv/tsconfig": "^0.32.6",
|
|
32
|
+
"eslint-config-pluv": "^0.32.6"
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
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
|
|
43
|
-
private readonly _listeners:
|
|
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
|
|
77
|
-
|
|
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({
|
|
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 {
|
|
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 = {
|
|
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
|
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SIGNATURE_HEADER = "x-pluv-signature-256";
|
package/src/schemas.ts
CHANGED
|
@@ -15,4 +15,35 @@ const ZodEventRoomDeleted = z.object({
|
|
|
15
15
|
}),
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
|
|
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
|
+
};
|