diff --git a/vendor/integrations/examples/docs/actors-access-control/chat-room.ts b/vendor/integrations/examples/docs/actors-access-control/chat-room.ts deleted file mode 100644 index 9ae778af..00000000 --- a/vendor/integrations/examples/docs/actors-access-control/chat-room.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { actor, event, queue, UserError } from "rivetkit"; - -type ConnParams = { - authToken: string; -}; - -type ConnState = { - userId: string; - role: "member" | "admin"; -}; - -async function authenticate( - authToken: string, -): Promise { - if (authToken === "admin-token") { - return { userId: "admin-1", role: "admin" }; - } - if (authToken === "member-token") { - return { userId: "member-1", role: "member" }; - } - return null; -} - -export const chatRoom = actor({ - state: { messages: [] as Array<{ userId: string; text: string }> }, - - onBeforeConnect: async (_c, params: ConnParams) => { - if (!params.authToken) { - throw new UserError("Forbidden", { code: "forbidden" }); - } - - const session = await authenticate(params.authToken); - if (!session) { - throw new UserError("Forbidden", { code: "forbidden" }); - } - }, - - createConnState: async (_c, params: ConnParams): Promise => { - const session = await authenticate(params.authToken); - if (!session) { - throw new UserError("Forbidden", { code: "forbidden" }); - } - return session; - }, - - events: { - messages: event<{ userId: string; text: string }>(), - moderationLog: event<{ entry: string }>({ - canSubscribe: (c) => { - if (c.conn?.state.role === "admin") { - return true; - } - return false; - }, - }), - }, - - queues: { - moderationJobs: queue<{ action: "ban"; userId: string }>({ - canPublish: (c) => { - if (c.conn?.state.role === "admin") { - return true; - } - return false; - }, - }), - }, - - actions: { - sendMessage: (c, text: string) => { - const role = c.conn?.state.role; - const userId = c.conn?.state.userId; - - if (!userId || (role !== "member" && role !== "admin")) { - throw new UserError("Forbidden", { code: "forbidden" }); - } - - const message = { userId, text }; - c.state.messages.push(message); - c.broadcast("messages", message); - }, - }, -}); diff --git a/vendor/integrations/examples/docs/actors-authentication/caching-tokens.ts b/vendor/integrations/examples/docs/actors-authentication/caching-tokens.ts deleted file mode 100644 index 501d7f54..00000000 --- a/vendor/integrations/examples/docs/actors-authentication/caching-tokens.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { actor, UserError } from "rivetkit"; - -interface ConnParams { - authToken: string; -} - -interface ConnState { - userId: string; - role: string; -} - -interface TokenCache { - [token: string]: { - userId: string; - role: string; - expiresAt: number; - }; -} - -// Example token validation function -async function validateToken(token: string): Promise<{ sub: string; role: string } | null> { - // In production, verify JWT or call auth service - if (token.length > 0) { - return { sub: "user-123", role: "member" }; - } - return null; -} - -const cachedAuthActor = actor({ - state: {}, - createVars: () => ({ tokenCache: {} as TokenCache }), - - createConnState: async (c, params: ConnParams): Promise => { - const token = params.authToken; - - // Check cache first - const cached = c.vars.tokenCache[token]; - if (cached && cached.expiresAt > Date.now()) { - return { userId: cached.userId, role: cached.role }; - } - - // Validate token (expensive operation) - const payload = await validateToken(token); - if (!payload) { - throw new UserError("Invalid token", { code: "invalid_token" }); - } - - // Cache the result - c.vars.tokenCache[token] = { - userId: payload.sub, - role: payload.role, - expiresAt: Date.now() + 5 * 60 * 1000, // 5 minutes - }; - - return { userId: payload.sub, role: payload.role }; - }, - - actions: { - getData: (c) => ({ userId: c.conn.state.userId }), - }, -}); diff --git a/vendor/integrations/examples/docs/actors-authentication/create-conn-state.ts b/vendor/integrations/examples/docs/actors-authentication/create-conn-state.ts deleted file mode 100644 index edb02561..00000000 --- a/vendor/integrations/examples/docs/actors-authentication/create-conn-state.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { actor, UserError } from "rivetkit"; - -interface ConnParams { - authToken: string; -} - -interface ConnState { - userId: string; - role: string; -} - -interface Message { - userId: string; - text: string; - timestamp: number; -} - -// Example token validation function -async function validateToken(token: string, roomKey: string[]): Promise<{ sub: string; role: string } | null> { - // In production, verify JWT or call auth service - if (token.length > 0 && roomKey.length > 0) { - return { sub: "user-123", role: "member" }; - } - return null; -} - -const chatRoom = actor({ - state: { messages: [] as Message[] }, - - createConnState: async (c, params: ConnParams): Promise => { - const roomName = c.key; - const payload = await validateToken(params.authToken, roomName); - if (!payload) { - throw new UserError("Forbidden", { code: "forbidden" }); - } - return { - userId: payload.sub, - role: payload.role, - }; - }, - - actions: { - sendMessage: (c, text: string) => { - // Access user data via c.conn.state - const { userId, role } = c.conn.state; - - if (role !== "member") { - throw new UserError("Insufficient permissions", { code: "insufficient_permissions" }); - } - - c.state.messages.push({ userId, text, timestamp: Date.now() }); - c.broadcast("newMessage", { userId, text }); - }, - }, -}); diff --git a/vendor/integrations/examples/docs/actors-authentication/external-auth-provider.ts b/vendor/integrations/examples/docs/actors-authentication/external-auth-provider.ts deleted file mode 100644 index 65b22f6d..00000000 --- a/vendor/integrations/examples/docs/actors-authentication/external-auth-provider.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { actor, UserError } from "rivetkit"; - -interface ConnParams { - apiKey: string; -} - -interface ConnState { - userId: string; - tier: string; -} - -const apiActor = actor({ - state: {}, - - createConnState: async (c, params: ConnParams): Promise => { - const response = await fetch(`https://api.my-auth-provider.com/validate`, { - method: "POST", - headers: { "X-API-Key": params.apiKey }, - }); - - if (!response.ok) { - throw new UserError("Invalid API key", { code: "invalid_api_key" }); - } - - const data = await response.json(); - return { userId: data.id, tier: data.tier }; - }, - - actions: { - premiumAction: (c) => { - if (c.conn.state.tier !== "premium") { - throw new UserError("Premium subscription required", { code: "forbidden" }); - } - return "Premium content"; - }, - }, -}); diff --git a/vendor/integrations/examples/docs/actors-authentication/jwt.ts b/vendor/integrations/examples/docs/actors-authentication/jwt.ts deleted file mode 100644 index d15028ab..00000000 --- a/vendor/integrations/examples/docs/actors-authentication/jwt.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { actor, UserError } from "rivetkit"; - -interface ConnParams { - token: string; -} - -interface ConnState { - userId: string; - role: string; - permissions: string[]; -} - -interface JwtPayload { - sub: string; - role: string; - permissions?: string[]; -} - -// Example JWT verification function - in production use a JWT library -function verifyJwt(token: string, secret: string): JwtPayload { - // This is a simplified example - use jsonwebtoken or similar in production - const parts = token.split("."); - if (parts.length !== 3) throw new Error("Invalid token"); - const payload = JSON.parse(atob(parts[1])) as JwtPayload; - return payload; -} - -const jwtActor = actor({ - state: {}, - - createConnState: (c, params: ConnParams): ConnState => { - try { - const payload = verifyJwt(params.token, process.env.JWT_SECRET || "secret"); - return { - userId: payload.sub, - role: payload.role, - permissions: payload.permissions || [], - }; - } catch { - throw new UserError("Invalid or expired token", { code: "invalid_token" }); - } - }, - - actions: { - protectedAction: (c) => { - if (!c.conn.state.permissions.includes("write")) { - throw new UserError("Write permission required", { code: "forbidden" }); - } - return { success: true }; - }, - }, -}); diff --git a/vendor/integrations/examples/docs/actors-authentication/on-before-connect.ts b/vendor/integrations/examples/docs/actors-authentication/on-before-connect.ts deleted file mode 100644 index df835ce0..00000000 --- a/vendor/integrations/examples/docs/actors-authentication/on-before-connect.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { actor, UserError } from "rivetkit"; - -interface ConnParams { - authToken: string; -} - -// Example token validation function -async function validateToken(token: string, roomKey: string[]): Promise { - // In production, verify JWT or call auth service - return token.length > 0 && roomKey.length > 0; -} - -interface Message { - text: string; - timestamp: number; -} - -const chatRoom = actor({ - state: { messages: [] as Message[] }, - - onBeforeConnect: async (c, params: ConnParams) => { - const roomName = c.key; - const isValid = await validateToken(params.authToken, roomName); - if (!isValid) { - throw new UserError("Forbidden", { code: "forbidden" }); - } - }, - - actions: { - sendMessage: (c, text: string) => { - c.state.messages.push({ text, timestamp: Date.now() }); - }, - }, -}); diff --git a/vendor/integrations/examples/docs/actors-authentication/rate-limiting.ts b/vendor/integrations/examples/docs/actors-authentication/rate-limiting.ts deleted file mode 100644 index d0cda637..00000000 --- a/vendor/integrations/examples/docs/actors-authentication/rate-limiting.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { actor, UserError } from "rivetkit"; - -interface ConnParams { - authToken: string; -} - -interface RateLimitEntry { - count: number; - resetAt: number; -} - -// Example token validation function -async function validateToken(token: string): Promise<{ userId: string }> { - // In production, verify JWT or call auth service - return { userId: "user-123" }; -} - -const rateLimitedActor = actor({ - state: {}, - createVars: () => ({ rateLimits: {} as Record }), - - onBeforeConnect: async (c, params: ConnParams) => { - // Extract user ID - const { userId } = await validateToken(params.authToken); - - // Check rate limit - const now = Date.now(); - const limit = c.vars.rateLimits[userId]; - - if (limit && limit.resetAt > now && limit.count >= 10) { - throw new UserError("Too many requests, try again later", { code: "rate_limited" }); - } - - // Update rate limit - if (!limit || limit.resetAt <= now) { - c.vars.rateLimits[userId] = { count: 1, resetAt: now + 60_000 }; - } else { - limit.count++; - } - }, - - actions: { - getData: (c) => ({ success: true }), - }, -}); diff --git a/vendor/integrations/examples/docs/actors-authentication/role-based-access-control.ts b/vendor/integrations/examples/docs/actors-authentication/role-based-access-control.ts deleted file mode 100644 index 969ad121..00000000 --- a/vendor/integrations/examples/docs/actors-authentication/role-based-access-control.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { actor, UserError } from "rivetkit"; - -const ROLE_HIERARCHY = { user: 1, moderator: 2, admin: 3 }; - -interface ConnState { - role: keyof typeof ROLE_HIERARCHY; - permissions: string[]; -} - -// Example token validation function -async function validateToken(token: string): Promise<{ role: keyof typeof ROLE_HIERARCHY; permissions: string[] }> { - // In production, verify JWT or call auth service - return { role: "user", permissions: ["read", "edit_posts"] }; -} - -function requireRole(requiredRole: keyof typeof ROLE_HIERARCHY) { - return (c: { conn: { state: ConnState } }) => { - const userRole = c.conn.state.role; - if (ROLE_HIERARCHY[userRole] < ROLE_HIERARCHY[requiredRole]) { - throw new UserError(`${requiredRole} role required`, { code: "forbidden" }); - } - }; -} - -function requirePermission(permission: string) { - return (c: { conn: { state: ConnState } }) => { - if (!c.conn.state.permissions?.includes(permission)) { - throw new UserError(`Permission '${permission}' required`, { code: "forbidden" }); - } - }; -} - -const forumActor = actor({ - state: {}, - - createConnState: async (c, params: { token: string }): Promise => { - const user = await validateToken(params.token); - return { role: user.role, permissions: user.permissions }; - }, - - actions: { - deletePost: (c, postId: string) => { - requireRole("moderator")(c); - // Delete post... - }, - - editPost: (c, postId: string, content: string) => { - requirePermission("edit_posts")(c); - // Edit post... - }, - }, -}); diff --git a/vendor/integrations/examples/docs/actors-authentication/using-state.ts b/vendor/integrations/examples/docs/actors-authentication/using-state.ts deleted file mode 100644 index 3bc7a777..00000000 --- a/vendor/integrations/examples/docs/actors-authentication/using-state.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { actor, UserError } from "rivetkit"; - -interface ConnParams { - userId?: string; -} - -const userProfile = actor({ - state: { - ownerId: "user-123", - isPrivate: true, - }, - - onBeforeConnect: (c, params: ConnParams) => { - // Use actor state to check access permissions - if (c.state.isPrivate && params.userId !== c.state.ownerId) { - throw new UserError("Access denied to private profile", { code: "forbidden" }); - } - }, - - actions: { - getProfile: (c) => ({ ownerId: c.state.ownerId }), - }, -}); diff --git a/vendor/integrations/examples/docs/actors-permissions/caching-tokens.ts b/vendor/integrations/examples/docs/actors-permissions/caching-tokens.ts new file mode 100644 index 00000000..cca3e07a --- /dev/null +++ b/vendor/integrations/examples/docs/actors-permissions/caching-tokens.ts @@ -0,0 +1,63 @@ +import { actor, UserError } from "rivetkit"; + +interface ConnParams { + authToken: string; +} + +interface ConnState { + userId: string; + role: string; +} + +interface TokenCache { + [token: string]: { + userId: string; + role: string; + expiresAt: number; + }; +} + +// Example token validation function +async function validateToken( + token: string, +): Promise<{ sub: string; role: string } | null> { + // In production, verify JWT or call auth service + if (token.length > 0) { + return { sub: "user-123", role: "member" }; + } + return null; +} + +const cachedAuthActor = actor({ + state: {}, + createVars: () => ({ tokenCache: {} as TokenCache }), + + createConnState: async (c, params: ConnParams): Promise => { + const token = params.authToken; + + // Check cache first + const cached = c.vars.tokenCache[token]; + if (cached && cached.expiresAt > Date.now()) { + return { userId: cached.userId, role: cached.role }; + } + + // Validate token (expensive operation) + const payload = await validateToken(token); + if (!payload) { + throw new UserError("Invalid token", { code: "invalid_token" }); + } + + // Cache the result + c.vars.tokenCache[token] = { + userId: payload.sub, + role: payload.role, + expiresAt: Date.now() + 5 * 60 * 1000, // 5 minutes + }; + + return { userId: payload.sub, role: payload.role }; + }, + + actions: { + getData: (c) => ({ userId: c.conn.state.userId }), + }, +}); diff --git a/vendor/integrations/examples/docs/actors-permissions/chat-room.ts b/vendor/integrations/examples/docs/actors-permissions/chat-room.ts new file mode 100644 index 00000000..ac949e52 --- /dev/null +++ b/vendor/integrations/examples/docs/actors-permissions/chat-room.ts @@ -0,0 +1,81 @@ +import { actor, event, queue, UserError } from "rivetkit"; + +type ConnParams = { + authToken: string; +}; + +type ConnState = { + userId: string; + role: "member" | "admin"; +}; + +async function authenticate(authToken: string): Promise { + if (authToken === "admin-token") { + return { userId: "admin-1", role: "admin" }; + } + if (authToken === "member-token") { + return { userId: "member-1", role: "member" }; + } + return null; +} + +export const chatRoom = actor({ + state: { messages: [] as Array<{ userId: string; text: string }> }, + + onBeforeConnect: async (_c, params: ConnParams) => { + if (!params.authToken) { + throw new UserError("Forbidden", { code: "forbidden" }); + } + + const session = await authenticate(params.authToken); + if (!session) { + throw new UserError("Forbidden", { code: "forbidden" }); + } + }, + + createConnState: async (_c, params: ConnParams): Promise => { + const session = await authenticate(params.authToken); + if (!session) { + throw new UserError("Forbidden", { code: "forbidden" }); + } + return session; + }, + + events: { + messages: event<{ userId: string; text: string }>(), + moderationLog: event<{ entry: string }>({ + canSubscribe: (c) => { + if (c.conn?.state.role === "admin") { + return true; + } + return false; + }, + }), + }, + + queues: { + moderationJobs: queue<{ action: "ban"; userId: string }>({ + canPublish: (c) => { + if (c.conn?.state.role === "admin") { + return true; + } + return false; + }, + }), + }, + + actions: { + sendMessage: (c, text: string) => { + const role = c.conn?.state.role; + const userId = c.conn?.state.userId; + + if (!userId || (role !== "member" && role !== "admin")) { + throw new UserError("Forbidden", { code: "forbidden" }); + } + + const message = { userId, text }; + c.state.messages.push(message); + c.broadcast("messages", message); + }, + }, +}); diff --git a/vendor/integrations/examples/docs/actors-permissions/create-conn-state.ts b/vendor/integrations/examples/docs/actors-permissions/create-conn-state.ts new file mode 100644 index 00000000..8d35f8b8 --- /dev/null +++ b/vendor/integrations/examples/docs/actors-permissions/create-conn-state.ts @@ -0,0 +1,60 @@ +import { actor, UserError } from "rivetkit"; + +interface ConnParams { + authToken: string; +} + +interface ConnState { + userId: string; + role: string; +} + +interface Message { + userId: string; + text: string; + timestamp: number; +} + +// Example token validation function +async function validateToken( + token: string, + roomKey: string[], +): Promise<{ sub: string; role: string } | null> { + // In production, verify JWT or call auth service + if (token.length > 0 && roomKey.length > 0) { + return { sub: "user-123", role: "member" }; + } + return null; +} + +const chatRoom = actor({ + state: { messages: [] as Message[] }, + + createConnState: async (c, params: ConnParams): Promise => { + const roomName = c.key; + const payload = await validateToken(params.authToken, roomName); + if (!payload) { + throw new UserError("Forbidden", { code: "forbidden" }); + } + return { + userId: payload.sub, + role: payload.role, + }; + }, + + actions: { + sendMessage: (c, text: string) => { + // Access user data via c.conn.state + const { userId, role } = c.conn.state; + + if (role !== "member") { + throw new UserError("Insufficient permissions", { + code: "insufficient_permissions", + }); + } + + c.state.messages.push({ userId, text, timestamp: Date.now() }); + c.broadcast("newMessage", { userId, text }); + }, + }, +}); diff --git a/vendor/integrations/examples/docs/actors-permissions/external-auth-provider.ts b/vendor/integrations/examples/docs/actors-permissions/external-auth-provider.ts new file mode 100644 index 00000000..29deaa45 --- /dev/null +++ b/vendor/integrations/examples/docs/actors-permissions/external-auth-provider.ts @@ -0,0 +1,42 @@ +import { actor, UserError } from "rivetkit"; + +interface ConnParams { + apiKey: string; +} + +interface ConnState { + userId: string; + tier: string; +} + +const apiActor = actor({ + state: {}, + + createConnState: async (c, params: ConnParams): Promise => { + const response = await fetch( + `https://api.my-auth-provider.com/validate`, + { + method: "POST", + headers: { "X-API-Key": params.apiKey }, + }, + ); + + if (!response.ok) { + throw new UserError("Invalid API key", { code: "invalid_api_key" }); + } + + const data = await response.json(); + return { userId: data.id, tier: data.tier }; + }, + + actions: { + premiumAction: (c) => { + if (c.conn.state.tier !== "premium") { + throw new UserError("Premium subscription required", { + code: "forbidden", + }); + } + return "Premium content"; + }, + }, +}); diff --git a/vendor/integrations/examples/docs/actors-authentication/handling-errors-connection.ts b/vendor/integrations/examples/docs/actors-permissions/handling-errors-connection.ts similarity index 61% rename from vendor/integrations/examples/docs/actors-authentication/handling-errors-connection.ts rename to vendor/integrations/examples/docs/actors-permissions/handling-errors-connection.ts index 80b33413..df4b67a3 100644 --- a/vendor/integrations/examples/docs/actors-authentication/handling-errors-connection.ts +++ b/vendor/integrations/examples/docs/actors-permissions/handling-errors-connection.ts @@ -3,10 +3,10 @@ import { ActorError, createClient } from "rivetkit/client"; // Define actor with protected action const myActor = actor({ - state: {}, - actions: { - protectedAction: (c) => ({ success: true }) - } + state: {}, + actions: { + protectedAction: (c) => ({ success: true }), + }, }); const registry = setup({ use: { myActor } }); @@ -15,14 +15,14 @@ const actorHandle = await client.myActor.getOrCreate(); // Helper to show errors function showError(message: string) { - console.error(message); + console.error(message); } const conn = actorHandle.connect(); conn.onError((error: ActorError) => { - if (error.code === "forbidden") { - window.location.href = "/login"; - } else if (error.code === "insufficient_permissions") { - showError("You don't have permission for this action"); - } + if (error.code === "forbidden") { + window.location.href = "/login"; + } else if (error.code === "insufficient_permissions") { + showError("You don't have permission for this action"); + } }); diff --git a/vendor/integrations/examples/docs/actors-authentication/handling-errors-stateless.ts b/vendor/integrations/examples/docs/actors-permissions/handling-errors-stateless.ts similarity index 51% rename from vendor/integrations/examples/docs/actors-authentication/handling-errors-stateless.ts rename to vendor/integrations/examples/docs/actors-permissions/handling-errors-stateless.ts index 0a0e2525..c4d21c1b 100644 --- a/vendor/integrations/examples/docs/actors-authentication/handling-errors-stateless.ts +++ b/vendor/integrations/examples/docs/actors-permissions/handling-errors-stateless.ts @@ -3,10 +3,10 @@ import { ActorError, createClient } from "rivetkit/client"; // Define actor with protected action const myActor = actor({ - state: {}, - actions: { - protectedAction: (c) => ({ success: true }) - } + state: {}, + actions: { + protectedAction: (c) => ({ success: true }), + }, }); const registry = setup({ use: { myActor } }); @@ -15,15 +15,18 @@ const actorHandle = await client.myActor.getOrCreate(); // Helper to show errors function showError(message: string) { - console.error(message); + console.error(message); } try { - const result = await actorHandle.protectedAction(); + const result = await actorHandle.protectedAction(); } catch (error) { - if (error instanceof ActorError && error.code === "forbidden") { - window.location.href = "/login"; - } else if (error instanceof ActorError && error.code === "insufficient_permissions") { - showError("You don't have permission for this action"); - } + if (error instanceof ActorError && error.code === "forbidden") { + window.location.href = "/login"; + } else if ( + error instanceof ActorError && + error.code === "insufficient_permissions" + ) { + showError("You don't have permission for this action"); + } } diff --git a/vendor/integrations/examples/docs/actors-permissions/jwt.ts b/vendor/integrations/examples/docs/actors-permissions/jwt.ts new file mode 100644 index 00000000..c602dc37 --- /dev/null +++ b/vendor/integrations/examples/docs/actors-permissions/jwt.ts @@ -0,0 +1,55 @@ +import { actor, UserError } from "rivetkit"; + +interface ConnParams { + token: string; +} + +interface ConnState { + userId: string; + role: string; + permissions: string[]; +} + +interface JwtPayload { + sub: string; + role: string; + permissions?: string[]; +} + +// Supply this from your auth provider's SDK or a JWT library such as `jose`. +// It must verify the signature and check the issuer, audience, and expiry. +// Decoding the payload without verifying the signature authenticates nobody: +// any client can forge a token. +declare function verifyAccessToken(token: string): Promise; + +const jwtActor = actor({ + state: {}, + + createConnState: async (c, params: ConnParams): Promise => { + let payload: JwtPayload; + try { + payload = await verifyAccessToken(params.token); + } catch { + throw new UserError("Invalid or expired token", { + code: "invalid_token", + }); + } + + return { + userId: payload.sub, + role: payload.role, + permissions: payload.permissions ?? [], + }; + }, + + actions: { + protectedAction: (c) => { + if (!c.conn.state.permissions.includes("write")) { + throw new UserError("Write permission required", { + code: "forbidden", + }); + } + return { success: true }; + }, + }, +}); diff --git a/vendor/integrations/examples/docs/actors-permissions/on-before-connect.ts b/vendor/integrations/examples/docs/actors-permissions/on-before-connect.ts new file mode 100644 index 00000000..1ab851a9 --- /dev/null +++ b/vendor/integrations/examples/docs/actors-permissions/on-before-connect.ts @@ -0,0 +1,37 @@ +import { actor, UserError } from "rivetkit"; + +interface ConnParams { + authToken: string; +} + +// Example token validation function +async function validateToken( + token: string, + roomKey: string[], +): Promise { + // In production, verify JWT or call auth service + return token.length > 0 && roomKey.length > 0; +} + +interface Message { + text: string; + timestamp: number; +} + +const chatRoom = actor({ + state: { messages: [] as Message[] }, + + onBeforeConnect: async (c, params: ConnParams) => { + const roomName = c.key; + const isValid = await validateToken(params.authToken, roomName); + if (!isValid) { + throw new UserError("Forbidden", { code: "forbidden" }); + } + }, + + actions: { + sendMessage: (c, text: string) => { + c.state.messages.push({ text, timestamp: Date.now() }); + }, + }, +}); diff --git a/vendor/integrations/examples/docs/actors-authentication/passing-credentials-connection.ts b/vendor/integrations/examples/docs/actors-permissions/passing-credentials-connection.ts similarity index 75% rename from vendor/integrations/examples/docs/actors-authentication/passing-credentials-connection.ts rename to vendor/integrations/examples/docs/actors-permissions/passing-credentials-connection.ts index 80d2c9b4..9a8a8eec 100644 --- a/vendor/integrations/examples/docs/actors-authentication/passing-credentials-connection.ts +++ b/vendor/integrations/examples/docs/actors-permissions/passing-credentials-connection.ts @@ -1,14 +1,14 @@ import { createClient } from "rivetkit/client"; async function getAuthToken(): Promise { - return "jwt-token-here"; + return "jwt-token-here"; } const client = createClient(); const chat = client.chatRoom.getOrCreate(["general"], { - getParams: async () => ({ - authToken: await getAuthToken(), - }), + getParams: async () => ({ + authToken: await getAuthToken(), + }), }); // Authentication will happen on connect by reading connection parameters diff --git a/vendor/integrations/examples/docs/actors-authentication/passing-credentials-headers.ts b/vendor/integrations/examples/docs/actors-permissions/passing-credentials-headers.ts similarity index 84% rename from vendor/integrations/examples/docs/actors-authentication/passing-credentials-headers.ts rename to vendor/integrations/examples/docs/actors-permissions/passing-credentials-headers.ts index 2da6a5f6..d0f4e1d9 100644 --- a/vendor/integrations/examples/docs/actors-authentication/passing-credentials-headers.ts +++ b/vendor/integrations/examples/docs/actors-permissions/passing-credentials-headers.ts @@ -2,9 +2,9 @@ import { createClient } from "rivetkit/client"; // This only works for stateless actions, not WebSockets const client = createClient({ - headers: { - Authorization: "Bearer my-token", - }, + headers: { + Authorization: "Bearer my-token", + }, }); const chat = client.chatRoom.getOrCreate(["general"]); diff --git a/vendor/integrations/examples/docs/actors-authentication/passing-credentials-stateless.ts b/vendor/integrations/examples/docs/actors-permissions/passing-credentials-stateless.ts similarity index 86% rename from vendor/integrations/examples/docs/actors-authentication/passing-credentials-stateless.ts rename to vendor/integrations/examples/docs/actors-permissions/passing-credentials-stateless.ts index 57615ef3..da16920a 100644 --- a/vendor/integrations/examples/docs/actors-authentication/passing-credentials-stateless.ts +++ b/vendor/integrations/examples/docs/actors-permissions/passing-credentials-stateless.ts @@ -2,7 +2,7 @@ import { createClient } from "rivetkit/client"; const client = createClient(); const chat = client.chatRoom.getOrCreate(["general"], { - params: { authToken: "jwt-token-here" }, + params: { authToken: "jwt-token-here" }, }); // Authentication will happen when calling the action by reading input diff --git a/vendor/integrations/examples/docs/actors-permissions/quickstart/client.ts b/vendor/integrations/examples/docs/actors-permissions/quickstart/client.ts new file mode 100644 index 00000000..89db2da8 --- /dev/null +++ b/vendor/integrations/examples/docs/actors-permissions/quickstart/client.ts @@ -0,0 +1,20 @@ +import { createClient } from "rivetkit/client"; +import type { registry } from "./index"; + +const client = createClient(); + +const doc = client.document.getOrCreate(["welcome"], { + params: { authToken: "member-token" }, +}); + +const conn = doc.connect(); + +// Allowed: every authenticated caller may read. +console.log(await conn.read()); + +try { + // Rejected: this connection is a member, not an admin. + await conn.edit("hello"); +} catch (error) { + console.error(error); +} diff --git a/vendor/integrations/examples/docs/actors-permissions/quickstart/index.ts b/vendor/integrations/examples/docs/actors-permissions/quickstart/index.ts new file mode 100644 index 00000000..a9109048 --- /dev/null +++ b/vendor/integrations/examples/docs/actors-permissions/quickstart/index.ts @@ -0,0 +1,44 @@ +import { actor, setup, UserError } from "rivetkit"; + +interface ConnParams { + authToken: string; +} + +interface ConnState { + userId: string; + role: "member" | "admin"; +} + +// Replace this with your session store or auth provider. +async function verifySession(authToken: string): Promise { + if (authToken === "admin-token") return { userId: "u_1", role: "admin" }; + if (authToken === "member-token") return { userId: "u_2", role: "member" }; + return null; +} + +export const document = actor({ + state: { body: "" }, + + // 1. Identify the caller once, at connect time. + createConnState: async (_c, params: ConnParams): Promise => { + const session = await verifySession(params.authToken); + if (!session) { + throw new UserError("Invalid token", { code: "invalid_token" }); + } + return session; + }, + + actions: { + read: (c) => c.state.body, + + // 2. Gate the operation on the identity you established. + edit: (c, body: string) => { + if (c.conn.state.role !== "admin") { + throw new UserError("Admins only", { code: "forbidden" }); + } + c.state.body = body; + }, + }, +}); + +export const registry = setup({ use: { document } }); diff --git a/vendor/integrations/examples/docs/actors-permissions/rate-limiting.ts b/vendor/integrations/examples/docs/actors-permissions/rate-limiting.ts new file mode 100644 index 00000000..3bdba330 --- /dev/null +++ b/vendor/integrations/examples/docs/actors-permissions/rate-limiting.ts @@ -0,0 +1,47 @@ +import { actor, UserError } from "rivetkit"; + +interface ConnParams { + authToken: string; +} + +interface RateLimitEntry { + count: number; + resetAt: number; +} + +// Example token validation function +async function validateToken(token: string): Promise<{ userId: string }> { + // In production, verify JWT or call auth service + return { userId: "user-123" }; +} + +const rateLimitedActor = actor({ + state: {}, + createVars: () => ({ rateLimits: {} as Record }), + + onBeforeConnect: async (c, params: ConnParams) => { + // Extract user ID + const { userId } = await validateToken(params.authToken); + + // Check rate limit + const now = Date.now(); + const limit = c.vars.rateLimits[userId]; + + if (limit && limit.resetAt > now && limit.count >= 10) { + throw new UserError("Too many requests, try again later", { + code: "rate_limited", + }); + } + + // Update rate limit + if (!limit || limit.resetAt <= now) { + c.vars.rateLimits[userId] = { count: 1, resetAt: now + 60_000 }; + } else { + limit.count++; + } + }, + + actions: { + getData: (c) => ({ success: true }), + }, +}); diff --git a/vendor/integrations/examples/docs/actors-permissions/role-based-access-control.ts b/vendor/integrations/examples/docs/actors-permissions/role-based-access-control.ts new file mode 100644 index 00000000..648e4942 --- /dev/null +++ b/vendor/integrations/examples/docs/actors-permissions/role-based-access-control.ts @@ -0,0 +1,61 @@ +import { actor, UserError } from "rivetkit"; + +const ROLE_HIERARCHY = { user: 1, moderator: 2, admin: 3 }; + +interface ConnState { + role: keyof typeof ROLE_HIERARCHY; + permissions: string[]; +} + +// Example token validation function +async function validateToken( + token: string, +): Promise<{ role: keyof typeof ROLE_HIERARCHY; permissions: string[] }> { + // In production, verify JWT or call auth service + return { role: "user", permissions: ["read", "edit_posts"] }; +} + +function requireRole(requiredRole: keyof typeof ROLE_HIERARCHY) { + return (c: { conn: { state: ConnState } }) => { + const userRole = c.conn.state.role; + if (ROLE_HIERARCHY[userRole] < ROLE_HIERARCHY[requiredRole]) { + throw new UserError(`${requiredRole} role required`, { + code: "forbidden", + }); + } + }; +} + +function requirePermission(permission: string) { + return (c: { conn: { state: ConnState } }) => { + if (!c.conn.state.permissions?.includes(permission)) { + throw new UserError(`Permission '${permission}' required`, { + code: "forbidden", + }); + } + }; +} + +const forumActor = actor({ + state: {}, + + createConnState: async ( + c, + params: { token: string }, + ): Promise => { + const user = await validateToken(params.token); + return { role: user.role, permissions: user.permissions }; + }, + + actions: { + deletePost: (c, postId: string) => { + requireRole("moderator")(c); + // Delete post... + }, + + editPost: (c, postId: string, content: string) => { + requirePermission("edit_posts")(c); + // Edit post... + }, + }, +}); diff --git a/vendor/integrations/examples/docs/actors-permissions/using-state.ts b/vendor/integrations/examples/docs/actors-permissions/using-state.ts new file mode 100644 index 00000000..721bb3b1 --- /dev/null +++ b/vendor/integrations/examples/docs/actors-permissions/using-state.ts @@ -0,0 +1,25 @@ +import { actor, UserError } from "rivetkit"; + +interface ConnParams { + userId?: string; +} + +const userProfile = actor({ + state: { + ownerId: "user-123", + isPrivate: true, + }, + + onBeforeConnect: (c, params: ConnParams) => { + // Use actor state to check access permissions + if (c.state.isPrivate && params.userId !== c.state.ownerId) { + throw new UserError("Access denied to private profile", { + code: "forbidden", + }); + } + }, + + actions: { + getProfile: (c) => ({ ownerId: c.state.ownerId }), + }, +}); diff --git a/vendor/integrations/examples/docs/general-jwt/grants.ts b/vendor/integrations/examples/docs/general-jwt/grants.ts new file mode 100644 index 00000000..bb456075 --- /dev/null +++ b/vendor/integrations/examples/docs/general-jwt/grants.ts @@ -0,0 +1,36 @@ +import { createClient } from "rivetkit/client"; +import type { registry } from "./quickstart/registry"; + +const client = createClient(); +const userId = "user_alice"; +const profile = client.userProfile.getOrCreate(["user", userId]); + +// Reach exactly one actor. This is the default, so `permissions` can be +// omitted entirely. The holder cannot create actors or discover others. +export const oneActor = () => profile.issueToken({ subject: userId }); + +// Widen what the holder may do to that same actor. Every grant stays scoped +// to its resolved ID. +export const oneActorWithKv = () => + profile.issueToken({ + subject: userId, + permissions: { + actor_gateway: ["read"], + actor_kv: ["read"], + }, + }); + +// Namespace-wide operations such as creating actors need an explicit grant +// list. Nothing is added automatically. +export const anyActorInNamespace = () => + client.auth.issueToken({ + subject: userId, + grants: [ + { + resource: "actor", + target: "any", + operations: ["create", "read"], + }, + { resource: "actor_gateway", target: "any", operations: ["read"] }, + ], + }); diff --git a/vendor/integrations/examples/docs/general-jwt/quickstart/client.ts b/vendor/integrations/examples/docs/general-jwt/quickstart/client.ts new file mode 100644 index 00000000..e3fb8ff1 --- /dev/null +++ b/vendor/integrations/examples/docs/general-jwt/quickstart/client.ts @@ -0,0 +1,27 @@ +import { createClient } from "rivetkit/client"; +import type { registry } from "./registry"; + +// Calls your own backend, never the Rivet control plane directly. +async function fetchToken(): Promise<{ actorId: string; token: string }> { + const response = await fetch("/token", { + method: "POST", + cache: "no-store", + }); + if (!response.ok) throw new Error("could not get a Rivet token"); + return (await response.json()) as { actorId: string; token: string }; +} + +const { actorId } = await fetchToken(); + +const client = createClient({ + endpoint: "https://api.rivet.dev", + namespace: "production", + + // Called whenever RivetKit needs a credential, including after one expires. + getToken: async () => (await fetchToken()).token, +}); + +const profile = client.userProfile.getForId(actorId); +const conn = profile.connect(); + +await conn.recordVisit(); diff --git a/vendor/integrations/examples/docs/general-jwt/quickstart/registry.ts b/vendor/integrations/examples/docs/general-jwt/quickstart/registry.ts new file mode 100644 index 00000000..b48ae124 --- /dev/null +++ b/vendor/integrations/examples/docs/general-jwt/quickstart/registry.ts @@ -0,0 +1,17 @@ +import { actor, setup } from "rivetkit"; + +export const userProfile = actor({ + state: { displayName: "", visits: 0 }, + + actions: { + recordVisit: (c) => { + c.state.visits += 1; + return c.state.visits; + }, + setDisplayName: (c, displayName: string) => { + c.state.displayName = displayName; + }, + }, +}); + +export const registry = setup({ use: { userProfile } }); diff --git a/vendor/integrations/examples/docs/general-jwt/quickstart/server.ts b/vendor/integrations/examples/docs/general-jwt/quickstart/server.ts new file mode 100644 index 00000000..86bfefdf --- /dev/null +++ b/vendor/integrations/examples/docs/general-jwt/quickstart/server.ts @@ -0,0 +1,35 @@ +import { Hono } from "hono"; +import { createClient } from "rivetkit/client"; +import { registry } from "./registry"; + +// The issuing credential stays on the backend. It is never sent to a browser. +const client = createClient({ + endpoint: process.env.RIVET_ENDPOINT!, + namespace: process.env.RIVET_NAMESPACE!, + token: process.env.RIVET_ADMIN_TOKEN!, +}); + +// Replace this with your own session check. +async function authenticateUser(request: Request): Promise { + return request.headers.get("x-demo-user"); +} + +const app = new Hono(); + +app.post("/token", async (c) => { + const userId = await authenticateUser(c.req.raw); + if (!userId) return c.json({ error: "unauthorized" }, 401); + + // Scoped to this one actor. The default permission is gateway read. + const profile = client.userProfile.getOrCreate(["user", userId]); + const { token, expiresAt } = await profile.issueToken({ + subject: userId, + expiresIn: 900, + }); + + return c.json({ actorId: await profile.resolve(), token, expiresAt }, 200, { + "Cache-Control": "no-store", + }); +}); + +export default app;