@syncular/server-hono 0.15.45 → 0.15.47

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/admin.d.ts CHANGED
@@ -7,8 +7,8 @@
7
7
  * refuses to mount without a host-provided `authorize` guard (it throws at
8
8
  * construction). Every request runs the guard first; a falsy result is a
9
9
  * 401. Admin is a privileged surface (it reads every partition's clients,
10
- * commit metadata, and scope activity) and SPEC.md deliberately says nothing
11
- * about it — authorization is entirely the host's, and mandatory.
10
+ * commit metadata, scope activity, and reactions) and authorization is
11
+ * entirely the host's and mandatory.
12
12
  */
13
13
  import { type SyncularAdmin } from '@syncular/server';
14
14
  import { Hono } from 'hono';
package/dist/admin.js CHANGED
@@ -7,8 +7,8 @@
7
7
  * refuses to mount without a host-provided `authorize` guard (it throws at
8
8
  * construction). Every request runs the guard first; a falsy result is a
9
9
  * 401. Admin is a privileged surface (it reads every partition's clients,
10
- * commit metadata, and scope activity) and SPEC.md deliberately says nothing
11
- * about it — authorization is entirely the host's, and mandatory.
10
+ * commit metadata, scope activity, and reactions) and authorization is
11
+ * entirely the host's and mandatory.
12
12
  */
13
13
  import { errorBody, matchesRingQuery, SyncError, } from '@syncular/server';
14
14
  import { Hono } from 'hono';
@@ -113,6 +113,32 @@ export function createSyncularAdminRoutes(admin, options) {
113
113
  return jsonError(error);
114
114
  }
115
115
  });
