@holo-js/broadcast 0.1.8 → 0.2.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/auth.d.ts CHANGED
@@ -13,6 +13,7 @@ declare function matchPattern(pattern: string, channel: string): Readonly<Record
13
13
  declare function resolveChannelMatch(channel: string, definitions: LoadedChannelDefinitions): MatchedChannelDefinition | null;
14
14
  declare function resolveAuthDefinitions(override?: BroadcastChannelAuthRuntimeBindings): Promise<LoadedChannelDefinitions>;
15
15
  declare function authorizeBroadcastChannel(input: BroadcastChannelAuthRequest, channelAuth?: BroadcastChannelAuthRuntimeBindings): Promise<BroadcastChannelAuthResult>;
16
+ declare function resolveBroadcastChannelGuard(input: Pick<BroadcastChannelAuthRequest, 'channel' | 'socketId'>, channelAuth?: BroadcastChannelAuthRuntimeBindings): Promise<string | undefined>;
16
17
  declare function resolveBroadcastWhisperSchema(channel: string, event: string, channelAuth?: BroadcastChannelAuthRuntimeBindings): Promise<{
17
18
  readonly channel: string;
18
19
  readonly event: string;
@@ -32,4 +33,4 @@ declare const broadcastAuthInternals: {
32
33
  reset(): void;
33
34
  };
34
35
 
35
- export { authorizeBroadcastChannel, broadcastAuthInternals, parseBroadcastAuthEndpointPayload, renderBroadcastAuthResponse, resolveBroadcastWhisperSchema, validateBroadcastWhisperPayload };
36
+ export { authorizeBroadcastChannel, broadcastAuthInternals, parseBroadcastAuthEndpointPayload, renderBroadcastAuthResponse, resolveBroadcastChannelGuard, resolveBroadcastWhisperSchema, validateBroadcastWhisperPayload };
package/dist/auth.mjs CHANGED
@@ -3,16 +3,18 @@ import {
3
3
  broadcastAuthInternals,
4
4
  parseBroadcastAuthEndpointPayload,
5
5
  renderBroadcastAuthResponse,
6
+ resolveBroadcastChannelGuard,
6
7
  resolveBroadcastWhisperSchema,
7
8
  validateBroadcastWhisperPayload
8
- } from "./chunk-I3KE6HDH.mjs";
9
- import "./chunk-QYXS4X72.mjs";
10
- import "./chunk-DHKMBH25.mjs";
9
+ } from "./chunk-U5JDBKXC.mjs";
10
+ import "./chunk-TTKGDABI.mjs";
11
+ import "./chunk-HE6HN7ID.mjs";
11
12
  export {
12
13
  authorizeBroadcastChannel,
13
14
  broadcastAuthInternals,
14
15
  parseBroadcastAuthEndpointPayload,
15
16
  renderBroadcastAuthResponse,
17
+ resolveBroadcastChannelGuard,
16
18
  resolveBroadcastWhisperSchema,
17
19
  validateBroadcastWhisperPayload
18
20
  };
@@ -1,12 +1,52 @@
1
+ // src/json.ts
2
+ function isPlainObject(value) {
3
+ return value !== null && typeof value === "object" && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
4
+ }
5
+ function isObjectRecord(value) {
6
+ return value !== null && typeof value === "object" && !Array.isArray(value);
7
+ }
8
+ function normalizeJsonValue(value, path, formatError, options = {}) {
9
+ if (typeof value === "number") {
10
+ if (!Number.isFinite(value)) {
11
+ throw new Error(formatError(path));
12
+ }
13
+ return value;
14
+ }
15
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
16
+ return value;
17
+ }
18
+ if (Array.isArray(value)) {
19
+ return Object.freeze(value.map((entry, index) => normalizeJsonValue(entry, `${path}[${index}]`, formatError, options)));
20
+ }
21
+ if (!isPlainObject(value)) {
22
+ throw new Error(formatError(path));
23
+ }
24
+ return Object.freeze(Object.fromEntries(
25
+ Object.entries(value).map(([key, entry]) => {
26
+ options.validateKey?.(key, path);
27
+ return [key, normalizeJsonValue(entry, `${path}.${key}`, formatError, options)];
28
+ })
29
+ ));
30
+ }
31
+ function parseJsonObject(value, label) {
32
+ let parsed;
33
+ try {
34
+ parsed = JSON.parse(value);
35
+ } catch {
36
+ throw new Error(`[@holo-js/broadcast] ${label} must be valid JSON.`);
37
+ }
38
+ if (!isPlainObject(parsed)) {
39
+ throw new Error(`[@holo-js/broadcast] ${label} must be a JSON object.`);
40
+ }
41
+ return parsed;
42
+ }
43
+
1
44
  // src/contracts.ts
2
45
  var HOLO_BROADCAST_DEFINITION_MARKER = /* @__PURE__ */ Symbol.for("holo-js.broadcast.definition");
3
46
  var HOLO_CHANNEL_DEFINITION_MARKER = /* @__PURE__ */ Symbol.for("holo-js.broadcast.channel");
4
47
  function isReadonlyArray(value) {
5
48
  return Array.isArray(value);
6
49
  }
