@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 +2 -1
- package/dist/auth.mjs +5 -3
- package/dist/{chunk-DHKMBH25.mjs → chunk-HE6HN7ID.mjs} +60 -29
- package/dist/chunk-MEYUTXNP.mjs +44 -0
- package/dist/{chunk-QYXS4X72.mjs → chunk-TTKGDABI.mjs} +7 -34
- package/dist/{chunk-I3KE6HDH.mjs → chunk-U5JDBKXC.mjs} +77 -51
- package/dist/client-config.d.ts +14 -0
- package/dist/client-config.mjs +8 -0
- package/dist/contracts.d.ts +65 -5
- package/dist/contracts.mjs +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.mjs +248 -22
- package/dist/runtime.mjs +2 -2
- package/package.json +8 -3
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-
|
|
9
|
-
import "./chunk-
|
|
10
|
-
import "./chunk-
|
|
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(
|
|
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
|
-
|
|
5
|
-
|
|
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 (!
|
|
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 &&
|
|
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-
|
|
3
|
+
} from "./chunk-TTKGDABI.mjs";
|
|
4
4
|
import {
|
|
5
|
-
isChannelDefinition
|
|
6
|
-
|
|
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 (!
|
|
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 (!
|
|
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
|
|
229
|
-
const channel = normalizeRequiredString(input.channel, "Broadcast auth channel");
|
|
209
|
+
async function resolveBroadcastChannelMatch(channel, channelAuth) {
|
|
230
210
|
const definitions = await resolveAuthDefinitions(channelAuth);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
237
|
-
|
|
220
|
+
...typeof socketId === "undefined" ? {} : { socketId },
|
|
221
|
+
params: match.params
|
|
222
|
+
};
|
|
223
|
+
return await match.definition.guard(context);
|
|
238
224
|
}
|
|
239
|
-
|
|
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
|
|
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
|
|
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 };
|
package/dist/contracts.d.ts
CHANGED
|
@@ -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
|
|
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 };
|
package/dist/contracts.mjs
CHANGED
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-
|
|
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-
|
|
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-
|
|
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") :
|
|
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
|
-
|
|
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-
|
|
10
|
-
import "./chunk-
|
|
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.
|
|
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.
|
|
42
|
-
"@holo-js/validation": "^0.
|
|
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": {
|