@remix-run/cli 0.1.0 → 0.3.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.
Files changed (110) hide show
  1. package/README.md +0 -3
  2. package/dist/lib/bootstrap-project.d.ts.map +1 -1
  3. package/dist/lib/bootstrap-project.js +32 -9
  4. package/dist/lib/cli.d.ts +29 -0
  5. package/dist/lib/cli.d.ts.map +1 -1
  6. package/dist/lib/cli.js +33 -14
  7. package/dist/lib/commands/completion.d.ts.map +1 -1
  8. package/dist/lib/commands/completion.js +5 -1
  9. package/dist/lib/commands/doctor.js +18 -18
  10. package/dist/lib/commands/help.d.ts.map +1 -1
  11. package/dist/lib/commands/help.js +9 -33
  12. package/dist/lib/commands/routes.js +3 -3
  13. package/dist/lib/commands/test.d.ts +1 -1
  14. package/dist/lib/commands/test.d.ts.map +1 -1
  15. package/dist/lib/commands/test.js +8 -4
  16. package/dist/lib/completion.d.ts.map +1 -1
  17. package/dist/lib/completion.js +4 -106
  18. package/dist/lib/controller-files.d.ts +0 -1
  19. package/dist/lib/controller-files.d.ts.map +1 -1
  20. package/dist/lib/controller-files.js +3 -5
  21. package/dist/lib/controller-ownership.d.ts +9 -9
  22. package/dist/lib/controller-ownership.d.ts.map +1 -1
  23. package/dist/lib/controller-ownership.js +56 -91
  24. package/dist/lib/doctor/controller-findings.d.ts +1 -1
  25. package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
  26. package/dist/lib/doctor/controller-findings.js +15 -87
  27. package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -1
  28. package/dist/lib/doctor/controller-fix-plans.js +13 -24
  29. package/dist/lib/doctor/controller-placeholders.d.ts +2 -3
  30. package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -1
  31. package/dist/lib/doctor/controller-placeholders.js +18 -149
  32. package/dist/lib/doctor/controllers.js +1 -1
  33. package/dist/lib/doctor/project.js +60 -52
  34. package/dist/lib/doctor/types.d.ts +2 -2
  35. package/dist/lib/doctor/types.d.ts.map +1 -1
  36. package/dist/lib/errors.d.ts +0 -6
  37. package/dist/lib/errors.d.ts.map +1 -1
  38. package/dist/lib/errors.js +0 -11
  39. package/dist/lib/load-route-map-worker.js +17 -9
  40. package/dist/lib/route-map.d.ts +1 -1
  41. package/dist/lib/route-map.d.ts.map +1 -1
  42. package/dist/lib/route-map.js +29 -17
  43. package/package.json +4 -5
  44. package/src/lib/bootstrap-project.ts +39 -13
  45. package/src/lib/cli.ts +46 -15
  46. package/src/lib/commands/completion.ts +6 -1
  47. package/src/lib/commands/doctor.ts +18 -21
  48. package/src/lib/commands/help.ts +9 -43
  49. package/src/lib/commands/routes.ts +3 -3
  50. package/src/lib/commands/test.ts +10 -4
  51. package/src/lib/completion.ts +4 -151
  52. package/src/lib/controller-files.ts +4 -8
  53. package/src/lib/controller-ownership.ts +78 -141
  54. package/src/lib/doctor/controller-findings.ts +20 -97
  55. package/src/lib/doctor/controller-fix-plans.ts +13 -29
  56. package/src/lib/doctor/controller-placeholders.ts +17 -189
  57. package/src/lib/doctor/controllers.ts +1 -1
  58. package/src/lib/doctor/project.ts +60 -52
  59. package/src/lib/doctor/types.ts +1 -5
  60. package/src/lib/errors.ts +0 -12
  61. package/src/lib/load-route-map-worker.ts +19 -10
  62. package/src/lib/route-map.ts +61 -16
  63. package/template/.agents/skills/remix/SKILL.md +588 -0
  64. package/template/.agents/skills/remix/references/animate-elements.md +195 -0
  65. package/template/.agents/skills/remix/references/assets-and-browser-modules.md +130 -0
  66. package/template/.agents/skills/remix/references/auth-and-sessions.md +443 -0
  67. package/template/.agents/skills/remix/references/component-model.md +282 -0
  68. package/template/.agents/skills/remix/references/create-mixins.md +158 -0
  69. package/template/.agents/skills/remix/references/data-and-validation.md +379 -0
  70. package/template/.agents/skills/remix/references/hydration-frames-navigation.md +297 -0
  71. package/template/.agents/skills/remix/references/middleware-and-server.md +233 -0
  72. package/template/.agents/skills/remix/references/mixins-styling-events.md +213 -0
  73. package/template/.agents/skills/remix/references/routing-and-controllers.md +391 -0
  74. package/template/.agents/skills/remix/references/testing-patterns.md +172 -0
  75. package/{bootstrap → template}/AGENTS.md +13 -8
  76. package/template/README.md +29 -0
  77. package/template/app/actions/controller.tsx +18 -0
  78. package/template/app/assets/entry.ts +8 -0
  79. package/template/app/assets/prompt-button.tsx +163 -0
  80. package/template/app/assets.ts +20 -0
  81. package/template/app/middleware/render.tsx +43 -0
  82. package/template/app/router.ts +20 -0
  83. package/template/app/routes.ts +6 -0
  84. package/template/app/ui/document.tsx +33 -0
  85. package/template/app/ui/scaffold-home-page.tsx +527 -0
  86. package/template/gitignore +4 -0
  87. package/{bootstrap → template}/package.json +5 -6
  88. package/template/public/favicon.svg +11 -0
  89. package/{bootstrap → template}/server.ts +4 -5
  90. package/{bootstrap → template}/tsconfig.json +3 -3
  91. package/bootstrap/README.md +0 -27
  92. package/bootstrap/app/controllers/auth.tsx +0 -21
  93. package/bootstrap/app/controllers/home.tsx +0 -26
  94. package/bootstrap/app/router.ts +0 -10
  95. package/bootstrap/app/routes.ts +0 -6
  96. package/bootstrap/app/ui/document.tsx +0 -21
  97. package/bootstrap/app/ui/layout.tsx +0 -22
  98. package/bootstrap/app/utils/render.tsx +0 -7
  99. package/dist/lib/commands/skills.d.ts +0 -6
  100. package/dist/lib/commands/skills.d.ts.map +0 -1
  101. package/dist/lib/commands/skills.js +0 -222
  102. package/dist/lib/skills-cache.d.ts +0 -19
  103. package/dist/lib/skills-cache.d.ts.map +0 -1
  104. package/dist/lib/skills-cache.js +0 -89
  105. package/dist/lib/skills.d.ts +0 -30
  106. package/dist/lib/skills.d.ts.map +0 -1
  107. package/dist/lib/skills.js +0 -441
  108. package/src/lib/commands/skills.ts +0 -306
  109. package/src/lib/skills-cache.ts +0 -140
  110. package/src/lib/skills.ts +0 -706
