Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions backend/selective-entry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Hono } from "hono";
import { cors } from "hono/cors";
import { WebSocketServer } from "./sync.ts";
import { SyncBackendDO } from "./sync.ts";
import syncHandler from "./sync.ts";
import {
workerGlobals,
Expand All @@ -21,7 +21,7 @@ import { TrcpContext } from "./trpc/trpc.ts";

// NOTE: This export is necessary at the root entry point for the Workers
// runtime for Durable Object usage
export { WebSocketServer };
export { SyncBackendDO };

const honoApp = new Hono<{ Bindings: Env; Variables: AuthContext }>();

Expand Down
100 changes: 56 additions & 44 deletions backend/sync.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import {
makeDurableObject,
handleWebSocket,
getSyncRequestSearchParams,
handleSyncRequest,
} from "@livestore/sync-cf/cf-worker";
import { type Env, type ExecutionContext } from "./types";

import { getValidatedUser } from "./auth";
import { Schema } from "@livestore/livestore";

export class WebSocketServer extends makeDurableObject({
onPush: async (message) => {
export class SyncBackendDO extends makeDurableObject({
onPush: async (message, _context) => {
console.log("onPush", message.batch);
},
onPull: async (message) => {
onPull: async (message, _context) => {
console.log("onPull", message);
},
}) {}
Expand All @@ -25,50 +26,61 @@ const decodePayload = Schema.decodeUnknownSync(SyncPayloadSchema);

export default {
fetch: async (request: Request, env: Env, ctx: ExecutionContext) => {
const url = new URL(request.url);
const requestParamsResult = getSyncRequestSearchParams(request as any);

const pathname = url.pathname;
if (requestParamsResult._tag === "Some") {
return handleSyncRequest({
request: request as any,
searchParams: requestParamsResult.value,
env,
ctx,
options: {
headers: {},
durableObject: {
name: "WEBSOCKET_SERVER",
},
validatePayload: async (rawPayload) => {
try {
const payload = decodePayload(rawPayload);
let validatedUser = await getValidatedUser(
payload.authToken,
env
);

if (!pathname.startsWith("/livestore")) {
return new Response("Invalid request", { status: 400 });
}

return handleWebSocket(request, env, ctx, {
validatePayload: async (rawPayload) => {
try {
const payload = decodePayload(rawPayload);
let validatedUser = await getValidatedUser(payload.authToken, env);
if (!validatedUser) {
throw new Error("User must be authenticated");
}

if (!validatedUser) {
throw new Error("User must be authenticated");
}
// User identity is validated via JWT token
// LiveStore will manage clientId for device/app instance identification
if (validatedUser.id === "runtime-agent") {
console.log("✅ Runtime agent authenticated");
} else if (payload?.runtime === true) {
// For API key authenticated runtime agents
console.log("✅ API key authenticated runtime agent:", {
userId: validatedUser.id,
});
} else {
// For regular users
console.log("✅ Authenticated user:", {
userId: validatedUser.id,
});
}

// User identity is validated via JWT token
// LiveStore will manage clientId for device/app instance identification
if (validatedUser.id === "runtime-agent") {
console.log("✅ Runtime agent authenticated");
} else if (payload?.runtime === true) {
// For API key authenticated runtime agents
console.log("✅ API key authenticated runtime agent:", {
userId: validatedUser.id,
});
} else {
// For regular users
console.log("✅ Authenticated user:", {
userId: validatedUser.id,
});
}
// SECURITY NOTE: This validation only occurs at connection time.
// The current version of `@livestore/sync-cf` does not provide a mechanism
// to verify that the `clientId` on incoming events matches the `clientId`
// that was validated with this initial connection payload. A malicious
// client could pass this check and then send events with a different clientId.
} catch (error: any) {
console.error("🚫 Authentication failed:", error.message);
throw error; // Reject the WebSocket connection
}
},
},
});
}

// SECURITY NOTE: This validation only occurs at connection time.
// The current version of `@livestore/sync-cf` does not provide a mechanism
// to verify that the `clientId` on incoming events matches the `clientId`
// that was validated with this initial connection payload. A malicious
// client could pass this check and then send events with a different clientId.
} catch (error: any) {
console.error("🚫 Authentication failed:", error.message);
throw error; // Reject the WebSocket connection
}
},
});
return new Response("Not found", { status: 404 });
},
};
27 changes: 14 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,18 @@
"@codemirror/lint": "^6.8.5",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.38.0",
"@effect/platform": "0.90.7",
"@effect/rpc": "0.69.2",
"@japikey/authenticate": "^0.4.0",
"@japikey/cloudflare": "^0.4.0",
"@japikey/japikey": "^0.4.0",
"@japikey/shared": "^0.4.0",
"@livestore/adapter-web": "^0.3.1",
"@livestore/livestore": "^0.3.1",
"@livestore/react": "^0.3.1",
"@livestore/sync-cf": "^0.3.1",
"@livestore/adapter-web": "0.4.0-dev.7",
"@livestore/livestore": "0.4.0-dev.7",
"@livestore/react": "0.4.0-dev.7",
"@livestore/sync-cf": "0.4.0-dev.7",
"@livestore/wa-sqlite": "1.0.5-dev.2",
"@livestore/webmesh": "^0.3.1",
"@livestore/webmesh": "0.4.0-dev.7",
"@microlink/react-json-view": "^1.26.2",
"@overengineering/fps-meter": "^0.1.2",
"@radix-ui/react-avatar": "^1.1.10",
Expand All @@ -75,7 +77,6 @@
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.1.6",
"@react-spring/web": "^10.0.1",
"@runt/schema": "jsr:^0.13.0",
"@tanstack/react-query": "^5.85.5",
"@tanstack/react-query-devtools": "^5.85.5",
"@tanstack/react-virtual": "^3.13.12",
Expand All @@ -90,7 +91,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"effect": "3.15.5",
"effect": "3.17.13",
"fractional-indexing": "^3.2.0",
"hono": "^4.9.1",
"jose": "^6.0.12",
Expand Down Expand Up @@ -123,8 +124,8 @@
"@cloudflare/workers-types": "^4.20250813.0",
"@effect/vitest": "0.23.7",
"@eslint/js": "^9.30.1",
"@livestore/adapter-node": "^0.3.1",
"@livestore/devtools-vite": "^0.3.1",
"@livestore/adapter-node": "0.4.0-dev.7",
"@livestore/devtools-vite": "0.4.0-dev.7",
"@tailwindcss/cli": "^4.1.10",
"@tailwindcss/postcss": "^4.1.10",
"@tailwindcss/typography": "^0.5.16",
Expand Down Expand Up @@ -155,22 +156,22 @@
"rollup-plugin-visualizer": "^6.0.3",
"tailwindcss": "^4.1.10",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vite": "^7.1.4",
"vitest": "^3.0.0",
"webpack-bundle-analyzer": "^4.10.2",
"wrangler": "4.27.0"
},
"pnpm": {
"overrides": {
"effect": "3.15.5",
"effect": "3.17.13",
"react": "19.0.0",
"react-dom": "19.0.0",
"@effect/platform": "0.82.4",
"@effect/platform": "0.90.7",
"@effect/typeclass": "0.34.2",
"@effect/cluster": "0.34.2",
"@effect/experimental": "0.46.8",
"@effect/sql": "0.35.8",
"@effect/rpc": "0.59.9"
"@effect/rpc": "0.69.2"
},
"onlyBuiltDependencies": [
"@parcel/watcher",
Expand Down
Loading
Loading