@syncular/server-hono 0.15.44 → 0.15.46

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.
@@ -1,10 +1,10 @@
1
1
  /**
2
- * The single static console page (TODO §2.5). Zero framework, no build
2
+ * The single static console page. Zero framework, no build
3
3
  * step, ~one file: it fetches the sibling JSON endpoints (relative to its
4
4
  * own mount path), renders tables, and offers an auto-refresh toggle —
5
5
  * 5% of the code a full console app would cost, the 80% operator value.
6
6
  *
7
- * Styling follows docs/DESIGN.md (the teletype theme): everything
7
+ * Styling follows docs/STYLE.md: everything
8
8
  * monospace, pure black, a single amber accent, 1px borders, sharp
9
9
  * corners, bracketed labels, inverse-video hover. The page ships as one
10
10
  * self-contained string, so it uses the system mono fallback stack.
@@ -1,10 +1,10 @@
1
1
  /**
2
- * The single static console page (TODO §2.5). Zero framework, no build
2
+ * The single static console page. Zero framework, no build
3
3
  * step, ~one file: it fetches the sibling JSON endpoints (relative to its
4
4
  * own mount path), renders tables, and offers an auto-refresh toggle —
5
5
  * 5% of the code a full console app would cost, the 80% operator value.
6
6
  *
7
- * Styling follows docs/DESIGN.md (the teletype theme): everything
7
+ * Styling follows docs/STYLE.md: everything
8
8
  * monospace, pure black, a single amber accent, 1px borders, sharp
9
9
  * corners, bracketed labels, inverse-video hover. The page ships as one
10
10
  * self-contained string, so it uses the system mono fallback stack.
package/dist/admin.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Admin / console HTTP surface (TODO §2.5) — a mountable Hono sub-app over a
2
+ * Admin / console HTTP surface: a mountable Hono sub-app over a
3
3
  * `SyncularAdmin`. JSON endpoints mirror the read surface; a single static
4
4
  * HTML page (zero framework, no build step) at `GET /` renders them.
5
5
  *
@@ -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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Admin / console HTTP surface (TODO §2.5) — a mountable Hono sub-app over a
2
+ * Admin / console HTTP surface: a mountable Hono sub-app over a
3
3
  * `SyncularAdmin`. JSON endpoints mirror the read surface; a single static
4
4
  * HTML page (zero framework, no build step) at `GET /` renders them.
5
5
  *
@@ -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';
@@ -32,7 +32,7 @@ function intParam(value, fallback) {
32
32
  */
33
33
  export function createSyncularAdminRoutes(admin, options) {
34
34
  if (typeof options.authorize !== 'function') {
35
- throw new Error('createSyncularAdminRoutes: an `authorize` guard is required — admin never mounts default-open (TODO §2.5)');
35
+ throw new Error('createSyncularAdminRoutes: an `authorize` guard is required — admin never mounts default-open');
36
36
  }
37
37
  const app = new Hono();
38
38
  /** Resolve the request's partition (query, else default) — required. */
@@ -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
- * Hono adapter (REVISE B2): a thin wrapper proving the embed boundary.
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
- * Hono adapter (REVISE B2): a thin wrapper proving the embed boundary.
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.44",
3
+ "version": "0.15.46",
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.44",
49
- "hono": "^4.11.0"
48
+ "@syncular/server": "0.15.46",
49
+ "hono": "^4.12.34"
50
50
  },
51
51
  "devDependencies": {
52
- "@syncular/core": "0.15.44"
52
+ "@syncular/core": "0.15.46"
53
53
  }
54
54
  }
package/src/admin-page.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
- * The single static console page (TODO §2.5). Zero framework, no build
2
+ * The single static console page. Zero framework, no build
3
3
  * step, ~one file: it fetches the sibling JSON endpoints (relative to its
4
4
  * own mount path), renders tables, and offers an auto-refresh toggle —
5
5
  * 5% of the code a full console app would cost, the 80% operator value.
6
6
  *
7
- * Styling follows docs/DESIGN.md (the teletype theme): everything
7
+ * Styling follows docs/STYLE.md: everything
8
8
  * monospace, pure black, a single amber accent, 1px borders, sharp
9
9
  * corners, bracketed labels, inverse-video hover. The page ships as one
10
10
  * self-contained string, so it uses the system mono fallback stack.
package/src/admin.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Admin / console HTTP surface (TODO §2.5) — a mountable Hono sub-app over a
2
+ * Admin / console HTTP surface: a mountable Hono sub-app over a
3
3
  * `SyncularAdmin`. JSON endpoints mirror the read surface; a single static
4
4
  * HTML page (zero framework, no build step) at `GET /` renders them.
5
5
  *
@@ -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,
@@ -67,7 +68,7 @@ export function createSyncularAdminRoutes(
67
68
  ): Hono {
68
69
  if (typeof options.authorize !== 'function') {
69
70
  throw new Error(
70
- 'createSyncularAdminRoutes: an `authorize` guard is required — admin never mounts default-open (TODO §2.5)',
71
+ 'createSyncularAdminRoutes: an `authorize` guard is required — admin never mounts default-open',
71
72
  );
72
73
  }
73
74
  const app = new Hono();
@@ -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
- * Hono adapter (REVISE B2): a thin wrapper proving the embed boundary.
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)