diff --git a/changelog/2026/jan/API_DOCUMENTATION_PLAN.md b/changelog/2026/jan/API_DOCUMENTATION_PLAN.md new file mode 100644 index 0000000..37dfe8d --- /dev/null +++ b/changelog/2026/jan/API_DOCUMENTATION_PLAN.md @@ -0,0 +1,1160 @@ +# API Documentation Plan + +**Goal:** Expose Host Hub APIs via api.host.uk.com with OpenAPI documentation, enabling SDK generation for multiple languages. + +--- + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ api.host.uk.com │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │ +│ │ Swagger │ │ ReDoc │ │ SDK Downloads │ │ +│ │ UI /docs │ │ /reference │ │ /sdks/php, /sdks/js... │ │ +│ └──────────────┘ └──────────────┘ └──────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ openapi.json │ │ +│ │ openapi.yaml │ │ +│ └────────┬─────────┘ │ +│ │ │ +└──────────────────────────────┼──────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────┐ +│ Host Hub Laravel │ +│ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ API Routes │ │ +│ │ /api/v1/workspaces, /api/v1/entitlements, /api/v1/commerce │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌───────────────┼───────────────┐ │ +│ ▼ ▼ ▼ │ +│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ +│ │ Scramble │ │ Controller │ │ Form │ │ +│ │ (auto-gen) │ │ Attributes │ │ Requests │ │ +│ └────────────┘ └────────────┘ └────────────┘ │ +│ │ +│ Note: MCP tools may call these same endpoints internally. │ +│ For agent-native interface, see mcp.host.uk.com │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Package Selection: Scramble + +**Why Scramble over L5-Swagger:** +- Zero annotations required - generates from code +- Understands Laravel conventions (Form Requests, Resources, Policies) +- Active development, Laravel-native +- Supports PHP 8 attributes for customization when needed + +```bash +composer require dedoc/scramble +``` + +--- + +## API Structure + +### Version Strategy + +``` +/api/v1/* - Current stable +/api/v2/* - Future (when breaking changes needed) +``` + +Version in URL, not headers. Simple, cacheable, debuggable. + +### Authentication + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Authentication Methods │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ 1. API Keys (Primary for SDK usage) │ +│ Authorization: Bearer │ +│ - Scoped to workspace │ +│ - Rotatable, revocable │ +│ - Rate limited per key │ +│ │ +│ 2. OAuth 2.0 (For third-party apps) │ +│ - Authorization code flow │ +│ - Scopes map to entitlements │ +│ - Managed via Passport │ +│ │ +│ 3. Session (Internal dashboard calls) │ +│ - Sanctum SPA authentication │ +│ - CSRF protected │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Endpoint Categories + +```yaml +/api/v1: + /auth: + - POST /token # Exchange credentials for token + - DELETE /token # Revoke token + - GET /me # Current user info + + /workspaces: + - GET / # List user's workspaces + - GET /{id} # Get workspace details + - POST / # Create workspace + - PATCH /{id} # Update workspace + - DELETE /{id} # Delete workspace + + /workspaces/{workspace}: + /members: + - GET / # List members + - POST / # Invite member + - DELETE /{user} # Remove member + + /entitlements: + - GET / # Get entitlement summary + - GET /check/{feature} # Check specific feature + - GET /usage # Usage breakdown + + /biolinks: + - GET / # List biolinks + - POST / # Create biolink + - GET /{id} # Get biolink + - PATCH /{id} # Update biolink + - DELETE /{id} # Delete biolink + + /links: + - GET / # List short links + - POST / # Create short link + - GET /{id} # Get link details + - PATCH /{id} # Update link + - DELETE /{id} # Delete link + - GET /{id}/stats # Link analytics + + /qr-codes: + - GET / # List QR codes + - POST / # Create QR code + - GET /{id} # Get QR code + - GET /{id}/download # Download QR image + + /commerce: + /subscriptions: + - GET / # Current subscription + - POST /change # Change plan (preview) + - POST /change/confirm # Execute plan change + - POST /cancel # Cancel subscription + + /invoices: + - GET / # List invoices + - GET /{id} # Get invoice + - GET /{id}/pdf # Download PDF + + /payment-methods: + - GET / # List payment methods + - POST / # Add payment method + - DELETE /{id} # Remove payment method + - POST /{id}/default # Set as default + + /support: + /tickets: + - GET / # List tickets + - POST / # Create ticket + - GET /{id} # Get ticket + - POST /{id}/reply # Reply to ticket + + /webhooks: + - GET / # List webhook endpoints + - POST / # Create webhook endpoint + - PATCH /{id} # Update webhook + - DELETE /{id} # Delete webhook + - GET /{id}/deliveries # View delivery history +``` + +--- + +## Implementation + +### Phase 1: Core Setup + +```php +// config/scramble.php +return [ + 'info' => [ + 'title' => 'Host Hub API', + 'description' => 'API for managing workspaces, biolinks, short links, and commerce.', + 'version' => '1.0.0', + 'contact' => [ + 'name' => 'Host UK Support', + 'url' => 'https://support.host.uk.com', + 'email' => 'api@host.uk.com', + ], + ], + + 'servers' => [ + ['url' => 'https://api.host.uk.com', 'description' => 'Production'], + ['url' => 'https://api.staging.host.uk.com', 'description' => 'Staging'], + ], + + 'api_path' => 'api/v1', + 'api_domain' => null, + + // Generate docs at this path + 'export_path' => 'api/openapi.json', +]; +``` + +### Phase 2: API Key System + +```php +// database/migrations/create_api_keys_table.php +Schema::create('api_keys', function (Blueprint $table) { + $table->id(); + $table->foreignId('workspace_id')->constrained()->cascadeOnDelete(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->string('name'); + $table->string('key', 64)->unique(); // hashed + $table->string('prefix', 8); // visible prefix for identification + $table->json('scopes')->nullable(); // ['read', 'write', 'delete'] + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->timestamps(); + $table->softDeletes(); + + $table->index(['workspace_id', 'deleted_at']); +}); + +// app/Models/ApiKey.php +class ApiKey extends Model +{ + use SoftDeletes; + + protected $casts = [ + 'scopes' => 'array', + 'last_used_at' => 'datetime', + 'expires_at' => 'datetime', + ]; + + public static function generate(Workspace $workspace, User $user, string $name): array + { + $plainKey = Str::random(48); + $prefix = Str::random(8); + + $apiKey = static::create([ + 'workspace_id' => $workspace->id, + 'user_id' => $user->id, + 'name' => $name, + 'key' => hash('sha256', $plainKey), + 'prefix' => $prefix, + 'scopes' => ['read', 'write'], + ]); + + // Return plain key only once - never stored + return [ + 'api_key' => $apiKey, + 'plain_key' => "{$prefix}_{$plainKey}", // hk_xxxxxxxx_xxxxx... + ]; + } + + public static function findByPlainKey(string $plainKey): ?static + { + [$prefix, $key] = explode('_', $plainKey, 2); + + return static::where('prefix', $prefix) + ->where('key', hash('sha256', $key)) + ->whereNull('deleted_at') + ->where(fn($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now())) + ->first(); + } + + public function recordUsage(): void + { + $this->update(['last_used_at' => now()]); + } +} +``` + +### Phase 3: Authentication Guard + +```php +// app/Http/Middleware/AuthenticateApiKey.php +class AuthenticateApiKey +{ + public function handle(Request $request, Closure $next) + { + $token = $request->bearerToken(); + + if (!$token) { + return response()->json([ + 'error' => 'unauthorized', + 'message' => 'API key required', + ], 401); + } + + // Check if it's an API key (prefixed with hk_) + if (str_starts_with($token, 'hk_')) { + $apiKey = ApiKey::findByPlainKey($token); + + if (!$apiKey) { + return response()->json([ + 'error' => 'unauthorized', + 'message' => 'Invalid API key', + ], 401); + } + + $apiKey->recordUsage(); + + // Set workspace context + $request->setUserResolver(fn() => $apiKey->user); + $request->attributes->set('api_key', $apiKey); + $request->attributes->set('workspace', $apiKey->workspace); + + return $next($request); + } + + // Fall back to Sanctum for OAuth tokens + return app(Authenticate::class)->handle($request, $next, 'sanctum'); + } +} +``` + +### Phase 4: API Controllers + +```php +// app/Http/Controllers/Api/V1/WorkspaceController.php +namespace App\Http\Controllers\Api\V1; + +use App\Http\Controllers\Controller; +use App\Http\Resources\WorkspaceResource; +use App\Http\Requests\Api\CreateWorkspaceRequest; +use App\Http\Requests\Api\UpdateWorkspaceRequest; + +/** + * @tags Workspaces + */ +class WorkspaceController extends Controller +{ + /** + * List workspaces + * + * Returns all workspaces the authenticated user has access to. + * + * @response WorkspaceResource[] + */ + public function index(Request $request) + { + $workspaces = $request->user() + ->workspaces() + ->with('subscription.package') + ->paginate(); + + return WorkspaceResource::collection($workspaces); + } + + /** + * Get workspace + * + * @response WorkspaceResource + */ + public function show(Workspace $workspace) + { + $this->authorize('view', $workspace); + + return new WorkspaceResource( + $workspace->load('subscription.package', 'members') + ); + } + + /** + * Create workspace + * + * @response 201 WorkspaceResource + */ + public function store(CreateWorkspaceRequest $request) + { + $workspace = Workspace::create([ + 'name' => $request->name, + 'slug' => $request->slug ?? Str::slug($request->name), + 'owner_id' => $request->user()->id, + ]); + + return new WorkspaceResource($workspace); + } +} +``` + +### Phase 5: API Resources (Response Schemas) + +```php +// app/Http/Resources/WorkspaceResource.php +namespace App\Http\Resources; + +use Illuminate\Http\Resources\Json\JsonResource; + +/** + * @property int $id + * @property string $name + * @property string $slug + * @property string $created_at + */ +class WorkspaceResource extends JsonResource +{ + public function toArray($request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'slug' => $this->slug, + 'created_at' => $this->created_at->toIso8601String(), + 'updated_at' => $this->updated_at->toIso8601String(), + + // Relationships + 'subscription' => new SubscriptionResource($this->whenLoaded('subscription')), + 'members' => UserResource::collection($this->whenLoaded('members')), + 'member_count' => $this->whenCounted('members'), + + // Computed + 'links' => [ + 'self' => route('api.v1.workspaces.show', $this->id), + 'biolinks' => route('api.v1.workspaces.biolinks.index', $this->id), + 'entitlements' => route('api.v1.workspaces.entitlements.index', $this->id), + ], + ]; + } +} +``` + +--- + +## SDK Generation + +### Target Registries (Maximum Developer Signal) + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ SDK Distribution Matrix │ +├──────────────┬────────────────────┬─────────────────────┬───────────────────┤ +│ Language │ Registry │ Audience │ Generator │ +├──────────────┼────────────────────┼─────────────────────┼───────────────────┤ +│ PHP │ Packagist │ Web devs │ php │ +│ TypeScript │ npm │ Frontend/Node │ typescript-fetch │ +│ Python │ PyPI │ Scripts/Data/AI │ python │ +│ Go │ pkg.go.dev │ DevOps/Cloud/CLI │ go │ +│ Rust │ crates.io │ Systems/Performance │ rust │ +│ Ruby │ RubyGems │ Rails/Web │ ruby │ +│ Java │ Maven Central │ Enterprise │ java │ +│ C#/.NET │ NuGet │ Enterprise/Windows │ csharp-netcore │ +│ Kotlin │ Maven Central │ Android/Modern Java │ kotlin │ +│ Swift │ Swift PM │ Apple ecosystem │ swift5 │ +│ Dart │ pub.dev │ Flutter/Mobile │ dart │ +├──────────────┴────────────────────┴─────────────────────┴───────────────────┤ +│ Infrastructure / DevOps │ +├──────────────┬────────────────────┬─────────────────────┬───────────────────┤ +│ Ansible │ Ansible Galaxy │ SysAdmins │ SwaggerToAnsible │ +│ Terraform │ Terraform Registry │ IaC DevOps │ Custom provider │ +│ Pulumi │ Pulumi Registry │ Modern IaC │ Pulumi bridge │ +│ CLI (Go) │ Homebrew/apt/yum │ Terminal users │ go + goreleaser │ +│ GitHub Action│ GH Marketplace │ CI/CD users │ Custom │ +└──────────────┴────────────────────┴─────────────────────┴───────────────────┘ +``` + +### Priority Tiers + +**Tier 1 - Core (Day 1):** +- PHP, TypeScript, Python (your primary audiences) + +**Tier 2 - High Signal (Week 1):** +- Go (DevOps love, plus builds CLI binary) +- Rust (growing fast, crates.io visibility) +- Ruby (still strong web community) + +**Tier 3 - Enterprise (Week 2):** +- Java, Kotlin (Maven Central = enterprise discovery) +- C#/.NET (NuGet = Microsoft ecosystem) + +**Tier 4 - Mobile/Niche:** +- Swift (Apple developers) +- Dart (Flutter mobile) + +**Tier 5 - Infrastructure:** +- Ansible (you have SwaggerToAnsible) +- Terraform Provider (huge IaC market) +- CLI binary via Homebrew +- GitHub Action + +### OpenAPI Generator Configs + +```yaml +# sdk-config/php.yaml +generatorName: php +outputDir: ./sdks/php +additionalProperties: + packageName: HostHubSdk + invokerPackage: HostUK\HostHub + apiPackage: Api + modelPackage: Model + composerVendorName: host-uk + composerProjectName: hosthub-sdk + gitUserId: host-uk + gitRepoId: hosthub-php-sdk + +# sdk-config/typescript.yaml +generatorName: typescript-fetch +outputDir: ./sdks/typescript +additionalProperties: + npmName: "@host-uk/hosthub-sdk" + npmVersion: "1.0.0" + supportsES6: true + typescriptThreePlus: true + +# sdk-config/python.yaml +generatorName: python +outputDir: ./sdks/python +additionalProperties: + packageName: hosthub + projectName: hosthub-sdk + packageVersion: "1.0.0" + +# sdk-config/go.yaml +generatorName: go +outputDir: ./sdks/go +additionalProperties: + packageName: hosthub + moduleName: github.com/host-uk/hosthub-go + +# sdk-config/rust.yaml +generatorName: rust +outputDir: ./sdks/rust +additionalProperties: + packageName: hosthub + packageVersion: "1.0.0" + +# sdk-config/ruby.yaml +generatorName: ruby +outputDir: ./sdks/ruby +additionalProperties: + gemName: hosthub + gemVersion: "1.0.0" + gemAuthor: "Host UK" + gemHomepage: "https://api.host.uk.com" + +# sdk-config/java.yaml +generatorName: java +outputDir: ./sdks/java +additionalProperties: + groupId: com.hostuk + artifactId: hosthub-sdk + artifactVersion: "1.0.0" + invokerPackage: com.hostuk.hosthub + apiPackage: com.hostuk.hosthub.api + modelPackage: com.hostuk.hosthub.model + +# sdk-config/csharp.yaml +generatorName: csharp-netcore +outputDir: ./sdks/csharp +additionalProperties: + packageName: HostUK.HostHub + packageVersion: "1.0.0" + targetFramework: "net6.0" + +# sdk-config/kotlin.yaml +generatorName: kotlin +outputDir: ./sdks/kotlin +additionalProperties: + groupId: com.hostuk + artifactId: hosthub-sdk + packageName: com.hostuk.hosthub + +# sdk-config/swift.yaml +generatorName: swift5 +outputDir: ./sdks/swift +additionalProperties: + projectName: HostHubSDK + podVersion: "1.0.0" + +# sdk-config/dart.yaml +generatorName: dart +outputDir: ./sdks/dart +additionalProperties: + pubName: hosthub + pubVersion: "1.0.0" + pubAuthor: "Host UK" +``` + +### CLI Tool (Go-based) + +```go +// cmd/hosthub/main.go +// Built from Go SDK, distributed via: +// - Homebrew (macOS/Linux) +// - apt repository (Debian/Ubuntu) +// - yum repository (RHEL/CentOS) +// - Scoop (Windows) +// - Direct binary downloads + +// Example usage: +// $ hosthub workspaces list +// $ hosthub links create --url https://example.com +// $ hosthub biolinks get bl_123 +``` + +```yaml +# .goreleaser.yml +builds: + - main: ./cmd/hosthub + binary: hosthub + goos: [linux, darwin, windows] + goarch: [amd64, arm64] + +brews: + - tap: + owner: host-uk + name: homebrew-tap + name: hosthub + homepage: https://api.host.uk.com + +nfpms: + - package_name: hosthub + vendor: Host UK + formats: [deb, rpm] +``` + +### Terraform Provider + +``` +terraform-provider-hosthub/ +├── internal/ +│ └── provider/ +│ ├── provider.go +│ ├── workspace_resource.go +│ ├── biolink_resource.go +│ └── link_resource.go +└── examples/ + └── main.tf +``` + +```hcl +# Example Terraform usage +terraform { + required_providers { + hosthub = { + source = "host-uk/hosthub" + version = "~> 1.0" + } + } +} + +provider "hosthub" { + api_key = var.hosthub_api_key +} + +resource "hosthub_workspace" "main" { + name = "My Workspace" + slug = "my-workspace" +} + +resource "hosthub_biolink" "landing" { + workspace_id = hosthub_workspace.main.id + name = "Landing Page" + url = "landing" +} +``` + +### GitHub Action + +```yaml +# action.yml +name: 'Host Hub Action' +description: 'Manage Host Hub resources in CI/CD' +branding: + icon: 'link' + color: 'blue' + +inputs: + api-key: + description: 'Host Hub API key' + required: true + command: + description: 'Command to run (e.g., "links create")' + required: true + +runs: + using: 'node16' + main: 'dist/index.js' +``` + +```yaml +# Usage in workflow +- uses: host-uk/hosthub-action@v1 + with: + api-key: ${{ secrets.HOSTHUB_API_KEY }} + command: links create --url ${{ github.event.deployment.url }} +``` + +### Package Registry Summary + +| Registry | Package Name | Install Command | +|----------|--------------|-----------------| +| Packagist | `host-uk/hosthub-sdk` | `composer require host-uk/hosthub-sdk` | +| npm | `@host-uk/hosthub-sdk` | `npm install @host-uk/hosthub-sdk` | +| PyPI | `hosthub` | `pip install hosthub` | +| pkg.go.dev | `github.com/host-uk/hosthub-go` | `go get github.com/host-uk/hosthub-go` | +| crates.io | `hosthub` | `cargo add hosthub` | +| RubyGems | `hosthub` | `gem install hosthub` | +| Maven | `com.hostuk:hosthub-sdk` | Maven/Gradle dependency | +| NuGet | `HostUK.HostHub` | `dotnet add package HostUK.HostHub` | +| pub.dev | `hosthub` | `dart pub add hosthub` | +| Swift PM | `HostHubSDK` | Package.swift dependency | +| Ansible Galaxy | `hostuk.hosthub` | `ansible-galaxy collection install hostuk.hosthub` | +| Terraform | `host-uk/hosthub` | `required_providers` block | +| Homebrew | `hosthub` | `brew install host-uk/tap/hosthub` | +| GitHub Actions | `host-uk/hosthub-action` | `uses: host-uk/hosthub-action@v1` | + +### Generation Script + +```bash +#!/bin/bash +# scripts/generate-sdks.sh + +OPENAPI_FILE="storage/app/openapi.json" +OUTPUT_DIR="./sdks" + +# Export fresh OpenAPI spec +php artisan scramble:export + +# Array of all SDK configs +SDKS=( + "php" + "typescript" + "python" + "go" + "rust" + "ruby" + "java" + "csharp" + "kotlin" + "swift" + "dart" +) + +# Generate all SDKs +for sdk in "${SDKS[@]}"; do + echo "Generating $sdk SDK..." + openapi-generator-cli generate \ + -i $OPENAPI_FILE \ + -c sdk-config/$sdk.yaml \ + -o $OUTPUT_DIR/$sdk +done + +echo "All SDKs generated in $OUTPUT_DIR" +``` + +### Publish Script + +```bash +#!/bin/bash +# scripts/publish-sdks.sh + +VERSION=$1 + +if [ -z "$VERSION" ]; then + echo "Usage: ./publish-sdks.sh " + exit 1 +fi + +# PHP → Packagist (auto via GitHub webhook) +echo "PHP: Push to host-uk/hosthub-php-sdk triggers Packagist" + +# TypeScript → npm +cd sdks/typescript +npm version $VERSION +npm publish --access public +cd ../.. + +# Python → PyPI +cd sdks/python +python -m build +twine upload dist/* +cd ../.. + +# Go → pkg.go.dev (auto on push + tag) +cd sdks/go +git tag v$VERSION +git push origin v$VERSION +cd ../.. + +# Rust → crates.io +cd sdks/rust +cargo publish +cd ../.. + +# Ruby → RubyGems +cd sdks/ruby +gem build hosthub.gemspec +gem push hosthub-$VERSION.gem +cd ../.. + +# Java/Kotlin → Maven Central +cd sdks/java +./gradlew publish +cd ../.. + +# C# → NuGet +cd sdks/csharp +dotnet pack -c Release +dotnet nuget push **/*.nupkg --source https://api.nuget.org/v3/index.json +cd ../.. + +# Dart → pub.dev +cd sdks/dart +dart pub publish +cd ../.. + +echo "All SDKs published at version $VERSION" +``` + +### SDK Usage Examples + +```php +// PHP SDK +use HostUK\HostHub\Api\WorkspacesApi; +use HostUK\HostHub\Configuration; + +$config = Configuration::getDefaultConfiguration() + ->setApiKey('Authorization', 'Bearer hk_xxxxxxxx_xxxxx...'); + +$api = new WorkspacesApi(config: $config); + +$workspaces = $api->listWorkspaces(); +foreach ($workspaces as $workspace) { + echo $workspace->getName(); +} +``` + +```typescript +// TypeScript SDK +import { WorkspacesApi, Configuration } from '@host-uk/hosthub-sdk'; + +const config = new Configuration({ + apiKey: 'Bearer hk_xxxxxxxx_xxxxx...', +}); + +const api = new WorkspacesApi(config); + +const workspaces = await api.listWorkspaces(); +workspaces.forEach(ws => console.log(ws.name)); +``` + +```python +# Python SDK +from hosthub import Configuration, WorkspacesApi + +config = Configuration() +config.api_key['Authorization'] = 'Bearer hk_xxxxxxxx_xxxxx...' + +api = WorkspacesApi(configuration=config) + +workspaces = api.list_workspaces() +for ws in workspaces: + print(ws.name) +``` + +--- + +## Documentation Site (api.host.uk.com) + +### Site Structure + +``` +api.host.uk.com/ +├── / # Landing page (getting started) +├── /docs # Swagger UI (interactive) +├── /reference # ReDoc (beautiful reference) +├── /openapi.json # Raw OpenAPI spec +├── /openapi.yaml # YAML version +├── /sdks # SDK download page +│ ├── /php # PHP SDK + docs +│ ├── /typescript # TypeScript SDK + docs +│ └── /python # Python SDK + docs +├── /guides # Integration guides +│ ├── /authentication # Auth guide +│ ├── /webhooks # Webhook setup +│ └── /rate-limits # Rate limiting info +└── /changelog # API changelog +``` + +### Landing Page Content + +```markdown +# Host Hub API + +Build integrations with Host Hub using our REST API. + +## Quick Start + +1. **Get an API Key** + Navigate to Settings → API Keys in your workspace dashboard. + +2. **Make Your First Request** + ```bash + curl https://api.host.uk.com/v1/workspaces \ + -H "Authorization: Bearer hk_your_api_key" + ``` + +3. **Use an SDK** + - [PHP SDK](/sdks/php) + - [TypeScript SDK](/sdks/typescript) + - [Python SDK](/sdks/python) + +## Looking for Agent Integration? + +For AI agent and MCP server integration, visit [mcp.host.uk.com](https://mcp.host.uk.com). +``` + +### Deployment + +```yaml +# Separate static site or Laravel routes +# Option A: Static site (Astro/Next.js) pulling openapi.json +# Option B: Laravel routes serving documentation + +# routes/api-docs.php (if using Laravel) +Route::domain('api.host.uk.com')->group(function () { + Route::get('/', [ApiDocsController::class, 'landing']); + Route::get('/docs', [ApiDocsController::class, 'swagger']); + Route::get('/reference', [ApiDocsController::class, 'redoc']); + Route::get('/openapi.json', [ApiDocsController::class, 'openApiJson']); + Route::get('/openapi.yaml', [ApiDocsController::class, 'openApiYaml']); + Route::get('/sdks', [ApiDocsController::class, 'sdks']); + Route::get('/sdks/{language}', [ApiDocsController::class, 'sdkDownload']); +}); +``` + +--- + +## Rate Limiting + +```php +// config/api.php +return [ + 'rate_limits' => [ + 'default' => [ + 'requests' => 1000, + 'per_minutes' => 60, + ], + 'authenticated' => [ + 'requests' => 5000, + 'per_minutes' => 60, + ], + 'by_tier' => [ + 'starter' => ['requests' => 1000, 'per_minutes' => 60], + 'pro' => ['requests' => 5000, 'per_minutes' => 60], + 'agency' => ['requests' => 20000, 'per_minutes' => 60], + ], + ], +]; + +// Rate limit headers included in all responses: +// X-RateLimit-Limit: 5000 +// X-RateLimit-Remaining: 4987 +// X-RateLimit-Reset: 1704067200 +``` + +--- + +## Error Responses + +```json +// Standard error format +{ + "error": "validation_error", + "message": "The given data was invalid.", + "details": { + "name": ["The name field is required."], + "slug": ["The slug has already been taken."] + } +} + +// Error codes +{ + "error": "unauthorized", // 401 - Missing/invalid auth + "error": "forbidden", // 403 - No permission + "error": "not_found", // 404 - Resource not found + "error": "validation_error", // 422 - Invalid input + "error": "rate_limit_exceeded", // 429 - Too many requests + "error": "internal_error", // 500 - Server error + "error": "entitlement_exceeded", // 403 - Plan limit reached +} +``` + +--- + +## Webhooks + +```php +// Webhook events +'workspace.created' +'workspace.updated' +'workspace.deleted' +'subscription.created' +'subscription.updated' +'subscription.cancelled' +'invoice.created' +'invoice.paid' +'invoice.failed' +'biolink.created' +'biolink.updated' +'link.created' +'link.clicked' // High volume - opt-in only + +// Webhook payload +{ + "id": "evt_123456", + "type": "subscription.updated", + "created_at": "2024-12-31T12:00:00Z", + "data": { + "subscription": { ... } + }, + "workspace_id": 123 +} + +// Signature verification +$signature = hash_hmac('sha256', $payload, $webhookSecret); +// Compare with X-HostHub-Signature header +``` + +--- + +## MCP Intersection Note + +The REST API and MCP tools share the same underlying services: + +``` +┌──────────────────┐ ┌──────────────────┐ +│ REST API │ │ MCP Tools │ +│ api.host.uk.com │ │ mcp.host.uk.com │ +└────────┬─────────┘ └────────┬─────────┘ + │ │ + └──────────┬─────────────┘ + ▼ + ┌──────────────────┐ + │ Service Layer │ + │ (Laravel) │ + └──────────────────┘ +``` + +PHP applications may call REST endpoints directly. AI agents should prefer the MCP interface at [mcp.host.uk.com](https://mcp.host.uk.com) for: +- Structured tool calling +- Entitlement-aware server discovery +- Agent-optimized response formats + +--- + +## Implementation Phases + +### Phase 1: Foundation +- [ ] Install Scramble +- [ ] Configure API versioning +- [ ] Create API key model and migration +- [ ] Implement API key authentication guard +- [ ] Set up rate limiting + +### Phase 2: Core Endpoints +- [ ] Auth endpoints (token exchange) +- [ ] Workspace CRUD +- [ ] Entitlement checking +- [ ] Member management + +### Phase 3: Product Endpoints +- [ ] Biolinks CRUD +- [ ] Short links CRUD +- [ ] QR codes CRUD +- [ ] Analytics endpoints + +### Phase 4: Commerce Endpoints +- [ ] Subscription management +- [ ] Invoice listing +- [ ] Payment methods + +### Phase 5: Documentation Site +- [ ] Deploy api.host.uk.com +- [ ] Swagger UI integration +- [ ] ReDoc integration +- [ ] SDK download pages + +### Phase 6: SDK Generation +- [ ] Set up OpenAPI Generator +- [ ] Generate PHP SDK +- [ ] Generate TypeScript SDK +- [ ] Generate Python SDK +- [ ] Publish to package registries + +### Phase 7: Webhooks +- [ ] Webhook endpoint management +- [ ] Event dispatching +- [ ] Delivery tracking +- [ ] Retry logic + +--- + +## Files to Create/Modify + +### New Files +``` +app/ +├── Http/ +│ ├── Controllers/Api/V1/ +│ │ ├── AuthController.php +│ │ ├── WorkspaceController.php +│ │ ├── EntitlementController.php +│ │ ├── BiolinkController.php +│ │ ├── LinkController.php +│ │ ├── QrCodeController.php +│ │ ├── SubscriptionController.php +│ │ ├── InvoiceController.php +│ │ └── WebhookController.php +│ ├── Middleware/ +│ │ └── AuthenticateApiKey.php +│ ├── Resources/ +│ │ ├── WorkspaceResource.php +│ │ ├── BiolinkResource.php +│ │ ├── LinkResource.php +│ │ └── ... (all API resources) +│ └── Requests/Api/ +│ └── ... (all form requests) +├── Models/ +│ ├── ApiKey.php +│ └── WebhookEndpoint.php +config/ +├── scramble.php +└── api.php +routes/ +├── api_v1.php +└── api-docs.php +database/migrations/ +├── create_api_keys_table.php +└── create_webhook_endpoints_table.php +scripts/ +└── generate-sdks.sh +sdk-config/ +├── php.yaml +├── typescript.yaml +└── python.yaml +``` + +### Modified Files +- `routes/api.php` - Include versioned routes +- `app/Http/Kernel.php` - Register API middleware +- `composer.json` - Add Scramble dependency + +--- + +*Created: 2024-12-31* +*Status: Planning* +*Destination: api.host.uk.com* diff --git a/changelog/2026/jan/code-review.md b/changelog/2026/jan/code-review.md new file mode 100644 index 0000000..cb21636 --- /dev/null +++ b/changelog/2026/jan/code-review.md @@ -0,0 +1,133 @@ +# Api Module Review + +**Updated:** 2026-01-21 - All recommended improvements verified and documented + +## Overview + +The Api module provides the core API infrastructure for the Host Hub platform, including: +- API key management with secure hashing (SHA-256) and rotation with grace periods +- Webhook system for event-driven integrations with HMAC signatures +- API usage tracking and analytics with daily aggregation +- Rate limiting with tier-based limits tied to entitlements +- Multiple authentication strategies (API keys, Sanctum/OAuth) +- Controllers for entitlements, SEO reports, unified pixel tracking, MCP HTTP bridge, and workspace management + +## Production Readiness Score: 92/100 (was 85/100 - Wave 2 improvements applied 2026-01-21) + +The module has solid foundations with good security practices, comprehensive test coverage for core features, and well-structured code. P1 issues fixed in Wave 1. + +## Critical Issues (Must Fix) + +- [x] **ApiUsageDaily now database-portable**: `recordFromUsage()` now uses Laravel's `upsert()` method followed by atomic `DB::raw()` increment updates. Works with MySQL, PostgreSQL, and SQLite. + +- [x] **McpApiController resource returns 501**: The `resource()` method now returns HTTP 501 Not Implemented with clear error message instead of misleading stub. + +- [x] **MCP server command mapping is configurable**: Moved to `config('api.mcp.server_commands')` - fully configurable via config file. Verified: config defines 6 server mappings (hosthub-agent, socialhost, biohost, commerce, supporthost, upstream). Controller uses `config('api.mcp.server_commands', [])` for lookup. + +- [x] **EntitlementApiController store() verified protected**: Route uses `middleware('commerce.api')` requiring Bearer token matching `config('services.commerce.api_secret')` - proper server-to-server auth. + +- [x] **Grace period cleanup scheduled**: Created `api:cleanup-grace-periods` command with `--dry-run` support. Registered hourly in scheduler with `withoutOverlapping()` and `runInBackground()`. + +## Recommended Improvements + +- [x] **Add request validation to MCP callTool**: Now validates arguments against tool's JSON Schema inputSchema. Verified: `validateToolArguments()` method checks required properties, type validation (string/integer/number/boolean/array/object/null), enum values, string min/max length, numeric min/max values. Returns 422 with detailed validation errors array. + +- [ ] **Add IP whitelist option for API keys**: Some users may want to restrict API keys to specific IPs. + +- [x] **Add webhook delivery queue configuration**: Added `config('api.webhooks.queue')` and `config('api.webhooks.queue_connection')`. Verified: Config defaults to 'webhooks' queue via `env('WEBHOOK_QUEUE', 'webhooks')`. `DeliverWebhookJob` constructor reads both settings and applies to job dispatch. + +- [ ] **Add API key permission audit logging**: Currently logs creation/rotation/revoke but not permission changes in a structured audit trail. + +- [x] **Add rate limit headers consistently**: Added `X-RateLimit-Reset` and `Retry-After` to CORS exposed headers for cross-origin visibility. + +- [x] **Add CORS configuration**: Created `PublicApiCors` middleware for public endpoints (pixel). Verified: Handles OPTIONS preflight with 204 response. Sets `Access-Control-Allow-Origin` to request origin, exposes rate limit headers, adds `Vary: Origin` for proper caching. Applied to pixel routes. + +- [x] **SeoReportController URL matching improved**: Now uses multiple strategies (full path, basename, normalised slug, common prefix stripping) to match URLs reliably. Verified: `findContentByUrl()` implements 4 strategies: full path match, basename match, extension-stripped match (.html/.php/.aspx/.jsp), and common prefix stripping (blog/posts/articles/pages/help/docs). + +- [x] **UnifiedPixelController rate limit by pixel_key**: Added `RateLimitPixelKey` middleware with configurable limits via `config('api.rate_limits.pixel_key')`. Verified: Middleware extracts pixel_key from request, sanitises to alphanumeric (max 64 chars), uses Laravel RateLimiter with configurable requests/minutes (default 10,000/min). Returns 429 with custom headers (X-PixelKey-RateLimit-*). + +- [ ] **Add dead letter queue for permanently failed webhooks**: Failed deliveries (after max retries) are just marked as failed. Consider a DLQ for manual inspection. + +- [x] **WorkspaceController switch method optimised**: Refactored to use direct DB queries in a transaction - clearer and more efficient. Verified: Uses `DB::transaction()` with two atomic `DB::table()` updates - first clears all hub workspace defaults for user, then sets new default. No N+1 queries. + +## Missing Features (Future) + +- [ ] **API versioning support**: Config has `api.version` but no version-based routing or deprecation handling. + +- [ ] **API key IP restrictions**: Model has no IP whitelist field. + +- [ ] **Webhook payload templating**: All webhooks send full event data; no way to customise payload shape. + +- [ ] **Webhook secret rotation grace period**: Unlike API keys, webhook secrets rotate immediately with no grace period. + +- [ ] **API usage alerts/notifications**: No alerting when usage approaches limits or unusual patterns detected. + +- [ ] **Usage export**: No endpoint to export usage data as CSV/JSON. + +- [ ] **Read-only API key scopes enforcement on routes**: Scope middleware exists but is not applied to routes - all authenticated routes allow any scope. + +- [ ] **Social API controllers tests**: Controllers in `Controllers/Social/` have no corresponding tests. + +- [ ] **ProductApiController implementation**: File exists but not reviewed - verify if stub or complete. + +## Test Coverage Assessment + +**Strong coverage for:** +- API key creation, authentication, expiry, scopes (ApiKeyTest.php - 33 tests) +- API key rotation and grace periods (ApiKeyRotationTest.php - 14 tests) +- Webhook delivery and retry logic (WebhookDeliveryTest.php - 14 tests) +- API usage tracking and aggregation (ApiUsageTest.php - 16 tests) + +**Missing coverage for:** +- `McpApiController` (no tests) +- `UnifiedPixelController` (no tests) +- `EntitlementApiController` (no tests) +- `SeoReportController` (no tests) +- `WorkspaceController` (no tests) +- `CommerceApiAuth` middleware (no tests) +- Social API controllers (no tests) +- HTTP integration tests for V1 controllers (partial - only auth rejection tested) + +**Estimated coverage: 60-65%** of critical paths, but gaps in controller-level integration tests. + +## Security Concerns + +1. **API keys are properly hashed** - Uses SHA-256, keys never stored in plain text. Good. + +2. **Webhook signatures use HMAC-SHA256** - Proper implementation with timing-safe comparison in `hash_equals()`. Good. + +3. **CommerceApiAuth uses timing-safe comparison** - Uses `hash_equals()` for token comparison. Good. + +4. **No SQL injection risks** - All queries use Eloquent/query builder with parameterised inputs. Good. + +5. **User agent truncated to prevent storage overflow** - ApiUsage truncates to 500 chars. Good. + +6. **Response body size limited in webhook deliveries** - Limited to 10,000 chars. Good. + +7. **POTENTIAL: MCP tool execution via proc_open** - The `executeToolViaArtisan()` spawns a PHP process. While arguments are JSON-encoded (not shell-escaped), ensure MCP server commands cannot be exploited. Currently appears safe as server/tool names are validated against known registries. + +8. **VERIFY: EntitlementApiController creates users** - Creates users with random passwords. If exposed publicly, could be an account creation vector. Verify route protection. + +9. **No request signing for webhook validation on incoming webhooks** - The module sends signed webhooks but the SEO report receiver doesn't verify signatures from external sources. + +## Notes + +1. **Well-structured module** - Clean separation between models, services, controllers, and middleware. Good use of concerns/traits for shared functionality. + +2. **Good documentation** - PHPDoc comments throughout, clear method naming, helpful inline comments. + +3. **Proper use of transactions** - Webhook dispatch and delivery use DB transactions with `afterCommit()` for job dispatch. + +4. **Exponential backoff well implemented** - Webhook retry delays are sensible (1m, 5m, 30m, 2h, 24h). + +5. **Factory support** - ApiKey has a factory with helpers for creating test keys with known plain text. + +6. **Config externalised** - Rate limits, key limits, webhook settings all in config file. + +7. **Code snippet generator is thorough** - Supports 11 languages for API docs. Nice feature. + +8. **Soft deletes used appropriately** - API keys and webhooks use soft deletes for audit trail. + +9. **ApiUsageDaily atomic upsert** - Uses raw SQL for atomic increment operations to prevent race conditions. Smart but MySQL-specific. + +10. **Middleware aliases registered via events** - Uses `ApiRoutesRegistering` and `ConsoleBooting` events for lazy middleware registration. Follows the modular architecture pattern.