Skip to content
This repository was archived by the owner on Oct 22, 2025. It is now read-only.

Commit b036bb9

Browse files
committed
chore: update serializable type validation to check for wider range of cbor types instead of json types (#1024)
1 parent 66c16d1 commit b036bb9

File tree

9 files changed

+171
-84
lines changed

9 files changed

+171
-84
lines changed
Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
import { betterAuth } from "better-auth";
2-
import { sqliteAdapter } from "@better-auth/sqlite";
3-
import Database from "better-sqlite3";
4-
5-
const db = new Database("./auth.db");
62

73
export const auth = betterAuth({
8-
// IMPORTANT: Connect your own database here
9-
database: sqliteAdapter(db),
4+
// IMPORTANT: Connect a real database for productoin use cases
5+
//
6+
// https://www.better-auth.com/docs/installation#create-database-tables
7+
// database: memoryAdapter({
8+
// user: [],
9+
// account: [],
10+
// session: [],
11+
// verifcation: [],
12+
// }),
13+
trustedOrigins: ["http://localhost:5173"],
1014
emailAndPassword: {
1115
enabled: true,
1216
},
13-
session: {
14-
expiresIn: 60 * 60 * 24 * 7, // 7 days
15-
updateAge: 60 * 60 * 24, // 1 day (every day the session expiry is updated)
16-
},
17-
plugins: [],
1817
});
19-
20-
export type Session = typeof auth.$Infer.Session;
21-
export type User = typeof auth.$Infer.User;

examples/better-auth/src/backend/registry.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,43 @@
1-
import { actor, setup } from "@rivetkit/actor";
2-
import { auth, type Session, type User } from "./auth";
1+
import { actor, OnAuthOptions, setup, UserError } from "@rivetkit/actor";
2+
import { auth } from "./auth";
3+
4+
interface State {
5+
messages: Message[];
6+
}
7+
8+
interface Message {
9+
id: string;
10+
userId: string;
11+
username: string;
12+
message: string;
13+
timestamp: number;
14+
}
315

416
export const chatRoom = actor({
5-
onAuth: async (c) => {
17+
onAuth: async (c: OnAuthOptions) => {
618
const authResult = await auth.api.getSession({
719
headers: c.req.headers,
820
});
21+
console.log("auth result", authResult);
922

1023
if (!authResult?.session || !authResult?.user) {
11-
throw new Error("Unauthorized");
24+
throw new UserError("Unauthorized");
1225
}
1326

1427
return {
15-
userId: authResult.user.id,
1628
user: authResult.user,
1729
session: authResult.session,
1830
};
1931
},
20-
state: {
21-
messages: [] as Array<{ id: string; userId: string; username: string; message: string; timestamp: number }>
22-
},
32+
state: {
33+
messages: [],
34+
} as State,
2335
actions: {
2436
sendMessage: (c, message: string) => {
2537
const newMessage = {
2638
id: crypto.randomUUID(),
27-
userId: c.auth.userId,
28-
username: c.auth.user.email,
39+
userId: "TODO",
40+
username: c.conn.auth.user.email || "Unknown",
2941
message,
3042
timestamp: Date.now(),
3143
};
@@ -44,4 +56,3 @@ export const chatRoom = actor({
4456
export const registry = setup({
4557
use: { chatRoom },
4658
});
47-
Lines changed: 16 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,27 @@
11
import { registry } from "./registry";
22
import { auth } from "./auth";
33
import { Hono } from "hono";
4-
import { serve } from "@hono/node-server";
4+
import { cors } from "hono/cors";
5+
6+
// Start RivetKit
7+
const { client, hono, serve } = registry.createServer();
58

69
// Setup router
710
const app = new Hono();
811

9-
// Start RivetKit
10-
const { client, hono } = registry.run({
11-
driver: createMemoryDriver(),
12-
cors: {
13-
// IMPORTANT: Configure origins in production
14-
origin: "*",
15-
},
16-
});
12+
app.use(
13+
"*",
14+
cors({
15+
origin: ["http://localhost:5173"],
16+
allowHeaders: ["Content-Type", "Authorization"],
17+
allowMethods: ["POST", "GET", "OPTIONS"],
18+
exposeHeaders: ["Content-Length"],
19+
maxAge: 600,
20+
credentials: true,
21+
}),
22+
);
1723

1824
// Mount Better Auth routes
1925
app.on(["GET", "POST"], "/api/auth/**", (c) => auth.handler(c.req.raw));
2026

21-
// Expose RivetKit to the frontend
22-
app.route("/registry", hono);
23-
24-
// Example HTTP endpoint to join chat room
25-
app.post("/api/join-room/:roomId", async (c) => {
26-
const roomId = c.req.param("roomId");
27-
28-
// Verify authentication
29-
const authResult = await auth.api.getSession({
30-
headers: c.req.header(),
31-
});
32-
33-
if (!authResult?.session || !authResult?.user) {
34-
return c.json({ error: "Unauthorized" }, 401);
35-
}
36-
37-
try {
38-
const room = client.chatRoom.getOrCreate(roomId);
39-
const messages = await room.getMessages();
40-
41-
return c.json({
42-
success: true,
43-
roomId,
44-
messages,
45-
user: authResult.user
46-
});
47-
} catch (error) {
48-
return c.json({ error: "Failed to join room" }, 500);
49-
}
50-
});
51-
52-
serve({ fetch: app.fetch, port: 8080 }, () =>
53-
console.log("Listening at http://localhost:8080"),
54-
);
27+
serve(app);

examples/better-auth/src/frontend/components/AuthForm.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ interface AuthFormProps {
88
export function AuthForm({ onAuthSuccess }: AuthFormProps) {
99
const [isLogin, setIsLogin] = useState(true);
1010
const [email, setEmail] = useState("");
11+
const [name, setName] = useState("");
1112
const [password, setPassword] = useState("");
1213
const [error, setError] = useState("");
1314
const [loading, setLoading] = useState(false);
@@ -26,6 +27,7 @@ export function AuthForm({ onAuthSuccess }: AuthFormProps) {
2627
} else {
2728
await authClient.signUp.email({
2829
email,
30+
name,
2931
password,
3032
});
3133
}
@@ -54,6 +56,20 @@ export function AuthForm({ onAuthSuccess }: AuthFormProps) {
5456
/>
5557
</div>
5658

59+
{!isLogin && (
60+
<div>
61+
<label htmlFor="name">Name:</label>
62+
<input
63+
id="name"
64+
type="text"
65+
value={name}
66+
onChange={(e) => setName(e.target.value)}
67+
required
68+
style={{ width: "100%", padding: "8px", marginTop: "5px" }}
69+
/>
70+
</div>
71+
)}
72+
5773
<div>
5874
<label htmlFor="password">Password:</label>
5975
<input

examples/better-auth/src/frontend/components/ChatRoom.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { useState, useEffect } from "react";
22
import { createClient, createRivetKit } from "@rivetkit/react";
33
import { authClient } from "../auth-client";
4-
import type { Registry } from "../../backend/registry";
4+
import type { registry } from "../../backend/registry";
55

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

88
const { useActor } = createRivetKit(client);
99

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

3131
// Listen for new messages
3232
chatRoom.useEvent("newMessage", (newMessage) => {
33-
setMessages(prev => [...prev, newMessage]);
33+
setMessages(prev => [...prev, newMessage as {
34+
id: string;
35+
userId: string;
36+
username: string;
37+
message: string;
38+
timestamp: number;
39+
}]);
3440
});
3541

3642
// Load initial messages when connected

packages/core/src/actor/errors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export class InvalidStateType extends ActorError {
182182
} else {
183183
msg += "Attempted to set invalid state.";
184184
}
185-
msg += " State must be JSON serializable.";
185+
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.";
186186
super("invalid_state_type", msg);
187187
}
188188
}

packages/core/src/actor/instance.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type * as wsToClient from "@/actor/protocol/message/to-client";
33
import type * as wsToServer from "@/actor/protocol/message/to-server";
44
import type { Client } from "@/client/client";
55
import type { Logger } from "@/common/log";
6-
import { isJsonSerializable, stringifyError } from "@/common/utils";
6+
import { isCborSerializable, stringifyError } from "@/common/utils";
77
import type { Registry } from "@/mod";
88
import invariant from "invariant";
99
import onChange from "on-change";
@@ -454,7 +454,7 @@ export class ActorInstance<S, CP, CS, V, I, AD, DB> {
454454
if (target === null || typeof target !== "object") {
455455
let invalidPath = "";
456456
if (
457-
!isJsonSerializable(
457+
!isCborSerializable(
458458
target,
459459
(path) => {
460460
invalidPath = path;
@@ -479,7 +479,7 @@ export class ActorInstance<S, CP, CS, V, I, AD, DB> {
479479
(path: string, value: any, _previousValue: any, _applyData: any) => {
480480
let invalidPath = "";
481481
if (
482-
!isJsonSerializable(
482+
!isCborSerializable(
483483
value,
484484
(invalidPathPart) => {
485485
invalidPath = invalidPathPart;

packages/core/src/common/utils.ts

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,13 @@ export function safeStringify(obj: unknown, maxSize: number) {
3737
// it. Roll back state if fails to serialize.
3838

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

68+
// Handle BigInt (CBOR tags 2 and 3)
69+
if (typeof value === "bigint") {
70+
return true;
71+
}
72+
73+
// Handle Date objects (CBOR tags 0 and 1)
74+
if (value instanceof Date) {
75+
return true;
76+
}
77+
78+
// Handle typed arrays (CBOR tags 64-82)
79+
if (
80+
value instanceof Uint8Array ||
81+
value instanceof Uint8ClampedArray ||
82+
value instanceof Uint16Array ||
83+
value instanceof Uint32Array ||
84+
value instanceof BigUint64Array ||
85+
value instanceof Int8Array ||
86+
value instanceof Int16Array ||
87+
value instanceof Int32Array ||
88+
value instanceof BigInt64Array ||
89+
value instanceof Float32Array ||
90+
value instanceof Float64Array
91+
) {
92+
return true;
93+
}
94+
95+
// Handle Map (CBOR tag 259)
96+
if (value instanceof Map) {
97+
for (const [key, val] of value.entries()) {
98+
const keyPath = currentPath ? `${currentPath}.key(${String(key)})` : `key(${String(key)})`;
99+
const valPath = currentPath ? `${currentPath}.value(${String(key)})` : `value(${String(key)})`;
100+
if (!isCborSerializable(key, onInvalid, keyPath) || !isCborSerializable(val, onInvalid, valPath)) {
101+
return false;
102+
}
103+
}
104+
return true;
105+
}
106+
107+
// Handle Set (CBOR tag 258)
108+
if (value instanceof Set) {
109+
let index = 0;
110+
for (const item of value.values()) {
111+
const itemPath = currentPath ? `${currentPath}.set[${index}]` : `set[${index}]`;
112+
if (!isCborSerializable(item, onInvalid, itemPath)) {
113+
return false;
114+
}
115+
index++;
116+
}
117+
return true;
118+
}
119+
120+
// Handle RegExp (CBOR tag 27)
121+
if (value instanceof RegExp) {
122+
return true;
123+
}
124+
125+
// Handle Error objects (CBOR tag 27)
126+
if (value instanceof Error) {
127+
return true;
128+
}
129+
65130
// Handle arrays
66131
if (Array.isArray(value)) {
67132
for (let i = 0; i < value.length; i++) {
68133
const itemPath = currentPath ? `${currentPath}[${i}]` : `[${i}]`;
69-
if (!isJsonSerializable(value[i], onInvalid, itemPath)) {
134+
if (!isCborSerializable(value[i], onInvalid, itemPath)) {
70135
return false;
71136
}
72137
}
73138
return true;
74139
}
75140

76-
// Handle plain objects
141+
// Handle plain objects and records (CBOR tags 105, 51, 57344-57599)
77142
if (typeof value === "object") {
78-
// Reject if it's not a plain object
79-
if (Object.getPrototypeOf(value) !== Object.prototype) {
80-
onInvalid?.(currentPath);
81-
return false;
143+
// Allow plain objects and objects with prototypes (for records and named objects)
144+
const proto = Object.getPrototypeOf(value);
145+
if (proto !== null && proto !== Object.prototype) {
146+
// Check if it's a known serializable object type
147+
const constructor = value.constructor;
148+
if (constructor && typeof constructor.name === "string") {
149+
// Allow objects with named constructors (records, named objects)
150+
// This includes user-defined classes and built-in objects
151+
// that CBOR can serialize with tag 27 or record tags
152+
}
82153
}
83154

84155
// Check all properties recursively
85156
for (const key in value) {
86157
const propPath = currentPath ? `${currentPath}.${key}` : key;
87158
if (
88-
!isJsonSerializable(
159+
!isCborSerializable(
89160
value[key as keyof typeof value],
90161
onInvalid,
91162
propPath,

0 commit comments

Comments
 (0)