@syncular/server-hono 0.0.6-159 → 0.0.6-168
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/blobs.d.ts +10 -4
- package/dist/blobs.d.ts.map +1 -1
- package/dist/blobs.js +260 -26
- package/dist/blobs.js.map +1 -1
- package/dist/console/gateway.d.ts +4 -0
- package/dist/console/gateway.d.ts.map +1 -1
- package/dist/console/gateway.js +97 -60
- package/dist/console/gateway.js.map +1 -1
- package/dist/console/route-descriptor.d.ts +6 -0
- package/dist/console/route-descriptor.d.ts.map +1 -0
- package/dist/console/route-descriptor.js +16 -0
- package/dist/console/route-descriptor.js.map +1 -0
- package/dist/console/routes.d.ts.map +1 -1
- package/dist/console/routes.js +153 -108
- package/dist/console/routes.js.map +1 -1
- package/dist/console/schema-errors.d.ts +2 -0
- package/dist/console/schema-errors.d.ts.map +1 -0
- package/dist/console/schema-errors.js +17 -0
- package/dist/console/schema-errors.js.map +1 -0
- package/dist/console/schemas.js +1 -1
- package/dist/console/schemas.js.map +1 -1
- package/dist/console/types.d.ts +32 -0
- package/dist/console/types.d.ts.map +1 -1
- package/dist/create-server.d.ts.map +1 -1
- package/dist/create-server.js +13 -10
- package/dist/create-server.js.map +1 -1
- package/dist/proxy/routes.d.ts +10 -0
- package/dist/proxy/routes.d.ts.map +1 -1
- package/dist/proxy/routes.js +57 -6
- package/dist/proxy/routes.js.map +1 -1
- package/dist/routes.d.ts +21 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +338 -352
- package/dist/routes.js.map +1 -1
- package/package.json +7 -6
- package/src/__tests__/blob-routes.test.ts +286 -18
- package/src/__tests__/console-gateway-live-routes.test.ts +61 -1
- package/src/__tests__/console-routes.test.ts +30 -1
- package/src/__tests__/create-server.test.ts +237 -1
- package/src/__tests__/pull-chunk-storage.test.ts +98 -0
- package/src/__tests__/sync-maintenance.test.ts +15 -2
- package/src/blobs.ts +360 -34
- package/src/console/gateway.ts +335 -288
- package/src/console/route-descriptor.ts +22 -0
- package/src/console/routes.ts +327 -248
- package/src/console/schema-errors.ts +23 -0
- package/src/console/schemas.ts +1 -1
- package/src/console/types.ts +32 -0
- package/src/create-server.ts +13 -10
- package/src/proxy/routes.ts +73 -9
- package/src/routes.ts +449 -396
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const BENIGN_CONSOLE_SCHEMA_ERROR_SUBSTRINGS = [
|
|
2
|
+
'driver has already been destroyed',
|
|
3
|
+
];
|
|
4
|
+
|
|
5
|
+
export function isBenignConsoleSchemaError(error: unknown): boolean {
|
|
6
|
+
const visited = new Set<Error>();
|
|
7
|
+
let current: unknown = error;
|
|
8
|
+
|
|
9
|
+
while (current instanceof Error && !visited.has(current)) {
|
|
10
|
+
visited.add(current);
|
|
11
|
+
const message = current.message.toLowerCase();
|
|
12
|
+
if (
|
|
13
|
+
BENIGN_CONSOLE_SCHEMA_ERROR_SUBSTRINGS.some((substring) =>
|
|
14
|
+
message.includes(substring)
|
|
15
|
+
)
|
|
16
|
+
) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
current = current.cause;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return false;
|
|
23
|
+
}
|
package/src/console/schemas.ts
CHANGED
|
@@ -286,7 +286,7 @@ export type ConsoleApiKeyBulkRevokeResponse = z.infer<
|
|
|
286
286
|
|
|
287
287
|
export const ConsolePaginationQuerySchema = z.object({
|
|
288
288
|
limit: z.coerce.number().int().min(1).max(100).default(50),
|
|
289
|
-
offset: z.coerce.number().int().min(0).default(0),
|
|
289
|
+
offset: z.coerce.number().int().min(0).max(10_000).default(0),
|
|
290
290
|
});
|
|
291
291
|
|
|
292
292
|
export const ConsolePartitionQuerySchema = z.object({
|
package/src/console/types.ts
CHANGED
|
@@ -84,6 +84,13 @@ export interface ConsoleMaintenanceOptions {
|
|
|
84
84
|
* Default: 5000.
|
|
85
85
|
*/
|
|
86
86
|
operationEventsMaxRows?: number;
|
|
87
|
+
/**
|
|
88
|
+
* Max rows to scan per source (commits/events) for `/timeline` requests.
|
|
89
|
+
* Prevents unbounded memory growth on large histories.
|
|
90
|
+
* Set to 0 to disable the guard.
|
|
91
|
+
* Default: 10000.
|
|
92
|
+
*/
|
|
93
|
+
timelineScanMaxRows?: number;
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
export interface ConsoleBlobObject {
|
|
@@ -176,6 +183,31 @@ export interface CreateConsoleRoutesOptions<
|
|
|
176
183
|
* Heartbeat interval in milliseconds. Default: 30000
|
|
177
184
|
*/
|
|
178
185
|
heartbeatIntervalMs?: number;
|
|
186
|
+
/**
|
|
187
|
+
* Maximum inbound websocket message size in bytes.
|
|
188
|
+
* Messages above this limit are rejected and the connection is closed.
|
|
189
|
+
* Default: 1048576 (1 MiB)
|
|
190
|
+
*/
|
|
191
|
+
maxMessageBytes?: number;
|
|
192
|
+
/**
|
|
193
|
+
* Maximum inbound websocket messages allowed per connection within one window.
|
|
194
|
+
* Set to 0 to disable rate limiting.
|
|
195
|
+
* Default: 120
|
|
196
|
+
*/
|
|
197
|
+
maxMessagesPerWindow?: number;
|
|
198
|
+
/**
|
|
199
|
+
* Window size in milliseconds for inbound websocket message rate limiting.
|
|
200
|
+
* Ignored when maxMessagesPerWindow is 0.
|
|
201
|
+
* Default: 10000 (10s)
|
|
202
|
+
*/
|
|
203
|
+
messageRateWindowMs?: number;
|
|
204
|
+
/**
|
|
205
|
+
* Optional list of allowed websocket origins.
|
|
206
|
+
* - undefined: allow all origins
|
|
207
|
+
* - '*': allow all origins
|
|
208
|
+
* - string[]: exact origin match (scheme + host + port)
|
|
209
|
+
*/
|
|
210
|
+
allowedOrigins?: string[] | '*';
|
|
179
211
|
};
|
|
180
212
|
/**
|
|
181
213
|
* Optional console schema readiness promise.
|
package/src/create-server.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
createConsoleRoutes,
|
|
22
22
|
createTokenAuthenticator,
|
|
23
23
|
} from './console/routes';
|
|
24
|
+
import { isBenignConsoleSchemaError } from './console/schema-errors';
|
|
24
25
|
import type {
|
|
25
26
|
ConsoleEventEmitter,
|
|
26
27
|
ConsoleSharedOptions,
|
|
@@ -140,7 +141,12 @@ export function createSyncServer<
|
|
|
140
141
|
: undefined;
|
|
141
142
|
const consoleSchemaReady =
|
|
142
143
|
isConsoleEnabled && dialect.ensureConsoleSchema
|
|
143
|
-
? dialect.ensureConsoleSchema(db)
|
|
144
|
+
? dialect.ensureConsoleSchema(db).catch((error) => {
|
|
145
|
+
if (isBenignConsoleSchemaError(error)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
149
|
+
})
|
|
144
150
|
: undefined;
|
|
145
151
|
|
|
146
152
|
// Create sync routes
|
|
@@ -159,17 +165,9 @@ export function createSyncServer<
|
|
|
159
165
|
...routes,
|
|
160
166
|
websocket: upgradeWebSocket
|
|
161
167
|
? {
|
|
168
|
+
...routes?.websocket,
|
|
162
169
|
enabled: true,
|
|
163
170
|
upgradeWebSocket,
|
|
164
|
-
...(routes?.websocket?.heartbeatIntervalMs !== undefined && {
|
|
165
|
-
heartbeatIntervalMs: routes.websocket.heartbeatIntervalMs,
|
|
166
|
-
}),
|
|
167
|
-
...(routes?.websocket?.maxConnectionsTotal !== undefined && {
|
|
168
|
-
maxConnectionsTotal: routes.websocket.maxConnectionsTotal,
|
|
169
|
-
}),
|
|
170
|
-
...(routes?.websocket?.maxConnectionsPerClient !== undefined && {
|
|
171
|
-
maxConnectionsPerClient: routes.websocket.maxConnectionsPerClient,
|
|
172
|
-
}),
|
|
173
171
|
}
|
|
174
172
|
: { enabled: false },
|
|
175
173
|
},
|
|
@@ -196,6 +194,11 @@ export function createSyncServer<
|
|
|
196
194
|
websocket: {
|
|
197
195
|
enabled: true,
|
|
198
196
|
upgradeWebSocket,
|
|
197
|
+
heartbeatIntervalMs: routes?.websocket?.heartbeatIntervalMs,
|
|
198
|
+
maxMessageBytes: routes?.websocket?.maxMessageBytes,
|
|
199
|
+
maxMessagesPerWindow: routes?.websocket?.maxMessagesPerWindow,
|
|
200
|
+
messageRateWindowMs: routes?.websocket?.messageRateWindowMs,
|
|
201
|
+
allowedOrigins: routes?.websocket?.allowedOrigins,
|
|
199
202
|
},
|
|
200
203
|
}),
|
|
201
204
|
});
|
package/src/proxy/routes.ts
CHANGED
|
@@ -58,6 +58,16 @@ interface CreateProxyRoutesConfig<DB extends SyncCoreDb = SyncCoreDb> {
|
|
|
58
58
|
maxConnections?: number;
|
|
59
59
|
/** Idle connection timeout in ms (default: 30000) */
|
|
60
60
|
idleTimeoutMs?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Maximum inbound websocket message size in bytes.
|
|
63
|
+
* Default: 1 MiB.
|
|
64
|
+
*/
|
|
65
|
+
maxMessageBytes?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Optional list of allowed websocket origins.
|
|
68
|
+
* Use '*' to allow all origins.
|
|
69
|
+
*/
|
|
70
|
+
allowedOrigins?: string[] | '*';
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
/**
|
|
@@ -90,6 +100,7 @@ export function createProxyRoutes<DB extends SyncCoreDb>(
|
|
|
90
100
|
config: CreateProxyRoutesConfig<DB>
|
|
91
101
|
): Hono {
|
|
92
102
|
const app = new Hono();
|
|
103
|
+
const maxMessageBytes = config.maxMessageBytes ?? 1024 * 1024;
|
|
93
104
|
|
|
94
105
|
const manager = new ProxyConnectionManager({
|
|
95
106
|
db: config.db,
|
|
@@ -104,6 +115,10 @@ export function createProxyRoutes<DB extends SyncCoreDb>(
|
|
|
104
115
|
|
|
105
116
|
// WebSocket upgrade endpoint - using regular route since WebSocket doesn't fit OpenAPI well
|
|
106
117
|
app.get('/', async (c) => {
|
|
118
|
+
if (!isWebSocketOriginAllowed(c, config.allowedOrigins)) {
|
|
119
|
+
return c.json({ error: 'FORBIDDEN_ORIGIN' }, 403);
|
|
120
|
+
}
|
|
121
|
+
|
|
107
122
|
// Authenticate before upgrade
|
|
108
123
|
const auth = await config.authenticate(c);
|
|
109
124
|
if (!auth) {
|
|
@@ -132,10 +147,19 @@ export function createProxyRoutes<DB extends SyncCoreDb>(
|
|
|
132
147
|
|
|
133
148
|
async onMessage(evt, ws) {
|
|
134
149
|
try {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
150
|
+
const messageBytes = measureWebSocketMessageBytes(evt.data);
|
|
151
|
+
if (messageBytes > maxMessageBytes) {
|
|
152
|
+
ws.send(
|
|
153
|
+
JSON.stringify({
|
|
154
|
+
type: 'error',
|
|
155
|
+
error: `Message exceeds max size (${maxMessageBytes} bytes)`,
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
ws.close(1009, 'Message too large');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const data = decodeWebSocketData(evt.data);
|
|
139
163
|
|
|
140
164
|
const message = JSON.parse(data);
|
|
141
165
|
|
|
@@ -172,11 +196,7 @@ export function createProxyRoutes<DB extends SyncCoreDb>(
|
|
|
172
196
|
} catch (err) {
|
|
173
197
|
// Send error response if we can parse the message ID
|
|
174
198
|
try {
|
|
175
|
-
const parsed = JSON.parse(
|
|
176
|
-
typeof evt.data === 'string'
|
|
177
|
-
? evt.data
|
|
178
|
-
: new TextDecoder().decode(evt.data as ArrayBuffer)
|
|
179
|
-
);
|
|
199
|
+
const parsed = JSON.parse(decodeWebSocketData(evt.data));
|
|
180
200
|
if (parsed.id) {
|
|
181
201
|
ws.send(
|
|
182
202
|
JSON.stringify({
|
|
@@ -213,6 +233,50 @@ export function createProxyRoutes<DB extends SyncCoreDb>(
|
|
|
213
233
|
return app;
|
|
214
234
|
}
|
|
215
235
|
|
|
236
|
+
function isWebSocketOriginAllowed(
|
|
237
|
+
c: Context,
|
|
238
|
+
allowedOrigins?: string[] | '*'
|
|
239
|
+
): boolean {
|
|
240
|
+
if (!allowedOrigins) return true;
|
|
241
|
+
if (allowedOrigins === '*') return true;
|
|
242
|
+
|
|
243
|
+
const origin = c.req.header('origin');
|
|
244
|
+
if (!origin) return false;
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const normalizedOrigin = new URL(origin).origin;
|
|
248
|
+
return allowedOrigins.includes(normalizedOrigin);
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function measureWebSocketMessageBytes(data: unknown): number {
|
|
255
|
+
if (typeof data === 'string') {
|
|
256
|
+
return new TextEncoder().encode(data).byteLength;
|
|
257
|
+
}
|
|
258
|
+
if (data instanceof ArrayBuffer) {
|
|
259
|
+
return data.byteLength;
|
|
260
|
+
}
|
|
261
|
+
if (ArrayBuffer.isView(data)) {
|
|
262
|
+
return data.byteLength;
|
|
263
|
+
}
|
|
264
|
+
if (typeof Blob !== 'undefined' && data instanceof Blob) {
|
|
265
|
+
return data.size;
|
|
266
|
+
}
|
|
267
|
+
return new TextEncoder().encode(String(data)).byteLength;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function decodeWebSocketData(data: unknown): string {
|
|
271
|
+
if (typeof data === 'string') return data;
|
|
272
|
+
if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
|
|
273
|
+
if (ArrayBuffer.isView(data)) {
|
|
274
|
+
const view = data as Uint8Array;
|
|
275
|
+
return new TextDecoder().decode(view);
|
|
276
|
+
}
|
|
277
|
+
return String(data);
|
|
278
|
+
}
|
|
279
|
+
|
|
216
280
|
/**
|
|
217
281
|
* Get the ProxyConnectionManager from a proxy routes instance.
|
|
218
282
|
*/
|