@mindstudio-ai/remy 0.1.268 → 0.1.270
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 +10 -18
- package/dist/headless.js +166 -220
- package/dist/index.js +140 -181
- package/dist/prompt/compiled/auth.md +20 -415
- package/dist/prompt/compiled/design.md +4 -2
- package/dist/prompt/compiled/dev-and-deploy.md +1 -8
- package/dist/prompt/compiled/interfaces.md +9 -27
- package/dist/prompt/compiled/methods.md +3 -37
- package/dist/prompt/compiled/msfm.md +1 -16
- package/dist/prompt/compiled/platform.md +7 -17
- package/dist/prompt/skills/auth.md +443 -0
- package/dist/prompt/{compiled → skills}/files.md +6 -0
- package/dist/prompt/{compiled → skills}/scenarios.md +6 -0
- package/dist/prompt/{compiled → skills}/secrets.md +6 -0
- package/dist/prompt/skills/voiceInterfaces.md +30 -10
- package/dist/prompt/static/authoring.md +1 -1
- package/dist/prompt/static/coding.md +4 -2
- package/dist/prompt/static/intake.md +1 -4
- package/dist/prompt/static/spec-maintenance.md +41 -0
- package/dist/prompt/static/team.md +1 -1
- package/package.json +1 -1
|
@@ -1,437 +1,42 @@
|
|
|
1
1
|
# Auth
|
|
2
2
|
|
|
3
|
-
Remy apps can have and manage their own users. Auth is opt-in: configure it in the manifest, define a user table, and build your own login UI. The platform handles verification codes,
|
|
3
|
+
Remy apps can have and manage their own users. Auth is opt-in: configure it in the manifest, define a user table, and build your own login UI. The platform handles verification codes, cookie sessions, and role enforcement. Apps without auth config use anonymous guest sessions — the default, and fine for single-user apps, internal utilities, and simple tools.
|
|
4
4
|
|
|
5
5
|
**Auth is optional.** Many apps don't need it. Only add auth when the app needs to identify users or restrict access.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Four auth methods, combinable per app in the manifest:
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"methods": ["email-code", "sms-code"],
|
|
14
|
-
"table": {
|
|
15
|
-
"name": "users",
|
|
16
|
-
"columns": {
|
|
17
|
-
"email": "email",
|
|
18
|
-
"phone": "phone",
|
|
19
|
-
"roles": "roles",
|
|
20
|
-
"apiKey": "apiKey"
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
"roles": [
|
|
25
|
-
{ "id": "vendor", "name": "Vendor" },
|
|
26
|
-
{ "id": "buyer", "name": "Buyer" },
|
|
27
|
-
{ "id": "admin", "name": "Admin" }
|
|
28
|
-
]
|
|
29
|
-
}
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
- **`auth.enabled`** — opt-in. No auth config = anonymous guest sessions.
|
|
33
|
-
- **`auth.methods`** — which verification methods the app supports. At least one required.
|
|
34
|
-
- `email-code` — 6-digit code sent via email
|
|
35
|
-
- `sms-code` — 6-digit code sent via SMS
|
|
36
|
-
- `api-key` — programmatic access via `Authorization: Bearer sk_...` header. Resolves to a user with full RBAC.
|
|
37
|
-
- `remy` — platform-delegated sign-in ("Sign in with Remy"). The platform resolves who the user is; roles and verification are platform-managed. Only usable when the app's owning organization has it enabled — the `<org_context>` block signals availability. See *Organization-Managed Sign-In* above.
|
|
38
|
-
- **`auth.table.name`** — name of the `defineTable` table that holds user records.
|
|
39
|
-
- **`auth.table.columns`** — maps platform-managed fields to column names in the developer's table.
|
|
40
|
-
- `email` — required if `email-code` is in methods
|
|
41
|
-
- `phone` — required if `sms-code` is in methods
|
|
42
|
-
- `roles` — optional. Maps to a JSON array column for role assignments.
|
|
43
|
-
- `apiKey` — optional. Required if `api-key` is in methods. Platform stores masked value (`sk_...xxxx`) for display; one key per user.
|
|
44
|
-
- **`roles`** — declares valid roles for the app. Same as before: `id`, `name`, optional `description`.
|
|
9
|
+
- `email-code` — 6-digit code sent via email. The natural default for business/desktop apps.
|
|
10
|
+
- `sms-code` — 6-digit code sent via SMS. The natural default for consumer/mobile apps.
|
|
11
|
+
- `api-key` — programmatic access via `Authorization: Bearer sk_...`; resolves to a user with full RBAC.
|
|
12
|
+
- `remy` — org-delegated "Sign in with Remy" (see below). Internal apps only; org-gated.
|
|
45
13
|
|
|
46
|
-
|
|
14
|
+
**Before writing any auth code — the manifest `auth` config, the user table, login/signup UI, frontend `auth.*` calls, API keys — load the `auth` skill.** It has the full contract: config schema, platform-managed columns, the frontend SDK (flows, auth state, error codes, phone/email helpers), delegated sign-in implementation, worked examples, and the auth-screen design rules.
|
|
47
15
|
|
|
48
|
-
|
|
16
|
+
## Backend Enforcement
|
|
49
17
|
|
|
50
18
|
```typescript
|
|
51
|
-
import {
|
|
52
|
-
|
|
53
|
-
export const Users = db.defineTable<{
|
|
54
|
-
// Mapped to auth — platform keeps these in sync
|
|
55
|
-
email: string;
|
|
56
|
-
phone?: string;
|
|
57
|
-
roles: string[];
|
|
58
|
-
apiKey?: string; // masked value (sk_...xxxx), read-only from code
|
|
59
|
-
// Developer's own fields
|
|
60
|
-
displayName: string;
|
|
61
|
-
plan: 'free' | 'pro';
|
|
62
|
-
avatarUrl?: string;
|
|
63
|
-
}>('users');
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### Platform-Managed Column Behavior
|
|
67
|
-
|
|
68
|
-
- **`email` / `phone` / `apiKey`** — read-only from code. Writing via `update()` or `push()` throws a `MindStudioError`. Use the auth API to change a user's email or phone, and `auth.createApiKey()` / `auth.revokeApiKey()` for API keys.
|
|
69
|
-
- **`roles`** — read/write from both code and the dashboard. `Users.update(userId, { roles: ['admin'] })` works and syncs to the platform. Dashboard role changes sync back to the table.
|
|
70
|
-
- All other columns are fully the developer's. When auth creates a user row, only the managed columns (email/phone, roles) are populated. All user-defined columns start as null until the user completes onboarding — type them as optional and guard against null.
|
|
71
|
-
|
|
72
|
-
## Frontend Auth (Interface SDK)
|
|
73
|
-
|
|
74
|
-
The developer builds their own login/signup UI. The SDK provides methods that handle the verification flow and session management.
|
|
75
|
-
|
|
76
|
-
```typescript
|
|
77
|
-
import { auth } from '@mindstudio-ai/interface';
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### User Shape
|
|
81
|
-
|
|
82
|
-
```typescript
|
|
83
|
-
interface AppUser {
|
|
84
|
-
id: string;
|
|
85
|
-
email: string | null;
|
|
86
|
-
phone: string | null;
|
|
87
|
-
roles: string[];
|
|
88
|
-
apiKey: string | null; // masked value (sk_...xxxx), null if no key
|
|
89
|
-
provider?: 'remy' | null; // 'remy' = delegated sign-in (platform-managed); null/absent = app-verified
|
|
90
|
-
createdAt: string;
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
`auth.getCurrentUser()` returns `AppUser | null`. `null` means unauthenticated.
|
|
95
|
-
|
|
96
|
-
### State
|
|
97
|
-
|
|
98
|
-
```typescript
|
|
99
|
-
auth.getCurrentUser() // AppUser | null
|
|
100
|
-
auth.currentUser // AppUser | null (sync getter, same as getCurrentUser())
|
|
101
|
-
auth.isAuthenticated() // boolean
|
|
102
|
-
auth.authStatus // 'authenticating' | 'authenticated' | 'unauthenticated' (sync getter)
|
|
103
|
-
auth.onAuthStateChanged(cb) // fires immediately with current user, then on every transition —
|
|
104
|
-
// verify/confirm/logout AND each authStatus change (a Sign in with Remy
|
|
105
|
-
// handshake starting or settling). During 'authenticating' it may fire
|
|
106
|
-
// null; that's expected — read authStatus to tell it apart from
|
|
107
|
-
// logged-out. Returns an unsubscribe function.
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
Use `onAuthStateChanged` in React instead of reading `currentUser` once at render time:
|
|
111
|
-
|
|
112
|
-
```typescript
|
|
113
|
-
function useAuth() {
|
|
114
|
-
const [user, setUser] = useState<AppUser | null>(null);
|
|
115
|
-
useEffect(() => auth.onAuthStateChanged(setUser), []);
|
|
116
|
-
return user;
|
|
117
|
-
}
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### Email Code Flow
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
const { verificationId } = await auth.sendEmailCode('user@example.com');
|
|
124
|
-
// User enters the 6-digit code from their email
|
|
125
|
-
const user = await auth.verifyEmailCode(verificationId, '123456');
|
|
126
|
-
// user is now authenticated — auth.getCurrentUser() returns the AppUser
|
|
19
|
+
import { auth } from '@mindstudio-ai/agent';
|
|
127
20
|
```
|
|
128
21
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const user = await auth.verifySmsCode(verificationId, '123456');
|
|
134
|
-
```
|
|
22
|
+
- **`auth.userId`** — the current user's ID (row ID in the auth table), or `null` for unauthenticated requests. Check for null before using in queries when the method might be called without auth.
|
|
23
|
+
- **`auth.requireRole(...roles)`** — throws 401 if unauthenticated, 403 if the user has none of the listed roles (OR semantics). `auth.hasRole(...roles)` is the non-throwing boolean form. `auth.roles` is the current user's role array.
|
|
24
|
+
- **Require login: check `auth.userId`. Roles are RBAC** — only declare roles that map to real business distinctions (vendor/buyer/admin), and only check them when behavior should differ. Newly verified users have `roles: []` until your code assigns them.
|
|
25
|
+
- **System role:** when the platform invokes a method on behalf of the app (cron, webhook, email), execution runs with `auth.roles: ['system']` — use `auth.requireRole('system')` to restrict a method to platform triggers. Web frontend, API, and agent calls run as the authenticated user and never get the system role unless explicitly assigned.
|
|
135
26
|
|
|
136
27
|
## Organization-Managed Sign-In ("Sign in with Remy")
|
|
137
28
|
|
|
138
|
-
Some apps are owned by an organization that centralizes sign-in.
|
|
29
|
+
Some apps are owned by an organization that centralizes sign-in. The `<org_context>` block (near the end of this prompt) states the facts; here is how to act on them:
|
|
139
30
|
|
|
140
|
-
- **When it says "Sign in with Remy" is available** — offer delegated sign-in: a
|
|
31
|
+
- **When it says "Sign in with Remy" is available** — offer delegated sign-in: a "Continue with {Org}" button (exact organization name from the block). For an org-owned app this is usually the primary sign-in — members already have platform identities, so a verification-code form is redundant.
|
|
141
32
|
- **When it says the organization requires delegated sign-in** — `remy` is the *only* human method: do not add `email-code` or `sms-code`. Those are blocked at the platform edge for the org's apps, so building them yields a login that can't work.
|
|
33
|
+
- **When the block is absent, or has no delegated-sign-in line** (the common case) — do not build or offer it. It's an enterprise scheme for internal apps only.
|
|
34
|
+
- Delegated users' **roles and email are platform-managed** — enforce with `requireRole`/`hasRole` as usual, but never assign their roles from app code.
|
|
142
35
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
```typescript
|
|
146
|
-
// "Continue with {Org}" button — must be triggered by a user gesture (click).
|
|
147
|
-
<button onClick={() => auth.signInWithRemy()}>Continue with Acme</button>
|
|
148
|
-
|
|
149
|
-
// On return from the handshake (or when opened from the Remy dashboard), the app
|
|
150
|
-
// cold-loads with sign-in still completing. Track authStatus and render a
|
|
151
|
-
// "Completing sign-in…" state — NOT the login screen — while it's 'authenticating'.
|
|
152
|
-
const [user, setUser] = useState(auth.currentUser);
|
|
153
|
-
const [status, setStatus] = useState(auth.authStatus);
|
|
154
|
-
useEffect(() => auth.onAuthStateChanged(() => {
|
|
155
|
-
setUser(auth.currentUser);
|
|
156
|
-
setStatus(auth.authStatus);
|
|
157
|
-
}), []);
|
|
158
|
-
useEffect(() => { auth.handleRemyRedirect().catch(() => {}); }, []); // no-op if nothing to redeem
|
|
159
|
-
// status === 'authenticating' → <CompletingSignIn/>; user → app; else → login
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
- `auth.signInWithRemy(options?)` → `Promise<AppUser | null>`. Auto-detects context: a top-level app redirects to the platform and back — **the page navigates away, so the promise never settles; don't await it to gate UI**. An app embedded in a cross-origin iframe (the dev IDE preview) uses a popup and the promise resolves with the user (or `null` if the popup is closed). Options: `redirectUri` (default current URL), `state` (CSRF, auto-generated), `mode: 'auto' | 'popup' | 'redirect'` (default `'auto'` — leave it). Because of the redirect case, **drive UI off `onAuthStateChanged`, not the return value.**
|
|
163
|
-
- `auth.handleRemyRedirect()` → `Promise<AppUser | null>`. Call once on load. Handles both the "Continue with {Org}" return and being opened from the Remy dashboard; on success it updates the session in-place (fires `onAuthStateChanged`) and cleans the URL.
|
|
164
|
-
- **Render the completing state, not the login screen, on return.** A redirect return (or dashboard launch) cold-loads with `auth.authStatus === 'authenticating'` — set *before first paint* and held until the exchange settles. Gate the UI on `authStatus`: `'authenticating'` → a brief "Completing sign-in…" state; a user → the app; `'unauthenticated'` → the login screen. Don't infer this from the URL `?code` (it's stripped when redemption starts) or treat the initial `null` user as logged-out — that flashes the login screen over a successful sign-in.
|
|
165
|
-
- Delegated users have `provider: 'remy'`. Their **roles and email are platform-managed** (like `email`/`phone` for code users) — enforce access with `requireRole`/`hasRole` on the backend as usual, but don't assign roles from app code; the platform owns them.
|
|
166
|
-
|
|
167
|
-
### Email/Phone Changes (must be authenticated)
|
|
168
|
-
|
|
169
|
-
```typescript
|
|
170
|
-
await auth.requestEmailChange('newemail@example.com');
|
|
171
|
-
const user = await auth.confirmEmailChange('newemail@example.com', '123456');
|
|
172
|
-
|
|
173
|
-
await auth.requestPhoneChange('+15559876543');
|
|
174
|
-
const user = await auth.confirmPhoneChange('+15559876543', '123456');
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### Logout
|
|
36
|
+
## Designing Auth Into the Experience
|
|
178
37
|
|
|
179
|
-
|
|
180
|
-
await auth.logout(); // clears session
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
### API Keys
|
|
184
|
-
|
|
185
|
-
For apps with `api-key` in their auth methods. API keys resolve to a user with the same `auth.userId`, `auth.roles`, and `requireRole()` enforcement as a cookie session.
|
|
186
|
-
|
|
187
|
-
```typescript
|
|
188
|
-
// Generate a key for the current user (must be logged in)
|
|
189
|
-
const { key } = await auth.createApiKey();
|
|
190
|
-
// key = "sk_..." — show once, not stored. user.apiKey updates to masked value.
|
|
191
|
-
|
|
192
|
-
// Revoke current user's key
|
|
193
|
-
await auth.revokeApiKey();
|
|
194
|
-
// user.apiKey becomes null
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
Both methods fire `onAuthStateChanged` since they modify the user object. One key per user — creating a new key replaces the old one.
|
|
198
|
-
|
|
199
|
-
Consumers use the key as a Bearer token: `Authorization: Bearer sk_...`. The platform resolves it to the user, populates the auth context, and executes the method normally. Invalid or revoked key: 401.
|
|
200
|
-
|
|
201
|
-
### Error Codes
|
|
202
|
-
|
|
203
|
-
All auth methods throw on failure with a `code` property:
|
|
204
|
-
|
|
205
|
-
| Code | HTTP | Meaning |
|
|
206
|
-
|------|------|---------|
|
|
207
|
-
| `rate_limited` | 429 | Too many requests |
|
|
208
|
-
| `invalid_code` | 400 | Wrong verification code |
|
|
209
|
-
| `verification_expired` | 400 | Code has expired |
|
|
210
|
-
| `max_attempts_exceeded` | 400 | Too many failed attempts |
|
|
211
|
-
| `not_authenticated` | 401 | No active session |
|
|
212
|
-
| `invalid_session` | 401 | Session expired or invalid |
|
|
213
|
-
| `not_supported` | 400 | Feature not enabled for this app (e.g. API keys without `api-key` in methods) |
|
|
214
|
-
| `invalid_state` | 400 | Sign in with Remy: returned CSRF state didn't match (stale/replayed redirect) |
|
|
215
|
-
| `popup_blocked` | — | Sign in with Remy (embedded): popup was blocked — prompt to allow popups and retry |
|
|
216
|
-
| `signin_timeout` | — | Sign in with Remy (embedded): popup didn't complete in time |
|
|
217
|
-
| `auth_required` | 401 | Agent/voice interface requires an authenticated user (interface `auth` block) |
|
|
218
|
-
| `role_required` | 403 | Agent/voice interface requires a role the user doesn't hold |
|
|
219
|
-
|
|
220
|
-
### Phone Helpers
|
|
221
|
-
|
|
222
|
-
```typescript
|
|
223
|
-
auth.phone.countries // ~180 countries with { code, dialCode, name, flag }
|
|
224
|
-
// Key selects by country code (US, CA, BB), not dial code — multiple countries share +1
|
|
225
|
-
auth.phone.detectCountry() // guess from timezone, e.g. 'US'
|
|
226
|
-
auth.phone.toE164('5551234567', 'US') // '+15551234567'
|
|
227
|
-
auth.phone.format('+15551234567') // '+1 (555) 123-4567'
|
|
228
|
-
auth.phone.isValid('+15551234567') // true
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
### Email Helpers
|
|
232
|
-
|
|
233
|
-
```typescript
|
|
234
|
-
auth.email.isValid('user@example.com') // true
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
## Backend Auth (Agent SDK)
|
|
238
|
-
|
|
239
|
-
```typescript
|
|
240
|
-
import { auth } from '@mindstudio-ai/agent';
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
### `auth.requireRole(...roles)`
|
|
244
|
-
|
|
245
|
-
Throws 401 (unauthenticated) if there is no current user. Throws 403 (forbidden) if the current user doesn't have **any** of the specified roles.
|
|
246
|
-
|
|
247
|
-
```typescript
|
|
248
|
-
auth.requireRole('admin');
|
|
249
|
-
auth.requireRole('admin', 'approver'); // any of these
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
**Require login: check `auth.userId`. Roles are RBAC** — only declare roles that map to real business distinctions (vendor/buyer/admin), and only check them when behavior should differ. Newly verified users have `roles: []` until your code assigns them.
|
|
253
|
-
|
|
254
|
-
### `auth.hasRole(...roles)`
|
|
255
|
-
|
|
256
|
-
Returns `boolean`. Same logic as `requireRole` but doesn't throw.
|
|
257
|
-
|
|
258
|
-
### `auth.userId`
|
|
259
|
-
|
|
260
|
-
The current user's ID (the row ID in the auth table), or `null` for unauthenticated requests. Check for null before using in database queries when the method might be called without auth.
|
|
261
|
-
|
|
262
|
-
### `auth.roles`
|
|
263
|
-
|
|
264
|
-
Array of role IDs assigned to the current user.
|
|
265
|
-
|
|
266
|
-
### `auth.getUsersByRole(role)`
|
|
267
|
-
|
|
268
|
-
Returns an array of user IDs with the specified role.
|
|
269
|
-
|
|
270
|
-
### System Role (Platform Triggers)
|
|
271
|
-
|
|
272
|
-
When the platform invokes a method on behalf of the app (cron, webhook, email), the execution runs as a system user with `auth.roles: ['system']`. Use `auth.requireRole('system')` to restrict methods to platform triggers only:
|
|
273
|
-
|
|
274
|
-
```typescript
|
|
275
|
-
export async function regenerateCache(input: {}) {
|
|
276
|
-
auth.requireRole('system');
|
|
277
|
-
// Only cron, webhooks, and users with 'system' role can reach this
|
|
278
|
-
}
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
Web frontend calls (`/_/methods`), API interface calls (`/_/api`), and agent chat all run as the authenticated user — they don't get the system role unless the user has been explicitly assigned it. You can assign `system` to app users via the dashboard or SDK if they need to manually trigger these methods.
|
|
282
|
-
|
|
283
|
-
## Login Page Example
|
|
284
|
-
|
|
285
|
-
```tsx
|
|
286
|
-
import { useState, useEffect } from 'react';
|
|
287
|
-
import { auth } from '@mindstudio-ai/interface';
|
|
288
|
-
import { useLocation } from 'wouter';
|
|
289
|
-
|
|
290
|
-
function useAuth() {
|
|
291
|
-
const [user, setUser] = useState<AppUser | null>(null);
|
|
292
|
-
useEffect(() => auth.onAuthStateChanged(setUser), []);
|
|
293
|
-
return user;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function LoginPage() {
|
|
297
|
-
const user = useAuth();
|
|
298
|
-
const [, navigate] = useLocation();
|
|
299
|
-
const [email, setEmail] = useState('');
|
|
300
|
-
const [code, setCode] = useState('');
|
|
301
|
-
const [verificationId, setVerificationId] = useState('');
|
|
302
|
-
const [error, setError] = useState('');
|
|
303
|
-
|
|
304
|
-
// Redirect when authenticated (fires via onAuthStateChanged after verify)
|
|
305
|
-
useEffect(() => { if (user) navigate('/dashboard'); }, [user]);
|
|
306
|
-
|
|
307
|
-
const handleSendCode = async () => {
|
|
308
|
-
try {
|
|
309
|
-
const { verificationId } = await auth.sendEmailCode(email);
|
|
310
|
-
setVerificationId(verificationId);
|
|
311
|
-
setError('');
|
|
312
|
-
} catch (err: any) {
|
|
313
|
-
setError(err.code === 'rate_limited' ? 'Too many attempts. Try again later.' : err.message);
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
const handleVerify = async () => {
|
|
318
|
-
try {
|
|
319
|
-
await auth.verifyEmailCode(verificationId, code);
|
|
320
|
-
// onAuthStateChanged fires, useAuth updates, redirect happens
|
|
321
|
-
} catch (err: any) {
|
|
322
|
-
if (err.code === 'invalid_code') setError('Wrong code. Try again.');
|
|
323
|
-
else if (err.code === 'verification_expired') setError('Code expired. Request a new one.');
|
|
324
|
-
else if (err.code === 'max_attempts_exceeded') setError('Too many attempts. Request a new code.');
|
|
325
|
-
else setError(err.message);
|
|
326
|
-
}
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
if (!verificationId) {
|
|
330
|
-
return (
|
|
331
|
-
<div>
|
|
332
|
-
<h1>Sign in</h1>
|
|
333
|
-
<input placeholder="Email" value={email} onChange={e => setEmail(e.target.value)} />
|
|
334
|
-
<button onClick={handleSendCode}>Send code</button>
|
|
335
|
-
{error && <p>{error}</p>}
|
|
336
|
-
</div>
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return (
|
|
341
|
-
<div>
|
|
342
|
-
<p>Enter the code we sent to {email}</p>
|
|
343
|
-
<input placeholder="123456" value={code} onChange={e => setCode(e.target.value)} />
|
|
344
|
-
<button onClick={handleVerify}>Verify</button>
|
|
345
|
-
<button onClick={() => setVerificationId('')}>Resend</button>
|
|
346
|
-
{error && <p>{error}</p>}
|
|
347
|
-
</div>
|
|
348
|
-
);
|
|
349
|
-
}
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
## Backend Method Example
|
|
353
|
-
|
|
354
|
-
```typescript
|
|
355
|
-
import { auth } from '@mindstudio-ai/agent';
|
|
356
|
-
import { Users } from './tables/users';
|
|
357
|
-
|
|
358
|
-
export async function getDashboard() {
|
|
359
|
-
const user = auth.userId ? await Users.get(auth.userId) : null;
|
|
360
|
-
|
|
361
|
-
if (auth.hasRole('admin')) {
|
|
362
|
-
const allUsers = await Users.toArray();
|
|
363
|
-
return { user, allUsers, isAdmin: true };
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return { user, isAdmin: false };
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
export async function promoteToAdmin(input: { userId: string }) {
|
|
370
|
-
auth.requireRole('admin');
|
|
371
|
-
await Users.update(input.userId, { roles: ['admin'] });
|
|
372
|
-
}
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
## Roles
|
|
376
|
-
|
|
377
|
-
Roles are declared in the manifest, stored as an array column on the user table, and enforced in backend methods. The platform manages role data across the user table and the dashboard.
|
|
378
|
-
|
|
379
|
-
- Declare roles in `mindstudio.json` with `id` and `name`
|
|
380
|
-
- The mapped `roles` column holds a JSON array of role ID strings: `["vendor", "admin"]`
|
|
381
|
-
- Writable from code: `Users.update(userId, { roles: ['admin'] })` — platform syncs automatically
|
|
382
|
-
- Writable from dashboard: Remy dashboard shows app users and their roles
|
|
383
|
-
- Backend enforcement: `auth.requireRole('admin')` works as before
|
|
384
|
-
|
|
385
|
-
## Interface-Level Auth (Agent + Voice)
|
|
386
|
-
|
|
387
|
-
Agent and voice interfaces additionally declare auth **in their config** (a required `auth` key:
|
|
388
|
-
`{ "requireUser": boolean, "requireRole"?: string[] }`) because those sessions spend money without
|
|
389
|
-
necessarily calling a backend method — the platform gates the lobby itself, before any model or
|
|
390
|
-
media spend. `requireRole` uses the same manifest role ids with OR semantics. Denials reach the
|
|
391
|
-
frontend SDK as `MindStudioInterfaceError` codes `auth_required` (401) and `role_required` (403) —
|
|
392
|
-
route them to the app's login flow. See the `agentInterfaces` / `voiceInterfaces` skills for the
|
|
393
|
-
full contract. Method-level `auth.requireRole(...)` checks still apply to every tool call inside.
|
|
394
|
-
|
|
395
|
-
## Apps Without Auth
|
|
396
|
-
|
|
397
|
-
Apps without `auth` in the manifest use anonymous guest sessions. No login, no user identity, no roles. This is the default and works fine for single-user apps, internal tools, and simple utilities. (Agent/voice interfaces on such apps declare `"auth": { "requireUser": false }` explicitly — anonymous callers are scoped by a per-browser visitor identity.)
|
|
398
|
-
|
|
399
|
-
## Important: Designing Auth in Web Interfaces
|
|
400
|
-
|
|
401
|
-
The most imporant user experience consideration with auth is that authentication moments must feel natural and intuitive - they should not feel jarring or surprising. Take care to integrate them into the entire experience when building.
|
|
402
|
-
|
|
403
|
-
For the overwhelming majority of apps, a user should never land on auth at the root of an app when opening it for the first time (except in cases where the app is, e.g., an internal tool or some other protected experience - and even then it should feel more like a welcome/splash screen than an error state). Users should be able to explore public resources, or at least encounter some kind of landing/introduction moment, before they get hit with a signup/login screen. Make auth feel like a natural moment in the user's journey.
|
|
404
|
-
|
|
405
|
-
Login and signup screens set the tone for the user's entire experience with the app and are important to get right - they should feel like exciting entry points into the next level of the user journy. A janky login form with misaligned inputs and no feedback dminishes excitement and undermines trust before the user even gets in.
|
|
406
|
-
|
|
407
|
-
Login and signup are separate moments - even if the underlying code is the same for both. A new user signing up should feel like they are creating a new account. A user logging in should feel like they are being welcomed back. Auth is always a pain for users, even when it's as frictionless as this, so take care to use Sign Up screens as moments to communicate value and help the user get excited about what they are joining.
|
|
408
|
-
|
|
409
|
-
Consult the `visualDesignExpert` to help you work through authentication at a high level, including when and where to show auth, and the design of specific screens.
|
|
410
|
-
|
|
411
|
-
### Rules for Building Auth Screens
|
|
412
|
-
**Auth modes:** Think about which mode(s) makes the most sense for the type of app you are building. Consumer apps likely to be used on mobile should probably tend toward SMS auth as the default - business apps used on desktop make more sense to use email verification - or allow both, there's no harm in giving the user choice!
|
|
413
|
-
|
|
414
|
-
**"Continue with {Org}" (delegated):** When the `<org_context>` block says delegated sign-in is available, a single "Continue with {Org}" button is the primary path — often the *only* one — and there's no verification-code step to design at all (the platform handles it). Give the button real weight in the branded login moment rather than treating it as a secondary option, and use the exact organization name. If the org also allows code methods, delegated goes first with the code form beneath. On return from the handshake (and on dashboard launch), render a brief "Completing sign-in…" state driven by `auth.authStatus === 'authenticating'` — never the login form — so a successful sign-in doesn't flash the logged-out screen.
|
|
415
|
-
|
|
416
|
-
**Verification code input:** The 6-digit code entry is the critical moment. Prefer to design it as individual digit boxes (not a single text input), with auto-advance between digits, a beautiful animation and auto-submit on paste, and clear visual feedback. The boxes should be large enough to tap easily on mobile. Show a subtle animation on successful verification. Error states should be inline and immediate, not a separate alert. Make sure there is no layout shift when loading in the success/error states - loading spinners must never pop in below the input and shift the content, for example.
|
|
417
|
-
|
|
418
|
-
**The send/resend flow:** After the user enters their email or phone and taps "Send code," show clear confirmation that the code was sent ("Check your email" with the address displayed). Include a resend option with a cooldown timer (e.g., "Resend in 30s"). The transition from "enter email/phone" to "enter code" should feel smooth, not like a page reload. Always make sure the user can cancel and exit the flow (e.g., they had a typo in their email, or remembered they used a different email to sign up).
|
|
419
|
-
|
|
420
|
-
**The overall login page:** This is a branding moment. Use the app's full visual identity — colors, typography, any logos, hero imagery, or illustration. A centered card on a branded background is a classic pattern. Don't make it look like a generic SaaS login template. The login page must feel like it belongs to this specific app. Consult the `visualDesignExpert` for guidance on how to really make this shine.
|
|
421
|
-
|
|
422
|
-
**Post-login transition:** After successful verification, the transition into the app should feel seamless and instant. Avoid a blank loading screen — if data needs to load, show the app shell with skeleton states. Always make sure the user has a way of logging out.
|
|
38
|
+
Authentication moments must feel natural and intuitive — never jarring or surprising. For the overwhelming majority of apps, a user should never land on auth at the root of the app on first open: let them explore public resources or meet a landing/introduction moment first (an internal tool can gate at the root, but as a welcome/splash, not an error state). Login and signup are separate moments even when the code is shared — signup should communicate value and build excitement about what the user is joining; login should feel like being welcomed back. The login page is a branding moment: the app's full visual identity, not a generic SaaS template. Consult the `visualDesignExpert` on when and where auth appears in the journey and on the screens themselves; the `auth` skill carries the concrete screen-building rules.
|
|
423
39
|
|
|
424
40
|
## Testing Auth in Development
|
|
425
41
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
- **Email:** `remy@mindstudio.ai` — verification code is always `123456`
|
|
429
|
-
- **Phone:** any `555` number (e.g. `+15551234567`) — verification code is always `123456`
|
|
430
|
-
|
|
431
|
-
All other emails and phone numbers receive real codes. There is no dev-mode bypass, no fake code, and no way to skip verification. When testing auth flows in the preview, use one of the test bypasses above or a real email/phone.
|
|
432
|
-
|
|
433
|
-
The `runMethod` tool's `userId: "testUser"` shortcut resolves to this same dev-bypass identity. The platform find-or-creates a real users-table row for it on first call and caches the row's UUID for the rest of the dev session. **`auth.userId` inside the method is that UUID — not the literal string `"testUser"`.** The user row already exists, so don't try to insert it. If you need the UUID to seed app-specific rows that reference it (profiles, preferences, foreign keys), read it from any method response or query the users table directly: `SELECT id FROM users WHERE email = 'remy@mindstudio.ai'` (or `phone = '+15555555555'` for SMS-auth apps).
|
|
434
|
-
|
|
435
|
-
For **"Sign in with Remy"** apps (`auth.methods` is `["remy"]`, with no `email-code`/`sms-code`), `testUser` — and `setupBrowser` — resolve to **the developer's own delegated Remy identity**, not the `remy@mindstudio.ai` code-bypass user. `auth.userId` is still that user's real UUID, but the `remy@mindstudio.ai` email lookup above does not apply — read the UUID from a method response instead.
|
|
436
|
-
|
|
437
|
-
Browser automation tools (screenshots, automated browser tests) handle their own auth sessions. Scenarios seed database data but do not create browser auth sessions.
|
|
42
|
+
Real verification codes are sent to real addresses in dev — the only bypasses are `remy@mindstudio.ai` (email) and any `555` phone number, both with fixed code `123456`. There is no other fake code or skip. `runMethod`'s `userId: "testUser"` resolves to a real users-table row for that same identity — **`auth.userId` inside the method is that row's UUID, not the literal string `"testUser"`**, and the row already exists (don't insert it). Full detail, including the "Sign in with Remy" variant, is in the `auth` skill.
|
|
@@ -53,6 +53,8 @@ Every interface must work on both desktop and mobile. Think about how the app wi
|
|
|
53
53
|
|
|
54
54
|
The `designExpert` can create and source amazing, high quality images, graphics, illustrations, and logos to use in the interface - both with and without transparency. This is a huge level for upgrading the premium look, feel, and quality of the app. Use image logos directly instead of plain text wordmarks; use images for empty states, onboarding screens, full-screen loading, and more.
|
|
55
55
|
|
|
56
|
+
Public CDN images accept resize query params — request the size the layout actually displays (e.g. `?w=400&dpr=2` so it stays crisp on Retina) instead of CSS-scaling a full-res original. The full parameter table is in the `files` skill.
|
|
57
|
+
|
|
56
58
|
## Forms
|
|
57
59
|
|
|
58
60
|
Forms should feel like interactions, not paperwork.
|
|
@@ -82,8 +84,8 @@ Buttons should use a small animated spinner during loading, not text labels like
|
|
|
82
84
|
The UI should feel instant. Never make the user wait for a server round-trip to see the result of their own action. Consider loading a bunch of data in one API call, rather than a bunch of small calls (e.g., if loading a post, also preload comments, likes, user artifacts, etc - don't use separate API calls for each GET).
|
|
83
85
|
|
|
84
86
|
- **Optimistic updates.** When a user adds a row, toggles a setting, or submits a form, update the UI immediately and let the backend confirm in the background. If the backend fails, revert and show an error.
|
|
85
|
-
- **
|
|
86
|
-
- **Mutate after actions.** After a successful create/update/delete, call `mutate()` to revalidate the relevant SWR
|
|
87
|
+
- **Data fetching.** For apps that can load everything on startup, use a Zustand store with a single initial fetch (see the coding instructions) — SWR (`useSWR`) is for server-cache patterns where data is too large or dynamic to hold in memory. Whichever fits, never leave fetches as ad-hoc `useEffect` + `useState` patterns.
|
|
88
|
+
- **Mutate after actions.** After a successful create/update/delete, update the store optimistically (or call `mutate()` to revalidate the relevant cache when using SWR) rather than refetching everything.
|
|
87
89
|
- **Skeleton loading.** Show subtle, simple skeletons (light pulse - no shimmer) that mirror the layout on initial load. Never show a blank page or centered spinner while data is loading.
|
|
88
90
|
|
|
89
91
|
### Errors
|
|
@@ -48,14 +48,7 @@ Every live deploy runs an automated Lighthouse audit of the app. Pull it via `mi
|
|
|
48
48
|
|
|
49
49
|
### Database Migrations on Deploy
|
|
50
50
|
|
|
51
|
-
Schema changes are automatic
|
|
52
|
-
- **New tables** — `CREATE TABLE` applied automatically
|
|
53
|
-
- **New columns** — `ALTER TABLE ADD COLUMN` applied automatically
|
|
54
|
-
- **Dropped columns** — `ALTER TABLE DROP COLUMN` applied automatically when a column is removed from the interface
|
|
55
|
-
- **Dropped tables** — `DROP TABLE` applied automatically when a table file is removed from the manifest
|
|
56
|
-
- **Type changes and renames** — not supported in the automatic migration path
|
|
57
|
-
|
|
58
|
-
Schema changes are always applied to a clone of the live database, never directly. If DDL fails, the live database is untouched and the release is marked `failed`.
|
|
51
|
+
Schema changes are automatic — adds and drops of tables/columns are diffed from the table definitions and applied as DDL (type changes and renames are not supported; full rules in the Tables docs). Changes are always applied to a clone of the live database, never directly. If DDL fails, the live database is untouched and the release is marked `failed`.
|
|
59
52
|
|
|
60
53
|
### Rollback
|
|
61
54
|
|
|
@@ -61,7 +61,7 @@ const api = createClient<{
|
|
|
61
61
|
const { vendorId } = await api.submitVendorRequest({ name: 'Acme' });
|
|
62
62
|
const { vendors } = await api.listVendors();
|
|
63
63
|
|
|
64
|
-
// File upload → client-direct to the app's file store (
|
|
64
|
+
// File upload → client-direct to the app's file store (load the `files` skill for the backend side).
|
|
65
65
|
// A backend method mints an upload token; the browser uploads straight to storage.
|
|
66
66
|
const token = await api.getUploadSlot({ filename: file.name, contentType: file.type });
|
|
67
67
|
const { key, url } = await platform.upload(token, file);
|
|
@@ -79,11 +79,9 @@ auth.getCurrentUser() // AppUser { id, email, phone, roles, create
|
|
|
79
79
|
auth.currentUser // same as getCurrentUser() (sync getter)
|
|
80
80
|
auth.isAuthenticated() // boolean
|
|
81
81
|
auth.onAuthStateChanged(cb) // fires immediately + on transitions; returns unsubscribe
|
|
82
|
-
auth.sendEmailCode(email) // → { verificationId }
|
|
83
|
-
auth.verifyEmailCode(verId, code) // → AppUser (sets session)
|
|
84
|
-
auth.sendSmsCode(phone) // → { verificationId }
|
|
85
|
-
auth.verifySmsCode(verId, code) // → AppUser (sets session)
|
|
86
82
|
auth.logout() // clears session
|
|
83
|
+
// Verification flows (send/verify email + SMS codes, delegated sign-in, API keys)
|
|
84
|
+
// are in the `auth` skill — load it before building login/signup.
|
|
87
85
|
```
|
|
88
86
|
|
|
89
87
|
For apps with an agent interface, the SDK also provides `createAgentChatClient()` for thread management and streaming chat. Load the `agentInterfaces` skill for its usage — thread APIs, streaming callbacks, and attachments are all there.
|
|
@@ -154,43 +152,27 @@ Each has its own skill carrying the config shape, the input the method receives,
|
|
|
154
152
|
|
|
155
153
|
## Cron
|
|
156
154
|
|
|
157
|
-
Scheduled method execution — a method plus a cron expression, synced to the platform on deploy.
|
|
158
|
-
|
|
159
|
-
**Load the `scheduledJobs` skill** before adding one.
|
|
155
|
+
Scheduled method execution — a method plus a cron expression, synced to the platform on deploy. **Load the `scheduledJobs` skill** before adding one.
|
|
160
156
|
|
|
161
157
|
## Webhook
|
|
162
158
|
|
|
163
|
-
Inbound HTTP endpoints that invoke a method directly and synchronously
|
|
164
|
-
|
|
165
|
-
Routing is by a secret in the URL rather than an auth header, which is what makes it the right fit for provider callbacks — they can't send a bearer token. The API interface is the alternative when the caller can.
|
|
166
|
-
|
|
167
|
-
**Load the `webhooks` skill** before adding one — the secret semantics, endpoint URL, input shape, and signature verification are all there.
|
|
159
|
+
Inbound HTTP endpoints that invoke a method directly and synchronously. Use for provider callbacks (Stripe, GitHub, Shopify, Slack, Twilio) — signature verification works natively; do **not** build confirmation-token or polling workarounds. Routing is by a secret in the URL rather than an auth header, which is what fits callers that can't send a bearer token; the API interface is the alternative when they can. **Load the `webhooks` skill** before adding one.
|
|
168
160
|
|
|
169
161
|
## Email
|
|
170
162
|
|
|
171
|
-
Inbound email triggers
|
|
172
|
-
|
|
173
|
-
**Load the `inboundEmail` skill** before writing the handler — address tiers, `approvedSenders`, the input shape, in-thread replies, and attachments are all there.
|
|
163
|
+
Inbound email triggers: one handler method per app, and the platform routes all inbound mail for the app to it. Addresses on the app's subdomain are catchall, so per-purpose addresses (`support@`, `receipts@`) work without registering anything. **Load the `inboundEmail` skill** before writing the handler.
|
|
174
164
|
|
|
175
165
|
## MCP (Model Context Protocol)
|
|
176
166
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
It supports the full MCP surface: tools (methods the agent can call), resources (read-only app data addressable by URI), prompts (parameterized templates), and instructions (server-level guidance for the whole toolset). The platform hosts the server, handles auth, and derives every tool's input schema from the method contract.
|
|
180
|
-
|
|
181
|
-
**Load the `mcpInterfaces` skill** before authoring `src/interfaces/mcp.md`. Because the consumer is an external agent with no knowledge of your app, the descriptions are the product — the skill carries both how to write them and the full config contract.
|
|
167
|
+
The app projected as an MCP server for *external* AI agents to drive (Claude Desktop, Cursor, other people's agents). Unlike the agent interface — which IS an agent, with its own LLM — MCP has no model of its own. The platform hosts the server, handles auth, and derives tool schemas from method contracts. **Load the `mcpInterfaces` skill** before authoring `src/interfaces/mcp.md` — the consumer knows nothing about your app, so the descriptions are the product.
|
|
182
168
|
|
|
183
169
|
## Agent (Conversational Interface)
|
|
184
170
|
|
|
185
|
-
A conversational interface where
|
|
186
|
-
|
|
187
|
-
**Load the `agentInterfaces` skill** before authoring `src/interfaces/agent.md` or building the chat UI — the spec frontmatter, compiled output, `agent.json`, and the entire frontend surface are all there.
|
|
171
|
+
A conversational interface where the app's own LLM orchestrates its methods as tools — its own personality, system prompt, and model config (the inverse of MCP). Chat runs as the authenticated user, so every tool call carries that user's roles, and the config must declare an `auth` block (`{ "requireUser": boolean, "requireRole"?: string[] }`) gating who may chat at all. **Load the `agentInterfaces` skill** before authoring `src/interfaces/agent.md` or building the chat UI.
|
|
188
172
|
|
|
189
173
|
## Voice (Realtime Conversation)
|
|
190
174
|
|
|
191
|
-
The app's agent as a live voice conversation —
|
|
192
|
-
|
|
193
|
-
**Load the `voiceInterfaces` skill** before authoring `src/interfaces/voice.md` or building the voice UI — the spoken-register rules, latency classes, spec format, `interface.json`, and the `createVoiceClient()` frontend surface are all there.
|
|
175
|
+
The app's agent as a live, interruptible voice conversation — a sibling of the agent interface, not a mode of it, with its own spec and a smaller toolset where every tool carries a latency class. Sessions run as the authenticated user, and the config must declare the same `auth` block gating who may start a session. **Load the `voiceInterfaces` skill** before authoring `src/interfaces/voice.md` or building the voice UI.
|
|
194
176
|
|
|
195
177
|
## Manifest Declaration
|
|
196
178
|
|