7
- function isPlainObject(value) {
8
- return value !== null && typeof value === "object" && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
9
- }
10
50
  function normalizeOptionalString(value, label) {
11
51
  if (typeof value === "undefined") {
12
52
  return void 0;
@@ -32,37 +72,23 @@ function normalizeDelayValue(value) {
32
72
  }
33
73
  return value;
34
74
  }
35
- function normalizeJsonValue(value, path) {
36
- if (typeof value === "number") {
37
- if (!Number.isFinite(value)) {
38
- throw new Error(`[Holo Broadcast] ${path} must be JSON-serializable.`);
39
- }
40
- return value;
41
- }
42
- if (value === null || typeof value === "string" || typeof value === "boolean") {
43
- return value;
44
- }
45
- if (Array.isArray(value)) {
46
- return Object.freeze(value.map((entry, index) => normalizeJsonValue(entry, `${path}[${index}]`)));
47
- }
48
- if (isPlainObject(value)) {
49
- return Object.freeze(Object.fromEntries(
50
- Object.entries(value).map(([key, entry]) => {
51
- if (!key.trim()) {
52
- throw new Error(`[Holo Broadcast] ${path} must not include empty payload keys.`);
53
- }
54
- return [key, normalizeJsonValue(entry, `${path}.${key}`)];
55
- })
56
- ));
57
- }
58
- throw new Error(`[Holo Broadcast] ${path} must be JSON-serializable.`);
59
- }
60
75
  function normalizePayload(payload) {
61
76
  const resolved = typeof payload === "function" ? payload() : payload;
62
77
  if (!isPlainObject(resolved)) {
63
78
  throw new Error("[Holo Broadcast] Broadcast payload must be a plain object.");
64
79
  }
65
- return normalizeJsonValue(resolved, "Broadcast payload");
80
+ return normalizeJsonValue(
81
+ resolved,
82
+ "Broadcast payload",
83
+ (path) => `[Holo Broadcast] ${path} must be JSON-serializable.`,
84
+ {
85
+ validateKey(key, path) {
86
+ if (!key.trim()) {
87
+ throw new Error(`[Holo Broadcast] ${path} must not include empty payload keys.`);
88
+ }
89
+ }
90
+ }
91
+ );
66
92
  }
67
93
  function normalizeQueueOptions(queue) {
68
94
  if (typeof queue === "boolean" || typeof queue === "undefined") {
@@ -237,6 +263,7 @@ function normalizeChannelDefinition(pattern, definition) {
237
263
  return {
238
264
  pattern: normalizeChannelPattern(pattern, "Channel pattern"),
239
265
  type: definition.type,
266
+ ...typeof definition.guard === "undefined" ? {} : { guard: definition.guard },
240
267
  authorize: definition.authorize,
241
268
  whispers: normalizeWhisperDefinitions(definition.whispers)
242
269
  };
@@ -280,6 +307,10 @@ var broadcastInternals = {
280
307
  };
281
308
 
282
309
  export {
310
+ isPlainObject,
311
+ isObjectRecord,
312
+ normalizeJsonValue,
313
+ parseJsonObject,
283
314
  extractChannelPatternParamNames,
284
315
  normalizeChannelPattern,
285
316
  formatChannelPattern,
@@ -0,0 +1,44 @@
1
+ // src/client-config.ts
2
+ function resolveDefaultHoloConnection(config) {
3
+ const connection = config.connections[config.default];
4
+ if (!connection || connection.driver !== "holo" || !("key" in connection) || !("options" in connection)) {
5
+ throw new Error('[@holo-js/broadcast] Broadcast client config requires the default broadcast connection to use the "holo" driver.');
6
+ }
7
+ return connection;
8
+ }
9
+ function resolveAuthEndpointPath(authEndpoint) {
10
+ const trimmed = authEndpoint.trim();
11
+ if (trimmed.length === 0) {
12
+ return trimmed;
13
+ }
14
+ try {
15
+ return new URL(trimmed).pathname;
16
+ } catch {
17
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
18
+ }
19
+ }
20
+ function resolveBroadcastClientConfig(config) {
21
+ const connection = resolveDefaultHoloConnection(config);
22
+ const publicHost = config.worker.publicHost ?? connection.options.host;
23
+ const publicPort = config.worker.publicHost ? config.worker.publicPort ?? connection.options.port : connection.options.port;
24
+ return Object.freeze({
25
+ key: connection.key,
26
+ host: publicHost,
27
+ port: publicPort,
28
+ path: config.worker.path,
29
+ scheme: config.worker.publicScheme,
30
+ ...typeof connection.clientOptions.authEndpoint === "string" ? { authEndpoint: resolveAuthEndpointPath(connection.clientOptions.authEndpoint) } : {}
31
+ });
32
+ }
33
+ function renderBroadcastClientConfigResponse(config) {
34
+ return Response.json(resolveBroadcastClientConfig(config), {
35
+ headers: {
36
+ "cache-control": "no-store"
37
+ }
38
+ });
39
+ }
40
+
41
+ export {
42
+ resolveBroadcastClientConfig,
43
+ renderBroadcastClientConfigResponse
44
+ };
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  formatChannelPattern,
3
3
  isBroadcastDefinition,
4
- normalizeBroadcastDefinition
5
- } from "./chunk-DHKMBH25.mjs";
4
+ isPlainObject,
5
+ normalizeBroadcastDefinition,
6
+ normalizeJsonValue
7
+ } from "./chunk-HE6HN7ID.mjs";
6
8
 
7
9
  // src/runtime.ts
8
10
  import { randomUUID } from "crypto";
@@ -110,34 +112,11 @@ function normalizeDelayValue(value, label) {
110
112
  }
111
113
  return value;
112
114
  }
113
- function isRecord(value) {
114
- return !!value && typeof value === "object" && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
115
- }
116
- function normalizeJsonValue(value, path) {
117
- if (typeof value === "number") {
118
- if (!Number.isFinite(value)) {
119
- throw new Error(`[@holo-js/broadcast] ${path} must be JSON-serializable.`);
120
- }
121
- return value;
122
- }
123
- if (value === null || typeof value === "string" || typeof value === "boolean") {
124
- return value;
125
- }
126
- if (Array.isArray(value)) {
127
- return Object.freeze(value.map((entry, index) => normalizeJsonValue(entry, `${path}[${index}]`)));
128
- }
129
- if (!isRecord(value)) {
130
- throw new Error(`[@holo-js/broadcast] ${path} must be JSON-serializable.`);
131
- }
132
- return Object.freeze(Object.fromEntries(
133
- Object.entries(value).map(([key, entry]) => [key, normalizeJsonValue(entry, `${path}.${key}`)])
134
- ));
135
- }
136
115
  function normalizePayload(payload) {
137
- if (!isRecord(payload)) {
116
+ if (!isPlainObject(payload)) {
138
117
  throw new Error("[@holo-js/broadcast] Broadcast payload must be a plain object.");
139
118
  }
140
- return normalizeJsonValue(payload, "Broadcast payload");
119
+ return normalizeJsonValue(payload, "Broadcast payload", (path) => `[@holo-js/broadcast] ${path} must be JSON-serializable.`);
141
120
  }
142
121
  function normalizeRawChannels(channels) {
143
122
  if (!Array.isArray(channels) || channels.length === 0) {
@@ -237,7 +216,7 @@ function createBaseResult(context, channels) {
237
216
  }
238
217
  function normalizeDriverResult(result, context, channels) {
239
218
  const publishedChannels = Array.isArray(result.publishedChannels) ? Object.freeze(result.publishedChannels.map((channel) => normalizeRequiredString(channel, "Published channel"))) : Object.freeze([...channels]);
240
- const provider = result.provider && isRecord(result.provider) ? Object.freeze({ ...result.provider }) : void 0;
219
+ const provider = result.provider && isPlainObject(result.provider) ? Object.freeze({ ...result.provider }) : void 0;
241
220
  return Object.freeze({
242
221
  connection: normalizeOptionalString(result.connection, "Broadcast result connection") ?? context.connection,
243
222
  driver: normalizeOptionalString(result.driver, "Broadcast result driver") ?? context.driver,
@@ -477,12 +456,6 @@ function broadcast(definition) {
477
456
  return new PendingDispatch(async (options) => {
478
457
  const resolvedDefinition = resolveBroadcastDefinition(definition);
479
458
  const raw = createRawInputFromDefinition(resolvedDefinition, options.broadcaster);
480
- const input = Object.freeze({
481
- broadcast: resolvedDefinition,
482
- raw,
483
- options
484
- });
485
- void input;
486
459
  return await executeResolvedRawBroadcast(raw, resolvedDefinition, options);
487
460
  });
488
461
  }
@@ -1,11 +1,14 @@
1
1
  import {
2
2
  getBroadcastRuntimeBindings
3
- } from "./chunk-QYXS4X72.mjs";
3
+ } from "./chunk-TTKGDABI.mjs";
4
4
  import {
5
- isChannelDefinition
6
- } from "./chunk-DHKMBH25.mjs";
5
+ isChannelDefinition,
6
+ isPlainObject,
7
+ normalizeJsonValue
8
+ } from "./chunk-HE6HN7ID.mjs";
7
9
 
8
10
  // src/auth.ts
11
+ import { createHmac } from "crypto";
9
12
  import { resolve } from "path";
10
13
  import { pathToFileURL } from "url";
11
14
  import { parse } from "@holo-js/validation";
@@ -14,9 +17,6 @@ function getRuntimeState() {
14
17
  runtime.__holoBroadcastAuthRuntime__ ??= {};
15
18
  return runtime.__holoBroadcastAuthRuntime__;
16
19
  }
17
- function isRecord(value) {
18
- return !!value && typeof value === "object" && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
19
- }
20
20
  function normalizeRequiredString(value, label) {
21
21
  const normalized = value.trim();
22
22
  if (!normalized) {
@@ -40,31 +40,11 @@ function normalizeLookupChannel(channel, label) {
40
40
  }
41
41
  return normalized;
42
42
  }
43
- function normalizeJsonValue(value, path) {
44
- if (typeof value === "number") {
45
- if (!Number.isFinite(value)) {
46
- throw new Error(`[@holo-js/broadcast] ${path} must be JSON-serializable.`);
47
- }
48
- return value;
49
- }
50
- if (value === null || typeof value === "string" || typeof value === "boolean") {
51
- return value;
52
- }
53
- if (Array.isArray(value)) {
54
- return Object.freeze(value.map((entry, index) => normalizeJsonValue(entry, `${path}[${index}]`)));
55
- }
56
- if (!isRecord(value)) {
57
- throw new Error(`[@holo-js/broadcast] ${path} must be JSON-serializable.`);
58
- }
59
- return Object.freeze(Object.fromEntries(
60
- Object.entries(value).map(([key, entry]) => [key, normalizeJsonValue(entry, `${path}.${key}`)])
61
- ));
62
- }
63
43
  function normalizePresenceMember(value) {
64
- if (!isRecord(value)) {
44
+ if (!isPlainObject(value)) {
65
45
  throw new Error("[@holo-js/broadcast] Presence authorization must return a serializable member object when allowed.");
66
46
  }
67
- return normalizeJsonValue(value, "Broadcast presence member");
47
+ return normalizeJsonValue(value, "Broadcast presence member", (path) => `[@holo-js/broadcast] ${path} must be JSON-serializable.`);
68
48
  }
69
49
  function normalizeDefinitionMap(bindings) {
70
50
  const definitions = bindings.definitions;
@@ -102,6 +82,7 @@ function normalizeRegistryEntry(entry) {
102
82
  type: entry.type,
103
83
  params: Object.freeze([...entry.params]),
104
84
  whispers: Object.freeze([...entry.whispers]),
85
+ ...typeof entry.guard === "string" ? { guard: normalizeRequiredString(entry.guard, "Broadcast channel guard") } : {},
105
86
  ...typeof entry.exportName === "string" ? { exportName: normalizeRequiredString(entry.exportName, "Broadcast channel exportName") } : {}
106
87
  });
107
88
  }
@@ -113,7 +94,7 @@ async function importChannelDefinition(entry, bindings) {
113
94
  const importPath = resolve(registry.projectRoot, entry.sourcePath);
114
95
  const importer = bindings.importModule ?? (async (absolutePath) => await import(pathToFileURL(absolutePath).href));
115
96
  const moduleValue = await importer(importPath);
116
- if (!isRecord(moduleValue)) {
97
+ if (!isPlainObject(moduleValue)) {
117
98
  throw new Error(`[@holo-js/broadcast] Broadcast channel module "${entry.sourcePath}" must export an object module namespace.`);
118
99
  }
119
100
  const exportName = entry.exportName ?? "default";
@@ -225,18 +206,26 @@ async function resolveAuthDefinitions(override) {
225
206
  }
226
207
  return await loadChannelDefinitions(bindings);
227
208
  }
228
- async function authorizeBroadcastChannel(input, channelAuth) {
229
- const channel = normalizeRequiredString(input.channel, "Broadcast auth channel");
209
+ async function resolveBroadcastChannelMatch(channel, channelAuth) {
230
210
  const definitions = await resolveAuthDefinitions(channelAuth);
231
- const match = resolveChannelMatch(normalizeLookupChannel(channel, "Broadcast auth channel"), definitions);
232
- if (!match) {
233
- return Object.freeze({
234
- ok: false,
211
+ return resolveChannelMatch(normalizeLookupChannel(channel, "Broadcast auth channel"), definitions);
212
+ }
213
+ async function resolveChannelGuard(channel, socketId, match) {
214
+ if (typeof match.definition.guard === "undefined") {
215
+ return void 0;
216
+ }
217
+ if (typeof match.definition.guard === "function") {
218
+ const context = {
235
219
  channel,
236
- code: "not-found"
237
- });
220
+ ...typeof socketId === "undefined" ? {} : { socketId },
221
+ params: match.params
222
+ };
223
+ return await match.definition.guard(context);
238
224
  }
239
- const decision = await match.definition.authorize(input.user, match.params);
225
+ return match.definition.guard;
226
+ }
227
+ async function authorizeMatchedBroadcastChannel(channel, user, match) {
228
+ const decision = await match.definition.authorize(user, match.params);
240
229
  if (match.definition.type === "private") {
241
230
  if (decision !== true) {
242
231
  return Object.freeze({
@@ -271,6 +260,26 @@ async function authorizeBroadcastChannel(input, channelAuth) {
271
260
  whispers: Object.freeze(Object.keys(match.definition.whispers))
272
261
  });
273
262
  }
263
+ async function authorizeBroadcastChannel(input, channelAuth) {
264
+ const channel = normalizeRequiredString(input.channel, "Broadcast auth channel");
265
+ const match = await resolveBroadcastChannelMatch(channel, channelAuth);
266
+ if (!match) {
267
+ return Object.freeze({
268
+ ok: false,
269
+ channel,
270
+ code: "not-found"
271
+ });
272
+ }
273
+ return await authorizeMatchedBroadcastChannel(channel, input.user, match);
274
+ }
275
+ async function resolveBroadcastChannelGuard(input, channelAuth) {
276
+ const channel = normalizeRequiredString(input.channel, "Broadcast auth channel");
277
+ const match = await resolveBroadcastChannelMatch(channel, channelAuth);
278
+ if (!match) {
279
+ return void 0;
280
+ }
281
+ return await resolveChannelGuard(channel, input.socketId, match);
282
+ }
274
283
  async function resolveBroadcastWhisperSchema(channel, event, channelAuth) {
275
284
  const normalizedChannel = normalizeRequiredString(channel, "Broadcast whisper channel");
276
285
  const normalizedEvent = normalizeRequiredString(event, "Broadcast whisper event");
@@ -338,6 +347,10 @@ function jsonResponse(body, status) {
338
347
  }
339
348
  });
340
349
  }
350
+ function signBroadcastAuth(appSecret, socketId, channel, channelData) {
351
+ const value = channelData ? `${socketId}:${channel}:${channelData}` : `${socketId}:${channel}`;
352
+ return createHmac("sha256", appSecret).update(value).digest("hex");
353
+ }
341
354
  async function renderBroadcastAuthResponse(request, options = {}) {
342
355
  let payload;
343
356
  try {
@@ -357,7 +370,21 @@ async function renderBroadcastAuthResponse(request, options = {}) {
357
370
  message
358
371
  }, 400);
359
372
  }
360
- const user = typeof options.resolveUser === "function" ? await options.resolveUser(request) : options.user;
373
+ const channel = normalizeRequiredString(payload.channel, "Broadcast auth channel");
374
+ const match = await resolveBroadcastChannelMatch(channel, options.channelAuth);
375
+ if (!match) {
376
+ return jsonResponse({
377
+ ok: false,
378
+ error: "not-found",
379
+ message: `No channel authorization rule matches "${channel}".`
380
+ }, 404);
381
+ }
382
+ const guard = await resolveChannelGuard(channel, payload.socketId, match);
383
+ const user = typeof options.resolveUser === "function" ? await options.resolveUser(request, {
384
+ channel,
385
+ ...typeof payload.socketId === "undefined" ? {} : { socketId: payload.socketId },
386
+ ...typeof guard === "undefined" ? {} : { guard }
387
+ }) : options.user;
361
388
  if (typeof user === "undefined" || user === null) {
362
389
  return jsonResponse({
363
390
  ok: false,
@@ -365,31 +392,29 @@ async function renderBroadcastAuthResponse(request, options = {}) {
365
392
  message: "Broadcast channel authorization requires an authenticated user."
366
393
  }, 401);
367
394
  }
368
- const result = await authorizeBroadcastChannel({
369
- channel: payload.channel,
370
- socketId: payload.socketId,
371
- user
372
- }, options.channelAuth);
395
+ const result = await authorizeMatchedBroadcastChannel(channel, user, match);
373
396
  if (!result.ok) {
374
- if (result.code === "not-found") {
375
- return jsonResponse({
376
- ok: false,
377
- error: "not-found",
378
- message: `No channel authorization rule matches "${result.channel}".`
379
- }, 404);
380
- }
381
397
  return jsonResponse({
382
398
  ok: false,
383
399
  error: "unauthorized",
384
400
  message: `Channel authorization denied for "${result.channel}".`
385
401
  }, 403);
386
402
  }
403
+ const channelData = JSON.stringify({
404
+ whispers: result.whispers,
405
+ ...result.type === "presence" ? { member: result.member } : {}
406
+ });
407
+ const signed = options.appKey && options.appSecret && payload.socketId ? {
408
+ auth: `${options.appKey}:${signBroadcastAuth(options.appSecret, payload.socketId, channel, channelData)}`,
409
+ channel_data: channelData
410
+ } : {};
387
411
  return jsonResponse({
388
412
  ok: true,
389
413
  channel: result.channel,
390
414
  type: result.type,
391
415
  params: result.params,
392
416
  whispers: result.whispers,
417
+ ...signed,
393
418
  ...result.type === "presence" ? { member: result.member } : {}
394
419
  }, 200);
395
420
  }
@@ -410,6 +435,7 @@ var broadcastAuthInternals = {
410
435
 
411
436
  export {
412
437
  authorizeBroadcastChannel,
438
+ resolveBroadcastChannelGuard,
413
439
  resolveBroadcastWhisperSchema,
414
440
  validateBroadcastWhisperPayload,
415
441
  parseBroadcastAuthEndpointPayload,
@@ -0,0 +1,14 @@
1
+ import { LoadedHoloConfig } from '@holo-js/config';
2
+
3
+ type BroadcastClientConfig = {
4
+ readonly key: string;
5
+ readonly host: string;
6
+ readonly port: number;
7
+ readonly path: string;
8
+ readonly scheme: 'http' | 'https';
9
+ readonly authEndpoint?: string;
10
+ };
11
+ declare function resolveBroadcastClientConfig(config: LoadedHoloConfig['broadcast']): BroadcastClientConfig;
12
+ declare function renderBroadcastClientConfigResponse(config: LoadedHoloConfig['broadcast']): Response;
13
+
14
+ export { type BroadcastClientConfig, renderBroadcastClientConfigResponse, resolveBroadcastClientConfig };
@@ -0,0 +1,8 @@
1
+ import {
2
+ renderBroadcastClientConfigResponse,
3
+ resolveBroadcastClientConfig
4
+ } from "./chunk-MEYUTXNP.mjs";
5
+ export {
6
+ renderBroadcastClientConfigResponse,
7
+ resolveBroadcastClientConfig
8
+ };
@@ -1,6 +1,11 @@
1
1
  import { NormalizedHoloBroadcastConfig } from '@holo-js/config';
2
2
  import { ValidationSchema, InferSchemaData } from '@holo-js/validation';
3
3
 
4
+ declare function isPlainObject(value: unknown): value is Record<string, unknown>;
5
+ declare function normalizeJsonValue(value: unknown, path: string, formatError: (path: string) => string, options?: {
6
+ readonly validateKey?: (key: string, path: string) => void;
7
+ }): BroadcastJsonValue;
8
+
4
9
  type BroadcastJsonPrimitive = string | number | boolean | null;
5
10
  type BroadcastJsonValue = BroadcastJsonPrimitive | readonly BroadcastJsonValue[] | BroadcastJsonObject;
6
11
  type BroadcastJsonObject = {
@@ -44,21 +49,40 @@ interface BroadcastDefinition<TName extends string = string, TPayload extends Br
44
49
  readonly queue: NormalizedBroadcastQueueOptions;
45
50
  readonly delay?: BroadcastDelayValue;
46
51
  }
47
- type ExportedBroadcastDefinition<TValue> = Extract<TValue, BroadcastDefinition> extends never ? BroadcastDefinition : Extract<TValue, BroadcastDefinition>;
52
+ type ExportedBroadcastDefinition<TValue> = TValue extends (...args: infer _TArgs) => infer TResult ? ExportedBroadcastDefinition<TResult> : Extract<TValue, BroadcastDefinition> extends never ? BroadcastDefinition : Extract<TValue, BroadcastDefinition>;
48
53
  type BroadcastAuthorizeResult<TType extends BroadcastChannelType, TPresenceMember extends BroadcastJsonObject> = TType extends 'presence' ? false | TPresenceMember : boolean;
49
54
  type BroadcastWhisperSchema = ValidationSchema;
50
55
  type BroadcastWhisperDefinitions = Readonly<Record<string, BroadcastWhisperSchema>>;
51
56
  type InferBroadcastWhisperPayload<TSchema> = TSchema extends {
52
57
  readonly $data?: infer TData;
53
58
  } ? TData : never;
59
+ declare global {
60
+ namespace HoloAuth {
61
+ interface TypeRegistry {
62
+ readonly __holoBroadcastAuthRegistry?: never;
63
+ }
64
+ }
65
+ }
66
+ type RegisteredAuthGuards = HoloAuth.TypeRegistry extends {
67
+ readonly guards: infer TGuards;
68
+ } ? TGuards : Readonly<Record<string, 'session'>>;
69
+ type BroadcastAuthGuardName = Extract<keyof RegisteredAuthGuards, string>;
70
+ interface ChannelGuardResolverContext<TPattern extends string = string> {
71
+ readonly channel: string;
72
+ readonly socketId?: string;
73
+ readonly params: ChannelPatternParams<TPattern>;
74
+ }
75
+ type ChannelGuardResolver<TPattern extends string = string> = BroadcastAuthGuardName | ((context: ChannelGuardResolverContext<TPattern>) => BroadcastAuthGuardName | Promise<BroadcastAuthGuardName>);
54
76
  interface ChannelDefinitionInput<TPattern extends string = string, TType extends Extract<BroadcastChannelType, 'private' | 'presence'> = Extract<BroadcastChannelType, 'private' | 'presence'>, TUser = unknown, TPresenceMember extends BroadcastJsonObject = BroadcastJsonObject, TWhispers extends BroadcastWhisperDefinitions = BroadcastWhisperDefinitions> {
55
77
  readonly type: TType;
78
+ readonly guard?: ChannelGuardResolver<TPattern>;
56
79
  readonly authorize: (user: TUser, params: ChannelPatternParams<TPattern>) => BroadcastAuthorizeResult<TType, TPresenceMember> | Promise<BroadcastAuthorizeResult<TType, TPresenceMember>>;
57
80
  readonly whispers?: TWhispers;
58
81
  }
59
82
  interface ChannelDefinition<TPattern extends string = string, TType extends Extract<BroadcastChannelType, 'private' | 'presence'> = Extract<BroadcastChannelType, 'private' | 'presence'>, TUser = unknown, TPresenceMember extends BroadcastJsonObject = BroadcastJsonObject, TWhispers extends BroadcastWhisperDefinitions = BroadcastWhisperDefinitions> {
60
83
  readonly pattern: TPattern;
61
84
  readonly type: TType;
85
+ readonly guard?: ChannelGuardResolver<TPattern>;
62
86
  readonly authorize: ChannelDefinitionInput<TPattern, TType, TUser, TPresenceMember, TWhispers>['authorize'];
63
87
  readonly whispers: Readonly<TWhispers>;
64
88
  }
@@ -102,6 +126,34 @@ interface BroadcastRuntimeBindings {
102
126
  publish?(input: ResolvedRawBroadcastSendInput, context: BroadcastDriverExecutionContext): BroadcastSendResult | Promise<BroadcastSendResult>;
103
127
  readonly channelAuth?: BroadcastChannelAuthRuntimeBindings;
104
128
  }
129
+ interface BroadcastRealtimeExecutionContext {
130
+ readonly headers: Headers;
131
+ readonly socketId: string;
132
+ readonly appId: string;
133
+ readonly connection: string;
134
+ }
135
+ interface BroadcastRealtimeExecutionResult<TResult = unknown> {
136
+ readonly name: string;
137
+ readonly data: TResult;
138
+ readonly dependencies: readonly string[];
139
+ }
140
+ interface BroadcastRealtimeSubscriptionSnapshot<TResult = unknown> extends BroadcastRealtimeExecutionResult<TResult> {
141
+ readonly version: number;
142
+ }
143
+ interface BroadcastRealtimeSubscription<TResult = unknown> {
144
+ readonly id: string;
145
+ readonly current: BroadcastRealtimeSubscriptionSnapshot<TResult>;
146
+ unsubscribe(): void;
147
+ }
148
+ interface BroadcastRealtimeRuntimeBindings {
149
+ query(name: string, args: Record<string, unknown>, context: BroadcastRealtimeExecutionContext): Promise<BroadcastRealtimeExecutionResult>;
150
+ mutate(name: string, args: Record<string, unknown>, context: BroadcastRealtimeExecutionContext): Promise<BroadcastRealtimeExecutionResult>;
151
+ subscribe(name: string, args: Record<string, unknown>, options: {
152
+ readonly context: BroadcastRealtimeExecutionContext;
153
+ readonly onData: (snapshot: BroadcastRealtimeSubscriptionSnapshot) => void | Promise<void>;
154
+ readonly onError: (error: unknown) => void | Promise<void>;
155
+ }): Promise<BroadcastRealtimeSubscription>;
156
+ }
105
157
  interface BroadcastRuntimeFacade {
106
158
  broadcast(definition: BroadcastDefinition | BroadcastDefinitionInput): PendingBroadcastDispatch<BroadcastSendResult>;
107
159
  broadcastRaw(input: RawBroadcastSendInput): PendingBroadcastDispatch<BroadcastSendResult>;
@@ -150,6 +202,7 @@ interface GeneratedChannelAuthRegistryEntry {
150
202
  readonly sourcePath: string;
151
203
  readonly pattern: string;
152
204
  readonly exportName?: string;
205
+ readonly guard?: string;
153
206
  readonly type: 'private' | 'presence';
154
207
  readonly params: readonly string[];
155
208
  readonly whispers: readonly string[];
@@ -164,6 +217,7 @@ interface BroadcastChannelAuthRuntimeBindings {
164
217
  readonly headers: Headers;
165
218
  readonly socketId: string;
166
219
  readonly channel: string;
220
+ readonly guard?: string;
167
221
  readonly appId: string;
168
222
  readonly connection: string;
169
223
  }) => unknown | Promise<unknown>;
@@ -199,6 +253,8 @@ interface BroadcastAuthEndpointSuccessBody {
199
253
  readonly type: 'private' | 'presence';
200
254
  readonly params: Readonly<Record<string, string>>;
201
255
  readonly whispers: readonly string[];
256
+ readonly auth?: string;
257
+ readonly channel_data?: string;
202
258
  readonly member?: Readonly<BroadcastJsonObject>;
203
259
  }
204
260
  interface BroadcastAuthEndpointErrorBody {
@@ -208,8 +264,14 @@ interface BroadcastAuthEndpointErrorBody {
208
264
  }
209
265
  type BroadcastAuthEndpointBody = BroadcastAuthEndpointSuccessBody | BroadcastAuthEndpointErrorBody;
210
266
  interface BroadcastAuthEndpointOptions {
267
+ readonly appKey?: string;
268
+ readonly appSecret?: string;
211
269
  readonly user?: unknown;
212
- readonly resolveUser?: (request: Request) => unknown | Promise<unknown>;
270
+ readonly resolveUser?: (request: Request, context: {
271
+ readonly channel: string;
272
+ readonly socketId?: string;
273
+ readonly guard?: string;
274
+ }) => unknown | Promise<unknown>;
213
275
  readonly channelAuth?: BroadcastChannelAuthRuntimeBindings;
214
276
  }
215
277
  interface BroadcastWhisperValidationResult<TPayload extends BroadcastJsonObject = BroadcastJsonObject> {
@@ -246,9 +308,7 @@ interface GeneratedBroadcastManifest {
246
308
  readonly channels: readonly GeneratedBroadcastManifestChannel[];
247
309
  }
248
310
  declare function isReadonlyArray(value: unknown): value is readonly unknown[];
249
- declare function isPlainObject(value: unknown): value is Record<string, unknown>;
250
311
  declare function normalizeDelayValue(value: BroadcastDelayValue | undefined): BroadcastDelayValue | undefined;
251
- declare function normalizeJsonValue(value: unknown, path: string): BroadcastJsonValue;
252
312
  declare function normalizeQueueOptions(queue: boolean | BroadcastQueueOptions | undefined): NormalizedBroadcastQueueOptions;
253
313
  declare function extractChannelPatternParamNames(pattern: string): readonly string[];
254
314
  declare function normalizeChannelPattern(pattern: string, label?: string): string;
@@ -284,4 +344,4 @@ declare const broadcastInternals: {
284
344
  normalizeWhisperDefinitions: typeof normalizeWhisperDefinitions;
285
345
  };
286
346
 
287
- export { type BroadcastAuthEndpointBody, type BroadcastAuthEndpointErrorBody, type BroadcastAuthEndpointOptions, type BroadcastAuthEndpointPayload, type BroadcastAuthEndpointSuccessBody, type BroadcastAuthorizeResult, type BroadcastChannelAuthFailure, type BroadcastChannelAuthRequest, type BroadcastChannelAuthResult, type BroadcastChannelAuthRuntimeBindings, type BroadcastChannelAuthSuccess, type BroadcastChannelTarget, type BroadcastChannelType, type BroadcastChannelsFor, type BroadcastDefinition, type BroadcastDefinitionInput, type BroadcastDelayValue, type BroadcastDispatchOptions, type BroadcastDriver, type BroadcastDriverExecutionContext, type BroadcastDriverName, type BroadcastJsonObject, type BroadcastJsonPrimitive, type BroadcastJsonValue, type BroadcastPayloadFor, type BroadcastQueueOptions, type BroadcastRuntimeBindings, type BroadcastRuntimeFacade, type BroadcastSendInput, type BroadcastSendResult, type BroadcastTargetParamInput, type BroadcastWhisperDefinitions, type BroadcastWhisperSchema, type BroadcastWhisperValidationResult, type BuiltInBroadcastDriverRegistry, type ChannelDefinition, type ChannelDefinitionFor, type ChannelDefinitionInput, type ChannelPatternParams, type ChannelPresenceMemberFor, type ChannelWhisperPayloadFor, type ExportedBroadcastDefinition, type ExportedChannelDefinition, type GeneratedBroadcastManifest, type GeneratedBroadcastManifestChannel, type GeneratedBroadcastManifestEvent, type GeneratedChannelAuthRegistryEntry, type HoloBroadcastDriverRegistry, type HoloBroadcastRegistry, type HoloChannelRegistry, type InferBroadcastWhisperPayload, type InferChannelPresenceMember, type InferChannelWhisperPayload, type InferSchemaOutput, type NormalizedBroadcastQueueOptions, type PendingBroadcastDispatch, type RawBroadcastSendInput, type RegisterBroadcastDriverOptions, type RegisteredBroadcastDriver, type ResolvedRawBroadcastSendInput, broadcastInternals, channel, defineBroadcast, defineChannel, extractChannelPatternParamNames, formatChannelPattern, isBroadcastChannelTarget, isBroadcastDefinition, isChannelDefinition, normalizeBroadcastDefinition, normalizeChannelDefinition, normalizeChannelPattern, presenceChannel, privateChannel };
347
+ export { type BroadcastAuthEndpointBody, type BroadcastAuthEndpointErrorBody, type BroadcastAuthEndpointOptions, type BroadcastAuthEndpointPayload, type BroadcastAuthEndpointSuccessBody, type BroadcastAuthGuardName, type BroadcastAuthorizeResult, type BroadcastChannelAuthFailure, type BroadcastChannelAuthRequest, type BroadcastChannelAuthResult, type BroadcastChannelAuthRuntimeBindings, type BroadcastChannelAuthSuccess, type BroadcastChannelTarget, type BroadcastChannelType, type BroadcastChannelsFor, type BroadcastDefinition, type BroadcastDefinitionInput, type BroadcastDelayValue, type BroadcastDispatchOptions, type BroadcastDriver, type BroadcastDriverExecutionContext, type BroadcastDriverName, type BroadcastJsonObject, type BroadcastJsonPrimitive, type BroadcastJsonValue, type BroadcastPayloadFor, type BroadcastQueueOptions, type BroadcastRealtimeExecutionContext, type BroadcastRealtimeExecutionResult, type BroadcastRealtimeRuntimeBindings, type BroadcastRealtimeSubscription, type BroadcastRealtimeSubscriptionSnapshot, type BroadcastRuntimeBindings, type BroadcastRuntimeFacade, type BroadcastSendInput, type BroadcastSendResult, type BroadcastTargetParamInput, type BroadcastWhisperDefinitions, type BroadcastWhisperSchema, type BroadcastWhisperValidationResult, type BuiltInBroadcastDriverRegistry, type ChannelDefinition, type ChannelDefinitionFor, type ChannelDefinitionInput, type ChannelGuardResolver, type ChannelGuardResolverContext, type ChannelPatternParams, type ChannelPresenceMemberFor, type ChannelWhisperPayloadFor, type ExportedBroadcastDefinition, type ExportedChannelDefinition, type GeneratedBroadcastManifest, type GeneratedBroadcastManifestChannel, type GeneratedBroadcastManifestEvent, type GeneratedChannelAuthRegistryEntry, type HoloBroadcastDriverRegistry, type HoloBroadcastRegistry, type HoloChannelRegistry, type InferBroadcastWhisperPayload, type InferChannelPresenceMember, type InferChannelWhisperPayload, type InferSchemaOutput, type NormalizedBroadcastQueueOptions, type PendingBroadcastDispatch, type RawBroadcastSendInput, type RegisterBroadcastDriverOptions, type RegisteredBroadcastDriver, type ResolvedRawBroadcastSendInput, broadcastInternals, channel, defineBroadcast, defineChannel, extractChannelPatternParamNames, formatChannelPattern, isBroadcastChannelTarget, isBroadcastDefinition, isChannelDefinition, normalizeBroadcastDefinition, normalizeChannelDefinition, normalizeChannelPattern, presenceChannel, privateChannel };
@@ -13,7 +13,7 @@ import {
13
13
  normalizeChannelPattern,
14
14
  presenceChannel,
15
15
  privateChannel
16
- } from "./chunk-DHKMBH25.mjs";
16
+ } from "./chunk-HE6HN7ID.mjs";
17
17
  export {
18
18
  broadcastInternals,
19
19
  channel,
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
- import { BroadcastChannelAuthRuntimeBindings, BroadcastRuntimeBindings, BroadcastDriver, RegisteredBroadcastDriver, RegisterBroadcastDriverOptions, channel, defineBroadcast, defineChannel, presenceChannel, privateChannel } from './contracts.js';
2
- export { BroadcastAuthEndpointBody, BroadcastAuthEndpointErrorBody, BroadcastAuthEndpointOptions, BroadcastAuthEndpointPayload, BroadcastAuthEndpointSuccessBody, BroadcastAuthorizeResult, BroadcastChannelAuthFailure, BroadcastChannelAuthRequest, BroadcastChannelAuthResult, BroadcastChannelAuthSuccess, BroadcastChannelTarget, BroadcastChannelType, BroadcastChannelsFor, BroadcastDefinition, BroadcastDefinitionInput, BroadcastDelayValue, BroadcastDispatchOptions, BroadcastDriverExecutionContext, BroadcastDriverName, BroadcastJsonObject, BroadcastJsonPrimitive, BroadcastJsonValue, BroadcastPayloadFor, BroadcastQueueOptions, BroadcastRuntimeFacade, BroadcastSendInput, BroadcastSendResult, BroadcastTargetParamInput, BroadcastWhisperDefinitions, BroadcastWhisperSchema, BroadcastWhisperValidationResult, BuiltInBroadcastDriverRegistry, ChannelDefinition, ChannelDefinitionFor, ChannelDefinitionInput, ChannelPatternParams, ChannelPresenceMemberFor, ChannelWhisperPayloadFor, ExportedBroadcastDefinition, ExportedChannelDefinition, GeneratedBroadcastManifest, GeneratedBroadcastManifestChannel, GeneratedBroadcastManifestEvent, GeneratedChannelAuthRegistryEntry, HoloBroadcastDriverRegistry, HoloBroadcastRegistry, HoloChannelRegistry, InferBroadcastWhisperPayload, InferChannelPresenceMember, InferChannelWhisperPayload, InferSchemaOutput, PendingBroadcastDispatch, RawBroadcastSendInput, ResolvedRawBroadcastSendInput, broadcastInternals, isBroadcastDefinition, isChannelDefinition } from './contracts.js';
3
- import { authorizeBroadcastChannel, parseBroadcastAuthEndpointPayload, renderBroadcastAuthResponse, resolveBroadcastWhisperSchema, validateBroadcastWhisperPayload } from './auth.js';
1
+ import { BroadcastChannelAuthRuntimeBindings, BroadcastRealtimeRuntimeBindings, BroadcastRuntimeBindings, BroadcastDriver, RegisteredBroadcastDriver, RegisterBroadcastDriverOptions, channel, defineBroadcast, defineChannel, presenceChannel, privateChannel } from './contracts.js';
2
+ export { BroadcastAuthEndpointBody, BroadcastAuthEndpointErrorBody, BroadcastAuthEndpointOptions, BroadcastAuthEndpointPayload, BroadcastAuthEndpointSuccessBody, BroadcastAuthGuardName, BroadcastAuthorizeResult, BroadcastChannelAuthFailure, BroadcastChannelAuthRequest, BroadcastChannelAuthResult, BroadcastChannelAuthSuccess, BroadcastChannelTarget, BroadcastChannelType, BroadcastChannelsFor, BroadcastDefinition, BroadcastDefinitionInput, BroadcastDelayValue, BroadcastDispatchOptions, BroadcastDriverExecutionContext, BroadcastDriverName, BroadcastJsonObject, BroadcastJsonPrimitive, BroadcastJsonValue, BroadcastPayloadFor, BroadcastQueueOptions, BroadcastRealtimeExecutionContext, BroadcastRealtimeExecutionResult, BroadcastRealtimeSubscription, BroadcastRealtimeSubscriptionSnapshot, BroadcastRuntimeFacade, BroadcastSendInput, BroadcastSendResult, BroadcastTargetParamInput, BroadcastWhisperDefinitions, BroadcastWhisperSchema, BroadcastWhisperValidationResult, BuiltInBroadcastDriverRegistry, ChannelDefinition, ChannelDefinitionFor, ChannelDefinitionInput, ChannelPatternParams, ChannelPresenceMemberFor, ChannelWhisperPayloadFor, ExportedBroadcastDefinition, ExportedChannelDefinition, GeneratedBroadcastManifest, GeneratedBroadcastManifestChannel, GeneratedBroadcastManifestEvent, GeneratedChannelAuthRegistryEntry, HoloBroadcastDriverRegistry, HoloBroadcastRegistry, HoloChannelRegistry, InferBroadcastWhisperPayload, InferChannelPresenceMember, InferChannelWhisperPayload, InferSchemaOutput, PendingBroadcastDispatch, RawBroadcastSendInput, ResolvedRawBroadcastSendInput, broadcastInternals, isBroadcastDefinition, isChannelDefinition } from './contracts.js';
3
+ import { authorizeBroadcastChannel, parseBroadcastAuthEndpointPayload, renderBroadcastAuthResponse, resolveBroadcastChannelGuard, resolveBroadcastWhisperSchema, validateBroadcastWhisperPayload } from './auth.js';
4
4
  export { broadcastAuthInternals } from './auth.js';
5
+ import { renderBroadcastClientConfigResponse, resolveBroadcastClientConfig } from './client-config.js';
6
+ export { BroadcastClientConfig } from './client-config.js';
5
7
  import { NormalizedHoloBroadcastConfig, NormalizedHoloQueueConfig, NormalizedHoloRedisConfig } from '@holo-js/config';
6
8
  export { HoloBroadcastConfig, NormalizedHoloBroadcastConfig, defineBroadcastConfig } from '@holo-js/config';
7
9
  import { broadcast, broadcastRaw, configureBroadcastRuntime, getBroadcastRuntime, getBroadcastRuntimeBindings, resetBroadcastRuntime } from './runtime.js';
@@ -34,6 +36,7 @@ type PresenceMember = Readonly<Record<string, unknown>>;
34
36
  type WorkerRuntimeOptions = {
35
37
  readonly config: NormalizedHoloBroadcastConfig;
36
38
  readonly channelAuth?: BroadcastChannelAuthRuntimeBindings;
39
+ readonly realtime?: BroadcastRealtimeRuntimeBindings;
37
40
  readonly fetch?: typeof fetch;
38
41
  readonly now?: () => number;
39
42
  readonly scaling?: BroadcastWorkerScalingRuntime;
@@ -123,6 +126,7 @@ declare function createRedisScalingAdapter(connection: BroadcastRedisScalingConn
123
126
  declare function buildWorkerApps(config: NormalizedHoloBroadcastConfig): Readonly<Record<string, BroadcastWorkerApp>>;
124
127
  declare function createBroadcastWorkerRuntime(options: WorkerRuntimeOptions): BroadcastWorkerRuntime;
125
128
  declare function startBroadcastWorker(runtimeBindings: Pick<BroadcastRuntimeBindings, 'config' | 'channelAuth'> & {
129
+ readonly realtime?: BroadcastRealtimeRuntimeBindings;
126
130
  readonly queue?: NormalizedHoloQueueConfig;
127
131
  readonly redis?: NormalizedHoloRedisConfig;
128
132
  readonly nodeId?: string;
@@ -168,12 +172,15 @@ declare const broadcastPackage: Readonly<{
168
172
  parseBroadcastAuthEndpointPayload: typeof parseBroadcastAuthEndpointPayload;
169
173
  presenceChannel: typeof presenceChannel;
170
174
  privateChannel: typeof privateChannel;
175
+ renderBroadcastClientConfigResponse: typeof renderBroadcastClientConfigResponse;
171
176
  renderBroadcastAuthResponse: typeof renderBroadcastAuthResponse;
172
177
  resetBroadcastRuntime: typeof resetBroadcastRuntime;
178
+ resolveBroadcastClientConfig: typeof resolveBroadcastClientConfig;
179
+ resolveBroadcastChannelGuard: typeof resolveBroadcastChannelGuard;
173
180
  resolveBroadcastWhisperSchema: typeof resolveBroadcastWhisperSchema;
174
181
  startBroadcastWorker: typeof startBroadcastWorker;
175
182
  validateBroadcastWhisperPayload: typeof validateBroadcastWhisperPayload;
176
183
  createBroadcastWorkerRuntime: typeof createBroadcastWorkerRuntime;
177
184
  }>;
178
185
 
179
- export { BroadcastChannelAuthRuntimeBindings, BroadcastDriver, BroadcastRuntimeBindings, type BroadcastWorkerRuntime, type BroadcastWorkerStats, RegisterBroadcastDriverOptions, RegisteredBroadcastDriver, type StartedBroadcastWorker, authorizeBroadcastChannel, broadcast, broadcastRaw, broadcastRegistryInternals, channel, configureBroadcastRuntime, createBroadcastWorkerRuntime, broadcastPackage as default, defineBroadcast, defineChannel, getBroadcastRuntime, getBroadcastRuntimeBindings, getRegisteredBroadcastDriver, listRegisteredBroadcastDrivers, parseBroadcastAuthEndpointPayload, presenceChannel, privateChannel, registerBroadcastDriver, renderBroadcastAuthResponse, resetBroadcastDriverRegistry, resetBroadcastRuntime, resolveBroadcastWhisperSchema, startBroadcastWorker, validateBroadcastWhisperPayload, workerInternals };
186
+ export { BroadcastChannelAuthRuntimeBindings, BroadcastDriver, BroadcastRealtimeRuntimeBindings, BroadcastRuntimeBindings, type BroadcastWorkerRuntime, type BroadcastWorkerStats, RegisterBroadcastDriverOptions, RegisteredBroadcastDriver, type StartedBroadcastWorker, authorizeBroadcastChannel, broadcast, broadcastRaw, broadcastRegistryInternals, channel, configureBroadcastRuntime, createBroadcastWorkerRuntime, broadcastPackage as default, defineBroadcast, defineChannel, getBroadcastRuntime, getBroadcastRuntimeBindings, getRegisteredBroadcastDriver, listRegisteredBroadcastDrivers, parseBroadcastAuthEndpointPayload, presenceChannel, privateChannel, registerBroadcastDriver, renderBroadcastAuthResponse, renderBroadcastClientConfigResponse, resetBroadcastDriverRegistry, resetBroadcastRuntime, resolveBroadcastChannelGuard, resolveBroadcastClientConfig, resolveBroadcastWhisperSchema, startBroadcastWorker, validateBroadcastWhisperPayload, workerInternals };
package/dist/index.mjs CHANGED
@@ -3,9 +3,14 @@ import {
3
3
  broadcastAuthInternals,
4
4
  parseBroadcastAuthEndpointPayload,
5
5
  renderBroadcastAuthResponse,
6
+ resolveBroadcastChannelGuard,
6
7
  resolveBroadcastWhisperSchema,
7
8
  validateBroadcastWhisperPayload
8
- } from "./chunk-I3KE6HDH.mjs";
9
+ } from "./chunk-U5JDBKXC.mjs";
10
+ import {
11
+ renderBroadcastClientConfigResponse,
12
+ resolveBroadcastClientConfig
13
+ } from "./chunk-MEYUTXNP.mjs";
9
14
  import {
10
15
  broadcast,
11
16
  broadcastRaw,
@@ -19,7 +24,7 @@ import {
19
24
  registerBroadcastDriver,
20
25
  resetBroadcastDriverRegistry,
21
26
  resetBroadcastRuntime
22
- } from "./chunk-QYXS4X72.mjs";
27
+ } from "./chunk-TTKGDABI.mjs";
23
28
  import {
24
29
  broadcastInternals,
25
30
  channel,
@@ -27,9 +32,12 @@ import {
27
32
  defineChannel,
28
33
  isBroadcastDefinition,
29
34
  isChannelDefinition,
35
+ isObjectRecord,
36
+ isPlainObject,
37
+ parseJsonObject,
30
38
  presenceChannel,
31
39
  privateChannel
32
- } from "./chunk-DHKMBH25.mjs";
40
+ } from "./chunk-HE6HN7ID.mjs";
33
41
 
34
42
  // src/worker.ts
35
43
  import { createHash, createHmac, randomInt, randomUUID, timingSafeEqual } from "crypto";
@@ -45,29 +53,46 @@ function normalizeRequiredString(value, label) {
45
53
  function escapeRegExp(value) {
46
54
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47
55
  }
48
- function parseJsonObject(value, label) {
49
- let parsed;
50
- try {
51
- parsed = JSON.parse(value);
52
- } catch {
53
- throw new Error(`[@holo-js/broadcast] ${label} must be valid JSON.`);
54
- }
55
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
56
- throw new Error(`[@holo-js/broadcast] ${label} must be a JSON object.`);
57
- }
58
- return parsed;
59
- }
60
56
  function parseSocketMessage(rawMessage) {
61
57
  const message = parseJsonObject(rawMessage, "Websocket message");
62
58
  const event = normalizeRequiredString(String(message.event ?? ""), "Websocket event");
63
59
  const channel2 = typeof message.channel === "string" ? normalizeRequiredString(message.channel, "Websocket channel") : void 0;
64
- const data = typeof message.data === "string" ? parseJsonObject(message.data, "Websocket message data") : message.data && typeof message.data === "object" && !Array.isArray(message.data) ? message.data : {};
60
+ const data = typeof message.data === "string" ? parseJsonObject(message.data, "Websocket message data") : isPlainObject(message.data) ? message.data : {};
65
61
  return Object.freeze({
66
62
  event,
67
63
  ...typeof channel2 === "undefined" ? {} : { channel: channel2 },
68
64
  data
69
65
  });
70
66
  }
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
+ }
71
96
  function normalizePublishBody(value) {
72
97
  if (!value || typeof value !== "object" || Array.isArray(value)) {
73
98
  throw new Error("[@holo-js/broadcast] Publish payload must be a JSON object.");
@@ -120,6 +145,60 @@ function logSocketCleanupError(socketId, channel2, error) {
120
145
  const message = error instanceof Error ? error.message : String(error);
121
146
  console.error(`[@holo-js/broadcast] Socket cleanup failed for socket "${socketId}" on "${channel2}": ${message}`);
122
147
  }
148
+ function logRealtimeSubscriptionCleanupError(socketId, subscriptionId, error) {
149
+ const message = error instanceof Error ? error.message : String(error);
150
+ console.error(`[@holo-js/broadcast] Realtime subscription cleanup failed for socket "${socketId}" on "${subscriptionId}": ${message}`);
151
+ }
152
+ function safeEqual(left, right) {
153
+ const leftBuffer = Buffer.from(left);
154
+ const rightBuffer = Buffer.from(right);
155
+ return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
156
+ }
157
+ function signChannelAuth(secret, socketId, channel2, channelData) {
158
+ const value = channelData ? `${socketId}:${channel2}:${channelData}` : `${socketId}:${channel2}`;
159
+ return createHmac("sha256", secret).update(value).digest("hex");
160
+ }
161
+ function parseClientChannelAuth(data) {
162
+ if (typeof data.auth !== "string") {
163
+ return void 0;
164
+ }
165
+ return Object.freeze({
166
+ auth: normalizeRequiredString(data.auth, "Subscription auth"),
167
+ ...typeof data.channel_data === "string" ? { channelData: data.channel_data } : {}
168
+ });
169
+ }
170
+ function parseSignedChannelData(value) {
171
+ if (!value) {
172
+ return Object.freeze({
173
+ whispers: Object.freeze([])
174
+ });
175
+ }
176
+ 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([]);
178
+ const member = data.member && typeof data.member === "object" && !Array.isArray(data.member) ? Object.freeze(data.member) : void 0;
179
+ return Object.freeze({
180
+ whispers,
181
+ ...typeof member === "undefined" ? {} : { member }
182
+ });
183
+ }
184
+ function verifyClientChannelAuth(app, socketId, channel2, clientAuth) {
185
+ const [key, signature] = clientAuth.auth.split(":", 2);
186
+ if (key !== app.key || !signature) {
187
+ throw new Error("[@holo-js/broadcast] Channel authorization signature is invalid.");
188
+ }
189
+ const expected = signChannelAuth(app.secret, socketId, channel2, clientAuth.channelData ?? "");
190
+ if (!safeEqual(signature, expected)) {
191
+ throw new Error("[@holo-js/broadcast] Channel authorization signature is invalid.");
192
+ }
193
+ return parseSignedChannelData(clientAuth.channelData);
194
+ }
195
+ function unsubscribeRealtimeSubscription(socket, id, subscription) {
196
+ try {
197
+ subscription.unsubscribe();
198
+ } catch (error) {
199
+ logRealtimeSubscriptionCleanupError(socket.socketId, id, error);
200
+ }
201
+ }
123
202
  function parseChannelKind(channel2) {
124
203
  if (channel2.startsWith("private-")) {
125
204
  return Object.freeze({
@@ -423,13 +502,16 @@ function pusherEvent(event, data, channel2) {
423
502
  data: typeof data === "string" ? data : JSON.stringify(data)
424
503
  });
425
504
  }
426
- async function authenticateSubscription(app, connection, channel2, channelAuth, fetcher) {
505
+ async function authenticateSubscription(app, connection, channel2, clientAuth, channelAuth, fetcher) {
427
506
  const { kind, canonical } = parseChannelKind(channel2);
428
507
  if (kind === "public") {
429
508
  return Object.freeze({
430
509
  whispers: Object.freeze([])
431
510
  });
432
511
  }
512
+ if (clientAuth) {
513
+ return verifyClientChannelAuth(app, connection.socketId, channel2, clientAuth);
514
+ }
433
515
  if (app.authEndpoint && fetcher) {
434
516
  const authRequest = new Request(app.authEndpoint, {
435
517
  method: "POST",
@@ -456,10 +538,15 @@ async function authenticateSubscription(app, connection, channel2, channelAuth,
456
538
  ...typeof member === "undefined" ? {} : { member }
457
539
  });
458
540
  }
541
+ const guard = await resolveBroadcastChannelGuard({
542
+ channel: canonical,
543
+ socketId: connection.socketId
544
+ }, channelAuth);
459
545
  const resolvedUser = typeof channelAuth?.resolveUser === "function" ? await channelAuth.resolveUser({
460
546
  headers: connection.headers,
461
547
  socketId: connection.socketId,
462
548
  channel: canonical,
549
+ ...typeof guard === "undefined" ? {} : { guard },
463
550
  appId: app.appId,
464
551
  connection: app.connection
465
552
  }) : null;
@@ -655,6 +742,130 @@ function createBroadcastWorkerRuntime(options) {
655
742
  excludeSocketId
656
743
  );
657
744
  }
745
+ function sendRealtimeMessage(socket, event, data) {
746
+ socket.send(pusherEvent(event, data));
747
+ }
748
+ function resolveRealtimeErrorStatus(error) {
749
+ if (!isRecord(error)) {
750
+ return void 0;
751
+ }
752
+ const decision = isRecord(error.decision) ? error.decision : void 0;
753
+ const status = decision?.status ?? error.status ?? error.statusCode;
754
+ if (typeof status === "number" && Number.isInteger(status) && status >= 400 && status <= 599) {
755
+ return status;
756
+ }
757
+ const name = typeof error.name === "string" ? error.name : "";
758
+ if (name === "RealtimeUnauthorizedError") {
759
+ return 401;
760
+ }
761
+ if (name === "RealtimeForbiddenError") {
762
+ return 403;
763
+ }
764
+ return void 0;
765
+ }
766
+ function resolveRealtimeErrorCode(error) {
767
+ if (!isRecord(error)) {
768
+ return void 0;
769
+ }
770
+ const decision = isRecord(error.decision) ? error.decision : void 0;
771
+ const code = decision?.code ?? error.code;
772
+ return typeof code === "string" ? code : void 0;
773
+ }
774
+ function resolveRealtimeErrorKind(error, status) {
775
+ if (!isRecord(error)) {
776
+ return "runtime";
777
+ }
778
+ const name = typeof error.name === "string" ? error.name : "";
779
+ if (name === "AuthorizationError" || name === "RealtimeUnauthorizedError" || name === "RealtimeForbiddenError" || status === 401 || status === 403 || status === 404) {
780
+ return "authorization";
781
+ }
782
+ return name === "RealtimeAuthUnavailableError" ? "transport" : "runtime";
783
+ }
784
+ function sendRealtimeError(socket, id, error) {
785
+ const status = resolveRealtimeErrorStatus(error);
786
+ const code = resolveRealtimeErrorCode(error);
787
+ const name = error instanceof Error ? error.name : void 0;
788
+ const kind = resolveRealtimeErrorKind(error, status);
789
+ sendRealtimeMessage(socket, "holo:realtime:error", {
790
+ id,
791
+ message: error instanceof Error ? error.message : String(error),
792
+ kind,
793
+ ...typeof name === "undefined" ? {} : { name },
794
+ ...typeof status === "undefined" ? {} : { status },
795
+ ...typeof code === "undefined" ? {} : { code }
796
+ });
797
+ }
798
+ function createRealtimeExecutionContext(socket) {
799
+ return Object.freeze({
800
+ headers: socket.headers,
801
+ socketId: socket.socketId,
802
+ appId: socket.app.appId,
803
+ connection: socket.app.connection
804
+ });
805
+ }
806
+ async function handleRealtimeMessage(socket, data) {
807
+ const realtime = options.realtime;
808
+ const request = parseRealtimeSocketMessage(data);
809
+ if (!realtime) {
810
+ sendRealtimeError(socket, request.id, new Error('[@holo-js/broadcast] Realtime requires broadcast worker support. Run "holo broadcast:work" from a project with @holo-js/realtime installed.'));
811
+ return;
812
+ }
813
+ if (request.action === "unsubscribe") {
814
+ const subscription = socket.realtimeSubscriptions.get(request.id);
815
+ if (subscription) {
816
+ unsubscribeRealtimeSubscription(socket, request.id, subscription);
817
+ }
818
+ socket.realtimeSubscriptions.delete(request.id);
819
+ sendRealtimeMessage(socket, "holo:realtime:unsubscribed", {
820
+ id: request.id
821
+ });
822
+ return;
823
+ }
824
+ const context = createRealtimeExecutionContext(socket);
825
+ try {
826
+ if (request.action === "query") {
827
+ const result = await realtime.query(request.name, request.args, context);
828
+ sendRealtimeMessage(socket, "holo:realtime:result", {
829
+ id: request.id,
830
+ action: request.action,
831
+ snapshot: {
832
+ ...result,
833
+ version: 1
834
+ }
835
+ });
836
+ return;
837
+ }
838
+ if (request.action === "mutation") {
839
+ const result = await realtime.mutate(request.name, request.args, context);
840
+ sendRealtimeMessage(socket, "holo:realtime:result", {
841
+ id: request.id,
842
+ action: request.action,
843
+ result
844
+ });
845
+ return;
846
+ }
847
+ const previousSubscription = socket.realtimeSubscriptions.get(request.id);
848
+ if (previousSubscription) {
849
+ unsubscribeRealtimeSubscription(socket, request.id, previousSubscription);
850
+ socket.realtimeSubscriptions.delete(request.id);
851
+ }
852
+ const subscription = await realtime.subscribe(request.name, request.args, {
853
+ context,
854
+ onData(snapshot) {
855
+ sendRealtimeMessage(socket, "holo:realtime:snapshot", {
856
+ id: request.id,
857
+ snapshot
858
+ });
859
+ },
860
+ onError(error) {
861
+ sendRealtimeError(socket, request.id, error);
862
+ }
863
+ });
864
+ socket.realtimeSubscriptions.set(request.id, subscription);
865
+ } catch (error) {
866
+ sendRealtimeError(socket, request.id, error);
867
+ }
868
+ }
658
869
  async function publishScalingEvent(body) {
659
870
  if (!scaling) {
660
871
  return;
@@ -766,9 +977,9 @@ function createBroadcastWorkerRuntime(options) {
766
977
  }
767
978
  }
768
979
  }
769
- async function handleSubscribe(socket, rawChannel) {
980
+ async function handleSubscribe(socket, rawChannel, clientAuth) {
770
981
  const channel2 = normalizeRequiredString(rawChannel, "Subscription channel");
771
- const authorization = await authenticateSubscription(socket.app, socket, channel2, options.channelAuth, options.fetch);
982
+ const authorization = await authenticateSubscription(socket.app, socket, channel2, clientAuth, options.channelAuth, options.fetch);
772
983
  if (!socket.active || connectedSockets.get(socket.socketId) !== socket) {
773
984
  return;
774
985
  }
@@ -931,8 +1142,7 @@ function createBroadcastWorkerRuntime(options) {
931
1142
  try {
932
1143
  publishBody = normalizePublishBody(parseJsonObject(bodyText, "Publish body"));
933
1144
  } catch (error) {
934
- const message = error instanceof Error ? error.message : "Invalid publish payload";
935
- return new Response(message, { status: 400 });
1145
+ return new Response(error.message, { status: 400 });
936
1146
  }
937
1147
  let result;
938
1148
  try {
@@ -992,6 +1202,7 @@ function createBroadcastWorkerRuntime(options) {
992
1202
  send: connection.send,
993
1203
  close: connection.close,
994
1204
  subscribedChannels: /* @__PURE__ */ new Set(),
1205
+ realtimeSubscriptions: /* @__PURE__ */ new Map(),
995
1206
  active: true,
996
1207
  pendingMessage: Promise.resolve()
997
1208
  });
@@ -1015,7 +1226,7 @@ function createBroadcastWorkerRuntime(options) {
1015
1226
  return;
1016
1227
  }
1017
1228
  if (message.event === "pusher:subscribe") {
1018
- await handleSubscribe(socket, String(message.data.channel ?? ""));
1229
+ await handleSubscribe(socket, String(message.data.channel ?? ""), parseClientChannelAuth(message.data));
1019
1230
  return;
1020
1231
  }
1021
1232
  if (message.event === "pusher:unsubscribe") {
@@ -1024,6 +1235,10 @@ function createBroadcastWorkerRuntime(options) {
1024
1235
  }
1025
1236
  if (message.event.startsWith("client-")) {
1026
1237
  await handleClientEvent(socket, message);
1238
+ return;
1239
+ }
1240
+ if (message.event === "holo:realtime") {
1241
+ await handleRealtimeMessage(socket, message.data);
1027
1242
  }
1028
1243
  });
1029
1244
  socket.pendingMessage = task.catch(() => {
@@ -1040,6 +1255,10 @@ function createBroadcastWorkerRuntime(options) {
1040
1255
  }
1041
1256
  socket.active = false;
1042
1257
  connectedSockets.delete(socketId);
1258
+ for (const [subscriptionId, subscription] of socket.realtimeSubscriptions) {
1259
+ unsubscribeRealtimeSubscription(socket, subscriptionId, subscription);
1260
+ }
1261
+ socket.realtimeSubscriptions.clear();
1043
1262
  const channelsToCleanup = [...socket.subscribedChannels];
1044
1263
  const scalingCleanupTasks = channelsToCleanup.map((channel2) => {
1045
1264
  const removedPresenceMember = removeSubscriptionLocal(socket.app.appId, socket.socketId, channel2);
@@ -1184,6 +1403,7 @@ async function startBroadcastWorker(runtimeBindings) {
1184
1403
  const runtime = createBroadcastWorkerRuntime({
1185
1404
  config,
1186
1405
  channelAuth: runtimeBindings.channelAuth,
1406
+ realtime: runtimeBindings.realtime,
1187
1407
  fetch: runtimeBindings.fetch ?? fetch,
1188
1408
  scaling: scalingConfig,
1189
1409
  scalingAutoSubscribe: false,
@@ -1409,8 +1629,11 @@ var broadcastPackage = Object.freeze({
1409
1629
  parseBroadcastAuthEndpointPayload,
1410
1630
  presenceChannel,
1411
1631
  privateChannel,
1632
+ renderBroadcastClientConfigResponse,
1412
1633
  renderBroadcastAuthResponse,
1413
1634
  resetBroadcastRuntime,
1635
+ resolveBroadcastClientConfig,
1636
+ resolveBroadcastChannelGuard,
1414
1637
  resolveBroadcastWhisperSchema,
1415
1638
  startBroadcastWorker,
1416
1639
  validateBroadcastWhisperPayload,
@@ -1443,8 +1666,11 @@ export {
1443
1666
  privateChannel,
1444
1667
  registerBroadcastDriver,
1445
1668
  renderBroadcastAuthResponse,
1669
+ renderBroadcastClientConfigResponse,
1446
1670
  resetBroadcastDriverRegistry,
1447
1671
  resetBroadcastRuntime,
1672
+ resolveBroadcastChannelGuard,
1673
+ resolveBroadcastClientConfig,
1448
1674
  resolveBroadcastWhisperSchema,
1449
1675
  startBroadcastWorker,
1450
1676
  validateBroadcastWhisperPayload,
package/dist/runtime.mjs CHANGED
@@ -6,8 +6,8 @@ import {
6
6
  getBroadcastRuntime,
7
7
  getBroadcastRuntimeBindings,
8
8
  resetBroadcastRuntime
9
- } from "./chunk-QYXS4X72.mjs";
10
- import "./chunk-DHKMBH25.mjs";
9
+ } from "./chunk-TTKGDABI.mjs";
10
+ import "./chunk-HE6HN7ID.mjs";
11
11
  export {
12
12
  broadcast,
13
13
  broadcastRaw,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holo-js/broadcast",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "Holo-JS Framework - broadcast contracts, channel definitions, and driver registration seams",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -15,6 +15,11 @@
15
15
  "import": "./dist/auth.mjs",
16
16
  "default": "./dist/auth.mjs"
17
17
  },
18
+ "./client-config": {
19
+ "types": "./dist/client-config.d.ts",
20
+ "import": "./dist/client-config.mjs",
21
+ "default": "./dist/client-config.mjs"
22
+ },
18
23
  "./contracts": {
19
24
  "types": "./dist/contracts.d.ts",
20
25
  "import": "./dist/contracts.mjs",
@@ -38,8 +43,8 @@
38
43
  "test": "vitest --run"
39
44
  },
40
45
  "dependencies": {
41
- "@holo-js/config": "^0.1.8",
42
- "@holo-js/validation": "^0.1.8",
46
+ "@holo-js/config": "^0.2.0",
47
+ "@holo-js/validation": "^0.2.0",
43
48
  "ws": "^8.18.3"
44
49
  },
45
50
  "peerDependencies": {