116
+ app.get('/reactions', async (c) => {
117
+ try {
118
+ const status = c.req.query('status');
119
+ const type = c.req.query('type');
120
+ const allowed = new Set([
121
+ 'pending',
122
+ 'leased',
123
+ 'completed',
124
+ 'dead-letter',
125
+ ]);
126
+ if (status !== undefined && !allowed.has(status)) {
127
+ throw new SyncError('sync.invalid_request', 'invalid reaction status');
128
+ }
129
+ const reactions = await admin.listReactions(partitionOf(c), {
130
+ ...(status !== undefined
131
+ ? { statuses: [status] }
132
+ : {}),
133
+ ...(type !== undefined ? { types: [type] } : {}),
134
+ limit: intParam(c.req.query('limit'), 100),
135
+ });
136
+ return Response.json({ reactions });
137
+ }
138
+ catch (error) {
139
+ return jsonError(error);
140
+ }
141
+ });
116
142
  app.get('/rows/:table/:rowId', async (c) => {
117
143
  try {
118
144
  const row = await admin.inspectRow(partitionOf(c), c.req.param('table'), c.req.param('rowId'));
package/dist/index.d.ts CHANGED
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * Hono adapter: a thin wrapper proving the embed boundary.
3
3
  * Hono is a dependency of this adapter only, never of the server core.
4
- * Mounts the §1.1 routes: POST /sync and GET /segments/:segmentId
5
- * (realtime upgrades are runtime-specific and stay with the host).
4
+ * Mounts the §1.1 routes including sync, registered operations, segments, and
5
+ * blobs. Realtime upgrades are runtime-specific and stay with the host.
6
6
  */
7
- import { type SyncServerConfig } from '@syncular/server';
7
+ import { type RemoteOperationRegistry, type SyncServerConfig } from '@syncular/server';
8
8
  import { Hono } from 'hono';
9
9
  export * from './admin.js';
10
10
  export interface SyncularHonoOptions {
11
11
  readonly config: SyncServerConfig;
12
+ readonly operations?: RemoteOperationRegistry;
12
13
  /** Host authentication (§1.1); `null` ⇒ 401 `sync.auth_required`. */
13
14
  readonly authenticate: (request: Request) => Promise<{
14
15
  actorId: string;
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Hono adapter: a thin wrapper proving the embed boundary.
3
3
  * Hono is a dependency of this adapter only, never of the server core.
4
- * Mounts the §1.1 routes: POST /sync and GET /segments/:segmentId
5
- * (realtime upgrades are runtime-specific and stay with the host).
4
+ * Mounts the §1.1 routes including sync, registered operations, segments, and
5
+ * blobs. Realtime upgrades are runtime-specific and stay with the host.
6
6
  */
7
- import { encodeSegmentBody, errorBody, handleBlobDownload, handleBlobUpload, handleBlobUploadGrant, handleSegmentDownload, handleSyncRequest, SSP2_CONTENT_TYPE, SyncError, } from '@syncular/server';
7
+ import { encodeSegmentBody, errorBody, handleBlobDownload, handleBlobUpload, handleBlobUploadGrant, handleSegmentDownload, handleRemoteOperation, handleSyncRequest, SSP2_CONTENT_TYPE, SyncError, } from '@syncular/server';
8
8
  import { Hono } from 'hono';
9
9
  export * from './admin.js';
10
10
  function errorResponse(error) {
@@ -38,6 +38,23 @@ export function createSyncularHono(options) {
38
38
  return errorResponse(error);
39
39
  }
40
40
  });
41
+ app.post('/operations', async (c) => {
42
+ const contentType = c.req.header('content-type')?.split(';')[0]?.trim();
43
+ if (contentType !== 'application/vnd.syncular.operations.v1+json') {
44
+ return Response.json(errorBody(new SyncError('operation.invalid_request')), { status: 415 });
45
+ }
46
+ if (options.operations === undefined) {
47
+ return errorResponse(new SyncError('operation.unknown'));
48
+ }
49
+ const auth = await options.authenticate(c.req.raw);
50
+ if (auth === null)
51
+ return errorResponse(new SyncError('sync.auth_required'));
52
+ const bytes = new Uint8Array(await c.req.arrayBuffer());
53
+ const out = await handleRemoteOperation(bytes, { ...options.config, ...auth }, options.operations);
54
+ return c.body(out.slice().buffer, 200, {
55
+ 'Content-Type': 'application/vnd.syncular.operations.v1+json',
56
+ });
57
+ });
41
58
  app.get('/segments/:segmentId', async (c) => {
42
59
  const auth = await options.authenticate(c.req.raw);
43
60
  if (auth === null)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/server-hono",
3
- "version": "0.15.45",
3
+ "version": "0.15.47",
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.15.45",
49
- "hono": "^4.11.0"
48
+ "@syncular/server": "0.15.47",
49
+ "hono": "^4.12.34"
50
50
  },
51
51
  "devDependencies": {
52
- "@syncular/core": "0.15.45"
52
+ "@syncular/core": "0.15.47"
53
53
  }
54
54
  }
package/src/admin.ts CHANGED
@@ -7,13 +7,14 @@
7
7
  * refuses to mount without a host-provided `authorize` guard (it throws at
8
8
  * construction). Every request runs the guard first; a falsy result is a
9
9
  * 401. Admin is a privileged surface (it reads every partition's clients,
10
- * commit metadata, and scope activity) and SPEC.md deliberately says nothing
11
- * about it — authorization is entirely the host's, and mandatory.
10
+ * commit metadata, scope activity, and reactions) and authorization is
11
+ * entirely the host's and mandatory.
12
12
  */
13
13
  import {
14
14
  errorBody,
15
15
  matchesRingQuery,
16
16
  type RingEventQuery,
17
+ type ReactionStatus,
17
18
  SyncError,
18
19
  type SyncularAdmin,
19
20
  type SyncularServerEvent,
@@ -159,6 +160,32 @@ export function createSyncularAdminRoutes(
159
160
  }
160
161
  });
161
162
 
163
+ app.get('/reactions', async (c) => {
164
+ try {
165
+ const status = c.req.query('status');
166
+ const type = c.req.query('type');
167
+ const allowed = new Set<ReactionStatus>([
168
+ 'pending',
169
+ 'leased',
170
+ 'completed',
171
+ 'dead-letter',
172
+ ]);
173
+ if (status !== undefined && !allowed.has(status as ReactionStatus)) {
174
+ throw new SyncError('sync.invalid_request', 'invalid reaction status');
175
+ }
176
+ const reactions = await admin.listReactions(partitionOf(c), {
177
+ ...(status !== undefined
178
+ ? { statuses: [status as ReactionStatus] }
179
+ : {}),
180
+ ...(type !== undefined ? { types: [type] } : {}),
181
+ limit: intParam(c.req.query('limit'), 100),
182
+ });
183
+ return Response.json({ reactions });
184
+ } catch (error) {
185
+ return jsonError(error);
186
+ }
187
+ });
188
+
162
189
  app.get('/rows/:table/:rowId', async (c) => {
163
190
  try {
164
191
  const row = await admin.inspectRow(
package/src/index.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Hono adapter: a thin wrapper proving the embed boundary.
3
3
  * Hono is a dependency of this adapter only, never of the server core.
4
- * Mounts the §1.1 routes: POST /sync and GET /segments/:segmentId
5
- * (realtime upgrades are runtime-specific and stay with the host).
4
+ * Mounts the §1.1 routes including sync, registered operations, segments, and
5
+ * blobs. Realtime upgrades are runtime-specific and stay with the host.
6
6
  */
7
7
  import {
8
8
  encodeSegmentBody,
@@ -11,6 +11,8 @@ import {
11
11
  handleBlobUpload,
12
12
  handleBlobUploadGrant,
13
13
  handleSegmentDownload,
14
+ handleRemoteOperation,
15
+ type RemoteOperationRegistry,
14
16
  handleSyncRequest,
15
17
  SSP2_CONTENT_TYPE,
16
18
  SyncError,
@@ -22,6 +24,7 @@ export * from './admin';
22
24
 
23
25
  export interface SyncularHonoOptions {
24
26
  readonly config: SyncServerConfig;
27
+ readonly operations?: RemoteOperationRegistry;
25
28
  /** Host authentication (§1.1); `null` ⇒ 401 `sync.auth_required`. */
26
29
  readonly authenticate: (
27
30
  request: Request,
@@ -67,6 +70,31 @@ export function createSyncularHono(options: SyncularHonoOptions): Hono {
67
70
  }
68
71
  });
69
72
 
73
+ app.post('/operations', async (c) => {
74
+ const contentType = c.req.header('content-type')?.split(';')[0]?.trim();
75
+ if (contentType !== 'application/vnd.syncular.operations.v1+json') {
76
+ return Response.json(
77
+ errorBody(new SyncError('operation.invalid_request')),
78
+ { status: 415 },
79
+ );
80
+ }
81
+ if (options.operations === undefined) {
82
+ return errorResponse(new SyncError('operation.unknown'));
83
+ }
84
+ const auth = await options.authenticate(c.req.raw);
85
+ if (auth === null)
86
+ return errorResponse(new SyncError('sync.auth_required'));
87
+ const bytes = new Uint8Array(await c.req.arrayBuffer());
88
+ const out = await handleRemoteOperation(
89
+ bytes,
90
+ { ...options.config, ...auth },
91
+ options.operations,
92
+ );
93
+ return c.body(out.slice().buffer as ArrayBuffer, 200, {
94
+ 'Content-Type': 'application/vnd.syncular.operations.v1+json',
95
+ });
96
+ });
97
+
70
98
  app.get('/segments/:segmentId', async (c) => {
71
99
  const auth = await options.authenticate(c.req.raw);
72
100
  if (auth === null)