Release v0.1.19 — Phase 19: API Foundation#273
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ute-group CONV-291: Create API v1 route group
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-error-contract CONV-292: Add standard API error contract
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…xception-mapping CONV-293: Add API domain exception mapping
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…table CONV-294: Create API keys table
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…odel-and-relations CONV-295: Create ApiKey model and relations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eneration-service CONV-296: Create API key generation service
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ntication-middleware CONV-297: Create API authentication middleware
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ss-feature-gate CONV-298: Enforce API access feature gate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…iting-baseline CONV-299: Add API rate limiting baseline
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ndex-endpoint CONV-300: Add converters index endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…hema-endpoint CONV-301: Add converter schema endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…oad-endpoint CONV-302: Add API file upload endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…formats-endpoint CONV-303: Add file target formats endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ost-estimate-endpoint CONV-304: Add conversion cost estimate endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rsion-endpoint CONV-305: Add create conversion endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tatus-endpoint CONV-306: Add conversion status endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…esult-download-endpoint CONV-307: Add conversion result download endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nce-endpoint CONV-308: Add credits balance endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-response-dtos CONV-309: Add API resource response DTOs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…p-guards CONV-310: Add API ownership guards
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…th-feature-test CONV-311: Add API happy path feature test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…on-final-smoke-tests CONV-312: Add API foundation final smoke tests
📝 WalkthroughWalkthroughThis PR introduces a complete Laravel API v1 with bearer-token authentication, feature-based access control, structured error handling, and full CRUD operations for file conversion workflows. It includes API key management, rate limiting per user plan, ownership validation, and endpoints for uploading files, discovering converters, estimating conversion costs, creating jobs, polling status, and downloading results. ChangesAPI v1 Core Implementation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
app/Http/Requests/Api/V1/CreateConversionRequest.php (1)
11-17: ⚡ Quick winExtract the shared conversion payload rules.
CreateConversionRequestandEstimateConversionRequestcurrently carry the same validation contract. Pulling these rules into a shared base request/helper will keep the estimate/create endpoints from drifting onfile_id,target_format, oroptionssemantics.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Http/Requests/Api/V1/CreateConversionRequest.php` around lines 11 - 17, Extract the shared validation array from CreateConversionRequest::rules() into a single reusable place (either a base request class like ConversionRequestBase with a protected conversionRules(): array method or a ConversionValidation trait exposing conversionRules()). Replace both CreateConversionRequest::rules() and EstimateConversionRequest::rules() to call and return $this->conversionRules() (or merge with any specific rules), keeping the same keys ('file_id','target_format','options') and validation definitions; update class declarations to extend the new base or use the trait so both requests share the canonical rules.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/Http/Controllers/Api/V1/ConversionController.php`:
- Around line 31-56: The controller's estimate() (and similarly download())
currently contains business flow: loading FileRecord, ownership checks via
ownershipGuard, converter lookup via registry->find, option validation, cost
estimation via EstimateConversionCostAction, and balance check—move this
orchestration into a new Action class (e.g., EstimateConversionFlowAction) under
app/Actions; the controller should call that Action with the request user,
file_id, target_format and options and simply return its result as JSON. Ensure
the new Action encapsulates: fetching FileRecord, calling
ownershipGuard->ensureFileOwner, resolving converter with registry->find
(throwing UnsupportedConversionException as needed), validating options via
converter->validateOptions, invoking EstimateConversionCostAction->handle, and
computing has_enough_credits using creditLedger->balance; keep controller
estimate() as a thin adapter that only transforms request to Action input and
formats the Action output to the JSON response.
In `@app/Support/Api/ApiExceptionMapper.php`:
- Around line 23-81: Add a match arm for
Illuminate\Validation\ValidationException in the ApiExceptionMapper::map (the
match (true) block) that returns a MappedApiError with status 422, a succinct
code like 'validation_failed', a message (e.g. $e->getMessage() or 'Validation
failed.'), and populate details using $e->errors(); place this arm alongside the
other instanceof checks (before the default) so validation exceptions are
returned in the API {error:{code,message,details}} envelope.
In `@bootstrap/app.php`:
- Around line 33-50: When handling API exceptions inside the exceptions->render
closure, preserve rate-limit headers from ThrottleRequestsException by detecting
if $e is an instance of ThrottleRequestsException, calling $e->getHeaders(), and
copying those headers onto the outgoing JsonResponse rather than dropping them;
also use the existing ApiErrorResponseFactory (instead of manually constructing
response()->json) to build the response body from the mapped MappedApiError
returned by app(ApiExceptionMapper::class)->map($e), then attach the exception
headers (if any) to that response before returning it so Retry-After and other
throttling headers are preserved.
In `@routes/api.php`:
- Around line 23-39: Move the rate-limit middleware before API key checks and
stop implicit model binding for the sensitive routes: reorder the group
middleware to ['throttle:api-v1', 'api.key', 'api.access'], and for the routes
using implicit models (/files/{file}/targets and /conversions/{conversion} and
/conversions/{conversion}/download) disable SubstituteBindings (e.g.
->withoutMiddleware(\Illuminate\Routing\Middleware\SubstituteBindings::class))
and change those route handlers (FileController::targets,
ConversionController::show, ConversionController::download) to accept an
identifier (e.g. $fileId / $conversionId) or resolve the model inside the
controller after performing api.key/api.access authorization checks so model
resolution/authorization happens post-auth and returns 401/403 instead of 404.
In `@tests/Feature/Api/V1/ApiRateLimitTest.php`:
- Around line 19-32: The test currently hits /api/v1/health which is not behind
the throttle:api-v1 middleware, so the custom RateLimiter::for('api-v1-test',
...) is never exercised; update the test to call a real throttled route (one
defined under the protected/throttle:api-v1 group in routes/api.php) instead of
/api/v1/health or programmatically apply the throttle:api-v1 middleware to the
request so the 'api-v1-test' limiter is invoked; also stop relying on the unused
X-RateLimit-Test-Key header and ensure the request is made with the same user/id
used in RateLimiter::for so the by($user->id) key is hit and you can assert a
429 after exceeding the perMinute(2) limit.
---
Nitpick comments:
In `@app/Http/Requests/Api/V1/CreateConversionRequest.php`:
- Around line 11-17: Extract the shared validation array from
CreateConversionRequest::rules() into a single reusable place (either a base
request class like ConversionRequestBase with a protected conversionRules():
array method or a ConversionValidation trait exposing conversionRules()).
Replace both CreateConversionRequest::rules() and
EstimateConversionRequest::rules() to call and return $this->conversionRules()
(or merge with any specific rules), keeping the same keys
('file_id','target_format','options') and validation definitions; update class
declarations to extend the new base or use the trait so both requests share the
canonical rules.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 8273d6e5-62b8-4795-93de-a6649cf5484a
📒 Files selected for processing (50)
app/Http/Controllers/Api/V1/ConversionController.phpapp/Http/Controllers/Api/V1/ConverterController.phpapp/Http/Controllers/Api/V1/CreditController.phpapp/Http/Controllers/Api/V1/FileController.phpapp/Http/Middleware/AuthenticateApiKey.phpapp/Http/Middleware/EnsureApiAccessIsAllowed.phpapp/Http/Requests/Api/V1/CreateConversionRequest.phpapp/Http/Requests/Api/V1/EstimateConversionRequest.phpapp/Http/Requests/Api/V1/UploadFileRequest.phpapp/Http/Resources/Api/V1/ConversionResource.phpapp/Http/Resources/Api/V1/ConverterResource.phpapp/Http/Resources/Api/V1/FileResource.phpapp/Http/Resources/Api/V1/TargetFormatResource.phpapp/Models/ApiKey.phpapp/Models/User.phpapp/Providers/AppServiceProvider.phpapp/Services/Api/ApiKeyGenerator.phpapp/Services/Api/GeneratedApiKey.phpapp/Support/Api/ApiErrorResponseFactory.phpapp/Support/Api/ApiExceptionMapper.phpapp/Support/Api/ApiOwnershipGuard.phpapp/Support/Api/MappedApiError.phpbootstrap/app.phpdatabase/factories/ApiKeyFactory.phpdatabase/migrations/2026_06_03_132511_create_api_keys_table.phpphpunit.xmlroutes/api.phptests/Feature/Api/V1/ApiAccessFeatureGateTest.phptests/Feature/Api/V1/ApiAuthenticationTest.phptests/Feature/Api/V1/ApiErrorMappingTest.phptests/Feature/Api/V1/ApiFoundationSmokeTest.phptests/Feature/Api/V1/ApiHappyPathTest.phptests/Feature/Api/V1/ApiHealthTest.phptests/Feature/Api/V1/ApiOwnershipGuardTest.phptests/Feature/Api/V1/ApiRateLimitTest.phptests/Feature/Api/V1/ApiResourceShapeTest.phptests/Feature/Api/V1/ConversionDownloadEndpointTest.phptests/Feature/Api/V1/ConversionEstimateEndpointTest.phptests/Feature/Api/V1/ConversionStatusEndpointTest.phptests/Feature/Api/V1/ConverterSchemaEndpointTest.phptests/Feature/Api/V1/ConvertersIndexEndpointTest.phptests/Feature/Api/V1/CreateConversionEndpointTest.phptests/Feature/Api/V1/CreditBalanceEndpointTest.phptests/Feature/Api/V1/FileTargetsEndpointTest.phptests/Feature/Api/V1/FileUploadEndpointTest.phptests/Feature/Database/ApiKeysSchemaTest.phptests/Feature/Models/ApiKeyTest.phptests/Feature/Services/Api/ApiKeyGeneratorTest.phptests/Unit/Support/Api/ApiErrorResponseFactoryTest.phptests/Unit/Support/Api/ApiExceptionMapperTest.php
There was a problem hiding this comment.
4 issues found
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/Http/Requests/Api/V1/CreateConversionRequest.php">
<violation number="1" location="app/Http/Requests/Api/V1/CreateConversionRequest.php:14">
P1: Missing `exists` validation rule on `file_id`. Without `exists:uploaded_files,id`, a non-existent file ID passes validation and causes a downstream failure (ModelNotFoundException or null reference). The API returns a 404 or 500 instead of a clean 422 validation error with a field-specific message. This is a standard Laravel validation pattern for foreign key references.</violation>
</file>
<file name="app/Http/Requests/Api/V1/EstimateConversionRequest.php">
<violation number="1" location="app/Http/Requests/Api/V1/EstimateConversionRequest.php:14">
P2: `file_id` validated as `integer` but missing `exists:file_records,id` rule — non-existent file IDs pass validation, causing a ModelNotFoundException downstream instead of a clean 422 validation error.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| public function rules(): array | ||
| { | ||
| return [ | ||
| 'file_id' => ['required', 'integer'], |
There was a problem hiding this comment.
P1: Missing exists validation rule on file_id. Without exists:uploaded_files,id, a non-existent file ID passes validation and causes a downstream failure (ModelNotFoundException or null reference). The API returns a 404 or 500 instead of a clean 422 validation error with a field-specific message. This is a standard Laravel validation pattern for foreign key references.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/Http/Requests/Api/V1/CreateConversionRequest.php, line 14:
<comment>Missing `exists` validation rule on `file_id`. Without `exists:uploaded_files,id`, a non-existent file ID passes validation and causes a downstream failure (ModelNotFoundException or null reference). The API returns a 404 or 500 instead of a clean 422 validation error with a field-specific message. This is a standard Laravel validation pattern for foreign key references.</comment>
<file context>
@@ -0,0 +1,19 @@
+ public function rules(): array
+ {
+ return [
+ 'file_id' => ['required', 'integer'],
+ 'target_format' => ['required', 'string'],
+ 'options' => ['sometimes', 'array'],
</file context>
| public function rules(): array | ||
| { | ||
| return [ | ||
| 'file_id' => ['required', 'integer'], |
There was a problem hiding this comment.
P2: file_id validated as integer but missing exists:file_records,id rule — non-existent file IDs pass validation, causing a ModelNotFoundException downstream instead of a clean 422 validation error.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/Http/Requests/Api/V1/EstimateConversionRequest.php, line 14:
<comment>`file_id` validated as `integer` but missing `exists:file_records,id` rule — non-existent file IDs pass validation, causing a ModelNotFoundException downstream instead of a clean 422 validation error.</comment>
<file context>
@@ -0,0 +1,19 @@
+ public function rules(): array
+ {
+ return [
+ 'file_id' => ['required', 'integer'],
+ 'target_format' => ['required', 'string'],
+ 'options' => ['sometimes', 'array'],
</file context>
…eption, preserve throttle headers, fix model binding auth order, fix rate limit test, deduplicate request rules Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Non-existent file IDs now fail with 422 validation error instead of causing a downstream ModelNotFoundException (404) or 500. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 19 — API Foundation (CONV-291–CONV-312)
Summary
Adds the first working REST API layer for File Converter, enabling programmatic access via API keys.
What's included
/api/v1) with public health endpoint{error: {code, message, details}}shape with stable error codesapi_keystable with SHA-256 hash storage (plain token shown once, never stored)ApiKeymodel with User relation and revocation supportfc_prefixed tokens, SHA-256 hashedlast_used_at, rejects revoked keysapi_accessfeature gate — Free plan blocked, Pro/Max allowed (viaFeatureAccessService)api-v1limiter, configurable per planGET /api/v1/converters— list all convertersGET /api/v1/converters/{source}/{target}/schema— options schemaPOST /api/v1/files— upload file (usesStoreUploadedFileAction)GET /api/v1/files/{file}/targets— target formats for filePOST /api/v1/conversions/estimate— cost estimate (no job created)POST /api/v1/conversions— create conversion (usesCreateConversionJobAction)GET /api/v1/conversions/{conversion}— statusGET /api/v1/conversions/{conversion}/download— download resultGET /api/v1/credits/balance— credits balancestored_path,token_hash,options_json) never exposedApiOwnershipGuardcentralizes file/conversion ownership enforcementArchitecture
CreateConversionJobAction,StoreUploadedFileAction,CreditLedger,FeatureAccessServiceas web UIApiExceptionMapper+bootstrap/app.phprender callback handles all exception → stable error code mappingTest count: 601 total (from 519 before Phase 19)
Pre-merge checklist
composer testpasses (601 tests)composer lintpassesnpm run buildpassesphp artisan migrate:fresh --seedpassesphp artisan route:list --path=api/v1reviewed — 10 routes, no accidental public endpoints🤖 Generated with Claude Code
Summary by cubic
Introduces the first REST API for File Converter with API key auth, stable error mapping, per‑plan rate limits, and endpoints for upload and conversions. Adds
exists:files,idvalidation forfile_idto return 422 on unknown IDs. Implements Phase 19 per Linear CONV-291–CONV-312.New Features
API v1at/api/v1with public health check.api_keys(SHA‑256 hashed; revoke support;last_used_atupdates).api_accessfeature gate: Free blocked; Pro/Max allowed.api-v1, configurable per plan; 429 uses standard error shape and preserves rate limit headers.file_idusesexists:files,id.has_enough_creditswithout creating a job.stored_path,token_hash,options_json); shared request base removes duplicate rules.Migration
api_keys.FeatureAccessService.Written for commit df6b79a. Summary will update on new commits.
Summary by CodeRabbit