@epcc-sdk/sdks-files 0.2.0 → 0.4.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.
- package/README.md +74 -1
- package/dist/client/mcp-support.gen.cjs +209 -0
- package/dist/client/mcp-support.gen.cjs.map +1 -0
- package/dist/client/mcp-support.gen.d.cts +42 -0
- package/dist/client/mcp-support.gen.d.ts +42 -0
- package/dist/client/mcp-support.gen.mjs +172 -0
- package/dist/client/mcp-support.gen.mjs.map +1 -0
- package/dist/client/mcp.gen.cjs +203 -0
- package/dist/client/mcp.gen.cjs.map +1 -0
- package/dist/client/mcp.gen.d.cts +967 -0
- package/dist/client/mcp.gen.d.ts +967 -0
- package/dist/client/mcp.gen.mjs +175 -0
- package/dist/client/mcp.gen.mjs.map +1 -0
- package/dist/client/types.gen.cjs.map +1 -1
- package/dist/client/types.gen.d.cts +4 -4
- package/dist/client/types.gen.d.ts +4 -4
- package/dist/client/zod.gen.cjs +160 -70
- package/dist/client/zod.gen.cjs.map +1 -1
- package/dist/client/zod.gen.d.cts +83 -769
- package/dist/client/zod.gen.d.ts +83 -769
- package/dist/client/zod.gen.mjs +113 -33
- package/dist/client/zod.gen.mjs.map +1 -1
- package/dist/mcp.cjs +23 -0
- package/dist/mcp.cjs.map +1 -0
- package/dist/mcp.d.cts +14 -0
- package/dist/mcp.d.ts +14 -0
- package/dist/mcp.mjs +2 -0
- package/dist/mcp.mjs.map +1 -0
- package/dist/zod.cjs.map +1 -1
- package/dist/zod.mjs.map +1 -1
- package/package.json +25 -4
package/README.md
CHANGED
|
@@ -173,7 +173,7 @@ interceptor: an interceptor cannot see the response, so it can never retry a 401
|
|
|
173
173
|
## Validation schemas
|
|
174
174
|
|
|
175
175
|
Zod schemas for every request body, path and response are published under the `/zod`
|
|
176
|
-
subpath. `zod` is an optional peer dependency (
|
|
176
|
+
subpath. `zod` is an optional peer dependency (4.x), so the root entry never imports it.
|
|
177
177
|
|
|
178
178
|
```ts
|
|
179
179
|
import { zFile } from "@epcc-sdk/sdks-files/zod";
|
|
@@ -181,6 +181,79 @@ import { zFile } from "@epcc-sdk/sdks-files/zod";
|
|
|
181
181
|
const file = zFile.parse(data?.data?.[0]);
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
+
## MCP endpoint tools
|
|
185
|
+
|
|
186
|
+
The `/mcp` subpath turns each of the 4 files operations into a Model Context Protocol (MCP) tool.
|
|
187
|
+
The main entry imports none of it. `zod` 4 and `@modelcontextprotocol/sdk` are needed only for this
|
|
188
|
+
subpath.
|
|
189
|
+
|
|
190
|
+
Each operation has one tool factory, named after its function with `Tool` added, such as
|
|
191
|
+
`getAllFilesTool`. A factory returns `{ name, config, handler }`, the three arguments that
|
|
192
|
+
`McpServer.registerTool` takes. `tools()` returns all 4 in operation order. A handler returns an API
|
|
193
|
+
error as an `isError` result and does not throw.
|
|
194
|
+
|
|
195
|
+
Register one tool:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
|
|
199
|
+
import { getAllFilesTool } from "@epcc-sdk/sdks-files/mcp"
|
|
200
|
+
|
|
201
|
+
const server = new McpServer({ name: "files", version: "1.0.0" })
|
|
202
|
+
const tool = getAllFilesTool()
|
|
203
|
+
server.registerTool(tool.name, tool.config, tool.handler)
|
|
204
|
+
```
|
|
205
|
+
A tool built without a client calls through the package's module level `client`, like an operation
|
|
206
|
+
does. That client has no credentials until you configure it. Configure it once:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { client, createFilesClient } from "@epcc-sdk/sdks-files"
|
|
210
|
+
|
|
211
|
+
client.setConfig(createFilesClient({ baseUrl, clientId, clientSecret }).getConfig())
|
|
212
|
+
```
|
|
213
|
+
Register every tool, with a client that sends the same headers on every call:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import * as files from "@epcc-sdk/sdks-files/mcp"
|
|
217
|
+
|
|
218
|
+
const client = createFilesClient({
|
|
219
|
+
baseUrl,
|
|
220
|
+
clientId,
|
|
221
|
+
clientSecret,
|
|
222
|
+
config: { headers: { "EP-Store-Id": storeId, "EP-Org-Id": orgId } },
|
|
223
|
+
})
|
|
224
|
+
files
|
|
225
|
+
.tools({ client })
|
|
226
|
+
.forEach((t) => server.registerTool(t.name, t.config, t.handler))
|
|
227
|
+
```
|
|
228
|
+
Register the read-only tools only:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
files
|
|
232
|
+
.tools({ client })
|
|
233
|
+
.filter((t) => t.config._meta["com.elasticpath/operation"].risk === "read")
|
|
234
|
+
.forEach((t) => server.registerTool(t.name, t.config, t.handler))
|
|
235
|
+
```
|
|
236
|
+
Each tool is named `files_<snake_case_function>`, and its arguments are grouped under `path`,
|
|
237
|
+
`query`, `headers` and `body`. The `path`, `query` and `headers` groups reject keys the spec does not
|
|
238
|
+
list. The `body` keeps them, so an attribute the spec does not list still reaches the API. The client
|
|
239
|
+
supplies the `Authorization` header, so no tool asks for it. Cancelling the MCP request cancels the
|
|
240
|
+
HTTP request.
|
|
241
|
+
|
|
242
|
+
`files_create_a_file` uploads a file. Its `file` field takes the file as a `data:` URI, such as
|
|
243
|
+
`data:image/png;base64,iVBORw0KGgo…`, and `file_name` takes its name, such as `photo.png`. The tool
|
|
244
|
+
decodes the file and sends it with that type and name. It accepts a file of at most 8,388,608 bytes (8 MiB), the API's default
|
|
245
|
+
limit, and returns an `isError` result for a larger file, a malformed URI, or a URL, without sending
|
|
246
|
+
anything. It never downloads a URL. The field carries `x-mcp-file: { maxSize: 8388608 }`, so a host that recognises the MCP file-input marker can offer a file picker. The design follows the MCP File Uploads Working Group's drafts; see [File uploads](../README.md#file-uploads) in the SDKs README for its sources. To register a file hosted elsewhere, send its URL in
|
|
247
|
+
`file_location`, which the API fetches.
|
|
248
|
+
|
|
249
|
+
Each tool's description is written for a model choosing between tools: what the tool does, when to
|
|
250
|
+
use it rather than a similar tool, and any caveat that changes when to call it, in at most 600
|
|
251
|
+
characters. The full spec description stays in the doc comment of the operation's function and its
|
|
252
|
+
tool factory.
|
|
253
|
+
|
|
254
|
+
`config._meta["com.elasticpath/operation"]` holds the operation's `id`, `method`, `path` and `risk`.
|
|
255
|
+
The risk is `read`, `create`, `update` or `delete`, and the annotations follow from it.
|
|
256
|
+
|
|
184
257
|
## Build URL
|
|
185
258
|
|
|
186
259
|
If you need to access the compiled URL, you can use the buildUrl() method. It's loosely typed by default to accept almost any value; in practice, you will want to pass a type hint.
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var mcp_support_gen_exports = {};
|
|
30
|
+
__export(mcp_support_gen_exports, {
|
|
31
|
+
dataUriFields: () => dataUriFields,
|
|
32
|
+
decodeDataUris: () => decodeDataUris,
|
|
33
|
+
loose: () => loose,
|
|
34
|
+
toToolResult: () => toToolResult
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(mcp_support_gen_exports);
|
|
37
|
+
var z = __toESM(require("zod"), 1);
|
|
38
|
+
const messageOf = (error) => error instanceof Error ? error.message : JSON.stringify(error);
|
|
39
|
+
const toToolResult = async (request) => {
|
|
40
|
+
let text;
|
|
41
|
+
try {
|
|
42
|
+
const result = await request;
|
|
43
|
+
if (result.error === void 0) {
|
|
44
|
+
const structuredContent = typeof result.data === "object" && result.data !== null && !Array.isArray(result.data) ? result.data : { data: result.data ?? null };
|
|
45
|
+
return {
|
|
46
|
+
structuredContent,
|
|
47
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent) }]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
text = result.response ? `HTTP ${result.response.status}: ${JSON.stringify(result.error)}` : messageOf(result.error);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
text = messageOf(error);
|
|
53
|
+
}
|
|
54
|
+
return { isError: true, content: [{ type: "text", text }] };
|
|
55
|
+
};
|
|
56
|
+
const loose = (schema) => {
|
|
57
|
+
const def = schema._zod.def;
|
|
58
|
+
const copy = (changes) => schema.clone({ ...def, ...changes }, { parent: true });
|
|
59
|
+
if (def.type === "object") {
|
|
60
|
+
return copy({
|
|
61
|
+
catchall: z.unknown(),
|
|
62
|
+
shape: Object.fromEntries(
|
|
63
|
+
Object.keys(def.shape).map((key) => [key, loose(def.shape[key])])
|
|
64
|
+
)
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (def.type === "array") {
|
|
68
|
+
return copy({ element: loose(def.element) });
|
|
69
|
+
}
|
|
70
|
+
if (def.type === "record") {
|
|
71
|
+
return copy({ valueType: loose(def.valueType) });
|
|
72
|
+
}
|
|
73
|
+
if (def.type === "lazy") {
|
|
74
|
+
return copy({ getter: () => loose(def.getter()) });
|
|
75
|
+
}
|
|
76
|
+
if ([
|
|
77
|
+
"optional",
|
|
78
|
+
"nullable",
|
|
79
|
+
"default",
|
|
80
|
+
"readonly",
|
|
81
|
+
"nonoptional",
|
|
82
|
+
"catch",
|
|
83
|
+
"prefault"
|
|
84
|
+
].includes(def.type)) {
|
|
85
|
+
return copy({ innerType: loose(def.innerType) });
|
|
86
|
+
}
|
|
87
|
+
if (def.type === "union") {
|
|
88
|
+
return copy({ options: def.options.map(loose) });
|
|
89
|
+
}
|
|
90
|
+
if (def.type === "intersection") {
|
|
91
|
+
return copy({ left: loose(def.left), right: loose(def.right) });
|
|
92
|
+
}
|
|
93
|
+
return schema;
|
|
94
|
+
};
|
|
95
|
+
const dataUriFields = (schema, fields, maxBytes) => schema.extend(
|
|
96
|
+
Object.fromEntries(
|
|
97
|
+
fields.flatMap((field) => {
|
|
98
|
+
const current = schema.shape[field];
|
|
99
|
+
const uri = z.string().meta({
|
|
100
|
+
format: "uri",
|
|
101
|
+
"x-mcp-file": { maxSize: maxBytes },
|
|
102
|
+
description: [
|
|
103
|
+
current.description,
|
|
104
|
+
`Send the file as a data: URI, such as data:text/csv;base64,<base64 bytes>, at most ${maxBytes} bytes once decoded.`
|
|
105
|
+
].filter(Boolean).join(" ")
|
|
106
|
+
});
|
|
107
|
+
const name = z.string().meta({
|
|
108
|
+
description: `The name of the file sent in ${field}, such as products.csv.`
|
|
109
|
+
}).optional();
|
|
110
|
+
return [
|
|
111
|
+
[field, current.isOptional() ? uri.optional() : uri],
|
|
112
|
+
[`${field}_name`, name]
|
|
113
|
+
];
|
|
114
|
+
})
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
const DATA_URI = /^data:([^,]*),(.*)$/is;
|
|
118
|
+
const PERCENT_ENCODED = /^[0-9a-f]{2}$/i;
|
|
119
|
+
const notADataUriError = (field) => new Error(
|
|
120
|
+
`${field} must be a data: URI, such as data:image/png;base64,iVBORw0KGgo. This tool does not download URLs.`
|
|
121
|
+
);
|
|
122
|
+
const fileTooBigError = (field, bytes, maxBytes) => new Error(
|
|
123
|
+
`${field} is ${bytes} bytes, over the ${maxBytes} bytes this tool accepts`
|
|
124
|
+
);
|
|
125
|
+
function percentDecoded(field, text) {
|
|
126
|
+
const bytes = [];
|
|
127
|
+
const encoder = new TextEncoder();
|
|
128
|
+
for (let index = 0; index < text.length; index++) {
|
|
129
|
+
if (text[index] === "%") {
|
|
130
|
+
const hex = text.slice(index + 1, index + 3);
|
|
131
|
+
if (!PERCENT_ENCODED.test(hex)) throw notADataUriError(field);
|
|
132
|
+
bytes.push(parseInt(hex, 16));
|
|
133
|
+
index += 2;
|
|
134
|
+
} else {
|
|
135
|
+
const codePoint = text.codePointAt(index);
|
|
136
|
+
const character = String.fromCodePoint(codePoint);
|
|
137
|
+
bytes.push(...encoder.encode(character));
|
|
138
|
+
index += character.length - 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return new Uint8Array(bytes);
|
|
142
|
+
}
|
|
143
|
+
function base64Decoded(field, text, maxBytes) {
|
|
144
|
+
const data = text.replace(/\s/g, "");
|
|
145
|
+
const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
|
|
146
|
+
const bytes = Math.floor(data.length * 3 / 4) - padding;
|
|
147
|
+
if (bytes > maxBytes) throw fileTooBigError(field, bytes, maxBytes);
|
|
148
|
+
let binary;
|
|
149
|
+
try {
|
|
150
|
+
binary = atob(data);
|
|
151
|
+
} catch {
|
|
152
|
+
throw new Error(`${field} is not valid base64`);
|
|
153
|
+
}
|
|
154
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
155
|
+
}
|
|
156
|
+
function fileFromDataUri(field, uri, maxBytes, givenName) {
|
|
157
|
+
const match = typeof uri === "string" ? DATA_URI.exec(uri) : null;
|
|
158
|
+
if (!match) throw notADataUriError(field);
|
|
159
|
+
const parameters = match[1].split(";");
|
|
160
|
+
const isBase64 = parameters.at(-1).toLowerCase() === "base64";
|
|
161
|
+
if (isBase64) parameters.pop();
|
|
162
|
+
const mediaType = parameters.shift().trim();
|
|
163
|
+
const nameParameter = parameters.find(
|
|
164
|
+
(parameter) => parameter.trim().toLowerCase().startsWith("name=")
|
|
165
|
+
);
|
|
166
|
+
let name = field;
|
|
167
|
+
if (typeof givenName === "string" && givenName.trim()) {
|
|
168
|
+
name = givenName.trim();
|
|
169
|
+
} else if (nameParameter) {
|
|
170
|
+
try {
|
|
171
|
+
name = decodeURIComponent(nameParameter.trim().slice("name=".length));
|
|
172
|
+
} catch {
|
|
173
|
+
throw notADataUriError(field);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const bytes = isBase64 ? base64Decoded(field, match[2], maxBytes) : percentDecoded(field, match[2]);
|
|
177
|
+
if (bytes.length > maxBytes)
|
|
178
|
+
throw fileTooBigError(field, bytes.length, maxBytes);
|
|
179
|
+
return new File([bytes], name, {
|
|
180
|
+
type: mediaType.includes("/") ? mediaType : "text/plain"
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const decodeDataUris = async (body, fields, maxBytes) => {
|
|
184
|
+
if (typeof body !== "object" || body === null) {
|
|
185
|
+
return body;
|
|
186
|
+
}
|
|
187
|
+
const decoded = { ...body };
|
|
188
|
+
for (const field of fields) {
|
|
189
|
+
const givenName = decoded[`${field}_name`];
|
|
190
|
+
delete decoded[`${field}_name`];
|
|
191
|
+
if (decoded[field] !== void 0) {
|
|
192
|
+
decoded[field] = fileFromDataUri(
|
|
193
|
+
field,
|
|
194
|
+
decoded[field],
|
|
195
|
+
maxBytes,
|
|
196
|
+
givenName
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return decoded;
|
|
201
|
+
};
|
|
202
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
203
|
+
0 && (module.exports = {
|
|
204
|
+
dataUriFields,
|
|
205
|
+
decodeDataUris,
|
|
206
|
+
loose,
|
|
207
|
+
toToolResult
|
|
208
|
+
});
|
|
209
|
+
//# sourceMappingURL=mcp-support.gen.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/client/mcp-support.gen.ts"],"sourcesContent":["import type { ToolCallback } from \"@modelcontextprotocol/sdk/server/mcp.js\"\nimport type { AnySchema } from \"@modelcontextprotocol/sdk/server/zod-compat.js\"\nimport type {\n CallToolResult,\n ToolAnnotations,\n} from \"@modelcontextprotocol/sdk/types.js\"\nimport * as z from \"zod\"\n\nexport type OperationMeta = {\n id: string\n method: string\n path: string\n risk: \"read\" | \"create\" | \"update\" | \"delete\"\n}\n\nexport type ToolConfig = {\n title?: string\n description?: string\n inputSchema: z.ZodObject<Record<string, z.ZodType<any>>>\n outputSchema?: AnySchema\n annotations?: ToolAnnotations\n _meta: {\n \"com.elasticpath/operation\": OperationMeta\n }\n}\n\nexport type Tool = {\n readonly name: string\n readonly config: ToolConfig\n readonly handler: ToolCallback<AnySchema>\n}\n\nconst messageOf = (error: unknown) =>\n error instanceof Error ? error.message : JSON.stringify(error)\n\nexport const toToolResult = async (\n request: Promise<{\n data?: unknown\n error?: unknown\n response?: Response\n }>,\n): Promise<CallToolResult> => {\n let text: string\n try {\n const result = await request\n if (result.error === undefined) {\n const structuredContent =\n typeof result.data === \"object\" &&\n result.data !== null &&\n !Array.isArray(result.data)\n ? (result.data as Record<string, unknown>)\n : { data: result.data ?? null }\n return {\n structuredContent,\n content: [{ type: \"text\", text: JSON.stringify(structuredContent) }],\n }\n }\n text = result.response\n ? `HTTP ${result.response.status}: ${JSON.stringify(result.error)}`\n : messageOf(result.error)\n } catch (error) {\n text = messageOf(error)\n }\n return { isError: true, content: [{ type: \"text\", text }] }\n}\n\nexport const loose = <T extends z.ZodType>(schema: T): T => {\n const def: any = schema._zod.def\n const copy = (changes: object): T =>\n schema.clone({ ...def, ...changes }, { parent: true })\n if (def.type === \"object\") {\n return copy({\n catchall: z.unknown(),\n shape: Object.fromEntries(\n Object.keys(def.shape).map((key) => [key, loose(def.shape[key])]),\n ),\n })\n }\n if (def.type === \"array\") {\n return copy({ element: loose(def.element) })\n }\n if (def.type === \"record\") {\n return copy({ valueType: loose(def.valueType) })\n }\n if (def.type === \"lazy\") {\n return copy({ getter: () => loose(def.getter()) })\n }\n if (\n [\n \"optional\",\n \"nullable\",\n \"default\",\n \"readonly\",\n \"nonoptional\",\n \"catch\",\n \"prefault\",\n ].includes(def.type)\n ) {\n return copy({ innerType: loose(def.innerType) })\n }\n if (def.type === \"union\") {\n return copy({ options: def.options.map(loose) })\n }\n if (def.type === \"intersection\") {\n return copy({ left: loose(def.left), right: loose(def.right) })\n }\n return schema\n}\n\ntype FileNameFields<K extends string> = {\n [P in `${K}_name`]: z.ZodOptional<z.ZodString>\n}\n\nexport const dataUriFields = <\n S extends z.ZodRawShape,\n C extends z.core.$ZodObjectConfig,\n K extends string,\n>(\n schema: z.ZodObject<S, C>,\n fields: ReadonlyArray<K>,\n maxBytes: number,\n): z.ZodObject<z.core.util.Extend<S, FileNameFields<K>>, C> =>\n schema.extend(\n Object.fromEntries(\n fields.flatMap((field) => {\n const current = schema.shape[field] as unknown as z.ZodType\n const uri = z.string().meta({\n format: \"uri\",\n \"x-mcp-file\": { maxSize: maxBytes },\n description: [\n current.description,\n `Send the file as a data: URI, such as data:text/csv;base64,<base64 bytes>, at most ${maxBytes} bytes once decoded.`,\n ]\n .filter(Boolean)\n .join(\" \"),\n })\n const name = z\n .string()\n .meta({\n description: `The name of the file sent in ${field}, such as products.csv.`,\n })\n .optional()\n return [\n [field, current.isOptional() ? uri.optional() : uri],\n [`${field}_name`, name],\n ]\n }),\n ),\n ) as unknown as z.ZodObject<z.core.util.Extend<S, FileNameFields<K>>, C>\n\ntype DataUrisDecoded<T, K extends string> = T extends object\n ? Omit<T, K | `${K}_name`> & {\n [P in K & keyof T]: undefined extends T[P] ? File | undefined : File\n }\n : T\n\nconst DATA_URI = /^data:([^,]*),(.*)$/is\nconst PERCENT_ENCODED = /^[0-9a-f]{2}$/i\n\nconst notADataUriError = (field: string) =>\n new Error(\n `${field} must be a data: URI, such as data:image/png;base64,iVBORw0KGgo. This tool does not download URLs.`,\n )\n\nconst fileTooBigError = (field: string, bytes: number, maxBytes: number) =>\n new Error(\n `${field} is ${bytes} bytes, over the ${maxBytes} bytes this tool accepts`,\n )\n\nfunction percentDecoded(field: string, text: string) {\n const bytes: number[] = []\n const encoder = new TextEncoder()\n for (let index = 0; index < text.length; index++) {\n if (text[index] === \"%\") {\n const hex = text.slice(index + 1, index + 3)\n if (!PERCENT_ENCODED.test(hex)) throw notADataUriError(field)\n bytes.push(parseInt(hex, 16))\n index += 2\n } else {\n const codePoint = text.codePointAt(index)!\n const character = String.fromCodePoint(codePoint)\n bytes.push(...encoder.encode(character))\n index += character.length - 1\n }\n }\n return new Uint8Array(bytes)\n}\n\nfunction base64Decoded(field: string, text: string, maxBytes: number) {\n const data = text.replace(/\\s/g, \"\")\n const padding = data.endsWith(\"==\") ? 2 : data.endsWith(\"=\") ? 1 : 0\n const bytes = Math.floor((data.length * 3) / 4) - padding\n if (bytes > maxBytes) throw fileTooBigError(field, bytes, maxBytes)\n let binary: string\n try {\n binary = atob(data)\n } catch {\n throw new Error(`${field} is not valid base64`)\n }\n return Uint8Array.from(binary, (character) => character.charCodeAt(0))\n}\n\nfunction fileFromDataUri(\n field: string,\n uri: unknown,\n maxBytes: number,\n givenName: unknown,\n) {\n const match = typeof uri === \"string\" ? DATA_URI.exec(uri) : null\n if (!match) throw notADataUriError(field)\n const parameters = match[1]!.split(\";\")\n const isBase64 = parameters.at(-1)!.toLowerCase() === \"base64\"\n if (isBase64) parameters.pop()\n const mediaType = parameters.shift()!.trim()\n const nameParameter = parameters.find((parameter) =>\n parameter.trim().toLowerCase().startsWith(\"name=\"),\n )\n let name = field\n if (typeof givenName === \"string\" && givenName.trim()) {\n name = givenName.trim()\n } else if (nameParameter) {\n try {\n name = decodeURIComponent(nameParameter.trim().slice(\"name=\".length))\n } catch {\n throw notADataUriError(field)\n }\n }\n const bytes = isBase64\n ? base64Decoded(field, match[2]!, maxBytes)\n : percentDecoded(field, match[2]!)\n if (bytes.length > maxBytes)\n throw fileTooBigError(field, bytes.length, maxBytes)\n return new File([bytes], name, {\n type: mediaType.includes(\"/\") ? mediaType : \"text/plain\",\n })\n}\n\nexport const decodeDataUris = async <T, K extends string>(\n body: T,\n fields: ReadonlyArray<K>,\n maxBytes: number,\n): Promise<DataUrisDecoded<T, K>> => {\n if (typeof body !== \"object\" || body === null) {\n return body as DataUrisDecoded<T, K>\n }\n const decoded = { ...body } as Record<string, unknown>\n for (const field of fields) {\n const givenName = decoded[`${field}_name`]\n delete decoded[`${field}_name`]\n if (decoded[field] !== undefined) {\n decoded[field] = fileFromDataUri(\n field,\n decoded[field],\n maxBytes,\n givenName,\n )\n }\n }\n return decoded as DataUrisDecoded<T, K>\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { AnySchema } from '@modelcontextprotocol/sdk/server/zod-compat.js';
|
|
3
|
+
import { ToolAnnotations, CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
|
|
6
|
+
type OperationMeta = {
|
|
7
|
+
id: string;
|
|
8
|
+
method: string;
|
|
9
|
+
path: string;
|
|
10
|
+
risk: "read" | "create" | "update" | "delete";
|
|
11
|
+
};
|
|
12
|
+
type ToolConfig = {
|
|
13
|
+
title?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
inputSchema: z.ZodObject<Record<string, z.ZodType<any>>>;
|
|
16
|
+
outputSchema?: AnySchema;
|
|
17
|
+
annotations?: ToolAnnotations;
|
|
18
|
+
_meta: {
|
|
19
|
+
"com.elasticpath/operation": OperationMeta;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
type Tool = {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly config: ToolConfig;
|
|
25
|
+
readonly handler: ToolCallback<AnySchema>;
|
|
26
|
+
};
|
|
27
|
+
declare const toToolResult: (request: Promise<{
|
|
28
|
+
data?: unknown;
|
|
29
|
+
error?: unknown;
|
|
30
|
+
response?: Response;
|
|
31
|
+
}>) => Promise<CallToolResult>;
|
|
32
|
+
declare const loose: <T extends z.ZodType>(schema: T) => T;
|
|
33
|
+
type FileNameFields<K extends string> = {
|
|
34
|
+
[P in `${K}_name`]: z.ZodOptional<z.ZodString>;
|
|
35
|
+
};
|
|
36
|
+
declare const dataUriFields: <S extends z.ZodRawShape, C extends z.core.$ZodObjectConfig, K extends string>(schema: z.ZodObject<S, C>, fields: ReadonlyArray<K>, maxBytes: number) => z.ZodObject<z.core.util.Extend<S, FileNameFields<K>>, C>;
|
|
37
|
+
type DataUrisDecoded<T, K extends string> = T extends object ? Omit<T, K | `${K}_name`> & {
|
|
38
|
+
[P in K & keyof T]: undefined extends T[P] ? File | undefined : File;
|
|
39
|
+
} : T;
|
|
40
|
+
declare const decodeDataUris: <T, K extends string>(body: T, fields: ReadonlyArray<K>, maxBytes: number) => Promise<DataUrisDecoded<T, K>>;
|
|
41
|
+
|
|
42
|
+
export { type OperationMeta, type Tool, type ToolConfig, dataUriFields, decodeDataUris, loose, toToolResult };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { AnySchema } from '@modelcontextprotocol/sdk/server/zod-compat.js';
|
|
3
|
+
import { ToolAnnotations, CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
|
|
6
|
+
type OperationMeta = {
|
|
7
|
+
id: string;
|
|
8
|
+
method: string;
|
|
9
|
+
path: string;
|
|
10
|
+
risk: "read" | "create" | "update" | "delete";
|
|
11
|
+
};
|
|
12
|
+
type ToolConfig = {
|
|
13
|
+
title?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
inputSchema: z.ZodObject<Record<string, z.ZodType<any>>>;
|
|
16
|
+
outputSchema?: AnySchema;
|
|
17
|
+
annotations?: ToolAnnotations;
|
|
18
|
+
_meta: {
|
|
19
|
+
"com.elasticpath/operation": OperationMeta;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
type Tool = {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly config: ToolConfig;
|
|
25
|
+
readonly handler: ToolCallback<AnySchema>;
|
|
26
|
+
};
|
|
27
|
+
declare const toToolResult: (request: Promise<{
|
|
28
|
+
data?: unknown;
|
|
29
|
+
error?: unknown;
|
|
30
|
+
response?: Response;
|
|
31
|
+
}>) => Promise<CallToolResult>;
|
|
32
|
+
declare const loose: <T extends z.ZodType>(schema: T) => T;
|
|
33
|
+
type FileNameFields<K extends string> = {
|
|
34
|
+
[P in `${K}_name`]: z.ZodOptional<z.ZodString>;
|
|
35
|
+
};
|
|
36
|
+
declare const dataUriFields: <S extends z.ZodRawShape, C extends z.core.$ZodObjectConfig, K extends string>(schema: z.ZodObject<S, C>, fields: ReadonlyArray<K>, maxBytes: number) => z.ZodObject<z.core.util.Extend<S, FileNameFields<K>>, C>;
|
|
37
|
+
type DataUrisDecoded<T, K extends string> = T extends object ? Omit<T, K | `${K}_name`> & {
|
|
38
|
+
[P in K & keyof T]: undefined extends T[P] ? File | undefined : File;
|
|
39
|
+
} : T;
|
|
40
|
+
declare const decodeDataUris: <T, K extends string>(body: T, fields: ReadonlyArray<K>, maxBytes: number) => Promise<DataUrisDecoded<T, K>>;
|
|
41
|
+
|
|
42
|
+
export { type OperationMeta, type Tool, type ToolConfig, dataUriFields, decodeDataUris, loose, toToolResult };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
const messageOf = (error) => error instanceof Error ? error.message : JSON.stringify(error);
|
|
3
|
+
const toToolResult = async (request) => {
|
|
4
|
+
let text;
|
|
5
|
+
try {
|
|
6
|
+
const result = await request;
|
|
7
|
+
if (result.error === void 0) {
|
|
8
|
+
const structuredContent = typeof result.data === "object" && result.data !== null && !Array.isArray(result.data) ? result.data : { data: result.data ?? null };
|
|
9
|
+
return {
|
|
10
|
+
structuredContent,
|
|
11
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent) }]
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
text = result.response ? `HTTP ${result.response.status}: ${JSON.stringify(result.error)}` : messageOf(result.error);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
text = messageOf(error);
|
|
17
|
+
}
|
|
18
|
+
return { isError: true, content: [{ type: "text", text }] };
|
|
19
|
+
};
|
|
20
|
+
const loose = (schema) => {
|
|
21
|
+
const def = schema._zod.def;
|
|
22
|
+
const copy = (changes) => schema.clone({ ...def, ...changes }, { parent: true });
|
|
23
|
+
if (def.type === "object") {
|
|
24
|
+
return copy({
|
|
25
|
+
catchall: z.unknown(),
|
|
26
|
+
shape: Object.fromEntries(
|
|
27
|
+
Object.keys(def.shape).map((key) => [key, loose(def.shape[key])])
|
|
28
|
+
)
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (def.type === "array") {
|
|
32
|
+
return copy({ element: loose(def.element) });
|
|
33
|
+
}
|
|
34
|
+
if (def.type === "record") {
|
|
35
|
+
return copy({ valueType: loose(def.valueType) });
|
|
36
|
+
}
|
|
37
|
+
if (def.type === "lazy") {
|
|
38
|
+
return copy({ getter: () => loose(def.getter()) });
|
|
39
|
+
}
|
|
40
|
+
if ([
|
|
41
|
+
"optional",
|
|
42
|
+
"nullable",
|
|
43
|
+
"default",
|
|
44
|
+
"readonly",
|
|
45
|
+
"nonoptional",
|
|
46
|
+
"catch",
|
|
47
|
+
"prefault"
|
|
48
|
+
].includes(def.type)) {
|
|
49
|
+
return copy({ innerType: loose(def.innerType) });
|
|
50
|
+
}
|
|
51
|
+
if (def.type === "union") {
|
|
52
|
+
return copy({ options: def.options.map(loose) });
|
|
53
|
+
}
|
|
54
|
+
if (def.type === "intersection") {
|
|
55
|
+
return copy({ left: loose(def.left), right: loose(def.right) });
|
|
56
|
+
}
|
|
57
|
+
return schema;
|
|
58
|
+
};
|
|
59
|
+
const dataUriFields = (schema, fields, maxBytes) => schema.extend(
|
|
60
|
+
Object.fromEntries(
|
|
61
|
+
fields.flatMap((field) => {
|
|
62
|
+
const current = schema.shape[field];
|
|
63
|
+
const uri = z.string().meta({
|
|
64
|
+
format: "uri",
|
|
65
|
+
"x-mcp-file": { maxSize: maxBytes },
|
|
66
|
+
description: [
|
|
67
|
+
current.description,
|
|
68
|
+
`Send the file as a data: URI, such as data:text/csv;base64,<base64 bytes>, at most ${maxBytes} bytes once decoded.`
|
|
69
|
+
].filter(Boolean).join(" ")
|
|
70
|
+
});
|
|
71
|
+
const name = z.string().meta({
|
|
72
|
+
description: `The name of the file sent in ${field}, such as products.csv.`
|
|
73
|
+
}).optional();
|
|
74
|
+
return [
|
|
75
|
+
[field, current.isOptional() ? uri.optional() : uri],
|
|
76
|
+
[`${field}_name`, name]
|
|
77
|
+
];
|
|
78
|
+
})
|
|
79
|
+
)
|
|
80
|
+
);
|
|
81
|
+
const DATA_URI = /^data:([^,]*),(.*)$/is;
|
|
82
|
+
const PERCENT_ENCODED = /^[0-9a-f]{2}$/i;
|
|
83
|
+
const notADataUriError = (field) => new Error(
|
|
84
|
+
`${field} must be a data: URI, such as data:image/png;base64,iVBORw0KGgo. This tool does not download URLs.`
|
|
85
|
+
);
|
|
86
|
+
const fileTooBigError = (field, bytes, maxBytes) => new Error(
|
|
87
|
+
`${field} is ${bytes} bytes, over the ${maxBytes} bytes this tool accepts`
|
|
88
|
+
);
|
|
89
|
+
function percentDecoded(field, text) {
|
|
90
|
+
const bytes = [];
|
|
91
|
+
const encoder = new TextEncoder();
|
|
92
|
+
for (let index = 0; index < text.length; index++) {
|
|
93
|
+
if (text[index] === "%") {
|
|
94
|
+
const hex = text.slice(index + 1, index + 3);
|
|
95
|
+
if (!PERCENT_ENCODED.test(hex)) throw notADataUriError(field);
|
|
96
|
+
bytes.push(parseInt(hex, 16));
|
|
97
|
+
index += 2;
|
|
98
|
+
} else {
|
|
99
|
+
const codePoint = text.codePointAt(index);
|
|
100
|
+
const character = String.fromCodePoint(codePoint);
|
|
101
|
+
bytes.push(...encoder.encode(character));
|
|
102
|
+
index += character.length - 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return new Uint8Array(bytes);
|
|
106
|
+
}
|
|
107
|
+
function base64Decoded(field, text, maxBytes) {
|
|
108
|
+
const data = text.replace(/\s/g, "");
|
|
109
|
+
const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
|
|
110
|
+
const bytes = Math.floor(data.length * 3 / 4) - padding;
|
|
111
|
+
if (bytes > maxBytes) throw fileTooBigError(field, bytes, maxBytes);
|
|
112
|
+
let binary;
|
|
113
|
+
try {
|
|
114
|
+
binary = atob(data);
|
|
115
|
+
} catch {
|
|
116
|
+
throw new Error(`${field} is not valid base64`);
|
|
117
|
+
}
|
|
118
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
119
|
+
}
|
|
120
|
+
function fileFromDataUri(field, uri, maxBytes, givenName) {
|
|
121
|
+
const match = typeof uri === "string" ? DATA_URI.exec(uri) : null;
|
|
122
|
+
if (!match) throw notADataUriError(field);
|
|
123
|
+
const parameters = match[1].split(";");
|
|
124
|
+
const isBase64 = parameters.at(-1).toLowerCase() === "base64";
|
|
125
|
+
if (isBase64) parameters.pop();
|
|
126
|
+
const mediaType = parameters.shift().trim();
|
|
127
|
+
const nameParameter = parameters.find(
|
|
128
|
+
(parameter) => parameter.trim().toLowerCase().startsWith("name=")
|
|
129
|
+
);
|
|
130
|
+
let name = field;
|
|
131
|
+
if (typeof givenName === "string" && givenName.trim()) {
|
|
132
|
+
name = givenName.trim();
|
|
133
|
+
} else if (nameParameter) {
|
|
134
|
+
try {
|
|
135
|
+
name = decodeURIComponent(nameParameter.trim().slice("name=".length));
|
|
136
|
+
} catch {
|
|
137
|
+
throw notADataUriError(field);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const bytes = isBase64 ? base64Decoded(field, match[2], maxBytes) : percentDecoded(field, match[2]);
|
|
141
|
+
if (bytes.length > maxBytes)
|
|
142
|
+
throw fileTooBigError(field, bytes.length, maxBytes);
|
|
143
|
+
return new File([bytes], name, {
|
|
144
|
+
type: mediaType.includes("/") ? mediaType : "text/plain"
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const decodeDataUris = async (body, fields, maxBytes) => {
|
|
148
|
+
if (typeof body !== "object" || body === null) {
|
|
149
|
+
return body;
|
|
150
|
+
}
|
|
151
|
+
const decoded = { ...body };
|
|
152
|
+
for (const field of fields) {
|
|
153
|
+
const givenName = decoded[`${field}_name`];
|
|
154
|
+
delete decoded[`${field}_name`];
|
|
155
|
+
if (decoded[field] !== void 0) {
|
|
156
|
+
decoded[field] = fileFromDataUri(
|
|
157
|
+
field,
|
|
158
|
+
decoded[field],
|
|
159
|
+
maxBytes,
|
|
160
|
+
givenName
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return decoded;
|
|
165
|
+
};
|
|
166
|
+
export {
|
|
167
|
+
dataUriFields,
|
|
168
|
+
decodeDataUris,
|
|
169
|
+
loose,
|
|
170
|
+
toToolResult
|
|
171
|
+
};
|
|
172
|
+
//# sourceMappingURL=mcp-support.gen.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/client/mcp-support.gen.ts"],"sourcesContent":["import type { ToolCallback } from \"@modelcontextprotocol/sdk/server/mcp.js\"\nimport type { AnySchema } from \"@modelcontextprotocol/sdk/server/zod-compat.js\"\nimport type {\n CallToolResult,\n ToolAnnotations,\n} from \"@modelcontextprotocol/sdk/types.js\"\nimport * as z from \"zod\"\n\nexport type OperationMeta = {\n id: string\n method: string\n path: string\n risk: \"read\" | \"create\" | \"update\" | \"delete\"\n}\n\nexport type ToolConfig = {\n title?: string\n description?: string\n inputSchema: z.ZodObject<Record<string, z.ZodType<any>>>\n outputSchema?: AnySchema\n annotations?: ToolAnnotations\n _meta: {\n \"com.elasticpath/operation\": OperationMeta\n }\n}\n\nexport type Tool = {\n readonly name: string\n readonly config: ToolConfig\n readonly handler: ToolCallback<AnySchema>\n}\n\nconst messageOf = (error: unknown) =>\n error instanceof Error ? error.message : JSON.stringify(error)\n\nexport const toToolResult = async (\n request: Promise<{\n data?: unknown\n error?: unknown\n response?: Response\n }>,\n): Promise<CallToolResult> => {\n let text: string\n try {\n const result = await request\n if (result.error === undefined) {\n const structuredContent =\n typeof result.data === \"object\" &&\n result.data !== null &&\n !Array.isArray(result.data)\n ? (result.data as Record<string, unknown>)\n : { data: result.data ?? null }\n return {\n structuredContent,\n content: [{ type: \"text\", text: JSON.stringify(structuredContent) }],\n }\n }\n text = result.response\n ? `HTTP ${result.response.status}: ${JSON.stringify(result.error)}`\n : messageOf(result.error)\n } catch (error) {\n text = messageOf(error)\n }\n return { isError: true, content: [{ type: \"text\", text }] }\n}\n\nexport const loose = <T extends z.ZodType>(schema: T): T => {\n const def: any = schema._zod.def\n const copy = (changes: object): T =>\n schema.clone({ ...def, ...changes }, { parent: true })\n if (def.type === \"object\") {\n return copy({\n catchall: z.unknown(),\n shape: Object.fromEntries(\n Object.keys(def.shape).map((key) => [key, loose(def.shape[key])]),\n ),\n })\n }\n if (def.type === \"array\") {\n return copy({ element: loose(def.element) })\n }\n if (def.type === \"record\") {\n return copy({ valueType: loose(def.valueType) })\n }\n if (def.type === \"lazy\") {\n return copy({ getter: () => loose(def.getter()) })\n }\n if (\n [\n \"optional\",\n \"nullable\",\n \"default\",\n \"readonly\",\n \"nonoptional\",\n \"catch\",\n \"prefault\",\n ].includes(def.type)\n ) {\n return copy({ innerType: loose(def.innerType) })\n }\n if (def.type === \"union\") {\n return copy({ options: def.options.map(loose) })\n }\n if (def.type === \"intersection\") {\n return copy({ left: loose(def.left), right: loose(def.right) })\n }\n return schema\n}\n\ntype FileNameFields<K extends string> = {\n [P in `${K}_name`]: z.ZodOptional<z.ZodString>\n}\n\nexport const dataUriFields = <\n S extends z.ZodRawShape,\n C extends z.core.$ZodObjectConfig,\n K extends string,\n>(\n schema: z.ZodObject<S, C>,\n fields: ReadonlyArray<K>,\n maxBytes: number,\n): z.ZodObject<z.core.util.Extend<S, FileNameFields<K>>, C> =>\n schema.extend(\n Object.fromEntries(\n fields.flatMap((field) => {\n const current = schema.shape[field] as unknown as z.ZodType\n const uri = z.string().meta({\n format: \"uri\",\n \"x-mcp-file\": { maxSize: maxBytes },\n description: [\n current.description,\n `Send the file as a data: URI, such as data:text/csv;base64,<base64 bytes>, at most ${maxBytes} bytes once decoded.`,\n ]\n .filter(Boolean)\n .join(\" \"),\n })\n const name = z\n .string()\n .meta({\n description: `The name of the file sent in ${field}, such as products.csv.`,\n })\n .optional()\n return [\n [field, current.isOptional() ? uri.optional() : uri],\n [`${field}_name`, name],\n ]\n }),\n ),\n ) as unknown as z.ZodObject<z.core.util.Extend<S, FileNameFields<K>>, C>\n\ntype DataUrisDecoded<T, K extends string> = T extends object\n ? Omit<T, K | `${K}_name`> & {\n [P in K & keyof T]: undefined extends T[P] ? File | undefined : File\n }\n : T\n\nconst DATA_URI = /^data:([^,]*),(.*)$/is\nconst PERCENT_ENCODED = /^[0-9a-f]{2}$/i\n\nconst notADataUriError = (field: string) =>\n new Error(\n `${field} must be a data: URI, such as data:image/png;base64,iVBORw0KGgo. This tool does not download URLs.`,\n )\n\nconst fileTooBigError = (field: string, bytes: number, maxBytes: number) =>\n new Error(\n `${field} is ${bytes} bytes, over the ${maxBytes} bytes this tool accepts`,\n )\n\nfunction percentDecoded(field: string, text: string) {\n const bytes: number[] = []\n const encoder = new TextEncoder()\n for (let index = 0; index < text.length; index++) {\n if (text[index] === \"%\") {\n const hex = text.slice(index + 1, index + 3)\n if (!PERCENT_ENCODED.test(hex)) throw notADataUriError(field)\n bytes.push(parseInt(hex, 16))\n index += 2\n } else {\n const codePoint = text.codePointAt(index)!\n const character = String.fromCodePoint(codePoint)\n bytes.push(...encoder.encode(character))\n index += character.length - 1\n }\n }\n return new Uint8Array(bytes)\n}\n\nfunction base64Decoded(field: string, text: string, maxBytes: number) {\n const data = text.replace(/\\s/g, \"\")\n const padding = data.endsWith(\"==\") ? 2 : data.endsWith(\"=\") ? 1 : 0\n const bytes = Math.floor((data.length * 3) / 4) - padding\n if (bytes > maxBytes) throw fileTooBigError(field, bytes, maxBytes)\n let binary: string\n try {\n binary = atob(data)\n } catch {\n throw new Error(`${field} is not valid base64`)\n }\n return Uint8Array.from(binary, (character) => character.charCodeAt(0))\n}\n\nfunction fileFromDataUri(\n field: string,\n uri: unknown,\n maxBytes: number,\n givenName: unknown,\n) {\n const match = typeof uri === \"string\" ? DATA_URI.exec(uri) : null\n if (!match) throw notADataUriError(field)\n const parameters = match[1]!.split(\";\")\n const isBase64 = parameters.at(-1)!.toLowerCase() === \"base64\"\n if (isBase64) parameters.pop()\n const mediaType = parameters.shift()!.trim()\n const nameParameter = parameters.find((parameter) =>\n parameter.trim().toLowerCase().startsWith(\"name=\"),\n )\n let name = field\n if (typeof givenName === \"string\" && givenName.trim()) {\n name = givenName.trim()\n } else if (nameParameter) {\n try {\n name = decodeURIComponent(nameParameter.trim().slice(\"name=\".length))\n } catch {\n throw notADataUriError(field)\n }\n }\n const bytes = isBase64\n ? base64Decoded(field, match[2]!, maxBytes)\n : percentDecoded(field, match[2]!)\n if (bytes.length > maxBytes)\n throw fileTooBigError(field, bytes.length, maxBytes)\n return new File([bytes], name, {\n type: mediaType.includes(\"/\") ? mediaType : \"text/plain\",\n })\n}\n\nexport const decodeDataUris = async <T, K extends string>(\n body: T,\n fields: ReadonlyArray<K>,\n maxBytes: number,\n): Promise<DataUrisDecoded<T, K>> => {\n if (typeof body !== \"object\" || body === null) {\n return body as DataUrisDecoded<T, K>\n }\n const decoded = { ...body } as Record<string, unknown>\n for (const field of fields) {\n const givenName = decoded[`${field}_name`]\n delete decoded[`${field}_name`]\n if (decoded[field] !== undefined) {\n decoded[field] = fileFromDataUri(\n field,\n decoded[field],\n maxBytes,\n givenName,\n )\n }\n }\n return decoded as DataUrisDecoded<T, K>\n}\n"],"mappings":"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","names":[]}
|