@uploadcn/server 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +38 -2
- package/dist/fs.d.ts +36 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +216 -0
- package/dist/fs.js.map +1 -0
- package/dist/index.d.ts +193 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +579 -0
- package/dist/index.js.map +1 -0
- package/dist/ocr.d.ts +154 -0
- package/dist/ocr.d.ts.map +1 -0
- package/dist/ocr.js +534 -0
- package/dist/ocr.js.map +1 -0
- package/dist/scan-B8LojfB0.d.ts +114 -0
- package/dist/scan-B8LojfB0.d.ts.map +1 -0
- package/dist/scan.d.ts +2 -0
- package/dist/scan.js +290 -0
- package/dist/scan.js.map +1 -0
- package/dist/signing-DZiuKRaJ.js +52 -0
- package/dist/signing-DZiuKRaJ.js.map +1 -0
- package/dist/storage-CGGIhRB9.d.ts +77 -0
- package/dist/storage-CGGIhRB9.d.ts.map +1 -0
- package/package.json +56 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 UploadCN contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @uploadcn/server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A Web-standard (`Request` → `Response`) upload route for [UploadCN](https://uploadcn.dev):
|
|
4
|
+
presigned uploads and multipart signing for Amazon S3, Cloudflare R2 and S3-compatible
|
|
5
|
+
storage. Runs on Node.js, Bun, Deno, Cloudflare Workers and edge runtimes. Signing uses
|
|
6
|
+
Web Crypto via `aws4fetch`. No AWS SDK.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @uploadcn/server
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
// app/api/upload/route.ts (Next.js)
|
|
14
|
+
import { createUploadRoute, s3Storage } from "@uploadcn/server"
|
|
15
|
+
|
|
16
|
+
export const { POST } = createUploadRoute({
|
|
17
|
+
storage: s3Storage({
|
|
18
|
+
bucket: process.env.S3_BUCKET!,
|
|
19
|
+
region: process.env.S3_REGION!,
|
|
20
|
+
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
|
|
21
|
+
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
|
|
22
|
+
}),
|
|
23
|
+
maxFileSize: 500 * 1024 * 1024,
|
|
24
|
+
allowedTypes: ["image/*", "application/pdf"],
|
|
25
|
+
async authorize({ request }) {
|
|
26
|
+
// throw new UploadRouteError("Unauthorized", 401)
|
|
27
|
+
},
|
|
28
|
+
async onUploadComplete({ key, file }) {
|
|
29
|
+
// save metadata, enqueue a virus scan…
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Signatures lock the content type and length; completed uploads are verified with `HEAD`
|
|
35
|
+
before `onUploadComplete` runs.
|
|
36
|
+
|
|
37
|
+
Documentation: https://uploadcn.dev/docs/reference/server
|
|
38
|
+
|
|
39
|
+
MIT
|
package/dist/fs.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { i as UploadStorage } from "./storage-CGGIhRB9.js";
|
|
2
|
+
//#region src/fs.d.ts
|
|
3
|
+
export interface FileSystemStorageOptions {
|
|
4
|
+
/** Folder where files are written, e.g. `./uploads`. */
|
|
5
|
+
directory: string;
|
|
6
|
+
/**
|
|
7
|
+
* URL where `handler` is mounted, e.g. `/api/files`. Signed upload and
|
|
8
|
+
* download URLs point here.
|
|
9
|
+
*/
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
/** HMAC secret for signed URLs. Use a long random value. */
|
|
12
|
+
secret: string;
|
|
13
|
+
/**
|
|
14
|
+
* Serve files publicly from this URL (e.g. `/uploads` when `directory` is
|
|
15
|
+
* inside `public/`). Without it, reads use short-lived signed URLs.
|
|
16
|
+
*/
|
|
17
|
+
publicUrl?: string;
|
|
18
|
+
/** Largest accepted request body, in bytes. Default 5 GiB. */
|
|
19
|
+
maxBodySize?: number;
|
|
20
|
+
/** Lifetime of signed download URLs, in seconds. Default 3600. */
|
|
21
|
+
downloadExpiresIn?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Stores uploads on the server's disk — for self-hosted apps, VPSs and
|
|
25
|
+
* local development. Implements the full presigned and multipart flow, so
|
|
26
|
+
* every UploadCN adapter and component works unchanged.
|
|
27
|
+
*
|
|
28
|
+
* Node.js only. Don't use it on serverless platforms with ephemeral disks.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createFileSystemStorage(options: FileSystemStorageOptions): {
|
|
31
|
+
storage: UploadStorage;
|
|
32
|
+
handler: (request: Request) => Promise<Response>;
|
|
33
|
+
baseUrl: string;
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
//# sourceMappingURL=fs.d.ts.map
|
package/dist/fs.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs.d.ts","names":[],"sources":["../src/fs.ts"],"mappings":";;iBAWiB;;EAEf;;;;;EAKA;;EAEA;;;;;EAKA;;EAEA;;EAEA;;;;;;;;;wBAcc,wBAAwB,SAAS;;EAiLf,UAAA,SAAA,YAAU,QAAQ"}
|
package/dist/fs.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { t as createUrlSigner } from "./signing-DZiuKRaJ.js";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { createReadStream, createWriteStream } from "node:fs";
|
|
4
|
+
import * as fs from "node:fs/promises";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { Readable, Transform } from "node:stream";
|
|
7
|
+
import { pipeline } from "node:stream/promises";
|
|
8
|
+
//#region src/fs.ts
|
|
9
|
+
const PARTS = ".parts";
|
|
10
|
+
var BodyTooLarge = class extends Error {};
|
|
11
|
+
/**
|
|
12
|
+
* Stores uploads on the server's disk — for self-hosted apps, VPSs and
|
|
13
|
+
* local development. Implements the full presigned and multipart flow, so
|
|
14
|
+
* every UploadCN adapter and component works unchanged.
|
|
15
|
+
*
|
|
16
|
+
* Node.js only. Don't use it on serverless platforms with ephemeral disks.
|
|
17
|
+
*/
|
|
18
|
+
function createFileSystemStorage(options) {
|
|
19
|
+
const root = path.resolve(options.directory);
|
|
20
|
+
const maxBodySize = options.maxBodySize ?? 5 * 1024 ** 3;
|
|
21
|
+
const { base, sign, verify } = createUrlSigner(options.secret, options.baseUrl);
|
|
22
|
+
function resolveKey(key) {
|
|
23
|
+
const normalized = path.posix.normalize(key.replace(/\\/g, "/"));
|
|
24
|
+
if (!normalized || normalized.startsWith("..") || path.posix.isAbsolute(normalized) || normalized.split("/").includes(PARTS)) throw new Error(`Invalid object key: ${key}`);
|
|
25
|
+
const target = path.join(root, normalized);
|
|
26
|
+
if (!target.startsWith(root + path.sep)) throw new Error(`Invalid object key: ${key}`);
|
|
27
|
+
return target;
|
|
28
|
+
}
|
|
29
|
+
function partsDir(uploadId) {
|
|
30
|
+
if (!/^[\w-]+$/.test(uploadId)) throw new Error("Invalid upload id");
|
|
31
|
+
return path.join(root, PARTS, uploadId);
|
|
32
|
+
}
|
|
33
|
+
/** Streams a request body to disk, enforcing the size limit. */
|
|
34
|
+
async function writeBody(body, target, expectedSize) {
|
|
35
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
36
|
+
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
37
|
+
const hash = createHash("sha256");
|
|
38
|
+
let size = 0;
|
|
39
|
+
const limit = expectedSize ?? maxBodySize;
|
|
40
|
+
const meter = new Transform({ transform(chunk, _encoding, callback) {
|
|
41
|
+
size += chunk.length;
|
|
42
|
+
if (size > limit) return callback(new BodyTooLarge());
|
|
43
|
+
hash.update(chunk);
|
|
44
|
+
callback(null, chunk);
|
|
45
|
+
} });
|
|
46
|
+
try {
|
|
47
|
+
await pipeline(Readable.fromWeb(body), meter, createWriteStream(temporary));
|
|
48
|
+
if (expectedSize != null && size !== expectedSize) throw new Error("Body size does not match the signed size");
|
|
49
|
+
await fs.rename(temporary, target);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
await fs.rm(temporary, { force: true });
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
size,
|
|
56
|
+
etag: `"${size}-${hash.digest("hex").slice(0, 32)}"`
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const storage = {
|
|
60
|
+
name: "filesystem",
|
|
61
|
+
async presignPut({ key, contentType, contentLength, expiresIn }) {
|
|
62
|
+
resolveKey(key);
|
|
63
|
+
return {
|
|
64
|
+
url: await sign({
|
|
65
|
+
key,
|
|
66
|
+
method: "PUT",
|
|
67
|
+
size: contentLength,
|
|
68
|
+
expires: Date.now() + expiresIn * 1e3
|
|
69
|
+
}),
|
|
70
|
+
headers: { "content-type": contentType }
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
async createMultipart({ key }) {
|
|
74
|
+
resolveKey(key);
|
|
75
|
+
const uploadId = randomUUID();
|
|
76
|
+
await fs.mkdir(partsDir(uploadId), { recursive: true });
|
|
77
|
+
return { uploadId };
|
|
78
|
+
},
|
|
79
|
+
presignPart({ key, uploadId, partNumber, expiresIn }) {
|
|
80
|
+
return sign({
|
|
81
|
+
key,
|
|
82
|
+
method: "PUT",
|
|
83
|
+
uploadId,
|
|
84
|
+
partNumber,
|
|
85
|
+
expires: Date.now() + expiresIn * 1e3
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
async listParts({ uploadId }) {
|
|
89
|
+
const dir = partsDir(uploadId);
|
|
90
|
+
const entries = await fs.readdir(dir).catch(() => null);
|
|
91
|
+
if (!entries) return [];
|
|
92
|
+
const parts = [];
|
|
93
|
+
for (const entry of entries.filter((name) => name.endsWith(".json"))) parts.push(JSON.parse(await fs.readFile(path.join(dir, entry), "utf8")));
|
|
94
|
+
return parts.sort((a, b) => a.partNumber - b.partNumber);
|
|
95
|
+
},
|
|
96
|
+
async completeMultipart({ key, uploadId, parts }) {
|
|
97
|
+
const dir = partsDir(uploadId);
|
|
98
|
+
const target = resolveKey(key);
|
|
99
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
100
|
+
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
101
|
+
const out = createWriteStream(temporary);
|
|
102
|
+
let size = 0;
|
|
103
|
+
try {
|
|
104
|
+
for (const part of [...parts].sort((a, b) => a.partNumber - b.partNumber)) {
|
|
105
|
+
const meta = JSON.parse(await fs.readFile(path.join(dir, `${part.partNumber}.json`), "utf8"));
|
|
106
|
+
if (meta.etag !== part.etag) throw new Error(`Part ${part.partNumber} does not match its ETag`);
|
|
107
|
+
size += meta.size;
|
|
108
|
+
await pipeline(createReadStream(path.join(dir, String(part.partNumber))), out, { end: false });
|
|
109
|
+
}
|
|
110
|
+
await new Promise((resolve, reject) => out.end((error) => error ? reject(error) : resolve()));
|
|
111
|
+
await fs.rename(temporary, target);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
out.destroy();
|
|
114
|
+
await fs.rm(temporary, { force: true });
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
await fs.rm(dir, {
|
|
118
|
+
recursive: true,
|
|
119
|
+
force: true
|
|
120
|
+
});
|
|
121
|
+
return { etag: `"${parts.length}-${size}"` };
|
|
122
|
+
},
|
|
123
|
+
async abortMultipart({ uploadId }) {
|
|
124
|
+
await fs.rm(partsDir(uploadId), {
|
|
125
|
+
recursive: true,
|
|
126
|
+
force: true
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
async headObject(key) {
|
|
130
|
+
const stat = await fs.stat(resolveKey(key)).catch(() => null);
|
|
131
|
+
return stat?.isFile() ? { size: stat.size } : null;
|
|
132
|
+
},
|
|
133
|
+
async deleteObject(key) {
|
|
134
|
+
await fs.rm(resolveKey(key), { force: true });
|
|
135
|
+
},
|
|
136
|
+
async getObject(key) {
|
|
137
|
+
const target = resolveKey(key);
|
|
138
|
+
if (!(await fs.stat(target).catch(() => null))?.isFile()) return null;
|
|
139
|
+
return Readable.toWeb(createReadStream(target));
|
|
140
|
+
},
|
|
141
|
+
async getUrl(key) {
|
|
142
|
+
resolveKey(key);
|
|
143
|
+
if (options.publicUrl) return `${options.publicUrl.replace(/\/+$/, "")}/${key.split("/").map(encodeURIComponent).join("/")}`;
|
|
144
|
+
return sign({
|
|
145
|
+
key,
|
|
146
|
+
method: "GET",
|
|
147
|
+
expires: Date.now() + (options.downloadExpiresIn ?? 3600) * 1e3
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Handles signed uploads (PUT) and downloads (GET). Mount it at
|
|
153
|
+
* `baseUrl`, e.g. `export { handler as GET, handler as PUT }`.
|
|
154
|
+
*/
|
|
155
|
+
async function handler(request) {
|
|
156
|
+
if (request.method !== "PUT" && request.method !== "GET") return new Response("Method not allowed", {
|
|
157
|
+
status: 405,
|
|
158
|
+
headers: { allow: "GET, PUT" }
|
|
159
|
+
});
|
|
160
|
+
const token = await verify(new URL(request.url).searchParams.get("token"));
|
|
161
|
+
if (!token || token.method !== request.method) return new Response("Invalid or expired signature", { status: 403 });
|
|
162
|
+
if (request.method === "GET") {
|
|
163
|
+
const file = resolveKey(token.key);
|
|
164
|
+
const stat = await fs.stat(file).catch(() => null);
|
|
165
|
+
if (!stat?.isFile()) return new Response("Not found", { status: 404 });
|
|
166
|
+
return new Response(Readable.toWeb(createReadStream(file)), { headers: {
|
|
167
|
+
"content-length": String(stat.size),
|
|
168
|
+
"content-type": "application/octet-stream",
|
|
169
|
+
"content-disposition": `inline; filename*=UTF-8''${encodeURIComponent(path.basename(file))}`,
|
|
170
|
+
"x-content-type-options": "nosniff"
|
|
171
|
+
} });
|
|
172
|
+
}
|
|
173
|
+
if (Number(request.headers.get("content-length") ?? NaN) > maxBodySize) return new Response("Payload too large", { status: 413 });
|
|
174
|
+
if (!request.body) return new Response("Empty body", { status: 400 });
|
|
175
|
+
try {
|
|
176
|
+
if (token.uploadId != null && token.partNumber != null) {
|
|
177
|
+
const dir = partsDir(token.uploadId);
|
|
178
|
+
if (!await fs.stat(dir).catch(() => null)) return new Response("Unknown upload", { status: 404 });
|
|
179
|
+
const { size, etag } = await writeBody(request.body, path.join(dir, String(token.partNumber)));
|
|
180
|
+
const meta = {
|
|
181
|
+
partNumber: token.partNumber,
|
|
182
|
+
etag,
|
|
183
|
+
size
|
|
184
|
+
};
|
|
185
|
+
await fs.writeFile(path.join(dir, `${token.partNumber}.json`), JSON.stringify(meta));
|
|
186
|
+
return new Response(null, {
|
|
187
|
+
status: 200,
|
|
188
|
+
headers: {
|
|
189
|
+
etag,
|
|
190
|
+
"access-control-expose-headers": "ETag"
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const { etag } = await writeBody(request.body, resolveKey(token.key), token.size);
|
|
195
|
+
return new Response(null, {
|
|
196
|
+
status: 200,
|
|
197
|
+
headers: {
|
|
198
|
+
etag,
|
|
199
|
+
"access-control-expose-headers": "ETag"
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (error instanceof BodyTooLarge) return new Response("Payload too large", { status: 413 });
|
|
204
|
+
return new Response(error instanceof Error ? error.message : "Upload failed", { status: 400 });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
storage,
|
|
209
|
+
handler,
|
|
210
|
+
baseUrl: base
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
//#endregion
|
|
214
|
+
export { createFileSystemStorage };
|
|
215
|
+
|
|
216
|
+
//# sourceMappingURL=fs.js.map
|
package/dist/fs.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs.js","names":[],"sources":["../src/fs.ts"],"sourcesContent":["import { createHash, randomUUID } from \"node:crypto\"\nimport { createReadStream, createWriteStream } from \"node:fs\"\nimport * as fs from \"node:fs/promises\"\nimport * as path from \"node:path\"\nimport { Readable, Transform } from \"node:stream\"\nimport { pipeline } from \"node:stream/promises\"\nimport type { ReadableStream as NodeReadableStream } from \"node:stream/web\"\n\nimport { createUrlSigner } from \"./signing\"\nimport type { StoragePart, StoredObjectInfo, UploadStorage } from \"./storage\"\n\nexport interface FileSystemStorageOptions {\n /** Folder where files are written, e.g. `./uploads`. */\n directory: string\n /**\n * URL where `handler` is mounted, e.g. `/api/files`. Signed upload and\n * download URLs point here.\n */\n baseUrl: string\n /** HMAC secret for signed URLs. Use a long random value. */\n secret: string\n /**\n * Serve files publicly from this URL (e.g. `/uploads` when `directory` is\n * inside `public/`). Without it, reads use short-lived signed URLs.\n */\n publicUrl?: string\n /** Largest accepted request body, in bytes. Default 5 GiB. */\n maxBodySize?: number\n /** Lifetime of signed download URLs, in seconds. Default 3600. */\n downloadExpiresIn?: number\n}\n\nconst PARTS = \".parts\"\n\nclass BodyTooLarge extends Error {}\n\n/**\n * Stores uploads on the server's disk — for self-hosted apps, VPSs and\n * local development. Implements the full presigned and multipart flow, so\n * every UploadCN adapter and component works unchanged.\n *\n * Node.js only. Don't use it on serverless platforms with ephemeral disks.\n */\nexport function createFileSystemStorage(options: FileSystemStorageOptions) {\n const root = path.resolve(options.directory)\n const maxBodySize = options.maxBodySize ?? 5 * 1024 ** 3\n const { base, sign, verify } = createUrlSigner(\n options.secret,\n options.baseUrl\n )\n\n function resolveKey(key: string) {\n const normalized = path.posix.normalize(key.replace(/\\\\/g, \"/\"))\n if (\n !normalized ||\n normalized.startsWith(\"..\") ||\n path.posix.isAbsolute(normalized) ||\n normalized.split(\"/\").includes(PARTS)\n ) {\n throw new Error(`Invalid object key: ${key}`)\n }\n const target = path.join(root, normalized)\n if (!target.startsWith(root + path.sep)) {\n throw new Error(`Invalid object key: ${key}`)\n }\n return target\n }\n\n function partsDir(uploadId: string) {\n if (!/^[\\w-]+$/.test(uploadId)) throw new Error(\"Invalid upload id\")\n return path.join(root, PARTS, uploadId)\n }\n\n /** Streams a request body to disk, enforcing the size limit. */\n async function writeBody(\n body: ReadableStream<Uint8Array>,\n target: string,\n expectedSize?: number\n ) {\n await fs.mkdir(path.dirname(target), { recursive: true })\n const temporary = `${target}.${randomUUID()}.tmp`\n const hash = createHash(\"sha256\")\n let size = 0\n const limit = expectedSize ?? maxBodySize\n const meter = new Transform({\n transform(chunk: Buffer, _encoding, callback) {\n size += chunk.length\n if (size > limit) return callback(new BodyTooLarge())\n hash.update(chunk)\n callback(null, chunk)\n },\n })\n try {\n await pipeline(\n Readable.fromWeb(body as NodeReadableStream),\n meter,\n createWriteStream(temporary)\n )\n if (expectedSize != null && size !== expectedSize) {\n throw new Error(\"Body size does not match the signed size\")\n }\n await fs.rename(temporary, target)\n } catch (error) {\n await fs.rm(temporary, { force: true })\n throw error\n }\n return { size, etag: `\"${size}-${hash.digest(\"hex\").slice(0, 32)}\"` }\n }\n\n const storage: UploadStorage = {\n name: \"filesystem\",\n async presignPut({ key, contentType, contentLength, expiresIn }) {\n resolveKey(key)\n const url = await sign({\n key,\n method: \"PUT\",\n size: contentLength,\n expires: Date.now() + expiresIn * 1000,\n })\n return { url, headers: { \"content-type\": contentType } }\n },\n async createMultipart({ key }) {\n resolveKey(key)\n const uploadId = randomUUID()\n await fs.mkdir(partsDir(uploadId), { recursive: true })\n return { uploadId }\n },\n presignPart({ key, uploadId, partNumber, expiresIn }) {\n return sign({\n key,\n method: \"PUT\",\n uploadId,\n partNumber,\n expires: Date.now() + expiresIn * 1000,\n })\n },\n async listParts({ uploadId }) {\n const dir = partsDir(uploadId)\n const entries = await fs.readdir(dir).catch(() => null)\n if (!entries) return []\n const parts: StoragePart[] = []\n for (const entry of entries.filter((name) => name.endsWith(\".json\"))) {\n parts.push(JSON.parse(await fs.readFile(path.join(dir, entry), \"utf8\")))\n }\n return parts.sort((a, b) => a.partNumber - b.partNumber)\n },\n async completeMultipart({ key, uploadId, parts }) {\n const dir = partsDir(uploadId)\n const target = resolveKey(key)\n await fs.mkdir(path.dirname(target), { recursive: true })\n const temporary = `${target}.${randomUUID()}.tmp`\n const out = createWriteStream(temporary)\n let size = 0\n try {\n for (const part of [...parts].sort(\n (a, b) => a.partNumber - b.partNumber\n )) {\n const meta = JSON.parse(\n await fs.readFile(path.join(dir, `${part.partNumber}.json`), \"utf8\")\n ) as StoragePart\n if (meta.etag !== part.etag) {\n throw new Error(`Part ${part.partNumber} does not match its ETag`)\n }\n size += meta.size\n await pipeline(\n createReadStream(path.join(dir, String(part.partNumber))),\n out,\n {\n end: false,\n }\n )\n }\n await new Promise<void>((resolve, reject) =>\n out.end((error?: Error | null) => (error ? reject(error) : resolve()))\n )\n await fs.rename(temporary, target)\n } catch (error) {\n out.destroy()\n await fs.rm(temporary, { force: true })\n throw error\n }\n await fs.rm(dir, { recursive: true, force: true })\n return { etag: `\"${parts.length}-${size}\"` }\n },\n async abortMultipart({ uploadId }) {\n await fs.rm(partsDir(uploadId), { recursive: true, force: true })\n },\n async headObject(key): Promise<StoredObjectInfo | null> {\n const stat = await fs.stat(resolveKey(key)).catch(() => null)\n return stat?.isFile() ? { size: stat.size } : null\n },\n async deleteObject(key) {\n await fs.rm(resolveKey(key), { force: true })\n },\n async getObject(key) {\n const target = resolveKey(key)\n const stat = await fs.stat(target).catch(() => null)\n if (!stat?.isFile()) return null\n return Readable.toWeb(\n createReadStream(target)\n ) as unknown as ReadableStream<Uint8Array>\n },\n async getUrl(key) {\n resolveKey(key)\n if (options.publicUrl) {\n const prefix = options.publicUrl.replace(/\\/+$/, \"\")\n return `${prefix}/${key.split(\"/\").map(encodeURIComponent).join(\"/\")}`\n }\n return sign({\n key,\n method: \"GET\",\n expires: Date.now() + (options.downloadExpiresIn ?? 3600) * 1000,\n })\n },\n }\n\n /**\n * Handles signed uploads (PUT) and downloads (GET). Mount it at\n * `baseUrl`, e.g. `export { handler as GET, handler as PUT }`.\n */\n async function handler(request: Request): Promise<Response> {\n if (request.method !== \"PUT\" && request.method !== \"GET\") {\n return new Response(\"Method not allowed\", {\n status: 405,\n headers: { allow: \"GET, PUT\" },\n })\n }\n const token = await verify(new URL(request.url).searchParams.get(\"token\"))\n if (!token || token.method !== request.method) {\n return new Response(\"Invalid or expired signature\", { status: 403 })\n }\n\n if (request.method === \"GET\") {\n const file = resolveKey(token.key)\n const stat = await fs.stat(file).catch(() => null)\n if (!stat?.isFile()) return new Response(\"Not found\", { status: 404 })\n return new Response(\n Readable.toWeb(createReadStream(file)) as ReadableStream,\n {\n headers: {\n \"content-length\": String(stat.size),\n \"content-type\": \"application/octet-stream\",\n \"content-disposition\": `inline; filename*=UTF-8''${encodeURIComponent(path.basename(file))}`,\n \"x-content-type-options\": \"nosniff\",\n },\n }\n )\n }\n\n const declared = Number(request.headers.get(\"content-length\") ?? NaN)\n if (declared > maxBodySize) {\n return new Response(\"Payload too large\", { status: 413 })\n }\n if (!request.body) return new Response(\"Empty body\", { status: 400 })\n try {\n if (token.uploadId != null && token.partNumber != null) {\n const dir = partsDir(token.uploadId)\n const stat = await fs.stat(dir).catch(() => null)\n if (!stat) return new Response(\"Unknown upload\", { status: 404 })\n const { size, etag } = await writeBody(\n request.body,\n path.join(dir, String(token.partNumber))\n )\n const meta: StoragePart = { partNumber: token.partNumber, etag, size }\n await fs.writeFile(\n path.join(dir, `${token.partNumber}.json`),\n JSON.stringify(meta)\n )\n return new Response(null, {\n status: 200,\n headers: { etag, \"access-control-expose-headers\": \"ETag\" },\n })\n }\n const { etag } = await writeBody(\n request.body,\n resolveKey(token.key),\n token.size\n )\n return new Response(null, {\n status: 200,\n headers: { etag, \"access-control-expose-headers\": \"ETag\" },\n })\n } catch (error) {\n if (error instanceof BodyTooLarge) {\n return new Response(\"Payload too large\", { status: 413 })\n }\n return new Response(\n error instanceof Error ? error.message : \"Upload failed\",\n { status: 400 }\n )\n }\n }\n\n return { storage, handler, baseUrl: base }\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { i as UploadStorage, n as StoragePart, r as StoredObjectInfo, t as PresignedRequest } from "./storage-CGGIhRB9.js";
|
|
2
|
+
import { a as ScanResult, o as Scanner } from "./scan-B8LojfB0.js";
|
|
3
|
+
import { FileDescriptor, FileDescriptor as FileDescriptor$1, StoredObject, UploadRouteAction, UploadRouteAction as UploadRouteAction$1, UploadRouteRequest } from "@uploadcn/core";
|
|
4
|
+
//#region src/route.d.ts
|
|
5
|
+
export declare class UploadRouteError extends Error {
|
|
6
|
+
readonly status: number;
|
|
7
|
+
/** `"rejected"` tells the client the file itself was refused (no retry). */
|
|
8
|
+
readonly code?: "rejected" | undefined;
|
|
9
|
+
readonly name = "UploadRouteError";
|
|
10
|
+
constructor(message: string, status?: number,
|
|
11
|
+
/** `"rejected"` tells the client the file itself was refused (no retry). */
|
|
12
|
+
code?: "rejected" | undefined);
|
|
13
|
+
}
|
|
14
|
+
interface UploadScanOptions<TAuth> {
|
|
15
|
+
/** `clamavScanner()`, `virusTotalScanner()`, `httpScanner()` or your own. */
|
|
16
|
+
scanner: Scanner;
|
|
17
|
+
/**
|
|
18
|
+
* What to do when the scanner can't decide, or fails: `"reject"` (default,
|
|
19
|
+
* fail closed) deletes the file; `"allow"` keeps it.
|
|
20
|
+
*/
|
|
21
|
+
onUnknown?: "reject" | "allow";
|
|
22
|
+
/** Files larger than this (bytes) are not scanned and count as unknown. */
|
|
23
|
+
maxFileSize?: number;
|
|
24
|
+
/** Every verdict, for logging, alerts or an audit trail. */
|
|
25
|
+
onResult?: (context: UploadRouteContext<TAuth> & {
|
|
26
|
+
key: string;
|
|
27
|
+
file: FileDescriptor$1;
|
|
28
|
+
result: ScanResult;
|
|
29
|
+
}) => void | Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
interface UploadRouteContext<TAuth> {
|
|
32
|
+
request: Request;
|
|
33
|
+
action: UploadRouteAction$1;
|
|
34
|
+
/** Value returned by `authorize`. */
|
|
35
|
+
auth: TAuth;
|
|
36
|
+
}
|
|
37
|
+
interface UploadRouteOptions<TAuth = undefined> {
|
|
38
|
+
storage: UploadStorage;
|
|
39
|
+
/**
|
|
40
|
+
* Runs before every action. Return a value (user, tenant…) to pass along,
|
|
41
|
+
* or throw `UploadRouteError` to deny. For multipart actions, `key` is
|
|
42
|
+
* provided so you can verify the caller owns it.
|
|
43
|
+
*/
|
|
44
|
+
authorize?: (context: {
|
|
45
|
+
request: Request;
|
|
46
|
+
action: UploadRouteAction$1;
|
|
47
|
+
key?: string;
|
|
48
|
+
}) => TAuth | Promise<TAuth>;
|
|
49
|
+
/** Largest allowed file in bytes. Enforced by the signature for single PUTs. */
|
|
50
|
+
maxFileSize?: number;
|
|
51
|
+
/** Allowed types, in `<input accept>` syntax (`image/*`, `.pdf`…). */
|
|
52
|
+
allowedTypes?: string | readonly string[];
|
|
53
|
+
/** Builds the object key. Default: `<uuid>/<sanitized file name>`. */
|
|
54
|
+
getKey?: (context: UploadRouteContext<TAuth> & {
|
|
55
|
+
file: FileDescriptor$1;
|
|
56
|
+
meta: Record<string, unknown>;
|
|
57
|
+
}) => string | Promise<string>;
|
|
58
|
+
/** Signed URL lifetime in seconds. Default 3600. */
|
|
59
|
+
expiresIn?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Called once an upload is stored, verified and (with `scan`) found clean:
|
|
62
|
+
* save metadata, trigger processing. The return value is sent to the
|
|
63
|
+
* client as `result.data`.
|
|
64
|
+
*/
|
|
65
|
+
onUploadComplete?: (context: UploadRouteContext<TAuth> & {
|
|
66
|
+
key: string;
|
|
67
|
+
url: string;
|
|
68
|
+
file: FileDescriptor$1;
|
|
69
|
+
meta: Record<string, unknown>;
|
|
70
|
+
/** The scan verdict, when `scan` is set. */
|
|
71
|
+
scan?: ScanResult;
|
|
72
|
+
}) => unknown;
|
|
73
|
+
/** Check that the object exists (HEAD) before completing. Default `true`. */
|
|
74
|
+
verify?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Scan every upload for malware before it completes. Infected files are
|
|
77
|
+
* deleted and the client sees the item as rejected. Requires a storage
|
|
78
|
+
* that can read objects (`s3Storage`, `r2Storage`, `createFileSystemStorage`).
|
|
79
|
+
*/
|
|
80
|
+
scan?: UploadScanOptions<TAuth>;
|
|
81
|
+
}
|
|
82
|
+
export declare function sanitizeFileName(name: string): string;
|
|
83
|
+
/**
|
|
84
|
+
* A single POST endpoint that signs uploads for the browser's `s3Adapter`.
|
|
85
|
+
* It speaks Web standard `Request`/`Response`, so it mounts directly in
|
|
86
|
+
* Next.js route handlers, TanStack Start server routes, Hono, Remix, Bun,
|
|
87
|
+
* Deno and Cloudflare Workers.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* // app/api/upload/route.ts
|
|
91
|
+
* export const { POST } = createUploadRoute({ storage: s3Storage({...}) })
|
|
92
|
+
*/
|
|
93
|
+
export declare function createUploadRoute<TAuth = undefined>(options: UploadRouteOptions<TAuth>): {
|
|
94
|
+
POST: (request: Request) => Promise<Response>;
|
|
95
|
+
handler: (request: Request) => Promise<Response>;
|
|
96
|
+
};
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/s3.d.ts
|
|
99
|
+
interface S3StorageOptions {
|
|
100
|
+
bucket: string;
|
|
101
|
+
/** AWS region, or `"auto"` for R2. */
|
|
102
|
+
region: string;
|
|
103
|
+
accessKeyId: string;
|
|
104
|
+
secretAccessKey: string;
|
|
105
|
+
sessionToken?: string;
|
|
106
|
+
/**
|
|
107
|
+
* Custom endpoint for S3-compatible services (R2, MinIO, B2, Spaces…).
|
|
108
|
+
* Defaults to AWS.
|
|
109
|
+
*/
|
|
110
|
+
endpoint?: string;
|
|
111
|
+
/** Use `endpoint/bucket/key` URLs. Defaults to `true` with a custom endpoint. */
|
|
112
|
+
forcePathStyle?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Base URL for reading objects (a CDN or public bucket domain). Without
|
|
115
|
+
* it, `getUrl` returns a signed GET URL valid for `readUrlExpiresIn`.
|
|
116
|
+
*/
|
|
117
|
+
publicUrl?: string;
|
|
118
|
+
/** Seconds. Default 3600. */
|
|
119
|
+
readUrlExpiresIn?: number;
|
|
120
|
+
}
|
|
121
|
+
export declare class S3StorageError extends Error {
|
|
122
|
+
readonly status: number;
|
|
123
|
+
readonly code?: string | undefined;
|
|
124
|
+
readonly name = "S3StorageError";
|
|
125
|
+
constructor(message: string, status: number, code?: string | undefined);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Amazon S3 and any S3-compatible service, signed with SigV4 via aws4fetch.
|
|
129
|
+
* Runs on Node, Bun, Deno, Cloudflare Workers and edge runtimes.
|
|
130
|
+
*/
|
|
131
|
+
export declare function s3Storage(options: S3StorageOptions): UploadStorage;
|
|
132
|
+
interface R2StorageOptions extends Omit<S3StorageOptions, "region" | "endpoint" | "forcePathStyle"> {
|
|
133
|
+
/** Your Cloudflare account id. */
|
|
134
|
+
accountId: string;
|
|
135
|
+
/** `"eu"` or `"fedramp"` for jurisdiction-restricted buckets. */
|
|
136
|
+
jurisdiction?: "eu" | "fedramp";
|
|
137
|
+
}
|
|
138
|
+
/** Cloudflare R2 through its S3-compatible API. */
|
|
139
|
+
export declare function r2Storage({ accountId, jurisdiction, ...options }: R2StorageOptions): UploadStorage;
|
|
140
|
+
//#endregion
|
|
141
|
+
//#region src/memory.d.ts
|
|
142
|
+
interface MemoryStorageOptions {
|
|
143
|
+
/**
|
|
144
|
+
* Absolute or root-relative URL where `handler` is mounted, e.g.
|
|
145
|
+
* `/api/storage`. Presigned URLs point here.
|
|
146
|
+
*/
|
|
147
|
+
baseUrl: string;
|
|
148
|
+
/** HMAC secret used to sign URLs. Use a random value per deployment. */
|
|
149
|
+
secret: string;
|
|
150
|
+
/** Largest accepted request body, in bytes. Default 5 GiB. */
|
|
151
|
+
maxBodySize?: number;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* A development storage that accepts uploads in-process and keeps only
|
|
155
|
+
* metadata (size, ETag) — the bytes are discarded. It implements the full
|
|
156
|
+
* presigned + multipart flow with HMAC-signed URLs, so you can build and
|
|
157
|
+
* test upload UIs without cloud credentials. Not for production.
|
|
158
|
+
*/
|
|
159
|
+
export declare function createMemoryStorage(options: MemoryStorageOptions): {
|
|
160
|
+
storage: UploadStorage;
|
|
161
|
+
handler: (request: Request) => Promise<Response>;
|
|
162
|
+
};
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/cloudinary.d.ts
|
|
165
|
+
/**
|
|
166
|
+
* Signs Cloudinary upload parameters with your API secret, for
|
|
167
|
+
* `cloudinaryAdapter({ sign })`. Run it on the server only.
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* // app/api/cloudinary/route.ts
|
|
171
|
+
* export async function POST(request: Request) {
|
|
172
|
+
* const params = await request.json()
|
|
173
|
+
* if (params.folder !== "avatars") return new Response(null, { status: 403 })
|
|
174
|
+
* return Response.json(
|
|
175
|
+
* await signCloudinaryParams(params, {
|
|
176
|
+
* apiKey: process.env.CLOUDINARY_API_KEY!,
|
|
177
|
+
* apiSecret: process.env.CLOUDINARY_API_SECRET!,
|
|
178
|
+
* })
|
|
179
|
+
* )
|
|
180
|
+
* }
|
|
181
|
+
*/
|
|
182
|
+
export declare function signCloudinaryParams(params: Record<string, string | number>, { apiKey, apiSecret, timestamp }: {
|
|
183
|
+
apiKey: string;
|
|
184
|
+
apiSecret: string;
|
|
185
|
+
timestamp?: number;
|
|
186
|
+
}): Promise<{
|
|
187
|
+
signature: string;
|
|
188
|
+
timestamp: number;
|
|
189
|
+
apiKey: string;
|
|
190
|
+
}>;
|
|
191
|
+
//#endregion
|
|
192
|
+
export type { FileDescriptor, MemoryStorageOptions, PresignedRequest, R2StorageOptions, S3StorageOptions, StoragePart, StoredObject, StoredObjectInfo, UploadRouteAction, UploadRouteContext, UploadRouteOptions, UploadRouteRequest, UploadStorage };
|
|
193
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":["FileDescriptor","UploadRouteAction"],"sources":["../src/route.ts","../src/s3.ts","../src/memory.ts","../src/cloudinary.ts"],"mappings":";;;;qBAWa,yBAAyB;WAIzB;;WAEA;WALO;EAEhB,YAAA,iBACS,iBAEA;;EAAA;;UAMI,kBAAkB;;EAEjC,SAAS;;;;;EAKT;;EAEA;;EAEA,YACE,SAAS,mBAAmB;IAC1B;IACA,MAAMA;IACN,QAAQ;eAEA;;UAGG,mBAAmB;EAClC,SAAS;EACT,QAAQC;;EAER,MAAM;;UAGS,mBAAmB;EAClC,SAAS;;;;;;EAMT,aAAa;IACX,SAAS;IACT,QAAQA;IACR;QACI,QAAQ,QAAQ;;EAEtB;;EAEA;;EAEA,UACE,SAAS,mBAAmB;IAC1B,MAAMD;IACN,MAAM;iBAEI;;EAEd;;;;;;EAMA,oBACE,SAAS,mBAAmB;IAC1B;IACA;IACA,MAAMA;IACN,MAAM;;IAEN,OAAO;;;EAIX;;;;;;EAMA,OAAO,kBAAkB;;wBAKX,iBAAiB;;;;;;;;;;;wBAyIjB,kBAAkB,mBAChC,SAAS,mBAAmB;EA4HG,OAAA,SAAA,YAAU,QAAQ;EAAlB,UAAA,SAAA,YAAU,QAAQ;;;;UCxWlC;EACf;;EAEA;EACA;EACA;EACA;;;;;EAKA;;EAEA;;;;;EAKA;;EAEA;;qBAoCW,uBAAuB;WAIvB;WACA;WAJO;EAEhB,YAAA,iBACS,gBACA;;;;;;wBAuBG,UAAU,SAAS,mBAAmB;UAoKrC,yBAAyB,KACxC;;EAIA;;EAEA;;;wBAIc,YACd,WACA,iBACG,WACF,mBAAmB;;;UCxQL;;;;;EAKf;;EAEA;;EAEA;;;;;;;;wBASc,oBAAoB,SAAS;;EA2DX,UAAA,SAAA,YAAU,QAAQ;;;;;;;;;;;;;;;;;;;;;wBC/D9B,qBACpB,QAAQ,mCAEN,QACA,WACA;EACG;EAAgB;EAAmB;IAAoB"}
|