@sveltebase/sync 1.2.0 → 1.3.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/README.md CHANGED
@@ -92,15 +92,17 @@ export const sync = new SyncClient<AppDatabaseSchema>({
92
92
  });
93
93
  ```
94
94
 
95
- Use it in your Svelte 5 components (using your preferred Dexie reactivity hook, such as `liveQuery` from `dexie` or `dexie-svelte-query`):
95
+ Use it in your Svelte 5 components with `createLiveQuery`. It wraps Dexie's `liveQuery` in Svelte 5 rune-based reactive state and exposes `data`, `isLoading`, and `error`.
96
+
96
97
  ```svelte
97
98
  <script lang="ts">
98
99
  import { sync } from "$lib/sync-client";
99
- import { liveQuery } from "dexie";
100
+ import { createLiveQuery } from "@sveltebase/sync/client";
100
101
  import { Check, Trash } from "lucide-svelte";
101
102
 
102
- // Standard Dexie liveQuery updates instantly on mutations & remote syncs
103
- const todos = liveQuery(() => sync.todos.orderBy("createdAt").reverse().toArray());
103
+ const todosQuery = createLiveQuery(() =>
104
+ sync.todos.orderBy("createdAt").reverse().toArray()
105
+ );
104
106
 
105
107
  let title = "";
106
108
 
