diff --git a/README.md b/README.md index 9885382..d0bb0e9 100644 --- a/README.md +++ b/README.md @@ -115,57 +115,13 @@ npm run cli -- internal-doc.jpg \ --- -### 🐳 Docker API Examples +The Docker API runs on port 3000 by default. It uses standard detection settings (Emails, IPs, Keys, PII) by default, but is **fully configurable** via the `settings` parameter. -The Docker API runs on port 3000 by default. It currently uses standard detection settings (Emails, IPs, Keys, PII). +👉 **[View Full API Documentation](docs/API.md)** for detailed usage, schema, and Python/Node.js examples. -#### 1. Quick Test via Curl +#### Quick Test (Curl) ```bash curl -X POST http://localhost:3000/redact \ -F "image=@/path/to/doc.jpg" \ -o redacted.png ``` -*Check the `X-Redacted-Stats` header in the response for detection counts.* - -#### 2. Python Integration -Process images programmatically in your Python apps: - -```python -import requests - -url = 'http://localhost:3000/redact' -files = {'image': open('contract.jpg', 'rb')} - -response = requests.post(url, files=files) - -if response.status_code == 200: - with open('redacted_contract.png', 'wb') as f: - f.write(response.content) - print("Stats:", response.headers.get('X-Redacted-Stats')) -else: - print("Error:", response.text) -``` - -#### 3. Node.js Integration (Native Fetch) -Requires Node.js 18+. No extra libraries needed! - -```javascript -import fs from 'fs'; - -const fileBuffer = fs.readFileSync('id_card.jpg'); -const blob = new Blob([fileBuffer], { type: 'image/jpeg' }); - -const formData = new FormData(); -formData.append('image', blob, 'id_card.jpg'); - -const response = await fetch('http://localhost:3000/redact', { - method: 'POST', - body: formData -}); - -if (response.ok) { - const buffer = Buffer.from(await response.arrayBuffer()); - fs.writeFileSync('redacted_id.png', buffer); - console.log('Stats:', response.headers.get('x-redacted-stats')); -} -``` diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..ad45a8d --- /dev/null +++ b/docs/API.md @@ -0,0 +1,133 @@ +# AutoRedact API Documentation + +The AutoRedact API provides a simple, high-performance interface for redacting sensitive information from images using the same engine as the CLI and Web UI. + +## Base URL + +By default, the Docker container exposes the API on port `3000`. + +``` +http://localhost:3000 +``` + +--- + +## Endpoints + +### 1. Redact Image + +**POST** \`/redact\` + +Uploads an image for processing and returns the redacted image. + +#### Request Headers +* **Content-Type**: \`multipart/form-data\` + +#### Request Body (Multipart Fields) + +| Field | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| \`image\` | File | **Yes** | The image file to process. Supports \`.jpg\` and \`.png\`. | +| \`settings\` | JSON String | No | Configuration object to control detection rules. | + +#### Settings Schema + +The \`settings\` field accepts a JSON string matching the \`DetectionSettings\` interface: + +```json +{ + "email": boolean, // Default: true + "ip": boolean, // Default: true + "creditCard": boolean, // Default: true + "secret": boolean, // Default: true + "pii": boolean, // Default: true + "allowlist": string[], // e.g. ["127.0.0.1", "MyCorp"] + "blockWords": string[], // e.g. ["CONFIDENTIAL"] + "customRegex": [ + { + "pattern": "regex_pattern", + "flags": "i" // Optional + } + ] +} +``` + +#### Response + +* **Status**: \`200 OK\` +* **Content-Type**: \`image/png\` (The redacted image) +* **Header** \`X-Redacted-Stats\`: JSON string containing detection counts. + +--- + +## Examples + +### 1. Basic Usage (Curl) +Redact an image using default settings: + +```bash +curl -X POST http://localhost:3000/redact \\ + -F "image=@/path/to/document.jpg" \\ + -o redacted.png +``` + +### 2. Advanced Configuration (Curl) +Disable email redaction and add custom block words: + +```bash +curl -X POST http://localhost:3000/redact \\ + -F "image=@invoice.jpg" \\ + -F 'settings={"email":false, "blockWords":["CONFIDENTIAL", "SSN"]}' \\ + -o redacted_invoice.png +``` + +### 3. Node.js (Native Fetch) +Requires Node 18+: + +```javascript +import fs from 'fs'; + +const fileBuffer = fs.readFileSync('doc.jpg'); +const blob = new Blob([fileBuffer], { type: 'image/jpeg' }); + +const formData = new FormData(); +formData.append('image', blob, 'doc.jpg'); +formData.append('settings', JSON.stringify({ + allowlist: ['CorpName'], + ip: false // Disable IP scanner +})); + +const response = await fetch('http://localhost:3000/redact', { + method: 'POST', + body: formData +}); + +if (response.ok) { + const buffer = Buffer.from(await response.arrayBuffer()); + fs.writeFileSync('redacted.png', buffer); + console.log('Stats:', response.headers.get('x-redacted-stats')); +} +``` + +### 4. Python (Requests) + +```python +import requests +import json + +url = 'http://localhost:3000/redact' +files = {'image': open('doc.jpg', 'rb')} +settings = { + "email": False, + "blockWords": ["DO NOT DISTRIBUTE"] +} + +data = {'settings': json.dumps(settings)} + +response = requests.post(url, files=files, data=data) + +if response.status_code == 200: + with open('redacted.png', 'wb') as f: + f.write(response.content) + print("Stats:", response.headers.get('X-Redacted-Stats')) +``` diff --git a/src/server.ts b/src/server.ts index c86579d..6adae32 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,6 +4,8 @@ import { processImage } from './core/processor'; import { NodeCanvasAdapter } from './adapters/NodeCanvasAdapter'; import type { DetectionSettings } from './types'; +import { DEFAULT_ALLOWLIST } from './constants/config'; + const server = fastify({ logger: true }); // Register multipart support for file uploads @@ -21,40 +23,44 @@ server.get('/health', async () => { }); server.post('/redact', async (req, reply) => { - const data = await req.file(); - - if (!data) { - return reply.code(400).send({ error: 'No image file uploaded' }); - } - - if (!['image/jpeg', 'image/png'].includes(data.mimetype)) { - return reply.code(400).send({ error: 'Only .jpg and .png files are supported' }); - } + const parts = req.parts(); + let imageBuffer: Buffer | undefined; + let settings: DetectionSettings = { + email: true, + ip: true, + creditCard: true, + secret: true, + pii: true, + allowlist: [...DEFAULT_ALLOWLIST], + blockWords: [], + customDates: [], + customRegex: [] + }; try { - const buffer = await data.toBuffer(); - - // Parse settings (optional) - // Note: multipart fields come as streams or values. - // For simplicity, we can inspect other fields if parsed, - // but @fastify/multipart with req.file() iterates. - // Let's attach settings via a specific header or query param for mvp, - // or parse fields if needed. - // Better: simple default settings for now, allow enhancement later. - const settings: DetectionSettings = { - email: true, - ip: true, - creditCard: true, - secret: true, - pii: true, - // Default empty limits - allowlist: [], - blockWords: [], - customDates: [], - customRegex: [] - }; - - const result = await processImage(buffer, { + for await (const part of parts) { + if (part.type === 'file' && part.fieldname === 'image') { + if (!['image/jpeg', 'image/png'].includes(part.mimetype)) { + // We continue to consume the stream to avoid hanging, but note the error + // Or just throw immediately if we want to fail fast + throw new Error('Only .jpg and .png files are supported'); + } + imageBuffer = await part.toBuffer(); + } else if (part.type === 'field' && part.fieldname === 'settings') { + try { + const parsed = JSON.parse(part.value as string); + settings = { ...settings, ...parsed }; + } catch { + req.log.warn('Failed to parse settings JSON'); + } + } + } + + if (!imageBuffer) { + return reply.code(400).send({ error: 'No image file uploaded' }); + } + + const result = await processImage(imageBuffer, { canvasFactory: adapter, detectionSettings: settings, }); @@ -70,9 +76,11 @@ server.post('/redact', async (req, reply) => { reply.type('image/png'); return outputBuffer; - } catch (err) { + } catch (err: unknown) { req.log.error(err); - return reply.code(500).send({ error: 'Redaction processing failed' }); + const errorMessage = err instanceof Error ? err.message : 'Unknown error'; + const status = errorMessage.includes('supported') ? 400 : 500; + return reply.code(status).send({ error: errorMessage || 'Redaction processing failed' }); } });