@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
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# @cedarjs/uploads
|
|
2
|
+
|
|
3
|
+
File uploads and storage for CedarJS apps.
|
|
4
|
+
|
|
5
|
+
- **Named storage targets** backed by providers for S3 (`@cedarjs/uploads/s3`),
|
|
6
|
+
the local filesystem, and the database, all implementing one small
|
|
7
|
+
`StorageProvider` contract.
|
|
8
|
+
- **Upload profiles** that the server owns: clients name a profile, the server
|
|
9
|
+
signs its constraints into a short-lived, user-bound upload token.
|
|
10
|
+
- **A Fastify plugin** with token-gated routes for filesystem uploads,
|
|
11
|
+
signed-URL file serving, and S3 event webhooks.
|
|
12
|
+
- **GraphQL directives** (`@requireUploadToken`, `@withSignedUrl`,
|
|
13
|
+
`@withDataUri`) and service helpers for issuing tokens, presigning direct
|
|
14
|
+
uploads, and confirming them.
|
|
15
|
+
- **Lifecycle utilities** (`storeFile`, `deleteFile`, `cleanupStaleUploads`) for
|
|
16
|
+
server-generated files and explicit cleanup.
|
|
17
|
+
- **Web hooks and components** at `@cedarjs/uploads/web`, built on Uppy.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
yarn cedar setup uploads
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
See the [uploads documentation](https://cedarjs.com/docs/uploads) for the full
|
|
24
|
+
guide.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { FastifyRequest } from 'fastify';
|
|
2
|
+
import type { Decoder } from '@cedarjs/api';
|
|
3
|
+
/** The identity an upload route resolves for a request. */
|
|
4
|
+
export interface UploadRequestUser {
|
|
5
|
+
id: string;
|
|
6
|
+
organizationId?: string;
|
|
7
|
+
}
|
|
8
|
+
export type UploadAuthenticator = (req: FastifyRequest) => Promise<UploadRequestUser | null>;
|
|
9
|
+
export interface CreateUploadAuthenticatorOptions {
|
|
10
|
+
/** The app's auth decoder, for example from `@cedarjs/auth-dbauth-api`. */
|
|
11
|
+
authDecoder: Decoder | Decoder[];
|
|
12
|
+
/**
|
|
13
|
+
* The app's `getCurrentUser`, as exported from `api/src/lib/auth`. It
|
|
14
|
+
* receives the decoded session exactly as the GraphQL server passes it.
|
|
15
|
+
*/
|
|
16
|
+
getCurrentUser: (decoded: unknown, raw: {
|
|
17
|
+
type: string;
|
|
18
|
+
schema: string;
|
|
19
|
+
token: string;
|
|
20
|
+
}, req: {
|
|
21
|
+
event: Request;
|
|
22
|
+
request: Request;
|
|
23
|
+
}) => Promise<unknown>;
|
|
24
|
+
/**
|
|
25
|
+
* Picks the organization id off the current user, for multi-tenant apps.
|
|
26
|
+
* Defaults to a top-level `organizationId` property when present.
|
|
27
|
+
*/
|
|
28
|
+
getOrganizationId?: (currentUser: unknown) => string | undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Builds the `authenticate` callback for the upload plugin from the same
|
|
32
|
+
* pieces the GraphQL server uses: the request's auth header is decoded with
|
|
33
|
+
* the app's auth decoder and the result handed to the app's
|
|
34
|
+
* `getCurrentUser`. With it in place the upload routes reject a token whose
|
|
35
|
+
* `sub` belongs to someone other than the requester.
|
|
36
|
+
*/
|
|
37
|
+
export declare function createUploadAuthenticator({ authDecoder, getCurrentUser, getOrganizationId, }: CreateUploadAuthenticatorOptions): UploadAuthenticator;
|
|
38
|
+
//# sourceMappingURL=authenticator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authenticator.d.ts","sourceRoot":"","sources":["../src/authenticator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAG7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C,2DAA2D;AAC3D,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,MAAM,mBAAmB,GAAG,CAChC,GAAG,EAAE,cAAc,KAChB,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAA;AAEtC,MAAM,WAAW,gCAAgC;IAC/C,2EAA2E;IAC3E,WAAW,EAAE,OAAO,GAAG,OAAO,EAAE,CAAA;IAChC;;;OAGG;IACH,cAAc,EAAE,CACd,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EACpD,GAAG,EAAE;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,KACtC,OAAO,CAAC,OAAO,CAAC,CAAA;IACrB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,CAAC,WAAW,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAA;CACjE;AAkCD;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,WAAW,EACX,cAAc,EACd,iBAAyC,GAC1C,EAAE,gCAAgC,GAAG,mBAAmB,CA+BxD"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getAuthenticationContext } from "@cedarjs/api";
|
|
2
|
+
function toWebRequest(req) {
|
|
3
|
+
const headers = new Headers();
|
|
4
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
5
|
+
if (typeof value === "string") {
|
|
6
|
+
headers.set(name, value);
|
|
7
|
+
} else if (Array.isArray(value)) {
|
|
8
|
+
headers.set(name, value.join(", "));
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const host = req.headers.host ?? "localhost";
|
|
12
|
+
return new Request(`${req.protocol}://${host}${req.url}`, {
|
|
13
|
+
method: "GET",
|
|
14
|
+
headers
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function defaultOrganizationId(user) {
|
|
18
|
+
if (typeof user === "object" && user !== null && "organizationId" in user && typeof user.organizationId === "string") {
|
|
19
|
+
return user.organizationId;
|
|
20
|
+
}
|
|
21
|
+
return void 0;
|
|
22
|
+
}
|
|
23
|
+
function createUploadAuthenticator({
|
|
24
|
+
authDecoder,
|
|
25
|
+
getCurrentUser,
|
|
26
|
+
getOrganizationId = defaultOrganizationId
|
|
27
|
+
}) {
|
|
28
|
+
return async (req) => {
|
|
29
|
+
const request = toWebRequest(req);
|
|
30
|
+
const authContext = await getAuthenticationContext({
|
|
31
|
+
authDecoder,
|
|
32
|
+
event: request
|
|
33
|
+
});
|
|
34
|
+
if (!authContext) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const [decoded, raw] = authContext;
|
|
38
|
+
const user = await getCurrentUser(decoded, raw, { event: request, request });
|
|
39
|
+
if (typeof user !== "object" || user === null || !("id" in user) || typeof user.id !== "string" && typeof user.id !== "number") {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const organizationId = getOrganizationId(user);
|
|
43
|
+
return {
|
|
44
|
+
id: String(user.id),
|
|
45
|
+
...organizationId ? { organizationId } : {}
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
createUploadAuthenticator
|
|
51
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { StorageTargets, UploadDatabase, UploadRecord } from './types.js';
|
|
2
|
+
export interface CleanupStaleUploadsOptions {
|
|
3
|
+
db: UploadDatabase;
|
|
4
|
+
targets: StorageTargets;
|
|
5
|
+
/**
|
|
6
|
+
* Rows still `pending` after this many milliseconds are claimed as
|
|
7
|
+
* `failed`. Defaults to one hour, well past the five-minute validity of a
|
|
8
|
+
* presigned upload URL.
|
|
9
|
+
*/
|
|
10
|
+
olderThan?: number;
|
|
11
|
+
/**
|
|
12
|
+
* `failed` rows that changed within this many milliseconds are re-checked
|
|
13
|
+
* and their bytes deleted if they landed after the claim. Measured from
|
|
14
|
+
* the row's last update (the claim, or the last failed attempt), so a row
|
|
15
|
+
* claimed long after it was created still gets its retries. Defaults to
|
|
16
|
+
* one day.
|
|
17
|
+
*/
|
|
18
|
+
retryWindow?: number;
|
|
19
|
+
/** Largest batch of rows to process per call. Defaults to 500. */
|
|
20
|
+
batchSize?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Called for each row whose storage operation failed. Defaults to
|
|
23
|
+
* `console.error`. The sweep continues with the next row either way.
|
|
24
|
+
*/
|
|
25
|
+
onError?: (error: unknown, upload: UploadRecord) => void;
|
|
26
|
+
}
|
|
27
|
+
export interface CleanupStaleUploadsResult {
|
|
28
|
+
/** Rows moved from `pending` to `failed` by this run. */
|
|
29
|
+
claimed: number;
|
|
30
|
+
/** Objects deleted from storage by this run. */
|
|
31
|
+
deleted: number;
|
|
32
|
+
/** Rows whose storage operation failed and will be retried next run. */
|
|
33
|
+
errors: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Sweeps stale uploads. Presigned uploads that were issued but never
|
|
37
|
+
* completed, and route uploads that crashed between row creation and write,
|
|
38
|
+
* leave `pending` rows behind. This claims them with a conditional
|
|
39
|
+
* `pending` to `failed` update, deletes any bytes that did land, and keeps
|
|
40
|
+
* the rows as `failed` tombstones so byte deletion can be retried on the
|
|
41
|
+
* next run. A row that completes mid-sweep is skipped because its claim
|
|
42
|
+
* matches zero rows, and a claimed row can no longer complete.
|
|
43
|
+
*
|
|
44
|
+
* Once a tombstone's bytes are confirmed gone its `storageKey` is cleared,
|
|
45
|
+
* which drops it from the next run's re-check. A row whose target is
|
|
46
|
+
* unknown or whose provider fails is reported through `onError` and left
|
|
47
|
+
* for the next run; it never aborts the sweep. Run it from a recurring job.
|
|
48
|
+
*/
|
|
49
|
+
export declare function cleanupStaleUploads({ db, targets, olderThan, retryWindow, batchSize, onError, }: CleanupStaleUploadsOptions): Promise<CleanupStaleUploadsResult>;
|
|
50
|
+
//# sourceMappingURL=cleanupStaleUploads.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleanupStaleUploads.d.ts","sourceRoot":"","sources":["../src/cleanupStaleUploads.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAK9E,MAAM,WAAW,0BAA0B;IACzC,EAAE,EAAE,cAAc,CAAA;IAClB,OAAO,EAAE,cAAc,CAAA;IACvB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,KAAK,IAAI,CAAA;CACzD;AAED,MAAM,WAAW,yBAAyB;IACxC,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAA;IACf,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAA;IACf,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,mBAAmB,CAAC,EACxC,EAAE,EACF,OAAO,EACP,SAAoB,EACpB,WAAqB,EACrB,SAAe,EACf,OAC0E,GAC3E,EAAE,0BAA0B,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAmFjE"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { resolveTarget } from "./targets.js";
|
|
2
|
+
const ONE_HOUR = 60 * 60 * 1e3;
|
|
3
|
+
const ONE_DAY = 24 * ONE_HOUR;
|
|
4
|
+
async function cleanupStaleUploads({
|
|
5
|
+
db,
|
|
6
|
+
targets,
|
|
7
|
+
olderThan = ONE_HOUR,
|
|
8
|
+
retryWindow = ONE_DAY,
|
|
9
|
+
batchSize = 500,
|
|
10
|
+
onError = (error, upload) => console.error(`[cedar uploads] cleanup failed for ${upload.id}:`, error)
|
|
11
|
+
}) {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
let claimed = 0;
|
|
14
|
+
let deleted = 0;
|
|
15
|
+
let errors = 0;
|
|
16
|
+
const claimedIds = /* @__PURE__ */ new Set();
|
|
17
|
+
const reclaim = async (upload) => {
|
|
18
|
+
if (!upload.storageKey) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const provider = resolveTarget(targets, upload.target);
|
|
23
|
+
const existed = await provider.exists(upload.storageKey);
|
|
24
|
+
if (existed) {
|
|
25
|
+
await provider.delete(upload.storageKey);
|
|
26
|
+
}
|
|
27
|
+
await db.upload.updateMany({
|
|
28
|
+
where: { id: upload.id, status: "failed" },
|
|
29
|
+
data: { storageKey: null }
|
|
30
|
+
});
|
|
31
|
+
return existed;
|
|
32
|
+
} catch (e) {
|
|
33
|
+
errors += 1;
|
|
34
|
+
onError(e, upload);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const stale = await db.upload.findMany({
|
|
39
|
+
where: { status: "pending", createdAt: { lt: new Date(now - olderThan) } },
|
|
40
|
+
omit: { data: true },
|
|
41
|
+
orderBy: { createdAt: "asc" },
|
|
42
|
+
take: batchSize
|
|
43
|
+
});
|
|
44
|
+
for (const upload of stale) {
|
|
45
|
+
const { count } = await db.upload.updateMany({
|
|
46
|
+
where: { id: upload.id, status: "pending" },
|
|
47
|
+
data: { status: "failed" }
|
|
48
|
+
});
|
|
49
|
+
if (count !== 1) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
claimed += 1;
|
|
53
|
+
claimedIds.add(upload.id);
|
|
54
|
+
if (await reclaim(upload)) {
|
|
55
|
+
deleted += 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const tombstones = await db.upload.findMany({
|
|
59
|
+
where: {
|
|
60
|
+
status: "failed",
|
|
61
|
+
storageKey: { not: null },
|
|
62
|
+
updatedAt: { gte: new Date(now - retryWindow) }
|
|
63
|
+
},
|
|
64
|
+
omit: { data: true },
|
|
65
|
+
orderBy: { createdAt: "asc" },
|
|
66
|
+
take: batchSize
|
|
67
|
+
});
|
|
68
|
+
for (const upload of tombstones) {
|
|
69
|
+
if (claimedIds.has(upload.id)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (await reclaim(upload)) {
|
|
73
|
+
deleted += 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { claimed, deleted, errors };
|
|
77
|
+
}
|
|
78
|
+
export {
|
|
79
|
+
cleanupStaleUploads
|
|
80
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Values shared by the api and web entry points. This module must stay free
|
|
3
|
+
* of Node-only imports because the web bundle includes it.
|
|
4
|
+
*/
|
|
5
|
+
/** Header the client sends an upload token in. */
|
|
6
|
+
export declare const UPLOAD_TOKEN_HEADER = "x-upload-token";
|
|
7
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,kDAAkD;AAClD,eAAO,MAAM,mBAAmB,mBAAmB,CAAA"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StorageProvider, UploadDatabase, UploadRecord } from './types.js';
|
|
2
|
+
export interface DeleteFileOptions {
|
|
3
|
+
db: UploadDatabase;
|
|
4
|
+
upload: Pick<UploadRecord, 'id' | 'storageKey'>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Deletes a stored file: bytes first, then the `Upload` row. Bytes-first
|
|
8
|
+
* ordering means a crash between the two steps leaves a row whose object is
|
|
9
|
+
* gone, which the app can see and fix by calling again, whereas the reverse
|
|
10
|
+
* order would leave an unreachable object nothing references. A missing
|
|
11
|
+
* object or row is tolerated, so the call is idempotent.
|
|
12
|
+
*/
|
|
13
|
+
export declare function deleteFile(target: StorageProvider, { db, upload }: DeleteFileOptions): Promise<void>;
|
|
14
|
+
//# sourceMappingURL=deleteFile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deleteFile.d.ts","sourceRoot":"","sources":["../src/deleteFile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE/E,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,cAAc,CAAA;IAClB,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,GAAG,YAAY,CAAC,CAAA;CAChD;AAQD;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,eAAe,EACvB,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,iBAAiB,GAChC,OAAO,CAAC,IAAI,CAAC,CAYf"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function isRecordNotFound(e) {
|
|
2
|
+
return typeof e === "object" && e !== null && "code" in e && e.code === "P2025";
|
|
3
|
+
}
|
|
4
|
+
async function deleteFile(target, { db, upload }) {
|
|
5
|
+
if (upload.storageKey) {
|
|
6
|
+
await target.delete(upload.storageKey);
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
await db.upload.delete({ where: { id: upload.id } });
|
|
10
|
+
} catch (e) {
|
|
11
|
+
if (!isRecordNotFound(e)) {
|
|
12
|
+
throw e;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
deleteFile
|
|
18
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { TransformerDirective, ValidatorDirective } from '@cedarjs/graphql-server';
|
|
2
|
+
import type { ContentDisposition, StorageTargets, UploadDatabase } from './types.js';
|
|
3
|
+
import type { UploadTokenPayload } from './uploadToken.js';
|
|
4
|
+
/** Context key `@requireUploadToken` stores the validated token under. */
|
|
5
|
+
export declare const UPLOAD_TOKEN_CONTEXT_KEY = "uploadTokenPayload";
|
|
6
|
+
/**
|
|
7
|
+
* Reads the validated upload token `@requireUploadToken` attached to the
|
|
8
|
+
* GraphQL context. Throws when the resolver runs without the directive.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getUploadTokenPayload(context: Record<string, unknown>): UploadTokenPayload;
|
|
11
|
+
/** The GraphQL context, as directives receive it. */
|
|
12
|
+
export type DirectiveContext = Record<string, unknown>;
|
|
13
|
+
export interface RequireUploadTokenDirectiveOptions {
|
|
14
|
+
/** Secret upload tokens were signed with. */
|
|
15
|
+
secret: string;
|
|
16
|
+
/**
|
|
17
|
+
* Resolves the organization the request is acting under, for multi-tenant
|
|
18
|
+
* apps. Defaults to `context.currentUser.organizationId`.
|
|
19
|
+
*/
|
|
20
|
+
getOrganizationId?: (context: DirectiveContext) => string | null | undefined;
|
|
21
|
+
}
|
|
22
|
+
export declare const requireUploadTokenSchema: import("graphql").DocumentNode;
|
|
23
|
+
/**
|
|
24
|
+
* Builds the `@requireUploadToken` validator directive. It verifies the
|
|
25
|
+
* token in the `x-upload-token` header, rejects a token issued to a
|
|
26
|
+
* different user or organization, and attaches the claims to the context
|
|
27
|
+
* for `getUploadTokenPayload()`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createRequireUploadTokenDirective({ secret, getOrganizationId, }: RequireUploadTokenDirectiveOptions): ValidatorDirective;
|
|
30
|
+
export interface UploadUrlDirectiveOptions {
|
|
31
|
+
db: UploadDatabase;
|
|
32
|
+
targets: StorageTargets;
|
|
33
|
+
}
|
|
34
|
+
export interface WithSignedUrlDirectiveOptions extends UploadUrlDirectiveOptions {
|
|
35
|
+
/** Lifetime of generated URLs, in seconds. Provider default when unset. */
|
|
36
|
+
expiresIn?: number;
|
|
37
|
+
/** Defaults to `attachment`. */
|
|
38
|
+
disposition?: ContentDisposition;
|
|
39
|
+
}
|
|
40
|
+
export declare const withSignedUrlSchema: import("graphql").DocumentNode;
|
|
41
|
+
/**
|
|
42
|
+
* Builds the `@withSignedUrl` transformer directive. It turns an Upload id
|
|
43
|
+
* stored in a field into a signed URL from the row's target, or a `data:`
|
|
44
|
+
* URI when the target is the DB provider. Lookups are batched per request
|
|
45
|
+
* and never fetch inline `data` for object-storage rows.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createWithSignedUrlDirective({ db, targets, expiresIn, disposition, }: WithSignedUrlDirectiveOptions): TransformerDirective;
|
|
48
|
+
export declare const withDataUriSchema: import("graphql").DocumentNode;
|
|
49
|
+
/**
|
|
50
|
+
* Builds the `@withDataUri` transformer directive. It reads the file from
|
|
51
|
+
* whichever target holds it and returns a base64 `data:` URI. Use it for
|
|
52
|
+
* small files only; every resolved field embeds the whole file.
|
|
53
|
+
*/
|
|
54
|
+
export declare function createWithDataUriDirective({ db, targets, }: UploadUrlDirectiveOptions): TransformerDirective;
|
|
55
|
+
//# sourceMappingURL=directives.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"directives.d.ts","sourceRoot":"","sources":["../src/directives.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,oBAAoB,EACpB,kBAAkB,EAEnB,MAAM,yBAAyB,CAAA;AAKhC,OAAO,KAAK,EACV,kBAAkB,EAClB,cAAc,EACd,cAAc,EAEf,MAAM,YAAY,CAAA;AAGnB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAE1D,0EAA0E;AAC1E,eAAO,MAAM,wBAAwB,uBAAuB,CAAA;AAE5D;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,kBAAkB,CAcpB;AAID,qDAAqD;AACrD,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AA8CtD,MAAM,WAAW,kCAAkC;IACjD,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAC7E;AAED,eAAO,MAAM,wBAAwB,gCAMnC,CAAA;AAEF;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAAC,EAChD,MAAM,EACN,iBAAiB,GAClB,EAAE,kCAAkC,GAAG,kBAAkB,CA8CzD;AAED,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,cAAc,CAAA;IAClB,OAAO,EAAE,cAAc,CAAA;CACxB;AAED,MAAM,WAAW,6BAA8B,SAAQ,yBAAyB;IAC9E,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gCAAgC;IAChC,WAAW,CAAC,EAAE,kBAAkB,CAAA;CACjC;AAED,eAAO,MAAM,mBAAmB,gCAM9B,CAAA;AA6BF;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,EAC3C,EAAE,EACF,OAAO,EACP,SAAS,EACT,WAAW,GACZ,EAAE,6BAA6B,GAAG,oBAAoB,CA2BtD;AAED,eAAO,MAAM,iBAAiB,gCAM5B,CAAA;AAEF;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,EAAE,EACF,OAAO,GACR,EAAE,yBAAyB,GAAG,oBAAoB,CAsBlD"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { parse } from "graphql";
|
|
2
|
+
import { getEventHeader } from "@cedarjs/api";
|
|
3
|
+
import {
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
createTransformerDirective,
|
|
6
|
+
createValidatorDirective,
|
|
7
|
+
ForbiddenError
|
|
8
|
+
} from "@cedarjs/graphql-server";
|
|
9
|
+
import { UploadError } from "./errors.js";
|
|
10
|
+
import { toDataUri } from "./serialize.js";
|
|
11
|
+
import { resolveTarget } from "./targets.js";
|
|
12
|
+
import { loadUpload } from "./uploadLoader.js";
|
|
13
|
+
import { UPLOAD_TOKEN_HEADER, verifyUploadToken } from "./uploadToken.js";
|
|
14
|
+
const UPLOAD_TOKEN_CONTEXT_KEY = "uploadTokenPayload";
|
|
15
|
+
function getUploadTokenPayload(context) {
|
|
16
|
+
const payload = context[UPLOAD_TOKEN_CONTEXT_KEY];
|
|
17
|
+
if (!payload) {
|
|
18
|
+
throw new UploadError(
|
|
19
|
+
"INVALID_TOKEN",
|
|
20
|
+
"No validated upload token on the GraphQL context. Add the `@requireUploadToken` directive to this field."
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return payload;
|
|
24
|
+
}
|
|
25
|
+
function isLambdaEvent(value) {
|
|
26
|
+
return typeof value === "object" && value !== null && "headers" in value && typeof value.headers === "object";
|
|
27
|
+
}
|
|
28
|
+
function requestSourceOf(context) {
|
|
29
|
+
if (context.request instanceof Request) {
|
|
30
|
+
return context.request;
|
|
31
|
+
}
|
|
32
|
+
if (isLambdaEvent(context.event)) {
|
|
33
|
+
return context.event;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function currentUserOf(context) {
|
|
38
|
+
const user = context.currentUser;
|
|
39
|
+
if (!user || typeof user !== "object" || !("id" in user)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const id = user.id;
|
|
43
|
+
if (typeof id !== "string" && typeof id !== "number") {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const organizationId = "organizationId" in user && typeof user.organizationId === "string" ? user.organizationId : null;
|
|
47
|
+
return { id, organizationId };
|
|
48
|
+
}
|
|
49
|
+
const requireUploadTokenSchema = parse(`
|
|
50
|
+
"""
|
|
51
|
+
Use to require a valid upload token in the x-upload-token request header.
|
|
52
|
+
The token is bound to the user it was issued to.
|
|
53
|
+
"""
|
|
54
|
+
directive @requireUploadToken on FIELD_DEFINITION
|
|
55
|
+
`);
|
|
56
|
+
function createRequireUploadTokenDirective({
|
|
57
|
+
secret,
|
|
58
|
+
getOrganizationId
|
|
59
|
+
}) {
|
|
60
|
+
const validate = ({ context }) => {
|
|
61
|
+
const source = requestSourceOf(context);
|
|
62
|
+
const header = source ? getEventHeader(source, UPLOAD_TOKEN_HEADER) : null;
|
|
63
|
+
if (!header) {
|
|
64
|
+
throw new AuthenticationError("Missing upload token.");
|
|
65
|
+
}
|
|
66
|
+
let payload;
|
|
67
|
+
try {
|
|
68
|
+
payload = verifyUploadToken(header, { secret });
|
|
69
|
+
} catch (e) {
|
|
70
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
71
|
+
throw new AuthenticationError(message);
|
|
72
|
+
}
|
|
73
|
+
const user = currentUserOf(context);
|
|
74
|
+
if (!user) {
|
|
75
|
+
throw new AuthenticationError(
|
|
76
|
+
"You must be logged in to use an upload token."
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (String(user.id) !== payload.sub) {
|
|
80
|
+
throw new ForbiddenError("Upload token was issued to a different user.");
|
|
81
|
+
}
|
|
82
|
+
const organizationId = getOrganizationId ? getOrganizationId(context) : user.organizationId;
|
|
83
|
+
if (payload.organizationId && payload.organizationId !== organizationId) {
|
|
84
|
+
throw new ForbiddenError(
|
|
85
|
+
"Upload token was issued for a different organization."
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
context[UPLOAD_TOKEN_CONTEXT_KEY] = payload;
|
|
89
|
+
};
|
|
90
|
+
return createValidatorDirective(requireUploadTokenSchema, validate);
|
|
91
|
+
}
|
|
92
|
+
const withSignedUrlSchema = parse(`
|
|
93
|
+
"""
|
|
94
|
+
Use on a field that holds an Upload id to resolve it to a time-limited URL
|
|
95
|
+
for the stored file.
|
|
96
|
+
"""
|
|
97
|
+
directive @withSignedUrl on FIELD_DEFINITION
|
|
98
|
+
`);
|
|
99
|
+
async function loadCompletedUpload(context, db, resolvedValue) {
|
|
100
|
+
if (typeof resolvedValue !== "string" || resolvedValue.length === 0) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const upload = await loadUpload(context, db, resolvedValue);
|
|
104
|
+
if (upload?.status !== "completed") {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return upload;
|
|
108
|
+
}
|
|
109
|
+
async function loadInlineData(db, upload) {
|
|
110
|
+
const row = await db.upload.findUnique({ where: { id: upload.id } });
|
|
111
|
+
return row?.data ?? null;
|
|
112
|
+
}
|
|
113
|
+
function createWithSignedUrlDirective({
|
|
114
|
+
db,
|
|
115
|
+
targets,
|
|
116
|
+
expiresIn,
|
|
117
|
+
disposition
|
|
118
|
+
}) {
|
|
119
|
+
return createTransformerDirective(
|
|
120
|
+
withSignedUrlSchema,
|
|
121
|
+
async ({ context, resolvedValue }) => {
|
|
122
|
+
const upload = await loadCompletedUpload(context, db, resolvedValue);
|
|
123
|
+
if (!upload) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const target = resolveTarget(targets, upload.target);
|
|
127
|
+
if (target.providerType === "db") {
|
|
128
|
+
const data = await loadInlineData(db, upload);
|
|
129
|
+
return data ? toDataUri(upload.mimeType, data) : null;
|
|
130
|
+
}
|
|
131
|
+
if (!upload.storageKey) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return target.getSignedReadUrl(upload.storageKey, {
|
|
135
|
+
expiresIn,
|
|
136
|
+
disposition
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const withDataUriSchema = parse(`
|
|
142
|
+
"""
|
|
143
|
+
Use on a field that holds an Upload id to resolve it to a data: URI of the
|
|
144
|
+
file's bytes. Suitable for small files only.
|
|
145
|
+
"""
|
|
146
|
+
directive @withDataUri on FIELD_DEFINITION
|
|
147
|
+
`);
|
|
148
|
+
function createWithDataUriDirective({
|
|
149
|
+
db,
|
|
150
|
+
targets
|
|
151
|
+
}) {
|
|
152
|
+
return createTransformerDirective(
|
|
153
|
+
withDataUriSchema,
|
|
154
|
+
async ({ context, resolvedValue }) => {
|
|
155
|
+
const upload = await loadCompletedUpload(context, db, resolvedValue);
|
|
156
|
+
if (!upload) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const target = resolveTarget(targets, upload.target);
|
|
160
|
+
const data = target.providerType === "db" ? await loadInlineData(db, upload) : upload.storageKey ? await target.read(upload.storageKey) : null;
|
|
161
|
+
return data ? toDataUri(upload.mimeType, data) : null;
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
export {
|
|
166
|
+
UPLOAD_TOKEN_CONTEXT_KEY,
|
|
167
|
+
createRequireUploadTokenDirective,
|
|
168
|
+
createWithDataUriDirective,
|
|
169
|
+
createWithSignedUrlDirective,
|
|
170
|
+
getUploadTokenPayload,
|
|
171
|
+
requireUploadTokenSchema,
|
|
172
|
+
withDataUriSchema,
|
|
173
|
+
withSignedUrlSchema
|
|
174
|
+
};
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type UploadErrorCode = 'CONFIGURATION' | 'UNKNOWN_TARGET' | 'UNKNOWN_PROFILE' | 'UNAUTHENTICATED' | 'INVALID_TOKEN' | 'TOKEN_USER_MISMATCH' | 'TOKEN_ORGANIZATION_MISMATCH' | 'MIME_TYPE_NOT_ALLOWED' | 'FILE_TOO_LARGE' | 'TOO_MANY_FILES' | 'PRESIGN_NOT_SUPPORTED' | 'NOT_SUPPORTED' | 'NOT_FOUND' | 'FORBIDDEN' | 'NOT_PENDING' | 'NOT_IN_STORAGE' | 'SIZE_MISMATCH' | 'INVALID_KEY';
|
|
2
|
+
/**
|
|
3
|
+
* Thrown by every part of the uploads package when a request cannot be
|
|
4
|
+
* honored. `code` is stable for programmatic handling and `statusCode` is the
|
|
5
|
+
* HTTP status the Fastify routes respond with.
|
|
6
|
+
*/
|
|
7
|
+
export declare class UploadError extends Error {
|
|
8
|
+
code: UploadErrorCode;
|
|
9
|
+
statusCode: number;
|
|
10
|
+
constructor(code: UploadErrorCode, message: string, cause?: unknown);
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GACvB,eAAe,GACf,gBAAgB,GAChB,iBAAiB,GACjB,iBAAiB,GACjB,eAAe,GACf,qBAAqB,GACrB,6BAA6B,GAC7B,uBAAuB,GACvB,gBAAgB,GAChB,gBAAgB,GAChB,uBAAuB,GACvB,eAAe,GACf,WAAW,GACX,WAAW,GACX,aAAa,GACb,gBAAgB,GAChB,eAAe,GACf,aAAa,CAAA;AAuBjB;;;;GAIG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,IAAI,EAAE,eAAe,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;gBAEN,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAMpE"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const STATUS_CODES = {
|
|
2
|
+
CONFIGURATION: 500,
|
|
3
|
+
UNKNOWN_TARGET: 500,
|
|
4
|
+
UNKNOWN_PROFILE: 400,
|
|
5
|
+
UNAUTHENTICATED: 401,
|
|
6
|
+
INVALID_TOKEN: 401,
|
|
7
|
+
TOKEN_USER_MISMATCH: 403,
|
|
8
|
+
TOKEN_ORGANIZATION_MISMATCH: 403,
|
|
9
|
+
MIME_TYPE_NOT_ALLOWED: 415,
|
|
10
|
+
FILE_TOO_LARGE: 413,
|
|
11
|
+
TOO_MANY_FILES: 400,
|
|
12
|
+
PRESIGN_NOT_SUPPORTED: 400,
|
|
13
|
+
NOT_SUPPORTED: 400,
|
|
14
|
+
NOT_FOUND: 404,
|
|
15
|
+
FORBIDDEN: 403,
|
|
16
|
+
NOT_PENDING: 409,
|
|
17
|
+
NOT_IN_STORAGE: 409,
|
|
18
|
+
SIZE_MISMATCH: 409,
|
|
19
|
+
INVALID_KEY: 400
|
|
20
|
+
};
|
|
21
|
+
class UploadError extends Error {
|
|
22
|
+
code;
|
|
23
|
+
statusCode;
|
|
24
|
+
constructor(code, message, cause) {
|
|
25
|
+
super(message, cause === void 0 ? void 0 : { cause });
|
|
26
|
+
this.name = "UploadError";
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.statusCode = STATUS_CODES[code];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
UploadError
|
|
33
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import type { UploadAuthenticator } from '../authenticator.js';
|
|
3
|
+
import type { StorageTargets, UploadDatabase } from '../types.js';
|
|
4
|
+
import type { FetchCertificate } from '../webhooks/sns.js';
|
|
5
|
+
export interface UploadPluginOptions {
|
|
6
|
+
/** Secret upload tokens and serve URLs are signed with. */
|
|
7
|
+
tokenSecret: string;
|
|
8
|
+
/** The app's storage targets. */
|
|
9
|
+
targets: StorageTargets;
|
|
10
|
+
db: UploadDatabase;
|
|
11
|
+
/** Route prefix. Defaults to `/upload`. */
|
|
12
|
+
prefix?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Hard outer ceiling on upload request size. Defaults to 500 MB. The
|
|
15
|
+
* effective bound per request comes from the token (`maxFiles` times
|
|
16
|
+
* `maxFileSize` plus a multipart envelope allowance), so this only needs
|
|
17
|
+
* to sit above the largest profile's aggregate.
|
|
18
|
+
*/
|
|
19
|
+
bodyLimit?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Resolves the requesting user. When configured, the upload routes reject
|
|
22
|
+
* unauthenticated requests and tokens issued to someone else. Build it
|
|
23
|
+
* with `createUploadAuthenticator()`. Without it the token itself is the
|
|
24
|
+
* only identity on the route, so configure it in any app with auth.
|
|
25
|
+
*/
|
|
26
|
+
authenticate?: UploadAuthenticator;
|
|
27
|
+
/**
|
|
28
|
+
* Enables `POST {prefix}/webhook/s3` for S3 event notifications delivered
|
|
29
|
+
* through SNS. `topicArn` is the only topic accepted.
|
|
30
|
+
*/
|
|
31
|
+
s3Webhook?: {
|
|
32
|
+
topicArn: string;
|
|
33
|
+
fetchCertificate?: FetchCertificate;
|
|
34
|
+
fetch?: typeof fetch;
|
|
35
|
+
};
|
|
36
|
+
/** `Cache-Control` for served files. Defaults to `private, max-age=3600`. */
|
|
37
|
+
serveCacheControl?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Registers the upload routes on a Fastify server. Register it on the
|
|
41
|
+
* `server` returned by `createServer()` in `api/src/server.ts`:
|
|
42
|
+
*
|
|
43
|
+
* - `POST {prefix}/fs` accepts multipart uploads for object-storage targets
|
|
44
|
+
* - `GET {prefix}/serve?token=...` serves files behind signed URLs
|
|
45
|
+
* - `POST {prefix}/webhook/s3` receives S3 event notifications (opt-in)
|
|
46
|
+
* - `GET {prefix}/health` answers liveness checks
|
|
47
|
+
*/
|
|
48
|
+
export declare function cedarUploadsPlugin(fastify: FastifyInstance, options: UploadPluginOptions): Promise<void>;
|
|
49
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/fastify/plugin.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAA;AAE5E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAgB9D,OAAO,KAAK,EAEV,cAAc,EACd,cAAc,EAEf,MAAM,aAAa,CAAA;AAIpB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AA2B1D,MAAM,WAAW,mBAAmB;IAClC,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAA;IACnB,iCAAiC;IACjC,OAAO,EAAE,cAAc,CAAA;IACvB,EAAE,EAAE,cAAc,CAAA;IAClB,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,mBAAmB,CAAA;IAClC;;;OAGG;IACH,SAAS,CAAC,EAAE;QACV,QAAQ,EAAE,MAAM,CAAA;QAChB,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;QACnC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;KACrB,CAAA;IACD,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAgCD;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAwQf"}
|