@@ -119,7 +121,12 @@ Use it in your Svelte 5 components (using your preferred Dexie reactivity hook,
119
121
 
120
122
  <input bind:value={title} onkeydown={(e) => e.key === 'Enter' && addTodo()} />
121
123
 
122
- {#each ($todos || []) as todo (todo.id)}
124
+ {#if todosQuery.isLoading}
125
+ <p>Loading...</p>
126
+ {:else if todosQuery.error}
127
+ <p>Failed to load todos.</p>
128
+ {:else}
129
+ {#each (todosQuery.data || []) as todo (todo.id)}
123
130
  <div>
124
131
  <button onclick={() => sync.todos.update(todo.id, { completed: !todo.completed })}>
125
132
  <Check class={todo.completed ? "text-emerald-500" : ""} />
@@ -127,9 +134,25 @@ Use it in your Svelte 5 components (using your preferred Dexie reactivity hook,
127
134
  <span>{todo.title}</span>
128
135
  <button onclick={() => sync.todos.delete(todo.id)}><Trash /></button>
129
136
  </div>
130
- {/each}
137
+ {/each}
138
+ {/if}
139
+ ```
140
+
141
+ `createLiveQuery` accepts a Dexie query function and an optional dependency getter. When any dependency changes, the live query is recreated with the latest reactive values:
142
+
143
+ ```typescript
144
+ const query = createLiveQuery(
145
+ () => sync.todos.where("completed").equals(false).toArray(),
146
+ () => [filterValue]
147
+ );
148
+
149
+ query.data;
150
+ query.isLoading;
151
+ query.error;
131
152
  ```
132
153
 
154
+ You can import it from either `@sveltebase/sync/client` or the root `@sveltebase/sync` entrypoint.
155
+
133
156
  ### Synced Database Operations
134
157
 
135
158
  Under the hood, `@sveltebase/sync` intercepts native Dexie table writes to capture and propagate mutations to the backend. The following methods automatically sync with the server:
@@ -324,6 +347,41 @@ async function getSession(ctx: SyncContext) {
324
347
  }
325
348
  ```
326
349
 
350
+ ### Connection Auth (`ctx.auth`)
351
+ Sveltebase Sync can resolve and store an authenticated app payload during the WebSocket handshake. The resolved payload is passed to every sync handler as `ctx.auth`.
352
+
353
+ ```typescript
354
+ // src/routes/api/sync/+server.ts
355
+ import { JWT_SECRET } from "$env/static/private";
356
+ import { getVerifiedUserFromRequest } from "@sveltebase/auth";
357
+ import { handleUpgrade } from "@sveltebase/sync";
358
+ import type { User } from "$lib/server/db/schema";
359
+ import type { RequestHandler } from "@sveltejs/kit";
360
+
361
+ export const GET: RequestHandler = (event) => {
362
+ return handleUpgrade(event.request, event.platform, {
363
+ auth: async (request) => {
364
+ const user = await getVerifiedUserFromRequest<User>(
365
+ request,
366
+ JWT_SECRET
367
+ );
368
+
369
+ return user ? { user } : null;
370
+ },
371
+ identity: (auth) => auth.user.id,
372
+ allowUnauthenticated: false
373
+ });
374
+ };
375
+ ```
376
+
377
+ After this, every handler can access the user object:
378
+
379
+ ```typescript
380
+ ctx.auth?.user;
381
+ ```
382
+
383
+ The `identity` function returns the stable string/number key used by scoped broadcasts. If omitted, Sync defaults to `auth.user.id` when present. Existing `userId` query parameter and `x-user-id` header identity are still supported as legacy fallback, but should not be used as the primary auth mechanism for private data.
384
+
327
385
  ---
328
386
 
329
387
  ### The `authorize` Hook
@@ -350,7 +408,8 @@ Use the handshake HTTP request (`ctx.request`) to dynamically filter the records
350
408
  ```typescript
351
409
  fetch: async (ctx, since) => {
352
410
  const db = getDB(ctx.platform);
353
- const user = await getSession(ctx);
411
+ const user = ctx.auth?.user;
412
+ if (!user) return [];
354
413
 
355
414
  let query = db.select().from(todos);
356
415
  const conditions = [];
@@ -393,12 +452,16 @@ create: async (ctx, data) => {
393
452
  },
394
453
 
395
454
  update: async (ctx, key, changes) => {
396
- const user = await getSession(ctx);
455
+ const user = ctx.auth?.user;
456
+ if (!user) {
457
+ throw new Error("Unauthorized");
458
+ }
459
+
397
460
  const db = getDB(ctx.platform);
398
461
 
399
462
  // Fetch target record to verify ownership
400
463
  const [record] = await db.select().from(todos).where(eq(todos.id, key));
401
- if (record.ownerId !== user.userId && user.role !== "admin") {
464
+ if (record.ownerId !== user.id && user.role !== "admin") {
402
465
  throw new Error("You cannot update a record owned by someone else.");
403
466
  }
404
467
 
@@ -456,35 +519,35 @@ export const todoSync = defineSync<Todo>({
456
519
  ```
457
520
 
458
521
  #### How Connection Identities are Registered
459
- The broker matches the user IDs returned by `scope` to each active client's socket state. The server registers the connection's identity during the handshake using query parameters or the `x-user-id` header:
460
- ```typescript
461
- const userId = url.searchParams.get("userId") || request.headers.get("x-user-id");
462
- ```
522
+ The broker matches the IDs returned by `scope` to each active connection's registered identity. That identity is resolved during the WebSocket handshake with the `identity` option on `handleUpgrade`.
463
523
 
464
524
  ##### Authenticating using SvelteKit Sessions & Cookies (Recommended)
465
- Instead of exposing user IDs in client-side WebSocket URLs, you can resolve the user session on the server inside your SvelteKit route (`+server.ts`) and inject the verified `x-user-id` header before calling `handleUpgrade()`:
525
+ Resolve the user session on the server inside your SvelteKit route (`+server.ts`) and return the full user object as connection auth:
466
526
 
467
527
  ```typescript
468
528
  // src/routes/api/sync/+server.ts
529
+ import { JWT_SECRET } from "$env/static/private";
530
+ import { getVerifiedUserFromRequest } from "@sveltebase/auth";
469
531
  import { handleUpgrade } from "@sveltebase/sync";
532
+ import type { User } from "$lib/server/db/schema";
470
533
  import type { RequestEvent, RequestHandler } from "@sveltejs/kit";
471
534
 
472
- export const GET: RequestHandler = (event: RequestEvent) => {
473
- // 1. Get user identity from your custom server-side session/cookies
474
- const user = event.locals.user; // e.g., set by your auth hook middleware
475
- if (!user) {
476
- return new Response("Unauthorized", { status: 401 });
477
- }
478
-
479
- // 2. Clone the request and inject the verified user ID header
480
- const request = new Request(event.request);
481
- request.headers.set("x-user-id", user.id);
535
+ export const GET: RequestHandler = async (event: RequestEvent) => {
536
+ return handleUpgrade(event.request, event.platform, {
537
+ auth: async (request) => {
538
+ const user = await getVerifiedUserFromRequest<User>(
539
+ request,
540
+ JWT_SECRET
541
+ );
482
542
 
483
- // 3. Hand off to the sync engine
484
- return handleUpgrade(request, event.platform);
543
+ return user ? { user } : null;
544
+ },
545
+ identity: (auth) => auth.user.id,
546
+ allowUnauthenticated: false
547
+ });
485
548
  };
486
549
  ```
487
- This approach keeps WebSocket URLs clean of private IDs and ensures all active sockets are automatically authenticated with their verified session roles/IDs.
550
+ This approach keeps WebSocket URLs clean of private IDs, makes `ctx.auth.user` available to your sync handlers, and gives the broker a stable identity for `scope` filtering.
488
551
 
489
552
  ---
490
553
 
@@ -520,6 +583,3 @@ await publish("todos", "delete", todo.id, undefined);
520
583
  // 4. Supports scoped/dynamic channels (e.g. "channelName:scopeId")
521
584
  await publish("todos:user_123", "update", todo.id, { title: "New Title" });
522
585
  ```
523
-
524
-
525
-
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { SyncClient, createLiveQuery } from "./client/index.js";
2
2
  export type { LiveQueryState } from "./client/index.js";
3
3
  export { defineSync } from "./server/index.js";
4
4
  export { handleUpgrade, publishEvent, publishBulkEvent, createPublisher, createBulkPublisher } from "./server/handler.js";
5
- export type { PublishEventData, InferSchemaFromHandlers } from "./server/handler.js";
6
- export type { SyncContext, SyncHandler } from "./server/index.js";
5
+ export type { PublishEventData, InferSchemaFromHandlers, SyncAuthResult, SyncUpgradeOptions } from "./server/handler.js";
6
+ export type { SyncConnectionAuth, SyncContext, SyncHandler } from "./server/index.js";
7
7
  export type { SyncMessage } from "./protocol.js";
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAChE,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1H,YAAY,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AACrF,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAClE,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAChE,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1H,YAAY,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzH,YAAY,EAAE,kBAAkB,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACtF,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
@@ -4,6 +4,8 @@ export interface ISyncConnection {
4
4
  close(code?: number, reason?: string): void;
5
5
  getAuth(): any;
6
6
  setAuth(auth: any): void;
7
+ getIdentity(): string | null;
8
+ setIdentity(identity: string | null): void;
7
9
  getSubscribedChannels(): Set<string>;
8
10
  readonly headers: Headers;
9
11
  readonly url: string;
@@ -11,8 +13,7 @@ export interface ISyncConnection {
11
13
  export declare class SyncBroker {
12
14
  private handlers;
13
15
  private connections;
14
- private authorizeConnection?;
15
- constructor(handlers: SyncHandler[], authorizeConnection?: (request: Request, platform: App.Platform | undefined) => Promise<any>);
16
+ constructor(handlers: SyncHandler[]);
16
17
  setHandlers(handlers: SyncHandler[]): void;
17
18
  registerConnection(conn: ISyncConnection): void;
18
19
  removeConnection(conn: ISyncConnection): void;
@@ -1 +1 @@
1
- {"version":3,"file":"broker.d.ts","sourceRoot":"","sources":["../../src/server/broker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAe,MAAM,YAAY,CAAC;AAG3D,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,OAAO,IAAI,GAAG,CAAC;IACf,OAAO,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,CAAC;IACzB,qBAAqB,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAA2B;IAC3C,OAAO,CAAC,WAAW,CAAmC;IACtD,OAAO,CAAC,mBAAmB,CAAC,CAGV;gBAGhB,QAAQ,EAAE,WAAW,EAAE,EACvB,mBAAmB,CAAC,EAAE,CACpB,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,GAAG,CAAC,QAAQ,GAAG,SAAS,KAC/B,OAAO,CAAC,GAAG,CAAC;IAOZ,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE;IAanC,kBAAkB,CAAC,IAAI,EAAE,eAAe;IAIxC,gBAAgB,CAAC,IAAI,EAAE,eAAe;IAI7C;;OAEG;IACH,OAAO,CAAC,WAAW;IAqCN,aAAa,CACxB,IAAI,EAAE,eAAe,EACrB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,GAAG,CAAC,QAAQ,GAAG,SAAS,EAClC,OAAO,EAAE,OAAO;YA0IJ,eAAe;IAoDhB,oBAAoB,CAC/B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EACtC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,IAAI,EAAE,GAAG;IAqBE,yBAAyB,CACpC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;CAkBvF"}
1
+ {"version":3,"file":"broker.d.ts","sourceRoot":"","sources":["../../src/server/broker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAe,MAAM,YAAY,CAAC;AAG3D,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,OAAO,IAAI,GAAG,CAAC;IACf,OAAO,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,CAAC;IACzB,WAAW,IAAI,MAAM,GAAG,IAAI,CAAC;IAC7B,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;IAC3C,qBAAqB,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAA2B;IAC3C,OAAO,CAAC,WAAW,CAAmC;gBAE1C,QAAQ,EAAE,WAAW,EAAE;IAK5B,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE;IAanC,kBAAkB,CAAC,IAAI,EAAE,eAAe;IAIxC,gBAAgB,CAAC,IAAI,EAAE,eAAe;IAI7C;;OAEG;IACH,OAAO,CAAC,WAAW;IAqCN,aAAa,CACxB,IAAI,EAAE,eAAe,EACrB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,GAAG,CAAC,QAAQ,GAAG,SAAS,EAClC,OAAO,EAAE,OAAO;YA0IJ,eAAe;IAmDhB,oBAAoB,CAC/B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EACtC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,IAAI,EAAE,GAAG;IAqBE,yBAAyB,CACpC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;CAkBvF"}
@@ -2,11 +2,9 @@ import { parseSyncMessage } from "../protocol.js";
2
2
  export class SyncBroker {
3
3
  handlers;
4
4
  connections = new Set();
5
- authorizeConnection;
6
- constructor(handlers, authorizeConnection) {
5
+ constructor(handlers) {
7
6
  this.handlers = new Map();
8
7
  this.setHandlers(handlers);
9
- this.authorizeConnection = authorizeConnection;
10
8
  }
11
9
  setHandlers(handlers) {
12
10
  this.handlers.clear();
@@ -194,8 +192,7 @@ export class SyncBroker {
194
192
  }
195
193
  // Filter based on scope
196
194
  if (allowedUserIds !== "all") {
197
- const connAuth = conn.getAuth();
198
- const userId = connAuth?.userId;
195
+ const userId = conn.getIdentity();
199
196
  if (!userId || !allowedUserIds.includes(userId)) {
200
197
  continue;
201
198
  }
@@ -1,11 +1,12 @@
1
1
  import type { SyncHandler } from "./index.js";
2
+ import type { SyncUpgradeOptions } from "./handler.js";
2
3
  import type { IncomingMessage } from "node:http";
3
4
  export declare function setHandlers(handlers: SyncHandler[]): void;
4
5
  export declare function addClient(ws: {
5
6
  send: (data: string) => void;
6
7
  close: (code?: number, reason?: string) => void;
7
8
  on: (event: string, listener: (...args: any[]) => void) => void;
8
- }, req: IncomingMessage): void;
9
+ }, req: IncomingMessage, options?: SyncUpgradeOptions): Promise<boolean>;
9
10
  export declare function broadcastExternalChange(channel: string, action: "create" | "update" | "delete", key: string | undefined, data: any): Promise<void>;
10
11
  export declare function broadcastExternalBatchChange(channel: string, changes: Array<{
11
12
  action: "create" | "update" | "delete";
@@ -1 +1 @@
1
- {"version":3,"file":"dev-engine.d.ts","sourceRoot":"","sources":["../../src/server/dev-engine.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAUjD,wBAAgB,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE,QASlD;AAcD,wBAAgB,SAAS,CACvB,EAAE,EAAE;IACF,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,KAAK,IAAI,CAAC;CACjE,EACD,GAAG,EAAE,eAAe,QA+ErB;AAyBD,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EACtC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,IAAI,EAAE,GAAG,iBAIV;AAED,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ,CAAC,iBAIH"}
1
+ {"version":3,"file":"dev-engine.d.ts","sourceRoot":"","sources":["../../src/server/dev-engine.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAUjD,wBAAgB,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE,QASlD;AAcD,wBAAsB,SAAS,CAC7B,EAAE,EAAE;IACF,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,KAAK,IAAI,CAAC;CACjE,EACD,GAAG,EAAE,eAAe,EACpB,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,OAAO,CAAC,CAwHlB;AAyBD,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EACtC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,IAAI,EAAE,GAAG,iBAIV;AAED,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ,CAAC,iBAIH"}
@@ -22,10 +22,11 @@ function getDevBroker() {
22
22
  }
23
23
  throw new Error("Sync dev broker not initialized. Call setHandlers first.");
24
24
  }
25
- export function addClient(ws, req) {
25
+ export async function addClient(ws, req, options) {
26
26
  const broker = getDevBroker();
27
27
  const subscribedChannels = new Set();
28
28
  let auth = null;
29
+ let identity = null;
29
30
  // Convert Node IncomingMessage headers to web-standard Headers
30
31
  const headers = new Headers();
31
32
  for (const [key, value] of Object.entries(req.headers)) {
@@ -41,11 +42,29 @@ export function addClient(ws, req) {
41
42
  }
42
43
  }
43
44
  const urlObj = new URL(req.url ?? "", `http://${req.headers.host || "localhost"}`);
44
- // Align with DO behavior to set local dev auth
45
- const userId = urlObj.searchParams.get("userId") || headers.get("x-user-id");
46
- if (userId) {
47
- auth = { userId };
48
- }
45
+ const initConnection = async () => {
46
+ const request = new Request(urlObj.toString(), { headers });
47
+ if (options?.auth) {
48
+ auth = (await options.auth(request, undefined)) ?? null;
49
+ if (!auth && options.allowUnauthenticated === false) {
50
+ ws.close(1008, "Unauthorized");
51
+ return false;
52
+ }
53
+ if (auth) {
54
+ const identityValue = options.identity
55
+ ? options.identity(auth)
56
+ : (auth?.user?.id ?? auth?.userId);
57
+ identity = identityValue == null ? null : String(identityValue);
58
+ }
59
+ }
60
+ // Legacy fallback for existing demos/apps that pass identity in URL/header.
61
+ const userId = urlObj.searchParams.get("userId") || headers.get("x-user-id");
62
+ if (userId && !auth) {
63
+ auth = { userId };
64
+ identity = userId;
65
+ }
66
+ return true;
67
+ };
49
68
  const conn = {
50
69
  send(data) {
51
70
  ws.send(data);
@@ -59,14 +78,33 @@ export function addClient(ws, req) {
59
78
  setAuth(newAuth) {
60
79
  auth = newAuth;
61
80
  },
81
+ getIdentity() {
82
+ return identity;
83
+ },
84
+ setIdentity(newIdentity) {
85
+ identity = newIdentity;
86
+ },
62
87
  getSubscribedChannels() {
63
88
  return subscribedChannels;
64
89
  },
65
90
  headers,
66
91
  url: urlObj.toString(),
67
92
  };
68
- broker.registerConnection(conn);
69
- console.log("dev-engine: addClient registered connection");
93
+ try {
94
+ const ok = await initConnection();
95
+ if (!ok)
96
+ return false;
97
+ broker.registerConnection(conn);
98
+ console.log("dev-engine: addClient registered connection");
99
+ }
100
+ catch (err) {
101
+ console.error("dev-engine: Error initializing connection:", err);
102
+ try {
103
+ ws.close(1011, "Internal server error");
104
+ }
105
+ catch { }
106
+ return false;
107
+ }
70
108
  ws.on("message", async (data) => {
71
109
  const messageString = String(data);
72
110
  console.log("dev-engine: WebSocket message received:", messageString.slice(0, 100));
@@ -92,6 +130,7 @@ export function addClient(ws, req) {
92
130
  console.error("dev-engine: WebSocket connection error:", err);
93
131
  broker.removeConnection(conn);
94
132
  });
133
+ return true;
95
134
  }
96
135
  const GLOBAL_PLATFORM_KEY = "__sync_dev_platform__";
97
136
  async function getPlatform() {
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/server/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAwB,MAAM,aAAa,CAAC;AA4C/D,qBAAa,cAAe,SAAQ,aAAa,CAAC,GAAG,CAAC;IACpD,SAAS,CAAC,MAAM,EAAE,UAAU,CAAC;IAC7B,OAAO,CAAC,OAAO,CAAkD;gBAErD,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE;IAKxD,KAAK,CAAC,OAAO,EAAE,OAAO;IAoC5B,OAAO,CAAC,gBAAgB;IA2BlB,gBAAgB,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW;IAiBnE,cAAc,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAa1D,cAAc,CAAC,EAAE,EAAE,SAAS;CAO7B"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/server/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAwB,MAAM,aAAa,CAAC;AAqE/D,qBAAa,cAAe,SAAQ,aAAa,CAAC,GAAG,CAAC;IACpD,SAAS,CAAC,MAAM,EAAE,UAAU,CAAC;IAC7B,OAAO,CAAC,OAAO,CAAkD;gBAErD,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE;IAKxD,KAAK,CAAC,OAAO,EAAE,OAAO;IAoC5B,OAAO,CAAC,gBAAgB;IAoClB,gBAAgB,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW;IAiBnE,cAAc,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAa1D,cAAc,CAAC,EAAE,EAAE,SAAS;CAO7B"}
@@ -1,8 +1,20 @@
1
1
  import { DurableObject } from "cloudflare:workers";
2
2
  import { SyncBroker } from "./broker.js";
3
+ import { INTERNAL_AUTH_HEADER } from "./handler.js";
4
+ function deserializeConnectionAuth(value) {
5
+ if (!value)
6
+ return null;
7
+ try {
8
+ return JSON.parse(decodeURIComponent(escape(atob(value))));
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
3
14
  class CloudflareSyncConnection {
4
15
  ws;
5
16
  auth = null;
17
+ identity = null;
6
18
  subscribedChannels = new Set();
7
19
  headers;
8
20
  url;
@@ -33,6 +45,12 @@ class CloudflareSyncConnection {
33
45
  setAuth(newAuth) {
34
46
  this.auth = newAuth;
35
47
  }
48
+ getIdentity() {
49
+ return this.identity;
50
+ }
51
+ setIdentity(identity) {
52
+ this.identity = identity;
53
+ }
36
54
  getSubscribedChannels() {
37
55
  return this.subscribedChannels;
38
56
  }
@@ -84,10 +102,16 @@ export class SyncEngineBase extends DurableObject {
84
102
  const [client, server] = Object.values(new WebSocketPair());
85
103
  this.ctx.acceptWebSocket(server);
86
104
  const conn = new CloudflareSyncConnection(server, request);
105
+ const forwardedAuth = deserializeConnectionAuth(request.headers.get(INTERNAL_AUTH_HEADER));
106
+ if (forwardedAuth) {
107
+ conn.setAuth(forwardedAuth.auth);
108
+ conn.setIdentity(forwardedAuth.identity);
109
+ }
87
110
  const url = new URL(request.url);
88
111
  const userId = url.searchParams.get("userId") || request.headers.get("x-user-id");
89
- if (userId) {
112
+ if (userId && !conn.getAuth()) {
90
113
  conn.setAuth({ userId });
114
+ conn.setIdentity(userId);
91
115
  }
92
116
  this.connMap.set(server, conn);
93
117
  this.broker.registerConnection(conn);
@@ -1,4 +1,23 @@
1
1
  import type { SyncHandler } from "./index.js";
2
+ export type SyncAuthResult<TAuth> = TAuth | null | undefined;
3
+ export type SyncUpgradeOptions<TAuth = any> = {
4
+ /**
5
+ * Resolves the authenticated app payload for this WebSocket connection.
6
+ * Return null/undefined when no session is present.
7
+ */
8
+ auth?: (request: Request, platform: App.Platform | undefined) => Promise<SyncAuthResult<TAuth>> | SyncAuthResult<TAuth>;
9
+ /**
10
+ * Returns the stable identity key used by scope filtering.
11
+ * Defaults to auth.user.id when available, then auth.userId for legacy payloads.
12
+ */
13
+ identity?: (auth: TAuth) => string | number | bigint | null | undefined;
14
+ /**
15
+ * Defaults to true so existing public channels continue to work.
16
+ * Set false to reject WebSocket upgrades without a resolved auth payload.
17
+ */
18
+ allowUnauthenticated?: boolean;
19
+ };
20
+ declare const INTERNAL_AUTH_HEADER = "x-sveltebase-sync-auth";
2
21
  export type PublishEventData<TRecord, TAction extends "create" | "update" | "delete"> = TAction extends "create" ? TRecord : TAction extends "update" ? Partial<TRecord> : {
3
22
  updatedAt?: string;
4
23
  } | undefined;
@@ -23,5 +42,6 @@ export declare function publishBulkEvent(channel: string, changes: Array<{
23
42
  key?: string;
24
43
  data?: any;
25
44
  }>): Promise<void>;
26
- export declare function handleUpgrade(request: Request, platform: App.Platform | undefined): Promise<Response>;
45
+ export declare function handleUpgrade<TAuth = any>(request: Request, platform: App.Platform | undefined, options?: SyncUpgradeOptions<TAuth>): Promise<Response>;
46
+ export { INTERNAL_AUTH_HEADER };
27
47
  //# sourceMappingURL=handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/server/handler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/server/handler.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,12 @@
1
+ const INTERNAL_AUTH_HEADER = "x-sveltebase-sync-auth";
2
+ function defaultIdentity(auth) {
3
+ const value = auth?.user?.id ?? auth?.userId;
4
+ return value == null ? null : String(value);
5
+ }
6
+ function serializeConnectionAuth(auth, identity) {
7
+ const payload = { auth, identity };
8
+ return btoa(unescape(encodeURIComponent(JSON.stringify(payload))));
9
+ }
1
10
  export function createPublisher(handlers) {
2
11
  return async (channel, action, key, data) => {
3
12
  const resolvedChannel = String(channel);
@@ -92,7 +101,7 @@ export async function publishBulkEvent(channel, changes) {
92
101
  console.error("Failed to publish bulk sync event to Durable Object:", err);
93
102
  }
94
103
  }
95
- export async function handleUpgrade(request, platform) {
104
+ export async function handleUpgrade(request, platform, options) {
96
105
  if (request.headers.get("Upgrade") !== "websocket") {
97
106
  return new Response("Expected Upgrade: websocket", { status: 426 });
98
107
  }
@@ -101,9 +110,31 @@ export async function handleUpgrade(request, platform) {
101
110
  return new Response("SyncEngine binding is not available", { status: 500 });
102
111
  }
103
112
  try {
113
+ let resolvedAuth = null;
114
+ let identity = null;
115
+ if (options?.auth) {
116
+ resolvedAuth = (await options.auth(request, platform)) ?? null;
117
+ if (!resolvedAuth && options.allowUnauthenticated === false) {
118
+ return new Response("Unauthorized", { status: 401 });
119
+ }
120
+ if (resolvedAuth) {
121
+ const identityValue = options.identity
122
+ ? options.identity(resolvedAuth)
123
+ : defaultIdentity(resolvedAuth);
124
+ identity = identityValue == null ? null : String(identityValue);
125
+ }
126
+ }
127
+ const forwardedRequest = new Request("https://realtime.internal/websocket", request);
128
+ forwardedRequest.headers.delete(INTERNAL_AUTH_HEADER);
129
+ if (resolvedAuth) {
130
+ forwardedRequest.headers.set(INTERNAL_AUTH_HEADER, serializeConnectionAuth(resolvedAuth, identity));
131
+ }
132
+ else if (options?.auth) {
133
+ forwardedRequest.headers.delete(INTERNAL_AUTH_HEADER);
134
+ }
104
135
  const id = namespace.idFromName("global");
105
136
  const stub = namespace.get(id);
106
- return await stub.fetch(new Request("https://realtime.internal/websocket", request));
137
+ return await stub.fetch(forwardedRequest);
107
138
  }
108
139
  catch (err) {
109
140
  return new Response(err.message || "SyncEngine binding is not available", {
@@ -111,3 +142,4 @@ export async function handleUpgrade(request, platform) {
111
142
  });
112
143
  }
113
144
  }
145
+ export { INTERNAL_AUTH_HEADER };
@@ -1,25 +1,28 @@
1
1
  import type { ZodSchema } from "zod";
2
- export type SyncContext = {
2
+ export type SyncConnectionAuth<TUser = unknown> = {
3
+ user: TUser;
4
+ };
5
+ export type SyncContext<TAuth = any> = {
3
6
  platform: App.Platform | undefined;
4
7
  request: Request;
5
- auth?: any;
8
+ auth: TAuth | null;
6
9
  };
7
- export type SyncHandlerConfig<TRow = any> = {
8
- channel: string | ((ctx: SyncContext) => string);
9
- fetch: (ctx: SyncContext, since?: string) => Promise<TRow[]>;
10
- create?: (ctx: SyncContext, data: TRow) => Promise<TRow>;
11
- update?: (ctx: SyncContext, key: string, changes: Partial<TRow>) => Promise<TRow>;
12
- delete?: (ctx: SyncContext, key: string) => Promise<void>;
13
- authorize?: (ctx: SyncContext) => Promise<void>;
10
+ export type SyncHandlerConfig<TRow = any, TAuth = any> = {
11
+ channel: string | ((ctx: SyncContext<TAuth>) => string);
12
+ fetch: (ctx: SyncContext<TAuth>, since?: string) => Promise<TRow[]>;
13
+ create?: (ctx: SyncContext<TAuth>, data: TRow) => Promise<TRow>;
14
+ update?: (ctx: SyncContext<TAuth>, key: string, changes: Partial<TRow>) => Promise<TRow>;
15
+ delete?: (ctx: SyncContext<TAuth>, key: string) => Promise<void>;
16
+ authorize?: (ctx: SyncContext<TAuth>) => Promise<void>;
14
17
  validate?: {
15
18
  create?: ZodSchema<any>;
16
19
  update?: ZodSchema<any>;
17
20
  };
18
- scope?: (ctx: SyncContext, action: "create" | "update" | "delete", data: TRow) => Promise<string[] | "all"> | string[] | "all";
21
+ scope?: (ctx: SyncContext<TAuth>, action: "create" | "update" | "delete", data: TRow) => Promise<string[] | "all"> | string[] | "all";
19
22
  };
20
- export interface SyncHandler<TRow = any> {
21
- config: SyncHandlerConfig<TRow>;
22
- resolveChannel(ctx: SyncContext): string;
23
+ export interface SyncHandler<TRow = any, TAuth = any> {
24
+ config: SyncHandlerConfig<TRow, TAuth>;
25
+ resolveChannel(ctx: SyncContext<TAuth>): string;
23
26
  }
24
- export declare function defineSync<TRow = any>(config: SyncHandlerConfig<TRow>): SyncHandler<TRow>;
27
+ export declare function defineSync<TRow = any, TAuth = any>(config: SyncHandlerConfig<TRow, TAuth>): SyncHandler<TRow, TAuth>;
25
28
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AAErC,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,EAAE,GAAG,CAAC,QAAQ,GAAG,SAAS,CAAC;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ,CAAC;AAEF,MAAM,MAAM,iBAAiB,CAAC,IAAI,GAAG,GAAG,IAAI;IAC1C,OAAO,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,WAAW,KAAK,MAAM,CAAC,CAAC;IACjD,KAAK,EAAE,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7D,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,MAAM,CAAC,EAAE,CACP,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,KACnB,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,QAAQ,CAAC,EAAE;QACT,MAAM,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;QACxB,MAAM,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;KACzB,CAAC;IACF,KAAK,CAAC,EAAE,CACN,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EACtC,IAAI,EAAE,IAAI,KACP,OAAO,CAAC,MAAM,EAAE,GAAG,KAAK,CAAC,GAAG,MAAM,EAAE,GAAG,KAAK,CAAC;CACnD,CAAC;AAEF,MAAM,WAAW,WAAW,CAAC,IAAI,GAAG,GAAG;IACrC,MAAM,EAAE,iBAAiB,CAAC,IAAI,CAAC,CAAC;IAChC,cAAc,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAAC;CAC1C;AAED,wBAAgB,UAAU,CAAC,IAAI,GAAG,GAAG,EACnC,MAAM,EAAE,iBAAiB,CAAC,IAAI,CAAC,GAC9B,WAAW,CAAC,IAAI,CAAC,CASnB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AAErC,MAAM,MAAM,kBAAkB,CAAC,KAAK,GAAG,OAAO,IAAI;IAChD,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,WAAW,CAAC,KAAK,GAAG,GAAG,IAAI;IACrC,QAAQ,EAAE,GAAG,CAAC,QAAQ,GAAG,SAAS,CAAC;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,KAAK,GAAG,IAAI,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,iBAAiB,CAAC,IAAI,GAAG,GAAG,EAAE,KAAK,GAAG,GAAG,IAAI;IACvD,OAAO,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,WAAW,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC,CAAC;IACxD,KAAK,EAAE,CAAC,GAAG,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IACpE,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,MAAM,CAAC,EAAE,CACP,GAAG,EAAE,WAAW,CAAC,KAAK,CAAC,EACvB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,KACnB,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,CAAC,KAAK,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,QAAQ,CAAC,EAAE;QACT,MAAM,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;QACxB,MAAM,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;KACzB,CAAC;IACF,KAAK,CAAC,EAAE,CACN,GAAG,EAAE,WAAW,CAAC,KAAK,CAAC,EACvB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EACtC,IAAI,EAAE,IAAI,KACP,OAAO,CAAC,MAAM,EAAE,GAAG,KAAK,CAAC,GAAG,MAAM,EAAE,GAAG,KAAK,CAAC;CACnD,CAAC;AAEF,MAAM,WAAW,WAAW,CAAC,IAAI,GAAG,GAAG,EAAE,KAAK,GAAG,GAAG;IAClD,MAAM,EAAE,iBAAiB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACvC,cAAc,CAAC,GAAG,EAAE,WAAW,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC;CACjD;AAED,wBAAgB,UAAU,CAAC,IAAI,GAAG,GAAG,EAAE,KAAK,GAAG,GAAG,EAChD,MAAM,EAAE,iBAAiB,CAAC,IAAI,EAAE,KAAK,CAAC,GACrC,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAS1B"}
package/dist/vite.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import type { Plugin } from "vite";
2
+ import type { SyncUpgradeOptions } from "./server/handler.js";
2
3
  export type SyncDevPluginOptions = {
3
4
  handlersPath?: string;
5
+ auth?: SyncUpgradeOptions["auth"];
6
+ identity?: SyncUpgradeOptions["identity"];
7
+ allowUnauthenticated?: SyncUpgradeOptions["allowUnauthenticated"];
4
8
  };
5
9
  export declare function syncDevPlugin(options?: SyncDevPluginOptions): Plugin;
6
10
  //# sourceMappingURL=vite.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAanC,MAAM,MAAM,oBAAoB,GAAG;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,MAAM,CA4EpE"}
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAa9D,MAAM,MAAM,oBAAoB,GAAG;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAClC,QAAQ,CAAC,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAC;IAC1C,oBAAoB,CAAC,EAAE,kBAAkB,CAAC,sBAAsB,CAAC,CAAC;CACnE,CAAC;AAEF,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,MAAM,CAwFpE"}
package/dist/vite.js CHANGED
@@ -32,7 +32,14 @@ export function syncDevPlugin(options) {
32
32
  // Remove temporary buffering listener
33
33
  client.off("message", onMessage);
34
34
  // Register client in the dev engine
35
- devEngine.addClient(client, request);
35
+ const connected = await devEngine.addClient(client, request, {
36
+ auth: options?.auth,
37
+ identity: options?.identity,
38
+ allowUnauthenticated: options?.allowUnauthenticated,
39
+ });
40
+ if (!connected) {
41
+ return;
42
+ }
36
43
  console.log("sync-dev-plugin: WebSocket upgrade handler completed, replaying buffered messages:", messageQueue.length);
37
44
  // Replay any buffered messages
38
45
  for (const msg of messageQueue) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltebase/sync",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"