From df8194d2f86b5fdb33b03426b624f8851c211465 Mon Sep 17 00:00:00 2001 From: "rivet-docs-sync[bot]" Date: Thu, 24 Sep 2026 12:39:02 +0000 Subject: [PATCH] docs(docs): sync from rivet-dev/rivet@f1c460d --- vendor/docs/docs/content/architecture.mdx | 50 ++++-- vendor/docs/docs/content/authentication.mdx | 22 +++ vendor/docs/docs/content/jwt.mdx | 155 ++++++++++++++++++ vendor/docs/docs/content/quickstart.mdx | 56 +++++++ vendor/docs/docs/sidebar.json | 133 +++++++++------ .../docs/engine/artifacts/config-schema.json | 5 + .../docs/actors-access-control/chat-room.ts | 83 ---------- .../actors-authentication/caching-tokens.ts | 61 ------- .../create-conn-state.ts | 55 ------- .../external-auth-provider.ts | 37 ----- .../docs/actors-authentication/jwt.ts | 52 ------ .../on-before-connect.ts | 34 ---- .../actors-authentication/rate-limiting.ts | 45 ----- .../role-based-access-control.ts | 52 ------ .../docs/actors-authentication/using-state.ts | 23 --- .../docs/actors-permissions/caching-tokens.ts | 63 +++++++ .../docs/actors-permissions/chat-room.ts | 81 +++++++++ .../actors-permissions/create-conn-state.ts | 60 +++++++ .../external-auth-provider.ts | 42 +++++ .../handling-errors-connection.ts | 20 +-- .../handling-errors-stateless.ts | 25 +-- .../examples/docs/actors-permissions/jwt.ts | 55 +++++++ .../actors-permissions/on-before-connect.ts | 37 +++++ .../passing-credentials-connection.ts | 8 +- .../passing-credentials-headers.ts | 6 +- .../passing-credentials-stateless.ts | 2 +- .../actors-permissions/quickstart/client.ts | 20 +++ .../actors-permissions/quickstart/index.ts | 44 +++++ .../docs/actors-permissions/rate-limiting.ts | 47 ++++++ .../role-based-access-control.ts | 61 +++++++ .../docs/actors-permissions/using-state.ts | 25 +++ .../docs/examples/docs/general-jwt/grants.ts | 36 ++++ .../docs/general-jwt/quickstart/client.ts | 27 +++ .../docs/general-jwt/quickstart/registry.ts | 17 ++ .../docs/general-jwt/quickstart/server.ts | 35 ++++ 35 files changed, 1039 insertions(+), 535 deletions(-) create mode 100644 vendor/docs/docs/content/authentication.mdx create mode 100644 vendor/docs/docs/content/jwt.mdx create mode 100644 vendor/docs/docs/content/quickstart.mdx delete mode 100644 vendor/docs/examples/docs/actors-access-control/chat-room.ts delete mode 100644 vendor/docs/examples/docs/actors-authentication/caching-tokens.ts delete mode 100644 vendor/docs/examples/docs/actors-authentication/create-conn-state.ts delete mode 100644 vendor/docs/examples/docs/actors-authentication/external-auth-provider.ts delete mode 100644 vendor/docs/examples/docs/actors-authentication/jwt.ts delete mode 100644 vendor/docs/examples/docs/actors-authentication/on-before-connect.ts delete mode 100644 vendor/docs/examples/docs/actors-authentication/rate-limiting.ts delete mode 100644 vendor/docs/examples/docs/actors-authentication/role-based-access-control.ts delete mode 100644 vendor/docs/examples/docs/actors-authentication/using-state.ts create mode 100644 vendor/docs/examples/docs/actors-permissions/caching-tokens.ts create mode 100644 vendor/docs/examples/docs/actors-permissions/chat-room.ts create mode 100644 vendor/docs/examples/docs/actors-permissions/create-conn-state.ts create mode 100644 vendor/docs/examples/docs/actors-permissions/external-auth-provider.ts rename vendor/docs/examples/docs/{actors-authentication => actors-permissions}/handling-errors-connection.ts (61%) rename vendor/docs/examples/docs/{actors-authentication => actors-permissions}/handling-errors-stateless.ts (51%) create mode 100644 vendor/docs/examples/docs/actors-permissions/jwt.ts create mode 100644 vendor/docs/examples/docs/actors-permissions/on-before-connect.ts rename vendor/docs/examples/docs/{actors-authentication => actors-permissions}/passing-credentials-connection.ts (75%) rename vendor/docs/examples/docs/{actors-authentication => actors-permissions}/passing-credentials-headers.ts (84%) rename vendor/docs/examples/docs/{actors-authentication => actors-permissions}/passing-credentials-stateless.ts (86%) create mode 100644 vendor/docs/examples/docs/actors-permissions/quickstart/client.ts create mode 100644 vendor/docs/examples/docs/actors-permissions/quickstart/index.ts create mode 100644 vendor/docs/examples/docs/actors-permissions/rate-limiting.ts create mode 100644 vendor/docs/examples/docs/actors-permissions/role-based-access-control.ts create mode 100644 vendor/docs/examples/docs/actors-permissions/using-state.ts create mode 100644 vendor/docs/examples/docs/general-jwt/grants.ts create mode 100644 vendor/docs/examples/docs/general-jwt/quickstart/client.ts create mode 100644 vendor/docs/examples/docs/general-jwt/quickstart/registry.ts create mode 100644 vendor/docs/examples/docs/general-jwt/quickstart/server.ts diff --git a/vendor/docs/docs/content/architecture.mdx b/vendor/docs/docs/content/architecture.mdx index 951cbfa5..38b87e07 100644 --- a/vendor/docs/docs/content/architecture.mdx +++ b/vendor/docs/docs/content/architecture.mdx @@ -8,22 +8,40 @@ Rivet separates the process that decides *where* work runs from the process that *runs* it. Everything else in these docs is a detail of one of those two sides, or of the boundary between them. -``` - ┌──────────────────────────────────┐ - your client ────▶│ Gateway │ - │ routes to a specific workload │ - ├──────────────────────────────────┤ - │ Control plane │ - │ schedules, versions, persists │ - └───────────────┬──────────────────┘ - │ runner protocol - ┌───────────────┴──────────────────┐ - │ Runner (your process, your code)│ - │ ┌────────┐ ┌────────┐ │ - │ │ actor │ │ actor │ ... │ - │ └────────┘ └────────┘ │ - └──────────────────────────────────┘ -``` + + + + + + + Your client + + + + Gateway + routes to a specific workload + + Control plane + schedules, versions, persists + + + runner protocol + + + Runner + your process, your code + + + + + + + actor + actor + actor + + … + ## Control plane diff --git a/vendor/docs/docs/content/authentication.mdx b/vendor/docs/docs/content/authentication.mdx new file mode 100644 index 00000000..4907f0ea --- /dev/null +++ b/vendor/docs/docs/content/authentication.mdx @@ -0,0 +1,22 @@ +--- +title: "Authentication" +description: "How clients authenticate to Rivet: secrets for your backend, short-lived JWTs for public clients, and ACL tokens for long-lived scoped access." +skill: true +--- + +Every request to Rivet carries a credential, and the control plane checks what that credential may reach before routing it to an actor. This page covers the kinds available. To authorize a caller after it is connected, see [Permissions](/actors/docs/permissions). + +Authentication is required by default. On a self-hosted control plane, `auth.insecure_allow_unauthenticated` restores the old unauthenticated behavior for a trusted network; do not enable it on anything publicly reachable. + +## Choosing a Credential + +| | Lifetime | Scope | Revoke | Available in | +| --- | --- | --- | --- | --- | +| [Admin token](/docs/deploy/self-host/control-plane/configuration#admin-token) | Static | Everything, every namespace | Rotate the secret and restart | Self-hosted | +| [Secret key](/docs/deploy/cloud#tokens) | Static | One namespace | Delete the key | Rivet Cloud | +| [JWT](/docs/jwt) | 1 hour by default, 24 hour maximum | One namespace, one grant set | Wait for expiry | Self-hosted, Rivet Cloud | +| [ACL token](/docs/deploy/self-host/control-plane/rbac) | Long-lived | Roles built from reusable policies | Delete the token | Enterprise | + +Your backend can hold a secret, so it uses one directly. A browser or mobile app cannot, because anyone can read it out of the bundle, so mint a short-lived [JWT](/docs/jwt) per user on your backend and hand that out instead. Reach for [ACL](/docs/deploy/self-host/control-plane/rbac) when a credential has to outlive a session and stay revocable. + +On Rivet Cloud the secret, publishable, and connection keys are created from the dashboard or the CLI. See [Tokens](/docs/deploy/cloud#tokens). diff --git a/vendor/docs/docs/content/jwt.mdx b/vendor/docs/docs/content/jwt.mdx new file mode 100644 index 00000000..ae1580ba --- /dev/null +++ b/vendor/docs/docs/content/jwt.mdx @@ -0,0 +1,155 @@ +--- +title: "JWTs" +seoTitle: "Scoped JWT Authentication for Rivet" +description: "Mint short-lived, grant-scoped JWTs on your backend so a browser can reach one Rivet Actor without holding a secret, and renew them automatically from RivetKit." +skill: true +--- + +A Rivet JWT is a short-lived credential you mint on your backend and hand to an untrusted client. It carries grants saying exactly what the holder may reach, and the control plane verifies it before routing any request. + +Use one whenever a browser or mobile app talks to Rivet directly. For which credential to use where, see [Authentication](/docs/authentication). + + + + + + + + + + + + + + Browser + Your backend + Rivet control plane + User Actor + + + + + + + + + POST /login + issueToken() + grant: actor_gateway read on user:alice + JWT, expires in 1h + session + JWT + connect with JWT + verify + check grants + actions & events + + + + + + + + + + + + + + +## Quickstart + + + + + +Authenticate the user however you already do, then call `issueToken` on the actor they own. It defaults to gateway read access scoped to that one actor, so there is nothing to spell out. Return only the token to the client. + + + + + + + + + + +Pass `getToken` to `createClient`. RivetKit calls it whenever it needs a credential, caches the result, and calls it again when the token expires. + + + + + + + +Point the same token at a different actor. The control plane rejects it before your code runs: + +```sh +curl -i -H "Authorization: Bearer $TOKEN" \ + "$RIVET_ENDPOINT/gateway/$SOME_OTHER_ACTOR_ID/" +``` + +``` +HTTP/1.1 403 Forbidden +x-rivet-error: auth.insufficient_permissions +``` + + + + + +Issuing needs a credential that already holds the grants being handed out, so it only works from a server-side client. `expiresIn` is in seconds and defaults to the control plane's lifetime, capped at 24 hours. `subject` is your user identifier, opaque to Rivet, and shows up in token inspection. `issuedAt` and `expiresAt` come back as Unix milliseconds. + +## Grants + +`actor.issueToken` covers the common case. Pass `permissions` to widen what the holder may do to that actor; every grant stays scoped to its resolved ID. For namespace-wide operations such as creating actors, use `client.auth.issueToken` with an explicit grant list, which adds nothing on its own. + +| Resource | Gates | +| --- | --- | +| `actor_gateway` | Connecting to an actor by ID: actions, events, and raw HTTP or WebSocket handlers. | +| `actor` | Resolving or creating actors by key, and the actors API. | +| `actor_kv` | Reading an actor's raw KV, which the [inspector](/actors/docs/debugging) needs. | +| `namespace`, `runner`, `runner_config`, `datacenter` | Control-plane management APIs. | + +A grant is a resource, a `target` of `"any"` or `{ id }`, and operations drawn from `create`, `read`, `update`, `delete`, and `list`. A grant set is capped at 32 grants, 5 operations each, and 2048 bytes encoded. + + + +### Resolving Versus Connecting + +These are different grants, and confusing them is the most common mistake. `getOrCreate(key)` and `get(key)` route through the query path, which checks `actor` with `create` and `read`. Connecting to an actor you already have the ID for checks `actor_gateway` with `read` on that ID. + +That distinction is what makes tight scoping possible. A token holding only `actor_gateway` `read` on one ID can reach that actor and nothing else. Resolve the ID on your backend, where you still hold the admin token, then grant against it. + +## Expiration and Renewal + +Expiry is enforced mid-flight, not just at connection time. When a token's `exp` passes, the control plane cancels in-flight requests and closes open WebSockets with code `1008` and `auth.token_expired`. + +With `getToken` wired up this is invisible: RivetKit catches the close, calls `getToken({ forceRefresh: true })`, and reconnects. With a static `token` the connection dies and does not come back, so **always use `getToken` for connections that outlive the token**. + +There is no revocation, so a leaked token is valid until it expires. Keep durations short and let renewal do the work. + +## Inspecting a Token + +`GET /auth/tokens/inspect`, called with the token itself, returns the namespace, subject, grants, and timestamps it actually carries. Use it when a request is rejected and you want to see what the holder was granted. + +## Errors + +| Error | Status | Cause | +| --- | --- | --- | +| `auth.invalid_token` | 401 | Malformed, unsigned, or signed by a key this cluster does not know. | +| `auth.token_expired` | 401 | Past `exp`. Refresh and retry. | +| `auth.insufficient_permissions` | 403 | Valid token, but no grant covers this resource, target, and operation. | +| `auth.issuance_disabled` | 400 | `auth.jwt.issuance_enabled` is off, so issuance will not mint. | + +Over HTTP the code arrives in the JSON body and, through the gateway, in the `x-rivet-error` response header. On a WebSocket it arrives in the close reason. + +## Configuration + +Issuance and verification are on by default when a self-hosted control plane has an [admin token](/docs/deploy/self-host/control-plane/configuration#admin-token). Tokens are signed with `EdDSA`, the issuer is derived from the leader datacenter's public URL, and signing keys rotate every seven days with no operator action. Tune `auth.jwt.default_duration`, `auth.jwt.max_duration`, and `auth.jwt.audience` if the defaults do not suit you. + +## Not Your Application's JWTs + +This page is about credentials Rivet issues and verifies. Tokens from Clerk, Auth0, Supabase, or your own issuer are never seen by Rivet. Pass them as connection parameters and verify them inside the actor. See [Permissions](/actors/docs/permissions). + +The two compose: a Rivet JWT decides which actor a client may reach, and your own token decides who the user is once they are there. + +[`examples/jwt-counter`](https://github.com/rivet-dev/rivet/tree/main/examples/jwt-counter) is a runnable backend, client, and smoke test covering issuance, scoped access, and renewal. diff --git a/vendor/docs/docs/content/quickstart.mdx b/vendor/docs/docs/content/quickstart.mdx new file mode 100644 index 00000000..19883459 --- /dev/null +++ b/vendor/docs/docs/content/quickstart.mdx @@ -0,0 +1,56 @@ +--- +title: "Quickstart" +description: "Pick the Actor type you are building and follow its quickstart. Every one runs on the same control plane." +--- + +Every Actor type on this site is built on the same primitive, so the control plane, deployment, and authentication work the same way whichever you start with. Pick the one closest to what you are building. + +## Actors + +The primitive itself. Start here if you are building your own durable processes, and pick the quickstart matching your stack. + + + + A standalone backend with the RivetKit SDK. + + + A frontend talking to Actors over the browser client. + + + Route handlers and server components. + + + The Rust SDK. + + + Actors inside an Effect runtime. + + + Serverless workers on Cloudflare. + + + Serverless workers on Supabase. + + + +## Higher-level Actor types + +Each of these is an Actor with a job already built into it, so you write less to get started. + + + + Multi-step operations that replay instead of starting over. + + + A filesystem, shell, and network for code you did not write. + + + A backend per user, deployed the moment it is generated. + + + +## Next steps + +- [Architecture](/docs/architecture) for the vocabulary the rest of these docs use. +- [Authentication](/docs/authentication) before you expose anything to a browser. +- [Deploy](/docs/deploy) to run the control plane and workers yourself, or on Rivet Cloud. diff --git a/vendor/docs/docs/sidebar.json b/vendor/docs/docs/sidebar.json index 96d8ebb3..37bd4c6f 100644 --- a/vendor/docs/docs/sidebar.json +++ b/vendor/docs/docs/sidebar.json @@ -9,67 +9,117 @@ "icon": "faSquareInfo" }, { - "title": "Architecture", - "href": "/docs/architecture" + "title": "Quickstart", + "href": "/docs/quickstart", + "icon": "faFastForward" } ] }, { - "title": "Running Workloads", + "title": "Concepts", "pages": [ { - "title": "Runtime Modes", - "href": "/docs/runtime-modes" - }, - { - "title": "Endpoints", - "href": "/docs/endpoints" - }, - { - "title": "Environment Variables", - "href": "/docs/environment-variables" - }, - { - "title": "Pool Configuration", - "href": "/docs/pool-configuration" - }, - { - "title": "Versions & Upgrades", - "href": "/docs/versions" + "title": "Architecture", + "href": "/docs/architecture" }, { - "title": "Regions & Multi-Region", - "href": "/docs/regions" + "title": "Runtime Modes", + "href": "/docs/runtime-modes" }, { - "title": "Container Runner", - "href": "/docs/container-runner" + "title": "Authentication", + "collapsible": true, + "pages": [ + { + "title": "Overview", + "href": "/docs/authentication" + }, + { + "title": "JWTs", + "href": "/docs/jwt" + }, + { + "title": "ACL", + "href": "/docs/deploy/self-host/control-plane/rbac", + "badge": "Enterprise" + } + ] } ] }, { - "title": "Operating", + "title": "Reference", "pages": [ { - "title": "Statuses", - "href": "/docs/statuses" + "title": "Configuration", + "collapsible": true, + "pages": [ + { + "title": "Connect", + "href": "/docs/endpoints" + }, + { + "title": "Environment Variables", + "href": "/docs/environment-variables" + }, + { + "title": "Pool Configuration", + "href": "/docs/pool-configuration" + }, + { + "title": "Versions & Upgrades", + "href": "/docs/versions" + }, + { + "title": "Regions & Multi-Region", + "href": "/docs/regions" + } + ] + }, + { + "title": "Monitoring", + "collapsible": true, + "pages": [ + { + "title": "OpenTelemetry", + "href": "/docs/tracing" + }, + { + "title": "Debugging", + "href": "/docs/debugging" + }, + { + "title": "Actor Statuses", + "href": "/docs/statuses" + } + ] }, { - "title": "Tracing", - "href": "/docs/tracing" + "title": "Advanced", + "collapsible": true, + "pages": [ + { + "title": "Container Runner", + "href": "/docs/container-runner" + } + ] }, { - "title": "Debugging", - "href": "/docs/debugging" + "title": "CLI", + "href": "/docs/cli" } ] }, { - "title": "Reference", + "title": "AI Integration", "pages": [ { - "title": "CLI", - "href": "/docs/cli" + "title": "Rivet MCP", + "href": "/docs/mcp" + }, + { + "title": "Skills", + "href": "/docs/skill" }, { "title": "API Reference", @@ -88,19 +138,6 @@ ] } ] - }, - { - "title": "AI Integration", - "pages": [ - { - "title": "Rivet MCP", - "href": "/docs/mcp" - }, - { - "title": "Skill File", - "href": "/docs/skill" - } - ] } ] } diff --git a/vendor/docs/engine/artifacts/config-schema.json b/vendor/docs/engine/artifacts/config-schema.json index 584443ab..f478f387 100644 --- a/vendor/docs/engine/artifacts/config-schema.json +++ b/vendor/docs/engine/artifacts/config-schema.json @@ -237,6 +237,11 @@ "admin_token": { "$ref": "#/definitions/Secret" }, + "insecure_allow_unauthenticated": { + "description": "Restores legacy unauthenticated access behavior for actor and routing endpoints.", + "default": false, + "type": "boolean" + }, "jwt": { "description": "Short-lived, namespace-scoped JWT authentication. Enabled by default.", "default": { diff --git a/vendor/docs/examples/docs/actors-access-control/chat-room.ts b/vendor/docs/examples/docs/actors-access-control/chat-room.ts deleted file mode 100644 index 9ae778af..00000000 --- a/vendor/docs/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/docs/examples/docs/actors-authentication/caching-tokens.ts b/vendor/docs/examples/docs/actors-authentication/caching-tokens.ts deleted file mode 100644 index 501d7f54..00000000 --- a/vendor/docs/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/docs/examples/docs/actors-authentication/create-conn-state.ts b/vendor/docs/examples/docs/actors-authentication/create-conn-state.ts deleted file mode 100644 index edb02561..00000000 --- a/vendor/docs/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/docs/examples/docs/actors-authentication/external-auth-provider.ts b/vendor/docs/examples/docs/actors-authentication/external-auth-provider.ts deleted file mode 100644 index 65b22f6d..00000000 --- a/vendor/docs/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/docs/examples/docs/actors-authentication/jwt.ts b/vendor/docs/examples/docs/actors-authentication/jwt.ts deleted file mode 100644 index d15028ab..00000000 --- a/vendor/docs/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/docs/examples/docs/actors-authentication/on-before-connect.ts b/vendor/docs/examples/docs/actors-authentication/on-before-connect.ts deleted file mode 100644 index df835ce0..00000000 --- a/vendor/docs/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/docs/examples/docs/actors-authentication/rate-limiting.ts b/vendor/docs/examples/docs/actors-authentication/rate-limiting.ts deleted file mode 100644 index d0cda637..00000000 --- a/vendor/docs/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/docs/examples/docs/actors-authentication/role-based-access-control.ts b/vendor/docs/examples/docs/actors-authentication/role-based-access-control.ts deleted file mode 100644 index 969ad121..00000000 --- a/vendor/docs/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/docs/examples/docs/actors-authentication/using-state.ts b/vendor/docs/examples/docs/actors-authentication/using-state.ts deleted file mode 100644 index 3bc7a777..00000000 --- a/vendor/docs/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/docs/examples/docs/actors-permissions/caching-tokens.ts b/vendor/docs/examples/docs/actors-permissions/caching-tokens.ts new file mode 100644 index 00000000..cca3e07a --- /dev/null +++ b/vendor/docs/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/docs/examples/docs/actors-permissions/chat-room.ts b/vendor/docs/examples/docs/actors-permissions/chat-room.ts new file mode 100644 index 00000000..ac949e52 --- /dev/null +++ b/vendor/docs/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/docs/examples/docs/actors-permissions/create-conn-state.ts b/vendor/docs/examples/docs/actors-permissions/create-conn-state.ts new file mode 100644 index 00000000..8d35f8b8 --- /dev/null +++ b/vendor/docs/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/docs/examples/docs/actors-permissions/external-auth-provider.ts b/vendor/docs/examples/docs/actors-permissions/external-auth-provider.ts new file mode 100644 index 00000000..29deaa45 --- /dev/null +++ b/vendor/docs/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/docs/examples/docs/actors-authentication/handling-errors-connection.ts b/vendor/docs/examples/docs/actors-permissions/handling-errors-connection.ts similarity index 61% rename from vendor/docs/examples/docs/actors-authentication/handling-errors-connection.ts rename to vendor/docs/examples/docs/actors-permissions/handling-errors-connection.ts index 80b33413..df4b67a3 100644 --- a/vendor/docs/examples/docs/actors-authentication/handling-errors-connection.ts +++ b/vendor/docs/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/docs/examples/docs/actors-authentication/handling-errors-stateless.ts b/vendor/docs/examples/docs/actors-permissions/handling-errors-stateless.ts similarity index 51% rename from vendor/docs/examples/docs/actors-authentication/handling-errors-stateless.ts rename to vendor/docs/examples/docs/actors-permissions/handling-errors-stateless.ts index 0a0e2525..c4d21c1b 100644 --- a/vendor/docs/examples/docs/actors-authentication/handling-errors-stateless.ts +++ b/vendor/docs/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/docs/examples/docs/actors-permissions/jwt.ts b/vendor/docs/examples/docs/actors-permissions/jwt.ts new file mode 100644 index 00000000..c602dc37 --- /dev/null +++ b/vendor/docs/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/docs/examples/docs/actors-permissions/on-before-connect.ts b/vendor/docs/examples/docs/actors-permissions/on-before-connect.ts new file mode 100644 index 00000000..1ab851a9 --- /dev/null +++ b/vendor/docs/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/docs/examples/docs/actors-authentication/passing-credentials-connection.ts b/vendor/docs/examples/docs/actors-permissions/passing-credentials-connection.ts similarity index 75% rename from vendor/docs/examples/docs/actors-authentication/passing-credentials-connection.ts rename to vendor/docs/examples/docs/actors-permissions/passing-credentials-connection.ts index 80d2c9b4..9a8a8eec 100644 --- a/vendor/docs/examples/docs/actors-authentication/passing-credentials-connection.ts +++ b/vendor/docs/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/docs/examples/docs/actors-authentication/passing-credentials-headers.ts b/vendor/docs/examples/docs/actors-permissions/passing-credentials-headers.ts similarity index 84% rename from vendor/docs/examples/docs/actors-authentication/passing-credentials-headers.ts rename to vendor/docs/examples/docs/actors-permissions/passing-credentials-headers.ts index 2da6a5f6..d0f4e1d9 100644 --- a/vendor/docs/examples/docs/actors-authentication/passing-credentials-headers.ts +++ b/vendor/docs/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/docs/examples/docs/actors-authentication/passing-credentials-stateless.ts b/vendor/docs/examples/docs/actors-permissions/passing-credentials-stateless.ts similarity index 86% rename from vendor/docs/examples/docs/actors-authentication/passing-credentials-stateless.ts rename to vendor/docs/examples/docs/actors-permissions/passing-credentials-stateless.ts index 57615ef3..da16920a 100644 --- a/vendor/docs/examples/docs/actors-authentication/passing-credentials-stateless.ts +++ b/vendor/docs/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/docs/examples/docs/actors-permissions/quickstart/client.ts b/vendor/docs/examples/docs/actors-permissions/quickstart/client.ts new file mode 100644 index 00000000..89db2da8 --- /dev/null +++ b/vendor/docs/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/docs/examples/docs/actors-permissions/quickstart/index.ts b/vendor/docs/examples/docs/actors-permissions/quickstart/index.ts new file mode 100644 index 00000000..a9109048 --- /dev/null +++ b/vendor/docs/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/docs/examples/docs/actors-permissions/rate-limiting.ts b/vendor/docs/examples/docs/actors-permissions/rate-limiting.ts new file mode 100644 index 00000000..3bdba330 --- /dev/null +++ b/vendor/docs/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/docs/examples/docs/actors-permissions/role-based-access-control.ts b/vendor/docs/examples/docs/actors-permissions/role-based-access-control.ts new file mode 100644 index 00000000..648e4942 --- /dev/null +++ b/vendor/docs/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/docs/examples/docs/actors-permissions/using-state.ts b/vendor/docs/examples/docs/actors-permissions/using-state.ts new file mode 100644 index 00000000..721bb3b1 --- /dev/null +++ b/vendor/docs/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/docs/examples/docs/general-jwt/grants.ts b/vendor/docs/examples/docs/general-jwt/grants.ts new file mode 100644 index 00000000..bb456075 --- /dev/null +++ b/vendor/docs/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/docs/examples/docs/general-jwt/quickstart/client.ts b/vendor/docs/examples/docs/general-jwt/quickstart/client.ts new file mode 100644 index 00000000..e3fb8ff1 --- /dev/null +++ b/vendor/docs/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/docs/examples/docs/general-jwt/quickstart/registry.ts b/vendor/docs/examples/docs/general-jwt/quickstart/registry.ts new file mode 100644 index 00000000..b48ae124 --- /dev/null +++ b/vendor/docs/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/docs/examples/docs/general-jwt/quickstart/server.ts b/vendor/docs/examples/docs/general-jwt/quickstart/server.ts new file mode 100644 index 00000000..86bfefdf --- /dev/null +++ b/vendor/docs/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;