@cedarjs/uploads 7.0.0-canary.3114
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 +24 -0
- package/dist/authenticator.d.ts +38 -0
- package/dist/authenticator.d.ts.map +1 -0
- package/dist/authenticator.js +51 -0
- package/dist/cleanupStaleUploads.d.ts +50 -0
- package/dist/cleanupStaleUploads.d.ts.map +1 -0
- package/dist/cleanupStaleUploads.js +80 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +4 -0
- package/dist/deleteFile.d.ts +14 -0
- package/dist/deleteFile.d.ts.map +1 -0
- package/dist/deleteFile.js +18 -0
- package/dist/directives.d.ts +55 -0
- package/dist/directives.d.ts.map +1 -0
- package/dist/directives.js +174 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +33 -0
- package/dist/fastify/plugin.d.ts +49 -0
- package/dist/fastify/plugin.d.ts.map +1 -0
- package/dist/fastify/plugin.js +241 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +88 -0
- package/dist/keys.d.ts +18 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +25 -0
- package/dist/pending.d.ts +39 -0
- package/dist/pending.d.ts.map +1 -0
- package/dist/pending.js +94 -0
- package/dist/profiles.d.ts +49 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +68 -0
- package/dist/providers/db.d.ts +9 -0
- package/dist/providers/db.d.ts.map +1 -0
- package/dist/providers/db.js +41 -0
- package/dist/providers/fs.d.ts +34 -0
- package/dist/providers/fs.d.ts.map +1 -0
- package/dist/providers/fs.js +125 -0
- package/dist/providers/s3.d.ts +25 -0
- package/dist/providers/s3.d.ts.map +1 -0
- package/dist/providers/s3.js +117 -0
- package/dist/s3.d.ts +3 -0
- package/dist/s3.d.ts.map +1 -0
- package/dist/s3.js +4 -0
- package/dist/serialize.d.ts +10 -0
- package/dist/serialize.d.ts.map +1 -0
- package/dist/serialize.js +22 -0
- package/dist/serveToken.d.ts +36 -0
- package/dist/serveToken.d.ts.map +1 -0
- package/dist/serveToken.js +45 -0
- package/dist/services.d.ts +70 -0
- package/dist/services.d.ts.map +1 -0
- package/dist/services.js +141 -0
- package/dist/storeFile.d.ts +27 -0
- package/dist/storeFile.d.ts.map +1 -0
- package/dist/storeFile.js +53 -0
- package/dist/targets.d.ts +14 -0
- package/dist/targets.d.ts.map +1 -0
- package/dist/targets.js +21 -0
- package/dist/types.d.ts +212 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +0 -0
- package/dist/uploadLoader.d.ts +11 -0
- package/dist/uploadLoader.d.ts.map +1 -0
- package/dist/uploadLoader.js +47 -0
- package/dist/uploadToken.d.ts +43 -0
- package/dist/uploadToken.d.ts.map +1 -0
- package/dist/uploadToken.js +61 -0
- package/dist/web/components/DbInput.d.ts +18 -0
- package/dist/web/components/DbInput.d.ts.map +1 -0
- package/dist/web/components/DbInput.js +46 -0
- package/dist/web/components/FsUploader.d.ts +6 -0
- package/dist/web/components/FsUploader.d.ts.map +1 -0
- package/dist/web/components/FsUploader.js +21 -0
- package/dist/web/components/S3Uploader.d.ts +6 -0
- package/dist/web/components/S3Uploader.d.ts.map +1 -0
- package/dist/web/components/S3Uploader.js +21 -0
- package/dist/web/components/UppyUploader.d.ts +23 -0
- package/dist/web/components/UppyUploader.d.ts.map +1 -0
- package/dist/web/components/UppyUploader.js +72 -0
- package/dist/web/createUppy.d.ts +62 -0
- package/dist/web/createUppy.d.ts.map +1 -0
- package/dist/web/createUppy.js +82 -0
- package/dist/web/graphql.d.ts +48 -0
- package/dist/web/graphql.d.ts.map +1 -0
- package/dist/web/graphql.js +36 -0
- package/dist/web/hooks/useDbUpload.d.ts +31 -0
- package/dist/web/hooks/useDbUpload.d.ts.map +1 -0
- package/dist/web/hooks/useDbUpload.js +80 -0
- package/dist/web/hooks/useFsUpload.d.ts +20 -0
- package/dist/web/hooks/useFsUpload.d.ts.map +1 -0
- package/dist/web/hooks/useFsUpload.js +58 -0
- package/dist/web/hooks/useS3Upload.d.ts +23 -0
- package/dist/web/hooks/useS3Upload.d.ts.map +1 -0
- package/dist/web/hooks/useS3Upload.js +99 -0
- package/dist/web/hooks/useUploadToken.d.ts +34 -0
- package/dist/web/hooks/useUploadToken.d.ts.map +1 -0
- package/dist/web/hooks/useUploadToken.js +60 -0
- package/dist/web/hooks/useUppyUpload.d.ts +23 -0
- package/dist/web/hooks/useUppyUpload.d.ts.map +1 -0
- package/dist/web/hooks/useUppyUpload.js +76 -0
- package/dist/web/index.d.ts +22 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +36 -0
- package/dist/webhooks/s3.d.ts +52 -0
- package/dist/webhooks/s3.d.ts.map +1 -0
- package/dist/webhooks/s3.js +122 -0
- package/dist/webhooks/sns.d.ts +34 -0
- package/dist/webhooks/sns.d.ts.map +1 -0
- package/dist/webhooks/sns.js +131 -0
- package/package.json +147 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { StorageProvider } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Marks a target whose bytes live inline in the `Upload.data` column. The
|
|
4
|
+
* provider itself owns no storage: `storeFile()` writes the row and the
|
|
5
|
+
* `@withSignedUrl` / `@withDataUri` directives read it back as a `data:` URI.
|
|
6
|
+
* Every method that would need external storage throws.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createDbProvider(): StorageProvider;
|
|
9
|
+
//# sourceMappingURL=db.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/providers/db.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,eAAe,CAoDlD"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { UploadError } from "../errors.js";
|
|
2
|
+
function createDbProvider() {
|
|
3
|
+
const notSupported = (operation) => new UploadError(
|
|
4
|
+
"NOT_SUPPORTED",
|
|
5
|
+
`The DB storage target '${provider.name}' keeps bytes inline in the Upload row, so \`${operation}\` is not available. Use storeFile() to write and the Upload row itself to read.`
|
|
6
|
+
);
|
|
7
|
+
const provider = {
|
|
8
|
+
name: "",
|
|
9
|
+
providerType: "db",
|
|
10
|
+
async write() {
|
|
11
|
+
throw notSupported("write");
|
|
12
|
+
},
|
|
13
|
+
async read() {
|
|
14
|
+
throw notSupported("read");
|
|
15
|
+
},
|
|
16
|
+
async delete() {
|
|
17
|
+
},
|
|
18
|
+
async exists() {
|
|
19
|
+
return false;
|
|
20
|
+
},
|
|
21
|
+
async getObjectSize() {
|
|
22
|
+
return null;
|
|
23
|
+
},
|
|
24
|
+
async getSignedReadUrl() {
|
|
25
|
+
throw notSupported("getSignedReadUrl");
|
|
26
|
+
},
|
|
27
|
+
async getPresignedUploadUrl() {
|
|
28
|
+
throw new UploadError(
|
|
29
|
+
"PRESIGN_NOT_SUPPORTED",
|
|
30
|
+
`The DB storage target '${provider.name}' does not support presigned uploads. Send the file as base64 through GraphQL and call storeFile() in the service.`
|
|
31
|
+
);
|
|
32
|
+
},
|
|
33
|
+
getConfig() {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
return provider;
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
createDbProvider
|
|
41
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { StorageProvider } from '../types.js';
|
|
2
|
+
export interface FsProviderOptions {
|
|
3
|
+
/** Directory files are written to. Created on first write. */
|
|
4
|
+
uploadDir: string;
|
|
5
|
+
/**
|
|
6
|
+
* Origin (and optional path) the api server is reachable at, for example
|
|
7
|
+
* `https://api.example.com`. Required for `getSignedReadUrl()`. Signed
|
|
8
|
+
* URLs carry a bearer token, so the origin must use `https:` unless it
|
|
9
|
+
* is a loopback address used in development.
|
|
10
|
+
*/
|
|
11
|
+
serveBaseUrl?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Secret that signs serve URLs. Use the same value the upload plugin is
|
|
14
|
+
* given as `tokenSecret`, because the serve route verifies with that.
|
|
15
|
+
*/
|
|
16
|
+
signSecret?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Prefix the upload plugin is registered under. Defaults to `/upload`.
|
|
19
|
+
*/
|
|
20
|
+
routePrefix?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Validates the origin signed URLs are built on. Plain `http:` is only
|
|
24
|
+
* accepted for loopback hosts, so a bearer token never travels in clear
|
|
25
|
+
* text to a real network address.
|
|
26
|
+
*/
|
|
27
|
+
export declare function assertServeBaseUrl(serveBaseUrl: string, targetName: string): void;
|
|
28
|
+
/**
|
|
29
|
+
* Stores files on the local filesystem. Uploads reach it through the upload
|
|
30
|
+
* plugin's `POST {prefix}/fs` route and are served by
|
|
31
|
+
* `GET {prefix}/serve?token=...`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createFsProvider(opts: FsProviderOptions): StorageProvider;
|
|
34
|
+
//# sourceMappingURL=fs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/providers/fs.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,MAAM,WAAW,iBAAiB;IAChC,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAaD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,QA0B1E;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,eAAe,CAgHzE"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { UploadError } from "../errors.js";
|
|
5
|
+
import { assertSafeKey, trimTrailingSlashes } from "../keys.js";
|
|
6
|
+
import {
|
|
7
|
+
createServeToken,
|
|
8
|
+
DEFAULT_SERVE_URL_EXPIRES_IN,
|
|
9
|
+
SERVE_ROUTE_PATH,
|
|
10
|
+
SERVE_TOKEN_PARAM
|
|
11
|
+
} from "../serveToken.js";
|
|
12
|
+
const LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
13
|
+
function isNotFound(e) {
|
|
14
|
+
return typeof e === "object" && e !== null && "code" in e && (e.code === "ENOENT" || e.code === "ENOTDIR");
|
|
15
|
+
}
|
|
16
|
+
function assertServeBaseUrl(serveBaseUrl, targetName) {
|
|
17
|
+
let parsed;
|
|
18
|
+
try {
|
|
19
|
+
parsed = new URL(serveBaseUrl);
|
|
20
|
+
} catch {
|
|
21
|
+
throw new UploadError(
|
|
22
|
+
"CONFIGURATION",
|
|
23
|
+
`The FS storage target '${targetName}' has an invalid serveBaseUrl: '${serveBaseUrl}'.`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const secure = parsed.protocol === "https:" || parsed.protocol === "http:" && LOOPBACK_HOSTS.has(parsed.hostname.toLowerCase());
|
|
27
|
+
if (!secure) {
|
|
28
|
+
throw new UploadError(
|
|
29
|
+
"CONFIGURATION",
|
|
30
|
+
`The FS storage target '${targetName}' has serveBaseUrl '${serveBaseUrl}'. Signed URLs carry a bearer token, so the origin must use https, or http on localhost for development.`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function createFsProvider(opts) {
|
|
35
|
+
const { uploadDir, serveBaseUrl, signSecret, routePrefix = "/upload" } = opts;
|
|
36
|
+
const filePath = (key) => {
|
|
37
|
+
assertSafeKey(key);
|
|
38
|
+
return path.join(uploadDir, key);
|
|
39
|
+
};
|
|
40
|
+
const provider = {
|
|
41
|
+
name: "",
|
|
42
|
+
providerType: "fs",
|
|
43
|
+
async write(key, data) {
|
|
44
|
+
const target = filePath(key);
|
|
45
|
+
await fs.mkdir(uploadDir, { recursive: true });
|
|
46
|
+
await fs.writeFile(target, data);
|
|
47
|
+
},
|
|
48
|
+
async read(key) {
|
|
49
|
+
return fs.readFile(filePath(key));
|
|
50
|
+
},
|
|
51
|
+
async readStream(key) {
|
|
52
|
+
const target = filePath(key);
|
|
53
|
+
await fs.access(target);
|
|
54
|
+
return createReadStream(target);
|
|
55
|
+
},
|
|
56
|
+
async delete(key) {
|
|
57
|
+
try {
|
|
58
|
+
await fs.unlink(filePath(key));
|
|
59
|
+
} catch (e) {
|
|
60
|
+
if (!isNotFound(e)) {
|
|
61
|
+
throw e;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
async exists(key) {
|
|
66
|
+
try {
|
|
67
|
+
await fs.access(filePath(key));
|
|
68
|
+
return true;
|
|
69
|
+
} catch (e) {
|
|
70
|
+
if (isNotFound(e)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
throw e;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
async getObjectSize(key) {
|
|
77
|
+
try {
|
|
78
|
+
const stat = await fs.stat(filePath(key));
|
|
79
|
+
return stat.size;
|
|
80
|
+
} catch (e) {
|
|
81
|
+
if (isNotFound(e)) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
throw e;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
async getSignedReadUrl(key, options = {}) {
|
|
88
|
+
assertSafeKey(key);
|
|
89
|
+
if (!signSecret || !serveBaseUrl) {
|
|
90
|
+
throw new UploadError(
|
|
91
|
+
"CONFIGURATION",
|
|
92
|
+
`The FS storage target '${provider.name}' needs both \`serveBaseUrl\` and \`signSecret\` to generate signed read URLs.`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
assertServeBaseUrl(serveBaseUrl, provider.name);
|
|
96
|
+
const {
|
|
97
|
+
expiresIn = DEFAULT_SERVE_URL_EXPIRES_IN,
|
|
98
|
+
disposition = "attachment"
|
|
99
|
+
} = options;
|
|
100
|
+
const token = createServeToken({
|
|
101
|
+
payload: { target: provider.name, key, disposition },
|
|
102
|
+
secret: signSecret,
|
|
103
|
+
expiresIn
|
|
104
|
+
});
|
|
105
|
+
const base = trimTrailingSlashes(serveBaseUrl);
|
|
106
|
+
const prefix = trimTrailingSlashes(routePrefix);
|
|
107
|
+
const params = new URLSearchParams({ [SERVE_TOKEN_PARAM]: token });
|
|
108
|
+
return `${base}${prefix}${SERVE_ROUTE_PATH}?${params}`;
|
|
109
|
+
},
|
|
110
|
+
async getPresignedUploadUrl() {
|
|
111
|
+
throw new UploadError(
|
|
112
|
+
"PRESIGN_NOT_SUPPORTED",
|
|
113
|
+
`The FS storage target '${provider.name}' does not support presigned uploads. Upload through the \`POST {prefix}/fs\` route.`
|
|
114
|
+
);
|
|
115
|
+
},
|
|
116
|
+
getConfig() {
|
|
117
|
+
return { uploadDir };
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
return provider;
|
|
121
|
+
}
|
|
122
|
+
export {
|
|
123
|
+
assertServeBaseUrl,
|
|
124
|
+
createFsProvider
|
|
125
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { S3Client } from '@aws-sdk/client-s3';
|
|
2
|
+
import type { StorageProvider } from '../types.js';
|
|
3
|
+
export interface S3ProviderOptions {
|
|
4
|
+
/** The app's own S3 client. It stays available for provider-specific work. */
|
|
5
|
+
client: S3Client;
|
|
6
|
+
bucket: string;
|
|
7
|
+
/**
|
|
8
|
+
* Region of the bucket, reported by `getConfig()` for third-party
|
|
9
|
+
* integrations. The client's own region setting is used for requests.
|
|
10
|
+
*/
|
|
11
|
+
region?: string;
|
|
12
|
+
/** Prepended to every key, for example `avatars/`. */
|
|
13
|
+
keyPrefix?: string;
|
|
14
|
+
/** Lifetime of presigned upload URLs, in seconds. Defaults to 300. */
|
|
15
|
+
uploadUrlExpiresIn?: number;
|
|
16
|
+
/** Lifetime of signed read URLs, in seconds. Defaults to 3600. */
|
|
17
|
+
readUrlExpiresIn?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Stores files in an S3 bucket (or any S3-compatible service) with the AWS
|
|
21
|
+
* SDK. Import it from `@cedarjs/uploads/s3`; the SDK packages are optional
|
|
22
|
+
* peer dependencies.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createS3Provider(opts: S3ProviderOptions): StorageProvider;
|
|
25
|
+
//# sourceMappingURL=s3.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../../src/providers/s3.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAGlD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,MAAM,WAAW,iBAAiB;IAChC,8EAA8E;IAC9E,MAAM,EAAE,QAAQ,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sEAAsE;IACtE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,kEAAkE;IAClE,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAmBD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,eAAe,CAwHzE"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DeleteObjectCommand,
|
|
3
|
+
GetObjectCommand,
|
|
4
|
+
HeadObjectCommand,
|
|
5
|
+
PutObjectCommand
|
|
6
|
+
} from "@aws-sdk/client-s3";
|
|
7
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
8
|
+
function isNotFound(e) {
|
|
9
|
+
if (typeof e !== "object" || e === null) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const name = "name" in e ? e.name : void 0;
|
|
13
|
+
const status = "$metadata" in e && typeof e.$metadata === "object" && e.$metadata !== null && "httpStatusCode" in e.$metadata ? e.$metadata.httpStatusCode : void 0;
|
|
14
|
+
return name === "NotFound" || name === "NoSuchKey" || status === 404;
|
|
15
|
+
}
|
|
16
|
+
function createS3Provider(opts) {
|
|
17
|
+
const {
|
|
18
|
+
client,
|
|
19
|
+
bucket,
|
|
20
|
+
region,
|
|
21
|
+
keyPrefix = "",
|
|
22
|
+
uploadUrlExpiresIn = 300,
|
|
23
|
+
readUrlExpiresIn = 3600
|
|
24
|
+
} = opts;
|
|
25
|
+
const fullKey = (key) => `${keyPrefix}${key}`;
|
|
26
|
+
const provider = {
|
|
27
|
+
name: "",
|
|
28
|
+
providerType: "s3",
|
|
29
|
+
async write(key, data, { contentType }) {
|
|
30
|
+
await client.send(
|
|
31
|
+
new PutObjectCommand({
|
|
32
|
+
Bucket: bucket,
|
|
33
|
+
Key: fullKey(key),
|
|
34
|
+
Body: data,
|
|
35
|
+
ContentType: contentType
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
async read(key) {
|
|
40
|
+
const res = await client.send(
|
|
41
|
+
new GetObjectCommand({ Bucket: bucket, Key: fullKey(key) })
|
|
42
|
+
);
|
|
43
|
+
if (!res.Body) {
|
|
44
|
+
return Buffer.alloc(0);
|
|
45
|
+
}
|
|
46
|
+
return Buffer.from(await res.Body.transformToByteArray());
|
|
47
|
+
},
|
|
48
|
+
async readStream(key) {
|
|
49
|
+
const res = await client.send(
|
|
50
|
+
new GetObjectCommand({ Bucket: bucket, Key: fullKey(key) })
|
|
51
|
+
);
|
|
52
|
+
return res.Body;
|
|
53
|
+
},
|
|
54
|
+
async delete(key) {
|
|
55
|
+
await client.send(
|
|
56
|
+
new DeleteObjectCommand({ Bucket: bucket, Key: fullKey(key) })
|
|
57
|
+
);
|
|
58
|
+
},
|
|
59
|
+
async exists(key) {
|
|
60
|
+
return await provider.getObjectSize(key) !== null;
|
|
61
|
+
},
|
|
62
|
+
async getObjectSize(key) {
|
|
63
|
+
try {
|
|
64
|
+
const res = await client.send(
|
|
65
|
+
new HeadObjectCommand({ Bucket: bucket, Key: fullKey(key) })
|
|
66
|
+
);
|
|
67
|
+
return typeof res.ContentLength === "number" ? res.ContentLength : null;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
if (isNotFound(e)) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
throw e;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
async getSignedReadUrl(key, options = {}) {
|
|
76
|
+
const { expiresIn = readUrlExpiresIn, disposition = "attachment" } = options;
|
|
77
|
+
return getSignedUrl(
|
|
78
|
+
client,
|
|
79
|
+
new GetObjectCommand({
|
|
80
|
+
Bucket: bucket,
|
|
81
|
+
Key: fullKey(key),
|
|
82
|
+
// The stored MIME type is client-asserted, so inline is opt-in
|
|
83
|
+
ResponseContentDisposition: disposition
|
|
84
|
+
}),
|
|
85
|
+
{ expiresIn }
|
|
86
|
+
);
|
|
87
|
+
},
|
|
88
|
+
async getPresignedUploadUrl(key, { contentType, expiresIn }) {
|
|
89
|
+
const url = await getSignedUrl(
|
|
90
|
+
client,
|
|
91
|
+
new PutObjectCommand({
|
|
92
|
+
Bucket: bucket,
|
|
93
|
+
Key: fullKey(key),
|
|
94
|
+
ContentType: contentType
|
|
95
|
+
}),
|
|
96
|
+
{
|
|
97
|
+
expiresIn: expiresIn ?? uploadUrlExpiresIn,
|
|
98
|
+
// Sign the content type as a header so the upload must send the
|
|
99
|
+
// exact type the URL was issued for
|
|
100
|
+
signableHeaders: /* @__PURE__ */ new Set(["content-type"])
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
return {
|
|
104
|
+
url,
|
|
105
|
+
method: "PUT",
|
|
106
|
+
headers: { "Content-Type": contentType }
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
getConfig() {
|
|
110
|
+
return { bucket, region, keyPrefix };
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
return provider;
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
createS3Provider
|
|
117
|
+
};
|
package/dist/s3.d.ts
ADDED
package/dist/s3.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../src/s3.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACpD,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA"}
|
package/dist/s3.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SerializedUpload, UploadRecord } from './types.js';
|
|
2
|
+
/** Builds a `data:` URI from a MIME type and raw bytes. */
|
|
3
|
+
export declare function toDataUri(mimeType: string, data: Uint8Array): string;
|
|
4
|
+
/**
|
|
5
|
+
* Converts an `Upload` row into plain JSON: `size` becomes a string (it is a
|
|
6
|
+
* `BigInt`), dates become ISO strings, and the inline `data` and `tokenId`
|
|
7
|
+
* columns are left out.
|
|
8
|
+
*/
|
|
9
|
+
export declare function serializeUpload(upload: UploadRecord): SerializedUpload;
|
|
10
|
+
//# sourceMappingURL=serialize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../src/serialize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAEhE,2DAA2D;AAC3D,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAEpE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,YAAY,GAAG,gBAAgB,CActE"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function toDataUri(mimeType, data) {
|
|
2
|
+
return `data:${mimeType};base64,${Buffer.from(data).toString("base64")}`;
|
|
3
|
+
}
|
|
4
|
+
function serializeUpload(upload) {
|
|
5
|
+
return {
|
|
6
|
+
id: upload.id,
|
|
7
|
+
target: upload.target,
|
|
8
|
+
status: upload.status,
|
|
9
|
+
filename: upload.filename,
|
|
10
|
+
mimeType: upload.mimeType,
|
|
11
|
+
size: upload.size.toString(),
|
|
12
|
+
storageKey: upload.storageKey,
|
|
13
|
+
userId: upload.userId,
|
|
14
|
+
organizationId: upload.organizationId,
|
|
15
|
+
createdAt: upload.createdAt.toISOString(),
|
|
16
|
+
updatedAt: upload.updatedAt.toISOString()
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
serializeUpload,
|
|
21
|
+
toDataUri
|
|
22
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ContentDisposition } from './types.js';
|
|
2
|
+
/** Purpose claim for the signed URLs the FS serve route accepts. */
|
|
3
|
+
export declare const SERVE_TOKEN_PURPOSE = "cedar-upload-serve";
|
|
4
|
+
/** Default lifetime of a signed serve URL, in seconds. */
|
|
5
|
+
export declare const DEFAULT_SERVE_URL_EXPIRES_IN = 3600;
|
|
6
|
+
/** Route (relative to the plugin prefix) that serves files by token. */
|
|
7
|
+
export declare const SERVE_ROUTE_PATH = "/serve";
|
|
8
|
+
/**
|
|
9
|
+
* Query parameter the serve route reads the token from. The token is a JWT,
|
|
10
|
+
* which is longer than Fastify's default limit for path parameters.
|
|
11
|
+
*/
|
|
12
|
+
export declare const SERVE_TOKEN_PARAM = "token";
|
|
13
|
+
export interface ServeTokenPayload {
|
|
14
|
+
target: string;
|
|
15
|
+
key: string;
|
|
16
|
+
disposition: ContentDisposition;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Signs a serve token. The disposition is part of the signed payload, so
|
|
20
|
+
* inline rendering is granted by whoever signs the URL, never by whoever
|
|
21
|
+
* requests it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createServeToken({ payload, secret, expiresIn, }: {
|
|
24
|
+
payload: ServeTokenPayload;
|
|
25
|
+
secret: string;
|
|
26
|
+
/** Seconds. */
|
|
27
|
+
expiresIn?: number;
|
|
28
|
+
}): string;
|
|
29
|
+
/**
|
|
30
|
+
* Verifies a serve token. Every failure is reported as `NOT_FOUND` so the
|
|
31
|
+
* serve route does not distinguish an expired link from a forged one.
|
|
32
|
+
*/
|
|
33
|
+
export declare function verifyServeToken(token: string | null | undefined, { secret }: {
|
|
34
|
+
secret: string;
|
|
35
|
+
}): ServeTokenPayload;
|
|
36
|
+
//# sourceMappingURL=serveToken.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serveToken.d.ts","sourceRoot":"","sources":["../src/serveToken.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAEpD,oEAAoE;AACpE,eAAO,MAAM,mBAAmB,uBAAuB,CAAA;AAEvD,0DAA0D;AAC1D,eAAO,MAAM,4BAA4B,OAAO,CAAA;AAEhD,wEAAwE;AACxE,eAAO,MAAM,gBAAgB,WAAW,CAAA;AAExC;;;GAGG;AACH,eAAO,MAAM,iBAAiB,UAAU,CAAA;AAExC,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,EAAE,kBAAkB,CAAA;CAChC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,MAAM,EACN,SAAwC,GACzC,EAAE;IACD,OAAO,EAAE,iBAAiB,CAAA;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,eAAe;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GAAG,MAAM,CAOT;AAgBD;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7B,iBAAiB,CAcnB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createSignedToken, verifySignedToken } from "@cedarjs/api";
|
|
2
|
+
import { UploadError } from "./errors.js";
|
|
3
|
+
const SERVE_TOKEN_PURPOSE = "cedar-upload-serve";
|
|
4
|
+
const DEFAULT_SERVE_URL_EXPIRES_IN = 3600;
|
|
5
|
+
const SERVE_ROUTE_PATH = "/serve";
|
|
6
|
+
const SERVE_TOKEN_PARAM = "token";
|
|
7
|
+
function createServeToken({
|
|
8
|
+
payload,
|
|
9
|
+
secret,
|
|
10
|
+
expiresIn = DEFAULT_SERVE_URL_EXPIRES_IN
|
|
11
|
+
}) {
|
|
12
|
+
return createSignedToken({
|
|
13
|
+
payload,
|
|
14
|
+
purpose: SERVE_TOKEN_PURPOSE,
|
|
15
|
+
expiresIn,
|
|
16
|
+
secret
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function isServeTokenPayload(value) {
|
|
20
|
+
if (typeof value !== "object" || value === null) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const p = value;
|
|
24
|
+
return typeof p.target === "string" && typeof p.key === "string" && (p.disposition === "attachment" || p.disposition === "inline");
|
|
25
|
+
}
|
|
26
|
+
function verifyServeToken(token, { secret }) {
|
|
27
|
+
let payload;
|
|
28
|
+
try {
|
|
29
|
+
payload = verifySignedToken(token, { purpose: SERVE_TOKEN_PURPOSE, secret });
|
|
30
|
+
} catch (e) {
|
|
31
|
+
throw new UploadError("NOT_FOUND", "File not found.", e);
|
|
32
|
+
}
|
|
33
|
+
if (!isServeTokenPayload(payload)) {
|
|
34
|
+
throw new UploadError("NOT_FOUND", "File not found.");
|
|
35
|
+
}
|
|
36
|
+
return payload;
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
DEFAULT_SERVE_URL_EXPIRES_IN,
|
|
40
|
+
SERVE_ROUTE_PATH,
|
|
41
|
+
SERVE_TOKEN_PARAM,
|
|
42
|
+
SERVE_TOKEN_PURPOSE,
|
|
43
|
+
createServeToken,
|
|
44
|
+
verifyServeToken
|
|
45
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { UploadProfiles } from './profiles.js';
|
|
2
|
+
import type { StorageTargets, UploadDatabase, UploadRecord } from './types.js';
|
|
3
|
+
import type { UploadTokenPayload } from './uploadToken.js';
|
|
4
|
+
/** The identity a token is issued to. */
|
|
5
|
+
export interface UploadActor {
|
|
6
|
+
id: string | number;
|
|
7
|
+
organizationId?: string | null;
|
|
8
|
+
}
|
|
9
|
+
export interface IssueUploadTokenOptions {
|
|
10
|
+
profiles: UploadProfiles;
|
|
11
|
+
/** Name of the profile the client asked for. */
|
|
12
|
+
profile: string;
|
|
13
|
+
secret: string;
|
|
14
|
+
/** The requesting user. Tokens are always bound to a user. */
|
|
15
|
+
currentUser: UploadActor | null | undefined;
|
|
16
|
+
/** The organization to bind the token to, in multi-tenant apps. */
|
|
17
|
+
organizationId?: string | null;
|
|
18
|
+
}
|
|
19
|
+
export interface UploadTokenResponse {
|
|
20
|
+
token: string;
|
|
21
|
+
allowedMimeTypes: string[];
|
|
22
|
+
maxFileSize: bigint;
|
|
23
|
+
maxFiles: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Issues an upload token for a server-defined profile, bound to the
|
|
27
|
+
* requesting user. This is what the generated `requestUploadToken` resolver
|
|
28
|
+
* calls. The response echoes the profile's constraints for client-side UX;
|
|
29
|
+
* the authoritative copy is inside the signed token.
|
|
30
|
+
*/
|
|
31
|
+
export declare function issueUploadToken({ profiles, profile: profileName, secret, currentUser, organizationId, }: IssueUploadTokenOptions): UploadTokenResponse;
|
|
32
|
+
export interface CreatePresignedUploadInput {
|
|
33
|
+
filename: string;
|
|
34
|
+
contentType: string;
|
|
35
|
+
size: bigint | number | string;
|
|
36
|
+
}
|
|
37
|
+
export interface CreatePresignedUploadOptions {
|
|
38
|
+
db: UploadDatabase;
|
|
39
|
+
targets: StorageTargets;
|
|
40
|
+
/** The validated token, as attached to context by `@requireUploadToken`. */
|
|
41
|
+
tokenPayload: UploadTokenPayload;
|
|
42
|
+
input: CreatePresignedUploadInput;
|
|
43
|
+
}
|
|
44
|
+
export interface PresignedUploadResponse {
|
|
45
|
+
uploadId: string;
|
|
46
|
+
url: string;
|
|
47
|
+
method: string;
|
|
48
|
+
headers: Record<string, string>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Validates a file against the token, creates its `pending` row, and returns
|
|
52
|
+
* a presigned URL the client PUTs the bytes to. This is what the generated
|
|
53
|
+
* `createPresignedUploadUrl` resolver calls.
|
|
54
|
+
*/
|
|
55
|
+
export declare function createPresignedUpload({ db, targets, tokenPayload, input, }: CreatePresignedUploadOptions): Promise<PresignedUploadResponse>;
|
|
56
|
+
export interface ConfirmUploadOptions {
|
|
57
|
+
db: UploadDatabase;
|
|
58
|
+
targets: StorageTargets;
|
|
59
|
+
uploadId: string;
|
|
60
|
+
currentUser: UploadActor | null | undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Client-side confirmation of a direct upload. Verifies that the object
|
|
64
|
+
* exists in storage and that its size matches what the token authorized,
|
|
65
|
+
* then flips the row `pending` to `completed`. On a size mismatch the row is
|
|
66
|
+
* claimed `failed` and the object deleted. Only the user (and organization)
|
|
67
|
+
* the upload was issued to can confirm it.
|
|
68
|
+
*/
|
|
69
|
+
export declare function confirmUpload({ db, targets, uploadId, currentUser, }: ConfirmUploadOptions): Promise<UploadRecord>;
|
|
70
|
+
//# sourceMappingURL=services.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../src/services.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAEnD,OAAO,KAAK,EAEV,cAAc,EACd,cAAc,EACd,YAAY,EACb,MAAM,YAAY,CAAA;AAEnB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAE1D,yCAAyC;AACzC,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,cAAc,CAAA;IACxB,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,8DAA8D;IAC9D,WAAW,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,CAAA;IAC3C,mEAAmE;IACnE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,OAAO,EAAE,WAAW,EACpB,MAAM,EACN,WAAW,EACX,cAAc,GACf,EAAE,uBAAuB,GAAG,mBAAmB,CA+B/C;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAA;CAC/B;AAED,MAAM,WAAW,4BAA4B;IAC3C,EAAE,EAAE,cAAc,CAAA;IAClB,OAAO,EAAE,cAAc,CAAA;IACvB,4EAA4E;IAC5E,YAAY,EAAE,kBAAkB,CAAA;IAChC,KAAK,EAAE,0BAA0B,CAAA;CAClC;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAUD;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,EAC1C,EAAE,EACF,OAAO,EACP,YAAY,EACZ,KAAK,GACN,EAAE,4BAA4B,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAkCjE;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,cAAc,CAAA;IAClB,OAAO,EAAE,cAAc,CAAA;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,CAAA;CAC5C;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,EAClC,EAAE,EACF,OAAO,EACP,QAAQ,EACR,WAAW,GACZ,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,CAoE9C"}
|