{"_id":"@parke.dev/arbiter","_rev":"2-13827176a24f7193b16b57c4c32b15c6","name":"@parke.dev/arbiter","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.1":{"name":"@parke.dev/arbiter","version":"1.0.1","keywords":["api","proxy","openapi","har","documentation"],"author":{"name":"Luke Hagar"},"license":"ISC","_id":"@parke.dev/arbiter@1.0.1","maintainers":[{"name":"lukehagar","email":"lukeslakemail@gmail.com"}],"homepage":"https://github.com/LukeHagar/arbiter#readme","bugs":{"url":"https://github.com/LukeHagar/arbiter/issues"},"bin":{"arbiter":"dist/src/cli.js"},"dist":{"shasum":"3e9bb60a175fc8a3ad51a65f442b7c3a4eac4b73","tarball":"https://registry.npmjs.org/@parke.dev/arbiter/-/arbiter-1.0.1.tgz","fileCount":139,"integrity":"sha512-CcgsXEVjf496Gk34zea3pV4w1UwPXBBZmyJD1ChQGDMkZemdG5Tdj5hbd7+XYKnHsu/L5iZhCWqfxWEQEReTcA==","signatures":[{"sig":"MEQCICkLGzf3rKoP0EvlQkw1vjwBUta9tPBBkUqVnR7nVS6CAiAcc5JtRO8kYq7f9AFVcf5hXjH4114tD+xpM000HzexnQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1119165},"main":"dist/src/server.js","type":"module","types":"./dist/src/server.d.ts","exports":{".":{"types":"./dist/src/server.d.ts","import":"./dist/src/server.js"},"./store":{"types":"./dist/src/store/openApiStore.d.ts","import":"./dist/src/store/openApiStore.js"},"./middleware":{"types":"./dist/src/middleware/harRecorder.d.ts","import":"./dist/src/middleware/harRecorder.js"}},"gitHead":"14e16ed1a98f3124a716631dd9ecb793cf5817a1","scripts":{"cli":"ts-node-dev --respawn --transpile-only src/cli.ts","dev":"ts-node-dev --respawn --transpile-only src/cli.ts","lint":"eslint . --ext .ts","test":"vitest run","build":"tsc","start":"node dist/src/cli.js","format":"prettier --write \"src/**/*.ts\" \"integration/**/*.ts\"","test:ci":"vitest run","lint:fix":"eslint . --ext .ts --fix","test:unit":"vitest src/**/__tests__/*.test.ts","format:check":"prettier --check \"src/**/*.ts\" \"integration/**/*.ts\"","test:integration":"vitest integration/__tests__/*.test.ts"},"_npmUser":{"name":"lukehagar","email":"lukeslakemail@gmail.com"},"repository":{"url":"git+https://github.com/LukeHagar/arbiter.git","type":"git"},"_npmVersion":"11.17.0","description":"API proxy with OpenAPI generation and HAR export capabilities","directories":{},"_nodeVersion":"26.4.0","dependencies":{"cors":"^2.8.5","hono":"^4.9.1","yaml":"^2.8.1","chalk":"^5.5.0","express":"^5.1.0","commander":"^14.0.0","http-proxy":"^1.18.1","body-parser":"^2.2.0","serve-static":"^2.2.0","better-sqlite3":"^12.2.0","swagger-ui-express":"^5.0.1","@scalar/api-reference":"^1.34.2","http-proxy-middleware":"^3.0.5"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.33.0","vitest":"^3.2.4","globals":"^16.3.0","ts-node":"^10.9.2","prettier":"^3.6.2","@eslint/js":"^9.33.0","node-fetch":"^3.3.2","typescript":"^5.9.2","@types/cors":"^2.8.19","@types/node":"^24.2.1","ts-node-dev":"^2.0.0","openapi-types":"^12.1.3","@types/express":"^5.0.3","@eslint/eslintrc":"^3.3.1","@hono/node-server":"^1.18.2","@types/http-proxy":"^1.17.16","@types/node-fetch":"^2.6.13","@types/body-parser":"^1.19.6","@types/serve-static":"^1.15.8","@vitest/coverage-v8":"^3.2.4","@types/better-sqlite3":"^7.6.13","eslint-config-prettier":"^10.1.8","@types/swagger-ui-express":"^4.1.8","@typescript-eslint/parser":"^8.39.1","@typescript-eslint/eslint-plugin":"^8.39.1"},"_npmOperationalInternal":{"tmp":"tmp/arbiter_1.0.1_1784150587019_0.21586542142459497","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@parke.dev/arbiter","version":"1.1.0","description":"API proxy with OpenAPI generation and HAR export capabilities","main":"dist/src/server.js","bin":{"arbiter":"dist/src/cli.js"},"type":"module","repository":{"type":"git","url":"git+https://github.com/LukasParke/arbiter.git"},"author":{"name":"Luke Hagar"},"license":"MIT","bugs":{"url":"https://github.com/LukasParke/arbiter/issues"},"homepage":"https://github.com/LukasParke/arbiter#readme","exports":{".":{"import":"./dist/src/server.js","types":"./dist/src/server.d.ts"},"./middleware":{"import":"./dist/src/middleware/harRecorder.js","types":"./dist/src/middleware/harRecorder.d.ts"},"./store":{"import":"./dist/src/store/openApiStore.js","types":"./dist/src/store/openApiStore.d.ts"},"./capture":{"import":"./dist/src/capture/index.js","types":"./dist/src/capture/index.d.ts"},"./replay":{"import":"./dist/src/replay/index.js","types":"./dist/src/replay/index.d.ts"},"./validation":{"import":"./dist/src/validation/index.js","types":"./dist/src/validation/index.d.ts"},"./bundle":{"import":"./dist/src/bundle/index.js","types":"./dist/src/bundle/index.d.ts"},"./gateway":{"import":"./dist/src/gateway/index.js","types":"./dist/src/gateway/index.d.ts"}},"scripts":{"build":"tsc","start":"node dist/src/cli.js","dev":"ts-node-dev --respawn --transpile-only src/cli.ts","cli":"ts-node-dev --respawn --transpile-only src/cli.ts","test":"vitest run","test:unit":"vitest src/**/__tests__/*.test.ts","test:integration":"vitest integration/__tests__/*.test.ts","test:ci":"vitest run","lint":"eslint . --ext .ts","lint:fix":"eslint . --ext .ts --fix","format":"prettier --write \"src/**/*.ts\" \"integration/**/*.ts\"","format:check":"prettier --check \"src/**/*.ts\" \"integration/**/*.ts\""},"keywords":["api","proxy","openapi","har","documentation"],"dependencies":{"@scalar/api-reference":"^1.34.2","better-sqlite3":"^12.11.1","body-parser":"^2.2.0","chalk":"^5.5.0","commander":"^14.0.0","cors":"^2.8.5","express":"^5.1.0","hono":"^4.9.1","http-proxy":"^1.18.1","http-proxy-middleware":"^3.0.5","serve-static":"^2.2.0","swagger-ui-express":"^5.0.1","yaml":"^2.8.1"},"devDependencies":{"@eslint/eslintrc":"^3.3.1","@eslint/js":"^9.33.0","@hono/node-server":"^1.18.2","@types/better-sqlite3":"^7.6.13","@types/body-parser":"^1.19.6","@types/cors":"^2.8.19","@types/express":"^5.0.3","@types/http-proxy":"^1.17.16","@types/node":"^24.2.1","@types/node-fetch":"^2.6.13","@types/serve-static":"^1.15.8","@types/swagger-ui-express":"^4.1.8","@typescript-eslint/eslint-plugin":"^8.39.1","@typescript-eslint/parser":"^8.39.1","@vitest/coverage-v8":"^3.2.4","eslint":"^9.33.0","eslint-config-prettier":"^10.1.8","globals":"^16.3.0","node-fetch":"^3.3.2","openapi-types":"^12.1.3","prettier":"^3.6.2","ts-node":"^10.9.2","ts-node-dev":"^2.0.0","typescript":"^5.9.2","vitest":"^3.2.4"},"gitHead":"0798bf7aa855172b05cfa1df8fe1a05ae26f5e12","types":"./dist/src/server.d.ts","_id":"@parke.dev/arbiter@1.1.0","_nodeVersion":"22.23.2","_npmVersion":"12.0.2","dist":{"integrity":"sha512-uGx61ncUMcJSlZWgHUQ9jw2Nk4Md4H0UKdUKdtj3xJlcunVvHzw1wAeFY+YTrDeve9sZovvSzHEPiikfnMrUZw==","shasum":"8ec9b120f6dfb114427ed93cef6154d67274b602","tarball":"https://registry.npmjs.org/@parke.dev/arbiter/-/arbiter-1.1.0.tgz","fileCount":290,"unpackedSize":1953909,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@parke.dev%2farbiter@1.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCuR8hss/g8TNyb9sGOj67xhL9tNXXhHCLsVda2KE1SOwIhAIRekvNdx2o6F2LIEEFqBNVrTZAqGmP1u8G6Yrh6LdRr"}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:c6a57dc5-1e05-4a7b-84e4-b69f89fc943c"}},"directories":{},"maintainers":[{"name":"lukehagar","email":"lukeslakemail@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/arbiter_1.1.0_1786567708808_0.944791886668682"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-15T21:23:06.911Z","modified":"2026-08-12T20:48:29.384Z","1.0.1":"2026-07-15T21:23:07.204Z","1.1.0":"2026-08-12T20:48:28.990Z"},"bugs":{"url":"https://github.com/LukasParke/arbiter/issues"},"author":{"name":"Luke Hagar"},"license":"MIT","homepage":"https://github.com/LukasParke/arbiter#readme","keywords":["api","proxy","openapi","har","documentation"],"repository":{"type":"git","url":"git+https://github.com/LukasParke/arbiter.git"},"description":"API proxy with OpenAPI generation and HAR export capabilities","maintainers":[{"name":"lukehagar","email":"lukeslakemail@gmail.com"}],"readme":"<p align=\"center\">\n  <img src=\"ArbiterLogo.png\" alt=\"Arbiter Logo\" width=\"250\">\n</p>\n\n# Arbiter\n\nArbiter is a powerful API proxy and documentation generator that automatically creates OpenAPI specifications and HAR (HTTP Archive) recordings for any API you access through it.\n\n## Features\n\n- **Exact Capture** - Byte-fidelity capture of application HTTP bodies with deterministic, content-addressed capture bundles\n- **Replay & Comparison** - Replay captured bundles with exact-byte, semantic-JSON, or semantic-SSE comparison\n- **Redaction & Secret Safety** - Credential headers and query values are redacted before persistence; exports are secret-scanned and fail closed\n- **Credential Gateway** - Untrusted clients use short-lived opaque tokens; real upstream credentials are injected server-side and never exposed\n- **API Proxy** - Transparently proxies all API requests to the target API \n- **Automatic OpenAPI Generation** - Builds a complete OpenAPI 3.1 specification based on observed traffic\n- **HAR Recording** - Records all requests and responses in HAR format for debugging and analysis\n- **Interactive API Documentation** - Provides beautiful, interactive API documentation using [Scalar](https://github.com/scalar/scalar)\n- **Security Scheme Detection** - Automatically detects and documents API key, Bearer token, and Basic authentication\n- **Schema Inference** - Analyzes JSON responses to generate accurate schema definitions\n- **Path Parameter Detection** - Intelligently identifies path parameters from multiple requests\n- **Support for Complex Content Types** - Handles JSON, XML, form data, and binary content\n\n## Getting Started\n\n### Installation\n\n```bash\nnpm install -g @parke.dev/arbiter\n```\n\n### Basic Usage\n\nStart Arbiter by pointing it to your target API:\n\n```bash\narbiter --target https://api.example.com\n# with persistence\narbiter --target https://api.example.com --db-path ./arbiter.db\n```\n\nThen send requests through the proxy:\n\n```bash\ncurl http://localhost:8080/users\n```\n\nAnd view the automatically generated documentation:\n\n```bash\nopen http://localhost:9000/docs\n```\n\n### Docker Usage\n\nYou can run Arbiter using Docker:\n\n```bash\n# Build the Docker image\ndocker build -t arbiter .\n\n# Run the container (ephemeral)\ndocker run -p 8080:8080 -p 9000:9000 arbiter --target https://api.example.com\n\n# Run the container with persistent storage\ndocker run -p 8080:8080 -p 9000:9000 \\\n  -v $(pwd)/data:/data \\\n  arbiter --target https://api.example.com --db-path /data/arbiter.db\n```\n\nThe container exposes:\n- Port 8080 for the proxy server\n- Port 9000 for the documentation server\n\nYou can customize the ports and other options:\n\n```bash\ndocker run -p 3000:3000 -p 3001:3001 arbiter \\\n  --target https://api.example.com \\\n  --port 3000 \\\n  --docs-port 3001 \\\n  --db-path /data/arbiter.db \\\n  --verbose\n```\n\n## Usage Options\n\n| Option | Description | Default |\n|--------|-------------|---------|\n| `-t, --target <url>` | Target API URL to proxy to | (required) |\n| `-p, --port <number>` | Port to run the proxy server on | 8080 |\n| `-d, --docs-port <number>` | Port to run the documentation server on | 9000 |\n| `--db-path <path>` | Path to SQLite database file for persistence | (disabled) |\n| `--docs-only` | Run only the documentation server | false |\n| `--proxy-only` | Run only the proxy server | false |\n| `-v, --verbose` | Enable verbose logging | false |\n\n## API Documentation\n\nAfter using the API through the proxy, you can access:\n\n- Interactive API docs: `http://localhost:9000/docs`\n- OpenAPI JSON: `http://localhost:9000/openapi.json`\n- OpenAPI YAML: `http://localhost:9000/openapi.yaml`\n- HAR Export: `http://localhost:9000/har`\n\n## Exact Capture and Replay\n\nArbiter's canonical capture path guarantees **exact application HTTP body bytes after transport decoding**:\n\n- request body bytes received from the client;\n- response body bytes delivered to the client;\n- ordered SSE bytes and terminal state (`message_stop`, `[DONE]`, `response.completed`, …);\n- request method and path/query, response status, and selected end-to-end headers.\n\nArbiter does **not** claim equality for TLS records, HTTP/2 frames, TCP segmentation, transfer-chunk framing, or provider compression framing — those are transport details, not API contract bytes. If an upstream compresses despite `Accept-Encoding: identity`, the compressed bytes are recorded canonically with their `content-encoding`, and analysis views are derived separately.\n\n### Capture\n\n```bash\narbiter capture \\\n  --target https://api.anthropic.com \\\n  --output ./capture \\\n  --exact \\\n  --reject-secret ANTHROPIC_API_KEY \\\n  --ready-file ./ready.json \\\n  --report ./report.json\n```\n\n`--exact` enables fail-closed semantics: any recording, persistence, body-limit, or secret-scan failure fails the export. On shutdown (SIGINT/SIGTERM or `--idle-timeout`), a deterministic bundle is written:\n\n```text\ncapture/\n  manifest.json        # version, mode, target origin, bundle digest, redaction policy\n  exchanges.ndjson     # one stable-JSON exchange per line, ordered by sequence\n  bodies/<sha256>.bin  # content-addressed body bytes\n  validation.ndjson    # optional structured violations\n```\n\nBundles are safe to load from untrusted sources: digests are verified, symlinks and path traversal are rejected.\n\n### Replay\n\n```bash\narbiter replay ./capture \\\n  --target http://127.0.0.1:8787 \\\n  --mode semantic-sse-response \\\n  --credential-env OPENROUTER_API_KEY:authorization:Bearer \\\n  --ignore-pointer /id \\\n  --fail-on-diff\n```\n\nModes: `status-only`, `exact-response-body` (first differing byte offset), `semantic-json-response` (first differing JSON pointer), `semantic-sse-response` (ordered events with declared volatile pointers; non-SSE exchanges fail loudly rather than matching vacuously). Replay resends the recorded method, path/query, safe headers, and exact body bytes. Redacted credentials are only re-injected through `--credential-env` or a credential-provider callback — never stored.\n\n**Redaction limits replayability by design.** A query value redacted at capture is not replayable: Arbiter never sends invented placeholder values to a target. Such exchanges fail as unreplayable unless a replacement is supplied via `--query-env NAME:ENV_VAR` (or a `queryValueProvider` callback in library use). The same applies to redacted headers and `--credential-env`. Legacy traffic JSONL replays remain available via `--legacy-jsonl` (or by passing a file path).\n\n### Sanitize and validate\n\n```bash\narbiter sanitize ./capture --output ./sanitized --reject-secret-env ANTHROPIC_API_KEY\narbiter validate ./capture --spec ./openapi.yaml --strict --report report.json\n```\n\nSanitize reloads an untrusted bundle with full verification, re-applies redaction, secret-scans everything, and emits a new deterministic bundle. It never edits in place.\n\n### Credential gateway\n\n```bash\narbiter gateway --policy policy.json --credential-command 'op read op://vault/anthropic/key' \\\n  --capture-output ./gateway-capture\n```\n\nThe policy pins a sha256 of an opaque client token plus expiry, target origin, methods, path prefixes, optional models, and request/byte/duration ceilings. The client never sees the upstream credential; the credential command's stdout is consumed as a secret and never logged. Oversized requests receive a clean `413` JSON response.\n\nWith `--capture-output` (or `capture: {}` in library use), allowed gateway traffic is routed through an exact `CaptureSession` and exported as a deterministic bundle on shutdown. The gateway fails closed if the injected credential header is not covered by the capture redaction policy, so neither the gateway token nor the upstream credential can reach the bundle.\n\n### Library usage\n\n```typescript\nimport { startCaptureSession } from '@parke.dev/arbiter/capture';\nimport { loadBundle } from '@parke.dev/arbiter/bundle';\nimport { replayCapture } from '@parke.dev/arbiter/replay';\nimport { validateCapture, BasicOpenAPIValidator, CallbackValidator } from '@parke.dev/arbiter/validation';\nimport { startGateway } from '@parke.dev/arbiter/gateway';\n\nconst session = await startCaptureSession({ target: 'https://api.anthropic.com', mode: 'exact' });\n// point your client at session.url …\nawait session.waitForIdle();\nconst { bundle } = await session.export({ output: './capture' });\nawait session.close();\n\nconst report = await replayCapture(loadBundle('./capture'), {\n  target: 'http://127.0.0.1:8787',\n  mode: 'semantic-sse-response',\n  normalization: { ignorePointers: ['/id'] },\n});\n```\n\n### Security model\n\n- Default redaction removes values for `authorization`, `proxy-authorization`, `cookie`, `set-cookie`, `x-api-key`, `x-auth-token`, `x-goog-api-key`, and any header matching `api[-_]?key|auth|credential|secret|token|cookie|session`; redacted names are kept as evidence.\n- Query parameter values are redacted by default; names are retained. Allow specific keys with `--allow-query`.\n- Exact exports secret-scan the manifest, headers, paths, and textual bodies for caller-supplied exact values (`--reject-secret ENV_NAME`) and common credential patterns (Anthropic/OpenAI/OpenRouter keys, GitHub tokens, AWS key ids, Google API keys, JWTs, PEM keys, Bearer/Basic values). Any finding fails the export; findings never contain the full secret.\n- Unexpected binary bodies fail exact export unless their media type is explicitly allowed.\n- Bundle output directories are created `0700`, files `0600`; output roots are realpath-resolved so writes never follow a symlinked directory.\n- `loadBundle` treats bundles as hostile input: every manifest/exchange field is runtime-validated with bounded sizes before allocation, sequences must be strictly increasing, base64 must be well-formed and consistent with declared sizes, digests are verified, and file reads reject symlinks at every path component under the bundle root.\n\n## How It Works\n\n### Proxy Server\n\nArbiter creates a proxy server that forwards all requests to your target API, preserving headers, method, body, and other request details. Responses are returned unmodified to the client, while Arbiter records the exchange in the background.\n\n### OpenAPI Generation\n\nAs requests flow through the proxy, Arbiter:\n\n1. Records endpoints, methods, and path parameters\n2. Analyzes request bodies and generates request schemas\n3. Processes response bodies and generates response schemas\n4. Detects query parameters and headers\n5. Identifies security schemes based on authentication headers\n6. Combines multiple observations to create a comprehensive specification\n\n### Schema Generation\n\nArbiter uses sophisticated algorithms to generate accurate JSON schemas:\n\n- Object property types are inferred from values\n- Array item schemas are derived from sample items\n- Nested objects and arrays are properly represented\n- Path parameters are identified from URL patterns\n- Query parameters are extracted and documented\n- Security requirements are automatically detected\n\n### HAR Recording\n\nAll requests and responses are recorded in HAR (HTTP Archive) format, providing:\n\n- Complete request details (method, URL, headers, body)\n- Complete response details (status, headers, body)\n- Timing information\n- Content size and type\n\n## Advanced Features\n\n### Structure Analysis\n\nArbiter can analyze the structure of JSON-like text that isn't valid JSON:\n\n- Detects array-like structures (`[{...}, {...}]`)\n- Identifies object-like structures (`{\"key\": \"value\"}`)\n- Extracts field names from malformed JSON\n- Provides fallback schemas for unstructured content\n\n### Content Processing\n\nArbiter handles various content types:\n\n- **JSON** - Parsed and converted to schemas with proper types\n- **XML** - Recognized and documented with appropriate schema format\n- **Form Data** - Processed and documented as form parameters\n- **Binary Data** - Handled with appropriate binary format schemas\n- **Compressed Content** - Automatically decompressed (gzip support)\n\n## Middleware Usage\n\nArbiter can also be used as middleware in your own application. Note that middleware mode observes the application *after* parsing and offers semantic capture only — proxy-level byte exactness requires `arbiter capture`:\n\n```typescript\nimport express from 'express';\nimport { harRecorder } from '@parke.dev/arbiter/middleware';\nimport { openApiStore } from '@parke.dev/arbiter/store';\n\nconst app = express();\n\n// Add Arbiter middleware\napp.use(harRecorder(openApiStore));\n\n// Your routes\napp.get('/users', (req, res) => {\n  res.json([{ id: 1, name: 'User' }]);\n});\n\napp.listen(3000);\n```\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## Publishing (maintainers)\n\nThis repo auto-publishes to npm on push to `main` if the version in `package.json` is newer than the version on npm.\n\nSetup (one-time):\n- On npmjs.com → `@parke.dev/arbiter` → Settings → Trusted Publisher, add GitHub Actions:\n  - Organization or user: `LukasParke`\n  - Repository: `arbiter`\n  - Workflow filename: `publish.yml`\n\nManual run:\n- You can also trigger the workflow manually from the Actions tab (workflow_dispatch).\n\n## License\n\nThis project is licensed under the MIT License - see the LICENSE file for details.\n","readmeFilename":"README.md"}