@mindstudio-ai/remy 0.1.269 → 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.
@@ -91,7 +91,7 @@ await mindstudio.sendEmail({
91
91
  // a handler. It has the full input shape and the threading rules; getting the headers
92
92
  // wrong sends a reply that starts a new conversation instead of continuing one.
93
93
 
94
- // Store a file → returns a stable URL (define the store at module scope; see Files & Storage)
94
+ // Store a file → returns a stable URL (define the store at module scope; load the `files` skill)
95
95
  const { url } = await Reports.put(buffer, { contentType: 'application/pdf', filename: 'report.pdf' });
96
96
 
97
97
  // Web scraping
@@ -168,17 +168,7 @@ Building the query progressively (one `.filter` per optional input) is the canon
168
168
 
169
169
  ### Role-Gated Operation
170
170
 
171
- ```typescript
172
- export async function deleteVendor(input: { vendorId: string }) {
173
- auth.requireRole('admin');
174
-
175
- const vendor = await Vendors.get(input.vendorId);
176
- if (!vendor) throw new Error('Vendor not found.');
177
-
178
- const { deleted } = await Vendors.remove(input.vendorId);
179
- return { deleted };
180
- }
181
- ```
171
+ Same shape as any method, with `auth.requireRole('admin')` (or the relevant role) as the first line — it throws for callers without the role, so nothing below it runs unauthorized.
182
172
 
183
173
  ### Multi-Table Transaction
184
174
 
@@ -319,30 +309,6 @@ input._request: {
319
309
 
320
310
  `rawBody` preserves the exact bytes the client sent — whitespace, key ordering, encoding. Use it for signature verification.
321
311
 
322
- **There are two inbound HTTP interfaces and they expose the raw body differently.** This one is the API interface, at `input._request.rawBody`. The Webhook interface puts it at top-level `input.rawBody` and routes by a secret in the URL instead of a bearer token, which usually suits provider callbacks better since a provider can't send one. Load the `webhooks` skill before choosing — the example below is the API-interface shape and won't work unchanged in a webhook handler.
323
-
324
- ```typescript
325
- export async function stripeWebhook(input: {
326
- type: string;
327
- data: any;
328
- _request: { headers: Record<string, string>; rawBody: string };
329
- }) {
330
- const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
331
-
332
- const event = stripe.webhooks.constructEvent(
333
- input._request.rawBody,
334
- input._request.headers['stripe-signature'],
335
- process.env.STRIPE_WEBHOOK_SECRET!,
336
- );
337
-
338
- switch (event.type) {
339
- case 'payment_intent.succeeded':
340
- // ...
341
- break;
342
- }
343
-
344
- return { received: true };
345
- }
346
- ```
312
+ **There are two inbound HTTP interfaces and they expose the raw body differently.** The API interface puts it at `input._request.rawBody`; the Webhook interface puts it at top-level `input.rawBody` and routes by a secret in the URL instead of a bearer token, which usually suits provider callbacks better since a provider can't send one. Load the `webhooks` skill before writing a signature-verifying handler (e.g. Stripe's `constructEvent` over the raw bytes) — the input shape depends on which interface you chose.
347
313
 
348
314
  For most methods, you don't need `_request` — the parsed path params, query params, and body fields are already on `input` directly.
@@ -223,19 +223,4 @@ clipboard fallback for unsupported browsers.
223
223
  Added share button to haiku detail view.
224
224
  ```
225
225
 
226
- Unbuilt item:
227
-
228
- ```markdown
229
- ---
230
- name: Daily Prompt Engine
231
- type: roadmap
232
- status: not-started
233
- description: A new writing prompt every day, tuned to the user's style and interests.
234
- requires: []
235
- effort: small
236
- ---
237
-
238
- Generate a personalized daily writing prompt based on the user's past haikus,
239
- preferred themes, and seasonal context. Surface it as a gentle nudge on the
240
- home screen, not a notification.
241
- ```
226
+ Unbuilt item — same shape with `status: not-started`, a body describing the intended feature, and no History section (History is appended when it's built).
@@ -12,7 +12,7 @@ my-app/
12
12
 
13
13
  src/ ← authored source (no code)
14
14
  app.md backend spec (MSFM)
15
- references/ supporting material (PDFs, notes, diagrams)
15
+ references/ supporting material (PDFs, notes, diagrams) — context for the agent, not consumed by the platform
16
16
  interfaces/
17
17
  @brand/ shared brand identity
18
18
  visual.md aesthetic direction, surfaces, spacing
@@ -34,7 +34,7 @@ my-app/
34
34
  common/ shared helpers (imported by methods, not methods themselves)
35
35
  *.ts one file per method (named async function export)
36
36
  .scenarios/ seed data scripts (dev only, not deployed)
37
- package.json backend dependencies
37
+ package.json backend dependencies — only declared packages are available at runtime
38
38
 
39
39
  interfaces/ interface projections
40
40
  web/ full project directory (Vite + React)
@@ -60,21 +60,11 @@ my-app/
60
60
  tools/ tool descriptions (one .md per method)
61
61
  ```
62
62
 
63
- ## What Goes Where
64
-
65
- | What | Where | Notes |
66
- |------|-------|-------|
67
- | Method handlers | `dist/methods/src/*.ts` | One file per method, named export |
68
- | Table definitions | `dist/methods/src/tables/*.ts` | One file per table |
69
- | Shared helpers | `dist/methods/src/common/*.ts` | Imported by methods, not invokable directly |
70
- | Scenarios | `dist/methods/.scenarios/*.ts` | Seed data for testing (not deployed) |
71
- | Backend dependencies | `dist/methods/package.json` | Only declared packages are available at runtime |
72
- | Web interface | `dist/interfaces/web/` | Full Vite + React project directory |
73
- | Interface configs | `dist/interfaces/*/interface.json` | One per non-web interface type |
74
- | Specs | `src/*.md` | Natural language, MSFM format |
75
- | Brand identity | `src/interfaces/@brand/` | visual.md (aesthetic), colors.md (palette), typography.md (fonts), voice.md (tone), assets/ |
76
- | Roadmap | `src/roadmap/*.md` | Feature roadmap items (type: roadmap). One file per feature with status, dependencies, and history. |
77
- | Reference material | `src/references/` | Context for the agent, not consumed by platform |
63
+ ## Platform Limits
64
+
65
+ Two things the platform cannot do — be upfront when a request heads this way:
66
+ - Native mobile apps (iOS/Android). Mobile-responsive web apps are fine.
67
+ - Real-time multiplayer with persistent connections (no WebSocket support). Turn-based or async multiplayer works great.
78
68
 
79
69
  ## The Two SDKs
80
70
 
@@ -0,0 +1,443 @@
1
+ ---
2
+ name: Auth & User Accounts
3
+ what: Apps manage their own users — opt-in via manifest config over a developer-owned user table, with the platform handling verification codes, cookie sessions, and role sync. Covers email/SMS code login (the platform sends real 6-digit codes; the developer builds the UI), per-user API keys that resolve to full RBAC over Bearer auth, org-delegated "Sign in with Remy" for internal apps (redirect/popup handshake, platform-managed identity), the full frontend SDK (auth state and onAuthStateChanged, flows, email/phone changes, phone and email helpers, error codes), backend enforcement (requireRole/hasRole/userId, the system role), auth-screen design rules, and the dev test bypasses.
4
+ when: Before writing ANY auth code — the manifest `auth` config, the user table, login/signup UI, frontend `auth.*` calls, API keys, delegated sign-in — and before testing or debugging auth flows.
5
+ ---
6
+
7
+ # Auth
8
+
9
+ 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, cookies, and session management. Apps without auth config use anonymous guest sessions (current default behavior).
10
+
11
+ **Auth is optional.** Many apps don't need it. Only add auth when the app needs to identify users or restrict access.
12
+
13
+ ## Manifest Config
14
+
15
+ ```json
16
+ {
17
+ "auth": {
18
+ "enabled": true,
19
+ "methods": ["email-code", "sms-code"],
20
+ "table": {
21
+ "name": "users",
22
+ "columns": {
23
+ "email": "email",
24
+ "phone": "phone",
25
+ "roles": "roles",
26
+ "apiKey": "apiKey"
27
+ }
28
+ }
29
+ },
30
+ "roles": [
31
+ { "id": "vendor", "name": "Vendor" },
32
+ { "id": "buyer", "name": "Buyer" },
33
+ { "id": "admin", "name": "Admin" }
34
+ ]
35
+ }
36
+ ```
37
+
38
+ - **`auth.enabled`** — opt-in. No auth config = anonymous guest sessions.
39
+ - **`auth.methods`** — which verification methods the app supports. At least one required.
40
+ - `email-code` — 6-digit code sent via email
41
+ - `sms-code` — 6-digit code sent via SMS
42
+ - `api-key` — programmatic access via `Authorization: Bearer sk_...` header. Resolves to a user with full RBAC.
43
+ - `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* below.
44
+ - **`auth.table.name`** — name of the `defineTable` table that holds user records.
45
+ - **`auth.table.columns`** — maps platform-managed fields to column names in the developer's table.
46
+ - `email` — required if `email-code` is in methods
47
+ - `phone` — required if `sms-code` is in methods
48
+ - `roles` — optional. Maps to a JSON array column for role assignments.
49
+ - `apiKey` — optional. Required if `api-key` is in methods. Platform stores masked value (`sk_...xxxx`) for display; one key per user.
50
+ - **`roles`** — declares valid roles for the app. Same as before: `id`, `name`, optional `description`.
51
+
52
+ ## Auth Table
53
+
54
+ The user table is a regular `defineTable` table. The platform manages the auth-mapped columns; all other columns are the developer's domain.
55
+
56
+ ```typescript
57
+ import { db } from '@mindstudio-ai/agent';
58
+
59
+ export const Users = db.defineTable<{
60
+ // Mapped to auth — platform keeps these in sync
61
+ email: string;
62
+ phone?: string;
63
+ roles: string[];
64
+ apiKey?: string; // masked value (sk_...xxxx), read-only from code
65
+ // Developer's own fields
66
+ displayName: string;
67
+ plan: 'free' | 'pro';
68
+ avatarUrl?: string;
69
+ }>('users');
70
+ ```
71
+
72
+ ### Platform-Managed Column Behavior
73
+
74
+ - **`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.
75
+ - **`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.
76
+ - 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.
77
+
78
+ ## Frontend Auth (Interface SDK)
79
+
80
+ The developer builds their own login/signup UI. The SDK provides methods that handle the verification flow and session management.
81
+
82
+ ```typescript
83
+ import { auth } from '@mindstudio-ai/interface';
84
+ ```
85
+
86
+ ### User Shape
87
+
88
+ ```typescript
89
+ interface AppUser {
90
+ id: string;
91
+ email: string | null;
92
+ phone: string | null;
93
+ roles: string[];
94
+ apiKey: string | null; // masked value (sk_...xxxx), null if no key
95
+ provider?: 'remy' | null; // 'remy' = delegated sign-in (platform-managed); null/absent = app-verified
96
+ createdAt: string;
97
+ }
98
+ ```
99
+
100
+ `auth.getCurrentUser()` returns `AppUser | null`. `null` means unauthenticated.
101
+
102
+ ### State
103
+
104
+ ```typescript
105
+ auth.getCurrentUser() // AppUser | null
106
+ auth.currentUser // AppUser | null (sync getter, same as getCurrentUser())
107
+ auth.isAuthenticated() // boolean
108
+ auth.authStatus // 'authenticating' | 'authenticated' | 'unauthenticated' (sync getter)
109
+ auth.onAuthStateChanged(cb) // fires immediately with current user, then on every transition —
110
+ // verify/confirm/logout AND each authStatus change (a Sign in with Remy
111
+ // handshake starting or settling). During 'authenticating' it may fire
112
+ // null; that's expected — read authStatus to tell it apart from
113
+ // logged-out. Returns an unsubscribe function.
114
+ ```
115
+
116
+ Use `onAuthStateChanged` in React instead of reading `currentUser` once at render time:
117
+
118
+ ```typescript
119
+ function useAuth() {
120
+ const [user, setUser] = useState<AppUser | null>(null);
121
+ useEffect(() => auth.onAuthStateChanged(setUser), []);
122
+ return user;
123
+ }
124
+ ```
125
+
126
+ ### Email Code Flow
127
+
128
+ ```typescript
129
+ const { verificationId } = await auth.sendEmailCode('user@example.com');
130
+ // User enters the 6-digit code from their email
131
+ const user = await auth.verifyEmailCode(verificationId, '123456');
132
+ // user is now authenticated — auth.getCurrentUser() returns the AppUser
133
+ ```
134
+
135
+ ### SMS Code Flow
136
+
137
+ ```typescript
138
+ const { verificationId } = await auth.sendSmsCode('+15551234567');
139
+ const user = await auth.verifySmsCode(verificationId, '123456');
140
+ ```
141
+
142
+ ## Organization-Managed Sign-In ("Sign in with Remy")
143
+
144
+ Some apps are owned by an organization that centralizes sign-in. When that applies to the current app, the system prompt includes an `<org_context>` block (near the end); for org-managed sign-in it states the organization name and whether delegated sign-in is available. Sign in with Remy is a way to make authentication seamless for internal apps - it should not be used for public-facing applications. The app owner will need to add users to their workspace's team on the Remy platform and those users will need Remy accounts for it to work.
145
+
146
+ - **When it says "Sign in with Remy" is available** — offer delegated sign-in: a **"Continue with {Org}"** button wired to the `remy` method (see *Sign in with Remy (delegated)* below). Use the exact organization name from the block for the label. For an org-owned app this is usually the primary sign-in — the members already have platform identities, so a verification-code form is redundant.
147
+ - **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.
148
+
149
+ When the `<org_context>` block is absent, or present without a delegated-sign-in line — the common case — do not build it or offer to build it. This is an auth scheme for enterprises building internal apps only, and it requires a Remy enterprise plan to use. When enabled, the platform decides who the user is (like "Sign in with Google"); the app just starts the flow and reads the result.
150
+
151
+ ```typescript
152
+ // "Continue with {Org}" button — must be triggered by a user gesture (click).
153
+ <button onClick={() => auth.signInWithRemy()}>Continue with Acme</button>
154
+
155
+ // On return from the handshake (or when opened from the Remy dashboard), the app
156
+ // cold-loads with sign-in still completing. Track authStatus and render a
157
+ // "Completing sign-in…" state — NOT the login screen — while it's 'authenticating'.
158
+ const [user, setUser] = useState(auth.currentUser);
159
+ const [status, setStatus] = useState(auth.authStatus);
160
+ useEffect(() => auth.onAuthStateChanged(() => {
161
+ setUser(auth.currentUser);
162
+ setStatus(auth.authStatus);
163
+ }), []);
164
+ useEffect(() => { auth.handleRemyRedirect().catch(() => {}); }, []); // no-op if nothing to redeem
165
+ // status === 'authenticating' → <CompletingSignIn/>; user → app; else → login
166
+ ```
167
+
168
+ - `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.**
169
+ - `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.
170
+ - **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.
171
+ - 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.
172
+
173
+ ### Email/Phone Changes (must be authenticated)
174
+
175
+ ```typescript
176
+ await auth.requestEmailChange('newemail@example.com');
177
+ const user = await auth.confirmEmailChange('newemail@example.com', '123456');
178
+
179
+ await auth.requestPhoneChange('+15559876543');
180
+ const user = await auth.confirmPhoneChange('+15559876543', '123456');
181
+ ```
182
+
183
+ ### Logout
184
+
185
+ ```typescript
186
+ await auth.logout(); // clears session
187
+ ```
188
+
189
+ ### API Keys
190
+
191
+ 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.
192
+
193
+ ```typescript
194
+ // Generate a key for the current user (must be logged in)
195
+ const { key } = await auth.createApiKey();
196
+ // key = "sk_..." — show once, not stored. user.apiKey updates to masked value.
197
+
198
+ // Revoke current user's key
199
+ await auth.revokeApiKey();
200
+ // user.apiKey becomes null
201
+ ```
202
+
203
+ Both methods fire `onAuthStateChanged` since they modify the user object. One key per user — creating a new key replaces the old one.
204
+
205
+ 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.
206
+
207
+ ### Error Codes
208
+
209
+ All auth methods throw on failure with a `code` property:
210
+
211
+ | Code | HTTP | Meaning |
212
+ |------|------|---------|
213
+ | `rate_limited` | 429 | Too many requests |
214
+ | `invalid_code` | 400 | Wrong verification code |
215
+ | `verification_expired` | 400 | Code has expired |
216
+ | `max_attempts_exceeded` | 400 | Too many failed attempts |
217
+ | `not_authenticated` | 401 | No active session |
218
+ | `invalid_session` | 401 | Session expired or invalid |
219
+ | `not_supported` | 400 | Feature not enabled for this app (e.g. API keys without `api-key` in methods) |
220
+ | `invalid_state` | 400 | Sign in with Remy: returned CSRF state didn't match (stale/replayed redirect) |
221
+ | `popup_blocked` | — | Sign in with Remy (embedded): popup was blocked — prompt to allow popups and retry |
222
+ | `signin_timeout` | — | Sign in with Remy (embedded): popup didn't complete in time |
223
+ | `auth_required` | 401 | Agent/voice interface requires an authenticated user (interface `auth` block) |
224
+ | `role_required` | 403 | Agent/voice interface requires a role the user doesn't hold |
225
+
226
+ ### Phone Helpers
227
+
228
+ ```typescript
229
+ auth.phone.countries // ~180 countries with { code, dialCode, name, flag }
230
+ // Key selects by country code (US, CA, BB), not dial code — multiple countries share +1
231
+ auth.phone.detectCountry() // guess from timezone, e.g. 'US'
232
+ auth.phone.toE164('5551234567', 'US') // '+15551234567'
233
+ auth.phone.format('+15551234567') // '+1 (555) 123-4567'
234
+ auth.phone.isValid('+15551234567') // true
235
+ ```
236
+
237
+ ### Email Helpers
238
+
239
+ ```typescript
240
+ auth.email.isValid('user@example.com') // true
241
+ ```
242
+
243
+ ## Backend Auth (Agent SDK)
244
+
245
+ ```typescript
246
+ import { auth } from '@mindstudio-ai/agent';
247
+ ```
248
+
249
+ ### `auth.requireRole(...roles)`
250
+
251
+ Throws 401 (unauthenticated) if there is no current user. Throws 403 (forbidden) if the current user doesn't have **any** of the specified roles.
252
+
253
+ ```typescript
254
+ auth.requireRole('admin');
255
+ auth.requireRole('admin', 'approver'); // any of these
256
+ ```
257
+
258
+ **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.
259
+
260
+ ### `auth.hasRole(...roles)`
261
+
262
+ Returns `boolean`. Same logic as `requireRole` but doesn't throw.
263
+
264
+ ### `auth.userId`
265
+
266
+ 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.
267
+
268
+ ### `auth.roles`
269
+
270
+ Array of role IDs assigned to the current user.
271
+
272
+ ### `auth.getUsersByRole(role)`
273
+
274
+ Returns an array of user IDs with the specified role.
275
+
276
+ ### System Role (Platform Triggers)
277
+
278
+ 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:
279
+
280
+ ```typescript
281
+ export async function regenerateCache(input: {}) {
282
+ auth.requireRole('system');
283
+ // Only cron, webhooks, and users with 'system' role can reach this
284
+ }
285
+ ```
286
+
287
+ 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.
288
+
289
+ ## Login Page Example
290
+
291
+ ```tsx
292
+ import { useState, useEffect } from 'react';
293
+ import { auth } from '@mindstudio-ai/interface';
294
+ import { useLocation } from 'wouter';
295
+
296
+ function useAuth() {
297
+ const [user, setUser] = useState<AppUser | null>(null);
298
+ useEffect(() => auth.onAuthStateChanged(setUser), []);
299
+ return user;
300
+ }
301
+
302
+ function LoginPage() {
303
+ const user = useAuth();
304
+ const [, navigate] = useLocation();
305
+ const [email, setEmail] = useState('');
306
+ const [code, setCode] = useState('');
307
+ const [verificationId, setVerificationId] = useState('');
308
+ const [error, setError] = useState('');
309
+
310
+ // Redirect when authenticated (fires via onAuthStateChanged after verify)
311
+ useEffect(() => { if (user) navigate('/dashboard'); }, [user]);
312
+
313
+ const handleSendCode = async () => {
314
+ try {
315
+ const { verificationId } = await auth.sendEmailCode(email);
316
+ setVerificationId(verificationId);
317
+ setError('');
318
+ } catch (err: any) {
319
+ setError(err.code === 'rate_limited' ? 'Too many attempts. Try again later.' : err.message);
320
+ }
321
+ };
322
+
323
+ const handleVerify = async () => {
324
+ try {
325
+ await auth.verifyEmailCode(verificationId, code);
326
+ // onAuthStateChanged fires, useAuth updates, redirect happens
327
+ } catch (err: any) {
328
+ if (err.code === 'invalid_code') setError('Wrong code. Try again.');
329
+ else if (err.code === 'verification_expired') setError('Code expired. Request a new one.');
330
+ else if (err.code === 'max_attempts_exceeded') setError('Too many attempts. Request a new code.');
331
+ else setError(err.message);
332
+ }
333
+ };
334
+
335
+ if (!verificationId) {
336
+ return (
337
+ <div>
338
+ <h1>Sign in</h1>
339
+ <input placeholder="Email" value={email} onChange={e => setEmail(e.target.value)} />
340
+ <button onClick={handleSendCode}>Send code</button>
341
+ {error && <p>{error}</p>}
342
+ </div>
343
+ );
344
+ }
345
+
346
+ return (
347
+ <div>
348
+ <p>Enter the code we sent to {email}</p>
349
+ <input placeholder="123456" value={code} onChange={e => setCode(e.target.value)} />
350
+ <button onClick={handleVerify}>Verify</button>
351
+ <button onClick={() => setVerificationId('')}>Resend</button>
352
+ {error && <p>{error}</p>}
353
+ </div>
354
+ );
355
+ }
356
+ ```
357
+
358
+ ## Backend Method Example
359
+
360
+ ```typescript
361
+ import { auth } from '@mindstudio-ai/agent';
362
+ import { Users } from './tables/users';
363
+
364
+ export async function getDashboard() {
365
+ const user = auth.userId ? await Users.get(auth.userId) : null;
366
+
367
+ if (auth.hasRole('admin')) {
368
+ const allUsers = await Users.toArray();
369
+ return { user, allUsers, isAdmin: true };
370
+ }
371
+
372
+ return { user, isAdmin: false };
373
+ }
374
+
375
+ export async function promoteToAdmin(input: { userId: string }) {
376
+ auth.requireRole('admin');
377
+ await Users.update(input.userId, { roles: ['admin'] });
378
+ }
379
+ ```
380
+
381
+ ## Roles
382
+
383
+ 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.
384
+
385
+ - Declare roles in `mindstudio.json` with `id` and `name`
386
+ - The mapped `roles` column holds a JSON array of role ID strings: `["vendor", "admin"]`
387
+ - Writable from code: `Users.update(userId, { roles: ['admin'] })` — platform syncs automatically
388
+ - Writable from dashboard: Remy dashboard shows app users and their roles
389
+ - Backend enforcement: `auth.requireRole('admin')` works as before
390
+
391
+ ## Interface-Level Auth (Agent + Voice)
392
+
393
+ Agent and voice interfaces additionally declare auth **in their config** (a required `auth` key:
394
+ `{ "requireUser": boolean, "requireRole"?: string[] }`) because those sessions spend money without
395
+ necessarily calling a backend method — the platform gates the lobby itself, before any model or
396
+ media spend. `requireRole` uses the same manifest role ids with OR semantics. Denials reach the
397
+ frontend SDK as `MindStudioInterfaceError` codes `auth_required` (401) and `role_required` (403) —
398
+ route them to the app's login flow. See the `agentInterfaces` / `voiceInterfaces` skills for the
399
+ full contract. Method-level `auth.requireRole(...)` checks still apply to every tool call inside.
400
+
401
+ ## Apps Without Auth
402
+
403
+ 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.)
404
+
405
+ ## Important: Designing Auth in Web Interfaces
406
+
407
+ 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.
408
+
409
+ 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.
410
+
411
+ 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.
412
+
413
+ 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.
414
+
415
+ 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.
416
+
417
+ ### Rules for Building Auth Screens
418
+ **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!
419
+
420
+ **"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.
421
+
422
+ **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.
423
+
424
+ **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).
425
+
426
+ **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.
427
+
428
+ **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.
429
+
430
+ ## Testing Auth in Development
431
+
432
+ Auth works the same in dev/preview as in production — real verification codes are sent to real email addresses and phone numbers. There are two test bypasses:
433
+
434
+ - **Email:** `remy@mindstudio.ai` — verification code is always `123456`
435
+ - **Phone:** any `555` number (e.g. `+15551234567`) — verification code is always `123456`
436
+
437
+ 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.
438
+
439
+ 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).
440
+
441
+ 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.
442
+
443
+ Browser automation tools (screenshots, automated browser tests) handle their own auth sessions. Scenarios seed database data but do not create browser auth sessions.
@@ -1,3 +1,9 @@
1
+ ---
2
+ name: Files, Storage & CDN
3
+ what: Per-app blob storage the database doesn't model — user uploads, generated documents, marketing images. Stores are private by default (reads authed by the app session, or short-lived signed share links) or deliberately public — public files are CDN-served on the app's own domain and images resize on the fly via query params (width, height, crop, format, dpr), so the frontend requests the size it displays instead of CSS-scaling originals. User uploads go client-direct: the backend mints a token and the browser uploads straight to storage, bytes never pass through a method. SDK actions that produce files (generateImage, generatePdf, …) can write straight into a store, and the CLI uploads build-time assets (heroes, logos, OG images) so binaries never land in git. One store is shared across dev and prod on purpose.
4
+ when: Before defining a file store or writing any upload, download, share-link, or image-serving code — user uploads, generated documents, marketing assets, config blobs, anything durable the app stores outside the database.
5
+ ---
6
+
1
7
  # Files & Storage
2
8
 
3
9
  Per-app blob storage: user uploads, generated documents, images, marketing assets. **Think of a store
@@ -1,3 +1,9 @@
1
+ ---
2
+ name: Scenarios
3
+ what: Seed scripts that reset the dev database to a specific state — the platform truncates all tables, runs an async function of plain `db.push()` calls, then impersonates a role, so the same scenario always produces the same state. They're how the user tests the app from each role's perspective and how a freshly built app makes its first impression already populated with data that fits its vibe. Declared in the manifest, written at `dist/methods/.scenarios/`.
4
+ when: Before writing or editing a scenario — including the initial build, where scenarios are required. Covers file placement and imports, truncate semantics, what scenarios must not touch (file stores, data sources), and seeding realistic data and bespoke images.
5
+ ---
6
+
1
7
  # Scenarios
2
8
 
3
9
  Scenarios are seed scripts that set up the dev database into a specific state for testing. Instead of manually creating data through the app, run a scenario and get a repeatable starting point. A scenario is just an async function that uses the same `db.push()` calls as methods — no new API to learn.
@@ -1,3 +1,9 @@
1
+ ---
2
+ name: Secrets & Environment Variables
3
+ what: Encrypted per-app secrets injected into backend method execution as `process.env`, with separate dev and prod values resolved automatically by execution context — test keys in the sandbox, live keys in releases, same code. Only for third-party services the MindStudio SDK doesn't already cover (a Stripe key, a webhook signing secret, a direct external API): the SDK handles auth, billing, and key management for AI models and platform integrations itself, so most apps never need one. Backend-only — secrets never reach frontends or interfaces.
4
+ when: Before wiring a credential for an external service the SDK doesn't provide — never for AI model keys or platform-provided integrations (use the SDK), and never in frontend code.
5
+ ---
6
+
1
7
  # Secrets & Environment Variables
2
8
 
3
9
  Apps can store encrypted secrets (API keys, database URLs, tokens) that get injected into method execution as `process.env` variables. Secrets have separate dev and prod values — dev values are used in the editor sandbox, prod values are used in deployed releases.
@@ -385,15 +385,21 @@ How answering works:
385
385
  in-call verification flow. The agent can serve whatever anonymous callers are allowed, and
386
386
  offers verification when the caller wants something account-bound.
387
387
  - **Verification uses the app's own auth methods** (`sms-code` / `email-code` from the
388
- manifest), existing accounts only — there is no sign-up over the phone:
389
- - SMS: a code is texted to the number the caller is calling from, if an account has that
390
- number on file. No other number is possible by design.
391
- - Email: the caller says their address; the platform matches it against the app's users
392
- (transcription-tolerant — no letter-by-letter spelling ceremony) and emails the account's
393
- stored address a code.
394
- - The flow never confirms or denies that an account exists — a code is "sent if an account
395
- matches", always phrased that neutrally. The persona should offer verification naturally
396
- when it unlocks something, never as a robotic gate.
388
+ manifest):
389
+ - SMS: a code is texted to the phone number on the call (no other number is possible by
390
+ design), and confirming it signs the caller in — **creating their account if they're new**,
391
+ the same find-or-create policy `sms-code` has on web (enabling the method is what enables
392
+ sign-up; there is no separate toggle on either surface). After a first-time caller verifies,
393
+ the session-context method re-fires with the fresh identity — that's the hook for seeding a
394
+ new account with data.
395
+ - Email: existing accounts only — the caller says their address; the platform matches it
396
+ against the app's users (transcription-tolerant — no letter-by-letter spelling ceremony) and
397
+ emails the account's stored address a code. There is no sign-up by email over the phone: a
398
+ call can't reliably capture a verbatim never-seen address, so new callers sign up by text
399
+ instead.
400
+ - The email flow never confirms or denies that an account exists — a code is "sent if an
401
+ account matches", always phrased that neutrally. The persona should offer verification
402
+ naturally when it unlocks something, never as a robotic gate.
397
403
  - **Verified mid-call, upgraded mid-call**: once the code checks out, the session becomes that
398
404
  user's — Current User block, roles on every tool call — without redialing.
399
405