@syncular/server-hono 0.0.6-84 → 0.0.6-86
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/console/gateway.d.ts +2 -0
- package/dist/console/gateway.d.ts.map +1 -1
- package/dist/console/gateway.js +218 -41
- package/dist/console/gateway.js.map +1 -1
- package/dist/console/routes.d.ts.map +1 -1
- package/dist/console/routes.js +165 -37
- package/dist/console/routes.js.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/console-gateway-live-routes.test.ts +54 -3
- package/src/console/gateway.ts +276 -52
- package/src/console/routes.ts +193 -41
package/dist/console/routes.js
CHANGED
|
@@ -1639,9 +1639,31 @@ export function createConsoleRoutes(options) {
|
|
|
1639
1639
|
const upgradeWebSocket = options.websocket.upgradeWebSocket;
|
|
1640
1640
|
const heartbeatIntervalMs = options.websocket.heartbeatIntervalMs ?? 30000;
|
|
1641
1641
|
const wsState = new WeakMap();
|
|
1642
|
+
const closeUnauthenticated = (ws) => {
|
|
1643
|
+
try {
|
|
1644
|
+
ws.send(JSON.stringify({ type: 'error', message: 'UNAUTHENTICATED' }));
|
|
1645
|
+
}
|
|
1646
|
+
catch {
|
|
1647
|
+
// ignore send errors
|
|
1648
|
+
}
|
|
1649
|
+
ws.close(4001, 'Unauthenticated');
|
|
1650
|
+
};
|
|
1651
|
+
const cleanup = (ws) => {
|
|
1652
|
+
const state = wsState.get(ws);
|
|
1653
|
+
if (!state)
|
|
1654
|
+
return;
|
|
1655
|
+
if (state.listener) {
|
|
1656
|
+
emitter.removeListener(state.listener);
|
|
1657
|
+
}
|
|
1658
|
+
if (state.heartbeatInterval) {
|
|
1659
|
+
clearInterval(state.heartbeatInterval);
|
|
1660
|
+
}
|
|
1661
|
+
if (state.authTimeout) {
|
|
1662
|
+
clearTimeout(state.authTimeout);
|
|
1663
|
+
}
|
|
1664
|
+
wsState.delete(ws);
|
|
1665
|
+
};
|
|
1642
1666
|
routes.get('/events/live', upgradeWebSocket(async (c) => {
|
|
1643
|
-
// Auth check via query param (WebSocket doesn't support headers easily)
|
|
1644
|
-
const token = c.req.query('token');
|
|
1645
1667
|
const authHeader = c.req.header('Authorization');
|
|
1646
1668
|
const partitionId = c.req.query('partitionId')?.trim() || undefined;
|
|
1647
1669
|
const replaySince = c.req.query('since');
|
|
@@ -1655,34 +1677,159 @@ export function createConsoleRoutes(options) {
|
|
|
1655
1677
|
const mockContext = {
|
|
1656
1678
|
req: {
|
|
1657
1679
|
header: (name) => name === 'Authorization' ? authHeader : undefined,
|
|
1658
|
-
query: (
|
|
1680
|
+
query: () => undefined,
|
|
1659
1681
|
},
|
|
1660
1682
|
};
|
|
1661
|
-
const
|
|
1683
|
+
const initialAuth = await options.authenticate(mockContext);
|
|
1684
|
+
const authenticateWithBearer = async (token) => {
|
|
1685
|
+
const trimmedToken = token.trim();
|
|
1686
|
+
if (!trimmedToken)
|
|
1687
|
+
return null;
|
|
1688
|
+
const authContext = {
|
|
1689
|
+
req: {
|
|
1690
|
+
header: (name) => name === 'Authorization' ? `Bearer ${trimmedToken}` : undefined,
|
|
1691
|
+
query: () => undefined,
|
|
1692
|
+
},
|
|
1693
|
+
};
|
|
1694
|
+
return options.authenticate(authContext);
|
|
1695
|
+
};
|
|
1662
1696
|
return {
|
|
1663
1697
|
onOpen(_event, ws) {
|
|
1698
|
+
const state = {
|
|
1699
|
+
listener: null,
|
|
1700
|
+
heartbeatInterval: null,
|
|
1701
|
+
authTimeout: null,
|
|
1702
|
+
isAuthenticated: false,
|
|
1703
|
+
};
|
|
1704
|
+
wsState.set(ws, state);
|
|
1705
|
+
const startAuthenticatedSession = () => {
|
|
1706
|
+
if (state.isAuthenticated)
|
|
1707
|
+
return;
|
|
1708
|
+
state.isAuthenticated = true;
|
|
1709
|
+
if (state.authTimeout) {
|
|
1710
|
+
clearTimeout(state.authTimeout);
|
|
1711
|
+
state.authTimeout = null;
|
|
1712
|
+
}
|
|
1713
|
+
const listener = (event) => {
|
|
1714
|
+
if (partitionId) {
|
|
1715
|
+
const eventPartitionId = event.data.partitionId;
|
|
1716
|
+
if (typeof eventPartitionId !== 'string' ||
|
|
1717
|
+
eventPartitionId !== partitionId) {
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
try {
|
|
1722
|
+
ws.send(JSON.stringify(event));
|
|
1723
|
+
}
|
|
1724
|
+
catch {
|
|
1725
|
+
// Connection closed
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
emitter.addListener(listener);
|
|
1729
|
+
state.listener = listener;
|
|
1730
|
+
ws.send(JSON.stringify({
|
|
1731
|
+
type: 'connected',
|
|
1732
|
+
timestamp: new Date().toISOString(),
|
|
1733
|
+
}));
|
|
1734
|
+
const replayEvents = emitter.replay({
|
|
1735
|
+
since: replaySince,
|
|
1736
|
+
limit: replayLimit,
|
|
1737
|
+
partitionId,
|
|
1738
|
+
});
|
|
1739
|
+
for (const replayEvent of replayEvents) {
|
|
1740
|
+
try {
|
|
1741
|
+
ws.send(JSON.stringify(replayEvent));
|
|
1742
|
+
}
|
|
1743
|
+
catch {
|
|
1744
|
+
// Connection closed
|
|
1745
|
+
break;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
const heartbeatInterval = setInterval(() => {
|
|
1749
|
+
try {
|
|
1750
|
+
ws.send(JSON.stringify({
|
|
1751
|
+
type: 'heartbeat',
|
|
1752
|
+
timestamp: new Date().toISOString(),
|
|
1753
|
+
}));
|
|
1754
|
+
}
|
|
1755
|
+
catch {
|
|
1756
|
+
clearInterval(heartbeatInterval);
|
|
1757
|
+
}
|
|
1758
|
+
}, heartbeatIntervalMs);
|
|
1759
|
+
state.heartbeatInterval = heartbeatInterval;
|
|
1760
|
+
};
|
|
1761
|
+
if (initialAuth) {
|
|
1762
|
+
startAuthenticatedSession();
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
state.authTimeout = setTimeout(() => {
|
|
1766
|
+
const current = wsState.get(ws);
|
|
1767
|
+
if (!current || current.isAuthenticated) {
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
closeUnauthenticated(ws);
|
|
1771
|
+
cleanup(ws);
|
|
1772
|
+
}, 5_000);
|
|
1773
|
+
},
|
|
1774
|
+
async onMessage(event, ws) {
|
|
1775
|
+
const state = wsState.get(ws);
|
|
1776
|
+
if (!state || state.isAuthenticated) {
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
if (typeof event.data !== 'string') {
|
|
1780
|
+
closeUnauthenticated(ws);
|
|
1781
|
+
cleanup(ws);
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
let token = '';
|
|
1785
|
+
try {
|
|
1786
|
+
const parsed = JSON.parse(event.data);
|
|
1787
|
+
if (parsed.type === 'auth' &&
|
|
1788
|
+
typeof parsed.token === 'string' &&
|
|
1789
|
+
parsed.token.trim().length > 0) {
|
|
1790
|
+
token = parsed.token;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
catch {
|
|
1794
|
+
// Ignore parse errors and close as unauthenticated below.
|
|
1795
|
+
}
|
|
1796
|
+
if (!token) {
|
|
1797
|
+
closeUnauthenticated(ws);
|
|
1798
|
+
cleanup(ws);
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
const auth = await authenticateWithBearer(token);
|
|
1802
|
+
const currentState = wsState.get(ws);
|
|
1803
|
+
if (!currentState || currentState.isAuthenticated) {
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1664
1806
|
if (!auth) {
|
|
1665
|
-
ws
|
|
1666
|
-
ws
|
|
1807
|
+
closeUnauthenticated(ws);
|
|
1808
|
+
cleanup(ws);
|
|
1667
1809
|
return;
|
|
1668
1810
|
}
|
|
1669
|
-
|
|
1811
|
+
currentState.isAuthenticated = true;
|
|
1812
|
+
if (currentState.authTimeout) {
|
|
1813
|
+
clearTimeout(currentState.authTimeout);
|
|
1814
|
+
currentState.authTimeout = null;
|
|
1815
|
+
}
|
|
1816
|
+
const listener = (liveEvent) => {
|
|
1670
1817
|
if (partitionId) {
|
|
1671
|
-
const eventPartitionId =
|
|
1818
|
+
const eventPartitionId = liveEvent.data.partitionId;
|
|
1672
1819
|
if (typeof eventPartitionId !== 'string' ||
|
|
1673
1820
|
eventPartitionId !== partitionId) {
|
|
1674
1821
|
return;
|
|
1675
1822
|
}
|
|
1676
1823
|
}
|
|
1677
1824
|
try {
|
|
1678
|
-
ws.send(JSON.stringify(
|
|
1825
|
+
ws.send(JSON.stringify(liveEvent));
|
|
1679
1826
|
}
|
|
1680
1827
|
catch {
|
|
1681
1828
|
// Connection closed
|
|
1682
1829
|
}
|
|
1683
1830
|
};
|
|
1684
1831
|
emitter.addListener(listener);
|
|
1685
|
-
|
|
1832
|
+
currentState.listener = listener;
|
|
1686
1833
|
ws.send(JSON.stringify({
|
|
1687
1834
|
type: 'connected',
|
|
1688
1835
|
timestamp: new Date().toISOString(),
|
|
@@ -1701,7 +1848,6 @@ export function createConsoleRoutes(options) {
|
|
|
1701
1848
|
break;
|
|
1702
1849
|
}
|
|
1703
1850
|
}
|
|
1704
|
-
// Start heartbeat
|
|
1705
1851
|
const heartbeatInterval = setInterval(() => {
|
|
1706
1852
|
try {
|
|
1707
1853
|
ws.send(JSON.stringify({
|
|
@@ -1713,23 +1859,13 @@ export function createConsoleRoutes(options) {
|
|
|
1713
1859
|
clearInterval(heartbeatInterval);
|
|
1714
1860
|
}
|
|
1715
1861
|
}, heartbeatIntervalMs);
|
|
1716
|
-
|
|
1862
|
+
currentState.heartbeatInterval = heartbeatInterval;
|
|
1717
1863
|
},
|
|
1718
1864
|
onClose(_event, ws) {
|
|
1719
|
-
|
|
1720
|
-
if (!state)
|
|
1721
|
-
return;
|
|
1722
|
-
emitter.removeListener(state.listener);
|
|
1723
|
-
clearInterval(state.heartbeatInterval);
|
|
1724
|
-
wsState.delete(ws);
|
|
1865
|
+
cleanup(ws);
|
|
1725
1866
|
},
|
|
1726
1867
|
onError(_event, ws) {
|
|
1727
|
-
|
|
1728
|
-
if (!state)
|
|
1729
|
-
return;
|
|
1730
|
-
emitter.removeListener(state.listener);
|
|
1731
|
-
clearInterval(state.heartbeatInterval);
|
|
1732
|
-
wsState.delete(ws);
|
|
1868
|
+
cleanup(ws);
|
|
1733
1869
|
},
|
|
1734
1870
|
};
|
|
1735
1871
|
}));
|
|
@@ -2704,25 +2840,17 @@ async function hashApiKey(secretKey) {
|
|
|
2704
2840
|
* The token can be set via SYNC_CONSOLE_TOKEN env var or passed directly.
|
|
2705
2841
|
*/
|
|
2706
2842
|
export function createTokenAuthenticator(token) {
|
|
2707
|
-
const expectedToken = token ?? process.env.SYNC_CONSOLE_TOKEN;
|
|
2843
|
+
const expectedToken = (token ?? process.env.SYNC_CONSOLE_TOKEN)?.trim() ?? '';
|
|
2708
2844
|
return async (c) => {
|
|
2709
|
-
if (!expectedToken)
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
}
|
|
2713
|
-
// Check Authorization header
|
|
2714
|
-
const authHeader = c.req.header('Authorization');
|
|
2845
|
+
if (!expectedToken)
|
|
2846
|
+
return null;
|
|
2847
|
+
const authHeader = c.req.header('Authorization')?.trim();
|
|
2715
2848
|
if (authHeader?.startsWith('Bearer ')) {
|
|
2716
|
-
const bearerToken = authHeader.slice(7);
|
|
2849
|
+
const bearerToken = authHeader.slice(7).trim();
|
|
2717
2850
|
if (bearerToken === expectedToken) {
|
|
2718
2851
|
return { consoleUserId: 'token' };
|
|
2719
2852
|
}
|
|
2720
2853
|
}
|
|
2721
|
-
// Check query parameter
|
|
2722
|
-
const queryToken = c.req.query('token');
|
|
2723
|
-
if (queryToken === expectedToken) {
|
|
2724
|
-
return { consoleUserId: 'token' };
|
|
2725
|
-
}
|
|
2726
2854
|
return null;
|
|
2727
2855
|
};
|
|
2728
2856
|
}
|