@syncular/server-hono 0.30.5 → 0.30.7

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.
Files changed (3) hide show
  1. package/dist/index.js +27 -22
  2. package/package.json +3 -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
- return Response.json(errorBody(sync), { status: sync.httpStatus });
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 Response.json(errorBody(new SyncError('sync.invalid_request', 'unsupported content type')), { status: 415 });
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 Response.json(errorBody(new SyncError('operation.invalid_request')), { status: 415 });
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.5",
3
+ "version": "0.30.7",
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.5",
48
+ "@syncular/server": "0.30.7",
49
49
  "hono": "^4.13.7"
50
50
  },
51
51
  "devDependencies": {
52
- "@syncular/core": "0.30.5"
52
+ "@syncular/core": "0.30.7"
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
- return Response.json(errorBody(sync), { status: sync.httpStatus });
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 Response.json(
69
+ return c.json(
65
70
  errorBody(
66
71
  new SyncError('sync.invalid_request', 'unsupported content type'),
67
72
  ),
68
- { status: 415 },
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 Response.json(
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
  };