create-syncular-app 0.58.0 → 0.59.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-syncular-app",
3
- "version": "0.58.0",
3
+ "version": "0.59.0",
4
4
  "author": "Benjamin Kniffler",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,24 +18,51 @@ export interface SyncTable<Row, Insert, Update, Id> {
18
18
 
19
19
  /** ServerSchema-compatible schema object (SPEC §2.4, §3.1). */
20
20
  export const schema = {
21
- version: 1,
22
- tables: [
21
+ "version": 1,
22
+ "tables": [
23
23
  {
24
- name: 'todos',
25
- columns: [
26
- { name: 'id', type: 'string', nullable: false },
27
- { name: 'list_id', type: 'string', nullable: false },
28
- { name: 'title', type: 'string', nullable: false },
29
- { name: 'done', type: 'boolean', nullable: false },
30
- { name: 'position', type: 'integer', nullable: false },
31
- { name: 'updated_at_ms', type: 'integer', nullable: false },
24
+ "name": "todos",
25
+ "primaryKey": "id",
26
+ "columns": [
27
+ {
28
+ "name": "id",
29
+ "type": "string",
30
+ "nullable": false
31
+ },
32
+ {
33
+ "name": "list_id",
34
+ "type": "string",
35
+ "nullable": false
36
+ },
37
+ {
38
+ "name": "title",
39
+ "type": "string",
40
+ "nullable": false
41
+ },
42
+ {
43
+ "name": "done",
44
+ "type": "boolean",
45
+ "nullable": false
46
+ },
47
+ {
48
+ "name": "position",
49
+ "type": "integer",
50
+ "nullable": false
51
+ },
52
+ {
53
+ "name": "updated_at_ms",
54
+ "type": "integer",
55
+ "nullable": false
56
+ }
32
57
  ],
33
- primaryKey: 'id',
34
- scopes: [
35
- { pattern: 'list:{list_id}', column: 'list_id' },
36
- ],
37
- },
38
- ],
58
+ "scopes": [
59
+ {
60
+ "pattern": "list:{list_id}",
61
+ "column": "list_id"
62
+ }
63
+ ]
64
+ }
65
+ ]
39
66
  } as const;
40
67
 
41
68
  /** One todos row (§2.4 column order). */
@@ -1,9 +1,10 @@
1
1
  {
2
- "formatVersion": 2,
2
+ "formatVersion": 3,
3
3
  "migrations": [
4
4
  {
5
5
  "name": "0001_initial",
6
- "sha256": "87b23cd0f8e3677ca6fa7ca67eb7c8373a285607b6de10854cc43df1290bb0fa"
6
+ "sha256": "87b23cd0f8e3677ca6fa7ca67eb7c8373a285607b6de10854cc43df1290bb0fa",
7
+ "schemaVersion": 1
7
8
  }
8
9
  ],
9
10
  "head": {
@@ -83,9 +83,14 @@ On desktop, the plugin owns the database path and the transport — see
83
83
  table before using a child wildcard. See
84
84
  [Scopes & authorization](https://syncular.dev/concepts-scopes/).
85
85
  2. **`src/server.ts` → `authenticate`** — plug in your real session/token
86
- check; return `{ actorId, partition }` or `null` for a 401.
86
+ check; return `{ actorId, partition }` or `null` for a 401. `POST /sync`
87
+ and the `/realtime` upgrade both run it. A browser WebSocket sends cookies
88
+ but no custom headers, so authenticate the upgrade from a cookie or a
89
+ [short-lived ticket](https://syncular.dev/guide-auth/#realtime-tickets).
87
90
  3. **`src-tauri/src/lib.rs` → `base_url`** — point the native core at your
88
91
  deployed sync endpoint (rotating auth goes through `client.setHeaders`).
92
+ Replace `localhost:8787` in the `connect-src` of the window's CSP
93
+ (`src-tauri/tauri.conf.json`) with the same origin.
89
94
  4. **`src/frontend/main.tsx`** — the UI. Everything under `<SyncProvider>` is
90
95
  shared code; grow it into your app.
91
96
  5. **`migrations/` + `syncular.json`** — append migrations and add tables or
@@ -30,8 +30,6 @@ import { buildSqliteImage, SqliteServerStorage } from '@syncular/server/sqlite';
30
30
  import { schema } from './syncular.generated';
31
31
 
32
32
  const PORT = Number(process.env.PORT ?? 8787);
33
- const PARTITION = 'demo';
34
- const ACTOR_ID = 'demo-user';
35
33
 
36
34
  // -- sync server ------------------------------------------------------------
37
35
 
@@ -57,11 +55,19 @@ const config: SyncServerConfig = {
57
55
  realtime: hub,
58
56
  };
59
57
  await ensureSyncServerReady(config);
60
- const hono = createSyncularHono({
61
- config,
62
- // Replace with your real auth: return { actorId, partition } or null (401).
63
- authenticate: async () => ({ actorId: ACTOR_ID, partition: PARTITION }),
64
- });
58
+ // §8.5: a client that hears nothing for twice this interval drops the socket.
59
+ setInterval(() => hub.heartbeat(), 30_000);
60
+
61
+ /** Replace with your real auth: return { actorId, partition } or null (401).
62
+ * POST /sync and the /realtime upgrade both run it. A browser WebSocket sends
63
+ * cookies but no custom headers, so authenticate the upgrade from a cookie or
64
+ * a query parameter. */
65
+ async function authenticate(
66
+ _request: Request,
67
+ ): Promise<{ actorId: string; partition: string } | null> {
68
+ return { actorId: 'demo-user', partition: 'demo' };
69
+ }
70
+ const hono = createSyncularHono({ config, authenticate });
65
71
 
66
72
  // -- frontend build + static assets ------------------------------------------
67
73
 
@@ -129,6 +135,8 @@ function staticResponse(body: string | Uint8Array, type: string): Response {
129
135
  // -- one process, one port ----------------------------------------------------
130
136
 
131
137
  interface SocketData {
138
+ actorId: string;
139
+ partition: string;
132
140
  clientId: string;
133
141
  session?: RealtimeSession;
134
142
  }
@@ -140,9 +148,15 @@ const server = Bun.serve<SocketData, never>({
140
148
  const path = url.pathname;
141
149
 
142
150
  if (path === '/realtime') {
151
+ const auth = await authenticate(request);
152
+ if (auth === null) {
153
+ return new Response('realtime requires authentication', {
154
+ status: 401,
155
+ });
156
+ }
143
157
  const clientId = url.searchParams.get('clientId') ?? crypto.randomUUID();
144
- if (bunServer.upgrade(request, { data: { clientId } })) {
145
- return undefined as unknown as Response;
158
+ if (bunServer.upgrade(request, { data: { ...auth, clientId } })) {
159
+ return;
146
160
  }
147
161
  return new Response('expected a websocket upgrade', { status: 400 });
148
162
  }
@@ -172,8 +186,8 @@ const server = Bun.serve<SocketData, never>({
172
186
  open(ws) {
173
187
  hub
174
188
  .connect({
175
- partition: PARTITION,
176
- actorId: ACTOR_ID,
189
+ partition: ws.data.partition,
190
+ actorId: ws.data.actorId,
177
191
  clientId: ws.data.clientId,
178
192
  send: (data) => {
179
193
  ws.send(data);
@@ -18,24 +18,51 @@ export interface SyncTable<Row, Insert, Update, Id> {
18
18
 
19
19
  /** ServerSchema-compatible schema object (SPEC §2.4, §3.1). */
20
20
  export const schema = {
21
- version: 1,
22
- tables: [
21
+ "version": 1,
22
+ "tables": [
23
23
  {
24
- name: 'todos',
25
- columns: [
26
- { name: 'id', type: 'string', nullable: false },
27
- { name: 'list_id', type: 'string', nullable: false },
28
- { name: 'title', type: 'string', nullable: false },
29
- { name: 'done', type: 'boolean', nullable: false },
30
- { name: 'position', type: 'integer', nullable: false },
31
- { name: 'updated_at_ms', type: 'integer', nullable: false },
24
+ "name": "todos",
25
+ "primaryKey": "id",
26
+ "columns": [
27
+ {
28
+ "name": "id",
29
+ "type": "string",
30
+ "nullable": false
31
+ },
32
+ {
33
+ "name": "list_id",
34
+ "type": "string",
35
+ "nullable": false
36
+ },
37
+ {
38
+ "name": "title",
39
+ "type": "string",
40
+ "nullable": false
41
+ },
42
+ {
43
+ "name": "done",
44
+ "type": "boolean",
45
+ "nullable": false
46
+ },
47
+ {
48
+ "name": "position",
49
+ "type": "integer",
50
+ "nullable": false
51
+ },
52
+ {
53
+ "name": "updated_at_ms",
54
+ "type": "integer",
55
+ "nullable": false
56
+ }
32
57
  ],
33
- primaryKey: 'id',
34
- scopes: [
35
- { pattern: 'list:{list_id}', column: 'list_id' },
36
- ],
37
- },
38
- ],
58
+ "scopes": [
59
+ {
60
+ "pattern": "list:{list_id}",
61
+ "column": "list_id"
62
+ }
63
+ ]
64
+ }
65
+ ]
39
66
  } as const;
40
67
 
41
68
  /** One todos row (§2.4 column order). */
@@ -21,7 +21,7 @@ tauri = { version = "2", features = [] }
21
21
  # The syncular plugin from crates.io. `native-transport` gives the core its
22
22
  # real HTTP + WebSocket transport (ureq + tungstenite) inside the host
23
23
  # process — the webview never talks to the sync server directly.
24
- tauri-plugin-syncular = { version = "0.58.0", features = ["native-transport"] }
24
+ tauri-plugin-syncular = { version = "0.59.0", features = ["native-transport"] }
25
25
 
26
26
  [features]
27
27
  default = []
@@ -17,7 +17,7 @@
17
17
  }
18
18
  ],
19
19
  "security": {
20
- "csp": null,
20
+ "csp": "default-src 'self'; connect-src 'self' ipc: http://ipc.localhost http://localhost:8787 ws://localhost:8787",
21
21
  "capabilities": ["syncular"]
22
22
  }
23
23
  },
@@ -1,47 +1,50 @@
1
1
  {
2
- "formatVersion": 1,
2
+ "formatVersion": 3,
3
3
  "migrations": [
4
4
  {
5
5
  "name": "0001_initial",
6
6
  "sha256": "87b23cd0f8e3677ca6fa7ca67eb7c8373a285607b6de10854cc43df1290bb0fa",
7
- "tables": [
8
- {
9
- "name": "todos",
10
- "primaryKey": "id",
11
- "columns": [
12
- {
13
- "name": "id",
14
- "type": "string",
15
- "nullable": false
16
- },
17
- {
18
- "name": "list_id",
19
- "type": "string",
20
- "nullable": false
21
- },
22
- {
23
- "name": "title",
24
- "type": "string",
25
- "nullable": false
26
- },
27
- {
28
- "name": "done",
29
- "type": "boolean",
30
- "nullable": false
31
- },
32
- {
33
- "name": "position",
34
- "type": "integer",
35
- "nullable": false
36
- },
37
- {
38
- "name": "updated_at_ms",
39
- "type": "integer",
40
- "nullable": false
41
- }
42
- ]
43
- }
44
- ]
7
+ "schemaVersion": 1
45
8
  }
46
- ]
9
+ ],
10
+ "head": {
11
+ "tables": [
12
+ {
13
+ "name": "todos",
14
+ "primaryKey": "id",
15
+ "columns": [
16
+ {
17
+ "name": "id",
18
+ "type": "string",
19
+ "nullable": false
20
+ },
21
+ {
22
+ "name": "list_id",
23
+ "type": "string",
24
+ "nullable": false
25
+ },
26
+ {
27
+ "name": "title",
28
+ "type": "string",
29
+ "nullable": false
30
+ },
31
+ {
32
+ "name": "done",
33
+ "type": "boolean",
34
+ "nullable": false
35
+ },
36
+ {
37
+ "name": "position",
38
+ "type": "integer",
39
+ "nullable": false
40
+ },
41
+ {
42
+ "name": "updated_at_ms",
43
+ "type": "integer",
44
+ "nullable": false
45
+ }
46
+ ]
47
+ }
48
+ ]
49
+ }
47
50
  }
@@ -54,7 +54,10 @@ covered by `tsc`).
54
54
  See
55
55
  [Scopes & authorization](https://syncular.dev/concepts-scopes/).
56
56
  2. **`src/server.ts` → `authenticate`** — plug in your real session/token
57
- check; return `{ actorId, partition }` or `null` for a 401.
57
+ check; return `{ actorId, partition }` or `null` for a 401. `POST /sync`
58
+ and the `/realtime` upgrade both run it. A browser WebSocket sends cookies
59
+ but no custom headers, so authenticate the upgrade from a cookie or a
60
+ [short-lived ticket](https://syncular.dev/guide-auth/#realtime-tickets).
58
61
  3. **`src/frontend/main.ts`** — the UI. It queries and mutates through the
59
62
  worker handle; grow it into your app.
60
63
  4. **`migrations/` + `syncular.json`** — append migrations and add tables or
@@ -26,8 +26,6 @@ import { buildSqliteImage, SqliteServerStorage } from '@syncular/server/sqlite';
26
26
  import { schema } from './syncular.generated';
27
27
 
28
28
  const PORT = Number(process.env.PORT ?? 8787);
29
- const PARTITION = 'demo';
30
- const ACTOR_ID = 'demo-user';
31
29
 
32
30
  // -- sync server ------------------------------------------------------------
33
31
 
@@ -53,11 +51,19 @@ const config: SyncServerConfig = {
53
51
  realtime: hub,
54
52
  };
55
53
  await ensureSyncServerReady(config);
56
- const hono = createSyncularHono({
57
- config,
58
- // Replace with your real auth: return { actorId, partition } or null (401).
59
- authenticate: async () => ({ actorId: ACTOR_ID, partition: PARTITION }),
60
- });
54
+ // §8.5: a client that hears nothing for twice this interval drops the socket.
55
+ setInterval(() => hub.heartbeat(), 30_000);
56
+
57
+ /** Replace with your real auth: return { actorId, partition } or null (401).
58
+ * POST /sync and the /realtime upgrade both run it. A browser WebSocket sends
59
+ * cookies but no custom headers, so authenticate the upgrade from a cookie or
60
+ * a query parameter. */
61
+ async function authenticate(
62
+ _request: Request,
63
+ ): Promise<{ actorId: string; partition: string } | null> {
64
+ return { actorId: 'demo-user', partition: 'demo' };
65
+ }
66
+ const hono = createSyncularHono({ config, authenticate });
61
67
 
62
68
  // -- frontend build + static assets ------------------------------------------
63
69
 
@@ -124,6 +130,8 @@ function staticResponse(body: string | Uint8Array, type: string): Response {
124
130
  // -- one process, one port ----------------------------------------------------
125
131
 
126
132
  interface SocketData {
133
+ actorId: string;
134
+ partition: string;
127
135
  clientId: string;
128
136
  session?: RealtimeSession;
129
137
  }
@@ -135,9 +143,15 @@ const server = Bun.serve<SocketData, never>({
135
143
  const path = url.pathname;
136
144
 
137
145
  if (path === '/realtime') {
146
+ const auth = await authenticate(request);
147
+ if (auth === null) {
148
+ return new Response('realtime requires authentication', {
149
+ status: 401,
150
+ });
151
+ }
138
152
  const clientId = url.searchParams.get('clientId') ?? crypto.randomUUID();
139
- if (bunServer.upgrade(request, { data: { clientId } })) {
140
- return undefined as unknown as Response;
153
+ if (bunServer.upgrade(request, { data: { ...auth, clientId } })) {
154
+ return;
141
155
  }
142
156
  return new Response('expected a websocket upgrade', { status: 400 });
143
157
  }
@@ -167,8 +181,8 @@ const server = Bun.serve<SocketData, never>({
167
181
  open(ws) {
168
182
  hub
169
183
  .connect({
170
- partition: PARTITION,
171
- actorId: ACTOR_ID,
184
+ partition: ws.data.partition,
185
+ actorId: ws.data.actorId,
172
186
  clientId: ws.data.clientId,
173
187
  send: (data) => {
174
188
  ws.send(data);
@@ -18,24 +18,51 @@ export interface SyncTable<Row, Insert, Update, Id> {
18
18
 
19
19
  /** ServerSchema-compatible schema object (SPEC §2.4, §3.1). */
20
20
  export const schema = {
21
- version: 1,
22
- tables: [
21
+ "version": 1,
22
+ "tables": [
23
23
  {
24
- name: 'todos',
25
- columns: [
26
- { name: 'id', type: 'string', nullable: false },
27
- { name: 'list_id', type: 'string', nullable: false },
28
- { name: 'title', type: 'string', nullable: false },
29
- { name: 'done', type: 'boolean', nullable: false },
30
- { name: 'position', type: 'integer', nullable: false },
31
- { name: 'updated_at_ms', type: 'integer', nullable: false },
24
+ "name": "todos",
25
+ "primaryKey": "id",
26
+ "columns": [
27
+ {
28
+ "name": "id",
29
+ "type": "string",
30
+ "nullable": false
31
+ },
32
+ {
33
+ "name": "list_id",
34
+ "type": "string",
35
+ "nullable": false
36
+ },
37
+ {
38
+ "name": "title",
39
+ "type": "string",
40
+ "nullable": false
41
+ },
42
+ {
43
+ "name": "done",
44
+ "type": "boolean",
45
+ "nullable": false
46
+ },
47
+ {
48
+ "name": "position",
49
+ "type": "integer",
50
+ "nullable": false
51
+ },
52
+ {
53
+ "name": "updated_at_ms",
54
+ "type": "integer",
55
+ "nullable": false
56
+ }
32
57
  ],
33
- primaryKey: 'id',
34
- scopes: [
35
- { pattern: 'list:{list_id}', column: 'list_id' },
36
- ],
37
- },
38
- ],
58
+ "scopes": [
59
+ {
60
+ "pattern": "list:{list_id}",
61
+ "column": "list_id"
62
+ }
63
+ ]
64
+ }
65
+ ]
39
66
  } as const;
40
67
 
41
68
  /** One todos row (§2.4 column order). */
@@ -1,47 +1,50 @@
1
1
  {
2
- "formatVersion": 1,
2
+ "formatVersion": 3,
3
3
  "migrations": [
4
4
  {
5
5
  "name": "0001_initial",
6
6
  "sha256": "87b23cd0f8e3677ca6fa7ca67eb7c8373a285607b6de10854cc43df1290bb0fa",
7
- "tables": [
8
- {
9
- "name": "todos",
10
- "primaryKey": "id",
11
- "columns": [
12
- {
13
- "name": "id",
14
- "type": "string",
15
- "nullable": false
16
- },
17
- {
18
- "name": "list_id",
19
- "type": "string",
20
- "nullable": false
21
- },
22
- {
23
- "name": "title",
24
- "type": "string",
25
- "nullable": false
26
- },
27
- {
28
- "name": "done",
29
- "type": "boolean",
30
- "nullable": false
31
- },
32
- {
33
- "name": "position",
34
- "type": "integer",
35
- "nullable": false
36
- },
37
- {
38
- "name": "updated_at_ms",
39
- "type": "integer",
40
- "nullable": false
41
- }
42
- ]
43
- }
44
- ]
7
+ "schemaVersion": 1
45
8
  }
46
- ]
9
+ ],
10
+ "head": {
11
+ "tables": [
12
+ {
13
+ "name": "todos",
14
+ "primaryKey": "id",
15
+ "columns": [
16
+ {
17
+ "name": "id",
18
+ "type": "string",
19
+ "nullable": false
20
+ },
21
+ {
22
+ "name": "list_id",
23
+ "type": "string",
24
+ "nullable": false
25
+ },
26
+ {
27
+ "name": "title",
28
+ "type": "string",
29
+ "nullable": false
30
+ },
31
+ {
32
+ "name": "done",
33
+ "type": "boolean",
34
+ "nullable": false
35
+ },
36
+ {
37
+ "name": "position",
38
+ "type": "integer",
39
+ "nullable": false
40
+ },
41
+ {
42
+ "name": "updated_at_ms",
43
+ "type": "integer",
44
+ "nullable": false
45
+ }
46
+ ]
47
+ }
48
+ ]
49
+ }
47
50
  }