diff --git a/examples/better-auth-external-db/src/backend/registry.ts b/examples/better-auth-external-db/src/backend/registry.ts index b9f69b532..4109e8a90 100644 --- a/examples/better-auth-external-db/src/backend/registry.ts +++ b/examples/better-auth-external-db/src/backend/registry.ts @@ -16,10 +16,10 @@ interface Message { export const chatRoom = actor({ // onAuth runs on the server & before connecting to the actor - onAuth: async (c: OnAuthOptions) => { + onAuth: async (opts: OnAuthOptions) => { // Access Better Auth session const authResult = await auth.api.getSession({ - headers: c.request.headers, + headers: opts.request.headers, }); if (!authResult) throw new Unauthorized(); diff --git a/examples/tenant/README.md b/examples/tenant/README.md index 47ee403ef..1189dfc8d 100644 --- a/examples/tenant/README.md +++ b/examples/tenant/README.md @@ -70,29 +70,6 @@ This tenant system demonstrates: - **Dashboard Stats**: Access to basic member statistics only - **No Invoice Access**: Cannot view or manage billing information -## Security Features - -### Authentication -```typescript -// Token-based authentication -createConnState: async (c, { params }) => { - const token = params.token; - const { userId, role } = await authenticate(token); - return { userId, role }; -} -``` - -### Authorization -```typescript -// Server-side permission checks -getInvoices: (c) => { - if (c.conn.role !== "admin") { - throw new UserError("Permission denied: Admin role required"); - } - return c.state.invoices; -} -``` - ### Data Isolation - Organization-scoped data using actor keys - User context stored in connection state @@ -217,4 +194,4 @@ To test the role-based access control: ## License -Apache 2.0 \ No newline at end of file +Apache 2.0 diff --git a/internal-docs/CONFIG_TYPES.md b/internal-docs/CONFIG_TYPES.md index 9b8245777..62e2466df 100644 --- a/internal-docs/CONFIG_TYPES.md +++ b/internal-docs/CONFIG_TYPES.md @@ -4,13 +4,20 @@ - All types must be included in `ActorTypes` so the user can hardcode types -- If using input parameters for inferring types, they must be raw parameters. e.g.: +- If using input parameters for inferring types, they must be raw parameters. They also must be the last parameter. e.g.: ```typescript - // It's hard for users to infer TConnParams + // DO NOT DO THIS: + // It's hard for users to infer TConnParams because they would have to import & use an extra type onAuth: (opts: OnAuthOpts) => TAuthData, - // Because you would have to import & use an extra type - onAuth: (opts: OnAuthOpts) => TAuthData, + + // DO NOT DO THIS: + // If you only want to access `opts`, you'll have to also define `params` + onAuth: (params: TConnParams, opts: OnAuthOpts) => TAuthData, + + // DO THIS: + // This allows you to not accept `params` and only access `opts` + onAuth: (opts: OnAuthOpts, params: TConnParams) => TAuthData, ``` - When inferring via return data, you must use a union. e.g.: diff --git a/packages/core/fixtures/driver-test-suite/auth.ts b/packages/core/fixtures/driver-test-suite/auth.ts index 7746b04ae..d7c0910e5 100644 --- a/packages/core/fixtures/driver-test-suite/auth.ts +++ b/packages/core/fixtures/driver-test-suite/auth.ts @@ -3,8 +3,8 @@ import { actor, UserError } from "@rivetkit/core"; // Basic auth actor - requires API key export const authActor = actor({ state: { requests: 0 }, - onAuth: (params) => { - const apiKey = (params as any)?.apiKey; + onAuth: (opts, params: { apiKey?: string } | undefined) => { + const apiKey = params?.apiKey; if (!apiKey) { throw new UserError("API key required", { code: "missing_auth" }); } @@ -27,9 +27,9 @@ export const authActor = actor({ // Intent-specific auth actor - checks different permissions for different intents export const intentAuthActor = actor({ state: { value: 0 }, - onAuth: (params, { request, intents }) => { + onAuth: ({ request, intents }, params: { role: string }) => { console.log("intents", intents, params); - const role = (params as any)?.role; + const role = params.role; if (intents.has("create") && role !== "admin") { throw new UserError("Admin role required for create operations", { @@ -80,8 +80,8 @@ export const noAuthActor = actor({ // Async auth actor - tests promise-based authentication export const asyncAuthActor = actor({ state: { count: 0 }, - onAuth: async (params) => { - const token = (params as any)?.token; + onAuth: async (opts, params: { token?: string } | undefined) => { + const token = params?.token; if (!token) { throw new UserError("Token required", { code: "missing_token" }); } diff --git a/packages/core/fixtures/driver-test-suite/conn-params.ts b/packages/core/fixtures/driver-test-suite/conn-params.ts index 9904d5f69..49200db25 100644 --- a/packages/core/fixtures/driver-test-suite/conn-params.ts +++ b/packages/core/fixtures/driver-test-suite/conn-params.ts @@ -3,9 +3,9 @@ import { actor } from "@rivetkit/core"; export const counterWithParams = actor({ onAuth: () => {}, state: { count: 0, initializers: [] as string[] }, - createConnState: (c, { params }: { params: { name?: string } }) => { + createConnState: (c, opts, params: { name?: string }) => { return { - name: params?.name || "anonymous", + name: params.name || "anonymous", }; }, onConnect: (c, conn) => { diff --git a/packages/core/fixtures/driver-test-suite/conn-state.ts b/packages/core/fixtures/driver-test-suite/conn-state.ts index 0be9e2445..22edc2192 100644 --- a/packages/core/fixtures/driver-test-suite/conn-state.ts +++ b/packages/core/fixtures/driver-test-suite/conn-state.ts @@ -16,7 +16,8 @@ export const connStateActor = actor({ // Define connection state createConnState: ( c, - { params }: { params?: { username?: string; role?: string } }, + opts, + params: { username?: string; role?: string }, ): ConnState => { return { username: params?.username || "anonymous", diff --git a/packages/core/fixtures/driver-test-suite/lifecycle.ts b/packages/core/fixtures/driver-test-suite/lifecycle.ts index 697596654..2b6ee5b65 100644 --- a/packages/core/fixtures/driver-test-suite/lifecycle.ts +++ b/packages/core/fixtures/driver-test-suite/lifecycle.ts @@ -8,13 +8,13 @@ export const counterWithLifecycle = actor({ count: 0, events: [] as string[], }, - createConnState: (c, params: ConnParams) => ({ + createConnState: (c, opts, params: ConnParams) => ({ joinTime: Date.now(), }), onStart: (c) => { c.state.events.push("onStart"); }, - onBeforeConnect: (c, params) => { + onBeforeConnect: (c, opts, params: ConnParams) => { if (params?.trackLifecycle) c.state.events.push("onBeforeConnect"); }, onConnect: (c, conn) => { diff --git a/packages/core/fixtures/driver-test-suite/raw-http-auth.ts b/packages/core/fixtures/driver-test-suite/raw-http-auth.ts index 6d89f0291..1b8017742 100644 --- a/packages/core/fixtures/driver-test-suite/raw-http-auth.ts +++ b/packages/core/fixtures/driver-test-suite/raw-http-auth.ts @@ -5,8 +5,8 @@ export const rawHttpAuthActor = actor({ state: { requestCount: 0, }, - onAuth: (params) => { - const apiKey = (params as any)?.apiKey; + onAuth: (opts, params: { apiKey?: string }) => { + const apiKey = params.apiKey; if (!apiKey) { throw new UserError("API key required", { code: "missing_auth" }); } diff --git a/packages/core/fixtures/driver-test-suite/raw-websocket-auth.ts b/packages/core/fixtures/driver-test-suite/raw-websocket-auth.ts index c90cb4b24..8bcdf9cd6 100644 --- a/packages/core/fixtures/driver-test-suite/raw-websocket-auth.ts +++ b/packages/core/fixtures/driver-test-suite/raw-websocket-auth.ts @@ -11,8 +11,8 @@ export const rawWebSocketAuthActor = actor({ connectionCount: 0, messageCount: 0, }, - onAuth: (params) => { - const apiKey = (params as any)?.apiKey; + onAuth: (opts, params: { apiKey?: string }) => { + const apiKey = params.apiKey; if (!apiKey) { throw new UserError("API key required", { code: "missing_auth" }); } diff --git a/packages/core/fixtures/driver-test-suite/request-access-auth.ts b/packages/core/fixtures/driver-test-suite/request-access-auth.ts index 6b17995d5..fe1322ff2 100644 --- a/packages/core/fixtures/driver-test-suite/request-access-auth.ts +++ b/packages/core/fixtures/driver-test-suite/request-access-auth.ts @@ -5,15 +5,7 @@ import { actor } from "@rivetkit/core"; * onAuth runs on the HTTP server, not in the actor, so we test it separately */ export const requestAccessAuthActor = actor({ - onAuth: ({ - request, - intents, - params, - }: { - request: Request; - intents: Set; - params?: { trackRequest?: boolean }; - }) => { + onAuth: ({ request, intents }, params: { trackRequest?: boolean }) => { if (params?.trackRequest) { // Extract request info and return it as auth data const headers: Record = {}; diff --git a/packages/core/fixtures/driver-test-suite/request-access.ts b/packages/core/fixtures/driver-test-suite/request-access.ts index caa44ef92..6a801e45d 100644 --- a/packages/core/fixtures/driver-test-suite/request-access.ts +++ b/packages/core/fixtures/driver-test-suite/request-access.ts @@ -32,13 +32,7 @@ export const requestAccessActor = actor({ requestHeaders: {} as Record, }, }, - createConnState: ( - c, - { - params, - request, - }: { params?: { trackRequest?: boolean }; request?: Request }, - ) => { + createConnState: (c, { request }, params: { trackRequest?: boolean }) => { // In createConnState, the state isn't available yet. return { @@ -60,7 +54,7 @@ export const requestAccessActor = actor({ c.state.createConnStateRequest = conn.state.requestInfo; } }, - onBeforeConnect: (c, { request, params }) => { + onBeforeConnect: (c, { request }, params) => { if (params?.trackRequest) { if (request) { c.state.onBeforeConnectRequest.hasRequest = true; diff --git a/packages/core/src/actor/config.ts b/packages/core/src/actor/config.ts index 85aa78044..97286be19 100644 --- a/packages/core/src/actor/config.ts +++ b/packages/core/src/actor/config.ts @@ -155,8 +155,8 @@ type CreateConnState< | { createConnState: ( c: InitContext, - params: TConnParams, opts: OnConnectOptions, + params: TConnParams, ) => TConnState | Promise; } | Record; @@ -221,8 +221,8 @@ type OnAuth = * @throws Throw an error to deny access to the actor */ onAuth: ( - params: TConnParams, opts: OnAuthOptions, + params: TConnParams, ) => TAuthData | Promise; } | Record; @@ -376,8 +376,8 @@ interface BaseActorConfig< TAuthData, TDatabase >, - params: TConnParams, opts: OnConnectOptions, + params: TConnParams, ) => void | Promise; /** diff --git a/packages/core/src/actor/errors.ts b/packages/core/src/actor/errors.ts index 49f212bcf..a39f129ac 100644 --- a/packages/core/src/actor/errors.ts +++ b/packages/core/src/actor/errors.ts @@ -181,7 +181,7 @@ export class InvalidStateType extends ActorError { msg += "Attempted to set invalid state."; } msg += - " State must be CBOR serializable. Valid types include: null, undefined, boolean, string, number, BigInt, Date, RegExp, Error, typed arrays (Uint8Array, Int8Array, Float32Array, etc.), Map, Set, Array, and plain objects. (https://www.rivet.gg/docs/actors/state/#limitations)"; + " Valid types include: null, undefined, boolean, string, number, BigInt, Date, RegExp, Error, typed arrays (Uint8Array, Int8Array, Float32Array, etc.), Map, Set, Array, and plain objects. (https://www.rivet.gg/docs/actors/state/#limitations)"; super("invalid_state_type", msg); } } diff --git a/packages/core/src/actor/instance.ts b/packages/core/src/actor/instance.ts index 4c5a64c72..27263da86 100644 --- a/packages/core/src/actor/instance.ts +++ b/packages/core/src/actor/instance.ts @@ -728,8 +728,8 @@ export class ActorInstance< if (this.#config.onBeforeConnect) { await this.#config.onBeforeConnect( this.actorContext, - params, onBeforeConnectOpts, + params, ); } @@ -745,8 +745,8 @@ export class ActorInstance< undefined, undefined >, - params, onBeforeConnectOpts, + params, ); if (dataOrPromise instanceof Promise) { connState = await deadline( diff --git a/packages/core/src/manager/auth.ts b/packages/core/src/manager/auth.ts index 0084482a9..28dc66c7e 100644 --- a/packages/core/src/manager/auth.ts +++ b/packages/core/src/manager/auth.ts @@ -71,10 +71,13 @@ export async function authenticateRequest( } try { - const dataOrPromise = actorDefinition.config.onAuth(params, { - request: c.req.raw, - intents, - }); + const dataOrPromise = actorDefinition.config.onAuth( + { + request: c.req.raw, + intents, + }, + params, + ); if (dataOrPromise instanceof Promise) { return await dataOrPromise; } else {