{"content":"# Aux4 API Server\n\nA Fastify-based HTTP server that bridges web requests to CLI commands using an AWS API Gateway-compatible request/response format. Supports REST APIs, WebSocket connections, convention-based Handlebars views, and static file serving.\n\n## Installation\n\n``bash\naux4 aux4 pkger install aux4/api\n`\n\n## Quick Start\n\n`bash\naux4 api start\n`\n\nWith a configuration file:\n\n`bash\naux4 api start --configFile config.yaml\n`\n\nStop the server:\n\n`bash\naux4 api stop\n`\n\n## Configuration\n\n`yaml\nconfig:\n port: 8080\n server:\n timeout: 30000\n maxConcurrency: 50\n maxQueue: 200\n trustProxy: true\n media: ./media\n limits:\n bodySize: 1048576\n files: 5\n fileSize: 10485760\n fieldSize: 1048576\n parts: 10\n security:\n auth:\n type: cookie\n command: aux4 auth validate\n cookie: auth_token\n redirect: /auth/signin\n rateLimit:\n max: 100\n timeWindow: 60000\n helmet: true\n allowedIPs:\n - 127.0.0.1\n cors:\n origin: \"*\"\n tls:\n key: path/to/key.pem\n cert: path/to/cert.pem\n production: false\n api:\n \"GET /contacts\":\n command: aux4 contacts list\n \"POST /contacts\":\n command: aux4 contacts create\n redirect: /contacts\n \"DELETE /contacts/{id}\":\n command: aux4 contacts delete\n redirect: /contacts\n ws:\n \"/chat\":\n routes:\n $connect: aux4 chat-connect\n $disconnect: aux4 chat-disconnect\n $default: aux4 chat-message\n`\n\n## REST API\n\nRoutes are defined in config.api with the format \"METHOD /path\". Path parameters use {name} syntax.\n\n### Command Variables\n\nRequest data is automatically injected as command variables:\n\n| Variable | Description | Example |\n|----------|-------------|---------|\n| ${params.id} | Path parameters | From {id} in route |\n| ${query.search} | Query string parameters | From ?search=... |\n| ${body.name} | Request body fields | From form/JSON body |\n| ${headers.authorization} | Request headers | Any header |\n| ${cookies.token} | Request cookies | Any cookie |\n| ${principal.email} | Authenticated user info | From auth command |\n\nUse value() for safe shell quoting: value(params.id), value(body).\n\n### Event Format (stdin)\n\nThe full AWS API Gateway-style event is also piped to the command via stdin for backward compatibility.\n\n### Response Format (stdout)\n\n| Output | Behavior |\n|--------|----------|\n| JSON with statusCode | API Gateway response (status, headers, body) |\n| JSON without statusCode | 200 with JSON body (or rendered partial if views exist) |\n| Plain text | 200 with text body |\n| data:<mimetype>;base64,<data> | Binary response with auto Content-Type |\n| Command fails (non-zero exit) | 500 with generic error message |\n\n### Route Options\n\n`yaml\n\"GET /endpoint\":\n command: aux4 my-command\n public: true # skip authentication\n timeout: 60000 # override default timeout\n stream: true # enable SSE streaming\n redirect: /other # redirect after success\n setCookie: # set cookie from response\n name: auth_token\n field: token\n clearCookie: auth_token # clear a cookie\n rateLimit: # per-route rate limiting\n max: 5\n timeWindow: 60000\n allowedIPs: # per-route IP allowlist\n - 10.0.0.1\n openapi: # OpenAPI operation overrides (see OpenAPI section)\n summary: My endpoint\n`\n\nRoutes without a command field can still use clearCookie and redirect (useful for logout):\n\n`yaml\n\"POST /auth/logout\":\n public: true\n clearCookie: auth_token\n redirect: /auth/signin\n`\n\n### Redirect\n\nAfter a successful command, redirect executes the target route's command and returns its response. If the target has a matching partial template, it renders HTML.\n\n## OpenAPI\n\nGenerate an [OpenAPI 3.0.3](https://spec.openapis.org/oas/v3.0.3) document from your config.api (plus any mounted components) with:\n\n`bash\naux4 api openapi --configFile config.yaml # JSON to stdout\naux4 api openapi --configFile config.yaml --format yaml\n`\n\nEvery \"METHOD /path\" route key becomes an OpenAPI path item and operation:\n\n- {name} path segments become required string path parameters.\n- operationId and summary are derived from the route's command (e.g. aux4 auth signin → aux4_auth_signin).\n- Operations are tagged by the first path segment (/contacts/{id} → contacts).\n- Component routes from config.components are merged under their mount paths, exactly as the running server resolves them.\n- config.info (title, version, description) populates the OpenAPI info block. Defaults: aux4 API / 1.0.0.\n\n### Parameter inference\n\nAs a best effort, the command's execute[] (looked up in the app .aux4 beside the config file) is statically scanned:\n\n- ${query.X} references → string query parameters (on GET/DELETE).\n- Body-field references → requestBody properties (on POST/PUT/PATCH). Fields are found via ${body.X} and via aliases created with set:alias=json:${body}, e.g. object(data.firstName:firstName, ...).\n\nInference is a heuristic and only sees fields the static scan can reach; everything else should be declared with the openapi: annotation below.\n\n### The openapi: annotation\n\nAny route entry may carry an openapi: block that is overlaid onto the generated operation — the reliable escape hatch for anything inference cannot derive:\n\n`yaml\nconfig:\n api:\n \"GET /search\":\n command: aux4 search run\n openapi:\n summary: Search records\n tags:\n - search\n parameters:\n - name: q\n in: query\n required: true\n schema:\n type: string\n responses:\n \"200\":\n description: Matching records\n \"400\":\n description: Bad query\n`\n\nOverlay rules:\n\n- summary, description, operationId, deprecated, tags — **replace** the generated value.\n- parameters — **merged** by in+name (annotation wins, new params appended).\n- requestBody — **replaces** the generated request body.\n- responses — **shallow-merged** onto the generated 200.\n\n## Single-Event Handling (Lambda)\n\naux4 api handle runs one **API Gateway REST v1 proxy event** through the same routing engine as api start, but **in-process** — no HTTP server, no socket. It reads the event as JSON on stdin and writes an API Gateway proxy response as JSON to stdout:\n\n`bash\necho '{ \"httpMethod\": \"GET\", \"path\": \"/contacts\", \"headers\": {}, \"body\": null, \"isBase64Encoded\": false, \"requestContext\": { \"identity\": { \"sourceIp\": \"1.2.3.4\" } } }' \\\n | aux4 api handle --configFile config.yaml\n`\n\n`json\n{\n \"statusCode\": 200,\n \"headers\": {\n \"content-type\": \"application/json\"\n },\n \"body\": \"[{\\\"id\\\":\\\"1\\\",\\\"name\\\":\\\"Alice\\\"}]\",\n \"isBase64Encoded\": false\n}\n`\n\nThis is the one-event-per-invocation entrypoint the AWS Lambda runtime calls when running a multi-route aux4/api app as a container image behind API Gateway. It reuses the same routing, authentication, rate-limiting, cookie, redirect, and response-shaping logic as the running server, so a single deployed function can serve every route in config.api.\n\n- **Event shape** — API Gateway **REST API (v1) / payload format 1.0**: httpMethod, path, flat headers, queryStringParameters / multiValueQueryStringParameters, body, isBase64Encoded, requestContext.identity.sourceIp. pathParameters are recomputed by matching path against the route patterns.\n- **Response contract** — identical to the server: a command that emits JSON with a statusCode produces that gateway response; plain JSON is wrapped as 200 application/json; data:/binary output is base64-encoded with isBase64Encoded: true; Set-Cookie is emitted (multiple cookies via multiValueHeaders).\n- **Base64 bodies** — an isBase64Encoded: true request body is decoded before routing.\n\n**Note:** streaming (stream: true, SSE), multipart uploads (multipart/form-data), and WebSocket routes require a live socket and are **not supported** through api handle — they are rejected with 501 / 415. Use aux4 api start for those transports.\n\n## Authentication\n\nConfigure authentication in security.auth:\n\n`yaml\nconfig:\n security:\n auth:\n type: cookie # cookie | bearer | apiKey | both\n command: aux4 auth validate\n cookie: auth_token # cookie name (for cookie/both types)\n redirect: /auth/signin # render this partial on 401\n`\n\n### Auth Types\n\n| Type | Description |\n|------|-------------|\n| cookie | Reads token from an httpOnly cookie |\n| bearer | Reads token from Authorization: Bearer <token> header |\n| apiKey | Static API key comparison (no command needed) |\n| both | Cookie first, bearer fallback (default) |\n| oauth | Full OAuth2/OIDC web-login flow with a signed session cookie |\n\n### Cookie Auth\n\n`yaml\nsecurity:\n auth:\n type: cookie\n command: aux4 auth validate\n cookie: auth_token\n redirect: /auth/signin\n`\n\nThe auth command receives --cookies and --headers and should return a JSON object with user info (the principal) on success, or exit with non-zero on failure:\n\n`json\n{\"email\": \"user@example.com\"}\n`\n\nThe principal is injected as --principal into route commands, accessible via ${principal.email}.\n\n### Auth Caching\n\nAuth validation results are cached in memory for 1 minute per token, avoiding repeated command execution for the same session. Configure the TTL:\n\n`yaml\nsecurity:\n auth:\n cacheTTL: 60000 # milliseconds (default: 60000)\n`\n\n### Cookie Management\n\nSet cookies from command responses:\n\n`yaml\n\"POST /auth/signin\":\n command: aux4 auth signin\n public: true\n setCookie:\n name: auth_token\n field: token\n redirect: /contacts\n`\n\nThe command returns {\"token\": \"UUID\", \"email\": \"user@example.com\"}. The API extracts the token field and sets it as an httpOnly cookie. In production mode, the Secure flag is added.\n\n### API Key Auth\n\n`yaml\nsecurity:\n auth:\n type: apiKey\n apiKey: my-secret-key\n header: X-API-Key\n`\n\nUses timing-safe comparison. No principal is set for API key auth.\n\n### Bearer Auth\n\n`yaml\nsecurity:\n auth:\n type: bearer\n command: aux4 auth validate\n`\n\nReads from Authorization: Bearer <token> header.\n\n### OAuth Web Login\n\ntype: oauth enables a full server-side OAuth2/OIDC login flow. The API handles the browser redirect dance, exchanges the authorization code through the aux4 oauth commands, and issues its own signed session cookie. Per-request authentication then verifies that session cookie in-process (no subprocess) and injects the principal into route commands. Works with any OAuth2/OIDC provider.\n\n`yaml\nsecurity:\n auth:\n type: oauth\n session:\n secret: \"secret://session-secret\" # HMAC secret for the session JWT\n cookie: auth_token # session cookie name (default: auth_token)\n ttl: 86400 # session lifetime in seconds (default: 86400)\n redirectAfterLogin: / # where to send the user after a successful login\n redirectOnError: /login # where to send the user on login failure / 401 (optional)\n providers:\n google:\n clientId: \"...\"\n clientSecret: \"secret://google-client-secret\"\n redirectUri: https://app.example.com/auth/callback\n # optional, for non-bundled providers:\n authUrl: https://accounts.google.com/o/oauth2/v2/auth\n tokenUrl: https://oauth2.googleapis.com/token\n userinfoUrl: https://openidconnect.googleapis.com/v1/userinfo\n scopes: openid,email,profile\n map: { \"id\": \"sub\" } # optional userinfo->principal field map\n`\n\nWhen type: oauth is set, the API auto-wires three routes:\n\n| Route | Behavior |\n|-------|----------|\n| GET /auth/signin?provider=<name> | Builds the provider authorize URL (PKCE S256), stashes {codeVerifier, state, provider} in a short-lived signed httpOnly cookie, and 302s to the provider. provider defaults to the sole configured provider when omitted. |\n| GET /auth/callback?code&state | Reads and clears the temp cookie, verifies state, exchanges the code for a principal, mints an HS256 session JWT (claims = principal + exp), sets it as the session cookie, and redirects to redirectAfterLogin. On any failure it redirects to redirectOnError. |\n| GET /auth/logout | Clears the session cookie and redirects (defaults to redirectOnError; override with ?redirect=/path). |\n\nThe PKCE state lives entirely in a signed, short-lived cookie — there is no server-side session store. The session cookie is an HS256 JWT signed and verified with session.secret using node's built-in crypto (no JWT library dependency). On every protected request the JWT signature and expiry are checked in-process, and the decoded claims are injected as --principal (accessible via ${principal.email}, ${principal.sub}, etc.). Missing, invalid, or expired session cookies return 401 (browsers are redirected to redirectOnError).\n\nCookies are httpOnly with SameSite=Lax, and gain the Secure flag in production mode. The provider's clientSecret and access tokens are never logged or stored in the session.\n\n**Requires** the aux4/oauth package to be installed (it provides aux4 oauth authorize-url and aux4 oauth exchange).\n\n## Convention-Based Views\n\nHandlebars templates in the views/ directory are automatically registered as GET routes:\n\n| File | Route | Layout |\n|------|-------|--------|\n| views/index.hbs | GET / | Yes |\n| views/about.hbs | GET /about | Yes |\n| views/users/{id}.hbs | GET /users/:id | Yes |\n| views/greet.p.hbs | GET /greet | No |\n\n- .hbs files render with the layout (views/layouts/main.hbs)\n- .p.hbs files render as partials (no layout wrapper)\n- views/error.p.hbs is used for error responses (not registered as a route)\n- {id} segments in filenames/directories become path parameters\n- layouts/, partials/, and i18n/ directories are skipped\n\n### Partial Auto-Rendering\n\nWhen an API command returns JSON and a matching .p.hbs partial exists, the server renders it as HTML automatically. The convention maps command names to template paths:\n\n| Command | Partial |\n|---------|---------|\n| aux4 contacts list | views/contacts/list.p.hbs |\n| aux4 contacts get | views/contacts/get.p.hbs |\n| aux4 auth signin | views/auth/signin.p.hbs |\n\nThe JSON response is available in the template as data:\n\n`handlebars\n{{#each data}}\n <tr><td>{{firstName}}</td><td>{{phone}}</td></tr>\n{{/each}}\n`\n\nClients requesting Accept: application/json receive raw JSON instead.\n\n### Handlebars Helpers\n\nBuilt-in helpers available in all templates:\n\n| Helper | Usage | Description |\n|--------|-------|-------------|\n| eq | {{#if (eq mode \"edit\")}} | Equality comparison |\n| ne | {{#if (ne status \"draft\")}} | Not-equal comparison |\n| fileSize | {{fileSize size}} | Human-readable file size (e.g., 38.8 KB) |\n\n#### Custom Helpers\n\nAdd custom helpers by creating a helpers/ directory. Each .js file becomes a helper named after the filename:\n\n`\nhelpers/\n uppercase.js → {{uppercase name}}\n formatDate.js → {{formatDate createdAt}}\n`\n\nEach file exports a single function:\n\n`js\n// helpers/uppercase.js\nmodule.exports = function(str) {\n return (str || \"\").toUpperCase();\n};\n`\n\n### SPA Catch-All\n\nWhen views/index.hbs exists, unmatched GET requests (non-API, non-static) serve the index page. This supports client-side URL routing with hx-push-url.\n\n## Error Handling\n\n### Error Redirects\n\nConfigure redirects for specific HTTP status codes:\n\n`yaml\nconfig:\n security:\n auth:\n redirect: /auth/signin # shorthand for 401 redirect\n errorRedirects:\n \"404\": /errors/not-found # renders views/errors/not-found.p.hbs\n \"500\": /errors/server # renders views/errors/server.p.hbs\n`\n\n### Error Templates\n\nError templates are checked in order:\n\n1. **Error redirect** — configured redirect path renders a partial (returns 200)\n2. **Status template** — views/401.p.hbs, views/404.p.hbs, etc.\n3. **Generic template** — views/error.p.hbs (receives statusCode, message, error)\n4. **JSON fallback** — structured JSON for Accept: application/json\n\n### Error Suppression\n\nCommand failures (non-zero exit) return a generic 500 Internal Server Error message. Internal details (stderr, stack traces) are never exposed to clients.\n\n## Static File Serving\n\nFiles in the static/ directory are served at /static/:\n\n`\nstatic/css/app.css → http://localhost:8080/static/css/app.css\n`\n\n### Uploads Directory\n\nConfigure a directory for user-uploaded files:\n\n`yaml\nconfig:\n server:\n media: ./media\n`\n\nFiles are served at /media/:\n\n`\nmedia/photo.jpg → http://localhost:8080/media/photo.jpg\n`\n\n## Production Mode\n\nEnable with --production true in config:\n\n`yaml\nconfig:\n production: true\n`\n\nProduction mode enables:\n- Secure flag on cookies (requires HTTPS)\n- Template caching without filesystem checks (faster, requires restart for changes)\n\nDevelopment mode (default):\n- No Secure flag (cookies work on HTTP localhost)\n- Template caching with mtime check (edit templates, see changes immediately)\n\n## Components\n\nComponents are reusable, mountable web modules. Each component is a self-contained package with commands, routes, and views that can be plugged into any aux4/api application.\n\n### Component Structure\n\nA component is an aux4 package with views and a config:\n\n`\ncomponents/aux4/contacts/\n .aux4 # commands\n config.yaml # routes (relative paths)\n views/\n list.p.hbs # partials\n get.p.hbs\n new.p.hbs\n edit.p.hbs\n`\n\n### Component Config\n\nThe component's config.yaml defines routes relative to its root:\n\n`yaml\napi:\n \"GET /\":\n command: aux4 contacts list\n \"GET /{id}\":\n command: aux4 contacts get\n \"POST /\":\n command: aux4 contacts create\n redirect: /\n`\n\n### Mounting Components\n\nThe host app mounts components at a path in its config.yaml:\n\n`yaml\nconfig:\n components:\n /contacts:\n package: aux4/contacts\n /chat:\n package: aux4/chat\n config:\n maxMessages: 100\n`\n\nRoutes are automatically prefixed with the mount path. GET / in the contacts component becomes GET /contacts in the host app. Redirects are prefixed too.\n\n### Installing Components\n\nInstall all components listed in config:\n\n`bash\naux4 api init\n`\n\nOr install individually:\n\n`bash\naux4 api package install aux4/contacts\n`\n\nThe init command:\n1. Downloads packages from hub.aux4.io (if not already installed)\n2. Copies component files to ./components/<scope>/<name>/\n3. Merges component command profiles into the host .aux4\n\n### Managing Components\n\n`bash\naux4 api package list # list installed components\naux4 api package uninstall aux4/contacts # remove a component\n`\n\n### Template Variables\n\nComponent partials receive {{apiPath}} and {{basePath}} for building links:\n\n- {{apiPath}} — the API route prefix (e.g., /api/contacts) for hx-get, hx-post, etc.\n- {{basePath}} — the page route prefix (e.g., /contacts) for hx-push-url and href\n\nInside {{#each}} loops, use {{../apiPath}} and {{../basePath}}.\n\n`handlebars\n<a href=\"{{../basePath}}/{{id}}\" hx-get=\"{{../apiPath}}/{{id}}\" hx-target=\"#app\">\n {{firstName}} {{lastName}}\n</a>\n`\n\n### Embedding Components\n\nUse <aux4-component> to embed components in any page. The custom element is auto-injected when components are configured.\n\n`html\n<!-- Load the full component with URL routing -->\n<aux4-component src=\"/contacts\" route=\"true\"></aux4-component>\n\n<!-- Load a specific view -->\n<aux4-component src=\"/contacts/card\" id=\"abc123\"></aux4-component>\n\n<!-- Multiple components on one page -->\n<aux4-component src=\"/contacts\" route=\"true\"></aux4-component>\n<aux4-component src=\"/chat/messages\" room=\"general\"></aux4-component>\n`\n\nAttributes:\n- src — the component path (automatically prefixed with /api/)\n- route=\"true\" — use the current page URL path instead of the static src (for SPA-style routing)\n- Any other attribute — passed as query parameters to the API\n\nThe component fetches HTML from the API, renders it, and processes HTMX attributes automatically. No JavaScript needed per component.\n\n### How It Works\n\n1. **On startup**, aux4/api reads config.components, loads each component's config.yaml, prefixes routes, and merges them into the API\n2. **Component views** are resolved from components/<scope>/<name>/views/ for partial rendering\n3. **<aux4-component>** is auto-injected as a <script> tag before </body> in all HTML pages\n4. **Authentication** flows through the host app's security.auth — components receive ${principal} automatically\n5. **On page load**, each <aux4-component> fetches its content from the API and renders it\n6. **Batch loading** — multiple components on a page are batched into a single /aux4/batch request\n7. **Placeholders** — empty components show a Bootstrap placeholder skeleton while loading\n\n### Batch Loading\n\nWhen multiple <aux4-component> elements exist on a page, their requests are batched into a single POST to /aux4/batch (10ms debounce). This reduces the number of HTTP requests from N to 1.\n\nThe batch endpoint:\n- Max 20 URLs per request\n- Only /api/ paths allowed\n- Auth flows through from the original request\n\n### Command Namespacing\n\nComponents should use the api:module profile to avoid command name collisions:\n\n`json\n{\n \"name\": \"api:module\",\n \"commands\": [{\n \"name\": \"files\",\n \"execute\": [\"profile:api:module:files\"]\n }]\n}\n`\n\nCommands are accessed via aux4 api module files list. The api:module profile is provided by aux4/api.\n\n### Binary Downloads\n\nCommands can return binary data via data URIs. By default, files are downloaded (not opened in browser):\n\n`\ndata:application/pdf;filename=report.pdf;base64,JVBERi0xLjQ...\n`\n\nTo open in browser instead of downloading, add inline:\n\n`\ndata:image/png;inline;filename=photo.png;base64,iVBORw0KGgo...\n`\n\n## WebSocket Support\n\nWebSocket routes are defined in config.ws. Each path maps lifecycle events and custom actions to commands.\n\n### Route Keys\n\n- $connect — fired when a client connects\n- $disconnect — fired when a client disconnects\n- $default — fired when no matching action is found\n- <action> — custom action matched from { \"action\": \"<action>\" } in the message body\n\n### Management API\n\n- POST /@connections/:connectionId — send a message to a specific connection\n- DELETE /@connections/:connectionId — disconnect a specific connection\n\n## SSE Streaming\n\nRoutes with stream: true use Server-Sent Events:\n\n`yaml\nconfig:\n api:\n \"GET /stream\":\n command: aux4 my-stream\n stream: true\n`\n\nEach stdout line is sent as data: <line>\\n\\n. On exit, event: done is sent.\n\n## Rate Limiting\n\nGlobal and per-route rate limiting with sliding window by client IP:\n\n`yaml\nconfig:\n security:\n rateLimit:\n max: 100\n timeWindow: 60000\n api:\n \"POST /login\":\n command: aux4 login\n rateLimit:\n max: 5\n timeWindow: 60000\n`\n\nHeaders: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.\n\n## IP Allowlist\n\n`yaml\nconfig:\n security:\n allowedIPs:\n - 127.0.0.1\n - 192.168.1.0/24\n api:\n \"GET /admin\":\n command: aux4 admin\n allowedIPs:\n - 10.0.0.1\n`\n\nPer-route allowedIPs replaces the global list. Behind a reverse proxy, set server.trustProxy: true.\n\n## HTTPS/TLS\n\n`yaml\nconfig:\n tls:\n key: path/to/key.pem\n cert: path/to/cert.pem\n`\n\n## Environment Variables\n\n- AUX4_API_PORT` — override the port (takes precedence over config file)\n"}