@syncular/server-hono 0.30.5 → 0.30.6
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/index.js +27 -22
- package/package.json +3 -3
- package/src/index.ts +29 -25
package/dist/index.js
CHANGED
|
@@ -11,14 +11,17 @@ export function createSyncularHono(options) {
|
|
|
11
11
|
const app = new Hono();
|
|
12
12
|
// A `SyncError` answers with its catalog status. Any other exception goes
|
|
13
13
|
// to `config.onError` and answers 500 `sync.internal_error` (§10.2).
|
|
14
|
-
const errorResponse = (error, route = 'sync') => {
|
|
14
|
+
const errorResponse = (c, error, route = 'sync') => {
|
|
15
15
|
const sync = adapterSyncError(error, options.config.onError, route);
|
|
16
|
-
|
|
16
|
+
const response = Response.json(errorBody(sync), {
|
|
17
|
+
status: sync.httpStatus,
|
|
18
|
+
});
|
|
19
|
+
return c.newResponse(response.body, response);
|
|
17
20
|
};
|
|
18
21
|
// Throws outside a route's own try, such as from `authenticate`.
|
|
19
22
|
app.onError((error, c) => {
|
|
20
23
|
const segment = c.req.path.split('/')[1];
|
|
21
|
-
return errorResponse(error, segment === 'operations' || segment === 'segments' || segment === 'blobs'
|
|
24
|
+
return errorResponse(c, error, segment === 'operations' || segment === 'segments' || segment === 'blobs'
|
|
22
25
|
? segment
|
|
23
26
|
: 'sync');
|
|
24
27
|
});
|
|
@@ -26,11 +29,11 @@ export function createSyncularHono(options) {
|
|
|
26
29
|
const contentType = c.req.header('content-type')?.split(';')[0]?.trim();
|
|
27
30
|
if (contentType !== SSP2_CONTENT_TYPE) {
|
|
28
31
|
// §1.1: any other content type is rejected with HTTP 415.
|
|
29
|
-
return
|
|
32
|
+
return c.json(errorBody(new SyncError('sync.invalid_request', 'unsupported content type')), 415);
|
|
30
33
|
}
|
|
31
34
|
const auth = await options.authenticate(c.req.raw);
|
|
32
35
|
if (auth === null)
|
|
33
|
-
return errorResponse(new SyncError('sync.auth_required'));
|
|
36
|
+
return errorResponse(c, new SyncError('sync.auth_required'));
|
|
34
37
|
try {
|
|
35
38
|
const bytes = new Uint8Array(await c.req.arrayBuffer());
|
|
36
39
|
const out = await handleSyncRequest(bytes, {
|
|
@@ -42,20 +45,20 @@ export function createSyncularHono(options) {
|
|
|
42
45
|
});
|
|
43
46
|
}
|
|
44
47
|
catch (error) {
|
|
45
|
-
return errorResponse(error);
|
|
48
|
+
return errorResponse(c, error);
|
|
46
49
|
}
|
|
47
50
|
});
|
|
48
51
|
app.post('/operations', async (c) => {
|
|
49
52
|
const contentType = c.req.header('content-type')?.split(';')[0]?.trim();
|
|
50
53
|
if (contentType !== 'application/vnd.syncular.operations.v1+json') {
|
|
51
|
-
return
|
|
54
|
+
return c.json(errorBody(new SyncError('operation.invalid_request')), 415);
|
|
52
55
|
}
|
|
53
56
|
if (options.operations === undefined) {
|
|
54
|
-
return errorResponse(new SyncError('operation.unknown'));
|
|
57
|
+
return errorResponse(c, new SyncError('operation.unknown'));
|
|
55
58
|
}
|
|
56
59
|
const auth = await options.authenticate(c.req.raw);
|
|
57
60
|
if (auth === null)
|
|
58
|
-
return errorResponse(new SyncError('sync.auth_required'));
|
|
61
|
+
return errorResponse(c, new SyncError('sync.auth_required'));
|
|
59
62
|
try {
|
|
60
63
|
const bytes = new Uint8Array(await c.req.arrayBuffer());
|
|
61
64
|
const out = await handleRemoteOperation(bytes, { ...options.config, ...auth }, options.operations);
|
|
@@ -64,13 +67,13 @@ export function createSyncularHono(options) {
|
|
|
64
67
|
});
|
|
65
68
|
}
|
|
66
69
|
catch (error) {
|
|
67
|
-
return errorResponse(error, 'operations');
|
|
70
|
+
return errorResponse(c, error, 'operations');
|
|
68
71
|
}
|
|
69
72
|
});
|
|
70
73
|
app.get('/segments/:segmentId', async (c) => {
|
|
71
74
|
const auth = await options.authenticate(c.req.raw);
|
|
72
75
|
if (auth === null)
|
|
73
|
-
return errorResponse(new SyncError('sync.auth_required'));
|
|
76
|
+
return errorResponse(c, new SyncError('sync.auth_required'));
|
|
74
77
|
try {
|
|
75
78
|
const result = await openSegmentDownload({ ...options.config, ...auth }, {
|
|
76
79
|
segmentId: c.req.param('segmentId'),
|
|
@@ -87,12 +90,12 @@ export function createSyncularHono(options) {
|
|
|
87
90
|
const encoded = encodeSegmentStream(result.body, acceptEncoding);
|
|
88
91
|
return new Response(encoded.body, {
|
|
89
92
|
status: 200,
|
|
90
|
-
headers: {
|
|
93
|
+
headers: c.newResponse(null, 200, {
|
|
91
94
|
...result.headers,
|
|
92
95
|
...(encoded.contentEncoding !== undefined
|
|
93
96
|
? { 'Content-Encoding': encoded.contentEncoding }
|
|
94
97
|
: { 'Content-Length': String(result.byteLength) }),
|
|
95
|
-
},
|
|
98
|
+
}).headers,
|
|
96
99
|
...ALREADY_ENCODED,
|
|
97
100
|
});
|
|
98
101
|
}
|
|
@@ -104,24 +107,24 @@ export function createSyncularHono(options) {
|
|
|
104
107
|
const encoded = encodeSegmentBody(bytes, acceptEncoding);
|
|
105
108
|
return new Response(encoded.bytes.slice().buffer, {
|
|
106
109
|
status: 200,
|
|
107
|
-
headers: {
|
|
110
|
+
headers: c.newResponse(null, 200, {
|
|
108
111
|
...result.headers,
|
|
109
112
|
...(encoded.contentEncoding !== undefined
|
|
110
113
|
? { 'Content-Encoding': encoded.contentEncoding }
|
|
111
114
|
: {}),
|
|
112
|
-
},
|
|
115
|
+
}).headers,
|
|
113
116
|
...ALREADY_ENCODED,
|
|
114
117
|
});
|
|
115
118
|
}
|
|
116
119
|
catch (error) {
|
|
117
|
-
return errorResponse(error, 'segments');
|
|
120
|
+
return errorResponse(c, error, 'segments');
|
|
118
121
|
}
|
|
119
122
|
});
|
|
120
123
|
// §5.9.3: blob upload with server-side content-address verification.
|
|
121
124
|
app.put('/blobs/:blobId', async (c) => {
|
|
122
125
|
const auth = await options.authenticate(c.req.raw);
|
|
123
126
|
if (auth === null)
|
|
124
|
-
return errorResponse(new SyncError('sync.auth_required'));
|
|
127
|
+
return errorResponse(c, new SyncError('sync.auth_required'));
|
|
125
128
|
try {
|
|
126
129
|
const bytes = new Uint8Array(await c.req.arrayBuffer());
|
|
127
130
|
const contentType = c.req.header('content-type')?.split(';')[0]?.trim();
|
|
@@ -136,14 +139,14 @@ export function createSyncularHono(options) {
|
|
|
136
139
|
return c.body(null, 200);
|
|
137
140
|
}
|
|
138
141
|
catch (error) {
|
|
139
|
-
return errorResponse(error, 'blobs');
|
|
142
|
+
return errorResponse(c, error, 'blobs');
|
|
140
143
|
}
|
|
141
144
|
});
|
|
142
145
|
// §5.9.3: presigned-upload grant — mint a direct-to-storage PUT URL.
|
|
143
146
|
app.post('/blobs/:blobId/upload-grant', async (c) => {
|
|
144
147
|
const auth = await options.authenticate(c.req.raw);
|
|
145
148
|
if (auth === null)
|
|
146
|
-
return errorResponse(new SyncError('sync.auth_required'));
|
|
149
|
+
return errorResponse(c, new SyncError('sync.auth_required'));
|
|
147
150
|
try {
|
|
148
151
|
const body = (await c.req.json().catch(() => ({})));
|
|
149
152
|
const grant = await handleBlobUploadGrant({ ...options.config, ...auth }, {
|
|
@@ -156,7 +159,7 @@ export function createSyncularHono(options) {
|
|
|
156
159
|
return c.json(grant, 200);
|
|
157
160
|
}
|
|
158
161
|
catch (error) {
|
|
159
|
-
return errorResponse(error, 'blobs');
|
|
162
|
+
return errorResponse(c, error, 'blobs');
|
|
160
163
|
}
|
|
161
164
|
});
|
|
162
165
|
// §5.9.5: blob download, re-authorized against referencing rows. When the
|
|
@@ -165,7 +168,7 @@ export function createSyncularHono(options) {
|
|
|
165
168
|
app.get('/blobs/:blobId', async (c) => {
|
|
166
169
|
const auth = await options.authenticate(c.req.raw);
|
|
167
170
|
if (auth === null)
|
|
168
|
-
return errorResponse(new SyncError('sync.auth_required'));
|
|
171
|
+
return errorResponse(c, new SyncError('sync.auth_required'));
|
|
169
172
|
try {
|
|
170
173
|
const result = await handleBlobDownload({ ...options.config, ...auth }, c.req.param('blobId'));
|
|
171
174
|
if (result.url !== undefined) {
|
|
@@ -180,7 +183,7 @@ export function createSyncularHono(options) {
|
|
|
180
183
|
});
|
|
181
184
|
}
|
|
182
185
|
catch (error) {
|
|
183
|
-
return errorResponse(error, 'blobs');
|
|
186
|
+
return errorResponse(c, error, 'blobs');
|
|
184
187
|
}
|
|
185
188
|
});
|
|
186
189
|
return app;
|
|
@@ -192,6 +195,8 @@ export function createSyncularHono(options) {
|
|
|
192
195
|
* would decode gzip inside gzip and fail §5.1 verification
|
|
193
196
|
* (SYNCULAR-WORKERS-SEGMENT-ENCODING-001). Other runtimes ignore the member.
|
|
194
197
|
*/
|
|
198
|
+
// Build segment headers through the Hono context, then keep this runtime-specific
|
|
199
|
+
// Response init. Hono newResponse/body discard encodeBody from their init.
|
|
195
200
|
const ALREADY_ENCODED = {
|
|
196
201
|
encodeBody: 'manual',
|
|
197
202
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/server-hono",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.6",
|
|
4
4
|
"description": "Hono adapter for the Syncular sync server",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
"!dist/**/*.test.d.ts"
|
|
46
46
|
],
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@syncular/server": "0.30.
|
|
48
|
+
"@syncular/server": "0.30.6",
|
|
49
49
|
"hono": "^4.13.7"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@syncular/core": "0.30.
|
|
52
|
+
"@syncular/core": "0.30.6"
|
|
53
53
|
}
|
|
54
54
|
}
|
package/src/index.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
type SyncularErrorRoute,
|
|
23
23
|
type SyncServerConfig,
|
|
24
24
|
} from '@syncular/server';
|
|
25
|
-
import { Hono } from 'hono';
|
|
25
|
+
import { Hono, type Context } from 'hono';
|
|
26
26
|
|
|
27
27
|
export * from './admin';
|
|
28
28
|
|
|
@@ -40,16 +40,21 @@ export function createSyncularHono(options: SyncularHonoOptions): Hono {
|
|
|
40
40
|
// A `SyncError` answers with its catalog status. Any other exception goes
|
|
41
41
|
// to `config.onError` and answers 500 `sync.internal_error` (§10.2).
|
|
42
42
|
const errorResponse = (
|
|
43
|
+
c: Context,
|
|
43
44
|
error: unknown,
|
|
44
45
|
route: SyncularErrorRoute = 'sync',
|
|
45
46
|
): Response => {
|
|
46
47
|
const sync = adapterSyncError(error, options.config.onError, route);
|
|
47
|
-
|
|
48
|
+
const response = Response.json(errorBody(sync), {
|
|
49
|
+
status: sync.httpStatus,
|
|
50
|
+
});
|
|
51
|
+
return c.newResponse(response.body, response);
|
|
48
52
|
};
|
|
49
53
|
// Throws outside a route's own try, such as from `authenticate`.
|
|
50
54
|
app.onError((error, c) => {
|
|
51
55
|
const segment = c.req.path.split('/')[1];
|
|
52
56
|
return errorResponse(
|
|
57
|
+
c,
|
|
53
58
|
error,
|
|
54
59
|
segment === 'operations' || segment === 'segments' || segment === 'blobs'
|
|
55
60
|
? segment
|
|
@@ -61,16 +66,16 @@ export function createSyncularHono(options: SyncularHonoOptions): Hono {
|
|
|
61
66
|
const contentType = c.req.header('content-type')?.split(';')[0]?.trim();
|
|
62
67
|
if (contentType !== SSP2_CONTENT_TYPE) {
|
|
63
68
|
// §1.1: any other content type is rejected with HTTP 415.
|
|
64
|
-
return
|
|
69
|
+
return c.json(
|
|
65
70
|
errorBody(
|
|
66
71
|
new SyncError('sync.invalid_request', 'unsupported content type'),
|
|
67
72
|
),
|
|
68
|
-
|
|
73
|
+
415,
|
|
69
74
|
);
|
|
70
75
|
}
|
|
71
76
|
const auth = await options.authenticate(c.req.raw);
|
|
72
77
|
if (auth === null)
|
|
73
|
-
return errorResponse(new SyncError('sync.auth_required'));
|
|
78
|
+
return errorResponse(c, new SyncError('sync.auth_required'));
|
|
74
79
|
try {
|
|
75
80
|
const bytes = new Uint8Array(await c.req.arrayBuffer());
|
|
76
81
|
const out = await handleSyncRequest(bytes, {
|
|
@@ -81,24 +86,21 @@ export function createSyncularHono(options: SyncularHonoOptions): Hono {
|
|
|
81
86
|
'Content-Type': SSP2_CONTENT_TYPE,
|
|
82
87
|
});
|
|
83
88
|
} catch (error) {
|
|
84
|
-
return errorResponse(error);
|
|
89
|
+
return errorResponse(c, error);
|
|
85
90
|
}
|
|
86
91
|
});
|
|
87
92
|
|
|
88
93
|
app.post('/operations', async (c) => {
|
|
89
94
|
const contentType = c.req.header('content-type')?.split(';')[0]?.trim();
|
|
90
95
|
if (contentType !== 'application/vnd.syncular.operations.v1+json') {
|
|
91
|
-
return
|
|
92
|
-
errorBody(new SyncError('operation.invalid_request')),
|
|
93
|
-
{ status: 415 },
|
|
94
|
-
);
|
|
96
|
+
return c.json(errorBody(new SyncError('operation.invalid_request')), 415);
|
|
95
97
|
}
|
|
96
98
|
if (options.operations === undefined) {
|
|
97
|
-
return errorResponse(new SyncError('operation.unknown'));
|
|
99
|
+
return errorResponse(c, new SyncError('operation.unknown'));
|
|
98
100
|
}
|
|
99
101
|
const auth = await options.authenticate(c.req.raw);
|
|
100
102
|
if (auth === null)
|
|
101
|
-
return errorResponse(new SyncError('sync.auth_required'));
|
|
103
|
+
return errorResponse(c, new SyncError('sync.auth_required'));
|
|
102
104
|
try {
|
|
103
105
|
const bytes = new Uint8Array(await c.req.arrayBuffer());
|
|
104
106
|
const out = await handleRemoteOperation(
|
|
@@ -110,14 +112,14 @@ export function createSyncularHono(options: SyncularHonoOptions): Hono {
|
|
|
110
112
|
'Content-Type': 'application/vnd.syncular.operations.v1+json',
|
|
111
113
|
});
|
|
112
114
|
} catch (error) {
|
|
113
|
-
return errorResponse(error, 'operations');
|
|
115
|
+
return errorResponse(c, error, 'operations');
|
|
114
116
|
}
|
|
115
117
|
});
|
|
116
118
|
|
|
117
119
|
app.get('/segments/:segmentId', async (c) => {
|
|
118
120
|
const auth = await options.authenticate(c.req.raw);
|
|
119
121
|
if (auth === null)
|
|
120
|
-
return errorResponse(new SyncError('sync.auth_required'));
|
|
122
|
+
return errorResponse(c, new SyncError('sync.auth_required'));
|
|
121
123
|
try {
|
|
122
124
|
const result = await openSegmentDownload(
|
|
123
125
|
{ ...options.config, ...auth },
|
|
@@ -137,12 +139,12 @@ export function createSyncularHono(options: SyncularHonoOptions): Hono {
|
|
|
137
139
|
const encoded = encodeSegmentStream(result.body, acceptEncoding);
|
|
138
140
|
return new Response(encoded.body, {
|
|
139
141
|
status: 200,
|
|
140
|
-
headers: {
|
|
142
|
+
headers: c.newResponse(null, 200, {
|
|
141
143
|
...result.headers,
|
|
142
144
|
...(encoded.contentEncoding !== undefined
|
|
143
145
|
? { 'Content-Encoding': encoded.contentEncoding }
|
|
144
146
|
: { 'Content-Length': String(result.byteLength) }),
|
|
145
|
-
},
|
|
147
|
+
}).headers,
|
|
146
148
|
...ALREADY_ENCODED,
|
|
147
149
|
});
|
|
148
150
|
}
|
|
@@ -156,16 +158,16 @@ export function createSyncularHono(options: SyncularHonoOptions): Hono {
|
|
|
156
158
|
const encoded = encodeSegmentBody(bytes, acceptEncoding);
|
|
157
159
|
return new Response(encoded.bytes.slice().buffer as ArrayBuffer, {
|
|
158
160
|
status: 200,
|
|
159
|
-
headers: {
|
|
161
|
+
headers: c.newResponse(null, 200, {
|
|
160
162
|
...result.headers,
|
|
161
163
|
...(encoded.contentEncoding !== undefined
|
|
162
164
|
? { 'Content-Encoding': encoded.contentEncoding }
|
|
163
165
|
: {}),
|
|
164
|
-
},
|
|
166
|
+
}).headers,
|
|
165
167
|
...ALREADY_ENCODED,
|
|
166
168
|
});
|
|
167
169
|
} catch (error) {
|
|
168
|
-
return errorResponse(error, 'segments');
|
|
170
|
+
return errorResponse(c, error, 'segments');
|
|
169
171
|
}
|
|
170
172
|
});
|
|
171
173
|
|
|
@@ -173,7 +175,7 @@ export function createSyncularHono(options: SyncularHonoOptions): Hono {
|
|
|
173
175
|
app.put('/blobs/:blobId', async (c) => {
|
|
174
176
|
const auth = await options.authenticate(c.req.raw);
|
|
175
177
|
if (auth === null)
|
|
176
|
-
return errorResponse(new SyncError('sync.auth_required'));
|
|
178
|
+
return errorResponse(c, new SyncError('sync.auth_required'));
|
|
177
179
|
try {
|
|
178
180
|
const bytes = new Uint8Array(await c.req.arrayBuffer());
|
|
179
181
|
const contentType = c.req.header('content-type')?.split(';')[0]?.trim();
|
|
@@ -190,7 +192,7 @@ export function createSyncularHono(options: SyncularHonoOptions): Hono {
|
|
|
190
192
|
);
|
|
191
193
|
return c.body(null, 200);
|
|
192
194
|
} catch (error) {
|
|
193
|
-
return errorResponse(error, 'blobs');
|
|
195
|
+
return errorResponse(c, error, 'blobs');
|
|
194
196
|
}
|
|
195
197
|
});
|
|
196
198
|
|
|
@@ -198,7 +200,7 @@ export function createSyncularHono(options: SyncularHonoOptions): Hono {
|
|
|
198
200
|
app.post('/blobs/:blobId/upload-grant', async (c) => {
|
|
199
201
|
const auth = await options.authenticate(c.req.raw);
|
|
200
202
|
if (auth === null)
|
|
201
|
-
return errorResponse(new SyncError('sync.auth_required'));
|
|
203
|
+
return errorResponse(c, new SyncError('sync.auth_required'));
|
|
202
204
|
try {
|
|
203
205
|
const body = (await c.req.json().catch(() => ({}))) as {
|
|
204
206
|
byteLength?: number;
|
|
@@ -216,7 +218,7 @@ export function createSyncularHono(options: SyncularHonoOptions): Hono {
|
|
|
216
218
|
);
|
|
217
219
|
return c.json(grant, 200);
|
|
218
220
|
} catch (error) {
|
|
219
|
-
return errorResponse(error, 'blobs');
|
|
221
|
+
return errorResponse(c, error, 'blobs');
|
|
220
222
|
}
|
|
221
223
|
});
|
|
222
224
|
|
|
@@ -226,7 +228,7 @@ export function createSyncularHono(options: SyncularHonoOptions): Hono {
|
|
|
226
228
|
app.get('/blobs/:blobId', async (c) => {
|
|
227
229
|
const auth = await options.authenticate(c.req.raw);
|
|
228
230
|
if (auth === null)
|
|
229
|
-
return errorResponse(new SyncError('sync.auth_required'));
|
|
231
|
+
return errorResponse(c, new SyncError('sync.auth_required'));
|
|
230
232
|
try {
|
|
231
233
|
const result = await handleBlobDownload(
|
|
232
234
|
{ ...options.config, ...auth },
|
|
@@ -246,7 +248,7 @@ export function createSyncularHono(options: SyncularHonoOptions): Hono {
|
|
|
246
248
|
...result.headers,
|
|
247
249
|
});
|
|
248
250
|
} catch (error) {
|
|
249
|
-
return errorResponse(error, 'blobs');
|
|
251
|
+
return errorResponse(c, error, 'blobs');
|
|
250
252
|
}
|
|
251
253
|
});
|
|
252
254
|
|
|
@@ -259,6 +261,8 @@ export function createSyncularHono(options: SyncularHonoOptions): Hono {
|
|
|
259
261
|
* would decode gzip inside gzip and fail §5.1 verification
|
|
260
262
|
* (SYNCULAR-WORKERS-SEGMENT-ENCODING-001). Other runtimes ignore the member.
|
|
261
263
|
*/
|
|
264
|
+
// Build segment headers through the Hono context, then keep this runtime-specific
|
|
265
|
+
// Response init. Hono newResponse/body discard encodeBody from their init.
|
|
262
266
|
const ALREADY_ENCODED: { readonly encodeBody: 'manual' } = {
|
|
263
267
|
encodeBody: 'manual',
|
|
264
268
|
};
|