@topazlabs/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +96 -0
  2. package/dist/api/client.d.ts +31 -0
  3. package/dist/api/client.d.ts.map +1 -0
  4. package/dist/api/client.js +150 -0
  5. package/dist/api/client.js.map +1 -0
  6. package/dist/api/models.d.ts +19 -0
  7. package/dist/api/models.d.ts.map +1 -0
  8. package/dist/api/models.js +75 -0
  9. package/dist/api/models.js.map +1 -0
  10. package/dist/api/types.d.ts +40 -0
  11. package/dist/api/types.d.ts.map +1 -0
  12. package/dist/api/types.js +2 -0
  13. package/dist/api/types.js.map +1 -0
  14. package/dist/apps/viewer.html +51 -0
  15. package/dist/lib/create-main.d.ts +9 -0
  16. package/dist/lib/create-main.d.ts.map +1 -0
  17. package/dist/lib/create-main.js +27 -0
  18. package/dist/lib/create-main.js.map +1 -0
  19. package/dist/lib/errors.d.ts +16 -0
  20. package/dist/lib/errors.d.ts.map +1 -0
  21. package/dist/lib/errors.js +17 -0
  22. package/dist/lib/errors.js.map +1 -0
  23. package/dist/lib/fetch-url.d.ts +18 -0
  24. package/dist/lib/fetch-url.d.ts.map +1 -0
  25. package/dist/lib/fetch-url.js +239 -0
  26. package/dist/lib/fetch-url.js.map +1 -0
  27. package/dist/lib/http-server.d.ts +18 -0
  28. package/dist/lib/http-server.d.ts.map +1 -0
  29. package/dist/lib/http-server.js +225 -0
  30. package/dist/lib/http-server.js.map +1 -0
  31. package/dist/lib/path-security.d.ts +21 -0
  32. package/dist/lib/path-security.d.ts.map +1 -0
  33. package/dist/lib/path-security.js +210 -0
  34. package/dist/lib/path-security.js.map +1 -0
  35. package/dist/main.d.ts +3 -0
  36. package/dist/main.d.ts.map +1 -0
  37. package/dist/main.js +8 -0
  38. package/dist/main.js.map +1 -0
  39. package/dist/server.d.ts +3 -0
  40. package/dist/server.d.ts.map +1 -0
  41. package/dist/server.js +108 -0
  42. package/dist/server.js.map +1 -0
  43. package/dist/tools/compare.d.ts +9 -0
  44. package/dist/tools/compare.d.ts.map +1 -0
  45. package/dist/tools/compare.js +133 -0
  46. package/dist/tools/compare.js.map +1 -0
  47. package/dist/tools/enhance.d.ts +22 -0
  48. package/dist/tools/enhance.d.ts.map +1 -0
  49. package/dist/tools/enhance.js +175 -0
  50. package/dist/tools/enhance.js.map +1 -0
  51. package/package.json +52 -0
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # @topazlabs/mcp
2
+
3
+ AI image enhancement and comparison for LLMs via the [Model Context Protocol](https://modelcontextprotocol.io).
4
+
5
+ Built by [Topaz Labs](https://www.topazlabs.com) — the company behind industry-leading AI upscaling and enhancement tools.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ npx @topazlabs/mcp
11
+ ```
12
+
13
+ No install required. The server starts on stdio and connects to any MCP client.
14
+
15
+ ## Configuration
16
+
17
+ ### Claude Desktop
18
+
19
+ Add to your `claude_desktop_config.json`:
20
+
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "topaz": {
25
+ "command": "npx",
26
+ "args": ["-y", "@topazlabs/mcp"],
27
+ "env": {
28
+ "TOPAZ_API_KEY": "<your-api-key>"
29
+ }
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ ### HTTP Mode (hosted)
36
+
37
+ ```bash
38
+ TOPAZ_API_KEY=<key> MCP_AUTH_TOKEN=<token> PORT=3001 npx @topazlabs/mcp --http
39
+ ```
40
+
41
+ ## Tools
42
+
43
+ ### `enhance`
44
+
45
+ Enhance an image using Topaz Labs AI. Upscales, sharpens, and improves quality.
46
+
47
+ **Parameters:**
48
+
49
+ | Parameter | Type | Required | Description |
50
+ |-----------|------|----------|-------------|
51
+ | `image` | string | ✅ | Local file path or URL |
52
+ | `size` | object | | Output size spec (see below) |
53
+ | `model` | string | | Model name (auto-selects if omitted) |
54
+
55
+ **Size options:**
56
+
57
+ ```json
58
+ { "width": 1920, "height": 1080 } // Exact dimensions
59
+ { "width": 1920 } // Scale proportionally
60
+ { "height": 1080 } // Scale proportionally
61
+ { "scale": 2.5 } // Multiply original dimensions
62
+ ```
63
+
64
+ Blank defaults to auto-upscale: images under 4K get upscaled to 4K width; images already 4K+ stay at 1x.
65
+
66
+ **Auto model selection:**
67
+
68
+ | Image Size | Default Model |
69
+ |-----------|--------------|
70
+ | Under 4K | Wonder |
71
+ | 4K+ | Standard V2 (with face enhancement) |
72
+
73
+ **Available models:** Standard V2, Low Resolution V2, CGI, High Fidelity V2, Text Refine, Redefine, Recovery V2, Standard MAX, Wonder, Bloom
74
+
75
+ ### `compare`
76
+
77
+ Compare two images side-by-side with an interactive before/after slider viewer.
78
+
79
+ **Parameters:**
80
+
81
+ | Parameter | Type | Required | Description |
82
+ |-----------|------|----------|-------------|
83
+ | `before` | string | ✅ | Path or URL to the "before" image |
84
+ | `after` | string | ✅ | Path or URL to the "after" image |
85
+ | `label_before` | string | | Label for before image (default: "Before") |
86
+ | `label_after` | string | | Label for after image (default: "After") |
87
+
88
+ **Viewer features:** Draggable slider, zoom (100%/200%/400%), pan, keyboard shortcuts (1/2/3 for before/split/after).
89
+
90
+ ## API Key
91
+
92
+ Get your API key at [topazlabs.com](https://www.topazlabs.com). Set it as the `TOPAZ_API_KEY` environment variable.
93
+
94
+ ## License
95
+
96
+ MIT
@@ -0,0 +1,31 @@
1
+ import type { EnhanceRequest, SubmitResponse, StatusResponse, DownloadResponse } from "./types.js";
2
+ export interface TopazClientOptions {
3
+ apiKey: string;
4
+ baseUrl?: string;
5
+ pollIntervalMs?: number;
6
+ timeoutMs?: number;
7
+ }
8
+ export declare class TopazClient {
9
+ private apiKey;
10
+ private baseUrl;
11
+ private pollIntervalMs;
12
+ private timeoutMs;
13
+ constructor(options: TopazClientOptions);
14
+ private headers;
15
+ private buildFormData;
16
+ enhance(req: EnhanceRequest): Promise<SubmitResponse>;
17
+ private submitJob;
18
+ getStatus(processId: string): Promise<StatusResponse>;
19
+ getDownloadUrl(processId: string): Promise<DownloadResponse>;
20
+ downloadToFile(processId: string, outputDir: string): Promise<string>;
21
+ pollUntilComplete(processId: string, onProgress?: (status: StatusResponse) => void): Promise<StatusResponse>;
22
+ }
23
+ export declare class TopazApiError extends Error {
24
+ statusCode: number;
25
+ constructor(statusCode: number, message: string);
26
+ }
27
+ export declare function readImageFile(filePath: string): {
28
+ buffer: Buffer;
29
+ filename: string;
30
+ };
31
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,cAAc,EACd,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAOpB,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,SAAS,CAAS;gBAEd,OAAO,EAAE,kBAAkB;IAOvC,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,aAAa;IA6Bf,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;YAW7C,SAAS;IA6BjB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAcrD,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAc5D,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAwBrE,iBAAiB,CACrB,SAAS,EAAE,MAAM,EACjB,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,GAC5C,OAAO,CAAC,cAAc,CAAC;CAqB3B;AAED,qBAAa,aAAc,SAAQ,KAAK;IAC/B,UAAU,EAAE,MAAM,CAAC;gBACd,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAKhD;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB,CAKA"}
@@ -0,0 +1,150 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { validateReadPath } from "../lib/path-security.js";
4
+ import { isGenerativeEnhanceModel } from "./models.js";
5
+ const BASE_URL = "https://api.topazlabs.com/image/v1";
6
+ const DEFAULT_POLL_INTERVAL_MS = 2000;
7
+ const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
8
+ export class TopazClient {
9
+ apiKey;
10
+ baseUrl;
11
+ pollIntervalMs;
12
+ timeoutMs;
13
+ constructor(options) {
14
+ this.apiKey = options.apiKey;
15
+ this.baseUrl = options.baseUrl ?? BASE_URL;
16
+ this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
17
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
18
+ }
19
+ headers() {
20
+ return { "X-API-Key": this.apiKey };
21
+ }
22
+ buildFormData(params, imageBuffer, imageFilename) {
23
+ const form = new FormData();
24
+ if (imageBuffer) {
25
+ const filename = imageFilename ?? "image.jpg";
26
+ const ext = path.extname(filename).toLowerCase();
27
+ const mimeMap = {
28
+ ".jpg": "image/jpeg",
29
+ ".jpeg": "image/jpeg",
30
+ ".png": "image/png",
31
+ ".tif": "image/tiff",
32
+ ".tiff": "image/tiff",
33
+ ".webp": "image/webp",
34
+ };
35
+ const mime = mimeMap[ext] ?? "image/jpeg";
36
+ const blob = new Blob([new Uint8Array(imageBuffer)], { type: mime });
37
+ form.append("image", blob, filename);
38
+ }
39
+ for (const [key, value] of Object.entries(params)) {
40
+ if (value === undefined || value === null)
41
+ continue;
42
+ if (key === "image" || key === "image_filename")
43
+ continue;
44
+ form.append(key, String(value));
45
+ }
46
+ return form;
47
+ }
48
+ async enhance(req) {
49
+ const isGen = req.model ? isGenerativeEnhanceModel(req.model) : false;
50
+ const endpoint = isGen ? "/enhance-gen/async" : "/enhance/async";
51
+ return this.submitJob(endpoint, req, req.image, req.image_filename);
52
+ }
53
+ async submitJob(endpoint, params, imageBuffer, imageFilename) {
54
+ const form = this.buildFormData(params, imageBuffer, imageFilename);
55
+ const resp = await fetch(`${this.baseUrl}${endpoint}`, {
56
+ method: "POST",
57
+ headers: this.headers(),
58
+ body: form,
59
+ });
60
+ if (!resp.ok) {
61
+ const text = await resp.text();
62
+ throw new TopazApiError(resp.status, `Submit failed: ${resp.status} ${text}`);
63
+ }
64
+ const json = (await resp.json());
65
+ const processId = json.process_id ?? resp.headers.get("X-Process-ID");
66
+ const eta = json.eta ?? (Number(resp.headers.get("X-ETA")) || 0);
67
+ if (!processId) {
68
+ throw new TopazApiError(0, "No process_id in response");
69
+ }
70
+ return { process_id: processId, eta };
71
+ }
72
+ async getStatus(processId) {
73
+ const resp = await fetch(`${this.baseUrl}/status/${processId}`, {
74
+ headers: this.headers(),
75
+ });
76
+ if (!resp.ok) {
77
+ const text = await resp.text();
78
+ throw new TopazApiError(resp.status, `Status check failed: ${resp.status} ${text}`);
79
+ }
80
+ return (await resp.json());
81
+ }
82
+ async getDownloadUrl(processId) {
83
+ const resp = await fetch(`${this.baseUrl}/download/${processId}`, {
84
+ headers: this.headers(),
85
+ });
86
+ if (!resp.ok) {
87
+ const text = await resp.text();
88
+ throw new TopazApiError(resp.status, `Download URL fetch failed: ${resp.status} ${text}`);
89
+ }
90
+ return (await resp.json());
91
+ }
92
+ async downloadToFile(processId, outputDir) {
93
+ const { download_url } = await this.getDownloadUrl(processId);
94
+ const resp = await fetch(download_url);
95
+ if (!resp.ok) {
96
+ throw new TopazApiError(resp.status, `Image download failed: ${resp.status}`);
97
+ }
98
+ const ct = resp.headers.get("content-type") ?? "image/png";
99
+ const extMap = {
100
+ "image/jpeg": ".jpg",
101
+ "image/png": ".png",
102
+ "image/tiff": ".tiff",
103
+ };
104
+ const ext = extMap[ct] ?? ".png";
105
+ const filename = `topaz-${processId}${ext}`;
106
+ const filepath = path.join(outputDir, filename);
107
+ fs.mkdirSync(outputDir, { recursive: true });
108
+ const buffer = Buffer.from(await resp.arrayBuffer());
109
+ fs.writeFileSync(filepath, buffer);
110
+ return filepath;
111
+ }
112
+ async pollUntilComplete(processId, onProgress) {
113
+ const startTime = Date.now();
114
+ while (true) {
115
+ const status = await this.getStatus(processId);
116
+ if (onProgress)
117
+ onProgress(status);
118
+ if (status.status === "Completed")
119
+ return status;
120
+ if (status.status === "Failed") {
121
+ throw new TopazApiError(0, `Job ${processId} failed`);
122
+ }
123
+ if (status.status === "Cancelled") {
124
+ throw new TopazApiError(0, `Job ${processId} was cancelled`);
125
+ }
126
+ if (Date.now() - startTime > this.timeoutMs) {
127
+ throw new TopazApiError(0, `Job ${processId} timed out after ${this.timeoutMs / 1000}s`);
128
+ }
129
+ await sleep(this.pollIntervalMs);
130
+ }
131
+ }
132
+ }
133
+ export class TopazApiError extends Error {
134
+ statusCode;
135
+ constructor(statusCode, message) {
136
+ super(message);
137
+ this.name = "TopazApiError";
138
+ this.statusCode = statusCode;
139
+ }
140
+ }
141
+ export function readImageFile(filePath) {
142
+ const validatedPath = validateReadPath(filePath);
143
+ const buffer = fs.readFileSync(validatedPath);
144
+ const filename = path.basename(validatedPath);
145
+ return { buffer, filename };
146
+ }
147
+ function sleep(ms) {
148
+ return new Promise((resolve) => setTimeout(resolve, ms));
149
+ }
150
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { EnhanceModel, EnhanceGenerativeModel } from "./types.js";
2
+ export interface ModelInfo {
3
+ description: string;
4
+ type: "standard" | "generative";
5
+ bestFor: string;
6
+ }
7
+ export declare const ENHANCE_MODELS: Record<EnhanceModel, ModelInfo>;
8
+ export declare function isGenerativeEnhanceModel(model: string): model is EnhanceGenerativeModel;
9
+ /** 4K width threshold */
10
+ export declare const FOUR_K_WIDTH = 3840;
11
+ /**
12
+ * Auto-select model based on image dimensions.
13
+ * Under 4K → Wonder (best for low-res), 4K+ → Standard V2.
14
+ */
15
+ export declare function autoSelectModel(imageWidth: number): {
16
+ model: EnhanceModel;
17
+ faceEnhancement: boolean;
18
+ };
19
+ //# sourceMappingURL=models.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/api/models.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEvE,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,UAAU,GAAG,YAAY,CAAC;IAChC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,YAAY,EAAE,SAAS,CAmD1D,CAAC;AAUF,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,MAAM,GACZ,KAAK,IAAI,sBAAsB,CAEjC;AAED,yBAAyB;AACzB,eAAO,MAAM,YAAY,OAAO,CAAC;AAEjC;;;GAGG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG;IACnD,KAAK,EAAE,YAAY,CAAC;IACpB,eAAe,EAAE,OAAO,CAAC;CAC1B,CAKA"}
@@ -0,0 +1,75 @@
1
+ export const ENHANCE_MODELS = {
2
+ "Standard V2": {
3
+ description: "Balanced enhancement for most photos",
4
+ type: "standard",
5
+ bestFor: "General photos",
6
+ },
7
+ "Low Resolution V2": {
8
+ description: "Best for very small or low-quality images",
9
+ type: "standard",
10
+ bestFor: "Tiny images, thumbnails",
11
+ },
12
+ CGI: {
13
+ description: "Optimized for digital art, renders, and illustrations",
14
+ type: "standard",
15
+ bestFor: "Digital art, CGI, illustrations",
16
+ },
17
+ "High Fidelity V2": {
18
+ description: "Maximum detail preservation for high-quality sources",
19
+ type: "standard",
20
+ bestFor: "Already-good photos needing upscale",
21
+ },
22
+ "Text Refine": {
23
+ description: "Preserves and sharpens text in images",
24
+ type: "standard",
25
+ bestFor: "Screenshots, documents, text-heavy images",
26
+ },
27
+ Redefine: {
28
+ description: "AI-powered creative enhancement",
29
+ type: "generative",
30
+ bestFor: "Creative reinterpretation",
31
+ },
32
+ "Recovery V2": {
33
+ description: "Restores old, damaged, or heavily degraded photos",
34
+ type: "generative",
35
+ bestFor: "Old photos, damaged images",
36
+ },
37
+ "Standard MAX": {
38
+ description: "Maximum quality generative enhancement",
39
+ type: "generative",
40
+ bestFor: "Best possible quality, slow",
41
+ },
42
+ Wonder: {
43
+ description: "Best for low-res images with text, logos, or fine detail",
44
+ type: "generative",
45
+ bestFor: "Low-res images, text, logos",
46
+ },
47
+ Bloom: {
48
+ description: "Creative/artistic generative enhancement",
49
+ type: "generative",
50
+ bestFor: "Stylistic detail, creative enhancement",
51
+ },
52
+ };
53
+ const GENERATIVE_ENHANCE_MODELS = new Set([
54
+ "Redefine",
55
+ "Recovery V2",
56
+ "Standard MAX",
57
+ "Wonder",
58
+ "Bloom",
59
+ ]);
60
+ export function isGenerativeEnhanceModel(model) {
61
+ return GENERATIVE_ENHANCE_MODELS.has(model);
62
+ }
63
+ /** 4K width threshold */
64
+ export const FOUR_K_WIDTH = 3840;
65
+ /**
66
+ * Auto-select model based on image dimensions.
67
+ * Under 4K → Wonder (best for low-res), 4K+ → Standard V2.
68
+ */
69
+ export function autoSelectModel(imageWidth) {
70
+ if (imageWidth < FOUR_K_WIDTH) {
71
+ return { model: "Wonder", faceEnhancement: false };
72
+ }
73
+ return { model: "Standard V2", faceEnhancement: true };
74
+ }
75
+ //# sourceMappingURL=models.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models.js","sourceRoot":"","sources":["../../src/api/models.ts"],"names":[],"mappings":"AAQA,MAAM,CAAC,MAAM,cAAc,GAAoC;IAC7D,aAAa,EAAE;QACb,WAAW,EAAE,sCAAsC;QACnD,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,gBAAgB;KAC1B;IACD,mBAAmB,EAAE;QACnB,WAAW,EAAE,2CAA2C;QACxD,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,yBAAyB;KACnC;IACD,GAAG,EAAE;QACH,WAAW,EAAE,uDAAuD;QACpE,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,iCAAiC;KAC3C;IACD,kBAAkB,EAAE;QAClB,WAAW,EAAE,sDAAsD;QACnE,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,qCAAqC;KAC/C;IACD,aAAa,EAAE;QACb,WAAW,EAAE,uCAAuC;QACpD,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,2CAA2C;KACrD;IACD,QAAQ,EAAE;QACR,WAAW,EAAE,iCAAiC;QAC9C,IAAI,EAAE,YAAY;QAClB,OAAO,EAAE,2BAA2B;KACrC;IACD,aAAa,EAAE;QACb,WAAW,EAAE,mDAAmD;QAChE,IAAI,EAAE,YAAY;QAClB,OAAO,EAAE,4BAA4B;KACtC;IACD,cAAc,EAAE;QACd,WAAW,EAAE,wCAAwC;QACrD,IAAI,EAAE,YAAY;QAClB,OAAO,EAAE,6BAA6B;KACvC;IACD,MAAM,EAAE;QACN,WAAW,EAAE,0DAA0D;QACvE,IAAI,EAAE,YAAY;QAClB,OAAO,EAAE,6BAA6B;KACvC;IACD,KAAK,EAAE;QACL,WAAW,EAAE,0CAA0C;QACvD,IAAI,EAAE,YAAY;QAClB,OAAO,EAAE,wCAAwC;KAClD;CACF,CAAC;AAEF,MAAM,yBAAyB,GAAG,IAAI,GAAG,CAAS;IAChD,UAAU;IACV,aAAa;IACb,cAAc;IACd,QAAQ;IACR,OAAO;CACR,CAAC,CAAC;AAEH,MAAM,UAAU,wBAAwB,CACtC,KAAa;IAEb,OAAO,yBAAyB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAC9C,CAAC;AAED,yBAAyB;AACzB,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC;AAEjC;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,UAAkB;IAIhD,IAAI,UAAU,GAAG,YAAY,EAAE,CAAC;QAC9B,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC;IACrD,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC;AACzD,CAAC"}
@@ -0,0 +1,40 @@
1
+ export type ImageFormat = "jpeg" | "png" | "tiff";
2
+ export type JobStatus = "Pending" | "Processing" | "Completed" | "Failed" | "Cancelled";
3
+ export type EnhanceStandardModel = "Standard V2" | "Low Resolution V2" | "CGI" | "High Fidelity V2" | "Text Refine";
4
+ export type EnhanceGenerativeModel = "Redefine" | "Recovery V2" | "Standard MAX" | "Wonder" | "Bloom";
5
+ export type EnhanceModel = EnhanceStandardModel | EnhanceGenerativeModel;
6
+ export interface EnhanceRequest {
7
+ image?: Buffer;
8
+ image_filename?: string;
9
+ source_url?: string;
10
+ model?: EnhanceModel;
11
+ output_width?: number;
12
+ output_height?: number;
13
+ output_format?: ImageFormat;
14
+ face_enhancement?: boolean;
15
+ }
16
+ export interface SubmitResponse {
17
+ process_id: string;
18
+ eta: number;
19
+ }
20
+ export interface StatusResponse {
21
+ process_id: string;
22
+ input_format?: string;
23
+ input_height?: number;
24
+ input_width?: number;
25
+ output_format?: string;
26
+ output_height?: number;
27
+ output_width?: number;
28
+ model?: string;
29
+ status: JobStatus;
30
+ progress: number;
31
+ eta: number;
32
+ creation_time: number;
33
+ modification_time: number;
34
+ }
35
+ export interface DownloadResponse {
36
+ download_url: string;
37
+ input_download_url?: string;
38
+ expiration: number;
39
+ }
40
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/api/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,CAAC;AAClD,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,GAAG,WAAW,CAAC;AAExF,MAAM,MAAM,oBAAoB,GAC5B,aAAa,GACb,mBAAmB,GACnB,KAAK,GACL,kBAAkB,GAClB,aAAa,CAAC;AAElB,MAAM,MAAM,sBAAsB,GAC9B,UAAU,GACV,aAAa,GACb,cAAc,GACd,QAAQ,GACR,OAAO,CAAC;AAEZ,MAAM,MAAM,YAAY,GAAG,oBAAoB,GAAG,sBAAsB,CAAC;AAEzE,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,WAAW,CAAC;IAC5B,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;CACpB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/api/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,51 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Compare</title>
7
+ <script type="module" crossorigin>(function(){const o=document.createElement("link").relList;if(o&&o.supports&&o.supports("modulepreload"))return;for(const n of document.querySelectorAll('link[rel="modulepreload"]'))r(n);new MutationObserver(n=>{for(const i of n)if(i.type==="childList")for(const b of i.addedNodes)b.tagName==="LINK"&&b.rel==="modulepreload"&&r(b)}).observe(document,{childList:!0,subtree:!0});function s(n){const i={};return n.integrity&&(i.integrity=n.integrity),n.referrerPolicy&&(i.referrerPolicy=n.referrerPolicy),n.crossOrigin==="use-credentials"?i.credentials="include":n.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(n){if(n.ep)return;n.ep=!0;const i=s(n);fetch(n.href,i)}})();const y=window.__COMPARE_DATA__??{before:"",after:"",labelBefore:"Before",labelAfter:"After"},t=document.getElementById("viewport"),B=document.getElementById("canvas"),d=document.getElementById("img-before"),f=document.getElementById("img-after"),P=document.getElementById("clip-before"),R=document.getElementById("slider"),Z=document.getElementById("label-before"),_=document.getElementById("label-after"),M=document.getElementById("meta-before"),$=document.getElementById("meta-after"),F=document.getElementById("zoom-level"),w=document.getElementById("btn-before"),C=document.getElementById("btn-split"),A=document.getElementById("btn-after"),T=document.getElementById("btn-zoom-in"),D=document.getElementById("btn-zoom-out"),N=document.getElementById("btn-zoom-fit");let c=0,a=0,l=1,m=0,p=0,L=.5,S="split";const E=[.25,.5,.75,1,1.5,2,3,4];Z.textContent=y.labelBefore;_.textContent=y.labelAfter;let I=0;function k(e,o){const s=e==="before"?M:$;s.textContent=`${y[`label${e==="before"?"Before":"After"}`]}: ${o.naturalWidth}×${o.naturalHeight}`,I++,I>=2&&q()}d.onload=()=>k("before",d);f.onload=()=>k("after",f);d.onerror=()=>{M.textContent="Failed to load before image"};f.onerror=()=>{$.textContent="Failed to load after image"};d.src=y.before;f.src=y.after;function q(){c=Math.max(d.naturalWidth,f.naturalWidth),a=Math.max(d.naturalHeight,f.naturalHeight),v()}function v(){const e=t.clientWidth,o=t.clientHeight;!c||!a||!e||!o||(l=Math.min(e/c,o/a,1),m=(e-c*l)/2,p=(o-a*l)/2,x())}function x(){B.style.transform=`translate(${m}px, ${p}px) scale(${l})`,B.style.width=`${c}px`,B.style.height=`${a}px`,d.style.width=f.style.width=`${c}px`,d.style.height=f.style.height=`${a}px`,O(),F.textContent=`${Math.round(l*100)}%`,t.classList.toggle("can-pan",l>Math.min(t.clientWidth/c,t.clientHeight/a,1)+.01)}function O(){const e=L*c;P.style.width=`${e}px`,R.style.left=`${e}px`}function u(e){S=e,t.classList.remove("mode-before","mode-after"),e!=="split"&&t.classList.add(`mode-${e}`),w.classList.toggle("active",e==="before"),C.classList.toggle("active",e==="split"),A.classList.toggle("active",e==="after")}w.addEventListener("click",()=>u("before"));C.addEventListener("click",()=>u("split"));A.addEventListener("click",()=>u("after"));function X(e,o,s){const r=o??t.clientWidth/2,n=s??t.clientHeight/2,i=(r-m)/l,b=(n-p)/l;l=Math.max(.1,Math.min(8,e)),m=r-i*l,p=n-b*l,x()}function h(e,o,s){const r=l;let n=r;if(e===1)n=E.find(i=>i>r+.01)??r*1.5;else{for(let i=E.length-1;i>=0;i--)if(E[i]<r-.01){n=E[i];break}n===r&&(n=r/1.5)}X(n,o,s)}T.addEventListener("click",()=>h(1));D.addEventListener("click",()=>h(-1));N.addEventListener("click",v);t.addEventListener("wheel",e=>{e.preventDefault();const o=e.deltaY<0?1:-1;h(o,e.clientX,e.clientY-t.getBoundingClientRect().top)},{passive:!1});let g=null,H=0,W=0,Y=0,z=0;function K(e){if(S!=="split")return!1;const o=t.getBoundingClientRect(),s=m+L*c*l;return Math.abs(e.clientX-o.left-s)<20}t.addEventListener("pointerdown",e=>{e.button===0&&(e.preventDefault(),t.setPointerCapture(e.pointerId),K(e)?(g="slider",t.classList.add("dragging-slider")):(g="pan",H=e.clientX,W=e.clientY,Y=m,z=p,t.classList.add("panning")))});t.addEventListener("pointermove",e=>{if(!g)return;const o=t.getBoundingClientRect();if(g==="slider"){const s=e.clientX-o.left;L=Math.max(0,Math.min(1,(s-m)/(c*l))),O()}else g==="pan"&&(m=Y+(e.clientX-H),p=z+(e.clientY-W),x())});t.addEventListener("pointerup",()=>{g=null,t.classList.remove("dragging-slider","panning")});t.addEventListener("dblclick",e=>{const o=t.getBoundingClientRect(),s=e.clientY-o.top,r=Math.min(t.clientWidth/c,t.clientHeight/a,1);Math.abs(l-r)<.05?X(2,e.clientX-o.left,s):v()});new ResizeObserver(()=>{l<=Math.min(t.clientWidth/c,t.clientHeight/a,1)+.05&&v()}).observe(t);document.addEventListener("keydown",e=>{e.key==="1"?u("before"):e.key==="2"?u("split"):e.key==="3"?u("after"):e.key==="+"||e.key==="="?h(1):e.key==="-"?h(-1):e.key==="0"&&v()});</script>
8
+ <style rel="stylesheet" crossorigin>*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}:root{--bg: #1a1a1a;--surface: #242424;--border: #333;--text: #e8e8e8;--text-muted: #888;--accent: #7c6bf5;--handle: #fff;--slider-line: rgba(255,255,255,.6)}html,body{height:100%;background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,system-ui,sans-serif;font-size:13px;overflow:hidden;user-select:none;-webkit-user-select:none}#app{display:flex;flex-direction:column;height:100%}#toolbar{background:var(--surface);border-bottom:1px solid var(--border);padding:8px 12px;display:flex;flex-direction:column;gap:6px;flex-shrink:0}#labels-bar{display:flex;justify-content:space-between}.label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted)}#controls{display:flex;align-items:center;gap:4px;justify-content:center;flex-wrap:wrap}#controls button{background:transparent;border:1px solid var(--border);color:var(--text);padding:4px 10px;border-radius:4px;cursor:pointer;font-size:12px;font-family:inherit;transition:all .15s}#controls button:hover{background:var(--border)}#controls button.active{background:var(--accent);border-color:var(--accent);color:#fff}.separator{width:1px;height:20px;background:var(--border);margin:0 4px}#zoom-level{font-size:12px;font-variant-numeric:tabular-nums;min-width:40px;text-align:center;color:var(--text-muted)}#viewport{flex:1;overflow:hidden;position:relative;cursor:default}#viewport.dragging-slider{cursor:ew-resize}#viewport.panning{cursor:grabbing}#viewport.can-pan{cursor:grab}#canvas{position:absolute;transform-origin:0 0}#canvas img{display:block;pointer-events:none;-webkit-user-drag:none}#img-after{position:relative}#clip-before{position:absolute;top:0;left:0;height:100%;overflow:hidden}#clip-before img{display:block}#slider{position:absolute;top:0;height:100%;width:3px;transform:translate(-50%);z-index:10;cursor:ew-resize}#slider-line{position:absolute;top:0;left:50%;transform:translate(-50%);width:2px;height:100%;background:var(--slider-line);box-shadow:0 0 6px #00000080}#slider-handle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:36px;height:36px;border-radius:50%;background:#000000b3;border:2px solid var(--handle);display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px #0006;cursor:ew-resize;transition:transform .1s}#slider-handle:hover{transform:translate(-50%,-50%) scale(1.1)}#meta-bar{background:var(--surface);border-top:1px solid var(--border);padding:4px 12px;display:flex;justify-content:space-between;font-size:11px;color:var(--text-muted);flex-shrink:0}.mode-before #clip-before{width:100%!important}.mode-before #slider{display:none}.mode-after #clip-before{width:0!important}.mode-after #slider{display:none}</style>
9
+ </head>
10
+ <body>
11
+ <div id="app">
12
+ <div id="toolbar">
13
+ <div id="labels-bar">
14
+ <span id="label-before" class="label">Before</span>
15
+ <span id="label-after" class="label">After</span>
16
+ </div>
17
+ <div id="controls">
18
+ <button id="btn-before" title="Show Before">◀ Before</button>
19
+ <button id="btn-split" class="active" title="Split View">Split</button>
20
+ <button id="btn-after" title="Show After">After ▶</button>
21
+ <span class="separator"></span>
22
+ <button id="btn-zoom-in" title="Zoom In">+</button>
23
+ <span id="zoom-level">100%</span>
24
+ <button id="btn-zoom-out" title="Zoom Out">−</button>
25
+ <button id="btn-zoom-fit" title="Fit">Fit</button>
26
+ </div>
27
+ </div>
28
+ <div id="viewport">
29
+ <div id="canvas">
30
+ <img id="img-after" alt="After" />
31
+ <div id="clip-before">
32
+ <img id="img-before" alt="Before" />
33
+ </div>
34
+ <div id="slider">
35
+ <div id="slider-line"></div>
36
+ <div id="slider-handle">
37
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none">
38
+ <path d="M9 18L3 12L9 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
39
+ <path d="M15 6L21 12L15 18" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
40
+ </svg>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ <div id="meta-bar">
46
+ <span id="meta-before"></span>
47
+ <span id="meta-after"></span>
48
+ </div>
49
+ </div>
50
+ </body>
51
+ </html>
@@ -0,0 +1,9 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /**
3
+ * Create and start the MCP server in the appropriate transport mode.
4
+ *
5
+ * - If `--http` flag or `PORT` env var is set → HTTP mode (strict path security)
6
+ * - Otherwise → stdio mode (CWD + temp allowed)
7
+ */
8
+ export declare function createMain(createServer: () => McpServer): Promise<void>;
9
+ //# sourceMappingURL=create-main.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-main.d.ts","sourceRoot":"","sources":["../../src/lib/create-main.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIzE;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,YAAY,EAAE,MAAM,SAAS,GAC5B,OAAO,CAAC,IAAI,CAAC,CAYf"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Entry-point factory — handles stdio vs HTTP transport selection,
3
+ * configures path security, and starts the server.
4
+ */
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { startHttpServer } from "./http-server.js";
7
+ import { configurePathSecurity } from "./path-security.js";
8
+ /**
9
+ * Create and start the MCP server in the appropriate transport mode.
10
+ *
11
+ * - If `--http` flag or `PORT` env var is set → HTTP mode (strict path security)
12
+ * - Otherwise → stdio mode (CWD + temp allowed)
13
+ */
14
+ export async function createMain(createServer) {
15
+ const httpMode = process.argv.includes("--http") || !!process.env.PORT;
16
+ if (httpMode) {
17
+ configurePathSecurity({ mode: "http" });
18
+ await startHttpServer({ createServer });
19
+ }
20
+ else {
21
+ configurePathSecurity({ mode: "stdio" });
22
+ const server = createServer();
23
+ const transport = new StdioServerTransport();
24
+ await server.connect(transport);
25
+ }
26
+ }
27
+ //# sourceMappingURL=create-main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-main.js","sourceRoot":"","sources":["../../src/lib/create-main.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAE3D;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,YAA6B;IAE7B,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;IAEvE,IAAI,QAAQ,EAAE,CAAC;QACb,qBAAqB,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;QACxC,MAAM,eAAe,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC;IAC1C,CAAC;SAAM,CAAC;QACN,qBAAqB,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QACzC,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;QAC9B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;QAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAClC,CAAC;AACH,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Shared MCP tool result helpers.
3
+ */
4
+ export interface ToolResult {
5
+ [key: string]: unknown;
6
+ content: Array<{
7
+ type: "text";
8
+ text: string;
9
+ }>;
10
+ isError?: boolean;
11
+ }
12
+ /** Create a standard MCP error tool result with isError: true */
13
+ export declare function toolError(message: string): ToolResult;
14
+ /** Create a standard MCP success tool result */
15
+ export declare function toolResult(text: string): ToolResult;
16
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,UAAU;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,iEAAiE;AACjE,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAKrD;AAED,gDAAgD;AAChD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAInD"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Shared MCP tool result helpers.
3
+ */
4
+ /** Create a standard MCP error tool result with isError: true */
5
+ export function toolError(message) {
6
+ return {
7
+ content: [{ type: "text", text: message }],
8
+ isError: true,
9
+ };
10
+ }
11
+ /** Create a standard MCP success tool result */
12
+ export function toolResult(text) {
13
+ return {
14
+ content: [{ type: "text", text }],
15
+ };
16
+ }
17
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAQH,iEAAiE;AACjE,MAAM,UAAU,SAAS,CAAC,OAAe;IACvC,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;QAC1C,OAAO,EAAE,IAAI;KACd,CAAC;AACJ,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;KAClC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,18 @@
1
+ export declare function validateUrl(url: string): Promise<void>;
2
+ export interface FetchUrlOptions {
3
+ /** Max response size in bytes (default: 100 MB) */
4
+ maxBytes?: number;
5
+ /** Request timeout in ms (default: 30 000) */
6
+ timeoutMs?: number;
7
+ }
8
+ /**
9
+ * Fetch a URL with SSRF protection, size limits, timeout, and redirect safety.
10
+ * Connects directly to the resolved IP to prevent DNS rebinding (TOCTOU) attacks.
11
+ */
12
+ export declare function fetchUrl(url: string, options?: FetchUrlOptions, _redirectCount?: number): Promise<Buffer>;
13
+ /**
14
+ * Load an image from a URL or local path.
15
+ * URLs go through SSRF-safe fetchUrl; paths are validated by the caller.
16
+ */
17
+ export declare function loadImage(source: string, options?: FetchUrlOptions): Promise<Buffer>;
18
+ //# sourceMappingURL=fetch-url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-url.d.ts","sourceRoot":"","sources":["../../src/lib/fetch-url.ts"],"names":[],"mappings":"AAoEA,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA+C5D;AAMD,MAAM,WAAW,eAAe;IAC9B,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA2BD;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,eAAoB,EAC7B,cAAc,SAAI,GACjB,OAAO,CAAC,MAAM,CAAC,CA6GjB;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,MAAM,CAAC,CAQjB"}