@@ -0,0 +1,443 @@
1
+ # Authentication and Sessions
2
+
3
+ ## What This Covers
4
+
5
+ How to remember things about a browser between requests and how to identify a user. Read this when
6
+ the task involves:
7
+
8
+ - Storing per-browser state across requests (login, cart, "I have submitted this form")
9
+ - Adding a credentials login flow or an OAuth provider
10
+ - Protecting routes with `requireAuth()` or stacking authorization checks
11
+ - Reading or writing `Session`, `Auth`, or other identity-related context values
12
+ - Logging in, logging out, or rotating session IDs
13
+
14
+ For raw cookies that are not session-backed (theme, locale, dismissed-banner), see
15
+ `createCookie` in this file plus the broader `Package Map` in `SKILL.md`.
16
+
17
+ ## Sessions vs Plain Cookies
18
+
19
+ Reach for `remix/session` when state is sensitive, must be tamper-resistant, or represents the
20
+ identity of a request: who is logged in, which form a browser already submitted, what items are in
21
+ a cart. Sessions sign or encrypt their backing cookie with a server-held secret and give you a
22
+ typed `Session` object you can `get`, `set`, `flash`, `unset`, and `regenerateId`.
23
+
24
+ Reach for `remix/cookie` directly when the browser is allowed to carry the value and the server
25
+ does not need session semantics. This often means preferences (theme, locale, dismissed banner),
26
+ but a signed cookie can also be fine for small low-risk values where you truly only need one
27
+ cookie-shaped fact and do not need `Session` helpers.
28
+
29
+ If a malicious user editing the value would be a bug, or if the value needs server-managed
30
+ lifecycle, reach for a session.
31
+
32
+ ### Quick chooser
33
+
34
+ | Need | Best fit | Why |
35
+ | ------------------------------------------------------------------- | --------------- | -------------------------------------------------- |
36
+ | Theme, locale, dismissed banner | `remix/cookie` | Browser-controlled preference |
37
+ | Small signed hint with minimal lifecycle | `remix/cookie` | One value, no `Session` helpers needed |
38
+ | "This browser already submitted", cart, flash messages, login state | `remix/session` | Tamper-sensitive, server-managed per-browser state |
39
+ | "One real person only", ownership, durable identity | account/auth | Cookies or sessions alone do not prove personhood |
40
+
41
+ ## Session Setup
42
+
43
+ ### Create a session cookie
44
+
45
+ ```typescript
46
+ import { createCookie } from 'remix/cookie'
47
+
48
+ let sessionSecret = process.env.SESSION_SECRET
49
+ if (!sessionSecret && process.env.NODE_ENV !== 'test') {
50
+ throw new Error('SESSION_SECRET is required')
51
+ }
52
+
53
+ export let sessionCookie = createCookie('session', {
54
+ secrets: [sessionSecret ?? 'test-only-secret'],
55
+ httpOnly: true,
56
+ sameSite: 'Lax',
57
+ secure: process.env.NODE_ENV === 'production',
58
+ maxAge: 2592000, // 30 days
59
+ path: '/',
60
+ })
61
+ ```
62
+
63
+ The cookie should always be `httpOnly`, default to `sameSite: 'Lax'`, and be `secure` in
64
+ production. Demo defaults like `'s3cr3t'` are fine in tests but should never reach production —
65
+ fail fast when the secret is missing.
66
+
67
+ ### Create session storage
68
+
69
+ ```typescript
70
+ // Filesystem storage
71
+ import { createFsSessionStorage } from 'remix/session-storage/fs'
72
+ export let sessionStorage = createFsSessionStorage('./tmp/sessions')
73
+
74
+ // Memory storage (for tests)
75
+ import { createMemorySessionStorage } from 'remix/session-storage/memory'
76
+ export let sessionStorage = createMemorySessionStorage()
77
+ ```
78
+
79
+ ### Add session middleware
80
+
81
+ ```typescript
82
+ import { session } from 'remix/middleware/session'
83
+
84
+ let router = createRouter({
85
+ middleware: [
86
+ session(sessionCookie, sessionStorage),
87
+ // ... other middleware
88
+ ],
89
+ })
90
+ ```
91
+
92
+ ### Using sessions in handlers
93
+
94
+ ```typescript
95
+ import { Session } from 'remix/session'
96
+
97
+ async function handler({ get }) {
98
+ let session = get(Session)
99
+
100
+ // Read
101
+ let userId = session.get('userId')
102
+
103
+ // Write
104
+ session.set('userId', 42)
105
+
106
+ // Flash (read once, then cleared)
107
+ session.flash('message', 'Settings saved!')
108
+ let message = session.get('message') // returns and clears
109
+
110
+ // Remove a key
111
+ session.unset('userId')
112
+
113
+ // Regenerate session ID (after login/logout)
114
+ session.regenerateId(true)
115
+ }
116
+ ```
117
+
118
+ ### Sessions for non-auth state
119
+
120
+ Sessions are not just for login. They are the right place to store any tamper-sensitive
121
+ per-browser fact: which form a browser already submitted, how many free actions are left in a
122
+ trial, which feature flags a tester opted into, what items are in a cart.
123
+
124
+ ```typescript
125
+ async function submit({ get }) {
126
+ let session = get(Session)
127
+ if (session.get('hasSubmitted')) {
128
+ return render(<AlreadySubmittedPage />, { status: 409 })
129
+ }
130
+
131
+ let parsed = s.parseSafe(submitSchema, get(FormData))
132
+ if (!parsed.success) {
133
+ return render(<SubmitPage errors={parsed.issues} />, { status: 400 })
134
+ }
135
+
136
+ await saveSubmission(parsed.value)
137
+ session.set('hasSubmitted', true)
138
+ session.flash('message', 'Thanks for submitting!')
139
+
140
+ return redirect(routes.thanks.href())
141
+ }
142
+ ```
143
+
144
+ Notice that there is no manual `Set-Cookie` plumbing in the action — the session middleware handles
145
+ that, and the handler returns an ordinary `Response`. Per-browser state enforced this way is still
146
+ bypassable by clearing cookies; if the guarantee needs to survive that, you also need an account
147
+ (see auth providers below).
148
+
149
+ ## Auth Middleware
150
+
151
+ ### Basic setup
152
+
153
+ ```typescript
154
+ import { auth, createSessionAuthScheme } from 'remix/middleware/auth'
155
+ import { Session } from 'remix/session'
156
+ import { Database } from 'remix/data-table'
157
+
158
+ export function loadAuth() {
159
+ return auth({
160
+ schemes: [
161
+ createSessionAuthScheme({
162
+ read(session) {
163
+ let data = session.get('auth')
164
+ return data ?? null
165
+ },
166
+ async verify(value, context) {
167
+ let db = context.get(Database)
168
+ return (await db.find(users, value.userId)) ?? null
169
+ },
170
+ invalidate(session) {
171
+ session.unset('auth')
172
+ },
173
+ }),
174
+ ],
175
+ })
176
+ }
177
+ ```
178
+
179
+ ### Reading auth state
180
+
181
+ ```typescript
182
+ import { Auth } from 'remix/middleware/auth'
183
+
184
+ function handler({ get }) {
185
+ let auth = get(Auth)
186
+
187
+ if (auth.ok) {
188
+ // User is authenticated
189
+ let user = auth.identity
190
+ }
191
+ }
192
+ ```
193
+
194
+ ## Credentials Auth
195
+
196
+ ### Define a credentials provider
197
+
198
+ ```typescript
199
+ import { createCredentialsAuthProvider, verifyCredentials, completeAuth } from 'remix/auth'
200
+ import * as s from 'remix/data-schema'
201
+ import * as f from 'remix/data-schema/form-data'
202
+
203
+ let loginSchema = f.object({
204
+ email: f.field(s.defaulted(s.string(), '')),
205
+ password: f.field(s.defaulted(s.string(), '')),
206
+ })
207
+
208
+ export let passwordProvider = createCredentialsAuthProvider({
209
+ parse(context) {
210
+ let formData = context.get(FormData)
211
+ return s.parse(loginSchema, formData)
212
+ },
213
+ async verify({ email, password }, context) {
214
+ let db = context.get(Database)
215
+ let user = await db.findOne(users, { where: { email } })
216
+ if (!user || !(await verifyPassword(password, user.password_hash))) {
217
+ return null
218
+ }
219
+ return user
220
+ },
221
+ })
222
+ ```
223
+
224
+ ### Login action
225
+
226
+ ```typescript
227
+ import { verifyCredentials, completeAuth } from 'remix/auth'
228
+ import { redirect } from 'remix/response/redirect'
229
+
230
+ async action(context) {
231
+ let user = await verifyCredentials(passwordProvider, context)
232
+
233
+ if (user == null) {
234
+ let session = context.get(Session)
235
+ session.flash('error', 'Invalid email or password.')
236
+ return redirect(routes.auth.login.href())
237
+ }
238
+
239
+ let session = completeAuth(context)
240
+ session.set('auth', { userId: user.id })
241
+
242
+ return redirect(routes.home.href())
243
+ },
244
+ ```
245
+
246
+ ### Logout action
247
+
248
+ ```typescript
249
+ import { Session } from 'remix/session'
250
+ import { redirect } from 'remix/response/redirect'
251
+
252
+ function logout(context) {
253
+ let session = context.get(Session)
254
+ session.unset('auth')
255
+ session.regenerateId(true)
256
+ return redirect(routes.home.href())
257
+ }
258
+ ```
259
+
260
+ ## OAuth / External Auth
261
+
262
+ ### Create providers
263
+
264
+ ```typescript
265
+ import {
266
+ createAtmosphereAuthProvider,
267
+ createGoogleAuthProvider,
268
+ createGitHubAuthProvider,
269
+ startExternalAuth,
270
+ finishExternalAuth,
271
+ completeAuth,
272
+ refreshExternalAuth,
273
+ } from 'remix/auth'
274
+
275
+ let googleProvider = createGoogleAuthProvider({
276
+ clientId: process.env.GOOGLE_CLIENT_ID,
277
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
278
+ redirectUri: new URL(routes.auth.google.callback.href(), origin),
279
+ })
280
+
281
+ let githubProvider = createGitHubAuthProvider({
282
+ clientId: process.env.GITHUB_CLIENT_ID,
283
+ clientSecret: process.env.GITHUB_CLIENT_SECRET,
284
+ redirectUri: new URL(routes.auth.github.callback.href(), origin),
285
+ })
286
+
287
+ let atmosphereSessionSecret = process.env.ATMOSPHERE_SESSION_SECRET
288
+ if (!atmosphereSessionSecret && process.env.NODE_ENV !== 'test') {
289
+ throw new Error('ATMOSPHERE_SESSION_SECRET is required')
290
+ }
291
+
292
+ let atmosphereProvider = createAtmosphereAuthProvider({
293
+ clientId: 'https://app.example.com/oauth/client-metadata.json',
294
+ redirectUri: new URL(routes.auth.atmosphere.callback.href(), origin),
295
+ sessionSecret: atmosphereSessionSecret ?? 'test-only-secret',
296
+ })
297
+ ```
298
+
299
+ For Atmosphere-compatible atproto OAuth, create the provider once, call
300
+ `atmosphereProvider.prepare(handleOrDid)` before `startExternalAuth(...)`, then pass the same
301
+ module-scope provider to `finishExternalAuth(...)` and `refreshExternalAuth(...)`.
302
+
303
+ ### OAuth controller
304
+
305
+ ```typescript
306
+ import { createController } from 'remix/router'
307
+
308
+ export default createController(routes.auth.google, {
309
+ actions: {
310
+ // GET /auth/google — redirect to Google
311
+ async index(context) {
312
+ return await startExternalAuth(googleProvider, context, {
313
+ returnTo: context.url.searchParams.get('returnTo'),
314
+ })
315
+ },
316
+
317
+ // GET /auth/google/callback — handle redirect back
318
+ async callback(context) {
319
+ let { result, returnTo } = await finishExternalAuth(googleProvider, context)
320
+
321
+ let db = context.get(Database)
322
+ let { user, authAccount } = await resolveExternalAuth(db, result)
323
+
324
+ let session = completeAuth(context)
325
+ session.set('auth', {
326
+ userId: user.id,
327
+ loginMethod: result.provider,
328
+ authAccountId: authAccount.id,
329
+ })
330
+
331
+ return redirect(returnTo ?? routes.account.index.href())
332
+ },
333
+ },
334
+ })
335
+ ```
336
+
337
+ ### Refresh stored provider tokens
338
+
339
+ Use `refreshExternalAuth(provider, tokens)` when an app has stored OAuth/OIDC tokens and needs a
340
+ fresh access token from a refresh token. Built-in OIDC providers, X, and Atmosphere support
341
+ refresh-token exchange. If the provider does not rotate the refresh token, the refreshed bundle
342
+ preserves the current one.
343
+
344
+ ```typescript
345
+ async function refreshGoogleTokens({ get }) {
346
+ let db = get(Database)
347
+ let account = await db.findOne(authAccounts, { where: { provider: 'google' } })
348
+ if (!account) return null
349
+
350
+ let refreshed = await refreshExternalAuth(googleProvider, account.tokens)
351
+ await db.update(authAccounts, account.id, { tokens: refreshed.tokens })
352
+
353
+ return refreshed.tokens
354
+ }
355
+ ```
356
+
357
+ ## Protecting Routes
358
+
359
+ ### Controller-level protection
360
+
361
+ Apply `requireAuth()` to every action in one controller:
362
+
363
+ ```typescript
364
+ import { createController } from 'remix/router'
365
+ import { requireAuth } from 'remix/middleware/auth'
366
+
367
+ export default createController(routes.account, {
368
+ middleware: [requireAuth()],
369
+ actions: {
370
+ index() {
371
+ /* guaranteed authenticated */
372
+ },
373
+ },
374
+ })
375
+ ```
376
+
377
+ Nested route maps need their own explicit protection:
378
+
379
+ ```typescript
380
+ // app/router.ts
381
+ router.map(routes.account, accountController)
382
+ router.map(routes.account.settings, accountSettingsController)
383
+
384
+ // app/actions/account/settings/controller.tsx
385
+ export default createController(routes.account.settings, {
386
+ middleware: [requireAuth()],
387
+ actions: {
388
+ index() {
389
+ /* guaranteed authenticated */
390
+ },
391
+ update() {
392
+ /* guaranteed authenticated */
393
+ },
394
+ },
395
+ })
396
+ ```
397
+
398
+ ### Stacking middleware
399
+
400
+ Combine auth checks with role checks:
401
+
402
+ ```typescript
403
+ export default createController(routes.admin, {
404
+ middleware: [requireAuth(), requireAdmin()],
405
+ actions: {
406
+ index() {
407
+ /* requires auth + admin */
408
+ },
409
+ },
410
+ })
411
+ ```
412
+
413
+ ### Action-level protection
414
+
415
+ Apply middleware to a single route:
416
+
417
+ ```typescript
418
+ import { Auth, requireAuth } from 'remix/middleware/auth'
419
+
420
+ router.get(routes.account.index, {
421
+ middleware: [requireAuth()],
422
+ handler(context) {
423
+ let auth = context.get(Auth)
424
+ return render(<AccountPage identity={auth.identity} />)
425
+ },
426
+ })
427
+ ```
428
+
429
+ ### Redirect on auth failure
430
+
431
+ ```typescript
432
+ import { requireAuth } from 'remix/middleware/auth'
433
+ import { redirect } from 'remix/response/redirect'
434
+
435
+ export function requireAuthRedirect() {
436
+ return requireAuth({
437
+ onFailure(context) {
438
+ let returnTo = encodeURIComponent(context.url.pathname)
439
+ return redirect(routes.auth.login.href() + `?returnTo=${returnTo}`, 303)
440
+ },
441
+ })
442
+ }
443
+ ```