@syncular/server-hono 0.0.1-60
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/api-key-auth.d.ts +49 -0
- package/dist/api-key-auth.d.ts.map +1 -0
- package/dist/api-key-auth.js +110 -0
- package/dist/api-key-auth.js.map +1 -0
- package/dist/blobs.d.ts +69 -0
- package/dist/blobs.d.ts.map +1 -0
- package/dist/blobs.js +383 -0
- package/dist/blobs.js.map +1 -0
- package/dist/console/index.d.ts +8 -0
- package/dist/console/index.d.ts.map +1 -0
- package/dist/console/index.js +7 -0
- package/dist/console/index.js.map +1 -0
- package/dist/console/routes.d.ts +106 -0
- package/dist/console/routes.d.ts.map +1 -0
- package/dist/console/routes.js +1612 -0
- package/dist/console/routes.js.map +1 -0
- package/dist/console/schemas.d.ts +308 -0
- package/dist/console/schemas.d.ts.map +1 -0
- package/dist/console/schemas.js +201 -0
- package/dist/console/schemas.js.map +1 -0
- package/dist/create-server.d.ts +78 -0
- package/dist/create-server.d.ts.map +1 -0
- package/dist/create-server.js +99 -0
- package/dist/create-server.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/openapi.d.ts +45 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +59 -0
- package/dist/openapi.js.map +1 -0
- package/dist/proxy/connection-manager.d.ts +78 -0
- package/dist/proxy/connection-manager.d.ts.map +1 -0
- package/dist/proxy/connection-manager.js +251 -0
- package/dist/proxy/connection-manager.js.map +1 -0
- package/dist/proxy/index.d.ts +8 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +8 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/proxy/routes.d.ts +74 -0
- package/dist/proxy/routes.d.ts.map +1 -0
- package/dist/proxy/routes.js +147 -0
- package/dist/proxy/routes.js.map +1 -0
- package/dist/rate-limit.d.ts +101 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +186 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/routes.d.ts +126 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +788 -0
- package/dist/routes.js.map +1 -0
- package/dist/ws.d.ts +230 -0
- package/dist/ws.d.ts.map +1 -0
- package/dist/ws.js +601 -0
- package/dist/ws.js.map +1 -0
- package/package.json +73 -0
- package/src/__tests__/create-server.test.ts +187 -0
- package/src/__tests__/pull-chunk-storage.test.ts +189 -0
- package/src/__tests__/rate-limit.test.ts +78 -0
- package/src/__tests__/realtime-bridge.test.ts +131 -0
- package/src/__tests__/ws-connection-manager.test.ts +176 -0
- package/src/api-key-auth.ts +179 -0
- package/src/blobs.ts +534 -0
- package/src/console/index.ts +17 -0
- package/src/console/routes.ts +2155 -0
- package/src/console/schemas.ts +299 -0
- package/src/create-server.ts +180 -0
- package/src/index.ts +42 -0
- package/src/openapi.ts +74 -0
- package/src/proxy/connection-manager.ts +340 -0
- package/src/proxy/index.ts +8 -0
- package/src/proxy/routes.ts +223 -0
- package/src/rate-limit.ts +321 -0
- package/src/routes.ts +1186 -0
- package/src/ws.ts +789 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/server-hono - Proxy Connection Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages WebSocket connections for the proxy.
|
|
5
|
+
*/
|
|
6
|
+
import { executeProxyQuery } from '@syncular/server';
|
|
7
|
+
/**
|
|
8
|
+
* Manages proxy WebSocket connections and their state.
|
|
9
|
+
*/
|
|
10
|
+
export class ProxyConnectionManager {
|
|
11
|
+
connections = new Map();
|
|
12
|
+
config;
|
|
13
|
+
idleTimeoutMs;
|
|
14
|
+
maxConnections;
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.idleTimeoutMs = config.idleTimeoutMs ?? 30000;
|
|
18
|
+
this.maxConnections = config.maxConnections ?? 100;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Check if a new connection can be accepted.
|
|
22
|
+
*/
|
|
23
|
+
canAccept() {
|
|
24
|
+
return this.connections.size < this.maxConnections;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Get the current connection count.
|
|
28
|
+
*/
|
|
29
|
+
getConnectionCount() {
|
|
30
|
+
return this.connections.size;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Handle the handshake message and register the connection.
|
|
34
|
+
*/
|
|
35
|
+
register(ws, handshake) {
|
|
36
|
+
const state = {
|
|
37
|
+
ws,
|
|
38
|
+
actorId: handshake.actorId,
|
|
39
|
+
clientId: handshake.clientId,
|
|
40
|
+
transaction: null,
|
|
41
|
+
lastActivity: Date.now(),
|
|
42
|
+
idleTimer: null,
|
|
43
|
+
};
|
|
44
|
+
this.connections.set(ws, state);
|
|
45
|
+
this.resetIdleTimer(state);
|
|
46
|
+
return state;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get the connection state for a WebSocket.
|
|
50
|
+
*/
|
|
51
|
+
get(ws) {
|
|
52
|
+
return this.connections.get(ws);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Unregister and cleanup a connection.
|
|
56
|
+
*/
|
|
57
|
+
async unregister(ws) {
|
|
58
|
+
const state = this.connections.get(ws);
|
|
59
|
+
if (!state)
|
|
60
|
+
return;
|
|
61
|
+
// Clear idle timer
|
|
62
|
+
if (state.idleTimer) {
|
|
63
|
+
clearTimeout(state.idleTimer);
|
|
64
|
+
}
|
|
65
|
+
// Rollback any pending transaction by rejecting the promise
|
|
66
|
+
if (state.transaction) {
|
|
67
|
+
const rejectTransaction = state.__rejectTransaction;
|
|
68
|
+
if (rejectTransaction) {
|
|
69
|
+
rejectTransaction(new Error('Connection closed'));
|
|
70
|
+
}
|
|
71
|
+
state.transaction = null;
|
|
72
|
+
state.__resolveTransaction = undefined;
|
|
73
|
+
state.__rejectTransaction = undefined;
|
|
74
|
+
}
|
|
75
|
+
this.connections.delete(ws);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Handle a proxy message and return the response.
|
|
79
|
+
*/
|
|
80
|
+
async handleMessage(ws, message) {
|
|
81
|
+
const state = this.connections.get(ws);
|
|
82
|
+
if (!state) {
|
|
83
|
+
return {
|
|
84
|
+
id: message.id,
|
|
85
|
+
type: 'error',
|
|
86
|
+
error: 'Connection not registered',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// Update activity and reset idle timer
|
|
90
|
+
state.lastActivity = Date.now();
|
|
91
|
+
this.resetIdleTimer(state);
|
|
92
|
+
try {
|
|
93
|
+
switch (message.type) {
|
|
94
|
+
case 'begin':
|
|
95
|
+
return await this.handleBegin(state, message);
|
|
96
|
+
case 'commit':
|
|
97
|
+
return await this.handleCommit(state, message);
|
|
98
|
+
case 'rollback':
|
|
99
|
+
return await this.handleRollback(state, message);
|
|
100
|
+
case 'query':
|
|
101
|
+
return await this.handleQuery(state, message);
|
|
102
|
+
default:
|
|
103
|
+
return {
|
|
104
|
+
id: message.id,
|
|
105
|
+
type: 'error',
|
|
106
|
+
error: `Unknown message type: ${message.type}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
return {
|
|
112
|
+
id: message.id,
|
|
113
|
+
type: 'error',
|
|
114
|
+
error: err instanceof Error ? err.message : 'Unknown error',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async handleBegin(state, message) {
|
|
119
|
+
if (state.transaction) {
|
|
120
|
+
return {
|
|
121
|
+
id: message.id,
|
|
122
|
+
type: 'error',
|
|
123
|
+
error: 'Transaction already in progress',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// Start a transaction and keep it open
|
|
127
|
+
// We use a workaround since Kysely doesn't expose raw transaction control
|
|
128
|
+
return new Promise((resolve) => {
|
|
129
|
+
this.config.db
|
|
130
|
+
.transaction()
|
|
131
|
+
.execute(async (trx) => {
|
|
132
|
+
state.transaction = trx;
|
|
133
|
+
// Wait for commit or rollback
|
|
134
|
+
return new Promise((resolveTransaction, rejectTransaction) => {
|
|
135
|
+
state.__resolveTransaction = resolveTransaction;
|
|
136
|
+
state.__rejectTransaction = rejectTransaction;
|
|
137
|
+
resolve({
|
|
138
|
+
id: message.id,
|
|
139
|
+
type: 'result',
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
})
|
|
143
|
+
.catch(() => {
|
|
144
|
+
// Transaction was rolled back externally
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async handleCommit(state, message) {
|
|
149
|
+
if (!state.transaction) {
|
|
150
|
+
return {
|
|
151
|
+
id: message.id,
|
|
152
|
+
type: 'error',
|
|
153
|
+
error: 'No transaction in progress',
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
// Resolve the transaction promise to commit
|
|
157
|
+
const resolveTransaction = state.__resolveTransaction;
|
|
158
|
+
if (resolveTransaction) {
|
|
159
|
+
resolveTransaction();
|
|
160
|
+
}
|
|
161
|
+
state.transaction = null;
|
|
162
|
+
state.__resolveTransaction = undefined;
|
|
163
|
+
state.__rejectTransaction = undefined;
|
|
164
|
+
return {
|
|
165
|
+
id: message.id,
|
|
166
|
+
type: 'result',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
async handleRollback(state, message) {
|
|
170
|
+
if (!state.transaction) {
|
|
171
|
+
return {
|
|
172
|
+
id: message.id,
|
|
173
|
+
type: 'error',
|
|
174
|
+
error: 'No transaction in progress',
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
// Reject the transaction promise to trigger rollback
|
|
178
|
+
const rejectTransaction = state.__rejectTransaction;
|
|
179
|
+
if (rejectTransaction) {
|
|
180
|
+
rejectTransaction(new Error('Transaction rolled back'));
|
|
181
|
+
}
|
|
182
|
+
state.transaction = null;
|
|
183
|
+
state.__resolveTransaction = undefined;
|
|
184
|
+
state.__rejectTransaction = undefined;
|
|
185
|
+
return {
|
|
186
|
+
id: message.id,
|
|
187
|
+
type: 'result',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
async handleQuery(state, message) {
|
|
191
|
+
if (!message.sql) {
|
|
192
|
+
return {
|
|
193
|
+
id: message.id,
|
|
194
|
+
type: 'error',
|
|
195
|
+
error: 'Missing SQL query',
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const db = state.transaction ?? this.config.db;
|
|
199
|
+
const result = await executeProxyQuery({
|
|
200
|
+
db,
|
|
201
|
+
dialect: this.config.dialect,
|
|
202
|
+
shapes: this.config.shapes,
|
|
203
|
+
ctx: {
|
|
204
|
+
actorId: state.actorId,
|
|
205
|
+
clientId: state.clientId,
|
|
206
|
+
},
|
|
207
|
+
sqlQuery: message.sql,
|
|
208
|
+
parameters: message.parameters ?? [],
|
|
209
|
+
});
|
|
210
|
+
return {
|
|
211
|
+
id: message.id,
|
|
212
|
+
type: 'result',
|
|
213
|
+
rows: result.rows,
|
|
214
|
+
rowCount: result.rowCount,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
resetIdleTimer(state) {
|
|
218
|
+
if (state.idleTimer) {
|
|
219
|
+
clearTimeout(state.idleTimer);
|
|
220
|
+
}
|
|
221
|
+
if (this.idleTimeoutMs <= 0)
|
|
222
|
+
return;
|
|
223
|
+
state.idleTimer = setTimeout(() => {
|
|
224
|
+
// Close idle connection
|
|
225
|
+
try {
|
|
226
|
+
state.ws.close(4000, 'Idle timeout');
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// Ignore close errors
|
|
230
|
+
}
|
|
231
|
+
this.unregister(state.ws);
|
|
232
|
+
}, this.idleTimeoutMs);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Close all connections.
|
|
236
|
+
*/
|
|
237
|
+
async closeAll() {
|
|
238
|
+
const promises = [];
|
|
239
|
+
for (const [ws] of this.connections) {
|
|
240
|
+
try {
|
|
241
|
+
ws.close(1000, 'Server shutdown');
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Ignore close errors
|
|
245
|
+
}
|
|
246
|
+
promises.push(this.unregister(ws));
|
|
247
|
+
}
|
|
248
|
+
await Promise.all(promises);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=connection-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection-manager.js","sourceRoot":"","sources":["../../src/proxy/connection-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,sBAAsB,CAAC;AACrC,cAAc,UAAU,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,sBAAsB,CAAC;AACrC,cAAc,UAAU,CAAC"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/server-hono - Proxy Routes
|
|
3
|
+
*
|
|
4
|
+
* WebSocket endpoint for database proxy.
|
|
5
|
+
*/
|
|
6
|
+
import type { ProxyHandshake, ProxyMessage, ProxyResponse } from '@syncular/core';
|
|
7
|
+
import type { ProxyTableRegistry, ServerSyncDialect, SyncCoreDb } from '@syncular/server';
|
|
8
|
+
import type { Context } from 'hono';
|
|
9
|
+
import { Hono } from 'hono';
|
|
10
|
+
import type { UpgradeWebSocket, WSContext } from 'hono/ws';
|
|
11
|
+
import type { Kysely } from 'kysely';
|
|
12
|
+
/**
|
|
13
|
+
* WeakMap for storing proxy connection manager per Hono instance.
|
|
14
|
+
*/
|
|
15
|
+
interface ProxyConnectionManagerHandle {
|
|
16
|
+
canAccept(): boolean;
|
|
17
|
+
getConnectionCount(): number;
|
|
18
|
+
register(ws: WSContext, handshake: ProxyHandshake): unknown;
|
|
19
|
+
handleMessage(ws: WSContext, message: ProxyMessage): Promise<ProxyResponse>;
|
|
20
|
+
unregister(ws: WSContext): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
interface ProxyAuthResult {
|
|
23
|
+
/** Actor ID for oplog tracking */
|
|
24
|
+
actorId: string;
|
|
25
|
+
}
|
|
26
|
+
interface CreateProxyRoutesConfig<DB extends SyncCoreDb = SyncCoreDb> {
|
|
27
|
+
/** Database connection */
|
|
28
|
+
db: Kysely<DB>;
|
|
29
|
+
/** Server sync dialect */
|
|
30
|
+
dialect: ServerSyncDialect;
|
|
31
|
+
/** Proxy table registry for oplog generation */
|
|
32
|
+
shapes: ProxyTableRegistry;
|
|
33
|
+
/** Authenticate the request and return actor info */
|
|
34
|
+
authenticate: (c: Context) => Promise<ProxyAuthResult | null>;
|
|
35
|
+
/** WebSocket upgrade function from Hono */
|
|
36
|
+
upgradeWebSocket: UpgradeWebSocket;
|
|
37
|
+
/** Maximum concurrent connections (default: 100) */
|
|
38
|
+
maxConnections?: number;
|
|
39
|
+
/** Idle connection timeout in ms (default: 30000) */
|
|
40
|
+
idleTimeoutMs?: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create Hono routes for the proxy WebSocket endpoint.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* import { Hono } from 'hono';
|
|
48
|
+
* import { createBunWebSocket } from 'hono/bun';
|
|
49
|
+
* import { createProxyRoutes } from '@syncular/server-hono/proxy';
|
|
50
|
+
*
|
|
51
|
+
* const { upgradeWebSocket, websocket } = createBunWebSocket();
|
|
52
|
+
*
|
|
53
|
+
* const app = new Hono();
|
|
54
|
+
*
|
|
55
|
+
* app.route('/proxy', createProxyRoutes({
|
|
56
|
+
* db,
|
|
57
|
+
* shapes: proxyTableRegistry,
|
|
58
|
+
* authenticate: async (c) => {
|
|
59
|
+
* // Verify admin auth
|
|
60
|
+
* return { actorId: 'admin:123' };
|
|
61
|
+
* },
|
|
62
|
+
* upgradeWebSocket,
|
|
63
|
+
* }));
|
|
64
|
+
*
|
|
65
|
+
* export default { fetch: app.fetch, websocket };
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare function createProxyRoutes<DB extends SyncCoreDb>(config: CreateProxyRoutesConfig<DB>): Hono;
|
|
69
|
+
/**
|
|
70
|
+
* Get the ProxyConnectionManager from a proxy routes instance.
|
|
71
|
+
*/
|
|
72
|
+
export declare function getProxyConnectionManager(routes: Hono): ProxyConnectionManagerHandle | undefined;
|
|
73
|
+
export {};
|
|
74
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/proxy/routes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,cAAc,EAEd,YAAY,EACZ,aAAa,EACd,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EACV,kBAAkB,EAClB,iBAAiB,EACjB,UAAU,EACX,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC3D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGrC;;GAEG;AACH,UAAU,4BAA4B;IACpC,SAAS,IAAI,OAAO,CAAC;IACrB,kBAAkB,IAAI,MAAM,CAAC;IAC7B,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,cAAc,GAAG,OAAO,CAAC;IAC5D,aAAa,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IAC5E,UAAU,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;AAOD,UAAU,eAAe;IACvB,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,uBAAuB,CAAC,EAAE,SAAS,UAAU,GAAG,UAAU;IAClE,0BAA0B;IAC1B,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACf,0BAA0B;IAC1B,OAAO,EAAE,iBAAiB,CAAC;IAC3B,gDAAgD;IAChD,MAAM,EAAE,kBAAkB,CAAC;IAC3B,qDAAqD;IACrD,YAAY,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;IAC9D,2CAA2C;IAC3C,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,oDAAoD;IACpD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qDAAqD;IACrD,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,SAAS,UAAU,EACrD,MAAM,EAAE,uBAAuB,CAAC,EAAE,CAAC,GAClC,IAAI,CA2HN;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,IAAI,GACX,4BAA4B,GAAG,SAAS,CAE1C"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/server-hono - Proxy Routes
|
|
3
|
+
*
|
|
4
|
+
* WebSocket endpoint for database proxy.
|
|
5
|
+
*/
|
|
6
|
+
import { logSyncEvent } from '@syncular/core';
|
|
7
|
+
import { Hono } from 'hono';
|
|
8
|
+
import { ProxyConnectionManager } from './connection-manager';
|
|
9
|
+
const proxyConnectionManagerMap = new WeakMap();
|
|
10
|
+
/**
|
|
11
|
+
* Create Hono routes for the proxy WebSocket endpoint.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { Hono } from 'hono';
|
|
16
|
+
* import { createBunWebSocket } from 'hono/bun';
|
|
17
|
+
* import { createProxyRoutes } from '@syncular/server-hono/proxy';
|
|
18
|
+
*
|
|
19
|
+
* const { upgradeWebSocket, websocket } = createBunWebSocket();
|
|
20
|
+
*
|
|
21
|
+
* const app = new Hono();
|
|
22
|
+
*
|
|
23
|
+
* app.route('/proxy', createProxyRoutes({
|
|
24
|
+
* db,
|
|
25
|
+
* shapes: proxyTableRegistry,
|
|
26
|
+
* authenticate: async (c) => {
|
|
27
|
+
* // Verify admin auth
|
|
28
|
+
* return { actorId: 'admin:123' };
|
|
29
|
+
* },
|
|
30
|
+
* upgradeWebSocket,
|
|
31
|
+
* }));
|
|
32
|
+
*
|
|
33
|
+
* export default { fetch: app.fetch, websocket };
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function createProxyRoutes(config) {
|
|
37
|
+
const app = new Hono();
|
|
38
|
+
const manager = new ProxyConnectionManager({
|
|
39
|
+
db: config.db,
|
|
40
|
+
dialect: config.dialect,
|
|
41
|
+
shapes: config.shapes,
|
|
42
|
+
maxConnections: config.maxConnections,
|
|
43
|
+
idleTimeoutMs: config.idleTimeoutMs,
|
|
44
|
+
});
|
|
45
|
+
// Store manager for external access if needed
|
|
46
|
+
proxyConnectionManagerMap.set(app, manager);
|
|
47
|
+
// WebSocket upgrade endpoint - using regular route since WebSocket doesn't fit OpenAPI well
|
|
48
|
+
app.get('/', async (c) => {
|
|
49
|
+
// Authenticate before upgrade
|
|
50
|
+
const auth = await config.authenticate(c);
|
|
51
|
+
if (!auth) {
|
|
52
|
+
return c.json({ error: 'UNAUTHENTICATED' }, 401);
|
|
53
|
+
}
|
|
54
|
+
// Check connection limit
|
|
55
|
+
if (!manager.canAccept()) {
|
|
56
|
+
logSyncEvent({
|
|
57
|
+
event: 'proxy.rejected',
|
|
58
|
+
userId: auth.actorId,
|
|
59
|
+
reason: 'max_connections',
|
|
60
|
+
});
|
|
61
|
+
return c.json({ error: 'PROXY_CONNECTION_LIMIT' }, 429);
|
|
62
|
+
}
|
|
63
|
+
logSyncEvent({
|
|
64
|
+
event: 'proxy.connect',
|
|
65
|
+
userId: auth.actorId,
|
|
66
|
+
});
|
|
67
|
+
return config.upgradeWebSocket(c, {
|
|
68
|
+
onOpen(_evt, _ws) {
|
|
69
|
+
// Connection opened, wait for handshake message
|
|
70
|
+
},
|
|
71
|
+
async onMessage(evt, ws) {
|
|
72
|
+
try {
|
|
73
|
+
const data = typeof evt.data === 'string'
|
|
74
|
+
? evt.data
|
|
75
|
+
: new TextDecoder().decode(evt.data);
|
|
76
|
+
const message = JSON.parse(data);
|
|
77
|
+
// Handle handshake
|
|
78
|
+
if (message.type === 'handshake') {
|
|
79
|
+
const handshake = message;
|
|
80
|
+
// Validate that the handshake actor matches authenticated actor
|
|
81
|
+
if (handshake.actorId !== auth.actorId) {
|
|
82
|
+
const ack = {
|
|
83
|
+
type: 'handshake_ack',
|
|
84
|
+
ok: false,
|
|
85
|
+
error: 'Actor ID mismatch',
|
|
86
|
+
};
|
|
87
|
+
ws.send(JSON.stringify(ack));
|
|
88
|
+
ws.close(4001, 'Unauthorized');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
manager.register(ws, handshake);
|
|
92
|
+
const ack = {
|
|
93
|
+
type: 'handshake_ack',
|
|
94
|
+
ok: true,
|
|
95
|
+
};
|
|
96
|
+
ws.send(JSON.stringify(ack));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Handle proxy messages
|
|
100
|
+
const proxyMessage = message;
|
|
101
|
+
const response = await manager.handleMessage(ws, proxyMessage);
|
|
102
|
+
ws.send(JSON.stringify(response));
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
// Send error response if we can parse the message ID
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(typeof evt.data === 'string'
|
|
108
|
+
? evt.data
|
|
109
|
+
: new TextDecoder().decode(evt.data));
|
|
110
|
+
if (parsed.id) {
|
|
111
|
+
ws.send(JSON.stringify({
|
|
112
|
+
id: parsed.id,
|
|
113
|
+
type: 'error',
|
|
114
|
+
error: err instanceof Error ? err.message : 'Unknown error',
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Ignore parse errors
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
async onClose(_evt, ws) {
|
|
124
|
+
await manager.unregister(ws);
|
|
125
|
+
logSyncEvent({
|
|
126
|
+
event: 'proxy.disconnect',
|
|
127
|
+
userId: auth.actorId,
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
async onError(_evt, ws) {
|
|
131
|
+
await manager.unregister(ws);
|
|
132
|
+
logSyncEvent({
|
|
133
|
+
event: 'proxy.error',
|
|
134
|
+
userId: auth.actorId,
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
return app;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get the ProxyConnectionManager from a proxy routes instance.
|
|
143
|
+
*/
|
|
144
|
+
export function getProxyConnectionManager(routes) {
|
|
145
|
+
return proxyConnectionManagerMap.get(routes);
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/proxy/routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/server-hono - Rate limiting middleware for sync endpoints
|
|
3
|
+
*
|
|
4
|
+
* Provides per-user rate limiting to prevent DoS attacks and excessive
|
|
5
|
+
* server load from misbehaving clients.
|
|
6
|
+
*/
|
|
7
|
+
import type { Context, MiddlewareHandler } from 'hono';
|
|
8
|
+
/**
|
|
9
|
+
* Rate limit configuration
|
|
10
|
+
*/
|
|
11
|
+
export interface RateLimitConfig {
|
|
12
|
+
/**
|
|
13
|
+
* Maximum requests per window (default: 60)
|
|
14
|
+
*/
|
|
15
|
+
maxRequests: number;
|
|
16
|
+
/**
|
|
17
|
+
* Time window in milliseconds (default: 60000 = 1 minute)
|
|
18
|
+
*/
|
|
19
|
+
windowMs: number;
|
|
20
|
+
/**
|
|
21
|
+
* Function to extract the rate limit key from a request.
|
|
22
|
+
* Typically returns userId, deviceId, or IP address.
|
|
23
|
+
* Return null to skip rate limiting for this request.
|
|
24
|
+
*/
|
|
25
|
+
keyGenerator: (c: Context) => string | null | Promise<string | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Whether to include rate limit headers in responses (default: true)
|
|
28
|
+
*/
|
|
29
|
+
includeHeaders?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Custom handler for rate-limited requests (optional)
|
|
32
|
+
* If not provided, returns a 429 JSON response
|
|
33
|
+
*/
|
|
34
|
+
onRateLimited?: (c: Context, retryAfterMs: number) => Response | Promise<Response>;
|
|
35
|
+
/**
|
|
36
|
+
* Whether to skip rate limiting in test environments (default: false)
|
|
37
|
+
*/
|
|
38
|
+
skipInTest?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Reset the global store (for testing)
|
|
42
|
+
*/
|
|
43
|
+
export declare function resetRateLimitStore(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Create a rate limiting middleware for Hono.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* import { createRateLimiter } from '@syncular/server-hono';
|
|
50
|
+
*
|
|
51
|
+
* const rateLimiter = createRateLimiter({
|
|
52
|
+
* maxRequests: 60,
|
|
53
|
+
* windowMs: 60_000,
|
|
54
|
+
* keyGenerator: async (c) => {
|
|
55
|
+
* const auth = await authenticate(c);
|
|
56
|
+
* return auth?.userId ?? null;
|
|
57
|
+
* },
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* app.use('/sync/*', rateLimiter);
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function createRateLimiter(config: Partial<RateLimitConfig> & Pick<RateLimitConfig, 'keyGenerator'>): MiddlewareHandler;
|
|
64
|
+
/**
|
|
65
|
+
* Create a rate limiter that uses userId from auth context.
|
|
66
|
+
*
|
|
67
|
+
* This is a convenience function for the common case of rate limiting
|
|
68
|
+
* by authenticated user.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const syncRoutes = createSyncRoutes({
|
|
73
|
+
* db,
|
|
74
|
+
* handlers: [tasksHandler],
|
|
75
|
+
* authenticate,
|
|
76
|
+
* sync: {
|
|
77
|
+
* rateLimit: {
|
|
78
|
+
* pull: { maxRequests: 120, windowMs: 60_000 },
|
|
79
|
+
* push: { maxRequests: 60, windowMs: 60_000 },
|
|
80
|
+
* },
|
|
81
|
+
* },
|
|
82
|
+
* });
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export interface SyncRateLimitConfig {
|
|
86
|
+
/**
|
|
87
|
+
* Rate limit config for pull requests.
|
|
88
|
+
* Set to false to disable rate limiting for pulls.
|
|
89
|
+
*/
|
|
90
|
+
pull?: Omit<RateLimitConfig, 'keyGenerator'> | false;
|
|
91
|
+
/**
|
|
92
|
+
* Rate limit config for push requests.
|
|
93
|
+
* Set to false to disable rate limiting for pushes.
|
|
94
|
+
*/
|
|
95
|
+
push?: Omit<RateLimitConfig, 'keyGenerator'> | false;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Default sync rate limit configuration
|
|
99
|
+
*/
|
|
100
|
+
export declare const DEFAULT_SYNC_RATE_LIMITS: SyncRateLimitConfig;
|
|
101
|
+
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAEvD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;;OAIG;IACH,YAAY,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAErE;;OAEG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;OAGG;IACH,aAAa,CAAC,EAAE,CACd,CAAC,EAAE,OAAO,EACV,YAAY,EAAE,MAAM,KACjB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAElC;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAwHD;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAK1C;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI,CAAC,eAAe,EAAE,cAAc,CAAC,GACvE,iBAAiB,CAqEnB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,IAAI,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,cAAc,CAAC,GAAG,KAAK,CAAC;IAErD;;;OAGG;IACH,IAAI,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,cAAc,CAAC,GAAG,KAAK,CAAC;CACtD;AAED;;GAEG;AACH,eAAO,MAAM,wBAAwB,EAAE,mBAWtC,CAAC"}
|