@syncular/server-hono 0.0.6-56 → 0.0.6-66
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/routes.d.ts +2 -2
- package/dist/console/routes.d.ts.map +1 -1
- package/dist/console/routes.js +103 -96
- package/dist/console/routes.js.map +1 -1
- package/dist/console/types.d.ts +3 -3
- package/dist/console/types.d.ts.map +1 -1
- package/dist/create-server.d.ts +10 -23
- package/dist/create-server.d.ts.map +1 -1
- package/dist/create-server.js +16 -24
- package/dist/create-server.js.map +1 -1
- package/dist/proxy/connection-manager.d.ts +3 -3
- package/dist/proxy/connection-manager.d.ts.map +1 -1
- package/dist/proxy/routes.d.ts +4 -4
- package/dist/proxy/routes.d.ts.map +1 -1
- package/dist/proxy/routes.js +1 -1
- package/dist/routes.d.ts +6 -8
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +9 -13
- package/dist/routes.js.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/create-server.test.ts +28 -4
- package/src/console/routes.ts +131 -120
- package/src/console/types.ts +6 -3
- package/src/create-server.ts +30 -40
- package/src/proxy/connection-manager.ts +3 -3
- package/src/proxy/routes.ts +4 -4
- package/src/routes.ts +23 -32
package/src/console/routes.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { logSyncEvent } from '@syncular/core';
|
|
19
|
-
import type { SyncCoreDb } from '@syncular/server';
|
|
19
|
+
import type { SyncCoreDb, SyncServerAuth } from '@syncular/server';
|
|
20
20
|
import {
|
|
21
21
|
compactChanges,
|
|
22
22
|
computePruneWatermarkCommitSeq,
|
|
@@ -391,9 +391,10 @@ const handlersResponseSchema = z.object({
|
|
|
391
391
|
items: z.array(ConsoleHandlerSchema),
|
|
392
392
|
});
|
|
393
393
|
|
|
394
|
-
export function createConsoleRoutes<
|
|
395
|
-
|
|
396
|
-
|
|
394
|
+
export function createConsoleRoutes<
|
|
395
|
+
DB extends SyncCoreDb,
|
|
396
|
+
Auth extends SyncServerAuth,
|
|
397
|
+
>(options: CreateConsoleRoutesOptions<DB, Auth>): Hono {
|
|
397
398
|
const routes = new Hono();
|
|
398
399
|
|
|
399
400
|
routes.onError((error, context) => {
|
|
@@ -3337,143 +3338,153 @@ export function createConsoleRoutes<DB extends SyncCoreDb>(
|
|
|
3337
3338
|
// -----------------------------------------------------------------------
|
|
3338
3339
|
// Storage endpoints
|
|
3339
3340
|
// -----------------------------------------------------------------------
|
|
3340
|
-
|
|
3341
|
-
const bucket = options.blobBucket;
|
|
3341
|
+
const bucket = options.blobBucket;
|
|
3342
3342
|
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
},
|
|
3343
|
+
routes.get(
|
|
3344
|
+
'/storage',
|
|
3345
|
+
describeRoute({
|
|
3346
|
+
tags: ['console'],
|
|
3347
|
+
summary: 'List storage items',
|
|
3348
|
+
responses: {
|
|
3349
|
+
200: {
|
|
3350
|
+
description: 'Paginated list of storage items',
|
|
3351
|
+
content: {
|
|
3352
|
+
'application/json': {
|
|
3353
|
+
schema: resolver(ConsoleBlobListResponseSchema),
|
|
3355
3354
|
},
|
|
3356
3355
|
},
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
},
|
|
3356
|
+
},
|
|
3357
|
+
401: {
|
|
3358
|
+
description: 'Unauthenticated',
|
|
3359
|
+
content: {
|
|
3360
|
+
'application/json': { schema: resolver(ErrorResponseSchema) },
|
|
3362
3361
|
},
|
|
3363
3362
|
},
|
|
3364
|
-
}
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
const { prefix, cursor, limit } = c.req.valid('query');
|
|
3371
|
-
const listed = await bucket.list({
|
|
3372
|
-
prefix: prefix || undefined,
|
|
3373
|
-
cursor: cursor || undefined,
|
|
3374
|
-
limit,
|
|
3375
|
-
});
|
|
3363
|
+
},
|
|
3364
|
+
}),
|
|
3365
|
+
zValidator('query', ConsoleBlobListQuerySchema),
|
|
3366
|
+
async (c) => {
|
|
3367
|
+
const auth = await requireAuth(c);
|
|
3368
|
+
if (!auth) return c.json({ error: 'UNAUTHENTICATED' }, 401);
|
|
3376
3369
|
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
items: listed.objects.map((obj) => ({
|
|
3380
|
-
key: obj.key,
|
|
3381
|
-
size: obj.size,
|
|
3382
|
-
uploaded: obj.uploaded.toISOString(),
|
|
3383
|
-
httpMetadata: obj.httpMetadata?.contentType
|
|
3384
|
-
? { contentType: obj.httpMetadata.contentType }
|
|
3385
|
-
: undefined,
|
|
3386
|
-
})),
|
|
3387
|
-
truncated: listed.truncated,
|
|
3388
|
-
cursor: listed.cursor ?? null,
|
|
3389
|
-
},
|
|
3390
|
-
200
|
|
3391
|
-
);
|
|
3370
|
+
if (!bucket) {
|
|
3371
|
+
return c.json({ error: 'BLOB_STORAGE_NOT_CONFIGURED' }, 501);
|
|
3392
3372
|
}
|
|
3393
|
-
);
|
|
3394
3373
|
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3374
|
+
const { prefix, cursor, limit } = c.req.valid('query');
|
|
3375
|
+
const listed = await bucket.list({
|
|
3376
|
+
prefix: prefix || undefined,
|
|
3377
|
+
cursor: cursor || undefined,
|
|
3378
|
+
limit,
|
|
3379
|
+
});
|
|
3380
|
+
|
|
3381
|
+
return c.json(
|
|
3382
|
+
{
|
|
3383
|
+
items: listed.objects.map((obj) => ({
|
|
3384
|
+
key: obj.key,
|
|
3385
|
+
size: obj.size,
|
|
3386
|
+
uploaded: obj.uploaded.toISOString(),
|
|
3387
|
+
httpMetadata: obj.httpMetadata?.contentType
|
|
3388
|
+
? { contentType: obj.httpMetadata.contentType }
|
|
3389
|
+
: undefined,
|
|
3390
|
+
})),
|
|
3391
|
+
truncated: listed.truncated,
|
|
3392
|
+
cursor: listed.cursor ?? null,
|
|
3393
|
+
},
|
|
3394
|
+
200
|
|
3395
|
+
);
|
|
3396
|
+
}
|
|
3397
|
+
);
|
|
3398
|
+
|
|
3399
|
+
routes.get(
|
|
3400
|
+
'/storage/:key{.+}/download',
|
|
3401
|
+
describeRoute({
|
|
3402
|
+
tags: ['console'],
|
|
3403
|
+
summary: 'Download a storage item',
|
|
3404
|
+
responses: {
|
|
3405
|
+
200: { description: 'Storage item contents' },
|
|
3406
|
+
401: {
|
|
3407
|
+
description: 'Unauthenticated',
|
|
3408
|
+
content: {
|
|
3409
|
+
'application/json': { schema: resolver(ErrorResponseSchema) },
|
|
3407
3410
|
},
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
},
|
|
3411
|
+
},
|
|
3412
|
+
404: {
|
|
3413
|
+
description: 'Blob not found',
|
|
3414
|
+
content: {
|
|
3415
|
+
'application/json': { schema: resolver(ErrorResponseSchema) },
|
|
3413
3416
|
},
|
|
3414
3417
|
},
|
|
3415
|
-
}
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
const key = decodeURIComponent(c.req.param('key'));
|
|
3421
|
-
const object = await bucket.get(key);
|
|
3422
|
-
if (!object) {
|
|
3423
|
-
return c.json({ error: 'BLOB_NOT_FOUND' }, 404);
|
|
3424
|
-
}
|
|
3418
|
+
},
|
|
3419
|
+
}),
|
|
3420
|
+
async (c) => {
|
|
3421
|
+
const auth = await requireAuth(c);
|
|
3422
|
+
if (!auth) return c.json({ error: 'UNAUTHENTICATED' }, 401);
|
|
3425
3423
|
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
'Content-Type',
|
|
3430
|
-
object.httpMetadata?.contentType ?? 'application/octet-stream'
|
|
3431
|
-
);
|
|
3432
|
-
const filename = key.split('/').pop() || key;
|
|
3433
|
-
headers.set(
|
|
3434
|
-
'Content-Disposition',
|
|
3435
|
-
`attachment; filename="${filename.replace(/"/g, '\\"')}"`
|
|
3436
|
-
);
|
|
3424
|
+
if (!bucket) {
|
|
3425
|
+
return c.json({ error: 'BLOB_STORAGE_NOT_CONFIGURED' }, 501);
|
|
3426
|
+
}
|
|
3437
3427
|
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
});
|
|
3428
|
+
const key = decodeURIComponent(c.req.param('key'));
|
|
3429
|
+
const object = await bucket.get(key);
|
|
3430
|
+
if (!object) {
|
|
3431
|
+
return c.json({ error: 'BLOB_NOT_FOUND' }, 404);
|
|
3442
3432
|
}
|
|
3443
|
-
);
|
|
3444
3433
|
|
|
3445
|
-
|
|
3446
|
-
'
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3434
|
+
const headers = new Headers();
|
|
3435
|
+
headers.set('Content-Length', String(object.size));
|
|
3436
|
+
headers.set(
|
|
3437
|
+
'Content-Type',
|
|
3438
|
+
object.httpMetadata?.contentType ?? 'application/octet-stream'
|
|
3439
|
+
);
|
|
3440
|
+
const filename = key.split('/').pop() || key;
|
|
3441
|
+
headers.set(
|
|
3442
|
+
'Content-Disposition',
|
|
3443
|
+
`attachment; filename="${filename.replace(/"/g, '\\"')}"`
|
|
3444
|
+
);
|
|
3445
|
+
|
|
3446
|
+
return new Response(object.body as ReadableStream, {
|
|
3447
|
+
status: 200,
|
|
3448
|
+
headers,
|
|
3449
|
+
});
|
|
3450
|
+
}
|
|
3451
|
+
);
|
|
3452
|
+
|
|
3453
|
+
routes.delete(
|
|
3454
|
+
'/storage/:key{.+}',
|
|
3455
|
+
describeRoute({
|
|
3456
|
+
tags: ['console'],
|
|
3457
|
+
summary: 'Delete a storage item',
|
|
3458
|
+
responses: {
|
|
3459
|
+
200: {
|
|
3460
|
+
description: 'Storage item deleted',
|
|
3461
|
+
content: {
|
|
3462
|
+
'application/json': {
|
|
3463
|
+
schema: resolver(ConsoleBlobDeleteResponseSchema),
|
|
3457
3464
|
},
|
|
3458
3465
|
},
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
},
|
|
3466
|
+
},
|
|
3467
|
+
401: {
|
|
3468
|
+
description: 'Unauthenticated',
|
|
3469
|
+
content: {
|
|
3470
|
+
'application/json': { schema: resolver(ErrorResponseSchema) },
|
|
3464
3471
|
},
|
|
3465
3472
|
},
|
|
3466
|
-
}
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3473
|
+
},
|
|
3474
|
+
}),
|
|
3475
|
+
async (c) => {
|
|
3476
|
+
const auth = await requireAuth(c);
|
|
3477
|
+
if (!auth) return c.json({ error: 'UNAUTHENTICATED' }, 401);
|
|
3470
3478
|
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
return c.json({ deleted: true }, 200);
|
|
3479
|
+
if (!bucket) {
|
|
3480
|
+
return c.json({ error: 'BLOB_STORAGE_NOT_CONFIGURED' }, 501);
|
|
3474
3481
|
}
|
|
3475
|
-
|
|
3476
|
-
|
|
3482
|
+
|
|
3483
|
+
const key = decodeURIComponent(c.req.param('key'));
|
|
3484
|
+
await bucket.delete(key);
|
|
3485
|
+
return c.json({ deleted: true }, 200);
|
|
3486
|
+
}
|
|
3487
|
+
);
|
|
3477
3488
|
|
|
3478
3489
|
return routes;
|
|
3479
3490
|
}
|
package/src/console/types.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
ServerSyncDialect,
|
|
3
3
|
ServerTableHandler,
|
|
4
4
|
SyncCoreDb,
|
|
5
|
+
SyncServerAuth,
|
|
5
6
|
} from '@syncular/server';
|
|
6
7
|
import type { Context } from 'hono';
|
|
7
8
|
import type { UpgradeWebSocket } from 'hono/ws';
|
|
@@ -90,11 +91,13 @@ export interface ConsoleSharedOptions {
|
|
|
90
91
|
blobBucket?: ConsoleBlobBucket;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
export interface CreateConsoleRoutesOptions<
|
|
94
|
-
extends
|
|
94
|
+
export interface CreateConsoleRoutesOptions<
|
|
95
|
+
DB extends SyncCoreDb = SyncCoreDb,
|
|
96
|
+
Auth extends SyncServerAuth = SyncServerAuth,
|
|
97
|
+
> extends ConsoleSharedOptions {
|
|
95
98
|
db: Kysely<DB>;
|
|
96
99
|
dialect: ServerSyncDialect;
|
|
97
|
-
handlers: ServerTableHandler<DB>[];
|
|
100
|
+
handlers: ServerTableHandler<DB, Auth>[];
|
|
98
101
|
/**
|
|
99
102
|
* Authentication function for console requests.
|
|
100
103
|
* Return null to reject the request.
|
package/src/create-server.ts
CHANGED
|
@@ -2,17 +2,16 @@
|
|
|
2
2
|
* Simplified server factory for Hono
|
|
3
3
|
*
|
|
4
4
|
* Breaking changes from legacy createSyncRoutes:
|
|
5
|
-
* -
|
|
5
|
+
* - sync contract instead of top-level handlers/authenticate
|
|
6
6
|
* - Combined sync + console routes in one call
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type {
|
|
10
|
+
ServerSyncConfig,
|
|
10
11
|
ServerSyncDialect,
|
|
11
|
-
ServerTableHandler,
|
|
12
12
|
SnapshotChunkStorage,
|
|
13
13
|
SyncCoreDb,
|
|
14
14
|
} from '@syncular/server';
|
|
15
|
-
import type { Context } from 'hono';
|
|
16
15
|
import type { UpgradeWebSocket } from 'hono/ws';
|
|
17
16
|
import type { Kysely } from 'kysely';
|
|
18
17
|
import {
|
|
@@ -31,26 +30,24 @@ import {
|
|
|
31
30
|
type SyncRoutesConfigWithRateLimit,
|
|
32
31
|
} from './routes';
|
|
33
32
|
|
|
34
|
-
export interface SyncServerOptions<
|
|
33
|
+
export interface SyncServerOptions<
|
|
34
|
+
DB extends SyncCoreDb = SyncCoreDb,
|
|
35
|
+
Auth extends SyncAuthResult = SyncAuthResult,
|
|
36
|
+
> {
|
|
35
37
|
/** Kysely database instance */
|
|
36
38
|
db: Kysely<DB>;
|
|
37
39
|
|
|
38
40
|
/** Server sync dialect */
|
|
39
41
|
dialect: ServerSyncDialect;
|
|
40
42
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
*/
|
|
44
|
-
handlers: ServerTableHandler<DB>[];
|
|
45
|
-
|
|
46
|
-
/** Authentication function - returns actorId or null for unauthenticated */
|
|
47
|
-
authenticate: (c: Context) => Promise<SyncAuthResult | null>;
|
|
43
|
+
/** Sync contract with auth + table handlers */
|
|
44
|
+
sync: ServerSyncConfig<DB, Auth>;
|
|
48
45
|
|
|
49
46
|
/** Snapshot chunk storage (external body storage, e.g. R2/S3) */
|
|
50
47
|
chunkStorage?: SnapshotChunkStorage;
|
|
51
48
|
|
|
52
49
|
/** Sync route configuration */
|
|
53
|
-
|
|
50
|
+
routes?: SyncRoutesConfigWithRateLimit;
|
|
54
51
|
|
|
55
52
|
/** WebSocket upgrader for realtime */
|
|
56
53
|
upgradeWebSocket?: UpgradeWebSocket;
|
|
@@ -81,45 +78,37 @@ export interface SyncServerResult {
|
|
|
81
78
|
*
|
|
82
79
|
* @example
|
|
83
80
|
* ```typescript
|
|
84
|
-
* // With
|
|
81
|
+
* // With sync contract
|
|
85
82
|
* const { syncRoutes } = createSyncServer({
|
|
86
83
|
* db,
|
|
87
84
|
* dialect,
|
|
88
|
-
*
|
|
89
|
-
* authenticate: async (c) => {
|
|
90
|
-
* const userId = c.req.header('x-user-id');
|
|
91
|
-
* return userId ? { actorId: userId } : null;
|
|
92
|
-
* },
|
|
85
|
+
* sync,
|
|
93
86
|
* });
|
|
94
87
|
*
|
|
95
88
|
* // With custom handlers
|
|
96
89
|
* const { syncRoutes, consoleRoutes } = createSyncServer({
|
|
97
90
|
* db,
|
|
98
91
|
* dialect,
|
|
99
|
-
*
|
|
100
|
-
* authenticate: async (c) => {
|
|
101
|
-
* const userId = c.req.header('x-user-id');
|
|
102
|
-
* return userId ? { actorId: userId } : null;
|
|
103
|
-
* },
|
|
92
|
+
* sync,
|
|
104
93
|
* console: { token: process.env.CONSOLE_TOKEN },
|
|
105
94
|
* });
|
|
106
95
|
* ```
|
|
107
96
|
*/
|
|
108
|
-
export function createSyncServer<
|
|
109
|
-
|
|
110
|
-
|
|
97
|
+
export function createSyncServer<
|
|
98
|
+
DB extends SyncCoreDb = SyncCoreDb,
|
|
99
|
+
Auth extends SyncAuthResult = SyncAuthResult,
|
|
100
|
+
>(options: SyncServerOptions<DB, Auth>): SyncServerResult {
|
|
111
101
|
const {
|
|
112
102
|
db,
|
|
113
103
|
dialect,
|
|
114
|
-
handlers,
|
|
115
|
-
authenticate,
|
|
116
|
-
chunkStorage,
|
|
117
104
|
sync,
|
|
105
|
+
chunkStorage,
|
|
106
|
+
routes,
|
|
118
107
|
upgradeWebSocket,
|
|
119
108
|
console: consoleConfig,
|
|
120
109
|
} = options;
|
|
121
110
|
|
|
122
|
-
if (handlers.length === 0) {
|
|
111
|
+
if (sync.handlers.length === 0) {
|
|
123
112
|
throw new Error('At least one handler must be provided');
|
|
124
113
|
}
|
|
125
114
|
|
|
@@ -146,24 +135,25 @@ export function createSyncServer<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
146
135
|
const syncRoutes = createSyncRoutes({
|
|
147
136
|
db,
|
|
148
137
|
dialect,
|
|
149
|
-
handlers,
|
|
150
|
-
authenticate
|
|
138
|
+
handlers: sync.handlers,
|
|
139
|
+
authenticate: async (context): Promise<Auth | null> =>
|
|
140
|
+
sync.authenticate(context.req.raw),
|
|
151
141
|
chunkStorage,
|
|
152
142
|
consoleLiveEmitter: consoleEventEmitter,
|
|
153
143
|
sync: {
|
|
154
|
-
...
|
|
144
|
+
...routes,
|
|
155
145
|
websocket: upgradeWebSocket
|
|
156
146
|
? {
|
|
157
147
|
enabled: true,
|
|
158
148
|
upgradeWebSocket,
|
|
159
|
-
...(
|
|
160
|
-
heartbeatIntervalMs:
|
|
149
|
+
...(routes?.websocket?.heartbeatIntervalMs !== undefined && {
|
|
150
|
+
heartbeatIntervalMs: routes.websocket.heartbeatIntervalMs,
|
|
161
151
|
}),
|
|
162
|
-
...(
|
|
163
|
-
maxConnectionsTotal:
|
|
152
|
+
...(routes?.websocket?.maxConnectionsTotal !== undefined && {
|
|
153
|
+
maxConnectionsTotal: routes.websocket.maxConnectionsTotal,
|
|
164
154
|
}),
|
|
165
|
-
...(
|
|
166
|
-
maxConnectionsPerClient:
|
|
155
|
+
...(routes?.websocket?.maxConnectionsPerClient !== undefined && {
|
|
156
|
+
maxConnectionsPerClient: routes.websocket.maxConnectionsPerClient,
|
|
167
157
|
}),
|
|
168
158
|
}
|
|
169
159
|
: { enabled: false },
|
|
@@ -178,7 +168,7 @@ export function createSyncServer<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
178
168
|
const consoleRoutes = createConsoleRoutes({
|
|
179
169
|
db,
|
|
180
170
|
dialect,
|
|
181
|
-
handlers,
|
|
171
|
+
handlers: sync.handlers,
|
|
182
172
|
authenticate: createTokenAuthenticator(consoleToken),
|
|
183
173
|
corsOrigins: resolvedConsoleConfig.corsOrigins ?? '*',
|
|
184
174
|
eventEmitter: consoleEventEmitter,
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
} from '@syncular/core';
|
|
12
12
|
import type {
|
|
13
13
|
ExecuteProxyQueryResult,
|
|
14
|
-
|
|
14
|
+
ProxyHandlerCollection,
|
|
15
15
|
ServerSyncDialect,
|
|
16
16
|
SyncCoreDb,
|
|
17
17
|
} from '@syncular/server';
|
|
@@ -26,8 +26,8 @@ export interface ProxyConnectionManagerConfig<
|
|
|
26
26
|
db: Kysely<DB>;
|
|
27
27
|
/** Server sync dialect */
|
|
28
28
|
dialect: ServerSyncDialect;
|
|
29
|
-
/** Proxy table
|
|
30
|
-
handlers:
|
|
29
|
+
/** Proxy table handlers for oplog generation */
|
|
30
|
+
handlers: ProxyHandlerCollection;
|
|
31
31
|
/** Maximum concurrent connections (default: 100) */
|
|
32
32
|
maxConnections?: number;
|
|
33
33
|
/** Idle connection timeout in ms (default: 30000) */
|
package/src/proxy/routes.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
} from '@syncular/core';
|
|
13
13
|
import { logSyncEvent } from '@syncular/core';
|
|
14
14
|
import type {
|
|
15
|
-
|
|
15
|
+
ProxyHandlerCollection,
|
|
16
16
|
ServerSyncDialect,
|
|
17
17
|
SyncCoreDb,
|
|
18
18
|
} from '@syncular/server';
|
|
@@ -48,8 +48,8 @@ interface CreateProxyRoutesConfig<DB extends SyncCoreDb = SyncCoreDb> {
|
|
|
48
48
|
db: Kysely<DB>;
|
|
49
49
|
/** Server sync dialect */
|
|
50
50
|
dialect: ServerSyncDialect;
|
|
51
|
-
/** Proxy table
|
|
52
|
-
handlers:
|
|
51
|
+
/** Proxy table handlers for oplog generation */
|
|
52
|
+
handlers: ProxyHandlerCollection;
|
|
53
53
|
/** Authenticate the request and return actor info */
|
|
54
54
|
authenticate: (c: Context) => Promise<ProxyAuthResult | null>;
|
|
55
55
|
/** WebSocket upgrade function from Hono */
|
|
@@ -75,7 +75,7 @@ interface CreateProxyRoutesConfig<DB extends SyncCoreDb = SyncCoreDb> {
|
|
|
75
75
|
*
|
|
76
76
|
* app.route('/proxy', createProxyRoutes({
|
|
77
77
|
* db,
|
|
78
|
-
* handlers:
|
|
78
|
+
* handlers: proxyHandlers,
|
|
79
79
|
* authenticate: async (c) => {
|
|
80
80
|
* // Verify admin auth
|
|
81
81
|
* return { actorId: 'admin:123' };
|
package/src/routes.ts
CHANGED
|
@@ -23,9 +23,11 @@ import type {
|
|
|
23
23
|
SyncCoreDb,
|
|
24
24
|
SyncRealtimeBroadcaster,
|
|
25
25
|
SyncRealtimeEvent,
|
|
26
|
+
SyncServerAuth,
|
|
26
27
|
} from '@syncular/server';
|
|
27
28
|
import {
|
|
28
29
|
type CompactOptions,
|
|
30
|
+
createServerHandlerCollection,
|
|
29
31
|
InvalidSubscriptionScopeError,
|
|
30
32
|
type PruneOptions,
|
|
31
33
|
type PullResult,
|
|
@@ -33,7 +35,6 @@ import {
|
|
|
33
35
|
pushCommit,
|
|
34
36
|
readSnapshotChunk,
|
|
35
37
|
recordClientCursor,
|
|
36
|
-
TableRegistry,
|
|
37
38
|
} from '@syncular/server';
|
|
38
39
|
import type { Context, MiddlewareHandler } from 'hono';
|
|
39
40
|
import { Hono } from 'hono';
|
|
@@ -64,10 +65,7 @@ import {
|
|
|
64
65
|
const wsConnectionManagerMap = new WeakMap<Hono, WebSocketConnectionManager>();
|
|
65
66
|
const realtimeUnsubscribeMap = new WeakMap<Hono, () => void>();
|
|
66
67
|
|
|
67
|
-
export interface SyncAuthResult {
|
|
68
|
-
actorId: string;
|
|
69
|
-
partitionId?: string;
|
|
70
|
-
}
|
|
68
|
+
export interface SyncAuthResult extends SyncServerAuth {}
|
|
71
69
|
|
|
72
70
|
/**
|
|
73
71
|
* WebSocket configuration for realtime sync.
|
|
@@ -162,11 +160,14 @@ export interface SyncRoutesConfigWithRateLimit {
|
|
|
162
160
|
};
|
|
163
161
|
}
|
|
164
162
|
|
|
165
|
-
export interface CreateSyncRoutesOptions<
|
|
163
|
+
export interface CreateSyncRoutesOptions<
|
|
164
|
+
DB extends SyncCoreDb = SyncCoreDb,
|
|
165
|
+
Auth extends SyncAuthResult = SyncAuthResult,
|
|
166
|
+
> {
|
|
166
167
|
db: Kysely<DB>;
|
|
167
168
|
dialect: ServerSyncDialect;
|
|
168
|
-
handlers: ServerTableHandler<DB>[];
|
|
169
|
-
authenticate: (c: Context) => Promise<
|
|
169
|
+
handlers: ServerTableHandler<DB, Auth>[];
|
|
170
|
+
authenticate: (c: Context) => Promise<Auth | null>;
|
|
170
171
|
sync?: SyncRoutesConfigWithRateLimit;
|
|
171
172
|
wsConnectionManager?: WebSocketConnectionManager;
|
|
172
173
|
/**
|
|
@@ -430,9 +431,10 @@ function emitConsoleLiveEvent(
|
|
|
430
431
|
});
|
|
431
432
|
}
|
|
432
433
|
|
|
433
|
-
export function createSyncRoutes<
|
|
434
|
-
|
|
435
|
-
|
|
434
|
+
export function createSyncRoutes<
|
|
435
|
+
DB extends SyncCoreDb = SyncCoreDb,
|
|
436
|
+
Auth extends SyncAuthResult = SyncAuthResult,
|
|
437
|
+
>(options: CreateSyncRoutesOptions<DB, Auth>): Hono {
|
|
436
438
|
const routes = new Hono();
|
|
437
439
|
routes.onError((error, c) => {
|
|
438
440
|
captureSyncException(error, {
|
|
@@ -442,10 +444,7 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
442
444
|
});
|
|
443
445
|
return c.text('Internal Server Error', 500);
|
|
444
446
|
});
|
|
445
|
-
const handlerRegistry =
|
|
446
|
-
for (const handler of options.handlers) {
|
|
447
|
-
handlerRegistry.register(handler);
|
|
448
|
-
}
|
|
447
|
+
const handlerRegistry = createServerHandlerCollection(options.handlers);
|
|
449
448
|
const config = options.sync ?? {};
|
|
450
449
|
const maxPullLimitCommits = config.maxPullLimitCommits ?? 100;
|
|
451
450
|
const maxSubscriptionsPerPull = config.maxSubscriptionsPerPull ?? 200;
|
|
@@ -631,8 +630,8 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
631
630
|
});
|
|
632
631
|
};
|
|
633
632
|
|
|
634
|
-
const authCache = new WeakMap<Context, Promise<
|
|
635
|
-
const getAuth = (c: Context): Promise<
|
|
633
|
+
const authCache = new WeakMap<Context, Promise<Auth | null>>();
|
|
634
|
+
const getAuth = (c: Context): Promise<Auth | null> => {
|
|
636
635
|
const cached = authCache.get(c);
|
|
637
636
|
if (cached) return cached;
|
|
638
637
|
const pending = options.authenticate(c);
|
|
@@ -787,8 +786,7 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
787
786
|
db: options.db,
|
|
788
787
|
dialect: options.dialect,
|
|
789
788
|
handlers: handlerRegistry,
|
|
790
|
-
|
|
791
|
-
partitionId,
|
|
789
|
+
auth,
|
|
792
790
|
request: {
|
|
793
791
|
clientId,
|
|
794
792
|
clientCommitId: pushBody.clientCommitId,
|
|
@@ -979,8 +977,7 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
979
977
|
db: options.db,
|
|
980
978
|
dialect: options.dialect,
|
|
981
979
|
handlers: handlerRegistry,
|
|
982
|
-
|
|
983
|
-
partitionId,
|
|
980
|
+
auth,
|
|
984
981
|
request,
|
|
985
982
|
chunkStorage: options.chunkStorage,
|
|
986
983
|
});
|
|
@@ -1383,13 +1380,7 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
1383
1380
|
if (!msg || typeof msg !== 'object') return;
|
|
1384
1381
|
|
|
1385
1382
|
if (msg.type === 'push') {
|
|
1386
|
-
void handleWsPush(
|
|
1387
|
-
msg,
|
|
1388
|
-
connRef,
|
|
1389
|
-
auth.actorId,
|
|
1390
|
-
partitionId,
|
|
1391
|
-
clientId
|
|
1392
|
-
);
|
|
1383
|
+
void handleWsPush(msg, connRef, auth, clientId);
|
|
1393
1384
|
return;
|
|
1394
1385
|
}
|
|
1395
1386
|
|
|
@@ -1481,10 +1472,11 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
1481
1472
|
async function handleWsPush(
|
|
1482
1473
|
msg: Record<string, unknown>,
|
|
1483
1474
|
conn: WebSocketConnection,
|
|
1484
|
-
|
|
1485
|
-
partitionId: string,
|
|
1475
|
+
auth: Auth,
|
|
1486
1476
|
clientId: string
|
|
1487
1477
|
): Promise<void> {
|
|
1478
|
+
const actorId = auth.actorId;
|
|
1479
|
+
const partitionId = auth.partitionId ?? 'default';
|
|
1488
1480
|
const requestId = typeof msg.requestId === 'string' ? msg.requestId : '';
|
|
1489
1481
|
if (!requestId) return;
|
|
1490
1482
|
const traceContext = readTraceContextFromMessage(msg);
|
|
@@ -1615,8 +1607,7 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
1615
1607
|
db: options.db,
|
|
1616
1608
|
dialect: options.dialect,
|
|
1617
1609
|
handlers: handlerRegistry,
|
|
1618
|
-
|
|
1619
|
-
partitionId,
|
|
1610
|
+
auth,
|
|
1620
1611
|
request: {
|
|
1621
1612
|
clientId,
|
|
1622
1613
|
clientCommitId: parsed.data.clientCommitId,
|