@rebasepro/cli 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/dist/commands/api-keys.d.ts +1 -0
- package/dist/commands/generate_sdk.d.ts +1 -1
- package/dist/index.es.js +399 -57
- package/dist/index.es.js.map +1 -1
- package/package.json +20 -15
- package/templates/template/backend/package.json +0 -1
- package/templates/template/backend/src/index.ts +1 -1
- package/templates/template/config/collections/posts.ts +1 -3
- package/templates/template/config/collections/tags.ts +1 -3
- package/templates/template/config/collections/users.ts +1 -4
- package/templates/template/package.json +0 -1
- package/templates/template/scripts/example.ts +4 -1
- package/skills/rebase-admin/SKILL.md +0 -710
- package/skills/rebase-api/SKILL.md +0 -662
- package/skills/rebase-api/references/.gitkeep +0 -3
- package/skills/rebase-auth/SKILL.md +0 -1143
- package/skills/rebase-auth/references/.gitkeep +0 -3
- package/skills/rebase-backend-postgres/SKILL.md +0 -633
- package/skills/rebase-backend-postgres/references/.gitkeep +0 -3
- package/skills/rebase-basics/SKILL.md +0 -749
- package/skills/rebase-basics/references/.gitkeep +0 -3
- package/skills/rebase-collections/SKILL.md +0 -1328
- package/skills/rebase-collections/references/.gitkeep +0 -3
- package/skills/rebase-cron-jobs/SKILL.md +0 -699
- package/skills/rebase-cron-jobs/references/.gitkeep +0 -1
- package/skills/rebase-custom-functions/SKILL.md +0 -233
- package/skills/rebase-deployment/SKILL.md +0 -885
- package/skills/rebase-deployment/references/.gitkeep +0 -3
- package/skills/rebase-design-language/SKILL.md +0 -692
- package/skills/rebase-email/SKILL.md +0 -701
- package/skills/rebase-email/references/.gitkeep +0 -1
- package/skills/rebase-entity-history/SKILL.md +0 -485
- package/skills/rebase-entity-history/references/.gitkeep +0 -1
- package/skills/rebase-local-env-setup/SKILL.md +0 -189
- package/skills/rebase-local-env-setup/references/.gitkeep +0 -3
- package/skills/rebase-realtime/SKILL.md +0 -755
- package/skills/rebase-realtime/references/.gitkeep +0 -3
- package/skills/rebase-sdk/SKILL.md +0 -594
- package/skills/rebase-sdk/references/.gitkeep +0 -0
- package/skills/rebase-storage/SKILL.md +0 -765
- package/skills/rebase-storage/references/.gitkeep +0 -3
- package/skills/rebase-studio/SKILL.md +0 -746
- package/skills/rebase-studio/references/.gitkeep +0 -3
- package/skills/rebase-ui-components/SKILL.md +0 -1488
- package/skills/rebase-ui-components/references/.gitkeep +0 -3
- package/skills/rebase-webhooks/SKILL.md +0 -623
- package/skills/rebase-webhooks/references/.gitkeep +0 -1
- package/templates/template/backend/drizzle.config.ts +0 -51
|
@@ -1,1143 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: rebase-auth
|
|
3
|
-
description: Guide for setting up and using Rebase Authentication, roles, Row-Level Security (RLS) policies, MFA, API keys, OAuth providers, custom auth adapters, and lifecycle hooks. Use this skill when the user needs to add authentication, manage users and roles, secure data access, configure OAuth, set up MFA, create API keys, or customize the auth pipeline.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Rebase Authentication
|
|
7
|
-
|
|
8
|
-
Rebase ships a complete, built-in authentication system with JWT sessions, OAuth, MFA/TOTP, API keys, Row-Level Security, and lifecycle hooks — or you can plug in an external provider (Clerk, Auth0, Firebase Auth) via the `AuthAdapter` interface.
|
|
9
|
-
|
|
10
|
-
> **IMPORTANT FOR AGENTS:** Always read the `rebase-basics` skill first. The auth system is configured inside `initializeRebaseBackend()` which is covered there.
|
|
11
|
-
|
|
12
|
-
## Table of Contents
|
|
13
|
-
|
|
14
|
-
- [Server-Side Configuration (RebaseAuthConfig)](#server-side-configuration)
|
|
15
|
-
- [OAuth Providers](#oauth-providers)
|
|
16
|
-
- [Auth Lifecycle Hooks](#auth-lifecycle-hooks)
|
|
17
|
-
- [MFA / TOTP](#mfa--totp)
|
|
18
|
-
- [API Keys](#api-keys)
|
|
19
|
-
- [REST Endpoints](#rest-endpoints)
|
|
20
|
-
- [Client SDK (auth module)](#client-sdk)
|
|
21
|
-
- [Row-Level Security (RLS)](#row-level-security)
|
|
22
|
-
- [Rate Limiting](#rate-limiting)
|
|
23
|
-
- [Custom Auth Adapters](#custom-auth-adapters)
|
|
24
|
-
- [Roles & Permissions](#roles--permissions)
|
|
25
|
-
- [Backend Hooks](#backend-hooks)
|
|
26
|
-
- [Email Configuration](#email-configuration)
|
|
27
|
-
- [Security Concepts](#security-concepts)
|
|
28
|
-
- [References](#references)
|
|
29
|
-
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
## Server-Side Configuration
|
|
33
|
-
|
|
34
|
-
Authentication is configured via the `auth` property of `initializeRebaseBackend()`. It accepts **either** a `RebaseAuthConfig` object (built-in auth) or an `AuthAdapter` (external auth).
|
|
35
|
-
|
|
36
|
-
### RebaseAuthConfig
|
|
37
|
-
|
|
38
|
-
| Property | Type | Default | Description |
|
|
39
|
-
|---|---|---|---|
|
|
40
|
-
| `collection` | `EntityCollection` | Built-in users collection | The collection used for auth user storage. Import `defaultUsersCollection` from `@rebasepro/common` or pass a custom collection with required auth fields. |
|
|
41
|
-
| `jwtSecret` | `string` | — | **Required.** Secret for signing JWT access tokens. |
|
|
42
|
-
| `accessExpiresIn` | `string` | `"1h"` | Access token lifetime (e.g. `"15m"`, `"2h"`). |
|
|
43
|
-
| `refreshExpiresIn` | `string` | `"30d"` | Refresh token lifetime. |
|
|
44
|
-
| `requireAuth` | `boolean` | `true` | When `true`, data routes return 401 for unauthenticated requests. Set to `false` to rely entirely on Postgres RLS. |
|
|
45
|
-
| `allowRegistration` | `boolean` | `false` | Enable self-service registration via `POST /auth/register`. |
|
|
46
|
-
| `serviceKey` | `string` | — | Static secret for server-to-server auth. Must be ≥ 32 characters. Requests with `Authorization: Bearer <serviceKey>` get admin access. |
|
|
47
|
-
| `defaultRole` | `string` | — | Role ID assigned to new users (except the first user, who always gets `"admin"`). **Must NOT be `"admin"`** — throws a security error at startup. |
|
|
48
|
-
| `providers` | `OAuthProvider<any>[]` | `[]` | Custom OAuth providers (use alongside the shorthand configs below). |
|
|
49
|
-
| `hooks` | `AuthHooks` | — | [Lifecycle hooks](#auth-lifecycle-hooks) to customize passwords, credentials, and auth events. |
|
|
50
|
-
| `email` | `EmailConfig` | — | [Email configuration](#email-configuration) for password resets, verification, and welcome emails. |
|
|
51
|
-
| `google` | `{ clientId, clientSecret? }` | — | Google OAuth shorthand. |
|
|
52
|
-
| `github` | `{ clientId, clientSecret }` | — | GitHub OAuth shorthand. |
|
|
53
|
-
| `microsoft` | `{ clientId, clientSecret, tenantId? }` | — | Microsoft/Entra ID shorthand. `tenantId` defaults to `"common"`. |
|
|
54
|
-
| `apple` | `{ clientId, teamId, keyId, privateKey }` | — | Apple Sign In shorthand. `privateKey` is the raw PEM (.p8) contents. |
|
|
55
|
-
| `facebook` | `{ clientId, clientSecret }` | — | Facebook/Meta OAuth. |
|
|
56
|
-
| `twitter` | `{ clientId, clientSecret }` | — | Twitter/X OAuth 2.0 with PKCE. |
|
|
57
|
-
| `discord` | `{ clientId, clientSecret }` | — | Discord OAuth. |
|
|
58
|
-
| `gitlab` | `{ clientId, clientSecret, baseUrl? }` | — | GitLab OAuth. `baseUrl` defaults to `"https://gitlab.com"` (supports self-hosted). |
|
|
59
|
-
| `linkedin` | `{ clientId, clientSecret }` | — | LinkedIn OAuth (OIDC). |
|
|
60
|
-
| `bitbucket` | `{ clientId, clientSecret }` | — | Bitbucket OAuth. |
|
|
61
|
-
| `slack` | `{ clientId, clientSecret }` | — | Slack OAuth (OIDC). |
|
|
62
|
-
| `spotify` | `{ clientId, clientSecret }` | — | Spotify OAuth. |
|
|
63
|
-
|
|
64
|
-
### Minimal Example
|
|
65
|
-
|
|
66
|
-
```typescript
|
|
67
|
-
import { initializeRebaseBackend } from "@rebasepro/server-core";
|
|
68
|
-
import { createPostgresAdapter } from "@rebasepro/server-postgresql";
|
|
69
|
-
|
|
70
|
-
await initializeRebaseBackend({
|
|
71
|
-
server,
|
|
72
|
-
app,
|
|
73
|
-
database: createPostgresAdapter({ connection: db, schema }),
|
|
74
|
-
auth: {
|
|
75
|
-
jwtSecret: process.env.JWT_SECRET!,
|
|
76
|
-
allowRegistration: true,
|
|
77
|
-
serviceKey: process.env.REBASE_SERVICE_KEY,
|
|
78
|
-
defaultRole: "member",
|
|
79
|
-
google: {
|
|
80
|
-
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
81
|
-
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
82
|
-
},
|
|
83
|
-
github: {
|
|
84
|
-
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
85
|
-
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
86
|
-
},
|
|
87
|
-
email: {
|
|
88
|
-
from: "noreply@myapp.com",
|
|
89
|
-
smtp: {
|
|
90
|
-
host: "smtp.resend.com",
|
|
91
|
-
port: 465,
|
|
92
|
-
secure: true,
|
|
93
|
-
auth: { user: "resend", pass: process.env.RESEND_API_KEY! },
|
|
94
|
-
},
|
|
95
|
-
appName: "MyApp",
|
|
96
|
-
resetPasswordUrl: "https://myapp.com",
|
|
97
|
-
verifyEmailUrl: "https://myapp.com",
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
### Collection-Level Auth Configuration
|
|
104
|
-
|
|
105
|
-
Instead of relying solely on the default database auth rules, you can mark any Postgres collection (such as `users.ts` or a custom `members.ts` collection) as the authentication collection. This is configured via the `auth` property on the collection itself:
|
|
106
|
-
|
|
107
|
-
```typescript
|
|
108
|
-
import { PostgresCollection } from "@rebasepro/types";
|
|
109
|
-
|
|
110
|
-
const membersCollection: PostgresCollection = {
|
|
111
|
-
name: "Members",
|
|
112
|
-
slug: "members",
|
|
113
|
-
table: "members",
|
|
114
|
-
auth: {
|
|
115
|
-
enabled: true,
|
|
116
|
-
|
|
117
|
-
// Customize what happens when an admin creates a user via the REST API
|
|
118
|
-
onCreateUser: async (values, ctx) => {
|
|
119
|
-
const hash = await ctx.hashPassword("welcome123");
|
|
120
|
-
return {
|
|
121
|
-
values: { ...values, passwordHash: hash, emailVerified: true },
|
|
122
|
-
temporaryPassword: "welcome123"
|
|
123
|
-
};
|
|
124
|
-
},
|
|
125
|
-
|
|
126
|
-
// Customize what happens when an admin resets a user's password in the admin panel
|
|
127
|
-
onResetPassword: async (userId, ctx) => {
|
|
128
|
-
const tempPassword = "reset_" + Math.random().toString(36).substring(2, 8);
|
|
129
|
-
return {
|
|
130
|
-
temporaryPassword: tempPassword,
|
|
131
|
-
invitationSent: false
|
|
132
|
-
};
|
|
133
|
-
},
|
|
134
|
-
|
|
135
|
-
// Inject/override auth-specific actions (e.g. show/hide the reset password button)
|
|
136
|
-
actions: {
|
|
137
|
-
resetPassword: true // Or false to disable, or a custom EntityAction
|
|
138
|
-
}
|
|
139
|
-
},
|
|
140
|
-
properties: { ... }
|
|
141
|
-
};
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
When custom hooks (`onCreateUser`, `onResetPassword`) are called, they receive an `AuthCollectionContext` facade containing:
|
|
145
|
-
- `hashPassword(password: string): Promise<string>` — Hash password using the configured hashing algorithm (e.g. scrypt).
|
|
146
|
-
- `sendEmail?: (options) => Promise<void>` — Send an email (only available when email service is configured).
|
|
147
|
-
- `emailConfigured: boolean` — Whether email service is configured.
|
|
148
|
-
- `appName: string` — The app name from email config.
|
|
149
|
-
- `resetPasswordUrl: string` — The password reset link base URL.
|
|
150
|
-
|
|
151
|
-
### First-User Bootstrap
|
|
152
|
-
|
|
153
|
-
> **IMPORTANT FOR AGENTS:** The very first user registered (via `POST /auth/register` or OAuth) is automatically promoted to `"admin"`. This prevents the chicken-and-egg problem. All subsequent users receive the `defaultRole`.
|
|
154
|
-
|
|
155
|
-
---
|
|
156
|
-
|
|
157
|
-
## OAuth Providers
|
|
158
|
-
|
|
159
|
-
Rebase supports 12 built-in OAuth providers. Each provider is configured via a shorthand property on `RebaseAuthConfig` and automatically mounts a `POST /api/auth/{providerId}` endpoint.
|
|
160
|
-
|
|
161
|
-
### Provider Reference
|
|
162
|
-
|
|
163
|
-
| Provider | ID | Config Properties | Client Payload |
|
|
164
|
-
|---|---|---|---|
|
|
165
|
-
| Google | `google` | `clientId`, `clientSecret?` | `{ idToken }` OR `{ accessToken }` OR `{ code, redirectUri }` |
|
|
166
|
-
| GitHub | `github` | `clientId`, `clientSecret` | `{ code, redirectUri }` |
|
|
167
|
-
| Microsoft | `microsoft` | `clientId`, `clientSecret`, `tenantId?` | `{ code, redirectUri }` |
|
|
168
|
-
| Apple | `apple` | `clientId`, `teamId`, `keyId`, `privateKey` | `{ code, redirectUri, user? }` |
|
|
169
|
-
| Facebook | `facebook` | `clientId`, `clientSecret` | `{ code, redirectUri }` |
|
|
170
|
-
| Twitter/X | `twitter` | `clientId`, `clientSecret` | `{ code, redirectUri, codeVerifier }` |
|
|
171
|
-
| Discord | `discord` | `clientId`, `clientSecret` | `{ code, redirectUri }` |
|
|
172
|
-
| GitLab | `gitlab` | `clientId`, `clientSecret`, `baseUrl?` | `{ code, redirectUri }` |
|
|
173
|
-
| LinkedIn | `linkedin` | `clientId`, `clientSecret` | `{ code, redirectUri }` |
|
|
174
|
-
| Bitbucket | `bitbucket` | `clientId`, `clientSecret` | `{ code, redirectUri }` |
|
|
175
|
-
| Slack | `slack` | `clientId`, `clientSecret` | `{ code, redirectUri }` |
|
|
176
|
-
| Spotify | `spotify` | `clientId`, `clientSecret` | `{ code, redirectUri }` |
|
|
177
|
-
|
|
178
|
-
### Google Three-Path Support
|
|
179
|
-
|
|
180
|
-
Google is unique — it supports three verification paths:
|
|
181
|
-
|
|
182
|
-
1. **ID Token** (One Tap / Sign In button) — `{ idToken }`. Cryptographic verification via Google's public keys. No `clientSecret` needed.
|
|
183
|
-
2. **Access Token** (popup via `initTokenClient`) — `{ accessToken }`. Validated via Google's userinfo endpoint. No `clientSecret` needed.
|
|
184
|
-
3. **Authorization Code** (most secure) — `{ code, redirectUri }`. Requires `clientSecret`. Tokens never touch the browser.
|
|
185
|
-
|
|
186
|
-
### Apple Special Behavior
|
|
187
|
-
|
|
188
|
-
- Apple only sends the user's name on the **first** authorization. The frontend must capture and forward it: `{ code, redirectUri, user: { name: { firstName, lastName }, email } }`.
|
|
189
|
-
- Apple does not provide a profile photo (`photoUrl` is always `null`).
|
|
190
|
-
- The `privateKey` is the raw PEM contents of the `.p8` file downloaded from Apple Developer.
|
|
191
|
-
|
|
192
|
-
### Twitter PKCE
|
|
193
|
-
|
|
194
|
-
Twitter uses OAuth 2.0 with PKCE. The client must send `codeVerifier` alongside `code` and `redirectUri`.
|
|
195
|
-
|
|
196
|
-
### OAuth Account Linking
|
|
197
|
-
|
|
198
|
-
When an OAuth user signs in:
|
|
199
|
-
1. If an identity record exists for `(providerId, provider)` → log in that user.
|
|
200
|
-
2. If no identity exists but a user with the same email exists → **link** the provider to the existing account.
|
|
201
|
-
3. If neither exists → create a new user, link the identity, assign `defaultRole`.
|
|
202
|
-
|
|
203
|
-
### Custom OAuth Provider
|
|
204
|
-
|
|
205
|
-
You can register any OAuth provider by implementing the `OAuthProvider<T>` interface:
|
|
206
|
-
|
|
207
|
-
```typescript
|
|
208
|
-
import { z } from "zod";
|
|
209
|
-
import type { OAuthProvider, OAuthProviderProfile } from "@rebasepro/server-core";
|
|
210
|
-
|
|
211
|
-
const myProvider: OAuthProvider<{ token: string }> = {
|
|
212
|
-
id: "my-provider",
|
|
213
|
-
schema: z.object({ token: z.string().min(1) }),
|
|
214
|
-
verify: async (payload): Promise<OAuthProviderProfile | null> => {
|
|
215
|
-
const userInfo = await verifyExternalToken(payload.token);
|
|
216
|
-
if (!userInfo) return null;
|
|
217
|
-
return {
|
|
218
|
-
providerId: userInfo.id,
|
|
219
|
-
email: userInfo.email,
|
|
220
|
-
displayName: userInfo.name || null,
|
|
221
|
-
photoUrl: userInfo.avatar || null,
|
|
222
|
-
};
|
|
223
|
-
},
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
// Use in config:
|
|
227
|
-
auth: {
|
|
228
|
-
jwtSecret: "...",
|
|
229
|
-
providers: [myProvider],
|
|
230
|
-
}
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
---
|
|
234
|
-
|
|
235
|
-
## Auth Lifecycle Hooks
|
|
236
|
-
|
|
237
|
-
The `AuthHooks` interface lets you customize specific behaviors of the built-in auth system. Every hook is optional — unset hooks fall through to built-in defaults.
|
|
238
|
-
|
|
239
|
-
### Hook Reference
|
|
240
|
-
|
|
241
|
-
| Hook | Signature | Default | Behavior |
|
|
242
|
-
|---|---|---|---|
|
|
243
|
-
| `hashPassword` | `(password: string) => Promise<string>` | scrypt (Node crypto, 64-byte key, 32-byte salt) | Hash a cleartext password for storage. |
|
|
244
|
-
| `verifyPassword` | `(password: string, storedHash: string) => Promise<boolean>` | scrypt with timing-safe comparison | Verify cleartext password against stored hash. |
|
|
245
|
-
| `validatePasswordStrength` | `(password: string) => PasswordValidationResult` | Min 8 chars, 1 uppercase, 1 lowercase, 1 digit | Return `{ valid: boolean, errors: string[] }`. |
|
|
246
|
-
| `verifyCredentials` | `(email, password, repo: AuthRepository) => Promise<UserData \| null>` | `getUserByEmail` + `verifyPassword` | Override the entire login credential check. Return user or `null`. |
|
|
247
|
-
| `onAuthenticated` | `(user: UserData, method: AuthMethod) => Promise<void>` | — | Called after **any** successful auth event. Fire-and-forget. |
|
|
248
|
-
| `beforeUserCreate` | `(data: CreateUserData) => Promise<CreateUserData>` | Passthrough | Modify or reject user creation. Throw to abort. |
|
|
249
|
-
| `afterUserCreate` | `(user: UserData) => Promise<void>` | — | Called after user creation. Fire-and-forget. |
|
|
250
|
-
| `beforeLogin` | `(email: string, method: AuthMethod) => Promise<void>` | — | Pre-login validation. Throw to reject (e.g. account lockout). |
|
|
251
|
-
| `afterLogout` | `(userId: string) => Promise<void>` | — | Post-logout cleanup. Fire-and-forget. |
|
|
252
|
-
| `onMfaVerified` | `(userId: string, factorId: string) => Promise<void>` | — | Called after successful MFA verification. Fire-and-forget. |
|
|
253
|
-
| `customizeAccessToken` | `(claims: Record<string, unknown>, user: UserData) => Promise<Record<string, unknown>>` | — | Modify JWT access token claims before signing. |
|
|
254
|
-
| `onPasswordReset` | `(userId: string) => Promise<void>` | — | Called after successful password reset. Fire-and-forget. |
|
|
255
|
-
| `beforeUserDelete` | `(userId: string) => Promise<void>` | — | Throw to prevent deletion. |
|
|
256
|
-
| `afterUserDelete` | `(userId: string) => Promise<void>` | — | Post-deletion cleanup. Fire-and-forget. |
|
|
257
|
-
|
|
258
|
-
### AuthMethod Values
|
|
259
|
-
|
|
260
|
-
`"login"` | `"register"` | `"oauth"` | `"refresh"` | `"password-reset"` | `"anonymous"` | `"magic-link"` | `"mfa"`
|
|
261
|
-
|
|
262
|
-
### PasswordValidationResult
|
|
263
|
-
|
|
264
|
-
```typescript
|
|
265
|
-
interface PasswordValidationResult {
|
|
266
|
-
valid: boolean;
|
|
267
|
-
errors: string[];
|
|
268
|
-
}
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
### Example: bcrypt Passwords
|
|
272
|
-
|
|
273
|
-
```typescript
|
|
274
|
-
import bcrypt from "bcrypt";
|
|
275
|
-
|
|
276
|
-
auth: {
|
|
277
|
-
jwtSecret: "...",
|
|
278
|
-
hooks: {
|
|
279
|
-
hashPassword: (pw) => bcrypt.hash(pw, 12),
|
|
280
|
-
verifyPassword: (pw, hash) => bcrypt.compare(pw, hash),
|
|
281
|
-
validatePasswordStrength: (pw) => ({
|
|
282
|
-
valid: pw.length >= 6,
|
|
283
|
-
errors: pw.length < 6 ? ["Password must be at least 6 characters"] : [],
|
|
284
|
-
}),
|
|
285
|
-
},
|
|
286
|
-
}
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
### Example: Custom JWT Claims
|
|
290
|
-
|
|
291
|
-
```typescript
|
|
292
|
-
hooks: {
|
|
293
|
-
customizeAccessToken: async (claims, user) => ({
|
|
294
|
-
...claims,
|
|
295
|
-
org_id: user.metadata?.organizationId,
|
|
296
|
-
plan: user.metadata?.plan || "free",
|
|
297
|
-
}),
|
|
298
|
-
}
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
### Example: Audit Logging
|
|
302
|
-
|
|
303
|
-
```typescript
|
|
304
|
-
hooks: {
|
|
305
|
-
onAuthenticated: async (user, method) => {
|
|
306
|
-
await auditLog.write({
|
|
307
|
-
event: "auth.success",
|
|
308
|
-
userId: user.id,
|
|
309
|
-
method,
|
|
310
|
-
timestamp: new Date(),
|
|
311
|
-
});
|
|
312
|
-
},
|
|
313
|
-
beforeLogin: async (email, method) => {
|
|
314
|
-
const isBlocked = await checkAccountLockout(email);
|
|
315
|
-
if (isBlocked) throw new Error("Account is locked");
|
|
316
|
-
},
|
|
317
|
-
}
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
---
|
|
321
|
-
|
|
322
|
-
## MFA / TOTP
|
|
323
|
-
|
|
324
|
-
Rebase supports Multi-Factor Authentication via TOTP (Time-based One-Time Password). The flow uses an enrollment → verify → challenge pattern with recovery codes.
|
|
325
|
-
|
|
326
|
-
### MFA Flow
|
|
327
|
-
|
|
328
|
-
1. **Enroll** — `POST /api/auth/mfa/enroll` returns a TOTP secret, URI (for QR), and 10 recovery codes.
|
|
329
|
-
2. **Verify enrollment** — `POST /api/auth/mfa/verify` with a 6-digit TOTP code to confirm the factor.
|
|
330
|
-
3. **Challenge on login** — After normal login (aal1), call `POST /api/auth/mfa/challenge` to create a challenge.
|
|
331
|
-
4. **Complete challenge** — `POST /api/auth/mfa/challenge/verify` with TOTP or recovery code. Upgrades token from `aal1` → `aal2`.
|
|
332
|
-
|
|
333
|
-
### MFA Endpoints
|
|
334
|
-
|
|
335
|
-
| Method | Endpoint | Auth | Description |
|
|
336
|
-
|---|---|---|---|
|
|
337
|
-
| `POST` | `/api/auth/mfa/enroll` | Required | Start enrollment. Returns `{ factor, totp: { secret, uri, qrUri }, recoveryCodes }`. |
|
|
338
|
-
| `POST` | `/api/auth/mfa/verify` | Required | Verify enrollment with `{ factorId, code }` (6-digit TOTP). |
|
|
339
|
-
| `POST` | `/api/auth/mfa/challenge` | Required | Create challenge with `{ factorId }`. Returns `{ challengeId, factorId, expiresAt }`. Challenge expires in 5 minutes. |
|
|
340
|
-
| `POST` | `/api/auth/mfa/challenge/verify` | Required | Complete challenge with `{ challengeId, code }`. Returns new tokens with `aal2`. Accepts TOTP (6 digits) or recovery code (>6 chars). |
|
|
341
|
-
| `GET` | `/api/auth/mfa/factors` | Required | List enrolled factors: `{ factors: [{ id, factorType, friendlyName, verified, createdAt }] }`. |
|
|
342
|
-
| `DELETE` | `/api/auth/mfa/unenroll` | Required | Remove factor with `{ factorId }` in body. Auto-cleans recovery codes when no verified factors remain. |
|
|
343
|
-
|
|
344
|
-
### MFA Types
|
|
345
|
-
|
|
346
|
-
```typescript
|
|
347
|
-
interface MfaFactor {
|
|
348
|
-
id: string;
|
|
349
|
-
userId: string;
|
|
350
|
-
factorType: "totp"; // Only TOTP is supported
|
|
351
|
-
friendlyName?: string;
|
|
352
|
-
verified: boolean;
|
|
353
|
-
createdAt: Date;
|
|
354
|
-
updatedAt: Date;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
interface MfaChallengeInfo {
|
|
358
|
-
id: string;
|
|
359
|
-
factorId: string;
|
|
360
|
-
createdAt: Date;
|
|
361
|
-
verifiedAt?: Date;
|
|
362
|
-
ipAddress?: string;
|
|
363
|
-
}
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
### AAL (Authentication Assurance Levels)
|
|
367
|
-
|
|
368
|
-
| Level | Meaning |
|
|
369
|
-
|---|---|
|
|
370
|
-
| `aal1` | Standard authentication (email/password, OAuth). |
|
|
371
|
-
| `aal2` | Elevated after MFA challenge verification. |
|
|
372
|
-
|
|
373
|
-
---
|
|
374
|
-
|
|
375
|
-
## API Keys
|
|
376
|
-
|
|
377
|
-
API keys provide machine-to-machine authentication for scripts, cron jobs, and third-party integrations. They are scoped to specific collections and operations.
|
|
378
|
-
|
|
379
|
-
### Key Format
|
|
380
|
-
|
|
381
|
-
- Prefix: `rk_` (e.g. `rk_live_abc123...`)
|
|
382
|
-
- Storage: SHA-256 hash of the full key. The plaintext key is returned **exactly once** at creation.
|
|
383
|
-
- Display: Only the first 12 characters (`key_prefix`) are shown in subsequent API responses.
|
|
384
|
-
|
|
385
|
-
### API Key Admin Endpoints
|
|
386
|
-
|
|
387
|
-
All endpoints are mounted under `/api/admin/api-keys` and require **admin** authentication (JWT with admin role or service key).
|
|
388
|
-
|
|
389
|
-
| Method | Endpoint | Description |
|
|
390
|
-
|---|---|---|
|
|
391
|
-
| `GET` | `/api/admin/api-keys` | List all API keys (masked — no hashes). |
|
|
392
|
-
| `POST` | `/api/admin/api-keys` | Create a new API key. Returns the full plaintext key once. |
|
|
393
|
-
| `GET` | `/api/admin/api-keys/:id` | Get single API key details (masked). |
|
|
394
|
-
| `PUT` | `/api/admin/api-keys/:id` | Update name, permissions, rate_limit, or expires_at. |
|
|
395
|
-
| `DELETE` | `/api/admin/api-keys/:id` | Revoke (soft-delete) an API key. |
|
|
396
|
-
|
|
397
|
-
### Create API Key Request
|
|
398
|
-
|
|
399
|
-
```typescript
|
|
400
|
-
interface CreateApiKeyRequest {
|
|
401
|
-
name: string;
|
|
402
|
-
permissions: ApiKeyPermission[];
|
|
403
|
-
rate_limit?: number | null; // Requests per 15-min window. null = unlimited
|
|
404
|
-
expires_at?: string | null; // ISO-8601 timestamp. null = no expiration
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
interface ApiKeyPermission {
|
|
408
|
-
collection: string; // Collection slug, or "*" for all collections
|
|
409
|
-
operations: ("read" | "write" | "delete")[];
|
|
410
|
-
}
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
### Example: Create an API Key
|
|
414
|
-
|
|
415
|
-
```bash
|
|
416
|
-
curl -X POST http://localhost:3000/api/admin/api-keys \
|
|
417
|
-
-H "Authorization: Bearer <admin-token-or-service-key>" \
|
|
418
|
-
-H "Content-Type: application/json" \
|
|
419
|
-
-d '{
|
|
420
|
-
"name": "Analytics Pipeline",
|
|
421
|
-
"permissions": [
|
|
422
|
-
{ "collection": "events", "operations": ["read", "write"] },
|
|
423
|
-
{ "collection": "users", "operations": ["read"] }
|
|
424
|
-
],
|
|
425
|
-
"rate_limit": 500,
|
|
426
|
-
"expires_at": "2025-12-31T23:59:59Z"
|
|
427
|
-
}'
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
### Using an API Key
|
|
431
|
-
|
|
432
|
-
```bash
|
|
433
|
-
curl http://localhost:3000/api/data/events \
|
|
434
|
-
-H "Authorization: Bearer rk_live_abc123..."
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
### API Key Middleware Behavior
|
|
438
|
-
|
|
439
|
-
When a request arrives with a `rk_` prefixed bearer token:
|
|
440
|
-
1. The token is SHA-256 hashed and looked up in the `_rebase_api_keys` table.
|
|
441
|
-
2. Expiry and revocation status are checked.
|
|
442
|
-
3. Permissions are validated against the requested collection and HTTP method (`GET` → `read`, `POST`/`PUT`/`PATCH` → `write`, `DELETE` → `delete`).
|
|
443
|
-
4. The DataDriver is scoped with `withAuth()` using the key's created_by user, effectively **bypassing RLS** (the key operates as a service account).
|
|
444
|
-
5. Per-key rate limiting is enforced if `rate_limit` is set.
|
|
445
|
-
|
|
446
|
-
> **WARNING FOR AGENTS:** API keys bypass RLS. They are designed for trusted server-side use only. Never expose API keys to client-side code.
|
|
447
|
-
|
|
448
|
-
### API Key Response Types
|
|
449
|
-
|
|
450
|
-
```typescript
|
|
451
|
-
// Returned once at creation (includes the full plaintext key)
|
|
452
|
-
interface ApiKeyWithSecret {
|
|
453
|
-
id: string;
|
|
454
|
-
name: string;
|
|
455
|
-
key_prefix: string; // First 12 chars, for display
|
|
456
|
-
key: string; // FULL plaintext key — save this immediately
|
|
457
|
-
permissions: ApiKeyPermission[];
|
|
458
|
-
rate_limit: number | null;
|
|
459
|
-
created_by: string;
|
|
460
|
-
created_at: string;
|
|
461
|
-
updated_at: string;
|
|
462
|
-
last_used_at: string | null;
|
|
463
|
-
expires_at: string | null;
|
|
464
|
-
revoked_at: string | null;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// All subsequent reads (no hash, no full key)
|
|
468
|
-
interface ApiKeyMasked {
|
|
469
|
-
id: string;
|
|
470
|
-
name: string;
|
|
471
|
-
key_prefix: string;
|
|
472
|
-
permissions: ApiKeyPermission[];
|
|
473
|
-
rate_limit: number | null;
|
|
474
|
-
created_by: string;
|
|
475
|
-
created_at: string;
|
|
476
|
-
updated_at: string;
|
|
477
|
-
last_used_at: string | null;
|
|
478
|
-
expires_at: string | null;
|
|
479
|
-
revoked_at: string | null;
|
|
480
|
-
}
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
---
|
|
484
|
-
|
|
485
|
-
## REST Endpoints
|
|
486
|
-
|
|
487
|
-
All auth endpoints are mounted under `/api/auth`. Admin endpoints are under `/api/admin`.
|
|
488
|
-
|
|
489
|
-
### Public Auth Endpoints
|
|
490
|
-
|
|
491
|
-
| Method | Endpoint | Rate Limit | Auth | Description |
|
|
492
|
-
|---|---|---|---|---|
|
|
493
|
-
| `POST` | `/auth/register` | default (200/15min) | No | Create account. Body: `{ email, password, displayName? }`. |
|
|
494
|
-
| `POST` | `/auth/login` | default | No | Email/password login. Body: `{ email, password }`. |
|
|
495
|
-
| `POST` | `/auth/{providerId}` | default | No | OAuth sign-in. Body varies by provider. |
|
|
496
|
-
| `POST` | `/auth/refresh` | — | No | Refresh access token. Body: `{ refreshToken }`. Rotates refresh token. |
|
|
497
|
-
| `POST` | `/auth/logout` | — | No | Invalidate refresh token. Body: `{ refreshToken? }`. |
|
|
498
|
-
| `POST` | `/auth/anonymous` | strict (50/15min) | No | Create anonymous user with temp credentials. |
|
|
499
|
-
| `POST` | `/auth/forgot-password` | strict | No | Request password reset email. Body: `{ email }`. Always returns success (security). |
|
|
500
|
-
| `POST` | `/auth/reset-password` | strict | No | Reset password with token. Body: `{ token, password }`. Invalidates all sessions. |
|
|
501
|
-
| `GET` | `/auth/verify-email` | — | No | Verify email. Query: `?token=<token>`. |
|
|
502
|
-
| `GET` | `/auth/config` | default | No | Get auth capabilities for frontend: `{ needsSetup, registrationEnabled, emailServiceEnabled, enabledProviders }`. |
|
|
503
|
-
|
|
504
|
-
### Authenticated Endpoints
|
|
505
|
-
|
|
506
|
-
| Method | Endpoint | Auth | Description |
|
|
507
|
-
|---|---|---|---|
|
|
508
|
-
| `GET` | `/auth/me` | Required | Get current user profile + roles. |
|
|
509
|
-
| `PATCH` | `/auth/me` | Required | Update profile. Body: `{ displayName?, photoURL? }`. |
|
|
510
|
-
| `POST` | `/auth/change-password` | Required | Change password. Body: `{ oldPassword, newPassword }`. Invalidates all sessions. |
|
|
511
|
-
| `POST` | `/auth/send-verification` | Required | Send email verification link. Requires email service. |
|
|
512
|
-
| `GET` | `/auth/sessions` | Required | List active sessions (refresh tokens). |
|
|
513
|
-
| `DELETE` | `/auth/sessions` | Required | Revoke all sessions (remote logout). |
|
|
514
|
-
| `DELETE` | `/auth/sessions/:id` | Required | Revoke a specific session. |
|
|
515
|
-
| `POST` | `/auth/anonymous/link` | Required | Upgrade anonymous → permanent. Body: `{ email, password }`. |
|
|
516
|
-
|
|
517
|
-
### Auth Response Format
|
|
518
|
-
|
|
519
|
-
All login/register/OAuth endpoints return:
|
|
520
|
-
|
|
521
|
-
```json
|
|
522
|
-
{
|
|
523
|
-
"user": {
|
|
524
|
-
"uid": "uuid",
|
|
525
|
-
"email": "user@example.com",
|
|
526
|
-
"displayName": "John",
|
|
527
|
-
"photoURL": null,
|
|
528
|
-
"roles": ["member"],
|
|
529
|
-
"metadata": {}
|
|
530
|
-
},
|
|
531
|
-
"tokens": {
|
|
532
|
-
"accessToken": "eyJ...",
|
|
533
|
-
"refreshToken": "hex-string",
|
|
534
|
-
"accessTokenExpiresAt": 1700000000000
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
### Error Response Format
|
|
540
|
-
|
|
541
|
-
```json
|
|
542
|
-
{
|
|
543
|
-
"error": {
|
|
544
|
-
"message": "Invalid email or password",
|
|
545
|
-
"code": "INVALID_CREDENTIALS"
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
### Common Error Codes
|
|
551
|
-
|
|
552
|
-
| Code | HTTP | Description |
|
|
553
|
-
|---|---|---|
|
|
554
|
-
| `INVALID_CREDENTIALS` | 401 | Wrong email/password. |
|
|
555
|
-
| `INVALID_TOKEN` | 401 | Invalid or expired refresh/reset token. |
|
|
556
|
-
| `TOKEN_EXPIRED` | 401 | Refresh token has expired. |
|
|
557
|
-
| `REGISTRATION_DISABLED` | 403 | `allowRegistration` is `false`. |
|
|
558
|
-
| `EMAIL_EXISTS` | 409 | Email already registered. |
|
|
559
|
-
| `WEAK_PASSWORD` | 400 | Password fails strength validation. |
|
|
560
|
-
| `INVALID_INPUT` | 400 | Zod validation failure. |
|
|
561
|
-
| `EMAIL_NOT_CONFIGURED` | 503 | Email service not set up (password reset/verification unavailable). |
|
|
562
|
-
| `ALREADY_VERIFIED` | 400 | Email already verified. |
|
|
563
|
-
| `NOT_ANONYMOUS` | 400 | User is not anonymous (cannot link). |
|
|
564
|
-
| `RATE_LIMITED` | 429 | Too many requests. |
|
|
565
|
-
|
|
566
|
-
---
|
|
567
|
-
|
|
568
|
-
## Client SDK
|
|
569
|
-
|
|
570
|
-
The client SDK's `auth` module is created via `createAuth(transport, options?)`. It manages tokens, auto-refresh, session persistence, and state change listeners.
|
|
571
|
-
|
|
572
|
-
### CreateAuthOptions
|
|
573
|
-
|
|
574
|
-
| Option | Type | Default | Description |
|
|
575
|
-
|---|---|---|---|
|
|
576
|
-
| `storage` | `AuthStorage` | `localStorage` (browser) or in-memory | Token persistence backend. |
|
|
577
|
-
| `authPath` | `string` | `"/auth"` | Base path for auth endpoints. |
|
|
578
|
-
| `autoRefresh` | `boolean` | `true` | Auto-refresh access tokens 2 minutes before expiry. |
|
|
579
|
-
| `persistSession` | `boolean` | `true` | Persist session to storage between page loads. |
|
|
580
|
-
|
|
581
|
-
### Client SDK Methods
|
|
582
|
-
|
|
583
|
-
```typescript
|
|
584
|
-
const { auth } = createRebaseClient({ baseUrl: "http://localhost:3000" });
|
|
585
|
-
|
|
586
|
-
// Email/password
|
|
587
|
-
await auth.signInWithEmail(email, password);
|
|
588
|
-
await auth.signUp(email, password, displayName?);
|
|
589
|
-
|
|
590
|
-
// OAuth (all return { user, accessToken, refreshToken })
|
|
591
|
-
await auth.signInWithGoogle({ idToken });
|
|
592
|
-
await auth.signInWithGoogle({ accessToken });
|
|
593
|
-
await auth.signInWithGoogle({ code, redirectUri });
|
|
594
|
-
await auth.signInWithGitHub(code, redirectUri);
|
|
595
|
-
await auth.signInWithMicrosoft(code, redirectUri);
|
|
596
|
-
await auth.signInWithApple(code, redirectUri, user?);
|
|
597
|
-
await auth.signInWithFacebook(code, redirectUri);
|
|
598
|
-
await auth.signInWithTwitter(code, redirectUri, codeVerifier);
|
|
599
|
-
await auth.signInWithDiscord(code, redirectUri);
|
|
600
|
-
await auth.signInWithGitLab(code, redirectUri);
|
|
601
|
-
await auth.signInWithLinkedin(code, redirectUri);
|
|
602
|
-
await auth.signInWithBitbucket(code, redirectUri);
|
|
603
|
-
await auth.signInWithSlack(code, redirectUri);
|
|
604
|
-
await auth.signInWithSpotify(code, redirectUri);
|
|
605
|
-
await auth.signInWithOAuth(providerId, payload); // Generic
|
|
606
|
-
|
|
607
|
-
// Session
|
|
608
|
-
await auth.signOut();
|
|
609
|
-
await auth.refreshSession();
|
|
610
|
-
auth.getSession(); // Returns RebaseSession | null (sync)
|
|
611
|
-
|
|
612
|
-
// Profile
|
|
613
|
-
await auth.getUser(); // GET /auth/me
|
|
614
|
-
await auth.updateUser({ displayName?, photoURL? });
|
|
615
|
-
|
|
616
|
-
// Password
|
|
617
|
-
await auth.resetPasswordForEmail(email);
|
|
618
|
-
await auth.resetPassword(token, newPassword);
|
|
619
|
-
await auth.changePassword(oldPassword, newPassword);
|
|
620
|
-
|
|
621
|
-
// Email verification
|
|
622
|
-
await auth.sendVerificationEmail();
|
|
623
|
-
await auth.verifyEmail(token);
|
|
624
|
-
|
|
625
|
-
// Sessions
|
|
626
|
-
await auth.getSessions(); // List active sessions
|
|
627
|
-
await auth.revokeSession(sessionId);
|
|
628
|
-
await auth.revokeAllSessions(); // Revokes all + signs out locally
|
|
629
|
-
|
|
630
|
-
// Config
|
|
631
|
-
await auth.getAuthConfig(); // GET /auth/config
|
|
632
|
-
|
|
633
|
-
// State listener
|
|
634
|
-
const unsubscribe = auth.onAuthStateChange((event, session) => {
|
|
635
|
-
// event: "SIGNED_IN" | "SIGNED_OUT" | "TOKEN_REFRESHED" | "USER_UPDATED"
|
|
636
|
-
console.log(event, session?.user);
|
|
637
|
-
});
|
|
638
|
-
```
|
|
639
|
-
|
|
640
|
-
### Client Types
|
|
641
|
-
|
|
642
|
-
```typescript
|
|
643
|
-
interface RebaseUser {
|
|
644
|
-
uid: string;
|
|
645
|
-
email: string | null;
|
|
646
|
-
displayName: string | null;
|
|
647
|
-
photoURL: string | null;
|
|
648
|
-
emailVerified?: boolean;
|
|
649
|
-
roles?: string[];
|
|
650
|
-
providerId: string;
|
|
651
|
-
isAnonymous: boolean;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
interface RebaseSession {
|
|
655
|
-
accessToken: string;
|
|
656
|
-
refreshToken: string;
|
|
657
|
-
expiresAt: number; // Timestamp (ms)
|
|
658
|
-
user: RebaseUser;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
type AuthChangeEvent = "SIGNED_IN" | "SIGNED_OUT" | "TOKEN_REFRESHED" | "USER_UPDATED";
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
### Custom Storage Backends
|
|
665
|
-
|
|
666
|
-
```typescript
|
|
667
|
-
import { createMemoryStorage, createCookieStorage } from "@rebasepro/client";
|
|
668
|
-
|
|
669
|
-
// In-memory (Node.js / SSR)
|
|
670
|
-
const auth = createAuth(transport, {
|
|
671
|
-
storage: createMemoryStorage(),
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
// Cookie-based (SSR-friendly)
|
|
675
|
-
const auth = createAuth(transport, {
|
|
676
|
-
storage: createCookieStorage({
|
|
677
|
-
path: "/",
|
|
678
|
-
sameSite: "Lax",
|
|
679
|
-
secure: true,
|
|
680
|
-
domain: ".myapp.com",
|
|
681
|
-
maxAge: 365 * 24 * 60 * 60, // 1 year (default)
|
|
682
|
-
}),
|
|
683
|
-
});
|
|
684
|
-
```
|
|
685
|
-
|
|
686
|
-
### Session Restoration
|
|
687
|
-
|
|
688
|
-
On initialization (when `persistSession` is `true`):
|
|
689
|
-
1. Load stored session from storage.
|
|
690
|
-
2. If access token is still valid → restore session and schedule refresh.
|
|
691
|
-
3. If access token is expired but refresh token exists → immediately attempt refresh.
|
|
692
|
-
4. If refresh fails → clear session and emit `SIGNED_OUT`.
|
|
693
|
-
|
|
694
|
-
---
|
|
695
|
-
|
|
696
|
-
## Row-Level Security
|
|
697
|
-
|
|
698
|
-
Rebase implements RLS by scoping the DataDriver via `withAuth()` before each request. This injects the authenticated user's identity into the database context.
|
|
699
|
-
|
|
700
|
-
### How RLS Scoping Works
|
|
701
|
-
|
|
702
|
-
1. Auth middleware verifies the JWT (or API key / service key).
|
|
703
|
-
2. The middleware calls `scopeDataDriver(driver, { uid, roles })`.
|
|
704
|
-
3. If the driver supports `withAuth()` (e.g. Postgres), it returns a scoped clone with Postgres session variables set:
|
|
705
|
-
- `auth.uid()` — the user's ID
|
|
706
|
-
- `auth.jwt()` — the JWT claims
|
|
707
|
-
- `auth.roles()` — the user's role IDs
|
|
708
|
-
4. All subsequent queries in that request use the scoped driver with RLS policies applied.
|
|
709
|
-
|
|
710
|
-
### Fail-Closed Security
|
|
711
|
-
|
|
712
|
-
> **IMPORTANT FOR AGENTS:** If `withAuth()` throws an error, the request is **rejected** with 500. The system never falls back to unscoped access. This is by design (fail-closed).
|
|
713
|
-
|
|
714
|
-
### Anonymous Users
|
|
715
|
-
|
|
716
|
-
When `requireAuth` is `false` and no token is provided, the driver is scoped with:
|
|
717
|
-
- `uid: "anon"`
|
|
718
|
-
- `roles: ["anon"]`
|
|
719
|
-
|
|
720
|
-
This allows Postgres RLS policies to handle public access explicitly.
|
|
721
|
-
|
|
722
|
-
### Service Key Scoping
|
|
723
|
-
|
|
724
|
-
Requests with the `serviceKey` bypass RLS — they receive admin-level access with `uid: "service"` and `roles: ["admin"]`.
|
|
725
|
-
|
|
726
|
-
### API Key Scoping
|
|
727
|
-
|
|
728
|
-
API keys bypass RLS entirely. The driver is scoped using the key's `created_by` user with admin-like access, since API keys are meant for trusted server-to-server use.
|
|
729
|
-
|
|
730
|
-
---
|
|
731
|
-
|
|
732
|
-
## Rate Limiting
|
|
733
|
-
|
|
734
|
-
Rebase uses an in-memory sliding-window rate limiter with IP-based keying.
|
|
735
|
-
|
|
736
|
-
### Pre-configured Limiters
|
|
737
|
-
|
|
738
|
-
| Limiter | Window | Limit | Applied To |
|
|
739
|
-
|---|---|---|---|
|
|
740
|
-
| `defaultAuthLimiter` | 15 minutes | 200 requests | `/auth/register`, `/auth/login`, `/auth/{provider}`, `/auth/config` |
|
|
741
|
-
| `strictAuthLimiter` | 15 minutes | 50 requests | `/auth/forgot-password`, `/auth/reset-password`, `/auth/anonymous` |
|
|
742
|
-
|
|
743
|
-
### Rate Limit Response Headers
|
|
744
|
-
|
|
745
|
-
All rate-limited endpoints include:
|
|
746
|
-
|
|
747
|
-
| Header | Description |
|
|
748
|
-
|---|---|
|
|
749
|
-
| `X-RateLimit-Limit` | Maximum requests in the window. |
|
|
750
|
-
| `X-RateLimit-Remaining` | Remaining requests in current window. |
|
|
751
|
-
| `X-RateLimit-Reset` | Unix timestamp (seconds) when the window resets. |
|
|
752
|
-
| `Retry-After` | Seconds until the client can retry (only on 429). |
|
|
753
|
-
|
|
754
|
-
### API Key Rate Limiting
|
|
755
|
-
|
|
756
|
-
API keys have their own per-key rate limiter. The `rate_limit` on each key specifies requests per 15-minute window. When `rate_limit` is `null`, a default of 1000 requests per 15 minutes is applied.
|
|
757
|
-
|
|
758
|
-
### Rate Limit Error Response
|
|
759
|
-
|
|
760
|
-
```json
|
|
761
|
-
{
|
|
762
|
-
"error": {
|
|
763
|
-
"message": "Too many requests, please try again later.",
|
|
764
|
-
"code": "RATE_LIMITED"
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
```
|
|
768
|
-
|
|
769
|
-
### Custom Rate Limiter
|
|
770
|
-
|
|
771
|
-
```typescript
|
|
772
|
-
import { createRateLimiter } from "@rebasepro/server-core";
|
|
773
|
-
|
|
774
|
-
const myLimiter = createRateLimiter({
|
|
775
|
-
windowMs: 60 * 1000, // 1 minute
|
|
776
|
-
limit: 10, // 10 requests per minute
|
|
777
|
-
message: "Slow down!",
|
|
778
|
-
keyGenerator: (c) => {
|
|
779
|
-
// Custom keying (e.g., by user ID)
|
|
780
|
-
const user = c.get("user");
|
|
781
|
-
return user?.userId || c.req.header("x-forwarded-for") || "unknown";
|
|
782
|
-
},
|
|
783
|
-
});
|
|
784
|
-
```
|
|
785
|
-
|
|
786
|
-
---
|
|
787
|
-
|
|
788
|
-
## Custom Auth Adapters
|
|
789
|
-
|
|
790
|
-
For external auth systems (Clerk, Auth0, Firebase Auth, or custom JWT), use the `AuthAdapter` interface or the `createCustomAuthAdapter()` helper.
|
|
791
|
-
|
|
792
|
-
### AuthAdapter Interface
|
|
793
|
-
|
|
794
|
-
```typescript
|
|
795
|
-
interface AuthAdapter {
|
|
796
|
-
readonly id: string;
|
|
797
|
-
verifyRequest(request: Request): Promise<AuthenticatedUser | null>;
|
|
798
|
-
verifyToken?(token: string): Promise<AuthenticatedUser | null>;
|
|
799
|
-
userManagement?: UserManagementAdapter;
|
|
800
|
-
createAuthRoutes?(): Hono<any> | undefined;
|
|
801
|
-
createAdminRoutes?(): Hono<any> | undefined;
|
|
802
|
-
getCapabilities(): AuthAdapterCapabilities | Promise<AuthAdapterCapabilities>;
|
|
803
|
-
initialize?(): Promise<void>;
|
|
804
|
-
destroy?(): Promise<void>;
|
|
805
|
-
serviceKey?: string;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
interface AuthenticatedUser {
|
|
809
|
-
uid: string;
|
|
810
|
-
email: string;
|
|
811
|
-
displayName?: string | null;
|
|
812
|
-
photoUrl?: string | null;
|
|
813
|
-
roles: string[];
|
|
814
|
-
isAdmin: boolean;
|
|
815
|
-
rawToken?: string;
|
|
816
|
-
claims?: Record<string, unknown>;
|
|
817
|
-
}
|
|
818
|
-
```
|
|
819
|
-
|
|
820
|
-
### createCustomAuthAdapter
|
|
821
|
-
|
|
822
|
-
The simplest way to plug an existing auth system into Rebase. Only `verifyRequest` is required:
|
|
823
|
-
|
|
824
|
-
```typescript
|
|
825
|
-
import { createCustomAuthAdapter } from "@rebasepro/server-core";
|
|
826
|
-
import jwt from "jsonwebtoken";
|
|
827
|
-
|
|
828
|
-
const auth = createCustomAuthAdapter({
|
|
829
|
-
verifyRequest: async (request) => {
|
|
830
|
-
const token = request.headers.get("Authorization")?.replace("Bearer ", "");
|
|
831
|
-
if (!token) return null;
|
|
832
|
-
|
|
833
|
-
try {
|
|
834
|
-
const decoded = jwt.verify(token, MY_SECRET) as any;
|
|
835
|
-
return {
|
|
836
|
-
uid: decoded.sub,
|
|
837
|
-
email: decoded.email,
|
|
838
|
-
displayName: decoded.name,
|
|
839
|
-
roles: decoded.roles || [],
|
|
840
|
-
isAdmin: decoded.roles?.includes("admin") ?? false,
|
|
841
|
-
};
|
|
842
|
-
} catch {
|
|
843
|
-
return null;
|
|
844
|
-
}
|
|
845
|
-
},
|
|
846
|
-
|
|
847
|
-
// Optional: separate token verification for WebSocket auth
|
|
848
|
-
verifyToken: async (token) => {
|
|
849
|
-
// Same as above but receives just the token string
|
|
850
|
-
// Default: synthesizes a Request and calls verifyRequest
|
|
851
|
-
},
|
|
852
|
-
|
|
853
|
-
// Optional: enable user management in admin panel
|
|
854
|
-
userManagement: { ... },
|
|
855
|
-
|
|
856
|
-
// Optional: static service key
|
|
857
|
-
serviceKey: process.env.REBASE_SERVICE_KEY,
|
|
858
|
-
|
|
859
|
-
// Optional: override default capabilities
|
|
860
|
-
capabilities: {
|
|
861
|
-
emailPasswordLogin: false,
|
|
862
|
-
registration: false,
|
|
863
|
-
enabledProviders: ["google"],
|
|
864
|
-
},
|
|
865
|
-
});
|
|
866
|
-
|
|
867
|
-
// Pass to initializeRebaseBackend:
|
|
868
|
-
await initializeRebaseBackend({
|
|
869
|
-
server, app,
|
|
870
|
-
database: createPostgresAdapter({ connection: db, schema }),
|
|
871
|
-
auth, // AuthAdapter directly
|
|
872
|
-
});
|
|
873
|
-
```
|
|
874
|
-
|
|
875
|
-
### AuthAdapterCapabilities
|
|
876
|
-
|
|
877
|
-
The frontend reads these from `GET /api/auth/config` to dynamically show/hide UI:
|
|
878
|
-
|
|
879
|
-
```typescript
|
|
880
|
-
interface AuthAdapterCapabilities {
|
|
881
|
-
hasBuiltInAuthRoutes: boolean; // true for built-in, false for external
|
|
882
|
-
emailPasswordLogin: boolean;
|
|
883
|
-
registration: boolean;
|
|
884
|
-
passwordReset: boolean;
|
|
885
|
-
sessionManagement: boolean;
|
|
886
|
-
profileUpdate: boolean;
|
|
887
|
-
emailVerification: boolean;
|
|
888
|
-
enabledProviders: string[]; // e.g. ["google", "github"]
|
|
889
|
-
externalLoginUrl?: string; // Redirect URL for external auth
|
|
890
|
-
needsSetup?: boolean; // true when no users exist
|
|
891
|
-
registrationEnabled?: boolean;
|
|
892
|
-
}
|
|
893
|
-
```
|
|
894
|
-
|
|
895
|
-
### Default Capabilities for Custom Adapters
|
|
896
|
-
|
|
897
|
-
When using `createCustomAuthAdapter`, all capabilities default to `false`/`[]` unless overridden via `capabilities`.
|
|
898
|
-
|
|
899
|
-
---
|
|
900
|
-
|
|
901
|
-
## Roles & Permissions
|
|
902
|
-
|
|
903
|
-
### Role Data Structure
|
|
904
|
-
|
|
905
|
-
```typescript
|
|
906
|
-
interface RoleData {
|
|
907
|
-
id: string;
|
|
908
|
-
name: string;
|
|
909
|
-
isAdmin: boolean;
|
|
910
|
-
defaultPermissions: {
|
|
911
|
-
read?: boolean;
|
|
912
|
-
create?: boolean;
|
|
913
|
-
edit?: boolean;
|
|
914
|
-
delete?: boolean;
|
|
915
|
-
} | null;
|
|
916
|
-
collectionPermissions: Record<string, {
|
|
917
|
-
read?: boolean;
|
|
918
|
-
create?: boolean;
|
|
919
|
-
edit?: boolean;
|
|
920
|
-
delete?: boolean;
|
|
921
|
-
}> | null;
|
|
922
|
-
}
|
|
923
|
-
```
|
|
924
|
-
|
|
925
|
-
### Built-in Role Behavior
|
|
926
|
-
|
|
927
|
-
- The **first user** in the system is automatically assigned the `"admin"` role.
|
|
928
|
-
- Subsequent users get the `defaultRole` (if configured).
|
|
929
|
-
- Setting `defaultRole: "admin"` throws a startup error to prevent privilege escalation.
|
|
930
|
-
- Admin status is determined by having a role with `id === "admin"` or `id === "schema-admin"`.
|
|
931
|
-
|
|
932
|
-
### Admin Routes for User/Role Management
|
|
933
|
-
|
|
934
|
-
Admin user and role management is handled via dedicated admin routes (mounted under `/api/admin`) which require `requireAuth` + `requireAdmin` middleware.
|
|
935
|
-
|
|
936
|
-
---
|
|
937
|
-
|
|
938
|
-
## Backend Hooks
|
|
939
|
-
|
|
940
|
-
Backend hooks intercept data at the **API boundary** (after DB operations, before API responses). They are separate from auth hooks and collection-level `EntityCallbacks`.
|
|
941
|
-
|
|
942
|
-
### BackendHooks Interface
|
|
943
|
-
|
|
944
|
-
```typescript
|
|
945
|
-
interface BackendHooks {
|
|
946
|
-
users?: UserHooks;
|
|
947
|
-
data?: DataHooks;
|
|
948
|
-
}
|
|
949
|
-
```
|
|
950
|
-
|
|
951
|
-
### UserHooks (Admin User Management)
|
|
952
|
-
|
|
953
|
-
| Hook | Signature | Description |
|
|
954
|
-
|---|---|---|
|
|
955
|
-
| `afterRead` | `(user, context) => AdminUser \| null` | Transform user after DB read. Return `null` to hide. |
|
|
956
|
-
| `beforeSave` | `(data, context) => data` | Transform before write. Throw to abort. |
|
|
957
|
-
| `afterSave` | `(user, context) => void` | After user create/update. Side effects. |
|
|
958
|
-
| `beforeDelete` | `(userId, context) => void` | Throw to prevent deletion. |
|
|
959
|
-
| `afterDelete` | `(userId, context) => void` | After user deleted. |
|
|
960
|
-
|
|
961
|
-
### DataHooks (All Collection Entities)
|
|
962
|
-
|
|
963
|
-
| Hook | Signature | Description |
|
|
964
|
-
|---|---|---|
|
|
965
|
-
| `afterRead` | `(slug, entity, context) => entity \| null` | Transform entity after read. Return `null` to filter out. |
|
|
966
|
-
| `beforeSave` | `(slug, values, entityId, context) => values` | Transform before write. Throw to abort. |
|
|
967
|
-
| `afterSave` | `(slug, entity, context) => void` | After entity create/update. Side effects. |
|
|
968
|
-
| `beforeDelete` | `(slug, entityId, context) => void` | Throw to prevent deletion. |
|
|
969
|
-
| `afterDelete` | `(slug, entityId, context) => void` | After entity deleted. |
|
|
970
|
-
|
|
971
|
-
### BackendHookContext
|
|
972
|
-
|
|
973
|
-
```typescript
|
|
974
|
-
interface BackendHookContext {
|
|
975
|
-
requestUser?: { userId: string; roles: string[] };
|
|
976
|
-
method: "GET" | "POST" | "PUT" | "DELETE";
|
|
977
|
-
}
|
|
978
|
-
```
|
|
979
|
-
|
|
980
|
-
### Example: PII Masking
|
|
981
|
-
|
|
982
|
-
```typescript
|
|
983
|
-
const hooks: BackendHooks = {
|
|
984
|
-
data: {
|
|
985
|
-
afterRead(slug, entity, ctx) {
|
|
986
|
-
if (!ctx.requestUser?.roles.includes("admin") && entity.email) {
|
|
987
|
-
return { ...entity, email: "***" };
|
|
988
|
-
}
|
|
989
|
-
return entity;
|
|
990
|
-
},
|
|
991
|
-
},
|
|
992
|
-
users: {
|
|
993
|
-
afterRead(user, ctx) {
|
|
994
|
-
// Hide system users from admin panel
|
|
995
|
-
if (user.email.endsWith("@system.internal")) return null;
|
|
996
|
-
return user;
|
|
997
|
-
},
|
|
998
|
-
},
|
|
999
|
-
};
|
|
1000
|
-
|
|
1001
|
-
await initializeRebaseBackend({
|
|
1002
|
-
// ...
|
|
1003
|
-
hooks,
|
|
1004
|
-
});
|
|
1005
|
-
```
|
|
1006
|
-
|
|
1007
|
-
---
|
|
1008
|
-
|
|
1009
|
-
## Email Configuration
|
|
1010
|
-
|
|
1011
|
-
Email is required for password reset, email verification, and welcome emails. Configure via `auth.email`.
|
|
1012
|
-
|
|
1013
|
-
### EmailConfig
|
|
1014
|
-
|
|
1015
|
-
| Property | Type | Required | Description |
|
|
1016
|
-
|---|---|---|---|
|
|
1017
|
-
| `from` | `string` | Yes | Sender address (e.g. `"MyApp <noreply@myapp.com>"`). |
|
|
1018
|
-
| `smtp` | `SMTPConfig` | One of `smtp` or `sendEmail` | SMTP server configuration. |
|
|
1019
|
-
| `sendEmail` | `(options) => Promise<void>` | One of `smtp` or `sendEmail` | Custom email sending function (e.g. AWS SES, Resend SDK). |
|
|
1020
|
-
| `resetPasswordUrl` | `string` | No | Base URL for reset links: `{url}/reset-password?token=xxx`. |
|
|
1021
|
-
| `verifyEmailUrl` | `string` | No | Base URL for verification links: `{url}/verify-email?token=xxx`. |
|
|
1022
|
-
| `appName` | `string` | No | App name in email templates. Defaults to `"Rebase"`. |
|
|
1023
|
-
| `templates` | Object | No | Custom template functions (see below). |
|
|
1024
|
-
|
|
1025
|
-
### SMTPConfig
|
|
1026
|
-
|
|
1027
|
-
```typescript
|
|
1028
|
-
interface SMTPConfig {
|
|
1029
|
-
host: string;
|
|
1030
|
-
port: number;
|
|
1031
|
-
secure?: boolean;
|
|
1032
|
-
auth?: { user: string; pass: string };
|
|
1033
|
-
name?: string;
|
|
1034
|
-
}
|
|
1035
|
-
```
|
|
1036
|
-
|
|
1037
|
-
### Custom Email Templates
|
|
1038
|
-
|
|
1039
|
-
```typescript
|
|
1040
|
-
email: {
|
|
1041
|
-
from: "noreply@myapp.com",
|
|
1042
|
-
smtp: { host: "smtp.example.com", port: 587 },
|
|
1043
|
-
templates: {
|
|
1044
|
-
passwordReset: (resetUrl, user) => ({
|
|
1045
|
-
subject: "Reset your password",
|
|
1046
|
-
html: `<p>Hi ${user.displayName || user.email},</p><p><a href="${resetUrl}">Reset</a></p>`,
|
|
1047
|
-
text: `Reset your password: ${resetUrl}`,
|
|
1048
|
-
}),
|
|
1049
|
-
emailVerification: (verifyUrl, user) => ({
|
|
1050
|
-
subject: "Verify your email",
|
|
1051
|
-
html: `<a href="${verifyUrl}">Verify</a>`,
|
|
1052
|
-
}),
|
|
1053
|
-
welcomeEmail: (user, appName) => ({
|
|
1054
|
-
subject: `Welcome to ${appName}!`,
|
|
1055
|
-
html: `<p>Welcome, ${user.displayName || user.email}!</p>`,
|
|
1056
|
-
}),
|
|
1057
|
-
userInvitation: (setPasswordUrl, user) => ({
|
|
1058
|
-
subject: "You've been invited",
|
|
1059
|
-
html: `<p>Set your password: <a href="${setPasswordUrl}">here</a></p>`,
|
|
1060
|
-
}),
|
|
1061
|
-
},
|
|
1062
|
-
}
|
|
1063
|
-
```
|
|
1064
|
-
|
|
1065
|
-
### Custom Email Provider (Non-SMTP)
|
|
1066
|
-
|
|
1067
|
-
```typescript
|
|
1068
|
-
import { Resend } from "resend";
|
|
1069
|
-
const resend = new Resend(process.env.RESEND_API_KEY);
|
|
1070
|
-
|
|
1071
|
-
email: {
|
|
1072
|
-
from: "noreply@myapp.com",
|
|
1073
|
-
sendEmail: async (options) => {
|
|
1074
|
-
await resend.emails.send({
|
|
1075
|
-
from: options.from || "noreply@myapp.com",
|
|
1076
|
-
to: options.to,
|
|
1077
|
-
subject: options.subject,
|
|
1078
|
-
html: options.html,
|
|
1079
|
-
text: options.text,
|
|
1080
|
-
});
|
|
1081
|
-
},
|
|
1082
|
-
appName: "MyApp",
|
|
1083
|
-
resetPasswordUrl: "https://myapp.com",
|
|
1084
|
-
}
|
|
1085
|
-
```
|
|
1086
|
-
|
|
1087
|
-
---
|
|
1088
|
-
|
|
1089
|
-
## Security Concepts
|
|
1090
|
-
|
|
1091
|
-
### Service Key
|
|
1092
|
-
|
|
1093
|
-
A static secret for server-to-server authentication. Generate with:
|
|
1094
|
-
```bash
|
|
1095
|
-
node -e "console.log(require('crypto').randomBytes(48).toString('base64'))"
|
|
1096
|
-
```
|
|
1097
|
-
|
|
1098
|
-
When a request includes `Authorization: Bearer <serviceKey>`:
|
|
1099
|
-
- It bypasses JWT verification.
|
|
1100
|
-
- It receives admin-level access (`uid: "service"`, `roles: ["admin"]`).
|
|
1101
|
-
- Comparison is done with constant-time comparison to prevent timing attacks.
|
|
1102
|
-
- Must be ≥ 32 characters (validated at startup).
|
|
1103
|
-
|
|
1104
|
-
### Token Rotation
|
|
1105
|
-
|
|
1106
|
-
Refresh tokens are rotated on every use:
|
|
1107
|
-
1. Client sends refresh token to `POST /auth/refresh`.
|
|
1108
|
-
2. Server deletes the old refresh token and creates a new one.
|
|
1109
|
-
3. New access + refresh tokens are returned.
|
|
1110
|
-
|
|
1111
|
-
### Password Reset Security
|
|
1112
|
-
|
|
1113
|
-
- `POST /auth/forgot-password` always returns success (doesn't reveal whether email exists).
|
|
1114
|
-
- Reset tokens are stored as SHA-256 hashes.
|
|
1115
|
-
- Tokens expire in 1 hour.
|
|
1116
|
-
- After password reset, **all sessions are invalidated** (all refresh tokens deleted).
|
|
1117
|
-
|
|
1118
|
-
### Zod Input Validation
|
|
1119
|
-
|
|
1120
|
-
All auth endpoints validate input with Zod schemas:
|
|
1121
|
-
|
|
1122
|
-
| Field | Validation |
|
|
1123
|
-
|---|---|
|
|
1124
|
-
| `email` | Valid email, max 255 chars |
|
|
1125
|
-
| `password` | Min 1 char, max 128 chars |
|
|
1126
|
-
| `displayName` | Max 255 chars |
|
|
1127
|
-
| `photoURL` | Valid URL, max 2048 chars |
|
|
1128
|
-
| `refreshToken` | Min 1 char |
|
|
1129
|
-
|
|
1130
|
-
---
|
|
1131
|
-
|
|
1132
|
-
## References
|
|
1133
|
-
|
|
1134
|
-
- Source: `packages/server-core/src/auth/` — All auth implementation
|
|
1135
|
-
- Source: `packages/server-core/src/auth/routes.ts` — REST auth endpoints
|
|
1136
|
-
- Source: `packages/server-core/src/auth/auth-hooks.ts` — Lifecycle hooks
|
|
1137
|
-
- Source: `packages/server-core/src/auth/api-keys/` — API key system
|
|
1138
|
-
- Source: `packages/server-core/src/auth/rate-limiter.ts` — Rate limiting
|
|
1139
|
-
- Source: `packages/server-core/src/init.ts` — `RebaseAuthConfig` and backend init
|
|
1140
|
-
- Source: `packages/client/src/auth.ts` — Client SDK auth module
|
|
1141
|
-
- Source: `packages/types/src/types/auth_adapter.ts` — `AuthAdapter` interface
|
|
1142
|
-
- Source: `packages/server-core/src/auth/rls-scope.ts` — RLS scoping
|
|
1143
|
-
- Source: `packages/server-core/src/email/types.ts` — Email configuration
|