Skip to content
This repository was archived by the owner on Oct 22, 2025. It is now read-only.
Closed
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
24 changes: 10 additions & 14 deletions examples/better-auth/src/backend/auth.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
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({
// IMPORTANT: Connect your own database here
database: sqliteAdapter(db),
// IMPORTANT: Connect a real database for productoin use cases
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a typo in the comment: productoin should be production

Suggested change
// IMPORTANT: Connect a real database for productoin use cases
// IMPORTANT: Connect a real database for production use cases

Spotted by Diamond

Is this helpful? React 👍 or 👎 to let us know.

//
// https://www.better-auth.com/docs/installation#create-database-tables
// database: memoryAdapter({
// user: [],
// account: [],
// session: [],
// verifcation: [],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a typo in the property name: verifcation is missing an 'i' and should be verification

Suggested change
// verifcation: [],
// verification: [],

Spotted by Diamond

Is this helpful? React 👍 or 👎 to let us know.

// }),
trustedOrigins: ["http://localhost:5173"],
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;
33 changes: 22 additions & 11 deletions examples/better-auth/src/backend/registry.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,43 @@
import { actor, setup } from "@rivetkit/actor";
import { auth, type Session, type User } from "./auth";
import { actor, OnAuthOptions, setup, UserError } from "@rivetkit/actor";
import { auth } from "./auth";

interface State {
messages: Message[];
}

interface Message {
id: string;
userId: string;
username: string;
message: string;
timestamp: number;
}

export const chatRoom = actor({
onAuth: async (c) => {
onAuth: async (c: OnAuthOptions) => {
const authResult = await auth.api.getSession({
headers: c.req.headers,
});
console.log("auth result", authResult);

if (!authResult?.session || !authResult?.user) {
throw new Error("Unauthorized");
throw new UserError("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 }>
},
state: {
messages: [],
} as State,
actions: {
sendMessage: (c, message: string) => {
const newMessage = {
id: crypto.randomUUID(),
userId: c.auth.userId,
username: c.auth.user.email,
userId: "TODO",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The userId field is currently hardcoded as "TODO", which will cause all messages to share the same user identifier. This should be replaced with the actual user ID from the authentication context, likely available at c.conn.auth.user.id. Using a static value here breaks user attribution in the chat system.

Suggested change
userId: "TODO",
userId: c.conn.auth.user.id,

Spotted by Diamond

Is this helpful? React 👍 or 👎 to let us know.

username: c.conn.auth.user.email || "Unknown",
message,
timestamp: Date.now(),
};
Expand All @@ -44,4 +56,3 @@ export const chatRoom = actor({
export const registry = setup({
use: { chatRoom },
});

59 changes: 16 additions & 43 deletions examples/better-auth/src/backend/server.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,27 @@
import { registry } from "./registry";
import { auth } from "./auth";
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { cors } from "hono/cors";

// Start RivetKit
const { client, hono, serve } = registry.createServer();

// Setup router
const app = new Hono();

// Start RivetKit
const { client, hono } = registry.run({
driver: createMemoryDriver(),
cors: {
// IMPORTANT: Configure origins in production
origin: "*",
},
});
app.use(
"*",
cors({
origin: ["http://localhost:5173"],
allowHeaders: ["Content-Type", "Authorization"],
allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length"],
maxAge: 600,
credentials: true,
}),
);

// 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: 8080 }, () =>
console.log("Listening at http://localhost:8080"),
);
serve(app);
16 changes: 16 additions & 0 deletions examples/better-auth/src/frontend/components/AuthForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface AuthFormProps {
export function AuthForm({ onAuthSuccess }: AuthFormProps) {
const [isLogin, setIsLogin] = useState(true);
const [email, setEmail] = useState("");
const [name, setName] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
Expand All @@ -26,6 +27,7 @@ export function AuthForm({ onAuthSuccess }: AuthFormProps) {
} else {
await authClient.signUp.email({
email,
name,
password,
});
}
Expand Down Expand Up @@ -54,6 +56,20 @@ export function AuthForm({ onAuthSuccess }: AuthFormProps) {
/>
</div>

{!isLogin && (
<div>
<label htmlFor="name">Name:</label>
<input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
required
style={{ width: "100%", padding: "8px", marginTop: "5px" }}
/>
</div>
)}

<div>
<label htmlFor="password">Password:</label>
<input
Expand Down
12 changes: 9 additions & 3 deletions examples/better-auth/src/frontend/components/ChatRoom.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useState, useEffect } from "react";
import { createClient, createRivetKit } from "@rivetkit/react";
import { authClient } from "../auth-client";
import type { Registry } from "../../backend/registry";
import type { registry } from "../../backend/registry";

const client = createClient<Registry>("http://localhost:8080/registry");
const client = createClient<typeof registry>("http://localhost:8080/registry");

const { useActor } = createRivetKit(client);

Expand All @@ -30,7 +30,13 @@ export function ChatRoom({ user, onSignOut }: ChatRoomProps) {

// Listen for new messages
chatRoom.useEvent("newMessage", (newMessage) => {
setMessages(prev => [...prev, newMessage]);
setMessages(prev => [...prev, newMessage as {
id: string;
userId: string;
username: string;
message: string;
timestamp: number;
}]);
});

// Load initial messages when connected
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/actor/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export class InvalidStateType extends ActorError {
} else {
msg += "Attempted to set invalid state.";
}
msg += " State must be JSON serializable.";
msg += " State must be CBOR serializable. Valid types include: null, undefined, boolean, string, number, BigInt, Date, RegExp, Error, typed arrays (Uint8Array, Int8Array, Float32Array, etc.), Map, Set, Array, and plain objects.";
super("invalid_state_type", msg);
}
}
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/actor/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type * as wsToClient from "@/actor/protocol/message/to-client";
import type * as wsToServer from "@/actor/protocol/message/to-server";
import type { Client } from "@/client/client";
import type { Logger } from "@/common/log";
import { isJsonSerializable, stringifyError } from "@/common/utils";
import { isCborSerializable, stringifyError } from "@/common/utils";
import type { Registry } from "@/mod";
import invariant from "invariant";
import onChange from "on-change";
Expand Down Expand Up @@ -454,7 +454,7 @@ export class ActorInstance<S, CP, CS, V, I, AD, DB> {
if (target === null || typeof target !== "object") {
let invalidPath = "";
if (
!isJsonSerializable(
!isCborSerializable(
target,
(path) => {
invalidPath = path;
Expand All @@ -479,7 +479,7 @@ export class ActorInstance<S, CP, CS, V, I, AD, DB> {
(path: string, value: any, _previousValue: any, _applyData: any) => {
let invalidPath = "";
if (
!isJsonSerializable(
!isCborSerializable(
value,
(invalidPathPart) => {
invalidPath = invalidPathPart;
Expand Down
89 changes: 80 additions & 9 deletions packages/core/src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,13 @@ export function safeStringify(obj: unknown, maxSize: number) {
// it. Roll back state if fails to serialize.

/**
* Check if a value is JSON serializable.
* Check if a value is CBOR serializable.
* Optionally pass an onInvalid callback to receive the path to invalid values.
*
* For a complete list of supported CBOR tags, see:
* https://github.com/kriszyp/cbor-x/blob/cc1cf9df8ba72288c7842af1dd374d73e34cdbc1/README.md#list-of-supported-tags-for-decoding
*/
export function isJsonSerializable(
export function isCborSerializable(
value: unknown,
onInvalid?: (path: string) => void,
currentPath = "",
Expand All @@ -62,30 +65,98 @@ export function isJsonSerializable(
return true;
}

// Handle BigInt (CBOR tags 2 and 3)
if (typeof value === "bigint") {
return true;
}

// Handle Date objects (CBOR tags 0 and 1)
if (value instanceof Date) {
return true;
}

// Handle typed arrays (CBOR tags 64-82)
if (
value instanceof Uint8Array ||
value instanceof Uint8ClampedArray ||
value instanceof Uint16Array ||
value instanceof Uint32Array ||
value instanceof BigUint64Array ||
value instanceof Int8Array ||
value instanceof Int16Array ||
value instanceof Int32Array ||
value instanceof BigInt64Array ||
value instanceof Float32Array ||
value instanceof Float64Array
) {
return true;
}

// Handle Map (CBOR tag 259)
if (value instanceof Map) {
for (const [key, val] of value.entries()) {
const keyPath = currentPath ? `${currentPath}.key(${String(key)})` : `key(${String(key)})`;
const valPath = currentPath ? `${currentPath}.value(${String(key)})` : `value(${String(key)})`;
if (!isCborSerializable(key, onInvalid, keyPath) || !isCborSerializable(val, onInvalid, valPath)) {
return false;
}
}
return true;
}

// Handle Set (CBOR tag 258)
if (value instanceof Set) {
let index = 0;
for (const item of value.values()) {
const itemPath = currentPath ? `${currentPath}.set[${index}]` : `set[${index}]`;
if (!isCborSerializable(item, onInvalid, itemPath)) {
return false;
}
index++;
}
return true;
}

// Handle RegExp (CBOR tag 27)
if (value instanceof RegExp) {
return true;
}

// Handle Error objects (CBOR tag 27)
if (value instanceof Error) {
return true;
}

// Handle arrays
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
const itemPath = currentPath ? `${currentPath}[${i}]` : `[${i}]`;
if (!isJsonSerializable(value[i], onInvalid, itemPath)) {
if (!isCborSerializable(value[i], onInvalid, itemPath)) {
return false;
}
}
return true;
}

// Handle plain objects
// Handle plain objects and records (CBOR tags 105, 51, 57344-57599)
if (typeof value === "object") {
// Reject if it's not a plain object
if (Object.getPrototypeOf(value) !== Object.prototype) {
onInvalid?.(currentPath);
return false;
// Allow plain objects and objects with prototypes (for records and named objects)
const proto = Object.getPrototypeOf(value);
if (proto !== null && proto !== Object.prototype) {
// Check if it's a known serializable object type
const constructor = value.constructor;
if (constructor && typeof constructor.name === "string") {
// Allow objects with named constructors (records, named objects)
// This includes user-defined classes and built-in objects
// that CBOR can serialize with tag 27 or record tags
}
Comment on lines +145 to +152
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation logic for objects with non-standard prototypes appears incomplete. While the code checks for the presence of a named constructor, it doesn't perform any actual validation to determine if these objects are CBOR serializable. The empty if block with only comments suggests an unfinished implementation.

This could potentially allow non-serializable objects to pass validation, leading to runtime errors when serialization is attempted. Consider either:

  1. Implementing proper validation for these object types
  2. Explicitly rejecting objects with non-standard prototypes
  3. Documenting which specific object types with custom prototypes are supported
// Example of a more complete implementation:
if (constructor && typeof constructor.name === "string") {
  // Check if it's one of the known serializable types
  if ([KnownType1, KnownType2].includes(constructor)) {
    return true;
  }
  onInvalid?.(currentPath);
  return false;
}
Suggested change
if (proto !== null && proto !== Object.prototype) {
// Check if it's a known serializable object type
const constructor = value.constructor;
if (constructor && typeof constructor.name === "string") {
// Allow objects with named constructors (records, named objects)
// This includes user-defined classes and built-in objects
// that CBOR can serialize with tag 27 or record tags
}
if (proto !== null && proto !== Object.prototype) {
// Check if it's a known serializable object type
const constructor = value.constructor;
if (constructor && typeof constructor.name === "string") {
// Allow objects with named constructors (records, named objects)
// This includes user-defined classes and built-in objects
// that CBOR can serialize with tag 27 or record tags
const knownSerializableTypes = [
Date,
Map,
Set,
RegExp
// Add other known serializable types here
];
if (knownSerializableTypes.some(type => constructor === type)) {
return true;
}
onInvalid?.(currentPath);
return false;
}

Spotted by Diamond

Is this helpful? React 👍 or 👎 to let us know.

}

// Check all properties recursively
for (const key in value) {
const propPath = currentPath ? `${currentPath}.${key}` : key;
if (
!isJsonSerializable(
!isCborSerializable(
value[key as keyof typeof value],
onInvalid,
propPath,
Expand Down
Loading
Loading