@holo-js/broadcast 0.2.6 → 0.3.1
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/auth.d.ts +3 -1
- package/dist/auth.mjs +3 -2
- package/dist/{chunk-TTKGDABI.mjs → chunk-6DCCSFFM.mjs} +97 -4
- package/dist/{chunk-U5JDBKXC.mjs → chunk-JVDFH2TW.mjs} +3 -3
- package/dist/chunk-XLUGF257.mjs +268 -0
- package/dist/client-config.d.ts +3 -3
- package/dist/config.d.ts +138 -0
- package/dist/config.mjs +34 -0
- package/dist/contracts.d.ts +1 -1
- package/dist/index.d.ts +88 -24
- package/dist/index.mjs +328 -227
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.mjs +2 -1
- package/package.json +10 -3
package/dist/index.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
resolveBroadcastChannelGuard,
|
|
7
7
|
resolveBroadcastWhisperSchema,
|
|
8
8
|
validateBroadcastWhisperPayload
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-JVDFH2TW.mjs";
|
|
10
10
|
import {
|
|
11
11
|
renderBroadcastClientConfigResponse,
|
|
12
12
|
resolveBroadcastClientConfig
|
|
@@ -21,10 +21,17 @@ import {
|
|
|
21
21
|
getBroadcastRuntimeBindings,
|
|
22
22
|
getRegisteredBroadcastDriver,
|
|
23
23
|
listRegisteredBroadcastDrivers,
|
|
24
|
+
loadBroadcastPluginDrivers,
|
|
24
25
|
registerBroadcastDriver,
|
|
25
26
|
resetBroadcastDriverRegistry,
|
|
27
|
+
resetBroadcastPluginDrivers,
|
|
26
28
|
resetBroadcastRuntime
|
|
27
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-6DCCSFFM.mjs";
|
|
30
|
+
import {
|
|
31
|
+
defineBroadcastConfig,
|
|
32
|
+
holoBroadcastDefaults,
|
|
33
|
+
normalizeBroadcastConfig
|
|
34
|
+
} from "./chunk-XLUGF257.mjs";
|
|
28
35
|
import {
|
|
29
36
|
broadcastInternals,
|
|
30
37
|
channel,
|
|
@@ -40,23 +47,22 @@ import {
|
|
|
40
47
|
} from "./chunk-HE6HN7ID.mjs";
|
|
41
48
|
|
|
42
49
|
// src/worker.ts
|
|
43
|
-
import { createHash, createHmac, randomInt, randomUUID, timingSafeEqual } from "crypto";
|
|
50
|
+
import { createHash, createHmac as createHmac2, randomInt, randomUUID, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
44
51
|
import { createServer } from "http";
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
|
|
53
|
+
// src/workerProtocol.ts
|
|
54
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
55
|
+
function normalizeWorkerRequiredString(value, label) {
|
|
47
56
|
const normalized = value.trim();
|
|
48
57
|
if (!normalized) {
|
|
49
58
|
throw new Error(`[@holo-js/broadcast] ${label} must be a non-empty string.`);
|
|
50
59
|
}
|
|
51
60
|
return normalized;
|
|
52
61
|
}
|
|
53
|
-
function
|
|
54
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
55
|
-
}
|
|
56
|
-
function parseSocketMessage(rawMessage) {
|
|
62
|
+
function parseWorkerSocketMessage(rawMessage) {
|
|
57
63
|
const message = parseJsonObject(rawMessage, "Websocket message");
|
|
58
|
-
const event =
|
|
59
|
-
const channel2 = typeof message.channel === "string" ?
|
|
64
|
+
const event = normalizeWorkerRequiredString(String(message.event ?? ""), "Websocket event");
|
|
65
|
+
const channel2 = typeof message.channel === "string" ? normalizeWorkerRequiredString(message.channel, "Websocket channel") : void 0;
|
|
60
66
|
const data = typeof message.data === "string" ? parseJsonObject(message.data, "Websocket message data") : isPlainObject(message.data) ? message.data : {};
|
|
61
67
|
return Object.freeze({
|
|
62
68
|
event,
|
|
@@ -64,41 +70,12 @@ function parseSocketMessage(rawMessage) {
|
|
|
64
70
|
data
|
|
65
71
|
});
|
|
66
72
|
}
|
|
67
|
-
function
|
|
68
|
-
if (value === "query" || value === "mutation" || value === "subscribe" || value === "unsubscribe") {
|
|
69
|
-
return value;
|
|
70
|
-
}
|
|
71
|
-
throw new Error("[@holo-js/broadcast] Realtime action is invalid.");
|
|
72
|
-
}
|
|
73
|
-
function normalizeRealtimeArgs(value) {
|
|
74
|
-
if (!isPlainObject(value)) {
|
|
75
|
-
return {};
|
|
76
|
-
}
|
|
77
|
-
return value;
|
|
78
|
-
}
|
|
79
|
-
function isRecord(value) {
|
|
80
|
-
return isObjectRecord(value);
|
|
81
|
-
}
|
|
82
|
-
function parseRealtimeSocketMessage(data) {
|
|
83
|
-
const id = normalizeRequiredString(String(data.id ?? ""), "Realtime request id");
|
|
84
|
-
const action = normalizeRealtimeAction(data.action);
|
|
85
|
-
const name = typeof data.name === "string" ? normalizeRequiredString(data.name, "Realtime definition name") : void 0;
|
|
86
|
-
if (action !== "unsubscribe" && !name) {
|
|
87
|
-
throw new Error("[@holo-js/broadcast] Realtime definition name is required.");
|
|
88
|
-
}
|
|
89
|
-
return Object.freeze({
|
|
90
|
-
id,
|
|
91
|
-
action,
|
|
92
|
-
...typeof name === "undefined" ? {} : { name },
|
|
93
|
-
args: normalizeRealtimeArgs(data.args)
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
function normalizePublishBody(value) {
|
|
73
|
+
function normalizeWorkerPublishBody(value) {
|
|
97
74
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
98
75
|
throw new Error("[@holo-js/broadcast] Publish payload must be a JSON object.");
|
|
99
76
|
}
|
|
100
77
|
const body = value;
|
|
101
|
-
const name = typeof body.name === "string" ?
|
|
78
|
+
const name = typeof body.name === "string" ? normalizeWorkerRequiredString(body.name, "Publish name") : typeof body.event === "string" ? normalizeWorkerRequiredString(body.event, "Publish event") : "";
|
|
102
79
|
if (!name) {
|
|
103
80
|
throw new Error("[@holo-js/broadcast] Publish payload must include an event name.");
|
|
104
81
|
}
|
|
@@ -106,13 +83,13 @@ function normalizePublishBody(value) {
|
|
|
106
83
|
if (typeof channel2 !== "string") {
|
|
107
84
|
throw new Error("[@holo-js/broadcast] Publish channel must be a non-empty string.");
|
|
108
85
|
}
|
|
109
|
-
return
|
|
110
|
-
}) : typeof body.channel === "string" ? [
|
|
86
|
+
return normalizeWorkerRequiredString(channel2, "Publish channel");
|
|
87
|
+
}) : typeof body.channel === "string" ? [normalizeWorkerRequiredString(body.channel, "Publish channel")] : [];
|
|
111
88
|
if (channels.length === 0) {
|
|
112
89
|
throw new Error("[@holo-js/broadcast] Publish payload must include at least one channel.");
|
|
113
90
|
}
|
|
114
91
|
const data = typeof body.data === "string" ? body.data : JSON.stringify(body.data ?? {});
|
|
115
|
-
const socketId = typeof body.socket_id === "string" ?
|
|
92
|
+
const socketId = typeof body.socket_id === "string" ? normalizeWorkerRequiredString(body.socket_id, "Publish socket_id") : void 0;
|
|
116
93
|
return Object.freeze({
|
|
117
94
|
name,
|
|
118
95
|
channels: Object.freeze(channels),
|
|
@@ -120,19 +97,178 @@ function normalizePublishBody(value) {
|
|
|
120
97
|
...typeof socketId === "undefined" ? {} : { socket_id: socketId }
|
|
121
98
|
});
|
|
122
99
|
}
|
|
123
|
-
function
|
|
100
|
+
function createWorkerPusherSignature(secret, method, pathname, params) {
|
|
124
101
|
const sorted = [...params.entries()].filter(([key]) => key !== "auth_signature").sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
|
|
125
102
|
const payload = `${method.toUpperCase()}
|
|
126
103
|
${pathname}
|
|
127
104
|
${sorted}`;
|
|
128
105
|
return createHmac("sha256", secret).update(payload).digest("hex");
|
|
129
106
|
}
|
|
130
|
-
function
|
|
107
|
+
function verifyWorkerPusherSignature(providedSignature, expectedSignature) {
|
|
131
108
|
if (providedSignature.length !== expectedSignature.length || !/^[a-f0-9]+$/i.test(providedSignature) || !/^[a-f0-9]+$/i.test(expectedSignature)) {
|
|
132
109
|
return false;
|
|
133
110
|
}
|
|
134
111
|
return timingSafeEqual(Buffer.from(providedSignature, "hex"), Buffer.from(expectedSignature, "hex"));
|
|
135
112
|
}
|
|
113
|
+
function parseWorkerChannelKind(channel2) {
|
|
114
|
+
if (channel2.startsWith("private-")) {
|
|
115
|
+
return Object.freeze({
|
|
116
|
+
kind: "private",
|
|
117
|
+
canonical: channel2.slice("private-".length)
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (channel2.startsWith("presence-")) {
|
|
121
|
+
return Object.freeze({
|
|
122
|
+
kind: "presence",
|
|
123
|
+
canonical: channel2.slice("presence-".length)
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return Object.freeze({
|
|
127
|
+
kind: "public",
|
|
128
|
+
canonical: channel2
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/workerNodeTransport.ts
|
|
133
|
+
var BroadcastPayloadTooLargeError = class extends Error {
|
|
134
|
+
};
|
|
135
|
+
async function readLimitedRequestText(request, maxBytes) {
|
|
136
|
+
const contentLength = Number(request.headers.get("content-length"));
|
|
137
|
+
if (Number.isFinite(contentLength) && contentLength > maxBytes) {
|
|
138
|
+
throw new BroadcastPayloadTooLargeError();
|
|
139
|
+
}
|
|
140
|
+
if (!request.body) return "";
|
|
141
|
+
const reader = request.body.getReader();
|
|
142
|
+
const chunks = [];
|
|
143
|
+
let size = 0;
|
|
144
|
+
while (true) {
|
|
145
|
+
const result = await reader.read();
|
|
146
|
+
if (result.done) break;
|
|
147
|
+
size += result.value.byteLength;
|
|
148
|
+
if (size > maxBytes) {
|
|
149
|
+
await reader.cancel();
|
|
150
|
+
throw new BroadcastPayloadTooLargeError();
|
|
151
|
+
}
|
|
152
|
+
chunks.push(result.value);
|
|
153
|
+
}
|
|
154
|
+
const bytes = new Uint8Array(size);
|
|
155
|
+
let offset = 0;
|
|
156
|
+
for (const chunk of chunks) {
|
|
157
|
+
bytes.set(chunk, offset);
|
|
158
|
+
offset += chunk.byteLength;
|
|
159
|
+
}
|
|
160
|
+
return new TextDecoder().decode(bytes);
|
|
161
|
+
}
|
|
162
|
+
function toNodeHeaders(headers) {
|
|
163
|
+
const normalized = new Headers();
|
|
164
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
165
|
+
if (typeof value === "undefined") continue;
|
|
166
|
+
if (Array.isArray(value)) {
|
|
167
|
+
for (const item of value) normalized.append(key, item);
|
|
168
|
+
} else {
|
|
169
|
+
normalized.set(key, value);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return normalized;
|
|
173
|
+
}
|
|
174
|
+
function toNodeRequestUrl(request, fallbackHost) {
|
|
175
|
+
const path = request.url ?? "/";
|
|
176
|
+
const host = request.headers.host ?? fallbackHost;
|
|
177
|
+
return `http://${host}${path}`;
|
|
178
|
+
}
|
|
179
|
+
async function readNodeRequestBody(request, maxBytes) {
|
|
180
|
+
if (request.method === "GET" || request.method === "HEAD") return void 0;
|
|
181
|
+
const chunks = [];
|
|
182
|
+
let size = 0;
|
|
183
|
+
for await (const chunk of request) {
|
|
184
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
185
|
+
size += buffer.byteLength;
|
|
186
|
+
if (size > maxBytes) throw new BroadcastPayloadTooLargeError();
|
|
187
|
+
chunks.push(buffer);
|
|
188
|
+
}
|
|
189
|
+
return chunks.length === 0 ? void 0 : Buffer.concat(chunks);
|
|
190
|
+
}
|
|
191
|
+
function writeNodeRequestBodyError(response, error) {
|
|
192
|
+
if (error instanceof BroadcastPayloadTooLargeError) {
|
|
193
|
+
response.statusCode = 413;
|
|
194
|
+
response.end("Payload Too Large");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
response.statusCode = 500;
|
|
198
|
+
response.end("Internal Server Error");
|
|
199
|
+
}
|
|
200
|
+
async function writeNodeResponse(response, value) {
|
|
201
|
+
response.statusCode = value.status;
|
|
202
|
+
response.statusMessage = value.statusText;
|
|
203
|
+
value.headers.forEach((headerValue, headerName) => response.setHeader(headerName, headerValue));
|
|
204
|
+
response.end(Buffer.from(await value.arrayBuffer()));
|
|
205
|
+
}
|
|
206
|
+
function decodeNodeWebSocketMessage(message) {
|
|
207
|
+
if (typeof message === "string") return message;
|
|
208
|
+
if (message instanceof ArrayBuffer) return new TextDecoder().decode(new Uint8Array(message));
|
|
209
|
+
if (Array.isArray(message)) return Buffer.concat(message).toString("utf8");
|
|
210
|
+
if (message instanceof Uint8Array) return Buffer.from(message).toString("utf8");
|
|
211
|
+
return String(message);
|
|
212
|
+
}
|
|
213
|
+
function getNodeWebSocketMessageBytes(message) {
|
|
214
|
+
if (typeof message === "string") return Buffer.byteLength(message);
|
|
215
|
+
if (message instanceof ArrayBuffer || message instanceof Uint8Array) return message.byteLength;
|
|
216
|
+
return message.reduce((size, chunk) => size + chunk.byteLength, 0);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/worker.ts
|
|
220
|
+
function createScalingMessageListener(runtime) {
|
|
221
|
+
return (payload) => {
|
|
222
|
+
void runtime.receiveScalingMessage(payload).catch((error) => {
|
|
223
|
+
logScalingMessageError(error);
|
|
224
|
+
});
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function isAllowedWorkerOrigin(request, allowedOrigins) {
|
|
228
|
+
const origin = request.headers.get("origin");
|
|
229
|
+
if (!origin) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const normalized = new URL(origin).origin;
|
|
234
|
+
return allowedOrigins.includes("*") || allowedOrigins.includes(normalized);
|
|
235
|
+
} catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
var MAX_PUBLISH_TIMESTAMP_SKEW_SECONDS = 300;
|
|
240
|
+
function escapeRegExp(value) {
|
|
241
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
242
|
+
}
|
|
243
|
+
function normalizeRealtimeAction(value) {
|
|
244
|
+
if (value === "query" || value === "mutation" || value === "subscribe" || value === "unsubscribe") {
|
|
245
|
+
return value;
|
|
246
|
+
}
|
|
247
|
+
throw new Error("[@holo-js/broadcast] Realtime action is invalid.");
|
|
248
|
+
}
|
|
249
|
+
function normalizeRealtimeArgs(value) {
|
|
250
|
+
if (!isPlainObject(value)) {
|
|
251
|
+
return {};
|
|
252
|
+
}
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
function isRecord(value) {
|
|
256
|
+
return isObjectRecord(value);
|
|
257
|
+
}
|
|
258
|
+
function parseRealtimeSocketMessage(data) {
|
|
259
|
+
const id = normalizeWorkerRequiredString(String(data.id ?? ""), "Realtime request id");
|
|
260
|
+
const action = normalizeRealtimeAction(data.action);
|
|
261
|
+
const name = typeof data.name === "string" ? normalizeWorkerRequiredString(data.name, "Realtime definition name") : void 0;
|
|
262
|
+
if (action !== "unsubscribe" && !name) {
|
|
263
|
+
throw new Error("[@holo-js/broadcast] Realtime definition name is required.");
|
|
264
|
+
}
|
|
265
|
+
return Object.freeze({
|
|
266
|
+
id,
|
|
267
|
+
action,
|
|
268
|
+
...typeof name === "undefined" ? {} : { name },
|
|
269
|
+
args: normalizeRealtimeArgs(data.args)
|
|
270
|
+
});
|
|
271
|
+
}
|
|
136
272
|
function logSocketMessageError(socketId, error) {
|
|
137
273
|
const message = error instanceof Error ? error.message : String(error);
|
|
138
274
|
console.error(`[@holo-js/broadcast] WebSocket message handling failed for socket "${socketId}": ${message}`);
|
|
@@ -152,18 +288,18 @@ function logRealtimeSubscriptionCleanupError(socketId, subscriptionId, error) {
|
|
|
152
288
|
function safeEqual(left, right) {
|
|
153
289
|
const leftBuffer = Buffer.from(left);
|
|
154
290
|
const rightBuffer = Buffer.from(right);
|
|
155
|
-
return leftBuffer.length === rightBuffer.length &&
|
|
291
|
+
return leftBuffer.length === rightBuffer.length && timingSafeEqual2(leftBuffer, rightBuffer);
|
|
156
292
|
}
|
|
157
293
|
function signChannelAuth(secret, socketId, channel2, channelData) {
|
|
158
294
|
const value = channelData ? `${socketId}:${channel2}:${channelData}` : `${socketId}:${channel2}`;
|
|
159
|
-
return
|
|
295
|
+
return createHmac2("sha256", secret).update(value).digest("hex");
|
|
160
296
|
}
|
|
161
297
|
function parseClientChannelAuth(data) {
|
|
162
298
|
if (typeof data.auth !== "string") {
|
|
163
299
|
return void 0;
|
|
164
300
|
}
|
|
165
301
|
return Object.freeze({
|
|
166
|
-
auth:
|
|
302
|
+
auth: normalizeWorkerRequiredString(data.auth, "Subscription auth"),
|
|
167
303
|
...typeof data.channel_data === "string" ? { channelData: data.channel_data } : {}
|
|
168
304
|
});
|
|
169
305
|
}
|
|
@@ -174,7 +310,7 @@ function parseSignedChannelData(value) {
|
|
|
174
310
|
});
|
|
175
311
|
}
|
|
176
312
|
const data = parseJsonObject(value, "Subscription channel data");
|
|
177
|
-
const whispers = Array.isArray(data.whispers) ? Object.freeze(data.whispers.map((item) =>
|
|
313
|
+
const whispers = Array.isArray(data.whispers) ? Object.freeze(data.whispers.map((item) => normalizeWorkerRequiredString(String(item), "Auth whisper"))) : Object.freeze([]);
|
|
178
314
|
const member = data.member && typeof data.member === "object" && !Array.isArray(data.member) ? Object.freeze(data.member) : void 0;
|
|
179
315
|
return Object.freeze({
|
|
180
316
|
whispers,
|
|
@@ -202,29 +338,11 @@ function unsubscribeRealtimeSubscription(socket, id, subscription) {
|
|
|
202
338
|
function isRealtimeSubscriptionCurrent(socket, id, token) {
|
|
203
339
|
return socket.active && socket.realtimeSubscriptionTokens.get(id) === token;
|
|
204
340
|
}
|
|
205
|
-
function parseChannelKind(channel2) {
|
|
206
|
-
if (channel2.startsWith("private-")) {
|
|
207
|
-
return Object.freeze({
|
|
208
|
-
kind: "private",
|
|
209
|
-
canonical: channel2.slice("private-".length)
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
if (channel2.startsWith("presence-")) {
|
|
213
|
-
return Object.freeze({
|
|
214
|
-
kind: "presence",
|
|
215
|
-
canonical: channel2.slice("presence-".length)
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
return Object.freeze({
|
|
219
|
-
kind: "public",
|
|
220
|
-
canonical: channel2
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
341
|
function createSocketId() {
|
|
224
342
|
return `${randomInt(1, 999999)}.${randomInt(1, 999999)}`;
|
|
225
343
|
}
|
|
226
344
|
function createScalingNodeId() {
|
|
227
|
-
return
|
|
345
|
+
return normalizeWorkerRequiredString(`${process.env.HOSTNAME ?? "node"}-${randomUUID()}`, "Broadcast worker node id");
|
|
228
346
|
}
|
|
229
347
|
function resolveScalingEventChannel(connection) {
|
|
230
348
|
return `holo:broadcast:scaling:${connection}:events`;
|
|
@@ -271,14 +389,15 @@ function parsePresenceHashMembers(values) {
|
|
|
271
389
|
function resolveRedisScalingConnection(queueConfig, connectionName, redisConfig) {
|
|
272
390
|
const queueConnection = queueConfig?.connections[connectionName];
|
|
273
391
|
if (queueConnection?.driver === "redis") {
|
|
392
|
+
const redisQueueConnection = queueConnection;
|
|
274
393
|
return Object.freeze({
|
|
275
|
-
...typeof
|
|
276
|
-
...typeof
|
|
277
|
-
host:
|
|
278
|
-
port:
|
|
279
|
-
username:
|
|
280
|
-
password:
|
|
281
|
-
db:
|
|
394
|
+
...typeof redisQueueConnection.redis.url === "undefined" ? {} : { url: redisQueueConnection.redis.url },
|
|
395
|
+
...typeof redisQueueConnection.redis.clusters === "undefined" ? {} : { clusters: redisQueueConnection.redis.clusters },
|
|
396
|
+
host: redisQueueConnection.redis.host,
|
|
397
|
+
port: redisQueueConnection.redis.port,
|
|
398
|
+
username: redisQueueConnection.redis.username,
|
|
399
|
+
password: redisQueueConnection.redis.password,
|
|
400
|
+
db: redisQueueConnection.redis.db
|
|
282
401
|
});
|
|
283
402
|
}
|
|
284
403
|
const redisConnection = redisConfig?.connections[connectionName];
|
|
@@ -481,7 +600,7 @@ function buildWorkerApps(config) {
|
|
|
481
600
|
continue;
|
|
482
601
|
}
|
|
483
602
|
const holoConnection = connection;
|
|
484
|
-
const authEndpoint = typeof holoConnection.clientOptions.authEndpoint === "string" ?
|
|
603
|
+
const authEndpoint = typeof holoConnection.clientOptions.authEndpoint === "string" ? normalizeWorkerRequiredString(holoConnection.clientOptions.authEndpoint, `Broadcast connection "${name}" authEndpoint`) : void 0;
|
|
485
604
|
if (appsByKey.has(holoConnection.key)) {
|
|
486
605
|
throw new Error(`[@holo-js/broadcast] duplicate broadcast app key "${holoConnection.key}" is already configured.`);
|
|
487
606
|
}
|
|
@@ -506,7 +625,7 @@ function pusherEvent(event, data, channel2) {
|
|
|
506
625
|
});
|
|
507
626
|
}
|
|
508
627
|
async function authenticateSubscription(app, connection, channel2, clientAuth, channelAuth, fetcher) {
|
|
509
|
-
const { kind, canonical } =
|
|
628
|
+
const { kind, canonical } = parseWorkerChannelKind(channel2);
|
|
510
629
|
if (kind === "public") {
|
|
511
630
|
return Object.freeze({
|
|
512
631
|
whispers: Object.freeze([])
|
|
@@ -534,7 +653,7 @@ async function authenticateSubscription(app, connection, channel2, clientAuth, c
|
|
|
534
653
|
throw new Error(`[@holo-js/broadcast] Channel authorization rejected (${response.status}).`);
|
|
535
654
|
}
|
|
536
655
|
const body = await response.json();
|
|
537
|
-
const whispers = Array.isArray(body.whispers) ? Object.freeze(body.whispers.map((value) =>
|
|
656
|
+
const whispers = Array.isArray(body.whispers) ? Object.freeze(body.whispers.map((value) => normalizeWorkerRequiredString(String(value), "Auth whisper"))) : Object.freeze([]);
|
|
538
657
|
const member = body.member && typeof body.member === "object" && !Array.isArray(body.member) ? Object.freeze(body.member) : void 0;
|
|
539
658
|
return Object.freeze({
|
|
540
659
|
whispers,
|
|
@@ -567,6 +686,42 @@ async function authenticateSubscription(app, connection, channel2, clientAuth, c
|
|
|
567
686
|
...authorized.type === "presence" ? { member: authorized.member } : {}
|
|
568
687
|
});
|
|
569
688
|
}
|
|
689
|
+
function resolveRealtimeErrorStatus(error) {
|
|
690
|
+
if (!isRecord(error)) {
|
|
691
|
+
return void 0;
|
|
692
|
+
}
|
|
693
|
+
const decision = isRecord(error.decision) ? error.decision : void 0;
|
|
694
|
+
const status = decision?.status ?? error.status ?? error.statusCode;
|
|
695
|
+
if (typeof status === "number" && Number.isInteger(status) && status >= 400 && status <= 599) {
|
|
696
|
+
return status;
|
|
697
|
+
}
|
|
698
|
+
const name = typeof error.name === "string" ? error.name : "";
|
|
699
|
+
if (name === "RealtimeUnauthorizedError") {
|
|
700
|
+
return 401;
|
|
701
|
+
}
|
|
702
|
+
if (name === "RealtimeForbiddenError") {
|
|
703
|
+
return 403;
|
|
704
|
+
}
|
|
705
|
+
return void 0;
|
|
706
|
+
}
|
|
707
|
+
function resolveRealtimeErrorCode(error) {
|
|
708
|
+
if (!isRecord(error)) {
|
|
709
|
+
return void 0;
|
|
710
|
+
}
|
|
711
|
+
const decision = isRecord(error.decision) ? error.decision : void 0;
|
|
712
|
+
const code = decision?.code ?? error.code;
|
|
713
|
+
return typeof code === "string" ? code : void 0;
|
|
714
|
+
}
|
|
715
|
+
function resolveRealtimeErrorKind(error, status) {
|
|
716
|
+
if (!isRecord(error)) {
|
|
717
|
+
return "runtime";
|
|
718
|
+
}
|
|
719
|
+
const name = typeof error.name === "string" ? error.name : "";
|
|
720
|
+
if (name === "AuthorizationError" || name === "RealtimeUnauthorizedError" || name === "RealtimeForbiddenError" || status === 401 || status === 403 || status === 404) {
|
|
721
|
+
return "authorization";
|
|
722
|
+
}
|
|
723
|
+
return name === "RealtimeAuthUnavailableError" ? "transport" : "runtime";
|
|
724
|
+
}
|
|
570
725
|
function createBroadcastWorkerRuntime(options) {
|
|
571
726
|
const appsByKey = buildWorkerApps(options.config);
|
|
572
727
|
const connectedSockets = /* @__PURE__ */ new Map();
|
|
@@ -576,14 +731,12 @@ function createBroadcastWorkerRuntime(options) {
|
|
|
576
731
|
const presenceSockets = /* @__PURE__ */ new Map();
|
|
577
732
|
const scaling = options.scaling;
|
|
578
733
|
const startedAt = options.now?.() ?? Date.now();
|
|
579
|
-
const scalingUnsubscribe = options.scalingUnsubscribe ? Promise.resolve(options.scalingUnsubscribe) : scaling && options.scalingAutoSubscribe !== false ? scaling.adapter.subscribe(scaling.eventChannel, (
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
});
|
|
583
|
-
}) : Promise.resolve(async () => {
|
|
734
|
+
const scalingUnsubscribe = options.scalingUnsubscribe ? Promise.resolve(options.scalingUnsubscribe) : scaling && options.scalingAutoSubscribe !== false ? scaling.adapter.subscribe(scaling.eventChannel, createScalingMessageListener({
|
|
735
|
+
receiveScalingMessage: handleScalingMessage
|
|
736
|
+
})) : Promise.resolve(async () => {
|
|
584
737
|
});
|
|
585
738
|
function createPresenceSocketRef(channel2, socketId) {
|
|
586
|
-
return scaling &&
|
|
739
|
+
return scaling && parseWorkerChannelKind(channel2).kind === "presence" ? createNodeSocketRef(scaling.nodeId, socketId) : socketId;
|
|
587
740
|
}
|
|
588
741
|
function setPresenceState(key, socketMembers) {
|
|
589
742
|
if (socketMembers.size === 0) {
|
|
@@ -617,7 +770,7 @@ function createBroadcastWorkerRuntime(options) {
|
|
|
617
770
|
if (!scaling) {
|
|
618
771
|
return;
|
|
619
772
|
}
|
|
620
|
-
const { kind } =
|
|
773
|
+
const { kind } = parseWorkerChannelKind(channel2);
|
|
621
774
|
if (kind !== "presence") {
|
|
622
775
|
return;
|
|
623
776
|
}
|
|
@@ -751,42 +904,6 @@ function createBroadcastWorkerRuntime(options) {
|
|
|
751
904
|
}
|
|
752
905
|
socket.send(pusherEvent(event, data));
|
|
753
906
|
}
|
|
754
|
-
function resolveRealtimeErrorStatus(error) {
|
|
755
|
-
if (!isRecord(error)) {
|
|
756
|
-
return void 0;
|
|
757
|
-
}
|
|
758
|
-
const decision = isRecord(error.decision) ? error.decision : void 0;
|
|
759
|
-
const status = decision?.status ?? error.status ?? error.statusCode;
|
|
760
|
-
if (typeof status === "number" && Number.isInteger(status) && status >= 400 && status <= 599) {
|
|
761
|
-
return status;
|
|
762
|
-
}
|
|
763
|
-
const name = typeof error.name === "string" ? error.name : "";
|
|
764
|
-
if (name === "RealtimeUnauthorizedError") {
|
|
765
|
-
return 401;
|
|
766
|
-
}
|
|
767
|
-
if (name === "RealtimeForbiddenError") {
|
|
768
|
-
return 403;
|
|
769
|
-
}
|
|
770
|
-
return void 0;
|
|
771
|
-
}
|
|
772
|
-
function resolveRealtimeErrorCode(error) {
|
|
773
|
-
if (!isRecord(error)) {
|
|
774
|
-
return void 0;
|
|
775
|
-
}
|
|
776
|
-
const decision = isRecord(error.decision) ? error.decision : void 0;
|
|
777
|
-
const code = decision?.code ?? error.code;
|
|
778
|
-
return typeof code === "string" ? code : void 0;
|
|
779
|
-
}
|
|
780
|
-
function resolveRealtimeErrorKind(error, status) {
|
|
781
|
-
if (!isRecord(error)) {
|
|
782
|
-
return "runtime";
|
|
783
|
-
}
|
|
784
|
-
const name = typeof error.name === "string" ? error.name : "";
|
|
785
|
-
if (name === "AuthorizationError" || name === "RealtimeUnauthorizedError" || name === "RealtimeForbiddenError" || status === 401 || status === 403 || status === 404) {
|
|
786
|
-
return "authorization";
|
|
787
|
-
}
|
|
788
|
-
return name === "RealtimeAuthUnavailableError" ? "transport" : "runtime";
|
|
789
|
-
}
|
|
790
907
|
function sendRealtimeError(socket, id, error) {
|
|
791
908
|
const status = resolveRealtimeErrorStatus(error);
|
|
792
909
|
const code = resolveRealtimeErrorCode(error);
|
|
@@ -1015,7 +1132,7 @@ function createBroadcastWorkerRuntime(options) {
|
|
|
1015
1132
|
}
|
|
1016
1133
|
}
|
|
1017
1134
|
async function handleSubscribe(socket, rawChannel, clientAuth) {
|
|
1018
|
-
const channel2 =
|
|
1135
|
+
const channel2 = normalizeWorkerRequiredString(rawChannel, "Subscription channel");
|
|
1019
1136
|
const authorization = await authenticateSubscription(socket.app, socket, channel2, clientAuth, options.channelAuth, options.fetch);
|
|
1020
1137
|
if (!socket.active || connectedSockets.get(socket.socketId) !== socket) {
|
|
1021
1138
|
return;
|
|
@@ -1034,7 +1151,7 @@ function createBroadcastWorkerRuntime(options) {
|
|
|
1034
1151
|
channelWhispers.delete(key);
|
|
1035
1152
|
}
|
|
1036
1153
|
}
|
|
1037
|
-
const { kind } =
|
|
1154
|
+
const { kind } = parseWorkerChannelKind(channel2);
|
|
1038
1155
|
if (kind === "presence") {
|
|
1039
1156
|
const member = authorization.member ?? Object.freeze({ id: socket.socketId });
|
|
1040
1157
|
const synchronized = await synchronizePresenceChannel(
|
|
@@ -1056,7 +1173,7 @@ function createBroadcastWorkerRuntime(options) {
|
|
|
1056
1173
|
socket.send(pusherEvent("pusher_internal:subscription_succeeded", {}, channel2));
|
|
1057
1174
|
}
|
|
1058
1175
|
async function handleUnsubscribe(socket, rawChannel) {
|
|
1059
|
-
const channel2 =
|
|
1176
|
+
const channel2 = normalizeWorkerRequiredString(rawChannel, "Unsubscribe channel");
|
|
1060
1177
|
socket.subscribedChannels.delete(channel2);
|
|
1061
1178
|
const removedPresenceMember = removeSubscriptionLocal(socket.app.appId, socket.socketId, channel2);
|
|
1062
1179
|
if (removedPresenceMember.removed && removedPresenceMember.member) {
|
|
@@ -1069,11 +1186,11 @@ function createBroadcastWorkerRuntime(options) {
|
|
|
1069
1186
|
socket.send(pusherEvent("pusher_internal:unsubscribed", {}, channel2));
|
|
1070
1187
|
}
|
|
1071
1188
|
async function handleClientEvent(socket, message) {
|
|
1072
|
-
const channel2 =
|
|
1189
|
+
const channel2 = normalizeWorkerRequiredString(message.channel ?? "", "Whisper channel");
|
|
1073
1190
|
if (!socket.subscribedChannels.has(channel2)) {
|
|
1074
1191
|
throw new Error(`[@holo-js/broadcast] Socket is not subscribed to "${channel2}".`);
|
|
1075
1192
|
}
|
|
1076
|
-
const { kind, canonical } =
|
|
1193
|
+
const { kind, canonical } = parseWorkerChannelKind(channel2);
|
|
1077
1194
|
if (kind === "public") {
|
|
1078
1195
|
throw new Error("[@holo-js/broadcast] Client events are only allowed on private or presence channels.");
|
|
1079
1196
|
}
|
|
@@ -1092,15 +1209,9 @@ function createBroadcastWorkerRuntime(options) {
|
|
|
1092
1209
|
appId: socket.app.appId,
|
|
1093
1210
|
socket_id: socket.socketId
|
|
1094
1211
|
});
|
|
1095
|
-
await publishToChannels(payload
|
|
1096
|
-
fromScaling: false,
|
|
1097
|
-
shouldReplicate: true
|
|
1098
|
-
});
|
|
1212
|
+
await publishToChannels(payload);
|
|
1099
1213
|
}
|
|
1100
|
-
async function publishToChannels(body
|
|
1101
|
-
fromScaling: false,
|
|
1102
|
-
shouldReplicate: true
|
|
1103
|
-
}) {
|
|
1214
|
+
async function publishToChannels(body) {
|
|
1104
1215
|
let deliveredSockets = 0;
|
|
1105
1216
|
const deliveredChannels = [];
|
|
1106
1217
|
for (const channel2 of body.channels) {
|
|
@@ -1110,9 +1221,7 @@ function createBroadcastWorkerRuntime(options) {
|
|
|
1110
1221
|
}
|
|
1111
1222
|
deliveredSockets += result.deliveredSockets;
|
|
1112
1223
|
}
|
|
1113
|
-
|
|
1114
|
-
await publishScalingEvent(body);
|
|
1115
|
-
}
|
|
1224
|
+
await publishScalingEvent(body);
|
|
1116
1225
|
return Object.freeze({
|
|
1117
1226
|
deliveredChannels: Object.freeze(deliveredChannels),
|
|
1118
1227
|
deliveredSockets
|
|
@@ -1121,19 +1230,27 @@ function createBroadcastWorkerRuntime(options) {
|
|
|
1121
1230
|
async function handlePublishRequest(request) {
|
|
1122
1231
|
const url = new URL(request.url);
|
|
1123
1232
|
const match = url.pathname.match(/\/apps\/([^/]+)\/events$/);
|
|
1124
|
-
const appId =
|
|
1233
|
+
const appId = normalizeWorkerRequiredString(match?.[1] ?? "", "Publish appId");
|
|
1125
1234
|
const app = Object.values(appsByKey).find((candidate) => candidate.appId === appId);
|
|
1126
1235
|
if (!app) {
|
|
1127
1236
|
return new Response("App not found", { status: 404 });
|
|
1128
1237
|
}
|
|
1129
|
-
|
|
1238
|
+
let bodyText;
|
|
1239
|
+
try {
|
|
1240
|
+
bodyText = await readLimitedRequestText(request, options.config.worker.maxRequestBytes);
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
if (error instanceof BroadcastPayloadTooLargeError) {
|
|
1243
|
+
return new Response("Payload Too Large", { status: 413 });
|
|
1244
|
+
}
|
|
1245
|
+
throw error;
|
|
1246
|
+
}
|
|
1130
1247
|
const bodyMd5 = createHash("md5").update(bodyText).digest("hex");
|
|
1131
1248
|
if (url.searchParams.get("body_md5") !== bodyMd5) {
|
|
1132
1249
|
return new Response("Invalid body signature", { status: 401 });
|
|
1133
1250
|
}
|
|
1134
1251
|
let authKey;
|
|
1135
1252
|
try {
|
|
1136
|
-
authKey =
|
|
1253
|
+
authKey = normalizeWorkerRequiredString(url.searchParams.get("auth_key") ?? "", "Publish auth_key");
|
|
1137
1254
|
} catch (error) {
|
|
1138
1255
|
const message = error instanceof Error ? error.message : "Invalid credentials";
|
|
1139
1256
|
return new Response(message, { status: 401 });
|
|
@@ -1144,7 +1261,7 @@ function createBroadcastWorkerRuntime(options) {
|
|
|
1144
1261
|
let authTimestamp;
|
|
1145
1262
|
try {
|
|
1146
1263
|
authTimestamp = Number.parseInt(
|
|
1147
|
-
|
|
1264
|
+
normalizeWorkerRequiredString(url.searchParams.get("auth_timestamp") ?? "", "Publish auth_timestamp"),
|
|
1148
1265
|
10
|
|
1149
1266
|
);
|
|
1150
1267
|
} catch (error) {
|
|
@@ -1161,8 +1278,8 @@ function createBroadcastWorkerRuntime(options) {
|
|
|
1161
1278
|
let providedSignature;
|
|
1162
1279
|
let expectedSignature;
|
|
1163
1280
|
try {
|
|
1164
|
-
providedSignature =
|
|
1165
|
-
expectedSignature =
|
|
1281
|
+
providedSignature = normalizeWorkerRequiredString(url.searchParams.get("auth_signature") ?? "", "Publish auth_signature");
|
|
1282
|
+
expectedSignature = createWorkerPusherSignature(
|
|
1166
1283
|
app.secret,
|
|
1167
1284
|
request.method,
|
|
1168
1285
|
url.pathname,
|
|
@@ -1172,12 +1289,12 @@ function createBroadcastWorkerRuntime(options) {
|
|
|
1172
1289
|
const message = error instanceof Error ? error.message : "Invalid auth signature";
|
|
1173
1290
|
return new Response(message, { status: 401 });
|
|
1174
1291
|
}
|
|
1175
|
-
if (!
|
|
1292
|
+
if (!verifyWorkerPusherSignature(providedSignature, expectedSignature)) {
|
|
1176
1293
|
return new Response("Invalid auth signature", { status: 401 });
|
|
1177
1294
|
}
|
|
1178
1295
|
let publishBody;
|
|
1179
1296
|
try {
|
|
1180
|
-
publishBody =
|
|
1297
|
+
publishBody = normalizeWorkerPublishBody(parseJsonObject(bodyText, "Publish body"));
|
|
1181
1298
|
} catch (error) {
|
|
1182
1299
|
return new Response(error.message, { status: 400 });
|
|
1183
1300
|
}
|
|
@@ -1186,9 +1303,6 @@ function createBroadcastWorkerRuntime(options) {
|
|
|
1186
1303
|
result = await publishToChannels({
|
|
1187
1304
|
...publishBody,
|
|
1188
1305
|
appId: app.appId
|
|
1189
|
-
}, {
|
|
1190
|
-
fromScaling: false,
|
|
1191
|
-
shouldReplicate: true
|
|
1192
1306
|
});
|
|
1193
1307
|
} catch (error) {
|
|
1194
1308
|
const message = error instanceof Error ? error.message : "Broadcast publish failed.";
|
|
@@ -1217,7 +1331,7 @@ function createBroadcastWorkerRuntime(options) {
|
|
|
1217
1331
|
}
|
|
1218
1332
|
});
|
|
1219
1333
|
}
|
|
1220
|
-
if (request.method.toUpperCase() === "GET" && url.pathname === options.config.worker.statsPath) {
|
|
1334
|
+
if (options.config.worker.statsEnabled && request.method.toUpperCase() === "GET" && url.pathname === options.config.worker.statsPath) {
|
|
1221
1335
|
return new Response(JSON.stringify({
|
|
1222
1336
|
...this.getStats()
|
|
1223
1337
|
}), {
|
|
@@ -1258,7 +1372,7 @@ function createBroadcastWorkerRuntime(options) {
|
|
|
1258
1372
|
if (!socket.active || connectedSockets.get(socketId) !== socket) {
|
|
1259
1373
|
return;
|
|
1260
1374
|
}
|
|
1261
|
-
const message =
|
|
1375
|
+
const message = parseWorkerSocketMessage(rawMessage);
|
|
1262
1376
|
if (message.event === "pusher:ping") {
|
|
1263
1377
|
socket.send(pusherEvent("pusher:pong", {}));
|
|
1264
1378
|
return;
|
|
@@ -1320,11 +1434,8 @@ function createBroadcastWorkerRuntime(options) {
|
|
|
1320
1434
|
await Promise.all(scalingCleanupTasks.map(async (task) => {
|
|
1321
1435
|
await task();
|
|
1322
1436
|
}));
|
|
1323
|
-
}).catch((error) => {
|
|
1324
|
-
logSocketMessageError(socket.socketId, error);
|
|
1325
|
-
});
|
|
1326
|
-
socket.pendingMessage = cleanupTask.catch(() => {
|
|
1327
1437
|
});
|
|
1438
|
+
socket.pendingMessage = cleanupTask;
|
|
1328
1439
|
},
|
|
1329
1440
|
getStats() {
|
|
1330
1441
|
return Object.freeze({
|
|
@@ -1355,64 +1466,6 @@ function createBroadcastWorkerRuntime(options) {
|
|
|
1355
1466
|
}
|
|
1356
1467
|
});
|
|
1357
1468
|
}
|
|
1358
|
-
function toNodeHeaders(headers) {
|
|
1359
|
-
const normalized = new Headers();
|
|
1360
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
1361
|
-
if (typeof value === "undefined") {
|
|
1362
|
-
continue;
|
|
1363
|
-
}
|
|
1364
|
-
if (Array.isArray(value)) {
|
|
1365
|
-
for (const item of value) {
|
|
1366
|
-
normalized.append(key, item);
|
|
1367
|
-
}
|
|
1368
|
-
continue;
|
|
1369
|
-
}
|
|
1370
|
-
normalized.set(key, value);
|
|
1371
|
-
}
|
|
1372
|
-
return normalized;
|
|
1373
|
-
}
|
|
1374
|
-
function toNodeRequestUrl(request, fallbackHost) {
|
|
1375
|
-
const path = request.url ?? "/";
|
|
1376
|
-
const host = request.headers.host ?? fallbackHost;
|
|
1377
|
-
return `http://${host}${path}`;
|
|
1378
|
-
}
|
|
1379
|
-
async function readNodeRequestBody(request) {
|
|
1380
|
-
if (request.method === "GET" || request.method === "HEAD") {
|
|
1381
|
-
return void 0;
|
|
1382
|
-
}
|
|
1383
|
-
const chunks = [];
|
|
1384
|
-
for await (const chunk of request) {
|
|
1385
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1386
|
-
}
|
|
1387
|
-
if (chunks.length === 0) {
|
|
1388
|
-
return void 0;
|
|
1389
|
-
}
|
|
1390
|
-
return Buffer.concat(chunks);
|
|
1391
|
-
}
|
|
1392
|
-
async function writeNodeResponse(response, value) {
|
|
1393
|
-
response.statusCode = value.status;
|
|
1394
|
-
response.statusMessage = value.statusText;
|
|
1395
|
-
value.headers.forEach((headerValue, headerName) => {
|
|
1396
|
-
response.setHeader(headerName, headerValue);
|
|
1397
|
-
});
|
|
1398
|
-
const body = await value.arrayBuffer();
|
|
1399
|
-
response.end(Buffer.from(body));
|
|
1400
|
-
}
|
|
1401
|
-
function decodeNodeWebSocketMessage(message) {
|
|
1402
|
-
if (typeof message === "string") {
|
|
1403
|
-
return message;
|
|
1404
|
-
}
|
|
1405
|
-
if (message instanceof ArrayBuffer) {
|
|
1406
|
-
return new TextDecoder().decode(new Uint8Array(message));
|
|
1407
|
-
}
|
|
1408
|
-
if (Array.isArray(message)) {
|
|
1409
|
-
return Buffer.concat(message).toString("utf8");
|
|
1410
|
-
}
|
|
1411
|
-
if (message instanceof Uint8Array) {
|
|
1412
|
-
return Buffer.from(message).toString("utf8");
|
|
1413
|
-
}
|
|
1414
|
-
return String(message);
|
|
1415
|
-
}
|
|
1416
1469
|
async function handleSubscribeFailure(runtime, subscribeError) {
|
|
1417
1470
|
try {
|
|
1418
1471
|
await runtime.close();
|
|
@@ -1451,11 +1504,10 @@ async function startBroadcastWorker(runtimeBindings) {
|
|
|
1451
1504
|
}
|
|
1452
1505
|
});
|
|
1453
1506
|
if (scalingConfig) {
|
|
1454
|
-
scalingUnsubscribe = await scalingConfig.adapter.subscribe(
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
}).catch((subscribeError) => handleSubscribeFailure(runtime, subscribeError));
|
|
1507
|
+
scalingUnsubscribe = await scalingConfig.adapter.subscribe(
|
|
1508
|
+
scalingConfig.eventChannel,
|
|
1509
|
+
createScalingMessageListener(runtime)
|
|
1510
|
+
).catch((subscribeError) => handleSubscribeFailure(runtime, subscribeError));
|
|
1459
1511
|
}
|
|
1460
1512
|
const bun = globalThis.Bun;
|
|
1461
1513
|
const appsByKey = buildWorkerApps(config);
|
|
@@ -1474,6 +1526,9 @@ async function startBroadcastWorker(runtimeBindings) {
|
|
|
1474
1526
|
if (!app) {
|
|
1475
1527
|
return new Response("Unknown app key", { status: 401 });
|
|
1476
1528
|
}
|
|
1529
|
+
if (!isAllowedWorkerOrigin(request, config.worker.allowedOrigins)) {
|
|
1530
|
+
return new Response("Forbidden", { status: 403 });
|
|
1531
|
+
}
|
|
1477
1532
|
const upgraded = wsServer2.upgrade(request, {
|
|
1478
1533
|
data: {
|
|
1479
1534
|
socketId: createSocketId(),
|
|
@@ -1501,6 +1556,12 @@ async function startBroadcastWorker(runtimeBindings) {
|
|
|
1501
1556
|
});
|
|
1502
1557
|
},
|
|
1503
1558
|
message(socket, message) {
|
|
1559
|
+
const messageBytes = typeof message === "string" ? Buffer.byteLength(message) : message.byteLength;
|
|
1560
|
+
if (messageBytes > config.worker.maxMessageBytes) {
|
|
1561
|
+
runtime.disconnectWebSocket(socket.data.socketId);
|
|
1562
|
+
socket.close(1009, "Message too large");
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1504
1565
|
const value = typeof message === "string" ? message : new TextDecoder().decode(message);
|
|
1505
1566
|
void runtime.receiveWebSocketMessage(socket.data.socketId, value).catch((error) => {
|
|
1506
1567
|
logSocketMessageError(socket.data.socketId, error);
|
|
@@ -1536,7 +1597,7 @@ async function startBroadcastWorker(runtimeBindings) {
|
|
|
1536
1597
|
throw new Error("[@holo-js/broadcast] Node runtime websocket module is missing WebSocketServer export.");
|
|
1537
1598
|
}
|
|
1538
1599
|
const requestConnectionInfo = /* @__PURE__ */ new WeakMap();
|
|
1539
|
-
const wsServer = new WebSocketServer({ noServer: true });
|
|
1600
|
+
const wsServer = new WebSocketServer({ noServer: true, maxPayload: config.worker.maxMessageBytes });
|
|
1540
1601
|
wsServer.on("connection", (socket, request) => {
|
|
1541
1602
|
const connectionInfo = requestConnectionInfo.get(request);
|
|
1542
1603
|
const socketId = connectionInfo.socketId;
|
|
@@ -1550,6 +1611,12 @@ async function startBroadcastWorker(runtimeBindings) {
|
|
|
1550
1611
|
}
|
|
1551
1612
|
});
|
|
1552
1613
|
socket.on("message", (message) => {
|
|
1614
|
+
const messageBytes = getNodeWebSocketMessageBytes(message);
|
|
1615
|
+
if (messageBytes > config.worker.maxMessageBytes) {
|
|
1616
|
+
runtime.disconnectWebSocket(socketId);
|
|
1617
|
+
socket.close(1009, "Message too large");
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1553
1620
|
const value = decodeNodeWebSocketMessage(message);
|
|
1554
1621
|
void runtime.receiveWebSocketMessage(socketId, value).catch((error) => {
|
|
1555
1622
|
logSocketMessageError(socketId, error);
|
|
@@ -1563,7 +1630,13 @@ async function startBroadcastWorker(runtimeBindings) {
|
|
|
1563
1630
|
});
|
|
1564
1631
|
const httpServer = createServer(async (request, response) => {
|
|
1565
1632
|
const requestUrl = toNodeRequestUrl(request, `${config.worker.host}:${config.worker.port}`);
|
|
1566
|
-
|
|
1633
|
+
let requestBody;
|
|
1634
|
+
try {
|
|
1635
|
+
requestBody = await readNodeRequestBody(request, config.worker.maxRequestBytes);
|
|
1636
|
+
} catch (error) {
|
|
1637
|
+
writeNodeRequestBodyError(response, error);
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1567
1640
|
const requestInit = {
|
|
1568
1641
|
method: request.method,
|
|
1569
1642
|
headers: toNodeHeaders(request.headers)
|
|
@@ -1590,6 +1663,12 @@ async function startBroadcastWorker(runtimeBindings) {
|
|
|
1590
1663
|
socket.destroy();
|
|
1591
1664
|
return;
|
|
1592
1665
|
}
|
|
1666
|
+
const originRequest = new Request(requestUrl, { headers: toNodeHeaders(request.headers) });
|
|
1667
|
+
if (!isAllowedWorkerOrigin(originRequest, config.worker.allowedOrigins)) {
|
|
1668
|
+
socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
|
|
1669
|
+
socket.destroy();
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1593
1672
|
requestConnectionInfo.set(request, {
|
|
1594
1673
|
socketId: createSocketId(),
|
|
1595
1674
|
app,
|
|
@@ -1642,19 +1721,37 @@ var workerInternals = {
|
|
|
1642
1721
|
buildWorkerApps,
|
|
1643
1722
|
createScalingNodeId,
|
|
1644
1723
|
createRedisScalingAdapter,
|
|
1645
|
-
createPusherSignature,
|
|
1646
|
-
verifyPusherSignature,
|
|
1724
|
+
createPusherSignature: createWorkerPusherSignature,
|
|
1725
|
+
verifyPusherSignature: verifyWorkerPusherSignature,
|
|
1647
1726
|
createSocketId,
|
|
1648
1727
|
resolveRedisScalingConnection,
|
|
1649
1728
|
resolveScalingEventChannel,
|
|
1650
|
-
normalizePublishBody,
|
|
1651
|
-
parseChannelKind,
|
|
1729
|
+
normalizePublishBody: normalizeWorkerPublishBody,
|
|
1730
|
+
parseChannelKind: parseWorkerChannelKind,
|
|
1652
1731
|
parsePresenceHashMembers,
|
|
1653
|
-
parseSocketMessage
|
|
1732
|
+
parseSocketMessage: parseWorkerSocketMessage,
|
|
1733
|
+
isAllowedWorkerOrigin,
|
|
1734
|
+
readLimitedRequestText,
|
|
1735
|
+
normalizeRealtimeAction,
|
|
1736
|
+
normalizeRealtimeArgs,
|
|
1737
|
+
parseRealtimeSocketMessage,
|
|
1738
|
+
logSocketMessageError,
|
|
1739
|
+
logScalingMessageError,
|
|
1740
|
+
logSocketCleanupError,
|
|
1741
|
+
logRealtimeSubscriptionCleanupError,
|
|
1742
|
+
safeEqual,
|
|
1743
|
+
signChannelAuth,
|
|
1744
|
+
parseClientChannelAuth,
|
|
1745
|
+
parseSignedChannelData,
|
|
1746
|
+
verifyClientChannelAuth,
|
|
1747
|
+
resolvePresenceMemberId,
|
|
1748
|
+
resolveRealtimeErrorStatus,
|
|
1749
|
+
resolveRealtimeErrorCode,
|
|
1750
|
+
resolveRealtimeErrorKind,
|
|
1751
|
+
writeNodeRequestBodyError
|
|
1654
1752
|
};
|
|
1655
1753
|
|
|
1656
1754
|
// src/index.ts
|
|
1657
|
-
import { defineBroadcastConfig } from "@holo-js/config";
|
|
1658
1755
|
var broadcastPackage = Object.freeze({
|
|
1659
1756
|
authorizeBroadcastChannel,
|
|
1660
1757
|
broadcast,
|
|
@@ -1697,9 +1794,12 @@ export {
|
|
|
1697
1794
|
getBroadcastRuntime,
|
|
1698
1795
|
getBroadcastRuntimeBindings,
|
|
1699
1796
|
getRegisteredBroadcastDriver,
|
|
1797
|
+
holoBroadcastDefaults,
|
|
1700
1798
|
isBroadcastDefinition,
|
|
1701
1799
|
isChannelDefinition,
|
|
1702
1800
|
listRegisteredBroadcastDrivers,
|
|
1801
|
+
loadBroadcastPluginDrivers,
|
|
1802
|
+
normalizeBroadcastConfig,
|
|
1703
1803
|
parseBroadcastAuthEndpointPayload,
|
|
1704
1804
|
presenceChannel,
|
|
1705
1805
|
privateChannel,
|
|
@@ -1707,6 +1807,7 @@ export {
|
|
|
1707
1807
|
renderBroadcastAuthResponse,
|
|
1708
1808
|
renderBroadcastClientConfigResponse,
|
|
1709
1809
|
resetBroadcastDriverRegistry,
|
|
1810
|
+
resetBroadcastPluginDrivers,
|
|
1710
1811
|
resetBroadcastRuntime,
|
|
1711
1812
|
resolveBroadcastChannelGuard,
|
|
1712
1813
|
resolveBroadcastClientConfig,
|