Your calm window into the Atmosphere. morgen.blue
rss atproto
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: wire ATProto OAuth login via revolution/laravel-bluesky

Replaces the Fortify scaffolding (torn out in 6352493) with session-based
auth backed by ATProto OAuth. Data-minimization is the core stance — the
users table stores only `did` (primary key), encrypted `refresh_token`,
and `iss`; handle + full OAuthSession live in Laravel session storage
and are refetched on every login.

- Users table rewritten (did as string PK, no id/name/email/password).
- User model uses WithBluesky trait + minimal tokenForBluesky().
- AuthenticatedSessionController + single-invoke OAuthCallbackController.
Both explicitly setScopes() to override the package's hardcoded
transition:* defaults.
- OAuthSessionUpdated + OAuthSessionRefreshing listeners keep DB and
session store in sync; refresh race is handled by Laravel's default
database session driver serializing per-user requests.
- Granular repo:app.skyreader.* scopes (no transition:generic).
- Custom /oauth-client-metadata.json + /oauth-jwks.json routes +
OAuthConfig::clientMetadataUsing override so the client_id/URLs
point at our canonical paths (not the package's /bluesky/oauth/*).
- / route intercepts ?iss callback in dev (package hardcodes the dev
redirect to 127.0.0.1:8000).
- Inertia shares auth.handle from the session; User type + UserInfo +
UserMenuContent + AppHeader rewritten (handle + initials, no avatar).
- Settings profile + delete-user flow removed pending ATProto-native
profile surface; settings redirects to /settings/appearance.
- Pest tests cover login render, POST redirect, callback happy path,
and logout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+1349 -4446
-524
.agents/skills/inertia-react-development/SKILL.md
··· 1 - --- 2 - name: inertia-react-development 3 - description: "Develops Inertia.js v3 React client-side applications. Activates when creating React pages, forms, or navigation; using <Link>, <Form>, useForm, useHttp, setLayoutProps, or router; working with deferred props, prefetching, optimistic updates, instant visits, or polling; or when user mentions React with Inertia, React pages, React forms, or React navigation." 4 - license: MIT 5 - metadata: 6 - author: laravel 7 - --- 8 - 9 - # Inertia React Development 10 - 11 - ## When to Apply 12 - 13 - Activate this skill when: 14 - 15 - - Creating or modifying React page components for Inertia 16 - - Working with forms in React (using `<Form>`, `useForm`, or `useHttp`) 17 - - Implementing client-side navigation with `<Link>` or `router` 18 - - Using v3 features: deferred props, prefetching, optimistic updates, instant visits, layout props, HTTP requests, WhenVisible, InfiniteScroll, once props, flash data, or polling 19 - - Building React-specific features with the Inertia protocol 20 - 21 - ## Documentation 22 - 23 - Use `search-docs` for detailed Inertia v3 React patterns and documentation. 24 - 25 - ## Basic Usage 26 - 27 - ### Page Components Location 28 - 29 - React page components should be placed in the `resources/js/pages` directory. 30 - 31 - ### Page Component Structure 32 - 33 - <!-- Basic React Page Component --> 34 - ```react 35 - export default function UsersIndex({ users }) { 36 - return ( 37 - <div> 38 - <h1>Users</h1> 39 - <ul> 40 - {users.map(user => <li key={user.id}>{user.name}</li>)} 41 - </ul> 42 - </div> 43 - ) 44 - } 45 - ``` 46 - 47 - ## Client-Side Navigation 48 - 49 - ### Basic Link Component 50 - 51 - Use `<Link>` for client-side navigation instead of traditional `<a>` tags: 52 - 53 - <!-- Inertia React Navigation --> 54 - ```react 55 - import { Link, router } from '@inertiajs/react' 56 - 57 - <Link href="/">Home</Link> 58 - <Link href="/users">Users</Link> 59 - <Link href={`/users/${user.id}`}>View User</Link> 60 - ``` 61 - 62 - ### Link with Method 63 - 64 - <!-- Link with POST Method --> 65 - ```react 66 - import { Link } from '@inertiajs/react' 67 - 68 - <Link href="/logout" method="post" as="button"> 69 - Logout 70 - </Link> 71 - ``` 72 - 73 - ### Prefetching 74 - 75 - Prefetch pages to improve perceived performance: 76 - 77 - <!-- Prefetch on Hover --> 78 - ```react 79 - import { Link } from '@inertiajs/react' 80 - 81 - <Link href="/users" prefetch> 82 - Users 83 - </Link> 84 - ``` 85 - 86 - ### Programmatic Navigation 87 - 88 - <!-- Router Visit --> 89 - ```react 90 - import { router } from '@inertiajs/react' 91 - 92 - function handleClick() { 93 - router.visit('/users') 94 - } 95 - 96 - // Or with options 97 - router.visit('/users', { 98 - method: 'post', 99 - data: { name: 'John' }, 100 - onSuccess: () => console.log('Success!'), 101 - }) 102 - ``` 103 - 104 - ## Form Handling 105 - 106 - ### Form Component (Recommended) 107 - 108 - The recommended way to build forms is with the `<Form>` component: 109 - 110 - <!-- Form Component Example --> 111 - ```react 112 - import { Form } from '@inertiajs/react' 113 - 114 - export default function CreateUser() { 115 - return ( 116 - <Form action="/users" method="post"> 117 - {({ errors, processing, wasSuccessful }) => ( 118 - <> 119 - <input type="text" name="name" /> 120 - {errors.name && <div>{errors.name}</div>} 121 - 122 - <input type="email" name="email" /> 123 - {errors.email && <div>{errors.email}</div>} 124 - 125 - <button type="submit" disabled={processing}> 126 - {processing ? 'Creating...' : 'Create User'} 127 - </button> 128 - 129 - {wasSuccessful && <div>User created!</div>} 130 - </> 131 - )} 132 - </Form> 133 - ) 134 - } 135 - ``` 136 - 137 - ### Form Component With All Props 138 - 139 - <!-- Form Component Full Example --> 140 - ```react 141 - import { Form } from '@inertiajs/react' 142 - 143 - <Form action="/users" method="post"> 144 - {({ 145 - errors, 146 - hasErrors, 147 - processing, 148 - progress, 149 - wasSuccessful, 150 - recentlySuccessful, 151 - clearErrors, 152 - resetAndClearErrors, 153 - defaults, 154 - isDirty, 155 - reset, 156 - submit 157 - }) => ( 158 - <> 159 - <input type="text" name="name" defaultValue={defaults.name} /> 160 - {errors.name && <div>{errors.name}</div>} 161 - 162 - <button type="submit" disabled={processing}> 163 - {processing ? 'Saving...' : 'Save'} 164 - </button> 165 - 166 - {progress && ( 167 - <progress value={progress.percentage} max="100"> 168 - {progress.percentage}% 169 - </progress> 170 - )} 171 - 172 - {wasSuccessful && <div>Saved!</div>} 173 - </> 174 - )} 175 - </Form> 176 - ``` 177 - 178 - ### Form Component Reset Props 179 - 180 - The `<Form>` component supports automatic resetting: 181 - 182 - - `resetOnError` - Reset form data when the request fails 183 - - `resetOnSuccess` - Reset form data when the request succeeds 184 - - `setDefaultsOnSuccess` - Update default values on success 185 - 186 - Use the `search-docs` tool with a query of `form component resetting` for detailed guidance. 187 - 188 - <!-- Form with Reset Props --> 189 - ```react 190 - import { Form } from '@inertiajs/react' 191 - 192 - <Form 193 - action="/users" 194 - method="post" 195 - resetOnSuccess 196 - setDefaultsOnSuccess 197 - > 198 - {({ errors, processing, wasSuccessful }) => ( 199 - <> 200 - <input type="text" name="name" /> 201 - {errors.name && <div>{errors.name}</div>} 202 - 203 - <button type="submit" disabled={processing}> 204 - Submit 205 - </button> 206 - </> 207 - )} 208 - </Form> 209 - ``` 210 - 211 - Forms can also be built using the `useForm` helper for more programmatic control. Use the `search-docs` tool with a query of `useForm helper` for guidance. 212 - 213 - ### `useForm` Hook 214 - 215 - For more programmatic control or to follow existing conventions, use the `useForm` hook: 216 - 217 - <!-- useForm Hook Example --> 218 - ```react 219 - import { useForm } from '@inertiajs/react' 220 - 221 - export default function CreateUser() { 222 - const { data, setData, post, processing, errors, reset } = useForm({ 223 - name: '', 224 - email: '', 225 - password: '', 226 - }) 227 - 228 - function submit(e) { 229 - e.preventDefault() 230 - post('/users', { 231 - onSuccess: () => reset('password'), 232 - }) 233 - } 234 - 235 - return ( 236 - <form onSubmit={submit}> 237 - <input 238 - type="text" 239 - value={data.name} 240 - onChange={e => setData('name', e.target.value)} 241 - /> 242 - {errors.name && <div>{errors.name}</div>} 243 - 244 - <input 245 - type="email" 246 - value={data.email} 247 - onChange={e => setData('email', e.target.value)} 248 - /> 249 - {errors.email && <div>{errors.email}</div>} 250 - 251 - <input 252 - type="password" 253 - value={data.password} 254 - onChange={e => setData('password', e.target.value)} 255 - /> 256 - {errors.password && <div>{errors.password}</div>} 257 - 258 - <button type="submit" disabled={processing}> 259 - Create User 260 - </button> 261 - </form> 262 - ) 263 - } 264 - ``` 265 - 266 - ## Inertia v3 Features 267 - 268 - ### HTTP Requests 269 - 270 - Use the `useHttp` hook for standalone HTTP requests that do not trigger Inertia page visits. It provides the same developer experience as `useForm`, but for plain JSON endpoints. 271 - 272 - <!-- useHttp Example --> 273 - ```react 274 - import { useHttp } from '@inertiajs/react' 275 - 276 - export default function Search() { 277 - const { data, setData, get, processing } = useHttp({ 278 - query: '', 279 - }) 280 - 281 - function search(e) { 282 - setData('query', e.target.value) 283 - get('/api/search', { 284 - onSuccess: (response) => { 285 - console.log(response) 286 - }, 287 - }) 288 - } 289 - 290 - return ( 291 - <> 292 - <input value={data.query} onChange={search} /> 293 - {processing && <div>Searching...</div>} 294 - </> 295 - ) 296 - } 297 - ``` 298 - 299 - ### Optimistic Updates 300 - 301 - Apply data changes instantly before the server responds, with automatic rollback on failure: 302 - 303 - <!-- Optimistic Update with Router --> 304 - ```react 305 - import { router } from '@inertiajs/react' 306 - 307 - function like(post) { 308 - router.optimistic((props) => ({ 309 - post: { 310 - ...props.post, 311 - likes: props.post.likes + 1, 312 - }, 313 - })).post(`/posts/${post.id}/like`) 314 - } 315 - ``` 316 - 317 - Optimistic updates also work with `useForm` and the `<Form>` component: 318 - 319 - <!-- Optimistic Update with Form Component --> 320 - ```react 321 - import { Form } from '@inertiajs/react' 322 - 323 - <Form 324 - action="/todos" 325 - method="post" 326 - optimistic={(props, data) => ({ 327 - todos: [...props.todos, { id: Date.now(), name: data.name, done: false }], 328 - })} 329 - > 330 - <input type="text" name="name" /> 331 - <button type="submit">Add Todo</button> 332 - </Form> 333 - ``` 334 - 335 - ### Instant Visits 336 - 337 - Navigate to a new page immediately without waiting for the server response. The target component renders right away with shared props, while page-specific props load in the background. 338 - 339 - <!-- Instant Visit with Link --> 340 - ```react 341 - import { Link } from '@inertiajs/react' 342 - 343 - <Link href="/dashboard" component="Dashboard">Dashboard</Link> 344 - 345 - <Link 346 - href="/posts/1" 347 - component="Posts/Show" 348 - pageProps={{ post: { id: 1, title: 'My Post' } }} 349 - > 350 - View Post 351 - </Link> 352 - ``` 353 - 354 - ### Layout Props 355 - 356 - Share dynamic data between pages and persistent layouts: 357 - 358 - <!-- Layout Props in Layout --> 359 - ```react 360 - export default function Layout({ title = 'My App', showSidebar = true, children }) { 361 - return ( 362 - <> 363 - <header>{title}</header> 364 - {showSidebar && <aside>Sidebar</aside>} 365 - <main>{children}</main> 366 - </> 367 - ) 368 - } 369 - ``` 370 - 371 - <!-- Setting Layout Props from Page --> 372 - ```react 373 - import { setLayoutProps } from '@inertiajs/react' 374 - 375 - export default function Dashboard() { 376 - setLayoutProps({ 377 - title: 'Dashboard', 378 - showSidebar: false, 379 - }) 380 - 381 - return <h1>Dashboard</h1> 382 - } 383 - ``` 384 - 385 - ### Deferred Props 386 - 387 - Use deferred props to load data after initial page render: 388 - 389 - <!-- Deferred Props with Empty State --> 390 - ```react 391 - export default function UsersIndex({ users }) { 392 - return ( 393 - <div> 394 - <h1>Users</h1> 395 - {!users ? ( 396 - <div className="animate-pulse"> 397 - <div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div> 398 - <div className="h-4 bg-gray-200 rounded w-1/2"></div> 399 - </div> 400 - ) : ( 401 - <ul> 402 - {users.map(user => ( 403 - <li key={user.id}>{user.name}</li> 404 - ))} 405 - </ul> 406 - )} 407 - </div> 408 - ) 409 - } 410 - ``` 411 - 412 - ### Polling 413 - 414 - Use the `usePoll` hook to automatically refresh data at intervals. It handles cleanup on unmount and throttles polling when the tab is inactive. 415 - 416 - <!-- Basic Polling --> 417 - ```react 418 - import { usePoll } from '@inertiajs/react' 419 - 420 - export default function Dashboard({ stats }) { 421 - usePoll(5000) 422 - 423 - return ( 424 - <div> 425 - <h1>Dashboard</h1> 426 - <div>Active Users: {stats.activeUsers}</div> 427 - </div> 428 - ) 429 - } 430 - ``` 431 - 432 - <!-- Polling With Request Options and Manual Control --> 433 - ```react 434 - import { usePoll } from '@inertiajs/react' 435 - 436 - export default function Dashboard({ stats }) { 437 - const { start, stop } = usePoll(5000, { 438 - only: ['stats'], 439 - onStart() { 440 - console.log('Polling request started') 441 - }, 442 - onFinish() { 443 - console.log('Polling request finished') 444 - }, 445 - }, { 446 - autoStart: false, 447 - keepAlive: true, 448 - }) 449 - 450 - return ( 451 - <div> 452 - <h1>Dashboard</h1> 453 - <div>Active Users: {stats.activeUsers}</div> 454 - <button onClick={start}>Start Polling</button> 455 - <button onClick={stop}>Stop Polling</button> 456 - </div> 457 - ) 458 - } 459 - ``` 460 - 461 - - `autoStart` (default `true`) - set to `false` to start polling manually via the returned `start()` function 462 - - `keepAlive` (default `false`) - set to `true` to prevent throttling when the browser tab is inactive 463 - 464 - ### WhenVisible 465 - 466 - Lazy-load a prop when an element scrolls into view. Useful for deferring expensive data that sits below the fold: 467 - 468 - <!-- WhenVisible Example --> 469 - ```react 470 - import { WhenVisible } from '@inertiajs/react' 471 - 472 - export default function Dashboard({ stats }) { 473 - return ( 474 - <div> 475 - <h1>Dashboard</h1> 476 - 477 - <WhenVisible data="stats" buffer={200} fallback={<div className="animate-pulse">Loading stats...</div>}> 478 - {({ fetching }) => ( 479 - <div> 480 - <p>Total Users: {stats.total_users}</p> 481 - <p>Revenue: {stats.revenue}</p> 482 - {fetching && <span>Refreshing...</span>} 483 - </div> 484 - )} 485 - </WhenVisible> 486 - </div> 487 - ) 488 - } 489 - ``` 490 - 491 - ### InfiniteScroll 492 - 493 - Automatically load additional pages of paginated data as users scroll: 494 - 495 - <!-- InfiniteScroll Example --> 496 - ```react 497 - import { InfiniteScroll } from '@inertiajs/react' 498 - 499 - export default function Users({ users }) { 500 - return ( 501 - <InfiniteScroll data="users"> 502 - {users.data.map(user => ( 503 - <div key={user.id}>{user.name}</div> 504 - ))} 505 - </InfiniteScroll> 506 - ) 507 - } 508 - ``` 509 - 510 - The server must use `Inertia::scroll()` to configure the paginated data. Use the `search-docs` tool with a query of `infinite scroll` for detailed guidance on buffers, manual loading, reverse mode, and custom trigger elements. 511 - 512 - ## Server-Side Patterns 513 - 514 - Server-side patterns (Inertia::render, props, middleware) are covered in inertia-laravel guidelines. 515 - 516 - ## Common Pitfalls 517 - 518 - - Using traditional `<a>` links instead of Inertia's `<Link>` component (breaks SPA behavior) 519 - - Forgetting to add loading states (skeleton screens) when using deferred props 520 - - Not handling the `undefined` state of deferred props before data loads 521 - - Using `<form>` without preventing default submission (use `<Form>` component or `e.preventDefault()`) 522 - - Forgetting to check if `<Form>` component is available in your Inertia version 523 - - Using `router.cancel()` instead of `router.cancelAll()` (v3 breaking change) 524 - - Using `router.on('invalid', ...)` or `router.on('exception', ...)` instead of the renamed `httpException` and `networkError` events
-190
.agents/skills/laravel-best-practices/SKILL.md
··· 1 - --- 2 - name: laravel-best-practices 3 - description: "Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns." 4 - license: MIT 5 - metadata: 6 - author: laravel 7 - --- 8 - 9 - # Laravel Best Practices 10 - 11 - Best practices for Laravel, prioritized by impact. Each rule teaches what to do and why. For exact API syntax, verify with `search-docs`. 12 - 13 - ## Consistency First 14 - 15 - Before applying any rule, check what the application already does. Laravel offers multiple valid approaches — the best choice is the one the codebase already uses, even if another pattern would be theoretically better. Inconsistency is worse than a suboptimal pattern. 16 - 17 - Check sibling files, related controllers, models, or tests for established patterns. If one exists, follow it — don't introduce a second way. These rules are defaults for when no pattern exists yet, not overrides. 18 - 19 - ## Quick Reference 20 - 21 - ### 1. Database Performance → `rules/db-performance.md` 22 - 23 - - Eager load with `with()` to prevent N+1 queries 24 - - Enable `Model::preventLazyLoading()` in development 25 - - Select only needed columns, avoid `SELECT *` 26 - - `chunk()` / `chunkById()` for large datasets 27 - - Index columns used in `WHERE`, `ORDER BY`, `JOIN` 28 - - `withCount()` instead of loading relations to count 29 - - `cursor()` for memory-efficient read-only iteration 30 - - Never query in Blade templates 31 - 32 - ### 2. Advanced Query Patterns → `rules/advanced-queries.md` 33 - 34 - - `addSelect()` subqueries over eager-loading entire has-many for a single value 35 - - Dynamic relationships via subquery FK + `belongsTo` 36 - - Conditional aggregates (`CASE WHEN` in `selectRaw`) over multiple count queries 37 - - `setRelation()` to prevent circular N+1 queries 38 - - `whereIn` + `pluck()` over `whereHas` for better index usage 39 - - Two simple queries can beat one complex query 40 - - Compound indexes matching `orderBy` column order 41 - - Correlated subqueries in `orderBy` for has-many sorting (avoid joins) 42 - 43 - ### 3. Security → `rules/security.md` 44 - 45 - - Define `$fillable` or `$guarded` on every model, authorize every action via policies or gates 46 - - No raw SQL with user input — use Eloquent or query builder 47 - - `{{ }}` for output escaping, `@csrf` on all POST/PUT/DELETE forms, `throttle` on auth and API routes 48 - - Validate MIME type, extension, and size for file uploads 49 - - Never commit `.env`, use `config()` for secrets, `encrypted` cast for sensitive DB fields 50 - 51 - ### 4. Caching → `rules/caching.md` 52 - 53 - - `Cache::remember()` over manual get/put 54 - - `Cache::flexible()` for stale-while-revalidate on high-traffic data 55 - - `Cache::memo()` to avoid redundant cache hits within a request 56 - - Cache tags to invalidate related groups 57 - - `Cache::add()` for atomic conditional writes 58 - - `once()` to memoize per-request or per-object lifetime 59 - - `Cache::lock()` / `lockForUpdate()` for race conditions 60 - - Failover cache stores in production 61 - 62 - ### 5. Eloquent Patterns → `rules/eloquent.md` 63 - 64 - - Correct relationship types with return type hints 65 - - Local scopes for reusable query constraints 66 - - Global scopes sparingly — document their existence 67 - - Attribute casts in the `casts()` method 68 - - Cast date columns, use Carbon instances in templates 69 - - `whereBelongsTo($model)` for cleaner queries 70 - - Never hardcode table names — use `(new Model)->getTable()` or Eloquent queries 71 - 72 - ### 6. Validation & Forms → `rules/validation.md` 73 - 74 - - Form Request classes, not inline validation 75 - - Array notation `['required', 'email']` for new code; follow existing convention 76 - - `$request->validated()` only — never `$request->all()` 77 - - `Rule::when()` for conditional validation 78 - - `after()` instead of `withValidator()` 79 - 80 - ### 7. Configuration → `rules/config.md` 81 - 82 - - `env()` only inside config files 83 - - `App::environment()` or `app()->isProduction()` 84 - - Config, lang files, and constants over hardcoded text 85 - 86 - ### 8. Testing Patterns → `rules/testing.md` 87 - 88 - - `LazilyRefreshDatabase` over `RefreshDatabase` for speed 89 - - `assertModelExists()` over raw `assertDatabaseHas()` 90 - - Factory states and sequences over manual overrides 91 - - Use fakes (`Event::fake()`, `Exceptions::fake()`, etc.) — but always after factory setup, not before 92 - - `recycle()` to share relationship instances across factories 93 - 94 - ### 9. Queue & Job Patterns → `rules/queue-jobs.md` 95 - 96 - - `retry_after` must exceed job `timeout`; use exponential backoff `[1, 5, 10]` 97 - - `ShouldBeUnique` to prevent duplicates; `ShouldBeUniqueUntilProcessing` for early lock release 98 - - Always implement `failed()`; with `retryUntil()`, set `$tries = 0` 99 - - `RateLimited` middleware for external API calls; `Bus::batch()` for related jobs 100 - - Horizon for complex multi-queue scenarios 101 - 102 - ### 10. Routing & Controllers → `rules/routing.md` 103 - 104 - - Implicit route model binding 105 - - Scoped bindings for nested resources 106 - - `Route::resource()` or `apiResource()` 107 - - Methods under 10 lines — extract to actions/services 108 - - Type-hint Form Requests for auto-validation 109 - 110 - ### 11. HTTP Client → `rules/http-client.md` 111 - 112 - - Explicit `timeout` and `connectTimeout` on every request 113 - - `retry()` with exponential backoff for external APIs 114 - - Check response status or use `throw()` 115 - - `Http::pool()` for concurrent independent requests 116 - - `Http::fake()` and `preventStrayRequests()` in tests 117 - 118 - ### 12. Events, Notifications & Mail → `rules/events-notifications.md`, `rules/mail.md` 119 - 120 - - Event discovery over manual registration; `event:cache` in production 121 - - `ShouldDispatchAfterCommit` / `afterCommit()` inside transactions 122 - - Queue notifications and mailables with `ShouldQueue` 123 - - On-demand notifications for non-user recipients 124 - - `HasLocalePreference` on notifiable models 125 - - `assertQueued()` not `assertSent()` for queued mailables 126 - - Markdown mailables for transactional emails 127 - 128 - ### 13. Error Handling → `rules/error-handling.md` 129 - 130 - - `report()`/`render()` on exception classes or in `bootstrap/app.php` — follow existing pattern 131 - - `ShouldntReport` for exceptions that should never log 132 - - Throttle high-volume exceptions to protect log sinks 133 - - `dontReportDuplicates()` for multi-catch scenarios 134 - - Force JSON rendering for API routes 135 - - Structured context via `context()` on exception classes 136 - 137 - ### 14. Task Scheduling → `rules/scheduling.md` 138 - 139 - - `withoutOverlapping()` on variable-duration tasks 140 - - `onOneServer()` on multi-server deployments 141 - - `runInBackground()` for concurrent long tasks 142 - - `environments()` to restrict to appropriate environments 143 - - `takeUntilTimeout()` for time-bounded processing 144 - - Schedule groups for shared configuration 145 - 146 - ### 15. Architecture → `rules/architecture.md` 147 - 148 - - Single-purpose Action classes; dependency injection over `app()` helper 149 - - Prefer official Laravel packages and follow conventions, don't override defaults 150 - - Default to `ORDER BY id DESC` or `created_at DESC`; `mb_*` for UTF-8 safety 151 - - `defer()` for post-response work; `Context` for request-scoped data; `Concurrency::run()` for parallel execution 152 - 153 - ### 16. Migrations → `rules/migrations.md` 154 - 155 - - Generate migrations with `php artisan make:migration` 156 - - `constrained()` for foreign keys 157 - - Never modify migrations that have run in production 158 - - Add indexes in the migration, not as an afterthought 159 - - Mirror column defaults in model `$attributes` 160 - - Reversible `down()` by default; forward-fix migrations for intentionally irreversible changes 161 - - One concern per migration — never mix DDL and DML 162 - 163 - ### 17. Collections → `rules/collections.md` 164 - 165 - - Higher-order messages for simple collection operations 166 - - `cursor()` vs. `lazy()` — choose based on relationship needs 167 - - `lazyById()` when updating records while iterating 168 - - `toQuery()` for bulk operations on collections 169 - 170 - ### 18. Blade & Views → `rules/blade-views.md` 171 - 172 - - `$attributes->merge()` in component templates 173 - - Blade components over `@include`; `@pushOnce` for per-component scripts 174 - - View Composers for shared view data 175 - - `@aware` for deeply nested component props 176 - 177 - ### 19. Conventions & Style → `rules/style.md` 178 - 179 - - Follow Laravel naming conventions for all entities 180 - - Prefer Laravel helpers (`Str`, `Arr`, `Number`, `Uri`, `Str::of()`, `$request->string()`) over raw PHP functions 181 - - No JS/CSS in Blade, no HTML in PHP classes 182 - - Code should be readable; comments only for config files 183 - 184 - ## How to Apply 185 - 186 - Always use a sub-agent to read rule files and explore this skill's content. 187 - 188 - 1. Identify the file type and select relevant sections (e.g., migration → §16, controller → §1, §3, §5, §6, §10) 189 - 2. Check sibling files for existing patterns — follow those first per Consistency First 190 - 3. Verify API syntax with `search-docs` for the installed Laravel version
-106
.agents/skills/laravel-best-practices/rules/advanced-queries.md
··· 1 - # Advanced Query Patterns 2 - 3 - ## Use `addSelect()` Subqueries for Single Values from Has-Many 4 - 5 - Instead of eager-loading an entire has-many relationship for a single value (like the latest timestamp), use a correlated subquery via `addSelect()`. This pulls the value directly in the main SQL query — zero extra queries. 6 - 7 - ```php 8 - public function scopeWithLastLoginAt($query): void 9 - { 10 - $query->addSelect([ 11 - 'last_login_at' => Login::select('created_at') 12 - ->whereColumn('user_id', 'users.id') 13 - ->latest() 14 - ->take(1), 15 - ])->withCasts(['last_login_at' => 'datetime']); 16 - } 17 - ``` 18 - 19 - ## Create Dynamic Relationships via Subquery FK 20 - 21 - Extend the `addSelect()` pattern to fetch a foreign key via subquery, then define a `belongsTo` relationship on that virtual attribute. This provides a fully-hydrated related model without loading the entire collection. 22 - 23 - ```php 24 - public function lastLogin(): BelongsTo 25 - { 26 - return $this->belongsTo(Login::class); 27 - } 28 - 29 - public function scopeWithLastLogin($query): void 30 - { 31 - $query->addSelect([ 32 - 'last_login_id' => Login::select('id') 33 - ->whereColumn('user_id', 'users.id') 34 - ->latest() 35 - ->take(1), 36 - ])->with('lastLogin'); 37 - } 38 - ``` 39 - 40 - ## Use Conditional Aggregates Instead of Multiple Count Queries 41 - 42 - Replace N separate `count()` queries with a single query using `CASE WHEN` inside `selectRaw()`. Use `toBase()` to skip model hydration when you only need scalar values. 43 - 44 - ```php 45 - $statuses = Feature::toBase() 46 - ->selectRaw("count(case when status = 'Requested' then 1 end) as requested") 47 - ->selectRaw("count(case when status = 'Planned' then 1 end) as planned") 48 - ->selectRaw("count(case when status = 'Completed' then 1 end) as completed") 49 - ->first(); 50 - ``` 51 - 52 - ## Use `setRelation()` to Prevent Circular N+1 53 - 54 - When a parent model is eager-loaded with its children, and the view also needs `$child->parent`, use `setRelation()` to inject the already-loaded parent rather than letting Eloquent fire N additional queries. 55 - 56 - ```php 57 - $feature->load('comments.user'); 58 - $feature->comments->each->setRelation('feature', $feature); 59 - ``` 60 - 61 - ## Prefer `whereIn` + Subquery Over `whereHas` 62 - 63 - `whereHas()` emits a correlated `EXISTS` subquery that re-executes per row. Using `whereIn()` with a `select('id')` subquery lets the database use an index lookup instead, without loading data into PHP memory. 64 - 65 - Incorrect (correlated EXISTS re-executes per row): 66 - 67 - ```php 68 - $query->whereHas('company', fn ($q) => $q->where('name', 'like', $term)); 69 - ``` 70 - 71 - Correct (index-friendly subquery, no PHP memory overhead): 72 - 73 - ```php 74 - $query->whereIn('company_id', Company::where('name', 'like', $term)->select('id')); 75 - ``` 76 - 77 - ## Sometimes Two Simple Queries Beat One Complex Query 78 - 79 - Running a small, targeted secondary query and passing its results via `whereIn` is often faster than a single complex correlated subquery or join. The additional round-trip is worthwhile when the secondary query is highly selective and uses its own index. 80 - 81 - ## Use Compound Indexes Matching `orderBy` Column Order 82 - 83 - When ordering by multiple columns, create a single compound index in the same column order as the `ORDER BY` clause. Individual single-column indexes cannot combine for multi-column sorts — the database will filesort without a compound index. 84 - 85 - ```php 86 - // Migration 87 - $table->index(['last_name', 'first_name']); 88 - 89 - // Query — column order must match the index 90 - User::query()->orderBy('last_name')->orderBy('first_name')->paginate(); 91 - ``` 92 - 93 - ## Use Correlated Subqueries for Has-Many Ordering 94 - 95 - When sorting by a value from a has-many relationship, avoid joins (they duplicate rows). Use a correlated subquery inside `orderBy()` instead, paired with an `addSelect` scope for eager loading. 96 - 97 - ```php 98 - public function scopeOrderByLastLogin($query): void 99 - { 100 - $query->orderByDesc(Login::select('created_at') 101 - ->whereColumn('user_id', 'users.id') 102 - ->latest() 103 - ->take(1) 104 - ); 105 - } 106 - ```
-202
.agents/skills/laravel-best-practices/rules/architecture.md
··· 1 - # Architecture Best Practices 2 - 3 - ## Single-Purpose Action Classes 4 - 5 - Extract discrete business operations into invokable Action classes. 6 - 7 - ```php 8 - class CreateOrderAction 9 - { 10 - public function __construct(private InventoryService $inventory) {} 11 - 12 - public function execute(array $data): Order 13 - { 14 - $order = Order::create($data); 15 - $this->inventory->reserve($order); 16 - 17 - return $order; 18 - } 19 - } 20 - ``` 21 - 22 - ## Use Dependency Injection 23 - 24 - Always use constructor injection. Avoid `app()` or `resolve()` inside classes. 25 - 26 - Incorrect: 27 - ```php 28 - class OrderController extends Controller 29 - { 30 - public function store(StoreOrderRequest $request) 31 - { 32 - $service = app(OrderService::class); 33 - 34 - return $service->create($request->validated()); 35 - } 36 - } 37 - ``` 38 - 39 - Correct: 40 - ```php 41 - class OrderController extends Controller 42 - { 43 - public function __construct(private OrderService $service) {} 44 - 45 - public function store(StoreOrderRequest $request) 46 - { 47 - return $this->service->create($request->validated()); 48 - } 49 - } 50 - ``` 51 - 52 - ## Code to Interfaces 53 - 54 - Depend on contracts at system boundaries (payment gateways, notification channels, external APIs) for testability and swappability. 55 - 56 - Incorrect (concrete dependency): 57 - ```php 58 - class OrderService 59 - { 60 - public function __construct(private StripeGateway $gateway) {} 61 - } 62 - ``` 63 - 64 - Correct (interface dependency): 65 - ```php 66 - interface PaymentGateway 67 - { 68 - public function charge(int $amount, string $customerId): PaymentResult; 69 - } 70 - 71 - class OrderService 72 - { 73 - public function __construct(private PaymentGateway $gateway) {} 74 - } 75 - ``` 76 - 77 - Bind in a service provider: 78 - 79 - ```php 80 - $this->app->bind(PaymentGateway::class, StripeGateway::class); 81 - ``` 82 - 83 - ## Default Sort by Descending 84 - 85 - When no explicit order is specified, sort by `id` or `created_at` descending. Without an explicit `ORDER BY`, row order is undefined. 86 - 87 - Incorrect: 88 - ```php 89 - $posts = Post::paginate(); 90 - ``` 91 - 92 - Correct: 93 - ```php 94 - $posts = Post::latest()->paginate(); 95 - ``` 96 - 97 - ## Use Atomic Locks for Race Conditions 98 - 99 - Prevent race conditions with `Cache::lock()` or `lockForUpdate()`. 100 - 101 - ```php 102 - Cache::lock('order-processing-'.$order->id, 10)->block(5, function () use ($order) { 103 - $order->process(); 104 - }); 105 - 106 - // Or at query level 107 - $product = Product::where('id', $id)->lockForUpdate()->first(); 108 - ``` 109 - 110 - ## Use `mb_*` String Functions 111 - 112 - When no Laravel helper exists, prefer `mb_strlen`, `mb_strtolower`, etc. for UTF-8 safety. Standard PHP string functions count bytes, not characters. 113 - 114 - Incorrect: 115 - ```php 116 - strlen('José'); // 5 (bytes, not characters) 117 - strtolower('MÜNCHEN'); // 'mÜnchen' — fails on multibyte 118 - ``` 119 - 120 - Correct: 121 - ```php 122 - mb_strlen('José'); // 4 (characters) 123 - mb_strtolower('MÜNCHEN'); // 'münchen' 124 - 125 - // Prefer Laravel's Str helpers when available 126 - Str::length('José'); // 4 127 - Str::lower('MÜNCHEN'); // 'münchen' 128 - ``` 129 - 130 - ## Use `defer()` for Post-Response Work 131 - 132 - For lightweight tasks that don't need to survive a crash (logging, analytics, cleanup), use `defer()` instead of dispatching a job. The callback runs after the HTTP response is sent — no queue overhead. 133 - 134 - Incorrect (job overhead for trivial work): 135 - ```php 136 - dispatch(new LogPageView($page)); 137 - ``` 138 - 139 - Correct (runs after response, same process): 140 - ```php 141 - defer(fn () => PageView::create(['page_id' => $page->id, 'user_id' => auth()->id()])); 142 - ``` 143 - 144 - Use jobs when the work must survive process crashes or needs retry logic. Use `defer()` for fire-and-forget work. 145 - 146 - ## Use `Context` for Request-Scoped Data 147 - 148 - The `Context` facade passes data through the entire request lifecycle — middleware, controllers, jobs, logs — without passing arguments manually. 149 - 150 - ```php 151 - // In middleware 152 - Context::add('tenant_id', $request->header('X-Tenant-ID')); 153 - 154 - // Anywhere later — controllers, jobs, log context 155 - $tenantId = Context::get('tenant_id'); 156 - ``` 157 - 158 - Context data automatically propagates to queued jobs and is included in log entries. Use `Context::addHidden()` for sensitive data that should be available in queued jobs but excluded from log context. If data must not leave the current process, do not store it in `Context`. 159 - 160 - ## Use `Concurrency::run()` for Parallel Execution 161 - 162 - Run independent operations in parallel using child processes — no async libraries needed. 163 - 164 - ```php 165 - use Illuminate\Support\Facades\Concurrency; 166 - 167 - [$users, $orders] = Concurrency::run([ 168 - fn () => User::count(), 169 - fn () => Order::where('status', 'pending')->count(), 170 - ]); 171 - ``` 172 - 173 - Each closure runs in a separate process with full Laravel access. Use for independent database queries, API calls, or computations that would otherwise run sequentially. 174 - 175 - ## Convention Over Configuration 176 - 177 - Follow Laravel conventions. Don't override defaults unnecessarily. 178 - 179 - Incorrect: 180 - ```php 181 - class Customer extends Model 182 - { 183 - protected $table = 'Customer'; 184 - protected $primaryKey = 'customer_id'; 185 - 186 - public function roles(): BelongsToMany 187 - { 188 - return $this->belongsToMany(Role::class, 'role_customer', 'customer_id', 'role_id'); 189 - } 190 - } 191 - ``` 192 - 193 - Correct: 194 - ```php 195 - class Customer extends Model 196 - { 197 - public function roles(): BelongsToMany 198 - { 199 - return $this->belongsToMany(Role::class); 200 - } 201 - } 202 - ```
-36
.agents/skills/laravel-best-practices/rules/blade-views.md
··· 1 - # Blade & Views Best Practices 2 - 3 - ## Use `$attributes->merge()` in Component Templates 4 - 5 - Hardcoding classes prevents consumers from adding their own. `merge()` combines class attributes cleanly. 6 - 7 - ```blade 8 - <div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}> 9 - {{ $message }} 10 - </div> 11 - ``` 12 - 13 - ## Use `@pushOnce` for Per-Component Scripts 14 - 15 - If a component renders inside a `@foreach`, `@push` inserts the script N times. `@pushOnce` guarantees it's included exactly once. 16 - 17 - ## Prefer Blade Components Over `@include` 18 - 19 - `@include` shares all parent variables implicitly (hidden coupling). Components have explicit props, attribute bags, and slots. 20 - 21 - ## Use View Composers for Shared View Data 22 - 23 - If every controller rendering a sidebar must pass `$categories`, that's duplicated code. A View Composer centralizes it. 24 - 25 - ## Use Blade Fragments for Partial Re-Renders (htmx/Turbo) 26 - 27 - A single view can return either the full page or just a fragment, keeping routing clean. 28 - 29 - ```php 30 - return view('dashboard', compact('users')) 31 - ->fragmentIf($request->hasHeader('HX-Request'), 'user-list'); 32 - ``` 33 - 34 - ## Use `@aware` for Deeply Nested Component Props 35 - 36 - Avoids re-passing parent props through every level of nested components.
-70
.agents/skills/laravel-best-practices/rules/caching.md
··· 1 - # Caching Best Practices 2 - 3 - ## Use `Cache::remember()` Instead of Manual Get/Put 4 - 5 - Cleaner cache-aside pattern that removes boilerplate. use `Cache::lock()` for race conditions. 6 - 7 - Incorrect: 8 - ```php 9 - $val = Cache::get('stats'); 10 - if (! $val) { 11 - $val = $this->computeStats(); 12 - Cache::put('stats', $val, 60); 13 - } 14 - ``` 15 - 16 - Correct: 17 - ```php 18 - $val = Cache::remember('stats', 60, fn () => $this->computeStats()); 19 - ``` 20 - 21 - ## Use `Cache::flexible()` for Stale-While-Revalidate 22 - 23 - On high-traffic keys, one user always gets a slow response when the cache expires. `flexible()` serves slightly stale data while refreshing in the background. 24 - 25 - Incorrect: `Cache::remember('users', 300, fn () => User::all());` 26 - 27 - Correct: `Cache::flexible('users', [300, 600], fn () => User::all());` — fresh for 5 min, stale-but-served up to 10 min, refreshes via deferred function. 28 - 29 - ## Use `Cache::memo()` to Avoid Redundant Hits Within a Request 30 - 31 - If the same cache key is read multiple times per request (e.g., a service called from multiple places), `memo()` stores the resolved value in memory. 32 - 33 - `Cache::memo()->get('settings');` — 5 calls = 1 Redis round-trip instead of 5. 34 - 35 - ## Use Cache Tags to Invalidate Related Groups 36 - 37 - Without tags, invalidating a group of entries requires tracking every key. Tags let you flush atomically. Only works with `redis`, `memcached`, `dynamodb` — not `file` or `database`. 38 - 39 - ```php 40 - Cache::tags(['user-1'])->flush(); 41 - ``` 42 - 43 - ## Use `Cache::add()` for Atomic Conditional Writes 44 - 45 - `add()` only writes if the key does not exist — atomic, no race condition between checking and writing. 46 - 47 - Incorrect: `if (! Cache::has('lock')) { Cache::put('lock', true, 10); }` 48 - 49 - Correct: `Cache::add('lock', true, 10);` 50 - 51 - ## Use `once()` for Per-Request Memoization 52 - 53 - `once()` memoizes a function's return value for the lifetime of the object (or request for closures). Unlike `Cache::memo()`, it doesn't hit the cache store at all — pure in-memory. 54 - 55 - ```php 56 - public function roles(): Collection 57 - { 58 - return once(fn () => $this->loadRoles()); 59 - } 60 - ``` 61 - 62 - Multiple calls return the cached result without re-executing. Use `once()` for expensive computations called multiple times per request. Use `Cache::memo()` when you also want cross-request caching. 63 - 64 - ## Configure Failover Cache Stores in Production 65 - 66 - If Redis goes down, the app falls back to a secondary store automatically. 67 - 68 - ```php 69 - 'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']], 70 - ```
-44
.agents/skills/laravel-best-practices/rules/collections.md
··· 1 - # Collection Best Practices 2 - 3 - ## Use Higher-Order Messages for Simple Operations 4 - 5 - Incorrect: 6 - ```php 7 - $users->each(function (User $user) { 8 - $user->markAsVip(); 9 - }); 10 - ``` 11 - 12 - Correct: `$users->each->markAsVip();` 13 - 14 - Works with `each`, `map`, `sum`, `filter`, `reject`, `contains`, etc. 15 - 16 - ## Choose `cursor()` vs. `lazy()` Correctly 17 - 18 - - `cursor()` — one model in memory, but cannot eager-load relationships (N+1 risk). 19 - - `lazy()` — chunked pagination returning a flat LazyCollection, supports eager loading. 20 - 21 - Incorrect: `User::with('roles')->cursor()` — eager loading silently ignored. 22 - 23 - Correct: `User::with('roles')->lazy()` for relationship access; `User::cursor()` for attribute-only work. 24 - 25 - ## Use `lazyById()` When Updating Records While Iterating 26 - 27 - `lazy()` uses offset pagination — updating records during iteration can skip or double-process. `lazyById()` uses `id > last_id`, safe against mutation. 28 - 29 - ## Use `toQuery()` for Bulk Operations on Collections 30 - 31 - Avoids manual `whereIn` construction. 32 - 33 - Incorrect: `User::whereIn('id', $users->pluck('id'))->update([...]);` 34 - 35 - Correct: `$users->toQuery()->update([...]);` 36 - 37 - ## Use `#[CollectedBy]` for Custom Collection Classes 38 - 39 - More declarative than overriding `newCollection()`. 40 - 41 - ```php 42 - #[CollectedBy(UserCollection::class)] 43 - class User extends Model {} 44 - ```
-73
.agents/skills/laravel-best-practices/rules/config.md
··· 1 - # Configuration Best Practices 2 - 3 - ## `env()` Only in Config Files 4 - 5 - Direct `env()` calls may return `null` when config is cached. 6 - 7 - Incorrect: 8 - ```php 9 - $key = env('API_KEY'); 10 - ``` 11 - 12 - Correct: 13 - ```php 14 - // config/services.php 15 - 'key' => env('API_KEY'), 16 - 17 - // Application code 18 - $key = config('services.key'); 19 - ``` 20 - 21 - ## Use Encrypted Env or External Secrets 22 - 23 - Never store production secrets in plain `.env` files in version control. 24 - 25 - Incorrect: 26 - ```bash 27 - 28 - # .env committed to repo or shared in Slack 29 - 30 - STRIPE_SECRET=sk_live_abc123 31 - AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI 32 - ``` 33 - 34 - Correct: 35 - ```bash 36 - php artisan env:encrypt --env=production --readable 37 - php artisan env:decrypt --env=production 38 - ``` 39 - 40 - For cloud deployments, prefer the platform's native secret store (AWS Secrets Manager, Vault, etc.) and inject at runtime. 41 - 42 - ## Use `App::environment()` for Environment Checks 43 - 44 - Incorrect: 45 - ```php 46 - if (env('APP_ENV') === 'production') { 47 - ``` 48 - 49 - Correct: 50 - ```php 51 - if (app()->isProduction()) { 52 - // or 53 - if (App::environment('production')) { 54 - ``` 55 - 56 - ## Use Constants and Language Files 57 - 58 - Use class constants instead of hardcoded magic strings for model states, types, and statuses. 59 - 60 - ```php 61 - // Incorrect 62 - return $this->type === 'normal'; 63 - 64 - // Correct 65 - return $this->type === self::TYPE_NORMAL; 66 - ``` 67 - 68 - If the application already uses language files for localization, use `__()` for user-facing strings too. Do not introduce language files purely for English-only apps — simple string literals are fine there. 69 - 70 - ```php 71 - // Only when lang files already exist in the project 72 - return back()->with('message', __('app.article_added')); 73 - ```
-192
.agents/skills/laravel-best-practices/rules/db-performance.md
··· 1 - # Database Performance Best Practices 2 - 3 - ## Always Eager Load Relationships 4 - 5 - Lazy loading causes N+1 query problems — one query per loop iteration. Always use `with()` to load relationships upfront. 6 - 7 - Incorrect (N+1 — executes 1 + N queries): 8 - ```php 9 - $posts = Post::all(); 10 - foreach ($posts as $post) { 11 - echo $post->author->name; 12 - } 13 - ``` 14 - 15 - Correct (2 queries total): 16 - ```php 17 - $posts = Post::with('author')->get(); 18 - foreach ($posts as $post) { 19 - echo $post->author->name; 20 - } 21 - ``` 22 - 23 - Constrain eager loads to select only needed columns (always include the foreign key): 24 - 25 - ```php 26 - $users = User::with(['posts' => function ($query) { 27 - $query->select('id', 'user_id', 'title') 28 - ->where('published', true) 29 - ->latest() 30 - ->limit(10); 31 - }])->get(); 32 - ``` 33 - 34 - ## Prevent Lazy Loading in Development 35 - 36 - Enable this in `AppServiceProvider::boot()` to catch N+1 issues during development. 37 - 38 - ```php 39 - public function boot(): void 40 - { 41 - Model::preventLazyLoading(! app()->isProduction()); 42 - } 43 - ``` 44 - 45 - Throws `LazyLoadingViolationException` when a relationship is accessed without being eager-loaded. 46 - 47 - ## Select Only Needed Columns 48 - 49 - Avoid `SELECT *` — especially when tables have large text or JSON columns. 50 - 51 - Incorrect: 52 - ```php 53 - $posts = Post::with('author')->get(); 54 - ``` 55 - 56 - Correct: 57 - ```php 58 - $posts = Post::select('id', 'title', 'user_id', 'created_at') 59 - ->with(['author:id,name,avatar']) 60 - ->get(); 61 - ``` 62 - 63 - When selecting columns on eager-loaded relationships, always include the foreign key column or the relationship won't match. 64 - 65 - ## Chunk Large Datasets 66 - 67 - Never load thousands of records at once. Use chunking for batch processing. 68 - 69 - Incorrect: 70 - ```php 71 - $users = User::all(); 72 - foreach ($users as $user) { 73 - $user->notify(new WeeklyDigest); 74 - } 75 - ``` 76 - 77 - Correct: 78 - ```php 79 - User::where('subscribed', true)->chunk(200, function ($users) { 80 - foreach ($users as $user) { 81 - $user->notify(new WeeklyDigest); 82 - } 83 - }); 84 - ``` 85 - 86 - Use `chunkById()` when modifying records during iteration — standard `chunk()` uses OFFSET which shifts when rows change: 87 - 88 - ```php 89 - User::where('active', false)->chunkById(200, function ($users) { 90 - $users->each->delete(); 91 - }); 92 - ``` 93 - 94 - ## Add Database Indexes 95 - 96 - Index columns that appear in `WHERE`, `ORDER BY`, `JOIN`, and `GROUP BY` clauses. 97 - 98 - Incorrect: 99 - ```php 100 - Schema::create('orders', function (Blueprint $table) { 101 - $table->id(); 102 - $table->foreignId('user_id')->constrained(); 103 - $table->string('status'); 104 - $table->timestamps(); 105 - }); 106 - ``` 107 - 108 - Correct: 109 - ```php 110 - Schema::create('orders', function (Blueprint $table) { 111 - $table->id(); 112 - $table->foreignId('user_id')->index()->constrained(); 113 - $table->string('status')->index(); 114 - $table->timestamps(); 115 - $table->index(['status', 'created_at']); 116 - }); 117 - ``` 118 - 119 - Add composite indexes for common query patterns (e.g., `WHERE status = ? ORDER BY created_at`). 120 - 121 - ## Use `withCount()` for Counting Relations 122 - 123 - Never load entire collections just to count them. 124 - 125 - Incorrect: 126 - ```php 127 - $posts = Post::all(); 128 - foreach ($posts as $post) { 129 - echo $post->comments->count(); 130 - } 131 - ``` 132 - 133 - Correct: 134 - ```php 135 - $posts = Post::withCount('comments')->get(); 136 - foreach ($posts as $post) { 137 - echo $post->comments_count; 138 - } 139 - ``` 140 - 141 - Conditional counting: 142 - 143 - ```php 144 - $posts = Post::withCount([ 145 - 'comments', 146 - 'comments as approved_comments_count' => function ($query) { 147 - $query->where('approved', true); 148 - }, 149 - ])->get(); 150 - ``` 151 - 152 - ## Use `cursor()` for Memory-Efficient Iteration 153 - 154 - For read-only iteration over large result sets, `cursor()` loads one record at a time via a PHP generator. 155 - 156 - Incorrect: 157 - ```php 158 - $users = User::where('active', true)->get(); 159 - ``` 160 - 161 - Correct: 162 - ```php 163 - foreach (User::where('active', true)->cursor() as $user) { 164 - ProcessUser::dispatch($user->id); 165 - } 166 - ``` 167 - 168 - Use `cursor()` for read-only iteration. Use `chunk()` / `chunkById()` when modifying records. 169 - 170 - ## No Queries in Blade Templates 171 - 172 - Never execute queries in Blade templates. Pass data from controllers. 173 - 174 - Incorrect: 175 - ```blade 176 - @foreach (User::all() as $user) 177 - {{ $user->profile->name }} 178 - @endforeach 179 - ``` 180 - 181 - Correct: 182 - ```php 183 - // Controller 184 - $users = User::with('profile')->get(); 185 - return view('users.index', compact('users')); 186 - ``` 187 - 188 - ```blade 189 - @foreach ($users as $user) 190 - {{ $user->profile->name }} 191 - @endforeach 192 - ```
-148
.agents/skills/laravel-best-practices/rules/eloquent.md
··· 1 - # Eloquent Best Practices 2 - 3 - ## Use Correct Relationship Types 4 - 5 - Use `hasMany`, `belongsTo`, `morphMany`, etc. with proper return type hints. 6 - 7 - ```php 8 - public function comments(): HasMany 9 - { 10 - return $this->hasMany(Comment::class); 11 - } 12 - 13 - public function author(): BelongsTo 14 - { 15 - return $this->belongsTo(User::class, 'user_id'); 16 - } 17 - ``` 18 - 19 - ## Use Local Scopes for Reusable Queries 20 - 21 - Extract reusable query constraints into local scopes to avoid duplication. 22 - 23 - Incorrect: 24 - ```php 25 - $active = User::where('verified', true)->whereNotNull('activated_at')->get(); 26 - $articles = Article::whereHas('user', function ($q) { 27 - $q->where('verified', true)->whereNotNull('activated_at'); 28 - })->get(); 29 - ``` 30 - 31 - Correct: 32 - ```php 33 - public function scopeActive(Builder $query): Builder 34 - { 35 - return $query->where('verified', true)->whereNotNull('activated_at'); 36 - } 37 - 38 - // Usage 39 - $active = User::active()->get(); 40 - $articles = Article::whereHas('user', fn ($q) => $q->active())->get(); 41 - ``` 42 - 43 - ## Apply Global Scopes Sparingly 44 - 45 - Global scopes silently modify every query on the model, making debugging difficult. Prefer local scopes and reserve global scopes for truly universal constraints like soft deletes or multi-tenancy. 46 - 47 - Incorrect (global scope for a conditional filter): 48 - ```php 49 - class PublishedScope implements Scope 50 - { 51 - public function apply(Builder $builder, Model $model): void 52 - { 53 - $builder->where('published', true); 54 - } 55 - } 56 - // Now admin panels, reports, and background jobs all silently skip drafts 57 - ``` 58 - 59 - Correct (local scope you opt into): 60 - ```php 61 - public function scopePublished(Builder $query): Builder 62 - { 63 - return $query->where('published', true); 64 - } 65 - 66 - Post::published()->paginate(); // Explicit 67 - Post::paginate(); // Admin sees all 68 - ``` 69 - 70 - ## Define Attribute Casts 71 - 72 - Use the `casts()` method (or `$casts` property following project convention) for automatic type conversion. 73 - 74 - ```php 75 - protected function casts(): array 76 - { 77 - return [ 78 - 'is_active' => 'boolean', 79 - 'metadata' => 'array', 80 - 'total' => 'decimal:2', 81 - ]; 82 - } 83 - ``` 84 - 85 - ## Cast Date Columns Properly 86 - 87 - Always cast date columns. Use Carbon instances in templates instead of formatting strings manually. 88 - 89 - Incorrect: 90 - ```blade 91 - {{ Carbon::createFromFormat('Y-d-m H-i', $order->ordered_at)->toDateString() }} 92 - ``` 93 - 94 - Correct: 95 - ```php 96 - protected function casts(): array 97 - { 98 - return [ 99 - 'ordered_at' => 'datetime', 100 - ]; 101 - } 102 - ``` 103 - 104 - ```blade 105 - {{ $order->ordered_at->toDateString() }} 106 - {{ $order->ordered_at->format('m-d') }} 107 - ``` 108 - 109 - ## Use `whereBelongsTo()` for Relationship Queries 110 - 111 - Cleaner than manually specifying foreign keys. 112 - 113 - Incorrect: 114 - ```php 115 - Post::where('user_id', $user->id)->get(); 116 - ``` 117 - 118 - Correct: 119 - ```php 120 - Post::whereBelongsTo($user)->get(); 121 - Post::whereBelongsTo($user, 'author')->get(); 122 - ``` 123 - 124 - ## Avoid Hardcoded Table Names in Queries 125 - 126 - Never use string literals for table names in raw queries, joins, or subqueries. Hardcoded table names make it impossible to find all places a model is used and break refactoring (e.g., renaming a table requires hunting through every raw string). 127 - 128 - Incorrect: 129 - ```php 130 - DB::table('users')->where('active', true)->get(); 131 - 132 - $query->join('companies', 'companies.id', '=', 'users.company_id'); 133 - 134 - DB::select('SELECT * FROM orders WHERE status = ?', ['pending']); 135 - ``` 136 - 137 - Correct — reference the model's table: 138 - ```php 139 - DB::table((new User)->getTable())->where('active', true)->get(); 140 - 141 - // Even better — use Eloquent or the query builder instead of raw SQL 142 - User::where('active', true)->get(); 143 - Order::where('status', 'pending')->get(); 144 - ``` 145 - 146 - Prefer Eloquent queries and relationships over `DB::table()` whenever possible — they already reference the model's table. When `DB::table()` or raw joins are unavoidable, always use `(new Model)->getTable()` to keep the reference traceable. 147 - 148 - **Exception — migrations:** In migrations, hardcoded table names via `DB::table('settings')` are acceptable and preferred. Models change over time but migrations are frozen snapshots — referencing a model that is later renamed or deleted would break the migration.
-72
.agents/skills/laravel-best-practices/rules/error-handling.md
··· 1 - # Error Handling Best Practices 2 - 3 - ## Exception Reporting and Rendering 4 - 5 - There are two valid approaches — choose one and apply it consistently across the project. 6 - 7 - **Co-location on the exception class** — keeps behavior alongside the exception definition, easier to find: 8 - 9 - ```php 10 - class InvalidOrderException extends Exception 11 - { 12 - public function report(): void { /* custom reporting */ } 13 - 14 - public function render(Request $request): Response 15 - { 16 - return response()->view('errors.invalid-order', status: 422); 17 - } 18 - } 19 - ``` 20 - 21 - **Centralized in `bootstrap/app.php`** — all exception handling in one place, easier to see the full picture: 22 - 23 - ```php 24 - ->withExceptions(function (Exceptions $exceptions) { 25 - $exceptions->report(function (InvalidOrderException $e) { /* ... */ }); 26 - $exceptions->render(function (InvalidOrderException $e, Request $request) { 27 - return response()->view('errors.invalid-order', status: 422); 28 - }); 29 - }) 30 - ``` 31 - 32 - Check the existing codebase and follow whichever pattern is already established. 33 - 34 - ## Use `ShouldntReport` for Exceptions That Should Never Log 35 - 36 - More discoverable than listing classes in `dontReport()`. 37 - 38 - ```php 39 - class PodcastProcessingException extends Exception implements ShouldntReport {} 40 - ``` 41 - 42 - ## Throttle High-Volume Exceptions 43 - 44 - A single failing integration can flood error tracking. Use `throttle()` to rate-limit per exception type. 45 - 46 - ## Enable `dontReportDuplicates()` 47 - 48 - Prevents the same exception instance from being logged multiple times when `report($e)` is called in multiple catch blocks. 49 - 50 - ## Force JSON Error Rendering for API Routes 51 - 52 - Laravel auto-detects `Accept: application/json` but API clients may not set it. Explicitly declare JSON rendering for API routes. 53 - 54 - ```php 55 - $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { 56 - return $request->is('api/*') || $request->expectsJson(); 57 - }); 58 - ``` 59 - 60 - ## Add Context to Exception Classes 61 - 62 - Attach structured data to exceptions at the source via a `context()` method — Laravel includes it automatically in the log entry. 63 - 64 - ```php 65 - class InvalidOrderException extends Exception 66 - { 67 - public function context(): array 68 - { 69 - return ['order_id' => $this->orderId]; 70 - } 71 - } 72 - ```
-52
.agents/skills/laravel-best-practices/rules/events-notifications.md
··· 1 - # Events & Notifications Best Practices 2 - 3 - ## Rely on Event Discovery 4 - 5 - Laravel auto-discovers listeners by reading `handle(EventType $event)` type-hints. No manual registration needed in `AppServiceProvider`. 6 - 7 - ## Run `event:cache` in Production Deploy 8 - 9 - Event discovery scans the filesystem per-request in dev. Cache it in production: `php artisan optimize` or `php artisan event:cache`. 10 - 11 - ## Use `ShouldDispatchAfterCommit` Inside Transactions 12 - 13 - Without it, a queued listener may process before the DB transaction commits, reading data that doesn't exist yet. 14 - 15 - ```php 16 - class OrderShipped implements ShouldDispatchAfterCommit {} 17 - ``` 18 - 19 - ## Always Queue Notifications 20 - 21 - Notifications often hit external APIs (email, SMS, Slack). Without `ShouldQueue`, they block the HTTP response. 22 - 23 - ```php 24 - class InvoicePaid extends Notification implements ShouldQueue 25 - { 26 - use Queueable; 27 - } 28 - ``` 29 - 30 - ## Use `afterCommit()` on Notifications in Transactions 31 - 32 - Same race condition as events — call `afterCommit()` to delay dispatch until the transaction commits. 33 - 34 - ```php 35 - $user->notify((new InvoicePaid($invoice))->afterCommit()); 36 - ``` 37 - 38 - ## Route Notification Channels to Dedicated Queues 39 - 40 - Mail and database notifications have different priorities. Use `viaQueues()` to route them to separate queues. 41 - 42 - ## Use On-Demand Notifications for Non-User Recipients 43 - 44 - Avoid creating dummy models to send notifications to arbitrary addresses. 45 - 46 - ```php 47 - Notification::route('mail', 'admin@example.com')->notify(new SystemAlert()); 48 - ``` 49 - 50 - ## Implement `HasLocalePreference` on Notifiable Models 51 - 52 - Laravel automatically uses the user's preferred locale for all notifications and mailables — no per-call `locale()` needed.
-160
.agents/skills/laravel-best-practices/rules/http-client.md
··· 1 - # HTTP Client Best Practices 2 - 3 - ## Always Set Explicit Timeouts 4 - 5 - The default timeout is 30 seconds — too long for most API calls. Always set explicit `timeout` and `connectTimeout` to fail fast. 6 - 7 - Incorrect: 8 - ```php 9 - $response = Http::get('https://api.example.com/users'); 10 - ``` 11 - 12 - Correct: 13 - ```php 14 - $response = Http::timeout(5) 15 - ->connectTimeout(3) 16 - ->get('https://api.example.com/users'); 17 - ``` 18 - 19 - For service-specific clients, define timeouts in a macro: 20 - 21 - ```php 22 - Http::macro('github', function () { 23 - return Http::baseUrl('https://api.github.com') 24 - ->timeout(10) 25 - ->connectTimeout(3) 26 - ->withToken(config('services.github.token')); 27 - }); 28 - 29 - $response = Http::github()->get('/repos/laravel/framework'); 30 - ``` 31 - 32 - ## Use Retry with Backoff for External APIs 33 - 34 - External APIs have transient failures. Use `retry()` with increasing delays. 35 - 36 - Incorrect: 37 - ```php 38 - $response = Http::post('https://api.stripe.com/v1/charges', $data); 39 - 40 - if ($response->failed()) { 41 - throw new PaymentFailedException('Charge failed'); 42 - } 43 - ``` 44 - 45 - Correct: 46 - ```php 47 - $response = Http::retry([100, 500, 1000]) 48 - ->timeout(10) 49 - ->post('https://api.stripe.com/v1/charges', $data); 50 - ``` 51 - 52 - Only retry on specific errors: 53 - 54 - ```php 55 - $response = Http::retry(3, 100, function (Throwable $exception, PendingRequest $request) { 56 - return $exception instanceof ConnectionException 57 - || ($exception instanceof RequestException && $exception->response->serverError()); 58 - })->post('https://api.example.com/data'); 59 - ``` 60 - 61 - ## Handle Errors Explicitly 62 - 63 - The HTTP Client does not throw on 4xx/5xx by default. Always check status or use `throw()`. 64 - 65 - Incorrect: 66 - ```php 67 - $response = Http::get('https://api.example.com/users/1'); 68 - $user = $response->json(); // Could be an error body 69 - ``` 70 - 71 - Correct: 72 - ```php 73 - $response = Http::timeout(5) 74 - ->get('https://api.example.com/users/1') 75 - ->throw(); 76 - 77 - $user = $response->json(); 78 - ``` 79 - 80 - For graceful degradation: 81 - 82 - ```php 83 - $response = Http::get('https://api.example.com/users/1'); 84 - 85 - if ($response->successful()) { 86 - return $response->json(); 87 - } 88 - 89 - if ($response->notFound()) { 90 - return null; 91 - } 92 - 93 - $response->throw(); 94 - ``` 95 - 96 - ## Use Request Pooling for Concurrent Requests 97 - 98 - When making multiple independent API calls, use `Http::pool()` instead of sequential calls. 99 - 100 - Incorrect: 101 - ```php 102 - $users = Http::get('https://api.example.com/users')->json(); 103 - $posts = Http::get('https://api.example.com/posts')->json(); 104 - $comments = Http::get('https://api.example.com/comments')->json(); 105 - ``` 106 - 107 - Correct: 108 - ```php 109 - use Illuminate\Http\Client\Pool; 110 - 111 - $responses = Http::pool(fn (Pool $pool) => [ 112 - $pool->as('users')->get('https://api.example.com/users'), 113 - $pool->as('posts')->get('https://api.example.com/posts'), 114 - $pool->as('comments')->get('https://api.example.com/comments'), 115 - ]); 116 - 117 - $users = $responses['users']->json(); 118 - $posts = $responses['posts']->json(); 119 - ``` 120 - 121 - ## Fake HTTP Calls in Tests 122 - 123 - Never make real HTTP requests in tests. Use `Http::fake()` and `preventStrayRequests()`. 124 - 125 - Incorrect: 126 - ```php 127 - it('syncs user from API', function () { 128 - $service = new UserSyncService; 129 - $service->sync(1); // Hits the real API 130 - }); 131 - ``` 132 - 133 - Correct: 134 - ```php 135 - it('syncs user from API', function () { 136 - Http::preventStrayRequests(); 137 - 138 - Http::fake([ 139 - 'api.example.com/users/1' => Http::response([ 140 - 'name' => 'John Doe', 141 - 'email' => 'john@example.com', 142 - ]), 143 - ]); 144 - 145 - $service = new UserSyncService; 146 - $service->sync(1); 147 - 148 - Http::assertSent(function (Request $request) { 149 - return $request->url() === 'https://api.example.com/users/1'; 150 - }); 151 - }); 152 - ``` 153 - 154 - Test failure scenarios too: 155 - 156 - ```php 157 - Http::fake([ 158 - 'api.example.com/*' => Http::failedConnection(), 159 - ]); 160 - ```
-27
.agents/skills/laravel-best-practices/rules/mail.md
··· 1 - # Mail Best Practices 2 - 3 - ## Implement `ShouldQueue` on the Mailable Class 4 - 5 - Makes queueing the default regardless of how the mailable is dispatched. No need to remember `Mail::queue()` at every call site — `Mail::send()` also queues it. 6 - 7 - ## Use `afterCommit()` on Mailables Inside Transactions 8 - 9 - A queued mailable dispatched inside a transaction may process before the commit. Use `$this->afterCommit()` in the constructor. 10 - 11 - ## Use `assertQueued()` Not `assertSent()` for Queued Mailables 12 - 13 - `Mail::assertSent()` only catches synchronous mail. Queued mailables fail `assertSent` with a "Did you mean to use assertQueued()?" hint. 14 - 15 - Incorrect: `Mail::assertSent(OrderShipped::class);` when mailable implements `ShouldQueue`. 16 - 17 - Correct: `Mail::assertQueued(OrderShipped::class);` 18 - 19 - ## Use Markdown Mailables for Transactional Emails 20 - 21 - Markdown mailables auto-generate both HTML and plain-text versions, use responsive components, and allow global style customization. Generate with `--markdown` flag. 22 - 23 - ## Separate Content Tests from Sending Tests 24 - 25 - Content tests: instantiate the mailable directly, call `assertSeeInHtml()`. 26 - Sending tests: use `Mail::fake()` and `assertSent()`/`assertQueued()`. 27 - Don't mix them — it conflates concerns and makes tests brittle.
-121
.agents/skills/laravel-best-practices/rules/migrations.md
··· 1 - # Migration Best Practices 2 - 3 - ## Generate Migrations with Artisan 4 - 5 - Always use `php artisan make:migration` for consistent naming and timestamps. 6 - 7 - Incorrect (manually created file): 8 - ```php 9 - // database/migrations/posts_migration.php ← wrong naming, no timestamp 10 - ``` 11 - 12 - Correct (Artisan-generated): 13 - ```bash 14 - php artisan make:migration create_posts_table 15 - php artisan make:migration add_slug_to_posts_table 16 - ``` 17 - 18 - ## Use `constrained()` for Foreign Keys 19 - 20 - Automatic naming and referential integrity. 21 - 22 - ```php 23 - $table->foreignId('user_id')->constrained()->cascadeOnDelete(); 24 - 25 - // Non-standard names 26 - $table->foreignId('author_id')->constrained('users'); 27 - ``` 28 - 29 - ## Never Modify Deployed Migrations 30 - 31 - Once a migration has run in production, treat it as immutable. Create a new migration to change the table. 32 - 33 - Incorrect (editing a deployed migration): 34 - ```php 35 - // 2024_01_01_create_posts_table.php — already in production 36 - $table->string('slug')->unique(); // ← added after deployment 37 - ``` 38 - 39 - Correct (new migration to alter): 40 - ```php 41 - // 2024_03_15_add_slug_to_posts_table.php 42 - Schema::table('posts', function (Blueprint $table) { 43 - $table->string('slug')->unique()->after('title'); 44 - }); 45 - ``` 46 - 47 - ## Add Indexes in the Migration 48 - 49 - Add indexes when creating the table, not as an afterthought. Columns used in `WHERE`, `ORDER BY`, and `JOIN` clauses need indexes. 50 - 51 - Incorrect: 52 - ```php 53 - Schema::create('orders', function (Blueprint $table) { 54 - $table->id(); 55 - $table->foreignId('user_id')->constrained(); 56 - $table->string('status'); 57 - $table->timestamps(); 58 - }); 59 - ``` 60 - 61 - Correct: 62 - ```php 63 - Schema::create('orders', function (Blueprint $table) { 64 - $table->id(); 65 - $table->foreignId('user_id')->constrained()->index(); 66 - $table->string('status')->index(); 67 - $table->timestamp('shipped_at')->nullable()->index(); 68 - $table->timestamps(); 69 - }); 70 - ``` 71 - 72 - ## Mirror Defaults in Model `$attributes` 73 - 74 - When a column has a database default, mirror it in the model so new instances have correct values before saving. 75 - 76 - ```php 77 - // Migration 78 - $table->string('status')->default('pending'); 79 - 80 - // Model 81 - protected $attributes = [ 82 - 'status' => 'pending', 83 - ]; 84 - ``` 85 - 86 - ## Write Reversible `down()` Methods by Default 87 - 88 - Implement `down()` for schema changes that can be safely reversed so `migrate:rollback` works in CI and failed deployments. 89 - 90 - ```php 91 - public function down(): void 92 - { 93 - Schema::table('posts', function (Blueprint $table) { 94 - $table->dropColumn('slug'); 95 - }); 96 - } 97 - ``` 98 - 99 - For intentionally irreversible migrations (e.g., destructive data backfills), leave a clear comment and require a forward fix migration instead of pretending rollback is supported. 100 - 101 - ## Keep Migrations Focused 102 - 103 - One concern per migration. Never mix DDL (schema changes) and DML (data manipulation). 104 - 105 - Incorrect (partial failure creates unrecoverable state): 106 - ```php 107 - public function up(): void 108 - { 109 - Schema::create('settings', function (Blueprint $table) { ... }); 110 - DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']); 111 - } 112 - ``` 113 - 114 - Correct (separate migrations): 115 - ```php 116 - // Migration 1: create_settings_table 117 - Schema::create('settings', function (Blueprint $table) { ... }); 118 - 119 - // Migration 2: seed_default_settings 120 - DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']); 121 - ```
-144
.agents/skills/laravel-best-practices/rules/queue-jobs.md
··· 1 - # Queue & Job Best Practices 2 - 3 - ## Set `retry_after` Greater Than `timeout` 4 - 5 - If `retry_after` is shorter than the job's `timeout`, the queue worker re-dispatches the job while it's still running, causing duplicate execution. 6 - 7 - Incorrect (`retry_after` ≤ `timeout`): 8 - ```php 9 - class ProcessReport implements ShouldQueue 10 - { 11 - public $timeout = 120; 12 - } 13 - 14 - // config/queue.php — retry_after: 90 ← job retried while still running! 15 - ``` 16 - 17 - Correct (`retry_after` > `timeout`): 18 - ```php 19 - class ProcessReport implements ShouldQueue 20 - { 21 - public $timeout = 120; 22 - } 23 - 24 - // config/queue.php — retry_after: 180 ← safely longer than any job timeout 25 - ``` 26 - 27 - ## Use Exponential Backoff 28 - 29 - Use progressively longer delays between retries to avoid hammering failing services. 30 - 31 - Incorrect (fixed retry interval): 32 - ```php 33 - class SyncWithStripe implements ShouldQueue 34 - { 35 - public $tries = 3; 36 - // Default: retries immediately, overwhelming the API 37 - } 38 - ``` 39 - 40 - Correct (exponential backoff): 41 - ```php 42 - class SyncWithStripe implements ShouldQueue 43 - { 44 - public $tries = 3; 45 - public $backoff = [1, 5, 10]; 46 - } 47 - ``` 48 - 49 - ## Implement `ShouldBeUnique` 50 - 51 - Prevent duplicate job processing. 52 - 53 - ```php 54 - class GenerateInvoice implements ShouldQueue, ShouldBeUnique 55 - { 56 - public function uniqueId(): string 57 - { 58 - return $this->order->id; 59 - } 60 - 61 - public $uniqueFor = 3600; 62 - } 63 - ``` 64 - 65 - ## Always Implement `failed()` 66 - 67 - Handle errors explicitly — don't rely on silent failure. 68 - 69 - ```php 70 - public function failed(?Throwable $exception): void 71 - { 72 - $this->podcast->update(['status' => 'failed']); 73 - Log::error('Processing failed', ['id' => $this->podcast->id, 'error' => $exception->getMessage()]); 74 - } 75 - ``` 76 - 77 - ## Rate Limit External API Calls in Jobs 78 - 79 - Use `RateLimited` middleware to throttle jobs calling third-party APIs. 80 - 81 - ```php 82 - public function middleware(): array 83 - { 84 - return [new RateLimited('external-api')]; 85 - } 86 - ``` 87 - 88 - ## Batch Related Jobs 89 - 90 - Use `Bus::batch()` when jobs should succeed or fail together. 91 - 92 - ```php 93 - Bus::batch([ 94 - new ImportCsvChunk($chunk1), 95 - new ImportCsvChunk($chunk2), 96 - ]) 97 - ->then(fn (Batch $batch) => Notification::send($user, new ImportComplete)) 98 - ->catch(fn (Batch $batch, Throwable $e) => Log::error('Batch failed')) 99 - ->dispatch(); 100 - ``` 101 - 102 - ## `retryUntil()` Needs `$tries = 0` 103 - 104 - When using time-based retry limits, set `$tries = 0` to avoid premature failure. 105 - 106 - ```php 107 - public $tries = 0; 108 - 109 - public function retryUntil(): \DateTimeInterface 110 - { 111 - return now()->addHours(4); 112 - } 113 - ``` 114 - 115 - ## Use `ShouldBeUniqueUntilProcessing` for Early Lock Release 116 - 117 - `ShouldBeUnique` holds the lock until the job completes. `ShouldBeUniqueUntilProcessing` releases it when processing starts, allowing new instances to queue. 118 - 119 - ```php 120 - class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing 121 - { 122 - // Lock releases when processing begins, not when it finishes 123 - } 124 - ``` 125 - 126 - ## Use Horizon for Complex Queue Scenarios 127 - 128 - Use Laravel Horizon when you need monitoring, auto-scaling, failure tracking, or multiple queues with different priorities. 129 - 130 - ```php 131 - // config/horizon.php 132 - 'environments' => [ 133 - 'production' => [ 134 - 'supervisor-1' => [ 135 - 'connection' => 'redis', 136 - 'queue' => ['high', 'default', 'low'], 137 - 'balance' => 'auto', 138 - 'minProcesses' => 1, 139 - 'maxProcesses' => 10, 140 - 'tries' => 3, 141 - ], 142 - ], 143 - ], 144 - ```
-99
.agents/skills/laravel-best-practices/rules/routing.md
··· 1 - # Routing & Controllers Best Practices 2 - 3 - ## Use Implicit Route Model Binding 4 - 5 - Let Laravel resolve models automatically from route parameters. 6 - 7 - Incorrect: 8 - ```php 9 - public function show(int $id) 10 - { 11 - $post = Post::findOrFail($id); 12 - } 13 - ``` 14 - 15 - Correct: 16 - ```php 17 - public function show(Post $post) 18 - { 19 - return view('posts.show', ['post' => $post]); 20 - } 21 - ``` 22 - 23 - ## Use Scoped Bindings for Nested Resources 24 - 25 - Enforce parent-child relationships automatically. 26 - 27 - ```php 28 - Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) { 29 - // $post is automatically scoped to $user 30 - })->scopeBindings(); 31 - ``` 32 - 33 - ## Use Resource Controllers 34 - 35 - Use `Route::resource()` or `apiResource()` for RESTful endpoints. 36 - 37 - ```php 38 - Route::resource('posts', PostController::class); 39 - // In routes/api.php — the /api prefix is applied automatically 40 - Route::apiResource('posts', Api\PostController::class); 41 - ``` 42 - 43 - ## Keep Controllers Thin 44 - 45 - Aim for under 10 lines per method. Extract business logic to action or service classes. 46 - 47 - Incorrect: 48 - ```php 49 - public function store(Request $request) 50 - { 51 - $validated = $request->validate([...]); 52 - if ($request->hasFile('image')) { 53 - $request->file('image')->move(public_path('images')); 54 - } 55 - $post = Post::create($validated); 56 - $post->tags()->sync($validated['tags']); 57 - event(new PostCreated($post)); 58 - return redirect()->route('posts.show', $post); 59 - } 60 - ``` 61 - 62 - Correct: 63 - ```php 64 - public function store(StorePostRequest $request, CreatePostAction $create) 65 - { 66 - $post = $create->execute($request->validated()); 67 - 68 - return redirect()->route('posts.show', $post); 69 - } 70 - ``` 71 - 72 - ## Type-Hint Form Requests 73 - 74 - Type-hinting Form Requests triggers automatic validation and authorization before the method executes. 75 - 76 - Incorrect: 77 - ```php 78 - public function store(Request $request): RedirectResponse 79 - { 80 - $validated = $request->validate([ 81 - 'title' => ['required', 'max:255'], 82 - 'body' => ['required'], 83 - ]); 84 - 85 - Post::create($validated); 86 - 87 - return redirect()->route('posts.index'); 88 - } 89 - ``` 90 - 91 - Correct: 92 - ```php 93 - public function store(StorePostRequest $request): RedirectResponse 94 - { 95 - Post::create($request->validated()); 96 - 97 - return redirect()->route('posts.index'); 98 - } 99 - ```
-39
.agents/skills/laravel-best-practices/rules/scheduling.md
··· 1 - # Task Scheduling Best Practices 2 - 3 - ## Use `withoutOverlapping()` on Variable-Duration Tasks 4 - 5 - Without it, a long-running task spawns a second instance on the next tick, causing double-processing or resource exhaustion. 6 - 7 - ## Use `onOneServer()` on Multi-Server Deployments 8 - 9 - Without it, every server runs the same task simultaneously. Requires a shared cache driver (Redis, database, Memcached). 10 - 11 - ## Use `runInBackground()` for Concurrent Long Tasks 12 - 13 - By default, tasks at the same tick run sequentially. A slow first task delays all subsequent ones. `runInBackground()` runs them as separate processes. 14 - 15 - ## Use `environments()` to Restrict Tasks 16 - 17 - Prevent accidental execution of production-only tasks (billing, reporting) on staging. 18 - 19 - ```php 20 - Schedule::command('billing:charge')->monthly()->environments(['production']); 21 - ``` 22 - 23 - ## Use `takeUntilTimeout()` for Time-Bounded Processing 24 - 25 - A task running every 15 minutes that processes an unbounded cursor can overlap with the next run. Bound execution time. 26 - 27 - ## Use Schedule Groups for Shared Configuration 28 - 29 - Avoid repeating `->onOneServer()->timezone('America/New_York')` across many tasks. 30 - 31 - ```php 32 - Schedule::daily() 33 - ->onOneServer() 34 - ->timezone('America/New_York') 35 - ->group(function () { 36 - Schedule::command('emails:send --force'); 37 - Schedule::command('emails:prune'); 38 - }); 39 - ```
-198
.agents/skills/laravel-best-practices/rules/security.md
··· 1 - # Security Best Practices 2 - 3 - ## Mass Assignment Protection 4 - 5 - Every model must define `$fillable` (whitelist) or `$guarded` (blacklist). 6 - 7 - Incorrect: 8 - ```php 9 - class User extends Model 10 - { 11 - protected $guarded = []; // All fields are mass assignable 12 - } 13 - ``` 14 - 15 - Correct: 16 - ```php 17 - class User extends Model 18 - { 19 - protected $fillable = [ 20 - 'name', 21 - 'email', 22 - 'password', 23 - ]; 24 - } 25 - ``` 26 - 27 - Never use `$guarded = []` on models that accept user input. 28 - 29 - ## Authorize Every Action 30 - 31 - Use policies or gates in controllers. Never skip authorization. 32 - 33 - Incorrect: 34 - ```php 35 - public function update(UpdatePostRequest $request, Post $post) 36 - { 37 - $post->update($request->validated()); 38 - } 39 - ``` 40 - 41 - Correct: 42 - ```php 43 - public function update(UpdatePostRequest $request, Post $post) 44 - { 45 - Gate::authorize('update', $post); 46 - 47 - $post->update($request->validated()); 48 - } 49 - ``` 50 - 51 - Or via Form Request: 52 - 53 - ```php 54 - public function authorize(): bool 55 - { 56 - return $this->user()->can('update', $this->route('post')); 57 - } 58 - ``` 59 - 60 - ## Prevent SQL Injection 61 - 62 - Always use parameter binding. Never interpolate user input into queries. 63 - 64 - Incorrect: 65 - ```php 66 - DB::select("SELECT * FROM users WHERE name = '{$request->name}'"); 67 - ``` 68 - 69 - Correct: 70 - ```php 71 - User::where('name', $request->name)->get(); 72 - 73 - // Raw expressions with bindings 74 - User::whereRaw('LOWER(name) = ?', [strtolower($request->name)])->get(); 75 - ``` 76 - 77 - ## Escape Output to Prevent XSS 78 - 79 - Use `{{ }}` for HTML escaping. Only use `{!! !!}` for trusted, pre-sanitized content. 80 - 81 - Incorrect: 82 - ```blade 83 - {!! $user->bio !!} 84 - ``` 85 - 86 - Correct: 87 - ```blade 88 - {{ $user->bio }} 89 - ``` 90 - 91 - ## CSRF Protection 92 - 93 - Include `@csrf` in all POST/PUT/DELETE Blade forms. In Inertia apps, the `@csrf` directive is automatically applied. 94 - 95 - Incorrect: 96 - ```blade 97 - <form method="POST" action="/posts"> 98 - <input type="text" name="title"> 99 - </form> 100 - ``` 101 - 102 - Correct: 103 - ```blade 104 - <form method="POST" action="/posts"> 105 - @csrf 106 - <input type="text" name="title"> 107 - </form> 108 - ``` 109 - 110 - ## Rate Limit Auth and API Routes 111 - 112 - Apply `throttle` middleware to authentication and API routes. 113 - 114 - ```php 115 - RateLimiter::for('login', function (Request $request) { 116 - return Limit::perMinute(5)->by($request->ip()); 117 - }); 118 - 119 - Route::post('/login', LoginController::class)->middleware('throttle:login'); 120 - ``` 121 - 122 - ## Validate File Uploads 123 - 124 - Validate extension, MIME type, and size. The `mimes` rule checks extensions; use `mimetypes` for actual MIME type validation. Never trust client-provided filenames. 125 - 126 - ```php 127 - public function rules(): array 128 - { 129 - return [ 130 - 'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'], 131 - ]; 132 - } 133 - ``` 134 - 135 - Store with generated filenames: 136 - 137 - ```php 138 - $path = $request->file('avatar')->store('avatars', 'public'); 139 - ``` 140 - 141 - ## Keep Secrets Out of Code 142 - 143 - Never commit `.env`. Access secrets via `config()` only. 144 - 145 - Incorrect: 146 - ```php 147 - $key = env('API_KEY'); 148 - ``` 149 - 150 - Correct: 151 - ```php 152 - // config/services.php 153 - 'api_key' => env('API_KEY'), 154 - 155 - // In application code 156 - $key = config('services.api_key'); 157 - ``` 158 - 159 - ## Audit Dependencies 160 - 161 - Run `composer audit` periodically to check for known vulnerabilities in dependencies. Automate this in CI to catch issues before deployment. 162 - 163 - ```bash 164 - composer audit 165 - ``` 166 - 167 - ## Encrypt Sensitive Database Fields 168 - 169 - Use `encrypted` cast for API keys/tokens and mark the attribute as `hidden`. 170 - 171 - Incorrect: 172 - ```php 173 - class Integration extends Model 174 - { 175 - protected function casts(): array 176 - { 177 - return [ 178 - 'api_key' => 'string', 179 - ]; 180 - } 181 - } 182 - ``` 183 - 184 - Correct: 185 - ```php 186 - class Integration extends Model 187 - { 188 - protected $hidden = ['api_key', 'api_secret']; 189 - 190 - protected function casts(): array 191 - { 192 - return [ 193 - 'api_key' => 'encrypted', 194 - 'api_secret' => 'encrypted', 195 - ]; 196 - } 197 - } 198 - ```
-125
.agents/skills/laravel-best-practices/rules/style.md
··· 1 - # Conventions & Style 2 - 3 - ## Follow Laravel Naming Conventions 4 - 5 - | What | Convention | Good | Bad | 6 - |------|-----------|------|-----| 7 - | Controller | singular | `ArticleController` | `ArticlesController` | 8 - | Model | singular | `User` | `Users` | 9 - | Table | plural, snake_case | `article_comments` | `articleComments` | 10 - | Pivot table | singular alphabetical | `article_user` | `user_article` | 11 - | Column | snake_case, no model name | `meta_title` | `article_meta_title` | 12 - | Foreign key | singular model + `_id` | `article_id` | `articles_id` | 13 - | Route | plural | `articles/1` | `article/1` | 14 - | Route name | snake_case with dots | `users.show_active` | `users.show-active` | 15 - | Method | camelCase | `getAll` | `get_all` | 16 - | Variable | camelCase | `$articlesWithAuthor` | `$articles_with_author` | 17 - | Collection | descriptive, plural | `$activeUsers` | `$data` | 18 - | Object | descriptive, singular | `$activeUser` | `$users` | 19 - | View | kebab-case | `show-filtered.blade.php` | `showFiltered.blade.php` | 20 - | Config | snake_case | `google_calendar.php` | `googleCalendar.php` | 21 - | Enum | singular | `UserType` | `UserTypes` | 22 - 23 - ## Prefer Shorter Readable Syntax 24 - 25 - | Verbose | Shorter | 26 - |---------|---------| 27 - | `Session::get('cart')` | `session('cart')` | 28 - | `$request->session()->get('cart')` | `session('cart')` | 29 - | `$request->input('name')` | `$request->name` | 30 - | `return Redirect::back()` | `return back()` | 31 - | `Carbon::now()` | `now()` | 32 - | `App::make('Class')` | `app('Class')` | 33 - | `->where('column', '=', 1)` | `->where('column', 1)` | 34 - | `->orderBy('created_at', 'desc')` | `->latest()` | 35 - | `->orderBy('created_at', 'asc')` | `->oldest()` | 36 - | `->first()->name` | `->value('name')` | 37 - 38 - ## Use Laravel String & Array Helpers 39 - 40 - Laravel provides `Str`, `Arr`, `Number`, and `Uri` helper classes that are more readable, chainable, and UTF-8 safe than raw PHP functions. Always prefer them. 41 - 42 - Strings — use `Str` and fluent `Str::of()` over raw PHP: 43 - ```php 44 - // Incorrect 45 - $slug = strtolower(str_replace(' ', '-', $title)); 46 - $short = substr($text, 0, 100) . '...'; 47 - $class = substr(strrchr('App\Models\User', '\'), 1); 48 - 49 - // Correct 50 - $slug = Str::slug($title); 51 - $short = Str::limit($text, 100); 52 - $class = class_basename('App\Models\User'); 53 - ``` 54 - 55 - Fluent strings — chain operations for complex transformations: 56 - ```php 57 - // Incorrect 58 - $result = strtolower(trim(str_replace('_', '-', $input))); 59 - 60 - // Correct 61 - $result = Str::of($input)->trim()->replace('_', '-')->lower(); 62 - ``` 63 - 64 - Key `Str` methods to prefer: `Str::slug()`, `Str::limit()`, `Str::contains()`, `Str::before()`, `Str::after()`, `Str::between()`, `Str::camel()`, `Str::snake()`, `Str::kebab()`, `Str::headline()`, `Str::squish()`, `Str::mask()`, `Str::uuid()`, `Str::ulid()`, `Str::random()`, `Str::is()`. 65 - 66 - Arrays — use `Arr` over raw PHP: 67 - ```php 68 - // Incorrect 69 - $name = isset($array['user']['name']) ? $array['user']['name'] : 'default'; 70 - 71 - // Correct 72 - $name = Arr::get($array, 'user.name', 'default'); 73 - ``` 74 - 75 - Key `Arr` methods: `Arr::get()`, `Arr::has()`, `Arr::only()`, `Arr::except()`, `Arr::first()`, `Arr::flatten()`, `Arr::pluck()`, `Arr::where()`, `Arr::wrap()`. 76 - 77 - Numbers — use `Number` for display formatting: 78 - ```php 79 - Number::format(1000000); // "1,000,000" 80 - Number::currency(1500, 'USD'); // "$1,500.00" 81 - Number::abbreviate(1000000); // "1M" 82 - Number::fileSize(1024 * 1024); // "1 MB" 83 - Number::percentage(75.5); // "75.5%" 84 - ``` 85 - 86 - URIs — use `Uri` for URL manipulation: 87 - ```php 88 - $uri = Uri::of('https://example.com/search') 89 - ->withQuery(['q' => 'laravel', 'page' => 1]); 90 - ``` 91 - 92 - Use `$request->string('name')` to get a fluent `Stringable` directly from request input for immediate chaining. 93 - 94 - Use `search-docs` for the full list of available methods — these helpers are extensive. 95 - 96 - ## No Inline JS/CSS in Blade 97 - 98 - Do not put JS or CSS in Blade templates. Do not put HTML in PHP classes. 99 - 100 - Incorrect: 101 - ```blade 102 - let article = `{{ json_encode($article) }}`; 103 - ``` 104 - 105 - Correct: 106 - ```blade 107 - <button class="js-fav-article" data-article='@json($article)'>{{ $article->name }}</button> 108 - ``` 109 - 110 - Pass data to JS via data attributes or use a dedicated PHP-to-JS package. 111 - 112 - ## No Unnecessary Comments 113 - 114 - Code should be readable on its own. Use descriptive method and variable names instead of comments. The only exception is config files, where descriptive comments are expected. 115 - 116 - Incorrect: 117 - ```php 118 - // Check if there are any joins 119 - if (count((array) $builder->getQuery()->joins) > 0) 120 - ``` 121 - 122 - Correct: 123 - ```php 124 - if ($this->hasJoins()) 125 - ```
-43
.agents/skills/laravel-best-practices/rules/testing.md
··· 1 - # Testing Best Practices 2 - 3 - ## Use `LazilyRefreshDatabase` Over `RefreshDatabase` 4 - 5 - `RefreshDatabase` migrates once per process and wraps each test in a rolled-back transaction. `LazilyRefreshDatabase` skips even that first migration if the schema is already up to date. 6 - 7 - ## Use Model Assertions Over Raw Database Assertions 8 - 9 - Incorrect: `$this->assertDatabaseHas('users', ['id' => $user->id]);` 10 - 11 - Correct: `$this->assertModelExists($user);` 12 - 13 - More expressive, type-safe, and fails with clearer messages. 14 - 15 - ## Use Factory States and Sequences 16 - 17 - Named states make tests self-documenting. Sequences eliminate repetitive setup. 18 - 19 - Incorrect: `User::factory()->create(['email_verified_at' => null]);` 20 - 21 - Correct: `User::factory()->unverified()->create();` 22 - 23 - ## Use `Exceptions::fake()` to Assert Exception Reporting 24 - 25 - Instead of `withoutExceptionHandling()`, use `Exceptions::fake()` to assert the correct exception was reported while the request completes normally. 26 - 27 - ## Call `Event::fake()` After Factory Setup 28 - 29 - Model factories rely on model events (e.g., `creating` to generate UUIDs). Calling `Event::fake()` before factory calls silences those events, producing broken models. 30 - 31 - Incorrect: `Event::fake(); $user = User::factory()->create();` 32 - 33 - Correct: `$user = User::factory()->create(); Event::fake();` 34 - 35 - ## Use `recycle()` to Share Relationship Instances Across Factories 36 - 37 - Without `recycle()`, nested factories create separate instances of the same conceptual entity. 38 - 39 - ```php 40 - Ticket::factory() 41 - ->recycle(Airline::factory()->create()) 42 - ->create(); 43 - ```
-75
.agents/skills/laravel-best-practices/rules/validation.md
··· 1 - # Validation & Forms Best Practices 2 - 3 - ## Use Form Request Classes 4 - 5 - Extract validation from controllers into dedicated Form Request classes. 6 - 7 - Incorrect: 8 - ```php 9 - public function store(Request $request) 10 - { 11 - $request->validate([ 12 - 'title' => 'required|max:255', 13 - 'body' => 'required', 14 - ]); 15 - } 16 - ``` 17 - 18 - Correct: 19 - ```php 20 - public function store(StorePostRequest $request) 21 - { 22 - Post::create($request->validated()); 23 - } 24 - ``` 25 - 26 - ## Array vs. String Notation for Rules 27 - 28 - Array syntax is more readable and composes cleanly with `Rule::` objects. Prefer it in new code, but check existing Form Requests first and match whatever notation the project already uses. 29 - 30 - ```php 31 - // Preferred for new code 32 - 'email' => ['required', 'email', Rule::unique('users')], 33 - 34 - // Follow existing convention if the project uses string notation 35 - 'email' => 'required|email|unique:users', 36 - ``` 37 - 38 - ## Always Use `validated()` 39 - 40 - Get only validated data. Never use `$request->all()` for mass operations. 41 - 42 - Incorrect: 43 - ```php 44 - Post::create($request->all()); 45 - ``` 46 - 47 - Correct: 48 - ```php 49 - Post::create($request->validated()); 50 - ``` 51 - 52 - ## Use `Rule::when()` for Conditional Validation 53 - 54 - ```php 55 - 'company_name' => [ 56 - Rule::when($this->account_type === 'business', ['required', 'string', 'max:255']), 57 - ], 58 - ``` 59 - 60 - ## Use the `after()` Method for Custom Validation 61 - 62 - Use `after()` instead of `withValidator()` for custom validation logic that depends on multiple fields. 63 - 64 - ```php 65 - public function after(): array 66 - { 67 - return [ 68 - function (Validator $validator) { 69 - if ($this->quantity > Product::find($this->product_id)?->stock) { 70 - $validator->errors()->add('quantity', 'Not enough stock.'); 71 - } 72 - }, 73 - ]; 74 - } 75 - ```
-159
.agents/skills/pest-testing/SKILL.md
··· 1 - --- 2 - name: pest-testing 3 - description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code." 4 - license: MIT 5 - metadata: 6 - author: laravel 7 - --- 8 - 9 - # Pest Testing 4 10 - 11 - ## Documentation 12 - 13 - Use `search-docs` for detailed Pest 4 patterns and documentation. 14 - 15 - ## Basic Usage 16 - 17 - ### Creating Tests 18 - 19 - All tests must be written using Pest. Use `php artisan make:test --pest {name}`. 20 - 21 - ### Test Organization 22 - 23 - - Unit/Feature tests: `tests/Feature` and `tests/Unit` directories. 24 - - Browser tests: `tests/Browser/` directory. 25 - - Do NOT remove tests without approval - these are core application code. 26 - 27 - ### Basic Test Structure 28 - 29 - Pest supports both `test()` and `it()` functions. Before writing new tests, check existing test files in the same directory to match the project's convention. Use `test()` if existing tests use `test()`, or `it()` if they use `it()`. 30 - 31 - <!-- Basic Pest Test Example --> 32 - ```php 33 - it('is true', function () { 34 - expect(true)->toBeTrue(); 35 - }); 36 - ``` 37 - 38 - ### Running Tests 39 - 40 - - Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`. 41 - - Run all tests: `php artisan test --compact`. 42 - - Run file: `php artisan test --compact tests/Feature/ExampleTest.php`. 43 - 44 - ## Assertions 45 - 46 - Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`: 47 - 48 - <!-- Pest Response Assertion --> 49 - ```php 50 - it('returns all', function () { 51 - $this->postJson('/api/docs', [])->assertSuccessful(); 52 - }); 53 - ``` 54 - 55 - | Use | Instead of | 56 - |-----|------------| 57 - | `assertSuccessful()` | `assertStatus(200)` | 58 - | `assertNotFound()` | `assertStatus(404)` | 59 - | `assertForbidden()` | `assertStatus(403)` | 60 - 61 - ## Mocking 62 - 63 - Import mock function before use: `use function Pest\Laravel\mock;` 64 - 65 - ## Datasets 66 - 67 - Use datasets for repetitive tests (validation rules, etc.): 68 - 69 - <!-- Pest Dataset Example --> 70 - ```php 71 - it('has emails', function (string $email) { 72 - expect($email)->not->toBeEmpty(); 73 - })->with([ 74 - 'james' => 'james@laravel.com', 75 - 'taylor' => 'taylor@laravel.com', 76 - ]); 77 - ``` 78 - 79 - ## Pest 4 Features 80 - 81 - | Feature | Purpose | 82 - |---------|---------| 83 - | Browser Testing | Full integration tests in real browsers | 84 - | Smoke Testing | Validate multiple pages quickly | 85 - | Visual Regression | Compare screenshots for visual changes | 86 - | Test Sharding | Parallel CI runs | 87 - | Architecture Testing | Enforce code conventions | 88 - 89 - ### Browser Test Example 90 - 91 - Browser tests run in real browsers for full integration testing: 92 - 93 - - Browser tests live in `tests/Browser/`. 94 - - Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories. 95 - - Use `RefreshDatabase` for clean state per test. 96 - - Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures. 97 - - Test on multiple browsers (Chrome, Firefox, Safari) if requested. 98 - - Test on different devices/viewports (iPhone 14 Pro, tablets) if requested. 99 - - Switch color schemes (light/dark mode) when appropriate. 100 - - Take screenshots or pause tests for debugging. 101 - 102 - <!-- Pest Browser Test Example --> 103 - ```php 104 - it('may reset the password', function () { 105 - Notification::fake(); 106 - 107 - $this->actingAs(User::factory()->create()); 108 - 109 - $page = visit('/sign-in'); 110 - 111 - $page->assertSee('Sign In') 112 - ->assertNoJavaScriptErrors() 113 - ->click('Forgot Password?') 114 - ->fill('email', 'nuno@laravel.com') 115 - ->click('Send Reset Link') 116 - ->assertSee('We have emailed your password reset link!'); 117 - 118 - Notification::assertSent(ResetPassword::class); 119 - }); 120 - ``` 121 - 122 - ### Smoke Testing 123 - 124 - Quickly validate multiple pages have no JavaScript errors: 125 - 126 - <!-- Pest Smoke Testing Example --> 127 - ```php 128 - $pages = visit(['/', '/about', '/contact']); 129 - 130 - $pages->assertNoJavaScriptErrors()->assertNoConsoleLogs(); 131 - ``` 132 - 133 - ### Visual Regression Testing 134 - 135 - Capture and compare screenshots to detect visual changes. 136 - 137 - ### Test Sharding 138 - 139 - Split tests across parallel processes for faster CI runs. 140 - 141 - ### Architecture Testing 142 - 143 - Pest 4 includes architecture testing (from Pest 3): 144 - 145 - <!-- Architecture Test Example --> 146 - ```php 147 - arch('controllers') 148 - ->expect('App\Http\Controllers') 149 - ->toExtendNothing() 150 - ->toHaveSuffix('Controller'); 151 - ``` 152 - 153 - ## Common Pitfalls 154 - 155 - - Not importing `use function Pest\Laravel\mock;` before using mock 156 - - Using `assertStatus(200)` instead of `assertSuccessful()` 157 - - Forgetting datasets for repetitive validation tests 158 - - Deleting tests without approval 159 - - Forgetting `assertNoJavaScriptErrors()` in browser tests
-119
.agents/skills/tailwindcss-development/SKILL.md
··· 1 - --- 2 - name: tailwindcss-development 3 - description: "Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS." 4 - license: MIT 5 - metadata: 6 - author: laravel 7 - --- 8 - 9 - # Tailwind CSS Development 10 - 11 - ## Documentation 12 - 13 - Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation. 14 - 15 - ## Basic Usage 16 - 17 - - Use Tailwind CSS classes to style HTML. Check and follow existing Tailwind conventions in the project before introducing new patterns. 18 - - Offer to extract repeated patterns into components that match the project's conventions (e.g., Blade, JSX, Vue). 19 - - Consider class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child elements carefully to reduce repetition, and group elements logically. 20 - 21 - ## Tailwind CSS v4 Specifics 22 - 23 - - Always use Tailwind CSS v4 and avoid deprecated utilities. 24 - - `corePlugins` is not supported in Tailwind v4. 25 - 26 - ### CSS-First Configuration 27 - 28 - In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed: 29 - 30 - <!-- CSS-First Config --> 31 - ```css 32 - @theme { 33 - --color-brand: oklch(0.72 0.11 178); 34 - } 35 - ``` 36 - 37 - ### Import Syntax 38 - 39 - In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3: 40 - 41 - <!-- v4 Import Syntax --> 42 - ```diff 43 - - @tailwind base; 44 - - @tailwind components; 45 - - @tailwind utilities; 46 - + @import "tailwindcss"; 47 - ``` 48 - 49 - ### Replaced Utilities 50 - 51 - Tailwind v4 removed deprecated utilities. Use the replacements shown below. Opacity values remain numeric. 52 - 53 - | Deprecated | Replacement | 54 - |------------|-------------| 55 - | bg-opacity-* | bg-black/* | 56 - | text-opacity-* | text-black/* | 57 - | border-opacity-* | border-black/* | 58 - | divide-opacity-* | divide-black/* | 59 - | ring-opacity-* | ring-black/* | 60 - | placeholder-opacity-* | placeholder-black/* | 61 - | flex-shrink-* | shrink-* | 62 - | flex-grow-* | grow-* | 63 - | overflow-ellipsis | text-ellipsis | 64 - | decoration-slice | box-decoration-slice | 65 - | decoration-clone | box-decoration-clone | 66 - 67 - ## Spacing 68 - 69 - Use `gap` utilities instead of margins for spacing between siblings: 70 - 71 - <!-- Gap Utilities --> 72 - ```html 73 - <div class="flex gap-8"> 74 - <div>Item 1</div> 75 - <div>Item 2</div> 76 - </div> 77 - ``` 78 - 79 - ## Dark Mode 80 - 81 - If existing pages and components support dark mode, new pages and components must support it the same way, typically using the `dark:` variant: 82 - 83 - <!-- Dark Mode --> 84 - ```html 85 - <div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white"> 86 - Content adapts to color scheme 87 - </div> 88 - ``` 89 - 90 - ## Common Patterns 91 - 92 - ### Flexbox Layout 93 - 94 - <!-- Flexbox Layout --> 95 - ```html 96 - <div class="flex items-center justify-between gap-4"> 97 - <div>Left content</div> 98 - <div>Right content</div> 99 - </div> 100 - ``` 101 - 102 - ### Grid Layout 103 - 104 - <!-- Grid Layout --> 105 - ```html 106 - <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> 107 - <div>Card 1</div> 108 - <div>Card 2</div> 109 - <div>Card 3</div> 110 - </div> 111 - ``` 112 - 113 - ## Common Pitfalls 114 - 115 - - Using deprecated v3 utilities (bg-opacity-*, flex-shrink-*, etc.) 116 - - Using `@tailwind` directives instead of `@import "tailwindcss"` 117 - - Trying to use `tailwind.config.js` instead of CSS `@theme` directive 118 - - Using margins for spacing between siblings instead of gap utilities 119 - - Forgetting to add dark mode variants when the project uses dark mode
-80
.agents/skills/wayfinder-development/SKILL.md
··· 1 - --- 2 - name: wayfinder-development 3 - description: "Use this skill for Laravel Wayfinder which auto-generates typed functions for Laravel controllers and routes. ALWAYS use this skill when frontend code needs to call backend routes or controller actions. Trigger when: connecting any React/Vue/Svelte/Inertia frontend to Laravel controllers, routes, building end-to-end features with both frontend and backend, wiring up forms or links to backend endpoints, fixing route-related TypeScript errors, importing from @/actions or @/routes, or running wayfinder:generate. Use Wayfinder route functions instead of hardcoded URLs. Covers: wayfinder() vite plugin, .url()/.get()/.post()/.form(), query params, route model binding, tree-shaking. Do not use for backend-only task" 4 - license: MIT 5 - metadata: 6 - author: laravel 7 - --- 8 - 9 - # Wayfinder Development 10 - 11 - ## Documentation 12 - 13 - Use `search-docs` for detailed Wayfinder patterns and documentation. 14 - 15 - ## Quick Reference 16 - 17 - ### Generate Routes 18 - 19 - Run after route changes if Vite plugin isn't installed: 20 - ```bash 21 - php artisan wayfinder:generate --no-interaction 22 - ``` 23 - For form helpers, use `--with-form` flag: 24 - ```bash 25 - php artisan wayfinder:generate --with-form --no-interaction 26 - ``` 27 - 28 - ### Import Patterns 29 - 30 - <!-- Controller Action Imports --> 31 - ```typescript 32 - // Named imports for tree-shaking (preferred)... 33 - import { show, store, update } from '@/actions/App/Http/Controllers/PostController' 34 - 35 - // Named route imports... 36 - import { show as postShow } from '@/routes/post' 37 - ``` 38 - 39 - ### Common Methods 40 - 41 - <!-- Wayfinder Methods --> 42 - ```typescript 43 - // Get route object... 44 - show(1) // { url: "/posts/1", method: "get" } 45 - 46 - // Get URL string... 47 - show.url(1) // "/posts/1" 48 - 49 - // Specific HTTP methods... 50 - show.get(1) 51 - store.post() 52 - update.patch(1) 53 - destroy.delete(1) 54 - 55 - // Form attributes for HTML forms... 56 - store.form() // { action: "/posts", method: "post" } 57 - 58 - // Query parameters... 59 - show(1, { query: { page: 1 } }) // "/posts/1?page=1" 60 - ``` 61 - 62 - ## Wayfinder + Inertia 63 - 64 - Use Wayfinder with the `<Form>` component: 65 - <!-- Wayfinder Form (React) --> 66 - ```typescript 67 - <Form {...store.form()}><input name="title" /></Form> 68 - ``` 69 - 70 - ## Verification 71 - 72 - 1. Run `php artisan wayfinder:generate` to regenerate routes if Vite plugin isn't installed 73 - 2. Check TypeScript imports resolve correctly 74 - 3. Verify route URLs match expected paths 75 - 76 - ## Common Pitfalls 77 - 78 - - Using default imports instead of named imports (breaks tree-shaking) 79 - - Forgetting to regenerate after route changes 80 - - Not using type-safe parameter objects for route model binding
-11
.amp/settings.json
··· 1 - { 2 - "amp.mcpServers": { 3 - "laravel-boost": { 4 - "command": "php", 5 - "args": [ 6 - "artisan", 7 - "boost:mcp" 8 - ] 9 - } 10 - } 11 - }
+59
.claude/settings.json
··· 1 + { 2 + "permissions": { 3 + "allow": [ 4 + "Bash(php artisan test)", 5 + "Bash(php artisan test:*)", 6 + "Bash(php artisan wayfinder:generate)", 7 + "Bash(php artisan route:list:*)", 8 + "Bash(./vendor/bin/pint --dirty)", 9 + "Bash(git status)", 10 + "Bash(git diff:*)", 11 + "Bash(git log:*)", 12 + "Bash(bun run build)", 13 + "Bash(bun run types)", 14 + "Bash(bun run lint)", 15 + "Bash(bun run format:dirty)" 16 + ], 17 + "deny": ["Read(.env)", "Bash(rm -rf :*)"], 18 + "ask": ["Bash(git add:*)", "Bash(git commit:*)", "Bash(git push:*)"] 19 + }, 20 + "hooks": { 21 + "Notification": [ 22 + { 23 + "matcher": "", 24 + "hooks": [ 25 + { 26 + "type": "command", 27 + "command": "notify-send 'Claude Code' 'Awaiting your input'" 28 + } 29 + ] 30 + } 31 + ], 32 + "PostToolUse": [ 33 + { 34 + "matcher": "Edit|Write", 35 + "hooks": [ 36 + { 37 + "type": "command", 38 + "command": "bun run lint" 39 + }, 40 + { 41 + "type": "command", 42 + "command": "bun run types" 43 + }, 44 + { 45 + "type": "command", 46 + "command": "./vendor/bin/pint --dirty" 47 + }, 48 + { 49 + "type": "command", 50 + "command": "bun run format:dirty" 51 + } 52 + ] 53 + } 54 + ] 55 + }, 56 + "enabledPlugins": { 57 + "atproto-skills@atproto-skills": true 58 + } 59 + }
+80
.claude/skills/socialite-development/SKILL.md
··· 1 + --- 2 + name: socialite-development 3 + description: "Manages OAuth social authentication with Laravel Socialite. Activate when adding social login providers; configuring OAuth redirect/callback flows; retrieving authenticated user details; customizing scopes or parameters; setting up community providers; testing with Socialite fakes; or when the user mentions social login, OAuth, Socialite, or third-party authentication." 4 + license: MIT 5 + metadata: 6 + author: laravel 7 + --- 8 + 9 + # Socialite Authentication 10 + 11 + ## Documentation 12 + 13 + Use `search-docs` for detailed Socialite patterns and documentation (installation, configuration, routing, callbacks, testing, scopes, stateless auth). 14 + 15 + ## Available Providers 16 + 17 + Built-in: `facebook`, `twitter`, `twitter-oauth-2`, `linkedin`, `linkedin-openid`, `google`, `github`, `gitlab`, `bitbucket`, `slack`, `slack-openid`, `twitch` 18 + 19 + Community: 150+ additional providers at [socialiteproviders.com](https://socialiteproviders.com). For provider-specific setup, use `WebFetch` on `https://socialiteproviders.com/{provider-name}`. 20 + 21 + Configuration key in `config/services.php` must match the driver name exactly — note the hyphenated keys: `twitter-oauth-2`, `linkedin-openid`, `slack-openid`. 22 + 23 + Twitter/X: Use `twitter-oauth-2` (OAuth 2.0) for new projects. The legacy `twitter` driver is OAuth 1.0. Driver names remain unchanged despite the platform rebrand. 24 + 25 + Community providers differ from built-in providers in the following ways: 26 + - Installed via `composer require socialiteproviders/{name}` 27 + - Must register via event listener — NOT auto-discovered like built-in providers 28 + - Use `search-docs` for the registration pattern 29 + 30 + ## Adding a Provider 31 + 32 + ### 1. Configure the provider 33 + 34 + Add the provider's `client_id`, `client_secret`, and `redirect` to `config/services.php`. The config key must match the driver name exactly. 35 + 36 + ### 2. Create redirect and callback routes 37 + 38 + Two routes are needed: one that calls `Socialite::driver('provider')->redirect()` to send the user to the OAuth provider, and one that calls `Socialite::driver('provider')->user()` to receive the callback and retrieve user details. 39 + 40 + ### 3. Authenticate and store the user 41 + 42 + In the callback, use `updateOrCreate` to find or create a user record from the provider's response (`id`, `name`, `email`, `token`, `refreshToken`), then call `Auth::login()`. 43 + 44 + ### 4. Customize the redirect (optional) 45 + 46 + - `scopes()` — merge additional scopes with the provider's defaults 47 + - `setScopes()` — replace all scopes entirely 48 + - `with()` — pass optional parameters (e.g., `['hd' => 'example.com']` for Google) 49 + - `asBotUser()` — Slack only; generates a bot token (`xoxb-`) instead of a user token (`xoxp-`). Must be called before both `redirect()` and `user()`. Only the `token` property will be hydrated on the user object. 50 + - `stateless()` — for API/SPA contexts where session state is not maintained 51 + 52 + ### 5. Verify 53 + 54 + 1. Config key matches driver name exactly (check the list above for hyphenated names) 55 + 2. `client_id`, `client_secret`, and `redirect` are all present 56 + 3. Redirect URL matches what is registered in the provider's OAuth dashboard 57 + 4. Callback route handles denied grants (when user declines authorization) 58 + 59 + Use `search-docs` for complete code examples of each step. 60 + 61 + ## Additional Features 62 + 63 + Use `search-docs` for usage details on: `enablePKCE()`, `userFromToken($token)`, `userFromTokenAndSecret($token, $secret)` (OAuth 1.0), retrieving user details. 64 + 65 + User object: `getId()`, `getName()`, `getEmail()`, `getAvatar()`, `getNickname()`, `token`, `refreshToken`, `expiresIn`, `approvedScopes` 66 + 67 + ## Testing 68 + 69 + Socialite provides `Socialite::fake()` for testing redirects and callbacks. Use `search-docs` for faking redirects, callback user data, custom token properties, and assertion methods. 70 + 71 + ## Common Pitfalls 72 + 73 + - Config key must match driver name exactly — hyphenated drivers need hyphenated keys (`linkedin-openid`, `slack-openid`, `twitter-oauth-2`). Mismatch silently fails. 74 + - Every provider needs `client_id`, `client_secret`, and `redirect` in `config/services.php`. Missing any one causes cryptic errors. 75 + - `scopes()` merges with defaults; `setScopes()` replaces all scopes entirely. 76 + - Missing `stateless()` in API/SPA contexts causes `InvalidStateException`. 77 + - Redirect URL in `config/services.php` must exactly match the provider's OAuth dashboard (including trailing slashes and protocol). 78 + - Do not pass `state`, `response_type`, `client_id`, `redirect_uri`, or `scope` via `with()` — these are reserved. 79 + - Community providers require event listener registration via `SocialiteWasCalled`. 80 + - `user()` throws when the user declines authorization. Always handle denied grants.
-4
.codex/config.toml
··· 1 - [mcp_servers.laravel-boost] 2 - command = "php" 3 - args = ["artisan", "boost:mcp"] 4 - cwd = "/Users/dominik/Developer/Projects/morgenblau-scaffold"
+11
.env.example
··· 63 63 AWS_USE_PATH_STYLE_ENDPOINT=false 64 64 65 65 VITE_APP_NAME="${APP_NAME}" 66 + 67 + # Generate with: php artisan bluesky:new-private-key 68 + BLUESKY_OAUTH_PRIVATE_KEY= 69 + BLUESKY_OAUTH_SCOPE="atproto repo:app.skyreader.feed.subscription repo:app.skyreader.feed.saved repo:app.skyreader.social.follow repo:app.skyreader.social.share" 70 + BLUESKY_OAUTH_CLIENT_NAME="Morgenblau" 71 + BLUESKY_OAUTH_CLIENT_URI="${APP_URL}" 72 + 73 + # Dev: leave empty. Prod: set BLUESKY_CLIENT_ID=https://<your-host>/oauth-client-metadata.json 74 + # and BLUESKY_REDIRECT=https://<your-host>/oauth/callback 75 + # BLUESKY_CLIENT_ID= 76 + # BLUESKY_REDIRECT=
-11
.gemini/settings.json
··· 1 - { 2 - "mcpServers": { 3 - "laravel-boost": { 4 - "command": "php", 5 - "args": [ 6 - "artisan", 7 - "boost:mcp" 8 - ] 9 - } 10 - } 11 - }
-218
AGENTS.md
··· 1 - <laravel-boost-guidelines> 2 - === foundation rules === 3 - 4 - # Laravel Boost Guidelines 5 - 6 - The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications. 7 - 8 - ## Foundational Context 9 - 10 - This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. 11 - 12 - - php - 8.4 13 - - inertiajs/inertia-laravel (INERTIA_LARAVEL) - v3 14 - - laravel/framework (LARAVEL) - v13 15 - - laravel/prompts (PROMPTS) - v0 16 - - laravel/wayfinder (WAYFINDER) - v0 17 - - laravel/boost (BOOST) - v2 18 - - laravel/mcp (MCP) - v0 19 - - laravel/pail (PAIL) - v1 20 - - laravel/pint (PINT) - v1 21 - - laravel/sail (SAIL) - v1 22 - - pestphp/pest (PEST) - v4 23 - - phpunit/phpunit (PHPUNIT) - v12 24 - - @inertiajs/react (INERTIA_REACT) - v3 25 - - react (REACT) - v19 26 - - tailwindcss (TAILWINDCSS) - v4 27 - - @laravel/vite-plugin-wayfinder (WAYFINDER_VITE) - v0 28 - - eslint (ESLINT) - v9 29 - - prettier (PRETTIER) - v3 30 - 31 - ## Skills Activation 32 - 33 - This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck. 34 - 35 - - `laravel-best-practices` — Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns. 36 - - `wayfinder-development` — Use this skill for Laravel Wayfinder which auto-generates typed functions for Laravel controllers and routes. ALWAYS use this skill when frontend code needs to call backend routes or controller actions. Trigger when: connecting any React/Vue/Svelte/Inertia frontend to Laravel controllers, routes, building end-to-end features with both frontend and backend, wiring up forms or links to backend endpoints, fixing route-related TypeScript errors, importing from @/actions or @/routes, or running wayfinder:generate. Use Wayfinder route functions instead of hardcoded URLs. Covers: wayfinder() vite plugin, .url()/.get()/.post()/.form(), query params, route model binding, tree-shaking. Do not use for backend-only task 37 - - `pest-testing` — Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code. 38 - - `inertia-react-development` — Develops Inertia.js v3 React client-side applications. Activates when creating React pages, forms, or navigation; using <Link>, <Form>, useForm, useHttp, setLayoutProps, or router; working with deferred props, prefetching, optimistic updates, instant visits, or polling; or when user mentions React with Inertia, React pages, React forms, or React navigation. 39 - - `tailwindcss-development` — Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS. 40 - 41 - ## Conventions 42 - 43 - - You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming. 44 - - Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. 45 - - Check for existing components to reuse before writing a new one. 46 - 47 - ## Verification Scripts 48 - 49 - - Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important. 50 - 51 - ## Application Structure & Architecture 52 - 53 - - Stick to existing directory structure; don't create new base folders without approval. 54 - - Do not change the application's dependencies without approval. 55 - 56 - ## Frontend Bundling 57 - 58 - - If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `bun run build`, `bun run dev`, or `composer run dev`. Ask them. 59 - 60 - ## Documentation Files 61 - 62 - - You must only create documentation files if explicitly requested by the user. 63 - 64 - ## Replies 65 - 66 - - Be concise in your explanations - focus on what's important rather than explaining obvious details. 67 - 68 - === boost rules === 69 - 70 - # Laravel Boost 71 - 72 - ## Tools 73 - 74 - - Laravel Boost is an MCP server with tools designed specifically for this application. Prefer Boost tools over manual alternatives like shell commands or file reads. 75 - - Use `database-query` to run read-only queries against the database instead of writing raw SQL in tinker. 76 - - Use `database-schema` to inspect table structure before writing migrations or models. 77 - - Use `get-absolute-url` to resolve the correct scheme, domain, and port for project URLs. Always use this before sharing a URL with the user. 78 - - Use `browser-logs` to read browser logs, errors, and exceptions. Only recent logs are useful, ignore old entries. 79 - 80 - ## Searching Documentation (IMPORTANT) 81 - 82 - - Always use `search-docs` before making code changes. Do not skip this step. It returns version-specific docs based on installed packages automatically. 83 - - Pass a `packages` array to scope results when you know which packages are relevant. 84 - - Use multiple broad, topic-based queries: `['rate limiting', 'routing rate limiting', 'routing']`. Expect the most relevant results first. 85 - - Do not add package names to queries because package info is already shared. Use `test resource table`, not `filament 4 test resource table`. 86 - 87 - ### Search Syntax 88 - 89 - 1. Use words for auto-stemmed AND logic: `rate limit` matches both "rate" AND "limit". 90 - 2. Use `"quoted phrases"` for exact position matching: `"infinite scroll"` requires adjacent words in order. 91 - 3. Combine words and phrases for mixed queries: `middleware "rate limit"`. 92 - 4. Use multiple queries for OR logic: `queries=["authentication", "middleware"]`. 93 - 94 - ## Artisan 95 - 96 - - Run Artisan commands directly via the command line (e.g., `php artisan route:list`). Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters. 97 - - Inspect routes with `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`. 98 - - Read configuration values using dot notation: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory. 99 - - To check environment variables, read the `.env` file directly. 100 - 101 - ## Tinker 102 - 103 - - Execute PHP in app context for debugging and testing code. Do not create models without user approval, prefer tests with factories instead. Prefer existing Artisan commands over custom tinker code. 104 - - Always use single quotes to prevent shell expansion: `php artisan tinker --execute 'Your::code();'` 105 - - Double quotes for PHP strings inside: `php artisan tinker --execute 'User::where("active", true)->count();'` 106 - 107 - === php rules === 108 - 109 - # PHP 110 - 111 - - Always use curly braces for control structures, even for single-line bodies. 112 - - Use PHP 8 constructor property promotion: `public function __construct(public GitHub $github) { }`. Do not leave empty zero-parameter `__construct()` methods unless the constructor is private. 113 - - Use explicit return type declarations and type hints for all method parameters: `function isAccessible(User $user, ?string $path = null): bool` 114 - - Use TitleCase for Enum keys: `FavoritePerson`, `BestLake`, `Monthly`. 115 - - Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic. 116 - - Use array shape type definitions in PHPDoc blocks. 117 - 118 - === deployments rules === 119 - 120 - # Deployment 121 - 122 - - Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications. 123 - 124 - === herd rules === 125 - 126 - # Laravel Herd 127 - 128 - - The application is served by Laravel Herd at `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs. Never run commands to serve the site. It is always available. 129 - - Use the `herd` CLI to manage services, PHP versions, and sites (e.g. `herd sites`, `herd services:start <service>`, `herd php:list`). Run `herd list` to discover all available commands. 130 - 131 - === tests rules === 132 - 133 - # Test Enforcement 134 - 135 - - Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. 136 - - Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter. 137 - 138 - === inertia-laravel/core rules === 139 - 140 - # Inertia 141 - 142 - - Inertia creates fully client-side rendered SPAs without modern SPA complexity, leveraging existing server-side patterns. 143 - - Components live in `resources/js/pages` (unless specified in `vite.config.js`). Use `Inertia::render()` for server-side routing instead of Blade views. 144 - - ALWAYS use `search-docs` tool for version-specific Inertia documentation and updated code examples. 145 - - IMPORTANT: Activate `inertia-react-development` when working with Inertia client-side patterns. 146 - 147 - # Inertia v3 148 - 149 - - Use all Inertia features from v1, v2, and v3. Check the documentation before making changes to ensure the correct approach. 150 - - New v3 features: standalone HTTP requests (`useHttp` hook), optimistic updates with automatic rollback, layout props (`useLayoutProps` hook), instant visits, simplified SSR via `@inertiajs/vite` plugin, custom exception handling for error pages. 151 - - Carried over from v2: deferred props, infinite scroll, merging props, polling, prefetching, once props, flash data. 152 - - When using deferred props, add an empty state with a pulsing or animated skeleton. 153 - - Axios has been removed. Use the built-in XHR client with interceptors, or install Axios separately if needed. 154 - - `Inertia::lazy()` / `LazyProp` has been removed. Use `Inertia::optional()` instead. 155 - - Prop types (`Inertia::optional()`, `Inertia::defer()`, `Inertia::merge()`) work inside nested arrays with dot-notation paths. 156 - - SSR works automatically in Vite dev mode with `@inertiajs/vite` - no separate Node.js server needed during development. 157 - - Event renames: `invalid` is now `httpException`, `exception` is now `networkError`. 158 - - `router.cancel()` replaced by `router.cancelAll()`. 159 - - The `future` configuration namespace has been removed - all v2 future options are now always enabled. 160 - 161 - === laravel/core rules === 162 - 163 - # Do Things the Laravel Way 164 - 165 - - Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`. 166 - - If you're creating a generic PHP class, use `php artisan make:class`. 167 - - Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. 168 - 169 - ### Model Creation 170 - 171 - - When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options. 172 - 173 - ## APIs & Eloquent Resources 174 - 175 - - For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. 176 - 177 - ## URL Generation 178 - 179 - - When generating links to other pages, prefer named routes and the `route()` function. 180 - 181 - ## Testing 182 - 183 - - When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. 184 - - Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. 185 - - When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. 186 - 187 - ## Vite Error 188 - 189 - - If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `bun run build` or ask the user to run `bun run dev` or `composer run dev`. 190 - 191 - === wayfinder/core rules === 192 - 193 - # Laravel Wayfinder 194 - 195 - Use Wayfinder to generate TypeScript functions for Laravel routes. Import from `@/actions/` (controllers) or `@/routes/` (named routes). 196 - 197 - === pint/core rules === 198 - 199 - # Laravel Pint Code Formatter 200 - 201 - - If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style. 202 - - Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues. 203 - 204 - === pest/core rules === 205 - 206 - ## Pest 207 - 208 - - This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`. 209 - - Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`. 210 - - Do NOT delete tests without approval. 211 - 212 - === inertia-react/core rules === 213 - 214 - # Inertia + React 215 - 216 - - IMPORTANT: Activate `inertia-react-development` when working with Inertia React client-side patterns. 217 - 218 - </laravel-boost-guidelines>
+33 -197
CLAUDE.md
··· 1 - <laravel-boost-guidelines> 2 - === foundation rules === 3 - 4 - # Laravel Boost Guidelines 5 - 6 - The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications. 7 - 8 - ## Foundational Context 9 - 10 - This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. 11 - 12 - - php - 8.4 13 - - inertiajs/inertia-laravel (INERTIA_LARAVEL) - v3 14 - - laravel/framework (LARAVEL) - v13 15 - - laravel/prompts (PROMPTS) - v0 16 - - laravel/wayfinder (WAYFINDER) - v0 17 - - laravel/boost (BOOST) - v2 18 - - laravel/mcp (MCP) - v0 19 - - laravel/pail (PAIL) - v1 20 - - laravel/pint (PINT) - v1 21 - - laravel/sail (SAIL) - v1 22 - - pestphp/pest (PEST) - v4 23 - - phpunit/phpunit (PHPUNIT) - v12 24 - - @inertiajs/react (INERTIA_REACT) - v3 25 - - react (REACT) - v19 26 - - tailwindcss (TAILWINDCSS) - v4 27 - - @laravel/vite-plugin-wayfinder (WAYFINDER_VITE) - v0 28 - - eslint (ESLINT) - v9 29 - - prettier (PRETTIER) - v3 30 - 31 - ## Skills Activation 32 - 33 - This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck. 34 - 35 - - `laravel-best-practices` — Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns. 36 - - `wayfinder-development` — Use this skill for Laravel Wayfinder which auto-generates typed functions for Laravel controllers and routes. ALWAYS use this skill when frontend code needs to call backend routes or controller actions. Trigger when: connecting any React/Vue/Svelte/Inertia frontend to Laravel controllers, routes, building end-to-end features with both frontend and backend, wiring up forms or links to backend endpoints, fixing route-related TypeScript errors, importing from @/actions or @/routes, or running wayfinder:generate. Use Wayfinder route functions instead of hardcoded URLs. Covers: wayfinder() vite plugin, .url()/.get()/.post()/.form(), query params, route model binding, tree-shaking. Do not use for backend-only task 37 - - `pest-testing` — Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code. 38 - - `inertia-react-development` — Develops Inertia.js v3 React client-side applications. Activates when creating React pages, forms, or navigation; using <Link>, <Form>, useForm, useHttp, setLayoutProps, or router; working with deferred props, prefetching, optimistic updates, instant visits, or polling; or when user mentions React with Inertia, React pages, React forms, or React navigation. 39 - - `tailwindcss-development` — Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS. 40 - 41 - ## Conventions 42 - 43 - - You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming. 44 - - Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. 45 - - Check for existing components to reuse before writing a new one. 46 - 47 - ## Verification Scripts 48 - 49 - - Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important. 50 - 51 - ## Application Structure & Architecture 52 - 53 - - Stick to existing directory structure; don't create new base folders without approval. 54 - - Do not change the application's dependencies without approval. 55 - 56 - ## Frontend Bundling 57 - 58 - - If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `bun run build`, `bun run dev`, or `composer run dev`. Ask them. 59 - 60 - ## Documentation Files 61 - 62 - - You must only create documentation files if explicitly requested by the user. 63 - 64 - ## Replies 65 - 66 - - Be concise in your explanations - focus on what's important rather than explaining obvious details. 67 - 68 - === boost rules === 69 - 70 - # Laravel Boost 71 - 72 - ## Tools 73 - 74 - - Laravel Boost is an MCP server with tools designed specifically for this application. Prefer Boost tools over manual alternatives like shell commands or file reads. 75 - - Use `database-query` to run read-only queries against the database instead of writing raw SQL in tinker. 76 - - Use `database-schema` to inspect table structure before writing migrations or models. 77 - - Use `get-absolute-url` to resolve the correct scheme, domain, and port for project URLs. Always use this before sharing a URL with the user. 78 - - Use `browser-logs` to read browser logs, errors, and exceptions. Only recent logs are useful, ignore old entries. 79 - 80 - ## Searching Documentation (IMPORTANT) 81 - 82 - - Always use `search-docs` before making code changes. Do not skip this step. It returns version-specific docs based on installed packages automatically. 83 - - Pass a `packages` array to scope results when you know which packages are relevant. 84 - - Use multiple broad, topic-based queries: `['rate limiting', 'routing rate limiting', 'routing']`. Expect the most relevant results first. 85 - - Do not add package names to queries because package info is already shared. Use `test resource table`, not `filament 4 test resource table`. 86 - 87 - ### Search Syntax 88 - 89 - 1. Use words for auto-stemmed AND logic: `rate limit` matches both "rate" AND "limit". 90 - 2. Use `"quoted phrases"` for exact position matching: `"infinite scroll"` requires adjacent words in order. 91 - 3. Combine words and phrases for mixed queries: `middleware "rate limit"`. 92 - 4. Use multiple queries for OR logic: `queries=["authentication", "middleware"]`. 93 - 94 - ## Artisan 95 - 96 - - Run Artisan commands directly via the command line (e.g., `php artisan route:list`). Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters. 97 - - Inspect routes with `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`. 98 - - Read configuration values using dot notation: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory. 99 - - To check environment variables, read the `.env` file directly. 100 - 101 - ## Tinker 102 - 103 - - Execute PHP in app context for debugging and testing code. Do not create models without user approval, prefer tests with factories instead. Prefer existing Artisan commands over custom tinker code. 104 - - Always use single quotes to prevent shell expansion: `php artisan tinker --execute 'Your::code();'` 105 - - Double quotes for PHP strings inside: `php artisan tinker --execute 'User::where("active", true)->count();'` 106 - 107 - === php rules === 108 - 109 - # PHP 110 - 111 - - Always use curly braces for control structures, even for single-line bodies. 112 - - Use PHP 8 constructor property promotion: `public function __construct(public GitHub $github) { }`. Do not leave empty zero-parameter `__construct()` methods unless the constructor is private. 113 - - Use explicit return type declarations and type hints for all method parameters: `function isAccessible(User $user, ?string $path = null): bool` 114 - - Use TitleCase for Enum keys: `FavoritePerson`, `BestLake`, `Monthly`. 115 - - Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic. 116 - - Use array shape type definitions in PHPDoc blocks. 117 - 118 - === deployments rules === 119 - 120 - # Deployment 1 + # Morgenblau 121 2 122 - - Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications. 123 - 124 - === herd rules === 125 - 126 - # Laravel Herd 127 - 128 - - The application is served by Laravel Herd at `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs. Never run commands to serve the site. It is always available. 129 - - Use the `herd` CLI to manage services, PHP versions, and sites (e.g. `herd sites`, `herd services:start <service>`, `herd php:list`). Run `herd list` to discover all available commands. 3 + A calm content platform powered by RSS and ATProto — daily digests instead of infinite feeds. 130 4 131 - === tests rules === 132 - 133 - # Test Enforcement 134 - 135 - - Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. 136 - - Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter. 137 - 138 - === inertia-laravel/core rules === 139 - 140 - # Inertia 141 - 142 - - Inertia creates fully client-side rendered SPAs without modern SPA complexity, leveraging existing server-side patterns. 143 - - Components live in `resources/js/pages` (unless specified in `vite.config.js`). Use `Inertia::render()` for server-side routing instead of Blade views. 144 - - ALWAYS use `search-docs` tool for version-specific Inertia documentation and updated code examples. 145 - - IMPORTANT: Activate `inertia-react-development` when working with Inertia client-side patterns. 5 + ## Stack 146 6 147 - # Inertia v3 7 + Laravel 13 (PHP 8.4) + Inertia 3 + React 19 + TypeScript + Tailwind v4. Auth via ATProto OAuth using `revolution/laravel-bluesky`. Pest for backend tests. Frontend routing via Laravel Wayfinder (`@/routes`, `@/actions`). 148 8 149 - - Use all Inertia features from v1, v2, and v3. Check the documentation before making changes to ensure the correct approach. 150 - - New v3 features: standalone HTTP requests (`useHttp` hook), optimistic updates with automatic rollback, layout props (`useLayoutProps` hook), instant visits, simplified SSR via `@inertiajs/vite` plugin, custom exception handling for error pages. 151 - - Carried over from v2: deferred props, infinite scroll, merging props, polling, prefetching, once props, flash data. 152 - - When using deferred props, add an empty state with a pulsing or animated skeleton. 153 - - Axios has been removed. Use the built-in XHR client with interceptors, or install Axios separately if needed. 154 - - `Inertia::lazy()` / `LazyProp` has been removed. Use `Inertia::optional()` instead. 155 - - Prop types (`Inertia::optional()`, `Inertia::defer()`, `Inertia::merge()`) work inside nested arrays with dot-notation paths. 156 - - SSR works automatically in Vite dev mode with `@inertiajs/vite` - no separate Node.js server needed during development. 157 - - Event renames: `invalid` is now `httpException`, `exception` is now `networkError`. 158 - - `router.cancel()` replaced by `router.cancelAll()`. 159 - - The `future` configuration namespace has been removed - all v2 future options are now always enabled. 9 + ## Spec Compliance 160 10 161 - === laravel/core rules === 11 + [SPEC.md](./SPEC.md) is the source of truth for product vision, content model, and guardrails. All code must follow the spec. When you notice a discrepancy between the code and the spec, flag it and either ask to update the spec or adapt the code — never silently diverge. 162 12 163 - # Do Things the Laravel Way 13 + ## Tooling 164 14 165 - - Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`. 166 - - If you're creating a generic PHP class, use `php artisan make:class`. 167 - - Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. 168 - 169 - ### Model Creation 170 - 171 - - When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options. 172 - 173 - ## APIs & Eloquent Resources 174 - 175 - - For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. 176 - 177 - ## URL Generation 178 - 179 - - When generating links to other pages, prefer named routes and the `route()` function. 15 + - **Package manager: bun only.** Never use npm, yarn, or pnpm for dependency changes. All install/run/build commands must use `bun`. PHP deps via Composer. 16 + - **Never read or search inside `node_modules/` or `vendor/`.** Treat as off-limits. 17 + - **Never use inline eval** (`node -e`, `bun -e`, or equivalent). Don't create temporary files to work around this. To investigate JS package APIs, use [npmx.dev](https://npmx.dev) instead. 18 + - **Wayfinder over hardcoded URLs.** Import route helpers from `@/routes/*` and action helpers from `@/actions/*`. Re-run `php artisan wayfinder:generate` after route or controller changes. 19 + - **Inertia + React pages live at `resources/js/pages/`.** Kebab-case filenames, default export a component. 180 20 181 21 ## Testing 182 22 183 - - When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. 184 - - Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. 185 - - When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. 23 + - Use **Red/Green TDD** as the standard development paradigm. 24 + - **Pest** for backend tests (`tests/Feature`, `tests/Unit`). Run with `php artisan test --compact` — add `--filter=<name>` to scope. 25 + - Prefer feature tests over unit tests unless the logic is genuinely pure. 26 + - No frontend test runner is installed yet; add Vitest when the first interaction-worthy UI lands. 186 27 187 - ## Vite Error 28 + ## Verification 188 29 189 - - If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `bun run build` or ask the user to run `bun run dev` or `composer run dev`. 30 + After each batch of work, run: 190 31 191 - === wayfinder/core rules === 32 + - `bun run lint` — ESLint (0 errors, 0 warnings) 33 + - `bun run types` — TypeScript check 34 + - `./vendor/bin/pint --dirty --format agent` — PHP formatter (Laravel's style) 35 + - `php artisan test --compact` — Pest suite 192 36 193 - # Laravel Wayfinder 37 + ## OAuth 194 38 195 - Use Wayfinder to generate TypeScript functions for Laravel routes. Import from `@/actions/` (controllers) or `@/routes/` (named routes). 196 - 197 - === pint/core rules === 198 - 199 - # Laravel Pint Code Formatter 200 - 201 - - If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style. 202 - - Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues. 39 + ATProto OAuth is the only auth mechanism — no passwords, email, or registration. The user row stores `did` (primary key), encrypted `refresh_token`, and `iss` (auth server URL). Handle is resolved at login and lives in the Laravel session, never the DB. Profile data (avatar, display name) is re-fetched live from the PDS when needed. 203 40 204 - === pest/core rules === 41 + Scopes: granular `repo:app.skyreader.*` per-collection, following [Dan Abramov's guidance](https://underreacted.leaflet.pub/3mjfozhlhys2z). Avoid `transition:generic`. 205 42 206 - ## Pest 207 - 208 - - This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`. 209 - - Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`. 210 - - Do NOT delete tests without approval. 43 + Client metadata is served at `/oauth-client-metadata.json`, JWKS at `/oauth-jwks.json`, callback at `/oauth/callback` (route name `bluesky.oauth.redirect`, package convention). 211 44 212 - === inertia-react/core rules === 45 + ## Key References 213 46 214 - # Inertia + React 47 + - [SPEC.md](./SPEC.md) — product spec 48 + - [ATProto OAuth](https://atproto.com/specs/oauth), [ATProto permissions](https://atproto.com/specs/permission) 49 + - [revolution/laravel-bluesky docs](https://github.com/invokable/laravel-bluesky/blob/main/docs/socialite.md) 50 + - Skyreader lexicons — `lexicons/app/skyreader/` 215 51 216 - - IMPORTANT: Activate `inertia-react-development` when working with Inertia React client-side patterns. 52 + ## Laravel Boost note 217 53 218 - </laravel-boost-guidelines> 54 + `laravel/boost` is installed and its `boost:update` hook runs on every `composer install/require/update`. That hook rewrites parts of this file. If it clobbers important content, recover from git. Long-term options: uninstall Boost, or merge Boost's guideline block into this file intentionally.
-218
GEMINI.md
··· 1 - <laravel-boost-guidelines> 2 - === foundation rules === 3 - 4 - # Laravel Boost Guidelines 5 - 6 - The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications. 7 - 8 - ## Foundational Context 9 - 10 - This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. 11 - 12 - - php - 8.4 13 - - inertiajs/inertia-laravel (INERTIA_LARAVEL) - v3 14 - - laravel/framework (LARAVEL) - v13 15 - - laravel/prompts (PROMPTS) - v0 16 - - laravel/wayfinder (WAYFINDER) - v0 17 - - laravel/boost (BOOST) - v2 18 - - laravel/mcp (MCP) - v0 19 - - laravel/pail (PAIL) - v1 20 - - laravel/pint (PINT) - v1 21 - - laravel/sail (SAIL) - v1 22 - - pestphp/pest (PEST) - v4 23 - - phpunit/phpunit (PHPUNIT) - v12 24 - - \@inertiajs/react (INERTIA_REACT) - v3 25 - - react (REACT) - v19 26 - - tailwindcss (TAILWINDCSS) - v4 27 - - \@laravel/vite-plugin-wayfinder (WAYFINDER_VITE) - v0 28 - - eslint (ESLINT) - v9 29 - - prettier (PRETTIER) - v3 30 - 31 - ## Skills Activation 32 - 33 - This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck. 34 - 35 - - `laravel-best-practices` — Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns. 36 - - `wayfinder-development` — Use this skill for Laravel Wayfinder which auto-generates typed functions for Laravel controllers and routes. ALWAYS use this skill when frontend code needs to call backend routes or controller actions. Trigger when: connecting any React/Vue/Svelte/Inertia frontend to Laravel controllers, routes, building end-to-end features with both frontend and backend, wiring up forms or links to backend endpoints, fixing route-related TypeScript errors, importing from @/actions or @/routes, or running wayfinder:generate. Use Wayfinder route functions instead of hardcoded URLs. Covers: wayfinder() vite plugin, .url()/.get()/.post()/.form(), query params, route model binding, tree-shaking. Do not use for backend-only task 37 - - `pest-testing` — Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code. 38 - - `inertia-react-development` — Develops Inertia.js v3 React client-side applications. Activates when creating React pages, forms, or navigation; using <Link>, <Form>, useForm, useHttp, setLayoutProps, or router; working with deferred props, prefetching, optimistic updates, instant visits, or polling; or when user mentions React with Inertia, React pages, React forms, or React navigation. 39 - - `tailwindcss-development` — Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS. 40 - 41 - ## Conventions 42 - 43 - - You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming. 44 - - Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. 45 - - Check for existing components to reuse before writing a new one. 46 - 47 - ## Verification Scripts 48 - 49 - - Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important. 50 - 51 - ## Application Structure & Architecture 52 - 53 - - Stick to existing directory structure; don't create new base folders without approval. 54 - - Do not change the application's dependencies without approval. 55 - 56 - ## Frontend Bundling 57 - 58 - - If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `bun run build`, `bun run dev`, or `composer run dev`. Ask them. 59 - 60 - ## Documentation Files 61 - 62 - - You must only create documentation files if explicitly requested by the user. 63 - 64 - ## Replies 65 - 66 - - Be concise in your explanations - focus on what's important rather than explaining obvious details. 67 - 68 - === boost rules === 69 - 70 - # Laravel Boost 71 - 72 - ## Tools 73 - 74 - - Laravel Boost is an MCP server with tools designed specifically for this application. Prefer Boost tools over manual alternatives like shell commands or file reads. 75 - - Use `database-query` to run read-only queries against the database instead of writing raw SQL in tinker. 76 - - Use `database-schema` to inspect table structure before writing migrations or models. 77 - - Use `get-absolute-url` to resolve the correct scheme, domain, and port for project URLs. Always use this before sharing a URL with the user. 78 - - Use `browser-logs` to read browser logs, errors, and exceptions. Only recent logs are useful, ignore old entries. 79 - 80 - ## Searching Documentation (IMPORTANT) 81 - 82 - - Always use `search-docs` before making code changes. Do not skip this step. It returns version-specific docs based on installed packages automatically. 83 - - Pass a `packages` array to scope results when you know which packages are relevant. 84 - - Use multiple broad, topic-based queries: `['rate limiting', 'routing rate limiting', 'routing']`. Expect the most relevant results first. 85 - - Do not add package names to queries because package info is already shared. Use `test resource table`, not `filament 4 test resource table`. 86 - 87 - ### Search Syntax 88 - 89 - 1. Use words for auto-stemmed AND logic: `rate limit` matches both "rate" AND "limit". 90 - 2. Use `"quoted phrases"` for exact position matching: `"infinite scroll"` requires adjacent words in order. 91 - 3. Combine words and phrases for mixed queries: `middleware "rate limit"`. 92 - 4. Use multiple queries for OR logic: `queries=["authentication", "middleware"]`. 93 - 94 - ## Artisan 95 - 96 - - Run Artisan commands directly via the command line (e.g., `php artisan route:list`). Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters. 97 - - Inspect routes with `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`. 98 - - Read configuration values using dot notation: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory. 99 - - To check environment variables, read the `.env` file directly. 100 - 101 - ## Tinker 102 - 103 - - Execute PHP in app context for debugging and testing code. Do not create models without user approval, prefer tests with factories instead. Prefer existing Artisan commands over custom tinker code. 104 - - Always use single quotes to prevent shell expansion: `php artisan tinker --execute 'Your::code();'` 105 - - Double quotes for PHP strings inside: `php artisan tinker --execute 'User::where("active", true)->count();'` 106 - 107 - === php rules === 108 - 109 - # PHP 110 - 111 - - Always use curly braces for control structures, even for single-line bodies. 112 - - Use PHP 8 constructor property promotion: `public function __construct(public GitHub $github) { }`. Do not leave empty zero-parameter `__construct()` methods unless the constructor is private. 113 - - Use explicit return type declarations and type hints for all method parameters: `function isAccessible(User $user, ?string $path = null): bool` 114 - - Use TitleCase for Enum keys: `FavoritePerson`, `BestLake`, `Monthly`. 115 - - Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic. 116 - - Use array shape type definitions in PHPDoc blocks. 117 - 118 - === deployments rules === 119 - 120 - # Deployment 121 - 122 - - Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications. 123 - 124 - === herd rules === 125 - 126 - # Laravel Herd 127 - 128 - - The application is served by Laravel Herd at `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs. Never run commands to serve the site. It is always available. 129 - - Use the `herd` CLI to manage services, PHP versions, and sites (e.g. `herd sites`, `herd services:start <service>`, `herd php:list`). Run `herd list` to discover all available commands. 130 - 131 - === tests rules === 132 - 133 - # Test Enforcement 134 - 135 - - Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. 136 - - Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter. 137 - 138 - === inertia-laravel/core rules === 139 - 140 - # Inertia 141 - 142 - - Inertia creates fully client-side rendered SPAs without modern SPA complexity, leveraging existing server-side patterns. 143 - - Components live in `resources/js/pages` (unless specified in `vite.config.js`). Use `Inertia::render()` for server-side routing instead of Blade views. 144 - - ALWAYS use `search-docs` tool for version-specific Inertia documentation and updated code examples. 145 - - IMPORTANT: Activate `inertia-react-development` when working with Inertia client-side patterns. 146 - 147 - # Inertia v3 148 - 149 - - Use all Inertia features from v1, v2, and v3. Check the documentation before making changes to ensure the correct approach. 150 - - New v3 features: standalone HTTP requests (`useHttp` hook), optimistic updates with automatic rollback, layout props (`useLayoutProps` hook), instant visits, simplified SSR via `@inertiajs/vite` plugin, custom exception handling for error pages. 151 - - Carried over from v2: deferred props, infinite scroll, merging props, polling, prefetching, once props, flash data. 152 - - When using deferred props, add an empty state with a pulsing or animated skeleton. 153 - - Axios has been removed. Use the built-in XHR client with interceptors, or install Axios separately if needed. 154 - - `Inertia::lazy()` / `LazyProp` has been removed. Use `Inertia::optional()` instead. 155 - - Prop types (`Inertia::optional()`, `Inertia::defer()`, `Inertia::merge()`) work inside nested arrays with dot-notation paths. 156 - - SSR works automatically in Vite dev mode with `@inertiajs/vite` - no separate Node.js server needed during development. 157 - - Event renames: `invalid` is now `httpException`, `exception` is now `networkError`. 158 - - `router.cancel()` replaced by `router.cancelAll()`. 159 - - The `future` configuration namespace has been removed - all v2 future options are now always enabled. 160 - 161 - === laravel/core rules === 162 - 163 - # Do Things the Laravel Way 164 - 165 - - Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`. 166 - - If you're creating a generic PHP class, use `php artisan make:class`. 167 - - Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. 168 - 169 - ### Model Creation 170 - 171 - - When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options. 172 - 173 - ## APIs & Eloquent Resources 174 - 175 - - For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. 176 - 177 - ## URL Generation 178 - 179 - - When generating links to other pages, prefer named routes and the `route()` function. 180 - 181 - ## Testing 182 - 183 - - When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. 184 - - Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. 185 - - When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. 186 - 187 - ## Vite Error 188 - 189 - - If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `bun run build` or ask the user to run `bun run dev` or `composer run dev`. 190 - 191 - === wayfinder/core rules === 192 - 193 - # Laravel Wayfinder 194 - 195 - Use Wayfinder to generate TypeScript functions for Laravel routes. Import from `@/actions/` (controllers) or `@/routes/` (named routes). 196 - 197 - === pint/core rules === 198 - 199 - # Laravel Pint Code Formatter 200 - 201 - - If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style. 202 - - Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues. 203 - 204 - === pest/core rules === 205 - 206 - ## Pest 207 - 208 - - This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`. 209 - - Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`. 210 - - Do NOT delete tests without approval. 211 - 212 - === inertia-react/core rules === 213 - 214 - # Inertia + React 215 - 216 - - IMPORTANT: Activate `inertia-react-development` when working with Inertia React client-side patterns. 217 - 218 - </laravel-boost-guidelines>
-29
app/Concerns/PasswordValidationRules.php
··· 1 - <?php 2 - 3 - namespace App\Concerns; 4 - 5 - use Illuminate\Contracts\Validation\ValidationRule; 6 - use Illuminate\Validation\Rules\Password; 7 - 8 - trait PasswordValidationRules 9 - { 10 - /** 11 - * Get the validation rules used to validate passwords. 12 - * 13 - * @return array<int, ValidationRule|array<mixed>|string> 14 - */ 15 - protected function passwordRules(): array 16 - { 17 - return ['required', 'string', Password::default(), 'confirmed']; 18 - } 19 - 20 - /** 21 - * Get the validation rules used to validate the current password. 22 - * 23 - * @return array<int, ValidationRule|array<mixed>|string> 24 - */ 25 - protected function currentPasswordRules(): array 26 - { 27 - return ['required', 'string', 'current_password']; 28 - } 29 - }
-51
app/Concerns/ProfileValidationRules.php
··· 1 - <?php 2 - 3 - namespace App\Concerns; 4 - 5 - use App\Models\User; 6 - use Illuminate\Contracts\Validation\ValidationRule; 7 - use Illuminate\Validation\Rule; 8 - 9 - trait ProfileValidationRules 10 - { 11 - /** 12 - * Get the validation rules used to validate user profiles. 13 - * 14 - * @return array<string, array<int, ValidationRule|array<mixed>|string>> 15 - */ 16 - protected function profileRules(?int $userId = null): array 17 - { 18 - return [ 19 - 'name' => $this->nameRules(), 20 - 'email' => $this->emailRules($userId), 21 - ]; 22 - } 23 - 24 - /** 25 - * Get the validation rules used to validate user names. 26 - * 27 - * @return array<int, ValidationRule|array<mixed>|string> 28 - */ 29 - protected function nameRules(): array 30 - { 31 - return ['required', 'string', 'max:255']; 32 - } 33 - 34 - /** 35 - * Get the validation rules used to validate user emails. 36 - * 37 - * @return array<int, ValidationRule|array<mixed>|string> 38 - */ 39 - protected function emailRules(?int $userId = null): array 40 - { 41 - return [ 42 - 'required', 43 - 'string', 44 - 'email', 45 - 'max:255', 46 - $userId === null 47 - ? Rule::unique(User::class) 48 - : Rule::unique(User::class)->ignore($userId), 49 - ]; 50 - } 51 - }
+45
app/Http/Controllers/Auth/AuthenticatedSessionController.php
··· 1 + <?php 2 + 3 + namespace App\Http\Controllers\Auth; 4 + 5 + use App\Http\Controllers\Controller; 6 + use Illuminate\Http\RedirectResponse; 7 + use Illuminate\Http\Request; 8 + use Illuminate\Support\Facades\Auth; 9 + use Inertia\Inertia; 10 + use Inertia\Response; 11 + use Laravel\Socialite\Facades\Socialite; 12 + use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse; 13 + 14 + class AuthenticatedSessionController extends Controller 15 + { 16 + public function create(): Response 17 + { 18 + return Inertia::render('auth/login'); 19 + } 20 + 21 + public function store(Request $request): SymfonyRedirectResponse 22 + { 23 + $validated = $request->validate([ 24 + 'handle' => ['nullable', 'string', 'max:253'], 25 + ]); 26 + 27 + $hint = $validated['handle'] ?? null; 28 + $request->session()->put('atproto.hint', $hint); 29 + 30 + return Socialite::driver('bluesky') 31 + ->setScopes(explode(' ', (string) config('bluesky.oauth.metadata.scope'))) 32 + ->hint($hint) 33 + ->redirect(); 34 + } 35 + 36 + public function destroy(Request $request): RedirectResponse 37 + { 38 + Auth::logout(); 39 + 40 + $request->session()->invalidate(); 41 + $request->session()->regenerateToken(); 42 + 43 + return redirect('/'); 44 + } 45 + }
+43
app/Http/Controllers/Auth/OAuthCallbackController.php
··· 1 + <?php 2 + 3 + namespace App\Http\Controllers\Auth; 4 + 5 + use App\Http\Controllers\Controller; 6 + use App\Models\User; 7 + use Illuminate\Http\RedirectResponse; 8 + use Illuminate\Http\Request; 9 + use Illuminate\Support\Facades\Auth; 10 + use Laravel\Socialite\Facades\Socialite; 11 + use Revolution\Bluesky\Session\OAuthSession; 12 + 13 + class OAuthCallbackController extends Controller 14 + { 15 + public function __invoke(Request $request): RedirectResponse 16 + { 17 + $hint = $request->session()->pull('atproto.hint'); 18 + 19 + /** @var \Laravel\Socialite\Two\User $oauthUser */ 20 + $oauthUser = Socialite::driver('bluesky') 21 + ->setScopes(explode(' ', (string) config('bluesky.oauth.metadata.scope'))) 22 + ->hint($hint) 23 + ->user(); 24 + 25 + /** @var OAuthSession $session */ 26 + $session = $oauthUser->session; 27 + 28 + $user = User::updateOrCreate( 29 + ['did' => $session->did()], 30 + [ 31 + 'refresh_token' => $session->refresh(), 32 + 'iss' => $session->issuer(), 33 + ], 34 + ); 35 + 36 + $request->session()->put('bluesky_session', $session->toArray()); 37 + $request->session()->put('atproto.handle', $session->handle()); 38 + 39 + Auth::login($user, remember: true); 40 + 41 + return to_route('dashboard'); 42 + } 43 + }
-62
app/Http/Controllers/Settings/ProfileController.php
··· 1 - <?php 2 - 3 - namespace App\Http\Controllers\Settings; 4 - 5 - use App\Http\Controllers\Controller; 6 - use App\Http\Requests\Settings\ProfileDeleteRequest; 7 - use App\Http\Requests\Settings\ProfileUpdateRequest; 8 - use Illuminate\Contracts\Auth\MustVerifyEmail; 9 - use Illuminate\Http\RedirectResponse; 10 - use Illuminate\Http\Request; 11 - use Illuminate\Support\Facades\Auth; 12 - use Inertia\Inertia; 13 - use Inertia\Response; 14 - 15 - class ProfileController extends Controller 16 - { 17 - /** 18 - * Show the user's profile settings page. 19 - */ 20 - public function edit(Request $request): Response 21 - { 22 - return Inertia::render('settings/profile', [ 23 - 'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail, 24 - 'status' => $request->session()->get('status'), 25 - ]); 26 - } 27 - 28 - /** 29 - * Update the user's profile information. 30 - */ 31 - public function update(ProfileUpdateRequest $request): RedirectResponse 32 - { 33 - $request->user()->fill($request->validated()); 34 - 35 - if ($request->user()->isDirty('email')) { 36 - $request->user()->email_verified_at = null; 37 - } 38 - 39 - $request->user()->save(); 40 - 41 - Inertia::flash('toast', ['type' => 'success', 'message' => __('Profile updated.')]); 42 - 43 - return to_route('profile.edit'); 44 - } 45 - 46 - /** 47 - * Delete the user's profile. 48 - */ 49 - public function destroy(ProfileDeleteRequest $request): RedirectResponse 50 - { 51 - $user = $request->user(); 52 - 53 - Auth::logout(); 54 - 55 - $user->delete(); 56 - 57 - $request->session()->invalidate(); 58 - $request->session()->regenerateToken(); 59 - 60 - return redirect('/'); 61 - } 62 - }
+1
app/Http/Middleware/HandleInertiaRequests.php
··· 40 40 'name' => config('app.name'), 41 41 'auth' => [ 42 42 'user' => $request->user(), 43 + 'handle' => $request->session()->get('atproto.handle'), 43 44 ], 44 45 'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true', 45 46 ];
-24
app/Http/Requests/Settings/ProfileDeleteRequest.php
··· 1 - <?php 2 - 3 - namespace App\Http\Requests\Settings; 4 - 5 - use App\Concerns\PasswordValidationRules; 6 - use Illuminate\Contracts\Validation\ValidationRule; 7 - use Illuminate\Foundation\Http\FormRequest; 8 - 9 - class ProfileDeleteRequest extends FormRequest 10 - { 11 - use PasswordValidationRules; 12 - 13 - /** 14 - * Get the validation rules that apply to the request. 15 - * 16 - * @return array<string, ValidationRule|array<mixed>|string> 17 - */ 18 - public function rules(): array 19 - { 20 - return [ 21 - 'password' => $this->currentPasswordRules(), 22 - ]; 23 - } 24 - }
-22
app/Http/Requests/Settings/ProfileUpdateRequest.php
··· 1 - <?php 2 - 3 - namespace App\Http\Requests\Settings; 4 - 5 - use App\Concerns\ProfileValidationRules; 6 - use Illuminate\Contracts\Validation\ValidationRule; 7 - use Illuminate\Foundation\Http\FormRequest; 8 - 9 - class ProfileUpdateRequest extends FormRequest 10 - { 11 - use ProfileValidationRules; 12 - 13 - /** 14 - * Get the validation rules that apply to the request. 15 - * 16 - * @return array<string, ValidationRule|array<mixed>|string> 17 - */ 18 - public function rules(): array 19 - { 20 - return $this->profileRules($this->user()->id); 21 - } 22 - }
+20
app/Listeners/ClearRefreshTokenOnRotate.php
··· 1 + <?php 2 + 3 + namespace App\Listeners; 4 + 5 + use App\Models\User; 6 + use Revolution\Bluesky\Events\OAuthSessionRefreshing; 7 + 8 + class ClearRefreshTokenOnRotate 9 + { 10 + public function handle(OAuthSessionRefreshing $event): void 11 + { 12 + $did = $event->session->did(); 13 + 14 + if (empty($did)) { 15 + return; 16 + } 17 + 18 + User::where('did', $did)->update(['refresh_token' => '']); 19 + } 20 + }
+25
app/Listeners/PersistOAuthSession.php
··· 1 + <?php 2 + 3 + namespace App\Listeners; 4 + 5 + use App\Models\User; 6 + use Revolution\Bluesky\Events\OAuthSessionUpdated; 7 + 8 + class PersistOAuthSession 9 + { 10 + public function handle(OAuthSessionUpdated $event): void 11 + { 12 + $did = $event->session->did(); 13 + 14 + if (empty($did)) { 15 + return; 16 + } 17 + 18 + User::where('did', $did)->update([ 19 + 'refresh_token' => $event->session->refresh(), 20 + 'iss' => $event->session->issuer(), 21 + ]); 22 + 23 + session()->put('bluesky_session', $event->session->toArray()); 24 + } 25 + }
+21 -8
app/Models/User.php
··· 2 2 3 3 namespace App\Models; 4 4 5 - // use Illuminate\Contracts\Auth\MustVerifyEmail; 6 5 use Database\Factories\UserFactory; 7 6 use Illuminate\Database\Eloquent\Attributes\Fillable; 8 7 use Illuminate\Database\Eloquent\Attributes\Hidden; 9 8 use Illuminate\Database\Eloquent\Factories\HasFactory; 10 9 use Illuminate\Foundation\Auth\User as Authenticatable; 11 10 use Illuminate\Notifications\Notifiable; 11 + use Revolution\Bluesky\Session\OAuthSession; 12 + use Revolution\Bluesky\Traits\WithBluesky; 12 13 13 - #[Fillable(['name', 'email', 'password'])] 14 - #[Hidden(['password', 'remember_token'])] 14 + #[Fillable(['did', 'refresh_token', 'iss'])] 15 + #[Hidden(['refresh_token', 'remember_token'])] 15 16 class User extends Authenticatable 16 17 { 17 18 /** @use HasFactory<UserFactory> */ 18 - use HasFactory, Notifiable; 19 + use HasFactory, Notifiable, WithBluesky; 20 + 21 + protected $primaryKey = 'did'; 22 + 23 + public $incrementing = false; 24 + 25 + protected $keyType = 'string'; 19 26 20 27 /** 21 - * Get the attributes that should be cast. 22 - * 23 28 * @return array<string, string> 24 29 */ 25 30 protected function casts(): array 26 31 { 27 32 return [ 28 - 'email_verified_at' => 'datetime', 29 - 'password' => 'hashed', 33 + 'refresh_token' => 'encrypted', 30 34 ]; 35 + } 36 + 37 + protected function tokenForBluesky(): OAuthSession 38 + { 39 + return OAuthSession::create(array_filter([ 40 + 'did' => $this->did, 41 + 'refresh_token' => $this->refresh_token, 42 + 'iss' => $this->iss, 43 + ])); 31 44 } 32 45 }
+27 -10
app/Providers/AppServiceProvider.php
··· 2 2 3 3 namespace App\Providers; 4 4 5 + use App\Listeners\ClearRefreshTokenOnRotate; 6 + use App\Listeners\PersistOAuthSession; 5 7 use Carbon\CarbonImmutable; 6 8 use Illuminate\Support\Facades\Date; 7 9 use Illuminate\Support\Facades\DB; 10 + use Illuminate\Support\Facades\Event; 8 11 use Illuminate\Support\ServiceProvider; 9 - use Illuminate\Validation\Rules\Password; 12 + use Revolution\Bluesky\Events\OAuthSessionRefreshing; 13 + use Revolution\Bluesky\Events\OAuthSessionUpdated; 14 + use Revolution\Bluesky\Socialite\OAuthConfig; 10 15 11 16 class AppServiceProvider extends ServiceProvider 12 17 { ··· 24 29 public function boot(): void 25 30 { 26 31 $this->configureDefaults(); 32 + $this->configureBluesky(); 27 33 } 28 34 29 35 /** ··· 36 42 DB::prohibitDestructiveCommands( 37 43 app()->isProduction(), 38 44 ); 45 + } 39 46 40 - Password::defaults(fn (): ?Password => app()->isProduction() 41 - ? Password::min(12) 42 - ->mixedCase() 43 - ->letters() 44 - ->numbers() 45 - ->symbols() 46 - ->uncompromised() 47 - : null, 48 - ); 47 + /** 48 + * Wire Bluesky OAuth: event listeners for token rotation and a custom 49 + * client-metadata document served at /oauth-client-metadata.json. 50 + */ 51 + protected function configureBluesky(): void 52 + { 53 + Event::listen(OAuthSessionUpdated::class, PersistOAuthSession::class); 54 + Event::listen(OAuthSessionRefreshing::class, ClearRefreshTokenOnRotate::class); 55 + 56 + OAuthConfig::clientMetadataUsing(function (): array { 57 + return collect(config('bluesky.oauth.metadata')) 58 + ->merge([ 59 + 'client_id' => url('/oauth-client-metadata.json'), 60 + 'jwks_uri' => url('/oauth-jwks.json'), 61 + 'redirect_uris' => [url('/oauth/callback')], 62 + ]) 63 + ->reject(fn ($v): bool => is_null($v)) 64 + ->toArray(); 65 + }); 49 66 } 50 67 }
+2 -5
boost.json
··· 1 1 { 2 2 "agents": [ 3 - "amp", 4 - "claude_code", 5 - "codex", 6 - "gemini" 3 + "claude_code" 7 4 ], 8 - "guidelines": true, 9 5 "mcp": true, 10 6 "nightwatch_mcp": false, 11 7 "sail": false, 12 8 "skills": [ 13 9 "laravel-best-practices", 10 + "socialite-development", 14 11 "wayfinder-development", 15 12 "pest-testing", 16 13 "inertia-react-development",
+624 -1
composer.lock
··· 4 4 "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 5 "This file is @generated automatically" 6 6 ], 7 - "content-hash": "9da0b67d6e4141bb81c69503f7af01f9", 7 + "content-hash": "7f357397d8efb09c4d851aa50565d586", 8 8 "packages": [ 9 9 { 10 10 "name": "brick/math", ··· 507 507 } 508 508 ], 509 509 "time": "2025-03-06T22:45:56+00:00" 510 + }, 511 + { 512 + "name": "firebase/php-jwt", 513 + "version": "v7.0.5", 514 + "source": { 515 + "type": "git", 516 + "url": "https://github.com/googleapis/php-jwt.git", 517 + "reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380" 518 + }, 519 + "dist": { 520 + "type": "zip", 521 + "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/47ad26bab5e7c70ae8a6f08ed25ff83631121380", 522 + "reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380", 523 + "shasum": "" 524 + }, 525 + "require": { 526 + "php": "^8.0" 527 + }, 528 + "require-dev": { 529 + "guzzlehttp/guzzle": "^7.4", 530 + "phpfastcache/phpfastcache": "^9.2", 531 + "phpspec/prophecy-phpunit": "^2.0", 532 + "phpunit/phpunit": "^9.5", 533 + "psr/cache": "^2.0||^3.0", 534 + "psr/http-client": "^1.0", 535 + "psr/http-factory": "^1.0" 536 + }, 537 + "suggest": { 538 + "ext-sodium": "Support EdDSA (Ed25519) signatures", 539 + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" 540 + }, 541 + "type": "library", 542 + "autoload": { 543 + "psr-4": { 544 + "Firebase\\JWT\\": "src" 545 + } 546 + }, 547 + "notification-url": "https://packagist.org/downloads/", 548 + "license": [ 549 + "BSD-3-Clause" 550 + ], 551 + "authors": [ 552 + { 553 + "name": "Neuman Vong", 554 + "email": "neuman+pear@twilio.com", 555 + "role": "Developer" 556 + }, 557 + { 558 + "name": "Anant Narayanan", 559 + "email": "anant@php.net", 560 + "role": "Developer" 561 + } 562 + ], 563 + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", 564 + "homepage": "https://github.com/firebase/php-jwt", 565 + "keywords": [ 566 + "jwt", 567 + "php" 568 + ], 569 + "support": { 570 + "issues": "https://github.com/googleapis/php-jwt/issues", 571 + "source": "https://github.com/googleapis/php-jwt/tree/v7.0.5" 572 + }, 573 + "time": "2026-04-01T20:38:03+00:00" 510 574 }, 511 575 { 512 576 "name": "fruitcake/php-cors", ··· 1470 1534 "time": "2026-04-14T13:33:34+00:00" 1471 1535 }, 1472 1536 { 1537 + "name": "laravel/socialite", 1538 + "version": "v5.26.1", 1539 + "source": { 1540 + "type": "git", 1541 + "url": "https://github.com/laravel/socialite.git", 1542 + "reference": "db6ec2ee967b7f06412c3a0cf1daaf072f4752a4" 1543 + }, 1544 + "dist": { 1545 + "type": "zip", 1546 + "url": "https://api.github.com/repos/laravel/socialite/zipball/db6ec2ee967b7f06412c3a0cf1daaf072f4752a4", 1547 + "reference": "db6ec2ee967b7f06412c3a0cf1daaf072f4752a4", 1548 + "shasum": "" 1549 + }, 1550 + "require": { 1551 + "ext-json": "*", 1552 + "firebase/php-jwt": "^6.4|^7.0", 1553 + "guzzlehttp/guzzle": "^6.0|^7.0", 1554 + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", 1555 + "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", 1556 + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", 1557 + "league/oauth1-client": "^1.11", 1558 + "php": "^7.2|^8.0", 1559 + "phpseclib/phpseclib": "^3.0" 1560 + }, 1561 + "require-dev": { 1562 + "mockery/mockery": "^1.0", 1563 + "orchestra/testbench": "^4.18|^5.20|^6.47|^7.55|^8.36|^9.15|^10.8|^11.0", 1564 + "phpstan/phpstan": "^1.12.23", 1565 + "phpunit/phpunit": "^8.0|^9.3|^10.4|^11.5|^12.0" 1566 + }, 1567 + "type": "library", 1568 + "extra": { 1569 + "laravel": { 1570 + "aliases": { 1571 + "Socialite": "Laravel\\Socialite\\Facades\\Socialite" 1572 + }, 1573 + "providers": [ 1574 + "Laravel\\Socialite\\SocialiteServiceProvider" 1575 + ] 1576 + }, 1577 + "branch-alias": { 1578 + "dev-master": "5.x-dev" 1579 + } 1580 + }, 1581 + "autoload": { 1582 + "psr-4": { 1583 + "Laravel\\Socialite\\": "src/" 1584 + } 1585 + }, 1586 + "notification-url": "https://packagist.org/downloads/", 1587 + "license": [ 1588 + "MIT" 1589 + ], 1590 + "authors": [ 1591 + { 1592 + "name": "Taylor Otwell", 1593 + "email": "taylor@laravel.com" 1594 + } 1595 + ], 1596 + "description": "Laravel wrapper around OAuth 1 & OAuth 2 libraries.", 1597 + "homepage": "https://laravel.com", 1598 + "keywords": [ 1599 + "laravel", 1600 + "oauth" 1601 + ], 1602 + "support": { 1603 + "issues": "https://github.com/laravel/socialite/issues", 1604 + "source": "https://github.com/laravel/socialite" 1605 + }, 1606 + "time": "2026-03-29T14:50:53+00:00" 1607 + }, 1608 + { 1473 1609 "name": "laravel/tinker", 1474 1610 "version": "v3.0.2", 1475 1611 "source": { ··· 1979 2115 "time": "2024-09-21T08:32:55+00:00" 1980 2116 }, 1981 2117 { 2118 + "name": "league/oauth1-client", 2119 + "version": "v1.11.0", 2120 + "source": { 2121 + "type": "git", 2122 + "url": "https://github.com/thephpleague/oauth1-client.git", 2123 + "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055" 2124 + }, 2125 + "dist": { 2126 + "type": "zip", 2127 + "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/f9c94b088837eb1aae1ad7c4f23eb65cc6993055", 2128 + "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055", 2129 + "shasum": "" 2130 + }, 2131 + "require": { 2132 + "ext-json": "*", 2133 + "ext-openssl": "*", 2134 + "guzzlehttp/guzzle": "^6.0|^7.0", 2135 + "guzzlehttp/psr7": "^1.7|^2.0", 2136 + "php": ">=7.1||>=8.0" 2137 + }, 2138 + "require-dev": { 2139 + "ext-simplexml": "*", 2140 + "friendsofphp/php-cs-fixer": "^2.17", 2141 + "mockery/mockery": "^1.3.3", 2142 + "phpstan/phpstan": "^0.12.42", 2143 + "phpunit/phpunit": "^7.5||9.5" 2144 + }, 2145 + "suggest": { 2146 + "ext-simplexml": "For decoding XML-based responses." 2147 + }, 2148 + "type": "library", 2149 + "extra": { 2150 + "branch-alias": { 2151 + "dev-master": "1.0-dev", 2152 + "dev-develop": "2.0-dev" 2153 + } 2154 + }, 2155 + "autoload": { 2156 + "psr-4": { 2157 + "League\\OAuth1\\Client\\": "src/" 2158 + } 2159 + }, 2160 + "notification-url": "https://packagist.org/downloads/", 2161 + "license": [ 2162 + "MIT" 2163 + ], 2164 + "authors": [ 2165 + { 2166 + "name": "Ben Corlett", 2167 + "email": "bencorlett@me.com", 2168 + "homepage": "http://www.webcomm.com.au", 2169 + "role": "Developer" 2170 + } 2171 + ], 2172 + "description": "OAuth 1.0 Client Library", 2173 + "keywords": [ 2174 + "Authentication", 2175 + "SSO", 2176 + "authorization", 2177 + "bitbucket", 2178 + "identity", 2179 + "idp", 2180 + "oauth", 2181 + "oauth1", 2182 + "single sign on", 2183 + "trello", 2184 + "tumblr", 2185 + "twitter" 2186 + ], 2187 + "support": { 2188 + "issues": "https://github.com/thephpleague/oauth1-client/issues", 2189 + "source": "https://github.com/thephpleague/oauth1-client/tree/v1.11.0" 2190 + }, 2191 + "time": "2024-12-10T19:59:05+00:00" 2192 + }, 2193 + { 1982 2194 "name": "league/uri", 1983 2195 "version": "7.8.1", 1984 2196 "source": { ··· 2672 2884 "time": "2026-02-16T23:10:27+00:00" 2673 2885 }, 2674 2886 { 2887 + "name": "paragonie/constant_time_encoding", 2888 + "version": "v3.1.3", 2889 + "source": { 2890 + "type": "git", 2891 + "url": "https://github.com/paragonie/constant_time_encoding.git", 2892 + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" 2893 + }, 2894 + "dist": { 2895 + "type": "zip", 2896 + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", 2897 + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", 2898 + "shasum": "" 2899 + }, 2900 + "require": { 2901 + "php": "^8" 2902 + }, 2903 + "require-dev": { 2904 + "infection/infection": "^0", 2905 + "nikic/php-fuzzer": "^0", 2906 + "phpunit/phpunit": "^9|^10|^11", 2907 + "vimeo/psalm": "^4|^5|^6" 2908 + }, 2909 + "type": "library", 2910 + "autoload": { 2911 + "psr-4": { 2912 + "ParagonIE\\ConstantTime\\": "src/" 2913 + } 2914 + }, 2915 + "notification-url": "https://packagist.org/downloads/", 2916 + "license": [ 2917 + "MIT" 2918 + ], 2919 + "authors": [ 2920 + { 2921 + "name": "Paragon Initiative Enterprises", 2922 + "email": "security@paragonie.com", 2923 + "homepage": "https://paragonie.com", 2924 + "role": "Maintainer" 2925 + }, 2926 + { 2927 + "name": "Steve 'Sc00bz' Thomas", 2928 + "email": "steve@tobtu.com", 2929 + "homepage": "https://www.tobtu.com", 2930 + "role": "Original Developer" 2931 + } 2932 + ], 2933 + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", 2934 + "keywords": [ 2935 + "base16", 2936 + "base32", 2937 + "base32_decode", 2938 + "base32_encode", 2939 + "base64", 2940 + "base64_decode", 2941 + "base64_encode", 2942 + "bin2hex", 2943 + "encoding", 2944 + "hex", 2945 + "hex2bin", 2946 + "rfc4648" 2947 + ], 2948 + "support": { 2949 + "email": "info@paragonie.com", 2950 + "issues": "https://github.com/paragonie/constant_time_encoding/issues", 2951 + "source": "https://github.com/paragonie/constant_time_encoding" 2952 + }, 2953 + "time": "2025-09-24T15:06:41+00:00" 2954 + }, 2955 + { 2956 + "name": "paragonie/random_compat", 2957 + "version": "v9.99.100", 2958 + "source": { 2959 + "type": "git", 2960 + "url": "https://github.com/paragonie/random_compat.git", 2961 + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" 2962 + }, 2963 + "dist": { 2964 + "type": "zip", 2965 + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", 2966 + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", 2967 + "shasum": "" 2968 + }, 2969 + "require": { 2970 + "php": ">= 7" 2971 + }, 2972 + "require-dev": { 2973 + "phpunit/phpunit": "4.*|5.*", 2974 + "vimeo/psalm": "^1" 2975 + }, 2976 + "suggest": { 2977 + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." 2978 + }, 2979 + "type": "library", 2980 + "notification-url": "https://packagist.org/downloads/", 2981 + "license": [ 2982 + "MIT" 2983 + ], 2984 + "authors": [ 2985 + { 2986 + "name": "Paragon Initiative Enterprises", 2987 + "email": "security@paragonie.com", 2988 + "homepage": "https://paragonie.com" 2989 + } 2990 + ], 2991 + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", 2992 + "keywords": [ 2993 + "csprng", 2994 + "polyfill", 2995 + "pseudorandom", 2996 + "random" 2997 + ], 2998 + "support": { 2999 + "email": "info@paragonie.com", 3000 + "issues": "https://github.com/paragonie/random_compat/issues", 3001 + "source": "https://github.com/paragonie/random_compat" 3002 + }, 3003 + "time": "2020-10-15T08:29:30+00:00" 3004 + }, 3005 + { 2675 3006 "name": "phpoption/phpoption", 2676 3007 "version": "1.9.5", 2677 3008 "source": { ··· 2745 3076 } 2746 3077 ], 2747 3078 "time": "2025-12-27T19:41:33+00:00" 3079 + }, 3080 + { 3081 + "name": "phpseclib/phpseclib", 3082 + "version": "3.0.51", 3083 + "source": { 3084 + "type": "git", 3085 + "url": "https://github.com/phpseclib/phpseclib.git", 3086 + "reference": "d59c94077f9c9915abb51ddb52ce85188ece1748" 3087 + }, 3088 + "dist": { 3089 + "type": "zip", 3090 + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/d59c94077f9c9915abb51ddb52ce85188ece1748", 3091 + "reference": "d59c94077f9c9915abb51ddb52ce85188ece1748", 3092 + "shasum": "" 3093 + }, 3094 + "require": { 3095 + "paragonie/constant_time_encoding": "^1|^2|^3", 3096 + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", 3097 + "php": ">=5.6.1" 3098 + }, 3099 + "require-dev": { 3100 + "phpunit/phpunit": "*" 3101 + }, 3102 + "suggest": { 3103 + "ext-dom": "Install the DOM extension to load XML formatted public keys.", 3104 + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", 3105 + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", 3106 + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", 3107 + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." 3108 + }, 3109 + "type": "library", 3110 + "autoload": { 3111 + "files": [ 3112 + "phpseclib/bootstrap.php" 3113 + ], 3114 + "psr-4": { 3115 + "phpseclib3\\": "phpseclib/" 3116 + } 3117 + }, 3118 + "notification-url": "https://packagist.org/downloads/", 3119 + "license": [ 3120 + "MIT" 3121 + ], 3122 + "authors": [ 3123 + { 3124 + "name": "Jim Wigginton", 3125 + "email": "terrafrost@php.net", 3126 + "role": "Lead Developer" 3127 + }, 3128 + { 3129 + "name": "Patrick Monnerat", 3130 + "email": "pm@datasphere.ch", 3131 + "role": "Developer" 3132 + }, 3133 + { 3134 + "name": "Andreas Fischer", 3135 + "email": "bantu@phpbb.com", 3136 + "role": "Developer" 3137 + }, 3138 + { 3139 + "name": "Hans-Jürgen Petrich", 3140 + "email": "petrich@tronic-media.com", 3141 + "role": "Developer" 3142 + }, 3143 + { 3144 + "name": "Graham Campbell", 3145 + "email": "graham@alt-three.com", 3146 + "role": "Developer" 3147 + } 3148 + ], 3149 + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", 3150 + "homepage": "http://phpseclib.sourceforge.net", 3151 + "keywords": [ 3152 + "BigInteger", 3153 + "aes", 3154 + "asn.1", 3155 + "asn1", 3156 + "blowfish", 3157 + "crypto", 3158 + "cryptography", 3159 + "encryption", 3160 + "rsa", 3161 + "security", 3162 + "sftp", 3163 + "signature", 3164 + "signing", 3165 + "ssh", 3166 + "twofish", 3167 + "x.509", 3168 + "x509" 3169 + ], 3170 + "support": { 3171 + "issues": "https://github.com/phpseclib/phpseclib/issues", 3172 + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.51" 3173 + }, 3174 + "funding": [ 3175 + { 3176 + "url": "https://github.com/terrafrost", 3177 + "type": "github" 3178 + }, 3179 + { 3180 + "url": "https://www.patreon.com/phpseclib", 3181 + "type": "patreon" 3182 + }, 3183 + { 3184 + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", 3185 + "type": "tidelift" 3186 + } 3187 + ], 3188 + "time": "2026-04-10T01:33:53+00:00" 2748 3189 }, 2749 3190 { 2750 3191 "name": "phpstan/phpdoc-parser", ··· 3481 3922 "source": "https://github.com/ramsey/uuid/tree/4.9.2" 3482 3923 }, 3483 3924 "time": "2025-12-14T04:43:48+00:00" 3925 + }, 3926 + { 3927 + "name": "revolution/atproto-lexicon-contracts", 3928 + "version": "1.0.92", 3929 + "source": { 3930 + "type": "git", 3931 + "url": "https://github.com/invokable/atproto-lexicon-contracts.git", 3932 + "reference": "2ef8f54c21fe2495207960dc0724d3a23d58a8bd" 3933 + }, 3934 + "dist": { 3935 + "type": "zip", 3936 + "url": "https://api.github.com/repos/invokable/atproto-lexicon-contracts/zipball/2ef8f54c21fe2495207960dc0724d3a23d58a8bd", 3937 + "reference": "2ef8f54c21fe2495207960dc0724d3a23d58a8bd", 3938 + "shasum": "" 3939 + }, 3940 + "require": { 3941 + "php": "^8.2" 3942 + }, 3943 + "require-dev": { 3944 + "guzzlehttp/guzzle": "^7.9", 3945 + "laravel/pint": "^1.22" 3946 + }, 3947 + "type": "library", 3948 + "autoload": { 3949 + "psr-4": { 3950 + "Revolution\\AtProto\\Lexicon\\": "src/" 3951 + } 3952 + }, 3953 + "notification-url": "https://packagist.org/downloads/", 3954 + "license": [ 3955 + "MIT" 3956 + ], 3957 + "authors": [ 3958 + { 3959 + "name": "kawax", 3960 + "email": "kawaxbiz@gmail.com" 3961 + } 3962 + ], 3963 + "description": "Auto generated pure PHP interface and enum", 3964 + "keywords": [ 3965 + "Lexicon", 3966 + "atproto", 3967 + "bluesky", 3968 + "contracts" 3969 + ], 3970 + "support": { 3971 + "source": "https://github.com/invokable/atproto-lexicon-contracts/tree/1.0.92" 3972 + }, 3973 + "funding": [ 3974 + { 3975 + "url": "https://github.com/invokable", 3976 + "type": "github" 3977 + } 3978 + ], 3979 + "time": "2026-04-22T09:15:28+00:00" 3980 + }, 3981 + { 3982 + "name": "revolution/laravel-bluesky", 3983 + "version": "1.2.1", 3984 + "source": { 3985 + "type": "git", 3986 + "url": "https://github.com/invokable/laravel-bluesky.git", 3987 + "reference": "416fa2d61883a225e3981096ad091d1a48e3cc10" 3988 + }, 3989 + "dist": { 3990 + "type": "zip", 3991 + "url": "https://api.github.com/repos/invokable/laravel-bluesky/zipball/416fa2d61883a225e3981096ad091d1a48e3cc10", 3992 + "reference": "416fa2d61883a225e3981096ad091d1a48e3cc10", 3993 + "shasum": "" 3994 + }, 3995 + "require": { 3996 + "firebase/php-jwt": "^7.0", 3997 + "guzzlehttp/guzzle": "^7.8", 3998 + "illuminate/support": "^12.0||^13.0", 3999 + "laravel/socialite": "^5.16", 4000 + "php": "^8.3", 4001 + "phpseclib/phpseclib": "^3.0", 4002 + "revolution/atproto-lexicon-contracts": "1.0.92", 4003 + "yocto/yoclib-multibase": "^1.2" 4004 + }, 4005 + "require-dev": { 4006 + "laravel/pint": "^1.22", 4007 + "orchestra/testbench": "^11.0", 4008 + "revolt/event-loop": "^1.0", 4009 + "revolution/laravel-boost-copilot-cli": "^2.0", 4010 + "workerman/workerman": "^5.0" 4011 + }, 4012 + "suggest": { 4013 + "ext-gmp": "*", 4014 + "ext-pcntl": "*", 4015 + "revolt/event-loop": "Required to use WebSocket.", 4016 + "workerman/workerman": "Required to use WebSocket." 4017 + }, 4018 + "type": "library", 4019 + "extra": { 4020 + "laravel": { 4021 + "providers": [ 4022 + "Revolution\\Bluesky\\Providers\\BlueskyServiceProvider" 4023 + ] 4024 + } 4025 + }, 4026 + "autoload": { 4027 + "psr-4": { 4028 + "Revolution\\Bluesky\\": "src/" 4029 + } 4030 + }, 4031 + "notification-url": "https://packagist.org/downloads/", 4032 + "license": [ 4033 + "MIT" 4034 + ], 4035 + "authors": [ 4036 + { 4037 + "name": "kawax", 4038 + "email": "kawaxbiz@gmail.com" 4039 + } 4040 + ], 4041 + "description": "Bluesky(AT Protocol) for Laravel", 4042 + "keywords": [ 4043 + "atproto", 4044 + "bluesky", 4045 + "feed-generator", 4046 + "labeler", 4047 + "laravel", 4048 + "notifications", 4049 + "socialite" 4050 + ], 4051 + "support": { 4052 + "source": "https://github.com/invokable/laravel-bluesky/tree/1.2.1" 4053 + }, 4054 + "funding": [ 4055 + { 4056 + "url": "https://github.com/invokable", 4057 + "type": "github" 4058 + } 4059 + ], 4060 + "time": "2026-04-22T22:58:19+00:00" 3484 4061 }, 3485 4062 { 3486 4063 "name": "symfony/clock", ··· 6068 6645 } 6069 6646 ], 6070 6647 "time": "2026-04-16T23:10:39+00:00" 6648 + }, 6649 + { 6650 + "name": "yocto/yoclib-multibase", 6651 + "version": "v1.2.0", 6652 + "source": { 6653 + "type": "git", 6654 + "url": "https://github.com/yocto/yoclib-multibase-php.git", 6655 + "reference": "c7171897bf61dbc4a4cc6bb3f2fd5c3e62298e13" 6656 + }, 6657 + "dist": { 6658 + "type": "zip", 6659 + "url": "https://api.github.com/repos/yocto/yoclib-multibase-php/zipball/c7171897bf61dbc4a4cc6bb3f2fd5c3e62298e13", 6660 + "reference": "c7171897bf61dbc4a4cc6bb3f2fd5c3e62298e13", 6661 + "shasum": "" 6662 + }, 6663 + "require": { 6664 + "ext-mbstring": "*", 6665 + "php": "^7.4||^8" 6666 + }, 6667 + "require-dev": { 6668 + "phpunit/phpunit": "^7||^8||^9" 6669 + }, 6670 + "type": "library", 6671 + "autoload": { 6672 + "psr-4": { 6673 + "YOCLIB\\Multiformats\\Multibase\\": "src/" 6674 + } 6675 + }, 6676 + "notification-url": "https://packagist.org/downloads/", 6677 + "license": [ 6678 + "GPL-3.0-or-later" 6679 + ], 6680 + "description": "This yocLibrary enables your project to encode and decode Multibases in PHP.", 6681 + "keywords": [ 6682 + "composer", 6683 + "multibase", 6684 + "multiformats", 6685 + "php", 6686 + "yoclib", 6687 + "yocto" 6688 + ], 6689 + "support": { 6690 + "issues": "https://github.com/yocto/yoclib-multibase-php/issues", 6691 + "source": "https://github.com/yocto/yoclib-multibase-php/tree/v1.2.0" 6692 + }, 6693 + "time": "2024-06-05T13:42:01+00:00" 6071 6694 } 6072 6695 ], 6073 6696 "packages-dev": [
+133
config/bluesky.php
··· 1 + <?php 2 + 3 + declare(strict_types=1); 4 + 5 + return [ 6 + // service / PDS / Entryway url 7 + 'service' => env('BLUESKY_SERVICE', 'https://bsky.social'), 8 + 9 + // PLC directory 10 + 'plc' => env('BLUESKY_PLC', 'https://plc.directory'), 11 + 12 + // Public endpoint 13 + 'public_endpoint' => env('BLUESKY_PUBLIC_ENDPOINT', 'https://public.api.bsky.app'), 14 + 15 + // App password 16 + 'identifier' => env('BLUESKY_IDENTIFIER'), 17 + 'password' => env('BLUESKY_APP_PASSWORD'), 18 + 19 + // Notification 20 + 'notification' => [ 21 + // Use a specific sender and receiver in a PrivateChannel. 22 + 'private' => [ 23 + 'sender' => [ 24 + 'identifier' => env('BLUESKY_SENDER_IDENTIFIER'), 25 + 'password' => env('BLUESKY_SENDER_APP_PASSWORD'), 26 + ], 27 + 'receiver' => env('BLUESKY_RECEIVER'), 28 + ], 29 + ], 30 + 31 + // OAuth 32 + 'oauth' => [ 33 + // Disable all OAuth features 34 + 'disabled' => env('BLUESKY_OAUTH_DISABLED', false), 35 + 36 + // Client Metadata 37 + 'metadata' => [ 38 + 'scope' => env('BLUESKY_OAUTH_SCOPE', 'atproto transition:generic transition:email transition:chat.bsky'), 39 + 40 + 'grant_types' => ['authorization_code', 'refresh_token'], 41 + 'response_types' => ['code'], 42 + 'application_type' => env('BLUESKY_OAUTH_APPLICATION_TYPE', 'web'), 43 + 'token_endpoint_auth_method' => env('BLUESKY_OAUTH_TOKEN_METHOD', 'private_key_jwt'), 44 + 'token_endpoint_auth_signing_alg' => env('BLUESKY_OAUTH_TOKEN_SIGN', 'ES256'), 45 + 46 + 'dpop_bound_access_tokens' => env('BLUESKY_OAUTH_DPOP', true), 47 + 48 + // Optional fields 49 + 'client_name' => env('BLUESKY_OAUTH_CLIENT_NAME'), 50 + 'client_uri' => env('BLUESKY_OAUTH_CLIENT_URI'), 51 + 'logo_uri' => env('BLUESKY_OAUTH_LOGO'), 52 + 'tos_uri' => env('BLUESKY_OAUTH_TOS'), 53 + 'policy_uri' => env('BLUESKY_OAUTH_POLICY'), 54 + ], 55 + 56 + // Socialite 57 + 'client_id' => env('BLUESKY_CLIENT_ID'), 58 + 'redirect' => env('BLUESKY_REDIRECT'), 59 + 60 + // Private key(base64url encoded) 61 + 'private_key' => env('BLUESKY_OAUTH_PRIVATE_KEY'), 62 + 63 + // Route prefix 64 + 'prefix' => env('BLUESKY_OAUTH_PREFIX', '/bluesky/oauth/'), 65 + ], 66 + 67 + /** 68 + * Feed Generator. 69 + * Optional, as if not set, `did:web:example.com` will be used from the current URL. 70 + */ 71 + 'generator' => [ 72 + // Disable Feed Generator routes 73 + 'disabled' => env('BLUESKY_GENERATOR_DISABLED', false), 74 + 75 + // did:plc:*** 76 + 'service' => env('BLUESKY_GENERATOR_SERVICE'), 77 + // did:plc:*** 78 + 'publisher' => env('BLUESKY_GENERATOR_PUBLISHER'), 79 + ], 80 + 81 + 'well-known' => [ 82 + // Disable well-known routes 83 + 'disabled' => env('BLUESKY_WELLKNOWN_DISABLED', false), 84 + ], 85 + 86 + // Labeler 87 + 'labeler' => [ 88 + // Disable Labeler routes 89 + 'disabled' => env('BLUESKY_LABELER_DISABLED', false), 90 + 91 + 'did' => env('BLUESKY_LABELER_DID'), 92 + 'identifier' => env('BLUESKY_LABELER_IDENTIFIER'), 93 + 'password' => env('BLUESKY_LABELER_APP_PASSWORD'), 94 + 95 + // Private key(K256, base64url encoded) 96 + 'private_key' => env('BLUESKY_LABELER_PRIVATE_KEY'), 97 + 98 + 'host' => env('BLUESKY_LABELER_HOST', '127.0.0.1'), 99 + 100 + // WebSocket server port. Http server port is this port + 1. 101 + 'port' => (int) env('BLUESKY_LABELER_PORT', 7000), 102 + 103 + 'logging' => [ 104 + 'driver' => env('BLUESKY_LABELER_LOG_DRIVER', 'daily'), 105 + 'days' => 14, 106 + 'path' => env('BLUESKY_LABELER_LOG_PATH', storage_path('logs/labeler.log')), 107 + ], 108 + ], 109 + 110 + // WebSocket Jetstream 111 + 'jetstream' => [ 112 + 'host' => env('BLUESKY_JETSTREAM_HOST', 'jetstream1.us-west.bsky.network'), 113 + 114 + 'max' => env('BLUESKY_JETSTREAM_MAX', 0), 115 + 116 + 'logging' => [ 117 + 'driver' => env('BLUESKY_JETSTREAM_LOG_DRIVER', 'daily'), 118 + 'days' => 7, 119 + 'path' => env('BLUESKY_JETSTREAM_LOG_PATH', storage_path('logs/jetstream.log')), 120 + ], 121 + ], 122 + 123 + // WebSocket Firehose 124 + 'firehose' => [ 125 + 'host' => env('BLUESKY_FIREHOSE_HOST', 'bsky.network'), 126 + 127 + 'logging' => [ 128 + 'driver' => env('BLUESKY_FIREHOSE_LOG_DRIVER', 'daily'), 129 + 'days' => 7, 130 + 'path' => env('BLUESKY_FIREHOSE_LOG_PATH', storage_path('logs/firehose.log')), 131 + ], 132 + ], 133 + ];
+3 -39
database/factories/UserFactory.php
··· 4 4 5 5 use App\Models\User; 6 6 use Illuminate\Database\Eloquent\Factories\Factory; 7 - use Illuminate\Support\Facades\Hash; 8 - use Illuminate\Support\Str; 9 7 10 8 /** 11 9 * @extends Factory<User> ··· 13 11 class UserFactory extends Factory 14 12 { 15 13 /** 16 - * The current password being used by the factory. 17 - */ 18 - protected static ?string $password; 19 - 20 - /** 21 - * Define the model's default state. 22 - * 23 14 * @return array<string, mixed> 24 15 */ 25 16 public function definition(): array 26 17 { 27 18 return [ 28 - 'name' => fake()->name(), 29 - 'email' => fake()->unique()->safeEmail(), 30 - 'email_verified_at' => now(), 31 - 'password' => static::$password ??= Hash::make('password'), 32 - 'remember_token' => Str::random(10), 33 - 'two_factor_secret' => null, 34 - 'two_factor_recovery_codes' => null, 35 - 'two_factor_confirmed_at' => null, 19 + 'did' => 'did:plc:'.fake()->regexify('[a-z0-9]{24}'), 20 + 'refresh_token' => fake()->sha256(), 21 + 'iss' => 'https://bsky.social', 36 22 ]; 37 - } 38 - 39 - /** 40 - * Indicate that the model's email address should be unverified. 41 - */ 42 - public function unverified(): static 43 - { 44 - return $this->state(fn (array $attributes) => [ 45 - 'email_verified_at' => null, 46 - ]); 47 - } 48 - 49 - /** 50 - * Indicate that the model has two-factor authentication configured. 51 - */ 52 - public function withTwoFactor(): static 53 - { 54 - return $this->state(fn (array $attributes) => [ 55 - 'two_factor_secret' => encrypt('secret'), 56 - 'two_factor_recovery_codes' => encrypt(json_encode(['recovery-code-1'])), 57 - 'two_factor_confirmed_at' => now(), 58 - ]); 59 23 } 60 24 }
+4 -13
database/migrations/0001_01_01_000000_create_users_table.php
··· 12 12 public function up(): void 13 13 { 14 14 Schema::create('users', function (Blueprint $table) { 15 - $table->id(); 16 - $table->string('name'); 17 - $table->string('email')->unique(); 18 - $table->timestamp('email_verified_at')->nullable(); 19 - $table->string('password'); 15 + $table->string('did')->primary(); 16 + $table->text('refresh_token')->nullable(); 17 + $table->string('iss')->nullable(); 20 18 $table->rememberToken(); 21 19 $table->timestamps(); 22 20 }); 23 21 24 - Schema::create('password_reset_tokens', function (Blueprint $table) { 25 - $table->string('email')->primary(); 26 - $table->string('token'); 27 - $table->timestamp('created_at')->nullable(); 28 - }); 29 - 30 22 Schema::create('sessions', function (Blueprint $table) { 31 23 $table->string('id')->primary(); 32 - $table->foreignId('user_id')->nullable()->index(); 24 + $table->string('user_id')->nullable()->index(); 33 25 $table->string('ip_address', 45)->nullable(); 34 26 $table->text('user_agent')->nullable(); 35 27 $table->longText('payload'); ··· 43 35 public function down(): void 44 36 { 45 37 Schema::dropIfExists('users'); 46 - Schema::dropIfExists('password_reset_tokens'); 47 38 Schema::dropIfExists('sessions'); 48 39 } 49 40 };
+1 -8
database/seeders/DatabaseSeeder.php
··· 2 2 3 3 namespace Database\Seeders; 4 4 5 - use App\Models\User; 6 - // use Illuminate\Database\Console\Seeds\WithoutModelEvents; 7 5 use Illuminate\Database\Seeder; 8 6 9 7 class DatabaseSeeder extends Seeder ··· 13 11 */ 14 12 public function run(): void 15 13 { 16 - // User::factory(10)->create(); 17 - 18 - User::factory()->create([ 19 - 'name' => 'Test User', 20 - 'email' => 'test@example.com', 21 - ]); 14 + // 22 15 } 23 16 }
+12 -9
resources/js/components/app-header.tsx
··· 3 3 import AppLogo from '@/components/app-logo'; 4 4 import AppLogoIcon from '@/components/app-logo-icon'; 5 5 import { Breadcrumbs } from '@/components/breadcrumbs'; 6 - import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; 6 + import { Avatar, AvatarFallback } from '@/components/ui/avatar'; 7 7 import { Button } from '@/components/ui/button'; 8 8 import { 9 9 DropdownMenu, ··· 30 30 } from '@/components/ui/tooltip'; 31 31 import { UserMenuContent } from '@/components/user-menu-content'; 32 32 import { useCurrentUrl } from '@/hooks/use-current-url'; 33 - import { useInitials } from '@/hooks/use-initials'; 34 33 import { cn, toUrl } from '@/lib/utils'; 35 34 import { dashboard } from '@/routes'; 36 35 import type { BreadcrumbItem, NavItem } from '@/types'; ··· 66 65 export function AppHeader({ breadcrumbs = [] }: Props) { 67 66 const page = usePage(); 68 67 const { auth } = page.props; 69 - const getInitials = useInitials(); 70 68 const { isCurrentUrl, whenCurrentUrl } = useCurrentUrl(); 69 + const initials = auth.handle 70 + ? auth.handle.slice(0, 2).toUpperCase() 71 + : (auth.user?.did 72 + .replace(/^did:[a-z]+:/, '') 73 + .slice(0, 2) 74 + .toUpperCase() ?? ''); 71 75 72 76 return ( 73 77 <> ··· 217 221 className="size-10 rounded-full p-1" 218 222 > 219 223 <Avatar className="size-8 overflow-hidden rounded-full"> 220 - <AvatarImage 221 - src={auth.user?.avatar} 222 - alt={auth.user?.name} 223 - /> 224 224 <AvatarFallback className="rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white"> 225 - {getInitials(auth.user?.name ?? '')} 225 + {initials} 226 226 </AvatarFallback> 227 227 </Avatar> 228 228 </Button> 229 229 </DropdownMenuTrigger> 230 230 <DropdownMenuContent className="w-56" align="end"> 231 231 {auth.user && ( 232 - <UserMenuContent user={auth.user} /> 232 + <UserMenuContent 233 + handle={auth.handle} 234 + did={auth.user.did} 235 + /> 233 236 )} 234 237 </DropdownMenuContent> 235 238 </DropdownMenu>
-120
resources/js/components/delete-user.tsx
··· 1 - import { Form } from '@inertiajs/react'; 2 - import { useRef } from 'react'; 3 - import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController'; 4 - import Heading from '@/components/heading'; 5 - import InputError from '@/components/input-error'; 6 - import PasswordInput from '@/components/password-input'; 7 - import { Button } from '@/components/ui/button'; 8 - import { 9 - Dialog, 10 - DialogClose, 11 - DialogContent, 12 - DialogDescription, 13 - DialogFooter, 14 - DialogTitle, 15 - DialogTrigger, 16 - } from '@/components/ui/dialog'; 17 - import { Label } from '@/components/ui/label'; 18 - 19 - export default function DeleteUser() { 20 - const passwordInput = useRef<HTMLInputElement>(null); 21 - 22 - return ( 23 - <div className="space-y-6"> 24 - <Heading 25 - variant="small" 26 - title="Delete account" 27 - description="Delete your account and all of its resources" 28 - /> 29 - <div className="space-y-4 rounded-lg border border-red-100 bg-red-50 p-4 dark:border-red-200/10 dark:bg-red-700/10"> 30 - <div className="relative space-y-0.5 text-red-600 dark:text-red-100"> 31 - <p className="font-medium">Warning</p> 32 - <p className="text-sm"> 33 - Please proceed with caution, this cannot be undone. 34 - </p> 35 - </div> 36 - 37 - <Dialog> 38 - <DialogTrigger asChild> 39 - <Button 40 - variant="destructive" 41 - data-test="delete-user-button" 42 - > 43 - Delete account 44 - </Button> 45 - </DialogTrigger> 46 - <DialogContent> 47 - <DialogTitle> 48 - Are you sure you want to delete your account? 49 - </DialogTitle> 50 - <DialogDescription> 51 - Once your account is deleted, all of its resources 52 - and data will also be permanently deleted. Please 53 - enter your password to confirm you would like to 54 - permanently delete your account. 55 - </DialogDescription> 56 - 57 - <Form 58 - {...ProfileController.destroy.form()} 59 - options={{ 60 - preserveScroll: true, 61 - }} 62 - onError={() => passwordInput.current?.focus()} 63 - resetOnSuccess 64 - className="space-y-6" 65 - > 66 - {({ resetAndClearErrors, processing, errors }) => ( 67 - <> 68 - <div className="grid gap-2"> 69 - <Label 70 - htmlFor="password" 71 - className="sr-only" 72 - > 73 - Password 74 - </Label> 75 - 76 - <PasswordInput 77 - id="password" 78 - name="password" 79 - ref={passwordInput} 80 - placeholder="Password" 81 - autoComplete="current-password" 82 - /> 83 - 84 - <InputError message={errors.password} /> 85 - </div> 86 - 87 - <DialogFooter className="gap-2"> 88 - <DialogClose asChild> 89 - <Button 90 - variant="secondary" 91 - onClick={() => 92 - resetAndClearErrors() 93 - } 94 - > 95 - Cancel 96 - </Button> 97 - </DialogClose> 98 - 99 - <Button 100 - variant="destructive" 101 - disabled={processing} 102 - asChild 103 - > 104 - <button 105 - type="submit" 106 - data-test="confirm-delete-user-button" 107 - > 108 - Delete account 109 - </button> 110 - </Button> 111 - </DialogFooter> 112 - </> 113 - )} 114 - </Form> 115 - </DialogContent> 116 - </Dialog> 117 - </div> 118 - </div> 119 - ); 120 - }
+8 -2
resources/js/components/nav-user.tsx
··· 34 34 className="group text-sidebar-accent-foreground data-[state=open]:bg-sidebar-accent" 35 35 data-test="sidebar-menu-button" 36 36 > 37 - <UserInfo user={auth.user} /> 37 + <UserInfo 38 + handle={auth.handle} 39 + did={auth.user.did} 40 + /> 38 41 <ChevronsUpDown className="ml-auto size-4" /> 39 42 </SidebarMenuButton> 40 43 </DropdownMenuTrigger> ··· 49 52 : 'bottom' 50 53 } 51 54 > 52 - <UserMenuContent user={auth.user} /> 55 + <UserMenuContent 56 + handle={auth.handle} 57 + did={auth.user.did} 58 + /> 53 59 </DropdownMenuContent> 54 60 </DropdownMenu> 55 61 </SidebarMenuItem>
-37
resources/js/components/password-input.tsx
··· 1 - import { Eye, EyeOff } from 'lucide-react'; 2 - import type { ComponentProps, Ref } from 'react'; 3 - import { useState } from 'react'; 4 - import { Input } from '@/components/ui/input'; 5 - import { cn } from '@/lib/utils'; 6 - 7 - export default function PasswordInput({ 8 - className, 9 - ref, 10 - ...props 11 - }: Omit<ComponentProps<'input'>, 'type'> & { ref?: Ref<HTMLInputElement> }) { 12 - const [showPassword, setShowPassword] = useState(false); 13 - 14 - return ( 15 - <div className="relative"> 16 - <Input 17 - type={showPassword ? 'text' : 'password'} 18 - className={cn('pr-10', className)} 19 - ref={ref} 20 - {...props} 21 - /> 22 - <button 23 - type="button" 24 - onClick={() => setShowPassword((prev) => !prev)} 25 - className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-3 text-muted-foreground hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring focus-visible:outline-none" 26 - aria-label={showPassword ? 'Hide password' : 'Show password'} 27 - tabIndex={-1} 28 - > 29 - {showPassword ? ( 30 - <EyeOff className="size-4" /> 31 - ) : ( 32 - <Eye className="size-4" /> 33 - )} 34 - </button> 35 - </div> 36 - ); 37 - }
+28 -13
resources/js/components/user-info.tsx
··· 1 - import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; 2 - import { useInitials } from '@/hooks/use-initials'; 3 - import type { User } from '@/types'; 1 + import { Avatar, AvatarFallback } from '@/components/ui/avatar'; 2 + 3 + function initialsFromHandle(handle: string | null, did: string): string { 4 + const source = handle ?? did.replace(/^did:[a-z]+:/, ''); 5 + 6 + return source.slice(0, 2).toUpperCase(); 7 + } 8 + 9 + function truncateDid(did: string): string { 10 + const suffix = did.replace(/^did:[a-z]+:/, ''); 11 + 12 + if (suffix.length <= 10) { 13 + return did; 14 + } 15 + 16 + return `${did.slice(0, 12)}…${suffix.slice(-4)}`; 17 + } 4 18 5 19 export function UserInfo({ 6 - user, 7 - showEmail = false, 20 + handle, 21 + did, 22 + showDid = false, 8 23 }: { 9 - user: User; 10 - showEmail?: boolean; 24 + handle: string | null; 25 + did: string; 26 + showDid?: boolean; 11 27 }) { 12 - const getInitials = useInitials(); 28 + const label = handle ? `@${handle}` : truncateDid(did); 13 29 14 30 return ( 15 31 <> 16 32 <Avatar className="h-8 w-8 overflow-hidden rounded-full"> 17 - <AvatarImage src={user.avatar} alt={user.name} /> 18 33 <AvatarFallback className="rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white"> 19 - {getInitials(user.name)} 34 + {initialsFromHandle(handle, did)} 20 35 </AvatarFallback> 21 36 </Avatar> 22 37 <div className="grid flex-1 text-left text-sm leading-tight"> 23 - <span className="truncate font-medium">{user.name}</span> 24 - {showEmail && ( 38 + <span className="truncate font-medium">{label}</span> 39 + {showDid && handle && ( 25 40 <span className="truncate text-xs text-muted-foreground"> 26 - {user.email} 41 + {truncateDid(did)} 27 42 </span> 28 43 )} 29 44 </div>
+7 -23
resources/js/components/user-menu-content.tsx
··· 1 1 import { Link, router } from '@inertiajs/react'; 2 - import { LogOut, Settings } from 'lucide-react'; 2 + import { LogOut } from 'lucide-react'; 3 3 import { 4 - DropdownMenuGroup, 5 4 DropdownMenuItem, 6 5 DropdownMenuLabel, 7 6 DropdownMenuSeparator, 8 7 } from '@/components/ui/dropdown-menu'; 9 8 import { UserInfo } from '@/components/user-info'; 10 9 import { useMobileNavigation } from '@/hooks/use-mobile-navigation'; 11 - import { edit } from '@/routes/profile'; 12 - import type { User } from '@/types'; 10 + import { logout } from '@/routes'; 13 11 14 12 type Props = { 15 - user: User; 13 + handle: string | null; 14 + did: string; 16 15 }; 17 16 18 - export function UserMenuContent({ user }: Props) { 17 + export function UserMenuContent({ handle, did }: Props) { 19 18 const cleanup = useMobileNavigation(); 20 19 21 20 const handleLogout = () => { ··· 27 26 <> 28 27 <DropdownMenuLabel className="p-0 font-normal"> 29 28 <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm"> 30 - <UserInfo user={user} showEmail={true} /> 29 + <UserInfo handle={handle} did={did} showDid /> 31 30 </div> 32 31 </DropdownMenuLabel> 33 32 <DropdownMenuSeparator /> 34 - <DropdownMenuGroup> 35 - <DropdownMenuItem asChild> 36 - <Link 37 - className="block w-full cursor-pointer" 38 - href={edit()} 39 - prefetch 40 - onClick={cleanup} 41 - > 42 - <Settings className="mr-2" /> 43 - Settings 44 - </Link> 45 - </DropdownMenuItem> 46 - </DropdownMenuGroup> 47 - <DropdownMenuSeparator /> 48 33 <DropdownMenuItem asChild> 49 34 <Link 50 35 className="block w-full cursor-pointer" 51 - href="/logout" 52 - method="post" 36 + href={logout()} 53 37 as="button" 54 38 onClick={handleLogout} 55 39 data-test="logout-button"
+1 -13
resources/js/layouts/settings/layout.tsx
··· 6 6 import { useCurrentUrl } from '@/hooks/use-current-url'; 7 7 import { cn, toUrl } from '@/lib/utils'; 8 8 import { edit as editAppearance } from '@/routes/appearance'; 9 - import { edit } from '@/routes/profile'; 10 - import { edit as editSecurity } from '@/routes/security'; 11 9 import type { NavItem } from '@/types'; 12 10 13 11 const sidebarNavItems: NavItem[] = [ 14 12 { 15 - title: 'Profile', 16 - href: edit(), 17 - icon: null, 18 - }, 19 - { 20 - title: 'Security', 21 - href: editSecurity(), 22 - icon: null, 23 - }, 24 - { 25 13 title: 'Appearance', 26 14 href: editAppearance(), 27 15 icon: null, ··· 35 23 <div className="px-4 py-6"> 36 24 <Heading 37 25 title="Settings" 38 - description="Manage your profile and account settings" 26 + description="Manage your account settings" 39 27 /> 40 28 41 29 <div className="flex flex-col lg:flex-row lg:space-x-12">
+47
resources/js/pages/auth/login.tsx
··· 1 + import { Form, Head } from '@inertiajs/react'; 2 + import InputError from '@/components/input-error'; 3 + import { Button } from '@/components/ui/button'; 4 + import { Input } from '@/components/ui/input'; 5 + import { Label } from '@/components/ui/label'; 6 + import AuthLayout from '@/layouts/auth-layout'; 7 + 8 + export default function Login() { 9 + return ( 10 + <AuthLayout 11 + title="Sign in with Bluesky" 12 + description="Enter your handle to continue" 13 + > 14 + <Head title="Log in" /> 15 + 16 + <Form action="/login" method="post" className="space-y-6"> 17 + {({ processing, errors }) => ( 18 + <> 19 + <div className="grid gap-2"> 20 + <Label htmlFor="handle">Handle</Label> 21 + <Input 22 + id="handle" 23 + type="text" 24 + name="handle" 25 + autoComplete="username" 26 + autoFocus 27 + placeholder="alice.bsky.social" 28 + inputMode="email" 29 + spellCheck={false} 30 + /> 31 + <InputError message={errors.handle} /> 32 + </div> 33 + 34 + <Button 35 + type="submit" 36 + disabled={processing} 37 + className="w-full" 38 + data-test="login-submit" 39 + > 40 + Continue 41 + </Button> 42 + </> 43 + )} 44 + </Form> 45 + </AuthLayout> 46 + ); 47 + }
-100
resources/js/pages/settings/profile.tsx
··· 1 - import { Form, Head, usePage } from '@inertiajs/react'; 2 - import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController'; 3 - import DeleteUser from '@/components/delete-user'; 4 - import Heading from '@/components/heading'; 5 - import InputError from '@/components/input-error'; 6 - import { Button } from '@/components/ui/button'; 7 - import { Input } from '@/components/ui/input'; 8 - import { Label } from '@/components/ui/label'; 9 - import { edit } from '@/routes/profile'; 10 - 11 - export default function Profile() { 12 - const { auth } = usePage().props; 13 - 14 - return ( 15 - <> 16 - <Head title="Profile settings" /> 17 - 18 - <h1 className="sr-only">Profile settings</h1> 19 - 20 - <div className="space-y-6"> 21 - <Heading 22 - variant="small" 23 - title="Profile information" 24 - description="Update your name and email address" 25 - /> 26 - 27 - <Form 28 - {...ProfileController.update.form()} 29 - options={{ 30 - preserveScroll: true, 31 - }} 32 - className="space-y-6" 33 - > 34 - {({ processing, errors }) => ( 35 - <> 36 - <div className="grid gap-2"> 37 - <Label htmlFor="name">Name</Label> 38 - 39 - <Input 40 - id="name" 41 - className="mt-1 block w-full" 42 - defaultValue={auth.user.name} 43 - name="name" 44 - required 45 - autoComplete="name" 46 - placeholder="Full name" 47 - /> 48 - 49 - <InputError 50 - className="mt-2" 51 - message={errors.name} 52 - /> 53 - </div> 54 - 55 - <div className="grid gap-2"> 56 - <Label htmlFor="email">Email address</Label> 57 - 58 - <Input 59 - id="email" 60 - type="email" 61 - className="mt-1 block w-full" 62 - defaultValue={auth.user.email} 63 - name="email" 64 - required 65 - autoComplete="username" 66 - placeholder="Email address" 67 - /> 68 - 69 - <InputError 70 - className="mt-2" 71 - message={errors.email} 72 - /> 73 - </div> 74 - 75 - <div className="flex items-center gap-4"> 76 - <Button 77 - disabled={processing} 78 - data-test="update-profile-button" 79 - > 80 - Save 81 - </Button> 82 - </div> 83 - </> 84 - )} 85 - </Form> 86 - </div> 87 - 88 - <DeleteUser /> 89 - </> 90 - ); 91 - } 92 - 93 - Profile.layout = { 94 - breadcrumbs: [ 95 - { 96 - title: 'Profile settings', 97 - href: edit(), 98 - }, 99 - ], 100 - };
+4 -7
resources/js/types/auth.ts
··· 1 1 export type User = { 2 - id: number; 3 - name: string; 4 - email: string; 5 - avatar?: string; 6 - email_verified_at: string | null; 2 + did: string; 3 + iss: string | null; 7 4 created_at: string; 8 5 updated_at: string; 9 - [key: string]: unknown; 10 6 }; 11 7 12 8 export type Auth = { 13 - user: User; 9 + user: User | null; 10 + handle: string | null; 14 11 };
+1 -6
routes/settings.php
··· 1 1 <?php 2 2 3 - use App\Http\Controllers\Settings\ProfileController; 4 3 use Illuminate\Support\Facades\Route; 5 4 6 5 Route::middleware(['auth'])->group(function () { 7 - Route::redirect('settings', '/settings/profile'); 8 - 9 - Route::get('settings/profile', [ProfileController::class, 'edit'])->name('profile.edit'); 10 - Route::patch('settings/profile', [ProfileController::class, 'update'])->name('profile.update'); 11 - Route::delete('settings/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); 6 + Route::redirect('settings', '/settings/appearance'); 12 7 13 8 Route::inertia('settings/appearance', 'settings/appearance')->name('appearance.edit'); 14 9 });
+33 -1
routes/web.php
··· 1 1 <?php 2 2 3 + use App\Http\Controllers\Auth\AuthenticatedSessionController; 4 + use App\Http\Controllers\Auth\OAuthCallbackController; 5 + use Illuminate\Http\Request; 3 6 use Illuminate\Support\Facades\Route; 7 + use Inertia\Inertia; 8 + use Revolution\Bluesky\Socialite\Http\OAuthMetaController; 4 9 5 - Route::inertia('/', 'welcome')->name('home'); 10 + Route::get('/', function (Request $request) { 11 + // In local dev the package hardcodes the OAuth redirect URI to 12 + // http://127.0.0.1:8000/ (BlueskyServiceProvider::socialite()). Intercept 13 + // the ?iss callback here and forward to the real handler. 14 + if (app()->isLocal() && $request->has('iss')) { 15 + return to_route('bluesky.oauth.redirect', $request->query()); 16 + } 17 + 18 + return Inertia::render('welcome'); 19 + })->name('home'); 20 + 21 + Route::middleware('guest')->group(function () { 22 + Route::get('login', [AuthenticatedSessionController::class, 'create'])->name('login'); 23 + Route::post('login', [AuthenticatedSessionController::class, 'store']); 24 + }); 25 + 26 + Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) 27 + ->middleware('auth') 28 + ->name('logout'); 29 + 30 + // Canonical feat-auth-aligned OAuth endpoints. The package's own 31 + // /bluesky/oauth/{client-metadata,jwks}.json routes still exist (harmless). 32 + Route::get('oauth-client-metadata.json', [OAuthMetaController::class, 'clientMetadata']) 33 + ->name('oauth.client-metadata'); 34 + Route::get('oauth-jwks.json', [OAuthMetaController::class, 'jwks']) 35 + ->name('oauth.jwks'); 36 + Route::get('oauth/callback', OAuthCallbackController::class) 37 + ->name('bluesky.oauth.redirect'); 6 38 7 39 Route::middleware(['auth'])->group(function () { 8 40 Route::inertia('dashboard', 'dashboard')->name('dashboard');
+72
tests/Feature/Auth/LoginTest.php
··· 1 + <?php 2 + 3 + use App\Models\User; 4 + use Illuminate\Foundation\Testing\RefreshDatabase; 5 + use Laravel\Socialite\Facades\Socialite; 6 + use Laravel\Socialite\Two\User as SocialiteUser; 7 + use Revolution\Bluesky\Session\OAuthSession; 8 + use Revolution\Bluesky\Socialite\BlueskyProvider; 9 + 10 + uses(RefreshDatabase::class); 11 + 12 + test('login page renders', function () { 13 + $this->get(route('login'))->assertOk(); 14 + }); 15 + 16 + test('POST /login redirects to Bluesky with handle hint', function () { 17 + $provider = Mockery::mock(BlueskyProvider::class); 18 + $provider->shouldReceive('setScopes')->andReturnSelf(); 19 + $provider->shouldReceive('hint')->with('alice.bsky.social')->andReturnSelf(); 20 + $provider->shouldReceive('redirect')->andReturn( 21 + redirect('https://bsky.social/oauth/authorize') 22 + ); 23 + 24 + Socialite::shouldReceive('driver')->with('bluesky')->andReturn($provider); 25 + 26 + $this->post(route('login'), ['handle' => 'alice.bsky.social']) 27 + ->assertRedirect('https://bsky.social/oauth/authorize'); 28 + 29 + expect(session('atproto.hint'))->toBe('alice.bsky.social'); 30 + }); 31 + 32 + test('OAuth callback creates user, stashes handle, and logs in', function () { 33 + $session = OAuthSession::create([ 34 + 'did' => 'did:plc:testuser1234567890abcd', 35 + 'handle' => 'alice.bsky.social', 36 + 'iss' => 'https://bsky.social', 37 + 'refresh_token' => 'fake-refresh-token', 38 + ]); 39 + 40 + $socialiteUser = new SocialiteUser; 41 + $socialiteUser->session = $session; 42 + 43 + $provider = Mockery::mock(BlueskyProvider::class); 44 + $provider->shouldReceive('setScopes')->andReturnSelf(); 45 + $provider->shouldReceive('hint')->andReturnSelf(); 46 + $provider->shouldReceive('user')->andReturn($socialiteUser); 47 + 48 + Socialite::shouldReceive('driver')->with('bluesky')->andReturn($provider); 49 + 50 + $this->withSession(['atproto.hint' => 'alice.bsky.social']) 51 + ->get(route('bluesky.oauth.redirect')) 52 + ->assertRedirect(route('dashboard')); 53 + 54 + $user = User::find('did:plc:testuser1234567890abcd'); 55 + 56 + expect($user)->not->toBeNull() 57 + ->and($user->refresh_token)->toBe('fake-refresh-token') 58 + ->and($user->iss)->toBe('https://bsky.social'); 59 + 60 + $this->assertAuthenticatedAs($user); 61 + expect(session('atproto.handle'))->toBe('alice.bsky.social'); 62 + }); 63 + 64 + test('POST /logout ends the session', function () { 65 + $user = User::factory()->create(); 66 + 67 + $this->actingAs($user) 68 + ->post(route('logout')) 69 + ->assertRedirect('/'); 70 + 71 + $this->assertGuest(); 72 + });
+4 -1
tests/Feature/DashboardTest.php
··· 1 1 <?php 2 2 3 3 use App\Models\User; 4 + use Illuminate\Foundation\Testing\RefreshDatabase; 5 + 6 + uses(RefreshDatabase::class); 4 7 5 8 test('guests are redirected to the login page', function () { 6 9 $response = $this->get(route('dashboard')); ··· 13 16 14 17 $response = $this->get(route('dashboard')); 15 18 $response->assertOk(); 16 - }); 19 + });
-85
tests/Feature/Settings/ProfileUpdateTest.php
··· 1 - <?php 2 - 3 - use App\Models\User; 4 - 5 - test('profile page is displayed', function () { 6 - $user = User::factory()->create(); 7 - 8 - $response = $this 9 - ->actingAs($user) 10 - ->get(route('profile.edit')); 11 - 12 - $response->assertOk(); 13 - }); 14 - 15 - test('profile information can be updated', function () { 16 - $user = User::factory()->create(); 17 - 18 - $response = $this 19 - ->actingAs($user) 20 - ->patch(route('profile.update'), [ 21 - 'name' => 'Test User', 22 - 'email' => 'test@example.com', 23 - ]); 24 - 25 - $response 26 - ->assertSessionHasNoErrors() 27 - ->assertRedirect(route('profile.edit')); 28 - 29 - $user->refresh(); 30 - 31 - expect($user->name)->toBe('Test User'); 32 - expect($user->email)->toBe('test@example.com'); 33 - expect($user->email_verified_at)->toBeNull(); 34 - }); 35 - 36 - test('email verification status is unchanged when the email address is unchanged', function () { 37 - $user = User::factory()->create(); 38 - 39 - $response = $this 40 - ->actingAs($user) 41 - ->patch(route('profile.update'), [ 42 - 'name' => 'Test User', 43 - 'email' => $user->email, 44 - ]); 45 - 46 - $response 47 - ->assertSessionHasNoErrors() 48 - ->assertRedirect(route('profile.edit')); 49 - 50 - expect($user->refresh()->email_verified_at)->not->toBeNull(); 51 - }); 52 - 53 - test('user can delete their account', function () { 54 - $user = User::factory()->create(); 55 - 56 - $response = $this 57 - ->actingAs($user) 58 - ->delete(route('profile.destroy'), [ 59 - 'password' => 'password', 60 - ]); 61 - 62 - $response 63 - ->assertSessionHasNoErrors() 64 - ->assertRedirect(route('home')); 65 - 66 - $this->assertGuest(); 67 - expect($user->fresh())->toBeNull(); 68 - }); 69 - 70 - test('correct password must be provided to delete account', function () { 71 - $user = User::factory()->create(); 72 - 73 - $response = $this 74 - ->actingAs($user) 75 - ->from(route('profile.edit')) 76 - ->delete(route('profile.destroy'), [ 77 - 'password' => 'wrong-password', 78 - ]); 79 - 80 - $response 81 - ->assertSessionHasErrors('password') 82 - ->assertRedirect(route('profile.edit')); 83 - 84 - expect($user->fresh())->not->toBeNull(); 85 - });