@verdant-web/server 3.3.11 → 4.0.1
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/bin/server.mjs +110 -107
- package/dist/esm/TokenVerifier.d.ts +1 -0
- package/dist/esm/TokenVerifier.js +3 -0
- package/dist/esm/TokenVerifier.js.map +1 -1
- package/dist/esm/adapters/node/errorHandler.d.ts +2 -0
- package/dist/esm/adapters/node/errorHandler.js +12 -0
- package/dist/esm/adapters/node/errorHandler.js.map +1 -0
- package/dist/esm/adapters/node/getRequestToken.d.ts +1 -0
- package/dist/esm/adapters/node/getRequestToken.js +25 -0
- package/dist/esm/adapters/node/getRequestToken.js.map +1 -0
- package/dist/esm/adapters/node/httpRouter.d.ts +47 -0
- package/dist/esm/adapters/node/httpRouter.js +88 -0
- package/dist/esm/adapters/node/httpRouter.js.map +1 -0
- package/dist/esm/adapters/node/index.d.ts +5 -0
- package/dist/esm/adapters/node/index.js +6 -0
- package/dist/esm/adapters/node/index.js.map +1 -0
- package/dist/esm/adapters/node/tokenMiddleware.d.ts +7 -0
- package/dist/esm/adapters/node/tokenMiddleware.js +22 -0
- package/dist/esm/adapters/node/tokenMiddleware.js.map +1 -0
- package/dist/esm/adapters/node/websocketHandler.d.ts +4 -0
- package/dist/esm/adapters/node/websocketHandler.js +54 -0
- package/dist/esm/adapters/node/websocketHandler.js.map +1 -0
- package/dist/esm/connections/ClientConnection.d.ts +25 -0
- package/dist/esm/{ClientConnection.js → connections/ClientConnection.js} +47 -38
- package/dist/esm/connections/ClientConnection.js.map +1 -0
- package/dist/esm/connections/MessageSender.d.ts +5 -0
- package/dist/esm/connections/MessageSender.js.map +1 -0
- package/dist/esm/connections/Presence.d.ts +23 -0
- package/dist/esm/connections/Presence.js +65 -0
- package/dist/esm/connections/Presence.js.map +1 -0
- package/dist/esm/core.d.ts +15 -0
- package/dist/esm/core.js +15 -0
- package/dist/esm/core.js.map +1 -0
- package/dist/esm/defaults.d.ts +2 -0
- package/dist/esm/defaults.js +3 -0
- package/dist/esm/defaults.js.map +1 -0
- package/dist/esm/files/FileStorage.d.ts +12 -3
- package/dist/esm/files/FileStorage.js +20 -0
- package/dist/esm/files/FileStorage.js.map +1 -1
- package/dist/esm/index.d.ts +7 -7
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internals.d.ts +14 -0
- package/dist/esm/internals.js +12 -0
- package/dist/esm/internals.js.map +1 -0
- package/dist/esm/libraries/Library.d.ts +78 -0
- package/dist/esm/{ServerLibrary.js → libraries/Library.js} +165 -151
- package/dist/esm/libraries/Library.js.map +1 -0
- package/dist/esm/{ServerLibrary.test.js → libraries/Library.test.js} +42 -33
- package/dist/esm/libraries/Library.test.js.map +1 -0
- package/dist/esm/libraries/generic.d.ts +8 -0
- package/dist/esm/libraries/generic.js +2 -0
- package/dist/esm/libraries/generic.js.map +1 -0
- package/dist/esm/libraries/singleNode.d.ts +53 -0
- package/dist/esm/libraries/singleNode.js +88 -0
- package/dist/esm/libraries/singleNode.js.map +1 -0
- package/dist/esm/logger.d.ts +1 -0
- package/dist/esm/logger.js +2 -0
- package/dist/esm/logger.js.map +1 -0
- package/dist/esm/storage/Storage.d.ts +39 -38
- package/dist/esm/storage/StorageManager.d.ts +11 -0
- package/dist/esm/storage/StorageManager.js +32 -0
- package/dist/esm/storage/StorageManager.js.map +1 -0
- package/dist/esm/storage/index.d.ts +1 -2
- package/dist/esm/storage/index.js +1 -2
- package/dist/esm/storage/index.js.map +1 -1
- package/dist/esm/storage/sqlShard/Databases.d.ts +5 -3
- package/dist/esm/storage/sqlShard/Databases.js +11 -7
- package/dist/esm/storage/sqlShard/Databases.js.map +1 -1
- package/dist/esm/storage/sqlShard/SqlBaselines.d.ts +16 -17
- package/dist/esm/storage/sqlShard/SqlBaselines.js +55 -59
- package/dist/esm/storage/sqlShard/SqlBaselines.js.map +1 -1
- package/dist/esm/storage/sqlShard/SqlFileMetadata.d.ts +13 -13
- package/dist/esm/storage/sqlShard/SqlFileMetadata.js +44 -57
- package/dist/esm/storage/sqlShard/SqlFileMetadata.js.map +1 -1
- package/dist/esm/storage/sqlShard/SqlOperations.d.ts +12 -12
- package/dist/esm/storage/sqlShard/SqlOperations.js +38 -73
- package/dist/esm/storage/sqlShard/SqlOperations.js.map +1 -1
- package/dist/esm/storage/sqlShard/SqlReplicas.d.ts +18 -17
- package/dist/esm/storage/sqlShard/SqlReplicas.js +63 -100
- package/dist/esm/storage/sqlShard/SqlReplicas.js.map +1 -1
- package/dist/esm/storage/sqlShard/database.d.ts +25 -5
- package/dist/esm/storage/sqlShard/database.js +47 -15
- package/dist/esm/storage/sqlShard/database.js.map +1 -1
- package/dist/esm/storage/sqlShard/migrations/v1.d.ts +2 -3
- package/dist/esm/storage/sqlShard/migrations/v1.js +42 -47
- package/dist/esm/storage/sqlShard/migrations/v1.js.map +1 -1
- package/dist/esm/storage/sqlShard/migrations/v2.d.ts +2 -3
- package/dist/esm/storage/sqlShard/migrations/v2.js +5 -15
- package/dist/esm/storage/sqlShard/migrations/v2.js.map +1 -1
- package/dist/esm/storage/sqlShard/migrations.d.ts +5 -7
- package/dist/esm/storage/sqlShard/migrations.js +48 -1
- package/dist/esm/storage/sqlShard/migrations.js.map +1 -1
- package/dist/esm/storage/sqlShard/sqlShardStorage.d.ts +25 -4
- package/dist/esm/storage/sqlShard/sqlShardStorage.js +43 -40
- package/dist/esm/storage/sqlShard/sqlShardStorage.js.map +1 -1
- package/dist/esm/storage/sqlShard/tables.d.ts +9 -6
- package/dist/esm/types.d.ts +2 -2
- package/package.json +24 -13
- package/src/TokenVerifier.ts +4 -0
- package/src/adapters/node/errorHandler.ts +18 -0
- package/src/adapters/node/getRequestToken.ts +34 -0
- package/src/adapters/node/httpRouter.ts +139 -0
- package/src/adapters/node/index.ts +5 -0
- package/src/adapters/node/tokenMiddleware.ts +32 -0
- package/src/adapters/node/websocketHandler.ts +79 -0
- package/src/{ClientConnection.ts → connections/ClientConnection.ts} +64 -57
- package/src/connections/MessageSender.ts +6 -0
- package/src/connections/Presence.ts +95 -0
- package/src/core.ts +36 -0
- package/src/defaults.ts +2 -0
- package/src/files/FileStorage.ts +36 -3
- package/src/index.ts +7 -7
- package/src/internals.ts +14 -0
- package/src/{ServerLibrary.test.ts → libraries/Library.test.ts} +51 -49
- package/src/{ServerLibrary.ts → libraries/Library.ts} +205 -206
- package/src/libraries/generic.ts +9 -0
- package/src/libraries/singleNode.ts +145 -0
- package/src/logger.ts +4 -0
- package/src/storage/Storage.ts +37 -66
- package/src/storage/StorageManager.ts +39 -0
- package/src/storage/index.ts +1 -2
- package/src/storage/sqlShard/Databases.ts +26 -13
- package/src/storage/sqlShard/SqlBaselines.ts +58 -67
- package/src/storage/sqlShard/SqlFileMetadata.ts +51 -72
- package/src/storage/sqlShard/SqlOperations.ts +57 -81
- package/src/storage/sqlShard/SqlReplicas.ts +80 -121
- package/src/storage/sqlShard/database.ts +82 -18
- package/src/storage/sqlShard/migrations/v1.ts +42 -52
- package/src/storage/sqlShard/migrations/v2.ts +5 -19
- package/src/storage/sqlShard/migrations.ts +66 -1
- package/src/storage/sqlShard/sqlShardStorage.ts +56 -55
- package/src/storage/sqlShard/tables.ts +6 -6
- package/src/types.ts +2 -4
- package/dist/esm/ClientConnection.d.ts +0 -19
- package/dist/esm/ClientConnection.js.map +0 -1
- package/dist/esm/MessageSender.d.ts +0 -5
- package/dist/esm/MessageSender.js.map +0 -1
- package/dist/esm/Presence.d.ts +0 -18
- package/dist/esm/Presence.js +0 -65
- package/dist/esm/Presence.js.map +0 -1
- package/dist/esm/ReplicaKeepaliveTimers.d.ts +0 -17
- package/dist/esm/ReplicaKeepaliveTimers.js +0 -27
- package/dist/esm/ReplicaKeepaliveTimers.js.map +0 -1
- package/dist/esm/Server.d.ts +0 -166
- package/dist/esm/Server.js +0 -572
- package/dist/esm/Server.js.map +0 -1
- package/dist/esm/ServerLibrary.d.ts +0 -64
- package/dist/esm/ServerLibrary.js.map +0 -1
- package/dist/esm/ServerLibrary.test.js.map +0 -1
- package/dist/esm/storage/sql/SqlBaselines.d.ts +0 -22
- package/dist/esm/storage/sql/SqlBaselines.js +0 -107
- package/dist/esm/storage/sql/SqlBaselines.js.map +0 -1
- package/dist/esm/storage/sql/SqlFileMetadata.d.ts +0 -18
- package/dist/esm/storage/sql/SqlFileMetadata.js +0 -69
- package/dist/esm/storage/sql/SqlFileMetadata.js.map +0 -1
- package/dist/esm/storage/sql/SqlOperations.d.ts +0 -19
- package/dist/esm/storage/sql/SqlOperations.js +0 -122
- package/dist/esm/storage/sql/SqlOperations.js.map +0 -1
- package/dist/esm/storage/sql/SqlReplicas.d.ts +0 -32
- package/dist/esm/storage/sql/SqlReplicas.js +0 -163
- package/dist/esm/storage/sql/SqlReplicas.js.map +0 -1
- package/dist/esm/storage/sql/database.d.ts +0 -9
- package/dist/esm/storage/sql/database.js +0 -24
- package/dist/esm/storage/sql/database.js.map +0 -1
- package/dist/esm/storage/sql/migrations/v1.d.ts +0 -3
- package/dist/esm/storage/sql/migrations/v1.js +0 -59
- package/dist/esm/storage/sql/migrations/v1.js.map +0 -1
- package/dist/esm/storage/sql/migrations/v2.d.ts +0 -3
- package/dist/esm/storage/sql/migrations/v2.js +0 -16
- package/dist/esm/storage/sql/migrations/v2.js.map +0 -1
- package/dist/esm/storage/sql/migrations.d.ts +0 -7
- package/dist/esm/storage/sql/migrations.js +0 -4
- package/dist/esm/storage/sql/migrations.js.map +0 -1
- package/dist/esm/storage/sql/sqlStorage.d.ts +0 -6
- package/dist/esm/storage/sql/sqlStorage.js +0 -35
- package/dist/esm/storage/sql/sqlStorage.js.map +0 -1
- package/dist/esm/storage/sql/tables.d.ts +0 -44
- package/dist/esm/storage/sql/tables.js +0 -2
- package/dist/esm/storage/sql/tables.js.map +0 -1
- package/dist/esm/storage/sqlShard/transfer.d.ts +0 -7
- package/dist/esm/storage/sqlShard/transfer.js +0 -90
- package/dist/esm/storage/sqlShard/transfer.js.map +0 -1
- package/dist/esm/storage/sqlShard/transfer.test.d.ts +0 -1
- package/dist/esm/storage/sqlShard/transfer.test.js +0 -109
- package/dist/esm/storage/sqlShard/transfer.test.js.map +0 -1
- package/src/MessageSender.ts +0 -10
- package/src/Presence.ts +0 -86
- package/src/ReplicaKeepaliveTimers.ts +0 -34
- package/src/Server.ts +0 -859
- package/src/storage/sql/SqlBaselines.ts +0 -147
- package/src/storage/sql/SqlFileMetadata.ts +0 -94
- package/src/storage/sql/SqlOperations.ts +0 -160
- package/src/storage/sql/SqlReplicas.ts +0 -234
- package/src/storage/sql/database.ts +0 -32
- package/src/storage/sql/migrations/v1.ts +0 -64
- package/src/storage/sql/migrations/v2.ts +0 -20
- package/src/storage/sql/migrations.ts +0 -4
- package/src/storage/sql/sqlStorage.ts +0 -57
- package/src/storage/sql/tables.ts +0 -51
- package/src/storage/sqlShard/transfer.test.ts +0 -149
- package/src/storage/sqlShard/transfer.ts +0 -123
- /package/dist/esm/{MessageSender.js → connections/MessageSender.js} +0 -0
- /package/dist/esm/{ServerLibrary.test.d.ts → libraries/Library.test.d.ts} +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { ClientMessage, VerdantError } from '@verdant-web/common';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { Readable } from 'stream';
|
|
4
|
+
import { SingleNodeLibraryManager } from '../../libraries/singleNode.js';
|
|
5
|
+
import { errorHandler } from './errorHandler.js';
|
|
6
|
+
import { tokenMiddleware } from './tokenMiddleware.js';
|
|
7
|
+
|
|
8
|
+
export function createHttpRouter(core: SingleNodeLibraryManager) {
|
|
9
|
+
const mw = tokenMiddleware(core.tokenVerifier);
|
|
10
|
+
const app = new Hono<any>()
|
|
11
|
+
.onError(errorHandler)
|
|
12
|
+
.use('*', async (c, next) => {
|
|
13
|
+
core.log('debug', 'Incoming request', {
|
|
14
|
+
method: c.req.method,
|
|
15
|
+
path: c.req.path,
|
|
16
|
+
});
|
|
17
|
+
await next();
|
|
18
|
+
})
|
|
19
|
+
.post('/', mw, async (ctx) => {
|
|
20
|
+
const key = ctx.get('key');
|
|
21
|
+
const info = ctx.get('tokenInfo');
|
|
22
|
+
|
|
23
|
+
const microserver = await core.get(info.libraryId);
|
|
24
|
+
|
|
25
|
+
const finish = microserver.onRequest?.(key, ctx.req.raw, info);
|
|
26
|
+
|
|
27
|
+
const body = (await ctx.req.json()) as
|
|
28
|
+
| { messages: ClientMessage[] }
|
|
29
|
+
| null
|
|
30
|
+
| undefined;
|
|
31
|
+
|
|
32
|
+
if (!body?.messages) {
|
|
33
|
+
throw new VerdantError(
|
|
34
|
+
VerdantError.Code.BodyRequired,
|
|
35
|
+
undefined,
|
|
36
|
+
'Invalid request body',
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const message of body.messages) {
|
|
41
|
+
await microserver.handleMessage(key, info, message);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const res = finish();
|
|
45
|
+
|
|
46
|
+
return res;
|
|
47
|
+
})
|
|
48
|
+
.post('/files/:fileId', mw, async (ctx) => {
|
|
49
|
+
const info = ctx.get('tokenInfo');
|
|
50
|
+
|
|
51
|
+
const id = ctx.req.param('fileId');
|
|
52
|
+
if (!id) {
|
|
53
|
+
throw new VerdantError(
|
|
54
|
+
VerdantError.Code.NotFound,
|
|
55
|
+
undefined,
|
|
56
|
+
'Supply a file ID as a path parameters',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// TODO: remove this once I figure out why request file body parsing is
|
|
61
|
+
// hanging indefinitely.
|
|
62
|
+
if (core.__testMode) {
|
|
63
|
+
const microserver = await core.get(info.libraryId);
|
|
64
|
+
|
|
65
|
+
core.log(
|
|
66
|
+
'warn',
|
|
67
|
+
'TEST MODE: FILE WILL NOT BE WRITTEN',
|
|
68
|
+
'\n',
|
|
69
|
+
'This Verdant server was launched in Test Mode. This is only meant for use',
|
|
70
|
+
'in automated testing environments. The file just uploaded WILL NOT be',
|
|
71
|
+
'stored on disk or retrievable.',
|
|
72
|
+
);
|
|
73
|
+
// Fake a file upload for testing
|
|
74
|
+
await microserver.uploadFile(
|
|
75
|
+
new Readable({
|
|
76
|
+
read() {
|
|
77
|
+
this.push('test');
|
|
78
|
+
this.push(null);
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
{
|
|
82
|
+
id,
|
|
83
|
+
libraryId: info.libraryId,
|
|
84
|
+
fileName: 'test.txt',
|
|
85
|
+
type: 'text/plain',
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
return ctx.json({ success: true }, 200);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const form = await ctx.req.parseBody();
|
|
92
|
+
const file = form.file;
|
|
93
|
+
if (!file || !(file instanceof File)) {
|
|
94
|
+
throw new VerdantError(
|
|
95
|
+
VerdantError.Code.InvalidRequest,
|
|
96
|
+
undefined,
|
|
97
|
+
'No file was uploaded to the `file` form key',
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const microserver = await core.get(info.libraryId);
|
|
102
|
+
|
|
103
|
+
const fileStream = file.stream();
|
|
104
|
+
await microserver.uploadFile(fileStream, {
|
|
105
|
+
id,
|
|
106
|
+
libraryId: info.libraryId,
|
|
107
|
+
fileName: file.name,
|
|
108
|
+
type: file.type,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return ctx.json({ success: true }, 200);
|
|
112
|
+
})
|
|
113
|
+
.get('/files/:fileId', mw, async (ctx) => {
|
|
114
|
+
const info = ctx.get('tokenInfo');
|
|
115
|
+
|
|
116
|
+
const id = ctx.req.param('fileId');
|
|
117
|
+
if (!id) {
|
|
118
|
+
throw new VerdantError(
|
|
119
|
+
VerdantError.Code.NotFound,
|
|
120
|
+
undefined,
|
|
121
|
+
'Supply a file ID as a path parameters',
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const microserver = await core.get(info.libraryId);
|
|
126
|
+
|
|
127
|
+
const file = await microserver.getFileInfo(id);
|
|
128
|
+
if (!file) {
|
|
129
|
+
throw new VerdantError(
|
|
130
|
+
VerdantError.Code.NotFound,
|
|
131
|
+
undefined,
|
|
132
|
+
`File with ID ${id} not found`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return ctx.json(file, 200);
|
|
137
|
+
});
|
|
138
|
+
return app;
|
|
139
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { generateId } from '@verdant-web/common';
|
|
2
|
+
import { createMiddleware } from 'hono/factory';
|
|
3
|
+
import { TokenInfo, TokenVerifier } from '../../TokenVerifier.js';
|
|
4
|
+
import { getRequestToken } from '../node/getRequestToken.js';
|
|
5
|
+
|
|
6
|
+
export const tokenMiddleware = (tokenVerifier: TokenVerifier) =>
|
|
7
|
+
createMiddleware<{
|
|
8
|
+
Variables: {
|
|
9
|
+
key: string;
|
|
10
|
+
tokenInfo: TokenInfo;
|
|
11
|
+
};
|
|
12
|
+
}>((ctx, next) => {
|
|
13
|
+
if (ctx.req.method === 'OPTIONS') {
|
|
14
|
+
// preflight request, np
|
|
15
|
+
return next();
|
|
16
|
+
}
|
|
17
|
+
const requestToken = getRequestToken(ctx.req.raw)!;
|
|
18
|
+
|
|
19
|
+
const key = generateId();
|
|
20
|
+
const info = tokenVerifier.verifyToken(requestToken);
|
|
21
|
+
ctx.set('key', key);
|
|
22
|
+
ctx.set('tokenInfo', info);
|
|
23
|
+
|
|
24
|
+
// if a Sec-Websocket-Protocol header was sent, we need to echo it back
|
|
25
|
+
// so the websocket connection is accepted by the client
|
|
26
|
+
const protocolHeader = ctx.req.header('Sec-WebSocket-Protocol');
|
|
27
|
+
if (protocolHeader) {
|
|
28
|
+
ctx.res.headers.set('Sec-WebSocket-Protocol', requestToken);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return next();
|
|
32
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ClientMessage,
|
|
3
|
+
VerdantError,
|
|
4
|
+
assert,
|
|
5
|
+
generateId,
|
|
6
|
+
} from '@verdant-web/common';
|
|
7
|
+
import { IncomingMessage } from 'node:http';
|
|
8
|
+
import internal from 'node:stream';
|
|
9
|
+
import { URL } from 'url';
|
|
10
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
11
|
+
import { TokenInfo } from '../../TokenVerifier.js';
|
|
12
|
+
import { SingleNodeLibraryManager } from '../../libraries/singleNode.js';
|
|
13
|
+
|
|
14
|
+
export function createNodeWebsocketHandler(core: SingleNodeLibraryManager) {
|
|
15
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
16
|
+
wss.on(
|
|
17
|
+
'connection',
|
|
18
|
+
async (ws: WebSocket, req: IncomingMessage, info: TokenInfo) => {
|
|
19
|
+
const key = generateId();
|
|
20
|
+
|
|
21
|
+
const microserver = await core.get(info.libraryId);
|
|
22
|
+
|
|
23
|
+
microserver.clientConnections.addSocket(key, ws, info);
|
|
24
|
+
|
|
25
|
+
ws.on('message', async (message: any) => {
|
|
26
|
+
const data = JSON.parse(message.toString()) as ClientMessage;
|
|
27
|
+
await microserver.handleMessage(key, info, data);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
ws.on('close', () => {
|
|
31
|
+
microserver.clientConnections.remove(key);
|
|
32
|
+
core.log('debug', 'Connection closed', { key, userId: info.userId });
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
function getRequestToken(req: IncomingMessage) {
|
|
38
|
+
if (req.headers['sec-websocket-protocol']) {
|
|
39
|
+
// hack: read the token from the websocket protocol header
|
|
40
|
+
const [type, token] = req.headers['sec-websocket-protocol'].split(',');
|
|
41
|
+
if (type === 'Bearer') {
|
|
42
|
+
return token.trim();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
assert(req.url, 'Request URL is required');
|
|
47
|
+
const url = new URL(req.url, 'http://localhost');
|
|
48
|
+
const token = url.searchParams.get('token');
|
|
49
|
+
if (!token) {
|
|
50
|
+
throw new VerdantError(
|
|
51
|
+
VerdantError.Code.NoToken,
|
|
52
|
+
undefined,
|
|
53
|
+
'Token query parameter is required',
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return token;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (req: IncomingMessage, socket: internal.Duplex, head: Buffer) => {
|
|
61
|
+
try {
|
|
62
|
+
const token = getRequestToken(req);
|
|
63
|
+
const info = core.tokenVerifier.verifyToken(token);
|
|
64
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
65
|
+
wss.emit('connection', ws, req, info);
|
|
66
|
+
});
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err instanceof VerdantError && err.httpStatus === 401) {
|
|
69
|
+
socket.write(
|
|
70
|
+
'HTTP/1.1 401 Unauthorized\r\n' +
|
|
71
|
+
'Connection: close\r\n' +
|
|
72
|
+
'Content-Length: 0\r\n' +
|
|
73
|
+
'\r\n',
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
socket.destroy();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { authz,
|
|
1
|
+
import { authz, ServerMessage } from '@verdant-web/common';
|
|
2
2
|
import { IncomingMessage, ServerResponse } from 'http';
|
|
3
|
-
import { WebSocket } from 'ws';
|
|
4
|
-
import {
|
|
3
|
+
import { WebSocket as WSWebSocket } from 'ws';
|
|
4
|
+
import { UserProfileLoader } from '../Profiles.js';
|
|
5
|
+
import { TokenInfo } from '../TokenVerifier.js';
|
|
6
|
+
import { MessageSender } from './MessageSender.js';
|
|
7
|
+
import { Presence } from './Presence.js';
|
|
5
8
|
|
|
6
9
|
abstract class ClientConnection {
|
|
7
10
|
constructor(
|
|
@@ -109,59 +112,79 @@ class FetchClientConnection extends ClientConnection {
|
|
|
109
112
|
};
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
function subscribeToSocketEvent(
|
|
116
|
+
socket: WSWebSocket | WebSocket,
|
|
117
|
+
event: string,
|
|
118
|
+
handler: (data: any) => void,
|
|
119
|
+
) {
|
|
120
|
+
if ('on' in socket) {
|
|
121
|
+
(socket as WSWebSocket).on(event as any, handler);
|
|
122
|
+
return () => (socket as WSWebSocket).off(event as any, handler);
|
|
123
|
+
} else {
|
|
124
|
+
(socket as WebSocket).addEventListener(event as any, (evt) =>
|
|
125
|
+
handler((evt as any).data),
|
|
126
|
+
);
|
|
127
|
+
return () =>
|
|
128
|
+
(socket as WebSocket).removeEventListener(event as any, handler);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
112
132
|
class WebSocketClientConnection extends ClientConnection {
|
|
113
133
|
readonly type = 'websocket';
|
|
114
134
|
|
|
115
135
|
constructor(
|
|
116
136
|
key: string,
|
|
117
|
-
|
|
137
|
+
// supports either NodeJS+WS mock websocket or native WebSocket
|
|
138
|
+
// for Cloudflare / Edge environments
|
|
139
|
+
readonly ws: WebSocket | WSWebSocket,
|
|
118
140
|
info: TokenInfo,
|
|
119
141
|
) {
|
|
120
142
|
super(key, info);
|
|
121
143
|
}
|
|
122
144
|
|
|
123
145
|
handleMessage = (message: ServerMessage) => {
|
|
124
|
-
this.ws.send(JSON.stringify(message));
|
|
146
|
+
return this.ws.send(JSON.stringify(message));
|
|
125
147
|
};
|
|
126
148
|
}
|
|
127
149
|
|
|
128
|
-
export class ClientConnectionManager {
|
|
129
|
-
private readonly connections:
|
|
130
|
-
|
|
150
|
+
export class ClientConnectionManager implements MessageSender {
|
|
151
|
+
private readonly connections: Map<string, ClientConnection> = new Map();
|
|
152
|
+
readonly presence: Presence;
|
|
131
153
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
return this.connections[libraryId]!;
|
|
137
|
-
};
|
|
154
|
+
constructor({ profiles }: { profiles: UserProfileLoader<any> }) {
|
|
155
|
+
this.presence = new Presence(profiles);
|
|
156
|
+
}
|
|
138
157
|
|
|
139
158
|
addSocket = (
|
|
140
|
-
libraryId: string,
|
|
141
159
|
key: string,
|
|
142
|
-
socket: WebSocket,
|
|
160
|
+
socket: WebSocket | WSWebSocket,
|
|
143
161
|
info: TokenInfo,
|
|
144
162
|
) => {
|
|
145
|
-
const lib = this.getLibraryConnections(libraryId);
|
|
146
|
-
|
|
147
163
|
const connection = new WebSocketClientConnection(key, socket, info);
|
|
148
164
|
|
|
149
|
-
|
|
165
|
+
this.connections.set(key, connection);
|
|
150
166
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
167
|
+
const unsubscribes: (() => void)[] = [];
|
|
168
|
+
unsubscribes.push(
|
|
169
|
+
subscribeToSocketEvent(socket, 'close', () => {
|
|
170
|
+
this.connections.delete(key);
|
|
171
|
+
this.presence.removeConnection(key);
|
|
172
|
+
unsubscribes.forEach((u) => u());
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
unsubscribes.push(
|
|
176
|
+
subscribeToSocketEvent(socket, 'messsage', () => {
|
|
177
|
+
this.presence.keepAlive(key);
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
154
180
|
};
|
|
155
181
|
|
|
156
182
|
addRequest = (
|
|
157
|
-
libraryId: string,
|
|
158
183
|
key: string,
|
|
159
184
|
request: IncomingMessage,
|
|
160
185
|
response: ServerResponse,
|
|
161
186
|
info: TokenInfo,
|
|
162
187
|
) => {
|
|
163
|
-
const lib = this.getLibraryConnections(libraryId);
|
|
164
|
-
|
|
165
188
|
const connection = new RequestClientConnection(
|
|
166
189
|
key,
|
|
167
190
|
request,
|
|
@@ -169,47 +192,37 @@ export class ClientConnectionManager {
|
|
|
169
192
|
info,
|
|
170
193
|
);
|
|
171
194
|
|
|
172
|
-
|
|
195
|
+
this.connections.set(key, connection);
|
|
173
196
|
|
|
174
197
|
return () => {
|
|
175
198
|
connection.end();
|
|
176
|
-
|
|
199
|
+
this.connections.delete(key);
|
|
177
200
|
};
|
|
178
201
|
};
|
|
179
202
|
|
|
180
|
-
addFetch = (
|
|
181
|
-
libraryId: string,
|
|
182
|
-
key: string,
|
|
183
|
-
req: Request,
|
|
184
|
-
info: TokenInfo,
|
|
185
|
-
): (() => Response) => {
|
|
186
|
-
const lib = this.getLibraryConnections(libraryId);
|
|
187
|
-
|
|
203
|
+
addFetch = (key: string, req: Request, info: TokenInfo): (() => Response) => {
|
|
188
204
|
const connection = new FetchClientConnection(key, req, info);
|
|
189
205
|
|
|
190
|
-
|
|
206
|
+
this.connections.set(key, connection);
|
|
191
207
|
|
|
192
208
|
return () => {
|
|
193
209
|
connection.end();
|
|
194
|
-
|
|
210
|
+
this.connections.delete(key);
|
|
195
211
|
return connection.res;
|
|
196
212
|
};
|
|
197
213
|
};
|
|
198
214
|
|
|
199
|
-
remove = (
|
|
200
|
-
|
|
201
|
-
|
|
215
|
+
remove = (key: string) => {
|
|
216
|
+
this.connections.delete(key);
|
|
217
|
+
this.presence.removeConnection(key);
|
|
202
218
|
};
|
|
203
219
|
|
|
204
|
-
respond = (
|
|
205
|
-
const
|
|
206
|
-
const connection = lib.get(key);
|
|
220
|
+
respond = (key: string, message: ServerMessage) => {
|
|
221
|
+
const connection = this.connections.get(key);
|
|
207
222
|
if (connection) {
|
|
208
223
|
connection.respond(message);
|
|
209
224
|
} else {
|
|
210
|
-
throw new Error(
|
|
211
|
-
`No connection found for library ${libraryId}, key ${key}`,
|
|
212
|
-
);
|
|
225
|
+
throw new Error(`No connection found for key ${key}`);
|
|
213
226
|
}
|
|
214
227
|
};
|
|
215
228
|
|
|
@@ -217,26 +230,20 @@ export class ClientConnectionManager {
|
|
|
217
230
|
* Broadcasts a message to all live-connected clients in a library.
|
|
218
231
|
* This does not reach push/pull clients.
|
|
219
232
|
*/
|
|
220
|
-
broadcast = (
|
|
221
|
-
|
|
222
|
-
message: ServerMessage,
|
|
223
|
-
omitKeys: string[] = [],
|
|
224
|
-
) => {
|
|
225
|
-
const lib = this.getLibraryConnections(libraryId);
|
|
226
|
-
for (const [key, connection] of lib) {
|
|
233
|
+
broadcast = (message: ServerMessage, omitKeys: string[] = []) => {
|
|
234
|
+
for (const [key, connection] of this.connections) {
|
|
227
235
|
if (!omitKeys.includes(key)) {
|
|
228
236
|
connection.respond(message);
|
|
229
237
|
}
|
|
230
238
|
}
|
|
231
239
|
};
|
|
232
240
|
|
|
233
|
-
disconnectAll = (
|
|
234
|
-
const
|
|
235
|
-
for (const connection of lib.values()) {
|
|
241
|
+
disconnectAll = () => {
|
|
242
|
+
for (const connection of this.connections.values()) {
|
|
236
243
|
if (connection instanceof WebSocketClientConnection) {
|
|
237
244
|
connection.ws.close();
|
|
238
245
|
}
|
|
239
246
|
}
|
|
240
|
-
|
|
247
|
+
this.connections.clear();
|
|
241
248
|
};
|
|
242
249
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EventSubscriber,
|
|
3
|
+
initialInternalPresence,
|
|
4
|
+
UserInfo,
|
|
5
|
+
UserInfoUpdate,
|
|
6
|
+
} from '@verdant-web/common';
|
|
7
|
+
import { UserProfiles } from '../Profiles.js';
|
|
8
|
+
|
|
9
|
+
export type PresenceEvents = {
|
|
10
|
+
lost: (connectionKey: string, userId: string) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Stores client presence in-memory for connected
|
|
15
|
+
* clients
|
|
16
|
+
*/
|
|
17
|
+
export class Presence extends EventSubscriber<PresenceEvents> {
|
|
18
|
+
private presences: Record<string, UserInfo<any, any>> = {};
|
|
19
|
+
private connectionToUser: Record<string, string> = {};
|
|
20
|
+
private userToConnection: Record<string, Set<string>> = {};
|
|
21
|
+
private keepalives = new Map<string, NodeJS.Timeout>();
|
|
22
|
+
|
|
23
|
+
constructor(private profiles: UserProfiles<any>) {
|
|
24
|
+
super();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
set = async (
|
|
28
|
+
connectionKey: string,
|
|
29
|
+
userId: string,
|
|
30
|
+
userInfo: UserInfoUpdate,
|
|
31
|
+
) => {
|
|
32
|
+
if (!this.presences[userId]) {
|
|
33
|
+
this.presences[userId] = {
|
|
34
|
+
...userInfo,
|
|
35
|
+
profile: await this.profiles.get(userId),
|
|
36
|
+
internal: userInfo.internal || initialInternalPresence,
|
|
37
|
+
};
|
|
38
|
+
} else {
|
|
39
|
+
Object.assign(this.presences[userId], userInfo);
|
|
40
|
+
}
|
|
41
|
+
this.connectionToUser[connectionKey] = userId;
|
|
42
|
+
this.userToConnection[userId] =
|
|
43
|
+
this.userToConnection[userId] || new Set<string>();
|
|
44
|
+
this.userToConnection[userId].add(connectionKey);
|
|
45
|
+
|
|
46
|
+
this.keepAlive(connectionKey);
|
|
47
|
+
|
|
48
|
+
return this.presences[userId]!;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
keepAlive = (connectionKey: string) => {
|
|
52
|
+
const existing = this.keepalives.get(connectionKey);
|
|
53
|
+
if (existing) {
|
|
54
|
+
clearTimeout(existing);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.keepalives.set(
|
|
58
|
+
connectionKey,
|
|
59
|
+
setTimeout(() => {
|
|
60
|
+
this.removeConnection(connectionKey);
|
|
61
|
+
this.keepalives.delete(connectionKey);
|
|
62
|
+
}, 30 * 1000),
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
get = (userId: string) => {
|
|
67
|
+
return this.presences[userId];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
removeConnection = (connectionKey: string) => {
|
|
71
|
+
const userId = this.connectionToUser[connectionKey];
|
|
72
|
+
if (!userId) return;
|
|
73
|
+
|
|
74
|
+
this.userToConnection[userId].delete(connectionKey);
|
|
75
|
+
if (this.userToConnection[userId].size === 0) {
|
|
76
|
+
delete this.presences[userId];
|
|
77
|
+
this.emit('lost', connectionKey, userId);
|
|
78
|
+
|
|
79
|
+
// memory cleanup
|
|
80
|
+
if (Object.keys(this.presences).length === 0) {
|
|
81
|
+
this.clear();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
all = () => {
|
|
87
|
+
return this.presences;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
clear = () => {
|
|
91
|
+
this.presences = {};
|
|
92
|
+
this.connectionToUser = {};
|
|
93
|
+
this.userToConnection = {};
|
|
94
|
+
};
|
|
95
|
+
}
|
package/src/core.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { FileStorage } from './files/FileStorage.js';
|
|
2
|
+
import { SingleNodeLibraryManager } from './libraries/singleNode.js';
|
|
3
|
+
import { Logger } from './logger.js';
|
|
4
|
+
import { UserProfileLoader, UserProfiles } from './Profiles.js';
|
|
5
|
+
import { StorageFactory } from './storage/Storage.js';
|
|
6
|
+
|
|
7
|
+
export function createVerdant({
|
|
8
|
+
profiles: profilesSource,
|
|
9
|
+
storage,
|
|
10
|
+
tokenSecret,
|
|
11
|
+
disableRebasing,
|
|
12
|
+
fileStorage,
|
|
13
|
+
log,
|
|
14
|
+
__testMode,
|
|
15
|
+
}: {
|
|
16
|
+
profiles: UserProfiles<any>;
|
|
17
|
+
storage: StorageFactory;
|
|
18
|
+
tokenSecret: string;
|
|
19
|
+
disableRebasing?: boolean;
|
|
20
|
+
fileStorage?: FileStorage;
|
|
21
|
+
log?: Logger;
|
|
22
|
+
__testMode?: boolean;
|
|
23
|
+
}) {
|
|
24
|
+
const profiles = new UserProfileLoader(profilesSource);
|
|
25
|
+
return new SingleNodeLibraryManager({
|
|
26
|
+
profiles,
|
|
27
|
+
storage,
|
|
28
|
+
tokenSecret,
|
|
29
|
+
disableRebasing,
|
|
30
|
+
fileStorage,
|
|
31
|
+
log,
|
|
32
|
+
__testMode,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type VerdantCore = ReturnType<typeof createVerdant>;
|
package/src/defaults.ts
ADDED
package/src/files/FileStorage.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import { Readable } from 'stream';
|
|
3
|
+
import type { Readable } from 'stream';
|
|
4
|
+
import { ReadableStream } from 'stream/web';
|
|
4
5
|
|
|
5
6
|
export interface FileInfo {
|
|
6
7
|
fileName: string;
|
|
@@ -18,7 +19,10 @@ export interface FileInfo {
|
|
|
18
19
|
* even if the file is not yet uploaded, based on its metadata.
|
|
19
20
|
*/
|
|
20
21
|
export interface FileStorage {
|
|
21
|
-
put(
|
|
22
|
+
put(
|
|
23
|
+
fileStream: Readable | ReadableStream | any,
|
|
24
|
+
data: FileInfo,
|
|
25
|
+
): Promise<void>;
|
|
22
26
|
getUrl(data: FileInfo): string | Promise<string>;
|
|
23
27
|
delete(data: FileInfo): Promise<void>;
|
|
24
28
|
}
|
|
@@ -49,7 +53,7 @@ export class LocalFileStorage implements FileStorage {
|
|
|
49
53
|
return host;
|
|
50
54
|
};
|
|
51
55
|
|
|
52
|
-
async put(fileStream:
|
|
56
|
+
async put(fileStream: any, data: FileInfo): Promise<void> {
|
|
53
57
|
this.log('debug', 'Saving file', data.id, data.fileName);
|
|
54
58
|
const filePath = this.getPath(data);
|
|
55
59
|
const containingDirectory = path.dirname(
|
|
@@ -61,6 +65,10 @@ export class LocalFileStorage implements FileStorage {
|
|
|
61
65
|
const location = this.getStorageLocation(data);
|
|
62
66
|
const dest = fs.createWriteStream(location);
|
|
63
67
|
this.log('info', 'File saving to', location);
|
|
68
|
+
if (fileStream instanceof ReadableStream) {
|
|
69
|
+
// @ts-ignore
|
|
70
|
+
fileStream = Readable.fromWeb(fileStream);
|
|
71
|
+
}
|
|
64
72
|
fileStream.pipe(dest);
|
|
65
73
|
}
|
|
66
74
|
|
|
@@ -100,3 +108,28 @@ export class LocalFileStorage implements FileStorage {
|
|
|
100
108
|
return path.join(this.rootDirectory, this.getPath(data));
|
|
101
109
|
}
|
|
102
110
|
}
|
|
111
|
+
|
|
112
|
+
export class FileStorageLibraryDelegate {
|
|
113
|
+
constructor(
|
|
114
|
+
private libraryId: string,
|
|
115
|
+
private fileStorage: FileStorage,
|
|
116
|
+
) {}
|
|
117
|
+
put = (fileStream: Readable, data: Omit<FileInfo, 'libraryId'>) => {
|
|
118
|
+
return this.fileStorage.put(fileStream, {
|
|
119
|
+
...data,
|
|
120
|
+
libraryId: this.libraryId,
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
getUrl = (data: Omit<FileInfo, 'libraryId'>) => {
|
|
124
|
+
return this.fileStorage.getUrl({
|
|
125
|
+
...data,
|
|
126
|
+
libraryId: this.libraryId,
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
delete = (data: Omit<FileInfo, 'libraryId'>) => {
|
|
130
|
+
return this.fileStorage.delete({
|
|
131
|
+
...data,
|
|
132
|
+
libraryId: this.libraryId,
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export type { MessageSender } from './MessageSender.js';
|
|
2
|
-
export type { UserProfiles } from './Profiles.js';
|
|
3
|
-
export type { ClientMessage, ServerMessage } from '@verdant-web/common';
|
|
4
1
|
export { ReplicaType } from '@verdant-web/common';
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
2
|
+
export type { ClientMessage, ServerMessage } from '@verdant-web/common';
|
|
3
|
+
export { createVerdant, type VerdantCore } from './core.js';
|
|
7
4
|
export { LocalFileStorage } from './files/FileStorage.js';
|
|
8
|
-
export type {
|
|
9
|
-
export type
|
|
5
|
+
export type { FileInfo, FileStorage } from './files/FileStorage.js';
|
|
6
|
+
export { type LibraryApi } from './libraries/generic.js';
|
|
7
|
+
export type { UserProfiles } from './Profiles.js';
|
|
8
|
+
export { TokenProvider } from './TokenProvider.js';
|
|
9
|
+
export { type TokenInfo } from './TokenVerifier.js';
|
|
10
10
|
export type { LibraryInfo } from './types.js';
|