@holo-js/broadcast 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  resolveBroadcastChannelGuard,
7
7
  resolveBroadcastWhisperSchema,
8
8
  validateBroadcastWhisperPayload
9
- } from "./chunk-U5JDBKXC.mjs";
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-TTKGDABI.mjs";
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
- var MAX_PUBLISH_TIMESTAMP_SKEW_SECONDS = 300;
46
- function normalizeRequiredString(value, label) {
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 escapeRegExp(value) {
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 = normalizeRequiredString(String(message.event ?? ""), "Websocket event");
59
- const channel2 = typeof message.channel === "string" ? normalizeRequiredString(message.channel, "Websocket channel") : void 0;
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 normalizeRealtimeAction(value) {
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" ? normalizeRequiredString(body.name, "Publish name") : typeof body.event === "string" ? normalizeRequiredString(body.event, "Publish event") : "";
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 normalizeRequiredString(channel2, "Publish channel");
110
- }) : typeof body.channel === "string" ? [normalizeRequiredString(body.channel, "Publish channel")] : [];
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" ? normalizeRequiredString(body.socket_id, "Publish socket_id") : void 0;
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 createPusherSignature(secret, method, pathname, params) {
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 verifyPusherSignature(providedSignature, expectedSignature) {
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 && timingSafeEqual(leftBuffer, rightBuffer);
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 createHmac("sha256", secret).update(value).digest("hex");
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: normalizeRequiredString(data.auth, "Subscription 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) => normalizeRequiredString(String(item), "Auth whisper"))) : Object.freeze([]);
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 normalizeRequiredString(`${process.env.HOSTNAME ?? "node"}-${randomUUID()}`, "Broadcast worker node id");
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 queueConnection.redis.url === "undefined" ? {} : { url: queueConnection.redis.url },
276
- ...typeof queueConnection.redis.clusters === "undefined" ? {} : { clusters: queueConnection.redis.clusters },
277
- host: queueConnection.redis.host,
278
- port: queueConnection.redis.port,
279
- username: queueConnection.redis.username,
280
- password: queueConnection.redis.password,
281
- db: queueConnection.redis.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" ? normalizeRequiredString(holoConnection.clientOptions.authEndpoint, `Broadcast connection "${name}" authEndpoint`) : void 0;
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 } = parseChannelKind(channel2);
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) => normalizeRequiredString(String(value), "Auth whisper"))) : Object.freeze([]);
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, (payload) => {
580
- void handleScalingMessage(payload).catch((error) => {
581
- logScalingMessageError(error);
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 && parseChannelKind(channel2).kind === "presence" ? createNodeSocketRef(scaling.nodeId, socketId) : socketId;
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 } = parseChannelKind(channel2);
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 = normalizeRequiredString(rawChannel, "Subscription channel");
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 } = parseChannelKind(channel2);
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 = normalizeRequiredString(rawChannel, "Unsubscribe channel");
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 = normalizeRequiredString(message.channel ?? "", "Whisper channel");
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 } = parseChannelKind(channel2);
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, options2 = {
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
- if (!options2.fromScaling && options2.shouldReplicate) {
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 = normalizeRequiredString(match?.[1] ?? "", "Publish 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
- const bodyText = await request.text();
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 = normalizeRequiredString(url.searchParams.get("auth_key") ?? "", "Publish auth_key");
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
- normalizeRequiredString(url.searchParams.get("auth_timestamp") ?? "", "Publish auth_timestamp"),
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 = normalizeRequiredString(url.searchParams.get("auth_signature") ?? "", "Publish auth_signature");
1165
- expectedSignature = createPusherSignature(
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 (!verifyPusherSignature(providedSignature, expectedSignature)) {
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 = normalizePublishBody(parseJsonObject(bodyText, "Publish body"));
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 = parseSocketMessage(rawMessage);
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(scalingConfig.eventChannel, (payload) => {
1455
- void runtime.receiveScalingMessage(payload).catch((error) => {
1456
- logScalingMessageError(error);
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
- const requestBody = await readNodeRequestBody(request);
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,