diff --git a/examples/better-auth/README.md b/examples/better-auth/README.md new file mode 100644 index 000000000..7f2594758 --- /dev/null +++ b/examples/better-auth/README.md @@ -0,0 +1,55 @@ +# Better Auth Integration for RivetKit + +Example project demonstrating authentication integration with [RivetKit](https://rivetkit.org) using Better Auth. + +[Learn More →](https://github.com/rivet-gg/rivetkit) + +[Discord](https://rivet.gg/discord) — [Documentation](https://rivetkit.org) — [Issues](https://github.com/rivet-gg/rivetkit/issues) + +## Getting Started + +### Prerequisites + +- Node.js 18+ +- npm or pnpm + +### Installation + +```sh +git clone https://github.com/rivet-gg/rivetkit +cd rivetkit/examples/better-auth +npm install +``` + +### Development + +```sh +npm run dev +``` + +Open your browser to `http://localhost:5173` to see the frontend and the backend will be running on `http://localhost:6420`. + +## Features + +- **Authentication**: Email/password authentication using Better Auth +- **Protected Workers**: RivetKit workers with authentication via `onAuth` hook +- **Real-time Chat**: Authenticated chat room with real-time messaging +- **SQLite Database**: Persistent user data and session storage + +## How It Works + +1. **Better Auth Setup**: Configured with SQLite adapter for user storage +2. **Protected Worker**: The `chatRoom` worker uses the `onAuth` hook to verify user sessions +3. **Frontend Integration**: React components handle authentication flow and chat interface +4. **Session Management**: Better Auth handles session creation, validation, and cleanup + +## Key Files + +- `src/backend/auth.ts` - Better Auth configuration with SQLite +- `src/backend/registry.ts` - RivetKit worker with authentication +- `src/frontend/components/AuthForm.tsx` - Login/signup form +- `src/frontend/components/ChatRoom.tsx` - Authenticated chat interface + +## License + +Apache 2.0 \ No newline at end of file diff --git a/examples/better-auth/package.json b/examples/better-auth/package.json new file mode 100644 index 000000000..eb6572e16 --- /dev/null +++ b/examples/better-auth/package.json @@ -0,0 +1,41 @@ +{ + "name": "example-better-auth", + "version": "0.9.0-rc.1", + "private": true, + "type": "module", + "scripts": { + "dev": "concurrently \"npm run dev:backend\" \"npm run dev:frontend\"", + "dev:backend": "tsx --watch src/backend/server.ts", + "dev:frontend": "vite", + "build": "vite build", + "check-types": "tsc --noEmit", + "test": "vitest run" + }, + "devDependencies": { + "@types/node": "^22.13.9", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.2.0", + "concurrently": "^8.2.2", + "rivetkit": "workspace:*", + "tsx": "^3.12.7", + "typescript": "^5.5.2", + "vite": "^5.0.0", + "vitest": "^3.1.1" + }, + "dependencies": { + "@hono/node-server": "^1.14.4", + "@rivetkit/memory": "workspace:0.9.0-rc.1", + "@rivetkit/react": "workspace:0.9.0-rc.1", + "better-auth": "^1.0.1", + "hono": "^4.7.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "example": { + "platforms": [ + "*" + ] + }, + "stableVersion": "0.8.0" +} diff --git a/examples/better-auth/src/backend/auth.ts b/examples/better-auth/src/backend/auth.ts new file mode 100644 index 000000000..c735ba539 --- /dev/null +++ b/examples/better-auth/src/backend/auth.ts @@ -0,0 +1,20 @@ +import { betterAuth } from "better-auth"; +import { sqliteAdapter } from "@better-auth/sqlite"; +import Database from "better-sqlite3"; + +const db = new Database("./auth.db"); + +export const auth = betterAuth({ + database: sqliteAdapter(db), + emailAndPassword: { + enabled: true, + }, + session: { + expiresIn: 60 * 60 * 24 * 7, // 7 days + updateAge: 60 * 60 * 24, // 1 day (every day the session expiry is updated) + }, + plugins: [], +}); + +export type Session = typeof auth.$Infer.Session; +export type User = typeof auth.$Infer.User; \ No newline at end of file diff --git a/examples/better-auth/src/backend/registry.ts b/examples/better-auth/src/backend/registry.ts new file mode 100644 index 000000000..7043bf599 --- /dev/null +++ b/examples/better-auth/src/backend/registry.ts @@ -0,0 +1,48 @@ +import { worker, setup } from "rivetkit"; +import { auth, type Session, type User } from "./auth"; + +export const chatRoom = worker({ + onAuth: async (c) => { + const authResult = await auth.api.getSession({ + headers: c.req.headers, + }); + + if (!authResult?.session || !authResult?.user) { + throw new Error("Unauthorized"); + } + + return { + userId: authResult.user.id, + user: authResult.user, + session: authResult.session, + }; + }, + state: { + messages: [] as Array<{ id: string; userId: string; username: string; message: string; timestamp: number }> + }, + actions: { + sendMessage: (c, message: string) => { + const newMessage = { + id: crypto.randomUUID(), + userId: c.auth.userId, + username: c.auth.user.email, + message, + timestamp: Date.now(), + }; + + c.state.messages.push(newMessage); + c.broadcast("newMessage", newMessage); + + return newMessage; + }, + getMessages: (c) => { + return c.state.messages; + }, + }, +}); + +export const registry = setup({ + workers: { chatRoom }, +}); + +export type Registry = typeof registry; \ No newline at end of file diff --git a/examples/better-auth/src/backend/server.ts b/examples/better-auth/src/backend/server.ts new file mode 100644 index 000000000..241bf8472 --- /dev/null +++ b/examples/better-auth/src/backend/server.ts @@ -0,0 +1,55 @@ +import { registry } from "./registry"; +import { auth } from "./auth"; +import { Hono } from "hono"; +import { serve } from "@hono/node-server"; +import { createMemoryDriver } from "@rivetkit/memory"; + +// Setup router +const app = new Hono(); + +// Start RivetKit +const { client, hono } = registry.run({ + driver: createMemoryDriver(), + cors: { + // IMPORTANT: Configure origins in production + origin: "*", + }, +}); + +// Mount Better Auth routes +app.on(["GET", "POST"], "/api/auth/**", (c) => auth.handler(c.req.raw)); + +// Expose RivetKit to the frontend +app.route("/registry", hono); + +// Example HTTP endpoint to join chat room +app.post("/api/join-room/:roomId", async (c) => { + const roomId = c.req.param("roomId"); + + // Verify authentication + const authResult = await auth.api.getSession({ + headers: c.req.header(), + }); + + if (!authResult?.session || !authResult?.user) { + return c.json({ error: "Unauthorized" }, 401); + } + + try { + const room = client.chatRoom.getOrCreate(roomId); + const messages = await room.getMessages(); + + return c.json({ + success: true, + roomId, + messages, + user: authResult.user + }); + } catch (error) { + return c.json({ error: "Failed to join room" }, 500); + } +}); + +serve({ fetch: app.fetch, port: 6420 }, () => + console.log("Listening at http://localhost:6420"), +); \ No newline at end of file diff --git a/examples/better-auth/src/frontend/App.tsx b/examples/better-auth/src/frontend/App.tsx new file mode 100644 index 000000000..30549a0e3 --- /dev/null +++ b/examples/better-auth/src/frontend/App.tsx @@ -0,0 +1,73 @@ +import { useState, useEffect } from "react"; +import { authClient } from "./auth-client"; +import { AuthForm } from "./components/AuthForm"; +import { ChatRoom } from "./components/ChatRoom"; + +function App() { + const [user, setUser] = useState<{ id: string; email: string } | null>(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Check if user is already authenticated + const checkAuth = async () => { + try { + const session = await authClient.getSession(); + if (session.data?.user) { + setUser(session.data.user); + } + } catch (error) { + console.error("Auth check failed:", error); + } finally { + setLoading(false); + } + }; + + checkAuth(); + }, []); + + const handleAuthSuccess = async () => { + try { + const session = await authClient.getSession(); + if (session.data?.user) { + setUser(session.data.user); + } + } catch (error) { + console.error("Failed to get user after auth:", error); + } + }; + + const handleSignOut = () => { + setUser(null); + }; + + if (loading) { + return ( +
+ Loading... +
+ ); + } + + return ( +
+
+

+ RivetKit with Better Auth +

+ + {user ? ( + + ) : ( + + )} +
+
+ ); +} + +export default App; \ No newline at end of file diff --git a/examples/better-auth/src/frontend/auth-client.ts b/examples/better-auth/src/frontend/auth-client.ts new file mode 100644 index 000000000..793a44e51 --- /dev/null +++ b/examples/better-auth/src/frontend/auth-client.ts @@ -0,0 +1,5 @@ +import { createAuthClient } from "better-auth/react"; + +export const authClient = createAuthClient({ + baseURL: "http://localhost:6420", +}); \ No newline at end of file diff --git a/examples/better-auth/src/frontend/components/AuthForm.tsx b/examples/better-auth/src/frontend/components/AuthForm.tsx new file mode 100644 index 000000000..005e89696 --- /dev/null +++ b/examples/better-auth/src/frontend/components/AuthForm.tsx @@ -0,0 +1,106 @@ +import { useState } from "react"; +import { authClient } from "../auth-client"; + +interface AuthFormProps { + onAuthSuccess: () => void; +} + +export function AuthForm({ onAuthSuccess }: AuthFormProps) { + const [isLogin, setIsLogin] = useState(true); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + setLoading(true); + + try { + if (isLogin) { + await authClient.signIn.email({ + email, + password, + }); + } else { + await authClient.signUp.email({ + email, + password, + }); + } + onAuthSuccess(); + } catch (err) { + setError(err instanceof Error ? err.message : "Authentication failed"); + } finally { + setLoading(false); + } + }; + + return ( +
+

{isLogin ? "Sign In" : "Sign Up"}

+ +
+
+ + setEmail(e.target.value)} + required + style={{ width: "100%", padding: "8px", marginTop: "5px" }} + /> +
+ +
+ + setPassword(e.target.value)} + required + style={{ width: "100%", padding: "8px", marginTop: "5px" }} + /> +
+ + {error && ( +
{error}
+ )} + + +
+ +
+ +
+
+ ); +} \ No newline at end of file diff --git a/examples/better-auth/src/frontend/components/ChatRoom.tsx b/examples/better-auth/src/frontend/components/ChatRoom.tsx new file mode 100644 index 000000000..dcd0f86dc --- /dev/null +++ b/examples/better-auth/src/frontend/components/ChatRoom.tsx @@ -0,0 +1,159 @@ +import { useState, useEffect } from "react"; +import { createClient, createRivetKit } from "@rivetkit/react"; +import { authClient } from "../auth-client"; +import type { Registry } from "../../backend/registry"; + +const client = createClient("http://localhost:6420/registry", { + transport: "sse", +}); + +const { useWorker } = createRivetKit(client); + +interface ChatRoomProps { + user: { id: string; email: string }; + onSignOut: () => void; +} + +export function ChatRoom({ user, onSignOut }: ChatRoomProps) { + const [message, setMessage] = useState(""); + const [messages, setMessages] = useState>([]); + const [roomId] = useState("general"); + + const chatRoom = useWorker({ + name: "chatRoom", + key: [roomId], + }); + + // Listen for new messages + chatRoom.useEvent("newMessage", (newMessage) => { + setMessages(prev => [...prev, newMessage]); + }); + + // Load initial messages when connected + useEffect(() => { + if (chatRoom.connection) { + chatRoom.connection.getMessages().then(initialMessages => { + setMessages(initialMessages); + }); + } + }, [chatRoom.connection]); + + const handleSendMessage = async (e: React.FormEvent) => { + e.preventDefault(); + if (!message.trim() || !chatRoom.connection) return; + + try { + await chatRoom.connection.sendMessage(message.trim()); + setMessage(""); + } catch (error) { + console.error("Failed to send message:", error); + } + }; + + const handleSignOut = async () => { + await authClient.signOut(); + onSignOut(); + }; + + return ( +
+
+
+

Chat Room: {roomId}

+

Logged in as: {user.email}

+
+ +
+ +
+ {messages.length === 0 ? ( +

No messages yet. Start the conversation!

+ ) : ( + messages.map((msg) => ( +
+
+ {msg.username} • {new Date(msg.timestamp).toLocaleTimeString()} +
+
{msg.message}
+
+ )) + )} +
+ +
+ setMessage(e.target.value)} + placeholder="Type your message..." + style={{ + flex: 1, + padding: "10px", + border: "1px solid #ccc", + borderRadius: "4px" + }} + /> + +
+ +
+ Connection Status: {chatRoom.connection ? "Connected" : "Connecting..."} +
+
+ ); +} \ No newline at end of file diff --git a/examples/better-auth/src/frontend/index.html b/examples/better-auth/src/frontend/index.html new file mode 100644 index 000000000..c8973df5d --- /dev/null +++ b/examples/better-auth/src/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + RivetKit + Better Auth + + +
+ + + \ No newline at end of file diff --git a/examples/better-auth/src/frontend/main.tsx b/examples/better-auth/src/frontend/main.tsx new file mode 100644 index 000000000..f5871ecc7 --- /dev/null +++ b/examples/better-auth/src/frontend/main.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); \ No newline at end of file diff --git a/examples/better-auth/tsconfig.json b/examples/better-auth/tsconfig.json new file mode 100644 index 000000000..c8e9f28f2 --- /dev/null +++ b/examples/better-auth/tsconfig.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "esnext", + /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "lib": ["esnext", "dom"], + /* Specify what JSX code is generated. */ + "jsx": "react-jsx", + + /* Specify what module code is generated. */ + "module": "esnext", + /* Specify how TypeScript looks up a file from a given module specifier. */ + "moduleResolution": "bundler", + /* Specify type package names to be included without being referenced in a source file. */ + "types": ["node"], + /* Enable importing .json files */ + "resolveJsonModule": true, + + /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ + "allowJs": true, + /* Enable error reporting in type-checked JavaScript files. */ + "checkJs": false, + + /* Disable emitting files from a compilation. */ + "noEmit": true, + + /* Ensure that each file can be safely transpiled without relying on other imports. */ + "isolatedModules": true, + /* Allow 'import x from y' when a module doesn't have a default export. */ + "allowSyntheticDefaultImports": true, + /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true, + + /* Enable all strict type-checking options. */ + "strict": true, + + /* Skip type checking all .d.ts files. */ + "skipLibCheck": true + }, + "include": ["src/**/*"] +} \ No newline at end of file diff --git a/examples/better-auth/vite.config.ts b/examples/better-auth/vite.config.ts new file mode 100644 index 000000000..315f881dc --- /dev/null +++ b/examples/better-auth/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + root: "src/frontend", + build: { + outDir: "../../dist", + }, + server: { + host: "0.0.0.0", + }, +}); \ No newline at end of file diff --git a/examples/cloudflare-workers/README.md b/examples/cloudflare-workers/README.md new file mode 100644 index 000000000..d05f1f95d --- /dev/null +++ b/examples/cloudflare-workers/README.md @@ -0,0 +1,57 @@ +# Cloudflare Workers for RivetKit + +Example project demonstrating Cloudflare Workers deployment with [RivetKit](https://rivetkit.org). + +[Learn More →](https://github.com/rivet-gg/rivetkit) + +[Discord](https://rivet.gg/discord) — [Documentation](https://rivetkit.org) — [Issues](https://github.com/rivet-gg/rivetkit/issues) + +## Getting Started + +### Prerequisites + +- Node.js +- Cloudflare account with Workers enabled +- Wrangler CLI installed globally (`npm install -g wrangler`) + +### Installation + +```sh +git clone https://github.com/rivet-gg/rivetkit +cd rivetkit/examples/cloudflare-workers +npm install +``` + +### Development + +```sh +npm run dev +``` + +This will start the Cloudflare Workers development server locally at http://localhost:8787. + +### Testing the Client + +In a separate terminal, run the client script to interact with your workers: + +```sh +npm run client +``` + +### Deploy to Cloudflare + +First, authenticate with Cloudflare: + +```sh +wrangler login +``` + +Then deploy: + +```sh +npm run deploy +``` + +## License + +Apache 2.0 \ No newline at end of file diff --git a/examples/cloudflare-workers/package.json b/examples/cloudflare-workers/package.json new file mode 100644 index 000000000..a7f34ace0 --- /dev/null +++ b/examples/cloudflare-workers/package.json @@ -0,0 +1,29 @@ +{ + "name": "example-cloudflare-workers", + "version": "0.9.0-rc.1", + "private": true, + "type": "module", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "check-types": "tsc --noEmit", + "client": "tsx scripts/client.ts" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250129.0", + "@types/node": "^22.13.9", + "rivetkit": "workspace:*", + "tsx": "^3.12.7", + "typescript": "^5.5.2", + "wrangler": "^3.0.0" + }, + "dependencies": { + "@rivetkit/cloudflare-workers": "workspace:*" + }, + "example": { + "platforms": [ + "cloudflare-workers" + ] + }, + "stableVersion": "0.8.0" +} diff --git a/examples/cloudflare-workers/scripts/client.ts b/examples/cloudflare-workers/scripts/client.ts new file mode 100644 index 000000000..5c7cec651 --- /dev/null +++ b/examples/cloudflare-workers/scripts/client.ts @@ -0,0 +1,37 @@ +import { createClient } from "rivetkit/client"; +import type { Registry } from "../src/registry.js"; + +// Create RivetKit client +const client = createClient("http://localhost:8787"); + +async function main() { + console.log("🚀 Cloudflare Workers Client Demo"); + + try { + // Create counter instance + const counter = client.counter.getOrCreate("demo"); + + // Increment counter + console.log("Incrementing counter 'demo'..."); + const result1 = await counter.increment(1); + console.log("New count:", result1); + + // Increment again with larger value + console.log("Incrementing counter 'demo' by 5..."); + const result2 = await counter.increment(5); + console.log("New count:", result2); + + // Create another counter + const counter2 = client.counter.getOrCreate("another"); + console.log("Incrementing counter 'another' by 10..."); + const result3 = await counter2.increment(10); + console.log("New count:", result3); + + console.log("✅ Demo completed!"); + } catch (error) { + console.error("❌ Error:", error); + process.exit(1); + } +} + +main().catch(console.error); diff --git a/examples/cloudflare-workers/src/index.ts b/examples/cloudflare-workers/src/index.ts new file mode 100644 index 000000000..341b3fff9 --- /dev/null +++ b/examples/cloudflare-workers/src/index.ts @@ -0,0 +1,6 @@ +import { createHandler } from "@rivetkit/cloudflare-workers"; +import { registry } from "./registry"; + +const { handler, WorkerHandler } = createHandler(registry); + +export { handler as default, WorkerHandler }; diff --git a/examples/cloudflare-workers/src/registry.ts b/examples/cloudflare-workers/src/registry.ts new file mode 100644 index 000000000..11f62e9b9 --- /dev/null +++ b/examples/cloudflare-workers/src/registry.ts @@ -0,0 +1,20 @@ +import { worker, setup } from "rivetkit"; + +export const counter = worker({ + onAuth: () => { + // Configure auth here + }, + state: { count: 0 }, + actions: { + increment: (c, x: number) => { + c.state.count += x; + return c.state.count; + }, + }, +}); + +export const registry = setup({ + workers: { counter }, +}); + +export type Registry = typeof registry; \ No newline at end of file diff --git a/packages/platforms/bun/public/tsconfig.json b/examples/cloudflare-workers/tsconfig.json similarity index 93% rename from packages/platforms/bun/public/tsconfig.json rename to examples/cloudflare-workers/tsconfig.json index 7578b052c..681844dd9 100644 --- a/packages/platforms/bun/public/tsconfig.json +++ b/examples/cloudflare-workers/tsconfig.json @@ -1,5 +1,4 @@ { - "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { /* Visit https://aka.ms/tsconfig.json to read more about this file */ @@ -15,7 +14,7 @@ /* Specify how TypeScript looks up a file from a given module specifier. */ "moduleResolution": "bundler", /* Specify type package names to be included without being referenced in a source file. */ - "types": ["bun"], + "types": ["@cloudflare/workers-types"], /* Enable importing .json files */ "resolveJsonModule": true, @@ -40,5 +39,5 @@ /* Skip type checking all .d.ts files. */ "skipLibCheck": true }, - "include": ["**/*.ts"] -} + "include": ["src/**/*"] +} \ No newline at end of file diff --git a/examples/cloudflare-workers/wrangler.json b/examples/cloudflare-workers/wrangler.json new file mode 100644 index 000000000..8791c7de9 --- /dev/null +++ b/examples/cloudflare-workers/wrangler.json @@ -0,0 +1,30 @@ +{ + "name": "rivetkit-cloudflare-workers-example", + "main": "src/index.ts", + "compatibility_date": "2025-01-20", + "compatibility_flags": ["nodejs_compat"], + "migrations": [ + { + "tag": "v1", + "new_classes": ["WorkerHandler"] + } + ], + "durable_objects": { + "bindings": [ + { + "name": "WORKER_DO", + "class_name": "WorkerHandler" + } + ] + }, + "kv_namespaces": [ + { + "binding": "WORKER_KV", + "id": "example_namespace", + "preview_id": "example_namespace_preview" + } + ], + "observability": { + "enabled": true + } +} \ No newline at end of file diff --git a/examples/rivet/.dockerignore b/examples/rivet/.dockerignore new file mode 100644 index 000000000..de4d1f007 --- /dev/null +++ b/examples/rivet/.dockerignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/examples/rivet/Dockerfile b/examples/rivet/Dockerfile new file mode 100644 index 000000000..643968a74 --- /dev/null +++ b/examples/rivet/Dockerfile @@ -0,0 +1,39 @@ +FROM node:22-alpine AS builder + +RUN npm i -g corepack && corepack enable + +WORKDIR /app + +COPY package.json ./ + +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ + pnpm install + +COPY . . + +RUN pnpm build + +COPY . . + +# === + +FROM node:22-alpine AS runtime + +RUN npm i -g corepack && corepack enable + +RUN addgroup -g 1001 -S rivet && \ + adduser -S rivet -u 1001 -G rivet + +WORKDIR /app + +COPY --from=builder --chown=rivet:rivet /app/package.json ./ + +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ + pnpm install --prod + +COPY --from=builder --chown=rivet:rivet /app/dist ./dist + +USER rivet + +CMD ["node", "dist/server.js"] + diff --git a/examples/rivet/README.md b/examples/rivet/README.md new file mode 100644 index 000000000..62a27648f --- /dev/null +++ b/examples/rivet/README.md @@ -0,0 +1,64 @@ +# Rivet Platform for RivetKit + +Example project demonstrating Rivet cloud platform deployment with [RivetKit](https://rivetkit.org). + +[Learn More →](https://github.com/rivet-gg/rivetkit) + +[Discord](https://rivet.gg/discord) — [Documentation](https://rivetkit.org) — [Issues](https://github.com/rivet-gg/rivetkit/issues) + +## Getting Started + +### Prerequisites + +- Node.js +- Rivet CLI (`npm install -g @rivet-gg/cli`) +- Rivet Cloud account + +### Installation + +```sh +git clone https://github.com/rivet-gg/rivetkit +cd rivetkit/examples/rivet +npm install +``` + +### Configuration + +Set up your environment variables: + +```sh +export RIVET_ENDPOINT=https://api.rivet.gg +export RIVET_SERVICE_TOKEN=your_service_token +export RIVET_PROJECT=your_project_id +export RIVET_ENVIRONMENT=your_environment +``` + +### Development + +```sh +npm run dev +``` + +This will start the RivetKit server locally at http://localhost:6420. + +### Testing the Client + +In a separate terminal, run the client script to interact with your workers: + +```sh +npm run client +``` + +### Deployment + +Deploy to Rivet Cloud: + +```sh +rivet deploy +``` + +Your RivetKit workers will be deployed as Rivet actors with automatic scaling and management. + +## License + +Apache 2.0 diff --git a/examples/rivet/package.json b/examples/rivet/package.json new file mode 100644 index 000000000..0cd0e06b4 --- /dev/null +++ b/examples/rivet/package.json @@ -0,0 +1,26 @@ +{ + "name": "example-rivet", + "version": "0.9.0-rc.1", + "private": true, + "scripts": { + "dev": "tsx --watch src/server.ts", + "check-types": "tsc --noEmit", + "build": "tsc", + "client": "tsx scripts/client.ts" + }, + "devDependencies": { + "@types/node": "^22.13.9", + "tsx": "^3.12.7", + "typescript": "^5.5.2" + }, + "dependencies": { + "rivetkit": "https://pkg.pr.new/rivet-gg/rivetkit@65c3659", + "@rivetkit/rivet": "https://pkg.pr.new/rivet-gg/rivetkit/@rivetkit/rivet@65c3659" + }, + "example": { + "platforms": [ + "*" + ] + }, + "stableVersion": "0.8.0" +} diff --git a/examples/rivet/rivet.json b/examples/rivet/rivet.json new file mode 100644 index 000000000..b821d3b1c --- /dev/null +++ b/examples/rivet/rivet.json @@ -0,0 +1,6 @@ +{ + "rivetkit": { + "registry": "./src/registry.ts", + "dockerfile": "Dockerfile" + } +} diff --git a/examples/rivet/scripts/client.ts b/examples/rivet/scripts/client.ts new file mode 100644 index 000000000..a8d79bf05 --- /dev/null +++ b/examples/rivet/scripts/client.ts @@ -0,0 +1,42 @@ +import { createClient } from "rivetkit/client"; +import { execSync } from "node:child_process"; +import type { Registry } from "../src/registry.js"; + +// Get endpoint from rivet kit +const endpoint = execSync("rivet kit endpoint", { encoding: "utf8" }).trim(); +console.log("🔗 Using endpoint:", endpoint); + +// Create RivetKit client +const client = createClient(endpoint); + +async function main() { + console.log("🚀 Rivet Client Demo"); + + try { + // Create counter instance + const counter = client.counter.getOrCreate("demo"); + + // Increment counter + console.log("Incrementing counter 'demo'..."); + const result1 = await counter.increment(1); + console.log("New count:", result1); + + // Increment again with larger value + console.log("Incrementing counter 'demo' by 5..."); + const result2 = await counter.increment(5); + console.log("New count:", result2); + + // Create another counter + const counter2 = client.counter.getOrCreate("another"); + console.log("Incrementing counter 'another' by 10..."); + const result3 = await counter2.increment(10); + console.log("New count:", result3); + + console.log("✅ Demo completed!"); + } catch (error) { + console.error("❌ Error:", error); + process.exit(1); + } +} + +main().catch(console.error); \ No newline at end of file diff --git a/examples/rivet/src/registry.ts b/examples/rivet/src/registry.ts new file mode 100644 index 000000000..ce4f8a995 --- /dev/null +++ b/examples/rivet/src/registry.ts @@ -0,0 +1,20 @@ +import { worker, setup } from "rivetkit"; + +export const counter = worker({ + onAuth: () => { + // Configure auth here + }, + state: { count: 0 }, + actions: { + increment: (c, x: number) => { + c.state.count += x; + return c.state.count; + }, + }, +}); + +export const registry = setup({ + workers: { counter }, +}); + +export type Registry = typeof registry; diff --git a/examples/rivet/src/server.ts b/examples/rivet/src/server.ts new file mode 100644 index 000000000..a2cbb7f78 --- /dev/null +++ b/examples/rivet/src/server.ts @@ -0,0 +1,4 @@ +import { startManager } from "@rivetkit/rivet/manager"; +import { registry } from "./registry"; + +startManager(registry); diff --git a/examples/rivet/tsconfig.json b/examples/rivet/tsconfig.json new file mode 100644 index 000000000..755f4644c --- /dev/null +++ b/examples/rivet/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "esnext", + /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "lib": ["esnext"], + /* Specify what JSX code is generated. */ + "jsx": "react-jsx", + + /* Specify what module code is generated. */ + "module": "nodenext", + /* Specify how TypeScript looks up a file from a given module specifier. */ + "moduleResolution": "nodenext", + /* Specify type package names to be included without being referenced in a source file. */ + "types": ["node"], + /* Enable importing .json files */ + "resolveJsonModule": true, + + /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ + "allowJs": true, + /* Enable error reporting in type-checked JavaScript files. */ + "checkJs": false, + + /* Ensure that each file can be safely transpiled without relying on other imports. */ + "isolatedModules": true, + /* Allow 'import x from y' when a module doesn't have a default export. */ + "allowSyntheticDefaultImports": true, + /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true, + + /* Enable all strict type-checking options. */ + "strict": true, + + /* Skip type checking all .d.ts files. */ + "skipLibCheck": true, + "outDir": "dist" + }, + "include": ["src/**/*"] +} diff --git a/packages/drivers/file-system/README.md b/packages/drivers/file-system/README.md deleted file mode 100644 index 2f27893c3..000000000 --- a/packages/drivers/file-system/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# RivetKit File System Driver - -_Lightweight Libraries for Backends_ - -[Learn More →](https://github.com/rivet-gg/rivetkit) - -[Discord](https://rivet.gg/discord) — [Documentation](https://rivetkit.org) — [Issues](https://github.com/rivet-gg/rivetkit/issues) - -## License - -Apache 2.0 \ No newline at end of file diff --git a/packages/platforms/bun/README.md b/packages/platforms/bun/README.md deleted file mode 100644 index 5fd4b42c9..000000000 --- a/packages/platforms/bun/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# RivetKit Bun Adapter - -_Lightweight Libraries for Backends_ - -[Learn More →](https://github.com/rivet-gg/rivetkit) - -[Discord](https://rivet.gg/discord) — [Documentation](https://rivetkit.org) — [Issues](https://github.com/rivet-gg/rivetkit/issues) - -## License - -Apache 2.0 \ No newline at end of file diff --git a/packages/platforms/bun/package.json b/packages/platforms/bun/package.json deleted file mode 100644 index 295b7fca7..000000000 --- a/packages/platforms/bun/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@rivetkit/bun", - "version": "0.9.0-rc.1", - "keywords": ["rivetkit", "bun", "platform", "runtime", "fast"], - "files": [ - "src", - "dist", - "package.json" - ], - "type": "module", - "exports": { - ".": { - "import": { - "types": "./dist/mod.d.ts", - "default": "./dist/mod.js" - }, - "require": { - "types": "./dist/mod.d.cts", - "default": "./dist/mod.cjs" - } - }, - "./tsconfig": "./dist/tsconfig.json" - }, - "sideEffects": false, - "scripts": { - "build": "tsup src/mod.ts", - "check-types": "tsc --noEmit" - }, - "peerDependencies": { - "@rivetkit/file-system": "*", - "@rivetkit/memory": "*", - "rivetkit": "*" - }, - "devDependencies": { - "@rivetkit/file-system": "workspace:^", - "@rivetkit/memory": "workspace:*", - "@types/bun": "^1.2.2", - "hono": "^4.7.0", - "rivetkit": "workspace:*", - "tsup": "^8.4.0", - "typescript": "^5.5.2" - }, - "dependencies": { - "dedent": "^1.5.3", - "zod": "^3.24.2" - }, - "stableVersion": "0.8.0" -} diff --git a/packages/platforms/bun/src/config.ts b/packages/platforms/bun/src/config.ts deleted file mode 100644 index 55b30fe8f..000000000 --- a/packages/platforms/bun/src/config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { DriverConfigSchema } from "rivetkit/driver-helpers"; -import { z } from "zod"; - -export const ConfigSchema = DriverConfigSchema.extend({ - mode: z.enum(["file-system", "memory"]).optional().default("file-system"), - hostname: z - .string() - .optional() - .default(process.env.HOSTNAME ?? "127.0.0.1"), - port: z - .number() - .optional() - .default(Number.parseInt(process.env.PORT ?? "6420")), -}).default({}); -export type InputConfig = z.input; diff --git a/packages/platforms/bun/src/log.ts b/packages/platforms/bun/src/log.ts deleted file mode 100644 index c58352cda..000000000 --- a/packages/platforms/bun/src/log.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { getLogger } from "rivetkit/log"; - -export const LOGGER_NAME = "bun"; - -export function logger() { - return getLogger(LOGGER_NAME); -} diff --git a/packages/platforms/bun/src/mod.ts b/packages/platforms/bun/src/mod.ts deleted file mode 100644 index 552c92af7..000000000 --- a/packages/platforms/bun/src/mod.ts +++ /dev/null @@ -1,105 +0,0 @@ -import type { Serve, Server, ServerWebSocket, WebSocketHandler } from "bun"; -import { assertUnreachable } from "rivetkit/utils"; -import { CoordinateTopology } from "rivetkit/topologies/coordinate"; -import { ConfigSchema, type InputConfig } from "./config"; -import { logger } from "./log"; -import { createBunWebSocket } from "hono/bun"; -import type { Hono } from "hono"; -import { type Registry, StandaloneTopology } from "rivetkit"; -import { - MemoryGlobalState, - MemoryManagerDriver, - MemoryWorkerDriver, -} from "@rivetkit/memory"; -import { FileSystemWorkerDriver, FileSystemGlobalState, FileSystemManagerDriver } from "@rivetkit/file-system"; - -export { InputConfig as Config } from "./config"; - -export function createRouter( - registry: Registry, - inputConfig?: InputConfig, -): { - router: Hono; - webSocketHandler: WebSocketHandler; -} { - const config = ConfigSchema.parse(inputConfig); - - // Setup WebSocket routing for Bun - const webSocket = createBunWebSocket(); - if (!config.getUpgradeWebSocket) { - config.getUpgradeWebSocket = () => webSocket.upgradeWebSocket; - } - - // HACK: Hono BunWebSocketHandler type is not compatible with Bun's - const webSocketHandler = webSocket.websocket as unknown as WebSocketHandler; - - // Configure default configuration - if (!config.topology) config.topology = "standalone"; - if (!config.driver.manager || !config.driver.worker) { - if (config.mode === "file-system") { - const fsState = new FileSystemGlobalState(); - if (!config.driver.manager) { - config.driver.manager = new FileSystemManagerDriver(registry, fsState); - } - if (!config.driver.worker) { - config.driver.worker = new FileSystemWorkerDriver(fsState); - } - } else if (config.mode === "memory") { - const memoryState = new MemoryGlobalState(); - if (!config.driver.manager) { - config.driver.manager = new MemoryManagerDriver(registry, memoryState); - } - if (!config.driver.worker) { - config.driver.worker = new MemoryWorkerDriver(memoryState); - } - } else { - assertUnreachable(config.mode); - } - } - - // Setup topology - if (config.topology === "standalone") { - const topology = new StandaloneTopology(registry.config, config); - return { router: topology.router, webSocketHandler }; - } else if (config.topology === "partition") { - throw new Error("Bun only supports standalone & coordinate topology."); - } else if (config.topology === "coordinate") { - const topology = new CoordinateTopology(registry.config, config); - return { router: topology.router, webSocketHandler }; - } else { - assertUnreachable(config.topology); - } -} - -export function createHandler( - registry: Registry, - inputConfig?: InputConfig, -): Serve { - const config = ConfigSchema.parse(inputConfig); - - const { router, webSocketHandler } = createRouter(registry, config); - - return { - hostname: config.hostname, - port: config.port, - fetch: router.fetch, - websocket: webSocketHandler, - }; -} - -export function serve( - registry: Registry, - inputConfig: InputConfig, -): Server { - const config = ConfigSchema.parse(inputConfig); - - const handler = createHandler(registry, config); - const server = Bun.serve(handler); - - logger().info("rivetkit started", { - hostname: config.hostname, - port: config.port, - }); - - return server; -} diff --git a/packages/platforms/bun/tsconfig.json b/packages/platforms/bun/tsconfig.json deleted file mode 100644 index 828128ae2..000000000 --- a/packages/platforms/bun/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "types": ["bun"], - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src/**/*"] -} diff --git a/packages/platforms/bun/tsup.config.ts b/packages/platforms/bun/tsup.config.ts deleted file mode 100644 index 677cffb7b..000000000 --- a/packages/platforms/bun/tsup.config.ts +++ /dev/null @@ -1,4 +0,0 @@ -import defaultConfig from "../../../tsup.base.ts"; -import { defineConfig } from "tsup"; - -export default defineConfig(defaultConfig); diff --git a/packages/platforms/bun/turbo.json b/packages/platforms/bun/turbo.json deleted file mode 100644 index 95960709b..000000000 --- a/packages/platforms/bun/turbo.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://turbo.build/schema.json", - "extends": ["//"] -} diff --git a/packages/platforms/cloudflare-workers/src/config.ts b/packages/platforms/cloudflare-workers/src/config.ts index 31cab579f..3ad481223 100644 --- a/packages/platforms/cloudflare-workers/src/config.ts +++ b/packages/platforms/cloudflare-workers/src/config.ts @@ -1,6 +1,6 @@ -import { DriverConfigSchema } from "rivetkit/driver-helpers"; +import { RunConfigSchema } from "rivetkit/driver-helpers"; import { z } from "zod"; -export const ConfigSchema = DriverConfigSchema.default({}); +export const ConfigSchema = RunConfigSchema.omit({ driver: true, getUpgradeWebSocket: true }).default({}); export type InputConfig = z.input; export type Config = z.infer; diff --git a/packages/platforms/cloudflare-workers/src/handler.ts b/packages/platforms/cloudflare-workers/src/handler.ts index 753071fbf..920127f28 100644 --- a/packages/platforms/cloudflare-workers/src/handler.ts +++ b/packages/platforms/cloudflare-workers/src/handler.ts @@ -15,7 +15,7 @@ import type { Hono } from "hono"; import { PartitionTopologyManager } from "rivetkit/topologies/partition"; import { logger } from "./log"; import { CloudflareWorkersManagerDriver } from "./manager-driver"; -import { Encoding, Registry } from "rivetkit"; +import { Encoding, Registry, RunConfig } from "rivetkit"; import { upgradeWebSocket } from "./websocket"; import invariant from "invariant"; import { AsyncLocalStorage } from "node:async_hooks"; @@ -76,178 +76,155 @@ export function createRouter( router: Hono<{ Bindings: Bindings }>; WorkerHandler: DurableObjectConstructor; } { - const driverConfig = ConfigSchema.parse(inputConfig); - - // Configure drivers - // - // Worker driver will get set in `WorkerHandler` - if (!driverConfig.driver.manager) - driverConfig.driver.manager = new CloudflareWorkersManagerDriver(); - - // Setup WebSockets - if (!driverConfig.getUpgradeWebSocket) - driverConfig.getUpgradeWebSocket = () => upgradeWebSocket; + const config = ConfigSchema.parse(inputConfig); + const runConfig = { + driver: { + topology: "partition", + manager: new CloudflareWorkersManagerDriver(), + // HACK: We can't build the worker driver until we're inside the Druable Object + worker: undefined as any, + }, + getUpgradeWebSocket: () => upgradeWebSocket, + ...config, + } satisfies RunConfig; // Create Durable Object - const WorkerHandler = createWorkerDurableObject(registry, driverConfig); - - driverConfig.topology = driverConfig.topology ?? "partition"; - if (driverConfig.topology === "partition") { - const managerTopology = new PartitionTopologyManager( - registry.config, - driverConfig, - { - sendRequest: async (workerId, workerRequest): Promise => { - const env = getCloudflareAmbientEnv(); - - logger().debug("sending request to durable object", { - workerId, - method: workerRequest.method, - url: workerRequest.url, - }); + const WorkerHandler = createWorkerDurableObject(registry, runConfig); - const id = env.WORKER_DO.idFromString(workerId); - const stub = env.WORKER_DO.get(id); + const managerTopology = new PartitionTopologyManager( + registry.config, + runConfig, + { + sendRequest: async (workerId, workerRequest): Promise => { + const env = getCloudflareAmbientEnv(); - return await stub.fetch(workerRequest); - }, - - openWebSocket: async ( + logger().debug("sending request to durable object", { workerId, - encodingKind: Encoding, - params: unknown, - ): Promise => { - const env = getCloudflareAmbientEnv(); - - logger().debug("opening websocket to durable object", { workerId }); - - // Make a fetch request to the Durable Object with WebSocket upgrade - const id = env.WORKER_DO.idFromString(workerId); - const stub = env.WORKER_DO.get(id); - - const headers: Record = { - Upgrade: "websocket", - Connection: "Upgrade", - [HEADER_EXPOSE_INTERNAL_ERROR]: "true", - [HEADER_ENCODING]: encodingKind, - }; - if (params) { - headers[HEADER_CONN_PARAMS] = JSON.stringify(params); - } - // HACK: See packages/platforms/cloudflare-workers/src/websocket.ts - headers["sec-websocket-protocol"] = "rivetkit"; - - const response = await stub.fetch("http://worker/connect/websocket", { - headers, - }); - const webSocket = response.webSocket; - - if (!webSocket) { - throw new InternalError( - "missing websocket connection in response from DO", - ); - } + method: workerRequest.method, + url: workerRequest.url, + }); - logger().debug("durable object websocket connection open", { - workerId, - }); + const id = env.WORKER_DO.idFromString(workerId); + const stub = env.WORKER_DO.get(id); - webSocket.accept(); + return await stub.fetch(workerRequest); + }, - // TODO: Is this still needed? - // HACK: Cloudflare does not call onopen automatically, so we need - // to call this on the next tick - setTimeout(() => { - (webSocket as any).onopen?.(new Event("open")); - }, 0); + openWebSocket: async ( + workerId, + encodingKind: Encoding, + params: unknown, + ): Promise => { + const env = getCloudflareAmbientEnv(); + + logger().debug("opening websocket to durable object", { workerId }); + + // Make a fetch request to the Durable Object with WebSocket upgrade + const id = env.WORKER_DO.idFromString(workerId); + const stub = env.WORKER_DO.get(id); + + const headers: Record = { + Upgrade: "websocket", + Connection: "Upgrade", + [HEADER_EXPOSE_INTERNAL_ERROR]: "true", + [HEADER_ENCODING]: encodingKind, + }; + if (params) { + headers[HEADER_CONN_PARAMS] = JSON.stringify(params); + } + // HACK: See packages/platforms/cloudflare-workers/src/websocket.ts + headers["sec-websocket-protocol"] = "rivetkit"; + + const response = await stub.fetch("http://worker/connect/websocket", { + headers, + }); + const webSocket = response.webSocket; + + if (!webSocket) { + throw new InternalError( + "missing websocket connection in response from DO", + ); + } + + logger().debug("durable object websocket connection open", { + workerId, + }); - return webSocket as unknown as WebSocket; - }, + webSocket.accept(); - proxyRequest: async (c, workerRequest, workerId): Promise => { - logger().debug("forwarding request to durable object", { - workerId, - method: workerRequest.method, - url: workerRequest.url, - }); + // TODO: Is this still needed? + // HACK: Cloudflare does not call onopen automatically, so we need + // to call this on the next tick + setTimeout(() => { + (webSocket as any).onopen?.(new Event("open")); + }, 0); - const id = c.env.WORKER_DO.idFromString(workerId); - const stub = c.env.WORKER_DO.get(id); + return webSocket as unknown as WebSocket; + }, - return await stub.fetch(workerRequest); - }, - proxyWebSocket: async ( - c, - path, + proxyRequest: async (c, workerRequest, workerId): Promise => { + logger().debug("forwarding request to durable object", { workerId, - encoding, - params, - authData, - ) => { - logger().debug("forwarding websocket to durable object", { - workerId, - path, - }); + method: workerRequest.method, + url: workerRequest.url, + }); - // Validate upgrade - const upgradeHeader = c.req.header("Upgrade"); - if (!upgradeHeader || upgradeHeader !== "websocket") { - return new Response("Expected Upgrade: websocket", { - status: 426, - }); - } + const id = c.env.WORKER_DO.idFromString(workerId); + const stub = c.env.WORKER_DO.get(id); - // TODO: strip headers - const newUrl = new URL(`http://worker${path}`); - const workerRequest = new Request(newUrl, c.req.raw); - - // Always build fresh request to prevent forwarding unwanted headers - // HACK: Since we can't build a new request, we need to remove - // non-standard headers manually - const headerKeys: string[] = []; - workerRequest.headers.forEach((v, k) => headerKeys.push(k)); - for (const k of headerKeys) { - if (!STANDARD_WEBSOCKET_HEADERS.includes(k)) { - workerRequest.headers.delete(k); - } - } + return await stub.fetch(workerRequest); + }, + proxyWebSocket: async (c, path, workerId, encoding, params, authData) => { + logger().debug("forwarding websocket to durable object", { + workerId, + path, + }); - // Add RivetKit headers - workerRequest.headers.set(HEADER_EXPOSE_INTERNAL_ERROR, "true"); - workerRequest.headers.set(HEADER_ENCODING, encoding); - if (params) { - workerRequest.headers.set( - HEADER_CONN_PARAMS, - JSON.stringify(params), - ); - } - if (authData) { - workerRequest.headers.set( - HEADER_AUTH_DATA, - JSON.stringify(authData), - ); + // Validate upgrade + const upgradeHeader = c.req.header("Upgrade"); + if (!upgradeHeader || upgradeHeader !== "websocket") { + return new Response("Expected Upgrade: websocket", { + status: 426, + }); + } + + // TODO: strip headers + const newUrl = new URL(`http://worker${path}`); + const workerRequest = new Request(newUrl, c.req.raw); + + // Always build fresh request to prevent forwarding unwanted headers + // HACK: Since we can't build a new request, we need to remove + // non-standard headers manually + const headerKeys: string[] = []; + workerRequest.headers.forEach((v, k) => headerKeys.push(k)); + for (const k of headerKeys) { + if (!STANDARD_WEBSOCKET_HEADERS.includes(k)) { + workerRequest.headers.delete(k); } + } + + // Add RivetKit headers + workerRequest.headers.set(HEADER_EXPOSE_INTERNAL_ERROR, "true"); + workerRequest.headers.set(HEADER_ENCODING, encoding); + if (params) { + workerRequest.headers.set(HEADER_CONN_PARAMS, JSON.stringify(params)); + } + if (authData) { + workerRequest.headers.set(HEADER_AUTH_DATA, JSON.stringify(authData)); + } + + const id = c.env.WORKER_DO.idFromString(workerId); + const stub = c.env.WORKER_DO.get(id); + + return await stub.fetch(workerRequest); + }, + }, + ); - const id = c.env.WORKER_DO.idFromString(workerId); - const stub = c.env.WORKER_DO.get(id); + // Force the router to have access to the Cloudflare bindings + const router = managerTopology.router as unknown as Hono<{ + Bindings: Bindings; + }>; - return await stub.fetch(workerRequest); - }, - }, - ); - - // Force the router to have access to the Cloudflare bindings - const router = managerTopology.router as unknown as Hono<{ - Bindings: Bindings; - }>; - - return { router, WorkerHandler }; - } else if ( - driverConfig.topology === "standalone" || - driverConfig.topology === "coordinate" - ) { - throw new Error("Cloudflare only supports partition topology."); - } else { - assertUnreachable(driverConfig.topology); - } + return { router, WorkerHandler }; } diff --git a/packages/platforms/cloudflare-workers/src/worker-handler-do.ts b/packages/platforms/cloudflare-workers/src/worker-handler-do.ts index 954b1b20f..3767aaca6 100644 --- a/packages/platforms/cloudflare-workers/src/worker-handler-do.ts +++ b/packages/platforms/cloudflare-workers/src/worker-handler-do.ts @@ -1,7 +1,6 @@ import { DurableObject } from "cloudflare:workers"; -import type { Registry, WorkerKey } from "rivetkit"; +import type { Registry, RunConfig, WorkerKey } from "rivetkit"; import { logger } from "./log"; -import type { Config } from "./config"; import { PartitionTopologyWorker } from "rivetkit/topologies/partition"; import { CloudflareDurableObjectGlobalState, @@ -43,7 +42,7 @@ interface LoadedWorker { export function createWorkerDurableObject( registry: Registry, - config: Config, + runConfig: RunConfig, ): DurableObjectConstructor { const globalState = new CloudflareDurableObjectGlobalState(); @@ -100,12 +99,13 @@ export function createWorkerDurableObject( if (!this.#initialized) throw new Error("Not initialized"); - // Create topology - if (!config.drivers) config.drivers = {}; - if (!config.driver.worker) { - config.driver.worker = new CloudflareWorkersWorkerDriver(globalState); - } - const workerTopology = new PartitionTopologyWorker(registry.config, config); + // Configure worker driver + runConfig.driver.worker = new CloudflareWorkersWorkerDriver(globalState); + + const workerTopology = new PartitionTopologyWorker( + registry.config, + runConfig, + ); // Register DO with global state // HACK: This leaks the DO context, but DO does not provide a native way diff --git a/packages/platforms/rivet/src/config.ts b/packages/platforms/rivet/src/config.ts index 31cab579f..c98a03918 100644 --- a/packages/platforms/rivet/src/config.ts +++ b/packages/platforms/rivet/src/config.ts @@ -1,6 +1,9 @@ -import { DriverConfigSchema } from "rivetkit/driver-helpers"; +import { RunConfigSchema } from "rivetkit/driver-helpers"; import { z } from "zod"; -export const ConfigSchema = DriverConfigSchema.default({}); +export const ConfigSchema = RunConfigSchema.omit({ + driver: true, + getUpgradeWebSocket: true, +}).default({}); export type InputConfig = z.input; export type Config = z.infer; diff --git a/packages/platforms/rivet/src/manager-driver.ts b/packages/platforms/rivet/src/manager-driver.ts index 3b5f36995..33dae4fef 100644 --- a/packages/platforms/rivet/src/manager-driver.ts +++ b/packages/platforms/rivet/src/manager-driver.ts @@ -123,11 +123,13 @@ export class RivetManagerDriver implements ManagerDriver { }, }, runtime: { - environment: workerLogLevel - ? { - _LOG_LEVEL: workerLogLevel, - } - : {}, + environment: { + RIVET_ENDPOINT: this.#clientConfig.endpoint, + RIVET_SERVICE_TOKEN: this.#clientConfig.token, + RIVET_PROJECT: this.#clientConfig.project, + RIVET_ENVIRONMENT: this.#clientConfig.environment, + ...(workerLogLevel ? { _LOG_LEVEL: workerLogLevel } : {}), + }, }, lifecycle: { durable: true, diff --git a/packages/platforms/rivet/src/manager.ts b/packages/platforms/rivet/src/manager.ts index 454027dd0..8c0014b31 100644 --- a/packages/platforms/rivet/src/manager.ts +++ b/packages/platforms/rivet/src/manager.ts @@ -8,7 +8,7 @@ import { PartitionTopologyManager } from "rivetkit/topologies/partition"; import { proxy } from "hono/proxy"; import invariant from "invariant"; import { ConfigSchema, InputConfig } from "./config"; -import type { Registry } from "rivetkit"; +import type { Registry, RunConfig } from "rivetkit"; import { createWebSocketProxy } from "./ws-proxy"; import { flushCache, getWorkerMeta } from "./worker-meta"; import { @@ -18,6 +18,7 @@ import { HEADER_EXPOSE_INTERNAL_ERROR, } from "rivetkit/driver-helpers"; import { importWebSocket } from "rivetkit/driver-helpers/websocket"; +import { RivetWorkerDriver } from "./worker-driver"; export async function startManager( registry: Registry, @@ -25,8 +26,6 @@ export async function startManager( ): Promise { setupLogging(); - const driverConfig = ConfigSchema.parse(inputConfig); - const portStr = process.env.PORT_HTTP; if (!portStr) { throw "Missing port"; @@ -36,8 +35,8 @@ export async function startManager( throw "Invalid port"; } - const endpoint = process.env.RIVET_API_ENDPOINT; - if (!endpoint) throw new Error("missing RIVET_API_ENDPOINT"); + const endpoint = process.env.RIVET_ENDPOINT; + if (!endpoint) throw new Error("missing RIVET_ENDPOINT"); const token = process.env.RIVET_SERVICE_TOKEN; if (!token) throw new Error("missing RIVET_SERVICE_TOKEN"); const project = process.env.RIVET_PROJECT; @@ -52,6 +51,26 @@ export async function startManager( environment, }; + const config = ConfigSchema.parse(inputConfig); + let injectWebSocket: NodeWebSocket["injectWebSocket"] | undefined; + const runConfig = { + driver: { + topology: "partition", + manager: new RivetManagerDriver(clientConfig), + // HACK: We can't build the worker driver until we're inside the worker + worker: undefined as any, + }, + // Setup WebSocket routing for Node + // + // Save `injectWebSocket` for after server is created + getUpgradeWebSocket: (app) => { + const webSocket = createNodeWebSocket({ app }); + injectWebSocket = webSocket.injectWebSocket; + return webSocket.upgradeWebSocket; + }, + ...config, + } satisfies RunConfig; + //// Force disable inspector //driverConfig.registry.config.inspector = { // enabled: false, @@ -80,29 +99,10 @@ export async function startManager( // }, //}; - // Setup manager driver - if (!driverConfig.drivers) driverConfig.drivers = {}; - if (!driverConfig.driver.manager) { - driverConfig.driver.manager = new RivetManagerDriver(clientConfig); - } - - // Setup WebSocket routing for Node - // - // Save `injectWebSocket` for after server is created - let injectWebSocket: NodeWebSocket["injectWebSocket"] | undefined; - if (!driverConfig.getUpgradeWebSocket) { - driverConfig.getUpgradeWebSocket = (app) => { - const webSocket = createNodeWebSocket({ app }); - injectWebSocket = webSocket.injectWebSocket; - return webSocket.upgradeWebSocket; - }; - } - // Create manager topology - driverConfig.topology = driverConfig.topology ?? "partition"; const managerTopology = new PartitionTopologyManager( registry.config, - driverConfig, + runConfig, { sendRequest: async (workerId, workerRequest) => { const meta = await getWorkerMeta(clientConfig, workerId); @@ -200,10 +200,12 @@ export async function startManager( ); // HACK: Expose endpoint for tests to flush cache - managerTopology.router.post("/.test/rivet/flush-cache", (c) => { - flushCache(); - return c.text("ok"); - }); + if (registry.config.test.enabled) { + managerTopology.router.post("/.test/rivet/flush-cache", (c) => { + flushCache(); + return c.text("ok"); + }); + } // Start server with ambient env wrapper logger().info("server running", { port }); diff --git a/packages/platforms/rivet/src/worker.ts b/packages/platforms/rivet/src/worker.ts index b792cd08e..d53317fde 100644 --- a/packages/platforms/rivet/src/worker.ts +++ b/packages/platforms/rivet/src/worker.ts @@ -6,17 +6,19 @@ import { PartitionTopologyWorker } from "rivetkit/topologies/partition"; import { RivetWorkerDriver } from "./worker-driver"; import invariant from "invariant"; import type { ActorContext } from "@rivet-gg/actor-core"; -import { Registry } from "rivetkit"; +import { Registry, RunConfig } from "rivetkit"; import { type Config, ConfigSchema, type InputConfig } from "./config"; import { stringifyError } from "rivetkit/utils"; +import { RivetManagerDriver } from "./manager-driver"; +import { RivetClientConfig } from "./rivet-client"; export function createWorkerHandler( registry: Registry, inputConfig?: InputConfig, ): RivetHandler { - let driverConfig: Config; + let config: Config; try { - driverConfig = ConfigSchema.parse(inputConfig); + config = ConfigSchema.parse(inputConfig); } catch (error) { logger().error("failed to start manager", { error: stringifyError(error) }); Deno.exit(1); @@ -26,7 +28,7 @@ export function createWorkerHandler( async start(ctx: ActorContext) { const role = ctx.metadata.actor.tags.role; if (role === "worker") { - await startWorker(ctx, registry, driverConfig); + await startWorker(ctx, registry, config); } else { throw new Error(`Unexpected role (must be worker): ${role}`); } @@ -37,7 +39,7 @@ export function createWorkerHandler( async function startWorker( ctx: ActorContext, registry: Registry, - driverConfig: Config, + config: Config, ): Promise { setupLogging(); @@ -50,26 +52,42 @@ async function startWorker( throw "Invalid port"; } - const endpoint = Deno.env.get("RIVET_API_ENDPOINT"); - if (!endpoint) throw new Error("missing RIVET_API_ENDPOINT"); + const endpoint = Deno.env.get("RIVET_ENDPOINT"); + if (!endpoint) throw new Error("missing RIVET_ENDPOINT"); + const token = Deno.env.get("RIVET_SERVICE_TOKEN"); + if (!token) throw new Error("missing RIVET_SERVICE_TOKEN"); + const project = Deno.env.get("RIVET_PROJECT"); + if (!project) throw new Error("missing RIVET_PROJECT"); + const environment = Deno.env.get("RIVET_ENVIRONMENT"); + if (!environment) throw new Error("missing RIVET_ENVIRONMENT"); + + const clientConfig: RivetClientConfig = { + endpoint, + token, + project, + environment, + }; + + const runConfig = { + driver: { + topology: "partition", + manager: new RivetManagerDriver(clientConfig), + worker: new RivetWorkerDriver(ctx), + }, + getUpgradeWebSocket: () => upgradeWebSocket, + ...config, + } satisfies RunConfig; // Initialization promise + // + // Resolve immediately if already initialized + // + // Otherwise, will wait for `POST /initialize` request const initializedPromise = Promise.withResolvers(); if ((await ctx.kv.get(["rivetkit", "initialized"])) === true) { initializedPromise.resolve(undefined); } - // Setup worker driver - if (!driverConfig.drivers) driverConfig.drivers = {}; - if (!driverConfig.driver.worker) { - driverConfig.driver.worker = new RivetWorkerDriver(ctx); - } - - // Setup WebSocket upgrader - if (!driverConfig.getUpgradeWebSocket) { - driverConfig.getUpgradeWebSocket = () => upgradeWebSocket; - } - //registry.config.inspector = { // enabled: true, // onRequest: async (c) => { @@ -117,8 +135,10 @@ async function startWorker( //}; // Create worker topology - driverConfig.topology = driverConfig.topology ?? "partition"; - const workerTopology = new PartitionTopologyWorker(registry.config, driverConfig); + const workerTopology = new PartitionTopologyWorker( + registry.config, + runConfig, + ); // Set a catch-all route const router = workerTopology.router; diff --git a/packages/platforms/rivet/src/ws-proxy.ts b/packages/platforms/rivet/src/ws-proxy.ts index 7bba7eeed..12b75d094 100644 --- a/packages/platforms/rivet/src/ws-proxy.ts +++ b/packages/platforms/rivet/src/ws-proxy.ts @@ -17,7 +17,7 @@ export async function createWebSocketProxy( ) { const WebSocket = await importWebSocket(); - let targetWs: WebSocket | undefined = undefined; + let targetWs: any | undefined = undefined; // WS not compatible between Node & browser const messageQueue: any[] = []; return { @@ -39,11 +39,11 @@ export async function createWebSocketProxy( } }; - targetWs.onmessage = (event) => { + targetWs.onmessage = (event: any) => { wsContext.send(event.data as any); }; - targetWs.onclose = (event) => { + targetWs.onclose = (event: any) => { logger().debug("target websocket closed", { code: event.code, reason: event.reason, @@ -55,7 +55,7 @@ export async function createWebSocketProxy( } }; - targetWs.onerror = (event) => { + targetWs.onerror = () => { logger().warn("target websocket error"); if (wsContext.readyState === WebSocket.OPEN) { diff --git a/packages/platforms/rivet/tests/rivet-deploy.ts b/packages/platforms/rivet/tests/rivet-deploy.ts index fb4d8cb30..37f7e4b83 100644 --- a/packages/platforms/rivet/tests/rivet-deploy.ts +++ b/packages/platforms/rivet/tests/rivet-deploy.ts @@ -127,7 +127,7 @@ export async function deployToRivet(projectPath: string) { dockerfile: "Dockerfile", runtime: { environment: { - RIVET_API_ENDPOINT: apiEndpoint, + RIVET_ENDPOINT: apiEndpoint, RIVET_SERVICE_TOKEN: rivetCloudToken, // TODO: This should be a service token, but both work RIVET_PROJECT: project, RIVET_ENVIRONMENT: environment, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b79583929..6900a7393 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,6 +49,61 @@ importers: specifier: ^8.3.2 version: 8.5.5 + examples/better-auth: + dependencies: + '@hono/node-server': + specifier: ^1.14.4 + version: 1.14.4(hono@4.8.0) + '@rivetkit/memory': + specifier: workspace:0.9.0-rc.1 + version: link:../../packages/drivers/memory + '@rivetkit/react': + specifier: workspace:0.9.0-rc.1 + version: link:../../packages/frameworks/react + better-auth: + specifier: ^1.0.1 + version: 1.2.10 + hono: + specifier: ^4.7.0 + version: 4.8.0 + react: + specifier: ^18.3 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.3.1) + devDependencies: + '@types/node': + specifier: ^22.13.9 + version: 22.15.32 + '@types/react': + specifier: ^18.2.0 + version: 18.3.23 + '@types/react-dom': + specifier: ^18.2.0 + version: 18.3.7(@types/react@18.3.23) + '@vitejs/plugin-react': + specifier: ^4.2.0 + version: 4.5.2(vite@5.4.19(@types/node@22.15.32)) + concurrently: + specifier: ^8.2.2 + version: 8.2.2 + rivetkit: + specifier: workspace:* + version: link:../../packages/core + tsx: + specifier: ^3.12.7 + version: 3.14.0 + typescript: + specifier: ^5.5.2 + version: 5.8.3 + vite: + specifier: ^5.0.0 + version: 5.4.19(@types/node@22.15.32) + vitest: + specifier: ^3.1.1 + version: 3.2.4(@types/node@22.15.32)(tsx@3.14.0)(yaml@2.8.0) + examples/chat-room: dependencies: '@rivetkit/nodejs': @@ -77,6 +132,31 @@ importers: specifier: ^3.1.1 version: 3.2.4(@types/node@22.15.32)(tsx@3.14.0)(yaml@2.8.0) + examples/cloudflare-workers: + dependencies: + '@rivetkit/cloudflare-workers': + specifier: workspace:* + version: link:../../packages/platforms/cloudflare-workers + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20250129.0 + version: 4.20250619.0 + '@types/node': + specifier: ^22.13.9 + version: 22.15.32 + rivetkit: + specifier: workspace:* + version: link:../../packages/core + tsx: + specifier: ^3.12.7 + version: 3.14.0 + typescript: + specifier: ^5.5.2 + version: 5.8.3 + wrangler: + specifier: ^3.0.0 + version: 3.114.9(@cloudflare/workers-types@4.20250619.0) + examples/counter: dependencies: '@rivetkit/nodejs': @@ -309,6 +389,25 @@ importers: specifier: ^3.1.1 version: 3.2.4(@types/node@22.15.32)(tsx@3.14.0)(yaml@2.8.0) + examples/rivet: + dependencies: + '@rivetkit/rivet': + specifier: https://pkg.pr.new/rivet-gg/rivetkit/@rivetkit/rivet@65c3659 + version: https://pkg.pr.new/rivet-gg/rivetkit/@rivetkit/rivet@65c3659(rivetkit@https://pkg.pr.new/rivet-gg/rivetkit@65c3659(@hono/node-server@1.14.4(hono@4.8.0))(@hono/node-ws@1.1.7(@hono/node-server@1.14.4(hono@4.8.0))(hono@4.8.0))(eventsource@3.0.7)(ws@8.18.2)) + rivetkit: + specifier: https://pkg.pr.new/rivet-gg/rivetkit@65c3659 + version: https://pkg.pr.new/rivet-gg/rivetkit@65c3659(@hono/node-server@1.14.4(hono@4.8.0))(@hono/node-ws@1.1.7(@hono/node-server@1.14.4(hono@4.8.0))(hono@4.8.0))(eventsource@3.0.7)(ws@8.18.2) + devDependencies: + '@types/node': + specifier: ^22.13.9 + version: 22.15.32 + tsx: + specifier: ^3.12.7 + version: 3.14.0 + typescript: + specifier: ^5.5.2 + version: 5.8.3 + examples/trpc: dependencies: '@rivetkit/memory': @@ -563,37 +662,6 @@ importers: specifier: ^3.101.0 version: 3.114.9(@cloudflare/workers-types@4.20250619.0) - packages/platforms/bun: - dependencies: - dedent: - specifier: ^1.5.3 - version: 1.6.0 - zod: - specifier: ^3.24.2 - version: 3.25.67 - devDependencies: - '@rivetkit/file-system': - specifier: workspace:^ - version: link:../../drivers/file-system - '@rivetkit/memory': - specifier: workspace:* - version: link:../../drivers/memory - '@types/bun': - specifier: ^1.2.2 - version: 1.2.17 - hono: - specifier: ^4.7.0 - version: 4.8.0 - rivetkit: - specifier: workspace:* - version: link:../../core - tsup: - specifier: ^8.4.0 - version: 8.5.0(@microsoft/api-extractor@7.52.8(@types/node@24.0.3))(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) - typescript: - specifier: ^5.5.2 - version: 5.8.3 - packages/platforms/cloudflare-workers: dependencies: hono: @@ -805,6 +873,12 @@ packages: resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==} engines: {node: '>=6.9.0'} + '@better-auth/utils@0.2.5': + resolution: {integrity: sha512-uI2+/8h/zVsH8RrYdG8eUErbuGBk16rZKQfz8CjxQOyCE6v7BqFYEbFwvOkvl1KbUdxhqOnXp78+uE5h8qVEgQ==} + + '@better-fetch/fetch@1.1.18': + resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==} + '@biomejs/biome@1.9.4': resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} engines: {node: '>=14.21.3'} @@ -1544,6 +1618,9 @@ packages: resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} + '@hexagon/base64@1.1.28': + resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} + '@hono/node-server@1.14.4': resolution: {integrity: sha512-DnxpshhYewr2q9ZN8ez/M5mmc3sucr8CT1sIgIy1bkeUXut9XWDkqHoFHRhWIQgkYnKpVRxunyhK7WzpJeJ6qQ==} engines: {node: '>=18.14.1'} @@ -1703,6 +1780,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@levischuck/tiny-cbor@0.2.11': + resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==} + '@microsoft/api-extractor-model@7.30.6': resolution: {integrity: sha512-znmFn69wf/AIrwHya3fxX6uB5etSIn6vg4Q4RB/tb5VDDs1rqREc+AvMC/p19MUN13CZ7+V/8pkYPTj7q8tftg==} @@ -1716,6 +1796,13 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@noble/ciphers@0.6.0': + resolution: {integrity: sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ==} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1728,6 +1815,21 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@peculiar/asn1-android@2.3.16': + resolution: {integrity: sha512-a1viIv3bIahXNssrOIkXZIlI2ePpZaNmR30d4aBL99mu2rO+mT9D6zBsp7H6eROWGtmwv0Ionp5olJurIo09dw==} + + '@peculiar/asn1-ecc@2.3.15': + resolution: {integrity: sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA==} + + '@peculiar/asn1-rsa@2.3.15': + resolution: {integrity: sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg==} + + '@peculiar/asn1-schema@2.3.15': + resolution: {integrity: sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==} + + '@peculiar/asn1-x509@2.3.15': + resolution: {integrity: sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1741,6 +1843,12 @@ packages: '@rivet-gg/api@25.4.2': resolution: {integrity: sha512-VmQ+1dfwWoDamF+49A+cB5htnjJbD4LzmQiaiycBS4ICqAiC3b9rW6l1UAyUfG32x02cSRRJ80yApBl54CdlqQ==} + '@rivetkit/rivet@https://pkg.pr.new/rivet-gg/rivetkit/@rivetkit/rivet@65c3659': + resolution: {tarball: https://pkg.pr.new/rivet-gg/rivetkit/@rivetkit/rivet@65c3659} + version: 0.9.0-rc.1 + peerDependencies: + rivetkit: '*' + '@rolldown/pluginutils@1.0.0-beta.11': resolution: {integrity: sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==} @@ -1875,6 +1983,13 @@ packages: '@rushstack/ts-command-line@5.0.1': resolution: {integrity: sha512-bsbUucn41UXrQK7wgM8CNM/jagBytEyJqXw/umtI8d68vFm1Jwxh1OtLrlW7uGZgjCWiiPH6ooUNa1aVsuVr3Q==} + '@simplewebauthn/browser@13.1.0': + resolution: {integrity: sha512-WuHZ/PYvyPJ9nxSzgHtOEjogBhwJfC8xzYkPC+rR/+8chl/ft4ngjiK8kSU5HtRJfczupyOh33b25TjYbvwAcg==} + + '@simplewebauthn/server@13.1.1': + resolution: {integrity: sha512-1hsLpRHfSuMB9ee2aAdh0Htza/X3f4djhYISrggqGe3xopNjOcePiSDkDDoPzDYaaMCrbqGP1H2TYU7bgL9PmA==} + engines: {node: '>=20.0.0'} + '@sinclair/typebox@0.34.35': resolution: {integrity: sha512-C6ypdODf2VZkgRT6sFM8E1F8vR+HcffniX0Kp8MsU8PIfrlXbNCBz0jzj17GjdmjTx1OtZzdH8+iALL21UjF5A==} @@ -1923,9 +2038,6 @@ packages: '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} - '@types/bun@1.2.17': - resolution: {integrity: sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q==} - '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} @@ -2167,6 +2279,10 @@ packages: as-table@1.0.55: resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} + asn1js@3.0.6: + resolution: {integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==} + engines: {node: '>=12.0.0'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -2180,6 +2296,12 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + better-auth@1.2.10: + resolution: {integrity: sha512-nEj1RG4DdLUuJiV5CR93ORyPCptGRBwksaPPCkUtGo9ka+UIlTpaiKoTaTqVLLYlqwX4bOj9tJ32oBNdf2G3Kg==} + + better-call@1.0.9: + resolution: {integrity: sha512-Qfm0gjk0XQz0oI7qvTK1hbqTsBY4xV2hsHAxF8LZfUYl3RaECCIifXuVqtPpZJWvlCCMlQSvkvhhyuApGUba6g==} + birpc@0.2.14: resolution: {integrity: sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==} @@ -2211,9 +2333,6 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - bun-types@1.2.17: - resolution: {integrity: sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ==} - bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2798,6 +2917,9 @@ packages: jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + jose@5.10.0: + resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -2834,6 +2956,10 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + kysely@0.28.2: + resolution: {integrity: sha512-4YAVLoF0Sf0UTqlhgQMFU9iQECdah7n+13ANkiuVfRvlK+uI0Etbgd7bVP36dKlG+NXWbhGua8vnGt+sdhvT7A==} + engines: {node: '>=18.0.0'} + lefthook-darwin-arm64@1.11.14: resolution: {integrity: sha512-YPbUK6kGytY5W6aNUrzK7Vod3ynLVvj5JQiBh0DjlxCHMgIQPpFkDfwQzGw1E8CySyC95HXO83En5Ule8umS7g==} cpu: [arm64] @@ -3031,6 +3157,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanostores@0.11.4: + resolution: {integrity: sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ==} + engines: {node: ^18.0.0 || >=20.0.0} + negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -3207,6 +3337,13 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pvtsutils@1.3.6: + resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} + + pvutils@1.1.3: + resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} + engines: {node: '>=6.0.0'} + qs@6.14.0: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} @@ -3291,6 +3428,25 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rivetkit@https://pkg.pr.new/rivet-gg/rivetkit@65c3659: + resolution: {tarball: https://pkg.pr.new/rivet-gg/rivetkit@65c3659} + version: 0.9.0-rc.1 + engines: {node: '>=22.0.0'} + peerDependencies: + '@hono/node-server': ^1.14.0 + '@hono/node-ws': ^1.1.1 + eventsource: ^3.0.5 + ws: ^8.0.0 + peerDependenciesMeta: + '@hono/node-server': + optional: true + '@hono/node-ws': + optional: true + eventsource: + optional: true + ws: + optional: true + rollup-plugin-inject@3.0.2: resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject. @@ -3306,6 +3462,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rou3@0.5.1: + resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==} + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -3350,6 +3509,9 @@ packages: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -3659,6 +3821,9 @@ packages: resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==} engines: {node: '>=18'} + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} @@ -4093,6 +4258,13 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@better-auth/utils@0.2.5': + dependencies: + typescript: 5.8.3 + uncrypto: 0.1.3 + + '@better-fetch/fetch@1.1.18': {} + '@biomejs/biome@1.9.4': optionalDependencies: '@biomejs/cli-darwin-arm64': 1.9.4 @@ -4503,6 +4675,8 @@ snapshots: '@fastify/busboy@2.1.1': {} + '@hexagon/base64@1.1.28': {} + '@hono/node-server@1.14.4(hono@4.8.0)': dependencies: hono: 4.8.0 @@ -4636,6 +4810,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@levischuck/tiny-cbor@0.2.11': {} + '@microsoft/api-extractor-model@7.30.6(@types/node@22.15.32)': dependencies: '@microsoft/tsdoc': 0.15.1 @@ -4699,6 +4875,10 @@ snapshots: '@microsoft/tsdoc@0.15.1': {} + '@noble/ciphers@0.6.0': {} + + '@noble/hashes@1.8.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4711,6 +4891,39 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@peculiar/asn1-android@2.3.16': + dependencies: + '@peculiar/asn1-schema': 2.3.15 + asn1js: 3.0.6 + tslib: 2.8.1 + + '@peculiar/asn1-ecc@2.3.15': + dependencies: + '@peculiar/asn1-schema': 2.3.15 + '@peculiar/asn1-x509': 2.3.15 + asn1js: 3.0.6 + tslib: 2.8.1 + + '@peculiar/asn1-rsa@2.3.15': + dependencies: + '@peculiar/asn1-schema': 2.3.15 + '@peculiar/asn1-x509': 2.3.15 + asn1js: 3.0.6 + tslib: 2.8.1 + + '@peculiar/asn1-schema@2.3.15': + dependencies: + asn1js: 3.0.6 + pvtsutils: 1.3.6 + tslib: 2.8.1 + + '@peculiar/asn1-x509@2.3.15': + dependencies: + '@peculiar/asn1-schema': 2.3.15 + asn1js: 3.0.6 + pvtsutils: 1.3.6 + tslib: 2.8.1 + '@pkgjs/parseargs@0.11.0': optional: true @@ -4731,6 +4944,18 @@ snapshots: transitivePeerDependencies: - encoding + '@rivetkit/rivet@https://pkg.pr.new/rivet-gg/rivetkit/@rivetkit/rivet@65c3659(rivetkit@https://pkg.pr.new/rivet-gg/rivetkit@65c3659(@hono/node-server@1.14.4(hono@4.8.0))(@hono/node-ws@1.1.7(@hono/node-server@1.14.4(hono@4.8.0))(hono@4.8.0))(eventsource@3.0.7)(ws@8.18.2))': + dependencies: + '@hono/node-server': 1.14.4(hono@4.8.0) + '@hono/node-ws': 1.1.7(@hono/node-server@1.14.4(hono@4.8.0))(hono@4.8.0) + hono: 4.8.0 + invariant: 2.2.4 + rivetkit: https://pkg.pr.new/rivet-gg/rivetkit@65c3659(@hono/node-server@1.14.4(hono@4.8.0))(@hono/node-ws@1.1.7(@hono/node-server@1.14.4(hono@4.8.0))(hono@4.8.0))(eventsource@3.0.7)(ws@8.18.2) + zod: 3.25.67 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@rolldown/pluginutils@1.0.0-beta.11': {} '@rollup/pluginutils@5.2.0(rollup@4.44.0)': @@ -4867,6 +5092,18 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@simplewebauthn/browser@13.1.0': {} + + '@simplewebauthn/server@13.1.1': + dependencies: + '@hexagon/base64': 1.1.28 + '@levischuck/tiny-cbor': 0.2.11 + '@peculiar/asn1-android': 2.3.16 + '@peculiar/asn1-ecc': 2.3.15 + '@peculiar/asn1-rsa': 2.3.15 + '@peculiar/asn1-schema': 2.3.15 + '@peculiar/asn1-x509': 2.3.15 + '@sinclair/typebox@0.34.35': optional: true @@ -4926,10 +5163,6 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 22.15.32 - '@types/bun@1.2.17': - dependencies: - bun-types: 1.2.17 - '@types/chai@5.2.2': dependencies: '@types/deep-eql': 4.0.2 @@ -5056,6 +5289,14 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 + '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@22.15.32)(tsx@3.14.0)(yaml@2.8.0))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 6.3.5(@types/node@22.15.32)(tsx@3.14.0)(yaml@2.8.0) + '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.0.3)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.4 @@ -5215,6 +5456,12 @@ snapshots: dependencies: printable-characters: 1.0.42 + asn1js@3.0.6: + dependencies: + pvtsutils: 1.3.6 + pvutils: 1.1.3 + tslib: 2.8.1 + assertion-error@2.0.1: {} asynckit@0.4.0: {} @@ -5223,6 +5470,28 @@ snapshots: base64-js@1.5.1: {} + better-auth@1.2.10: + dependencies: + '@better-auth/utils': 0.2.5 + '@better-fetch/fetch': 1.1.18 + '@noble/ciphers': 0.6.0 + '@noble/hashes': 1.8.0 + '@simplewebauthn/browser': 13.1.0 + '@simplewebauthn/server': 13.1.1 + better-call: 1.0.9 + defu: 6.1.4 + jose: 5.10.0 + kysely: 0.28.2 + nanostores: 0.11.4 + zod: 3.25.67 + + better-call@1.0.9: + dependencies: + '@better-fetch/fetch': 1.1.18 + rou3: 0.5.1 + set-cookie-parser: 2.7.1 + uncrypto: 0.1.3 + birpc@0.2.14: {} blake3-wasm@2.1.5: {} @@ -5268,10 +5537,6 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - bun-types@1.2.17: - dependencies: - '@types/node': 22.15.32 - bundle-require@5.1.0(esbuild@0.25.5): dependencies: esbuild: 0.25.5 @@ -5925,6 +6190,8 @@ snapshots: jju@1.4.0: {} + jose@5.10.0: {} + joycon@3.1.1: {} js-base64@3.7.7: {} @@ -5949,6 +6216,8 @@ snapshots: kolorist@1.8.0: {} + kysely@0.28.2: {} + lefthook-darwin-arm64@1.11.14: optional: true @@ -6134,6 +6403,8 @@ snapshots: nanoid@3.3.11: {} + nanostores@0.11.4: {} + negotiator@1.0.0: {} node-domexception@1.0.0: {} @@ -6271,6 +6542,12 @@ snapshots: punycode@2.3.1: {} + pvtsutils@1.3.6: + dependencies: + tslib: 2.8.1 + + pvutils@1.1.3: {} + qs@6.14.0: dependencies: side-channel: 1.1.0 @@ -6341,6 +6618,21 @@ snapshots: reusify@1.1.0: {} + rivetkit@https://pkg.pr.new/rivet-gg/rivetkit@65c3659(@hono/node-server@1.14.4(hono@4.8.0))(@hono/node-ws@1.1.7(@hono/node-server@1.14.4(hono@4.8.0))(hono@4.8.0))(eventsource@3.0.7)(ws@8.18.2): + dependencies: + '@hono/zod-openapi': 0.19.8(hono@4.8.0)(zod@3.25.67) + cbor-x: 1.6.0 + hono: 4.8.0 + invariant: 2.2.4 + on-change: 5.0.1 + p-retry: 6.2.1 + zod: 3.25.67 + optionalDependencies: + '@hono/node-server': 1.14.4(hono@4.8.0) + '@hono/node-ws': 1.1.7(@hono/node-server@1.14.4(hono@4.8.0))(hono@4.8.0) + eventsource: 3.0.7 + ws: 8.18.2 + rollup-plugin-inject@3.0.2: dependencies: estree-walker: 0.6.1 @@ -6381,6 +6673,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.44.0 fsevents: 2.3.3 + rou3@0.5.1: {} + router@2.2.0: dependencies: debug: 4.4.1 @@ -6442,6 +6736,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-cookie-parser@2.7.1: {} + setprototypeof@1.2.0: {} sharp@0.33.5: @@ -6790,6 +7086,8 @@ snapshots: uint8array-extras@1.4.0: {} + uncrypto@0.1.3: {} + undici-types@5.26.5: {} undici-types@6.21.0: {} @@ -7017,7 +7315,7 @@ snapshots: dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.0.3)(tsx@4.20.3)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.15.32)(tsx@3.14.0)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4