@zintrust/socket 0.4.58 → 0.4.61
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/README.md +44 -0
- package/dist/build-manifest.json +11 -7
- package/dist/index.js +210 -17
- package/dist/register.js +12 -1
- package/package.json +4 -7
package/README.md
CHANGED
|
@@ -189,6 +189,50 @@ Accepted publish authorization headers:
|
|
|
189
189
|
|
|
190
190
|
You can also provide `channels` instead of `channel`, and `name` instead of `event`.
|
|
191
191
|
|
|
192
|
+
## Example: Core-Owned Policy Hooks
|
|
193
|
+
|
|
194
|
+
Projects can keep the transport routes owned by core while still supplying business rules through `config/broadcast.ts`.
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
export default {
|
|
198
|
+
default: 'inmemory',
|
|
199
|
+
socket: {
|
|
200
|
+
authMiddleware: ['auth', 'jwt'],
|
|
201
|
+
async authorize(_request, context) {
|
|
202
|
+
if (context.channelName.startsWith('private-')) {
|
|
203
|
+
return {
|
|
204
|
+
authorized: context.user !== null && context.user !== undefined,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (context.channelName.startsWith('public-')) {
|
|
209
|
+
return {
|
|
210
|
+
authorized: true,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
authorized: false,
|
|
216
|
+
};
|
|
217
|
+
},
|
|
218
|
+
async publish(_request, context) {
|
|
219
|
+
if (context.event.startsWith('admin.')) {
|
|
220
|
+
return {
|
|
221
|
+
allowed: context.user !== null && context.user !== undefined,
|
|
222
|
+
message: 'Admin publish requires an authenticated user.',
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
allowed: true,
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The publish hook may also rewrite the outgoing event, channels, data, or `socketId` before the framework fans the event out.
|
|
235
|
+
|
|
192
236
|
## Example: Auth Request
|
|
193
237
|
|
|
194
238
|
```http
|
package/dist/build-manifest.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/socket",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"buildDate": "2026-04-
|
|
3
|
+
"version": "0.4.61",
|
|
4
|
+
"buildDate": "2026-04-05T07:58:39.948Z",
|
|
5
5
|
"buildEnvironment": {
|
|
6
6
|
"node": "v22.22.1",
|
|
7
7
|
"platform": "darwin",
|
|
8
8
|
"arch": "arm64"
|
|
9
9
|
},
|
|
10
10
|
"git": {
|
|
11
|
-
"commit": "
|
|
11
|
+
"commit": "b4dbe529",
|
|
12
12
|
"branch": "release"
|
|
13
13
|
},
|
|
14
14
|
"package": {
|
|
@@ -21,21 +21,25 @@
|
|
|
21
21
|
]
|
|
22
22
|
},
|
|
23
23
|
"files": {
|
|
24
|
+
"build-manifest.json": {
|
|
25
|
+
"size": 1086,
|
|
26
|
+
"sha256": "a55e096a80190637fe7d56c36e53865451f623c88ceb5ccb7b9124e4a14335bb"
|
|
27
|
+
},
|
|
24
28
|
"index.d.ts": {
|
|
25
29
|
"size": 932,
|
|
26
30
|
"sha256": "6432952783fd7eacfc46813fcbd6e96672ff94c73fb0bad8e2f20fc278c64377"
|
|
27
31
|
},
|
|
28
32
|
"index.js": {
|
|
29
|
-
"size":
|
|
30
|
-
"sha256": "
|
|
33
|
+
"size": 35299,
|
|
34
|
+
"sha256": "112c1becf555e48ea7f0ca52b4e0588c764777cefc90c33dd8c8fe52715b54ce"
|
|
31
35
|
},
|
|
32
36
|
"register.d.ts": {
|
|
33
37
|
"size": 16,
|
|
34
38
|
"sha256": "71d366165dd36f1675aa253a76262b226fb6c62e5ab632746b8aea61c0c625fc"
|
|
35
39
|
},
|
|
36
40
|
"register.js": {
|
|
37
|
-
"size":
|
|
38
|
-
"sha256": "
|
|
41
|
+
"size": 744,
|
|
42
|
+
"sha256": "a3233333a9dfc2357a198aac093cc4dfa1a3e83bb4285c52fce639f0ab1364ad"
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @zintrust/socket v0.4.60
|
|
3
|
+
*
|
|
4
|
+
* Unified socket runtime for ZinTrust.
|
|
5
|
+
*
|
|
6
|
+
* Build Information:
|
|
7
|
+
* Built: 2026-04-05T07:58:23.814Z
|
|
8
|
+
* Node: >=20.0.0
|
|
9
|
+
* License: MIT
|
|
10
|
+
*
|
|
11
|
+
*/
|
|
12
|
+
import { Cloudflare, broadcastConfig, ErrorFactory, isArray, isNonEmptyString, Logger, middlewareConfig, Router, SocketFeature, } from '@zintrust/core';
|
|
2
13
|
const encoder = new TextEncoder();
|
|
3
14
|
const websocketGuid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
4
15
|
const socketHubBindingName = 'ZT_SOCKET_HUB';
|
|
@@ -491,7 +502,7 @@ const shouldUseCloudflareHub = (settings) => {
|
|
|
491
502
|
};
|
|
492
503
|
const parseJsonResponse = async (response) => {
|
|
493
504
|
try {
|
|
494
|
-
return
|
|
505
|
+
return await response.clone().json();
|
|
495
506
|
}
|
|
496
507
|
catch {
|
|
497
508
|
try {
|
|
@@ -502,6 +513,114 @@ const parseJsonResponse = async (response) => {
|
|
|
502
513
|
}
|
|
503
514
|
}
|
|
504
515
|
};
|
|
516
|
+
const isSocketAuthorizerHandler = (value) => {
|
|
517
|
+
return typeof value === 'function';
|
|
518
|
+
};
|
|
519
|
+
const isSocketAuthorizer = (value) => {
|
|
520
|
+
const candidate = value;
|
|
521
|
+
return typeof value === 'object' && value !== null && typeof candidate?.authorize === 'function';
|
|
522
|
+
};
|
|
523
|
+
const createDefaultSocketAuthorizer = () => {
|
|
524
|
+
return Object.freeze({
|
|
525
|
+
async authorize(_request, context) {
|
|
526
|
+
if (context.channelName.startsWith('private-') ||
|
|
527
|
+
context.channelName.startsWith('presence-')) {
|
|
528
|
+
return {
|
|
529
|
+
authorized: context.user !== null && context.user !== undefined,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
authorized: false,
|
|
534
|
+
};
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
};
|
|
538
|
+
const resolveSocketAuthorizer = () => {
|
|
539
|
+
const configured = broadcastConfig.socket.authorize;
|
|
540
|
+
if (configured === undefined) {
|
|
541
|
+
return createDefaultSocketAuthorizer();
|
|
542
|
+
}
|
|
543
|
+
if (isSocketAuthorizerHandler(configured)) {
|
|
544
|
+
return Object.freeze({
|
|
545
|
+
authorize: configured,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
if (isSocketAuthorizer(configured)) {
|
|
549
|
+
return configured;
|
|
550
|
+
}
|
|
551
|
+
throw ErrorFactory.createConfigError('broadcastConfig.socket.authorize must be a function or an object with an authorize(request, context) method.');
|
|
552
|
+
};
|
|
553
|
+
const isSocketPublishPolicyHandler = (value) => {
|
|
554
|
+
return typeof value === 'function';
|
|
555
|
+
};
|
|
556
|
+
const isSocketPublishPolicy = (value) => {
|
|
557
|
+
const candidate = value;
|
|
558
|
+
return typeof value === 'object' && value !== null && typeof candidate?.authorize === 'function';
|
|
559
|
+
};
|
|
560
|
+
const createDefaultSocketPublishPolicy = () => {
|
|
561
|
+
return Object.freeze({
|
|
562
|
+
async authorize() {
|
|
563
|
+
return {
|
|
564
|
+
allowed: true,
|
|
565
|
+
};
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
};
|
|
569
|
+
const resolveSocketPublishPolicy = () => {
|
|
570
|
+
const configured = broadcastConfig.socket.publish;
|
|
571
|
+
if (configured === undefined) {
|
|
572
|
+
return createDefaultSocketPublishPolicy();
|
|
573
|
+
}
|
|
574
|
+
if (isSocketPublishPolicyHandler(configured)) {
|
|
575
|
+
return Object.freeze({
|
|
576
|
+
authorize: configured,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
if (isSocketPublishPolicy(configured)) {
|
|
580
|
+
return configured;
|
|
581
|
+
}
|
|
582
|
+
throw ErrorFactory.createConfigError('broadcastConfig.socket.publish must be a function or an object with an authorize(request, context) method.');
|
|
583
|
+
};
|
|
584
|
+
const resolveSocketAuthMiddleware = () => {
|
|
585
|
+
const configured = broadcastConfig.socket.authMiddleware
|
|
586
|
+
.filter(isNonEmptyString)
|
|
587
|
+
.map((value) => value.trim())
|
|
588
|
+
.filter((value) => value !== '');
|
|
589
|
+
const middleware = configured.length > 0 ? configured : ['auth'];
|
|
590
|
+
const known = new Set(Object.keys(middlewareConfig.route));
|
|
591
|
+
const unknown = middleware.filter((value) => !known.has(value));
|
|
592
|
+
if (unknown.length > 0) {
|
|
593
|
+
throw ErrorFactory.createConfigError(`Unknown socket auth middleware configured: ${unknown.join(', ')}`);
|
|
594
|
+
}
|
|
595
|
+
return middleware;
|
|
596
|
+
};
|
|
597
|
+
const isSocketAuthRouteOverrideEnabled = () => {
|
|
598
|
+
return broadcastConfig.socket.allowAuthRouteOverride === true;
|
|
599
|
+
};
|
|
600
|
+
const routeExists = (router, method, path) => {
|
|
601
|
+
return router.routes.some((route) => route.method === method && route.path === path);
|
|
602
|
+
};
|
|
603
|
+
const assertReservedSocketRouteAvailable = (router, method, path, options) => {
|
|
604
|
+
if (!routeExists(router, method, path)) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (options?.allowOverride === true && isSocketAuthRouteOverrideEnabled()) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
throw ErrorFactory.createConfigError(`Socket compatibility route ${method} ${path} is reserved while sockets are enabled.`);
|
|
611
|
+
};
|
|
612
|
+
const createSocketAuthPayload = async (settings, socketId, channelName, channelData) => {
|
|
613
|
+
if (settings.secret.trim() === '' || settings.appKey.trim() === '') {
|
|
614
|
+
throw ErrorFactory.createConfigError('Socket auth is not configured.');
|
|
615
|
+
}
|
|
616
|
+
const signature = await hmacSha256Hex(settings.secret, channelData === undefined
|
|
617
|
+
? `${socketId}:${channelName}`
|
|
618
|
+
: `${socketId}:${channelName}:${channelData}`);
|
|
619
|
+
return {
|
|
620
|
+
auth: `${settings.appKey}:${signature}`,
|
|
621
|
+
...(channelData === undefined ? {} : { channel_data: channelData }),
|
|
622
|
+
};
|
|
623
|
+
};
|
|
505
624
|
const parsePublishPayload = (payload) => {
|
|
506
625
|
let event = '';
|
|
507
626
|
if (isNonEmptyString(payload.name)) {
|
|
@@ -527,6 +646,29 @@ const parsePublishPayload = (payload) => {
|
|
|
527
646
|
...(isNonEmptyString(payload.socket_id) ? { socket_id: payload.socket_id.trim() } : {}),
|
|
528
647
|
};
|
|
529
648
|
};
|
|
649
|
+
const normalizePublishDecisionPayload = (payload, decision) => {
|
|
650
|
+
const event = isNonEmptyString(decision.event) ? decision.event.trim() : payload.event;
|
|
651
|
+
const channels = isArray(decision.channels)
|
|
652
|
+
? decision.channels.filter(isNonEmptyString).map((item) => item.trim())
|
|
653
|
+
: payload.channels;
|
|
654
|
+
if (event === '' || channels.length === 0) {
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
channels,
|
|
659
|
+
event,
|
|
660
|
+
data: decision.data === undefined ? payload.data : decision.data,
|
|
661
|
+
...(() => {
|
|
662
|
+
if (isNonEmptyString(decision.socketId)) {
|
|
663
|
+
return { socket_id: decision.socketId.trim() };
|
|
664
|
+
}
|
|
665
|
+
if (payload.socket_id === undefined) {
|
|
666
|
+
return {};
|
|
667
|
+
}
|
|
668
|
+
return { socket_id: payload.socket_id };
|
|
669
|
+
})(),
|
|
670
|
+
};
|
|
671
|
+
};
|
|
530
672
|
const forwardPublishToHub = async (settings, payload, envSource) => {
|
|
531
673
|
const stub = getSocketHubStub(settings, envSource);
|
|
532
674
|
if (stub === null) {
|
|
@@ -612,10 +754,6 @@ const respondUpgradeRequired = (req, res) => {
|
|
|
612
754
|
};
|
|
613
755
|
const authenticateSubscription = async (req, res) => {
|
|
614
756
|
const settings = getSocketRuntimeSettings();
|
|
615
|
-
if (settings.secret.trim() === '' || settings.appKey.trim() === '') {
|
|
616
|
-
res.setStatus(503).json({ error: 'Socket auth is not configured.' });
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
757
|
const body = req.getBody();
|
|
620
758
|
const payload = body !== null && typeof body === 'object' ? body : {};
|
|
621
759
|
const socketId = isNonEmptyString(payload['socket_id']) ? payload['socket_id'].trim() : '';
|
|
@@ -629,13 +767,28 @@ const authenticateSubscription = async (req, res) => {
|
|
|
629
767
|
res.setStatus(400).json({ error: 'socket_id and channel_name are required.' });
|
|
630
768
|
return;
|
|
631
769
|
}
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
770
|
+
const authorizer = resolveSocketAuthorizer();
|
|
771
|
+
const decision = await authorizer.authorize(req, {
|
|
772
|
+
channelName,
|
|
773
|
+
socketId,
|
|
774
|
+
user: req.user ?? null,
|
|
775
|
+
channelData,
|
|
638
776
|
});
|
|
777
|
+
if (decision.authorized !== true) {
|
|
778
|
+
res.setStatus(403).json({ message: 'Forbidden' });
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const resolvedChannelData = isNonEmptyString(decision.channelData)
|
|
782
|
+
? decision.channelData.trim()
|
|
783
|
+
: channelData;
|
|
784
|
+
if (isNonEmptyString(decision.auth)) {
|
|
785
|
+
res.json({
|
|
786
|
+
auth: decision.auth.trim(),
|
|
787
|
+
...(resolvedChannelData === undefined ? {} : { channel_data: resolvedChannelData }),
|
|
788
|
+
});
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
res.json(await createSocketAuthPayload(settings, socketId, channelName, resolvedChannelData));
|
|
639
792
|
};
|
|
640
793
|
const publishEvent = async (req, res) => {
|
|
641
794
|
const settings = getSocketRuntimeSettings();
|
|
@@ -658,24 +811,64 @@ const publishEvent = async (req, res) => {
|
|
|
658
811
|
res.setStatus(400).json({ error: 'event/name and channel/channels are required.' });
|
|
659
812
|
return;
|
|
660
813
|
}
|
|
814
|
+
const publishPolicy = resolveSocketPublishPolicy();
|
|
815
|
+
const decision = await publishPolicy.authorize(req, {
|
|
816
|
+
appId: appId.trim(),
|
|
817
|
+
channels: normalizedPayload.channels,
|
|
818
|
+
event: normalizedPayload.event,
|
|
819
|
+
data: normalizedPayload.data,
|
|
820
|
+
socketId: normalizedPayload.socket_id,
|
|
821
|
+
user: req.user ?? null,
|
|
822
|
+
});
|
|
823
|
+
if (decision.allowed !== true) {
|
|
824
|
+
res.setStatus(decision.statusCode ?? 403).json({
|
|
825
|
+
message: decision.message ?? 'Forbidden',
|
|
826
|
+
});
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const allowedPayload = normalizePublishDecisionPayload(normalizedPayload, decision);
|
|
830
|
+
if (allowedPayload === null) {
|
|
831
|
+
res
|
|
832
|
+
.setStatus(400)
|
|
833
|
+
.json({ error: 'publish policy must return a non-empty event and channels.' });
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
661
836
|
if (shouldUseCloudflareHub(settings)) {
|
|
662
|
-
const response = await forwardPublishToHub(settings,
|
|
837
|
+
const response = await forwardPublishToHub(settings, allowedPayload, Cloudflare.getWorkersEnv());
|
|
663
838
|
const responseBody = await parseJsonResponse(response);
|
|
664
839
|
res.setStatus(response.status).json(responseBody);
|
|
665
840
|
return;
|
|
666
841
|
}
|
|
667
|
-
const deliveries = publishToChannels(getNodeSocketState(),
|
|
842
|
+
const deliveries = publishToChannels(getNodeSocketState(), allowedPayload.channels, allowedPayload.event, allowedPayload.data, allowedPayload.socket_id);
|
|
668
843
|
res.setStatus(202).json({
|
|
669
844
|
ok: true,
|
|
670
|
-
channels:
|
|
671
|
-
event:
|
|
845
|
+
channels: allowedPayload.channels,
|
|
846
|
+
event: allowedPayload.event,
|
|
672
847
|
deliveries,
|
|
673
848
|
});
|
|
674
849
|
};
|
|
675
850
|
const registerSocketRoutes = (router) => {
|
|
676
851
|
const settings = getSocketRuntimeSettings();
|
|
852
|
+
const allowAuthRouteOverride = isSocketAuthRouteOverrideEnabled();
|
|
853
|
+
const hasExistingAuthRoute = routeExists(router, 'POST', '/broadcasting/auth');
|
|
854
|
+
assertReservedSocketRouteAvailable(router, 'GET', `${settings.path}/:appKey`);
|
|
855
|
+
assertReservedSocketRouteAvailable(router, 'POST', '/apps/:appId/events');
|
|
677
856
|
Router.get(router, `${settings.path}/:appKey`, respondUpgradeRequired);
|
|
678
|
-
|
|
857
|
+
if (hasExistingAuthRoute) {
|
|
858
|
+
if (!allowAuthRouteOverride) {
|
|
859
|
+
Logger.info('Detected existing application-owned POST /broadcasting/auth route; preserving it while sockets are enabled.');
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
Router.post(router, '/broadcasting/auth', authenticateSubscription, {
|
|
864
|
+
middleware: resolveSocketAuthMiddleware(),
|
|
865
|
+
meta: {
|
|
866
|
+
summary: 'Socket broadcast authorization',
|
|
867
|
+
tags: ['Sockets'],
|
|
868
|
+
responseStatus: 200,
|
|
869
|
+
},
|
|
870
|
+
});
|
|
871
|
+
}
|
|
679
872
|
Router.post(router, '/apps/:appId/events', publishEvent);
|
|
680
873
|
};
|
|
681
874
|
const socketRouteRegistrar = Object.freeze({
|
package/dist/register.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { socketRouteRegistrar, socketRuntime } from './index.js';
|
|
2
|
+
const registerViaGlobalFallback = () => {
|
|
3
|
+
const globalRegistry = globalThis;
|
|
4
|
+
globalRegistry.__zintrustSocketRuntimeRegistry = {
|
|
5
|
+
...globalRegistry.__zintrustSocketRuntimeRegistry,
|
|
6
|
+
runtime: socketRuntime,
|
|
7
|
+
routeRegistrar: socketRouteRegistrar,
|
|
8
|
+
};
|
|
9
|
+
};
|
|
2
10
|
const importCore = async () => {
|
|
3
11
|
try {
|
|
4
12
|
return (await import('@zintrust/core'));
|
|
@@ -8,7 +16,10 @@ const importCore = async () => {
|
|
|
8
16
|
}
|
|
9
17
|
};
|
|
10
18
|
const core = await importCore();
|
|
11
|
-
if (core.SocketRuntimeRegistry
|
|
19
|
+
if (core.SocketRuntimeRegistry === undefined) {
|
|
20
|
+
registerViaGlobalFallback();
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
12
23
|
core.SocketRuntimeRegistry.registerRuntime(socketRuntime);
|
|
13
24
|
core.SocketRuntimeRegistry.registerRoutes(socketRouteRegistrar);
|
|
14
25
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/socket",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.61",
|
|
4
4
|
"description": "Unified socket runtime for ZinTrust.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"node": ">=20.0.0"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"@zintrust/core": "^0.4.
|
|
26
|
+
"@zintrust/core": "^0.4.61"
|
|
27
27
|
},
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
|
@@ -34,8 +34,5 @@
|
|
|
34
34
|
"websocket",
|
|
35
35
|
"broadcast"
|
|
36
36
|
],
|
|
37
|
-
"scripts": {
|
|
38
|
-
|
|
39
|
-
"prepublishOnly": "npm run build"
|
|
40
|
-
}
|
|
41
|
-
}
|
|
37
|
+
"scripts": {}
|
|
38
|
+
}
|